xiangze's sparse blog

機械学習、ベイズ統計、コンピュータビジョンと関連する数学について

RBMのtheanoコード解説

deeplearning.netのRBM(Restricted Boltzmann Machine)のTheanoでの実装面からの説明です。RBMのアルゴリズムについてはsinhrksさんが
Theano で Deep Learning <6>: 制約付きボルツマンマシン <前編> - StatsFragments
に書かれています。

説明の流れはdeeplearning.netのImplementationの部分と同じですが、ここではTheanoの共有変数(shared)とscan,updatesの使い方について重点的に説明します。

概要

RBMでは観測変数vの組が与えられた場合最もとりうるであろうでパラメータW,h,bの組を推定します。その際に対数尤度を各変数で偏微分した値が0に成るような値を推定したいのですが、 観測値の分布を条件付き確率p(h|v), p(v|h)を繰り返し用いて逐次的にサンプリングすることで近似しています。コードではこの繰り返しサンプリング手順をscanを用いて実装しています。対数尤度の微分は記号微分gradを用いています。入力データ毎に勾配降下法でパラメータを更新し、その値を保持するためパラメータは共有変数として定義する必要があります。

コードはひとつのクラスとなっていて

  • __init__ (初期化)
  • free_energy (自由エネルギーの式)
  • sample_v_given_h (隠れ変数vが与えられたときの観測変数サンプリング)
  • sample_h_given_v(観測変数hが与えられたときの隠れ変数サンプリング)
  • gibbs_hvh (vが与えられたときのsample*を使ったh,vのサンプリング)
  • gibbs_vhv (hが与えられたときのsample*を使ったv,hのサンプリング)
  • get_cost_updates 繰り返しサンプリングとパラメータW,h,vの更新
  • get_reconstruction_cost
  • get_pseudo_likelihood_cost

の関数で構成されています。

  • test_rbm

関数でget_cost_updates関数を呼び出し、コンパイルして学習を行いgibbs_hvhをコンパイルした結果でサンプリング、画像生成を行っています。

初期化(__init__)

外部から共有変数W,hbias,vbiasを指定できるようになっています。他のアルゴリズムの一部としたときに変数を共有するためだそうです。sharedの引数borrowはここではTrueとなっています。FalseにするとGPUのメモリに値がコピーされ(deep copy)、 同じ名前でPythonコードから呼び出すことが困難になってしまうそうです*1
inputに学習(training)に必要なデータを入れるようになっています。

サンプリング

free_energyは自由エネルギーの式
\(-\sum_v (1+\exp(Wv+h_{bias})-v\cdot v_{bias} \)
を実装、出力しています。

Gibbsサンプリングはgibbs_hvh, gibbs_vhvの2つの関数で行われています。これらは隠れ変数h,観測変数vをそれぞれ固定した場合にもう一方をサンプルする関数sample_v_given_h, sample_h_given_vを呼び出す順序が逆になっているのが違いです。学習では

sample_v_given_h, sample_h_given_vでは2項分布binominalでサンプリング値を生成し、そのときに使う平均値の式をpropup, propdownで計算しています。

普通に考えると単に

\(sig ( Wh+v_{bias}) =( \exp(Wh+v_{bias})/( \exp(Wh+v_{bias})+1)) \)
\(sig ( Wv+h_{bias}) =( \exp(Wv+h_{bias})/( \exp(Wv+h_{bias})+1)) \)
の式を出力すればいいのですが、sigmoidをかける前の関数もpre_sigmoid*として出力しています。これはあとのget_cost_updatesの部分で繰り返し計算をscanで実行しているのを最適化したいためです。

get_cost_updates

get_cost_updatesで繰り返しサンプリングを行ってパラメータW,hbias,vbias を更新していきます。

scanの引数outputs_infoにはgibbs_hvhの出力のうち入力に再代入していう変数(nh_sample)を代入します。分かりにくいように思える記述ですが、これでscanでの繰り返しによって得られたnh_sampleがリストとして得られそれをchainとしています。

変数を入れたparamsを確率的勾配降下法で更新しています。式(10) (Restricted Boltzmann Machine の導出に至る自分用まとめの式(10) )に基づいてサンプリング前後の自由エネルギーの差の勾配を計算し、元のパラメータ値から引いています。勾配部分の計算を自動微分で行っています。この時点では自由エネルギーに代入された変数chain_endも式の形をしているのでそれを防ぐためにgradの引数consider_constantにchain_endが入っています。計算されたパラメータの結果はOrderddictであるupdatesに追加しないとコンパイル、実行後に値が更新されません。

f:id:xiangze:20150907045236p:plain

cost関数

get_cost_updatesの計算途中に出力されるmonitored_cost(cross entropy)の計算は2種類の方法が実装されており引数persistentでCD法かp-CD法かを選択しています。

get_reconstruction_costの方は比較的簡単でcross entropy
\( E_v(\sum_x(x\log(sig(v))+(1-x)\log(1-sig(v)))) \)

を計算する(式を出力する)だけです。この書き方でx(input)の全配置に対する和が出せるようです。ここで
\(\log(sig(x))=\log(1/(1+\exp(-x)))\)
という形の式を作って自動で最適化させたいためにpre_sigmoid_nvを使っています。

get_pseudo_likelihood_costではpersistent-CD法(p-CD)という方法が実装されています。gibbs samplingのように入力値を0,1の2値に丸めたものを1つだけ反転させてエネルギーを計算しています。反転した変数の位置をupdate[bit_i_idx]に代入することで覚えておきます。input xが一様分布U(0,N)に従うという過程をおいてget_cost_updatesで行われる繰り返しでのサンプリングでcross entropyの値を近似しているためにsigmoid(v)の計算をしなくて良いところが利点のようです。学習の単位(epoch)を超えて隠れ変数hのサンプルを保持するようにしています。

学習、サンプリングの実行

test_rbmでtrainingと出力のサンプリングを行っています。

training

入力データをtensor x、乱数(rng, theano_rng), p-CDで使う共有変数persistent_chainの定義などの後にRBMを生成し、theano.functionでget_cost_updatesの出力costをコンパイルしています。scanの戻り値updatesには共有変数が更新された情報が入っていて、これをtheano.functionに渡すことで副作用として共有変数が更新されるような関数を作ることが出来ます。ここではget_cost_updatesのなかでさらにパラメータW,hbias,vbiasの更新結果を加えたものを代入しています*2。データを分割して複数のepochでbatchの数だけ繰り返します。その結果得られたWの値を共有変数の関数get_valuesで画像に落としています。

sampling

その後は学習できた内部パラメータのみにもとづいたサンプリングです。gibbs_vhvをscanで繰り返し実行し、vの平均値とサンプルを出力する関数sample_fnを生成します。scanでのパラメータ更新に加えpersistent_vis_chainを更新する処理を入れた後にコンパイルをしています。ここではupdatesに更新する変数の情報を追加するためにupdates.update()という関数を使っています。

sample_fnは引数なしにn_samples回だけ反復し、観測データの推定値を出力します。それを用いて画像が生成されます。

*1:参考: Understanding Memory Aliasing for Speed and Correctness — Theano 0.7 documentation

*2:この書き方はわかりづらいのでtheano.functio_withdate([inputs],f(inputs),givens=..) みたいのはどうでしょうか