先讀完全部資料,問題出在哪
上一篇你學了普通的梯度下降:站在損失曲面上的某處,量出哪個方向是下坡,邁一步,再重複。但在「量出哪個方向是下坡」裡悄悄藏著一個假設。要算出損失的真實梯度,你必須在每一個訓練樣本上都跑一遍模型、把所有誤差加起來,然後才能動一步。這叫做全批量(full-batch)梯度下降。
如果是幾百列的教科書式小資料集,這沒問題。可一旦換成真實資料——上百萬張圖片、幾十億個詞——那就是災難。你得把整個資料集啃一遍,僅僅為了往下坡邁一步,而模型需要成千上萬步。更糟的是,整個資料集往往根本無法一次性塞進記憶體。實話實說:全批量下降在數學上很乾淨,但在大規模場景下實際不可用。
突破口就在這裡。真實梯度不過是每個樣本各自梯度的平均值。而要估計一個平均值,你並不需要問遍所有成員——抽樣問一部分就夠了。隨機挑一百個樣本問「哪邊是下坡?」,它們的平均答案與全體人群的答案方向大致相同,可計算成本只有原來的萬分之一。正是這一個想法,才讓訓練大模型成為可能。
隨機梯度下降:步子勤,看得少
這種取樣思路最極端的版本,就是[[stochastic-gradient-descent|隨機梯度下降]](SGD)。你不再用整個資料集算出一個巨大的平均步,而是隨手抓一個隨機樣本、算出它的梯度、立刻邁步;然後再抓下一個,再邁一步。「隨機」(stochastic)這個詞就是「隨意、帶運氣」的意思——每一步都基於一個隨機挑出的資料點,而不是全局畫面。
你可以把全批量梯度想成:動身之前先對全體人口做一次細緻普查;而 SGD 則像隨便攔住一個路人、立刻照他的回答行動。任何單個回答都可能出錯,甚至指向上坡。但在大量快速的步子裡,這些誤差大體相互抵消,而你每分鐘取得的進展反而多得多,因為每一步都極其廉價。你是在用「每一步的品質」去換「步子的數量」。
SGD 走出的軌跡不是沿著山谷平滑滑下去的——它會左右鋸齒擺動、抖動,偶爾還短暫往上爬。這種抖動不是一個要被消滅的缺陷;正如我們下面會看到的,它其實能幫上忙。你之前認識的學習率在這裡仍然控制步長,而且在這裡更要緊:方向本就這麼嘈雜,學習率一旦太大,抖動就會失控放大。
小批量:處在中間的甜蜜點
純粹「一次一個」的 SGD 也有自己的毛病:它在現代硬體上很浪費。一塊 GPU 天生就是為成千上萬次並行乘法而造的,而每次只餵它一個樣本,會讓它幾乎全部算力閒置。所以實踐中沒人真用大小恰好為 1 的批量。我們折中處理:把一小組樣本放在一起算。這一組就叫[[mini-batch|小批量]](mini-batch)。
你在比如 32 個或 256 個樣本上求平均梯度,然後邁一步。今天人們口中說的「SGD」,幾乎都暗指的是這個版本。它處在兩個極端之間:比單樣本 SGD 更穩定,因為在 32 個點上平均能抵消掉更多雜訊;但又遠比全批量便宜、快得多。整場博弈,就是要決定這個旋鈕該停在哪個位置。
輪次與迭代:把工作量數清楚
有兩個詞總讓人栽跟頭,我們把它們徹底釘死。一次[[epoch-vs-iteration|迭代]](iteration,也叫一步 step 或一次更新 update)指的是:處理一個小批量、邁出一步。一個輪次(epoch)則是把整個訓練集完整掃過一遍——每個樣本恰好被看過一次。它們數的是不同的東西:迭代數的是「步數」,輪次數的是「在資料上走了幾趟」。
兩者之間的關係不過是一道除法。如果你有 10000 個訓練樣本、批量大小為 100,那麼一個輪次就是 10000 / 100 = 100 次迭代。訓練 20 個輪次,總共就邁了 2000 步。注意這意味著什麼:把批量調小,每個輪次裡的步數就更多——從同一批資料裡學習的機會更多——這也是為什麼小批量常常在「每個輪次」的尺度上學得更快。
shuffle(training_data) # reshuffle each epoch
for epoch in range(num_epochs):
for batch in split(training_data, batch_size):
g = average_gradient(loss, batch) # one mini-batch
weights = weights - learning_rate * g # one iteration
# iterations_per_epoch = dataset_size / batch_size為什麼雜訊其實幫了忙
一個更嘈雜、更草率的梯度居然能贏過一個精確的梯度,這聽起來很反直覺。但深度網路的損失曲面佈滿了陷阱——淺淺的凹坑和平坦的鞍點區域,完美光滑的全批量下降一旦走進去就可能卡住,因為在這種點上精確梯度幾乎為零,明顯的去處一個也沒有。而 SGD 不那麼容易被困住。
這種隨機性就像一種溫和而持續的輕推。當小批量下降落進一個淺陷阱裡,下一個帶雜訊的梯度很可能恰好「錯得夠多」,把它一腳踹出來,讓它繼續去尋找更深、更好的山谷。雜訊是一種內建的探索來源——而且多少有點神奇的是,它往往會把訓練引向「平坦」的極小值,也就是那些寬闊的盆地,它們通常比狹窄而脆弱的極小值更能泛化到新資料上。
不過要小心:這是一種傾向,不是保證,而且學界對「平坦極小值為什麼更能泛化」究竟原因何在,至今仍有爭論。也別把「雜訊有幫助」過度解讀成「雜訊越多越好」——雜訊太多(極小的批量配上很高的學習率)只會讓訓練劇烈亂晃、永遠安定不下來。雜訊是一味有用的調料,不是主菜。
速度與穩定的取捨:選一個批量大小
現在來看那個核心旋鈕。小批量意味著梯度嘈雜、抖動,但步子廉價而快、還帶來大量有益的探索——只是它沒把 GPU 餵飽,而且可能需要更小的學習率才穩得住。大批量意味著梯度平滑、準確、硬體被充分利用,但每一步代價更高、總步數更少,還有滑進一個尖銳極小值、泛化變差的風險。兩端都不是免費的。
還有一道與理論無關的硬性天花板:一個小批量必須能和模型本身及其中間激活值一起塞進 GPU 記憶體。在很多真實實踐中,決定批量大小上限的是這道「記憶體牆」,而不是數學。一條常見的經驗法則:當你增大批量時,通常可以把學習率大致成比例地一起調大,因為梯度雜訊變小了,你就能更大膽地邁步。
那到底怎麼選?老實說,批量大小是一個超參數——並不存在一個放之四海皆準的正確值,32 到 512 已能涵蓋大多數情形。挑一個你的硬體喜歡的值,盯著學習曲線看,再去調整。還要記住本篇的邊界:SGD 只告訴你*方向和步長*。至於現代那些精巧的技巧——*在什麼時候該邁多大一步*,比如動量、Adam、學習率排程——是下一篇的主題,那台引擎將在那裡真正活起來。