在 GitHub 上执行或查看/下载此笔记本
从零开始进行语音分类
您想了解如何使用 SpeechBrain 实现一个分类系统吗?不用再找了,您来对地方了。本教程将引导您完成在 SpeechBrain 中实现话语级分类器所需的所有步骤。
本教程将首先重点介绍说话人识别,并在此过程中描述如何将其扩展到许多其他分类任务,例如语言识别、情感识别、声音分类、关键词识别等。
模型
许多神经网络模型可用于解决此类任务。在本教程中,我们将重点介绍 TDNN 分类器(xvector)以及一个非常新的模型,称为 ECAPA-TDNN,该模型在说话人确认和分割方面表现出色。
数据
训练将使用一个名为 mini-librispeech 的小型开源数据集,该数据集只包含少量训练数据。在实际情况下,您需要更大的数据集。对于实际任务的一些示例,请查看我们的 Voxceleb recipe。
代码
在本教程中,我们将参考 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 中训练话语级分类器相对容易。需要遵循的步骤是
准备您的数据。此步骤的目标是创建数据清单文件(CSV 或 JSON 格式)。数据清单文件告诉 SpeechBrain 在哪里找到语音数据及其对应的话语级分类(例如,说话人身份)。在本教程中,数据清单文件由 mini_librispeech_prepare.py 创建。
训练分类器。此时,我们准备好训练分类器。要训练基于 TDNN + 统计池化 (xvectors) 的说话人身份识别分类器,请运行以下命令
cd speechbrain/templates/speaker_id/
python train.py train.yaml
稍后,我们将描述如何接入另一个称为强调通道注意力、传播和聚合模型 (ECAPA) 的模型,该模型在说话人识别任务中提供了令人印象深刻的性能。
使用分类器(推理):训练后,我们可以使用分类器进行推理。一个名为
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"
},
}
如您所见,我们有一个分层结构,其中第一个键是口语句子的唯一标识符。然后,我们指定解决任务所需的所有字段。例如,我们报告语音录音的路径、其秒级的长度(如果想在创建 mini-batch 之前对句子进行排序则需要),以及给定录音中说话人的说话人身份。
实际上,您可以在此处指定所有需要的条目(语言识别、情感标注等)。但是,这些条目的名称必须与实验脚本(例如 train.py)期望的名称匹配。稍后我们将对此进行更详细的阐述。
您可能已经注意到,我们定义了一个名为 data_root
的特殊变量。这允许用户从命令行(或从 yaml 超参数文件)动态更改数据文件夹。
准备脚本
每个数据集的格式都不同。解析您自己的数据集并创建 JSON 或 CSV 文件的脚本需要您自己编写。大多数情况下,这非常简单。
例如,对于 mini-librispeech 数据集,我们编写了这个简单的数据准备脚本,名为 mini_librispeech_prepare.py。
此函数自动下载数据(在此示例中是公开可用的)。我们搜索所有音频文件,并在读取它们时创建带有说话人身份标注的 JSON 文件。
您可以使用此脚本作为在目标数据集上进行自定义准备的良好基础。如您所见,我们创建了三个独立的数据清单文件来管理训练、验证和测试阶段。
在本地复制您的数据
在 HPC 集群中使用 speechbrain(或任何其他工具包)时,一个好的做法是将您的数据集压缩到单个文件中,并将数据复制(并解压缩)到计算节点的本地文件夹中。这会使代码快得多,因为数据不是从共享文件系统而是从本地文件系统获取的。此外,您不会通过大量的读取操作损害共享文件系统的性能。我们强烈建议用户遵循此方法(在 Google Colab 中不可能实现)。
步骤 2:训练分类器
我们现在展示如何使用 SpeechBrain 训练一个话语级分类器。所提出的 recipe 执行特征计算/归一化,使用编码器处理特征,并在其之上应用分类器。还使用了数据增强来提高系统性能。
训练说话人身份识别模型
我们将训练用于 x-vector 的基于 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
从打印信息中可以看出,在前几个 epoch 中,验证和训练的损失都下降得非常快。然后,我们基本上看到一些微小的改进和性能波动。
在训练结束时,验证误差应该趋近于零(或非常接近零)。
本教程中提出的任务非常简单,因为我们只需要对 mini-librispeech 数据集的 28 位说话人进行分类。将本教程仅作为解释如何设置开发语音分类器所需所有组件的示例。如果您想查看流行说话人识别数据集上的示例,请参考我们的 voxceleb recipe
在深入研究代码之前,让我们看看在指定的 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 计数器等)。嵌入模型的参数记录在 embedding_model.ckpt
文件中,而分类器的参数则在 classifier.ckpt
中。这只是一个可以使用 torch.load
读取的二进制格式。
保存文件夹中还包含标签编码器(label_encoder.txt
),它将每个说话人身份条目映射到其对应的索引。
'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
现在我们可以指定一些训练超参数,例如 epoch 数量、批处理大小、学习率、epoch 数量和嵌入维度。
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
可用于在训练 epoch 内每隔 N 分钟保存检查点。在某些情况下,一个 epoch 可能需要几个小时,定期保存检查点是一种良好且安全的做法。对于这个基于微小数据集的简单教程来说,此功能并不是必需的。
我们现在可以定义训练模型所需的最重要模块
# 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 个 epoch 内采用线性学习率衰减。
现在让我们将最佳模型保存到一个单独的文件夹中(对稍后解释的推理部分很有用)
# 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'
实验文件
现在让我们看看 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,
)
# 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 文件。
使用 load_hyperpyyaml
读取 yaml 文件后,超参数文件中声明的所有对象都被初始化并以字典形式可用(以及 yaml 文件中报告的其他函数和参数)。例如,我们将有 hparams['embedding_model']
、hparams['classifier']
、hparams['batch_size']
等。
我们还运行数据准备脚本 prepare_mini_librispeech
来创建数据清单文件。它被 sb.utils.distributed.run_on_main
包裹,因为此操作将清单文件写入磁盘,即使在多 GPU DDP 场景下也必须在单个进程上完成。有关如何使用多个 GPU 的更多信息,请查看此教程。
数据 IO 流水线
然后,我们调用一个特殊函数来创建用于训练、验证和测试的数据集对象。
# 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
并将其与上面定义的处理函数连接。我们定义了要公开的期望输出键。这些键将在 brain 类中通过 batch 变量可用,如下所示
batch.id
batch.sig
batch.spk_id_encoded
函数的最后一部分用于初始化标签编码器。标签编码器接收训练数据集作为输入,并为所有找到的 spk_id
条目分配不同的索引。这些索引将对应于分类器的输出索引。
定义数据集后,main 函数可以继续进行 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
在此例中,计算链非常简单。我们只需将 batch 放到正确的设备上并计算声学特征。然后,我们使用 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 文件中声明了环境损坏时,我们在同一个 batch 中串联信号的干净版本和增强版本。
这种方法使 batch 大小加倍(因此也增加了所需的 GPU 内存),但它实现了一个非常强大的正则化器。在同一个 batch 中同时包含信号的干净版本和噪声版本,迫使梯度指向参数空间中对信号失真具有鲁棒性的方向。
计算目标
现在让我们看看 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
输入的预测是在 forward 方法中计算得到的。通过将这些预测与目标标签进行比较来评估成本函数。这是通过负对数似然 (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:将编码器应用于输入 batch 并返回一些编码嵌入。
classify_batch:执行完整的分类步骤,并返回分类器的输出概率、最佳分数、最佳类别的索引及其文本格式的标签(参见上面的示例)。
要将此接口与之前训练的模型一起使用,我们必须创建一个推理 yaml 文件,该文件与用于训练的文件略有不同。主要区别如下
您可以移除所有仅用于训练的超参数和对象。您只需保留与模型定义相关的部分。
您必须分配一个
Categorical encoder
对象,该对象允许您将索引转换为文本标签。您必须使用 pre-trainer 将您的模型与其相应的文件链接起来。
推理 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:将特征转换为固定大小的嵌入。
classifier:在嵌入之上对 N 个类别执行最终分类。
如果您的模型无法以这种方式构建,您可以随时自定义 EncoderClassifier
接口以满足您的需求。请参阅此教程以获取更多信息。
扩展到不同的任务
通常情况下,您可能有自己的数据和分类任务,并且想使用您自己的模型。让我们再详细说明一下如何自定义您的 recipe。
建议:从一个正在工作的 recipe(例如此模板使用的那个)开始,并仅进行最少的修改以进行自定义。逐步测试您的模型。确保您的模型可以在由少量句子组成的微小数据集上过拟合。如果它不过拟合,则您的模型中可能存在 bug。
使用您的数据在您的任务上进行训练
如果我必须解决我的数据上的另一个话语级分类任务,例如语言识别、情感识别、声音分类、关键词识别,该怎么办?
您需要做的就是
修改 JSON 文件以包含任务所需的标注。
修改
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
中 dataloader 期望的名称匹配。例如,如果您在 JSON 中定义了 emotion 键,则在 train.py
的 dataio 流水线中应该有类似如下的内容
# 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 中已存在的模型。如果您的模型是一个简单的计算流水线,您可以使用序列容器。如果模型是一个更复杂的计算链,您可以将其创建为 torch.nn.Module
的实例,并在其中定义 __init__
和 forward
方法,就像这里一样。
定义模型后,您只需在 yaml 文件中声明它,并在 train.py
中使用即可
重要
在接入新模型时,您必须重新调整系统中最重要的超参数(例如学习率、batch 大小和架构参数),以使其良好运行。
ECAPA-TDNN 模型
我们发现对说话人识别特别有效的一个模型是 ECAPA-TDNN 模型,在此处实现。
ECAPA-TDNN 架构基于流行的 x-vector 拓扑,并引入了多项增强功能以创建更鲁棒的说话人嵌入。
池化层使用通道和上下文依赖的注意力机制,这使得网络能够关注每个通道的不同帧。一维 SqueezeExcitation (SE) 块对中间帧级特征图的通道进行重新缩放,以在局部操作的卷积块中插入全局上下文信息。接下来,集成一维 Res2-blocks 提高了性能,同时通过分层使用分组卷积减少了总参数数量。
最后,多层特征聚合 (MFA) 通过将最终的帧级特征图与前一层中间特征图串联,在统计池化之前合并互补信息。
通过优化训练语料库中说话人身份上的 AAMsoftmax 损失来训练网络。AAM-softmax 在细粒度分类和确认问题中是相对于常规 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}
}