x-vector sid/nnet3/xvector/get_egs.sh 由特征到网络输入
x-vector模型训练的脚本是nnet3bin/nnet3-train.cc
该脚本的调用方式是 Usage: nnet3-train [options] <raw-model-in> <training-examples-in> <raw-model-out>
该脚本训练模型的方式是从一个文件(<training-examples-in>)中依次读取一个NnetExample对象进行训练迭代,所以如何有已有的mfcc特征构造出NnetExample对象?
在执行nnet3-train.cc 的log中关于egs的输入分别调用三个bin,分别是 nnet3-copy-egs,nnet3-shuffle-egs,nnet3-merge-egs。前两个脚本是复制和打乱顺序,网络的输入关键是nnet3-merge-egs, 这个merge就是将多个(minibatch个)NnetExample合并成一个.
所以网络的输入构造分为两步
1. 将一个utt对应mfcc特征,标签,转换为一个NnetExample
2. 将多个NnetExample实体合并成一个NnetExample对象
每个utt对应着mfcc特征和说话人id(将字符串映射成数字,0至num_speaker-1,便于构成网络输出标签),
每个utt可以构造一个NnetExample对象。这个构造过程是有脚本 sid/nnet3/xvector/get_egs.sh产生
产生的过程是第一,由sid/nnet3/xvector/allocate_egs.py 产生range文件,然后range文件作为nnet3bin/nnet3-xvector-get-egs.cc的输入,然后产生NnetExample文件。
现在有训练集feats.scp 用此产生range 文件, feats.scp包含很多utt对应的mfcc,会产生多个 egs 文件,具体多少个,控制方式如下,在get_egs.sh的参数中有一个参数--frames-per-iter 1000000000这个参数字面含义是每轮迭代的帧数,每次迭代训练的输入是一个完整的 egs 文件,所以可以理解为每个egs文件包含的总帧数,假设feats.scp包含M帧,每个egs文件大概N帧,则会产生M/N + 1 个ges文件.
而allocate_egs.py产生的range文件的个数是有nj决定的但nj应该小于等于egs文件个数,然后用nj个进程调用nnet3-xvector-get-egs.cc输入range产生egs文件。
将这么多utt分到不同的egs文件里,就需要一个标志来指定某个utt属于第几个range文件,这个标志位于range文件的第三列,
有range文件产生egs(NnetExample)文件时,调用的nnet3-xvector-get-egs.cc 可以有多个输出,调用方式如下。
Usage: nnet3-xvector-get-egs [options] <ranges-filename> <features-rspecifier>
<egs-0-out> <egs-1-out> ... <egs-N-1-out>
可以看出这个脚本可以有多个输出,假设现在要产生5个egs文件,而进程数量是2,此时每个进行就会产生多个egs文件,此时仅仅指定一个utt属于第几个egs文件还不够,还需要指定这个utt在某个进程中输出至第几个egs文件,这个标志位于range文件第二列,如果用10个进程产生10个egs文件,每个进程只产生一个,这个标志就全部为0. 综上所述,range文件的第二第三列只是确定一个utt产生的NnetExample位于第几个egs文件,与如何产生NnetExample无关。
在get_egs.sh的参数中有两个参数 --min-frames-per-chunk 200 ,--max-frames-per-chunk 400 调用这个脚本之前,会先删除帧数小于max-frames-per-chunk 的utt。 在产生egs的时候,并不是用的所有mfcc特征,而只是截取其中的部分,而截取的长度则是在这两个参数之间的随机数,截取的起始位置则又是另一个随机数,令在max-frames-per-chank和min-frames-per-chunk之间的随机数为random-length,假设需要产生10个egs文件,则每个文件对应一个random-length,每个文件的random-length都是随机的,几乎不同,所以由一个utt产生一个NnetExample时,还需要指定这个utt对应的random-length,该标志位于range文件的第5列。
假设一个utt帧长num_rows截取的mfcc长度为random-length,则起始位置是在0-(num_rows - random-length)之间的随机数,这个随机数就是截取mfcc的起始位置,这个标志位于range文件的第4列,所以range文件的第4第5列决定了截取mfcc的子矩阵。
由于range文件中的第一列标志utt可能会重复,所以在构成egs文件时,utt标记改为 utt + "-" + 第四列 + "-" + 第五列 + "-" + 第六列构成NnetExample的标记,与训练并无联系,
range文件的最后一列为spk-id构成网络的输出,假设整个训练有N个人,则某个utt对应的网络输出就是一个N维的0-1向量,对应spk-id的值为1
一个NnetExample只包含一个属性 vector< NnetIo >,由utt产生NnetExample的过程,会生成两个NnetIo对象,然后这两个对象存在vector< NnetIo>中,这两个对象分别称为input_NnetIo 和 output_NnetIo, 分别对应着网络的输入特性和输出标签。
struct NnetIo {
std::string name;
std::vector<Index> indexes;
GeneralMatrix features;
}
两个NnetIo的name分别为"input" , "output" ; features分别是mfcc子矩阵,标签矩阵(一维,有稀疏矩阵生成)
假设子矩阵的帧长为M,则 input_NnetIo 的indexes为:
n 0 0 0 ... 0
t 1 2 3 ... M
x 0 0 0 ... 0
output_NnetIo的indexes为:<n,t,x> = <0,0,0>
目前有utt产生产生NnetExample已经完成,接下来是NnetExample的合并,在模型训练的时候,会首先将--minibatch-size=64数量的NnetExample进行合并,NnetExample的合并本质上就是NnetIo的合并,input_NnetIo的合并为 name仍然为"input",因为所有的NnetIo的name都一样,features是矩阵,合并后的结果就是64个句子的拼接,indexes的合并也相当于拼接,只不过n表示这个utt在合并的这个批次中的索引,最终形式为
n 0 0 0 ... 0 1 1 1 ... 1 2 2 2 ... 2 ... ... 63 63 63 ... 63
t 1 2 3 ... M 0 1 2 ... M 0 1 2 ... M ... ... 0 1 2 ... M
x 0 0 0 ... 0 0 0 0 ... 0 0 0 0 ... 0 ... ... 0 0 0 ... 0
output_NnetIo 合并类似,也是简单的拼接,index的n指定合并批次的索引。
网友评论