日本語単語ベクトルchiVeを使ってみた―RNNで文章生成〈5〉

はじめに

前回の記事↓

コーパスの整形とSudachiによる分かち書き―RNNで文章生成〈4〉

前回は前処理の前半の工程として、形態素解析器Sudachiを用いて青空文庫の小説の文章を分かち書きに整形しました。その後の工程で単語を単語ベクトルに変換する必要があります。この変換に使う学習済みのword2vecをどうするか検討中だったのですが、いろいろググって調べた結果chiVeという日本語単語ベクトルが見つかったのでこれを使うことにしました。

chiVeについて

chiVeの入手はこちらのページから行いました。↓

WorksApplications/chiVe (GitHub)

chiVeの説明は以下の通りです。

"chiVe" (sudachi Vector) は、大規模コーパスと複数粒度分割に基づく日本語単語ベクトルです。

Skip-gramアルゴリズムを元に、word2vec (gensim) を使用して単語分散表現を構築しています。

学習コーパスとして約1億のウェブページ文章を含む国立国語研究所の日本語ウェブコーパス(NWJC)を採用し、分かち書きにはワークスアプリケーションズの形態素解析器Sudachiを使用しています。

Sudachiで定義されている短・中・長単位の3つの分割単位でNWJCを解析した結果を元に分散表現の学習を行なっています。Sudachiはversion 0.1.1を使用しています。

出典:WorksApplications/chiVe/README.md

ネット上には他にも学習済みの単語ベクトルがあるのですが、その中でもchiVeを選んだ理由は、形態素解析にSudachiを使っていることと語彙数が豊富なこと、比較的最近に公開されたことの3つです。

余談ですがchiveは英語でエゾネギのことを意味するらしいです。mecabといいsudachiといい、食べ物の名前を使うのが慣例なのでしょうか。。。

データをNumPyを使った行列へ

このchiVeは語彙数が豊富なのですがその分データサイズが大きくなっていて、展開すると12GBにもなります。これをそのままgensimライブラリを通して使用することは(少なくとも僕の使っているパソコン上では)難しいので、別の利用方法を考えました。それは、データを1行ずつ読み込んでnumpyを使った行列に整形して、npy形式で保存するという方法です。npyはnumpy配列を保存するバイナリファイルです。npy形式で一度保存してしまえば、その後の利用時に元のデータを直接読み込むより速く読み込めるのではないかと思い試してみました。データを読み込む際は同時に、単語と単語ID(単語ベクトルの行列内での位置を示す)が互いに結び付けられた辞書をつくっていきます。これはPythonの辞書型を使うので、pickleで保存します。

コードは以下の通りです。

chive2npy.py
import numpy as np
import pickle

chive_path = 'C:/path/to/chive/nwjc.sudachi_full_abc_w2v.txt'

# npyとpickleの保存先
npy_path = 'save_data/emb_layer.npy'
pickle_path = 'save_data/word2id.pkl'


# データロード
def load_data():
    emb_layer = np.load(npy_path)
    with open(pickle_path, 'rb') as f:
        word2id = pickle.load(f)
    
    return emb_layer, word2id


emb_list = []
word2id = {}
vocab_size = 0

# つづきから行う場合は実行
# emb_layer, word2id = load_data()
# vocab_size = len(emb_layer)

# chiveのテキストデータを開く
with open(chive_path, 'r', encoding='utf-8') as chive_file:
    # 最初の行を読み込み(語彙数 次元数)
    line = chive_file.readline().strip('\n')
    print(line)
    word_num = int(line.split()[0])
    emb_size = int(line.split()[1])

    # 語彙数だけ繰り返し
    for i in range(word_num):
        line = chive_file.readline().strip('\n')

        # すでに読み込んだ行をスキップ
        if i < vocab_size:
            continue

        line_list = line.split()
        print(line_list[0], line_list[1])

        # 単語と単語IDを辞書へ
        word2id[line_list[0]] = i
        # 単語ベクトルをnp.float32で追加
        vec = np.array(line_list[-emb_size:], dtype=np.float32)
        emb_list.append(vec)

        # 10000ごとに保存
        if (i+1) % 10000 == 0:
            if i+1 == 10000:
                emb_layer = np.array(emb_list)
            else:
                # emb_listをemb_layerに結合
                emb_layer = np.concatenate((emb_layer, emb_list), axis=0)
            np.save(npy_path, emb_layer)
            emb_list = []

            with open(pickle_path, 'wb') as f:
                pickle.dump(word2id, f, protocol=-1)
            
            print('-'*50)
            print(i+1, '/', word_num, 'have been done')
            print('-'*50)

# すべて読み込んだら保存
emb_layer = np.concatenate((emb_layer, emb_list), axis=0)
np.save(npy_path, emb_layer)
with open(pickle_path, 'wb') as f:
    pickle.dump(word2id, f, protocol=-1)

print('complete!')

関数のload_data()は、データの読み込みを途中でやめた場合に続きから読み込めるようにするためのものです。

テキストデータの一行目には語彙数と単語ベクトルの次元数が記されているので、最初にそれをreadline()で読み込んで表示しています。表示した結果はこうなります。

3644628 300

語彙数が約360万、次元数が300です。語彙数がありえないくらい大きい。。。

そのあとは1行読み込んでは単語と単語ベクトルを取得することを繰り返していきます。単語と単語ベクトルの各成分は空白で区切られているので、split()を使って分割しています。確認のために単語とベクトルの成分の1列目をprint(line_list[0], line_list[1])で表示しています。

行列と辞書の保存は10000ごとに行っています。npy形式で保存されるnumpy行列は、それぞれの行が単語ベクトルとなっているemb_layerです。そしてpickleで保存される辞書は、単語とその単語ベクトルのemb_layer上での行番号が結びついたword2idです。これら2つを使うと、例えば「君」という単語の単語ベクトルを取り出すときにこうなります。

vec = emb_layer[word2id['君']]

約360万の単語すべてを保存するにはかなりの時間がかかるので、100万個の単語を読み込んだところでいったん止めました。(おそらく)データが下に行くほど出現頻度の低い単語になるので、上位100万の単語があれば十分でしょう。

類似単語を計算・表示

読み込んだ単語ベクトルを使って、入力単語の類似単語を返すプログラムを作ってみます。ベクトル間の類似度の計測にはコサイン類似度を使用します。コードは以下の通りです。

chive_test.py
import numpy as np
import pickle

npy_path = 'save_data/emb_layer.npy'
pickle_path = 'save_data/word2id.pkl'

# データロード
emb_layer = np.load(npy_path)
vocab_size = len(emb_layer)

with open(pickle_path, 'rb') as f:
    word2id = pickle.load(f)
    # 単語IDから単語を返す辞書も作成
    id2word = {v:k for k,v in word2id.items()}

# 語彙数を5万に絞る
short_vocab_size = 50000
emb_layer = emb_layer[:short_vocab_size]
for i in range(len(word2id) - short_vocab_size):
    word2id.pop(id2word[short_vocab_size + i])
    id2word.pop(short_vocab_size + i)
print('-'*50)


# コサイン類似度
def cos_similarity(x, y):
    nx = x / np.sqrt(np.sum(x**2))
    ny = y / np.sqrt(np.sum(y**2))
    return np.dot(nx, ny)


def most_similar(word, top=5):
    if word not in word2id:
        print(word + ' is not found')
        print('-'*50)
        return
    print(word + ' is the most similar to...')

    # 単語ベクトルを取得
    word_vec = emb_layer[word2id[word]]

    # コサイン類似度を計算
    similarity = np.zeros(short_vocab_size)
    for i in range(short_vocab_size):
        similarity[i] = cos_similarity(word_vec, emb_layer[i])

    # 上位を出力
    count = 0
    # 降順にするために-1をかける
    for word_id in (-1 * similarity).argsort():
        if count >= top:
            print('-'*50)
            return
        if id2word[word_id] == word:
            continue        
        print('%s: %s' % (id2word[word_id], similarity[word_id]))
        count += 1


# main
words = []
for word in words:
    most_similar(word)

語彙数100万のままだと類似度の計算に時間がかかるので、さらに上位5万単語に絞りました。おそらくこれだけの語彙を持っていれば一般的に使われる単語はほぼカバーしているでしょう。

単語をwordsの中に並べて実行すると、おのおのの類似単語を出力するという仕組みです。デフォルトは上位5個ですがmost_similar()の引数のtopの値を変えれば、上位何個までを出力するか変更可能です。

おわりに

次回は今回作成したプログラムを使って、実際にいろいろな単語の類似単語を出力させてみようと思います。ではまた。

次回の記事↓

単語ベクトルchiVeを使った類似度計算―RNNで文章生成〈6〉