1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Apache spark Word2Vec で関係性を加味した類似語を求める

Posted at

Apache spark で Word2Vecの続きになります。

Word2Vec では単語と単語の関係性をベクトルの演算で表現できます。
例えば、king から man の属性を引き、woman を加えると queen が導かれるようなイメージです。
“king”-“man”+“woman”=“queen”

テーマ

青空文庫で公開されている "吾輩は猫である" を読み込ませて Word2Vec を用いてモデルを作成します。
そのモデルに対して、関係性を加味した類似語を求めてみます。

事前準備

テキストには、ルビなどを示す記号が含まれているため、それらを除去しておきます。
そして、/data/mllib/word2vec-2 配下に保存しておきます。

$ sed -i 's/《.*》\|[#.*]\||\|〔.*〕//g' wagahai.txt

コアプログラム

実行は spark-shell を用いますが、コアとなるプログラムについては事前にファイルに書いておいて、それをロードして使います。以下がそのプログラムになります。
読み込んだテキストをトークン化(文字列の分割)する tokenize() と、類似語を計算する getSynonymWords() を定義しておきます。

word2vec.scala
import java.io.StringReader
import org.apache.spark.mllib.feature.{Word2Vec, Word2VecModel}
import org.apache.lucene.analysis.ja.JapaneseTokenizer
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute
import org.apache.lucene.analysis.ja.tokenattributes.BaseFormAttribute
import org.apache.lucene.analysis.ja.tokenattributes.PartOfSpeechAttribute
import org.apache.spark.mllib.linalg.Vectors

import scala.collection.mutable.ArrayBuffer
def tokenize(sentence: String): Seq[String] = {
  val word: ArrayBuffer[String] = new ArrayBuffer[String]()
  lazy val stream = new JapaneseTokenizer(
    new StringReader(sentence),
    null,
    false,
    JapaneseTokenizer.Mode.NORMAL)

  try {
    while(stream.incrementToken()) {
      var charAtt = stream.getAttribute(
        classOf[CharTermAttribute]
      ).toString

      var bfAtt = stream.getAttribute(
        classOf[BaseFormAttribute]
      ).getBaseForm

      var partOfSpeech = stream.getAttribute(
        classOf[PartOfSpeechAttribute]
      ).getPartOfSpeech().split("-")(0)

      (partOfSpeech, bfAtt) match {
        case ("名詞", _)  => word += charAtt
        case ("動詞", null)  => word += charAtt
        case ("動詞", baseForm)  => word += baseForm
        case (_, _)  =>
      }
    }
  } finally {
    stream.close
  }
  word.toSeq
}

def getSynonymWords(r1: String, r2: String, word: String, model: Word2VecModel): Array[(String, Double)] = {
  val src1 = breeze.linalg.Vector(model.getVectors(r1))
  val src2 = breeze.linalg.Vector(model.getVectors(r2))
  val target = breeze.linalg.Vector(model.getVectors(word))
  val targetWithRelation = target + (src2 - src1)
  model.findSynonyms(Vectors.dense(targetWithRelation.toArray.map(_.toDouble)), 10)
}

spark-shell を起動する

spark(今回は version 2.1.1) を使用しています。
-i オプションで上記の word2vec.scala を読み込みつつ spark-shell を起動します。
これで spark-shell 内で tokenize() と getSynonymWords() を使用することができます。

/usr/hdp/2.6.2.0-205/spark2/bin/spark-shell --master local --deploy-mode client --packages org.apache.lucene:lucene-kuromoji:3.6.2 --conf spark.serializer=org.apache.spark.serializer.KryoSerializer -i /spark/src/mllib/word2vec.scala

コマンド実行

以下、spark-shell でのコマンドです。
1.テキストファイル読み込み
予め用意しておいたテキストファイルを読み込み、トークン化します。

val input = sc.textFile("/data/mllib/word2vec-2").map(line => tokenize(line))

2.単語出現数の下限数と生成する単語のベクトルの次元数をセット

val word2vec = new Word2Vec()
word2vec.setMinCount(3)
word2vec.setVectorSize(30)

3.モデルの生成
以上のステップでモデルが完成します。

val model = word2vec.fit(input)

4.関係性を加味した類似語を探す
私⇒人間 の関係性に対して、猫⇒人間 が推測されていることになります。
一般的には 猫⇒動物 と考えられそうですが、猫が人間のように描かれている小説であるため、この結果自体には違和感はないと思います。

getSynonymWords("私","人間","猫",model)
res2: Array[(String, Double)] = Array((人間,0.9024111003792761), (申す,0.8644184750169329), (者,0.8520097131666761), (もの,0.84971317882835), (打つ,0.8355519738235462), (吾輩,0.8329425382047636), (容赦,0.8295952706147414), (思う,0.8287090333122259), (甲,0.8275915949463694), (事件,0.8273686781030468))

他の例を見てみると、男⇒女 の関係性に対して、伯父⇒祖母 は辛うじて分かりますが、それ以外は推測されている意味が分かりません。

getSynonymWords("男","女","伯父",model)
res15: Array[(String, Double)] = Array((碁,0.8453217654328342), (毎月,0.8363989347717034), (紳士,0.8290600068004638), (祖母,0.828510628102892), (据,0.824090994336325), (もぐる,0.8229206529639936), (いくら,0.8217076493616556), (あれ,0.8202529119250722), (柿,0.8186006480467625), (たぎる,0.8115129920852875))

また、男⇒女の関係性に対して、彼⇒女 という結果も得られました。

getSynonymWords("男","女","彼",model)
res16: Array[(String, Double)] = Array((女,0.8764545716038742), (水,0.8503040881551271), (吾,0.830030327479066), (四つ,0.8224119408842357), (おだやか,0.8138338653641275), (三,0.8058477340727931), (働,0.7940961238401808), (保護,0.7886006533220054), (習慣,0.7885285547520795), (風邪,0.7874742971018587))

"彼女"と結果が出ればバッチリでしたが、そもそも"彼女"という単語が小説内で1度しか出てきておらず、モデルに単語が登録されていなかったようです。

model.findSynonyms("彼女",5)
java.lang.IllegalStateException: 彼女 not in vocabulary

その他の主要な単語も調べてみると、"吾輩" の類似語のトップが "事件" や "働" であったり、"猫" に対しても "水" や "ため" であったりしました。
共通して出たのは "保護" という単語ですが、特に小説内で "吾輩" や "猫" が "保護" されるような箇所はないため、どのように推測されているのか理解が難しいところです。

model.findSynonyms("吾輩",5)
res8: Array[(String, Double)] = Array((事件,0.8872753095002464), (働,0.8790613435155954), (保護,0.8639525373437402), (いう,0.8599642072826214), (芸術,0.8596569602093977))
model.findSynonyms("猫",5)
res9: Array[(String, Double)] = Array((水,0.8945408332617345), (ため,0.8559240176609174), (保護,0.8081953536934757), (吾輩,0.7901985849430482), (一,0.7817463337524215))

"主人" という単語は小説内でも出現頻度が高いです。
"うち" という単語がトップの類似スコアを持っていましたが、小説内では"うちの亭主"という表現が何度か出てきていますので、"うち(の亭主)" = "主人" として解釈され、それが "うち" として推測されているのかもしれません。

model.findSynonyms("主人",5)
res10: Array[(String, Double)] = Array((うち,0.8927250488177164), (これ,0.8305789052095515), (皮,0.8074327990147161), (双方,0.7981844409788807), (違う,0.7853646794971598))

以上のように、納得感のある回答もある中で、理解が難しい結果も多々ありました。
小説という題材を元にモデルを生成しているため、表現力が豊かであるが故の学習の難しさがあるような気もします。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?