ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

kaldi入门详解——nnet3实现tdnn

2019-08-31 21:38:56  阅读:7090  来源: 互联网

标签:ark num 训练 egs kaldi train tdnn nnet3


aishell/s5为例

sets.txt
[外链图片转存失败(img-rqSwSxab-1567257066235)(en-resource://database/2684:1)]
这里在构建决策树,初始把所有音素,每个音素的每个状态作为一颗决策树,这里把i1,i2,i3,i4绑在一块,作为i,只建立一颗决策树。

因此我们能看见 ,transition-states的个数大于pdfs的个数,就是因为i1,i2里有的pdf是相同的(有用同一个pdf,但是tid还是分开的)
[外链图片转存失败(img-eMzayiEu-1567257066236)(en-resource://database/2686:1)]


  • objf:目标函数(目标函数可以用均方误差MSE(L=(yf(x))2L=(y-f(x))^2L=(y−f(x))2)、交叉熵 )

  • VTLN:特征级声道长度归一化

  • cmvn:倒谱均值和方差归一化

  • DNN特征:统一用 fbank,不加 pitch

nnet3

Dan’s setup uses a fixed number of epochs and averages the parameters over the last few epochs of training.

nnet3的数据结构:

参考:Kaldi nnet3 -------- Data Type

索引Index:
是一个元组 (n t x),
其中 n 是该在 minibatch的索引 ,t 是指时间索引,x 是一个占位符索引,以供将来使用,通常会是零。

作为索引的一个例子:如果我们训练非常简单的前馈网络,索引可能只在“n”维度变化,我们可以任意“t”值设置为0,所以索引如下
[ (0, 0, 0) (1, 0, 0) (2, 0, 0) … ]

另一方面,如果我们使用相同类型的网络解码一个句子,索引只会在“t”维度不同,所以我们会有
[ (0, 0, 0) (0, 1, 0) (0, 2, 0) … ]

对应于矩阵的行。在网络使用时间信息的情况下,在早期的层我们在训练时需要不同的“t”值,所以我们可能会遇到在“n”和“t”都不同的索引列表。如:
[ (0, -1,0) (0, 0, 0) (0, 1, 0) (1, -1, 0) (1, 0, 0) (1, 1, 0) …]

索引结构体Index 有默认的排序操作,默认由n来排序,然后t,那么x,所以通常我们也按上面排序。当你看到代码打印向量索引,你会经常看到他们是紧凑的形式,其中在“x”索引省略(如果零),和“t”的范围值页是紧凑表示,因此上述向量可能写成
[ (0, -1:1) (1,-1:1) … ]

索引Cindexes:

Cindex 是一对(int32,Index),其中int32对应神经网络中的一个节点的索引。
一个Cindex除了告诉矩阵哪一行,也告诉我们哪个矩阵,。例如,假设图中有一个节点叫做“affine1”,输出维度1000和节点列表编号为2,在Cindex (2,(0,0,0))相当于列维度1000的矩阵的某一行,这将被分配为“affine1”组件的输出。

Nnet3配置中的上下文和块大小

参考:Context and chunk-size in the “nnet3” setup chunksNnet3配置中的上下文和块大小

chunk:就是拼帧后的输入。每帧特征,其实比如是5帧一块输入。

chunk size

Chunk的大小是我们在训练或解码中每个数据块所含(输出)帧的数量。在get_egs.sh和train_dnn.py脚本中,chunk-size的也被称为frames-per-eg(在某些上下文中,这与块大小不同;见下文)。在解码中,我们把它称为frame-per-chunk。

当训练TDNN时,每帧都需要左右各10帧的上文和下文,并写入到磁盘中,就必须知道某一帧的左右上下文具体是哪些帧,并记录。

然而,不使用chunk,即以普通的方法生成训练样本时,所需的数据量可能会变成原来的20倍:

8帧,总共需要160帧的左右上下文

[-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

[-9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11]

[-8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ,12]

[-7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]

[-6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

[-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

[-4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]

[-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]

为了解决这个问题,将

  • 某个时间范围内的帧(大小由frame-per-eg控制,默认为8)
  • 对应的标签
  • 左上下文
  • 右上下文

组合为一个块,即chunk,使得这些帧能共享左右上下文帧:

8帧,总共需要20帧的左右上下文

chunk与egs的区别:

chunk:[-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]

eg:[0, 1, 2, 3, 4, 5, 6, 7]

即:chunk是显式包含左右上下文帧的eg

当训练模型时,将以chunk为单位。

TDNN解码:

TDNN解码时,输入第一帧时,需要10帧左上文和10帧右下文。而第一帧是没有左上文的,因此,在输入层处对第一帧拷贝10次,作为其左上文;而10帧的右下文还没到来,因此无法输出第一帧的输出,需要等到第11帧到来时,才能输出第1帧,等到第12帧到来时,输出第2帧。

因此,时延为11* 0.01=0.11秒,且只与右下文有关。

输出到最后一帧时,没有右下文了,将最后一帧拷贝10次,作为其右下文

不需要chunk,直接对整个语句进行解码输出。

minibatch大小(n)

Minibatch大小nnet3-merge-egs将各个训练样本合并到包含许多不同样本的minibatch中(每个原始样本获得不同的’n’索引)。 minibatch-size是minibatch的大小,指的是:将多个样本的帧以及label组合为一个样本,即eg的数量

这里的样本不是很多句不同的语音,还是一句话,但分了几个片段,每个片段(eg)是一个minibatch,所以每帧就是一个样本n?

minibatch是以帧为单位,如64。

假设chunk-width=20,那么一个minibatch将横跨3.2个chunk。短句的时长一般为3秒~4秒,设为3秒,设帧移为10ms,则1秒包含1000/10=100帧,一个短句包含300帧,如果minibatch=64,那么一句话被切分为4个minibatch=64* 4=256帧,尾部的44帧被丢弃。

num_jobs_initial=2      #   开始时候的gpu个数,这里一开始用2个gpu。
num_jobs_final=12       #   最后用的gpu个数,训练中根据某个增加规则,慢慢增加gpu个数。
# 每个job,就是每用一个gpu,就训练一批数据,并生成一个新的模型,一开始只训练两个模型,逐渐增加训练模型个数,因为一开始可能会面临模型分散的问题,后面逐渐不会有这个问题,因此可以把模型数目增加。

$num_iters*$avg_num_jobs) == $num_epochs*$num_archives

avg_num_jobs=(num_jobs_initial+num_jobs_final)/2.

num_archives_expanded = num_archives * args.frames_per_eg

num_archives_to_process = int(args.num_epochs * num_archives_expanded)

num_archives_processed = 0

num_iters = int(num_archives_to_process * 2 / (args.num_jobs_initial + args.num_jobs_final)

network.xconfig:

  # the first splicing is moved before the lda layer, so no splicing here
  relu-batchnorm-layer name=tdnn1 dim=850
  relu-batchnorm-layer name=tdnn2 dim=850 input=Append(-1,0,2)
  relu-batchnorm-layer name=tdnn3 dim=850 input=Append(-3,0,3)
  relu-batchnorm-layer name=tdnn4 dim=850 input=Append(-7,0,2)  # 拼帧操作,只拼左第7帧和右第2帧,所以只拼了三帧。
  relu-batchnorm-layer name=tdnn5 dim=850 input=Append(-3,0,3)  
  output-layer name=output input=tdnn6 dim=$num_targets max-change=1.5

#将 网络配置转换为 nnet3 网络配置文件
steps/nnet3/xconfig_to_configs.py --xconfig-file $dir/configs/network.xconfig --config-dir $dir/configs/
# xconfig要转换为后面可读的配置文件形式,通过xconfig_to_configs.py 进行转换,生成final.config文件,供后续读取,因此也可以直接修改final.config文件。

最后一层在进入softxmax之前,是 f (wx+b) ,而不是 wx+b 去经过softmax,因为wx+b是线性变换,线性变换没意义,要经过激活函数才是非线性变换,softmax最后只是为了分类。

nnet3-copy

nnet3-copy --binary=false 0.raw text.raw
把二进制的raw写成可以text输出。
这里头的.raw是只保存了网络(每层的神经元,权重偏置),而.mdl里还有转移概率。.raw里没有转移概率。

nnet3-copy-egs,等一系列 copy 操作,可以理解为,读取文件,供后续操作,把文件读取进来,再给后面步骤。

把网络结构打印出来:用pdf看图

steps/nnet3/nnet3_to_dot.sh

 # dir=exp_sil/plp/chain/nnet3_tdnn_8b_relu_train_9000_sub2000
# component_attributes="name,type,input-dim,output-dim"
# steps/nnet3/nnet3_to_dot.sh --component-attributes "$component_attributes" $dir/100.mdl temp.dot chain.pdf`

[外链图片转存失败(img-Y75C8pes-1567257066237)(en-resource://database/2562:1)]

先拼帧append作为输入。

affine:做仿射变换,就是经过权重矩阵 Wx+b

再经过激活函数

再经过batchnorm BN层做归一化。

configs/final.config:
[外链图片转存失败(img-gzgqkLN1-1567257066238)(en-resource://database/3120:1)]
这里batchnorm完输出是850,下一层怎么输入是2550呢,因为做了拼帧,这里拼了3帧,850* 3 = 2550,输入为拼帧后的维度。


看nnet2流程,比较详细,但也有部分与实际代码不同:egs/wsj/s5/steps/nnet2/train_pnorm.sh

调用 steps/nnet3/get_egs.sh 进行构建样本数据
get_egs:把输入特征分块,供后续读取。

local/nnet3/run_ivector_common.sh
utils/data/perturb_data_dir_speed_3way.sh 对音频做速度扰动
这个操作是扩充数据量,对原音频做速度* 1.1倍,0.9倍的增速/减速操作,数据量扩充三倍,再对做了变速的音频提取特征。

之后提取ivector特征。(把这步去掉)

data/train_sp_hires:43维特征

with open('{0}/num_jobs'.format(args.dir), 'w') as f:
         f.write('{}'.format(num_jobs))
# 写成文件保存在exp/nnet3/tdnn_sp/目录下,比如这里的文件名叫num_jobs

egs

get_egs.sh

生成egs样本: steps/nnet3/get_egs.sh:

  • 有多少archives:
    num_archives=$[$num_frames/($frames_per_eg_principal*$samples_per_iter)+1]
    (因此samples_per_iter为每个egs的行数(frames_per_eg_principal是每行帧数))

archives与job数无关的,archives数=总帧数/(frame_per_eg* 默认samples_per_iter)

  • 每个archive有多少帧:egs_per_archive=$[$num_frames/($frames_per_eg_principal*$num_archives)]

job数要大于egs数
如果实在小了,可以改 train_dnn.py --trainer.samples-per-iter 数(通过train_dnn.py -h可知,默认是40000),把samples-per-iter改小,让egs数增大。

nnet3-copy-egs

nnet3-copy-egs ark:exp/nnet3/tdnn_sp/egs/combine.egs ark,t:text.egs

nnet3-copy-egs:读入egs,供后续操作。如果不用nnet3-copy-egs,系统无法直接读egs格式文件。

<> </>是一种类似html网页前端的写法,把中间的包起来。
这里Num Io为3,说明有三个IO结构(分别是input,ivector,label)

Nnet Io为input,input即特征,从 exp/nnet3/tdnn_sp/configs/vars 可知,
model_left_context=16
model_right_context=12
因此左边拼帧16个,右边12个。

由 exp/nnet3/tdnn_sp/egs/info/frames_per_eg 可知,为8。即input为28+8=36。
因此egs的结构为: -1607~19

这里的input就是特征,因此有36行,每行代表一个43维的特征帧。每37行代表0-7帧,下一个37行是下一个0-7帧。

36行之后,又有一个nnet io结构,这里是ivector,一个100维的ivector。

之后又有一个nnet io结构,这里是output,就是label(pdf),对应0~7帧(由frames-per-eg决定)的label,可以看出,这里的pdf是73,73,73,73,68,68,68,68(一共有3048个类)

sp1.1-BAC009S0093W0133-54 <Nnet3Eg> <NumIo> 3 <NnetIo> input <I1V> 36 <I1> 0 -16 0 <I1> 0 -15 0 <I1> 0 -14 0 <I1> 0 -13 0 <I1> 0 -12 0 <I1> 0 -11 0 <I1> 0 -10 0 <I1> 0 -9 0 <I1> 0 -8 0 <I1> 0 -7 0 <I1> 0 -6 0 <I1> 0 -5 0 <I1> 0       -4 0 <I1> 0 -3 0 <I1> 0 -2 0 <I1> 0 -1 0 <I1> 0 0 0 <I1> 0 1 0 <I1> 0 2 0 <I1> 0 3 0 <I1> 0 4 0 <I1> 0 5 0 <I1> 0 6 0 <I1> 0 7 0 <I1> 0 8 0 <I1> 0 9 0 <I1> 0 10 0 <I1> 0 11 0 <I1> 0 12 0 <I1> 0 13 0 <I1> 0 14 0 <I1> 0 15 0 <I1>       0 16 0 <I1> 0 17 0 <I1> 0 18 0 <I1> 0 19 0  [
    2   58.1462 -16.71331 -3.481852 -2.037196 16.62354 5.209322 -2.611221 6.887256 -2.900389 8.712128 4.888224 -2.270636 0.1796503 11.02032 9.399782 -0.3942706 2.574737 -0.92206 0.9272239 -1.551546 -3.168484 -1.394178 0.1337952 -0.167      8659 -0.4507127 0.1205595 -2.324222 -1.278793 0.7736654 2.112495 -1.612445 1.374034 1.197014 3.466681 0.8661218 -1.12272 1.09042 1.143164 -2.249245 4.392267 0.04311262 -0.2133515 0.09222194
    3   58.28896 -20.30789 -0.8382912 4.231624 8.744699 8.690948 -3.211851 4.007006 1.659628 0.96556 23.54519 10.25486 -3.040819 12.04378 6.125127 -14.51103 -11.2128 -7.926831 -5.885276 3.343732 2.080305 3.610453 0.3568268 -0.1344005       0.1886809 -0.7712536 -1.280014 -2.730994 -1.910352 1.954276 2.549769 3.325892 4.496462 5.637393 2.080765 -1.573948 2.512764 -0.9002939 -1.197941 0.2535739 -0.1040649 -0.2133515 -0.12233
................
</NnetIo> <NnetIo> ivector <I1V> 1 <I1> 0 0 0  [
  -0.5305816 1.538003 -0.9305822 0.4371424 -0.5148872 -0.7118077 1.652424 -0.2992517 -0.6875259 0.03755641 0.9156742 1.017125 1.162166 -0.4764507 0.6428876 0.9080343 -0.5324175 1.237795 -0.1231189 0.7359288 -0.1426629 1.186329 0.4315162 0.9228404 -0.655367 -1.345033 -0.1296335 -1.007396 -0.2306701 0.3371127 -1.998573 0.6014304 -0.6337502 1.404274 0.8053987 1.882688 -0.4076914 -0.4952247 -0.01675212 -0.004137278 -1.369197 0.3291175 -0.003485918 -0.3181442 0.5315459 -0.2948099 0.2926354 0.184788 0.3029404 0.001015186 0.2149332 -0.5604306 0.388164 0.2270741 0.523906 0.66101 -0.2441732 0.3215368 0.2906218 0.4189606 -0.3424854 0.08582425 0.5393636 -0.0316174 -0.002182961 0.8417625 0.6025558 -0.02208233 0.4656293 -0.0909009 0.3782144 0.105546 -0.8044345 1.175195 1.634953 -0.7527317 -0.5289825 0.867229 -0.5897466 -0.7349645 0.6145191 -0.9760071 -0.48261 0.5750756 -0.1214606 -0.8742598 0.04276824 0.4206781 -0.1094973 -0.3552779 0.3021111 -0.9126964 -0.4534717 0.4071751 -0.06122947 -0.3020945 -0.2818398 -0.5227047 0.05360627 -0.2737854 ]
</NnetIo> <NnetIo> output <I1V> 8 <I1> 0 0 0 <I1> 0 1 0 <I1> 0 2 0 <I1> 0 3 0 <I1> 0 4 0 <I1> 0 5 0 <I1> 0 6 0 <I1> 0 7 0 rows=8 dim=3048 [ 73 1 ] dim=3048 [ 73 1 ] dim=3048 [ 73 1 ] dim=3048 [ 73 1 ] dim=3048 [ 68 1 ] dim=3048 [ 68 1 ] dim=3048 [ 68 1 ] dim=3048 [ 68 1 ]

验证集

egs里还从训练集中取了一小部分,作为验证集,valid_diagnostic.egs;
还取了一小部分,作为小训练集,train_diagnostic.egs,用来看模型训练得怎么样,这个小训练集是有更新模型参数的。

验证集accuracy、loss 计算命令: nnet3-compute-prob

对应log:compute_prob_x.$iter.log。

compute_prob_train.$ite.log是小训练集的结果;(有更新模型参数)

compute_prob_valid.$ite.log是小验证集的结果;(没更新模型参数)

combine

最后模型要做一个combine,
nnet3bin/nnet3-combine.cc

nnet3-combine --use-gpu=yes --max-objective-evaluations=30 --verbose=3 exp/nnet3/tdnn_fbank/340.mdl exp/nnet3/tdnn_fbank/339.mdl  'ark,bg:nnet3-copy-egs ark:exp/nnet3/tdnn_fbank/egs/combine.egs ark:- | nnet3-merge-egs --minibatch-size=1:256 ark:- ark:- |' '| nnet3-am-copy --set-raw-nnet=- exp/nnet3/tdnn_fbank/340.mdl exp/net3/tdnn_fbank/combined1.mdl'

权重的

先统计有多少个model要combine,
计算loss function,objf,

 objf = ComputeObjf(batchnorm_test_mode, dropout_test_mode, egs, moving_average_nnet, &prob_computer);

[外链图片转存失败(img-UywqK93R-1567257066238)(en-resource://database/3633:1)]

调用 nnet-diagnostics.cc 的 GetTotalObjective :

[外链图片转存失败(img-C5gakzhl-1567257066238)(en-resource://database/3635:1)]

其中,weight是 那些pdf占的帧数。。。

模型combine,里头的参数w,b 好像没有combine,比如第一个model的w和第二个model的w结合。好像没有。

DNN哪些用计算使用GPU、哪些使用CPU:

GPU:DNN的前向计算,反向传播,与神经元相关的。
CPU:

  1. 前向计算完后,求loss,accuracy;
  2. 算平均(多个jobs每轮迭代生成多个模型,取一个平均);
  3. 求P(O|M),即DNN求得后验概率,要推回似然,这个计算也是CPU做的;
  4. combine;

GPU相关

要设置GPU运行个数(GPU是哪个卡有空闲,就会取用哪个卡)。

通过设置num_jobs改变GPU运行个数。

比如只用一个GPU,令 num_jobs_initial=1和num_jobs_final=1。

指定用哪个GPU:
在 bashrc 里 export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 (用8卡)
然后source一下,退出再进入才能生效。(最好把gpu的编号写成以2为公差的等差数列,这样散热会好一点。)

要在cmd.sh里设置gpu数量:
export cuda_cmd=“run.pl --gpu 2”

egs:

开始训练前,要把数据标签,变成kaldi能识别的结构体,这个结构体叫做 egs ,样例,样本。就是把数据,标签等,变成一个个egs,给后续DNN训练。

对应代码为:steps/libs/nnet3/train/frame_level_objf/acoustic_model.py 的 generate_egs 函数。

tdnn用到的地方在:steps/nnet3/train_dnn.py:train_lib.acoustic_model.generate_egs。

显然,这里egs只用生成一次,即使后续重新训练DNN了,这里egs也无需重新生成,所以可以把把上层的stage设置为大于-4,这里就不会运行了。

[外链图片转存失败(img-sp25cI1A-1567257066239)(en-resource://database/2560:1)]

  1. 没有用batchnorm,因为这个是批归一化;用renorm,帧归一化
  2. 神经元节点多,但是拼帧少,输出当前帧时,都没有用到当前帧的信息,用的是前后拼的帧。
    因此网络结构mdl比较大,但是raw小,因为输入维度拼帧少,则输入维度小(input = dim* 拼帧数)

配置文件config里
[外链图片转存失败(img-OiGAFiDI-1567257066239)(en-resource://database/2564:1)]
这里的input dim取决于输入的训练集train_set里的特征文件的维度。

一般特征多少维度,写在 conf 里。
cat conf/mfcc_hires.conf : 40维

所以现在想用不同的特征去训练:

  • 方法1. 用已经训练好的43维(40维+3维pitch,这里40维不是lda,是因为配置文件mfcc_hires写的40维)把train_sp(里头是3倍特征量)的后面数据量删掉。

在这里插入图片描述
把feats.scp的spl开头的行删掉,然后用 utils/fix_data_dir.sh 文件夹名(比如data/train_sp),就自动把该文件夹下其他的,比如text、utt等和feats.scp不同的修复成相同(删掉多余项)

就可以用43维特征了。

  • 方法2. 重新训练43维特征

  • 方法3. 用16维特征,把input dim设置为16.

LDA特征矩阵去相关,可以降维也可以升维

fixed-affine-layer name=lda input=Append(-2,-1,0,1,2) affine-transform-file=$dir/configs/lda.mat

注意,配置文件xconfig里,这里的DNN输入(拼帧后)是80维,做了LDA后也还是80维,没有降维,目的是去除DNN特征矩阵的相关性。

训练出LDA.mat:对应到train_dnn.py里的 train_lib.common.compute_preconditioning_matrix

config文件中,输入特征要先经过(乘以) lda.mat矩阵,得到一个维度不变的,去相关后的特征,因此DNN中第一层是经过LDA特征变换,再进行后续的仿射变换等操作。

LDA必须要做,不做的话,DNN效果会差很多,因为DNN特征和相关性有很大关系,这点和GMM对特征矩阵的要求相同,他们都需要特征不同维度之间的相关性很弱,不同的是,GMM里需要给特征降维,因为太多维的特征训练不出GMM,而DNN没有限制。

注意,前面步骤中的LDA+MLLT并没有把LDA特征保存下来,这步是在代码执行过程中利用了对特征做了LDA然后训练出的新模型,但是没有保存做了LDA后的特征文件,工程中保存的还是只有原始特征文件mfcc,因此后续还想用lda,要重新对原始特征做lda。(这就和每一次生成模型中,都要做delta是一个道理。)

每轮iter里做了不同eg次(由job数决定)(egs即archive)。

  • 每个iter里输出一个mdl/raw
  • 每轮iter里输出job个mdl/raw,这些小mdl/raw,选择一种策略,作为本次iter的输出mdl/raw
    两种策略:
    做 平均 或者 取 最好 :
    steps/libs/nnet3/train/frame_level_objf/common.py:
     if do_average:
         # average the output of the different jobs.
         common_train_lib.get_average_nnet_model(
             dir=dir, iter=iter,
             nnets_list=" ".join(nnets_list),
             run_opts=run_opts,
             get_raw_nnet_from_am=get_raw_nnet_from_am)

     else:
         # choose the best model from different jobs
         common_train_lib.get_best_nnet_model(
             dir=dir, iter=iter,
             best_model_index=best_model,
             run_opts=run_opts,
             get_raw_nnet_from_am=get_raw_nnet_from_am)

等训练完了,取一些iter出来的mdl,做 combination 融合,再输出一个最终mdl。
ps. 现实中,设置很大的epoch,一般看验证集的loss和accuracy,如果差不多了,就停止,其实根本没有到combine那一步

train_dnn.py的输入参数:

    python -m pdb steps/nnet3/train_dnn.py --stage=$train_stage \
    --cmd="$decode_cmd" \
    --feat.cmvn-opts="--norm-means=false --norm-vars=false" \
    --trainer.num-epochs $num_epochs \
    --trainer.optimization.num-jobs-initial $num_jobs_initial \
    --trainer.optimization.num-jobs-final $num_jobs_final \
    --trainer.optimization.initial-effective-lrate $initial_effective_lrate \
    --trainer.optimization.final-effective-lrate $final_effective_lrate \
    --egs.dir "$common_egs_dir" \
    --cleanup.remove-egs $remove_egs \
    --cleanup.preserve-model-interval 500 \
    --use-gpu wait \    #记得这里在做combine时要改成wait 否则会下图1错误
    --feat-dir=data/${train_set} \
    --ali-dir $ali_dir \
    --lang data/lang \
    --reporting.email="$reporting_email" \
    --trainer.optimization.minibatch-size 256 \
    --dir=$dir  || exit 1;

图1:
在这里插入图片描述

matrix-sum-rows:
对矩阵输入表的行求和并输出相应的向量表

输出模型mdl

combined.mdl:
在这里插入图片描述
第一个lda矩阵,这里是一个矩阵,它不会更新(不会参与dnn bp更新),意思是输入的任意特征,要乘以这个LDA矩阵(和里头的很多参数相乘),得到新的一个特征矩阵。再传给后续。

在这里插入图片描述
之后经过一个affine仿射变换曾,进行Wx+b操作。
这里头的参数就是神经元权重和偏置。

MaxChange:这里是一个限制项,限制每次迭代的参数变化,不能变化太大了,这里0.75意思是变化不能超过上一轮的75%,

计算先验 prior

kaldi里除了传统计算先验P(W)以外,

还有一个计算后验平均的操作 compute_average_posterior。也是为了重新计算先验。

avg_post_vec_file = train_lib.common.compute_average_posterior(
             dir=args.dir, iter=real_iter,
             egs_dir=egs_dir, num_archives=num_archives,
             prior_subset_size=args.prior_subset_size, run_opts=run_opts)
             
具体实现:
nnet3-copy-egs ark:exp/nnet3/tdnn/egs/egs.1.ark ark:- | nnet3-subset-egs --srand=1 --n=20000 ark:- ark:- | nnet3-merge-egs --minibatch-size=128 ark:- ark:- | nnet3-compute-from-egs --use-gpu=wait --apply-exp=true exp/nnet3/tdnn/combined.mdl ark:- ark:- | matrix-sum-rows ark:- ark:- | vector-sum ark:- exp/nnet3/tdnn/post.combined.1.vec

其中,–apply-exp=true,是因为softmax出来的是取了log的概率,因此这里取了指数e(elog(A)=Ae^{log(A)} = Aelog(A)=A)。

由get_post.combined.x.log可知,
首先把取了egs的子集subset,Selected a subset of 20000 out of 366360 neural-network training examples,然后按照minibatch分开成一个个矩阵,这里minibatch为128,因此有156个example(20000/128=156),对每个example,按行对pdf求和。

这里的矩阵,列数是pdf个数(每帧的softmax输出,不是单一一个概率,而是一个后验概率的向量,比如3078个pdf,就是一个1* 3078的向量,每个向量里的值代表这个pdf的概率);

行数是帧数,因为按行求和,就是把每个分类的所有行上的概率加起来,作为后验概率的平均,其实也就是作为这个分类的先验。

然后用 nnet3-am-adjust-priors 直接除以刚才的vector的sum,进行归一化,然后替换之前的先验概率old_priors。

这个计算后验平均,再作为先验的操作,效果提升很小。

训练好的DNN模型,再做对齐,再训练一个DNN模型

steps/align_si.sh --cmd "$train_cmd" --nj 32 data/train data/lang exp/nnet3/tdnn exp/nnet3/tdnn_ali || exit 1;

改动 steps/align_si.sh 的几个地方:
1. feats要和神经网络维度一致:
将 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:- |";;

改为: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:- |";; 
这里把delta去掉了。


2. 将 gmm-align-compiled $scale_opts --beam=$beam --retry-beam=$retry_beam --careful=$careful "$mdl" ark:- "$feats" "ark,t:|gzip -c >$dir/ali.JOB.gz" || exit 1;

改为:nnet3-align-compiled $scale_opts --beam=$beam --retry-beam=$retry_beam --use-gpu=no --careful=$careful "$mdl" ark:- "$feats" "ark,t:|gzip -c >$dir/ali.JOB.gz" || exit 1;
这里把gmm的对齐改成nnet3的对齐。因为它们网络结构不同,转移概率保存的结构不同。


ali文件 用来给DNN提供label,和计算先验,音频特征是按utt去找到ali文件里对应的label序列(tid序列)的。
因为我发现一个现象,feats.scp,我只取了20000条句子,但是ali还是原先那个几十万条句子的文件,理论上应该新生成一个ali,但是没有,原因就是feats.scp找到对应对齐序列,靠的是utt去索引的,所以ali对齐文件集可以大于输入特征集合。

combined.mdl:每层的Rms都是0.。?

用一个mdl,作为训练的初始0.mdl,去重新训练迭代网络

把模型路径作为参数传入train_dnn.py
steps/nnet3/train_dnn.py –trainer.input-mode=模型路径

在这里插入图片描述

迭代到某步停了,从那一步迭代继续跑起

run_tdnn.sh里的train_stage设置为迭代值。

$ steps/nnet3/train_dnn.py --h 就能看见所有参数了。


特征还是用的MFCC,如果想用Fbank,可以在MFCC的基础上做IDCT;conf用fbank.conf生成特征文件。

训练参数

参考:kaldi 神经网络

  • job number
    一般GPU设为4,CPU设为8或者16,因为GPU的训练速度比CPU快20%到50%。
    minibatch的大小跟-num-jobs-nnet有关,如果使用多线程(比如n个线程)更新参数的方式,那么minibatch size相当于变成了原来的n倍。
    学习率的设置跟-num-jobs-nnet有关,如果我们jobs变为原来的n倍,那么学习率也要变为原来的n倍。因为并行使用的是n个模型参数平均的方式。但是学习率不能设置过大,否者会引起训练的不稳定。

  • 隐层数量
    一般tanh网络是2到5层,p-norm网络是2到4层,增加层数的时候一般保持节点数不变

  • 节点数
    一般是512/1024/2048,一般1024的网络就比较大了,最多是2048。和训练数据量的增加成二次关系,比如数据量变为原来的10倍,节点数变为原来的2倍。

  • 学习率
    小数据量(几个小时)的初始值和结束值分为设为0.04和0.004;数据量变大以后,学习率要调低。
    可以通过绘制目标函数和时间的关系图来判断学习率是否合适。如果学习率太大,一开始目标函数值提升很快,但是最终值缺不理想,或者发生震荡,目标函数值突然变得很差;如果学习率太小,需要花费很长的时间才能获得最优值。
    一般来说网络的最后两层参数学习的速度更快,可以通过–final-learning-rate-factor参数(比如0.5)使得最后两层学习率衰减。

  • minibatch size
    数值越大训练速度越快,但是数值过大会引起训练的不稳定性。一般设为2的倍数,多线程CPU设为128,GPU设为512.

  • max-change
    训练的时候如果学习率设置太大,将会导致参数变化量过大,引起训练不稳定。该参数的设置为参数的变化量设定一个上限。当minibatch大小为512,max-change设为40,minibatch大小为128,max-change设为10,max-change和minibatch的大小成正比。

  • epoch个数
    两个参数–num-epochs(一般15)和–num-epochs-extra(一般5)设置,从0到–num-epochs之间学习率会衰减,最后的–num-epochs-extra学习率保持不变。小数据量一般设置更多的epoch(20+5),大数据量设置更少的epoch。

  • feature splice width
    对于MFCC+splice+LDA+MLLT+fMLLR这种经过特殊处理的特征,一般设为4,因为LDA+MLLT已经是基于spliced(3或者4)的特征了;对于原始的MFCC/fbank特征,一般设为5。
    如果数值设置的更大,对于帧准确率是有益的,但是对于语音识别却是有损的。或许是因为违反了HMM帧独立性的假设。

  • momentum:动量常数,是一种Momentum constant to apply during training (help stabilize update). e.g. 0.9. Note: we automatically multiply the learning rate by (1-momenum) so that the ‘effective’ learning rate is the same as before (because momentum would normally increase the effective learning rate by 1/(1-momentum)) (float, default = 0)

  • backstitch:参考论文《 Backstitch: Counteracting Finite-sample Bias via Negative Steps》
    学习率由SGD的 [外链图片转存失败(img-0wr3ua3L-1567257066241)(en-resource://database/3248:1)]
    变化为 [外链图片转存失败(img-oGRjzwqQ-1567257066241)(en-resource://database/3246:1)]
    先往梯度反方向走一小步,再反方向走一步多一点。
    雷博说,实际没什么用。

序列化过程(计算图、前向后向计算)

libs/nnet3/train/frame_level_objf/common.py

nnet3-train

前向计算,计算loss得到objf目标函数,也做了BP。

nnet3-train --use-gpu=yes --read-cache=exp/nnet3/tdnn_new_1/cache.108 --print-interval=10 --momentum=0.0 --max-param-change=2.0 --backstitch-training-scale=0.0 --l2-regularize-factor=0.5 --backstitch-training-interval=1 --srand=108
  • 跳转 → nnet3bin/nnet3-train.cc → nnet3/nnet-training.cc → nnet3/nnet-compute.cc

对应log:train.{iter}.{job}.log

nnet3/nnet-training.cc:
在这里插入图片描述

第一个 compute.run :前向计算 forward
第二个 compute.run :反向计算 backward

nnet3/nnet-compute.h:

 /// This does either the forward or backward computation, depending
  /// when it is called (in a typical computation, the first time you call
  /// this it will do the forward computation; then you'll take the outputs
  /// and provide derivatives; and the second time you call it, it will do
  /// the backward computation.  There used to be two separate functions
  /// Forward() and Backward().
void Run();

nnet3/nnet-compute.cc:

ExecuteCommand();   // Returns the matrix index where the input (if is_output==false) or output matrix index for "node_name" is stored. (nnet3/nnet-compute.h)
  • softmax前向,在kaldi-matrix. cc(CPU实现)

  • 反向在nnet-simple里3560行,调用cu-matrix. cc的1868行。

训练 取egs数据

保存egs时是这么保存的,但,具体训练时,由于数据特征很像,所以每次取frames-per-eg帧,只训练一帧。

注意:见:steps/libs/nnet3/train/frame_level_objf/common.py
在这里插入图片描述
余数 %frames_per_eg,因此每次是0-7中的一个数(frames_per_eg=8),代表这frams_per_eg个数据,去进行训练,只训练出一个值。

nnet3-train  'nnet3-copy --learning-rate=0.0029  exp/nnet3/tdnn_new_1/1.mdl - |' 'ark,bg:nnet3-copy-egs --frame=0(取出egs里所有的第0帧) ark:exp/nnet3/tdnn_new_1/egs/egs.3.ark ark:- | nnet3-shuffle-egs --buffer-size=5000 -srand=1 ark:- ark:- | nnet3-merge-egs --minibatch-size=256 ark:- ark:- |' exp/nnet3/tdnn_new_1/2.1.raw

shuffle 打乱顺序的目的:减少说话人引入的影响。不同说话人差异很大时,是训练不出模型的。比如一个说话人对应512帧,一个minibatch也为512,那么一次训练出来的是最符合这个说话人的,因此不具有随机性,不符合独立同分布,说话人A训练出model A,说话人B训练出model B,modelA,B差别很大,那么最终这个模型是训练不出来的。

iter、egs、frame的关系

  • 举个例子. jobs数=4,egs数等于17,frame_per_egs=8
  1. 第一轮迭代 iter=0,取前4个egs(1~4)的,第1个gpu取第1个egs的所有第1帧,,第2个gpu取第2个egs的所有第2帧,对应4个gpu(job),4个egs的4帧,每次用minibatch地训练4个模型,做一个平均/best,得到一个模型;
  2. 第二轮迭代 iter=1,分别取中间4个egs(5~8)的第5,6,7,0帧,训练4个模型,做一个平均;
  3. 第三轮迭代 iter=2,分别取中后的4个egs(9~12)的第1,2,3,4帧,训练4个模型,做一个平均,得到平均模型。

在这里插入图片描述

在这里插入图片描述

由于每次取的是不同egs的不同帧,会不会出现没取到某些egs的某些帧的情况发生呢。

不会发生的,观察可以发现,kaldi里对取训练数据有一个特点:

这次取这个egs的第n帧,下次再取到这个egs时,取的就是第n+1帧。
原因是 因为 archive_index相同时,下一轮 k//num_archives增加了一个。

比如 job=1,num_archives = 3,令 t = k // num_archives:
k=0,t=0;
k=1,t=0;
k=2,t=0;
k=3,t=1;# 这里增加了1,对应上文这次取这个egs的第n帧,下次再取到这个egs时,取的就是第n+1帧。
k=4,t=1;
k=5,t=1;
k=6,t=2;

在这里插入图片描述

  • egs序号为archive_index
  • num_archives_processed是已经进行的多少步,是按job数更新的,比如job数=4,则每次累加4个(num_archives_processed数依次是 0,4,8,12,16…(当num_jobs_initial==num_jobs_final时))

可化简为:

achi = 12
job=4
egs = (achi+job-1)%17 +1
frame=((achi+job-1)//17 + egs) % 8
print("egs",egs)
print("frame",frame)

并行计算对模型造成的影响

理论上应该是只设置一个job,让这轮的模型参数,去用SGD更新权重,去训练新一轮模型的,现在用并行计算,一次性训练多个模型,进行一个SGD,因此理论上这个SGD更新得不是流畅的,并行去用SGD更新权重,去训练新一轮模型的,现在用并行计算,一次性训练多个模型,进行一个SGD,因此理论上这个SGD更新得是不流畅的,不连续的。

但是文献中,用了很多自然梯度算法,比如学习率的并行算法等等,让即使并行计算,最后效果和不并行的效果差不多。

但是并行计算大大地减小了计算时间。

egs的影响

理论上每个egs里的数据应该越多越好,或者说每个egs里的数据分布越分散越好,这样才能保证每个egs都能涵盖尽可能多的类别数,这样对于训练是有益的,因为这个egs训练出modelA,另一个egs训练出modelB,如果两个egs的类别交集很少,那么这次iter进行的模型average就没有意义了。

因此 samples_per_iter 应该设置大一点好。(我猜)

epoch选取

可以根据switchboard来,比如300小时,epoch=15,按比例推得其他数据要多少epoch。


前向后向:可以参考 nnet3/nnet-simple-component.h/cc (有时候看.h更直观,.cc是实现)

ElementwiseProductComponent:

Cuda报错

ERROR (nnet3-train[5.5.458~2-84ab]:CopyFromVec():cudamatrix/cu-array-inl.h:110) cudaError_t 77 : “an illegal memory access was encountered” returned from ‘cudaStreamSynchronize(cudaStreamPerThread)’

这个原因,分析是显卡散热不足,当内存占用很多时,就会崩。

于是要把GPU运行时,最多占多少内存设置一下,让不能超过一个值。

设置方法:

把steps/nnet3/train_dnn.py里的:

run_opts.parallel_train_opts = “–use-gpu={}”.format(args.use_gpu)

改为:

run_opts.parallel_train_opts = “–use -gpu=yes --cuda-memory-proportion=0.25”

这是参考了src/cudamatrix/cu-allocator.h里传参的写法。让cuda内存最多是2.5G。

解码decode

  • nj 进程数;
  • thread 线程数;

开几个nj,根据split,把总体数据分成nj块,然后生成nj个解码图。
可以nj开小一点,然后thread开多一点,在一个图上,让多个线程去解码。

解码很少用到GPU,可以直接设置use-gpu=false,因为解码过程中,GPU只是用来:特征输入声学模型给出声学模型分数,然后看对应的是哪个tid

结果

  1. 20000条句子;特征:/data/train,16维;解码图:/tri5a/graph
    网络结构:
  fixed-affine-layer name=lda input=Append(-2,-1,0,1,2) affine-transform-file=$dir/configs/lda.mat
  relu-renorm-layer name=tdnn1 dim=850
  relu-renorm-layer name=tdnn2 dim=850 input=Append(-1,2)
  relu-renorm-layer name=tdnn3 dim=850 input=Append(-2,1)
  relu-renorm-layer name=tdnn4 dim=850 input=Append(-3,3)
  relu-renorm-layer name=tdnn6 dim=850
  output-layer name=output input=tdnn6 dim=$num_targets max-change=1.5
名称 对齐文件 结果
tdnn_new_2 tri5a_ali WER 20.14,CER 10.95
tdnn_new_1 tdnn_new重新对齐得到的tdnn_ali WER 19.74,CER 10.68

打印 report、输出accuracy曲线

由于服务器没装matplotlib.pyplot包,因此把结果保存到本地,进行画图。

新建 report.py:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import sys
sys.path.insert(0, 'steps')
import libs.nnet3.report.log_parse as nnet3_log_parse

[report, times, data] = nnet3_log_parse.generate_acc_logprob_report('/home/data/yelong/kaldi/egs/aishell/s5/exp/nnet3/tdnn_new_2')
print(data)

执行 report.py,将输出结果 data 复制下来。

新建本地 pltloss.py:

import matplotlib.pyplot as plt

data=[(0, 0.0442313, 0.047502), (1, 0.434783, 0.439005), (2, 0.546942, 0.552719), (3, 0.568631, 0.570827), (4, 0.582137, 0.582364), (5, 0.598303, 0.588583)]

iter=[x[0] for x in data]
train_objf = [x[1] for x in data]
valid_objf = [x[2] for x in data]

plt.figure()
plt.plot(iter,train_objf,label="train_accuracy")
plt.plot(iter,valid_objf,label="valid_accuracy")
plt.legend()
plt.show()
#plt.savefig("accuracy.jpg")

就可以得到 accuracy.jpg了:

在这里插入图片描述

这个是从小训练集和小验证集取出的,

实验中可以看出,accuracy一般都在60~70%多,这是正常的。只要50%以上就可以,因为这只是DNN softmax 分类结果,还不是DNN-HMM模型。

(虽然dnn的label是GMM-HMM对齐给出的,但是不能说,DNN分类结果,到100%,就等于GMM,因为GMM-HMM 不等于 GMM。)

因为分类数很多,比如aishell这里的3048个类,每次正确的都要在3048个类中选对那个label,因此能做到60%~70%就已经很高了。

我们的目的是,训练一个DNN-HMM模型,让这个模型给出的声学模型分数,经过解码后,wer尽可能低。

因此从宏观上来说,DNN模型和GMM模型的差异在于,经过模型后,打出的声学分数不同。


三元组

egs 用三元组保存:

std::vector egs

举例: nnet3bin/nnet3-combine.cc

在这里插入图片描述

三元组对应的label(一行8帧,8帧对应8个label,属于哪个pdf)
在这里插入图片描述

在这里插入图片描述
举个例子:[ 837 1 ],label是3048类中的pdf-id=837,1是权重,first=837,second=1。second只有0.5和1两个值,0.5是两个帧重合的时候取0.5(举例:egs里只有15帧,每8帧一行,则第二行的最后一帧是和第一帧共用共享了)

标签:ark,num,训练,egs,kaldi,train,tdnn,nnet3
来源: https://blog.csdn.net/eqiang8848/article/details/100177315

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有