けものみち

まったりと、きのむくままに。

Confusion Matrix を用いたタイピングの打鍵精度表現を考えてみる

概要

タイピングゲーム・練習サイトにおける打鍵の精度(正確さ、正確率など日本語の表記ゆれは多数あり)を混同行列(Confusion Matrix)を用いて表現してみました。

モチベーション

e-typing の全国平均データ

タイピングゲーム・練習サイトにおいて「精度」(Accuracy)といったとき、一般的には以下の式で算出されます。

\text{精度} = \dfrac{\text{正しく打った打鍵数}}{\text{総打鍵数}}

打鍵数ベースではなく、文字数ベースであれば、

\text{精度} = \dfrac{\text{正しく打った文字数}}{\text{総文字数}}

と算出されることでしょう。

この指標は、シンプルで理解しやすいのが利点です。一方で、何の文字をどれくらい間違えたかといった情報はすべて失われています。

そう考えると、「どの文字でミスタイプしたか」というのが気になってくると思います。

もっともシンプルな方法でミスタイプ数を計算するのであれば、入力すべき文字に対して違う文字が入力された場合、入力すべき文字のミスタイプ数を1増やしていくというのが考えられます。 例えば、a という文字を入力するべき時に、b を入力したとき、a のミスタイプ数を1増やします。

この方法はわかりやすいですが、出現頻度が異なる文字を単純な打鍵数ないし文字数で比較してよいのか、という点は気になるとは思います。

en.wikipedia.org

Wikipedia からの引用で、言語ごとにアルファベットの出現頻度を並べてみたものが掲載されています。例えば、E の出現頻度が高いとか顕著だと思います。 E がたくさん出てくるため、E のミスタイプ数が増えてしまうのは自然なことだとは思いますが、Eのミスタイプ数が多いからといって「Eが自分にとって苦手なキーである」と言い切れるかというと根拠に乏しいとは思います。

さらに、E を打つべきところでミスタイプしたとき、どのキーでミスタイプしているのか?という情報は欠落しているので、自分の打鍵の癖がわかりにくいというのももったいないように感じます。

さて、これらのことから、

  • 何の文字をどれくらい間違えたか見てみたい
  • 文字の出現頻度をある程度均等にならしたうえで苦手なキーを見てみたい
  • ミスタイプしたときどういうミスをする傾向があるのかを見てみたい

というモチベーションが出てきます。

混同行列(Confusion Matrix)

ja.wikipedia.org

混同行列についての詳しい説明は割愛します。難しくはないので適当にググって読んでみてください。

この混同行列をマルチクラスに拡張して、タイピングに応用してみましょう!

※以下の内容は難しければ、最後の「実験」まで読み飛ばしてもらって構いません。わかりやすくかみ砕いた説明を用意してあります。

定式化

文字の集合を $C$ とします。例えば、すべてのアルファベット小文字 "a"~"z" を取り扱う場合、

 C=\lbrace \text{"a", "b", "c", ..., "x", "y", "z"} \rbrace

といった感じになります(数学的にこの表記が正しいかどうかはさておき、文字が要素であることがわかればよいです)。

集合 $C$ の濃度(有限集合なので素数)を $|C|$ とあらわすことにしましょう。

さて、ここまで準備できれば、混同行列を作成することができます。具体的には、混同行列 M を $|C| \times |C|$ 行列とし、 $i$ 行 $j$ 列目の成分は、正しい文字 $c_i \in C$ に対してユーザが文字 $c_j \in C$ をタイプする確率 $p_{ij}$ で表すこととします。

 M = \begin{pmatrix} p_{11} & \cdots & p_{1j} & \cdots & p_{1|C|} \\
 \vdots & \ddots & \vdots & \ddots & \vdots \\
p_{i1} & \cdots & p_{ij} & \cdots & p_{i|C|} \\
 \vdots & \ddots & \vdots & \ddots & \vdots \\
p_{|C|1} & \cdots & p_{|C|j} & \cdots & p_{|C||C|} \end{pmatrix} \\
\displaystyle{
\sum_{j=1}^{|C|}{p_{ij}} = 1
}

初期値は精度100%としたいので、クロネッカーのデルタ  \delta_{ij} を用いると以下のようになります(要するに、初期値は単位行列です)。

 M = (\delta_{ij}) = \begin{pmatrix}
 1 & 0 & \cdots & 0 \\
 0 & 1 & \cdots & 0 \\
 \vdots & \vdots & \ddots & \vdots \\
 0 & 0 & \cdots & 1
\end{pmatrix} \\
\delta_{ij} = \begin{cases}
1 & (i = j) \\
0 & (i \ne j)
\end{cases}

実際の実装をするときは、 $|C|$ が大きくなるほどスパースな行列(つまり、成分が0となる箇所が多い行列)になりがちであることと、浮動小数点数での計算が面倒で誤差もあるので、打鍵数ベースで必要なデータだけを持つように実装します。 C# でクラス設計をすると例えば以下のようになります。文字が打たれるたびに打鍵数データを更新していきます。

public class TypingConfusionMatrix
{
    private static readonly string[] _characterSet =
    {
        "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
        "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-", ",", ".", "/", ";", ":", " "
    };

    private readonly Dictionary<string, Dictionary<string, int>> _countDataDictionary = new();

    public TypingConfusionMatrix()
    {
        // 確率1で初期化
        foreach (var character in _characterSet)
        {
            _countDataDictionary[character] = new Dictionary<string, int>() { { character, 1 } };
        }
    }

    public void UpdateConfusionMatrix(string correctCharacter, string inputCharacter)
    {
        // 文字列集合に含まれていない文字が渡された場合は処理しない
        if (!_characterSet.Contains(correctCharacter) || !_characterSet.Contains(inputCharacter))
        {
            Console.WriteLine($"文字列集合に含まれていない文字が渡されたので混同行列の更新は行いません(correctCharacter = {correctCharacter}, inputCharacter = {inputCharacter}");
            return;
        }

        if (!_countDataDictionary.TryGetValue(correctCharacter, out var currentCountData))
        {
            Console.Error.WriteLine($"文字列集合には文字 {correctCharacter} が含まれていますが、打鍵カウントデータがありません");
            return;
        }

        if (currentCountData.ContainsKey(inputCharacter))
        {
            var currentCount = currentCountData[inputCharacter];
            var newCount = currentCount + 1;
            currentCountData[inputCharacter] = newCount;
        }
        else
        {
            currentCountData.Add(inputCharacter, 1);
        }
    }

    public string DumpCountData()
    {
        var sb = new StringBuilder();
        const string header = "correct,input,count\n";
        sb.Append(header);

        foreach (var data in _countDataDictionary)
        {
            var correctCharacter = data.Key;

            foreach (var countData in data.Value)
            {
                var inputCharacter = countData.Key;
                var count = countData.Value;
                var dataString = $"{correctCharacter},{inputCharacter},{count}\n";
                sb.Append(dataString);
            }
        }

        return sb.ToString();
    }
}

実験

KIH2023 で自作した MimicTypeGenerator くんに打鍵ログをとる仕組みを忍ばせ、数分間適当に打鍵します。その後、打鍵データを dump します(ローカルなのでデプロイしていません)。

さて、dump した打鍵データ(混同行列)を jupyter notebook など適当に python の力をお借りしてヒートマップに可視化してみましょう。えいえいっ!

適当に数分打ったときのヒートマップ

$i$ 行 $j$ 列目の成分は、正しい文字 $c_i \in C$ に対してユーザが文字 $c_j \in C$ をタイプする確率 $p_{ij}$ でした。 今、ヒートマップが赤いほど確率が1に近く、青いほど確率が0に近いので、対角成分が赤いということは精度が高いということを表しています。

下から7行目の Hyphen の行の対角成分だけ黄緑色ですね。これはハイフン(-)を打つ時の精度が低いことを表しており、実際成分をみると 0.57 と書いてあるのでハイフンは 57% しか正しく打てていないことがわかります。また、Hyphen を打つときに、e、i、r、uが青くなっていることから打ち損じがある可能性、0を押していることから隣のキーを押してしまっている可能性が推測できます。どうやら私はハイフンが結構苦手なようです。

上記のデータはまあまあ精度高く打ってしまったので、もっとスピードを上げてミスりまくってみましょう。

対角成分が橙色の行はやや精度が低い行です。例えば m の行は対角成分が 0.81 なので m の精度が 81% で、そのときほかのキーにまばらに0以外の数値が入っているので、mの打ち損じが多かった可能性が推測できます。

さらに、乱打してみましょう。手動で乱打するのはめんどくさいので、10万打鍵適当な文字を打つスクリプトを書いて回しました。自作サイトでローカル環境なのでいくら雑に荒らしても全く問題がありません。

全体的に青くなりましたね。よさそうです。

対角成分が1になっている部分がところどころあるのは、課題文章にその文字を打つケースが出てこなかった、その文字が正解だとしてもその文字を打たなかったことが想定されます。例えば q とか数字とか出てなさそうですね。

メリット・デメリット

メリット

  • 文字の集合さえ定義されていれば計算可能
  • 何の文字をどれくらいの確率でどのように間違えるかが可視化できる
  • 文字の出現頻度を考慮できている

文字ごとの精度、どう打ち間違えたかが可視化されるのでより客観的な分析ができそうな気がしますね。

デメリット

  • 正確なデータを取るには相当打鍵数が必要
    • 特に出現頻度が低いものはなかなかデータが取れない
  • 文字の集合の要素が増えるとデータ取り直しになる(文字の集合が固定されている必要性がある)
  • 解釈が難しい
    • 対角成分以外の小さい値をどう評価するかが難しい

やるまえから予測できてはいましたが、数千打鍵程度だと全然特徴が表れてこないので、たくさん打ってデータを蓄積する必要があります。 しかも、文字の種類が増えるほど行列がスパースになるし、対角成分以外の値が小さくなりがちで評価が難しくなる傾向にあります。

終わりに

タイピングにおける「精度」という概念を、混同行列という方法を用いて表現してみました。応用可能性はあると思うのでぜひアレンジして試してみてください。