音频课程文档

微调 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 包含大约十个小时标记的迪维希语数据,其中三个小时是保留的测试数据。对于微调来说,这数据量非常少,因此我们将依靠 Whisper 在预训练期间获得的广泛的多语言 ASR 知识来处理低资源迪维希语。

使用 🤗 Datasets,下载和准备数据非常简单。我们只需一行代码即可下载和准备 Common Voice 13 分割。由于迪维希语是非常低资源的,我们将合并 trainvalidation 分割,以提供大约七个小时的训练数据。我们将使用三个小时的 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" 应设置为源音频语言,任务对于语音识别应设置为 "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 预训练的 最相似 语言。迪维希语的 Wikipedia 文章指出,迪维希语与斯里兰卡的僧伽罗语密切相关。如果我们再次检查语言代码,我们可以看到僧伽罗语存在于 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_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,首先将 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 以进行训练,因为我们正在使用梯度检查点,并且两者不兼容。我们还将覆盖两个生成参数以控制模型在推理期间的行为:我们将通过设置 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

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

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 进行补偿。

输出

训练损失 Epoch 步骤 验证损失 Wer Ortho 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)并使用更大的预训练检查点(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")

结论

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

如果您一路跟进到这里,那么您现在应该已经拥有了一个用于语音识别的微调检查点,做得好!🥳 更重要的是,您已经掌握了在任何语音识别数据集或领域上微调 Whisper 模型所需的所有工具。还在等什么呢!选择 选择数据集 章节中介绍的数据集之一,或者选择您自己的数据集,看看您是否能获得最先进的性能!排行榜正在等着您...

< > 在 GitHub 上更新