当前位置: 首页 > 工具软件 > Kaldi > 使用案例 >

【Kaldi例子】Kaldi特征提取

汲永嘉
2023-12-01

声学特征提取

因为从语音时域信号中很难找到发音规律,即使是类似的发音,也可能看起来非常不同,因此一般不同直接用于识别。 事实上,我们的耳朵是通过频域而不是波形来辨认声音的,吧时域信号做短时傅里叶变换(Short-time Fourier Transform,STFT),就得到了声音的频谱。我们以帧为单位,根据听觉感知原理,按需调整声音片段频谱中各个片段的赋值,将其参数化,得到适合表示语音信号特性的向量,这就是声学特征(Acoustic Feature)。虽然在端到端语音识别研究中,也出现了一些直接输入语音波形的技术,但是性能和效率均无显著优势,传统提取声学特征的技术,仍然是语音识别的主流。

梅尔频率倒谱系数(Mel-Frequency Cepstral Coefficient,MFCCs)是最常见的声学特征,提取流程如下:

  • 对语音滑动加窗,实现分帧。通常帧长25ms,帧移10ms,这样可以保证帧内信号的平稳性,并使帧之间有交叠。
  • 每一帧做快速傅里叶变换,并计算功率谱。
  • 对功率谱应用梅尔滤波器组,获取每个滤波器内的对数能量作为系数。
  • 对得到的梅尔滤波器对数能量向量做离散余弦变换(Discrete Cosine Transform,DCT)。

通过设定DCT的输出个数,可以得到不同维数的MFCCs特征。

除了MFCCs特征外,还有许多其他声学特征,均可用compute-xxx-feats脚本计算。

特征存储

无论使用什么工具提取声学特征,要使用Kaldi进行训练,必须将特征保存为特征表单,最简单的方式是把特征矩阵携程文本格式的Kaldi表单,如:

103-1240-0000 [.0 .0 .0 .0 .0 ...]
103-1240-0001 [.0 .0 .0 .0 .0 ...]
...

方括号里每行为一帧,有多少个维特征,就有多少列,然后将其转换成二进制形式:

copy-matrix ark,t:feat_in_text.ark.txt ark:feat_in_binary.ark

因为特征表单很大,通常需要创建分散存储环境:

# 创建分散存储环境
if [ $stage -le 5 ]; then
  # spread the mfccs over various machines, as this data-set is quite large.
  if [[  $(hostname -f) ==  *.clsp.jhu.edu ]]; then
    mfcc=$(basename mfccdir) # in case was absolute pathname (unlikely), get basename.
    utils/create_split_dir.pl /export/b{02,11,12,13}/$USER/kaldi-data/egs/librispeech/s5/$mfcc/storage \
     $mfccdir/storage
  fi
fi
# 提取特征
if [ $stage -le 6 ]; then
  for part in dev_clean test_clean dev_other test_other train_clean_100; do
    # 特征提取脚本会自动检测分散存储链接文件并将特特征文件分散在这些目标路径中
    steps/make_mfcc.sh --cmd "$train_cmd" --nj 40 data/$part exp/make_mfcc/$part $mfccdir
    # 计算每个人倒谱均值方差归一化系数
    steps/compute_cmvn_stats.sh data/$part exp/make_mfcc/$part $mfccdir
  done
fi

第7步使用create_split_dir.pl,从train_clean_100中创建了三个不同的子集:

  • train_2kshort:最短的2000句
  • train_5k:随机挑选5000句
  • train_10k:随机挑选10000句

这些子集分别用于声学模型训练的不同阶段。

特征的使用

特征提取完成后,可通过声学特征表单feats.scp和倒谱均值方差归一化系数表单cmvn.scp获取归一化的特征。在训练声学模型时,通常还要对特征做更多扩展。

如Kaldi的单音子模型训练,在CMVN的基础上做了差分系数(Delta)扩展。

feats="ark,s,cs:apply-cmvn $cmvn_opts --utt2spk=ark:$sdata/JOB/utt2spk scp:$sdata/JOB/cmvn.scp scp:$sdata/JOB/feats.scp ark:- | add-deltas $delta_opts ark:- ark:- |"

在说话人自适应训练中,在CMVN的基础上做了前后若干帧的拼接,然后使用LDA矩阵降维。

sifeats="ark,s,cs:apply-cmvn $cmvn_opts --utt2spk=ark:$sdata/JOB/utt2spk scp:$sdata/JOB/cmvn.scp scp:$sdata/JOB/feats.scp ark:- | splice-feats $splice_opts ark:- ark:- | transform-feats $alidir/final.mat ark:- ark:- |"

Kaldi通过基础特征和碎片化工具和管道方法相配合,使得训练过程中的特征选择更加灵活。如在中文示例中,就有在某些阶段使用谱特征加基频的训练方法,某些阶段只用谱特征的训练方法。为了加速训练,在可以并行的训练阶段,大部分脚本根据指定的任务并行度拆分数据文件夹。

常用特征类型

脚本名作用配置文件
make_mfcc.sh提取MFCC特征mfcc.conf
make_mfcc_pitch.sh提取MFCC加基频特征mfcc.conf pitch.conf
make_mfcc_pitch_online.sh提取MFCC加在线基频特征mfcc.conf pitch_online.conf
make_fbank.sh提取Fbank特征fbank.conf
make_fbank_pitch.sh提取Fbank加基频特征fbank.conf pitch.conf
make_plp.sh提取PLP特征plp.conf
make_plp_pitch.sh提取PLP加基频特征plp.conf pitch.conf

在训练GMM声学模型时,由于计算量的限制,通常使用对角协方差矩阵,要求GMM概率密度函数各维度间条件独立,所以通常使用MFCC特征,并通过LDA等方法进一步解耦。

在训练神经网络,尤其是卷积神经网络的声学模型时,通常使用Fbank特征。

在提取三种谱特征时,有一个叫做dither的选项,默认值为1,作用是在计算滤波器系数能量时加入随机扰动,防止能量为0的情况出现。但会导致同一条音频输出特征前后不一致。若要保持一致,则需要配置文件中设置--dither=0

Kaldi的基频提取分两步:

  • 第一步是输出二维基频特征,分别是以Hz为单位的基频和表示其置信度的归一化相关系数(NCCF)
  • 第二部是在此基础上将其处理成适合语音识别系统的特征。

特征变换

特征变换是指将一帧声学特征经过某种变换,转换为另外一帧特征。输入声学特征是一个TxM的矩阵,输出是一个TxN的矩阵。

无监督特征变换

常用无监督特征变换技术包括

  • 差分(Delta): Δ f ( t , m ) = ∑ d = − W d = W d × f ( t + d , m ) ∑ d = − W d = W d 2 \Delta f(t,m)=\frac{\sum\limits_{d=-W}^{d=W}d\times f(t+d,m)}{\sum\limits_{d=-W}^{d=W}d^2} Δf(t,m)=d=Wd=Wd2d=Wd=Wd×f(t+d,m)
  • 拼帧(Splicing):将前后若干帧拼接成一帧特征
  • 归一化(Normalize,CMVN):将输入的声学特征进行规整,使其符合标准正态分布。不同范围的归一化形成不同归一化方法(如全局、说话人)

有监督特征变换

有监督特征变换最常用形式是将输入乘以一个特征变换矩阵,主要分两大类:

  • 线性判别分析(LDA):减小同类状态特征间的方差,增加不同类特征间的方差
  • 最大似然线性变换(MLLT)
    • 针对模型参数变换:MeanMLLR、VarMLLR
    • 针对特征变换:STC(全局特征)、fMLLR(说话人特征)

其中LDA+MLLT训练主要流程如下:

if [ $stage -le -5 ]; then
    echo "$0: Accumulating LDA statistics."
    rm $dir/lda.*.acc 2>/dev/null
    # 根据声学特征和对齐计算LDA统计量
    $cmd JOB=1:$nj $dir/log/lda_acc.JOB.log \
    ali-to-post "ark:gunzip -c $alidir/ali.JOB.gz|" ark:- \| \
      weight-silence-post 0.0 $silphonelist $alidir/final.mdl ark:- ark:- \| \
      acc-lda --rand-prune=$randprune $alidir/final.mdl "$splicedfeats" ark,s,cs:- \
      $dir/lda.JOB.acc || exit 1;
    # 估计LDA矩阵
    est-lda --write-full-matrix=$dir/full.mat --dim=$dim $dir/0.mat $dir/lda.*.acc \
      2>$dir/log/lda_est.log || exit 1;
    rm $dir/lda.*.acc
fi

# ...(决策树聚类过程)

x=1
while [ $x -lt $num_iters ]; do
  echo Training pass $x
  if echo $realign_iters | grep -w $x >/dev/null && [ $stage -le $x ]; then
    echo Aligning data
    mdl="gmm-boost-silence --boost=$boost_silence `cat $lang/phones/optional_silence.csl` $dir/$x.mdl - |"
    # 重新对齐
    $cmd JOB=1:$nj $dir/log/align.$x.JOB.log \
      gmm-align-compiled $scale_opts --beam=$beam --retry-beam=$retry_beam --careful=$careful "$mdl" \
      "ark:gunzip -c $dir/fsts.JOB.gz|" "$feats" \
      "ark:|gzip -c >$dir/ali.JOB.gz" || exit 1;
  fi
  if echo $mllt_iters | grep -w $x >/dev/null; then
    if [ $stage -le $x ]; then
      echo "$0: Estimating MLLT"
      $cmd JOB=1:$nj $dir/log/macc.$x.JOB.log \
        ali-to-post "ark:gunzip -c $dir/ali.JOB.gz|" ark:- \| \
        weight-silence-post 0.0 $silphonelist $dir/$x.mdl ark:- ark:- \| \
        gmm-acc-mllt --rand-prune=$randprune  $dir/$x.mdl "$feats" ark,s,o,cs:- $dir/$x.JOB.macc \
        || exit 1;
      # 计算mllt统计量
      est-mllt $dir/$x.mat.new $dir/$x.*.macc 2> $dir/log/mupdate.$x.log || exit 1;
      gmm-transform-means  $dir/$x.mat.new $dir/$x.mdl $dir/$x.mdl \
        2> $dir/log/transform_means.$x.log || exit 1;
      compose-transforms --print-args=false $dir/$x.mat.new $dir/$cur_lda_iter.mat $dir/$x.mat || exit 1;
      rm $dir/$x.*.macc
    fi
    feats="$splicedfeats transform-feats $dir/$x.mat ark:- ark:- |"
    cur_lda_iter=$x
  fi
  # 参数更新
  if [ $stage -le $x ]; then
    $cmd JOB=1:$nj $dir/log/acc.$x.JOB.log \
      gmm-acc-stats-ali  $dir/$x.mdl "$feats" \
      "ark,s,cs:gunzip -c $dir/ali.JOB.gz|" $dir/$x.JOB.acc || exit 1;
    $cmd $dir/log/update.$x.log \
      gmm-est --write-occs=$dir/$[$x+1].occs --mix-up=$numgauss --power=$power \
        $dir/$x.mdl "gmm-sum-accs - $dir/$x.*.acc |" $dir/$[$x+1].mdl || exit 1;
    rm $dir/$x.mdl $dir/$x.*.acc $dir/$x.occs
  fi
  [ $x -le $max_iter_inc ] && numgauss=$[$numgauss+$incgauss];
  x=$[$x+1];
done

在解码时,如果使用的是全局变换,只用使用训练过程中估计的矩阵进行特征变换。如果是说话人的,则需要在解码时估计,即先使用未变换的特征解码,根据解码的对齐结果估计fMLLR系数。

 类似资料: