
1. 项目概述为什么Tokenization是LLM的基石如果你最近在折腾大语言模型无论是想自己微调一个还是想搞个RAG应用或者单纯想理解ChatGPT们是怎么“看懂”人话的那你大概率会反复遇到一个词Tokenization中文常叫“分词”或“词元化”。这玩意儿听起来平平无奇不就是把句子拆成词吗但我要告诉你在LLM的世界里它远不止“拆词”那么简单。它实际上是连接人类自然语言和机器可计算数学表示的第一座也是最关键的一座桥梁。你喂给模型的每一段文本无论是“你好世界”还是整本《三体》都必须先经过Tokenization这道工序变成一串数字ID模型才能开始工作。很多人觉得这步太基础直接调用tokenizer.encode()就完事了里面的细节是个黑盒。但当你开始处理中文长文本、纠结于上下文窗口不够用、或者发现模型对某些专业术语理解有偏差时你就会意识到问题很可能就出在这个“黑盒”里。不同的分词方式直接决定了模型看到的“世界”是什么样子也从根本上影响了模型的效率、效果和成本。今天我们就抛开那些高深的Transformer架构从最底层的Tokenization开始把这条从文本到向量的完整旅程彻底走通让你不仅会用更懂其所以然。2. 核心概念拆解Token、Tokenizer与词汇表在深入流程之前我们必须先统一几个核心概念。这些概念是理解后续所有内容的基础。2.1 什么是Token在传统NLP中“词”是最自然的语言单元。但在LLM的Tokenization里Token词元是一个更灵活的单位。它可以是一个完整的词如“apple”一个子词如“un”、“##fortunately”中的“##fortune”和“##ate”甚至是一个字符如“a”、“?”。关键在于Token是模型处理的基本原子。为什么不用完整的词主要问题在于词汇表爆炸和未登录词OOV。如果只用完整词词汇表会变得极其庞大英语可能超过百万且无法处理新词或拼写错误。子词分词Subword Tokenization完美地平衡了这两者常用词保持完整生僻词拆分成更小的、可重用的子词单元。2.2 词汇表模型的“字典”每个Tokenizer背后都对应一个词汇表Vocabulary。你可以把它想象成一本巨大的字典里面列出了所有模型认识的Token每个Token都有一个唯一的数字ID。例如在GPT系列模型中“hello”可能对应ID 12345“ world”可能对应ID 23456。这个词汇表是在模型训练前通过在大量语料上运行特定的分词算法如BPE统计构建出来的。词汇表的大小是一个关键超参数。太小如1k则每个Token承载信息过多语义模糊太大如10万则模型参数剧增计算和存储成本高昂且容易过拟合。常见的LLM词汇表大小在3万到10万之间。2.3 Tokenizer文本与ID的转换器Tokenizer就是一个实现分词算法、并持有词汇表的工具。它的核心工作有两个方向编码Encode将原始文本字符串如“Hello world!”转换为一串Token ID如[12345, 23456, 0]。解码Decode将一串Token ID转换回人类可读的文本字符串。这个过程并非简单的查字典。Tokenizer需要处理大小写、标点、空格、不同语言字符、甚至表情符号并遵循一套复杂的规则来决定如何拆分。例如空格通常会被处理成特殊Token如Ġ在BPE中但具体规则因Tokenizer而异。注意不同模型家族如GPT、BERT、T5的Tokenizer是不同的它们的词汇表和分词规则都针对其训练数据和模型架构进行了优化。千万不要混用Tokenizer和模型用BERT的Tokenizer去处理GPT的输入会导致灾难性后果。3. 主流分词算法深度剖析了解了“是什么”我们再来深挖“怎么做”。目前主流的LLM几乎都采用子词分词算法其中最具代表性的是以下三种3.1 Byte-Pair Encoding从数据压缩到NLP基石BPE可以说是当前LLM分词界的“扛把子”GPT系列、Llama系列等都使用它或其变种。它的核心思想非常巧妙从最基础的字符开始不断合并最高频的相邻符号对直到达到预设的词汇表大小。算法步骤详解初始化将训练语料中所有文本拆分成单个字符包括空格并统计每个字符的频率。此时词汇表就是所有字符的集合。迭代合并 a. 找出语料中相邻共现频率最高的一个符号对比如(h, e)经常一起出现。 b. 将这个符号对合并成一个新的符号比如he并加入到词汇表中。 c. 在语料中将所有出现的该符号对替换为这个新符号。 d. 重复步骤a-c直到合并次数即词汇表大小达到预设值。举个例子假设语料中有单词“low”5次、“lower”2次、“newest”6次、“widest”3次。初始词汇表{l, o, w, e, r, n, s, t, i, d}第一轮统计发现e和s共现了9次newest6次widest3次频率最高。合并它们得到新符号es词汇表新增一项。语料变为low,lower,n e s t,wid e s t这里用空格分隔符号。第二轮现在es和t共现了9次合并为est。词汇表新增est。如此反复最终可能会形成像low、lower、newest、widest这样的分词结果。BPE的优势与局限优势数据驱动能自适应地根据语料统计生成词汇表能有效平衡词表大小和Token序列长度。局限贪婪的合并策略可能不是全局最优对同一单词的不同形态如eat,ate,eating可能无法很好地关联。3.2 WordPieceBERT的沉默功臣WordPiece是BERT模型使用的算法整体流程与BPE非常相似但合并标准不同。BPE合并频率最高的对而WordPiece合并能最大程度提升语言模型概率的符号对。具体来说在每次合并时WordPiece会计算合并每一个候选符号对后对整个训练语料的似然值likelihood的提升。选择那个能带来最大似然值提升的符号对进行合并。这使它更紧密地与语言建模目标相结合。简单理解BPE问“谁最常在一起”WordPiece问“谁在一起最像一句‘人话’”。WordPiece的特点通常会在词前添加##来表示子词如playing可能被分为play和##ing便于区分词边界。由于合并标准更“语义化”在某些任务上表现略优于BPE但计算开销更大。3.3 Unigram Language Model逆向思维的分词法与前两者“自底向上”合并的思路相反Unigram LM是一种“自顶向下”的分词方法。它从一个巨大的种子词汇表比如包含所有常见词和子词开始逐步淘汰那些对整体语言模型似然值贡献最小的词元直到词汇表缩小到目标大小。算法思想初始化一个很大的词汇表。使用当前词汇表用维特比Viterbi算法找出训练语料中每个句子的最优分词方式。计算每个词元在语料中的损失loss即如果移除该词元语言模型似然值会下降多少。移除损失最小即最不重要的一批词元。重复步骤2-4直到词汇表达到目标大小。Unigram LM的优势非常灵活可以评估任何候选分词方案。能够输出每个可能分词结果的概率而不仅仅是唯一结果。SentencePiece工具默认采用此算法也可配置为BPE。三种算法对比速查表特性Byte-Pair Encoding (BPE)WordPieceUnigram Language Model核心思想自底向上合并高频相邻对自底向上合并最大似然提升对自顶向下淘汰最不重要词元合并/淘汰标准共现频率语言模型似然值提升语言模型似然值损失方向贪心合并贪心合并迭代淘汰代表性模型GPT, Llama, GPT-2/3/4BERT, DistilBERTSentencePiece (XLNet, ALBERT)优点简单高效普及度高分词结果与语言模型目标一致灵活可输出概率分布缺点贪婪策略非全局最优计算复杂度稍高初始化和迭代计算开销大4. 从Token ID到语义向量Embedding层的奥秘Tokenizer把文本变成了一串数字ID但模型神经网络并不能直接处理数字ID。它需要的是稠密、连续、蕴含语义信息的向量表示。这就是Embedding层登场的时候。4.1 Embedding层一个可查找的矩阵你可以把Embedding层想象成一个巨大的查找表Look-up Table。这个表的大小是[词汇表大小V, 隐藏维度D]。V就是你的词汇表大小比如50257GPT-2。D是模型的隐藏层维度比如768BERT-base或4096Llama2-7B。当模型拿到一个Token ID比如12345时它就去这个表的第12345行把那一整行的D个数字拿出来。这一行D维的向量就是这个Token的“嵌入向量”Embedding Vector。初始化与学习这个巨大的矩阵在训练开始时是随机初始化的。在模型训练过程中通过海量文本数据的反向传播这个矩阵中的数值会被不断调整。理想情况下语义相近的Token其向量在空间中的距离也会更近。例如“猫”和“狗”的向量距离应该比“猫”和“汽车”的近。4.2 位置编码给Token顺序感原始的Transformer模型和大多数早期LLM使用绝对位置编码。因为Transformer的自注意力机制本身不考虑顺序所以必须显式地注入位置信息。经典的正余弦函数位置编码Positional Encoding, PE为序列中的每个位置第1个Token第2个Token...计算一个唯一的、固定的向量然后把这个向量加到对应Token的嵌入向量上。$$ PE_{(pos, 2i)} \sin(pos / 10000^{2i/d_{model}}) $$ $$ PE_{(pos, 2i1)} \cos(pos / 10000^{2i/d_{model}}) $$其中pos是位置i是维度索引。这种编码的特点是能捕捉相对位置关系且能外推到比训练序列更长的位置尽管效果会衰减。现代LLM的演进旋转位置编码RoPE像GPT NeoX、Llama、GPT-4等现代模型广泛采用了旋转位置编码。RoPE的巧妙之处在于它不直接加一个位置向量而是通过旋转矩阵对Token嵌入向量进行变换旋转的角度与Token的位置相关。简单理解把Token向量想象成多维空间中的一个点RoPE根据这个Token在句子中的位置将这个点“旋转”一个特定的角度。位置不同旋转的角度不同。这样模型在计算注意力分数时内积运算就会自然地包含位置信息。RoPE的优势更好的长度外推性相对位置信息通过旋转角度的差值体现理论上可以处理任意长度的序列。保持向量模长旋转操作不改变向量的长度这在数学上更稳定。相对位置感知注意力机制能更自然地学会关注相对位置关系。4.3 完整的前向传播第一步现在我们可以串联起模型输入处理的全过程原始文本“人工智能改变世界”TokenizationTokenizer将其转换为Token IDs[101, 1234, 5678, 9012, 2333, 102]假设101是[CLS]102是[SEP]。嵌入查找每个ID在Embedding矩阵中找到对应的D维向量。得到形状为[6, D]的矩阵。位置编码为位置0,1,2,3,4,5生成位置向量或进行旋转与嵌入矩阵相加或变换。输出仍是[6, D]的矩阵。送入Transformer层这个蕴含了词汇信息和位置信息的矩阵被送入第一个Transformer编码器层开始真正的“理解”过程。5. 实操深入Hugging Face Transformers库的Tokenizer理论说了这么多不动手都是空谈。现在我们以最流行的transformers库为例深入看看Tokenizer在实际中如何工作。5.1 加载与探索Tokenizerfrom transformers import AutoTokenizer # 加载Llama2的Tokenizer tokenizer AutoTokenizer.from_pretrained(meta-llama/Llama-2-7b-hf) # 查看词汇表大小 print(f词汇表大小: {tokenizer.vocab_size}) # 通常是32000 # 编码一个简单句子 text Large Language Models are amazing! encoded tokenizer.encode(text) print(fToken IDs: {encoded}) print(fTokens: {tokenizer.convert_ids_to_tokens(encoded)})运行后你可能会看到类似输出Token IDs: [1, 1678, 11436, 2642, 526, 11234, 29892, 0] Tokens: [s, Large, ▁Language, ▁Models, ▁are, ▁amazing, !, /s]注意▁是一个特殊符号代表空格。s和/s是句子开始和结束的特殊Token。5.2 关键参数详解与避坑指南tokenizer.encode()和tokenizer()方法有许多参数理解它们至关重要。# 更常用的调用方式返回字典 inputs tokenizer( text, paddingTrue, # 填充到批次内最大长度 truncationTrue, # 截断到模型最大长度 max_length512, # 设定最大长度 return_tensorspt, # 返回PyTorch张量 add_special_tokensTrue # 添加特殊Token如s, /s ) print(inputs.keys()) # dict_keys([input_ids, attention_mask]) print(finput_ids shape: {inputs[input_ids].shape}) print(fattention_mask: {inputs[attention_mask]})input_ids就是Token ID序列。attention_mask注意力掩码。1表示真实Token0表示填充部分Padding。在计算注意力时模型会忽略掩码为0的位置。padding和truncation处理批量数据和不规则长度文本的生命线。务必根据你的任务场景设置。return_tensors指定返回框架ptfor PyTorch,tffor TensorFlow。弄错会导致后续模型输入报错。实操心得一关于max_length的陷阱模型有一个固定的model_max_length如Llama2是4096。但你的max_length参数应该始终小于它。因为max_length指的是你输入序列的Token数而模型需要一些空间来生成输出。通常我会设置为model_max_length - 预留空间如50-100。另外paddingmax_length会强制所有序列填充到max_length这在训练时常用但在推理时使用paddingTrue动态填充更高效。5.3 处理中文文本的特殊挑战英文等拉丁语系语言有天然的空格分隔而中文是连续字符串。这对BPE等算法是个挑战。text_zh 深度学习模型正在快速发展。 encoded_zh tokenizer.encode(text_zh) tokens_zh tokenizer.convert_ids_to_tokens(encoded_zh) print(f中文Tokens: {tokens_zh})输出可能令人困惑中文Tokens: [s, 深, 度, 学, 习, 模, 型, 正, 在, 快, 速, 发, 展, 。, /s]一个基于英文语料训练的Tokenizer很可能将中文字符全部拆成单个字。这是因为在它的BPE合并过程中中文字符的共现频率可能不足以让它们合并成词。解决方案使用针对中文优化的Tokenizer/模型如bert-base-chinese、chatglm系列、Qwen系列等。它们在预训练时使用了海量中文语料分词更合理。在训练前对中文进行预分词使用jieba、pkuseg等工具先进行粗粒度分词再将分词结果送入Tokenizer。这能显著提升模型对中文语义单元的理解。扩充词汇表如果你在微调一个英文基础模型处理中文任务可以考虑在词汇表中添加常见的中文词汇或子词。但这涉及修改Tokenizer和模型的嵌入层操作复杂。实操心得二长度计算的天差地别永远不要用len(text)来估算Token数量对于英文一个Token大约对应0.75个单词对于中文一个Token可能对应0.3到1.5个汉字取决于分词粒度。使用tokenizer.encode()后查看列表长度或者直接用tokenizer(text, return_lengthTrue)来获取精确的Token数。这是做上下文窗口管理、计算API调用成本如按Token收费的API的基础。6. 高级话题与性能优化掌握了基础我们来看看那些影响实际应用效果的高级问题和优化技巧。6.1 上下文窗口与长文本处理模型的上下文窗口Context Window由其架构和训练决定如Llama2是4kClaude 100k。一个序列的Token数不能超过这个限制。处理长文本的策略滑动窗口Sliding Window将长文本切成重叠的片段分别处理再合并结果。常用于RAG中的文档检索段落切割。层次化摘要Hierarchical Summarization先对段落或章节进行摘要再将摘要组合起来送入模型。这需要额外的摘要模型或提示工程。使用长上下文模型直接选用支持更长窗口的模型如GPT-4 Turbo128k、Claude-3200k。但成本更高。压缩位置编码/外推技术一些研究通过缩放位置索引如Linear Scaling、YaRN或微调位置编码让模型处理比训练时更长的序列。但这属于前沿研究稳定性待考。6.2 Tokenization对模型性能的隐形影响信息密度如果分词太细如字符级序列会很长计算开销大且模型需要学习更长的依赖关系。如果分词太粗词汇表庞大未登录词问题严重。跨语言迁移一个在英文语料上训练的Tokenizer处理中文效果差反之亦然。多语言模型如mBERT、XLM-R通过构建包含多种语言子词的大词汇表来解决但内部可能存在语言间的不平衡。领域适应性通用Tokenizer在处理医学、法律、代码等专业文本时会频繁将专业术语拆分成无意义的子词。领域微调Domain-adaptive Pretraining或使用领域语料重新训练Tokenizer能显著提升效果。6.3 自定义Tokenizer训练实战当你需要处理特定领域文本如古汉语、医学文献、程序代码时训练一个自定义Tokenizer可能是最佳选择。这里以tokenizers库Hugging Face为例展示训练一个BPE Tokenizer的简化流程。from tokenizers import Tokenizer from tokenizers.models import BPE from tokenizers.trainers import BpeTrainer from tokenizers.pre_tokenizers import Whitespace # 1. 初始化一个BPE模型 tokenizer Tokenizer(BPE(unk_token[UNK])) # 2. 设置预分词器这里用空格中文需用其他 tokenizer.pre_tokenizer Whitespace() # 3. 初始化训练器指定参数 trainer BpeTrainer( vocab_size30000, # 目标词汇表大小 special_tokens[[PAD], [UNK], [CLS], [SEP], [MASK]], # 特殊Token min_frequency2 # 词元出现的最小频率 ) # 4. 准备训练文件列表每个文件一行一个句子/文档 files [path/to/your/corpus.txt] # 5. 开始训练 tokenizer.train(files, trainer) # 6. 保存 tokenizer.save(my_custom_tokenizer.json) # 7. 使用 tokenizer.encode(Your domain specific text here.)关键决策点vocab_size根据语料大小和计算资源权衡。领域语料小词汇表可以小些如1万-2万。pre_tokenizer对于中文不能直接用Whitespace。可以考虑用BertPreTokenizer按字切分或先使用jieba分词再用空格连接。语料质量清洗你的语料去除乱码、重复、无关符号。语料质量直接决定Tokenizer质量。7. 常见问题排查与调试技巧在实际开发中Tokenizer相关的问题层出不穷。下面是一个快速排查指南。问题现象可能原因排查步骤与解决方案模型输出乱码或胡言乱语Tokenizer与模型不匹配1. 检查model和tokenizer的from_pretrained是否来自同一路径或同名模型。2. 确保没有意外混用不同家族的Tokenizer如用BERT的给GPT用。输入长度超出限制错误文本Token数超过model_max_length1. 使用tokenizer(text, return_lengthTrue)确认实际长度。2. 启用truncationTrue并合理设置max_length。3. 对于长文档实现前文提到的滑动窗口或摘要策略。处理速度极慢文本过长或批处理不当1. 对长文本进行预分割。2. 在批处理时使用paddingTrue而非paddingmax_length避免不必要的填充。3. 考虑使用tokenizers库的Rust后端它比纯Python实现快得多。中文被拆成单字效果差使用基于英文的Tokenizer处理中文1. 换用支持中文的模型如Qwen、ChatGLM、Yi。2. 对输入文本进行预分词jieba.cut后再送入Tokenizer。3. 高级对模型进行持续预训练融入中文词汇。特殊符号或表情处理异常词汇表未包含这些符号1. 检查tokenizer.convert_tokens_to_ids()看符号是否被识别为[UNK]。2. 可以考虑在输入前过滤掉这些符号或训练Tokenizer时加入相关语料。3. 使用更现代的Tokenizer如cl100k_base用于GPT-4它们对Unicode支持更好。微调后模型生成结果异常微调时数据处理与推理时不一致1.确保微调数据和推理数据使用完全相同的Tokenizer和预处理管道包括大小写、空格处理、特殊Token添加等。2. 检查训练时attention_mask是否正确应用。一个深度调试技巧可视化Attention当模型对某个输入理解出错时可以可视化该输入经过Tokenizer后的Tokens甚至查看第一层Transformer的注意力权重看看模型到底在关注哪些Token。这能帮你判断是分词不合理还是模型本身学习有问题。工具如BertViz可以帮助完成这项工作。Tokenization这条从文本到向量的旅程看似是LLM流水线上一个简单的预处理步骤实则暗藏玄机是模型理解能力的根基。理解它不仅能帮你避开无数坑更能让你在模型选型、数据处理、性能优化乃至问题调试上游刃有余。下次当你调用tokenizer.encode()时希望你能想起这背后的一整套复杂而精妙的系统正是它让冰冷的机器得以触碰人类语言温度的开端。