以下の本を読みます。
直感 Deep Learning ―Python×Kerasでアイデアを形にするレシピ Antonio Gulli Sujit Pal 大串 正矢 オライリージャパン 2018-08-11 売り上げランキング : 1992 Amazonで詳しく見る by G-Tools |
5章の5.3節まで読みましたが、5.4節は実際に単語分散表現を学習する場合はこれらのようなやり方があるといった紹介なので、手を動かしてみた方がよさそうですね。そもそも、ここまで登場した単語分散表現モデルも、手元で実装してみませんか?
じゃあまず Skip-gram からだな。142~144ページにコードが載ってるからこれを実行するだけ…あれっ、ど初っ端の一行目から、keras.layers から Merge という名前のオブジェクトはインポートできませんって言われたんだけど! どういうことなの!?
えーどうする? 勝手にプロデューサーの環境の keras のバージョン下げる?
それはやめた方がいいんじゃ…。そもそも、keras.layers.Merge でやりたかったのは中心語の数値ベクトルと文脈語の数値ベクトルの内積を取ることですよね…keras Merge で検索すると、keras.layers に Dot というそれそのもののレイヤーがありますよ。試してみると…ちゃんと内積が取れますね。
# -*- coding: utf-8 -*- import numpy as np from keras.layers import Dot, Input from keras.models import Model x0 = Input(shape=(3,)) x1 = Input(shape=(3,)) y = Dot(axes=-1)([x0, x1]) model = Model(inputs=[x0, x1], outputs=y) model.compile(loss='mean_squared_error', optimizer='adam') # dummy print(model.summary()) x0_samples = np.array([[1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0]]) x1_samples = np.array([[1.0, 1.0, 1.0], [2.0, 2.0, 2.0], [3.0, 3.0, 3.0]]) y_samples = model.predict([x0_samples, x1_samples]) print(y_samples) # --> [[6.0], [12.0], [18.0]]
# -*- 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_samples = np.array([[1], [4], [1], [4], [2]]) # 中心語: love,and,love,and,green x1_samples = np.array([[0], [5], [2], [2], [2]]) # 文脈語: i,ham,green,green,green y_samples = sg.discriminator.predict([x0_samples, x1_samples]) print(y_samples) # 中心語と文脈語のペアであるかどうかの判定結果(学習まだ) # 中心語の数値ベクトル表現は中心語の Embedding 層の重みそのもの print(sg.word_embedder.get_weights()) # IDから数値ベクトル表現を取り出せることの確認 print(sg.word_embedder.predict([[0]])) # i の数値ベクトル表現 print(sg.word_embedder.predict([[1]])) # love の数値ベクトル表現
# 中心語と文脈語のペアであるかどうかの判定結果(学習まだ) [ [0.45034274] [0.4851311 ] [0.5297962 ] [0.5046127 ] [0.41040125] ] # 中心語の数値ベクトル表現は中心語の Embedding 層の重みそのもの [array([ [-0.3271621 , -0.14325774, -0.4473939 ], [-0.63613635, 0.71521103, 0.3045839 ], [-0.4950593 , -0.3844918 , -0.79637474], [ 0.52346075, -0.1862371 , -0.66979444], [-0.79966545, -0.35221177, 0.17136681], [ 0.13642508, -0.48409775, -0.34481126] ], dtype=float32)] # IDから数値ベクトル表現を取り出せることの確認 [ [-0.3271621 -0.14325774 -0.4473939 ] ] [ [-0.63613635 0.71521103 0.3045839 ] ]
そっか、Skip-gram の学習データって、コーパス内の単語に ID を割り振って、各IDのペアに正例か負例かのラベルを貼らないといけないんだよな。いちいち手でやってらんないし、学習データを生成する関数も書いた方がよさそうだな。
2018-10-14 追記
もうめんどくさいから機関車トーマスでいいよ。
トーマスの原著(英語)があればいいのですが、手元には日本語版しかないので、ではやはり日本語用のプレ処理をしましょう。以下のリンクを参考にしました。
MeCab の形態素解析結果を用いて、Skip-grams を愚直に書くなら以下のようになると思うんですよね。書いたクラスは、文章のリストを渡すと1文ずつ形態素解析して、未登録の単語を登録しながら文章をID列に変換して、Skip-grams の正例をひたすら集めています。ただ、負例をどのように準備すべきかがわからないんですよね。もちろん、正例にないIDのペアは負例なのですが…。keras の skipgrams 関数ではどのようにサンプリングされているのか確認したいですね。skipgrams 関数の入力はID列のようなので、単語にIDを割り振るまではプレ処理して後は任せるのがいいのかもしれませんが。# -*- coding: utf-8 -*- import MeCab import codecs class SkipGramCollector(): def __init__(self, window_size): self.window_size = window_size # 周辺の何単語を考慮するか self.tagger = MeCab.Tagger('-Owakati') self.raw_words = [] self.center_words = [] self.context_words = [] def _process_one_sentence(self, sentence): words = self.tagger.parse(sentence).split(' ') # 文 --> 単語列 id_seq = [] # 単語列 --> ID列 for word in words: if word in ['、', '「', '」', '\n']: continue # とりあえず無視 if word not in self.raw_words: self.raw_words.append(word) index = self.raw_words.index(word) id_seq.append(index) n_words = len(id_seq) for id in id_seq: # 中心語と文脈語のペアの正例を登録 for context_id in range(id - self.window_size, id): if -1 < context_id: self.center_words.append(id) self.context_words.append(context_id) for context_id in range(id + 1, id + self.window_size + 1): if context_id < n_words: self.center_words.append(id) self.context_words.append(context_id) def process_sentences(self, sentences): for sentence in sentences: if sentence == u"\r\n": continue self._process_one_sentence(sentence) if __name__ == '__main__': split_sentence = None f = codecs.open('thomas_the_tank_engine.txt', 'r', 'utf-8') # 改行なしのテキスト for line in f: split_sentence = line.split(u'。') sgc = SkipGramCollector(2) sgc.process_sentences(split_sentence) # 最初の10単語をプリント print(sgc.raw_words[0:10]) # 最初の10個の正例をプリント for i in range(10): print(sgc.raw_words[sgc.center_words[i]], ',', sgc.raw_words[sgc.context_words[i]])
['機関', '車', 'トーマス', 'は', '大きな', '駅', 'で', 'はたらい', 'て', 'い'] 機関 , 車 機関 , トーマス 車 , 機関 車 , トーマス 車 , は トーマス , 機関 トーマス , 車 トーマス , は トーマス , 大きな は , 車
…なあジュン、「機関車」が「機関」と「車」に分けられるのってどうなの? 機関車は機関車だろ。
それは僕も結構そう思ったんですよね…。
2018-10-22 追記
まあ形態素解析が多少意図通りでない点は置いておいて、Mecab で形態素解析だけして、後は keras に任せた例が以下です。日本語テキストについて Skip-grams の正例も負例も得られていますね。ここでは「機関車トーマス」の最初の見開き3ページ分の文章を使いました。周辺の何単語を考慮するかの window_size は 1 にしています。skipgrams 関数は全ての正例をサンプリングし、デフォルトでは正例と同数の負例をランダムサンプリングしてくれるようですね(正例の何倍の数の負例をサンプリングするかは引数 negative_samples で調整できます)。
# -*- coding: utf-8 -*- from keras.preprocessing.text import * from keras.preprocessing.sequence import skipgrams import MeCab import codecs import numpy as np import pandas as pd if __name__ == '__main__': tagger = MeCab.Tagger('-Owakati') # list_text に文章たちを格納(ただし、各文章は単語ごとに半角空白区切りになった状態) list_text = [] f = codecs.open('thomas_the_tank_engine.txt', 'r', 'utf-8') # 改行なしのテキスト for line in f: list_text_raw = line.split('。') for text_raw in list_text_raw: text = tagger.parse(text_raw) list_text.append(text) # 半角空白区切りの文章たちを Tokenizer に渡して含まれる全単語を収集 tokenizer = Tokenizer(filters='、。「」\n\r') tokenizer.fit_on_texts(list_text) # 単語-->ID辞書と、ID-->単語辞書を作成 word2id = tokenizer.word_index id2word = {v:k for k, v in word2id.items()} # 文章ごとに Skip-gram 対を得る df = pd.DataFrame() for text in list_text: # 文章をID列化 wids = [word2id[w] for w in text_to_word_sequence(text, filters='、。「」\n\r')] # ID列から Skip-gram 対を得る pairs, labels = skipgrams(wids, len(word2id), window_size=1) 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_]) df.reset_index(drop=True, inplace=True) print(df.shape) # 負例にサンプリングされた 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(df.shape) # 結果確認 print("正例の数:", np.sum(df.y == 1)) print("負例の数:", np.sum(df.y == 0)) for index, row in df.iterrows(): print("({:s} ({:d}), {:s} ({:d})) -> {:d}".format( id2word[row['x0']], row['x0'], id2word[row['x1']], row['x1'], row['y'])) if index == 10: break
(444, 3) (442, 3) 正例の数: 222 負例の数: 220 (は (3), ぼく (65)) -> 0 (大きな (13), 駅 (21)) -> 1 (まし (10), た (4)) -> 1 (駅 (21), 大きな (13)) -> 1 (まし (10), さ (69)) -> 0 (駅 (21), ピッピー (20)) -> 0 (はたらい (22), とおく (33)) -> 0 (トーマス (8), つい (30)) -> 0 (車 (7), からっぽ (39)) -> 0 (で (14), 駅 (21)) -> 1 (い (9), まし (10)) -> 1
ふーん? まあでも、学習データができたんなら早速学習してみようぜ!
そうですね。例えば上のスクリプトに対して、上で実装したニューラルネットワークを読み込んで、抽出した学習データをそのまま渡せば学習できますよ。
# -*- coding: utf-8 -*- from keras.preprocessing.text import * from keras.preprocessing.sequence import skipgrams import MeCab import codecs import numpy as np import pandas as pd from skip_gram_discriminator import SkipGramDiscriminator # 上で実装したクラスを読み込み if __name__ == '__main__': # ここの部分のスクリプトは省略 sg = SkipGramDiscriminator(len(word2id), 3) # 3次元に埋め込む場合 sg.create_model() sg.discriminator.fit([df.x0.values, df.x1.values], df.y.values, batch_size=32, epochs=100)