どん底から這い上がるまでの記録

どん底から這い上がりたいけど這い上がれない人がいろいろ書くブログ(主にプログラミング)

gensimを使ってWikipediaの全日本語記事からWord2Vecを作る

 

自分でカスタマイズしたMeCabの辞書を使ってWikipediaの記事を形態素解析しWord2Vecを作りたくなったので、やってみました。

本記事ではまず、Wikiextractorを使ってWikipediaの日本語記事から本文を抽出し、形態素解析したのちにGoogle Colaboratory上でWord2Vecを学習させます。

 

 

環境

私はWindows 10で作業していますがLinuxでもMacでも同じようにできると思います。今回使うライブラリでインストールに手間取りそうなものはMeCabくらいですね。

ほかはpip installで楽にインストールできると思います。

Wikipediaの全日本語記事の用意

こちらからjawiki-latest-pages-articles.xml.bz2をダウンロードします。

ダウンロードには少々時間がかかります。

WikiExtractorで抽出からのBeautifulSoup、そして形態素解析

こちらからWikiExtractor.pyjawiki-latest-pages-articles.xml.bz2があるディレクトリにコピペし以下のコマンドを実行する。

python WikiExtractor.py jawiki-latest-pages-articles.xml.bz2 -b 10M -o articles

ここでやっていることは抽出した記事を1つのファイルに10Mに分けて出力。出力先にはarticlesというフォルダが作成される。さらにその中にはAA, AB, ACというフォルダが作成され抽出された記事の中身がファイルに書き込まれています。

作業ディレクトリの中身

WikiExtractor.py
articles
  AA
    wiki_00 ~ wiki_99
  AB
    wiki_00 ~ wiki_99
  AC
    wiki_00 ~ wiki_71

私のPCの場合抽出作業には約2時間かかり生成されたファイルのサイズはおよそ2.6GBになりました。

抽出されたファイルは以下のようにdoc要素の集まりになっています。

<doc id="0" url="https://ja.wikipedia.org/wiki?curid=0" title="社会">
タイトル
本文
</doc>
<doc id="1" url="https://ja.wikipedia.org/wiki?curid=1" title="生物学">
タイトル
本文
</doc>

ここで、不必要なdocタグとタイトルを取り除きます。

BeautifulSoupを使って抽出したdoc要素から本文のみを取り出してから、形態素解析しファイルに書き込んでいきます。

コードにするとこんな感じ。

import MeCab
import codecs
import glob
from bs4 import BeautifulSoup
from datetime import datetime

tagger = MeCab.Tagger('-Owakati')

def save(src):
    with codecs.open(src, 'r', 'utf-8') as f:
        soup = BeautifulSoup(f.read(), "lxml")
        doc_tags = soup.find_all("doc")
        sentences = []
        dst = "contents/wiki{}".format(len(glob.glob("contents/wiki*"))+1)
        for doc in doc_tags:
            content = "".join(doc.text.splitlines()[3:])
            content = " ".join(tagger.parse(content).split())
            sentences.append(content)
        print(*sentences, sep="\n", file=codecs.open(dst, 'w', 'utf-8'))


# 1. globを使ってarticlesにある3つのフォルダのパスを取得。
dirlist = glob.glob("articles/*")
# dirlist = ["articles/AA", "articles/AB", "articles/AC"]
for dirname in dirlist:
    # 2. AA, AB, ACの中の全てのファイルのパスを取得。
    for src in glob.glob(dirname+"/*"):
        # 3. ファイルをひとつずつ処理していく。
        # src = "articles/AA/wiki_00"
        print("{}:\t{}".format(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), src))
        save(src)

このコードでやっていることはこんな感じ。

  1. globを使ってarticlesにある3つのフォルダのパスを取得。
  2. 3つのフォルダAA, AB, ACのなかの全てのファイルのパスを取得。
  3. ファイルをひとつずつ処理していく。(本文を抽出ー>形態素解析ー>contentsフォルダにファイルを保存。)

Google Colaboratoryを使います。

これからやろうとすることはGoogle Colaboratoryを使わなくてもできると思います。ですが、個人的な理由によりGoogle Colaboratoryを使って作業を進めていきます。

contentsフォルダに形態素解析済みのファイルを用意できたので、これからWord2Vecの学習を進めていきますが、ここで大きな問題があるのですがとりあえずこのまま進めます。(※今からやることはGoogle Colaboratoryで試してみてください。)

まずはGoogle Colaboratoryを開き、さきほど用意した形態素解析済みのファイルが入ったcontentsフォルダをGoogle Driveにアップロードします。

Google Driveにあるファイルを読み込むには以下のようにマウントする必要があります。セルを実行するには▷をクリックするか、shift-EnterでOkです。

f:id:pytry3g:20190304075842p:plain

 

作業ディレクトリに移動。cd drive/My \Drive/

f:id:pytry3g:20190304080929p:plain

 

ちなみに、私はGoogle Driveの一番上のマイドライブで作業をしています。

f:id:pytry3g:20190304080815p:plain

必要なライブラリのインポート。

import codecs
import glob

 

それでは用意したファイルを使ってWord2Vecを学習させていきたいと思います。

gensimのWord2Vecには学習データを以下の形にして渡します。

[
['単語', '単語', '単語'],
['単語', '単語', '単語', '単語']
]

リストの中に単語が入ったリストの状態になっているので、コードにするとこんな感じになります。

sentences = []
for fname in glob.glob("contents/wiki*"):
    for content in codecs.open(fname, 'r', 'utf-8').read().splitlines():
        sentences.append([token for token in content.split()])

このコードをセルにコピペして実行すると、ようやくWikipediaの記事をWord2Vecで学習することが、できません。

もう一度言いますが、できません。\(^o^)/

メモリ不足が原因でセッションがクラッシュしますよね?

そりゃメモリ不足にもなりますよね。下のような大量のテキストをリストにぶち込もうとしたら。

f:id:pytry3g:20190304082437p:plain

ということで、Wikipediaの全日本語記事からWord2Vecを作ることはできませんでした(^^

 

 

はい、冗談です。

じゃあ、どうすればメモリ不足という問題を解決できるのか?

自分が導き出した答えは単語辞書を作る、です。(※gensimのText8CorpusLineSentenceを使うとこんなことする必要がないみたいです、、)

Wikipedia全記事に出てくる全ての単語に固有のIDを割り当てます。

例えば、このような辞書があった場合、

{'織田信長': 1, 'は': 2, '尾張': 3, 'の': 4, '大名': 5}

下のリストは

['織田信長', 'は', '尾張', 'の', '大名']

このように単語辞書から単語をIDに変換できメモリ使用量を抑えることができます。

[1, 2, 3, 4, 5]

なので、まずは単語辞書を作ります。引き続きGoogle Colaboratoryを使って作業を進めていきます。

改めて必要なライブラリをインポート。word2sidを単語辞書にする。

import codecs
import glob
import logging
import os
import pickle
from gensim.models.word2vec import Word2Vec
from tqdm import tqdm

word2sid = {"<unk>": "0"} # 単語辞書
files = glob.glob("contents/wiki*")

 

単語辞書の作成。

for fname in tqdm(files):
    for content in codecs.open(fname, 'r', 'utf-8').read().splitlines():
        for token in content.split():
            if token in word2sid:
                continue
            word2sid[token] = str(len(word2sid))

gensimのWord2Vecにはintではなくstrを渡す必要があるので固有のIDをstrに変換。

 

後で使えるようにpickleで辞書を保存。

with open("dictionary.pickle", mode="wb") as f:
    pickle.dump(word2sid, f)

 

Wikipediaの記事を単語辞書をもとに固有のIDに変換。

sentences = []
for fname in tqdm(files):
    for content in codecs.open(fname, 'r', 'utf-8').read().splitlines():
        sentences.append([word2sid[token] for token in content.split()])

 

作業ディレクトリにmodelフォルダを作ってから、パラメータの設定とモデルの保存先を決める。

size = 200
min_count = 20
window = 10
sg = 1
dirname = "size{}-min_count{}-window{}-sg{}".format(size, min_count, window, sg)
if not "model/"+dirname in glob.glob("model/*"):
    os.mkdir("model/"+dirname)

 

学習の開始。(※学習にはめちゃくちゃ時間がかかります。)

logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
# モデルを作る
model = Word2Vec(sentences, size=size, min_count=min_count, window=window, sg=sg, iter=5, workers=10)
"""
size -> 単語ベクトルの次元
min_count -> min_count以下の単語を除外
window -> 文脈の単語数
iter -> 学習の回数(epoch)
"""
# モデルの保存
model.save("model/"+dirname+'/wikipedia.model')

iterを1にすると1時間くらいで学習が完了するかも?

学習したモデルを使ってみる。

学習が完了したので試してみます。

import pickle
from gensim.models.word2vec import Word2Vec


class Wiki2Vec:
    def __init__(self, dic_path, model_path):
        with open(dic_path, mode="rb") as f:
            self.word2sid = pickle.load(f)
        self.sid2word = {v:k for k, v in self.word2sid.items()}
        self.model = Word2Vec.load(model_path)

    def get_vector(self, token):
        # 単語のベクトルを見る
        sid = self.word2sid[token]
        word_vector = self.model.wv[sid]
        print('-- {} --'.format(token))
        print(word_vector)

    def get_similar_tokens(self, token, topn=10):
        # 与えられる単語と似ている単語を見る
        sid = self.word2sid[token]
        similar_tokens = self.model.wv.most_similar(positive=[sid], topn=topn)
        print('-- {} --'.format(token))
        for sid, similarity in similar_tokens:
            token = self.sid2word[sid]
            print("{}: {:.2f}".format(token, similarity))

このコードは単語辞書とWord2Vecのモデルを読み込んで与えられた単語のベクトルの中身を見たり、似ている単語を見ることができます。

以下のコードを実行すると与えた単語と似ている単語を見ることができます。

size = 200
min_count = 20
window = 10
sg = 1

dirname = "size{}-min_count{}-window{}-sg{}".format(size, min_count, window, sg)
dic_path = "dictionary.pickle"
model_path = "model/"+dirname+"/wikipedia.model"
# 辞書とWord2Vecのパスを渡す
wiki2vec = Wiki2Vec(dic_path, model_path)
sample = "本田圭佑"
# sampleと似ている単語を見る
wiki2vec.get_similar_tokens(sample)
"""結果
-- 本田圭佑 --
長谷部誠: 0.81
柿谷曜一朗: 0.79
中澤佑二: 0.79
岡崎慎司: 0.79
吉田麻也: 0.79
宮本恒靖: 0.78
長友佑都: 0.78
安田理大: 0.77
藤田俊哉: 0.77
大迫勇也: 0.77
"""

おわりに

ここまでWikipediaの全日本語記事からWord2Vecのモデルを作って試してみました。

今後このモデルを使ってテキスト分類や他のアプリケーションで使っていきたいと思います。

Q&A

pickleを読み込むには?

今回作った辞書を読み込むにはこうする。

with open("dictionary.pickle", mode="rb") as f:
    word2sid = pickle.load(f)

word2vecを再学習するには?

再学習するには、

  1. モデルを読み込む。
  2. train()で学習。
  3. モデルの保存。
# 1. モデルを読み込む、パスは各自変えてください。
model = Word2Vec.load("model/"+dirname+"/wikipedia.model")
# 2. train()で学習
model.train(sentences, total_examples=len(sentences), epochs=1)
"""
sentences -> リストの中に単語が入ったリストが入っている
total_examples -> sentencesのサイズ
epochs -> 学習の回数
"""
# 3. モデルの保存
model.save("model/"+dirname+'/wikipedia.model')