
很多人剛開始學(xué)AI以為最難啃的是微積分??烧娴綄懘a的時候會發(fā)現(xiàn)最先攔住你的往往是線性代數(shù)——向量、矩陣、張量這些看起來像數(shù)學(xué)課的“抽象概念”其實是每一個模型骨架里的“語法規(guī)則”。這篇進階篇數(shù)學(xué)篇第6講就專門把這些計算掰開揉碎向量、矩陣、張量在AI里到底怎么運算、怎么用以及你寫模型時踩過的那些形狀錯誤、廣播警告根源在哪里。適合已經(jīng)會用框架搭模型、但數(shù)學(xué)細節(jié)一直似懂非懂的同學(xué)也適合準備面試前臨時抱佛腳的人。1. 為什么說向量、矩陣、張量是 AI 的“計算語法”1.1 從單個數(shù)字到高維容器標量、向量、矩陣、張量一個普通的數(shù)字在數(shù)學(xué)里叫標量在代碼里就是一個float或int。它沒有任何方向也沒有“批量”的概念。但AI模型每次推理都要處理成千上萬個樣本每個樣本又包含幾百上千個特征如果還一個數(shù)字一個數(shù)字地管理模型根本沒法寫。于是有了向量一串數(shù)字排成一行或一列通常表示一個樣本的特征。比如某個用戶有年齡、收入、活躍度三個數(shù)值就可以用一個三維向量[28, 15000, 0.8]表示。矩陣則是二維的像一個表格。它的第一個維度常常是“樣本數(shù)”第二個維度是“特征數(shù)”。一個包含100個用戶、每人3個特征的輸入就是一個形狀為(100, 3)的矩陣。張量的定義更寬泛它是任意維度的數(shù)組。三維張量可以表示視頻或圖像序列四維張量常用來表示一批圖片五維、六維也偶有出現(xiàn)。對AI工程師來說張量就是數(shù)據(jù)的通用容器它的每一維都有一個名字比如batch_size、channel、height、width。搞定了維度命名張量就不可怕了。數(shù)據(jù)類型維度常見形狀例子AI中的典型對象標量0維()或[]損失值、學(xué)習(xí)率向量1維(3,)單個樣本的特征向量、詞向量矩陣2維(100, 3)一批樣本的特征表、單個線性層的權(quán)重張量3維及以上(16, 3, 224, 224)一批圖片、注意力分數(shù)矩陣、序列批次1.2 “計算語法”到底指什么把數(shù)據(jù)裝進張量只是第一步真正的關(guān)鍵是運算規(guī)則。同樣是一個三維數(shù)組你可以把它當成“一批2D圖片”也可以當成“一條序列的隱藏狀態(tài)”這決定了維度怎么對位、乘法怎么執(zhí)行。我常把向量、矩陣、張量的運算比作英語的語法單詞是數(shù)據(jù)語法是數(shù)據(jù)之間的組合規(guī)則。你背了再多單詞不知道主謂賓順序也說不出完整句子。深度學(xué)習(xí)框架里torch.matmul、torch.reshape、torch.transpose這些函數(shù)就是語法規(guī)則而forward函數(shù)本質(zhì)上就是把這些規(guī)則按一定順序串起來描述數(shù)據(jù)如何從輸入變成輸出。而且GPU 芯片內(nèi)部對矩陣乘法和張量搬運做了大量定制優(yōu)化模型訓(xùn)練的速度瓶頸往往不在計算本身而在于你寫的張量運算是否貼合底層硬件習(xí)慣。后面我會專門講內(nèi)存布局和廣播問題這些看著不起眼卻直接影響訓(xùn)練速度和顯存占用。2. 向量運算點積、范數(shù)和相似度計算到底在算些什么2.1 點積AI里出場率最高的親密關(guān)系度量向量點積的算法很簡單對應(yīng)元素相乘再把所有乘積加起來。寫成公式就是a · b a1*b1 a2*b2 ... an*bn。在NumPy里一行代碼搞定import numpy as np a np.array([1.0, 2.0, 3.0]) b np.array([2.0, 0.0, 1.0]) dot np.dot(a, b) # 1*2 2*0 3*1 5點積的幾何意義是“一個向量在另一個向量方向上的投影長度”乘以“另一個向量的長度”。所以它天然適合度量兩個向量的相似度方向越一致點積越大方向垂直時點積等于0方向相反時點積為負。這在AI里太常用了。自注意力機制的分數(shù)計算就是Q和K的點積除以維度的平方根推薦系統(tǒng)算用戶向量和物品向量的匹配度用的也是點積或余弦相似度文本檢索里計算查詢向量和文檔向量的相關(guān)性本質(zhì)還是點積??梢哉f大部分模型的“相關(guān)性”概念都是用點積定義的。2.2 范數(shù)向量長度是正則化和歸一化的底層邏輯范數(shù)用來衡量向量的大小。最常見的L2范數(shù)就是向量的歐幾里得長度公式是sqrt(a1^2 a2^2 ... an^2)。L1范數(shù)則是絕對值之和。L2范數(shù)在訓(xùn)練里最出名的應(yīng)用是權(quán)重衰減也就是給損失函數(shù)加上權(quán)重矩陣的L2范數(shù)平方逼著參數(shù)不要長得太大。L1范數(shù)則常用來做稀疏化讓一部分特征權(quán)重變成0相當于自動做特征選擇。歸一化也是靠范數(shù)完成的。比如把向量a除以它的L2范數(shù)得到單位向量。兩個單位向量再做點積結(jié)果就是余弦相似度數(shù)值穩(wěn)定在 -1 到 1 之間。很多向量召回模型在輸出層之后都會加一個L2歸一化目的就是讓點積結(jié)果只跟方向有關(guān)跟向量長度無關(guān)。我自己做相似度模型的時候就踩過這個坑不加L2歸一化訓(xùn)練時模型會把所有向量都拉長來提高點積分數(shù)推理時結(jié)果完全不可控。加上歸一化之后訓(xùn)練穩(wěn)定很多。3. 矩陣運算線性層、批量推理與參數(shù)壓縮背后的乘法邏輯3.1 矩陣乘法的形狀對位先記住這一條規(guī)則矩陣乘法C A B要求A的列數(shù)必須等于B的行數(shù)。結(jié)果C的形狀是“A的行數(shù) × B的列數(shù)”。比如A形狀(m, n)B形狀(n, p)結(jié)果就是(m, p)。這個規(guī)則是初學(xué)者最容易卡殼的地方??纯催@張表A形狀B形狀是否合法C形狀(3, 4)(4, 2)合法(3, 2)(3, 4)(3, 4)不合法無(3, 4)(4,)合法(3,)為什么會這樣因為矩陣乘法里A的每一行要和B的每一列做點積所以要求行的長度也就是A的列數(shù)和列的高度也就是B的行數(shù)一致。從另一種視角看矩陣乘法等于把B看作列向量的集合A B等價于對A的每一行分別與B的所有列向量做點積。這一下子就把矩陣乘法拆回第2章的向量點積了心理負擔小很多。3.2 線性層的本質(zhì)把“一組神經(jīng)元”同時作用到“一批樣本”上全連接層的公式是Y X W.T b。其中X是輸入形狀一般是(batch_size, input_dim)W是權(quán)重形狀是(output_dim, input_dim)W.T轉(zhuǎn)置成(input_dim, output_dim)b是偏置形狀(output_dim,)。為什么要轉(zhuǎn)置因為W的每一行本來就是一個神經(jīng)元的權(quán)重向量。我們想讓X的每個樣本和W的每個神經(jīng)元做點積得到一個(batch_size, output_dim)的矩陣。如果直接X W維度會變成(batch_size, input_dim)完全不是我們要的。轉(zhuǎn)置一下XW.T就變成“每一行是樣本每一列是神經(jīng)元”的相似度矩陣輸出里第i行第j列就是第i個樣本對第j個神經(jīng)元的原始激活值。這個操作把兩層循環(huán)遍歷樣本、遍歷神經(jīng)元壓縮成一次矩陣乘法。我剛開始學(xué)的時候很疑惑為什么框架里能直接寫nn.Linear(in_features, out_features)底層不就是一個矩陣乘加偏置嗎后來才意識到矩陣乘法的價值就是把“大量重復(fù)的點積”集中起來并行執(zhí)行。CPU 和 GPU 底層的 BLAS 庫對矩陣乘法做了極致的優(yōu)化分塊、內(nèi)存緩存、SIMD 指令甚至多核并行。手動寫for sample in batch: for neuron in weight:做點積速度連的百分之一都不到。3.3 矩陣運算中的批量與維度擴展模型訓(xùn)練時通常一次喂一個 batch而不是一個樣本。假設(shè) batch size 是 64輸入特征數(shù)是 128輸出神經(jīng)元數(shù)是 256那么X是(64, 128)W是(256, 128)輸出Z是(64, 256)。一次把 64×256 個點積全部算完這才是真正的“批量計算”。有時候你還會需要把多個權(quán)重矩陣堆起來那就需要三維張量。舉個例子多頭注意力里每個頭都有自己的W_q、W_k、W_v頭數(shù)為8每個頭的輸入維度是d_k。你當然可以 for 循環(huán)8次但更高效的做法是把8個權(quán)重矩陣疊成一個形狀(8, input_dim, d_k)的三維張量然后一次torch.matmul完成多頭的線性變換。這就是張量運算延展矩陣乘法的經(jīng)典場景。4. 張量運算廣播機制、維度變換與內(nèi)存布局4.1 張量是帶批次的矩陣圖片、文本和序列的高維真相很多初學(xué)者看到四維張量就頭暈其實只要記住“前面的維度是批量相關(guān)后面的維度是單樣本內(nèi)部結(jié)構(gòu)”就夠用了。圖像分類中一批 RGB 圖片的形狀是(batch_size, channels, height, width)比如(16, 3, 224, 224)。16是圖片張數(shù)3是RGB通道后面是每張圖片的高寬。如果你把單張圖片(3, 224, 224)當成“一張?zhí)厥獾木仃嚒蹦?16, 3, 224, 224)就是16個這樣的矩陣疊在一起所以叫張量。自然語言處理中一句話經(jīng)過詞嵌入后得到(seq_len, hidden_size)一批句子則是(batch_size, seq_len, hidden_size)。注意力分數(shù)矩陣是(batch_size, num_heads, seq_len_q, seq_len_k)簡直是一個套娃。但只要你給每個維度起了名字就不會亂。4.2 廣播機制沒有復(fù)制數(shù)據(jù)卻像把數(shù)據(jù)擴展了一樣兩個張量做逐元素運算時形狀不一定要完全一樣。NumPy 和 PyTorch 遵循一套廣播規(guī)則從右往左對齊維度每個維度要么相等要么其中一個等于1要么一個維度缺失。看這個例子import torch a torch.randn(3, 1) # 形狀 (3, 1) b torch.randn(1, 4) # 形狀 (1, 4) c a b # 結(jié)果形狀 (3, 4)這里是(3, 1)和(1, 4)相加廣播后相當于a的第1維復(fù)制4份b的第0維復(fù)制3份最終得到(3, 4)。廣播看起來很方便但也埋了很多雷。有一次我調(diào)試一個模型發(fā)現(xiàn)某個中間結(jié)果在 batch size 大于1時出現(xiàn)奇怪的低精度排查半天才發(fā)現(xiàn)是形狀(batch, 1)的向量和一個(batch, feature)的矩陣相加廣播把向量“橫向拉伸”到了所有特征維上導(dǎo)致每個樣本都加了同樣的偏置。這不是錯誤但完全不是我想要的。所以遇到形狀警告或者奇怪的數(shù)值第一反應(yīng)就是檢查是不是廣播在偷偷“幫倒忙”。4.3 維度變換是改“視圖”而不是改“數(shù)據(jù)”view、reshape、permute、transpose是張量維度操作的四件套但它們的底層邏輯完全不同。permute/transpose只交換維度順序不改變底層數(shù)據(jù)的排列順序。view/reshape則要求兩個視圖的元素順序一致如果之前做過transpose底層內(nèi)存順序已經(jīng)不是原來的順序直接view會報錯或者產(chǎn)生錯誤結(jié)果。解決辦法是先contiguous()讓張量在內(nèi)存里變得連續(xù)再view。我見過很多同學(xué)在attention實現(xiàn)里把(batch, heads, seq_len, d_head)轉(zhuǎn)成(batch, seq_len, heads * d_head)時直接view報錯然后一臉懵。原因就是之前做過transpose。正確的做法是permute之后.contiguous().view(...)。理解內(nèi)存布局還有性能意義。GPU 對連續(xù)內(nèi)存的訪問效率遠高于跳躍式訪問。如果你把NCHW的圖片張量transpose成NHWC雖然形狀看著沒問題但每次內(nèi)存讀取都不連續(xù)訓(xùn)練速度可能下降百分之二三十。所以盡量讓數(shù)據(jù)從加載到計算都保持同一套布局別在模型中間來回轉(zhuǎn)置。5. 手寫一個線性層用 NumPy 復(fù)現(xiàn)前向傳播和反向梯度5.1 前向傳播一行矩陣乘法就夠我們用一個小例子把前面所有知識串起來。假設(shè)輸入X有3個樣本每個樣本4個特征線性層有2個神經(jīng)元import numpy as np X np.array([ [1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 10.0, 11.0, 12.0], ]) # 形狀 (3, 4) W np.array([ [0.1, 0.2, 0.3, 0.4], [0.5, 0.6, 0.7, 0.8], ]) # 形狀 (2, 4)一行是一個神經(jīng)元 b np.array([0.1, 0.2]) # 形狀 (2,) Z X W.T b # (3, 2) print(Z)X W.T的形狀是(3, 2)加上b時廣播到每一行。這個Z就是兩個神經(jīng)元對三個樣本的原始輸出。如果后面接一個sigmoid它就是2個神經(jīng)元的前向激活值。很多人會疑惑為什么不把W定義成(input_dim, output_dim)直接用X W當然也可以但 PyTorch 的nn.Linear官方實現(xiàn)里權(quán)重形狀是(out_features, in_features)所以需要轉(zhuǎn)置。我這里故意用同樣的定義方式就是為了讓你在看框架文檔時能對上號。5.2 反向傳播梯度也是矩陣乘法訓(xùn)練模型時反向傳播算梯度本質(zhì)上還是在做矩陣乘法。對于線性層Z X W.T b如果我們得到了損失對Z的梯度dZ那么權(quán)重的梯度就是dW dZ.T X。用剛才的數(shù)據(jù)試一下dZ np.array([ [1.0, -1.0], [0.5, 0.5], [-0.5, 0.2], ]) # 模擬上游梯度 dW dZ.T X # 形狀 (2, 4) print(dW)這里dZ.T形狀是(2, 3)X形狀是(3, 4)結(jié)果恰好是(2, 4)和W形狀一致。如果寫成X.T dZ也行關(guān)鍵是矩陣乘法要讓我們把“每個神經(jīng)元對每個輸入特征的梯度”都算出來。自己用 NumPy 寫一遍這個流程比看十遍原理都管用。做完之后你會特別直觀地理解為什么 autograd 能自動求梯度它只是在計算圖的每個節(jié)點上應(yīng)用了類似的矩陣乘法規(guī)則而已。5.3 加上激活函數(shù)非線性藏在逐元素操作里線性層串再多矩陣乘法所有層加起來仍然是一個線性變換因為矩陣乘法滿足結(jié)合律。真正讓深度學(xué)習(xí)變“深”的是激活函數(shù)ReLU、Sigmoid、Tanh 等都是逐元素的張量運算。用代碼體現(xiàn)一下def relu(tensor): return np.maximum(tensor, 0) A1 relu(X W1.T b1) A2 relu(A1 W2.T b2)這里A1是(3, hidden_dim)的矩陣它同時是下一層的輸入。所以張量的形狀就是在這樣的逐層變換中流轉(zhuǎn)。每個 batch 的數(shù)據(jù)從(batch, input_dim)變成(batch, hidden_dim)再變成(batch, output_dim)整個模型就是一系列矩陣乘法和逐元素激活函數(shù)的組合。6. 我踩過的張量形狀坑維度錯位、廣播誤用和性能瓶頸6.1 形狀錯誤“忘記轉(zhuǎn)置”是頭號坑我自己剛寫 Transformer 時卡得最久的就是注意力分數(shù)計算。Q和K的形狀都是(batch, heads, seq_len, head_dim)按理說要對最后兩個維度做點積Q K.transpose(-2, -1)結(jié)果是(batch, heads, seq_len, seq_len)同時轉(zhuǎn)置的位置還關(guān)系到K要不要先contiguous。有次我把K轉(zhuǎn)置后直接view報錯說“view size is not compatible with input size”。后來才意識到transpose會讓內(nèi)存布局變成非連續(xù)必須先.contiguous()。這個坑幾乎人人都會踩一次解決辦法就是記住transpose和permute之后如果要改變形狀或者接入某些要求連續(xù)內(nèi)存的操作先加contiguous()。6.2 廣播誤用相似度矩陣被悄悄“降維”另一個經(jīng)典問題是計算兩個矩陣兩兩之間的相似度。假設(shè)A是(m, d)B是(n, d)你想要(m, n)的相似度矩陣。最自然的寫法是兩層循環(huán)for i in range(m): for j in range(n): sim[i, j] np.dot(A[i], B[j])但這樣既冗長又慢。高效寫法是sim A B.T # 形狀 (m, n)直接把所有點積一次算完。這個例子說明很多“看起來要循環(huán)”的場景其實都能用矩陣乘法表達。學(xué)會用替代顯式循環(huán)不僅代碼整潔速度優(yōu)勢尤其明顯。在一組 512 個 query 和 512 個 key 上對比測試循環(huán)要幾毫秒矩陣乘法可以到微秒級而且批量越大差距越明顯。6.3 精度和內(nèi)存隱藏的性能殺手我遇過一種情況模型在 PyTorch 上是float32但中間為了調(diào)試把數(shù)據(jù)轉(zhuǎn)成 NumPy某些操作默認變成了float64再轉(zhuǎn)回 Tensor 時顯存直接翻倍。在batch size很大的場景下這一下就可能讓 OOM 出現(xiàn)。解決辦法是時刻關(guān)注dtype。NumPy 創(chuàng)建數(shù)組時默認float64如果你只是做數(shù)值驗證沒關(guān)系但如果你在模型訓(xùn)練循環(huán)里做自定義數(shù)據(jù)處理記得顯式指定dtypenp.float32或者用torch.float32。另外GPU 上減少不必要的張量復(fù)制也很重要。大多數(shù)框架的view和reshape不會復(fù)制數(shù)據(jù)但contiguous()在某些情況下會觸發(fā)新拷貝頻繁調(diào)用會拖慢整體速度。我在調(diào)優(yōu)時常用的一個原則是從一個張量到另一個張量盡量只改變每個維度的含義而不是反復(fù)交換維度順序。如果非要換就把所有轉(zhuǎn)置集中到數(shù)據(jù)預(yù)處理階段不要在模型主循環(huán)里做。最后再補充一個很多人忽略的點不要迷信einsum。它確實能讓復(fù)雜的高維矩陣乘法寫得很簡潔比如注意力里的torch.einsum(bqhd,bkhd-bhqk, Q, K)。但einsum的缺點是報錯信息非常隱晦內(nèi)部還可能走通用路徑性能未必優(yōu)于顯式的加transpose。我的習(xí)慣是先用顯式矩陣乘法調(diào)通功能確認需求后再考慮用einsum簡化代碼。這樣踩的坑最少效率也最高。機器學(xué)習(xí)這條路走到越深越會發(fā)現(xiàn)數(shù)學(xué)不是“紙上公式”而是嵌在每一行代碼里的思維框架。向量、矩陣、張量的運算規(guī)則就是AI的底層語法搞懂了它們你看模型源碼會順暢很多排查 bug 也不用靠瞎猜。希望這篇“計算語法”筆記能幫你把線性代數(shù)和實際模型的最后一段縫隙也補上。