cabochaソースを読む(2)入力データから情報抽出
前記事の続き。
tree->read() (tree.cpp l.406) が文字列入力からデータ構造を構築する。まずデータ構造の宣言をチェック。
cabocha.h に struct cabocha_chunk_t (l.75), struct cabocha_token_t (l.87) の宣言がある。chunk が文節、token が形態素。ちなみにこれらはそれぞれ Chunk, Token に typedef されている。
さてtree->read() を見る。入力は文節区切りとするので、switch文はINPUT_CHUNKを想定。
l.435 の while がメインループだが、この中は l.438 の if 文により大きく2つに分かれる。
入力フォーマットはこんな感じなので
* 2 -1D
読ん 動詞,自立,*,*,五段・マ行,連用タ接続,読む,ヨン,ヨン
で 助詞,接続助詞,*,*,*,*,で,デ,デ
要はif文の前半(l.486のelseの前)は'*'ではじまる「文節情報」の行の処理であり、後半は各形態素の行にあたる。
前半の方から。l.439 の tokenize() は、定義は utils.h にあるが、文字列を指定された分割文字(この例だと空白)でトークンに分割するものである。(ここの"token"は形態素ではないので注意。)上の例では、char* column[] の column[0] に最初のトークン '*' が、column[1]に次のトークン '2' が、というふうに入る。
l.444 add_chunk() は treeの中に文節スロットを追加する。定義はl.338にある。文節の情報はここに入る。
コードでは文節の情報をいろいろセットしているが、入力が文節区切りならばほとんど関係ない情報ばかりである。参考までに書いておくと、素性選択や係り受けの出力においては、各columnは次の情報が入る:
column[0] '*' 固定
column[1] 文節番号
column[2] 係り先文節番号
column[3] 主辞/機能辞の位置
column[4] 係り受けスコア
column[5] 素性情報
if文後半、形態素の方。l.487 はまた tokenize()。今度は分割文字がタブなのに注意。上の入力フォーマットで「読ん」の次がタブなのだ。つまりここでは
column[0] 読ん
column[1] 動詞,自立,*,*,五段・マ行,連用タ接続,読む,ヨン,ヨン
のようになる。なおこのcolumn[1]の内容は、mecabの辞書のエントリ内容ほとんどそのままである。
l.490 add_token() は add_chunk() と同様、treeへのスロット追加。surface は形態素の文字列そのもの。normalized_surface は「正規化された」文字列。normalize()のコードはちゃんと追ってはいないが、ソースの normalizer.rule からして、全角/半角表記を統一するものだろう。
l. 497 token->feature には column[1]、つまりコンマ並びが入るが、l.505 feature_list には l.502 tokenizeCSV() でコンマを分割したものが入る。tokenizeCSV()はtokenize()と同様の動作、ただし分割文字がコンマになる。つまり上の例では
feature_list[0] 動詞
feature_list[1] 自立
のようになる。
次回は素性選択レイヤを見る。
cabochaソースを読む(1)処理の流れ概要
係り受けツールのcabochaをいじっているが、いろいろとひっかかってる。cabochaは係り受けをどうやって決めているのか?を知るため、ソースを少し読んでみた。以下そのメモ。
南瓜のページ http://code.google.com/p/cabocha/
からソースをDL。とったのは v0.64。このページにいろいろ基本的な解説があるので、まずそれを読んでおく。論文もあるので目を通しておく:
チャンキングの段階適用による係り受け解析
http://chasen.org/~taku/publications/nl142.pdf
tar.gz を展開。ソースは当然 src/ ディレクトリにある。
ソースを読み出す前に、まず「誰か既にソース読んで、わかったことブログに書いたりしてないかなぁ」などと怠慢なことを考えてざっと探すと、こんなのを発見:
Read Cabocha http://hayashibe.jp/tr/
pdfが2つある。特に2つめの方は、いろいろ参考になる。これ以外には残念ながらソース読みに役立ちそうな情報は見つからなかった。
さてソースに行く。知りたいのは当面「係り受け決定にどんな特徴(素性)を使ってるのか」だけなのだが、さすがに処理の流れがわからないとどこにその情報があるのかもわからないので、まずはメイン関数から処理の流れをざっと追ってみる。
メイン関数 main() はいくつかあるが、普通のparse用のは cabocha.cpp にあるのがそれらしい。(他のは学習用とか、モデル作成用とか?)この main() の中では、 cabocha_do() を呼ぶだけ。
cabocha_do()の定義は parser.cpp の最後にある。ここのメインループ(l.512)は
read_sentence() して parser.parseToString(str, size)
をひたすら繰り返す。1文ずつ、読んではパース、読んではパース、なんだろう。
read_sentence()の定義は utils.cpp にあるが、これは単に文字列を読み込むだけで難しい処理はしていない。
ちなみに入力フォーマットだが、まず cabocha には「レイヤ」という概念があることに留意。処理ステージ、みたいなもの。先の cabocha ページに解説があるが、
2. 文節区切り
3. 素性選択
4. 係り受け解析
とあり、それぞれの出力フォーマットも載っている。今の興味は係り受けの処理なので、文節区切りの出力を入力とすると思っておく。ならば'EOS'(=End of Sentence?)まで読み込めばよいことになる。なお1.2.4.はまあわかるとして、3.素性選択レイヤって何だ?と気になるところだが、これは後ほど。
さて parseToString(str, size) だが、これも parser.cpp にある。同じ名前がいくつかオーバーライドされててちょっとまぎらわしいが、今見るのはl.410の。要は parse(str, len) を呼んでから toString() で tree の内容を出力するみたい。ParserImpl クラスに tree_ というメンバ変数があり、parse() はその tree_ の内容を書き換える。Tree, Parser 両クラスの定義は cabocha.h にある。中身はおいおい見ていく。
parse()もオーバーライドがまぎらわしい。parse(str, len) の定義はl.367にあり、これは tree->read() してから parse(tree) する。2種類のparse()に留意のこと。
tree->read() は tree.cpp l.406 にある。詳細は後ほど書くが、ざっと見ると文字列を読みこんで tree のデータ構造に直しているようだ。
parse(tree) の定義は parser.cpp:l.352 に。analyzer_[i]->parse() をいくつかの i について順に呼ぶ。この analyzer_ とは何かと見ると、ParserImpl::open() が parser.cpp:l.197 で定義されていておそらく最初に呼ばれているのだが、ここで
310 PUSH_ANALYZER(Selector);
311 PUSH_ANALYZER(DependencyParser);
などとやっている。どうやら上述の各「レイヤ」に対して Analyzer クラスがあり、ここでpushしたAnalyzerを順に呼ぶ、ということのようだ。入力が文節区切り済みなら、その後の素性選択と係り受けだけやる、というわけ。
さて素性選択って何?だが、先のcabochaページに「素性選択レイヤの出力フォーマット」というのが載っている。これを見ると、こんな情報が付加されている。
* 2 -1D 0/2 0.000000 F_H0:読ん,F_H1:動詞,F_H2:自立,F_H5:五段・マ行,F_H6:連用タ接続,F_F0:いる,F_F1:動詞,F_F2:非自立,F_F5:一段,F_F6:基本形,A:基本形,B:動詞-自立
細かくは後で見るが、今は「ほー、素性としてこんな情報を使っているのか」程度に理解しておく。こういった情報を抽出するのが「素性選択」。
今回は大まかな処理の流れを見た。次回は具体的にtreeのデータ構造の中身を見ていく。