kaldi學習筆記之卷積神經網絡(CNN)

kaldi學習筆記之卷積神經網絡(CNN)

摘要:

本文將以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的一些步驟進行解讀。)

好,現在開始正式進入話題。

  • 訓練流程:

Created with Raphaël 2.1.0HMM-GMM模型進行標註對齊數據準備CNN模型訓練CNN模型測試


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] <data> <lang> <ali-dir> <exp-dir>
 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>                           # config file containing options
  --cmd (utils/run.pl|utils/queue.pl <queue opts>) # how to run jobs.
  --num-epochs <#epochs|15>                        # Number of epochs of training
  --initial-effective-lrate <lrate|0.02> # effective learning rate at start of training,
                                         # actual learning-rate is this time num-jobs.
  --final-effective-lrate <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 <num-jobs|1>                  # Number of parallel jobs to use for neural net training, at the start.
  --num-jobs-final <num-jobs|8>                    # Number of parallel jobs to use for neural net training, at the end
  --num-threads <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 <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 <opts|"-tc 10">                      # Options given to e.g. queue.pl for jobs that do a lot of I/O.
  --minibatch-size <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 <width|4>                         # Number of frames on each side to append for feature input
                                                   # (note: we splice processed, typically 40-dimensional frames
  --realign-epochs <list-of-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 <queue opts>) # 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 <stage|-4>                               # Used to run a partially-completed training process from somewhere in
                                                   # the middle.
ConvNet configurations
  --num-filters1 <num-filters1|128>                # number of filters in the first convolutional layer.
  --patch-step1 <patch-step1|1>                    # patch step of the first convolutional layer.
  --patch-dim1 <patch-dim1|7>                      # dim of convolutional kernel in the first layer.
                                                   # (note: (feat-dim - patch-dim1) % patch-step1 should be 0.)
  --pool-size <pool-size|3>                        # size of pooling after the first convolutional layer.
                                                   # (note: (feat-dim - patch-dim1 + 1) % pool-size should be 0.)
  --num-filters2 <num-filters2|256>                # number of filters in the second convolutional layer.
  --patch-dim2 <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 <<EOF
SpliceComponent 
    input-dim=$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 <<EOF
Convolutional1dComponent  
    input-dim=$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需要較長時間,如果腳本運行過程中出錯或者由於某些原因中斷,設置該值可以讓腳本從某個步驟重新運行,從而跳過中斷前已經順利完成的任務,避免不必要的重複運行。 跟指定的腳本有關。

====================================未完待續… ====================================

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章