Analisi del punto di commutazione bayesiano

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza la fonte su GitHub Scarica taccuino

Questo notebook reimplementa ed estende il bayesiana “Change analisi punto” esempio dalla documentazione pymc3 .

Prerequisiti

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

set di dati

Il set di dati è da qui . Nota, c'è un'altra versione di questo esempio galleggia intorno , ma è “mancante” i dati - in questo caso avresti bisogno di imputare i valori mancanti. (Altrimenti il ​​tuo modello non lascerà mai i suoi parametri iniziali perché la funzione di verosimiglianza non sarà definita.)

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

Modello probabilistico

Il modello presuppone un "punto di commutazione" (ad esempio un anno durante il quale le norme di sicurezza sono cambiate) e il tasso di disastro distribuito da Poisson con tassi costanti (ma potenzialmente diversi) prima e dopo tale punto di commutazione.

Il conteggio effettivo dei disastri è fisso (osservato); qualsiasi campione di questo modello dovrà specificare sia il punto di commutazione che il tasso di catastrofi "precoce" e "tardivo".

Modello originale da esempio documentazione 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*} \]

Tuttavia, la media tasso di disastro \(r_t\) ha una discontinuità alla commutazione \(s\), che lo rende non differenziabile. Così fornisce alcun segnale di gradiente al (HMC) algoritmo di Hamilton Monte Carlo - ma perché la \(s\) prima è continuo, fallback di HMC per una passeggiata casuale è abbastanza buono per trovare le zone di alta massa di probabilità in questo esempio.

Come un secondo modello, modifichiamo il modello originale con un “switch” sigma tra E ed L per rendere la transizione differenziabile, e utilizzare una distribuzione uniforme continuo per la commutazione \(s\). (Si potrebbe obiettare che questo modello è più fedele alla realtà, poiché un "passaggio" del tasso medio probabilmente si estenderebbe su più anni.) Il nuovo modello è quindi:

\[ \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*} \]

In assenza di ulteriori informazioni assumiamo \(r_e = r_l = 1\) come parametri per i priori. Eseguiremo entrambi i modelli e confronteremo i risultati dell'inferenza.

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)

Il codice sopra definisce il modello tramite le distribuzioni JointDistributionSequential. Le disaster_rate funzioni sono chiamate con un array di [0, ..., len(years)-1] per produrre un vettore di len(years) variabili aleatorie - anni prima della switchpoint sono early_disaster_rate , quelli dopo late_disaster_rate (modulo la transizione sigmoidea).

Ecco un controllo di integrità che la funzione di verifica del registro di destinazione è sana:

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 per fare l'inferenza bayesiana

Definiamo il numero di risultati e i passaggi di burn-in richiesti; il codice è in gran parte modellato la documentazione di tfp.mcmc.HamiltonianMonteCarlo . Utilizza una dimensione del passo adattiva (altrimenti il ​​risultato è molto sensibile al valore della dimensione del passo scelto). Usiamo i valori di uno come stato iniziale della catena.

Questa non è la storia completa però. Se torni alla definizione del modello sopra, noterai che alcune delle distribuzioni di probabilità non sono ben definite sull'intera linea dei numeri reali. Perciò abbiamo vincolare la spazio che HMC esamina avvolgendo il kernel HMC con un TransformedTransitionKernel che specifica il bijectors avanti per trasformare i numeri reali sul dominio che la distribuzione di probabilità è definita (vedere commenti nel codice qui sotto).

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)

Esegui entrambi i modelli in parallelo:

Visualizza il risultato

Visualizziamo il risultato come istogrammi di campioni della distribuzione a posteriori per il tasso di disastro precoce e tardivo, nonché il punto di commutazione. Gli istogrammi sono sovrapposti con una linea continua che rappresenta la mediana del campione, nonché i limiti dell'intervallo credibile al 95% come linee tratteggiate.

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