在Hugging Face中使用🤗 Transformers对Wav2Vec2进行英语ASR的微调
微调Hugging Face的🤗 Transformers库以在Wav2Vec2上进行英语ASR
Wav2Vec2是一个用于自动语音识别(ASR)的预训练模型,由Alexei Baevski、Michael Auli和Alex Conneau于2020年9月发布。
使用一种新颖的对比预训练目标,Wav2Vec2从超过50,000小时的无标签语音中学习到了强大的语音表示。类似于BERT的掩码语言建模,该模型通过在将特征向量传递给Transformer网络之前随机屏蔽特征向量来学习上下文化的语音表示。
首次证明了预训练后,在非常少量的标记语音数据上进行微调可以达到与最先进的ASR系统相竞争的结果。使用仅10分钟的标记数据,Wav2Vec2在LibriSpeech的干净测试集上的词错误率(WER)低于5% – 参见论文的表9。
在本笔记本中,我们将详细解释如何在任何英语ASR数据集上微调Wav2Vec2的预训练检查点。请注意,在本笔记本中,我们将在不使用语言模型的情况下微调Wav2Vec2。使用Wav2Vec2作为端到端的ASR系统而不使用语言模型更简单,并且已经证明独立的Wav2Vec2声学模型可以取得令人印象深刻的结果。为了演示目的,我们在只包含5小时训练数据的Timit数据集上微调了“base”大小的预训练检查点。
Wav2Vec2使用连接主义时序分类(CTC)进行微调,这是一种用于训练序列到序列问题的神经网络的算法,主要用于自动语音识别和手写识别。
我强烈建议阅读Awni Hannun撰写的博客文章《Sequence Modeling with CTC(2017)》。
在我们开始之前,让我们从主库中安装datasets
和transformers
。此外,我们还需要soundfile
包来加载音频文件,以及jiwer
来使用词错误率(WER)指标评估我们微调的模型。
!pip install datasets>=1.18.3
!pip install transformers==4.11.3
!pip install librosa
!pip install jiwer
接下来,我们强烈建议在训练过程中直接将训练检查点上传到Hugging Face Hub。Hub具有集成的版本控制,因此您可以确保在训练过程中不会丢失任何模型检查点。
为此,您需要存储来自Hugging Face网站的身份验证令牌(如果尚未注册,请在此处注册!)
from huggingface_hub import notebook_login
notebook_login()
输出结果:
登录成功
您的令牌已保存到/root/.huggingface/token
通过git凭证存储进行身份验证,但这不是您计算机上定义的助手。
在向Hugging Face Hub推送时,您需要重新认证。在终端中运行以下命令将其设置为默认值
git config --global credential.helper store
然后,您需要安装Git-LFS以上传您的模型检查点:
!apt install git-lfs
1 Timit通常使用音素错误率(PER)进行评估,但在ASR中最常见的度量标准是词错误率(WER)。为了使本笔记本尽可能通用,我们决定使用WER评估模型。
准备数据、标记器和特征提取器
ASR模型将语音转录为文本,这意味着我们需要一个特征提取器来处理语音信号,将其转化为模型的输入格式(例如特征向量),以及一个标记器来将模型的输出格式转化为文本。
在🤗 Transformers中,Wav2Vec2模型配备了一个名为Wav2Vec2CTCTokenizer的标记器和一个名为Wav2Vec2FeatureExtractor的特征提取器。
让我们从创建分词器开始,该分词器负责解码模型的预测。
创建Wav2Vec2CTCTokenizer
预训练的Wav2Vec2检查点将语音信号映射到上图所示的上下文表示序列。微调的Wav2Vec2检查点需要将这个上下文表示序列映射到对应的转录,因此需要在变换器块(黄色部分)的顶部添加一个线性层。该线性层用于将每个上下文表示分类为一个标记类别,类似于在BERT的嵌入顶部添加线性层进行进一步分类。预训练后的线性层添加可以参考这篇博文中的”BERT”部分。
该层的输出大小对应于词汇表中的标记数,这与Wav2Vec2的预训练任务无关,只与用于微调的标记数据集有关。因此,在第一步中,我们将查看Timit数据集并根据数据集的转录定义一个词汇表。
让我们从加载数据集并查看其结构开始。
from datasets import load_dataset, load_metric
timit = load_dataset("timit_asr")
print(timit)
打印输出:
DatasetDict({
train: Dataset({
features: ['file', 'audio', 'text', 'phonetic_detail', 'word_detail', 'dialect_region', 'sentence_type', 'speaker_id', 'id'],
num_rows: 4620
})
test: Dataset({
features: ['file', 'audio', 'text', 'phonetic_detail', 'word_detail', 'dialect_region', 'sentence_type', 'speaker_id', 'id'],
num_rows: 1680
})
})
许多ASR数据集只提供每个音频文件'file'
的目标文本'text'
。Timit实际上还提供了关于每个音频文件的更多信息,例如'phonetic_detail'
等。因此,许多研究人员在使用Timit进行工作时,选择在音素分类而不是语音识别上评估模型。然而,我们希望尽可能地保持笔记本的通用性,所以我们只考虑转录文本进行微调。
timit = timit.remove_columns(["phonetic_detail", "word_detail", "dialect_region", "id", "sentence_type", "speaker_id"])
让我们编写一个显示数据集的随机样本的简短函数,并运行它几次以对转录有所了解。
from datasets import ClassLabel
import random
import pandas as pd
from IPython.display import display, HTML
def show_random_elements(dataset, num_examples=10):
assert num_examples <= len(dataset), "无法选择超过数据集中的元素数量的元素。"
picks = []
for _ in range(num_examples):
pick = random.randint(0, len(dataset)-1)
while pick in picks:
pick = random.randint(0, len(dataset)-1)
picks.append(pick)
df = pd.DataFrame(dataset[picks])
display(HTML(df.to_html()))
show_random_elements(timit["train"].remove_columns(["file", "audio"]))
打印输出:
好的!转录看起来非常干净,语言似乎更符合书面文本而不是对话。这在考虑到Timit是一个阅读语音语料库时是有道理的。
我们可以看到转录包含一些特殊字符,如,.?!;:
。没有语言模型的情况下,要将语音块分类为这些特殊字符更加困难,因为它们实际上并不对应于一个特定的声音单位。例如,字母"s"
有一个相对清晰的声音,而特殊字符"."
没有。此外,为了理解语音信号的含义,通常不需要在转录中包含特殊字符。
此外,我们将文本标准化为仅包含小写字母。
import re
chars_to_ignore_regex = '[\,\?\.\!\-\;\:\"]'
def remove_special_characters(batch):
batch["text"] = re.sub(chars_to_ignore_regex, '', batch["text"]).lower()
return batch
timit = timit.map(remove_special_characters)
让我们来看一下预处理后的转录。
show_random_elements(timit["train"].remove_columns(["file", "audio"]))
输出结果:
很好!这看起来更好。我们已经从转录中删除了大部分特殊字符,并将它们规范化为仅小写。
在CTC中,将语音块分类为字母是很常见的,所以我们在这里也会这样做。让我们提取训练和测试数据中的所有不同字母,并从这组字母中构建我们的词汇表。
我们编写一个映射函数,将所有转录连接成一个长转录,然后将字符串转换为字符集合。重要的是要将参数batched=True
传递给map(...)
函数,以便映射函数可以一次访问所有转录。
def extract_all_chars(batch):
all_text = " ".join(batch["text"])
vocab = list(set(all_text))
return {"vocab": [vocab], "all_text": [all_text]}
vocabs = timit.map(extract_all_chars, batched=True, batch_size=-1, keep_in_memory=True, remove_columns=timit.column_names["train"])
现在,我们创建训练数据集和测试数据集中所有不同字母的并集,并将结果列表转换为一个枚举字典。
vocab_list = list(set(vocabs["train"]["vocab"][0]) | set(vocabs["test"]["vocab"][0]))
vocab_dict = {v: k for k, v in enumerate(vocab_list)}
vocab_dict
输出结果:
{
' ': 21,
"'": 13,
'a': 24,
'b': 17,
'c': 25,
'd': 2,
'e': 9,
'f': 14,
'g': 22,
'h': 8,
'i': 4,
'j': 18,
'k': 5,
'l': 16,
'm': 6,
'n': 7,
'o': 10,
'p': 19,
'q': 3,
'r': 20,
's': 11,
't': 0,
'u': 26,
'v': 27,
'w': 1,
'x': 23,
'y': 15,
'z': 12
}
很棒,我们看到数据集中包含了所有字母(这并不令人惊讶),我们还提取了特殊字符" "
和'
。请注意,我们没有排除这些特殊字符,因为:
- 模型必须学会预测单词何时结束,否则模型的预测将始终是一个字符序列,这将使得无法将单词从彼此分开。
- 在英语中,我们需要保留
'
字符以区分单词,例如"it's"
和"its"
,它们具有非常不同的含义。
为了更清楚地表明" "
具有自己的标记类别,我们给它一个更可见的字符|
。此外,我们还添加了一个”unknown”标记,以便模型可以处理Timit训练集中未遇到的字符。
vocab_dict["|"] = vocab_dict[" "]
del vocab_dict[" "]
最后,我们还添加了一个填充标记,对应于CTC的”空白标记”。”空白标记”是CTC算法的核心组成部分。详细信息,请参阅此处的”对齐”部分。
vocab_dict["[UNK]"] = len(vocab_dict)
vocab_dict["[PAD]"] = len(vocab_dict)
print(len(vocab_dict))
输出结果:
30
很棒,现在我们的词汇表已经完整,包含30个标记,这意味着我们将在预训练的Wav2Vec2检查点之上添加的线性层将具有30个输出维度。
现在,让我们将词汇表保存为一个json文件。
import json
with open('vocab.json', 'w') as vocab_file:
json.dump(vocab_dict, vocab_file)
在最后一步中,我们使用json文件来实例化Wav2Vec2CTCTokenizer
类的对象。
from transformers import Wav2Vec2CTCTokenizer
tokenizer = Wav2Vec2CTCTokenizer("./vocab.json", unk_token="[UNK]", pad_token="[PAD]", word_delimiter_token="|")
如果想要重新使用刚刚创建的tokenizer与此notebook中的fine-tuned模型,强烈建议将tokenizer
上传到🤗 Hub。让我们将要上传的文件命名为"wav2vec2-large-xlsr-turkish-demo-colab"
:
repo_name = "wav2vec2-base-timit-demo-colab"
然后将tokenizer上传到🤗 Hub。
tokenizer.push_to_hub(repo_name)
太好了,你可以在https://huggingface.co/<your-username>/wav2vec2-base-timit-demo-colab
下看到刚刚创建的仓库。
创建Wav2Vec2特征提取器
语音是连续信号,为了让计算机处理,首先需要离散化,这通常被称为采样。采样率在此起着重要作用,它定义了每秒测量语音信号的数据点数量。因此,使用更高的采样率会得到更好的真实语音信号近似,但也需要更多的数值。
预训练模型期望其输入数据的采样率与训练模型时使用的数据的采样率基本相同。相同的语音信号在两个不同的采样率下具有非常不同的分布,例如,将采样率加倍会导致数据点的长度加倍。因此,在对ASR模型的预训练模型进行微调之前,验证用于预训练模型的数据的采样率是否与用于微调模型的数据的采样率匹配非常重要。
Wav2Vec2是在LibriSpeech和LibriVox的音频数据上进行的预训练,两者的采样率都为16kHz。我们的微调数据集Timit也恰好采样率为16kHz。如果微调数据集的采样率低于或高于16kHz,我们需要先上采样或下采样语音信号,使其与用于预训练的数据的采样率匹配。
创建Wav2Vec2特征提取器对象需要以下参数:
feature_size
:语音模型将一系列特征向量作为输入。虽然此序列的长度显然会有所不同,但特征大小不应该变化。对于Wav2Vec2而言,特征大小为1,因为该模型是在原始语音信号上进行训练的。sampling_rate
:模型训练时的采样率。padding_value
:对于批量推断,需要使用特定值对较短的输入进行填充。do_normalize
:输入是否应进行零均值单位方差归一化。通常,归一化输入可以提高语音模型的性能。return_attention_mask
:模型在批量推断时是否使用attention_mask
。一般来说,模型应始终使用attention_mask
来屏蔽填充的标记。然而,由于Wav2Vec2
的“base”检查点的一个非常特定的设计选择,不使用attention_mask
可以获得更好的结果。这不推荐用于其他语音模型。有关更多信息,可以查看此问题。 重要提示 如果要使用此notebook来微调large-lv60,则应将此参数设置为True
。
from transformers import Wav2Vec2FeatureExtractor
feature_extractor = Wav2Vec2FeatureExtractor(feature_size=1, sampling_rate=16000, padding_value=0.0, do_normalize=True, return_attention_mask=False)
太好了,Wav2Vec2的特征提取流程已经完全定义!
为了使Wav2Vec2的使用尽可能用户友好,特征提取器和分词器被封装到一个单独的Wav2Vec2Processor
类中,这样只需要一个model
和processor
对象。
from transformers import Wav2Vec2Processor
processor = Wav2Vec2Processor(feature_extractor=feature_extractor, tokenizer=tokenizer)
预处理数据
到目前为止,我们还没有看到语音信号的实际值,只有转录结果。除了句子之外,我们的数据集还包括两个列名:path和audio。path指定了音频文件的绝对路径。让我们来看一下。
print(timit[0]["path"])
输出结果:
'/root/.cache/huggingface/datasets/downloads/extracted/404950a46da14eac65eb4e2a8317b1372fb3971d980d91d5d5b221275b1fd7e0/data/TRAIN/DR4/MMDM0/SI681.WAV'
Wav2Vec2
期望输入是16 kHz的一维数组。这意味着音频文件必须被加载和重采样。
幸运的是,datasets会自动调用其他列audio来完成这个过程。让我们试一试。
common_voice_train[0]["audio"]
输出结果:
{'array': array([-2.1362305e-04, 6.1035156e-05, 3.0517578e-05, ...,
-3.0517578e-05, -9.1552734e-05, -6.1035156e-05], dtype=float32),
'path': '/root/.cache/huggingface/datasets/downloads/extracted/404950a46da14eac65eb4e2a8317b1372fb3971d980d91d5d5b221275b1fd7e0/data/TRAIN/DR4/MMDM0/SI681.WAV',
'sampling_rate': 16000}
我们可以看到音频文件已经被自动加载了。这要归功于datasets == 4.13.3中引入的新功能"Audio" feature
,它在调用时会实时加载和重采样音频文件。
采样率被设置为16kHz,正是Wav2Vec2
所期望的输入格式。
太好了,让我们听一些音频文件,以更好地了解数据集并验证音频是否被正确加载。
import IPython.display as ipd
import numpy as np
import random
rand_int = random.randint(0, len(timit["train"]))
print(timit["train"][rand_int]["text"])
ipd.Audio(data=np.asarray(timit["train"][rand_int]["audio"]["array"]), autoplay=True, rate=16000)
可以听到,说话人的变化伴随着他们的语速、口音等的变化。总体而言,录音听起来相对清晰,这是可以预期的,因为它是一个朗读语音语料库。
让我们通过打印语音输入的形状、转录文本和相应的采样率,最后检查数据是否准备正确。
rand_int = random.randint(0, len(timit["train"]))
print("目标文本:", timit["train"][rand_int]["text"])
print("输入数组形状:", np.asarray(timit["train"][rand_int]["audio"]["array"]).shape)
print("采样率:", timit["train"][rand_int]["audio"]["sampling_rate"])
输出结果:
目标文本: she had your dark suit in greasy wash water all year
输入数组形状: (52941,)
采样率: 16000
好!一切看起来都很好 – 数据是一个一维数组,采样率始终对应16kHz,并且目标文本已经归一化。
最后,我们可以将数据集处理成模型训练所期望的格式。我们将使用map(...)
函数。
首先,我们通过调用batch["audio"]
来加载和重采样音频数据。其次,我们从加载的音频文件中提取input_values
。在我们的情况下,Wav2Vec2Processor
只对数据进行归一化处理。然而,对于其他语音模型,这一步可能包括更复杂的特征提取,比如对数-梅尔特征提取。第三,我们将转录文本编码为标签id。
注意:这个映射函数是如何使用Wav2Vec2Processor
类的一个很好的示例。在“正常”情况下,调用processor(...)
会重定向到Wav2Vec2FeatureExtractor
的调用方法。然而,当将处理器包装到as_target_processor
上下文中时,相同的方法会重定向到Wav2Vec2CTCTokenizer
的调用方法。更多信息请查阅文档。
“`python
def prepare_dataset(batch):
audio = batch[“audio”]
# 将批量输出“解批”以确保映射正确
batch[“input_values”] = processor(audio[“array”], sampling_rate=audio[“sampling_rate”]).input_values[0]
with processor.as_target_processor():
batch[“labels”] = processor(batch[“text”]).input_ids
return batch
“`
让我们将数据准备函数应用到所有示例上。
“`python
timit = timit.map(prepare_dataset, remove_columns=timit.column_names[“train”], num_proc=4)
“`
注意:目前datasets
使用torchaudio
和librosa
进行音频加载和重采样。如果你希望实现自己的自定义数据加载/重采样,可以自由地只使用"path"
列,并忽略"audio"
列。
## 训练与评估
数据经过处理,我们准备好开始设置训练流程。我们将使用🤗的Trainer,需要完成以下步骤:
– 定义一个数据收集器(data collator)。与大多数NLP模型不同,Wav2Vec2的输入长度要大于输出长度。例如,输入长度为50000的样本的输出长度最多为100。鉴于输入大小较大,动态填充训练批次更加高效,意味着所有训练样本只需填充到其批次中最长的样本长度,而不是整个数据集中最长的样本长度。因此,微调Wav2Vec2需要一个特殊的填充数据收集器,我们将在下面定义它。
– 评估指标。在训练过程中,模型应该根据单词错误率进行评估。我们需要相应地定义一个compute_metrics
函数。
– 加载预训练的检查点。我们需要加载一个预训练的检查点,并正确配置它进行训练。
– 定义训练配置。
在对模型进行微调后,我们将在测试数据上对其进行正确评估,并验证其是否确实学会了正确的语音转录。
### 设置Trainer
让我们首先定义数据收集器。数据收集器的代码是从这个示例中复制过来的。
不去过多介绍,与常见的数据收集器不同,这个数据收集器对待input_values
和labels
不同,并对它们应用不同的填充函数(再次利用Wav2Vec2的上下文管理器)。这是必需的,因为在语音中,输入和输出是不同的模态,因此不能使用相同的填充函数对待。与常见的数据收集器类似,标签中的填充标记用-100
替换,以在计算损失时不计入这些标记。
“`python
import torch
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Union
@dataclass
class DataCollatorCTCWithPadding:
“””
Data collator that will dynamically pad the inputs received.
Args:
processor (:class:`~transformers.Wav2Vec2Processor`)
The processor used for proccessing the data.
padding (:obj:`bool`, :obj:`str` or :class:`~transformers.tokenization_utils_base.PaddingStrategy`, `optional`, defaults to :obj:`True`):
Select a strategy to pad the returned sequences (according to the model’s padding side and padding index)
among:
* :obj:`True` or :obj:`’longest’`: Pad to the longest sequence in the batch (or no padding if only a single
sequence if provided).
* :obj:`’max_length’`: Pad to a maximum length specified with the argument :obj:`max_length` or to the
maximum acceptable input length for the model if that argument is not provided.
* :obj:`False` or :obj:`’do_not_pad’` (default): No padding (i.e., can output a batch with sequences of
different lengths).
max_length (:obj:`int`, `optional`):
Maximum length of the “input_values“ of the returned list and optionally padding length (see above).
max_length_labels (:obj:`int`, `optional`):
Maximum length of the “labels“ returned list and optionally padding length (see above).
pad_to_multiple_of (:obj:`int`, `optional`):
If set will pad the sequence to a multiple of the provided value.
This is especially useful to enable the use of Tensor Cores on NVIDIA hardware with compute capability >=
7.5 (Volta).
“””
processor: Wav2Vec2Processor
padding: Union[bool, str] = True
max_length: Optional[int] = None
max_length_labels: Optional[int] = None
pad_to_multiple_of: Optional[int] = None
pad_to_multiple_of_labels: Optional[int] = None
def __call__(self, features: List[Dict[str, Union[List[int], torch.Tensor]]]) -> Dict[str, torch.Tensor]:
# 将输入和标签分开处理,因为它们的长度不同,需要不同的填充方法
input_features = [{“input_values”: feature[“input_values”]} for feature in features]
label_features = [{“input_ids”: feature[“labels”]} for feature in features]
batch = self.processor.pad(
input_features,
padding=self.padding,
max_length=self.max_length,
pad_to_multiple_of=self.pad_to_multiple_of,
return_tensors=”pt”,
)
with self.processor.as_target_processor():
labels_batch = self.processor.pad(
label_features,
padding=self.padding,
max_length=self.max_length_labels,
pad_to_multiple_of=self.pad_to_multiple_of_labels,
return_tensors=”pt”,
)
# 用-100替换填充标记以正确忽略损失
labels = labels_batch[“input_ids”].masked_fill(labels_batch.attention_mask.ne(1), -100)
batch[“labels”] = labels
return batch
“`
让我们初始化数据整理器。
data_collator = DataCollatorCTCWithPadding(processor=processor, padding=True)
接下来,定义评估指标。如前所述,ASR中主要的指标是词错误率(WER),因此在这个笔记本中我们也将使用它。
wer_metric = load_metric("wer")
模型将返回一系列的逻辑向量:
y 1 , … , y m \mathbf{y}_1, \ldots, \mathbf{y}_m y 1 , … , y m ,
其中 y 1 = f θ ( x 1 , … , x n ) [ 0 ] \mathbf{y}_1 = f_{\theta}(x_1, \ldots, x_n)[0] y 1 = f θ ( x 1 , … , x n ) [ 0 ] 且 n > > m n >> m n > > m 。
逻辑向量 y 1 \mathbf{y}_1 y 1 包含了我们之前定义的词汇表中每个单词的对数几率,因此 len ( y i ) = \text{len}(\mathbf{y}_i) = len ( y i ) = config.vocab_size
。我们对模型的最可能预测感兴趣,因此取对数几率的 argmax(...)
。此外,我们通过将 -100
替换为 pad_token_id
并解码标签的 id,确保连续的标记不以 CTC 样式 1 {}^1 1 组合为同一个标记。
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_metric.compute(predictions=pred_str, references=label_str)
return {"wer": wer}
现在,我们可以加载预训练的 Wav2Vec2
检查点。分词器的 pad_token_id
必须定义模型的 pad_token_id
,或者在 Wav2Vec2ForCTC
的情况下还需定义 CTC 的空白标记 2 {}^2 2 。为了节省 GPU 内存,我们开启 PyTorch 的梯度检查点,并将损失降低方式设置为 ” mean “。
from transformers import Wav2Vec2ForCTC
model = Wav2Vec2ForCTC.from_pretrained(
"facebook/wav2vec2-base",
ctc_loss_reduction="mean",
pad_token_id=processor.tokenizer.pad_token_id,
)
输出结果:
Some weights of Wav2Vec2ForCTC were not initialized from the model checkpoint at facebook/wav2vec2-base and are newly initialized: ['lm_head.weight', 'lm_head.bias']
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
Wav2Vec2 的第一个组件是一堆 CNN 层,用于从原始语音信号中提取声学上具有意义但在上下文中是独立的特征。这部分模型在预训练过程中已经得到了充分的训练,并且正如论文中所述,不需要再进行微调。因此,我们可以将特征提取部分的所有参数的 requires_grad
设置为 False
。
model.freeze_feature_extractor()
最后一步,我们定义与训练相关的所有参数。对一些参数进行更详细的解释:
group_by_length
通过将相似输入长度的训练样本分组到一个批次中,使训练更加高效。这可以通过大大减少通过模型的无用填充标记的总数来显著加快训练时间learning_rate
和weight_decay
经过启发式调整,直到微调变得稳定。请注意,这些参数在 Timit 数据集上强烈依赖,对于其他语音数据集可能不是最优的。
对于其他参数的更多解释,可以查看文档。
在训练过程中,每400个训练步骤将异步上传一个检查点到hub。这样即使在模型仍在训练的时候,您也可以玩弄演示小部件。
注意:如果不想将模型检查点上传到hub,只需设置push_to_hub=False
。
from transformers import TrainingArguments
training_args = TrainingArguments(
output_dir=repo_name,
group_by_length=True,
per_device_train_batch_size=32,
evaluation_strategy="steps",
num_train_epochs=30,
fp16=True,
gradient_checkpointing=True,
save_steps=500,
eval_steps=500,
logging_steps=500,
learning_rate=1e-4,
weight_decay=0.005,
warmup_steps=1000,
save_total_limit=2,
)
现在,所有实例都可以传递给Trainer,我们可以开始训练了!
from transformers import Trainer
trainer = Trainer(
model=model,
data_collator=data_collator,
args=training_args,
compute_metrics=compute_metrics,
train_dataset=timit_prepared["train"],
eval_dataset=timit_prepared["test"],
tokenizer=processor.feature_extractor,
)
1 {}^1 1 为了使模型独立于说话者的语速,CTC中连续的相同标记被简单地组合为一个标记。然而,在解码时,编码的标签不应该被组合,因为它们不对应模型的预测标记,所以必须传递group_tokens=False
参数。如果我们不传递这个参数,像"hello"
这样的单词会被错误地编码和解码成"helo"
。
2 {}^2 2 空白标记允许模型通过强制在两个l之间插入空白标记来预测一个单词,比如"hello"
。我们模型对"hello"
的CTC兼容预测将是[PAD] [PAD] "h" "e" "e" "l" "l" [PAD] "l" "o" "o" [PAD]
。
训练
训练时间将在90到180分钟之间,具体取决于分配给此笔记本附加的Google Colab的GPU。虽然经过训练的模型在Timit的测试数据上产生了令人满意的结果,但它绝不是一个优化调整的模型。此笔记本的目的是演示如何在任何英语数据集上微调Wav2Vec2的base、large和large-lv60检查点。
如果您想使用这个Google Colab来微调您的模型,您应该确保您的训练不会因为不活动而停止。一个简单的方法是将以下代码粘贴到此选项卡的控制台中(右键单击 -> 检查 -> 控制台选项卡并插入代码)。
function ConnectButton(){
console.log("Connect pushed");
document.querySelector("#top-toolbar > colab-connect-button").shadowRoot.querySelector("#connect").click()
}
setInterval(ConnectButton,60000);
trainer.train()
根据您的GPU,可能会出现"out-of-memory"
错误。在这种情况下,最好将per_device_train_batch_size
减小到16或更小,并最终使用gradient_accumulation
。
打印输出:
最终的WER应该低于0.3,这是合理的,因为最先进的音素错误率(PER)略低于0.1(见排行榜),而WER通常比PER更差。
现在,您可以将训练结果上传到Hub,只需执行这个指令:
trainer.push_to_hub()
现在,您可以与所有的朋友、家人和喜爱的宠物共享这个模型,他们都可以使用”your-username/the-name-you-picked”这个标识符加载它,例如:
from transformers import AutoModelForCTC, Wav2Vec2Processor
model = AutoModelForCTC.from_pretrained("patrickvonplaten/wav2vec2-base-timit-demo-colab")
processor = Wav2Vec2Processor.from_pretrained("patrickvonplaten/wav2vec2-base-timit-demo-colab")
评估
在最后一部分,我们对测试集上的微调模型进行评估,并对其进行一些调试。
让我们加载processor
和model
。
processor = Wav2Vec2Processor.from_pretrained(repo_name)
model = Wav2Vec2ForCTC.from_pretrained(repo_name)
现在,我们将使用map(...)
函数来预测每个测试样本的转录,并将预测保存在数据集中。我们将称结果字典为"results"
。
注意:出于以下原因,我们故意使用batch_size=1
来评估测试数据集。由于填充的输入产生的输出与非填充的输入不完全相同,通过完全不填充输入可以获得更好的WER。
def map_to_result(batch):
with torch.no_grad():
input_values = torch.tensor(batch["input_values"], device="cuda").unsqueeze(0)
logits = model(input_values).logits
pred_ids = torch.argmax(logits, dim=-1)
batch["pred_str"] = processor.batch_decode(pred_ids)[0]
batch["text"] = processor.decode(batch["labels"], group_tokens=False)
return batch
results = timit["test"].map(map_to_result, remove_columns=timit["test"].column_names)
现在,我们来计算整体WER。
print("测试WER:{:.3f}".format(wer_metric.compute(predictions=results["pred_str"], references=results["text"])))
输出结果:
测试WER:0.221
22.1%的WER – 不错!我们的示例模型可能已经在官方排行榜上获得了好成绩。
让我们看一些预测,看看模型犯了哪些错误。
输出结果:
show_random_elements(results.remove_columns(["speech", "sampling_rate"]))
很明显,预测的转录在声学上与目标转录非常相似,但经常包含拼写或语法错误。这并不奇怪,因为我们纯粹依赖于Wav2Vec2,而没有使用语言模型。
最后,为了更好地理解CTC的工作原理,值得深入研究模型的准确输出。让我们将第一个测试样本输入模型,提取出预测的id,并将其转换为相应的标记。
model.to("cuda")
with torch.no_grad():
logits = model(torch.tensor(timit["test"][:1]["input_values"], device="cuda")).logits
pred_ids = torch.argmax(logits, dim=-1)
# 将id转换为标记
" ".join(processor.tokenizer.convert_ids_to_tokens(pred_ids[0].tolist()))
输出结果:
[PAD] [PAD] [PAD] [PAD] [PAD] [PAD] t t h e e | | b b [PAD] u u n n n g g [PAD] a [PAD] [PAD] l l [PAD] o o o [PAD] | w w a a [PAD] s s | | [PAD] [PAD] p l l e e [PAD] [PAD] s s e n n t t t [PAD] l l y y | | | s s [PAD] i i [PAD] t t t [PAD] u u u u [PAD] [PAD] [PAD] a a [PAD] t t e e e d d d | n n e e a a a r | | t h h e | | s s h h h [PAD] o o o [PAD] o o r r [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD] [PAD]
输出结果应该让CTC的工作原理更加清晰。由于模型已经学会了在待分类的语音块仍然对应相同标记的情况下只是重复相同的标记,因此它在某种程度上对于说话速度是不变的。这使得CTC成为语音识别的一个非常强大的算法,因为语音文件的转录通常与其长度无关。
我再次建议读者阅读这篇非常好的博客文章,以更好地理解CTC。