pythonのgensimライブラリを利用して日本語wikipediaの全文からword2vecを学習させるまでの全手順

Word2Vecを計算するまでの全手順を書いておこうと思います。

ご存知の方も多いと思いますが、Word2Vecは、単語をvector化して扱う技術です。
以下の解説がわかりやすいと思います。
Vector Representations of Words  |  TensorFlow

同じような使われ方をする単語同士が近くなります。
例えば、「夏」という単語の近くには、「冬」が配置されます。
さらに、vector化することで、単語の足し引きができるようになります。
例えば、「叔母」ー「女」+「男」=「叔父」となります。

前置きは、これくらいにして、実際に計算するまでの手順に移りたいと思います。

Word2Vecを計算するまでの概要

学習に利用する文章の収集

  • 今回は、wikipediaの全文から学習させます。

フィルタリング

  • wikipediaには、学習には必要のないXMLのコンテナ部分や記号(wiki記法)などが含まれているため削除します。

わかち書き

  • 文章のままでは学習できないので、単語に分割します。

Word2Vecのモデルの学習

  • Pythonのgensimライブラリを利用して学習させます。

手順の詳細

学習に利用する文章の収集

  1. wikipediaの全文(bz2形式)をダウンロードします。
  2. w2vの解析に必要な日本語の部分を取得する。
    • 今回は、日本語からword2vecを学習させるため、wiki記法など通常の日本語には現れない記号などは削除しておいた方が無難です。
    • また、上記のwikipediaのdumpはXML形式で、各ページは、pageタグで区切られています。
    • 例えば、以下のように、pageタグで囲まれた範囲を切り出し、数字を除いた非ascii文字を取り出すと、それっぽい日本語文が得られます。
#!/usr/bin/env python
# -*- coding: utf-8 -*-

#ja.wikipediaのdumpから<page>...</page>の部分の非ascii文字(日本語を期待)を切り出しファイルへ出力する。

import string
import bz2
import sys
import os
import re

def simple_ja_filter(ifn,ofn):
    try :
        MAX_FILE_SIZE = 64 * 1024 * 1024; #1ファイルが大体、数十Mbyteくらいになるようにしておく 
        TEXT_NUM_IN_A_ZIPFILE = 10000;
        table = string.maketrans(
            "".join(map (lambda x : chr(x), #(文字コードから文字へ変換する.)
                         filter (lambda x : x not in range(48,58),#数字(0-9)は除いて、
                                 [i for i in range(32,127)]))), #print可能なascii文字は、
            "".join(chr(127) for i in range(127 - 32 - 10)) #削除する (注:正しくは、del文字に置き換え)
        )

        with bz2.BZ2File(ifn) as bz2_f :
            buf = "";
            is_in_page = False;
            cnt = 0;
            of = None;

            for ln in bz2_f: #1行読み出し
                ln = ln.strip();
                
                if cnt % TEXT_NUM_IN_A_ZIPFILE == 0 or of != None and of.tell() > MAX_FILE_SIZE:
                    if of != None:
                        of.close();

                    of = open(
                        ofn + "_" + str(cnt / TEXT_NUM_IN_A_ZIPFILE + 1).zfill(4) + ".txt",
                        "w"
                    )
                    
                if ln == "<page>": #<page>タグを見つける
                    buf = ""
                    is_in_page = True;
                    continue;
                
                if ln == "</page>": #</page>までを1ページとして扱う
                    is_in_page = False;
                    if len(buf) != 0:
                        of.write(buf)
                    cnt = cnt + 1;
                    
                    if cnt % 10000 == 0: #どれくらい進んだかを出力する
                        print str(cnt) + " articles done";
                    continue;

                if is_in_page: #数字を除いてascii文字を削除する
                    non_ascii = ln.translate(table).replace(chr(127),"");

                    #空の行、数字のみの行は省く
                    if len(non_ascii) != 0 and re.match(r"^[0-9]+$",non_ascii) == None:
                        buf += non_ascii + os.linesep
                    continue;                
                        
            if of != None: #最後に残っている内容を書き出し
                of.close();
    except Exception as e:
        print e

if __name__ == '__main__':
    if len(sys.argv) < 3:
        print ("usage : [wiki dump file name(bz2)] [output file prefix]")
        sys.exit();

    ja_wiki_dump_fn = sys.argv[1]
    out_zip_fn = sys.argv[2]
        
    simple_ja_filter(ja_wiki_dump_fn,out_zip_fn)

分かち書き

  • MeCabを利用して単語に分割します。
    • MeCabのインストールなどは、以前の記事の中に記載しています。
    • 今回は、名詞と動詞、形容詞、形容動詞、副詞だけ取り出して学習させてみることにします。
    • 例えば、以下のコマンドを実行するとモデルの学習に利用する単語列が得られます(mecabに100Mbyte弱割り当てています)。
cat [入力となるテキストファイル名] | mecab -b 100000000 | grep -e 名詞 -e 動詞 -e 形容詞 -e 形容動詞 -e 副詞 | grev -v 助動詞 | cut -f 1  > [結果の出力先]
  • bashなどで、for文を回して、ファイルに出力するのが手っ取り早いと思います。
    • こんな感じ。for i in {1..214}; do time cat jawiki_`printf %04d $i`.txt | mecab -b 100000000 | grep -e 名詞 -e 動詞 -e 形容詞 -e 形容動詞 -e 副詞 | grep -v 助動詞 | cut -f 1 > jawiki_`printf %04d $i`.wl; done

Word2Vecのモデルの学習

  • Gensimのmodelにword2vecもあるのでそれを利用します。
  • 以下のプログラムを走らせると学習し、モデルが得られます。
    • (長いので本文の最後においておきます)
  • ただ、上記のプログラムは大量のメモリを食います。wikipedia全文だと数十Gbyteほど。ですので、AWSのEC2(r4.4xlarge = 122Gbyteのメモリ)を利用します。
  • word2vecの学習のプログラムは、以下の通りです。
    • プログラムのインプットは、分かち書きして得られた複数の文書をzipで固めたものです。
      • zipで固めずに、1つづつファイルを読み込んでも良いのですが、容量を食うのでまとめてしまいました(この辺は好みの問題かと思います)。
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import pickle
import zipfile
import collections

from gensim.models import Word2Vec

"""
<<入力(引数)>>
引数は、zipfileの名前を期待する。

zipfileの構造は以下を期待する
archive内の各memberは、単語(word)が改行(\n)で区切られている

例:
 archiveの中身(member)は以下の様になっており
  jawiki_word_list_0001
  jawiki_word_list_0002
  ...

 各memberの中身は以下の様担っている
 word_1
 word_2
 word_3
 ...

<<出力>>
wordのリスト
[word,word,word...]

"""
def read_word_list_from_zip(filename):
    try : 
        word_list = []
        with zipfile.ZipFile(filename) as archive:
            for name in archive.namelist():
                print name
                with zipfile.ZipFile.open(archive,name) as zf:
                    for word in zf:
                        word_list.append(word.strip())
        return word_list
    except IOError as ioe:
        print ioe; #File Open失敗

"""
利用する単語の集合を規定する。

今回は、word_listに登場するwordの内、上からvocabulary_size分を利用する単語とする。
(全部利用するならこの処理は要らない)
"""
def build_dic(word_list,vocabulary_size):
    #vocabulary size分の単語を選定する
    counter = collections.Counter(word_list);
    count = [['UNK',-1]]; #'UNK'は、UNKnown wordsで、未知語
    count.extend(counter.most_common(vocabulary_size - 1)); #-1するのは、'UNK'の文

    #辞書の作成
    dictionary = dict();
    for word,_ in count:
        dictionary[word] = len(dictionary);

    #今回は使いませんが、逆引き辞書を作る場合
    #reverse_dictionary = dict(zip(dictionary.values(),dictionary.keys()));
    
    return dictionary

"""
word_list (単語の1次元配列)から利用する単語のみを抜粋し、
sentences (単語の2次元配列)に変換する
"""
def wordlist2sentences(word_list,dic):
    MAX_SENTENCE_LEN = 30;    
    sentences = [];
    sentence = [];
    cnt = 0;
    
    for word in filter(lambda word : word in dic, word_list):
        cnt = cnt + 1;
        if cnt % MAX_SENTENCE_LEN == 0:
            sentences.append(sentence);
            sentence = [];
        sentence.append(word);
    if len(sentence) > 0:
        sentences.append(sentence);

    return sentences;
        
def w2v_learn(sentences,size,min_count):
    model = Word2Vec(
        sentences,
        size=size,
        window=5,
        min_count=min_count,
        workers=4)
    return model;

def output_model_and_dic(w2v_model,fn,size,min_count,dic):
    output_file_name = os.path.basename(fn) + "_" + \
                       str(size) + "_" + \
                       str(min_count) + ".model"

    w2v_model.save_word2vec_format(output_file_name,binary=True)

    '''
    dic_file_name = os.path.basename(zip_file) + ".dic"
    with open(dic_file_name, 'w') as f:
        pickle.dump(dic,f)
    '''

def main(zip_file_name):
    #データの読み出し
    word_list = read_word_list_from_zip(zip_file_name);
    print ("reading done");

    #利用する単語の選定
    dic = build_dic(word_list,100000);
    print ("build dic done")

    #Word2Vec様のインプット生成
    sentences = wordlist2sentences(word_list,dic);

    #Word2Vecのモデルの学習
    print ("start learning")
    W2V_SIZE = 200;
    W2V_MIN_COUNT = 3;
    w2v_model = w2v_learn(sentences,W2V_SIZE,W2V_MIN_COUNT);
    print ("learning done")

    #書き出し
    output_model_and_dic(w2v_model,zip_file_name,W2V_SIZE,W2V_MIN_COUNT,dic);
            
if __name__ == '__main__':
    if len(sys.argv) < 2:
        print ("usage : [zip file]")
        sys.exit();
    zip_file_name = sys.argv[1]

    main(zip_file_name);
EC2でのgensimのインストール
  • Gensimがまだ入っていない人は以下の手順でインストールしてください。
  1. pip自身をアップデートします。
    • sudo pip install --upgrade pip
  2. gcc (g++)をインストールします。
  3. gensimをインストールします。
    • sudo pip install gensim
  • pipが見当たらないという人は、which pip とかで探してみましょう。それでもなければそもそも入ってない様ですので、sudo yum install python-pipを試してみましょう。

遊んでみる

  • word2vecを計算するコードは以下になります。
    • 「夏」に一番近い単語だと、「春」になりました。
    • 「叔母」 - 「女」 + 「男」=「叔父」となっているので、いい感じですね。
#!/usr/bin/env python
# coding: utf-8

from gensim.models import Word2Vec
import json

model_file_name = "jawiki_wl_all.zip_200_1.model"

model = Word2Vec.load_word2vec_format(model_file_name, binary=True)

def most_similar(pos_list,neg_list):
    try :
        return map(lambda t: (t[0].encode('utf-8'),t[1]),
                   model.most_similar(
                       map(lambda w : unicode(w,'utf-8'),pos_list),
                       map(lambda w : unicode(w,'utf-8'),neg_list),
                       topn=10));
    except Exception as e:
        print (str(e))

#'夏'に近い単語を上位10個出力する
print "「夏」に一番近いのは… "
for t in most_similar(['夏'],[]):
    print t[0], t[1]

print ""

print "「叔母」- 「女」+ 「男」 = ?"
for t in most_similar(['叔母','男'],['女']):
    print t[0], t[1]    

あとがき

  • 今回、名詞、動詞や形容詞も放り込みましたが、利用する単語の取捨選択は、要調整です。
  • 単語は、原型の方を選択して、学習させた方が上手くいくと思います。