kaldi nnet3模型对单一语音文件在线解码

本文主要以 kaldi中的CSJ日语数据集为例进行讲解,文中涉及的路径都是在 egs/csj/s5 中可以找到的。

online2-wav-nnet3-latgen-faster 是nnet3模型解码的核心程序,它读取 wav 文件,进行 mfccivector 特征处理,根据单词符号表生成对应的词图,即 lattice,一般是以压缩文件形式存在的 lat.gz, 它的C++源码文件是
kaldi-master/src/online2bin/online2-wav-nnet3-latgen-faster.cc,其中逐帧读取语音文件的代码在244-273行,如下所示:

        while (samp_offset < data.Dim()) {
          int32 samp_remaining = data.Dim() - samp_offset;
          int32 num_samp = chunk_length < samp_remaining ? chunk_length
                                                         : samp_remaining;
          KALDI_LOG << "current time: " << samp_offset / samp_freq;
          SubVector<BaseFloat> wave_part(data, samp_offset, num_samp);
          feature_pipeline.AcceptWaveform(samp_freq, wave_part);

          samp_offset += num_samp;
          decoding_timer.WaitUntil(samp_offset / samp_freq);
          if (samp_offset == data.Dim()) {

            feature_pipeline.InputFinished();
          }

          if (silence_weighting.Active() &&
              feature_pipeline.IvectorFeature() != NULL) {
            silence_weighting.ComputeCurrentTraceback(decoder.Decoder());
            silence_weighting.GetDeltaWeights(feature_pipeline.NumFramesReady(),
                                              &delta_weights);
            feature_pipeline.IvectorFeature()->UpdateFrameWeights(delta_weights);
          }

          decoder.AdvanceDecoding();

          if (do_endpointing && decoder.EndpointDetected(endpoint_opts)) {
            break;
          }
        }
        decoder.FinalizeDecoding();

解码调试的时候在这里加入输出日志的代码比较合适,可以观察对音频文件读取的进度,比如这里添加的 KALDI_LOG

该程序除了要输入音频文件,还有很多配置文件也是必须的,下面我给出自己的运行脚本,主要参考的是 csj目录下的 s5/steps/online/nnet3/decode.sh

#!/bin/bash
model_dir="online-data/models/nnet3"
silence_weight=1
max_state_duration=40
do_endpointing=false
frames_per_chunk=20
extra_left_context_initial=0
online=true
frame_subsampling_opt="--frame-subsampling-factor=3"
online_config=online-data/models/nnet3/online.conf
min_active=200
max_active=7000
beam=15.0
lattice_beam=6.0
acwt=1.0
post_decode_acwt=10.0
symtable=$model_dir/words.txt
model=$model_dir/final.mdl
FST=$model_dir/HCLG.fst
wav_rspecifier="ark,s,cs:wav-copy scp,p:$model_dir/wav.scp ark:- |"

spk2utt_rspecifier="ark:$model_dir/spk2utt"

if [ "$post_decode_acwt" == 1.0 ]; then
    lat_wspecifier="ark:|gzip -c > work/lat.gz"
else
    lat_wspecifier="ark:|lattice-scale --acoustic-scale=$post_decode_acwt ark:- ark:- | gzip -c >work/lat.gz"
fi

if ["$silence_weight" != "1.0" ]; then
    silphones=$(cat $model_dir/phones/silence.csl) || exit 1
    silence_weighting_opts="--ivector-silence-weighting.max-state-duration=$max_state_duration --ivector-silence-weighting.silence_phones=$silencephones --ivector-silence-weighting.silence-weighting=$silence_weight"
else
    silence_weighting_opts=
fi

online2-wav-nnet3-latgen-faster $silence_weighting_opts \
 --do-endpointing=$do_endpointing \
 --frames-per-chunk=$frames_per_chunk \
 --extra-left-context-initial=$extra_left_context_initial \
 --online=$online \
 $frame_subsampling_opt \
 --config=$online_config \
 --min-active=$min_active --max-active=$max_active --beam=$beam --lattice-beam=$lattice_beam \
 --acoustic-scale=$acwt --word-symbol-table=$symtable \
 $model $FST $spk2utt_rspecifier "$wav_rspecifier" \
 "$lat_wspecifier"

在这里讲几个关键的配置:

这个是终点检测用的,没有特殊需求的话最好不要设置,如果静音时间超过某一阈值程序会提前结束识别,源码中对应的片段是:

if (do_endpointing && decoder.EndpointDetected(endpoint_opts)) {
    break;
}

该配置文件设置了mfcc和iverctor的提取参数,具体如下所示:

--feature-type=mfcc
--mfcc-config=/home/kaldi-master/egs/csj/online_demo/online-data/models/nnet3/conf/mfcc.conf
--ivector-extraction-config=/home/kaldi-master/egs/csj/online_demo/online-data/models/nnet3/conf/ivector_extractor.conf
--endpoint.silence-phones="1:2:3:4:5:6:7:8:9:10"

一般来说,如果用 s5/local/chain/run_tdnn.sh 顺利地完成了nnet3模型的训练和解码,这些跑配置文件是可以自动生成的,准确来讲是通过 s5/steps/online/nnet3/prepare_online_decode.sh 生成的,可以在 s5/exp/chain/tdnn1a_online/conf 中找到,注意里面都要写成绝对路径。

这个其实没什么说的,就是单词表words.txt

这两个就直接用训练好的就行,分别是 final.mdlHCLG.fst

这两个文件一开始我是有点迷惑的,因为在训练和测试数据中,utterance是有明确的时间戳标注的,但对于未知的单一音频文件的解码很显然没法提供明确的时间戳,只能让他一次读取整个音频文件,下面是spk2utt和wav.scp的写法:
spk2utt:

kaldiAudio kaldiAudio

后面的utt直接用文件名代替就行。
wav.scp:

kaldiAudio cat online-data/audio/kaldiAudio.wav |

这样写可以让程序一次读取整个音频再输出结果,但事实上在实际的应用中这样做很不科学,最有效的方法是用 VAD之类的端点检测技术先检测出包含语音数据的帧片段,打上时间戳提取出来再进行识别,避免静音部分对识别造成影响。否则的话,直接对整个音频识别的时候,静音部分很容易被识别成一些语气词,影响整体识别效果。

最后需要注意的就是输出的不是识别结果,而是词图 lattice, 后面还要经过进一步的解码从词图中找到一条最佳路径才是最终的识别结果。

#!/bin/bash
beam=6.0

lattice=work/lat.gz
symtable=online-data/models/nnet3/words.txt
symtable_nopos=words-nopos.txt
spk="kaldiAudio"

lattice-scale --inv-acoustic-scale=10 "ark:gunzip -c $lattice|" ark:- | \
    lattice-add-penalty --word-ins-penalty=0.0 ark:- ark:- | \
    lattice-best-path --word-symbol-table=$symtable ark:- ark,t:word_ids

perl int2sym.pl -f 2- $symtable_nopos word_ids | tr -d "$spk "

解码的输入就是前面生成的词图压缩文件 lat.gz, 注意为了达到最佳的解码效果, lattice-scale 中的 –inv-acousic-scale 要和前面的脚本中 post_decode_acwt 保持一致,这里为10。

最后的 lattice-best-path 以单词id的形式输出一条最优的解码路径,我们用 int2sym.pl 将id转换为单词显示出来,
ini2sym.pl 的输入就是words.txt,我这里的words-nopos.txt 是去掉了原来文件中的词性部分,只留下单词,看起来会更清楚一些。

那么nnet3模型线上解码的流程大致就是这样,有机会我会在理论部分中深入讲解一下解码参数中的 acousitc-scale 和各种 beam 如何对解码结果产生影响。

Original: https://blog.csdn.net/dhj_tsukuba/article/details/113620910
Author: dhj_tsukuba
Title: kaldi nnet3模型对单一语音文件在线解码

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/515607/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球