Kaldi 入門教學 - Kaldi tutorial for dummy

最近開始接觸語音辨識,經過一番搜尋後,在網路上找到幾個相關的 toolkit,分別是 CMUSphinxKaldi 還有 MozillaDeepSpeechCMUSphinx 是由卡內基美隆大學開發的,DeepSpeech 則是由 Mozilla 基於百度的 Deep Speech 研究所開發的,百度發的論文在這邊,有興趣的可以去看一下,這裡就不對這些語音辨識工具多做介紹了,文章把重點放在 Kaldi 的安裝與使用教學

為什麼選擇 Kaldi

我一開始是想找個可以用 Python 開發的語音辨識工具,因為寫起來比較方便,後來找到 CMUSphinx,他有提供 Python 的介面,但是在網路上看到大家對 CMUSphinx 的評價都不太好(準確度方面),於是我就繼續找,過程中在 Telegram 上面找到一個討論 Speech Recognition 的群組,就上去問一下有沒有比較推薦的訓練工具,有人建議我可以試試 Kaldi,花了些時間搜尋有關 Kaldi 的教學還有說明文件後,發現他的參考資料比 CMUSphinx 多很多,而且官方說明文件也很完整,不過原生的 Kaldi 貌似沒有提供 Python 的 API,使用上是以 shell script 為主,這讓我猶豫了一下,不過後來翻了一下那些 shell script,其實也沒有很複雜,就是檔案多了點,畢竟我目前沒有打算把模型的實作方法整個看懂,重心主要擺在「訓練及使用模型」,了解訓練流程就足夠了,所以就決定先試試看 Kaldi,看他的表現如何。至於為什麼不用 MozillaDeepSpeech,因為我寫這篇文章的前幾個小時才發現有這個東西,等文章寫好之後我就會去看看 DeepSpeech 了,如果可以的話希望也可以寫一篇教學,寫寫自己的心路歷程。

廢話不多說,現在就開始我們今天的重點,Kaldi 的官網有兩篇教學,Kaldi tutorialKaldi for Dummies tutorial,這篇文章會針對後者做解說,文章的編排我會盡量按照 Kaldi for Dummies tutorial 的目錄走,內容則會用我自己的方法去說明,不會完全照著原文翻譯,當然還是建議大家有能力的話可以直接去看原文。

開發環境

官方說__盡量在 Linux 環境執行__,畢竟不管是安裝套件或是修改設定,Linux 對開發者還是比較友善的。我則是在 wsl 上執行,因為我只有筆電(Windows 10 Home),然後又不想裝虛擬機,覺得每次開開關關很麻煩,所以決定裝 wsl,我選的是 Ubuntu 18.04 的版本。

官方教學在這邊有列出一些「必須安裝的工具」跟「可裝可不裝的工具」,可以先不管他們,等到安裝完 Kaldi 之後這些工具也一併安裝好了。

下載與安裝 Kaldi

用 git 把 Kaldi 的 source 下載下來

1
git clone https://github.com/kaldi-asr/kaldi.git kaldi --origin upstream

然後先照著 kaldi/tools/INSTALL 裡面的說明把該裝的裝一裝,再照著 kaldi/src/INSTALL 裡的步驟安裝 Kaldi

Kaldi 路徑設置

clone 下來的 kaldi 資料夾裡面有很多東西,官方列出其中幾個比較需要知道的:

  • egs:各種範例,每個範例裡面都有 README 可以參考,範例中的 shell script 也都有很詳細的註解,後面的教學也會用到這個資料夾
  • misc:一些額外的工具,我自己是沒有管他啦
  • src:Kaldi 的 source code,這裡有官方的詳細解說
  • tools:很多很多的工具,在訓練的時候會用到,這裡有官方的詳細解說
  • windows:如果真的必須在 Windows 上面執行 Kaldi 的話,務必要看看這個資料夾,官方很貼心的提供了一些工具

簡易 Kaldi 範例

情境

現在我們有許多來自不同受試者的錄音檔,錄音的內容為英文數字,每個錄音檔都包含三個數字。例如:「one nine zero」。

目的

我們希望把這些錄音檔拆成 training set 跟 testing set,然後建立自動語音辨識。

第一步

先在 egs 裡面建立一個 digits 資料夾,接下來的動作都會在這個資料夾裡面進行。

準備訓練資料

錄音檔

訓練會用到的錄音檔可以在這邊下載,裡面一共有 128 個單聲道 16000 Hz 的 wav 檔,並且已經被分成 train 與 test,分別包含 80 個與 40 個錄音,檔名的格式是 ID_<錄音裡面的三個數字>,這邊有八個 ID,可以想成是八個不同的人的錄音,錄音檔按照 ID 又被細分為八個資料夾,所以我們錄音檔的結構會長這樣:

  • 八個不同的人
  • 每個人有 16 個錄音檔
  • 總共 128 個句子(每個人 16 個)
  • 總共 128 * 3 個英文數字(每個句子有三個英文數字)

在剛剛建立的 digits 資料夾裡面建立一個名為 digits_audio 的資料夾,在 digits_audio 裡面再建立兩個資料夾 train 跟 test,然後把先前下載好的錄音檔裡面的 train 跟 test 分別放到剛剛建立的 train 跟 test 裡面。

聲學資料

建立好錄音檔後,還得告訴 Kaldi 一些關於這些聲音的資訊,首先先在 digits 下建立一個 data 資料夾,然後在 data 裡面一樣建立 train 跟 test,接著分別在 train 跟 test 裡面依序建立以下這些檔案:

我相信不會有人想要自己一行一行打這些檔案(我自己就不想),所以我寫了幾個簡單的 script 來自動產生這些檔案(除了 spk2gender),連結在這裡,不過建議大家先把下面關於檔案的說明都看完,了解檔案格式之後再跑 script。

spk2gender

這個檔案是關於說話的人的性別,格式如下:

1
<speakerID> <gender>

speakerID 可以當成是說話的人的名字,前提是這些名字不重複,gender 就是性別,男生是 m,女生是 f,就我們的錄音檔來看,speakerID 就是 1 到 8,前五個是男生,後三個是女生,所以檔案內容會像下面這樣:

1
2
3
4
1 m
2 m
3 m
...

wav.scp

這個檔案是每個句子所對應的錄音檔路徑,格式如下:

1
<utteranceID> <full_path_to_audio_file>

每個句子都有一個 utteranceIDutteranceID 沒有特別規定的格式,不過為了方便起見,這邊就以錄音檔的檔名作為 utteranceIDfull_path_to_audio_file 就檔案的完整路徑,所以檔案內容會像下面這樣:

1
2
3
4
1-040 /path/to/1-040.wav
1-089 /path/to/1-089.wav
1-104 /path/to/1-104.wav
...

text

這個檔案是每個句子所對應的錄音內容,格式跟參考內容如下:

1
<utteranceID> <text_transcription>
1
2
3
4
1-040 zero four zero
1-089 zero eight nine
1-104 one zero four
...

utt2spk

這個檔案是每個句子所對應的說話者,格式跟參考內容如下:

1
<utteranceID> <speakerID>
1
2
3
4
1-040 1
1-089 1
1-104 1
...

corpus.txt

這個檔案要包含所有會出現在自動語音辨識系統裡面的句子,以我們的錄音為例, corpus.txt 裡面會有 128 個句子,分別對應到 128 錄音檔的內容,格式跟參考內容如下:

1
<text_transcription>
1
2
3
4
zero four zero
zero eight nine
one zero four
...

另外要注意這個檔案存放的地方跟上面那些檔案比較不一樣,要在之前建立的 data 資料夾裡面,建立一個 local 資料夾,並把 corpus.txt 丟進去。

語言資料

這部分就是要準備一些跟我們要辨識的語言有關的文件,首先先在上個環節建立的 local 下建立 dict 資料夾,接著依序建立以下檔案:

lexicon.txt

這個檔案包含了所有會出現在自動語音辨識系統裡面的字的發音,發音則是由一個至多個音素(phone)組成,格式如下:

1
<word> <phone 1> <phone 2> ...

以我們的教學為例, lexicon.txt 裡面就是 0 到 9 的發音,這邊直接把所有的發音提供給大家使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
!SIL sil
<UNK> spn
eight ey t
five f ay v
four f ao r
nine n ay n
one hh w ah n
one w ah n
seven s eh v ah n
six s ih k s
three th r iy
two t uw
zero z ih r ow
zero z iy r ow

nonsilence_phones.txt

這個檔案包含了所有 non silence 的音素,這邊一樣把格式跟檔案內容提供給大家:

1
<phone>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ah
ao
ay
eh
ey
f
hh
ih
iy
k
n
ow
r
s
t
th
uw
w
v
z

silence_phones.txt

這應該看檔名就知道了,格式跟檔案內容如下:

1
<phone>
1
2
sil
spn

optional_silence.txt

1
<phone>
1
sil

到目前為止我們已經把訓練要用的資料都準備好了,接下來要準備訓練用的工具。

準備訓練用的工具

一些必要的工具

把 kaldi/egs/wsj/s5 裡面的 utils 跟 steps 這兩個資料夾複製到我們的 digits 下面,如果想省硬碟空間的話也可以考慮用 symbolic link 的方式。

評估模型表現的工具

在我們的 digits 資料夾下建立一個 local 資料夾,把 kaldi/egs/voxforge/s5/local 裡面的 score.sh 複製到這邊。

SRILM

詳細的安裝說明請到 kaldi/tools/install_srilm.sh 裡面看。

建立 configure

在 digits 下建立一個 conf 資料夾,在裡面建立 decode.config 跟 mfcc.conf 兩個檔案,檔案的內容如下:

decode.config

1
2
3
first_beam=10.0
beam=13.0
lattice_beam=6.0

mfcc.conf

1
--use-energy=false

這兩個檔案是用來調整訓練模型時候的一些參數。

準備訓練用的 shell script

接著要準備訓練模型的程式,在 digits 下依序建立這三個檔案:

cmd.sh

1
2
3
# Setting local system jobs (local CPU - no external clusters)
export train_cmd=run.pl
export decode_cmd=run.pl

path.sh

1
2
3
4
5
6
7
8
9
10
# Defining Kaldi root directory
export KALDI_ROOT=`pwd`/../..
# Setting paths to useful tools
export PATH=$PWD/utils/:$KALDI_ROOT/src/bin:$KALDI_ROOT/tools/openfst/bin:$KALDI_ROOT/src/fstbin/:$KALDI_ROOT/src/gmmbin/:$KALDI_ROOT/src/featbin/:$KALDI_ROOT/src/lmbin/:$KALDI_ROOT/src/sgmm2bin/:$KALDI_ROOT/src/fgmmbin/:$KALDI_ROOT/src/latbin/:$PWD:$PATH
# Defining audio data directory (modify it for your installation directory!)
export DATA_ROOT="/home/{user}/kaldi/egs/digits/digits_audio"
# Enable SRILM
. $KALDI_ROOT/tools/env.sh
# Variable needed for proper data sorting
export LC_ALL=C

run.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#!/bin/bash
. ./path.sh || exit 1
. ./cmd.sh || exit 1
nj=1 # number of parallel jobs - 1 is perfect for such a small dataset
lm_order=1 # language model order (n-gram quantity) - 1 is enough for digits grammar
# Safety mechanism (possible running this script with modified arguments)
. utils/parse_options.sh || exit 1
[[ $# -ge 1 ]] && { echo "Wrong arguments!"; exit 1; }
# Removing previously created data (from last run.sh execution)
rm -rf exp mfcc data/train/spk2utt data/train/cmvn.scp data/train/feats.scp data/train/split1 data/test/spk2utt data/test/cmvn.scp data/test/feats.scp data/test/split1 data/local/lang data/lang data/local/tmp data/local/dict/lexiconp.txt
echo
echo "===== PREPARING ACOUSTIC DATA ====="
echo
# Needs to be prepared by hand (or using self written scripts):
#
# spk2gender [<speaker-id> <gender>]
# wav.scp [<uterranceID> <full_path_to_audio_file>]
# text [<uterranceID> <text_transcription>]
# utt2spk [<uterranceID> <speakerID>]
# corpus.txt [<text_transcription>]
# Making spk2utt files
utils/utt2spk_to_spk2utt.pl data/train/utt2spk > data/train/spk2utt
utils/utt2spk_to_spk2utt.pl data/test/utt2spk > data/test/spk2utt
echo
echo "===== FEATURES EXTRACTION ====="
echo
# Making feats.scp files
mfccdir=mfcc
# Uncomment and modify arguments in scripts below if you have any problems with data sorting
# utils/validate_data_dir.sh data/train # script for checking prepared data - here: for data/train directory
# utils/fix_data_dir.sh data/train # tool for data proper sorting if needed - here: for data/train directory
steps/make_mfcc.sh --nj $nj --cmd "$train_cmd" data/train exp/make_mfcc/train $mfccdir
steps/make_mfcc.sh --nj $nj --cmd "$train_cmd" data/test exp/make_mfcc/test $mfccdir
# Making cmvn.scp files
steps/compute_cmvn_stats.sh data/train exp/make_mfcc/train $mfccdir
steps/compute_cmvn_stats.sh data/test exp/make_mfcc/test $mfccdir
echo
echo "===== PREPARING LANGUAGE DATA ====="
echo
# Needs to be prepared by hand (or using self written scripts):
#
# lexicon.txt [<word> <phone 1> <phone 2> ...]
# nonsilence_phones.txt [<phone>]
# silence_phones.txt [<phone>]
# optional_silence.txt [<phone>]
# Preparing language data
utils/prepare_lang.sh data/local/dict "<UNK>" data/local/lang data/lang
echo
echo "===== LANGUAGE MODEL CREATION ====="
echo "===== MAKING lm.arpa ====="
echo
loc=`which ngram-count`;
if [ -z $loc ]; then
if uname -a | grep 64 >/dev/null; then
sdir=$KALDI_ROOT/tools/srilm/bin/i686-m64
else
sdir=$KALDI_ROOT/tools/srilm/bin/i686
fi
if [ -f $sdir/ngram-count ]; then
echo "Using SRILM language modelling tool from $sdir"
export PATH=$PATH:$sdir
else
echo "SRILM toolkit is probably not installed.
Instructions: tools/install_srilm.sh"
exit 1
fi
fi
local=data/local
mkdir $local/tmp
ngram-count -order $lm_order -write-vocab $local/tmp/vocab-full.txt -wbdiscount -text $local/corpus.txt -lm $local/tmp/lm.arpa
echo
echo "===== MAKING G.fst ====="
echo
lang=data/lang
arpa2fst --disambig-symbol=#0 --read-symbol-table=$lang/words.txt $local/tmp/lm.arpa $lang/G.fst
echo
echo "===== MONO TRAINING ====="
echo
steps/train_mono.sh --nj $nj --cmd "$train_cmd" data/train data/lang exp/mono || exit 1
echo
echo "===== MONO DECODING ====="
echo
utils/mkgraph.sh --mono data/lang exp/mono exp/mono/graph || exit 1
steps/decode.sh --config conf/decode.config --nj $nj --cmd "$decode_cmd" exp/mono/graph data/test exp/mono/decode
echo
echo "===== MONO ALIGNMENT ====="
echo
steps/align_si.sh --nj $nj --cmd "$train_cmd" data/train data/lang exp/mono exp/mono_ali || exit 1
echo
echo "===== TRI1 (first triphone pass) TRAINING ====="
echo
steps/train_deltas.sh --cmd "$train_cmd" 2000 11000 data/train data/lang exp/mono_ali exp/tri1 || exit 1
echo
echo "===== TRI1 (first triphone pass) DECODING ====="
echo
utils/mkgraph.sh data/lang exp/tri1 exp/tri1/graph || exit 1
steps/decode.sh --config conf/decode.config --nj $nj --cmd "$decode_cmd" exp/tri1/graph data/test exp/tri1/decode
echo
echo "===== run.sh script is finished ====="
echo

訓練模型

該準備的都準備好了,目前的 digits 資料夾結構會長這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
.
├── cmd.sh
├── conf
│   ├── decode.config
│   └── mfcc.conf
├── data
│   ├── local
│   │   ├── corpus.txt
│   │   └── dict
│   │   ├── lexicon.txt
│   │   ├── nonsilence_phones.txt
│   │   ├── optional_silence.txt
│   │   └── silence_phones.txt
│   ├── test
│   │   ├── spk2gender
│   │   ├── text
│   │   ├── utt2spk
│   │   └── wav.scp
│   └── train
│   └── 跟 test 一樣的結構
├── digits_audio
│   ├── test
│   │   └── 編號 1 到 8 的資料夾
│   │      └── 每個資料夾內都有 6 個 wav
│   └── train
│      └── 編號 1 到 8 的資料夾
│         └── 每個資料夾內都有 10 個 wav
├── local
│   └── score.sh
├── path.sh
├── run.sh
├── steps
│   ├── align_basis_fmllr.sh
│   ├── align_basis_fmllr_lats.sh
│   └── ...
└── utils
├── add_disambig.pl
├── add_lex_disambig.pl
└── ...

最後在 terminal 打上 ./run.sh 就可以開始訓練了,訓練出來的結果可以去 exp 裡面看,副檔名為 .mdl 的就是我們訓練出來的模型,其中 final.mdl 是訓練過程跑完後的最終模型,他其實是個 link,連到同一個資料夾裡面的 <number>.mdl。

以上就是官方 Kaldi for Dummies tutorial 的解說,接下來會說明怎麼使用訓練好的模型來測試自己的錄音檔。

如何使用模型

準備測試工具

先到 kaldi/src/online 下面 make,把該編譯的東西編一編,然後把 kaldi/egs/voxforge 下面的 online_demo 整個複製到我們的 digits 資料夾下面。

準備模型與錄音檔

在我們剛剛複製過來的 online_demo 下建立兩個資料夾,online-data 跟 work,online-data 要擺我們的模型跟錄音,work 可以先不管他,跑測試的過程中產生的檔案會被丟進去,然後在 online-data 裡面建立 audio 跟 model 兩個資料夾。

把要測試的 wav 放到 audio 裡面,另外在 audio 裡建立一個 trans.txt,檔案內容就是 wav 的對應到的語音內容,舉例來說:test1.wav 的內容是「one two three」,那 trans.txt 就會是:

1
test1 one two three

接著在 model 下建立 tri1 資料夾,資料夾內要包含以下四個檔案:

  • <number.mdl>:模型(在 digits/exp/tri1 裡面)
  • final.mdl:一個 link,連到模型(在 digits/exp/tri1 裡面)
  • HCLG.fst:完整的 fst(在 digits/exp/tri1/graph 裡面)
  • words.txt:字典,給每個字一個編號(在 digits/exp/tri1/graph 裡面)

修改一下 online_demo 下的 run.sh,把 ac_model_type 改成 tri1(原本應該是 tri2b_mmi),另外原本的 run.sh 裡面有一段下載 testing data 的程式碼,把它註解掉。

1
2
3
4
5
6
7
8
9
if [ ! -s ${data_file}.tar.bz2 ]; then
echo "Downloading test models and data ..."
wget -T 10 -t 3 $data_url;

if [ ! -s ${data_file}.tar.bz2 ]; then
echo "Download of $data_file has failed!"
exit 1
fi
fi

最後,把 run.sh 裡面 $ac_model/model 的地方都改成 $ac_model/final.mdl(應該有兩個地方要改),都弄好之後 online_demo 的結構應該會像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.
├── README.txt
├── online-data
│   ├── audio
│   │   ├── test1.wav
│   │   └── trans.txt
│   └── models
│   ├── mono
│   │   ├── 40.mdl
│   │   ├── HCLG.fst
│   │   ├── final.mdl -> 40.mdl
│   │   └── words.txt
│   └── tri1
│   ├── 35.mdl
│   ├── HCLG.fst
│   ├── final.mdl -> 35.mdl
│   └── words.txt
├── run.sh
└── work

執行 run.sh 就可以看到測試結果了。

vosk api 補上 沒用過