最初 最後

直感 Deep Learning 5章 単語分散表現
を読んで面白いことがしたかった話

テキスト

直感 Deep Learning ―Python×Kerasでアイデアを形にするレシピ直感 Deep Learning ―Python×Kerasでアイデアを形にするレシピ
Antonio Gulli Sujit Pal 大串 正矢

オライリージャパン 2018-08-11
売り上げランキング : 1992

Amazonで詳しく見る
by G-Tools

その他の参考文献

  1. 日本語 Wikipedia エンティティベクトル(東北大学 乾・岡崎研究室)

単語分散表現とは(1)

なので単語分散表現(= 単語集合から実ベクトル空間への写像であって、単語どうしの何らかの関係性を反映したもの)がほしい。

単語分散表現の例

参考文献 1. で公開されている、日本語 Wikipedia で記事となっているエンティティの100次元表現(Skip-gram で学習)。 https://github.com/singletongue/WikiEntVec \[ f({\large ドーナツ}) = \left( \begin{array}{c} 0.15019251 \\ 0.4490861 \\ \vdots \\ -0.5708073 \end{array} \right), \; \; \; f({\large クッキー}) = \left( \begin{array}{c} -0.04867858 \\ 0.44743043 \\ \vdots \\ -0.5990641 \end{array} \right) \] \[ {\rm cos} \bigl(f({\large ドーナツ}) , \; f({\large クッキー}) \bigr) = 0.8620 \] \[ {\rm cos} \bigl(f({\large ドーナツ}) , \; f({\large ラーメン}) \bigr) = 0.6936 \]
from gensim.models import KeyedVectors
model = KeyedVectors.load_word2vec_format('entity_vectors.txt')
print(model[u'ドーナツ'])
print(model[u'クッキー'])
print(model.similarity(u'ドーナツ', u'クッキー'))
print(model.similarity(u'ドーナツ', u'ラーメン'))

単語分散表現とは(2)

Skip-gram による単語分散表現の作り方

  1. 以下のようなニューラルネットワークを組んで、「ある単語(中心語)について、他のある単語(文脈語)が文章中で N 単語以内の距離に登場するか否か(1 or 0)」を学習する。
    1. 中心語と文脈語のペアを入力する。
    2. 中心語を中心語用の Embedding 層で数値ベクトルにする(★)。
    3. 文脈語を文脈語用の Embedding 層で数値ベクトルにする。
    4. 1. と 2. の内積をとる。
    5. 3. に出力の次元数が1の Dense 層をかぶせ、sigmoid で活性化して最終的な判定(1 or 0)とする。
  2. (★)が得たい単語分散表現に他ならない。

Keras による Skip-gram 識別器の実装例

「単語数」、「何次元に埋め込むか」さえ決めればネットワーク構造が決まる。
# -*- coding: utf-8 -*-
import numpy as np
from keras.layers import Dot, Input, Dense, Reshape, Embedding
from keras.models import Model

class SkipGramDiscriminator():
    def __init__(self, vocab_size, embed_size):
        self.vocab_size = vocab_size # 語彙数
        self.embed_size = embed_size # 埋め込み次元数
    def create_model(self):
        # 中心語ID --> 中心語数値ベクトル表現
        x0 = Input(shape=(1,))
        y0 = Embedding(self.vocab_size, self.embed_size,
                       embeddings_initializer='glorot_uniform')(x0)
        y0 = Reshape((self.embed_size,))(y0)
        self.word_embedder = Model(x0, y0)
        # 文脈語ID --> 文脈語数値ベクトル表現
        x1 = Input(shape=(1,))
        y1 = Embedding(self.vocab_size, self.embed_size,
                       embeddings_initializer='glorot_uniform')(x1)
        y1 = Reshape((self.embed_size,))(y1)
        self.context_embedder = Model(x1, y1)
        # 内積 --> ロジスティック回帰
        y = Dot(axes=-1)([y0, y1])
        y = Dense(1, kernel_initializer='glorot_uniform', activation='sigmoid')(y)
        self.discriminator = Model(inputs=[x0, x1], outputs=y)
        self.discriminator.compile(loss='mean_squared_error', optimizer='adam')
        print(self.discriminator.summary())
    
if __name__ == '__main__':
    sg = SkipGramDiscriminator(6, 3) # i,love,green,eggs,and,ham 6語を3次元空間へ埋込
    sg.create_model()
    
    x0 = np.array([[1], [4], [1], [4], [2]]) # 中心語: love,and,love,and,green
    x1 = np.array([[0], [5], [2], [2], [2]]) # 文脈語: i,ham,green,green,green
    y  = np.array([[1], [1], [1], [0], [0]]) # 正解ラベル
    sg.discriminator.fit([x0, x1], y, epochs=1000) # 学習
    
    y_pred = sg.discriminator.predict([x0, x1])
    print(y_pred) # 中心語と文脈語のペアであるかどうかの判定結果

    # 中心語の数値ベクトル表現は中心語の Embedding 層の重みそのもの
    print(sg.word_embedder.get_weights())

    # IDから数値ベクトル表現を取り出せることの確認
    print(sg.word_embedder.predict([[0]])) # i の数値ベクトル表現
    print(sg.word_embedder.predict([[1]])) # love の数値ベクトル表現
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to
==================================================================================================
input_1 (InputLayer)            (None, 1)            0
__________________________________________________________________________________________________
input_2 (InputLayer)            (None, 1)            0
__________________________________________________________________________________________________
embedding_1 (Embedding)         (None, 1, 3)         18          input_1[0][0]
__________________________________________________________________________________________________
embedding_2 (Embedding)         (None, 1, 3)         18          input_2[0][0]
__________________________________________________________________________________________________
reshape_1 (Reshape)             (None, 3)            0           embedding_1[0][0]
__________________________________________________________________________________________________
reshape_2 (Reshape)             (None, 3)            0           embedding_2[0][0]
__________________________________________________________________________________________________
dot_1 (Dot)                     (None, 1)            0           reshape_1[0][0]
                                                                 reshape_2[0][0]
__________________________________________________________________________________________________
dense_1 (Dense)                 (None, 1)            2           dot_1[0][0]
==================================================================================================
Total params: 38
Trainable params: 38
Non-trainable params: 0
__________________________________________________________________________________________________
...
Epoch 1000/1000
5/5 [==============================] - 0s 397us/step - loss: 0.0020
[[0.9564351 ]
 [0.94431126]
 [0.9720375 ]
 [0.05245696]
 [0.03800274]]
[array([[-0.13738829, -0.79445064, -0.73896384],
       [-0.8272589 , -0.03786828, -0.72830474],
       [ 0.6510331 ,  0.11700394,  0.94733346],
       [ 0.4470725 , -0.807297  , -0.64195573],
       [ 0.6196652 , -0.00116872,  0.8576324 ],
       [ 0.4181409 ,  0.26542222,  0.7537674 ]], dtype=float32)]
[[-0.13738829 -0.79445064 -0.73896384]]
[[-0.8272589  -0.03786828 -0.72830474]]
ネットワークはできたが、問題は学習データの準備。
日本語のテキストを学習してみたいが、テキストは元々英語の本なので日本語の取り扱いについて書いていない。
\(\to\) 今回やった学習データ準備の手順は以下(全体的によくわからない)
とにかくこの手順で、以下のドーナツ好きな2人のゲームキャラクターのセリフをそれぞれ学習して、ドーナツの学習結果がどのようになるか見てみたい。 セリフはあらかじめ上の URL からパースして、半角を全角にするプレ処理のみしてある。
# -*- coding: utf-8 -*-
import numpy as np
import pandas as pd
from keras.layers import Dot, Input, Dense, Reshape, Embedding
from keras.models import Model
from keras.preprocessing.text import *
from keras.preprocessing.sequence import skipgrams
import MeCab
import codecs
from sklearn.manifold import TSNE
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties

# Skip-gram ペア収集クラス
class SkipGramCollector():
    def __init__(self, list_text_raw, window_size):
        self.filters = '~?♪○!、….!?、。「」『』\n\r'
        self.tagger = MeCab.Tagger('-Owakati')
        self.window_size = window_size # 周辺の何単語を考慮するか
        self.stop_words = []
        for w in open('Japanese.txt', 'r'): # SlothLib の StopWords
            w = w.replace('\n', '')
            if len(w) > 0:
                self.stop_words.append(w)
        
        # 形態素解析し全単語を収集して、単語-->ID辞書と、ID-->単語辞書を作成
        self.list_text = []
        for text_raw in list_text_raw:
            text = self.tagger.parse(text_raw)
            # ストップワード除去
            _text = text.split()
            _text = [_t for _t in _text if (_t not in self.stop_words)]
            text = ' '.join(_text)
            self.list_text.append(text)
        self.tokenizer = Tokenizer(filters=self.filters)
        self.tokenizer.fit_on_texts(self.list_text)
        self.word2id = self.tokenizer.word_index
        self.id2word = {v:k for k, v in self.word2id.items()}
        
        # 文章ごとに Skip-gram 対を得る
        df = pd.DataFrame()
        for text in self.list_text:
            wids = [self.word2id[w] for w in text_to_word_sequence(text, filters=self.filters)]
            pairs, labels = skipgrams(wids, len(self.word2id), window_size=self.window_size)
            df_ = pd.DataFrame()
            df_['x0'] = np.array([pair[0] for pair in pairs]).astype(np.int64) # 中心語
            df_['x1'] = np.array([pair[1] for pair in pairs]).astype(np.int64) # 文脈語
            df_['y'] = np.array(labels).astype(np.int64) # 正解ラベル
            df = pd.concat([df, df_])
        
        # 負例にサンプリングされた Skip-gram 対のうち正例に含まれているものを除去
        df_dup_check = df[df.duplicated(keep=False) & (df.y == 0)] # 複数ある対であって負例
        df_dup_check = df_dup_check[df_dup_check.duplicated() == False].copy()
        for index, row in df_dup_check.iterrows():
            df_temp = df[(df.x0 == row['x0']) & (df.x1 == row['x1'])]
            if np.sum(df_temp.y == 1) > 0: # 正例があるのでこの対の負例からは削除する
                df_temp = df_temp[df_temp.y == 0]
                df = df.drop(index=df_temp.index)
        df.reset_index(drop=True, inplace=True)
        print("正例の数:", np.sum(df.y == 1))
        print("負例の数:", np.sum(df.y == 0))
        self.df = df

# Skip-gram 識別器クラス
class SkipGramDiscriminator():
    def __init__(self, vocab_size, embed_size):
        self.vocab_size = vocab_size # 語彙数
        self.embed_size = embed_size # 埋め込み次元数
    
    def create_model(self):
        # 中心語ID --> 中心語数値ベクトル表現
        x0 = Input(shape=(1,))
        y0 = Embedding(self.vocab_size, self.embed_size,
                       embeddings_initializer='glorot_uniform')(x0)
        y0 = Reshape((self.embed_size,))(y0)
        self.word_embedder = Model(x0, y0)
        
        # 文脈語ID --> 文脈語数値ベクトル表現
        x1 = Input(shape=(1,))
        y1 = Embedding(self.vocab_size, self.embed_size,
                       embeddings_initializer='glorot_uniform')(x1)
        y1 = Reshape((self.embed_size,))(y1)
        self.context_embedder = Model(x1, y1)
        
        # 内積 --> ロジスティック回帰
        y = Dot(axes=-1)([y0, y1])
        y = Dense(1, kernel_initializer='glorot_uniform', activation='sigmoid')(y)
        self.discriminator = Model(inputs=[x0, x1], outputs=y)
        self.discriminator.compile(loss='mean_squared_error', optimizer='adam')
        print(self.discriminator.summary())

if __name__ == '__main__':
    _train = False # 単語分散表現を学習する
    _plot = True # 結果をプロットする
    who = 'haruna' # 'noriko'
    
    if _train:
        # ===== 学習用 Skip-gram 対の生成 =====
        df = pd.read_csv(who + '.csv') # 'str' というカラムにセリフが入っているデータフレーム
        print(df['str'].head(3))
        sgc = SkipGramCollector(df['str'].values, 2)
        print(sgc.df.shape)
        np.savetxt('word_' + who + '.csv', np.array([k for k, v in sgc.word2id.items()]),
                   delimiter=',', fmt='%s')
        
        print('\r\n----- 単語登場回数 (ユニーク単語数:' + str(len(sgc.word2id)) +
             ', 語数:' + str(sum([v[1] for v in sgc.tokenizer.word_counts.items()])) + ') -----')
        print(sorted(sgc.tokenizer.word_counts.items(), key=lambda x:x[1], reverse=True)[0:25])
        
        print('\r\n----- 文書ベース単語登場回数 (文書数:' + str(len(sgc.list_text)) + ') -----')
        print(sorted(sgc.tokenizer.word_docs.items(), key=lambda x:x[1], reverse=True)[0:25])
        
        print('\r\n----- Skip-gram対の例 -----')
        for index, row in sgc.df.iterrows():
            print("({:s} ({:d}), {:s} ({:d})) -> {:d}".format(
              sgc.id2word[row['x0']], row['x0'], sgc.id2word[row['x1']], row['x1'], row['y']))
            if index == 10:
                break
        
        # ===== ネットワークの学習 =====
        sg = SkipGramDiscriminator(len(sgc.word2id), 4) # 4次元に埋め込む場合
        sg.create_model()
        sg.discriminator.fit([sgc.df.x0.values, sgc.df.x1.values], sgc.df.y.values,
                             batch_size=32, epochs=100)
        weight = sg.word_embedder.get_weights()[0]
        print(weight.shape)
        np.savetxt('weight_' + who + '.csv', weight, delimiter=',')
    
    if _plot:
        # ===== 2次元に次元削減してプロット =====
        fp = FontProperties(fname=r'C:\WINDOWS\Fonts\YuGothB.ttc', size=13)
        weight = np.loadtxt('weight_' + who + '.csv', delimiter=',')
        words = np.loadtxt('word_' + who + '.csv', delimiter=',', dtype='unicode')
        print(weight.shape)
        weight2 = TSNE(n_components=2, random_state=0).fit_transform(weight)
        print(weight2.shape)
        plt.scatter(weight2[:,0], weight2[:,1], c='darkgray')
        for word in (['オレ', 'プロデューサー', 'ドーナツ', 'ドラム'] if who is 'haruna' else \
                     ['あたし', 'プロデューサー', 'ドーナツ', 'アイドル']):
            i = np.where(words == word)
            plt.scatter(weight2[i,0], weight2[i,1], c='black')
            plt.text(weight2[i,0], weight2[i,1], word, fontproperties=fp)
        plt.savefig('figure_' + who + '.png')

noriko

正例の数: 3966
負例の数: 3901
(7867, 3)

----- 単語登場回数 (ユニーク単語数:902, 語数:3238) -----
[('の', 132), ('て', 110), ('に', 108), ('は', 90), ('プロデューサー', 89),
 ('ドーナツ', 72), ('た', 69), ('だ', 65), ('で', 58), ('ー', 57), 
 ('も', 56), ('よ', 53), ('っ', 52), ('ね', 49), ('と', 45), 
 ('あたし', 42), ('し', 41), ('を', 41), ('か', 39), ('が', 37), 
 ('な', 37), ('お', 32), ('ない', 31), ('ん', 25), ('食べ', 20)]

----- 文書ベース単語登場回数 (文書数:309) -----
[('の', 115), ('に', 95), ('プロデューサー', 89), ('て', 89), ('は', 81), 
 ('ドーナツ', 70), ('た', 61), ('だ', 57), ('で', 54), ('よ', 50), 
 ('も', 50), ('ね', 49), ('ー', 47), ('っ', 45), ('あたし', 40), 
 ('し', 40), ('と', 38), ('か', 37), ('を', 37), ('が', 35), 
 ('な', 33), ('お', 31), ('ない', 29), ('ん', 23), ('食べ', 19)]

----- Skip-gram対の例 -----
(ごちそうさま (336), た (7)) -> 1
(ー (10), プロデュース (738)) -> 0
(ー (10), い (40)) -> 1
(でし (337), ごちそうさま (336)) -> 1
(でし (337), ブレスレット (724)) -> 0
(い (40), がり (475)) -> 0
(い (40), 聞い (288)) -> 0
(い (40), かける (866)) -> 0
(でし (337), 負け (850)) -> 0
(ごちそうさま (336), 手作り (234)) -> 0
(た (7), でし (337)) -> 1

haruna

正例の数: 4378
負例の数: 4231
(8609, 3)

----- 単語登場回数 (ユニーク単語数:1181, 語数:5425) -----
[('て', 232), ('の', 220), ('な', 195), ('に', 181), ('だ', 155),
 ('が', 112), ('は', 111), ('も', 111), ('オレ', 91), ('よ', 91),
 ('た', 90), ('し', 87), ('と', 85), ('で', 82), ('ん', 81), 
 ('プロデューサー', 75), ('って', 75), ('ぜ', 74), ('か', 69), ('ない', 66), 
 ('を', 66), ('ドーナツ', 57), ('さ', 47), ('てる', 43), ('いい', 42)]

----- 文書ベース単語登場回数 (文書数:286) -----
[('な', 161), ('て', 154), ('の', 153), ('に', 131), ('だ', 128), 
 ('が', 96), ('も', 96), ('オレ', 88), ('よ', 88), ('は', 86), 
 ('で', 77), ('し', 75), ('ぜ', 74), ('と', 73), ('ん', 73), 
 ('た', 73), ('プロデューサー', 71), ('って', 66), ('か', 64), ('ない', 62), 
 ('を', 62), ('ドーナツ', 48), ('さ', 45), ('てる', 40), ('いい', 40)]

----- Skip-gram対の例 -----
(ない (20), で (14)) -> 1
(なら (44), が (6)) -> 1
(なら (44), 作る (478)) -> 1
(の (2), 時代 (755)) -> 0
(悪く (483), 減っ (1008)) -> 0
(きく (484), こ (397)) -> 0
(許し (287), ねだり (732)) -> 0
(の (2), コーヒー (161)) -> 1
(悪く (483), ない (20)) -> 1
(は (7), 甘酒 (365)) -> 0
(悪く (483), と (13)) -> 1

よくわからなかった。
そもそも登場頻度が多い語が「の」とか「て」なのが駄目そうな気がする。
データも少ないと思うし埋め込む次元数もよくわからない。
でもせっかくなので gensim で読める形式にして、「ドーナツ」に類似の単語を出してみる。
# -*- coding: utf-8 -*-
import numpy as np
from gensim.models import KeyedVectors

if __name__ == '__main__':
    who = 'noriko'
    with open('word_weight_' + who + '.txt', mode='w', encoding='utf-8') as f:
        words = np.loadtxt('word_' + who + '.csv', delimiter=',', dtype='unicode')
        f.write(str(len(words)) + ' ' + '4\n')
        i = 0
        weight = open('weight_' + who + '.csv', 'r')
        for line in weight:
            f.write(words[i] + ' ' + line.replace(',', ' '))
            i = i + 1
        weight.close()
    
    model = KeyedVectors.load_word2vec_format('word_weight_' + who + '.txt')
    print(len(model.vocab))
    print(model[u'ドーナツ'])
    print(model.most_similar(u'ドーナツ'))

noriko

902
[-0.37491137 -0.10871826  1.0103428   0.5701179 ]
[('くる', 0.9954972863197327),
 ('だろ', 0.9908122420310974), 
 ('来', 0.9868786931037903), 
 ('おっかな', 0.9773104190826416), 
 ('忙しい', 0.9730000495910645), 
 ('お', 0.9729252457618713), 
 ('全力', 0.9648570418357849), 
 ('ぶ', 0.9618979692459106), 
 ('ねえ', 0.9618968367576599), 
 ('もっ', 0.959823489189148)]

haruna

1181
[ 0.07973216 -0.8829228   0.758757   -0.4895581 ]
[('足', 0.990249752998352), 
 ('いっちょ', 0.9864041209220886), 
 ('プロ', 0.9842278957366943), 
 ('教科書', 0.9799279570579529), 
 ('仲間', 0.9799133539199829), 
 ('声', 0.978289008140564), 
 ('頼っ', 0.9779394865036011), 
 ('ホワイト', 0.9706403613090515), 
 ('いき', 0.9678006768226624), 
 ('いっ', 0.9661396741867065)]

わからなかったこと

5章の内容のつづき

試行錯誤の形跡