音频课程文档

微调 ASR 模型

Hugging Face's logo
加入 Hugging Face 社区

并获得增强的文档体验

开始使用

微调 ASR 模型

在本节中,我们将逐步指导您如何在 Common Voice 13 数据集上微调 Whisper 进行语音识别。我们将使用模型的“small”版本和相对轻量级的数据集,使您能够在任何具有 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 身份验证令牌:here 并在提示时输入

from huggingface_hub import notebook_login

notebook_login()

输出

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

加载数据集

Common Voice 13 包含大约十小时的马尔代夫语标记数据,其中三小时是保留的测试数据。这对于微调来说数据量极少,因此我们将依靠利用 Whisper 在预训练期间获得的广泛多语言 ASR 知识来处理资源稀缺的马尔代夫语。

使用 🤗 Datasets,下载和准备数据非常简单。我们只需一行代码即可下载和准备 Common Voice 13 分割。由于马尔代夫语资源非常稀缺,我们将合并 `train` 和 `validation` 分割,以提供大约七小时的训练数据。我们将使用三小时的 `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 包含额外的元数据信息,如 `accent` 和 `locale`,这些信息我们可以忽略,因为它们与 ASR 无关。为了使笔记本尽可能通用,我们只考虑输入音频和转录文本进行微调,而忽略额外的元数据信息

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

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

ASR 管道可分为三个阶段:

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

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

执行多语言微调时,在实例化处理器时需要设置 `“language”` 和 `“task”`。`“language”` 应设置为源音频语言,任务设置为 `“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. 我们使用特征提取器从一维音频数组计算对数梅尔谱图输入特征。
  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_features` 和 `labels`:`input_features` 必须由特征提取器处理,`labels` 必须由分词器处理。

`input_features` 已填充到 30 秒并转换为固定维度的对数梅尔谱图,因此我们所要做的就是使用特征提取器的 `.pad` 方法将其转换为批处理的 PyTorch 张量,并设置 `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` 的函数首先将 `label_ids` 中的 `-100` 替换为 `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` 进行训练,因为我们正在使用 梯度检查点,两者不兼容。我们还将覆盖两个生成参数以控制模型在推理期间的行为:通过设置 `language` 和 `task` 参数强制在生成期间使用语言和任务标记,并重新启用生成缓存以加速推理时间

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。这个步数足以使 WER 相较于预训练的 Whisper 模型获得显著改进,同时确保在 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 “内存不足”错误。在这种情况下,您可以将 `per_device_train_batch_size` 逐步减小 2 倍,并采用 `gradient_accumulation_steps` 来补偿。

输出

训练损失 轮次 步骤 验证损失 正字法词错误率 词错误率 (Wer)
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*,以及使用更大的预训练检查点(`medium` 或 `large`),结果可能会进一步改善。我们将其留作读者的练习。

分享您的模型

现在,您可以使用 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 模型所需的所有工具。还在等什么!选择“选择数据集”部分中涵盖的一个数据集,或者选择您自己的数据集,看看您能否获得最先进的性能!排行榜正等着您……

< > 在 GitHub 上更新