nykergoto’s blog

機械学習とpythonをメインに

本のクラスタリングをやってみよう - 吾輩は猫であるに近い本は何なのか

最近理論よりなことばかりやっていたので今回は実際のデータを使った解析をやってみます。

今回使うデータは、読書メーターからクロールさせてもらって作成した、ユーザーに紐付いた読書履歴のデータです。ユーザーごとに [だれの, どんな] 本を読んだかがわかるようなデータになっています。一例は以下のような感じです。

アガサ・クリスティー   おづ まりこ    トマス・H・クック ムア・ラファティ    川口俊和    ジョナサン・オージエ  村田 沙耶香    岡崎 琢磨   米澤 穂信   ピエール・ルメートル  金内 朋子

この人はミステリーが好きなのかもしれませんね。 先の例は作者でしたが、これと同じように本のタイトルも取得しています。 取得した本の数(累積)は 100万冊, ユーザー数は 2500 と気づいたら案外大きいデータセットになっていました。 このうち今回は本のタイトルのデータを使って、本のクラスタリングをやってみたいと思います。

github.com

方法

さてどうやってクラスタリングをやっていくか、ということですが、基本的なアイディアは「読書の履歴にたいする各本は、文章に対する単語の関係と似ているのではないか」という仮説です。

ユーザーは一定の嗜好パターンを持っていて、それに基づいて読む本を決めているはずです。 これは文章がある意味合い(例えば新聞記事とかラブレターとか)を持っていて、その意味合いによって出現する単語の分布が異なる、ということによく似ています。 このことから、文章と単語の関係から単語の意味表現を得たり、クラスタリングを行うような手法は、読書の履歴から本の特徴を得る場合にも用いれるはずです。はず。

以上の仮説に基づいて文章のクラスタ分類でおなじみの LDA (Latent Dirichlet Allocation) と Word2Vec を使います。

LDA

まずは LDA をやってみます。 LDAは文章中に出てくる単語の回数をカウントして文章の特徴とする BoW (Bag of Words) を用いるモデルです。

例えば、僕が文章を書くことを考えてみます。 僕には文章の嗜好や知識の偏りがあるので、野球や数学の話題は出て来るでしょうが、裁縫に関する話題はあまり出てこないように思われます。 このような話題のことをトピックと呼んでいます。 トピックモデルでは「文章には何らかの話題の偏りがある」ということを仮定します。

そして文章中に現れる単語は、文章に紐付けられたトピックのどれかから生成されるものと考えます。すなわち、すべての単語はそれに紐付いたトピックを持っている、と考えます。 またトピックは、どの単語がよく現れるか、また現れにくいかという分布を持っていて、この分布はトピックごとに異なっています。

例えば仮に野球を表すであれば バット, ヒット といった単語の確率が高くなっていて、反対に シュート という単語は低い、といった感じです。 このトピックごとの単語のかたよりも学習していきます。

トピックモデルはグラフィカルモデルの有用さを示すいい例なので数式をたどると面白いかもしれません。僕はベイズとグラフィカルモデル大好きなので、LDA大好きです。 こんな雑な説明でもこれから勉強したい!となった方は、ぜひさとういっせい先生の本をおすすめします。とても丁寧に式を追っていて、数式の気持ちというか「なぜこれをやってるんだろう」を丁寧に説明してくれていて、数式の途中でまいごになることが少なく、おすすめです。

www.amazon.co.jp

結局 LDA をして嬉しいのは何かというか「文章がどのような偏りを持ったトピックで生成されたのか」という情報を知ることが出来るということです。 この情報は先の 野球トピック のようなものに相当しています。 すなわちこちらから何も情報を与えなくても(文章だけ与えれば) バットとヒットは同じ文脈で出てきやすいんだな、というようなクラスの情報を得ることが出来るわけで、それはとてもハッピーですね。

というわけで早速やってみましょう。 feature.py の関数を用いてデータをロードした後に, 10個のトピックに分類してみます。

from features import load_feature, compile_corpus_and_dictionary
from lda import LDATrainer

data = load_feature(root_path="./data/raw/", target="title")
corpus, dictionary = compile_corpus_and_dictionary(data, no_below=5, save_to="checkpoints/dictionary.txt")

lda_trainer = LDATrainer(output_dir="./checkpoints/")
lda_trainer.run(corpus, dictionary, topics=10)  # 学習の実行

# 上位のトピックを保存
lda_trainer.show_topics(save_to_dir="./checkpoints/")

出てきたトピックの分布は以下のようになりました。 10個のトピックをすべて載せてもよいのですが、ちょっと見にくいのと、あとでも述べますがあまりトピックごとに差分が見えなかったので2つだけ載せています。

topic 1

[('阪急電車(幻冬舎文庫)', 0.0013286450514788421),
 ('和菓子のアン(光文社文庫)', 0.0010029719922129846),
 ('永遠の0(講談社文庫)', 0.00095220966510543164),
 ('氷菓(角川文庫)', 0.000931927887614856),
 ('火花', 0.00088232395541623144),
 ('レインツリーの国(新潮文庫)', 0.00087065130728654241),
 ('ビブリア古書堂の事件手帖3~栞子さんと消え…', 0.00083625459565052221),
 ('陽だまりの彼女(新潮文庫)', 0.00078834353368666273),
 ('夜は短し歩けよ乙女(角川文庫)', 0.00076325430885853943),
 ('ぼくは明日、昨日のきみとデートする(宝島社…', 0.00076090024369739853)]

topic2

[('永遠の0(講談社文庫)', 0.0012030737598141335),
 ('阪急電車(幻冬舎文庫)', 0.0011685757490659541),
 ('イニシエーション・ラブ(文春文庫)', 0.0011032171029623371),
 ('舟を編む', 0.00093492775265262469),
 ('西の魔女が死んだ(新潮文庫)', 0.00085344844568903389),
 ('夜のピクニック(新潮文庫)', 0.00084074342930131642),
 ('図書館戦争図書館戦争シリーズ(1)(角…', 0.00079957001036084336),
 ('カラフル(文春文庫)', 0.00078677125722370774),
 ('死神の精度(文春文庫)', 0.00077641049565474061),
 ('ぼくは明日、昨日のきみとデートする(宝島社…', 0.00076590742659798081)]

なんだかどちらのトピックも大差ない内容になってしまっていますね。 本のタイトルをクラスタに分けたいというモチベーションだったので、その意味でこれは失敗になってしまいました。 その後も色々と試行錯誤はしてみた*1のですが、あまり良い結果が得られなかったので LDA は宿題に回して Word2Vec の方に移ります。

Word2Vec

次は word2vec をやってみます。

word2vec は単語の分散表現 (word embedding) を獲得手法と表現されることが多いように、単語が何の意味を持っているのかをベクトルで表したい、ということがモチベーションの手法です。 *2

word2vec には Continuous Bag of Words (CBOW) と skip-gram の二種類がありますが、発想はほとんど同じです。 CBOW では周辺の単語から中央の言葉を予測していて、 skip-gram では中心の単語から周辺の単語の予測をします。そしてどちらの手法もその予測器は隠れ層1の auto-encoder です。 数式にすると中心の単語が入力になる skip-gram の目的関数は以下のようなります。

$$ {\min_{W_1, W_2}} {\rm Loss}(W_1, W_2) = \sum_{i \in {\rm train}} \sum_{j \in {\rm surround(i)}} {\rm CrossEntropy} ( W_2^{T} ( W_1 x_i ), y_j) $$

このとき $x, y \in \mathbb{R}^n$ は単語を表す one-hot-vector で $n$ は文章中の語彙数を表しています。また $W_1, W_2 \in {\mathbb R}^{m \times n}$ は隠れ層の次元数が $m$ の auto-encoder のパラメータです。

CBOW の場合には入力が周辺の単語になりますから、上の数式で $x_i$ と $y_j$ が入れ替わったような形になります。

こうして得られた auto-encoder の隠れ層の部分、すなわちある単語 $x$ にたいして $W_1 x$ で計算される $m$ 次元ベクトルが、単語の情報をより低次元で表現できるベクトル(これを単語の分散表現と呼びます)になっていると解釈できます。

現在はそれにいろいろな改良やアイディアが付け加えられています。 例えば、ロス関数の計算において発生する softmax の計算が語彙の数 $n$ のオーダーで必要なことが計算のボトルネックになっているとして、すべての単語に対する計算をさぼって、近似的に softmax を計算してしまおうというネガティブサンプリングという手法もそのひとつです。 これにより最適化における大幅な速度向上を達成しました。 また、word2vecの入力に文章のidを加えて、文章も分類してしまおうという方法 (doc2vec) も提案されています。

python で word2vec をやりたいときは chainer や keras などのディープなフレームワークを使ってもできますが内部が C で実装されていて速度の早い gensim を用いるのが良いと思います。 特にGPU環境でなければ。(一回 word2vec を実装した時には cpu しかなく全く使い物になりませんでした)

このライブラリ関数やクラスの定義の仕方に若干の癖があるという難点はありますが cpu 環境でも十分実用的な時間に計算できるという点はとても魅力です。 では実際に計算をやってみます。 今回は僕が作成した訓練のための V2WTrainer クラスを用いていますが, 実装見てもらうとわかるように内部で大したことはやっていないので直接 gensim を使うときもこんなのりで出来るんだーという感じで見てもらえれば。

from word_to_vec import W2VTrainer

w2v_trainer = W2VTrainer(output_dir="./checkpoints/")
w2v_trainer.run(data)

結果

Word2Vec では単語のベクトルが得られるので、内積を取ることでその近さを知ることが可能です。よってある単語に最も近い単語、最も遠い単語を計算できます。 今回は単語ではなく本の名前なので、ある本と似た本、全く似ていない本を計算出来ることになります。

ここでようやくタイトルの「吾輩は猫である」が出てきます。吾輩は猫であるも色々とバージョンがある(ハードカバーとか文庫版とか)ので、まずは最も読まれていた新潮文庫版で試します。 吾輩は猫であるとの類似度の計算結果が以下になります。

我輩は猫である(新潮文庫) に近い本・遠い本

近い本

[('三四郎(新潮文庫)', 0.8102374076843262),
 ('こころ(新潮文庫)', 0.7891632914543152),
 ('門(新潮文庫)', 0.7769464254379272),
 ('草枕(新潮文庫)', 0.7562223672866821),
 ('虞美人草(新潮文庫)', 0.7479068636894226),
 ('阿部一族・舞姫(新潮文庫)', 0.7449460625648499),
 ('坊っちゃん(新潮文庫)', 0.7428607940673828),
 ('それから(新潮文庫)', 0.7405833005905151),
 ('行人(新潮文庫)', 0.7404835224151611),
 ('道草(新潮文庫)', 0.7297923564910889)]

遠い本

[('死神姫の再婚-五つの絆の幕間劇-(ビーズ…', 0.13127756118774414),
 ('絶園のテンペスト2(ガンガンコミックス)', 0.1260046362876892),
 ('死神姫の再婚-始まりの乙女と終わりの教師-…', 0.1172419935464859),
 ('信長協奏曲1(ゲッサン少年サンデーコミッ…', 0.09469656646251678),
 ('レベルE(上)(集英社文庫―コミック版)', 0.09304021298885345),
 ('絶園のテンペスト(5)(ガンガンコミックス)', 0.09189711511135101),
 ('信長協奏曲7(ゲッサン少年サンデーコミッ…', 0.09187307208776474),
 ('信長協奏曲2(少年サンデーコミックス)', 0.08920496702194214),
 ('天使1/2方程式1(花とゆめCOMICS)', 0.08579257875680923),
 ('闇の皇太子偽悪の革命家(ビーズログ文庫)', 0.08444569259881973)]

予想以上に綺麗に分離してくれました。 同じ夏目漱石の文庫本が多く並んでいます。 また森鴎外が入っているのも納得できます。 反対の遠い本ではコミックス系が多くなっていて、これも納得できる結果になりました。 LDA よりはうまくいってそう…

いい感じだったので森見登美彦の「四畳半神話大系(角川文庫)」でもやってみました

四畳半神話大系(角川文庫)に近い本・遠い本

近い本

[('夜は短し歩けよ乙女(角川文庫)', 0.8690416216850281),
 ('太陽の塔(新潮文庫)', 0.8665028810501099),
 ('美女と竹林(光文社文庫)', 0.8509087562561035),
 ('有頂天家族(幻冬舎文庫)', 0.8440086841583252),
 ('新釈走れメロス他四篇(祥伝社文庫も…', 0.8257927894592285),
 ('ペンギン・ハイウェイ(角川文庫)', 0.8094362020492554),
 ('([も]3-1)恋文の技術(ポプラ文庫)', 0.8090072274208069),
 ('きつねのはなし(新潮文庫)', 0.7985487580299377),
 ('四畳半王国見聞録(新潮文庫)', 0.7920351028442383),
 ('宵山万華鏡(集英社文庫)', 0.7382686734199524)]

遠い本

[('血界戦線4―拳客のエデン―(ジャンプコ…', 0.07178327441215515),
 ('光とともに…13―自閉症児を抱えて', 0.06594447791576385),
 ('王家の紋章(17)(Princessc…', 0.0623263344168663),
 ('VIVO!1(マッグガーデンコミックス…', 0.05496706813573837),
 ('王家の紋章(15)(Princessc…', 0.05385550856590271),
 ('光とともに…14―自閉症児を抱えて', 0.052545785903930664),
 ('彼氏彼女の事情(15)(花とゆめCOMI…', 0.050349362194538116),
 ('王家の紋章(16)(Princessc…', 0.050084952265024185),
 ('Vassalord.6(マッグガーデンコ…', 0.04914539307355881),
 ('彼氏彼女の事情(16)(花とゆめCOMI…', 0.0489741712808609)]

こちらも上位がすべて森見登美彦氏で埋まっていますね。 同じ作家で固まることがよく起こるというのは、作家によらず、特定の作家の本を読むと同じ作家の本を再び読む、ということが言えるのかもしれません。まあそりゃ当たり前か。 一度気にいると同じ作家の本ばかり買ってしまう、という感覚が可視化された、ということでしょうか。

ここで「ハードカバーの方がコアな客が多いから、同じ作家で固まりやすいのでは(類似度がほぼ1になるとか」と思ったので検証してみます。 ハードカバーの四畳半神話大系の類似度は以下のようになりました。

[('きつねのはなし', 0.7952536344528198),
 ('リンダリンダラバーソール(新潮文庫)', 0.7498961091041565),
 ('新釈走れメロス他四篇', 0.7390938401222229),
 ('夜は短し歩けよ乙女', 0.7354095578193665),
 ('太陽の塔', 0.7339460849761963),
 ('有頂天家族', 0.7336863875389099),
 ('風に舞いあがるビニールシート', 0.7267143130302429),
 ('ぐるぐるまわるすべり台', 0.7245926260948181),
 ('天国はまだ遠く', 0.7150847911834717),
 ('二枚舌は極楽へ行く(FUTABA・NOVE…', 0.7144078016281128)]

「きつねのはなし」は森見登美彦氏ですが、類似度は先よりも落ちています。 二番目の本はしらなかったのでググったのですが大槻ケンヂ氏の本なんですね、知りませんでした。

www.amazon.co.jp

内容紹介
僕らのバンドが、メジャーデビューすることになった! その頃、日本はバンドブームに沸いていた。無名だった若者が、次々とスターになった。ライブ会場は熱狂に満ちた。でも、ブームはいつか終わるものだ。大人たちは、潮が引くように去ってゆく。誰もが時の流れと無縁ではいられないんだ。僕と愛すべきロック野郎たちの、熱くて馬鹿馬鹿しくて切なかった青春を、いま再生する。

普通に面白そう……。 あでもそう思うってことは、ちがう作家だけどその人が面白いと思いそうな本を拾えてるってことなのかもしれないですね。サンプル僕だけであれですが。

文庫版の森見登美彦氏の本も見当たらないのが不思議ですね。これはハードカバーを読む人と、文庫を読む人はそもそも文脈がだいぶ違っていて、あの本はハードカバーだけど今回は文庫までまとう、といった読み方はしないんだなあということが伺えます。

ちなみに吾輩は猫である岩波文庫版になるとだいぶ様相が違っていました

[('「超」文章法(中公新書)', 0.8472597002983093),
 ('無限論の教室(講談社現代新書)', 0.8468811511993408),
 ('天の瞳幼年編〈2〉(角川文庫)', 0.8414252400398254),
 ('日本語の教室(岩波新書)', 0.8320746421813965),
 ('不都合な真実', 0.8299296498298645),
 ('朝の少女(新潮文庫)', 0.8163926601409912),
 ('人生に意味はあるか(講談社現代新書)', 0.8158650398254395),
 ('けものたちは故郷をめざす(新潮文庫あ4…', 0.813538670539856),
 ('DeepLove―アユの物語完全版', 0.812150239944458),
 ('ブラック・ジョーク大全(講談社文庫)', 0.8112159967422485)]

夏目漱石が激減してしまったのが不思議です。岩波文庫でも夏目漱石の本はいくつか出ているのですが…

まとめと今後の課題

  • word2vec の応用の広さすごい
  • 著者のデータでも何かしたい
  • LDA かわいそうなのでなんとかチューニングして結果出してあげたい。ベイズだしね。

*1:トピック数, αの事前分布, 最適化方法などいろいろやったものの徒労に終わる悲しみ

*2:元論文: [1301.3781] Efficient Estimation of Word Representations in Vector Space

ニューラルネットへのベイズ推定 - Bayesian Neural Network

ニューラルネットワーク過学習防止としてDropout という機構が用いられているのはご案内のとおりです。

この Dropout 、見方を変えるとディープラーニングにおける重みのベイズ推定に相当しているのではないか、という内容が Uncertainty in Deep Learning にて述べられていて、この記事ではその内容について解説していきたいと思います。

また末尾では実際にベイズ推定を実装して、予測がちゃんと不確実性を盛り込んだものになっているかどうか、を確認します。

基本的に記事の内容は元の論文(YARIN GAL さんの博士論文です)と同著者の解説ページを元にしています。それぞれ以下からアクセスできますので、解説じゃなくて自分で読みたい!という方はそちらを参考にしてください。個人的には解説も論文もとても読みやい (なんと数式もとても丁寧に記述されています!!) ので、英語が苦手ではない方は原典にあたっていただいたほうが良いかもです。

ベイズ推定とは

はじめにベイズ推定について簡単に。
ベイズ推定で目指す目的とは何でしょうか。

仮定として, $N$ 個の入力データ $X=\{x_1, \cdots. x_N \}$ と、それに対応する出力 $Y=\{ y_1, \cdots, y_N \}$ をすでに観測しているとします。またデータは独立同分布から生成されている, とします。
この状態で、新しい入力 $x$ が入ってきたときに出力 $y$ を予測するという問題を考えます。
ベイズ推定で求めたいのは、データが与えられたときの、隠れ変数の事後分布 $p(w \mid X, Y)$ です。

重みの事後分布さえ入手できてしまえば、それを用いて新しいデータ $\hat{x}$ が与えられたとき、その予測値 $\hat{y}$ が従う分布は以下で計算することが可能です。

$$p(\hat{y} \mid \hat{x}, X, Y) = \int p(\hat{y} \mid w, \hat{x}) p(w \mid X, Y) dw$$

ですから、ベイズ推定の立場にたてば、事後分布 $p(w \mid X, Y)$ をいかにして求めていくか, ということが問題となります。

事後分布の計算

直接事後分布が計算できるような単純なモデルでは、先のアイディアに基づいて事後分布を計算すれば良いです。

しかし、複雑なモデルでは、陽に事後分布が計算できない場合が出てきます。 このような仮定のもとでは、何らかの関数で事後分布を近似する必要があります。
以下ではあるパラメータ $\theta$ を持つ関数 $p_\theta(w)$ を事後分布に近づいけていく, という方法を考えます。
このとき、2つの分布の近さを測る距離として, 2つの確率分布から実数空間へ射影する関数として KL-Divergence (以下では ${\rm KL}$ と表記) を用います。
KL距離は距離空間ではありませんが, 確率分布 $p,q$ に対して, ${\rm KL}(q, p) = 0$ のとき $q=p$ が成立します。よって KL 距離の意味で最小化を行えば, 分布 $q_\theta$ は $p(w\mid X, Y)$ に近づく事が期待されます.
最小化する関数を変形すると、以下のようになります。

$$\begin{aligned}{\rm KL}(q_{\theta}(w) | p(w | X, Y))&=\int q_{\theta}(w) \log \left( \frac{q_{\theta}(w)}{p(w | X,Y)} \right)dw \\&=\int q_{\theta}(w) \log \left( q_{\theta}(w) \frac{p(X,Y)}{p(X,Y|w) p(w)} \right) dw \\&=\int q_{\theta}(w) \left( \log \frac{q_{\theta}(w)}{ p(w)} + \log \frac{p(Y|X) p(X)}{p(Y|X, w) p(X|w)} \right) dw \\&=\int q_{\theta}(w) \left( \log \frac{q_{\theta}(w)}{ p(w)} + \log \frac{p(Y|X)}{p(Y|X, w)} \right) dw \\&= \int q_{\theta}(w) \log p(Y|X) dw - \int q_{\theta}(w) \log p(Y|X,w)dw + {\rm KL} (q_{\theta}(w) | p(w)) \\&= \log p(Y|X) - \int q_{\theta}(w) \log p(Y| X, w)dw + {\rm KL} (q_{\theta}(w) | p(w))\end{aligned}$$

ここで第4行目の変形で $p(X|w)=p(X)$ を用いました。今 Variance Inference を ${\rm VI}$ と表し

$${\rm VI}(\theta) = - \int q_{\theta}(w) \log p(Y|X,w)dw + {\rm KL} (q_{\theta}(w) | p(w))$$

と定義します。すると上記の式は以下のように変形することができます.

$${\rm KL}(q_{\theta}(w) | p(w | X, Y)) = {\rm VI}(\theta) + \log p(X,Y)$$

ここで右辺最終項 $\log p(X,Y)$ は定数ですから, 結局 KL 距離の最小化問題は以下のように書き換えられます。

$$\min_{\theta} {\rm KL}(q_{\theta}(w) | p(w | X, Y)) = \min_{\theta}{\rm VI}(\theta)$$

よって ${\rm VI}$ を $\theta$ に関して最小化することは, KL-Divergence の意味での最小化と一致する事がわかります。
また ${\rm VI}$ の第一項

$$-\int q_{\theta}(w) \log p(Y|X,w)dw$$

の部分は, $0 \le p(Y|X,w) \le 1$ であることを考慮すると必ず正の値です。また得られたデータ $Y$ が現れる確率 $p(Y|X,w)$ の値が高い $w$ に対して、分布 $q_\theta(w)$ も高い値を取るときに小さくなり、データへの当てはまりを表していることがわかります。

一方第二項の

$${\rm KL} (q_{\theta}(w) | p(w))$$

については, 事前分布 $p(w)$ と事後分布の推定密度関数 $q_\theta(w)$ との距離を表しています。これは事前分布からかけ離れた分布へのペナルティを与える項に相当しており、一般に正則化項と呼ばれるものです。(簡単な例で言えば互いにガウス分布であるときこのKLダイバージェンス部分は $L2$ ノルムとなり線形回帰であれば Ridge 回帰とよばれる枠組みになります)
このように自分が提案する分布 $q$ をもとめたい分布 $p$ に近づけていく方法を変分推論 (Variational Inference) とよびます.

ニューラルネットワークに対する既存の VI

つぎにニューラルネットワークに対して Variance Inference を行う既存の枠組みについて振り返ってみます。近年のニューラルネットワークへのベイズ推定のアプローチの一つである Hinston and Van Camp, 1993 では近似する分布 $q_\theta$ にたいして, すべての重みに対して独立したガウス分布を仮定しました。

すなわち $I$ 層の隠れ層を持ち, その各 $i$ 層で重み $w_{ijk}$ を持つネットワークに対して $m_{ijk}, \sigma_{ijk} \in \mathbb{R}_{+}$ とおいたとき

$$q_\theta (w) = \prod_{i, j ,k} N(w_{ijk}\mid m_{ikj}, \sigma_{ijk}^2)$$

と記述することになります。 この分布を仮定した場合の最適化はとてもむずかしく, 論文中では一つの隠れ層を持つニューラルネットワークに対しての実験にとどまっていました。 実際理論的には事後分布の推論にガウス分布を用いると一層の隠れ層をもつニューラルネットに対しては数式上綺麗に解析が行えるものの、実験上性能がよくありませんでした。理由としては、この方法が本来重要である重み同士の関係性を記述できないことなどが挙げられています。

これに対し Barver and Bishop, 1998 では重みに対して混合ガウスを仮定したモデルを提案しました。これによって同一層の重みに対して関連性を考慮することが可能となりました。しかし、その反面計算量が増大してしまうため、複雑なモデル等に対してうまく働きません。

Variance Inference の近似

そのままの形ではなかなか良い結果を得ることができていなかった変分推論ですが、今回は期待値の意味で一致する近似式を用いて最適化することを考えていきます。


まず $X,Y$ は独立であると仮定しているので $p(Y | w,X) = \prod_{i} p(y_i | w, x_i )$ が成り立ちます。これを用いて Variance Inference を変形すると以下のようになります。

$$\begin{aligned}{\rm VI}(\theta) &= - \int q_{\theta}(w) \log p(X,Y|w)dw + {\rm KL} (q_{\theta}(w) | p(w)) \\&= -\sum_{i=1}^N \int q_{\theta}(w) \log p(y_i | x_i, w) dw + {\rm KL} (q_{\theta}(w) | p(w)) \\&= -\sum_{i=1}^N \int q_{\theta}(w) \log p(y_i | f^w(x_i)) dw + {\rm KL} (q_{\theta}(w) | p(w))\end{aligned}$$

ここで $f^w(x_i)$ は重み $w$ を持つモデルに $x_i$ が入力されたときの出力を表します。

この形式を取り扱う上での難しさは, 主に以下の二点にあります

  1. $w$ に関する積分の部分が扱いやすい形式ではない
  2. データ $N$ に対する和を取らなくてはならないため, データの数が多くなると計算が困難になる

この内 2 に関してはすべてのデータのうちで \( M \) 個だけサンプルする (mini-batchを計算する) ことで回避することができます. この近似を行った $\hat{{\rm VI}}$ は以下のようになります。

$$\hat{{\rm VI}} = - \frac{N}{M} \sum_{i \in S} \int q_{\theta}(w) \log p(y_i | f^w(x_i)) dw + {\rm KL} (q_{\theta}(w) | p(w))$$

ここで, $S$ はサンプルされた添字の集合を表します。

しかし理由 1 によりこの計算は難しいままです。

$w$ の積分の近似

$w$ に関する積分を近似することを考えます。 積分の部分を見ると, $q_\theta$ と対数尤度との掛け算になっています。よって $q_\theta$ から $w$ をサンプリングすることができれば積分計算の近似を行うことができます。

しかし, $q_\theta$ の形式には仮定が置かれておらず任意の分布を取ることが可能です。したがってこの分布からサンプリングすることはできません。

そこで $q_\theta(w)$ がパラメータを持たない別の分布 $p(\epsilon)$ を用いて, $w = g(\theta, \epsilon)$ で表現できるという仮定を加えてみます。そうすると分布 $q$ に対する情報がわかっていなくても、分布 $p(\epsilon)$ からサンプルした値 $\hat{\epsilon}$ を用いて提案分布からのサンプル $\hat{w} = g(\theta, \hat{\epsilon})$ を生成することが可能となります。

平均 0 分散 1 のガウス分布 (正規分布) のサンプル値から 任意の分散と平均値をもつガウス分布のサンプルを作成することなどがこれに相当します。

これを用いて先の VI の式中の $q_\theta$ を $p(\epsilon)$ で表現することで, 分布の部分から $\theta$ を取り除きます。

$$ \begin{aligned} \hat{{\rm VI}} &= - \frac{N}{M} \sum_{i \in S} \int q_{\theta}(w) \log p(y_i | f^w(x_i)) dw + {\rm KL} (q_{\theta}(w) | p(w)) \\ &= - \frac{N}{M} \sum_{i \in S} \int p(\epsilon) \log p(y_i | f^{g(\theta, \epsilon)}(x_i)) d\epsilon + {\rm KL} (q_{\theta}(w) | p(w)) \end{aligned} $$

こうなると積分はパラメータを持たない分布 $p(\epsilon)$ からのサンプリングを行うことで効率的に近似をおこなうことが可能になります。 サンプリングをミニバッチのサンプルと同時に行うとし、サンプルされた実現値を $\epsilon_i \sim p(\epsilon) (i = 1, \cdots, M)$ とします。
するとこのモンテカルロ法によって積分が近似された $\hat{{\rm VI}}_{MC}$ は以下のようになります。

$$\hat{{\rm VI}}_{MC} = - \frac{N}{M} \sum_{i \in S} \log p(y_i | f^{g(\theta, \epsilon_i)}(x_i)) + {\rm KL} (q_{\theta}(w) | p(w))$$

このとき $\epsilon, S$ に対する期待値を取ると, $\mathbb{E}_{S, \epsilon} \left[\hat{{\rm VI}}_{MC} \right] = {\rm VI}$ が成立します。

この新しい $\hat{{\rm VI}}_{MC}$ を目的関数として例えば勾配法を用いて $\theta$ について最適化を行えば、期待値を取れば元の Variance Inference を最適化することと同値です。よって最適化の各 $t$ ステップにおいて

$$ \displaystyle \theta_{t} = \theta_{t-1} + \eta \frac{\partial}{\partial\theta} \hat{{\rm VI}}_{MC} $$

によりパラメータ $\theta$ を更新すれば良いことがわかります。

Dropout による学習

ここで一旦 Variance Inference のことをおいておいて, ニューラルネットワーク過学習を抑える手法の一つである Dorpout について考えます。
Dropout とは訓練データが与えられたとき, 各層においてすべての隠れノードを用いて出力を行わず, ランダムに選ばれたノードの値のみを用いて出力をし, backword においても出力に関わったノードの値のみを更新する, という方法です。

単純に一層の隠れ層を持つネットワークを考え, 入力から隠れ層, 隠れ層から出力層への重みをそれぞれ $M_1 \in \mathbb{R}^{n\times m}, M_2 \in \mathbb{R}^{m\times l} $とします. また隠れ層の定数 $b \in \mathbb{R}^{m}$, 活性化関数 $\sigma$ とします。

入力として $x \in \mathbb{R}^n$ のデータが入ってきた場合を考えてみます。Dropout では出力に使う要素を選ぶ処理が入ります。この選択をバイナリベクトル $e_1 \in \{0, 1\}^n$ を使って表すとDropoutを適用した隠れ層 $h$ は 

$$ \begin{aligned} h = \sigma ( (x \bullet e_1) M_1 + b) \end{aligned} $$

となります。 ここで $\bullet$ は要素積 $(x \bullet y)_{i} = x_i y_i $ を表します。$e_1$ の作成は, $n$ 次元上の確率 $p_1 (0\le p_1\le 1)$ のベルヌーイ分布 $Q$ から生成します。*1

同様に隠れ層 $h$ に対しても確率 $p_2$ で要素を0にします。すなわち 先と同様に $m$ 次元のベルヌーイ分布から $e_2 \in \{0,1\}^m$ をサンプリングして一定の隠れ層ノードの値を0にします。すなわち

$$\hat{h} = h \bullet e_2$$

をノードの値であるとします。出力はこの値を用いて

$$\hat{y} = \hat{h} M_2$$

となります。
この $\hat{y}$ は $\bullet e = {\rm diag}(e)$ と変形できることを用いれば以下のように変形できます。

$$ \begin{aligned}\hat{y} & = (h \bullet e_2) M_2 \\&= ( \sigma \left( (x \bullet e_1) M_1 + b \right) \bullet e_2) M_2 \\&= ( \sigma (x ({\rm diag}(e_1) M_1) + b)({\rm diag}(e_2) M_2 ) \\&= \sigma \left( x\hat{W}_1 + b \right) \hat{W}_2 \end{aligned} $$

ここで  {\rm diag}(e_1) M_1 = \hat{W}_1, {\rm diag}(e_2) M_2 = \hat{W}_2 と定義しました。

以上を用いるとニューラルネットワークの出力は確率変数 $\hat{\omega} = \{ \hat{W_1}, \hat{W_2}, b\}$ を用いて

$$\hat{y} = f^{\hat{W_1}, \hat{W_2}, b}(x)$$

と記述できます. 


ながながとゴニョゴニョしましたが, 以上からdropout による mask のかけられた出力も重みの確率変数をもつネットワークの出力として表現できる, ということが確認できました。

Dropout の目的関数

これらの記号を用いてニューラルネットワークの目的関数を記述していきましょう。入力値と正解ラベルから誤差を計算するロス関数を $E$ とおき、ニューラルネットワークが最小化する真の関数を記述すると以下の用になります。

$$L_{dropout} = \frac{1}{M} \sum_{n \in N} E \left( f(x_n), y_n \right) + \lambda_1 ||M_1|| + \lambda_2 || M_2 || + \lambda_3 || b ||$$

ここで $f(x_n)$ は $n$ 番目の入力データに対する出力値を表しています。また右辺第二項以降は重みに対する正則化 (weight decay)を表しています.

実際には $N$ 個すべてのデータを使うことは困難ですから、その中からある一定の大きさのサンプル $S$ を取得します。Dropout ではそれと同時にネットワークに対する mask をかけて出力を作ります。したがってネットワークの出力は確定値 $f(x)$ ではなく、確率変数 $\omega =\{ \hat{W_1}, \hat{W_2}, b\}$ によって決定する確率的な出力となります。よって Dropout を用いたニューラルネットワークのロス関数は

$$L_{dropout} = \frac{1}{M} \sum_{i \in S} E \left( f^{ \{ \hat{W_1^i}, \hat{W_2^i}, b \} }(x), y_i \right) + \lambda_1 ||M_1|| + \lambda_2 || M_2 || + \lambda_3 || b ||$$

となります. ここで $\hat{W_1^i}, \hat{W_2^i}$ はサンプルされた $i$ のデータに対する dropout のマスクがかけられた重みを表しています。

回帰問題においては, 関数 $E$ は定数部分を除いて負の対数尤度関数で書き換えることができます。すなわち

$$E (f(x),y) = \frac{1}{2} \| y - f(x) \|^2 = - \frac{1}{\tau} \log p(y | f(x)) + {\rm const}$$

と変形できます。 ここで尤度関数は $p(y | f(x)) = N(y;f(x), \tau^{-1}I)$ のガウス分布で, $\tau$ は精度パラメータです.
今回は回帰問題を考えましたが、分類問題においても同様に負の対数尤度を用いて定式化することが可能です. (その場合 $\tau = 1$ となります.)

これよりロス関数は

$$L_{dropout} =  - \frac{1}{M} \sum_{i \in S} \frac{1}{\tau} \log p( y_i | f^{ \{ \hat{W_1^i}, \hat{W_2^i}, b \} }(x)) + \lambda_1 ||M_1|| + \lambda_2 || M_2 || + \lambda_3 || b ||$$

つぎに確率変数 $\hat{\omega}$ について考えます。この集合はネットワークの重みという確定的な値とdropout による確率変数の部分をあわせたものでした。それを明示的に記述すると

$$\hat{\omega_i} = \{ \hat{W_1}, \hat{W_2}, b\} = \{{\rm diag}(e_1^i)M_1, {\rm diag}(e_2^i)M_2, b \} := g(\theta, \hat{e_i})$$

と書き換えることができます。ここで $\theta = \{M_1, M_2, b\}$ は確定的な値を要素に持つ集合と定義し、$\hat{e_i} = \{ e_1^i, e_2^i \}$ は $i$ 番目のミニバッチのサンプルによって作成される dropout のマスクを要素に持つ、確率変数の集合であると定義します。 また関数 $g$ は2つの集合 $\theta, \hat{e_i}$ から $\hat{\omega_i}$ をつくる射影であるとします。
また $1 \le i \le N$ にたいして $e_1^i \sim p(e_1)$, $e_2^i \sim p(e_2)$です. ここで, $p(e_j)\ (j=1,2)$ はそれぞれ確率 $p_j$ のベルヌーイ分布の積であるとします。
これらを用いると dropout のロス関数は確率変数 $\hat{e_i}$ を用いて

$$L_{dropout} = - \frac{1}{M\tau} \sum_{i \in S} \log p(y_i | f^{g(\theta, \hat{e_i})}(x_i)) + \lambda_1 ||M_1|| + \lambda_2 || M_2 || + \lambda_3 || b ||$$

となります。この目的関数の重み $\theta := \{M_1, M_2, b\}$ に関する勾配は

$$\frac{\partial}{\partial \theta} L_{dropout} = - \frac{1}{M\tau} \sum_{i \in S} \frac{\partial}{\partial \theta} \log p(y_i | f^{g(\theta, \hat{e_i})}(x_i)) + \frac{\partial}{\partial \theta} \left( \lambda_1 ||M_1|| + \lambda_2 || M_2 || + \lambda_3 || b || \right)$$

となり, この勾配を逆伝搬させてネットワークの重みを更新します.

ところでこれは先程の変分推論の式ととても良く似ています. 再掲すると

$$\hat{{\rm VI}}_{MC} = - \frac{N}{M} \sum_{i \in S} \log p(y_i | f^{g(\theta, \epsilon_i)}(x_i)) + {\rm KL} (q_{\theta}(w) | p(w))$$

であり, これは ${\rm KL} (q_{\theta}(w) | p(w)) = N\tau \left( \lambda_1 ||M_1|| + \lambda_2 || M_2 || + \lambda_3 || b || \right)$ とおけば

$$L_{dropout} = \frac{1}{M\tau} \hat{{\rm VI}}_{MC}$$

となりdropout と変分推論は厳密に一致します!!


厳密に一致するということは, dropout を用いて学習したネットワークは、変分推論による重みの事後分布となっているということです。

すなわち dropout を用いるのはベイズ学習をしていることにほかならない ということです。ですから学習によって得られている値は確定的な値ではなく、 dropout と組み合わせることで重みの事後分布を計算できます。

さらに、学習済みのネットワークにとあるdropoutをかけた出力は重みの事後分布から一つの重みをサンプルしているという風に解釈することが可能です。したがって、複数のdropoutの係数を用いて複数の出力を生成しそのばらつきを用いると事後分布の分散を推定することも可能です。すなわち 出力がどの程度信頼できるか(ばらつきをもっているか) を見積もることが可能になっているということです。すごい。

例えば: 犬猫分類

例えば犬猫の分類問題をこの枠組みでやってみたとしましょう。

普通にネットワークを組むと出力は犬の確率でロス関数はクロスエントロピーなどになるでしょう。この場合推論時には犬の確率を出力として得ることが出来ますがそれ以上の情報は得ることが出来ません。

一方で dropout を推論時とともに使って何回もネットワークに推論を行わせると、それぞれ違った値を出力として得られます(なぜなら dropout を推論時にも使っているため毎回微妙に違ったネットワークになるためです)。これらのばらつきを使って事後分布の計算が出来るため、推論している画像が犬である確率、だけでなくその分布を知ることが出来るのです!

例えば p=0.95 にピークのあるなだらかな分布だったなら、犬っぽいけれど猫の可能性も高いと思ってるんだねえとか、p=0.5に鋭いピークの事後分布だったら、犬か猫かわからないけれどその自信がとても高いと思ってるんだねーとか、ニューラルネットワークの気持がちょっとわかるのです。すごくないですか。

数値実験

簡単な人口データの回帰問題を解いてみます.

実験に用いたコードは以下においています。

github.com

条件

レーニングデータは1次元の100個のデータで, ターゲット変数は $f(x) = x + \sin 5x$ に平均0, 分散 0.1 のガウスノイズを載せて作成します.

ネットワークは5層で各隠れ層が512次元としました。dropoutは確率 0.5 のベルヌーイ分布を用います。

relu

活性化関数を relu として 1000 epoch 計算したものの出力が下のグラフです。

f:id:dette:20170814172111p:plain

青い点がデータで, 橙色で事後分布からサンプルされたネットワークの出力を表しています。 これを見ると, データが存在する部分の分散は小さくなっていますがデータがない部分に行くに連れて分散が大きくなっていることがわかります. これは事後分布の分散が大きくなっているためです。このように従来のニューラルネットの予測値に加えて、その信頼度が出せていることが確認できます。

tanh

次に活性化関数を tanh に変えて実験したものが以下の図です。

f:id:dette:20170814174525p:plain

tanh に関してはreluのときのようにデータがない部分で極端に分散が大きくなりませんでした。またデータへの当てはまりも tanh の方は積極的に行っておらず、$x$ 軸性の領域ではほとんど 0 を予測しておりあたかも「データが足りないよ」と言っているようです。 

これは relu のほうが勾配消失に強く学習が素早く進むため、同じ epoch 数でもデータへ強くフィッティングしていくことが原因と考えられます。

また範囲外への予測も relu のようにそれまでの傾きを踏まえた予測ではなく、だんだんと 0 に向かうように予測しているように見受けられますが、これも活性化関数の形の影響 (reluは非ゼロのとき単調増加関数となる) を受けていると考えられます。

ちなみに tanh でも epoch さえ増やせばデータへしっかり当てはめてくれるようで、 epoch 数を更に増やして 4000 回としたものが以下になります。

f:id:dette:20170814175210p:plain

これを見るとデータがある部分に関しては平均値はほぼデータ通りで、分散もこちらが設定した値 0.1に近い値となっていることが伺えます。 1000 epoch では epoch が足りていなかったみたいですね。

感想

今まで確定値しか扱えなかったところに、Dropoutという既存の手法とベイズ推定の枠組みを結びつけて行くところは面白いなと思って読んでいました。CNNやRNNへ応用すれば「この画像はよくわかんないけど猫」とか「絶対犬です!」みたいな信頼度を同時にだすネットワークが自然に作れそうで今後発展すると面白そうだなと思います。

おまけ - 動画バージョン

tanh

 

f:id:dette:20170814180735g:plain

うにょうにょしながら学習していくの面白い。

*1:ベルヌーイ分布以外からサンプルする Dropout もありますがここでは単に 0-1 を出力とするベルヌーイ分布を相手に議論します

機械学習のコード整理

タイトルのままですが、今まで適当に書いてローカルに放置してたりブログには書いたけどそのまま放置してたりしていたコードを一つのリポジトリにまとめる作業をしました。

まとめながら、昔書いたコードをいろいろと手直ししていましたが、ひどく密につながりすぎたひどいコードがたくさん出てきてよりきれいな洗練されたコードを書きたいなと思う次第でした。

github.com

個人的には エビデンス最大化 と

f:id:dette:20170712221500p:plain

RVM のグラフが好きです。

f:id:dette:20170712221506p:plain

matplotlibもこの二年でだいぶおしゃれになって、seaborn使わなくても最初からかっこよく描画してくれるようになりました。かっこいいグラフは出てくるだけでやる気が出るので良いです。

Gpy vs scikit-learn: pythonでガウス過程回帰

Gpy と Scikit-learn

Pythonガウス過程を行うモジュールには大きく分けて2つが存在します。 一つは Gpy (Gaussian Process の専門ライブラリ) で、もう一つは Scikit-learn 内部の Gaussian Process です。

この2つのモジュールでどのような違いがあるのかを以下の項目で比較していきます。

  • カーネルの種類, 可視化
    • どんな種類のカーネルがあるのか
    • 可視化は容易か
  • 予測モデルの作成
    • モデルの作成はどのように行うのか
    • モデルの訓練方法, 結果の可視化方法はどうなっているか
  • 事後分布からのサンプリング
    • モデルの事後分布からのサンプリングを行えるか

使用した jupyter-notebook は以下の gist を参照してください。

GPy と Scikit-learn のガウス過程の比較 · GitHub

カーネルの種類, 可視化

カーネルを定義して可視化する難易度を比較していきます。

scikit-learn

scikit-learn でガウス過程をする際には sklearn.gaussian_process を用います。 カーネルの定義も同じ名前空間内に定義されていますので from sklearn.gaussian_process import kernels でインポートできます。 今回は Gpy のカーネルもインポートしていたので as を指定していますが別にそのままでも問題ないです。

色々とカーネルは定義されています (公式サイトを参照してください)

が今回は中心から遠くなると小さい値になる RBF カーネルExpSineSquared カーネル ( Periodic Kernel と呼ぶのが普通のような気がしますが sklearn ではこう呼ばれているようです) をかけ合わせたものを可視化します.

from sklearn.gaussian_process import kernels as sk_kern

kern = sk_kern.RBF(length_scale=.5)
kern = sk_kern.ExpSineSquared() * sk_kern.RBF()

# ほかにも色々と種類はある。ドキュメント参照
# kern = sk_kern.RationalQuadratic(length_scale=.5)
# kern = sk_kern.ConstantKernel()
# kern = sk_kern.WhiteKernel(noise_level=3.)

# 可視化は定義されていないので自分で用意する必要あり
X = np.linspace(-2, 2, 100)
plt.plot(X, kern(X.reshape(-1, 1), np.array([[0.]])))

f:id:dette:20170529033631p:plain

GPyの場合

kern = GPy.kern.PeriodicExponential(lengthscale=.1, variance=3) * GPy.kern.Matern32(1)
# kern = GPy.kern.RatQuad(1, lengthscale=.5, variance=.3)
# kern = GPy.kern.White(input_dim=1)
kern.plot()

f:id:dette:20170529033651p:plain

kern = GPy.kern.Linear(input_dim=2) * GPy.kern.Matern52(input_dim=2)
kern.plot()

f:id:dette:20170529033708p:plain

違い

カーネルの数

GPy の方が用意されている関数の数が多いです。 具体的には PeriodicExponential など周期性を持ったカーネルMatern52 のようなカーネルも用意されています。

可視化

GPy には可視化用の関数 plot が用意されています。 しかし Scikit-learn には同様のメソッドが無いため、自分で入力するベクトルを作る部分から書き下す必要があります。

定義方法

GPy においては、カーネルの定義の時点で、入力する特徴量の次元数 input_dim を指定する必要があります。 カーネル計算のためのデータがあたえられれば、特徴量の次元数は自明に判明するため、この変数はやや冗長のようにも感じられます。 一方 Scikit-learn ではカーネルの定義では純粋にカーネルの情報のみを入力すればよいです。

予測モデルの作成

Scikit-learn と GPy のそれぞれで、人工データに対する予測モデルを作っていきます。

データ作成

今回予測モデルに与える訓練データデータを作成します。 正しい関数は $f(x) = x + \sin(5x) $ とし, 分散 .1 のガウス分布によるノイズを付与します。

def true_func(x):
    """
    正しい関数
    
    :param np.array x:
    :return: 関数値 y
    :rtype: np.array
    """
    y = x + np.sin(5 * x)
    return y

np.random.seed(1)
x_train = np.random.normal(0, 1., 20)
y_train = true_func(x_train) + np.random.normal(loc=0, scale=.1, size=x_train.shape)
xx = np.linspace(-3, 3, 200)
plt.scatter(x_train, y_train, label="Data")
plt.plot(xx, true_func(xx), "--", color="C0", label="True Function")
plt.legend()
plt.title("トレーニングデータ")
plt.savefig("training_data.png", dpi=150)

f:id:dette:20170529033728p:plain

Scikit-learn

まず Scikit-learn でモデルを作成します。

kernel = sk_kern.RBF(1.0, (1e-3, 1e3)) + sk_kern.ConstantKernel(1.0, (1e-3, 1e3)) + sk_kern.WhiteKernel()
clf = GaussianProcessRegressor(
    kernel=kernel,
    alpha=1e-10, 
    optimizer="fmin_l_bfgs_b", 
    n_restarts_optimizer=20,
    normalize_y=True)

パラメータ詳細

  • alpha:
    • ガウス過程ではカーネル逆行列を計算する必要があります。一般に実用上用いられるカーネル関数がつくる行列は、入力のベクトルの値がすべて異なる場合、数学的にはかならず正定値です。しかし、固有値の値が非常に小さくなる場合があり、この時逆行列の計算は数値的に不安定性となってしまいます。 これを補正するための値が alpha で、行列に対角成分に alpha を持つ対角行列を加えることで、最小固有値の値が alpha よりもおおきくなるように補正し、計算安定性を確保します。
  • normalize_y: default True
    • 予測変数の平均を 0 になるように正規化します。Gaussian Process の計算の際の数値的安定性担保のため行われます.
  • n_restarts_optimizer
    • カーネルのハイパーパラメータを最適化する回数です。 0の場合1回の最適化で終わりますが、0以上が設定されると, 直前の最適化で得られた最適解を初期点として再び最適化を行います.
# X は (n_samples, n_features) の shape に変形する必要がある
clf.fit(x_train.reshape(-1, 1), y_train)

# パラメータ学習後のカーネルは self.kernel_ に保存される
clf.kernel_ # < RBF(length_scale=0.374) + 0.0316**2 + WhiteKernel(noise_level=0.00785)

# 予測は平均値と、オプションで 分散、共分散 を得ることが出来る
pred_mean, pred_std= clf.predict(x_test, return_std=True)
def plot_result(x_test, mean, std):
    plt.plot(x_test[:, 0], mean, color="C0", label="predict mean")
    plt.fill_between(x_test[:, 0], mean + std, mean - std, color="C0", alpha=.3,label= "1 sigma confidence")
    plt.plot(x_train, y_train, "o",label= "training data")

x_test = np.linspace(-3., 3., 200).reshape(-1, 1)
plot_result(x_test, pred_mean, pred_std)
plt.title("Scikit-learn による予測")
plt.legend()
plt.savefig("sklern_predict.png", dpi=150)

f:id:dette:20170529033748p:plain

GPy

次に GPy で予測モデルを作成していきます.

import GPy.kern as gp_kern
# kern = gp_kern.PeriodicMatern32(input_dim=1) * gp_kern.RBF(input_dim=1)
kern = gp_kern.RBF(input_dim=1) + gp_kern.Bias(input_dim=1)
kern = gp_kern.PeriodicExponential(input_dim=1)
gpy_model = GPy.models.GPRegression(X=x_train.reshape(-1, 1), Y=y_train.reshape(-1, 1), kernel=kern, normalizer=None)

パラメータ詳細

  • normalizer (default False)
    • 予測変数 Y の正規化を決定する変数です。 None が与えられるとガウス正規化が行われます。
  • noise_var (default 1)
    • データのノイズの分散を指定します. scikit-learn ではノイズは自分でカーネルに仕込む必要がありますが、 GPy.model.GPRegression ではデフォルトでノイズを考慮する用になっているため、カーネルにノイズを加える必要はありません。
fig = plt.figure(figsize=(6,8))
ax1 = fig.add_subplot(211)
gpy_model.plot(ax=ax1)  # 最適化前の予測
gpy_model.optimize()

ax2 = fig.add_subplot(212, sharex=ax1)
gpy_model.plot(ax=ax2)  # カーネル最適化後の予測

ax1.set_ylim(ax2.set_ylim(-4, 4))
ax1.set_title("GPy effect of kernel optimization")
ax1.set_ylabel("Before")
ax2.set_ylabel("After")
fig.tight_layout()
fig.savefig("GPy_kernel_optimization.png", dpi=150)

f:id:dette:20170529033803p:plain

# 最適化されたモデルの確認
print(gpy_model)
Name : GP regression
Objective : 6.799282795295307
Number of Parameters : 4
Number of Optimization Parameters : 4
Updates : True
Parameters:
  [1mGP_regression.                  [0;0m  |             value  |  constraints  |  priors
  [1mperiodic_exponential.variance   [0;0m  |     2.85057672019  |      +ve      |        
  [1mperiodic_exponential.lengthscale[0;0m  |    0.416248308257  |      +ve      |        
  [1mperiodic_exponential.period     [0;0m  |      11.478988407  |      +ve      |        
  [1mGaussian_noise.variance         [0;0m  |  0.00923971637791  |      +ve      |        
pred_mean, pred_var = gpy_model.predict(x_test.reshape(-1, 1), )
pred_std = pred_var ** .5
plot_result(x_test, mean=pred_mean[:, 0], std=pred_std[:, 0])
plt.legend()
plt.title("GPyによる予測")
plt.savefig("GPy_predict.png", dpi=150)

f:id:dette:20170529033824p:plain

事後分布からのサンプリング

scikit-learn には、そもそもサンプリングする関数が存在しません。 (事後分布の共分散を取得して, 自分でゴニョゴニョ計算してサンプリングする必要があります. )

一方 GPy では posterior_samples という関数が用意されており, これを用いて事後分布からのサンプリングを行うことができます. また posterior_samples_f を用いれば, 事後分布から確率過程をサンプルすることも可能です。以下では30個の確率過程をサンプリングして図示しています.

posterior = gpy_model.posterior_samples_f(x_test.reshape(-1, 1), size=30)

for i, pos in enumerate(posterior.T):
    label = None
    if i == 0:
        label = "posteror"        
    plt.plot(x_test[:, 0], pos, color="C0", alpha=.1, label=label)
plt.plot(x_train, y_train, "o")
plt.title("事後分布からのサンプリング")
plt.legend()
plt.savefig("posterior.png", dpi=150)

f:id:dette:20170529035551p:plain

以上をまとめましょう。

カーネルの定義, 可視化

カーネルの種類

GPy の方が多くのカーネルが用意されていますので、GPy > Scikit-learn といえるでしょう。 しかし一方で生成時にデータの情報を入れ無くてはならないという冗長性があり、この点ではインスタンス生成時に純粋にカーネルに関する情報だけ入れれば良い Scikit-learn は綺麗です 🍺。

可視化

GPy >> Scikit-learn といえるでしょう. GPy にはカーネル関数はもちろんのこと、予測モデル自体にもplotする機能がついています。 このため 「今回のデータにはどういう周期性を持ったカーネルが良いか」 などの検討を用意に行えます。 また model.optimize を呼び出す前後での model.plot をすることで、最適化の妥当性のチェックをすぐさま行うことができます。

予測モデルの作成

事後分布平均と分散

事後分布の平均値と分散の取得はどちらのライブラリでも簡単に取得することができます。この点で違いは無いです。

手続き的な違い

GPyではインスタンス生成時にデータ X, y をわたし, model.optimize メソッドの呼び出し時に、カーネルのパラメータを最適化します。 一方で, Scikit-learn ではインスタンス生成時には何も行わずインスタンス変数の初期化のみを行い, fit メソッドでデータ X, y をわたし, ここで同時にカーネルのパラメータ最適化を行っています。

事後分布からのサンプリング

Scikit-learn では一つの点での平均と分散を得ることはできますが、事後分布から確率過程をサンプリングすることはできません。 一方で GPy では poseterior_samples, poseterior_samples_f を用いればかんたんに事後分布からのサンプルを行えます.

その他の違い

GPyにはシンプルなガウス過程を用いた回帰問題以外にも, 損失関数をポアソン分布に変更し、二次の意味でガウス分布として近似してやるポアソン回帰モデルなども作成することができ, 拡張性に長けています。*1

一方 Scikit-learn では 回帰と分類 のモデルのみで, 複雑な目的関数を扱う枠組みは用意されていません。

以上のことから

  • GPy
    • カーネルの形状やモデルのフィッティングの確認を行いつつどのようなカーネルの設計にしていくのかを考えていきたい
    • より複雑なモデルを定義したい
  • Scikit-learn
    • もうどのカーネルを用いたら良いかがわかっていて特段可視化を重要視しない
    • Gridsearch で最適なカーネルの組み合わせを調べたい

という使い分けをするのがよいと言えるでしょう。