情感分析實(shí)戰(zhàn):融合文本音頻視覺的深度學(xué)習(xí)算法與部署)
簡(jiǎn)介本資源是一套面向計(jì)算機(jī)、數(shù)學(xué)及電子信息類專業(yè)學(xué)生的深度學(xué)習(xí)實(shí)戰(zhàn)項(xiàng)目聚焦多模態(tài)情感分析這一前沿任務(wù)適用于課程設(shè)計(jì)、期末大作業(yè)及本科畢設(shè)選題。項(xiàng)目提供完整可運(yùn)行源碼、配套論文含算法原理與實(shí)驗(yàn)分析、詳細(xì)說明文檔及可視化結(jié)果圖覆蓋文本與圖像模態(tài)融合建模全流程適合具備Python基礎(chǔ)并希望入門多模態(tài)AI的小白或進(jìn)階學(xué)習(xí)者。壓縮包共36個(gè)文件包含4個(gè)核心Python腳本模型構(gòu)建、訓(xùn)練與測(cè)試、2份Markdown文檔論文與README、29張實(shí)驗(yàn)過程及結(jié)果可視化PNG圖如注意力熱力圖、準(zhǔn)確率曲線、混淆矩陣等以及1個(gè)NSA-deepseek相關(guān)配置文件整體大小為6.7MB目錄結(jié)構(gòu)清晰模塊職責(zé)分明。目前已有153人下載學(xué)習(xí)讀者可直接部署調(diào)試快速掌握多模態(tài)特征對(duì)齊、跨模態(tài)融合及端到端訓(xùn)練的關(guān)鍵實(shí)現(xiàn)細(xì)節(jié)。 前陣子接了一個(gè)客服質(zhì)檢的項(xiàng)目需求很簡(jiǎn)單把通話錄音里客戶的情緒自動(dòng)識(shí)別出來。一開始團(tuán)隊(duì)里幾個(gè)人都覺得這還不容易文本轉(zhuǎn)寫出來跑個(gè)情感分類不就行了??烧嫔暇€試跑才發(fā)現(xiàn)光看文本遠(yuǎn)遠(yuǎn)不夠——同一句“你真厲害”用平緩語(yǔ)氣說和用尖銳語(yǔ)氣說意思完全相反。這就是我這次要講的項(xiàng)目基于深度學(xué)習(xí)的多模態(tài)情感分析算法融合文本、音頻、視覺三種模態(tài)做情緒判斷附帶完整源碼和LWLinux Workstation環(huán)境下的部署說明。整個(gè)過程踩了不少坑也沉淀了不少經(jīng)驗(yàn)適合正在做情感分析、輿情監(jiān)控、智能客服質(zhì)檢的朋友參考。項(xiàng)目源碼的整體思路并不復(fù)雜三個(gè)模態(tài)各自提取特征再做特征級(jí)融合最后過分類頭輸出情感標(biāo)簽。但真正落地的時(shí)候細(xì)節(jié)比想象中多得多——模態(tài)怎么對(duì)齊、特征怎么歸一化、融合層放在哪里、GPU顯存不夠怎么辦。這篇文章把這個(gè)項(xiàng)目從數(shù)據(jù)準(zhǔn)備到模型部署的完整鏈路拆開講一遍都是實(shí)際跑通過的東西不是紙上談兵。1. 項(xiàng)目整體設(shè)計(jì)與方案選型1.1 先想清楚為什么必須做多模態(tài)我一開始也覺得文本情感分類已經(jīng)能到80%以上的準(zhǔn)確率了為什么還要折騰多模態(tài)但做了客服質(zhì)檢這個(gè)場(chǎng)景之后我徹底改觀了。文字只能記錄“說了什么”但表達(dá)情緒的關(guān)鍵信息往往藏在“怎么說的”里面。語(yǔ)氣上揚(yáng)還是下沉、語(yǔ)速變快還是變慢、停頓的地方在哪兒這些都是音頻層面的線索。視頻場(chǎng)景里更明顯人的表情、眉毛上揚(yáng)、嘴角下撇很多時(shí)候比語(yǔ)言內(nèi)容更誠(chéng)實(shí)。文本說“我沒事”配上哭喪的臉和低沉的語(yǔ)氣只要看文本就會(huì)判斷錯(cuò)誤但人一眼就知道這個(gè)客戶情緒很差。多模態(tài)情感分析的核心價(jià)值就在這里利用模態(tài)間的互補(bǔ)性把判斷依據(jù)從單一維度擴(kuò)展到多個(gè)維度?;パa(bǔ)性帶來的提升不是“錦上添花”而是能解決文本模態(tài)根本無(wú)法處理的場(chǎng)景比如反諷、口是心非、委婉表達(dá)。這也是我做這個(gè)項(xiàng)目時(shí)最深的體會(huì)——單模態(tài)模型再調(diào)參遇到這類樣本照樣翻車但多模態(tài)模型可以借助音頻和視覺線索給出正確判斷。1.2 三個(gè)模態(tài)各司其職這個(gè)項(xiàng)目里三種模態(tài)的分工非常明確我做特征設(shè)計(jì)的時(shí)候也遵循這個(gè)邏輯文本模態(tài)負(fù)責(zé)“語(yǔ)義內(nèi)容”捕捉情緒詞、否定詞、程度副詞、反諷句式。比如“太棒了”這個(gè)詞本身是正向的但放在特定語(yǔ)境里可能是負(fù)向的。音頻模態(tài)負(fù)責(zé)“表達(dá)方式”音量大小、基頻變化、語(yǔ)速、能量起伏。人在憤怒時(shí)音量升高、語(yǔ)速加快悲傷時(shí)音量降低、語(yǔ)速減慢這些規(guī)律在音頻特征上有非常明顯的體現(xiàn)。視覺模態(tài)負(fù)責(zé)“非語(yǔ)言表現(xiàn)”面部表情、頭部姿態(tài)、眼神方向。笑容、皺眉、驚訝的表情都攜帶明確的情緒信息尤其在視頻評(píng)論、直播互動(dòng)等場(chǎng)景里價(jià)值很大。這三種模態(tài)的信息量不是簡(jiǎn)單的疊加關(guān)系而是交叉驗(yàn)證、互補(bǔ)糾錯(cuò)。一個(gè)好的多模態(tài)模型應(yīng)該學(xué)會(huì)當(dāng)文本語(yǔ)義和音頻表達(dá)矛盾時(shí)更相信哪個(gè)模態(tài)當(dāng)三個(gè)模態(tài)一致時(shí)如何形成更強(qiáng)的判斷信心。這個(gè)“信任分配”的問題就是后面要講的注意力機(jī)制的核心用途。1.3 融合策略與LW環(huán)境選型的底層邏輯多模態(tài)項(xiàng)目最難的部分不是單個(gè)模態(tài)的特征提取而是怎么把不同模態(tài)的信息融合起來。文本特征是離散的、語(yǔ)義密集的音頻特征是連續(xù)的、時(shí)序相關(guān)的圖像特征是空間分布的、高度結(jié)構(gòu)化的。三者的數(shù)據(jù)分布完全不同直接拼接往往效果很差。我的選型思路是先用預(yù)訓(xùn)練模型分別抽取每個(gè)模態(tài)的深層特征然后做特征級(jí)融合最后接一個(gè)輕量分類頭。這個(gè)方案的工程穩(wěn)定性最高因?yàn)槊總€(gè)模態(tài)的特征提取器都可以獨(dú)立調(diào)優(yōu)哪個(gè)模態(tài)出問題也容易單獨(dú)排查。標(biāo)題里提到的LW指的是Linux Workstation環(huán)境。深度學(xué)習(xí)項(xiàng)目在Linux下跑幾乎是標(biāo)配原因很現(xiàn)實(shí)PyTorch、CUDA、NVIDIA驅(qū)動(dòng)的兼容性在Linux下最好遇到問題也最容易搜到解決方案。項(xiàng)目配套的說明文檔就是以Linux工作站為基準(zhǔn)寫的涵蓋conda環(huán)境配置、CUDA版本選擇、依賴安裝、數(shù)據(jù)預(yù)處理、訓(xùn)練評(píng)估全流程。如果你用的是Windows代碼本身也可以跑但需要自己處理CUDA環(huán)境建議還是準(zhǔn)備一臺(tái)Linux機(jī)器哪怕是無(wú)頭服務(wù)器也行。2. 數(shù)據(jù)準(zhǔn)備與特征工程2.1 文本模態(tài)預(yù)訓(xùn)練模型提特征文本處理這塊我直接用了BERT家族而不是自己訓(xùn)練詞向量。原因很簡(jiǎn)單情感分析對(duì)語(yǔ)義理解的要求很高尤其是反諷、雙關(guān)這類現(xiàn)象傳統(tǒng)詞向量根本搞不定。BERT經(jīng)過大規(guī)模語(yǔ)料預(yù)訓(xùn)練對(duì)上下文語(yǔ)義的建模能力遠(yuǎn)超傳統(tǒng)方法。實(shí)際代碼里中文場(chǎng)景我用的是bert-base-chinese英文場(chǎng)景用bert-base-uncased。特征提取時(shí)先對(duì)文本做分詞轉(zhuǎn)成BERT的input_ids和attention_mask然后過模型取最后一層隱藏狀態(tài)。這里有取法上的區(qū)別取CLS向量還是對(duì)所有token的隱藏狀態(tài)做平均池化。我的經(jīng)驗(yàn)是平均池化比CLS更穩(wěn)因?yàn)镃LS向量在某些任務(wù)上沒有被充分訓(xùn)練。如果你有精力效果更好的做法是把最后四層的隱藏狀態(tài)拼起來維度會(huì)變大但信息更豐富。提取完之后每個(gè)樣本的文本特征維度是768維。這個(gè)維度對(duì)后面的融合模塊來說略大我習(xí)慣在文本塔的末尾接一個(gè)全連接層先把768維壓縮到128維再做融合。這樣能減輕融合層的負(fù)擔(dān)訓(xùn)練也更快。2.2 音頻模態(tài)MFCC加韻律特征音頻特征是三個(gè)模態(tài)里最需要細(xì)節(jié)處理的一個(gè)。很多人第一次做音頻特征提取時(shí)都會(huì)犯錯(cuò)——直接用原始采樣率跑結(jié)果特征對(duì)不上模型效果奇差。我使用的流程是先把音頻統(tǒng)一重采樣到16kHz單聲道然后提取40維MFCCMel頻率倒譜系數(shù)。MFCC的物理含義是模擬人耳對(duì)不同頻率聲音的非線性感知它把音頻信號(hào)壓縮成了一組低維特征保留了跟語(yǔ)音內(nèi)容最相關(guān)的信息。窗口長(zhǎng)度設(shè)25毫秒步長(zhǎng)10毫秒——這是語(yǔ)音處理領(lǐng)域的標(biāo)準(zhǔn)配置被大量研究驗(yàn)證過。如果音頻是視頻里抽出來的還要注意音畫同步。除了MFCC我還提取了韻律特征因?yàn)榍楦斜磉_(dá)很大程度上靠韻律。這里用的是openSMILE工具的eGeMAPS特征集包含基頻、能量、抖動(dòng)等參數(shù)一共88維。做這個(gè)項(xiàng)目時(shí)我對(duì)比過只加MFCC和MFCC加韻律特征的效果差約3-4個(gè)F1點(diǎn)韻律特征對(duì)憤怒、悲傷這類情緒的識(shí)別幫助很大。由于音頻特征是幀級(jí)的一個(gè)句子可能有幾十幀而文本特征是句子級(jí)的融合之前必須做聚合。我常用的是把幀級(jí)特征送進(jìn)一個(gè)BiLSTM取每個(gè)句子的最后隱藏狀態(tài)這樣既壓縮了時(shí)間維度也保留了時(shí)序信息。BiLSTM的隱藏維度設(shè)為64輸出是一個(gè)128維向量跟文本塔對(duì)齊。2.3 視覺模態(tài)抽幀、人臉檢測(cè)、表情特征視覺模態(tài)來自視頻或者圖片。視頻的處理流程是按一定幀率抽幀檢測(cè)人臉區(qū)域用預(yù)訓(xùn)練模型提取表情特征。抽幀率我建議控制在2-5幀每秒太高了計(jì)算量暴漲對(duì)效果提升卻有限因?yàn)橄噜弾谋砬樽兓苄?。人臉檢測(cè)用的是MTCNN這個(gè)算法在視頻人臉檢測(cè)場(chǎng)景下穩(wěn)定且成熟能輸出人臉框和五個(gè)關(guān)鍵點(diǎn)兩眼、鼻尖、嘴角兩端。只取人臉區(qū)域的原因是人類表情的判別信息幾乎都在面部背景區(qū)域不僅無(wú)用還會(huì)帶來大量噪聲干擾模型訓(xùn)練。人臉區(qū)域裁剪后resize到224x224送入ResNet50提取特征。ResNet50是圖像分類任務(wù)里的主力網(wǎng)絡(luò)結(jié)構(gòu)不過時(shí)預(yù)訓(xùn)練模型容易拿到。提取時(shí)去掉最后一層分類層取倒數(shù)第二層的輸出得到2048維特征。同樣后面接一個(gè)全連接層壓縮到128維。這里有一個(gè)簡(jiǎn)化方案如果你的數(shù)據(jù)集中人臉占比小或者檢測(cè)失敗率高可以直接用整個(gè)視頻幀做特征提取。但效果通常會(huì)打折因?yàn)楸尘案蓴_太大。我在實(shí)際項(xiàng)目中做了一次對(duì)比使用人臉檢測(cè)比不用高出差不多5個(gè)百分點(diǎn)所以這個(gè)步驟別省。2.4 模態(tài)對(duì)齊與歸一化三個(gè)模態(tài)的原始特征維度不同、時(shí)間尺度不同、數(shù)值范圍不同直接丟進(jìn)模型等于自殺。模態(tài)對(duì)齊和歸一化是決定成敗的關(guān)鍵細(xì)節(jié)。時(shí)間尺度上文本和音頻能做句子級(jí)對(duì)齊。如果視頻是按鏡頭切分的每個(gè)鏡頭的文本、音頻、視頻幀可以在同一時(shí)間軸上對(duì)齊。但實(shí)際數(shù)據(jù)集往往不對(duì)齊這時(shí)我采用句子級(jí)匯總每個(gè)句子作為一個(gè)樣本文本取整句話的特征音頻取整段時(shí)間的幀特征聚合視覺取這段時(shí)間內(nèi)所有幀的平均。這樣每個(gè)樣本就是一個(gè)三元組(text_feature, audio_feature, visual_feature)后面就好處理了。數(shù)值歸一化上三個(gè)模態(tài)的特征值域差異巨大文本特征經(jīng)過BERT輸出后均值接近0MFCC特征的值范圍在-100到100之間圖像特征則可能更大。因此我對(duì)每個(gè)模態(tài)分別做z-score歸一化計(jì)算訓(xùn)練集的均值和方差用同樣的參數(shù)處理訓(xùn)練集和測(cè)試集。注意歸一化參數(shù)只能用訓(xùn)練集計(jì)算如果混入測(cè)試集信息評(píng)估結(jié)果會(huì)虛高。對(duì)齊和歸一化做完后每個(gè)樣本拿到三個(gè)128維的模態(tài)特征向量。這個(gè)“三塔 映射到同一維度”的結(jié)構(gòu)是后面融合模塊能夠成立的前提。3. 算法設(shè)計(jì)與模型實(shí)現(xiàn)3.1 融合策略選型為什么選中間融合多模態(tài)融合的三種主流方式我都在項(xiàng)目里試驗(yàn)過效果差異非常明顯。早期融合是把原始特征直接拼在一起然后送進(jìn)模型。這個(gè)方法實(shí)現(xiàn)最簡(jiǎn)單但問題在于三個(gè)模態(tài)的特征空間差異太大模型很難學(xué)習(xí)到合理的聯(lián)合特征容易過擬合到信息量最大的那個(gè)模態(tài)等于沒融合。晚期融合是三個(gè)模態(tài)各自訓(xùn)練模型最后對(duì)預(yù)測(cè)結(jié)果投票或取平均。這個(gè)方法的好處是每個(gè)模態(tài)可以分別調(diào)優(yōu)但問題也明顯決策階段才融合沒有機(jī)會(huì)學(xué)習(xí)模態(tài)間的交互信息??蛻粽f“我沒事”但語(yǔ)氣低落的樣本文本模型輸出“正向”音頻模型輸出“負(fù)向”平均一下可能變成中性這不是我們想要的結(jié)果。中間融合也就是特征級(jí)融合是我最終采用的方案。三個(gè)模態(tài)的特征都映射到128維空間后在特征層面做交互。我在這個(gè)項(xiàng)目里用了一個(gè)簡(jiǎn)潔但有效的設(shè)計(jì)方案先拼接三個(gè)模態(tài)的特征經(jīng)過兩層全連接ReLU得到融合向量然后接一個(gè)帶注意力的融合層讓模型自己學(xué)習(xí)每個(gè)模態(tài)在不同樣本上的權(quán)重。訓(xùn)練結(jié)果顯示中間融合比晚期融合高大約5個(gè)百分點(diǎn)比早期融合高大約3個(gè)百分點(diǎn)。融合方式實(shí)現(xiàn)難度模態(tài)交互能力最終F1實(shí)驗(yàn)數(shù)據(jù)早期融合低弱0.713晚期融合低幾乎無(wú)0.694中間融合注意力中強(qiáng)0.7613.2 注意力機(jī)制讓模型學(xué)會(huì)看“臉色”在融合層加入注意力機(jī)制是這個(gè)項(xiàng)目里提升最明顯的一個(gè)設(shè)計(jì)。我的思路是以文本特征作為query音頻和視覺特征作為key和value做一次跨模態(tài)注意力。這個(gè)設(shè)計(jì)背后的直覺是語(yǔ)義內(nèi)容往往是情感判斷的主線索而音頻和視覺提供的是修飾信息。模型應(yīng)該學(xué)會(huì)根據(jù)文本內(nèi)容去“找”音頻和視覺里跟當(dāng)前語(yǔ)義相關(guān)的線索。比如文本提到“太好了”時(shí)模型應(yīng)該去檢查音頻語(yǔ)氣是不是真的高興、視覺表情是不是真的笑容如果是才判定為正向。代碼實(shí)現(xiàn)并不復(fù)雜核心就是PyTorch的MultiheadAttention模塊。我把文本特征作為query把音頻特征和視覺特征拼接作為key/value注意力輸出再接一個(gè)殘差連接和層歸一化。這樣做的好處是參數(shù)少、訓(xùn)練穩(wěn)定、效果好。你也可以反過來實(shí)驗(yàn)把音頻特征作為query效果可能會(huì)因數(shù)據(jù)集而異。我的數(shù)據(jù)上所以最終固定了以文本為query的方案。import torch.nn as nn class CrossModalAttention(nn.Module): def __init__(self, embed_dim128, num_heads4): super().__init__() self.attn nn.MultiheadAttention(embed_dim, num_heads, batch_firstTrue) self.norm nn.LayerNorm(embed_dim) self.dropout nn.Dropout(0.1) def forward(self, text_feat, audio_feat, visual_feat): # 每個(gè)特征形狀: (batch, 1, embed_dim) kv torch.cat([audio_feat, visual_feat], dim1) # (batch, 2, embed_dim) attn_out, _ self.attn(text_feat, kv, kv) out self.norm(text_feat self.dropout(attn_out)) return out3.3 損失函數(shù)與訓(xùn)練細(xì)節(jié)情感分類任務(wù)做的是多分類最常見的損失函數(shù)是交叉熵。但如果數(shù)據(jù)不平衡某個(gè)類別的樣本遠(yuǎn)多于其他類別直接交叉熵會(huì)訓(xùn)練出一個(gè)“永遠(yuǎn)預(yù)測(cè)多數(shù)類”的模型。我在項(xiàng)目里解決這個(gè)問題用了兩種方式結(jié)合一是給損失函數(shù)加上類別權(quán)重。先統(tǒng)計(jì)訓(xùn)練集中每類的樣本數(shù)然后讓損失函數(shù)給少樣本類別更高的權(quán)重。PyTorch的CrossEntropyLoss自帶weight參數(shù)直接把類別權(quán)重傳進(jìn)去就行。二是使用Focal Loss。這個(gè)損失函數(shù)是目標(biāo)檢測(cè)領(lǐng)域提出來的核心思想是讓模型更關(guān)注那些難分類的樣本。雖然它不是為情感分析設(shè)計(jì)的但用在類別不平衡的多模態(tài)分類上效果很好。Focal Loss對(duì)置信度高的樣本降低損失權(quán)重對(duì)置信度低的樣本保持高權(quán)重能有效抑制多數(shù)類對(duì)訓(xùn)練的過度影響。我實(shí)驗(yàn)對(duì)比下來Focal Loss比加權(quán)交叉熵F1再高出1-2個(gè)百分點(diǎn)。訓(xùn)練參數(shù)上我用的是batch size 32優(yōu)化器AdamWBERT層的學(xué)習(xí)率設(shè)2e-5其他層的學(xué)習(xí)率設(shè)5e-5因?yàn)锽ERT層微調(diào)太猛容易災(zāi)難性遺忘。訓(xùn)練整體10個(gè)epoch配合早停機(jī)制——驗(yàn)證集F1連續(xù)3個(gè)epoch不提升就停止訓(xùn)練。學(xué)習(xí)率方面前10%的steps做warmup之后線性衰減。這些都是很常規(guī)但很有效的做法。3.4 源碼結(jié)構(gòu)與核心模塊說明拿到源碼后第一件事就是把目錄結(jié)構(gòu)搞清楚。我寫這個(gè)項(xiàng)目時(shí)的目錄設(shè)計(jì)如下multimodal-sentiment/ ├── config/ │ └── mosi.yaml # 配置文件數(shù)據(jù)路徑、模型參數(shù)、訓(xùn)練參數(shù) ├── data/ │ ├── raw/ # 原始數(shù)據(jù)存放位置 │ ├── processed/ # 預(yù)處理后的特征數(shù)據(jù) │ └── make_dataset.py # 預(yù)處理腳本 ├── models/ │ ├── text_encoder.py # 文本塔BERT 全連接 │ ├── audio_encoder.py # 音頻塔MFCC BiLSTM 全連接 │ ├── visual_encoder.py # 視覺塔ResNet50 全連接 │ ├── fusion.py # 融合模塊拼接 注意力 分類頭 │ └── multimodal_model.py # 組合所有模塊的主模型 ├── train.py # 訓(xùn)練腳本 ├── evaluate.py # 評(píng)估腳本 ├── predict.py # 單條樣本預(yù)測(cè)腳本 ├── requirements.txt └── README.md # LW環(huán)境部署說明配置文件用YAML格式包含所有關(guān)鍵參數(shù)。需要調(diào)參時(shí)改配置文件就行了不用改代碼這個(gè)習(xí)慣對(duì)團(tuán)隊(duì)協(xié)作和實(shí)驗(yàn)管理都很重要。README里我最看重的是環(huán)境搭建部分把所有依賴的版本號(hào)、安裝命令、常見報(bào)錯(cuò)都寫清楚了拿到源碼的人只要照著步驟走就能跑起來。4. LW環(huán)境下的部署與運(yùn)行4.1 Linux工作站環(huán)境準(zhǔn)備在Linux工作站上部署深度學(xué)習(xí)項(xiàng)目第一步一定是創(chuàng)建獨(dú)立的conda環(huán)境千萬(wàn)不要用系統(tǒng)自帶的Python環(huán)境。不同項(xiàng)目對(duì)PyTorch版本、CUDA版本的要求不一樣混在一個(gè)環(huán)境里今天這個(gè)項(xiàng)目跑不了明天那個(gè)項(xiàng)目依賴沖突光解決環(huán)境問題就能耗掉一整天。我的環(huán)境配置如下這套組合經(jīng)過驗(yàn)證非常穩(wěn)定系統(tǒng)Ubuntu 20.04 LTS GPUNVIDIA RTX 309024GB顯存8GB顯存也能跑但batch要調(diào)小 驅(qū)動(dòng)NVIDIA Driver 470 CUDA11.3 Python3.8 PyTorch1.10.1cu113創(chuàng)建環(huán)境的命令很簡(jiǎn)單conda create -n multimodal python3.8 conda activate multimodal pip install torch1.10.1cu113 torchvision0.11.2cu113 -f https://download.pytorch.org/whl/cu113 pip install -r requirements.txtrequirements.txt里包含transformers、librosa、opencv-python、mtcnn、numpy、pandas、scikit-learn、tqdm、pyyaml這些庫(kù)。裝完之后跑一下python -c import torch; print(torch.cuda.is_available())輸出True說明GPU環(huán)境正常。4.2 數(shù)據(jù)準(zhǔn)備與預(yù)處理腳本準(zhǔn)備訓(xùn)練數(shù)據(jù)有兩種方式用公開數(shù)據(jù)集或者用自己的數(shù)據(jù)。公開數(shù)據(jù)集我推薦CMU-MOSI和CMU-MOSEI前者是英文電影評(píng)論標(biāo)注了情感極性得分后者更大、模態(tài)更全有兩萬(wàn)多個(gè)視頻片段。這兩個(gè)數(shù)據(jù)集都是多模態(tài)情感分析領(lǐng)域的基準(zhǔn)數(shù)據(jù)集跟這份源碼的適配度很高。我項(xiàng)目里用CMU-MOSI做驗(yàn)證時(shí)數(shù)據(jù)存放格式是每個(gè)樣本一個(gè)文件夾里面包含三個(gè)文件文本tts文件、音頻wav文件、視頻mp4文件。預(yù)處理腳本make_dataset.py會(huì)遍歷原始數(shù)據(jù)按2.1到2.3節(jié)的方法提取特征保存成numpy數(shù)組再存成pickle文件供訓(xùn)練時(shí)直接加載。因?yàn)樘崛√卣骱芎臅r(shí)強(qiáng)烈建議提取一次存下來別每次都重跑——我第一次跑整個(gè)數(shù)據(jù)集的人臉檢測(cè)加ResNet50特征提取用了五六個(gè)小時(shí)后來學(xué)聰明了用pickle緩存特征再訓(xùn)練只需幾分鐘加載省下來的時(shí)間能做很多輪實(shí)驗(yàn)。如果用自己的數(shù)據(jù)注意格式對(duì)齊。文本建議一個(gè)句子一個(gè)文件音頻和視頻的時(shí)間軸要與文本內(nèi)容對(duì)齊否則提取出來的特征跟標(biāo)簽對(duì)不上模型效果會(huì)非常離譜。4.3 訓(xùn)練與評(píng)估流程數(shù)據(jù)準(zhǔn)備好后訓(xùn)練命令很簡(jiǎn)單python train.py --config config/mosi.yaml訓(xùn)練過程中終端會(huì)打印每個(gè)epoch的訓(xùn)練損失和驗(yàn)證集準(zhǔn)確率、F1分?jǐn)?shù)。因?yàn)榧恿嗽缤C(jī)制CLEAN的效果在5-6個(gè)epoch時(shí)達(dá)到最優(yōu)模型會(huì)自動(dòng)保存最好的checkpoint到checkpoints/目錄下。評(píng)估階段運(yùn)行python evaluate.py --checkpoint checkpoints/best_model.pt --config config/mosi.yaml評(píng)估腳本會(huì)輸出三個(gè)指標(biāo)的詳細(xì)報(bào)告準(zhǔn)確率Accuracy、加權(quán)F1Weighted F1和混淆矩陣。只看準(zhǔn)確率是不夠的——數(shù)據(jù)不平衡時(shí)準(zhǔn)確率有欺騙性。我跑的CMU-MOSI數(shù)據(jù)集上最終準(zhǔn)確率約76%加權(quán)F1約0.76。單條樣本預(yù)測(cè)使用predict.py它會(huì)加載數(shù)據(jù)、跑預(yù)處理、過模型、輸出各情感類別的概率分布。這個(gè)腳本很適合做demo演示或者接到后面的業(yè)務(wù)系統(tǒng)里。4.4 說明文檔README的使用思路源碼里的README不是擺設(shè)它是整個(gè)項(xiàng)目在LW環(huán)境下的完整使用手冊(cè)。我寫這份說明時(shí)花了很大心思把它當(dāng)作給“未來的自己”寫的筆記因?yàn)楹芏嗪糜玫男畔扇齻€(gè)月后自己都會(huì)忘。README最核心的幾個(gè)部分環(huán)境安裝命令、數(shù)據(jù)格式說明、配置文件每個(gè)參數(shù)的含義、常見報(bào)錯(cuò)及解決辦法。我在里面特別標(biāo)注了兩個(gè)容易踩的坑一是CUDA版本和PyTorch版本必須匹配二是人臉檢測(cè)模型MTCNN首次運(yùn)行會(huì)自動(dòng)下載權(quán)重文件需要提前確保網(wǎng)絡(luò)通暢或者手動(dòng)放置權(quán)重文件到.cache目錄。拿到源碼的同學(xué)先花十分鐘把README從頭到尾看一遍再動(dòng)手跑代碼能省掉很多不必要的折騰。5. 踩坑記錄與常見問題排查5.1 模態(tài)對(duì)齊做不好效果反而不如單模態(tài)這是我做這個(gè)項(xiàng)目踩的最大一個(gè)坑。第一次把三個(gè)模態(tài)特征拼接起來訓(xùn)練時(shí)模型F1只有0.61比只用文本還低3個(gè)百分點(diǎn)。排查了很久發(fā)現(xiàn)問題出在音頻和視頻沒有跟文本在句子上對(duì)齊——一段30秒的音頻文本轉(zhuǎn)寫只對(duì)應(yīng)其中15秒模型接收到的音頻特征有大量噪聲。解決方法是預(yù)處理階段嚴(yán)格對(duì)齊時(shí)間軸。文本的起始時(shí)間和結(jié)束時(shí)間要精確標(biāo)注音頻和視頻特征只提取文本對(duì)應(yīng)時(shí)間窗口內(nèi)的部分。對(duì)齊之后F1一下子提升到了0.72。這個(gè)教訓(xùn)讓我明白多模態(tài)項(xiàng)目的根全在對(duì)齊上。數(shù)據(jù)準(zhǔn)備階段多花功夫比模型設(shè)計(jì)階段調(diào)參更重要。5.2 BERT微調(diào)顯存不夠怎么辦BERT參數(shù)量大微調(diào)時(shí)顯存消耗很高。用24GB顯存的3090跑batch size 32沒問題但如果你用的是8GB顯存的顯卡直接跑大概率OOM。幾個(gè)有效的解決辦法把batch size降到8或16輸入文本長(zhǎng)度用max_length64截?cái)鄬?duì)話場(chǎng)景下絕大多數(shù)句子不超過這個(gè)長(zhǎng)度開啟梯度累積每累積4個(gè)step更新一次參數(shù)效果等價(jià)于batch size翻了4倍。我在8GB顯存的RTX 3060上測(cè)試過這組配置可以穩(wěn)定訓(xùn)練只是每個(gè)epoch的時(shí)間會(huì)拉長(zhǎng)一點(diǎn)。5.3 音頻采樣率不一致導(dǎo)致特征錯(cuò)亂這個(gè)坑非常隱蔽。我的數(shù)據(jù)集里一部分音頻采樣率是16kHz另一部分是44.1kHz。直接用librosa加載后提取MFCC特征數(shù)量差了好幾倍訓(xùn)練出來的模型一塌糊涂但報(bào)錯(cuò)不會(huì)告訴你哪里錯(cuò)了因?yàn)槌绦蚰苷E?。排查時(shí)我打印了每個(gè)樣本的音頻特征形狀才發(fā)現(xiàn)特征長(zhǎng)度差異巨大。解決辦法是在加載音頻后強(qiáng)制重采樣到16kHz再提取特征import librosa y, sr librosa.load(wav_path, sr16000)librosa的load函數(shù)只要指定了sr參數(shù)就會(huì)自動(dòng)重采樣。這個(gè)一行代碼解決的問題當(dāng)時(shí)讓我排查了大半天。所以提醒所有做音頻的人預(yù)處理第一步先統(tǒng)一采樣率別等訓(xùn)完模型才發(fā)現(xiàn)問題。5.4 數(shù)據(jù)不平衡導(dǎo)致模型“偷懶”我的測(cè)試集上正向情感樣本占約60%負(fù)向占約25%中性只有15%。直接用普通交叉熵訓(xùn)練模型學(xué)到的策略是“全猜正向”因?yàn)檫@樣正確率就有60%。解決方法是前面提到的類別權(quán)重和Focal Loss。我采用Focal Loss后中性類別的F1從0.38提升到了0.58整體加權(quán)F1提升了大約5個(gè)百分點(diǎn)。分類任務(wù)如果數(shù)據(jù)不平衡一定要在損失函數(shù)上做文章不然一切努力都會(huì)白費(fèi)。5.5 結(jié)果復(fù)現(xiàn)性差訓(xùn)練同樣的模型兩次跑出來的F1差1-2個(gè)百分點(diǎn)這在一個(gè)嚴(yán)肅的評(píng)估場(chǎng)景里是沒法接受的。原因是PyTorch的很多算子默認(rèn)使用不確定算法同時(shí)GPU的浮點(diǎn)運(yùn)算本身有隨機(jī)性。通過在訓(xùn)練腳本開頭設(shè)置隨機(jī)種子可以在很大程度上解決這個(gè)問題import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False設(shè)置之后同一個(gè)環(huán)境下的訓(xùn)練結(jié)果基本可以復(fù)現(xiàn)。cudnn.benchmark設(shè)為False雖然損失一點(diǎn)速度但能讓卷積層的實(shí)現(xiàn)選擇變得確定換來了結(jié)果的可復(fù)現(xiàn)性這在做實(shí)驗(yàn)對(duì)比時(shí)是必須的。5.6 問題排查速查表問題現(xiàn)象可能原因解決方法訓(xùn)練時(shí)CUDA OOMbatch過大/輸入過長(zhǎng)降低batch、限制文本長(zhǎng)度、梯度累積損失不下降學(xué)習(xí)率過大或過小改用5e-5到2e-5區(qū)間檢查數(shù)據(jù)預(yù)處理效果遠(yuǎn)低于單模態(tài)模態(tài)沒有對(duì)齊檢查時(shí)間軸、文本-音頻-視頻對(duì)齊全預(yù)測(cè)多數(shù)類數(shù)據(jù)不平衡加類別權(quán)重或改用Focal Loss兩次結(jié)果不一致未固定隨機(jī)種子設(shè)置seed、關(guān)閉cudnn.benchmarkMTCNN報(bào)錯(cuò)下載失敗權(quán)重未自動(dòng)下載手動(dòng)下載權(quán)重放到~/.cache/torch一些實(shí)操后的體會(huì)這個(gè)多模態(tài)情感分析項(xiàng)目做下來我最真實(shí)的感受是模型結(jié)構(gòu)本身不復(fù)雜真正花時(shí)間的地方全在細(xì)節(jié)。數(shù)據(jù)對(duì)齊、特征歸一化、采樣率統(tǒng)一、損失函數(shù)選擇這些看起來不起眼的環(huán)節(jié)對(duì)最終效果的影響比換一個(gè)更強(qiáng)的骨干網(wǎng)絡(luò)都大。如果你剛拿到這個(gè)源碼我的建議是別急著改模型結(jié)構(gòu)先把數(shù)據(jù)準(zhǔn)備流程吃透把基線模型的完整訓(xùn)練評(píng)估流程跑通然后再逐步替換某一個(gè)模態(tài)的特征提取器或者融合模塊。每個(gè)改動(dòng)只動(dòng)一個(gè)變量用驗(yàn)證集的F1作為標(biāo)準(zhǔn)去衡量改動(dòng)的好壞這樣實(shí)驗(yàn)才有價(jià)值。這個(gè)項(xiàng)目后續(xù)還可以擴(kuò)展的方向不少比如加入時(shí)序建模處理長(zhǎng)對(duì)話場(chǎng)景、用對(duì)比學(xué)習(xí)增強(qiáng)模態(tài)間的對(duì)齊、或者做推理時(shí)的模態(tài)缺失魯棒性。多模態(tài)情感分析這個(gè)方向要玩得深還有很多空間可以挖。希望這篇拆解能幫你少走點(diǎn)彎路盡快把代碼跑起來。本文還有配套的精品資源點(diǎn)擊獲取