アイソモカ

知の遊牧民の開発記録

しりとり辞書を作る【kuromoji.js】試行&諦め記録

しりとりぼっとを作成中で、辞書がほしい。kuromoji.js の辞書から語彙を取得しようと試みたが、難しすぎて諦めた記録。
※筆者はJavaScript歴半年のぴぇぴぇです。

やりたいこと

  • しりとり辞書:しりとりで、システム(ボット)のターンでの単語選択に使うデータベースを作りたい
    • 指定したカタカナ1文字に対して、その文字から始まる読みをもつ名詞を1つ出力する(「ア」→「朝」、「イ」→「イギリス」…など)
    • 名詞が一般的な名詞かどうかの目安として(「険要」よりも「毛虫」のほうがしりとりっぽいので)何らかの頻度情報がほしい
  • しりとり辞書の構成
    • 「key: カタカナ1文字, value: その文字から始まる読みをもつ名詞のリスト」の配列
    • 名詞には読みと頻度情報をつける
    • 形態素解析の「コスト」が頻度情報として利用できるか?

これまでやったことと、考えたこと、調べたこと

  • kuromoji.js 形態素解析器
  • しりとりぼっとと kuromoji.js
    • 作成中の「しりとりぼっと」アプリでは、kuromoji.js を利用して、ユーザーが入力したテキストが名詞かどうかを判定したり、読みを取得したりする(実装済み) 👉 piijey/shiritori: しりとりぼっと - GitHub
    • システム(ボット)のターンで選択する名詞は、kuromoji.js の辞書に含まれている必要がある
    • kuromoji.js の辞書をしりとり辞書に使えれば、データ量が少なくて済み、アプリをコンパクトにできるかも

今回やったこと、結論と今後

  • しりとりでボットのターンで使う語彙を、kuromoji.js の辞書から取得しようと試みた
  • kuromoji.js の辞書は形態素解析のために最適化されている(バイナリ化されて独自のデータ構造を持っている)ので、任意の語を取得したり、全件を走査したりすることは難しいことがわかった
  • 諦めて、他の「目で読める辞書」(mecab-ipadicのCSVファイル)を使うことにする

抹茶と和菓子の写真、和菓子には黒文字(楊枝)が添えられている
黒文字(2020/4/11 宇治)

今回やったことの詳細

kuromoji.js パッケージに付属している辞書について、情報収集

kuromoji.js の辞書の利用方法を知るには、大きく分けて2つの作戦が考えられる

  • 辞書のビルド方法からデータ構造を読み解き、自分が読める形式に変換する
    • Builder 関連コードは kuromoji/src/dict/
  • Tokenizer が辞書をロード・利用する方法を真似する 👈 今回はこっち
    • Tokenizer kuromoji/src/Tokenizer.js
    • Loader 関連コードは kuromoji/src/loader/

Tokeniser の通常の使い方

  • Tokenizer は、kuromoji/README.md に従えば、Node.js ですぐ動かせる。
// node run_tokenizer.js
var kuromoji = require("kuromoji");

kuromoji.builder({ dicPath: "node_modules/kuromoji/dict/" }).build(function (err, tokenizer) {
    var path = tokenizer.tokenize("すもももももももものうち");
    console.log(path);
});
  • 出力
[
  {
    word_id: 415760,
    word_type: 'KNOWN',
    word_position: 1,
    surface_form: 'すもも',
    pos: '名詞',
    pos_detail_1: '一般',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: 'すもも',
    reading: 'スモモ',
    pronunciation: 'スモモ'
  },
//  ...続く

結果を表示する際、 Tokenizer は kuromoji/src/Tokenizer.jsで、this.token_info_dictionary.getFeatures(node.name)によって品詞情報を取得する。node.name(上記では word_id と表示されている番号)が、品詞情報を取得するためのキーになる。

node.name: 415760
features_line すもも,名詞,一般,*,*,*,*,すもも,スモモ,スモモ

※ ちなみにコストはここには含まれていないので、別途探してくる必要がある(後回し)

この word_id を0から1ずつ増やして最後まで指定すれば、全エントリの品詞情報が取得できるのでは?(結論から言うと、そうではなかった)

Tokenizer を使わずに、自分で辞書にアクセスする

まず、直接辞書をロードして、指定した word_id の品詞情報を取得する

// node get_feature_from_id.js
var DictionaryLoader = require("kuromoji/src/loader/NodeDictionaryLoader");
var loader = new DictionaryLoader("node_modules/kuromoji/dict/");

loader.load(function(err, dic) {
    const word_id = 415760;
    features_line = dic.token_info_dictionary.getFeatures(word_id);
    console.log(features_line);
});
  • 出力
すもも,名詞,一般,*,*,*,*,すもも,スモモ,スモモ

word_id さえわかっていれば、簡単〜

次に、word_id を0から200まで指定して、品詞情報を取得してみる

// get_feature_from_all_ids.js
// ...
    for (let word_id = 0; word_id < 200; word_id++) {
        //if ([17, 47, 67, 87, 107, 127, 147].includes(word_id)) continue; 
        console.log(word_id)
        try {
            features_line = dic.token_info_dictionary.getFeatures(word_id);
            console.log(features_line);
        } catch (error){
            continue;
        }
    }
});
  • 出力
    • word_id = 1, 3, 5, … では、品詞情報が得られない
    • word_id = 17 では、V8エンジンのメモリアクセスに関するエラーが出る
    • このエラーはtry-catch ブロックでは捕捉できていない
    • 17 をスキップして様子を見てみると、word_id = 47, 67, 87, 107, 127, 147, ... でも同様のエラーが出るとわかった
0
やぼったい,形容詞,自立,*,*,形容詞・アウオ段,基本形,やぼったい,ヤボッタイ,ヤボッタイ
1

2
ポン,キイッポン
3

4
自分勝手,名詞,形容動詞語幹,*,*,*,*,自分勝手,ジブンガッテ,ジブンガッテ
5

//...

17


#
# Fatal error in , line 0
# Fatal JavaScript invalid size error 169220804 (see crbug.com/1201626)
#
#
#
#FailureMessage Object: 0x309eaf5c0
 1: 0x10e9d4042 node::NodePlatform::GetStackTracePrinter()::$_0::__invoke() [/path/to/envs/webdev/lib/libnode.115.dylib]
 2: 0x10f4e5933 V8_Fatal(char const*, ...) [/path/to/envs/webdev/lib/libnode.115.dylib]
 3: 0x10edae746 v8::internal::FactoryBase<v8::internal::Factory>::NewFixedArray(int, v8::internal::AllocationType) [/path/to/envs/webdev/lib/libnode.115.dylib]
 4: 0x10ef7bf87 v8::internal::(anonymous namespace)::ElementsAccessorBase<v8::internal::(anonymous namespace)::FastPackedObjectElementsAccessor, v8::internal::(anonymous namespace)::ElementsKindTraits<(v8::internal::ElementsKind)2>>::ConvertElementsWithCapacity(v8::internal::Handle<v8::internal::JSObject>, v8::internal::Handle<v8::internal::FixedArrayBase>, v8::internal::ElementsKind, unsigned int, unsigned int, unsigned int) [/path/to/envs/webdev/lib/libnode.115.dylib]
 5: 0x10ef7a840 v8::internal::(anonymous namespace)::ElementsAccessorBase<v8::internal::(anonymous namespace)::FastPackedObjectElementsAccessor, v8::internal::(anonymous namespace)::ElementsKindTraits<(v8::internal::ElementsKind)2>>::GrowCapacity(v8::internal::Handle<v8::internal::JSObject>, unsigned int) [/path/to/envs/webdev/lib/libnode.115.dylib]
 6: 0x10f1f6325 v8::internal::Runtime_GrowArrayElements(int, unsigned long*, v8::internal::Isolate*) [/path/to/envs/webdev/lib/libnode.115.dylib]
 7: 0x10e746176 Builtins_CEntry_Return1_ArgvOnStack_NoBuiltinExit [/path/to/envs/webdev/lib/libnode.115.dylib]
Trace/BPT trap: 5

エラーの原因を探るべく、品詞情報を取得する方法の詳細を見てみる。

品詞情報を取得する方法

  • kuromoji/src/dict/TokenInfoDictionary.jstoken_info_dictionary.getFeatures() の機能
    • pos_buffer は、tid_pos.dat.gz から読み込んだ品詞 (part-of-speech) 情報を保持するためのバイト配列 (Uint8Array)
    • まず、引数として受け取った token_info_id_str (さっきのword_id )を、 pos_buffer 上でのインデックス pos_id に変換する
    • それから、pos_id を使って品詞情報を取得する
  • 考えられること
    • さっきのV8エンジンのメモリアクセスエラーは、pos_buffer のデータの無い位置にアクセスしようとしたせい… と思われる
    • word_id を0から1ずつ増やしていく方法では、無効な pos_id を発生させるし、有効な pos_id を網羅できるとも限らない(pos_buffer のデータ構造に関する理解が必要)

まとめ

  • しりとりでボットのターンで使う語彙を、kuromoji.js の辞書から取得しようと試みた
  • kuromoji.js の辞書は形態素解析のために最適化されている(バイナリ化されて独自のデータ構造を持っている)ので、任意の語を取得したり、全件を走査したりすることは難しいことがわかった
  • ここは諦めて、mecab-ipadicのCSVファイルを使うことにする
  • 感想:すごい… 形態素解析器の内部を垣間見ることができて、面白かった