数据准备

翻译:V([email protected])

时间:2014年5月

介绍

在运行完示例脚本后(见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>符号移除掉。