StatModeling Memorandum

StatModeling Memorandum

StanとRとPythonでベイズ統計モデリングします. たまに書評.

2次元以上のスポット検出を行う統計モデル

1次元の場合の変化点検出は以下の記事で扱いました。

変化点検出のポイントは状態空間モデルのシステムモデルに「ほとんどの場合は0の近くの値を生成するが,まれにとても大きな値を生成する」という性質を持つ分布を使うことでした。そこで、上記の例ではコーシー分布を使いました。しかし、コーシー分布はかなり厄介な分布なので逆関数法を利用したコーディングをしないとうまくサンプリングできません。そのため2次元以上の場合の同様のモデル(マルコフ場モデル)に拡張することができません。2次元の場合のマルコフ場モデルについては以下の記事で簡単に扱いました(少しコードが古いです)。詳しくは書籍「StanとRでベイズ統計モデリング」の12章を参照してください。

それでも2次元以上のスポット検出を行いたくて色々なモデルを試しました。コーシー分布+実装の工夫、線過程(ライン過程)、混合正規分布、ベルヌーイ分布と正規分布の混合分布、両側指数分布(ラプラス分布)を使ったモデルなどなど。しかし、少なくとも私の環境においてはこれらのモデルだと、推定がうまくいかない場合や、推定できてもスポットのようにシャープではなくボワッと境界があいまいに推定されてしまう場合が多かったです。その後も2年にわたり粘り続けて合計150個弱のモデルを試した結果、一つの解決策を見つけましたのでこの記事を書きます。

UBN分布

イデアは「ほとんどの場合は0の近くの値を生成するが,まれにとても大きな値を生成する」という性質を工夫して表現することです。そこで、状態空間モデルのシステムモデルに相当する部分に以下の分布を使いました。

f:id:StatModeling:20201106175441p:plain

f:id:StatModeling:20201106175401p:plain

この分布は [a,b]の範囲の値を生成する、切断正規分布に一様分布のゲタをはかせたような分布です。ここで、 a,bは一様分布の範囲を決めるパラメータ、 uは一様分布のゲタの高さを決めるパラメータ、 \mu,\sigma正規分布の平均と標準偏差 Cは正規化項です。この分布は一様分布と切断正規分布の混合分布なのですが、ベルヌーイ分布とポアソン分布の混合分布をZero-inflated Poisson分布と分かりやすく呼ぶのになぞらえて、この記事では上記の分布をUniformly Boosted Normal Distributionと呼び、以下では略してUBN分布と呼びます。思いつけばコロンブスの卵ですが、UBN分布はコーシー分布の利点である裾の長さを保ったまま、サンプリングの非効率さを解消します。例えば、 a=-1, b=9, u=0.1, \mu=3, \sigma=1の場合には Cはほぼ2となり、以下のような確率密度になります。

f:id:StatModeling:20201106175435p:plain

なお、Stanの計算に必要な対数尤度は以下のように式変形して表現することで高速化できます。

f:id:StatModeling:20201106175444p:plain

ここでlog1p_exp関数とnormal_lpdf関数は計算の高速化するためのStanの便利関数です。

2次元のスポット検出例 その1

30x30の大きさで中央部分にシグナルがあり、ノイズを加えたデータYを考えます。図にすると以下です。

f:id:StatModeling:20201106175431p:plain

上下左右のマスとつながるマルコフ場モデルを、UBN分布を利用して実装したStanコードは以下になります。

  • 5行目: 上記で説明したUBN分布の uを固定値Uで与えています。
  • 9行目: muにノイズがのってデータYになります。設定した下限と上限がUBN分布の a bにそれぞれ対応しています。
  • 17,20行目: 17行目がマルコフ場モデルの左右のつながり、20行目が上下のつながりに対応します。ここではUを固定値として与えていること、またs_muの値が高々1.5ぐらいと考えられることから、UBN分布の u/Cはほぼ一定の定数です。Stanでは対数確率の偏微分が重要であり、微分で消える定数項を無視して構いません。そのため u/Cを無視してコーディングしています。
  • 21行目: ノイズをのせている部分です。状態空間モデルの観測モデルに相当します。
  • 22,23行目: なくても構いませんが、変分ベイズが安定になることがあるのでs_mus_Yに弱情報事前分布を設定しています。

このモデルは局所最適解が多くMCMCの一種であるNUTSではうまく推定できませんが、自動変分ベイズだと高速に推定できるモデルとなっています。データを生成し、推定を実行するRコードは以下になります。

  • 4~9行目:データ生成部分です。
  • 12行目:ここではU0.1としました。少し値を変えて実行してみて結果が頑健かチェックするとよいでしょう。

変分ベイズが高速なため、計算時間はSurface Pro 3(core i5)でも10秒以下です。推定結果は以下です。以下の図はmuの乱数サンプルの中央値を2次元でプロットした図です。

f:id:StatModeling:20201106175423p:plain

以下の図はRowの真ん中(ここでは15)に固定してスライスし、横軸をColumn・縦軸をmuにしてプロットした図です。薄い灰帯は乱数サンプルから計算した95%ベイズ信頼区間、濃い灰帯は80%ベイズ信頼区間、青線は中央値、赤線はデータです。

f:id:StatModeling:20201106175427p:plain

シグナルのスポットを検出できているのがわかるかと思います。上記のRコードと結果はシグナルの大きさがノイズのSDの3倍となっている場合ですが、シグナルの大きさがSDの2.5倍となっている場合の結果は以下になります。徐々にカドから崩れていきます。

f:id:StatModeling:20201106175419p:plain

このシグナルが正方形の場合、少なくともシグナルがノイズのSDの2.3倍ぐらいないとスポットとして綺麗に検出されないようでした。

2次元のスポット検出例 その2

素数が300x600の場合も試してみました。データ例を図にすると以下です。

f:id:StatModeling:20201106175415p:plain

モデルは同じままで、実行するRコードもほぼ同じです(データ生成部分だけわずかに異なります)。推定に要する時間は5分ぐらいでした。シグナルがノイズのSDの3倍の場合の推定結果は以下になりました。図の凡例は上記の場合と同じです。丸いスポットの検出は比較的得意ですが、尖っている箇所はやはり難しいようです。

f:id:StatModeling:20201106175405p:plain

f:id:StatModeling:20201106175410p:plain

この場合、少なくともシグナルがノイズのSDの3倍ぐらいないとスポットとして綺麗に検出されないようでした。

試した範囲では、この方法によって地図上でも3次元でもスポット検出できそうです。Enjoy!

情報量規準LOOCVとWAICの比較

この記事はStan Advent Calendar 2016およびR Advent Calendar 2016の12月7日の記事です。StanコードとRコードは記事の最後にあります。

背景は以下です。

  • [1] Aki Vehtari, Andrew Gelman, Jonah Gabry (2015). Practical Bayesian model evaluation using leave-one-out cross-validation and WAIC. arXiv:1507.04544. (url)
  • [2] 渡辺澄夫. 広く使える情報量規準(WAIC)の続き (注4)【WAICとクロスバリデーションの違いについて】 (url)
  • [3] Sumio Watanabe. Comparison of PSIS Cross Validation with WAIC. (url)

leave-one-outクロスバリデーション(以下LOOCV)およびWAICは予測のよさをベースにしたモデル選択に用いられる情報量規準であり、ともに汎化誤差(Generalization Error、以下GE)の近似です。それにもかかわらず[1]では、本来性能評価では必須と思われる汎化誤差との比較がありません。実データ(真のモデルが未知の状況)で用いているためと思いますが、これではいけないように思います。この記事では僕が日常的に使用するような5つの基本的なモデルを使い、真のモデルが既知の状況でGE・LOOCV・WAICの性能比較を行いました。

具体的には[2]に以下のようなコメントがあります。なお[1]ではPareto Smoothed Importance SamplingでLOOCVを算出しており、PSISCVとも呼ばれるようです。

  • (0) まず同じデータに対してマルコフ連鎖モンテカルロ法を何度も行ったときの値の揺らぎを調べてみましょう.WAICの分散はISCVおよびPSISCVの分散よりも小さくなります。つまり、WAICはISCVおよびPSISCVよりもマルコフ連鎖揺らぎに対して強いということができます.
  • (2) しかしながら,CVもWAICも汎化誤差を推定することが本来の目的です.CVとWAICの両方の厳密値が計算できたとして,(つまりMCMC法で無限にサンプルが取れたとき),どちらの方が汎化誤差の推定として優れているのでしょうか。(中略) 我々の実験では,GEを汎化誤差とするとき,ほぼ,いつでも E[|PSISCV-GE|] > E[|WAIC-GE|] が成り立つのですが・・・。このページをご覧の皆様にはぜひ実験してみていただければと思います。なお、E[ ]は学習用データのでかたについての平均を表しています。

そこでこれら2点について検証してみました。先に「(2)のコメント」について説明します。

GE・LOOCV・WAICの比較

使用した5つのモデルとシミュレーションの手順を説明します。

重回帰

真のモデルは以下です。

  Y \sim Normal(1.3 - 3.1 X_1 + 0.7 X_2, 2.5)

あてはめたモデルは以下です。

  Y \sim Normal(b_1 + b_2 X_1 + b_3 X_2, \sigma)

データ点の数Nについては10,30,100,300を試しました。例としてN = 10の場合を説明します。まず乱数でデータX(すなわち X_1, X_2)を生成します。次にそのXの値を使って1000通りのYを生成します(学習用データのでかたの平均をとるため)。各YについてStanでiter=11000, warmup=1000, chains=4で実行して合計40000個のMCMCサンプルを得てGE・LOOCV・WAICと「LOOCV - GE」と「WAIC - GE」を求めました。その後、Nごとに「LOOCV - GE」と「WAIC - GE」のboxplotを描きました。

ロジスティック回帰

手順は重回帰の場合と同じです。使用したモデルだけが異なります。真のモデルは以下です。

  Y \sim Bernoulli(inv\_logit(0.8 - 1.1 X_1 + 0.1 X_2))

あてはめたモデルは以下です。

  Y \sim Bernoulli(inv\_logit(b_1 + b_2 X_1 + b_3 X_2))

  b_1,b_2,b_3 \sim Student\_t(4,0,1)

非線形回帰 ミカエリス・メンテン型

手順は重回帰の場合と同じです。使用したモデルだけが異なります。真のモデルは以下です。

  Y \sim Normal(10.0 X / (2.0 + X), 0.8)

あてはめたモデルは以下です。

  Y \sim Normal(m X / (k + X), \sigma)

  k \sim Uniform(0, 12)

  m \sim Uniform(0, 20)

真のモデルが含まれない場合

あてはめたモデルが以下の場合も試しました。

  Y \sim Normal(a + b X, \sigma)

ノイズがt分布に従う重回帰

手順は重回帰の場合と同じです。使用したモデルだけが異なります。真のモデルは以下です。

  Y \sim Student\_t(4, 1.3 - 3.1 X_1 + 0.7 X_2, 2.5)

あてはめたモデルは以下です。

  Y \sim Student\_t(4, b_1 + b_2 X_1 + b_3 X_2, \sigma)

階層モデル

手順は重回帰の場合とおおよそ同じですが、データ点の数とモデルが異なります。グループの数を10に固定し、データ点の数Nについては20,50,130,400を試しました(それぞれ各グループで2,5,13,40人)。また真のモデルは以下です。

  \mu\left[g\right] \sim Normal(0, 10)   g = 1,2,\dots,10

  Y\left[n\right] \sim Normal(\mu\left[g\left[n\right]\right], 2.0)   n = 1,2,\dots,N

あてはめたモデルは以下です。

  \mu\left[g\right] \sim Normal(\mu_0, \sigma_0)   g = 1,2,\dots,10

  Y\left[n\right] \sim Normal(\mu\left[g\left[n\right]\right], \sigma)   n = 1,2,\dots,N

  \mu_0,\sigma_0 \sim Student\_t(4, 0, 10)

階層モデルにおいては何を予測したいのか(どういう状況の汎化誤差を知りたいのか)を注意深く考える必要があります。以下の記事を参照。

statmodeling.hatenablog.com

ここでは以下の2つの場合を計算しました。

  • 既存の各グループに、新しく1人ずつ加わる場合
  • 別の新しいクラスができて、新しく1人が加わる場合

結果

重回帰

f:id:StatModeling:20201106180427p:plain

大きな差はありませんでした。Nが小さい場合にWAICの方がわずかにGEに近くなる傾向があるようです。

ロジスティック回帰

f:id:StatModeling:20201106180430p:plain

大きな差はありませんでした。Nが小さい場合にLOOCVの方がわずかにGEに近くなる傾向があるようです。

非線形回帰 ミカエリス・メンテン型

f:id:StatModeling:20201106180439p:plain

大きな差はありませんでした。Nが小さい場合にWAICの方がわずかにGEに近くなる傾向があるようです。

  • 真のモデルが含まれない場合

f:id:StatModeling:20201106180435p:plain

この場合はNを増やしても汎化誤差引くエントロピー(=予測分布と真の分布のKL情報量)は0に近づかず、0.89ほどで下げ止まります。そして、Nが小さい場合にWAICの方がGEに近くなる傾向があるようです。

ノイズがt分布に従う重回帰

f:id:StatModeling:20201106180443p:plain

Nが小さい場合にWAICの方がGEに近くなる傾向があるようです。なおboxからはみ出るoutlierの値が大きく、そのままプロットすると見づらくなるので、図の縦軸を制限しました。

階層モデル

  • 既存の各グループに、新しく1人ずつ加わる場合

f:id:StatModeling:20201106180447p:plain

Nが小さい場合にWAICの方がGEに近くなる傾向があるようです。

  • 別の新しいクラスができて、新しく1人が加わる場合

f:id:StatModeling:20201106180457p:plain

グループ数が少ない場合、グループあたりの人数を増やしてもLOOCV - GEおよびWAIC - GEの中央値は0に近づいていきません。グループ数が少ないと、グループあたりの人数を増やしてもグループを生成する平均と標準偏差のパラメータは精度よく求められないことを反映しているのだと思います。全体的にWAICの方がわずかにGEに近くなる傾向があるようです。

階層モデル その2 2016.12.14追記

伊庭先生から以下のような要望がありました。

そこでモデルは同じものを用いて、グループあたりの人数を固定し(2,5,13のうちいずれか)、グループの数が10,30,100の各場合で実行してみました。

  • 既存の各グループに、新しく1人ずつ加わる場合

f:id:StatModeling:20201106180452p:plain

グループあたりの人数が少ない場合、グループ数を増やすとLOOCV - GEおよびWAIC - GEのバラツキは少なくなるもの、それらの中央値は0に近づいていきません。グループ数を増やしても各グループには2人しかいないため、グループごとの予測はあたらないままということを反映しているのだと思います。グループあたりの人数が小さい場合にWAICの方がGEに近くなる傾向があるようです。

  • 別の新しいクラスができて、新しく1人が加わる場合

f:id:StatModeling:20201106180500p:plain

Gが小さい場合にWAICの方がわずかにGEに近くなる傾向があるようです。

* * *

5つのモデルを通して見ると、Nが小さい場合、すなわち1サンプルの重みが大きい場合にはWAICのほうがLOOCVよりもよい汎化誤差の近似になっているようです。[2]の「PSISCVとWAIC:実験例 追加」によると、影響の大きい(重みの大きい)サンプルが存在する場合にも同じような結果になるようです。以下の伊庭先生のツイートはこのような状況を指していると思われます。

WAICはLOOCVよりMCMCの揺らぎに強いか?

次に「(0)のコメント」について検証しました。前述の5つのモデルに対し、N10または100とし、各Nについてデータを5通り生成しました。さらに各データセットに対して、MCMCのシードを1000通り試し、1000個のLOOCVとWAICを求め、それぞれの分散と平均と変動係数を求めました。

結果

いずれの場合についても実用上の差が認められませんでした。N100の場合より10の場合の方がMCMCの揺らぎがありますが、それでも変動係数にして高々1%程度でした。なお、この結果はMCMCサンプルを求めるアルゴリズムの違いやMCMCサンプルの数にも依存すると思います。

まとめ

  • Nが大きい場合には、WAICとLOOCVにほぼ差がないと言えるでしょう。
  • Nが小さい場合には、WAICの方が汎化誤差のよい近似になっていると言えるでしょう。
  • さらに理論的な美しさや計算速度も含めて総合的に判断すると、WAICに軍配が上がると思います。

おまけ

データ Xとは異なる xにおける yの予測分布 y_{pred}(y|x)を考えたい場合があると思います。その場合は、一般によい情報量規準があるか未解明で、研究対象として興味深いようです(渡辺澄夫先生(私信))。例えば単回帰の場合には、真のモデルがあてはめたモデルに含まれており、かつモデルに依存する量を使うと情報量規準に準ずる量を構築できるようです。興味深いです。


ソースコード

「(2)のコメント」を検証するための、重回帰の場合と階層モデルの場合のStanコードとRコードを公開します。

重回帰

Stanコード

重回帰のモデルの部分は「StanとRでベイズ統計モデリング」の9.2.1項と同じです。異なる点はgenerated quantitiesブロックでGE・LOOCV・WAICを算出するためにlog_liky_predを算出している点です。

Rコード

例としてN100の場合を載せます。理解しやすさのため、並列化していないコードにしてありますが、実際には色々並列化して計算しています。

  • 16~17行目: 学習用データの出かたの平均をとりたいので、データYを乱数で生成しています。
  • 22~31行目: データごとのエントロピーと汎化誤差を計算しています。以前の記事参照。このコードのように十分なMCMCサンプルから予測分布の近似関数を求めて汎化誤差を算出する方法のほか、直接MCMCサンプルを使って求める方法もあるかと思います(渡辺先生はそうしています)。
  • 23~24行目: 予測分布は滑らかだと仮定し、40000個のMCMCサンプルから予測分布の密度関数を計算しています。Rのデフォルトのdensity関数よりも{KernSmooth}パッケージのbkde関数の方が優秀っぽいのでこちらを使っています(参考pdf)。
  • 25行目: 真の分布です。
  • 26行目: エントロピーの計算の際に積分される関数です。
  • 27行目: 汎化誤差の計算の際に積分される関数です。予測分布の密度推定した結果をapproxfunで関数に変換している関係上、f_predがごくまれに絶対値の小さな0以下の値を返します。それを避けるためにifelse関数をかませてあります。
  • 28~29行目: それぞれエントロピーと汎化誤差を計算しています。ここではf_true正規分布なので、-6SD~+6SDまで積分すれば十分よい近似となります。Rのデフォルトのintegrate関数はちょっと賢くてadaptiveに積分しているので計算は早いのですが、まれに不安定で計算ができない場合があります。そのため、少し遅いですが安定な数値積分の手法であるRombergの方法を使っています({pracma}パッケージのromberg関数または{Rmpfr}パッケージのintegrateR関数を使うことができます)。
  • 34行目: 汎化誤差(GE)のサンプルに関する平均を求めています。学習用データと同じNXの値を持つ新しいデータセットに対して予測を行い、1サンプルあたりの汎化誤差を求めていることに相当します。
  • 35~36行目: Stanのチームが開発している{loo}パッケージを用いてlooicwaicを求めています。彼らの情報量規準のスケールはAICやDICとあわせて 2N倍となっているので、1サンプルあたりにスケールをあわせる意味で2*Nで割っています。

階層モデル「既存の各グループに、新しく1人ずつ加わる場合」

階層モデルの場合のWAICの詳しい解説は以前の記事を参照してください。

Stanコード

Rコード

重回帰の場合と似ています。

  • 23~32行目: グループgに1人加わった場合の汎化誤差error_by_groupを求めています。重回帰の場合にサンプルごと(nごと)だったのが、グループごと(gごと)にインデックスが変わっただけで、内容は変わっていません。
  • 35行目: 「既存の各グループに、新しく1人ずつ加わる場合」なので、各グループの汎化誤差の和になります。
  • 36~37行目: 各グループごとにLOOCVまたはWAICを求めて和をとっています。

階層モデル「別の新しいクラスができて、新しく1人が加わる場合」

Stanコード

Rコード

  • 25~26行目: この場合は真の分布が積分を含んでいるので少し複雑です。

残りは「既存の各グループに、新しく1人ずつ加わる場合」とほぼ同じです。

なお、この記事は以下のツイートによってやってみようかなと思いました。

GPy(Pythonのガウス過程用ライブラリ)の使い方

概要

GPyを用いて、サンプルパスの生成、ガウス過程回帰、クラス分類、ポアソン回帰、Bayesian GPLVMを実行しました。自分用のメモです。

参考資料

理論的背景は上記の[3]を参考にしてください。日本語でもガウス過程の解説がMLPシリーズから豪華著者陣で出るようです。超期待しています。

以下のサンプルプログラムは基本的に[2]を元にしています。しかし、古くてそのままでは動かないプログラムや分かりにくいプログラムを少し加工修正しています。なお、環境は以下の通りです。

サンプルパスの生成

RBFカーネルで適当に定めたパラメータの値でサンプルパスを生成するプログラムです。カーネルそのものやカーネルのパラメータを変えることでどのようなサンプルパスを生成するのかシミュレーションしたい場合によく使います。

import GPy
import numpy as np
import matplotlib.pyplot as plt

kernel = GPy.kern.RBF(input_dim=1, variance=1, lengthscale=0.2)

np.random.seed(seed=123)
N_sim = 100
x_sim = np.linspace(-1, 1, N_sim)
x_sim = x_sim[:, None]
mu = np.zeros(N_sim)
cov = kernel.K(x_sim, x_sim)
y_sim = np.random.multivariate_normal(mu, cov, size=20)

fig = plt.figure()
ax = fig.add_subplot(1,1,1)
for i in range(20):
    ax.plot(x_sim[:], y_sim[i,:])
fig.savefig('output/fig1.png')
  • 5行目: これでカーネルを定義します。入力の次元(input_dim)は必須です。
  • 10行目: GPyの関数の多くは、引数のshape(データ点の数, 1)である必要があります。そこで[:, None]を加えてその形にしています。
  • 12行目: kernelオブジェクトに対しK関数を使うと分散共分散行列を作成できます。
  • 13行目: numpyの関数で多変量正規分布からサンプルを生成しています。

ガウス過程回帰(入力1次元・出力1次元)

手順は カーネルを定める→モデル作成→最適化 だけです。

import GPy
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

kernel = GPy.kern.RBF(1)
# kernel = GPy.kern.RBF(1) + GPy.kern.Bias(1) + GPy.kern.Linear(1)

d = pd.read_csv('data-GPbook-Fig2_05.txt')
model = GPy.models.GPRegression(d.X[:, None], d.Y[:, None], kernel=kernel)
model.optimize()
model.plot()
plt.savefig('output/fig2.png')

## prediction
x_pred = np.linspace(-10, 10, 100)
x_pred = x_pred[:, None]
y_qua_pred = model.predict_quantiles(x_pred, quantiles=(2.5, 50, 97.5))[0]
  • 6行目: RBFカーネルinput_dim = 1で作成しています。7行目はRBF+Bias+Linearのカーネルを使う場合です。足したり掛けたりするだけで複雑なカーネルを作ることができるインターフェースが素敵です。
  • 10行目: GPy.models.GPRegression関数でモデルを作成しています。print(model)m['']とするとモデルに含まれるパラメータを見ることができます。特に指定しなければ、すべてのパラメータが最適化の対象となります。ちなみに各パラメータに固定値を与えることや制限をかけることができます(詳しくはこれこれを参照)。
  • 11行目: 最適化をしています。オプションでiterationの数などを指定できます。
  • 12~13行目: 最適化後のモデルをプロットしています。
  • 16~18行目: 最適化後のモデルを使って予測を行っています。

ガウス過程回帰(入力2次元・出力1次元)

import GPy
import numpy as np
import matplotlib.pyplot as plt

kernel = GPy.kern.Matern52(2, ARD=True)

np.random.seed(seed=123)
N = 50
X = np.random.uniform(-3.,3.,(N, 2))
Y = np.sin(X[:,0:1]) * np.sin(X[:,1:2]) + np.random.randn(N,1)*0.05

model = GPy.models.GPRegression(X, Y, kernel)
model.optimize(messages=True, max_iters=1e5)
model.plot()
plt.savefig('output/fig3.png')

model.plot(fixed_inputs=[(0, -1.0)], plot_data=False)
plt.savefig('output/fig3-slice.png')

## prediction
x_pred = np.array([np.linspace(-3, 3, 100), np.linspace(3, -3, 100)]).T
y_qua_pred = model.predict_quantiles(x_pred, quantiles=(2.5, 50, 97.5))[0]
  • 5行目: 今回はMatern5/2カーネルを使っています。オプションのARD=Trueは入力の次元1つに対し、1つのlengthscaleパラメータを割り振ること(すなわちGPは等方でないことを表します)。
  • 17~18行目: 2次元の入力のうち一部を固定した図(スライスした図;2枚目の図)を描いています。ここでは、fixed_inputs=[(0, -1.0)]でインデックス0の入力を-1.0に固定しています。
  • 21~22行目: 入力1次元のガウス過程回帰と同様に予測をしています。入力の次元に注意です。

スパースなガウス過程回帰

補助変数法やコンパクトなガウス過程回帰とも呼ばれます。ガウス過程はデータ点の数N逆行列を求める必要があり、その部分にN^3のオーダーの時間がかかります。そのため、データ点が増えると次第に遅くなります。そこで、一部の補助変数(inducing inputs)を入力次元の代表点として扱い、対数尤度を近似することで計算を高速化させる方法があります。それがこの節の方法になります。

import GPy
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

kernel = GPy.kern.RBF(1)

d = pd.read_csv('data-GPbook-Fig2_05.txt')
m_full = GPy.models.GPRegression(d.X[:, None], d.Y[:, None], kernel=kernel)
m_full.optimize()

Z = np.hstack((np.linspace(-6, -3, 3), np.linspace(3, 6, 3)))[:,None]
# Z = np.linspace(-6, 6, 12)[:, None]
m_sparse = GPy.models.SparseGPRegression(d.X[:, None], d.Y[:, None], Z=Z)
m_sparse.optimize()
m_sparse.plot()
plt.savefig('output/fig4.png')
print(m_sparse.log_likelihood(), m_full.log_likelihood())
  • 12行目: 補助変数の初期値です。6個をテキトーに定めました。結果は1枚目の図です。
  • 13行目: こちらは12個の場合です。結果は2枚目の図です。
  • 14行目: GPy.models.SparseGPRegression関数を使います。補助変数はZで指定します。
  • 15行目: 補助変数の位置も最適化の対象となります。
  • 18行目: modelオブジェクトに対しlog_likelihood関数を使うと対数尤度を取得できます。最適化の後の対数尤度を見ると、補助変数6個の場合が-28.85、補助変数12個の場合が-18.02、補助変数を使わないフルモデルの場合が-17.92となりました。12個の補助変数で十分よく近似できていることが分かります。

クラス分類

ここではPRML下のFig.6.12相当の図を再現してみます。

import GPy
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

kernel = GPy.kern.RBF(2, ARD=True)

d = pd.read_csv('data-classification.txt')
model = GPy.models.GPClassification(d[['X1', 'X2']].values, d.Y[:, None])
model.optimize()

ax = model.plot(plot_data=False)
d0 = d[d.Y == 0]
d1 = d[d.Y == 1]
ax.plot(d0.X1, d0.X2, 'ro')
ax.plot(d1.X1, d1.X2, 'bo')
plt.savefig('output/fig5.png')
  • 9行目: GPy.models.GPClassification関数でクラス分類のモデルを組み立てることができます。

ポアソン回帰

久保緑本の11章の欠測値なしのモデルを実行します。

import GPy
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

kernel = GPy.kern.RBF(1)

d = pd.read_csv('data-kubo11a.txt')

model = GPy.core.GP(X=np.linspace(1, 50, 50)[:,None], Y=d.Y[:,None], kernel=kernel,
    inference_method=GPy.inference.latent_function_inference.Laplace(),
    likelihood=GPy.likelihoods.Poisson())
model.optimize()
model.plot()
plt.savefig('output/fig6.png')

x_pred = np.linspace(1, 50, 50)[:, None]
f_mean, f_var = model._raw_predict(x_pred) # Predictive GP for log intensity mean and variance
f_upper, f_lower = f_mean + 2*np.sqrt(f_var), f_mean - 2.*np.sqrt(f_var)
plt.plot(x_pred, np.exp(f_mean), color='blue', lw=2)
plt.fill_between(x_pred[:,0], np.exp(f_lower[:,0]), np.exp(f_upper[:,0]), color='blue', alpha=.1)
plt.savefig('output/fig6-mean.png')
  • 10~12行目: 少し凝ったモデルを使用したい場合には、GPy.core.GP関数を使って尤度を自分で設定する必要があります。推定方法もあわせて指定します。ここでは単純なポアソン回帰なので、用意されているGPy.likelihoods.Poisson関数を使えば完了です。
  • 17~21行目: 真の平均の推定値と±2SDのグラフ(2枚目の図)を描いています。

ガウス過程回帰(入力2次元・出力2次元)

出力が2次元となると、モデルの選択肢が増えます。その前に「どうして複数次元の出力が必要なのか?各出力は相関しているのか(一方の出力が他方の出力を予測するヒントになるのか)?」といった問いが重要だとNeil Lawrenceは述べています(メーリングリストより)。もしそれらの問いの回答がNoならば、出力1次元のモデルを複数組み合わせたモデルの方がよいかもしれません。

ここでは、「出力間に相関がある簡単なモデル」「出力間に相関がないモデル」「出力間に相関がある凝ったモデル」の順にすすめます。

import GPy
import numpy as np
import matplotlib.pyplot as plt

f_output1 = lambda x: 4*np.cos(x/5) - 0.4*x - 35 + np.random.rand(x.size) * 2
f_output2 = lambda x: 6*np.cos(x/5) + 0.2*x + 35 + np.random.rand(x.size) * 8

np.random.seed(seed=123)
X1 = np.random.rand(100)
X2 = np.random.rand(100)
X1 = X1*75
X2 = X2*70 + 30
Y1 = f_output1(X1)
Y2 = f_output2(X2)

x_pred1 = np.random.rand(100)*100
x_pred2 = np.random.rand(100)*100
y_pred1 = f_output1(x_pred1)
y_pred2 = f_output2(x_pred2)

def plot_2outputs(m):
    fig = plt.figure(figsize=(12, 8))
    ax1 = fig.add_subplot(211)
    ax1.set_ylim([-120, -20])
    ax1.set_title('Output 1')
    m.plot(plot_limits=[0, 100], fixed_inputs=[(1, 0)], which_data_rows=slice(0, 100), ax=ax1)
    ax1.plot(x_pred1, y_pred1, 'rx', mew=1.5)
    ax2 = fig.add_subplot(212)
    ax2.set_ylim([-20, 100])
    ax2.set_title('Output 2')
    m.plot(plot_limits=[0, 100], fixed_inputs=[(1, 1)], which_data_rows=slice(100, 200), ax=ax2)
    ax2.plot(x_pred2, y_pred2, 'rx', mew=1.5)


K = GPy.kern.Matern32(1)
B = GPy.kern.Coregionalize(input_dim=1, output_dim=2)
kernel = K**B
model = GPy.models.GPCoregionalizedRegression(X_list=[X1[:, None],X2[:, None]], Y_list=[Y1[:, None],Y2[:, None]], kernel=kernel)

model['.*Mat32.var'].constrain_fixed(1)
model.optimize()
plot_2outputs(model)
plt.savefig('output/fig7a.png')

model['.*coregion.W'].constrain_fixed(0)
model.randomize()
model.optimize()
plot_2outputs(model)
plt.savefig('output/fig7b.png')


K1 = GPy.kern.RBF(1)
K2 = GPy.kern.Bias(1) + GPy.kern.Linear(1)
B1 = GPy.kern.Coregionalize(1, output_dim=2)
B2 = GPy.kern.Coregionalize(1, output_dim=2)
kernel = K1**B1 + K2**B2

X = np.vstack((np.concatenate([X1, X2]), np.hstack((np.zeros(100), np.ones(100))))).T
Y = np.hstack((Y1, Y2))[:, None]
model = GPy.models.GPRegression(X, Y, kernel)
model.optimize()
plot_2outputs(model)
plt.savefig('output/fig7c.png')


## prediction
x_pred = np.arange(100, 110)[:, None]
x_pred = np.hstack([x_pred, np.ones_like(x_pred)])
output_index_pred = {'output_index':x_pred[:,1:].astype(int)}
y_pred = model.predict(x_pred, Y_metadata=output_index_pred)
  • 5~19行目: デモデータ作成部分です。
  • 21~32行目: プロットする関数を定義しています。
  • 36行目: 出力間の関係を定める行列B(coregionalization matrix)を作成しています。詳しくはここを参照。
  • 37行目: kernelオブジェクトに対する**クロネッカー積となります。
  • 38行目: GPy.models.GPCoregionalizedRegression関数を使うことで、複数の出力の次元が相関を持ち、出力の各次元でノイズの大きさが異なるモデルを簡単に作成することができます。なお、入力と出力はlistで渡します。
  • 40行目: 38行目でもモデルにノイズが入るので、Matern3/2カーネルに含まれるノイズを1に固定しています。
  • 41~43行目: このモデルの結果は1枚目の図です。
  • 45行目: Bを対角行列に固定しています。出力次元ごとにガウス過程回帰を行うのと同じになります。
  • 46行目: 41行目で最適化された値になっているので、いったん初期値をぐちゃぐちゃにする意味です。
  • 47~49行目: このモデルの結果は2枚目の図です。1枚目の図との違いに注目してください。
  • 52~56行目: coregionalization matrixをカーネルの種類ごとに用意して組み合わせることもできます。
  • 60行目: GPy.models.GPRegression関数を使うと、すべての出力次元の共通の大きさのノイズとなります。なお、GPy.models.GPRegression関数を用いる場合にはXは「1列目に値・2列目に出力次元のインデックス」となっているndarrayを渡す必要があります。YXに対応するndarrayです。
  • 61~63行目: このモデルの結果は3枚目の図です。予測区間が狭いです。モデルが複雑で過学習の恐れがあるかもしれません。
  • 67~70行目: 予測の例です。出力が複数ある場合にはdictionaryを作って渡します。ここではoutput_index1(すなわち2番目の出力)が100110でどのような出力になるか予測しています。

Bayesian GPLVM

前の記事と同じようにPRMLでおなじみのOil Flowのデータに対してBayesian GPLVMを実行します。

from scipy.io import loadmat
import scipy.io as spio
import GPy
import matplotlib.pyplot as plt

d = spio.loadmat('input/3Class.mat')
X = d['DataTrn']
X -= X.mean(0)
L = d['DataTrnLbls'].nonzero()[1]
input_dim = 2 # How many latent dimensions to use

kernel = GPy.kern.RBF(input_dim, ARD=True) + GPy.kern.Bias(input_dim) + GPy.kern.Linear(input_dim) + GPy.kern.White(input_dim)
model = GPy.models.BayesianGPLVM(X, input_dim, kernel=kernel, num_inducing=30)
model.optimize(messages=True, max_iters=5e3)
model.plot_latent(labels=L)
plt.savefig('output/fig8.png')
  • 10行目: 潜在変数の次元。ここではチュートリアルと同じように2にしています。
  • 13行目: GPy.models.BayesianGPLVM関数の一発で補助変数込みでモデルが作成できます。

不明点

GPy.models.GPRegression関数を使うと、パラメータにノイズの大きさが加わります。このノイズと、カーネル側でGPy.kern.White関数で設定したノイズの違いがよく分かりません。なお、簡単なモデルで両方とも設定すると数式の上では識別できなくなると思うのですが、最適化の結果は分散をちょうど半分に分ける形で推定されて全体的な予測分布は変わりません。