ベイジアンスイッチポイント分析

TensorFlow.orgで表示 GoogleColabで実行 GitHubでソースを表示 ノートブックをダウンロード

このノート再実装してからベイジアン「の変更点分析」の例を拡張しpymc3ドキュメント

前提条件

import tensorflow.compat.v2 as tf
tf.enable_v2_behavior()
import tensorflow_probability as tfp
tfd = tfp.distributions
tfb = tfp.bijectors
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = (15,8)
%config InlineBackend.figure_format = 'retina'
import numpy as np
import pandas as pd

データセット

データセットからであるここに。注、この例の別のバージョンがあり出回っ、それは、データを「欠落」した-その場合、あなたは欠損値を転嫁する必要があると思います。 (そうしないと、尤度関数が未定義になるため、モデルが初期パラメーターを離れることはありません。)

disaster_data = np.array([ 4, 5, 4, 0, 1, 4, 3, 4, 0, 6, 3, 3, 4, 0, 2, 6,
                           3, 3, 5, 4, 5, 3, 1, 4, 4, 1, 5, 5, 3, 4, 2, 5,
                           2, 2, 3, 4, 2, 1, 3, 2, 2, 1, 1, 1, 1, 3, 0, 0,
                           1, 0, 1, 1, 0, 0, 3, 1, 0, 3, 2, 2, 0, 1, 1, 1,
                           0, 1, 0, 1, 0, 0, 0, 2, 1, 0, 0, 0, 1, 1, 0, 2,
                           3, 3, 1, 1, 2, 1, 1, 1, 1, 2, 4, 2, 0, 0, 1, 4,
                           0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1])
years = np.arange(1851, 1962)
plt.plot(years, disaster_data, 'o', markersize=8);
plt.ylabel('Disaster count')
plt.xlabel('Year')
plt.title('Mining disaster data set')
plt.show()

png

確率モデル

このモデルは、「切り替えポイント」(たとえば、安全規制が変更された1年)、およびその切り替えポイントの前後で一定の(ただし潜在的に異なる)レートを持つポアソン分布の災害率を想定しています。

実際の災害件数は固定されています(監視されています)。このモデルのサンプルでは、​​スイッチポイントと「初期」および「後期」の災害率の両方を指定する必要があります。

オリジナルモデルpymc3ドキュメントの例

$$ \begin{align*} (D_t|s,e,l)&\sim \text{Poisson}(r_t), \\ & \,\quad\text{with}\; r_t = \begin{cases}e & \text{if}\; t < s\\l &\text{if}\; t \ge s\end{cases} \\ s&\sim\text{Discrete Uniform}(t_l,\,t_h) \\ e&\sim\text{Exponential}(r_e)\\ l&\sim\text{Exponential}(r_l) \end{align*} $$

ただし、平均災害率$ r_t $には、スイッチポイント$ s $で不連続性があるため、微分可能ではありません。したがって、ハミルトニアンモンテカルロ(HMC)アルゴリズムに勾配信号を提供しませんが、$ s $の事前分布は連続的であるため、この例では、HMCのランダムウォークへのフォールバックで確率質量の高い領域を見つけるのに十分です。

第二のモデルとして、我々は、使用して、元のモデルを修正シグモイド「スイッチ」遷移微分を作るためにELの間、およびスイッチポイント$ S $のための連続的な均一な分布を使用します。 (平均レートの「切り替え」は複数年にわたって拡大する可能性が高いため、このモデルはより現実に忠実であると主張することができます。)したがって、新しいモデルは次のようになります。

$$ \begin{align*} (D_t|s,e,l)&\sim\text{Poisson}(r_t), \\ & \,\quad \text{with}\; r_t = e + \frac{1}{1+\exp(s-t)}(l-e) \\ s&\sim\text{Uniform}(t_l,\,t_h) \\ e&\sim\text{Exponential}(r_e)\\ l&\sim\text{Exponential}(r_l) \end{align*} $$

詳細情報がない場合は、事前確率のパラメーターとして$ r_e = r_l = 1 $と想定します。両方のモデルを実行し、それらの推論結果を比較します。

def disaster_count_model(disaster_rate_fn):
  disaster_count = tfd.JointDistributionNamed(dict(
    e=tfd.Exponential(rate=1.),
    l=tfd.Exponential(rate=1.),
    s=tfd.Uniform(0., high=len(years)),
    d_t=lambda s, l, e: tfd.Independent(
        tfd.Poisson(rate=disaster_rate_fn(np.arange(len(years)), s, l, e)),
        reinterpreted_batch_ndims=1)
  ))
  return disaster_count

def disaster_rate_switch(ys, s, l, e):
  return tf.where(ys < s, e, l)

def disaster_rate_sigmoid(ys, s, l, e):
  return e + tf.sigmoid(ys - s) * (l - e)

model_switch = disaster_count_model(disaster_rate_switch)
model_sigmoid = disaster_count_model(disaster_rate_sigmoid)

上記のコードは、JointDistributionSequentialディストリビューションを介してモデルを定義します。 disaster_rate機能は、アレイと呼ばれている[0, ..., len(years)-1]のベクターを生成するためにlen(years)確率変数-前年switchpointあるearly_disaster_rate後、ものlate_disaster_rate (モジュロシグモイド遷移)。

これが健全性テストです。ターゲットのログ確率関数が正常であることを確認してください。

def target_log_prob_fn(model, s, e, l):
  return model.log_prob(s=s, e=e, l=l, d_t=disaster_data)

models = [model_switch, model_sigmoid]
print([target_log_prob_fn(m, 40., 3., .9).numpy() for m in models])  # Somewhat likely result
print([target_log_prob_fn(m, 60., 1., 5.).numpy() for m in models])  # Rather unlikely result
print([target_log_prob_fn(m, -10., 1., 1.).numpy() for m in models]) # Impossible result
[-176.94559, -176.28717]
[-371.3125, -366.8816]
[-inf, -inf]

ベイジアン推論を行うHMC

必要な結果の数とバーンインステップを定義します。コードは、ほとんどの後にモデル化されtfp.mcmc.HamiltonianMonteCarloのドキュメント。適応ステップサイズを使用します(そうでない場合、結果は選択したステップサイズ値に非常に敏感です)。チェーンの初期状態として1の値を使用します。

しかし、これは完全な話ではありません。上記のモデル定義に戻ると、いくつかの確率分布が実数直線全体で明確に定義されていないことに気付くでしょう。そこで我々は、HMCは、HMCとカーネルラップすることによって審査しなければならないという制約スペースTransformedTransitionKernel (下記のコード内のコメントを参照してください)確率分布が上で定義されているドメインに実数を変換するために、前方bijectorsを指定します。

num_results = 10000
num_burnin_steps = 3000

@tf.function(autograph=False, jit_compile=True)
def make_chain(target_log_prob_fn):
   kernel = tfp.mcmc.TransformedTransitionKernel(
       inner_kernel=tfp.mcmc.HamiltonianMonteCarlo(
          target_log_prob_fn=target_log_prob_fn,
          step_size=0.05,
          num_leapfrog_steps=3),
       bijector=[
          # The switchpoint is constrained between zero and len(years).
          # Hence we supply a bijector that maps the real numbers (in a
          # differentiable way) to the interval (0;len(yers))
          tfb.Sigmoid(low=0., high=tf.cast(len(years), dtype=tf.float32)),
          # Early and late disaster rate: The exponential distribution is
          # defined on the positive real numbers
          tfb.Softplus(),
          tfb.Softplus(),
      ])
   kernel = tfp.mcmc.SimpleStepSizeAdaptation(
        inner_kernel=kernel,
        num_adaptation_steps=int(0.8*num_burnin_steps))

   states = tfp.mcmc.sample_chain(
      num_results=num_results,
      num_burnin_steps=num_burnin_steps,
      current_state=[
          # The three latent variables
          tf.ones([], name='init_switchpoint'),
          tf.ones([], name='init_early_disaster_rate'),
          tf.ones([], name='init_late_disaster_rate'),
      ],
      trace_fn=None,
      kernel=kernel)
   return states

switch_samples = [s.numpy() for s in make_chain(
    lambda *args: target_log_prob_fn(model_switch, *args))]
sigmoid_samples = [s.numpy() for s in make_chain(
    lambda *args: target_log_prob_fn(model_sigmoid, *args))]

switchpoint, early_disaster_rate, late_disaster_rate = zip(
    switch_samples, sigmoid_samples)

両方のモデルを並行して実行します。

結果を視覚化する

結果を、初期および後期の災害率の事後分布のサンプルのヒストグラム、およびスイッチポイントとして視覚化します。ヒストグラムは、サンプルの中央値を表す実線と、95%ileの信頼区間の境界を破線でオーバーレイしています。

def _desc(v):
  return '(median: {}; 95%ile CI: $[{}, {}]$)'.format(
      *np.round(np.percentile(v, [50, 2.5, 97.5]), 2))

for t, v in [
    ('Early disaster rate ($e$) posterior samples', early_disaster_rate),
    ('Late disaster rate ($l$) posterior samples', late_disaster_rate),
    ('Switch point ($s$) posterior samples', years[0] + switchpoint),
]:
  fig, ax = plt.subplots(nrows=1, ncols=2, sharex=True)
  for (m, i) in (('Switch', 0), ('Sigmoid', 1)):
    a = ax[i]
    a.hist(v[i], bins=50)
    a.axvline(x=np.percentile(v[i], 50), color='k')
    a.axvline(x=np.percentile(v[i], 2.5), color='k', ls='dashed', alpha=.5)
    a.axvline(x=np.percentile(v[i], 97.5), color='k', ls='dashed', alpha=.5)
    a.set_title(m + ' model ' + _desc(v[i]))
  fig.suptitle(t)
  plt.show()

png

png

png