从零打造大语言模型--处理文本数据
从零打造大语言模型 · 第 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)