融合情感分析:從文本到四類情感的Python分層方案)
簡(jiǎn)介一套基于Python與TensorFlow實(shí)現(xiàn)的多模態(tài)情感分析項(xiàng)目資源輸入涵蓋文本、語(yǔ)音、圖片和視頻面向畢業(yè)設(shè)計(jì)、課程設(shè)計(jì)與項(xiàng)目開發(fā)。源碼已通過(guò)嚴(yán)格測(cè)試可放心參考并二次擴(kuò)展。項(xiàng)目采用分層融合思路先提取單模態(tài)特征再逐步構(gòu)建雙模態(tài)、三模態(tài)組合特征最后用softmax層輸出喜、怒、哀、其他四類情感比常見的正負(fù)中性分類更適合用戶分層與目標(biāo)分析。壓縮包共21個(gè)文件主要包括pickle數(shù)據(jù)文件、py源碼、含iemocap數(shù)據(jù)集的zip、txt說(shuō)明、pdf文檔、md說(shuō)明及結(jié)果示意圖總大小約58.48MB目錄結(jié)構(gòu)清晰便于按模塊查閱。環(huán)境要求Python 3.6和TensorFlow 1.7支持CPU或GPU。目前已有六百八十二人學(xué)習(xí)使用對(duì)于想理解多模態(tài)情感識(shí)別完整流程的開發(fā)者是一套可直接落地的實(shí)踐資料。1. 多模態(tài)融合情感分析從文本單模態(tài)到四類情感的完整 Python 方案多模態(tài)融合情感分析這個(gè) Python 項(xiàng)目解決的是純文本情感分析的盲區(qū)。彈幕里一句“這也太厲害了吧”配上白眼表情包和嘲諷語(yǔ)氣只看文本的模型十有八九判成喜實(shí)際上人家在罵。項(xiàng)目輸入同時(shí)覆蓋文本含 emojis、語(yǔ)音、圖片和視頻用 TensorFlow 搭了一套從單模態(tài)向量逐步融合到雙模態(tài)、三模態(tài)的分層結(jié)構(gòu)最后通過(guò) softmax 輸出喜、怒、哀、其他四類情感。自帶源碼、項(xiàng)目文檔和 MOSI/IEMOCAP 數(shù)據(jù)集畢業(yè)設(shè)計(jì)、課程設(shè)計(jì)都能直接接跑通就是一份能演示、能寫進(jìn)論文的完整基線。2. 項(xiàng)目結(jié)構(gòu)與融合原理單模態(tài)向量到三模態(tài)向量的分層設(shè)計(jì)2.1 為什么先做單模態(tài)編碼而不是直接拼特征多模態(tài)數(shù)據(jù)最麻煩的地方是三個(gè)模態(tài)根本不在同一個(gè)度量空間里。文本特征來(lái)自詞向量或 embedding維度幾百語(yǔ)音特征來(lái)自 openSMILE 這類工具抽出的聲學(xué)參數(shù)維度幾十到上千不等圖片和視頻特征過(guò)完 CNN動(dòng)輒上千維。如果直接把三段原始特征 concat 起來(lái)喂給全連接層維度高的模態(tài)會(huì)主導(dǎo)梯度方向模型基本學(xué)不到弱模態(tài)的信息。這是多模態(tài)項(xiàng)目里最常見的“單模態(tài)近視”也是很多人復(fù)現(xiàn)多模態(tài)論文發(fā)現(xiàn)準(zhǔn)確率反而不如單模態(tài)的原因。項(xiàng)目采用的分層方案是先給每個(gè)模態(tài)單獨(dú)做一層編碼把不同維度的原始特征統(tǒng)一映射到相同的 hidden_dim再做兩兩組合的雙模態(tài)融合最后做三模態(tài)融合接 softmax。每一步都有獨(dú)立的激活函數(shù)和參數(shù)相當(dāng)于先讓模型分別理解“這句話在說(shuō)什么”“這個(gè)語(yǔ)氣什么情緒”“這個(gè)畫面什么氛圍”再學(xué)三者之間的互補(bǔ)關(guān)系。文本里的 emojis 也被當(dāng)作語(yǔ)義信號(hào)保留在向量里不需要單獨(dú)清洗掉這一點(diǎn)在預(yù)處理腳本里處理得很干凈。我一般會(huì)拿這種分層結(jié)構(gòu)和直接 concat 做對(duì)比實(shí)驗(yàn)后者準(zhǔn)確率通常低 510 個(gè)點(diǎn)。原因是直接拼接時(shí)模型要同時(shí)學(xué)模態(tài)內(nèi)表示和模態(tài)間關(guān)系優(yōu)化難度大分層結(jié)構(gòu)把任務(wù)拆成兩段每段都更容易收斂。它屬于 early fusion 和 late fusion 之間的折中——既不是把原始特征生拼也不是三個(gè)模型最后投票而是在中間層逐級(jí)對(duì)齊。2.2 雙模態(tài)到三模態(tài)融合路徑與向量對(duì)齊雙模態(tài)層有三條路徑文本語(yǔ)音、文本視頻、語(yǔ)音視頻三條路徑的輸出再接三模態(tài)層。為什么不把三條并成一條不同模態(tài)對(duì)的互補(bǔ)性不一樣文本和語(yǔ)音都攜帶語(yǔ)言信息但文本是字面內(nèi)容、語(yǔ)音是語(yǔ)調(diào)情緒兩者結(jié)合能糾正“字面客氣但語(yǔ)氣輕蔑”的樣本語(yǔ)音和視頻結(jié)合能捕捉說(shuō)話人表情與語(yǔ)調(diào)的一致程度比如皺著眉說(shuō)“沒事”和笑著說(shuō)的情感完全不同。對(duì)齊指的是把每個(gè)模態(tài)的向量統(tǒng)一到相同維度再參與拼接。實(shí)現(xiàn)里編碼層都輸出 hidden_dim默認(rèn) 128 維雙模態(tài)拼接后是 256 維經(jīng)過(guò)一層 relu 全連接保持 256 維三模態(tài)把三條雙模態(tài)路徑拼起來(lái)是 768 維再過(guò)一層全連接壓到 512 維附近。逐層壓縮的設(shè)計(jì)避免了特征維度爆炸也讓最終分類層只面對(duì)一個(gè)緊湊表示。hidden_dim 不是越大越好IEMOCAP 這類幾千條樣本的數(shù)據(jù)集128 已經(jīng)是比較穩(wěn)的起點(diǎn)。2.3 四分類標(biāo)簽設(shè)計(jì)喜、怒、哀、其他常見文本情感分析是三分類正向、負(fù)向、中性。項(xiàng)目刻意改成喜、怒、哀、其他四類項(xiàng)目文檔里寫得很直接為了用戶分層和目標(biāo)用戶分析。運(yùn)營(yíng)場(chǎng)景里把“負(fù)向”再拆成“怒”和“哀”非常實(shí)用——憤怒用戶需要安撫和退款引導(dǎo)悲傷用戶需要共情和陪伴話術(shù)完全兩樣只分正負(fù)根本不夠用。標(biāo)簽來(lái)自數(shù)據(jù)集自帶標(biāo)注。IEMOCAP 原始是六類情緒項(xiàng)目給了 6way 和 4way 兩份 pickleMOSI 原始是情感強(qiáng)度回歸值2way pickle 把它二值化成正負(fù)。4way 的常見映射關(guān)系大致如下IEMOCAP 原始情緒映射到四分類happy、excited喜angry、frustrated怒sad哀neutral 及其他其他訓(xùn)練時(shí)不需要手動(dòng)做這個(gè)映射pickle 里的 label 數(shù)組已經(jīng)是映射后的整數(shù)索引03 對(duì)應(yīng)四類。壓縮包里的文件按下面這份清單對(duì)照檢查文件作用model.py分層融合模型定義run.py訓(xùn)練、驗(yàn)證、推理入口data_prep.py數(shù)據(jù)加載、切分、歸一化create_data.py從原始數(shù)據(jù)構(gòu)造特征 pickleunimodal_mosi_2way.pickleMOSI 單模態(tài)特征二分類標(biāo)簽unimodal_iemocap_4way.pickleIEMOCAP 單模態(tài)特征四分類標(biāo)簽unimodal_iemocap_6way.pickleIEMOCAP 單模態(tài)特征六分類標(biāo)簽iemocap-data.zip原始 IEMOCAP 數(shù)據(jù)Canonical code.pdf項(xiàng)目文檔與代碼說(shuō)明README.md環(huán)境部署與運(yùn)行說(shuō)明result.png作者跑出的訓(xùn)練結(jié)果截圖新建 文本文檔.txt作者備注草稿可忽略3. 數(shù)據(jù)準(zhǔn)備MOSI、IEMOCAP 與三份 pickle 文件怎么讀3.1 兩份數(shù)據(jù)集各自的形態(tài)和適用場(chǎng)景MOSICMU 多模態(tài)觀點(diǎn)情感數(shù)據(jù)集收錄的是 YouTube 影評(píng)視頻片段發(fā)言者邊說(shuō)邊有畫面文本由字幕對(duì)齊是學(xué)術(shù)圈多模態(tài)情感分析最常用的公開集之一。它的情感自帶強(qiáng)度原本是回歸任務(wù)范圍在 [-3, 3]二值化后轉(zhuǎn)成正負(fù)二分類樣本量適中、類別干凈適合用來(lái)快速驗(yàn)證模型能不能跑通。IEMOCAP 是交互式情感動(dòng)態(tài)捕捉數(shù)據(jù)集由演員按腳本表演對(duì)話錄制時(shí)長(zhǎng)約 12 小時(shí)情緒標(biāo)注比 MOSI 細(xì)帶 angry、happy、sad、neutral、excited、frustrated 六類。如果你的畢業(yè)設(shè)計(jì)要強(qiáng)調(diào)細(xì)粒度情感分析主線用 IEMOCAP 的 4way 那份更有說(shuō)服力如果只是做系統(tǒng)演示MOSI 2way 一個(gè) epoch 跑得飛快演示過(guò)程不冷場(chǎng)。3.2 pickle 文件解析讀取代碼與數(shù)據(jù)結(jié)構(gòu)項(xiàng)目把三個(gè)模態(tài)的特征預(yù)先提取好存成了 pickle。拿到文件第一件事不是跑訓(xùn)練而是先讀進(jìn)來(lái)看結(jié)構(gòu)確認(rèn) key 名和維度再改 run.py 里的對(duì)應(yīng)參數(shù)。下面這段代碼可以做快速普查import pickle import numpy as np def load_unimodal_pickle(path): # 關(guān)鍵老 pickle 是 Python 2 生成的必須指定 latin1 編碼 with open(path, rb) as f: data pickle.load(f, encodinglatin1) return data data load_unimodal_pickle(unimodal_mosi_2way.pickle) print(頂層類型:, type(data)) if isinstance(data, dict): for key, val in data.items(): if hasattr(val, shape): print(key, shape:, val.shape, dtype:, val.dtype) else: print(key, type(val))這段代碼的邏輯分兩塊pickle.load 的 encoding 參數(shù)解決 Python 2 序列化文件的兼容問題很多人卡在這一步后面的循環(huán)按字典逐項(xiàng)打印 key 和 shape是確認(rèn)特征維度最快的辦法。常見的結(jié)構(gòu)是 {text: ..., audio: ..., video: ..., label: ...}也可能按 {train: ..., valid: ..., test: ...} 分層組織具體以 print 結(jié)果為準(zhǔn)。提示如果打印出來(lái)沒有 label 這個(gè) key先列出所有 key 找找常見命名還有 labels 或 y改一下腳本里的字段名即可。注意 label 的取值。2way 的 label 一般是 0/14way 是 036way 是 05。如果 run.py 里寫死 num_classes4 卻拿 6way 那份去訓(xùn)會(huì)報(bào)維度不匹配或準(zhǔn)確率異常低這是我見過(guò)最多的用法錯(cuò)誤。3.3 create_data.py 與 data_prep.py誰(shuí)負(fù)責(zé)什么create_data.py 負(fù)責(zé)從原始數(shù)據(jù)構(gòu)造特征 pickle輸入是 iemocap-data.zip 這類原始錄制數(shù)據(jù)輸出是上面那種特征字典。常見做法是文本走預(yù)訓(xùn)練詞向量或句向量語(yǔ)音用 openSMILE 提聲學(xué)特征視頻幀過(guò)預(yù)訓(xùn)練 CNN 取全連接層輸出。工程上這一步最耗時(shí)所以作者把特征提前存成 pickle 發(fā)出來(lái)省掉重復(fù)抽特征的一兩個(gè)小時(shí)。data_prep.py 負(fù)責(zé)把 pickle 里的特征變成模型能直接吃的數(shù)據(jù)劃分訓(xùn)練/驗(yàn)證/測(cè)試集、歸一化、把各模態(tài)維度對(duì)齊。切分有個(gè)容易踩的坑——三個(gè)模態(tài)來(lái)自同一條樣本必須按下標(biāo)同步切不能分別隨機(jī)打亂。代碼寫法如下from sklearn.model_selection import train_test_split def split_multimodal(text, audio, video, labels, test_ratio0.1, valid_ratio0.1): # 用同一個(gè) random_state 和同一組下標(biāo)同步切四個(gè)數(shù)組 idx np.arange(len(labels)) train_idx, temp_idx train_test_split( idx, test_sizetest_ratio valid_ratio, stratifylabels, random_state42) valid_idx, test_idx train_test_split( temp_idx, test_sizevalid_ratio / (test_ratio valid_ratio), stratifylabels[temp_idx], random_state42) def pick(arr, ix): return arr[ix] return (pick(text, train_idx), pick(text, valid_idx), pick(text, test_idx), pick(audio, train_idx), pick(audio, valid_idx), pick(audio, test_idx), pick(video, train_idx), pick(video, valid_idx), pick(video, test_idx), pick(labels, train_idx), pick(labels, valid_idx), pick(labels, test_idx))train_test_split 的 stratify 參數(shù)保證訓(xùn)練/驗(yàn)證/測(cè)試?yán)锩款悩颖颈壤恢虑楦袛?shù)據(jù)通常類不均衡這個(gè)參數(shù)能避免“驗(yàn)證集里全是多數(shù)類”的假象。random_state 固定成 42保證每次跑復(fù)現(xiàn)一致答辯時(shí)被問“實(shí)驗(yàn)可復(fù)現(xiàn)嗎”直接報(bào)種子。先切出 temp 再二次切分是為了分別控制驗(yàn)證集和測(cè)試集比例常見配比是 8:1:1 或 6:2:2。4. 模型與訓(xùn)練model.py 與 run.py 的代碼走讀和參數(shù)調(diào)優(yōu)4.1 model.py分層融合模型的核心實(shí)現(xiàn)model.py 是這套項(xiàng)目的主干接收三個(gè)模態(tài)的輸入占位符依次經(jīng)過(guò)單模態(tài)編碼、雙模態(tài)融合、三模態(tài)融合輸出 logits 和 softmax 概率。一個(gè)可用的實(shí)現(xiàn)大致如下import tensorflow as tf def fused_model(text_feat, audio_feat, video_feat, hidden_dim128, num_classes4): # 單模態(tài)編碼三個(gè)模態(tài)統(tǒng)一映射到 hidden_dim with tf.variable_scope(unimodal): t tf.layers.dense(text_feat, hidden_dim, activationtf.nn.relu, nametext_enc) a tf.layers.dense(audio_feat, hidden_dim, activationtf.nn.relu, nameaudio_enc) v tf.layers.dense(video_feat, hidden_dim, activationtf.nn.relu, namevideo_enc) # 雙模態(tài)融合兩兩拼接后過(guò)一層全連接 with tf.variable_scope(bimodal): ta tf.concat([t, a], axis1) ta tf.layers.dense(ta, hidden_dim * 2, activationtf.nn.relu, nameta_fc) tv tf.concat([t, v], axis1) tv tf.layers.dense(tv, hidden_dim * 2, activationtf.nn.relu, nametv_fc) av tf.concat([a, v], axis1) av tf.layers.dense(av, hidden_dim * 2, activationtf.nn.relu, nameav_fc) # 三模態(tài)融合把三條雙模態(tài)路徑拼起來(lái) with tf.variable_scope(trimodal): tri tf.concat([ta, tv, av], axis1) tri tf.layers.dense(tri, hidden_dim * 4, activationtf.nn.relu, nametri_fc) logits tf.layers.dense(tri, num_classes, namelogits) return tf.nn.softmax(logits), logits邏輯說(shuō)明單模態(tài)層用三個(gè)獨(dú)立的 variable_scope防止不同模態(tài)的 Dense 權(quán)重互相覆蓋雙模態(tài)層輸出 hidden_dim2因?yàn)閮蓚€(gè) 128 維向量拼接后是 256 維容量不縮水三模態(tài)層輸出 hidden_dim4 同理。最后一層不接激活函數(shù)直接出 logitssoftmax 交給損失函數(shù)內(nèi)部處理數(shù)值上更穩(wěn)定也比手動(dòng)算 softmax 再算 cross entropy 少一次中間精度損失。參數(shù)說(shuō)明hidden_dim 影響模型容量幾千條樣本用 128 足夠數(shù)據(jù)量上十萬(wàn)可以調(diào)到 256num_classes 必須和 pickle 的 label 上界對(duì)齊用 4way 就傳 4用 6way 就傳 6。另一個(gè)容易忽略的點(diǎn)是 relu 之后可以加 dropout訓(xùn)練時(shí) drop 0.5、預(yù)測(cè)時(shí)關(guān)掉對(duì)抑制過(guò)擬合很有效調(diào)參階段建議加上。提示如果你裝了 TensorFlow 1.15 而不是 1.7上面的代碼無(wú)需改動(dòng)兩個(gè)版本對(duì) tf.layers 和 tf.variable_scope 的支持一致。4.2 run.py訓(xùn)練循環(huán)與關(guān)鍵超參數(shù)run.py 負(fù)責(zé)把 model.py 定義的圖真正跑起來(lái)核心是占位符、損失函數(shù)、優(yōu)化器和會(huì)話循環(huán)text_ph tf.placeholder(tf.float32, [None, TEXT_DIM], nametext) audio_ph tf.placeholder(tf.float32, [None, AUDIO_DIM], nameaudio) video_ph tf.placeholder(tf.float32, [None, VIDEO_DIM], namevideo) label_ph tf.placeholder(tf.int32, [None], namelabel) _, logits fused_model(text_ph, audio_ph, video_ph) loss tf.reduce_mean( tf.nn.sparse_softmax_cross_entropy_with_logits( logitslogits, labelslabel_ph)) train_op tf.train.AdamOptimizer(learning_rate1e-3).minimize(loss) correct tf.equal(tf.argmax(logits, axis1), tf.cast(label_ph, tf.int64)) accuracy tf.reduce_mean(tf.cast(correct, tf.float32)) with tf.Session() as sess: sess.run(tf.global_variables_initializer()) for epoch in range(EPOCHS): for batch in make_batches(X_train, y_train, batch_size32): sess.run(train_op, feed_dict{ text_ph: batch[text], audio_ph: batch[audio], video_ph: batch[video], label_ph: batch[label], }) val_acc sess.run(accuracy, feed_dict{ text_ph: X_valid[text], audio_ph: X_valid[audio], video_ph: X_valid[video], label_ph: y_valid, }) print(epoch, epoch, val_acc, val_acc)邏輯說(shuō)明sparse_softmax_cross_entropy_with_logits 要求 label 是整數(shù)索引而不是 one-hot正好對(duì)上 pickle 里的 03 標(biāo)簽省一步轉(zhuǎn)換AdamOptimizer 配學(xué)習(xí)率 1e-3 是通用起點(diǎn)情感分類這種小數(shù)據(jù)任務(wù)很少需要超過(guò) 1e-2。驗(yàn)證集每個(gè) epoch 滾一次用來(lái)判斷收斂和過(guò)擬合。參數(shù)說(shuō)明batch_size 32 在 4G 顯存下比較穩(wěn)如果用的是視頻幀特征且每幀維度大降到 16 或 8EPOCHS 先設(shè) 50觀察驗(yàn)證準(zhǔn)確率不再上升就早停不用硬跑滿。make_batches 可以自己寫一個(gè) generator按 batch_size 切片注意最后一次不足一個(gè) batch 也要能跑別用取余把尾巴丟掉。4.3 怎么判斷訓(xùn)練真的有效訓(xùn)練完別只盯著訓(xùn)練集準(zhǔn)確率。第一步對(duì)比訓(xùn)練 loss 和驗(yàn)證 loss訓(xùn)練 loss 一直降、驗(yàn)證 loss 回升是過(guò)擬合加 dropout 或減 hidden_dim兩個(gè) loss 都平了說(shuō)明模型容量到頭。第二步做消融實(shí)驗(yàn)——分別只喂文本、只喂語(yǔ)音、只喂視頻記錄各自準(zhǔn)確率再和多模態(tài)結(jié)果對(duì)比。如果融合后沒比最好的單模態(tài)高大概率是特征沒對(duì)齊或者數(shù)據(jù)量撐不起融合層回頭查第 3 章的 pickle 維度是否真的匹配。result.png 是作者跑出的效果截圖可以作為你的基準(zhǔn)線。把自己訓(xùn)出的準(zhǔn)確率和混淆矩陣截圖存檔寫論文、做答辯 PPT 都用得上這也是為什么我建議每次實(shí)驗(yàn)固定隨機(jī)種子、固定數(shù)據(jù)切分不然兩份結(jié)果對(duì)不上答辯現(xiàn)場(chǎng)很難解釋。5. 環(huán)境部署與常見問題排查TensorFlow 1.7 時(shí)代的五個(gè)坑5.1 環(huán)境版本配對(duì)Python 3.6 TensorFlow 1.7README 寫得很明確Python 3.6、TensorFlow 1.7、支持 CPU 或 GPU。這套組合在當(dāng)年是主流放到今天問題就來(lái)了——TensorFlow 1.7 是 2018 年的版本和現(xiàn)在的 Python 3.9/3.10、新版 CUDA 完全不兼容。如果機(jī)器上已經(jīng)裝了 TF 2.x直接拿項(xiàng)目代碼跑幾乎必翻車。CPU 版安裝命令很簡(jiǎn)單關(guān)鍵是版本號(hào)必須鎖死conda create -n msa python3.6 conda activate msa pip install tensorflow1.7.0 pip install numpy1.19.5 scikit-learnGPU 版先確認(rèn)顯卡驅(qū)動(dòng)再裝pip install tensorflow-gpu1.7.0邏輯說(shuō)明TF 1.7 的 GPU 版對(duì)應(yīng) CUDA 9.0 和 cuDNN 7驅(qū)動(dòng)太新反而可能跑不起來(lái)這是老框架的通病。CPU 版雖然慢但對(duì)情感分析這種幾千條樣本的小數(shù)據(jù)集完全夠用一個(gè) epoch 幾十秒畢業(yè)設(shè)計(jì)演示不需要 GPU。裝完用下面這行驗(yàn)證環(huán)境能打印版本號(hào)就算過(guò)了python -c import tensorflow as tf; print(tf.__version__)注意確認(rèn)機(jī)器上沒有其他項(xiàng)目依賴 TF 2.x別在同一個(gè)環(huán)境里強(qiáng)行降級(jí)新建 conda 環(huán)境最省事。5.2 五個(gè)高頻踩坑記錄以下五條是這個(gè)項(xiàng)目上最常見的真實(shí)翻車現(xiàn)場(chǎng)按出現(xiàn)頻率排序??右籘ensorFlow 2.x 環(huán)境直接報(bào) AttributeError現(xiàn)象運(yùn)行 run.py 報(bào) module tensorflow has no attribute placeholder或 has no attribute layers。原因機(jī)器裝的是 TF 2.xtf.placeholder、tf.layers 在 2.0 里被移除項(xiàng)目代碼按 1.x API 寫的。解決新建 conda 環(huán)境裝 Python 3.6然后 pip install tensorflow1.15.0。1.15 對(duì) 1.x 代碼兼容最好支持到更晚的 Python 3.7比 1.7 省心。如果堅(jiān)持用 2.x可以在代碼頭部加 import tensorflow.compat.v1 as tf 并調(diào)用 tf.disable_v2_behavior()但老代碼里其他 op 還會(huì)陸續(xù)報(bào)錯(cuò)不建議。坑二加載 pickle 報(bào) UnicodeDecodeError現(xiàn)象pickle.load 拋 UnicodeDecodeError: ascii codec cant decode byte。原因pickle 是作者用 Python 2 序列化的Python 3 默認(rèn)按 ascii 解析字符串撞上非 ASCII 字符就炸。解決open 后加 encodinglatin1第 3 章代碼已經(jīng)寫好了。這是老多模態(tài)項(xiàng)目最經(jīng)典的兼容性坑踩的人特別多??尤齨umpy 報(bào) module numpy has no attribute float現(xiàn)象import 階段報(bào) AttributeError: module numpy has no attribute float。原因numpy 1.24 起刪掉了 np.float、np.int 這些老別名data_prep.py 或依賴庫(kù)還在用。解決把 numpy 降到 1.19.5。兼容 Python 3.6 的 numpy 最高就到 1.19.5和 TF 1.7 正好同一代裝的時(shí)候務(wù)必鎖版本??铀腉PU 訓(xùn)練 OOMResourceExhaustedError現(xiàn)象run.py 跑到某個(gè) epoch 報(bào) ResourceExhaustedError: OOM when allocating tensor with shape [...]。原因三個(gè)模態(tài)特征同時(shí)進(jìn) feed_dict視頻特征每幀幾千維時(shí)一個(gè) batch 占用的顯存會(huì)暴漲8G 以下顯存尤其明顯。解決batch_size 從 32 降到 16 或 8確認(rèn) feed_dict 里沒有把整個(gè)驗(yàn)證集一次性塞進(jìn)去再不行用編碼層把輸入先降維或者干脆跑 CPU 版??游迥P椭粚W(xué)到多數(shù)類準(zhǔn)確率虛高現(xiàn)象訓(xùn)練準(zhǔn)確率很高比如 80%但打印每一類的召回率發(fā)現(xiàn)模型幾乎把所有樣本都判成“其他”。原因情感類別天然不均衡IEMOCAP 里 neutral 和其他類樣本多網(wǎng)絡(luò)學(xué)了個(gè)偷懶策略。解決切分?jǐn)?shù)據(jù)時(shí)用 stratifylabels給損失函數(shù)加類別權(quán)重對(duì)少數(shù)類放大梯度評(píng)估時(shí)看混淆矩陣而不是只看總準(zhǔn)確率。6. 落地?cái)U(kuò)展把四分類模型改造成自己的數(shù)據(jù)集如果你手頭有自己的數(shù)據(jù)而不是直接用項(xiàng)目自帶的 pickle步驟其實(shí)不復(fù)雜核心就是把原始數(shù)據(jù)換成和 pickle 里一樣的特征格式。特征提取按模態(tài)分開處理。文本部分量小用預(yù)訓(xùn)練詞向量平均量大用 BERT 句向量輸出維度按需裁剪語(yǔ)音部分用 openSMILE 提取聲學(xué)特征拿到 CSV 再轉(zhuǎn) numpy圖片/視頻部分用預(yù)訓(xùn)練 CNN 去掉分類頭把每幀過(guò)一遍網(wǎng)絡(luò)取倒數(shù)第二層輸出作為該模態(tài)特征多條視頻幀可以平均成一個(gè)向量。特征到位后用第 3 章的 split_multimodal 同步切分再直接跑 run.py。這里有一個(gè)我反復(fù)強(qiáng)調(diào)的驗(yàn)證方法先用單模態(tài)特征分別訓(xùn)練拿到三條基線再打開融合。如果某一模態(tài)特征質(zhì)量太差比如語(yǔ)音采集噪聲大融合后反而被它拖累這時(shí)候在 data_prep.py 里給該模態(tài)特征做歸一化或者干脆先剔除只做雙模態(tài)。進(jìn)階技巧是用項(xiàng)目自帶的 unimodal_iemocap_4way.pickle 訓(xùn)練出一個(gè)預(yù)訓(xùn)練權(quán)重再在自己的數(shù)據(jù)上微調(diào)只重新訓(xùn)練最后兩層。做法是在 run.py 里把初始化換成加載 checkpoint并限定可訓(xùn)練變量saver tf.train.Saver() with tf.Session() as sess: saver.restore(sess, ./pretrained/iemocap_4way.ckpt) # 只微調(diào)三模態(tài)層和分類層前面層凍結(jié) train_vars [v for v in tf.trainable_variables() if tri_fc in v.name or logits in v.name] train_op tf.train.AdamOptimizer(1e-4).minimize(loss, var_listtrain_vars)邏輯說(shuō)明saver.restore 加載 IEMOCAP 4way 訓(xùn)練出的權(quán)重作為初始化var_list 限定只更新 tri_fc 和 logits 兩層前面的模態(tài)編碼層保持不動(dòng)避免小數(shù)據(jù)量下把預(yù)訓(xùn)練表示沖掉。學(xué)習(xí)率從 1e-3 降到 1e-4微調(diào)階段步子要小。驗(yàn)證是否成功的標(biāo)準(zhǔn)很簡(jiǎn)單項(xiàng)目自帶的 pickle 跑出來(lái)的準(zhǔn)確率與你自己數(shù)據(jù)的準(zhǔn)確率放在同一張表里融合模型高于任意單模態(tài)基線說(shuō)明多模態(tài)確實(shí)在起作用這個(gè)結(jié)果寫進(jìn)論文很有說(shuō)服力。說(shuō)句實(shí)在話我在多模態(tài)項(xiàng)目上栽過(guò)的跟頭十有七八不在模型而在數(shù)據(jù)對(duì)齊和版本兼容。從那以后我每次拿到情感分析數(shù)據(jù)集都強(qiáng)制自己先走一遍“讀 pickle → 查維度 → 單模態(tài)基線 → 融合對(duì)比”的流程十分鐘的檢查能省掉一晚上的排錯(cuò)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取