TensorFlow Probability : Tutorials : ガウス過程 潜在変数 (状態空間) モデル (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 10/30/2018
* 本ページは、TensorFlow の本家サイトの TensorFlow Probability – Tutorials の次のページを翻訳した上で
適宜、補足説明したものです:
* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
TensorFlow Probability : ガウス過程 潜在変数 (状態空間) モデル
潜在変数モデルは高次元データの隠れ構造を捕捉することを企てます。サンプルは主成分分析 (PCA) と因子分析を含みます。ガウス過程は「ノンパラメトリック」モデルでこれは局所相関構造と不確かさを柔軟に捕捉できます。ガウス過程潜在変数モデル (Lawrence, 2004) はこれらの概念を結合します。
背景: ガウス過程
ガウス過程は確率変数の任意のコレクションで任意の有限個の部分集合に渡る周辺分布が多変量正規分布であるようなものです。回帰のコンテキストにおける GP の詳細な見え方は、TensorFlow Probability のガウス過程回帰 をチェックしてください。
GP が構成されるコレクションの確率変数の各々をラベル付けするいわゆるインデックス集合を使用します。有限のインデックス集合の場合には、単に多変量正規分布を得ます。けれども無限コレクションを考慮するとき、GP は非常に興味深いです。$\mathbb{R}^D$ のようなインデックス集合の場合、そこでは $D$ 次元空間の総ての点について確率変数を持ち、GP はランダム関数に渡る分布として考えることができます。そのような GP からの単一のドローは、それが現実化するならば、$\mathbb{R}^D$ の総ての点に (結合的に正規分布に従う) 値を割り当てるでしょう。この colab では、ある $\mathbb{R}^D$ に渡る GP に注目します。
正規分布はそれらの最初と 2 番目の統計値で完全に決定されます — 実際に、正規分布を定義する一つの方法はその高次キュムラントが総てゼロであるものとしてです。これは GP の場合にもまた、当てはまります: 平均と共分散* を記述することで GP を完全に指定します。有限次元多変量正規分布については、平均はベクトルで共分散は正方 (= square) で、対称な正定値行列であることを思い出してください。無限次元 GP では、これらの構造はインデックス集合の各点で定義される平均関数, $m : \mathbb{R}^D \to \mathbb{R}$、そして共分散「カーネル」関数, $k : \mathbb{R}^D \times \mathbb{R}^D \to \mathbb{R}$ に一般化されます。カーネル関数は 正定値 であることが要求されます、これは本質的には、有限集合の点に制限されれば、それは正定値行列を生成することを言っています。
GP の構造の殆どはその共分散カーネル関数に由来します — この関数はサンプリングされた関数の値が近くの (or それほど近くはない) 点に渡りどのように変化するかを記述します。異なる共分散関数は滑らかさの異なる度合いに働きかけます。一つの一般的に使用されるカーネル関数は「指数二次 (= exponentiated quadratic)」 (a.k.a. ガウシアン、二乗指数 (= squared exponential)、動径基底関数 (= radial basis function)), $k(x, x’) = \sigma^2 e^{(x – x^2) / \lambda^2}$ です。他の例は伝統的なテキスト Gaussian Processes for Machine Learning 内に加えて、David Duvenaud の kernel cookbook ページ で概説されます。
GP の適用: 回帰と潜在変数モデル
GP を使用できる一つの方法は回帰のためです: 入力 $\{x_i\}_{i=1}^N$ (インデックス集合の要素)と観測 $\{y_i\}_{i=1}^N$ の形式で多くの観測されたデータが与えられたとき、点 $\{x_j^*\}_{j=1}^M$ の新しい集合における事後予測分布を形成するためにこれらを使用できます。分布は総て正規分布ですので、これは何某かの straightforward な線形代数に要約されます (しかし注意してください: 必要な計算はデータポイントの数で 3 次の実行時間を持ちそしてデータポイントの数で 2 次の空間を必要とします — これは GP の使用における主要な制限的要因で現在の研究の大半は正確な事後推論への計算的に実行可能な代替案に焦点を合わせています)。TFP における GP 回帰 で GP 回帰を詳しくカバーします。
GP を使用できるもう一つの方法は潜在変数モデルとしてです: 高次元観測 (e.g., 画像) のコレクションが与えられたとき、ある低次元潜在構造を推測することができます。潜在構造を条件とするとき、巨大な数の出力 (画像のピクセル) は互いに独立であることを仮定します。このモデルの訓練は以下から成ります :
- モデル・パラメータを最適化します (カーネル関数パラメータと、e.g. 観測ノイズ分散)、そして
- 各訓練観測 (画像) に対して、インデックス集合の対応する点位置を見つけます。最適化の総てはデータの周辺対数尤度を最大化することにより成されます。
インポート
import numpy as np import tensorflow as tf from tensorflow_probability import distributions as tfd from tensorflow_probability import positive_semidefinite_kernels as tfk
numpy と matplotlib から対話的な名前空間を取り込みます。
def reset_session(): """Creates a new global, interactive session in Graph-mode.""" global sess try: tf.reset_default_graph() sess.close() except: pass sess = tf.InteractiveSession() reset_session()
MNIST データをロードする
# Load the MNIST data set and isolate a subset of it. (x_train, y_train), (_, _) = tf.keras.datasets.mnist.load_data() N = 1000 small_x_train = x_train[:N, ...].astype(np.float64) / 256. small_y_train = y_train[:N]
訓練可能な変数を準備する
3 モデル・パラメータと潜在入力を結合的に訓練していきます。
# Create some trainable model parameters. We also constrain them to be strictly # positive amplitude = np.finfo(np.float64).eps + tf.nn.softplus( tf.get_variable(name='amplitude', dtype=tf.float64, initializer=np.float64(1.))) length_scale = np.finfo(np.float64).eps + tf.nn.softplus( tf.get_variable(name='length_scale', dtype=tf.float64, initializer=np.float64(1.))) observation_noise_variance = np.finfo(np.float64).eps + tf.nn.softplus( tf.get_variable(name='observation_noise_variance', dtype=tf.float64, initializer=np.float64(1.)))
# We need to flatten the images and, somewhat unintuitively, transpose from # shape [100, 784] to [784, 100]. This is because the 784 pixels will be # treated as *independent* conditioned on the latent inputs, meaning we really # have a batch of 784 GP's with 100 index_points. observations_ = small_x_train.reshape(N, -1).transpose() # Create a collection of N 2-dimensional index points that will represent our # latent embeddings of the data. (Lawrence, 2004) prescribes initializing these # with PCA, but a random initialization actually gives not-too-bad results, so # we use this for simplicity. For a fun exercise, try doing the # PCA-initialization yourself! init_ = np.random.normal(size=(N, 2)) latent_index_points = tf.get_variable( name='latent_index_points', dtype=tf.float64, initializer=init_)
モデルと訓練 ops を構築する
# Create our kernel and GP distribution kernel = tfk.ExponentiatedQuadratic(amplitude, length_scale) gp = tfd.GaussianProcess( kernel=kernel, index_points=latent_index_points, observation_noise_variance=observation_noise_variance) # Compute the log probability density of each of the observations. This is what # we will maximize below. log_probs = gp.log_prob(observations_, name='log_prob') # Our training loss is the negative log likelihood of the observations. Since # the observations are IID, the log likelihoods are summed. We normalize the sum # (take the average) to reduce the risk of extreme values subject to floating # point issues. loss = -tf.reduce_mean(log_probs) optimizer = tf.train.AdamOptimizer(learning_rate=.1) train_op = optimizer.minimize(loss)
予測モデルとサンプリング ops を構築する
# We'll draw samples at evenly spaced points on a 10x10 grid in the latent # input space. sample_grid_points = 10 grid_ = np.linspace(-4, 4, sample_grid_points).astype(np.float64) # Create a 10x10 grid of 2-vectors, for a total shape [10, 10, 2] grid_ = np.stack(np.meshgrid(grid_, grid_), axis=-1) # This part's a bit subtle! What we defined above was a batch of 784 (=28x28) # independent GP distributions over the input space. Each one corresponds to a # single pixel of an MNIST image. Now what we'd like to do is draw 100 (=10x10) # *independent* samples, each one separately conditioned on all the observations # as well as the learned latent input locations above. # # The GP regression model below will define a batch of 784 independent # posteriors. We'd like to get 100 independent samples each at a different # latent index point. We could loop over the points in the grid, but that might # be a bit slow. Instead, we can vectorize the computation by tacking on *even # more* batch dimensions to our GaussianProcessRegressionModel distribution. # In the below grid_ shape, we have concatentaed # 1. batch shape: [sample_grid_points, sample_grid_points, 1] # 2. number of examples: [1] # 3. number of latent input dimensions: [2] # The `1` in the batch shape will broadcast with 784. The final result will be # samples of shape [10, 10, 784, 1]. The `1` comes from the "number of examples" # and we can just `np.squeeze` it off. grid_ = grid_.reshape(sample_grid_points, sample_grid_points, 1, 1, 2) # Create the GPRegressionModel instance which represents the posterior # predictive at the grid of new points. gprm = tfd.GaussianProcessRegressionModel( kernel=kernel, index_points=grid_, observation_index_points=latent_index_points, observations=observations_) # Create a sample operation in the graph. samples = gprm.sample()
訓練と結果の潜在埋め込みのプロット
# Initialize variables and train! sess.run(tf.global_variables_initializer()) num_iters = 100 log_interval = 20 lips_ = np.zeros((num_iters, N, 2), np.float64) for i in range(num_iters): _, loss_, lips_[i] = sess.run([train_op, loss, latent_index_points]) if i % log_interval == 0 or i + 1 == num_iters: print("Loss at step %d: %f" % (i, loss_))
Loss at step 0: 869611.863774 Loss at step 20: 330985.762144 Loss at step 40: -90885.405676 Loss at step 60: -182680.904868 Loss at step 80: -207781.362585 Loss at step 99: -212932.604949
結果をプロットする
# Plot the latent locations before and after training plt.figure(figsize=(7, 7)) plt.title("Before training") plt.grid('off') plt.scatter(x=init_[:, 0], y=init_[:, 1], c=y_train[:N], cmap=plt.get_cmap('Paired'), s=50) plt.show() plt.figure(figsize=(7, 7)) plt.title("After training") plt.grid('off') plt.scatter(x=lips_[-1, :, 0], y=lips_[-1, :, 1], c=y_train[:N], cmap=plt.get_cmap('Paired'), s=50) plt.show()
データ上で条件付けられたサンプルのドローと潜在埋め込み
潜在空間の 2-d グリッド上の 100 点でサンプリングします。
samples_ = sess.run(samples) # Plot the grid of samples at new points. We do a bit of tweaking of the samples # first, squeezing off extra 1-shapes and normalizing the values. samples_ = np.squeeze(samples_) samples_ = ((samples_ - samples_.min(-1, keepdims=True)) / (samples_.max(-1, keepdims=True) - samples_.min(-1, keepdims=True))) samples_ = samples_.reshape(sample_grid_points, sample_grid_points, 28, 28) samples_ = samples_.transpose([0, 2, 1, 3]) samples_ = samples_.reshape(28 * sample_grid_points, 28 * sample_grid_points) plt.figure(figsize=(7, 7)) ax = plt.subplot() ax.grid('off') ax.imshow(-samples_, interpolation='none', cmap='Greys') plt.show()
以上