異常検知と変化検知: 11章メモ(密度比推定による変化検知)

以下の赤い本の11章を読みます。キャラクターは適当です。誤りがありましたらご指摘いただけますと幸いです。

異常検知と変化検知 (機械学習プロフェッショナルシリーズ)異常検知と変化検知 (機械学習プロフェッショナルシリーズ)
井手 剛 杉山 将

講談社 2015-08-08
売り上げランキング : 56236

Amazonで詳しく見る
by G-Tools
f:id:cookie-box:20180513082851p:plain:w60

10章の「疎構造学習による異常検知」では、9章のように「ある変数のようすが少し前と今とで変化したか」を測ろうとするだけでは検知できない、「複数の変数間の依存関係の変化」を見出そうという話でしたね。そのために、まず「直接相関」と「間接相関」を区別しました。そして、この本では変数間の依存関係に専らガウス型グラフィカルモデルを仮定することにして、データから精度行列を推定すればどの変数どうしが直接相関をもつのかわかるという寸法でしたね。

f:id:cookie-box:20180513082908p:plain:w60

でもその精度行列は単にデータに対して事後分布が最大になるような精度行列をとればいい(MAP推定すればいい)ってわけじゃなかったね。精度行列は解釈のしやすさの観点でもロバスト性の観点でもなるべくスカスカであってほしい。だったら、最大化すべき目的関数が「精度行列の各要素の絶対値の和の定数倍」を差し引くようなペナルティ項を含んでいるといい。これはMAP推定において事前分布にラプラス分布を仮定すると達成される。この最適化にはブロック座標降下法を用いるんだったね。

f:id:cookie-box:20180513082851p:plain:w60

疎構造学習はある意味、明示的にわかりやすさを求めたモデルだったんですね。しかし、143ページの最後の方に気になる書き方がしてあります。つまり、明示的にわかりやすさを求める必要はないはずだよね、と。もちろん、わかりやすさを目指さずともわかりやすいモデルがぽんと出てくれば苦労しませんが…。

f:id:cookie-box:20180513082908p:plain:w60

(10.30) 式は正常データとテストデータの確率密度の比を含んでいる。最初からこれを推定すればもっとシンプルになるのでは、って10章は結ばれているね。細かい構造まで立ち入らないに越したことはないからね。

f:id:cookie-box:20180513082851p:plain:w60

いや…(10.30) 式っていうほど「密度比直接推定すればいいじゃん」って示唆してますかね…。まあ前置きが長くなったので本題の11章に入りましょう。ここでの問題設定は、正常だとわかっているデータ  \mathcal{D} が与えられたときに検査対象データ  \mathcal{D'} から異常標本を見つけ出す…特に検査対象データをひとまとめに考える…特異スペクトル変換法やグラフィカルラッソに基づく異常解析も検査対象データをひとまとめに考えていたような感じでしたが…まあいいです。正常なデータがしたがう分布  p(x) と検査対象データがしたがう分布 p'(x) をつかって、ある検査対象データの「異常さの度合い」を  \ln \bigl(p'(x) / p(x)\bigr)=- \ln \bigl(r(x)\bigr) という負の対数密度比で測ることはできます。これは4ページの「異常度」とは違って、 p'(x) は異常データのしたがう分布というわけではないので、密度比は「異常っぽさよりも正常っぽさが何倍強い」というのを表すわけではありません。異常標本に対して密度比  r(x) が小さな値を取ることは図11.1からも直感的に理解できますが、特に異常標本の割合を  \alpha、異常標本がしたがう分布を  \tilde{p}(x) として  p'(x) = (1 - \alpha) p(x) + \alpha \tilde{p}(x) とかける場合は密度比の取りうる値の範囲が議論できて、つまり 0 と  1/(1-\alpha) の間になるということですね。それで、正常標本の「異常さの度合い」は  - \alpha 程度になるので、「異常さの度合い」がここから大きくずれたらその標本は異常が疑われると。

f:id:cookie-box:20180513082908p:plain:w60

146~147ページが主張しているのは、「正常データと検査対象データの密度比 r(x)」さえわかれば検査対象データに異常標本が混入しているかどうかがわかるってことだね。

f:id:cookie-box:20180513082851p:plain:w60

なるほど、だから密度比を直接推定すればよいとなるのですね。ただ「検査対象データに異常標本が含まれているか」という問題設定自体がここで初めてで、かついきなり密度比から出発したので、147ページにあるように「発想の転換」というよりは、とりあえず考えてみたらこうなってた、って感じがしますが…。まあそれはそれで、じゃあどうやって密度比 r(x) を推定するのかということで、基本的なモデルは必要なわけで、それが (11.2) 式の線形モデル r_{\theta}(x) ですね。基底関数  \psi(x) というのは各データを b 次元空間に埋め込む写像に見えますね。この基底関数の具体的な形は後回しで、線形結合のパラメータを如何に推定するかですが、目的関数が必要ですよね。しかし、密度比の学習の目的関数などどう設定すればよいのでしょう…。

f:id:cookie-box:20180513082908p:plain:w60

r_{\theta}(x) の定義より、「r_{\theta}(x)p(x)/p'(x) に近くなければならない」→「r_{\theta}(x)p'(x)p(x) に近くなければならない」→「r_{\theta}(x)p'(x)p(x) の一般化KLダイバージェンスは小さくなければならない」という流れだね。

f:id:cookie-box:20180513082851p:plain:w60

だから r_{\theta}(x)p'(x)p(x) の一般化KLダイバージェンス(の  \theta に依存しない項を除いたもの)を目的関数とするということですか…確かにそれで線形モデル r_{\theta}(x) のパラメータを最適化できそうです。あれでも、一般化KLダイバージェンスではなくてKLダイバージェンスでは駄目なのですか? r_{\theta}(x)p'(x)p(x) もいま確率分布でしょう?

f:id:cookie-box:20180513082908p:plain:w60

r_{\theta}(x)p'(x) は確率分布の近似であって確率分布ではないかな。この積分を常に1にする制約を入れておけば KL ダイバージェンスの最小化でもいいんじゃないかな。

f:id:cookie-box:20180513082851p:plain:w60

r_{\theta}(x) は密度比なので非負ではあるとして、(11.5) 式の J(\theta) の第1項は r_{\theta}(x) が小さければ小さいほど(非負なのでゼロに近いほど)小さくなり、 第2項は逆に r_{\theta}(x) が大きければ大きいほど小さくなるので、これらが上手くバランスするところに最適な r_{\theta}(x) があるんですよねきっと。ただ、この式を最適化するときに第1項を1に等しくするという制約は自ずとは入らないですよね? 150ページの「すなわち、問題 (11.5) は、制約  \langle r_{\theta} \rangle _{\mathcal{D}'} =1 のもとで訓練データに対する異常度を最小にする最適化問題と解釈できます」というの、ここで新たにそのような制約を課したんでしょうか?? 厳密に1に等しくならなければならないというのではなく単に第1項が r_{\theta}(x) に制約を課していると言いたかったんでしょうか?? よくわかりません…。

つづきは後で

直感 Deep Learning: 5章メモ(単語分散表現)(その3)

以下の本を読みます。

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

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

Amazonで詳しく見る
by G-Tools
キャラクターの原作とは関係ありません。本読みメモですがよく脱線します。誤りがありましたらご指摘いただけますと幸いです。
前回:その2 / 次回:まだ
f:id:cookie-box:20180305232608p:plain:w60

5章の5.3節まで読みましたが、5.4節は実際に単語分散表現を学習する場合はこれらのようなやり方があるといった紹介なので、手を動かしてみた方がよさそうですね。そもそも、ここまで登場した単語分散表現モデルも、手元で実装してみませんか?

f:id:cookie-box:20180305231302p:plain:w60

じゃあまず Skip-gram からだな。142~144ページにコードが載ってるからこれを実行するだけ…あれっ、ど初っ端の一行目から、keras.layers から Merge という名前のオブジェクトはインポートできませんって言われたんだけど! どういうことなの!?

f:id:cookie-box:20180305232608p:plain:w60

何? keras のバージョンの違いでしょうか…この本の要求バージョンは xii ページによると 2.1.6 ですね。プロデューサーさんのマシンに入っているのは…2.2.2 ですね。

f:id:cookie-box:20180305231302p:plain:w60

えーどうする? 勝手にプロデューサーの環境の keras のバージョン下げる?

f:id:cookie-box:20180305232608p:plain:w60

それはやめた方がいいんじゃ…。そもそも、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]]

これを応用すれば Skip-gram モデルは容易に実装できるはずです。せっかくなのでクラスにしてみましょう。Skip-gram が正例か負例か判定するモデルなので、SkipGramDiscriminator という名前にしてみました。

# -*- 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 ] ]

まだ学習データを準備していないので正しく学習できるか試行できていませんが、モデル構造は意図通りなのではないでしょうか。

f:id:cookie-box:20180305231302p:plain:w60

そっか、Skip-gram の学習データって、コーパス内の単語に ID を割り振って、各IDのペアに正例か負例かのラベルを貼らないといけないんだよな。いちいち手でやってらんないし、学習データを生成する関数も書いた方がよさそうだな。

f:id:cookie-box:20180305232608p:plain:w60

それについては、143ページで紹介されている keras の skipgrams 関数がそれをやってくれるのでしょうね。ただ、英語コーパスを使用する場合なら本の通りでよさそうですが、もし日本語コーパスを使用したいならもっと特別な前処理が必要でしょうから、その方法を自分で調べる必要がありそうですね。今回は学習データに何をつかいましょうか?


2018-10-14 追記
f:id:cookie-box:20180305231302p:plain:w60

もうめんどくさいから機関車トーマスでいいよ。

f:id:cookie-box:20180305232608p:plain:w60

トーマスの原著(英語)があればいいのですが、手元には日本語版しかないので、ではやはり日本語用のプレ処理をしましょう。以下のリンクを参考にしました。

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]])
['機関', '車', 'トーマス', 'は', '大きな', '駅', 'で', 'はたらい', 'て', 'い']
機関 , 車
機関 , トーマス
車 , 機関
車 , トーマス
車 , は
トーマス , 機関
トーマス , 車
トーマス , は
トーマス , 大きな
は , 車

f:id:cookie-box:20180305231302p:plain:w60

…なあジュン、「機関車」が「機関」と「車」に分けられるのってどうなの? 機関車は機関車だろ。

f:id:cookie-box:20180305232608p:plain:w60

それは僕も結構そう思ったんですよね…。


2018-10-22 追記
f:id:cookie-box:20180305232608p:plain:w60

まあ形態素解析が多少意図通りでない点は置いておいて、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

ここで1点取り扱いがわからなかったのですが、本の143ページでは1つの文章のみから Skip-gram 対を抽出していますが、実際のコーパスには文章が複数含まれているはずです。その場合、1文1文ずつ skipgrams 関数を適用すると、負例としてサンプリングされる Skip-gram 対が実は他の文章では正例であったということがあるかもしれません。上のスクリプトでは、まずは1文1文ずつ全ての文章に対して Skip-gram 対を得て、最後にもし正例にもなっている負例が含まれていれば除去するということをしています(ので、最終的な正例の数と負例の数が微妙にずれています)。 しかし、どう処理するのが一般的なのかわかりません。

f:id:cookie-box:20180305231302p:plain:w60

ふーん? まあでも、学習データができたんなら早速学習してみようぜ!

f:id:cookie-box:20180305232608p:plain:w60

そうですね。例えば上のスクリプトに対して、上で実装したニューラルネットワークを読み込んで、抽出した学習データをそのまま渡せば学習できますよ。

# -*- 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)

(次回があれば)つづく

直感 Deep Learning: 5章メモ(単語分散表現)(その2)

以下の本を読みます。

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

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

Amazonで詳しく見る
by G-Tools
キャラクターの原作とは関係ありません。本読みメモですがよく脱線します。誤りがありましたらご指摘いただけますと幸いです。
前回:その1 / 次回:まだ
f:id:cookie-box:20180305232608p:plain:w60

単語分散表現とは、単語間の関係が反映された単語の数値ベクトル表現を得ることでしたね。

f:id:cookie-box:20180305231302p:plain:w60

今回は5.2節の word2vec からか。word2vec には CBOW と Skip-gram というアーキテクチャ?があって、前者では文脈語から中心語を予測して、後者では中心語から文脈語を予測する…あのさ、

  • word2vec 自体は何を指すの? 「CBOW と Skip-gram があります」じゃ説明になってなくない?
  • 文脈語と中心語って何?
  • なんか単語から単語を「予測する」って言ってるけどさ、いまやりたいのは単語を数値ベクトルにすることだよね? なんで予測始めちゃったの??

f:id:cookie-box:20180305232608p:plain:w60

CBOW と Skip-gram の具体的な手順を追ってみれば自ずとわかるでしょう。本でも英語の童話の文を例に挙げられていますが、ここでは以下の文章で考えてみます。機関車トーマス (汽車のえほん 2) | ウィルバート・オードリー, レジナルド・ドールビー, 桑原 三郎, 清水 周裕 |本 | 通販 | Amazon の最初の文章ですね。

「タンク式機関車のトーマスは大きな駅ではたらいていました」

f:id:cookie-box:20180305231302p:plain:w60

なんでトーマス!?

f:id:cookie-box:20180305232608p:plain:w60

プロデューサーさんがはまっているので。上の日本語文を形態素解析すると以下のようになると思います。実際にやっていないので脳内 tokenizer ですが。 11個の単語がありますので、1~11のIDを付けておきます。

タンク式機関車12トーマス34大きな567はたらい89いまし1011

f:id:cookie-box:20180305231302p:plain:w60

なんか1個目の単語から聞き慣れないんだけど。タンク式機関車って何?

f:id:cookie-box:20180305232608p:plain:w60

タンク式というんだから水を貯めるタンクが専用の炭水車ではなく本体に積載されている機関車に決まってるでしょう。物語中では最重要キャラクターのトーマスとパーシーがこのタンク式なんです。

f:id:cookie-box:20180305231302p:plain:w60

そっか…。

f:id:cookie-box:20180305232608p:plain:w60

ここである単語とその周辺の数単語を中心語と文脈語とよぶことにします。例えば、「トーマス」に着目してこれを中心語として、ここから2単語以内にある単語を文脈語とするとします。

タンク式機関車12トーマス34大きな567はたらい89いまし1011
先に Skip-gram の方の手順から追いかけます。Skip-gram では中心語から文脈語を予測します。正確には、以下のようなモデルを学習します。
  • 「中心語:トーマス、文脈語:タンク式機関車」という入力 → OK(1)
  • 「中心語:トーマス、文脈語:駅」という入力 → NG(0)
「タンク式機関車」は「トーマス」から2単語以内にあるので OK だが、「駅」は「トーマス」から2単語以内にはないので NG ということです。予測器というより判定器といった方がしっくりきますね。結果的に予測はしているんですが。そして、この判定器は以下のようにつくることにします。
  1. 中心語を中心語用の Embedding 層で数値ベクトルにする。
  2. 文脈語を文脈語用の Embedding 層で数値ベクトルにする。
  3. 1. と 2. の内積をとる。
  4. 3. に出力の次元数が1の Dense 層をかぶせ、sigmoid で活性化して最終的な判定とする。
    言い換えるとこの層では、中心語と文脈語の内積という1次元の説明変数をつかって「本当に中心語と文脈語のペアでありうるか?」の分離境界をロジスティック回帰するんですね。

f:id:cookie-box:20180305231302p:plain:w60

はいはい質問! Embedding 層って何? 説明がないよね?

f:id:cookie-box:20180305232608p:plain:w60

調べると、例えば全11単語を5次元に埋め込むとしたら Embedding 層は11×5次元の行列であるようですね。何のことはない、各単語がどのような数値ベクトルになるかをそのまま重みとして保持する(ように学習する)層なんですね。まあそれで、中心語側の Embedding 層による数値ベクトル化が、今回得たい単語分散表現に他なりません。

f:id:cookie-box:20180305231302p:plain:w60

あっ、そこを最終的につかうんだ! 予測したいわけじゃないのに予測してたのは、後から Embedding 層を取り出すためだったのか。…でもさ、そもそもなんでそんな予測しようって思ったんだ? なんか天から降ってきた感じしない?

f:id:cookie-box:20180305232608p:plain:w60

そうですね…頭を真っ白にして、「目の前にあるコーパス内の全単語を、単語間の何らかの距離を反映するようにN次元空間に埋め込んでください」と言われたらどうすればいいか考えてみましょう。但し、そのコーパス以外に何の辞書も知識もないとします。その制約下である単語とある単語が近くにあるかどうかの基準は、「登場頻度が似ているか」「同じ文章内に登場しやすいか」「文章内で隣り合って登場しやすいか」「文章内で登場する位置が序盤か中盤か終盤かが似ているか」など色々考えられます。文章内での登場位置に踏み込んだルールであって最も基本的なのはやはり「文章内で隣り合って登場しやすいか」なのではないでしょうか。そしておそらく「隣同士」としてしまうと単語間が近いとみなす基準があまりに狭いので「M単語以内」の方がいいと思います。であれば、「ある単語とある単語がM単語以内に登場しやすいか」を考えることになります。Skip-gram で「中心語」と「文脈語」を別々に Embedding するのはおそらく単語間の距離が非対称だからでしょう。どの単語からみても近い単語というのはありそうですが、それで対称性をもつ距離を入れたらあらゆる単語が近くに集まってしまいそうですよね。…のように考えれば、Skip-gram 分類器を学習しようとするのは素朴な発想なのではないでしょうか。

f:id:cookie-box:20180305231302p:plain:w60

長っ! じゃあ Skip-gram は無理がない発想なんだとして、もう1つの CBOW ってのはどうなの?

f:id:cookie-box:20180305232608p:plain:w60

CBOW は Skip-gram とは逆に文脈語から中心語を予測するということですが、入出力はこうですね。出力されるのは、入力された文脈語群の中心語はどれかという確率分布です。以下の入力だと、「トーマス」のところで大きい確率であってほしいですね。

  • 「文脈語群:タンク式機関車、の、は、大きな」という入力 → 全ての単語上の確率分布
このモデルは以下のようにつくることにします。
  • 各文脈語を共通の Embedding 層で数値ベクトルにする。
  • 全文脈語の平均ベクトルを出す。
  • それを出力の次元が語彙数の Dense(softmax で活性化)で中心語の予測分布にする。
こうやって学習したときの Embedding 層が求める単語分散表現です。ネットワーク構造は Skip-gram のときと多少異なりますが、要は文脈語への Embedding の方を利用しようということですね。

f:id:cookie-box:20180305231302p:plain:w60

…Skip-gram のときの「内積をとる」もそうだったんだけど、こっちの CBOW の「平均をとる」もかなり決め打ちの操作っぽいような? せっかくニューラルネットつかってるのにさ。

f:id:cookie-box:20180305232608p:plain:w60

そうすることで単語空間がユークリッド空間っぽくなるんじゃないですか(適当)。

f:id:cookie-box:20180305231302p:plain:w60

じゃ Skip-gram と CBOW はどっちをつかえばいいの?

f:id:cookie-box:20180305232608p:plain:w60

140ページに、Skip-gram の方は低頻度語の予測に優れ、CBOW の方は高速とあります。確かに CBOW は Embedding 層が1つしかないので学習が速そうですね。ただ予測器の入力が「文脈語群」なので、低頻度語が混ざった入力の場合、低頻度語以外の寄与で正解を当ててしまい、低頻度語の表現が正しく得られないのかもしれません。Skip-gram は、各単語を「ある単語が周囲に登場しそうか」で特徴づけますから、低頻度語であっても表現の学習が疎かにはならなさそうです。

f:id:cookie-box:20180305231302p:plain:w60

なるほど。これで Skip-gram と CBOW は終わって…147ページの真ん中らへんの「類似した単語がひとかたまりになります」ってどういうこと?

f:id:cookie-box:20180305232608p:plain:w60

例えば…「[ X ] はバンドに欠かせない弦楽器である」という文章の [ X ] には「ギター」も「ベース」も入りますよね。なのでありうる文章をじゅうぶん含んだコーパスで学習すれば Skip-gram や CBOW で「ギター」や「ベース」は近くなるはずです。次に「High×Joker の[ X ] 担当は[ Y ] である」という文章を考えると、[ X ] に応じて[ Y ] には「秋山隼人」「榊夏来」が入るはずですが、「ギター」と「ベース」がほとんど近いならば先ほどと同様の原理で「秋山隼人」と「榊夏来」も近くなるはずです。ということは、「秋山隼人→ギター」「榊夏来→ベース」という2つのベクトルはほとんど似ているはずなんです。このベクトルはさしずめメンバーと担当楽器を結ぶベクトルですね。

f:id:cookie-box:20180305231302p:plain:w60

おお! なんか熱い!

f:id:cookie-box:20180305232608p:plain:w60

となると、「榊夏来 - ベース + ギター = 秋山隼人」のような計算式も成り立ちそうです。

f:id:cookie-box:20180305231302p:plain:w60

…それは違くない? その計算結果は俺じゃなくて、ギター持ったナツキじゃん。

f:id:cookie-box:20180305232608p:plain:w60

左辺はナツキからベース取り上げてギター持たせるって意味じゃないんで…。

f:id:cookie-box:20180305231302p:plain:w60

次の 5.2.4 節はどんな話?

f:id:cookie-box:20180305232608p:plain:w60

現実に単語分散表現を学習するときには Keras より gensim などが便利でしょうという話ですね。

f:id:cookie-box:20180305231302p:plain:w60

Keras の本なのに!?

f:id:cookie-box:20180305232608p:plain:w60

gensim にはある単語に類似度が高い上位の単語を出せる関数や、単語間の類似度を出せる関数が備わっているようですね。

f:id:cookie-box:20180305231302p:plain:w60

この類似度って具体的に何を計算しているんだろう?

f:id:cookie-box:20180305232608p:plain:w60

実際に使ってみて数値表現から検算してみるとよさそうですね。


2018-10-07 追記
f:id:cookie-box:20180305231302p:plain:w60

5.3節は GloVe か。なんか word2vec とは総じて何なのかわからないまま GloVe に入っちゃったんだけど…。GloVe では「文脈」というものを考えるのか。LSA の特異値やトピックモデルのトピックのようなものなのかな…いや違うな。むしろ LSA やトピックモデルでいう文書に相当するのがここでの文脈(通常は単語列)なのか。単語文脈共起行列をさらに単語特徴行列と特徴文脈行列の積に分解するみたいだし。だから、特異値やトピックに相当するのはむしろ「特徴」か。じゃあ GloVe は、文脈は特徴の(重み付き)組合せでてきていて、特徴は生成する単語分布をもつと考えているのかな。でどうやって行列を分解するのかというと…ランダムな値から出発して誤差(再構築誤差)を減少させるように SGD で学習するのか。この誤差の定義がわかんないから全然わかんないな。

f:id:cookie-box:20180305232608p:plain:w60

論文を参照するように書いてありますからね。

f:id:cookie-box:20180305231302p:plain:w60

まあいいや。153ページの図で単語特徴行列がグレーに塗られているから、これが求める単語分散表現そのものなんだな。152ページと153ページで繰り返し出てくる「word2vec は予測ベースで GloVe はカウントベース」ってどういうことなんだろ?

f:id:cookie-box:20180305232608p:plain:w60

153ページの最下部からは、前者は個別データから学習が始まり後者は全体データの単語共起統計量から学習が始まるというように書いてありますね。word2vec の Skip-gram はある別の単語がある単語の文脈語であるかどうかを個別に学習させますが、GloVe はある単語の文脈のヒストグラムを再現するように一気に特徴を学んでいるようには見えますね。Skip-gram でもバッチサイズが大きければ一気に学習する感じにはなると思うのですが。最初から単語共起統計量をターゲットにする方が精度よく特徴を学べるのかもしれません。154ページに GloVe の方が一般に精度がよいとありますから。ただ並列化しないと計算が遅いのが欠点ということなのでしょうか。この本ではあまり詳細に書いていないのでわかりませんね。

(次回があれば)つづく

雑記: Windows10上AnacondaへのGPU版TensorFlow導入

GPU環境がほしいとかあると思います。ハードのことはよくわからないですが、さすがにGPU環境を手に入れるためにはそれに対応したGPUというものが必要そうな気がします。ちなみにGPUが何なのかはよくわかりません。画面を描画するための並列処理をする演算装置らしいですが、画面を描画するための情報ってそんなに後処理が必要な状態になっているんでしょうか。何もわかりません。

今回やったことは以下です。先に書いておくと大事なのは正しいバージョンの CUDA と cuDNN をインストールすることです。そこガバガバだと駄目です。というかちゃんとそう書いている先達がいらっしゃいました。
WindowsにTensorflowを入れてみる(成功編)

それっぽいGPUが入ったマシンを手に入れます。

手元のマシンにGPUを組み込むことができればGPUを調達すればいいと思うんですが、手元のノート2台はどちらも結構古いので今回はGPUが入っているノートを新調しました。ノートなのは自分には据え置き機を据え置いて作業できる場所がないので致し方ないです。OS は Windows 10 で、NVIDIA GeForce GTX 1060 というGPUが入っているらしく、それっぽさが感じられます。なおこれがどういうGPUなのかはよくわかりません。あとこういうGPUが入ったマシンがほしいときは自作するかBTOショップで購入するらしいです。インターネットに書いてありました。今回はBTOショップで購入しました。

  • それでよくわからないんですが、こういう単体GPUが入っているマシンにおいて単体GPUはいつどのように使われているんでしょうか。基本的に画面描画には単体GPUから使うんでしょうか。機械学習のために単体GPUを使いたい人は「画面描画を頑張っていただかなくてもよいです」とかならないんでしょうか。→ 設定から自分で細かく設定できるんですね。
    Windows10 - グラフィックの設定(アプリごとに使用するGPUを選択) - PC設定のカルマ
    ただグラフィックの設定という項目が見当たらないんですが、単体GPUを画面描画に使用するためのドライバをインストールしていないからかもしれません。あと普通にタスク マネージャーのパフォーマンス タブにGPUが表示されるんですね。みるとGPU 0=内臓GPUIntel HD Graphics 630)は使用されていましたがGPU 1=単体GPUNVIDIA GeForce GTX 1060)は使用率0%でした。
GPU版 TensorFlow を入れる前に用意しておくものを確認します。

どのように環境構築するか何も考えていなかったんですが、よくわからないので Anaconda を入れてそこに TensorFlow を入れることにします。TensorFlow 公式を参照します。
https://www.tensorflow.org/install/install_windows
これを参照すると、GPU対応の TensorFlow を入れるときは以下を入れておくこととありますが、それぞれ何なのかよくわからないので確認しておきます。

CUDA Toolkit 9.0 及び NVIDIA Driver

  • CUDAとはどうもGPU向けのコンパイラであるようです。ライブラリも含まれているようです。通常のCPU向けのコンパイラでは駄目なのかはよくわからないですが、GPUとCPUは構造が違うので、GPUが処理しやすいような命令にした方がよさそうな気はします。NVIDIA Driver というのは、なんかプリンタで印刷するときにもプリンタを動かすドライバがいるのでGPUを動かすドライバが要るんだと思います。
    CUDA - Wikipedia

cuDNN v7.0

CUDA Compute Capability 3.5 以上の GPU (TensorFlow をソースからビルドする場合は 3.0 以上)

  • 最後に GPU です。GPU が要るのは当たり前です。ただし、CUDA Compute Capability なるものが 3.5 以上ある GPU でなければならないとのことです。下のページで確認すると、GeForce GTX 1060 の CUDA Compute Capability は 6.1 なので要求を満たしています。CUDA Compute Capability が何なのかはさっぱりわかりませんが、GPU にもなんか女子力みたいなものがあるんだろうと思っておきます。
    CUDA GPUs | NVIDIA Developer
CUDA Toolkit 9.0 及び NVIDIA Driver をインストールします。

TensorFlow 公式からリンクがある以下のドキュメントの自分に必要な部分を日本語にしたにすぎません。
Installation Guide Windows :: CUDA Toolkit Documentation

  • そもそもGPUの存在を確認します。Windowsマークを右クリックしてデバイス マネージャーを起動、ディスプレイ アダプターを展開して、NVIDIA GeForce GTX 1060 が含まれていることを確認します。
  • Microsoft Visual Studio が必要なのでドキュメントに記載のあるバージョンをインストールします。以下のリンクから Visual Studio Community 2017 をインストールしました。インストール時にワークロードのうち Desktop development with C++ を選択しました。必要な Visual C++コンパイラが含まれていればいいと思うんですがどこまでインストールするのが最小限なのかわかりません。
    ダウンロード | IDE、Code、Team Foundation Server | Visual Studio
  • ドキュメント内のリンクから、CUDA Toolkit 9.0 のインストーラをダウンロードします。リンク先には最新版(この記事を書いている時点では 9.2)のダウンロードボタンがありますが、要求のバージョンはこれではないので、ちゃんと Legacy Releases から 9.0 を探してダウンロードします
    CUDA Toolkit 9.2 Download | NVIDIA Developer
    適切なインストーラをダウンロードしたら画面にしたがってインストールします。再起動の指示があれば再起動します。
    • ここで不用意な人(自分)は最新版 CUDA 9.2 をインストールしてしまったかもしれません。その場合は改めて CUDA 9.0 をインストールすればいいのですが、何も考えずに CUDA 9.0 をインストールすると、The installed version of Nsight Visual Studio Edition is newer than the one to be installed などというメッセージが出て失敗します。これはメッセージの続きの指示通りこれをアンインストールすればよく、コントロールパネルのプログラムのアンインストールから NVIDIA Nsight Visual Studio をアンインストールした上で CUDA 9.0 をインストールし直せば成功します(手元の環境では)。
  • ドキュメントには適切な Driver Model を使うこととあるんですがよくわかりません。Driver 自体は上の手順で一緒にインストールされたと思います。
  • コマンドプロンプトを起動し、
    C:\Users\Cookie>nvcc -V
    と入力してインストールされた CUDA Toolkit のバージョンが 9.0 であることを確認します。
cuDNN v7.0 をインストールします。

TensorFlow 公式からリンクがある以下からインストールすればいいんですが NVIDIA アカウントが必要なので作成します。色々なアンケートに答えるとようやく zip をダウンロードできます。ここでも最新版をダウンロードするのではなく、Archived cuDNN Releases から CUDA 9.0 向けの cuDNN 7.0 系を選択します。ダウンロードした zip は展開しておきます。
NVIDIA cuDNN | NVIDIA Developer
以下の 4.3. Installing cuDNN on Windows にしたがい、ファイルを所定の場所にコピーします。
cuDNN Installation Guide :: Deep Learning SDK Documentation

cuDNN を置いた場所に環境変数を通します(しかし、手元では既に通っていました)。

  • コマンドプロンプトを起動し、
    C:\Users\Cookie>control sysdm.cpl
    と入力すると「システムのプロパティ」ウィンドウが出てくるので「詳細設定」タブの「環境変数」をクリックします。システム環境変数に CUDA_PATH がなければ「新規」から変数名 CUDA_PATH に変数値 C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v9.0 をセットします。既にあればいいです。
Anaconda をインストールし、GPU版 TensorFlow をインストールします。

ここからは TensorFlow 公式の Installing with Anaconda の通りです。
https://www.tensorflow.org/install/install_windows

  • 以下のサイトから Anaconda 5.2 For Windows Installer の Python 3.6 version をダウンロードします。
    Downloads - Anaconda
    基本的に Next をクリックすればインストールできるはずです。
  • Anaconda Prompt を起動し、tensorflow という名前の環境をつくります。別の名前でもいいんですが。今回は Python3.6 なので引数もそうします。
    (base) C:\Users\Cookie>conda create -n tensorflow pip python=3.6
  • そのままつくった環境に入り、GPU版 TensorFlow をインストールします。
    (base) C:\Users\Cookie>activate tensorflow
    (tensorflow) C:\Users\Cookie>pip install --ignore-installed --upgrade tensorflow-gpu
  • インストールできたか検証します。過去に TensorFlow をインストールしたことがあることがある人にはいつものです。sess = tf.Session() を入力した後でぶおおんとなるので早くしろといった気持ちになると思います(手元では)。画面に GeForce GTX 1060 と表示されているので GPU を使っていそうです。
    (tensorflow) C:\Users\Cookie>python
    >>> import tensorflow as tf
    >>> hello = tf.constant('Hello, TensorFlow!')
    >>> sess = tf.Session()
    >>> print(sess.run(hello))
  • GPU とひもづいているかには以下のような確認方法もあるようで、これで確認してもOKでした。
    TensorFlowからGPUが認識できているかを2行コードで確認する - 動かざることバグの如し
Keras もインストールして画像分類タスクで動作確認します(GPU版 vs CPU版)。
  • 先の環境に Keras もインストールします。
    (tensorflow) C:\Users\Cookie>pip install keras
  • 試しにCPU版環境もつくってみます。別の名前にします。こちらの環境で GPU とのひもづけを確認すると当然ひもづいていません。
    (tensorflow) C:\Users\Cookie>deactivate
    (base) C:\Users\Cookie>conda create -n tensorflow-cpu pip python=3.6
    (base) C:\Users\Cookie>activate tensorflow-cpu
    (tensorflow-cpu) C:> pip install --ignore-installed --upgrade tensorflow
    (tensorflow-cpu) C:> pip install keras
  • 動作確認用のタスクを準備します。以下の記事で Augmentation 付きの画像分類をしたことがあったのでこれをもっとたくさんの画像でやってみます。ネットワーク構造その他は適当です。このタスクが GPU と CPU の比較に適しているかは知らないです。
    Keras で少ない画像データから学習したい - クッキーの日記
    • 以下のページから花の画像のデータを拾ってきます(Dataset images)。解凍すると jpg\ 以下に 1360枚の jpg が入っていますが、ファイル名順に80枚ずつ17種類の花の画像になっているので、以下のように振り分けておきます。それぞれの花は Iris とか Pansy とかきちんと名前がありますが、面倒なのでフォルダ名は flower0~flower16 とします。なお自動で振り分けるスクリプトは下の方にあります(Python でやることなのかは知りません)。
      Visual Geometry Group Home Page
      • data/train/flower0/image_0001.jpg(各花の画像80枚のうち60枚は訓練用)
      • data/validation/flower0/image_0061.jpg(各花の画像80枚のうち20枚は訓練用)
    • この花の画像を17クラス分類する畳込みニューラルネットワークを学習します。スクリプトは下の方にあります。PIL というモジュールがないと怒られたら以下の記事を参照して対処します(普通に conda install PIL しようとすると Python を 2.7 系にしようとしてくるので止めてください)。
      Kerasに「PILが無い」と怒られた場合の対策
    • GPU環境(上)とCPU環境(下)でそれぞれ実行した結果は以下です。エポック数は20です。
      Elapsed Time: 296.8[sec]
      Accuracy (train): 95.4%
      Accuracy (test): 54.7%
      Elapsed Time: 1116.6[sec]
      Accuracy (train): 93.2%
      Accuracy (test): 50.6%
      乱数固定できていないので正解率はぶれぶれですがとりあえずGPU版の方がだいぶ早かったです。
動作確認に使用したスクリプトは以下です。

画像データのフォルダ振り分け
jpg フォルダがある場所でこれを実行します。data/train フォルダと data/validation フォルダだけ作ってから実行しましたが作っておかなくても os.makedirs が作ってくれる気がします。なお、jpg フォルダの下から画像ファイルを移動してしまうので注意してください。

import os
import glob
import shutil
imgs = glob.glob('jpg\\*.jpg')
for img in imgs:
    img_num = int(img.replace('jpg\\image_', '').replace('.jpg', '')) - 1
    flower_num = int(img_num / 80)
    is_train = ((img_num - flower_num * 80) < 60)
    dir_name = 'data\\' + ('train\\' if is_train else 'validation\\') + 'flower' + str(flower_num) + '\\'
    if not os.path.exists(dir_name):
        os.makedirs(dir_name)
    shutil.move(img, dir_name + img.replace('jpg\\', ''))

畳込みニューラルネットワークによるクラス分類
昔の記事を2クラス分類から17クラス分類にして微調整したつもりなんですが、突貫で改造したので何か間違っていたらご連絡ください。

# -*- coding: utf-8 -*-
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Activation, Dropout, Flatten, Dense
from keras import backend as K
import time
#import pandas as pd

if __name__ == '__main__':
  img_width, img_height = 256, 256 # 訓練時の画像サイズ
  
  train_data_dir = 'data/train'
  validation_data_dir = 'data/validation'
  nb_train_samples = 60 * 17
  nb_validation_samples = 20 * 17
  epochs = 20
  batch_size = 32
  
  if K.image_data_format() == 'channels_first':
    input_shape = (3, img_width, img_height)
  else:
    input_shape = (img_width, img_height, 3)
  
  # モデル構築
  model = Sequential()
  model.add(Conv2D(32, (3, 3), activation='relu', input_shape=input_shape))
  model.add(MaxPooling2D(pool_size=(2, 2)))
  model.add(Conv2D(32, (3, 3), activation='relu'))
  model.add(MaxPooling2D(pool_size=(2, 2)))
  model.add(Conv2D(64, (3, 3), activation='relu'))
  model.add(MaxPooling2D(pool_size=(2, 2)))
  model.add(Flatten())
  model.add(Dense(128, activation='relu'))
  model.add(Dropout(0.5))
  model.add(Dense(17, activation='softmax'))
  
  model.compile(loss='categorical_crossentropy', optimizer='rmsprop', metrics=['accuracy'])

  # 訓練データのプレ処理の設定: RGB値のスケーリングに加え、シア変形や拡大縮小によって訓練増強
  train_datagen = ImageDataGenerator(rescale=1. / 255, shear_range=0.2, zoom_range=0.2, horizontal_flip=True)
  # テストデータのプレ処理の設定: こちらはRGB値のスケーリングのみ
  test_datagen = ImageDataGenerator(rescale=1. / 255)
  
  # 訓練データをディレクトリ内のデータから随時作成するジェネレータ
  train_generator = train_datagen.flow_from_directory(train_data_dir,
    target_size=(img_width, img_height), batch_size=batch_size, class_mode='categorical', shuffle=True)
  # テストデータ作成をディレクトリ内のデータから随時作成するジェネレータ
  validation_generator = test_datagen.flow_from_directory(validation_data_dir,
    target_size=(img_width, img_height), batch_size=batch_size, class_mode='categorical', shuffle=True)
  
  # 訓練
  time_start = time.time()
  model.fit_generator(train_generator, steps_per_epoch=nb_train_samples//batch_size,
    epochs=epochs, validation_data=validation_generator, validation_steps=nb_validation_samples//batch_size)
  time_end = time.time()

  # 結果確認(のときは増強しない)
  train_generator = test_datagen.flow_from_directory(train_data_dir,
    target_size=(img_width, img_height), batch_size=batch_size, class_mode='categorical', shuffle=False)
  validation_generator = test_datagen.flow_from_directory(validation_data_dir,
    target_size=(img_width, img_height), batch_size=batch_size, class_mode='categorical', shuffle=False)

  print ('Elapsed Time: %.1f[sec]' % (time_end - time_start))
  result = model.evaluate_generator(train_generator)
  print('Accuracy (train): %.1f%%' % (result[1]*100))
  result = model.evaluate_generator(validation_generator)
  print('Accuracy (test): %.1f%%' % (result[1]*100))

直感 Deep Learning: 5章メモ(単語分散表現)(その1)

以下の本を読みます。

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

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

Amazonで詳しく見る
by G-Tools
キャラクターの原作とは関係ありません。本読みメモですがよく脱線します。誤りがありましたらご指摘いただけますと幸いです。
次回:まだ
f:id:cookie-box:20180305231302p:plain:w60

ジュン、これプロデューサーが買ってきた本なんだけど、5章を読むの手伝ってくれない?

f:id:cookie-box:20180305232608p:plain:w60

「直感 Deep Learning」? プロデューサーさんは深層学習の本なんてもう何冊も持ってるでしょう? しかもなんで5章?

f:id:cookie-box:20180305231302p:plain:w60

これ先月出た本で、評判がいいから買ったらしいんだけど、5章以外はもう知ってる話も結構ある感じだったから、まず5章から読みたいんだって。プロデューサーは自然言語処理の勉強したことないからって。

f:id:cookie-box:20180305232608p:plain:w60

じゃあ自然言語処理の本買えばいいだろ。

f:id:cookie-box:20180305231302p:plain:w60

それで、5章(137ページ~)の「単語分散表現」なんだけど、このタイトルからもう割と意味わかんないんだけど、2段落目まで読むと「単語分散表現」っていうのは「テキスト中の単語を数値ベクトルに変換する方法のひとつ」らしいんだ。でも分散表現って何かの説明がないのに「最も基本的な分散表現の手法である」って one-hot エンコーディングが引き合いに出てきて、one-hot エンコーディングには問題点があってとかなっててよくわかんない! いまの目的は何なの!?

f:id:cookie-box:20180305232608p:plain:w60

…ハヤトは、one-hot エンコーディングは理解しているんですか?

f:id:cookie-box:20180305231302p:plain:w60

それはこの本の10ページに書いてあって、6行しかないから読むのすぐだったよ。10個の手書き数字をそれぞれ以下のように表現するのが one-hot エンコーディングなんだろ?
  x手書き数字の"0" = (1, 0, 0, 0, 0, 0, 0, 0, 0, 0)
  x手書き数字の"1" = (0, 1, 0, 0, 0, 0, 0, 0, 0, 0)
  x手書き数字の"2" = (0, 0, 1, 0, 0, 0, 0, 0, 0, 0)
  \cdots
でも、そもそもなんで one-hot エンコーディングするんだろ? 「どの数字か」って情報はせっかく1次元だったのになんで10次元にしちゃったのって感じなんだけど。

f:id:cookie-box:20180305232608p:plain:w60

常に正解が確実にわかるのであれば確かに「どの数字か」という情報は1次元ですね。0~9のいずれかの整数などで表現できます。しかし今は正解に絶対の自信があるわけではありません。例えば数字の1にも数字の7にも見える字を書く人はたまにいますね。1らしいが7らしくもある。そんなときに出力が1次元だったらどうするんです? 1と7の間を取って4を返すわけにもいかないでしょう? 出力が10次元あれば、2番目と8番目の要素を 0.5 にすることでこの状況が表現できます。このように出力を柔軟に10次元とするなら、目指すべき正解も10次元にしておくべきですね。

f:id:cookie-box:20180305231302p:plain:w60

じゃあ、one-hot エンコーディングは、絶対の自信がないときの出力が目指すべき正解になるような表現ってこと…? そういうときに用いるのが分散表現なの?

f:id:cookie-box:20180305232608p:plain:w60

いえ、分散表現の正確な語義は知りませんが、少なくとも狭義には違うと思います。

f:id:cookie-box:20180305231302p:plain:w60

違うの!?

f:id:cookie-box:20180305232608p:plain:w60

おそらく137ページ3段落目の「最も基本的な分散表現」の「分散表現」は「データを数値ベクトルとして表現すること」程度の意味合いと思いますが、一般的に分散表現(distributed representation)といったとき、 局所表現(local representation)と対になる概念としてそういわれていることが多いようにみえます。one-hot エンコーディングはむしろ局所表現ですから、分散表現ではないと思います。以下などを参照させていただきました。

しかし、分散表現の英語 distributed representation は「分布表現」とも読めます。デルタ関数も特殊な確率分布だというような気持ちなら、局所表現も分散表現の特殊なケースといえるのかもしれません。であれば、one-hot エンコーディングも分散表現ですが。

f:id:cookie-box:20180305231302p:plain:w60

ごめんよくわかんない。

f:id:cookie-box:20180305232608p:plain:w60

そうですね、例えば…ハヤトは「ハヤトである」という特徴をもっていますね?

f:id:cookie-box:20180305231302p:plain:w60

え? いやまあ俺は俺だけど…特徴っていうかトートロジーだなそれは。

f:id:cookie-box:20180305232608p:plain:w60

はい、トートロジーです。何であれ、僕たち軽音部5人をそのように特徴付けるなら、僕たちはそれぞれ以下のような5次元の特徴ベクトルで表現できます。
  x隼人 = (1, 0, 0, 0, 0)
  x = (0, 1, 0, 0, 0)
  x夏来 = (0, 0, 1, 0, 0)
  x四季 = (0, 0, 0, 1, 0)
  x春名 = (0, 0, 0, 0, 1)

f:id:cookie-box:20180305231302p:plain:w60

できるって言われても…これ、one-hot エンコーディング

f:id:cookie-box:20180305232608p:plain:w60

ええ。そして、この one-hot エンコーディングは「分散」していないですね。

f:id:cookie-box:20180305231302p:plain:w60

何が?

f:id:cookie-box:20180305232608p:plain:w60

例えば、僕たち5人を先のような5次元ベクトルで表現して、「任意の2人を入力してその2人の仲のよさを出力するニューラルネットワーク」を学習するとします。

f:id:cookie-box:20180305231302p:plain:w60

なんでだよ、俺たちはみんな仲いいだろ! 不仲がいるみたいな感じやめろ! ちゃんとバンド内で仲良くして!?

f:id:cookie-box:20180305232608p:plain:w60

物の喩えです。しかし、先の表現では意味のある学習などできません。いえ、訓練データを暗記することはできるでしょう。しかし、訓練データを暗記することしかできません。なぜなら、ニューラルネットワークはハヤトについて「ハヤトである」以上の情報を知り得ませんので。そして当然、未知の新しいメンバーが入力された場合にも対応できません。そこで、表現を変えてみます。
  x隼人 = (1, 1, 1)
  x = (0, 0, 1)
  x夏来 = (1, 0, 1)
  x四季 = (0, 1, 1)
  x春名 = (0, 0, 0)

f:id:cookie-box:20180305231302p:plain:w60

何これ、3次元になった?

f:id:cookie-box:20180305232608p:plain:w60

ベクトルの1番目の要素は「担当が弦楽器である」、2番目の要素は「上にきょうだいがいる」、3番目の要素は「留年せず進級している」としてみました。

f:id:cookie-box:20180305231302p:plain:w60

3番目の要素が個人を特定しにかかってるな。

f:id:cookie-box:20180305232608p:plain:w60

こちらが(おそらく狭義の)分散表現です。ハヤトの特徴を「ハヤトである」から「弦楽器担当で、上にきょうだいがいて、留年していない」に「分散」したんです。こうすると、ニューラルネットワークは単なる暗記以上の学習をできる見込みがあります。例えば、「弦楽器担当の人と、上にきょうだいがいない人は特に仲がいいようだ」という情報が抽出できるかもしれません。そして、新メンバーにも対応できる可能性があります。このエンコーディングは未知のメンバーにも適用できますから。もちろん、これは例示的なヒューリスティックエンコーディングなので、実際の分散表現は未知データに対応できるとは限りません。というか、おそらく標準的な単語分散表現では対応できないですね。例えばこの章で紹介されている word2vec や GloVe では、膨大な数の文章(学習用コーパス)を与えて、そこに含まれる膨大な単語を教師なしで所与の次元数にエンコーディングします。なので、得られた表現の各次元にヒューリスティックな意味はありませんから、訓練に用いた文章に含まれていなかった単語のベクトル表現を知ることはできませんね。

f:id:cookie-box:20180305231302p:plain:w60

…なあジュン、この3次元の表現だと、俺とハルナは真逆ってこと?

f:id:cookie-box:20180305232608p:plain:w60

「弦楽器かどうか」「上にきょうだいがいるか」「留年していないか」のみが意味をもつ世界では最も遠くに配置されるというだけです。モデルの学習の上でこれらのみが意味をもつのでは足りないなら、4番目の要素、5番目の要素…をベクトルに追加するべきですね。もっとも、先にも言及したように、少なくともテキスト中の単語の分散表現では、ベクトルの要素の意味を人間的に与えて単語間の距離を決めるのではなく、逆に単語間の距離の手がかりの方を与えて「単語間の距離を適切に表すように空間内に単語を配置するやり方を何らかのアルゴリズムで学習する」と言った方が正しいです。また、いまの例示ではベクトルの各要素が  \{0,1\} のどちらかの値しかとっていませんが、もちろん実際には(計算機の範囲で)自由に実数値をとって構いません。

f:id:cookie-box:20180305231302p:plain:w60

「単語間の距離」って?

f:id:cookie-box:20180305232608p:plain:w60

どんなコーパスを用いて、どんな関係をもつ単語どうしが近いと定義するかによりますが、例えば「ギター」と「ベース」という単語は「近い」んじゃないか、と考えられます。同じ楽器ですし、「ギター」と「カレー」よりは「ギター」と「ベース」の方が近そうでしょう?

f:id:cookie-box:20180305231302p:plain:w60

…でもさ、ハルナの中では「ドラム」と「ドーナツ」が近いかもしれないじゃん。どっちも好きな物?みたいな感じで。

f:id:cookie-box:20180305232608p:plain:w60

そうですね、「春名さんのこれまでの発言」のようなコーパスを使って単語分散表現を学習したら本当にそうなるかもしれません。それはそれでいいです。単語分散表現は用いたコーパスによりますから。

f:id:cookie-box:20180305231302p:plain:w60

ふーん…あ、あとさ、単語って同じ単語でも違う意味があったりするじゃん。例えば、「翼」っていったら、うちの事務所の翼さんかもしれないし、765プロの翼さんかもしれないし、普通に鳥とかの翼の意味かもしれないし。

f:id:cookie-box:20180305232608p:plain:w60

いきなり単語分散表現の弱点を突くのはやめてください。

f:id:cookie-box:20180305231302p:plain:w60

ごめん…。

f:id:cookie-box:20180305232608p:plain:w60

整理しますね。

  • one-hot エンコーディング データを数値ベクトルにする手法であって、現れるデータが N 種類なら one-hot な N 次元のベクトルにするだけというもの。
    • なので、エンコーディングする対象のデータ集合さえあればベクトル表現が得られる。
    • ベクトルの各次元の意味は、「i 種類目のデータである」でしかない。
    • このエンコーディングをする目的は、多クラス分類問題においてモデルが出力すべきお手本をつくるためといった向きが大きい(はず)。
  • 分散表現: データを数値ベクトルにする手法であって、データ間の距離(類似性)を考慮した上でベクトル化する=空間内に配置するもの(具体的なアルゴリズムは色々ある)。
    • なので、エンコーディングする対象のデータ集合だけでなく、データ間の距離を示唆する情報も必要である(現実の場面では、エンコーディングする対象の単語たちが文章セットとして与えられるのでこれが達成される=例えば、文章の中で5単語以内の距離に出てくる頻度が多い単語どうしは距離が近いと定義するなど)。
    • ふつうベクトルの各次元にはヒューリスティックな意味はない(アルゴリズムによる)。
    • このエンコーディングをする目的は、データ(多くの場合、単語)を「その意味上の距離を保ったまま」モデルが扱える数値ベクトルにするためである(はず)。そうして得られた数値ベクトル表現は機械学習モデルの入力にも出力にもなりうるが、入力になることが多いと思われる。例えば以下のような使用シナリオが考えられる。
      • 文章セット全体から、登場する各単語を適当な分散表現手法で数値ベクトルにする。
      • 「各文章(=数値ベクトル列)→ その文章がポジティブかネガティブか」のようなRNNを教師あり学習する。
僕たちが機械学習で扱うデータの中で、「画像」や「音声」などは(何らかのセンサーを通して)直ちに実数の多次元配列です。しかし文章は違います。もちろん各文字に文字コードなどはあるでしょうが、文字コードの羅列は単語の意味を反映しません。それを無視した学習は(できなくはないですが)有効ではないでしょう。単語の意味を考慮した数値表現を得ることが必要です。それがいまの目的ですね。

f:id:cookie-box:20180305231302p:plain:w60

目的はわかったよ。それで、137ページの下の方に、単語分散表現以前の単語のベクトル化手法みたいなのが出てきて、「これらの手法は文章中の単語に着目しており、単語それ自体の意味を捉えようとする単語分散表現とは異なる手法になります」「単語分散表現は、文章を前提としそれを文脈とする従来の(中略)手法とは異なり、単語自体を文脈として使用します」ってあるんだけど、この辺も抽象的でよくわかんなくない? 単語を文脈として使用って何?

f:id:cookie-box:20180305232608p:plain:w60

本のタイトルが「直感」だから深く考えなくても。きちんと捉えたいなら、列挙されている単語分散表現以前の手法を追うべきでしょう。リンクも書いてありますし、調べてみましょう。

  • TF-IDF: 各単語を「全文書中でその単語が頻度ベースで占める割合」と「全文書中でその単語が出てくる文書の割合の逆数の対数」の積であるスカラーで表現する手法のようですね。
  • 潜在意味解析(LSA): (i,j) 成分が単語 i の文書 j における出現頻度であるような行列を特異値分解して、特異値が大きい方から k 個までに対応する左特異ベクトルと右特異ベクトルだけ残すという操作をして先の行列のランクを  k に落として、i 番目の単語を「k 本の左特異ベクトルの i 番目の要素をつなげたベクトル」、j 番目の文書を「k 本の右特異ベクトルの j 番目の要素をつなげたベクトル」にエンコードするようです。もっとも、そのような数値ベクトル表現を得て、単語や文書をクラスタリングすることが目的のようですが。
  • トピックモデル: 「トピック」という隠れ変数を考えるようですね。各文書はいくつかのトピックの重ね合わせを生成して、各トピックは単語分布を生成すると仮定して、「各文書のトピックの混合比」と「各トピックの単語分布」を推定するようです。なので、各単語について得られる数値表現は「各トピックでどれだけの出現確率をもつか」のようなベクトルになりますね。トピックを軸に各単語や各文書を解析するようです。具体的なアルゴリズムは色々ありうるようですが。Wikipedia にあまり具体的なことが書いていなかったので2つ目のページも参考にしました。LSA において、各右特異ベクトルが確率的に生成された各文書がそのトピックをどれだけ強くもっているかのベクトル、各特異値がトピック、各左特異ベクトルが確率的に生成されたトピック毎の単語分布、になるようにやり直そうとするとトピックモデルになりますね。Wikipedia のアニメーションは、これはトピックを見出す1つの方法といった感じなのですかね。
ただこれらが単語分散表現に比して何を欠いているのかというのは結局単語分散表現の方も追わないとわかりませんね。ただ word2vec と GloVe を流し読みした範囲ですぐわかるのは、上の従来の手法は「この単語とこの単語は文書内で隣り合って出現することが多い」という情報を使っていませんね(LSA やトピックモデルでは「同じ文書に現れやすい」というレベルでは考慮していますが)。…言い換えに近くなってしまいますが、137~138ページの「単語それ自体の意味を捉える」「単語自体を文脈として使用する」とは、「コーパス内での出現頻度以上の単語間の関係を織り込んだ単語空間を構成する」ということと考えます。TF-IDF は単語の数値表現ですが出現頻度に寄りすぎですし、LSAとトピックモデルは特異値やトピックを通した相関は表現しているんですが、結局各文書における出現頻度に基づくクラスタリングのようなものなので、文書の構造などには立ち入れないと思います。

f:id:cookie-box:20180305231302p:plain:w60

ジュンごめん、あのさ、まだ5章の序文なんだけど…。

f:id:cookie-box:20180305232608p:plain:w60

いいじゃないですか、気付きがあったなら全部回収しておけば。それに、5.1節はもういいでしょう。単語間の関係を保った数値ベクトル表現がほしいって話だし。5.2節も word2vec の具体的なアルゴリズムの解説なので、適当なコーパスで実際にやってみればいいですよ。その後も GloVe の紹介と実用場面での分散表現の得方で、1つ目は完全に自前で分散表現を学習する、2つ目は学習済みの重みで初期化した上で学習する(ファインチューニング)、3つ目は学習済みの重みをそのまま利用するやり方ですね。

f:id:cookie-box:20180305231302p:plain:w60

かなり内容あるじゃん! あと、word2vec を試してみるのに使えるデータってある? 本では1つの文章しか出てきてないっぽいし。

f:id:cookie-box:20180305232608p:plain:w60

ハヤトのソロ曲の歌詞とかでいいじゃないですか。

f:id:cookie-box:20180305231302p:plain:w60

なんで!?

(次回があれば)つづく