読者です 読者をやめる 読者になる 読者になる

中年engineerの独り言 - crumbjp

LinuxとApacheの憂鬱

All in mongo の形態素解析エンジン


https://github.com/crumbjp/analysis/tree/master/monmorp

About MONMORP

MongoDB上で動作する ”とにかくお手軽な 形態素解析エンジンです。

 MongoDBが立ち上がってる状態からなら、10分程度で使える状態になります!!

MongoDBのコレクションを直接、形態素解析を掛ける事ができます。
結果は別のコレクションに保存するかJSON形式で標準出力に出力できます。

特徴

単純な辞書にマッチさせるだけでなく、ある程度、文法や活用形を判断しているので
正確な分割や、品詞の抽出ができます。

 とはいえ、特に短い単語は難度が高く、混ざるケースもあります。(※下記既知の問題参照のこと)

MongoDB上に辞書を持ち、外来語や数値などを検出して、解析中にも辞書をアップデートしながら解析します。

解析精度

速度よりも手軽さと精度を重視した解析エンジンです。

速度を重視するならば、単純な先頭からコストマッチが早いのですがそれでは精度が出ません。
特にひらがな・カタカナの部分は壊滅的です。

また速度重視のエンジンは巷に溢れているのでわざわざ作る必要もありませんしね。


MONMORPは通常2深度以上の探索を行います。
  1. 直前の品詞により、品詞の候補を絞る。
  2. 先頭最大マッチをかける。
  3. その次に来る品詞や単語がマッチするなら採用。駄目なら2.に戻る。
  4. 3.と同様、更に次を評価し続ける。助詞、助動詞、名詞、動詞の打ち切りなどで、1.に戻る。

本来はドキュメント全体を評価し終えるまで3.を続けるべきですが、その様な探索アルゴリズムは複雑で重くなる。
軽量化の為、適当なところで探索を打ち切る必要があり、こうなっている。

例: |仕事|が|大方|終わっ|た|

 1. 文章の先頭は助詞、助動詞以外
  2. 最大マッチ                  => 仕事  :(名詞)
   3. 名詞の後は基本助詞がくる。         => が   :(助詞)
 1. 助詞の後は殆ど何でもあり。
  2. 最大マッチ                  => 大方  :(名詞&副詞可能)
   3. 副詞可能の後は動詞が来る場合がある。    => 終わっ :(動詞)『終わる』連用形
    4. 動詞の連用形は『っ』は助詞、助動詞が必須。=> た   :(助動詞)『た』原型

ディレクトリ構成

 [analysis]
  |- mongo.env            : MongoDBのパスを設定
  |- [lib]                : 共通ライブラリ
  |- [monmorp]
       |- README
       |- [bin]
       |   |- gendic.sh    : IPADICからMONMPRP用の辞書をビルドします
       |   |- parse.sh     : 形態素解析を行います
       |- [lib]            : スクリプト群
       |- [data]           : 一時データ用
       |   |- testdata.json: テストデータ
       |- [html]           : testdata.sh用

クイックスタート

 0.MongoDBを準備

    /usr/local/mongo以下にMongoDBを構築するか、既存のMongoDBのパスに合わせてmongo.envを編集する。

    MongoDB構築編

      cd /tmp
      wget http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-2.4.5.tgz
      tar xzf mongodb-linux-x86_64-2.4.5.tgz
      mkdir /usr/local/mongo
      mv mongodb-linux-x86_64-2.4.5/bin /usr/local/mongo
      mkdir /usr/local/mongo/data
      mkdir /usr/local/mongo/logs

      mongodの起動にはサンプルコンフィグを利用しても良いでしょう。
        https://github.com/crumbjp/analysis/blob/master/sampleconf/mongod.conf    

      /usr/local/mongo/bin/mongod -f <path to project>/sampleconf/mongod.conf

 1.IPA辞書をダウンロード

    pushd ./data
    wget http://iij.dl.sourceforge.jp/ipadic/24435/ipadic-2.7.0.tar.gz
    tar xzf ipadic-2.7.0.tar.gz
    popd

 2.辞書をビルド(辞書コレクション="analysis.dictionary"

    ./bin/gendic.sh -D analysis.dictionary -i data/ipadic-2.7.0

 3.動作確認(形態素解析結果が出力されます

    ./bin/parse.sh -D analysis.dictionary -i "世界の皆さんこんにちは。" -o - -V
続き

 4.テストデータをMongoDBに投入

    mongoimport --drop -d test -c testdoc --file data/testdata.json

 5.複数ドキュメントを一気に解析

    ./bin/parse.sh -D analysis.dictionary -c test.testdoc -f body -o test.out -C
    ./bin/parse.sh -D analysis.dictionary -c test.testdoc -f body -o test.out

   並列処理する場合:

    ./bin/parse.sh -D analysis.dictionary -c test.testdoc -f body -o test.out -C
    ./bin/parse.sh -D analysis.dictionary -c test.testdoc -f body -o test.out -j 4

 6.結果を確認

    mongo test <<<'db.out.find().sort({docid:1,idx:1})'

辞書詳細:( defalut: analysis.dictionary )

辞書はIPADICを元に同字意義語や活用形などを考慮した状態に直して使います。

辞書コレクションの構造
w 単語 or 活用候補配列
単語か活用候補
h 先頭文字配列
最長マッチにかける為のインデックス用の要素
l 文字列長
最長マッチにかける為のインデックス用の要素
s 品詞優先度
最長マッチにかける為のインデックス用の要素
c コスト
未使用(品詞で別けた方が結果が良かったので使わなくなった)
f 活用系区分
1 => 打ち切り可能
2 => 後ろ助詞、助動詞必須
3 => 後ろ動詞可能(通常は動詞+動詞は無い)
4 => 後ろ名詞優先

   ※ この辺は今後チューニングが必要
インデックス
最大マッチ用:{h:1,l:-1,s:1}
単語指定:{w:1}
品詞指定:{t:1}

結果コレクション

 形態素解析の結果であると共に、Vectorize結果になっています。

   docidとcを集計するとDF,TFが得られます。

結果コレクションの構造
docid
元ドキュメントの_id
idx
単語の現れた順
pos
ドキュメント中の単語の位置
w
単語
l
単語長
      活用している場合は同じ単語でも長さが変わります
c
単語ID(辞書コレクション中の_id)
インデックス
 結果取得用{docid:1,idx:1}

並列実行の動作と性能

ジョブ管理コレクション: <結果コレクション>.job

 MONMORPは各ジョブが同じドキュメントを処理する事を防ぐためにジョブ管理コレクションを作る。
 同じ結果コレクション名に対する処理は、同一ホストは勿論、別ホストで起動したジョブでも正常に動作できる。

プライマリノードへの処理:

  MONMORPの形態素解析中の書き込み処理は以下の3つである。
    1. ジョブ管理コレクションへのドキュメントID登録
    2. 解析結果の書き込み
    3. カタカナ、アルファベット、数値を新単語として辞書へ登録

性能面:

 プライマリノードのホスト
   MONGO_NODE環境変数をローカルホスト(プライマリノード)に指定した場合
   全てのDB操作がローカルホストで処理出来る為、一番高速に動作できる。

   他のホストでもジョブを走らせる時は、mongod用のコアを1つ分は確保して置くと良い。

 セカンダリのホスト
   MONGO_NODE環境変数をローカルホスト(セカンダリノード)に指定した場合
   ドキュメントの取得と、辞書の参照(最大ボトルネック)をローカルホストから行える為、そこそこ早い。

 他のホスト
   全てのDB操作をリモートから行う為、低速になる。
   しかしMongoDBノード以外のCPUリソースを利用できるメリットがある。

辞書ビルド:

    
  gendic.sh の --nheads オプションは単語のインデックスに含める先頭文字数です。(デフォルト2)
   この値は辞書コレクションの".meta"レコードに保存され、解析時にも使われます。
 
  nheadsを大きくすると、インデックスサイズが大きくなり、クエリー回数が増え、比較回数が減ります。
 
 
   例えばnheads=2の場合『インデックス』という単語では{ h: ["イ","イン"],l:6,s:300} この様にインデックスを張ります。
 
    解析時に最初に以下のクエリーを試します。
 
 
   『イン』で始まる単語を長い順:
     db.dictionary.find({h:"イン"}).sort({l:-1,s:1})
      - インフォメーション       
      - インターナショナル
      - インフォメーション       
      - インターネット
      - インデックス
      - インデアン
 
     この場合、インデックスにマッチさせる為には5回の比較が必要です。
             
    ところがnheads=3ではこうなります。
 
   『インデ』で始まる単語を長い順:
     db.dictionary.find({h:"インデ"}).sort({l:-1,s:1})
      - インデックス
      - インデアン
 
     この場合、1回の比較でマッチできます。

 nheadsを4以上にしても効果は薄くデメリットが目立つようになるようです。
 事実上、nheadsは2or3なのですが、2=>3ではクエリー回数は約1.5倍。比較回数(=辞書からフェッチする単語数)は約1/4。
 データ転送量が4倍違うにも関わらず、性能はほぼ同じか2の方が良い事が多いのです。

既知の問題

 MONMORPは通常、実用上問題ない程度の精度で解析できますが
 幾つか間違った分割をしてしまうケースが判明しています。

1.動詞+名詞のパターンで判定を間違える場合があります。

 文章:

   これを買うとしあわせに、、
   これを買うときは、、

 正しい分割:

   これ|を|買う|と|しあわせ|に|、、
   これ|を|買う|とき|は|、、

 MONMORPでは以下の様に分割してしまいます。

   これ|を|買う|と|しあわせ|に|、、
   これ|を|買う||き|は|、、

 これは動詞の活用形で、後ろに助詞、助動詞、動詞などが続く可能性がある場合、助詞を優先するようになっているからです。
 名詞を優先したり、最長マッチを採用してしまうと

   これ|を|買う|とし|あわせ|に|、、

 この様な結果を得てしまい、難しく解決できていません。
 これを解くには、もう少し深い探索を実装する必要があるのですが、性能との兼ね合いもあるので保留しています。

2.外来語がサ変接続する場合

 文章:

   ビルドする、、

 正しい分割:

   ビルド|する|、、

 MONMORPでは以下の様に分割してしまいます。

   ビルド||る|、、

 MONMORPは連続したアルファベット、数字、カタカナを一般名詞として扱います。
 名詞の直後には基本的に動詞は来ないのですが(『アメリカ』する。など)、例外的に『する』に接続する場合があります。
 特に『する』はサ行変格活用で厄介な活用をする動詞で、助詞などと誤判定し易く、現状助ける手立てがありません。

 どうしてもこのパターンを拾いたい場合は辞書を編集し、この様な単語を登録する必要があります。

  {w:"ビルド", h:["ビ","ビル"] l:3 , c:0, s:0 , t:["名詞","サ変接続"]}


 辞書の編集機能はまだ開発中ですが、mongo shellからはこの様に操作します。

  load('../lib/utils.js')
  load('lib/morpho.js')
  var nheads = db.dictionary.findOne({w:'.meta'}).nheads
  var word = {w:'ビルド',l:3,c:0,s:300,t:['名詞','ORG','外来語','サ変接続']}
  var dicword = morpho.forms(nheads,word)
    db.dictionary.findAndModify({ query: {w:dicword.w}, update: dicword, upsert: true, new:true});