chinese-ime-lm
面向中文输入法的语言模型工程:语料、统计模型、神经模型,以及判断它们好坏的评测集。
仓库里有两条并行的路线,服务同一件事——在拼音解码给出的若干读法之间做出选择:
| 路线 | 位置 | |
|---|---|---|
| n-gram | 语料清洗 → KenLM 统计 → Viterbi 解码 | corpus/clean/、ngram/ |
| 神经模型 | 语料获取 → 字级 Transformer → 候选重排 | corpus/fetch.py、neural/、reference/ |
两者用同一套评测集(eval/)衡量,所以可以直接比较。评测集本身比任何一个模型都更值得先看:它把"某个改动好不好"从意见变成数字,并且把两个会悄悄毁掉测量的陷阱固化成了用例(详见 docs/measurements.md)。
与其自己训,不如直接用开源小模型? 在同一套候选池上量过四个 Qwen:观测上没有一个超过本项目 25.5 MB 的模型,0.6B 与 0.8B 打平 4.5 MB 的那个,而体积差 140 到 380 倍、单次决策慢一个数量级。但排序质量上的名次差距全部落在噪声里——52 条上两两分歧只有 2 到 6 条,McNemar 没有一对显著,要检出 1 到 2pp 需要一千到近万条用例。选自训权重的理由是尺寸和延迟这两项确定性测量,不是名次。同一次测量还发现整句集里有 12 条「他/她」在拼音上不可判的用例,本就不该收。全部记在 docs/measurements.md。
当前状态:神经模型两条都在用,n-gram 一条从未出过这个仓库。 权重以 Apache-2.0 发布在 Hugging Face —— pinyin-ime-reranker-4M(4.5 MB,按键路径用这个)和 pinyin-ime-reranker-25M(25.5 MB,精度到顶),同一批文件也在 model-v1 release 里,MSIME 的 host-api 通过资源锁装载它们:小的跑按键路径,大的在输入停顿时接手。
n-gram 一侧从未有产物进入过任何产品,而神经一侧按音节直接解码(reference/examples/decode.rs)的表现仍然比它本想改进的引擎还差——重排有用,直接解码没到。采用之前先把话说在前面。
目录
| 路径 | 内容 |
|---|---|
corpus/ |
语料:fetch.py 获取可再分发语料,clean/ 清洗自备语料,pinyin/ 生成单字拼音表 |
ngram/ |
n-gram 路线:KenLM + Viterbi 解码 |
neural/ |
神经路线:字级 Transformer 的训练、导出、评测 |
reference/ |
Rust 推理参考实现,无 unsafe,除 serde 外无依赖(Apache-2.0) |
eval/ |
25,119 条词级用例 + 60 条整句用例 |
docs/format.md |
模型文件格式规范,用任何语言实现加载器只需要这一篇 |
docs/measurements.md |
实测结论:模型在哪里有用、在哪里有害,两个测量陷阱,以及与开源通用模型的对比 |
tests/ |
语料清洗的回归测试 |
许可按目录区分
仓库整体为 GPL-3.0(见 LICENSE)。
reference/ 下的 Rust 代码著作权单一,另以 Apache-2.0 授权(见 reference/LICENSE),因为大量开源输入法是 MIT/Apache/BSD,链不了 GPL 代码——第三方可以只取那个目录。
格式规范、评测集和发布的权重都不受代码许可约束。发布的权重为 Apache-2.0,理由和 reference/ 一样:链不了 GPL 的输入法同样用不了 GPL 的权重,而一个没人能采用的公共资源不成其为公共资源。该许可写在权重文件自己的 __metadata__ 里,所以它不会与权重分离。
权重另有语料的署名义务,见 NOTICE;那份义务来自语料而非代码,两者不可相互替代。
语料来源与许可
本仓库只分发处理脚本。 语料和训练好的模型都不入库:data/、model/、kenlm_bin/ 下除 .gitkeep 外全部被 gitignore,没有任何内容被跟踪。语料由你自己准备。
这一点要紧,因为本项目最初依据的语料不能自由再授权,而你下载它的那个仓库上的许可并不能解决这个问题:
- 上游聚合仓库 brightmart/nlp_chinese_corpus 以 MIT 发布。那覆盖的是聚合者自己的工作,不是它打包的第三方文本。
wiki2019zh派生自中文维基百科,即 CC BY-SA 3.0。该许可要求署名,且具传染性:任何实质派生于它的东西都继承这些条款。news2016zh是抓取的新闻,baike2018qa是百度百科内容。两者都没有允许再分发的许可。
维基百科下游的任何人都无法把维基的文本重新授权为 MIT,所以聚合仓库的 MIT 徽章只应理解为覆盖其脚本与打包工作。
这在实践中意味着什么。 从语料算出来的统计量(n-gram 计数、概率)通常不等于语料本身,但"统计量"和"衍生作品"之间的界线并不清晰——一个能复现源句子的模型,比一张二元频率表更接近衍生作品。所以:
- 本仓库的产物从未进入过已发布的产品。MSIME-Engine 中目前没有任何 n-gram 或 KenLM 模型,也没有加载它们的代码路径。
- 在任何由本管线构建的模型进入输入法之前,必须先解决所用子集的许可问题:要么把训练限制在可以干净再分发的子集上,要么满足维基派生部分的 CC BY-SA 署名与传染性要求,并把结论记进 Engine 的
NOTICE.md,与其他词库来源并列。
corpus/fetch.py 正是第一条路线的实现:它只获取许可清晰、可再分发的语料(C4 为 ODC-BY,LCCC 为 MIT),发布的神经模型权重只用这些训练。详见 NOTICE。
本仓库的 LICENSE(GPL-3.0)只适用于这里的脚本。它不涉及、也无法授予你喂给这些脚本的语料的任何权利。
获取语料
两种方式,按你的用途选:
自己准备。 语料来源见上节那个聚合仓库,解压后放进 data/ 目录,再用 corpus/clean/ 清洗。
让 corpus/fetch.py 下载。 它只取许可清晰的来源,流式下载并直接规范化,原始压缩包不落盘:
python corpus/fetch.py c4 --out data/c4.txt --max-chars 1_000_000_000 python corpus/fetch.py lccc --out data/lccc.txt --split large
语料清洗(自备语料)
对于句子中的非中文字符,进行过滤,只要是非中文的字符,一律将其扩充成连通块,然后,将这个连通块当作是一个分隔符,将句子切分成小句子。最后,预处理好的数据就都是一个个由纯粹的中文组成的单独的连通块。
python .\corpus\clean\extract_connected_hanzi_components.py python .\corpus\clean\extract_wiki_connected_hanzi_components.py python .\corpus\clean\split_all_cleaned_txt_using_space.py python .\corpus\clean\split_wiki_txt_using_space.py
注意,这里是比较耗时的,至少需要半个小时左右,在我的 11 代 intel 处理器上。
分词
对上面预处理好的数据进行分词。由于使用的是字级 n-gram,分词只需要把字一个一个分开即可。
统计 n-gram
分别计算一元数据和二元数据。
- 一元数据:
- 纯粹单字在所有字符中出现的频率。
- 二元数据:
- 按单字切分的 2-gram 数据。
- 三元数据:
- 按单字切分的 3-gram 数据。
这里的处理就都交给 kenlm 来做,kenlm 的编译和使用参见我的笔记。需要准备的东西是这种格式的文本语料:
我 爱 北 京
我 爱 中 国
我 爱 上 海
北 京 是 中 国 首 都
需要将 kenlm 项目编译好的 Release 版本的二进制文件复制到 kenlm_bin 目录下。然后,将生成的 model.arpa 和 model.binary 文件放到相应的 model\all 和 model\only_wiki 目录下。
cmd /c ".\kenlm_bin\lmplz.exe -o 3 < .\data\output\all_cleaned_only_wiki_zh_spaced.txt > model.arpa" .\kenlm_bin\build_binary.exe model.arpa model.binary mkdir model\only_wiki mv model.arpa model\only_wiki mv model.binary model\only_wiki
cmd /c ".\kenlm_bin\lmplz.exe -o 3 < .\data\output\all_cleaned_spaced.txt > model.arpa" .\kenlm_bin\build_binary.exe model.arpa model.binary mkdir model\all mv model.arpa model\all mv model.binary model\all
神经模型
字级 Transformer,在拼音解码拼装出的候选之间重排。它不是普遍有益的——实测见
docs/measurements.md,结论是:词典对整个键的精确命中已经携带语料词频,
覆盖它们在任何权重下都掉分;而解码器拼装的候选不带词频证据,模型的价值只在那里。
pip install -r neural/requirements.txt python corpus/fetch.py c4 --out data/c4.txt --max-chars 1_000_000_000 python corpus/fetch.py lccc --out data/lccc.txt --split large python neural/train.py --corpus data/c4.txt data/lccc.txt --out runs/desktop --preset desktop python neural/export.py --run runs/desktop --out dist/model.safetensors --precision int8
corpus/fetch.py 与 corpus/clean/ 的关系:后者处理你自己准备好的语料,前者负责获取许可清晰、可再分发的语料。两者的中文规范化是同一个算法——保留汉字连通块,其余一律当分隔符。
推理见 reference/,格式见 docs/format.md。
构建与测试
n-gram 路线
Windows 上用 PowerShell 7.0+:
python -m venv .venv .\.venv\Scripts\Activate.ps1 pip install -r requirements.txt pip install https://github.com/kpu/kenlm/archive/master.zip
需要 Python 3.12。
跑解码之前先生成拼音到单字的候选表:
python .\corpus\pinyin\make_single_pinyin_table.py
然后就可以运行 Viterbi 解码:
python .\ngram\viterbi.py
语料清洗的回归测试:
python -m unittest discover -s tests -v
神经路线
pip install -r neural/requirements.txt cd reference cargo test cargo clippy --all-targets -- -D warnings cargo fmt --check
参考实现除 serde 外无依赖,测试里手工构造模型文件并加载,因此它同时校验 docs/format.md 与实现是否一致。
参考资料
- open-gram: https://github.com/sunpinyin/open-gram
- nlp_chinese_corpus: https://github.com/brightmart/nlp_chinese_corpus