HanLP — HMM隱馬爾可夫模型 -- 維特比(Viterbi)算法 --示例代碼 - Java

Viterbi 維特比算法解決的是籬笆型的圖的最短路徑問題,圖的節點按列組織,每列的節點數量可以不一樣,每一列的節點只能和相鄰列的節點相連,不能跨列相連,節點之間有着不同的距離,距離的值就不在
image

題目背景

從前有個村兒,村裏的人的身體情況只有兩種可能:健康、發燒
假設這個村兒的人沒有體溫計或者百度這種神奇東西,他唯一判斷他身體情況的途徑就是到村頭我的偶像金正月的小診所詢問。月兒通過詢問村民的感覺,判斷她的病情,再假設村民只會回答正常、頭暈或冷
有一天村裏奧巴驢就去月兒那去詢問了。

  • 第一天她告訴月兒她感覺正常。
  • 第二天她告訴月兒感覺有點冷。
  • 第三天她告訴月兒感覺有點頭暈。
    那麼問題來了,月兒如何根據阿驢的描述的情況,推斷出這三天中阿驢的一個身體狀態呢?

已知情況

隱含的身體狀態 = {健康,發燒}
可觀察的感覺狀態 = {正常,冷,頭暈}

月兒預判的阿驢身體狀態的概率分佈(初始概率矩陣) = {健康:0.6,發燒:0.4}
月兒認爲的阿驢身體健康狀態的轉換概率分佈(轉移概率矩陣) =

{
健康->健康: 0.7 ,
健康->發燒: 0.3 ,
發燒->健康:0.4 ,
發燒->發燒: 0.6
}

月兒認爲的在相應健康狀況條件下,阿驢的感覺的概率分佈(發射概率矩陣) =

{
健康,正常:0.5 ,冷 :0.4 ,頭暈: 0.1 ;
發燒,正常:0.1 ,冷 :0.3 ,頭暈: 0.6
}

由上面我們可以發現,HMM的三要素都齊備了,下面就是解決問題了。
阿驢連續三天的身體感覺依次是: 正常、冷、頭暈 。

過程:

第一天的時候,對每一個狀態(健康或者發燒),分別求出第一天身體感覺正常的概率:P(第一天健康) = P(正常|健康)P(健康|初始情況) = 0.5 * 0.6 = 0.3 P(第一天發燒) = P(正常|發燒)P(發燒|初始情況) = 0.1 * 0.4 = 0.04
第二天的時候,對每個狀態,分別求在第一天狀態爲健康或者發燒情況下觀察到冷的最大概率。在維特比算法中,我們先要求得路徑的單個路徑的最大概率,然後再乘上觀測概率。P(第二天健康) = max{0.30.7, 0.040.4}0.4=0.30.70.4=0.084 此時我們需要記錄概率最大的路徑的前一個狀態,即0.084路徑的前一個狀態,我們在小本本上記下,第一天健康。 P(第二天發燒)=max{0.30.3, 0.040.6}0.3=0.027, 同樣的在0.027這個路徑上,第一天也是健康的。
第三天的時候,跟第二天一樣。P(第三天健康)=max{0.0840.7, 0.0270.4}0.1=0.00588,在這條路徑上,第二天是健康的。P(第三天發燒)=max{0.0840.3, 0.0270.6}0.6=0.01512,在這條路徑上,第二天是健康的。
最後一天的狀態概率分佈即爲最優路徑的概率,即P(最優)=0.01512,這樣我們可以得到最優路徑的終點,是發燒
由最優路徑開始回溯。請看我們的小本本,在求得第三天發燒概率的時候,我們的小本本上面寫的是第二天健康,好了,第二天就應該是健康的狀態,然後在第二天健康的情況下,我們記錄的第一天是健康的。這樣,我們的狀態序列逆推出來了。即爲:健康,健康,發燒
簡略的畫個圖吧:
image
這兒的箭頭指向就是一個回溯查詢小本本的過程,我們在編寫算法的時候,其實也得注意,每一個概率最大的單條路徑上都要把前一個狀態記錄下來。

代碼

Viterbi

package com.vipsoft.viterbi;

/**
 * 維特比算法
 * @author hankcs
 */
public class Viterbi
{
    /**
     * 求解HMM模型
     * @param obs 觀測序列
     * @param states 隱狀態
     * @param start_p 初始概率(隱狀態)
     * @param trans_p 轉移概率(隱狀態)
     * @param emit_p 發射概率 (隱狀態表現爲顯狀態的概率)
     * @return 最可能的序列
     */
    public static int[] compute(int[] obs, int[] states, double[] start_p, double[][] trans_p, double[][] emit_p)
    {
        double[][] V = new double[obs.length][states.length];
        int[][] path = new int[states.length][obs.length];

        for (int y : states)
        {
            V[0][y] = start_p[y] * emit_p[y][obs[0]];
            path[y][0] = y;
        }

        for (int t = 1; t < obs.length; ++t)
        {
            int[][] newpath = new int[states.length][obs.length];

            for (int y : states)
            {
                double prob = -1;
                int state;
                for (int y0 : states)
                {
                    double nprob = V[t - 1][y0] * trans_p[y0][y] * emit_p[y][obs[t]];
                    if (nprob > prob)
                    {
                        prob = nprob;
                        state = y0;
                        // 記錄最大概率
                        V[t][y] = prob;
                        // 記錄路徑
                        System.arraycopy(path[state], 0, newpath[y], 0, t);
                        newpath[y][t] = y;
                    }
                }
            }

            path = newpath;
        }

        double prob = -1;
        int state = 0;
        for (int y : states)
        {
            if (V[obs.length - 1][y] > prob)
            {
                prob = V[obs.length - 1][y];
                state = y;
            }
        }

        return path[state];
    }
}

DoctorExample

package com.vipsoft.viterbi;

import static com.vipsoft.viterbi.DoctorExample.Feel.cold;
import static com.vipsoft.viterbi.DoctorExample.Feel.dizzy;
import static com.vipsoft.viterbi.DoctorExample.Feel.normal;
import static com.vipsoft.viterbi.DoctorExample.Status.Fever;
import static com.vipsoft.viterbi.DoctorExample.Status.Healthy;

public class DoctorExample
{
    enum Status
    {
        /**
         * 健康
         */
        Healthy,
        /**
         * 發熱
         */
        Fever,
    }

    enum Feel
    {
        /**
         *  正常
         */
        normal,
        /**
         * 冷
         */
        cold,
        /**
         * 頭暈
         */
        dizzy,
    }

    static int[] states = new int[]{Healthy.ordinal(), Fever.ordinal()};
    /**
     * 初始概率矩陣
     * { 健康:0.6 , 發燒: 0.4 }
     */
    static double[] start_probability = new double[]{0.6, 0.4};
    /**
     * 轉移概率矩陣
     * {
     *  健康->健康:0.7 ,
     *  健康->發燒:0.3 ,
     *  發燒->健康:0.4 ,
     *  發燒->發燒:0.6
     * }
     */
    static double[][] transititon_probability = new double[][]{
            {0.7, 0.3},
            {0.4, 0.6},
    };
    /**
     * 發射概率矩陣
     * {
     *   健康,正常:0.5 ,冷 :0.4 ,頭暈: 0.1 ;
     *   發燒,正常:0.1 ,冷 :0.3 ,頭暈: 0.6
     * }
     */
    static double[][] emission_probability = new double[][]{
            {0.5, 0.4, 0.1},
            {0.1, 0.3, 0.6},
    };

    public static void main(String[] args)
    {
        // 連續三天的身體感覺依次是: 正常、冷、頭暈,推算出這三天的身體狀態
        int[] observations = new int[]{normal.ordinal(), cold.ordinal(), dizzy.ordinal()};
        int[] result = Viterbi.compute(observations, states, start_probability, transititon_probability, emission_probability);
        for (int r : result)
        {
            System.out.print(Status.values()[r] + " ");
        }
        System.out.println();
    }
}

源碼:https://gitee.com/VipSoft/VipBoot/tree/develop/vipsoft-viterbi/src/main/java/com/vipsoft/viterbi

引用:https://www.zhihu.com/question/20136144 裏的問題回答,正好是 HanLP 的Demo示例

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章