PyTorch : Pyro イントロ (1) Pyro のモデル: プリミティブな分布から確率関数 (翻訳)
翻訳 : (株)クラスキャット セールスインフォメーション
更新日時 : 11/20, 11/07/2018 (v0.2.1)
作成日時 : 10/09/2018 (v0.2.1)
* 本ページは、Pyro のドキュメント Introduction : Models in Pyro: From Primitive Distributions to Stochastic Functions を
動作確認・翻訳した上で適宜、補足説明したものです:
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
Pyro のモデル: プリミティブな分布から確率関数
Pyro プログラムの基本ユニットは確率関数 (= stochastic function) です。これは次の 2 つの構成要素を結合した任意の Python callable です :
- 決定論的 Python コード; そして
- プリミティブ確率関数
具体的には、確率関数は (関数、メソッド、あるいは PyTorch nn.Module のように) __call__() メソッドを持つ任意の Python オブジェクトです。
チュートリアルとドキュメントを通して、しばしば 確率関数をモデルと呼びます、何故ならば確率関数は (プロセスによって) データが生成されるプロセスの単純化されたあるいは抽象的な記述を表わすために利用可能だからです。モデルを Pyro の確率関数で表わすことはモデルはちょうど通常の Python callable のように構成され、再利用され、インポートされ、そしてシリアライズされることを意味します。
難しい話しは抜きにして、基本的なビルディング・ブロックの一つを紹介します: プリミティブ確率関数 です。
プリミティブ確率関数
プリミティブ確率関数、あるいは分布は、確率関数の重要なクラスです、そのために 入力が与えられたときに 出力の確率を明示的に計算することができます。PyTorch 0.4 と Pyro 0.2 の時点では、Pyro は PyTorch の 分布ライブラリ を使用します。transforms を使用してカスタム分布もまた作成できます。
プリミティブ確率関数を使用することは簡単です。例えば、単位正規分布 $\mathcal{N}(0,1)$ からサンプル x をドローするためには、次を行ないます :
# import some dependencies import torch import pyro import pyro.distributions as dist
loc = 0. # 平均ゼロ scale = 1. # 単位分散 normal = dist.Normal(loc, scale) # 正規分布オブジェクトを作成する x = normal.sample() # N(0,1) からサンプルをドローする print("sample", x) print("log prob", normal.log_prob(x)) # score the sample from N(0,1)
sample tensor(0.1700) log prob tensor(-0.9334)
ここで、dist.Normal は Distribution クラスの callable インスタンスで、パラメータを取り sample と score メソッドを提供します。dist.Normal に渡されるパラメータは torch.Tensor であることに注意してください。これが必要なのは、推論の間 PyTorch の高速な数学と autograd 機能を利用することを望むからです。
pyro.sample プリミティブ
Pyro のコア言語プリミティブの一つは pyro.sample ステートメントです。pyro.sample を使用することは、一つの重要な違いとともにプリミティブ確率関数を呼び出すように単純です :
x = pyro.sample("my_sample", dist.Normal(loc, scale)) print(x)
ちょうど dist.Normal().sample() への直接呼び出しのように、これは単位正規分布からのサンプルを返します。決定的な違いはこのサンプルが名前付けられていることです。Pyro のバックエンドは、ランタイムに sample ステートメントを一意に識別してそれらの挙動を変更するためにこれらの名前を使用します。私達が見ていくように、これは、推論アルゴリズムの基礎となる様々な操作を Pyro がどのように実装できるかです。
単純なモデル
pyro.sample と pyro.distributions を紹介した今、単純なモデルを書くことが出来ます。現実世界の何かをモデル化することを望むために究極的には確率プログラミングに興味がありますので、具体的な何かを選択しましょう。
日々の平均気温と雲量を持つ多くのデータを持つと仮定しましょう。気温が晴れか曇りであったこととどのように相互作用するかについて推論することを望みます。それを行なう単純な確率関数は以下で与えられます :
def weather(): cloudy = pyro.sample('cloudy', dist.Bernoulli(0.3)) # Bernoulli 分布からのサンプルに 'cloudy' と名前をつける cloudy = 'cloudy' if cloudy.item() == 1.0 else 'sunny' mean_temp = {'cloudy': 55.0, 'sunny': 75.0}[cloudy] scale_temp = {'cloudy': 10.0, 'sunny': 15.0}[cloudy] temp = pyro.sample('temp', dist.Normal(mean_temp, scale_temp)) return cloudy, temp.item() for _ in range(3): print(weather())
これを行毎に調べていきましょう。最初に、行 2 では二値確率変数 ‘cloudy’ を定義するために pyro.sample を使用します、これは 0.3 のパラメータを持つベルヌーイ分布からのドローにより与えられます。ベルヌーイ分布は 0 か 1 を返しますので、行 3 では weather の返し値がより簡単に解析できるように値 cloudy を文字列に変換します。それでこのモデルに従えば時間の 30% は曇りで 70% は晴れです。
行 4-5 では行 6 で気温をサンプリングするために使用していくパラメータを定義します。これらのパラメータは行 2 でサンプリングした cloudy の特定の値に依存します。例えば、平均気温は曇りの日には 55 度 (華氏) で晴れの日には 75 度です。最後に行 7 で 2 つの値 cloudy と temp を返します。
手続き的には、weather() は 2 つのランダム・サンプルを返す非決定論的 Python callable です。何故ならば pyro.sample によりランダムネスが引き起こされるからです、けれども、それはそれを遥かに越えています。特に weather() は 2 つの名前付けられた変数: cloudy と temp に渡る同時確率分布を指定します。そのようなものとして、それは確率理論のテクニックを使用して (推論できる) 確率モデルを定義します。例えば私達は尋ねるかもしれません: 私が 70 度の気温を観測する場合、どのくらいの尤度で曇りでしょう? これらの類の質問をどのように定式化して答えるかが次のチュートリアルの主題です。
単純なモデルをどのように定義するかを今見てきました。それを作り直すことは簡単です。例えば :
def ice_cream_sales(): cloudy, temp = weather() expected_sales = 200. if cloudy == 'sunny' and temp > 80.0 else 50. ice_cream = pyro.sample('ice_cream', dist.Normal(expected_sales, 10.0)) return ice_cream
この類のモジュール性は、どのプログラマにも馴染みがあり、明らかに非常にパワフルです。しかしそれは (私達が表現したい) 総ての異なる類のモデルを包含するに十分なほどパワフルでしょうか?
普遍性: 確率的再帰 (Stochastic Recursion)、高階確率関数、そしてランダム制御フロー
Pyro は Python に埋め込まれていますので、確率関数は複雑な決定論的 Python を任意に含むことができてランダムネスは制御フローに自由に影響を与えることができます。例えば、pyro.sample の一意なサンプル名を (それが呼び出されたときにはいつでも) 渡すことに留意すると仮定すれば、その再帰性を非決定論的に打ち切る再帰関数を構築することができます。例えば幾何分布をこのように定義できます :
def geometric(p, t=None): if t is None: t = 0 x = pyro.sample("x_{}".format(t), dist.Bernoulli(p)) if x.item() == 0: return x else: return x + geometric(p, t + 1) print(geometric(0.5))
geometric() の名前 x_0, x_1, etc. は動的に生成されて異なる実行は名前付けられた確率変数の異なる数字を持てることに注意してください。
他の確率関数を入力として受け取り出力として生成する確率関数もまた自由に定義できます :
def normal_product(loc, scale): z1 = pyro.sample("z1", dist.Normal(loc, scale)) z2 = pyro.sample("z2", dist.Normal(loc, scale)) y = z1 * z2 return y def make_normal_normal(): mu_latent = pyro.sample("mu_latent", dist.Normal(0, 1)) fn = lambda scale: normal_product(mu_latent, scale) return fn print(make_normal_normal()(1.))
ここで make_normal_normal() は確率関数で、一つの引数を取り、そして実行時には 3 つの名前付けられた確率変数を生成します。
Pyro が、ランダム制御フローと連動したこのような — 反復、再帰、高階関数, etc.— 任意の Python コードをサポートするという事実は Pyro 確率関数が universal である、i.e. それらが 任意の計算可能な確率分布を表わすために使用できる ことを意味します。続くチュートリアルで見るように、これは信じられないほどパワフルです。
これが何故 Pyro が PyTorch 上に構築されたかの一つの理由であることを強調する価値があります: 動的計算グラフは GPU で高速化された tensor math から恩恵を受けられる普遍モデルを可能にする際の重要な構成要素です。
以上