织梦CMS - 轻松建站从此开始!

罗索

Kaldi 中的声纹识别

jackyhwei 发布于 2020-01-02 20:22 点击:次 
前段时间一直到在使用 kaldi 来做声纹识别,算是可以把整个 ivector 的例程可以跑下来,也可以根据例程来改写脚本,使用自己的数据来训练和测试。
TAG: 声纹识别  kaldi  

前段时间一直到在使用 kaldi 来做声纹识别,算是可以把整个 ivector 的例程可以跑下来,也可以根据例程来改写脚本,使用自己的数据来训练和测试。接下来可能要去做其他的项目了,所以要趁着还记得的时候赶紧写个总结,也算是对之前的工作也算是归纳一下。

kaldi 的安装

kaldi 在 Linux 下的安装总的来说还是比较简单的,首先是先进入 tools 中运行 extras/check_dependenices.sh 看下还有哪些依赖项没有安装,然后就可以按照他的提示来安装依赖项目。安装完依赖项之后就分别进入 tools 目录和 src 目录下执行命令 make -j8,其中 8 时 cpu 可以同时运行的线程数量。这个过程还是需要一定时间的。在 make 完之后就可以运行一个小的例程来看下有没有成功地安装 kaldi,我们进入到 egs/yesno/s5 目录下然后运行 run.sh 脚本,这是一个判断语音中说的是 yes 还是 no 的程序,他会自动下载数据并训练和测试,最终可以有 0.0% 的 WER,这就代表 kaldi 安装成功啦✌️

运行 aishell 例程

首先我们来看下 kaldi 下的目录:

  • egs:保存了各种例程,均使用脚本编写,以使用的数据库的名字命名。在下一级目录中以 s 开头的文件是语音识别,以 v 开头的是声纹识别,一般 v1 就是使用 i-vector 的方法来进行声纹识别。
  • src:保存了 kaldi 的 C++ 代码。
  • tools:包括了 kaldi 依赖的库和一些实用的脚本。
  • windows:包括了在 Windows 下安装需要的一些工具和配置文件

接下来我们就来跑一下 aishell 的声纹识别例程,在 egs/aishell/v1 中的 run.sh 就包括了整个声纹识别的流程,最好将 run.sh 中的命令复制到另外一个脚本中,一句一句地执行,这样就能及时发现错误然后修改。

  1. data=/export/a05/xna/data 
  2. data_url=www.openslr.org/resources/33 
  3.  
  4. . ./cmd.sh 
  5. . ./path.sh 
  6.  
  7. set -e # exit on error 
  8.  
  9. local/download_and_untar.sh $data $data_url data_aishell 
  10. local/download_and_untar.sh $data $data_url resource_aishell 
  11.  
  12. # Data Preparation 
  13. local/aishell_data_prep.sh $data/data_aishell/wav $data/data_aishell/transcript 

首先是数据准备阶段,如果没有下载数据,脚本也可以自动下载和解压;如果下载好了就要把 data 的路径改成自己存放数据的路径。之后的 cmd.sh 和 path.sh 分别是设置执行命令的方式和 kaldi 的路径。如果我们是在自己的电脑上运行,就需要进入到 cmd.sh 中,把 queue.pl 修改成 run.pl。path.sh 就是设置和 kaldi 相关的路径,如果是例程的话就不用修改了。配置好之后就开始下载和解压数据。

之后就是最关键的部分了,准备一些下面环节需要的文档,使用 aishell_data_prep.sh 这个脚本来生成。声纹识别需要用到的分别是 utt2spk spk2utt wav.scp 这三个文件。其中 utt 指的是 utterance 代表一个音频文件的文件名,spk 代表 speaker 是说话人的 ID,这里在下一节做详细的介绍。如果是做语音识别,还需要 text 文件,这里就不做介绍了。

  1. # Now make MFCC  features. 
  2. # mfccdir should be some place with a largish disk where you 
  3. # want to store MFCC features. 
  4. mfccmfccdir=mfcc 
  5. for x in train test; do 
  6.   steps/make_mfcc.sh --cmd "$train_cmd" --nj 10 data/$x exp/make_mfcc/$x $mfccdir 
  7.   sid/compute_vad_decision.sh --nj 10 --cmd "$train_cmd" data/$x exp/make_mfcc/$x $mfccdir 
  8.   utils/fix_data_dir.sh data/$x 
  9. done 

在准备好数据之后就要开始提取 mfcc 特征了(make_mfcc 的过程中也包括了分帧加窗),进行端点检测(VAD),以及检查文件符不符合要求对文件进行排序(其实我也没有看太懂 fix_data_dir.sh 这个脚本到底做了什么

  1. # train diag ubm 
  2. sid/train_diag_ubm.sh --nj 10 --cmd "$train_cmd" --num-threads 16  
  3.   data/train 1024 exp/diag_ubm_1024 
  4.  
  5. #train full ubm 
  6. sid/train_full_ubm.sh --nj 10 --cmd "$train_cmd" data/train  
  7.   exp/diag_ubm_1024 exp/full_ubm_1024 
  8.  
  9. #train ivector 
  10. sid/train_ivector_extractor.sh --cmd "$train_cmd --mem 10G"  
  11.   --num-iters 5 exp/full_ubm_1024/final.ubm data/train  
  12.   exp/extractor_1024 

再接下来就是训练 UBM 和 ivector extractor 了,这里需要注意的是训练 ivector extractor 的脚本会默认同时执行程序非常多,会占用很高的内存导致内存溢出。我们需要进入 train_ivector_extractor.sh 中修改一下。它默认同时执行的程序数量为 nj*num_thread*num_processes, 在 16G 内存下我把这三个参数都改为 2 才能跑通。这里也还有两个超参数可以修改,分别是 UBM 的维数和 ivector 的维数,UBM 的维数就直接在 run.sh 中修改就行,train_diag_ubm.sh 中 data/train 后面那个参数就是 UBM 的维数,默认为 1024。要修改 ivector 的维数就同样需要进到 train_ivector_extractor.sh 中修改 ivector_dim,默认为 400。

  1. #extract ivector 
  2. sid/extract_ivectors.sh --cmd "$train_cmd" --nj 10  
  3.   exp/extractor_1024 data/train exp/ivector_train_1024 
  4.  
  5. #train plda 
  6. $train_cmd exp/ivector_train_1024/log/plda.log  
  7.   ivector-compute-plda ark:data/train/spk2utt  
  8.   'ark:ivector-normalize-length scp:exp/ivector_train_1024/ivector.scp  ark:- |'  
  9.   exp/ivector_train_1024/plda 

训练完 ivector 之后就要开始提取训练集的 ivector 了,然后用训练集的 ivector 来训练 plda 模型用于打分。

  1. # split the test to enroll and eval 
  2. mkdir -p data/test/enroll data/test/eval 
  3. cp data/test/{spk2utt,feats.scp,vad.scp} data/test/enroll 
  4. cp data/test/{spk2utt,feats.scp,vad.scp} data/test/eval 
  5. local/split_data_enroll_eval.py data/test/utt2spk  data/test/enroll/utt2spk 
  6.  data/test/eval/utt2spk 
  7. trials=data/test/aishell_speaker_ver.lst 
  8. local/produce_trials.py data/test/eval/utt2spk $trials 
  9. utils/fix_data_dir.sh data/test/enroll 
  10. utils/fix_data_dir.sh data/test/eval 

之后就要将测试集分为注册集和验证集,这一步主要通过 loacl/split_data_enroll_eval.py 这个脚本来完成,我们先来看一下这个脚本:

  1. # split_data_enroll_eval.py 
  2. import sys,random 
  3.  
  4. dictutt = {} 
  5.  
  6. for line in open(sys.argv[1]): 
  7.   lineline = line.rstrip('  ') 
  8.   utt, spk = line.split(' ') 
  9.   if spk not in dictutt: 
  10.     dictutt[spk] = [] 
  11.   dictutt[spk].append(utt) 
  12.  
  13. fenroll = open(sys.argv[2], 'w') 
  14. feval = open(sys.argv[3], 'w') 
  15.  
  16. for key in dictutt: 
  17.   utts = dictutt[key] 
  18.   random.shuffle(utts) 
  19.   for i in range(0, len(utts)): 
  20.     line = utts[i] + ' ' + key 
  21.     if(i < 3): 
  22.       fenroll.write(line + ' ') 
  23.     else: 
  24.       feval.write(line + ' ') 
  25.  
  26. fenroll.close() 
  27. feval.close() 

这个脚本首先先将每个 spk 和与其对应的 utt 存入 dictutt 中,然后再将 spk 的 utt 顺序随机打乱,重新分配到 enroll(注册集)和 eval(评估集)中。可以看到在程序的倒数第六行中,if(i<3): 就将 utt 写入 enroll 中,否则就写入 eval 中。所以我们可以通过改这个值来改变注册集和评估集中的语音数。

在重新生成完 utt2spk 之后,就要生成 trials 了。trials 通过 loacl/product_trials.py 来生成。trials 是指需要进行打分的注册说话人和不同的语音的一个列表,它的格式为 (举个例子):

uttID spkID target/nontarget
spkA-utt1 spkA target
spkA-utt2 spkB nontarget
spkB-utt1 spkA nontarget
spkB-utt1 spkB target

 

  1. #extract enroll ivector 
  2. sid/extract_ivectors.sh --cmd "$train_cmd" --nj 10  
  3.   exp/extractor_1024 data/test/enroll  exp/ivector_enroll_1024 
  4. #extract eval ivector 
  5. sid/extract_ivectors.sh --cmd "$train_cmd" --nj 10  
  6.   exp/extractor_1024 data/test/eval  exp/ivector_eval_1024 
  7.  
  8. #compute plda score 
  9. $train_cmd exp/ivector_eval_1024/log/plda_score.log  
  10.   ivector-plda-scoring --num-utts=ark:exp/ivector_enroll_1024/num_utts.ark  
  11.   exp/ivector_train_1024/plda  
  12.   ark:exp/ivector_enroll_1024/spk_ivector.ark  
  13.   "ark:ivector-normalize-length scp:exp/ivector_eval_1024/ivector.scp ark:- |"  
  14.   "cat '$trials' | awk '{print \$2, \$1}' |" exp/trials_out 
  15.  
  16. #compute eer 
  17. awk '{print $3}' exp/trials_out | paste - $trials | awk '{print $1, $4}' | compute-eer - 

在将测试集分成注册集和评估集之后,就开始分别提取注册集和评估集的 ivector,然后按照生成的 trials 打分,最终打分结果输出在 trials_out 中, 最终跑出来的结果为 eer 为 0.183%。

使用 TIMIT 数据库进行声纹识别

在了解了 kaldi 中整个声纹识别的流程后,我们就可以 AISHELL 的例程来改写使用自己数据的声纹识别系统,这里我使用 TIMIT 数据库。

我们首先看下 AISHELL 和 TIMIT 数据库中的数据划分。AISHELL 中一共有 400 人,默认分为 train、dev 和 test 集。其中 train 里面有 340 人;dev 里面有 40 人;test 里面有 20 人。在例程中,使用 train 作为训练集,test 作为测试集,并没有使用 dev。AISHELL 里每个人大概有 300 多段语音,每段语音是一句话,每段语音大概在 2~6s。在 TIMIT 数据库中一共有 630 人,分为 train 和 test。训练集中有 462 人,测试集中有 168 人。每个人分别有 10 段语音,每段语音大概在 2~4s。这里就直接使用 TIMIT 的原本的分配方式,用 462 人作为训练集,168 人作为测试集。

不过使用 TIMIT 数据库还有一个问题就是,TIMIT 数据库中文件存放以及命名的方式和 AISHELL 不太一样。TIMIT 数据库下文件存放的结构是,/TRAIN/DR/SPEARKER_ID/UTTERANCE_ID.wav,train 代表是训练集或者测试集,DR(1~8)代表了说话人的方言类型,然后是说话人的 ID 文件夹,文件夹下存放了 10 段语音。TIMIT 数据库中不同的人会说同一段话,说的话的内容是一样的话文件名就是一样的,我不知道如果有相同的文件名会不会引发错误,稳妥起见还是把每个文件都重新命名了。我写了个程序,将文件都重新命名为说话人的 ID 加上音频的序号,并且将其重新保存在 / TRAIN/SPEAKER_ID 这样的目录下,这样就在下面的程序就可以不用修改太多。

在了解完两个数据库的区别和整个声纹识别的流程之后,我们就可以开始改写我们的程序了。其实整个过程中需要改的地方并不多,主要就是在准备数据阶段和生成 trials 的过程需要修改一下。首先是数据准备阶段,我们就可以根据哈 aishell_data_prepare.sh 这个脚本来改写自己的 timit_data_prepare.sh 了。数据准备阶段就要生成 utt2spk spk2utt 和 wav.scp 这三个文件。这三个文件的格式如下:

文件名 格式
utt2spk [音频文件名] [说话人 ID]
spk2utt [说话人名] [音频文件名] [音频文件名] [音频文件名]
wav.scp [音频文件名] [音频文件的具体路径]
  1. . ./path.sh || exit 1; 
  2.  
  3. if [ $# != 2 ]; then 
  4.   echo "Usage: $0 <audio-path> <text-path>
  5.   echo " $0 /export/a05/xna/data/data_aishell/wav /export/a05/xna/data/data_aishell/transcript" 
  6.   exit 1; 
  7. fi 
  8.  
  9. aishell_audio_dir=$1 
  10. aishell_text_dir=$2 
  11.  
  12. train_dir=data/local/train 
  13. dev_dir=data/local/dev 
  14. test_dir=data/local/test 
  15.  
  16. mkdir -p $train_dir 
  17. mkdir -p $dev_dir 
  18. mkdir -p $test_dir 
  19.  
  20. # data directory check 
  21. if [ ! -d $aishell_audio_dir ] || [ ! -d $aishell_text_dir ]; then 
  22.   echo "Error: $0 requires two directory arguments" 
  23.   exit 1; 
  24. fi 
  25.  
  26. # find wav audio file for train, dev and test resp. 
  27. find $aishell_audio_dir -iname "*.wav"
  28.  | grep -i "wav/train" > $train_dir/wav.flist || exit 1; 
  29. find $aishell_audio_dir -iname "*.wav"
  30.  | grep -i "wav/dev" > $dev_dir/wav.flist || exit 1; 
  31. find $aishell_audio_dir -iname "*.wav"
  32.  | grep -i "wav/test" > $test_dir/wav.flist || exit 1; 

前面首先是检查路径和创建用来存放文件的路径,由于在 TIMIT 中没有 dev 集,所以要把带有 dev 的都删掉。接下来脚本查找目录下的所有 wav 文件。

  1. n=`cat $train_dir/wav.flist $dev_dir/wav.flist $test_dir/wav.flist | wc -l` 
  2. [ $n -ne 141925 ] &&  
  3.   echo Warning: expected 141925 data data files, found $n 
  4.  
  5. # Transcriptions preparation 
  6. for dir in $train_dir $test_dir; do 
  7.   echo Preparing $dir transcriptions 
  8.   sed -e 's/.wav//' $dir/wav.flist | awk -F '/' '{print $NF}' > $dir/utt.list 
  9.   sed -e 's/.wav//' $dir/wav.flist | awk -F '/' '{i=NF-1;printf("%s %s ",$NF,$i)}' > $dir/utt2spk_all 
  10.   paste -d' ' $dir/utt.list $dir/wav.flist > $dir/wav.scp_all 
  11.   utils/filter_scp.pl -f 1 $dir/utt.list $aishell_text_dir/*.txt > $dir/transcripts.txt 
  12.   awk '{print $1}' $dir/transcripts.txt | sort -u > $dir/utt.list 
  13.   utils/filter_scp.pl -f 1 $dir/utt.list $dir/utt2spk_all | sort -u > $dir/utt2spk 
  14.   utils/filter_scp.pl -f 1 $dir/utt.list $dir/wav.scp_all | sort -u > $dir/wav.scp 
  15.   sort -u $dir/transcripts.txt > $dir/text 
  16.   utils/utt2spk_to_spk2utt.pl $dir/utt2spk > $dir/spk2utt 
  17. done 
  18.  
  19. mkdir -p data/train data/test 
  20. for f in spk2utt utt2spk wav.scp text; do 
  21.   cp $train_dir/$f data/train/$f || exit 1; 
  22.   cp $test_dir/$f data/test/$f || exit 1; 
  23. done 
  24.  
  25. echo "$0: AISHELL data preparation succeeded" 
  26. exit 0; 

接下来就检查找到的 wav 文件加起来有没有 141924 个,然后就开始做 wav.scp、utt2spk 和 spk2utt 以及用于语音识别的 transcripts.txt,这里我们就要找到脚本中和 transcripts.txt 相关的,然后删掉就可以了。

再做完准备数据的阶段之后,我们就可以开始按照上面的流程来进行声纹识别了。还需要注意的一点是 trials,如果一个人只有两三段语音的话,就需要修改分配 enroll 集和 eval 集的比例。不过由于 TIMIT 数据库每个人有 10 段语音,所以不用修改也是可以的。这里就用 3 段语音去注册,然后剩下的 7 段语音用于验证。

最终跑出来的等错误率在 4.5% 左右,虽然是一个还可以接受的结果,但是和 AISHELL 的 0.18% 的等错误率相比还是差了很多的。分析一下原因:首先是用于训练的语音较少,虽然人数有 462 人,但是每个人只有 10 段语音,和 AISHELL 中 340 人用于训练,每个人 300 多段语音相比差了很多。同样的,TIMIT 中测试集中一共有 168 人,相比于 AISHELL 中测试集只有 40 人多了很多。而且,AISHELL 默认的训练的 UBM 阶数和 ivector 的维度都非常高,所以这两点可能导致了等错误率比较高。如果想进一步降低等错误率可以尝试降低训练的 UBM 和 ivector 的维度。我把 UBM 和 ivector 的维度都降低后,等错误率最终可以达到 1.53%。

kaldi 中声纹识别的流程

总结一下,kaldi 中声纹的识别(ivector)的流程图如下:

首先,将数据集分为训练集和测试集。然后对先对训练集做处理,先提取训练集的 mfcc 特征,然后训练 UBM 和 ivector extractor,接着提取训练集的 ivector,并使用训练集的 ivector 去训练 plda 模型。之后就开始对测试集进行处理,先把测试集分为注册集和验证集,分别提取 mfcc 然后在提取 ivector,在用 plda 进行打分。这就是整个 kaldi 中 ivector 声纹识别的流程了。

(yutouwd)
本站文章除注明转载外,均为本站原创或编译欢迎任何形式的转载,但请务必注明出处,尊重他人劳动,同学习共成长。转载请注明:文章转载自:罗索实验室 [http://www.rosoo.net/a/202001/17610.html]
本文出处:github 作者:yutouwd 原文
顶一下
(0)
0%
踩一下
(0)
0%
------分隔线----------------------------
发表评论
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
评价:
表情:
用户名: 验证码:点击我更换图片
栏目列表
将本文分享到微信
织梦二维码生成器
推荐内容