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

由上面兩幅圖可知,recordings
用于描述音頻文件信息,包含了音頻樣本的 id、具體路徑、通道、采樣率、子樣本數(shù)和時(shí)長(zhǎng)等。supervisions
用于記錄監(jiān)督信息,包含了音頻樣本對(duì)應(yīng)的 id、起始時(shí)間、時(shí)長(zhǎng)、通道、文本和語(yǔ)言類(lèi)型等。
接下來(lái),我們將對(duì)音頻數(shù)據(jù)提取特征。
計(jì)算、提取和貯存音頻特征
首先,對(duì)數(shù)據(jù)進(jìn)行預(yù)處理,包括對(duì)文本進(jìn)行標(biāo)準(zhǔn)化和對(duì)音頻進(jìn)行時(shí)域上的增廣,可參考文件 preprocess_wenetspeech.py[10]。
python3 ./local/preprocess_wenetspeech.py
其次,將數(shù)據(jù)集切片并對(duì)每個(gè)切片數(shù)據(jù)集進(jìn)行特征提取??蓞⒖嘉募? compute_fbank_wenetspeech_splits.py[11]。
(注:這里的切片是為了可以開(kāi)啟多個(gè)進(jìn)程同時(shí)對(duì)大規(guī)模數(shù)據(jù)集進(jìn)行特征提取,提高效率。如果數(shù)據(jù)集比較小,對(duì)數(shù)據(jù)進(jìn)行切片處理不是必須的。)
# 這里的 L 也可修改為 M 或 S, 表示訓(xùn)練數(shù)據(jù)子集
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
最后,待提取完每個(gè)切片數(shù)據(jù)集的特征后,將所有切片數(shù)據(jù)集的特征數(shù)據(jù)合并成一個(gè)總的特征數(shù)據(jù)集:
# 這里的 L 也可修改為 M 或 S, 表示訓(xùn)練數(shù)據(jù)子集
pieces=$(find data/fbank/L_split_1000 -name 'cuts_L.*.jsonl.gz')
lhotse combine $pieces data/fbank/cuts_L.jsonl.gz
至此,我們基本完成了音頻文件的準(zhǔn)備和特征提取。接下來(lái),我們將構(gòu)建語(yǔ)言建模文件。
構(gòu)建語(yǔ)言建模文件
在 RNN-T
模型框架中,我們實(shí)際需要的用于訓(xùn)練和測(cè)試的建模文件有 tokens.txt
、words.txt
和 Linv.pt
。我們按照如下步驟構(gòu)建語(yǔ)言建模文件:
規(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
的形式如下:
怎么樣這些日子住得還習(xí)慣吧
挺好的
對(duì)了美靜這段日子經(jīng)常不和我們一起用餐
是不是對(duì)我回來(lái)有什么想法啊
哪有的事啊
她這兩天挺累的身體也不太舒服
我讓她多睡一會(huì)那就好如果要是覺(jué)得不方便
我就搬出去住
............
分詞并生成 words.txt
這里我們用 jieba
對(duì)中文句子進(jìn)行分詞,可參考文件 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
的形式如下:
怎么樣 這些 日子 住 得 還 習(xí)慣 吧
挺 好 的
對(duì) 了 美靜 這段 日子 經(jīng)常 不 和 我們 一起 用餐
是不是 對(duì) 我 回來(lái) 有 什么 想法 啊
哪有 的 事 啊
她 這 兩天 挺累 的 身體 也 不 太 舒服
我 讓 她 多 睡 一會(huì) 那就好 如果 要是 覺(jué)得 不 方便
我 就 搬出去 住
............
words_no_ids.txt
的形式如下:
............
阿
阿Q
阿阿虎
阿阿離
阿阿瑪
阿阿毛
阿阿強(qiáng)
阿阿淑
阿安
............
words.txt
的形式如下:
............
阿 225
阿Q 226
阿阿虎 227
阿阿離 228
阿阿瑪 229
阿阿毛 230
阿阿強(qiáng) 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光線(xiàn) X 光 線(xiàn)
X射線(xiàn) X 射 線(xiàn)
Y Y
YC Y C
YS Y S
YY Y Y
Z Z
ZO Z O
ZSU Z S U
○ ○
一 一
一一 一 一
一一二 一 一 二
一一例 一 一 例
............
至此,第一步全部完成。對(duì)于不同數(shù)據(jù)集來(lái)說(shuō),其基本思路也是類(lèi)似的。在數(shù)據(jù)準(zhǔn)備和處理階段,我們主要做兩件事情:準(zhǔn)備音頻文件并進(jìn)行特征提取
、構(gòu)建語(yǔ)言建模文件
。
這里我們使用的范例是中文漢語(yǔ),建模單元是字。在英文數(shù)據(jù)中,我們一般用 BPE 作為建模單元,具體的可參考 egs/librispeech/ASR/prepare.sh[15] 。
第二步:模型訓(xùn)練和測(cè)試
在完成第一步的基礎(chǔ)上,我們可以進(jìn)入到第二步,即模型的訓(xùn)練和測(cè)試了。這里,我們根據(jù)操作流程和功能,將第二步劃分為更加具體的幾步:文件準(zhǔn)備、數(shù)據(jù)加載、模型訓(xùn)練、解碼測(cè)試。
文件準(zhǔn)備
首先,創(chuàng)建 pruned_transducer_stateless2 的文件夾。
mkdir pruned_transducer_stateless2
cd pruned_transducer_stateless2
其次,我們需要準(zhǔn)備數(shù)據(jù)讀取、模型、訓(xùn)練、測(cè)試、模型導(dǎo)出等腳本文件。在這里,我們?cè)?egs/librispeech/ASR/pruned_transducer_stateless2[16] 的基礎(chǔ)上創(chuàng)建我們需要的文件。
對(duì)于公共的腳本文件(即不需要修改的文件),我們可以用軟鏈接直接復(fù)制過(guò)來(lái),如:
ln -s ../../../librispeech/ASR/pruned_transducer_stateless2/conformer.py .
其他相同文件的操作類(lèi)似。另外,讀者也可以使用自己的模型,替換本框架內(nèi)提供的模型文件即可。
對(duì)于不同的腳本文件(即因?yàn)閿?shù)據(jù)集或者語(yǔ)言不同而需要修改的文件),我們先從 egs/librispeech/ASR/pruned_transducer_stateless2
中復(fù)制過(guò)來(lái),然后再進(jìn)行小范圍的修改,如:
cp -r ../../../librispeech/ASR/pruned_transducer_stateless2/train.py .
在本示例中,我們需要對(duì) train.py
中的數(shù)據(jù)讀取、graph_compiler(圖編譯器)及
vocab_size 的獲取等部分進(jìn)行修改,如(截取部分代碼,便于讀者直觀認(rèn)識(shí)):
數(shù)據(jù)讀?。?/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
............
更加詳細(xì)的修改后的 train.py 可參考 egs/wenetspeech/ASR/pruned_transducer_stateless2/train.py[17] 。其他 decode.py、pretrained.py、export.py 等需要修改的文件也可以參照上述進(jìn)行類(lèi)似的修改和調(diào)整。
(注:在準(zhǔn)備文件時(shí),應(yīng)該遵循相同的文件不重復(fù)造輪子、不同的文件盡量小改、缺少的文件自己造
的原則。icefall 中大多數(shù)函數(shù)和功能文件在很多數(shù)據(jù)集上都進(jìn)行了測(cè)試和驗(yàn)證,都是可以直接遷移使用的。)
數(shù)據(jù)加載
實(shí)際上,對(duì)于數(shù)據(jù)加載這一步,也可以視為文件準(zhǔn)備的一部分,即修改文件 asr_datamodule.py[18],但是考慮到不同數(shù)據(jù)集的 asr_datamodule.py 都不一樣,所以這里單獨(dú)拿出來(lái)講述。
首先,這里以 egs/librispeech/ASR/pruned_transducer_stateless2/asr_datamodule.py[19] 為基礎(chǔ),在這個(gè)上面進(jìn)行修改:
cp -r ../../../librispeech/ASR/pruned_transducer_stateless2/asr_datamodule.py .
其次,修改函數(shù)類(lèi)的名稱(chēng),如這里將 LibriSpeechAsrDataModule
修改為 WenetSpeechAsrDataModule
,并讀取第一步中生成的 jsonl.gz
格式的訓(xùn)練測(cè)試文件。本示例中,第一步生成了 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
............
其他的訓(xùn)練測(cè)試集的 jsonl.gz
文件讀取和上述類(lèi)似。另外,對(duì)于 train_dataloaders
、valid_dataloaders
和 test_dataloaders
等幾個(gè)函數(shù)基本是不需要修改的,如有需要,調(diào)整其中的具體參數(shù)即可。
最后,調(diào)整修改后的 asr_datamodule.py
和 train.py
聯(lián)合調(diào)試,把 WenetSpeechAsrDataModule
導(dǎo)入到 train.py
,運(yùn)行它,如果在數(shù)據(jù)讀取和加載過(guò)程中不報(bào)錯(cuò),那么數(shù)據(jù)加載部分就完成了。
另外,在數(shù)據(jù)加載的過(guò)程中,我們也有必要對(duì)數(shù)據(jù)樣本的時(shí)長(zhǎng)進(jìn)行統(tǒng)計(jì),并過(guò)濾一些過(guò)短、過(guò)長(zhǎng)且占比極小的樣本,這樣可以使我們的訓(xùn)練過(guò)程更加穩(wěn)定。
在本示例中,我們對(duì) WenetSpeech 的樣本進(jìn)行了時(shí)長(zhǎng)統(tǒng)計(jì)(L 數(shù)據(jù)集太大,這里沒(méi)有對(duì)它進(jìn)行統(tǒng)計(jì)),具體的可參考 display_manifest_statistics.py[20],統(tǒng)計(jì)的部分結(jié)果如下:
............
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
根據(jù)上面的統(tǒng)計(jì)結(jié)果,我們?cè)?train.py
中設(shè)置了樣本的最大時(shí)長(zhǎng)為 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)
............
模型訓(xùn)練
在完成相關(guān)必要文件準(zhǔn)備和數(shù)據(jù)加載成功的基礎(chǔ)上,我們可以開(kāi)始進(jìn)行模型的訓(xùn)練了。
在訓(xùn)練之前,我們需要根據(jù)訓(xùn)練數(shù)據(jù)的規(guī)模和我們的算力條件(比如 GPU 顯卡的型號(hào)、GPU 顯卡的數(shù)量、每個(gè)卡的顯存大小等)去調(diào)整相關(guān)的參數(shù)。
這里,我們將主要介紹幾個(gè)比較關(guān)鍵的參數(shù),其中,world-size
表示并行計(jì)算的 GPU 數(shù)量,max-duration
表示每個(gè) batch 中所有音頻樣本的最大時(shí)長(zhǎng)之和,num-epochs
表示訓(xùn)練的 epochs 數(shù),valid-interval
表示在驗(yàn)證集上計(jì)算 loss 的 iterations 間隔,model-warm-step
表示模型熱啟動(dòng)的 iterations 數(shù),use-fp16
表示是否用16位的浮點(diǎn)數(shù)進(jìn)行訓(xùn)練等,其他參數(shù)可以參考 train.py[21] 具體的參數(shù)解釋和說(shuō)明。
在這個(gè)示例中,我們用 WenetSpeech 中 L subset
訓(xùn)練集來(lái)進(jìn)行訓(xùn)練,并綜合考慮該數(shù)據(jù)集的規(guī)模和我們的算力條件,訓(xùn)練參數(shù)設(shè)置和運(yùn)行指令如下(沒(méi)出現(xiàn)的參數(shù)表示使用默認(rèn)的參數(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
到這里,如果能看到訓(xùn)練過(guò)程中的 loss
記錄的輸出,則說(shuō)明訓(xùn)練已經(jīng)成功開(kāi)始了。
另外,如果在訓(xùn)練過(guò)程中,出現(xiàn)了 Out of Memory
的報(bào)錯(cuò)信息導(dǎo)致訓(xùn)練中止,可以嘗試使用更小一些的 max-duration
值。如果還有其他的報(bào)錯(cuò)導(dǎo)致訓(xùn)練中止,一方面希望讀者可以靈活地根據(jù)實(shí)際情況修改或調(diào)整某些參數(shù),另一方面,讀者可以在相關(guān)討論群或者在icefall 上通過(guò) issues
和 pull request
等形式進(jìn)行反饋。
如果程序在中途中止訓(xùn)練,我們也不必從頭開(kāi)始訓(xùn)練,可以通過(guò)加載保存的某個(gè) epoch-X.pt
或 checkpoint-X.pt
模型文件(包含了模型參數(shù)、采樣器和學(xué)習(xí)率等參數(shù))繼續(xù)訓(xùn)練,如加載 epoch-3.pt 的模型文件繼續(xù)訓(xùn)練:
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
這樣即使程序中斷了,我們也不用從零開(kāi)始訓(xùn)練模型。
另外,我們也不用從第一個(gè) batch
進(jìn)行迭代訓(xùn)練,因?yàn)椴蓸悠髦斜4媪说?batch 數(shù),我們可以設(shè)置參數(shù) --start-batch xxx
, 使得我們可以從某一個(gè) epoch 的某個(gè) batch 處開(kāi)始訓(xùn)練,這大大節(jié)省了訓(xùn)練時(shí)間和計(jì)算資源,尤其是在訓(xùn)練大規(guī)模數(shù)據(jù)集時(shí)。
在 icefall 中,還有更多類(lèi)似這樣人性化的訓(xùn)練設(shè)置,等待大家去發(fā)現(xiàn)和使用。
當(dāng)訓(xùn)練完畢以后,我們可以得到相關(guān)的訓(xù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
記錄查看網(wǎng)址復(fù)制到本地瀏覽器的網(wǎng)址欄中即可查看。如在本示例中,我們將 https://v/experiment/wM4ZUNtASRavJx79EOYYcg/ 復(fù)制到本地瀏覽器的網(wǎng)址欄中,損失函數(shù)的 tensorboard 記錄如下:
(PS: 讀者可從上圖發(fā)現(xiàn),筆者在訓(xùn)練 WenetSpeech L subset 時(shí),也因?yàn)槟承┰蛑袛嗔擞?xùn)練,但是,icefall 中人性化的接續(xù)訓(xùn)練操作讓筆者避免了從零開(kāi)始訓(xùn)練,并且前后兩個(gè)訓(xùn)練階段的 loss
和 learning rate
曲線(xiàn)還連接地如此完美。)
解碼測(cè)試
當(dāng)模型訓(xùn)練完畢,我們就可以進(jìn)行解碼測(cè)試了。
在運(yùn)行解碼測(cè)試的指令之前,我們依然需要對(duì) decode.py
進(jìn)行如文件準(zhǔn)備過(guò)程中對(duì) train.py
相似位置的修改和調(diào)整,這里將不具體講述,修改后的文件可參考 decode.py[22]。
這里為了在測(cè)試過(guò)程中更快速地加載數(shù)據(jù),我們將測(cè)試數(shù)據(jù)導(dǎo)出為 webdataset
要求的形式(注:這一步不是必須的,如果測(cè)試過(guò)程中速度比較快,這一步可以省略),操作如下:
............
# 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)
............
同時(shí),在 asr_datamodule.py
中修改 test_dataloader
函數(shù),修改如下(注:這一步不是必須的,如果測(cè)試過(guò)程中速度比較快,這一步可以省略):
............
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)合調(diào)試 decode.py 和 asr_datamodule.py, 解碼過(guò)程能正常加載數(shù)據(jù)即可。
在進(jìn)行解碼測(cè)試時(shí),icefall 為我們提供了四種解碼方式:greedy_search
、beam_search
、modified_beam_search
和 fast_beam_search
,更為具體實(shí)現(xiàn)方式,可參考文件 beam_search.py[23]。
這里,因?yàn)榻卧臄?shù)量非常多(5500+),導(dǎo)致解碼速度非常慢,所以,筆者不建議使用 beam_search 的解碼方式。
在本示例中,如果使用 greedy_search 進(jìn)行解碼,我們的解碼指令如下 (
關(guān)于如何使用其他的解碼方式,讀者可以自行參考 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
運(yùn)行上述指令進(jìn)行解碼,在終端將會(huì)展示如下內(nèi)容(部分):
............
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
............
這里,讀者可能還有一個(gè)疑問(wèn),如何選取合適的 epoch
和 avg
參數(shù),以保證平均模型的性能最佳呢?這里我們通過(guò)遍歷所有的 epoch 和 avg 組合來(lái)搜索最好的平均模型,可以使用如下指令得到所有可能的平均模型的性能,然后進(jìn)行找到最好的解碼結(jié)果所對(duì)應(yīng)的平均模型的 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
以上方法僅供讀者參考,讀者可根據(jù)自己的實(shí)際情況進(jìn)行修改和調(diào)整。目前,icefall 也提供了一種新的平均模型參數(shù)的方法,性能更好,這里將不作細(xì)述,有興趣可以參考文件 decode.py[24] 中的參數(shù) --use-averaged-model
。
至此,解碼測(cè)試就完成了。使用者也可以通過(guò)查看 egs/pruned_transducer_stateless2/exp/greedy_search
中 recogs-*.txt
、errs-*.txt
和 wer-*.txt
等文件,看看每個(gè)樣本的具體解碼結(jié)果和最終解碼性能。
本示例中,筆者的訓(xùn)練模型和測(cè)試結(jié)果可以參考 icefall_asr_wenetspeech_pruned_transducer_stateless2[25],讀者可以在 icefall_asr_wenetspeech_pruned_transducer_stateless2_colab_demo[26] 上直接運(yùn)行和測(cè)試提供的模型,這些僅供讀者參考。
第三步:服務(wù)端部署演示
在順利完成第一步和第二步之后,我們就可以得到訓(xùn)練模型和測(cè)試結(jié)果了。
接下來(lái),筆者將講述如何利用 sherpa 框架把訓(xùn)練得到的模型部署到服務(wù)端,筆者強(qiáng)烈建議讀者參考和閱讀 sherpa使用文檔[27],該框架還在不斷地更新和優(yōu)化中,感興趣的讀者可以保持關(guān)注并參與到開(kāi)發(fā)中來(lái)。
本示例中,我們用的 sherpa 版本為 sherpa-for-wenetspeech-pruned-rnnt2[28]。
為了將整個(gè)過(guò)程描述地更加清晰,筆者同樣將第三步細(xì)分為以下幾步:將訓(xùn)練好的模型編譯為 TorchScript 代碼
、服務(wù)器終端運(yùn)行
、本地 web 端測(cè)試使用
。
將訓(xùn)練好的模型編譯為 TorchScript 代碼
這里,我們使用 torch.jit.script
對(duì)模型進(jìn)行編譯,使得 nn.Module
形式的模型在生產(chǎn)環(huán)境下變得可用,具體的代碼實(shí)現(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
運(yùn)行上述指令,我們可以在 egs/wenetspeech/ASR/pruned_transducer_stateless2/exp
中得到一個(gè) cpu_jit.pt
的文件,這是我們?cè)?sherpa 框架里將要使用的模型文件。
服務(wù)器終端運(yùn)行
本示例中,我們的模型是中文非流式的,所以我們選擇非流式模式來(lái)運(yùn)行指令,同時(shí),我們需要選擇在上述步驟中生成的 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,這里的端口也不是固定的,讀者可以根據(jù)自己的實(shí)際情況進(jìn)行修改,如6007等。但是,修改本端口的同時(shí),必須要在 sherpa/bin/web/js
中對(duì) offline_record.js
和 streaming_record.js
中的端口進(jìn)行同步修改,以保證 web 的數(shù)據(jù)和 server 的數(shù)據(jù)可以互通。
與此同時(shí),我們還需要在服務(wù)器終端另開(kāi)一個(gè)窗口開(kāi)啟 web 網(wǎng)頁(yè)端服務(wù),指令如下:
cd sherpa/bin/web
python3 -m http.server 6008
本地 web 端測(cè)試使用
在服務(wù)器端運(yùn)行相關(guān)功能的調(diào)用指令后,為了有更好的 ASR 交互體驗(yàn),我們還需要將服務(wù)器端的 web 網(wǎng)頁(yè)端服務(wù)進(jìn)行本地化,所以使用 ssh 來(lái)連接本地端口和服務(wù)器上的端口:
ssh -R 6006:localhost:6006 -R 6008:localhost:6008 local_username@local_ip
接下來(lái),我們可以在本地瀏覽器的網(wǎng)址欄輸入:localhost:6008
,我們將可以看到如下頁(yè)面:
我們選擇 Offline-Record
,并打開(kāi)麥克風(fēng),即可錄音識(shí)別了。筆者的一個(gè)識(shí)別結(jié)果如下圖所示:
到這里,從數(shù)據(jù)準(zhǔn)備和處理、模型訓(xùn)練和測(cè)試、服務(wù)端部署演示等三步就基本完成了。
新一代 Kaldi 語(yǔ)音識(shí)別開(kāi)源框架還在快速地迭代和發(fā)展之中,本文所展示的只是其中極少的一部分內(nèi)容,筆者在本文中也只是粗淺地概述了它的部分使用流程,更多詳細(xì)具體的細(xì)節(jié),希望讀者能夠自己去探索和發(fā)現(xiàn)。
總結(jié)
在本文中,筆者試圖以 WenetSpeech 的 pruned transducer stateless2 recipe 構(gòu)建、訓(xùn)練、部署的全流程為線(xiàn)索,貫通 k2、icefall、lhotse、sherpa四個(gè)獨(dú)立子項(xiàng)目, 將新一代 Kaldi 框架的數(shù)據(jù)準(zhǔn)備和處理、模型訓(xùn)練和測(cè)試、服務(wù)端部署演示等流程一體化地全景展示出來(lái),形成一個(gè)簡(jiǎn)易的教程,希望能夠更好地幫助讀者認(rèn)識(shí)和使用新一代 Kaldi 語(yǔ)音識(shí)別開(kāi)源框架,真正做到上手即用。
參考資料
[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