【Kaldi】train_mono.sh源码阅读

这个脚本会训练一个mono phone模型。

整体结构

除去数据准备等命令,我们逐行解析里面的指令。首先还是了解一下脚本的大致情况:

  • 前30行,定义训练超参数
  • 30~49行,定义个解析该脚本的命令行参数
  • 51~65行,准备数据
  • 67~110行,初始化训练
  • 112行之后,后续训练

初始化训练

gmm-init-mono

80行的gmm-init-mono指令:

1
2
gmm-init-mono $shared_phones_opt "--train-feats=$feats subset-feats --n=10 ark:- ark:-|" $lang/topo $feat_dim \
$dir/0.mdl $dir/tree || exit 1;

这个指令用于初始化mono phone的GMM,它的一般用法为:

1
gmm-init-mono <topology-in> <dim> <model-out> <tree-out>

这个指令可以参考之前的文章 ==> 【Kaldi】gmm-init-mono源码阅读

ark文件

注意到“ark”字样频繁出现,在kaldi中,ark文件是一种数据存储文件,比如常用的mfcc就会用ark存储。ark文件可以用kaldi目录下的src/featbin/copy-feats查看,一般用法为:

1
copy-feats [options] <feature-rspecifier> <feature-wspecifier>

<feature-rspecifier>表示读取文件,<feature-wspecifier>表示写入文件,比如要查看一个二进制的abc.ark文件可以用:

1
copy-feats ark:./abc.ark ark,t:target.ark

其中ark,t表示用文本形式展现,如果不加上这个指令那么默认采用二进制格式。target.ark可以为空,这个使用copy-feats会把数据打印到终端。

compile-train-graphs

90行的compile-train-graphs指令:

1
2
3
compile-train-graphs --read-disambig-syms=$lang/phones/disambig.int $dir/tree $dir/0.mdl  $lang/L.fst \
"ark:sym2int.pl --map-oov $oov_sym -f 2- $lang/words.txt < $sdata/JOB/text|" \
"ark:|gzip -c >$dir/fsts.JOB.gz" || exit 1;

这个指令用于编译训练图,一般用法为:

1
compile-train-graphs [options] <tree-in> <model-in> <lexicon-fst-in> <transcriptions-rspecifier> <graphs-wspecifier>

align-equal-compiled和gmm-acc-stats-ali

98行的align-equal-compiled和99行的gmm-acc-stats-ali指令:

1
2
3
align-equal-compiled "ark:gunzip -c $dir/fsts.JOB.gz|" "$feats" ark,t:-  \| \
gmm-acc-stats-ali --binary=true $dir/0.mdl "$feats" ark:- \
$dir/0.JOB.acc || exit 1;

首先align-equal-compiled的一般用法为:

1
align-equal-compiled <graphs-rspecifier> <features-rspecifier> <alignments-wspecifier>

这个指令用于产生最简单的对齐方式,即假设每个状态持续时间相同,这种方式便于初始化GMM的初步训练,单后续的训练一定会使用这种方式。

接着gmm-acc-stats-ali将读取初始化模型0.mdl、特征$feats以及对齐结果,输出用于更新GMM参数的0.JOB.acc文件。其一般用法如下:

1
gmm-acc-stats [options] <model-in> <feature-rspecifier> <posteriors-rspecifier> <stats-out>

gmm-est

107行的gmm-est指令:

1
2
gmm-est --min-gaussian-occupancy=3  --mix-up=$numgauss --power=$power \
$dir/0.mdl "gmm-sum-accs - $dir/0.*.acc|" $dir/1.mdl 2> $dir/log/update.0.log || exit 1;

这段代码会使用初始化模型0.mdl和之前的gmm-acc-stats-ali的输出更新参数后的GMM 1.mdl。另外,先前的0.mdl的GMM只有一个分量,而从1到多GMM的操作也是在这个指令中完成的,通过扰动1个高斯分量的均值,把1个高斯分量分裂为2个,作为下次迭代的基础。

后续训练

经过上面的初步训练,我们得到了1.mdl,但为了得到更精确的模型,还需要多轮迭代,从112行到140行的代码做的就是这个事。我们可以把上面的代码简化一下,省略打印日志等细节,只保留逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
beam=$initial_beam # will change to regular_beam below after 1st pass
x=1
while [ $x 不超过最大迭代次数 ]; do
  if [ $stage -le $x ]; then
    if 当前迭代次数允许对齐; then
      使用gmm-align-compiled重新生成对齐结果
    fi
    使用gmm-acc-stats-ali统计并得到$x.mdl的更新参数
    使用gmm-est更新参数(可能分裂高斯分量)
    删除上一次迭代过程中产生的无用中间文件
  fi
  if [ $x 不超过一个阈值 ]; then
     提升GMM中分量个数的上限
  fi
  beam=$regular_beam
  x=$[$x+1]
done

这段代码的总体流程和之前的第1次迭代类似,不过生成对齐数据时第一次迭代会将beam设置为$initial_beam(在timit中是6),之后就会变成$regular_beam(timit中是10),beam的数值越大对齐结果越准确,但是时间消耗也越大,所以生成对齐数据的操作并不是在每一次迭代中都有的。其余的部分和初始化迭代类似,无非是使用EM算法获取更新GMM的参数、更新GMM,总之每次迭代训练GMM都会用到gmm-acc-stats-aligmm-est