1. 从零理解大语言模型分词器的核心原理
作为一名长期从事自然语言处理开发的工程师,我经常需要向团队新人解释分词器的工作原理。很多人误以为分词只是简单的字符串切割,实际上它承载着文本向量化的关键桥梁作用。今天我就用最接地气的方式,带你从零实现一个完整的LLM分词器。
先看一个直观例子。当我们输入句子"I love NLP!"时,分词器会将其转换为:
code复制["I", "love", "NLP", "!"]
然后进一步映射为数字ID:
code复制[45, 102, 3047, 3]
这个转换过程为什么重要?因为神经网络本质上是在处理数字矩阵运算。想象你教小孩认字,必须先教他们认识单个汉字,然后才能组词造句。分词器做的就是这件事——把连贯的文本拆解为神经网络能"认识"的基本单元。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分词器的完整实现路径
2.1 文本预处理实战
让我们用Edith Wharton的短篇小说《The Verdict》作为示例文本。这个选择很巧妙:
- 公有领域无版权问题
- 2万字符长度适合教学
- 包含丰富的文学表达形式
python复制with open("the-verdict.txt", "r", encoding="utf-8") as f:
raw_text = f.read()
print(f"字符数:{len(raw_text)}") # 输出:20479
关键决策:保留原始大小写。虽然小写化能减少词汇量,但会丢失:
- 专有名词识别(如"John" vs "john")
- 句子开头的重要信号
- 特定语境下的强调含义
2.2 正则表达式分词的三阶段演进
第一阶段:基础空白分割
python复制re.split(r'(\s)', "Hello, world.")
# 输出:['Hello,', ' ', 'world.']
问题:标点粘连在单词上,不符合token化原则
第二阶段:加入标点处理
python复制re.split(r'([,.]|\s)', "Hello, world.")
# 输出:['Hello', ',', '', ' ', 'world', '.', '']
改进:使用过滤空白
python复制[item for item in result if item.strip()]
第三阶段:支持全部特殊字符
python复制pattern = r'([,.?_!"()\']|--|\s)'
re.split(pattern, 'Is this--a test?')
# 输出:['Is', ' ', 'this', '--', 'a', ' ', 'test', '?']
经验技巧:
- 双破折号
--需要单独处理 - 使用
strip()过滤空白字符 - 保留原始文本中的引号等特殊符号
2.3 词汇表构建的工程实践
对全文分词后得到4,649个token,去重后剩下1,159个唯一token。这就是我们的初始词汇表。
python复制all_tokens = sorted(list(set(preprocessed)))
vocab = {token:idx for idx,token in enumerate(all_tokens)}
重要设计选择:
- 按字母排序保证确定性
- 使用枚举保证ID连续
- 同时建立反向映射:
{id:token}
3. 完整分词器类实现
3.1 SimpleTokenizerV1核心结构
python复制class SimpleTokenizerV1:
def __init__(self, vocab):
self.str_to_int = vocab
self.int_to_str = {i:s for s,i in vocab.items()}
def encode(self, text):
tokens = re.split(r'([,.?_!"()\']|--|\s)', text)
tokens = [t.strip() for t in tokens if t.strip()]
return [self.str_to_int[t] for t in tokens]
def decode(self, ids):
text = " ".join(self.int_to_str[i] for i in ids)
return re.sub(r'\s+([,.?!])', r'\1', text)
3.2 编码解码实测
python复制text = '"It\'s the last he painted," she said.'
ids = tokenizer.encode(text) # [1, 58, 2, 872, 1013, ...]
decoded = tokenizer.decode(ids) # 完美还原原始文本
关键细节:
decode中的正则修复了标点前的多余空格- 引号等特殊字符被正确处理
- 保持原始文本的大小写和格式
3.3 处理未知词(OOV)的挑战
当遇到词汇表外的词时:
python复制tokenizer.encode("Hello newword") # KeyError!
这是简单分词器的主要局限,解决方案包括:
- 扩大训练语料规模
- 引入子词分割算法(BPE/WordPiece)
- 添加[UNK]特殊token
4. 生产级分词器的进阶思考
4.1 性能优化技巧
- 预编译正则表达式:
re.compile - 使用更高效的分割算法
- 对高频词建立缓存
4.2 多语言支持
- Unicode字符处理
- 语言特定规则(中文无空格)
- 混合语言文本分割
4.3 与嵌入层的协同
- Token ID与嵌入矩阵的对应关系
- 特殊token的处理([CLS],[SEP])
- 位置编码的配合
5. 从理论到实践的完整代码
python复制import re
from collections import defaultdict
class AdvancedTokenizer:
def __init__(self, corpus_files):
self.pattern = re.compile(r'([,.?_!"()\']|--|\s)')
self.vocab = self.build_vocab(corpus_files)
self.unk_token = "[UNK]"
def build_vocab(self, files):
token_counts = defaultdict(int)
for file in files:
with open(file, encoding='utf-8') as f:
text = f.read()
tokens = self._tokenize(text)
for token in tokens:
token_counts[token] += 1
# 按频率排序,保留前50000个
sorted_tokens = sorted(token_counts.items(),
key=lambda x: x[1], reverse=True)[:50000]
return {token:i for i,(token,_) in enumerate(sorted_tokens)}
def _tokenize(self, text):
tokens = self.pattern.split(text)
return [t.strip() for t in tokens if t.strip()]
def encode(self, text):
tokens = self._tokenize(text)
return [self.vocab.get(t, self.vocab[self.unk_token])
for t in tokens]
def decode(self, ids):
text = " ".join(self.int_to_str[i] for i in ids)
return re.sub(r'\s+([,.?!])', r'\1', text)
这个进阶版本增加了:
- 多文件词汇表构建
- 基于频率的词表裁剪
- 未知词处理机制
- 更健壮的代码结构
6. 分词器设计的核心考量
在实际项目中,选择或开发分词器时需要权衡:
-
粒度选择:
- 词级别:语义明确但词表大
- 字符级别:词表小但序列长
- 子词级别(BPE):平衡两者
-
语言特性:
- 英语:空格分割为主
- 中文:需要分词算法
- 德语:复合词处理
-
领域适配:
- 医学文本:保留专业术语
- 社交媒体:处理表情符号
- 代码:保留缩进等结构
7. 前沿发展与实用建议
现代大语言模型主要采用:
- BPE(Byte Pair Encoding)
- WordPiece
- SentencePiece
对初学者的实用建议:
- 从小规模文本开始实验
- 可视化检查分词结果
- 监控OOV比率
- 考虑使用预训练分词器
我在实际项目中踩过的坑:
- 忽略大小写导致实体识别失败
- 未处理连续空格影响格式
- 特殊符号编码问题
记住:好的分词器应该像优秀的翻译官——既忠实原文,又让机器能理解。这需要平衡语言学知识和工程实践。
