文本转语音
文本转语音 (TTS) 是将自然声音的语音从文本中创建的任务,其中语音可以在多种语言和多种说话者中生成。当前在 🤗 Transformers 中有几种文本转语音模型可用,例如 Bark、MMS、VITS 和 SpeechT5。
您可以使用 "text-to-audio"
管道(或其别名 - "text-to-speech"
)轻松生成音频。某些模型,例如 Bark,还可以被调节以生成非言语交流,例如笑声、叹气和哭泣,甚至添加音乐。以下是如何使用 "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"])
有关 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。如果您在笔记本中工作,请运行以下行以检查 GPU 是否可用
!nvidia-smi
或者,对于 AMD GPU
!rocm-smi
我们鼓励您登录您的 Hugging Face 帐户,以将您的模型上传并与社区共享。在收到提示时,输入您的令牌以登录
>>> from huggingface_hub import notebook_login
>>> notebook_login()
加载数据集
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 预计音频数据具有 16 kHz 的采样率,因此请确保数据集中示例满足此要求
dataset = dataset.cast_column("audio", Audio(sampling_rate=16000))
预处理数据
让我们从定义要使用的模型检查点并加载适当的处理器开始
>>> 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 个示例,而大约十个说话者拥有超过 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
您剩下不到 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-向量模型。这将确保模型能够更好地捕捉到荷兰语中独特的语音特征。
处理数据集
最后,让我们将数据处理成模型期望的格式。创建一个 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 个梅尔频段的对数梅尔谱图。
>>> 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)
数据整理器
为了将多个示例组合成一个批次,您需要定义一个自定义数据整理器。此整理器将用填充标记填充较短的序列,确保所有示例都具有相同的长度。对于谱图标签,填充部分将被替换为特殊值 -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", # 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,
... tokenizer=processor,
... )
有了这些,您就可以开始训练了!训练将需要几个小时。根据您的 GPU,您可能会在开始训练时遇到 CUDA “内存不足”错误。在这种情况下,您可以将 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-向量预训练的,因此使用英语说话人嵌入时效果最佳。如果合成的语音听起来很差,请尝试使用不同的说话人嵌入。
增加训练时长也有可能提高结果的质量。即使如此,语音明显是荷兰语而不是英语,并且确实捕捉到了说话人的语音特征(与示例中的原始音频进行比较)。另一个可以尝试的实验是模型的配置。例如,尝试使用 config.reduction_factor = 1
来查看这是否会改善结果。
最后,必须考虑伦理因素。虽然 TTS 技术有许多有用的应用,但它也可能被用于恶意目的,例如在不知情或未经同意的情况下冒充他人的声音。请谨慎负责地使用 TTS。
< > 在 GitHub 上更新