这份文档的目的是帮助您通过构建一个简单的项目快速了解CAT的工作的流程,workflow中已经按顺序整理了每个文件的流程作用,这份文档采用workflow的结构,建议以workflow作为大纲对照阅读。
以下为完整项目的目录结构,通过该目录结构可以了解项目的大致组成。
yesno
├── cmd.sh #使用脚本配置
├── path.sh #环境变量配置
├── run.sh #主程序
├── conf #配置文件存放目录
│ ├── decode_dnn.config #解码
│ ├── fbank.conf #fbank提取
│ └── mfcc.conf #mfcc提取
├── ctc-crf -> ../../scripts/ctc-crf #ctc-crf程序
├── exp #项目文件目录
│ ├── demo #demo项目
│ │ └── config.json #demo项目的训练参数
├── input #输入目录(非必需)
│ └── lexicon.txt #手动准备的词典
├── local #存放主程序运行过程中用到的模块化代码
│ ├── create_yesno_txt.pl #数据预处理waves.txt
│ ├── create_yesno_waves_test_train.pl #数据集划分
│ ├── create_yesno_wav_scp.pl #数据预处理waves.scp
│ ├── get_word_map.pl #对每个词建立映射
│ ├── prepare_data.sh #数据预处理程序
│ ├── prepare_dict.sh #词典预处理程序
│ ├── score.sh #打分脚本(如果需要cer必须编写)
│ ├── yesno_decode_graph.sh #fst文件整理打包程序
│ └── yesno_train_lms.sh #语言模型训练程序
├── steps -> /home/hpl/workspace/kaldi/egs/wsj/s5/steps #kaldi
└── utils -> /home/hpl/workspace/kaldi/egs/wsj/s5/utils
我们将一步步利用CAT和yesno数据搭建一个语音识别项目,请确保您已经完成了CAT的安装和环境配置。
这个部分中,我们先准备好项目的框架。
-
在egs下创建目录yesno
-
编写以下两个个文件
-
path.sh
# CAT toolkit export CAT_ROOT=../../ export PATH=$CAT_ROOT/src/ctc_crf/path_weight/build:$PATH export PATH=$PWD/ctc-crf:$PATH # Kaldi export KALDI_ROOT=${KALDI_ROOT:-/home/hpl/workspace/kaldi} [ -f $KALDI_ROOT/tools/env.sh ] && . $KALDI_ROOT/tools/env.sh export PATH=$PWD/utils/:$KALDI_ROOT/tools/openfst/bin:$PWD:$PATH [ ! -f $KALDI_ROOT/tools/config/common_path.sh ] && echo >&2 "The standard file $KALDI_ROOT/tools/config/common_path.sh is not present -> Exit!" && exit 1 . $KALDI_ROOT/tools/config/common_path.sh export LC_ALL=C # Data export DATA_ROOT=data/yesno
配置全局的环境变量,分别配置CAT、kaldi、数据集的环境变量,代码来源为wsj项目下的同名文件。
创建完后可以在终端里运行一遍
. ./path.sh
方便接下来配置。 -
cmd.sh
export train_cmd=run.pl export decode_cmd=run.pl export mkgraph_cmd=run.pl export cuda_cmd=run.pl
这里也是沿用自kaldi的并行化工具,适应不同的环境可以配置queue.pl等以及不同的参数。此处使用run.pl即可。
-
-
创建链接到kaldi以及cat工具包的目录,便于代码的编写以及迁移
ln -s ../../scripts/ctc-crf ctc-crf ln -s $KALDI_ROOT/egs/wsj/s5/utils utils ln -s $KALDI_ROOT/egs/wsj/s5/steps steps
-
创建local目录,存放本项目专用的数据处理,训练等脚本文件
-
创建run.sh,在run.sh中完成我们整个工作流程的编写
#!/bin/bash # Copyright 2018-2021 Tsinghua University # Author: Siwei Li # yesno for CAT # environment . ./cmd.sh . ./path.sh #set H=`pwd` # home dir n=12 # parallel jobs=$(nproc) stage=1 # set work stages stop_stage=9 yesno=$DATA_ROOT #data root . utils/parse_options.sh NODE=$1 if [ ! $NODE ]; then NODE=0 fi if [ $NODE == 0 ]; then if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then echo "stage 1: *" # work fi #more stages fi
$NODE指实验运行的节点数,若运行run.sh时直接传参节点数,则进入多节点联合训练的环节;否则先进行数据准备,用stage和stop_stage控制运行的代码部分。
自此进入workflow的工作流程,我们按顺序编写每个脚本。
workflow: Data preparation
在step 1,我们完成以下步骤:获取数据,建立词典,训练语言模型。
以下为step 1的代码,在本节中我们会详细解释这部分代码的思路。
if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then
echo "stage 1: Data Preparation and FST Construction"
local/prepare_data.sh || exit 1; # Get data and lists
local/prepare_dict.sh || exit 1; # Get lexicon dict
# Compile the lexicon and token FSTs
# generate lexicon FST L.fst according to words.txt, generate token FST T.fst according to tokens.txt
ctc-crf/ctc_compile_dict_token.sh --dict-type "phn" \
data/dict data/local/lang_phn_tmp data/lang || exit 1;
# Train and compile LMs. Generate G.fst according to lm, and compose FSTs into TLG.fst
local/yesno_train_lms.sh data/train/text data/dict/lexicon.txt data/lm || exit 1;
local/yesno_decode_graph.sh data/lm/srilm/srilm.o1g.kn.gz data/lang data/lang_test || exit 1;
fi
我们将数据准备的步骤集成到prepare_data.sh中,在prepare.sh完成后,我们期望获得以及划分为训练集与开发集的data(wav.scp),说话人信息(spk2utt,utt2spk,均为global),原文(text),分别存储在data/dev,data/train下,你也可以尝试自己实现这部分功能。
-
在local目录下创建文件prepare_data.sh,并获取数据
#!/usr/bin/env bash # This script prepares data and create necessary files . ./path.sh data=${H}/data local=${H}/local mkdir -p ${data}/local cd ${data} # acquire data if not downloaded if [ ! -d waves_yesno ]; then echo "Getting Data" wget http://www.openslr.org/resources/1/waves_yesno.tar.gz || exit 1; tar -xvzf waves_yesno.tar.gz || exit 1; rm waves_yesno.tar.gz || exit 1; fi
这一步完成后,我们在data/waves_yesno下得到原始数据,每个wav文件的标题为该文件的内容。
-
将数据划分为训练集和开发集
注:此处直接将开发集作为测试集,可以修改
echo "Preparing train and dev data" rm -rf train dev # Create waves list and Divide into dev and train set waves_dir=${data}/waves_yesno ls -1 $waves_dir | grep "wav" > ${data}/local/waves_all.list cd ${data}/local ${local}/create_yesno_waves_test_train.pl waves_all.list waves.dev waves.train
create_yesno_waves_test_train.pl
注:这部分代码来源于kaldi中yesno项目
.pl为perl代码,代码特点为简洁高效,文本处理方便,缺点为较难懂。
#!/usr/bin/env perl $full_list = $ARGV[0]; $test_list = $ARGV[1]; $train_list = $ARGV[2]; open FL, $full_list; $nol = 0; while ($l = <FL>) { $nol++; } close FL; $i = 0; open FL, $full_list; open TESTLIST, ">$test_list"; open TRAINLIST, ">$train_list"; while ($l = <FL>) { chomp($l); $i++; if ($i <= $nol/2 ) { print TRAINLIST "$l\n"; } else { print TESTLIST "$l\n"; } }
等分$full_list:waves_all.list到waves.dev, waves.train
-
生成*_wav.scp, *.txt(*代指train, test, dev)
cd ${data}/local for x in train dev; do # create id lists ${local}/create_yesno_wav_scp.pl ${waves_dir} waves.$x > ${x}_wav.scp #id to wavfile ${local}/create_yesno_txt.pl waves.$x > ${x}.txt #id to content done ${local}/create_yesno_wav_scp.pl ${waves_dir} waves.dev > test_wav.scp #id to wavfile ${local}/create_yesno_txt.pl waves.dev > test.txt #id to content
create_yesno_wav_scp.pl
创建*.scp文件,内容为文件名对应的存储位置。
#!/usr/bin/env perl $waves_dir = $ARGV[0]; $in_list = $ARGV[1]; open IL, $in_list; while ($l = <IL>) { chomp($l); $full_path = $waves_dir . "\/" . $l; $l =~ s/\.wav//; print "$l $full_path\n"; }
create_yesno_txt.pl
创建.txt文件,内容为文件名对应的语句内容。
#!/usr/bin/env perl $in_list = $ARGV[0]; open IL, $in_list; while ($l = <IL>) { chomp($l); $l =~ s/\.wav//; $trans = $l; $trans =~ s/0/NO/g; $trans =~ s/1/YES/g; $trans =~ s/\_/ /g; print "$l $trans\n"; }
-
将数据转移到data/dev, data/train, data/test下,并生成utt2spk, spk2utt
for x in train dev test; do # sort wave lists and create utt2spk, spk2utt mkdir -p $x sort local/${x}_wav.scp -o $x/wav.scp sort local/$x.txt -o $x/text cat $x/text | awk '{printf("%s global\n", $1);}' > $x/utt2spk sort $x/utt2spk -o $x/utt2spk ${H}/utils/utt2spk_to_spk2utt.pl < $x/utt2spk > $x/spk2utt done
utils和step目录下的脚本均为kaldi的脚本,在代码目录下打开有详细注释。
这一流程完成后,data下的目录结构为:
├── dev #开发集 │ ├── spk2utt #说话人-音频名 │ ├── text #音频名-文本 │ ├── utt2spk #音频名-说话人 │ └── wav.scp #音频名-文件位置 ├── train #训练集 │ ├── spk2utt │ ├── text │ ├── utt2spk │ └── wav.scp ├── test #测试集 │ ├── spk2utt │ ├── text │ ├── utt2spk │ └── wav.scp ├── local #中间文件 │ ├── dev.txt #开发集的text │ ├── dev_wav.scp #开发集的wav.scp │ ├── test.txt │ ├── test_wav.scp │ ├── train.txt │ ├── train_wav.scp │ ├── waves.dev #开发集的文件名列表 │ ├── waves.train │ └── waves_all.list └── waves_yesno #数据储存位置
以下展示train目录下的文件的部分内容:
spk2utt
[speaker] [wav_name1] [wav_name2] ...
global 0_0_0_0_1_1_1_1 0_0_0_1_0_0_0_1 0_0_0_1_0_1_1_0 0_0_1_0_0_0_1_0 0_0_1_0_0_1_1_0 0_0_1_0_0_1_1_1 0_0_1_0_1_0_0_0 0_0_1_0_1_0_0_1 0_0_1_0_1_0_1_1 0_0_1_1_0_0_0_1 0_0_1_1_0_1_0_0 0_0_1_1_0_1_1_0 0_0_1_1_0_1_1_1 0_0_1_1_1_0_0_0 0_0_1_1_1_0_0_1 0_0_1_1_1_1_0_0 0_0_1_1_1_1_1_0 0_1_0_0_0_1_0_0 0_1_0_0_0_1_1_0 0_1_0_0_1_0_1_0 0_1_0_0_1_0_1_1 0_1_0_1_0_0_0_0 0_1_0_1_1_0_1_0 0_1_0_1_1_1_0_0 0_1_1_0_0_1_1_0 0_1_1_0_0_1_1_1 0_1_1_1_0_0_0_0 0_1_1_1_0_0_1_0 0_1_1_1_0_1_0_1 0_1_1_1_1_0_1_0
utt2spk
[wav_name] [speaker]
0_0_0_0_1_1_1_1 global 0_0_0_1_0_0_0_1 global 0_0_0_1_0_1_1_0 global 0_0_1_0_0_0_1_0 global 0_0_1_0_0_1_1_0 global ...
wav.scp
[wav_name] [wav_location]
0_0_0_0_1_1_1_1 /home/hpl/workspace/CAT/egs/yesno/data/waves_yesno/0_0_0_0_1_1_1_1.wav 0_0_0_1_0_0_0_1 /home/hpl/workspace/CAT/egs/yesno/data/waves_yesno/0_0_0_1_0_0_0_1.wav 0_0_0_1_0_1_1_0 /home/hpl/workspace/CAT/egs/yesno/data/waves_yesno/0_0_0_1_0_1_1_0.wav 0_0_1_0_0_0_1_0 /home/hpl/workspace/CAT/egs/yesno/data/waves_yesno/0_0_1_0_0_0_1_0.wav ...
text
[wav_name] [wav_content]
0_0_0_0_1_1_1_1 NO NO NO NO YES YES YES YES 0_0_0_1_0_0_0_1 NO NO NO YES NO NO NO YES 0_0_0_1_0_1_1_0 NO NO NO YES NO YES YES NO 0_0_1_0_0_0_1_0 NO NO YES NO NO NO YES NO 0_0_1_0_0_1_1_0 NO NO YES NO NO YES YES NO ...
通过生成这些固定格式的文件,我们可以方便地使用kaldi的工具优化工作流程。
你现在的目录结构应该是:
├── cmd.sh ├── ctc-crf -> ../../scripts/ctc-crf ├── data │ ├── dev │ ├── local │ ├── test │ ├── train │ └── waves_yesno ├── local │ ├── create_yesno_txt.pl │ ├── create_yesno_wav_scp.pl │ ├── create_yesno_waves_test_train.pl │ └── prepare_data.sh ├── path.sh ├── run.sh ├── steps -> /home/hpl/workspace/kaldi/egs/wsj/s5/steps └── utils -> /home/hpl/workspace/kaldi/egs/wsj/s5/utils
将词典准备的工作集成到prepare_dict.sh中。
通过这部分代码,我们期待在data/dict下获得经过去重和补充噪音,未知发音等的词典lexicon.txt,排序并用数字标准的音素units.txt,以及用数字标号的词典,lexicon_numbers.txt。
-
自己准备原始词典,在input/lexicon.txt中 (注:#为注释,原文件不包含,后面不再说明)
<SIL> SIL #静音silence YES Y NO N
-
编写local/prepare_dict.sh,详见注释
#!/bin/bash # This script prepares the phoneme-based lexicon. It also generates the list of lexicon units # and represents the lexicon using the indices of the units. dir=${H}/data/dict mkdir -p $dir srcdict=input/lexicon.txt . ./path.sh # Check if lexicon dictionary exists [ ! -f "$srcdict" ] && echo "No such file $srcdict" && exit 1; # Raw dictionary preparation # grep removes SIL, perl removes repeated lexicons cat $srcdict | grep -v "SIL" | \ perl -e 'while(<>){@A = split; if(! $seen{$A[0]}) {$seen{$A[0]} = 1; print $_;}}' \ > $dir/lexicon_raw.txt || exit 1; # Get the set of units in the lexicon without noises # cut: remove words, tr: remove spaces and lines, sort -u: sort and unique cut -d ' ' -f 2- $dir/lexicon_raw.txt | tr ' ' '\n' | sort -u > $dir/units_raw.txt # add noises for lexicons (echo '<SPOKEN_NOISE> <SPN>'; echo '<UNK> <SPN>'; echo '<NOISE> <NSN>'; ) | \ cat - $dir/lexicon_raw.txt | sort | uniq > $dir/lexicon.txt || exit 1; # add noises and number the units (echo '<NSN>'; echo '<SPN>';) | cat - $dir/units_raw.txt | awk '{print $1 " " NR}' > $dir/units.txt # Convert phoneme sequences into the corresponding sequences of units indices, encoded by units.txt utils/sym2int.pl -f 2- $dir/units.txt < $dir/lexicon.txt > $dir/lexicon_numbers.txt echo "Phoneme-based dictionary preparation succeeded"
这一脚本运行完成后,data目录下生成了一个dict文件夹:
├── dict │ ├── lexicon_raw.txt #原词典去重和去非语言学发音 │ ├── units_raw.txt #lexicon_raw词典中的所有音素去重 │ ├── lexicon.txt #lexicon_raw词典加入非语言学发音并排序 │ ├── units.txt #units_raw加入非语言学发音并排序标号 │ └── lexicon_numbers.txt #用units.txt中的音素标号替代词典中的音素
以下展示dict中文件的部分内容:
lexicon_raw.txt
[word] [unit1] [unit2] ...
YES Y NO N
units_raw.txt
[unit]
N Y
lexicon.txt
<NOISE> <NSN> #自然噪音 <SPOKEN_NOISE> <SPN> #说话噪音 <UNK> <SPN> #未知词语,默认为说话噪音 NO N YES Y
units.txt
[unit] [unit_number]
<NSN> 1 <SPN> 2 N 3 Y 4
lexicon_numbers.txt
[word] [unit_number1] [unit_number2] ...
<NOISE> 1 <SPOKEN_NOISE> 2 <UNK> 2 NO 3 YES 4
在yesno数据集中并没有自然噪音和说话噪音,所以你可以修改代码去掉这部分因素,此处加入以便普适性说明。
这时,你需要对FST(Finite State Transducers 有限状态转换器)有一定的了解,安装的openfst正是为了处理这类模型。
根据发音词典,ctc需要的token<eps>,<blk>,生成词典(lexicon)的L.fst以及音素(token)的T.fst,此处用到我们在prepare_dict.sh中准备好的lexicon.txt, units.txt, lexicon_numbers.txt这3个文件。
# Compile the lexicon and token FSTs
# generate lexicon FST L.fst according to words.txt, generate token FST T.fst according to tokens.txt
ctc-crf/ctc_compile_dict_token.sh --dict-type "phn" \
data/dict data/local/lang_phn_tmp data/lang || exit 1;
详见ctc-crf/ctc_compile_dict_token.sh的注释。
fst文件的可视化,参考[https://www.cnblogs.com/welen/p/7611320.html],[https://www.dazhuanlan.com/shitou103/topics/1489883]
这一步中,脚本先通过lexicon_numbers.txt, units.txt生成了words.txt, tokens.txt,然后生成了T.fst, L.fst。
words.txt
<eps> 0 #epsilon,空标签,代表跳转输出标签为空
<NOISE> 1
<SPOKEN_NOISE> 2
<UNK> 3
NO 4
YES 5
#0 6 #语言模型G的回退符,用于跳转后的确定化
<s> 7 #起始
</s> 8 #结束
确定化是指,对于一个fst图,任意输入序列只对应唯一跳转,消歧符号帮助我们确保我们的WFST是确定化的,进一步了解推荐阅读《Kaldi语音识别实战》(作者:陈果果)第五章。
tokens.txt
<eps> 0
<blk> 1
<NSN> 2
<SPN> 3
N 4
Y 5
#0 6 #G.fst回退符
#1 7 #注:#1,#2为对<SPOKEN_NOISE>和<UNK>的消歧
#2 8
#3 9 #sil的消歧
为了方便理解,以下通过fstprint展示我们生成的fst文件:
T.fst
L.fst
为了便于对比理解,我们去掉<NOISE>, <SPOKEN_NOISE>再看一下生成的fst图,此时:
words.txt
<eps> 0
NO 1
YES 2
#0 3
<s> 4
</s> 5
tokens.txt
<eps> 0
<blk> 1
N 2
Y 3
#0 4
#1 5
T.fst
L.fst
根据train/text,dict/lexicon.txt,生成语言模型的G.fst。
这部分训练我们通过srilm工具完成,集成到local/yesno_train_lms.sh中。
# Train and compile LMs. Generate G.fst according to lm, and compose FSTs into TLG.fst
local/yesno_train_lms.sh data/train/text data/dict/lexicon.txt data/lm || exit 1;
yesno_train_lms.sh
#!/bin/bash
# To be run from one directory above this script.
. ./path.sh
text=$1
lexicon=$2
dir=$3
for f in "$text" "$lexicon"; do
[ ! -f $x ] && echo "$0: No such file $f" && exit 1;
done
#text=data/train/text
#lexicon=data/dict/lexicon.txt
#dir=data/lm
mkdir -p $dir
cleantext=$dir/text.no_oov
# Replace unknown words in text by <UNK>
cat $text | awk -v lex=$lexicon 'BEGIN{while((getline<lex) >0){ seen[$1]=1; } }
{for(n=1; n<=NF;n++) { if (seen[$n]) { printf("%s ", $n); } else {printf("<UNK> ");} } printf("\n");}' \
> $cleantext || exit 1;
# Count unique words
cat $cleantext | awk '{for(n=2;n<=NF;n++) print $n; }' | sort | uniq -c | \
sort -nr > $dir/word.counts || exit 1;
# Get counts from acoustic training transcripts, and add one-count
# for each word in the lexicon (but not silence, we don't want it
# in the LM-- we'll add it optionally later).
cat $cleantext | awk '{for(n=2;n<=NF;n++) print $n; }' | \
cat - <(grep -w -v '!SIL' $lexicon | awk '{print $1}') | \
sort | uniq -c | sort -nr > $dir/unigram.counts || exit 1;
# note: we probably won't really make use of <UNK> as there aren't any OOVs
cat $dir/unigram.counts | awk '{print $2}' | ${H}/local/get_word_map.pl "<s>" "</s>" "<UNK>" > $dir/word_map \
|| exit 1;
# note: ignore 1st field of train.txt, it's the utterance-id.
cat $cleantext | awk -v wmap=$dir/word_map 'BEGIN{while((getline<wmap)>0)map[$1]=$2;}
{ for(n=2;n<=NF;n++) { printf map[$n]; if(n<NF){ printf " "; } else { print ""; }}}' | gzip -c >$dir/train.gz \
|| exit 1;
# LM is small enough that we don't need to prune it (only about 0.7M N-grams).
# From here is some commands to do a baseline with SRILM (assuming
# you have it installed).
heldout_sent=3
sdir=$dir/srilm
mkdir -p $sdir
cat $cleantext | awk '{for(n=2;n<=NF;n++){ printf $n; if(n<NF) printf " "; else print ""; }}' | \
head -$heldout_sent > $sdir/heldout
cat $cleantext | awk '{for(n=2;n<=NF;n++){ printf $n; if(n<NF) printf " "; else print ""; }}' | \
tail -n +$heldout_sent > $sdir/train
cat $dir/word_map | awk '{print $1}' | cat - <(echo "<s>"; echo "</s>" ) > $sdir/wordlist
ngram-count -text $sdir/train -order 1 -limit-vocab -vocab $sdir/wordlist -unk \
-map-unk "<UNK>" -interpolate -lm $sdir/srilm.o1g.kn.gz
# -kndiscount
ngram -lm $sdir/srilm.o1g.kn.gz -ppl $sdir/heldout
取3句计算困惑度,运行结果如下:
file data/lm/srilm/heldout: 3 sentences, 24 words, 0 OOVs
0 zeroprobs, logprob= -11.09502 ppl= 2.575885 ppl1= 2.899294
srilm工具的使用可以见工具的readme,训练中需要处理的文件储存在data/lm目录下,我们将srilm的训练结果存储在data/lm/srilm下,使用1-gram的语言模型结果储存到srilm.o1g.kn中,语言模型如下:
srilm.o1g.km
\data\
ngram 1=7
\1-grams:
-0.9542425 </s>
-99 <NOISE>
-99 <SPOKEN_NOISE>
-99 <UNK>
-99 <s>
-0.3079789 NO
-0.4014005 YES
\end\
使用n-gram作为语言模型时,习惯上用以上的arpa格式表示,以上[value] [word]的形式意义为logP(word)=value,画图如下:
G.fst
把以上生成的fst文件合成到TLG.fst中。
local/yesno_decode_graph.sh data/lm/srilm/srilm.o1g.kn.gz data/lang data/lang_test || exit 1;
这部分代码中,我们先将语言模型根据word.txt打包到G.fst中,因为arpa格式的语言模型已经足够直观,此处不再做可视化,然后用openfst合成TLG.fst用于训练。
yesno_decode_graph.sh
#!/bin/bash
#
if [ -f path.sh ]; then . path.sh; fi
#lm_dir=$1
arpa_lm=$1
src_lang=$2
tgt_lang=$3
#arpa_lm=${lm_dir}/3gram-mincount/lm_unpruned.gz
[ ! -f $arpa_lm ] && echo No such file $arpa_lm && exit 1;
rm -rf $tgt_lang
cp -r $src_lang $tgt_lang
# Compose the language model to FST
gunzip -c "$arpa_lm" | \
grep -v '<s> <s>' | \
grep -v '</s> <s>' | \
grep -v '</s> </s>' | \
arpa2fst - | fstprint | \
utils/remove_oovs.pl /dev/null | \
utils/eps2disambig.pl | utils/s2eps.pl | fstcompile --isymbols=$tgt_lang/words.txt \
--osymbols=$tgt_lang/words.txt --keep_isymbols=false --keep_osymbols=false | \
fstrmepsilon | fstarcsort --sort_type=ilabel > $tgt_lang/G.fst
echo "Checking how stochastic G is (the first of these numbers should be small):"
fstisstochastic $tgt_lang/G.fst
# Compose the token, lexicon and language-model FST into the final decoding graph
fsttablecompose $tgt_lang/L.fst $tgt_lang/G.fst | fstdeterminizestar --use-log=true | \
fstminimizeencoded | fstarcsort --sort_type=ilabel > $tgt_lang/LG.fst || exit 1;
fsttablecompose $tgt_lang/T.fst $tgt_lang/LG.fst > $tgt_lang/TLG.fst || exit 1;
echo "Composing decoding graph TLG.fst succeeded"
rm -r $tgt_lang/LG.fst # We don't need to keep this intermediate FST
到此,我们完成了样本文件的准备以及TLG.fst的生成,TLG.fst画图如下:
现在你的data目录结构应该如下:
├── dev
│ ├── spk2utt
│ ├── text
│ ├── utt2spk
│ └── wav.scp
├── test
│ ...
├── train
│ ...
├── dict
│ ├── lexicon_numbers.txt
│ ├── lexicon_raw.txt
│ ├── lexicon.txt
│ ├── units_raw.txt
│ └── units.txt
├── lang
│ ├── lexicon_numbers.txt
│ ├── L.fst
│ ├── T.fst
│ ├── tokens.txt
│ ├── units.txt
│ └── words.txt
├── lang_test
│ ├── G.fst
│ ├── lexicon_numbers.txt
│ ├── L.fst
│ ├── T.fst
│ ├── TLG.fst
│ ├── tokens.txt
│ ├── units.txt
│ └── words.txt
├── lm
│ ├── srilm
│ ├── text.no_oov
│ ├── train.gz
│ ├── unigram.counts
│ ├── word.counts
│ └── word_map
├── local
│ ├── dev.txt
│ ├── dev_wav.scp
│ ├── lang_phn_tmp
│ ├── test.txt
│ ├── test_wav.scp
│ ├── train.txt
│ ├── train_wav.scp
│ ├── waves_all.list
│ ├── waves.dev
│ └── waves.train
└── waves_yesno
请再次确认你是否理解这个目录结构中每个文件的来源和意义。
关于词典文件的说明较为简略,希望进一步了解每一个文件的意义,请阅读kaldi文档。
workflow: Feature extraction
第二步我们提取声音文件的特征,这一部分中我对音频进行变速并在fbank文件夹下得到提取完成的音频的FBank特征。
注意在conf目录下建立fbank.conf文件,内容为:
--sample-frequency=8000
--num-mel-bins=40
分别为音频采样率和滤波器个数,yesno数据集数据采样率为8000,滤波器个数我们取40。
关于FBank:[https://www.jianshu.com/p/b25abb28b6f8]
if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then
echo "stage 2: FBank Feature Generation"
#perturb the speaking speed to achieve data augmentation
utils/data/perturb_data_dir_speed_3way.sh data/train data/train_sp
utils/data/perturb_data_dir_speed_3way.sh data/dev data/dev_sp
# Generate the fbank features; by default 40-dimensional fbanks on each frame
fbankdir=fbank
for set in train_sp dev_sp; do
steps/make_fbank.sh --cmd "$train_cmd" --nj 1 data/$set exp/make_fbank/$set $fbankdir || exit 1;
utils/fix_data_dir.sh data/$set || exit; #filter and sort the data files
steps/compute_cmvn_stats.sh data/$set exp/make_fbank/$set $fbankdir || exit 1; #achieve cmvn normalization
done
for set in test; do
steps/make_fbank.sh --cmd "$train_cmd" --nj 1 data/$set exp/make_fbank/$set $fbankdir || exit 1;
utils/fix_data_dir.sh data/$set || exit; #filter and sort the data files
steps/compute_cmvn_stats.sh data/$set exp/make_fbank/$set $fbankdir || exit 1; #achieve cmvn normalization
done
fi
在提取声音文件的特征时,此处使用了将声音进行0.9 1.0 1.1三种变速的操作,在一些数据集上可以取得更好的WER,yesno上无需采用该操作,此处使用该代码作为演示。
utils/data/perturb_data_dir_speed_3way.sh:变速
steps/make_fbank.sh:fbank提取
utils/fix_data_dir.sh:数据排序和过滤
steps/compute_cmvn_stats.sh:特征正则化
workflow: Denominator LM preparation
在这一部分过程中,我们先得到得到标号储存的数据文件,并通过计算基于音素的语言模型和音素得到den_lm.fst,由此和数据文件联合计算lable序列中的logp(l)。详细的步骤内容见注释。
data_tr=data/train_sp
data_cv=data/dev_sp
if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then
#convert word sequences to label sequences according to lexicon_numbers.txt and text files in data/lang_phn
#the result will be placed in $data_tr/ and $data_cv/
ctc-crf/prep_ctc_trans.py data/lang/lexicon_numbers.txt $data_tr/text "<UNK>" > $data_tr/text_number
ctc-crf/prep_ctc_trans.py data/lang/lexicon_numbers.txt $data_cv/text "<UNK>" > $data_cv/text_number
echo "convert text_number finished"
# prepare denominator
ctc-crf/prep_ctc_trans.py data/lang/lexicon_numbers.txt data/train/text "<UNK>" > data/train/text_number
#sort the text_number file, and then remove the duplicate lines
cat data/train/text_number | sort -k 2 | uniq -f 1 > data/train/unique_text_number
mkdir -p data/den_meta
#generate phone_lm.fst, a phone-based language model
chain-est-phone-lm ark:data/train/unique_text_number data/den_meta/phone_lm.fst
#generate the correct T.fst, called T_den.fst
ctc-crf/ctc_token_fst_corrected.py den data/lang/tokens.txt | fstcompile | fstarcsort --sort_type=olabel > data/den_meta/T_den.fst
#compose T_den.fst and phone_lm.fst into den_lm.fst
fstcompose data/den_meta/T_den.fst data/den_meta/phone_lm.fst > data/den_meta/den_lm.fst
echo "prepare denominator finished"
#calculate and save the weight for each label sequence based on text_number and phone_lm.fst
path_weight $data_tr/text_number data/den_meta/phone_lm.fst > $data_tr/weight
path_weight $data_cv/text_number data/den_meta/phone_lm.fst > $data_cv/weight
echo "prepare weight finished"
fi
workflow: Neural network training preparation
不同项目中,这部分处理差别不大,我们对数据集的的特征进行正则化并和以上计算的weights一起整合到data/pickle下。
if [ $stage -le 4 ] && [ $stop_stage -ge 4 ]; then
mkdir -p data/all_ark
for set in test; do
eval data_$set=data/$set
done
for set in test cv tr; do
tmp_data=`eval echo '$'data_$set`
#apply CMVN feature normalization, calculate delta features, then sub-sample the input feature sequence
feats="ark,s,cs:apply-cmvn --norm-vars=true --utt2spk=ark:$tmp_data/utt2spk scp:$tmp_data/cmvn.scp scp:$tmp_data/feats.scp ark:- \
| add-deltas ark:- ark:- | subsample-feats --n=3 ark:- ark:- |"
ark_dir=$(readlink -f data/all_ark)/$set.ark
#copy feature files, generate scp and ark files to save features.
copy-feats "$feats" "ark,scp:$ark_dir,data/all_ark/$set.scp" || exit 1
done
fi
if [ $stage -le 5 ] && [ $stop_stage -ge 5 ]; then
mkdir -p data/pickle
#create a pickle file to save the feature, text_number and path weights.
python3 ctc-crf/convert_to.py -f=pickle -W \
data/all_ark/cv.scp $data_cv/text_number $data_cv/weight data/pickle/cv.pickle || exit 1
python3 ctc-crf/convert_to.py -f=pickle \
data/all_ark/tr.scp $data_tr/text_number $data_tr/weight data/pickle/tr.pickle || exit 1
fi
在stage5结束后,用fi结束最开始if [ $NODE == 0 ]; then
的大括号,进入到训练部分。
workflow: Model training
此时训练需要的所有数据已经准备完成,剩下只需要在exp下建立你的一次训练的文件夹(例:demo),建立config.json,此处yesno我们采用:
{
"net": {
"type": "BLSTM",
"lossfn": "crf",
"lamb": 0.01,
"kwargs": {
"n_layers": 3,
"idim": 120,
"hdim": 320,
"num_classes": 5,
"dropout": 0.5
}
},
"scheduler": {
"type": "SchedulerEarlyStop",
"optimizer": {
"type_optim": "Adam",
"kwargs": {
"lr": 1e-3,
"betas": [
0.9,
0.99
],
"weight_decay": 0.0
}
},
"kwargs": {
"epoch_min": 4,
"lr_stop": 1e-5,
"reverse_metric_direc": true
}
}
}
训练的代码如下:
PARENTDIR='.'
dir="exp/demo"
DATAPATH=$PARENTDIR/data/
if [ $stage -le 6 ] && [ $stop_stage -ge 6 ]; then
unset CUDA_VISIBLE_DEVICES
if [[ $NODE == 0 && ! -f $dir/scripts.tar.gz ]]; then
echo ""
tar -zcf $dir/scripts.tar.gz $(readlink ctc-crf) $0
elif [ $NODE == 0 ]; then
echo ""
echo "'$dir/scripts.tar.gz' already exists."
echo "If you want to update it, please manually rm it then re-run this script."
fi
# uncomment the following line if you want to use specified GPUs
CUDA_VISIBLE_DEVICES="0" \
python3 ctc-crf/train.py --seed=0 \
--world-size 1 --rank $NODE \
--batch_size=3 \
--dir=$dir \
--config=$dir/config.json \
--data=$DATAPATH \
|| exit 1
fi
通过以上代码即可完成训练,训练的过程可以在你的demo目录下的monitor.jpg中找到。
如果需要重新训练,删除scripts.tar.gz和ckpt文件夹即可,yesno数据集的训练可能不太稳定,如果训练集loss不下降,可以考虑重新训练。
workflow: Decoding
计算测试集的logits并解码。
nj=1
if [ $stage -le 7 ] && [ $stop_stage -ge 7 ]; then
for set in test; do
ark_dir=$dir/logits/$set
mkdir -p $ark_dir
python3 ctc-crf/calculate_logits.py \
--resume=$dir/ckpt/infer.pt \
--config=$dir/config.json \
--nj=$nj --input_scp=data/all_ark/$set.scp \
--output_dir=$ark_dir \
|| exit 1
done
fi
if [ $stage -le 8 ] && [ $stop_stage -ge 8 ]; then
for set in test; do
mkdir -p $dir/decode_${set}
ln -s $(readlink -f $dir/logits/$set) $dir/decode_${set}/logits
ctc-crf/decode.sh --stage 1 \
--cmd "$decode_cmd" --nj 1 --acwt 1.0 --post_decode_acwt 1.0\
data/lang_${set} data/${set} data/all_ark/${set}.scp $dir/decode_${set}
done
fi
if [ $stage -le 9 ] && [ $stop_stage -ge 9 ]; then
for set in test; do
grep WER $dir/decode_${set}/wer_* | utils/best_wer.sh
done
fi
恭喜你已经完成了你的第一个yesno项目的搭建,训练和解码。
现在你的目录结构应该如下图所示:
├── cmd.sh
├── conf
│ ├── decode_dnn.config
│ ├── fbank.conf
│ └── mfcc.conf
├── ctc-crf -> ../../scripts/ctc-crf
├── exp
│ └── demo
├── input
│ └── lexicon.txt
├── local
│ ├── create_yesno_txt.pl
│ ├── create_yesno_waves_test_train.pl
│ ├── create_yesno_wav_scp.pl
│ ├── get_word_map.pl
│ ├── prepare_data.sh
│ ├── prepare_dict.sh
│ ├── score.sh
│ ├── yesno_decode_graph.sh
│ └── yesno_train_lms.sh
├── path.sh
├── run.sh
├── steps -> /home/hpl/workspace/kaldi/egs/wsj/s5/steps
└── utils -> /home/hpl/workspace/kaldi/egs/wsj/s5/utils
以下是其中一次训练的结果展示:
识别的结果如下:
%WER 5.00 [ 12 / 240, 0 ins, 9 del, 3 sub ]
识别的详细log在exp/demo/decode_test中。
在训练完成后,请在demo文件夹下自动生成的readme.md文件中对你的这次实验进行记录。