こんにちは!エン・ジャパン株式会社でエンジニアをしております、まつなみです。社内勉強会でword2vecを扱った時、猫ひろしから猫を引きたいという話題になったのですが、学習済みモデルには「猫ひろし」という単語が存在せず試すことが出来なかったため、今回は1からword2vecモデルを作り「猫ひろしベクトル」を作成してみたいと思います。
勉強会の内容
勉強会は以下の内容で行いました。
- 単語の意味をコンピュータに与える方法
- シソーラス
- 分散表現
- word2vecについて
- 実際にword2vecを触ってみる
- chiVe のv1.2mc15というバージョンを利用
- 同義語取得や単語の足し引きのデモ
猫ひろしがモデルに存在しなかった原因
猫ひろしがモデルに存在しない原因として以下の2点が考えられます。
- そもそも猫ひろしがデータセットに存在していなかった
- 単語分割時に猫ひろしが「猫」と「ひろし」に分かれてしまった
データセットの準備
まずは猫ひろしを含まれているデータセットを探す必要があります。今回は日本語Wikipediaのデータを選びました。Wikipediaには猫ひろしについての記事が存在するため、「猫ひろし」をベクトル化することが出来ます。こちらのページからjawiki-latest-pages-articles.xml.bz2をダウンロード出来ます。
jawiki-latest-pages-articles.xmlはxmlファイルであるため、学習に必要なテキストのみを抽出する必要があります。WikiExtractorという便利なスクリプトが公開されているので、こちらを利用してテキスト抽出を行います。使い方は簡単で以下のように実行するだけです。
pip install wikiextractor
python -m wikiextractor.WikiExtractor jawiki-latest-pages-articles.xml.bz2
上記のコマンドを実行するとtextというフォルダが作成され、その中には、100個ずつにまとめられた、テキストファイルが生成されます。次に、モデルを学習する際には単語をまとめてモデルに与えることになるので、1つのファイルにまとめます。下記コマンドでwiki.txtという名前で1つのテキストファイルにまとめました。
find text | grep wiki | awk '{system("cat "$0" >> wiki.txt")}'
wiki.txtの中身を下記コマンドで確認してみます。
head -10 wiki.txt
<doc id="5" url="https://ja.wikipedia.org/wiki?curid=5" title="アンパサンド">
アンパサンド
アンパサンド(&, )は、並立助詞「…と…」を意味する記号である。ラテン語で「…と…」を表す接続詞 "et" の合字を起源とする。現代のフォントでも、Trebuchet MS など一部のフォントでは、"et" の合字であることが容易にわかる字形を使用している。
語源.
英語で教育を行う学校でアルファベットを復唱する場合、その文字自体が単語となる文字("A", "I", かつては " も)については、伝統的にラテン語の "(それ自体)を用いて "A per se A" のように
唱えられていた。また、アルファベットの最後に、27番目の文字のように "&" を加えることも広く行われていた。"&" はラテン語で "et" と読まれていたが、後に英語で "and" と読まれるようになった。結果として、アルファベットの復唱の最後は "X, Y, Z, "and per se and"" という形になった。この最後のフレーズが繰り返されるうちに "ampersand" と訛っていき、この言葉は1837年までには英語の一般的な語法となった。
アンドレ=マリ・アンペールがこの記号を自身の著作で使い、これが広く読まれたため、この記号が "Ampère's and" と呼ばれるようになったという誤った語源俗説がある。
歴史.
アンパサンドの起源は1世紀の古ローマ筆記体にまで遡ることができる。古ローマ筆記体では、E と T はしばしば合字として繋げて書かれていた(左図「アンパサンドの変遷」の字形1)。それに続く、
流麗さを増した新ローマ筆記体では、様々な合字が極めて頻繁に使われるようになった。字形2と3は4世紀中頃における et の合字の例である。その後、9世紀のカロリング小文字体に至るラテン文字の変遷の過程で、合字の使用は一般には廃れていった。しかし、et の合字は使われ続け、次第に元の文字がわかりにくい字形に変化していった(字形4から6)。
現代のイタリック体のアンパサンドは、ルネサンス期に発展した筆記体での et の合字に遡る。1455
年のヨーロッパにおける印刷技術の発明以降、印刷業者はイタリック体とローマ筆記体のアンパサン
ドの両方を多用するようになった。アンパサンドのルーツはローマ時代に遡るため、ラテンアルファ
ベットを使用する多くの言語でアンパサンドが使用されるようになった。
中身を確認すると、<doc>というタグが残っていることが分かります。単語をベクトル化する際にタグは邪魔となってしまうので、下記コマンドを用いてタグを削除してしまいましょう。
sed '/^<[^>]*>$/d' wiki.txt
単語分割を行う
word2vecを学習させる際、文を形態素解析器を用いて単語毎に区切りモデルに入力させる必要があります。さまざまな形態素解析器がありますが、今回は「猫ひろし」を識別するために、mecabという形態素解析器と、mecab-ipadic-NEologdという辞書を用います。mecab-ipadic-NEologdは頻繁に更新されていた辞書であり、新語や人名を多く含んでいます。(現在は更新が止まっており、最新の単語は入っていません。)実際に猫ひろしを含んでいる文をmecabとNEologdで解析すると以下のようになります。猫ひろしを固有名詞として識別できていることがわかります。
それでは、先ほど作ったwiki.txtを単語分割してきます。以下のようなpythonのコードでwiki.txtを分かち書きしたwiki_wakati.txtというテキストファイルを作成しました。
import MeCab
from typing import List
mecab = MeCab.Tagger()
mecab.parse("")
def parse(line: str) -> List[str]:
"""文を単語分割する(活用する単語は基本形に変換)
Args:
line (str): 文(私は野球をします)
Returns:
List[str]: 単語分割結果(['私', 'は', '野球', 'を', 'する', 'ます'])
"""
node = mecab.parseToNode(line)
word_list = []
while node:
if node.surface == "":
node = node.next
continue
if node.feature.split(",")[0] == "動詞" or node.feature.split(",")[0] == "形容詞":
word_list.append(node.feature.split(",")[6])
else:
word_list.append(node.surface)
node = node.next
return word_list
def main():
with open("wiki.txt", "r") as rf:
line = rf.readline()
while line:
word_list = parse(line)
if word_list != []:
with open("wiki_wakati.txt", "a") as wf:
wf.write(" ".join(word_list))
wf.write("\n")
line = rf.readline()
if __name__ == "__main__":
main()
今回の分かち書きの処理では、動詞と形容詞を基本形に変換しています。これは動詞と形容詞は活用する品詞であり、同じ単語で活用形によって違うベクトルが作られてしまうということを防ぐためです。
word2vecを学習させる
gensimというライブラリを用います。gensimを使うと簡単に学習モデルを作成することが可能です。以下のように実装しました。今回は出現頻度が10回以上の単語のみに絞り、200次元の単語ベクトルを作成します。word2vecのモデルはskip-gramです。作成したモデルを再利用できるように、word2vec.modelというファイル名で保存します。
from gensim.models import Word2Vec
model = Word2Vec(corpus_file="wiki_wakati.txt", min_count=10, vector_size=200, window=10, sg=1)
model.save("word2vec.model")
sentenceという引数に単語区切りにしたリストを渡すことも可能ですが、wiki_wakati.txtは4GB以上のファイルで非常に大きいため、corpus_fileにファイル名を渡してあげることで、効率よくモデルに単語を学習させることが可能です。4時間ほどで作成が可能です。
単語の足し引きをやってみる
まずは猫ひろしの同義語を取得してみます。以下のような形で同義語と類似度を出力させることが可能です。
from gensim.models import Word2Vec
model = Word2Vec.load("word2vec.model")
print(model.wv.most_similar(positive=["猫ひろし"]))
出力結果は以下のようになりました。
[('庄司智春', 0.7805563807487488), ('にしおかすみこ', 0.7529377937316895), ('松井絵里奈', 0.7520824074745178), ('パッション屋良', 0.7513907551765442), ('ANZEN漫才', 0.7500274777412415),
('安田大サーカス', 0.7498866319656372), ('じゅんいちダビッドソン', 0.7428767681121826), ('弾丸ジャッキー', 0.7407255172729492), ('重盛さと美', 0.7404525279998779), ('中川パラダイス', 0.7396645545959473)]
芸人さんが多く出力されていることが分かります。次に、猫ひろしから猫を引いた単語ベクトルの同義語を出力してみます。弾きたい単語はmost_similar(negative=['単語'])
としてやれば良いです。
from gensim.models import Word2Vec
model = Word2Vec.load("word2vec.model")
print(model.wv.most_similar(positive=["猫ひろし"], negative=["猫"]))
出力結果は以下のようになりました。
[('川内優輝', 0.4621363580226898), ('松井絵里奈', 0.45769035816192627), ('谷川真理', 0.4569548964500427), ('水野裕子', 0.4466515779495239), ('高岡寿成', 0.4451706111431122), ('菊地毅', 0.4439202547073364),
('高橋健一', 0.44183745980262756), ('東京マラソン2009', 0.4400731027126312), ('大畑大介', 0.43958279490470886), ('大迫傑', 0.43841105699539185)]
川内優輝さんをはじめマラソンの選手が多く出ていることが確認できます。これによって、猫ひろしから猫を引くとお笑いの要素が消されることが分かりました。
まとめ
本記事では、以下のような内容でword2vecを学習し単語の分散表現を獲得しました。
- 日本語wikiデータをダウンロードしテキスト抽出を行う
- 形態素解析器(mecab)を用いて単語分割を行う
- gensimでモデルの学習を行う
実際に「猫ひろし」 - 「猫」を行うと以下のことが分かりました。
- 猫ひろしの猫はお笑い要素であり、無くなってしまうとマラソン選手に似たベクトルとなる