Kaldi解码加速策略概述

前言

本文介绍几种优化解码器加速方法,基于kaldi chain模型解码器(online2-wav-nnet3-latgen-faster),训练的模型用于唤醒词场景,主要优化内容包含:特征提取、TDNN神经网络计算、FST优化、lattice获取1-best等。

除了以上方法,kaldi解码器、openfst、openblas等在编译时添加 -O3 优化选项 和 硬浮点运算(需硬件支持)的编译选项(如ARM neon: -mfloat-abi=softfp -mfpu=neon)

1. 特征提取加速

这里主要介绍mfcc特征提取加速优化,在提取mfcc特征时,默认开启了dither功能,此功能主要目的是添加了随机抖动噪声,防止声音的溢出,因为随机数生成耗时较多(见以下代码片段3),移除抖动可以节省mfcc 1/2 ~ 2/3 左右的时间。

// 片段1
void ProcessWindow(const FrameExtractionOptions &opts,
                   const FeatureWindowFunction &window_function,
                   VectorBase<BaseFloat> *window,
                   BaseFloat *log_energy_pre_window) {
  ...
  if (opts.dither != 0.0)
    Dither(window, opts.dither);
  ...
  window->MulElements(window_function.window);
}

// 片段2
void Dither(VectorBase<BaseFloat> *waveform, BaseFloat dither_value) {
  ...
  RandomState rstate;
  for (int32 i = 0; i < dim; i++)
    data[i] += RandGauss(&rstate) * dither_value;
}

// 片段3
/// Returns a random number strictly between 0 and 1.
inline float RandUniform(struct RandomState* state = NULL) {
  return static_cast<float>((Rand(state) + 1.0) / (RAND_MAX+2.0));
}

inline float RandGauss(struct RandomState* state = NULL) {
  return static_cast<float>(sqrtf (-2 * Log(RandUniform(state)))
                            * cosf(2*M_PI*RandUniform(state)));
}

2. TDNN神经网络计算

优化1:
除了openblas数学库编译选项优化方法外,就是依据硬件平台改写具体的矩阵乘法加法运算(X * W + B),为什么说只有矩阵乘法加法运算呢?因为在加载声学模型final.mdl时,会有一个初始化的过程,compile后仅保留权重和偏置值,final.mdl文件中的batch-normal、dropout等参数在解码时并不会使用到。final.mdl模型compile入口如下:
nnet3::CollapseModel(nnet3::CollapseModelConfig(), &(am_nnet.GetNnet()));

神经网络解码时是前向计算,在前向计算中component 组件只会出现三种:NaturalGradientAffineComponent、AffineComponent及RectifiedLinearComponent(Relu),其中NaturalGradientAffineComponent继承自AffineComponent,在前向计算时其实就只用了AffineComponent和RectifiedLinearComponent。每次解码时都会根据compile得到的commands来循环计算(每层网络的权重和偏置在compile后固定不变)。

void NnetComputer::ExecuteCommand() {
  const NnetComputation::Command &c = computation_.commands[program_counter_];
  int32 m1, m2;
  ...
      case kPropagate: {
        const Component *component = nnet_.GetComponent(c.arg1);
        ComponentPrecomputedIndexes *indexes =
            computation_.component_precomputed_indexes[c.arg2].data;
        const CuSubMatrix<BaseFloat> input(GetSubMatrix(c.arg3));
        CuSubMatrix<BaseFloat> output(GetSubMatrix(c.arg4));
        void *memo = component->Propagate(indexes, input, &output);
        ...
        }
        SaveMemo(c.arg5, *component, memo);
        break;
      }
}

由于网络每层的权重和偏置在copmpile后不变,我们可以获取到对应的参数,进行8bit或者16bit量化,量化一方面将浮点转定点运算,加快速度,另一方面由于float类型占用更多的内存,量化可以减少内存。量化唯一的缺点是需要重写矩阵运算(_ 效率要比openblas高才可以,openblas提供的库函数传参是float或double类型,未支持量化,也是一个难点)

优化2:减小声学模型的大小

  1. 模型训练时移除ivector特征,ivector会使得声学模型变的较大
  2. 对已经训练生成的final.mdl声学模型进行奇异值分解,可以减小模型的大小,利用nnet3-copy或者nnet3-am-copy "–edits-config"中的apply-svd方法可以减小final.mdl尺寸,1.2M左右的声学模型可减小300k左右。新生成的final.mdl识别率极差,需要重新retrain,迭代几个epoch后,识别率可以与之前基本保持一致。

3. FST优化

优化1:
准确的说也不叫fst优化,主要方法是处理发射弧时,调整剪枝参数beam的大小,kaldi默认值为15.0,可以减小该值达到加速的目的,而识别率仅仅略微下降,同时内存也可以降低一些(token数目减少)

优化2:
HCLG.fst的裁剪,HCLG.fst大小越小,遍历的弧就会越少,相应的速度也会更快。

4. lattice获取1-best

online2-wav-nnet3-latgen-faster解码器最终获得的是CompactLattice,在生成CompactLattice之前,还有一个Lattice(也就是代码中所说的RawLattice),其实我们只需要这个RawLattice即可获取1-best path, 关键原因是生成CompactLattice比较耗时,尤其是环境人声嘈杂时,会使得解码时间变得不稳定,有时甚至6~8s左右的时延(因为做了phone、word的Determinize),而RawLattice时间相对稳定(50ms内,同性能CPU比较),因为最终GetDiagnosticsAndPrintOutput输出结果时,又把CompactLattice转换成Lattice,来获取解码序列。

template <typename FST>
void SingleUtteranceNnet3DecoderTpl<FST>::GetLattice(bool end_of_utterance,
                                             CompactLattice *clat) const {
  if (NumFramesDecoded() == 0)
    KALDI_ERR << "You cannot get a lattice if you decoded no frames.";
  Lattice raw_lat;
  decoder_.GetRawLattice(&raw_lat, end_of_utterance);

  if (!decoder_opts_.determinize_lattice)
    KALDI_ERR << "--determinize-lattice=false option is not supported at the moment";

  BaseFloat lat_beam = decoder_opts_.lattice_beam;
  //
  DeterminizeLatticePhonePrunedWrapper(
      trans_model_, &raw_lat, lat_beam, clat, decoder_opts_.det_opts);
}

总结

本文主要总结了几种加快解码速度的方法,解码器从最初的5~8s(嘈杂环境)到现在的400-500ms(嘈杂环境)稳定解码(vad截取特定长度语音片段,CPU 1.2GHz);

此外解码器的精简,移除冗余代码,也可加快一些速度,但速度没有想象中提升很大,尝试裁剪解码器的代码,除去final.mdl、HCLG.fst模型,openfst库的替换改写(占用内存较多),解码器可执行文件可以缩减到300k以下(openblas保留)。

如有其它方法,欢迎一起讨论。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页