Word2Vec
Word2Vecとは、2013年にGoogle研究所が発表したアルゴリズムで、「同じ文脈で利用される単語は、同じ意味を持つ」という仮説に基づき、「単語」の特徴をベクトルで表現する技術になります。
単語の特徴や意味を含めてベクトル化し、意味的に近い単語は、空間上で近くに存在するベクトルとして表現されることから、類義語の抽出に用いられています。
テーマ
今回は Apache spark の MLlib を用いて、Word2Vec を試してみますが、テーマとして wikipedia の”昭和”と”平成”のなど記事をロードさせてみて、それを元に出来たモデルに色んな用語を入力すると、どのような類義語が得られるかを見てみたいと思います。
コアプログラム
実行は spark-shell を用いますが、コアとなるプログラムについては事前にファイルに書いておいて、それをロードして使います。以下がそのプログラムになります。
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
}
データソースのダウンロード
以下のページのコンテンツを使用しています。HTMLタグは含めずにコンテンツのみをテキストに貼り付けました。
これらのテキストファイルは /data/mllib/word2vec 配下に保存しておきます。
昭和
平成
令和
spark-shell を起動する
spark(今回は version 2.1.1) を使用しています。
-i オプションで上記の word2vec.scala を読み込みつつ spark-shell を起動します。
これで spark-shell 内で tokenize() を使用することができます。
/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").map(line => tokenize(line))
2.単語出現数の下限数と生成する単語のベクトルの次元数をセット
val word2vec = new Word2Vec()
word2vec.setMinCount(3)
word2vec.setVectorSize(30)
3.モデルの生成
以上のステップでモデルが完成します。
val model = word2vec.fit(input)
類義語を探してみる
スコア(類似度合い)の高いものから順に5つ検出します。
model.findSynonyms("平成",5)
res2: Array((昭和,0.8683416787288002), (年,0.7191081645948658), (:,0.7109386795501776), (1970,0.6096518744108191), (日本,0.5882052253525422))
"平成"の類義語として、"昭和"がトップに検出されているのは(偶然にしても)素晴らしいです。
":" とか意味不明ですが、モデル生成前に予め除去しておくべきテキストになると思います。
model.findSynonyms("昭和",5)
res3: Array[(String, Double)] = Array((平成,0.8683416675677946), (年,0.7414342466252306), (現代,0.7280670946869915), (敗戦,0.6568551583363003), (1970,0.6326117643599328))
逆に"昭和"で検索すると、"平成"がトップの類義語として出てきてくれました。
"1970" など明らかに年代を示すキーワードも除去しておくべきテキストかもしれません。
model.findSynonyms("令和",5)
java.lang.IllegalStateException: 令和 not in vocabulary
"令和"という単語自体はテキスト内で頻出しているはずでしたが、生成したモデル内では単語の一つとして登録がされていませんでした。これは使用した lucene-kuromoji のバージョンが古かったため、"令和" という単語で形態素解析されなかったためと思われます。
model.findSynonyms("元号",5)
res9: Array[(String, Double)] = Array((令,0.7863372339299479), (''',0.7558561207740144), (和,0.7263245520368361), (淑,0.7139302506327456), (曲,0.7057422726424456))
"元号"の類義語として、"令"が1位、シングルクォートが2位になってしまったのを除くと、"和"が2位として検出されました。
これが元々は"令和"という単語から来ているのかはわかりませんが、面白い結果になりました。
たった3ページのウィキペディアの情報をインプットしただけですが、それとなく関連するワードが検出されることが分かります。
ただ、情報量をとにかく増やせば精度が上がるというわけでもなく、記号や数字などの余分な情報が混在することにより、精度が下がる印象を受けました。
インプットする情報をどのようにして一貫性のある質の高いものに出来るかが良いモデル生成のポイントの一つになりそうです。