音频课程文档

微调 ASR 模型

Hugging Face's logo
加入 Hugging Face 社区

并获得增强文档体验的访问权限

开始

微调 ASR 模型

在本节中,我们将介绍一个关于在 Common Voice 13 数据集上微调 Whisper 以进行语音识别的分步指南。我们将使用模型的“小型”版本和一个相对较轻量级的数据集,使您能够在任何 16GB+ GPU 上相当快速地运行微调,并且对磁盘空间的要求较低,例如 Google Colab 免费层提供的 16GB T4 GPU。

如果您拥有较小的 GPU 或在训练期间遇到内存问题,您可以按照提供的建议来减少内存使用量。相反,如果您有权访问更大的 GPU,您可以修改训练参数以最大化您的吞吐量。因此,无论您的 GPU 规格如何,本指南都适用!

同样,本指南概述了如何针对迪维希语微调 Whisper 模型。但是,此处介绍的步骤可以推广到 Common Voice 数据集中的任何语言,更普遍地推广到 Hugging Face Hub 上的任何 ASR 数据集。您可以调整代码以快速切换到您选择的语言,并用您的母语微调 Whisper 模型🌍

好了!现在这些都搞定了,让我们开始并启动我们的微调管道吧!

准备环境

我们强烈建议您在训练期间将模型检查点直接上传到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

加载数据集

Common Voice 13 包含大约 10 小时的带标签迪维希语数据,其中 3 小时是保留的测试数据。对于微调来说,这极少数据,因此我们将依靠利用 Whisper 在预训练期间获得的广泛的多语言 ASR 知识来处理资源匮乏的迪维希语。

使用 🤗 Datasets,下载和准备数据非常简单。我们只需一行代码即可下载并准备 Common Voice 13 的拆分。由于迪维希语的资源非常匮乏,我们将合并 trainvalidation 拆分,以提供大约 7 小时的训练数据。我们将使用 3 小时的 test 数据作为我们的保留测试集。

from datasets import load_dataset, DatasetDict

common_voice = DatasetDict()

common_voice["train"] = load_dataset(
    "mozilla-foundation/common_voice_13_0", "dv", split="train+validation"
)
common_voice["test"] = load_dataset(
    "mozilla-foundation/common_voice_13_0", "dv", split="test"
)

print(common_voice)

输出

DatasetDict({
    train: Dataset({
        features: ['client_id', 'path', 'audio', 'sentence', 'up_votes', 'down_votes', 'age', 'gender', 'accent', 'locale', 'segment', 'variant'],
        num_rows: 4904
    })
    test: Dataset({
        features: ['client_id', 'path', 'audio', 'sentence', 'up_votes', 'down_votes', 'age', 'gender', 'accent', 'locale', 'segment', 'variant'],
        num_rows: 2212
    })
})
您可以将语言标识符从 `"dv"` 更改为您选择的语言标识符。要查看 Common Voice 13 中所有可能的语言,请查看 Hugging Face Hub 上的数据集卡片:https://huggingface.co/datasets/mozilla-foundation/common_voice_13_0

大多数 ASR 数据集仅提供输入音频样本 (audio) 和相应的转录文本 (sentence)。Common Voice 包含其他元数据信息,例如 accentlocale,这些信息我们可以忽略 ASR。为了使笔记本尽可能通用,我们仅考虑用于微调的输入音频和转录文本,丢弃其他元数据信息。

common_voice = common_voice.select_columns(["audio", "sentence"])

特征提取器、分词器和处理器

ASR 管道可以分解成三个阶段

  1. 特征提取器,它将原始音频输入预处理为对数梅尔谱图
  2. 执行序列到序列映射的模型
  3. 分词器,它将预测的标记后处理为文本

在 🤗 Transformers 中,Whisper 模型具有关联的特征提取器和分词器,分别称为WhisperFeatureExtractorWhisperTokenizer。为了简化我们的工作,这两个对象被封装在一个名为 WhisperProcessor 的单个类中。我们可以调用 WhisperProcessor 来执行音频预处理和文本标记后处理。这样,我们只需要在训练期间跟踪两个对象:处理器和模型。

在执行多语言微调时,我们需要在实例化处理器时设置 "language""task""language" 应设置为源音频语言,"task" 应设置为语音识别的 "transcribe" 或语音翻译的 "translate"。这些参数修改了分词器的行为,应正确设置以确保目标标签被正确编码。

我们可以通过导入语言列表来查看 Whisper 支持的所有可能的语言。

from transformers.models.whisper.tokenization_whisper import TO_LANGUAGE_CODE

TO_LANGUAGE_CODE

如果滚动浏览此列表,您会注意到许多语言都存在,但迪维希语是少数几个不存在的语言之一!这意味着 Whisper 没有在迪维希语上进行预训练。但是,这并不意味着我们不能在它上面微调 Whisper。这样做,我们将教 Whisper 一种新的语言,即预训练检查点不支持的语言。这很酷,对吧!

当您在新语言上对其进行微调时,Whisper 在利用其对其他 96 种预训练语言的知识方面做得很好。总的来说,所有现代语言在语言上都将与 Whisper 已知的 96 种语言中的至少一种相似,因此我们将遵循这种跨语言知识表示的范式。

我们需要为在新语言上微调 Whisper 做的是找到 Whisper 预训练过的最相似的语言。迪维希语的维基百科文章指出,迪维希语与斯里兰卡的僧伽罗语密切相关。如果我们再次检查语言代码,我们可以看到僧伽罗语存在于 Whisper 语言集中,因此我们可以安全地将我们的语言参数设置为 "sinhalese"

没错!我们将从预训练的检查点加载我们的处理器,并将语言设置为"sinhalese",任务设置为"transcribe",如上所述。

from transformers import WhisperProcessor

processor = WhisperProcessor.from_pretrained(
    "openai/whisper-small", language="sinhalese", task="transcribe"
)

值得重申的是,在大多数情况下,您会发现您想要微调的语言位于预训练语言集中,在这种情况下,您可以简单地将语言直接设置为您的源音频语言!请注意,对于仅限英语的微调,这两个参数都应省略,因为在这种情况下,语言("English")和任务("transcribe")只有一个选项。

预处理数据

让我们看一下数据集特征。特别注意"audio"列 - 这详细说明了我们的音频输入的采样率。

common_voice["train"].features

输出

{'audio': Audio(sampling_rate=48000, mono=True, decode=True, id=None),
 'sentence': Value(dtype='string', id=None)}

由于我们的输入音频以 48kHz 采样,因此在将其传递给 Whisper 特征提取器之前,我们需要将其降采样到 16kHz,16kHz 是 Whisper 模型期望的采样率。

我们将使用数据集的cast_column方法将音频输入设置为正确的采样率。此操作不会就地更改音频,而是向数据集发出信号,以便在加载音频样本时动态地对其进行重采样。

from datasets import Audio

sampling_rate = processor.feature_extractor.sampling_rate
common_voice = common_voice.cast_column("audio", Audio(sampling_rate=sampling_rate))

现在我们可以编写一个函数来准备我们的数据,使其准备好用于模型。

  1. 我们通过调用sample["audio"]逐个样本地加载和重采样音频数据。如上所述,🤗 Datasets 会动态执行任何必要的重采样操作。
  2. 我们使用特征提取器从我们的 1 维音频数组计算对数梅尔谱图输入特征。
  3. 我们通过使用标记器将转录编码为标签 ID。
def prepare_dataset(example):
    audio = example["audio"]

    example = processor(
        audio=audio["array"],
        sampling_rate=audio["sampling_rate"],
        text=example["sentence"],
    )

    # compute input length of audio sample in seconds
    example["input_length"] = len(audio["array"]) / audio["sampling_rate"]

    return example

我们可以使用 🤗 Datasets 的.map方法将数据准备函数应用于我们所有的训练示例。我们将从原始训练数据中删除列(音频和文本),只保留prepare_dataset函数返回的列。

common_voice = common_voice.map(
    prepare_dataset, remove_columns=common_voice.column_names["train"], num_proc=1
)

最后,我们过滤掉音频样本时长超过 30 秒的任何训练数据。否则,这些样本将被 Whisper 特征提取器截断,这可能会影响训练的稳定性。我们定义一个函数,对于时长小于 30 秒的样本返回True,对于时长更长的样本返回False

max_input_length = 30.0


def is_audio_in_length_range(length):
    return length < max_input_length

我们通过 🤗 Datasets 的.filter方法将我们的过滤函数应用于训练数据集的所有样本。

common_voice["train"] = common_voice["train"].filter(
    is_audio_in_length_range,
    input_columns=["input_length"],
)

让我们检查一下通过此过滤步骤删除了多少训练数据。

common_voice["train"]

输出

Dataset({
    features: ['input_features', 'labels', 'input_length'],
    num_rows: 4904
})

好的!在这种情况下,我们实际上与之前具有相同数量的样本,因此没有样本时长超过 30 秒。如果您切换语言,情况可能并非如此,因此最好将此过滤步骤保留到位以确保鲁棒性。有了这些,我们的数据已完全准备好进行训练!让我们继续看看如何使用这些数据来微调 Whisper。

训练和评估

现在我们已经准备好了数据,我们可以深入了解训练流程。 🤗 Trainer 将为我们完成大部分繁重的工作。我们只需

  • 定义一个数据整理器:数据整理器获取我们预处理的数据,并准备模型所需的 PyTorch 张量。

  • 评估指标:在评估期间,我们希望使用词错率 (WER) 指标评估模型。我们需要定义一个compute_metrics函数来处理此计算。

  • 加载预训练检查点:我们需要加载预训练检查点并将其正确配置以进行训练。

  • 定义训练参数:这些参数将由 🤗 Trainer 用于构建训练计划。

一旦我们微调了模型,我们将使用测试数据对其进行评估,以验证我们是否已将其正确训练以转录迪维希语语音。

定义数据整理器

序列到序列语音模型的数据整理器是独一无二的,因为它独立处理input_featureslabelsinput_features必须由特征提取器处理,labels必须由标记器处理。

input_features已填充到 30 秒并转换为固定维度的对数梅尔谱图,因此我们只需将其转换为批处理的 PyTorch 张量即可。我们使用特征提取器的.pad方法和return_tensors=pt来执行此操作。请注意,此处没有应用其他填充,因为输入是固定维度的,input_features只是简单地转换为 PyTorch 张量。

另一方面,labels未填充。我们首先使用标记器的.pad方法将序列填充到批次中的最大长度。然后,将填充标记替换为-100,以便在计算损失时考虑这些标记。然后,我们从标签序列的开头剪掉转录开始标记,因为我们在训练期间稍后会追加它。

我们可以利用之前定义的WhisperProcessor来执行特征提取器和标记器操作。

import torch

from dataclasses import dataclass
from typing import Any, Dict, List, Union


@dataclass
class DataCollatorSpeechSeq2SeqWithPadding:
    processor: Any

    def __call__(
        self, features: List[Dict[str, Union[List[int], torch.Tensor]]]
    ) -> Dict[str, torch.Tensor]:
        # split inputs and labels since they have to be of different lengths and need different padding methods
        # first treat the audio inputs by simply returning torch tensors
        input_features = [
            {"input_features": feature["input_features"][0]} for feature in features
        ]
        batch = self.processor.feature_extractor.pad(input_features, return_tensors="pt")

        # get the tokenized label sequences
        label_features = [{"input_ids": feature["labels"]} for feature in features]
        # pad the labels to max length
        labels_batch = self.processor.tokenizer.pad(label_features, return_tensors="pt")

        # replace padding with -100 to ignore loss correctly
        labels = labels_batch["input_ids"].masked_fill(
            labels_batch.attention_mask.ne(1), -100
        )

        # if bos token is appended in previous tokenization step,
        # cut bos token here as it's append later anyways
        if (labels[:, 0] == self.processor.tokenizer.bos_token_id).all().cpu().item():
            labels = labels[:, 1:]

        batch["labels"] = labels

        return batch

现在我们可以初始化我们刚刚定义的数据整理器。

data_collator = DataCollatorSpeechSeq2SeqWithPadding(processor=processor)

继续前进!

评估指标

接下来,我们定义将在评估集上使用的评估指标。我们将使用在评估部分介绍的词错率 (WER) 指标,这是评估 ASR 系统的“事实上的”指标。

我们将从 🤗 Evaluate 中加载 WER 指标。

import evaluate

metric = evaluate.load("wer")

然后,我们只需定义一个函数,该函数获取我们的模型预测并返回 WER 指标即可。此函数称为compute_metrics,首先将-100替换为label_ids中的pad_token_id(撤消我们在数据整理器中应用的步骤,以便在损失中正确忽略填充标记)。然后,它将预测和标签 ID 解码为字符串。最后,它计算预测和参考标签之间的 WER。在这里,我们可以选择使用“规范化”的转录和预测进行评估,这些转录和预测已删除标点符号和大写。我们建议您遵循此操作,以受益于通过规范化转录获得的 WER 改进。

from transformers.models.whisper.english_normalizer import BasicTextNormalizer

normalizer = BasicTextNormalizer()


def compute_metrics(pred):
    pred_ids = pred.predictions
    label_ids = pred.label_ids

    # replace -100 with the pad_token_id
    label_ids[label_ids == -100] = processor.tokenizer.pad_token_id

    # we do not want to group tokens when computing the metrics
    pred_str = processor.batch_decode(pred_ids, skip_special_tokens=True)
    label_str = processor.batch_decode(label_ids, skip_special_tokens=True)

    # compute orthographic wer
    wer_ortho = 100 * metric.compute(predictions=pred_str, references=label_str)

    # compute normalised WER
    pred_str_norm = [normalizer(pred) for pred in pred_str]
    label_str_norm = [normalizer(label) for label in label_str]
    # filtering step to only evaluate the samples that correspond to non-zero references:
    pred_str_norm = [
        pred_str_norm[i] for i in range(len(pred_str_norm)) if len(label_str_norm[i]) > 0
    ]
    label_str_norm = [
        label_str_norm[i]
        for i in range(len(label_str_norm))
        if len(label_str_norm[i]) > 0
    ]

    wer = 100 * metric.compute(predictions=pred_str_norm, references=label_str_norm)

    return {"wer_ortho": wer_ortho, "wer": wer}

加载预训练检查点

现在让我们加载预训练的 Whisper small 检查点。同样,这可以通过 🤗 Transformers 轻松实现!

from transformers import WhisperForConditionalGeneration

model = WhisperForConditionalGeneration.from_pretrained("openai/whisper-small")

我们将use_cache设置为False以进行训练,因为我们正在使用梯度检查点,这两者不兼容。我们还将覆盖两个生成参数以控制模型在推理期间的行为:我们将通过设置languagetask参数强制生成期间的语言和任务标记,并且还重新启用缓存以加快推理时间。

from functools import partial

# disable cache during training since it's incompatible with gradient checkpointing
model.config.use_cache = False

# set language and task for generation and re-enable cache
model.generate = partial(
    model.generate, language="sinhalese", task="transcribe", use_cache=True
)

定义训练配置

在最后一步,我们定义所有与训练相关的参数。在这里,我们将训练步数设置为 500。与预训练的 Whisper 模型相比,这足以看到 WER 的显著提升,同时确保微调可以在 Google Colab 免费层上大约 45 分钟内完成。有关训练参数的更多详细信息,请参阅 Seq2SeqTrainingArguments 文档

from transformers import Seq2SeqTrainingArguments

training_args = Seq2SeqTrainingArguments(
    output_dir="./whisper-small-dv",  # name on the HF Hub
    per_device_train_batch_size=16,
    gradient_accumulation_steps=1,  # increase by 2x for every 2x decrease in batch size
    learning_rate=1e-5,
    lr_scheduler_type="constant_with_warmup",
    warmup_steps=50,
    max_steps=500,  # increase to 4000 if you have your own GPU or a Colab paid plan
    gradient_checkpointing=True,
    fp16=True,
    fp16_full_eval=True,
    evaluation_strategy="steps",
    per_device_eval_batch_size=16,
    predict_with_generate=True,
    generation_max_length=225,
    save_steps=500,
    eval_steps=500,
    logging_steps=25,
    report_to=["tensorboard"],
    load_best_model_at_end=True,
    metric_for_best_model="wer",
    greater_is_better=False,
    push_to_hub=True,
)
如果您不想将模型检查点上传到 Hub,请设置 push_to_hub=False

我们可以将训练参数与我们的模型、数据集、数据整理器和 compute_metrics 函数一起转发到 🤗 Trainer。

from transformers import Seq2SeqTrainer

trainer = Seq2SeqTrainer(
    args=training_args,
    model=model,
    train_dataset=common_voice["train"],
    eval_dataset=common_voice["test"],
    data_collator=data_collator,
    compute_metrics=compute_metrics,
    tokenizer=processor,
)

有了这些,我们就可以开始训练了!

训练

要启动训练,只需执行以下操作

trainer.train()

训练大约需要 45 分钟,具体取决于您的 GPU 或分配给 Google Colab 的 GPU。根据您的 GPU,您在开始训练时可能会遇到 CUDA "out-of-memory" 错误。在这种情况下,您可以将 per_device_train_batch_size 以 2 为因子递减,并使用 gradient_accumulation_steps 进行补偿。

输出

训练损失 轮次 步数 验证损失 词错率(正字法) 词错率
0.136 1.63 500 0.1727 63.8972 14.0661

我们的最终 WER 为 14.1% - 对于七个小时的训练数据和仅 500 个训练步数来说还不错!这相当于比预训练模型提高了 112%!这意味着我们采用了一个之前对迪维希语一无所知的模型,并对其进行了微调,使其能够在不到一个小时内以足够的准确度识别迪维希语语音 🤯

最大的问题是如何与其他 ASR 系统进行比较。为此,我们可以查看自动评估 排行榜,该排行榜按语言和数据集对模型进行分类,然后根据它们的 WER 对其进行排名。

查看排行榜,我们发现我们训练了 500 步的模型令人信服地击败了我们在上一节中评估的预训练 Whisper Small 检查点。干得好 👏

我们看到有一些检查点的表现优于我们训练的检查点。Hugging Face Hub 的优点在于它是一个协作平台 - 如果我们没有时间或资源自己进行更长时间的训练运行,我们可以加载社区中的其他人训练并慷慨分享的检查点(务必感谢他们!)。您可以像以前使用 pipeline 类那样完全相同的方式加载这些检查点!因此,没有什么可以阻止您挑选排行榜上最适合您任务的模型!

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

kwargs = {
    "dataset_tags": "mozilla-foundation/common_voice_13_0",
    "dataset": "Common Voice 13",  # a 'pretty' name for the training dataset
    "language": "dv",
    "model_name": "Whisper Small Dv - Sanchit Gandhi",  # a 'pretty' name for your model
    "finetuned_from": "openai/whisper-small",
    "tasks": "automatic-speech-recognition",
}

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

trainer.push_to_hub(**kwargs)

这将把训练日志和模型权重保存在 "your-username/the-name-you-picked" 下。对于此示例,请查看 sanchit-gandhi/whisper-small-dv 上的上传。

虽然微调后的模型在 Common Voice 13 迪维希语测试数据上产生了令人满意的结果,但它绝非最佳。本指南的目的是演示如何使用 🤗 Trainer 对多语言语音识别进行 ASR 模型的微调。

如果您有权访问自己的 GPU 或订阅了 Google Colab 付费计划,您可以将 max_steps 增加到 4000 步,通过训练更多步数来进一步提高 WER。训练 4000 步大约需要 3-5 个小时,具体取决于您的 GPU,并且产生的 WER 结果比训练 500 步低大约 3%。如果您决定训练 4000 步,我们还建议将学习率调度程序更改为线性调度(设置 lr_scheduler_type="linear"),因为这将在长时间训练运行中带来额外的性能提升。

通过优化训练超参数(例如学习率dropout)以及使用更大的预训练检查点(mediumlarge),可能会进一步改进结果。我们将其留作读者的练习。

分享您的模型

您现在可以使用 Hub 上的链接与任何人共享此模型。他们可以使用标识符 "your-username/the-name-you-picked" 直接加载到 pipeline() 对象中。例如,要加载微调的检查点 “sanchit-gandhi/whisper-small-dv”

from transformers import pipeline

pipe = pipeline("automatic-speech-recognition", model="sanchit-gandhi/whisper-small-dv")

结论

在本节中,我们介绍了使用 🤗 Datasets、Transformers 和 Hugging Face Hub 对 Whisper 模型进行语音识别微调的分步指南。我们首先加载了 Common Voice 13 数据集的迪维希语子集,并通过计算对数梅尔谱图和标记化文本对其进行了预处理。然后,我们定义了数据整理器、评估指标和训练参数,然后使用 🤗 Trainer 训练和评估我们的模型。最后,我们将微调后的模型上传到 Hugging Face Hub,并展示了如何使用 pipeline() 类共享和使用它。

如果您按照本指南操作到此步骤,您现在应该拥有一个用于语音识别的微调检查点,干得好!🥳 更重要的是,您已经具备了对任何语音识别数据集或领域微调 Whisper 模型所需的所有工具。还在等什么!选择“选择数据集”部分中介绍的数据集之一,或选择您自己的数据集,看看您是否可以获得最先进的性能!排行榜在等着您……