CLOVER🍀

That was when it all began.

Unicode絵文字に試験的に対応したmecab-ipadic-NEologdを使って、Lucene Kuromojiで形態素解析する

mecab-ipadic-NEologdが、Unicode絵文字に試験的に対応したと聞いて。

(Beer Mug)の読み方を考える(mecab-ipadic-NEologdのUnicode 絵文字対応)
http://www.slideshare.net/overlast/mecab-ipadicneologdpydatatokyo05pub-48560060

これをLucene Kuromojiの辞書に取り込んで、試してみようということで。

なお、最初に試した時、一部の絵文字のコストが高過ぎてLucene Kuromojiに取り込めなかったのですが、@overlastさんに対応していただけました。いつもありがとうございます…。

mecab-ipadic-NEologd in Lucene Kuromoji

まずは、Lucene Kuromojiにmecab-ipadic-NEologdを適用したJARファイルを作ります。

作り方は、すでに以前この作業を自動的に行うbashスクリプトを書いているので、こちらを使用します。

Lucene Kuromojiに対して、mecab-ipadic-neologdの辞書を適用してビルドするbashスクリプトを書きました
http://d.hatena.ne.jp/Kazuhira/20150317/1426606053

以下のコマンドを任意のディレクトリで実行して、スクリプトを取得します。

$ wget https://raw.githubusercontent.com/kazuhira-r/lucene-kuromoji-with-mecab-neologd-buildscript/master/build-lucene-kuromoji-with-mecab-ipadic-neologd.sh
$ chmod a+x build-lucene-kuromoji-with-mecab-ipadic-neologd.sh

ビルド実行。

$ ./build-lucene-kuromoji-with-mecab-ipadic-neologd.sh 
### [2015-05-28 21:35:32] [main] [INFO] START.

####################################################################
applied build options.

[MeCab Version]                 ... mecab-0.996
[MeCab IPA Dictionary Version]  ... mecab-ipadic-2.7.0-20070801
[Dictionary CharacterSet]       ... utf-8
[mecab-ipadic-NEologd Tag (-N)] ... master
[Max BaseForm Length]           ... 15
[Lucene Version Tag (-L)]       ... lucene_solr_5_1_0
[Kuromoji Package Name (-p)]    ... org.apache.lucene.analysis.ja

####################################################################

今のスクリプトでは、実行時の情報をいろいろ出すようになりました(笑)。

このエントリを書いている時点で使用したLuceneバージョンは5.1.0、mecab-ipadic-NEologdの辞書の日付は20150526です。

時間がかかるので、しばらく待ちます…。

数分待つと、JARファイルがカレントディレクトリにできあがります。

BUILD SUCCESSFUL
Total time: 7 seconds
-rw-rw-r-- 1 xxxxx xxxxx 28053544  528 21:41 lucene-analyzers-kuromoji-ipadic-neologd-5.1.0-20150526-SNAPSHOT.jar
### [2015-05-28 21:41:25] [main] [INFO] END.

それでは、このJARファイルを使って形態素解析をしてみましょう。

動作確認

動作確認には、Luceneのcoreとanayzers-commonが必要です。

val luceneVersion = "5.1.0"
libraryDependencies ++= Seq(
  "org.apache.lucene" % "lucene-core" % luceneVersion,
  "org.apache.lucene" % "lucene-analyzers-common" % luceneVersion
)

そして、先ほどビルドしたLucene KuromojiのJARファイルを、クラスパスが通った場所に配置します。sbtの場合は、libディレクトリ。

$ cp lucene-analyzers-kuromoji-ipadic-neologd-5.1.0-20150526-SNAPSHOT.jar /path/to/lib/lucene-analyzers-kuromoji-ipadic-neologd-5.1.0-20150526-SNAPSHOT.jar

ちょっと作為的ですが、このようなプログラムを用意。
src/main/scala/org/littlewings/lucene/kuromoji/KuromojiWithNeologd.scala

package org.littlewings.lucene.kuromoji

import scala.collection.immutable.ListMap

import java.io.StringReader

import org.apache.lucene.analysis.TokenStream
import org.apache.lucene.analysis.cjk.CJKWidthFilter
import org.apache.lucene.analysis.core.{LowerCaseFilter, StopFilter}
import org.apache.lucene.analysis.ja._
import org.apache.lucene.analysis.ja.dict.UserDictionary
import org.apache.lucene.analysis.ja.tokenattributes.{BaseFormAttribute, PartOfSpeechAttribute, ReadingAttribute, InflectionAttribute}
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute

object KuromojiWithNeologd {
  def main(args: Array[String]): Unit = {
    val texts = Array(
      "MySQLの🍣🍺問題",
      "今日は🍖と🐟どっちにしようかな。💊飲んでるし🍵にしよう😞"
    )

    for (text <- texts) {
      val tokenizer = new JapaneseTokenizer(null, true, JapaneseTokenizer.Mode.NORMAL)
      tokenizer.setReader(new StringReader(text))
      var tokenStream: TokenStream = tokenizer

      val charTermAttr = tokenStream.addAttribute(classOf[CharTermAttribute])
      val baseForm = tokenStream.addAttribute(classOf[BaseFormAttribute])
      val partOfSpeech = tokenStream.addAttribute(classOf[PartOfSpeechAttribute])
      val reading = tokenStream.addAttribute(classOf[ReadingAttribute])
      val inflection = tokenStream.addAttribute(classOf[InflectionAttribute])

      tokenStream.reset()

      val tokenAndAttrs =
        Iterator
          .continually(tokenStream.incrementToken())
          .takeWhile(identity)
          .map { _ =>
            ListMap(
              "token" -> charTermAttr.toString,
              "原型" -> baseForm.getBaseForm,
              "品詞" -> partOfSpeech.getPartOfSpeech,
              "読み" -> reading.getReading,
              "活用形" -> s"${inflection.getInflectionForm}",
              "活用型" -> s"${inflection.getInflectionType}"
            )
          }

      println(s"InputText = $text")
      println("  Tokenized:")

      tokenAndAttrs
        .foreach { taa =>
          println(taa.map { case (k, v) => s"$k: $v" }.mkString("    ", System.lineSeparator + "    ", System.lineSeparator))
        }

      println()

      tokenStream.close()
    }
  }
}

はてなに書いても、絵文字の部分が数値文字参照になってしまうので…絵文字の部分だけ抜き出すとこう書いています。

このプログラムを実行すると、このような結果になります。
※こちらも画像で



絵文字を理解している上に、読みまで付いてます。すごい!!

補足

先ほどのプログラムを出した時に、「ちょっと作為的」と書きましたが、これについて補足を。

実は、先ほどのプログラムはAnalyzerではなくTokenizerを使用しました。

ここで、Tokenizerではなく以下のようにAnalyzerを使用すると

    val texts = Array(
      "MySQLの&#127843;&#127866;問題",
      "今日は&#127830;と&#128031;どっちにしようかな。&#128138;飲んでるし&#127861;にしよう&#128542;"
    )

    val analyzer = new JapaneseAnalyzer

    for (text <- texts) {
      val tokenStream = analyzer.tokenStream("", text)

      /*
      val tokenizer = new JapaneseTokenizer(null, true, JapaneseTokenizer.Mode.NORMAL)
      tokenizer.setReader(new StringReader(text))
      var tokenStream: TokenStream = tokenizer
       */

絵文字が全部なくなってしまいます。

InputText = MySQLの&#127843;&#127866;問題
  Tokenized:
    token: mysql
    原型: null
    品詞: 名詞-固有名詞-一般
    読み: マイエスキューエル
    活用形: null
    活用型: null

    token: 寿司
    原型: 寿司
    品詞: 名詞-一般
    読み: スシ
    活用形: null
    活用型: null

    token: 問題
    原型: null
    品詞: 名詞-ナイ形容詞語幹
    読み: モンダイ
    活用形: null
    活用型: null


InputText = 今日は&#127830;と&#128031;どっちにしようかな。&#128138;飲んでるし&#127861;にしよう&#128542;
  Tokenized:
    token: 今日
    原型: null
    品詞: 名詞-副詞可能
    読み: キョウ
    活用形: null
    活用型: null

    token: どっち
    原型: null
    品詞: 名詞-代名詞-一般
    読み: ドッチ
    活用形: null
    活用型: null

    token: どっちにしようかな
    原型: null
    品詞: 名詞-固有名詞-一般
    読み: ドッチニシヨウカナ
    活用形: null
    活用型: null

    token: 飲む
    原型: 飲む
    品詞: 動詞-自立
    読み: ノン
    活用形: 連用タ接続
    活用型: 五段・マ行

    token: るし
    原型: null
    品詞: 名詞-固有名詞-一般
    読み: ルシ
    活用形: null
    活用型: null

これについてですが、現在は絵文字が「記号-一般」に分類されるため、JapanesePartOfSpeechStopFilterで除去されてしまいます。

なお、絵文字自体は複数の読みで登録されています。

$ grep 肉 mecab-ipadic-neologd/build/mecab-ipadic-2.7.0-20070801-neologd-20150526/mecab-user-dict-seed.20150526.csv
〜省略〜
&#127830;,1285,1285,5484,名詞,一般,*,*,*,*,肉,ニク,ニク
&#127830;,5,5,2024,記号,一般,*,*,*,*,肉,ニク,ニク
&#127831;,1285,1285,5484,名詞,一般,*,*,*,*,肉,ニク,ニク
&#127831;,5,5,2024,記号,一般,*,*,*,*,肉,ニク,ニク

複数登録されていますが、記号、一般の方がコストが低いため、こちらが選ばれるようです。

もっと細かく確認されたい方は、Tokenizerの部分を以下の用に定義して(JapaneseAnalyzerと同じものです)、TokenFilterを外したりしてみるとよいでしょう。

      val tokenizer = new JapaneseTokenizer(null, true, JapaneseTokenizer.Mode.NORMAL)
      tokenizer.setReader(new StringReader(text))
      var tokenStream: TokenStream = tokenizer
      tokenStream = new JapaneseBaseFormFilter(tokenStream)
      tokenStream = new JapanesePartOfSpeechStopFilter(tokenStream, JapaneseAnalyzer.getDefaultStopTags)
      tokenStream = new CJKWidthFilter(tokenStream)
      tokenStream = new StopFilter(tokenStream, JapaneseAnalyzer.getDefaultStopSet)
      tokenStream = new JapaneseKatakanaStemFilter(tokenStream)
      tokenStream = new LowerCaseFilter(tokenStream)

で、これらについて、@overlastさんに質問してみました。

twitter:603584068203188224:detail

このエントリの最初の方で参照していた資料を見ていたり、絵文字のコストの調整をお願いしていたのでわかりそうなものですが、なんとも微妙な質問の仕方をしてしまいました…。が、よくわかりました。

難しいですね…。

それにしても、いろいろ勉強になりました。mecab-ipadic-NEologdを更新、そして質問に答えていただいた@overlastさん、ありがとうございました!!