これはG* Advent Calendar 2015の2日目の記事です。
昨日のG* Advent Calendar 2015の1日目の記事では、Groovyとkuromojiによる形態素解析の基本的な部分に触れました。
2日目の今日は少し踏み込んで、自分で専用の辞書を作成してみます。
おさらい
Groovyとkuromojiを利用すれば、形態素解析が以下のようなとても短くシンプルなコードで実現できます。
以下のコードをkuromoji.groovy
として保存してください。
@Grapes([
@Grab(group='com.atilika.kuromoji', module='kuromoji-ipadic', version='0.9.0')
])
import com.atilika.kuromoji.ipadic.Token
import com.atilika.kuromoji.ipadic.Tokenizer
// ロジック部分
Tokenizer tokenizer = new Tokenizer()
tokenizer.tokenize("私は車で帰社した").each {
println it
}
コレをgroovy kuromoji.groovy
と実行すれば、形態素解析された結果が表示されます。
モードを切り替える
さて、Googleなどの検索エンジンを思い出してください。
例えば 関西国際空港 と検索した場合、当然 関西国際空港 に関連した情報が表示されますが、どんどんページを辿って行くと、関西 や 空港といったキーワードだけでヒットした結果も表示されたりしますよね?
kuromojiでそういった単語を形態素解析するとどうなるのでしょう?試してみましょう。
デフォルト(ノーマルモード)
// ロジック部分
println "NORMAL MODE:"
Tokenizer tokenizer = new Tokenizer()
tokenizer.tokenize("関西国際空港").each {
println "${it.surface}"
}
これを実行すると
NORMAL MODE:
関西国際空港
と表示されます。
確かに関西国際空港は固有名詞なのでコレで間違いではないのですが、検索エンジン的には関西と国際と空港に分けたい、と思いますよね。
そこで、kuromojiのモードを切り替えます。
サーチモード
コードを以下のように書き換えてみます。
// ロジック部分
Tokenizer.Builder builder = new Tokenizer.Builder()
builder.mode = com.atilika.kuromoji.TokenizerBase.Mode.SEARCH
Tokenizer searchedTokenizer = new Tokenizer(builder)
searchedTokenizer.tokenize("関西国際空港").each {
println "${it.surface}"
}
コレを実行します。
SEARCH MODE:
関西
国際
空港
ちゃんと関西国際空港がそれぞれの単語ごとに分割されました!
Tokenizerの内部クラスにBuilderというクラスがあるので、そのクラスのインスタンスを生成して、modeプロパティにcom.atilika.kuromoji.TokenizerBase.Mode.SEARCH
をセットします。
そして、Tokenizerインスタンスを生成する際に、コンストラクタにそのBuilderインスタンスを渡してあげるだけでOKです。
自分で辞書を定義する
さて、万能に思えるkuromojiですが、やはり限界はあります。
kuromojiに限らず、形態素解析では未知の単語を検出することは基本的に不可能です。
その対策としてN-Gramなどの方式が存在しますが、当然「だろう」という扱いになるので不完全です。
そこで、kuromojiでは新しい単語や独自の単語を簡単に追加することが出来ます!
(例えば流行り言葉や社内用語などなど)
まず、ロジック部分を以下のように書き換えてください。
// ロジック部分
Tokenizer.Builder userBuilder = new Tokenizer.Builder()
Tokenizer userTokenizer = new Tokenizer(userBuilder)
userTokenizer.tokenize("関西国際空港でターゲスシュピーゲル紙日曜版をほげる").each {
println "${it.surface}(${it.user})"
}
これを実行します。
関西国際空港(false)
で(false)
ターゲスシュピーゲル(false)
紙(false)
日曜(false)
版(false)
を(false)
ほ(false)
げ(false)
る(false)
このような実行結果が表示されています。
パット見ちゃんと形態素解析できているように見えますが、ちょっとコレをカスタマイズしてみましょう。
具体的には、ターゲスシュピーゲル紙
, 日曜版
, ほげる
をそれぞれ1つの単語としてkuromojiに扱ってもらうように、自分で新しく辞書を作ってみます。
辞書と言ってもただのテキストです。
辞書のフォーマットは、
単語,形態素解析後の各単語をスペース区切りで羅列,それぞれの読みをスペース区切りで羅列,Part of speech level 1に使われる値(名詞とか動詞とか何でもOK)
となります。
複数指定する場合は単純に改行で指定すばOKです。
ではコードを以下のように変更します。
// ロジック部分
ByteArrayInputStream myDictionary = new ByteArrayInputStream("""\
ターゲスシュピーゲル紙日曜版,ターゲスシュピーゲル紙 日曜版,ターゲスシュピーゲルシ ニチヨウバン,オレオレ名詞\n
ほげる,ほげる,ホゲル,オレオレ動詞
""".getBytes("UTF-8"))
Tokenizer.Builder userBuilder = new Tokenizer.Builder()
userBuilder.userDictionary(myDictionary).build()
Tokenizer userTokenizer = new Tokenizer(userBuilder)
userTokenizer.tokenize("関西国際空港でターゲスシュピーゲル紙日曜版をほげる").each {
println "${it.surface}(${it.user})"
}
実行します。
関西国際空港(false)
で(false)
ターゲスシュピーゲル紙(true)
日曜版(true)
を(false)
ほげる(true)
自分で定義した辞書がちゃんとkuromojiの解析結果に利用されていますね!
モードを切り替える際には、Builderクラスのインスタンスのmode
プロパティに値を設定していました。
同様に、自前のユーザ定義辞書を指定する際には、BuilderクラスのインスタンスのuserDictionary
メソッドに辞書を渡して、build
メソッドを実行してあげます。
単語の横に表示している${it.user}
の値がtrueになっているものが、ユーザ定義辞書から発見された値になります。
つまり、今我々はkuromojiに対して、ターゲスシュピーゲル紙
, 日曜版
, ほげる
という単語がこの世に存在しているということを教えてあげたわけです。
なお、1スクリプトで形態素解析!というコンセプトに基づいているために、スクリプト内に直接ユーザ定義辞書を定義していて、それをByteArrayInputStream型の変数に格納しています。
コレでも動くので特に問題はありませんが、テキストファイルとして外に追い出したい!という場合には、適当に辞書の内容をテキストファイルに書いて保存して、userDictionaryメソッドにフルパスでそのファイル名を渡してあげるだけでOKです。
その場合は以下のようになります。
// ロジック部分
Tokenizer.Builder userBuilder = new Tokenizer.Builder()
userBuilder.userDictionary("/PATH/TO/userdic.txt").build()
Tokenizer userTokenizer = new Tokenizer(userBuilder)
userTokenizer.tokenize("関西国際空港でターゲスシュピーゲル紙日曜版をほげる").each {
println "${it.surface}(${it.user})"
}
動作は同じです。
更に詳細
1日目の記事でやったように、ユーザ定義辞書を扱った際の詳細なデータも表示してみましょう。
// ロジック部分
ByteArrayInputStream myDictionary = new ByteArrayInputStream("""\
ターゲスシュピーゲル紙日曜版,ターゲスシュピーゲル紙 日曜版,ターゲスシュピーゲルシ ニチヨウバン,オレオレ名詞\n
ほげる,ほげる,ホゲル,オレオレ動詞
""".getBytes("UTF-8"))
Tokenizer.Builder userBuilder = new Tokenizer.Builder()
userBuilder.userDictionary(myDictionary).build()
Tokenizer userTokenizer = new Tokenizer(userBuilder)
userTokenizer.tokenize("関西国際空港でターゲスシュピーゲル紙日曜版をほげる").each {Token token ->
println "Surface: ${token.surface}"
println "All Features: ${token.allFeatures}"
println "allFeaturesArray: ${token.allFeaturesArray}"
println "Position: ${token.position}"
println "Part of speech level 1: ${token.partOfSpeechLevel1}"
println "Part of speech level 2: ${token.partOfSpeechLevel2}"
println "Part of speech level 3: ${token.partOfSpeechLevel3}"
println "Part of speech level 4: ${token.partOfSpeechLevel4}"
println "ConjugationType: ${token.conjugationType}"
println "ConjugationForm: ${token.conjugationForm}"
println "BaseForm: ${token.baseForm}"
println "Reading: ${token.reading}"
println "Pronunciation: ${token.pronunciation}"
println "Is there in dictionary?: ${token.known}"
println "declared by user?: ${token.user}"
println "*"*30
}
コレを実行すると、詳細な情報が表示されます。
今回定義した単語の部分のみ抽出すると以下のようになります。
Surface: ターゲスシュピーゲル紙
All Features: オレオレ名詞,*,*,*,*,*,*,ターゲスシュピーゲルシ,*
allFeaturesArray: [オレオレ名詞, *, *, *, *, *, *, ターゲスシュピーゲルシ, *]
Position: 7
Part of speech level 1: オレオレ名詞
Part of speech level 2: *
Part of speech level 3: *
Part of speech level 4: *
ConjugationType: *
ConjugationForm: *
BaseForm: *
Reading: ターゲスシュピーゲルシ
Pronunciation: *
Is there in dictionary?: false
declared by user?: true
******************************
Surface: 日曜版
All Features: オレオレ名詞,*,*,*,*,*,*,ニチヨウバン,*
allFeaturesArray: [オレオレ名詞, *, *, *, *, *, *, ニチヨウバン, *]
Position: 18
Part of speech level 1: オレオレ名詞
Part of speech level 2: *
Part of speech level 3: *
Part of speech level 4: *
ConjugationType: *
ConjugationForm: *
BaseForm: *
Reading: ニチヨウバン
Pronunciation: *
Is there in dictionary?: false
declared by user?: true
******************************
Surface: ほげる
All Features: オレオレ動詞,*,*,*,*,*,*,ホゲル,*
allFeaturesArray: [オレオレ動詞, *, *, *, *, *, *, ホゲル, *]
Position: 22
Part of speech level 1: オレオレ動詞
Part of speech level 2: *
Part of speech level 3: *
Part of speech level 4: *
ConjugationType: *
ConjugationForm: *
BaseForm: *
Reading: ホゲル
Pronunciation: *
Is there in dictionary?: false
declared by user?: true
******************************
まとめ
さて、どうでしたでしょうか。
基本的にkuromojiの説明じゃないか!という話になってしまいそうですが、Javaライブラリであるkuromojiを簡単に扱えて、さらにコードも余分な部分を削ることができるという点を見てみれば、Groovyがいかにイケてる言語なのかを実感することができるのではないでしょうか。
2015年はGroovyだけでなくGrailsもPivotalからスポンサードを打ち切られるというお家騒動が起きるなど波瀾万丈な年だったと思います。
しかしGrailsはOCIへ、GroovyはApache傘下となった結果、月間ダウンロード数が倍増するなど、結果的ににプラスに働いた面も合ったのではないでしょうか。
個人的にも2015年は自社サービスをPHP+SymfonyからGrovy+Grails2.4で完全に書き換えることができました。
これらを一人でやり遂げることが出来たのはGroovyとGrailsの異常なまでの生産性の高さと現実的なコンセプト、そしてTwitterなどでサポートしてくださった多くのGroovy、Grailsユーザの皆様のおかげです。
2016年もGroovyとGrailsの進化は止まらないハズです。(GradleやRatpackなど他のGも!)
これからもGを追い続けたいと思います。
参考
kuromoji
kuromoji(Github)
kuromoji-ipadic(Maven repository)
UserDictionaryTokenizerTest.java <--- UserDictionaryの書き方の参考になる
userdict.txt <--- kuromoji-coreのテストで利用されているサンプル