こんにちは、Karouです。今回は、「KUKU BATTLE MASTER」で実装した手書き数字認識システムについて、技術的な視点から振り返ります。外部APIやMLライブラリに頼らず、完全クライアントサイドで動く認識エンジンがどのように進化していったのか——5つのステージに分けてお話しします。
なぜ手書き認識を自前実装したのか
KUKU BATTLE MASTERは子ども向けの九九学習ゲームです。手書き入力を採用したのは、キーボードやテンキーよりも直感的で、小さな子どもでも使いやすいから。しかし、手書き認識のために外部APIを呼ぶとなると、いくつかの問題が出てきます。
- ネットワーク遅延が発生し、テンポの良いバトル体験が損なわれる
- API利用料がかかり、無料で提供し続けるのが難しくなる
- オフライン環境(PWA)で使えなくなる
- 子どもの手書きデータを外部に送信するプライバシー懸念
これらを考慮した結果、「完全クライアントサイドで動く認識エンジンを自前で作ろう」という結論に至りました。0〜9のたった10種類の数字を認識すればいい——スコープが限定されているからこそ、自前実装が現実的な選択肢になりました。
第1段階:テンプレートマッチング
最初のアプローチは、5x5グリッドでのテンプレートマッチングでした。キャンバスに描かれたストロークを5x5の二値画像に変換し、あらかじめ用意した各数字のテンプレートとピクセル単位で比較する、というシンプルな方法です。
実装は簡単でしたが、精度はお世辞にも良いとは言えませんでした。人によって数字の書き方は大きく異なります。「7」に横棒を入れる人、「1」をまっすぐ縦線で書く人と斜めに書く人。5x5の荒いグリッドでは、こうした個人差を吸収しきれません。
ただ、この段階で「方向性は正しい」という手応えは得られました。問題は解像度と判定ロジックにある——ここから改善の旅が始まります。
第2段階:特徴量ベース分類器
テンプレートマッチングの限界を突破するために、特徴量ベースの分類器に切り替えました。描画データから50以上の特徴量を抽出し、それらを組み合わせてスコアリングする方式です。
抽出した主な特徴量は以下の通りです。
- 5x5ゾーン密度:キャンバスを5x5のゾーンに分割し、各ゾーンのインク密度を計算
- アスペクト比:バウンディングボックスの縦横比。「1」は縦長、「0」は正方形に近い
- インク充填率:バウンディングボックスに対するインクの面積比
- 交差検出:水平・垂直方向のスキャンラインとストロークの交差回数
- 穴検出:閉じた領域(穴)の有無と数。「0」「8」は穴あり、「1」「7」は穴なし
この段階で精度は大幅に向上しました。特に穴検出と交差検出の組み合わせは強力で、「0」と「6」、「8」と「9」の区別が格段に良くなりました。
第3段階:ペア特化ルール
特徴量ベースの分類器でかなりの精度が出るようになりましたが、特定のペアで混同が頻発する問題が残りました。特に子どもの手書きでは、大人とは異なる独特の混同パターンがあります。
- 2と8/9の混同:子どもが書く「2」は曲線が大きく、「8」や「9」に見えることがある
- 5と2の混同:「5」の上部の横棒が短いと「2」と判定されやすい
- 6と0の混同:「6」の上部の曲がりが小さいと「0」に見える
- 4と9の混同:「4」を丸く書く子どもの場合、「9」と判定されてしまう
これらの混同ペアに対して、ペア特化の追加ルールを実装しました。たとえば「2と8の判定が拮抗した場合、上半分のインク密度が下半分より低ければ2」というような、具体的な条件分岐です。地道な作業ですが、この泥臭いチューニングが実用精度を支えています。
第4段階:適応型k-NN学習
ルールベースの限界は、「あらゆる人の筆跡をカバーしきれない」という点にあります。そこで導入したのが、適応型k-NN(k近傍法)です。
仕組みはこうです。ユーザーが書いた数字とその正解ラベルをlocalStorageに保存します。数字あたり最大20サンプル。次回以降の認識時、保存されたサンプルとの類似度をユークリッド距離で計算し、ルールベースのスコアにボーナスとして加算します。
スコアの計算式は以下の通りです。
score = rule_score + max(0, 15 - distance * 20)
この式により、保存サンプルに近い入力ほど高いボーナスを得られます。ルールベースの判定を基盤としつつ、個人の書き癖に適応していく——ハイブリッドなアプローチです。
使えば使うほど認識精度が上がるため、ユーザーからすると「最初はたまに間違えるけど、だんだん賢くなっていく」という体験になります。
第5段階:キャリブレーション画面
適応型k-NNの効果は絶大でしたが、「最初の数回の精度が低い」という課題がありました。学習データが蓄積されるまでの間、誤認識が発生するとユーザー体験が悪くなります。
この問題を解決するために、初回起動時のキャリブレーション画面を実装しました。0から9までの数字をそれぞれ書いてもらい、3重に保存することで初期学習データを確保します。これにより、最初のバトルからその子の筆跡に最適化された認識が提供できるようになりました。
キャリブレーションは1分もかからない簡単な作業ですが、この一手間が認識精度を劇的に改善します。特に個性的な筆跡を持つ子ども——たとえば鏡文字を書きがちな子や、極端に大きい・小さい字を書く子——にとって、この仕組みは非常に効果的でした。
得られた知見
手書き認識を自前実装して学んだこと
- 完璧な認識より「ユーザーに合わせる」方が満足度が高い:汎用的な高精度モデルよりも、その人に適応するシンプルなモデルの方が、体感的な精度は上になる。100%の認識率を目指すよりも、95%の認識率+適応学習の方がユーザー満足度は高い。
- 外部MLライブラリなしでも、特徴量設計で十分な精度は出せる:TensorFlow.jsやONNX Runtimeを使わなくても、対象が限定されていれば(今回は0〜9の10種類)、手作業で設計した特徴量とルールベースの分類器で実用的な精度に到達できる。バンドルサイズも小さく済む。
- 誤認識は「再入力」でなく「修正ダイアログ」で対応する方がUXが良い:認識結果が間違っていたとき、「もう一度書いてください」ではなく「これで合っていますか?」と確認ダイアログを出す方が、ユーザーのストレスは格段に少ない。特に子どもは、自分の字が否定されたと感じると意欲を失いやすい。
手書き認識は、機械学習の花形のように思えますが、実際にはドメインに特化した地道な工夫の積み重ねがものを言います。外部に依存しない自前実装だからこそ、プロダクトの要件に合わせた柔軟なチューニングが可能になりました。