为 Java 中的隐马尔可夫模型进行正确的数学运算



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

不幸的是,我的最高教育水平包括高中计算机科学和统计学。我没有机器学习的背景,除了随意地玩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;
}

其余函数用于实现鲍姆-韦尔奇算法。

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;
}

我的测试版功能遇到了类似的"正确性"问题:

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;
}

我对自己的伽马函数更有信心;但是,由于它明确要求使用 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;
}

我的"波浪线"功能也是如此 - 我为命名道歉;不确定符号的实际名称。

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实现。至于鲍姆-韦尔奇算法,我很难理解如何实现完整的函数 - 它会像在所有状态上运行 aStar 一样简单(作为 n * n FOR 循环(和 bStar 用于所有观测,在一个具有收敛函数的循环中?另外,在不过度拟合的情况下检查收敛的最佳实践函数是什么?

请让我知道我需要做的一切,以便做到这一点。

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

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

后向方法类似于前向函数。 您可以从鲍姆-韦尔奇算法的方法调用它们。

最新更新