新聞中心
譯者 | 朱先忠

成都創(chuàng)新互聯(lián)公司是一家集網(wǎng)站建設(shè),肅北企業(yè)網(wǎng)站建設(shè),肅北品牌網(wǎng)站建設(shè),網(wǎng)站定制,肅北網(wǎng)站建設(shè)報價,網(wǎng)絡(luò)營銷,網(wǎng)絡(luò)優(yōu)化,肅北網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強企業(yè)競爭力??沙浞譂M足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時我們時刻保持專業(yè)、時尚、前沿,時刻以成就客戶成長自我,堅持不斷學習、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實用型網(wǎng)站。
審校 | 孫淑娟
簡介
為什么選擇音頻數(shù)據(jù)?
與用于文本和計算機視覺任務(wù)的NLP相比,用于音頻數(shù)據(jù)的NLP目前尚沒有得到業(yè)界足夠的重視。因此,現(xiàn)在是時候進行一些改變了!
任務(wù)
情緒識別——識別語音中是否表現(xiàn)出憤怒、幸福、悲傷、厭惡、驚訝或中性情緒。 注意:如果你能夠完整地閱讀完本教程,那么,你應該能夠重用本文示例工程中提供的代碼來實現(xiàn)任何音頻分類任務(wù)。
數(shù)據(jù)集
在本教程中,我們將使用Kaggle上公開可用的??Crema-D數(shù)據(jù)集????(在此非常感謝David? Cooper Cheyney整理了這個令人敬畏的數(shù)據(jù)集)。然后點擊??鏈接????處的下載(Download)按鈕。您應該會看到包含Crema-D音頻文件的文件archive.zip開始下載了,其中包含了7k+.wav格式的音頻文件。
注意:您可以自由使用您自己收集的任何音頻數(shù)據(jù)來取代CremaD數(shù)據(jù)集。
如果您想繼續(xù)學習本教程,GitHub示例工程所在地址是https://github.com/V-Sher/Audio-Classification-HF/tree/main/src。
Hugging Face庫與訓練API
正如本文標題中提到的,我們將使用Hugging Face庫來訓練模型。特別是,我們將使用這個庫中提供的Trainer類API。
那么,為什么使用這里的Trainer類呢?為什么不在PyTorch中編寫一個標準的訓練循環(huán)呢?
以下給出Pytorch框架中的標準樣板代碼,供您參考:
摘自我自己撰寫的PyTorch入門教程
相比之下,使用Trainer類能夠簡化編寫訓練循環(huán)所涉及的復雜性,因此可以通過一行代碼來實現(xiàn)訓練任務(wù):
trainer.train()
除了支持基本訓練循環(huán)外,Trainer類還支持在多個GPU/TPU上進行分布式訓練,如提前停止這樣的回調(diào)操作,在測試集上評估結(jié)果,等等。所有這些都可以通過在初始化Trainer類時設(shè)置幾個參數(shù)來實現(xiàn)。
如果不是因為什么,我覺得用Trainer類代替普通的PyTorch編碼肯定會有助于您開發(fā)出一個更有組織性和更干凈的代碼庫。
開發(fā)示例工程
安裝
雖然是可選的一步,但我強烈建議您通過創(chuàng)建并激活一個新的虛擬環(huán)境來開始本教程:在這個虛擬環(huán)境中,我們可以完成所有的pip安裝。
python -m venv audio_env
source activate audio_env/bin/activate
加載數(shù)據(jù)集
與任何數(shù)據(jù)建模任務(wù)一樣,我們首先需要使用數(shù)據(jù)集庫加載數(shù)據(jù)集(我們將把此數(shù)據(jù)集傳遞給Trainer類)。
pip install datasets
假定我們正在使用自定義數(shù)據(jù)集(與此庫附帶的預安裝數(shù)據(jù)集相反)的話,我們需要首先編寫一個加載腳本(我們不妨稱之為crema.py),并以Trainer類可以接受的格式來加載數(shù)據(jù)集。
在??前一篇文章?? 中,我已經(jīng)非常詳細地介紹了如何創(chuàng)建這個腳本。(我強烈建議您仔細閱讀源碼工程,以便充分了解下面代碼片段中提到的config、cache_dir、data_dir等的用法)。數(shù)據(jù)集中的每個示例都有兩個特征:文件(file)和標簽(label)。
dataset_config = {
"LOADING_SCRIPT_FILES": os.path.join(PROJECT_ROOT, "crema.py"),
"CONFIG_NAME": "clean",
"DATA_DIR": os.path.join(PROJECT_ROOT, "data/archive.zip"),
"CACHE_DIR": os.path.join(PROJECT_ROOT, "cache_crema"),
}
ds = load_dataset(
dataset_config["LOADING_SCRIPT_FILES"],
dataset_config["CONFIG_NAME"],
data_dir=dataset_config["DATA_DIR"],
cache_dir=dataset_config["CACHE_DIR"]
)
print(ds)
********* OUTPUT ********DatasetDict({
train: Dataset({
features: ['file', 'label'],
num_rows: 7442
})
})注意:當我們?yōu)镃remaD數(shù)據(jù)集創(chuàng)建一個datasets.Dataset對象時(要傳遞給Trainer類),不一定非要這樣做。我們還可以定義和使用torch.utils.data.Dataset(類似于我們在教程?https://towardsdatascience.com/recreating-keras-code-in-pytorch-an-introductory-tutorial-8db11084c60c中創(chuàng)建的CSVDataset)。
編寫模型訓練腳本
Github倉庫中的工程目錄結(jié)構(gòu)如下圖所示:
下面,讓我們開始撰寫腳本文件audio_train.py。
導入必需的庫
import os
import logging
import librosa
import wandb
import numpy as np
from datasets import DatasetDict, load_dataset, load_metric
from transformers import (
HubertForSequenceClassification,
PretrainedConfig,
Trainer,
TrainingArguments,
Wav2Vec2FeatureExtractor,
)
from utils import collator
logging.basicConfig(
format="%(asctime)s | %(levelname)s: %(message)s", level=logging.INFO
)
實驗跟蹤(可選)
USER = "vsher"
WANDB_PROJECT = "audio-classifier"
wandb.init(entity=USER, project=WANDB_PROJECT)
這段代碼節(jié)選自文件wandp.py。
我們在本處代碼中使用了Weight&Biases進行實驗跟蹤。因此,請確保您已經(jīng)創(chuàng)建了一個相應的賬戶;然后,根據(jù)您個人的詳細信息替換上述代碼中的USER和WANDB_PROJECT。
加載特征提取器
[問題]從廣義上講,什么是特征提取器?
[回答]特征提取器是一個負責為模型準備輸入特征的類。例如:對于圖像,可以包括裁剪圖像、填充;對于音頻,可以包括將原始音頻轉(zhuǎn)換為頻譜圖特征、應用標準化、填充等。
針對圖像數(shù)據(jù)的特征提取程序示例代碼如下:
>>> from transformers import ViTFeatureExtractor
>>> vit_extractor = ViTFeatureExtractor()
>>> print(vit_extractor)
ViTFeatureExtractor {
“do_normalize”: true,
“do_resize”: true,
“feature_extractor_type”: “ViTFeatureExtractor”,
“image_mean”: [0.5, 0.5, 0.5],
“image_std”: [0.5, 0.5, 0.5],
“resample”: 2,
“size”: 224
}
更具體地說,我們將使用Wav2Vec2FeatureExtractor類。其實這個類是SequenceFeatureExtractor類的一個派生類,它是Huggingface提供的用于語音識別的通用特征提取類。
歸納來看,共有三種方法可以使用Wav2Vec2FeatureExtractor類:
方法1:使用默認值。
from transformers import Wav2Vec2FeatureExtractor
feature_extractor = Wav2Vec2FeatureExtractor()
print(feature_extractor)
**** OUTPUT ****
Wav2Vec2FeatureExtractor {
"do_normalize": true,
"feature_extractor_type": "Wav2Vec2FeatureExtractor",
"feature_size": 1,
"padding_side": "right",
"padding_value": 0.0,
"return_attention_mask": false,
"sampling_rate": 16000
}
方法2:修改任何Wav2Vec2FeatureExtractor參數(shù)以創(chuàng)建自定義特征提取程序。
from transformers import Wav2Vec2FeatureExtractor
feature_extractor = Wav2Vec2FeatureExtractor(
sampling_rate=24000,
truncation=True
)
print(feature_extractor)
**** OUTPUT ****
Wav2Vec2FeatureExtractor {
"do_normalize": true,
"feature_extractor_type": "Wav2Vec2FeatureExtractor",
"feature_size": 1,
"padding_side": "right",
"padding_value": 0.0,
"return_attention_mask": false,
"sampling_rate": 24000,
"truncation": true
}
方法3:因為我們不需要任何定制,所以我們只需使用from_pretrained()方法加載預處理模型的默認特征提取器參數(shù)(通常存儲在名為preprocessor_config.json的文件中)。由于我們將使用facebook/hubert-base-ls960作為基本模型,因此我們可以獲得其特征提取器參數(shù)(可在鏈接https://huggingface.co/facebook/wav2vec2-base-960h/tree/main處的preprocessor_config.json下進行可視化檢查)。
from transformers import Wav2Vec2FeatureExtractor
model = "facebook/hubert-base-ls960"
feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(model)
print(feature_extractor)
*** OUTPUT ***
Wav2Vec2FeatureExtractor {
"do_normalize": true,
"feature_extractor_type": "Wav2Vec2FeatureExtractor",
"feature_size": 1,
"padding_side": "right",
"padding_value": 0,
"return_attention_mask": false,
"sampling_rate": 16000
}
為了看一下特征提取器的具體使用情形,讓我們將一個虛擬音頻文件作為raw_speech提供給Wav2Vec2FeatureExtractor:
model_id = "facebook/hubert-base-ls960"
feature_extractor = Wav2Vec2FeatureExtractor.from_pretrained(model_id)
audio_file = "dummy1.wav"
audio_array = librosa.load(audio_file, sr=16000, mono=False)[0]
input = feature_extractor(
raw_speech=audio_array,
sampling_rate=16000,
padding=True,
return_tensors="pt"
)
print(input)
print(input.shape)
print(audio_array.shape)
***** OUTPUT ******
{'input_values': tensor([[-0.0003, -0.0003, -0.0003, ..., 0.0006, -0.0003, -0.0003]])}
torch.Size([1, 36409])
(36409,)
需要注意的幾點:
- 特征提取器的輸出是一個包含input_values的字典。它的值只不過是應用于audio_array的規(guī)范化,即librosa庫的輸出。事實上,input.input_values和audio_array兩者都具有相同的形狀。
- 調(diào)用特征提取程序時,請確保使用的采樣頻率sampling_rate與基礎(chǔ)模型在訓練數(shù)據(jù)集時所使用的相同。我們正在使用這個Facebook模型進行訓練,它的模型卡明確聲明以16Khz的頻率采樣語音輸入。
- return_tensor可以分別取值為“pt”、“tf”和“np”(分別對應于PyTorch張量、TensorFlow對象和NumPy數(shù)組)。
- 填充對于單個音頻文件來說沒有多大意義,但當我們進行批處理時,它確實有意義,因為它會填充較短的音頻(結(jié)合額外的0秒或-1秒),使其與最長的音頻具有相同的長度。下面是用不同長度填充音頻文件的示例:
audio_file_1 = "dummy1.wav"
audio_file_2 = "dummy2.wav"
audio_array_1 = librosa.load(audio_file_1, sr=16000, mono=False)[0]
audio_array_2 = librosa.load(audio_file_2, sr=16000, mono=False)[0]
input_with_one_audio = feature_extractor(
audio_array_1,
sampling_rate=16000,
padding=True,
return_tensors="pt"
)
input_with_two_audio = feature_extractor(
[audio_array_1, audio_array_2],
sampling_rate=16000,
padding=True,
return_tensors="pt"
)
print(input_with_one_audio.input_values.shape)
print(input_with_two_audios.input_values.shape)
***** OUTPUT ****
torch.Size([1, 36409])
torch.Size([2, 37371])
既然我們知道了特征提取器模型的輸出在形狀上可能會有所不同(這取決于輸入音頻),那么,在將一批輸入推送到模型進行訓練之前,填充為什么很重要就很清楚了。處理批次時,我們可以:(a)將所有音頻填充到訓練集中最長音頻的長度,或者(b)將所有的音頻截取到最大長度。(a)的問題是,我們不必要地增加存儲這些額外填充值的內(nèi)存開銷;(b)的問題是由于截斷可能會導致一些信息丟失。
有一種更好的替代方法——在模型訓練期間使用數(shù)據(jù)收集器應用動態(tài)填充。我們很快就會看到這種用法的。
在構(gòu)建批處理(用于訓練)時,數(shù)據(jù)收集器只能對特定的輸入批應用預處理(例如填充)。
加載用于分類的基本模型
如前所述,我們將使用Facebook公司的Hubert模型對音頻進行分類。如果您對HuBERT的內(nèi)部工作機制感到好奇,請查看Jonathan Bgn為HuBERT編寫的這篇很棒的入門教程。
HubertModel可以說是一個“裸”模型類,它是唯一的一個由24個轉(zhuǎn)換器編碼器層組成的堆棧,并為這24個層中的每個層輸出原始隱藏狀態(tài)(不需要指定任何用于分類的特定的預定義參數(shù))。
bare_model = HubertModel.from_pretrained("facebook/hubert-large-ls960-ft")
last_hidden_state = bare_model(input.input_values).last_hidden_state
print(last_hidden_state.shape)
*** OUTPUT ***
torch.Size([1, 113, 1024]) # the hidden size i.e. 113 can vary depending on audio我們需要在這個裸模型之上指定某種分類相應的頭部信息,以便可以獲取最后一個隱藏層的輸出,并將其輸入到一個最終輸出6個值(六個情感類中的每一個)的線性層中。這正是HubertForSequenceClassification所做的。它在頂部有一個分類頭,用于音頻分類等任務(wù)。
然而,與上面解釋的特征提取器配置類似,如果你從預訓練模型中獲得HubertForSequenceClassification的默認配置,那么,你會注意到,由于其默認配置的定義方式,它只適用于二進制分類任務(wù)。
model_path = ""facebook/hubert-large-ls960-ft""
hubert_model = HubertForSequenceClassification.from_pretrained(model_path)
hubert_model_config = hubert_model.config
print("Num of labels:", hubert_model_config.num_labels)
**** OUTPUT ****
Num of labels: 2
為了實現(xiàn)我們的6類型分類目標,我們需要使用PretrainedConfig來更新傳遞給Hubert模型的配置(請查看“參數(shù)微調(diào)”一節(jié)的介紹:https://huggingface.co/docs/transformers/v4.21.2/en/main_classes/configuration#transformers.PretrainedConfig.architectures)。
NUM_LABELS = 6
model_id = "facebook/hubert-base-ls960"
config = PretrainedConfig.from_pretrained(model_id, num_labels=NUM_LABELS)
hubert_model = HubertForSequenceClassification.from_pretrained(
model_id,
config=config, # because we need to update num_labels as per our dataset
ignore_mismatched_sizes=True, # to avoid classifier size mismatch from from_pretrained.
)
這里有幾點需要注意:
- 在第5行,from_pretrained()函數(shù)從facebook/hubert-base-ls960加載模型架構(gòu)和模型權(quán)重(即所有24個轉(zhuǎn)換器層的權(quán)重+線性分類器)。
注意:如果只執(zhí)行hubert_model=HubertForSequenceClassification(),則會隨機初始化轉(zhuǎn)換器編碼器和分類器權(quán)重。
- 將ignore_mismatched_sizes參數(shù)設(shè)置為True很重要,因為如果沒有這項設(shè)置,那么由于大小不匹配,您會得到一個錯誤(請參見下圖):作為facebook/hubert-base-ls960的一部分提供的分類器權(quán)重具有形狀(2,classifier_proj_size),而根據(jù)我們新定義的配置,權(quán)重應該具有形狀(6,classifer_proj_size)??紤]到我們將從頭開始重新訓練線性分類器層,我們可以選擇忽略不匹配的大小。
分類器大小不匹配導致的錯誤信息
凍結(jié)網(wǎng)絡(luò)層以便于微調(diào)
一般經(jīng)驗法則是,如果訓練預訓練模型的基礎(chǔ)數(shù)據(jù)集與您正在使用的數(shù)據(jù)集有顯著不同,最好在頂部解凍和重新訓練幾層。
首先,我們正在解凍頂部兩個編碼器層(最靠近分類頭)的權(quán)重,同時保持所有其他層的權(quán)重凍結(jié)。為了凍結(jié)/解凍權(quán)重,我們設(shè)置param.require_grad的值為False/True,其中param表示模型參數(shù)。
在模型訓練期間,解凍權(quán)重意味著這些權(quán)重將像往常一樣更新,以便能夠達到當前任務(wù)的最佳值。
#首先凍結(jié)所有神經(jīng)網(wǎng)絡(luò)層
for param in hubert_model.parameters():
param.requires_grad = False
#凍結(jié)兩個編碼器層
layers_freeze_num = 2
n_layers = (
4 + layers_freeze_num * 16
) # 4是指投影器和分類器的權(quán)重和偏差。
for name, param in list(hubert_model.named_parameters())[-n_layers:]:
param.requires_grad = True
注意:雖然在訓練過程的一開始就開始解凍許多層看起來很直觀,但我并不建議這樣做。實際上,我從凍結(jié)所有層開始實驗,只訓練分類器頭。因為最后的訓練結(jié)果很不理想(毫不奇怪);所以,我通過解凍兩層來恢復訓練。
加載數(shù)據(jù)集
借助于我們的自定義加載腳本crema.py,我們現(xiàn)在可以使用數(shù)據(jù)集庫中的loaddataset()方法來加載數(shù)據(jù)集。
dataset_config = {
"LOADING_SCRIPT_FILES": os.path.join(PROJECT_ROOT, "src/data/crema.py"),
"CONFIG_NAME": "clean",
"DATA_FILES": os.path.join(PROJECT_ROOT,"data/archive.zip"),
"CACHE_DIR": os.path.join(PROJECT_ROOT, "cache_crema")
}
ds = load_dataset(
dataset_config["LOADING_SCRIPT_FILES"],
dataset_config["CONFIG_NAME"],
cache_dir=dataset_config["CACHE_DIR"],
data_files=dataset_config["DATA_FILES"]
)接下來,我們使用map()函數(shù)將數(shù)據(jù)集中的所有原始音頻(.wav格式)轉(zhuǎn)換為數(shù)組。
一般來說,map會將函數(shù)重復應用于數(shù)據(jù)集中的所有行/樣本上。
這里,函數(shù)(定義為lambda函數(shù))接受單個參數(shù)x(對應于數(shù)據(jù)集中的一行),并使用librosa.load()方法將該行中的音頻文件轉(zhuǎn)換為數(shù)組。如上所述,確保采樣率(sr)合適。
# 將原始音頻轉(zhuǎn)換為陣列
ds = ds.map(
lambda x: {
"array": librosa.load(x["file"], sr=16000, mono=False)[0]
},
num_proc=2,
)
注意:如果你在這個階段進行打?。╠s)的話,你會注意到數(shù)據(jù)集中的三個特征:
print(ds)
***** OUTPUT *****
DatasetDict({
train: Dataset({
features: ['file', 'label', 'array'],
num_rows: 7442
})
})
生成數(shù)組后,我們將再次使用map,這一次使用helper函數(shù)prepare_dataset()來準備輸入。
#將數(shù)據(jù)集處理為訓練模型所需的格式
INPUT_FIELD = "input_values"
LABEL_FIELD = "labels"
def prepare_dataset(batch, feature_extractor):
audio_arr = batch["array"]
input = feature_extractor(
audio_arr, sampling_rate=16000, padding=True, return_tensors="pt"
)
batch[INPUT_FIELD] = input.input_values[0]
batch[LABEL_FIELD] = batch[
"label"
] #colname必須是標簽,因為Trainer 類默認會查找它
return batch
在此,prepare_dataset是一個幫助函數(shù),它將處理函數(shù)應用于數(shù)據(jù)集中的每個樣本(或一組樣本,如批處理)。更具體地說,該函數(shù)做兩件事:(1)讀取batch["array"]中存在的音頻數(shù)組,并使用上面討論的feature_extractor從中提取特征,并將其存儲為一個名為input_values的新特征(除了文件、標簽和數(shù)組之外);(2)創(chuàng)建一個稱為label的新特征,其值與batch["label"]相同。
[問題]:您可能想知道每個樣本都有標簽和標簽有什么意義,特別是當它們具有相同的值時。
[原因]:默認情況下,Trainer API將查找列名標簽,我們正是考慮到此目的才這樣做的。如果您希望在這一步甚至可以完全刪除其他標簽列,那么請在創(chuàng)建加載腳本時將特征命名為“l(fā)abels”。
如果仔細觀察,你會注意到,與之前l(fā)ambda函數(shù)只接受一個輸入?yún)?shù)的映射用例不同,prepare_dataset()需要兩個參數(shù)。
記?。好慨斘覀冃枰騧ap內(nèi)的函數(shù)傳遞多個參數(shù)時,我們都必須向map傳遞fn_kwargs參數(shù)。此參數(shù)是包含要傳遞給函數(shù)的所有參數(shù)的字典。
根據(jù)其函數(shù)定義,我們需要為prepare_dataset準備兩個參數(shù):(a)數(shù)據(jù)集中的行和(b)特征提取器——因此我們必須使用以下fn_kwargsas:
# 針對所有樣本使用特征抽取器來預處理數(shù)據(jù)
ds = ds.map(
prepare_dataset,
fn_kwargs={"feature_extractor": feature_extractor},
# num_proc=2,
)
logging.info("Finished extracting features from audio arrays.")
接下來,我們將使用class_encode_column()將所有字符串標簽轉(zhuǎn)換為id(0,1,2,3,4,5,6)。
# LABEL TO ID
ds = ds.class_encode_column("label")
最后,?我們使用train_test_split()引入了訓練-測試-驗證分割。我們需要以這種方式進行兩次拆分,以獲得三個不重疊的數(shù)據(jù)集,所有這些數(shù)據(jù)集在下面的步驟8中合并為一個DatasetDict。
# 引入了訓練-測試-值分割
# 90%用于訓練,10%用于測試+驗證
train_testvalid = ds["train"].train_test_split(shuffle=True, test_size=0.1)
# 把用于測試+驗證的10%進一步對半分割:一半用于測試,一半用于校驗
test_valid = train_testvalid['test'].train_test_split(test_size=0.5)
# 如果您想擁有一個DatasetDict的話,請收集所有人的信息
ds = DatasetDict({
'train': train_testvalid['train'],
'test': test_valid['test'],
'val': test_valid['train']})
開始訓練
所有的零碎工作都準備好后,我們現(xiàn)在可以開始使用Trainer類進行訓練了。
首先,我們需要指定訓練參數(shù),這包括總迭代次數(shù)、批大小、存儲訓練模型的目錄、實驗日志記錄等。
trainer_config = {
"OUTPUT_DIR": "results",
"TRAIN_EPOCHS": 3,
"TRAIN_BATCH_SIZE": 8,
"EVAL_BATCH_SIZE": 8,
"GRADIENT_ACCUMULATION_STEPS": 4,
"WARMUP_STEPS": 500,
"DECAY": 0.01,
"LOGGING_STEPS": 10,
"MODEL_DIR": "models/test-hubert-model",
"SAVE_STEPS": 100
}
# 使用Trainer類進行微調(diào)
training_args = TrainingArguments(
output_dir=trainer_config["OUTPUT_DIR"], # output directory
gradient_accumulation_steps=trainer_config[
"GRADIENT_ACCUMULATION_STEPS"
], # 在運行優(yōu)化步驟之前累積梯度
num_train_epochs=trainer_config[
"TRAIN_EPOCHS"
], #總訓練迭代次數(shù)
per_device_train_batch_size=trainer_config[
"TRAIN_BATCH_SIZE"
], # 訓練期間每個設(shè)備的批量大小
per_device_eval_batch_size=trainer_config[
"EVAL_BATCH_SIZE"
], #用于評估的批量大小
warmup_steps=trainer_config[
"WARMUP_STEPS"
], # 學習率調(diào)度器的預熱步驟數(shù)
save_steps=trainer_config["SAVE_STEPS"], # 每100步保存檢查點
weight_decay=trainer_config["DECAY"], #權(quán)重衰減強度
logging_steps=trainer_config["LOGGING_STEPS"],
evaluation_strategy="epoch", # 在每個迭代末尾輸出指標值
report_to="wandb", # 啟動指向W&B的日志功能
)這里也有幾個需要考慮的事項:
- 梯度累積步驟在訓練期間想存儲大批量但內(nèi)存有限的情況下非常有用。設(shè)置gradient_accumulation_steps=4允許我們在每4個步驟后更新權(quán)重:在每個步驟中,batch_size=32個樣本被處理并累積它們的梯度。只有在累積足夠梯度的4個步驟后,才能更新權(quán)重。
其次,我們用這些訓練參數(shù)實例化Trainer類,并指定訓練和評估數(shù)據(jù)集。
#開始訓練
trainer = Trainer(
model=hubert_model, # 實例:對應于將訓練的轉(zhuǎn)換器模型
args=training_args, # 訓練參數(shù),如下所定義的
data_collator=data_collator,
train_dataset=ds["train"], # 訓練數(shù)據(jù)集
eval_dataset=ds["val"], # 評估數(shù)據(jù)集
compute_metrics=compute_metrics,
)
這里也還有幾個需要考慮的事項:
在第5行,我們使用了data_collator。我們在教程開始時簡要討論了這一點,作為動態(tài)填充輸入音頻陣列的一種方法。數(shù)據(jù)收集器初始化如下:
# 定義數(shù)據(jù)收集器以便動態(tài)填充訓練批次
data_collator = DataCollatorCTCWithPadding(
processor=feature_extractor,
padding=True
)
?DataCollatorCTCWithPadding是一個根據(jù)鏈接https://huggingface.co/blog/fine-tune-wav2vec2-english處教程改編的數(shù)據(jù)類。我強烈建議快速閱讀一下這個教程中的“設(shè)置Trainer”部分,以詳細了解這個類內(nèi)部的實現(xiàn)邏輯。
這個類中的__call__方法負責準備接收到的輸入,但沒有太多細節(jié)。它從數(shù)據(jù)集中獲取一批樣本(記住每個樣本都有5個特征:file、labels、label、array和input_values),并返回相同的批次,但是使用processor.pad對input_values應用了填充。此外,批次中的標簽將轉(zhuǎn)換為Pytorch張量。
from dataclasses import dataclass
from typing import Dict, List, Optional, Union
import torch
from transformers import Wav2Vec2Processor
INPUT_FIELD = "input_values"
LABEL_FIELD = "labels"
@dataclass
class DataCollatorCTCWithPadding:
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, examples: List[Dict[str, Union[List[int], torch.Tensor]]]
) -> Dict[str, torch.Tensor]:
input_features = [
{INPUT_FIELD: example[INPUT_FIELD]} for example in examples
] # 樣本基本上對應于row0, row1,等等...
labels = [example[LABEL_FIELD] for example in examples]
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",
)
batch[LABEL_FIELD] = torch.tensor(labels)
return batch
?在第8行,我們定義了compute_metrics(),這是一種告訴Trainer在評估期間必須計算哪些指標(準確度、精度、f1、召回率等)的方法。它將評估預測(eval_pred)作為輸入,并使用metric.compute(predictions=.., references=...)將實際標簽與預測標簽進行比較。同樣,compute_metrics()的樣板代碼也是從鏈接https://huggingface.co/course/chapter3/3?fw=pt處改編而來的。
from datasets import load_metric
def compute_metrics(eval_pred):
# 定義評估指標
compute_accuracy_metric = load_metric("accuracy")
logits, labels = eval_pred
predictions = np.argmax(logits, axis=-1)
return compute_accuracy_metric.compute(predictions=predictions, references=labels)
?注意:如果您想發(fā)揮創(chuàng)意并顯示自定義指標的話,那么,你可以自行修改一下compute_metrics()。在執(zhí)行此操作之前,您需要知道的只是eval_pred返回的內(nèi)容。在實際訓練模型之前,通過對eval/test數(shù)據(jù)集運行trainer.predict可以提前發(fā)現(xiàn)這一點。在我們的例子中,它返回實際的標簽和預測(即logits,在其上應用argmax函數(shù)以獲取預測的類型):
trainer = Trainer(model=..., args=...,...)
output = trainer.predict(ds["test"])
print(output)
**** 輸出*****
PredictionOutput(
predictions=array([
[ 0.0331, -0.0193, -0.98767, 0.0229, 0.01693, -0.0745],
[-0.0445, 0.0020, 0.13196, 0.2219, 0.94693, -0.0614],
.
.
.
], dtype=float32),
label_ids=array([0, 5, ......]),
metrics={'test_loss': 1.780486822128296, 'test_accuracy': 0.0, 'test_runtime': 1.6074, 'test_samples_per_second': 1.244, 'test_steps_per_second': 0.622}
)
核心訓練代碼
如果你仔細分析的話,你會注意到真正的訓練代碼也就是一行:
trainer.train()
# 從檢查點恢復培訓
# trainer.train("results/checkpoint-2000")
?請注意:源碼中第4行包含從檢查站繼續(xù)訓練的命令。但首先要搞清楚:什么是檢查點呢?
在訓練過程中,Trainer將創(chuàng)建模型權(quán)重的快照,并將其存儲在由TrainingArguments(output_dir="results")所定義的output_dir位置。這些文件夾通常命名為“checkpoint-XXXX”形式,其中包含模型權(quán)重、訓練參數(shù)等信息。
?檢查點
您可以分別使用save_strategy和save_steps指定創(chuàng)建這些檢查點的時間和頻率。默認情況下,檢查點將在每500個步驟后保存(save_steps=500)。我之所以提到這一點,是因為我不知道這些默認值。在一次訓練(持續(xù)7小時)中,我看到?jīng)]有在輸出目錄中創(chuàng)建任何檢查點。下面的數(shù)據(jù)是我正在使用的配置信息:
- 訓練樣本數(shù):6697
- epochs:5
- 批大小:32
- 梯度累積步長Step:4
?經(jīng)過數(shù)小時的調(diào)試后,我發(fā)現(xiàn)在我的例子中,總步驟只有260個,而默認保存只發(fā)生在第500個步驟之后。通過在TrainingArguments()的實現(xiàn)代碼中添加一個設(shè)置(save_steps=100)的方法幫助我修復了這個問題。
在上圖的底部,你可以找到優(yōu)化步驟的總步數(shù)。
需要提醒的是,如果您想知道如何計算總步驟數(shù)(即本例中的260步),那么請注意下面的計算公式:
總訓練批次大小=批量(Batch size)梯度累積步長=324=128
總優(yōu)化步數(shù)=(訓練樣本/總訓練批量)*epochs=(6697/128)*5≈ 260。
在測試集上進行預測和記錄結(jié)果
#測試結(jié)果
test_results = trainer.predict(ds["test"])
logging.info("Test Set Result: {}".format(test_results.metrics))
wandb.log({"test_accuracy": test_results.metrics["test_accuracy"]})
trainer.save_model(os.path.join(PROJECT_ROOT, trainer_config["MODEL_DIR"]))
#把訓練后的模型數(shù)據(jù)日志保存到wandb
wandb.save(
os.path.join(PROJECT_ROOT, trainer_config["MODEL_DIR"], "*"),
base_path=os.path.dirname(trainer_config["MODEL_DIR"]),
policy="end",
)
?這里有幾件事情需要考慮:
- 要將任何其他度量/變量記錄到權(quán)重和偏差,我們可以調(diào)用wandb.log()方法。例如,在第4行中,我們記錄下測試集的精度信息。
- 默認情況下,wandb不會記錄經(jīng)過訓練的模型,因此它僅在訓練結(jié)束后才可在本地計算機上使用。為了顯式地存儲模型信息,我們需要調(diào)用wandb.save(設(shè)置policy="end"),表示僅在運行結(jié)束時同步文件。
結(jié)果和反思
使用不同超參數(shù)組合的所有不同模型運行的結(jié)果都記錄在我的權(quán)重和偏差儀表盤中。
?WandB儀表盤
?從學習曲線來看,我們最近的運行結(jié)果(faithful-planet-28:測試準確率=68%??紤]到只需要4小時的訓練,這還算不錯)可能會從額外的訓練迭代中受益,因為訓練和評估損失仍在減少,并且還沒有穩(wěn)定下來(或者更糟的是,開始偏離)。根據(jù)是否允許存在這種情況,可能需要解凍更多的編碼器層。
?學習曲線展示
?這里有幾點值得思考:
- 如果我們增加訓練迭代次數(shù),那么通過使用回調(diào)技巧來提前停止訓練可能是值得考慮的方案。
# 提前停止訓練
trainer = Trainer(
callbacks=[EarlyStoppingCallback(early_stopping_patience = 10)]
)
- 除了Cuda之外,Trainer最近還添加了對使用新Mac M1 GPU的支持(只需設(shè)置args = TrainingArguments(use_mps_device=True))。如果您正在與他們合作,請注意網(wǎng)絡(luò)上已經(jīng)有些人提到這種情況下會出現(xiàn)指標下降的問題(這是一個已知的錯誤,請參閱鏈https://github.com/huggingface/transformers/issues/17971處提到的問題)。
結(jié)論
希望您現(xiàn)在能夠更加自信地使用轉(zhuǎn)換器庫來微調(diào)深度學習模型。如果你真正在推進這個項目,那么,請與我和更廣泛的社區(qū)共同分享您的成果(以及提高準確性的步驟)。
與任何ML項目一樣,關(guān)注負責任的AI開發(fā)對于評估未來工作的影響至關(guān)重要。最近的研究表明,情緒檢測方法可能具有內(nèi)置的性別/種族偏見,并可能對現(xiàn)實世界造成傷害,這一點變得更加重要。此外,如果您正在處理敏感音頻數(shù)據(jù)(例如包含信用卡詳細信息的客戶支持電話),請應用脫敏技術(shù)來保護個人身份信息、敏感個人數(shù)據(jù)或業(yè)務(wù)數(shù)據(jù)。
像往常一樣,如果有更簡單的方法來實現(xiàn)或解釋本文中提到的一些事情,請您告訴我一下。
譯者介紹
朱先忠,社區(qū)編輯,專家博客、講師,濰坊一所高校計算機教師,自由編程界老兵一枚。
文章題目:基于Huggingface的定制音頻數(shù)據(jù)情感識別
鏈接URL:http://m.5511xx.com/article/cosedpe.html


咨詢
建站咨詢
