音频课程文档

音乐分类模型的微调

Hugging Face's logo
加入 Hugging Face 社区

并获得增强的文档体验

开始使用

音乐分类模型的微调

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

数据集

为了训练我们的模型,我们将使用 GTZAN 数据集,这是一个流行的音乐流派分类数据集,包含 1,000 首歌曲。每首歌曲都是一段 30 秒的剪辑,来自 10 种音乐流派之一,从迪斯科到金属音乐。我们可以使用 🤗 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 个示例而不是 1,000 个。

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

接下来,我们使用 🤗 Datasets 中的 cast_column() 方法和 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)形成对比,在 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 用于当我们一次处理一批音频输入时 - 它用于告诉模型我们在哪里填充了不同长度的输入。

我们可以看到,均值现在非常接近于零,方差正好为 1!这正是我们希望在将音频样本馈送到 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 身份验证令牌 here

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% - 对于仅使用 899 个训练数据示例进行 10 个 epoch 的训练来说还不错!我们可以通过训练更多 epoch、使用dropout等正则化技术或将每个音频示例从 30 秒细分为 15 秒片段以使用更有效的数据预处理策略来改进此结果。

最大的问题是这与其他音乐分类系统相比如何 🤔 为此,我们可以查看 autoevaluate 排行榜,这是一个按语言和数据集对模型进行分类,然后根据其准确率对其进行排名的排行榜。

当我们将训练结果推送到 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 上分享该演示。

< > 在 GitHub 上更新