Pyro 1.3 : Examples : 変分オートエンコーダ

Pyro 1.3 : Examples : 変分オートエンコーダ (翻訳)

翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 07/21/2020 (1.3.1)

* 本ページは、Pyro の以下のドキュメントを翻訳した上で適宜、補足説明したものです:

* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

無料セミナー開催中 クラスキャット主催 人工知能 & ビジネス Web セミナー

人工知能とビジネスをテーマにウェビナー (WEB セミナー) を定期的に開催しています。スケジュールは弊社 公式 Web サイト でご確認頂けます。
  • お住まいの地域に関係なく Web ブラウザからご参加頂けます。事前登録 が必要ですのでご注意ください。
  • Windows PC のブラウザからご参加が可能です。スマートデバイスもご利用可能です。

お問合せ : 本件に関するお問い合わせ先は下記までお願いいたします。

株式会社クラスキャット セールス・マーケティング本部 セールス・インフォメーション
E-Mail:sales-info@classcat.com ; WebSite: https://www.classcat.com/
Facebook: https://www.facebook.com/ClassCatJP/

 

変分オートエンコーダ

イントロダクション

変分オートエンコーダ (VAE) はほぼ間違いなく深層確率的モデリングを実現する最も単純なセットアップです。ここでは用語の選択に私達は慎重でいることに注意してください。VAE はそのようなモデルではありません — むしろ VAE はモデルの特定のクラスのために変分推論を行なうための特定のセットアップです。モデルのクラスは非常に幅広いです : 基本的には潜在確率変数を持つ任意の (教師なし) 密度推定器です。そのようなモデルの基本構造は単純で、殆ど見かけによらずそのようなものです (Fig. 1 参照)。


Figure 1: 私達が興味ある深層モデルのクラス

ここで興味がある類のモデルの構造をグラフィカルモデルとして描写しました。私達は $N$ 観測データポイント \(\{ \bf x_i \}\) を持ちます。各データポイントは (局所) 潜在確率変数 \(\bf z_i\) により生成されます。パラメータ \(\theta\) もあります、これは総てのデータポイントがそれに依拠するという意味でグローバルです (これが何故それが矩形の外側に描かれているかです)。\(\theta\) はパラメータですから、それについて私達がベイジアンである何かではないことに注意してください。最後に、ここで特に重要なことは各 \(\bf x_i\) が複雑で非線形な方法で \(\bf z_i\) に依拠することを許容することです。実際にこの依存性はパラメータ \(\theta\) を持つ (深層) ニューラルネットワークによりパラメータ化されます。モデルのこのクラスのための推論を特に挑戦的にしているのはこの非線形です。

もちろんこの非線形構造もまた複雑なデータをモデル化するためにこのモデルのクラスが非常に柔軟なアプローチを供給する一つの理由です。実際にモデルの構成要素の各々が様々な異なる方法で「再構成 (= reconfigure)」可能であることは強調するに値します。例えば :

  • \(p_\theta({\bf x} | {\bf z})\) のニューラルネットワークは通常の総ての方法で変更されます (層数、非線形のタイプ、隠れユニットの数, etc.)。
  • 手元のデータセットに適合する観測尤度を選択できます : gaussian, bernoulli, categorical, etc.
  • 潜在空間の次元数を選択できます。

グラフィカルモデル表現はモデルの構造について考えるための有用な方法ですが、同時確率密度の明示的な因子分解を見ることもまた有益です :

\[
p({\bf x}, {\bf z}) = \prod_{i=1}^N p_\theta({\bf x}_i | {\bf z}_i) p({\bf z}_i)
\]

\(p({\bf x}, {\bf z})\) がこのような項の積に分解されるという事実は \(\bf z_i\) を局所確率変数と呼ぶときに意味するものを明確にします。任意の特定の \(i\) について、単一データポイント \(\bf x_i\) だけが \(\bf z_i\) に依拠します。そのようなものとして \(\{\bf z_i\}\) はローカル構造 i.e. 各データポイントに private な構造を記述します。この分解された構造はまた、学習の過程でサブサンプリングを行えることも意味します。そのようなものとしてこの種類のモデルは巨大なデータ設定に従います (= amenable)。(これと関連トピックの更なる議論については SVI Part II を見てください。)

モデルについてはそれだけのことです。観測が複雑な非線型な方法で潜在確率変数に依拠しますので、潜在変数に渡る事後分布に複雑な構造を持つことを想定します。その結果このモデルで推論するため、ガイド (i.e. 変分分布) の柔軟な族を指定する必要があります。巨大なデータセットにスケールできることを望みますので、私達のガイドは幾つかの変分パラメータを制御下に保持するために amortization を利用していきます (amortization のある程度一般的な議論は SVI パート II 参照)。

思い出してください、ガイドのジョブは潜在確率変数のために良い値を「推測 (= guess)」することです — それらがモデル事前分布に対して真でそしてデータに対して真であるという意味で「良い」です。amortization を利用しないのであれば、各データポイント \(\bf x_i\) に対して変分パラメータ \(\{ \lambda_i \}\) を導入するでしょう。これらの変分パラメータは \(\bf z_i\) の「良い」値についての信念を表します ; 例えば、それらは \({\bf z}_i\) 空間のガウス分布の平均と分散をエンコードできるでしょう。amortization は変分パラメータ \(\{ \lambda_i \}\) を導入するよりも寧ろ、代わりに各 \(\bf x_i\) を適切な \(\lambda_i\) にマップする関数を学習することを意味します。この関数が柔軟であることを必要としますので、それをニューラルネットワークとしてパラメータ化します。こうして総ての \(N\) データポイント \({\bf x}_i\) に対してインスタン化できる潜在的 \(\bf z\) 空間に渡る分布のパラメータ化された族で終わります (Fig. 2 参照)。


Figure 2: ガイドのグラフィカル表現

ガイド \(q_{\phi}({\bf z} | {\bf x})\) は、総てのデータポイントにより共有される大域パラメータ \(\phi\) によりパラメータ化されることに注意してください。推論の目標は 2 つの条件が満たされるように \(\theta\) と \(\phi\) のための「良い」値を見つけることです :

  • log エビデンス \(\log p_\theta({\bf x})\) は巨大です。これは私達のモデルがデータへの良い fit であることを意味します。
  • ガイド \(q_{\phi}({\bf z} | {\bf x})\) は事後分布への良い近似を提供します。

(確率的変分推論へのイントロダクションについては SVI パート I 参照。)

この時点でズームアウトして私達のセットアップの高位構造を考えます。具体性のために、\(\{ \bf x_i \}\) を画像としてモデルが画像の生成モデルであると仮定しましょう。ひとたび \(\theta\) の良い値を学習したならば次のようにモデルから画像を生成できます :

  • 事前分布 \(p({\bf z})\) に従って \(\bf z\) をサンプリングする
  • 尤度 \(p_\theta({\bf x}|{\bf z})\) に従って\(\bf x\) をサンプリングする

各画像は潜在コード \(\bf z\) により表現されそしてそのコードは尤度を使用して画像にマップされます、これは 学習した \(\theta\) に依拠します。これがこのコンテキストで尤度がしばしばデコーダと呼ばれる理由です : そのジョブは \(\bf z\) を \(\bf x\) にデコードすることです。これは確率モデルですので、与えられたデータポイント \(\bf x\) をエンコードする \(\bf z\) についての不確かさがあることに注意してください。

ひとたび \(\theta\) と \(\phi\) のための良い値を学習したのであれば次の課題も通り抜けることができます :

  • 与えられた画像 \(\bf x\) で始めます。
  • ガイドを使用してそれを \(\bf z\) としてエンコードします。
  • モデル尤度を使用して \(\bf z\) をデコードして再構築された画像 \({\bf x}_\rm{reco}\) を得ます。

\(\theta\) と \(\phi\) のための良い値を学習したのであれば、\(\bf x\) と \({\bf x}_\rm{reco}\) は類似するはずです。これは用語オートエンコーダがこのセットアップを記述するために何故使用されることになるかを明らかにするはずです : モデルはデコーダでガイドはエンコーダです。まとめて、それらはオートエンコーダとして考えることができます。

 

Pyro の VAE

Pyro でどのように VAE を実装するかを見ましょう。モデル化していくデータセットは MNIST です、手書き数字の画像のコレクションです。これはポピュラーなベンチマーク・データセットですので、書く必要があるボイラープレート・コードの総量を減らすために PyTorch の便利なデータローダ機能を利用できます :

import os

import numpy as np
import torch
import torchvision.datasets as dset
import torch.nn as nn
import torchvision.transforms as transforms

import pyro
import pyro.distributions as dist
import pyro.contrib.examples.util  # patches torchvision
from pyro.infer import SVI, Trace_ELBO
from pyro.optim import Adam
assert pyro.__version__.startswith('1.3.0')
pyro.enable_validation(True)
pyro.distributions.enable_validation(False)
pyro.set_rng_seed(0)
# Enable smoke test - run the notebook cells on CI.
smoke_test = 'CI' in os.environ
# for loading and batching MNIST dataset
def setup_data_loaders(batch_size=128, use_cuda=False):
    root = './data'
    download = True
    trans = transforms.ToTensor()
    train_set = dset.MNIST(root=root, train=True, transform=trans,
                           download=download)
    test_set = dset.MNIST(root=root, train=False, transform=trans)

    kwargs = {'num_workers': 1, 'pin_memory': use_cuda}
    train_loader = torch.utils.data.DataLoader(dataset=train_set,
        batch_size=batch_size, shuffle=True, **kwargs)
    test_loader = torch.utils.data.DataLoader(dataset=test_set,
        batch_size=batch_size, shuffle=False, **kwargs)
    return train_loader, test_loader

ここで注目する主要なことはピクセル濃度を範囲 [0.0, 1.0] に正規化するために transforms.ToTensor() を使用することです。

次にデコーダ・ネットワークをカプセル化する PyTorch モジュールを定義します :

class Decoder(nn.Module):
    def __init__(self, z_dim, hidden_dim):
        super().__init__()
        # setup the two linear transformations used
        self.fc1 = nn.Linear(z_dim, hidden_dim)
        self.fc21 = nn.Linear(hidden_dim, 784)
        # setup the non-linearities
        self.softplus = nn.Softplus()
        self.sigmoid = nn.Sigmoid()

    def forward(self, z):
        # define the forward computation on the latent z
        # first compute the hidden units
        hidden = self.softplus(self.fc1(z))
        # return the parameter for the output Bernoulli
        # each is of size batch_size x 784
        loc_img = self.sigmoid(self.fc21(hidden))
        return loc_img

潜在コード $z$ が与えられたとき、Decoder の forward 呼び出しは画像空間の Bernoulli 分布のためのパラメータを返します。各画像はサイズ 28×28=784 なので、loc_img はサイズ batch_size x 784 です。

次にエンコーダ・ネットワークをカプセル化する PyTorch モジュールを定義します :

class Encoder(nn.Module):
    def __init__(self, z_dim, hidden_dim):
        super().__init__()
        # setup the three linear transformations used
        self.fc1 = nn.Linear(784, hidden_dim)
        self.fc21 = nn.Linear(hidden_dim, z_dim)
        self.fc22 = nn.Linear(hidden_dim, z_dim)
        # setup the non-linearities
        self.softplus = nn.Softplus()

    def forward(self, x):
        # define the forward computation on the image x
        # first shape the mini-batch to have pixels in the rightmost dimension
        x = x.reshape(-1, 784)
        # then compute the hidden units
        hidden = self.softplus(self.fc1(x))
        # then return a mean vector and a (positive) square root covariance
        # each of size batch_size x z_dim
        z_loc = self.fc21(hidden)
        z_scale = torch.exp(self.fc22(hidden))
        return z_loc, z_scale

画像 x が与えられたとき Encoder の forward 呼び出しは平均と共分散を返します、それらは一緒に潜在空間の (対角) ガウス分布をパラメータ化します。

手元のエンコーダとデコーダ・ネットワークで、今はモデルとガイドを表わす確率関数を書き下すことができます。最初にモデルです :

# define the model p(x|z)p(z)
def model(self, x):
    # register PyTorch module `decoder` with Pyro
    pyro.module("decoder", self.decoder)
    with pyro.plate("data", x.shape[0]):
        # setup hyperparameters for prior p(z)
        z_loc = x.new_zeros(torch.Size((x.shape[0], self.z_dim)))
        z_scale = x.new_ones(torch.Size((x.shape[0], self.z_dim)))
        # sample from prior (value will be sampled by guide when computing the ELBO)
        z = pyro.sample("latent", dist.Normal(z_loc, z_scale).to_event(1))
        # decode the latent code z
        loc_img = self.decoder.forward(z)
        # score against actual images
        pyro.sample("obs", dist.Bernoulli(loc_img).to_event(1), obs=x.reshape(-1, 784))

model() は画像 x のミニバッチを入力として取る callable であることに注意してください。これはサイズ batch_size x 784 の torch.Tensor です。

model() の内部で行なう最初のことは Pyro で (前にインスタンス化された) デコーダ・モジュールを登録することです。それに適切な (そして一意な) 名前を与えることに注意してください。pyro.module へのこの呼び出しは Pyro にデコーダ・ネットワーク内の総てのパラメータについて知らせます。

次に単なる単位正規ガウス分布である、事前分布のためのハイパーパラメータをセットアップします。注意してください :- pyro.plate を通してミニバッチ (i.e. 最左端次元) のデータの中の独立性を特に明示します。また、潜在変数 z からサンプリングするとき .to_event(1) の使用方法に注意してください – これは、サンプルを batch_size = z_dim を持つ単変量正規分布から生成されたものとして取り扱う代わりに、それらを対角共分散を持つ多変量正規分布から生成されたものとして取り扱うことを確実にします。そのようなものとして、「潜在」サンプルのための .log_prob を評価するとき各次元に沿う対数確率は総計されます (= sum out)。より詳細は Tensor Shapes チュートリアルを参照してください。- 画像のミニバッチ全体を処理していますので、ミニバッチ size に等しい z_loc と z_scale の最左端次元が必要です – GPU 上の場合には、新たに作成された tensor が同じ GPU デバイス上にあることを確実にするために new_zeros と new_ones を使用します。

次に確率変数に一意な Pyro 名 ‘latent’ を与えることを確実にして、事前分布から潜在変数 z をサンプリングします。それから loc_img を返すデコーダ・ネットワークを通して z を渡します。それから loc_img によりパラメータ化された Bernoulli 尤度に対してミニバッチ x の観測された画像をスコアします。総てのピクセルが最右端次元にあるように x を平坦化することに注意してください。

That’s all there is to it! モデルの Pyro プリミティブのフローがモデルの生成ストーリーをどれほど密接に追っているかに注意してください、e.g. Figure 1 でカプセル化されたもののように。今はガイドに進みます :

# define the guide (i.e. variational distribution) q(z|x)
def guide(self, x):
    # register PyTorch module `encoder` with Pyro
    pyro.module("encoder", self.encoder)
    with pyro.plate("data", x.shape[0]):
        # use the encoder to get the parameters used to define q(z|x)
        z_loc, z_scale = self.encoder.forward(x)
        # sample the latent code z
        pyro.sample("latent", dist.Normal(z_loc, z_scale).to_event(1))

ちょうどモデルでのように、最初に使用している PyTorch モジュール (つまり encoder) を Pyro で登録します。画像 x のミニバッチを取りそれをエンコーダを通して渡します。それからエンコーダ・ネットワークにより出力されたパラメータを使用してミニバッチの各画像に対して潜在変数の値をサンプリングするために正規分布を利用します。重要なこととして、(モデルでもそうしたように) 潜在確率変数のために同じ名前を使用します : ‘latent’。また、ミニバッチ次元の独立性を明示するための pyro.plate と、そして z_dims 上の依存性を強要する .to_event(1) の利用方法には注意してください、モデルで行なったことと正確に同じです。

完全なモデルとガイドを定義した今、推論に進むことができます。しかしそれを行なう前に PyTorch モジュールでモデルとガイドをどのようにパッケージ化するかを見ましょう :

class VAE(nn.Module):
    # by default our latent space is 50-dimensional
    # and we use 400 hidden units
    def __init__(self, z_dim=50, hidden_dim=400, use_cuda=False):
        super().__init__()
        # create the encoder and decoder networks
        self.encoder = Encoder(z_dim, hidden_dim)
        self.decoder = Decoder(z_dim, hidden_dim)

        if use_cuda:
            # calling cuda() here will put all the parameters of
            # the encoder and decoder networks into gpu memory
            self.cuda()
        self.use_cuda = use_cuda
        self.z_dim = z_dim

    # define the model p(x|z)p(z)
    def model(self, x):
        # register PyTorch module `decoder` with Pyro
        pyro.module("decoder", self.decoder)
        with pyro.plate("data", x.shape[0]):
            # setup hyperparameters for prior p(z)
            z_loc = x.new_zeros(torch.Size((x.shape[0], self.z_dim)))
            z_scale = x.new_ones(torch.Size((x.shape[0], self.z_dim)))
            # sample from prior (value will be sampled by guide when computing the ELBO)
            z = pyro.sample("latent", dist.Normal(z_loc, z_scale).to_event(1))
            # decode the latent code z
            loc_img = self.decoder.forward(z)
            # score against actual images
            pyro.sample("obs", dist.Bernoulli(loc_img).to_event(1), obs=x.reshape(-1, 784))

    # define the guide (i.e. variational distribution) q(z|x)
    def guide(self, x):
        # register PyTorch module `encoder` with Pyro
        pyro.module("encoder", self.encoder)
        with pyro.plate("data", x.shape[0]):
            # use the encoder to get the parameters used to define q(z|x)
            z_loc, z_scale = self.encoder.forward(x)
            # sample the latent code z
            pyro.sample("latent", dist.Normal(z_loc, z_scale).to_event(1))

    # define a helper function for reconstructing images
    def reconstruct_img(self, x):
        # encode image x
        z_loc, z_scale = self.encoder(x)
        # sample in latent space
        z = dist.Normal(z_loc, z_scale).sample()
        # decode the image (note we don't sample in image space)
        loc_img = self.decoder(z)
        return loc_img

ここで強調したいことは 2 つのモジュール、エンコーダとデコーダは VAE の属性であることです (それ自身は nn.Module から継承しています)。これはそれらが両者とも VAE モジュールに属するものとして自動的に登録されるという結果になります。従って、例えば、VAE のインスタンス上で parameters() を呼び出すとき、PyTorch は総ての関連パラメータを返すことを知ります。それはまた GPU 上で実行している場合、cuda() への呼び出しが総ての (サブ) モジュールの総てのパラメータを GPU メモリに移動することも意味します。

 

推論

今は推論のための準備ができました。次のセクションで完全なコードを参照します。

最初に VAE モジュールのインスタンスをインスタンス化します。

vae = VAE()

それから Adam optimizer のインスタンスをセットアップします。

optimizer = Adam({"lr": 1.0e-3})

それから推論アルゴリズムをセットアップします、これは ELBO を最大化することによりモデルとガイドのための良いパラメータを学習していきます :

svi = SVI(vae.model, vae.guide, optimizer, loss=Trace_ELBO())

That’s all there is to it. 今は訓練ループを定義しなければならないだけです :

def train(svi, train_loader, use_cuda=False):
    # initialize loss accumulator
    epoch_loss = 0.
    # do a training epoch over each mini-batch x returned
    # by the data loader
    for x, _ in train_loader:
        # if on GPU put mini-batch into CUDA memory
        if use_cuda:
            x = x.cuda()
        # do ELBO gradient and accumulate loss
        epoch_loss += svi.step(x)

    # return epoch loss
    normalizer_train = len(train_loader.dataset)
    total_epoch_loss_train = epoch_loss / normalizer_train
    return total_epoch_loss_train

総てのミニバッチ・ロジックはデータローダにより処理されることに注意してください。訓練ループの要点は svi.step(x) です。ここで注目すべきことは 2 つあります :

  • step への任意の引数はモデルとガイドに渡されます。結果としてモデルとガイドは同じ呼び出しシグネチャを持つ必要があります。
  • step は損失のノイズを持つ推定を返します (i.e. minus ELBO)。この推定はどのような方法でも正規化されていません、従って e.g. それはミニバッチのサイズでスケールします。

評価ロジックを追加するためのロジックは類推的です :

def evaluate(svi, test_loader, use_cuda=False):
    # initialize loss accumulator
    test_loss = 0.
    # compute the loss over the entire test set
    for x, _ in test_loader:
        # if on GPU put mini-batch into CUDA memory
        if use_cuda:
            x = x.cuda()
        # compute ELBO estimate and accumulate loss
        test_loss += svi.evaluate_loss(x)
    normalizer_test = len(test_loader.dataset)
    total_epoch_loss_test = test_loss / normalizer_test
    return total_epoch_loss_test

基本的には行なう必要がある唯一の変更は step の代わりに evaluate_loss を呼び出すことです。この関数は ELBO の推定を計算しますが、どのような勾配ステップも取りません。

ハイライトしたいコードの最後のピースは VAE クラスのヘルパー・メソッド reconstruct_img です : これは (コードに翻訳された) イントロダクションで説明した単なる画像再構築実験です。画像を取りそれをエンコーダに渡します。それからエンコーダから提供されるガウス分布を使用して潜在空間でサンプリングします。最後に潜在コードを画像にデコードします : それでサンプリングする代わりに平均ベクトル loc_img を返します。sample() ステートメントは確率的ですので、reconstruct_img 関数を実行するたびに z の異なるドローを得ることに注意してください。良いモデルとガイドを学習したのであれば — 特に良い潜在表現を学習したのであれば — z サンプルのこの大多数は書き数字の異なるスタイルに対応するでしょう、そして再構築された画像は興味深い様々な異なるスタイルを展示します。

 

コードとサンプル結果

訓練は訓練データセットに渡りエビデンス下限 (ELBO, evidence lower bound) を最大化することに相当します。100 反復の間訓練してテストデータセットのために ELBO を評価します。Figure 3 を見てください。

# Run options
LEARNING_RATE = 1.0e-3
USE_CUDA = False

# Run only for a single iteration for testing
NUM_EPOCHS = 1 if smoke_test else 100
TEST_FREQUENCY = 5
train_loader, test_loader = setup_data_loaders(batch_size=256, use_cuda=USE_CUDA)

# clear param store
pyro.clear_param_store()

# setup the VAE
vae = VAE(use_cuda=USE_CUDA)

# setup the optimizer
adam_args = {"lr": LEARNING_RATE}
optimizer = Adam(adam_args)

# setup the inference algorithm
svi = SVI(vae.model, vae.guide, optimizer, loss=Trace_ELBO())

train_elbo = []
test_elbo = []
# training loop
for epoch in range(NUM_EPOCHS):
    total_epoch_loss_train = train(svi, train_loader, use_cuda=USE_CUDA)
    train_elbo.append(-total_epoch_loss_train)
    print("[epoch %03d]  average training loss: %.4f" % (epoch, total_epoch_loss_train))

    if epoch % TEST_FREQUENCY == 0:
        # report test diagnostics
        total_epoch_loss_test = evaluate(svi, test_loader, use_cuda=USE_CUDA)
        test_elbo.append(-total_epoch_loss_test)
        print("[epoch %03d] average test loss: %.4f" % (epoch, total_epoch_loss_test))


Figure 3: 訓練過程に渡りテスト ELBO がどのように展開するか

 
次にモデルからランダムにサンプリングされた画像のセットを示します。z のランダム・サンプルをドローして各々一つに対して画像を生成することによりこれらは生成されます、Figure 4 を見てください。


Figure 4: 生成モデルからのサンプル

私達はまた総ての MNIST 画像をエンコードしてそれらの平均を 2-次元 T-SNE 空間に埋め込むことによりテストデータセット全体の 50-次元潜在空間を学習します。それからそのクラスにより各々の埋め込まれた画像を彩色します。結果としての Figure 5 は各クラス・クラスタ内の分散を持つクラスによる分離を示します。


Figure 5: 潜在変数 z の T-SNE 埋め込み。カラーは数字の異なるクラスに対応

GitHub 上のフルコードを見てください。

 

References

  • [1] Auto-Encoding Variational Bayes, Diederik P Kingma, Max Welling
  • [2] Stochastic Backpropagation and Approximate Inference in Deep Generative Models, Danilo Jimenez Rezende, Shakir Mohamed, Daan Wierstra
 

以上