Skip to content

Latest commit

 

History

History
1389 lines (1057 loc) · 47.5 KB

yesno_tutorial_ch.md

File metadata and controls

1389 lines (1057 loc) · 47.5 KB

CAT(yesno)项目搭建流程

! This tutorial is originally for CAT v2, which is helpful for readers to go inside of CAT.

  • 2024/4 updated for CAT v3

目录

此文档的目的是,通过介绍如何基于CAT搭建一个简单的语音识别项目,帮助读者更多了解CAT的工作流程,先知其然。欲知所以然,建议进一步阅读以下基本文献。

  • L. R. Rabiner, “A tutorial on hidden Markov models and selected applications in speech recognition”, Proceedings of the IEEE, 1989. PDF
  • A. Graves, S. Fernandez, F. Gomez, and J. Schmidhuber, “Connectionist temporal classification: Labelling unsegmented sequence data with recurrent neural networks”, ICML, 2006. PDF
  • Hongyu Xiang, Zhijian Ou, "CRF-based Single-stage Acoustic Modeling with CTC Topology", ICASSP, 2019. PDF
  • Zhijian Ou, "State-of-the-Art of End-to-End Speech Recognition", Tutorial at The 6th Asian Conference on Pattern Recognition (ACPR2021), Jeju Island, Korea, 2021. PDF

CAT workflow已经整理了CAT的工作流程,分为六步,前五步为训练,第六步是解码。 这份文档将根据CAT workflow,更具体地以一个简单语音识别项目(yesno项目)为例,对CAT工作流程加以解释。

yesno语音识别项目,来自Kaldi中的yesno项目。如下所述,yesno项目只含有两个词汇,yes和no;一句话中会包含多个由希伯来语(Hebrew)说的yes和no。

The "yesno" corpus is a very small dataset of recordings of one individual
saying yes or no multiple times per recording, in Hebrew.  It is available from
http://www.openslr.org/1.

项目目录结构

一个语音识别项目,指在一个特定的数据集上的项目,通常各个项目会安排在egs(examples的缩写)文件目录下。读者也可以尝试训练egs目录下的其它数据集实验。

yesno

├── cmd.sh #脚本配置
├── path.sh #环境变量配置
├── run.sh #实验主程序
├── conf #配置文件目录
│   ├── decode_dnn.config #解码
│   ├── fbank.conf #fbank提取
│   └── mfcc.conf #mfcc提取
├── ctc-crf -> ../../scripts/ctc-crf #ctc-crf程序
├── exp #模型配置
│   ├── demo #demo模型
│   │   └── config.json #demo模型的训练参数
├── input #输入目录
│   └── lexicon.txt #yesno字典
├── local #存放主程序运行各部分脚本块
│   ├── create_yesno_txt.pl #数据预处理waves.txt(音频ID和对应本地路径)
│   ├── create_yesno_waves_test_train.pl #数据训练开发集划分
│   ├── create_yesno_wav_scp.pl #数据预处理waves.scp(音频ID和对应音频内容)
│   ├── get_word_map.pl #对每个词建立映射
│   ├── prepare_data.sh #数据预处理程序
│   ├── prepare_dict.sh #词典预处理程序
│   ├── score.sh #打分脚本(WER)
│   ├── yesno_decode_graph.sh #fst文件整理打包
│   └── yesno_train_lms.sh #语言模型训练
├── steps -> /myhome/kaldi/egs/wsj/s5/steps #链接到kaldi中同名目录,包含各个训练阶段的子脚本,如特征提取 make_fbank.sh等,此路径软连接到Kaldi所在路径
└── utils -> /myhome/kaldi/egs/wsj/s5/utils #链接到kaldi中同名目录,用于协助处理,如数据复制与验证等

接下来我们将利用CAT和yesno数据,一步步搭建一个语音识别项目,再次之前请确保您已经完成了CAT环境配置CAT的安装

0. 文件准备

在这部分中,我们先准备好项目所需要的整体框架。

  1. 在egs下创建yesno目录

  2. 编写以下两个脚本

    • path.sh

      # CAT toolkit
      export CAT_ROOT=../../
      export PATH=$CAT_ROOT/src/ctc_crf/path_weight/build:$PATH
      export PATH=$PWD/ctc-crf:$PATH
      # Kaldi
      export KALDI_ROOT=${KALDI_ROOT:-/myhome/kaldi}
      [ -f $KALDI_ROOT/tools/env.sh ] && . $KALDI_ROOT/tools/env.sh
      export PATH=$PWD/utils/:$KALDI_ROOT/tools/openfst/bin:$PWD:$PATH
      [ ! -f $KALDI_ROOT/tools/config/common_path.sh ] && echo >&2 "The standard file $KALDI_ROOT/tools/config/common_path.sh is not present -> Exit!" && exit 1
      . $KALDI_ROOT/tools/config/common_path.sh
      export LC_ALL=C
      # Data
      export DATA_ROOT=data/yesno

      配置全局的环境变量,分别配置CAT、kaldi、Data(数据集的环境变量),代码来源为egs\wsj项目下的同名文件。

      CAT toolkit: 此处ctc_crf文件夹中的path_weight方法仅在CAT-v2中存在,需要下载CAT-v2中的ctc_crf文件夹,在文件夹中执行make命令,再在path.sh中配置相应的地址

      Kaldi: 路径需要修改到下载好的kaldi根目录下

      Data: 你的yesno根目录下

      创建完后可以在终端里运行一遍./path.sh,没有问题后我们进行下一步。

    • cmd.sh

      # copy from kaldi
      export train_cmd=run.pl
      export decode_cmd=run.pl
      export mkgraph_cmd=run.pl
      export cuda_cmd=run.pl

      这里是沿用来自kaldi的并行化工具,适应不同的环境可以配置queue.pl等以及不同的参数。一般情况下我们默认run.pl即可。

  3. 创建软连接到kaldi以及CAT工具包的目录,便于代码的编写以及迁移

    # path ctc-crf
    ln -s ../../scripts/ctc-crf ctc-crf
    # path utils from kaldi
    ln -s $KALDI_ROOT/egs/wsj/s5/utils utils
    # path steps from kaldi
    ln -s $KALDI_ROOT/egs/wsj/s5/steps steps

    本实验使用的ctc-crf工具包仅在CAT-v2中存在并使用,需要读者自行下载。

  4. 创建local目录,在该目录下存放用于进行数据处理、模型训练、模型评分等流程的相关脚本

  5. 创建run.sh,在run.sh文件中编写代码,逐步实现yesno实验所需的各个步骤,完成实验的构建

    #!/bin/bash
    
    # Copyright 2022 TasiTech
    # Author: Ziwei Li
    # yesno for CAT
    
    # environment
    . ./cmd.sh
    . ./path.sh
    
    #set 
    H=`pwd`  # home dir
    n=12     # parallel jobs=$(nproc)
    stage=0  # set work stages
    stop_stage=9
    change_config=0
    yesno=$DATA_ROOT  #data root
    
    . utils/parse_options.sh
    
    NODE=$1
    if [ ! $NODE ]; then
      NODE=0
    fi
    
    if [ $NODE == 0 ]; then
      if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then
        echo "stage 1: *"
        # work
      fi
    
      #more stages
    fi

    $NODE指实验运行的节点数,若运行run.sh时直接传参节点数,用stage和stop_stage控制代码运行部分。

1. 数据准备

Step 1: Data preparation

至此我们完成了框架准备,下面进入CAT workflow的工作流程,我们按顺序编写每个脚本。

在run.sh中step 1,我们完成以下步骤:获取训练数据,建立所需字典,训练语言模型。

以下为step 1的代码,在本节中我们会详细解释这部分代码的思路。

if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then
  echo "stage 1: Data Preparation and FST Construction"

  local/prepare_data.sh || exit 1; # Get data and lists
  local/prepare_dict.sh || exit 1; # Get lexicon dict

  # Compile the lexicon and token FSTs
  # generate lexicon FST L.fst according to words.txt, generate token FST T.fst according to tokens.txt
  ctc-crf/ctc_compile_dict_token.sh --dict-type "phn" \
    data/dict data/local/lang_phn_tmp data/lang || exit 1;
  
  # Train and compile LMs. Generate G.fst according to lm, and compose FSTs into TLG.fst
  local/yesno_train_lms.sh data/train/text data/dict/lexicon.txt data/lm || exit 1;
  local/yesno_decode_graph.sh data/lm/srilm/srilm.o1g.kn.gz data/lang data/lang_test || exit 1;
fi

prepare_data.sh

我们将数据下载及准备的步骤放在prepare_data.sh中完成。在prepare_data.sh完成后,我们将获得划分为训练集(train)与开发集(dev)的data(wav.scp)、说话人信息(spk2utt、utt2spk,说话人信息默认为global,即无说话人标签)、标注文本信息(text),分别存储在data/dev、data/train下。下面分四步进行介绍。

  1. 在local目录下创建文件prepare_data.sh,并获取数据

    #!/usr/bin/env bash
    # This script prepares data and create necessary files
    
    . ./path.sh
    
    H=`pwd`
    data=${H}/data
    local=${H}/local 
    mkdir -p ${data}/local
    
    cd ${data}
    
    # acquire data if not downloaded
    if [ ! -d waves_yesno ]; then
      echo "Getting Data"
      wget http://www.openslr.org/resources/1/waves_yesno.tar.gz || exit 1;
      tar -xvzf waves_yesno.tar.gz || exit 1;
      rm waves_yesno.tar.gz || exit 1;
    fi

    这一步完成后,我们在data/waves_yesno下得到原始音频数据集。

  2. 由于原始数据没有划分,这部分我们将音频数据集划分为训练集(train)和开发集(dev)

    注:由于数据量较小,这里直接将开发集作为测试集,可以修改。

    echo "Preparing train and dev data"
    
    rm -rf train dev
    
    # Create waves list and Divide into dev and train set
    waves_dir=${data}/waves_yesno
    ls -1 $waves_dir | grep "wav" > ${data}/local/waves_all.list
    cd ${data}/local
    ${local}/create_yesno_waves_test_train.pl waves_all.list waves.dev waves.train

下面编写create_yesno_waves_test_train.pl

create_yesno_waves_test_train.pl

注:这部分代码来源于kaldi中yesno项目,等分waves_all.list到waves.dev, waves.train中。

.pl为perl代码,此部分代码比较难理解。

#!/usr/bin/env perl

$full_list = $ARGV[0];
$test_list = $ARGV[1];
$train_list = $ARGV[2];

open FL, $full_list;
$nol = 0;
while ($l = <FL>)
{
	$nol++;
}
close FL;

$i = 0;
open FL, $full_list;
open TESTLIST, ">$test_list";
open TRAINLIST, ">$train_list";
while ($l = <FL>)
{
	chomp($l);
	$i++;
	if ($i <= $nol/2 )
	{
		print TRAINLIST "$l\n";
	}
	else
	{
		print TESTLIST "$l\n";
	}
}

下面我们继续回到prepare.data.sh,生成test.txt和wave.scp。

  1. 生成*_wav.scp, *.txt(*代指train, test, dev)

    cd ${data}/local
    
    for x in train dev; do
      # create id lists
      ${local}/create_yesno_wav_scp.pl ${waves_dir} waves.$x > ${x}_wav.scp #id to wavfile
      ${local}/create_yesno_txt.pl waves.$x > ${x}.txt #id to content
    done
    
    ${local}/create_yesno_wav_scp.pl ${waves_dir} waves.dev > test_wav.scp #id to wavfile
    ${local}/create_yesno_txt.pl waves.dev > test.txt #id to content

生成的*.scp文件格式为:音频ID和对应的存储位置。

create_yesno_wav_scp.pl

创建*.scp文件,内容为文件名对应的存储位置。

#!/usr/bin/env perl

$waves_dir = $ARGV[0];
$in_list = $ARGV[1];

open IL, $in_list;

while ($l = <IL>)
{
	chomp($l);
	$full_path = $waves_dir . "\/" . $l;
	$l =~ s/\.wav//;
	print "$l $full_path\n";
}

生成*.txt文件,内容为音频ID和对应的文本内容。

create_yesno_txt.pl

创建.txt文件,内容为文件名对应的文本内容。

#!/usr/bin/env perl

$in_list = $ARGV[0];

open IL, $in_list;

while ($l = <IL>)
{
	chomp($l);
	$l =~ s/\.wav//;
	$trans = $l;
	$trans =~ s/0/NO/g;
	$trans =~ s/1/YES/g;
	$trans =~ s/\_/ /g;
	print "$l $trans\n";
}

最后编写prepare_data.sh,生成utt2spk,spk2utt。

  1. 将数据转移到data/dev, data/train, data/test下,并生成utt2spk, spk2utt

    cd ${data}
    
    for x in train dev test; do
      # sort wave lists and create utt2spk, spk2utt
      mkdir -p $x
      sort local/${x}_wav.scp -o $x/wav.scp
      sort local/$x.txt -o $x/text
      cat $x/text | awk '{printf("%s global\n", $1);}' > $x/utt2spk
      sort $x/utt2spk -o $x/utt2spk
      ${H}/utils/utt2spk_to_spk2utt.pl < $x/utt2spk > $x/spk2utt
    done

    utils和step目录下的脚本均为kaldi的脚本,在其目录下有详细解释。

    这一流程完成后,data下的目录结构为:

    ├── dev #开发集
    │   ├── spk2utt #说话人-音频ID
    │   ├── text #音频ID-文本
    │   ├── utt2spk #音频ID-说话人
    │   └── wav.scp #音频ID-文件位置
    ├── train #训练集
    │   ├── spk2utt
    │   ├── text
    │   ├── utt2spk
    │   └── wav.scp
    ├── test #测试集
    │   ├── spk2utt
    │   ├── text
    │   ├── utt2spk
    │   └── wav.scp
    ├── local #中间文件
    │   ├── dev.txt #开发集的text
    │   ├── dev_wav.scp #开发集的wav.scp
    │   ├── test.txt
    │   ├── test_wav.scp
    │   ├── train.txt
    │   ├── train_wav.scp
    │   ├── waves.dev 
    │   ├── waves.train #训练集文件名列表
    │   └── waves_all.list
    └── waves_yesno #音频数据集存储位置
    

    以下展示train目录下的文件的部分内容:

    spk2utt

    [speaker] [wav_name1] [wav_name2] ...

    global 0_0_0_0_1_1_1_1 0_0_0_1_0_0_0_1 0_0_0_1_0_1_1_0 0_0_1_0_0_0_1_0 0_0_1_0_0_1_1_0 0_0_1_0_0_1_1_1 0_0_1_0_1_0_0_0 0_0_1_0_1_0_0_1 0_0_1_0_1_0_1_1 0_0_1_1_0_0_0_1 0_0_1_1_0_1_0_0 0_0_1_1_0_1_1_0 0_0_1_1_0_1_1_1 0_0_1_1_1_0_0_0 0_0_1_1_1_0_0_1 0_0_1_1_1_1_0_0 0_0_1_1_1_1_1_0 0_1_0_0_0_1_0_0 0_1_0_0_0_1_1_0 0_1_0_0_1_0_1_0 0_1_0_0_1_0_1_1 0_1_0_1_0_0_0_0 0_1_0_1_1_0_1_0 0_1_0_1_1_1_0_0 0_1_1_0_0_1_1_0 0_1_1_0_0_1_1_1 0_1_1_1_0_0_0_0 0_1_1_1_0_0_1_0 0_1_1_1_0_1_0_1 0_1_1_1_1_0_1_0
    

    utt2spk

    [wav_name] [speaker]

    0_0_0_0_1_1_1_1 global
    0_0_0_1_0_0_0_1 global
    0_0_0_1_0_1_1_0 global
    0_0_1_0_0_0_1_0 global
    0_0_1_0_0_1_1_0 global
    ...
    

    wav.scp

    [wav_name] [wav_location]

    0_0_0_0_1_1_1_1 /myhome/CAT/egs/yesno/data/waves_yesno/0_0_0_0_1_1_1_1.wav
    0_0_0_1_0_0_0_1 /myhome/CAT/egs/yesno/data/waves_yesno/0_0_0_1_0_0_0_1.wav
    0_0_0_1_0_1_1_0 /myhome/CAT/egs/yesno/data/waves_yesno/0_0_0_1_0_1_1_0.wav
    0_0_1_0_0_0_1_0 /myhome/CAT/egs/yesno/data/waves_yesno/0_0_1_0_0_0_1_0.wav
    ...
    

    text

    [wav_name] [wav_content]

    0_0_0_0_1_1_1_1 NO NO NO NO YES YES YES YES
    0_0_0_1_0_0_0_1 NO NO NO YES NO NO NO YES
    0_0_0_1_0_1_1_0 NO NO NO YES NO YES YES NO
    0_0_1_0_0_0_1_0 NO NO YES NO NO NO YES NO
    0_0_1_0_0_1_1_0 NO NO YES NO NO YES YES NO
    ...
    

    通过生成这些固定格式的文件,我们可以方便地使用kaldi的工具来优化工作流程。

    当前目录下:

    ├── cmd.sh
    ├── ctc-crf -> ../../scripts/ctc-crf
    ├── data
    │   ├── dev
    │   ├── local
    │   ├── test
    │   ├── train
    │   └── waves_yesno
    ├── local
    │   ├── create_yesno_txt.pl
    │   ├── create_yesno_wav_scp.pl
    │   ├── create_yesno_waves_test_train.pl
    │   └── prepare_data.sh
    ├── path.sh
    ├── run.sh
    ├── steps -> /myhome/kaldi/egs/wsj/s5/steps
    └── utils -> /myhome/kaldi/egs/wsj/s5/utils
    

prepare_dict.sh

在prepare_dict.sh中准备词典,下面分两步介绍。

通过这部分代码,我们将在data/dict下获得经过去重和补充噪音<NOISE>、人声噪声<SPOKEN_NOISE>、未知词<UNK>等的词典lexicon.txt,排序并用数字编号的声学单元units.txt,以及用数字标号的词典lexicon_numbers.txt。

声学单元(unit)的选择有多种,可以是音素(phone)、英文字母(character)、汉字、片段(wordpiece)等。词典(lexicon)的作用是,将待识别的词汇表(vocabulary)中的词分解为声学单元的序列。本文档例子中以音素作为声学单元。

  1. 在input目录下创建lexicon.txt,词典保存在input/lexicon.txt中

    <SIL> SIL #静音silence
    YES Y
    NO N
    
  2. 编写local/prepare_dict.sh

    #!/bin/bash
    
    # This script prepares the phoneme-based lexicon. It also generates the list of lexicon units
    # and represents the lexicon using the indices of the units. 
    
    H=`pwd`
    dir=${H}/data/dict
    mkdir -p $dir
    srcdict=input/lexicon.txt
    
    . ./path.sh
    
    # Check if lexicon dictionary exists
    [ ! -f "$srcdict" ] && echo "No such file $srcdict" && exit 1;
    
    # Raw dictionary preparation
    # grep removes SIL, perl removes repeated lexicons
    cat $srcdict | grep -v "SIL" | \
      perl -e 'while(<>){@A = split; if(! $seen{$A[0]}) {$seen{$A[0]} = 1; print $_;}}' \
      > $dir/lexicon_raw.txt || exit 1;
    
    # Get the set of units in the lexicon without noises
    # cut: remove words, tr: remove spaces and lines, sort -u: sort and unique
    cut -d ' ' -f 2- $dir/lexicon_raw.txt | tr ' ' '\n' | sort -u > $dir/units_raw.txt
    
    # add noises for lexicons
    (echo '<SPOKEN_NOISE> <SPN>'; echo '<UNK> <SPN>'; echo '<NOISE> <NSN>'; ) | \
     cat - $dir/lexicon_raw.txt | sort | uniq > $dir/lexicon.txt || exit 1;
    
    # add noises and number the units
    (echo '<NSN>'; echo '<SPN>';) | cat - $dir/units_raw.txt | awk '{print $1 " " NR}' > $dir/units.txt
    
    # Convert phoneme sequences into the corresponding sequences of units indices, encoded by units.txt
    utils/sym2int.pl -f 2- $dir/units.txt < $dir/lexicon.txt > $dir/lexicon_numbers.txt
    
    echo "Phoneme-based dictionary preparation succeeded"

    通过这一脚本的运行后,data目录下会生成一个dict目录如下:

    ├── dict
    │   ├── lexicon_raw.txt #原词典去重和去非语言学发音
    │   ├── units_raw.txt #lexicon_raw词典中音素去重
    │   ├── lexicon.txt #lexicon_raw词典加入非语言学发音并排序
    │   ├── units.txt #units_raw中所有音素标号
    │   └── lexicon_numbers.txt #用units.txt代表词典标号
    

    以下展示dict目录中文件的部分内容:

    lexicon_raw.txt

    [word] [unit1] [unit2] ...

    YES Y
    NO N
    

    units_raw.txt

    [unit]

    N
    Y
    

    lexicon.txt

    <NOISE> <NSN> #自然噪声
    <SPOKEN_NOISE> <SPN> #人声噪声
    <UNK> <SPN> #未知词
    NO N
    YES Y
    

    units.txt

    [unit] [unit_number]

    <NSN> 1
    <SPN> 2
    N 3
    Y 4
    

    lexicon_numbers.txt

    [word] [unit_number1] [unit_number2] ...

    <NOISE> 1
    <SPOKEN_NOISE> 2
    <UNK> 2
    NO 3
    YES 4
    

    yesno数据集上人声噪声和自然噪声可以忽略。

T.fst & L.fst

FST(Finite State Transducers 有限状态转换器)常与WFST(Weighted Finite State Transducers 加权有限状态转换器)的称呼混用,与之差异的是WFST在转移路径上附加了权重。安装openfst正是为了使用(W)FST。如下图所示,理论上,一个WFST表示了输入符号序列和输出符号序列的加权关系。

WFST

想要了解更多了解以下文献:

M. Mohri, F. Pereira, and M. Riley, “Speech Recognition with Weighted Finite-State Transducers”, Handbook on Speech Processing and Speech, Springer, 2008.

根据发音词典,我们生成词典(Lexicon)对应L.fst以及CTC对应的T.fst。此处用到我们在prepare_dict.sh中准备好的lexicon.txt, units.txt, lexicon_numbers.txt这3个文件去生成。 有关Connectionist Temporal Classification (CTC),见原文介绍。T.fst表示状态符号序列(含<blank>)到单元(unit)符号序列的映射。

# Compile the lexicon and token FSTs
# generate Lexicon FST L.fst according to words.txt, generate Topology FST T.fst according to tokens.txt
ctc-crf/ctc_compile_dict_token.sh --dict-type "phn" \
  data/dict data/local/lang_phn_tmp data/lang || exit 1;

详见ctc-crf/ctc_compile_dict_token.sh的注释。

脚本依次通过lexicon_numbers.txt, units.txt生成了words.txt, tokens.txt,进而生成了T.fst, L.fst。

words.txt (代表了L.fst的output symbol inventory,也就是G.fst的input symbol inventory)

<eps> 0 #epsilon,空标签,跳出标签为空
<NOISE> 1
<SPOKEN_NOISE> 2
<UNK> 3
NO 4
YES 5
#0 6 #语言模型G的回退符,确定G.fst
<s> 7 #起始
</s> 8 #结束

FST确定化(determinization)是指,对于一个FST图,任意输入序列只对应唯一跳转。消歧符号(#开头的符号,如#0等)用来确保我们使用的WFST是确定化的,进一步了解推荐阅读《Kaldi语音识别实战》第五章。

tokens.txt (记录了L.fst的input symbol inventory,也是T.fst的output symbol inventory)

<eps> 0
<blk> 1
<NSN> 2
<SPN> 3
N 4
Y 5
#0 6 #G.fst回退符
#1 7 #注:#1,#2为对<SPOKEN_NOISE>和<UNK>的消歧,因为两者都映射到<SPN>
#2 8
#3 9 #SIL的消歧

为了方便理解,以下通过fstprint展示我们生成的fst文件。fst文件的可视化,参考fst可视化教程

T.fst

T.fst

L.fst(注:如果L.fst中没有#3的话,则T.fst中#3也没有必要。历史上若使用HMM拓扑,则需要引入SIL unit,每个词汇可接SIL也可以不接,因而L.fst需要#3进行消岐。本例使用CTC拓扑,L.fst实际上用不着#3)

L.fst

为方便观察,我们去掉<NOISE>, <SPOKEN_NOISE>展示fst生成图,当前:

words.txt

<eps> 0
NO 1
YES 2
#0 3
<s> 4
</s> 5

tokens.txt

<eps> 0
<blk> 1
N 2
Y 3
#0 4
#1 5

T.fst

T.fst(new)

L.fst

L.fst(new)

G.fst

根据data/train/text、dict/lexicon.txt,生成生成语言模型G.fst。

这部分训练我们通过srilm工具完成,放到local/yesno_train_lms.sh中。

# Train and compile LMs. Generate G.fst according to lm, and compose FSTs into TLG.fst
    local/yesno_train_lms.sh data/train/text data/dict/lexicon.txt data/lm || exit 1;

yesno_train_lms.sh

#!/bin/bash

# To be run from one directory above this script.

. ./path.sh

H=`pwd`
text=$1
lexicon=$2
dir=$3
for f in "$text" "$lexicon"; do
  [ ! -f $x ] && echo "$0: No such file $f" && exit 1;
done

#text=data/train/text
#lexicon=data/dict/lexicon.txt
#dir=data/lm
mkdir -p $dir

cleantext=$dir/text.no_oov

# Replace unknown words in text by <UNK>
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("<UNK> ");} } printf("\n");}' \
  > $cleantext || exit 1;

# Count unique words
cat $cleantext | awk '{for(n=2;n<=NF;n++) print $n; }' | sort | uniq -c | \
   sort -nr > $dir/word.counts || exit 1;

# Get counts from acoustic training transcripts, and add  one-count
# for each word in the lexicon (but not silence, we don't want it
# in the LM-- we'll add it optionally later).
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;

# note: we probably won't really make use of <UNK> as there aren't any OOVs
cat $dir/unigram.counts  | awk '{print $2}' | ${H}/local/get_word_map.pl "<s>" "</s>" "<UNK>" > $dir/word_map \
   || exit 1;

# note: ignore 1st field of train.txt, it's the utterance-id.
cat $cleantext | awk -v wmap=$dir/word_map 'BEGIN{while((getline<wmap)>0)map[$1]=$2;}
  { for(n=2;n<=NF;n++) { printf map[$n]; if(n<NF){ printf " "; } else { print ""; }}}' | gzip -c >$dir/train.gz \
   || exit 1;

# LM is small enough that we don't need to prune it (only about 0.7M N-grams).

# From here is some commands to do a baseline with SRILM (assuming
# you have it installed).
heldout_sent=3 
sdir=$dir/srilm
mkdir -p $sdir
cat $cleantext | awk '{for(n=2;n<=NF;n++){ printf $n; if(n<NF) printf " "; else print ""; }}' | \
  head -$heldout_sent > $sdir/heldout
cat $cleantext | awk '{for(n=2;n<=NF;n++){ printf $n; if(n<NF) printf " "; else print ""; }}' | \
  tail -n +$heldout_sent > $sdir/train

cat $dir/word_map | awk '{print $1}' | cat - <(echo "<s>"; echo "</s>" ) > $sdir/wordlist

ngram-count -text $sdir/train -order 1 -limit-vocab -vocab $sdir/wordlist -unk \
  -map-unk "<UNK>" -interpolate -lm $sdir/srilm.o1g.kn.gz
# -kndiscount
ngram -lm $sdir/srilm.o1g.kn.gz -ppl $sdir/heldout 

编写local/get_word_map.pl

get_word_map.pl

#!/usr/bin/perl


# This program reads in a file with one word
# on each line, and outputs a "translation file" of the form:
# word short-form-of-word
# on each line, 
# where short-form-of-word is a kind of abbreviation of the word.
#
# It uses the letters a-z and A-Z, plus the characters from 
# 128 to 255.  The first words in the file have the shortest representation.
#
# For convenience, it makes sure to give <s>, </s> and <UNK>
# a consistent labeling, as A, B and C respectively.


# set up character table and some variables.
@C = ();
foreach $x (ord('A')...ord('Z')) { push @C, chr($x); }
foreach $x (ord('a')...ord('z')) {  push @C, chr($x); }
foreach $x(128...254) { push @C, chr($x); } # 255 is space so don't include it.

@index = ( 3 ); # array of indexes into @C... count up to [dim of C -1]
   # then add another index onto this.  Set it to 3, since 0, 1 and 2 are
   # reserved for <s>, </s> and <UNK> respectively.

if (@ARGV != 3 && @ARGV != 4) {
  die "Usage: get_word_map.pl bos-symbol eos-symbol unk-symbol [words-in-order]\n";
}

$bos = shift @ARGV;
$eos = shift @ARGV;
$unk = shift @ARGV;
print "$bos $C[0]\n";
print "$eos $C[1]\n";
print "$unk $C[2]\n";

sub get_short_form();

while(<>) {
  chop;
  $word = $_;
  $word =~ m:^\S+$: || die "Bad word $word";
  if($seen{$word}) { die "Word $word repeated"; }
  $seen{$word}++;
  if ($word ne $bos && $word ne $eos && $word ne $unk) {
    $short_form = get_short_form();
    print "$word $short_form\n";
  }
}

sub get_short_form() {
  $ans = "";
  foreach $i (@index) { $ans = $C[$i] . $ans; } # 
  # Now increment the index.
  $index[0]++;
  $cur_idx = 0;
  while ($index[$cur_idx] == @C) { # E.g. if the least significant digit
    # is out of the valid range... carry one.
    $index[$cur_idx] = 0;
    $cur_idx++;
    $index[$cur_idx]++; # This will extend the array if necessary.
  }
  return $ans;
}

取3句用SRILM工具计算困惑度,运行结果如下:

file data/lm/srilm/heldout: 3 sentences, 24 words, 0 OOVs
0 zeroprobs, logprob= -11.09502 ppl= 2.575885 ppl1= 2.899294

SRILM工具的使用可以见该工具下的README。训练中需要处理的文件存放在data/lm目录下,我们将SRILM的训练结果存储在data/lm/srilm下。yesno实验使用1-gram的语言模型的结果,储存到srilm.o1g.kn中,语言模型如下:

srilm.o1g.km

\data\
ngram 1=7

\1-grams:
-0.9542425	</s>
-99	<NOISE>
-99	<SPOKEN_NOISE>
-99	<UNK>
-99	<s>
-0.3079789	NO
-0.4014005	YES

\end\

使用n-gram作为语言模型时,习惯上用以上的arpa格式表示,以上[value] [word]的形式意义为logProbability(word)=value,画图如下:

G.fst

G.fst

TLG.fst

把以上生成的FST文件进行组合(compose),生成TLG.fst。

TLG.fst

local/yesno_decode_graph.sh data/lm/srilm/srilm.o1g.kn.gz data/lang data/lang_test || exit 1;

这部分代码中,我们先将语言模型根据word.txt打包到G.fst中,然后用openfst组合出TLG.fst,用于训练。

yesno_decode_graph.sh

#!/bin/bash 
#

if [ -f path.sh ]; then . path.sh; fi

#lm_dir=$1
arpa_lm=$1
src_lang=$2
tgt_lang=$3

#arpa_lm=${lm_dir}/3gram-mincount/lm_unpruned.gz
[ ! -f $arpa_lm ] && echo No such file $arpa_lm && exit 1;

rm -rf $tgt_lang
cp -r $src_lang $tgt_lang

# Compose the language model to FST
gunzip -c "$arpa_lm" | \
   grep -v '<s> <s>' | \
   grep -v '</s> <s>' | \
   grep -v '</s> </s>' | \
   arpa2fst - | fstprint | \
   utils/remove_oovs.pl /dev/null | \
   utils/eps2disambig.pl | utils/s2eps.pl | fstcompile --isymbols=$tgt_lang/words.txt \
     --osymbols=$tgt_lang/words.txt  --keep_isymbols=false --keep_osymbols=false | \
    fstrmepsilon | fstarcsort --sort_type=ilabel > $tgt_lang/G.fst


echo  "Checking how stochastic G is (the first of these numbers should be small):"
fstisstochastic $tgt_lang/G.fst 

# Compose the token, lexicon and language-model FST into the final decoding graph
fsttablecompose $tgt_lang/L.fst $tgt_lang/G.fst | fstdeterminizestar --use-log=true | \
    fstminimizeencoded | fstarcsort --sort_type=ilabel > $tgt_lang/LG.fst || exit 1;
fsttablecompose $tgt_lang/T.fst $tgt_lang/LG.fst > $tgt_lang/TLG.fst || exit 1;

echo "Composing decoding graph TLG.fst succeeded"
rm -r $tgt_lang/LG.fst   # We don't need to keep this intermediate FST

到此,我们完成了数据文件的准备以及TLG.fst的生成,具体流程如下:

TLG.fst

现在你的data目录结构应该如下:

├── dev
│   ├── spk2utt
│   ├── text
│   ├── utt2spk
│   └── wav.scp
├── test
│   ...
├── train
│   ...
├── dict
│   ├── lexicon_numbers.txt
│   ├── lexicon_raw.txt
│   ├── lexicon.txt
│   ├── units_raw.txt
│   └── units.txt
├── lang
│   ├── lexicon_numbers.txt
│   ├── L.fst
│   ├── T.fst
│   ├── tokens.txt
│   ├── units.txt
│   └── words.txt
├── lang_test
│   ├── G.fst
│   ├── lexicon_numbers.txt
│   ├── L.fst
│   ├── T.fst
│   ├── TLG.fst
│   ├── tokens.txt
│   ├── units.txt
│   └── words.txt
├── lm
│   ├── srilm
│   ├── text.no_oov
│   ├── train.gz
│   ├── unigram.counts
│   ├── word.counts
│   └── word_map
├── local
│   ├── dev.txt
│   ├── dev_wav.scp
│   ├── lang_phn_tmp
│   ├── test.txt
│   ├── test_wav.scp
│   ├── train.txt
│   ├── train_wav.scp
│   ├── waves_all.list
│   ├── waves.dev
│   └── waves.train
└── waves_yesno

至此我们已经完成yesno项目搭建的90%,再次确认目录下每个文件代表内容。

关于词典文件的说明较为简略,希望进一步了解每一个文件的意义,请阅读Kaldi Data preparation文档。

2. 提取FBank特征

Step 2: Feature extraction

在第2步,我们提取波形文件的FBank特征(FBank是Filter Bank的缩写,指音频信号经过短时傅里叶变换,得到幅度谱,再经过一组滤波器组的输出),提取的FBank特征存放在fbank文件夹。

注意在conf目录下建立fbank.conf文件,内容为:

--sample-frequency=8000
--num-mel-bins=40

分别为音频采样率和滤波器个数,yesno数据集音频采样率为8k(8000),滤波器个数我们取40。

关于FBank:特征提取

if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then
  echo "stage 2: FBank Feature Generation"
  #perturb the speaking speed to achieve data augmentation
  utils/data/perturb_data_dir_speed_3way.sh data/train data/train_sp
  utils/data/perturb_data_dir_speed_3way.sh data/dev data/dev_sp
  
  # Generate the fbank features; by default 40-dimensional fbanks on each frame
  fbankdir=fbank
  for set in train_sp dev_sp; do
    steps/make_fbank.sh --cmd "$train_cmd" --nj 1 data/$set exp/make_fbank/$set $fbankdir || exit 1;
    utils/fix_data_dir.sh data/$set || exit;  #filter and sort the data files
    steps/compute_cmvn_stats.sh data/$set exp/make_fbank/$set $fbankdir || exit 1;  #achieve cmvn normalization
  done

  for set in test; do
    steps/make_fbank.sh --cmd "$train_cmd" --nj 1 data/$set exp/make_fbank/$set $fbankdir || exit 1;
    utils/fix_data_dir.sh data/$set || exit;  #filter and sort the data files
    steps/compute_cmvn_stats.sh data/$set exp/make_fbank/$set $fbankdir || exit 1;  #achieve cmvn normalization
  done
fi

在提取声音文件的特征时,此处使用了将声音进行0.9、1.0、1.1三种变速的操作,在一般识别任务中效果会更好。yesno项目我们不做此操作,此处使用该代码作为演示。相关脚本简要说明如下:

  • utils/data/perturb_data_dir_speed_3way.sh:变速脚本

  • steps/make_fbank.sh:fbank提取脚本

  • utils/fix_data_dir.sh:数据排序与过滤

  • steps/compute_cmvn_stats.sh:特征归一化,cmvn是指cepstra mean and variance normalization,即减去均值除以标准差的操作。早期语音识别中提取的音频特征是倒谱,故由此得名。在FBank特征的归一化处理,也沿用了该称呼。

通过这部分脚本的运行,会生成fbank目录内容如下:

├── fbank
│   ├── cmvn_dev_sp.ark 
│   ├── cmvn_dev_sp.scp
│   ├── cmvn_test.ark
│   ├── cmvn_test.scp
│   └── cmvn_train_sp.ark
│   ├── cmvn_train_sp.scp
│   ├── raw_fbank_dev_sp.1.ark
│   ├── raw_fbank_dev_sp.1.scp
│   ├── raw_fbank_test_sp.1.ark
│   └── raw_fbank_test_sp.1.scp
│   ├── raw_fbank_train_sp.1.ark
│   └── raw_fbank_train_sp.1.scp	

其中ark文件为FBank提取的特征向量;scp文件记录了音频文件或说话人与相应的ark文件对应的相对关系。

前缀cmvn格式类似:(egs:global /...path to file/cmvn_dev_sp.ark:7)

前缀raw为格式类似:(egs:0_1_1_1_1_1_1_1 /.....path to file/raw_fbank_dev_sp.1.ark:16)

3. 准备分母图语言模型

Step 3: Denominator LM preparation

在第3步,我们先得到训练集中每句话的标签(label)序列;可能用到的标签集(label inventory)之前已保存在units.txt中。然后,通过计算标签序列的语言模型并将其表示成den_lm.fst(分母图语言模型对应的FST文件,den是denominator的缩写)。最后,由den_lm.fst和标签文件出发,计算出标签序列$l$的对数概率 $logp(l)$,称为path weight。详细的步骤内容见注释。

data_tr=data/train_sp
data_cv=data/dev_sp

if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then
  #convert word sequences to label sequences according to lexicon_numbers.txt and text files in data/lang_phn
  #the result will be placed in $data_tr/ and $data_cv/
  ctc-crf/prep_ctc_trans.py data/lang/lexicon_numbers.txt $data_tr/text "<UNK>" > $data_tr/text_number
  ctc-crf/prep_ctc_trans.py data/lang/lexicon_numbers.txt $data_cv/text "<UNK>" > $data_cv/text_number
  echo "convert text_number finished"

  # prepare denominator
  ctc-crf/prep_ctc_trans.py data/lang/lexicon_numbers.txt data/train/text "<UNK>" > data/train/text_number
  #sort the text_number file, and then remove the duplicate lines
  cat data/train/text_number | sort -k 2 | uniq -f 1 > data/train/unique_text_number
  mkdir -p data/den_meta
  #generate phone_lm.fst, a phone-based language model
  chain-est-phone-lm ark:data/train/unique_text_number data/den_meta/phone_lm.fst
  #generate the correct T.fst, called T_den.fst
  ctc-crf/ctc_token_fst_corrected.py den data/lang/tokens.txt | fstcompile | fstarcsort --sort_type=olabel > data/den_meta/T_den.fst
  #compose T_den.fst and phone_lm.fst into den_lm.fst
  fstcompose data/den_meta/T_den.fst data/den_meta/phone_lm.fst > data/den_meta/den_lm.fst
  echo "prepare denominator finished"
  
  #calculate and save the weight for each label sequence based on text_number and phone_lm.fst
  path_weight $data_tr/text_number data/den_meta/phone_lm.fst > $data_tr/weight
  path_weight $data_cv/text_number data/den_meta/phone_lm.fst > $data_cv/weight
  echo "prepare weight finished"
fi

此处可能需要将ctc-crf/prep_ctc_trans.py文件中#!/usr/bin/env python一行改为#!/usr/bin/env python3,使文件能够使用python3执行。

4. 神经网络训练准备

Step 4: Neural network training preparation

不同语音识别项目中,这部分处理差别不大。我们对数据集的的特征进行归一化并和之前计算的path weights一起整合到data/pickle下。

if [ $stage -le 4 ] && [ $stop_stage -ge 4 ]; then
  mkdir -p data/all_ark
  
  for set in test; do
    eval data_$set=data/$set
  done

  for set in test cv tr; do
    tmp_data=`eval echo '$'data_$set`

    #apply CMVN feature normalization, calculate delta features, then sub-sample the input feature sequence
    feats="ark,s,cs:apply-cmvn --norm-vars=true --utt2spk=ark:$tmp_data/utt2spk scp:$tmp_data/cmvn.scp scp:$tmp_data/feats.scp ark:- \
    | add-deltas ark:- ark:- | subsample-feats --n=3 ark:- ark:- |"

    ark_dir=$(readlink -f data/all_ark)/$set.ark
    #copy feature files, generate scp and ark files to save features.
    copy-feats "$feats" "ark,scp:$ark_dir,data/all_ark/$set.scp" || exit 1
  done
fi

if [ $stage -le 5 ] && [ $stop_stage -ge 5 ]; then
  mkdir -p data/pickle
  #create a pickle file to save the feature, text_number and path weights.
  python3 ctc-crf/convert_to.py -f=pickle --describe='L//4' --filer=1500 \
      data/all_ark/cv.scp $data_cv/text_number $data_cv/weight data/pickle/cv.pickle || exit 1
  python3 ctc-crf/convert_to.py -f=pickle --describe='L//4' --filer=1500 \
      data/all_ark/tr.scp $data_tr/text_number $data_tr/weight data/pickle/tr.pickle || exit 1
fi

在stage 5结束后,用fi结束最开始if [ $NODE == 0 ]; then的大括号,进入到训练部分。

5. 模型训练

Step 5: Model training

此时模型训练需要的所有数据已经准备完成,剩下只需要在exp下创建你的一次实验的文件夹(demo),建立config.json。此处yesno实验可以将config.json进行修改多次实验:

{
    "net": {
        "type": "LSTM",
        "lossfn": "crf",
        "lamb": 0.01,
        "kwargs": {
            "n_layers": 3,
            "idim": 120,
            "hdim": 320,
            "num_classes": 5,
            "dropout": 0.5
        }
    },
    "scheduler": {
        "type": "SchedulerCosineAnnealing",
        "optimizer": {
            "type_optim": "Adam",
            "kwargs": {
                "lr": 1e-3,
                "betas": [
                    0.9,
                    0.99
                ],
                "weight_decay": 0.0
            }
        },
        "kwargs": {
            "lr_min": 1e-5,
            "period": 5,
            "epoch_max": 30,
            "reverse_metric_direc": true
        }
    }
}

参数设置说明

settings
lossfn 损失函数默认crf(ctc-crf)或ctc
lamb 稳定ctc loss的权重
n_layers 神经网络循环层数
idim 输入特征的维度,(Fbank中取40维+一阶差分+二阶差分=120维)
hdim 神经网络每个隐藏层中的单元数
num_classes 神经网络输出层数一般为音素集大小+1(#phone+1 for phone-based model).(#char+1 for char-based model)
dropout 防止过拟合(默认0.5)
optimizer 优化器(默认Adam)
lr 学习率大小

此处"type": "LSTM"表示我们采用LSTM模型,对使用其它模型时有关net参数的设置,具体参考ctc_crf/model.py。 scheduler参数设置学习调度器,具体参考ctc_crf/scheduler.py。 optimizer相关参数及设置参考:优化器相关说明

训练的代码如下:

PARENTDIR='.'
dir="exp/demo"
DATAPATH=$PARENTDIR/data/

if [ $stage -le 6 ] && [ $stop_stage -ge 6 ]; then

  if [ $change_config == 1 ]; then
    rm $dir/scripts.tar.gz
    rm -rf $dir/ckpt
  fi

  unset CUDA_VISIBLE_DEVICES

  if [[ $NODE == 0 && ! -f $dir/scripts.tar.gz ]]; then
    echo ""
    tar -zcf $dir/scripts.tar.gz $(readlink ctc-crf) $0
  elif [ $NODE == 0 ]; then
    echo ""
    echo "'$dir/scripts.tar.gz' already exists."
    echo "If you want to update it, please manually rm it then re-run this script."
  fi

  # uncomment the following line if you want to use specified GPUs
  CUDA_VISIBLE_DEVICES="0"                      \
  python3 ctc-crf/train.py --seed=0             \
    --world-size 1 --rank $NODE                 \
    --batch_size=3                              \
    --dir=$dir                                  \
    --config=$dir/config.json                   \
    --data=$DATAPATH                            \
    || exit 1
fi

通过以上代码即可完成模型训练。训练的过程图展示可以在你创建的demo目录下的monitor.jpg中找到。

如果需要重新训练,删除scripts.tar.gz和ckpt文件即可。

6. 解码

Step 6: Decoding

编写打分脚本local/score.sh

score.sh

#!/usr/bin/env bash

set -e -o pipefail
set -x
steps/score_kaldi.sh "$@"
steps/scoring/score_kaldi_cer.sh --stage 2 "$@"

echo "$0: Done"

计算测试集中每句话每帧的logits并解码。关于在解码过程中使用的latgen-faster函数,可以参照CAT-v2安装介绍中CAT安装的第三步,将补丁文件latgen-faster.cc打包到Kaldi中,修改Makefile文件后进行编译来解决。

nj=1
if [ $stage -le 7 ] && [ $stop_stage -ge 7 ]; then
  for set in test; do
    ark_dir=$dir/logits/$set
    mkdir -p $ark_dir
    python3 ctc-crf/calculate_logits.py               \
      --resume=$dir/ckpt/bestckpt.pt                     \
      --config=$dir/config.json                       \
      --nj=$nj --input_scp=data/all_ark/$set.scp      \
      --output_dir=$ark_dir                           \
      || exit 1
  done
fi


if [ $stage -le 8 ] && [ $stop_stage -ge 8 ]; then
  for set in test; do
    mkdir -p $dir/decode_${set}
    ln -s $(readlink -f $dir/logits/$set) $dir/decode_${set}/logits
    ctc-crf/decode.sh --stage 1 \
        --cmd "$decode_cmd" --nj 1 --acwt 1.0 --post_decode_acwt 1.0\
        data/lang_${set} data/${set} data/all_ark/${set}.scp $dir/decode_${set}
  done
fi

if [ $stage -le 9 ] && [ $stop_stage -ge 9 ]; then
  for set in test; do
    grep WER $dir/decode_${set}/wer_* | utils/best_wer.sh
  done
fi

恭喜你已经完成了你的第一个语音识别项目(yesno)的搭建——训练及解码过程。

现在你的目录结构应该如下图所示:

├── cmd.sh
├── conf
│   ├── decode_dnn.config
│   ├── fbank.conf
│   └── mfcc.conf
├── ctc-crf -> ../../scripts/ctc-crf
├── exp
│   └── demo
├── input
│   └── lexicon.txt
├── local
│   ├── create_yesno_txt.pl
│   ├── create_yesno_waves_test_train.pl
│   ├── create_yesno_wav_scp.pl
│   ├── get_word_map.pl
│   ├── prepare_data.sh
│   ├── prepare_dict.sh
│   ├── score.sh
│   ├── yesno_decode_graph.sh
│   └── yesno_train_lms.sh
├── path.sh
├── run.sh
├── steps -> /myhome/kaldi/egs/wsj/s5/steps
└── utils -> /myhome/kaldi/egs/wsj/s5/utils

7. 实验展示

以下是一次默认训练结果展示:

monitor

识别结果如下:

%WER 5.83 [ 14 / 240, 1 ins, 13 del, 0 sub ] 

识别的详细log在exp/demo/decode_test中。

在训练完成后,请在demo文件夹下自动生成的readme.md文件中对你的这次实验进行记录。

8. 结果分析

我们利用waves_yesno提供的60条语音数据,按照5:5将数据分为训练集和验证集。

使用不同的超参设置,进行分别的实验(训练及测试),并记录每一次的实验结果记录如下:

Model Loss_fune N-gram featrue_size hidm/layers Scheduler Batch_size lr epoch_max min_loss WER
BLSTM CTC 1-gram 120 320/3 CosineAnnealing 4 0.001 30 -7.71 8.79
BLSTM CTC 3-gram 120 320/3 CosineAnnealing 4 0.001 30 1.91 20.83
BLSTM CTC 1-gram 120 320/3 EarlyStop 4 0.001 30 4.45 17.50
BLSTM CTC 3-gram 120 320/3 EarlyStop 4 0.001 30 8.15 33.25
VGG-BLSYM CTC-CRF 1-gram 120 320/3 CosineAnnealing 4 0.001 30 -13.06 2.92
VGG-BLSTM CTC-CRF 3-gram 120 320/3 CosineAnnealing 4 0.001 30 -17.77 12.92
VGG-BLSTM CTC-CRF 1-gram 120 320/3 EarlyStop 4 0.001 12 -16.54 1.25
vGG-BLSTM CTC-CRF 3-gram 120 320/3 EarlyStop 4 0.001 30 -10.89 18.75

从以上实验结果可以看出CAT(ctc-crf)明显优于ctc。注意到,由于yesno数据的特点,1-gram语言模型比多阶语言模型效果更好。

读者可以尝试修改exp/demo/config.json中参数尝试多次实验。

🐱‍🏍