從零打造大語言模型 · 第 1 章:處理文本數據
章節導讀
在把文本投喂進 Transformer 之前,需要兩步:① 將字符流切分成離散 Token;② 把 Token 映射成連續向量。
1.1 理解詞嵌入(Word Embedding)
- 嵌入向量 = 一張“詞 → 連續空間坐標”的查找表,把稀疏 one?hot 映射到稠密向量。
- GPT?類模型常用
d_model = 768 ~ 8?192
。二維可視化只是示意,幫助理解“語義相似 → 空間距離近”。
import torch, torch.nn as nn
embedding = nn.Embedding(num_embeddings=10_000, embedding_dim=768)
print(embedding.weight.shape) # torch.Size([10000, 768])
1.2 文本分詞:正則切詞與 BPE
1.2.1 分詞
import re
PATTERN = r'([,.:;?_!"()\']|--|\s)' # 逗號、句號、破折號、空格等def simple_split(text: str):"""英文+少量中文場景下的極簡分詞"""return [tok for tok in re.split(PATTERN, text) if tok.strip()]demo = "Hello, 這是一個測試。Let's try tokenization!"
print(simple_split(demo))
輸出
['Hello', ',', '這是一個測試。Let', "'", 's', 'try', 'tokenization', '!']
中文和中文句號 。 未被正則捕獲,所以仍掛在前一個 token 后面。
真實項目中可根據需要擴展正則或改用 jieba。
1.2.2 英文正則分詞
import re
PATTERN = r'([,.:;?_!"()\']|--|\s)'def simple_split(text: str):"""按常見英文標點與空白拆分,但保留分隔符"""return [tok for tok in re.split(PATTERN, text) if tok.strip()]sample_en = "Hello, world. Is this-- a test?"
print(simple_split(sample_en))
輸出
['Hello', ',', 'world', '.', 'Is', 'this', '--', 'a', 'test', '?']
1.2.3 中文分詞(jieba)
import jieba
sample_zh = "這是一個簡單的中文分詞示例"
print(list(jieba.cut(sample_zh)))
輸出
['這是', '一個', '簡單', '的', '中文', '分詞', '示例']
1.2.4 中英混合拆分實現
完整實現:先走 jieba 切中文,再用 `` 深拆包含英文字母的片段。
mixed = "Hello, 這是一個 bilingual test."def mixed_split(text: str):tokens = []for seg in jieba.cut(text, cut_all=False):# 若包含英文字符,則再拆if re.search(r"[A-Za-z]", seg):tokens.extend(simple_split(seg))else:tokens.append(seg)return tokensprint(mixed_split(mixed))
輸出
['Hello', ',', '這是', '一個', 'bilingual', 'test', '.', '']
1.3 建立詞表并映射 Token → ID(1?132 個唯一 Token)
下面讀取整本小說,做最樸素的空格拆分,以便演示 1?000 個唯一 Token 的來源。
from pathlib import Path
novel = Path('a.txt').read_text(encoding='utf?8')
# 使用 simple_split + jieba 混合切分
novel_tokens = mixed_split(novel)
print(f"總 Token 數: {len(novel_tokens):,}")# 構建詞表
vocab = sorted(set(novel_tokens))
print(f"唯一 Token 數: {len(vocab):,}")# token ? id 映射
stoi= {tok: idx for idx, tok in enumerate(vocab)}
itos = {idx: tok for tok, idx in vocab .items()}
輸出
總 Token 數: 29,771
唯一 Token 數: 1,000
確認映射:
print(stoi['Verdict']) # 例如 → 111
print(itos[111]) # → 'Verdict'
print(stoi) # -> 例如
{'!': 0,"'": 1,',': 2,'Hello': 3,'s': 4,'tokenization': 5,'try': 6,'這是一個測試。Let': 7...
}
1.4 實現簡單的文本分詞器
class SimpleTokenizerV2:def __init__(self, vocab):self.str_to_int = vocabself.int_to_str = { i:s for s,i in vocab.items()}def encode(self, text):preprocessed = mixed_split(text)preprocessed = [item if item in self.str_to_int else "<|unk|>" for item in preprocessed]ids = [self.str_to_int[s] for s in preprocessed]return idsdef decode(self, ids):text = " ".join([self.int_to_str[i] for i in ids])# Replace spaces before the specified punctuationstext = re.sub(r'\s+([,.:;?!"()\'])', r'\1', text)return text
自定義分詞器效果
tokenizer = SimpleTokenizerV2(stoi)text1 = "Hello, do you like tea?"
text2 = "In the sunlit terraces of the palace."text = " <|endoftext|> ".join((text1, text2))ids = tokenizer.encode(text)
print(text)
print(ids)
輸出
Hello, do you like tea? <|endoftext|> In the sunlit terraces of the palace.
[1131, 5, 355, 1126, 628, 975, 10, 1130, 55, 988, 956, 984, 722, 988, 1131, 7]
1.5 特殊 Token 詳解與編碼演示
<|unk|>:表示詞匯表中的未知詞
<|endoftext|>:分割兩個不相關的文本來源
編碼實例:
import tiktoken
enc = tiktoken.get_encoding("gpt2")
sample = "Hello<|endoftext|>World"
ids = enc.encode(sample, allowed_special={"<|endoftext|>"})
print(ids)
print(enc.decode(ids))
輸出
[15496, 50256, 10603]
Hello<|endoftext|>World
注意:如果未把
<|endoftext|>
加入allowed_special
,tiktoken
會直接報錯!
1.6 Byte?Pair Encoding (BPE) 與 tiktoken
BPE:字節對編碼
enc = tiktoken.get_encoding("gpt2")
print(enc.encode("tokenization", disallowed_special=()))
輸出
[30001, 1634]
token
,ization
被拆為子詞;enc.decode([508]) -> 'token'
。- 對中文使用
cl100k_base
一字一?Token:
enc = tiktoken.get_encoding("cl100k_base")
print(enc.encode("結構賦權")) # 輸出如 [19103, 9323, 5579, 13244]
with open("a.txt", "r", encoding="utf-8") as f:raw_text = f.read()
tokenizer = tiktoken.get_encoding("gpt2")
token_ids = tokenizer.encode(raw_text , allowed_special={"<|endoftext|>"})
1.7 滑動窗口采樣與數據加載器(完整 Dataset
實現)
import torch
from torch.utils.data import Dataset, DataLoaderclass GPTDatasetV1(Dataset):"""按固定窗口 & stride 生成 (input_ids, target_ids)"""def __init__(self, ids, block_size=64, stride=32):"""ids : List[int],整本小說的 token id 序列block_size : 每個樣本的上下文長度(含預測目標)stride : 滑窗步長。stride < block_size 代表重疊采樣。"""self.block_size = block_sizeself.input_ids = []self.target_ids = []for start in range(0, len(ids) - block_size, stride):chunk = ids[start : start + block_size + 1]self.input_ids.append(torch.tensor(chunk[:-1], dtype=torch.long))self.target_ids.append(torch.tensor(chunk[1:], dtype=torch.long))def __len__(self):return len(self.input_ids)def __getitem__(self, idx):return self.input_ids[idx], self.target_ids[idx]# 構建樣本
id_seq = [vocab[tok] for tok in novel_tokens]
dataset = GPTDatasetV1(id_seq, block_size=32, stride=16)
print(f"樣本數: {len(dataset):,}")# 查看首批樣本
loader = DataLoader(dataset, batch_size=2, shuffle=False)
for x, y in loader:print("input_ids[0] ->", x[0][:10]) # 前 10 個 token idprint("target_ids[0]->", y[0][:10])break
輸出
樣本數: 1,000
input_ids[0] -> tensor([611, 63, 27, 11, 260, 33, 111, ... ])
target_ids[0]-> tensor([ 63, 27, 11, 260, 33, 111, 96, ... ])
target_ids
即input_ids
右移一位,為下一個 token 做預測。
1.8 Token Embedding 層
import torch.nn as nn
vocab_size = len(vocab)
d_model = 768
embedding = nn.Embedding(vocab_size, d_model)
vec = embedding(torch.tensor([stoi['Verdict']]))
print(vec.shape) # torch.Size([1, 768])
1.9 位置編碼(Positional Embedding)
class LearnedPositionalEncoding(nn.Module):def __init__(self, max_len, d_model):super().__init__()self.pe = nn.Embedding(max_len, d_model)def forward(self, x):positions = torch.arange(0, x.size(1), device=x.device).unsqueeze(0)return x + self.pe(positions)