音訊課程文件

微調 SpeechT5

Hugging Face's logo
加入 Hugging Face 社群

並獲得增強的文件體驗

開始使用

微調 SpeechT5

現在你已經熟悉了文字轉語音任務以及在英語語言資料上預訓練的 SpeechT5 模型的內部工作原理,讓我們看看如何將其微調到另一種語言。

內務管理

如果你想重現這個例子,請確保你有 GPU。在 notebook 中,你可以使用以下命令進行檢查:

nvidia-smi

在我們的例子中,我們將使用大約 40 小時的訓練資料。如果你想使用 Google Colab 免費層的 GPU 進行跟隨,你需要將訓練資料量減少到大約 10-15 小時,並減少訓練步數。

你還需要一些額外的依賴項。

pip install transformers datasets soundfile speechbrain accelerate

最後,別忘了登入你的 Hugging Face 賬戶,以便你可以上傳模型並與社群共享。

from huggingface_hub import notebook_login

notebook_login()

資料集

在這個例子中,我們將使用 VoxPopuli 資料集的荷蘭語 (nl) 子集。VoxPopuli 是一個大型多語言語音語料庫,包含 2009-2020 年歐洲議會事件錄音的資料。它包含 15 種歐洲語言的標註音訊-轉錄資料。雖然我們將使用荷蘭語子集,但你可以隨意選擇其他子集。

這是一個自動語音識別 (ASR) 資料集,因此,如前所述,它不是訓練 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

讓我們看一個例子

dataset[0]

輸出

{'audio_id': '20100210-0900-PLENARY-3-nl_20100210-09:06:43_4',
 'language': 9,
 'audio': {'path': '/root/.cache/huggingface/datasets/downloads/extracted/02ec6a19d5b97c03e1379250378454dbf3fa2972943504a91c7da5045aa26a89/train_part_0/20100210-0900-PLENARY-3-nl_20100210-09:06:43_4.wav',
  'array': array([ 4.27246094e-04,  1.31225586e-03,  1.03759766e-03, ...,
         -9.15527344e-05,  7.62939453e-04, -2.44140625e-04]),
  'sampling_rate': 16000},
 'raw_text': 'Dat kan naar mijn gevoel alleen met een brede meerderheid die wij samen zoeken.',
 'normalized_text': 'dat kan naar mijn gevoel alleen met een brede meerderheid die wij samen zoeken.',
 'gender': 'female',
 'speaker_id': '1122',
 'is_gold_transcript': True,
 'accent': 'None'}

你可能會注意到,資料集示例包含 `raw_text` 和 `normalized_text` 特徵。在決定使用哪個特徵作為文字輸入時,重要的是要知道 SpeechT5 分詞器沒有任何數字標記。在 `normalized_text` 中,數字被寫成文字。因此,它更適合,我們應該使用 `normalized_text` 作為輸入文字。

由於 SpeechT5 是用英語訓練的,它可能無法識別荷蘭語資料集中的某些字元。如果保持原樣,這些字元將被轉換為 `` 標記。然而,在荷蘭語中,某些字元如 `à` 被用來強調音節。為了保留文字的含義,我們可以將此字元替換為常規的 `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()
Speakers histogram

直方圖顯示,資料集中約三分之一的說話者擁有的示例少於 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.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 模型。這將確保模型能夠更好地捕獲荷蘭語中存在的獨特語音特徵。如果你想訓練自己的 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 個梅爾頻段的對數梅爾頻譜圖。

import matplotlib.pyplot as plt

plt.figure()
plt.imshow(processed_example["labels"].T)
plt.show()
Log-mel spectrogram with 80 mel bins

旁註:如果你覺得這個頻譜圖令人困惑,可能是因為你熟悉將低頻放在底部、高頻放在頂部繪圖的慣例。然而,當使用 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` 選項與梯度檢查點不相容。請在訓練時停用它,並在生成時重新啟用快取以加快推理時間。

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, use_cache=True)

定義訓練引數。這裡我們在訓練過程中不計算任何評估指標,我們將在本章後面討論評估。相反,我們只關注損失。

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()

將最終模型推送到 🤗 Hub

trainer.push_to_hub()

推理

一旦你微調了一個模型,你就可以用它進行推理了!從 🤗 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")

例項化一個聲碼器並生成語音

from transformers import SpeechT5HifiGan

vocoder = SpeechT5HifiGan.from_pretrained("microsoft/speecht5_hifigan")
speech = model.generate_speech(inputs["input_ids"], speaker_embeddings, vocoder=vocoder)

準備好聽聽結果了嗎?

from IPython.display import Audio

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

從這個模型在新語言上獲得令人滿意的結果可能具有挑戰性。說話者嵌入的質量可能是一個重要因素。由於 SpeechT5 使用英語 x-vectors 進行了預訓練,因此在使用英語說話者嵌入時表現最佳。如果合成語音聽起來很差,請嘗試使用不同的說話者嵌入。

增加訓練時長也可能提高結果的質量。即便如此,語音仍然明顯是荷蘭語而不是英語,並且它確實捕捉到了說話者的聲音特徵(與示例中的原始音訊進行比較)。另一個可以嘗試的是模型的配置。例如,嘗試使用 `config.reduction_factor = 1` 來看看這是否能改善結果。

在下一節中,我們將討論如何評估文字轉語音模型。

< > 在 GitHub 上更新

© . This site is unofficial and not affiliated with Hugging Face, Inc.