微调 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 的拆分。由于迪维希语的资源非常匮乏,我们将合并 train
和 validation
拆分,以提供大约 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
})
})
大多数 ASR 数据集仅提供输入音频样本 (audio
) 和相应的转录文本 (sentence
)。Common Voice 包含其他元数据信息,例如 accent
和 locale
,这些信息我们可以忽略 ASR。为了使笔记本尽可能通用,我们仅考虑用于微调的输入音频和转录文本,丢弃其他元数据信息。
common_voice = common_voice.select_columns(["audio", "sentence"])
特征提取器、分词器和处理器
ASR 管道可以分解成三个阶段
- 特征提取器,它将原始音频输入预处理为对数梅尔谱图
- 执行序列到序列映射的模型
- 分词器,它将预测的标记后处理为文本
在 🤗 Transformers 中,Whisper 模型具有关联的特征提取器和分词器,分别称为WhisperFeatureExtractor 和 WhisperTokenizer。为了简化我们的工作,这两个对象被封装在一个名为 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))
现在我们可以编写一个函数来准备我们的数据,使其准备好用于模型。
- 我们通过调用
sample["audio"]
逐个样本地加载和重采样音频数据。如上所述,🤗 Datasets 会动态执行任何必要的重采样操作。 - 我们使用特征提取器从我们的 1 维音频数组计算对数梅尔谱图输入特征。
- 我们通过使用标记器将转录编码为标签 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 秒并转换为固定维度的对数梅尔谱图,因此我们只需将其转换为批处理的 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
以进行训练,因为我们正在使用梯度检查点,这两者不兼容。我们还将覆盖两个生成参数以控制模型在推理期间的行为:我们将通过设置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。与预训练的 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,
)
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)以及使用更大的预训练检查点(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 模型所需的所有工具。还在等什么!选择“选择数据集”部分中介绍的数据集之一,或选择您自己的数据集,看看您是否可以获得最先进的性能!排行榜在等着您……