1. 项目概述:为什么一个“切词器”能卡住你整个LLM开发流程?
你有没有遇到过这样的场景:模型权重下载好了,环境也配完了,代码就差最后三行——结果一运行,报错直接甩在脸上: NotImplementedError: BPE pre-tokenizer was not recognized - update get_vocab 。你翻遍文档、查GitHub Issues、重装transformers、升级tokenizers库,甚至把Python版本从3.9升到3.11,问题还在那儿,像块嚼不烂的牛皮糖。这不是玄学,这是你在和 分词器(Tokenizer) 打交道时,被它最底层的逻辑反手教育了一顿。
“LLM Tokenizers Simplified”这个标题里藏着一个残酷现实: 大语言模型不是直接读汉字或英文单词的,它只认数字——而Tokenizers,就是那个把人类语言翻译成模型能吞下去的整数序列的“翻译官”。 它不显山不露水,却决定了你的输入能不能进模型、进得准不准、进得快不快、进得省不省。BPE、SentencePiece、WordPiece、Unigram……这些名字听起来像密码学名词,但它们其实是不同年代、不同团队为解决同一个问题——“怎么把无限可能的自然语言,压缩进有限且可学习的离散符号空间”——所设计的工程解法。我做过27个LLM应用落地项目,其中19个在初期卡点都出在Tokenizer上:中文乱码、长文本截断异常、特殊符号丢失、RAG检索召回率低得离谱……最后发现全是分词策略没对齐。这不是配置错误,是认知断层。今天这篇,不讲抽象理论,不堆公式推导,就用你调试时真正在意的视角—— 它怎么切、为什么这么切、切错了会怎样、换一个要动哪些骨头 ——把BPE、SentencePiece这些“黑盒翻译官”扒开来看清它的字典、规则和脾气。适合所有正在调Qwen、Llama、Phi-3,或者自己微调小模型的开发者,尤其适合那些被 tokenizer.encode() 返回一串看不懂数字、被 max_length 限制得莫名其妙、被 add_special_tokens 搞晕过的人。我们从第一行日志开始,讲到最后一行 decode() 输出。
2. 核心思路拆解:Tokenizer不是“切分器”,而是“编译器”
2.1 为什么不能直接按空格/标点切?——语言的“颗粒度陷阱”
新手最容易掉进的坑,就是以为Tokenizer就是个高级版 str.split() 。你试试看: "I'm going to the U.S.A. tomorrow!" 如果按空格切,得到 ["I'm", "going", "to", "the", "U.S.A.", "tomorrow!"] ;再按标点切, ["I", "'", "m", " going", " to", " the", " U", ".", "S", ".", "A", ".", " tomorrow", "!"] 。这两种切法,前者把缩写 U.S.A. 当一个整体,后者把它剁成 U 、 . 、 S 、 . 、 A 、 . 六个碎片。问题来了:模型该学 U.S.A. 这个国家名,还是学 U 、 S 、 A 这三个字母?如果训练时 U.S.A. 被切成六块,推理时你给它一个完整的 U.S.A. ,它根本没见过这个组合,只能硬猜——这就是“OOV(Out-of-Vocabulary)问题”的根源。中文更麻烦:“北京大学”是一个词,“北京”是地名,“大学”是机构,“北大”是简称,“北京大”是错词……按字切( ['北','京','大','学'] )损失语义;按词切( ['北京大学'] )又需要庞大词典,且无法处理新词如“奥密克戎”。所以Tokenizer的核心任务,从来不是“怎么切”,而是“ 在语义完整性、词表大小、泛化能力三者之间找一个工程最优解 ”。它本质上是个 编译器 :把源代码(自然语言)编译成目标机器(LLM)能执行的指令(token ID序列)。编译过程包含预处理(pre-tokenization)、主切分(core tokenization)、后处理(post-processing)三个阶段,而BPE、SentencePiece等,主要定义的是“主切分”这一步的算法逻辑。
2.2 BPE:从“贪心合并”到“频率驱动”的渐进式构建
Byte Pair Encoding(BPE)是当前LLM最主流的分词算法,GPT系列、Llama、Qwen全系都在用。它的思想极其朴素: 从字符级开始,不断把高频共现的相邻符号对合并成新符号,直到词表达到预定大小。 比如原始文本是 "low lows lowest low" ,初始词表是所有字符: ['l','o','w',' ','l','o','w','s',' ','l','o','w','e','s','t',' ','l','o','w'] 。第一步,统计所有相邻字符对频率: 'lo':3, 'ow':3, 'w ':2, ' s':2, 'se':1, 'es':1, 'st':1, 't ':1, ' l':1 。最高频的是 'lo' 和 'ow' (都是3次),选一个(比如 'lo' )合并,生成新符号 <lo> ,文本变成 "<lo>w <lo>ws <lo>west <lo>w" 。第二步,再统计新文本的相邻对: '<lo>w':3, 'w ':2, 'ws ':1, ... ,继续合并最高频对。如此循环,最终得到一个混合了子词(subword)的词表: ['l','o','w','<lo>','<low>','<lowest>','<lows>'] 。关键点在于: BPE不依赖预设词典,完全由训练语料的统计规律驱动;它天然支持未登录词——遇到新词如 'flow' ,会切分为 'fl'+'ow' 或 'f'+'low' ,只要 'ow' 或 'low' 在词表里就行。 我在部署Qwen-1.5B时发现,原始tokenizer对中文“量子纠缠”切分为 ['量','子','纠','缠'] (纯字粒度),导致注意力机制难以捕捉跨字语义;换成BPE训练的词表后, '量子' 、 '纠缠' 常作为高频对被合并,切分质量直线上升。但BPE也有硬伤:它对长尾分布敏感,小语种或专业术语(如生物基因名 'BRCA1' )可能因出现频次低而被过度切碎,这时就需要SentencePiece的“无监督+正则化”来补位。
2.3 SentencePiece:脱离Unicode束缚的“字节级自由主义”
Google推出的SentencePiece,核心突破在于 彻底抛弃了“文本必须先按Unicode字符切分”的前提 。传统BPE实现(如Hugging Face的 tokenizers 库)默认以UTF-8字节流为输入,再映射回Unicode字符;而SentencePiece直接把原始字节序列(raw bytes)当作输入,用一种叫“unigram language model”的概率模型来学习最优切分。什么意思?它不预设任何切分规则,而是穷举所有可能的子串组合,计算每个子串在语料中出现的概率,然后用动态规划选出概率乘积最大的切分路径。比如句子 "hello world" ,它会评估 'h'+'e'+'l'+'l'+'o'+' '+'w'+'o'+'r'+'l'+'d' 、 'he'+'ll'+'o w'+'or'+'ld' 、 'hello'+' world' 等无数种切法,选总概率最高的那一个。这种设计带来三大优势:第一, 真正端到端 ——输入是原始文本文件,输出是token ID,中间不经过 str.split() 或正则清洗,避免了编码转换错误(比如Windows的 \r\n 和Unix的 \n 混用导致的切分偏移);第二, 对多语言友好 ——中文、日文、阿拉伯文、emoji全部统一处理,无需为不同脚本写不同规则;第三, 可控性强 ——通过设置 -character_coverage 参数(如0.9995),强制模型覆盖99.95%的字符,剩余0.05%用字节fallback,杜绝了``乱码。我在做跨境电商客服LLM时,用户输入夹杂中英日韩和大量emoji(👍📦🚚),用Hugging Face默认tokenizer经常把 '📦' 切碎成多个字节ID,导致模型理解成“盒子”+“乱码”,换成SentencePiece后, '📦' 稳定对应单个ID,意图识别准确率提升12%。但代价是:SentencePiece模型文件( .model )是二进制格式,无法像BPE的 vocab.json 那样直接阅读,调试时得靠 sp.EncodeAsPieces() 逐句验证。
2.4 WordPiece与Unigram:两种不同的“概率哲学”
除了BPE和SentencePiece,WordPiece(BERT系)和Unigram(SentencePiece的底层模型)代表了另外两种分词哲学。WordPiece和BPE很像,都用贪心合并,但关键区别在 合并标准 :BPE选“绝对频次最高”的对,WordPiece选“加入后使语言模型困惑度(perplexity)下降最多”的对。换句话说,BPE问“哪个对出现最多?”,WordPiece问“哪个对能让模型预测得最准?”。这使得WordPiece更关注语义相关性,比如 'unhappy' 和


2750

被折叠的 条评论
为什么被折叠?



