上一个博客为B-W算法准备了基础,也就是前向算法和后向算法以及EM模型。
现在看看到底Baum-Welch算法是如何利用上述算法及模型来更新HMM的参数的。
之前也分析过多次了,在语音识别领域HMM模型之所以这么复杂,是因为观察向量对应的隐含状态不可得。存在隐藏数据,如果能得知这些标注数据,那么无论是计算初始概率,状态转移概率还是混淆概率都异常的简单而直接。
隐藏向量,假设它为I,观察向量为O,(O,I)表明为完全数据。
现在假设表示当前模型下,完全数据的联合概率,
分别表示完全数据的对数似然概率,现在求得
,使这个Q函数最大值的
即为这次迭代的结果,即为完成一次迭代训练。
整个HMM模型里需要训练的参数包括三个分别是初始概率、转移概率和混淆概率。
我们需要从公式里推导出计算上述三个概率的计算式。公式推导可以参考这篇博客 ,我希望能从直觉上理解这些计算式。
在前一篇博客里,我们知道了前向算法表示在t时刻状态i下,输出观察向量
的概率,
表示在t时刻,t+1,t+2, ... ,N时刻观察向量为
的概率,那么
呢?表示在t时刻状态为i,整个观察向量为O的概率,可以写出连等式:
。定义
,更一般的定义一个变量
,
,是一个特例。
那么在已知观察向量和模型参数情况下,如何估算模型状态转移的概率呢?,根据定义可以把这一概率值计算分部进行,首先计算状态i的概率,然后计算状态(i,j)同时发生的概率。
由于计算状态转移概率时没有给定时间,假设为t时刻,,它与我们前面定义的
存在一个比例关系。
而,其值也可以通过前向-后向算法来求得,
,定义新的变量
。从这些公式以及状态转移的意义可以推出公式:
,
,
,
下面就是调试程序,看看在HERest程序中,是如何处理标注文件和特征文件来训练HMM模型。
大部分代码都是在处理命令行参数和读取数据。其实核心的代码不过数十行。但是要充分理解那数十行的代码要穿透重重迷雾。
首先读取phones0.mlf文件中的所有数据,构建一个个的MLFEntity,每个MLFEntity对应一个文件脚本,会指出该实体对应phones0.mlf文件的偏移位置。例如下面是phones0.mlf的开头。它包括三部分,MLF头文件,必须要包含否则报错,然后是两个MLFEntity,分别“S0001.lab”和“S0002.lab”后面的省略了。然后把这些实体对象被保存到一个全局的链表中。而这是在处理命令行参数 -I phones0.mlf 时发生的。
#!MLF!#
"*/S0001.lab"
sil
d
ay
ax
l
ey
t
f
ay
v
sil
.
"*/S0002.lab"
sil
d
ay
ax
l
z
ia
r
ow
z
ia
r
ow
ey
t
s
ih
k
s
ow
w
ah
n
z
ia
r
ow
n
ay
n
th
r
iy
f
ay
v
ey
t
f
ay
v
th
r
iy
th
r
iy
n
ay
n
z
ia
r
ow
sil
.
接着是处理-t参数和 -H参数。-H参数指定MMF( Master Model File)文件,它与MLF文件都是HTK的一种脚本文件,一个是用来定义模型的,一个是用来指定标签的。它们之间也存在某种内在关系,暂时不表。它们的文件路径和名字被暂时保存在HMMSet模型(全局变量hset)的“依赖文件链表中”,供后续处理使用。
最后就是初始化一个HMMSet集合,由monophones0参数指定,该文件的内容是指定了整个训练系统所有的音子,也就是通过该文件需要建立的声学模型个数。
现在分析主要的处理函数: Initialise(fbInfo, &fbInfoStack, &hset, GetStrArg()); InitUttInfo(utt, twoDataFiles); DoForwardBackward(fbInfo, utt, datafn, datafn2) ;看它们分别完成了哪些功能。
在进入这些函数之前,有两个重要的struct需要介绍,因为它们和后面的计算息息相关,一个是FBInfo,一个是UttInfo。
/* structure storing the model set and a pointer to it's alpha-beta pass structure */
typedef struct {
Boolean twoModels; /* Enable two model reestimation */
HMMSet *up_hset; /* set of HMMs to be re-estimated */
HMMSet *al_hset; /* HMMs to use for alignment */
/* these are equal unless 2 model reest */
HSetKind hsKind; /* kind of the alignment HMM system */
UPDSet uFlags; /* parameter update flags */
int skipstart; /* Skipover region - debugging only */
int skipend;
int maxM; /* maximum number of mixtures in hmmset */
int maxMixInS[SMAX];/* array[1..swidth[0]] of max mixes */
AlphaBeta *ab; /* Alpha-beta structure for this model */
AdaptXForm *inXForm;/* current input transform (if any) */
AdaptXForm *al_inXForm;/* current input transform for al_hset (if any) */
AdaptXForm *paXForm;/* current parent transform (if any) */
} FBInfo;
主要包括两个HMMSet和AlphaBeta指针,它们包含了模型训练的一些重要信息。两一个struct就是UttInfo。
/* structure for the utterance information */
typedef struct {
MemHeap transStack; /* utterance transcript information heap */
MemHeap dataStack; /* utterance data information heap */
MemHeap dataStack2; /* utterance data2 information heap */
int Q; /* number of models in transcription */
Transcription *tr; /* current transcription */
Boolean twoDataFiles; /* Using two data files */
int S; /* number of data streams */
int T; /* number of frames in utterance */
ParmBuf pbuf; /* parameter buffer */
ParmBuf pbuf2; /* a second parameter buffer (if required) */
Observation ot; /* Observation at time t ... */
Observation ot2; /* Cepstral Mean Normalised obervation, used in
single pass re-training */
LogDouble pr; /* log prob of current utterance */
} UttInfo;
它主要包括Transcription指针和Observation。
下面看函数调用 Initialise(fbInfo, &fbInfoStack, &hset, GetStrArg())的内部逻辑。
1、根据提供的模型脚本文件构建和初始化一个当前的模型以及其参数,MakeHMMSet( hset, hmmListFn )和LoadHMMSet( hset,hmmDir,hmmExt)。
2、然后调用 InitialiseForBack(fbInfo, x, hset, uFlags, pruneInit, pruneInc, pruneLim, minFrwdP);实现前向后向算法做准备。
主要是初始fbInfo里的一些参数,例如为AlphaBeta对象分配空间,设定模型的混合个数,等等
然后是InitUttInfo(utt, twoDataFiles);函数调用:初始化UttInfo对象的空间。
接着就是循环执行(语音,文本)对的前向-后向算法来重新估算模型参数,这里正式进入Baum-Welch算法的代码。
主要实现在DoForwardBackward(fbInfo, utt, datafn, datafn2)中,它执行的逻辑如下:
1、 LoadLabs(utt, lff, datafn_lab, labDir, labExt);
2、 LoadData(fbInfo->al_hset, utt, dff, datafn, datafn2);
3、 InitUttObservations(utt, fbInfo->al_hset, datafn, fbInfo->maxMixInS);
4、 FBFile(fbInfo, utt, datafn);
现在来分析每一步的执行逻辑,以及代码实现的对应公式。
第一步:在LoadLabs函数中,主要完成了通过datafn_lab的文件名,找到MLF文件中(phoneses0.mlf)它对应的transcription,也就是语音段对应的模型标注。例如第一个utterance是S0001.mfc,它对应于S0001.lab所指定的模型。该项目的所有标注数据都保存在phones0.mlf文件中,其实也可以写成单独的MLF格式的文件。
在LoadLabs函数中,会为UttInfo对象构建一个Transcription,它对应一个data/train/S000X.mfc文件。接下来的代码就是在这个链表中添加节点,每个节点对应一个模型的Label。节点的连接顺序就是文本的先后顺序。
typedef struct {
LabList *head; /* Pointer to head of Label List */
LabList *tail; /* Pointer to tail of Label List */
int numLists; /* num label lists (default=1) */
}Transcription;
执行顺序是LOpen ->LoadHTKLabels ->LoadHTKList。执行的结果,在utt对象的Transcription* tr对象中,指定了一个LabelList,它包含了当前音频对应的模型Label。主要逻辑是在LoadHTKList中实现的,它通过读取之前构建的MLFEntry对象,来实现Label节点并构建它们的链表。
第二步:执行LoadData。读取S0001.mfc的数据到utt对象的pbuf中,并把相关信息保存在BufferInfo中,例如包含多少个frame,每个frame包含多少个采样点,等等。
第三步InitUttObservations是第一次执行时,初始化一个观察向量的对象,就是分配空间。
第四步:FBFile(fbInfo, utt, datafn)执行前向-后向算法。这是咱们重点分析的地方。
算法所依赖的Label和frame data都预处理完了,分别保存在utt和fbInfo中。主要分两步:StepBack和StepForward。
在StepBack中,构建Beta矩阵。在StepForward中计算alpha矩阵。有了这两个参数,根据Baum-Welch的计算公式,就能重新评估模型参数了。