训练器存在的价值就是为 Transformers 下的基于 PyTorch框架的预训练模型提供完整的训练和循环评估,获取自己的新模型。训练前,将预训练模型、预处理器、数据集以及训练参数插入到训练器中;然后启动训练即可,其他的事情,训练器会自动处理。

一般的,训练器在训练过程中有几个必须的步骤:

  • 在每步的训练中计算损失;
  • 使用 [~accelerate.Accelerator.backward] 方法计算梯度;
  • 基于梯度来更新权重;
  • 循环训练,直到达到预定的 epoch 数量。

[Trainer] 训练类在 PyTorch 框架下提供了功能齐全的训练接口API;在多GPU/TPU环境下,可以提供分布式训练;在 NVIDIA GPUsAMD GUPs 以及 torch.amp 环境下提供混合精度的训练;使用 [TrainingArguments] 参数类可以支持广泛定制模型方式进行训练。这两个类为提供完整的训练API提供了支持。

但是,如果使用了其他的模型或者自有的模型,需要模型确保:

  1. 定制模型返回的必须是元组或者[~utils.ModelOutput]的子类;
  2. 当 定义了labels 参数,定制模型必须支持:一是能够计算损失;二是计算的损失必须作为第一个返回;
  3. 定制模型能够支持多标签参数(使用[TrainingArguments] 中的 label_names 标签)并能够传给训练器;但不能命名为 label

语音

语音分类模型训练

音频分类将分类标签输出分配给输入数据。唯一的区别是,使用原始音频波形而不是文本输入。音频分类的一些实际应用包括识别话音意图,语言分类,甚至通过声音识别动物物种。

本节案例将使用到:

  • MInDS-14 数据集上微调 Wav2Vec2 以对话音意图进行分类;
  • 使用微调后的模型进行推理。

💡 提示
所示的任务支持以下模型架构:
音频频谱图变换器,Data2VecAudioHubertSEWSEW-DUniSpeechUniSpeechSatWav2Vec2Wav2Vec2-ConformerWavLMWhisper

在开始之前,请确保你已安装所有必要的库:

pip install transformers datasets evaluate

加载 MInDS-14 数据集

首先从使用 Datasets 库的 load_dataset 加载 MInDS-14 数据集:

from datasets import load_dataset, Audio

minds = load_dataset("PolyAI/minds14", name="en-US", split="train")

使用 datasets.Dataset.train_test_split 方法将数据集的训练集拆分为较小的训练集和测试集。这样就有机会在处理完整数据集之前进行实验和确认一切正常。

minds = minds.train_test_split(test_size=0.2)
minds

结果为:

DatasetDict({
    train: Dataset({
        features: ['path', 'audio', 'transcription', 'english_transcription', 'intent_class', 'lang_id'],
        num_rows: 450
    })
    test: Dataset({
        features: ['path', 'audio', 'transcription', 'english_transcription', 'intent_class', 'lang_id'],
        num_rows: 113
    })
})

数据集包含许多有用的信息,如 lang_idenglish_transcription,但你可使用[datasets.Dataset.remove_columns]方法删除其他列,只需专注于 audiointent_class。:

minds = minds.remove_columns(["path", "transcription", "english_transcription", "lang_id"])
minds["train"][0]

结果为:

{'audio': {'array': array([ 0.        ,  0.        ,  0.        , ..., -0.00048828,
         -0.00024414, -0.00024414], dtype=float32),
  'path': '/root/.cache/huggingface/datasets/downloads/extracted/f14948e0e84be638dd7943ac36518a4cf3324e8b7aa331c5ab11541518e9368c/en-US~APP_ERROR/602b9a5fbb1e6d0fbce91f52.wav',
  'sampling_rate': 8000},
 'intent_class': 2}

其中:

  • audio:一个一维的数组,存储了必须调用以加载和重新采样音频文件的语音信号。
  • intent_class:代表语音意图的类别id。

为了使模型可以通过“标签id”获取“标签名称”,可创建一个将标签名称映射到整数和相反的字典来做演示:

labels = minds["train"].features["intent_class"].names

label2id, id2label = dict(), dict()
for i, label in enumerate(labels):
     label2id[label] = str(i)
     id2label[str(i)] = label
     
id2label[str(2)]

结果为:

'app_error'

预处理

现在通过加载 Wav2Vec2 特征提取器来处理音频信号:

from transformers import AutoFeatureExtractor

feature_extractor = AutoFeatureExtractor.from_pretrained("facebook/wav2vec2-base")

MInDS-14 数据集的采样率为 8000khz(可以在数据集卡片中找到此信息),这意味着需要将数据集重采样为 16000kHz 来使用预训练的 Wav2Vec2 模型:

minds = minds.cast_column("audio", Audio(sampling_rate=16_000))
minds["train"][0]

结果为:

{'audio': {'array': array([ 2.2098757e-05,  4.6582241e-05, -2.2803260e-05, ...,
         -2.8419291e-04, -2.3305941e-04, -1.1425107e-04], dtype=float32),
  'path': '/root/.cache/huggingface/datasets/downloads/extracted/f14948e0e84be638dd7943ac36518a4cf3324e8b7aa331c5ab11541518e9368c/en-US~APP_ERROR/602b9a5fbb1e6d0fbce91f52.wav',
  'sampling_rate': 16000},
 'intent_class': 2}

现在创建一个预处理函数:

  1. 调用 audio 列来加载;如果必要,可以重新采样音频文件;
  2. 检查音频文件的采样率是否与音频数据模型的采样率匹配。可以在 Wav2Vec2 模型卡片中找到此信息。
  3. 设置最大输入长度,以便批量处理更长的输入而不会将其截断。
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=16000, truncation=True
     )
     return inputs

使用 Datasets [~datasets.Dataset.map] 函数将预处理函数进行配置并应用到整个数据集。可以通过设置 batched=True 加快 map 一次处理数据集的多个元素的速度。删除不需要的列,并将 intent_class 重命名为 label ,因为模型期望的名称是 label

encoded_minds = minds.map(preprocess_function, remove_columns="audio", batched=True)
encoded_minds = encoded_minds.rename_column("intent_class", "label")

评估

在训练过程中包含指标通常有助于评估模型的性能。你可以使用 Evaluate 库来快速加载评估方法。对于此任务,加载 accuracy 指标:

import evaluate

accuracy = evaluate.load("accuracy")

然后创建一个函数,将你的预测和标签传递给 evaluate.EvaluationModule.compute 以计算准确率:

import numpy as np

def compute_metrics(eval_pred):
     predictions = np.argmax(eval_pred.predictions, axis=1)
     return accuracy.compute(predictions=predictions, references=eval_pred.label_ids)

现在 compute_metrics 函数已准备就绪。

训练

现在准备开始训练模型了!使用[AutoModelForAudioClassification]载入 Wav2Vec2 模型以及期望标签的数量和标签映射:

from transformers import AutoModelForAudioClassification, TrainingArguments, Trainer

num_labels = len(id2label)
model = AutoModelForAudioClassification.from_pretrained(
     "facebook/wav2vec2-base", num_labels=num_labels, label2id=label2id, id2label=id2label
 )

到此为止,只剩下三个步骤:

  1. 在[TrainingArguments]中定义你的训练超参数。唯一需要的参数是 output_dir,它指定了模型保存的位置。通过设置 push_to_hub=True 将此模型推送到Hub(需要登录到 Hugging Face 上传训练得到的模型)。在每个阶段结束时,训练器 Trainer 将评估训练的准确性并保存阶段训练成果。
  2. 将训练参数包括模型、数据集、分词器、数据整理器和 compute_metrics 函数等一起传递给训练器Trainer
  3. 调用[~Trainer.train]微调模型。
training_args = TrainingArguments(
     output_dir="my_awesome_mind_model",
     evaluation_strategy="epoch",
     save_strategy="epoch",
     learning_rate=3e-5,
     per_device_train_batch_size=32,
     gradient_accumulation_steps=4,
     per_device_eval_batch_size=32,
     num_train_epochs=10,
     warmup_ratio=0.1,
     logging_steps=10,
     load_best_model_at_end=True,
     metric_for_best_model="accuracy",
     push_to_hub=True,
 )

trainer = Trainer(
     model=model,
     args=training_args,
     train_dataset=encoded_minds["train"],
     eval_dataset=encoded_minds["test"],
     tokenizer=feature_extractor,
     compute_metrics=compute_metrics,
 )

trainer.train()

训练完成后,使用[transformers.Trainer.push_to_hub]方法将模型推送到Hub,以便所有人都可以使用你的模型:

trainer.push_to_hub()

推理

现在你已经微调了一个模型,可以将其用于推理!

加载要运行推理的音频文件。如果有必要,记得将音频文件的采样率重新采样以匹配模型的采样率!

from datasets import load_dataset, Audio

dataset = load_dataset("PolyAI/minds14", name="en-US", split="train")
dataset = dataset.cast_column("audio", Audio(sampling_rate=16000))
sampling_rate = dataset.features["audio"].sampling_rate
audio_file = dataset[0]["audio"]["path"]

使用微调的模型最简单的方法是使用 pipeline 来测试音频分类。用新微调的模型实例化一个 pipeline,并将音频文件传给实例:

from transformers import pipeline

classifier = pipeline("audio-classification", model="stevhliu/my_awesome_minds_model")
classifier(audio_file)

结果为:

[
    {'score': 0.09766869246959686, 'label': 'cash_deposit'},
    {'score': 0.07998877018690109, 'label': 'app_error'},
    {'score': 0.0781070664525032, 'label': 'joint_account'},
    {'score': 0.07667109370231628, 'label': 'pay_bill'},
    {'score': 0.0755252093076706, 'label': 'balance'}
]

也可以手动复制 pipeline 的结果,加载一个特征提取器来预处理音频文件并将输入作为 PyTorch 张量返回:

from transformers import AutoFeatureExtractor

feature_extractor = AutoFeatureExtractor.from_pretrained("stevhliu/my_awesome_minds_model")
inputs = feature_extractor(dataset[0]["audio"]["array"], sampling_rate=sampling_rate, return_tensors="pt")

将输入传递给模型并返回 logits

from transformers import AutoModelForAudioClassification

model = AutoModelForAudioClassification.from_pretrained("stevhliu/my_awesome_minds_model")
with torch.no_grad():
     logits = model(**inputs).logits

获取概率最高的类别,并使用模型的 id2label 映射将其转换为标签:

import torch

predicted_class_ids = torch.argmax(logits).item()
predicted_label = model.config.id2label[predicted_class_ids]
predicted_label

结果为:

'cash_deposit'

自动语音识别

自动语音识别(ASR)将语音信号转换为文本,将一系列音频输入映射到文本输出。

演示的任务支持以下模型架构:Data2VecAudioHubert,M-CTC-TSEWSEW-DUniSpeechUniSpeechSatWav2Vec2Wav2Vec2-ConformerWavLM。本节技术点如下:

  • 在数据集 MInDS-14 上微调 Wav2Vec2 以将音频转录为文本;
  • 使用微调后的模型进行推理。

开始之前,请确保已安装所有必需的库:

pip install transformers datasets evaluate jiwer

加载MInDS-14数据集

首先加载 Datasets 库中 MInDS-14 数据集的一个较小子集。保证在花更多时间在完整数据集上进行训练前验证和确保工作正常。

from datasets import load_dataset, Audio

minds = load_dataset("PolyAI/minds14", name="en-US", split="train[:100]")

使用[Dataset.train_test_split]方法将数据集的 train 拆分为训练集和测试集两部分:

minds = minds.train_test_split(test_size=0.2)
minds

结果为:

DatasetDict({
    train: Dataset({
        features: ['path', 'audio', 'transcription', 'english_transcription', 'intent_class', 'lang_id'],
        num_rows: 16
    })
    test: Dataset({
        features: ['path', 'audio', 'transcription', 'english_transcription', 'intent_class', 'lang_id'],
        num_rows: 4
    })
})

虽然数据集包含了很多有用的信息,如 lang_idenglish_transcription,但现在只是关注 audiotranscription,其他并不关心。可以使用[datasets.Dataset.remove_columns]方法删除其他列:

minds = minds.remove_columns(["english_transcription", "intent_class", "lang_id"])
minds["train"][0]

结果为:

{'audio': {'array': array([-0.00024414,  0.        ,  0.        , ...,  0.00024414,
          0.00024414,  0.00024414], dtype=float32),
  'path': '/root/.cache/huggingface/datasets/downloads/extracted/f14948e0e84be638dd7943ac36518a4cf3324e8b7aa331c5ab11541518e9368c/en-US~APP_ERROR/602ba9e2963e11ccd901cd4f.wav',
  'sampling_rate': 8000},
 'path': '/root/.cache/huggingface/datasets/downloads/extracted/f14948e0e84be638dd7943ac36518a4cf3324e8b7aa331c5ab11541518e9368c/en-US~APP_ERROR/602ba9e2963e11ccd901cd4f.wav',
 'transcription': "hi I'm trying to use the banking app on my phone and currently my checking and savings account balance is not refreshing"}

其中:

  • audio:一维数组形式的语音信号,必须调用它来加载和重新采样音频文件。
  • transcription:目标文本。

预处理

下一步是载入 Wav2Vec2 处理器来处理音频信号:

from transformers import AutoProcessor

processor = AutoProcessor.from_pretrained("facebook/wav2vec2-base")

MInDS-14 数据集的采样率为 8000kHz(你可以在其数据集卡片中找到这些信息),这意味着你需要将数据集重新采样为 16000kHz 以使用 Wav2Vec2 模型:

minds = minds.cast_column("audio", Audio(sampling_rate=16_000))
minds["train"][0]

结果为:

{'audio': {'array': array([-2.38064706e-04, -1.58618059e-04, -5.43987835e-06, ...,
          2.78103951e-04,  2.38446111e-04,  1.18740834e-04], dtype=float32),
  'path': '/root/.cache/huggingface/datasets/downloads/extracted/f14948e0e84be638dd7943ac36518a4cf3324e8b7aa331c5ab11541518e9368c/en-US~APP_ERROR/602ba9e2963e11ccd901cd4f.wav',
  'sampling_rate': 16000},
 'path': '/root/.cache/huggingface/datasets/downloads/extracted/f14948e0e84be638dd7943ac36518a4cf3324e8b7aa331c5ab11541518e9368c/en-US~APP_ERROR/602ba9e2963e11ccd901cd4f.wav',
 'transcription': "hi I'm trying to use the banking app on my phone and currently my checking and savings account balance is not refreshing"}

如上所示,在上面的 transcription 文本包含一系列大写和小写字符。Wav2Vec2 的分词器只是在大写字符上进行训练,所以你需要确保文本与分词器的词汇表匹配:

def uppercase(example):
     return {"transcription": example["transcription"].upper()}

minds = minds.map(uppercase)

现在创建一个预处理函数 prepare_dataset 来处理:

  1. 调用 audio 列获取音频输入和重新采样音频文件。
  2. 从音频文件中提取 input_values 并使用 processortranscription 列进行标记。
def prepare_dataset(batch):
     audio = batch["audio"]
     batch = processor(audio["array"], sampling_rate=audio["sampling_rate"], text=batch["transcription"])
     batch["input_length"] = len(batch["input_values"][0])
     return batch

要在整个数据集上应用prepare_dataset,要使用 Datasets~datasets.Dataset.map 函数。可以通过 num_proc 参数提高处理器数量来为 map 提速。对于不需要的列,可使用 datasets.Dataset.remove_columns 方法删除你不需要的列:

encoded_minds = minds.map(prepare_dataset, remove_columns=minds.column_names["train"], num_proc=4)

Transformers 没有适用于 ASR 的数据整理器,因此你需要通过 DataCollatorWithPadding 来创建一批示例并动态地将文本和标签填充至该批次中最长元素的长度(不是整个数据集的长度),以确保它们具有相同的长度。尽管可以通过设置 padding=True 让分词器 tokenizer 来填充文本,但动态填充更高效。

与其他数据整理器不同,此特定数据整理器需要对 input_valueslabels 应用不同的填充方法:

import torch

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

@dataclass
class DataCollatorCTCWithPadding:
     processor: AutoProcessor
     padding: Union[bool, str] = "longest"

     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
         input_features = [{"input_values": feature["input_values"][0]} for feature in features]
         label_features = [{"input_ids": feature["labels"]} for feature in features]

         batch = self.processor.pad(input_features, padding=self.padding, return_tensors="pt")

         labels_batch = self.processor.pad(labels=label_features, padding=self.padding, 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)

         batch["labels"] = labels

         return batch

现在实例化 DataCollatorForCTCWithPadding

data_collator = DataCollatorCTCWithPadding(processor=processor, padding="longest")

评估

在训练过程中包含一个度量标准通常有助于评估模型的性能,可通过 Evaluate 库快速加载WERword error rate)度量指标:

import evaluate

wer = evaluate.load("wer")

然后创建一个函数,将预测和标签传递给 ~evaluate.EvaluationModule.compute 计算 WER

import numpy as np

def compute_metrics(pred):
     pred_logits = pred.predictions
     pred_ids = np.argmax(pred_logits, axis=-1)

     pred.label_ids[pred.label_ids == -100] = processor.tokenizer.pad_token_id

     pred_str = processor.batch_decode(pred_ids)
     label_str = processor.batch_decode(pred.label_ids, group_tokens=False)

     wer = wer.compute(predictions=pred_str, references=label_str)

     return {"wer": wer}

现在,你的 compute_metrics 函数已准备好了,在设置训练时将返回到它。

训练

现在,你已经准备好开始对模型进行训练了!使用 AutoModelForCTC 加载 Wav2Vec2 模型。使用 ctc_loss_reduction 参数指定要应用的缩减。通常最好使用平均而不是默认的求和:

from transformers import AutoModelForCTC, TrainingArguments, Trainer

model = AutoModelForCTC.from_pretrained(
     "facebook/wav2vec2-base",
     ctc_loss_reduction="mean",
     pad_token_id=processor.tokenizer.pad_token_id,
 )

接下来,我们还要处理以下三个步骤:

  1. TrainingArguments 中定义你的训练超参数。唯一需要的参数是 output_dir,它指定要保存模型的位置。通过设置 push_to_hub=True 将该模型推送到Hub(你需要登录到 HuggingFace 才能上传你的模型)。在每个阶段结束后,训练器将评估 WER 并保存训练阶段训练成果。
  2. 将训练参数包括模型、数据集、标记器、数据整理器和compute_metrics函数等一起传递给训练器 Trainer
  3. 调用 ~Trainer.train 来微调模型。
training_args = TrainingArguments(
     output_dir="my_awesome_asr_mind_model",
     per_device_train_batch_size=8,
     gradient_accumulation_steps=2,
     learning_rate=1e-5,
     warmup_steps=500,
     max_steps=2000,
     gradient_checkpointing=True,
     fp16=True,
     group_by_length=True,
     evaluation_strategy="steps",
     per_device_eval_batch_size=8,
     save_steps=1000,
     eval_steps=1000,
     logging_steps=25,
     load_best_model_at_end=True,
     metric_for_best_model="wer",
     greater_is_better=False,
     push_to_hub=True,
 )

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

trainer.train()

完成训练后,使用 transformers.Trainer.push_to_hub 方法将你的模型推送到Hub,以便每个人都可以使用你的模型。

trainer.push_to_hub()

推断

现在已经微调了一个模型,可以将其用于推理!

加载要运行推理的音频文件。如果有必要,记得将音频文件的采样率重新采样以匹配模型的采样率!

from datasets import load_dataset, Audio

dataset = load_dataset("PolyAI/minds14", "en-US", split="train")
dataset = dataset.cast_column("audio", Audio(sampling_rate=16000))
sampling_rate = dataset.features["audio"].sampling_rate
audio_file = dataset[0]["audio"]["path"]

使用微调的模型的最简单方法是通过 pipeline 尝试使用它。使用你的模型实例化一个 pipeline,并将音频文件传给该实例:

from transformers import pipeline

transcriber = pipeline("automatic-speech-recognition", model="stevhliu/my_awesome_asr_minds_model")
transcriber(audio_file)

结果为:

{'text': 'I WOUD LIKE O SET UP JOINT ACOUNT WTH Y PARTNER'}

到此这次转录已经完成,但可能还有进一步的改进空间!尝试在更多的示例上微调你的模型以获得更好的结果!

如果需要,你还可以手动复制 pipeline 的结果,加载处理器预处理音频文件和转录,并将输入以 PyTorch 张量返回:

from transformers import AutoProcessor

processor = AutoProcessor.from_pretrained("stevhliu/my_awesome_asr_mind_model")
inputs = processor(dataset[0]["audio"]["array"], sampling_rate=sampling_rate, return_tensors="pt")

将输入传给模型并返回 logits

from transformers import AutoModelForCTC

model = AutoModelForCTC.from_pretrained("stevhliu/my_awesome_asr_mind_model")
with torch.no_grad():
     logits = model(**inputs).logits

得到最高概率的预测 input_ids后并通过处理器将预测的 input_ids 解码成文本:

import torch

pred_ids = torch.argmax(logits, dim=-1)
pred_str = processor.batch_decode(pred_ids)

pred_str

结果为:

['I would like to set up a joint account with my partner.']

语音合成

语音合成(TTS)是将文本转换为自然语音的任务,其中生成的语音可以为多种语种或者多种语声一起构成。目前在 transformer中有几种语音合成模型可用,如 BarkMMSVITSspeech等。

你可以使用pipeline 调用任务 text-to-audio(另名 text-to-speech)轻松生成音频。有的模型如 Bark也可以通过条件反射产生非语言交流如大笑、叹息和哭泣,甚至音乐。

下面来演示如何通过 pipeline调用 text-to-speech 任务使用 Bark:

from transformers import pipeline

pipe = pipeline("text-to-speech", model="suno/bark-small")
text = "[clears throat] This is a test ... and I just took a long pause."
output = pipe(text)

可使用下面的代码例子来听听刚才转换的语音:

from IPython.display import Audio
Audio(output["audio"], rate=output["sampling_rate"])

前在 transformer 中唯一可用的文本到语音模型是 SpeechT5FastSpeech2Conformer,将来会不断的增加。SpeechT5 是在”语音到文本“和”文本到语音“的组合数据上进行预训练的,允许它在文本和语音共享的隐藏表示统一空间中学习。这意味着相同的预训练模型可以针对不同的任务进行微调。此外,SpeechT5 支持多个话音并在 x-vector 上嵌入话音。

本节的技术要点如下:

  1. 微调 SpeechT5,它最初是在 VoxPopuli 数据集的荷兰语语言子集上对英语语音进行训练的。
  2. 有两种方法可以使用改进后的模型进行推理:使用 pipeline 或直接使用。

在开始之前,请确保你已经安装了所有必要的库:

pip install datasets soundfile speechbrain accelerate

通过源代码安装 transformer,因为尚未将 SpeechT5 的所有功能合并到官方版本中:

pip install git+https://github.com/huggingface/transformers.git

本节样例需要一个 GPU ,运行下面这行代码来检查是否有 GPU可用:

!nvidia-smi

AMD上,运行以下代码检查:

!rocm-smi

加载数据集

VoxPopuli 是一个大型多语言语音语料库,由来自2009-2020年欧洲议会事件录音的数据组成。它包含15种欧洲语言的带标签的音频转录数据。在本节中,我们使用的是荷兰语子集,也可以随意选择其他子集。

注意VoxPopuli 或任何其他自动语音识别(ASR)数据集可能不是训练 TTS 模型的最合适选择。对 ASR 有益的特性,如过多的背景噪声,在 TTS 中通常是不可取的。然而,寻找高质量、多语言和多话音的 TTS 数据集可能非常具有挑战性。

让我们加载数据:

from datasets import load_dataset, Audio

dataset = load_dataset("facebook/voxpopuli", "nl", split="train")
len(dataset)

输出:

20968

20968个示例应该足以进行微调。SpeechT5 期望音频数据的采样率为16khz,因此请确保数据集中的示例满足此要求:

dataset = dataset.cast_column("audio", Audio(sampling_rate=16000))

预处理

让我们从定义模型检查点来使用和载入适当的处理器开始:

from transformers import SpeechT5Processor

checkpoint = "microsoft/speecht5_tts"
processor = SpeechT5Processor.from_pretrained(checkpoint)

文本清理

首先要做的是清理文本数据。你需要处理器的分词器部分来处理文本:

tokenizer = processor.tokenizer

数据集示例有 raw_textnormalized_text 两个特征,在决定使用哪个特征作为输入文本时,需要确定 SpeechT5 分词器中没有任何数字标记。在 normalized_text 中,数字是用文本来表达的。因此,更适合的是 normalized_text,我们建议将其用作输入文本。

由于 SpeechT5 是用英语训练的,它可能无法识别荷兰语数据集中的某些字符。如果保持不变,这些字符将被转换为<unk>标记。然而,在荷兰语中,某些字符(如à)用于强调音节。为了保留文本的含义,我们可以将该字符替换为常规的a

要识别不受支持的标记,使用按字符标记的SpeechT5Tokenizer 分词器提取数据集中的所有唯一字符。

编写 extract_all_chars 的函数,该函数将来自所有示例的转录连接为一个字符串,然后将其转换为字符集合。确保在dataset.map()中设置batched=Truebatch_size=-1,以便所有转录一次性可用于映射函数。

def extract_all_chars(batch):
     all_text = " ".join(batch["normalized_text"])
     vocab = list(set(all_text))
     return {"vocab": [vocab], "all_text": [all_text]}


vocabs = dataset.map(
     extract_all_chars,
     batched=True,
     batch_size=-1,
     keep_in_memory=True,
     remove_columns=dataset.column_names,
 )

dataset_vocab = set(vocabs["vocab"][0])
tokenizer_vocab = {k for k, _ in tokenizer.get_vocab().items()}

现在,你有两个字符集:一个是数据集的词汇表,一个是分词器的词汇表。要识别数据集中不在分词器词汇表中的不支持字符,可以取两个字符集之间的差集。结果集将包含数据集中存在但不在分词器词汇表中的字符。

dataset_vocab - tokenizer_vocab

结果为:

{' ', 'à', 'ç', 'è', 'ë', 'í', 'ï', 'ö', 'ü'}

定义一个函数来处理前一步中发现的不支持的字符,将这些字符映射到有效的标记。请注意,分词器中的空格已更换为,不需要单独处理。

replacements = [
     ("à", "a"),
     ("ç", "c"),
     ("è", "e"),
     ("ë", "e"),
     ("í", "i"),
     ("ï", "i"),
     ("ö", "o"),
     ("ü", "u"),
 ]


def cleanup_text(inputs):
     for src, dst in replacements:
         inputs["normalized_text"] = inputs["normalized_text"].replace(src, dst)
     return inputs


dataset = dataset.map(cleanup_text)

现在,已经处理了文本中的特殊字符,是时候将重点转移到音频数据上了。

话音

VoxPopuli 数据集包括多个话音,但数据集中有多少个话音呢?为了确定这一点,我们可以统计唯一话音的数量以及每个话音对数据集的贡献的示例数量。数据集中共有20968个示例,这些信息将更好地了解数据中话音和示例的分布。

from collections import defaultdict

speaker_counts = defaultdict(int)

for speaker_id in dataset["speaker_id"]:
     speaker_counts[speaker_id] += 1

通过绘制直方图,可以了解每个话音有多少数据。

import matplotlib.pyplot as plt

plt.figure()
plt.hist(speaker_counts.values(), bins=20)
plt.ylabel("Speakers")
plt.xlabel("Examples")
plt.show()

在这里插入图片描述

直方图显示,大约有三分之一的话音在数据集中仅有少于100个示例,而其中大约十个话音有超过500个示例。为了提高训练效率并平衡数据集,我们可以将数据限制为有100到400个示例的话音者。

def select_speaker(speaker_id):
     return 100 <= speaker_counts[speaker_id] <= 400

dataset = dataset.filter(select_speaker, input_columns=["speaker_id"])

看看剩下多少个话音:

len(set(dataset["speaker_id"]))

结果为:

42

看看剩下多少个示例:

len(dataset)

结果为:

9973

你还剩下了约10000个示例,约40个独特话音,应该足够了。

请注意:一些示例较少的话音实际上可能有更多的音频,如果示例很长。然而,确定每个话音的总音频量需要扫描整个数据集,这是一个耗时的过程,需要加载和解码每个音频文件。因此,我们选择在此跳过此步骤。

话音嵌入

为了使TTS模型能够区分多个话音,你需要为每个示例创建一个话音嵌入。话音嵌入是模型的额外输入,可以捕获特定话音的声音特征。为了生成这些话音嵌入,使用 SpeechBrain 中预训练的 spkrec-xvect-voxceleb 模型。

创建一个函数create_speaker_embedding(),接受输入音频波形并输出包含相应话音嵌入的512个元素矢量。

import os
import torch
from speechbrain.pretrained import EncoderClassifier

spk_model_name = "speechbrain/spkrec-xvect-voxceleb"

device = "cuda" if torch.cuda.is_available() else "cpu"
speaker_model = EncoderClassifier.from_hparams(
     source=spk_model_name,
     run_opts={"device": device},
     savedir=os.path.join("/tmp", spk_model_name),
 )


def create_speaker_embedding(waveform):
     with torch.no_grad():
         speaker_embeddings = speaker_model.encode_batch(torch.tensor(waveform))
         speaker_embeddings = torch.nn.functional.normalize(speaker_embeddings, dim=2)
         speaker_embeddings = speaker_embeddings.squeeze().cpu().numpy()
     return speaker_embeddings

重要的是要注意,speechbrain/spkrec-xvect-voxceleb 模型是根据VoxCeleb数据集的英语语音训练的,而本指南中的训练示例是荷兰语。虽然我们相信该模型仍可为荷兰语数据集生成合理的话音嵌入,但这一假设可能在所有情况下都不成立。

为了获得最佳结果,建议首先在目标语音上训练X-Vector模型。这将确保模型能够更好地捕捉荷兰语中的独特语音特征。

处理数据集

最后,让我们将数据处理为模型期望的格式。创建一个prepare_dataset函数,它接受单个示例,并使用SpeechT5Processor对象标记化输入文本,并将目标音频加载到对数梅尔频谱图中。 它还应将话音嵌入作为附加输入加入。

def prepare_dataset(example):
     audio = example["audio"]

     example = processor(
         text=example["normalized_text"],
         audio_target=audio["array"],
         sampling_rate=audio["sampling_rate"],
         return_attention_mask=False,
     )

     # strip off the batch dimension
     example["labels"] = example["labels"][0]

     # use SpeechBrain to obtain x-vector
     example["speaker_embeddings"] = create_speaker_embedding(audio["array"])

     return example

通过查看单个示例,验证处理是否正确:

processed_example = prepare_dataset(dataset[0])
list(processed_example.keys())

结果为:

['input_ids', 'labels', 'stop_labels', 'speaker_embeddings']

话音嵌入应该是一个512个元素的矢量:

processed_example["speaker_embeddings"].shape

结果为:

(512,)

标签应该是一个具有80个mel 乐器的对数梅尔频谱图。

import matplotlib.pyplot as plt

plt.figure()
plt.imshow(processed_example["labels"].T)
plt.show()

在这里插入图片描述

顺便说一下:如果你对此频谱图感到困惑,可能是由于你熟悉将低频放在底部,高频放在顶部的绘图惯例。然而,当使用 matplotlib 库将频谱图作为图像绘制时,y轴是上下翻转的,频谱图会倒置。

现在将处理函数应用于整个数据集,这将花费5到10分钟。

dataset = dataset.map(prepare_dataset, remove_columns=dataset.column_names)

你将看到一个警告,说数据集中的一些示例长度超过了模型的最大输入长度(600个标记)。从数据集中删除这些示例。为了进一步允许更大的批处理大小,我们将删除超过200个标记的数据。

def is_not_too_long(input_ids):
     input_length = len(input_ids)
     return input_length < 200


dataset = dataset.filter(is_not_too_long, input_columns=["input_ids"])
len(dataset)

结果为:

8259

接下来,创建一个基本的训练/测试拆分:

dataset = dataset.train_test_split(test_size=0.1)

数据整理器(Data Collator)

为了将多个示例合并为一个批次,需要定义一个自定义的数据整理器。这个整理器将使用填充标记填充较短的序列,确保所有示例具有相同的长度。对于频谱标签,填充部分将被特殊值-100替换。这个特殊值指示模型在计算频谱损失时忽略该部分的频谱。

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


@dataclass
class TTSDataCollatorWithPadding:
     processor: Any

     def __call__(self, features: List[Dict[str, Union[List[int], torch.Tensor]]]) -> Dict[str, torch.Tensor]:
         input_ids = [{"input_ids": feature["input_ids"]} for feature in features]
         label_features = [{"input_values": feature["labels"]} for feature in features]
         speaker_features = [feature["speaker_embeddings"] for feature in features]

         # collate the inputs and targets into a batch
         batch = processor.pad(input_ids=input_ids, labels=label_features, return_tensors="pt")

         # replace padding with -100 to ignore loss correctly
         batch["labels"] = batch["labels"].masked_fill(batch.decoder_attention_mask.unsqueeze(-1).ne(1), -100)

         # not used during fine-tuning
         del batch["decoder_attention_mask"]

         # round down target lengths to multiple of reduction factor
         if model.config.reduction_factor > 1:
             target_lengths = torch.tensor([len(feature["input_values"]) for feature in label_features])
             target_lengths = target_lengths.new(
                 [length - length % model.config.reduction_factor for length in target_lengths]
             )
             max_length = max(target_lengths)
             batch["labels"] = batch["labels"][:, :max_length]

         # also add in the speaker embeddings
         batch["speaker_embeddings"] = torch.tensor(speaker_features)

         return batch

SpeechT5 中,模型的解码器部分的输入减少了2倍。换句话说,它从目标序列中丢弃了每个时间步长的另一部分。然后,解码器预测一个长度为原始目标序列两倍的序列。由于原始目标序列长度可能是奇数,数据整理器确保将批次的最大长度向下舍入为2的倍数。

data_collator = TTSDataCollatorWithPadding(processor=processor)

训练模型

从与加载处理器相同的检查点加载预训练模型:

from transformers import SpeechT5ForTextToSpeech

model = SpeechT5ForTextToSpeech.from_pretrained(checkpoint)

use_cache=True选项与梯度检查点不兼容。在训练时将其禁用。

model.config.use_cache = False

定义训练参数。在训练过程中,我们不计算任何评估指标,只关注损失值:

from transformers import Seq2SeqTrainingArguments

training_args = Seq2SeqTrainingArguments(
     output_dir="speecht5_finetuned_voxpopuli_nl",  # 更改为你选择的仓库名称
     per_device_train_batch_size=4,
     gradient_accumulation_steps=8,
     learning_rate=1e-5,
     warmup_steps=500,
     max_steps=4000,
     gradient_checkpointing=True,
     fp16=True,
     evaluation_strategy="steps",
     per_device_eval_batch_size=2,
     save_steps=1000,
     eval_steps=1000,
     logging_steps=25,
     report_to=["tensorboard"],
     load_best_model_at_end=True,
     greater_is_better=False,
     label_names=["labels"],
     push_to_hub=True,
 )

实例化Trainer对象,并将模型、数据集和数据收集器传递给它。

from transformers import Seq2SeqTrainer

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

有了上述准备,你就可以开始训练了!训练需要几个小时的时间。根据你的GPU,当你开始训练时,可能会遇到 CUDA "out-of-memory" 错误。在这种情况下,你可以逐渐将per_device_train_batch_size减小2倍,并将gradient_accumulation_steps增加2倍来进行补偿。

trainer.train()

为了能够在流水线中使用你的检查点,请确保将处理器与检查点一起保存:

processor.save_pretrained("YOUR_ACCOUNT_NAME/speecht5_finetuned_voxpopuli_nl")

最后将模型推送到 Hub:

trainer.push_to_hub()

推理

使用流水线进行推理

太好了,现在你已经微调了模型,可以用它进行推理了!首先,让我们看看如何使用对应的流水线。让我们创建一个具有你的检查点的"text-to-speech"流水线:

from transformers import pipeline

pipe = pipeline("text-to-speech", model="YOUR_ACCOUNT_NAME/speecht5_finetuned_voxpopuli_nl")

选择一段你想要朗读的荷兰语文本,例如:

text = "hallo allemaal, ik praat nederlands. groetjes aan iedereen!"

要使用流水线的 ·SpeechT5,你需要一个说话人嵌入。让我们从测试数据集中获取一个示例的说话人嵌入:

example = dataset["test"][304]
speaker_embeddings = torch.tensor(example["speaker_embeddings"]).unsqueeze(0)

现在,你可以将文本和说话人嵌入传递给流水线,它将为你处理剩下的部分:

forward_params = {"speaker_embeddings": speaker_embeddings}
output = pipe(text, forward_params=forward_params)
output

结果为:

{'audio': array([-6.82714235e-05, -4.26525949e-04,  1.06134125e-04, ...,
        -1.22392643e-03, -7.76011671e-04,  3.29112721e-04], dtype=float32),
 'sampling_rate': 16000}

然后,你可以听到结果:

from IPython.display import Audio
Audio(output['audio'], rate=output['sampling_rate'])
手动运行推理

你可以在不使用流水线的情况下实现相同的推理结果,但是,需要更多的步骤。

从Hub加载模型:

model = SpeechT5ForTextToSpeech.from_pretrained("YOUR_ACCOUNT/speecht5_finetuned_voxpopuli_nl")

从测试数据集中选择一个示例并获取说话人嵌入:

example = dataset["test"][304]
speaker_embeddings = torch.tensor(example["speaker_embeddings"]).unsqueeze(0)

定义输入文本并对其进行标记化:

text = "hallo allemaal, ik praat nederlands. groetjes aan iedereen!"
inputs = processor(text=text, return_tensors="pt")

使用模型创建一个频谱图:

spectrogram = model.generate_speech(inputs["input_ids"], speaker_embeddings)

如果你想要可视化频谱图,可以执行以下操作:

plt.figure()
plt.imshow(spectrogram.T)
plt.show()

在这里插入图片描述

最后,使用语音合成器将频谱图转化为声音。

with torch.no_grad():
     speech = vocoder(spectrogram)

from IPython.display import Audio

Audio(speech.numpy(), rate=16000)

根据我们的经验,从该模型获得令人满意的结果可能具有一定的挑战性。说话人嵌入的质量似乎是一个重要因素。由于 SpeechT5 是使用英语 x-vectors 进行预训练的,因此在使用英语说话人嵌入时效果最好。如果合成的语音听起来不好,请尝试使用不同的说话人嵌入。

增加训练持续时间很可能会提高结果的质量。即便如此,语音明显是荷兰语而不是英语,并且它确实捕捉到了话音的声音特点(与示例中的原始音频相比较)。 还可以尝试使用模型的不同配置。例如,尝试使用config.reduction_factor = 1来查看是否改善了结果。

最后,重要的是要考虑伦理问题。虽然TTS技术有许多有用的应用,但也可能被用于恶意目的,例如在没有知情或同意的情况下模仿某人的声音。请谨慎和负责任地使用TTS。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐