情感分析實戰(zhàn):Jupyter+Python下的模型構(gòu)建與注意力融合)
簡介面向畢業(yè)設計、課程設計及項目開發(fā)的多模態(tài)情感分析完整實現(xiàn)包基于JupyterPython開發(fā)整合文本、圖像等多模態(tài)輸入提供從數(shù)據(jù)清洗、特征提取、模型構(gòu)建到訓練預測與結(jié)果評估的端到端流程。項目總計2000個文件壓縮包大小約202MB其中1997個txt數(shù)據(jù)與結(jié)果文件便于分階段測試、擴展訓練集或做交叉驗證另有項目說明、實驗報告文檔及主程序腳本目錄按數(shù)據(jù)、腳本、文檔劃分方便直接定位核心代碼。已有522人學習使用源碼經(jīng)過嚴格測試可獨立運行配合項目文檔能深入理解多模態(tài)特征融合、情感分類等關(guān)鍵環(huán)節(jié)的實現(xiàn)細節(jié)既可支撐課程設計與畢業(yè)設計答辯演示也可作為后續(xù)智能交互、輿情分析等研究方向的起步模板。內(nèi)容預覽顯示包含大量文本樣本與預測輸出文件讀者可對照結(jié)果驗證模型效果并在現(xiàn)有代碼基礎上繼續(xù)延伸應用。1. 多模態(tài)情感分析不是三分類JupyterPython把它變成了可上手的項目社交媒體上的負面輿情往往不是靠一段文案表達出來的。語氣里壓抑的顫抖、畫面里的暗沉色調(diào)、配上看似中性的文字組合在一起才是真實態(tài)度。如果只給模型看文本準確率會卡在75%上下加入語音和圖像特征后通常能越過85%。這個項目標題把JupyterPython放在最前面說明它要的并不是一篇paper復現(xiàn)而是從安裝環(huán)境到模型輸出一整套能直接跑通的交付物。對畢業(yè)設計和課程設計來說多模態(tài)情感分析模型恰好覆蓋了數(shù)據(jù)采集、特征工程、深度學習、可視化展示幾個必備環(huán)節(jié)對想進入多模態(tài)方向的開發(fā)者來說它又是一個能講清楚“為什么融合比單模態(tài)好”的最小實驗臺。下面直接進入架構(gòu)選型把每一步說清楚。2. 多模態(tài)情感分析的模型架構(gòu)文本、音頻、圖像如何編碼與融合2.1 單個模態(tài)先編碼文本用BERT音頻用MFCC加時序網(wǎng)絡圖像用ResNet多模態(tài)情感分析的前提是每個模態(tài)有自己的特征形式。文本是token序列音頻是采樣點或頻譜圖像是像素。工程上最常見做法是分別用一個預訓練或輕量級編碼器把輸入變成向量再讓融合層去組合。文本側(cè)我一般會優(yōu)先考慮中文預訓練模型比如bert-base-chinese取最后一層CLS向量作為句子表示如果訓練機器沒有顯卡退而求其次用Word2Vec把詞向量平均再加一層BiLSTM也能保住基本上下文信息。音頻側(cè)先計算MFCC特征常見配置是40維、窗長25ms、幀移10ms然后送進一個兩層LSTM或小尺寸CNN。圖像或視頻幀用ResNet18去掉最后的全連接層輸出512維特征圖再做全局平均池化。下表是我在類似項目里常用的一組替換方案模態(tài)輸入特征編碼器輸出維度說明文本token序列max_len128bert-base-chinese768也可換RoBERTa-wwm效果更穩(wěn)但更吃顯存音頻MFCC40 x 幀數(shù)Conv1D BiLSTM256幀數(shù)需要統(tǒng)一到固定長度圖像224 x 224 x 3ResNet18512對光照變化敏感需要做歸一化選型時不要貪大。課程設計環(huán)境里如果只有CPUBERT前向會非常慢此時預設編碼器輸出維度統(tǒng)一到256再融合是一種更務實的做法。2.2 融合策略不是簡單的concat注意力加權(quán)更符合情感表達常見融合有早期、中期、晚期。早期把特征拼接后一起過層參數(shù)少但面臨對齊噪聲晚期每個模態(tài)先獨立出分類結(jié)果再投票實現(xiàn)簡單卻丟失模態(tài)間的相關(guān)性。我一般使用中期注意力融合也就是先得到各模態(tài)的向量再計算一個可學習的權(quán)重按加權(quán)和得到聯(lián)合表示。計算上就是把三個向量分別過一層線性映射到相同隱層維度拼接后經(jīng)過兩層全連接產(chǎn)生三個權(quán)重softmax歸一化再對原始向量加權(quán)求和。這里面可以不用太復雜的跨模態(tài)注意力文本和音頻的語義相關(guān)性強于文本和圖像但項目迭代時可以先用統(tǒng)一權(quán)重后續(xù)再觀察注意力值變化決定要不要單獨給文本模態(tài)更高優(yōu)先級。2.3 特征對齊統(tǒng)一長度、統(tǒng)一采樣率、統(tǒng)一隱層維度對齊是融合前最容易出錯的地方。文本側(cè)要固定max_len超過截斷、不足padding出一個attention_mask。音頻側(cè)用librosa把音頻重采樣到16000Hz然后對MFCC幀軸做padding或直接插值到固定幀數(shù)。圖像側(cè)只需要把短邊resize到256再中心裁剪到224。最后在模型里把三個向量通過Linear層映射到同樣的hidden_dim融合層才不需要處理維度不對稱。常見誤用是直接用各自編碼器輸出不同維度強行concat高維模態(tài)會淹沒低維信號另一個誤用是沒處理音頻的靜音段導致大量樣本幀全是零向量訓練時loss先降到很低再反彈。解決方式是計算幀級能量去掉能量低于閾值的幀或在預處理階段直接過濾靜音段。3. 在Jupyter Notebook里搭建Python多模態(tài)項目環(huán)境3.1 用conda創(chuàng)建獨立環(huán)境避免jupyter notebook無法運行的版本沖突很多報錯都出在“系統(tǒng)里本來就有Python再加裝依賴導致內(nèi)核混亂”這一步。我一般會先裝Miniconda然后為項目創(chuàng)建一個獨立環(huán)境conda create -n msa python3.9 -y conda activate msa python -m pip install --upgrade pip pip install jupyter notebook pip install torch torchaudio torchvision --index-url https://download.pytorch.org/whl/cu118 pip install transformers librosa soundfile opencv-python pandas scikit-learn matplotlib這里把torch系列裝成CPU版就刪掉--index-url參數(shù)反之要確認CUDA版本。安裝完在base環(huán)境里執(zhí)行conda install nb_conda_kernels重啟jupyter之后就能在新內(nèi)核里選擇msa。如果發(fā)現(xiàn)jupyter notebook無法運行或內(nèi)核列表為空優(yōu)先檢查pip list里有沒有ipykernel沒有就執(zhí)行python -m ipykernel install --user --name msa。3.2 數(shù)據(jù)集目錄約定一個樣品一個路徑標簽用CSV統(tǒng)一管理多模態(tài)項目最大的坑不是模型是數(shù)據(jù)對不上。常見做法是每個樣本單獨編個IDID下放txt、wav、jpg三個文件標簽集中放在train_label.csv中data/train/0001.txt data/train/0001.wav data/train/0001.jpg data/train/0002.txt data/train/0002.wav data/train/0002.jpg ... data/label.csvlabel.csv至少三個字段示例為id,text,emotion其中emotion使用數(shù)字0負面1中性2正面。這樣后面用pandas合并時不需要猜路徑。音頻文件盡量統(tǒng)一為wav格式不要混用mp3和m4alibrosa在加載mp3時依賴soundfile部分環(huán)境會因編碼問題讀不出文件。3.3 用Python寫一個多模態(tài)數(shù)據(jù)讀取與預處理類核心是保證每條樣本都能取出三個模態(tài)對齊后的張量。以下是實際可用的簡化版放在dataset.py里import librosa import torch import pandas as pd from torch.utils.data import Dataset from PIL import Image from torchvision import transforms from transformers import BertTokenizer class MultiModalDataset(Dataset): def __init__(self, csv_path, data_dir): self.df pd.read_csv(csv_path) self.data_dir data_dir self.tokenizer BertTokenizer.from_pretrained(bert-base-chinese) self.img_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] sample_id row[id] text row[text] text_ids self.tokenizer(text, max_length128, paddingmax_length, truncationTrue, return_tensorspt) audio, sr librosa.load(f{self.data_dir}/{sample_id}.wav, sr16000) mfcc librosa.feature.mfcc(yaudio, srsr, n_mfcc40) mfcc torch.FloatTensor(mfcc).permute(1, 0) # [time, 40] if mfcc.size(0) 157: mfcc torch.nn.functional.pad(mfcc, (0, 0, 0, 157 - mfcc.size(0))) else: mfcc mfcc[:157, :] image Image.open(f{self.data_dir}/{sample_id}.jpg).convert(RGB) image self.img_tf(image) label row[emotion] return text_ids[input_ids].squeeze(0), text_ids[attention_mask].squeeze(0), \ mfcc, image, torch.tensor(label)代碼邏輯說明tokenizer返回值是字典直接取input_ids和attention_mask兩者都要參與訓練。librosa.load強制sr16000把音頻統(tǒng)一采樣率避免不同采樣率的wav被模型當作另一個特征。MFCC維度設置成(time, 40)與PyTorch默認的(seq, feature)一致后續(xù)網(wǎng)絡不需要每次轉(zhuǎn)置。固定幀數(shù)157對應2秒音頻按默認hop_length計算出的MFCC幀數(shù)樣本不足時右側(cè)padding超過則截斷。如果音頻長度差異很大可以把157調(diào)成實際幀數(shù)分布的中位數(shù)不要在數(shù)據(jù)集里混用多個長度。圖像側(cè)用Resize到224再歸一化Normalize的均值方差必須和預訓練模型保持一致否則ResNet輸出特征會發(fā)生漂移。4. Python實現(xiàn)多模態(tài)情感分析模型融合網(wǎng)絡、訓練與評估4.1 先寫一個可擴展的融合模型我把三個編碼器統(tǒng)一封裝在MultiModalFusion里文本用BertModel音頻用Conv1d加BiLSTM圖像用ResNet18。融合部分采用前面提到的注意力加權(quán)import torch import torch.nn as nn from transformers import BertModel from torchvision.models import resnet18 class TextEncoder(nn.Module): def __init__(self): super().__init__() self.bert BertModel.from_pretrained(bert-base-chinese) def forward(self, input_ids, attention_mask): out self.bert(input_idsinput_ids, attention_maskattention_mask) return out.last_hidden_state[:, 0, :] # CLS向量 class AudioEncoder(nn.Module): def __init__(self, input_dim40, hidden128, num_layers2): super().__init__() self.conv nn.Conv1d(input_dim, 64, kernel_size3, padding1) self.lstm nn.LSTM(64, hidden, num_layersnum_layers, batch_firstTrue, bidirectionalTrue) self.fc nn.Linear(hidden * 2, 256) def forward(self, x): x x.permute(0, 2, 1) x torch.relu(self.conv(x)) x x.permute(0, 2, 1) out, _ self.lstm(x) return self.fc(out[:, -1, :]) class ImageEncoder(nn.Module): def __init__(self): super().__init__() self.backbone resnet18(pretrainedTrue) self.backbone.fc nn.Identity() self.fc nn.Linear(512, 256) def forward(self, x): return self.fc(self.backbone(x))然后是把三者融合的模型class MultiModalFusion(nn.Module): def __init__(self, num_classes3): super().__init__() self.text_enc TextEncoder() self.audio_enc AudioEncoder() self.image_enc ImageEncoder() self.attn nn.Sequential( nn.Linear(768 256 256, 384), nn.ReLU(), nn.Linear(384, 3), nn.Softmax(dim-1) ) self.classifier nn.Sequential( nn.Linear(768 256 256, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, num_classes) ) def forward(self, text_ids, text_mask, audio, image): t self.text_enc(text_ids, text_mask) a self.audio_enc(audio) i self.image_enc(image) h torch.cat([t, a, i], dim-1) attn self.attn(h) fused attn[:, 0:1] * t attn[:, 1:2] * a attn[:, 2:3] * i return self.classifier(fused)參數(shù)說明AudioEncoder里的Conv1d輸入通道是40對應MFCC維數(shù)LSTM的hidden設為128雙向后輸出256。TextEncoder直接取BERT的CLS向量句長被統(tǒng)一成128因此開銷可控。MultiModalFusion的attn在拼接向量上預測三模態(tài)權(quán)重softmax后對三個特征加權(quán)再分類這比簡單concat更能體現(xiàn)每種模態(tài)對當前樣本的貢獻。4.2 訓練循環(huán)、損失函數(shù)與超參表的對應關(guān)系訓練時文本側(cè)BERT通常需要較小的學習率其他層可以稍大因此使用AdamW和分層學習率比較穩(wěn)妥from transformers import AdamW from torch.utils.data import DataLoader from sklearn.metrics import f1_score, accuracy_score model MultiModalFusion(num_classes3) optimizer AdamW([ {params: model.text_enc.parameters(), lr: 2e-5}, {params: model.audio_enc.parameters(), lr: 1e-4}, {params: model.image_enc.parameters(), lr: 1e-4}, {params: model.attn.parameters(), lr: 1e-4}, {params: model.classifier.parameters(), lr: 1e-4}, ], weight_decay0.01) loss_fn nn.CrossEntropyLoss() for epoch in range(10): model.train() total_loss 0 for text_ids, text_mask, mfcc, images, labels in DataLoader( dataset, batch_size16, shuffleTrue): optimizer.zero_grad() out model(text_ids, text_mask, mfcc, images) loss loss_fn(out, labels) loss.backward() optimizer.step() total_loss loss.item() print(fepoch {epoch1}, loss{total_loss/len(dataset):.4f})這里有幾個參數(shù)值得現(xiàn)場改batch_size16適合8GB顯存顯存不足優(yōu)先降到8而不是調(diào)小BERT的max_length。epochs對中文BERT微調(diào)來說10到15輪足夠看到收斂趨勢如果loss震蕩把weight_decay從0.01改成0.05。AdamW的weight_decay通常只作用在非bias參數(shù)上上面的寫法會整體衰減但簡單項目里影響不大可以先用。4.3 評估、混淆矩陣與模型保存訓練完成后不能只看loss情感分類類別不平衡很常見我用macro F1和混淆矩陣判斷哪個類別被模型忽略from sklearn.metrics import classification_report, confusion_matrix model.eval() all_preds, all_labels [], [] with torch.no_grad(): for text_ids, text_mask, mfcc, images, labels in DataLoader(val_ds, batch_size16): out model(text_ids, text_mask, mfcc, images) preds out.argmax(dim-1) all_preds.extend(preds.tolist()) all_labels.extend(labels.tolist()) print(classification_report(all_labels, all_preds, digits4)) print(confusion_matrix(all_labels, all_preds)) torch.save(model.state_dict(), checkpoints/msa_epoch10.pt)說明classification_report輸出的macro avg比accuracy更值得寫進項目文檔混淆矩陣如果顯示某類嚴重偏低說明訓練集該模態(tài)樣本少優(yōu)先做數(shù)據(jù)增強而不是改網(wǎng)絡。模型保存成state_dict是為了方便部署時只保留權(quán)重結(jié)構(gòu)通過模型代碼還原。5. 項目文檔、源碼結(jié)構(gòu)與Jupyter產(chǎn)物讓畢設和課設可交付5.1 源碼目錄把notebook、模型代碼和文檔分開如果全部堆在一個notebook里最后答辯時很難講清楚哪個文件負責數(shù)據(jù)加載、哪個負責訓練。我建議把工程化代碼和探索代碼分開my_project/ ├── README.md ├── requirements.txt ├── config.py ├── dataset.py ├── models.py ├── train.py ├── predict.py └── notebooks/ └── main.ipynbconfig.py集中放路徑、超參和隨機種子dataset.py和models.py是純Python類方便命令行訓練notebooks/main.ipynb只做演示和可視化不承載核心訓練邏輯。這樣別人拿到項目能直接跑python train.py而不是先打開每個cell。5.2 README、requirements.txt與docstring三樣缺一不可README需要覆蓋項目是做什么的、數(shù)據(jù)集放在哪里、環(huán)境如何安裝、訓練命令是什么、預期結(jié)果截圖。requirements.txt要注意版本鎖定pip freeze requirements.txt生成后手動把torch版本確認到可復現(xiàn)版本例如torch2.1.0。每個類和函數(shù)至少要有docstring不要寫一大段注釋函數(shù)內(nèi)部關(guān)鍵行一兩個注釋就夠了def load_config(): 讀取config.py并返回參數(shù)字典避免在notebook里手動輸入路徑。 return config.__dict__docstring的價值在于生成API文檔和IDE提示畢設評閱時老師會直接看代碼跳轉(zhuǎn)有docstring的代碼整體觀感會好很多。5.3 用jupyter nbconvert把Notebook導出為無需運行的演示稿答辯現(xiàn)場經(jīng)常出現(xiàn)kernel忙、依賴丟失等尷尬所以我在提交前會把主notebook導出為兩種格式jupyter nbconvert --to html notebooks/main.ipynb jupyter nbconvert --to script notebooks/main.ipynb --output demo_script.py--to html用于演示瀏覽器打開后不依賴Python環(huán)境--to script用于代碼走查方便老師直接看邏輯。如果notebook里有耗時訓練導出前先把所有cell的輸出清理干凈只保留關(guān)鍵結(jié)果圖這樣導出文件會小很多加載也快。Another pointnotebook里執(zhí)行過的變量不會保留到其他文件所以代碼評審時最好以train.py為準notebook只放核心中間結(jié)果和可視化。6. 端到端復現(xiàn)與驗證固定隨機種子、導出模型、分析錯誤樣本6.1 固定隨機種子讓每次訓練結(jié)果可復現(xiàn)答辯時最怕?lián)Q一臺機器結(jié)果大相徑庭。我一般在config.py里固定seed并在訓練開頭調(diào)用import random, numpy as np, torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)注意DataLoader的shuffleTrue在不同PyTorch版本下仍可能因為隨機算法變化導致結(jié)果微小浮動所以最終要鎖定整個環(huán)境版本號不只是種子。6.2 單樣本推理函數(shù)驗證融合是否真的起作用用state_dict恢復模型后寫一個最簡單的predict函數(shù)對同一條數(shù)據(jù)分別給文本、給音頻、給圖像看預測變化def predict_single(model, text, audio_path, image_path): model.eval() text_ids, text_mask, mfcc, image process_one_sample(text, audio_path, image_path) with torch.no_grad(): logits model(text_ids, text_mask, mfcc.unsqueeze(0), image.unsqueeze(0)) return logits.argmax(dim-1).item()如果單獨給文本是負面、單獨給音頻是中性融合后變成正面說明注意力層在起作用如果三個單模態(tài)和融合結(jié)果全一樣說明融合層沒有學會加權(quán)需要回去檢查attention權(quán)重是否在訓練后被常數(shù)覆蓋。6.3 用錯誤樣本定位是標注問題還是模態(tài)缺失把預測錯誤樣本按字段保存逐個看文本和對應音頻波形。很多情況是標注本身有爭議比如“這電影還挺逗的”配著生無可戀的語氣標注為負面但模型融合后給正面這時優(yōu)先修數(shù)據(jù)而不是改網(wǎng)絡。讓錯誤樣本可視化成為項目文檔的一項內(nèi)容畢設答辯時能直接展示。這個predict_single函數(shù)可以直接留在predict.py里后續(xù)接API或批量跑測試集時都只需要復用這一個入口。本文還有配套的精品資源點擊獲取