pythonのgensimライブラリを利用して日本語wikipediaの全文からword2vecを学習させるまでの全手順
Word2Vecを計算するまでの全手順を書いておこうと思います。
ご存知の方も多いと思いますが、Word2Vecは、単語をvector化して扱う技術です。
以下の解説がわかりやすいと思います。
Vector Representations of Words | TensorFlow
同じような使われ方をする単語同士が近くなります。
例えば、「夏」という単語の近くには、「冬」が配置されます。
さらに、vector化することで、単語の足し引きができるようになります。
例えば、「叔母」ー「女」+「男」=「叔父」となります。
前置きは、これくらいにして、実際に計算するまでの手順に移りたいと思います。
Word2Vecを計算するまでの概要
学習に利用する文章の収集
- 今回は、wikipediaの全文から学習させます。
わかち書き
- 文章のままでは学習できないので、単語に分割します。
Word2Vecのモデルの学習
- Pythonのgensimライブラリを利用して学習させます。
手順の詳細
学習に利用する文章の収集
- wikipediaの全文(bz2形式)をダウンロードします。
- ファイルは以下のリンクから辿った先にあります。
- Wikipedia:データベースダウンロード - Wikipedia
- ファイル名が「jawiki-latest-pages-articles.xml.bz2」が最新のダンプです。
- もしくは、以下のコマンドを実行することで、カレントディレクトリに保存できます。
- ファイルは以下のリンクから辿った先にあります。
- w2vの解析に必要な日本語の部分を取得する。
#!/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を利用して単語に分割します。
cat [入力となるテキストファイル名] | mecab -b 100000000 | grep -e 名詞 -e 動詞 -e 形容詞 -e 形容動詞 -e 副詞 | grev -v 助動詞 | cut -f 1 > [結果の出力先]
Word2Vecのモデルの学習
- Gensimのmodelにword2vecもあるのでそれを利用します。
- 以下のプログラムを走らせると学習し、モデルが得られます。
- (長いので本文の最後においておきます)
- ただ、上記のプログラムは大量のメモリを食います。wikipedia全文だと数十Gbyteほど。ですので、AWSのEC2(r4.4xlarge = 122Gbyteのメモリ)を利用します。
- word2vecの学習のプログラムは、以下の通りです。
- プログラムのインプットは、分かち書きして得られた複数の文書をzipで固めたものです。
- zipで固めずに、1つづつファイルを読み込んでも良いのですが、容量を食うのでまとめてしまいました(この辺は好みの問題かと思います)。
- プログラムのインプットは、分かち書きして得られた複数の文書をzipで固めたものです。
#!/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);
遊んでみる
- 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]
あとがき
- 今回、名詞、動詞や形容詞も放り込みましたが、利用する単語の取捨選択は、要調整です。
- 単語は、原型の方を選択して、学習させた方が上手くいくと思います。