kaldi 训练 aishell 解析
April 14, 2018, 10:50 a.m.
read: 3286
此文章首发于hupeng.me
前言
以下段落来自知乎,作者-喜欢吃,著作权归作者所有
在做aishell的v1,总而言之,你现在有一段语音比如 hellowrold.wav,你需要写个脚本生成一个data 目录里面包含着utt2spk,spk2utt,wav.scp,wav.list文件,至于怎么生成的可以看local/aishell data prep.sh,用python os和sys库可能会更快更简单写出来。然后你有了训练好的模型,你就先把你的语音mfcc,vad之后呢,对于speaker 然后把你的语音就直接丢进去一些decode.sh decoding一下得分就好,你可以参考timit,wsj的ASR
根据README文档显示
This folder contains two subfolders:
s5: a speech recognition recipe
v1: a speaker recognition recipe
s5目录做的 语音识别
v1目录做的 说话人识别
此处我们所做的是语音识别
前期准备
1.linux(最好是 Ubuntu系列) + 足够大大大大的硬盘(100G以上的空闲的大小)
2.git 克隆 kaldi-asr 仓库
3.编译之
4.下载aishell训练集(不下也行),因为在run.sh里面存在下载逻辑,需要修改data字段,如下:
data=/mnt/raid/data/ELEVOC_DATA/asr/
data_url=www.openslr.org/resources/33
下载完毕之后需要data字段指定一下下回来的路径。
5.修改 cmd.sh,有这样注释
# you can change cmd.sh depending on what type of queue you are using.
# If you have no queueing system and want to run on a local machine, you
# can change all instances 'queue.pl' to run.pl (but be careful and run
# commands one by one: most recipes will exhaust the memory on your
# machine). queue.pl works with GridEngine (qsub). slurm.pl works
# with slurm. Different queues are configured differently, with different
# queue names and different ways of specifying things like memory;
# to account for these differences you can create and edit the file
# conf/queue.conf to match your queue's configuration. Search for
# conf/queue.conf in http://kaldi-asr.org/doc/queue.html for more information,
# or search for the string 'default_config' in utils/queue.pl or utils/slurm.pl.
谷歌翻译之
您可以根据您使用的队列类型更改cmd.sh。
如果你没有排队系统并想在本地机器上运行,你可以将所有实例的queue.pl改为run.pl(但要小心并逐个运行命令:大多数配方会耗尽你机器上的内存)。
queue.pl与GridEngine(qsub)一起使用。
slurm.pl使用slurm。
不同的队列配置不同,具有不同的队列名称和不同的指定内存等方式;
为了解决这些差异,您可以创建和编辑文件conf / queue.conf以匹配队列的配置。
有关更多信息,请在http://kaldi-asr.org/doc/queue.html中搜索conf / queue.conf,或在utils / queue.pl或utils / slurm.pl中搜索字符串'default_config'。
由于我们在单机运行,配置需要做如下修改
原来的配置:
export train_cmd="queue.pl --mem 2G"
export decode_cmd="queue.pl --mem 4G"
export mkgraph_cmd="queue.pl --mem 8G"
修改为:
export train_cmd="run.pl"
export decode_cmd=run.pl
export mkgraph_cmd="run.pl"
run.sh解析
行号:15,16
data=/mnt/raid/data/ELEVOC_DATA/asr/
data_url=www.openslr.org/resources/33
配置下载目录以及,下载地址。(其实用迅雷更快)
行号:20,21
local/download_and_untar.sh $data $data_url data_aishell || exit 1;
local/download_and_untar.sh $data $data_url resource_aishell || exit 1;
下载两个文件
local/download_and_untar.sh
主要的业务逻辑如下:
传入三个参数
$data : /mnt/raid/data/ELEVOC_DATA/asr/
$data_url : www.openslr.org/resources/33
part : data_aishell / resource_aishell
通过 判断 .complate隐藏文件以及两个隐藏文件来判断下回来的两个tgz文件的大小来判断下载的文件是不是完整的。
在判断wget这个程序是不是存在。一般的linux完整的发行版自带这个程序。
解压连个tgz
if ! tar -xvzf $part.tgz; then
echo "$0: error un-tarring archive $data/$part.tgz"
exit 1;
fi
解压里面的tar.gz文件
if [ $part == "data_aishell" ]; then
cd $data/$part/wav
for wav in ./*.tar.gz; do
echo "Extracting wav from $wav"
tar -zxf $wav && rm $wav
done
fi
至此
local/download_and_untar.sh
完事了
下面继续。。。。run.sh
行号:24
local/aishell_prepare_dict.sh $data/resource_aishell || exit 1;
处理 $data/resource_aishell
里面包含两个文件(还有一个隐藏文件来确认完整性)
0 Apr 12 14:54 .complete
3548019 Jul 5 2017 lexicon.txt
2800 Jun 21 2017 speaker.info
lexicon.txt 文件的部分内容如下:
可以看到 这些包含着读音之类的
振飞 zh en4 f ei1
振刚 zh en4 g ang1
振国 zh en4 g uo2
振华 zh en4 h ua2
振辉 zh en4 h ui1
振杰 zh en4 j ie2
振林 zh en4 l in2
振南 zh en4 n an2
振宁 zh en4 n ing2
振鹏 zh en4 p eng2
振强 zh en4 q iang2
振庆 zh en4 q ing4
振山 zh en4 sh an1
振先 zh en4 x ian1
振鑫 zh en4 x in1
振雄 zh en4 x iong2
振英 zh en4 ii ing1
振宇 zh en4 vv v3
振源 zh en4 vv van2
振洲 zh en4 zh ou1
另一个文件 speaker.info 的部分内容如下:
说话人与性别的映射
0002 M
0003 M
0004 M
0005 M
0006 M
0007 M
0008 M
0009 M
0010 M
0011 M
0012 M
0013 M
0014 M
分析如下文件:
local/aishell_prepare_dict.sh
调用path.sh 设置环境变量,设置如下的环境变量:
export PATH=$PWD/utils/:$KALDI_ROOT/tools/openfst/bin:$PWD:$PATH
export PATH=\
${KALDI_ROOT}/src/bin:\
${KALDI_ROOT}/src/chainbin:\
${KALDI_ROOT}/src/featbin:\
${KALDI_ROOT}/src/fgmmbin:\
${KALDI_ROOT}/src/fstbin:\
${KALDI_ROOT}/src/gmmbin:\
${KALDI_ROOT}/src/ivectorbin:\
${KALDI_ROOT}/src/kwsbin:\
${KALDI_ROOT}/src/latbin:\
${KALDI_ROOT}/src/lmbin:\
${KALDI_ROOT}/src/nnet2bin:\
${KALDI_ROOT}/src/nnet3bin:\
${KALDI_ROOT}/src/nnetbin:\
${KALDI_ROOT}/src/online2bin:\
${KALDI_ROOT}/src/onlinebin:\
${KALDI_ROOT}/src/rnnlmbin:\
${KALDI_ROOT}/src/sgmm2bin:\
${KALDI_ROOT}/src/sgmmbin:\
${KALDI_ROOT}/src/tfrnnlmbin:\
$PATH
接下来处理生成5份文件:
883 Apr 13 10:40 extra_questions.txt:构建决策树的问题集(可以为空)
4107515 Apr 12 14:54 lexiconp.txt:字典文件(词语 音素1 音素2 ...)
3548019 Apr 13 10:40 lexicon.txt 包含概率的字典文件(词语 概率 音素1 音素2 ...)
872 Apr 13 10:40 nonsilence_phones.txt:静音/非静音 音素,每行代表相同的base phone, 但是会用不同的音调,例如 a a1 a2 a3 a4
4 Apr 13 10:40 optional_silence.txt 默认静音音素
4 Apr 13 10:40 silence_phones.txt 默认静音音素
文件1.lexicon.txt:字典文件(词语 音素1 音素2 …)
直接拷贝文件:
cp $res_dir/lexicon.txt $dict_dir
文件2.nonsilence_phones.txt:静音/非静音 音素,每行代表相同的base phone, 但是会用不同的音调,例如 a a1 a2 a3 a4
部分文件
a1
a2
a3
a4
a5
aa
ai1
ai2
ai3
ai4
ai5
an1
an2
an3
an4
an5
ang1
ang2
ang3
ang4
ang5
ao1
ao2
ao3
把音素文件进行排序去重之后输出
文件3.silence_phones.txt:默认静音音素 sil
echo sil > $dict_dir/silence_phones.txt
显而易见文件内容
文件4.optional_silence.txt:默认静音音素 sil
echo sil > $dict_dir/optional_silence.txt
显而易见文件内容
文件5.extra_questions.txt:构建决策树的问题集(可以为空)
sil
a4 ai4 an4 ang4 ao4 e4 ei4 en4 eng4 er4 i4 ia4 ian4 iang4 iao4 ie4 in4 ing4 iong4 iu4 ix4 iy4 iz4 o4 ong4 ou4 u4 ua4 uai4 uan4 uang4 ueng4 ui4 un4 uo4 v4 van4 ve4 vn4
a3 ai3 an3 ang3 ao3 e3 ei3 en3 eng3 er3 i3 ia3 ian3 iang3 iao3 ie3 in3 ing3 iong3 iu3 ix3 iy3 o3 ong3 ou3 u3 ua3 uai3 uan3 uang3 ueng3 ui3 un3 uo3 v3 van3 ve3 vn3
a1 ai1 an1 ang1 ao1 e1 ei1 en1 eng1 i1 ia1 ian1 iang1 iao1 ie1 in1 ing1 iong1 iu1 ix1 iy1 o1 ong1 ou1 u1 ua1 uai1 uan1 uang1 ueng1 ui1 un1 uo1 v1 van1 ve1 vn1
aa b c ch d ee f g h ii j k l m n oo p q r s sh t uu vv x z zh
a2 ai2 an2 ang2 ao2 e2 ei2 en2 eng2 er2 i2 ia2 ian2 iang2 iao2 ie2 in2 ing2 iong2 iu2 ix2 iy2 o2 ong2 ou2 u2 ua2 uai2 uan2 uang2 ui2 un2 uo2 v2 van2 ve2 vn2
a5 ai5 an5 ang5 ao5 e5 ei5 en5 eng5 er5 i5 ia5 ian5 iang5 iao5 ie5 in5 ing5 iong5 iu5 ix5 iy5 o5 ong5 ou5 u5 ua5 uai5 uan5 uang5 ueng5 ui5 un5 uo5 v5 van5 ve5 vn5
目前无从得知这些文件的用处。
至此
local/aishell_prepare_dict.sh
分析结束!
行号:27
local/aishell_data_prep.sh $data/data_aishell/wav $data/data_aishell/transcript || exit 1;
进入数据处理部分,这个部分是数据处理部分的核心部分
输入两个参数:
$data/data_aishell/wav:
$data/data_aishell/transcript:
data_aishell里面包含两字子目录,结构如下:
----transcript
'
----aishell_transcript_v0.8.txt
'
----wav
'
----dev
----test
----train
'
local/aishell_data_prep.sh
生成以下的目录:
----train
----test
----dev
以train目录为例
----train
----spk2utt(每个说话+其语音文件 \n)
----text(每个语音文件对应的汉字)
----transcripts.txt(每个语音文件对应的汉字)
----utt2spk(每个语音文件(唯一)对应的说话人,语音文件顺序排列)
----utt2spk_all(应该用不上)
----utt.list(所有的语音文件)
----wav.flist(wav文件的路径)
----wav.scp (文件名->绝对路径的映射)
----wav.scp_all(应该也用不上)
local/aishell_train_lms.sh
创建一个新的目录 lm在local目录下,local目录原来就有三个子目录,现在多一个
----train
----test
----dev
----lm (新)
----text.no_oov
----train.gz
----unigram.counts
----word.counts
----word_map
----wordlist.mapped
----3gram-mincount
----....其余文件
行号:30-34
cleantext=$dir/text.no_oov
cat $text | awk -v lex=$lexicon 'BEGIN{while((getline<lex) >0){ seen[$1]=1; } }
{for(n=1; n<=NF;n++) { if (seen[$n]) { printf("%s ", $n); } else {printf("<SPOKEN_NOISE> ");} } printf("\n");}' \
> $cleantext || exit 1;
读取
train目录下的text文件,判断长度写入新文件。
此处的逻辑就是把原来的语音文件编号变成了
<SPOKEN_NOISE>
下面给出 train/text 与 lm/text.no_oov 对比信息
train/text 1-8行
BAC009S0002W0122 而 对 楼市 成交 抑制 作用 最 大 的 限 购
BAC009S0002W0123 也 成为 地方 政府 的 眼中 钉
BAC009S0002W0124 自 六月 底 呼和浩特 市 率先 宣布 取消 限 购 后
BAC009S0002W0125 各地 政府 便 纷纷 跟进
BAC009S0002W0126 仅 一 个 多 月 的 时间 里
BAC009S0002W0127 除了 北京 上海 广州 深圳 四 个 一 线 城市 和 三亚 之外
BAC009S0002W0128 四十六 个 限 购 城市 当中
BAC009S0002W0129 四十一 个 已 正式 取消 或 变相 放松 了 限 购
lm/text.no_oov 1-8行
<SPOKEN_NOISE> 而 对 楼市 成交 抑制 作用 最 大 的 限 购
<SPOKEN_NOISE> 也 成为 地方 政府 的 眼中 钉
<SPOKEN_NOISE> 自 六月 底 呼和浩特 市 率先 宣布 取消 限 购 后
<SPOKEN_NOISE> 各地 政府 便 纷纷 跟进
<SPOKEN_NOISE> 仅 一 个 多 月 的 时间 里
<SPOKEN_NOISE> 除了 北京 上海 广州 深圳 四 个 一 线 城市 和 三亚 之外
<SPOKEN_NOISE> 四十六 个 限 购 城市 当中
<SPOKEN_NOISE> 四十一 个 已 正式 取消 或 变相 放松 了 限 购
行号:36-37
cat $cleantext | awk '{for(n=2;n<=NF;n++) print $n; }' | sort | uniq -c | \
sort -nr > $dir/word.counts || exit 1;
看输出的文件名就知道这个文件的功能是统计单词的数量,在Hadoop里面有一个经典的样例做的就是这个任务。。。
下面给出word.counts的部分内容(可以看出来已经按照 value 排过序了)
48876 的
11114 在
10171 一
9609 了
9421 不
8400 是
6729 有
5362 个
5219 和
4606 将
4220 为
4199 上
4118 中国
3976 也
3769 之
行号:42-44
cat $cleantext | awk '{for(n=2;n<=NF;n++) print $n; }' | \
cat - <(grep -w -v '!SIL' $lexicon | awk '{print $1}') | \
sort | uniq -c | sort -nr > $dir/unigram.counts || exit 1;
写入文件这个到 unigram.counts,下面给出这个文件的部分内容:
48876 的
11114 在
10171 一
9609 了
9421 不
8400 是
6729 有
5362 个
5219 和
4606 将
4220 为
4199 上
4118 中国
3976 也
3769 之
乍一看与word.counts没啥区别,实际上是有略微区别的。。。
区别是
本文件这些词在音素文件能找到对应的音素的
接下来的几个文件的生成过程没看懂。。。。
最后调用了这个文件,流程比较复杂暂不分析:
lm的含义为语言模型
${KALDI_ROOT}/tools/kaldi_lm/train_lm.sh
行号:37-38
# 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;
经过资料查询:
utils/format_lm.sh:上述的语言工具基于第三方工具,为ARPA-format,脚本的作业是将其转换为fst,方便与之前的字典fst(L.fst)结合,发挥fst的优势。脚本最后会检测G.fst中是否存在没有单词的空回环,如果存在会报错,因为这会导致后续HLG determinization的出现错误。
脚本utils/format_lm.sh解决把ARPA格式的语言模型转换成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.
这个脚本的一些关键命令如下:
gunzip -c $lm \
| arpa2fst --disambig-symbol=#0 \
--read-symbol-table=$out_dir/words.txt - $out_dir/G.fst
Kaldi程序arpa2fst将ARPA格式的语言模型转换成一个加权有限状态转移机(实际上是接收机)。
构建语言模型的常用工具时SRILM。不同的语言模型工具都在Kaldi的样例脚本中用到了。SRILM有最好的文档和最全面的功能,通常推荐它(唯一的缺点是它没有最免费的许可证)。 以下是utils / format_lm_sri.sh的使用信息:
Usage: utils/format_lm_sri.sh [options] <lang-dir> <arpa-LM> <out-dir>
E.g.: utils/format_lm_sri.sh data/lang data/local/lm/foo.kn.gz data/lang_test
Converts ARPA-format language models to FSTs. Change the LM vocabulary using SRILM.
行号: 40 - 48
# Now make MFCC plus pitch features.
# mfccdir should be some place with a largish disk where you
# want to store MFCC features.
mfccdir=mfcc
for x in train dev test; do
steps/make_mfcc_pitch.sh --cmd "$train_cmd" --nj 10 data/$x exp/make_mfcc/$x $mfccdir || exit 1;
steps/compute_cmvn_stats.sh data/$x exp/make_mfcc/$x $mfccdir || exit 1;
utils/fix_data_dir.sh data/$x || exit 1;
done
(1)mfccdir=mfcc:将mfcc字符串赋值给变量mfccdir
(2)然后运行steps/下的特征提取脚本make_mfcc.sh,其中–cmd traincmd为脚本cmd.sh中设置的run.pl;−−nj
feats_nj 为运行job数目; data/x为源数据的目录(data/train,data/dev,data/test为数据准备阶段生成的);exp/makemfcc/x 为中间生成的.log文件。$mfccdir 为目标目录mfcc文件夹,里面的文件机位提取得分mfcc;
(3)compute_cmvn_stats.sh 是为了计算提取特征的CMVN,即为倒谱方差均值归一化!
把 copy-feats 加入到环境变量里面:
utils/fix_data_dir.sh
使用的命令为:
copy-feats ark:cmvn_train.ark ark,t:cmvn_train.txt
该脚本会修复排序错误,并会移除那些被指明需要特征数据或标注,但是却找不到被需要的数据的那些发音(utterances)。
行号: 50-51
steps/train_mono.sh --cmd "$train_cmd" --nj 10 \
data/train data/lang exp/mono || exit 1;
用来训练单音子隐马尔科夫模型,一共进行40次迭代,每两次迭代进行一次对齐操作
gmm-init-mono->compile-train-graphs->align-equal-compiled->gmm-est->
{gmm-align-compiled->gmm-acc-stats-ali->gmm-est}40->analyze_alignments.sh
摘要:对语音数据进行分帧和提取特征以后,语音标注是对一整段话进行标注而没有具体到某一帧,但训练系统需要有每一帧语音的具体对应标注。本文介绍了kaldi训练monophone脚本的过程,脚本中每个程序的作用以及相关参数配置对训练结果的影响。
#!/bin/bash
# Copyright 2012 Johns Hopkins University (Author: Daniel Povey)
# Apache 2.0
# To be run from ..
# Flat start and monophone training, with delta-delta features.
# This script applies cepstral mean normalization (per speaker).
# Begin configuration section.
nj=4
cmd=run.pl
scale_opts="--transition-scale=1.0 --acoustic-scale=0.1 --self-loop-scale=0.1"
num_iters=40 # Number of iterations of training
max_iter_inc=30 # Last iter to increase #Gauss on.
totgauss=1000 # Target #Gaussians.
careful=false
boost_silence=1.0 # Factor by which to boost silence likelihoods in alignment
realign_iters="1 2 3 4 5 6 7 8 9 10 12 14 16 18 20 23 26 29 32 35 38";
config= # name of config file.
stage=-4
power=0.25 # exponent to determine number of gaussians from occurrence counts
norm_vars=false # deprecated, prefer --cmvn-opts "--norm-vars=false"
cmvn_opts= # can be used to add extra options to cmvn.
# End configuration section.
echo "$0 $@" # Print the command line for logging
if [ -f path.sh ]; then . ./path.sh; fi
. parse_options.sh || exit 1;
if [ $# != 3 ]; then
echo "Usage: steps/train_mono.sh [options] <data-dir> <lang-dir> <exp-dir>"
echo " e.g.: steps/train_mono.sh data/train.1k data/lang exp/mono"
echo "main options (for others, see top of script file)"
echo " --config <config-file> # config containing options"
echo " --nj <nj> # number of parallel jobs"
echo " --cmd (utils/run.pl|utils/queue.pl <queue opts>) # how to run jobs."
exit 1;
fi
data=$1
lang=$2
dir=$3
oov_sym=`cat $lang/oov.int` || exit 1;
# 按照任务数,将训练数据分成多份,每个任务处理一份数据。
mkdir -p $dir/log
echo $nj > $dir/num_jobs
sdata=$data/split$nj;
[[ -d $sdata && $data/feats.scp -ot $sdata ]] || split_data.sh $data $nj || exit 1;
# 特征归一化选项,这里默认指定要对variance进行归一化,还可从外部接收其他归一化选项,如果外部指定不对variance进行归一化,则外部指定生效。
$norm_vars && cmvn_opts="--norm-vars=true $cmvn_opts"
echo $cmvn_opts > $dir/cmvn_opts # keep track of options to CMVN.
feats="ark,s,cs:apply-cmvn $cmvn_opts --utt2spk=ark:$sdata/JOB/utt2spk scp:$sdata/JOB/cmvn.scp scp:$sdata/JOB/feats.scp ark:- | add-deltas ark:- ark:- |"
example_feats="`echo $feats | sed s/JOB/1/g`";
echo "$0: Initializing monophone system."
[ ! -f $lang/phones/sets.int ] && exit 1;
shared_phones_opt="--shared-phones=$lang/phones/sets.int"
if [ $stage -le -3 ]; then
# Note: JOB=1 just uses the 1st part of the features-- we only need a subset anyway.
# 获取特征的维度
if ! feat_dim=`feat-to-dim "$example_feats" - 2>/dev/null` || [ -z $feat_dim ]; then
feat-to-dim "$example_feats" -
echo "error getting feature dimension"
exit 1;
fi
# Flat-start(又称为快速启动),作用是利用少量的数据快速得到一个初始化的 HMM-GMM 模型和决策树
# $lang/topo 中定义了每个音素(phone)所对应的 HMM 模型状态数以及初始时的转移概率
# --shared-phones=$lang/phones/sets.int 选项指向的文件,即$lang/phones/sets.int(该文件生成roots.txt中开头为share split的部分,表示同一行元素共享pdf,允许进行决策树分裂),文件中同一行的音素(phone)共享 GMM 概率分布。tree文件由sets.int产生。
# --train-feats=$feats subset-feats --n=10 ark:- ark:-| 选项指定用来初始化训练用的特征,一般采用少量数据,程序内部会计算这批数据的means和variance,作为初始高斯模型。sets.int中所有行的初始pdf都用这个计算出来的means和variance进行初始化。
$cmd JOB=1 $dir/log/init.log \
gmm-init-mono $shared_phones_opt "--train-feats=$feats subset-feats --n=10 ark:- ark:-|" $lang/topo $feat_dim \
$dir/0.mdl $dir/tree || exit 1;
fi
# 计算当前高斯数,(目标高斯数 - 当前高斯数)/ 增加高斯迭代次数 得到每次迭代需要增加的高斯数目
numgauss=`gmm-info --print-args=false $dir/0.mdl | grep gaussians | awk '{print $NF}'`
incgauss=$[($totgauss-$numgauss)/$max_iter_inc] # per-iter increment for #Gauss
# 构造训练的网络,从源码级别分析,是每个句子构造一个phone level 的fst网络。
# $sdaba/JOB/text 中包含对每个句子的单词(words level)级别标注, L.fst是字典对于的fst表示,作用是将一串的音素(phones)转换成单词(words)
# 构造monophone解码图就是先将text中的每个句子,生成一个fst(类似于语言模型中的G.fst,只是相对比较简单,只有一个句子),然后和L.fst 进行composition 形成训练用的音素级别(phone level)fst网络(类似于LG.fst)。
# fsts.JOB.gz 中使用 key-value 的方式保存每个句子和其对应的fst网络,通过 key(句子) 就能找到这个句子的fst网络,value中保存的是句子中每两个音素之间互联的边(Arc),例如句子转换成音素后,标注为:"a b c d e f",那么value中保存的其实是 a->b b->c c->d d->e e->f 这些连接(kaldi会为每种连接赋予一个唯一的id),后面进行 HMM 训练的时候是根据这些连接的id进行计数,就可以得到转移概率。
if [ $stage -le -2 ]; then
echo "$0: Compiling training graphs"
$cmd JOB=1:$nj $dir/log/compile_graphs.JOB.log \
compile-train-graphs $dir/tree $dir/0.mdl $lang/L.fst \
"ark:sym2int.pl --map-oov $oov_sym -f 2- $lang/words.txt < $sdata/JOB/text|" \
"ark:|gzip -c >$dir/fsts.JOB.gz" || exit 1;
fi
if [ $stage -le -1 ]; then
echo "$0: Aligning data equally (pass 0)"
$cmd JOB=1:$nj $dir/log/align.0.JOB.log \
# 训练时需要将标注跟每一帧特征进行对齐,由于现在还没有可以用于对齐的模型,所以采用最简单的方法 -- 均匀对齐
# 根据标注数目对特征序列进行等间隔切分,例如一个具有5个标注的长度为100帧的特征序列,则认为1-20帧属于第1个标注,21-40属于第2个...
# 这种划分方法虽然会有误差,但待会在训练模型的过程中会不断地重新对齐。
align-equal-compiled "ark:gunzip -c $dir/fsts.JOB.gz|" "$feats" ark,t:- \| \
# 对对齐后的数据进行训练,获得中间统计量,每个任务输出到一个acc文件。
# acc中记录跟HMM 和GMM 训练相关的统计量:
# HMM 相关的统计量:两个音素之间互联的边(Arc) 出现的次数。
# 如上面所述,fst.JOB.gz 中每个key对于的value保存一个句子中音素两两之间互联的边。
# gmm-acc-stats-ali 会统计每条边(例如a->b)出现的次数,然后记录到acc文件中。
# GMM 相关的统计量:每个pdf-id 对应的特征累计值和特征平方累计值。
# 对于每一帧,都会有个对齐后的标注,gmm-acc-stats-ali 可以根据标注检索得到pdf-id,
# 每个pdf-id 对应的GMM可能由多个单高斯Component组成,会先计算在每个单高斯Component对应的分布下这一帧特征的似然概率(log-likes),称为posterior。
# 然后:
# (1)把每个单高斯Component的posterior加到每个高斯Component的occupancy(占有率)计数器上,用于表征特征对于高斯的贡献度,
# 如果特征一直落在某个高斯的分布区间内,那对应的这个值就比较大;相反,如果一直落在区间外,则表示该高斯作用不大。
# gmm-est中可以设置一个阈值,如果某个高斯的这个值低于阈值,则不更新其对应的高斯。
# 另外这个值(向量)其实跟后面GMM更新时候的高斯权重weight的计算相关。
# (2)把这一帧数据加上每个单高斯Component的posterior再加到每个高斯的均值累计值上;
# 这个值(向量)跟后面GMM的均值更新相关。
# (3)把这一帧数据的平方值加上posterior再加到每个单高斯Component的平方累计值上;
# 这个值(向量)跟后面GMM的方差更新相关。
# 最后将均值累计值和平方累计值写入到文件中。
gmm-acc-stats-ali --binary=true $dir/0.mdl "$feats" ark:- \
$dir/0.JOB.acc || exit 1;
fi
# In the following steps, the --min-gaussian-occupancy=3 option is important, otherwise
# we fail to est "rare" phones and later on, they never align properly.
# 根据上面得到的统计量,更新每个GMM模型,AccumDiagGmm中occupancy_的值决定混合高斯模型中每个单高斯Component的weight;
# --min-gaussian-occupancy 的作用是设置occupancy_的阈值,如果某个单高斯Component的occupancy_低于这个阈值,那么就不会更新这个高斯,
# 而且如果 --remove-low-count-gaussians=true,则对应得单高斯Component会被移除。
if [ $stage -le 0 ]; then
gmm-est --min-gaussian-occupancy=3 --mix-up=$numgauss --power=$power \
$dir/0.mdl "gmm-sum-accs - $dir/0.*.acc|" $dir/1.mdl 2> $dir/log/update.0.log || exit 1;
rm $dir/0.*.acc
fi
beam=6 # will change to 10 below after 1st pass
# note: using slightly wider beams for WSJ vs. RM.
x=1
while [ $x -lt $num_iters ]; do
echo "$0: Pass $x"
if [ $stage -le $x ]; then
if echo $realign_iters | grep -w $x >/dev/null; then
echo "$0: Aligning data"
# gmm-boost-silence 的作用是让某些phones(由第一个参数指定)对应pdf的weight乘以--boost 参数所指定的数字,强行提高(如果大于1)/降低(如果小于1)这个phone的概率。
# 如果多个phone共享同一个pdf,程序中会自动做去重,乘法操作只会执行一次。
mdl="gmm-boost-silence --boost=$boost_silence `cat $lang/phones/optional_silence.csl` $dir/$x.mdl - |"
# 执行force-alignment操作。
# --self-loop-scale 和 --transition-scale 选项跟HMM 状态跳转相关,前者是设置自转因子,后者是非自传因子,可以修改这两个选项控制HMM的跳转倾向。
# --acoustic-scale 选项跟GMM输出概率相关,用于平衡 GMM 输出概率和 HMM 跳转概率的重要性。
# --beam 选项用于计算对解码过程中出现较低log-likelihood的token进行裁剪的阈值,该值设计的越小,大部分token会被裁剪以便提高解码速度,但可能会在开始阶段把正确的token裁剪掉导致无法得到正确的解码路径。
# --retry-beam 选项用于修正上述的问题,当无法得到正确的解码路径后,会增加beam的值,如果找到了最佳解码路径则退出,否则一直增加指定该选项设置的值,如果还没找到,就抛出警告,导致这种问题要么是标注本来就不对,或者retry-beam也设计得太小。
$cmd JOB=1:$nj $dir/log/align.$x.JOB.log \
gmm-align-compiled $scale_opts --beam=$beam --retry-beam=$[$beam*4] --careful=$careful "$mdl" \
"ark:gunzip -c $dir/fsts.JOB.gz|" "$feats" "ark,t:|gzip -c >$dir/ali.JOB.gz" \
|| exit 1;
fi
# 更新模型
$cmd JOB=1:$nj $dir/log/acc.$x.JOB.log \
gmm-acc-stats-ali $dir/$x.mdl "$feats" "ark:gunzip -c $dir/ali.JOB.gz|" \
$dir/$x.JOB.acc || exit 1;
$cmd $dir/log/update.$x.log \
gmm-est --write-occs=$dir/$[$x+1].occs --mix-up=$numgauss --power=$power $dir/$x.mdl \
"gmm-sum-accs - $dir/$x.*.acc|" $dir/$[$x+1].mdl || exit 1;
rm $dir/$x.mdl $dir/$x.*.acc $dir/$x.occs 2>/dev/null
fi
# 线性增加混合高斯模型的数目,直到指定数量。
if [ $x -le $max_iter_inc ]; then
numgauss=$[$numgauss+$incgauss];
fi
# 提高裁剪门限。
beam=10
x=$[$x+1]
done
( cd $dir; rm final.{mdl,occs} 2>/dev/null; ln -s $x.mdl final.mdl; ln -s $x.occs final.occs )
utils/summarize_warnings.pl $dir/log
echo Done
# example of showing the alignments:
# show-alignments data/lang/phones.txt $dir/30.mdl "ark:gunzip -c $dir/ali.0.gz|" | head -4
行号: 53-58
# 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