しりとりぼっとを作成中で、辞書がほしい。kuromoji.js の辞書から語彙を取得しようと試みたが、難しすぎて諦めた記録。
※筆者はJavaScript歴半年のぴぇぴぇです。
やりたいこと
- しりとり辞書:しりとりで、システム(ボット)のターンでの単語選択に使うデータベースを作りたい
- 指定したカタカナ1文字に対して、その文字から始まる読みをもつ名詞を1つ出力する(「ア」→「朝」、「イ」→「イギリス」…など)
- 名詞が一般的な名詞かどうかの目安として(「険要」よりも「毛虫」のほうがしりとりっぽいので)何らかの頻度情報がほしい
- しりとり辞書の構成
- 「key: カタカナ1文字, value: その文字から始まる読みをもつ名詞のリスト」の配列
- 名詞には読みと頻度情報をつける
- 形態素解析の「コスト」が頻度情報として利用できるか?
これまでやったことと、考えたこと、調べたこと
- kuromoji.js 形態素解析器
- kuromoji.js はクライアントサイドのみ(ブラウザ上)で動作するので手軽
- 設定方法の記事を以前書いた 👉 React + Kuromoji.js で形態素解析(Webpackの設定と辞書ファイルの配置) #React - Qiita
- しりとりぼっとと kuromoji.js
- 作成中の「しりとりぼっと」アプリでは、kuromoji.js を利用して、ユーザーが入力したテキストが名詞かどうかを判定したり、読みを取得したりする(実装済み) 👉 piijey/shiritori: しりとりぼっと - GitHub
- システム(ボット)のターンで選択する名詞は、kuromoji.js の辞書に含まれている必要がある
- kuromoji.js の辞書をしりとり辞書に使えれば、データ量が少なくて済み、アプリをコンパクトにできるかも
今回やったこと、結論と今後
- しりとりでボットのターンで使う語彙を、kuromoji.js の辞書から取得しようと試みた
- kuromoji.js の辞書は形態素解析のために最適化されている(バイナリ化されて独自のデータ構造を持っている)ので、任意の語を取得したり、全件を走査したりすることは難しいことがわかった
- 諦めて、他の「目で読める辞書」(mecab-ipadicのCSVファイル)を使うことにする
今回やったことの詳細
kuromoji.js パッケージに付属している辞書について、情報収集
- 辞書のファイル:
kuromoji/dict/*.dat.gz
- 形態素解析器をロードする際に、この辞書のパスを指定しロードする
- この辞書は、IPAdic (CSVファイル)を元にしている(バイナリ形式でビルドし圧縮)
- ちなみに、辞書に単語を追加したり、他の辞書を使ったりする方法もある
- 辞書に単語を追加 👉 [JavaScript] kuromoji.js の辞書に単語を追加する | 「それなら猫の手で」
- mecab-ipadic-nelogd や他の辞書をビルドする 👉 sable-virt/kuromoji-js-dictionary: kuromoji.js dictionary generator - GitHub
kuromoji.js の辞書の利用方法を知るには、大きく分けて2つの作戦が考えられる
- 辞書のビルド方法からデータ構造を読み解き、自分が読める形式に変換する
- Builder 関連コードは
kuromoji/src/dict/
- Builder 関連コードは
- Tokenizer が辞書をロード・利用する方法を真似する 👈 今回はこっち
- Tokenizer
kuromoji/src/Tokenizer.js
- Loader 関連コードは
kuromoji/src/loader/
- Tokenizer
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.js
のtoken_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
のデータ構造に関する理解が必要)
- さっきのV8エンジンのメモリアクセスエラーは、
まとめ
- しりとりでボットのターンで使う語彙を、kuromoji.js の辞書から取得しようと試みた
- kuromoji.js の辞書は形態素解析のために最適化されている(バイナリ化されて独自のデータ構造を持っている)ので、任意の語を取得したり、全件を走査したりすることは難しいことがわかった
- ここは諦めて、mecab-ipadicのCSVファイルを使うことにする
- 感想:すごい… 形態素解析器の内部を垣間見ることができて、面白かった