在 Colab 中打开GitHub 上执行或查看/下载此 notebook

从头开始进行语音识别

准备好使用 SpeechBrain 构建自己的语音识别器了吗?

你很幸运,因为本教程正是你所寻找的!我们将引导你完成设置离线端到端基于注意力的语音识别器的整个过程。

但在我们开始之前,让我们快速了解一下语音识别,并查看 SpeechBrain 带来的酷炫技术。

让我们开始吧! 🚀

语音识别概述

图中展示了 SpeechBrain 中使用的典型语音识别管道示例

SpeechBrain-Page-2.png

语音识别过程从原始波形直接开始 🎤

原始波形会通过各种语音增强技术进行污染,例如时域/频域丢弃速度改变添加噪声混响等。这些干扰会根据用户指定的概率随机激活,并且会即时应用,无需将增强信号存储在磁盘上。

如需更深入地了解污染技术,请查看我们关于语音增强环境污染的教程。

接下来,我们提取语音特征,例如短时傅里叶变换 (STFT)频谱图FBANKsMFCCs。得益于高效且 GPU 友好的实现,这些特征可以即时计算。

欲了解更多详细信息,请参阅我们关于语音表示语音特征的教程。

随后,特征被馈送到语音识别器,这是一个将输入特征序列映射到输出标记序列(例如,音素、字符、子词、单词)的神经网络。SpeechBrain 支持流行的技术,如联结主义时间分类 (CTC)、Transducers 或带注意力的编码器/解码器(使用基于 RNN 和 Transformer 的系统)。

输出标记的后验概率由束搜索器处理,该搜索器探索替代方案并输出最佳方案。可选地,可以使用外部语言模型对替代方案进行重新评分,该语言模型可以基于 RNN 或 Transformer 🤖。

并非所有提到的模块都是必需的;例如,如果对特定任务没有帮助,可以跳过数据污染。即使是束搜索也可以用贪婪搜索代替,以实现快速解码。

现在,让我们深入讨论语音识别支持的不同技术:🚀

SpeechBrain-Page-3.png

联结主义时间分类 (CTC)

CTC 是 SpeechBrain 中最简单的语音识别系统。

它在每个时间步生成一个预测。CTC 引入了一个唯一的标记,blank,使网络能够在不确定时输出空白。CTC 损失函数采用动态规划对所有可能的对齐进行对齐。

对于每个对齐,可以计算相应的概率。最终的 CTC 损失是所有可能对齐的概率之和,使用前向算法高效计算(与神经网络中使用的算法不同,如隐马尔可夫模型文献中所述)。

在编码器-解码器架构中,注意力用于学习输入-输出序列之间的对齐。在 CTC 中,不对齐进行学习;相反,它对所有可能的对齐进行积分。

本质上,CTC 的实现包括在语音识别器之上(通常基于循环神经网络 (RNN),但并非独有)集成一个专门的损失函数。🧠

转换器 (Transducers)

在所示图中,Transducers 通过引入自回归预测器和联合网络来增强 CTC。

编码器将输入特征转换为一系列编码表示。另一方面,预测器根据先前发出的输出生成潜在表示。联合网络将这两者结合起来,然后 softmax 分类器预测当前的输出标记。训练期间,CTC 损失在分类器之后应用。

如需更深入地了解 Transducers,请查看 Loren Lugosch 的这篇 informative 教程:Transducer 教程 📚。

带有注意力的编码器-解码器 👂

语音识别中另一种广泛使用的方法是采用编码器-解码器架构。

  • 编码器处理语音特征序列(或直接处理原始样本)以生成一系列状态,记为 h。

  • 解码器利用最后的隐藏状态并产生 N 个输出标记。通常,解码器是自回归的,先前的输出被反馈到输入中。解码在预测句尾 (eos) 标记时停止。

  • 编码器和解码器可以使用各种神经网络架构构建,例如 RNN、CNN、Transformer 或它们的组合。

注意力的包含促进了编码器和解码器状态之间的动态连接。SpeechBrain 支持不同的注意力类型,包括基于 RNN 系统的内容位置感知注意力,以及基于 Transformer 的键值注意力。作为一种收敛增强,通常在编码器之上应用 CTC 损失。🚀

这种架构提供了灵活性和适应性,允许在各种应用中进行有效的语音识别。

束搜索 (Beamsearch)

编码器-解码器模型中使用的束搜索器遵循自回归过程。其工作原理如下:

  1. 初始化:过程始于(句子开始)标记。

  2. 预测:模型根据当前输入预测 N 个最有希望的下一个标记。

  3. 馈送候选项:将这 N 个候选项馈送到解码器以生成未来的假设。

  4. 选择:根据某些标准或评分机制选择最佳的 N 个假设。

  5. 迭代:循环继续,直到(句子结束)标记被预测。

SpeechBrain-Page-2 (1).png

我们鼓励对语音识别不够熟悉的读者在继续之前更加熟悉这项技术。除了学术论文,你还可以在网上找到精彩的教程和博客文章,例如:

在简要概述之后,现在让我们看看如何使用 SpeechBrain 开发一个语音识别系统(编码器-解码器 + CTC)。

为简单起见,训练将使用一个名为 mini-librispeech 的小型开源数据集,其中只包含几小时的训练数据。在实际应用中,你需要更多训练材料(例如 100 甚至 1000 小时)才能达到可接受的性能。

安装

为了足够快地运行代码,我们建议使用 GPU(运行时 => 更改运行时类型 => GPU)。在本教程中,我们将参考 speechbrain/templates/ASR 中的代码。

在开始之前,让我们安装 speechbrain

%%capture
# Installing SpeechBrain via pip
BRANCH = 'develop'
!python -m pip install git+https://github.com/speechbrain/speechbrain.git@$BRANCH

# Clone SpeechBrain repository
!git clone https://github.com/speechbrain/speechbrain/

需要哪些步骤?

1. 准备你的数据

  • 创建数据清单文件(CSV 或 JSON 格式),指定语音数据和相应文本标注的位置。

  • 利用 mini_librispeech_prepare.py 等工具生成这些清单文件。

2. 训练一个分词器

  • 确定用于训练语音识别器和语言模型的基本单元(例如,字符、音素、子词、单词)。

  • 执行分词器训练脚本

    cd speechbrain/templates/speech_recognition/Tokenizer
    python train.py tokenizer.yaml
    

3. 训练一个语言模型

  • 使用大型文本语料库训练语言模型(最好与你的目标应用处于同一语言领域)。

  • 语言模型训练脚本示例

    pip install datasets
    cd speechbrain/templates/speech_recognition/LM
    python train.py RNNLM.yaml
    

4. 训练语音识别器

  • 使用选定的模型(例如 CRDNN)训练语音识别器,该模型具有自回归 GRU 解码器和注意力机制。

  • 将束搜索与训练好的语言模型结合使用进行序列生成

    cd speechbrain/templates/speech_recognition/ASR
    python train.py train.yaml
    

5. 使用语音识别器 (推理)

  • 训练完成后,部署训练好的语音识别器进行推理。

  • 利用 SpeechBrain 中的 EncoderDecoderASR 等类来简化推理过程。

每个步骤对于构建有效的端到端语音识别器都至关重要。

我们现在将详细描述所有这些步骤。

步骤 1:准备你的数据

数据准备是训练端到端语音识别器的关键初始步骤。其主要目标是生成数据清单文件,这些文件指示 SpeechBrain 音频数据及其相应转录的位置。这些以广泛使用的 CSV 和 JSON 格式编写的清单文件在组织训练过程中起着至关重要的作用。

数据清单文件

让我们深入了解 JSON 格式的数据清单文件的结构

{
  "1867-154075-0032": {
    "wav": "{data_root}/LibriSpeech/train-clean-5/1867/154075/1867-154075-0032.flac",
    "length": 16.09,
    "words": "AND HE BRUSHED A HAND ACROSS HIS FOREHEAD AND WAS INSTANTLY HIMSELF CALM AND COOL VERY WELL THEN IT SEEMS I'VE MADE AN ASS OF MYSELF BUT I'LL TRY TO MAKE UP FOR IT NOW WHAT ABOUT CAROLINE"
  },
  "1867-154075-0001": {
    "wav": "{data_root}/LibriSpeech/train-clean-5/1867/154075/1867-154075-0001.flac",
    "length": 14.9,
    "words": "THAT DROPPED HIM INTO THE COAL BIN DID HE GET COAL DUST ON HIS SHOES RIGHT AND HE DIDN'T HAVE SENSE ENOUGH TO WIPE IT OFF AN AMATEUR A RANK AMATEUR I TOLD YOU SAID THE MAN OF THE SNEER WITH SATISFACTION"
  },
  "1867-154075-0028": {
    "wav": "{data_root}/LibriSpeech/train-clean-5/1867/154075/1867-154075-0028.flac",
    "length": 16.41,
    "words": "MY NAME IS JOHN MARK I'M DOONE SOME CALL ME RONICKY DOONE I'M GLAD TO KNOW YOU RONICKY DOONE I IMAGINE THAT NAME FITS YOU NOW TELL ME THE STORY OF WHY YOU CAME TO THIS HOUSE OF COURSE IT WASN'T TO SEE A GIRL"
  },
}

这种结构遵循分层格式,其中口语句子的唯一标识符作为第一个键。每个条目都指定了关键字段,例如语音录音的路径、以秒为单位的长度以及所说单词的序列。

一个特殊变量 data_root 允许从命令行或 YAML 超参数文件动态更改数据文件夹。

准备脚本

为特定数据集创建准备脚本至关重要,因为每个数据集都有自己的格式。例如,针对 mini-librispeech 数据集的 mini_librispeech_prepare.py 脚本可作为基础模板。此脚本会自动下载公共可用数据,搜索音频文件和转录,并创建 JSON 文件。

使用此脚本作为起点,为目标数据集进行自定义数据准备。它提供了一个实用的指南,通过三个单独的数据清单文件来组织训练、验证和测试阶段。

将你的数据复制到本地

在 HPC 集群或类似环境中,优化代码性能涉及将数据复制到计算节点的本地文件夹。虽然在 Google Colab 中不适用,但此做法通过从本地文件系统而不是共享文件系统获取数据来显著加快代码执行速度。

在为训练语音识别器踏上关键的数据准备之旅时,请注意这些注意事项。🚀🎙️

步骤 2:分词器

为你的语音识别器选择基本标记是一个关键的决定,它会影响模型的性能。你有几个选项,每个选项都有自己的一组优点和挑战。

使用字符作为标记

一种直接的方法是预测字符,将单词序列转换为字符序列。例如:

THE CITY OF MONTREAL => ['T','H','E', '_', 'C','I','T','Y','_', 'O', 'F', '_, 'M','O','N','T','R','E','A','L']

此方法的优点和缺点包括标记总数少,有机会泛化到未见过的单词,以及预测长序列的挑战。

使用单词作为标记

预测完整的单词是另一种选择

THE CITY OF MONTREAL => ['THE','CITY','OF','MONTREAL']

优点包括输出序列短,但系统无法泛化到新词,并且可能分配给训练材料少的标记。

字节对编码 (BPE) 标记

一种折衷方法是字节对编码 (BPE),这是一种继承自数据压缩的技术。它为最频繁的字符序列分配标记。

THE CITY OF MONTREAL => ['THE', '▁CITY', '▁OF', '▁MO', 'NT', 'RE', 'AL']

BPE 根据最频繁的字符对查找标记,允许标记长度灵活。

需要多少 BPE 标记?

标记数量是一个超参数,取决于可用的语音数据。作为参考,对于像 LibriSpeech(1000 小时的英语句子)这样的数据集,1k 到 10k 个标记是合理的。

训练一个分词器

SpeechBrain 利用 SentencePiece 进行分词。要为你的训练转录查找标记,请运行以下代码:

cd speechbrain/templates/speech_recognition/Tokenizer
python train.py tokenizer.yaml

此步骤对于塑造语音识别器的行为至关重要。尝试不同的分词策略,找到最适合你的数据集和目标的策略。🚀🔍

让我们训练分词器

%cd /content/speechbrain/templates/speech_recognition/Tokenizer
!python train.py tokenizer.yaml

由于需要下载和准备数据,代码可能需要一些时间。与 SpeechBrain 中的所有其他 recipe 一样,我们有一个训练脚本 (train.py) 和一个超参数文件 (tokenizer.yaml)。让我们先仔细看看后者

# ############################################################################
# Tokenizer: subword BPE tokenizer with unigram 1K
# Training: Mini-LibriSpeech
# Authors:  Abdel Heba 2021
#           Mirco Ravanelli 2021
# ############################################################################


# Set up folders for reading from and writing to
data_folder: ../data
output_folder: ./save

# Path where data-specification files are stored
train_annotation: ../train.json
valid_annotation: ../valid.json
test_annotation: ../test.json

# Tokenizer parameters
token_type: unigram  # ["unigram", "bpe", "char"]
token_output: 1000  # index(blank/eos/bos/unk) = 0
character_coverage: 1.0
annotation_read: words # field to read

# Tokenizer object
tokenizer: !name:speechbrain.tokenizers.SentencePiece.SentencePiece
   model_dir: !ref <output_folder>
   vocab_size: !ref <token_output>
   annotation_train: !ref <train_annotation>
   annotation_read: !ref <annotation_read>
   model_type: !ref <token_type> # ["unigram", "bpe", "char"]
   character_coverage: !ref <character_coverage>
   annotation_list_to_check: [!ref <train_annotation>, !ref <valid_annotation>]
   annotation_format: json

分词器仅在训练标注上进行训练。我们在这里设置词汇大小为 1000。我们不使用标准的 BPE 算法,而是使用其基于 unigram 平滑的变体。更多信息请参见 sentencepiece。分词器将保存在指定的 output_folder 中。

现在让我们看看训练脚本 train.py

if __name__ == "__main__":

    # Load hyperparameters file with command-line overrides
    hparams_file, run_opts, overrides = sb.parse_arguments(sys.argv[1:])
    with open(hparams_file) as fin:
        hparams = load_hyperpyyaml(fin, overrides)

    # Create experiment directory
    sb.create_experiment_directory(
        experiment_directory=hparams["output_folder"],
        hyperparams_to_save=hparams_file,
        overrides=overrides,
    )

    # Data preparation, to be run on only one process.
    prepare_mini_librispeech(
        data_folder=hparams["data_folder"],
        save_json_train=hparams["train_annotation"],
        save_json_valid=hparams["valid_annotation"],
        save_json_test=hparams["test_annotation"],
    )

    # Train tokenizer
    hparams["tokenizer"]()

本质上,我们使用 prepare_mini_librispeech 脚本准备数据,然后运行包裹在 speechbrain.tokenizers.SentencePiece.SentencePiece 中的 sentencepiece 分词器。

让我们看看分词器生成的文件。如果你进入指定的输出文件夹 (Tokenizer/save),你会发现两个文件

  • 1000_unigram.model

  • 1000_unigram.vocab

第一个是包含对输入文本进行分词所需的所有信息的二进制文件。第二个是文本文件,报告已分配的标记列表(及其对数概率)

▁THE  -3.2458
S -3.36618
ED  -3.84476
▁ -3.91777
E -3.92101
▁AND  -3.92316
▁A  -3.97359
▁TO -4.00462
▁OF -4.08116
....

现在我来展示如何使用学习到的模型对文本进行分词

import torch
import sentencepiece as spm
sp = spm.SentencePieceProcessor()
sp.load("/content/speechbrain/templates/speech_recognition/Tokenizer/save/1000_unigram.model")

# Encode as pieces
print(sp.encode_as_pieces('THE CITY OF MONTREAL'))

# Encode as ids
print(sp.encode_as_ids('THE CITY OF MONTREAL'))

请注意,sentencepiece 分词器还为每个分配的标记分配一个唯一的索引。这些索引将对应于我们的语言模型和 ASR 神经网络的输出。

步骤 3:训练一个语言模型

语言模型 (LM) 在增强语音识别器的性能方面起着至关重要的作用。在本教程中,我们采用浅融合的概念,将语言信息整合到语音识别器的束搜索器中,以对部分假设进行重新评分。这涉及使用语言分数对语音识别器提供的部分假设进行评分,惩罚“不太可能”观察到的标记序列。

文本语料库

训练语言模型通常需要使用大型文本语料库,预测最可能的下一个标记。如果你的应用缺少大量文本语料库,你可以选择跳过此部分。此外,在大型文本语料库上训练语言模型需要大量的计算资源,因此如果需要,可以考虑利用预训练模型并进行微调。

出于本教程的目的,我们在 mini-librispeech 的训练转录上训练一个语言模型。请记住,这只是一个用于教育目的的简化演示。

训练一个 LM

我们将训练一个简单的基于 RNN 的语言模型,该模型根据先前的标记估计下一个标记。

SpeechBrain-Page-3 (1).png

要进行训练,请运行以下代码

!pip install datasets
%cd /content/speechbrain/templates/speech_recognition/LM
!python train.py RNNLM.yaml #--device='cpu'

从输出可以看出,训练损失和验证损失都随着时间的推移持续下降。

在深入了解代码之前,让我们探索一下指定的 output_folder 中生成的内容

  • train_log.txt: 此文件包含在每个 epoch 计算的统计信息(例如 train_loss, valid_loss)。

  • log.txt: 提供每个基本操作时间戳的详细日志。

  • env.log: 显示所有使用的依赖项及其相应的版本,有助于可复现性。

  • train.py, hyperparams.yaml: 实验文件及其相应超参数的副本,对于确保可复现性至关重要。

  • save: 存储学习模型的仓库。

save 文件夹中包含训练期间保存的检查点子文件夹,格式为 CKPT+data+time。通常,此处有两个检查点:最佳(即最旧的,代表最佳性能)和最新(即最近的)。如果只有一个检查点存在,则表示最后一个 epoch 也是最佳的。

每个检查点文件夹包含恢复训练所需的所有信息,包括模型、优化器、调度器、epoch 计数器等。RNNLM 模型的参数存储在 model.ckpt 文件中,使用二进制格式,可以使用 torch.load 读取。

教程的超参数部分提供了用于训练语言模型的设置的全面概述。以下是改进后的解释版本

超参数

有关完整的 RNNLM.yaml 文件的详细信息,请参阅此链接

在初始部分,定义了基本配置,例如随机种子、输出文件夹路径和训练日志器

seed: 2602
__set_seed: !apply:torch.manual_seed [!ref <seed>]
output_folder: !ref results/RNNLM/
save_folder: !ref <output_folder>/save
train_log: !ref <output_folder>/train_log.txt

后续部分概述了用于训练、验证和测试的文本语料库的路径

lm_train_data: data/train.txt
lm_valid_data: data/valid.txt
lm_test_data: data/test.txt

与其他 recipe 不同,语言模型 (LM) 直接处理大型原始文本语料库,无需 JSON/CSV 文件,利用 HuggingFace 数据集以提高效率。

接下来,详细介绍了训练日志器的设置和分词器的指定(使用上一步训练的分词器)

train_logger: !new:speechbrain.utils.train_logger.FileTrainLogger
    save_file: !ref <train_log>

tokenizer_file: ../Tokenizer/save/1000_unigram.model

继续,定义了必要的训练超参数,包括 epoch、批处理大小和学习率,以及关键的架构参数,例如嵌入维度、RNN 大小、层数和输出维度

number_of_epochs: 20
batch_size: 80
lr: 0.001
accu_steps: 1
ckpt_interval_minutes: 15

emb_dim: 256
rnn_size: 512
layers: 2
output_neurons: 1000

随后,介绍了训练语言模型所需的对象,包括 RNN 模型、损失函数、优化器和学习率调度器

model: !new:templates.speech_recognition.LM.custom_model.CustomModel
    embedding_dim: !ref <emb_dim>
    rnn_size: !ref <rnn_size>
    layers: !ref <layers>

compute_cost: !name:speechbrain.nnet.losses.nll_loss

optimizer: !name:torch.optim.Adam
    lr: !ref <lr>
    betas: (0.9, 0.98)
    eps: 0.000000001

lr_annealing: !new:speechbrain.nnet.schedulers.NewBobScheduler
    initial_value: !ref <lr>
    improvement_threshold: 0.0025
    annealing_factor: 0.8
    patient: 0

YAML 文件以指定 epoch 计数器、分词器和检查点结束

epoch_counter: !new:speechbrain.utils.epoch_loop.EpochCounter
    limit: !ref <number_of_epochs>

modules:
    model: !ref <model>

tokenizer: !new:sentencepiece.SentencePieceProcessor

checkpointer: !new:speechbrain.utils.checkpoints.Checkpointer
    checkpoints_dir: !ref <save_folder>
    recoverables:
        model: !ref <model>
        scheduler: !ref <lr_annealing>
        counter: !ref <epoch_counter>

pretrainer: !new:speechbrain.utils.parameter_transfer.Pretrainer
    loadables:
        tokenizer: !ref <tokenizer>
    paths:
        tokenizer: !ref <tokenizer_file>

pre-trainer 类促进了分词器对象与预训练分词器文件之间的连接。

实验文件

现在让我们看看 yaml 文件中声明的对象、函数和超参数如何在 train.py 中用于实现语言模型。

让我们从 train.py 的 main 函数开始

# Recipe begins!
if __name__ == "__main__":

    # Reading command line arguments
    hparams_file, run_opts, overrides = sb.parse_arguments(sys.argv[1:])

    # Initialize ddp (useful only for multi-GPU DDP training)
    sb.utils.distributed.ddp_init_group(run_opts)

    # Load hyperparameters file with command-line overrides
    with open(hparams_file) as fin:
        hparams = load_hyperpyyaml(fin, overrides)

    # Create experiment directory
    sb.create_experiment_directory(
        experiment_directory=hparams["output_folder"],
        hyperparams_to_save=hparams_file,
        overrides=overrides,
    )

我们在这里执行一些初步操作,例如解析命令行、初始化分布式数据并行(如果使用多个 GPU 则需要)、创建输出文件夹以及读取 yaml 文件。

在用 load_hyperpyyaml 读取 yaml 文件后,超参数文件中声明的所有对象都被初始化,并以字典形式可用(以及 yaml 文件中报告的其他函数和参数)。例如,我们将拥有 hparams['model']hparams['optimizer']hparams['batch_size'] 等等。

数据输入输出管道

然后我们调用一个特殊函数,该函数创建用于训练、验证和测试的数据集对象。

    # Create dataset objects "train", "valid", and "test"
    train_data, valid_data, test_data = dataio_prepare(hparams)

让我们仔细看看。

def dataio_prepare(hparams):
    """This function prepares the datasets to be used in the brain class.
    It also defines the data processing pipeline through user-defined functions.

    The language model is trained with the text files specified by the user in
    the hyperparameter file.

    Arguments
    ---------
    hparams : dict
        This dictionary is loaded from the `train.yaml` file, and it includes
        all the hyperparameters needed for dataset construction and loading.

    Returns
    -------
    datasets : list
        List containing "train", "valid", and "test" sets that correspond
        to the appropriate DynamicItemDataset object.
    """

    logging.info("generating datasets...")

    # Prepare datasets
    datasets = load_dataset(
        "text",
        data_files={
            "train": hparams["lm_train_data"],
            "valid": hparams["lm_valid_data"],
            "test": hparams["lm_test_data"],
        },
    )

    # Convert huggingface's dataset to DynamicItemDataset via a magical function
    train_data = sb.dataio.dataset.DynamicItemDataset.from_arrow_dataset(
        datasets["train"]
    )
    valid_data = sb.dataio.dataset.DynamicItemDataset.from_arrow_dataset(
        datasets["valid"]
    )
    test_data = sb.dataio.dataset.DynamicItemDataset.from_arrow_dataset(
        datasets["test"]
    )

    datasets = [train_data, valid_data, test_data]
    tokenizer = hparams["tokenizer"]

    # Define text processing pipeline. We start from the raw text and then
    # encode it using the tokenizer. The tokens with bos are used for feeding
    # the neural network, the tokens with eos for computing the cost function.
    @sb.utils.data_pipeline.takes("text")
    @sb.utils.data_pipeline.provides("text", "tokens_bos", "tokens_eos")
    def text_pipeline(text):
        yield text
        tokens_list = tokenizer.encode_as_ids(text)
        tokens_bos = torch.LongTensor([hparams["bos_index"]] + (tokens_list))
        yield tokens_bos
        tokens_eos = torch.LongTensor(tokens_list + [hparams["eos_index"]])
        yield tokens_eos

    sb.dataio.dataset.add_dynamic_item(datasets, text_pipeline)

    # 4. Set outputs to add into the batch. The batch variable will contain
    # all these fields (e.g, batch.id, batch.text, batch.tokens.bos,..)
    sb.dataio.dataset.set_output_keys(
        datasets, ["id", "text", "tokens_bos", "tokens_eos"],
    )
    return train_data, valid_data, test_data

第一部分只是从 HuggingFace 数据集到 SpeechBrain 中使用的 DynamicItemDataset 的转换。

你可以注意到我们公开了文本处理函数 text_pipeline,它接受一条句子的文本作为输入并以不同方式进行处理。

文本处理函数将原始文本转换为相应的标记(索引形式)。我们还创建了其他变量,例如前面带有句首 <bos> 标记的序列版本,以及最后一个元素是句尾 <eos> 标记的版本。它们的用处稍后会更清楚。

在返回数据集对象之前,dataio_prepare 指定了我们想要输出的键。正如我们稍后将看到的,这些键将在 brain 类中作为 batch.idbatch.textbatch.tokens_bos 等可用。有关数据加载器的更多信息,请参阅本教程

在定义数据集之后,主函数可以继续初始化 brain 类

    # Initialize the Brain object to prepare for LM training.
    lm_brain = LM(
        modules=hparams["modules"],
        opt_class=hparams["optimizer"],
        hparams=hparams,
        run_opts=run_opts,
        checkpointer=hparams["checkpointer"],
    )

brain 类实现了支持训练和验证循环所需的所有功能。其 fitevaluate 方法分别执行训练和测试

    lm_brain.fit(
        lm_brain.hparams.epoch_counter,
        train_data,
        valid_data,
        train_loader_kwargs=hparams["train_dataloader_opts"],
        valid_loader_kwargs=hparams["valid_dataloader_opts"],
    )

    # Load best checkpoint for evaluation
    test_stats = lm_brain.evaluate(
        test_data,
        min_key="loss",
        test_loader_kwargs=hparams["test_dataloader_opts"],
    )

训练和验证数据加载器作为输入提供给 fit 方法,而测试数据集则馈入 evaluate 方法。

现在让我们看看 brain 类中定义的最重要的方法。

前向计算

让我们从 forward 函数开始,该函数定义了将输入文本转换为输出预测所需的所有计算。

    def compute_forward(self, batch, stage):
        """Predicts the next word given the previous ones.

        Arguments
        ---------
        batch : PaddedBatch
            This batch object contains all the relevant tensors for computation.
        stage : sb.Stage
            One of sb.Stage.TRAIN, sb.Stage.VALID, or sb.Stage.TEST.

        Returns
        -------
        predictions : torch.Tensor
            A tensor containing the posterior probabilities (predictions).
        """
        batch = batch.to(self.device)
        tokens_bos, _ = batch.tokens_bos
        pred = self.hparams.model(tokens_bos)
        return pred

在这种情况下,计算链非常简单。我们只需要将 batch 放在正确的设备上,并将编码后的标记馈入模型。我们将带有 <bos> 的标记馈入模型。事实上,在添加 <bos> 标记时,我们将所有标记偏移一个元素。这样,我们的输入就对应于前一个标记,而我们的模型则试图预测当前的标记。

计算目标

现在让我们看看 compute_objectives 方法,它接受目标、预测并估计损失函数

    def compute_objectives(self, predictions, batch, stage):
        """Computes the loss given the predicted and targeted outputs.

        Arguments
        ---------
        predictions : torch.Tensor
            The posterior probabilities from `compute_forward`.
        batch : PaddedBatch
            This batch object contains all the relevant tensors for computation.
        stage : sb.Stage
            One of sb.Stage.TRAIN, sb.Stage.VALID, or sb.Stage.TEST.

        Returns
        -------
        loss : torch.Tensor
            A one-element tensor used for backpropagating the gradient.
        """
        batch = batch.to(self.device)
        tokens_eos, tokens_len = batch.tokens_eos
        loss = self.hparams.compute_cost(
            predictions, tokens_eos, length=tokens_len
        )
        return loss

预测是在 forward 方法中计算的。损失函数通过将这些预测与目标标记进行比较来评估。我们在这里使用带有特殊 <eos> 标记的标记,因为我们也想预测句子何时结束。

####其他方法 除了这两个重要的函数,我们还有一些其他方法由 brain 类使用。特别是,fit_batch 训练每个数据批次(通过使用 backward 方法计算梯度并使用 step one 进行更新)。on_stage_end 在每个阶段结束时调用(例如,在每个训练 epoch 结束时),主要负责统计管理、学习率退火和检查点。 有关 brain 类的更详细描述,请参阅此教程。有关检查点的更多信息,请查看此处

步骤 4:训练基于注意力的端到端语音识别器

现在是时候训练我们基于注意力的端到端语音识别器了。这个离线识别器采用复杂的架构,在编码器中使用了卷积、循环和全连接模型的组合,以及一个自回归 GRU 解码器。

编码器和解码器之间的关键连接是注意力机制。为了提高性能,最终的单词序列是通过束搜索获得的,并与之前训练的 RNNLM 相结合。

架构概述:

  • 编码器: 结合了卷积、循环和全连接模型。

  • 解码器: 自回归 GRU 解码器。

  • 注意力机制: 增强了编码器和解码器之间的信息流。

  • CTC(联结主义时间分类): 与基于注意力的系统联合训练,应用于编码器顶部。

  • 数据增强: 采用了增强数据的技术,提高了整体系统性能。

训练语音识别器

要训练语音识别器,请运行以下代码

%cd /content/speechbrain/templates/speech_recognition/ASR
!python train.py train.yaml --number_of_epochs=1  --batch_size=2  --enable_add_reverb=False --enable_add_noise=False #To speed up

在 Google Colab 上执行此代码可能需要相当长的时间。监控日志,你会观察到每个 epoch 后损失逐渐改善。

与 RNNLM 部分类似,指定的 output_folder 将包含前面讨论的文件和文件夹。此外,还会保存一个名为 wer.txt 的文件,提供每个测试句子实现的词错误率 (WER) 的全面报告。此文件不仅捕获 WER 值,还包括与真实转录的对齐信息,以进行更深入的分析

%WER 3.09 [ 1622 / 52576, 167 ins, 171 del, 1284 sub ]
%SER 33.66 [ 882 / 2620 ]
Scored 2620 sentences, 0 not present in hyp.
================================================================================
ALIGNMENTS

Format:
<utterance-id>, WER DETAILS
<eps> ; reference  ; on ; the ; first ;  line
  I   ;     S      ; =  ;  =  ;   S   ;   D  
 and  ; hypothesis ; on ; the ; third ; <eps>
================================================================================
672-122797-0033, %WER 0.00 [ 0 / 2, 0 ins, 0 del, 0 sub ]
A ; STORY
= ;   =  
A ; STORY
================================================================================
2094-142345-0041, %WER 0.00 [ 0 / 1, 0 ins, 0 del, 0 sub ]
DIRECTION
    =    
DIRECTION
================================================================================
2830-3980-0026, %WER 50.00 [ 1 / 2, 0 ins, 0 del, 1 sub ]
VERSE ; TWO
  S   ;  =
FIRST ; TWO
================================================================================
237-134500-0025, %WER 50.00 [ 1 / 2, 0 ins, 0 del, 1 sub ]
OH ;  EMIL
=  ;   S  
OH ; AMIEL
================================================================================
7127-75947-0012, %WER 0.00 [ 0 / 2, 0 ins, 0 del, 0 sub ]
INDEED ; AH
  =    ; =
INDEED ; AH
================================================================================

现在让我们仔细看看超参数 (train.yaml) 和实验脚本 (train.py)。

超参数

超参数文件以定义基本内容开始,例如种子和路径设置

# Seed needs to be set at top of yaml, before objects with parameters are instantiated
seed: 2602
__set_seed: !apply:torch.manual_seed [!ref <seed>]

# If you plan to train a system on an HPC cluster with a big dataset,
# we strongly suggest doing the following:
# 1- Compress the dataset in a single tar or zip file.
# 2- Copy your dataset locally (i.e., the local disk of the computing node).
# 3- Uncompress the dataset in the local folder.
# 4- Set data_folder with the local path
# Reading data from the local disk of the compute node (e.g. $SLURM_TMPDIR with SLURM-based clusters) is very important.
# It allows you to read the data much faster without slowing down the shared filesystem.

data_folder: ../data # In this case, data will be automatically downloaded here.
data_folder_noise: !ref <data_folder>/noise # The noisy sequencies for data augmentation will automatically be downloaded here.
data_folder_rir: !ref <data_folder>/rir # The impulse responses used for data augmentation will automatically be downloaded here.

# Data for augmentation
NOISE_DATASET_URL: https://www.dropbox.com/scl/fi/a09pj97s5ifan81dqhi4n/noises.zip?rlkey=j8b0n9kdjdr32o1f06t0cw5b7&dl=1
RIR_DATASET_URL: https://www.dropbox.com/scl/fi/linhy77c36mu10965a836/RIRs.zip?rlkey=pg9cu8vrpn2u173vhiqyu743u&dl=1

output_folder: !ref results/CRDNN_BPE_960h_LM/<seed>
test_wer_file: !ref <output_folder>/wer_test.txt
save_folder: !ref <output_folder>/save
train_log: !ref <output_folder>/train_log.txt

# Language model (LM) pretraining
# NB: To avoid mismatch, the speech recognizer must be trained with the same
# tokenizer used for LM training. Here, we download everything from the
# speechbrain HuggingFace repository. However, a local path pointing to a
# directory containing the lm.ckpt and tokenizer.ckpt may also be specified
# instead. E.g if you want to use your own LM / tokenizer.
pretrained_path: speechbrain/asr-crdnn-rnnlm-librispeech


# Path where data manifest files will be stored. The data manifest files are created by the
# data preparation script
train_annotation: ../train.json
valid_annotation: ../valid.json
test_annotation: ../test.json
noise_annotation: ../noise.csv
rir_annotation: ../rir.csv

skip_prep: False

# The train logger writes training statistics to a file, as well as stdout.
train_logger: !new:speechbrain.utils.train_logger.FileTrainLogger
    save_file: !ref <train_log>

data_folder 对应于 mini-librispeech 存储的路径。如果不可用,mini-librispeech 数据集将在此处下载。如前所述,该脚本还支持数据增强。为此,我们使用开放 rir 数据集的脉冲响应和噪声序列(同样,如果不可用,将在此处下载)。

我们还指定了语言模型的保存文件夹。在这种情况下,我们使用 HuggingFace 上提供的官方预训练语言模型,但你可以更改它并使用上一步训练的模型(你应该指向保存最佳 model.cpkt 的文件夹中的检查点)。重要的是用于 LM 和用于训练语音识别器的标记集完全匹配。

我们还需要指定用于训练、验证和测试的数据清单文件。如果这些文件不可用,将由 train.py 中调用的数据准备脚本创建。

之后,我们定义了一系列用于训练、特征提取、模型定义和解码的参数

# Training parameters
number_of_epochs: 15
number_of_ctc_epochs: 5
batch_size: 8
lr: 1.0
ctc_weight: 0.5
sorting: ascending
ckpt_interval_minutes: 15 # save checkpoint every N min
label_smoothing: 0.1

# Dataloader options
train_dataloader_opts:
    batch_size: !ref <batch_size>

valid_dataloader_opts:
    batch_size: !ref <batch_size>

test_dataloader_opts:
    batch_size: !ref <batch_size>


# Feature parameters
sample_rate: 16000
n_fft: 400
n_mels: 40

# Model parameters
activation: !name:torch.nn.LeakyReLU
dropout: 0.15
cnn_blocks: 2
cnn_channels: (128, 256)
inter_layer_pooling_size: (2, 2)
cnn_kernelsize: (3, 3)
time_pooling_size: 4
rnn_class: !name:speechbrain.nnet.RNN.LSTM
rnn_layers: 4
rnn_neurons: 1024
rnn_bidirectional: True
dnn_blocks: 2
dnn_neurons: 512
emb_size: 128
dec_neurons: 1024
output_neurons: 1000  # Number of tokens (same as LM)
blank_index: 0
bos_index: 0
eos_index: 0
unk_index: 0

# Decoding parameters
min_decode_ratio: 0.0
max_decode_ratio: 1.0
valid_beam_size: 8
test_beam_size: 80
eos_threshold: 1.5
using_max_attn_shift: True
max_attn_shift: 240
lm_weight: 0.50
ctc_weight_decode: 0.0
coverage_penalty: 1.5
temperature: 1.25
temperature_lm: 1.25

例如,我们定义了 epoch 数量、初始学习率、批处理大小、CTC 损失权重以及许多其他参数。

通过将 sorting 设置为 ascending,我们在创建批次之前按升序对所有句子进行排序。这最大程度地减少了零填充的需要,从而在不损失性能的情况下加速训练(至少在这个任务和这个模型上)。

定义了许多其他参数,例如数据增强参数。要了解所有这些参数的具体含义,可以参考使用此超参数的函数/类的文档字符串。

在下一个块中,我们定义了实现语音识别器所需的最重要的类

# The first object passed to the Brain class is this "Epoch Counter"
# which is saved by the Checkpointer so that training can be resumed
# if it gets interrupted at any point.
epoch_counter: !new:speechbrain.utils.epoch_loop.EpochCounter
    limit: !ref <number_of_epochs>

# Feature extraction
compute_features: !new:speechbrain.lobes.features.Fbank
    sample_rate: !ref <sample_rate>
    n_fft: !ref <n_fft>
    n_mels: !ref <n_mels>

# Feature normalization (mean and std)
normalize: !new:speechbrain.processing.features.InputNormalization
    norm_type: global

# Added noise and reverb come from OpenRIR dataset, automatically
# downloaded and prepared with this Environmental Corruption class.
env_corrupt: !new:speechbrain.lobes.augment.EnvCorrupt
    openrir_folder: !ref <data_folder_rirs>
    babble_prob: 0.0
    reverb_prob: 0.0
    noise_prob: 1.0
    noise_snr_low: 0
    noise_snr_high: 15

# Adds speech change + time and frequnecy dropouts (time-domain implementation).
augmentation: !new:speechbrain.lobes.augment.TimeDomainSpecAugment
    sample_rate: !ref <sample_rate>
    speeds: [95, 100, 105]

# The CRDNN model is an encoder that combines CNNs, RNNs, and DNNs.
encoder: !new:speechbrain.lobes.models.CRDNN.CRDNN
    input_shape: [null, null, !ref <n_mels>]
    activation: !ref <activation>
    dropout: !ref <dropout>
    cnn_blocks: !ref <cnn_blocks>
    cnn_channels: !ref <cnn_channels>
    cnn_kernelsize: !ref <cnn_kernelsize>
    inter_layer_pooling_size: !ref <inter_layer_pooling_size>
    time_pooling: True
    using_2d_pooling: False
    time_pooling_size: !ref <time_pooling_size>
    rnn_class: !ref <rnn_class>
    rnn_layers: !ref <rnn_layers>
    rnn_neurons: !ref <rnn_neurons>
    rnn_bidirectional: !ref <rnn_bidirectional>
    rnn_re_init: True
    dnn_blocks: !ref <dnn_blocks>
    dnn_neurons: !ref <dnn_neurons>
    use_rnnp: False

# Embedding (from indexes to an embedding space of dimension emb_size).
embedding: !new:speechbrain.nnet.embedding.Embedding
    num_embeddings: !ref <output_neurons>
    embedding_dim: !ref <emb_size>

# Attention-based RNN decoder.
decoder: !new:speechbrain.nnet.RNN.AttentionalRNNDecoder
    enc_dim: !ref <dnn_neurons>
    input_size: !ref <emb_size>
    rnn_type: gru
    attn_type: location
    hidden_size: !ref <dec_neurons>
    attn_dim: 1024
    num_layers: 1
    scaling: 1.0
    channels: 10
    kernel_size: 100
    re_init: True
    dropout: !ref <dropout>

# Linear transformation on the top of the encoder.
ctc_lin: !new:speechbrain.nnet.linear.Linear
    input_size: !ref <dnn_neurons>
    n_neurons: !ref <output_neurons>

# Linear transformation on the top of the decoder.
seq_lin: !new:speechbrain.nnet.linear.Linear
    input_size: !ref <dec_neurons>
    n_neurons: !ref <output_neurons>

# Final softmax (for log posteriors computation).
log_softmax: !new:speechbrain.nnet.activations.Softmax
    apply_log: True

# Cost definition for the CTC part.
ctc_cost: !name:speechbrain.nnet.losses.ctc_loss
    blank_index: !ref <blank_index>

# Tokenizer initialization
tokenizer: !new:sentencepiece.SentencePieceProcessor

# Objects in "modules" dict will have their parameters moved to the correct
# device, as well as having train()/eval() called on them by the Brain class
modules:
    encoder: !ref <encoder>
    embedding: !ref <embedding>
    decoder: !ref <decoder>
    ctc_lin: !ref <ctc_lin>
    seq_lin: !ref <seq_lin>
    normalize: !ref <normalize>
    env_corrupt: !ref <env_corrupt>
    lm_model: !ref <lm_model>

# Gathering all the submodels in a single model object.
model: !new:torch.nn.ModuleList
    - - !ref <encoder>
      - !ref <embedding>
      - !ref <decoder>
      - !ref <ctc_lin>
      - !ref <seq_lin>

# This is the RNNLM that is used according to the Huggingface repository
# NB: It has to match the pre-trained RNNLM!!
lm_model: !new:speechbrain.lobes.models.RNNLM.RNNLM
    output_neurons: !ref <output_neurons>
    embedding_dim: !ref <emb_size>
    activation: !name:torch.nn.LeakyReLU
    dropout: 0.0
    rnn_layers: 2
    rnn_neurons: 2048
    dnn_blocks: 1
    dnn_neurons: 512
    return_hidden: True  # For inference

例如,我们定义了计算特征并对其进行归一化的函数。我们定义了环境污染和数据增强的类(请参阅此教程),以及编码器、解码器和语音识别器所需的其他模型的架构。

然后,我们报告束搜索的参数

# Define scorers for beam search

# If ctc_scorer is set, the decoder uses CTC + attention beamsearch. This
# improves the performance, but slows down decoding.
ctc_scorer: !new:speechbrain.decoders.scorer.CTCScorer
    eos_index: !ref <eos_index>
    blank_index: !ref <blank_index>
    ctc_fc: !ref <ctc_lin>

# If coverage_scorer is set, coverage penalty is applied based on accumulated
# attention weights during beamsearch.
coverage_scorer: !new:speechbrain.decoders.scorer.CoverageScorer
    vocab_size: !ref <output_neurons>

# If the lm_scorer is set, a language model
# is applied (with a weight specified in scorer).
rnnlm_scorer: !new:speechbrain.decoders.scorer.RNNLMScorer
    language_model: !ref <lm_model>
    temperature: !ref <temperature_lm>

# Gathering all scorers in a scorer instance for beamsearch:
# - full_scorers are scorers which score on full vocab set, while partial_scorers
# are scorers which score on pruned tokens.
# - The number of pruned tokens is decided by scorer_beam_scale * beam_size.
# - For some scorers like ctc_scorer, ngramlm_scorer, putting them
# into full_scorers list would be too heavy. partial_scorers are more
# efficient because they score on pruned tokens at little cost of
# performance drop. For other scorers, please see the speechbrain.decoders.scorer.
test_scorer: !new:speechbrain.decoders.scorer.ScorerBuilder
    scorer_beam_scale: 1.5
    full_scorers: [
        !ref <rnnlm_scorer>,
        !ref <coverage_scorer>]
    partial_scorers: [!ref <ctc_scorer>]
    weights:
        rnnlm: !ref <lm_weight>
        coverage: !ref <coverage_penalty>
        ctc: !ref <ctc_weight_decode>

valid_scorer: !new:speechbrain.decoders.scorer.ScorerBuilder
    full_scorers: [!ref <coverage_scorer>]
    weights:
        coverage: !ref <coverage_penalty>

# Beamsearch is applied on the top of the decoder. For a description of
# the other parameters, please see the speechbrain.decoders.S2SRNNBeamSearcher.

# It makes sense to have a lighter search during validation. In this case,
# we don't use scorers during decoding.
valid_search: !new:speechbrain.decoders.S2SRNNBeamSearcher
    embedding: !ref <embedding>
    decoder: !ref <decoder>
    linear: !ref <seq_lin>
    bos_index: !ref <bos_index>
    eos_index: !ref <eos_index>
    min_decode_ratio: !ref <min_decode_ratio>
    max_decode_ratio: !ref <max_decode_ratio>
    beam_size: !ref <valid_beam_size>
    eos_threshold: !ref <eos_threshold>
    using_max_attn_shift: !ref <using_max_attn_shift>
    max_attn_shift: !ref <max_attn_shift>
    temperature: !ref <temperature>
    scorer: !ref <valid_scorer>

# The final decoding on the test set can be more computationally demanding.
# In this case, we use the LM + CTC probabilities during decoding as well,
# which are defined in scorer.
# Please, remove scorer if you need a faster decoder.
test_search: !new:speechbrain.decoders.S2SRNNBeamSearcher
    embedding: !ref <embedding>
    decoder: !ref <decoder>
    linear: !ref <seq_lin>
    bos_index: !ref <bos_index>
    eos_index: !ref <eos_index>
    min_decode_ratio: !ref <min_decode_ratio>
    max_decode_ratio: !ref <max_decode_ratio>
    beam_size: !ref <test_beam_size>
    eos_threshold: !ref <eos_threshold>
    using_max_attn_shift: !ref <using_max_attn_shift>
    max_attn_shift: !ref <max_attn_shift>
    temperature: !ref <temperature>
    scorer: !ref <test_scorer>

我们在这里对验证和测试束搜索采用不同的超参数。特别是,验证阶段使用较小的束大小。原因是验证在每个 epoch 结束时进行,因此应该快速完成。而评估仅在结束时进行一次,我们可以更准确。

最后,我们声明训练 recipe 所需的最后一个对象,例如 lr_annealing、optimizer、checkpointer 等

 This function manages learning rate annealing over the epochs.
# We here use the NewBoB algorithm, that anneals the learning rate if
# the improvements over two consecutive epochs is less than the defined
# threshold.
lr_annealing: !new:speechbrain.nnet.schedulers.NewBobScheduler
    initial_value: !ref <lr>
    improvement_threshold: 0.0025
    annealing_factor: 0.8
    patient: 0

# This optimizer will be constructed by the Brain class after all parameters
# are moved to the correct device. Then it will be added to the checkpointer.
opt_class: !name:torch.optim.Adadelta
    lr: !ref <lr>
    rho: 0.95
    eps: 1.e-8

# Functions that compute the statistics to track during the validation step.
error_rate_computer: !name:speechbrain.utils.metric_stats.ErrorRateStats

cer_computer: !name:speechbrain.utils.metric_stats.ErrorRateStats
    split_tokens: True

# This object is used for saving the state of training both so that it
# can be resumed if it gets interrupted, and also so that the best checkpoint
# can be later loaded for evaluation or inference.
checkpointer: !new:speechbrain.utils.checkpoints.Checkpointer
    checkpoints_dir: !ref <save_folder>
    recoverables:
        model: !ref <model>
        scheduler: !ref <lr_annealing>
        normalizer: !ref <normalize>
        counter: !ref <epoch_counter>

# This object is used to pretrain the language model and the tokenizers
# (defined above). In this case, we also pretrain the ASR model (to make
# sure the model converges on a small amount of data)
pretrainer: !new:speechbrain.utils.parameter_transfer.Pretrainer
    collect_in: !ref <save_folder>
    loadables:
        lm: !ref <lm_model>
        tokenizer: !ref <tokenizer>
        model: !ref <model>
    paths:
        lm: !ref <pretrained_path>/lm.ckpt
        tokenizer: !ref <pretrained_path>/tokenizer.ckpt
        model: !ref <pretrained_path>/asr.ckpt

最后一个对象是 pretrainer,它将语言模型、分词器和声学语音识别模型与其用于预训练的相应文件连接起来。我们在这里也预训练声学模型。对于如此小的数据集,端到端语音识别器很难收敛,因此我们使用另一个模型对其进行预训练(在大型数据集上训练时应跳过此部分)。

实验文件

现在让我们看看 train.py 中 yaml 文件中声明的不同元素是如何连接的。训练脚本与语言模型已描述的脚本密切相关。

main 函数从实现基本功能开始,例如解析命令行、初始化分布式数据并行(多 GPU 训练需要)和读取 yaml 文件。


if __name__ == "__main__":

    # Reading command line arguments
    hparams_file, run_opts, overrides = sb.parse_arguments(sys.argv[1:])

    # Initialize ddp (useful only for multi-GPU DDP training)
    sb.utils.distributed.ddp_init_group(run_opts)

    # Load hyperparameters file with command-line overrides
    with open(hparams_file) as fin:
        hparams = load_hyperpyyaml(fin, overrides)

    # Create experiment directory
    sb.create_experiment_directory(
        experiment_directory=hparams["output_folder"],
        hyperparams_to_save=hparams_file,
        overrides=overrides,
    )

    # Data preparation, to be run on only one process.
    if not hparams["skip_prep"]:
        sb.utils.distributed.run_on_main(
            prepare_mini_librispeech,
            kwargs={
                "data_folder": hparams["data_folder"],
                "save_json_train": hparams["train_annotation"],
                "save_json_valid": hparams["valid_annotation"],
                "save_json_test": hparams["test_annotation"],
            },
        )
    sb.utils.distributed.run_on_main(hparams["prepare_noise_data"])
    sb.utils.distributed.run_on_main(hparams["prepare_rir_data"])

使用 load_hyperpyyaml 函数读取 yaml 文件。读取后,所有声明的对象都将初始化并与 hparams 字典中的其他函数和变量一起可用(例如 hparams['model']hparams['test_search']hparams['batch_size'])。

之后,我们运行数据准备,其目标是创建数据清单文件(如果尚不可用)。此操作需要在磁盘上写入一些文件。因此,我们必须使用 sb.utils.distributed.run_on_main 来确保此操作仅由主进程执行。这避免了使用多 GPU 和 DDP 时可能发生的冲突。有关 Speechbrai 中多 GPU 训练的更多信息,请参阅此教程

数据输入输出管道

此时,我们可以创建我们将用于训练、验证和测试循环的数据集对象

    # We can now directly create the datasets for training, valid, and test
    datasets = dataio_prepare(hparams)

此函数允许用户完全自定义数据读取管道。让我们仔细看看它

def dataio_prepare(hparams):
    """This function prepares the datasets to be used in the brain class.
    It also defines the data processing pipeline through user-defined functions.


    Arguments
    ---------
    hparams : dict
        This dictionary is loaded from the `train.yaml` file, and it includes
        all the hyperparameters needed for dataset construction and loading.

    Returns
    -------
    datasets : dict
        Dictionary containing "train", "valid", and "test" keys that correspond
        to the DynamicItemDataset objects.
    """
    # Define audio pipeline. In this case, we simply read the path contained
    # in the variable wav with the audio reader.
    @sb.utils.data_pipeline.takes("wav")
    @sb.utils.data_pipeline.provides("sig")
    def audio_pipeline(wav):
        """Load the audio signal. This is done on the CPU in the `collate_fn`."""
        sig = sb.dataio.dataio.read_audio(wav)
        return sig

    # Define text processing pipeline. We start from the raw text and then
    # encode it using the tokenizer. The tokens with BOS are used for feeding
    # decoder during training, the tokens with EOS for computing the cost function.
    # The tokens without BOS or EOS is for computing CTC loss.
    @sb.utils.data_pipeline.takes("words")
    @sb.utils.data_pipeline.provides(
        "words", "tokens_list", "tokens_bos", "tokens_eos", "tokens"
    )
    def text_pipeline(words):
        """Processes the transcriptions to generate proper labels"""
        yield words
        tokens_list = hparams["tokenizer"].encode_as_ids(words)
        yield tokens_list
        tokens_bos = torch.LongTensor([hparams["bos_index"]] + (tokens_list))
        yield tokens_bos
        tokens_eos = torch.LongTensor(tokens_list + [hparams["eos_index"]])
        yield tokens_eos
        tokens = torch.LongTensor(tokens_list)
        yield tokens

    # Define datasets from json data manifest file
    # Define datasets sorted by ascending lengths for efficiency
    datasets = {}
    data_folder = hparams["data_folder"]
    for dataset in ["train", "valid", "test"]:
        datasets[dataset] = sb.dataio.dataset.DynamicItemDataset.from_json(
            json_path=hparams[f"{dataset}_annotation"],
            replacements={"data_root": data_folder},
            dynamic_items=[audio_pipeline, text_pipeline],
            output_keys=[
                "id",
                "sig",
                "words",
                "tokens_bos",
                "tokens_eos",
                "tokens",
            ],
        )
        hparams[f"{dataset}_dataloader_opts"]["shuffle"] = False

    # Sorting traiing data with ascending order makes the code  much
    # faster  because we minimize zero-padding. In most of the cases, this
    # does not harm the performance.
    if hparams["sorting"] == "ascending":
        datasets["train"] = datasets["train"].filtered_sorted(sort_key="length")
        hparams["train_dataloader_opts"]["shuffle"] = False

    elif hparams["sorting"] == "descending":
        datasets["train"] = datasets["train"].filtered_sorted(
            sort_key="length", reverse=True
        )
        hparams["train_dataloader_opts"]["shuffle"] = False

    elif hparams["sorting"] == "random":
        hparams["train_dataloader_opts"]["shuffle"] = True
        pass

    else:
        raise NotImplementedError(
            "sorting must be random, ascending or descending"
        )
    return datasets

dataio_prepare 中,我们定义了用于处理 JSON 文件中定义的条目的子函数。第一个函数,名为 audio_pipeline,接受音频信号的路径 (wav) 并读取它。它返回一个包含读取的语音句子的张量。此函数的输入(即 wav)必须与数据清单文件中相应键的名称相同

  "1867-154075-0032": {
    "wav": "{data_root}/LibriSpeech/train-clean-5/1867/154075/1867-154075-0032.flac",
    "length": 16.09,
    "words": "AND HE BRUSHED A HAND ACROSS HIS FOREHEAD AND WAS INSTANTLY HIMSELF CALM AND COOL VERY WELL THEN IT SEEMS I'VE MADE AN ASS OF MYSELF BUT I'LL TRY TO MAKE UP FOR IT NOW WHAT ABOUT CAROLINE"
  },

类似地,我们定义了另一个名为 text_pipeline 的函数,用于处理信号转录并将其转换为定义的模型可用的格式。该函数读取 JSON 文件中定义的字符串 words 并对其进行分词(输出每个标记的索引)。它返回前面带有特殊句首 <bos> 标记的标记序列,以及末尾带有句尾 <eos> 标记的版本。我们稍后会看到为什么需要这些额外的元素。

然后我们创建 DynamicItemDataset 并将其与上面定义的处理函数连接起来。我们定义了所需的输出键。这些键将在 brain 类中的 batch 变量中可用,如下所示:

  • batch.id

  • batch.sig

  • batch.words

  • batch.tokens_bos

  • batch.tokens_eos

  • batch.tokens

dataio_prepare 函数的最后一部分管理数据排序。在这种情况下,我们按升序对数据进行排序,以最大程度地减少零填充并加速训练。有关数据加载器的更多信息,请参阅此教程

在定义 dataio 函数后,我们对语言模型、ASR 模型和分词器进行预训练

    run_on_main(hparams["pretrainer"].collect_files)
    hparams["pretrainer"].load_collected(device=run_opts["device"])

我们这里使用 run_on_main wrapper,因为 collect_files 方法可能需要从网络下载预训练模型。即使使用多 GPU 和 DDP,此操作也应仅由单个进程执行)。

此时,我们初始化 Brain 类并使用它运行训练和评估


    # Trainer initialization
    asr_brain = ASR(
        modules=hparams["modules"],
        opt_class=hparams["opt_class"],
        hparams=hparams,
        run_opts=run_opts,
        checkpointer=hparams["checkpointer"],
    )

    # Training
    asr_brain.fit(
        asr_brain.hparams.epoch_counter,
        datasets["train"],
        datasets["valid"],
        train_loader_kwargs=hparams["train_dataloader_opts"],
        valid_loader_kwargs=hparams["valid_dataloader_opts"],
    )

    # Load best checkpoint for evaluation
    test_stats = asr_brain.evaluate(
        test_set=datasets["test"],
        min_key="WER",
        test_loader_kwargs=hparams["test_dataloader_opts"],
    )

有关 Brain 类如何工作的更多信息,请参阅此教程 注意,fitevaluate 方法也将数据集对象作为输入。从这个数据集中,会自动创建一个 pytorch dataloader。后者创建用于训练和评估的 batch。

当采样到不同长度的语音句子时,会执行零填充。为了跟踪每个 batch 中每个句子的实际长度,数据加载器也会返回一个包含相对长度的特殊张量。例如,假设 batch.sig[0] 是一个包含输入波形的变量,表示为一个 [batch, time] 张量

tensor([[1, 1, 0, 0],
        [1, 1, 1, 0],
        [1, 1, 0, 0]])

batch.sig[1] 将包含以下相对长度

tensor([0.5000, 0.7500, 1.0000])

有了这些信息,我们可以从某些计算(例如特征归一化、统计池化、损失等)中排除零填充的步骤。

为什么使用相对长度而不是绝对长度?

优先使用相对长度而不是绝对长度源于神经网络中时间分辨率的动态性质。包括池化、步长卷积、转置卷积、FFT 计算等在内的多种操作都有可能改变序列中的时间步长数量。

通过采用相对位置技巧,在神经网络计算的每个阶段计算实际时间步长变得更加灵活。这是通过将相对长度乘以张量的总长度来实现的。因此,这种方法能够适应各种网络操作引入的时间分辨率变化,确保在整个神经网络的计算过程中,时间信息具有更稳健和适应性更强的表示。

前向计算

在 Brain 类中,我们必须定义一些重要的方法,例如:

  • compute_forward,它指定了将输入波形转换为输出后验概率所需的所有计算。

  • compute_objective,它根据标签和模型执行的预测计算损失函数。

让我们先看看 compute_forward

    def compute_forward(self, batch, stage):
        """Runs all the computation of the CTC + seq2seq ASR. It returns the
        posterior probabilities of the CTC and seq2seq networks.

        Arguments
        ---------
        batch : PaddedBatch
            This batch object contains all the relevant tensors for computation.
        stage : sb.Stage
            One of sb.Stage.TRAIN, sb.Stage.VALID, or sb.Stage.TEST.

        Returns
        -------
        predictions : dict
            At training time it returns predicted seq2seq log probabilities.
            If needed it also returns the ctc output log probabilities.
            At validation/test time, it returns the predicted tokens as well.
        """
        # We first move the batch to the appropriate device.
        batch = batch.to(self.device)

        feats, self.feat_lens = self.prepare_features(stage, batch.sig)
        tokens_bos, _ = self.prepare_tokens(stage, batch.tokens_bos)

        # Running the encoder (prevent propagation to feature extraction)
        encoded_signal = self.modules.encoder(feats.detach())

        # Embed tokens and pass tokens & encoded signal to decoder
        embedded_tokens = self.modules.embedding(tokens_bos.detach())
        decoder_outputs, _ = self.modules.decoder(
            embedded_tokens, encoded_signal, self.feat_lens
        )

        # Output layer for seq2seq log-probabilities
        logits = self.modules.seq_lin(decoder_outputs)
        predictions = {"seq_logprobs": self.hparams.log_softmax(logits)}

        if self.is_ctc_active(stage):
            # Output layer for ctc log-probabilities
            ctc_logits = self.modules.ctc_lin(encoded_signal)
            predictions["ctc_logprobs"] = self.hparams.log_softmax(ctc_logits)

        elif stage != sb.Stage.TRAIN:
            if stage == sb.Stage.VALID:
                hyps, _, _, _ = self.hparams.valid_search(
                    encoded_signal, self.feat_lens
                )
            elif stage == sb.Stage.TEST:
                hyps, _, _, _ = self.hparams.test_search(
                    encoded_signal, self.feat_lens
                )

            predictions["tokens"] = hyps

        return predictions

该函数接受 batch 变量和当前阶段(可以是 sb.Stage.TRAINsb.Stage.VALIDsb.Stage.TEST)。然后我们将 batch 放在正确的设备上,计算特征,并使用我们的 CRDNN 编码器对其进行编码。有关特征计算的更多信息,请参阅此教程,有关语音增强的更多详细信息,请参阅此处。之后,我们将编码的状态输入到基于注意力的自回归解码器中,该解码器对标记执行一些预测。在验证和测试阶段,我们在标记预测之上应用束搜索。我们的系统在编码器之上应用额外的 CTC 损失。如果需要,可以在 N 个 epoch 后关闭 CTC。

计算目标

现在让我们看看 compute_objectives 函数


    def compute_objectives(self, predictions, batch, stage):
        """Computes the loss given the predicted and targeted outputs. We here
        do multi-task learning and the loss is a weighted sum of the ctc + seq2seq
        costs.

        Arguments
        ---------
        predictions : dict
            The output dict from `compute_forward`.
        batch : PaddedBatch
            This batch object contains all the relevant tensors for computation.
        stage : sb.Stage
            One of sb.Stage.TRAIN, sb.Stage.VALID, or sb.Stage.TEST.

        Returns
        -------
        loss : torch.Tensor
            A one-element tensor used for backpropagating the gradient.
        """

        # Compute sequence loss against targets with EOS
        tokens_eos, tokens_eos_lens = self.prepare_tokens(
            stage, batch.tokens_eos
        )
        loss = sb.nnet.losses.nll_loss(
            log_probabilities=predictions["seq_logprobs"],
            targets=tokens_eos,
            length=tokens_eos_lens,
            label_smoothing=self.hparams.label_smoothing,
        )

        # Add ctc loss if necessary. The total cost is a weighted sum of
        # ctc loss + seq2seq loss
        if self.is_ctc_active(stage):
            # Load tokens without EOS as CTC targets
            tokens, tokens_lens = self.prepare_tokens(stage, batch.tokens)
            loss_ctc = self.hparams.ctc_cost(
                predictions["ctc_logprobs"], tokens, self.feat_lens, tokens_lens
            )
            loss *= 1 - self.hparams.ctc_weight
            loss += self.hparams.ctc_weight * loss_ctc

        if stage != sb.Stage.TRAIN:
            # Converted predicted tokens from indexes to words
            predicted_words = [
                self.hparams.tokenizer.decode_ids(prediction).split(" ")
                for prediction in predictions["tokens"]
            ]
            target_words = [words.split(" ") for words in batch.words]

            # Monitor word error rate and character error rated at
            # valid and test time.
            self.wer_metric.append(batch.id, predicted_words, target_words)
            self.cer_metric.append(batch.id, predicted_words, target_words)

        return loss

根据预测和目标,我们计算负对数似然损失(NLL),如果需要,还计算联结主义时间分类(CTC)损失。这两个损失通过权重(ctc_weight)组合。在验证或测试阶段,我们计算词错误率(WER)和字符错误率(CER)。

其他方法

除了主要函数 forwardcompute_objective,代码还包含 on_stage_starton_stage_end 函数。前者初始化统计对象,例如词错误率 (WER) 和字符错误率 (CER)。后者负责监督几个关键方面

  • 统计更新: 管理训练期间统计数据的更新。

  • 学习率退火: 处理跨 epoch 的学习率调整。

  • 日志记录: 促进训练过程中关键信息的日志记录。

  • 检查点: 管理检查点的创建和存储,以便恢复训练。

通过集成这些函数,代码确保了语音识别系统的全面高效的训练管道。

就这样了。你只需运行代码并训练你的语音识别器。

预训练和微调

在从头开始训练可能不是最佳选择的情况下,从预训练模型开始并对其进行微调的选择变得很有价值。

需要注意的是,为了使这种方法无缝地工作,你的模型的架构必须与预训练模型的架构完全匹配。

实现这一点的一种便捷方法是使用 YAML 文件中的 pretrainer 类。如果你想预训练语音识别器的编码器,可以使用以下代码片段

pretrainer: !new:speechbrain.utils.parameter_transfer.Pretrainer
 loadables:
     encoder: !ref <encoder>
 paths:
   encoder: !ref <encoder_ptfile>

此处,!ref <encoder> 指向 YAML 文件中之前定义的编码器模型,而 encoder_ptfile 表示预训练模型存储的路径。

要执行预训练过程,请确保在 train.py 文件中调用 pre-trainer

run_on_main(hparams["pretrainer"].collect_files)
hparams["pretrainer"].load_collected(device=run_opts["device"])

在 Brain 类的 fit 方法之前调用此函数至关重要。

如需更全面的理解和实际示例,请参阅我们关于预训练和微调的教程。此资源提供了关于如何有效利用预训练模型在你的语音识别系统中的详细见解。

步骤 5:推理

至此,我们可以使用训练好的语音识别器了。对于这种类型的 ASR 模型,SpeechBrain 提供了一些类(请在此查看),例如 EncoderDecoderASR 类,可以简化推理过程。例如,我们可以仅用 4 行代码转录我们 HuggingFace 仓库中托管的预训练模型的音频文件

from speechbrain.inference.ASR import EncoderDecoderASR

asr_model = EncoderDecoderASR.from_hparams(source="speechbrain/asr-crdnn-rnnlm-librispeech", savedir="/content/pretrained_model")
audio_file = 'speechbrain/asr-crdnn-rnnlm-librispeech/example.wav'
asr_model.transcribe_file(audio_file)

但是,这如何与你的自定义 ASR 系统一起使用呢?

利用你的自定义语音识别器

此时,您可以使用两种选项在您的数据上训练和部署您的语音识别器

  1. 利用可用接口(例如,EncoderDecoderASR

    • 被认为是最优雅和便捷的选项。

    • 您的模型应遵循某些约束,以便与提议的接口无缝契合。

    • 这种方法简化了您的自定义 ASR 模型与现有接口的集成,提高了适应性和可维护性。

  2. 构建您自己的自定义接口

    • 精心设计一个完全适合您自定义 ASR 模型的接口。

    • 提供灵活性以满足独特的要求和规范。

    • 非常适合现有接口无法完全满足您需求的场景。

注意:这些解决方案不仅限于 ASR,还可以扩展到其他任务,如说话人识别和声源分离。

使用 EndoderDecoderASR 接口

EncoderDecoderASR 类接口允许您将训练好的模型与训练方案解耦,并通过几行代码对任何新的音频文件进行推断(或编码)。该类具有以下方法:

  • encode_batch:将编码器应用于输入批次并返回一些编码特征。

  • transcribe_file:转录输入的单个音频文件。

  • transcribe_batch:转录输入批次。

事实上,如果您满足我们在下一段将详细介绍的一些约束,您可以简单地这样做:

from speechbrain.inference.ASR import EncoderDecoderASR

asr_model = EncoderDecoderASR.from_hparams(source="your_local_folder", hparams_file='your_file.yaml', savedir="pretrained_model")
audio_file = 'your_file.wav'
asr_model.transcribe_file(audio_file)

然而,为了允许对所有可能的 EncoderDecoder ASR 流水线进行这种泛化,在部署系统时,您必须考虑一些约束。

  1. 必需的模块。正如您在 EncoderDecoderASR 类中看到的,您的 yaml 文件中定义的模块必须包含具有特定名称的某些元素。实际上,您需要一个分词器、一个编码器和一个解码器。编码器可以简单地是一个 speechbrain.nnet.containers.LengthsCapableSequential,它由特征计算、归一化和模型编码序列组成。

    HPARAMS_NEEDED = ["tokenizer"]
    MODULES_NEEDED = [
        "encoder",
        "decoder",
    ]

您还需要在 YAML 文件中声明这些实体,并创建以下名为 modules 的字典:

encoder: !new:speechbrain.nnet.containers.LengthsCapableSequential
    input_shape: [null, null, !ref <n_mels>]
    compute_features: !ref <compute_features>
    normalize: !ref <normalize>
    model: !ref <enc>

decoder: !new:speechbrain.nnet.RNN.AttentionalRNNDecoder
    enc_dim: !ref <dnn_neurons>
    input_size: !ref <emb_size>
    rnn_type: gru
    attn_type: location
    hidden_size: !ref <dec_neurons>
    attn_dim: 1024
    num_layers: 1
    scaling: 1.0
    channels: 10
    kernel_size: 100
    re_init: True
    dropout: !ref <dropout>


modules:
    encoder: !ref <encoder>
    decoder: !ref <decoder>
    lm_model: !ref <lm_model>

在这种情况下,enc 是一个 CRDNN,但也可以是任何自定义神经网络。

为什么需要确保这一点?嗯,这仅仅是因为当我们在 EncoderDecoderASR 类上进行推断时会调用这些模块。下面是 encode_batch() 函数的一个示例。

[...]
  wavs = wavs.float()
  wavs, wav_lens = wavs.to(self.device), wav_lens.to(self.device)
  encoder_out = self.modules.encoder(wavs, wav_lens)
return encoder_out

如果我有一个复杂的 asr_encoder 结构,包含多个深度神经网络等等,怎么办?只需在您的 yaml 文件中将所有内容放入一个 torch.nn.ModuleList。

asr_encoder: !new:torch.nn.ModuleList
    - [!ref <enc>, my_different_blocks ... ]
  1. 调用预训练器加载检查点。最后,您需要定义对预训练器的调用,它将把您训练好的模型的不同检查点加载到相应的 SpeechBrain 模块中。简而言之,它将加载您的编码器、语言模型甚至仅仅加载分词器的权重。

pretrainer: !new:speechbrain.utils.parameter_transfer.Pretrainer
    loadables:
        asr: !ref <asr_model>
        lm: !ref <lm_model>
        tokenizer: !ref <tokenizer>
    paths:
      asr: !ref <asr_model_ptfile>
      lm: !ref <lm_model_ptfile>
      tokenizer: !ref <tokenizer_ptfile>

可加载字段(loadable field)创建了一个文件(例如 lm,它与 <lm_model_ptfile> 中的检查点相关联)与一个 yaml 实例(例如 <lm_model>,它实际上就是您的语言模型)之间的链接。

如果您遵守这两个约束,它应该就能工作!这里我们提供一个仅用于推断的 yaml 完整示例:

# ############################################################################
# Model: E2E ASR with attention-based ASR
# Encoder: CRDNN model
# Decoder: GRU + beamsearch + RNNLM
# Tokens: BPE with unigram
# Authors:  Ju-Chieh Chou, Mirco Ravanelli, Abdel Heba, Peter Plantinga 2020
# ############################################################################


# Feature parameters
sample_rate: 16000
n_fft: 400
n_mels: 40

# Model parameters
activation: !name:torch.nn.LeakyReLU
dropout: 0.15
cnn_blocks: 2
cnn_channels: (128, 256)
inter_layer_pooling_size: (2, 2)
cnn_kernelsize: (3, 3)
time_pooling_size: 4
rnn_class: !name:speechbrain.nnet.RNN.LSTM
rnn_layers: 4
rnn_neurons: 1024
rnn_bidirectional: True
dnn_blocks: 2
dnn_neurons: 512
emb_size: 128
dec_neurons: 1024
output_neurons: 1000  # index(blank/eos/bos) = 0
blank_index: 0

# Decoding parameters
bos_index: 0
eos_index: 0
min_decode_ratio: 0.0
max_decode_ratio: 1.0
beam_size: 80
eos_threshold: 1.5
using_max_attn_shift: True
max_attn_shift: 240
lm_weight: 0.50
coverage_penalty: 1.5
temperature: 1.25
temperature_lm: 1.25

normalize: !new:speechbrain.processing.features.InputNormalization
    norm_type: global

compute_features: !new:speechbrain.lobes.features.Fbank
    sample_rate: !ref <sample_rate>
    n_fft: !ref <n_fft>
    n_mels: !ref <n_mels>

enc: !new:speechbrain.lobes.models.CRDNN.CRDNN
    input_shape: [null, null, !ref <n_mels>]
    activation: !ref <activation>
    dropout: !ref <dropout>
    cnn_blocks: !ref <cnn_blocks>
    cnn_channels: !ref <cnn_channels>
    cnn_kernelsize: !ref <cnn_kernelsize>
    inter_layer_pooling_size: !ref <inter_layer_pooling_size>
    time_pooling: True
    using_2d_pooling: False
    time_pooling_size: !ref <time_pooling_size>
    rnn_class: !ref <rnn_class>
    rnn_layers: !ref <rnn_layers>
    rnn_neurons: !ref <rnn_neurons>
    rnn_bidirectional: !ref <rnn_bidirectional>
    rnn_re_init: True
    dnn_blocks: !ref <dnn_blocks>
    dnn_neurons: !ref <dnn_neurons>

emb: !new:speechbrain.nnet.embedding.Embedding
    num_embeddings: !ref <output_neurons>
    embedding_dim: !ref <emb_size>

dec: !new:speechbrain.nnet.RNN.AttentionalRNNDecoder
    enc_dim: !ref <dnn_neurons>
    input_size: !ref <emb_size>
    rnn_type: gru
    attn_type: location
    hidden_size: !ref <dec_neurons>
    attn_dim: 1024
    num_layers: 1
    scaling: 1.0
    channels: 10
    kernel_size: 100
    re_init: True
    dropout: !ref <dropout>

ctc_lin: !new:speechbrain.nnet.linear.Linear
    input_size: !ref <dnn_neurons>
    n_neurons: !ref <output_neurons>

seq_lin: !new:speechbrain.nnet.linear.Linear
    input_size: !ref <dec_neurons>
    n_neurons: !ref <output_neurons>

log_softmax: !new:speechbrain.nnet.activations.Softmax
    apply_log: True

lm_model: !new:speechbrain.lobes.models.RNNLM.RNNLM
    output_neurons: !ref <output_neurons>
    embedding_dim: !ref <emb_size>
    activation: !name:torch.nn.LeakyReLU
    dropout: 0.0
    rnn_layers: 2
    rnn_neurons: 2048
    dnn_blocks: 1
    dnn_neurons: 512
    return_hidden: True  # For inference

tokenizer: !new:sentencepiece.SentencePieceProcessor

asr_model: !new:torch.nn.ModuleList
    - [!ref <enc>, !ref <emb>, !ref <dec>, !ref <ctc_lin>, !ref <seq_lin>]

# We compose the inference (encoder) pipeline.
encoder: !new:speechbrain.nnet.containers.LengthsCapableSequential
    input_shape: [null, null, !ref <n_mels>]
    compute_features: !ref <compute_features>
    normalize: !ref <normalize>
    model: !ref <enc>

ctc_scorer: !new:speechbrain.decoders.scorer.CTCScorer
    eos_index: !ref <eos_index>
    blank_index: !ref <blank_index>
    ctc_fc: !ref <ctc_lin>

coverage_scorer: !new:speechbrain.decoders.scorer.CoverageScorer
    vocab_size: !ref <output_neurons>

rnnlm_scorer: !new:speechbrain.decoders.scorer.RNNLMScorer
    language_model: !ref <lm_model>
    temperature: !ref <temperature_lm>

scorer: !new:speechbrain.decoders.scorer.ScorerBuilder
    scorer_beam_scale: 1.5
    full_scorers: [
        !ref <rnnlm_scorer>,
        !ref <coverage_scorer>]
    partial_scorers: [!ref <ctc_scorer>]
    weights:
        rnnlm: !ref <lm_weight>
        coverage: !ref <coverage_penalty>
        ctc: !ref <ctc_weight_decode>

decoder: !new:speechbrain.decoders.S2SRNNBeamSearcher
    embedding: !ref <emb>
    decoder: !ref <dec>
    linear: !ref <seq_lin>
    bos_index: !ref <bos_index>
    eos_index: !ref <eos_index>
    min_decode_ratio: !ref <min_decode_ratio>
    max_decode_ratio: !ref <max_decode_ratio>
    beam_size: !ref <test_beam_size>
    eos_threshold: !ref <eos_threshold>
    using_max_attn_shift: !ref <using_max_attn_shift>
    max_attn_shift: !ref <max_attn_shift>
    temperature: !ref <temperature>
    scorer: !ref <scorer>

modules:
    encoder: !ref <encoder>
    decoder: !ref <decoder>
    lm_model: !ref <lm_model>

pretrainer: !new:speechbrain.utils.parameter_transfer.Pretrainer
    loadables:
        asr: !ref <asr_model>
        lm: !ref <lm_model>
        tokenizer: !ref <tokenizer>

如您所见,这是一个标准的 YAML 文件,但包含一个加载模型的预训练器。它与用于训练的 yaml 文件类似。我们只需要移除所有特定于训练的部分(例如,训练参数、优化器、检查点等),并添加预训练器以及将所需模块与其预训练文件关联起来的 encoderdecoder 元素。

开发您自己的推断接口

尽管 EncoderDecoderASR 类已被设计得尽可能通用,但您可能需要一个更复杂的推断方案以更好地满足您的需求。在这种情况下,您必须开发自己的接口。为此,请遵循以下步骤:

  1. 创建继承自 Pretrained 的自定义接口(代码在这里

class MySuperTask(Pretrained):
  # Here, do not hesitate to also add some required modules
  # for further transparency.
  HPARAMS_NEEDED = ["mymodule1", "mymodule2"]
  MODULES_NEEDED = [
        "mytask_enc",
        "my_searcher",
  ]
  def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Do whatever is needed here w.r.t your system

这将使您的类能够调用有用的函数,例如根据 HyperPyYAML 文件获取和加载的 .from_hparams(),以及加载给定音频文件的 load_audio()。很可能我们在 Pretrained 类中编写的大多数方法都能满足您的需求。如果不能,您可以重写它们以实现您的自定义功能。

  1. 开发您的接口及各种功能。不幸的是,我们无法在此提供一个足够通用的示例。您可以向此类添加任何您认为可以使数据/模型推断更轻松自然的函数。例如,我们可以在此创建一个函数,仅使用 mytask_enc 模块对 wav 文件进行编码。

class MySuperTask(Pretrained):
  # Here, do not hesitate to also add some required modules
  # for further transparency.
  HPARAMS_NEEDED = ["mymodule1", "mymodule2"]
  MODULES_NEEDED = [
        "mytask_enc",
        "my_searcher",
  ]
  def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Do whatever is needed here w.r.t your system
  
  def encode_file(self, path):
        waveform = self.load_audio(path)
        # Fake a batch:
        batch = waveform.unsqueeze(0)
        rel_length = torch.tensor([1.0])
        with torch.no_grad():
          rel_lens = rel_length.to(self.device)
          encoder_out = self.encode_batch(waveform, rel_lens)
        
        return encode_file

现在,我们可以通过以下方式使用您的接口:

from speechbrain.pretrained import MySuperTask

my_model = MySuperTask.from_hparams(source="your_local_folder", hparams_file='your_file.yaml', savedir="pretrained_model")
audio_file = 'your_file.wav'
encoded = my_model.encode_file(audio_file)

如您所见,这种形式主义极其灵活,使您能够创建一个全面的接口,用于对您的预训练模型进行任何操作。

我们为端到端 ASR、说话人识别、声源分离、语音增强等提供了不同的通用接口。如果您有兴趣,请在此查看!

自定义您的语音识别器

一般情况下,您可能有自己的数据并想使用自己的模型。让我们详细讨论一下如何自定义您的方案。

建议:从一个可用的方案(例如用于此模板的方案)开始,只进行定制所需的最小修改。逐步测试您的模型。确保您的模型可以在包含少量句子的微型数据集上过拟合。如果不能过拟合,您的模型很可能存在错误。

使用您的数据进行训练

更改数据集时,您所需要做的就是更新数据准备脚本,使其创建符合预期格式的 JSON 文件。train.py 脚本期望 JSON 文件如下所示:

{
  "1867-154075-0032": {
    "wav": "{data_root}/LibriSpeech/train-clean-5/1867/154075/1867-154075-0032.flac",
    "length": 16.09,
    "words": "AND HE BRUSHED A HAND ACROSS HIS FOREHEAD AND WAS INSTANTLY HIMSELF CALM AND COOL VERY WELL THEN IT SEEMS I'VE MADE AN ASS OF MYSELF BUT I'LL TRY TO MAKE UP FOR IT NOW WHAT ABOUT CAROLINE"
  },
  "1867-154075-0001": {
    "wav": "{data_root}/LibriSpeech/train-clean-5/1867/154075/1867-154075-0001.flac",
    "length": 14.9,
    "words": "THAT DROPPED HIM INTO THE COAL BIN DID HE GET COAL DUST ON HIS SHOES RIGHT AND HE DIDN'T HAVE SENSE ENOUGH TO WIPE IT OFF AN AMATEUR A RANK AMATEUR I TOLD YOU SAID THE MAN OF THE SNEER WITH SATISFACTION"
  },

您必须解析您的数据集,并为每句话创建一个包含唯一 ID、音频信号路径(wav)、语音句子长度(秒,length)以及词语转录文本(“words”)的 JSON 文件。就是这样!

使用您自己的模型进行训练

在某些时候,您可能有自己的模型,并想将其插入语音识别流程中。例如,您可能想用其他模型替换我们的 CRDNN 编码器。为此,您必须创建自己的类并在其中指定神经网络的计算列表。您可以查看 speechbrain.lobes.models 中已有的模型。如果您的模型是一个简单的计算流水线,您可以使用序列容器(sequential container)。如果模型是一个更复杂的计算链,您可以将其创建为 torch.nn.Module 的实例,并在其中定义 __init__forward 方法,就像这里一样。

定义好模型后,您只需在 yaml 文件中声明它并在 train.py 中使用即可。

重要提示
插入新模型时,您必须重新调整系统最重要的超参数(例如,学习率、批量大小和架构参数),以使其正常工作。

总结

在本教程中,我们展示了如何使用 SpeechBrain 从零开始创建一个端到端语音识别器。所提出的系统包含了开发最先进系统的所有基本要素(即数据增强、分词、语言模型、束搜索、注意力等)。

我们仅使用一个小数据集描述了所有步骤。在实际情况中,您需要使用更多数据进行训练(例如,请参阅我们的 LibriSpeech 方案)。

引用 SpeechBrain

如果您在研究或业务中使用 SpeechBrain,请使用以下 BibTeX 条目进行引用:

@misc{speechbrainV1,
  title={Open-Source Conversational AI with {SpeechBrain} 1.0},
  author={Mirco Ravanelli and Titouan Parcollet and Adel Moumen and Sylvain de Langen and Cem Subakan and Peter Plantinga and Yingzhi Wang and Pooneh Mousavi and Luca Della Libera and Artem Ploujnikov and Francesco Paissan and Davide Borra and Salah Zaiem and Zeyu Zhao and Shucong Zhang and Georgios Karakasidis and Sung-Lin Yeh and Pierre Champion and Aku Rouhe and Rudolf Braun and Florian Mai and Juan Zuluaga-Gomez and Seyed Mahed Mousavi and Andreas Nautsch and Xuechen Liu and Sangeet Sagar and Jarod Duret and Salima Mdhaffar and Gaelle Laperriere and Mickael Rouvier and Renato De Mori and Yannick Esteve},
  year={2024},
  eprint={2407.00463},
  archivePrefix={arXiv},
  primaryClass={cs.LG},
  url={https://arxiv.org/abs/2407.00463},
}
@misc{speechbrain,
  title={{SpeechBrain}: A General-Purpose Speech Toolkit},
  author={Mirco Ravanelli and Titouan Parcollet and Peter Plantinga and Aku Rouhe and Samuele Cornell and Loren Lugosch and Cem Subakan and Nauman Dawalatabad and Abdelwahab Heba and Jianyuan Zhong and Ju-Chieh Chou and Sung-Lin Yeh and Szu-Wei Fu and Chien-Feng Liao and Elena Rastorgueva and François Grondin and William Aris and Hwidong Na and Yan Gao and Renato De Mori and Yoshua Bengio},
  year={2021},
  eprint={2106.04624},
  archivePrefix={arXiv},
  primaryClass={eess.AS},
  note={arXiv:2106.04624}
}