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,可见算法的重要性啊)
另外还有一些细节:
- 随着字符串的加长,概率的乘积也在不断加大,最终会在一定量后越界!所以可以采用Log方法,在算概率时,提前先Log一下,之后的 概率相乘也就变成相加了(Log(a*b) = Log a + Log b, 不要担心Log后变成负数怎么办,没问题的,Log是递增函数,谁大还是谁大 0.0)
- 注意每次最大划分的范围为汉语字典词的最大长度
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;
}
欢迎关注深度学习自然语言处理公众号