大语言模型分词技术解析:从BPE到实战应用
发布时间:2026/8/26 22:18:53 作者:尧图编辑部 阅读量:1,286

1. 从“词”到“Token”理解大语言模型的第一道门槛如果你最近开始接触大语言模型无论是想自己动手微调一个还是单纯想理解ChatGPT、Claude这些模型到底是怎么“读”懂我们说的话的那么“Tokenization”分词/词元化这个概念就是你绕不开的第一个、也是最基础的技术环节。它远不止是把句子拆成单词那么简单而是决定了模型如何看待和理解文本世界的“世界观”。Stanford CS336这门关于大语言模型基础与对齐的课程把Tokenization作为开篇第一讲其重要性不言而喻——地基打不牢后面所有关于模型架构、训练、对齐的讨论都将是空中楼阁。简单来说Tokenization就是将一个原始的文本字符串比如“Hello, world!”转换成一串数字序列比如[15496, 11, 995, 0]的过程。这串数字就是模型真正“吃”进去的东西。你可能会觉得这不就是像查字典一样把单词映射成编号吗但现实要复杂和微妙得多。为什么“ChatGPT”可能会被拆成[Chat, G, PT]三个token为什么同一个中文句子用不同的分词器会得到完全不同的token序列这些选择背后直接影响了模型的词汇量大小、训练效率、对生僻词或新词的处理能力甚至决定了模型在某些语言上的表现优劣。今天我们就抛开复杂的数学公式从工程实践和模型设计的角度深入聊聊Tokenization的那些核心门道、常见陷阱以及在实际项目中的选型思考。2. 为什么需要Tokenization不仅仅是压缩文本在深入技术细节之前我们必须先回答一个根本问题为什么大语言模型不直接处理字符比如英文字母a-z或Unicode码点直观上看字符序列是最自然的表示26个英文字母加上标点词汇表vocabulary极小似乎很简单。但这样做模型将面临巨大的学习挑战。2.1 字符级模型的困境效率与语义的缺失想象一下如果模型以字符为单位学习。单词“apple”需要模型从5个连续的字符a,p,p,l,e中自己归纳出这是一个表示“苹果”的语义单元。这要求模型具备非常强大的序列建模能力去捕捉这种远距离的字符组合模式。更重要的是这极其低效。模型需要处理的序列长度会变得非常长一篇文章可能有数万个字符而Transformer架构的核心注意力机制的计算成本与序列长度的平方成正比。处理一个长文档字符级表示带来的计算开销将是灾难性的。此外字符本身几乎不携带明确的语义信息。字母p单独出现时你无法推断任何意思。模型必须从海量的字符共现中费力地重新“发明”出单词和词根的概念这相当于让模型从零开始重建一门语言的词汇体系是对训练数据和学习能力的巨大浪费。2.2 词级模型的局限词汇表爆炸与未知词问题那么直接用空格分割的单词Word-level作为token如何这确实解决了字符级的语义模糊问题“apple”作为一个整体token其语义是明确的。但这种方法有两大硬伤词汇表爆炸Vocabulary Explosion一种语言中的单词数量是巨大的并且是开放的。想想英语中的各种时态play, plays, playing, played、复数、所有格以及无数的专业术语和网络新词。如果每个词形都作为一个独立的token词汇表大小会轻易膨胀到几十万甚至上百万。这会导致两个问题一是模型嵌入层Embedding Layer的参数会变得极其庞大占用大量内存二是每个token在训练初期被见到的次数非常少学习不充分模型泛化能力差。未知词Out-of-Vocabulary, OOV问题无论你把词汇表设得多大总会有新的、没见过的单词出现。对于这些OOV词传统的词级模型通常用一个特殊的UNKunknowntoken来替代。这意味着模型完全失去了对这个词的信息句子“I used the latest框架名”如果框架名被替换成UNK整个句子的含义就可能丢失或扭曲。2.3 子词折衷方案Byte Pair Encoding (BPE) 的崛起正是为了在“字符的灵活性”和“单词的语义性”之间取得平衡子词分词Subword Tokenization成为了主流。其核心思想是将单词拆分成更小的、有意义的片段子词这些片段既能覆盖大多数常见单词作为整体也能通过组合来表示罕见词或新词。其中Byte Pair Encoding (BPE)是当前最流行、影响最深远的算法被GPT系列、RoBERTa等众多顶尖模型采用。它的思想非常巧妙来源于数据压缩领域。BPE的训练过程可以概括为以一个巨大的文本语料库作为输入。初始时将每个单词拆分成字符包括一个特殊的单词结束符如/w并统计所有相邻字符对bigram的频率。例如“low”初始为l, o, w, /w字符对有(l, o),(o, w),(w, /w)。找到语料库中出现频率最高的字符对比如e和s经常连在一起出现将它们合并成一个新的符号比如es。将这个新符号加入词汇表并在所有单词中用这个新符号替换原来的字符对。重复步骤3和4直到合并了预定的次数即词汇表达到预定大小。通过这个过程高频的字符组合如ing,ed,tion,pre会被优先合并成子词。最终常见单词如“playing”可能被整体保留为一个token而罕见单词“tokenization”则可能被拆分为token,ization两个已知的子词token。这完美解决了词汇表爆炸和OOV问题词汇表大小可控通常5万-10万且任何新词理论上都可以用已有的子词拼凑出来。注意BPE有一个关键变体是Byte-level BPE (BBPE)由GPT-2引入。它不是在Unicode字符级别进行合并而是在字节Byte级别进行。这使得它的词汇表很小256个字节作为基础但通过合并可以表示任何Unicode文本实现了真正的“全字符集覆盖”在多语言混合文本处理上表现更鲁棒。这也是当前许多先进模型的选择。3. 主流分词算法巡礼BPE、WordPiece与Unigram虽然BPE是事实上的霸主但了解其“竞争对手”有助于我们理解不同设计哲学。在实际项目中选择哪种分词器往往是“拿来主义”——直接使用预训练模型配套的分词器。但知其所以然能帮你更好地理解模型的某些行为特性。3.1 WordPieceBERT家族的沉默功臣WordPiece是Google为BERT模型开发的分词算法其整体流程与BPE非常相似。关键区别在于合并字符对的标准。BPE的标准合并频率最高的字符对。WordPiece的标准合并能最大程度提升语言模型概率的字符对。具体来说它计算合并一对符号后对整个训练语料库的似然值likelihood的提升选择提升最大的进行合并。从结果上看WordPiece产生的词汇表与BPE往往大同小异但在处理某些边界情况时可能有细微差别。由于BERT的巨大成功WordPiece也被广泛应用于后续的Transformer模型如ALBERT、ELECTRA。使用Hugging Face的transformers库时如果你加载一个BERT模型其配套的BertTokenizer通常就是WordPiece分词器。3.2 Unigram一种“自上而下”的 probabilistic 视角Unigram语言模型分词法采取了与BPE/WordPiece“自下而上”合并相反的思路。它是一种“自上而下”的概率模型。初始化它首先用一个很大的种子词汇表比如所有字符和常见子串开始。训练它假设一个句子是由词汇表中的token根据一个unigram语言模型即每个token独立出现生成的。然后它用EM算法等方法来优化两个东西a) 每个token的概率b) 给定当前词汇表和概率句子的最佳分词方式。剪枝逐步移除那些对整体似然值贡献最小的token例如移除后用剩余token重新分词语料库的总似然值下降最少直到词汇表缩小到目标大小。Unigram的优势在于它是一个显式的概率模型非常灵活。你可以通过调整token的概率来轻松地采样不同的分词结果这在数据增强中有用。SentencePiece工具包由Google发布同时实现了BPE和Unigram算法并且它的一大特点是不依赖空格进行预分词直接将原始文本包括空格当作一个字符流来处理这对于中文、日文等不以空格分词的语言尤其友好。许多多语言模型如T5、mT5都使用基于SentencePiece的Unigram分词器。3.3 算法对比与选型启示为了更直观我们用一个表格来对比这三大算法特性BPE (Byte-Pair Encoding)WordPieceUnigram (with SentencePiece)核心思想自下而上迭代合并最高频字符对自下而上迭代合并最大似然提升字符对自上而下基于概率模型迭代剪枝词汇表训练目标频率最大化语言模型似然最大化语言模型似然最大化空格处理通常依赖预分词空格分隔单词通常依赖预分词空格分隔单词无需预分词空格作为普通字符处理典型代表模型GPT系列, RoBERTa, LlamaBERT, ALBERTT5, mT5, ALBERT (部分版本)主要优势简单高效广泛适用与BPE类似在BERT生态中成熟灵活支持概率分词对无空格语言友好实操关注点需处理字节级(BPE) vs 字符级与BPE tokenizer基本可互换使用配置更复杂但功能强大尤其适合多语言选型心得对于绝大多数应用者来说你不需要从头训练一个分词器。你的选择通常被锁定在你想要使用的预训练模型上。如果你用Llama那就用它的BPE分词器如果你用BERT那就用WordPiece。重要的是理解你所用分词器的特性比如它的词汇表大小、是否区分大小写、如何处理数字和标点。这些细节会直接影响你预处理数据、处理模型输入输出以及进行提示工程Prompt Engineering的方式。4. 分词器的实战陷阱与细节剖析理解了原理我们来看看在实际编码和模型使用中分词器会给你埋下哪些“坑”。这些经验往往不会写在官方文档的显眼位置。4.1 词汇表与特殊Token看不见的“基础设施”加载一个分词器例如AutoTokenizer.from_pretrained(gpt2)你得到的不仅仅是一个切割文本的函数而是一个包含了几万到几十万参数嵌入向量的“基础设施”。其中有几个特殊的token至关重要bos/[CLS]序列开始Beginning of Sequence或用于分类的特殊token。在自回归模型如GPT中bos常作为生成起点在BERT中[CLS]位的输出用于分类任务。eos/[SEP]序列结束End of Sequence或分隔符Separator。eos用于标记文本结束也是生成停止的信号[SEP]用于分隔句子对。pad填充token用于将一批batch中不同长度的序列补齐到相同长度以便并行计算。unk未知token虽然子词分词大大减少了它的出现但依然存在。踩坑记录1忽略add_special_tokens参数。当你调用tokenizer(text)时默认行为add_special_tokensTrue可能会自动加上bos和eos。这在训练或微调时通常是需要的。但在进行文本相似度计算、或者单纯想查看原始文本的分词结果时这个自动添加的行为会导致意想不到的偏差。比如你计算两个句子的嵌入向量余弦相似度如果它们都自动加上了相同的bos这个无关的token会拉高相似度。务必根据场景显式设置add_special_tokensFalse。from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(gpt2) text Hello world # 默认情况可能加特殊token ids_with_special tokenizer.encode(text) # 例如 [50256, 15496, 995, 50256] # 关闭特殊token ids_raw tokenizer.encode(text, add_special_tokensFalse) # 例如 [15496, 995]4.2 长度限制与截断策略输入管道的“守门员”Transformer模型有严格的最大序列长度限制如512或2048。分词器负责执行长度管控。max_length设置模型能处理的最大token数。truncationTrue当文本超过max_length时启用截断。truncation_side决定从哪边截断。left截首通常用于保留更重要的后半部分如问答的答案right截尾更常见。对于长文档摘要可能需要从中间截断这需要自定义处理。paddingTrue与padding_side批处理时进行填充。padding_sideright是默认且最常用的。但在某些自回归生成场景为了缓存优化可能会需要padding_sideleft。踩坑记录2截断导致语义断裂。这是最隐蔽的坑。假设你有一段代码或一个JSON字符串被无情地从中间截断导致括号不匹配、语法错误。模型接收到的输入是无效的其输出自然也是无意义的。对于非自然语言代码、结构化数据简单的头部或尾部截断是危险的。解决方案包括1) 使用滑动窗口将长文本分块处理2) 针对特定结构如代码设计更智能的截断点例如在函数边界、完整语句后截断。4.3 分词不一致性同一个词不同的“命运”这是子词分词的一个固有特性也是提示工程需要特别注意的地方。大小写敏感有些分词器如原始的BERT是大小写不敏感的会将“Hello”和“hello”都转换为“hello”。而GPT系列的分词器通常是大小写敏感的。这会导致“Hello”和“hello”在模型眼中是两个不同的token拥有不同的嵌入向量。在构建搜索系统或进行精确匹配时需要做统一的大小写规范化lowercasing。空格与标点的微妙处理分词器如何处理空格前的标点例如“Hello,world”和“Hello, world”的分词结果可能不同。前者可能被分成[Hello, ,, world]后者可能是[Hello, ,, world]注意“world”前的空格可能被合并或保留。这种细微差别在要求精确复现的生成任务如代码生成中可能带来问题。数字的处理数字“123”可能被整体作为一个token也可能被拆成[12, 34]或每个数字一个token。这取决于它在训练语料中的出现频率。这会影响模型对数学和数值推理的能力。实操技巧在开始任何重要工作前务必用你的目标分词器对一批有代表性的样例文本进行分词并仔细检查结果。使用tokenizer.tokenize()方法查看token列表而不是直接看ID。这能帮你提前发现许多潜在的数据清洗和预处理问题。5. 分词如何影响模型训练与性能分词的选择不是中立的它直接塑造了模型的“认知”能力。5.1 词汇表大小一个关键的超级参数词汇表大小Vocab Size是一个需要在训练前就确定的超参数。它是一场权衡词汇表过大例如10万以上每个token的语义更具体模型可能对常见表达学习得更快。但缺点是1) 嵌入矩阵巨大增加模型参数量和内存消耗2) 每个token在训练数据中出现的平均次数变少可能导致学习不充分泛化能力下降3) 序列长度可能更短因为平均每个token代表的字符更多但这不是绝对的。词汇表过小例如1万以下嵌入矩阵小每个token被看到的次数多学习更充分。但几乎所有单词都需要被拆分成多个子词导致序列长度变长增加了计算成本注意力复杂度O(n²)并且模型需要学习更多关于子词组合的规律。经验之谈对于主流的大语言模型词汇表大小通常在3万到10万之间。例如GPT-2是50257BERT-base是30522Llama 2是32000。这个范围被实践证明能在计算效率、模型容量和泛化能力之间取得较好的平衡。当你从头开始为特定领域如医学、法律训练一个分词器时基于领域语料统计选择一个合适的词汇表大小是关键一步。5.2 对序列长度与计算效率的影响这是最直接的影响。给定一段文本分词器产生的token数量直接决定了模型需要处理的序列长度。由于Transformer注意力机制的计算复杂度与序列长度的平方成正比更长的序列意味着指数级增长的计算和内存开销。中英文混合文本一个中文字符在UTF-8中通常占3个字节在BBPE分词器下很可能被拆成多个字节token。而一个常见的英文单词可能只是一个token。这导致相同字符长度的中英文文本中文产生的token数通常远多于英文。这也是为什么在处理中文长文本时更容易遇到长度限制问题。子词分词 vs 字符分词显然子词分词能显著缩短序列长度。例如“tokenization”作为一个单词是1个token作为字符序列可能是13个tokent, o, k, e, n, i, z, a, t, i, o, n。优化思路对于需要处理超长文本的应用如长文档摘要、书籍分析除了使用具有更长上下文窗口的模型如128K在数据预处理阶段可以考虑1) 使用更“激进”的分词器在相同词汇表大小下倾向于生成更长子词的算法2) 对文本进行智能分块并在模型架构层面引入层次化注意力或记忆机制。5.3 分词与模型能力边界以代码和数学为例分词器对非自然语言数据的处理能力直接决定了模型在该领域的上限。代码生成代码具有精确的语法结构。变量名userAuthenticationHandler如果被不幸地拆分成user,Authent,ication,Handler模型学习变量名整体含义和用法的难度就增加了。好的代码分词器应该在常见编程语言的命名习惯如驼峰命名、下划线命名上进行优化尽可能保持标识符的完整性。例如Codex/GPT-3.5系列使用的分词器在这方面就做了特殊处理。数学推理数字和公式的分词至关重要。“3.14159”是作为一个token还是拆成[3, ., 14, 15, 9]后者显然不利于模型理解这是一个连续的数值。复杂的LaTeX公式更是分词器的噩梦。不合理的分词会严重损害模型的数学能力。因此领域适配的分词器是一个重要的研究方向。如果你要在特定领域微调模型使用该领域语料如GitHub代码、arXiv论文重新训练或微调一个分词器可能会带来显著的性能提升。6. 在真实项目中操作分词器以Hugging Face Transformers为例理论说了这么多最后我们落到代码上看看如何在项目中使用分词器。Hugging Face的transformers库提供了统一的接口。6.1 加载与基本使用from transformers import AutoTokenizer # 加载预训练模型的分词器 tokenizer AutoTokenizer.from_pretrained(meta-llama/Llama-2-7b-hf) # 以Llama 2为例 # 或者指定具体的分词器类 # from transformers import LlamaTokenizer # tokenizer LlamaTokenizer.from_pretrained(meta-llama/Llama-2-7b-hf) text Your input text here. # 方法1: encode - 得到input_ids (token IDs) input_ids tokenizer.encode(text, return_tensorspt) # 返回PyTorch张量 # 方法2: 直接调用tokenizer得到包含所有信息的字典 encoding tokenizer(text, return_tensorspt) # encoding 包含 # - input_ids: token ID序列 # - attention_mask: 注意力掩码1表示真实token0表示padding # - 可能还有 token_type_ids (用于区分句子对如BERT) print(encoding)6.2 批处理与填充在实际训练或推理中我们几乎总是处理一个批次的样本。batch_texts [Text one., This is a longer text example., Short.] # 自动进行分词、截断、填充并返回张量 batch_encoding tokenizer( batch_texts, paddingTrue, # 启用填充 truncationTrue, # 启用截断 max_length512, # 最大长度 return_tensorspt, # 返回PyTorch张量 ) print(batch_encoding[input_ids].shape) # 例如 torch.Size([3, 512]) print(batch_encoding[attention_mask]) # 查看哪些是真实内容哪些是填充6.3 解码从Token IDs回到文本生成模型输出的是token ID序列我们需要将其解码回人类可读的文本。# 假设 model_output 是模型生成的一串 token IDs (形状为 [1, seq_len]) generated_ids model_output # 简单解码 decoded_text tokenizer.decode(generated_ids[0], skip_special_tokensTrue) print(decoded_text) # skip_special_tokensTrue 是关键参数它会过滤掉 pad, bos, eos 等特殊token只留下有意义的文本。6.4 处理分词器中的“坑”一个完整示例假设我们正在构建一个问答系统需要处理用户可能输入的很长的问题。def preprocess_for_qa(question, context, tokenizer, max_seq_length512): 预处理问答对处理截断问题。 策略优先保留问题完整从上下文尾部截断。 # 1. 拼接问题和上下文用 [SEP] 分隔假设是类似BERT的分词器 encoded_question tokenizer.encode(question, add_special_tokensFalse) encoded_context tokenizer.encode(context, add_special_tokensFalse) # 2. 计算预留空间 [CLS] question [SEP] context [SEP] reserved_tokens 3 # [CLS], [SEP], [SEP] max_context_len max_seq_length - len(encoded_question) - reserved_tokens if max_context_len 0: # 问题本身就超长了必须截断问题这是一个极端情况需要业务逻辑处理比如返回错误 # 这里我们简单地从尾部截断问题 encoded_question encoded_question[:max_seq_length - reserved_tokens] max_context_len 0 encoded_context [] else: # 从上下文尾部截断保留最新的信息对于QA答案可能在末尾 encoded_context encoded_context[-max_context_len:] # 3. 构建最终的input_ids input_ids [tokenizer.cls_token_id] \ encoded_question \ [tokenizer.sep_token_id] \ encoded_context \ [tokenizer.sep_token_id] # 4. 创建attention_mask和token_type_ids (对于BERT风格) attention_mask [1] * len(input_ids) token_type_ids [0] * (len(encoded_question) 2) [1] * (len(encoded_context) 1) # [CLS] Q [SEP] C [SEP] # 5. 填充到最大长度 padding_length max_seq_length - len(input_ids) if padding_length 0: input_ids input_ids [tokenizer.pad_token_id] * padding_length attention_mask attention_mask [0] * padding_length token_type_ids token_type_ids [0] * padding_length # 填充部分的token type通常为0 return { input_ids: torch.tensor([input_ids]), attention_mask: torch.tensor([attention_mask]), token_type_ids: torch.tensor([token_type_ids]) # 如果模型需要 }这个例子展示了在实际应用中你需要根据任务逻辑如QA中问题和上下文的重要性不同来定制截断策略而不是依赖分词器的默认行为。Tokenization作为大语言模型流水线的第一步其重要性怎么强调都不为过。它不是一个简单的“预处理步骤”而是模型数据表示的核心组成部分与模型架构、训练目标紧密耦合。理解你使用的分词器了解它的词汇表、特殊token、截断和填充行为是进行有效的模型开发、调试和优化的基础。下次当你看到模型产生一个奇怪的输出时不妨先看看输入文本被切分成了什么样子——答案往往就隐藏在这些小小的token之中。