因为从语音时域信号中很难找到发音规律,即使是类似的发音,也可能看起来非常不同,因此一般不同直接用于识别。 事实上,我们的耳朵是通过频域而不是波形来辨认声音的,吧时域信号做短时傅里叶变换(Short-time Fourier Transform,STFT),就得到了声音的频谱。我们以帧为单位,根据听觉感知原理,按需调整声音片段频谱中各个片段的赋值,将其参数化,得到适合表示语音信号特性的向量,这就是声学特征(Acoustic Feature)。虽然在端到端语音识别研究中,也出现了一些直接输入语音波形的技术,但是性能和效率均无显著优势,传统提取声学特征的技术,仍然是语音识别的主流。
梅尔频率倒谱系数(Mel-Frequency Cepstral Coefficient,MFCCs)是最常见的声学特征,提取流程如下:
通过设定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
中创建了三个不同的子集:
这些子集分别用于声学模型训练的不同阶段。
特征提取完成后,可通过声学特征表单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的基频提取分两步:
特征变换是指将一帧声学特征经过某种变换,转换为另外一帧特征。输入声学特征是一个TxM的矩阵,输出是一个TxN的矩阵。
常用无监督特征变换技术包括
有监督特征变换最常用形式是将输入乘以一个特征变换矩阵,主要分两大类:
其中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系数。