【问题标题】:Getting the math right for a Hidden Markov Model in Java正确计算 Java 中的隐马尔可夫模型
【发布时间】:2017-12-13 08:46:39
【问题描述】:

为了学习和使用隐马尔可夫模型,我正在编写自己的代码来实现它们。我正在使用这个wiki article 来帮助我的工作。我不想求助于预先编写的库,因为我发现如果我自己编写它可以更好地理解。不,这不是学校作业! :)

很遗憾,我的最高学历是高中计算机科学和统计学。除了随意研究 ANN 库和 TensorFlow 之外,我没有机器学习方面的背景。因此,我在将数学方程式转换为代码时遇到了一些麻烦。具体来说,我担心我对 alpha 和 beta 函数的实现在功能上不正确。如果有人可以帮助描述我在哪里搞砸了,以及如何纠正我的错误以拥有一个正常运行的 HMM 实现,我们将不胜感激。

这是我的全班全局变量:

public int n; //number of states
public int t; //number of observations
public int time; //iteration holder
public double[][] emitprob; //Emission parameter
public double[][] stprob; //State transition parameter
public ArrayList<String> states, observations, x, y;

我的构造函数:

public Model(ArrayList<String> sts, ArrayList<String> obs)
{

    //the most important algorithm we need right now is 
    //unsupervised learning through BM. Supervised is 
    //pretty easy.

    //need hashtable of count objects... Aya... 

    //perhaps a learner...?
    states = sts;
    observations = obs;
    n = states.size();
    t = observations.size();

    x = new ArrayList();
    y = new ArrayList();

    time = 0;

    stprob = new double[n][n];

    emitprob = new double[n][t];

    stprob = newDistro(n,n);
    emitprob = newDistro(n,t);

}

newDistro 方法用于创建一个新的、均匀的、正态分布:

public double[][] newDistro(int x, int y)
{
    Random r = new Random(System.currentTimeMillis());
    double[][] returnme = new double[x][y];
    double sum = 0;
    for(int i = 0; i < x; i++)
    {
        for(int j = 0; j < y; j++)
        {
            returnme[i][j] = Math.abs(r.nextInt());
            sum += returnme[i][j];
        }
    }

    for(int i = 0; i < x; i++)
    {
        for(int j = 0; j < y; j++)
        {
            returnme[i][j] /= sum;
        }
    }

    return returnme;
}

我的维特比算法实现:

public ArrayList<String> viterbi(ArrayList<String> obs)
{
    //K means states
    //T means observations
    //T arrays should be constructed as K * T (N * T)
    ArrayList<String> path = new ArrayList();
    String firstObservation = obs.get(0);
    int firstObsIndex = observations.indexOf(firstObservation);
    double[] pi = new double[n]; //initial probs of first obs for each st
    int ts = obs.size();
    double[][] t1 = new double[n][ts];
    double[][] t2 = new double[n][ts];
    int[] y = new int[obs.size()];
    for(int i = 0; i < obs.size(); i++)
    {
        y[i] = observations.indexOf(obs.get(i));
    }

    for(int i = 0; i < n; i++)
    {
        pi[i] = emitprob[i][firstObsIndex];
    }

    for(int i = 0; i < n; i++)
    {
        t1[i][0] = pi[i] * emitprob[i][y[0]];
        t2[i][0] = 0;
    }

    for(int i = 1; i < ts; i++)
    {
        for(int j = 0; j < n; j++)
        {
            double maxValue = 0;
            int maxIndex = 0;
            //first we compute the max value
            for(int q = 0; q < n; q++)
            {
                double value = t1[q][i-1] * stprob[q][j];
                if(value > maxValue)
                {
                    maxValue = value; //the max
                    maxIndex = q; //the argmax
                }
            }
            t1[j][i] = emitprob[j][y[i]] * maxValue;
            t2[j][i] = maxIndex;
        }
    }
    int[] z = new int[ts];

    int maxIndex = 0;
    double maxValue = 0.0d;
    for(int k = 0; k < n; k++)
    {
        double myValue =  t1[k][ts-1];
        if(myValue > maxValue)
        {
            myValue = maxValue;
            maxIndex = k;
        }
    }
    path.add(states.get(maxIndex));

    for(int i = ts-1; i >= 2; i--)
    {
        z[i-1] = (int)t2[z[i]][i];
        path.add(states.get(z[i-1]));
    }
    System.out.println(path.size());
    for(String s: path)
    {
        System.out.println(s);
    }
    return path;
}

我的前向算法,它取代了后面描述的 alpha 函数:

public double forward(ArrayList<String> obs)
{
    double result = 0;
    int length = obs.size()-1;
    for(int i = 0; i < n; i++)
    {
        result += alpha(i, length, obs);
    }

    return result;
}

其余函数用于实现 Baum-Welch 算法。

恐怕我在这里做错最多的就是 alpha 函数。我很难理解它需要遍历序列的哪个“方向” - 我是从最后一个元素(size-1)还是第一个元素(索引为零)开始?

public double alpha(int j, int t, ArrayList<String> obs)
{

    double sum = 0;

    if(t == 0)
    {
        return stprob[0][j];
    }

    else
    {
        String lastObs = obs.get(t);
        int obsIndex = observations.indexOf(lastObs);
        for(int i = 0; i < n; i++)
        {
            sum += alpha(i, t-1, obs) * stprob[i][j] * emitprob[j][obsIndex];
        }
    }

    return sum;
}

我的 beta 函数也有类似的“正确性”问题:

public double beta(int i, int t, ArrayList<String> obs)
{
    double result = 0;

    int obsSize = obs.size()-1;

    if(t == obsSize)
    {
        return 1;
    }

    else
    {
        String lastObs = obs.get(t+1);
        int obsIndex = observations.indexOf(lastObs);
        for(int j = 0; j < n; j++)
        {
            result += beta(j, t+1, obs) * stprob[i][j] * emitprob[j][obsIndex];
        }
    }

    return result;
}

我对自己的 gamma 函数更有信心;但是,由于它明确要求使用 alpha 和 beta,显然我担心它会以某种方式“关闭”。

public double gamma(int i, int t, ArrayList<String> obs)
{
    double top = alpha(i, t, obs) * beta(i, t, obs);
    double bottom = 0;
    for(int j = 0; j < n; j++)
    {
        bottom += alpha(j, t, obs) * beta(j, t, obs);
    }
    return top / bottom;
}

我的“squiggle”功能也是如此——我为命名道歉;不确定符号的实际名称。

public double squiggle(int i, int j, int t, ArrayList<String> obs)
{
    String lastObs = obs.get(t+1);
    int obsIndex = observations.indexOf(lastObs);
    double top = alpha(i, t, obs) * stprob[i][j] * beta(j, t+1, obs) * emitprob[j][obsIndex];
    double bottom = 0;
    double innerSum = 0;
    double outterSum = 0;
    for(i = 0; i < n; i++)
    {
        for(j = 0; j < n; j++)
        {
            innerSum += alpha(i, t, obs) * stprob[i][j] * beta(j, t+1, obs) * emitprob[j][obsIndex];
        }
        outterSum += innerSum;
    }

    return top / bottom;
}

最后,为了更新我的状态转换和发射概率数组,我将这些函数实现为 aStar 和 bStar。

public double aStar(int i, int j, ArrayList<String> obs)
{
    double squiggleSum = 0;
    double gammaSum = 0; 
    int T = obs.size()-1;
    for(int t = 0; t < T; t++)
    {
        squiggleSum += squiggle(i, j, t, obs);
        gammaSum += gamma(i, t, obs);
    }
    return squiggleSum / gammaSum;
}

public double bStar(int i, String v, ArrayList<String> obs)
{
    double top = 0;
    double bottom = 0;

    for(int t = 0; t < obs.size()-1; t++)
    {
        if(obs.get(t).equals(v))
        {
            top += gamma(i, t, obs);
        }
        bottom += gamma(i, t, obs);
    }


    return top / bottom;
}

在我的理解中,由于 b* 函数包含一个返回 1 或 0 的分段函数,我认为在“if”语句中实现它并且仅在字符串等于观察历史时才添加结果是相同的正如所描述的那样,因为该函数会将调用呈现给 gamma 0,从而节省一点计算时间。这是正确的吗?

总之,我想让我的数学正确,以确保成功(尽管很简单)HMM 实施。至于 Baum-Welch 算法,我无法理解如何实现完整的功能 - 它是否像在所有状态上运行 aStar (作为 n * n FOR 循环)和 bStar 用于所有观察一样简单,在一个循环内收敛函数?此外,在不过度拟合的情况下检查收敛的最佳实践函数是什么?

请让我知道我需要做的所有事情才能做到这一点。

非常感谢您能给我的任何帮助!

【问题讨论】:

  • 这里有很多很棒的工作。不幸的是,没有简单的答案。我对如何从这里取得进展的建议:(1)从简单开始并建立起来。从简单的马尔可夫过程开始。首先计算前向演化,然后是似然函数,然后是转移矩阵的最大似然估计。添加观察模型以获得 HMM。像以前一样添加越来越复杂的功能。 (2) 当你遇到困难时,试着把问题简化为一个非常简单的案例,你知道(以某种方式)正确的结果,然后在这里提出一个问题。祝你好运,玩得开心,这是一个很棒的话题。

标签: java statistics


【解决方案1】:

为避免下溢,应在前向和后向算法中使用比例因子。为了得到正确的结果,可以使用嵌套的 for 循环,并且在 forward 方法中的步骤是向前的。

backward 方法类似于 forward 函数。 您可以从 Baum-Welch 算法的方法中调用它们。

【讨论】:

    猜你喜欢
    • 2015-11-22
    • 2012-08-15
    • 2012-06-18
    • 1970-01-01
    • 2012-09-18
    • 1970-01-01
    • 1970-01-01
    • 2014-06-11
    • 1970-01-01
    相关资源
    最近更新 更多