以下の本を読みます。
![]() | 直感 Deep Learning ―Python×Kerasでアイデアを形にするレシピ Antonio Gulli Sujit Pal 大串 正矢 オライリージャパン 2018-08-11 売り上げランキング : 1992 Amazonで詳しく見る by G-Tools |

前回 keras で Skip-gram 学習器をつくってみたけど、学習した結果ってどんな感じになるんだ?

それなんですが…当初「機関車トーマス」を題材にするつもりだったんですが、解析するために持ち出そうとすると小さい子が泣いちゃうんですよね…。

小さい子から絵本取り上げるなよ!

それに、元々テキストファイルになっていませんし…なので、題材を変えて、春名さんのセリフを解析することにします。春名さんにとってドーナツがどのように重要なのか手がかりが得られるかもしれませんし。

趣旨変わってる!?

春名さんのセリフはまとめられているページ(若里 春名 - アイドルマスターsideM Wiki*)からパースしてきたんですが、セリフ中のカタカナが半角になっているので以下のページのスクリプトを拝借しました。ただ、このスクリプトで半角の伸ばし棒が全角の伸ばし棒に変換できなかったので、それだけはテキストエディタ上で手でやりました。
今回 Beautiful Soup で td タグ中の文字列を収集したんですが、改行が含まれているセリフがパースできなかったんですよね。面倒なのでとばしました。セリフを csv に書き出して、この記事の最下部にあるスクリプト(※)を実行した結果が以下です。0 楽しいことがないなら、自分で作る! 1 ジュンはコーヒーのコトはまだ許してないけど、勉強教えてくれるのはホント助かってる。ナツキは無... 2 なぁ、プロデューサー。オレ、バカだけど…やる気だけは本物だからさ。たまには思い出してくれよなっ。 Name: str, dtype: object 正例の数: 5021 負例の数: 4947 (9968, 3) ===== 単語登場回数 (ユニーク単語数:1280, 語数:5922) ===== [('て', 232), ('の', 220), ('な', 195), ('に', 181), ('だ', 155), ('が', 112), ('は', 111), ('も', 111), ('オレ', 91), ('よ', 91), ('た', 90), ('し', 87), ('と', 85), ('で', 82), ('ん', 81), ('プロデューサー', 75), ('って', 75), ('ぜ', 74), ('か', 69), ('ない', 66), ('を', 66), ('ドーナツ', 57), ('さ', 47), ('てる', 43), ('から', 43)] ===== 文書ベース単語登場回数 (文書数:286) ===== [('な', 161), ('て', 154), ('の', 153), ('に', 131), ('だ', 128), ('が', 96), ('も', 96), ('オレ', 88), (' よ', 88), ('は', 86), ('で', 77), ('し', 75), ('ぜ', 74), ('と', 73), ('ん', 73), ('た', 73), ('プロデューサー', 71), ('って', 66), ('か', 64), ('ない', 62), ('を', 62), ('ドーナツ', 48), ('さ', 45), ('から', 41), ('てる', 40)] ===== Skip-gram対の例 ===== (こと (30), 今日 (45)) -> 0 (ない (20), け (977)) -> 0 (自分 (128), で (14)) -> 1 (が (6), なら (49)) -> 1 (ない (20), 自分 (128)) -> 1 (楽しい (75), が (6)) -> 1 (の (2), は (7)) -> 1 (けど (28), て (1)) -> 1 (てる (24), ナツキ (244)) -> 1 (ホント (129), は (7)) -> 1 (けど (28), 補給 (1064)) -> 0

いや俺が知りたいよ! …でも、助詞や助動詞ばかりっていいのか? 多くの単語が助詞や助動詞と隣り合っていることが多い、ってだけになっちゃわない?


おお、こうなるのか…だから何!?

そうですね…「オレ」から見て「ドーナツ」「ドラム」が同じ方角にあるので、どっちも好きなんじゃないですか?

分析が雑!

春名さんの結果だけだとよくわからないので、参考のために他の事務所のアイドルの方ですが、椎名法子さんのセリフも解析してみました。何でも彼女もドーナツに目がないということで。

なんで他の事務所にまでドーナツに目がないアイドルがいるんだ!?

春名さんの方が後発ですけどね。椎名さんについてもセリフがまとめられているページ(椎名法子 -アイマス デレステ攻略まとめwiki【アイドルマスター シンデレラガールズ スターライトステージ】 - Gamerch)から適当にパースしました。スクリプトの実行結果が以下です。
0 すっごーい!ごちそうさまでしたー! 1 ぷはーっ!もう最っ高!会場中が幸せの輪になっちゃった☆ 2 いい仕上がりだね!私たち、とってもクリスピー! Name: str, dtype: object 正例の数: 3631 負例の数: 3629 (7260, 3) ===== 単語登場回数 (ユニーク単語数:994, 語数:4198) ===== [('!', 191), ('○', 147), ('♪', 146), ('の', 132), ('て', 110), ('に', 108), ('?', 94), ('は', 90), ('プロデューサー', 89), ('ドーナツ', 72), ('た', 69), ('だ', 65), ('~', 60), ('で', 58), ('ー', 57), ('も', 56), ('よ', 53), ('っ', 52), ('ね', 49), ('と', 45), ('あたし', 42), ('し', 41), ('を', 41), ('か', 39), ('が', 37)] ===== 文書ベース単語登場回数 (文書数:309) ===== [('!', 154), ('♪', 134), ('の', 115), ('に', 95), ('プロデューサー', 89), ('て', 89), ('?', 82), ('は', 81), ('○', 74), ('ドーナツ', 70), ('た', 61), ('だ', 57), ('~', 54), ('で', 54), ('よ', 50), ('も', 50), ('ね', 49), ('ー', 47), ('っ', 45), ('あたし', 40), ('し', 40), ('と', 38), ('か', 37), ('を', 37), ('が', 35)] ===== Skip-gram対の例 ===== (でし (400), ー (15)) -> 1 (! (1), でし (400)) -> 1 (ごちそうさま (399), ! (1)) -> 1 (い (48), カワイク (371)) -> 0 (ー (15), ちゃお (839)) -> 0 (ごちそうさま (399), また (310)) -> 0 (た (11), ! (1)) -> 1 (もう (104), 最 (401)) -> 1 (輪 (49), 幸せ (130)) -> 1 (幸せ (130), 話 (380)) -> 0 (高 (402), こう (560)) -> 0


なんでだよ!?

まあ今回は処理に行き届いていない面も多いですし、実際には膨大なコーパスで学習済みのネットワークを調整するべきなんでしょう。確かテキストの5章の最後の方はそのようなやり方の話だったので、きちんと分析したい場合はそちらを参考にしてみましょう。
※ スクリプト
# -*- 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 # 周辺の何単語を考慮するか # 形態素解析し全単語を収集して、単語-->ID辞書と、ID-->単語辞書を作成 self.list_text = [] for text_raw in list_text_raw: text = self.tagger.parse(text_raw) 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 = 'noriko' # 'haruna' 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')