§01學習重點
- 說出「把多個線性層直接疊起來」為什麼等於一個線性層,並親手把一疊線性層併成單一仿射映射
- 解釋非線性激活函數為什麼是深度能生效的前提,而不是可有可無的裝飾
- 用「折疊」與「裁剪」兩種觀點各講一次深度網路怎麼把函數變複雜,並說出兩者各自看不到什麼
- 說明線性區域數在複合之下是相乘而不是相加,並用程式數出一個具體例子的區域數
- 寫出 \(K\) 層網路的矩陣遞推式,並由相鄰兩層的寬度推出每個權重矩陣與偏置向量的尺寸
- 分清超參數與參數,並分別說出寬度、深度、容量三個詞各自在計量什麼
- 在固定參數預算下比較淺與深的線性區域數上界,並說明這個上界為什麼通常達不到
- 指出深度帶來優勢的條件,以及「層數越多越好」這個外推會在哪裡反轉
§02課程內容
一、先確認一件事:疊起來會不會其實是白疊
第 3 章講過的分段線性給了一個很強的結論:一個隱藏層的網路,只要單元夠多,就能把任意一個連續函數逼近到你要的精度。既然一層就夠,為什麼還要疊很多層?
這一問不先回答,本章後面全部懸空。答案的起點是把兩件常被混為一談的事分開:「能不能表示」是一回事,「要花掉多少單元才表示得出來」是另一回事。 前者問的是存在性,後者問的是預算。一層確實「能」,但它可能得花掉多到不划算的單元數。深度要爭的是後面那一項。
不過在比預算之前,得先確認一件更基本的事:把運算層疊起來,本身到底有沒有意義。
一層網路做兩件事。先做一次仿射映射(affine transformation)——乘上一個權重矩陣,再加一個偏置向量;然後把結果逐個元素送進激活函數 \(a[\cdot]\)。假設我們圖個省事,把第二件事拿掉,只留仿射映射,兩層接起來會變成什麼?
逐項拆解:\(\mathbf{x}\) 是輸入向量;\(\boldsymbol\Omega_0\) 是第一層的權重矩陣,\(\mathbf{b}_0\) 是它的偏置向量,兩者合起來就是第一次仿射映射;\(\boldsymbol\Omega_1\) 與 \(\mathbf{b}_1\) 是第二層的同一對東西;\(\mathbf{h}_2\) 是兩層算完之後的結果。中間那個乘法之所以能拆開重組,只用到矩陣乘法對加法的分配律,以及矩陣乘法可以先乘哪一對都行的結合律。
括號一拆,右邊又變回一個仿射映射了——一個新的權重矩陣 \(\boldsymbol\Omega_1\boldsymbol\Omega_0\),一個新的偏置向量 \(\boldsymbol\Omega_1\mathbf{b}_0 + \mathbf{b}_1\)。兩層變一層,中間那一層在數學上根本不存在。
這個併吸可以一路做下去:三層併成兩層,兩層再併成一層,不管你疊幾層,最後都塌回同一個形式。沒有非線性的話,深度這個維度在表達力上完全是零。 這是本章最需要先釘死的一件事——它不是一個技術細節,它是整章動機的存亡點。如果你只記得「所以要加激活函數」,卻沒有算過那個併吸,你就沒有真正相信過「不加會白疊」。
下面這段程式把五層純線性的堆疊跟它的等效單層擺在一起比:
import numpy as np
rng = np.random.default_rng(0)
# 五層「只有線性運算、沒有激活函數」的堆疊
widths = [1, 6, 9, 7, 4, 1]
Ws = [rng.normal(0, 0.6, (widths[k + 1], widths[k]))
for k in range(5)]
bs = [rng.normal(0, 0.4, widths[k + 1]) for k in range(5)]
def stack5(x): # 逐層算,五層都不加 a[·]
h = x
for W, b in zip(Ws, bs):
h = W @ h + b
return h
W_eq, b_eq = np.eye(1), np.zeros(1) # 併吸成一層
for W, b in zip(Ws, bs):
W_eq, b_eq = W @ W_eq, W @ b_eq + b
xs = np.linspace(-3.0, 3.0, 13)
gap = max(abs(stack5(np.array([x]))[0]
- (W_eq @ [x] + b_eq)[0]) for x in xs)
n_par = sum(W.size for W in Ws) + sum(b.size for b in bs)
print("等效權重 =", round(float(W_eq[0, 0]), 6))
print("等效偏置 =", round(float(b_eq[0]), 6))
print("五層 vs 單層,最大差距 =", f"{gap:.2e}")
print("五層的參數量 =", n_par, "/等效單層 =", 2)實跑輸出:等效權重是 0.009275,等效偏置是 0.697087,五層堆疊與單層仿射在十三個測試點上的最大差距是 8.88e-16——那個量級是浮點數的捨入誤差,不是真正的差異。而參數量的對比更難堪:五層總共放了 182 個可調的數字,它們合起來只等於 2 個數字(一個斜率、一個截距)能做到的事。
還有一個更隱蔽的損失值得順帶一提。乘出來的矩陣 \(\boldsymbol\Omega_1\boldsymbol\Omega_0\),它的秩(rank)不可能超過中間那一層的寬度——秩粗略地說就是「這個映射真正能撐開幾個獨立方向」。所以一旦中間夾了一個很窄的線性層,它不但沒有增加表達力,還會把整個映射能撐開的方向數卡死在那個寬度上。純線性的疊層不只是沒好處,用錯了還會扣分。
比喻: 漆器的素胎上刷一道透明的清漆,乾透之後再刷一道,再一道。十道之後你得到的是一片更厚、更亮的表面——但紋樣一個也沒有多。要讓第二道髹塗真的多做成一件事,兩道之間必須先發生某種不可逆的加工:推光、罩染、剔刻,總得有一樣。激活函數在深度網路裡扮演的就是那道加工;沒有它,上漆再多遍也只是同一件事做了很多次。這個比喻在一處明確失準:清漆疊起來至少厚度是真的在累加,物理上確實多了東西;而純線性層疊起來連這點都沒有——上面那段程式裡的 182 個數字合起來只等於 2 個數字,是徹底的歸零,不是效果打折。比喻低估了塌陷的徹底程度。
二、折疊:一條折線再折一次,區域數是相乘的
確定了非線性不可少,接著就要問:加上非線性之後,多一層究竟多做了什麼?
先用最容易看清楚的設定:兩個都只有一個隱藏層、一個輸入一個輸出的網路,前一個叫 \(f\),後一個叫 \(g\),把 \(f\) 的輸出直接餵給 \(g\) 的輸入,得到複合(composition)函數 \(g[f[x]]\)。這裡沒有任何新東西,只是把兩個第 3 章的網路頭尾相接。
關鍵在 \(f\) 的形狀。\(f\) 是一條折線,而折線的每一段斜率有正有負;只要斜率換過號,就會有好幾個不同的 \(x\) 被送到同一個中間值上。這就是多對一映射:從中間值那一端往回看,你分不出來它是從哪一個 \(x\) 來的。
把這件事講得更有畫面一點,就是所謂的折疊觀點:\(f\) 把輸入軸摺回自身,原本攤開的一條線被摺成好幾疊。而 \(g\) 只看得到摺完之後的那個值,它沒有能力分辨腳下疊了幾層。結果就是——\(g\) 的整個形狀,會在 \(f\) 的每一段上各出現一次,其中一些是左右翻過來的,一些被壓縮或拉長,因為每一段的斜率大小和正負都不同。
這裡是全章最容易猜錯的地方。很多人的直覺是「多接一個網路,等於多添幾個折點」,於是猜區域數相加。實際上折點不是被添上去的,是每一段各自複製了一整套,所以是相乘:\(f\) 有 \(p\) 段、\(g\) 有 \(q\) 段,複合之後最多有 \(p \times q\) 段。相加與相乘的差別,在層數變多之後就是多項式與指數的差別。
拿一組具體的函數數給你看:
import numpy as np
def relu(z):
return np.maximum(0.0, z)
def f(x): # 前段:把 [0,1] 折成四段
return (1.0 - 4.0 * relu(0.25 - x)
- (10 / 3) * relu(x - 0.25)
+ (22 / 3) * relu(x - 0.55)
- 9.0 * relu(x - 0.80))
def g(y): # 後段:自己也是一條四段折線
return (0.4 + 1.6 * relu(0.30 - y)
+ 2.2 * relu(y - 0.30)
- 3.3 * relu(y - 0.50)
+ 2.0 * relu(y - 0.85))
def n_regions(x, y, tol=1e-6): # 折點數 + 1
d = np.abs(np.diff(np.diff(y) / np.diff(x)))
return 1 + int(np.count_nonzero(d > tol))
def n_preimages(x, y, t): # 有幾個 x 被送到 t
sgn = np.sign(y - t)
sgn = sgn[sgn != 0]
return int(np.count_nonzero(np.diff(sgn) != 0))
x = np.linspace(0.0, 1.0, 400001)
print("前段 f 的區域數 =", n_regions(x, f(x)))
print("後段 g 的區域數 =", n_regions(x, g(x)))
print("複合 g[f[x]] 的區域數 =", n_regions(x, g(f(x))))
print("相加直覺 =", 4 + 4, "/相乘才對 =", 4 * 4)
for t in (0.35, 0.72):
print(f"中間值 {t} 的原像數 =", n_preimages(x, f(x), t))實跑輸出:前段 \(f\) 的區域數是 4,後段 \(g\) 的區域數是 4,複合之後是 16;相加直覺會給出 8,相乘給出 16,實際數到的是後者。最後兩行是折疊的直接證據——中間值 0.35 和 0.72 各自都有 4 個原像,也就是有四個不同的輸入被 \(f\) 送到同一個地方。
(順帶說明程式怎麼數的:分段線性函數的斜率在每一段內是常數,所以只要在很密的格點上算出相鄰兩點的斜率,再數斜率變了幾次,加一就是區域數。折點有 \(n\) 個,區域就有 \(n+1\) 個——這個「多一」很容易寫錯,特別留意。)
比喻: 剔犀這一類的漆器做法,是先在素胎上一層一層堆出漆層,再用刀在表面剔刻出紋樣;刀鋒切到哪一層,那一層的顏色就露出來。現在想像素胎不是平的,而是先被壓出了四道等高的稜——同一組刀路走過去,紋樣就在四道稜上各出現一次,而且因為稜的坡向有正有反,其中兩次是反過來的。你只做了一組刀路,卻得到四組紋樣,這就是複製效應。這個比喻在一處失準:漆器上那四組紋樣是可以各自再修的,工匠隨時能只動其中一組;而複合函數裡的四份複本是同一批參數同時決定的,你動了後段的任何一個數字,四份會一起變。這個「不能各自調」的限制,正是第六節那句警語的來源。
三、串接不等於一般的兩層網路:外積留下的痕跡
上一節把兩個淺層網路串起來,這其實已經是一個有兩個隱藏層的網路了——只要把參數重新命名一下,形式上完全一樣。但它是一個受限的兩層網路,限制藏在權重矩陣裡,值得攤開看一次。
設前段 \(f\) 的隱藏單元活性是 \(h_1,\dots,h_{D_1}\),沿用第 3 章那套符號,它的輸出是 \(y_1 = b_0 + \sum_d v_d h_d\)(這個 \(b_0\) 是純量,指的是輸出端那個偏置;和第一節那個粗體的 \(\mathbf{b}_0\)——第一層的偏置向量——同字母不同東西,別看混了)。後段 \(g\) 的第 \(j\) 個隱藏單元在進激活函數之前的值是 \(b'_j + w'_j y_1\)(那一撇只是用來跟前段的參數區分開,不是微分記號)。把 \(y_1\) 代進去:
逐項拆解:\(h_d\) 是前段第 \(d\) 個隱藏單元的活性;\(v_d\) 是前段輸出層給它的權重,\(b_0\) 是前段的輸出偏置;\(w'_j\) 是後段第 \(j\) 個單元對輸入的權重,\(b'_j\) 是它的偏置。整理之後,這確實長成「一個隱藏層的活性乘上一組權重再加偏置」的樣子,所以它就是一個兩隱藏層網路。
但看清楚新權重的長相:第 \(j\) 個單元對第 \(d\) 個活性的權重是 \(w'_j v_d\)——一個只跟 \(j\) 有關的數,乘上一個只跟 \(d\) 有關的數。整個權重矩陣是兩個向量的外積(outer product),秩只有 1。
這個限制有多重?用自由度來說最清楚。一般的兩隱藏層網路,這個位置放的是一個 \(D_2 \times D_1\) 的矩陣,\(D_2 \times D_1\) 個數字可以各自獨立地調。串接版只有 \(D_1 + D_2\) 個數字可調(而且其中還有一個共同的尺度可以在兩邊互相抵消,實際的獨立方向再少一個)。假設 \(D_1 = D_2 = 20\),一般版有四百個自由度,串接版只有三十九個。
所以結論是一個嚴格的包含關係:兩個淺層網路串接得到的函數,一般的兩隱藏層網路都做得到;反過來不成立。 深度網路的價值,不在於「把小網路接起來」這個動作本身,而在於它把接起來時被外積綁住的那些自由度全部鬆開。
四、裁剪:新的折點到底從哪裡冒出來
折疊觀點回答了「為什麼是相乘」,但它有個盲點:它說不出那些折點具體落在哪裡。要看清楚這件事,得換一個觀點,直接盯著一個兩隱藏層網路的內部。
先把名詞排好。網路裡每個隱藏單元在被送進激活函數之前的那個值,叫前激活(pre-activation);送進去之後的值叫激活(activation),也就是這個單元的活性。這兩個詞只差一步運算,混用會讓後面完全講不通,請務必分清楚。
於是一個兩隱藏層網路可以拆成三段來讀:
- 第一層前激活:輸入的仿射映射。每個單元一個,過了 \(a[\cdot]\) 之後得到第一層的活性。
- 第二層前激活:第一層各個活性的線性組合。每個單元一個,過了 \(a[\cdot]\) 之後得到第二層的活性。
- 輸出:第二層各個活性的線性組合。
現在看第二步。第二層的每一個前激活,都是第一層那些活性的加權和加上一個偏置。第一層的活性各自在自己的位置上有一個折點;把它們線性組合起來,折點的位置一個都沒有變——線性組合只能改變各段的斜率,不能把折點搬到別處,更不能無中生有。換句話說,第二層所有的前激活共用同一批折點位置,就是第一層那批的聯集。
那新的折點是哪來的?是 \(a[\cdot]\) 給的。以 ReLU 為例,它把負的部分整段壓成零——前激活穿過零的每一個位置,都會多出一個新折點。這一步才是深度真正產生新東西的地方。
而且它同時也會毀掉折點。如果第一層某個折點所在的位置,剛好落在前激活小於零的那一段裡面,裁剪之後那一整段都是平的零,原本的轉折就看不見了。這件事在後面談上界時會變得很重要,先記著。
用一組具體的位置把這兩件事一次看完。設想第一層三個單元的折點分別落在 \(x = 0.30\)、\(0.55\)、\(0.75\),第二層某個前激活是它們的某種線性組合,在這三個位置轉折,並且在 \(x \approx 0.206\) 與 \(x \approx 0.488\) 這兩處由正穿到負、再由負穿回正。裁剪之後會發生三件事:那兩個零交叉各生出一個新折點;\(x = 0.30\) 那個舊折點因為整段被壓成零而消失;於是折點從 3 個變成 4 個,線性區域從 4 個變成 5 個。淨增加只有一個,遠不到「每個單元都貢獻一個新折點」的理想值——這正是理想上界跟實際值拉開差距的機制。
兩個觀點各有各的盲點,合起來才完整:折疊看得到「為什麼是相乘」,看不到折點落在哪裡;裁剪看得到折點落在哪裡,卻看不出那些區域彼此其實是複製品。 遇到不同的問題要換不同的觀點,這是本章想留給你的一個習慣。
最後說一句關於閱讀的事。把上面三段全部代進同一條式子,一個兩隱藏層網路可以寫成一個完整的巢狀表達式:外面一層線性組合,裡面包著一層 \(a[\cdot]\),再裡面又是一層線性組合,再裡面又是一層 \(a[\cdot]\)。這條式子是對的,而且完整——但它幾乎沒有人讀得動,因為下標會多到你光是對齊就要花掉全部注意力。讀不動那條式子不代表你沒學會;真正該學會的是上面那三段的分工。式子難讀是記法的問題,而記法是可以換的,這就是下一節要做的事。
五、記法與計量:矩陣寫法、寬度、深度、容量
下標爆炸是換記法的訊號。把每個權重都寫成 \(\omega_{jd}\)、每個偏置都寫成 \(b_j\),一個三層網路就要管上百個符號;而這些下標其實只在說一件很單純的事:某一層的所有前激活,是上一層所有活性的線性組合。矩陣就是為這句話發明的。
一個有 \(K\) 個隱藏層的深度神經網路(deep neural network)可以寫成一條遞推式:
逐項拆解:\(\mathbf{x}\) 是輸入向量;\(\mathbf{h}_k\) 是第 \(k\) 個隱藏層所有單元的活性排成的向量;\(\boldsymbol\Omega_k\) 是把第 \(k\) 層接到第 \(k+1\) 層的權重矩陣;\(\mathbf{b}_k\) 是對應的偏置向量;\(\mathbf{y}\) 是輸出。最後一行沒有 \(a[\cdot]\),因為輸出層通常不做裁剪——要不要在輸出端加東西,取決於任務要什麼形態的輸出,那是第 5 章的事。
這裡有一個很容易誤讀的地方:\(a[\cdot]\) 作用在一個向量上時,意思是對每個元素各做一次,不是什麼作用在整個向量上的矩陣運算。它不會把不同單元的值混在一起——會混在一起的只有 \(\boldsymbol\Omega_k\)。
(記法上補一句:偏置這個量在不同教材有不同寫法,本課全站統一寫成粗體羅馬的 \(\mathbf{b}\),因為希臘字母那個寫法會跟第 6 章要用的一個係數撞名。你在別的材料上看到別的符號,指的是同一個東西。)
尺寸規則完全由相鄰兩層的寬度決定。若第 \(k\) 層有 \(D_k\) 個單元、第 \(k+1\) 層有 \(D_{k+1}\) 個,那麼 \(\boldsymbol\Omega_k\) 是 \(D_{k+1} \times D_k\) 的矩陣,\(\mathbf{b}_k\) 是長度 \(D_{k+1}\) 的向量。列數看下一層、行數看上一層——這個方向很多人第一次寫會寫反,記住它是為了讓 \(\boldsymbol\Omega_k\mathbf{h}_k\) 這個乘法接得起來就不會錯。整個網路的參數 \(\boldsymbol\phi\) 就是所有 \(\boldsymbol\Omega_k\) 與所有 \(\mathbf{b}_k\) 的集合,沒有別的東西。
用程式把尺寸規則跑一遍最踏實:
import numpy as np
rng = np.random.default_rng(0)
# 輸入 3 維|隱藏層 5、4、6|輸出 2 維
dims = [3, 5, 4, 6, 2]
Omega = [rng.normal(0, 0.5, (dims[k + 1], dims[k]))
for k in range(4)]
b = [rng.normal(0, 0.2, dims[k + 1]) for k in range(4)]
def forward(x):
h = x
for k in range(3): # 三個隱藏層
z = Omega[k] @ h + b[k] # 前激活
h = np.maximum(0.0, z) # a[·] 逐元素作用
return Omega[3] @ h + b[3] # 輸出層不過 a[·]
for k in range(4):
print(f"Omega[{k}] {Omega[k].shape}"
f" b[{k}] {b[k].shape}")
n_par = sum(w.size for w in Omega) + sum(v.size for v in b)
print("參數總數 =", n_par)
print("寬度 =", dims[1:4], "/深度 K = 3",
"/容量 =", sum(dims[1:4]))
x0 = np.array([0.4, -1.2, 0.9])
print("輸出 y =", np.round(forward(x0), 4))實跑輸出:四個權重矩陣的形狀依序是 (5, 3)、(4, 5)、(6, 4)、(2, 6),對應的偏置向量長度是 5、4、6、2;參數總數 88;三個隱藏層的寬度是 [5, 4, 6],深度是 3,隱藏單元總數是 15;輸入 [0.4, -1.2, 0.9] 的輸出是 [0.4559, 0.5192]。你可以驗證每個形狀都符合「列數看下一層、行數看上一層」。
有了尺寸,順便把幾個常被混用的計量詞釘死。
超參數(hyperparameter)與參數的分界:參數就是 \(\boldsymbol\phi\),訓練時由資料決定;超參數是你在訓練開始之前就得選定、而且訓練過程不會去動它的量。層數、每層幾個單元都是超參數。分界的判準很簡單——它是被資料調出來的,還是被你敲定的。
寬度(width)指的是一層裡有幾個隱藏單元。注意這個詞在各層寬度不一致時是有歧義的:上面那個網路的「寬度」是 5、4 還是 6?正確的講法是逐層報,或者說明你指的是最寬的那一層。只有在各層一樣寬時,說「寬度是 \(D\)」才沒有問題。
深度(depth)指的是隱藏層的數目 \(K\),輸入層與輸出層都不算在內。上面那個網路的深度是 3,不是 5。這個口徑要固定,不然跨教材對照時會差兩。
容量(capacity)在本章指的是隱藏單元的總數。它跟參數量是兩回事:上面那個網路容量是 15,參數量是 88。容量長得像「格子數」,參數量才是真正可調的數字個數,兩者不成比例——一個很深很窄的網路和一個很淺很寬的網路,可以容量相同而參數量差好幾倍。
順帶提醒一件第 1 章說過、但這裡最容易復發的事:「深」是在描述結構,不是在標記能力等級。一個 30 層的網路不會因為層數多就比 3 層的「聰明」,它只是換了一種花掉參數預算的方式。
六、淺與深:五條軸上分別比一次
現在可以正面回答開頭那個問題了。淺與深的比較不是一句話能收的,得分開幾條軸各比一次,答案在不同軸上並不一樣。
軸一・能不能表示:打平。 深度網路至少不會比淺層網路差,理由是一個很乾淨的構造。給定任何一個淺層網路,我們拿它當深度網路的第一層,然後把後面每一層的權重矩陣設成單位矩陣、偏置設成零。這時每一層做的事情是 \(a[\mathbf{h}]\)——而 \(\mathbf{h}\) 是上一層 ReLU 的輸出,本來就每個元素都非負,再過一次 ReLU 完全不變。所以後面那些層等於什麼都沒做,整個深度網路的輸出跟原來的淺層網路一模一樣。這叫恆等映射(identity mapping)構造。既然深度網路裝得下任何淺層網路,而第 3 章已經證明淺層網路能逼近任意連續函數,那麼深度網路當然也能。兩邊在「能不能」上是平手,所以差別只可能在「划不划算」。
軸二・每個參數換到幾個線性區域:深大幅領先,但要小心讀。 對單輸入單輸出的情形有兩條可以直接算的通式。一個隱藏層、\(D\) 個單元的淺層網路,參數量是 \(3D+1\)(輸入到隱藏的權重 \(D\) 個、隱藏層偏置 \(D\) 個、隱藏到輸出的權重 \(D\) 個、輸出偏置 1 個),線性區域數最多是 \(D+1\)。\(K\) 個隱藏層、每層 \(D\) 個單元的深度網路,參數量是 \(3D+1+(K-1)D(D+1)\),線性區域數最多是 \((D+1)^K\)。前者對 \(D\) 是線性的,後者對 \(K\) 是指數的——這就是整章的核心對比。
用一組自己挑的設定算給你看,順便把上界跟實際值的落差一起量出來:
import numpy as np
def deep_params(D, K): # K 個隱藏層、每層 D 個單元
return 3 * D + 1 + (K - 1) * D * (D + 1)
def deep_net(x, D, K, rng): # 隨機參數的深度網路
h = x[:, None]
for _ in range(K):
s = 1.4 / np.sqrt(h.shape[1])
W = rng.normal(0, s, (h.shape[1], D))
h = np.maximum(0.0, h @ W + rng.normal(0, 0.5, D))
return h @ rng.normal(0, 1.0, D)
def n_regions(x, y, tol=1e-6):
d = np.abs(np.diff(np.diff(y) / np.diff(x)))
return 1 + int(np.count_nonzero(d > tol))
D, K = 7, 3 # 自選的教學設定
budget = deep_params(D, K)
Ds = (budget - 1) // 3 # 同預算的淺層買得起幾個單元
print("深:參數", budget, "/區域數上界", (D + 1) ** K)
print("淺:單元", Ds, "/參數", 3 * Ds + 1,
"/區域數上界", Ds + 1)
x = np.linspace(-4.0, 4.0, 400001)
counts = [n_regions(x, deep_net(x, D, K,
np.random.default_rng(sd)))
for sd in range(5)]
print("隨機參數實際數到的區域數 =", counts)
print("上界", (D + 1) ** K, "/實際平均",
round(float(np.mean(counts)), 1))實跑輸出:三個隱藏層、每層七個單元的深度網路用掉 134 個參數,區域數上界是 512;同樣的預算給淺層網路,買得起 44 個隱藏單元(用掉 133 個參數),區域數上界是 45。上界的差距超過十倍,而且這個倍數會隨層數繼續拉開。
但接下來那兩行才是這一節真正的重點。隨機給參數的同一個深度網路,實際數到的區域數是 [53, 31, 35, 43, 37],平均只有 39.8——連上界 512 的十分之一都不到,甚至比淺層網路的上界 45 還低。 為什麼?第四節已經先埋好答案了:上界要達到,必須每一層的每一個前激活都在有效範圍內乖乖穿過零;隨機丟參數的時候,很多前激活整段都在零的同一側,一個新折點也沒生出來,反而還吃掉了幾個舊的。上界描述的是這個函數族最好的情況能有多複雜,不是你隨手拿一個成員就有那麼複雜。
還有一個更容易被忽略的限制。那些區域彼此不是獨立的。第二節的折疊觀點已經說明過:後段的形狀在每一疊上各複製一次,複本們由同一批參數同時決定。所以就算某個設定真的長出 512 個線性區域,你能自由調的仍然只有 134 個數字,這些區域之間帶著大量的對稱與依賴。「區域數多」不等於「函數自由度高」,這兩件事的差距,正是深度既強大又受約束的地方。
順著這裡再多說一句,因為這是本章最常見的過度推論:區域數指數成長講的是這個函數族裝得下多複雜的東西,它完全沒有承諾「訓練完之後在新資料上比較準」。表達力與泛化能力是兩個不同的問題,後者要等第 8 章才有工具談。看到指數就興奮,是這一章最典型的踩空。
軸三・什麼時候深真的划算:看真實函數的結構。 上面那個「複本們一起變」的限制,反過來看就是深度的歸納偏好(inductive bias)——深度網路天生擅長的,是本身就帶有重複結構、或本身就是「簡單函數一層層套起來」的那種目標函數。 如果你要學的東西剛好長這樣,深度用很少的參數就能表達它;如果不是,那些被綁在一起的複本就是浪費。理論上這一點有相當強的支持:存在一些函數,深度網路只要多項式量級的單元就能表示,換成淺層網路卻需要指數量級的寬度才行——這叫深度效率(depth efficiency)。構造細節超出本課範圍,但結論的方向很明確:深不是萬用的好,是在對的結構上特別好。
軸四・大型結構化輸入:層數是必要的。 一張影像的意義是從局部堆到全域的——先是邊、再是紋理與零件、最後才是整體。要讓資訊從局部走到全域,中間必須經過足夠多次的匯整,這件事在結構上就需要層數。第 10 章的卷積網路(convolutional network)處理的正是這一類輸入。
軸五・訓練與泛化的經驗觀察:有規律,但會反轉。 在實務上,較深的網路往往比同樣參數量的淺網路更好訓練、也更常在新資料上表現得更好。這是經驗觀察,機制到今天都還沒有完全講清楚,第 20 章會回頭追問。而且它有上限:層數推到某個程度之後,優勢會反過來——梯度在很深的網路裡傳不動,那是第 7 章的主題,而怎麼把層數推更高又不失控,是第 11 章的事。「層數越多越好」是一個會在中途轉彎的外推,不要線性地往下推。
比喻: 回到漆器。一件剔紅要堆上百來道漆層才動刀,因為刀能剔出幾層深淺,取決於底下堆了多少道;漆層數往上加一點,可表現的層次就往上翻一級,而不是等比例地多一點——這就是指數與線性的差別。但兩件事情要一起記住:第一,堆得夠厚不代表紋樣就會好看,那是刀路的事,堆漆只是讓好刀路成為可能;第二,堆過頭之後漆層會開裂,反而毀掉整件。這個比喻在一處失準:漆層的厚度是實打實累加的,工匠每上一道都能量得到;而網路的線性區域數是「這個結構最多能長出多少」的上界,上面那段程式已經量給你看——隨手一組參數只長出四十個左右,離五百多還很遠。可能性與實得之間有落差,這是比喻不會告訴你的部分。
收個尾,而且要收得節制。本章沒有證明「深一定比淺好」,證明的是三件比較窄的事:不加非線性的話疊層完全沒有意義;加了之後,複合會讓線性區域數相乘而不是相加,因此在固定參數預算下深的表達上限遠高於淺;但這個上限是可能性而不是保證,能不能兌現要看目標函數的結構、看訓練得起來、也要看它在新資料上撐不撐得住。後面的每一章,基本上都是在補上這三個條件裡的某一個。
§03原書對照
原書第 4 章只有十五頁,卻同時處理了複合的圖解直觀、代數等價的推導、矩陣記法的建立,以及淺與深的五項比較。本課把敘事順序重排成「先問為什麼要疊、再看疊起來發生什麼事」,因此有幾處原書鋪陳得更細,值得進階讀者翻回去。
第一處是複合的圖解。原書 pp.41–43 用兩個各三個隱藏單元的網路做串接,並在 p.42 配一組四格圖,把第一個網路的輸出範圍、第二個網路的形狀、以及兩者複合後的結果並排比對。p.44 再換成二維輸入重做一次,圖上標出哪一塊區域是平的、複合後各塊又被切成幾塊;同一頁另有一張把「折疊」畫成實體動作的示意圖。本課只用一維把機制講透,二維那一版的視覺說服力更強。
第二處是代數等價的完整代入。原書 pp.43–45 把第二個網路的前激活逐項代入第一個網路的輸出式,再重新命名成新的參數符號,並點出兩層網路比串接版自由在哪裡——串接版的斜率被一組外積結構綁住,一般的兩層網路沒有這個限制。那三頁把每一個下標都寫了出來,想逐行自我校對的人值得一讀。
第三處是裁剪觀點的分解圖。p.46 用四個步驟描述同一個網路怎麼把函數逐步變複雜,p.47 的十格圖再把每一步各畫成一張小圖;p.46 本身還把整個兩層網路壓成一條巢狀式子,並坦白說它難讀——這個坦白對讀者其實比式子更有用。
第四處是矩陣記法的尺寸規則。p.48 的插圖用一組具體的層寬,標出每個權重矩陣與偏差向量的形狀各自由哪兩層決定,頁邊並把矩陣運算的複習指向附錄 B.3;p.49 給出一般 \(K\) 層的遞推寫法與尺寸對照。
第五處是淺深比較的量化。p.50 給出兩種網路在單輸入單輸出下的區域數上界與參數數通式,並提醒區域之間帶有對稱與依賴、數目多不等於自由度高;p.51 的兩張曲線圖把「固定參數預算下深比淺產生更多區域」畫出來,圖上各標一個定位點供讀者感受量級,同頁還有訓練難易與泛化的經驗觀察。
另有兩件容易被翻過去的東西。一是頁邊掛的線上筆記本標記:p.43、p.46、p.49 各對應一份可執行的筆記本,主題依序是網路的複合、ReLU 的裁剪、以及一般形式的深度網路;想親手重畫那幾張圖的人,從那裡入手最省力。二是 p.50 開頭那段把深度網路退回淺層網路的論證——只要讓後半段計算恆等映射,深度網路就退化成一個淺層網路,逼近能力的比較因此一句話就收乾淨;這段論證在原書只佔一段,卻是整個淺深比較的支點。p.52 的章末摘要把全章壓成五條結論,適合當複習檢查表。
最值得追的是註記。pp.52–53 收了五則,依序談「深度學習」一詞的來歷、線性區域數上界的文獻脈絡與一條精確計數公式、通用逼近定理的深度版本、深度效率的構造性結果,以及與之對照的寬度效率。習題在 pp.53–55:一題用 ReLU 的非負齊次性導出權重尺度不變性,另一題要讀者把一個淺層網路與自己複合 \(K\) 次來數區域。原書第 4 章對應印刷頁 pp.41–55。
§04作業和解答
作業一:把一疊線性層併成一層
給定兩個沒有激活函數的線性層:
(a)求出等效單層的權重矩陣與偏置向量,並用輸入 \(\mathbf{x} = (3, -2)\) 驗算兩種算法給同一個答案。(b)這兩層總共放了幾個參數?等效單層需要幾個?(c)如果在兩層之間插入 ReLU,上面的併吸還成立嗎?請說明是哪一個代數性質失效了。
解答 SOLUTION
(a)等效權重是 \(\boldsymbol\Omega_1\boldsymbol\Omega_0\)。第一列:\((1,0,-2)\) 與 \(\boldsymbol\Omega_0\) 的兩行分別內積,得 \(2 - 2 = 0\) 與 \(-1 - 2 = -3\)。第二列:\((-1,2,1)\) 得 \(-2 + 0 + 1 = -1\) 與 \(1 + 6 + 1 = 8\)。所以
驗算 \(\mathbf{x} = (3,-2)\):分兩步算,先得 \(\boldsymbol\Omega_0\mathbf{x} + \mathbf{b}_0 = (9, -8, 1)\),再套第二層得 \((7.5, -25)\)。用等效單層一步算:\((0 \cdot 3 - 3 \cdot (-2) + 1.5,\; -1 \cdot 3 + 8 \cdot (-2) - 6) = (7.5, -25)\)。兩者相同。(以 numpy 重算核對過。)
(b)兩層共 \(6 + 3 + 6 + 2 = 17\) 個參數;等效單層只需要 \(4 + 2 = 6\) 個。多出來的 11 個參數沒有換到任何表達力。
(c)不成立。併吸靠的是「線性映射複合之後還是線性映射」這個封閉性;插入 ReLU 之後,\(a[\boldsymbol\Omega_0\mathbf{x}+\mathbf{b}_0]\) 不再是 \(\mathbf{x}\) 的線性函數,第二層的矩陣就沒辦法穿過它去跟第一層的矩陣相乘。具體地說,失效的是齊次性與可加性——ReLU 雖然滿足對正純量的齊次性,但 \(a[u+v] \neq a[u]+a[v]\)(取 \(u = 1, v = -3\) 即為反例:左邊是 0,右邊是 1)。
作業二:固定參數預算下的深淺取捨
考慮單輸入單輸出的網路,參數預算上限訂為 320 個。用本章第六節的兩條通式回答:(a)在 \(K = 1\)(即淺層網路)、\(K = 2\)、\(K = 3\)、\(K = 5\) 四種深度下,各能買到多大的每層寬度 \(D\)(取不超過預算的最大整數);(b)四者的線性區域數上界各是多少;(c)根據結果,說明「把預算全部拿去加寬」與「拿去加深」的差別,並指出這個比較沒有告訴你什麼。
解答 SOLUTION
(a)(b)用 \(3D+1+(K-1)D(D+1) \le 320\) 逐一試最大的 \(D\),以程式重算核對如下。\(K = 1\):\(D = 106\),用掉 319 個參數,區域數上界 \(D+1 = 107\)。\(K = 2\):\(D = 15\),用掉 286 個參數,上界 \(16^2 = 256\)。\(K = 3\):\(D = 11\),用掉 298 個參數,上界 \(12^3 = 1{,}728\)。\(K = 5\):\(D = 8\),用掉 313 個參數,上界 \(9^5 = 59{,}049\)。
(c)同樣三百二十個參數,全部拿去加寬只換到一百零七個區域;拿去加深,五層就換到將近六萬個,差了五百倍以上。原因在通式的形狀:寬度只讓上界線性成長,深度讓它指數成長。而且注意寬度那一欄的代價——為了多一個區域,就得多付三個參數。
但這個比較有三件事沒告訴你。第一,那是上界——本章第六節的實跑已經看到,隨機參數的網路離上界非常遠。第二,那將近六萬個區域彼此不獨立,你能調的仍然只有三百二十個數字。第三,區域多不代表學得好;把預算全部押在深度上,還要面對第 7 章會講的訓練困難。這一題的用途是建立量級感,不是給你一條選層數的公式。
作業三:用恆等層把淺層網路裝進深度網路
給定一個有三個隱藏單元的淺層網路:\(h_1 = a[x+1]\)、\(h_2 = a[2x-1]\)、\(h_3 = a[-x+0.5]\),輸出 \(y = 0.2 + 1.5h_1 - 2.0h_2 + 0.8h_3\),其中 \(a[\cdot]\) 是 ReLU。請建構一個有三個隱藏層、每層三個單元的深度網路,使它對每一個 \(x\) 都輸出完全相同的值。(a)寫出三個權重矩陣與三個偏置向量;(b)說明為什麼多出來的兩層真的什麼都沒改變——關鍵用到 ReLU 的哪一個性質;(c)這個構造說明了淺與深在表達力上的什麼關係?
解答 SOLUTION
(a)第一層照抄原網路:\(\boldsymbol\Omega_0 = (1,\, 2,\, -1)^{\mathsf{T}}\)(三列一行),\(\mathbf{b}_0 = (1,\, -1,\, 0.5)^{\mathsf{T}}\)。第二層與第三層都取 \(\boldsymbol\Omega_1 = \boldsymbol\Omega_2 = \mathbf{I}_3\)(3×3 單位矩陣)、\(\mathbf{b}_1 = \mathbf{b}_2 = \mathbf{0}\)。輸出層 \(\boldsymbol\Omega_3 = (1.5,\, -2.0,\, 0.8)\)、\(b_3 = 0.2\)。以 numpy 在 \(x \in [-5, 5]\) 上取十萬個點核對,兩者輸出的最大絕對差是 0.0。
(b)第二層做的事是 \(a[\mathbf{I}_3\mathbf{h}_1 + \mathbf{0}] = a[\mathbf{h}_1]\)。而 \(\mathbf{h}_1\) 本身是第一層 ReLU 的輸出,每個元素都非負;ReLU 對非負的輸入是恆等的(\(\max(0, z) = z\) 當 \(z \ge 0\))。所以 \(a[\mathbf{h}_1] = \mathbf{h}_1\),這一層原封不動地把值傳下去,第三層同理。關鍵性質就是「ReLU 在非負區間上是恆等映射」——換成一個在非負區間上會壓縮的激活函數(例如把大值壓向 1 的那類),這個構造就不成立了。
(c)它說明深度網路的函數族包含同寬度的淺層網路能表達的一切,所以深度絕不會讓表達力變差。配合第 3 章已經確立的「淺層網路能逼近任意連續函數」,兩邊在逼近能力上是平手。順帶一提,這個構造也解釋了「加層數為什麼理論上不該變糟」——雖然實際訓練時未必找得到這組恆等參數,那是最佳化的問題,不是表達力的問題。
§05參考資料
- 3Blue1Brown:線性代數的本質 — 用動畫講「矩陣乘法就是映射的複合」,正是本章第一節塌陷論證的幾何版本
- On the Number of Linear Regions of Deep Neural Networks(arXiv 1402.1869) — 線性區域計數的代表性文獻,本章只給了單輸入的通式,這篇處理一般維度
- Benefits of depth in neural networks(arXiv 1602.04485) — 深度效率的構造性結果:某些函數淺層要指數寬度,深層只要多項式
- TensorFlow Playground — 在瀏覽器裡直接加減層數與單元數,看決策邊界怎麼變,配第六節服用
- Deep Learning(Goodfellow 等)第 6 章:深度前饋網路 — 另一本標準教材對同一主題的寫法,記法與本課略有出入,正好用來練習對照
- CS231n:神經網路基礎 — 史丹佛課程講義,對激活函數的選擇與層數的實務取捨講得很直白
- NumPy 官方使用手冊 — 本章四段程式只依賴 numpy,矩陣形狀與廣播規則查這裡
- Understanding Deep Learning(MIT Press) — 本課課綱主題所本的原書出版頁(ISBN 9780262048644,2023-12 出版)
- udlbook 官方網站(作者釋出的 PDF、投影片與習題) — 原書作者維護的免費資源站(udlbook.com 會轉址到此)