资讯动态

SentencePiece原理与应用:从子词切分到中文NLP分词器训练

发布时间:2026/10/16 10:04:09 来源:尧图企业网站定制
1. 为什么现在做 NLP 都绕不开 SentencePiece最近整理项目代码时又用到 SentencePiece顺手把踩过的坑和关键原理梳理了一遍。这个工具在 NLP 圈子里基本属于“隐形基础设施”——你未必直接调用它但翻开 BERT、T5、LLaMA 这些模型的预处理脚本十有八九躺着 SentencePiece 生成的模型文件。尤其是做中文、日文、韩文这类没有天然空格分隔的语言或者想自己训练一个分词器适配垂直领域数据SentencePiece 几乎是绕不开的选择。它解决的问题很直接把原始文本切成模型能处理的 token 序列。但它的做法和传统分词器完全不同——它不关心你的语言有没有单词边界甚至不关心你输入的是不是自然语言只要给足数据它就能无监督地学出一套子词切分规则。这意味着你拿它处理中文、英文、代码、HTML 标签混排的文本它都能用同一套机制搞定。这篇文章我想从原理讲起然后给出一套可以直接参考的训练流程最后把我实际项目中遇到过的坑和排查思路整理成速查表。适合的人群有两类一是刚接触 NLP、准备自己训练 tokenizer 的初学者二是已经在用 SentencePiece 但遇到性能、切分效果或模型加载问题的开发者。我默认你已经知道 BPE、WordPiece 这些基础概念如果完全没听过也没关系下文会把这些概念串起来讲不会让你卡在术语上。2. SentencePiece 的设计理念把“分词”这件事彻底抽象化2.1 从一个反直觉的输入格式说起传统分词器的工作流程一般是先做语言特定的预处理比如英文按空格拆词、中文先分词或按字切分然后再进入词表构建。这意味着每换一种语言你就要换一套预处理逻辑。SentencePiece 的第一个关键设计就是不要输入任何形式的预切分结果直接把原始文本喂给它。它的底层逻辑是“把整个句子视为一串 Unicode 字符流”。空格不再作为单词边界而是被转成普通字符▁U2581下划线符号显式出现在 token 里。这样做的好处是中英文混排、代码片段、甚至表情符号都被统一当作字符序列处理训练出的模型天然具备跨语言的鲁棒性。我试过直接拿中英混合的客服语料训练不需要任何人工标注切分质量比之前用 Jieba BPE 的级联方案还稳定。“SentencePiece”这个命名也很有意思它强调按“句”处理而不是按“词”处理。内部实现里文本会被按行切分每一行当作一个句子样本。所以训练语料的预处理很简单一行一条数据清洗掉不可见字符剩下的交给模型去学。2.2 字节级无损什么都能编码什么都能还原我们训练好的 tokenizer 最终要服务于模型。如果词表外字符无法表达推理时就会出现[UNK]泛滥的问题。SentencePiece 在这一点上做了很彻底的设计——它支持直接在字节级别训练也就是把 UTF-8 编码后的字节序列作为最底层的字符集合。这样带来的核心能力是任何 Unicode 字符都可以被底层字节兜住不会出现“这个词我训练时没见过就完全卡死”的情况。代价是某些生僻字会被切成多个字节级 token序列长度变长。但实际效果是值得的尤其在处理多语言场景时这种“保底机制”不可或缺。还有一个容易被忽略的设计unk_piece和control符号是显式存在的。你可以在训练时自定义控制符号比如sep、cls这些符号会被保留进词表且不会被切分。这个特性在做多句拼接、指令微调时特别有用。我在训练一个对话模型的 tokenizer 时就把user、assistant这些角色分隔符加进了控制符号列表里省去了之后单独扩展词表的工作。2.3 和 BPE、WordPiece、Unigram 的关系SentencePiece 不是一个算法而是一个框架。它内部实现了三种建模算法BPEByte Pair Encoding反复合并频次最高的字符对直到达到目标词表大小。合并过程是不可逆的。WordPiece类似 BPE但合并准则不是频次而是使训练集似然增量最大的字符对最早由谷歌用于机器翻译。Unigram从一个较大的种子词表出发迭代剪枝每次去掉让损失上升最少的 token最终得到紧凑词表。它是很多新一代模型的首选比如 LLaMA 使用的就是 SentencePiece Unigram。这三种算法的差异主要体现在“如何决定哪些子词值得进词表”。BPE 看重频率Unigram 看重损失贡献。实际使用中Unigram 因为训练可控性和解码效率更好越来越受青睐。但如果你是第一次使用直接用默认设置就好SentencePiece 默认已经是 Unigram。需要特别说明的是SentencePiece 训练出的模型天然支持“无损还原”。它保存了足够的映射信息你不需要另外保存词表文件和合并规则文件两个文件。.model文件本身就是完整的加载后可以直接 encode/decode这比很多传统分词器的部署方式都要简洁。3. 训练自己的分词器从数据准备到参数调优3.1 训练数据准备质量比数量更重要训练 tokenizer 看似是“无监督”任务不需要标注但数据质量直接影响切分效果。下面是我总结的几条实操原则。第一条数据要“杂而精”。如果最终场景是通用对话就不要只拿新闻语料训练。因为新闻语料里几乎没有口语词、网络新词、语气词训练出来的词表在对话场景会有很多低频 token 覆盖不到。我的做法是把多个来源的数据混合进行去重和格式统一后再喂给 SentencePiece。第二条按行清洗。SentencePiece 按行读取文本遇到特别长的行会占用大量内存。我会先用脚本做一轮过滤去掉空行、控制字符、连续重复的行。注意不要在这一步把标点全删了因为 punctuation 的切分方案会影响 tokenizer 在完整句子上的表现。第三条控制数据量。很多人误以为 tokenizer 训练数据越多越好。实际上对大多数场景几百 MB 到几 GB 的文本已经足够。Unigram 算法在数据量过大时训练时间呈线性增长但词表质量的提升会出现平台期。我通常先准备 1GB 左右的清洗后数据做初始训练效果不足再增量补充。下面给出一份可以直接参考的数据清洗伪代码import re def clean_line(line: str) - str: # 去掉不可见控制字符 line .join(ch for ch in line if ord(ch) 32 or ch in \n\t) # 统一换行符 line line.replace(\r, ).replace(\u3000, ) # 压缩连续空格保留单空格帮助 BPE 学习空格边界 line re.sub(r , , line) return line.strip() with open(raw.txt, r, encodingutf-8) as fin, \ open(clean.txt, w, encodingutf-8) as fout: for line in fin: cleaned clean_line(line) if len(cleaned) 0: fout.write(cleaned \n)3.2 核心训练参数解析vocab_size、model_type、character_coverage参数用的是以下这套spm_train \ --inputclean.txt \ --model_prefixmy_spm \ --vocab_size32000 \ --model_typeunigram \ --character_coverage0.9995 \ --bos_id1 \ --eos_id2 \ --unk_id0 \ --pad_id-1 \ --user_defined_symbolsuser,assistant,sep \ --input_sentence_size5000000 \ --shuffle_input_sentencetrue \ --hard_vocab_limitfalsevocab_size是最重要的参数。它决定了词表大小。常见模型中 32K、50K 是比较主流的选择。如果语料语言种类多或者领域术语多可以适当上调。注意这里还有一个隐藏参数hard_vocab_limit如果保持默认true最终词表大小不会超过vocab_size设为false后由于要确保所有字符被覆盖词表可能略大于设定值。我的建议是保持默认或者显式设为true让词表大小可控。model_type我推荐直接用unigram。它训练速度略慢一点但生成词表的表示效率更好解码性能也稳定。bpe在极小数据量下或许有奇效但数据量上来之后优势不明显。character_coverage表示字符覆盖率。0.9995意味着训练语料中 99.95% 的字符会被覆盖剩余 0.05% 的稀有字符会落到未知 token 上。中文语料建议设成0.9995或0.9999纯英文可以设成0.999。设得太低会频繁出现[UNK]设得太高会导致词表被生僻字占满降低常用 token 的质量。user_defined_symbols是一个容易被忽略但极其有用的参数。你可以把业务中需要整体保留的字符串传进去比如多轮对话的分隔符、特殊占位符。这些字符串会被原样保留在词表里不会被切成更小的子词。input_sentence_size是采样句子数。如果你的语料特别大不想全部加载到内存就设一个合适的值比如 500 万到 1000 万句。配合shuffle_input_sentencetrue可以降低数据排列顺序带来的偏差。3.3 训练流程实操从命令行到 Python API训练 SentencePiece 最简单的方式是命令行。上面给出的就是完整命令。训练完成后你会得到两个文件my_spm.model二进制模型文件包含所有 token 和切分规则。my_spm.vocab可读词表文件每行一个 token 及其得分通常用于后续分析或导入其他框架。在 Python 中使用模型import sentencepiece as spm sp spm.SentencePieceProcessor(model_filemy_spm.model) text 今天天气不错适合出去散步。 pieces sp.encode(text, out_typestr) print(pieces) # 输出类似 [▁今天, 天气, 不错, , 适合, 出去, 散步, 。] ids sp.encode(text, out_typeint) print(ids) # 输出 token id 列表 decoded sp.decode(ids) print(decoded) # 输出还原文本应当与原始文本一致这里要注意encode的out_type参数。out_typestr返回的是 token 字符串out_typeint返回 token id。很多新手会混淆这两个返回值导致后续数据处理出错。另外decode接收的是 id 列表而不是字符串。如果你需要把模型输出的 token 字符串重新拼回文本不要直接.join(pieces)因为空格被编码成了▁直接拼接会丢失空格信息。正确做法是让decode来处理。如果想把 SentencePiece 接入 HuggingFace Transformers只需要把模型文件拷到模型目录并在tokenizer_config.json中指定tokenizer_class: SentencePieceProcessor或者直接使用SentencePieceTokenizer完成加载。这个流程比 BERT 时代需要同时加载 vocab 和 merges 文件要省事得多。3.4 评估训练效果不只是看词表大小训练完成后切片效果好不好不能只看感觉要用几个可量化的指标来衡量。第一是未登录词率。随机抽取测试集文本用训练好的 tokenizer 编码统计[UNK]出现的比例。理想情况应该低于 0.1%。如果偏高优先检查character_coverage和数据清洗是否到位。第二是平均 token 长度。一个句子编码后 token 数越少说明词表表达能力越强。对中文来说单字 token 占比过高意味着训练不充分但 token 太长又可能导致生僻词覆盖不足要平衡。第三是解码还原率。随机取一批文本encode 后 decode统计有多少句子能完全还原。这个指标几乎决定了 tokenizer 是否能安全上线。我做项目时会把这一项作为硬性门槛还原率低于 99.9% 就会重新调整数据或参数。评估脚本不需要很复杂核心逻辑如下import random def evaluate(model_file, test_file, sample_size1000): random.seed(42) sp spm.SentencePieceProcessor(model_filemodel_file) total 0 match 0 unk_count 0 with open(test_file, r, encodingutf-8) as f: lines [line.strip() for line in f if line.strip()] for line in random.sample(lines, min(sample_size, len(lines))): ids sp.encode(line, out_typeint) decoded sp.decode(ids) total 1 if decoded line: match 1 unk_count ids.count(sp.unk_id()) print(f还原率: {match / total:.4f}) print(f平均每句 UNK 数: {unk_count / total:.4f})4. 实际项目中必须理解的几个深层机制4.1 解码时空格是怎么恢复的SentencePiece 给所有空格字符分配了特殊 token▁。编码时句首的空格也会被标记这是传统分词器不会做的事。解码时▁会被转换回普通空格同时利用“是否在词首”的信息恢复切分边界。这意味着什么意味着你不需要在分词后的序列里额外维护一个“词语边界”张量。Transformer 模型通常通过绝对位置编码感知序列顺序但一些涉及词级特征提取的任务如词性标注、命名实体识别需要知道 token 是否为词首。此时只需要检查 token 字符串是否以▁开头即可。在 Python 中判断 token 是否为词首def is_begin_of_word(token: str) - bool: return token.startswith(▁)这个特性让 SentencePiece 在序列标注任务里特别受欢迎。我在做中文实体识别时直接用 SentencePiece 作为基础 tokenizer再加一层词首标记不需要额外的 CRF 分词步骤。4.2 词表里的“假”空格和 NMT 的坑有一个现象需要特别提醒▁并不总代表真实空格。当一个 token 本身以▁结尾时比如▁walking这里▁是词首标记不是行尾空格。但在某些场景下比如“北京▁上海”空格被切成了独立 token而“▁北京”里的▁表示北京是新词的开始。在神经机器翻译或生成任务中解码器输出的 token 序列经过decode还原时▁会被合理转换为空格。但如果开发者在后处理时不小心把▁当普通字符串处理就会得到带下划线的错误文本比如_北京_上海。这种问题在 C 和 Java 部署时更容易出现因为部分语言库对▁的显示不直观。解决方法是不要在后处理中手动拼接 token始终用官方 API 的decode方法。如果需要定制输出再在decode结果上做字符串替换。4.3 控制符号和特殊 Token 的实际用途在一个多轮对话项目中我用--control_symbolscls,sep,mask把预训练模型常用的特殊 token 加进去了又在user_defined_symbols里放了user和assistant。区别在于control_symbols中的 token 可以被模型当作普通 token 参与注意力计算而user_defined_symbols里的 token 会在编码时被整体保留不会拆开。这个区分为后续微调省了很多事。因为预训练时如果词表里没有这些符号微调时要么扩展词表、重新随机初始化对应 embedding 层要么把角色前缀映射到多个已有 token 上都会带来额外的复杂度。直接训练到词表里一劳永逸。4.4 为什么 LLaMA 和很多新模型都在用LLaMA 的 tokenizer 就是 SentencePiece 训练的词表大小 32000使用 Unigram 算法。它选择 SentencePiece 而不用传统 BPE有两个原因。一是 SentencePiece 天然支持多语言二是它把空格编码进 token 的方式对“代码补全”这类任务更友好。代码中缩进和空格是有意义的传统分词器把空格当成普通 delimiter 处理会丢失这些信息。在多语言基础模型训练里SentencePiece 的字节级回退机制也很有价值。它保证低资源语言不会被完全丢弃每个字符至少能映射到字节级 token。相比之下很多基于空格切词的传统方法遇到阿拉伯语、泰语这类语言就直接崩了。5. 踩坑记录与问题排查速查表5.1 问题一未登录词率居高不下现象训练时使用了大量中文语料但编码测试文本时频繁出现[UNK]。排查步骤首先检查模型是否加载成功。有人直接把your_model.model文件名改成了tokenizer.model但在 Python 中加载时还是用的旧路径导致加载了错误或旧的模型文件。这个错误极其常见。检查character_coverage。中文语料建议不低于0.9995。如果设成0.99意味着 1% 的字符没有被覆盖这些字符在推理时全部变成[UNK]。这个比例被低估时错误会被放大。检查数据清洗是否把全角字符转换成了半角。全角逗号和半角逗号,会被当成不同字符数据量少时可能出现低频字符覆盖不足。如果以上都排除了可以考虑重新训练并显著提高input_sentence_size。某些领域术语在训练集中出现次数太少Unigram 算法会直接把它剪掉。补充领域数据再训练是解决问题的根本方式。5.2 问题二还原率不达标现象encode再decode后文本和原文本不一致。排查步骤检查空白字符。SentencePiece 对多个连续空格、tab 的处理方式不同于普通文本。如果原始文本里有\u200b零宽空格这类不可见字符训练时没有被清洗解码时可能无法还原。我的建议是训练前统一替换掉所有零宽字符。检查输入是否合规。encode方法的out_typeint只适用于普通文本或已编码的 id 列表。如果你传入的本来就是 id就不应该再走encode而直接用decode。检查unk_id是否设为了 -1。如果unk_id-1遇到 OOV 字符时 encode 会直接抛异常而不是返回[UNK]id。这个异常捕获和处理方式也会影响还原率判断。5.3 问题三训练内存爆炸或耗时过长现象spm_train在数据量较大时把内存占满或者训练了一天还在迭代。排查方向减少input_sentence_size。假设你有 10GB 语料不需要全部加载。我们可以先随机采样 1000 万句效果通常足够。降低vocab_size。如果当前设定 96K可以先用 32K 做快速验证等数据配比调好再训练最终版本。使用--split_by_unicode_scripttrue按 Unicode 脚本分组处理减少字符表规模。尝试--num_threads设置更大的并发数。训练是多线程实现的调大线程数对耗时改善明显。5.4 独家技巧补充训练时保存中间模型--keep_vocabtrue可以保留完整词表而不是只保留剪枝后的词表。如果你想分析哪些 token 被算法剪掉了这个参数很有用。复用已有模型的前缀词表如果你只想在已有模型中新增领域词不要直接重新训练。可以先加载旧模型收集新语料的 OOV token然后用spm_export_vocab导出词表进行合并。虽然官方 API 不直接支持增量学习但这种手工合并方式能显著减少训练成本。多语言机器翻译项目里可以用--hard_vocab_limitfalse让词表略超过预设值多出来的 token 往往都是低资源语言的高频词对低资源语言效果提升明显。6. 一次完整的实战案例训练一个中文对话模型分词器聊完原理和坑我用一个实际项目串一遍完整流程。项目背景是某平台需要做一个中文客服对话模型语料混合了用户消息、机器人回复、工单文本和少量英文产品名。目标是训练一个 32K 词表的 SentencePiece 模型并为后续指令微调预置角色分隔符。我先整理了语料清洗后大约 1.2GB、3000 万行。随后把其中的用户消息和机器人回复分别按行写入all_text.txt。为了让模型多学点领域词我又额外加入了一部分产品手册和用户手册数据。接下来执行训练命令spm_train \ --inputall_text.txt \ --model_prefixchat_spm \ --vocab_size32000 \ --model_typeunigram \ --character_coverage0.9995 \ --bos_id1 \ --eos_id2 \ --unk_id0 \ --pad_id3 \ --user_defined_symbolsuser,assistant,system,sep \ --control_symbolscls,mask \ --input_sentence_size10000000 \ --shuffle_input_sentencetrue \ --num_threads16这里我把pad_id指向了 3这样训练到的 token id 可以稳定区分。后续在训练 Transformer 时Pad token 直接使用 id 3不需要重定义。训练完成后我先做了还原率测试测试集是 5000 条未参与训练的真实对话样本。结果显示还原率达到 99.98%没有 OOV 问题。然后我将模型接入 Transformers 的配置。由于我要用自定义角色 token 做指令微调我在tokenizer_config.json里设置了additional_special_tokens并确保这些 token 已经在 SentencePiece 词表里。这样微调时不需要扩展 embedding 层可以原样加载预训练权重训练也更加稳定。这个项目上线后观察到两个正向变化一是中英文混合输入时tokenizer 不再像之前 Jieba BPE 那样频繁吐出超长 token 或切碎英文单词二是模型在客服场景的回复一致性明显提高因为角色分隔符变成了固定 token而不是模型要硬学的隐式格式。7. 个人使用体会和几个实用建议我在多个多语言、中文、以及代码混合文本的项目里用过 SentencePiece 之后最深的感觉是这个工具最大的价值不是“分词多准”而是“把分词这件事从工程问题变成了配置问题”。传统方案需要为每种语言、每个业务场景定制语料切分逻辑而 SentencePiece 把这些都抽象成了字符级序列学习数据和参数得当的情况下效果往往比手工规则好得多。给新手的建议是不要一上来就追求大词表。先用 32K 验证数据和流程再根据任务需求调整。把 10GB 语料直接喂进去很容易导致长时间训练而第一次训练只是想看切分效果的话完全不需要那大的数据量。给进阶用户的建议是关注character_coverage和user_defined_symbols这两个参数。前者决定了覆盖率和词表质量之间的平衡后者能让你的业务 token 直接进入词表省掉后续大量特殊处理。如果在实际使用中碰到奇怪现象比如 decode 后文本多了一个空格、某类文本 encode 后序列特别长、token 里出现异常的▁▁都可以先尝试升级 SentencePiece 到最新版本再逐步检查数据清洗和三段式参数。大多数问题的根源最终都会回到“训练数据不够干净”或者“参数细节没吃透”这两点上。我最后一次把 SentencePiece 用在一个包含代码补全的小模型上时还发现了一个意外的好处它天然能处理缩进和空格 token这让模型在补全 Python 代码时几乎不需要额外的代码结构特征。只要你的项目涉及自然语言和代码混合、多语言数据、或需要自定义特殊 tokenSentencePiece 都值得认真一试。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑