一言以蔽之:self-attention 其實就是「每個位置自己決定要看哪些位置」而已。
好,各位同學大家好,那我們就來上課吧。
今天要跟大家分享的,是整個現代 AI 最重要的一塊積木。ChatGPT、Gemini、Claude,這些模型裡面最核心的東西就是它。
而我今天想證明給你看的是:它其實沒有你想的那麼難。今天上完,你會自己從零把它刻出來,而且會親眼看到它真的學到東西——而且是在你的瀏覽器裡當場 train 出來的,不是預先錄好的表演。
這堂課分成四段:
先備知識:第 1~3 講(找函式、疊深、怎麼 train)。會矩陣乘法會更好,但不會也沒關係,我會講它在幹嘛。
這一版是互動的。每一個滑桿你都可以拉、每一個顏色你都可以點,圖會馬上重畫。尤其是「動手玩 ②」,那是今天的高潮,你一定要自己點點看。
到目前為止,我們的輸入都長一樣:固定長度的一組數字。第 1 講是 7 個數字(過去七天的觀看數)。就這樣,永遠 7 個。
但你想想看,真實世界的輸入常常不是這樣:一個句子有時候 5 個字、有時候 50 個字;一段語音長度不固定;一個分子原子數量不固定。
光是「長度會變」就已經很麻煩了。但真正致命的是第二個問題。
假設我們要處理這兩個句子:
同樣是「蘋果」這兩個字。上面那個是公司,下面那個是水果。
那問題來了:如果我們把每個字都轉成一個固定的向量,那兩個「蘋果」拿到的會是同一個向量。一模一樣的輸入,你要 model 吐出不一樣的答案?那是不可能的。
所以我們需要的是:「蘋果」這個字的向量,要能夠隨著它旁邊的字而改變。
怎麼辦呢?
講新方法之前,我們先把最直覺的兩個作法拿出來撞一次牆。因為你要先知道它們為什麼不夠,才會知道 attention 到底在解什麼。
把整個句子的向量接成一條超長的向量,丟進第 2 講學的那種網路。兩個問題,而且都是硬傷:
那我不要看全部,我只看左右各 3 個字,這樣長度就固定了。聽起來合理。但你看這句:
「買」跟「蘋果」中間隔了老遠。你窗口要開多大才夠?
而且你根本不知道要開多大——每個句子需要的距離都不一樣。你如果乾脆開到跟句子一樣長,那就回到笨方法一了,繞了一圈回到原點。
你會發現,這兩個笨方法都犯了同一個錯:
它們都在「事先」決定要看哪些字。
一個決定看全部,一個決定看附近。但「該看哪些字」這件事,本來就應該看情況啊。
那……如果我們不要事先決定呢?如果我們讓每個字自己去找它該看的字呢?
好,這邊神奇的地方來了。
假設你今天在一個很大的辦公室,你手上有個問題要解決。你會怎麼做?
你不會挨個問過去。你會喊一聲,說你要找什麼——比如說「報帳流程要跑幾天?」
然後辦公室裡每個人心裡都有一個「我會什麼」的標籤。會計小陳的標籤是「報帳、發票」,工程師阿德的標籤是「部署、資料庫」。你的問題跟小陳的標籤很搭,跟阿德的標籤不太搭。
所以你就主要去聽小陳講什麼,順便瞄一下阿德。你不是完全不理阿德——你只是分配比較少的注意力給他。
好,這整件事情,就是 attention。它裡面有三個角色:
流程就三步:
然後就結束了。這就是 attention 的全部。
與其用講的,不如你自己來喊一次。下面這個辦公室裡有五個人,每個人站的位置就是他的 Key(他是關於什麼的),手上的數字是他的 Value(他會告訴你「大概要幾天」)。那支藍色箭頭就是你的 Query,拖著它到處指指看。
拖著箭頭你應該會發現三件事:
然後有一件事你一定要注意:最後的輸出,不是「你看向誰」,而是「你看的那些人手上的數字的加權平均」。Key 是拿來比對的,Value 才是拿來用的。這兩個是不同的東西——這件事等一下「坑一」會變成重點。
那 self-attention 的「self」是什麼意思呢?意思是問問題的人跟回答的人是同一群——句子裡每個字都同時扮演這三個角色:它會發問,它也有標籤,它也有內容。
所以我們把輸入 (每一列是一個位置的向量)分別乘上三個矩陣,變出三種身分:
然後兩兩比對、變百分比、加權平均:
就這兩行。真的就這樣。
注意輸出的形狀:進去 8 個位置,出來還是 8 個位置。self-attention 不會把序列變長或變短,它只是把每個位置的向量換成一個「參考過全場」的新向量。
而那個 attention 矩陣 ,第 列第 行的數字,意思就是:「第 i 個位置,把多少比例的注意力分給了第 j 個位置」。
光有式子不算數,我們要看它真的學得起來。所以要設計一個任務,這個任務只有學會「該看誰」才做得出來。
任務是這樣的。每個位置上有兩個東西:一個顏色(紅、綠、藍其中一種),一個數值。要輸出的答案是:「跟我同顏色的那些位置,它們數值的平均」。
你想想看,要做出這件事,model 非得學會一件事不可:每個位置要把注意力放在跟自己同色的位置上。沒有別條路。
所以如果它做得出來,那 attention map 就一定會呈現出「同色的互相看」的樣子。這是一個可以被推翻的預測——我們現在就來驗證。
下面這個 model 是你打開這一頁的時候,在你的瀏覽器裡當場 train 的:8 個位置、、Adam、… 步,總共花了 … 毫秒。Loss 從 … 掉到 …。拿沒看過的 256 個句子驗收,MSE = …(如果它只會亂猜 0,MSE 會是 …)。
好,那就打開它的腦袋看吧。上面那排顏色格子是可以點的——點下去會換顏色,attention map 會立刻重新分塊。
有沒有很漂亮?attention map 呈現出一塊一塊的結構——顏色一樣的位置,互相之間特別亮;顏色不一樣的,幾乎全黑。
但真正讓我覺得厲害的是格子裡的數字。你去按一下「全部同色」,再按「四四對半」,然後自己亂點幾個顏色,盯著格子裡的數字看:
0.13、0.25、0.50、0.99……你發現了嗎?這就是 1/8、1/4、1/2、1/1。
它不只學會了「要看同色的」,它還學會了「要平均分配給它們」。而且同色的有幾個,它就自動分成幾份。
想想看這件事有多不簡單:每個句子的顏色分佈都不一樣,它沒辦法把「除以 5」這件事寫死在參數裡。它必須看了當下這個句子之後,臨時算出要除以幾。而 softmax 剛好就有這個能力——分數一樣的東西,它自然就會平分。
而且請注意一件事:我們從頭到尾沒有告訴它「要看同色的」。我們只給了它一堆 (輸入, 答案) 的配對,然後硬 train 一發。「該看誰」這件事,是它自己在 gradient descent 的過程中摸索出來的。
這就是 attention 最厲害的地方——連結是學出來的,不是我們接好的。
好,我們現在知道它會動了。接下來我要回答三個問題,這三個問題是大家第一次學 attention 一定會卡住的地方。
這是最多人問的問題。你可能會想說:我要算「兩個字搭不搭」,那我直接把兩個字的向量做內積不就好了?幹嘛還要先乘三個矩陣,多此一舉?
好問題。那我們就來試試看,把投影關掉,直接拿輸入自己跟自己算內積,看會怎樣。
下面用的是 8 個位置、每個 64 維的隨機向量——真實的 word embedding 就是這種東西,高維、每個字一個。兩個按鈕切換「有投影」跟「沒投影」,其他條件完全一樣。
你會發現不投影的那張圖,對角線亮到刺眼,其他地方全黑。
為什麼?因為一個向量跟自己做內積,一定是最大的——那就是它的長度平方,永遠是正的、而且是全場最大。而別人跟它的內積,方向是隨機的,平均起來是 0。
所以如果不做投影,結果就是每個字都在看自己。注意力給自己的比例平均是 …——如果完全平均分配,應該只有 0.125。那 attention 就白做了,它等於什麼事都沒幹。
投影之後呢?給自己的比例掉到 …,回到跟「完全平均分配」同一個量級——自己不再有特權了。
你可以多按幾次「再抽一組詞向量」。我一口氣抽了 200 組:不投影那邊從來沒有低於 0.97;投影那邊在 0.05 到 0.26 之間晃,平均 0.13。會晃是因為這裡只有 8 個位置、樣本很小——但它再也回不到 0.98 那種等級了。
而且還有一個更根本的問題:「我想找什麼」跟「我是什麼」,本來就是兩件事。
回到辦公室那個比喻。你喊「有沒有人知道報帳流程?」——這是你的 Query。但你身上的標籤(Key)可能是「我是做前端的」。你想找的東西,跟你自己是什麼,完全不一樣。
所以我們才需要兩個不同的矩陣: 負責把「我是什麼」翻譯成「我想找什麼」, 負責把「我是什麼」翻譯成「我可以提供什麼」。
那 呢? 是「找到你之後,我要給你什麼內容」。這又是第三件事了——能被搜到的關鍵字,跟實際的內容,也是兩回事。你 Google 搜「報帳」,match 的是標題,但你要看的是內文。
前面式子裡有一個 。那個東西到底幹嘛的?很多人是背起來的,但它的道理其實一句話就講完了。
內積是「把 個乘積加起來」。所以 d 越大,這個和的波動就越大——它像一段隨機漫步,走越多步就跑越遠,標準差大約是 。
而 softmax 遇到差距很大的數字會怎樣?它會變得非常極端——幾乎把 100% 都給最大的那個,其他全部趨近於 0。
與其用講的,你自己把維度拉大看看。
把滑桿從左邊拉到右邊,你會看到紅色那組(沒除以 )越來越極端,到 d 幾百的時候幾乎是贏者全拿,一根吃掉全部。藍色那組(有除)從頭到尾都很穩。
右邊那條曲線是抽 64 組取平均的結果:沒除的最大注意力從 d=4 的 … 一路爬到 d=512 的 …;有除的則是從 … 到 …,幾乎沒動。
左邊那張圖只是一次抽籤,所以你拉滑桿的時候它會上下跳,甚至偶爾 d 變大反而沒那麼極端——那是抽樣的雜訊,很正常。要看趨勢請看右邊那條平均曲線。這件事本身也值得記起來:單一個例子說服不了人,要看平均。
那為什麼極端不行?兩個理由:
除以 就是把波動拉回到 1 附近,讓 softmax 待在它還有梯度的區域。就這樣而已,不是什麼神奇的東西。
對了,還記得「動手玩 ①」裡面「箭頭拉越長、注意力越集中」嗎?那是同一件事。內積的尺度會直接決定 softmax 有多極端。
這個坑最隱蔽,但也最重要。
你回頭看我們的式子。每個位置去跟所有位置比對,然後加權平均。請問這裡面,哪一項跟「位置」有關?
沒有。一項都沒有。
也就是說,「貓 追 狗」跟「狗 追 貓」,在 self-attention 眼裡是一樣的。它完全不知道誰在前面誰在後面。
這件事我們可以直接驗證:把輸入的順序打亂,看輸出會怎樣。
完全一樣。你把輸入打亂,輸出就只是跟著一起打亂而已,內容一模一樣,連小數點後十幾位都一樣。
這個性質有個名字,叫 permutation equivariant(排列等變)。
對我們今天這個「同色平均」的任務來說,這其實剛好沒差——因為算平均本來就跟順序無關嘛。但你想想看,如果任務是理解一個句子呢?那就完蛋了。
那怎麼辦呢?
解法很直接,甚至有點土:既然它不知道位置,那我們就把位置「加」進去。
每個位置給它一個獨一無二的向量(叫 positional encoding),直接加到輸入上。這樣第 3 個位置的「蘋果」,跟第 7 個位置的「蘋果」,輸入向量就不一樣了。
早期用的是 sin 跟 cos 組出來的固定向量,現在也很多是直接讓它學。經典的 sin/cos 版本長這樣:
左邊那張圖,每一橫排就是一個位置的「指紋」。左半邊的維度變化很快(高頻),右半邊變化很慢(低頻)——這跟二進位有點像:低位數跳很快,高位數跳很慢,合起來就能不重複地數很多個數字。
拉滑桿或直接點左圖換位置,你會看到右邊那條線跟著換一個形狀。任兩個位置的指紋都不一樣,所以把它加到輸入上,model 就分得出誰前誰後了。
最後補一個很重要的觀念。
我們剛剛的 attention,一個位置只算一組注意力分數。但「相關」這件事,其實有很多種啊。比如說在一個句子裡:「他」跟「小明」相關,因為指的是同一個人;「吃」跟「蘋果」相關,因為是動詞跟受詞;「昨天」跟「吃」相關,因為是時間修飾。
這是三種完全不同的關係。你用一組 Q、K 去算,它只能學出一種「什麼叫相關」。
那怎麼辦呢?很簡單——那就做好幾組嘛。
這就是 multi-head attention。開 8 組(8 個 head),每組各自有自己的 ,各自算各自的注意力,最後把結果接起來再過一個矩陣。每個 head 就會自己去專精一種關係。其實就是這樣而已。
那真的每個 head 都學到不同的東西嗎?這是個好問題,而且答案是「不一定」——研究發現有些 head 其實在做重複的事,剪掉也沒差。這個我們就不展開了,你知道有這件事就好。
講到這邊,我要特別提醒一件事,因為這個誤會非常普遍。
很多人看到 attention map,會說:「你看,這就是模型的注意力,這就是它做決定的理由。」
千萬不要這樣講。
attention 的權重告訴你的是資訊怎麼流動,它不等於「模型為什麼這樣決定」。為什麼呢?想想看:
這個議題在研究上叫 attention is not explanation,是有專門討論的。
我自己是覺得,attention map 拿來當除錯的線索很好用——像我們今天在「動手玩 ②」做的這樣,驗證它有沒有學到該學的東西。但把它當成「模型的理由」拿去跟別人解釋,那就走太遠了。
三題,選了馬上告訴你對不對。
光看是學不會的,你一定要自己改改看。
還有一條要記在心裡:attention map 不是模型的理由,它只是資訊流動的地圖。
最後留個問題給你,這也是下一堂課的主題。
我們今天做的任務,是給一串東西、輸出一串東西。那如果我要它生成呢?
比如說我給它「今天天氣真」,要它自己接下去寫。它要怎麼知道要接什麼?寫完一個字之後,下一個字又要怎麼決定?而且更麻煩的是——它要寫到什麼時候才停?
這個我們下一堂課再跟大家講。
以上就是我今天想跟大家分享的內容。