Xime 输入法的手写输入技术实现
基于 StrokeTransformer 的手写汉字识别模型设计与 Xime 输入法落地实践。从 CNN 的局限出发,介绍了笔画轨迹序列分类方案,以及模型生命周期管理和两种手写键盘的实现。
大概在去年的时候,我有基于 CASIA-HWDB 1.0 和 CNN 卷积神经网络实现了手写汉字识别模型https://github.com/ximeiorg/ochw。原理是把数据转为图片,再通过卷积神经网络进行分类。
这种方案缺陷非常的大,所以只能是个demo。
基于 CNN 卷积网络 最大的问题是,它携带了在大量的无效信息。对于分类任务来说,它的粒度过于大,无法区分细节。
比如,在“金” 和“全” 这2个字中,CNN 可能会觉得它们的特征会比较像。
想要实用,还得基于汉字笔画轨迹序列来做文章。
StrokeTransformer
设计目标
这个模型要足够的小,足够的快,足够的省电,其次才是准确率。
整体架构
输入轨迹 (B, T, 5) 输出 logits (B, 7356)
│ ↑
▼ │
┌─────────────┐ ┌──────────────────┐ ┌──────────┐
│ Input Proj │───►│ Transformer │───►│ Classifier│
│ Linear(5→d) │ │ Encoder ×3 │ │ Head │
└─────────────┘ └──────────────────┘ └──────────┘
│ ↑
▼ │
┌─────────────┐ ┌──────────────┐
│ [CLS] Token │───►│ Positional │
│ concat │ │ Encoding │
└─────────────┘ └──────────────┘
StrokeTransformer 将手写汉字识别建模为笔画轨迹序列的分类问题。其核心思路是:将用户书写时产生的时空轨迹点序列,通过 Transformer 编码器建模点与点之间的长程依赖关系,从而捕捉汉字的笔画结构和整体形态。
核心编码器由 3 层 Pre-Norm 结构的 Transformer Encoder 堆叠而成,每层包含 4 头自注意力和一个 192→384→192 的前馈网络。自注意力机制使每个轨迹点能够聚合全局上下文信息——例如识别“口”字时,末笔横折的位置需要参考首笔起点的位置才能判断是否闭合。3 层的堆叠则逐步构建从局部笔画到全局字形的层次化特征。
分类输出时,取 [CLS] token 在编码器输出端的对应向量,经 LayerNorm 后送入 2 层 MLP 分类头(192→96→7356),输出 7356 个汉字类别的 logits。
整个模型不大,也就大概不到 200 万参数。导出到手机使用也就 6MB 的权重大小。虽然看着参数不多,但是训练完成后,在验证集上达到了 90% 的准确率。
对于这个结果,我的结论是,可用,但是离好用还差很远。但是至少它是离线的,不会收集你的笔画数据去做训练。
示例
复杂的字,比如 曦码输入法的 “曦”:

其他demo
我使用 Rust 的 candle 框架实现了 WASM 版本的 web demo => https://pro.ochw.ximei.me , 如果你感兴趣的话,可以去试一下。
Xime 输入法的实现
这个模型最后还是落地到 Xime 输入法里,做最后的呈现。
可能会有朋友疑问,为什么要给输入法加一个手写功能?
因为会有这么一个场景: 你看到了一个字,但是不知道它怎么念(假设你使用拼音),不知道它怎么拆字(假设你使用五笔),那要如何把它打出来呢? 答案是手写。
为了给 Xime 输入法实现手写功能,我这段时间做了大量的底层重构。
当前输入法已经存在了预测联想词、语音转文本、标点符号预测三种模型,这些模型功能不同,而且管理是分散的。因此,我把模型的管理做了重新的整合,首先是下载的整合,现在统一使用索引数据,并提供了统一的下载页面。

这么多个模型,初始化、加载、卸载这些功能也需要抽象出来,统一管理模型运行时生命周期。
我这里分了三个等级:
- HOT([keepWarm]):常驻内存,
onTrimMemory不会释放 - WARM(默认):引用计数归零后自动释放
- COLD / OFF:未加载
像联想词,一旦开始了之后,就会常驻,直到这个功能被关闭。
手写键盘
这里做了2种手写键盘:
第一种:手写模式,属于 Main 级别,和全键盘、笔画键盘、未来的T9键盘同属于一个级别。

第二种:手写找字,属于次级键盘,和符号键盘、数字键盘一样,所有键盘共享。

如你所见,手写找字会提供相应的反查编码功能。
这个功能会在2.5.0 上出现。