WikipediaのデータでWord2Vecを試す

いまさらWord2Vecをやってみる。

やりたいこと

Wikipediaの全日本語記事をWord2Vecする。これにより、単語を単語ベクトルに変換できる。
その後、単語の関連度とかを見て遊ぶ。

環境

  • OS
    Ubuntu 16.04 LTS
  • Python
    3.5.2
  • gensim
    3.7.3

流れ

Word2Vec使うときはいつもこんな感じっぽい。

  1. テキストを用意する
  2. 分かち書きにする
  3. Word2Vecにかける

テキストを用意する

ダンプデータのダウンロード

Wikipediaのダンプデータについてはここに書いてある。
これをたどると記事全体のダンプデータのリンクがわかる。以下のようにしてダウンロードする。(-o でダウンロード先を指定している。)

curl -o ~/Downloads/wikipedia.bz2 https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-pages-articles.xml.bz2

ダンプデータをテキストにする

上で取得したダンプデータの中身はXMLであり、かつWiki記法なので、これを単純なテキストにする。
WikiExtractorというツールを使う。
python3 WikiExtractor.py -o <OUTPUT_DIRECTORY> <INPUT_FILE> という使い方。解凍不要で、bz2ファイルのまま使える。
けっこう時間がかかる。

git clone https://github.com/attardi/wikiextractor.git
cd wikiextractor
python3 WikiExtractor.py -o ~/Downloads/wikidata ~/Downloads/wikipedia.bz2

これで ~/Downloads/wikidata にテキストができるが、1MBごとにファイルに分割されている。これを1つのファイルにまとめる。

cd ~/Downloads/wikidata
find . -name "wiki_*" | xargs cat > ~/Downloads/wikidata_all_tmp.txt

また、WikiExtractorで変換したファイルには

<doc id="5" url="https://ja.wikipedia.org/wiki?curid=5" title="アンパサンド">
...
</doc>

のようなタグが残っているので、これを削除する。

grep -v '^<doc id.*>$' ~/Downloads/wikidata_all_tmp.txt | grep -v '^</doc>$' > ~/Downloads/wikidata_all.txt

(参考) ダンプデータとテキストの確認

上で取得したダンプデータは bzip2 -d wikipedia.bz2 で解凍して中身を見れる。こんな感じ。

  <page>
    <title>アンパサンド</title>
    <ns>0</ns>
    <id>5</id>
    <revision>
      <id>71254632</id>
      <parentid>70334050</parentid>
      <timestamp>2019-01-10T10:45:42Z</timestamp>
      <contributor>
        <username>タバコはマーダー</username>
        <id>631644</id>
      </contributor>
      <minor />
      <comment>曖昧さ回避の体裁</comment>
      <model>wikitext</model>
      <format>text/x-wiki</format>
      <text xml:space="preserve">{{redirect|&}}
{{WikipediaPage|「アンパサンド (&)」の使用|WP:JPE#具体例による説明}}
{{記号文字|&amp;}}
{{複数の問題|出典の明記=2018年10月8日 (月) 14:50 (UTC)|独自研究=2018年10月8日 (月) 14:50 (UTC)}}
[[Image:Trebuchet MS ampersand.svg|right|thumb|100px|[[Trebuchet MS]] フォント]]
'''アンパサンド''' ('''&amp;'''、英語名:{{lang|en|ampersand}}) とは並立助詞「…と…」を意味する[[記号]]である。[[ラテン語]]の {{lang|la|"et"}} の[[合字]]で、[[Trebuchet MS]]フォントでは、[[
ファイル:Trebuchet MS ampersand.svg|10px]]と表示され "et" の合字であることが容易にわかる。ampersa、すなわち "and per se and"、その意味は"and [the symbol which] by itself [is] an
d"である。

WikiExtractorによってテキストにすると、この部分は次のようになる。

<doc id="5" url="https://ja.wikipedia.org/wiki?curid=5" title="アンパサンド">
アンパサンド

アンパサンド (&、英語名:) とは並立助詞「…と…」を意味する記号である。ラテン語の の合字で、Trebuchet MSフォントでは、と表示され "et" の合字であることが容易にわかる。ampersa、すなわち "and per se and"、その意味は"and [the symbol which] by itself [is] and"である。

分かち書きにする

Word2Vecを使うには単語に分割されてないといけない。
英語は空白などで分割されているのですぐにわかるが、日本語はそのままだと分割されていないので、分かち書きにする作業が必要。

分かち書きにするためのライブラリとしては形態素解析器のMeCabを使う。
他にはJUMAN++Janomeというのもあるらしい。

また、標準の辞書だと新語に弱い。1語として扱ってほしい語を複数単語にされたりする。これを避けるためにNEologdという辞書を使う。
この辞書によって結果がどう変わるかはNElogdのREADMEに例が書いてあるのでそれを見れば一発でわかる。

インストール

NEologdのREADMEに書いてある通りにやる。

sudo apt update
sudo apt install mecab libmecab-dev mecab-ipadic-utf8 git make curl xz-utils file
git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git
cd mecab-ipadic-neologd
./bin/install-mecab-ipadic-neologd -n
# yes とタイプする

これでインストールが完了する。

辞書のパスは以下でわかる。

echo `mecab-config --dicdir`"/mecab-ipadic-neologd"

自分の環境では /usr/lib/mecab/dic/mecab-ipadic-neologd だった。

標準の辞書とNEologdの辞書で形態素解析すると、それぞれこんな感じになる。

$ echo 令和元年 | mecab
令   名詞,一般,*,*,*,*,令,リョウ,リョー
和   名詞,一般,*,*,*,*,和,ワ,ワ
元年  名詞,副詞可能,*,*,*,*,元年,ガンネン,ガンネン
EOS

$ echo 令和元年 | mecab -d /usr/lib/mecab/dic/mecab-ipadic-neologd
令和元年    名詞,固有名詞,一般,*,*,*,2019年,レイワガンネン,レイワガンネン
EOS

Wikipediaのテキストを分かち書きにする

これも時間がかかる。

mecab ~/Downloads/wikidata_all.txt -d /usr/lib/mecab/dic/mecab-ipadic-neologd -Owakati -o ~/Downloads/wikidata_all.wakati.txt -b 16384

オプションの意味は以下。
+ -d
使用する辞書を指定。上で調べたパスを入れる。
+ -Owakati
分かち書きにする。これを指定しない場合、普通に形態素解析される。
+ -o
出力ファイルを指定。
+ -b
バッファサイズを指定。

なお最後の -b 16384

input-buffer overflow. The line is split. use -b #SIZE option.

というエラーが途中で出たので追加した。デフォルトでは8192(mecab -hでわかる)。

変換後はバイナリファイルになるという話だったが、大丈夫そう。

$ file wikidata_all.wakati.txt
wikidata_all.wakati.txt: UTF-8 Unicode text, with very long lines

(参考) 分かち書きしたファイル

こんな感じになる。

アンパサンド

アンパサンド (&、 英語名 :) と は 並立 助詞 「 … と … 」 を 意味 する 記号 で ある 。 ラテン語 の の 合字 で 、 Trebuchet MS フォント で は 、 と 表示 さ れ " et " の 合字 で ある こと が 容易 に わかる 。 ampersa 、 すなわち " and per se and "、 その 意味 は " and [ the symbol which ] by itself [ is ] and " で ある 。

Word2Vecにかける

ここまでで分かち書きテキストを用意できたので、ようやく本題。

gensimというライブラリを使う。

gensimのインストール

cythonも入れた方が早いらしいので入れる。

sudo pip3 install --upgrade gensim cython

モデルの作成

wikidata_all.wakati.txt があるディレクトリに移動して python3 を実行し、そのままREPLで以下を全部実行するのが手軽。
時間がかかるので注意。

from gensim.models import Word2Vec
from gensim.models.word2vec import LineSentence
import logging
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

sentences = LineSentence('./wikidata_all.wakati.txt')
model = Word2Vec(sentences, size=200, window=20, min_count=5, workers=4)
model.save('./wiki_word2vec.model')

wiki_word2vec.model というファイルが保存される。
これがWikipediaのWord2Vecモデル。

ハイパーパラメータの設定について

モデル作成時に一番重要なのは以下の部分。値は調整必要。他にも指定したいパラメータがあればここを見ながらやる。

model = Word2Vec(sentences, size=200, window=20, min_count=5, workers=4)
  • size
    単語ベクトルの次元の数。
  • window
    この単語数だけ、前後の単語を見る。
  • min_count
    テキスト中に現れる回数がこの数未満の単語は無視。
  • workers
    訓練時に使うスレッド数。

使ってみる

モデルの読み込み

いろいろ試す前に、まずモデルを読み込んでおく必要がある。

(REPLでモデルの作成をした直後は、既にメモリ上にモデルが載っているので、改めて読み込まなくていい。)

from gensim.models import Word2Vec

model = Word2Vec.load('wiki_word2vec.model')
wv = model.wv

なおwvのクラスは以下になる。このクラスのドキュメントに使い方が載っている。

>>> type(wv)
<class 'gensim.models.keyedvectors.Word2VecKeyedVectors'>

似た単語の表示

wv.similar_by_word('foo')

でわかる。

>>> wv.similar_by_word('猫')
[('ネコ', 0.7882304787635803), ('ウサギ', 0.7559791207313538), ('子猫', 0.7516933679580688), ('黒猫', 0.7485260367393494), ('野良猫', 0.7470539808273315), ('犬', 0.7432234883308411), ('飼い猫', 0.7408008575439453), ('愛猫', 0.7271249890327454), ('柴犬', 0.6979021430015564), ('飼い主', 0.6935858726501465)]

>>> wv.similar_by_word('犬')
[('猟犬', 0.7577184438705444), ('猫', 0.7432234883308411), ('犬種', 0.7246831059455872), ('飼い主', 0.7098195552825928), ('牧羊犬', 0.7084189653396606), ('仔犬', 0.7059577703475952), ('番犬', 0.6994990110397339), ('プードル', 0.6932131052017212), ('子犬', 0.6915150880813599), ('柴犬', 0.688454270362854)]

>>> wv.similar_by_word('令和')
[('新元号', 0.8433001041412354), ('平成最後', 0.6003873348236084), ('生前退位', 0.5694811940193176), ('令和元年', 0.5659081935882568), ('天皇誕生日', 0.5473066568374634), ('建国記念の日', 0.5416209697723389), ('改元', 0.5325936079025269), ('退位特例法', 0.5113210678100586), ('即位礼正殿の儀', 0.5051107406616211), ('明治節', 0.49450594186782837)]

>>> wv.similar_by_word('Apple')
[('アップル', 0.7637709975242615), ('iPhone', 0.6721816062927246), ('Macintosh', 0.6700955033302307), ('VisiCalc', 0.6614382863044739), ('iMac', 0.6558083295822144), ('アップルコンピュータ', 0.6533167958259583), ('スティーブ・ジョブズ', 0.6456233263015747), ('Xerox', 0.619143009185791), ('Apple社', 0.614047110080719), ('iPad', 0.6136564016342163)]


>>> wv.similar_by_word('リンゴ')
[('サクランボ', 0.6949737668037415), ('イチゴ', 0.6846523284912109), ('りんご', 0.6731216907501221), ('果物', 0.6545817852020264), ('トマト', 0.6475458145141602), ('シードル', 0.6408026218414307), ('プルーン', 0.6374067664146423), ('レタス', 0.6371461153030396), ('ジャガイモ', 0.6367175579071045), ('ピーナッツ', 0.6305103898048401)]

2語の関連度

wv.similarity('foo', 'bar')

でわかる。

>>> wv.similarity('猫', '犬')
0.74322355

>>> wv.similarity('猫', '夏目漱石')
0.19329126

>>> wv.similarity('吾輩は猫である', '夏目漱石')
0.73291266

単語の和・差

wv.most_similar(positive=['foo', 'bar'], negative=['baz'])

これで foo + bar – baz がわかる。

>>> wv.most_similar(positive=['王様', '女'], negative=['男'])
[('お姫様', 0.6474927663803101), ('王女', 0.601335346698761), ('白雪姫', 0.5954098701477051), ('シンデレラ', 0.589481770992279), ('貴婦人', 0.5760519504547119), ('女王', 0.5691637992858887), ('妖精', 0.5630465149879456), ('花嫁', 0.5587092638015747), ('魔女', 0.5510756969451904), ('オーベロン', 0.5459994077682495)]

>>> wv.most_similar(positive=['日本', '北京'], negative=['東京都'])
[('中国', 0.6868176460266113), ('上海', 0.6344862580299377), ('天津', 0.566441535949707), ('香港', 0.5649573802947998), ('中国本土', 0.563534677028656), ('広州', 0.549053430557251), ('台湾', 0.5482343435287476), ('中華', 0.5481294393539429), ('厦門', 0.547395646572113), ('中華圏', 0.5437194108963013)]

感想

やってみるだけならわりと簡単にできた。
でも wv.similar_by_word() の精度は良くない感じ。エポック数を増やしたり、前処理を厳密にやらないといけない。
wv.most_similar() は想定通りの結果になった。

参考

WikipediaデータをWord2Vecする件

WikiExtractor

MeCab

NEologd

gensim

ローカルに作ったgitリポジトリをGithubに上げる

ローカルでgitリポジトリを作って何度かコミットし、それをGithubに上げた。
このときcontributionsがどうなるかわからなかったが、結局ちゃんとコミットの日が緑になった。

方法

  1. Githubに新しいリポジトリを作る
    普通に”New repository”で作成。
    このときREADMEや.gitignore、LICENSEも作れるが、既存のリポジトリと衝突するのが怖いので作らなかった。
  2. コマンドを打つ
    上でリポジトリ作ると、その次の画面で注意書きが出る。
    その中に既存リポジトリをpushする方法が書いてある。その通りやればいい。

    Quick_setup

    git remote add origin https://github.com/<USERNAME>/<REPOSITORY_NAME>.git
    git push -u origin master
    
  3. Githubを見て、うまくpushされたか確認する
    contributionsもちゃんと緑になっていた。実はこれが一番心配だったのでほっとした。
    “Created 1 repository”は今日の日付だが、過去のコミットもcontributionsとして扱われているようだ。

その他の方法

Githubの右上の「+」を押すと”Import repository”があり、ここから既存のリポジトリ(gitでもsvnでも)をインポートできるよう。
だがここではURLを指定しないといけない。今回はローカルのリポジトリだったのでできなかった。

Import repository

参考

radiko録音スクリプトのdocker化

radikoを録音するシェルスクリプトmatchy2/rec_radiko.shをdockerで動かすようにした。他の方のスクリプトを使うだけだが…。

できたもの

docker_rec_radiko

あとはCRONでdocker runすればいい(docker runはradiko_docker_run.shの中でやっている)。
録音時にdocker runするか、それともコンテナは起動しっぱなしにしてその中でCRONで録音するか迷った。(つまりCRONでコンテナを起動するか、コンテナの中でCRONするか。)だが録音しないときはこのコンテナは何もしないので、前者でいいということにした。

作った理由

環境構築が面倒だから。

関東を離れてもTBSラジオなどが聞きたいので、ConoHaで東京リージョンのVPSを借り、そこでこのシェルスクリプトを動かすようにした。スクリプトではffmpegなど必要となるツールがあるのでそれらをインストールしないといけない。VPSではいろいろなOSの中からCentOSを(サーバならCentOSだろという固定観念から)選んだが、CentOSだとこのインストール作業がやや面倒なのでdockerにした。

radikoのプレミアム会員になれば全国どこの局でも聞けるというのは知ってる。借りたサーバでは他にも何かするつもり。

もろもろ

  • 時刻設定が面倒だった
    はじめubuntu:16.04のdockerイメージを使っていたが環境変数 TZAsia/Tokyo にしてもJSTにならないようだった。他にも設定の方法はあるのだろうが、ubuntuはやめてdebianのイメージを使うことにした。debianだとこれでいける。
  • dockerでのマウント方法
    新しい指定方法ができていた(docker 17.06からなので新しくもない…知らなかっただけ)。
    ホストのディレクトリ[ファイル]をコンテナのディレクトリ[ファイル]にマウントさせたいとき、以下が同じ意味になる。

    # -v で指定する方法
    $ docker run -v <PATH_IN_HOST>:<PATH_IN_CONTAINER> <IMAGE_NAME>
    # --mount で指定する方法
    $ docker run --mount type=bind,source=<PATH_IN_HOST>,target=<PATH_IN_CONTAINER> <IMAGE_NAME>
    

    -v では、コロンの前と後どっちがホストのパスだっけと迷うことがよくあった。 --mount だといちいちsourceなどと書くのでわかりやすい。
    またvolumeのマウントでも、 -v だとホストディレクトリ[ファイル]のマウントと区別しにくかったが、 --mount ではtypeが変わるのでわかりやすい。

    # -v で指定する方法
    $ docker run -v <VOLUME_NAME>:<PATH_IN_CONTAINER> <IMAGE_NAME>
    # --mount で指定する方法
    $ docker run --mount type=volume,source=<VOLUME_NAME>,target=<PATH_IN_CONTAINER> <IMAGE_NAME>
    

参考