最自然的想法:往下坡走
在上一篇,你學到了地貌的形狀:一個待最小化的函數 f、它指向最陡上升方向的梯度 grad f(x),以及把曲面彎成碗、脊與鞍點的海森矩陣。一階最佳化條件告訴你,極小值只可能落在 grad f(x) = 0 的地方。現在我們想要一個真正能找到這種點的演算法。整個最佳化裡最自然的想法,簡單到幾乎令人不好意思:既然梯度指向上坡,那它的反方向就指向下坡,所以就往那邊走吧。
這短短一句話,就是梯度下降法(也叫最陡下降法)。從你目前的點 x_n,算出梯度,往它的反方向走一段距離,然後重複。寫出來,更新式是 x_{n+1} = x_n - alpha * grad f(x_n),其中正數 alpha 是步長(在機器學習裡叫做學習率)。負梯度是下降方向的標準範例:一個讓 f 變小的方向 d,至少在足夠小的一步內如此,因為沿著它的方向導數 grad f(x)·d 在那裡是負的。
gradient descent, the whole algorithm:
x = x0 # starting guess
repeat:
g = grad_f(x) # the gradient at the current point
if ||g|| < tol: # gradient ~ 0 => near a stationary point, stop
break
x = x - alpha * g # one step downhill
the entire method is that one line x = x - alpha*g , repeated.
the only real decisions are: the starting point x0, and how big alpha is.步該多大?金髮女孩的難題
方向已經定了——直直往下坡——但距離 alpha 是個貨真價實的選擇,也正是梯度下降法挨揍的地方。梯度告訴你「此時此地」哪邊是下坡,卻對「斜坡還能陡多遠」隻字不提。alpha 取太小,每一步幾乎不動;你一吋一吋蹭向極小值,燒掉成千上萬次迭代。alpha 取太大,你可能衝過谷底、落在對面更高的牆上,有時還會愈彈愈高,直到數字炸到無窮大。有一個「剛剛好」的中間值,而把它找出來,就是整個實務上的關鍵。
處理步長有兩個誠實的辦法。最簡單的是把 alpha 固定成某個小常數,然後祈禱——這在機器學習裡很常見,靠試誤來調。有原則的辦法是線搜尋:在每次迭代中,把沿著所選方向看成一個一維問題,挑一個真的能把 f 降得夠多的步。你不需要精確的最佳步(那本身就是一個小小的最小化);你需要一個「夠好」的步。沃爾夫條件把「夠好」說得精確:一條不等式要求 f 確實按斜率有多陡的比例下降(這樣你才不會蝸行),另一條要求斜率已經夠平(這樣你才不會停得太早)。同時滿足兩者的步會被接受,於是方法保證能持續取得進展。
鋸齒:當最陡並不是最短
這幅畫面值得燒進記憶。想像成本曲面是一條又長又窄的山谷——一個塔可餅殼,橫過寬度很陡,沿著長度卻幾乎平坦。極小值遠遠地落在中軸線的下方。現在站在其中一面陡壁上,問梯度該往哪走。它幾乎直直地橫過山谷指向對面那面牆,沿著長度方向只下降一丁點,因為曲面是橫著最陡,而非沿著最陡。於是你橫著跨一步,衝過中軸線、落到對面牆上,而那裡的梯度又指回你來時的方向。結果就是緩慢的鋸齒,在兩壁之間來回彈跳,每彈一次卻只朝真正的極小值多爬一絲絲。
這不是運氣差,也不是程式有錯;它是內建在方法裡的。負梯度只在無窮小的意義上是最陡方向,只在那一個無窮小的瞬間有效。一旦你跨出有限的一步,它就可能大錯特錯,而在窄谷裡它錯得將近九十度。決定它能糟到什麼地步的,是碗的形狀,由海森矩陣的特徵值刻畫:它最大與最小特徵值之比,就是這個問題的條件數。圓碗的條件數接近 1,梯度下降法幾乎直直走進去;又長又細的山谷條件數很大,梯度下降法就鋸齒前進。這正是線性代數那一級裡同一個條件性的概念——一個條件數為 10^4 的問題,把碗拉得比它的寬還長 10^4 倍,那爬行也就相應地殘酷。
用一個最乾淨的例子把它變具體:f(x,y) = (1/2)(x^2 + 100 y^2),它的極小值在原點,條件數恰好是 100/1 = 100。梯度是 (x, 100 y),所以梯度下降的一步把 x 乘上倍數 (1 - alpha)、把 y 乘上倍數 (1 - 100 alpha)。為了不讓陡峭的 y 方向衝過頭而發散,你必須把 alpha 壓在 2/100 = 0.02 以下——但正是這同一個小 alpha,讓平坦的 x 方向每步只縮小 (1 - 0.02) = 0.98,每次迭代只去掉區區 2% 的 x 誤差。陡的方向封死了步長;平的方向接著為此付帳,需要上百步才收斂。就是這一場「最大與最小特徵值之間」的拔河,是整個鋸齒的縮影。
慢到什麼程度?誠實的速率
把成本講精確是值得的,因為「慢」有一條公式。在一個彎得漂亮(凸)的問題上,梯度下降法線性收斂:到極小值的距離每一步乘上一個固定倍數,這個倍數大約是 (cond - 1) / (cond + 1),其中 cond 是海森矩陣的條件數。當 cond 接近 1,這倍數接近 0,你幾步就收斂。當 cond = 100,倍數約 0.98,所以每一步只去掉 2% 的誤差,你需要大約一百步才換到一位準確的數字。當 cond = 10^4,你需要數萬步。這就是「在病態問題上爬行」的量化意義,也是這個方法的頭號弱點。
請注意這裡用詞上的對比,因為它很重要。這裡的「線性收斂」聽起來快,其實是慢的那一檔——誤差每一步縮小一個常數倍,所以正確位數隨迭代次數線性增長。對照下一篇的牛頓法,它用海森矩陣在極小值附近達到二次收斂:正確位數每一步大約翻倍。梯度下降法的收斂速率,正是你「只用一階導數、從不建構海森矩陣」所付出的代價。
兩個誠實的但書,讓這篇不至於變成抹黑文。第一,整個乾淨的故事——單一極小值、保證的下坡長征——倚賴凸性:一個碗狀、沒有假谷底的曲面。在非凸的地貌上(深度學習的常態),梯度下降法可能卡在鞍點,或安頓在一個並非全域最小的局部極小值,上面那個漂亮的速率也就不再成立。第二,它產出的每一個數都是「提早停下」得到的近似值;你在梯度夠小、或步子不再有幫助時停手,而在有限精度的算術裡,一個「為零」的梯度其實是淹沒在捨入誤差裡的梯度,所以存在一個地板,再多迭代也買不到任何東西。
修補那場爬行,以及接下來會發生什麼
一旦你看清鋸齒來自病態,療法就自動圍繞一個目標排好隊:把碗變圓,或者乾脆別跟它的形狀硬碰。最深的療法是動用二階導數的資訊——海森矩陣告訴你碗被拉長了,並據此重新縮放步長,把塔可餅殼變回一個圓。那就是牛頓法及它較便宜的表親,正是下一篇的主題。它們用「建構海森矩陣並求解」的成本,去換一個好得多的方向,以及在答案附近的二次收斂。
但還有一個更便宜、優美得簡單的修補,且仍停留在一階:動量。與其純粹沿著今天的梯度走,你保留一個近期梯度的滑動平均,並沿著它走。在窄谷裡,橫過山谷的分量不停變號而相互抵消,而那個雖小卻一致的、沿著山谷的分量則累積起來——就像一顆彈珠無視左右的抖動,穩穩地沿著槽溝滾下去。涅斯捷羅夫加速梯度法是這個想法的精煉版本,在凸問題上它可被證明把速率從依賴 cond 改善到依賴 sqrt(cond),當 cond 巨大時是一筆大勝。動量正是讓樸素的梯度下降法在現代神經網路的規模下變得實用的東西。