Algoritmo Forward-Backward¶

En este ejemplo, planteamos una forma eficiente de programar el procedimiento de avance a partir de operaciones con arreglos de numpy. En primer lugar, llamamos a la librería de numpy, la cual utilizaremos para crear y operar con los arreglos.

In [1]:
import numpy as np
import pandas as pd
from ForwardBackward import forward, backward, forward_backward

Modelo Oculto de Márkov¶

Supongamos que tenemos un Modelo Oculto de Markov $HMM = (O,S,A,B,\Pi)$, donde el alfabeto de emisiones se define por:

$$S = \{D, NN, V\}$$

Y el alfabeto de observaciones está determinado como:

$$O = \{la, nina, garza, pasa\}$$

Para determinar estos símbolos, indexaremos los elementos de cada alfabeto a partir de diccionarios (en este caso hace falta únicamente indexar el alfabeto $\Sigma$):

In [2]:
O = {'la': 0, 'nina': 1, 'garza':2, 'pasa':3}
S = {0:'DA', 1:'N', 2:'V'}

Las probabilidades se pueden determinar de la siguiente forma:

In [3]:
Pi = np.array([4/6, 1/6, 1/6])
A = np.array([[1/6,1/6,1/5],[4/6,1/6,1/5],[1/6,3/6,1/5]])
B = np.array([[4/7,1/7,1/6],[1/7,2/7,1/6],[1/7,2/7,1/6],[1/7,2/7,3/6]])

pd.DataFrame(data=A, columns=S.values(), index=S.values())
Out[3]:
DA N V
DA 0.166667 0.166667 0.2
N 0.666667 0.166667 0.2
V 0.166667 0.500000 0.2
In [4]:
pd.DataFrame(data=Pi, columns=['Start'], index=S.values())
Out[4]:
Start
DA 0.666667
N 0.166667
V 0.166667
In [5]:
pd.DataFrame(data=B, columns=S.values(), index=O.keys())
Out[5]:
DA N V
la 0.571429 0.142857 0.166667
nina 0.142857 0.285714 0.166667
garza 0.142857 0.285714 0.166667
pasa 0.142857 0.285714 0.500000

Estas probabilidades, junto con los alfabetos de observaciones y emisiones nos dan el Modelo Oculto de Markov $HMM$. Ahora supongamos que queremos determinar la probabilidad de una cadena de observaciones $d\in\Delta^*$, definida como sigue:

In [6]:
obs = 'la nina pasa la garza'
obs = obs.split()
print(obs)
['la', 'nina', 'pasa', 'la', 'garza']

Forward Procedure¶

Utilizaremos el procedimiento de avance para determinar dicha probabilidad. Realizaremos los pasos de inicialización, inducción y terminación (en cada paso, guardamos las variables en una matriz):

(1) Inicialización. La inicialización está dada por el almacenamiento de las probabilidades iniciales en la variable de avance:

$$\alpha_i(0) := \pi_i$$

(2) Inducción. Los siguientes pasos consistirán en ir actualizando la variable de avance a partir de los diferentes estados de la cadena:

$$\alpha_i(t+1) = \sum_{j=1}^N p(o_t|s_i) p(s_i|s_j)\alpha_j(t)$$

En este caso, utilizaremos operaciones entre matrices y vectores, pues es evidente que (tomando los elementos del modelo $A,B,\Pi$):

$$\alpha_i(t+1) = B_{t,\cdot} \odot (A_{i,\cdot} \alpha_{\cdot}(t))$$

donde $B_{t,\cdot}$ representa el vector renglón que corresponde a la observación en el estado $t$ y $\alpha_{\cdot}(t)$ es el vector que contiene todas las variables $\alpha_i(t)$. Aquí, $A_{i,\cdot}$ representa el i-ésimo vector renglón de $A$ y $\odot$ representa el producto de Hadamard. En general, podemos actualizar todo el vector $\alpha(t+1)$ (cuyas entradas son $\alpha_i(t+1)$) tomando la matriz completa $A$. De tal forma que:

$$\alpha(t+1) = B_{t,\cdot} \odot (A \alpha_{\cdot}(t)) $$

(3) Terminación. Para la terminación, tenemos que:

$$p(o^{(1)}...o^{(T)}) = \sum_{i=1}^N \alpha_i(T)$$

Pero ya que hemos guardado cada variable $\alpha_i$ como la entrada de un vector, basta sumar las entradas de este vector para obtener esta probabilidad:

In [7]:
alpha = forward(obs,(O,S,A,B,Pi))

pd.DataFrame(data=alpha, columns=S.values(), index=[0] + obs)
simbolo: la - probabilidad 0.20859788359788356
simbolo: la nina - probabilidad 0.03951877047115142
simbolo: la nina pasa - probabilidad 0.011220368570708704
simbolo: la nina pasa la - probabilidad 0.0020895512786453284
simbolo: la nina pasa la garza - probabilidad 0.00041117213569291135
Out[7]:
DA N V
0 0.666667 0.166667 0.166667
la 0.098413 0.072222 0.037963
nina 0.005147 0.024354 0.010018
pasa 0.000989 0.002713 0.007519
la 0.001212 0.000374 0.000504
garza 0.000052 0.000277 0.000082
In [8]:
print('Probabilidad de la cadena',obs, 'es:', alpha[-1].sum(0) )
Probabilidad de la cadena ['la', 'nina', 'pasa', 'la', 'garza'] es: 0.00041117213569291135

Backward Procedure¶

El procedimiento de retroceso o backward procedure es similar al procedimiento de avance, pero la cadena se recorre en sentido inverso, es decir, de derecha a izquierda. Aquí presentamos una implementación simple de este procedimiento utilizando las propiedades de operaciones entre arreglos en numpy.

Ahora es cuando iniciamos con el procedimiento de retroceso. El paso de inducción consiste en determinar las variables $\beta_i(T) = 1$. En este caso, utilizamos un vector, que podemos denotar como $\beta_\cdot(T)$, el cual contendrá en un primer momento sólo unos.

En el paso inductivo tenemos que obtener las variables:

$$\beta_j(t) = \sum_{i=1}^N p(o^{(t+1)} | s_i^{(t+1)}) p(s_i^{(t+1)}|s_{j}^{(t)}) \beta_i(t+1)$$

Ya que estamos trabajando con arreglos de numpy, podemos ver que la suma y el producto sobre los $i$-ésimos elementos determinan un producto punto. Así, $B_{(t+1),\cdot}$ representa el vector columna correspondiente al elemento de la cadena de observaciones en el estado $t+1$, $A_{\cdot, j}$ a la $j$-ésima columna de la matriz de transiciones $A$ y $\hat{\beta}(t+1)$ al vector de las variables $\beta$ en el estado subsecuente. Así, podemos expresar la ecuación anterior como:

$$\beta_j(t) = [\hat{\beta}(t+1) \odot B_{(t+1),\cdot}] \cdot A_{\cdot, j}$$

Donde $\cdot$ es el producto punto, y $\odot$ el producto de Hadarmard. En términos generales, podemos actualizar todas las variables $\beta$ expresándolo como un vector $\hat{\beta}(t)$ y de esta forma, tenemos que realizar la operación:

$$\hat{\beta(t)} = A^T [\hat{\beta}(t+1) \odot B_{(t+1),\cdot}]$$

Donde el producto punto se hace con toda la matriz de transiciones $A$.

Finalmente, el paso de terminación consiste en obtener la probabilidad de la cadena de la siguiente forma:

$$p(o^{(1)}...o^{(T)}) = \sum_{j=1}^N \pi_j \beta_j(0)$$

Donde $\pi_j$ es la $j$-ésima probabilidad inicial. De igual forma, podemos aprovechar la estructura de los arreglos y expresar la suma de los productos por medio de un producto punto con el vector de probabilidades iniciales $\Pi$ y el vector de variables $\beta(0)$. Esto es:

$$p(o^{(1)}...o^{(T)}) = \Pi \cdot \hat{\beta}(0)$$

In [9]:
beta = backward(obs,(O,S,A,B,Pi))

pd.DataFrame(data=beta, columns=S.values(), index=[0] + obs)
simbolo: garza - probabilidad: 0.20701058201058198
simbolo: la garza - probabilidad: 0.03951877047115142
simbolo: pasa la garza - probabilidad: 0.010412587433562487
simbolo: nina pasa la garza - probabilidad: 0.002226236896034973
simbolo: la nina pasa la garza - probabilidad: 0.0004111721356929113
Out[9]:
DA N V
0 0.000422 0.000390 0.000387
la 0.002688 0.001393 0.001213
nina 0.010966 0.011738 0.006875
pasa 0.041100 0.036659 0.036054
la 0.242063 0.154762 0.119048
garza 1.000000 1.000000 1.000000
In [10]:
print('La probabilidad de la observación es:', np.dot(Pi,beta[0]) )
La probabilidad de la observación es: 0.0004111721356929113

Forward-Backward Procedure¶

El procedimiento de avance-retroceso puede usarse para calcular la probabilidad de una cadena; en particular, tenemos que:

$$p(o^{(1)}...o^{(T)}) = \sum_{i=1}^N \alpha_i(t) \beta_i(t)$$

En este sentido para cada $t$ (de $1$ a $T$) podremos utilizar las variables en ese estado para calcular la probabilidad.

El avance-retroceso nos da la probabilidad conjunta de la cadena y el símbolo en un estado $t$:

$$p(o^{(1)}...o^{(T)}, s_i^{(t)}) = \alpha_i(t) \beta_i(t)$$

Más aún, también nos puede dar las probabilidades condicionales de la siguiente forma:

$$p(s_i^{(t)}|o^{(1)}...o^{(T)}) = \frac{\alpha_i(t) \beta_i(t)}{\sum_i \alpha_i(t) \beta_i(t)}$$

In [11]:
result = forward_backward(obs, (O,S,A,B,Pi), normalize=True)

#Probabilidad condicional
pd.DataFrame(data=result, columns=list(S.values()), index=['0']+obs)
simbolo: la - probabilidad 0.20859788359788356
simbolo: la nina - probabilidad 0.03951877047115142
simbolo: la nina pasa - probabilidad 0.011220368570708704
simbolo: la nina pasa la - probabilidad 0.0020895512786453284
simbolo: la nina pasa la garza - probabilidad 0.00041117213569291135
simbolo: garza - probabilidad: 0.20701058201058198
simbolo: la garza - probabilidad: 0.03951877047115142
simbolo: pasa la garza - probabilidad: 0.010412587433562487
simbolo: nina pasa la garza - probabilidad: 0.002226236896034973
simbolo: la nina pasa la garza - probabilidad: 0.0004111721356929113
Out[11]:
DA N V
0 0.684777 0.158185 0.157038
la 0.643323 0.244665 0.112012
nina 0.137279 0.695233 0.167488
pasa 0.098821 0.241848 0.659331
la 0.713422 0.140610 0.145968
garza 0.126837 0.674711 0.198452

Podemos entonces predecir la probabilidad de una etiqueta y obtener la etiqueta adecuada para cada estado de la cadena de observaciones. La predicción se hace como:

\begin{align} \hat{s}^{(t)} &= \arg\max_i p(s_i^{(t)}|o^{(1)}...o^{(T)}) = \arg\max_i \frac{\alpha_i(t) \beta_i(t)}{\sum_i \alpha_i(t) \beta_i(t)} \\ &= \arg\max_i p(o^{(1)}...o^{(T)}, s_i^{(t)}) = \arg\max_i \alpha_i(t) \beta_i(t) \end{align}

Como ya hemos guardado las variables $\alpha$ y $\beta$ basta multiplicarlas y obtener el argumento que maximiza el producto.

In [12]:
#Predicción en base a forward-backward
pred = [S[t] for t in np.argmax(result, axis=1)]
#Obtención de la secuencia etiquetada
tag_obs = list(zip(obs,pred[1:]))

print(tag_obs)
[('la', 'DA'), ('nina', 'N'), ('pasa', 'V'), ('la', 'DA'), ('garza', 'N')]