前记:先说下模型训练的背景。正如一般的机器学习的模型训练那样,首先会用较大的数据集训练生成一个较大的模型,然后在这个模型基础上进行调优,也就是finetune。
我这边基于kaldi的模型训练也是采用这个的思路。Kaldi下面通常是用GMM+Chain的形式进行声学模型训练,然后还要结合语言模型进行训练和解码(这点同端对端的方案是不一样的)。GMM用来做语音序列同文本的对齐,然后再做chain模型的训练,得到声学模型。这可以看作是预训练(pre-training)。Kaldi提供的chain模型训练脚本可以参考egs/libspeech/s5/local/chain/run_tdnn.sh或者egs/wsj/s5/local/chain/run_tdnn.sh。
以上这些实现起来比较容易,不多赘述。后面开始进行finetune的时候就开始遇到问题了。Kaldi中其实提供了两种调参模式:一种叫tune,也就是调整整个训练过程的方案和参数,得到新的模型,这种形式不会基于已有的模型进行,所以相当于就是重新跑一边chain的训练过程,比较耗时。如果去看kaldi的egs下面的脚本,里面出现比如tdnn_7r这样的字段,其中,7r就是对应这种调参方式的不同版本编号。另一种就是finetune,这个就是本文要介绍的方案。
基础模型是用数千小时的语料训练出来的,针对在应用场景下出现的识别问题,又整理了数百小时的数据进行finetune。
对于GMM模型和对齐(主要是对齐结果),可以参照librispeech中的run.sh,与预训练模型的训练一样。
对于Chain模型finetune部分,起初,我参考了egs/rm/s5/local/run/tuning/run_tdnn_wsj_rm_1c.sh,但是发现一个问题(第一个坑),里面的tree文件夹是预先生成的,但是我找不到生成这个tree文件夹的脚本。这里先解释下tree文件夹是干嘛的:chain模型训练实际上是区分性训练,所以有分子和分母,分子采用GMM的声学模型解码所得,分母则是force-aligned的对齐结果。tree文件夹中主要保存的就是对齐结果(.ali)和声学模型的聚类树(tree)。另外,从GMM到chain,聚类的对象从triphone变为biphone,而phone topo从5-state变为1-state,所以这里有一个topo和tree的转换工作要做,然后根据新的topo和tree重新对齐。这个过程就是run_tdnn.sh中steps/nnet3/chain/gen_topo.py, steps/nnet3/chain/build_tree.sh做的事情。回到run_tdnn_wsj_rm_1c.sh,里面的tree文件夹按我的理解(并未实际去验证)应该是做完以上两个操作之后生成的,在这个脚本中并未体现,所以这里对使用者(至少是我)产生了一定困惑。
此时,如果按照run_tdnn.sh中的步骤,重新生成phone topo和tree,那么在之后的chain训练中,就会出现第二个坑:pdf数量不匹配!pdf就是每个分类的概率密度分布,是tree的聚类数量,体现在神经网络里面,就是最后一层的维度。所以,这里出现问题的原因就是重新生成的tree的聚类数量同原来预训练模型的分类数量不匹配。在build_tree.sh,里面生成tree采用的是当前的训练数据,数百小时的和数千小时的数据的聚类结果出现很大差异也是正常的。再看build_tree.sh做的事情,就是两个:生成new tree,以及用生成的new tree重新做对齐:convert-ali。所以,我们可以不用重新生成tree,而是用预训练模型的tree只做重新对齐就可以了。
最后,就可以用steps/nnet3/chain/train.py进行chain模型的训练。在finetune时,需要加载预训练模型作为启动,加载方式分两步:首先,对于声学模型final.mdl的格式,里面存在一些其它信息,作为预训练模型加载时需要转成纯nnet原始数据的格式,可以通过”nnet-copy --raw=true src.mdl tgt.raw”实现;之后,在train.py中,通过--trainer.input_model将模型配置进来。
以上就是我在基于kaldi做声学模型的fintune时遇到的问题和解决方案,可能写的比较乱,下面就把完整步骤整理如下:
1、语言模型与原预训练模型一致
一般在egs/xxx/s5中都有一个run.sh文件,而开头会有一些准备工作,比如生成dict,lang,lm. 这些操作根据不同的训练集可能会得到不同的结果,尤其是phone表!这会影响后续聚类和对齐的问题,所以这些语言模型相关的信息在finetune时,需要与预训练模型保持一致,不要在finetune时进行处理。建议可以生成一个固定的版本,独立维护。
2、特征提取
特征提取一般用mfcc,在GMM训练时就都会做。
在chain训练时,也可以用ivector。
我这边是在GMM时用mfcc,而chain训练时用了ivector。
3、GMM模型训练
输入:finetune的数据集;比如(data/train_for_finetune)
输出:GMM声学模型、基于GMM的强制对齐结果;比如(finetune_tri5a & finetune_tri5a_ali)
4、沿用预训练模型的相关结果
a.预训练模型从final.mdl转成input.raw:nnet-copy --raw=true final.mdl input.raw
PS: 这里说明下,我并没有对预训练模型的神经网络进行任何改变,如果想要做些改变,可以参考下run_tdnn_wsj_rm_1c.sh里面的处理。
b.将预训练模型用于chain训练的tree文件夹(文件一般存放在exp/chain/中,比如:exp/chain/tri5a_tree)中的tree和1.mdl拷贝到当前finetune的tree文件夹中。
PS:只需要这两个就可以了,其它的.ali和.trans都是要重新生成的。
5、生成Chain训练需要的分子分母
- 分子:steps/align_fmllr_lats.sh, 参数具体可以参照run_tdnn.sh
- 分母:steps/nnet3/chain/build_tree.sh, 可以参照run_tdnn.sh的参数配置,但是增加一个配置项:--stage -1,这样就可以不生成new tree,只执行第二步convert-ali。那么这时候的tree哪里来呢,就是将预训练的tree放到build_tree.sh的生成目录中,也就是上面4.b做的事情。举个例子:
steps/nnet3/chain/build_tree.sh --stage -1 --frame-subsampling-factor 3 \
--context-opts "--context-width=2 --central-position=1" \
--cmd "$train_cmd" 7000 data/$train_set/train $lang exp/$train_set""_tri5a_ali $treedir
其中最后一个参数treedir就是本次finetune的chain训练需要的tree所在的目录,我事先生成这个目录,并将预训练中对应文件夹中的tree和1.mdl拷贝到这里,然后在执行build_tree.sh即可。
6、chain训练,等待结果即可。
后记:以上絮絮叨叨写了很多,但难免受制于个人能力,可能会有众多不足和谬误,对于存在的问题欢迎指正;如果我有没说清楚的地方,也欢迎交流。
之所以写篇文章,一方面也是对最近工作的记录总结;另外,发现kaldi中对于finetune的实际操作没有一个比较现成的脚本,所以我在这个过程中踩了很多坑,希望我的经验可以帮助到同行。他山之石可以攻玉。
标签:chain,训练,kaldi,finetune,tree,sh,模型 From: https://www.cnblogs.com/benjaminzhou/p/16796642.html