神经网络的区分性训练 | 文末赠书

从20世纪八十年代到21世纪第一个十年,语音识别基本上被基于隐马尔可夫模型的框架所垄断。在这长达三十年的时间中,主流的语音识别框架都没有太大的变化,这期间也涌现出来不少基于这个框架的优化技巧,有些技巧让语音识别的准确率有了质的提升,这其中的杰出代表便是区分性训练。

区分性训练不但对传统的基于隐马尔可夫模型和高斯混合模型(GMM-HMM)的语音识别系统效果显著,而且对基于隐马尔可夫模型和神经网络的“混合模型”(NN-HMM)语音识别系统也至关重要。接下来将介绍神经网络区分性训练的基本原理和使用方法,以及Kaldi中的纯序列建模神经网络——一种不需要额外生成词格(Lattice-free)便可以对神经网络进行区分性训练的方法。

区分性训练的基本思想

基于隐马尔可夫模型的语音识别系统中最关键的一个环节是隐马尔可夫模型参数的估计,其常用的训练方法是最大似然估计。最大似然估计的基本原理是通过调整模型参数,最大化训练数据在所建立模型上的似然度。

最大似然估计在隐马尔可夫模型参数的训练中非常流行,一是因为在最大似然估计准则下,通过Baum-Welch算法可以快速地收敛到最优模型参数上;二是因为当模型建立的假设成立的时候,最大似然估计从理论上可以保证找到最优的参数。

然而,在一个实用的语音识别系统中,为了简化训练过程,我们做的一些假设并不一定是成立的。在这种情况下,使用最大似然估计方法找到的隐马尔可夫模型的参数就不一定是最优的了。区分性训练提供了参数优化的另外一个思路。不同于最大似然估计调整模型参数来提高训练数据在所建立模型上的似然度,区分性训练通过调整模型参数,尽可能地减少那些容易混淆正确结果和错误结果的情况,从而提高整体模型参数的正确性。

具体到语音识别中,常用的做法是,用当前声学模型的参数(隐马尔可夫模型的参数和神经网络模型的参数),配合以一个比较弱的语言模型(比如二元语法语言模型,甚至一元语法语言模型),将训练数据进行解码并且生成相应的词格。这个解码生成的词格,会和训练数据的强制对齐结果(可以认为是训练数据的标注)一起,输入到抑制错误参数产生的目标函数中,目标函数进而调整参数,抑制那些让训练数据的词格中产生错误结果的参数。需要指出的是,我们使用较弱的语言模型是为了尽可能放大声学模型中不合理的参数,“鼓励”它们在训练数据的词格中引入错误结果,从而可以在区分性训练过程中得到纠正。

区分性训练的目标函数

神经网络的区分性训练方法和传统训练方法一致,都是通过反向传播来实现参数的优化。区别是,传统训练中神经网络一般会以交叉熵作为训练的目标函数,用以最小化训练数据中的帧错误率,而在区分性训练中,神经网络会采用一些特殊的目标函数,用以抑制那些让训练数据在其解码词格中产生错误的参数,从而优化整体模型性能。神经网络的区分性训练常用的目标函数有最大互信息(Maximum Mutual Information,MMI)、增进式最大互信息(Boosted Maximum Mutual Information,BMMI)、最小音素错误(Minimum Phone Error,MPE)及状态级最小贝叶斯风险(state-level Minimum Bayes Risk,sMBR)等。接下来会逐一介绍这些目标函数,首先定义一些符号。

:第个句子中的帧数。

:第个句子中的词数。

:模型参数。

:训练数据。

=:第个句子的声学特征序列。

=:第m个句子的标注文本。

=:和对应的声学状态序列。

目标函数最大互信息(Maximum Mutual Information,MMI)试图最大化观测序列和单词序列分布的互信息,其本质和降低训练数据中的句错误率是非常相关的。最大互信息目标函数公式如下;

目标函数增进式最大互信息(Boosted Maximum Mutual Information,BMMI)是最大互信息的优化版本,其做法是在MMI的基础上,引入一个增进项,用以提升产生错误路径的似然度。增进式最大互信息目标函数公式如下:

其中,便是在最大互信息的基础上引入的增进项,b是一个用来调节增进强度的参数,是序列相对于的准确率,这个准确率可以是基于单词序列的准确率,也可以是基于音素序列的准确率。

目标函数最小音素错误(Minimum Phone Error,MPE)和目标函数状态级最小贝叶斯风险(state-level Minimum Bayes Risk, sMBR)比较相似。顾名思义,前者主要试图减小音素错误率,而后者主要试图减小隐马尔可夫模型中的状态错误率。这两个目标函数的公式可以统一如下:

其中,是序列相对于的准确率。如果这个准确率是基于音素序列计算的,那么对应的目标函数就是最小音素错误;如果这个准确率是基于状态序列计算的,那么对应的目标函数就是状态级最小贝叶斯风险。

区分性训练的实用技巧

区分性训练虽然从公式和理论上来看都非常完美,但是在实际应用中,却是工程性非常强的一个技术点,往往需要反复调整配置,最终才有可能得到较好的效果。本节简单介绍一些可以被调节的配置。

首先,可以被调节的配置是目标函数。神经网络的区分性训练对应了诸多的目标函数,在6.4.2节中介绍的主流的目标函数就多达4种。一般的经验性分析认为,目标函数状态级最小贝叶斯风险会略好于其他的几个目标函数。但是,实际的目标函数选择往往会取决于具体的训练数据和其他的一些配置。建议读者在使用的过程中尝试不同的目标函数。

其次,可以使用的一个技巧叫作帧平滑。研究人员发现,当只使用区分性训练的目标函数时,由于训练数据生成的词格并不能包含所有可能的单词序列,因此神经网络往往会产生过拟合。为了避免过拟合的出现,研究人员尝试将区分性训练的目标函数和交叉熵目标函数进行插值,生成一个新的目标函数来对神经网络进行训练。这个插值也叫作帧平滑。读者如果想要尝试帧平滑,可以按照1:10的比例对交叉熵目标函数和区分性训练目标函数进行插值。

词格的生成对区分性训练的性能影响也非常大。一般认为,我们需要用当前最好的语音识别系统来生成区分性训练所需要的词格。区分性训练所需的词格分成分母词格和分子词格两部分。分子词格往往就是训练数据强制对齐所生成的结果。经验表明,强制对齐越准确,区分性训练效果越佳。分母词格是通过对训练数据进行解码生成的。为了“鼓励”分母词格中产生错误,往往会使用较弱的语言模型,如一元语法语言模型或二元语法语言模型。语言模型的选择和实际的任务相关,因此建议读者在实践中尝试不同的语言模型。同时,也有研究人员提出,经过一遍完整的区分性训练之后,如果重新生成分子词格和分母词格,并且继续训练,则有可能进一步提升模型效果,这个技巧读者也可以尝试。

Kaldi神经网络区分性训练示例

Kaldi中神经网络的区分性训练脚本相对还是比较好理解的。我们以Librispeech为例进行讲解。目前,在Librispeech的默认脚本中,区分性训练是被注释掉的。假设已经按照步骤完整运行了总脚本run.sh并进入到egs/librispeech/s5目录,需要训练一个基础的神经网络模型,以nnet2为例,命令如下:

local/online/run_nnet2_ms.sh

基础神经网络模型训练完成以后,便可以对这个神经网络模型进行区分性训练,命令如下:

local/online/run_nnet2_ms_disc.sh

进一步看一下local/online/run_nnet2_ms_disc.sh脚本。这个脚本的核心有四部分,第一部分的作用是生成6.4.3节中提到的分母词格,即对训练数据进行解码,脚本如下:

if [ $stage -le 1 ]; then
nj=50
num_threads_denlats=6
subsplit=40
steps/nnet2/make_denlats.sh --cmd "$decode_cmd --mem 1G \
--num-threads $num_threads_denlats" \
--online-ivector-dir exp/nnet2_online/ivectors_train_960_hires \
--nj $nj --sub-split $subsplit --num-threads "$num_threads_denlats" \
--config conf/decode.config \
data/train_960_hires data/lang_pp $srcdir ${srcdir}_denlats || exit 1;
fi

第二部分的作用是生成6.4.3节中提到的分子词格,即对训练数据进行强制对齐,脚本如下:

if [ $stage -le 2 ]; then
# hardcode no-GPU for alignment, although you could use GPU [you wouldn't
# get excellent GPU utilization though.]
nj=350
use_gpu=no
gpu_opts=
steps/nnet2/align.sh --cmd "$decode_cmd $gpu_opts" --use-gpu "$use_gpu" \
--online-ivector-dir exp/nnet2_online/ivectors_train_960_hires \
--nj $nj data/train_960_hires data/lang_pp $srcdir ${srcdir}_ali \
|| exit 1;
fi

第三部分的作用是将分子词格和分母词格转换成Kaldi中神经网络训练的样本存档文件,脚本如下:

if [ $stage -le 3 ]; then
# have a higher maximum num-jobs if
if [ -d ${srcdir}_degs/storage ]; then max_jobs=10; else max_jobs=5; fi
steps/nnet2/get_egs_discriminative2.sh \
--cmd "$decode_cmd --max-jobs-run $max_jobs" \
--online-ivector-dir exp/nnet2_online/ivectors_train_960_hires \
--criterion $criterion --drop-frames $drop_frames \
data/train_960_hires data/lang_pp \
${srcdir}{_ali,_denlats,/final.mdl,_degs} || exit 1;
fi

第四部分是对神经网络进行相应的区分性训练,脚本如下:

if [ $stage -le 4 ]; then
steps/nnet2/train_discriminative2.sh --cmd "$decode_cmd $parallel_opts" \
--stage $train_stage \
--effective-lrate $effective_lrate \
--criterion $criterion --drop-frames $drop_frames \
--num-epochs $num_epochs \
--num-jobs-nnet 6 --num-threads $num_threads \
${srcdir}_degs ${srcdir}_${criterion}_${effective_lrate} || exit 1;
fi

建议读者仔细阅读local/online/run_nnet2_ms_disc.sh脚本,或者阅读对应的nnet3版本的local/nnet3/run_tdnn_discriminative.sh脚本,然后根据自己的需求调节相应的参数,如并发处理的CPU核数、区分性训练目标函数选择等。

chain模型

纯序列建模神经网络在Kaldi中又被叫作chain模型,是Kaldi中当前效果最好的神经网络模型。chain模型的开发源于Kaldi对Connectionist Temporal Classification(CTC)的实现。尽管因为数据量等原因,研究人员在Kaldi中实现的CTC网络效果并不理想,但是研究人员意外地发现,CTC实现过程中的一些想法,可以被引用到区分性训练中基于最大互信息目标函数的训练上来,这也就是后来的chain模型。

区别于之前神经网络区分性训练中基于最大互信息的训练,chain模型有如下特点:

—从头开始训练神经网络,不需要基于交叉熵训练的神经网络作为起点;

—采用跳帧技术,每3帧处理一次;

—使用更加简化的隐马尔可夫模型拓扑结构,不训练转移概率;

—不需要产生分母词格,对可能路径进行求和的前向—后向算法(Forward-backward algorithm)演绎过程直接在GPU上进行;

—分母语言模型采用四元语法音素单元的语言模型,而不是一元语法词单元的语言模型;

—使用基于上文双音子(Biphone)的声学建模单元;

—一个训练句子会被拆分成若干个训练块。

简单来理解,读者可以认为chain模型就是一个利用最大互信息目标函数进行训练,但是不需要生成分母词格的区分性训练神经网络模型。事实上,在chain模型中,传统基于最大互信息的区分性训练中的分子词格和分母词格都被有限状态机(Finite State Acceptor,FSA)所代替,并且不同于传统区分性训练中每个句子都有一个单独的分母词格,在chain模型的训练中,所有训练数据将共用一个分母有限状态机。

我们同样用Librispeech示例来对chain模型的使用做一个简单介绍。假设已经按照步骤成功运行了egs/librispeech/s5/run.sh的前19个阶段(stage 19),并且当前处于egs/librispeech/s5目录中,训练chain模型只需执行以下命令:

local/chain/run_tdnn.sh

事实上,目前chain模型是Librispeech默认脚本的一部分,因此如果完整执行egs/librispeech/s5/run.sh,则local/chain/run_tdnn.sh脚本默认会被运行。

进一步观察这个脚本,我们可以发现,这个脚本的核心有四部分。第一部分的作用是提取i-vector说话人信息,作为神经网络输入特征的一部分,本书第8章将介绍i-vector。很多实验已经证明了i-vector可以帮助提高语音识别的效果,脚本如下:

# The iVector-extraction and feature-dumping parts are the same as the standard
# nnet3 setup, and you can skip them by setting "--stage 11" if you have already
# run those things.
local/nnet3/run_ivector_common.sh --stage $stage \
--train-set $train_set \
--gmm $gmm \
--num-threads-ubm 6 --num-processes 3 \
--nnet3-affix "$nnet3_affix" || exit 1;

上述脚本会根据已有训练数据,训练一个i-vector提取器,并对训练数据和测试数据进行i-vector特征提取,方便后续训练过程和测试过程使用。在Librispeech示例中,所生成的i-vector提取器位于exp/nnet3_cleaned/extractor目录,而训练数据、开发数据和测试数据所提取的i-vector位于:

exp/nnet3_cleaned/ivectors_train_960_cleaned_sp_hires
exp/nnet3_cleaned/ivectors_{dev_clean,dev_other,test_clean,test_other}_hires

第二部分主要在做一些chain模型训练的准备工作,按照其内容又可以分成三大块。

第二部分中的第一大块是生成一个新的语言目录,包含chain模型所特有的隐马尔可夫模型拓扑结构。不同于传统语音识别系统中隐马尔可夫模型的拓扑结构,chain模型使用了一种非常简化的拓扑结构,最少用一帧语音数据就可以遍历,其可以产生的第一帧的数据标签和后续帧的数据标签不同,比如可以产生类似于“a,ab,abb,abbb”这样的标签序列。在Librispeech中,这个新生成的语言目录位于data/lang_chain。

第二部分中的第二大块是对训练数据进行强制对齐,用于生成训练数据词格,以待之后进一步转换成chain模型训练所需的分子有限状态机。值得一提的是,为了更好地处理多音词的情况,这部分对齐结果事实上是通过对每个训练语句的标注文本生成解码图,然后对训练音频利用其相应的解码图进行解码所获得的。一个训练音频的对齐结果可以包含多条路径,而不单单是传统对齐结果中的最佳路径。第二大块中的对齐结果存放于lat.*.gz这样的文件中(它们事实上就是词格),在Librispeech示例中,其目录位于exp/chain_cleaned/tri6b_cleaned_train_960_cleaned_sp_lats。

第二部分中第三大块的主要作用是生成chain模型训练所特需的决策树。首先,在上面的第一大块中提到,chain模型训练使用了特殊的隐马尔可夫模型拓扑结构,因此需要根据这个拓扑结构重新构建决策树。其次,在chain模型决策树构建的过程中,使用了基于上文相关的双音子的决策树,而非传统的上下文相关的三音子决策树。最后,由于chain模型训练过程中做了降帧处理(默认参数为每3帧处理一次),因此还需要把降帧的概念引入决策树的构建过程中。值得一提的是,在构建新的决策树的过程中,第三大块还会把原始的训练数据对齐结果根据新的决策树转换成新的对齐结果,这部分对齐结果之后会被用来生成chain模型中用到的音素语言模型。在Librispeech示例中,重新生成的决策树位于exp/chain_cleaned/tree_sp目录。

第二部分具体训练脚本如下,由于每一大块的过程相对比较简单,因此不再对脚本做进一步展开:

# Please take this as a reference on how to specify all the options of
# local/chain/run_chain_common.sh
local/chain/run_chain_common.sh --stage $stage \
--gmm-dir $gmm_dir \
--ali-dir $ali_dir \
--lores-train-data-dir ${lores_train_data_dir} \
--lang $lang \
--lat-dir $lat_dir \
--num-leaves 7000 \
--tree-dir $tree_dir || exit 1;

第三部分的核心内容是模型结构的定义,Librispeech中的chain模型利用Kaldi中的nnet3神经网络训练框架来实现。关于nnet3模型结构的定义,在6.2.3节中已经有非常详细的定义,这里不再做详细阐述。简单来看,chain模型结构的定义和nnet3模型结构的定义一样,分为两部分:第一部分是Kaldi建议用户直接修改的部分,是用户比较容易读懂的模型结构描述,在Kaldi中也称作xconfig;第二部分是Kaldi训练过程中真正使用的config,一般由steps/nnet3/xconfig_to_configs.py脚本根据用户定义的xconfig产生。在Librispeech的chain模型中,模型结构对应的xconfig及相应的config文件生成代码如下:

if [ $stage -le 14 ]; then
echo "$0: creating neural net configs using the xconfig parser";
num_targets=$(tree-info $tree_dir/tree | grep num-pdfs | awk '{print $2}')
learning_rate_factor=$(echo "print (0.5/$xent_regularize)" | python)
affine_opts="l2-regularize=0.008 dropout-proportion=0.0 dropout-per-dim=true dropout-per-dim-continuous=true"
tdnnf_opts="l2-regularize=0.008 dropout-proportion=0.0 bypass-scale=0.75"
linear_opts="l2-regularize=0.008 orthonormal-constraint=-1.0"
prefinal_opts="l2-regularize=0.008"
output_opts="l2-regularize=0.002"
mkdir -p $dir/configs
cat input dim=100 name=ivector
input dim=40 name=input
# please note that it is important to have input layer with the name=input
# as the layer immediately preceding the fixed-affine-layer to enable
# the use of short notation for the descriptor
fixed-affine-layer name=lda input=Append(-1,0,1,ReplaceIndex(ivector, t, 0)) affine-transform-file=$dir/configs/lda.mat
# the first splicing is moved before the lda layer, so no splicing here
relu-batchnorm-dropout-layer name=tdnn1 $affine_opts dim=1536
tdnnf-layer name=tdnnf2 $tdnnf_opts dim=1536 bottleneck-dim=160 time-stride=1
... (此处略过若干层定义)
tdnnf-layer name=tdnnf16 $tdnnf_opts dim=1536 bottleneck-dim=160 time-stride=3
tdnnf-layer name=tdnnf17 $tdnnf_opts dim=1536 bottleneck-dim=160 time-stride=3
linear-component name=prefinal-l dim=256 $linear_opts
prefinal-layer name=prefinal-chain input=prefinal-l $prefinal_opts big-dim=1536 small-dim=256
output-layer name=output include-log-softmax=false dim=$num_targets $output_opts
prefinal-layer name=prefinal-xent input=prefinal-l $prefinal_opts big-dim=1536 small-dim=256
output-layer name=output-xent dim=$num_targets learning-rate-factor=$learning_rate_factor $output_opts
EOF
steps/nnet3/xconfig_to_configs.py --xconfig-file $dir/configs/network.xconfig --config-dir $dir/configs/
fi

在上述代码块中可以看到,首先定义了xconfig生成过程中用到的一些参数,如神经网络输出数目num_targets、学习速率调节比例learning_rate_factor,以及网络中特定层相关参数affine_opts、tdnnf_opts、linear_opts、prefinal_opts、output_opts等;然后定义了具体的xconfig内容,并且写到了exp/chain_cleaned/tdnn_1d_sp/ configs/network.xconfig文件中。network.xconfig文件中的模型定义比较简单易懂,在上述示例中,network.xconfig中定义的主要网络层如下:

—输入层ivector,100维ivector特征;

—输入层input,40维MFCC特征;

—固定仿射变换层lda,用LDA矩阵做特征变换,并且用Append描述符定义如何拼接ivector特征和MFCC特征。在本例中由于Append用到的相邻帧是-1、0、1,因此LDA矩阵的实际维度是100 + 40 ´3 = 220;

—TDNN相关层tdnn1、tdnnf2……tdnnf17。注意,tdnn1因为要连接输入特征,所以和其他层略有区别;

—输出相关层prefinal-chain、output、prefinal-xent和output-xent。

值得一提的是,为了防止过拟合,chain模型训练过程中同时用到了交叉熵损失函数和chain自身的损失函数(基于最大互信息),这就是为什么在xconfig中看到了两个输出层。生成了network.xconfig文件之后,代码块最后利用steps/nnet3/xconfig_to_configs.py脚本,生成了chain模型训练所需的配置文件,并置于exp/chain_cleaned/tdnn_1d_sp/configs文件夹中,核心的文件包括init.config、ref.config、final.config、init.raw、ref.config、vars等。其中,init.config是初始网络结构;final.config是最终网络结构;ref.config和final.config基本一致,但是final.config中用到固定矩阵、向量变换的地方(如LDA变换),在ref.config中都由随机初始化的矩阵或向量来代替实际的矩阵或向量;init.raw和ref.raw是分别对应于init.config和ref.config的网络模型;而vars文件中记录了网络所使用的上下文。steps/nnet3/xconfig_to_configs.py也会在相同目录下面生成一些中间的文件,如network.config的备份文件xconfig、中间过程文件xconfig.expanded.1和xconfig.expanded.2等,读者也可以自行查看。

第四部分就是chain模型的具体训练了,通过steps/nnet3/chain/train.py脚本来实现,具体代码如下:

if [ $stage -le 15 ]; then
if [[ $(hostname -f) == *.clsp.jhu.edu ]] && [ ! -d $dir/egs/storage ]; then
utils/create_split_dir.pl \
/export/b{09,10,11,12}/$USER/kaldi-data/egs/swbd-$(date +'%m_%d_%H_%M')/s5c/$dir/egs/storage $dir/egs/storage
fi
steps/nnet3/chain/train.py --stage $train_stage \
--cmd "$decode_cmd" \
--feat.online-ivector-dir $train_ivector_dir \
--feat.cmvn-opts "--norm-means=false --norm-vars=false" \
--chain.xent-regularize $xent_regularize \
--chain.leaky-hmm-coefficient 0.1 \
--chain.l2-regularize 0.0 \
--chain.apply-deriv-weights false \
--chain.lm-opts="--num-extra-lm-states=2000" \
--egs.dir "$common_egs_dir" \
--egs.stage $get_egs_stage \
--egs.opts "--frames-overlap-per-eg 0 --constrained false" \
--egs.chunk-width $frames_per_eg \
--trainer.dropout-schedule $dropout_schedule \
--trainer.add-option="--optimization.memory-compression-level=2" \
--trainer.num-chunk-per-minibatch 64 \
--trainer.frames-per-iter 2500000 \
--trainer.num-epochs 4 \
--trainer.optimization.num-jobs-initial 3 \
--trainer.optimization.num-jobs-final 16 \
--trainer.optimization.initial-effective-lrate 0.00015 \
--trainer.optimization.final-effective-lrate 0.000015 \
--trainer.max-param-change 2.0 \
--cleanup.remove-egs $remove_egs \
--feat-dir $train_data_dir \
--tree-dir $tree_dir \
--lat-dir $lat_dir \
--dir $dir || exit 1;
fi

由于Kaldi的神经网络训练过程需要把训练样本存档写到磁盘,而样本存档往往占用大量磁盘空间,在训练过程中会给服务器读写操作造成巨大压力,因此Kaldi往往先使用utils/create_split_dir.pl脚本,把样本存档分散到不同的机器上。在上述示例中,样本存档被分散到了b09、b10、b11、b12这四台机器上。

做好样本存档的存储空间准备工作之后,就可以调用steps/nnet3/chain/train.py脚本,开始chain模型的训练。训练脚本的核心可以分成两大部分,即训练样本存档的生成和具体模型的训练。需要指出的是,train.py虽然是一个Python脚本,但是历史上Kaldi本身的训练过程是由大量的C++可执行文件和Bash脚本构成的,因此上述train.py脚本中不可避免地大量调用了C++可执行文件和Bash脚本。当然,Python的使用还是为训练脚本增加了很多可读性。在具体操作上,train.py通过steps/libs/nnet3/train/chain_objf/acoustic_model.py脚本对核心的C++可执行文件和Bash脚本进行了封装,并且以chain_lib库的形式引入到模型训练中。为了让读者对chain模型的训练过程有更好的了解,接下来简单介绍chain模型的样本存档生成和具体模型训练。

前面已经介绍了chain模型的基本原理。类似于基于最大互信息的区分性训练,chain模型训练样本存档的生成依赖于分子有限状态机和分母有限状态机的生成。在分母有限状态机方面,区别于传统最大互信息的区分性训练,chain模型利用训练数据的强制对齐结果,训练了一个四元语法音素单元的语言模型,并将它转换为有限状态机,具体生成的文件是exp/chain_cleaned/tdnn_1d_sp/phone_lm.fst。该过程通过调用chain_lib库中的create_phone_lm()函数来完成,其输入依赖是决策树目录中的训练数据强制对齐结果,也即exp/chain_cleaned/tree_sp/ali.*.gz文件。生成音素语言模型之后,训练脚本进一步将其和C有限状态转换器和H有限状态转换器结合,形成最终的分母有限状态机。这个过程通过chain_lib库中的create_denominator_fst()函数来实现,其生成的文件是exp/chain_cleaned/tdnn_1d_sp/den.fst和exp/chain_cleaned/ tdnn_1d_sp/normalization.fst,其中den.fst就是分母有限状态机,而normalization.fst在den.fst的基础上修改了初始状态和终止概率(由于chain模型训练过程使用的是句子片段而不是完整的句子,而音素语言模型产生的有限状态机的初始状态和终止概率本身包含了句子开始和结束的统计信息,因此需要做此修改),训练过程中实际使用的是normalization.fst有限状态机。至此,分母有限状态机准备完毕。在分子有限状态机方面,我们在介绍local/chain/run_chain_common.sh脚本的时候已经提到,分子有限状态机通过对训练数据做强制对齐而产生,并且保存于exp/chain_cleaned/tri6b_cleaned_train_960_cleaned_sp_lats/lat.*.gz文件中,因此,这个时候分子有限状态机也已经准备妥当。分子有限状态机和分母有限状态机均准备就绪以后,就可以调用chain_lib中的generate_chain_egs()函数,生成训所需的样本存档。生成训练所需的样本存档之后,可以调用chain_lib中的prepare_initial_acoustic_model()函数生成初始的模型,然后通过train_one_iteration()函数对模型进行训练,模型训练最后阶段还可能通过combine_models()函数对最后的一些模型进行融合,这些步骤都和Kaldi中传统的神经网络训练一致,这里不再展开赘述。

至此,我们介绍了chain模型的基本概念,以及如何在Librispeech示例中训练一个chain模型。建议读者仿照Librispeech训练的第一个chain模型,将相应的配置修改到自己的数据集上,训练自己的chain模型。

作者介绍

本文节选自电子工业出版社博文视点出版的《Kaldi语音识别实战》,由陈果果等著。

文末福利

AI 前线联合博文视点Broadview为粉丝送出《Kaldi语音识别实战》纸质书籍 5 本!长按识别下图小程序,参与抽奖活动,由小程序随机抽出 5 位,每人赠送一本书。开奖时间:6 月 19 日(周五)18:00,获奖者每人获得一本。另附购买地址,请戳「阅读原文」

雪球转发:0回复:0喜欢:0