「訓練」一個視覺模型到底是什麼意思
撇開那些神秘感,一個視覺模型其實就是一個函數:你餵進一張圖片,數字在裡面流動,最後從另一端吐出一個預測——比方說,這張圖是貓而不是狗的機率。它特別的地方在於:這個函數內部有極大量可調的數字,叫做權重(weights)或參數(parameters)——常常是好幾百萬個,有時甚至上億個。這些數字的確切數值,就是為什麼有的模型能漂亮地辨認出貓,而有的模型只會吐出一堆亂碼。
一張循環圖:訓練資料流入模型產生預測;預測與標籤比較算出損失;損失驅動權重更新並回饋給模型。
這裡有一個請你記住、整個系列都會用到的比喻。想像一塊巨大的混音台,上面有好幾百萬個旋鈕,每個旋鈕就是一個權重。把旋鈕往某個方向轉,模型「聽起來」就是貓狗不分的一團混亂;往另一個方向轉,乾淨、正確的預測就出來了。訓練,就是去轉那些旋鈕的過程,轉到輸出符合我們想要的樣子為止。沒有人會用手去轉——數量實在太多了——所以我們需要一套自動、有原則的方法,來決定該轉哪一個旋鈕、往哪個方向轉、轉多少。
要明智地轉旋鈕,我們得先有辦法把「模型現在錯得多離譜」量化成一個數字。這個數字就是損失(loss)。損失高,代表模型預測很差;損失低,代表它大致都對。如果我們給模型看一張貓的照片,它卻很有自信地說「狗」,損失就很大;如果它很有把握地說「貓」,損失就很小。整個訓練其實就濃縮成一個任務:把損失變小。
在開始狩獵之前,先誠實地提醒一句:在訓練用的照片上把損失壓得很小,並不是真正的目標。我們要的是模型能認出它從沒見過的貓和狗。一個把訓練照片背起來、卻在新照片上慘敗的模型,得的是過度配適(overfitting)這種病——本篇結尾會再提到它,而後面也有一整篇專門教你怎麼打敗它。現在你只要把這件事放在心底就好:「訓練損失很低」和「真的是個好模型」相關,但並不是同一回事。
損失地形與梯度下降的核心概念
這裡有一個關鍵的視角轉換。損失不是一個固定的數字——它取決於權重。改變權重,損失就跟著變。所以我們可以把損失看成一個權重的函數:餵進一組旋鈕的設定,回傳一個損失值。如果你能把這個函數畫出來,每一種權重的選擇就是一個位置,而損失就是那個位置的「高度」。結果就是一片地形——有山丘、有山脊、有山谷——而我們想抵達一座深谷,也就是我們找得到的最低處。
一個彎曲的損失曲面畫成山谷,一個標記沿著坡度一步步往最低點走下去。
當地形有好幾百萬個維度、我們又看不見它時,要怎麼往下坡走呢?想像一個登山者困在濃霧瀰漫的山上。他看不見山谷,但他能用腳感受到坡度——哪個方向最陡地往上。要往下走,他只要往相反方向踏一步,停下來,再感受一次坡度,再踏一步。這個「最陡上坡的方向」有一個精確的數學名字:梯度(gradient),寫作 ∇L。關鍵在於,梯度指向上坡、指向更大的損失——所以要讓損失變小,我們要往負的梯度方向移動。
梯度下降的更新規則——訓練的心跳。
讓我們非常慢地讀這條——你的第一條方程式——每個符號都有它存在的理由。θ(theta)是一次裝下所有權重的向量;把它想成權重空間裡的一個「位置」,也就是每個旋鈕的完整設定。下標 t 是步數:θ_t 是我們現在站的地方,θ_{t+1} 是踏一步之後站的地方。∇L(θ_t) 是在我們目前位置上計算出來的損失梯度——一個每個權重對應一個分量的向量,每個分量都在說「把這個權重增加一點點,損失會往這個方向變化這麼多」;它們合起來就直直指向上坡。那個負號把上坡翻成下坡——這就是讓我們往下走的關鍵。而 η(eta)就是學習率:一個很小的正數,用來縮放我們步伐的大小。講白一點,這條規則說的是:看看哪個方向最陡地往上,然後往相反方向踏出一步,步幅是 η。具體來說,假設某個權重現在是 2.0,它在這裡的梯度是 +4(這個權重變大時損失會上升),η = 0.1。更新後得到 2.0 − 0.1 × 4 = 1.6——這個權重往下移了,也順帶把損失往下推。把這條規則重複個幾千次,θ 就會一步一步地往下坡漂,漂向一座損失很低的谷。
為什麼是「隨機」?小批次與 SGD
那條乾淨俐落的更新規則藏著一筆代價。「真正的」梯度 ∇L 是定義在整個訓練集上的——要精確算出它,你得把每一張訓練圖片都跑過模型一次,量出全部的損失再取平均。如果有 50,000 張圖片,那就是 50,000 次前向傳遞,只為了往下坡踏一步。現代的資料集動輒上百萬張圖片;每一步都這樣做,訓練會慢到讓人崩潰。我們需要一個捷徑。
這個捷徑簡單得叫人開心:不用整個資料集,而是隨手抓一小把隨機的圖片——一個小批次(mini-batch)——只在這些圖片上算梯度,然後踏步。小批次梯度是真正梯度的一個有雜訊但便宜的近似。因為每一步都用一個隨機樣本,這個方法就叫做隨機(stochastic 就是「隨機」的意思)梯度下降,也就是隨機梯度下降(SGD)。它正是讓「訓練深度網路」這件事根本變得可行的主力。
用小批次估計整個資料集的梯度。
逐符號來看:B 是目前的小批次——這一步我們抽到的那一小把隨機樣本。|B| 是批次大小(batch size),就是裡面有幾個樣本(常見值是 128)。對某個樣本 i,那一對 (x_i, y_i) 是它的圖片和它的真實標籤,而 ℓ(x_i, y_i; θ) 是在目前權重 θ 之下、這單一個樣本上的損失(小寫 ℓ 指一個樣本,大寫 L 指整個集合)。Σ 把整個批次裡每個樣本的梯度加起來,而 1/|B| 把這個總和變成平均。為什麼把隨手一把樣本平均起來,能近似上百萬張圖片的真正梯度?道理跟「民調訪問 1,000 人就能估出整個國家怎麼投票」一樣:一個隨機樣本,平均而言,長得像它所抽出的那個母體。每個樣本的梯度都是對「哪邊是上坡」投的一票;任何單獨一票都有雜訊,但一個公正隨機樣本的平均,是真正方向的不偏估計(unbiased estimate)——平均來說正中靶心,只是一個批次到下一個批次之間會抖動。還有兩個你會一直用到的詞:一次迭代(iteration,或稱步/step)就是一次小批次更新;一個 epoch 是完整掃過整個訓練集一次。它們之間的關係純粹是算術——50,000 張圖片、批次 128,一個 epoch 就是 50000 ÷ 128 = 390.6,向上取整成每個 epoch 391 步(最後一個批次只裝剩下的 50000 − 390×128 = 80 張圖片)。
# One epoch of stochastic gradient descent (pseudocode)
eta = 0.1 # learning rate (the step size)
for x_batch, y_batch in loader: # loader yields ~391 mini-batches
preds = model(x_batch) # forward pass: predictions for this batch
loss = loss_fn(preds, y_batch) # a single number: how wrong we are now
grads = gradient_of(loss, model.weights) # noisy mini-batch gradient
# The update rule: theta <- theta - eta * grad L
for w, g in zip(model.weights, grads):
w -= eta * g # step downhill
# Run this whole loop many times (many epochs) until the loss stops falling.挑選學習率:最重要的一個旋鈕
如果你只調一個東西,那就調學習率 η。它是初學者最常調錯的那個旋鈕,一個糟糕的值可以把一個本來完美的模型整個拖下水。回到霧中的登山者:學習率根本就是他的步伐大小。梯度告訴他哪邊是下坡;η 決定他每次大步跨多遠,才停下來再感受一次坡度。
同一個損失山谷的兩條下降路徑:小而謹慎的步伐慢慢收斂,對比大步伐衝過頭、在山谷兩側來回震盪。
兩個極端都會出問題,只是出在相反的地方。如果 η 太大,每一步都衝過谷底、落在對面坡的半山腰;下一步又衝回來;損失就在原地彈來彈去、安定不下來,或者——更糟——步伐越走越大,直到損失炸到無限大,你的訓練印出 NaN(not a number,「不是數字」,數值溢位的信號)。如果 η 太小,每一步都是膽怯的小碎步:損失確實會下降,但慢到你得等上好幾天,而且搜尋很可能卡在它遇到的第一個淺凹陷裡。真正的功夫,是找出那個「仍然能可靠地往下走」的最大步伐。
- 損失在幾步內就衝上去、劇烈震盪、或變成 NaN → 學習率太高。把它砍成原來的 1/3 到 1/10 再試一次。
- 損失幾乎察覺不到地慢慢往下飄、近乎一條水平線 → 學習率太低(或模型因為別的原因沒在學)。把它調高 3 到 10 倍。
- 損失穩定、相當平順地下降,然後趨於平緩 → 學習率落在健康的範圍。這就是你想看到的。
怎樣才算合理的起始值?這取決於優化器(下一篇會介紹比純 SGD 更高級的),但大致給你一張地圖:純 SGD 用在視覺模型上,常常喜歡 1e-1 到 1e-2 左右;而你接下來會遇到的 Adam 優化器,通常想要小一點,大約 1e-3 到 1e-4。先試一個值,盯著損失的前十幾步,再照上面的檢查清單調整。另外給下一篇先許個諾:我們很少在整段訓練都讓 η 固定不變。通常一開始稍微大一點,好快速進展,然後隨時間把它縮小,讓後期的步伐能溫和地安定進谷裡——這就是學習率排程(schedule),也正是下一篇的主角。
批次大小:速度、記憶體與雜訊
批次大小 |B| 是個比學習率安靜的旋鈕,但它一次牽動三條線。雜訊:較大的批次平均了更多樣本,所以它的梯度估計雜訊較少、更可靠地指向下坡——較小的批次則比較跳。硬體效率:GPU 最愛同時平行處理很多張圖片,所以較大的批次通常代表每秒處理更多圖片,直到你的記憶體爆掉為止。泛化:有趣的是,非常大的批次有時泛化反而稍差(它們容易安定進又尖又脆的谷裡),而小批次額外的雜訊,反倒能幫模型應付沒見過的圖片。並沒有單一「最好的」批次大小——這是一個你要去權衡調整的取捨。
記憶體通常是你最先撞上的硬牆,所以把它講具體。訓練時 GPU 記憶體裡裝著兩種很不一樣的東西。權重(以及優化器為它們做的記帳)佔用的量是固定的,跟批次大小無關——一個兩千五百萬參數的模型,不管批次是 1 還是 1,000,它的權重都佔一樣的空間。而激活值(activations)——網路在通往預測的路上算出的每一張中間特徵圖,為了之後算梯度都必須存起來——則隨批次大小線性增長。所以如果批次 32 張圖片塞滿了,比方說,6 GB 的激活值,那批次 64 就需要約 12 GB、批次 128 約 24 GB,而權重那部分動都不動。這正是為什麼把批次加倍,會突然讓你掉進記憶體不足(out-of-memory)的當機。
線性縮放法則:當你改變批次大小時,跟著等比例調整學習率。
批次大小和學習率其實偷偷綁在一起,而這條經驗法則抓住了這層關係。逐符號來看:|B|_base 是某個你已經找到可用學習率的參考批次大小,η_base 是那個有效的學習率,|B| 是你新選的批次大小,η_new 則是為它而設的起始學習率。比值 |B|/|B|_base 只是在問「我的新批次比原來大幾倍?」,再把 η 乘上同樣的倍數。背後的直覺是:較大的批次給出雜訊較少的梯度,所以你能更信任它、踏出等比例更大的一步。實際算一遍:如果 η_base = 0.1 在 |B|_base = 256 時可用,那麼對 |B| = 512,你會從 η_new = 0.1 × (512/256) = 0.2 開始。兩個關鍵提醒:這是一個經驗法則、一個起點——不是自然律;對非常大的批次它就失靈了。而且在大幅跳升之後,你幾乎一定需要暖身(warmup)——前幾百步先用極小的學習率,再慢慢爬升到 η_new——以免那些剛剛被放大、又位在早期的步伐直接炸掉。暖身和排程,正是下一篇要展開來講的內容。
讀懂訓練曲線:學習的徵兆
整個系列裡最有用的一項技能,就是讀訓練曲線——把損失(或準確率)對步數或 epoch 畫出來的圖。它是你模型的心電圖。最健康的形狀很單純:訓練損失穩定、相當平順地下降,一開始很陡,因為模型學會了那些簡單的樣式,接著變得平緩,因為它在打磨困難的案例,最後在谷底附近趨於水平。如果你看到這個形狀,前面四節的那套機制就是在運作的。
兩條隨 epoch 變化的曲線:訓練損失穩定下降;驗證損失先降後升,兩者之間的差距越來越大。
但光是訓練損失在下降,可能會騙了你。為了看出模型是真的在學,而不只是在背,我們把資料切開:大部分當作模型用來學習的訓練集,另外保留一小片它從不拿來訓練的驗證集(validation set),只用來檢查它在新樣本上的進展。現在同時盯著兩條損失曲線。整個深度學習裡第一個、也是最重要的警訊:訓練損失持續下降,但驗證損失探底後開始往上爬。這個越來越大的差距,意味著模型在背訓練照片,而不是在學貓和狗實際長什麼樣子——這就是過度配適。認出它,是今天這一課的內容;至於治好它的完整工具箱(更多資料、資料增強、正則化、提早停止),在這個系列後面有專篇來講。
- 損失向上爆炸或變成 NaN → 發散。原因:學習率太高(或大批次後沒做暖身)。把 η 調低。
- 損失從一開始就近乎水平 → 沒在學。原因:學習率太低,或資料/模型接線有 bug。先把 η 調高,再檢查整條流程。
- 損失整體往下但非常鋸齒、尖刺 → 雜訊太大。原因:批次太小,或 η 略高。把批次加大,或把 η 稍微調低。
- 訓練損失下降,但驗證損失上升(差距越來越大)→ 過度配適。原因:模型在死背。解法在本系列的第 4 篇。
- 兩條損失一起下降、最後在一個低值附近趨平 → 健康的訓練。你完成了(或可以再往上追求更好)。
退一步,看看你現在手上握有什麼。一個視覺模型是有好幾百萬個旋鈕的函數;損失替它的錯誤程度打分;梯度指向上坡,所以我們往相反方向踏步;我們用隨機小批次便宜地估計那個梯度(SGD);學習率決定我們的步伐大小,而批次大小在雜訊、速度與記憶體之間做取捨;訓練曲線則讓我們一眼看出這一切是否運作正常。這個迴圈——預測、量測、踏步、重複——就是這個系列裡每一個模型底下的引擎。接下來四篇各升級它的一個部分:更聰明的步伐(動量與 Adam)、更健康的梯度(正規化)、能泛化的模型(打敗過度配適),以及一個巨大的起跑優勢(遷移學習)。你現在有了地圖;接著,我們來把這台引擎調得更好。