為「一堆數字」準備的四個詞
如果「線性代數」這個詞讓你肩膀一緊,那就放鬆下來吧。本篇裡幾乎所有內容,都不過是把數字裝進盒子,再約定好搬動它們的規則而已。機器學習背後的數學,大體上就是這件事,只是在極大的盒子上飛快地重複。我們先給這些盒子起名字:純量、向量、矩陣、張量這四個名字,說穿了就是在回答一句話——「我手上的數字有幾個方向?」
純量(scalar)就是單獨一個數,孤零零的:21 度的氣溫、4.99 的價格、一個學習率。向量(vector)是按固定順序排好的一串數,比如 `[21, 4.99, 0]`——一個帶有幾個座標的點。矩陣(matrix)是一格一格排成列與欄的數字網格,像一張試算表。而張量(tensor)只是個總稱,指「任意維數的數字陣列」——純量是 0 維張量、向量是 1 維張量、矩陣是 2 維張量,往上還能繼續堆。
來看一個具體的階梯。一個灰階像素是純量。一列像素是向量。一整張灰階圖是矩陣。彩色影像再加上紅、綠、藍三個通道——它就成了 3 維張量。而你一次餵給模型的一批 64 張彩色圖,就是 4 維張量。這裡沒發生什麼神祕的事;你只是不斷地「再加一個可以沿著數下去的方向」罷了。
向量既是一支箭,也是一個地址
同時握住關於向量的兩幅圖,會很有幫助。第一幅圖:向量是一支從原點指向空間中某一點的箭頭——`[3, 2]` 就是「向右走 3,向上走 2」。第二幅圖:向量是一個地址,是一串能精確釘住唯一一個位置的座標。它們是同一件事的兩種看法,哪幅圖更順手取決於當下。箭頭讓方向與長度變得真切;地址則讓它成為電腦能存下的東西。
兩個日常操作幾乎是白送的。相加兩個向量,就是逐格相加:`[3, 2] + [1, 4] = [4, 6]`——把第二支箭的尾巴接到第一支箭的頭上,看你最後落在哪裡。用一個數去縮放,會把整支箭拉長或縮短:`2 × [3, 2] = [6, 4]`,方向不變,長度翻倍。箭頭的長度有它自己的名字,叫向量範數(norm),它回答的是「這東西有多大?」——每當你要衡量一個預測偏離了多遠時,都會再遇到這個問題。
下面這一躍,正是向量對機器學習至關重要的原因:沒有任何規定說你必須止步於 2 個或 3 個方向。一個向量可以活在 10 維、768 維,乃至 4096 維裡。你沒法在腦中想像一支 768 維的箭,這沒關係——沒人能。你保留那些規則(逐格相加、每格縮放、測量長度),而悄悄丟掉那幅心像。正是這一步,讓同樣樸素的算術能去描述一個點、一句話,或一張臉。
點積:兩樣東西有多「合拍」?
兩個向量之間最重要的一個操作,就是點積(dot product)。它的配方簡單到近乎讓人小看:把對應的格子相乘,再全部加起來。對於 `[1, 2, 3]` 和 `[4, 0, 5]`,就是 `1×4 + 2×0 + 3×5 = 4 + 0 + 15 = 19`。兩個向量進去,一個純量出來。正是這種「坍縮」——把許多數字壓成一個——讓它無處不在。
可這一個數到底意味著什麼?把它想成一個「合拍程度」的分數。當兩支箭指向同一方向時,它們的點積又大又正。當它們成直角——徹底毫不相關——時,點積為零。當它們指向相反方向時,點積變負。於是點積悄悄衡量的是:「這兩個向量在多大程度上朝同一方向使勁,並按各自的長度加權?」這正是為什麼它會化身為「相似度分數」,出現在你的搜尋詞嵌入向量(embedding)與每篇文件的嵌入向量之間,或兩個詞義之間。
這也是單個人工神經元的心跳。一個神經元把它的輸入當作一個向量、把它的權重(weight)當作另一個向量,取兩者的點積,得到的這一個數就是神經元在決定要不要「激活」之前的原始反應。每當你聽說某個網路在做「數十億次乘加運算」,它做的正是這件事——成卡車成卡車的點積。把這一個操作吃透,後面相當大一部分內容就不再嚇人了。
def dot(a, b):
total = 0
for i in range(len(a)): # walk both lists together
total += a[i] * b[i] # multiply matching slots, add up
return total
dot([1, 2, 3], [4, 0, 5]) # -> 19 (one number out)矩陣乘法,無非是許多個點積
如果說點積是兩個向量之間的一次握手,那麼矩陣乘法(matrix multiplication)就是一整屋子的握手一氣呵成。要把兩個矩陣相乘,你把第一個矩陣的每一列(row),與第二個矩陣的每一欄(column)做點積。每一個這樣的點積,就成為結果中的一個元素。這就是全部規則——底下沒有藏著別的魔法。
有一條禮儀規則,幾乎絆倒每一個初學者:內側的尺寸必須相等。一個 3×2 的矩陣,只能去乘一個有 2 列(row)的矩陣,因為第一個矩陣的每一列(長度為 2)必須與第二個矩陣的每一欄(長度為 2)逐格對齊。結果的尺寸則取外側的兩個數:3×2 乘以 2×4,得到 3×4。如果那兩個內側的數對不上,乘法就根本沒有定義——而「形狀不匹配」的報錯,將是你在這個領域裡最常撞見的一個 bug。
理解矩陣乘法更深一層的方式,是把它看作一種變換(transformation):矩陣是一台機器,吃進一個向量,吐出一個(通常不一樣的)向量。餵給它一個點,它就把這個點旋轉、拉伸、壓扁,或投影到一個新位置。神經網路裡的一整層做的恰恰是這件事——它用一個權重矩陣去乘你的輸入向量,把這些輸入重新組合成一組新的特徵,其中每一個輸出,都是全部輸入各自的一份加權混合。矩陣乘法,本質上就是「按選定的比例把東西摻和到一起」這個操作。
為什麼一切都變成了向量
模型沒法直接拿「貓」這個詞或一張日落照片做任何事。它只懂得把數字相乘、相加。所以第一件事永遠是:把那個雜亂的真實事物,變成一個數字向量——一串模型能在上面運算的列表。你在前面的階梯裡已經認識了特徵(feature)這個概念:向量的每一格,都是這個事物的一項可測量的屬性。一座房子可能變成 `[面積, 臥室數, 屋齡, 到學校距離]`;就數學而言,這一個向量就是這座房子。
而美妙的回報是幾何性的。一旦一切都成了向量,「相似的東西」就變成「彼此挨得很近的點」,「這個和那個不一樣」就變成「這些箭指向不同的方向」——而你現在知道,這不過就是個點積。學習得到的嵌入向量對詞語和影像做的正是這件事:它把它們安放進一個高維空間,使得「意義」化為了「距離與方向」。搜尋、推薦、聚類、比對,最後全都歸結為在向量之間做測量。
把工具箱合起來
讓我們追蹤一次極小的「模型走一遍」,看看這些零件如何咬合到一起。無論你跑的是線性迴歸,還是一個龐大的網路,它的形狀都一樣,只是規模不同。
- 把輸入編碼成一個向量——每一格都是這個樣本的一項特徵(那座房子、那塊像素、那個詞)。
- 用一個權重矩陣去乘這個向量——每個輸出對應一個點積,把輸入重新組合成新的特徵。
- 把得到的結果向量讀作模型的回答——一個分數、一個預測,或是送給下一層的輸入。
- 把許多個這樣的步驟疊起來,你就得到了一個深層網路;矩陣裡的那些數字,正是訓練所要學習的東西。
這真的就是你讀懂這條階梯餘下部分所需的、絕大部分的線性代數了。資料變成向量;向量被排進矩陣與張量;點積衡量合拍程度;矩陣乘法負責重組與變換;而一個模型,就是這些乘法疊成的高高一摞,其中的數字由訓練來調校。要跟上後面的內容,你不需要證明,也不需要什麼巧妙的技巧——你需要的是這寥寥幾招,鬆鬆地握著,常常地用著。