基于GitHub項目:https://github.com/datawhalechina/llms-from-scratch-cn
字節對編碼(BPE)
上一篇博文說到
為什么GPT模型不需要[PAD]
和[UNK]
?
GPT使用更先進的字節對編碼(BPE),總能將詞語拆分成已知子詞
為什么需要BPE?
-
簡單分詞器的問題:遇到新詞就卡住(如"Hello")
-
BPE的解決方案:把陌生詞拆成已知的小零件
BPE如何工作?
就像拼樂高:
-
基礎零件:先準備256個基礎字符(a-z, A-Z, 標點等)
-
拼裝訓練:統計哪些字符組合常出現
-
創建新零件:把高頻組合變成新"積木塊"
# 使用GPT-2的BPE分詞器
import tiktoken
tokenizer = tiktoken.get_encoding("gpt2")text = "Akwirw ier" # 模型沒見過的詞
integers = tokenizer.encode(text) # [33901, 86, 343, 86, 220, 959]# 查看每個部分的含義
for i in integers:print(f"{i} -> {tokenizer.decode([i])}")
# 33901 -> Ak
# 86 -> w
# 343 -> ir
# 86 -> w
# 220 -> (空格)
# 959 -> ier
舉例:
陌生詞 | BPE分解 | 計算機理解 |
---|---|---|
Akwirw | Ak + w + ir + w | "Ak"是已知前綴,"w"是字母,"ir"是常見組合 |
someunknownPlace | some + unknown + Place | 拆成三個已知部分 |
滑動窗口 - 文本的"記憶訓練法"
語言模型的核心任務:根據上文預測下一個詞。比如:“白日依山_",語言模型會根據上文推測出下文可能是“盡”,“水”……等,最終經過對比,選取最可能的詞填上,得到“白日依山盡”。
#用滑動窗口創建訓練數據
文本:"I had a cat" → 分詞后:[40, 367, 2885, 1464]# 滑動窗口(窗口大小=4)
輸入(x) 目標(y) 訓練內容
[40] → [367] 看到"I"預測"had"
[40, 367] → [2885] 看到"I had"預測"a"
[40,367,2885]→[1464] 看到"I had a"預測"cat"
?使用舉例:
from torch.utils.data import Dataset
from transformers import GPT2Tokenizerclass GPTDatasetV1(Dataset):def __init__(self, txt, max_length=256, stride=128): #stride控制相鄰片段間的重疊長度self.tokenizer = GPT2Tokenizer.from_pretrained('gpt2') #使用GPT-2原生分詞器self.tokenizer.pad_token = self.tokenizer.eos_token #用結束符代替填充符# 分詞得到ID序列,自動添加特殊標記(默認添加<|endoftext|>),但不會添加BOS(開始符)token_ids = self.tokenizer.encode(txt)# 用滑動窗口創建多個訓練片段self.examples = []for i in range(0, len(token_ids) - max_length, stride):input_ids = token_ids[i:i + max_length]target_ids = token_ids[i + 1:i + max_length + 1]self.examples.append((input_ids, target_ids))# 假設 token_ids = [1,2,3,4,5], max_length=3, stride=2# 窗口1:i=0 → input=[1,2,3], target=[2,3,4]# 窗口2:i=2 → input=[3,4,5], target=[4,5]def __len__(self):return len(self.examples)def __getitem__(self, idx):input_ids, target_ids = self.examples[idx]return input_ids, target_ids
當i + max_length +1
超過數組長度時,target_ids
會自動截斷(可能產生短序列)
優化方向建議:
- 動態填充:使用
attention_mask
區分真實token與填充 - 緩存機制:對大型文本文件進行分塊處理
- 長度統計:添加樣本長度分布分析功能
- 批處理優化:結合
collate_fn
處理變長序列
參數選擇指南:
- 短文本(<1k tokens):
max_length=64-128
,?stride=32-64
- 長文本(>10k tokens):
max_length=512-1024
,?stride=256-512
# 示例使用方式
text = "Self-explanatory knowledge, human intelligence, personal knowledge..." # 你的長文本數據
dataset = GPTDatasetV1(text)# 獲取第一個樣本
input_seq, target_seq = dataset[0]
詞元嵌入
之前的ID只是編號,沒有含義。嵌入層給每個詞從多個維度作向量表示
# 創建嵌入層(詞匯表大小=6,向量維度=3)
embedding = torch.nn.Embedding(6, 3)# 查看權重矩陣
print(embedding.weight)
"""
tensor([[ 0.3374, -0.1778, -0.1690], # ID=0的向量[ 0.9178, 1.5810, 1.3010], # ID=1的向量... # 以此類推], requires_grad=True) # 可學習!
"""
嵌入層的本質:
相當于高效版的"獨熱編碼+矩陣乘法":
獨熱編碼:[0,0,0,0,1] → 矩陣乘法 → [0.1, -0.5, 0.8]
嵌入層:直接取矩陣的第5行 → [0.1, -0.5, 0.8]
流程總結:原始文本->BPE分詞->ID序列->滑動窗口->訓練樣本->嵌入層->詞向量
單詞位置編碼
?為什么需要位置編碼?
簡單詞嵌入的局限:
-
詞嵌入只表示詞語含義,不包含位置信息
-
模型會把所有詞語當作無序集合處理
比如"貓追老鼠"和"老鼠追貓",雖然詞語相同但意思完全相反!
?位置編碼的解決方案
就像給教室座位編號:每個詞語有"含義身份證"(詞嵌入)->再加個"座位號"(位置編碼)
詞嵌入層實現
import torch
import torch.nn as nn# 定義詞嵌入層
token_embedding = nn.Embedding(num_embeddings=50257, embedding_dim=256)#num_embeddings參數表示嵌入字典的大小
#embedding_dim參數控制輸出向量的維度
位置嵌入層實現
# 定義位置嵌入層(假設序列最大長度為4)
pos_embedding = nn.Embedding(num_embeddings=4, embedding_dim=256)
生成位置編碼向量
# 生成位置編號0-3的序列
position_ids = torch.arange(4) # tensor([0, 1, 2, 3])# 獲取位置向量(形狀為[4, 256])
position_vectors = pos_embedding(position_ids)
組合詞嵌入與位置嵌入
# 假設輸入的token_ids形狀為[batch_size, seq_len]
token_vectors = token_embedding(token_ids)
final_embeddings = token_vectors + position_vectors.unsqueeze(0)
#廣播機制:
position_vectors
擴展為(batch_size, seq_len, embedding_dim)
,與token_vectors
維度對齊??
假設參數:
batch_size = 4 #4個樣本
seq_len = 16 #每個樣本有16個tokens
embedding_dim = 512 #詞嵌入為512維向量操作流程:
Token IDs形狀 : (4, 16)
↓ 經過嵌入層
Token向量形狀 : (4, 16, 512)
Position向量原始形狀 : (16, 512)
↓ unsqueeze(0)
Position向量調整后 : (1, 16, 512)
↓ 廣播相加
Final嵌入形狀 : (4, 16, 512)
- 位置嵌入通常需要擴展到與詞嵌入相同的維度
- 在Transformer架構中,位置嵌入可以是可學習的(如本例)或使用固定公式計算
?為什么用加法而不是拼接?
????????維度一致:保持向量維度不變(256維)
????????計算高效:加法比拼接更省資源
????????信息融合:位置和語義自然融合