JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

滑動視窗:卷積到底在做什麼

認識所有視覺模型的核心運算:一個小小的卷積核滑過影像、找出圖樣。

為什麼不能直接把影像攤平

在動手做任何東西之前,先回想一下影像到底是什麼。在先前的學習路線中你已經知道,影像是一個由像素組成的二維網格——一排排、一列列的小方格——每個像素存放一個亮度數值,例如從 0(黑)到 255(白)。一張彩色照片只是把三個這樣的網格疊在一起:一個放紅、一個放綠、一個放藍;這些疊起來的網格就是影像的特徵通道。所以一張不算大的 224×224 彩色影像,其實是三張 224×224 的數字表格:一個高 224、寬 224、深 3 的小數值方塊。

影像是一疊數字網格——每個顏色通道一張網格——而不是一串攤平的數字。

一張放大的照片,露出像素網格,後方並排疊著紅、綠、藍三張網格,每個格子裡都是一個數字。

最直覺的做法,就是動用我們在神經網路基礎裡已經會的工具:全連接(稠密)層——每一條輸入線都連到每一個神經元。但全連接層吃的是一串攤平的數字,而不是一個三維方塊。所以我們得先把影像攤平——把每一列、每一行、每一個通道全部展開成一條長向量。以我們的 224×224×3 影像來說,這條向量有 224 × 224 × 3 ≈ 150,000 個元素。而這正是這個天真計畫崩潰的地方。

\begin{aligned}\text{params} &= (H \times W \times C)\times N\\&= (224 \times 224 \times 3)\times 1000\\&= 150{,}528{,}000 \approx 1.5\times 10^{8}\end{aligned}

把攤平後的影像餵給單一個全連接層所需的權重數量。

我們慢慢把這條式子讀一遍。HW 是影像的高和寬,以像素計(各 224),C 是顏色通道數(RGB 為 3)。把它們相乘,H × W × C,就是攤平後輸入的長度——大約 150,000 個數字。N 是我們在這一層放幾個神經元。每個神經元對它所聽取的每一個輸入都需要一個權重,所以光是一個神經元就已經背著約 150,000 個權重。再疊上不算多的 N = 1000 個神經元,這單單一層就握有約 1.5 億 個權重——而且這還只是層,我們連深層網路都還沒開始疊。記憶體和運算量就這樣爆炸了。

一次一個視窗:卷積運算

解藥來了,而且簡單得驚人。我們不再把一個神經元連到全部 150,000 個像素,而是給它一個小視窗——一個由權重組成的小網格,叫做卷積核(常常只有 3×3)。卷積運算會讓這個視窗在影像上滑動。每停一次,就把卷積核蓋在底下那一小塊像素上,將每個核權重乘上它所覆蓋的像素,再把這些乘積全部加起來,成為單一個輸出數字。接著移動一步,再做一次。這個「滑動—相乘—相加」在整張影像上反覆執行,就是每個視覺模型核心的全部運算。

一個 3×3 卷積核在影像上滑動;每放一次就產生一個輸出值。

示意圖:一個 3×3 的高亮小視窗在較大的像素網格上由左到右、由上到下移動,並有箭頭指向較小輸出網格中的單一格子。

S(i,j)=\sum_{m}\sum_{n} I(i+m,\,j+n)\,K(m,n)

二維互相關:位置 (i, j) 上的輸出值。

我們把每個符號都拆開來看。I 是輸入影像(一個由像素數字組成的網格),K 是卷積核(那一小格權重)。配對 (m, n) 索引的是卷積核內部的位置——對 3×3 的核來說,m 和 n 各自跑遍 0、1、2——所以 K(m, n) 是某一個特定的核權重。配對 (i, j) 是我們正在計算的輸出位置。式中的 I(i+m, j+n) 就是當視窗的角落落在 (i, j) 時,蓋在核格子 (m, n) 底下的那個像素。雙重加總 ∑ₘ∑ₙ 的意思只是「把這些值在整個核視窗上加起來」。把其中一項唸出聲來:當 m = 0、n = 0,我們得到 K(0,0) · I(i, j)——卷積核左上角的權重乘上正下方的那個像素。對 3×3 核的全部九個格子都這麼做,再把九個乘積相加;這一個總和就是 S(i, j)。

# 5x5 grayscale image patch I (one channel, values 0-9)
I = [[3, 1, 2, 0, 4],
     [1, 0, 5, 2, 1],
     [2, 3, 1, 4, 0],
     [0, 1, 2, 1, 3],
     [4, 2, 0, 3, 1]]

# 3x3 kernel K (a small blur-style kernel)
K = [[1, 2, 1],
     [2, 4, 2],
     [1, 2, 1]]

# --- Output position (0,0): K over rows 0-2, cols 0-2 ---
#   3*1 + 1*2 + 2*1
# + 1*2 + 0*4 + 5*2
# + 2*1 + 3*2 + 1*1
# = (3+2+2) + (2+0+10) + (2+6+1) = 28   -> S(0,0) = 28

# --- Slide one step RIGHT, position (0,1): cols 1-3 ---
#   1*1 + 2*2 + 0*1
# + 0*2 + 5*4 + 2*2
# + 3*1 + 1*2 + 4*1
# = (1+4+0) + (0+20+4) + (3+2+4) = 38   -> S(0,1) = 38

# --- Slide one more step RIGHT, position (0,2): cols 2-4 ---
#   2*1 + 0*2 + 4*1
# + 5*2 + 2*4 + 1*2
# + 1*1 + 4*2 + 0*1
# = (2+0+4) + (10+8+2) + (1+8+0) = 35   -> S(0,2) = 35

# First row of the output feature map: [28, 38, 35]
# (continue sliding down to fill the remaining rows)
同一個卷積核的三次擺放,全部手算。每一站都重複使用相同的九個權重。

誠實補上一個命名的小提醒,免得這個詞日後絆倒你。我們剛剛算的——把卷積核原封不動蓋上、相乘、相加——嚴格說來叫做互相關(cross-correlation)。教科書裡的數學卷積幾乎一樣,只是在滑動前會先把卷積核左右翻轉、上下翻轉。為什麼做深度學習的人不在意?因為卷積核裡的數字是學出來的,不是手動選的:如果真的需要翻轉,網路只要直接學出翻轉後的權重就好。所以兩者在實務上可以互換,而大家也就把這個用學習權重的版本通稱為「卷積」。你只要認得互相關這個詞,日後讀論文時看到才不會陌生。

卷積核就是特徵偵測器

我們已經看過卷積核怎麼滑動,但它到底在做什麼?這裡有個最好用的心像:一個卷積核就是一枚小小的橡皮圖章,凡是影像中出現它自己的圖樣之處,它就會「亮起來」。只要把圖章裡的數字選得巧妙,就能讓它對垂直邊緣、模糊區域、或銳利細節起反應。它留下的輸出,就是一張記錄該圖樣出現在何處的地圖。我們看三個經典的手工設計卷積核,把這件事講具體。

(1) 垂直邊緣(Sobel)卷積核一側是正權重、另一側是負權重;它的特徵圖會沿著垂直邊界——物體的左右輪廓——發亮,而在平滑區域則接近零。(2) 均值模糊卷積核就是九個都等於 1/9 的權重;滑過去後,每個像素都被換成鄰居的平均值,所以輸出是一份柔化、暈開的輸入副本。(3) 銳化卷積核則相反:中央一個大正值、周圍是小負值,把一個像素與其周遭的差異放大,讓邊緣更銳利。每一次都是同一個卷積運算——改變的只有卷積核裡的數字。

K_{\text{Sobel}}=\begin{bmatrix}-1&0&1\\-2&0&2\\-1&0&1\end{bmatrix}

垂直邊緣的 Sobel 卷積核。

把這個卷積核一欄一欄地讀。左欄全是負的(−1、−2、−1),中欄是零(0、0、0),右欄全是正的(1、2、1)。所以當我們相乘再相加時,這個核算的是右側亮度減去左側亮度——正是亮度由左到右的變化。中間那一列被加倍(−2 與 2),讓緊鄰中心的像素份量更重,使偵測器更銳利一點。看它在一塊由暗到亮的邊緣上運作,[[0,0,9],[0,0,9],[0,0,9]]:反應是 (−1·0 + 0·0 + 1·9) + (−2·0 + 0·0 + 2·9) + (−1·0 + 0·0 + 1·9) = 9 + 18 + 9 = 36,一個大數字在喊「這裡有邊緣!」。現在餵它一塊處處都是 5 的平坦區塊:(−1·5 + 0 + 1·5) + (−2·5 + 0 + 2·5) + (−1·5 + 0 + 1·5) = 0 + 0 + 0 = 0——正負相抵,於是平滑區域被正確地忽略。記住:這些精確的數字,正是網路若不靠人手、就會自己學出來的東西。

讓 Sobel 卷積核掃過整張照片,它的特徵圖就會描出所有垂直邊緣。

左:一張建築物的照片。右:它的 Sobel 特徵圖,除了窗戶與牆面垂直邊緣處的亮線之外,其餘皆為暗色。

特徵圖:卷積核的作答卡

把我們一直在用的這個詞釘牢。一張特徵圖,就是你把一個卷積核滑過整張輸入後,所得到的完整二維反應陣列——把卷積運算產生的每一個 S(i, j) 值,重新排回成一個網格。輸入一個卷積核,輸出一張特徵圖。如果這個核是邊緣偵測器,那它的特徵圖就是整張影像的一張「邊緣成績單」。

可愛的地方在於,特徵圖本身也只是一個數字網格,所以你可以把它當成一張灰階影像來。這樣讀它:的像素代表「卷積核的圖樣在這裡強烈出現」,的像素代表「這裡沒有那個圖樣」。所以邊緣偵測器的特徵圖大致是暗的,只有在邊緣經過之處才有亮紋。特徵圖一次替每一個位置回答同一個問題——「我的圖樣出現在哪裡?」。

把特徵圖當影像讀:亮=找到圖樣,暗=沒有圖樣。

一張灰階特徵圖,在大致為暗的背景上,以亮塊標出卷積核強烈反應的位置。

一個卷積核給出一張特徵圖,但真正的層會讓許多卷積核並排一起跑——一個調來抓垂直邊緣、一個抓水平邊緣、一個抓某種色塊,依此類推。每一個都產生自己的特徵圖,我們再把它們疊成一個小方塊,就跟我們一開始的紅/綠/藍網格一模一樣。這些疊起來的特徵圖,就成為餵給下一層的特徵通道——這也正是下一篇要打造一個真正的卷積層、並開始堆疊深度的方式。通道進來,更豐富的通道出去。

權重共享與平移等變性

我們來收割成果,與第 1 節那場全連接層的災難正面對照。第一份大獎是權重共享。因為同一個小卷積核掃過整張影像,一個卷積層每個卷積核只握有 K × K 個權重——3×3 的核就是九個數字——不管影像是 224×224 還是 4000×4000 都一樣。拿這個跟單一個全連接層所要求的 1.5 億個權重比比看。而且還內建一個附贈:卷積運算在影像某個角落學到的特徵偵測器,自動在其他每個地方都管用,因為同樣那九個權重在每個位置都會套用。我們只需學「邊緣長什麼樣」一次,而不是每個位置學一次。

第二份大獎名字比較長:平移等變性。它的意思是,如果你平移輸入——把貓往右滑十個像素——特徵圖也會剛好平移同樣的量,它的亮點會跟著貓走。要小心把這個跟不變性(invariance)分開。等變性是指輸出跟著輸入一起移動;不變性是指輸入移動時輸出完全不變。一個俐落的比喻:你的影子是等變的——你往旁邊跨一步,它就剛好跟著滑同樣的距離;牆上的溫度計則是不變的——你在房間裡走來走去,不管站在哪,它讀到的溫度都一樣。卷積給我們的是等變性,不是不變性。

\begin{aligned}\text{equivariance:}\quad & f\big(T_t(x)\big)=T_t\big(f(x)\big)\\[4pt]\text{invariance:}\quad & f\big(T_t(x)\big)=f(x)\end{aligned}

等變性與不變性,並排比較。

我們把符號讀一遍。x 是輸入影像。Tₜ 是一個平移——移動某個量 t(比如往右十個像素)。f 是我們的卷積。最上面那行 f(Tₜ(x)) = Tₜ(f(x)),唸出聲來就是「先平移再卷積,等於先卷積再平移」:不管你是先把影像滑動再跑卷積核,還是先跑卷積核再把答案滑動,得到的結果都一樣。這個等式正是「特徵圖跟著輸入一起移動」的意思。下面那行 f(Tₜ(x)) = f(x) 則是不變性:平移輸入完全不會動到輸出。卷積滿足的是上面那行,而非下面那行——而能體會這兩行之間的差別,正是本節的全部重點。

卷積一開始為什麼會是等變的?因為這個運算在每個位置都一模一樣——同一個卷積核、同樣的相乘相加,在每一站都套用。並不存在某個只活在左上角的特殊「角落偵測器」,所以一個移動的圖樣,只會在它的新位置觸發同樣的反應。對於應該回報特徵在哪裡的早期層來說,這是件實實在在的好事。但遲早我們會想要相反的東西:一個分類器無論貓在左還是在右都該說「貓」——它要的是不變性。連接這兩者的橋樑是池化(pooling),一個之後才出場的運算,它刻意把精確位置模糊掉,用一點等變性換取不變性。那是第三篇的主角;在這裡,你只要牢牢記住一個乾淨的事實:卷積讓它的答案與影像同步移動。