這篇文章是學(xué)習(xí)Kaldi的第二篇。對應(yīng)SUSTech CS310課程的Lab6和Lab7。
第一篇里探索了如何對toy language(僅包含兩個單音素單詞)進(jìn)行語言模型的建模。至于訓(xùn)練和解碼的部分,時間條件和理解能力暫時不允許去整理。
本篇文章的主要目標(biāo)是理解復(fù)雜的中文多音素語言模型和使用AiShell語料集來真實(shí)的訓(xùn)練出一個可用的中文語音識別模型。完整的AiShell例子包含GMM-HMM和神經(jīng)網(wǎng)絡(luò)。Lab6先展示了GMM-HMM后的結(jié)果。Lab7則補(bǔ)充了神經(jīng)網(wǎng)絡(luò)。
AiShell描述和下載
AiShell 是 ??公司開源的中文普通話語料集。400個來自不同方言區(qū)的人參與錄制, 錄制的條件是在室內(nèi)使用高保真的麥克風(fēng),音頻降采樣到16000Hz。
//中文文字腳本95%的準(zhǔn)確度
//170小時的語料。劃分為85%的訓(xùn)練集,10%的開發(fā)集(作用?),5%的測試集。在上課的時候我被錄制語料的成本嚇到了,2000小時的語料大約需要100萬人民幣的費(fèi)用。
AiShell語料集可以免費(fèi)由于學(xué)術(shù)目的。
語料集下載
Kaldi中包含Aishell的示例腳本。在kaldi/egs/aishell/s5中。下文所有的文件都在該目錄之下。
下載語料集的腳本包含在run.sh中。
先安裝好語言模型的工具才能運(yùn)行run.sh
run ./install_kaldi_lm.sh && source ../env.sh
上一篇文章沒有說每一個項(xiàng)目下的s5文件夾中有什么,在網(wǎng)上找到了別人寫的一個總結(jié):kaldi 源碼分析(三) - run.pl 分析
cmd.sh # 并行執(zhí)行命令,通常分 run.pl, queue.pl 兩種
config # 參數(shù)定制化配置文件, mfcc, decode, cmvn 等配置文件
local # 工程定制化內(nèi)容
path.sh # 環(huán)境變量相關(guān)腳本
run.sh # 整體流程控制腳本,主入口腳本
steps # 存放單一步驟執(zhí)行的腳本
utils # 存放解析文件,預(yù)處理等相
關(guān)工具的腳本
最重要的入口腳本是run.sh。包含所有腳本。如果要在本地運(yùn)行,需要修改這個腳本。把其中的queue.pl改成run.pl。
export train_cmd="run.pl"
export decode_cmd=run.pl
export mkgraph_cmd="run.pl"
先做Lab6,注釋掉神經(jīng)網(wǎng)絡(luò)訓(xùn)練部分。為了對比加不加神經(jīng)網(wǎng)絡(luò)對最后的識別準(zhǔn)確率有多大的影響。
# nnet3
#local/nnet3/run_tdnn.sh
# chain
#local/chain/run_tdnn.sh
運(yùn)行run.sh腳本,一步到位
在上一篇文章中,主要講了kaldi的工作流程,復(fù)雜一點(diǎn)的項(xiàng)目除了要考慮多音素的對齊以外?基本流程是差不多的。先運(yùn)行整體流程腳本run.sh看一下效果。然后再具體深入進(jìn)腳本中看有哪些關(guān)鍵步驟。
你是否遇到過連接遠(yuǎn)程服務(wù)器跑訓(xùn)練,然后網(wǎng)絡(luò)掉線殺掉了正在跑的進(jìn)程?我遇到過,后來主要使用nohup來避免這個問題。課件里推薦使用screen來避免遠(yuǎn)程登陸進(jìn)程被殺掉后,訓(xùn)練進(jìn)程也停止的問題。screen的原理不是本篇文章關(guān)心的重點(diǎn)。
加上screen后運(yùn)行run.sh:
screen -S run
run ./run.sh
就能看到腳本在一個新的頁面輸出內(nèi)容了。
如果要結(jié)束進(jìn)程ctrl a + d//我其實(shí)不喜歡這個命令,因?yàn)楹芙?jīng)常使用ctrl+a來編輯命令,兩個快捷鍵沖突。
查看結(jié)果
中文語音識別的準(zhǔn)確度通常使用CER(Char Error Rate)來表示。因?yàn)橹形闹凶质亲钚≌Z義單位,而英文中詞是基本語義單位。
和上一篇文章差不多的命令。腳本的運(yùn)行結(jié)果保存在了exp目錄下。
for x in exp/*/decode_test; do [ -d $x ] && grep WER $x/cer_* | utils/best_wer.sh; done 2>/dev/null
訓(xùn)練出來的結(jié)果如下:

可以cat RESULTS,和官方跑出來的結(jié)果做一下對比。

需要注意的是,和上篇文章的實(shí)驗(yàn)不一樣的是,輸出的結(jié)果是多行的,因?yàn)閳?zhí)行了多次的實(shí)驗(yàn),上面的腳本輸出的是每次實(shí)驗(yàn)最好的結(jié)果。
我自己跑出來的最好結(jié)果是tri5a的cer_14_0.5而RESULTS中的GMM-HMM模型中最好的結(jié)果是tri5a的cer_13_0.5。兩者CER都是12.12。每次實(shí)驗(yàn)本身都有一定的隨機(jī)性。結(jié)果有一些誤差是沒問題的。為了確認(rèn)模型有被正確的訓(xùn)練,查看自己結(jié)果的
tri5a/decode_test/cer_13_0.5的CER是12.18,恰好不是最優(yōu)解而已。這里的13和14是lmwt(語言模型權(quán)重)。具體的可以看上一篇文章。
細(xì)節(jié)
使用命令cat run.sh | grep "#"將run.sh腳本中的環(huán)節(jié)注釋提取打印出來。其中倒數(shù)2,4行是我們在一開始注釋掉的??梢钥吹交究梢苑譃闇?zhǔn)備、訓(xùn)練和獲取結(jié)果三個部分。

準(zhǔn)備
-
下載語料集
需要注意的是aishell語料集有大概20GB的大小。意味著需要很長的時間才能下載下來。我是直接用服務(wù)器里提前下好的語料集。
aishell目錄概覽
local/download_and_untar.sh $data $data_url data_aishell || exit 1;
這里的 a || b是一個邏輯符號,代表著如果a執(zhí)行失敗則執(zhí)行b。這里要放一個小插曲。去年面試阿里云的實(shí)習(xí)項(xiàng)目時,面試官開頭就問了如何知道上一條linux命令是否成功執(zhí)行。我當(dāng)時不知道,現(xiàn)在要知道了。就是看變量$?的值,如果為0代表成功執(zhí)行。這里的exit 1終止當(dāng)前進(jìn)程并且將$?設(shè)置為1。表示不成功執(zhí)行。
$data 是aishell在本機(jī)的位置,既可以新建一個空目錄來下載,也可以指定到已經(jīng)下好的路徑,aishell 分為resource_aishell和data_aishell兩部分來下載,腳本會通過檢查每一個部分下是否有.complete文件來判斷當(dāng)前部分是否下載完全。如果沒有才會到指定網(wǎng)址下載。
- Lexicon Preparation
local/aishell_prepare_dict.sh $data/resource_aishell || exit 1;
這個腳本的功能主要是將resource_aishell下的lexicon.txt復(fù)制到data/local/dict中。并且提取出nonsilence_phones.txt、optional_silence.txt、silence_phones.txt和extra_questions.txt。用到了很多awk和perl的腳本。沒看懂。(要是看懂了,第一次assignment就不會搞砸了)
那就要求自己看懂把。




每行代表一組相同的base phone,包含各種不同的重音或者聲調(diào)。

- Data preparation
local/aishell_data_prep.sh $data/data_aishell/wav $data/data_aishell/transcript || exit 1;
$data/data_aishell/wav目錄下放著的是音頻文件。其中有兩級目錄speaker/filename.wav。
$data/data_aishell/transcript目錄下放著的是aishell_transcript_v0.8.txt文字翻錄。
這個shell腳本的功能是將$data/data_aishell/wav下的 train,test,dev分別建立索引。并且建立Kaldi能夠理解的語料格式。具體有些什么可以參考上一篇文章和下面這一段腳本。
# Transcriptions preparation
for dir in $train_dir $dev_dir $test_dir; do
echo Preparing $dir transcriptions
# 將當(dāng)前集合目錄下的wav的文件名提取出來作為utt_id
sed -e 's/\.wav//' $dir/wav.flist | awk -F '/' '{print $NF}' > $dir/utt.list
# 根據(jù)目錄結(jié)構(gòu)建立utt2spk的關(guān)系
sed -e 's/\.wav//' $dir/wav.flist | awk -F '/' '{i=NF-1;printf("%s %s\n",$NF,$i)}' > $dir/utt2spk_all
# 按列合并utt.list和wav.flist,達(dá)到對音頻文件的映射。
paste -d' ' $dir/utt.list $dir/wav.flist > $dir/wav.scp_all
utils/filter_scp.pl -f 1 $dir/utt.list $aishell_text > $dir/transcripts.txt
awk '{print $1}' $dir/transcripts.txt > $dir/utt.list
utils/filter_scp.pl -f 1 $dir/utt.list $dir/utt2spk_all | sort -u > $dir/utt2spk
utils/filter_scp.pl -f 1 $dir/utt.list $dir/wav.scp_all | sort -u > $dir/wav.scp
sort -u $dir/transcripts.txt > $dir/text
utils/utt2spk_to_spk2utt.pl $dir/utt2spk > $dir/spk2utt
done
4. Phone sets, questions, L compilation
utils/prepare_lang.sh --position-dependent-phones false data/local/dict \
"<SPOKEN_NOISE>" data/local/lang data/lang || exit 1;
上面shell腳本的目的是創(chuàng)建L.fst,音素模型,其中fst是Finite State Transducers(有限狀態(tài)轉(zhuǎn)換器)的縮寫。找到這篇Kaldi學(xué)習(xí)筆記 -- 構(gòu)建字典FST腳本 -- prepare_lang.sh 關(guān)鍵內(nèi)容解析詳細(xì)的說明了這個腳本的工作。而關(guān)于prepare_lang的一點(diǎn)理解給腳本進(jìn)行了一些翻譯和注釋。
Kaldi中FST(Finite State Transducer)含義及其可視化
L.fst: 音素詞典(Phonetic Dictionary or Lexicon)模型,phone symbols作為輸入,word symbols作為輸出,如圖Figure 1所示。
Figure 1 L.fst結(jié)構(gòu)
L_disambig.fst是為了消除模棱兩可(disambiguation)而引入的模型,表述為 the lexicon with disambiguation symbols。分歧的情況如:一個詞是另一個詞的前綴,cat 和 cats在同一個詞典中,則需要"k ae t #1"; 有同音的詞,red: "r eh d #1", read: "r eh d #2"。
我一直疑惑lexiconp.txt是怎么生成的,查了好久。結(jié)果竟然只是一段在詞和音素之間插入1.0的代碼:
if [[ ! -f $srcdir/lexiconp.txt ]]; then
echo "**Creating $srcdir/lexiconp.txt from $srcdir/lexicon.txt"
perl -ape 's/(\S+\s+)(.+)/${1}1.0\t$2/;' < $srcdir/lexicon.txt > $srcdir/lexiconp.txt || exit 1;
fi

那么L.fst是怎么得到的呢?
通過
make_lexicon_fst.py(現(xiàn)有的博客都說是.pl結(jié)尾,可能kaldi重構(gòu)了)。還有一個消歧義的過程。具體的就看不懂了。解碼圖創(chuàng)建示例(測試階段)里有較為詳細(xì)的文檔講解。
5. LM training
local/aishell_train_lms.sh || exit 1;
這個shell腳本讀取data/local/train/text,data/local/dict/lexicon.txt
得到text的計(jì)數(shù)文件word.counts并以word.counts為基礎(chǔ)添加lexicon.txt中的字(除了SIL)出現(xiàn)的次數(shù)到unigram.counts中。我就沒深入看下去了,期間用到的腳本文件有:get_word_map.pl、train_lm.sh --arpa --lmtype 3gram-mincount $dir || exit 1;這個步驟的結(jié)果保存在data/local/lm/3gram-mincount/lm_unpruned.gz中。
6. G compilation, check LG composition
utils/format_lm.sh data/lang data/local/lm/3gram-mincount/lm_unpruned.gz \
data/local/dict/lexicon.txt data/lang_test || exit 1;
這個步驟是編譯G.fst并將LG串聯(lián)起來。
Kaldi中FST(Finite State Transducer)含義及其可視化
G.fst: 語言模型,大部分是FSA(finite state acceptor, i.e. 每個arc的輸入輸出是相同的),如圖Figure 2所示。
Figure 2 G.fst結(jié)構(gòu)(由指令詞識別1-gram語法產(chǎn)生,disambiguation symbol #0 未加入)
kaldi 訓(xùn)練 aishell 解析
utils/format_lm.sh:上述的語言工具基于第三方工具,為ARPA-format,腳本的作業(yè)是將其轉(zhuǎn)換為fst,方便與之前的字典fst(L.fst)結(jié)合,發(fā)揮fst的優(yōu)勢。腳本最后會檢測G.fst中是否存在沒有單詞的空回環(huán),如果存在會報(bào)錯,因?yàn)檫@會導(dǎo)致后續(xù)HLG determinization的出現(xiàn)錯誤。
腳本utils/format_lm.sh解決把ARPA格式的語言模型轉(zhuǎn)換成OpenFST格式類型。腳本用法如下:
Usage: utils/format_lm.sh <lang_dir> <arpa-LM> <lexicon> <out_dir>
E.g.: utils/format_lm.sh data/lang data/local/lm/foo.kn.gz data/local/dict/lexicon.txt data/lang_test
Convert ARPA-format language models to FSTs.
這個腳本的一些關(guān)鍵命令如下:
gunzip -cout_dir/words.txt - $out_dir/G.fst
Kaldi程序arpa2fst將ARPA格式的語言模型轉(zhuǎn)換成一個加權(quán)有限狀態(tài)轉(zhuǎn)移機(jī)(實(shí)際上是接收機(jī))。
流程很復(fù)雜,未來可能再把L.fst,LM training,G.fst, LG composition另起一篇。(就是說現(xiàn)在時間條件不允許深入)
訓(xùn)練
訓(xùn)練的環(huán)節(jié)開始我就讀不懂了。主要是邏輯和概念不懂。也不浪費(fèi)時間了。簡單的去了解一下輸入輸出和功能。
1. MFCC 特征生成
這個環(huán)節(jié)和yesno項(xiàng)目的沒有不同。主要就是獲得train,test, dev三個集合的歸一化的梅爾倒譜系數(shù)。最后修復(fù)排序錯誤,并會移除那些被指明需要特征數(shù)據(jù)或標(biāo)注,但是卻找不到被需要的數(shù)據(jù)的那些發(fā)音(utterances)。
2. 訓(xùn)練單音素模型
steps/train_mono.sh --cmd "$train_cmd" --nj 10 \
data/train data/lang exp/mono || exit 1;
參考kaldi-GMM-HMM pipeline,上面的shell腳本主要是對齊音素和每一幀音頻的。Kaldi 入門train_mono.sh詳解、kaldi學(xué)習(xí)筆記 -- 訓(xùn)練單音素(monophone)模型腳本 -- steps/train_mono.sh都有講一些。
對流程講得最好的是:
mkgraph 需要 lang_test 下的 L.fst G.fst phones.txt, words.txt , phones/silence.csl , phones/http://disambig.int
以及 exp/tri 下的 tree, final.mdl
在訓(xùn)練的job并行訓(xùn)練過程中,訓(xùn)練數(shù)據(jù)的各個子集合是分散到不同的處理器去進(jìn)行訓(xùn)練,然后每輪迭代后會進(jìn)行合并。
下面就講一下訓(xùn)練的過程:
1.首先是初始化GMM,使用的腳本是/kaldi-trunk/src/gmmbin/gmm-init-mono,輸出是0.mdl和tree文件;
2.compile training graphs,使用的腳本是/kaldi-trunk/source/bin/compile-training-graphs,輸入是tree,0.mdl和L.fst,輸出是fits.JOB.gz,其是在訓(xùn)練過程中構(gòu)建graph的過程;
3.接下來是一個對齊的操作,kaldi-trunk/source/bin/align-equal-compiled;
4.然后是基于GMM的聲學(xué)模型進(jìn)行最大似然估計(jì)得過程,腳本為/kaldi-trunk/src/gmmbin/gmm-est;
5.然后進(jìn)行迭代循環(huán)中進(jìn)行操作,如果本步驟到了對齊的步驟,則調(diào)用腳本kaldi-kaldi/src/gmmbin/gmm-align-compiled;
6.重新估計(jì)GMM,累計(jì)狀態(tài),用腳本/kaldi-trunk/src/gmmbin/gmm-acc-states-ali;調(diào)用新生成的參數(shù)(高斯數(shù))重新估計(jì)GMM,調(diào)用腳本/kaldi-trunk/src/gmmbin/gmm-est;
7.對分散在不同處理器上的結(jié)果進(jìn)行合并,生成.mdl結(jié)果,調(diào)用腳本gmm-acc-sum;
8.增加高斯數(shù),如果沒有超過設(shè)定的迭代次數(shù),則跳轉(zhuǎn)到步驟5重新進(jìn)行訓(xùn)練
最后生成的.mdl即為聲學(xué)模型文件
在離線識別階段,即可以調(diào)用utils/mkgraph.sh;來對剛剛生成的聲學(xué)文件進(jìn)行構(gòu)圖
之后解碼,得到離線測試的識別率。
3. (Monophone decoding) 單音素解碼
utils/mkgraph.sh data/lang_test exp/mono exp/mono/graph || exit 1;
steps/decode.sh --cmd "$decode_cmd" --config conf/decode.config --nj 10 \
exp/mono/graph data/dev exp/mono/decode_dev
steps/decode.sh --cmd "$decode_cmd" --config conf/decode.config --nj 10 \
exp/mono/graph data/test exp/mono/decode_test
mkgraph.sh將L_disambig.fst 和 G.fst 復(fù)合生成LG.fst。中間經(jīng)歷了我看不懂的處理。最終生成用于解碼的 HCLG.fst。
看不懂的部分
后面就已經(jīng)看不懂了。
# Get alignments from monophone system.
steps/align_si.sh --cmd "$train_cmd" --nj 10 \
data/train data/lang exp/mono exp/mono_ali || exit 1;
# train tri1 [first triphone pass]
steps/train_deltas.sh --cmd "$train_cmd" \
2500 20000 data/train data/lang exp/mono_ali exp/tri1 || exit 1;
# decode tri1
utils/mkgraph.sh data/lang_test exp/tri1 exp/tri1/graph || exit 1;
steps/decode.sh --cmd "$decode_cmd" --config conf/decode.config --nj 10 \
exp/tri1/graph data/dev exp/tri1/decode_dev
steps/decode.sh --cmd "$decode_cmd" --config conf/decode.config --nj 10 \
exp/tri1/graph data/test exp/tri1/decode_test
# align tri1
steps/align_si.sh --cmd "$train_cmd" --nj 10 \
data/train data/lang exp/tri1 exp/tri1_ali || exit 1;
# train tri2 [delta+delta-deltas]
steps/train_deltas.sh --cmd "$train_cmd" \
2500 20000 data/train data/lang exp/tri1_ali exp/tri2 || exit 1;
# decode tri2
utils/mkgraph.sh data/lang_test exp/tri2 exp/tri2/graph
steps/decode.sh --cmd "$decode_cmd" --config conf/decode.config --nj 10 \
exp/tri2/graph data/dev exp/tri2/decode_dev
steps/decode.sh --cmd "$decode_cmd" --config conf/decode.config --nj 10 \
exp/tri2/graph data/test exp/tri2/decode_test
# train and decode tri2b [LDA+MLLT]
steps/align_si.sh --cmd "$train_cmd" --nj 10 \
data/train data/lang exp/tri2 exp/tri2_ali || exit 1;
# Train tri3a, which is LDA+MLLT,
steps/train_lda_mllt.sh --cmd "$train_cmd" \
2500 20000 data/train data/lang exp/tri2_ali exp/tri3a || exit 1;
utils/mkgraph.sh data/lang_test exp/tri3a exp/tri3a/graph || exit 1;
steps/decode.sh --cmd "$decode_cmd" --nj 10 --config conf/decode.config \
exp/tri3a/graph data/dev exp/tri3a/decode_dev
steps/decode.sh --cmd "$decode_cmd" --nj 10 --config conf/decode.config \
exp/tri3a/graph data/test exp/tri3a/decode_test
# From now, we start building a more serious system (with SAT), and we'll
# do the alignment with fMLLR.
steps/align_fmllr.sh --cmd "$train_cmd" --nj 10 \
data/train data/lang exp/tri3a exp/tri3a_ali || exit 1;
steps/train_sat.sh --cmd "$train_cmd" \
2500 20000 data/train data/lang exp/tri3a_ali exp/tri4a || exit 1;
utils/mkgraph.sh data/lang_test exp/tri4a exp/tri4a/graph
steps/decode_fmllr.sh --cmd "$decode_cmd" --nj 10 --config conf/decode.config \
exp/tri4a/graph data/dev exp/tri4a/decode_dev
steps/decode_fmllr.sh --cmd "$decode_cmd" --nj 10 --config conf/decode.config \
exp/tri4a/graph data/test exp/tri4a/decode_test
steps/align_fmllr.sh --cmd "$train_cmd" --nj 10 \
data/train data/lang exp/tri4a exp/tri4a_ali
# Building a larger SAT system.
steps/train_sat.sh --cmd "$train_cmd" \
3500 100000 data/train data/lang exp/tri4a_ali exp/tri5a || exit 1;
utils/mkgraph.sh data/lang_test exp/tri5a exp/tri5a/graph || exit 1;
steps/decode_fmllr.sh --cmd "$decode_cmd" --nj 10 --config conf/decode.config \
exp/tri5a/graph data/dev exp/tri5a/decode_dev || exit 1;
steps/decode_fmllr.sh --cmd "$decode_cmd" --nj 10 --config conf/decode.config \
exp/tri5a/graph data/test exp/tri5a/decode_test || exit 1;
steps/align_fmllr.sh --cmd "$train_cmd" --nj 10 \
data/train data/lang exp/tri5a exp/tri5a_ali || exit 1;
# nnet3
local/nnet3/run_tdnn.sh
# chain
local/chain/run_tdnn.sh
獲取結(jié)果
# getting results (see RESULTS file)
for x in exp/*/decode_test; do [ -d $x ] && grep WER $x/cer_* | utils/best_wer.sh; done 2>/dev/null
for x in exp/*/*/decode_test; do [ -d $x ] && grep WER $x/cer_* | utils/best_wer.sh; done 2>/dev/null
exit 0;
和上一篇文章一樣的步驟。
Kaldi入門:yesno項(xiàng)目


