作者: Sreyan Ghosh
创建日期 2022/07/01
最后修改 2022/08/27
描述: 使用 Hugging Face Transformers 训练 Wav2Vec 2.0 用于音频分类。
语音指令识别,也称为关键词检测(KWS),对于从索引音频数据库和索引关键词到在微控制器上本地运行语音模型的各种应用,从工程角度来看非常重要。目前,许多人机界面(HCI),如 Google Assistant、Microsoft Cortana、Amazon Alexa、Apple Siri 等,都依赖于关键词检测。所有主要公司,尤其是谷歌和百度,在该领域都有大量的研究。
在过去的十年中,深度学习在该任务上取得了显著的性能提升。虽然从原始音频中提取的低级音频特征(如 MFCC 或梅尔滤波器组)已经使用了几十年,但这些低级特征的设计存在偏见。此外,在这些低级特征上训练的深度学习模型很容易过度拟合噪声或与任务无关的信号。因此,任何系统都必须学习语音表示,从而使高层次信息(例如声学和语言内容,包括音素、单词、语义含义、语调、说话者特征)可用于解决下游任务。 Wav2Vec 2.0 解决了一个自监督对比学习任务来学习高层次语音表示,为训练用于 KWS 的深度学习模型提供了传统低层次特征的绝佳替代方案。
在本笔记本中,我们以端到端的方式在关键词检测任务上训练基于 Hugging Face Transformers 库构建的 Wav2Vec 2.0(基础)模型,并在 Google Speech Commands 数据集上取得最先进的结果。
pip install git+https://github.com/huggingface/transformers.git
pip install datasets
pip install huggingface-hub
pip install joblib
pip install librosa
import random
import logging
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
# Only log error messages
tf.get_logger().setLevel(logging.ERROR)
# Set random seed
tf.keras.utils.set_random_seed(42)
# Maximum duration of the input audio file we feed to our Wav2Vec 2.0 model.
MAX_DURATION = 1
# Sampling rate is the number of samples of audio recorded every second
SAMPLING_RATE = 16000
BATCH_SIZE = 32 # Batch-size for training and evaluating our model.
NUM_CLASSES = 10 # Number of classes our dataset will have (11 in our case).
HIDDEN_DIM = 768 # Dimension of our model output (768 in case of Wav2Vec 2.0 - Base).
MAX_SEQ_LENGTH = MAX_DURATION * SAMPLING_RATE # Maximum length of the input audio file.
# Wav2Vec 2.0 results in an output frequency with a stride of about 20ms.
MAX_FRAMES = 49
MAX_EPOCHS = 2 # Maximum number of training epochs.
MODEL_CHECKPOINT = "facebook/wav2vec2-base" # Name of pretrained model from Hugging Face Model Hub
我们现在下载Google Speech Commands V1 数据集,这是一个用于训练和评估为解决 KWS 任务而构建的深度学习模型的常用基准。该数据集总共包含 60,973 个音频文件,每个文件持续 1 秒,分为十个关键词类别(“Yes”、“No”、“Up”、“Down”、“Left”、“Right”、“On”、“Off”、“Stop”和“Go”)、一个静音类别和一个包含假阳性的未知类别。我们从 Hugging Face Datasets 加载数据集。这可以通过 load_dataset
函数轻松完成。
from datasets import load_dataset
speech_commands_v1 = load_dataset("superb", "ks")
数据集具有以下字段
print(speech_commands_v1)
DatasetDict({
train: Dataset({
features: ['file', 'audio', 'label'],
num_rows: 51094
})
validation: Dataset({
features: ['file', 'audio', 'label'],
num_rows: 6798
})
test: Dataset({
features: ['file', 'audio', 'label'],
num_rows: 3081
})
})
为了演示工作流程,在本笔记本中,我们只取训练集的少量分层平衡分割(50%)作为训练集和测试集。我们可以使用 train_test_split
方法轻松拆分数据集,该方法期望拆分大小和要相对于其进行分层的列的名称。
拆分数据集后,我们删除 unknown
和 silence
类,只关注十个主要类。filter
方法可以轻松地为您完成此操作。
接下来,我们将训练集和测试集采样到 BATCH_SIZE
的倍数,以方便平滑训练和推理。您可以使用 select
方法实现此目的,该方法期望您要保留的样本的索引。其余的都会被丢弃。
speech_commands_v1 = speech_commands_v1["train"].train_test_split(
train_size=0.5, test_size=0.5, stratify_by_column="label"
)
speech_commands_v1 = speech_commands_v1.filter(
lambda x: x["label"]
!= (
speech_commands_v1["train"].features["label"].names.index("_unknown_")
and speech_commands_v1["train"].features["label"].names.index("_silence_")
)
)
speech_commands_v1["train"] = speech_commands_v1["train"].select(
[i for i in range((len(speech_commands_v1["train"]) // BATCH_SIZE) * BATCH_SIZE)]
)
speech_commands_v1["test"] = speech_commands_v1["test"].select(
[i for i in range((len(speech_commands_v1["test"]) // BATCH_SIZE) * BATCH_SIZE)]
)
print(speech_commands_v1)
DatasetDict({
train: Dataset({
features: ['file', 'audio', 'label'],
num_rows: 896
})
test: Dataset({
features: ['file', 'audio', 'label'],
num_rows: 896
})
})
此外,您可以检查与每个标签 ID 对应的实际标签。
labels = speech_commands_v1["train"].features["label"].names
label2id, id2label = dict(), dict()
for i, label in enumerate(labels):
label2id[label] = str(i)
id2label[str(i)] = label
print(id2label)
{'0': 'yes', '1': 'no', '2': 'up', '3': 'down', '4': 'left', '5': 'right', '6': 'on', '7': 'off', '8': 'stop', '9': 'go', '10': '_silence_', '11': '_unknown_'}
在我们可以将音频话语样本输入到我们的模型之前,我们需要对它们进行预处理。这是通过 Hugging Face Transformers 的“特征提取器”完成的,它将(顾名思义)将您的输入重新采样到模型期望的采样率(如果它们以不同的采样率存在),并生成模型需要的其他输入。
为了完成所有这些,我们使用 AutoFeatureExtractor.from_pretrained
实例化我们的 Feature Extractor
,这将确保
我们获得与我们要使用的模型架构相对应的 Feature Extractor
。我们下载预训练此特定检查点时使用的配置。这将被缓存,以便下次我们运行单元格时不会再次下载。
from_pretrained()
方法期望来自 Hugging Face Hub 的模型的名称。这与 MODEL_CHECKPOINT
完全类似,我们只需传递它即可。
我们编写一个简单的函数来帮助我们进行与 Hugging Face Datasets 兼容的预处理。总而言之,我们的预处理函数应
from transformers import AutoFeatureExtractor
feature_extractor = AutoFeatureExtractor.from_pretrained(
MODEL_CHECKPOINT, return_attention_mask=True
)
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=MAX_SEQ_LENGTH,
truncation=True,
padding=True,
)
return inputs
# This line with pre-process our speech_commands_v1 dataset. We also remove the "audio"
# and "file" columns as they will be of no use to us while training.
processed_speech_commands_v1 = speech_commands_v1.map(
preprocess_function, remove_columns=["audio", "file"], batched=True
)
# Load the whole dataset splits as a dict of numpy arrays
train = processed_speech_commands_v1["train"].shuffle(seed=42).with_format("numpy")[:]
test = processed_speech_commands_v1["test"].shuffle(seed=42).with_format("numpy")[:]
我们现在定义我们的模型。准确地说,我们定义一个 Wav2Vec 2.0 模型,并在顶部添加一个分类头,以输出每个输入音频样本的所有类别的概率分布。由于模型可能变得复杂,我们首先将带有分类头的 Wav2Vec 2.0 模型定义为 Keras 层,然后使用它构建模型。
我们使用 TFWav2Vec2Model
类实例化我们的主要 Wav2Vec 2.0 模型。这将实例化一个模型,该模型将根据您选择的配置(BASE 或 LARGE)输出 768 或 1024 维嵌入。 from_pretrained()
还可以帮助您从 Hugging Face 模型中心加载预训练权重。它将下载预训练权重以及调用该方法时您提到的模型名称对应的配置。对于我们的任务,我们选择模型的 BASE 变体,该变体刚刚经过预训练,因为我们对其进行微调。
from transformers import TFWav2Vec2Model
def mean_pool(hidden_states, feature_lengths):
attenion_mask = tf.sequence_mask(
feature_lengths, maxlen=MAX_FRAMES, dtype=tf.dtypes.int64
)
padding_mask = tf.cast(
tf.reverse(tf.cumsum(tf.reverse(attenion_mask, [-1]), -1), [-1]),
dtype=tf.dtypes.bool,
)
hidden_states = tf.where(
tf.broadcast_to(
tf.expand_dims(~padding_mask, -1), (BATCH_SIZE, MAX_FRAMES, HIDDEN_DIM)
),
0.0,
hidden_states,
)
pooled_state = tf.math.reduce_sum(hidden_states, axis=1) / tf.reshape(
tf.math.reduce_sum(tf.cast(padding_mask, dtype=tf.dtypes.float32), axis=1),
[-1, 1],
)
return pooled_state
class TFWav2Vec2ForAudioClassification(layers.Layer):
"""Combines the encoder and decoder into an end-to-end model for training."""
def __init__(self, model_checkpoint, num_classes):
super().__init__()
# Instantiate the Wav2Vec 2.0 model without the Classification-Head
self.wav2vec2 = TFWav2Vec2Model.from_pretrained(
model_checkpoint, apply_spec_augment=False, from_pt=True
)
self.pooling = layers.GlobalAveragePooling1D()
# Drop-out layer before the final Classification-Head
self.intermediate_layer_dropout = layers.Dropout(0.5)
# Classification-Head
self.final_layer = layers.Dense(num_classes, activation="softmax")
def call(self, inputs):
# We take only the first output in the returned dictionary corresponding to the
# output of the last layer of Wav2vec 2.0
hidden_states = self.wav2vec2(inputs["input_values"])[0]
# If attention mask does exist then mean-pool only un-masked output frames
if tf.is_tensor(inputs["attention_mask"]):
# Get the length of each audio input by summing up the attention_mask
# (attention_mask = (BATCH_SIZE x MAX_SEQ_LENGTH) ∈ {1,0})
audio_lengths = tf.cumsum(inputs["attention_mask"], -1)[:, -1]
# Get the number of Wav2Vec 2.0 output frames for each corresponding audio input
# length
feature_lengths = self.wav2vec2.wav2vec2._get_feat_extract_output_lengths(
audio_lengths
)
pooled_state = mean_pool(hidden_states, feature_lengths)
# If attention mask does not exist then mean-pool only all output frames
else:
pooled_state = self.pooling(hidden_states)
intermediate_state = self.intermediate_layer_dropout(pooled_state)
final_state = self.final_layer(intermediate_state)
return final_state
我们现在构建并编译我们的模型。我们使用 SparseCategoricalCrossentropy
来训练我们的模型,因为它是一个分类任务。遵循大量的文献,我们在 accuracy
指标上评估我们的模型。
def build_model():
# Model's input
inputs = {
"input_values": tf.keras.Input(shape=(MAX_SEQ_LENGTH,), dtype="float32"),
"attention_mask": tf.keras.Input(shape=(MAX_SEQ_LENGTH,), dtype="int32"),
}
# Instantiate the Wav2Vec 2.0 model with Classification-Head using the desired
# pre-trained checkpoint
wav2vec2_model = TFWav2Vec2ForAudioClassification(MODEL_CHECKPOINT, NUM_CLASSES)(
inputs
)
# Model
model = tf.keras.Model(inputs, wav2vec2_model)
# Loss
loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False)
# Optimizer
optimizer = keras.optimizers.Adam(learning_rate=1e-5)
# Compile and return
model.compile(loss=loss, optimizer=optimizer, metrics=["accuracy"])
return model
model = build_model()
在我们开始训练模型之前,我们将输入分为其因变量和自变量。
# Remove targets from training dictionaries
train_x = {x: y for x, y in train.items() if x != "label"}
test_x = {x: y for x, y in test.items() if x != "label"}
现在我们终于可以开始训练我们的模型了。
model.fit(
train_x,
train["label"],
validation_data=(test_x, test["label"]),
batch_size=BATCH_SIZE,
epochs=MAX_EPOCHS,
)
Epoch 1/2
28/28 [==============================] - 25s 338ms/step - loss: 2.3122 - accuracy: 0.1205 - val_loss: 2.2023 - val_accuracy: 0.2176
Epoch 2/2
28/28 [==============================] - 5s 189ms/step - loss: 2.0533 - accuracy: 0.2868 - val_loss: 1.8177 - val_accuracy: 0.5089
<keras.callbacks.History at 0x7fcee542dc50>
太棒了!现在我们已经训练了我们的模型,我们使用 model.predict()
方法预测测试集中音频样本的类别!我们看到模型预测效果不太好,因为它只在少量样本上训练了 1 个 epoch。为了获得最佳结果,我们建议在完整数据集上至少训练 5 个 epoch!
preds = model.predict(test_x)
28/28 [==============================] - 4s 44ms/step
现在我们尝试推断我们在随机采样的音频文件上训练的模型。我们听到音频文件,然后也看看我们的模型预测效果如何!
import IPython.display as ipd
rand_int = random.randint(0, len(test_x))
ipd.Audio(data=np.asarray(test_x["input_values"][rand_int]), autoplay=True, rate=16000)
print("Original Label is ", id2label[str(test["label"][rand_int])])
print("Predicted Label is ", id2label[str(np.argmax((preds[rand_int])))])
Original Label is up
Predicted Label is on
现在您可以将此模型推送到 Hugging Face 模型中心,并与您所有的朋友、家人、最喜欢的宠物分享:他们都可以使用标识符 "your-username/the-name-you-picked"
加载它,例如
model.push_to_hub("wav2vec2-ks", organization="keras-io")
tokenizer.push_to_hub("wav2vec2-ks", organization="keras-io")
在您推送模型后,这是您将来加载它的方式!
from transformers import TFWav2Vec2Model
model = TFWav2Vec2Model.from_pretrained("your-username/my-awesome-model", from_pt=True)