Java實現:拋開jieba等工具,寫HMM+維特比演算法進行詞性標註

Charzueus發表於2020-10-21

一、前言:詞性標註

二、經典維特比演算法(Viterbi)

三、演算法實現

四、完整程式碼

五、效果演示:

六、總結


一、前言:詞性標註

  詞性標註(Part-Of-Speech tagging, POS tagging),是語料庫語言學中將語料庫中單詞的詞性按其含義和上下文內容進行標記的文字資料處理技術。詞性標註可以由人工或特定演算法完成,使用機器學習(machine learning)方法實現詞性標註是自然語言處理(NLP)的研究內容。常見的詞性標註演算法包括隱馬爾可夫模型(Hidden Markov Model, HMM)、條件隨機場(Conditional random fields, CRFs)等。

在進入本篇演算法的應用和實踐之前,建議學習以下兩篇內容,會有更好更容易的理解。

1、隱馬爾可夫模型(HMM)來龍去脈(一)(https://www.cnblogs.com/chenzhenhong/p/13537680.html)

2、隱馬爾可夫模型(HMM)來龍去脈(二)(https://www.cnblogs.com/chenzhenhong/p/13592058.html)

本篇實踐的目標:

除了用jieba等分詞詞性標註工具,不如自行寫一個演算法實現同樣的功能,這個過程可以對理論知識更好地理解和應用。下面將詳細介紹使用HMM+維特比演算法實現詞性標註。在給定的單詞發射矩陣和詞性狀態轉移矩陣,完成特定句子的詞性標註。

二、經典維特比演算法(Viterbi)

詞性標註使用隱馬爾可夫模型原理,結合維特比演算法(Viterbi),具體演算法虛擬碼如下:

維特比演算法正是解決HMM的三個基本問題中的第二個問題:在給定的觀察序列中,找出最優的隱狀態序列。應用在詞性標註上,就是找到概率最大化的單詞的詞性。

下面是對維特比演算法更加容易的解釋:

  1. 觀察序列長度 T,狀態個數N
  2. for 狀態s from 1 to N:do
  3.     //計算每個狀態的概率,相當於計算第一觀察值的隱狀態t=1
  4.      v[s,1] = a(0,s)*b(O1|s) //初始狀態概率 * 發射概率
  5.     //回溯儲存最大概率狀態
  6.     back[s,1]=0
  7. //計算每個觀察(詞語)取各個詞性的概率,儲存最大者
  8. for from 觀察序列第二個 to T do:
  9.     for 狀態s from 1 to N:do
  10.     //當前狀態由前一個狀態*轉移*發射(該狀態/詞性下詞t的概率),儲存最大者
  11.       v[s,t]=max v[i,t-1]*a[i,s]*b(Ot | s)
  12.       //儲存回溯點,該點為前一個狀態轉移到當前狀態的最大概率點
  13.       back[s,t]=arg{1,N} max v[i,t-1]*a(i,s)
  14.  //最後
  15.   v[T]=max v[T]
  16.   back[T] = arg{1,N} max v[T]
  17.  //回溯輸出隱狀態序列

三、演算法實現

第一步,拆分演算法計算問題,計算狀態轉移概率矩陣和符號發射概率矩陣方法:

根據給出的單詞出現次數和詞性狀態矩陣,使用computeProp()方法計算得到發射概率矩陣和狀態轉移矩陣。

public void computeProp(float[][] A) {//計算概率矩陣
        int i, j;
        float[] t = new float[A.length];
        //平滑資料,對陣列每個元素值加一
        for (i = 0; i < A.length; i++) {
            for (j = 0; j < A[i].length; j++) {
                A[i][j] += 1;
                t[i] += A[i][j];
            }
        }
        //計算當前元素在該行中的概率
        for (i = 0; i < A.length; i++)
            for (j = 0; j < A[i].length; j++)
                A[i][j] /= t[i];
 
    }

得到狀態轉移概率矩陣如下:

 得到符號發射概率矩陣如下:

第二步,核心演算法。本程式的關鍵部分是維特比演算法的實現,計算得到最大概率的隱狀態,然後儲存最佳狀態轉移位置。對於每個觀察值,先計算對應的可能的隱狀態。

public int[] Viterbi(float[][] A, float[][] B, String[] O,double[] init) {
        int back[][] = new int[A.length][O.length];
        float V[][] = new float[A.length][O.length];
        int i, s, k, t;
 
        for (i = 0; i < A.length; i++) {
            V[i][0] = (float) (init[i] * B[i][0]);
            back[i][0] = i;
        }
        //計算每個觀察值的取隱狀態中的最大概率
        for (t = 1; t < O.length; t++) {
            int[][] path = new int[A.length][O.length];
            //遍歷每個隱狀態,計算狀態轉移到當前狀態的概率,得到最大概率狀態
            for (s = 0; s < A.length; s++) {
                float maxSProb = -1;
                int preS = 0;
                for (i = 0; i < A.length; i++) {
                    float prob = V[i][t - 1] * A[i][s] * B[s][t];//B[s][t]為隱狀態s到觀察值t的發射概率
                    if (prob > maxSProb) {
                        maxSProb = prob;
                        preS = i;
                    }
                }
                //儲存該狀態的最大概率
                V[s][t] = maxSProb;
                //記錄路徑
                System.arraycopy(back[preS],0,path[s],0,t);
                path[s][t]=s;//最大概率狀態轉移記錄
            }
            back=path;//更新最優路徑
        }
 
        //回溯路徑,找到最後狀態
        float maxP = -1;
        int lastS = 0;
        for (s = 0; s < A.length; s++) {
            if (V[s][O.length - 1] > maxP) {
                maxP = V[s][O.length - 1];
                lastS = s;
            }
        }
        return back[lastS];//返回最佳路徑
    }

以上是維特比演算法,重要的程式碼語句解析可見註釋。演算法實現了將待標註句子使用維特比演算法計算最大概率,得到最佳路徑。

網上大部分使用了python實現該演算法,python寫起來簡單,所以我嘗試使用java實現,略有不同,期間遇到了一些小問題,後來不斷debug解決問題。得到正確的java編寫的維特比演算法。

四、完整程式碼

Java實現:拋開jieba等工具,寫HMM+維特比演算法進行詞性標註
 
 
/*
 *  hmm_viterbi.java
 * Copyright (c) 2020-10-17
 * author : Charzous
 * All right reserved.
 */
 
public class hmm_viterbi {
 
    public int[] Viterbi(float[][] A, float[][] B, String[] O) {
        int back[][] = new int[A.length][O.length];
        float V[][] = new float[A.length][O.length];
        double[] init = {0.2, 0.1, 0.1, 0.2, 0.3, 0.1};
        int i, s, k, t;
 
        for (i = 0; i < A.length; i++) {
            V[i][0] = (float) (init[i] * B[i][0]);
            back[i][0] = i;
        }
        //計算每個觀察值的取隱狀態中的最大概率
        for (t = 1; t < O.length; t++) {
            int[][] path = new int[A.length][O.length];
            //遍歷每個隱狀態,計算狀態轉移到當前狀態的最大概率
            for (s = 0; s < A.length; s++) {
                float maxSProb = -1;
                int preS = 0;
                for (i = 0; i < A.length; i++) {
                    float prob = V[i][t - 1] * A[i][s] * B[s][t];//B[s][t]為隱狀態s到觀察值t的發射概率
                    if (prob > maxSProb) {
                        maxSProb = prob;
                        preS = i;
                    }
                }
                //儲存該狀態的最大概率
                V[s][t] = maxSProb;
                //記錄路徑
                System.arraycopy(back[preS],0,path[s],0,t);
                path[s][t]=s;
            }
            back=path;
        }
 
 
        //回溯路徑
        float maxP = -1;
        int lastS = 0;
        for (s = 0; s < A.length; s++) {
            if (V[s][O.length - 1] > maxP) {
                maxP = V[s][O.length - 1];
                lastS = s;
            }
        }
        return back[lastS];
    }
 
    public void computeProp(float[][] A) {
        int i, j;
        float[] t = new float[A.length];
        //平滑資料,對陣列每個元素值加一
        for (i = 0; i < A.length; i++) {
            for (j = 0; j < A[i].length; j++) {
                A[i][j] += 1;
                t[i] += A[i][j];
            }
        }
        //計算當前元素在該行中的概率
        for (i = 0; i < A.length; i++)
            for (j = 0; j < A[i].length; j++)
                A[i][j] /= t[i];
 
        System.out.println();
//        for (i = 0; i < A.length; i++) {
//            for (j = 0; j < A[i].length; j++)
//                System.out.print(A[i][j] + " ");
//            System.out.println();
//        }
 
    }
 
    public static void main(String[] args) {
        //狀態轉移矩陣
        float A[][] = {{0, 0, 0, 48636, 0, 19}, {1973, 0, 426, 187, 0, 38}, {43322, 0, 1325, 17314, 0, 185}, {1067, 3720, 42470, 11773, 614, 21392}, {6072, 42, 4758, 1476, 129, 1522}, {8016, 75, 4656, 1329, 954, 0}};
        //發射矩陣
        float B[][] = {{0, 0, 0, 0, 0, 0, 69016, 0}, {0, 10065, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 5484, 0, 0, 0, 0}, {10, 0, 36, 0, 382, 108, 0, 0}, {43, 0, 133, 0, 0, 4, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 48809}};
 
        int i, j;
        //語料庫詞語
        String[] word = {"bear", "is", "move", "on", "president", "progress", "the", "."};
        //待標註句子
        String O[] = {"The", "bear", "is", "on", "the", "move", "."};
        //語料庫詞性
        String Q[] = {"/AT ", "/BEZ ", "/IN ", "/NN ", "/VB ", "/PERIOD "};
        String seq="Bear move on the president .";
        String Os[]=seq.split(" ");
        for (String w:O)
            System.out.println(w);
 
        float emission[][] = new float[B.length][O.length];
 
        hmm_viterbi hmmViterbi = new hmm_viterbi();
        hmmViterbi.computeProp(A);
 
        //計算觀察序列的轉移矩陣
        //根據待標註句子的詞計算出每個單詞的出現次數矩陣
        for (i = 0; i < O.length; i++) {
            int r = 0;
            for (int t = 0; t < word.length; t++) {
                if (O[i].equalsIgnoreCase(word[t]))
                    r = t;
            }
            for (j = 0; j < B.length; j++)
                emission[j][i] = B[j][r];
        }
        hmmViterbi.computeProp(emission);
        int path[];
        path=hmmViterbi.Viterbi(A, emission, O);
 
        for (i=0;i<O.length;i++){
            System.out.print(O[i]+Q[path[i]]);
        }
 
//        for (int p:path)
//            System.out.print(p+" ");
 
    }
}
View Code

五、效果演示:

對於本實驗的詞性標註,簡單設計了互動介面,方面測試不同句子的標註結果。在給定的測試句子”The bear is on the move .”上,實驗結果如下:

The/AT bear/NN is/BEZ on/IN the/AT move/NN ./PERIOD

然後根據語料庫自己造了一個句子,僅供測試用:”The president is bear .”實驗結果:The/AT president/NN is/IN bear/NN ./PERIOD

感覺還可以,當然這只是一個例子,更確切需要更大的語料庫。

六、總結

  本篇詳細介紹Java實現的HMM+維特比演算法實現詞性標註。在給定的單詞發射矩陣和詞性狀態轉移矩陣,完成特定句子的詞性標註。這個任務可以在剛接觸HMM和維特比演算法進行詞性標註作為實踐,為之後實現特定語料庫的詞性標註鋪墊。在完成本任務時,java程式設計實現演算法時遇到了一些的問題,如:最佳路徑的儲存,回溯路徑的返回。經過了一段時間的debug,實現了最基本的演算法對句子進行詞性標註。完成這個任務後,對HMM+Viterbi 演算法的詞性標註有了更深刻的理解,之後準備完成第三個任務:基於人民日報資料集的中文詞性標註,可以對該演算法進行更實際的應用,加深知識的理解。


我的部落格園:https://www.cnblogs.com/chenzhenhong/p/13850687.html

我的CSDN部落格:https://blog.csdn.net/Charzous/article/details/109138830

 

版權宣告:本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連結和本宣告。

 

本文連結:https://blog.csdn.net/Charzous/article/details/109138830

 

相關文章