kaldi的安装和使用案例(librispeech)

1. kaldi的安装

按照官网教程,kaldi的安装首先通过git获取项目,再进行编译。

git clone https://github.com/kaldi-asr/kaldi.git
cd kaldi/tools/; make; cd ../src; ./configure; make

如果报错,则可能是相关的依赖项没有安装,可按照提示一步步安装(需要root权限)。

sudo apt-get install zlib1g-dev automake autoconf sox subversion
sudo bash extras/install_mkl.sh

2. kaldi的使用

2.1 librispeech的ASR模型训练

egs目录下放着各个数据库的样例代码,一个文件夹就是一个数据库,非常全面。进入egs/librispeech/s5/.每个代码里边都会有一份cmd.sh(引入单机多卡run.pl或者多机多卡quene.pl模式), path.sh(引入各种kaldi的路径), run.sh(训练及测试的整个主流程)。以下主要细看run.sh,整体流程为 导入参数->下载部分数据并预处理->准备并创建语言模型->提取特征->训练部分数据集->训练单因素、三音素模型并变换训练->加入更多数据集->变换训练->加入全部数据集->变换训练->解码->训练tdnn模型。具体如下:

#!/usr/bin/env bash

## 导入参数
data=/home/fwq/Project/kaldi/kaldi/data

data_url=www.openslr.org/resources/12
lm_url=www.openslr.org/resources/11
mfccdir=mfcc
stage=1

. ./cmd.sh
. ./path.sh
. parse_options.sh

set -e

## 下载数据
if [ $stage -le 1 ]; then
  for part in dev-clean test-clean dev-other test-other train-clean-100; do
    local/download_and_untar.sh $data $data_url $part
  done


  local/download_lm.sh $lm_url data/local/lm
fi

## 生成各种数据的各种文件,如wav.scp,text,utt2spk,spk2gender,utt2dur
if [ $stage -le 2 ]; then
  for part in dev-clean test-clean dev-other test-other train-clean-100; do
    local/data_prep.sh $data/LibriSpeech/$part data/$(echo $part | sed s/-/_/g)
  done
fi

## 准备语言模型,准备字典(local/prepare_dict_sh),准备语言相关数据(utils/prepare_lang.sh),格式化数据(local/format_lms.sh)
if [ $stage -le 3 ]; then
  local/prepare_dict.sh --stage 3 --nj 30 --cmd "$train_cmd" \
   data/local/lm data/local/lm data/local/dict_nosp

  utils/prepare_lang.sh data/local/dict_nosp \
   "<UNK>" data/local/lang_tmp_nosp data/lang_nosp

  local/format_lms.sh --src-dir data/lang_nosp data/local/lm
fi

## 用3-gram和4-gram语言模型创建ConstArpaLm格式语言模型
if [ $stage -le 4 ]; then
  utils/build_const_arpa_lm.sh data/local/lm/lm_tglarge.arpa.gz \
    data/lang_nosp data/lang_nosp_test_tglarge
  utils/build_const_arpa_lm.sh data/local/lm/lm_fglarge.arpa.gz \
    data/lang_nosp data/lang_nosp_test_fglarge
fi

## 数据特征提取,提取mfcc,计算每条wav文件的均值方差
if [ $stage -le 5 ]; then
  if [[  $(hostname -f) ==  *.clsp.jhu.edu ]]; then
    utils/create_split_dir.pl /export/b{02,11,12,13}/$USER/kaldi-data/egs/librispeech/s5/$mfcc/storage \
     $mfccdir/storage
  fi
fi

if [ $stage -le 6 ]; then
  for part in dev_clean test_clean dev_other test_other train_clean_100; do
    steps/make_mfcc.sh --cmd "$train_cmd" --nj 40 data/$part exp/make_mfcc/$part $mfccdir
    steps/compute_cmvn_stats.sh data/$part exp/make_mfcc/$part $mfccdir
  done
fi

## 训练100小时的小数据集
if [ $stage -le 7 ]; then

  utils/subset_data_dir.sh --shortest data/train_clean_100 2000 data/train_2kshort
  utils/subset_data_dir.sh data/train_clean_100 5000 data/train_5k
  utils/subset_data_dir.sh data/train_clean_100 10000 data/train_10k
fi

## 训练单音素模型(mono)
if [ $stage -le 8 ]; then
  steps/train_mono.sh --boost-silence 1.25 --nj 20 --cmd "$train_cmd" \
                      data/train_2kshort data/lang_nosp exp/mono
fi

## 对齐,训练三音素模型(tri1)
if [ $stage -le 9 ]; then
  steps/align_si.sh --boost-silence 1.25 --nj 10 --cmd "$train_cmd" \
                    data/train_5k data/lang_nosp exp/mono exp/mono_ali_5k

  steps/train_deltas.sh --boost-silence 1.25 --cmd "$train_cmd" \
                        2000 10000 data/train_5k data/lang_nosp exp/mono_ali_5k exp/tri1
fi

## 对齐,对三音素做LDA+MLLT变换(tri2b)
if [ $stage -le 10 ]; then
  steps/align_si.sh --nj 10 --cmd "$train_cmd" \
                    data/train_10k data/lang_nosp exp/tri1 exp/tri1_ali_10k


  steps/train_lda_mllt.sh --cmd "$train_cmd" \
                          --splice-opts "--left-context=3 --right-context=3" 2500 15000 \
                          data/train_10k data/lang_nosp exp/tri1_ali_10k exp/tri2b
fi

## 对齐,对三音素做LDA+MLLT+SAT变换(tri3b)
if [ $stage -le 11 ]; then
  steps/align_si.sh  --nj 10 --cmd "$train_cmd" --use-graphs true \
                     data/train_10k data/lang_nosp exp/tri2b exp/tri2b_ali_10k

  steps/train_sat.sh --cmd "$train_cmd" 2500 15000 \
                     data/train_10k data/lang_nosp exp/tri2b_ali_10k exp/tri3b

fi

## 对齐,对三音素做LDA+MLLT+SAT变换(tri4b)
if [ $stage -le 12 ]; then
  steps/align_fmllr.sh --nj 20 --cmd "$train_cmd" \
    data/train_clean_100 data/lang_nosp \
    exp/tri3b exp/tri3b_ali_clean_100

  steps/train_sat.sh  --cmd "$train_cmd" 4200 40000 \
                      data/train_clean_100 data/lang_nosp \
                      exp/tri3b_ali_clean_100 exp/tri4b
fi

## 从训练数据中计算发音和静音概率,并重新创建lang目录
if [ $stage -le 13 ]; then
  steps/get_prons.sh --cmd "$train_cmd" \
                     data/train_clean_100 data/lang_nosp exp/tri4b
  utils/dict_dir_add_pronprobs.sh --max-normalize true \
                                  data/local/dict_nosp \
                                  exp/tri4b/pron_counts_nowb.txt exp/tri4b/sil_counts_nowb.txt \
                                  exp/tri4b/pron_bigram_counts_nowb.txt data/local/dict

  utils/prepare_lang.sh data/local/dict \
                        "<UNK>" data/local/lang_tmp data/lang
  local/format_lms.sh --src-dir data/lang data/local/lm

  utils/build_const_arpa_lm.sh \
    data/local/lm/lm_tglarge.arpa.gz data/lang data/lang_test_tglarge
  utils/build_const_arpa_lm.sh \
    data/local/lm/lm_fglarge.arpa.gz data/lang data/lang_test_fglarge
fi

## 对齐,训练nnet2模型,现在已经不这么用了,所以and了个false
if [ $stage -le 14 ] && false; then
  steps/align_fmllr.sh --nj 30 --cmd "$train_cmd" \
    data/train_clean_100 data/lang exp/tri4b exp/tri4b_ali_clean_100

  local/nnet2/run_5a_clean_100.sh
fi

## 合并360小时的数据,变成460小时
if [ $stage -le 15 ]; then
  local/download_and_untar.sh $data $data_url train-clean-360

  local/data_prep.sh \
    $data/LibriSpeech/train-clean-360 data/train_clean_360
  steps/make_mfcc.sh --cmd "$train_cmd" --nj 40 data/train_clean_360 \
                     exp/make_mfcc/train_clean_360 $mfccdir
  steps/compute_cmvn_stats.sh \
    data/train_clean_360 exp/make_mfcc/train_clean_360 $mfccdir

  utils/combine_data.sh \
    data/train_clean_460 data/train_clean_100 data/train_clean_360
fi

## 对齐,做LDA+MLLT+SAT变换(tri5b)
if [ $stage -le 16 ]; then
  steps/align_fmllr.sh --nj 40 --cmd "$train_cmd" \
                       data/train_clean_460 data/lang exp/tri4b exp/tri4b_ali_clean_460

  steps/train_sat.sh  --cmd "$train_cmd" 5000 100000 \
                      data/train_clean_460 data/lang exp/tri4b_ali_clean_460 exp/tri5b
fi
#local/nnet2/run_6a_clean_460.sh

## 合并500小时的数据,变成960小时
if [ $stage -le 17 ]; then
  local/download_and_untar.sh $data $data_url train-other-500

  local/data_prep.sh \
    $data/LibriSpeech/train-other-500 data/train_other_500
  steps/make_mfcc.sh --cmd "$train_cmd" --nj 40 data/train_other_500 \
                     exp/make_mfcc/train_other_500 $mfccdir
  steps/compute_cmvn_stats.sh \
    data/train_other_500 exp/make_mfcc/train_other_500 $mfccdir

  utils/combine_data.sh \
    data/train_960 data/train_clean_460 data/train_other_500
fi

## 对齐,做LDA+MLLT+SAT变换(tri6b),解码
if [ $stage -le 18 ]; then
  steps/align_fmllr.sh --nj 40 --cmd "$train_cmd" \
                       data/train_960 data/lang exp/tri5b exp/tri5b_ali_960

  steps/train_quick.sh --cmd "$train_cmd" \
                       7000 150000 data/train_960 data/lang exp/tri5b_ali_960 exp/tri6b

  utils/mkgraph.sh data/lang_test_tgsmall \
                   exp/tri6b exp/tri6b/graph_tgsmall
  for test in test_clean test_other dev_clean dev_other; do
      steps/decode_fmllr.sh --nj 20 --cmd "$decode_cmd" \
                            exp/tri6b/graph_tgsmall data/$test exp/tri6b/decode_tgsmall_$test
      steps/lmrescore.sh --cmd "$decode_cmd" data/lang_test_{tgsmall,tgmed} \
                         data/$test exp/tri6b/decode_{tgsmall,tgmed}_$test
      steps/lmrescore_const_arpa.sh \
        --cmd "$decode_cmd" data/lang_test_{tgsmall,tglarge} \
        data/$test exp/tri6b/decode_{tgsmall,tglarge}_$test
      steps/lmrescore_const_arpa.sh \
        --cmd "$decode_cmd" data/lang_test_{tgsmall,fglarge} \
        data/$test exp/tri6b/decode_{tgsmall,fglarge}_$test
  done
fi

## 划分“好”的数据来训练数据(tri6b_cleaned)
if [ $stage -le 19 ]; then
  local/run_cleanup_segmentation.sh
fi

## 训练和测试nnet3 tdnn模型
if [ $stage -le 20 ]; then
  local/chain/run_tdnn.sh
fi

2.2 使用预训练模型测试自己的数据集

首先,新建自己数据库的文件夹,并设置steps、utils、rnnlm的软链接。

ln -s /home/fwq/Project/kaldi/kaldi/egs/wsj/s5/utils utils
ln -s /home/fwq/Project/kaldi/kaldi/egs/wsj/s5/steps steps
ln -s /home/fwq/Project/kaldi/kaldi/scripts/rnnlm rnnlm

然后开始准备自己的数据库,kaldi需要的文件如下,这部分需要根据自己的数据库格式来编写生成,放置在data/corpus_name/里,以下corpus命名为test。

  1. wav.scp:此列表包含系统中的语音ID和相应的WAV位置
  2. utt2spk:话语ID和相应的说话者ID的列表。如果您没有发言人信息,则可以将utt-id复制为spk-id。
  3. text:话语的转录。这将需要对您的解码输出进行评分。

再对数据进行排序、复制等处理。

utils/fix_data_dir.sh data/test
utils/utt2spk_to_spk2utt.pl data/test/utt2spk > data/test/spk2utt
for datadir in test; do
    utils/copy_data_dir.sh data/$datadir data/${datadir}_hires
done

有了数据,就要准备生成mfcc特征,需要新建一个conf文件夹,并新建conf/mfcc_hires.conf的配置文件,添加如下:

-use-energy=false   # use average of log energy, not energy.
--num-mel-bins=40     # similar to Google's setup.
--num-ceps=40     # there is no dimensionality reduction.
--low-freq=20     # low cutoff frequency for mel bins... this is high-bandwidth data, so
                  # there might be some information at the low end.
--high-freq=-400 # high cutoff frequently, relative to Nyquist of 8000 (=7600)

然后就可以为数据计算特征和CMVN统计信息。

for datadir in test; do
    steps/make_mfcc.sh --nj 20 --mfcc-config conf/mfcc_hires.conf --cmd "$train_cmd" data/${datadir}_hires
    steps/compute_cmvn_stats.sh data/${datadir}_hires
    utils/fix_data_dir.sh data/${datadir}_hires
done

接下来是预训练模型的下载和导入。默认情况下,内容将提取到data和exp目录。这里提供了2种语言模型:(tgsmall小三元组模型)和rnnlm(基于LSTM),这两种语言模型都经过LibriSpeech训练转录本的训练。我们将使用tgsmall模型进行解码,并使用RNNLM进行记录。

wget http://kaldi-asr.org/models/13/0013_librispeech_v1_chain.tar.gz
wget http://kaldi-asr.org/models/13/0013_librispeech_v1_extractor.tar.gz
wget http://kaldi-asr.org/models/13/0013_librispeech_v1_lm.tar.gz
tar -xvzf 0013_librispeech_v1_chain.tar.gz
tar -xvzf 0013_librispeech_v1_extractor.tar.gz
tar -xvzf 0013_librispeech_v1_lm.tar.gz

使用i-vector提取器来获取测试数据的i-vector。 这会将100维i-向量提取到exp/nnet3_cleaned。

for data in test; do
    nspk=$(wc -l <data/${data}_hires/spk2utt)
    steps/online/nnet2/extract_ivectors_online.sh --cmd "$train_cmd" --nj "${nspk}" \
      data/${data}_hires exp/nnet3_cleaned/extractor \
      exp/nnet3_cleaned/ivectors_${data}_hires
done

使用tgsmallLM创建解码图。

export dir=exp/chain_cleaned/tdnn_1d_sp
export graph_dir=$dir/graph_tgsmall
utils/mkgraph.sh --self-loop-scale 1.0 --remove-oov \
  data/lang_test_tgsmall $dir $graph_dir

使用创建的图形进行解码。

export decode_cmd="run.pl"
for decode_set in test; do
  steps/nnet3/decode.sh --acwt 1.0 --post-decode-acwt 10.0 \
    --nj 8 --cmd "$decode_cmd" \
    --online-ivector-dir exp/nnet3_cleaned/ivectors_${decode_set}_hires \
    $graph_dir data/${decode_set}_hires $dir/decode_${decode_set}_tgsmall
done

在核对之前检查WER。在这里,我们使用sclite评分,这在Kaldi中可用,并用于大多数egs。

for decode_set in test; do
  steps/score_kaldi.sh --cmd "run.pl" data/${decode_set}_hires $graph_dir $dir/decode_${decode_set}_tgsmall
done
cat exp/chain_cleaned/tdnn_1d_sp/decode_test_tgsmall/scoring_kaldi/best_wer
%WER 57.15 [ 14722 / 25761, 2501 ins, 2559 del, 9662 sub ] exp/chain_cleaned/tdnn_1d_sp/decode_test_tgsmall/wer_17_1.0

使用RNNLM重新评分。

export decode_cmd="run.pl"
for decode_set in test; do
    decode_dir=exp/chain_cleaned/tdnn_1d_sp/decode_${decode_set}_tgsmall;
    rnnlm/lmrescore_pruned.sh \
        --cmd "$decode_cmd" \
        --weight 0.45 --max-ngram-order 4 \
        data/lang_test_tgsmall exp/rnnlm_lstm_1a \
        data/${decode_set}_hires ${decode_dir} \
        exp/chain_cleaned/tdnn_1d_sp/decode_${decode_set}_rescore
done

计分包含在lmrescore_pruned.sh脚本中。

cat exp/chain_cleaned/tdnn_1d_sp/decode_test_rescore/wer_17_1.0
# %WER 56.12 [ 14456 / 25761, 2607 ins, 2452 del, 9397 sub ]

3. kaldi使用感受

不同于以往用的python,kaldi的初步使用通过shell脚本来实现,它基于c++的底层,通过社区不同发展,现在已经有了非常庞大的脚本库。很多函数都实现了高效的封装,但如果要自己提特征训模型的话,还需要细看shell代码进一步看c++代码。

4. 参考文献

参考1
参考2