我每天混着写日语、英语和中文,最磨人的其实是切换输入法。
忘了按 Ctrl+Space,打出 nihao 却得到「にはお」,删掉,切换,再打一遍。
一天要来几十次。
那不如让输入法自己从按键序列里猜语言。于是我给 fcitx5 + hazkey(AzooKey 引擎 + 神经网络假名汉字转换 Zenzai)加上了英语和中文的自动识别。
不用切换键,kyouhaiitenkidesune(今日はいい天気ですね)、meeting、nihao 都在同一个状态下直接打。
用了一阵,我发现一件怪事:多打一长段再转换,需要改的次数明显更少。 这和一般输入法的用法正好相反,所以我实际测了一下。
一般的输入法,切碎了反而轻松
日语输入法用久了的人,大多习惯一个文节(短语)一转、确定,或者几个文节一转。 整句一次性转换(连文节转换)早就有了,但长句一旦转错,改文节边界、重选候选词都很麻烦。 勤确定,转错时损失就小。这是很合理的习惯。
可是在自动识别语言的输入法里,这个前提不成立了。原因有两个。
- 输入越短,语言识别越容易错。
nihao(你好)、wo(我)、ma(吗)都能顺理成章地读成日语罗马字。 我做了一个只看按键序列猜语言的分类器,按长度测:3 个字母时准确率 89%,8 个以上 99%。 短输入里根本没有足够的信息来判断。 - 转换模型看的是整段输入。 Zenzai 把整段输入当作上下文。 「かれは」该转成「彼は」(他)还是「枯葉」(枯叶),不看后面接什么就定不下来。
做了什么
- 英语识别:字符 1–5gram 的对数似然比(英语 vs 日语罗马字)。像英语就把原样的字母放在第一候选。
长输入里夹着英语单词时,用动态规划把它切成英语段和日语段。
例如
kinouhagithubnosetteiwokaeta→ 「昨日はgithubの設定を変えた」(昨天改了 github 的设置)。 - 中文识别:在英语识别之后,用同样形式的 n-gram 判断是拼音还是日语罗马字。 像中文就交给 libime 转换。它和 fcitx5 自带拼音输入法用的是同一个引擎,单独跑一个进程(每个短语 2.8ms)。
- 上下文:光标前的文字里有假名就偏向日语,没有就偏向最后一次确定的语言。 需要后者,是因为 Discord 这类应用不会把前后文传给输入法。
日语罗马字和拼音重叠得很厉害:46% 的日语片段也能拆成合法的拼音音节。 所以「能不能读成拼音」几乎帮不上忙,真正起作用的只有长度和上下文。
怎么测的
用 Tatoeba 的句子,按键序列完全相同,只改切分方式:
- 每个文节转换、确定(不传上下文,模拟不传前后文的应用)
- 每个文节转换、确定(把已确定的文字作为下一次转换的上下文)
- 每两个文节 / 每四个文节
- 整句一次
每段都用第一候选确定,和标准答案比较字符错误率(CER)和整句完全正确的比例。 另外还测了「边改边打」:每一段如果第一候选不对、而列表里有正确答案,就选它。 第一候选不对的次数记为「修改次数」。为了不让学习功能影响结果,每句都重启一次服务器。
数据三组,各 150 句:
- 纯日语
- 日语夹英语(把「名词 + 助词」中的名词换成
github、meeting等) - 中文(用 jieba 分词)
结果
纯日语(平均 5.2 个文节)
| 一次转换的长度 | 错误率 | 整句正确 | 修改次数/句 |
|---|---|---|---|
| 逐文节·无上下文 | 11.9% | 31% | 1.21 |
| 逐文节·有上下文 | 6.8% | 57% | 0.60 |
| 每两个文节 | 4.1% | 64% | 0.42 |
| 每四个文节 | 3.8% | 66% | 0.37 |
| 整句 | 3.6% | 68% | 0.32 |
即使是几乎不涉及语言识别的纯日语,也是越长越好。改善分两步。
- 从无上下文到有上下文,修改次数减半。这是左侧上下文的作用。
- 从逐文节到整句,又减半。这是右侧上下文,也就是后面接什么的作用。
左侧上下文可以传给输入法,右侧上下文只存在于打字的人脑子里。 典型的错误是「かれは」→「枯葉」:逐文节确定,就得在看到后面的「シャーウッドの森を」(舍伍德森林)之前做决定。
日语夹英语(平均 5.5 个文节)
| 一次转换的长度 | 错误率 | 整句正确 | 修改次数/句 |
|---|---|---|---|
| 逐文节·无上下文 | 11.5% | 31% | 1.17 |
| 逐文节·有上下文 | 7.3% | 50% | 0.68 |
| 每两个文节 | 6.3% | 55% | 0.50 |
| 每四个文节 | 5.9% | 56% | 0.45 |
| 整句 | 5.9% | 59% | 0.41 |
趋势相同,到四个文节左右就到顶了。
中文(平均 7.2 个词)
| 一次转换的长度 | 错误率 | 整句正确 | 修改次数/句 |
|---|---|---|---|
| 逐词·无上下文 | 66% | 12% | 1.85 |
| 逐词·有上下文 | 85% | 12% | 1.84 |
| 每两个词 | 34% | 42% | 0.86 |
| 每四个词 | 23% | 55% | 0.62 |
| 整句 | 14% | 57% | 0.43 |
这里差别最明显。一个词一个词地打,根本不会被当成中文。
你快要走了。 → に快要走了。
你和我有一些共同点。 → にheをyouyixiegongtongdian。
ni、he 只有两个字母,读成日语罗马字反而更自然。
传上下文甚至更糟(66% → 85%)。第一个 ni 变成「に」之后,它就成了「前面是日语」的上下文,
把后面的拼音也拉向日语。错的结果成为下一次判断的依据,错误一路传下去。
整句打的话,七个词的拼音几乎不可能被认成别的语言。
打字方式反过来了
在一般的输入法里,切碎是保险,转错时损失小。 在自动识别语言的输入法里,切碎本身就是出错的原因。
- 纯日语:整句所需的修改次数约为逐文节的一半(0.60 → 0.32)
- 中文:约为四分之一(1.84 → 0.43)
- 最差的是在不传上下文的应用里逐文节打(日语 1.21,中文 1.85)
长输入不再是「错了很麻烦」的东西,而是「让它转对的材料」。 实际用下来,我也自然变成了一直打到标点再按空格。
测量才发现的 bug
这次测量与其说是为了写文章,不如说更像是一次实现的体检。
- 句中有逗号的中文句子全都转乱了。 libime 读不了带标点的输入,
nihao,wojiaoqingshui.变成了一串日语假名。中文句子里有 15% 句中带逗号。 改成按标点切开分别转换后,整句的错误率从 39% 降到 14%。上面的中文表格是修复后的数字。 - 较长的输入里,
youtube会变成「ようつべ」(日语里 YouTube 的俗称)。 逐文节打时能保留英语, 两个文节以上一起转换就输给了 Zenzai 的转换。尚未修复。
一句一句手动试的时候,这两个都没发现。 逗号的 bug 正好打在「越长越好」这个结论上。要是不测就写文章, 我会把中文的结果误读成「长了也没好多少」。
这些数字的局限
- 错误率包含写法差异(ご飯/ごはん、行った/いった)。各种切分方式都按同样条件计算,不影响比较。
- 「修改次数」假设的是理想的用户:列表里有正确答案就一定会选。
- Tatoeba 的句子较短(约五个文节)。更长的段落会到顶还是反而变差,还没测。
- 中文按 jieba 的词切分,可能和人实际停顿的地方不同。
- 只测了第一候选是否正确,没测转换耗时。Zenzai 在 CPU 上每句约 200ms,整句转换比打字时的实时候选更重。
结语
「多打一段更好」本来只是用着用着隐约感觉到的。 一测才知道,纯日语也有明显差距,而中文的差距大到不改打字方式就没法用。 输入法好不好用,取决于引擎和用户「在哪里切」的习惯, 引擎变了,最合适的习惯也会跟着变。
