第2章:「よかった」が「よがんた」になる

全体像の中でこの章が扱う部分: 主経路(詳細は付録「全体像」を参照)

「よかった」と打ったつもりが、画面には「よがんた」という文字列が出ることがありました。 ローマ字入力ではなく、親指キーとの同時打鍵で仮名を決めるNICOLA方式ならではの化け方です。 入力を受け取る側のコードに誤りがあったわけではありません。二つのキーがほぼ同時に押された とき、その「ほぼ同時」をどちらの文字に結びつけるべきかという判定そのものが、際どい場合には 決めようがなかったのです。

第1章では、キーを受け取り、NICOLAの規則で判定し、仮名を出力するまでの経路を 組み立てました。この章で扱うのは、その経路の中でもとりわけ小さな、しかし避けようのない 曖昧さです。二つのキーがほぼ同時に届いたとき、どちらを先とみなすべきか。時間だけでは 決め切れないこの問いに、awaseは意外な道具で答えることになります。

三つのキーが並んだとき、親指はどちらと組むか

NICOLA方式の親指シフトでは、文字キーと親指キーをほぼ同時に押すことで一つの仮名を確定 させます。ここで厄介なのは、char1→thumb→char2という順で三つのキーが短時間に並んだ 場合です。親指キーは、直前の文字キー(char1)と組むべきか、直後の文字キー(char2)と 組むべきか。両者は排他的な二つの仮名を生み出すため、どちらか一方に決めなければなりません。

src/engine/timing.rsには、この判定を「3キー仲裁」と呼ぶコメントが残っています。

/// 3キー仲裁: char1→thumb→char2 の並びで、thumb をどちらとペアリングするか。

最初の判定方法は単純でした。char1とthumbの間隔(d1)と、thumbとchar2の間隔(d2)を比べ、 d1のほうが短ければchar1と組む、というものです。人間の指がキーを押す物理的な間隔を、 そのまま判定材料にする発想であり、これ自体は自然な出発点でした。

タイミングだけでは、決まらない場合がある

ところがこの単純な比較には、見過ごされていた偏りがありました。自然な打鍵では、直前の キーとの間隔(d1)のほうが、直後のキーとの間隔(d2)より短くなりやすいのです。指の運動として 「押してすぐ次を押す」ほうが「押してからやや間を置いて次を押す」より起こりやすいという、 人間の側の癖でした。

この癖のせいで、「か」に続けて右親指(濁点相当のシフト)を押し、その後「ん」を打つと、 d1が短くなりがちなために「か+右親指=が」が選ばれ、続けて「ん」「た」と打った結果が 「がんた」になってしまいます。「よ」「か」「thumb」「っ」「た」という五打鍵のうち、 中央の三打鍵の親指帰属だけが逆に判定され、「よかった」ではなく「よがんた」という、 意味の異なる文字列が出力されていました。

打鍵のたびに毎回起こるわけではありません。d1とd2の差が十分に大きければ、タイミングの 比較だけで問題なく判定できます。問題が起きるのは、二つの間隔が拮抗し、どちらとも言い切れない ときだけでした。この「際どいときにだけ間違える」という性質が、原因の特定を難しくして いました。

統計的な文脈を、判定材料に加える

タイミングが拮抗しているなら、タイミング以外の材料で決めるしかありません。ここでawaseが 持ち込んだのが、直前までに確定した文字列という文脈でした。「よ」の後には「かった」が続き やすく、「よ」の後に「がんた」が続く頻度は低い。この日本語としての自然さを、確率として 持っておけば、タイミングが決め切れない場合の判定材料になります。

src/ngram.rsには、この考え方を実装したNgramModelが定義されています。持っている データは二つの頻度表(直前1文字から見た二文字の連なりの確率をbigram、直前2文字から 見た三文字の連なりの確率をtrigramと呼びます)と、そこから導いた閾値を何マイクロ秒まで 動かしてよいかという上下限だけです。文字の並びを丸ごと覚えるのではなく、「この二文字 (三文字)がどれだけ自然に連続するか」という一点だけを、あらかじめ数値にしておく設計 でした。

このモデルを最初に実装したコミット6990a7dは、2026年3月28日21時17分のものでした。 awaseの開発が始まったまさにその日、プロトタイプが動いた同じ日のうちに、この統計モデルの 土台はすでに置かれていたことになります。

three_key_pairingの判定フローは、次の三段階でした(timing.rsのコメントより)。

判定フロー:
1. n-gram なし → タイミング比較(d1 < d2 なら char1)
2. タイミング差が大きい(30%マージン超)→ タイミング優先
3. タイミングが接近 → n-gram スコアで判定

図にすると、タイミングの差が大きい間はn-gramへ迂回しないことがはっきりします。

3キー仲裁の判定フロー: タイミング差が大きければタイミングだけで決め、拮抗した場合だけn-gramスコアを参照する

n-gramスコアは、タイミングに取って代わるものではありません。呼び出されるのは、 タイミングが際どく単独では決め切れない場合だけでした。

Wikipediaをひらがなにして、頻度表を作る

このbigram/trigramの頻度データは、どこから来たのでしょうか。data/ngram_hiragana.csv.gzの ヘッダーには、出所が明記されています。

# Auto-generated hiragana n-gram frequency table
# Source: Japanese Wikipedia (CirrusSearch dump)
# Analyzer: Sudachi.rs (UniDic) + ambiguous reading correction

ARCHITECTURE.mdにも、同じ手順がやや詳しく記されています。

日本語の百科事典の本文まるごとを、意味のある単語としてではなく、ひらがなの連鎖という 音の並びとして数え直す。もとは知識を記述するための文章が、ここでは「どの仮名の後に どの仮名が続きやすいか」という、タイミング判定のための統計値に姿を変えています。

生成過程を実行した具体的なツールも、探してみると見つかりました。build-ngramという、 awase本体とは別の小さなRustプロジェクトです。Wikipediaのダンプから短すぎる文を除いて 取り出し、Sudachi.rsで形態素解析してからbigram・trigramの出現頻度を数え上げます。 この過程では「今日」は次に「は」が続けば「コンニチ」(こんにちは)、それ以外は「キョウ」と 読むというように、同じ表記でも前後の文脈で読みが変わる語(多読字)をルールベースの表で 補正しています。

このツール自体のファイルが最後に書き換えられた時刻を見ると、awase本体にNgramModelが 組み込まれたコミット(21時17分)より何時間も前、同じ3月28日の早朝から午後にかけて、 Wikipediaを処理するための専用ツールが先に作られていたことが分かります。プロトタイプが 動いた同じ一日のうちに、コーパスを作る側の作業まで進んでいたのです。

生成されたデータは、決して小さくありませんでした。TOML形式で5.7MB、25万エントリ。 これをアプリに同梱するには重すぎます。3月30日、CSV+gzip形式への変換で1.3MB(23時41分の コミット)まで圧縮し、さらに8分後、スコアが一定より低い低頻度のtrigramを間引くことで 240KBまで削りました。25万エントリのうち、実際に残されたのは4万3千エントリほどです。 どちらの段階でどれだけ小さくなったかをまとめると、次のようになります。

頻度表は、同梱までに二段階で圧縮された

際どさの外側では、統計は口を出さない

n-gramスコアの使われ方をもう一度確認します。frequency_score()はまずtrigramを参照し、 無ければbigramにフォールバックし、それでも無ければ0.0という中立値を返します。このスコアを tanhで-1から1の範囲に正規化し、既定では前後20ミリ秒の調整幅に掛けて、30ミリ秒から 120ミリ秒という範囲に収まるよう閾値を動かします。

大事なのは、この仕組みが「かな漢字変換の候補を選ぶ」ためのものではないという点です。 awase自身は、かな漢字変換を行いません。それはOS側のIMEの役目です。awaseが決めているのは、 あくまで「どの一文字の仮名を確定させるか」という、もっと手前の一点だけでした。

2026年5月8日、1f93e7dというコミットで、このn-gramモデルを設定ファイル上で明示的に 有効化する変更が入りました。コミットメッセージには、この章の冒頭で挙げた「よかった」が 「よがんた」になる現象が、3キー仲裁でn-gramモデルが効いていないことに起因すると 記録されています。

この3キー仲裁を呼び出す側のNicolaFsm自体にも、見過ごされていた問題がありました。 初期の実装は、再帰呼び出しによるresolvehandle_idlecombine_prev_and_newという 形で書かれていました。動いてはいましたが、読みやすいとは言えないコードでした。3月30日の ADR-015は、この実装が抱える問題を次の三点に絞って言語化しています。

ADR-015が採った解決は、起こりうる遷移のパターンをあらかじめParseActionという列挙型に 書き出すことでした。実装を担ったClaude Opus 4.6が積んだ同日中の二つのコミット (0edf8e8526f898)で、手動マージだったcombine_prev_and_newは削除され、分散していた update_historyの呼び出しもループ内の一箇所に集約されています。「何が問題か」を先に 三点へ絞って言語化してから実装を任せる進め方は、この章のn-gramモデルと同じく、際どい 判定を確実に処理へ落とし込むための工夫の一つでした。

他分野への転用

際どい判定にだけ、独立した手がかりを足す

一つの観測だけでは判定が際どくなる場面は、日本語入力に限りません。予測変換やオート コンプリートも、直前の入力だけでは次の候補を絞り切れないとき、単語の出現頻度という 統計的な文脈を判定材料に加えます。センサーフュージョンの分野でも、単一のセンサー値が 拮抗して読み取れないとき、過去の動きのパターンや周囲の情報といった、観測そのものとは 別の手がかりを組み合わせる設計が広く使われています。awaseがタイミングにn-gramを足した 構図は、この一般的なパターンの一例にすぎません。

ただし、この組み合わせは万能ではありません。観測同士の差が十分大きいときには、単一の 観測をそのまま信じるほうが速く、かつ正確です。統計的な補正は、観測同士が拮抗している、 まさにその瞬間にだけ呼び出されるべきものであり、常時介入させると、かえって本来明確 だったはずの判断を曖昧にしてしまいます。

タイミングが意味を持つ入力は、層に分けにくい

NICOLA方式のように、キーを押す間隔そのものが確定する文字を左右する入力方式は、この本が 初めて出会った設計ではありません。身近な例で言えば、スマートフォンの画面が「タップ」と 「長押し」を区別するのも、同じ仕組みです。指を離すタイミングだけで、まったく違う動作が 起こります。

この種の入力に共通するのは、「まず状況を把握してから、それに基づいて決める」という素直な 二段階では割り切れない、という性質です。ロボティクスの分野では、知覚・計画・行動という 決まった順序でパイプラインを組む設計に対し、Rodney Brooksが提唱したSubsumption Architectureが、その順序そのものへ異議を唱えたことで知られています。NICOLA方式が 「タイミングが意味そのものを担っている場合、判定を知覚と決定にきれいに層分離できない」と 教えてくれるのは、この一般論を、親指シフトという具体的な入力方式でなぞり直した結果に すぎません。


設計原則

原則: 一つの観測だけでは判定が際どくなる場面には、独立した第二の手がかりを用意しておきます。

適用条件: 観測同士の差が十分に大きい場合は、単一の観測だけで判定して構いません。第二の手がかりが必要になるのは、観測が拮抗し、単独では決め切れない場合に限られます。

実装の形: 主たる観測(このawaseの例ではタイミング差)による判定を基本とし、差が閾値を下回るときにだけ、統計的な文脈や事前知識といった別種の手がかりを参照します。

限界: 第二の手がかり自体も、確率的な傾向にすぎません。まれな連鎖や、統計に現れない新しい文脈に対しては、判定を誤る余地が残ります。