Linux/Mozc辞書データで漢字変換

Mozc辞書データを使った漢字変換(2019-08-09)


漢字変換のためにMozc辞書データを利用します。
辞書ファイルだけではなく、ライブラリ全体を使えば予測変換を含めてフル機能使えると思いますが、
今回はアプリに漢字変換機能を入れるのが目的だったので、部分的に使います。

  • 予測変換は使わない
  • 可能な限りサイズを小さくしたい

ここではMozcライブラリの話はしません。
辞書データ(dictionary01.txt) のお話だけです。
このメモ時点ではフリーのライセンスですが、詳細はMozc辞書のREADME.txt をご覧ください。

もちろんMozcはLinux以外でも使えますが、
ここではshellやawkコマンドを混じえて説明します。

Mozc辞書データの例

Web検索で

Mozc  dictionary_oss

で探すと、GitHubの辞書データ一式が見つかると思います。

ちゃんとした解説は本家や詳しい人にゆずるとして、
サイトの dictionary01.txt〜というのが、
「かな」->「仮名」のような漢字変換、予測変換に利用できる対応表です。

#表層形(変換前の文字),左文脈ID,右文脈ID,コスト,変換後の文字
いぬ   1847    1847    3734    犬

一番左(いぬ)と一番右(犬)があれば、漢字変換だけはできそうです。
それに優先順位をつける情報がコストですね。
文脈IDが、名詞なのか動詞なのかを判断する情報なのですが、左と右がありますね?
数字が意味するところは id.def ファイルに書いてあります。
左右あるのは...辞書を眺めて見ると、なんとなく雰囲気は分かります。

 # 左右の文脈IDが異なるデータを抜き出す例
 awk '{ if($2 != $3){ print $0; } }' dictionary0*.txt
つなおじさん   1916    1847    6850    ツナおじさん

(...いや、私の作った単語ではありませんよ!?辞書に載っているんです!)
"1916 名詞,固有名詞(ツナ)" + "1847 名詞,一般(おじさん)" ですね。
これを使えば、
左や右に繋げることのできる言葉の判断材料にできそうです。
たとえば右文脈IDが1919(人名,姓)や、1918(人名,名)なら、右に「様」とか付けても違和感が無いし、
動詞や形容詞の後に「様」とか君、さん、殿は無さそうです。
「ツナ田中さん」はできるけど、「ツナおじさん様」や「ツナ泳ぐ君」は無い、
そういった単語連結の判断材料として文脈IDが利用できそうです。

「ツナ」と「おじさん」別々にデータがあるのに、なぜ「ツナおじさん」も登録されているのかと言うと、
単によく使われる単語だからではないでしょうか?
一緒にすれば検索/変換の効率が上がって「コスト(優先順位)」も別途付与できるので、予測変換の精度も上がるのかと。
ツナおじさんのコストは6850、「プリキュアおじさん(6787)」が僅差で勝ちました。
そして、2019.06時点の暫定王座は「ハイサイおじさん(5428)」です。*1

# おじさんを探す例
egrep "おじさん$" dictionary*.txt | sort -k4,4n

通常は、
日本語入力でMozcを使って「ちいさいお」まで入れると
小さいおじさんとか、小さいお子さんとか、予測した単語を先行して出してくれることになるわけですが、
今回はそれ(予測変換)は、やりません。

辞書データを圧縮する

まずはやってみようという心意気で、辞書をできる限り軽量化しようと思います。

日本語的な話題について

優先順位について

当初は、文章変換や予測変換は諦めて、単一の漢字変換のみを目指しました。
「いぬ -> 犬」みたいなやつです。「いぬです(犬+です)」の変換は一旦あとまわしです。
文字の連結や単語の推測をやらないなら文脈IDは削って、
コスト(優先順位)のみでソートすればいい、はず。

ところが「いぬ -> いぬ」変換でも

いぬ  1847   1847   5356   いぬ
いぬ  833    833    5177   いぬ

と同じ文字が2つもあります。名詞(1847)と動詞(833)...どうやら「犬」と「往ぬ」の平仮名らしい。
はい、「コストのみでソート」の目論見が、早くも消えました。

それでも、まぁ多少の精度は、仕方ないじゃない?
と自分に言い訳して、コストの低い方だけをDBへ登録しながら突き進むと、今度は
「みき」という単語を変換する時に、

みき    1918    1918    4855    幹
みき    1847    1847    8343    御酒

の「御酒(順位8343)」よりも先に、大量の名前(姓名)の「ミキ」さんが優先されてしまう事態になってしまいました。
本来は前後の文脈IDとの接続で、適切なコスト(優先順位)が導かれるのだと思いますが。

固有名詞についてはいくらでも新語・造語を作れてしまうので、
「人名、組織」等の固有名詞には適当にコスト加算して、一般名詞よりも優先順位を下げるように調整しました。

実は2語の件

次におきた問題として、
いままで単語の感覚で使っていたけど、実際は2つの言葉をつないだもの
「お+菓子」や「温ま+って」など、これは変換のしようがありません。
自分がキーボードで入力する時にまず「温ま」変換「って」変換、なんて打ちませんから。

そもそも単語とは何か...という日本語の難しい話はさておき
(もちろんWebで資料を見て回ったのですが、小学校(だっけ?)で品詞の授業をやって、何も覚えていないことを思い出しました*2
ひとまず、接頭、接尾(おかしの「お」、あたたまっての「って」)に絞って考えるなら、
文脈IDや形態素...の全てではなく(id.def全てではなく)、
ざっくりとした品詞の分類(名詞,動詞,助詞,語尾,接頭辞,接尾辞)だけ持たせるならDBサイズも圧縮できるか?
と思い悩みましたが...ここで使えるのが"suffix.txt" です。
(最初は意味が分からなくて見なかったことにしていたsuffix.txtです)

接尾の「って」の部分は、suffix.txt で解決できます。
具体的には、単純なdictionary0x.txtの検索に加えて、dictionary0x.txt + suffix.txt も見る。
「いぬだ」を探す場合は

  • い(=dictionary.txtを探す) + ぬだ(=suffix.txtを探す)
  • いぬ(=dictionary.txtを探す) + だ(=suffix.txtを探す)
  • いぬだ(=dictionary.txtを探す)

を見れば「犬だ」が拾えるようになり、一気に守備範囲が広がるようになりました。
文脈IDを無視して繋ぐと、すこし不自然な変換が発生する場合がありますが...そこは、よしなに。誤差です。

接頭の「お菓子」の「お」の部分は、
実はMozc辞書ではほぼカバーされているように見えて、
おかし、おけいこ、ふせいかい、みぶんるい
といった単語は2語ではなく1語で単語登録されています。
なので、接頭の方は特に対処しないことにしました。

体言接続特殊2について

細かい点はきりが無いので、これが最後のキモの部分です。
タイトルの「体言接続特殊2」というid.txt で573が採番されている単語を特別扱いします。
「温ま」+「った」のように、単語にsuffixの「っ」が続く語が1000語程度あります。
(例:有、漁、生き残、入れ替、埋ま、怒、終わ)
このID573「以外の」単語に、「っ」で続けてもしっくりきません。犬った、食べった、とか。
よって、文脈IDが573か否かで、あとに続くsuffixを許容するか否かを判定する処理を入れました。
(この千語はDBとは別に、直接コード内に持ちました)


このへんの話は、実際に変換をやってみないと気が付かないし、
たぶん調整の余地は次々に出てくると思います...が、とにかく試しに変換機能を作って実際に文章を書いてみたほうが早い気がしました。

ちなみに、
細かい調整を"全て"入れていくと、最終的には(既存の)Mozcのライブラリが完成するだけなので、
それなら最初からMozcをフルで導入すればいいと思います。
結果そうなるのもアリですが、まずは自分の目的に合わせて工夫・実装してみましょう。


データベース的な話題について

日本語の仕様ではなく、DBの観点でサイズの削減を目指す話です。

1DBを1MB以下にする件について

dictionary01.txt から 09 まで(未加工)で合計55MBくらい。
ここから、文脈ID列を削ってDBへ突っ込んだら、100MBくらいになりました。
AndroidアプリにDBを入れるには、DB1つにつき1MB未満にしないと不味いらしく、
都合、100個以上に分割する必要があります。

(分割はともかく、総量100MBは大きい...以下ではもう少し削減を試みます)

私の場合は先頭文字と総文字数で区切りました(「イヌ」だと「イの二文字DB」に入れる)
犬、居ぬ、往ぬを同じDBに入れて、一回で取り出したかったので。

データの省略について

変換前後で同じ(つまり「平仮名」のまま登録されている)データが大量に有ります。

dictionary0x.txt の中身を見る例として、10文字以上の無変換の語。

awk '{ if($1 == $5 && length($5)>10){ print $0; } }' dictionary0*.txt

"ありがとうございました" -> "ありがとうございました"
のような変換です。
そこへ「カタカナ」に変えただけのものも加えると、もっとあります。
レインボーマウンテンブレンド、レギュラーソリュブルコーヒー、とか。

これらを削減します。
具体例として、変換後の情報に "+" でDB登録しておいて(カタカナは "-")
検索した時に結果が "+" だったら、そのままを返す( or カタカナに変換して返す)、
とすれば、DBのデータ量を減らせます。
("+"、"-" は、辞書内の他の語と被らず、支障の無いてきとうな一文字で)

検索処理的には一手間増えるわけですが、DBサイズを減らすことを優先しました。

DBの数字フォーマットのサイズの件

Androidに、SQLiteとして辞書情報を入れたのですが、
SQLiteの数字(NUMBER)のサイズって可変なんですね。
127以下の数字にすれば、1byte に収まるっぽいのですが、
コスト(単語の優先順位情報)は127を超えるのが大半です。
128以上で2byteになれば、単純にサイズも倍、ですね。

今回は予測変換や学習機能は使わない、
つまり、文脈IDとの組み合わせによる正確なコスト計算を行っていない、ので、
DBに入れる際に、コストは127未満になるように振り直すことにしました。

インデックスについて

同じく、DBでおなじみのインデックスを付けて検索を早くする機能ですが、
今回は付けるのはやめました。
インデックス分のサイズが増える割には、付けなくても十分早かったからです。
これは環境や設計方法によるので、実際に動かしてみないと分からないと思います。

実際の、DBを作成する為のコードの例

こちらです。
shellとperlで、dictionary01.txt から、SQLite用のDBを作ります。

上記を踏まえて、もっと上手くやる為のアイデアの一助になれば、なによりです。

余談

副産物として、面白かったのは「逆変換」です。
Word(Microsoft Office)とかには付いていましたよね、確か。「犬 -> いぬ」にもどすやつです。
全てのDBを、しかもキーではない値を全検索なんて、正気の沙汰とは思えない処理だったのですが、
やってみると、想像よりは遥かに早かったです。
実際に、ぜひ私のアプリでやってみて頂く...までもなく、例によってawkで

 awk '{ if($5 == "犬"){ print $0; } }' dictionary0*.txt

コマンドラインで「この程度」の早さなら、DBでもそこそこの早さが期待できるんじゃないか、と想像がつくと思います。

Mozc辞書は、実用性も素晴らしいですが、
使ってみると、面白い、です。
日本語が難しい、MOZC便利、
ということで、せっかくフリー辞書ですし、
少しでもフィードバックしておこうと思いメモを残しました。


*1 データが古くても良いなら、このランキングづけ遊びはわりと楽しいです。grepで拾ってコストでソートすれば、誰が有名(?)なのか分かります。
*2 いま読んで、ようやく理解できる内容を小学校で覚えるのは、当時の自分にはさぞかし苦痛だったと思います

  最終更新のRSS