本文将以switchboard为例,解读kaldi卷积神经网络部分的bash脚本。一方面便于以后自己回顾,另一方面希望能与大家互相交流。
在switchboard部分的训练代码中,kaldi官方并未提供相关训练的deamon,但kaldi本身支持卷积神经网络的训练,在egs/swbd/s5c/steps/nnet2中,kaldi提供了训练的CNN网络的核心代码脚本 train_convnet_accel2.sh,因此我们只需按照egs/swbd/s5c/run.sh的执行步骤,对代码进行一定的修改,便能进行卷积神经网络的构建。
在本文中,假设读者已经熟悉run.sh中的训练脚本并且对语音识别模型训练相关步骤有一定的经验。(目前没时间,不久之后我将会对run.sh的一些步骤进行解读。)
好,现在开始正式进入话题。
1. 标注对齐:训练CNN模型需要对每一帧进行标注,由于switchboard数据中仅对某段时间内的数据内容进行标注,因此我们需要用一个前面已经通过run.sh训练过的HMM-GMM模型进行数据对齐。
2. 数据准备:kaldi提供的CNN训练所选用的是FBANK特征,此处为了便于下文网络的结构解析,因此沿用kaldi的特征,读者理解完脚本以后可以根据自己的需要修改特征。FBANK特征维度是36维,对每一个说话人的特征进行归一化,训练CNN网络时还会用到特征的一阶和二阶差分参数。
对训练集进行划分,从中选取4000句作为交叉验证,剩下的全部作为训练集使用。
3. CNN模型训练:应用kaldi提供的核心训练代码,向训练脚本中传递相关的训练参数:网络的结构,learning rate,运行环境,任务数等。下文将会展开脚本对各个参数进行解析。
4. CNN模型测试:对训练所得的模型进行测试,与HMM-GMM模型,DNN模型进行比较。
s5c/conf/fbank.conf的配置:
--window-type=hamming # disable Dans window, use the standard
--sample-frequency=8000
--low-freq=64 # typical setup from Frantisek Grezl
--high-freq=3800
--dither=1
--num-mel-bins=36 # 8kHz so we use 36 bins (@ 8 filters/octave to get closer to 40 filters/16Khz used by IBM)
#!/bin/bash
#=> This script training the CNN(Convoluntional neural network ) model for swbd
temp_dir=
dir=nnet_cnn_fbank
has_fisher=true
. cmd.sh
. path.sh
set -e
printf 'Start CNN training in:'
date
. utils/parse_options.sh
#data prepare
echo '===============================CNN data preparing================================='
#fbank feature extract
fbankdir=fbank
for x in train eval2000;do
mkdir -p data/fbank/$x
cp data/$x/* data/fbank/$x
rm -rf data/fbank/$x/.backup data/fbank/$x/cmvn.scp data/fbank/$x/feats.scp
steps/make_fbank.sh --nj 64 --cmd "$train_cmd" \
data/fbank/$x exp/make_fbank/$x $fbankdir
#对每一个说话人进行特征归一化,将归一化值写入到$mfccdir/cmvn_$x.ark文件,不改变上一步提取的特征。
steps/compute_cmvn_stats.sh data/fbank/$x exp/make_fbank/$x $fbankdir
#去除utt2spk spk2utt feats.scp text segments wav.scp cmvn.scp vad.scp reco2file_and_channel spk2gender utt2lang中的物理上不存在的文件,
#并按照文件名对标注进行排序。
utils/fix_data_dir.sh data/fbank/$x
done
echo '*******************************************Start subset data for training *******************************************'
# Use the first 4k sentences as dev set. Note: when we trained the LM, we used
# the 1st 10k sentences as dev set, so the 1st 4k won't have been used in the
# LM training data. However, they will be in the lexicon, plus speakers
# may overlap, so it's still not quite equivalent to a test set.
#将数据分为训练集(train set)和交叉验证集(dev set)
utils/subset_data_dir.sh --first data/fbank/train 4000 data/train_cnn_dev # 5hr 6min
n=$[`cat data/fbank/train/segments | wc -l` - 4000]
utils/subset_data_dir.sh --last data/fbank/train $n data/train_cnn_nodev
#The full training set:
#取整个完整的训练集,去除其中重复次数超过300次的句子。
local/remove_dup_utts.sh 300 data/train_cnn_nodev data/train_cnn_nodup # 286hr
parallel_opts="--gpu 1"
echo "==============================Start CNN training.==============================="
(
if [ ! -f exp/$dir/final.mdl ];then
if [[ $(hostname -f) == hnlg.cn ]] || [[ $(hostname -f) == compute-0-* ]] && [ ! -d exp/$dir/egs/storage ]; then
# spread the egs over various machines.
utils/create_split_dir.pl \
/export/home/$USER/b0{1,2,3,4}/kaldi-data/egs/swbd-$(date +'%m_%d_%H_%M')/s5c/$dir/egs/storage exp/$dir/egs/storage
fi
steps/nnet2/train_convnet_accel2.sh --parallel-opts "$parallel_opts" \
--cmd "$cuda_train_cmd" --stage -3 \
--num-threads 1 --minibatch-size 512 \
--mix-up 20000 --samples-per-iter 300000 \
--num-epochs 15 --num-hidden-layers 4 \
--initial-effective-lrate 0.005 --final-effective-lrate 0.0002 \
--num-jobs-initial 3 --num-jobs-final 24 \
--delta-order 2 --splice-width 5 \
--num-filters1 128 --patch-dim1 7 --pool-size 3 --patch-step1 1 \
--num-filters2 256 --patch-dim2 4 \
data/train_cnn_nodup \
data/lang exp/tri4_ali_nodup exp/$dir || exit 1;
fi
steps/nnet2/decode.sh --cmd "$cuda_decode_cmd" --nj 32 \
--config conf/decode.config \
--transform-dir exp/tri4/decode_eval2000_sw1_tg \
exp/tri4/graph_sw1_tg data/eval2000 \
exp/$dir/decode_eval2000_sw1_tg || exit 1;
if $has_fisher; then
steps/lmrescore_const_arpa.sh --cmd "$decode_cmd" \
data/lang_sw1_{tg,fsh_fg} data/eval2000 \
exp/$dir/decode_eval2000_sw1_{tg,fsh_fg} || exit 1;
fi
)
steps/nnet2/train_convnet_accel2.sh
Usage: steps/nnet2/train_convnet_accel2.sh [opts]
e.g.: steps/nnet2/train_convnet_accel2.sh data/train data/lang exp/tri3_ali exp/tri4_nnet
Main options (for others, see top of script file)
--config # config file containing options
--cmd (utils/run.pl|utils/queue.pl ) # how to run jobs.
--num-epochs <#epochs|15> # Number of epochs of training
--initial-effective-lrate 0.02> # effective learning rate at start of training,
# actual learning-rate is this time num-jobs.
--final-effective-lrate 0.004> # effective learning rate at end of training.
--add-layers-period <#iters|2> # Number of iterations between adding hidden layers
--mix-up <#pseudo-gaussians|0> # Can be used to have multiple targets in final output layer,
# per context-dependent state. Try a number several times #states.
--num-jobs-initial 1> # Number of parallel jobs to use for neural net training, at the start.
--num-jobs-final 8> # Number of parallel jobs to use for neural net training, at the end
--num-threads 16> # Number of parallel threads per job (will affect results
# as well as speed; may interact with batch size; if you increase
# this, you may want to decrease the batch size.
--parallel-opts "-pe smp 16 -l ram_free=1G,mem_free=1G"> # extra options to pass to e.g. queue.pl for processes that
# use multiple threads... note, you might have to reduce mem_free,ram_free
# versus your defaults, because it gets multiplied by the -pe smp argument.
--io-opts "-tc 10"> # Options given to e.g. queue.pl for jobs that do a lot of I/O.
--minibatch-size 128> # Size of minibatch to process (note: product with --num-threads
# should not get too large, e.g. >2k).
--samples-per-iter <#samples|400000> # Number of samples of data to process per iteration, per
# process.
--splice-width 4> # Number of frames on each side to append for feature input
# (note: we splice processed, typically 40-dimensional frames
--realign-epochs ""> # A list of space-separated epoch indices the beginning of which
# realignment is to be done
--align-cmd (utils/run.pl|utils/queue.pl ) # passed to align.sh
--align-use-gpu (yes/no) # specify is gpu is to be used for realignment
--num-jobs-align <#njobs|30> # Number of jobs to perform realignment
--stage 4> # Used to run a partially-completed training process from somewhere in
# the middle.
ConvNet configurations
--num-filters1 128> # number of filters in the first convolutional layer.
--patch-step1 1> # patch step of the first convolutional layer.
--patch-dim1 7> # dim of convolutional kernel in the first layer.
# (note: (feat-dim - patch-dim1) % patch-step1 should be 0.)
--pool-size 3> # size of pooling after the first convolutional layer.
# (note: (feat-dim - patch-dim1 + 1) % pool-size should be 0.)
--num-filters2 256> # number of filters in the second convolutional layer.
--patch-dim2 4> # dim of convolutional kernel in the second layer.
steps/nnet2/train_convnet_accel2.sh中使用了两个卷积层,第一个卷积层卷积后的结果会经过max-pooling层,再进入第二个卷积层,第二个卷积层以后的结果直接作为后面全连接层的输入。
卷积层相关参数:
参数 | 意义 |
---|---|
num-filters1 | 第一个卷积层的卷积核数目 |
patch-step1 | 第一个卷积层卷积核每次前进的步数 |
patch-dim | 第一个卷积层卷积核的大小(维度) |
pool-size | 池化面积 |
num-filters2 | 第二个卷积层的卷积核数目 |
patch-dim2 | 第二个卷积层的卷积核的大小(维度) |
在steps/nnet2/train_convnet_accel2.sh的脚本中,会根据以上的输入参数配置卷积层:
echo "$0: initializing neural net";
tot_splice=$[($delta_order+1)*($left_context+1+$right_context)]
#添加一阶二阶差分参数以后的特征维度
delta_feat_dim=$[($delta_order+1)*$feat_dim]
#CNN网络输入维度
tot_input_dim=$[$feat_dim*$tot_splice]
#=>一个卷积核卷积后的输出维度
num_patch1=$[1+($feat_dim-$patch_dim1)/$patch_step1]
#=>patch_dim1= --patch-dim1|7 #第一个卷积层的卷积核维度
#=>patch_step1= --patch-step1|1 #第一个卷积层的滤波器步进
#=>patch_stride1= $feat_dim #第一个卷积层输入矩阵/向量的行数
#=>一个卷积核进行池化后的输出维度
num_pool=$[$num_patch1/$pool_size]
#=>多个卷积核经过卷积层后的输出维度
conv_out_dim1=$[$num_filters1*$num_patch1] # 128 x (36 - 7 + 1)
#=>多个卷积核池化后的输出维度
pool_out_dim=$[$num_filters1*$num_pool]
#=>第二个卷积层卷积核维度
patch_dim2=$[$patch_dim2*$num_filters1]
#=>卷积核步进长度
patch_step2=$num_filters1
#=>第二个卷积层的输入矩阵/向量的行数
patch_stride2=$[$num_pool*$num_filters1] # same as pool outputs
#=> num_patch2=$[1+($num_pool-$patch_dim2)]
#=>第二个卷积层一个卷积核的输出维度
num_patch2=$[1+($num_pool*$num_filters1-$patch_dim2)/$patch_step2]
#=>多个滤波器的输出维度
conv_out_dim2=$[$num_filters2*$num_patch2]
计算的结果将用来配置卷积网络:
cat >$dir/nnet.config <$delta_feat_dim
left-context=$left_context
right-context=$right_context
Convolutional1dComponent
input-dim=$tot_input_dim
output-dim=$conv_out_dim1
learning-rate=$initial_lrate
param-stddev=$stddev
bias-stddev=$bias_stddev
patch-dim=$patch_dim1
patch-step=$patch_step1
patch-stride=$feat_dim
MaxpoolingComponent
input-dim=$conv_out_dim1
output-dim=$pool_out_dim
pool-size=$pool_size
pool-stride=$num_filters1
NormalizeComponent
dim=$pool_out_dim
AffineComponentPreconditionedOnline
input-dim=$pool_out_dim
output-dim=$num_leaves
$online_preconditioning_opts
learning-rate=$initial_lrate
param-stddev=0
bias-stddev=0
SoftmaxComponent dim=$num_leaves
EOF
cat >$dir/replace.1.config <$pool_out_dim
output-dim=$conv_out_dim2
learning-rate=$initial_lrate
param-stddev=$stddev
bias-stddev=$bias_stddev
patch-dim=$patch_dim2
patch-step=$patch_step2
patch-stride=$patch_stride2
NormalizeComponent
dim=$conv_out_dim2
AffineComponentPreconditionedOnline
input-dim=$conv_out_dim2
output-dim=$num_leaves
$online_preconditioning_opts
learning-rate=$initial_lrate
param-stddev=0
bias-stddev=0
SoftmaxComponent
dim=$num_leaves
EOF
以上代码会构建卷积网络,这部分有点让人疑惑,因为涉及卷积核特征是怎样展开的,下面是我的个人观点:
SpliceComponent :
对输入特征进行左右展开,目的是为了让网络能够获取到帧间特征的关联性。例如我要识别当前帧是哪个triphone,我可以将当前帧之前5帧和当前帧以后5帧一起构成一个由11个帧组成的特征作为网络输入。
参数 | 意义 | 例子 |
---|---|---|
input-dim | 每一帧特征维度 | input-dim=36*3(一阶差分和二阶差分 |
left-context | 向左展开帧数 | left-context=5 |
right-context | 向左展开帧数 | right-context=5 |
Convolutional1dComponent:
卷积层Component,该层会对输入特征进行卷积运算。
参数 | 作用 | 例子 |
---|---|---|
input-dim | 卷积层的输入特征维度 | 第一层卷积层 : fbank特征的维度[包含差分部分]*(1+left-context+right-context) |
output-dim | output-dim=卷积层输出特征维度 | 跟卷积核的步进大小以及卷积的个数有关; 若: 一个卷积核的输出维度: num_patch1=$[1+($feat_dim-$patch_dim1)/$patch_step1] 卷积核的数目为:num_filters1 则: output-dim=$num_patch1*$num_filters1 |
learning-rate | 网络的学习率,该参数决定网络的收敛速度及稳定性 : 较低,模型学习速度缓慢但稳定,比较容易陷入较差的局部最优点; 较高,模型收敛速度快且能够帮助模型跳过较差的局部最优点但收敛不稳定 |
推荐两种常用的方法: - 根据模型开始训练时选择较高再随着迭代逐渐降低:这样能够让模型在一开始时能够快速收敛到较好的局部最优点,并在较低的学习率下收敛于该局部最优点。 - 根据模型在训练集以及交叉验证集上的error-rate选择,若某轮迭代前后的error-rate差值比上一轮迭代的差值大,说明此处cost-function比较陡,可以增大learning-rate,否则降低lerning-rate。 |
param-stddev | 将参数的标注差限制在一个范围内,防止参数变化过大,该方法有利于防止over-fitting | param-stddev=$stddev |
bias-stddev | 限制bias参数的标注差,其他同上 | bias-stddev=$bias_stddev |
patch-dim | 卷积核的大小(维度) | patch-dim=7 |
patch-step | 卷积核的每次步进大小 | patch-step=1 若大于patch-dim,则卷积运算没有重叠部分。 |
patch-stride | 卷积层会将输入向量特征转换成二维矩阵(类似于图像)进行卷积,该值确定了二维矩阵的行数,同时,卷积核也受该值的影响 | 以kaldi提供核心代码为例: 第一个卷积层输入是一个36*3*11的一维特征向量,令该值等于fbank不包含差分特征的维度(即36),则输入特征向量可转换成一个36*33的特征矩阵,再利用卷积核(7*33)进行卷积。 第二个卷积层的输入是池化层的输出,令该值等于输入的维度,则转换成的特征矩阵仍然是原来的向量。 |
MaxpoolingComponent:
池化层Component,该层会对卷积的特征进行最大化池化,即在一个范围内(池化面积)从同一个卷积核的输出选取最大的一个作为下一层的输入,池化核不重叠。池化的好处除了能够降维以外,更重要的一点是能够去除输入特征中的一些扰动。
参数 | 作用 | 例子 |
---|---|---|
input-dim | 池化层输入维度 | input-dim=$conv_out_dim1 |
output-dim | 池化层输出维度 | output-dim=$pool_out_dim |
pool-size | 池化面积 | pool-size=$pool_size |
pool-stride | 池化范围,此处与卷积层相同,会将向量转换成矩阵进行处理。 | pool-stride=$num_filters1 |
NormalizeComponent :
归一化层,对输入进行归一化。网络训练过程中,输入特征是一个mini-batch,即包含多个特征向量的矩阵。归一化层会对这个mini-batch进行归一化。
参数 | 作用 | 例子 |
---|---|---|
dim | 输入特征维度 | dim=$pool_out_dim |
AffineComponentPreconditionedOnline
全连接层的权重参数层,在kaldi的表示中,一层网络被拆分成权重层和后面的非线性变换层,其中权重层保存了网络的连接参数W,这些参数是可以改变的,而后面的非线性变换层(如下面的SoftmaxComponent)是固定的。
参数 | 作用 | 例子 |
---|---|---|
input-dim | 网络层输入维度 | input-dim=$pool_out_dim |
output-dim | 网络层输出维度 | output-dim=$num_leaves |
learning-rate | 学习率,同Convolutional1dComponent | 同Convolutional1dComponent |
param-stddev | 参数标准差,同Convolutional1dComponent | 同Convolutional1dComponent |
bias-stddev | bias标准差,同Convolutional1dComponent | 同Convolutional1dComponent |
其他参数 | 跟在线预处理有关,暂时没搞懂 | alpha=$alpha num-samples-history=$num_samples_history update-period=$update_period rank-in=$precondition_rank_in rank-out=$precondition_rank_out max-change-per-sample=$max_change_per_sample |
SoftmaxComponent
非线性变换层,这一层一旦定义以后就是固定的了。
参数 | 作用 | 例子 |
---|---|---|
dim | 输入特征维度 | dim=$pool_out_dim |
网络训练其他参数:
参数 | 作用 | 例子 |
---|---|---|
config | 配置文件;但在接下来的训练过程中,并没有用到这个选项,可以暂时忽略 | |
cmd (utils/run.pl|utils/queue.pl ) | 指定任务训练方式,如果单机环境采用run.pl脚本,如果是安装了SGE的集群,则采用queue.pl提交集群任务 | - -cmd “queue.pl -q CPU_QUEUE -l arch=64” 一般此选项内容在cmd.sh中配置 |
num-epochs <#epochs|15> | 整个训练集数据训练的轮次,模型的迭代次数将根据这数字计算得到,这里可暂时理解为同个数据在模型训练过程中被用到的次数 | - -num-epochs 15 |
initial-effective-lrate | 初始时训练网络的学习率,如果采用多任务训练,则实际的学习率是这个数值乘以任务数 | - -initial-effective-lrate 0.02 |
final-effective-lrate | 结束时训练网络的学习率,如果采用多任务训练,则实际的学习率是这个数值乘以任务数 | - -final-effective-lrate 0.001 |
add-layers-period <#iters|2> | 添加网络的迭代间隔,网络起始训练时是采用两个CNN层加一个softmax层这个三层网络,随着训练的进行,会逐渐往第二个卷积层和softmax层间添加全连接网络,这个参数选择会影响网络的更新稳定度。 | - -add-layers-period 2 |
mix-up <#pseudo-gaussians|0> | 在网络输出层前加入一层mixup层,网络的输出层神经元输出概率是mixup层神经元输出概率的加权求和。(可借鉴GMM模型的方法进行类比,mixup层一个节点的网络输出概率是单个高斯的输出概率 P(vl|μi,σi) ;多个节点进行加权求和相当于GMM中的加权求和 ∑Ni=0αiP(vl|μi,σi) ) | - -mix-up=20000 |
num-jobs-initial | 网络开始训练时的任务数,为了训练的稳定性,一般选择较小的任务数开始 | - -num-jobs-initial 3 |
num-jobs-final | 网络结束训练时的任务数,为了训练速度,一般选择较大的任务数结果,网络训练过程中,会根据起始任务数已经结束任务数逐渐增加训练的任务数。 | |
num-threads | 任务内并行线程数目,kaldi集群任务支持任务内并行训练,但如果该值设定超过1,将会使用CPU而不是GPU进行网络训练。 | 若是使用GPU训练则:–num-threads 1 若是使用CPU进行训练则可选为:每个节点CPU数目*每个CPU支持线程数/当前平均每个节点的任务数(–num-jobs) |
parallel-opts | 其他跟队列配置相关的参数(包括内存需求等等) | |
io-opts | 跟磁盘IO相关配置,限制IO操作严重的任务数 | - -io-opts 3 |
minibatch-size | mini-batch 大小,一次前向传播的输入特征数。 | - -minibatch-size 128 |
samples-per-iter <#samples|400000> | 一次迭代(一个轮次里面有多个迭代)的样本数目,这个数值只是起引导作用,脚本会根据实际总的迭代次数计算出样本数目 | - -samples-per-iter 400000 |
splice-width | 当前帧向左右两边拓展作为网络输入的帧数 | - -splice-width 5 |
realign-epochs | 进行数据对齐的轮次,该值应该小于–num-epochs 参数 | - -realign-epochs 8 |
align-cmd (utils/run.pl|utils/queue.pl ) | 跟对齐相关的任务环境,可在cmd.sh中进行定义。 | |
align-use-gpu (yes/no) | 是否对齐的时候使用GPU | - -align-use-gpu yes |
num-jobs-align | 对齐的集群任务数 | - -num-jobs-align 32 |
stage | 训练CNN需要较长时间,如果脚本运行过程中出错或者由于某些原因中断,设置该值可以让脚本从某个步骤重新运行,从而跳过中断前已经顺利完成的任务,避免不必要的重复运行。 | 跟指定的脚本有关。 |
====================================未完待续… ====================================