Algorithme Forward-Backward - Exemple 1

par Joseph Razik, le 2019-10-18
Forward-Backward-exemple-1

From 'An interactive spreadsheet for teaching the forward-backward algorithm', Jason Eisner 2002

In [1]:
%pylab inline
Populating the interactive namespace from numpy and matplotlib

Description du système:

Nombre de glaces mangées: entre 1 et 3 (valeur des observations possibles)

Nombre d'états: 2

In [2]:
N = 2  # nombre d'états C: Cold et H: Hot. On ne compte pas les deux états fictifs START et STOP
In [3]:
# Probabilités initiales de transition
a_ij = array(
    [[0.8, 0.1],
     [0.1, 0.8],
    ])

#     C->C, C->H, 
#     H->H, H->C, 
In [4]:
# probabilités initiales de commencer dans un des états P(C|START), P(H|START)
pi_i = [0.5, 0.5]
In [5]:
# probabilités initiales des observations selons les états
b_i_o = array([[0.7, 0.2, 0.1],
               [0.1, 0.2, 0.7],
               ])

# P(1|C) P(2|C) P(3|C)
# P(1|H) P(2|H) P(3|H)
In [6]:
# une suite d'observations
obs = [2, 3, 3, 2, 3, 2, 3, 2, 2, 3, 1, 3, 3, 1, 1, 1, 2, 1, 1, 1, 3, 1, 2, 1, 1, 1, 2, 3, 3, 2, 3, 2, 2]
T = len(obs)  # le nombre d'observations

Soit $\lambda$ le système initial défini par les probabilités d'observations $b_j(o_t)$, les probabilités de transition $a_{i,j}$ et les probabilités initiales $\pi_i$, $\alpha_t(i) = P(o_1o_2\cdots o_t, q_t=i|\lambda)$ et peut être calculé par récurrence tel que: $$\alpha_1(i) = \pi_i b_i(o_1)$$ $$\alpha_t(j) = \sum_{i=1}^N \alpha_{t-1}(i) a_{i,j} b_j(o_t)\, pour\, tout \, 1 < t \leq T$$

In [7]:
def calc_alpha():
    """ Fonction qui calcul les alphas, sens forward, par récurence.
    (probabilité de la suite d'observations partielle se terminant à l'instant t et à l'état i)
    """
    # obs[t]-1 pour le décalage par rapport à la matrice
    alpha = empty((T, N))
    # initialisation de la récurrence
    alpha[0, :] = [pi_i[i] * b_i_o[i, obs[0]-1] for i in range(N)]
    # on avance petit à petit l'évaluation
    for t in range(1, T):
        alpha[t, :] = [b_i_o[i, obs[t]-1] * sum([a_ij[j, i] * alpha[t-1, j] for j in range(N)]) for i in range(N)]
    return alpha
In [8]:
alpha = calc_alpha()
In [9]:
alpha
Out[9]:
array([[  1.00000000e-01,   1.00000000e-01],
       [  9.00000000e-03,   6.30000000e-02],
       [  1.35000000e-03,   3.59100000e-02],
       [  9.34200000e-04,   5.77260000e-03],
       [  1.32462000e-04,   3.29805000e-03],
       [  8.71549200e-05,   5.30337240e-04],
       [  1.22757660e-05,   3.03089699e-04],
       [  8.02591654e-06,   4.87398671e-05],
       [  2.25894399e-06,   7.95889707e-06],
       [  2.60304490e-07,   4.61510844e-06],
       [  4.68828105e-07,   3.71811720e-07],
       [  4.12243656e-08,   2.41032531e-07],
       [  5.70827455e-09,   1.37863923e-07],
       [  1.28471083e-08,   1.10861966e-08],
       [  7.97041443e-09,   1.01536681e-09],
       [  4.53450776e-09,   1.60933489e-10],
       [  7.28739911e-10,   1.16439513e-10],
       [  4.16245116e-10,   1.66025602e-11],
       [  2.34259444e-10,   5.49065597e-12],
       [  1.31569635e-10,   2.78184692e-12],
       [  1.05533892e-11,   1.07677087e-11],
       [  6.66363759e-12,   9.66950589e-13],
       [  1.08552103e-12,   2.87984846e-13],
       [  6.28050713e-13,   3.38939979e-14],
       [  3.54080979e-13,   8.99202697e-15],
       [  1.98914790e-13,   4.26017195e-15],
       [  3.19115699e-14,   4.65992332e-15],
       [  2.59952482e-15,   4.84336695e-15],
       [  2.56395655e-16,   2.89425223e-15],
       [  9.89083495e-17,   4.68208270e-16],
       [  1.25947507e-17,   2.69120216e-16],
       [  7.39756442e-18,   4.33111295e-17],
       [  2.04983290e-18,   7.07773201e-18]])

Soit $\lambda$ le système initial défini par les probabilités d'observations $b_j(o_t)$, les probabilités de transition $a_{i,j}$ et les probabilités initiales $\pi_i$, $\beta_t(i) = P(o_{t+1}o_{t+2}\cdots o_T, q_t=i|\lambda)$ et peut être calculé par récurrence tel que: $$ \beta_T(i) = 1$$ $$ \beta_t(i) = \sum_{j=1}^N \beta_{t+1}(j) a_{i,j} b_j(o_{t+1}) \, pour \, tout \, 0 \leq t < T$$

In [10]:
def calc_beta():
    """ Fonction qui calcul les betas, sens backward, par récurence.
    (probabilité d'observer la suite partielle ot+1ot+2...o_T, de t+1 à T est dans l'état i à l'instant t)
    """
    # obs[t]-1 pour le décalage par rapport à la matrice
    beta = empty((T, N))
    beta[T-1, :] = 1.0  # valeur d'initialisation arbitraire, égale pour tous les états (cf rabiner89) (0.1 dans iesner) 
    for t in range(T - 2, -1, -1):
        beta[t, :] = [sum([beta[t+1, j] * a_ij[i, j] * b_i_o[j, obs[t+1]-1] for j in range(N)]) for i in range(N)]
    return beta
In [11]:
beta = calc_beta()
In [12]:
beta
Out[12]:
array([[  1.17798404e-17,   7.94958087e-17],
       [  2.34014516e-17,   1.41538918e-16],
       [  7.24963411e-17,   2.51453491e-16],
       [  2.60727967e-16,   1.53899332e-15],
       [  8.67984786e-16,   2.73270263e-15],
       [  3.34220291e-15,   1.66616161e-14],
       [  1.59936622e-14,   2.94672847e-14],
       [  7.81603276e-14,   1.74400489e-13],
       [  3.57842962e-13,   1.04527268e-12],
       [  2.88487462e-12,   1.81504275e-12],
       [  4.82175833e-12,   1.84689958e-11],
       [  3.19128109e-11,   3.24104780e-11],
       [  3.53796840e-10,   5.15580528e-11],
       [  6.30117158e-10,   9.31231475e-11],
       [  1.12195331e-09,   1.82330194e-10],
       [  1.99394474e-09,   5.34425777e-10],
       [  1.22358192e-08,   1.81068371e-09],
       [  2.17859115e-08,   3.57087381e-09],
       [  3.87112059e-08,   1.07636174e-08],
       [  6.77836803e-08,   7.52344974e-08],
       [  7.41325310e-07,   1.21109365e-07],
       [  1.31734538e-06,   3.61189856e-07],
       [  8.07743901e-06,   1.24775672e-06],
       [  1.43700126e-05,   3.02319802e-06],
       [  2.53825176e-05,   1.55802724e-05],
       [  4.25124418e-05,   1.57555018e-04],
       [  1.44876600e-04,   9.66609288e-04],
       [  3.05402400e-04,   1.72063440e-03],
       [  1.14696000e-03,   3.05208000e-03],
       [  4.86000000e-03,   1.84680000e-02],
       [  3.24000000e-02,   3.24000000e-02],
       [  1.80000000e-01,   1.80000000e-01],
       [  1.00000000e+00,   1.00000000e+00]])

Pour verifier que tout s'est bien passé on peut utiliser le fait que

$$P(O/\lambda) = \sum_{i=1}^N \alpha_T(i) = \sum_{i=1}^N \beta_1(i) = \sum_{i=1}^N \alpha_t(i) \beta_t(i) \,\, \forall t$$
In [13]:
p_o_lambda = sum(alpha*beta, axis=1)
print(p_o_lambda)
[  9.12756491e-18   9.12756491e-18   9.12756491e-18   9.12756491e-18
   9.12756491e-18   9.12756491e-18   9.12756491e-18   9.12756491e-18
   9.12756491e-18   9.12756491e-18   9.12756491e-18   9.12756491e-18
   9.12756491e-18   9.12756491e-18   9.12756491e-18   9.12756491e-18
   9.12756491e-18   9.12756491e-18   9.12756491e-18   9.12756491e-18
   9.12756491e-18   9.12756491e-18   9.12756491e-18   9.12756491e-18
   9.12756491e-18   9.12756491e-18   9.12756491e-18   9.12756491e-18
   9.12756491e-18   9.12756491e-18   9.12756491e-18   9.12756491e-18
   9.12756491e-18]
In [14]:
# tout va bien ?
np.testing.assert_almost_equal(p_o_lambda, [p_o_lambda[0]]*T)

Une fois les coefficients $\alpha_t(i)$ et $\beta_t(i)$ calculés, il est possible d'estimer les coefficients $\gamma_t(i)$, la probabilité d'être dans l'état $i$ à l'instant $t$ sachant les observations et le modèle $\lambda$.

$$\gamma_t(i) = \frac{\alpha_t(i) \beta_t(i)}{P(O/\lambda)}$$
In [15]:
def calc_gamma():
    """ Fonction qui calcule les valeurs gamma en fonction des alphas et betas. """
    gamma = empty((T, N))
    for t in range(T):
        gamma[t, :] = (alpha[t, :]*beta[t, :]) / p_o_lambda[t]  
    return gamma
In [16]:
gamma = calc_gamma()
In [17]:
gamma
Out[17]:
array([[ 0.12905787,  0.87094213],
       [ 0.0230744 ,  0.9769256 ],
       [ 0.01072247,  0.98927753],
       [ 0.02668533,  0.97331467],
       [ 0.01259646,  0.98740354],
       [ 0.03191316,  0.96808684],
       [ 0.02151006,  0.97848994],
       [ 0.06872679,  0.93127321],
       [ 0.0885611 ,  0.9114389 ],
       [ 0.08227231,  0.91772769],
       [ 0.24766472,  0.75233528],
       [ 0.14413323,  0.85586677],
       [ 0.22126049,  0.77873951],
       [ 0.88689409,  0.11310591],
       [ 0.97971726,  0.02028274],
       [ 0.99057722,  0.00942278],
       [ 0.97690127,  0.02309873],
       [ 0.99350477,  0.00649523],
       [ 0.99352518,  0.00647482],
       [ 0.97707046,  0.02292954],
       [ 0.85712834,  0.14287166],
       [ 0.96173648,  0.03826352],
       [ 0.96063188,  0.03936812],
       [ 0.98877376,  0.01122624],
       [ 0.98465108,  0.01534892],
       [ 0.92646325,  0.07353675],
       [ 0.50651404,  0.49348596],
       [ 0.08697841,  0.91302159],
       [ 0.0322184 ,  0.9677816 ],
       [ 0.05266405,  0.94733595],
       [ 0.04470742,  0.95529258],
       [ 0.14588355,  0.85411645],
       [ 0.2245761 ,  0.7754239 ]])
In [18]:
def calc_xi():
    """ Fonction qui calcule les valeurs zeta à partir des alphas et des betas."""
    xi = empty((T-1, N, N))
    for t in range(T-1):
        for i in range(N):
            xi[t, i, :] = alpha[t, i] * a_ij[i, :N] * beta[t+1, :] * b_i_o[:, obs[t+1]-1] / p_o_lambda[t]
            # -1 pour faire correspondre avec la matrice d'observation, les valeurs commençant à 1 et non à 0 (nb glaces)
    return xi
        
In [19]:
xi = calc_xi()
In [20]:
xi
Out[20]:
array([[[  2.05105759e-02,   1.08547289e-01],
        [  2.56382199e-03,   8.68378313e-01]],

       [[  5.71865181e-03,   1.73557461e-02],
        [  5.00382034e-03,   9.71921782e-01]],

       [[  6.17001812e-03,   4.55245403e-03],
        [  2.05153102e-02,   9.68762218e-01]],

       [[  7.10701174e-03,   1.95783166e-02],
        [  5.48944765e-03,   9.67825224e-01]],

       [[  7.76049053e-03,   4.83596887e-03],
        [  2.41526681e-02,   9.63250872e-01]],

       [[  1.22172900e-02,   1.96958686e-02],
        [  9.29276839e-03,   9.58794073e-01]],

       [[  1.68189944e-02,   4.69106407e-03],
        [  5.19077988e-02,   9.26582143e-01]],

       [[  5.03445162e-02,   1.83822770e-02],
        [  3.82165859e-02,   8.93056621e-01]],

       [[  5.71172728e-02,   3.14438293e-02],
        [  2.51550336e-02,   8.86283864e-01]],

       [[  7.70052252e-02,   5.26708117e-03],
        [  1.70659496e-01,   7.47068197e-01]],

       [[  1.31133531e-01,   1.16531191e-01],
        [  1.29996962e-02,   7.39335582e-01]],

       [[  1.27833002e-01,   1.63002250e-02],
        [  9.34274896e-02,   7.62439284e-01]],

       [[  2.20678110e-01,   5.82381499e-04],
        [  6.66215982e-01,   1.12523527e-01]],

       [[  8.84327782e-01,   2.56630961e-03],
        [  9.53894775e-02,   1.77164305e-02]],

       [[  9.75050522e-01,   4.66673748e-03],
        [  1.55267011e-02,   4.75603911e-03]],

       [[  9.72586534e-01,   1.79906895e-02],
        [  4.31473912e-03,   5.10803746e-03]],

       [[  9.74050306e-01,   2.85096659e-03],
        [  1.94544621e-02,   3.64426493e-03]],

       [[  9.88596227e-01,   4.90854157e-03],
        [  4.92895523e-03,   1.56627629e-03]],

       [[  9.74216208e-01,   1.93089742e-02],
        [  2.85425315e-03,   3.62056471e-03]],

       [[  8.54868970e-01,   1.22201491e-01],
        [  2.25936879e-03,   2.06701701e-02]],

       [[  8.52952223e-01,   4.17611617e-03],
        [  1.08784259e-01,   3.40874018e-02]],

       [[  9.43517825e-01,   1.82186567e-02],
        [  1.71140594e-02,   2.11494585e-02]],

       [[  9.57036463e-01,   3.59542227e-03],
        [  3.17372939e-02,   7.63082136e-03]],

       [[  9.78053262e-01,   1.07204948e-02],
        [  6.59782214e-03,   4.62842149e-03]],

       [[  9.23531568e-01,   6.11195162e-02],
        [  2.93168133e-03,   1.24172349e-02]],

       [[  5.05161651e-01,   4.21301598e-01],
        [  1.35238529e-03,   7.21843658e-02]],

       [[  8.54192340e-02,   4.21094803e-01],
        [  1.55918011e-03,   4.91926783e-01]],

       [[  2.61322797e-02,   6.08461344e-02],
        [  6.08612288e-03,   9.06935463e-01]],

       [[  2.18429848e-02,   1.03754178e-02],
        [  3.08210700e-02,   9.36960527e-01]],

       [[  2.80874959e-02,   2.45765589e-02],
        [  1.66199289e-02,   9.30716016e-01]],

       [[  3.97399331e-02,   4.96749164e-03],
        [  1.06143619e-01,   8.49148956e-01]],

       [[  1.29674269e-01,   1.62092836e-02],
        [  9.49018275e-02,   7.59214620e-01]]])

Les nouvelles estimations des probabilités initiales $\pi_i$ sont données par la formule suivant:

$$\pi_i = \gamma_1(i)$$
In [21]:
# les nouvelles valeurs des p_i
new_pi = gamma[0, :]
In [22]:
new_pi
Out[22]:
array([ 0.12905787,  0.87094213])

La somme des $\pi_i$ doit faire 1

In [23]:
sum(new_pi)
Out[23]:
1.0

Les nouvelles estimations des probabilités des transitions sont données par la formule suivante:

$$a_{ij} = \frac{\sum_{t=1}^{T-1} \xi_t(i, j)}{\sum_{t=1}^{T-1} \gamma_t(i)} \, pour\, i \ge 1 \, et \, j \leq N$$
In [24]:
# les nouvelles probabilités de transition
new_a_ij = array([[sum(xi[:, i, j])/sum(gamma[:T-1, i]) for j in range(N)] for i in range(N)])
In [25]:
new_a_ij
Out[25]:
array([[ 0.88934694,  0.11065306],
       [ 0.09660587,  0.90339413]])

Les nouvelles probabilités d'observations peuvent être calculées par la formule suivante:

$$b_i(k) = \frac{\sum_{t=1\,et\, o_t=k}^T \gamma_t(i)}{\sum_{t=1}^T \gamma_t(i)}$$
In [26]:
new_b_i_o = array([
            [sum([gamma[t, i] for t in range(T) if (obs[t]-1) == k])/sum(gamma[:, i]) for i in range(N)]
                    for k in range(3)]).T
In [27]:
new_b_i_o
Out[27]:
array([[ 0.67650238,  0.21881944,  0.10467818],
       [ 0.0583723 ,  0.42508654,  0.51654116]])

Ensuite il faut itérer ce processus d'évaluation du nouveau modèle $\lambda'$ jusqu'à convergence (avec la même séquence d'observations).