Transformer入門必讀!從文本分詞到輸出概率的全解析 原創(chuàng) 精華
在當今的AI世界里,Transformer架構無疑是自然語言處理(NLP)領域的超級英雄。從ChatGPT這樣的聊天機器人,到翻譯工具,再到代碼生成器,Transformer都扮演著核心角色。它在2017年的論文《Attention is All You Need》中首次亮相,徹底改變了我們對語言的理解和生成方式。今天,我們就來深入解析Transformer和大型語言模型(LLMs)的核心機制,用簡單易懂的方式帶你領略它們的魅力。
Transformer和LLMs是什么?
想象一下,你在讀一本書,當看到“巴黎”時,你立刻就能聯(lián)想到“埃菲爾鐵塔”,即使它們相隔數(shù)頁。這就是Transformer的魔法——它能夠瞬間連接文本中的任意兩個詞,無論它們相隔多遠。Transformer是一種基于自注意力機制的神經(jīng)網(wǎng)絡架構,它可以讓模型同時考慮句子中的每一個詞(或稱為“token”),并根據(jù)上下文動態(tài)調(diào)整它們的權重。這種并行處理方式比傳統(tǒng)的循環(huán)神經(jīng)網(wǎng)絡(RNN)快得多,也更智能。
大型語言模型(LLMs)則是將Transformer架構擴展到極致的產(chǎn)物。它們擁有數(shù)十億甚至數(shù)千億的參數(shù)(可以理解為“神經(jīng)元”),并在海量的文本數(shù)據(jù)上進行訓練,比如整個互聯(lián)網(wǎng)上的文本內(nèi)容。像GPT-4、PaLM 2、LLaMA 3這樣的模型,不僅能寫詩、翻譯語言、總結書籍,還能通過逐步推理解決數(shù)學問題。
1. 文本分詞:把文本切成小塊
在Transformer處理文本之前,它需要先把文本切分成更小的單元,也就是“token”。這就好比把一個句子切成拼圖的碎片。簡單的分詞器會按照單詞來切分(比如“I love AI”會被切分成["I", "love", "AI"]),但現(xiàn)代的LLMs通常使用子詞分詞(subword tokenization),這樣可以更好地處理罕見單詞(比如“playing”會被切分成["play", "##ing"])。
為什么要這么做呢?因為分詞可以把原始文本映射到一個固定的詞匯表中,而不是面對一個無限大的單詞字典。子詞方案(比如“Un”、“bel”、“iev”、“able”、“!”)可以讓模型更好地處理新出現(xiàn)的單詞,同時保持詞匯表的規(guī)模可控。
不同的模型使用不同的分詞器:
- GPT-2/3/4:使用字節(jié)級BPE(Byte Pair Encoding),它可以處理任何Unicode字符,甚至連表情符號“??”都有對應的編碼。
- BERT / RoBERTa:使用WordPiece,會合并常見的詞片段。
- T5 / LLaMA:使用SentencePiece,直接在原始字節(jié)上學習合并規(guī)則,不依賴于特定語言。
分詞器是模型的一部分,如果換掉分詞器,整個模型的ID映射就會崩潰。分詞器還會注入一些特殊標記,比如[CLS],用來標記句子的開頭、分隔符或結尾。分詞的數(shù)量通常不等于單詞的數(shù)量,這也是為什么API定價和序列長度限制都是基于token的。
舉個例子,我們用text-davinci-002模型來分詞一個單詞“Unbelievable!”:
# Term: Unbelievable!
['Un', 'bel', 'iev', 'able', '!'] # Tokens
[3118, 6667, 11203, 540, 0] # Token IDs
# 注意:token的數(shù)量是5,而不是2!
這里有一個簡單的代碼示例,展示如何使用GPT-2的分詞器:
# 導入分詞器
from transformers import AutoTokenizer
# 要分詞的文本
text = "Unbelievable!"
# 初始化GPT-2分詞器
tok = AutoTokenizer.from_pretrained("gpt2")
# 分詞并打印結果
print(tok.tokenize(text)) # 輸出:['Un', 'bel', 'iev', 'able', '!']
2. 嵌入(Embeddings):給單詞賦予數(shù)字靈魂
分詞后的token ID只是數(shù)字,但Transformer需要更有意義的數(shù)字。嵌入層就是一個巨大的查找表(比如50,000 × 768),它將每個token映射到一個密集向量中,向量的方向編碼了語義信息。向量越接近,表示單詞的語義越相似。
舉個例子,想象一個三維星系,單詞是星星:國王(king)和王后(queen)在同一個星座中閃閃發(fā)光,而香蕉(banana)則在遠處軌道上運行。每個星星的坐標就是它的嵌入向量。
在預訓練過程中,模型會不斷調(diào)整這個矩陣,使得在相似上下文中出現(xiàn)的token向量更接近。這有點像Word2Vec或GloVe,但它們是與整個網(wǎng)絡一起學習的。
靜態(tài)嵌入(如Word2Vec、GloVe):每個單詞只有一個向量,不考慮句子上下文。 上下文嵌入(如Transformer):深層的輸出會細化基礎嵌入,比如“bank”在“river bank”和“savings bank”中的嵌入會有所不同。
我們用BERT模型來獲取一個“上下文嵌入”:
# 打印前5維的嵌入向量
Queen: [ 0.119-0.1380.2360.006-0.087]
King: [ 0.102-0.1380.262-0.051-0.118]
Toy: [ 0.333-0.1060.223-0.105-0.278]
這里有一個簡單的代碼示例,展示如何可視化GPT-2的token嵌入:
# !pip install torch transformers scikit-learn matplotlib --quiet
# =========================
# 1. 設置
# =========================
import torch
import numpy as np
import matplotlib.pyplot as plt
from transformers import AutoTokenizer, AutoModel
from sklearn.decomposition import PCA # 如果喜歡可以用TSNE替換
# =========================
# 2. 加載模型和分詞器
# =========================
model_name = "gpt2" # 使用的模型
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(
model_name,
torch_dtype=torch.float16, # 保持低內(nèi)存占用,同時不影響嵌入
low_cpu_mem_usage=True,
).eval() # 將模型置于評估模式
# =========================
# 3. 分詞并創(chuàng)建向量嵌入
# =========================
WORDS = ["Queen", "King", "Toy", "Man", "Women", "Lady"] # 要分詞的單詞
embed_vectors = []
for word in WORDS:
ids = tok(word, add_special_tokens=False)["input_ids"]
ids_t = torch.tensor(ids).unsqueeze(0) # 形狀:(1, tokens)
with torch.no_grad():
token_vecs = model.get_input_embeddings()(ids_t) # 形狀:(1, tokens, 768)
# 將子詞片段合并為單個768維向量(取均值即可)
embed_vectors.append(token_vecs.mean(dim=1).squeeze().cpu().numpy())
vecs = np.stack(embed_vectors) # 列表轉(zhuǎn)矩陣 | 形狀:(len(WORDS), 768)
# =========================
# 4. PCA(降維到2D)并可視化
# =========================
coords = PCA(n_compnotallow=2, random_state=0).fit_transform(vecs)
plt.figure()
for (x, y), label in zip(coords, WORDS):
plt.scatter(x, y)
plt.text(x + 0.02, y + 0.02, label)
plt.title("GPT-2 Token Embeddings (PCA → 2-D)")
plt.xlabel("PC-1"); plt.ylabel("PC-2"); plt.tight_layout(); plt.show()
# 打印向量的前幾維
for w, v in zip(WORDS, vecs):
print(f"{w:>5}: {np.round(v[:5], 3)}")
3. 位置編碼:記住順序
雖然Transformer可以并行處理所有token,但純嵌入向量并不知道誰在前面誰在后面。位置編碼的作用就是給每個token貼上一個獨特的“GPS定位”,讓注意力層能夠推理出順序關系。
位置編碼就像是在每個向量中輕聲提醒“我是第17個token”,從而防止Transformer變成一個簡單的詞袋模型,讓它能夠記住單詞的先后順序。
經(jīng)典的正弦余弦位置編碼公式(來自2017年的論文《Attention is All You Need》):為每個位置生成兩個不同頻率的波形,并將它們加到嵌入向量上。相鄰位置的模式相似,而遠處位置的模式則不同。
這里有一個簡單的代碼示例,展示正弦余弦位置編碼是如何工作的:
import torch
# 定義序列長度和嵌入維度
seq_len, d_model = 10, 8# seq_len:位置數(shù)量,d_model:嵌入維度
# 創(chuàng)建位置索引張量,形狀為(seq_len, 1)
pos = torch.arange(seq_len).unsqueeze(1) # [[0], [1], [2], ..., [9]]
# 創(chuàng)建偶數(shù)維度索引:[0, 2, 4, 6]
idx = torch.arange(0, d_model, 2)
# 計算每個維度的頻率/角度
# 隨著idx/d_model的增加,10000^(idx/d_model)呈指數(shù)增長
# 這樣就產(chǎn)生了從高頻率(小idx)到低頻率(大idx)的變化
angle_rates = 1 / (10000 ** (idx / d_model))
# 初始化位置編碼矩陣,形狀為(seq_len, d_model)
pe = torch.zeros(seq_len, d_model)
# 填充偶數(shù)維度為正弦值,奇數(shù)維度為余弦值
# pos * angle_rates通過廣播創(chuàng)建一個角度矩陣
pe[:, 0::2] = torch.sin(pos * angle_rates) # 偶數(shù)維度(0,2,4,6)為正弦值
pe[:, 1::2] = torch.cos(pos * angle_rates) # 奇數(shù)維度(1,3,5,7)為余弦值
4. 自注意力:上下文的超能力
想象一下,你在讀句子“The cat, which was hiding, pounced on the toy.”時,要理解“pounced”,你自然會考慮“cat”和“toy”這兩個詞的上下文,即使它們之間隔著其他單詞。自注意力機制就是模仿這個過程,讓模型中的每個單詞(或token)都能“查看”句子中的其他單詞,從而獲取上下文信息。
自注意力的核心問題是:“在編碼這個單詞時,我應該關注其他單詞的哪些部分?”例如,在編碼“pounced”時,模型可能會給“cat”(主語)分配較高的注意力權重,給“toy”(賓語)分配較高的權重,而對“which”或“hiding”這樣的單詞則分配較低的權重。這種動態(tài)加權方式,無論單詞之間的距離有多遠,都能讓Transformer靈活地捕捉到單詞之間的關系。
自注意力的步驟
(1)輸入嵌入 → 線性投影 → 查詢(Q)、鍵(K)、值(V)
在任何“注意力”發(fā)生之前,每個token的嵌入向量會被送入三個獨立的線性層,分別生成查詢向量(Q)、鍵向量(K)和值向量(V)。這些投影讓模型將相同的輸入嵌入重新寫入不同的“角色”:查詢向量(Q)問“我在找什么?”,鍵向量(K)說“我提供什么?”,值向量(V)則包含了要融合的實際內(nèi)容。
(2)計算原始未歸一化的注意力分數(shù)
對于每個token i,我們通過計算當前token i的查詢向量(q-vector)與其他所有token的鍵向量(k-vector)的點積,得到原始注意力分數(shù)。
(3)縮放與softmax操作
為了避免維度較大時原始分數(shù)過大,我們將其除以根號d(以確保數(shù)值穩(wěn)定性)。然后,我們對縮放后的原始分數(shù)進行softmax操作,使它們的總和為1,從而將分數(shù)轉(zhuǎn)換為權重。
(4)加權求和值向量,得到上下文嵌入
每個token的值向量(攜帶輸入token的實際信息)乘以其對應的注意力權重,然后將它們相加,得到一個單一的輸出向量。這就是模型從原始查詢的角度對序列的“融合”視圖。
5. 多頭注意力:多角度的視角
多頭注意力通過并行運行多個自注意力“頭”,進一步擴展了自注意力的能力。每個頭可以專注于句子的不同方面。例如,一個頭可能會關注語法結構,比如將“cat”與“pounced”聯(lián)系起來以確保主謂一致,而另一個頭則會捕捉語義聯(lián)系,比如將“toy”與“pounced”聯(lián)系起來以理解動作的目標。
如果token嵌入是一個大小為d_model(例如768)的向量,而你選擇了h個頭(比如12個),那么你會將這個向量分成h個相等的部分,每個部分的大小為d_model/h(這里就是768 ÷ 12 = 64)。每個頭獨立地將它的64維切片投影到自己的查詢、鍵和值上,并計算注意力。最后,你將所有h個頭的64維輸出拼接回一個d維向量(12 × 64 = 768),并將其傳遞下去。
通過結合這些不同的視角,模型能夠構建出更豐富、更細致的句子表示。
6. 交叉注意力:連接兩個世界
交叉注意力是Transformer中的一種機制,它允許解碼器(負責生成輸出的部分,比如GPT風格的LLMs)關注編碼器的輸出(負責理解輸入的部分)。在Transformer中,編碼器處理輸入序列(比如一個法語句子),并為每個token創(chuàng)建一個豐富的表示。解碼器在生成輸出(比如英語翻譯)時,使用交叉注意力來“查看”編碼器的表示,以決定下一步該說什么。
想象一下,你在翻譯法語短語“Je t’aime”為英語。編碼器讀取法語句子,并為每個單詞創(chuàng)建一個含義的總結。解碼器在寫出“I love you”時,使用交叉注意力來查看編碼器對“Je”(我)、“t’”(你)和“aime”(愛)的總結,以確保它選擇正確的英文單詞,并且順序正確。這就好比解碼器在寫翻譯時,向編碼器詢問“法語句子中這部分說了什么?”。
交叉注意力的作用在于,它將編碼器的理解與解碼器的生成緊密相連,確保輸出與輸入相關。如果沒有交叉注意力,解碼器就會盲目猜測,就像在不知道原始語言的情況下嘗試翻譯句子一樣。它在編碼器-解碼器Transformer(比如T5,用于翻譯)中非常重要,但在像GPT這樣的僅解碼器LLMs中則不太常見,因為它們專注于生成文本,而沒有一個明確的輸入序列可供關注。
7. 遮蔽注意力:保持未來的神秘
遮蔽注意力(也稱為因果注意力或遮蔽自注意力)是自注意力的一種變體,用于Transformer的解碼器中。它確保每個token只能“關注”它自己以及它之前的token,而不能關注它之后的token。這是因為在文本生成任務中,模型不應該“作弊”去查看它尚未生成的未來單詞。這對于僅解碼器的LLMs(如GPT)至關重要,因為它們是按順序生成文本的,而且在編碼器-解碼器模型(如T5)的解碼器中生成時也會用到。
想象一下,你在寫一部懸疑小說,寫到句子“The detective opened the...”時,你可以根據(jù)“detective”和“opened”來決定下一個詞(可能是“door”),但你不能偷看“door”或后面的詞,因為它們還沒有被寫出來。遮蔽注意力就是強制執(zhí)行這個規(guī)則,保持未來token的“隱藏”,讓模型像人類作家一樣一次生成一個詞。
在自注意力中,我們會計算序列中所有token之間的注意力分數(shù)。而在遮蔽注意力中,我們會應用一個遮罩來阻止對未來的token的關注。這是通過將未來token的注意力分數(shù)設置為一個非常小的數(shù)(例如-無窮大)來實現(xiàn)的,這樣在softmax步驟后,它們的權重就會變?yōu)榱恪=Y果是,每個token只能關注它自己和之前的token,確保了一個因果的、從左到右的流動。
遮蔽注意力是讓像GPT這樣的LLMs能夠?qū)懗鲞B貫、富有上下文意識的文本的關鍵,而不會偷看未來的內(nèi)容。它模擬了人類生成語言的方式——一次一個詞,基于已經(jīng)說過的內(nèi)容。在編碼器-解碼器模型中,它確保解碼器以邏輯順序生成輸出,同時交叉注意力將其與輸入相連。如果沒有遮蔽注意力,解碼器可能會“看到”未來的token,從而導致無意義的預測。
8. 前饋網(wǎng)絡 + 殘差連接
前饋網(wǎng)絡
在自注意力匯集了所有token之間的關系之后,每個token會獨立地通過一個小型的前饋網(wǎng)絡(Feed-Forward Network, FFN),以進一步深化其表示。可以想象,每個token就像一碗半熟的食材(比如“cat”或“sat”),而FFN就是廚師,它會將這碗食材加入調(diào)料(擴展表示)、以巧妙的方式混合(應用非線性激活函數(shù),如ReLU),并將其精美地裝盤(將維度還原到原來的大?。?。這個過程會根據(jù)注意力提供的上下文信息,細化token的含義,捕捉更復雜的模式。
具體來說:
- 擴展:通過一個線性層將維度從768擴展到3072。
- 激活:應用GELU非線性激活函數(shù)(其他模型可能會使用ReLU、Swish或更新的函數(shù))。
- 投影回原維度:通過另一個線性層將維度從3072還原到768。
這個MLP模塊不會混合token,它只是將每個向量重新映射到一個更豐富的空間,然后將其傳遞下去。
殘差連接
殘差(或跳躍)連接是Transformer中的一個重要特性。它通過將一個子層的輸入直接添加到其輸出中,讓深度網(wǎng)絡能夠?qū)W習增量變化(殘差),而不是完整的轉(zhuǎn)換。具體來說,它會將子層的輸入x傳遞給該子層的函數(shù)F(x),然后計算輸出為F(x) + x,而不是僅僅使用F(x)。
在標準的Transformer編碼器或解碼器塊中,你會看到這種模式兩次:
y1 = LayerNorm(MultiHeadAttention(x) + x)
y2 = LayerNorm(FeedForward(y1) + y1)
這里的每個“+x”或“+y1”都是一個殘差快捷方式,它保持了信息的流動,即使在塊變得更深時也不會受阻。
殘差連接的好處在于,它通過保持信息流動并減輕梯度消失問題,穩(wěn)定了訓練過程。這使得模型能夠?qū)W習到增量的變化,因此如果FFN的轉(zhuǎn)換沒有幫助,模型可以退回到輸入狀態(tài)。這不僅穩(wěn)定了訓練,還提高了性能。
9. 層歸一化:平衡交響樂
深度神經(jīng)網(wǎng)絡就像一場混亂的交響樂:當激活值在層與層之間流動時,它們的規(guī)模會發(fā)生變化,梯度會波動,訓練速度也會變慢。層歸一化(LayerNorm)通過讓每個token的向量在進入下一個子層之前自我標準化(均值接近0,方差接近1)來解決這個問題。
可以想象,一個40人的交響樂團暫停演奏,指揮悄悄調(diào)整每個樂器的音量,以確保沒有一個單獨的小號聲蓋過其他樂器。一旦平衡,交響樂就可以繼續(xù),聲音清晰且有控制。
層歸一化會重新調(diào)整每個token的特征向量,使其具有零均值和單位方差,然后立即讓模型學習它自己的首選比例和偏移量。這種“每個樣本的歸一化”穩(wěn)定了訓練過程,消除了批量大小帶來的問題,并且對于現(xiàn)代Transformer的深度和性能至關重要。
具體來說,對于一個維度為d的token:
- 計算d個數(shù)字的平均值μ和方差σ2。
- 從μ中減去,除以√(σ2 + ε),得到一個零均值、單位方差的向量。
它解決了以下問題:
- 內(nèi)部協(xié)變量偏移(每層的輸入在模型學習過程中不斷變化)。
- 穩(wěn)定梯度并加速訓練——尤其是在像Transformer這樣的非常深的網(wǎng)絡中,通過保持每層激活值在一個共同的規(guī)模上。
想象一下,每個token是一個學生的答卷。層歸一化就像是老師重新調(diào)整并重新中心化每個人的分數(shù),使得班級平均分為0,分布為1。這樣,“下一層的考官”就不會被完全不同的分數(shù)范圍所混淆。
這里有一個簡單的代碼示例,展示層歸一化是如何工作的:
import torch
from torch.nn import LayerNorm
# 創(chuàng)建一個張量,形狀為[batch=2, seq_len=2, dim=3]
# 每個序列有2個時間步/token,每個時間步有3個特征
x = torch.tensor([
[[1.0, 2.0, 3.0], [2.0, 4.0, 6.0]], # 第一個序列
[[0.5, 1.5, 3.5], [2.5, 5.0, 7.5]] # 第二個序列
]) # 形狀 (2, 2, 3)
# 應用層歸一化,作用于最后一個維度(對3個特征進行歸一化)
# eps防止除以零,affine=False表示沒有可學習的參數(shù)
ln = LayerNorm(normalized_shape=3, eps=1e-5, elementwise_affine=False)
# 使用層歸一化歸一化輸入張量
y = ln(x)
print("--------------\n輸入\n--------------\n", x)
print("--------------\n經(jīng)過層歸一化后的輸出\n--------------\n", y)
# 分析第一個序列(batch=0)的第一個時間步
print("--------------\n第一個序列,第一個時間步\n--------------\n", x[0,0])
sample = x[0,0] # 獲取 [1., 2., 3.]
mean = sample.mean()
print("\n均值:", mean.item()) # 應該是 2.0([1,2,3] 的平均值)
# 方差計算:與均值的平方差的平均值
# ((1-2)^2 + (2-2)^2 + (3-2)^2) / 3 = (1 + 0 + 1) / 3 = 0.6667
var = sample.var(unbiased=False)
print("方差:", np.round(var.item(), 4)) # 應該是 ~0.6667
# 層歸一化的公式是:(x - mean) / sqrt(variance + eps)
# 這將數(shù)據(jù)中心化到0,并使其具有單位方差
normalized = (sample - mean) / torch.sqrt(var + 1e-5)
print("\n手動歸一化的值:", normalized) # 手動歸一化的值:tensor([-1.2247, 0.0000, 1.2247])
# 看看沒有層歸一化會發(fā)生什么
print("\n--------------\n沒有層歸一化\n--------------")
# 創(chuàng)建一個尺度差異很大的張量
x_unscaled = torch.tensor([
[[0.001, 2000.0, -500.0], [0.002, 4000.0, -1000.0]], # 第一個序列 - 大尺度變化
[[0.003, 6000.0, -1500.0], [0.004, 8000.0, -2000.0]] # 第二個序列
])
# 應用層歸一化以查看差異
y_normalized = ln(x_unscaled)
print("\n尺度差異很大的輸入:\n", x_unscaled)
print("\n經(jīng)過層歸一化后(注意值已被歸一化):\n", y_normalized)
10. Transformer架構(編碼器和解碼器)
Transformer由編碼器和解碼器堆疊而成。編碼器負責處理輸入(例如理解文本),而解碼器負責生成輸出(例如文本生成)。每個堆疊包含多個注意力層、前饋網(wǎng)絡和歸一化層,它們協(xié)同工作。
編碼器是一個深度閱讀器,它分析輸入的每一個細節(jié)。解碼器是一個作家,它在參考編碼器的筆記時生成輸出。
可以想象一個法庭:編碼器是速記員,它聽取整個證詞并寫出一份整潔的記錄(上下文向量)。解碼器是法官,他在宣讀判決書的每一個詞時,都會參考這份記錄,確保每個新詞都與之前的判決詞以及記錄保持一致。
編碼器-僅編碼器 vs 僅解碼器 vs 編碼器-解碼器
- 僅編碼器(BERT):適用于分類、檢索、嵌入等任務。沒有交叉注意力——不生成文本。
- 僅解碼器(GPT):去掉編碼器堆疊;使用遮蔽自注意力,使每個token只能看到過去的內(nèi)容。非常適合文本補全和聊天。
- 編碼器-解碼器(T5、機器翻譯模型):當輸入≠輸出時(例如英語→法語)效果最佳。解碼器的交叉注意力允許它在每一步生成時關注源句子的任何部分。
編碼器層內(nèi)部
- 完整的自注意力(無因果遮罩):輸入中的每個token都可以關注其他所有token,構建一個豐富、雙向的上下文。
- 前饋網(wǎng)絡(擴展 → GELU → 投影回原維度):獨立地細化每個token。
- 在兩個子塊之后添加并應用層歸一化,保持穩(wěn)定的激活值。
解碼器層內(nèi)部
- 遮蔽自注意力:僅對已生成的token進行注意力計算(因果遮罩意味著每個位置t只能向左看——三角遮罩將“未來”token的注意力權重設置為-∞,防止“劇透”)。
- 交叉注意力:解碼器的查詢(Q)與編碼器的鍵(K)和值(V)進行交互,允許每個新詞參考源句子的任何部分。
- 前饋網(wǎng)絡(擴展 → GELU → 投影回原維度)。
- 每個子塊后添加并應用層歸一化。
11. 輸出概率和Logits:預測下一個token
在處理完成后,Transformer會輸出logits——詞匯表中每個可能的下一個token的原始分數(shù)。Softmax函數(shù)將這些分數(shù)轉(zhuǎn)換為概率,從而指導模型的預測。
詞匯表
詞匯表是模型所知道的所有離散token的固定集合——可以將其視為模型的“字母表”(但通常是子詞片段,而不僅僅是字母)。在每一步生成時,模型都會從這個有限的列表中選擇下一個token。詞匯表越豐富(例如50,000 vs 100,000 tokens),模型的表達就越精確。
以下是一個小的PyTorch代碼片段,展示如何查看GPT-2的詞匯表:
from transformers import AutoTokenizer
import numpy as np
# 初始化GPT-2分詞器,它包含詞匯表和編碼/解碼方法
tokenizer = AutoTokenizer.from_pretrained("gpt2")
# 打印GPT-2詞匯表的大小
print("GPT-2詞匯表大小:", tokenizer.vocab_size) # 應該是50257
# 獲取詞匯表作為(token, id)元組的列表,并隨機打亂
vocab_items = list(tokenizer.get_vocab().items())
np.random.shuffle(vocab_items)
# 打印詞匯表中的前10個隨機token-ID對
# 使用repr()顯示token,以顯示特殊字符/空格
print("\n詞匯表中的一些token→ID映射:")
for token, idx in vocab_items[:10]:
print(f"{idx:5d} → {repr(token)}")
在每一步生成時,模型不會直接輸出單詞——它會為詞匯表中的每個token生成一個logit,然后將這些logit通過softmax轉(zhuǎn)換為概率。
- Logits:原始的、無界的分數(shù)——每個詞匯表條目一個數(shù)字。
- Softmax:對這些分數(shù)進行指數(shù)化并歸一化,使它們的總和為1,從而得到一個有效的概率分布,表示“下一個token是什么”。
以下是一個小的PyTorch代碼片段,展示如何可視化GPT-2的輸出Logits和Softmax值:
# 導入所需庫
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch.nn.functional as F
# 初始化模型和分詞器
tokenizer = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2")
# 輸入文本,預測下一個token
text = "Hello, how are you"
# 對輸入文本進行分詞
inputs = tokenizer(text, return_tensors="pt")
# 獲取模型預測
with torch.no_grad(): # 推理時不需要跟蹤梯度
outputs = model(**inputs)
# 獲取最后一個token的logits
logits = outputs.logits[:, -1, :] # 形狀:[batch_size, vocab_size] -> torch.Size([1, 50257]) 在這個例子中
# 獲取softmax之前的前5個token的原始logits
top_k = 5
top_logits, top_indices = torch.topk(logits[0], top_k)
# 將logits轉(zhuǎn)換為概率
probs = F.softmax(logits, dim=-1)
# 獲取softmax之后的前5個概率
top_probs = probs[0][top_indices]
print(f"\n在'{text}'之后的下一個token的前{top_k}個預測:")
print("-" * 50)
print("原始Logits與Softmax概率")
print("-" * 50)
for logit, prob, idx in zip(top_logits, top_probs, top_indices):
token = tokenizer.decode([idx])
print(f"Token ID: {idx} | Token: {token:10} | Logit: {logit:.4f} | Probability: {prob:.4f}")
12. 解碼策略(溫度、Top-p等)
在Transformer模型處理輸入序列并為下一個token生成詞匯表上的概率分布后,解碼策略決定了如何從這個分布中選擇或采樣下一個token。在推理過程中,這個過程會自回歸地重復,以生成一系列token。解碼策略在連貫性、多樣性和計算效率之間進行了權衡。以下是一些關鍵的解碼策略及其參數(shù)(例如top-k、top-p)。
(1)貪婪解碼
在每一步中,貪婪解碼會選擇softmax輸出中概率最高的token。這種方法簡單、快速,并且能夠產(chǎn)生一致的結果。然而,它通常會生成重復或過于保守的文本,缺乏創(chuàng)造力或多樣性,因為它只關注最有可能的token。
(2)束搜索(Beam Search)
束搜索是一種用于生成文本的解碼策略,它通過同時探索多個可能的序列(稱為“束”)來找到一個高概率的單詞或token序列。與貪婪解碼不同,束搜索會同時考慮多個可能性,就像在迷宮中同時探索幾條路徑一樣,只保留最有希望的路徑,以達到一個好的目的地。
束搜索可以找到比貪婪解碼更連貫的序列,因為它考慮了多種可能性,但它的計算量比貪婪解碼要大。
例如,假設你正在生成句子“The cat...”,束寬度為2:
- 第一步:模型預測下一個token:“is”(0.4)、“sits”(0.3)、“runs”(0.2)。保留概率最高的2個束:“The cat is”和“The cat sits”。
- 第二步:對于“The cat is”,預測下一個token:“happy”(0.5)、“sleeping”(0.3)。對于“The cat sits”,預測:“on”(0.4)、“quietly”(0.2)。計算序列的整體概率,如“The cat is happy”、“The cat is sleeping”、“The cat sits on”等。
- 第三步:保留概率最高的2個序列(例如,“The cat is happy”和“The cat sits on”),然后重復。
- 最終:選擇整體概率最高的序列,例如“The cat is happy”。
(3)溫度采樣
語言模型有許多可能的下一個單詞。溫度是決定模型是堅持安全選擇還是冒險嘗試的“創(chuàng)造力調(diào)節(jié)器”。低值(≈0–0.3)會產(chǎn)生可預測的、幾乎是確定性的文本,而高值(>1)則鼓勵新穎和驚喜。
溫度采樣的工作原理如下:
- 模型為每個token輸出logits(原始分數(shù))。
- 將這些logits除以溫度值T:scaled = logits / T。
- 將縮放后的分數(shù)輸入softmax以獲得概率。
a.T = 1:對原始概率沒有變化。模型直接從其自然分布中采樣,平衡連貫性和多樣性。
b.T < 1(例如0.7):使分布更尖銳,給高概率token(可能的單詞)更多權重。這使得輸出更可預測、更專注、更連貫,但缺乏創(chuàng)造力。
c.T > 1(例如1.5):使分布更平坦,增加低概率token(不太可能的單詞)的機會。這使得輸出更具多樣性和創(chuàng)造力,但可能不太連貫或離題。
(4)Top-k采樣
Top-k采樣是語言模型保持創(chuàng)造力而不完全失控的“窄漏斗”。語言模型的詞匯表可能有50,000到250,000個token。從整個列表中采樣既耗時,又容易讓低質(zhì)量的長尾token混入,從而破壞流暢性和連貫性。通過限制為k個最佳選項,Top-k采樣可以快速生成文本,同時過濾掉無意義的單詞或離題的token。
Top-k采樣的機制如下:
- 模型輸出每個token的logits(z)。
- 保留logits最大的k個索引(z_topk)。
- 將所有其他logits設置為-∞,這樣softmax會將它們的概率歸零。
- 應用softmax:p_i = exp(z_i) / Σ exp(z_topk)。
- 從結果概率p中采樣一個token。
(5)Top-p(核)采樣
Top-p采樣只保留累積概率達到p(例如0.9)的最可能的token,并丟棄其余部分。因為截斷點會根據(jù)每個分布進行調(diào)整,所以它可以在模型自信時保持保守,在模型不確定時擴大選擇范圍。
Top-p采樣的工作原理如下:
- 按降序排列l(wèi)ogits。
- 通過softmax(或先按溫度縮放)將logits轉(zhuǎn)換為概率pi。
- 累積pi,直到運行總和≥p。
- 保留這個“核”中的token,將所有其他token設置為-∞,使其概率變?yōu)?。
- 重新歸一化并采樣。
總結
Transformer架構和LLMs的強大之處在于它們能夠并行處理文本,并通過自注意力機制捕捉單詞之間的復雜關系。從文本分詞到嵌入,再到位置編碼、自注意力、多頭注意力、交叉注意力、遮蔽注意力,以及最終的輸出概率計算和解碼策略,每一步都為模型提供了強大的語言理解和生成能力。這些技術的結合,使得今天的AI能夠以驚人的精度和創(chuàng)造力生成人類語言。
本文轉(zhuǎn)載自 ??Halo咯咯?? 作者:基咯咯
