Viterbi Algorithm applies dynamic programming to find the optimal path of states to maximize the probability of generating the given sequence. In detail, assume the set of all states is \[\mathcal{S} = \{q_0,q_1,\cdots,q_n\},\] the given sequence is \[L = x_0x_1\cdots x_{l-1},\] our goal is to find the path of states $P$ s.t. \[P = \underset{Q\in \text{all paths}}{\operatorname{arg max}} \,\mathbb{P}(L|Q) = \underset{Q\in \text{all paths}}{\operatorname{arg max}} \,\mathbb{P}_t(q_{0}\,|\,q_{k_{l-1}})\mathbb{P}_t(q_{k_0}\,|\,q_0)\mathbb{P}_e(x_{0}\,|\,q_{k_{0}}) \prod_{i=1}^{l-1} \mathbb{P}_t(q_{k_{i}}\,|\,q_{k_{i - 1}})\mathbb{P}_e(x_{i}\,|\,q_{k_{i}}),\] where $Q= q_{0}q_{k_0}q_{k_1}\cdots q_{k_{l-1}}q_0$. Denote the maximal probability as \[\mathbb{P}_m = \max_{Q\in \text{all paths}} \,\mathbb{P}(L|Q).\] Viterbi algorithm uses the following recursive equation to calculate $\mathbb{P}_m$ and find the path $P$ by tracing back, \[F(i,j)=\begin{cases} \displaystyle\max_{q_s\in \mathcal{S}}F(s,j-1)\mathbb{P}_t(i\,|\,q_s)\mathbb{P}_e(x_j\,|\,q_i)&1\leq j\leq l-1\\ \mathbb{P}_t(q_i\,|\,q_0)\mathbb{P}_e(x_0\,|\,q_i)&j=0\end{cases}\] where $F(i,j)$ is the function of the i-th state ($q_{i}$) and the j-th position in the sequence ($x_{j}$). Since the final step should be the transition to $q_0$, the maximal probability is \[\mathbb{P}_m(L) = \max_{q_s\in \mathcal{S}}F(s,l-1)\mathbb{P}_t(q_0\,|\,q_s).\]
Below is the implementation of Viterbi Algorithm.
import numpy as np def DecodeHMM(HMM, Input_seq): State = list(HMM.keys()) m = len(Input_seq) #length of seq n = len(State) #number of states #Initialization Score = [[-np.inf for x in range(n)] for x in range(m)] #scoring matrix Score[seq][state] Pointer =[['Null' for x in range(n)] for x in range(m)] #pointer matrix for i in range(1,n): PrTr = HMM[State[0]]['TR'][State[i]] PrEm = HMM[State[i]]['EM'][Input_seq[0]] Pr = np.log(PrTr)+np.log(PrEm) Score[0][i] = Pr if Pr > -np.inf : Pointer[0][i] = State[0] #Fill the score matrix and pointer matrix for pos in range(1,m): Nuc = Input_seq[pos]#nucleotide StateEmPos = [ i for i in State if HMM[i]['EM'][Nuc] > 0] #set of states that can emit current nucleotide for q in StateEmPos: StateTrq = [i for i in State if HMM[i]['TR'][q] > 0] #set of states that can transit to current state PrEm = HMM[q]['EM'][Nuc] for p in StateTrq: PrTr = HMM[p]['TR'][q] value = Score[pos-1][State.index(p)] + np.log(PrTr)+np.log(PrEm) if value > Score[pos][State.index(q)]: Score[pos][State.index(q)] = value Pointer[pos][State.index(q)] = p #trace back Best = State[1] Output = [State[0]] for i in range(2,n): CurrentScore = Score[m-1][i] + np.log(HMM[State[i]]['TR'][State[0]]) MaxScore = Score[m-1][State.index(Best)] + np.log(HMM[Best]['TR'][State[0]]) if CurrentScore > MaxScore : Best = State[i] MaxScore = Score[m-1][State.index(Best)] + np.log(HMM[Best]['TR'][State[0]]) Output.append(Best) pos = m - 1 while pos != -1: Best = Pointer[pos][State.index(Best)] Output.append(Best) pos -= 1 if MaxScore == - np.inf: Output = 'Null' else : Output = Output[::-1] return MaxScore,Output
For example, we can input a HMM and run the decoding function:
HMM = {'q0':{'TR':{'q0':0,'q1':0.7,'q2':0.2,'q3':0, 'q4':0.1},'EM':{'A':0,'T':0,'C':0,'G':0}}, 'q1':{'TR':{'q0':0,'q1':0.2,'q2':0.3,'q3':0.5,'q4':0}, 'EM':{'A':0.45,'T':0.45,'C':0.05,'G':0.0}}, 'q2':{'TR':{'q0':0,'q1':0, 'q2':0.8,'q3':0.2,'q4':0}, 'EM':{'A':0.05,'T':0.05,'C':0.25,'G':0.65}}, 'q3':{'TR':{'q0':0,'q1':0, 'q2':0.5,'q3':0,'q4':0.5}, 'EM':{'A':0.2,'T':0.6,'C':0.1,'G':0.1}}, 'q4':{'TR':{'q0':0.3,'q1':0.2,'q2':0.5,'q3':0,'q4':0},'EM':{'A':0.4,'T':0.1,'C':0.1,'G':0.4}}} DecodeHMM(HMM, 'ACCCT')
The output is:
(-13.466615801344505, ['q0', 'q1', 'q2', 'q2', 'q3', 'q4', 'q0'])