第五章 ?模型架構
在前述章節中已經對預訓練數據的準備流程(第 4 章)進行了介紹。本章主 要討論大語言模型的模型架構選擇,主要圍繞 Transformer 模型(第 5.1 節)、詳細 配置(第 5.2 節)、主流架構(第 5.3 節)、長上下文模型(第 5.4 節)以及創新型 模型 5.5 節)等五個主要方面展開討論。表 5.1 列舉了一些典型的大語言模型的詳細配置。
表 5.1 大語言模型架構配置表(L 表示層數,N 表示注意力頭數,H 表示隱藏狀態的大小)
模型 | 類別 | 大小 | 歸一化 | 位置編碼 | 激活函數 | L | N | H |
GPT-3 | 因果 | 175B | Pre Layer | Learned | GELU | 96 | 96 | 12288 |
PanGU- | 因果 | 207B | Pre Layer | Learned | GELU | 64 | 128 | 16384 |
OPT | 因果 | 175B | Pre Layer | Learned | ReLU | 96 | 96 | 12288 |
PaLM | 因果 | 540B | Pre Layer | RoPE | SwiGLU | 118 | 48 | 18432 |
BLOOM | 因果 | 176B | Pre Layer | ALiBi | GELU | 70 | 112 | 14336 |
MT-NLG | 因果 | 530B | - | - | - | 105 | 128 | 20480 |
Gopher | 因果 | 280B | Pre RMS | Relative | - | 80 | 128 | 16384 |
Chinchilla | 因果 | 70B | Pre RMS | Relative | - | 80 | 64 | 8192 |
Galactica | 因果 | 120B | Pre Layer | Learned | GELU | 96 | 80 | 10240 |
LaMDA | 因果 | 137B | - | Relative | GeGLU | 64 | 128 | 8192 |
Jurassic-1 | 因果 | 178B | Pre Layer | Learned | GELU | 76 | 96 | 13824 |
LLaMA-2 | 因果 | 70B | Pre RMS | RoPE | SwiGLU | 80 | 64 | 8192 |
Pythia | 因果 | 12B | Pre Layer | RoPE | GELU | 36 | 40 | 5120 |
Baichuan-2 | 因果 | 13B | Pre RMS | ALiBi | SwiGLU | 40 | 40 | 5120 |
Qwen-1.5 | 因果 | 72B | Pre RMS | RoPE | SwiGLU | 80 | 64 | 8192 |
InternLM-2 | 因果 | 20B | Pre RMS | RoPE | SwiGLU | 48 | 48 | 6144 |
Falcon | 因果 | 180B | Pre Layer | RoPE | GELU | 80 | 232 | 14848 |
MPT | 因果 | 30B | Pre Layer | ALiBi | GELU | 48 | 64 | 7168 |
Mistral | 因果 | 7B | Pre RMS | RoPE | SwiGLU | 32 | 32 | 4096 |
Gemma | 因果 | 7B | Pre RMS | RoPE | GELU | 28 | 16 | 3072 |
DeepSeek | 因果 | 67B | Pre RMS | RoPE | SwiGLU | 95 | 64 | 8192 |
Yi | 因果 | 34B | Pre RMS | RoPE | SwiGLU | 60 | 56 | 7168 |
YuLan | 因果 | 12B | Pre RMS | RoPE | SwiGLU | 40 | 38 | 4864 |
GLM-130B | 前綴 | 130B | Post Deep | RoPE | GeGLU | 70 | 96 | 12288 |
T5 | 編-解 | 11B | Pre RMS | Relative | ReLU | 24 | 128 | 1024 |
5.1.Transformer 模型
????????當前主流的大語言模型都基于 Transformer 模型進行設計的。Transformer 是由 多層的多頭自注意力(Multi-head Self-attention)模塊堆疊而成的神經網絡模型。原 始的 Transformer 模型由編碼器和解碼器兩個部分構成,而這兩個部分實際上可以 獨立使用,例如基于編碼器架構的 BERT 模型和解碼器架構的 GPT 模型。 與 BERT 等早期的預訓練語言模型相比,大語言模型的特點是使用了更長的向量 維度、更深的層數,進而包含了更大規模的模型參數,并主要使用解碼器架構,對 于 Transformer 本身的結構與配置改變并不大。本部分內容將首先介紹 Transformer 模型的基本組成,包括基礎的輸入、多頭自注意力模塊和前置網絡層;接著分別 介紹 Transformer 模型中的編碼器和解碼器模塊。
5.1.1 ?輸入編碼
????????在 Transformer 模型中,輸入的詞元序列 ( = [
,
, . . . ,
?]) 首先經過一個 輸入嵌入模塊(Input Embedding Module)轉化成詞向量序列。具體來說,為了捕獲 詞匯本身的語義信息,每個詞元在輸入嵌入模塊中被映射成為一個可學習的、具有固定維度的詞向量
∈
?。由于 Transformer 的編碼器結構本身無法識別序列中元素的順序,位置編碼(Position Embedding, PE)被引入來表示序列中的位置信 息。給定一個詞元
?,位置編碼根據其在輸入中的絕對位置分配一個固定長度的 嵌入向量
∈
?。然后,每個詞元對應的詞向量和位置向量將直接相加,生成了 最終的輸入嵌入序列 X = [
, . . . ,
?],并且被傳入到后續層中:
????????????????????????????????????????????????????????????????????????? ? ??=
?+
?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??(5.1)
????????通過這種建模方法的表示,Transformer 模型可以利用位置編碼?建模不同詞元 的位置信息。由于不同詞元的位置編碼僅由其位置唯一決定,因此這種位置建模方式被稱為絕對位置編碼。盡管絕對位置編碼能夠一定程度上建模位置信息,然而它只能局限于建模訓練樣本中出現的位置,無法建模訓練數據中未出現過的位 置,因此極大地限制了它們處理長文本的能力。
5.1.2 ?多頭自注意力機制
????????多頭自注意力是 Transformer 模型的核心創新技術。相比于循環神經網絡(Re- current Neural Network, RNN)和卷積神經網絡(Convolutional Neural Network, CNN) 等傳統神經網絡,多頭自注意力機制能夠直接建模任意距離的詞元之間的交互關系。在卷積神經網絡中,只有位于同一個卷積核的窗口中的詞元可以直接進行交互,通過堆疊層數來實現遠距離詞元間信息的交換。
????????多頭自注意力機制通常由多個自注意力模塊組成。在每個自注意力模塊中,對 于輸入的詞元序列,將其映射為相應的查詢(Query, Q)、鍵(Key, K)和值(Value,V) 三個矩陣。然后,對于每個查詢,將和所有沒有被掩蓋的鍵之間計算點積。這 些點積值進一步除以 進行縮放(D是鍵對應的向量維度),被傳入到 softmax函數中用于權重的計算。進一步,這些權重將作用于與鍵相關聯的值,通過加權和的形式計算得到最終的輸出。在數學上,上述過程可以表示為:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?Q= ,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?(5.2)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? K= ,????????????????????????????????????????? (5.3)
????????????????????????????????????????????????????????????????????????????????????V= ,? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??(5.4)
????????????????????????????????????????????????Attention(Q,?K,?V)?= softmax( Q/
?)V.? ? ? ? ? ? ? ? ??(5.5)
????????與單頭注意力相比,多頭注意力機制的主要區別在于它使用了H組結構相同 但映射參數不同的自注意力模塊。輸入序列首先通過不同的權重矩陣被映射為一 組查詢、鍵和值。每組查詢、鍵和值的映射構成一個“頭”,并獨立地計算自注意 力的輸出。最后,不同頭的輸出被拼接在一起,并通過一個權重矩陣 ∈
?進行映射,產生最終的輸出。如下面的公式所示:
? ? ? ? ? ? ?????????????????????????????????MHA = Concat(, . . . ,
)?
??????????????????????????????????(5.6)
?????????????????????????????????????????????= Attention(
,
,??
).? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?(5.7)
????????由上述內容可見,自注意力機制能夠直接建模序列中任意兩個位置之間的關 系,進而有效捕獲長程依賴關系,具有更強的序列建模能力。另一個主要的優勢 是,自注意力的計算過程對于基于硬件的并行優化(如 GPU、TPU 等)非常友好, 因此能夠支持大規模參數的高效優化。
5.1.3 ?前饋網絡層
為了學習復雜的函數關系和特征,Transformer 模型引入了一個前饋網絡層
(Feed Forward Netwok, FFN),對于每個位置的隱藏狀態進行非線性變換和特征 提取。具體來說,給定輸入 x,Transformer 中的前饋神經網絡由兩個線性變換和 一個非線性激活函數組成:
????????????????????????????????????????FFN(X) = 𝜎( X?+
)
+
,????????????????????????????????????????????????????? (5.8)
其中 ∈
?和
∈
???分別是第一層和第二層的線性變換權重矩陣,
?∈
和
∈
?是偏置項,𝜎 是激活函數(在原始的 Transformer 中,采用 ReLU 作為激活函數)。前饋網絡層通過激活函數引入了非線性映射變換,提升了 模型的表達能力,從而更好地捕獲復雜的交互關系。
圖 5.1?? Transformer 架構圖
5.1.4 ?編碼器
在 Transformer 模型中,編碼器(Encoder)(圖 5.1 (a))的作用是將每個輸入 詞元都編碼成一個上下文語義相關的表示向量。編碼器結構由多個相同的層堆疊 而成,其中每一層都包含多頭自注意力模塊和前饋網絡模塊。在注意力和前饋網 絡后,模型使用層歸一化和殘差連接來加強模型的訓練穩定度。其中,殘差連接(Residual Connection)將輸入與該層的輸出相加,實現了信息在不同層的跳躍傳遞,從而緩解梯度爆炸和消失的問題。而 LayerNorm 則對數據進行重新放縮,提升模型的訓練穩定性。編碼器接受經過位置編碼層的 詞嵌入序列X作為輸入,通過多個堆疊的編碼器層來建模上下文信息,進而對于 整個輸入序列進行編碼表示。由于輸入數據是完全可見的,編碼器中的自注意力 模塊通常采用雙向注意力,每個位置的詞元表示能夠有效融合上下文的語義關系。 在編碼器-解碼器架構中,編碼器的輸出將作為解碼器(Decoder)的輸入,進行后 續計算。形式化來說,第 l 層(l∈ {1, . . . , L})的編碼器的數據處理過程如下所示:
????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????? ? ? ? ? ? ? ? ? ? ? ?(5.9)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??= LayerNorm(FFN(
) +
),
其中,?和
分別是該 Transformer 層的輸入和輸出,
是該層中輸入經過多頭注意力模塊后的中間表示,LayerNorm 表示層歸一化。
5.1.5 ?解碼器
????????Transformer 架構中的解碼器(圖 5.1 (b))基于來自編碼器編碼后的最后一層 的輸出表示以及已經由模型生成的詞元序列,執行后續的序列生成任務。與編碼 器不同,解碼器需要引入掩碼自注意力(Masked Self-attention)模塊,用來在計算 注意力分數的時候掩蓋當前位置之后的詞,以保證生成目標序列時不依賴于未來 的信息。除了建模目標序列的內部關系,解碼器還引入了與編碼器相關聯的多頭 注意力層,從而關注編碼器輸出的上下文信息 。同編碼器類似,在每個模塊之 后,Transformer 解碼器 也采用了層歸一化和殘差連接。在經過解碼器之后,模型 會通過一個全連接層將輸出映射到大小為 𝑉 的目標詞匯表的概率分布,并基于某 種解碼策略生成對應的詞元。在訓練過程中,解碼器可以通過一次前向傳播,讓 每個詞元的輸出用于預測下一個詞元。而在解碼過程,解碼器需要經過一個逐步 的生成過程,將自回歸地生成完整的目標序列。解碼器數據流程如下所示:
????????????????????????????????????????????????????????= LayerNorm(MaskedMHA(
) + Y_{l-1}),
???????????????????????????????????????????????????????????????????????????? = LayerNorm(CrossMHA(
,
?) +
?),? ? ? ? ? ? ? ? ? ? ? ? ? ? ??(5.10)?
????????????????????????????????????????????????????????????? ? ? ?????????????= LayerNorm(FFN(
) +
)
其中,?和
分別是該 Transformer 層的輸入和輸出,
和
是該層中輸入經過掩碼多頭注意力 MaskedMHA 和交叉多頭注意力 CrossMHA 模塊后的中間表 示,LayerNorm 表示層歸一化。然后將最后一層的輸入
映射到詞表的維度上:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?O?= softmax(?),? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?(5.11)? 其中,O∈
?是模型最終的輸出,代表下一個詞在詞表上的概率分布;
∈
是將輸入表示映射到詞匯表維度的參數矩陣,而
是概率化前的中間值,通常被稱為 logits。
5.2 ?詳細配置
自從 Transformer 模型公開發布以來,研究人員針對訓練穩定性、性能與 計算效率提升等方面提出了多種改進方法。本節主要探討了 Transformer 模型四個 核心組件的配置,包括歸一化、位置、激活函數和注意力機制,并介紹混合專家 結構。最后,我們將通過演示代碼對于 LLaMA 的模型實現進行介紹。
5.2.1 ?歸一化方法
大語言模型的預訓練過程中經常會出現不穩定的問題。為了應對這一問題,深 度學習方法通常會采用特定的歸一化策略來加強神經網絡訓練過程的穩定性。原 始的 Transformer 模型主要使用了層歸一化方法(Layer Normalization, LN)。隨著研究工作的不斷深入,基于層歸一化的改進技術不斷涌現,例如均方根層歸 一化(Root Mean Square Layer Normalization, RMSNorm)和 DeepNorm, 這些新技術已經在一些大語言模型中得到應用。
- LayerNorm. 在早期的研究中,批次歸一化(Batch Normalization, BN)是一種廣泛采用的歸一化方法。然而,該方法難以處理可變長度的序列數據和小 批次數據。因此,相關研究提出了層歸一化這一技術,針對數據進行逐層歸一化。
- ? RMSNorm. 為了提高層歸一化的訓練速度,RMSNorm僅利用激活值 總和的均方根 RMS(x) 對激活值進行重新縮放。使用 RMSNorm 的 Transformer 模 型相比于之前 LayerNorm 訓練的模型在訓練速度和性能上均具有一定優勢。采用 RMSNorm 的代表性模型包括 Gopher和 Chinchilla。下面給出了 Transformers 代碼庫中 LLaMA 的 RMSNorm 實現代碼:
class LlamaRMSNorm(nn.Module):def ?__init__ (self, hidden_size, eps=1e-6): super(). __init__()self.weight = nn.Parameter(torch.ones(hidden_size)) self.variance_epsilon = epsdef forward(self, hidden_states): input_dtype = hidden_states.dtypehidden_states = hidden_states.to(torch.float32) variance = hidden_states.pow(2).mean(-1, keepdim=True)# 計算隱狀態的均方根hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)# 將隱狀態除以其均方根后重新縮放return self.weight * hidden_states.to(input_dtype)
? ? DeepNorm. DeepNorm 由微軟的研究人員提出,旨在穩定深層 Trans- former 的訓練。具體而言,DeepNorm 在 LayerNorm 的基礎上,在殘差連接中對 之前的激活值 x 按照一定比例 進行放縮。通過這一簡單的操作,Transformer 的 層數可以被成功地擴展至 1,000 層,進而有效提升了模型性能與訓練穩定性。 其計算公式如下:
DeepNorm(x) = LayerNorm( · x + Sublayer(x)),? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??(5.16)
其中,Sublayer 表示 Transformer 層中的前饋神經網絡或自注意力模塊。GLM-130B采用了 DeepNorm 作為歸一化技術。
5.2.2 ?歸一化模塊位置
為了加強大語言模型訓練過程的穩定性,除了歸一化方法外,歸一化模塊的 位置也具有重要的影響。如圖 5.2(a) 所示,歸一化模塊的位置通常有三種選擇,分 別是層后歸一化(Post-Layer Normalization, Post-Norm)、層前歸一化(Pre-Layer Normalization, Pre-Norm)和夾心歸一化(Sandwich-Layer Normalization, Sandwich- Norm)。
- Post-Norm. Post-Norm 是在原始 Transformer 模型中所使用的一種歸一化技 術。其中,歸一化模塊被放置于殘差計算之后。其計算公式如下:
? ? ? ? ? ? ? ? ? ? ? ?Post-Norm(x)=Norm(x+Sublayer(x)),? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?(5.17) ????????????????????????????????????????????????????????????????????????????
????????其中,Norm 表示任意一種歸一化方法。在原理上,后向歸一化具有很多優勢。首 先,有助于加快神經網絡的訓練收斂速度使模型可以更有效地傳播梯度,從而減少訓練時間。其次,后向歸一化可以降低神經網絡對于超參數(如學習率、初始化參數等)的敏感性,使得網絡更容易調優,并減少了超參數調整的難度。然而,由于在輸出層附近存在梯度較大的問題,采用 Post-Norm 的Transformer 模型在訓練過程中通常會出現不穩定的現象。因此,現有的大語言模型中, PostNorm 很少被單獨使用,通常是與其他策略相結合應用。
? Pre-Norm. 與 Post-Norm 不同,Pre-Norm [164] 將歸一化模塊應用在每個子 層之前。其計算公式如下:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?Pre-Norm(x) = x + Sublayer(Norm(x)),? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??(5.18)
此處的 Norm 泛指任意一種歸一化方法。此外,Pre-Norm 在最后一個 Transformer 層后還額外添加了一個 LayerNorm。相較于 Post-Norm,Pre-Norm 直接把每個子層 加在了歸一化模塊之后,僅僅對輸入的表示進行了歸一化,從而可以防止模型的 梯度爆炸或者梯度消失現象。雖然使用了 Pre-Norm 的 Transformer 模型在訓練過 程中更加穩定,但是性能卻遜色于采用了 Post-Norm 的模型。盡管對于性能有一 定的影響,但由于其能夠有效維持訓練的穩定性,很多主流的大語言模型仍然采 用 Pre-Norm。
- Sandwich-Norm. 在 Pre-Norm 的基礎上,Sandwich-Norm在殘差連接之前增加了額外的 LayerNorm,旨在避免 Transformer 層的輸出出現數值爆炸的情況。 具體的實現方式如下所示:
Sandwich-Norm(x) = x +Norm(Sublayer(Norm(x))).? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??(5.19)
?????????????????????????????????????????????????????????????????????????????????????????????????
本質上,Sandwich-Norm 可以看作是 Pre-Norm 和 Post-Norm 兩種方法的組合,理 論上具有更加靈活的表達能力。但是研究人員發現,Sandwich-Norm 有時仍然無法保證大語言模型的穩定訓練,甚至會引發訓練崩潰的問題。
5.2.3 ?激活函數
前饋網絡中激活函數的選擇對于大語言模型的表現至關重要。通常來說,激 活函數主要是為神經網絡中引入非線性變化,從而提升神經網絡的模型能力。在 原始的 Transformer 中采用了 ReLU(Rectified Linear Unit)激活函數。該激活函數 計算較為簡單,僅僅是將對輸入中每個神經元和“零值”進行比較,并將小于零 的神經元的值設置為 0。然而,ReLU 可能會產生神經元失效的問題,被置為 0 的 神經元將學習不到有用的信息。ReLU??? ?函數的具體形式如下所示:
ReLU(x) = max(x, 0).???????????????????????????????????????????? (5.20)
針對 ReLU 存在的不足,研究人員進一步探索了 ReLU 函數的變種,以實現 更好的性能。Swish 激活函數將神經元和該神經元的 sigmoid 激活的乘積作為新的 激活函數。而 GELU(Gaussian Error Linear Unit)則利用標準高斯累積分布 函數作為激活函數,被很多的 Transformer 模型所采用。相比于原始的 ReLU 函數, 這些新的激活函數通常能夠帶來更好的性能并且收斂性更好,但是計算過程更為 復雜。Swish 和 GELU 與 ReLU 的對比如圖 5.2(b) 所示。
近來,大語言模型(例如 PaLM 和 LaMDA)也經常采用 GLU(Gated Linear Unit)激活函數以及它的變種,特別是 SwiGLU 和 GeGLU。不同于其他激活 函數,GLU 激活函數引入了兩個不同的線性層。其中一個線性層的輸出將被輸入 到一個激活函數(例如,GeGLU 采用 GELU 激活函數)中,其結果將和另一個線 性層的輸出進行逐元素相乘作為最終的輸出。相比于其他的激活函數,使用 GLU激活函數變體通常能夠帶來更佳的性能表現。SwiGLU 和 GeGLU 激活函數 的計算公式如下所示:
SwiGLU(x) = Swish() ⊙ (
),????????????????????????????????????????????????????????? (5.23)
GeGLU(x) = GELU() ⊙ (
),? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?(5.24)
? ? ? ? ? ? ? ?(a) 三種歸一化模塊位置? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??(b) 不同激活函數的示意圖
圖 5.2 ?歸一化和激活函數的示意圖
5.2.4 ?位置編碼
由于 Transformer 模型中自注意力模塊具有置換不變性,因此僅使用注意力機 制無法捕捉序列中的順序關系,從而退化為“詞袋模型”。為了解決這一問題,需 要引入位置編碼(Position Embedding, PE)對于序列信息進行精確建模,從而將絕 對或相對位置信息整合到模型中。
- ? 絕對位置編碼. 在原始的 Transformer 模型中,為了處理序列數據的順序信 息,采用了絕對位置編碼方法。在編碼器和解碼器的輸入端,根據輸入的詞元在 序列中的絕對位置生成唯一的位置嵌入,并與詞元的嵌入表示進行相加來注入位置信息。絕對位置編碼的公式如下所示:
??=
?+
?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??(5.25)
其中, 表示位置 t的位置嵌入,
是該位置詞元對應的詞向量。原始的 Trans- former 采用了正余弦位置編碼。該位置編碼在不同維度上預先定義了特定的正弦 或余弦函數,通過將詞元的絕對位置作為輸入代入這些函數,從而為這些維度賦予相應的值。此外,絕對位置編碼還可以采用可學習的嵌入表示,并被很多早期的預訓練語言 模型(如 BERT)廣泛采用。
- ? 相對位置編碼與絕對位置編碼不同,相對位置編碼是根據鍵和查詢之間的偏移量計算得來的。計算得到的相對位置編碼通常應用于注意力矩陣的計算中, 而不是直接與詞元本身的位置編碼進行相加。其中,Transformer-XL 提出了 一種相對位置編碼方法,在計算鍵和查詢之間的注意力分數時引入了相對位置信 息。對于使用絕對位置編碼的模型,其注意力值可以進行進一步的分解。而 Transformer-XL 對上述注意力值進行了改寫,使用相對位置信息代替絕對位置信息。作為另一種方法,T5提出了一種較為簡化的相對位置編碼。具體來說,它在注意力分數中引入了可學習的標量,這些標量是基于查詢和鍵的位 置之間的距離計算的。與絕對位置編碼相比,應用了相對位置編碼的 Transformer 模型常常可以對比訓練序列更長的序列進行建模,即具備一定的外推能力。
- 旋轉位置編碼(Rotary Position Embedding, RoPE). RoPE 巧妙地使用了基于絕對位置信息的旋轉矩陣來表示注意力中的相對位置信息。RoPE 根據位置信息為 序列中每個詞元所對應的設置了獨有的旋轉矩陣,并和對應的查詢和鍵進行相乘進行融合。利用旋轉矩陣中三角函數的特性,位置索引為 i的旋轉矩陣和位置索引為 j的旋轉矩陣的轉置的乘積等同于位置索引為它們相對距離i-j的旋轉矩陣,進一步,每個子空間定義了波長
?,即在該子空間上完成一個完整周期(2𝜋)旋轉所需的距離由于 RoPE 具有良好的性能以及長期衰減的特性,已經主流的大語言模型廣泛采 用,例如 PaLM ?和 LLaMA。這里給出了 Transformers 代碼庫中 LLaMA 的 RoPE 實現代碼:
def rotate_half(x):x1 = x[..., : x.shape[-1] // 2]x2 = x[..., x.shape[-1] // 2 :]# 將向量每兩個元素視為一個子空間return torch.cat((-x2, x1), dim=-1)def apply_rotary_pos_emb(q, k, cos, sin, position_ids): cos = cos[position_ids].unsqueeze(1)sin = sin[position_ids].unsqueeze(1)# 獲得各個子空間旋轉的正余弦值q_embed = (q * cos) + (rotate_half(q) * sin) k_embed = (k * cos) + (rotate_half(k) * sin)# 將每個子空間按照特定角度進行旋轉return q_embed, k_embed
? ALiBi 位置編碼: ALiBi 是一種特殊的相對位置編碼,主要用于增強 Transformer 模型的外推能力。具體來說,ALiBi 通過在鍵和查詢之間的距離上施 加相對距離相關的懲罰來調整注意力分數。 其與 T5 等模型中的相對位置編碼不同,ALiBi 中的懲罰分數是預先設定的,不需 要引入任何可訓練的參數。此外,ALiBi 展現出了優秀的外推性能,能夠對于超 過上下文窗口更遠距離的詞元進行有效建模。下面給出 Transformers 庫中 BLOOM 中的 ALiBi 代碼實現:
import math
import torchdef build_alibi_tensor(attention_mask: torch.Tensor,num_heads: int,dtype: torch.dtype,
) -> torch.Tensor:"""構建ALiBi(Attention with Linear Biases)位置偏置張量"""batch_size, seq_length = attention_mask.shape# 計算最接近且不大于num_heads的2的冪次方closest_power_of_2 = 2 ** math.floor(math.log2(num_heads))# 計算基礎衰減系數(公式中的beta參數)base = torch.tensor(2 ** (-(2 ** -(math.log2(closest_power_of_2) - 3))),device=attention_mask.device,dtype=torch.float32,)# 生成基礎指數序列(從1開始,長度=closest_power_of_2)powers = torch.arange(1,1 + closest_power_of_2,device=attention_mask.device,dtype=torch.int32,)# 計算基礎頭的懲罰系數(slopes)slopes = torch.pow(base, powers)# 處理非2的冪次方頭數的情況if closest_power_of_2 != num_heads:# 計算額外衰減系數(針對非2冪部分)extra_base = torch.tensor(2 ** (-(2 ** -(math.log2(2 * closest_power_of_2) - 3))),device=attention_mask.device,dtype=torch.float32,)# 計算需要補充的額外頭數(保證不超過closest_power_of_2)num_remaining_heads = min(closest_power_of_2, num_heads - closest_power_of_2)# 生成額外指數序列(奇數序列,長度=num_remaining_heads)extra_powers = torch.arange(1,1 + 2 * num_remaining_heads,2,device=attention_mask.device,dtype=torch.int32,)# 合并基礎系數和額外系數slopes = torch.cat([slopes, torch.pow(extra_base, extra_powers)], dim=0)# 生成位置索引張量(形狀:[batch_size, 1, seq_length])arange_tensor = ((attention_mask.cumsum(dim=-1) - 1) * attention_mask)[:, None, :]# 計算ALiBi偏置(廣播機制自動對齊維度)alibi = slopes[..., None] * arange_tensor # [batch_size, num_heads, seq_length]# 調整形狀以匹配注意力計算需求return alibi.reshape(batch_size * num_heads, 1, seq_length).to(dtype)
# 假設輸入參數
batch_size = 2
seq_length = 128
num_heads = 12 # 非2的冪次方示例
attention_mask = torch.ones((batch_size, seq_length)) # 全有效序列# 構建ALiBi張量
alibi_bias = build_alibi_tensor(attention_mask, num_heads, torch.float16)
print(alibi_bias.shape) # 應輸出:torch.Size([24, 1, 128])
5.2.5 ?注意力機制
注意力機制是 Transformer 架構中的核心技術,它能夠針對序列中的詞元對構 建交互關系,聚合來自于不同位置的語義信息。下面介紹四種常見的注意力機制 的設計方法。
- ? 完整自注意力機制. 在原始的 Transformer 模型中,注意力機制通過成對的方 式進行序列數據的語義建模,充分考慮了序列中所有詞元之間的相互關系。其中, 每個詞元在注意力計算中都需要對于其前序的所有詞元的鍵值對予以訪問,因此對于序列長度為 T 的序列需要 O(
) 的計算復雜度。此外,Transformer 還引入了 多頭注意力機制,將查詢、鍵和值在不同的語義空間進行線性投影,然后將每個頭 的輸出進行聚合形成最終的輸出。對于完整注意力的詳細介紹,可以參考第 5.1.2 節。
- ? 稀疏注意力機制. 盡管完整自注意力機制具有較強的建模能力,但是它需要 平方級的計算復雜性。在處理長序列時較為顯著,帶來了較大的計算和存儲開銷。 為了降低注意力機制的計算復雜度,研究人員提出了多種高效的注意力變種。其中,滑動窗口注意力機制(Sliding Window Attention, SWA)是大語言模型中使用 最多的一種稀疏注意力機制。不同于完整的注意力機制,滑動窗口注意力根據詞 元位置,僅僅將位置索引上距離該詞元一定范圍內的詞元考慮到注意力的計算中。 具體來說,滑動窗口注意力設置了一個大小為 w的窗口,對每個詞元
?,只對窗口內的詞元 [
?, . . . ,
??] 進行注意力計算,從而將復雜度降低到 O?(wT )。進一步,通過信息的逐層傳遞,模型實現了隨著層數線性增長的感受野,從而獲取遠 處詞元的信息。關于滑動窗口注意力詳細機制如圖 5.3 展示。
- ? 多查詢/分組查詢注意力. 為了提升注意力機制的效率,多查詢注意力提出針對不同的頭共享相同的鍵和值變換矩陣。這種 方法減少了訪存量,提高了計算強度,從而實現了更快的解碼速度(具體可以參詞元并且對于模型性能產生的影響也比較小。一些代表性的大語言模 型,如 PaLM 和 StarCoder,已經使用了多查詢注意力機制。為了結合多查詢注意力機制的效率與多頭注意力機制的性能,研究人員進一步提出了分組查 詢注意力機制(Grouped-Query Attention, GQA)。GQA 將全部的頭劃分為若干組,并且針對同一組內的頭共享相同的變換矩陣。這種注意力機制有效地平衡 了效率和性能,被 LLaMA-2 模型所使用。圖 5.4 展示了上述兩種注意力查詢機制。
? ?? ? ? ? ? ? ? ? ??
? ? ? (a) 滑動窗口注意力的掩碼矩陣? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?(b) 滑動窗口注意力信息的逐層傳遞
圖 5.3 ?滑動窗口注意力示意圖
圖 5.4 ?多頭注意力、分組查詢注意力和多查詢注意力示意圖
- ? 硬件優化的注意力機制. 除了在算法層面上提升注意力機制的計算效率,還可以進一步利用硬件設施來優化注意力模塊的速度和內存消耗。其中,兩個具有 代表性的工作是 FlashAttention?與 PagedAttention。相比于傳統的注意力 實現方式,FlashAttention 通過矩陣分塊計算以及減少內存讀寫次數的方式,提高 注意力分數的計算效率;PagedAttention 則針對增量解碼階段,對于 KV 緩存進行 分塊存儲,并優化了計算方式,增大了并行計算度,從而提高了計算效率。
圖 5.5 ?混合專家模型示意圖
5.2.6 ?混合專家模型
如第 2.2 節所述,大語言模型能夠通過擴展參數規模實現性能的提升。然而, 隨著模型參數規模的擴大,計算成本也隨之增加。為了解決這一問題,研究人員 在大語言模型中引入了基于稀疏激活的混合專家架構(Mixture-of-Experts, MoE), 旨在不顯著提升計算成本的同時實現對于模型參數的拓展。
在混合專家架構中,每個混合專家層包含K?個專家組件,記為 [,
, . . . ,
?], 其中每個專家組件
都是一個前饋神經網絡。對于輸入的每個詞元表示
,模 型通過一個路由網絡(或稱為門控函數)G來計算該詞元對應于各個專家的權重。 在路由函數中,首先通過線性層
?映射為 K個專家的得分,并基于此 選擇出概率最高的k?個專家進行激活。隨后,這 k個專家的得分將被送入 softmax 函數計算出它們的權重
?= [
?, . . . ,
??],沒有被選擇的專家權重將 被置為 0。上述路由網絡的計算過程如下式所示:
.???????????????????????????????????????????????????????????? (5.35)
之后,每個被選擇的詞元的輸出的加權和將作為該混合專家網絡層的最終輸出。
????????目前具有代表性的混合專家模型是 Mixtral (8×7B),該模型在 Mistral (7B) 的 基礎上,使用了混合專家模塊。具體來說,Mixtral 每一層都配備了 8 個專家(7B), 并對每個詞元選擇 2 個專家進行后續計算。在每次計算被激活的參數僅僅有 13B 的情況下,其性能超越了更熟規模更大的 LLaMA-2 (70B),進一步證明了混合專家架構的有效性。下面給出了 Mixtral 混合專家層的一個 PyTorch 示例代碼:
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Listclass MoeLayer(nn.Module):def __init__(self,experts: List[nn.Module],gate: nn.Module,num_experts_per_tok: int):"""混合專家層(Mixture-of-Experts Layer)實現Args:experts: 專家網絡列表,每個專家是一個nn.Module實例gate: 路由網絡,用于生成專家選擇概率num_experts_per_tok: 每個詞元選擇的專家數量"""super().__init__()assert len(experts) > 0, "至少需要提供一個專家網絡"self.experts = nn.ModuleList(experts) # 將專家列表轉換為ModuleListself.gate = gate # 路由網絡self.num_experts_per_tok = num_experts_per_tok # 每個token選擇的專家數def forward(self, inputs: torch.Tensor) -> torch.Tensor:"""前向傳播過程Args:inputs: 輸入張量,形狀為 [batch_size, seq_length, hidden_dim]Returns:輸出張量,形狀與輸入相同"""# 生成路由網絡的原始logits(未歸一化概率)gate_logits = self.gate(inputs) # 形狀 [batch_size, seq_length, num_experts]# 選擇top-k專家及其對應權重weights, selected_indices = torch.topk(gate_logits,self.num_experts_per_tok,dim=-1 # 在專家維度上進行選擇)# 對權重進行softmax歸一化(在專家選擇維度上)weights = F.softmax(weights, dim=-1, dtype=torch.float32).to(inputs.dtype)# 初始化輸出張量(與輸入形狀相同)outputs = torch.zeros_like(inputs)# 遍歷所有專家,計算加權輸出for expert_idx, expert in enumerate(self.experts):# 找到選擇當前專家的位置(batch和序列維度)batch_indices, seq_indices = torch.where(selected_indices == expert_idx)# 提取對應位置的輸入和權重selected_inputs = inputs[batch_indices, seq_indices]selected_weights = weights[batch_indices, seq_indices, expert_idx]# 計算專家輸出并加權累加expert_output = expert(selected_inputs)outputs[batch_indices, seq_indices] += (selected_weights[:, None] * expert_output)return outputs
使用示例:
# 定義示例專家網絡和路由網絡
class Expert(nn.Module):def __init__(self, hidden_dim):super().__init__()self.mlp = nn.Linear(hidden_dim, hidden_dim)def forward(self, x):return self.mlp(x)class Router(nn.Module):def __init__(self, hidden_dim, num_experts):super().__init__()self.router = nn.Linear(hidden_dim, num_experts)def forward(self, x):return self.router(x)# 創建MoE層
num_experts = 4
hidden_dim = 768
num_experts_per_tok = 2experts = [Expert(hidden_dim) for _ in range(num_experts)]
gate = Router(hidden_dim, num_experts)
moe_layer = MoeLayer(experts, gate, num_experts_per_tok)# 測試前向傳播
inputs = torch.randn(8, 128, hidden_dim) # [batch_size, seq_length, hidden_dim]
outputs = moe_layer(inputs)
print(outputs.shape) # 應該輸出 torch.Size([8, 128, 768])
5.2.7 ??????????????LLaMA 的詳細配置
綜合本節討論的內容,下面給出了關于模型詳細配置的推薦建議。首先,為 了增強模型的訓練穩定性,建議采用前置的 RMSNorm 作為層歸一化方法。其次, 在選擇激活函數時,為了獲得更優的模型性能,可以優先考慮使用 SwiGLU 或 GeGLU。最后,對于位置編碼,可以優先選擇 RoPE 或者 ALiBi,這兩種位置編碼 方法在建模長序列數據時通常能夠具有較好的性能。接下來,我們以 LLaMA 模型 的代碼實現,來介紹 Transformer 解碼器模型是如何進行模型搭建并且實現前向計 算的過程。
對于一個 LLaMA 模型,其首先將輸入的詞元序列通過詞嵌入矩陣轉化為詞 向量序列。之后,詞向量序列作為隱狀態因此通過 � ?個解碼器層,并在最后使 用 RMSNorm 進行歸一化。歸一化后的最后一層隱狀態將作為輸出。LLaMA 在 Transformers 庫中的整體實現如下所示:
import torch
import torch.nn as nn
from typing import Optional, Tuple, Union
from transformers.models.llama.modeling_llama import LlamaPreTrainedModel, LlamaConfig, LlamaDecoderLayer, LlamaRMSNorm
from transformers.file_utils import add_start_docstrings_to_model_forward
from transformers.modeling_outputs import BaseModelOutputWithPastclass LlamaModel(LlamaPreTrainedModel):def __init__(self, config: LlamaConfig):"""LLaMA 模型實現Args:config: 模型配置參數"""super().__init__(config)self.vocab_size = config.vocab_size # 詞表大小# 詞嵌入層(包含padding處理)self.embed_tokens = nn.Embedding(num_embeddings=config.vocab_size,embedding_dim=config.hidden_size,padding_idx=config.pad_token_id # 顯式指定padding索引)# 解碼器層堆疊self.layers = nn.ModuleList([LlamaDecoderLayer(config, layer_idx=i)for i in range(config.num_hidden_layers)])# RMSNorm歸一化層self.norm = LlamaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)# 預生成因果掩碼(用于自注意力)self.register_buffer("causal_mask",torch.full((config.max_position_embeddings, config.max_position_embeddings),fill_value=True,dtype=torch.bool).triu(diagonal=1) # 上三角矩陣(排除對角線))@add_start_docstrings_to_model_forward(Llama_INPUTS_DOCSTRING)def forward(self,input_ids: torch.LongTensor = None,attention_mask: Optional[torch.Tensor] = None,position_ids: Optional[torch.LongTensor] = None,**kwargs) -> Union[Tuple, BaseModelOutputWithPast]:"""模型前向傳播Args:input_ids: 輸入token ID序列 [batch_size, sequence_length]attention_mask: 注意力掩碼 [batch_size, sequence_length]position_ids: 位置ID序列 [batch_size, sequence_length]Returns:包含最終隱藏狀態的BaseModelOutputWithPast對象"""# 輸入嵌入處理if input_ids is not None:inputs_embeds = self.embed_tokens(input_ids)else:inputs_embeds = kwargs["inputs_embeds"]# 獲取序列長度seq_length = inputs_embeds.size(1)# 生成因果掩碼(根據當前序列長度動態調整)if self.training:# 訓練時使用預生成的完整掩碼causal_mask = self.causal_mask[:seq_length, :seq_length]else:# 推理時動態生成(適應不同序列長度)causal_mask = torch.full((seq_length, seq_length),fill_value=True,dtype=torch.bool,device=inputs_embeds.device).triu(diagonal=1)# 合并注意力掩碼(如果有)if attention_mask is not None:combined_mask = torch.logical_and(causal_mask.unsqueeze(0).expand(inputs_embeds.size(0), -1, -1),attention_mask.unsqueeze(-1).expand(-1, seq_length, seq_length))else:combined_mask = causal_mask.unsqueeze(0).expand(inputs_embeds.size(0), -1, -1)# 通過解碼器層hidden_states = inputs_embedsfor decoder_layer in self.layers:hidden_states = decoder_layer(hidden_states=hidden_states,attention_mask=combined_mask,position_ids=position_ids)[0] # 只取隱藏狀態輸出# 最終歸一化hidden_states = self.norm(hidden_states)return BaseModelOutputWithPast(last_hidden_state=hidden_states,)
使用示例:
from transformers import LlamaConfig# 創建模型配置
config = LlamaConfig(vocab_size=32000,hidden_size=4096,num_hidden_layers=32,num_attention_heads=32,max_position_embeddings=2048
)# 初始化模型
model = LlamaModel(config)# 生成測試輸入
input_ids = torch.randint(0, config.vocab_size, (4, 128)) # [batch_size, seq_length]# 前向傳播
outputs = model(input_ids)
print(outputs.last_hidden_state.shape) # 應該輸出 torch.Size([4, 128, 4096])
在每個解碼器層中,隱狀態首先通過層前的 RMSNorm 歸一化并被送入注意 力模塊。注意力模塊的輸出將和歸一化前的隱狀態做殘差連接。之后,新的隱狀 態進行 RMSNorm 歸一化,并送入前饋網絡層。和上面一樣,前饋網絡層的輸出同樣做殘差連接,并作為解碼器層的輸出。Transformers 庫中 LLaMA 每一層的代 碼實現如下所示:
class LlamaDecoderLayer(nn.Module):def __init__(self, config: LlamaConfig, layer_idx: int):super().__init__()self.hidden_size = config.hidden_sizeself.self_attn = LlamaAttention(config=config, layer_idx=layer_idx) # 注意力層self.mlp = LlamaMLP(config) # 前饋網絡層self.input_layernorm = LlamaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)self.post_attention_layernorm = LlamaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)# 注意力層和前饋網絡層前的 RMSNormdef forward(self,hidden_states: torch.Tensor,attention_mask: Optional[torch.Tensor] = None,position_ids: Optional[torch.LongTensor] = None,**kwargs,) -> Tuple[torch.FloatTensor, Optional[Tuple[torch.FloatTensor, torch.FloatTensor]]]:residual = hidden_stateshidden_states = self.input_layernorm(hidden_states)# 注意力層前使用 RMSNorm 進行歸一化hidden_states, self_attn_weights, present_key_value = self.self_attn(hidden_states=hidden_states,attention_mask=attention_mask,position_ids=position_ids,**kwargs,)# 進行注意力模塊的計算hidden_states = residual + hidden_states # 殘差連接residual = hidden_stateshidden_states = self.post_attention_layernorm(hidden_states)# 前饋網絡層前使用 RMSNorm 進行歸一化hidden_states = self.mlp(hidden_states) # 進行前饋網絡層的計算hidden_states = residual + hidden_states # 殘差連接outputs = (hidden_states,)return outputs
該實現遵循LLaMA的原始架構,包含:
- 詞嵌入層
- 32層Transformer解碼器
- RMSNorm歸一化
- 因果注意力掩碼
- 支持動態序列長度
可以通過調整LlamaConfig
中的參數來適配不同規模的LLaMA模型(7B, 13B, 70B等)。
參考:
?llmbook/LLMBook.pdf at main · datawhalechina/llmbook · GitHub