Wenet脚本 LM

语言模型

LM for WeNet

img
  1. 没有LM,用CTC prefix beam search来产生N-best
  2. 有LM,用CTC WFST search来产生N-best,其中CTC WFST search是传统的基于WFST的decoder

第一步是构建解码图,用建模单元T,词典L,语言模型G组合compose到统一一个解码图TLG里,其中T是端到端训练的建模单元,中文里是char,英文是char或BPE;L是词典,就是把词组分开成建模单元,比如把word“我们”分开成两个chars“我 们”,word“APPLE”分开成五个字母letters“A P P L E”,没有音素,词典不用特殊设计;G是语言模型,就是将n-gram编译成标准的WFST表示;

第二步是解码,和传统方法一样,用Viterbi搜索算法;

WeNet利用Kaldi中的解码器和相关工具来支持基于LM和WFST的解码。为了便于使用和保持独立性,我们在WeNet运行时直接将Kaldi中与解码相关的代码迁移到此目录。并按以下原则进行修改和组织:

  1. 为了最小化更改,迁移后的代码保持与原始代码相同的目录结构。
  2. 我们使用GLOG代替Kaldi的日志系统。
1
2
3
4
5
6
7
8
9
#define KALDI_WARN \
google::LogMessage(__FILE__, __LINE__, google::GLOG_WARNING).stream()
#define KALDI_ERR \
google::LogMessage(__FILE__, __LINE__, google::GLOG_ERROR).stream()
#define KALDI_INFO \
google::LogMessage(__FILE__, __LINE__, google::GLOG_INFO).stream()
#define KALDI_VLOG(v) VLOG(v)

#define KALDI_ASSERT(condition) CHECK(condition)
  1. 我们修改了代码格式,以满足WeNet代码风格的lint要求。

核心代码是 https://github.com/wenet-e2e/wenet/blob/main/runtime/core/decoder/ctc_wfst_beam_search.cc , 它在Kaldi中包装wrap了LatticeFasterDecoder。我们使用空白帧跳转来加速解码。

此外,WeNet还迁移了构建解码图的相关工具,如arpa2fst、fstdeterminizestar、fsttablecompose、fstminimizeencoded等工具。因此,所有与LM相关的工具都是内置工具,可以开箱即用。

我们在不同的数据集上获得了一致的增益(3%~10%),包括aishell、aishell2和librispeech

词典的构造:

tools/fst/prepare_dict.py

之前训练时,按字建模,没有词组,因此字一共有4232个字(训练集)(data/dict/lexicon.txt),加eos/sos是4233个

现在:把aishell里的一个自带的词典corpus/aishell/resource_aishell/lexicon.txt(13万个词组),通过data/dict/lexicon.txt进行过滤,只保留训练集有的字的词组,

得到的词典用字建模,组成词组,12万词的词典(data/local/dict/lexicon.txt);

训练LM:

local/aishell_train_lms.sh

用训练集训练LM:

从corpus/data_aishell/transcript/aishell_transcript_v0.8.txt(所有train/dev/test音频对应的文本,进行了分词)过滤出训练集对应的12万条分词文本(data/local/lm/text)

用分词过的12万条训练集文本、12万词的词典 训练语言模型,得到 16M的 3-gram的lm.arpa

file data/local/lm/heldout: 10000 sentences, 89496 words, 0 OOVs
0 zeroprobs, logprob= -272791.2 ppl= 551.7352 ppl1= 1117.077

可以看出,ppl=500多,还不是特别好(300以内才好)

构建解码中使用的TLG

tools/fst/compile_lexicon_token_fst.sh

tools/fst/make_tlg.sh

TLG.fst = T compose(determini(L compose G))

TLG参与推理解码

./tools/decode.sh

1
2
3
4
5
6
./tools/decode.sh --nj 16 \
--beam 15.0 --lattice_beam 7.5 --max_active 7000 \
--blank_skip_thresh 0.98 --ctc_weight 0.5 --rescoring_weight 1.0 \
--fst_path data/lang_test/TLG.fst \
data/test/wav.scp data/test/text $dir/final.zip \
data/lang_test/words.txt $dir/lm_with_runtime

./tools/decode.sh 具体为:

1
2
3
4
5
6
7
8
9
10
11
decoder_main \
--rescoring_weight $rescoring_weight \
--ctc_weight $ctc_weight \
--reverse_weight $reverse_weight \
--chunk_size $chunk_size \
--wav_scp ${dir}/split${nj}/wav.${n}.scp \
--model_path $model_file \
--dict_path $dict_file \
$wfst_decode_opts \
--result ${dir}/split${nj}/${n}.text &> ${dir}/split${nj}/${n}.log

要到runtime/server/x86 进行编译:mkdir build && cd build && cmake .. && cmake --build . [用docker的话,这步就不需要]

cmake版本要3.14 ,我没有编译成功(3.12),错误报告见:/home/data/yelong/newest_wenet/wenet/runtime/server/x86/build/CMakeFiles/CMakeOutput.log

因此决定直接用 docker

进入到container中:docker exec -it 5627fbb7b503 bash

进入到/home/wenet/runtime/server/x86

用c进行解码

使用docker启动语音识别服务

先要拉去最新的image

1
2
3
4
5
cd docker
docker build --no-cache -t wenet:latest .
# 或者:
docker run -it -p 10086:10086 -v $model_dir:/home/wenet/model wenetorg/wenet-mini:latest bash /home/run.sh
# 这里10086:10086是服务器端口,映射到容器的端口,内部端口:外部端口

最简单的使用 Wenet 的方式是通过官方提供的 docker 镜像 wenetorg/wenet:mini 来启动服务。

(镜像默认都是放在https://hub.docker.com/里,然后虽然没有这个前缀,但是会去这上面找镜像,镜像名叫wenetorg/wenet-mini)

下面的命令先下载官方提供的预训练模型,并启动 docker 服务,加载模型,提供 websocket 协议的语音识别服务。

1
2
3
4
5
6
7
cd wenet/runtime/server/x86
wget https://wenet-1256283475.cos.ap-shanghai.myqcloud.com/models/aishell/20210601_u2%2B%2B_conformer_libtorch.tar.gz
tar -xf 20210602_u2++_conformer_libtorch.tar.gz
model_dir=$PWD/20210602_u2++_conformer_libtorch
docker run --rm -it -p 10086:10086 -v $model_dir:/home/wenet/model wenetorg/wenet-mini:latest bash /home/run.sh
# 这里起一次就生成一个容器了,之后只要用:
docker exec -it .... bash # 再次进入就好

$model_dir 是模型在本机的目录,将被映射到容器的 /home/wenet/model 目录,然后启动 web 服务。

用网页的实时识别

上面的docker容器中运行 bash /home/run.sh

/home/run.sh内容:

1
2
3
4
5
6
7
#!/bin/bash
export LD_LIBRARY_PATH=/home/wenet/lib
export GLOG_logtostderr=1
export GLOG_v=2

model=/home/wenet/model
/home/wenet/websocket_server_main --port 10086 --chunk_size 16 --model_path $model/final.zip --dict_path $model/words.txt 2>&1 | tee server.log

然后下载github的wenet整个项目到windows本地,使用浏览器打开文件runtime/server/x86/web/templates/index.html,在 WebSocket URL:填入 服务器的地址,端口号,比如ws://10.22.23.14:10086

就可以了!

经我测试,识别非常慢,识别不准确。。??

在 Docker 环境中使用

如果遇到问题比如无法编译,我们提供了 docker 镜像用于直接执行示例。需要先安装好 docker,运行如下命令,进入 docker 容器环境。

1
docker run -it mobvoiwenet/wenet:latest bash

该镜像包含了编译过程中所依赖的所有第三方库、编译好的文件和预训练模型。

预训练模型在 /home/model 目录, 可执行程序在 /home/wenet/runtime/server/x86/build 目录。

有了这个镜像后,平时需要的话,可以新建容器:

1
docker run -it mobvoiwenet/wenet:latest bash

如果修改了cc文件,要执行 mkdir build && cd build && cmake .. && cmake --build .进行编译

自行编译运行时程序

==该方式不推荐,因为本地编译麻烦,cmake升级版本还和gcc版本有关==

如果想使用非 docker 方式,需要自行编译。Wenet 支持 linux/macos/windows 三种平台上的编译。需要安装 cmake 3.14 或者更高版本。

运行如下命令,完成编译。

1
2
3
# 当前目录为 wenet/runtime/server/x86
mkdir build && cd build && cmake .. && cmake --build .

或者使用命令编译以支持 gRPC。

1
mkdir build && cd build && cmake -DGRPC=ON .. && cmake --build .

编译好的可执行程序在 wenet/runtime/server/x86/build/ 下:

  • decoder_main 本地文件识别工具
  • websocket_server_main 基于websocket协议的识别服务端
  • websocket_client_main 基于websocket协议的识别客户端

下载预训练模型

1
2
3
# 当前目录为 wenet/runtime/server/x86
wget https://wenet-1256283475.cos.ap-shanghai.myqcloud.com/models/aishell/20210601_u2%2B%2B_conformer_libtorch.tar.gz
tar -xf 20210602_u2++_conformer_libtorch.tar.gz

以上不推荐,因为本地编译麻烦,cmake升级版本还和gcc版本有关


本地wav文件识别

本地文件识别,即程序每次运行时,给定一个语音文件或者一组语音文件列表,输出识别结果,然后结束程序。

下载好模型后,执行如下的命令进行本地wav文件识别,将 wav_path 设为你想测试的 wav 文件地址,将 model_dir 设为你的模型目录地址。

进入到镜像为mobvoiwenet/wenet:latest的容器里

cd /home/wenet/runtime/server/x86

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 当前目录为 wenet/runtime/server/x86
# 已经下载并解压20210602_unified_transformer_server.tar.gz到当前目录
# 准备好一个16k采样率,单通道,16bits的音频文件test.wav

export GLOG_logtostderr=1
export GLOG_v=2
# 如果要显示多一点LOG,比如RTF信息,要设置为:
export GLOG_logtostderr=
export GLOG_v=

wav_path=test.wav
model_dir=./20210602_unified_transformer_server
./build/decoder_main \
--chunk_size -1 \
--wav_path $wav_path \
--model_path $model_dir/final.zip \
--dict_path $model_dir/words.txt 2>&1 | tee log.txt

./build/decoder_main --chunk_size -1 --wav_path $wav_path --model_path $model_dir/final.zip --dict_path $model_dir/words.txt 2>&1 | tee log.txt
#或者
./build/decoder_main --chunk_size -1 --wav_scp ./wav_scp --model_path $model_dir/final.zip --dict_path $model_dir/words.txt

decoder_main工具支持两种wav文件模式:

  • 使用--wav_path指定单个文件,一次识别单个wav文件。
  • 使用--wav_scp指定一个.scp格式的wav列表,一次识别多个wav文件。

执行 ./build/decoder_main --help 可以了解更多的参数意义。

1
2
3
docker run -d -v /home/data/yelong/newest_wenet/wenet/runtime/server/x86/docker/docker_resource:/home/data/yelong/newest_wenet/wenet/runtime/server/x86/docker_resource -it wenet bash

docker exec -it 276e2d438bf6 bash

测试不同解码方法的rtf

测试集、模型在:10.22.24.3:/home/data/to_yelong/rtf-test

在空机器10.22.23.14,cpu,单线程测试

1
2
3
4
5
6
7
8
9
10
grep Decoded exp/hua/xueyuan_0.0_1.0/split1/1.log | awk '{print$6}' | sed s'/ms//g' | awk '{sum+=$1} END {print sum}'
grep Decoded exp/hua/xueyuan_0.0_1.0/split1/1.log | awk '{print$9}' | sed s'/ms.//g' | awk '{sum+=$1} END {print sum}'

grep Decoded exp/140-models/xueyuan_0.0_1.0/split1/1.log | awk '{print$6}' | sed s'/ms//g' | awk '{sum+=$1} END {print sum}'
grep Decoded exp/140-models/xueyuan_0.0_1.0/split1/1.log | awk '{print$9}' | sed s'/ms.//g' | awk '{sum+=$1} END {print sum}'

grep Decoded exp/140-models/xueyuan_0.0_0.0/split1/1.log | awk '{print$6}' | sed s'/ms//g' | awk '{sum+=$1} END {print sum}'
grep Decoded exp/140-models/xueyuan_0.0_0.0/split1/1.log | awk '{print$9}' | sed s'/ms.//g' | awk '{sum+=$1} END {print sum}'


decoder_main 单线程测试,n-best=10,无LM,解码方式CTC prefix beam search (+rescore):

  • 37.pt(195M模型,大概50M参数):rtf=0.08475(有rescore)

  • 4.pt(500M模型,140M参数):rtf=0.2759(有rescore);rtf=0.1377(无rescore)