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)
这里有一个代码片段,你可以在 notebook 中运行它来试听生成的音频:
from IPython.display import Audio
Audio(output["audio"], rate=output["sampling_rate"])
想了解更多关于 Bark 和其他预训练 TTS 模型的示例,请参考我们的音频课程。
如果你想要微调一个 TTS 模型,目前 🤗 Transformers 中可用的文本转语音模型只有 SpeechT5 和 FastSpeech2Conformer,不过未来会添加更多模型。
SpeechT5 是在语音转文本和文本转语音数据的组合上进行预训练的,这使得它能够学习一个统一的隐含表示空间,这个空间由文本和语音共享。这意味着同一个预训练模型可以针对不同任务进行微调。此外,SpeechT5 通过 x-vector 说话者嵌入支持多个说话者。
本指南的其余部分将展示如何:
在开始之前,请确保你已经安装了所有必要的库:
pip install datasets soundfile speechbrain accelerate
需要从源代码安装 🤗Transformers,因为并非所有 SpeechT5 的功能都已合并到官方版本中:
pip install git+https://github.com/huggingface/transformers.git
若要按照本指南操作,你还需要一个 GPU。如果你在 notebook 中工作,运行以下命令来检查是否有可用的 GPU:
!nvidia-smi
对于 AMD GPU,可以使用:
!rocm-smi
我们鼓励你登录你的 Hugging Face 账户,以便上传并与社区分享你的模型。当提示时,输入你的令牌进行登录:
from huggingface_hub import notebook_login
notebook_login()
from datasets import load_dataset, Audio
dataset = load_dataset("facebook/voxpopuli", "nl", split="train")
len(dataset)
20968 个示例应该足够用于微调。SpeechT5 要求音频数据的采样率为 16 kHz,所以请确保数据集中的示例满足这一要求:
dataset = dataset.cast_column("audio", Audio(sampling_rate=16000))
预处理数据¶
首先,定义要使用的模型 checkpoint 并加载相应的处理器:
from transformers import SpeechT5Processor
checkpoint = "microsoft/speecht5_tts"
processor = SpeechT5Processor.from_pretrained(checkpoint)
为 SpeechT5 分词进行文本清理¶
首先,清理文本数据。你需要使用处理器的分词器部分来处理文本:
tokenizer = processor.tokenizer
数据集示例包含 raw_text 和 normalized_text 特征。在决定使用哪个特征作为文本输入时,考虑到 SpeechT5 的分词器没有用于数字的任何标记。在 normalized_text 中,数字被写成文本形式。因此,它更适合作为输入文本,我们建议使用 normalized_text。
由于 SpeechT5 是在英语上训练的,它可能无法识别荷兰语数据集中的某些字符。如果保留原样,这些字符将被转换为 <unk> 标记。然而,在荷兰语中,某些字符如 à 用于强调音节。为了保留文本的意义,我们可以将这个字符替换为普通的 a。
为了识别不支持的标记,使用 SpeechT5Tokenizer 提取数据集中的所有唯一字符,该分词器以字符作为标记。为此,编写 extract_all_chars 映射函数,将所有示例的转录文本连接成一个字符串,并将其转换为字符集合。确保在 dataset.map() 中设置 batched=True 和 batch_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 数据集包含多位说话者的语音,但数据集中有多少位说话者呢?
为了确定这一点,我们可以统计具有独特特征的说话者的数量以及每位说话者为数据集贡献的示例数量。考虑到数据集中一共有 20,968 个示例,这些信息将帮助我们更好地理解说话者和示例在数据中的分布情况。
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 个示例,而大约 10 位说话者拥有超过 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"]))
让我们看看还剩下多少个示例:
len(dataset)
你现在大约有10,000 个示例,来自大约 40 位独特的说话者,这应该是足够的。
请注意,如果示例较长,那些只有少量示例的说话者实际上可能会拥有更多的音频数据。然而,确定每位说话者的总音频量需要遍历整个数据集,这是一个比较耗时的过程,涉及加载和解码每个音频文件。因此,我们选择在这里跳过这一步。
说话者嵌入¶
为了让 TTS 模型能够区分多位说话者,你需要为每个示例创建一个说话者嵌入。说话者嵌入是模型的额外输入,用于捕捉特定说话者的声音特征。
为了生成这些说话者嵌入,这里使用 SpeechBrain 的预训练模型 spkrec-xvect-voxceleb。
创建一个函数 create_speaker_embedding(),它接受一个输入音频波形,并输出一个包含相应说话者嵌入的 512 元素向量。
import os
import torch
from speechbrain.inference.classifiers 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工具对输入的文字进行编码,同时把目标音频转换成对数梅尔频谱图。此外,还要加入speaker embeddings作为额外的输入信息。
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())
说话者的嵌入信息应该是一个包含512个元素的向量:
processed_example["speaker_embeddings"].shape
标记应该是一个包含80个梅尔频段的日志梅尔频谱图。
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)
接下来,我们需要创建一个基本的训练/测试数据拆分:
dataset = dataset.train_test_split(test_size=0.1)
数据整理器¶
为了把多个样本合并成一个批次处理,我们需要定义一个自定义的数据整理器。这个整理器会用填充标记来补齐较短的序列,确保所有样本长度一致。对于频谱图标签,填充部分会用特殊值-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的倍数。
data_collator = TTSDataCollatorWithPadding(processor=processor)
训练模型¶
从之前用于加载处理器的同一个 checkpoint 中加载预训练模型:
from transformers import SpeechT5ForTextToSpeech
model = SpeechT5ForTextToSpeech.from_pretrained(checkpoint)
use_cache=True选项与梯度 checkpointing 功能不兼容。为了顺利进行训练,我们需要关闭这个选项。
model.config.use_cache = False
接下来,我们定义训练参数。在这个阶段,我们不会在训练过程中计算任何评估指标。我们只关注损失值:
from transformers import Seq2SeqTrainingArguments
training_args = Seq2SeqTrainingArguments(
output_dir="speecht5_finetuned_voxpopuli_nl", # change to a repo name of your choice
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,
eval_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,
processing_class=processor,
)
准备工作完成后,你就可以开始训练了!训练过程可能需要几个小时。根据你的GPU性能,启动训练时可能会遇到CUDA“内存不足”的错误。如果出现这种情况,你可以将per_device_train_batch_size减半,同时将gradient_accumulation_steps加倍来补偿。
trainer.train()
为了确保checkpoint能与pipeline一起使用,记得把处理器和checkpoint一起保存:
processor.save_pretrained("YOUR_ACCOUNT_NAME/speecht5_finetuned_voxpopuli_nl")
将最终模型上传到🤗 Hub:
trainer.push_to_hub()
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 与 pipeline 一起使用,你需要一个speaker embeddings。我们可以从测试数据集中的一个示例中获取它:
example = dataset["test"][304]
speaker_embeddings = torch.tensor(example["speaker_embeddings"]).unsqueeze(0)
现在你可以将文本和说话者嵌入传递给 pipeline,它会自动处理剩下的步骤:
forward_params = {"speaker_embeddings": speaker_embeddings}
output = pipe(text, forward_params=forward_params)
output
然后你可以聆听生成的结果。
from IPython.display import Audio
Audio(output['audio'], rate=output['sampling_rate'])
手动运行推理¶
你也可以在不使用 pipeline 的情况下实现相同的推理结果,但这需要更多步骤。
从 🤗 Hub 加载模型:
model = SpeechT5ForTextToSpeech.from_pretrained("YOUR_ACCOUNT/speecht5_finetuned_voxpopuli_nl")
从测试数据集中挑选一个例子,获取speaker embeddings:
example = dataset["test"][304]
speaker_embeddings = torch.tensor(example["speaker_embeddings"]).unsqueeze(0)
定义输入文本并将其标记化(tokenize):
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()
最后,使用声码器(vocoder)将频谱图转换为音频。
with torch.no_grad():
speech = vocoder(spectrogram)
from IPython.display import Audio
Audio(speech.numpy(), rate=16000)
根据我们的经验,要从这个模型中获得满意的结果可能有一定难度。speaker embeddings的质量似乎是一个关键因素。由于SpeechT5是使用英语x向量进行预训练的,所以在使用英语的speaker embeddings时表现最佳。如果合成的语音效果不佳,建议尝试使用不同的speaker embeddings。
增加训练时间也可能提升结果的质量。尽管如此,合成的语音明显是荷兰语而非英语,并且确实捕捉到了 speaker 的语音特征(与示例中的原始音频相比)。
另一个可以尝试的点是模型的配置。例如,可以尝试设置config.reduction_factor = 1,看看是否会有所改善。
最后,必须考虑到伦理问题。虽然TTS技术有许多有益的应用,但也可能被用于恶意目的,比如在未经知情或同意的情况下模仿某人的声音。请务必谨慎和负责任地使用TTS技术。