天然語言處理之維特比算法

劉勇  Email:lyssym@sina.comhtml

簡介java

  鑑於維特比算法可解決多步驟中每步多選擇模型的最優選擇問題,本文簡要介紹了維特比算法的基本理論,並從源代碼角度對維特比算法進行剖析,並對源碼中涉及的要點進行了解讀,以便能快速應用該算法解決天然語言處理中的問題。git

理論github

  維特比算法是一個特殊但應用最廣的動態規劃算法,利用動態規劃,能夠解決任何一個圖中的最短路徑問題。而維特比算法是針對一個特殊的圖——籬笆網絡的有向圖(Lattice )的最短路徑問題而提出的。 凡是使用隱含馬爾可夫模型描述的問題均可以用它來解碼,包括今天的數字通訊、語音識別、機器翻譯、拼音轉漢字、分詞等。——摘自《數學之美》算法

  給定某一個觀察者序列Y= {y1, y2, ..., yn}, 而產生它們的隱含狀態爲X={x1, x2, ..., xn}, 其中在任意時刻t(即觀察者)下,其對應的隱含狀態存在多種可能;若將每一個觀察者視爲一步,則每步是一個多選擇問題。如圖-1所示爲維特比算法的數學表達式, 其目標即爲獲取最大機率值下的隱含狀態序列:數組

圖-1 維比特算法數學表達式網絡

  此外,若將上述隱含狀態序列按值進行展開,則會獲得常見的籬笆網絡結構,如圖-2所示: app

圖-2 籬笆網絡post

  維特比算法利用動態規劃思想求解機率最大路徑(可理解爲求圖最短路徑問題), 其時間複雜度爲O(N*L*L),其中N爲觀察者序列長度,L爲隱含狀態大小。該算法的核心思想是:經過綜合狀態之間的轉移機率和前一個狀態的狀況計算出機率最大的狀態轉換路徑,從而推斷出隱含狀態的序列的狀況,即在每一步的全部選擇都保存了前繼全部步驟到當前步驟當前選擇的最小總代價(或者最大價值)以及當前代價的狀況下後續步驟的選擇。依次計算完全部步驟後,經過回溯的方法找到最優選擇路徑。學習

  簡單來講,在計算第t+1時刻的最短路徑時,只須要考慮從開始到當前t時刻下k個狀態值的最短路徑和當前狀態值到第t+1狀態值的最短路徑便可。如求t=3時的最短路徑,等於求t=2時的全部狀態結點x2t(見上圖-2所示)的最短路徑再加上t=2到t=3的各節點的最短路徑。

  如下將從源代碼的角度,對維比特算法進行剖析,分別從wikipedia、HanLP以及我的冗餘代碼3種實現方式進行剖析。

源碼之Wiki(有改動)

public class Viterbi {
    private static class TNode {
        public int[] v_path;  // 節點路徑
        public double v_prob; // 機率累計值

        public TNode( int[] v_path, double v_prob) {
            this.v_path = copyIntArray(v_path);
            this.v_prob = v_prob;
        }
    }

    private static int[] copyIntArray(int[] ia) { // 數組拷貝
        int[] newIa = new int[ia.length];
        System.arraycopy(ia, 0, newIa, 0, ia.length); // 較wiki源碼有改動
        return newIa;
    }

    private static int[] copyIntArray(int[] ia, int newInt) { // 數組拷貝
        int[] newIa = new int[ia.length + 1];
        System.arraycopy(ia, 0, newIa, 0, ia.length); // 較wiki的源碼稍有改動
        newIa[ia.length] = newInt;
        return newIa;
    }

    // forwardViterbi(observations, states, start_probability,
    // transition_probability, emission_probability)
    public int[] forwardViterbi(String[] y, String[] X, double[] sp,
            double[][] tp, double[][] ep) {
        TNode[] T = new TNode[X.length];
        for (int state = 0; state < X.length; state++) {
            int[] intArray = new int[1];
            intArray[0] = state;
            T[state] = new TNode(intArray, sp[state] * ep[state][0]);
        }

        for (int output = 1; output < y.length; output++) {
            TNode[] U = new TNode[X.length];
            for (int next_state = 0; next_state < X.length; next_state++) {
                int[] argmax = new int[0];
                double valmax = 0;
                for (int state = 0; state < X.length; state++) {
                    int[] v_path = copyIntArray(T[state].v_path);
                    double v_prob = T[state].v_prob;
                    double p = ep[next_state][output] * tp[state][next_state]; 
                    v_prob *= p; // 核心元語
                    if (v_prob > valmax) { // 每一輪會增長節點
                        if (v_path.length == y.length) { // 最終截止
                            argmax = copyIntArray(v_path);
                        } else {
                            argmax = copyIntArray(v_path, next_state); // 增長新的節點
                        }
                        valmax = v_prob;
                    }
                } // the number 3 for
                U[next_state] = new TNode(argmax, valmax);
            } // the number 2 for
            T = U;
        }
        // apply sum/max to the final states:
        int[] argmax = new int[0];
        double valmax = 0;
        for (int state = 0; state < X.length; state++) {
            int[] v_path = copyIntArray(T[state].v_path);
            double v_prob = T[state].v_prob;
            if (v_prob > valmax) {
                argmax = copyIntArray(v_path);
                valmax = v_prob;
            }
        }
        return argmax;
    }
}

  該算法的核心在於,內部類TNode中維護了一個動態數組v_path, 它隨着每一輪的迭代,即觀察者序列按序迭代時,其路徑長度在動態遞增;同時伴隨着機率累積值v_prob在更新。因爲v_path中是按照正序維護了觀察者序列按序到達最終節點中的局部機率最大值所對應的隱含狀態序列,所以該算法不須要進行回溯求解路徑。

源碼之HanLP

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]; } }

  從上述代碼可知,HanLP與Wiki中的實現比較相似,按照正序維護了最大機率所對應的路徑,所以也無需進行回溯。

源碼之冗餘代碼

    public static List<Integer> Viterbi(int[] obState, String[] termList, 
            double[] hiddenState, double[][]transitionMatrix, double[][]emitterMatrix) {
        int len = obState.length;
        int labelSize = hiddenState.length;
        double[][] result = new double[len][labelSize];
        int[][] max = new int[len][labelSize];
        double tmp = 0;
        
        // 初始化
        for (int i = 0; i < labelSize; i++) {
            result[0][i] = hiddenState[i] * emitterMatrix[i][obState[0]]; // 對初始行進行賦值
        }
        
        // 迭代
        for (int i = 1; i < len; i++) {
            for (int j = 0; j < labelSize; j++) {
                tmp = result[i-1][0] * transitionMatrix[0][j] * emitterMatrix[j][obState[i]];
                max[i][j] = 0;
                for (int k = 1; k < labelSize; k++) { // 初始值時從0開始,則此處從1開始
                    double prob = result[i-1][k] * transitionMatrix[k][j] 
* emitterMatrix[j][obState[i]]; // 核心元語 if (prob > tmp) { tmp = prob; max[i][j] = k; // 保存路徑節點 } result[i][j] = tmp; } } } // 從結束點開始 List<Integer> adjList = new ArrayList<Integer>(); int maxNode = 0; double maxValue = result[len-1][0]; for (int k = 1; k < labelSize; k++) { // maxValue 默認從0開始, 則此處從1開始 if (result[len-1][k] > maxValue) { maxValue = result[len-1][k]; maxNode = k; } } adjList.add(maxNode); // 回溯 for (int i = len-1; i > 0; i--) { maxNode = max[i][maxNode]; adjList.add(maxNode); } // 獲取結果 List<Integer> retList = new ArrayList<Integer>(); for (int i = adjList.size()-1; i >= 0; i--) { retList.add(adjList.get(i)); } return retList; }

  該部分冗餘代碼爲我的所寫,從代碼流程來看,它從初始化到回溯整個過程都進行詳細的闡述。尤爲須要注意註釋中「默認從0開始, 則此處從1開始」的部分,其中更多的從代碼簡化的角度進行了必定的優化工做。

總結

  上述3種實現,都可以在實際工程中應用,我的強烈推薦採用第1和第2,第3種做爲學習能夠參考。維特比算法只是解決隱馬爾科夫第三個問題(求觀察序列的最可能的標註序列)的一種實現方式,所以維特比算法和隱馬爾科夫模型不能等價。涉及多步驟每步多選擇模型的最優選擇問題,可採用該算法來解決。

 

 參考內容:

1) 吳軍. 數學之美.

2) HanLP-Viterbi: https://github.com/hankcs/Viterbi/blob/master/src/com/hankcs/algorithm/Viterbi.java

3) Wikipedia-Viterbi:https://en.wikibooks.org/wiki/Algorithm_Implementation/Viterbi_algorithm

4) http://www.cnblogs.com/ryuham/p/4686496.html

 

 


  做者:志青雲集
  出處:http://www.cnblogs.com/lyssym
  若是,您認爲閱讀這篇博客讓您有些收穫,不妨點擊一下右下角的【推薦】。
  若是,您但願更容易地發現個人新博客,不妨點擊一下左下角的【關注我】。
  若是,您對個人博客所講述的內容有興趣,請繼續關注個人後續博客,我是【志青雲集】。
  本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然將依法追究法律責任。

相關文章
相關標籤/搜索