程式碼範例 / 音訊資料 / 使用 Hugging Face Transformers 進行音訊分類

使用 Hugging Face Transformers 進行音訊分類

作者: Sreyan Ghosh
建立日期 2022/07/01
上次修改日期 2022/08/27
描述: 使用 Hugging Face Transformers 訓練 Wav2Vec 2.0 進行音訊分類。

ⓘ 這個範例使用 Keras 2

在 Colab 中檢視 GitHub 原始碼


簡介

語音指令的識別,也稱為「關鍵字定位」(KWS),從工程角度來看,對於廣泛的應用至關重要,從索引音訊資料庫和關鍵字,到在微控制器上本地執行語音模型。目前,許多人機介面(HCI),如 Google Assistant、Microsoft Cortana、Amazon Alexa、Apple Siri 等都依賴於關鍵字定位。所有主要公司,特別是 Google 和百度,都在該領域進行了大量研究。

在過去十年中,深度學習在此任務上取得了顯著的效能提升。雖然從原始音訊中提取的低階音訊特徵(如 MFCC 或梅爾濾波器組)已經使用了數十年,但這些低階特徵的設計存在偏差。此外,在這些低階特徵上訓練的深度學習模型很容易過度擬合到雜訊或與任務無關的訊號。這使得任何系統都必須學習語音表示,使其能夠從語音訊號中獲得高階資訊,例如聲學和語言內容,包括音素、單字、語義含義、語調、語者特徵,以解決下游任務。Wav2Vec 2.0 解決了自我監督對比學習任務,以學習高階語音表示,為訓練 KWS 深度學習模型提供了傳統低階特徵的絕佳替代方案。

在本筆記中,我們以端到端的方式在關鍵字定位任務上訓練基於 Hugging Face Transformers 庫的 Wav2Vec 2.0 (base) 模型,並在 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 資料集

我們現在下載 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")

資料集具有以下欄位

  • file:音訊原始 .wav 檔案的路徑
  • audio:以 16kHz 取樣的音訊檔案
  • label:音訊語音的標籤 ID
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 方法分割資料集,該方法預期分割大小和相對於您要分層的欄名稱。

分割資料集後,我們刪除 unknownsilence 類別,只關注十個主要類別。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。我們下載預訓練此特定檢查點時使用的 config。這將被快取,以便下次執行儲存格時不會再次下載。

from_pretrained() 方法預期來自 Hugging Face Hub 的模型名稱。這與 MODEL_CHECKPOINT 完全類似,我們只需傳遞它。

我們編寫一個簡單的函式,幫助我們進行與 Hugging Face Datasets 相容的預處理。總之,我們的預處理函式應

  • 呼叫音訊欄以載入音訊檔案,並在必要時重新取樣。
  • 檢查音訊檔案的取樣率是否與預訓練模型使用的音訊資料的取樣率相符。您可以在 Wav2Vec 2.0 模型卡上找到此資訊。
  • 設定最大輸入長度,以便對較長的輸入進行批處理而不會被截斷。
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 模型,並在頂部新增一個分類標頭,以輸出每個輸入音訊樣本所有類別的機率分佈。由於模型可能會變得複雜,因此我們首先將帶有分類標頭的 Wav2Vec 2.0 模型定義為 Keras 層,然後使用它來建構模型。

我們使用 TFWav2Vec2Model 類別實例化我們的主要 Wav2Vec 2.0 模型。這將實例化一個模型,該模型將根據您選擇的 config(BASE 或 LARGE)輸出 768 或 1024 維的嵌入。此外,from_pretrained() 還可以協助您從 Hugging Face 模型 Hub 載入預訓練權重。它將下載預訓練權重以及呼叫方法時所提及的模型名稱的 config。對於我們的任務,我們選擇剛剛預訓練的模型的 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 個週期。為了獲得最佳結果,我們建議在完整資料集上訓練至少 5 個週期!

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 模型 Hub,並與您的所有朋友、家人、最喜歡的寵物分享:他們都可以使用識別碼 "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)