Open In Colab 要在GitHub上执行或查看/下载此笔记本

从零开始的语音分类

你想了解如何使用SpeechBrain实现一个分类系统吗?不用再找了,你来对地方了。本教程将带你完成在SpeechBrain中实现一个话语级别分类器所需的所有步骤。
教程最初将专注于说话人识别,并在此过程中描述如何将其扩展到许多其他分类任务,如语言识别情感识别声音分类关键词检测等。

模型

许多神经网络模型可以用于处理这类任务。在本教程中,我们将重点介绍一个TDNN分类器(xvector)和一个非常新的模型,称为ECAPA-TDNN,该模型在说话人验证和分割方面表现出色。

数据

训练将使用一个名为mini-librispeech的小型开源数据集进行,该数据集仅包含几小时的训练数据。在实际情况下,您需要一个更大的数据集。 有关实际任务的一些示例,请查看我们的Voxceleb食谱

代码

在本教程中,我们将参考speechbrain/templates/speaker_id中的代码。

安装

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

%%capture

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

# Clone SpeechBrain repository (development branch)
!git clone https://github.com/speechbrain/speechbrain/
%cd /content/speechbrain/

需要哪些步骤?

在SpeechBrain中训练一个话语级别的分类器相对容易。需要遵循的步骤如下:

  1. 准备你的数据。 这一步的目标是创建数据清单文件(CSV或JSON格式)。数据清单文件告诉SpeechBrain在哪里找到语音数据及其对应的语句级别分类(例如,说话者ID)。在本教程中,数据清单文件由mini_librispeech_prepare.py创建。

  2. 训练分类器。 此时,我们已经准备好训练我们的分类器。 要训练一个基于TDNN + 统计池化(xvectors)的说话人识别分类器,请运行以下命令:

cd speechbrain/templates/speaker_id/
python train.py train.yaml

稍后,我们将描述如何插入另一个名为Emphasized Channel Attention, Propagation, and Aggregation model (ECAPA)的模型,该模型在说话人识别任务中表现出色。

  1. 使用分类器(推理): 训练完成后,我们可以使用分类器进行推理。设计了一个名为EncoderClassifier的类,以使推理更容易。我们还设计了一个名为SpeakerRecognition的类,以使说话人验证任务的推理更容易。

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

步骤1:准备你的数据

数据准备的目标是创建数据清单文件。 这些文件告诉SpeechBrain在哪里找到音频数据及其对应的语句级别分类。它们是以流行的CSV和JSON格式编写的文本文件。

数据清单文件

让我们看一下JSON格式的数据清单文件的样子:

{
  "163-122947-0045": {
    "wav": "{data_root}/LibriSpeech/train-clean-5/163/122947/163-122947-0045.flac",
    "length": 14.335,
    "spk_id": "163"
  },
  "7312-92432-0025": {
    "wav": "{data_root}/LibriSpeech/train-clean-5/7312/92432/7312-92432-0025.flac",
    "length": 12.01,
    "spk_id": "7312"
  },
  "7859-102519-0036": {
    "wav": "{data_root}/LibriSpeech/train-clean-5/7859/102519/7859-102519-0036.flac",
    "length": 11.965,
    "spk_id": "7859"
  },
}

如你所见,我们有一个层次结构,其中第一个键是口语句子的唯一标识符。 然后,我们指定了任务所需的所有字段。例如,我们报告了语音记录的路径,其长度(以秒为单位,如果我们在创建小批量之前想要对句子进行排序,则需要),以及给定记录中说话者的说话者身份

实际上,您可以在这里指定您需要的所有条目(语言ID、情感注释等)。然而,这些条目的名称必须与实验脚本(例如,train.py)所期望的内容相匹配。我们稍后会对此进行更详细的说明。

你可能已经注意到我们定义了一个名为data_root的特殊变量。这允许用户从命令行(或从yaml超参数文件)动态更改数据文件夹。

准备脚本

每个数据集的格式都不同。解析你自己的数据集并创建JSON或CSV文件的脚本需要你自己编写。大多数情况下,这非常简单。

例如,对于mini-librispeech数据集,我们编写了这个简单的数据准备脚本,名为mini_librispeech_prepare.py

此函数会自动下载数据(在这种情况下是公开可用的)。我们搜索所有的音频文件,并在读取它们时创建带有说话者ID注释的JSON文件。

你可以使用这个脚本作为你在目标数据集上进行自定义准备的良好基础。正如你所见,我们创建了三个独立的数据清单文件来管理训练、验证和测试阶段。

将您的数据本地复制

在HPC集群中使用speechbrain(或任何其他工具包)时,一个良好的做法是将数据集压缩为单个文件,并将数据复制(并解压缩)到计算节点的本地文件夹中。这将使代码快得多,因为数据不是从共享文件系统中获取的,而是从本地文件系统中获取的。此外,您不会因为大量的读取操作而影响共享文件系统的性能。我们强烈建议用户遵循这种方法(在Google Colab中无法实现)。

步骤2:训练分类器

我们现在展示如何使用SpeechBrain训练一个话语级别分类器。 所提出的方案执行特征计算/归一化,使用编码器处理特征,并在其上应用分类器。还采用数据增强来提高系统性能。

训练一个说话人识别模型

我们将训练用于x-向量的基于TDNN的模型。在卷积层的顶部使用统计池化,将可变长度的句子转换为固定长度的嵌入

在嵌入层的顶部,使用了一个简单的全连接分类器来预测给定句子中哪个N个说话者是活跃的。

要训练这个模型,请运行以下代码:

%cd /content/speechbrain/templates/speaker_id
!python train.py train.yaml --number_of_epochs=15 #--device='cpu'
/content/speechbrain/templates/speaker_id
speechbrain.core - Beginning experiment!
speechbrain.core - Experiment folder: ./results/speaker_id/1986
Downloading http://www.openslr.org/resources/31/train-clean-5.tar.gz to ./data/train-clean-5.tar.gz
train-clean-5.tar.gz: 333MB [00:18, 18.2MB/s]               
mini_librispeech_prepare - Creating train.json, valid.json, and test.json
mini_librispeech_prepare - train.json successfully created!
mini_librispeech_prepare - valid.json successfully created!
mini_librispeech_prepare - test.json successfully created!
Downloading https://www.dropbox.com/scl/fi/a09pj97s5ifan81dqhi4n/noises.zip?rlkey=j8b0n9kdjdr32o1f06t0cw5b7&dl=1 to ./data/noise/data.zip
noises.zip?rlkey=j8b0n9kdjdr32o1f06t0cw5b7&dl=1: 569MB [00:05, 105MB/s]               
Extracting ./data/noise/data.zip to ./data/noise
speechbrain.dataio.encoder - Load called, but CategoricalEncoder is not empty. Loaded data will overwrite everything. This is normal if there is e.g. an unk label defined at init.
speechbrain.core - Info: ckpt_interval_minutes arg from hparam file is used
speechbrain.core - Exception:
Traceback (most recent call last):
  File "/content/speechbrain/templates/speaker_id/train.py", line 307, in <module>
    spk_id_brain = SpkIdBrain(
  File "/usr/local/lib/python3.10/dist-packages/speechbrain/core.py", line 695, in __init__
    torch.cuda.set_device(int(self.device[-1]))
  File "/usr/local/lib/python3.10/dist-packages/torch/cuda/__init__.py", line 404, in set_device
    torch._C._cuda_setDevice(device)
  File "/usr/local/lib/python3.10/dist-packages/torch/cuda/__init__.py", line 298, in _lazy_init
    torch._C._cuda_init()
RuntimeError: Found no NVIDIA driver on your system. Please check that you have an NVIDIA GPU and installed a driver from http://www.nvidia.com/Download/index.aspx

从打印结果可以看出,验证和训练的损失在前几个时期下降得非常快。然后,我们基本上看到了一些小的改进和性能波动。

在训练结束时,验证误差应该趋近于零(或非常接近零)。

本教程中提出的任务非常简单,因为我们只需要对mini-librispeech数据集中的28个说话者进行分类。请将此教程仅作为一个示例,解释如何设置所有必要的组件来开发一个语音分类器。如果您想查看一个流行的说话者识别数据集的示例,请参考我们的voxceleb食谱

在深入代码之前,让我们看看在指定的output_folder中生成了哪些文件/文件夹:

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

  • log.txt: 是一个更详细的日志记录器,包含每个基本操作的时间戳。

  • env.log: 显示所有使用的依赖项及其对应的版本(对可复制性有用)。

  • train.py, hyperparams.yaml: 是实验文件及其相应超参数的副本(用于可重复性)。

  • save: 是我们存储学习模型的地方。

save文件夹中,您可以找到包含训练期间保存的检查点的子文件夹(格式为CKPT+data+time)。通常,您会在这里找到两个检查点:best(即最早的一个)和latest(即最近的一个)。如果只找到一个检查点,这意味着最后一个周期也是最好的。

在每个检查点内,我们存储了所有需要的信息以恢复训练(例如,模型、优化器、调度器、epoch计数器等)。嵌入模型的参数记录在embedding_model.ckpt文件中,而分类器的参数则在classifier.ckpt中。这只是一个可以用torch.load读取的二进制格式。

保存文件夹中还包含标签编码器label_encoder.txt),它将每个说话者ID条目映射到其相应的索引。

'163' => 0
'7312' => 1
'7859' => 2
'19' => 3
'1737' => 4
'6272' => 5
'1970' => 6
'2416' => 7
'118' => 8
'6848' => 9
'4680' => 10
'460' => 11
'3664' => 12
'3242' => 13
'1898' => 14
'7367' => 15
'1088' => 16
'3947' => 17
'3526' => 18
'1867' => 19
'8629' => 20
'332' => 21
'4640' => 22
'2136' => 23
'669' => 24
'5789' => 25
'32' => 26
'226' => 27
================
'starting_index' => 0

像往常一样,我们使用实验文件 train.py 和一个名为 train.yaml 的超参数文件来实现系统。

超参数

yaml 文件包含实现所需分类器所需的所有模块和超参数。 您可以在这里查看完整的 train.yaml 文件

在第一部分中,我们指定了一些基本设置,例如种子和输出文件夹的路径:

# Seed needs to be set at top of yaml, before objects with parameters are made
seed: 1986
__set_seed: !!python/object/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
output_folder: !ref ./results/speaker_id/<seed>
save_folder: !ref <output_folder>/save
train_log: !ref <output_folder>/train_log.txt

然后我们指定用于训练、验证和测试的数据清单文件的路径:

# 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

这些文件将在从实验文件(train.py)调用数据准备脚本(mini_librispeech_prepare.py)时自动创建。

接下来,我们设置train_logger并声明error_stats对象,这些对象将收集分类错误率的统计信息:

# 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>

error_stats: !name:speechbrain.utils.metric_stats.MetricStats
    metric: !name:speechbrain.nnet.losses.classification_error
        reduction: batch

我们现在可以指定一些训练超参数,例如训练轮数、批量大小、学习率、训练轮数以及嵌入维度。

ckpt_interval_minutes: 15 # save checkpoint every N min

# Feature parameters
n_mels: 23

# Training Parameters
sample_rate: 16000
number_of_epochs: 35
batch_size: 16
lr_start: 0.001
lr_final: 0.0001
n_classes: 28 # In this case, we have 28 speakers
emb_dim: 512 # dimensionality of the embeddings
dataloader_options:
    batch_size: !ref <batch_size>

变量 ckpt_interval_minutes 可以用于在训练周期内每隔N分钟保存检查点。在某些情况下,一个周期可能需要几个小时,定期保存检查点是一个良好且安全的做法。对于这个基于小数据集的简单教程来说,这个功能并不是真正需要的。

我们现在可以定义训练模型所需的最重要模块:

# 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>
    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 frequency dropouts (time-domain implementation)
# # A small speed change help to improve the performance of speaker-id as well.
augmentation: !new:speechbrain.lobes.augment.TimeDomainSpecAugment
    sample_rate: !ref <sample_rate>
    speeds: [95, 100, 105]

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

# Mean and std normalization of the input features
mean_var_norm: !new:speechbrain.processing.features.InputNormalization
    norm_type: sentence
    std_norm: False

# To design a custom model, either just edit the simple CustomModel
# class that's listed here, or replace this `!new` call with a line
# pointing to a different file you've defined.
embedding_model: !new:custom_model.Xvector
    in_channels: !ref <n_mels>
    activation: !name:torch.nn.LeakyReLU
    tdnn_blocks: 5
    tdnn_channels: [512, 512, 512, 512, 1500]
    tdnn_kernel_sizes: [5, 3, 3, 1, 1]
    tdnn_dilations: [1, 2, 3, 1, 1]
    lin_neurons: !ref <emb_dim>

classifier: !new:custom_model.Classifier
    input_shape: [null, null, !ref <emb_dim>]
    activation: !name:torch.nn.LeakyReLU
    lin_blocks: 1
    lin_neurons: !ref <emb_dim>
    out_neurons: !ref <n_classes>

# 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>

# 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:
    compute_features: !ref <compute_features>
    env_corrupt: !ref <env_corrupt>
    augmentation: !ref <augmentation>
    embedding_model: !ref <embedding_model>
    classifier: !ref <classifier>
    mean_var_norm: !ref <mean_var_norm>

增强部分基于env_corrupt(添加噪声和混响)和augmentation(添加时间/频率丢失和速度变化)。 有关这些模块的更多信息,请查看环境干扰语音增强的教程。

我们通过声明优化器、学习率调度器和检查点器来结束超参数规范:

# 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.Adam
    lr: !ref <lr_start>

# This function manages learning rate annealing over the epochs.
# We here use the simple lr annealing method that linearly decreases
# the lr from the initial value to the final one.
lr_annealing: !new:speechbrain.nnet.schedulers.LinearScheduler
    initial_value: !ref <lr_start>
    final_value: !ref <lr_final>
    epoch_count: !ref <number_of_epochs>

# 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:
        embedding_model: !ref <embedding_model>
        classifier: !ref <classifier>
        normalizer: !ref <mean_var_norm>
        counter: !ref <epoch_counter>

在这种情况下,我们使用Adam作为优化器,并在15个周期内使用线性学习率衰减。

现在让我们将最佳模型保存到一个单独的文件夹中(对后面解释的推理部分有用):

# Create folder for best model
!mkdir /content/best_model/

# Copy label encoder
!cp results/speaker_id/1986/save/label_encoder.txt /content/best_model/

# Copy best model
!cp "`ls -td results/speaker_id/1986/save/CKPT* | tail -1`"/* /content/best_model/
ls: cannot access 'results/speaker_id/1986/save/CKPT*': No such file or directory
cp: -r not specified; omitting directory '/bin'
cp: -r not specified; omitting directory '/boot'
cp: -r not specified; omitting directory '/content'
cp: -r not specified; omitting directory '/datalab'
cp: -r not specified; omitting directory '/dev'
cp: -r not specified; omitting directory '/etc'
cp: -r not specified; omitting directory '/home'
cp: -r not specified; omitting directory '/kaggle'
cp: -r not specified; omitting directory '/lib'
cp: -r not specified; omitting directory '/lib32'
cp: -r not specified; omitting directory '/lib64'
cp: -r not specified; omitting directory '/libx32'
cp: -r not specified; omitting directory '/media'
cp: -r not specified; omitting directory '/mnt'
cp: -r not specified; omitting directory '/opt'
cp: -r not specified; omitting directory '/proc'
cp: -r not specified; omitting directory '/root'
cp: -r not specified; omitting directory '/run'
cp: -r not specified; omitting directory '/sbin'
cp: -r not specified; omitting directory '/srv'
cp: -r not specified; omitting directory '/sys'
cp: -r not specified; omitting directory '/tmp'
cp: -r not specified; omitting directory '/tools'
cp: -r not specified; omitting directory '/usr'
cp: -r not specified; omitting directory '/var'

实验文件

现在让我们来看看在train.py中如何使用yaml文件中声明的对象、函数和超参数来实现分类器。

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

# 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,
    )

    # Data preparation, to be run on only one process.
    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"],
            "split_ratio": [80, 10, 10],
        },
    )

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

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

我们还运行了数据准备脚本 prepare_mini_librispeech,该脚本创建数据清单文件。它被封装在 sb.utils.distributed.run_on_main 中,因为此操作将清单文件写入磁盘,即使在多GPU DDP场景中,这也必须在单个进程中完成。有关如何使用多个GPU的更多信息,请查看本教程

数据输入输出管道

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

    # Create dataset objects "train", "valid", and "test".
    datasets = dataio_prep(hparams)

让我们更仔细地看一下。

def dataio_prep(hparams):
    """This function prepares the datasets to be used in the brain class.
    It also defines the data processing pipeline through user-defined functions.
    We expect `prepare_mini_librispeech` to have been called before this,
    so that the `train.json`, `valid.json`,  and `valid.json` manifest files
    are available.
    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
        Contains two keys, "train" and "valid" that correspond
        to the appropriate DynamicItemDataset object.
    """

    # Initialization of the label encoder. The label encoder assignes to each
    # of the observed label a unique index (e.g, 'spk01': 0, 'spk02': 1, ..)
    label_encoder = sb.dataio.encoder.CategoricalEncoder()

    # Define audio pipeline
    @sb.utils.data_pipeline.takes("wav")
    @sb.utils.data_pipeline.provides("sig")
    def audio_pipeline(wav):
        """Load the signal, and pass it and its length to the corruption class.
        This is done on the CPU in the `collate_fn`."""
        sig = sb.dataio.dataio.read_audio(wav)
        return sig

    # Define label pipeline:
    @sb.utils.data_pipeline.takes("spk_id")
    @sb.utils.data_pipeline.provides("spk_id", "spk_id_encoded")
    def label_pipeline(spk_id):
        yield spk_id
        spk_id_encoded = label_encoder.encode_label_torch(spk_id)
        yield spk_id_encoded

    # Define datasets. We also connect the dataset with the data processing
    # functions defined above.
    datasets = {}
    hparams["dataloader_options"]["shuffle"] = False
    for dataset in ["train", "valid", "test"]:
        datasets[dataset] = sb.dataio.dataset.DynamicItemDataset.from_json(
            json_path=hparams[f"{dataset}_annotation"],
            replacements={"data_root": hparams["data_folder"]},
            dynamic_items=[audio_pipeline, label_pipeline],
            output_keys=["id", "sig", "spk_id_encoded"],
        )

    # Load or compute the label encoder (with multi-GPU DDP support)
    # Please, take a look into the lab_enc_file to see the label to index
    # mappinng.
    lab_enc_file = os.path.join(hparams["save_folder"], "label_encoder.txt")
    label_encoder.load_or_create(
        path=lab_enc_file,
        from_didatasets=[datasets["train"]],
        output_key="spk_id",
    )

    return datasets

第一部分只是声明了CategoricalEncoder,它将用于将分类标签转换为相应的索引。

然后你可以注意到我们暴露了音频和标签处理函数。

audio_pipeline 接收音频信号的路径(wav)并读取它。它返回一个包含读取的语音句子的张量。此函数的输入(即 wav)必须与数据清单文件中相应键的名称相同:

{
  "163-122947-0045": {
    "wav": "{data_root}/LibriSpeech/train-clean-5/163/122947/163-122947-0045.flac",
    "length": 14.335,
    "spk_id": "163"
  },
}

同样地,我们定义了另一个名为label_pipeline的函数,用于处理话语级别的标签,并将它们放入定义模型可用的格式中。该函数读取JSON文件中定义的字符串spk_id,并使用分类编码器对其进行编码。

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

  • batch.id

  • batch.sig

  • batch.spk_id_encoded

函数的最后一部分专门用于标签编码器的初始化。标签编码器接收训练数据集作为输入,并为所有找到的spk_id条目分配不同的索引。这些索引将对应于分类器的输出索引。

有关数据加载器的更多信息,请查看本教程

在数据集定义之后,主函数可以继续进行brain类的初始化和使用:

    # Initialize the Brain object to prepare for mask training.
    spk_id_brain = SpkIdBrain(
        modules=hparams["modules"],
        opt_class=hparams["opt_class"],
        hparams=hparams,
        run_opts=run_opts,
        checkpointer=hparams["checkpointer"],
    )

    # The `fit()` method iterates the training loop, calling the methods
    # necessary to update the parameters of the model. Since all objects
    # with changing state are managed by the Checkpointer, training can be
    # stopped at any point, and will be resumed on next call.
    spk_id_brain.fit(
        epoch_counter=spk_id_brain.hparams.epoch_counter,
        train_set=datasets["train"],
        valid_set=datasets["valid"],
        train_loader_kwargs=hparams["dataloader_options"],
        valid_loader_kwargs=hparams["dataloader_options"],
    )

    # Load the best checkpoint for evaluation
    test_stats = spk_id_brain.evaluate(
        test_set=datasets["test"],
        min_key="error",
        test_loader_kwargs=hparams["dataloader_options"],
    )

fit 方法执行训练,而测试则通过 evaluate 方法进行。训练和验证数据加载器作为输入提供给 fit 方法,而测试数据集则输入到 evaluate 方法中。

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

前向计算

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

    def compute_forward(self, batch, stage):
        """Runs all the computation of that transforms the input into the
        output probabilities over the N classes.
        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 : Tensor
            Tensor that contains the posterior probabilities over the N classes.
        """

        # We first move the batch to the appropriate device.
        batch = batch.to(self.device)

        # Compute features, embeddings, and predictions
        feats, lens = self.prepare_features(batch.sig, stage)
        embeddings = self.modules.embedding_model(feats, lens)
        predictions = self.modules.classifier(embeddings)

        return predictions

在这种情况下,计算链非常简单。我们只需将批次放在正确的设备上并计算声学特征。然后,我们使用TDNN编码器处理这些特征,该编码器输出一个固定大小的张量。后者输入到一个分类器,该分类器输出N个类别的后验概率(在这种情况下是28个说话者)。数据增强在prepare_features方法中添加:

    def prepare_features(self, wavs, stage):
        """Prepare the features for computation, including augmentation.
        Arguments
        ---------
        wavs : tuple
            Input signals (tensor) and their relative lengths (tensor).
        stage : sb.Stage
            The current stage of training.
        """
        wavs, lens = wavs

        # Add augmentation if specified. In this version of augmentation, we
        # concatenate the original and the augment batches in a single bigger
        # batch. This is more memory-demanding, but helps to improve the
        # performance. Change it if you run OOM.
        if stage == sb.Stage.TRAIN:
            if hasattr(self.modules, "env_corrupt"):
                wavs_noise = self.modules.env_corrupt(wavs, lens)
                wavs = torch.cat([wavs, wavs_noise], dim=0)
                lens = torch.cat([lens, lens])

            if hasattr(self.hparams, "augmentation"):
                wavs = self.hparams.augmentation(wavs, lens)

        # Feature extraction and normalization
        feats = self.modules.compute_features(wavs)
        feats = self.modules.mean_var_norm(feats, lens)

        return feats, lens

特别是,当在yaml文件中声明环境损坏时,我们在同一批次中连接信号的干净版本和增强版本。

这种方法将批量大小加倍(因此也增加了所需的GPU内存),但它实现了一个非常强大的正则化器。在同一批次中同时包含信号的干净版本和噪声版本,迫使梯度指向参数空间中的一个方向,该方向对信号失真具有鲁棒性

计算目标

现在让我们来看一下compute_objectives方法,它接收目标、预测并估计一个损失函数:

    def compute_objectives(self, predictions, batch, stage):
        """Computes the loss given the predicted and targeted outputs.
        Arguments
        ---------
        predictions : tensor
            The output tensor 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.
        """

        _, lens = batch.sig
        spkid, _ = batch.spk_id_encoded

        # Concatenate labels (due to data augmentation)
        if stage == sb.Stage.TRAIN and hasattr(self.modules, "env_corrupt"):
            spkid = torch.cat([spkid, spkid], dim=0)
            lens = torch.cat([lens, lens])

        # Compute the cost function
        loss = sb.nnet.losses.nll_loss(predictions, spkid, lens)

        # Append this batch of losses to the loss metric for easy
        self.loss_metric.append(
            batch.id, predictions, spkid, lens, reduction="batch"
        )

        # Compute classification error at test time
        if stage != sb.Stage.TRAIN:
            self.error_metrics.append(batch.id, predictions, spkid, lens)

        return loss

输入中的预测是前向方法中计算的预测。通过将这些预测与目标标签进行比较来评估成本函数。这是通过负对数似然(NLL)损失来完成的。

####其他方法 除了这两个重要的函数外,我们还有一些其他方法被brain类使用。on_state_starts在每个epoch开始时被调用,用于设置统计跟踪器。on_stage_end在每个阶段结束时(例如,在每个训练epoch结束时)被调用,主要用于统计管理、学习率退火和检查点保存。有关brain类的更详细描述,请查看本教程。有关检查点保存的更多信息,请查看这里

步骤3:推理

此时,我们可以使用训练好的分类器对新数据进行预测。Speechbrain 提供了一些类(看看这里),例如EncoderClassifier,可以使推理更容易。该类还可以用于在编码器输出处提取一些嵌入。

让我们首先看看如何使用它来加载我们最好的xvector模型(在Voxceleb上训练并存储在HuggingFace上)来计算一些嵌入并执行说话者分类:

import torchaudio
from speechbrain.inference.classifiers import EncoderClassifier
classifier = EncoderClassifier.from_hparams(source="speechbrain/spkrec-xvect-voxceleb")
signal, fs =torchaudio.load('/content/speechbrain/tests/samples/single-mic/example1.wav')

# Compute speaker embeddings
embeddings = classifier.encode_batch(signal)

# Perform classification
output_probs, score, index, text_lab = classifier.classify_batch(signal)

# Posterior log probabilities
print(output_probs)

# Score (i.e, max log posteriors)
print(score)

# Index of the predicted speaker
print(index)

# Text label of the predicted speaker
print(text_lab)
tensor([[-31.8672, -35.2024, -25.7930,  ..., -21.0044, -12.4279, -21.5265]])
tensor([-1.1278])
tensor([2710])
['id10892']

对于那些对说话人验证感兴趣的人,我们还创建了一个名为SpeakerRecognition的推理接口:

from speechbrain.inference.speaker import SpeakerRecognition
verification = SpeakerRecognition.from_hparams(source="speechbrain/spkrec-ecapa-voxceleb", savedir="pretrained_models/spkrec-ecapa-voxceleb")

file1 = '/content/speechbrain/tests/samples/single-mic/example1.wav'
file2 = '/content/speechbrain/tests/samples/single-mic/example2.flac'

score, prediction = verification.verify_files(file1, file2)

print(score)
print(prediction) # True = same speaker, False=Different speakers
tensor([0.1799])
tensor([False])

但是,这如何与我们之前训练的自定义分类器一起工作

此时,您有一些选项可供选择。要全面了解所有选项,请查看本教程

我们在这里只展示如何在刚刚训练的模型上使用现有的EncoderClassifier

在你的模型上使用EncoderClassifier接口

EncoderClassifier 类接受一个预训练模型,并使用以下方法对其进行推理:

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

  • classify_batch: 执行完整的分类步骤,并返回分类器的输出概率、最佳分数、最佳类别的索引及其文本格式的标签(参见上面的示例)。

要使用之前训练的模型与此接口,我们需要创建一个推理yaml文件,该文件与用于训练的文件略有不同。主要区别如下:

  1. 您可以移除仅用于训练的所有超参数和对象。您可以只保留与模型定义相关的部分。

  2. 你需要分配一个Categorical encoder对象,它允许你将索引转换为文本标签。

  3. 您必须使用预训练器将您的模型与相应的文件链接起来。

推理的yaml文件看起来像这样:

%%writefile /content/best_model/hparams_inference.yaml

# #################################
# Basic inference parameters for speaker-id. We have first a network that
# computes some embeddings. On the top of that, we employ a classifier.
#
# Author:
#  * Mirco Ravanelli 2021
# #################################

# pretrain folders:
pretrained_path: /content/best_model/


# Model parameters
n_mels: 23
sample_rate: 16000
n_classes: 28 # In this case, we have 28 speakers
emb_dim: 512 # dimensionality of the embeddings

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

# Mean and std normalization of the input features
mean_var_norm: !new:speechbrain.processing.features.InputNormalization
    norm_type: sentence
    std_norm: False

# To design a custom model, either just edit the simple CustomModel
# class that's listed here, or replace this `!new` call with a line
# pointing to a different file you've defined.
embedding_model: !new:custom_model.Xvector
    in_channels: !ref <n_mels>
    activation: !name:torch.nn.LeakyReLU
    tdnn_blocks: 5
    tdnn_channels: [512, 512, 512, 512, 1500]
    tdnn_kernel_sizes: [5, 3, 3, 1, 1]
    tdnn_dilations: [1, 2, 3, 1, 1]
    lin_neurons: !ref <emb_dim>

classifier: !new:custom_model.Classifier
    input_shape: [null, null, !ref <emb_dim>]
    activation: !name:torch.nn.LeakyReLU
    lin_blocks: 1
    lin_neurons: !ref <emb_dim>
    out_neurons: !ref <n_classes>

label_encoder: !new:speechbrain.dataio.encoder.CategoricalEncoder

# 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:
    compute_features: !ref <compute_features>
    embedding_model: !ref <embedding_model>
    classifier: !ref <classifier>
    mean_var_norm: !ref <mean_var_norm>

pretrainer: !new:speechbrain.utils.parameter_transfer.Pretrainer
    loadables:
        embedding_model: !ref <embedding_model>
        classifier: !ref <classifier>
        label_encoder: !ref <label_encoder>
    paths:
        embedding_model: !ref <pretrained_path>/embedding_model.ckpt
        classifier: !ref <pretrained_path>/classifier.ckpt
        label_encoder: !ref <pretrained_path>/label_encoder.txt
Writing /content/best_model/hparams_inference.yaml

如你所见,我们这里只有模型定义(没有优化器、检查点等)。yaml文件的最后一部分管理预训练,我们将模型对象与训练时创建的预训练文件绑定在一起。

现在让我们使用EncoderClassifier类进行推理:

from speechbrain.inference.classifiers import EncoderClassifier

classifier = EncoderClassifier.from_hparams(source="/content/best_model/", hparams_file='hparams_inference.yaml', savedir="/content/best_model/")

# Perform classification
audio_file = 'data/LibriSpeech/train-clean-5/5789/70653/5789-70653-0036.flac'
signal, fs = torchaudio.load(audio_file) # test_speaker: 5789
output_probs, score, index, text_lab = classifier.classify_batch(signal)
print('Target: 5789, Predicted: ' + text_lab[0])

# Another speaker
audio_file = 'data/LibriSpeech/train-clean-5/460/172359/460-172359-0012.flac'
signal, fs =torchaudio.load(audio_file) # test_speaker: 460
output_probs, score, index, text_lab = classifier.classify_batch(signal)
print('Target: 460, Predicted: ' + text_lab[0])

# And if you want to extract embeddings...
embeddings = classifier.encode_batch(signal)
---------------------------------------------------------------------------
OSError                                   Traceback (most recent call last)
/usr/lib/python3.10/pathlib.py in resolve(self, strict)
   1086             try:
-> 1087                 p.stat()
   1088             except OSError as e:

/usr/lib/python3.10/pathlib.py in stat(self, follow_symlinks)
   1096         """
-> 1097         return self._accessor.stat(self, follow_symlinks=follow_symlinks)
   1098 

OSError: [Errno 40] Too many levels of symbolic links: '/content/best_model/embedding_model.ckpt'

During handling of the above exception, another exception occurred:

RuntimeError                              Traceback (most recent call last)
<ipython-input-7-c0f62d2f5dc4> in <cell line: 3>()
      1 from speechbrain.inference.classifiers import EncoderClassifier
      2 
----> 3 classifier = EncoderClassifier.from_hparams(source="/content/best_model/", hparams_file='hparams_inference.yaml', savedir="/content/best_model/")
      4 
      5 # Perform classification

/usr/local/lib/python3.10/dist-packages/speechbrain/inference/interfaces.py in from_hparams(cls, source, hparams_file, pymodule_file, overrides, savedir, use_auth_token, revision, download_only, huggingface_cache_dir, **kwargs)
    488         pretrainer.set_collect_in(savedir)
    489         # For distributed setups, have this here:
--> 490         run_on_main(pretrainer.collect_files, kwargs={"default_source": source})
    491         # Load on the CPU. Later the params can be moved elsewhere by specifying
    492         if not download_only:

/usr/local/lib/python3.10/dist-packages/speechbrain/utils/distributed.py in run_on_main(func, args, kwargs, post_func, post_args, post_kwargs, run_post_on_main)
     58         post_kwargs = {}
     59 
---> 60     main_process_only(func)(*args, **kwargs)
     61     ddp_barrier()
     62 

/usr/local/lib/python3.10/dist-packages/speechbrain/utils/distributed.py in main_proc_wrapped_func(*args, **kwargs)
    100         MAIN_PROC_ONLY += 1
    101         if if_main_process():
--> 102             result = function(*args, **kwargs)
    103         else:
    104             result = None

/usr/local/lib/python3.10/dist-packages/speechbrain/utils/parameter_transfer.py in collect_files(self, default_source, internal_ddp_handling)
    258                 fetch_from, source = source
    259             if fetch_from is FetchFrom.LOCAL or (
--> 260                 pathlib.Path(path).resolve()
    261                 == pathlib.Path(source).resolve() / filename
    262             ):

/usr/lib/python3.10/pathlib.py in resolve(self, strict)
   1087                 p.stat()
   1088             except OSError as e:
-> 1089                 check_eloop(e)
   1090         return p
   1091 

/usr/lib/python3.10/pathlib.py in check_eloop(e)
   1072             winerror = getattr(e, 'winerror', 0)
   1073             if e.errno == ELOOP or winerror == _WINERROR_CANT_RESOLVE_FILENAME:
-> 1074                 raise RuntimeError("Symlink loop from %r" % e.filename)
   1075 
   1076         try:

RuntimeError: Symlink loop from '/content/best_model/embedding_model.ckpt'

EncoderClassifier 接口假设你的模型在 yaml 文件中指定了以下模块:

  • compute_features:负责从原始音频信号中提取特征

  • mean_var_norm: 执行特征归一化

  • embedding_model: 将特征转换为固定大小的嵌入。

  • 分类器:在嵌入的顶部对N个类执行最终分类。

如果你的模型不能以这种方式构建,你可以随时自定义EncoderClassifier接口以满足你的需求。 请查看本教程以获取更多信息

扩展到不同任务

在一般情况下,您可能拥有自己的数据和分类任务,并且希望使用自己的模型。让我们进一步讨论如何自定义您的配方。

建议: 从一个有效的配方开始(比如用于此模板的配方),并仅进行所需的最小修改以进行定制。逐步测试你的模型。确保你的模型可以在由少量句子组成的微小数据集上过拟合。如果它没有过拟合,那么你的模型中可能存在错误。

在您的任务中使用您的数据进行训练

如果我要在我的数据上解决另一个话语级别的分类任务,比如语言识别情感识别声音分类关键词检测,该怎么办?

你只需要做的是:

  1. 根据您的任务需求更改带有注释的JSON。

  2. 更改train.py中的数据管道以符合新的注释。

更改JSON

本教程期望的JSON文件如下:

{
  "163-122947-0045": {
    "wav": "{data_root}/LibriSpeech/train-clean-5/163/122947/163-122947-0045.flac",
    "length": 14.335,
    "spk_id": "163"
  },
  "7312-92432-0025": {
    "wav": "{data_root}/LibriSpeech/train-clean-5/7312/92432/7312-92432-0025.flac",
    "length": 12.01,
    "spk_id": "7312"
  },
  "7859-102519-0036": {
    "wav": "{data_root}/LibriSpeech/train-clean-5/7859/102519/7859-102519-0036.flac",
    "length": 11.965,
    "spk_id": "7859"
  },
}

然而,您可以在这里添加所有您想要的条目。例如,如果您想解决一个语言识别任务,JSON文件应该如下所示:

{
  "sentence001": {
    "wav": "{data_root}/your_path/your_file1.wav",
    "length": 10.335,
    "lang_id": "Italian"
  },
{
  "sentence002": {
    "wav": "{data_root}/your_path/your_file2.wav",
    "length": 12.335,
    "lang_id": "French"
  },
}

如果你想解决一个情绪识别任务,它将会是这样的:

{
  "sentence001": {
    "wav": "{data_root}/your_path/your_file1.wav",
    "length": 10.335,
    "emotion": "Happy"
  },
{
  "sentence002": {
    "wav": "{data_root}/your_path/your_file2.wav",
    "length": 12.335,
    "emotion": "Sad"
  },
}

要创建数据清单文件,您必须解析您的数据集并创建JSON文件,每个句子都有一个唯一的ID,音频信号的路径(wav),语音句子的长度(以秒为单位)(length),以及您想要的注释。

更改 train.py

唯一需要记住的是,JSON文件中的名称条目必须与train.py中数据加载器期望的内容匹配。例如,如果你在JSON中定义了一个情感键,你应该在train.py的数据处理管道中有类似这样的内容:

    # Define label pipeline:
    @sb.utils.data_pipeline.takes("emotion")
    @sb.utils.data_pipeline.provides("emotion", "emotion_encoded")
    def label_pipeline(emotion):
        yield emotion
        emotion_encoded = label_encoder.encode_label_torch(emotion)
        yield emotion_encoded

基本上,你需要在代码中所有地方将spk_id条目替换为emotion。就是这样!

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

在某些时候,你可能会有自己的模型,并且希望将其插入到语音识别管道中。 例如,你可能想用不同的东西替换我们的xvector编码器。

要做到这一点,您必须创建自己的类,并在其中指定神经网络的计算列表。您可以查看speechbrain.lobes.models中已经存在的模型。如果您的模型是一个简单的计算管道,您可以使用sequential container。如果模型是一个更复杂的计算链,您可以将其创建为torch.nn.Module的实例,并在那里定义__init__forward方法,如这里所示。

一旦你定义了你的模型,你只需要在yaml文件中声明它并在train.py中使用它

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

ECAPA-TDNN 模型

我们发现对于说话人识别特别有效的一个模型是ECAPA-TDNN模型implemented here

ECAPA-TDNN架构基于流行的x-vector拓扑,并引入了多项增强以创建更稳健的说话人嵌入。

池化层使用了一种通道和上下文依赖的注意力机制,这使得网络能够关注每个通道的不同帧。 一维的SqueezeExcitation(SE)块重新调整中间帧级特征图的通道,以在局部操作的卷积块中插入全局上下文信息。 接下来,一维Res2块的集成通过以分层方式使用分组卷积来提高性能,同时减少总参数数量。

最后,多层特征聚合(MFA)通过在统计池化之前将最终的帧级特征图与前几层的中间特征图连接起来,合并互补信息。

网络通过在训练语料库中的说话人身份上优化AAMsoftmax损失来进行训练。与常规的softmax损失相比,AAM-softmax在细粒度分类和验证问题中是一个强大的增强。它直接优化了说话人嵌入之间的余弦距离。

该模型在说话人验证说话人分割方面表现出色。我们发现它在其他话语级别分类任务中也非常有效,例如语言识别、情感识别和关键词检测。

请查看原始论文以获取更多信息

结论

在本教程中,我们展示了如何使用SpeechBrain从头创建一个话语级别的分类器。所提出的系统包含了开发一个最先进系统所需的所有基本要素(即数据增强、特征提取、编码、统计池化、分类器等)。

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

引用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}
}