CLOVER🍀

That was when it all began.

Kuromoji(Atilika)に、mecab-ipadic-neologdの辞書を適用できない?という話

先日まで、Luceneに付いているKuromojiを使って、mecab-ipadic-neologdの辞書を頑張って適用していましたが、最後にこちらに試してみたいと思います。

kuromoji
http://atilika.org/

GitHub
https://github.com/atilika/kuromoji

Lucene Kuromojiを使ったエントリはこちら
http://d.hatena.ne.jp/Kazuhira/20150316/1426520209

こちらのKuromojiは、Luceneに依存していない、単体で使える形態素解析器です。後にLuceneに寄贈、取り込まれた、と。
Lucene版のKuromoji同様、IPA辞書とNAIST辞書、そしてUnidicが使用できるようです。
※Lucene版Kuromojiは、Unidicは不可

このKuromojiが使用する辞書を、mecab-ipadic-neologdの提供するものに替えてみます。

mecab-ipadic-NEologd
https://github.com/neologd/mecab-ipadic-neologd

結論

やめとけ

LuceneのKuromojiを使おう

-終了-

じゃなくて

で、それで終わっても何なので、一応やったことは載せておきます。
※mecab-ipadic-neologdに何か問題があったとか、そういう話ではありません

個人的には、KuromojiのDoubleArrayTrieをなんとかできるのなら、使えるのではないかなーとは思っています。

なので、以降は微妙な結果になることを踏まえてご覧ください。

mecab-ipadic-neologdによる辞書のビルドは、あらかじめ完了しているものとします。
また、Lucene Kuromojiの時のように、原形が15文字を超えていると取り込めない、なんてことはありません。

とりあえずサンプルプログラム

まずは、通常のKuromojiを使ったサンプルプログラムを書いてみます。ちなみに、AtilikaのKuromojiを使うのは初めてです。
sample.groovy

@GrabResolver(name = 'Atilika Open Source repository', root = 'http://www.atilika.org/nexus/content/repositories/atilika')
@Grab('org.atilika.kuromoji:kuromoji:0.7.7')
import org.atilika.kuromoji.Tokenizer

def texts = ['すもももももももものうち',
             'きゃりーぱみゅぱみゅは、2012年に「つけまつける」でデビュー!',
             '日本経済新聞でモバゲーの記事を読んだ',
             'くりぃむしちゅーは、上田晋也と有田哲平の2人からなる日本のお笑いコンビ',
             '艦隊これくしょんは、角川ゲームスが開発し、DMM.comが配信しているブラウザゲーム']

def tokenizer = Tokenizer.builder().build()

texts.each { text ->
  println("$text")
  println('   [' + tokenizer.tokenize(text).collect { it.surfaceForm }.join(', ') + ']')
}

形態素解析する対象は、Lucene Kuromojiで試した時と同じです。

実行すると、このような結果に。

$ groovy sample.groovy
すもももももももものうち
   [すもも, も, もも, も, もも, の, うち]
きゃりーぱみゅぱみゅは、2012年に「つけまつける」でデビュー!
   [きゃ, り, ー, ぱみゅぱみゅは, 、, 2012, 年, に, 「, つけ, ま, つける, 」, で, デビュー, !]
日本経済新聞でモバゲーの記事を読んだ
   [日本経済新聞, で, モバゲー, の, 記事, を, 読ん, だ]
くりぃむしちゅーは、上田晋也と有田哲平の2人からなる日本のお笑いコンビ
   [くり, ぃむしちゅ, ー, は, 、, 上田, 晋, 也, と, 有田, 哲, 平, の, 2, 人, から, なる, 日本, の, お笑い, コンビ]
艦隊これくしょんは、角川ゲームスが開発し、DMM.comが配信しているブラウザゲーム
   [艦隊, これ, くし, ょんは, 、, 角川, ゲームス, が, 開発, し, 、, DMM, ., com, が, 配信, し, て, いる, ブラウザゲーム]

品詞などの情報も出力するサンプルが多いと思いますが、今回は形態素解析された結果のみ出力しました。

Kuromojiをビルドする

続いて、Kuromojiをビルドします。まずは、普通にビルドしてみます。

GitHubからclone。

$ git clone https://github.com/atilika/kuromoji.git

最新版でタグがふられている、0.7.7を選びます。

$ cd kuromoji
$ git checkout 0.7.7

ビルド。-Ddownload=trueで、IPA辞書のダウンロードを含めて実行してくれます。

$ mvn -Ddownload=true package

ここは普通に終了します。

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:44 min
[INFO] Finished at: 2015-03-18T22:52:35+09:00
[INFO] Final Memory: 31M/945M
[INFO] ------------------------------------------------------------------------

ちなみに、カレントディレクトリのdictionaryディレクトリ内に、ダウンロードしてきたIPA辞書が置かれています。

$ ls -l dictionary
合計 11928
drwxrwxr-x 2 xxxxx xxxxx     4096  3月 18 22:51 mecab-ipadic-2.7.0-20070801
-rw-rw-r-- 1 xxxxx xxxxx 12208105  3月 18 22:51 mecab-ipadic-2.7.0-20070801.tar.gz

medab-ipadic-neologdの辞書を使って、Kuromojiをビルドする

では、このdictionaryディレクトリの中に、mecab-ipadic-neologdの辞書を放り込みます。対象は、ビルド時に作成される「build」ディレクトリの中身になります。

$ cp -Rp [mecab-ipadic-neologdをビルドしたディレクトリ]/build/mecab-ipadic-2.7.0-20070801-neologd-20150317 dictionary

辞書が大きいので、ヒープを広げます。

$ export MAVEN_OPTS='-Xmx2g'

辞書の置かれているディレクトリ、辞書のエンコーディングを指定して、パッケージング。-Ddownload=trueは不要です。また、上手くいった場合は形態素解析の結果が変わってしまい、テストが失敗するのでテストはスキップします。

$ mvn \
> -Dkuromoji.dict.dir=dictionary/mecab-ipadic-2.7.0-20070801-neologd-20150317 \
> -Dkuromoji.dict.encoding=utf-8 \
> -DskipTests=true \
> package

辞書のビルドが始まります。

dictionary builder

dictionary format: IPADIC
input directory: dictionary/mecab-ipadic-2.7.0-20070801-neologd-20150317
output directory: target/classes
input encoding: utf-8
normalize entries: false

マシンスペックにもよると思いますが、けっこうな時間がかかります。で、根気強く待っていると…

コケてくれます。

building tokeninfo dict...
  building double array trie...  done
  processing target map...[WARNING] 
java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:297)
	at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.ArrayIndexOutOfBoundsException: -1
	at org.atilika.kuromoji.dict.TokenInfoDictionary.addMapping(TokenInfoDictionary.java:104)
	at org.atilika.kuromoji.util.DictionaryBuilder.build(DictionaryBuilder.java:61)
	at org.atilika.kuromoji.util.DictionaryBuilder.main(DictionaryBuilder.java:108)
	... 6 more
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 13:07 min
[INFO] Finished at: 2015-03-18T23:17:18+09:00
[INFO] Final Memory: 16M/1727M

ここまで13分!Lucene Kuromojiでmecab-ipadic-neologdを使った場合は、3分くらいで済んだのにこの差は…。

で、どこでコケているかなのですが、この部分です。

		// Prepare array -- extend the length of array by one
		int[] current = targetMap[sourceId];

https://github.com/atilika/kuromoji/blob/0.7.7/src/main/java/org/atilika/kuromoji/dict/TokenInfoDictionary.java#L104

このsourceIdが「-1」だとおっしゃっている…。

「-1」を返しているのはどこかというと、複数あるのですがどうも今回はここっぽいです。

	private int matchTail(int base, int index, String key) {
		int positionInTailArr = base - TAIL_OFFSET;
		
		int keyLength = key.length();
		for(int i = 0; i < keyLength; i++) {
			if(key.charAt(i) != tailBuffer.get(positionInTailArr + i)){
				return -1;
			}
		}
		return tailBuffer.get(positionInTailArr + keyLength) == TERMINATING_CHARACTER ? index : 0;
		
	}

https://github.com/atilika/kuromoji/blob/0.7.7/src/main/java/org/atilika/kuromoji/trie/DoubleArrayTrie.java#L238

なんかですね、最初に構築した単語情報からDoubleArrayTrieを作っているのですが、一部の単語に対するインデックスをDoubleArrayTrieから引けなくなっているみたいなのですよ。

			int doubleArrayId = trie.lookup(surfaceform);

https://github.com/atilika/kuromoji/blob/0.7.7/src/main/java/org/atilika/kuromoji/util/DictionaryBuilder.java#L59

作ったばかりの情報を見失ってる…。

このDoubleArrayTrieはbaseBuffer、checkBuffer、tailBufferという3つのNIOのBufferでできているのですが、このうち見に行ってはいけないパターンでtailBufferを見に行っているような気がします(代わりに、checkBufferを見るべきでは?という予想…)。

そこで、DoubleArrayTrieでのtailBufferを探索に行く閾値をちょっと変えてみます。ここを
src/main/java/org/atilika/kuromoji/trie/DoubleArrayTrie.java

	private static final int TAIL_OFFSET = 10000000;

ちょっと10倍に(適当)。

	private static final int TAIL_OFFSET = 100000000;

では、気を取り直して再実行!

$ mvn \
> -Dkuromoji.dict.dir=dictionary/mecab-ipadic-2.7.0-20070801-neologd-20150317 \
> -Dkuromoji.dict.encoding=utf-8 \
> -DskipTests=true \
> package

また待ちます…。

結果…

dictionary format: IPADIC
input directory: dictionary/mecab-ipadic-2.7.0-20070801-neologd-20150317
output directory: target/classes
input encoding: utf-8
normalize entries: false

building tokeninfo dict...
  building double array trie...  done
  processing target map...  done
done
building unknown word dict...done
building connection costs...done

〜省略〜

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 13:40 min
[INFO] Finished at: 2015-03-18T23:41:39+09:00
[INFO] Final Memory: 23M/1806M
[INFO] ------------------------------------------------------------------------

今度は成功しました!!13分かかりましたけど…。

…ホントか??

一応、動かしてみる

それでは、先ほど作成したGroovyスクリプトから、Grapeの部分を除いたものを用意します。
sample_neologd.groovy

import org.atilika.kuromoji.Tokenizer

def texts = ['すもももももももものうち',
             'きゃりーぱみゅぱみゅは、2012年に「つけまつける」でデビュー!',
             '日本経済新聞でモバゲーの記事を読んだ',
             'くりぃむしちゅーは、上田晋也と有田哲平の2人からなる日本のお笑いコンビ',
             '艦隊これくしょんは、角川ゲームスが開発し、DMM.comが配信しているブラウザゲーム']

def tokenizer = Tokenizer.builder().build()

texts.each { text ->
  println("$text")
  println('   [' + tokenizer.tokenize(text).collect { it.surfaceForm }.join(', ') + ']')
}

このスクリプトと同じディレクトリに、できあがったKuromojiのJARファイルをコピーします。

$ cp target/kuromoji-0.7.7.jar [スクリプトの置いてあるディレクトリ]

動かしてみます。

$ groovy -cp kuromoji-0.7.7.jar sample_neologd.groovy 
すもももももももものうち
   [すもももももももものうち]
きゃりーぱみゅぱみゅは、2012年に「つけまつける」でデビュー!
   [きゃりーぱみゅぱみゅ, は, 、, 2012年, に, 「, つけまつける, 」, で, デビュー, !]
日本経済新聞でモバゲーの記事を読んだ
   [日本経済新聞, で, モバゲー, の, 記事, を, 読ん, だ]
くりぃむしちゅーは、上田晋也と有田哲平の2人からなる日本のお笑いコンビ
   [くりぃむしちゅー, は, 、, 上田, 晋也, と, 有田哲平, の, 2, 人, から, なる, 日本, の, お笑いコンビ]
艦隊これくしょんは、角川ゲームスが開発し、DMM.comが配信しているブラウザゲーム
   [艦隊これくしょん, は, 、, 角川ゲームス, が, 開発, し, 、, DMM.com, が, 配信, し, て, いる, ブラウザゲーム]

一応、Lucene Kuromojiを使った時と同じような結果が出ています。

そんなわけで

なんともまあ、微妙な結果になりました。なんとなく、大量の単語が登録された辞書をKuromojiに食わせるとこうなるような気がするのですが、DoubleArrayTrieがちゃんと読めてない上に(読め)、辞書のビルドに失敗させるだけの単語量を与えるとかなり時間がかかるので、正直やってられません…。

この修正には全然自信がないので、DoubleArrayTrieを直せる人がいれば、お願いします…。

なお、Lucene Kuromojiの方だと、実装が別物になっていて、辞書まわりのクラスもまったく異なるものになっています(代わりに、原形の15文字制限が…)。

あと、Kuromojiのmasterブランチに対しては試していません。