新聞中心
譯者 | 朱先忠

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


咨詢
建站咨詢
