1. 程式人生 > 實用技巧 >OfficialKaldi(六)——資料準備(翻譯註解)

OfficialKaldi(六)——資料準備(翻譯註解)

介紹

在執行完示例指令碼後(見Kaldi教程),你可能會想用自己的資料在Kaldi上跑一下。本節主要講述如何準備相關資料。本頁的讀者應該使用最新版本的例項指令碼(即在指令碼目錄下被命名為s5的那些,例如egs/rm/s5)。 另外,除了閱讀本頁所述內容外,你還可以閱讀指令碼目錄下的那些資料準備相關的指令碼。(譯者:結合起來看更易理解。) 在頂層的run.sh指令碼(例如egs/rm/s5/run.sh)中,最前面的幾行命令都是和資料準備相關的,代表資料準備的不同步驟。子目錄local/下的指令碼都是和資料集相關的。例如,Resource Management(RM)資料集相應的指令碼就是local/rm_data_prep.sh

。對RM資料集來說,這幾行資料準備的命令為:

local/rm_data_prep.sh /export/corpora5/LDC/LDC93S3A/rm_comp || exit 1;

utils/prepare_lang.sh data/local/dict '!SIL' data/local/lang data/lang || exit 1;

local/rm_prepare_grammar.sh || exit 1;

而對WSJ來說,命令為:

wsj0=/export/corpora5/LDC/LDC93S6B
wsj1=/export/corpora5/LDC/LDC94S13B

local/wsj_data_prep.sh $wsj0/??-{?,??}.? $wsj1/??-{?,??}.?  || exit 1;

local/wsj_prepare_dict.sh || exit 1;

utils/prepare_lang.sh data/local/dict "<SPOKEN_NOISE>" data/local/lang_tmp data/lang || exit 1;

local/wsj_format_data.sh || exit 1;

在WSJ的示例指令碼中,上述命令之後還有一些訓練語言模型的命令(根據標註重新訓練,而不是使用LDC提供的), 但是上述幾條命令是最重要的。

資料準備階段的輸出包含兩部分。一部分與“資料”相關(儲存在諸如data/train/之類的目錄下),另一部分 則與“語言”相關(儲存在諸如data/lang/之類的目錄下)。“資料”部分與資料集的錄音相關,而“語言”部分 則與語言本身更相關的內容,例如發音字典、音素集合以及其他很多Kaldi需要的關於音素的額外資訊。 如果你想用已有的識別系統和語言模型對你的資料進行解碼,那麼你只需要重寫“資料”部分。

資料準備-- 資料部分.

舉個數據準備階段關於“資料”部分的例子,請檢視任何一個示例指令碼目錄下的data/train

目錄(假設你已經執行過一遍這些指令碼了)。注意:目錄名字data/train本身沒有什麼特別的。一些被命名為其他名字的目錄,如data/eval2000(為一個測試集建立的),有幾乎差不多的目錄結構和檔案格式(說“幾乎”是因為在測試集的目錄下,可能含有“sgm”和“glm”檔案,用於sclite評分)。我們以Switchboard資料為例,對應指令碼在egs/swbd/s5下。

s5# ls data/train
cmvn.scp  feats.scp  reco2file_and_channel  segments  spk2utt  text  utt2spk  wav.scp

不是所有的檔案都同等重要。如果要設定簡單點,分段(segmentation)資訊是不必要的(即一個檔案裡只有一段發音),你只需要自己建立utt2spktextwav.scpsegmentsreco2file_and_channel是可選的,根據實際需要決定是否建立。剩下的就都交給標準指令碼。

下面我們會詳細描述該目錄下的這些檔案。首先從那些需要你手動建立的檔案開始。

需要手動建立的檔案

檔案“text”包含每段發音的標註。

s5# head -3 data/train/text
sw02001-A_000098-001156 HI UM YEAH I'D LIKE TO TALK ABOUT HOW YOU DRESS FOR WORK AND 
sw02001-A_001980-002131 UM-HUM
sw02001-A_002736-002893 AND IS

每行的第一項是發音編號(utterance-id),可以是任意的文字字串,但是如果在你的設定中還包含說話人資訊, 你應該把說話人編號(speaker-id)作為發音編號的字首。這對於音訊檔案的排序非常重要。發音編號後面跟著的 是每段發音的標註。你不用保證這裡出現的每一個字都出現在你的詞彙表中。詞彙表之外的詞會被對映到data/lang/oov.txt中。 注意:儘管在這個特別的例子中,我們用下劃線分割了發音編號中的“說話人”和“發音”部分,但是通常用破折號(“-”)會更安全 一點。這是因為破折號的ASCII值更小。有人向我指出說,如果使用下劃線,並且說話人編號的長度不一,在某些特殊的情況下, 如果使用標準"C"語言風格對字串進行排序,說話人編號和對應的發音編號會被排成不同的順序。 另外一個很重要的檔案是wav.scp。在Switchboard例子中,

s5# head -3 data/train/wav.scp
sw02001-A /home/dpovey/kaldi-trunk/tools/sph2pipe_v2.5/sph2pipe -f wav -p -c 1 /export/corpora3/LDC/LDC97S62/swb1/sw02001.sph |
sw02001-B /home/dpovey/kaldi-trunk/tools/sph2pipe_v2.5/sph2pipe -f wav -p -c 2 /export/corpora3/LDC/LDC97S62/swb1/sw02001.sph |

這個檔案的格式是

<recording-id> <extended-filename>

其中,“extended-filename”可能是一個實際的檔名,或者就像本例中所述那樣,是一段提取wav格式檔案的命令。 extended-filename末尾的管道符號表明,整個命令應該被解釋為一個管道。等會我們會解釋什麼是“recording-id”, 但是首先,我們需要指出,如果“segments”檔案不存在,“wav.scp”每一行的第一項就是發音編號。 在Switchboard設定中,我們有“segments”檔案,所以下面我們就討論一下這個檔案。

s5# head -3 data/train/segments
sw02001-A_000098-001156 sw02001-A 0.98 11.56
sw02001-A_001980-002131 sw02001-A 19.8 21.31
sw02001-A_002736-002893 sw02001-A 27.36 28.93

"segments" 檔案的格式是:

<utterance-id> <recording-id> <segment-begin> <segment-end>

其中,segment-begin和segment-end以秒為單位。它們指明瞭一段發音在一段錄音中的時間偏移量。“recording-id” 和在“wav.scp”中使用的是同一個標識字串。再次宣告一下,這只是一個任意的標識字串,你可以隨便指定。 檔案"reco2file_and_channel"只是在你用NIST的sclite工具對結果進行評分(計算錯誤率)的時候使用:

s5# head -3 data/train/reco2file_and_channel 
sw02001-A sw02001 A
sw02001-B sw02001 B
sw02005-A sw02005 A

格式為:

<recording-id> <filename> <recording-side (A or B)>

filename通常是.sph檔案的名字,當然需要去掉sph這個字尾;但是也可以是任何其他你在“stm”檔案中使用的標識字串。 “錄音方”(recording side)則是一個電話對話中兩方通話(A或者B)的概念。如果不是兩方通話,那麼為保險起見最好 使用“A”。如果你並沒有“stm”檔案,或者你根本不知道這些都是什麼東西,那麼你可能就不需要reco2file_and_channel"檔案。 最後一個需要你手動建立的檔案是“utt2spk”。該檔案指明某一段發音是哪一個說話人發出的。

s5# head -3 data/train/utt2spk
sw02001-A_000098-001156 2001-A
sw02001-A_001980-002131 2001-A
sw02001-A_002736-002893 2001-A

檔案格式是:

<utterance-id> <speaker-id>

注意一點,說話人編號並不需要與說話人實際的名字完全一致——只需要大概能夠猜出來就行。 在這種情況下,我們假定每一個說話方(電話對話的每一方)對應一個說話人。這可能不完全正確—— 有時一個說話人會把電話交給另外一個說話人,或者同一個說話人會在不同的對話中出現——但是上述假定 對我們來說也足夠用了。如果你完全沒有關於說話人的資訊,你可以把發音編號當做說話人編號。那麼 對應的檔案格式就變為```

```。 在一些例項指令碼中還出現了另外一個檔案,它在Kaldi的識別系統的建立過程中只是偶爾使用。在Resource Management (RM)設定中該檔案是這樣的:

s5# head -3 ../../rm/s5/data/train/spk2gender
adg0 f
ahh0 m
ajp0 m

這個檔案根據說話人的性別,將每個說話人編號對映為“m”或者“f”。 上述所有檔案都應該被排序。如果沒有排序,你在執行指令碼的時候就會出現錯誤。在 \ref io_sec_tables 中我們解釋了為什麼需要這樣。這與(Kaldi的)I/O框架有關,歸根到底是因為排序後的檔案可以在一些不支援 fseek()的流中——例如,含有管道的命令——提供類似於隨機存取查詢的功能。需要Kaldi程式都會從其他Kaldi命令 中讀取多個管道流,讀入各種不同型別的物件,然後對不同輸入做一些類似於“合併然後排序”的處理。既然要合併排序, 當然需要輸入是經過排序的。小心確保你的shell環境變數LC_ALL定義為“C”。例如,在bash中,你需要這樣做:

export LC_ALL=C

如果你不這樣做,這些檔案的排序方式會與C++排序字串的方式不一樣,Kaldi就會崩潰。這一點我已經再三強調過了!

如果你的資料集中包含NIST提供的測試集,其中有“stm”和“glm”檔案可以用作計算WER,那麼你可以直接把這些檔案拷貝 到資料目錄下,並分別命名為“stm”和“glm”。注意,我們把評分指令碼score.sh(可以計算WER)放到local/下, 這意味著該指令碼是與資料集相關的。不是所有的示例設定下的評分指令碼都能識別stm和glm檔案。能夠使用這些檔案的一個 例子在Switchboard設定裡,即egs/swbd/s5/local/score_sclite.sh。如果檢測到你有stm和glm檔案該指令碼會被頂層的評分指令碼egs/swbd/s5/local/score.sh呼叫。

不需要手動建立的檔案

資料目錄下的其他檔案可以由前述你提供的檔案所生成。你可以用如下的一條命令建立“spk2utt”檔案( 這是一條從egs/rm/s5/local/rm_data_prep.sh中摘取的命令):

utils/utt2spk_to_spk2utt.pl data/train/utt2spk > data/train/spk2utt

這是因為utt2spk和spk2utt檔案中包含的資訊是一樣的。spk2utt檔案的格式是:

<speaker-id> <utterance-id1> <utterance-id2> ....

下面我們講一講feats.scp檔案.

s5# head -3 data/train/feats.scp
sw02001-A_000098-001156 /home/dpovey/kaldi-trunk/egs/swbd/s5/mfcc/raw_mfcc_train.1.ark:24
sw02001-A_001980-002131 /home/dpovey/kaldi-trunk/egs/swbd/s5/mfcc/raw_mfcc_train.1.ark:54975
sw02001-A_002736-002893 /home/dpovey/kaldi-trunk/egs/swbd/s5/mfcc/raw_mfcc_train.1.ark:62762

這個檔案指向已經提取好的特徵——在這個例子中所使用的是MFCC。feats.scp檔案的格式是:

<utterance-id> <extended-filename-of-features>

每一個特徵檔案儲存的都是Kaldi格式的矩陣。在這個例子中,矩陣的維度是13(譯者注:即列數;行數則 和你的檔案長度有關,標準情況下幀長20ms,幀移10ms,所以一行特徵資料對應10ms的音訊資料)。第一行的“extended filename”, 即/home/dpovey/kaldi-trunk/egs/swbd/s5/mfcc/raw_mfcc_train.1.ark:24,意思是,開啟存檔(archive)檔案/home/dpovey/kaldi-trunk/egs/swbd/s5/mfcc/raw_mfcc_train.1.ark, fseek()定位到24(位元組),然後開始讀資料。

feats.scp檔案由如下命令建立:

steps/make_mfcc.sh --nj 20 --cmd "$train_cmd" data/train exp/make_mfcc/train $mfccdir

該句被底層的run.sh指令碼呼叫。命令中一些shell變數的定義,請查閱對應run.sh。$mfccdir是.ark檔案將被寫入的目錄,由使用者自定義。

data/train下最後一個未講到的檔案是cmvn.scp。該檔案包含了倒譜均值和方差歸一化的統計量,以說話人編號為索引。 每個統計量幾何都是一個矩陣,在本例中是2乘以14維。在我們的例子中,有:

s5# head -3 data/train/cmvn.scp 
2001-A /home/dpovey/kaldi-trunk/egs/swbd/s5/mfcc/cmvn_train.ark:7
2001-B /home/dpovey/kaldi-trunk/egs/swbd/s5/mfcc/cmvn_train.ark:253
2005-A /home/dpovey/kaldi-trunk/egs/swbd/s5/mfcc/cmvn_train.ark:499

與feats.scp不同,這個scp檔案是以說話人編號為索引,而不是發音編號。該檔案由如下的命令建立:

steps/compute_cmvn_stats.sh data/train exp/make_mfcc/train $mfccdir

(這個例子來自egs/swbd/s5/run.sh).

因為資料準備階段的錯誤會影響後續指令碼的執行,所以我們有一個指令碼資料目錄的檔案格式是否 正確。執行:

utils/validate_data_dir.sh data/train

你可能會發現下面這個命令也很有用:

utils/fix_data_dir.sh data/train

(當然可對任何資料目錄使用該命令,而不只是data/train)。該指令碼會修復排序錯誤,並會移除那些被指明需要特徵資料或標註,但是卻找不到被需要的資料的那些發音(utterances)。

資料準備-- “lang”目錄

現在我們關注一下資料準備的“lang”這個目錄。

s5# ls data/lang
L.fst  L_disambig.fst  oov.int    oov.txt  phones  phones.txt  topo  words.txt

除data/lang,可能還有其他目錄擁有相似的檔案格式:例如有個目錄被命名為“data/lang_test”,其中包含和data/lang完全一樣的資訊,但是要多一個G.fst檔案。該檔案是一個FST形式的語言模型:

s5# ls data/lang_test
G.fst  L.fst  L_disambig.fst  oov.int  oov.txt    phones    phones.txt  topo  words.txt

注意,lang_test/由拷貝lang/目錄而來,並加入了G.fst。每個這樣的目錄都似乎只包含為數不多的幾個檔案。但事實上不止如此,因為其中phones是一個目錄而不是檔案:

s5# ls data/lang/phones
context_indep.csl  disambig.txt         nonsilence.txt        roots.txt    silence.txt
context_indep.int  extra_questions.int  optional_silence.csl  sets.int     word_boundary.int
context_indep.txt  extra_questions.txt  optional_silence.int  sets.txt     word_boundary.txt
disambig.csl       nonsilence.csl       optional_silence.txt  silence.csl

phones目錄下有許多關於音素集的資訊。同一類資訊可能有三種不同的格式,分別以.csl、.int和.txt結尾。幸運的是,作為一個Kaldi使用者,你沒有必要去一一手動建立所有這些檔案,因為我們有一個指令碼utils/prepare_lang.sh能夠根據更簡單的輸入為你建立所有這些檔案。在講述該指令碼和所謂更簡單的輸入之前,有必要先解釋一下lang目錄下到底有些什麼內容。之後我們將解釋如何輕鬆建立該目錄。如果使用者不需要理解Kaldi是如何工作的,而是秉著快速建立識別系統的目的,那麼可以跳過下面的“lang”目錄下的內容這一節。

“lang”目錄下的內容

首先是有檔案phones.txtwords.txt。這些都是符號表(symbol-table)檔案,符合OpenFst的格式定義。其中每一行首先是一個文字項,接著是一個數字項:

s5# head -3 data/lang/phones.txt
<eps> 0
SIL 1
SIL_B 2
s5# head -3 data/lang/words.txt
<eps> 0
!SIL 1
-'S 2

在Kaldi中,這些檔案被用於在這些音素符號的文字形式和數字形式之間進行轉換。 大多數情況下,只有指令碼utils/int2sym.pl、utils/sym2int.pl和OpenFst中的程式fstcompile和fstprint會讀取這些檔案。

檔案L.fst是FST形式的發音字典(L,見 "Speech Recognition with Weighted Finite-State Transducers" by Mohri, Pereira and Riley, in Springer Handbook on SpeechProcessing and Speech Communication, 2008), 其中,輸入是音素,輸出是詞。檔案L_disambig.fst也是發音字典,但是還包含了為消歧而引入的符號,諸如#1、 #2 之類,以及為自環(self-loop)而引入的 #0 。 #0 能讓消岐符號“通過”(pass through)整個語法(譯者注:n元語法,即我們的語言模型。 另外前面這句話我是在不知道該怎麼翻譯)。更多解釋見 \ref graph_disambig 。但是不管 明白與否,你其實不用自己手動去引入這些符號。

檔案data/lang/oov.txt僅僅只有一行:

s5# cat data/lang/oov.txt
<UNK>

在訓練過程中,所有詞彙表以外的詞都會被對映為這個詞(譯者注:UNK即unknown)。“”本身並沒有特殊的地方,也不一定非要用這個詞。重要的是需要保證這個詞 的發音只包含一個被指定為“垃圾音素”(garbage phone)的音素。該音素會與各種 口語噪聲對齊。在我們的這個特別設定中,該音素被稱為<SPN>, 就是“spoken noise”的縮寫:

s5# grep -w UNK data/local/dict/lexicon.txt 
<UNK> SPN

檔案oov.int則是SPN的數字形式(從words.txt中提取的),在本設定中是221。你可能已經注意到了,在Resource Management設定中,oov.txt裡有一個 靜音詞,在RM設定中被稱為“!SIL”。在這種情況下,我們從詞彙表中任意選一個詞(放入oov.txt)—— 因為訓練集中沒有oov詞,所以選哪個都不起作用。

檔案data/lang/topo則含有如下資料:

s5# cat data/lang/topo
<Topology>
<TopologyEntry>
<ForPhones>
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 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
</ForPhones>
<State> 0 <PdfClass> 0 <Transition> 0 0.75 <Transition> 1 0.25 </State>
<State> 1 <PdfClass> 1 <Transition> 1 0.75 <Transition> 2 0.25 </State>
<State> 2 <PdfClass> 2 <Transition> 2 0.75 <Transition> 3 0.25 </State>
<State> 3 </State>
</TopologyEntry>
<TopologyEntry>
<ForPhones>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
</ForPhones>
<State> 0 <PdfClass> 0 <Transition> 0 0.25 <Transition> 1 0.25 <Transition> 2 0.25 <Transition> 3 0.25 </State>
<State> 1 <PdfClass> 1 <Transition> 1 0.25 <Transition> 2 0.25 <Transition> 3 0.25 <Transition> 4 0.25 </State>
<State> 2 <PdfClass> 2 <Transition> 1 0.25 <Transition> 2 0.25 <Transition> 3 0.25 <Transition> 4 0.25 </State>
<State> 3 <PdfClass> 3 <Transition> 1 0.25 <Transition> 2 0.25 <Transition> 3 0.25 <Transition> 4 0.25 </State>
<State> 4 <PdfClass> 4 <Transition> 4 0.75 <Transition> 5 0.25 </State>
<State> 5 </State>
</TopologyEntry>
</Topology>

這個檔案指明瞭我們所用HMM模型的拓撲結構。在這個例子中,一個“真正”的音素內含3個發射狀態,呈標準 的三狀態從左到右拓撲結構——即“Bakis”模型。發射狀態即能“發射”特徵向量的狀態,與之對應的就是那些 “假”的僅用於連線其他狀態的非發射狀態。音素1到20是各種靜音和噪聲。之所以會有這麼多, 是因為低詞中的不同位置相同音素進行了區分。這種情況下,實際上這裡的靜音和噪聲音素大多數都用不上。 不考慮在詞中的位置的話,應該只有5個代表靜音和噪聲的音素。所謂靜音音素(silence phones)有更復雜的 拓撲結構。每個靜音音素都有一個起始發射狀態和一個結束髮射狀態,中間還有另外三個發射狀態。 你不用手動建立data/lang/topo

data/lang/phones/下有一系列的檔案,指明瞭音素集合的各種資訊。這些檔案大多數有三個不同版本:一個 “.txt”形式,如:

s5# head -3 data/lang/phones/context_indep.txt 
SIL
SIL_B
SIL_E

一個“.int”形式,如:

s5# head -3 data/lang/phones/context_indep.int
1
2
3

以及一個“.csl”形式,內含一個冒號分割的列表:

s5# cat data/lang/phones/context_indep.csl 
1:2:3:4:5:6:7:8:9:10:11:12:13:14:15:16:17:18:19:20

三種形式的檔案包含的是相同的資訊,所以我們只關注人們更易閱讀的“.txt”形式。檔案context_indep.txt包含一個音素列表,用於建立文字無關的模型。也就是說,對這些音素。 我們不會建立需要參考左右音素上下文的決策樹。實際上,我們建立的是更小的決策樹,只參考中心音素和HMM狀態。這依賴於roots.txt,下面將會介紹到。關於決策樹的更深入討論見 \ref tree_externals 。

檔案context_indep.txt包含所有音素,包括那些所謂“假”音素:例如靜音SIL,口語噪聲SPN, 非口語噪聲NSN和笑聲LAU

# cat data/lang/phones/context_indep.txt
SIL
SIL_B
SIL_E
SIL_I
SIL_S
SPN
SPN_B
SPN_E
SPN_I
SPN_S
NSN
NSN_B
NSN_E
NSN_I
NSN_S
LAU
LAU_B
LAU_E
LAU_I
LAU_S

因為考慮了在詞中的位置,這些音素都有許多變體。不是所有的變體都會被實際使用。這裡,SIL代表靜音詞, 會被插入到發音字典中(是一個詞而不是一個詞的一部分,可選的);SIL_B則代表一個靜音音素,應該出現 在一個詞的開端(這種情況應該永不出現);SIL_I代表詞內靜音(也很少存在);SIL_E代表 詞末靜音(不應該存在);而SIL_S則表示一種被視為“單獨詞”(singleton word)的靜音,意指這個音素 只對應一個詞——當你的發音字典中有“靜音詞”且標註中有明確的靜音段時會有用。

silence.txtnonsilence.txt分別包含靜音音素列表和非靜音音素列表。 這兩個集合是互斥的,且如果合併在一起,應該是音素的總集。在本例中,silence.txtcontext_indep.txt的內容完全一致。 我們說“非靜音”音素,是指我們將要估計各種線性變換的音素。所謂線性變換是指全域性變換,如LDA 和MLLT,以及說話人自適應變換,如fMLLR。 根據之前的實驗,我們相信,加入靜音對這些變換沒有 影響。我們的經驗是,把噪聲和發生噪聲都列為“靜音”音素,而其他傳統的音素則是“非靜音”音素。 在Kaldi中我們還沒有通過實驗找到一個最佳的方法來這樣做。

s5# head -3 data/lang/phones/silence.txt 
SIL
SIL_B
SIL_E
s5# head -3 data/lang/phones/nonsilence.txt 
IY_B
IY_E
IY_I

disambig.txt包含一個“消岐符號”列表“(見 \ref graph_disambig):

s5# head -3 data/lang/phones/disambig.txt 
#0
#1
#2

這些符號會出現在phones.txt中,被當做音素使用。

optional_silence.txt只含有一個音素。該音素可在需要的時候出現在詞之間:

s5# cat data/lang/phones/optional_silence.txt 
SIL

你可以自行選擇是否讓該音素出現在詞之間,方法就是在發音字典的FST中,可選地讓該音素 出現在每個詞的詞尾(以及每段發音的前面)。該因素必須在phones/中 指明而不是僅僅出現在L.fst中。這個原因比較複雜,這裡就不講了。

檔案sets.txt包含一系列的音素集,在聚類音素時被分組(被當做同一個音素),以便建立文字相關問題集(在Kaldi中,建立決策樹時使用自動生成的問題集,而不是具有語言語義 的問題集)。 在本設定中,sets.txt將每個音素的所有與在詞中位置相關的變體 組合為一行:

s5# head -3 data/lang/phones/sets.txt 
SIL SIL_B SIL_E SIL_I SIL_S
SPN SPN_B SPN_E SPN_I SPN_S
NSN NSN_B NSN_E NSN_I NSN_S

檔案extra_questions.txt包含那些自動產生的問題集之外的一些問題:

s5# cat data/lang/phones/extra_questions.txt 
IY_B B_B D_B F_B G_B K_B SH_B L_B M_B N_B OW_B AA_B TH_B P_B OY_B R_B UH_B AE_B S_B T_B AH_B V_B W_B Y_B Z_B CH_B AO_B DH_B UW_B ZH_B EH_B AW_B AX_B EL_B AY_B EN_B HH_B ER_B IH_B JH_B EY_B NG_B 
IY_E B_E D_E F_E G_E K_E SH_E L_E M_E N_E OW_E AA_E TH_E P_E OY_E R_E UH_E AE_E S_E T_E AH_E V_E W_E Y_E Z_E CH_E AO_E DH_E UW_E ZH_E EH_E AW_E AX_E EL_E AY_E EN_E HH_E ER_E IH_E JH_E EY_E NG_E 
IY_I B_I D_I F_I G_I K_I SH_I L_I M_I N_I OW_I AA_I TH_I P_I OY_I R_I UH_I AE_I S_I T_I AH_I V_I W_I Y_I Z_I CH_I AO_I DH_I UW_I ZH_I EH_I AW_I AX_I EL_I AY_I EN_I HH_I ER_I IH_I JH_I EY_I NG_I 
IY_S B_S D_S F_S G_S K_S SH_S L_S M_S N_S OW_S AA_S TH_S P_S OY_S R_S UH_S AE_S S_S T_S AH_S V_S W_S Y_S Z_S CH_S AO_S DH_S UW_S ZH_S EH_S AW_S AX_S EL_S AY_S EN_S HH_S ER_S IH_S JH_S EY_S NG_S 
SIL SPN NSN LAU 
SIL_B SPN_B NSN_B LAU_B 
SIL_E SPN_E NSN_E LAU_E 
SIL_I SPN_I NSN_I LAU_I 
SIL_S SPN_S NSN_S LAU_S

你可以看到,所謂一個問題就是一組音素。 前4個問題是關於普通音素的詞位資訊,後面五個則是關於”靜音音素“的。 ”靜音“音素也可能沒有像_B這樣的字尾,比如SIL。這些可被作為發音字典中可選的靜音詞的表示,即不會出現在某個詞中,而是 單獨成詞。 在具有語調和語氣的設定中,extra_questions.txt可以包含與之相關的問題集。

word_boundary.txt解釋了這些音素與詞位的關聯情況:

s5# head  data/lang/phones/word_boundary.txt 
SIL nonword
SIL_B begin
SIL_E end
SIL_I internal
SIL_S singleton
SPN nonword
SPN_B begin

這和音素中的字尾是相同的資訊,但我們並不想給音素的文字形式附加這樣的強制限制——記住一件事, Kaldi的可執行程式從不使用音素的文字形式,而是整數形式。)所以我們使用檔案word_boundary.txt來指明各音素與詞位間的對應關係。建立這種對應關係的原因是因為我們需要這些資訊從音素網格中恢復詞的邊界(例如,lattice-align-words 需要讀取word_boundary.txt的整數版本word_boundary.int)。找出詞的邊界是有 用的,其中之一是用作NIST的sclite評分,該工具需要詞的時間標記。還有其他的後續處理需要這些資訊。

roots.txt包含如何建立音素上下文決策樹的資訊:

head data/lang/phones/roots.txt 
shared split SIL SIL_B SIL_E SIL_I SIL_S
shared split SPN SPN_B SPN_E SPN_I SPN_S
shared split NSN NSN_B NSN_E NSN_I NSN_S
shared split LAU LAU_B LAU_E LAU_I LAU_S
...
shared split B_B B_E B_I B_S

暫時你可以忽略”shared“和”split“——這些與我們建立決策樹時的具體選項有關(更多資訊見 \ref tree_externals)。 像SIL SIL_B SIL_E SIL_I SIL_S這樣,幾個音素出現在同一行的意義是, 在決策樹中它們都有同一個”共享根“(shared root), 因此狀態可在這些音素間共享。 對於帶語氣語調的系統,通常所有與語氣和語調相關的音素變體都會出現在同一行。此外, 一個HMM中的3個狀態(對靜音來說有5個狀態)共享一個根, 且決策樹的建立過程需要知道 狀態(的共享情況)。 HMM狀態間共享決策樹根節點,這就是roots檔案中”shared“代表的意思。

建立"lang"目錄

data/lang/目錄下有很多不同檔案,所以我們提供了一個指令碼為你建立這個目錄,你只需要提供一些相對簡單的輸入資訊:

utils/prepare_lang.sh data/local/dict "<UNK>" data/local/lang data/lang

這裡,輸入目錄是data/local/dict/<UNK>需在字典中,是標註中 所有OOV詞的對映詞(對映情況會寫入data/lang/oov.txt中)。data/local/lang/只是指令碼使用的一個臨時目錄,data/lang/才是輸出檔案將會寫入的地方。

作為資料準備者,你需要做的事就是建立data/local/dict/這個目錄。該 目錄包含以下資訊:

s5# ls data/local/dict
extra_questions.txt  lexicon.txt nonsilence_phones.txt  optional_silence.txt  silence_phones.txt

(實際上還有一些檔案我們沒有列出來,但那都是在建立目錄時所遺留下的臨時檔案,可以忽略)。下面的這些命令可以讓你知道這些檔案中大概都有些什麼:

s5# head -3 data/local/dict/nonsilence_phones.txt 
IY
B
D
s5# cat data/local/dict/silence_phones.txt 
SIL
SPN
NSN
LAU
s5# cat data/local/dict/extra_questions.txt 
s5# head -5 data/local/dict/lexicon.txt 
!SIL SIL
-'S S
-'S Z
-'T K UH D EN T
-1K W AH N K EY

正如你看到的,本設定(Switchboard)中,這個目錄下的內容都非常簡單。 我們只是分別列出了“真正”的音素和“靜音”音素,一個叫extra_questions.txt的空檔案,以及一個有如下格式的lexicon.txt

<word> <phone1> <phone2> ...

注意:lexicon.txt中,如果一個詞有不同發音,則會在不同行中出現多次。如果你想使用發音概率,你需要建立lexiconp.txt而不是lexicon.txt

lexiconp.txt中第二域就是概率值。 注意,一個通常的作法是,對發音概率進行歸一化,使最大的那個概率值為1,而不是使同一個 詞的所有發音概率加起來等於1。 這樣可能會得到更好的結果。 如果想在頂層指令碼中找一個 與發音概率相關的指令碼,請在egs/wsj/s5/run.sh目錄下搜尋pp

需要注意的是,在這些輸入中,沒有詞位資訊,即沒有像_B_E這樣的字尾。 這是因為指令碼prepare_lang.sh新增這些字尾。

從空的extra_questions.txt檔案中你會發現,可能還有些潛在的功能我們沒有利用。 這其中就包括重音和語調標記。對具有不同重音和語調的同一音素,你可能會想用不同的標記去表示。 為展示如何這樣做,我們看看在另外一個設定egs/wsj/s5/下的這些檔案。結果如下:

s5# cat data/local/dict/silence_phones.txt 
SIL
SPN
NSN
s5# head data/local/dict/nonsilence_phones.txt 
S 
UW UW0 UW1 UW2 
T 
N 
K 
Y 
Z 
AO AO0 AO1 AO2 
AY AY0 AY1 AY2 
SH 
s5# head -6 data/local/dict/lexicon.txt 
!SIL SIL
<SPOKEN_NOISE> SPN
<UNK> SPN
<NOISE> NSN
!EXCLAMATION-POINT  EH2 K S K L AH0 M EY1 SH AH0 N P OY2 N T
"CLOSE-QUOTE  K L OW1 Z K W OW1 T
s5# cat data/local/dict/extra_questions.txt 
SIL SPN NSN 
S UW T N K Y Z AO AY SH W NG EY B CH OY JH D ZH G UH F V ER AA IH M DH L AH P OW AW HH AE R TH IY EH 
UW1 AO1 AY1 EY1 OY1 UH1 ER1 AA1 IH1 AH1 OW1 AW1 AE1 IY1 EH1 
UW0 AO0 AY0 EY0 OY0 UH0 ER0 AA0 IH0 AH0 OW0 AW0 AE0 IY0 EH0 
UW2 AO2 AY2 EY2 OY2 UH2 ER2 AA2 IH2 AH2 OW2 AW2 AE2 IY2 EH2 
s5#

你可能已經注意到了,nonsilence_phones.txt中的某些行,一行中 有多個音素。這些是同一母音的與重音相關的不同表示。 注意,在CMU版的字典中, 每個音素有4種表示:例如,UW UW0 UW1 UW2。 基於某些原因,其中 一種表示沒有數字字尾。行中音素的順序沒有關係。通常,我們建議用於將每個“真實音素” 的不同形式都組織在單獨的一行中。 我們使用CMU字典中的重音標記。檔案extra_questions.txt中只有一個問題 包含所有的“靜音”音素(實際上這是不必要的,只是指令碼prepare_lang.sh會新增這麼一個問題),以及一個涉及不同重音標記的問題。 這些問題對利用重音標記資訊來說是必要的,因為在nonsilence_phones.txt中 每個音素的不同重音表示都在同一行,這確保了他們在data/lang/phones/roots.txtdata/lang/phones/sets.txt也屬同一行,這又反過來確保了它們共享同一個(決策)樹 根,並且不會有決策問題弄混它們。因此,我們需要提供一個特別的問題,能為決策樹的建立過程 提供一種區分音素的方法。 注意:我們在sets.txtroots.txt中 將音素分組放在一起的原因是,這些同一音素的不同重音變體可能缺乏足夠的資料去穩健地估計 一個單獨的決策樹,或者是產生問題集時需要的聚類資訊。 像這樣把它們組合在一起,我們 可以確保當資料不足以對它們分別估計決策樹時,這些變體能在決策樹的建立過程中“聚集在一起”(stay together)。

寫到這裡我們需要提一點,指令碼utils/prepare_lang.sh支援很多選項。下面是該指令碼 的用法,可讓你們瞭解這些選項都有哪些:

usage: utils/prepare_lang.sh <dict-src-dir> <oov-dict-entry> <tmp-dir> <lang-dir>
e.g.: utils/prepare_lang.sh data/local/dict <SPOKEN_NOISE> data/local/lang data/lang
options: 
     --num-sil-states <number of states>             # default: 5, #states in silence models.
     --num-nonsil-states <number of states>          # default: 3, #states in non-silence models.
     --position-dependent-phones (true|false)        # default: true; if true, use _B, _E, _S & _I
                                                     # markers on phones to indicate word-internal positions.
     --share-silence-phones (true|false)             # default: false; if true, share pdfs of
                                                     # all non-silence phones.
     --sil-prob <probability of silence>             # default: 0.5 [must have 0 < silprob < 1]

一個可能的重要選項是--share-silence-phones。 該選項預設是false。 如果該選項被設為true, 所有靜音音素——如靜音、發生噪聲、 噪聲和笑聲——的概率密度函式(PDF,高斯混合模型)都會共享,只有模型中的轉移概率不同。 現在還不清楚為什麼這樣做有用,但我們發現者對IARPA的BABEL專案中的廣東話資料集非常有效。 該資料集非常亂,其中有很長的未標註的部分,我們試著將其與一個特別標記的音素對齊。 我們懷疑訓練資料可能沒能成功正確對齊,而且基於某些不明原因,將上述選項設定為true則 改變了結果。

另外一個可能的重要選項是--sil-prob。 通常,對這些選項我們所作的實驗都不同,所以 對具體如何設定也不能給出非常詳細的建議。

建立語言模型或者語法檔案

前面的關於如何建立lang/目錄的教程沒有涉及如何產生G.fst檔案。該檔案 是語言模型——或者稱為語法——的有限狀態轉換器格式的表示,我們解碼時需要它。 實際上,在一些設定 中,為做不同的測試,我們可能會有許多“lang”目錄。這些目錄中有不同的語言模型和字典。以 華爾街日報(WSJ)的設定為例:

s5# echo data/lang*
data/lang data/lang_test_bd_fg data/lang_test_bd_tg data/lang_test_bd_tgpr data/lang_test_bg \
 data/lang_test_bg_5k data/lang_test_tg data/lang_test_tg_5k data/lang_test_tgpr data/lang_test_tgpr_5k

根據我們使用的語言模型的不同——是統計語言模型還是別的種類的語法形式——生成G.fst的 步驟會不同。 在RM設定中,使用的是二元語法,只允許某些詞對。我們將總概率值1分配給所有向外 的弧,以確保每個語法狀態的概率和為1。在local/rm_data_prep.sh中有這樣一句程式碼:

local/make_rm_lm.pl $RMROOT/rm1_audio1/rm1/doc/wp_gram.txt  > $tmpdir/G.txt || exit 1;

指令碼local/make_rm_lm.pl會建立一個FST格式的語法檔案(文字格式,不是二進位制格式)。 該檔案包含如下形式的行:

s5# head data/local/tmp/G.txt 
0    1    ADD    ADD    5.19849703126583
0    2    AJAX+S    AJAX+S    5.19849703126583
0    3    APALACHICOLA+S    APALACHICOLA+S    5.19849703126583

www.openfst.org上查閱更多關於OpenFst的資訊(他們 有一個很詳細的教程)。 指令碼local/rm_prepare_grammar.sh會將文字格式的 語法檔案轉換為二進位制檔案G.fst。所用命令如下:

fstcompile --isymbols=data/lang/words.txt --osymbols=data/lang/words.txt --keep_isymbols=false \
    --keep_osymbols=false $tmpdir/G.txt > data/lang/G.fst

如果你要建立自己的語法問,你也應做類似的事。 注意:這種過程只適用於一類語法:用上述方法你不能建立上下文無法的語法,因為這類語法不 能被表示為OpenFst格式。 在WFST框架下還是有辦法這麼做(見Mike Riley最近關於push down transducers 的研究工作),但是在Kaldi中我們還沒實現這些功能。

在WSJ設定中,我們使用了一個統計語言模型。 指令碼local/wsj_format_data.sh將WSJ資料庫提供的ARPA格式的語言模型檔案轉換為OpenFst格式的。 指令碼中關鍵的命令如下:

  gunzip -c $lmdir/lm_${lm_suffix}.arpa.gz | \
   utils/find_arpa_oovs.pl $test/words.txt  > $tmpdir/oovs_${lm_suffix}.txt
 ...
  gunzip -c $lmdir/lm_${lm_suffix}.arpa.gz | \
    grep -v '<s> <s>' | \
    grep -v '</s> <s>' | \
    grep -v '</s> </s>' | \
    arpa2fst - | fstprint | \
    utils/remove_oovs.pl $tmpdir/oovs_${lm_suffix}.txt | \
    utils/eps2disambig.pl | utils/s2eps.pl | fstcompile --isymbols=$test/words.txt \
      --osymbols=$test/words.txt  --keep_isymbols=false --keep_osymbols=false | \
     fstrmepsilon > $test/G.fst

這裡,變數$test的值形如data/lang_test_tg。最重要的一條命令是arpa2fst, 這是一個Kaldi程式。該程式將ARPA格式的語言模型轉換為一個 加權有限狀態轉換器(實際上是一個接收器)。

grep命令移除語言模型中“不可用”的N元語法。 程式remove_oovs.pl移除包含集外詞的N元語法(如果不移除會引起fstcompile崩潰)。eps2disambig.pl將回退弧上的<eps>()符號轉換為一個特殊的符號#0,以 保證語法檔案是確定的(determinizable),見 \ref graph_disambig 。 如果你知道“回退弧”是什麼, 你可以參考關於回退N元語法的文獻,例如Goodman的A bit of progress in language modeling,以及我們前面引用的Mohri的論文。 命令s2eps.pl將句首和句末 符號<s></s>轉換為epsilon(<eps>), 意即“沒有符號”。fstcompile是一個OpenFst命令,可將文字形式的FST轉換為二進位制形式的。fstrmepsilon也是一個OpenFst命令,可將FST中由<s></s>替換而來的 少量的<eps>符號移除掉。