注意力機制是現代大語言模型(LLM)的核心組成部分,它讓模型能有效捕捉長序列資料中的依賴關係。傳統的迴圈神經網路(RNN)在處理長序列時,由於資訊壓縮在單一隱藏狀態中,容易造成資訊遺失。注意力機制則允許模型在每一步都參考整個輸入序列,從而更好地理解上下文資訊。本文將逐步實作一個簡化的自注意力機制,並使用 PyTorch 計算注意力分數、權重以及上下文向量,幫助讀者理解其核心概念和運作原理。同時,我們也將討論多頭注意力機制如何提升模型的表達能力,以及因果注意力機制如何確保模型在生成文字時只依賴於之前的上下文,為構建更強大的 LLM 奠定基礎。
注意力機制的實作
注意力機制的實作在後續章節中將會詳細介紹,包括基本的自注意力框架、因果注意力模組、以及多頭注意力模組等。這些機制對於LLM在文字生成、理解等任務中的表現至關重要。
多頭注意力模組
多頭注意力模組透過堆積疊多個因果注意力模組來增強模型的表達能力,並且可以減少過擬合。
此圖示展示了從輸入嵌入到最終輸出的流程,突出了注意力機制在LLM中的關鍵作用。
圖示解說:
- 輸入嵌入首先經過自注意力機制處理,以捕捉序列內的依賴關係。
- 然後,透過因果注意力模組確保模型在生成文字時只依賴於之前的上下文。
- 多頭注意力模組透過平行處理多個注意力機制來增強模型的表示能力。
- 最終輸出結果可用於各種下游任務,如文字生成、問答等。
實作注意力機制:LLM建構的關鍵步驟
本章節將探討注意力機制(Attention Mechanism),這是GPT類別大語言模型(LLM)的重要組成部分。我們將逐步實作四種不同的注意力機制變體,並最終建構一個能夠生成文字的模型。
長序列建模的挑戰
在深入瞭解自注意力機制(Self-Attention Mechanism)之前,我們先來探討傳統架構在處理長序列時的侷限性。以語言翻譯模型為例,由於源語言和目標語言的語法結構不同,無法簡單地逐字翻譯。
為瞭解決這個問題,通常會採用具有編碼器(Encoder)和解碼器(Decoder)的深度神經網路架構。在Transformer出現之前,迴圈神經網路(RNN)是最受歡迎的編碼器-解碼器架構。RNN是一種特殊的神經網路,它將前一步的輸出作為當前步驟的輸入,使其非常適合處理序列資料,如文字。
編碼器-解碼器RNN的運作機制
在編碼器-解碼器RNN中,輸入文字首先被送入編碼器進行順序處理。編碼器在每個步驟中更新其隱藏狀態(Hidden State),試圖在最終的隱藏狀態中捕捉整個輸入句子的含義。然後,解碼器利用這個最終隱藏狀態開始逐字生成翻譯後的句子,並在每個步驟中更新其自身的隱藏狀態,以維持上下文資訊。
編碼器-解碼器RNN的侷限性
編碼器-解碼器RNN的主要限制在於,在解碼階段,RNN無法直接存取編碼器之前的隱藏狀態。它完全依賴於當前的隱藏狀態,這可能導致上下文資訊的丟失,尤其是在複雜句子中,依賴關係可能跨越很長的距離。
注意力機制的變體
本章節將實作四種不同的注意力機制變體,如圖3.2所示。這些變體逐步構建,最終實作一個高效的多頭注意力(Multi-Head Attention)機制,可以整合到下一章將要介紹的LLM架構中。
- 簡化自注意力(Simplified Self-Attention):介紹注意力機制的基本概念。
- 自注意力(Self-Attention):加入可訓練權重,形成LLM中使用的注意力機制基礎。
- 因果注意力(Causal Attention):在自注意力的基礎上新增掩碼,使LLM能夠一次生成一個詞,同時保持序列的時間順序。
- 多頭注意力(Multi-Head Attention):擴充套件自注意力和因果注意力,使模型能夠同時關注來自不同表示子空間的資訊。
這些注意力機制的變體將逐步實作,最終形成一個高效且緊湊的多頭注意力機制,為建構LLM奠定堅實基礎。
3.2 透過注意力機制捕捉資料依賴關係
雖然遞迴神經網路(RNN)能夠很好地處理短句子的翻譯,但對於較長的文字,RNN 的表現並不理想,因為它們無法直接存取輸入中的前幾個詞。這個方法的主要缺陷在於,RNN 必須在將編碼後的輸入傳遞給解碼器之前,將整個編碼後的輸入記住在單一的隱藏狀態中(圖 3.4)。
因此,研究人員在 2014 年開發了 Bahdanau 注意力機制(以論文的第一作者命名;更多資訊請參見附錄 B),該機制修改了編碼器-解碼器 RNN,使得解碼器可以在每個解碼步驟中選擇性地存取輸入序列的不同部分,如圖 3.5 所示。
有趣的是,僅僅三年後,研究人員發現構建用於自然語言處理的深度神經網路並不需要 RNN 架構,並提出了原始的 Transformer 架構(在第 1 章中討論),其中包括一個受 Bahdanau 注意力機制啟發的自注意力機制。
3.3 利用自注意力機制關注輸入的不同部分
自注意力是一種機制,允許輸入序列中的每個位置在計算序列表示時考慮序列中所有其他位置的相關性,或「關注」其他位置。自注意力是根據 Transformer 架構的當代大語言模型(LLM)的關鍵組成部分,例如 GPT 系列。
本章重點關注對 GPT-like 模型中使用的自注意力機制的編碼和理解,如圖 3.6 所示。在下一章中,我們將編碼 LLM 的剩餘部分。
3.3.1 簡化的無可訓練權重的自注意力機制
讓我們首先實作一個簡化的自注意力變體,不包含任何可訓練的權重,如圖 3.7 所示。目標是在新增可訓練權重之前闡述自注意力中的幾個關鍵概念。
自注意力中的「自」指的是該機制透過關聯單個輸入序列內的不同位置來計算注意力權重。它評估和學習輸入本身不同部分之間的關係和依賴關係,例如句子中的單詞或影像中的畫素。
這與傳統的注意力機制形成對比,傳統注意力機制的重點是兩個不同序列的元素之間的關係,例如在序列到序列模型中,注意力可能在輸入序列和輸出序列之間,如圖 3.5 所示的例子。
# 簡化的自注意力實作範例
import numpy as np
def simple_self_attention(inputs):
# inputs 形狀:(序列長度, 特徵維度)
sequence_length, feature_dim = inputs.shape
# 初始化 context 向量列表
context_vectors = []
for i in range(sequence_length):
# 計算注意力權重
attention_weights = np.zeros(sequence_length)
for j in range(sequence_length):
# 簡單的相似度計算(例如,點積)
attention_weights[j] = np.dot(inputs[i], inputs[j])
# 正規化注意力權重
attention_weights = attention_weights / np.sum(attention_weights)
# 計算 context 向量
context_vector = np.zeros(feature_dim)
for j in range(sequence_length):
context_vector += attention_weights[j] * inputs[j]
context_vectors.append(context_vector)
return np.array(context_vectors)
# 範例使用
inputs = np.random.rand(4, 8) # 假設有4個輸入向量,每個向量有8個特徵
context_vectors = simple_self_attention(inputs)
print(context_vectors)
#### 內容解密:
- 簡單的自注意力實作:上述程式碼實作了一個簡化的自注意力機制,沒有使用可訓練的權重。對於每個輸入元素,它計算一個 context 向量,這個向量是所有輸入元素的加權和,權重由輸入元素之間的相似度決定。
- 相似度計算:在這個簡化的例子中,我們使用點積來計算兩個輸入向量之間的相似度,以此作為注意力權重。
- 正規化注意力權重:為了確保注意力權重形成一個有效的機率分佈(即它們的總和為1),我們對它們進行了正規化處理。
- 計算 context 向量:最後,對於每個輸入元素,我們根據正規化後的注意力權重和所有輸入元素計算出一個 context 向量。
這個簡化的例子有助於理解自注意力機制的基本工作原理。在實際應用中,自注意力機制通常涉及可訓練的權重,以動態地學習不同輸入元素之間的複雜關係。
自注意力機制中對輸入不同部分的關注
圖 3.7 展示了一個輸入序列,表示為 x,包含 T 個元素,分別表示為 x^(1) 到 x^(T)。該序列通常代表文字,例如已經轉換為標記嵌入的句子。
例如,考慮一個輸入文字,如「Your journey starts with one step.」。在這種情況下,序列的每個元素,如 x^(1),對應於一個 d 維的嵌入向量,代表一個特定的標記,如「Your」。圖 3.7 將這些輸入向量顯示為三維嵌入。
在自注意力機制中,我們的目標是計算輸入序列中每個元素 x^(i) 的上下文向量 z^(i)。上下文向量可以被解釋為豐富的嵌入向量。
為了說明這個概念,讓我們關注第二個輸入元素 x^(2)(對應於標記「journey」)的嵌入向量,以及圖 3.7 底部的對應上下文向量 z^(2)。這個增強的上下文向量 z^(2) 是一個嵌入,包含了 x^(2) 和所有其他輸入元素 x^(1) 到 x^(T) 的資訊。
上下文向量在自注意力機制中扮演著至關重要的角色。它們的目的是透過整合序列中所有其他元素的資訊,建立輸入序列中每個元素的豐富表示(例如,一個句子)。這對於大語言模型(LLM)至關重要,因為它們需要了解句子中單詞之間的關係和相關性。稍後,我們將新增可訓練的權重,以幫助 LLM 學習構建這些上下文向量,使其與生成下一個標記相關。但首先,讓我們一步一步地實作一個簡化的自注意力機制,以計算這些權重和結果上下文向量。
考慮以下輸入句子,該句子已經嵌入到三維向量中(參見第 2 章)。我選擇了一個小的嵌入維度,以確保它適合頁面而不換行:
import torch
inputs = torch.tensor(
[[0.43, 0.15, 0.89], # Your (x^1)
[0.55, 0.87, 0.66], # journey (x^2)
[0.57, 0.85, 0.64], # starts (x^3)
[0.22, 0.58, 0.33], # with (x^4)
[0.77, 0.25, 0.10], # one (x^5)
[0.05, 0.80, 0.55]] # step (x^6)
)
程式碼解析:
- 匯入 PyTorch 函式庫:使用
import torch將 PyTorch 函式庫匯入,以便進行張量運算。 - 定義輸入張量:使用
torch.tensor()定義一個包含六個三維向量的張量,每個向量代表一個單詞的嵌入表示。 - 嵌入維度:選擇三維嵌入以簡化範例,使其適合頁面顯示。
實作自注意力的第一步是計算中間值 ω,即注意力分數,如圖 3.8 所示。由於空間限制,圖中顯示的前面的輸入張量的值被截斷;例如,0.87 被截斷為 0.8。在這個截斷版本中,單詞「journey」和「starts」的嵌入可能由於隨機機會而顯得相似。
圖 3.8 說明瞭如何計算查詢標記和每個輸入標記之間的中間注意力分數。我們透過計算查詢 x^(2) 與每個其他輸入標記的點積來確定這些分數:
query = inputs[1]
attn_scores_2 = torch.empty(inputs.shape[0])
for i, x_i in enumerate(inputs):
attn_scores_2[i] = torch.dot(x_i, query)
print(attn_scores_2)
注意力分數計算解析:
- 選擇查詢向量:將
inputs[1]指定給query,代表第二個輸入元素「journey」。 - 初始化注意力分數張量:使用
torch.empty(inputs.shape[0])初始化一個用於儲存注意力分數的張量。 - 計算點積:遍歷每個輸入元素,並使用
torch.dot(x_i, query)計算其與查詢向量的點積,將結果儲存到attn_scores_2中。 - 輸出注意力分數:列印出計算得到的注意力分數張量。
計算出的注意力分數是:
tensor([0.9544, 1.4950, 1.4754, 0.8434, 0.7070, 1.0865])
理解點積運算
點積本質上是一種簡潔的方式,將兩個向量逐元素相乘後求和,可以表示如下:
res = 0.
for idx, element in enumerate(inputs[0]):
res += inputs[0][idx] * query[idx]
print(res)
print(torch.dot(inputs[0], query))
輸出確認逐元素乘法求和與點積結果相同:
tensor(0.9544)
tensor(0.9544)
圖示說明
圖 3.8:使用第二個輸入元素 x^(2) 作為查詢來計算上下文向量 z^(2)。該圖展示了計算查詢 x^(2) 與所有其他輸入元素之間的注意力分數 ω 的第一個中間步驟。
下一步,如圖 3.9 所示,我們對之前計算的每個注意力分數進行歸一化。歸一化的主要目標是獲得總和為 1 的注意力權重。這種歸一化是一種約定,有助於解釋和維持 LLM 中的訓練穩定性。以下是實作歸一化的簡單方法:
attn_weights_2_tmp = attn_scores_2 / attn_scores_2.sum()
print("Attention weights:", attn_weights_2_tmp)
print("Sum:", attn_weights_2_tmp.sum())
輸出顯示,注意力權重現在總和為 1:
Attention weights: tensor([0.1455, 0.2278, 0.2249, 0.1285, 0.1077, 0.1656])
Sum: tensor(1.)
注意力權重歸一化解析:
- 歸一化注意力分數:將每個注意力分數除以所有注意力分數的總和,以獲得歸一化的注意力權重。
- 輸出歸一化結果:列印出歸一化後的注意力權重及其總和,驗證其是否等於 1。
在實踐中,更常見和建議使用 softmax 函式進行歸一化。這種方法更好地處理極端值,並提供更有利於訓練的特性。
除了將點積運算視為結合兩個向量以產生標量值的數學工具之外,點積還是一種相似性度量,因為它量化了兩個向量之間的對齊程度:較高的點積表示兩個向量之間有較高的對齊或相似程度。在自注意力機制的背景下,點積決定了序列中的每個元素關注其他元素的程度:點積越高,兩個元素之間的相似性和注意力分數越高。
圖示說明
圖 3.9:在計算出相對於輸入查詢 x^(2) 的注意力分數 ω21 到 ω2T 後,下一步是透過歸一化注意力分數來獲得注意力權重 α21 到 α2T。
3.3 實作自我注意力機制於不同輸入部分
3.3.1 基本實作與理解注意力權重
在前面的章節中,我們已經瞭解了注意力機制的基本概念。現在,我們將深入實作自我注意力(self-attention)機制,並進一步理解如何計算注意力權重以及上下文向量。
首先,我們來實作一個簡單的softmax函式,用於正規化注意力分數:
def softmax_naive(x):
return torch.exp(x) / torch.exp(x).sum(dim=0)
attn_weights_2_naive = softmax_naive(attn_scores_2)
print("注意力權重:", attn_weights_2_naive)
print("總和:", attn_weights_2_naive.sum())
輸出結果如下:
注意力權重: tensor([0.1385, 0.2379, 0.2333, 0.1240, 0.1082, 0.1581])
總和: tensor(1.)
這個結果表明,softmax函式成功地將注意力權重正規化,使其總和為1。
內容解密:
torch.exp(x)計算輸入張量x中每個元素的指數函式值。torch.exp(x).sum(dim=0)對指數函式值的結果在第0維度上進行求和,得到分母。- 將每個指數函式值除以分母,得到正規化後的注意力權重。
使用PyTorch內建的torch.softmax函式可以獲得相同的結果:
attn_weights_2 = torch.softmax(attn_scores_2, dim=0)
print("注意力權重:", attn_weights_2)
print("總和:", attn_weights_2.sum())
輸出結果相同:
注意力權重: tensor([0.1385, 0.2379, 0.2333, 0.1240, 0.1082, 0.1581])
總和: tensor(1.)
現在,我們已經計算出了正規化的注意力權重,可以進行最後一步:計算上下文向量 $z^{(2)}$。上下文向量是透過將嵌入的輸入詞元 $x^{(i)}$ 與對應的注意力權重相乘,然後對結果向量進行求和得到的。
query = inputs[1]
context_vec_2 = torch.zeros(query.shape)
for i, x_i in enumerate(inputs):
context_vec_2 += attn_weights_2[i] * x_i
print(context_vec_2)
輸出結果為:
tensor([0.4419, 0.6515, 0.5683])
內容解密:
query = inputs[1]選擇第二個輸入詞元作為查詢向量。context_vec_2 = torch.zeros(query.shape)初始化上下文向量為零向量,形狀與查詢向量相同。for迴圈遍歷所有輸入詞元,將每個詞元與對應的注意力權重相乘,並累加到上下文向量中。
3.3.2 同時計算所有輸入詞元的注意力權重
接下來,我們將擴充套件這個過程,同時計算所有輸入詞元的上下文向量。
首先,我們計算所有輸入詞元之間的注意力分數:
attn_scores = torch.empty(6, 6)
for i, x_i in enumerate(inputs):
for j, x_j in enumerate(inputs):
attn_scores[i, j] = torch.dot(x_i, x_j)
print(attn_scores)
輸出結果如下:
tensor([[0.9995, 0.9544, 0.9422, 0.4753, 0.4576, 0.6310],
[0.9544, 1.4950, 1.4754, 0.8434, 0.7070, 1.0865],
[0.9422, 1.4754, 1.4570, 0.8296, 0.7154, 1.0605],
[0.4753, 0.8434, 0.8296, 0.4937, 0.3474, 0.6565],
[0.4576, 0.7070, 0.7154, 0.3474, 0.6654, 0.2935],
[0.6310, 1.0865, 1.0605, 0.6565, 0.2935, 0.9450]])
內容解密:
attn_scores = torch.empty(6, 6)初始化一個6x6的空張量,用於儲存注意力分數。for迴圈遍歷所有輸入詞元對,計算它們之間的點積,作為注意力分數。
我們也可以使用矩陣乘法來計算注意力分數:
attn_scores = inputs @ inputs.T
print(attn_scores)
輸出結果相同:
tensor([[0.9995, 0.9544, 0.9422, 0.4753, 0.4576, 0.6310],
[0.9544, 1.4950, 1.4754, 0.8434, 0.7070, 1.0865],
[0.9422, 1.4754, 1.4570, 0.8296, 0.7154, 1.0605],
[0.4753, 0.8434, 0.8296, 0.4937, 0.3474, 0.6565],
[0.4576, 0.7070, 0.7154, 0.3474, 0.6654, 0.2935],
[0.6310, 1.0865, 1.0605, 0.6565, 0.2935, 0.9450]])
圖示說明
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title LLM注意力機制深入解析與實作
package "機器學習流程" {
package "資料處理" {
component [資料收集] as collect
component [資料清洗] as clean
component [特徵工程] as feature
}
package "模型訓練" {
component [模型選擇] as select
component [超參數調優] as tune
component [交叉驗證] as cv
}
package "評估部署" {
component [模型評估] as eval
component [模型部署] as deploy
component [監控維護] as monitor
}
}
collect --> clean : 原始資料
clean --> feature : 乾淨資料
feature --> select : 特徵向量
select --> tune : 基礎模型
tune --> cv : 最佳參數
cv --> eval : 訓練模型
eval --> deploy : 驗證模型
deploy --> monitor : 生產模型
note right of feature
特徵工程包含:
- 特徵選擇
- 特徵轉換
- 降維處理
end note
note right of eval
評估指標:
- 準確率/召回率
- F1 Score
- AUC-ROC
end note
@enduml此圖示展示了從輸入詞元到上下文向量的計算流程。
綜上所述,我們成功地實作了自我注意力機制,並計算了所有輸入詞元的上下文向量。這些上下文向量將用於後續的自然語言處理任務。