日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

如何基于新一代 Kaldi 框架快速搭建服務(wù)端 ASR 系統(tǒng)

 520jefferson 2022-07-07 發(fā)布于北京
來(lái)源丨新一代Kaldi

本文將介紹如何基于新一代 Kaldi 框架快速搭建一個(gè)服務(wù)端的 ASR 系統(tǒng),包括數(shù)據(jù)準(zhǔn)備、模型訓(xùn)練測(cè)試、服務(wù)端部署運(yùn)行。

更多內(nèi)容建議參考:

  • k2[1]
  • icefall[2]
  • lhotse[3]
  • sherpa[4]

前言

距離新一代 Kaldi 開(kāi)源框架的正式發(fā)布已經(jīng)有一段時(shí)間了。截至目前,框架基本的四梁八柱都已經(jīng)立起來(lái)了。那么,如何用它快速搭建一個(gè) ASR 系統(tǒng)呢?

閱讀過(guò)前面幾期公眾文的讀者可能都知道新一代 Kaldi 框架主要包含了四個(gè)不同的子項(xiàng)目:k2、icefalllhotse、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ù)功能,分為三步:

  • 數(shù)據(jù)準(zhǔn)備和處理
  • 模型訓(xùn)練和測(cè)試
  • 服務(wù)端部署演示

本文介紹的 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)該事先安裝好 k2icefall、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], 其主要功能是生成 recordingssupervisionsjsonl.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ì) recordingssupervisionsjsonl.gz 文件進(jìn)行查看, 其中:

wenetspeech_recordings_S.jsonl.gz:

Image

wenetspeech_supervisions_S.jsonl.gz:

Image

由上面兩幅圖可知,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.txtwords.txtLinv.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_dataloaderstest_dataloaders 等幾個(gè)函數(shù)基本是不需要修改的,如有需要,調(diào)整其中的具體參數(shù)即可。

最后,調(diào)整修改后的 asr_datamodule.pytrain.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ò) issuespull request 等形式進(jìn)行反饋。

如果程序在中途中止訓(xùn)練,我們也不必從頭開(kāi)始訓(xùn)練,可以通過(guò)加載保存的某個(gè) epoch-X.ptcheckpoint-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 記錄如下:

Image

(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)練階段的 losslearning 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_searchbeam_search、modified_beam_searchfast_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),如何選取合適的 epochavg 參數(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_searchrecogs-*.txt、errs-*.txtwer-*.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.pttokens.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.jsstreaming_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è)面:Image

我們選擇 Offline-Record,并打開(kāi)麥克風(fēng),即可錄音識(shí)別了。筆者的一個(gè)識(shí)別結(jié)果如下圖所示:

Image

到這里,從數(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

Image

Image

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶(hù)發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶(hù) 評(píng)論公約

    類(lèi)似文章 更多