本文將介紹如何基于新一代 Kaldi 框架快速搭建一個服務端的 ASR 系統(tǒng),包括數(shù)據準備、模型訓練測試、服務端部署運行。
更多內容建議參考:
前言
距離新一代 Kaldi 開源框架的正式發(fā)布已經有一段時間了。截至目前,框架基本的四梁八柱都已經立起來了。那么,如何用它快速搭建一個 ASR 系統(tǒng)呢?
閱讀過前面幾期公眾文的讀者可能都知道新一代 Kaldi 框架主要包含了四個不同的子項目:k2、icefall、lhotse、sherpa。其中,k2 是核心算法庫;icefall 是數(shù)據集訓練測試示例腳本;lhotse 是語音數(shù)據處理工具集;sherpa 是服務端框架,四個子項目共同構成了新一代 Kaldi 框架。
另一方面,截至目前,新一代 Kaldi 框架在很多公開數(shù)據集上都獲得了很有競爭力的識別結果,在 WenetSpeech 和 GigaSpeech 上甚至都獲得了 SOTA 的性能。
看到這,相信很多小伙伴都已經摩拳擦掌、躍躍欲試了。那么本文的目標就是試圖貫通新一代 Kaldi 的四個子項目,為快速搭建一個服務端的 ASR 系統(tǒng)提供一個簡易的教程。希望看完本文的小伙伴都能順利搭建出自己的 ASR 系統(tǒng)。
三步搭建 ASR 服務端系統(tǒng)
本文主要介紹如何從原始數(shù)據下載處理、模型訓練測試、到得到一個服務端 ASR 系統(tǒng)的過程,根據功能,分為三步:
本文介紹的 ASR 系統(tǒng)是基于 RNN-T 框架且不涉及外加的語言模型。所以,本文將不涉及 WFST 等語言模型的內容,如后期有需要,會在后面的文章中另行講述。
為了更加形象、具體地描述這個過程,本文以構建一個基于 WenetSpeech 數(shù)據集訓練的 pruned transducer stateless2[5] recipe 為例,希望盡可能為讀者詳細地描述這一過程,也希望讀者在本文的基礎上能夠無障礙地遷移到其他數(shù)據集的處理、訓練和部署使用上去。
本文描述的過程和展示的代碼更多的是為了描述功能,而非詳細的實現(xiàn)過程。詳細的實現(xiàn)代碼請讀者自行參考 egs/wenetspeech/ASR[6]。
Note: 使用者應該事先安裝好 k2、icefall、lhotse、sherpa。
第一步:數(shù)據準備和處理
對于數(shù)據準備和處理部分,所有的運行指令都集成在文件 prepare.sh[7] 中,主要的作用可以總結為兩個:準備音頻文件并進行特征提取、構建語言建模文件。
準備音頻文件并進行特征提取
(注:在這里我們也用了 musan 數(shù)據集對訓練數(shù)據進行增廣,具體的可以參考 prepare.sh[8] 中對 musan 處理和使用的相關指令,這里不針對介紹。)
下載并解壓數(shù)據
為了統(tǒng)一文件名,這里將數(shù)據包文件名變?yōu)?WenetSpeech, 其中 audio 包含了所有訓練和測試的音頻數(shù)據
>> tree download/WenetSpeech -L 1
download/WenetSpeech
├── audio
├── TERMS_OF_ACCESS
└── WenetSpeech.json
>> tree download/WenetSpeech/audio -L 1
download/WenetSpeech/audio
├── dev
├── test_meeting
├── test_net
└── train
WenetSpeech.json 中包含了音頻文件路徑和相關的監(jiān)督信息,我們可以查看 WenetSpeech.json 文件,部分信息如下所示:
'audios': [
{
'aid': 'Y0000000000_--5llN02F84',
'duration': 2494.57,
'md5': '48af998ec7dab6964386c3522386fa4b',
'path': 'audio/train/youtube/B00000/Y0000000000_--5llN02F84.opus',
'source': 'youtube',
'tags': [
'drama'
],
'url': 'https://www./watch?v=--5llN02F84',
'segments': [
{
'sid': 'Y0000000000_--5llN02F84_S00000',
'confidence': 1.0,
'begin_time': 20.08,
'end_time': 24.4,
'subsets': [
'L'
],
'text': '怎么樣這些日子住得還習慣吧'
},
{
'sid': 'Y0000000000_--5llN02F84_S00002',
'confidence': 1.0,
'begin_time': 25.0,
'end_time': 26.28,
'subsets': [
'L'
],
'text': '挺好的'
(注:WenetSpeech 中文數(shù)據集中包含了 S,M,L 三個不同規(guī)模的訓練數(shù)據集)
利用 lhotse 生成 manifests
關于 lhotse 是如何將原始數(shù)據處理成 jsonl.gz 格式文件的,這里可以參考文件wenet_speech.py[9], 其主要功能是生成 recordings 和 supervisions 的 jsonl.gz 格式文件
>> lhotse prepare wenet-speech download/WenetSpeech data/manifests -j 15
>> tree data/manifests -L 1
├── wenetspeech_recordings_DEV.jsonl.gz
├── wenetspeech_recordings_L.jsonl.gz
├── wenetspeech_recordings_M.jsonl.gz
├── wenetspeech_recordings_S.jsonl.gz
├── wenetspeech_recordings_TEST_MEETING.jsonl.gz
├── wenetspeech_recordings_TEST_NET.jsonl.gz
├── wenetspeech_supervisions_DEV.jsonl.gz
├── wenetspeech_supervisions_L.jsonl.gz
├── wenetspeech_supervisions_M.jsonl.gz
├── wenetspeech_supervisions_S.jsonl.gz
├── wenetspeech_supervisions_TEST_MEETING.jsonl.gz
└── wenetspeech_supervisions_TEST_NET.jsonl.gz
這里,可用 vim 對 recordings 和 supervisions 的 jsonl.gz 文件進行查看, 其中:
wenetspeech_recordings_S.jsonl.gz:
wenetspeech_supervisions_S.jsonl.gz:

由上面兩幅圖可知,recordings 用于描述音頻文件信息,包含了音頻樣本的 id、具體路徑、通道、采樣率、子樣本數(shù)和時長等。supervisions 用于記錄監(jiān)督信息,包含了音頻樣本對應的 id、起始時間、時長、通道、文本和語言類型等。
接下來,我們將對音頻數(shù)據提取特征。
計算、提取和貯存音頻特征
首先,對數(shù)據進行預處理,包括對文本進行標準化和對音頻進行時域上的增廣,可參考文件 preprocess_wenetspeech.py[10]。
python3 ./local/preprocess_wenetspeech.py
其次,將數(shù)據集切片并對每個切片數(shù)據集進行特征提取。可參考文件 compute_fbank_wenetspeech_splits.py[11]。
(注:這里的切片是為了可以開啟多個進程同時對大規(guī)模數(shù)據集進行特征提取,提高效率。如果數(shù)據集比較小,對數(shù)據進行切片處理不是必須的。)
# 這里的 L 也可修改為 M 或 S, 表示訓練數(shù)據子集
lhotse split 1000 ./data/fbank/cuts_L_raw.jsonl.gz data/fbank/L_split_1000
python3 ./local/compute_fbank_wenetspeech_splits.py \
--training-subset L \
--num-workers 20 \
--batch-duration 600 \
--start 0 \
--num-splits 1000
最后,待提取完每個切片數(shù)據集的特征后,將所有切片數(shù)據集的特征數(shù)據合并成一個總的特征數(shù)據集:
# 這里的 L 也可修改為 M 或 S, 表示訓練數(shù)據子集
pieces=$(find data/fbank/L_split_1000 -name 'cuts_L.*.jsonl.gz')
lhotse combine $pieces data/fbank/cuts_L.jsonl.gz
至此,我們基本完成了音頻文件的準備和特征提取。接下來,我們將構建語言建模文件。
構建語言建模文件
在 RNN-T 模型框架中,我們實際需要的用于訓練和測試的建模文件有 tokens.txt、words.txt 和 Linv.pt 。我們按照如下步驟構建語言建模文件:
規(guī)范化文本并生成 text
在這一步驟中,規(guī)范文本的函數(shù)文件可參考 text2token.py[12]。
# Note: in Linux, you can install jq with the following command:
# 1. wget -O jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64
# 2. chmod +x ./jq
# 3. cp jq /usr/bin
gunzip -c data/manifests/wenetspeech_supervisions_L.jsonl.gz \
| jq 'text' | sed 's/'//g' \
| ./local/text2token.py -t 'char' > data/lang_char/text
text 的形式如下:
怎么樣這些日子住得還習慣吧
挺好的
對了美靜這段日子經常不和我們一起用餐
是不是對我回來有什么想法啊
哪有的事啊
她這兩天挺累的身體也不太舒服
我讓她多睡一會那就好如果要是覺得不方便
我就搬出去住
............
分詞并生成 words.txt
這里我們用 jieba 對中文句子進行分詞,可參考文件 text2segments.py[13] 。
python3 ./local/text2segments.py \
--input-file data/lang_char/text \
--output-file data/lang_char/text_words_segmentation
cat data/lang_char/text_words_segmentation | sed 's/ /\n/g' \
| sort -u | sed '/^$/d' | uniq > data/lang_char/words_no_ids.txt
python3 ./local/prepare_words.py \
--input-file data/lang_char/words_no_ids.txt \
--output-file data/lang_char/words.txt
text_words_segmentation 的形式如下:
怎么樣 這些 日子 住 得 還 習慣 吧
挺 好 的
對 了 美靜 這段 日子 經常 不 和 我們 一起 用餐
是不是 對 我 回來 有 什么 想法 啊
哪有 的 事 啊
她 這 兩天 挺累 的 身體 也 不 太 舒服
我 讓 她 多 睡 一會 那就好 如果 要是 覺得 不 方便
我 就 搬出去 住
............
words_no_ids.txt 的形式如下:
............
阿
阿Q
阿阿虎
阿阿離
阿阿瑪
阿阿毛
阿阿強
阿阿淑
阿安
............
words.txt 的形式如下:
............
阿 225
阿Q 226
阿阿虎 227
阿阿離 228
阿阿瑪 229
阿阿毛 230
阿阿強 231
阿阿淑 232
阿安 233
............
生成 tokens.txt 和 lexicon.txt
這里生成 tokens.txt 和 lexicon.txt 的函數(shù)文件可參考 prepare_char.py[14] 。
python3 ./local/prepare_char.py \
--lang-dir data/lang_char
tokens.txt 的形式如下:
<blk> 0
<sos/eos> 1
<unk> 2
怎 3
么 4
樣 5
這 6
些 7
日 8
子 9
............
lexicon.txt 的形式如下:
............
X光 X 光
X光線 X 光 線
X射線 X 射 線
Y Y
YC Y C
YS Y S
YY Y Y
Z Z
ZO Z O
ZSU Z S U
○ ○
一 一
一一 一 一
一一二 一 一 二
一一例 一 一 例
............
至此,第一步全部完成。對于不同數(shù)據集來說,其基本思路也是類似的。在數(shù)據準備和處理階段,我們主要做兩件事情:準備音頻文件并進行特征提取、構建語言建模文件。
這里我們使用的范例是中文漢語,建模單元是字。在英文數(shù)據中,我們一般用 BPE 作為建模單元,具體的可參考 egs/librispeech/ASR/prepare.sh[15] 。
第二步:模型訓練和測試
在完成第一步的基礎上,我們可以進入到第二步,即模型的訓練和測試了。這里,我們根據操作流程和功能,將第二步劃分為更加具體的幾步:文件準備、數(shù)據加載、模型訓練、解碼測試。
文件準備
首先,創(chuàng)建 pruned_transducer_stateless2 的文件夾。
mkdir pruned_transducer_stateless2
cd pruned_transducer_stateless2
其次,我們需要準備數(shù)據讀取、模型、訓練、測試、模型導出等腳本文件。在這里,我們在 egs/librispeech/ASR/pruned_transducer_stateless2[16] 的基礎上創(chuàng)建我們需要的文件。
對于公共的腳本文件(即不需要修改的文件),我們可以用軟鏈接直接復制過來,如:
ln -s ../../../librispeech/ASR/pruned_transducer_stateless2/conformer.py .
其他相同文件的操作類似。另外,讀者也可以使用自己的模型,替換本框架內提供的模型文件即可。
對于不同的腳本文件(即因為數(shù)據集或者語言不同而需要修改的文件),我們先從 egs/librispeech/ASR/pruned_transducer_stateless2 中復制過來,然后再進行小范圍的修改,如:
cp -r ../../../librispeech/ASR/pruned_transducer_stateless2/train.py .
在本示例中,我們需要對 train.py 中的數(shù)據讀取、graph_compiler(圖編譯器)及
vocab_size 的獲取等部分進行修改,如(截取部分代碼,便于讀者直觀認識):
數(shù)據讀?。?/p>
............
from asr_datamodule import WenetSpeechAsrDataModule
............
wenetspeech = WenetSpeechAsrDataModule(args)
train_cuts = wenetspeech.train_cuts()
valid_cuts = wenetspeech.valid_cuts()
............
graph_compiler:
............
y = graph_compiler.texts_to_ids(texts)
if type(y) == list:
y = k2.RaggedTensor(y).to(device)
else:
y = y.to(device)
............
lexicon = Lexicon(params.lang_dir)
graph_compiler = CharCtcTrainingGraphCompiler(
lexicon=lexicon,
device=device,
)
............
vocab_size 的獲取:
............
params.blank_id = lexicon.token_table['<blk>']
params.vocab_size = max(lexicon.tokens) + 1
............
更加詳細的修改后的 train.py 可參考 egs/wenetspeech/ASR/pruned_transducer_stateless2/train.py[17] 。其他 decode.py、pretrained.py、export.py 等需要修改的文件也可以參照上述進行類似的修改和調整。
(注:在準備文件時,應該遵循相同的文件不重復造輪子、不同的文件盡量小改、缺少的文件自己造的原則。icefall 中大多數(shù)函數(shù)和功能文件在很多數(shù)據集上都進行了測試和驗證,都是可以直接遷移使用的。)
數(shù)據加載
實際上,對于數(shù)據加載這一步,也可以視為文件準備的一部分,即修改文件 asr_datamodule.py[18],但是考慮到不同數(shù)據集的 asr_datamodule.py 都不一樣,所以這里單獨拿出來講述。
首先,這里以 egs/librispeech/ASR/pruned_transducer_stateless2/asr_datamodule.py[19] 為基礎,在這個上面進行修改:
cp -r ../../../librispeech/ASR/pruned_transducer_stateless2/asr_datamodule.py .
其次,修改函數(shù)類的名稱,如這里將 LibriSpeechAsrDataModule 修改為 WenetSpeechAsrDataModule ,并讀取第一步中生成的 jsonl.gz 格式的訓練測試文件。本示例中,第一步生成了 data/fbank/cuts_L.jsonl.gz,我們用 load_manifest_lazy 讀取它:
............
group.add_argument(
'--training-subset',
type=str,
default='L',
help='The training subset for using',
)
............
@lru_cache()
def train_cuts(self) -> CutSet:
logging.info('About to get train cuts')
cuts_train = load_manifest_lazy(
self.args.manifest_dir
/ f'cuts_{self.args.training_subset}.jsonl.gz'
)
return cuts_train
............
其他的訓練測試集的 jsonl.gz 文件讀取和上述類似。另外,對于 train_dataloaders、valid_dataloaders 和 test_dataloaders 等幾個函數(shù)基本是不需要修改的,如有需要,調整其中的具體參數(shù)即可。
最后,調整修改后的 asr_datamodule.py 和 train.py 聯(lián)合調試,把 WenetSpeechAsrDataModule 導入到 train.py,運行它,如果在數(shù)據讀取和加載過程中不報錯,那么數(shù)據加載部分就完成了。
另外,在數(shù)據加載的過程中,我們也有必要對數(shù)據樣本的時長進行統(tǒng)計,并過濾一些過短、過長且占比極小的樣本,這樣可以使我們的訓練過程更加穩(wěn)定。
在本示例中,我們對 WenetSpeech 的樣本進行了時長統(tǒng)計(L 數(shù)據集太大,這里沒有對它進行統(tǒng)計),具體的可參考 display_manifest_statistics.py[20],統(tǒng)計的部分結果如下:
............
Starting display the statistics for ./data/fbank/cuts_M.jsonl.gz
Cuts count: 4543341
Total duration (hours): 3021.1
Speech duration (hours): 3021.1 (100.0%)
***
Duration statistics (seconds):
mean 2.4
std 1.6
min 0.2
25% 1.4
50% 2.0
75% 2.9
99% 8.0
99.5% 8.8
99.9% 12.1
max 405.1
............
Starting display the statistics for ./data/fbank/cuts_TEST_NET.jsonl.gz
Cuts count: 24774
Total duration (hours): 23.1
Speech duration (hours): 23.1 (100.0%)
***
Duration statistics (seconds):
mean 3.4
std 2.6
min 0.1
25% 1.4
50% 2.4
75% 4.8
99% 13.1
99.5% 14.5
99.9% 18.5
max 33.3
根據上面的統(tǒng)計結果,我們在 train.py 中設置了樣本的最大時長為 15.0 seconds:
............
def remove_short_and_long_utt(c: Cut):
# Keep only utterances with duration between 1 second and 15.0 seconds
#
# Caution: There is a reason to select 15.0 here. Please see
# ../local/display_manifest_statistics.py
#
# You should use ../local/display_manifest_statistics.py to get
# an utterance duration distribution for your dataset to select
# the threshold
return 1.0 <= c.duration <= 15.0
train_cuts = train_cuts.filter(remove_short_and_long_utt)
............
模型訓練
在完成相關必要文件準備和數(shù)據加載成功的基礎上,我們可以開始進行模型的訓練了。
在訓練之前,我們需要根據訓練數(shù)據的規(guī)模和我們的算力條件(比如 GPU 顯卡的型號、GPU 顯卡的數(shù)量、每個卡的顯存大小等)去調整相關的參數(shù)。
這里,我們將主要介紹幾個比較關鍵的參數(shù),其中,world-size 表示并行計算的 GPU 數(shù)量,max-duration 表示每個 batch 中所有音頻樣本的最大時長之和,num-epochs 表示訓練的 epochs 數(shù),valid-interval 表示在驗證集上計算 loss 的 iterations 間隔,model-warm-step 表示模型熱啟動的 iterations 數(shù),use-fp16 表示是否用16位的浮點數(shù)進行訓練等,其他參數(shù)可以參考 train.py[21] 具體的參數(shù)解釋和說明。
在這個示例中,我們用 WenetSpeech 中 L subset 訓練集來進行訓練,并綜合考慮該數(shù)據集的規(guī)模和我們的算力條件,訓練參數(shù)設置和運行指令如下(沒出現(xiàn)的參數(shù)表示使用默認的參數(shù)值):
export CUDA_VISIBLE_DEVICES='0,1,2,3,4,5,6,7'
python3 pruned_transducer_stateless2/train.py \
--lang-dir data/lang_char \
--exp-dir pruned_transducer_stateless2/exp \
--world-size 8 \
--num-epochs 15 \
--start-epoch 0 \
--max-duration 180 \
--valid-interval 3000 \
--model-warm-step 3000 \
--save-every-n 8000 \
--training-subset L
到這里,如果能看到訓練過程中的 loss 記錄的輸出,則說明訓練已經成功開始了。
另外,如果在訓練過程中,出現(xiàn)了 Out of Memory 的報錯信息導致訓練中止,可以嘗試使用更小一些的 max-duration 值。如果還有其他的報錯導致訓練中止,一方面希望讀者可以靈活地根據實際情況修改或調整某些參數(shù),另一方面,讀者可以在相關討論群或者在icefall 上通過 issues 和 pull request 等形式進行反饋。
如果程序在中途中止訓練,我們也不必從頭開始訓練,可以通過加載保存的某個 epoch-X.pt 或 checkpoint-X.pt 模型文件(包含了模型參數(shù)、采樣器和學習率等參數(shù))繼續(xù)訓練,如加載 epoch-3.pt 的模型文件繼續(xù)訓練:
export CUDA_VISIBLE_DEVICES='0,1,2,3,4,5,6,7'
python3 pruned_transducer_stateless2/train.py \
--lang-dir data/lang_char \
--exp-dir pruned_transducer_stateless2/exp \
--world-size 8 \
--num-epochs 15 \
--start-batch 3 \
--max-duration 180 \
--valid-interval 3000 \
--model-warm-step 3000 \
--save-every-n 8000 \
--training-subset L
這樣即使程序中斷了,我們也不用從零開始訓練模型。
另外,我們也不用從第一個 batch 進行迭代訓練,因為采樣器中保存了迭代的 batch 數(shù),我們可以設置參數(shù) --start-batch xxx, 使得我們可以從某一個 epoch 的某個 batch 處開始訓練,這大大節(jié)省了訓練時間和計算資源,尤其是在訓練大規(guī)模數(shù)據集時。
在 icefall 中,還有更多類似這樣人性化的訓練設置,等待大家去發(fā)現(xiàn)和使用。
當訓練完畢以后,我們可以得到相關的訓練 log 文件和 tensorboard 損失記錄,可以在終端使用如下指令:
cd pruned_transducer_stateless2/exp
tensorboard dev upload --logdir tensorboard
如在使用上述指令之后,我們可以在終端看到如下信息:
............
To stop uploading, press Ctrl-C.
New experiment created. View your TensorBoard at: https://v/experiment/wM4ZUNtASRavJx79EOYYcg/
[2022-06-30T15:49:38] Started scanning logdir.
Uploading 4542 scalars...
............
將上述顯示的 tensorboard 記錄查看網址復制到本地瀏覽器的網址欄中即可查看。如在本示例中,我們將 https://v/experiment/wM4ZUNtASRavJx79EOYYcg/ 復制到本地瀏覽器的網址欄中,損失函數(shù)的 tensorboard 記錄如下:
(PS: 讀者可從上圖發(fā)現(xiàn),筆者在訓練 WenetSpeech L subset 時,也因為某些原因中斷了訓練,但是,icefall 中人性化的接續(xù)訓練操作讓筆者避免了從零開始訓練,并且前后兩個訓練階段的 loss 和 learning rate 曲線還連接地如此完美。)
解碼測試
當模型訓練完畢,我們就可以進行解碼測試了。
在運行解碼測試的指令之前,我們依然需要對 decode.py 進行如文件準備過程中對 train.py 相似位置的修改和調整,這里將不具體講述,修改后的文件可參考 decode.py[22]。
這里為了在測試過程中更快速地加載數(shù)據,我們將測試數(shù)據導出為 webdataset 要求的形式(注:這一步不是必須的,如果測試過程中速度比較快,這一步可以省略),操作如下:
............
# Note: Please use 'pip install webdataset==0.1.103'
# for installing the webdataset.
import glob
import os
from lhotse import CutSet
from lhotse.dataset.webdataset import export_to_webdataset
wenetspeech = WenetSpeechAsrDataModule(args)
dev = 'dev'
............
if not os.path.exists(f'{dev}/shared-0.tar'):
os.makedirs(dev)
dev_cuts = wenetspeech.valid_cuts()
export_to_webdataset(
dev_cuts,
output_path=f'{dev}/shared-%d.tar',
shard_size=300,
)
............
dev_shards = [
str(path)
for path in sorted(glob.glob(os.path.join(dev, 'shared-*.tar')))
]
cuts_dev_webdataset = CutSet.from_webdataset(
dev_shards,
split_by_worker=True,
split_by_node=True,
shuffle_shards=True,
)
............
dev_dl = wenetspeech.valid_dataloaders(cuts_dev_webdataset)
............
同時,在 asr_datamodule.py 中修改 test_dataloader 函數(shù),修改如下(注:這一步不是必須的,如果測試過程中速度比較快,這一步可以省略):
............
from lhotse.dataset.iterable_dataset import IterableDatasetWrapper
test_iter_dataset = IterableDatasetWrapper(
dataset=test,
sampler=sampler,
)
test_dl = DataLoader(
test_iter_dataset,
batch_size=None,
num_workers=self.args.num_workers,
)
return test_dl
待修改完畢,聯(lián)合調試 decode.py 和 asr_datamodule.py, 解碼過程能正常加載數(shù)據即可。
在進行解碼測試時,icefall 為我們提供了四種解碼方式:greedy_search、beam_search、modified_beam_search 和 fast_beam_search,更為具體實現(xiàn)方式,可參考文件 beam_search.py[23]。
這里,因為建模單元的數(shù)量非常多(5500+),導致解碼速度非常慢,所以,筆者不建議使用 beam_search 的解碼方式。
在本示例中,如果使用 greedy_search 進行解碼,我們的解碼指令如下 (
關于如何使用其他的解碼方式,讀者可以自行參考 decode.py):
export CUDA_VISIBLE_DEVICES='0'
python pruned_transducer_stateless2/decode.py \
--epoch 10 \
--avg 2 \
--exp-dir ./pruned_transducer_stateless2/exp \
--lang-dir data/lang_char \
--max-duration 100 \
--decoding-method greedy_search
運行上述指令進行解碼,在終端將會展示如下內容(部分):
............
2022-06-30 16:58:17,232 INFO [decode.py:487] About to create model
2022-06-30 16:58:17,759 INFO [decode.py:508] averaging ['pruned_transducer_stateless2/exp/epoch-9.pt', 'pruned_transducer_stateless2/exp/epoch-10.pt']
............
2022-06-30 16:58:42,260 INFO [decode.py:393] batch 0/?, cuts processed until now is 104
2022-06-30 16:59:41,290 INFO [decode.py:393] batch 100/?, cuts processed until now is 13200
2022-06-30 17:00:35,961 INFO [decode.py:393] batch 200/?, cuts processed until now is 27146
2022-06-30 17:00:38,370 INFO [decode.py:410] The transcripts are stored in pruned_transducer_stateless2/exp/greedy_search/recogs-DEV-greedy_search-epoch-10-avg-2-context-2-max-sym-per-frame-1.txt
2022-06-30 17:00:39,129 INFO [utils.py:410] [DEV-greedy_search] %WER 7.80% [51556 / 660996, 6272 ins, 18888 del, 26396 sub ]
2022-06-30 17:00:41,084 INFO [decode.py:423] Wrote detailed error stats to pruned_transducer_stateless2/exp/greedy_search/errs-DEV-greedy_search-epoch-10-avg-2-context-2-max-sym-per-frame-1.txt
2022-06-30 17:00:41,092 INFO [decode.py:440]
For DEV, WER of different settings are:
greedy_search 7.8 best for DEV
............
這里,讀者可能還有一個疑問,如何選取合適的 epoch 和 avg 參數(shù),以保證平均模型的性能最佳呢?這里我們通過遍歷所有的 epoch 和 avg 組合來搜索最好的平均模型,可以使用如下指令得到所有可能的平均模型的性能,然后進行找到最好的解碼結果所對應的平均模型的 epoch 和 avg 即可,如:
export CUDA_VISIBLE_DEVICES='0'
num_epochs=15
for ((i=$num_epochs; i>=0; i--));
do
for ((j=1; j<=$i; j++));
do
python3 pruned_transducer_stateless2/decode.py \
--exp-dir ./pruned_transducer_stateless2/exp \
--lang-dir data/lang_char \
--epoch $i \
--avg $j \
--max-duration 100 \
--decoding-method greedy_search
done
done
以上方法僅供讀者參考,讀者可根據自己的實際情況進行修改和調整。目前,icefall 也提供了一種新的平均模型參數(shù)的方法,性能更好,這里將不作細述,有興趣可以參考文件 decode.py[24] 中的參數(shù) --use-averaged-model。
至此,解碼測試就完成了。使用者也可以通過查看 egs/pruned_transducer_stateless2/exp/greedy_search 中 recogs-*.txt、errs-*.txt 和 wer-*.txt 等文件,看看每個樣本的具體解碼結果和最終解碼性能。
本示例中,筆者的訓練模型和測試結果可以參考 icefall_asr_wenetspeech_pruned_transducer_stateless2[25],讀者可以在 icefall_asr_wenetspeech_pruned_transducer_stateless2_colab_demo[26] 上直接運行和測試提供的模型,這些僅供讀者參考。
第三步:服務端部署演示
在順利完成第一步和第二步之后,我們就可以得到訓練模型和測試結果了。
接下來,筆者將講述如何利用 sherpa 框架把訓練得到的模型部署到服務端,筆者強烈建議讀者參考和閱讀 sherpa使用文檔[27],該框架還在不斷地更新和優(yōu)化中,感興趣的讀者可以保持關注并參與到開發(fā)中來。
本示例中,我們用的 sherpa 版本為 sherpa-for-wenetspeech-pruned-rnnt2[28]。
為了將整個過程描述地更加清晰,筆者同樣將第三步細分為以下幾步:將訓練好的模型編譯為 TorchScript 代碼、服務器終端運行、本地 web 端測試使用。
將訓練好的模型編譯為 TorchScript 代碼
這里,我們使用 torch.jit.script 對模型進行編譯,使得 nn.Module 形式的模型在生產環(huán)境下變得可用,具體的代碼實現(xiàn)可參考文件 export.py[29],操作指令如下:
python3 pruned_transducer_stateless2/export.py \
--exp-dir ./pruned_transducer_stateless2/exp \
--lang-dir data/lang_char \
--epoch 10 \
--avg 2 \
--jit True
運行上述指令,我們可以在 egs/wenetspeech/ASR/pruned_transducer_stateless2/exp 中得到一個 cpu_jit.pt 的文件,這是我們在 sherpa 框架里將要使用的模型文件。
服務器終端運行
本示例中,我們的模型是中文非流式的,所以我們選擇非流式模式來運行指令,同時,我們需要選擇在上述步驟中生成的 cpu_jit.pt 和 tokens.txt :
python3 sherpa/bin/conformer_rnnt/offline_server.py \
--port 6006 \
--num-device 1 \
--max-batch-size 10 \
--max-wait-ms 5 \
--max-active-connections 500 \
--feature-extractor-pool-size 5 \
--nn-pool-size 1 \
--nn-model-filename ~/icefall/egs/wenetspeech/ASR/pruned_transducer_stateless2/exp/cpu_jit.pt \
--token-filename ~/icefall/egs/wenetspeech/ASR/data/lang_char/tokens.txt
注:在上述指令的參數(shù)中,port 為6006,這里的端口也不是固定的,讀者可以根據自己的實際情況進行修改,如6007等。但是,修改本端口的同時,必須要在 sherpa/bin/web/js 中對 offline_record.js 和 streaming_record.js中的端口進行同步修改,以保證 web 的數(shù)據和 server 的數(shù)據可以互通。
與此同時,我們還需要在服務器終端另開一個窗口開啟 web 網頁端服務,指令如下:
cd sherpa/bin/web
python3 -m http.server 6008
本地 web 端測試使用
在服務器端運行相關功能的調用指令后,為了有更好的 ASR 交互體驗,我們還需要將服務器端的 web 網頁端服務進行本地化,所以使用 ssh 來連接本地端口和服務器上的端口:
ssh -R 6006:localhost:6006 -R 6008:localhost:6008 local_username@local_ip
接下來,我們可以在本地瀏覽器的網址欄輸入:localhost:6008,我們將可以看到如下頁面:
我們選擇 Offline-Record,并打開麥克風,即可錄音識別了。筆者的一個識別結果如下圖所示:
到這里,從數(shù)據準備和處理、模型訓練和測試、服務端部署演示等三步就基本完成了。
新一代 Kaldi 語音識別開源框架還在快速地迭代和發(fā)展之中,本文所展示的只是其中極少的一部分內容,筆者在本文中也只是粗淺地概述了它的部分使用流程,更多詳細具體的細節(jié),希望讀者能夠自己去探索和發(fā)現(xiàn)。
總結
在本文中,筆者試圖以 WenetSpeech 的 pruned transducer stateless2 recipe 構建、訓練、部署的全流程為線索,貫通 k2、icefall、lhotse、sherpa四個獨立子項目, 將新一代 Kaldi 框架的數(shù)據準備和處理、模型訓練和測試、服務端部署演示等流程一體化地全景展示出來,形成一個簡易的教程,希望能夠更好地幫助讀者認識和使用新一代 Kaldi 語音識別開源框架,真正做到上手即用。
參考資料
[1]k2: https://github.com/k2-fsa/k2
[2]icefall: https://github.com/k2-fsa/icefall
[3]lhotse: https://github.com/lhotse-speech/lhotse
[4]sherpa: https://github.com/k2-fsa/sherpa
[5]pruned transducer stateless2 recipe: https://github.com/k2-fsa/icefall/tree/master/egs/wenetspeech/ASR
[6]pruned transducer stateless2 recipe: https://github.com/k2-fsa/icefall/tree/master/egs/wenetspeech/ASR
[7]prepare.sh: https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/prepare.sh
[8]prepare.sh: https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/prepare.sh
[9]wenet_speech.py: https://github.com/lhotse-speech/lhotse/blob/master/lhotse/recipes/wenet_speech.py
[10]preprocess_wenetspeech.py: https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/local/preprocess_wenetspeech.py
[11]compute_fbank_wenetspeech_splits.py: https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/local/compute_fbank_wenetspeech_splits.py
[12]text2token.py: https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/local/text2token.py
[13]text2segments.py: https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/local/text2segments.py
[14]prepare_char.py: https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/local/prepare_char.py
[15]egs/librispeech/ASR/prepare.sh: https://github.com/k2-fsa/icefall/tree/master/egs/librispeech/ASR
[16]egs/librispeech/ASR/pruned_transducer_stateless2: https://github.com/k2-fsa/icefall/tree/master/egs/librispeech/ASR/pruned_transducer_stateless2
[17]egs/wenetspeech/ASR/pruned_transducer_stateless2/train.py: https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/pruned_transducer_stateless2/train.py
[18]asr_datamodule.py: https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py
[19]egs/librispeech/ASR/pruned_transducer_stateless2/asr_datamodule.py: https://github.com/k2-fsa/icefall/blob/master/egs/librispeech/ASR/pruned_transducer_stateless2/asr_datamodule.py
[20]display_manifest_statistics.py: https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/local/display_manifest_statistics.py,
[21]train.py: https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/pruned_transducer_stateless2/train.py
[22]decode.py: https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/pruned_transducer_stateless2/decode.py
[23]beam_search.py: https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/pruned_transducer_stateless2/train.py
[24]decode.py: https://github.com/k2-fsa/icefall/blob/master/egs/librispeech/ASR/pruned_transducer_stateless5/train.py
[25]icefall_asr_wenetspeech_pruned_transducer_stateless2: https:///luomingshuang/icefall_asr_wenetspeech_pruned_transducer_stateless2
[26]icefall_asr_wenetspeech_pruned_transducer_stateless2_colab_demo: https://colab.research.google.com/drive/1EV4e1CHa1GZgEF-bZgizqI9RyFFehIiN?usp=sharing
[27]sherpa使用文檔: https://k2-fsa./sherpa/
[28]sherpa-for-wenetspeech-pruned-rnnt2: https://github.com/k2-fsa/sherpa/tree/9da5b0779ad6758bf3150e1267399fafcdef4c67
[29]export.py: https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/pruned_transducer_stateless2/export.py