從一個神經元到一整層
上一篇我們造出了最小的學習機器:單一個人工神經元。它接收一個輸入向量 x,用自己專屬的權重 w 與偏置 b 算出加權總和 z = w·x + b,再把這個總和送進激活函數壓縮一下。一個神經元只能對影像問一個近似是非題——「這一個模式吻合嗎?」要辨認像手寫數字這麼豐富的東西,一個問題遠遠不夠。
於是我們做最理所當然的事:把神經元複製很多份。每一份都讀同一個輸入向量 x——同樣那些像素——但各自帶著自己的權重與偏置,因此各自在搜尋不同的模式。別把它想成一個評審團,而是想成一整排並列的評審團:一團打「圓不圓」的分、一團打「中央是不是暗的」、一團打「有沒有直筆畫」。把一群神經元接成全部同時盯著同一個輸入,你就造出了一個全連接層——叫「全連接」是因為每個輸入都餵給每一個神經元。
多層感知器示意圖:左側的輸入節點各自連到下一欄的每一個神經元,呈現輸入向量與一整層神經元之間的全連接。
把這條式子讀成:「對每個神經元同時做一遍上一篇的加權總和,然後壓縮。」這裡 x 是長度為 n_in 的輸入向量(輸入的個數——小例子裡是 2 個像素特徵)。W 是一個有 n_out 列、n_in 行的矩陣;它的第 j 列正好就是上一篇神經元 j 的權重向量 w。乘積 W x 是長度為 n_out 的一串加權總和——每一列做一個內積。b 是長度為 n_out 的偏置向量,替每個神經元加上各自的位移。g 是激活函數,對每個元素獨立作用。結果 h 長度為 n_out,每個神經元一個數。具體地說,2 個輸入、3 個神經元時:x 長度 2,W 是 3×2,W x 得到 3 個總和,加 b 仍是 3 個,h 長度 3。完全沒有新東西——這就是 z = w·x + b 疊三次,矩陣只是替我們把三個內積記好帳。若 x = [1, 2],三列權重為 [0.5, −0.5]、[1, 0]、[−1, 2],偏置為 [0, 1, −3],則原始總和 W x 是 −0.5、1.0、3.0;加上偏置得到 z = [−0.5, 2.0, 0.0];再用 ReLU 壓縮(留下正數、把負數歸零)得到 h = [0, 2.0, 0]——第 2 個神經元發火,其他保持沉默。
import numpy as np
x = np.array([1.0, 2.0]) # input vector, length n_in = 2
# Each ROW of W is one neuron's weights; n_out = 3 neurons
W = np.array([[ 0.5, -0.5], # neuron 1 looks for this pattern
[ 1.0, 0.0], # neuron 2
[-1.0, 2.0]]) # neuron 3 -> W has shape (3, 2)
b = np.array([0.0, 1.0, -3.0]) # one bias per neuron, length 3
z = W @ x + b # 3 weighted sums at once -> [-0.5, 2.0, 0.0]
h = np.maximum(0.0, z) # ReLU, elementwise -> [ 0.0, 2.0, 0.0]
print(z, h)堆疊多層:多層感知器
把這些層疊起來,你就得到一個多層感知器(MLP):若干個全連接層排成一列接起來,前一層的輸出向量成為下一層的輸入向量。第 1 層把像素變成一串「模式分數」;第 2 層把這些分數當成自己的輸入,產生新的一串;如此繼續。某一層既不知道、也不在乎它的輸入是來自另一層還是來自原始像素——對它而言,輸入向量就是輸入向量。
三個你會用一輩子的名字。輸入層就是送進去的原始資料——對我們來說是攤平成一排的像素(它本身不做運算,它就是那張圖)。隱藏層是中間那些層,我們從不把它們的輸出直接當成答案來讀;它們存放網路學到的中間特徵。輸出層是最後一層,我們把它的數字解讀成答案——也就是各類別的分數。「隱藏」只是指「既不是輸入、也不是最終輸出」——埋在中間而已。
多層感知器畫成一欄欄節點:輸入節點、兩欄隱藏節點、一欄輸出節點,每一欄的每個節點都連到下一欄的每個節點。
這裡就是支撐整個深度學習的核心想法。每一層只能用下一層交給它的東西來搭建。所以讀原始像素的第一個隱藏層只能學到簡單的東西——這裡有道亮邊、那裡有塊暗斑。下一層讀這些「邊與斑」的分數,能把它們組合成更豐富的東西——一個角、一段弧、一個圈。再上一層能把圈與筆畫拼成「9 的上半」或「貓的耳朵」。深度建立起一個特徵的階層:複雜的模式被表達成下層較簡單模式的組合。沒有任何單一神經元被告知該找什麼——有用的中間特徵是從訓練中浮現出來的(第 4 篇)。
兩個旋鈕描述一個 MLP 的形狀。深度是層的數目——資料在穿過網路時被重新組合幾次。寬度是一層裡神經元的數目——這一層一次能搜尋多少種不同的模式。兩者都是工程師在訓練前自己定的設計選擇,不是網路學出來的值;我們把這類旋鈕叫做超參數。更寬的層能在同一層級辨認更多模式;更深的網路能搭出更抽象的模式。把它們挑好,一半靠工藝、一半靠實驗。
前向傳播:資料一層一層往前流
前向傳播(又叫一次前向傳遞)就只是把一個輸入依序推過每一層、產生最終輸出這件事。你餵進影像,第 1 層轉換它,第 2 層再轉換那個結果,一路到輸出層交給你分數。關鍵在於:權重與偏置固定時,這個過程是完全決定性的——同一張影像永遠給出同一個輸出。前向傳遞當中不發生學習;學習是另一件事——調整權重——我們在第 4 篇處理。現在權重凍結,我們只看資料流動。
- 從輸入開始:攤平的像素向量成為 h⁰ = x。
- 第 1 層:算 z = W¹h⁰ + b¹,再套上激活函數得到 h¹。
- 第 2 層:把 h¹ 餵進去,用同樣方式算出 h²。
- 一層一層重複——每一層的輸出都成為下一層的輸入。
- 最後一層:它的輸出就是預測——各類別的分數。
別被上標嚇到——括號裡的 (l) 只是表示「屬於第 l 層」。所以 W⁽ˡ⁾ 是第 l 層的權重矩陣,b⁽ˡ⁾ 是它的偏置,g⁽ˡ⁾ 是它的激活函數,h⁽ˡ⁾ 是它輸出的向量。這條式子讀作:要得到第 l 層的輸出,就把前一層的輸出 h⁽ˡ⁻¹⁾ 拿來,過一遍這一層自己的「加權總和加偏置」,再壓縮。它就是第 1 節那條單層規則 h = g(Wx + b),只是輸入現在叫 h⁽ˡ⁻¹⁾(下面那一層)而不是 x。起始條件 h⁽⁰⁾ = x 不過是說「第 0 層就是輸入本身」。對 l = 1, 2, … 一直套到最後一層,最終的 h 就是你的預測。
來看一個值穿過一個迷你 2→2→1 網路。餵入 x = [2, −1]。第 1 層的權重列是 [1, 1] 和 [1, −1],偏置是 [0, −4]:原始總和是 2+(−1)=1 和 2−(−1)=3,加上偏置後 z¹ = [1, −1]。ReLU 留下 1,但把 −1 夾到 0,得到 h¹ = [1, 0]——第二個隱藏神經元對這個輸入是關著的。第 2 層(輸出)權重是 [2, 5]、偏置是 1:它的分數是 2·1 + 5·0 + 1 = 3。所以整段旅程是 [2, −1] → [1, −1] → [1, 0] → 3。注意激活函數確實改變了答案:若沒有 ReLU 的夾擠,輸出會是 2·1 + 5·(−1) + 1 = −2,而不是 3。
import numpy as np
def relu(z):
return np.maximum(0.0, z)
x = np.array([2.0, -1.0]) # h^(0): the input
# ---- Layer 1 (hidden) ----
W1 = np.array([[1.0, 1.0],
[1.0, -1.0]]) # shape (2, 2)
b1 = np.array([0.0, -4.0])
h1 = relu(W1 @ x + b1) # z=[1,-1] -> relu -> [1., 0.]
# ---- Layer 2 (output) ----
W2 = np.array([[2.0, 5.0]]) # shape (1, 2)
b2 = np.array([1.0])
out = W2 @ h1 + b2 # 2*1 + 5*0 + 1 = 3.0 (a raw score)
print(h1, out) # [1. 0.] [3.]選擇壓縮方式:ReLU 與 sigmoid
第 1 篇我們認識了兩種壓縮神經元總和的方式,並答應做一次正式比較。就在這裡。激活函數的工作是把筆直的加權總和彎成有非線性的東西——少了它,正如第 5 節會證明的,整個深層堆疊會塌成什麼都不是。兩個經典選擇:sigmoid 像調光旋鈕,把任何輸入平滑地滑到 0 與 1 之間的某個值;ReLU 像單向閥,擋掉所有負的、讓正的原封不動直接通過。
兩個都讀一遍。ReLU(z) = max(0, z),當 z 為正時輸出 z,z 為負時輸出 0——在原點有個硬折角。sigmoid σ(z) = 1/(1 + e^(−z)) 把任意實數 z 擠進 (0, 1):很大的正 z → 接近 1,很大的負 z → 接近 0,z = 0 → 正好 0.5(因為 e⁰ = 1,所以是 1/2)。等網路開始學習時,最要緊的數字是每個函數的斜率——它的導數,也就是輸入微微移動時輸出變多快。ReLU 的斜率在每個 z > 0 是平平的 1、在每個 z < 0 是 0:正的這一側信號毫不衰減地通過。sigmoid 的斜率在 z = 0 附近最陡(約 0.25),對大的 |z| 則縮向 0——在兩端尾巴上曲線變平。把「斜率」記牢,因為第 4 篇裡網路學習的方式,正是把信號往回傳、乘上的就是這些斜率。
ReLU 函數的圖:x 為負時沿著零水平延伸,x 為正時以 45 度直線上升。
數個激活函數疊在一起的圖,凸顯 S 形 sigmoid 在兩極變平,對比折線狀的 ReLU。
這就是為什麼現代的隱藏層幾乎總是用 ReLU。它便宜到不行(一個 max 而已),而且正側斜率是 1,讓學習信號一層又一層流過去而不縮水——而 sigmoid 會飽和:餵它一個稍大的輸入,它接近 0 的斜率就把信號掐住,這個問題會隨深度累積成第 4 篇要遇到的梯度消失。不過 ReLU 也不是完美:一個對所有樣本輸入都保持負值的神經元會永遠輸出 0、斜率也是 0,於是沒有任何學習信號到得了它——它卡死了。這個 dying ReLU(神經元死亡) 的陷阱是真實存在的,但通常只影響一小部分神經元(而且像「leaky ReLU」這樣的變體可以補救)。sigmoid 仍有它的舞台——在二元分類器的輸出層,那裡我們確實想要一個介於 0 與 1 的數來當機率讀(第 3 篇)。一句話總結:裡面用 ReLU,會飽和的壓縮留給輸出。
非線性為何重要:通用近似定理
我們一再強調激活函數不可或缺。該證明了。假設我們偷懶把激活函數拿掉,於是每一層都是純線性映射——就只是乘一個矩陣、加一個偏置。那麼把兩個這樣的層疊起來,到底算出什麼?
把代數走一遍。第一層把 x 變成 W₁x + b₁。第二層把它乘上 W₂ 再加 b₂,得到 W₂(W₁x + b₁) + b₂。把 W₂ 乘進去、重新分組:含 x 的項收成 (W₂W₁)x,常數項收成 (W₂b₁ + b₂)。但 W₂W₁ 不過是某個矩陣——叫它 W'——而 W₂b₁ + b₂ 不過是某個向量 b'。所以這個兩層堆疊等於 W'x + b':一個單一的線性層。同樣的把戲能把任意多個沒有激活函數的層摺成一個。直覺上,兩個平的映射複合起來還是平的映射——把一張平的紙拉伸一次、再拉伸一次,它還是平的。在層與層之間不楔進一個非線性的 g,深度買到的東西字面上是零。
二維散點圖:兩個交纏的類別之間有一條彎曲邊界穿梭其中,對比一條無法分開它們的直線。
把非線性激活函數放回層與層之間,一切都變了。通用近似定理把這份回報講得精確:一個至少有一個隱藏層、並帶非線性激活函數的 MLP,可以把幾乎任何連續函數近似到你要的任意精度——只要這個隱藏層獲准擁有足夠多的神經元。「任何連續函數」包含那個極其彎曲、把一格格原始像素值對映到「貓」還是「狗」的函數。於是原則上,一疊樸素的神經元,表達力已足以成為一套視覺系統:定理說,對的權重是存在的。