音频课程文档

微调模型进行音乐分类

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 中看到的,音频文件表示为 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 等其他模型形成对比,在 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 身份验证令牌

from huggingface_hub import notebook_login

notebook_login()

输出

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

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

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 的“内存不足”错误。在这种情况下,您可以将 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 秒的片段来改进此结果,以使用更有效的数据预处理策略。

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

当我们将训练结果推送到 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 上更新