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