第 4 講・互動版

Self-attention —— 讓每個字自己決定要看誰

一言以蔽之:self-attention 其實就是「每個位置自己決定要看哪些位置」而已。

好,各位同學大家好,那我們就來上課吧。

今天要跟大家分享的,是整個現代 AI 最重要的一塊積木。ChatGPT、Gemini、Claude,這些模型裡面最核心的東西就是它。

而我今天想證明給你看的是:它其實沒有你想的那麼難。今天上完,你會自己從零把它刻出來,而且會親眼看到它真的學到東西——而且是在你的瀏覽器裡當場 train 出來的,不是預先錄好的表演。

今天的 Roadmap

這堂課分成四段:

  1. 先讓你看到舊方法為什麼不夠用(這段最重要,不要跳)
  2. 從「怎麼辦呢」一路推出 attention 的三個角色:Query、Key、Value
  3. 手刻一個 self-attention,train 一個小任務,把 attention map 打開來看
  4. 三個大家最容易踩的坑:為什麼要三個矩陣、為什麼要除以根號 d、為什麼還要位置編碼

先備知識:第 1~3 講(找函式、疊深、怎麼 train)。會矩陣乘法會更好,但不會也沒關係,我會講它在幹嘛。

這一版是互動的。每一個滑桿你都可以拉、每一個顏色你都可以點,圖會馬上重畫。尤其是「動手玩 ②」,那是今天的高潮,你一定要自己點點看。

先看我們卡在哪裡

到目前為止,我們的輸入都長一樣:固定長度的一組數字。第 1 講是 7 個數字(過去七天的觀看數)。就這樣,永遠 7 個。

但你想想看,真實世界的輸入常常不是這樣:一個句子有時候 5 個字、有時候 50 個字;一段語音長度不固定;一個分子原子數量不固定。

光是「長度會變」就已經很麻煩了。但真正致命的是第二個問題

致命的第二個問題

假設我們要處理這兩個句子:

我 買 了 一 台 蘋果
我 吃 了 一 顆 蘋果

同樣是「蘋果」這兩個字。上面那個是公司,下面那個是水果

那問題來了:如果我們把每個字都轉成一個固定的向量,那兩個「蘋果」拿到的會是同一個向量。一模一樣的輸入,你要 model 吐出不一樣的答案?那是不可能的。

所以我們需要的是:「蘋果」這個字的向量,要能夠隨著它旁邊的字而改變。

怎麼辦呢?

笨方法先來一次

講新方法之前,我們先把最直覺的兩個作法拿出來撞一次牆。因為你要先知道它們為什麼不夠,才會知道 attention 到底在解什麼。

笨方法一:全部接起來,丟進一個大網路

把整個句子的向量接成一條超長的向量,丟進第 2 講學的那種網路。兩個問題,而且都是硬傷:

  1. 長度不固定。你的網路第一層要開多寬?開 50 個字的寬度,來一個 100 字的句子就爆了。
  2. 參數量爆炸。而且更慘的是——第 3 個字跟第 7 個字的關係,跟第 10 個字跟第 14 個字的關係,其實是同一種關係啊(都是隔 4 個字)。但這個網路要分開學兩次。它學不會「舉一反三」。

笨方法二:開一個窗口,只看左右幾個字

那我不要看全部,我只看左右各 3 個字,這樣長度就固定了。聽起來合理。但你看這句:

他 昨天 在 那家 我 上禮拜 跟 你 提過 的 店 裡 蘋果

「買」跟「蘋果」中間隔了老遠。你窗口要開多大才夠?

而且你根本不知道要開多大——每個句子需要的距離都不一樣。你如果乾脆開到跟句子一樣長,那就回到笨方法一了,繞了一圈回到原點。

所以真正的問題是什麼?

你會發現,這兩個笨方法都犯了同一個錯:

它們都在「事先」決定要看哪些字。

一個決定看全部,一個決定看附近。但「該看哪些字」這件事,本來就應該看情況啊。

那……如果我們不要事先決定呢?如果我們讓每個字自己去找它該看的字呢?

那要怎麼「自己去找」?

好,這邊神奇的地方來了。

假設你今天在一個很大的辦公室,你手上有個問題要解決。你會怎麼做?

你不會挨個問過去。你會喊一聲,說你要找什麼——比如說「報帳流程要跑幾天?」

然後辦公室裡每個人心裡都有一個「我會什麼」的標籤。會計小陳的標籤是「報帳、發票」,工程師阿德的標籤是「部署、資料庫」。你的問題跟小陳的標籤很搭,跟阿德的標籤不太搭

所以你就主要去聽小陳講什麼,順便瞄一下阿德。你不是完全不理阿德——你只是分配比較少的注意力給他。

好,這整件事情,就是 attention。它裡面有三個角色:

角色
英文
在剛剛的故事裡是什麼
查詢
Query (Q)
你喊出來的那個問題:「我在找什麼」
標籤
Key (K)
每個人身上的標籤:「我是關於什麼的」
內容
Value (V)
那個人實際講出來的話:「我的內容是什麼」

流程就三步:

  1. 拿你的 Query,去跟每個人的 Key 比對,算出有多搭
  2. 把「有多搭」變成一組百分比(加起來是 100%)
  3. 照這個百分比,把大家的 Value 加權平均起來

然後就結束了。這就是 attention 的全部。

與其用講的,不如你自己來喊一次。下面這個辦公室裡有五個人,每個人站的位置就是他的 Key(他是關於什麼的),手上的數字是他的 Value(他會告訴你「大概要幾天」)。那支藍色箭頭就是你的 Query,拖著它到處指指看。

動手玩 ①・你來當 Query
拖曳藍色箭頭 = 換一個問題。箭頭拉越長代表你問得越明確。
上:相關度(Query·Key) 下:softmax 之後分到的注意力

拖著箭頭你應該會發現三件事:

  1. 指向誰,誰的注意力就高。相關度就是內積,白話說就是「方向合不合」。
  2. 注意力永遠加起來是 100%。你多給了小陳,就一定得從別人身上扣。這是 softmax 的功勞。
  3. 箭頭拉越長,注意力越集中;縮到很短,五個人幾乎平分。這個等一下「坑二」會再出現一次,先記著。

然後有一件事你一定要注意:最後的輸出,不是「你看向誰」,而是「你看的那些人手上的數字的加權平均」。Key 是拿來比對的,Value 才是拿來用的。這兩個是不同的東西——這件事等一下「坑一」會變成重點。

寫成式子,短到有點好笑

那 self-attention 的「self」是什麼意思呢?意思是問問題的人跟回答的人是同一群——句子裡每個字都同時扮演這三個角色:它會發問,它也有標籤,它也有內容。

所以我們把輸入 (每一列是一個位置的向量)分別乘上三個矩陣,變出三種身分:

然後兩兩比對、變百分比、加權平均:

就這兩行。真的就這樣。

注意輸出的形狀:進去 8 個位置,出來還是 8 個位置。self-attention 不會把序列變長或變短,它只是把每個位置的向量換成一個「參考過全場」的新向量

而那個 attention 矩陣 ,第 列第 行的數字,意思就是:「第 i 個位置,把多少比例的注意力分給了第 j 個位置」

給它一個任務,看它會不會學

光有式子不算數,我們要看它真的學得起來。所以要設計一個任務,這個任務只有學會「該看誰」才做得出來

任務是這樣的。每個位置上有兩個東西:一個顏色(紅、綠、藍其中一種),一個數值。要輸出的答案是:「跟我同顏色的那些位置,它們數值的平均」

位置  1  2  3  4
顏色  紅  藍  紅  藍
數值  1.0  5.0  3.0  7.0
─────────────────────
答案  2.0  6.0  2.0  6.0 ← (1+3)/2 跟 (5+7)/2

你想想看,要做出這件事,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 一定會卡住的地方。

坑一:為什麼要 Q、K、V 三個矩陣?

這是最多人問的問題。你可能會想說:我要算「兩個字搭不搭」,那我直接把兩個字的向量做內積不就好了?幹嘛還要先乘三個矩陣,多此一舉?

好問題。那我們就來試試看,把投影關掉,直接拿輸入自己跟自己算內積,看會怎樣。

下面用的是 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 拉大
隨便抽一個 query 對 8 個 key 的注意力分配
抽 64 組取平均的「最大注意力」,趨勢才看得清楚
64

把滑桿從左邊拉到右邊,你會看到紅色那組(沒除以 )越來越極端,到 d 幾百的時候幾乎是贏者全拿,一根吃掉全部。藍色那組(有除)從頭到尾都很穩。

右邊那條曲線是抽 64 組取平均的結果:沒除的最大注意力從 d=4 的 一路爬到 d=512 的 ;有除的則是從 ,幾乎沒動。

左邊那張圖只是一次抽籤,所以你拉滑桿的時候它會上下跳,甚至偶爾 d 變大反而沒那麼極端——那是抽樣的雜訊,很正常。要看趨勢請看右邊那條平均曲線。這件事本身也值得記起來:單一個例子說服不了人,要看平均。

那為什麼極端不行?兩個理由:

  1. 它就變成「只看一個」了,加權平均的意義就沒了——那跟笨方法二的窗口有什麼兩樣?
  2. 更致命的是——softmax 在極端的地方梯度幾乎是 0。梯度是 0,就代表學不動。這就是第 3 講講的那種 train 不起來的病。

除以 就是把波動拉回到 1 附近,讓 softmax 待在它還有梯度的區域。就這樣而已,不是什麼神奇的東西。

對了,還記得「動手玩 ①」裡面「箭頭拉越長、注意力越集中」嗎?那是同一件事。內積的尺度會直接決定 softmax 有多極端。

坑三:attention 根本不知道誰前誰後

這個坑最隱蔽,但也最重要。

你回頭看我們的式子。每個位置去跟所有位置比對,然後加權平均。請問這裡面,哪一項跟「位置」有關?

沒有。一項都沒有。

也就是說,「貓 追 狗」跟「狗 追 貓」,在 self-attention 眼裡是一樣的。它完全不知道誰在前面誰在後面。

這件事我們可以直接驗證:把輸入的順序打亂,看輸出會怎樣。

動手玩 ⑤・把順序打亂

完全一樣。你把輸入打亂,輸出就只是跟著一起打亂而已,內容一模一樣,連小數點後十幾位都一樣。

這個性質有個名字,叫 permutation equivariant(排列等變)。

對我們今天這個「同色平均」的任務來說,這其實剛好沒差——因為算平均本來就跟順序無關嘛。但你想想看,如果任務是理解一個句子呢?那就完蛋了。

那怎麼辦呢?

解法很直接,甚至有點土:既然它不知道位置,那我們就把位置「加」進去。

每個位置給它一個獨一無二的向量(叫 positional encoding),直接加到輸入上。這樣第 3 個位置的「蘋果」,跟第 7 個位置的「蘋果」,輸入向量就不一樣了。

早期用的是 sin 跟 cos 組出來的固定向量,現在也很多是直接讓它學。經典的 sin/cos 版本長這樣:

動手玩 ⑥・位置的指紋
看第幾個位置:
橫軸是向量的第幾維,縱軸是第幾個位置・點一下換要看的位置
挑三個位置疊起來看 ← 每一條都不一樣
20

左邊那張圖,每一橫排就是一個位置的「指紋」。左半邊的維度變化很快(高頻),右半邊變化很慢(低頻)——這跟二進位有點像:低位數跳很快,高位數跳很慢,合起來就能不重複地數很多個數字。

拉滑桿或直接點左圖換位置,你會看到右邊那條線跟著換一個形狀。任兩個位置的指紋都不一樣,所以把它加到輸入上,model 就分得出誰前誰後了。

Multi-head:為什麼要好幾組 Q、K、V?

最後補一個很重要的觀念。

我們剛剛的 attention,一個位置只算一組注意力分數。但「相關」這件事,其實有很多種啊。比如說在一個句子裡:「他」跟「小明」相關,因為指的是同一個人;「吃」跟「蘋果」相關,因為是動詞跟受詞;「昨天」跟「吃」相關,因為是時間修飾

這是三種完全不同的關係。你用一組 Q、K 去算,它只能學出一種「什麼叫相關」。

那怎麼辦呢?很簡單——那就做好幾組嘛。

這就是 multi-head attention。開 8 組(8 個 head),每組各自有自己的 ,各自算各自的注意力,最後把結果接起來再過一個矩陣。每個 head 就會自己去專精一種關係。其實就是這樣而已。

那真的每個 head 都學到不同的東西嗎?這是個好問題,而且答案是「不一定」——研究發現有些 head 其實在做重複的事,剪掉也沒差。這個我們就不展開了,你知道有這件事就好。

一個很重要的誤解要澄清

講到這邊,我要特別提醒一件事,因為這個誤會非常普遍

很多人看到 attention map,會說:「你看,這就是模型的注意力,這就是它做決定的理由。」

千萬不要這樣講。

attention 的權重告訴你的是資訊怎麼流動,它不等於「模型為什麼這樣決定」。為什麼呢?想想看:

這個議題在研究上叫 attention is not explanation,是有專門討論的。

我自己是覺得,attention map 拿來當除錯的線索很好用——像我們今天在「動手玩 ②」做的這樣,驗證它有沒有學到該學的東西。但把它當成「模型的理由」拿去跟別人解釋,那就走太遠了。

隨堂小測驗

三題,選了馬上告訴你對不對。

換你動手:三個練習

光看是學不會的,你一定要自己改改看。

  1. 練習 1(暖身):回到「動手玩 ④」,把 d 從 1 慢慢拉到 512,記下紅色最高那根在哪些地方跳一階。然後想一想:如果今天 key 有 100 個而不是 8 個,這個現象會變嚴重還是變輕微?
  2. 練習 2(核心):改任務。現在的答案是「同色位置的平均」,改成「同色位置的最大值」。它還學得起來嗎?為什麼?(提示:attention 做的是加權平均。加權平均有辦法做出「取最大值」嗎?)這題要寫程式,打開 notebooks/04_Self-attention_讓每個字自己決定要看誰.ipynb 的練習區做。
  3. 練習 3(挑戰):把 single-head 改成 2 個 head,然後設計一個需要兩種關係的任務——比如說答案是「同色位置的平均」加上「左右鄰居的平均」。畫出兩個 head 的 attention map,看它們有沒有分工。

好,我們複習一下

  1. 問題:舊方法都在「事先」決定要看哪些位置。但該看誰,本來就該看情況
  2. 解法:讓每個位置自己去找。發問的叫 Query,掛牌子的叫 Key,實際內容叫 Value。算相關度 → 變成百分比 → 加權平均。就結束了。
  3. 三個坑:要三個矩陣,因為「我想找什麼」「我是什麼」「我能給什麼」是三件事;要除以 ,不然 softmax 會極端到沒有梯度;attention 不知道順序,要另外把位置加進去。
  4. 而且我們親眼看到——「該看誰」是它自己學出來的,我們沒教。那個 1/8、1/4、1/2 的數字,是它自己從 gradient descent 裡摸出來的。

還有一條要記在心裡:attention map 不是模型的理由,它只是資訊流動的地圖。

最後留個問題給你,這也是下一堂課的主題。

我們今天做的任務,是給一串東西、輸出一串東西。那如果我要它生成呢?

比如說我給它「今天天氣真」,要它自己接下去寫。它要怎麼知道要接什麼?寫完一個字之後,下一個字又要怎麼決定?而且更麻煩的是——它要寫到什麼時候才停

這個我們下一堂課再跟大家講。

以上就是我今天想跟大家分享的內容。