阿里妹導讀
論文介紹
中文摘要:傳統的序列轉換模型使用複雜的迴圈或卷積神經網路,包括編碼器和解碼器。表現最好的模型會透過注意力機制連線編碼器和解碼器。
作者團隊提出了一種新的簡單網路結構,Transformer,完全基於注意力機制,不再使用迴圈和卷積。
在兩個機器翻譯任務上進行實驗,發現這些模型在質量上的表現優越,並且更容易進行平行運算,訓練所需時間明顯減少。
該模型在WMT 2014年英德翻譯任務上達到了28.4 BLEU,比現有最佳結果(包括整體模型)提高了超過2 BLEU。在WMT 2014年英法翻譯任務中,模型在八個GPU上訓練3.5天后,取得了新的單模型最佳BLEU分數41.8,訓練成本僅為文獻中最佳模型的一小部分。
展示出無論是在大量或有限的訓練資料下,Transformer在其他任務中的泛化能力,成功應用於英語組成句分析。
論文連結:https://arxiv.org/pdf/1706.03762.pdf
核心技術:模型架構(此處先留下大體印象 encode+decode)

LLM 淺談
很多人認為大模型可以直接回答問題或參與對話,但實際上,它們的核心功能是根據輸入的文字預測下一個可能出現的詞彙,即“Token”。這種預測能力使得LLM在各種應用中表現出色,包括但不限於:
文字生成:LLM可以生成連貫且有意義的文字段落,用於寫作輔助、內容創作等。
問答系統:透過理解問題的上下文,LLM能夠生成準確的回答,廣泛應用於智慧客服和資訊檢索。
翻譯:LLM可以根據上下文進行高質量的語言翻譯,支援多語言交流。
文字摘要:LLM能夠提取長文件的關鍵內容,生成簡潔的摘要,方便快速理解。
對話系統:LLM可以模擬人類對話,提供自然流暢的互動體驗,應用於聊天機器人和虛擬助手。
透過理解Token的概念,我們可以更好地掌握LLM的工作原理及其在實際應用中的強大能力。
Token
樣例
談到 token,不得不提到近期一個大模型被揪出來的低階錯誤“Strawberry裡有幾個 r”。

嘲笑之後,大家也冷靜了下來,開始思考:低階錯誤背後的本質是什麼?
大家普遍認為,是 Token 化(Tokenization)的鍋。
在國內,Tokenization 經常被翻譯成「分詞」。這個翻譯有一定的誤導性,因為 Tokenization 裡的 token 指的未必是詞,也可以是標點符號、數字或者某個單詞的一部分。比如,在 OpenAI 提供的一個工具中,我們可以看到,Strawberry 這個單詞就被分為了 Str-aw-berry 三個 token。在這種情況下,你讓 AI 大模型數單詞裡有幾個 r,屬實是為難它。
為了讓大家直觀地看到大模型眼裡的文字世界,Karpathy特地寫了一個小程式,用表情符號(emoji)來表示 token。

嘗試
Token 是 LLM 理解的文字基本單位。雖然將 Token 看作單詞很方便,但對 LLM 來說,目標是儘可能高效地編碼文字。所以在許多情況下,Token 代表的字元序列比整個單詞都要短或長。標點符號和空格也被表示為 Token,可能是單獨或與其他字元組合表示。LLM 詞彙中的每個 Token 都有一個唯一的識別符號,通常是一個數字。LLM 使用分詞器在常規文字字串和等效的 Token 數列表之間進行轉換。
import tiktoken
# 獲取適用於 GPT-2 模型的編碼器
encoding = tiktoken.encoding_for_model("gpt-2")
# 編碼示例句子
encoded_text = encoding.encode("A journey of a thousand miles begins with a single step.")
print(encoded_text)
# 解碼回原始文字
decoded_text = encoding.decode(encoded_text)
print(decoded_text)
# 單個 token 的編碼與解碼
print(encoding.decode([32])) # 'A'
print(encoding.decode([7002])) # ' journey'
print(encoding.decode([286])) # 'of'
# 編碼單詞 "Payment"
payment_encoded = encoding.encode("thousand")
print(payment_encoded)
# 解碼 "Payment" 的編碼結果
print(encoding.decode([400])) # 'th'
print(encoding.decode([29910])) # 'ousand'
[32, 7002, 286, 257, 7319, 4608, 6140, 351, 257, 2060, 2239, 13]
A journey of a thousand miles begins with a single step.
A
journey
of
[400, 29910]
th
ousand
預測下一個 Token
如上所述,給定一段文字,語言模型的任務是預測接下來的一個 Token。如果用 Python 虛擬碼來表示,這看起來就像這樣:
predictions = predict_next_token(['A', 'journey', 'of', 'a'])
這裡predict_next_token 函式接收一個由使用者提供的提示詞轉換成的一系列輸入 Token。在本例中,我們假設每個單詞都構成一個單獨的 Token。實際上,每個 Token 都會被編碼為一個數字,而非直接以文字形式傳入模型。函式的輸出是一個數據結構,其中包含了詞彙表中每一個可能的 Token 出現在當前輸入序列之後的機率值。
語言模型需要透過一個訓練過程來學會做出這樣的預測。訓練過程中,模型會接觸到大量的文字資料,從中學習語言模式和規則。訓練完成後,模型就能夠利用所學知識來估計任何給定 Token 序列之後可能出現的下一個 Token 的機率。
要生成連續的文字,模型需要反覆呼叫自身,每次生成一個新的 Token 並將其加入到已有的序列中,直至達到預設的長度。下面是一段更詳盡的 Python 虛擬碼,展示了這一過程:
defgenerate_text(prompt, num_tokens, hyperparameters):
tokens = tokenize(prompt) # 將提示轉換為 Token 列表
for _ in range(num_tokens): # 根據指定的 Token 數量重複執行
predictions = predict_next_token(tokens) # 獲取下一個 Token 的預測
next_token = select_next_token(predictions, hyperparameters) # 根據機率選擇下一個 Token
tokens.append(next_token) # 將選中的 Token 新增到列表中
return''.join(tokens) # 將 Token 列表合併為字串
# 輔助函式用於選擇下一個 Token
defselect_next_token(predictions, hyperparameters):
# 使用溫度(temperature)、top-k 或 top-p 等策略調整 Token 選擇
# 實現細節取決於具體的超引數設定
pass
在這段程式碼中,`generate_text` 函式接受一個提示字串、生成 Token 的數量以及一組超引數作為輸入。`tokenize` 函式負責將提示轉換成 Token 列表,而 `select_next_token` 函式則根據預測的機率分佈選擇下一個 Token。透過調整 `select_next_token` 中的超引數,比如溫度(temperature)、top-k 和 top-p,可以控制生成文字的多樣性和創造性。隨著迴圈的不斷迭代,新的 Token 不斷被新增到序列中,最終形成了連貫的文字輸出。
模型訓練
['I','you','love','oranges','grapes']
我們有三句話作為訓練資料:
-
I love oranges -
I love grapes -
you love oranges
我們可以想象一個5×5的表格,表格中的每個格子代表一個詞後面跟著另一個詞的次數。這個表格可能看起來像這樣:

但是,我們遇到了一個問題:“oranges”和“grapes”沒有出現在其他詞後面。這意味著,如果沒有其他資訊,模型將無法預測這兩個詞後面可能是什麼。為了解決這個問題,我們可以假設每個詞後面都可能跟著詞彙表中的任何其他詞,儘管這並不完美,但它確保了模型即使在訓練資料有限的情況下也能做出預測。
在現實世界中,大型語言模型(LLM)使用了大量的訓練資料,這減少了這種“漏洞”出現的機會。然而,由於某些詞的組合在訓練資料中出現得較少,LLM可能在某些情況下表現不佳,導致生成的文字雖然語法上正確,但邏輯上可能存在錯誤或不一致。這種現象有時被稱為“模型幻覺”。
上下文視窗
改進上下文視窗
然而,即使使用兩個 Token 的上下文視窗,生成的文字仍可能缺乏連貫性。為了生成更加一致且有意義的文字,需要進一步增加上下文視窗的大小。例如,將上下文視窗增加到三個 Token 將使機率表的行數增加到 125 行(5^3),但這仍然不足以生成高質量的文字。
隨著上下文視窗的增大,機率表的大小呈指數級增長。以 GPT-2 模型為例,它使用了 1024 個 Token 的上下文視窗。如果按上文中使用馬爾可夫鏈來實現這樣一個大的上下文視窗,每行機率表都需要代表一個長度在 1 到 1024 個 Token 之間的序列。對於一個包含 5 個 Token 的詞彙表,可能的序列數量為 5^1024,這是一個天文數字。這個數字太大了,以至於無法實際儲存和處理如此龐大的機率表。因此,馬爾可夫鏈在處理大規模上下文視窗時存在嚴重的可擴充套件性問題。
從馬爾可夫鏈到神經網路
顯然,使用機率表的方法在處理大規模上下文視窗時不可行。我們需要一種更高效的方法來預測下一個 Token。這就是神經網路發揮作用的地方。神經網路是一種特殊的函式,它可以接受輸入資料,對其進行一系列計算,然後輸出預測結果。對於語言模型而言,輸入是一系列 Token,輸出是下一個 Token 的機率分佈。
神經網路的關鍵在於其引數。這些引數在訓練過程中逐漸調整,以最佳化模型的預測效能。訓練過程涉及大量的數學運算,包括前向傳播和反向傳播。前向傳播是指輸入資料透過網路的各個層進行計算,生成預測結果;反向傳播則是根據預測結果與真實標籤之間的差異,調整網路的引數,以減小誤差。
現代語言模型,如 GPT-2、GPT-3 和 GPT-4,使用了非常深的神經網路,擁有數億甚至數萬億的引數。這些模型的訓練過程非常複雜,通常需要數週甚至數月的時間。儘管如此,訓練有素的 LLM 能夠在生成文字時保持較高的連貫性和一致性,這得益於其強大的上下文理解和生成能力。
Transformer 和注意力機制
Transformer 是目前最流行的神經網路架構之一,特別適用於自然語言處理任務。Transformer 模型的核心特點是其注意力機制。注意力機制允許模型在處理輸入序列時,動態地關注序列中的不同部分,從而更好地捕捉上下文資訊。
注意力機制最初應用於機器翻譯任務,目的是幫助模型識別輸入序列中的關鍵資訊。透過注意力機制,模型可以“關注”輸入序列中的重要 Token,從而生成更準確的翻譯結果。在語言生成任務中,注意力機制同樣發揮了重要作用,使得模型能夠在生成下一個 Token 時,綜合考慮上下文視窗中的多個 Token,從而生成更加連貫和有意義的文字。
總結來說,雖然馬爾可夫鏈提供了一種簡單的文字生成方法,但其在處理大規模上下文視窗時存在明顯的侷限性。現代語言模型透過使用神經網路和注意力機制,克服了這些侷限性,實現了高效且高質量的文字生成。
Transformer 的輸入

回到框架圖,Transformer中單詞的輸入表示x由單詞Embedding和位置Embedding (Positional Encoding)相加得到。
單詞的 Embedding 有很多種方式可以獲取,例如可以採用 Word2Vec、Glove 等演算法預訓練得到,也可以在 Transformer 中訓練得到。
Transformer 中除了單詞的 Embedding,還需要使用位置 Embedding 表示單詞出現在句子中的位置。因為 Transformer 不採用 RNN 的結構,而是使用全域性資訊,不能利用單詞的順序資訊,而這部分資訊對於 NLP 來說非常重要。所以 Transformer 中使用位置 Embedding 儲存單詞在序列中的相對或絕對位置。
簡而言之:以蘋果為例
“水果店裡有蘋果,香蕉”中蘋果代指水果,“商店裡最新推出了蘋果 16”中蘋果代表品牌
Self-Attention(自注意力機制)

上圖是論文中 Transformer 的內部結構圖,左側為 Encoder block,右側為 Decoder block。紅色圈中的部分為 Multi-Head Attention,是由多個 Self-Attention組成的,可以看到 Encoder block 包含一個 Multi-Head Attention,而 Decoder block 包含兩個 Multi-Head Attention (其中有一個用到 Masked)。Multi-Head Attention 上方還包括一個 Add & Norm 層,Add 表示殘差連線 (Residual Connection) 用於防止網路退化,Norm 表示 Layer Normalization,用於對每一層的啟用值進行歸一化。
因為 Self-Attention是 Transformer 的重點,所以我們重點關注 Multi-Head Attention 以及 Self-Attention,首先詳細瞭解一下 Self-Attention 的內部邏輯。
當然,我們可以用更簡潔的方式來理解Self-Attention機制。
Self-Attention 簡介
工作流程
-
轉換:
-
每個輸入元素(比如一個詞)都會被轉換成三個向量:Query (查詢)、Key (鍵) 和 Value (值)。這些向量是透過將輸入向量分別乘以三個不同的權重矩陣WQ、WK 和 WV 得到的。
-
計算注意力分數: -
對於每個元素,使用它的 Query 向量與所有其他元素的 Key 向量進行點積運算,得到一個分數列表。這個分數表示當前元素與其他所有元素的相關性。
-
歸一化: -
將這些分數透過 softmax 函式進行歸一化,得到一個機率分佈,表示當前元素對其他所有元素的注意力權重。
-
加權求和: -
使用這些注意力權重對所有元素的 Value 向量進行加權求和,得到最終的輸出向量。
公式
-
轉換:
-
計算注意力分數:
其中dk是Key向量的維度。
-
歸一化: Attention Weights=softmax(Scores) -
加權求和:Output=Attention Weights⋅V
Multi-Head Attention
-
多頭注意力(Multi-Head Attention)是為了讓模型從多個不同的角度捕捉資訊。具體做法是並行執行多個Self-Attention層(每個稱為一個“頭”),然後將所有頭的輸出拼接在一起,再透過一個線性變換。
總結
-
Self-Attention 讓模型能夠關注序列中的不同部分,從而更好地捕捉長距離依賴關係。 -
Multi-Head Attention 透過多個Self-Attention層增強模型的表達能力,使其能夠從多個角度綜合考慮資訊。
Add & Norm 和 Feed Forward
Add & Norm (殘差連線與層歸一化)
殘差連線 (Residual Connection)
-
作用:幫助模型更好地學習,防止訓練過程中資訊丟失。 -
方法:把輸入直接加到輸出上。
層歸一化 (Layer Normalization)
-
作用:讓資料更穩定,加快訓練速度。 -
方法:把每個樣本的特徵值調整到一個標準範圍內,通常是平均值為0,標準差為1。
結合使用
-
步驟:
-
先計算某一層的輸出 F(x)F(x)。 -
把輸入 xx 加到 F(x)F(x) 上,得到 y=F(x)+xy=F(x)+x。 -
對 yy 進行層歸一化,得到最終的輸出。
Feed Forward (前饋神經網路)
作用
-
增加非線性:讓模型更靈活,能處理更復雜的資料。
結構
-
兩層全連線網路:
-
第一層:把輸入透過一個線性變換(乘以一個矩陣),然後用 ReLU 啟用函式處理。 -
第二層:再透過一個線性變換(乘以另一個矩陣)。
總結
-
Add & Norm:透過殘差連線和層歸一化,讓模型更穩定,訓練更快。
-
Feed Forward:透過兩層全連線網路增加模型的靈活性,使其能處理更復雜的資料。
Decoder 結構

上圖紅色部分為 Transformer 的 Decoder block 結構,與 Encoder block 相似,但是存在一些區別:
-
包含兩個 Multi-Head Attention 層。 -
第一個 Multi-Head Attention 層採用了 Masked 操作。 -
第二個 Multi-Head Attention 層的K, V矩陣使用 Encoder 的編碼資訊矩陣C進行計算,而Q使用上一個 Decoder block 的輸出計算。 -
最後由一個 Softmax 層計算下一個翻譯單詞的機率。
附
# 匯入必要的庫
import torch
import torch.nn as nn
import torch.optim as optim
import math
# 定義位置編碼層,用於為輸入序列新增位置資訊
classPositionalEncoding(nn.Module):
def__init__(self, d_model, max_len=5000):
super(PositionalEncoding, self).__init__()
# 初始化位置編碼張量
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
# 計算正弦和餘弦值
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0).transpose(0, 1)
# 將位置編碼註冊為緩衝區
self.register_buffer('pe', pe)
defforward(self, x):
# 將位置編碼與輸入相加
x = x + self.pe[:x.size(0), :]
return x
# 定義基於Transformer的模型類
classTransformerModel(nn.Module):
def__init__(self, input_dim, output_dim, d_model=512, nhead=8, num_encoder_layers=6, dim_feedforward=2048, dropout=0.1):
super(TransformerModel, self).__init__()
self.model_type = 'Transformer'
# 定義嵌入層
self.embedding = nn.Embedding(input_dim, d_model)
# 定義位置編碼層
self.pos_encoder = PositionalEncoding(d_model)
# 定義編碼器層
encoder_layers = nn.TransformerEncoderLayer(d_model, nhead, dim_feedforward, dropout)
self.transformer_encoder = nn.TransformerEncoder(encoder_layers, num_encoder_layers)
self.d_model = d_model
# 定義解碼器層
self.decoder = nn.Linear(d_model, output_dim)
# 初始化權重
self.init_weights()
definit_weights(self):
initrange = 0.1
self.embedding.weight.data.uniform_(-initrange, initrange)
self.decoder.bias.data.zero_()
self.decoder.weight.data.uniform_(-initrange, initrange)
defforward(self, src, src_mask):
# 嵌入輸入並乘以根號下d_model
src = self.embedding(src) * math.sqrt(self.d_model)
src = self.pos_encoder(src)
# 編碼輸入
output = self.transformer_encoder(src, src_mask)
# 解碼輸出
output = self.decoder(output)
return output
# 生成一個上三角矩陣作為掩碼
defgenerate_square_subsequent_mask(sz):
mask = (torch.triu(torch.ones(sz, sz)) == 1).transpose(0, 1)
mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
return mask
# 示例用法
input_dim = 1000# 詞彙表大小
output_dim = 1000# 輸出維度
seq_length = 10# 序列長度
# 建立模型例項
model = TransformerModel(input_dim=input_dim, output_dim=output_dim)
# 示例資料
src = torch.randint(0, input_dim, (seq_length, 32)) # (序列長度, 批次大小)
src_mask = generate_square_subsequent_mask(seq_length)
# 前向傳播
output = model(src, src_mask)
print(output.shape) # 預期輸出: [序列長度, 批次大小, 輸出維度]
# 定義簡單的損失函式和最佳化器用於訓練
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 示例訓練迴圈
for epoch in range(10): # 迭代次數
optimizer.zero_grad()
output = model(src, src_mask)
loss = criterion(output.view(-1, output_dim), src.view(-1))
loss.backward()
optimizer.step()
print(f"Epoch {epoch+1}, Loss: {loss.item()}")