0. はじめに
現在デファクトスタンダードとなっているQWERTY配列がゴミであることは周知の事実です。
そのことに気がついた先人達によって様々なキー配列が考案されてきました。
代表的なものは、
- 英語の入力に適したDvorak配列、Colemak配列、Workman配列
- 日本語の入力に適した親指シフト、AZIK、薙刀式、大西配列、漢字直接入力
などです。
では、ここに書ききれないくらい大量の配列が考案されているにも関わらず「この配列が最も理にかなっているからこれに統一しよう」のような動きが出てこないのはなぜでしょう。
それは、キーボードのユーザーそれぞれ手の形や、よく打つ文字列、脳の作りが異なっているからだと思います。
そこで、本記事では焼きなまし法というアルゴリズムを用いて、各人の用途と特性に合わせたキー配列を作成する方法と実装を提供します。
1. コーパスを作る
まずは、自分のキーボード利用シーンに合わせたコーパスを作成します。
コーパスとは、大量に収集された言語資料のことです。
1.1 コーパスのもとになる文章を選ぶ
コーパスをもとにキーボードの配列の効率性を評価するため、普段日本語を多く入力する方は日本語のコーパスを、英語を多く入力する方は英語のコーパスを用意する必要があります。
自信のブログ記事や、日記、仕事で作成した文章などをそのまま利用するのが良いと思います。
1.2 コーパスをローマ字化する
1.1で日本語以外の言語を選んだ方、日本語をかな入力される方はスキップしてください。
次に、日本語の文章をキーボードで入力できる形に変換します。
上記のような変換ツールを使うのが簡単ですが、セキリュティ上の問題などで使えない場合はkuromojiや、mecabを使うことで手元での変換も可能です。
2. キーの押しやすさを決める
人それぞれ手の大きさや形、動かしやすい指には差があります。
そのため、自分の手での押しやすさに応じて十点満点で評価します。
人それぞれ違うと思いますが、おそらくほとんどの方は下記のようにホームポジションで押しやすいキーほど点が高く、人差し指→小指の順で点が高く並ぶと思います。
ここで、日本語を3000文字(ローマ字換算で7000文字)ほど打鍵したときにどのキーが何回押されたかを見てみます。
高得点が付きやすいと思われる中段人差し指がほとんど活用されていないことが分かります。オリジナル配列を作って最適化するモチベが湧いてきますね。
キーが何度押されたかを画像のように確認できるツールを公開しているので、自分が普段使う文章で試してみたい方はこちらからお試しください。
3. 指の連続使用を減らす
QWERTY配列しか使用したことがなかった時は気がつかなかったのですが、指の連続使用は明らかにタイピングの速度を下げます。
QWERTY配列をお使いの読者は「む」や「ざ」を試しに打ってみてください。「か」や「て」などを打つのにかかる時間と比べて、1.5倍から2倍程度時間がかかっているのがわかると思います。
基本的には指の連続使用は可能な限り減らした方が良いのですが、例えば人差し指の連続使用と上段小指が絡む異指の連続使用だと、どちらが打ちやすいかは人によって来ると思います。そのため、指の連続使用にどの程度ペナルティを与えるかをあらかじめ考えておく必要があります。
経験上、手順2で決めた指ごとの配点を使い下記の式に従ってペナルティ係数を算出すると丁度良いペナルティ係数を得ることができます。
$$
ペナルティ係数=((最も強い指の1番高い配点)+(最も強い指の2番目に高い配点))-((最も強い指の1番高い配点)+(最も弱い指の最も弱い配点))
$$
この式は、強い指での同指連続打鍵(QWERTY配列での「じゅ」J+Uなど)と弱い指での異指連続打鍵(「ず」Z+Uなど)を比較した差分をペナルティ係数とするという意味です。
4. 左右交互打鍵を増やす
押しやすいキーを多く押す、指の連続使用を減らすことのほかに広くタイピング速度の向上に効果があると言われているのが、左右交互打鍵を増やすことです。
QWERTY配列をお使いの方は、「か」と「こ」を押し比べてみてください。
こちらも左右交互打鍵ではなかった場合のペナルティ係数を決めておきます。
左右交互打鍵については検証不足のため適当に3としました。
5. 最適化するために式を作る
ここでやっと焼きなまし法の登場です。
焼きなまし法は、組み合わせ最適化問題を解くためのアルゴリズムです。
今回解くべき最適化問題は、
- コーパスの文章を全て打鍵した場合に
- 押しやすいキーができるだけ多く押され
- 同指連続打鍵ができるだけ少なくなり
- 左右交互打鍵ができるだけ多くなる
ように各キーを配置する。
つまり
$$
スコア=\sum_{ コーパス } ((押されたキーの配点) - 同指打鍵回数*(同指連続打鍵のペナルティ係数) - 左右交互打鍵ではなかった回数*(左右交互打鍵ではない場合のペナルティ係数))
$$
で表されるスコアを最大化するという最適化問題です。
最適化問題を解くためのアルゴリズムには実装が簡単な焼きなまし法(シミュレーテッド・アニーリング)を利用します。下記のように実装しました。コーパスと各指の押しやすさの評価をカスタマイズして実行していただくと、その条件に最適化されたキーマップが出力されます。
// 対象文字
const CHARS = "abcdefghijklmnopqrstuvwxyz,.-;";
// キーマップ初期値(QWERTY)
const INITIAL_LAYOUT = [
"q","w","e","r","t","y","u","i","o","p",
"a","s","d","f","g","h","j","k","l","-",
"z","x","c","v","b","n","m",",",".",";",
];
// コーパス
const CORPUS =
"watashi wa kaishain desu, kyou-wa ii tenki desu ne.nihongo no bunshou wo ro-maji de takusan nyuuryoku shite, ki-sutoro-ku no bunpu wo shiraberu.".replace(
/\s+/g,
""
);
// 指の割り当て
const FINGER_LIST = [
"L5", "L4", "L3", "L2", "L2", "R2", "R2", "R3", "R4", "R5",
"L5", "L4", "L3", "L2", "L2", "R2", "R2", "R3", "R4", "R5",
"L5", "L4", "L3", "L2", "L2", "R2", "R2", "R3", "R4", "R5"
];
// 押しやすさ
const LOCATION_POINT = [
1, 5, 6, 6, 3, 3, 6, 6, 5, 1,
4, 7, 8, 10, 5, 5, 10, 8, 7, 4,
1, 3, 3, 4, 1, 1, 4, 3, 3, 1
];
// layoutに対するindexの逆引きマップを作成
function buildIndexMap(layout) {
const map = {};
layout.forEach((c, i) => (map[c] = i));
return map;
}
function locationPoint(index) {
if (index) return LOCATION_POINT[index];
return 0;
}
// スコア評価
function evaluate(layout) {
let score = 0;
let prevFinger = "";
let currentFinger = "";
const indexMap = buildIndexMap(layout);
for (const c of CORPUS) {
const index = indexMap[c];
// 押しやすさの得点を加点
score += locationPoint(index);
currentFinger = FINGER_LIST[index];
if (currentFinger && prevFinger) {
// 同指連打の場合のペナルティ
if (currentFinger === prevFinger) {
score -= 5;
}
// 左右交互打鍵でない場合のペナルティ
if (currentFinger[0] === prevFinger[0]) {
score -= 3;
}
}
prevFinger = currentFinger;
}
return score;
}
// ランダムな2箇所を入れ替える
function neighbor(layout) {
const a = [...layout];
const i = Math.floor(Math.random() * a.length);
let j = Math.floor(Math.random() * a.length);
if (j === i) j = (j + 1) % a.length;
[a[i], a[j]] = [a[j], a[i]];
return a;
}
// 焼きなまし
function simulatedAnnealing({
initTemp, // 初期温度
finalTemp, // 収束温度
alpha, // 幾何冷却 T <- alpha * T
itersPerTemp, // 各温度での試行回数
} = {}) {
let current = INITIAL_LAYOUT; // 初期解
let currentScore = evaluate(current);
let best = [...current];
let bestScore = currentScore;
let T = initTemp;
let step = 0;
while (T > finalTemp) {
for (let it = 0; it < itersPerTemp; it++) {
const cand = neighbor(current);
const candScore = evaluate(cand);
const delta = candScore - currentScore;
if (delta >= 0 || Math.random() < Math.exp(delta / T)) {
current = cand;
currentScore = candScore;
if (candScore > bestScore) {
best = cand;
bestScore = candScore;
}
}
step++;
}
T *= alpha; // 冷却
}
return { best, bestScore };
}
// 実行
const result = simulatedAnnealing({
initTemp: 1000,
finalTemp: 1,
alpha: 0.99,
itersPerTemp: 50,
});
function printLayout10(layout) {
for (let i = 0; i < layout.length; i += 10) {
console.log(layout.slice(i, i + 10).join(" "));
}
}
printLayout10(result.best);
6. 作成した配列を評価する
上記のツール(自作。ご自由にご利用ください。)を利用して焼きなまし法で作成したオリジナル配列とQWERTY配列を比較して、作成した配列が本当に最適化されているか確認します。
QWERTY配列
QWERTY配列では、ホームポジションで押せるキーにあまり頻度が高いキーが割り当てられていないことがわかります。
また、同指連打は6700文字を打つ間に909回登場したことがわかります。
オリジナル配列
今回作成した配列では、ホームポジションに押される頻度が高いキーがまとまっていて、指の移動がQWERTY配列比で少なくなっていることがわかります。
また、同指連打も398回とQWERTY配列の半分以下になっていることがわかります。
AZIKのような変速ローマ字変換を使用しない方はここまででオリジナル配列完成です。
7. AZIKを考慮に入れる
AZIK(エイズィック)は、ローマ字変換規則を拡張することで日本語によく出てくる読みを少ないキー操作で入力する方法です。
AZIKを考慮に入れた最適かを行うにはコツがあります。
7.1 専用キーを割り当てる音を決める
まずは、拡張入力で入力する音を決めます。
今回は、(ann,inn,unn,enn,onn,ai,ei,ou,uu,ltu)を1キーで入力することにします。
7.2 専用キーを割当てるアルファベットと記号を決める
一度AZIKを考慮に入れずに最適化して
- 押される回数が少ない
- 母音側(aiueo)がまとまっている方の手に配置されている
を満たすキーに短縮入力したい音を割り当てます。
例えば下記のように設定します。
| 短縮入力する音 | 対応するキー |
|---|---|
| ann | v |
| inn | x |
| unn | f |
| enn | ; |
| onn | c |
| ai | q |
| ei | , |
| ou | l |
| uu | / |
| ltu | . |
7.3 専用キーを考慮に入れたローマ字コーパスを作成する
短縮入力を考慮に入れた変換は下記のコードで作成しました。
ひらがなを入れると専用キーを考慮にいれたローマ字文字列を出力します。
function hiraganaToAzik(str) {
const table = {
あ: "a", い: "i", う: "u", え: "e", お: "o",
か: "ka", き: "ki", く: "ku", け: "ke", こ: "ko",
さ: "sa", し: "si", す: "su", せ: "se", そ: "so",
た: "ta", ち: "ti", つ: "tu", て: "te", と: "to",
な: "na", に: "ni", ぬ: "nu", ね: "ne", の: "no",
は: "ha", ひ: "hi", ふ: "hu", へ: "he", ほ: "ho",
ま: "ma", み: "mi", む: "mu", め: "me", も: "mo",
や: "ya", ゆ: "yu", よ: "yo",
ら: "ra", り: "ri", る: "ru", れ: "re", ろ: "ro",
わ: "wa", を: "wo", ん: "nn",
が: "ga", ぎ: "gi", ぐ: "gu", げ: "ge", ご: "go",
ざ: "za", じ: "zi", ず: "zu", ぜ: "ze", ぞ: "zo",
だ: "da", ぢ: "di", づ: "du", で: "de", ど: "do",
ば: "ba", び: "bi", ぶ: "bu", べ: "be", ぼ: "bo",
ぱ: "pa", ぴ: "pi", ぷ: "pu", ぺ: "pe", ぽ: "po",
きゃ: "kya", きゅ: "kyu", きょ: "kyo",
しゃ: "sha", しゅ: "shu", しょ: "sho",
ちゃ: "tya", ちゅ: "tyu", ちょ: "tyo",
にゃ: "nya", にゅ: "nyu", にょ: "nyo",
ひゃ: "hya", ひゅ: "hyu", ひょ: "hyo",
みゃ: "mya", みゅ: "myu", みょ: "myo",
りゃ: "rya", りゅ: "ryu", りょ: "ryo",
ぎゃ: "gya", ぎゅ: "gyu", ぎょ: "gyo",
じゃ: "ja", じゅ: "ju", じょ: "jo",
びゃ: "bya", びゅ: "byu", びょ: "byo",
ぴゃ: "pya", ぴゅ: "pyu", ぴょ: "pyo",
っ: ".",
ー: "-","、": ",", "。": ".",
ぁ: "la", ぃ: "li", ぅ: "lu", ぇ: "le", ぉ: "lo"
};
let romaji = "";
for (let i = 0; i < str.length; i++) {
const two = str.slice(i, i + 2); // 2文字チェック(ゃゅょ対応)
if (table[two]) {
romaji += table[two];
i++; // 2文字消費
continue;
}
const one = str[i];
if (table[one]) {
romaji += table[one];
} else {
romaji += one; // 未対応文字はそのまま
}
}
const hTable = {
ann: "v",
inn: "x",
unn: "f",
enn: ";",
onn: "c",
};
// 二重母音
const diphthongTable = {
ai: "q",
uu: "/",
ei: ",",
ou: "l",
};
let result = romaji[0];
for (let i = 2; i < romaji.length; i++) {
const three = romaji.slice(i, i + 3);
if (hTable[three]) {
result += hTable[three];
i++;
i++;
continue;
}
const two = romaji.slice(i, i + 2);
if (diphthongTable[two]) {
result += diphthongTable[two];
i++;
continue;
}
result += romaji[i];
}
return result;
}
8. おわりに
最終的にできたのが下記の配列です。
短縮入力キーにより、もともと6,711打鍵必要だった文章を5,798打鍵で入力できるようになり約14%削減、同指連続打鍵も68回と約83%削減することができました。
オリジナル配列は作って終わりではなく、練習してQRWERTY配列よりも早く打てるようになって初めて意味があります。
しかし、マスターできればかなり業務効率を向上できると思います。
🔥練習頑張るぞ🔥
9. 練習風景👇





