NLP Viterbi 算法应用之Unigrams,Bigrams

96
zenRRan
2016.10.18 13:24* 字数 970

Viterbi概述

维特比算法是一种 动态规划 算法用于寻找最有可能产生观测事件序列的-维特比路径-隐含状态序列,特别是在马尔可夫信息源上下文和隐马尔可夫模型中(隐马尔可夫模型(HMM)属于概率论之随机过程。动态规划,乃基本核心算法)

下面介绍n_gram算法的一元,二元模型算法基于Viterbi的具体实现。

Unigrams

算法实现思想:

例子初始化:
比如有个句子:中国人民生活水平
词最大长度 : 4
s[n] :Int 表示长度为n的句子的最优解
h[n] :Int 表示长度为n的句子的最佳划分点
P[str] : Double表示str的在汉语字典中出现的概率
一步一步来:

      s[0] = P(中)  h[0] = 0//前一个字最佳划分就是本身

      s[1] =  max( s[0] * P(国),P(中国) )
      h[1] = 此时选取最大的划分点

      s[2] = max( s[1] *  P(人) , s[0] * P(国人),P(中国人) )
      h[2] = 此时选取最大的划分点

      s[3] = max( s[2] *  P(民) , s[1] * P(人民),s[0] * P(国人民),P(中国人民) )
      h[3] = 此时选取最大的划分点
       ... ...
      s[] 目的是通过动态规划帮助h[] 选取最佳划分点。
      最后h = 0 0 2 2 4 4 6 6 
      切割过程:
              h[7] = 6 // 表示从6前面划分。(前面后面由自己h选取划分点决定)
              此时句子为:中国人民生活   水平 

             Int t = h[7] - 1
              将h[t] = 4
              此时句子为:中国人民   生活   水平 

             t = h[t] - 1
              将h[t] = 2
              此时句子为:中国   人民   生活   水平                   
      
             t = h[t] - 1
             t = 0结束分割

基本思路就是这样。汉语字典概率建议用数据结构字典Dictionary存储,因为这个有Hash查找,速度极快。(用数组存的时间查找划分可以为半天,Hash查找则为几百毫秒,QAQ,可见算法的重要性啊)

另外还有一些细节:

  1. 随着字符串的加长,概率的乘积也在不断加大,最终会在一定量后越界!所以可以采用Log方法,在算概率时,提前先Log一下,之后的 概率相乘也就变成相加了(Log(a*b) = Log a + Log b, 不要担心Log后变成负数怎么办,没问题的,Log是递增函数,谁大还是谁大 0.0)
  2. 注意每次最大划分的范围为汉语字典词的最大长度
    3.有一些比如一些半角的0,1,2...,或者偶尔有字典里没有的汉字或字母之类的,要计算它概率时要给他们赋一个值 ( 根据实际情况,赋什么值自己安排 )

核心算法代码如下:

public static String unigram(String str){
        int i,j;
        double q = 0 - Integer.MAX_VALUE;  // 初始化为负的最大值
        double[] s = new double[str.length()];
        int[] k = new int[str.length()];
        for(j = 0; j < str.length(); j++){            //动态规划双重循环
            if(j + 1 > data.theMaxWordLength){    // theMaxWordLength词的最大长度
                q = algorithm.P(str.substring(j - data.theMaxWordLength + 1, j+1));    // algorithm为自己写的类,封装各种需要的算法,P为概率
                k[j] = -1;
                for(i = j - data.theMaxWordLength ; i < j; i++){
                    double p = algorithm.P(str.substring(i+1 , j + 1));
                    if(q < s[i] + p){
                        q = s[i] + p;
                        k[j] = i;
                    }
                }
            }else{     //字符串长度不大于最大词长度,算法和上面的一样
                q = algorithm.P(str.substring(0, j+1));
                k[j] = -1;
                if(j == 0) {
                    s[j] = q;
                }
                for(i = 0; i < j; i++){
                    double p = algorithm.P(str.substring(i+1 , j + 1));
                    if(q < s[i] + p){
                        q = s[i] + p;
                        k[j] = i;
                    }
                }
            }
            s[j] = q;
        }
        String ss = str;
        int t = str.length() - 1;
        if (t != -1){
          while(k[t] != -1){      // 通过数组进行切割
           ss = ss.substring(0, t+1)+ "  "+ss.substring(t+1, ss.length());
           t = k[t];
           if(t == -1) break;
         }
       ss = ss.substring(0, t+1)+ "  "+ss.substring(t+1, ss.length());
       System.out.println(ss);
       
      }
        return ss;
    }

Bigrams

算法思想:

这个其实可以说是Unigrams的变种(一元变二元),但内部变化不是一般的小啊,一种变异吧。总的来说,比Unigrams难的多。

例子初始化:
比如还是那个句子:中国人民生活水平
词最大长度 : 4
s[i,j] :Int 表示以str长度在i,j之间的句子结尾的最优解
h[n] :Int 表示长度为n的句子的最佳划分点
P[str] : Double表示str的在汉语字典中出现的概率
P[str1,str2] : Double表示str1 在 str2前面的概率 //二元,就需要涉及到前面的 词,其实也可以表示str2 在str1后面的概率。这都是一样的。
一步一步来:

   判断长度为1的分割
    s[0,0] = P(中)  //前一个以"中"结尾的划分就这一个
    h[0] = 0

   判断长度为2的分割
    s[0,1] = P(中国)  //前一个以"中国"结尾的划分就这一个
    s[1,1] = s[0,0]*P(中,国)   //表示以"国" 结尾的情况
   h[1]  = i  //s[i,j]选取最佳的,则把此s[i,j] 的i 赋给 h[j] 即 h[1]

   判断长度为3的分割
    s[0,2] = P(中国人)  //前一个以"中国人"结尾的划分就这一个
    s[1,2] = max( s[0,0]*P(中,国人)  ) //表示以"国人" 结尾的情况,前面的以"中"结尾的情况
    s[2,2] = max(  s[0,1]*P(中国,人) ,s[1,1]*P(国,人)  )  //怕难以理解,以s[2,2] = s[0,1]*P(中国,人)为例子,表示当前结尾是str[2,2]的即"人",并且前面以str[0,1]结尾的即"中国" 的概率 ,即P(中国,人) 
   h[2]  = i  //s[i,j]选取最佳的,则把此s[i,j] 的i 赋给 h[j] 即 h[2]

... ...

(这里和Unigrams分割是一样的,不再赘述)
 最后 h = 0 0 2 2 4 4 6 6 
          切割过程:
                  h[7] = 6 
                  中国人民生活   水平 

                 Int t = h[7] - 1
                  h[t] = 4
                  中国人民   生活   水平 

                 t = h[t] - 1
                  h[t] = 2
                  中国   人民   生活   水平                   
          
                 t = h[t] - 1
                 t = 0结束分割

分割训练语句建立字典:
比如: 中国 人民 ,中国 社会
首先建立字典:fore_bigram : Dictionary 存所有词语的开头,这里存"中国"
其次建立字典:bigram : Dictionary 存连续两个词语,这里存key = "中国人民" value = 1,key = "中国社会" value = 1 (value 表示key出现的次数)
在fore_bigram 中 key = "中国" ,value = 2 // 表示以"中国"开头的词语有2个

除了Unigram要注意的细节外,另外还有一些重要细节:
比如 : 共同创造美好的新世纪
字典bigram中没有"创造美好" //没有 "美好"前面是"创造"的字符串
这里要退而求其次,回归到Unigram,Unigram怎么做,这里有问题的地方就怎么做,做完再跳回到Bigram方法继续做。

核心算法代码如下:

public static String bigram(String str){
        
        int i,j,k;
        double q = 0 - Integer.MAX_VALUE;
        double[][] s = new double[str.length()][str.length()];
        int[] h = new int[str.length()];
        for(i = 0; i < str.length(); i++){
            if(data.fore_bigram_map.containsKey(String.valueOf(str.charAt(i))) == false){  //防止单个字符不存在问题
                data.bigram_unigram_map.put(String.valueOf(str.charAt(i)), 1.0);
                data.bigram_unigram_map.put(String.valueOf(str.charAt(i)), 1.0);
                continue;
            }
            if(i + 1 > data.bigram_Max_wordLength){
                s[i-data.theMaxWordLength + 1][i] = PP(str.substring(i-data.theMaxWordLength + 1, i+1));
                q = s[i-data.theMaxWordLength + 1][i];
                for(j = i - data.theMaxWordLength + 1; j <= i; j++){
                    for(k = 0; k < j; k++){
                        double memo = s[k][j-1] + PP(str.substring(k, j),str.substring(j, i+1)); 
                        if(q < memo){
                           q = memo;
                           h[i] = j;
                        }
                    }
                    s[j][i] = q;
                }
            }else{
                s[0][i] = PP(str.substring(0, i+1));
                q = s[0][i];
                for(j = 0; j <= i; j++){
                    for(k = 0; k < j; k++){
                        double memo = s[k][j-1] + PP(str.substring(k, j),str.substring(j, i + 1)); 
                        if(q < memo){
                            q = memo;
                           h[i] = j;
                        }
                    }
                    s[j][i] = q;
                }
            }
      }
        for(i = 0; i < str.length(); i++){
            if(i-4 >=0 && h[i] == 0){
                h[i] = i;
            }
        }
        System.out.println();
        String ss = str;
        int t = str.length() - 1;
        if (t != -1){
          while(h[t] != 0){
           t = h[t]-1;
           if(t == -1) break;
           ss = ss.substring(0, t+1)+ "  "+ss.substring(t+1, ss.length());
         }
       System.out.println(ss);
        }
        return ss;
    }

欢迎关注深度学习自然语言处理公众号

image
自然语言处理
Web note ad 1