本記事では、前回説明した確率的変分ベイズについて、Pyroを使うとどのように書くことができるのかを見ていきたいと思います。対応するPyroのドキュメントはここです。
SVI Class
確率変分ベイズは、PyroでSVIクラスとして提供されています。現在Pyroでは目的関数にELBOしか選択できないのですが、将来的には対応していく予定ですと書かれています(たぶん)。
SVIは次のようにインポートします。
import pyro
from pyro.infer import SVI, Trace_ELBO
そしてSVIは以下のような引数を持ちます。
svi = SVI(model, guide, optimizer, loss=Trace_ELBO())
model、guideは前回より前の記事で扱ったようなものです。optimizerはAdamなどのことを指し、どのようなoptimizerが用意されているかはドキュメントを参照してください。lossは損失関数のことで、ここではELBOを指定しています(ここは現状ELBO(とその派生)しかない?)。
次に、SVIクラスのメソッドについてです。SVIクラス2つのメソッドを提供しています。step()とevaluate_loss()です。それぞれのメソッドは以下のような計算をします。
- step()は、勾配計算をおこない、lossの推定値を返します。
- evaluate_loss()は、勾配計算をおこなわず、lossの推定値を返します。
つまり、2つのメソッドの違いは勾配計算をするかしないかということになります。
Optimizers
Pyroでは、modelとguideに任意の関数を指定できます。しかし、以下のような制約があります。
- guideは、obs引数を持つpyro.sample()を持たない。
- modelとguideは同じ引数を持つ。
modelとguideの関数内では複数のパラメータが出現します。そして、これらのパラメータをSVIを使って更新し、最適化していくことになります。この際、SVIはPyroで用意されているOptimizerを使ってパラメータを更新します。
つまり、パラメータはOptimizerがコントロールします。Pyroでは、optim.PyroOptimというクラスが用意されています。これは、Pytorchのtorch.optimのラッパーとなっており、挙動はtorch.optimと類似しているところがあります。
それでは具体的にOptimizerのプログラムを以下に示します。
from pyro.optim import Adam
adam_params = {"lr": 0.005, "betas": (0.95, 0.999)}
optimizer = Adam(adam_params)
簡単なプログラム例
例としてコイントスを考えます。コイントスを何回か繰り返したとき、コイントスに用いたコインは表と裏が出る確率が同じなのかどうかを調べます。
私たちは、このことを調べる前に、ある事実を知っているとします。
・アメリカ造幣局発行のスタンダードクォーターであること。(ここは直訳です。)
・コインは使い古してボロボロであるということ。
これらの事実を事前分布として取り入れます。
コインが作られたばかりの頃は表と裏の出る確率は同じだったでしょう。しかし、使われていくことでいろんな箇所が削られたり様々な要因によって、等確率ではなくなっていくと思われます。
次に、コインの表が出る=1、裏が出る=0として、表(もしくは裏)の出る確率をパラメータfで定めます。もし、コインが公正なコインであればf=0.5となるはずです。そしてパラメータfは、ベータ分布に従うと仮定します。ここが、先ほど話したコインの事前分布に関する部分です。パラメータfは、上で書いたような事実に基づいて、ある分布に従います。そのある分布は今回はベータ分布とします、という具合です。ベータ分布は2つのパラメータがあります。今回は2つとも10.0であるとします。すなわち、パラメータfの事前分布は、Beta(10, 10)に従います。
今回の例で用いるデータは、10回コイントスをおこなった結果です。簡単のため、以下のようにデータを生成します。
data = []
for _ in range(6):
data.append(torch.tensor(1.0))
for _ in range(4):
data.append(torch.tensor(0.0))
このdataを観測ずみデータとして使います。
また、今回は1回のコイントスによって、0か1のどちらかを観測するので、これを表すモデルとしてベルヌーイ分布を用います。したがって、モデルは以下のようになります。
import pyro.distributions as dist
def model(data):
alpha0 = torch.tensor(10.0)
beta0 = torch.tensor(10.0)
f = pyro.sample("latent_fairness", dist.Beta(alpha0, beta0))
for i in range(len(data)):
pyro.sample("obs_{}".format(i), dist.Bernoulli(f), obs=data[i])
alpha0とbeta0はベータ分布のパラメータです。そしてfor文の箇所では、各試行はベルヌーイ分布からのサンプルとなっています。
次にguideを設計します。今回は、ベルヌーイ分布の事前分布にベータ分布を設定しているので、共役事前分布の性質から、事後分布はベータ分布になります。
この事実を使ってguideを設計します。必ずしもこのように設計する必要はなく、むしろguideが今回のように厳密にベータ分布などの分布になるというわけではありません。簡単のため、今回はそのように設計しただけです。現実の事象を捉えて表現するときはこのように簡単にいかないこともあります。
guideは以下のようになります。
def guide(data):
alpha_q = pyro.param("alpha_q", torch.tensor(15.0),
constraint=constraints.positive)
beta_q = pyro.param("beta_q", torch.tensor(15.0),
constraint=constraints.positive)
pyro.sample("latent_fairness", dist.Beta(alpha_q, beta_q))
alpha_qとbeta_qは事後分布であるベータ分布のパラメータです。
ここで以下の点に注意です。
・潜在変数latent_fairnessは、modelと対応しているということ。
・modelとguideが同じ引数を取ること。
・alpha_qなどのパラメータがtensor型になっていること。
・constraintはパラメータに関する制約で、positiveとすると、正の値しか取らないという制約を加えることができる。
最後に、Optimizerの設定と、SVIによる学習のステップです。コードは以下のようになります。
adam_params = {"lr": 0.0005, "betas": (0.90, 0.999)}
optimizer = Adam(adam_params)
svi = SVI(model, guide, optimizer, loss=Trace_ELBO())
n_steps = 5000
for step in range(n_steps):
svi.step(data)
ここまでで大事な箇所の説明は終わりです。コード全体の実装は、Pyroのチュートリアルを参照してください。
まとめ
本記事では、Pyroで実際にmodelとguideを設計し、簡単な例で実装しました。ここまでの説明が理解できれば簡単なモデルの作成はできると思います。
ここまで読んで下さりありがとうございました!