Viterbi-EM语音识别训练方法
前文刚研究过语音识别特征提取以及基于Viterbi的状态解码方法,现着手研究基于GMM-HMM的语音语音识别声学模型训练方法,其理论部分可参考本人前期所写的GMM-HMM理论推导拖成,但上述推导过程是采用前后向算法更新模型参数,本人则主要采用Viterbi-EM训练方法对GMM中参数进行更新训练。
实际上该训练方法主要是针对GMM 中均值与方差参数进行调整,这其实恰恰证明了以HMM构建的语音识别系统核心在于GMM模型的参数训练,故语音识别核心在于声学模型,而声学模型核心在于GMM参数训练,实际上用神经网络更新模型HMM对应状态的发射概率亦是同样道理,只是神经网络是更新神经元中的权重以及偏置而GMM则是更新GMM中权重、均值以及方差。二者中GMM参数更新称之为似然训练,而NN训练则叫区分性训练,两种算法对于语音特征的拟合效果存在差异,实际上本质上相同:均采用相关算法得到特征对应的概率(前者、后者分别称为似然概率与后验概率)。
假设建模单元均为单音素且均采用单个GMM模型对HMM中单个状态进行建模,先确定三个容器m_gaussCounts、m_gaussStats1以及m_gaussStats2,现对三种容器介绍如下:
(1)鉴于GMM-HMM模型中状态数据是确定的,故m_gaussCounts采用vector容器存储语料库中对应状态出现的次数,假设状态数目为100,则m_gaussCounts容器从0状态开始存储各状态在语料中出现的次数,至于如何确定状态出现的次数,实际上就是将每帧结果对应至对应至概率最大的状态(实际过程中对应的是弧号,而弧号可以与状态进行唯一对应,具体可以参考本人之前所写解码过程,其对弧与状态之间转化进行了说明);
(2)m_gaussStats1采用Matrix容器存储总体特征数据,其维度大小受状态以及特征大小影响,必须指出的是:特征维度与m_gaussStats1容器的列大小一致。正如上文假设,则该容器大小维度为:100*12,其中100表示状态数目,12则表示前文提取的12维语音MFCC特征;
(3)同理,m_gaussStats2也采用Matrix容器存储总体平方特征数据,后文将对平方特征进行理论说明。其维度大小与m_gaussStats1维度一致。
1.容器初始化
上述容器在具体实现过程中,本人给出自己参考哥大代码所写的,容器初始化如下:
/** Total counts of each Gaussian. **/
vector<double> m_gaussCounts;
/** First-order stats for each dim of each Gaussian. **/
matrix<double> m_gaussStats1;
/** Second-order stats for each dim of each Gaussian. **/
matrix<double> m_gaussStats2;
2.容器统计量存储
介绍完上述三种容器,现介绍如何对语料库中特征进行统计进而存储至三种对应的容器中。
2.1 m_gaussCounts容器存储
正如其名所述,m_gaussCounts容器大小与语料库中状态数目必一致,前文设置为100,则该容器大小必为100。其中容器中每个元素代表对应的状态序号(其可以通过解码对其得到对应的弧号,进而转化为对应的状态,该过程称之为对齐),将语料库中所有帧属于的状态存储至该容器对应位置,故该容器统计结果为语料库中对应状态的个数,其实现核心代码如下:
double GmmStats::add_gmm_count(unsigned gmmIdx, double posterior,
const vector<double>& feats) {
if (m_gmmSet.get_component_count(gmmIdx) != 1)
throw runtime_error("GMM doesn't have single component.");
int gaussIdx = m_gmmSet.get_gaussian_index(gmmIdx, 0);
int dimCnt = m_gmmSet.get_dim_count();
//统计所有帧属于状态的个数,依次叠加;
m_gaussCounts[gaussIdx] += posterior;
for (int dimIdx = 0; dimIdx < dimCnt; ++dimIdx) {
m_gaussStats1(gaussIdx, dimIdx) += posterior * feats[dimIdx];
m_gaussStats2(gaussIdx, dimIdx) += posterior * pow(feats[dimIdx], 2);
}
return 0.0;
}
2.2 m_gaussStats1容器存储
m_gaussStats1容器存储的为语料库中对应状态的特征叠加结果,其是通过对齐得到具体某帧属于的状态号,该容器大小为:100*12,其中100表示状态,12则表示对应的特征,其具体含义可以表述为:属于该状态所有帧结果的累加,为后期计算均值所用,其具体理论实现树下:
double GmmStats::add_gmm_count(unsigned gmmIdx, double posterior,
const vector<double>& feats) {
if (m_gmmSet.get_component_count(gmmIdx) != 1)
throw runtime_error("GMM doesn't have single component.");
int gaussIdx = m_gmmSet.get_gaussian_index(gmmIdx, 0);
int dimCnt = m_gmmSet.get_dim_count();
m_gaussCounts[gaussIdx] += posterior;
for (int dimIdx = 0; dimIdx < dimCnt; ++dimIdx) {
//m_gaussStats1存储帧所对应状态所有特征的累加
m_gaussStats1(gaussIdx, dimIdx) += posterior * feats[dimIdx];
m_gaussStats2(gaussIdx, dimIdx) += posterior * pow(feats[dimIdx], 2);
}
return 0.0;
}
2.3 m_gaussStats容器存储
m_gaussStats2容器与m_gaussStats1容器类似,其理论概述与m_gaussStats1容器一致,不过m_gaussStats2容器存储的是对应特征属于该状态的平方,也就是前文所述的总平方特征,其具体实现如下:
double GmmStats::add_gmm_count(unsigned gmmIdx, double posterior,
const vector<double>& feats) {
if (m_gmmSet.get_component_count(gmmIdx) != 1)
throw runtime_error("GMM doesn't have single component.");
int gaussIdx = m_gmmSet.get_gaussian_index(gmmIdx, 0);
int dimCnt = m_gmmSet.get_dim_count();
m_gaussCounts[gaussIdx] += posterior;
for (int dimIdx = 0; dimIdx < dimCnt; ++dimIdx) {
m_gaussStats1(gaussIdx, dimIdx) += posterior * feats[dimIdx];
//m_gaussStats2表示特征对应状态总特征平方;
m_gaussStats2(gaussIdx, dimIdx) += posterior * pow(feats[dimIdx], 2);
}
return 0.0;
}
3.容器参数更新
将特征统计量结果赌赢存储至对应的容器中,现开始对容器中统计量结果进行参数更新,其主要涉及均值以及方差的计算,其中均值理论计算如下:
则方差的理论计算如下:
现介绍实际过程中均值与方差理论计算方法,现展示其代码如下所示:
void GmmStats::reestimate() const {
int gaussCnt = m_gmmSet.get_gaussian_count();
int dimCnt = m_gmmSet.get_dim_count();
double occupancy, mean, var;
for (int gaussIdx = 0; gaussIdx < gaussCnt; ++gaussIdx) {
occupancy = m_gaussCounts[gaussIdx];
for (int dimIdx = 0; dimIdx < dimCnt; ++dimIdx) {
//均值与方差重新估计,
mean = m_gaussStats1(gaussIdx, dimIdx) / occupancy;
var = m_gaussStats2(gaussIdx, dimIdx) / occupancy - mean * mean;
m_gmmSet.set_gaussian_mean(gaussIdx, dimIdx, mean);
m_gmmSet.set_gaussian_var(gaussIdx, dimIdx, var);
}
}
}
至此,基于Viterbi-EM的参数更新方法推导完毕。