音频课程文档

微调用于音乐分类的模型

Hugging Face's logo
加入 Hugging Face 社区

并获得增强文档体验

开始

微调用于音乐分类的模型

在本节中,我们将介绍一个逐步指南,指导您如何微调用于音乐分类的仅编码器 Transformer 模型。在本演示中,我们将使用一个轻量级模型和一个相当小的数据集,这意味着代码可以在任何消费级 GPU 上端到端运行,包括 Google Colab 免费层提供的 T4 16GB GPU。本节包含一些提示,如果您遇到较小的 GPU 和内存问题,可以尝试这些提示。

数据集

为了训练我们的模型,我们将使用 GTZAN 数据集,这是一个流行的数据集,包含 1000 首用于音乐流派分类的歌曲。每首歌曲都是从 10 种音乐流派中的一首中截取的 30 秒片段,涵盖迪斯科到金属。我们可以使用 🤗 Datasets 的 load_dataset() 函数从 Hugging Face Hub 获取音频文件及其相应的标签

from datasets import load_dataset

gtzan = load_dataset("marsyas/gtzan", "all")
gtzan

输出

Dataset({
    features: ['file', 'audio', 'genre'],
    num_rows: 999
})

GTZAN 中的录音之一已损坏,因此已从数据集中删除。这就是我们有 999 个示例而不是 1000 个示例的原因。

GTZAN 没有提供预定义的验证集,因此我们必须自己创建一个。数据集在流派之间是平衡的,因此我们可以使用 train_test_split() 方法快速创建 90/10 拆分,如下所示

gtzan = gtzan["train"].train_test_split(seed=42, shuffle=True, test_size=0.1)
gtzan

输出

DatasetDict({
    train: Dataset({
        features: ['file', 'audio', 'genre'],
        num_rows: 899
    })
    test: Dataset({
        features: ['file', 'audio', 'genre'],
        num_rows: 100
    })
})

很好,现在我们已经获得了训练集和验证集,让我们看看其中一个音频文件

gtzan["train"][0]

输出

{
    "file": "~/.cache/huggingface/datasets/downloads/extracted/fa06ce46130d3467683100aca945d6deafb642315765a784456e1d81c94715a8/genres/pop/pop.00098.wav",
    "audio": {
        "path": "~/.cache/huggingface/datasets/downloads/extracted/fa06ce46130d3467683100aca945d6deafb642315765a784456e1d81c94715a8/genres/pop/pop.00098.wav",
        "array": array(
            [
                0.10720825,
                0.16122437,
                0.28585815,
                ...,
                -0.22924805,
                -0.20629883,
                -0.11334229,
            ],
            dtype=float32,
        ),
        "sampling_rate": 22050,
    },
    "genre": 7,
}

正如我们在 单元 1 中看到的,音频文件表示为一维 NumPy 数组,其中数组的值表示该时间步的振幅。对于这些歌曲,采样率为 22,050 Hz,这意味着每秒采样 22,050 个振幅值。在使用具有不同采样率的预训练模型时,我们必须牢记这一点,并自行转换采样率以确保它们匹配。我们还可以看到流派表示为一个整数或类别标签,这是模型将做出预测的格式。让我们使用 genre 特征的 int2str() 方法将这些整数映射到人类可读的名称

id2label_fn = gtzan["train"].features["genre"].int2str
id2label_fn(gtzan["train"][0]["genre"])

输出

'pop'

此标签看起来是正确的,因为它与音频文件的文件名匹配。现在,让我们使用 Gradio 使用 Blocks API 创建一个简单的界面,以便收听更多示例

import gradio as gr


def generate_audio():
    example = gtzan["train"].shuffle()[0]
    audio = example["audio"]
    return (
        audio["sampling_rate"],
        audio["array"],
    ), id2label_fn(example["genre"])


with gr.Blocks() as demo:
    with gr.Column():
        for _ in range(4):
            audio, label = generate_audio()
            output = gr.Audio(audio, label=label)

demo.launch(debug=True)

从这些示例中,我们可以肯定地听到流派之间的区别,但 Transformer 可以做到这一点吗?让我们训练一个模型来找出答案!首先,我们需要找到一个适合此任务的预训练模型。让我们看看如何做到这一点。

选择用于音频分类的预训练模型

首先,让我们选择一个适合音频分类的预训练模型。在这个领域,预训练通常是在大量未标记的音频数据上进行的,使用诸如 LibriSpeechVoxpopuli 之类的数据集。在 Hugging Face Hub 上查找这些模型的最佳方法是使用“音频分类”过滤器,如上一节所述。虽然 Wav2Vec2 和 HuBERT 等模型非常流行,但我们将使用一个名为DistilHuBERT 的模型。这是一个 HuBERT 模型的更小(或蒸馏)版本,训练速度快约 73%,同时保留了大部分性能。

从音频到机器学习特征

预处理数据

与 NLP 中的标记化类似,音频和语音模型要求输入以模型可以处理的格式进行编码。在 🤗 Transformers 中,从音频到输入格式的转换由模型的特征提取器处理。与标记器类似,🤗 Transformers 提供了一个方便的 AutoFeatureExtractor 类,可以自动为给定模型选择正确的特征提取器。要了解我们如何处理音频文件,让我们首先实例化来自预训练检查点的 DistilHuBERT 的特征提取器

from transformers import AutoFeatureExtractor

model_id = "ntu-spml/distilhubert"
feature_extractor = AutoFeatureExtractor.from_pretrained(
    model_id, do_normalize=True, return_attention_mask=True
)

由于模型和数据集的采样率不同,因此我们必须在将其传递给特征提取器之前将音频文件重新采样到 16,000 Hz。我们可以先从特征提取器中获取模型的采样率来做到这一点

sampling_rate = feature_extractor.sampling_rate
sampling_rate

输出

16000

接下来,我们使用 cast_column() 方法和 🤗 Datasets 中的 Audio 特征对数据集进行重新采样

from datasets import Audio

gtzan = gtzan.cast_column("audio", Audio(sampling_rate=sampling_rate))

现在,我们可以检查数据集训练拆分的第一个示例,以验证它是否确实为 16,000 Hz。🤗 Datasets 将在我们加载每个音频样本时动态重新采样音频文件

gtzan["train"][0]

输出

{
    "file": "~/.cache/huggingface/datasets/downloads/extracted/fa06ce46130d3467683100aca945d6deafb642315765a784456e1d81c94715a8/genres/pop/pop.00098.wav",
    "audio": {
        "path": "~/.cache/huggingface/datasets/downloads/extracted/fa06ce46130d3467683100aca945d6deafb642315765a784456e1d81c94715a8/genres/pop/pop.00098.wav",
        "array": array(
            [
                0.0873509,
                0.20183384,
                0.4790867,
                ...,
                -0.18743178,
                -0.23294401,
                -0.13517427,
            ],
            dtype=float32,
        ),
        "sampling_rate": 16000,
    },
    "genre": 7,
}

太好了!我们可以看到采样率已降采样至 16kHz。数组值也不同,因为我们现在每 1.5 个以前的值只有大约一个振幅值。

Wav2Vec2 和 HuBERT 等模型的一个定义特征是它们接受与语音信号原始波形的浮点数组作为输入。这与其他模型(例如 Whisper)形成对比,在其他模型中,我们将原始音频波形预处理为频谱图格式。

我们提到音频数据表示为一维数组,因此它已经处于可以被模型读取的正确格式(一组在离散时间步长上的连续输入)。那么,特征提取器到底做了什么呢?

嗯,音频数据确实处于正确格式,但我们没有对它可以取的值施加任何限制。为了使我们的模型能以最佳方式工作,我们希望将所有输入保持在相同的动态范围内。这将确保我们对样本获得相似的激活和梯度范围,有助于在训练期间保持稳定性和收敛。

为此,我们通过将每个样本重新缩放到零均值和单位方差来规范化我们的音频数据,这是一个称为特征缩放的过程。这正是我们的特征提取器所执行的特征规范化!

我们可以通过将特征提取器应用于第一个音频样本,来查看其在运行时的效果。首先,让我们计算原始音频数据的均值和方差。

import numpy as np

sample = gtzan["train"][0]["audio"]

print(f"Mean: {np.mean(sample['array']):.3}, Variance: {np.var(sample['array']):.3}")

输出

Mean: 0.000185, Variance: 0.0493

我们可以看到,均值已经接近零,但方差更接近 0.05。如果样本的方差更大,可能会导致我们的模型出现问题,因为音频数据的动态范围将非常小,因此难以分离。让我们应用特征提取器,看看输出是什么样的。

inputs = feature_extractor(sample["array"], sampling_rate=sample["sampling_rate"])

print(f"inputs keys: {list(inputs.keys())}")

print(
    f"Mean: {np.mean(inputs['input_values']):.3}, Variance: {np.var(inputs['input_values']):.3}"
)

输出

inputs keys: ['input_values', 'attention_mask']
Mean: -4.53e-09, Variance: 1.0

好!我们的特征提取器返回一个包含两个数组的字典:input_valuesattention_maskinput_values 是我们将传递给 HuBERT 模型的预处理音频输入。 attention_mask 用于我们一次处理一音频输入时 - 它用于告诉模型我们已对哪些不同长度的输入进行了填充。

我们可以看到,均值现在非常接近零,方差正好为一!这正是我们将音频样本馈送到 HuBERT 模型之前所期望的格式。

请注意,我们如何将音频数据的采样率传递给我们的特征提取器。这是一个好习惯,因为特征提取器在后台执行一个检查,以确保我们的音频数据的采样率与模型期望的采样率匹配。如果我们的音频数据的采样率与模型的采样率不匹配,则需要将音频数据上采样或下采样到正确的采样率。

太好了,现在我们知道如何处理重新采样的音频文件,最后要做的是定义一个可以应用于数据集中的所有示例的函数。由于我们预计音频剪辑的长度为 30 秒,因此我们还将使用特征提取器的 max_lengthtruncation 参数截断任何更长的剪辑,如下所示

max_duration = 30.0


def preprocess_function(examples):
    audio_arrays = [x["array"] for x in examples["audio"]]
    inputs = feature_extractor(
        audio_arrays,
        sampling_rate=feature_extractor.sampling_rate,
        max_length=int(feature_extractor.sampling_rate * max_duration),
        truncation=True,
        return_attention_mask=True,
    )
    return inputs

定义了此函数后,我们现在可以使用 map() 方法将其应用于数据集。.map() 方法支持处理示例的批次,我们通过设置 batched=True 来启用此功能。默认批次大小为 1000,但我们将将其缩减到 100,以确保峰值 RAM 停留在 Google Colab 免费层的合理范围内

gtzan_encoded = gtzan.map(
    preprocess_function,
    remove_columns=["audio", "file"],
    batched=True,
    batch_size=100,
    num_proc=1,
)
gtzan_encoded

输出

DatasetDict({
    train: Dataset({
        features: ['genre', 'input_values','attention_mask'],
        num_rows: 899
    })
    test: Dataset({
        features: ['genre', 'input_values','attention_mask'],
        num_rows: 100
    })
})
如果您在执行上述代码时用尽了设备的 RAM,则可以调整批次参数以降低峰值 RAM 使用量。特别是,以下两个参数可以修改:* batch_size:默认为 1000,但在上面设置为 100。尝试再次将其缩减 2 倍,降至 50 * writer_batch_size:默认为 1000。尝试将其缩减到 500,如果不起作用,则再次将其缩减 2 倍,降至 250

为了简化训练,我们已从数据集中删除了 audiofile 列。input_values 列包含编码的音频文件,attention_mask 包含 0/1 值的二元掩码,指示我们对音频输入进行了填充的位置,而 genre 列包含相应的标签(或目标)。为了让 Trainer 能够处理类标签,我们需要将 genre 列重命名为 label

gtzan_encoded = gtzan_encoded.rename_column("genre", "label")

最后,我们需要从数据集中获取标签映射。此映射将从整数 ID(例如 7)到人类可读的类标签(例如 "pop")以及反过来进行转换。通过这样做,我们可以将模型的整数 ID 预测转换为人类可读的格式,使我们能够在任何下游应用程序中使用模型。我们可以通过使用 int2str() 方法来实现,如下所示

id2label = {
    str(i): id2label_fn(i)
    for i in range(len(gtzan_encoded["train"].features["label"].names))
}
label2id = {v: k for k, v in id2label.items()}

id2label["7"]
'pop'

好了,我们现在拥有了一个可以用于训练的数据集!让我们来看看如何在该数据集上训练模型。

微调模型

为了微调模型,我们将使用 🤗 Transformers 中的 Trainer 类。正如我们在其他章节中所看到的,Trainer 是一个高级 API,旨在处理最常见的训练场景。在本例中,我们将使用 Trainer 在 GTZAN 上微调模型。为此,我们需要先为这项任务加载一个模型。我们可以使用 AutoModelForAudioClassification 类来实现,该类将自动向我们预先训练的 DistilHuBERT 模型添加合适的分类头。让我们继续实例化模型

from transformers import AutoModelForAudioClassification

num_labels = len(id2label)

model = AutoModelForAudioClassification.from_pretrained(
    model_id,
    num_labels=num_labels,
    label2id=label2id,
    id2label=id2label,
)

我们强烈建议您在训练期间直接将模型检查点上传到 Hugging Face Hub。Hub 提供了以下功能:

  • 集成版本控制:您可以确保在训练期间不会丢失任何模型检查点。
  • Tensorboard 日志:跟踪训练过程中的重要指标。
  • 模型卡:记录模型的功能及其预期用途。
  • 社区:一种轻松与社区共享和协作的便捷方式! 🤗

将笔记本链接到 Hub 很简单 - 只需在提示时输入您的 Hub 身份验证令牌即可。您可以 此处找到您的 Hub 身份验证令牌

from huggingface_hub import notebook_login

notebook_login()

输出

Login successful
Your token has been saved to /root/.huggingface/token

下一步是定义训练参数,包括批次大小、梯度累积步数、训练轮数和学习率。

from transformers import TrainingArguments

model_name = model_id.split("/")[-1]
batch_size = 8
gradient_accumulation_steps = 1
num_train_epochs = 10

training_args = TrainingArguments(
    f"{model_name}-finetuned-gtzan",
    evaluation_strategy="epoch",
    save_strategy="epoch",
    learning_rate=5e-5,
    per_device_train_batch_size=batch_size,
    gradient_accumulation_steps=gradient_accumulation_steps,
    per_device_eval_batch_size=batch_size,
    num_train_epochs=num_train_epochs,
    warmup_ratio=0.1,
    logging_steps=5,
    load_best_model_at_end=True,
    metric_for_best_model="accuracy",
    fp16=True,
    push_to_hub=True,
)

这里我们设置了 push_to_hub=True 来启用在训练期间自动上传我们微调后的检查点。如果您不希望将检查点上传到 Hub,可以将其设置为 False

我们需要做的最后一件事是定义指标。由于数据集是平衡的,我们将使用准确率作为指标,并使用 🤗 Evaluate 库加载它。

import evaluate
import numpy as np

metric = evaluate.load("accuracy")


def compute_metrics(eval_pred):
    """Computes accuracy on a batch of predictions"""
    predictions = np.argmax(eval_pred.predictions, axis=1)
    return metric.compute(predictions=predictions, references=eval_pred.label_ids)

我们现在拥有了所有部分!让我们实例化 Trainer 并训练模型

from transformers import Trainer

trainer = Trainer(
    model,
    training_args,
    train_dataset=gtzan_encoded["train"],
    eval_dataset=gtzan_encoded["test"],
    tokenizer=feature_extractor,
    compute_metrics=compute_metrics,
)

trainer.train()

根据您的 GPU,您可能在开始训练时遇到 CUDA "out-of-memory" 错误。在这种情况下,您可以将 batch_size 逐步缩减 2 倍,并使用 gradient_accumulation_steps 进行补偿。

输出

| Training Loss | Epoch | Step | Validation Loss | Accuracy |
|:-------------:|:-----:|:----:|:---------------:|:--------:|
| 1.7297        | 1.0   | 113  | 1.8011          | 0.44     |
| 1.24          | 2.0   | 226  | 1.3045          | 0.64     |
| 0.9805        | 3.0   | 339  | 0.9888          | 0.7      |
| 0.6853        | 4.0   | 452  | 0.7508          | 0.79     |
| 0.4502        | 5.0   | 565  | 0.6224          | 0.81     |
| 0.3015        | 6.0   | 678  | 0.5411          | 0.83     |
| 0.2244        | 7.0   | 791  | 0.6293          | 0.78     |
| 0.3108        | 8.0   | 904  | 0.5857          | 0.81     |
| 0.1644        | 9.0   | 1017 | 0.5355          | 0.83     |
| 0.1198        | 10.0  | 1130 | 0.5716          | 0.82     |

训练大约需要 1 小时,具体取决于您的 GPU 或分配给 Google Colab 的 GPU。我们最佳的评估准确率为 83% - 对于只有 10 个轮次和 899 个训练数据示例来说,还不错!我们可以通过训练更多轮次、使用正则化技术(例如dropout)或将每个音频示例从 30 秒细分为 15 秒片段以使用更有效的 数据预处理策略来进一步提高此结果。

最关键的问题是,它与其他音乐分类系统相比如何 🤔 为此,我们可以查看 自动评估排行榜,该排行榜按语言和数据集对模型进行分类,然后根据其准确率对模型进行排名。

当我们将训练结果推送到 Hub 时,我们可以自动将我们的检查点提交到排行榜 - 我们只需设置适当的关键字参数(kwargs)即可。您可以相应地更改这些值以匹配您的数据集、语言和模型名称

kwargs = {
    "dataset_tags": "marsyas/gtzan",
    "dataset": "GTZAN",
    "model_name": f"{model_name}-finetuned-gtzan",
    "finetuned_from": model_id,
    "tasks": "audio-classification",
}

训练结果现在可以上传到 Hub。为此,请执行 .push_to_hub 命令

trainer.push_to_hub(**kwargs)

这将在 "your-username/distilhubert-finetuned-gtzan" 下保存训练日志和模型权重。对于此示例,请查看上传内容,地址为 "sanchit-gandhi/distilhubert-finetuned-gtzan".

共享模型

您现在可以使用 Hub 上的链接与任何人共享此模型。他们可以使用标识符 "your-username/distilhubert-finetuned-gtzan" 直接将其加载到 pipeline() 类中。例如,要加载微调后的检查点 "sanchit-gandhi/distilhubert-finetuned-gtzan"

from transformers import pipeline

pipe = pipeline(
    "audio-classification", model="sanchit-gandhi/distilhubert-finetuned-gtzan"
)

结论

在本节中,我们介绍了有关微调 DistilHuBERT 模型以进行音乐分类的分步指南。虽然我们重点介绍了音乐分类任务和 GTZAN 数据集,但此处介绍的步骤更广泛地适用于任何音频分类任务 - 相同的脚本可用于语音音频分类任务,如关键词识别或语言识别。您只需要将数据集替换为对应于您感兴趣任务的数据集!如果您有兴趣为音频分类微调其他 Hugging Face Hub 模型,我们鼓励您查看 🤗 Transformers 存储库中的其他 示例

在下一节中,我们将使用您刚刚微调的模型构建一个音乐分类演示,您可以在 Hugging Face Hub 上分享。