Kaldi Tutorial翻译

英文原版地址:Kaldi: Kaldi tutorial

Github地址:kaldi Github

前提条件

本教程假设您了解使用HMM-GMM方法进行语音识别的基本知识。网上提供的一个简介是:M.Gales和S.Young(2007年)。“隐马尔可夫模型在语音识别中的应用”信号处理的基础和趋势1(3):195-304。HTK图书也是一个很好的资源。然而,除非你有很强的数学背景,并且非常专注,否则我们不愿意在机构设置之外学习语音识别。本教程的预期受众要么是语音识别研究人员,要么是毕业生,要么是正在学习这一领域的研究生。

我们假设您了解C++,并且至少熟悉shell脚本,最好使用bash或类似的shell。本教程假设您正在使用类似UNIX的环境或Cygwin(尽管Kaldi不一定在所有此类环境中编译和运行)。

此外,重要的是,本教程假设您可以从语言数据集团(LDC)以最不发达国家分发的原始形式访问资源管理光盘上的数据。也就是说,我们假设这些数据位于您的系统的某处。我们获得的这个目录号为LDC93S3A。它也有两个单独的部分。要小心,因为以前RM数据的分布是不同的,布局是不同的。

系统要求相当基本。我们假设您有工具,包括wget、git、svn、awk、perl等等,或者您知道如何安装它们。安装过程中最困难的部分与数学库ATLAS有关;如果尚未将其安装为系统上的库,则必须编译它,这需要关闭CPU节流,这可能需要root特权。我们提供所有安装步骤的脚本和详细说明。当脚本失败时,仔细阅读输出,因为它试图为如何修复问题提供指导。如果有任何问题,无论多么小,请告知我们;请参阅其他与Kaldi相关的资源(以及如何获得帮助)。

我们试图为它应该需要多长时间来执行教程的每一步提供一些想法。如果有有限的时间来完成教程,我们建议尽量发布的时间表保持一致,如果必要的话,可以跳过避免以下链接到我们在文本中提供的更多信息的步骤。这将有助于确保您得到一个平衡的概述。你总是可以在稍后更详细地回顾材料。如果要在课堂设置中提供本教程,那么重要的是,必须事先在相关系统上运行教程,以验证是否安装了所有先决条件。

天才第一步(15分钟)

第一步,下载安装Kaldi。我们将使用工具包的版本1,这样本教程就不会过时。但是,请注意,"trunk"中的代码和脚本(总是最新的)更容易安装,而且通常更好。如果使用"trunk"代码,也可以尝试使用最近的脚本,这些脚本位于目录“egs/rm/s5”中,而不是本教程中提到的"s3"脚本。但请注意,如果您这样做,教程的某些方面可能已经过时。

假设已经安装了Git,为了获得最新代码可以输入:

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

然后cd到kaldi目录。查看INSTALL文件并按照说明(它指向两个子目录)。仔细看看安装脚本的输出,因为它们试图指导您该做什么。有些安装错误是非致命的,安装脚本会告诉你(即它安装的一些东西是很好的,但不是真正需要的)。“最好的情况”是输入:

1
cd kaldi/tools/; make; cd ../src; ./configure; make

一切都将正常工作;然而,如果发生这种情况,就会有回退的计划(例如:您可能需要在机器上安装一些包,或者在Tools/中运行install_atlas.sh,或者手动运行Tools/INSTALL中的一些步骤,或者在src/中为配置脚本提供选项)。如果有问题,在构建过程中可能会有一些信息(Kaldi是如何编译的)将帮助您;否则,请随时与维护人员联系(其他与Kaldi相关的资源(以及如何获得帮助)),我们将很乐意提供帮助。

git版本控制

Git是一个分布式版本控制系统。这意味着,与Subversion不同的是,存储库有多个副本,并且这些更改以多种不同的方式显式地在这些副本之间传输,但大多数情况下,一个人的工作都由存储库的单个副本支持。由于副本的多重性,您可能希望遵循多个可能的工作流。这里有一个我们认为最适合您的,如果您只想编译和使用Kaldi,但在某一时刻选择贡献您的工作回到项目。

首次git设置

如果您以前从未使用过Git,请先执行一些最小配置。至少,设置您的姓名和电子邮件地址:

1
2
git config --global user.name "JackeyLea"
git config --global user.email 1768478912@qq.com

此外,为您最常键入的最有用的git命令设置简短的名称。

1
2
3
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.st status

另一个非常有用的工具是git-prompts.sh,它是Git的bash提示扩展工具(如果您没有它,请搜索Internet如何在系统上安装它)。当安装时,它提供了一个shell函数__git_ps1当添加到提示符时,扩展到当前分支名称和挂起的提交标记,因此不要忘记您在哪里。您可以修改PS1 shell变量,使其包括字面上的$(__git_ps1“[%s]”)。我在我的~/.bashrc中有这个:

1
2
3
4
5
6
PS1='\[\033[00;32m\]\u@\h\[\033[0m\]:\[\033[00;33m\]\w\[\033[01;36m\]$(__git_ps1 "[%s]")\[\033[01;33m\]\$\[\033[00m\] '
export GIT_PS1_SHOWDIRTYSTATE=true GIT_PS1_SHOWSTASHSTATE=true
# fake __git_ps1 when git-prompts.sh not installed
if [ "$(type -t __git_ps1)" == "" ]; then
function __git_ps1() { :; }
fi

用户工作流

使用此命令设置存储库和工作目录:

1
2
3
4
5
6
7
8
9
10
kkm@yupana:~$ git clone https://github.com/kaldi-asr/kaldi.git --branch master --single-branch --origin golden
Cloning into 'kaldi'...
remote: Counting objects: 51770, done.
remote: Compressing objects: 100% (8/8), done.
remote: Total 51770 (delta 2), reused 0 (delta 0), pack-reused 51762
Receiving objects: 100% (51770/51770), 67.72 MiB | 6.52 MiB/s, done.
Resolving deltas: 100% (41117/41117), done.
Checking connectivity... done.
kkm@yupana:~$ cd kaldi/
kkm@yupana:~/kaldi[master]$

现在,您可以配置和编译Kaldi并使用它。偶尔您需要本地分支中的最新更改。这类似于您通常对svn更新所做的工作。

但请首先让我们同意一件事:您不会在主分支上提交任何文件。我们到下面再说明(提交文件到master)。到目前为止,您只使用代码。如果你不遵守规则,很难解开,Git在分支方面非常容易,以至于你总是想在分支上做你的工作。

1
2
3
4
5
6
7
8
kkm@yupana:~/kaldi[master]$ git pull golden
remote: Counting objects: 148, done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 148 (delta 111), reused 130 (delta 93), pack-reused 0
Receiving objects: 100% (148/148), 18.39 KiB | 0 bytes/s, done.
Resolving deltas: 100% (111/111), completed with 63 local objects.
From https://github.com/kaldi-asr/kaldi
658e1b4..827a5d6 master -> golden/master

您使用的命令是git pull,而golden是我们以前用来指定Kaldi存储库的主副本的别名。

从用户到贡献者

在某个时候,您决定更改Kaldi代码,无论是脚本还是源代码。也许你做了一个简单的错误修复。也许你在贡献一个完整的方法。无论如何,你总是在一个分支上做你的工作。即使您有未提交的更改,Git也处理它。例如,您刚刚意识到fisher_english方法实际上并没有使用hubscr.pl进行评分,而是检查它的存在并失败。您可以在工作树中快速修复它,并希望与项目共享此更改。

本地分支工作

1
2
3
4
5
6
kkm@yupana:~/kaldi[master *]$ git fetch golden
kkm@yupana:~/kaldi[master *]$ git co golden/master -b fishfix --no-track
M fisher_english/s5/local/score.sh
Branch fishfix set up to track remote branch master from golden.
Switched to a new branch 'fishfix'
kkm@yupana:~/kaldi[myfix *]$

因此,我们在这里所做的,首先将当前对gloden存储库的更改获取到您的机器。这没有更新您的master(事实上,如果您有本地工作树更改,则不能pull),而是更新了远程引用Golden/Master。在第二个命令中,我们在本地存储库中分叉了一个分支,称为fishfix。是不是更符合逻辑?一点也不!首先,这是一个更多的操作。你不需要更新master,所以你为什么要? 第二,我们同意了(记得吗?)那个master不会有变化,但你也有一些。第三,相信我,这种情况发生了,你可能错误地向你的主人承诺了一些东西,你不想把这种野性的变化带入你的新分支。

现在你检查你的变化,既然它们是好的,你就承诺它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kkm@yupana:~/kaldi[fishfix *]$ git diff
diff --git a/egs/fisher_english/s5/local/score.sh b/egs/fisher_english/s5/local/score.sh
index 60e4706..552fada 100755
--- a/egs/fisher_english/s5/local/score.sh
+++ b/egs/fisher_english/s5/local/score.sh
@@ -27,10 +27,6 @@ dir=$3
model=$dir/../final.mdl # assume model one level up from decoding dir.
-hubscr=$KALDI_ROOT/tools/sctk/bin/hubscr.pl
-[ ! -f $hubscr ] && echo "Cannot find scoring program at $hubscr" && exit 1;
-hubdir=`dirname $hubscr`
-
for f in $data/text $lang/words.txt $dir/lat.1.gz; do
[ ! -f $f ] && echo "$0: expecting file $f to exist" && exit 1;
done
kkm@yupana:~/kaldi[fishfix *]$ git commit -am 'fisher_english scoring does not really need hubscr.pl from sctk.'
[fishfix d7d76fe] fisher_english scoring does not really need hubscr.pl from sctk.
1 file changed, 4 deletions(-)
kkm@yupana:~/kaldi[fishfix]$

请注意,-a转换到git commit使它提交所有修改后的文件(我们只有一个更改,那么为什么不呢?)。如果您想将文件修改分离为多个单独提交的功能,git添加特定文件,然后在没有-a开关的情况下添加git提交,然后在下一个修复的第一个点上启动另一个分支:git co Golden/master -b another-fix -no-track,在那里您可以添加和提交其他更改的文件。在Git中,有十几个分支正在运行并不少见。请记住,将多个特征分支组合为一个是非常容易的,但是将一个大的变更集分割成许多较小的特征需要更多的工作。

现在您需要向Kaldi的maintaners创建一个拉请求,以便他们可以从存储库中提取更改。为此,您的存储库需要在线提供给他们。为此,您需要一个GitHub帐户。

一次性GitHub设置

创建pull request

确保您的fork是在名称origin下注册的(别名是任意的,这就是我们这里要用到的)。如果没有,则添加。URL列在存储库页面的“SSH克隆URL”下,看起来像git@github.com:YOUR_USER_NAME/kaldi.git

1
2
3
4
5
6
7
8
9
kkm@yupana:~/kaldi[fishfix]$ git remote -v
golden https://github.com/kaldi-asr/kaldi.git (fetch)
golden https://github.com/kaldi-asr/kaldi.git (push)
kkm@yupana:~/kaldi[fishfix]$ git remote add origin git@github.com:kkm000/kaldi.git
kkm@yupana:~/kaldi[fishfix]$ git remote -v
golden https://github.com/kaldi-asr/kaldi.git (fetch)
golden https://github.com/kaldi-asr/kaldi.git (push)
origin git@github.com:kkm000/kaldi.git (fetch)
origin git@github.com:kkm000/kaldi.git (push)

现在把分支推到你的kaldi分支里:

1
2
3
4
5
6
7
8
9
kkm@yupana:~/kaldi[fishfix]$ git push origin HEAD -u
Counting objects: 632, done.
Delta compression using up to 12 threads.
Compressing objects: 100% (153/153), done.
Writing objects: 100% (415/415), 94.45 KiB | 0 bytes/s, done.
Total 415 (delta 324), reused 326 (delta 262)
To git@github.com:kkm000/kaldi.git
* [new branch] HEAD -> fishfix
Branch fishfix set up to track remote branch fishfix from origin.

git push中的HEAD告诉Git“在与当前分支同名的远程回购中创建分支”,并且-u还记得本地分支fishfix和存储库中origin/fishfix之间的连接。

现在转到存储库页面并创建一个拉请求。检查您的更改,并提交请求,如果一切看起来都很好。维护人员将收到请求,并接受或评论它。按照commit,提交修复您的分支,再次推送到原点,GitHub将自动更新拉请求网页。然后在你收到的评论下回复,例如“完成”,这样他们就知道你跟进了他们的评论。

如果您正在创建一个拉请求,仅用于审查一个不完整的工作,这是有意义的,并且如果您希望对建议的功能进行早期反馈,则使用前缀WIP开始您的拉请求的标题。这将告诉维护人员不要合并拉请求。当您向分支推送更多提交时,它们会自动显示在拉请求中。当您认为工作完成后,编辑拉请求标题以删除WIP前缀,然后为此添加注释,以便通知维护人员。

发布版概述

在我们跳入示例脚本之前,让我们花几分钟时间看看Kaldi发行版中还包含了哪些内容。转到kaldi-1目录并列出它。有几个文件和子目录。重要的子目录是tools/src/egs/,我们将在下一节中查看它们。我们将概述tools/src/

tools/目录(10分钟)

目录tools/是我们安装Kaldi以各种方式依赖的东西的地方。将目录更改为tools/并列出它。您将看到各种文件和子目录,主要是make命令安装的东西。快速查看文件INSTALL。此文件给出了如何安装工具的说明。

最重要的子目录是OpenFst的子目录。cd到openfst/。这是一个软链接到实际目录,其中有一个版本号。列出openfst目录。如果安装成功,将有一个带有已安装的二进制文件的bin/目录和一个带有库的lib/目录(我们需要这两个)。最重要的代码在include/fst/目录中/。如果你想深入了解kaldi,你需要了解OpenFst。为此,最好的出发点是www.openfst.org

现在,只需查看文件include/fst/fst.h。这包括一些抽象FST类型的声明。可以看到,涉及的模板很多。如果模板不是你的东西,你可能会很难理解这个代码。

将目录更改为bin/,或将其添加到路径中。我们将从这里执行一些简单的示例指令。

将以下命令粘贴到shell中:

1
2
3
4
5
6
7
8
9
10
# arc format: src dest ilabel olabel [weight]
# final state format: state [weight]
# lines may occur in any order except initial state must be first line
# unspecified weights default to 0.0 (for the library-default Weight type)
cat >text.fst <<EOF
0 1 a x .5
0 1 b y 1.5
1 2 c z 2.5
2 3.5
EOF

下面的命令创建符号表;也将它们粘贴到shell中。

1
2
3
4
5
6
7
8
9
10
11
12
13
cat >isyms.txt <<EOF
<eps> 0
a 1
b 2
c 3
EOF

cat >osyms.txt <<EOF
<eps> 0
x 1
y 2
z 3
EOF

注意:要实现以下步骤,如果路径上没有当前目录,则可能必须键入:

1
export PATH=.:$PATH

接下来创建一个二进制格式的FST:

1
fstcompile --isymbols=isyms.txt --osymbols=osyms.txt text.fst binary.fst

让我们执行一个示例命令:

1
fstinvert binary.fst | fstcompose - binary.fst > binary2.fst

得到的WFST,binary2.fst,应该与binary.fst相似,但权重为2倍。 你可以把它们都打印出来看看:

1
2
fstprint --isymbols=isyms.txt --osymbols=osyms.txt binary.fst
fstprint --isymbols=isyms.txt --osymbols=osyms.txt binary2.fst

此示例是从www.openfst.org提供的更长的教程中修改的。做完这件事后,通过输入来清理:

1
rm *.fst *.txt

src/目录(10分钟)

跳转目录到顶层(kaldi-1)并更改为src/。列出目录。您将看到一些文件和大量的子目录。查看Makefile。在顶部设置变量SUBDIRS。这是包含代码的子目录列表。注意其中一些以bin结尾。这些是包含可执行文件的文件(代码和可执行文件在同一个目录中)。其他目录包含内部代码。

您可以看到Makefile中的目标之一是test。键入make test。该命令进入各个子目录并在其中运行测试程序。所有的测试都应该成功。如果你感到幸运,也可以键入make valgrind。使用内存检查器运行相同的测试,需要更长的时间,但会发现更多的错误。如果这不起作用,那就算了;现在不重要了。如果时间太长,用ctrl-c停止。

将目录更改为base/。查看Makefile。注意行:

1
include ../kaldi.mk

每当调用子目录中的Makefile(就像C语言中#include指令一样)时,这一行都包含文件../kaldi.mk逐字记录。看看文件./kaldi.mk。它将包含一些与valgrind(用于内存调试)相关的规则,然后以诸如CXXFLAGS等变量的形式进行一些特定于系统的配置。查看是否有任何-O选项(例如:-0)。默认情况下,标志-O0和-DKALDI_PARANOID被禁用,因为它们会使事情变慢(您可能希望启用它们以进行更好的调试)。再查看base/Makefile。顶部的语句all:告诉Make:all是顶级目标(因为kaldi.mk中有目标,我们不希望这些目标成为顶级目标)。由于all的依赖关系依赖于后面定义的变量,所以我们有另一个语句(目标在default_rules.mk中定义),其中我们定义了all依赖于什么。找找看定义了其他几个目标,从clean开始,去找他们。要使clean,你会键入make clean。目标.valgrind不是您将从命令行调用的东西;您将键入make valgrind(目标在kaldi.mk中定义)。调用所有这些目标,即键入make clean和其他命令相同,并注意当您这样做时会发出哪些命令。

base/目录中的Makefile中:选择TESTFILES中列出的二进制文件之一,并运行它。然后简要查看相应的.cc文件。数学函数是一个很好的例子(注意:这排除了Kaldi中的大多数数学函数,它们是矩阵向量相关的函数,位于./matrix/)。注意,有很多使用宏KALDI_ASSERT的断言。这些测试程序被设计为在出现问题时以错误状态退出(它们不应该依赖于对输出的人工检查)。

查看头文件kaldi-math.h。您将看到我们的编码实践的一些元素。请注意,我们所有本地的#include都是相对于src/目录的(因此,我们添加#include <base/kaldi-type.h>,尽管我们已经在base/目录中)。请注意,我们#define的所有宏,除了我们只是确保它们的正常值的标准宏外,都是从KALDI_开始的。这是一种预防措施,以避免将来与其他代码库发生冲突(因为#define不会将自己限制在kaldi名称空间)。注意函数名的样式:LikeThis()。我们的风格一般是基于这一种,以符合OpenFst,但也有一些不同之处。

要查看样式的其他元素,这将帮助您理解Kaldi代码,cd到../util和查看text-utils.h。注意,这些函数的输入总是第一位的,通常是const引用,而输出(或修改后的输入)总是最后一位的,并且是指针参数。不允许将非const引用作为函数参数。如果您感兴趣,您可以稍后在这里阅读更多关于编码样式的Kaldi特定元素的信息。现在,请注意有一种具有相当特定规则的编码样式。

将目录更改为../gmmbin并输入

1
./gmm-init-model

它打印出用法,这将使您了解如何调用Kaldi程序。请注意,虽然有一个-config选项可以用来传递配置文件,但一般来说,Kaldi不像HTK那样由配置驱动,而且这些文件没有被广泛使用。您将看到一个-binary选项。一般来说,Kaldi文件格式有二进制和文本形式,而-binary选项控制它们的编写方式。然而,这只控制单个对象(例如声学模型)是如何写的。用于整个对象集合(例如功能文件的集合),有一个不同的机制,我们稍后会来说明。输入

1
./gmm-init-model >/dev/null

你看到了什么,这告诉你kaldi如何处理日志类型输出?使用消息所去的地方与所有错误和日志消息所去的地方是相同的,这有一个原因,当您开始查看脚本时,这应该变得明显。

要深入了解构建过程,可以使用cd到../matrix,然后输入

1
2
rm *.o
make

查看传递给编译器的选项。这些变量最终由./kaldi.mk中设置的变量控制,而这些变量又由./configure决定。还可以查看链接选项,当它创建matrix-lib-test时传入。您将了解它所链接的数学库(这在一定程度上取决于系统)。有关如何使用外部矩阵库的更多信息,您可以阅读外部矩阵库

将目录更改为上一级(src/),然后查看configure文件。如果您熟悉automake生成的configure文件,您将注意到它不是其中之一。它是手工生成的。在其中搜索"makefiles/",并快速扫描该字符串发生的所有位置(例如shell中输入less configure,键入/makefiles[Enter],然后键入n以查看后面的实例)。您将看到它使用了子目录makefiles/中带有后缀.mk的一些文件。这些基本上是kaldi.mk的“原型”版本。查看其中一个原型makefiles/cygwin.mk,查看它们包含的各种东西。对于更可预测的系统,它只是将特定于系统的makefile与makefile/kaldi.mk.common连接起来,并将其写入kaldi.mk。对于Linux,它必须做更多的调查,因为有这么多的发行版。这主要涉及到在哪里安装数学库。如果您在构建过程中遇到问题,一个解决方案是尝试手工修改kaldi.mk。为了做到这一点,您可能应该了解Kaldi如何使用外部数学库(参见外部矩阵库)。

运行示例脚本(40分钟)

开始、准备

本教程的下一个阶段是开始运行资源管理的示例脚本。将目录更改为顶层(我们称之为kaldi-1),然后更改为egs/。查看该目录中的README.txt文件,具体查看资源管理部分。它提到了与语料库对应的最不发达国家目录号。这可能有助于您从最不发达国家获得数据。如果您由于某种原因无法获得数据,只需继续阅读本教程并执行您可以在没有数据的情况下所做的步骤,您仍然可以从中获得一些值。最好的情况是系统上有一些目录,例如/export/corpora5/LDC/LDC93S3A/rm_comp,其中包含三个子目录;rm1_audio1、rm1_audio2和rm2_audio调用它们。这将与最不发达国家数据分发中的三个原始磁盘相对应。这些指令假设您的shell是bash。如果您有不同的shell,这些命令将不起作用,或者应该进行修改(只需键入"bash"进入bash,所有操作都应该起作用)。现在将目录更改为rm/,查看文件README.txt以查看总体结构是什么。cd改为s5/,这是与工具包版本5中的主要功能相对应的基本实验序列。在s5/中,列出目录并查看Result文件,这样您就可以了解其中的内容了(稍后,您应该验证您得到的结果是否与其中的内容相似)。我们将要查看的主要文件是run.sh。注意:run.sh不打算直接从shell中运行;其思想是您可以手工逐个运行其中的命令。

数据准备

我们首先需要配置作业是需要在本地运行还是在Oracle GridEngine上运行。关于如何做到这一点的说明以cmd.sh为单位。

如果没有安装Grid Engine,或者在较小的数据集上运行实验,请在shell上执行以下命令。

1
2
train_cmd="run.pl"
decode_cmd="run.pl"

如果安装了Grid Engine,则应该使用带有参数的queueue.pl文件来指定Grid Engine驻留的位置。在这种情况下,您将执行以下命令(参数-q是一个示例,您希望用网格引擎详细信息替换它)。

1
2
train_cmd="queue.pl -q all.q@a*.clsp.jhu.edu"
decode_cmd="queue.pl -q all.q@[ah]*.clsp.jhu.edu"

下一步是从RM语料库创建测试和训练集。要做到这一点,在shell上运行以下命令(假设您的数据在/export/corpora5/LDC/LDC93S3A/rm_comp中):

1
local/rm_data_prep.sh /export/corpora5/LDC/LDC93S3A/rm_comp

如果这样做,它会回应:“RM_data_prep成功了”。如果没有,您必须查明脚本失败的地方以及问题所在。

现在列出当前目录的内容,您应该可以看到创建了一个名为"data"的新目录。进入新创建的数据目录,列出内容。您应该看到三种主要类型的文件夹:

  • 本地:包含当前数据的字典。
  • 训练:从语料库中分割出来的数据用于训练目的。
  • test_*:从语料库中分割出来的数据用于测试目的。

让我们花一段时间看看创建的数据文件。这将使您很好地了解Kaldi期望输入数据是什么样的。(详情请参阅:详细资料准备指南)

本地目录:假设您在数据目录中,执行以下命令:

1
2
3
4
cd local/dict
head lexicon.txt
head nonsilence_phones.txt
head silence_phones.txt

这些将使您了解通用数据准备过程的输出是什么样子。您应该理解的是,并非所有这些文件都是“本机”Kaldi格式,即并不是所有的都可以由Kaldi的C程序读取,并且需要使用OpenFST工具进行处理,然后Kaldi才能使用它们。

  • lexicon.txt:这是词典。
  • silence.txt:这些文件包含关于哪些设备是沉默的,哪些不是沉默的信息。

现在回到数据目录,将目录更改为/train。然后执行以下命令查看此目录中文件的输出:

1
2
3
4
5
head text
head spk2gender
head spk2utt
head utt2spk
head wav.scp
  • text - 此文件包含话语和话语ID之间的映射,Kaldi将使用这些映射。这个文件将变成一个整数格式-仍然是一个文本文件,但用整数替换单词。
  • spk2gender - 此文件包含说话人与性别之间的映射。这也是参与训练的独特用户名单。
  • spk2utt - 这是说话者标识符和与说话者相关的所有话语标识符之间的映射。
  • utt2spk - 这是一个一对一的映射之间的话语ID和相应的扬声器标识符。
  • wav.scp - 这个文件实际上是由Kaldi程序在进行特征提取时直接读取的。再查看文件。它被解析为一组键值对,其中键是每一行的第一个字符串。值是一种“扩展文件名”,您可以猜测它是如何工作的。由于它是为了阅读,我们将把这种类型的字符串称为rxfilename(在编写时,我们使用wxfilename一词)。如果您好奇,请参阅扩展文件名:rxfilenames和wxfilenames。请注意,虽然我们使用扩展名scp,但这不是HTK意义上的脚本文件(即它不被视为命令行参数的扩展)。

训练文件夹与test_*文件夹结构一致。然而,训练数据的大小明显大于测试数据。您可以通过返回到数据目录并执行以下命令来验证这一点,该命令将为培训和测试集提供单词计数:

1
wc train/text test_feb89/text

下一步是创建Kaldi使用的原始语言文件。在大多数情况下,这些文件将是整数格式的文本文件。确保回到s5目录并执行以下命令:

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

这将在本地文件夹中创建一个名为lang的新文件夹,该文件夹将包含描述相关语言的FST。查看脚本,它将在data/创建的一些文件转换为由Kaldi读取的更规范化的形式。此脚本在data/lang/目录中创建其输出。我们下面提到的文件将在该目录中。

这个脚本创建的前两个文件名为words.txt和phone.txt(都在data/lang/目录中/)。这些是OpenFst格式的符号表,表示从字符串到整数和返回的映射。查看这些文件,因为它们很重要,而且会经常使用,所以你需要了解它们中的内容。它们的格式与我们以前在分发概述中遇到的符号表格式相同。

查看带有后缀.csl的文件(以数据/lang/phone表示)。这些是冒号分隔的整数id的非沉默和沉默电话。有时需要它们作为程序命令行的选项(例如指定沉默设备列表),并用于其他目的。

请看phones.txt(在data/lang/目录)。该文件是一个设备符号表,它还处理标准FST配方中使用的“消歧符号“。这些符号通常被称为#1、#2等等;参见论文“加权有限状态转换器的语音识别”。我们还添加了一个符号#0,它替换了语言模型中的epsilon转换;有关更多信息,请参见消歧符号。有多少消歧符号?在一些内容中,消歧符号的数量与具有相同发音的最大单词数相同。在我们的内容中还有一些,你可以在这里找到更多的解释。

文件L.fst是FST格式的编译词典。要看里面是什么样的信息,你可以(在s5/目录)输入:

1
fstprint --isymbols=data/lang/phones.txt --osymbols=data/lang/words.txt data/lang/L.fst | head

如果bash找不到命令fstprint,则需要将Open FST的安装路径添加到PATH环境变量中。只需运行脚本path.sh就可以做到这一点:

1
./path.sh

下一步是使用上一步创建的文件来创建描述语言语法的FST。要做到这一点,请回到目录s5并执行以下命令:

1
local/rm_prepare_grammer.sh

如果成功,这将返回消息“成功地为RM准备语法。” 将在/data/lang中创建一个名为G.fst的新文件。

特征提取

下一步是提取训练特征。在run.sh中搜索"mfcc"并运行相应的三行脚本(您必须决定要将功能放在第一位的位置并相应地修改示例)。确保您决定放置特性的那个目录有很大的空间。假设我们决定把这些特性放在/my/disk/rm_mfccdir上,我们会做一些类似的事情:

1
2
3
4
5
6
7
8
export featdir=/my/disk/rm_mfccdir
# make sure featdir exists and is somewhere you can write.
# can be local if you want.
mkdir $featdir
for x in test_mar87 test_oct87 test_feb89 test_oct89 test_feb91 test_sep92 train; do \
steps/make_mfcc.sh --nj 8 --cmd "run.pl" data/$x exp/make_mfcc/$x $featdir; \
steps/compute_cmvn_stats.sh data/$x exp/make_mfcc/$x $featdir; \
done

运行这些程序。他们使用几个CPU并行,应该在两分钟左右在一个快速的机器上完成。您可以根据机器的CPU数量更改-nj选项(该选项指定要运行的作业数)。查看文件exp/make_mfcc/train/make_mfcc.1.log,以查看创建MFCC的程序的日志输出。在它的顶部,您将看到命令行(Kaldi程序将始终与命令行相呼应,除非您指定-print-args=false)。

在脚本步骤/make_mfcc.sh中,查看调用split_scp.pl的行。你也许能猜到这是怎么回事。

通过输入:

1
2
wc $featdir/raw_mfcc_train.1.scp 
wc data/train/wav.scp

你可以确认。

接下来看看调用compute-mfcc-feats的行。这些选择应该相当不言自明。涉及配置文件的选项是一种机制,可以在Kaldi中使用它来传递配置选项,就像HTK配置文件一样,但它实际上很少被使用。立场论点以"scp"和"ark,scp"开头的论点需要更多的解释。

在解释这一点之前,请再次查看脚本中的命令行,并使用:检查输入和输出:

1
2
3
head data/train/wav.scp
head $featdir/raw_mfcc_train.1.scp
less $featdir/raw_mfcc_train.1.ark

小心-.ark文件包含二进制数据(如果您的终端在查看后不工作,您可能必须键入"reset")。

通过列出文件,您可以看到.ark文件相当大(因为它们包含实际数据)。您可以通过键入更方便地查看这些存档文件之一(假设您在s5目录中并运行了脚本path.sh):

1
copy-feats ark:$featdir/raw_mfcc_train.1.ark ark,t:- | head

您可以从这个命令中删除",t"修饰符,如果您愿意,可以再试一次——但是将它插入到"less"中可能是个好消息,因为数据将是二进制的。查看相同数据的另一种方法是:

1
copy-feats scp:$featdir/raw_mfcc_train.1.scp ark,t:- | head

这是因为这些归档文件和脚本文件都表示相同的数据(从技术上讲,归档文件只表示其中的八分之一,因为我们将其分成八个部分)。注意这些命令中的"scp:"和"ark:"前缀。kaldi并不试图从数据本身中找出某物是脚本文件还是存档格式,事实上,kaldi从未试图从文件后缀中找出问题。这是出于一般的哲学原因,也是为了防止与管道的不良相互作用(因为管道通常没有名称)。

现在输入以下命令:

1
head -10 $featdir/raw_mfcc_train.1.scp | tail -1 | copy-feats scp:- ark,t:- | head

这将从第十个训练文件中打印出一些数据。请注意,在"scp:-“中,”-"告诉它从标准输入读取,而"scp"则告诉它将输入解释为脚本文件。

接下来,我们将描述脚本和存档文件实际上是什么。我们要做的第一点是代码以同样的方式看到它们。对于用户级调用代码的一个特别简单的示例,请键入以下命令:

1
tail -30 ../../../src/featbin/copy-feats.cc

您可以看到,这个程序中实际工作的部分只是三行代码(实际上有两个分支,每个分支都有三行代码)。如果您熟悉Open Fst中的状态迭代器类型,您将注意到我们迭代的方式是相同的样式(我们已经尝试与Open Fst尽可能兼容样式)。

基础脚本和档案是表的概念。表基本上是一组有序的项目(例如特征文件),由唯一字符串索引(例如话语标识符)。表并不是真正的C对象,因为我们有单独的C对象来访问数据,这取决于我们是在编写、迭代还是随机访问。其中所述对象为浮点数矩阵(matrix<basic_float>)的这些类型的一个例子是:

1
2
3
BaseFloatMatrixWriter
RandomAccessBaseFloatMatrixReader
SequentialBaseFloatMatrixReader

这些类型都是typedef,它们实际上是模板化的类。我们不会再详细讨论了。脚本(.scp)文件或存档(.ark)文件都被视为数据表。格式如下:

  • .scp格式是一种只有文本的格式,它有带键的行,然后是一个“扩展文件名”,告诉Kaldi在哪里可以找到数据。
  • 存档格式可以是文本格式,也可以是二进制格式(您可以使用",t"修饰符以文本模式编写;二进制格式是默认的)。格式是:键(例如话语id),然后是一个空间,然后是对象数据。

关于脚本和档案的几个通用要点:

  • 指定如何读取表(存档或脚本)的字符串称为rspecifier;例如"ark:gunzip-c my/dir/foo.ark.gz|"。
  • 指定如何编写表(存档或脚本)的字符串称为wspecifier;例如"ark,t:foo.ark"。
  • 档案可以连在一起,仍然是有效的档案(其中没有“中央索引“)。
  • 代码可以按顺序或通过随机访问读取脚本和档案。用户级代码只知道它是迭代还是查找;它不知道它是访问脚本还是存档。
  • Kaldi不试图在存档中表示对象类型;您必须事先知道对象类型
  • 档案和脚本文件不能包含类型的混合。
  • 通过随机访问读取档案可能内存不足,因为代码可能必须缓存内存中的对象。
  • 为了有效地随机访问存档,您可以使用"ark,scp"写入机制(例如,在将mfcc特性写入磁盘时使用)编写相应的脚本文件。然后通过scp文件访问它。
  • 在对档案进行随机访问时,避免代码在内存中缓存一堆东西的另一种方法是告诉代码存档被排序并将按排序顺序调用(例如"ark,cs:-")。
  • 读和写档案的类型是在Holder类型上模板化的,这是一种“知道如何”读和写有关对象的类型。

在这里,我们刚刚给出了一个非常快速的概述,它可能会提出更多的问题,而不是它提供的答案;它只是为了让您了解所涉及的各种问题。有关详细信息,请参阅Kaldi I/O机制

要让您了解如何在管道中使用档案和脚本文件,请键入以下命令,并尝试了解正在发生的事情:

1
head -1 $featdir/raw_mfcc_train.1.scp | copy-feats scp:- ark:- | copy-feats ark:- ark,t:- | head

它可能有助于按顺序运行这些命令并观察发生了什么。使用copy-feats,请记住将管连接输出和头,因为您可能列出了许多内容(在ark文件的情况下可能是二进制的)。

最后,为了方便起见,让我们将所有测试数据合并到一个目录中。我们将在这个平均步骤上做所有的测试。下面的命令还将合并扬声器,注意这些扬声器的重复和重新生成统计数据,这样我们的工具就不会抱怨了。通过运行以下命令(从S5目录)来实现这一点。

1
2
utils/combine_data.sh data/test data/test_{mar87,oct87,feb89,oct89,feb91,sep92}
steps/compute_cmvn_stats.sh data/test exp/make_mfcc/test $featdir

让我们也创建一个训练数据子集(train.1k)。每位演讲者只保留1000个话语。我们将用于训练。通过执行以下命令来做到这一点:

1
utils/subset_data_dir.sh data/train 1000 data/train.1k 

单音节训练

下一步是训练单音模型。如果安装Kaldi的磁盘不大,您可能希望将exp/一个软链接到一个大磁盘上的某个目录(如果运行所有的实验并且不清理,它可以达到几G)。输入

1
nohup steps/train_mono.sh --nj 4 --cmd "$train_cmd" data/train.1k data/lang exp/mono &

您可以查看此内容的最新输出,输入:

1
tail nohup.out

您可以这样运行更长的作业,这样即使我们断开连接,它们也可以完成运行,尽管一个更好的想法是从"Screen"运行外壳,这样它就不会被杀死。实际上,只有很少的输出达到这个脚本的标准输出和错误;其中大部分输出用于记录exp/mono/中的文件。

运行时,请查看文件data/lang/topo。此文件立即创建。其中一部手机具有与其他手机不同的拓扑结构。查看data/phone.txt,以便从数字id中找出它是哪个电话。注意,拓扑文件中的每个条目都有一个最终状态,没有从中转换。拓扑文件中的约定是第一状态是初始状态(具有概率一),最后状态是最终状态(具有概率一)。

输入:

1
gmm-copy --binary=false exp/mono/0.mdl - | less

查看模型文件。您将看到它在拓扑文件的顶部包含信息,然后在模型参数之前包含一些其他内容。惯例是.mdl文件包含两个对象:类型TransitionModel的一个对象,该对象作为类型HMM拓扑的成员变量包含拓扑信息,以及相关模型类型的一个对象(在本例中是类型AmGmm)。通过“包含两个对象”,我们的意思是对象具有标准形式的写和读函数,我们调用这些函数将对象写入文件。对于这样的对象,它们不是表的一部分(即没有"ark:"或"SCP:“涉及),写入是二进制或文本模式,可以由标准命令行选项-binary=true或-binary=false控制(不同的程序有不同的默认值)。表(即档案和脚本)、二进制或文本模型由说明符中的”,t"选项控制。

浏览模型文件,看看它包含了什么样的信息。在这一点上,我们不会更详细地讨论如何在Kaldi中表示模型;请参阅HMM拓扑和转换建模以了解更多信息。

不过,我们将提到一个重要的问题:p.d.f。在Kaldi中,由数字id表示,从零开始(我们称之为pdf-ids)。他们没有“名字”,如HTK.mdl文件没有足够的信息在上下文相关的手机和pdf-id之间映射。有关这些信息,请参见树文件,输入:

1
copy-tree --binary=false exp/mono/tree - | less

请注意,这是一个单字“树”,所以它是非常琐碎的-它没有任何“分裂”。虽然这种树格式并不是很容易读的,但我们已经收到了一些关于树格式的查询,所以我们将解释它。这一段的其余部分可以被随意的读者跳过。在"ToPdf"之后,树文件包含一个多态类型Event Map的对象,该对象可以被认为是将表示上下文中电话和HMM状态的整型(键、值)对的映射存储到数字p.d.f id。从事件映射派生的类型是常量事件映射(表示树的叶子)、表事件映射(表示某种查找表)和拆分事件映射(表示树拆分)。在这个文件exp/mono/tree中,“CE”是常量事件映射的标记(并且对应于树的叶子),“TE”是表事件映射的标记(没有“SE”,也没有拆分事件映射,因为这是单音情况)。“TE049”是一个表事件映射的开始,该表事件映射在键0上“分割”(表示长度为1的电话上下文向量中的zeroth电话位置,单音格)。在括号中,后面是49个类型的Event Map对象。第一个是NULL,表示指向Event Map的零指针,因为phone-id零是为“epsilon”保留的。一个非空对象的例子是字符串"TE-13(CE33CE34CE35)",它表示键-1上的表事件映射分裂。此键表示拓扑文件中指定的pdf类,在我们的示例中,它与HMM状态索引相同。此电话有3个HMM状态,因此分配给此键的值可以取0、1或2。括号内有三个常量事件映射类型的对象,每个对象表示树的叶子。

现在看看文件exp/mono/ali.1.gz(如果训练进展足够大,它应该存在):

1
copy-int-vector "ark:gunzip -c exp/mono/ali.1.gz|" ark,t:- | head -n 2

这是训练数据的Viterbi对齐;每个训练文件都有一行。现在再看一遍exp/mono/tree(如上所述),并寻找数量最高的p.d.f.id(这是文件中的最后一个数字)。将其与exp/mono/ali.1.gz中的数字进行比较。有什么不对劲吗?对齐方式中的数字太大。原因是对齐文件不包含p.d.f.id。它包含一个稍微细粒度的标识符,我们称之为“transition-id”。这也编码了电话和电话原型拓扑中的转换。这有几个原因。如果你想解释一个特定的过渡ID是什么(例如您正在查看cur.ali中的对齐,您看到一个重复了很多,您想知道为什么),您可以使用程序“显示-转换”向您展示一些关于转换-ID的信息。输入:

1
show-transitions data/lang/phones.txt exp/mono/0.mdl

有关HMM拓扑、过渡id、过渡建模等方面的详细信息,请参见HMM拓扑和过渡建模

接下来,让我们看看训练是如何进行的(这个步骤假设您的shell是bash)。输入:

1
grep Overall exp/mono/log/acc.{?,??}.{?,??}.log

您可以在每次迭代中看到声学的可能性。接下来看看其中一个文件exp/mono/log/update.*.log查看更新日志中的信息类型。

当单音训练完成后,我们可以测试单音解码。在解码之前,我们必须创建解码图。输入:

1
utils/mkgraph.sh --mono data/lang exp/mono exp/mono/graph

看看utils/mkgraph.sh调用的程序。其中许多人的名字以“fst”开头(例如fsttablecompose)这些程序中的大多数实际上不是来自OpenFST发行版。我们创建了一些自己的FST操作程序。您可以找到这些程序的位置如下。取在utils/mkgraph.sh中调用的任意程序(例如,fstdeterminizestar)。 然后输入:

1
which fstdeterminizestar

我们有不同版本的程序的原因主要是因为我们在语音识别中使用FSTs的方式略有不同(较少AT&T-ish)。例如,“fstdeterminizestar”对应于去除epsilon弧的“经典”确定。有关更多信息,请参见Kaldi中的解码图构造。在图形创建过程之后,开始进行单音解码,我们可以用:

1
2
steps/decode.sh --config conf/decode.config --nj 20 --cmd "$decode_cmd" \
exp/mono/graph data/test exp/mono/decode

查看解码输出:

1
less exp/mono/decode/log/decode.2.log 

你可以看到它把结果放在屏幕上。结果的文本形式只出现在日志信息中:此程序的实际输出显示在文件exp/mono/decode/scoring/2.tra中。这些tra文件中的数字表示使用解码过程的语言模型(LM)标度。在这里,我们默认使用LM标度等于从2到13(详见local/score.sh)。若要从tra文件中查看实际解码的单词序列(以2.tra为例),请键入:

1
utils/int2sym.pl -f 2- data/lang/words.txt exp/mono/decode/scoring/2.tra

有一个对应的脚本叫做sym2int.pl。将其转换回整数形式,您可以通过键入:

1
2
utils/int2sym.pl -f 2- data/lang/words.txt exp/mono/decode/scoring/2.tra | \
utils/sym2int.pl -f 2- data/lang/words.txt

-f 2-选项是这样的,它不会尝试将语句id转换为整数。接下来,试着做:

1
tail exp/mono/decode/log/decode.2.log

它将在最后打印出一些有用的摘要信息,包括实时因子和每个帧的平均对数似然。实时因子通常约为0.2至0.3(即比实时快)。这取决于您的CPU、机器上有多少作业和其他因素。此脚本并行运行20个作业,因此如果您的机器有少于20个内核,那么可能会慢得多。请注意,我们使用相当宽的光束(20)来获得准确的结果;在典型的LVCSR设置中,光束将小得多(例如大约13)。

再次查看日志文件的顶部,并关注命令行。可选参数位于位置参数之前(这是强制性的)。输入:

1
gmm-decode-faster

来查看使用消息,并将参数与日志文件中看到的参数匹配。回想一下,"rspecifier"是指定如何读取表的字符串之一,而"wspecifier"则指定如何写入表。仔细看这些论点,试着弄清楚它们的意思。看看对应于特性的rspecifier,试着理解它(这个里面有空格,所以Kaldi在它周围用单引号打印出来,这样你就可以把它粘贴到shell中,程序就会按预期运行)。

单音系统现在已经完成,我们将在下一步的教程中进行三音训练和解码。

理解和修改源码(30分钟)

当triphone(音素)系统构建正在运行时,我们将花一点时间来查看代码的某些部分。您将从本教程的这一部分中了解到的主要内容是如何组织代码以及依赖结构是什么;以及修改和调试代码的一些经验。如果您想更深入地理解它的代码,我们建议您遵循主文档页上的链接,在那里我们有按主题组织的更详细的文档。

通用工具

转到顶层目录(我们称之为kaldi-1),然后进入src/。首先查看文件base/kaldi-common.h(不要跟踪此文档中的链接;从shell或编辑器中查看它)。这个#includes来自base/目录的许多东西,几乎每个Kaldi程序都使用这些东西。您主要可以从文件名中猜测所提供的事物类型:诸如错误记录宏、typedefs、数学实用程序函数(如随机数生成)和杂项#define。但是这是一个精简的实用程序集合;在util/common-utils.h中有一个更完整的集合,包括命令行解析和I/O函数,它们处理诸如管道之类的扩展文件名。花几秒钟时间浏览util/common-utils.h,看看它包含了什么。我们将一个实用程序子集分离到基/目录中的原因是为了最小化矩阵/目录的依赖(这本身是有用的);matrix/目录只依赖于base/目录。查看matrix/Makefile并搜索base/以查看这是如何指定的。查看Makefiles中的这类规则可以让您深入了解工具包的结构。

矩阵库(改进和调试模式)

现在看看文件matrix/matrix-lib.h。看看它包括哪些文件。这提供了矩阵库中各种事物的概述。这个库基本上是BLAS和LAPACK的C包装器,如果这对您有任何意义(如果没有,不要担心)。文件sp-matrix.htp-matrix.h分别涉及对称填充矩阵和三角形填充矩阵。快速扫描文件matrix/kaldi-matrix.h。这将使您了解矩阵代码是什么样子的。它由表示矩阵的C类组成。如果您有兴趣,我们在这里提供一个关于矩阵库的迷你教程。您可能会注意到代码中似乎是一种奇怪的注释样式,注释由三个斜杠(///)开始。这些类型的注释,并阻止注释以:

1
/**/

开头,由自动生成文档的Doxygen软件解释。它还生成您正在阅读的页面(这类文档的来源是src/doc/)。

此时,我们希望您修改代码并编译它。我们将向文件matrix/matrix-lib-test.cc添加一个测试函数。正如前面提到的,测试程序的设计是为了在出现错误时中止或退出非零状态。

我们将为函数添加一个测试例程:Vector::AddVec。这个函数将一个向量的一些常数乘以另一个向量。通读下面的代码,尽可能多地理解它(注意:我们故意在代码中插入了两个错误)。如果你不熟悉模板,理解它可能很困难。我们已经尽量避免使用模板,所以Kaldi的大部分不知道模板编程仍然可以理解,。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<class Real>
void UnitTestAddVec() {
// note: Real will be float or double when instantiated.
int32 dim = 1 + Rand() % 10;
Vector<Real> v(dim); w(dim); // two vectors the same size.
v.SetRandn();
w.SetRandn();
Vector<Real> w2(w); // w2 is a copy of w.
Real f = RandGauss();
w.AddVec(f, v); // w <-- w + f v
for (int32 i = 0; i < dim; i++) {
Real a = w(i), b = f * w2(i) + v(i);
AssertEqual(a, b); // will crash if not equal to within
// a tolerance.
}
}

将此代码添加到文件<a href=“http://www.kaldi-asr.org/doc/matrix-lib-test_8cc.html” rel=“nofollow"target=”_blank">matrix-lib-test.cc中,就在函数MatrixUnitTest()之上。然后,在MatrixUnitTest()中,添加一行:

1
UnitTestAddVec<Real>();

在函数中的什么位置添加这个并不重要。然后键入make test。应该有一个错误(分号应该是逗号);修复它,然后再试一次。现在键入./matrix-lib-test。这应该与断言失败一起崩溃,因为单元测试代码中还有一个错误。接下来我们将调试它。输入:

1
gdb ./matrix-lib-test

(如果您在cygwin上,现在应该键入gdb提示符"break__assert_func")。键入"r"。当它崩溃时,它调用中止,这会被调试器捕获。键入"bt"以查看堆栈跟踪。在测试函数内输入"up",使堆栈向上扩展。当你在正确的地方,你应该看到输出如下:

1
2
#5  0x080943cf in kaldi::UnitTestAddVec<float> () at matrix-lib-test.cc:2568
2568 AssertEqual(a, b); // will crash if not equal to within

如果你走得太远,你可以键入"down"。然后键入"p a"和"p b"以查看a和b的值("p"是"print"的缩写)。你的屏幕应该是这样的:

1
2
3
4
5
(gdb) p a
$5 = -0.931363404
(gdb) p b
$6 = -0.270584524
(gdb)

确切的值当然是随机的,对你来说可能是不同的。由于数字有很大的不同,很明显,这不仅仅是一个公差是错误的问题。一般来说,您可以使用"print"表达式从调试器访问任何类型的表达式,但是括号运算符(如"v(i)"之类的表达式)不起作用,因此要查看向量中的值,必须输入如下表达式:

1
2
3
4
5
6
7
(gdb) p v.data_[0]
$8 = 0.281656802
(gdb) p w.data_[0]
$9 = -0.931363404
(gdb) p w2.data_[0]
$10 = -1.07592916
(gdb)

这可能会帮助您计算出"b"的表达式是错误的。在代码中修复它,重新编译并再次运行(您可以在gdb提示符中键入"r"以重新运行)。它现在应该运行OK。强制gdb在它以前失败的时候闯入代码,这样您就可以再次检查表达式的值,并看到事情现在工作正常。要让调试器在那里中断,您必须设置断点。计算出断言失败的行号(在UnitTestAddVec()中的某个地方),并键入gdb,如下所示:

1
2
(gdb) b matrix-lib-test.cc:2568
Breakpoint 1 at 0x80943b4: file matrix-lib-test.cc, line 2568. (4 locations)

然后运行程序(键入"r"),当程序中断时,使用"p"命令查看表达式的值。若要继续,请键入"c"。它会一直停在那里,因为它在一个循环中。键入"d1"以删除断点(假设是断点第一),键入"c"以继续。程序应运行到底。键入"q"退出调试器。如果需要调试接受命令行参数的程序,可以这样做:

1
2
3
gdb --args kaldi-program arg1 arg2 ...
(gdb) r
...

或者您可以在没有参数的情况下调用gdb,然后在提示符下键入r arg1 arg2

当您完成并编译时,输入:

1
git diff

看看你做了什么改变。如果您正在为Kaldi项目做出贡献,并计划在不久的将来向我们发送代码,您可能希望将它们提交到前面所描述的分支,以便您以后可以生成一个干净的GitHub pull request。我们建议您熟悉Git分支,即使您没有直接贡献您的更改;Git是维护本地代码更改以及您可能贡献的更改的强大工具。

声学建模代码

接下来看看gmm/diag-gmm.h(该类存储高斯混合模型)。类Diag Gmm可能看起来有点混乱,因为它有许多不同的访问函数。 搜索"private"并查看类成员变量(根据Kaldi样式,它们总是以下划线结尾)。这应该清楚我们如何存储GMM。这只是一个GMM,而不是一个完整的GMM集合。看看gmm/am-diag-gmm.h;这个类存储了GMM的集合。请注意,它不继承任何东西。搜索"private"可以看到成员变量(其中只有两个)。您可以从这里理解类是多么简单(其他所有内容都由各种访问器和方便函数组成)。一个自然的问题是:转换在哪里,决策树在哪里,HMM拓扑在哪里?所有这些都是与声学模型分离的,因为研究人员可能希望在保持系统其余部分相同的情况下替换声学可能性。我们稍后再谈其他的事情。

特征提取代码

接下来看feat/feature-mfcc.h。关注Mfcc选项结构。结构成员让您了解在MFCC特征提取中支持什么样的选项。注意,一些结构成员是选项结构本身。看看寄存器函数。这是Kaldi选项类中的标准。然后查看fetebin/compute-mfcc-feats.cc(这是一个命令行程序)并搜索Register。您可以看到选项结构的Register函数在哪里调用。要查看MFCC特征提取支持的选项的完整列表,请执行没有参数的程序fatebin/compute-mfcc-feats。回想一下,您看到其中一些选项在Mfcc Options类中注册,而另一些选项则在fetebin/compute-mfcc-feats.cc中注册。指定选项的方法是-option=value。输入:

1
featbin/compute-mfcc-feats ark:/dev/null ark:/dev/null

这应该运行成功,因为它将/dev/null解释为一个空存档。您可以尝试使用此示例设置选项。例如,试试,

1
featbin/compute-mfcc-feats --raw-energy=false ark:/dev/null ark:/dev/null

您从中得到的唯一有用的信息是它不会崩溃;尝试删除"="符号或缩写选项名称或更改参数数量,并看到它失败并打印一条使用消息。

声学决策树和HMM拓扑代码

接下来看看tree/build-tree.h。找到构建树函数。这是构建决策树的主要顶层功能。请注意,它返回的指针是类型Event Map。这是一种将函数从一组(键、值)对存储到整数的类型。它在tree/event-map.h中定义。键和值都是整数,但键表示语音上下文位置(通常为0、1或2),值表示电话。还有一个特殊的键-1,它大致表示HMM中的位置。转到实验目录(…/egs/rm/s5),我们将查看树是如何构建的。对BuildTree函数的主要输入是类型BuildTreeStatsType,这是一个typedef,如下所示:

1
typedef vector<pair<EventType, Clusterable*> > BuildTreeStatsType;

这里,EventType是以下类型:

1
typedef vector<pair<EventKeyType, EventValueType> > EventType;

事件类型表示一组(键、值)对。例如,一个典型的是{ {-1,1},{0,15},{1,21},{2,38} },它代表声素21,左上下文为电话15,右上下文为电话38,p.d.f类1(在正常情况下,这意味着它处于状态号1,这是三个状态的中间)。Clusterable*指针是指向一个虚拟类的指针,它具有一个通用接口,支持将统计数据相加和评估某种目标函数(例如一种可能性)。在普通内容中,它实际上指向一个类,该类包含用于估计对角高斯p.d.f的足够统计数据。输入

1
less exp/tri1/log/acc_tree.log

这个文件中不会有太多信息,但是可以看到命令行。该程序为每个可见的三音机上下文的每个HMM状态(实际上是pdf类)积累单高斯统计数据。ci-phones选项是这样的,它知道避免为不同的手机上下文积累单独的统计数据,比如沉默,我们不想依赖上下文(这是一个优化;如果没有这个选项,它将工作)。这个程序的输出可以被认为是上面讨论的类型BuildTreeStatsType,尽管为了读取它,我们必须知道它是什么具体类型。输入:

1
less exp/tri1/log/train_tree.log

该程序执行决策树聚类;它读取由输出的统计数据。它基本上是上面讨论的Build Tree函数的包装器。它在决策树聚类中提出的问题是自动生成的,如您在脚本步骤/train_tri1.sh(查找程序集群电话和编译问题中所看到的)。

接下来看看hmm/hmm-topology.h。类HMM拓扑为许多手机定义了一组HMM拓扑。一般来说,每个声素都可以有不同的拓扑结构。拓扑包括用于初始化的"默认"转换。看看头顶部的扩展注释中的示例拓扑。有一个<pdf class>的标记(注意:与HTK文本格式一样,这个文件看起来有点像XML,但它并不是真正的XML)。<pdf class>总是与这里的HMM-状态(<state>)相同;一般来说,它不一定是。这是一种在不同的HMM状态之间强制绑定分布的机制;如果您想要5~创建更有趣的转换模型,这可能是有用的。


Kaldi Tutorial翻译
https://blog.jackeylea.com/kaldi/translation-of-kaldi-tutorial/
作者
JackeyLea
发布于
2021年1月11日
许可协议