漢直における動的な交ぜ書き変換、および形態素解析との融合について


## 漢直とは
「[漢字直接入力](https://ja.wikipedia.org/wiki/%E6%BC%A2%E5%AD%97%E7%9B%B4%E6%8E%A5%E5%85%A5%E5%8A%9B)」を省略して「漢直」と称しています。キーボードからの２ストローク以上の打鍵列に対して漢字１文字を割り当てて、MS-IME や Google 日本語入力[^google_ime]などに頼らずに漢字を直接に入力できるようにするものです。

[^google_ime]: Google 日本語入力では、ローマ字テーブルをカスタマイズすることで、漢直を実現することができます。筆者が公開している [KanchokuWS](https://github.com/oktopus1959/KanchokuWS/releases) はそのローマ字テーブルをファイル出力する機能を提供しています。

筆者が使用している漢直のコード系は T-Code[^tcode] というものですが、2ストロークだけで文字を入力するため、このコード系は他のものに比べて直接に打てる漢字の数が少ない[^kanji_num]です。また、2,000文字を超える漢字をカバーしているコード系[^tcode_else]を使用していても、その大半の打鍵を覚えているという方は少ないのではないでしょうか。

[^tcode]: 『[Ｔコードについて](http://www.ki.nu/~makoto/tcode/what-is-tcode-by-maeda.html)』tc2.el を開発された方による檄文です。tc2.el は、昔 Emacs を使っていた頃は必須のプログラムでした。もう25年くらい経つでしょうか？(遠い目)
[^kanji_num]: 『[変換せずに打てる文字の割合を漢直の入力方式ごとに調べた](https://qiita.com/saka_bar/items/8bea26428094b8c226c6)』には、T-Code, TUT-Code, G-Code, Phoenix の文字数が記載されています。
[^tcode_else]: 逆に2,000文字にも満たないのはT-Codeくらいです。

世の中の漢直ツールは、利用者が直接に打てる漢字の少なさをカバーする手段として、たいていは「部首合成」と「交ぜ書き変換」という機能を提供しています。筆者が作成して公開している、Windows用の漢直ツールである [KanchokuWS](https://github.com/oktopus1959/KanchokuWS#readme) も、もちろんこれらの機能を提供しています。

「部首合成」の説明はまた別の機会に譲るとして、本稿では通常の「交ぜ書き変換」と KanchokuWS で実装されている「動的交ぜ書き」について説明します。後半では「動的交ぜ書き」と形態素解析の融合について考察をしたいと考えています。

## 交ぜ書き変換とは
漢字の熟語には同音異形語が数多く存在します。よく見かける例は、「貴社」「記者」「汽車」「帰社」でしょう。「きしゃのきしゃがきしゃできしゃした」というやつですね。ちなみに筆者の環境にある Google 日本語入力で変換させたら、大半が「記者」になってしまいました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2375976/2f180ec4-21d3-ba7d-626d-06b58c8b0adf.png)

交ぜ書き変換というのは、読みの部分に一部漢字を混ぜることで変換形のバリエーションを絞り込もうという機能です。たとえば「社」「記」「車」「者」を直接打てると仮定すると、「記者」は直接打てるとして、上述のそれ以外の熟語は「き社」と「き車」から変換できることになります。「き社」にマッチする熟語は「貴社」と「帰社」の2通り、「き車」にマッチするのは「汽車」だけ、というように絞り込めるわけです[^maze-pros]。

[^maze-pros]: 交ぜ書きにはもう一つ「省打鍵」という利点があります。「しゅう」と打つのはT-Codeだと6打鍵必要ですが「習」なら2打鍵ですみます。実は筆者にとってはこちらの省打鍵の利点のほうが大きいと思っています。

このようなマッチングを可能にするために、従来の交ぜ書き変換では、あり得る交ぜ書きパターンをすべて辞書に登録しておくという力任せの手法を取っていました。「貴社」の例でいうと、「きしゃ」「貴しゃ」「き社」という3つのパターンを登録しておくわけです。

```
きしゃ /貴社/記者/汽車/帰者/
き社 /貴社/帰社/
き者 /記者/
き車 /汽車/
貴しゃ /貴社/
記しゃ /記者/
汽しゃ /汽車/
帰しゃ /帰社/
```

すぐにお気づきだと思いますが、この手法を機械的に適用すると、2文字熟語で3パターン、3文字熟語で7パターン、4文字熟語で15パターン、・・・というように、N文字熟語に対して `2 ^ N - 1` 個の登録が必要になります。エントリ数があっという間に膨れあがってしまうのです。

登録するパターン数を抑えるにしてもいろいろ考慮が必要です。たとえば「ゆううつ : 憂鬱」という「<読み> : <変換形>」の組み合わせがあったとして、どのような交ぜ書きパターンを登録したらよいでしょうか。おそらくは「ゆううつ」「憂うつ」の2つになると思いますが、もし「ゆううつ」だけしかエントリにないとすると、簡単に「憂」と打てる人にとってはわざわざ「ゆううつ」というように平仮名に開いて打たなければならないので、まさに「ゆううつ」になるでしょう[^yuuutu]。

[^yuuutu]: 実際、漢直Winの交ぜ書き辞書には「ゆううつ」というエントリしかありません。これは元々が T-Code の開発者によって作成された辞書であり、T-Code では「憂」が打鍵できないためだと思われます。

実際、 KanchokuWS のベースとなっている「[漢直Win](https://github.com/kanchoku/kw)」の交ぜ書き辞書には75,000行ほどのエントリが登録されていますが、そのうちユニークな変換形の数は32,300ほどです[^maze_form]。

[^maze_form]: 漢直Winの交ぜ書き辞書は「読み /変換形/変換形/…」という形式になっています。例:「あかり /灯/明かり/明り/」

```
$ nkf -w kwmaze.dic | ruby -ne 'if $_ =~ /\s\/(.*)\//; puts $1.gsub("/", "\n"); end' | sort -u | wc -l
32310
```

変換用の辞書として3万2千はやや物足りないと思います。フリーで使える日本語辞書としては、形態素解析器の MeCab で用いられている ipadic があります。こちらは21万4千語の変換形が登録されています[^ipa_num]。漢直Winの交ぜ書き辞書の約7倍です。これを何とかして交ぜ書き用辞書に変換できないだろうか、と考えていて思いついたのが次項で説明する「動的交せ書き」の仕組みです。

[^ipa_num]: KanchokuWS 用の辞書に変換した後で、同様の ruby コマンドを実行してカウント。変換後の ipa 辞書は[こちら](https://github.com/oktopus1959/KanchokuWS/releases)からダウンロードできる .zip に含まれています(kwmaze.ipa.dic)。

## 動的交ぜ書き
アイデアは非常に単純です。辞書としては「ひらがなの読み」と「漢字の変換形」のペアだけを登録しておき、入力された交ぜ書きパターンに対して動的にそれにマッチするペアを検索して、その変換形を出力してやればよい、というものです。

概念的には、たとえば「かま門ねづ子」が入力されたら「かま.\*ねづ.\*」という読み側の正規表現と「.\*門.\*子」という漢字側の正規表現を生成し、それぞれでマッチングを行って両者ともマッチするペアを検索するというイメージです。もし「かまどねづこ : 竈門禰豆子」のペアが登録されていれば、これがマッチして、入力文字列「かま門ねづ子」はその変換形である「竈門禰豆子」に置換されます[^movie]。

[^movie]: 実際に動的交ぜ書き変換を実行している様子を動画にして Twitter に投稿していますので、興味のある方はご覧ください。<br/> <ul><li>[竈門炭治郎と禰豆子](https://twitter.com/kanchokker/status/1403533755701026826)</li><li>[動的交ぜ書きでワイルドカード](https://twitter.com/kanchokker/status/1474935216708145159)</li></ul>後者は、動的交ぜ書きの副産物としてワイルドカードを用いた変換も可能になっているという例です。

```
「かま門ねづ子」⇒「かま.*ねづ.* : .*門.*子」という正規表現ペアを生成

かま.*ねづ.* ⇒ かまどねづこ
.*門.*子 ⇒ 竈門禰豆子

「かまどねづこ : 竈門禰豆子」というペアにマッチする
```

KanchokuWS での実装上は正規表現検索を行っているわけではなく、文字ごとにそれを含むエントリの集合というものを用意しておき、入力文字列に含まれる各文字に対するエントリ集合の集合積によって候補を絞り込んでから、さらに文字並び順、連接ぐあいといったものでフィルタリングして最終的な候補を出力しています[^source]。やや詳しい説明は[後述](#集合積で絞り込むインデックス)します。

[^source]: 結構ややこしい実装になっていますが、詳しく知りたい方は[ソース](https://github.com/oktopus1959/KanchokuWS/blob/main/kw-uni/Mazegaki/MazegakiDic.cpp)の `GetCandidates()` メソッドあたりを参照してください。

実際に使い物になるようにするには、他にも、読みが最長になる候補を優先するだとか、活用語については語尾変化を考慮するだとか、様々な課題はあるにしても、動的交ぜ書きの概念的な仕組みは上に述べた通りの本当に単純なものです。


## 動的交ぜ書きの問題点

筆者の実装した動的交ぜ書きエンジンは単文節しか解析しません。そのため、たとえば「ひどい目にあう」という一文全体を交ぜ書き変換すると「ひど色目にあう」や「ひどい目似合う」となったりします。つまり辞書のエントリと最初にマッチするところだけを取り出して変換してしまうわけです。

```
ひどい目にあう ⇒ ひど色目にあう
ひどい目にあう ⇒ ひどい目似合う
```

とくに前者の変換は「`いろめ：色目`」の登録に対して「`い目`」から得られる「`い.*`」「`.*目`」というパターンがマッチしてしまったことによるもので、動的交ぜ書き特有の現象です。

これを避けるには、現状では「あう」の前にスペースを入れたり「交ぜ書きブロッカー[^maze_blocker]」を置いたりして変換対象となる読みを制限するという、人間側の操作が必要になっています。

[^maze_blocker]: 「交ぜ書きブロッカー」とは入力した文字に対して設定できるフラグの一種で、利用者が入力した文字列の末尾から遡っていって「ここで読み候補の採取を停止」と判断するためのものです。

```
ひどい目に あう ⇒ ひどい目に遭う
```

漢直に熟練してくれば、交ぜ書き変換を使う局面は限られてくるので、上記のような操作をしてもそれほど苦にはならないのですが、これから漢直を始めてみよう、という入門者にとってはかなり使いづらいのではないでしょうか。

この問題は、あらかじめ漢字の読みのバリエーションを統計的に学習しておいて、ツールの起動時、読みと変換形のペアを読み込む際に読みの部分文字列と漢字との間のマッピングを取っておけば、ある程度解消できるでしょう。

「いろめ : 色目」の例でいうと、「色」の読みは「いろ」「しき」「しょく」「じき」のどれかであり、「目」の読みは「め」「ま」「もく」「ぼく」のどれかであるということが分かっていれば、辞書の読み込み時に「いろ」＝「色」、「め」＝「目」というマッピングを取ることができます。これにより、「い目」を「いろめ : 色目」にマッチさせなくできるでしょう。

また「はっぱ : 発破」「ほったん : 発端」のような例があるので、「発」の読みとして「はっ」「ほっ」、「破」の読みとして「ぱ」が学習できている必要があります。このあたりは十分な量の「読み : 変換形」のペアがあれば学習可能だと思われます。

## 形態素解析との融合

2つめの問題、「目にあう」が「目似合う」になってしまうという問題を解消するには、やはり形態素解析が必要になると考えています。また、長い文を一気に入力し、複数文節の交ぜ書き変換が可能な、入門者に優しい漢直ツールにするためにも、形態素解析が必要です。

理想的には Microsoft なり Google なりが自社製の IME に動的交ぜ書き変換機能を取り込んでくれるのが一番なのですが、それは何年後かに実現されることを期待するとして、ここでは形態素解析に動的交ぜ書きを取り込む場合の叩き台的なものを考察してみようと思います。

例文は

```
きょうきょうとであった（今日京都で会った）
```
にしましょう。ちなみに筆者の Google 日本語入力では以下のような候補が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2375976/590cc236-f6f4-2497-d1dd-021695c2ce1b.png)

期待通り[^expected]の解を出力してくれています。

[^expected]: 「本稿を書く上で好都合」という意味ですw。

さて、辞書には次のエントリが登録されているとしましょう。

```
きょ:虚
うきょう:右京
きょう:今日
きょうきょう:恐々
きょうと:京都
と:都
と:と (格助詞)
で:で (格助詞)
であう:出会う
あう:会う
ある:有る
きょくじつき:旭日旗
にっき:日記
```

「きょうきょうとであった」を形態素解析すると、だいたい次のようなラティス[^lattice]構造が得られます。緑色の丸をつないだものが欲しい解析結果ですが、Google 日本語入力では、他に水色の丸をつないだものも解として出力されていることが分かります。

[^lattice]: 「ラティス」(lattice) 園芸用品屋とかで売ってる木製の斜め網の塀をイメージしてください。

```
きょうきょうとであった
```
![morph-lattice.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2375976/99579043-3238-5708-c70c-b3e83895e374.png)

では次に交ぜ書きで「きょ日きょう都で会った」を形態素解析したときにどのようなラティス構造が得られるべきかを考えます。

```
きょ日きょう都で会った
```
![morph-lattice-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2375976/24be61c4-9883-2fc9-3cfe-83a0de2c87d3.png)

こちらは「日き」のような余計な形態素が現れるはずです。そのかわり「あった」の曖昧性が無くなり「会った」だけになります。また、あり得る経路の組合せの数も減ることが期待できます[^nikki]。

[^nikki]: 「日き」の後は「ょう・・・」ですが、「ょ」で始まるエントリが存在しないので、この経路の解析はここで停止します。

「きょ日き？」は「旭日旗」にマッチするか？、という意味で置いておきましたが、前節でも説明したように、「旭」が「きょく」にしかマッチしない、ということが分かっていれば、「きょ日き」は「旭日旗」にはマッチしないようにできます。

このような解析を可能にするには、どのようなインデックスを持てばよいでしょうか。通常のIMEであれば平仮名だけからなるインデックスであり、MeCabなどの形態素解析器なら変換形だけからなるインデックスになるでしょう。交ぜ書き文の形態素解析を行うには、その両方、もしくは両者の融合インデックスを持つ必要があると考えられます。以下では、両者を融合したインデックスを考えることにします。

## 読みと変換形を融合したインデックス
交ぜ書き用のインデックスとして、以下の2通りを考えてみました。

1. 筆者が KanchokuWS で実装したような、集合積による絞り込みをするインデックス
1. 交ぜ書きのすべてのパターンを網羅したトライ木インデックス

### 集合積で絞り込むインデックス
KanchokuWS で採用している方式です。一例として「きょ日」を解析してみましょう。

```
「き」= {きょ:虚、うきょう:右京、きょう:今日、きょうきょう:恐々、きょうと:京都、きょくじつき:旭日旗、にっき:日記}
「ょ」= {きょ:虚、うきょう:右京、きょう:今日、きょうきょう:恐々、きょうと:京都、きょくじつき:旭日旗}
「日」= {きょう:今日、きょくじつき:旭日旗、にっき:日記}
```
ここから集合積をとると

```
{きょう:今日、きょくじつき:旭日旗}
```
が残ります。さらに、読みが「きょ」で始まり、変換形が「日」で終わるものを検索して、最終的に「きょう:今日」が残ります。

原理的にはこれで解析ができることは分かりますが、非常に効率が悪いということもお分かりかと思います。辞書のエントリ数が少なければまだしも、たとえば KanchokuWS の辞書には、読みに「あ」を含むエントリは、1万7千個以上あります。単文節の解析しかしていない KanchokuWS ですが、ちょっと長い文字列に対して交ぜ書き変換を実行すると、少し待たされる感じがあります。これで複文節解析をやったら、かなり待たされることになりそうです。


### 全交ぜ書きパターンを網羅するインデックス
以下のような簡単な交ぜ書き辞書を考えます。

```
/けっさい/けっ済/決さい/：決済
/けっさい/けっ裁/決さい/：決裁
/けっさいしゃ/けっさい者/けっ裁しゃ/けっ裁者/決さいしゃ/決さい者/決裁しゃ/：決裁者
/けっさく/けっ作/傑さく/：傑作
/けっさん/けっ算/決さん/：決算
```

読みのほうに全パターンの交ぜ書きが含まれています。まずは話を簡単にするため、ひらがなだけのインデックスを作ってみましょう。大体、下図のような感じになるかと思います。

![tri-index-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2375976/a4590ffc-ff21-ab3f-cb0e-3bc7943db260.png)

これは「トライ木[^trie]」と呼ばれるデータ構造です。「けっさいしゃ」という文字列を形態素解析しようとした場合、木をたどることで「けっさい」「けっさいしゃ」という2つの候補を得られます。形態素解析を実装する際には定番とも言えるデータ構造です。

[^trie]: たとえばMeCabはDARTS(Double ARray Trie System)と名付けられた、ダブルアレイによるトライ木を用いています。ダブルアレイについては https://takeda25.hatenablog.jp/entry/20120219/1329634865 が簡にして要を得た説明になっているかと思います。

ふつうの仮名漢字変換用のインデックスならこれで十分ですが、交ぜ書き用インデックスにするために、全交ぜ書きパターンをインデックスに入れてみましょう。

![tri-index-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2375976/4d11a837-9e53-817a-a90a-7364fb9fe3e4.png)

なかなかに複雑になりました。ノード間の破線はそれらのノードが「読み」と「漢字」のペアになっていることを表しています（たとえば、「裁」と「さい」）。ひらがなだけの時は各ノードが1文字からなる木構造でしたが、これはノードが複数文字を含み、かつ、同じ文字列を持つノードが存存する[^multi-chars-nodes]、ラティス構造になっています。

[^multi-chars-nodes]: 「読み」と「漢字」をペアにしたので、共通するひらがなを共有できなくなったためです。

今回の考察はここまでとします。上記のような構造をうまく表現できるような仕組みを考えついたら、続編記事を書きたいと思います。