自作キーボードに機械学習モデルを仕込む
このブログについて
40%キーボードのHold&Tap判定に機械学習モデルを導入してみたという話です。
Tap & Holdとは?
Tap&Holdというのは、1つのキーに2つのキーの役割を割り当てる機能です。
40%キーボードのようにキー数の少ないキーボードのスペース効率を高めることができる画期的な機能です。
具体的には、ちょい押しなら普通のキー、長押しなら修飾キー(またはレイヤ切り替え)といったように、ボタンを押す長さで二つのキーを判別する仕組みになっています。
このルールが基本ですが、人間は機械のように正確な動きはできないので、ホールドのつもりで押したのにタップと判定される、あるいは逆のタイプミス(誤爆)が頻発するので、他にもルールベースの対応がいくつか考案されてきた経緯があります。
ルールベースの例としては、例えばHold on other key pressという機能があります。この機能は、1つ目のタップキーが押されている間に別のキーが押されたというイベントを強制的にホールドとみなすというルールです。このルールはボタンを押す長さとは無関係に適用されます。
TAPPING_TERM
+---------------------------|--------+
| +-------------+ | |
| | LT(2, KC_A) | | |
| +-------------+ | |
| +--------------+ | |
| | KC_L | | |
| +--------------+ | |
+---------------------------|--------+
(source: QMK Firmware | Tap-Hold Configuration Options)
このルールでは、前提として、人間がタイピングするときは、一つ目のキーを押して離してから次のキーを押すという、二つのキー同士の押下イベントに重なりがないことを前提としています。しかしながら、人間は高速にタイプしているときに、一つ目のキーを離す前に次のキーを押すということを頻繁に行うので、こういうパターンの打鍵を問答無用でホールドとみなしてしまうと、例えばタイピング中にCmd+Cが押されて、クリップボードの文字列が画面に貼り付けられるといった誤爆が起こります。
タップ&ホールドも既存のルールを網羅的に説明することはここではしませんが、ルールをどんどん追加していくと、「あるパターンの誤爆には対応できたが、別のパターンの誤爆を誘発するようになった」といった、あちらを立てればこちらが立たない状況がよく起こります。そういった泥臭い調整に苦しめられた自作キーボードユーザは少なくないと思います。
今回のブログは、そういったルールの継ぎはぎでやってきた職人芸的な調整を、機械学習的なアプローチでもう少し整理できないか?というのを試してみた記録です。
Tap or Hold 分類問題
タップ&ホールドを機械学習のタスクとして見ると、2クラス分類になります。
クラスのラベルは、その人がタップのつもりで押したか、ホールドのつもりで押したかという2クラスです。Tap&Holdの基本ルールは、「ボタンを押す長さ」を特徴量とした閾値による単純な「線形分類」モデルとみなすことができます。ここではホールドとみなす時間の閾値がモデルのパラメータです。
次に、もう少し複雑なモデルを考えてみましょう。特徴量として、「ボタンを押す長さ」に加えて、「次のボタンが押されるまでの時間」を追加してみます:
- X: ボタンを押してから離すまでの時間(ms)
- Y: 1つ目のボタンを押した瞬間から、次のボタンが押されるまでの時間(ms)
実際に特徴量の分布を可視化してみましょう。以下の図は、実際に測定したタップ&ホールドキーの特徴量分布です。横軸がボタンを押す長さ、縦軸が次のボタンを押す時間です。

青いデータ点がTap、赤いデータ点がHoldを表しています。Tapの分布は左半分の上下に広く分布していて、Holdの分布は右側の領域の下の方に分布しているのがわかります。
より詳細に観察すると、Tapのデータ点については、ボタンを押す時間と、次のキーを押す時間については、ほとんど相関がないというのがわかります。これは直感的には、人間がTapとして打鍵するときは、キーが押されている時間はほとんど意識していないことを示していると考えられます。
一方、ホールドについては、ホールド時間と次のキーが押される時間に明確な相関が見られます。これはどういうことかというと、基本的にはホールドキーはシフトやコマンドといった修飾キー、またはレイヤー切り替えキーであり、別のキーと同時に押される前提のキーだからです。つまり、次のキーを押すまで離さずにホールドしているという状況を表しています。
これらのクラスを分類するにあたって、どのような境界線を引くことができるでしょうか?
最も単純な判別境界は、ボタンを押す長さの閾値に境界線を引く方法です。可視化すると以下の図のようになります。

実は直感的に、この基本ルールでもそれなりに良い判別境界線が引けてそうだということがわかります。ただ、いくつかのデータ点は境界線に近く、誤分類(誤爆)しやすそうな点があるということもわかります。
もう一つの単純な判別境界の例は、キーの重なりの有無で判別する方法です。可視化すると以下の図になります。

判別境界は直線
この境界線は、直感的にあまり良くない境界線であることがわかります。Tapのデータ点が右下の領域に食い込んでいるからです。つまり、少し前にhold on other key pressのところで説明したように、実際に連続する打鍵でキーの重なりが生じている状況を表しています。
さて、続いてこのブログの本題である機械学習モデルを導入するという部分になります。基本的には判別境界をどのように引くか?という問題を考えることになります。最もオーソドックスなアプローチとして、SVMによる線形分類を試したのが以下の図です。

この数十点程度のストロークのデータからは誤りがない直線を引くことができていますが、直感的にこの直線は過学習してそうだというのが想像できるかと思います。
実際、何度か同じ例文に対してストロークのデータを取得しましたが、その都度異なる直線が得られました。よりロバストな直線を引くためには、ストロークのデータをもっと増やす必要があるかもしれません。
いずれにせよ、特徴量を増やしたりサンプル点を増やすことができれば、単純な閾値での判別よりは賢い判別ができる余地がありそうです。
早期確定 - 不完全なデータでもレスポンスを返す
キーボードに機械学習モデルを導入する、という机上のアイデアの基本的な部分は以上です。
一方で、実際のキーボードにクラス分類モデルを組み込むにあたっては、これだけでは済まないところがあります。
基本的に機械学習は、オフラインでの学習の場合は、データ点が全て揃った状態で判別ということができるんですが、キーボードの場合はリアルタイムなデバイスなので、データが全部揃うまで待っているとレスポンスが悪いと感じるという特徴があります。従って、少しでもデータが揃ったら振る舞いを決めるということが重要になってきます。
このブログではこのような仕組みを 早期確定 と呼ぶことにします。
さいわい、今回設計した線形分類モデルの場合は明確な早期確定のロジックを組むことができて、以下では図を使って説明していきます。
1. Tapの早期確定ロジック
まず、Tapの早期確定から見ていきます。以下の図で50msの時点で先にXが確定する、つまり、ホールドキーを先に離されたとします。この場合、Yの値はまだ確定していませんが、Xは確定しているので、実際に確定するデータ点は直線

また、即時に確定できなかった場合も、待ち時間の最大値を縮めることができます。
例えば、最初のキーが押されてから150ms後にキーを離した状況を考えます。

X(離した時間)が確定した後、次のキーが押されるのを待っている間、データ点は時間が経つにつれて
2. Holdの早期確定ロジック
ホールドの早期確定も、Tapの早期確定とほぼ同様に考えることができます。今度は逆にホールドキーを離す前に次のキーが押されるという状況を考えることにします。
以下の図において、200msの時点で次のキーが押されたとします。
すると、実際に確定するデータ点は、直線
容易にわかるように、最初に次のキーが押されたタイミングがホールドの領域に入り込んでいる場合は、ホールドをどの時刻に離したとしてもホールドであるということは変わりません。したがって、次のキーが押された時点でホールドと確定して良いことがわかります。

まだ青い領域にいるときに次のキーが押された場合であっても、判別境界をまたいだ時点でホールドであることは確定するので、タイムアウトをこの時間に設定すれば、待つ時間は最小限にできます。

ラベル作成方法
この取り組みはデータとラベル作成が簡単に準備できることが重要なので、ラベルの作成方法について簡単に説明しておきます。具体的な手順は以下です。
- タップとホールドのそれぞれのキーが両方現れる例文を用意する
- ターゲットのキーのpush/release時刻のログを取得できる状態にする
- MonkeyTypingのようなタイピングソフトで例文をタイプする
- 例文と打鍵のログを突合させて「そのキーをタップとして押したか、ホールドとして押したか」を決定する
例えば私は右手親指のホームキーにスペースとシフトを割り当てています。この場合、スペースとシフトは一般的な英文に自然に現れるので、標準的な英文の例文を用意すれば十分でした。また記号レイヤなど少し特殊な例文はAIで生成しました。
シフトとレイヤ切り替えについてはこの方法でラベルを生成できると思います。一方でシフト以外の修飾キー(Ctrl, Cmd, Alt)についてはこの方法でラベルを生成するのは難しいかも知れません。
また、現状はラベル作成は自動化できても、例文を自分でタイプする必要があるので、大量のデータを用意するのは骨が折れます。
まとめ
このブログでは、自作キーボードという身近なデバイスに機械学習モデルを導入する方法を紹介しました。実際のコードは以下に公開したので、あまり整理されたコードではありませんが、具体的にどう実現したかについては実装を確認することができます(余談ですが、このコードは実験用に作って捨てるつもりでしたが、意外と普通に使うことができていて、レスポンス性・正確性の両面で快適に使うことができています)。
応用として、特徴量を増やしたり、データ点を増やすことができれば、よりロバストな判別モデルが実現できるかも知れません。
Discussion