MeCab+NEologdを使って日本語を分かち書きにするツールを作った

Word2VecとかするときにMeCab + NEologdを使って分かち書きする。だがインストールがやや面倒。Dockerを使って、手軽に分かち書きするツールを作った。

やりたいこと

  1. MeCab + NEologdで分かち書きする
  2. ディレクトリ構造を保つ
    Word2Vecで全テキストを1つにまとめてしまうときは問題ないが、Doc2Vecとかするときには1ファイルごとに分かち書きしたい。そのときディレクトリ構造を変えられると面倒なので、同じになるようにする。
    たとえば以下のような感じ。inputに分かち書きしたいファイルをディレクトリごと入れたら、その構造を保ったままoutputに結果が出る。

    ├── input
    │   ├── 1
    │   │   ├── 11
    │   │   │   └── c.txt
    │   │   └── b.txt
    │   ├── 2
    │   │   └── d.txt
    │   └── a.txt
    └── output
       ├── 1
       │   ├── 11
       │   │   └── c.txt
       │   └── b.txt
       ├── 2
       │   └── d.txt
       └── a.txt
    

できたもの

sankaku/docker-mecab

実行例

./mount/input に上のツリー構造の通りファイルを入れる。中身はすべて同じで、NEologdにある例と同じにした。
そしてDockerイメージを作成し、 ./split.sh を実行。

入力

$ for f in `ls mount/input/**/*.txt`; do echo $f; cat $f; done
mount/input/1/11/c.txt
8月3日に放送された「中居正広の金曜日のスマイルたちへ」(TBS系)で、1日たった5分でぽっこりおなかを解消するというダイエット方法を紹介。キンタロー。のダイエットにも密着。
mount/input/1/b.txt
8月3日に放送された「中居正広の金曜日のスマイルたちへ」(TBS系)で、1日たった5分でぽっこりおなかを解消するというダイエット方法を紹介。キンタロー。のダイエットにも密着。
mount/input/2/d.txt
8月3日に放送された「中居正広の金曜日のスマイルたちへ」(TBS系)で、1日たった5分でぽっこりおなかを解消するというダイエット方法を紹介。キンタロー。のダイエットにも密着。
mount/input/a.txt
8月3日に放送された「中居正広の金曜日のスマイルたちへ」(TBS系)で、1日たった5分でぽっこりおなかを解消するというダイエット方法を紹介。キンタロー。のダイエットにも密着。

出力

$ for f in `ls mount/output/**/*.txt`; do echo $f; cat $f; done
mount/output/1/11/c.txt
8月3日 に 放送 さ れ た 「 中居正広の金曜日のスマイルたちへ 」( TBS 系 ) で 、 1日 たった 5分 で ぽっこり おなか を 解消 する という ダイエット方法 を 紹介 。 キンタロー。 の ダイエット に も 密着 。 
mount/output/1/b.txt
8月3日 に 放送 さ れ た 「 中居正広の金曜日のスマイルたちへ 」( TBS 系 ) で 、 1日 たった 5分 で ぽっこり おなか を 解消 する という ダイエット方法 を 紹介 。 キンタロー。 の ダイエット に も 密着 。 
mount/output/2/d.txt
8月3日 に 放送 さ れ た 「 中居正広の金曜日のスマイルたちへ 」( TBS 系 ) で 、 1日 たった 5分 で ぽっこり おなか を 解消 する という ダイエット方法 を 紹介 。 キンタロー。 の ダイエット に も 密着 。 
mount/output/a.txt
8月3日 に 放送 さ れ た 「 中居正広の金曜日のスマイルたちへ 」( TBS 系 ) で 、 1日 たった 5分 で ぽっこり おなか を 解消 する という ダイエット方法 を 紹介 。 キンタロー。 の ダイエット に も 密着 。

ちゃんとファイルごとに分かち書きできてる。
ディレクトリ構造もそのまま。


$ tree mount/ mount/ ├── input │   ├── 1 │   │   ├── 11 │   │   │   └── c.txt │   │   └── b.txt │   ├── 2 │   │   └── d.txt │   └── a.txt └── output ├── 1 │   ├── 11 │   │   └── c.txt │   └── b.txt ├── 2 │   └── d.txt └── a.txt

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