Kaldi中iVector的提取【二】

本篇接着kaldi中ivector的提取【一】继续分析ivector的在线提取方法。

特征设计

在feed模型之前,前端的特征处理操作包括如下:

  • 基本的特征提取(PLP,MFCC,FBANK)
  • 拼帧
  • Delta
  • 线性变换(LDA,PCA)
  • 特征融合(MFCC + Pitch)
  • 归一化(CMVN)

在线状态下,由于上述操作必须online的进行,kaldi将上述操作封装成下面几个类,定义在online-feature.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 模板类,提取PLP,MFCC,FBANK特征
// typedef OnlineGenericBaseFeature<MfccComputer> OnlineMfcc;
// typedef OnlineGenericBaseFeature<PlpComputer> OnlinePlp;
// typedef OnlineGenericBaseFeature<FbankComputer> OnlineFbank;
class OnlineGenericBaseFeature;
// 在线拼帧
class OnlineSpliceFrames;
// delta
class OnlineDeltaFeature;
// LDA等线性变换
class OnlineTransform;
// 特征拼接:MFCC+Pitch
class OnlineAppendFeature;
// 在线cmvn
class OnlineCmvn;

以上特征类继承OnlineFeatureInterface 通过GetFrame(int32, VectorBase<BaseFloat>*)获取特征输出,在构造函数中指定输入源。

比如,在ivector提取中,UBM模型的输入特征需要经过online-CMVN+Splice+LDA,那么最终的特征构造如下:

1
2
3
4
5
6
7
8
// base表示基本声学特征PLP/MFCC/FBANK
// cmvn表示对base做了cmvn的结果
OnlineCmvnState naive_cmvn_state(info.global_cmvn_stats);
cmvn = new OnlineCmvn(info.cmvn_opts, naive_cmvn_state, base);
// splice_normalized表示对cmvn进行在线拼帧
splice_normalized = new OnlineSpliceFrames(info_.splice_opts, cmvn);
// lda_normalized表示对splice_normalized进行在线LDA
lda_normalized = new OnlineTransform(info.lda_mat, splice_normalized);

可以这么理解这种设计,将OnlineFeatureInterface的基类理解为一个节点,那么制定每个节点之间的输入输出关系,输入节点为原始音频采样数据,调用输出节点的GetFrame()函数即可获得最终的特征,计算逻辑在节点的内部实现。

ivector-extract-online2.cc中,实现ivector提取的类OnlineIvectorFeature也是OnlineFeatureInterface的基类。调用GetFrame(int32, VectorBase<BaseFloat>*)即可获得截止当前帧的估计ivector。

在ivector提取的过程中,需要用到两种特征

  • Splice + LDA:作为ivector的零阶统计量
  • CMVN + Splice + LDA:作为UBM的输入,获取后验概率

OnlineIvectorFeature中,用成员lda_lda_normalized_表示。

在线估计

OnlineIvectorFeature类内部使用num_frames_states_来追踪上次估计的时间戳,每一次估计首先遍历新增的帧,更新统计量,并以一定的周期提取ivector。这个周期是可以设置的,代码逻辑如下:

1
2
3
4
5
6
7
8
for (; num_frames_stats_ <= frame; num_frames_stats_++) {
// 更新统计量
UpdateStatsForFrame(num_frames_stats_, 1.0);
// 以一定的周期提取ivector
if (t % ivector_period == 0)
// ivector_stats_: OnlineIvectorEstimationStats
ivector_stats_.GetIvector(num_cg_iters, &current_ivector_);
}

更新的统计量包括通过UBM获取的后验和Splice+LDA的特征。GetIvector函数内部使用共轭梯度法计算ivector,(没有直接计算ivector)主要考虑这么做可以减少计算耗时。

OnlineIvectorEstimationStats类中具体实现ivector的在线估计算法,核心函数是累积统计量的AccStats(IvectorExtractor, VectorBase<BaseFloat>, std::vector<std::pair<int32, BaseFloat> >)和ivector估计函数GetIvector(int32, VectorBase<double>*)

首先看一下ivector的计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void OnlineIvectorEstimationStats::GetIvector(
int32 num_cg_iters,
VectorBase<double> *ivector) const {
if (num_frames_ > 0.0) {
// 也可以这么做得到准确结果
// SpMatrix<double> quadratic_inv(quadratic_term_);
// quadratic_inv.Invert();
// ivector->AddSpVec(1.0, quadratic_inv, linear_term_, 0.0);
if ((*ivector)(0) == 0.0)
(*ivector)(0) = prior_offset_;
LinearCgdOptions opts;
opts.max_iters = num_cg_iters;
LinearCgd(opts, quadratic_term_, linear_term_, ivector);
} else {
ivector->SetZero();
(*ivector)(0) = prior_offset_;
}
}

根据注释部分,ivector提取方式为:

$\mathbf{Q}$(quadratic_term_),$\mathbf{L}$(linear_term_)在AccStats中完成估计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void OnlineIvectorEstimationStats::AccStats(
const IvectorExtractor &extractor,
const VectorBase<BaseFloat> &feature,
const std::vector<std::pair<int32, BaseFloat> > &gauss_post) {

Vector<double> feature_dbl(feature);
int32 ivector_dim = this->IvectorDim(),
quadratic_term_dim = (ivector_dim * (ivector_dim + 1)) / 2;
SubVector<double> quadratic_term_vec(quadratic_term_.Data(), quadratic_term_dim);

for (size_t idx = 0; idx < gauss_post.size(); idx++) {
int32 g = gauss_post[idx].first;
double weight = gauss_post[idx].second;
if (weight == 0.0)
continue;
linear_term_.AddMatVec(weight, extractor.Sigma_inv_M_[g], kTrans,
feature_dbl, 1.0);
SubVector<double> U_g(extractor.U_, g);
quadratic_term_vec.AddVec(weight, U_g);
}
}

离线方法中,$\mathbf{L},\mathbf{Q}$的计算如下:

在线方法中,一帧一帧的以累加形式不算修正:

$\mathbf{Q}$被初始化为$\mathbf{I}$。$\mathbf{x}$表示当前输入特征(feature_dbl),$p_c$表示以$\mathbf{x}$为输入的情况下,UBM第$c$个component的后验。$\mathbf{B}_c$和$\mathbf{U}_c$为ivector提取器中的Sigma_inv_M_[g]U_g

结合对OnlineIvectorEstimationStats::GetIvector的分析,式子$(1)$可以写成:

综上所述,考虑到在线方法中的ivector输出周期,因此,一个句子($T$帧)作为输入往往可以得到$N$个ivector($N < T$)。

共轭梯度法

考虑到效率问题,每次计算$\mathbf{w}$的时候都需要对矩阵求逆,共轭梯度法在这里起的作用是在不进行矩阵求逆的操作下求出$\mathbf{Q}\mathbf{w} = \mathbf{L}$的解$\mathbf{w}$,即线性方程组求解问题。

使用共轭梯度法求解线性方程组的思想是将$\mathbf{w}$看成方程:

的驻点。此时$\nabla_w f(\mathbf{w}) = \mathbf{Q}\mathbf{w} - \mathbf{L} = 0$。

定义$\mathbf{d}_1, \mathbf{d}_2$,若满足$\mathbf{d}_1^\top \mathbf{Q} \mathbf{d}_2 = 0$,那么称$\mathbf{d}_1, \mathbf{d}_2$关于$\mathbf{Q}$共轭。令$\mathbf{d}_k$表示迭代第$k$次的搜索方向,那么第$k + 1$次的搜索方向$\mathbf{d}_{k + 1}$为:

$\beta_{k}$使得$\mathbf{d}_{k + 1}$和$\mathbf{d}_k$关于$\mathbf{Q}$共轭。它的计算可以通过FR和PR算法得到,kaldi中使用的FR算法如下:

在第$k$步的搜索步长$\alpha_k$使得解从$\mathbf{w}_k$迁移到$\mathbf{w}_{k + 1}$:

其中$\alpha_k$计算如下:

共轭梯度法初始化$\mathbf{w}_0$为之前估计的ivector:

之后反复的计算$\mathbf{w}_k,\mathbf{g}_k,\mathbf{d}_k$,直到$\mathbf{g}_k = \mathbf{0}$的时候,$\mathbf{w}_k$即为所求的ivector。