自然语言处理之维特比算法

刘勇  Email:[email protected]

简介

  鉴于维特比算法可解决多步骤中每步多选择模型的最优选择问题,本文简要介绍了维特比算法的基本理论,并从源代码角度对维特比算法进行剖析,并对源码中涉及的要点进行了解读,以便能快速应用该算法解决自然语言处理中的问题。

理论

  维特比算法是一个特殊但应用最广的动态规划算法,利用动态规划,可以解决任何一个图中的最短路径问题。而维特比算法是针对一个特殊的图——篱笆网络的有向图(Lattice )的最短路径问题而提出的。 凡是使用隐含马尔可夫模型描述的问题都可以用它来解码,包括今天的数字通信、语音识别、机器翻译、拼音转汉字、分词等。——摘自《数学之美》

  给定某一个观察者序列Y= {y1, y2, ..., yn}, 而产生它们的隐含状态为X={x1, x2, ..., xn}, 其中在任意时刻t(即观察者)下,其对应的隐含状态存在多种可能;若将每个观察者视为一步,则每步是一个多选择问题。如图-1所示为维特比算法的数学表达式, 其目标即为获取最大概率值下的隐含状态序列:

图-1 维比特算法数学表达式

  此外,若将上述隐含状态序列按值进行展开,则会得到常见的篱笆网络结构,如图-2所示:

图-2 篱笆网络

  维特比算法利用动态规划思想求解概率最大路径(可理解为求图最短路径问题), 其时间复杂度为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
  如果,您认为阅读这篇博客让您有些收获,不妨点击一下右下角的【推荐】。
  如果,您希望更容易地发现我的新博客,不妨点击一下左下角的【关注我】。
  如果,您对我的博客所讲述的内容有兴趣,请继续关注我的后续博客,我是【志青云集】。
  本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则将依法追究法律责任。


时间: 2024-11-05 07:19:34

自然语言处理之维特比算法的相关文章

机器学习算法总结(七)——隐马尔科夫模型(前向后向算法、鲍姆-韦尔奇算法、维特比算法)

概率图模型是一类用图来表达变量相关关系的概率模型.它以图为表示工具,最常见的是用一个结点表示一个或一组随机变量,结点之间的变表是变量间的概率相关关系.根据边的性质不同,可以将概率图模型分为两类:一类是使用有向无环图表示变量间的依赖关系,称为有向图模型或贝叶斯网:另一类是使用无向图表示变量间的相关关系,称为无向图模型或马尔科夫网. 隐马尔科夫模型(简称HMM)是结构最简单的动态贝叶斯网,是一种著名的有向图模型,主要用于时间序数据建模,在语音识别,自然语言处理,生物信息,模式识别中有着广泛的应用,虽

隐马尔科夫模型(前向后向算法、鲍姆-韦尔奇算法、维特比算法)

概率图模型是一类用图来表达变量相关关系的概率模型.它以图为表示工具,最常见的是用一个结点表示一个或一组随机变量,结点之间的变表是变量间的概率相关关系.根据边的性质不同,可以将概率图模型分为两类:一类是使用有向无环图表示变量间的依赖关系,称为有向图模型或贝叶斯网:另一类是使用无向图表示变量间的相关关系,称为无向图模型或马尔科夫网. 隐马尔科夫模型(简称HMM)是结构最简单的动态贝叶斯网,是一种著名的有向图模型,主要用于时间序数据建模,在语音识别,自然语言处理,生物信息,模式识别中有着广泛的应用,虽

条件随机场(CRF) - 4 - 学习方法和预测算法(维特比算法)

声明: 1,本篇为个人对<2012.李航.统计学习方法.pdf>的学习总结,不得用作商用,欢迎转载,但请注明出处(即:本帖地址). 2,由于本人在学习初始时有很多数学知识都已忘记,所以为了弄懂其中的内容查阅了很多资料,所以里面应该会有引用其他帖子的小部分内容,如果原作者看到可以私信我,我会将您的帖子的地址付到下面. 3,如果有内容错误或不准确欢迎大家指正. 4,如果能帮到你,那真是太好了. 学习方法 条件随机场模型实际上是定义在时序数据上的对数线性模型,其学习方法包括极大似然估计和正则化的极大

算法:HMM模型+维特比算法详解

一.HMM模型+维特比算法实例 1.问题描述 假设连续观察3天的海藻湿度为(Dry,Damp,Soggy),求这三天最可能的天气情况. 2.已知信息 ①天气只有三类(Sunny,Cloudy,Rainy),海藻湿度有四类{Dry,Dryish, Damp,Soggy },而且海藻湿度和天气有一定的关系. ②隐藏的状态:Sunny, Cloudy, Rainy; ③观察状态序列:{Dry, Damp, Soggy} ④初始状态序列: Sunny Cloudy Rainy 0.63 0.17 0.2

HMM的维特比算法简单示例

今天读了一位大牛的关于HMM的技术博客,读完之后,写了一个关于维特比算法的简单示例,用scala和java语言混合编写的.现在上传之. package com.txq.hmm import java.utilimport scala.collection._ /** * HMM维特比算法,根据显示状态链条估计隐式链条 * @param states 隐式states * @param observations 显式states * @param start_probability 初始概率向量

马尔可夫特性&&维特比算法

由于马尔可夫特性,最可能到达状态 Rain5 = true 的路径包含了到达时刻 4 的某个状态的最可能路径 ------ P421 所谓的"马尔可夫特性"究竟是?..? 维特比算法基于动态规划的思想(DP一般都求最优,不求最优的应用还未见过), 仔细观察"天气和雨伞"模型的维特比算法状态转移方程, 维特比算法是自底向上构建的动归 可以知道求解状态t+1的最优概率路径是基于从起始到状态t的所有可能状态(rain/sunny)的最优概率路径 也就是说,对于状态t的每一

维特比算法在隐马尔可夫模型中的应用

前言 文章标题的两个概念也许对于许多同学们来说都相对比较陌生,都比较偏向于于理论方面的知识,但是这个算法非常的强大,在很多方面都会存在他的影子.2个概念,1个维特比算法,1个隐马尔可夫模型.你很难想象,输入法的设计也会用到其中的一些知识. HMM-隐马尔可夫模型 隐马尔可夫模型如果真的要展开来讲,那短短的一篇文章当然无法阐述的清,所以我会以最简单的方式解释.隐马尔可夫模型简称HMM,根据百度百科中的描述,隐马尔可夫模型描述的是一个含有隐含未知参数的马尔可夫模型.模型的本质是从观察的参数中获取隐含

隐马尔可夫模型(五)——隐马尔可夫模型的解码问题(维特比算法)(转载)

阅读目录 HMM解码问题 维特比算法 时间复杂度 程序例证 回到顶部 HMM解码问题       给定一个观察序列O=O1O2...OT,和模型μ=(A,B,π),如何快速有效地选择在一定意义下"最优"的状态序列Q=q1q2...qT,使该状态最好地解释观察序列. 一种想法是求出每个状态的概率rt(i)最大(rt(i)=P(qt=si,O|μ)),记q't(i)=argQmax(rt(i)),但是这样做,忽略了状态之间的关系,很可能两个状态之间的概率为0,即aq't(i)q't+1(i

维特比算法基础

维特比算法是一个特殊,但应用最广的动态规划算法.利用动态规划,可以解决任何一个图中的最短路径问题.而维特比算法是针对一个特殊的图--篱笆网络(Lattice)的有向图最短路径问题而提出的.它之所以重要是因为,凡是使用隐含马尔科夫模型描述的问题都可以用它来解码. 假如用户输入的拼音是y1,y2,...,yn,对应的汉字是x1,x2,...,xn,根据概率公式: 输入的序列为y1,y2,...,yN,而产生他们的隐含序列是x1,x2,...,xN,可以用下图描述这个过程: 图1  适合维特比算法的隐