首页 / 资讯中心 / 文章详情

多模态情感分析入门:从三路特征融合到注意力机制实战

多模态情感分析入门:从三路特征融合到注意力机制实战 ★ FEATURED ARTICLE
简介《多模态情感分析入门》是一本由清华大学出版社与Springer联合出版的PDF专著作者为清华大学徐华主要面向自然语言处理、人机交互和智能系统领域的入门读者与研究人员。全书系统讲解如何从文本、音频与视觉三种模态中分析与理解情感状态重点涉及不同模态的特征表示、早期融合与晚期融合、基于模型的融合和决策级融合等策略并结合CNN、RNN、LSTM、注意力机制等深度模型与SVM、随机森林等传统机器学习方法解析前沿分类模型在情感识别中的实际应用。内容还包含大量实验设计与数据集介绍帮助读者掌握数据处理、模型评估和实验流程等关键环节适合作为学术入门参考和技术研发的案头手册。压缩包内共1个PDF文件文件大小11.86MB便于下载后跨设备阅读。目前已有159人学习下载对于刚接触多模态情感分析或计划开展相关实验的读者具有不错的实用价值。1. 多模态情感分析入门从文本、音频、视觉三路信号到最终那个“-0.7”做客服质检和用户反馈分析的人大概率遇到过同样的困境文本情感分析模型跑得好好的可一旦遇到“语音转出来的文字很平淡、语气却充满嘲讽”“表情不悦但嘴上说着还行”这类场景单模态模型基本失灵。多模态情感分析就是为解决这种信息割裂而生的一门技术。该书系统覆盖了三类信号的特征表示、融合方法与分类模型配套数据集与开源实现适合正要从纯文本情感分析跨到多模态、或要在自然语言处理与人机交互方向开新坑的研究者和工程师。下面按我从数据集到落地验证的实操顺序拆解这份资源。2. 数据集选型与预处理先用 CMU-MOSI 跑通全流程2.1 五个数据集怎么选从样本量到标注粒度的取舍书里系统整理了多模态情感分析的几大公开数据集我自己的经验是第一次接触这个方向别一上来就挑难度最大的先拿小数据集把流程跑通再考虑换大的。CMU-MOSI 是大多数论文的默认起点样本来自短视频评论每段视频带连续的情感分数取值范围通常为 -3 到 3。这个数据集规模适中文本、音频、视觉特征都有官方版本适合用来验证基线模型和融合策略。CMU-MOSEI 是它的放大版样本量约是 MOSI 的十倍覆盖更多说话人和话题。代价是标注噪声变大训练时间明显变长。如果只是做入门学习MOSEI 不建议作为第一个跑通的实验对象。IEMOCAP 偏对话场景有脚本化表演的成分情感标注是离散类别适合做对话中的情感识别。MELD 则是从电视剧中截取的多人对话同样是离散标签但包含说话人关系和对话历史适合研究上下文建模。CH-SIMS 是国内团队构建的中文多模态数据集特殊之处在于每一段视频分别对文本、音频、视觉三个模态独立打了情感标签同时还有一个整体标签。这意味着你可以直接做模态级监督也能分析单一模态对最终结果的贡献。我一般会建议刚入门的人用 MOSI 做开发集因为它的连续情感分数能同时支持回归和分类两种任务。想深入中文场景再加 CH-SIMS。四个数据集的对比如下数据集语言标注形式适用场景CMU-MOSI英文连续分数 -3~3入门跑通流程CMU-MOSEI英文连续分数大样本验证IEMOCAP英文离散类别对话情感MELD英文离散类别多轮对话建模CH-SIMS中文模态级整体标签中文细粒度分析选型时首先确认实验目标如果算法是核心诉求选基准数据集才方便对比论文数字如果应用是核心诉求选和真实场景语言、交互形式更接近的数据集。2.2 预处理第一坑模态对齐与统一采样拿到原始视频后最头疼的问题就是三路信号不同步。文本是离散词序列音频是连续的声学帧视觉是每秒几十帧的画面。三者必须对齐到同一个时间单元后续才能融合。常见做法是把视频按固定窗口比如 0.5 秒切分每个窗口取出对应的文本片段、音频帧序列和视觉帧序列。原始视频要先做人脸检测把人脸区域裁出来再按帧抽取。文本则需要借助语音识别工具转成带时间戳的文字。实际处理代码如下import librosa import numpy as np from transformers import pipeline # 1. 读视频抽音频和帧 # 假设 video_path 指向 mp4 文件这里用 ffmpeg 提取音频 # ffmpeg -i input.mp4 -ar 16000 -ac 1 audio.wav # 2. 按 0.5 秒窗口分帧 window_size 0.5 sr 16000 hop_length int(window_size * sr) # 每个窗口的采样点数 # librosa 读取音频 audio, sr librosa.load(audio.wav, srsr) num_windows len(audio) // hop_length # 3. 对每个窗口提取音高和音量特征 def extract_audio_features(window_start): seg audio[window_start*hop_length : (window_start1)*hop_length] # 音量用 RMS 能量 rms np.sqrt(np.mean(seg**2)) # 音高取基频估计这里简化为过零率 zcr np.mean(librosa.zero_crossings(seg)) return np.array([rms, zcr]) audio_feats [extract_audio_features(i) for i in range(num_windows)]这段处理的逻辑是先把连续音频切成等长窗口再对每个窗口算两个基础声学特征。参数上采样率 16kHz 是语音识别和声学特征提取的常见选择窗口 0.5 秒则兼顾了时间分辨率和特征稳定性。窗口太长会丢失语气转折太短则特征噪声大。过零率和 RMS 能量虽然简单但对入门实验足够能直观体现愤怒音量偏高和轻蔑过零率异常的声学差别。2.3 文本模态的时序对齐方案文本对齐比音频更麻烦。语音识别输出的词带时间戳但一个词可能跨多个窗口一个窗口里也可能没有完整词。书里提到的做法是按词的时间戳对齐到窗口窗口内没有词的填充特殊占位。实际工程中更稳妥的方案是用词嵌入的加权平均窗口内有多个词就把这些词的向量按时长加权合并成一个向量。from transformers import BertTokenizer, BertModel import torch tokenizer BertTokenizer.from_pretrained(bert-base-uncased) model BertModel.from_pretrained(bert-base-uncased) def text_window_feature(words_with_ts, window_words): # words_with_ts: [(word, start_ts, end_ts), ...] # window_words: 当前窗口内包含的词 if not window_words: return torch.zeros(768) # 空窗口补零 tokens tokenizer(window_words, paddingTrue, truncationTrue, return_tensorspt) with torch.no_grad(): outputs model(**tokens) # 取 [CLS] 向量作为窗口文本特征 return outputs.last_hidden_state[:, 0, :].squeeze()BERT 输出的 [CLS] 向量在句子级任务里通常当作整句的语义表示。窗口内词少时 [CLS] 依然有信息但窗口为空时只能补零。补零在后续融合里需要特别处理否则全零向量会在归一化时造成均值偏移这一点在第五部分会专门说。3. 三模态特征提取从文本嵌入到面部动作单元3.1 文本BERT 之外还有更轻的基线书里提到的深度学习模型以 CNN、RNN、LSTM 和注意力机制为主。放在今天的工程环境里文本模态我推荐直接从预训练语言模型入手。BERT 的效果通常比训练随机初始化的 LSTM 稳定得多但如果你在资源受限的环境里做基线一个双向 LSTM 加 GloVe 嵌入也能跑出合理结果还不容易过拟合。import torch.nn as nn class TextEncoder(nn.Module): def __init__(self, vocab_size, embed_dim300, hidden_dim128): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim) self.lstm nn.LSTM(embed_dim, hidden_dim, batch_firstTrue, bidirectionalTrue) self.fc nn.Linear(hidden_dim * 2, hidden_dim) def forward(self, x): emb self.embedding(x) # (batch, seq_len, 300) out, _ self.lstm(emb) # (batch, seq_len, 256) # 取最后时刻的双向拼接输出 last out[:, -1, :] # (batch, 256) return self.fc(last)双向 LSTM 的 hidden_dim 设为 128双向拼接后是 256全连接层压回 128。这个设计的逻辑是情感语义往往由整句话的上下文共同决定双向结构让每个位置同时看到前后文。词表大小按实际语料构建建议过滤低频词把词表控制在 5 万以内否则 Embedding 层参数膨胀严重。3.2 音频openSMILE 特征集与手工特征的边界音频模态的特征提取学术界最常用的工具是 openSMILE它导出的 eGeMAPS 特征集包含 88 维低层次声学描述子覆盖音高、响度、频谱倾斜、抖动等。这份资源里讨论音调、节奏、音量等声学特性正是这类特征集能覆盖的内容。在实际工程里我通常先跑 openSMILE 生成特征再送入一个小型时序模型。原因是手工声学特征已经高度压缩了声学信息直接接到 LSTM 或全连接上够用不必非得上复杂的预训练语音模型。SMILExtract -C config/eGeMAPS.cfg -I audio.wav -O audio_features.csv输出文件的每一行是一个窗口的声学特征向量。常见参数是窗口 25ms、步长 10ms这是语音处理的老传统配置。如果后续要和 0.5 秒的文本窗口融合需要再做一次时序池化通常做法是对窗口内的多个声学帧取均值和标准差拼接相当于做个局部聚合。3.3 视觉人脸表情特征优于全局帧特征视觉模态最容易走弯路。初学者常直接把整帧视频图喂给预训练 CNN但这样会引入大量背景干扰。正确的做法是先做人脸检测只保留人脸区域再提取表情特征。人脸特征有两个层级底层用 3D-CNN 模型直接提取人脸区域的特征向量推荐用预训练好的模型输出 512 维或 1024 维向量高层则用面部动作单元Action Units编码常见的是 FACS 系统的 AU1、AU2、AU4、AU6、AU12 等这些单元直接对应特定的表情动作比如 AU12 对应嘴角上扬。import cv2 def extract_face_frames(video_path, interval0.5): cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) frame_ids [] current 0 while True: ret, frame cap.read() if not ret: break if current % int(fps * interval) 0: # 此处应有面部检测和对齐简化为裁剪中心区域 h, w frame.shape[:2] face frame[h//4:h*3//4, w//4:w*3//4] frame_ids.append(face) current 1 cap.release() return frame_ids人脸检测这一步在工程上有很多现成方案先做检测再做特征提取比直接把全图送入模型能显著提升表情相关的特征质量。间隔 0.5 秒与前面音频窗口保持一致这样后续融合不需要再插值对齐。4. 融合策略早期拼接、晚期投票与中间融合的取舍4.1 早期融合和晚期融合的代码对照特征融合是全书的核心话题之一。早期融合early fusion把三路特征在输入层直接拼接模型看到的是一个既含文本语义又含声学与视觉信息的综合向量。晚期融合late fusion则让三个模态各自建模最后再对输出做决策综合。import torch import torch.nn as nn # 早期融合三路特征拼接后送入分类器 class EarlyFusion(nn.Module): def __init__(self, text_dim128, audio_dim88, video_dim512, hidden_dim256, num_classes1): super().__init__() self.fusion nn.Sequential( nn.Linear(text_dim audio_dim video_dim, hidden_dim), nn.ReLU(), nn.Dropout(0.3), nn.Linear(hidden_dim, num_classes) ) def forward(self, text_feat, audio_feat, video_feat): combined torch.cat([text_feat, audio_feat, video_feat], dim-1) return self.fusion(combined) # 晚期融合各模态独立编码最后平均分类分数 class LateFusion(nn.Module): def __init__(self, text_dim128, audio_dim88, video_dim512, hidden_dim128, num_classes1): super().__init__() self.text_net nn.Linear(text_dim, hidden_dim) self.audio_net nn.Linear(audio_dim, hidden_dim) self.video_net nn.Linear(video_dim, hidden_dim) self.classifier nn.Linear(hidden_dim, num_classes) def forward(self, text_feat, audio_feat, video_feat): t torch.relu(self.text_net(text_feat)) a torch.relu(self.audio_net(audio_feat)) v torch.relu(self.video_net(video_feat)) # 三路分类分数取平均 scores [self.classifier(x) for x in (t, a, v)] return torch.stack(scores).mean(dim0)早期融合的优点是实现简单模型能自己学到模态间的交互。缺点是模态间尺度差异大时数值大的特征会主导梯度所以拼接前必须做归一化。晚期融合的每个模态独立建模适合模态缺失场景比如只有文本但模型学不到跨模态的交互信息。实际效果上早期融合在 CMU-MOSI 上通常优于晚期融合前提是特征做过标准化。晚期融合更适合部署环境不稳定的产品因为某一路输入坏了其他路照常能出结果。4.2 中间融合注意力机制是性价比最高的选择介于两者之间的中间融合对每个模态先做局部编码再在融合层引入注意力机制。核心思想是让模型自己学习当前说话场景里到底哪个模态更值得信。import torch.nn.functional as F class AttentionFusion(nn.Module): def __init__(self, feat_dim128): super().__init__() self.text_attn nn.Linear(feat_dim, 1) self.audio_attn nn.Linear(feat_dim, 1) self.video_attn nn.Linear(feat_dim, 1) def forward(self, t, a, v): # 计算三个模态的注意力分数 scores torch.cat([ self.text_attn(t), self.audio_attn(a), self.video_attn(v) ], dim-1) # (batch, 3) weights F.softmax(scores, dim-1) # 归一化成权重 out weights[:, 0:1] * t weights[:, 1:2] * a weights[:, 2:3] * v return out这套注意力融合的逻辑非常直观每个模态先独立压缩到相同维度再各自学习一个标量重要性分数softmax 之后变成和为 1 的权重最后加权求和。参数上feat_dim 要和三个模态编码器的输出维度保持一致这要求前面每个单模态网络的输出维度都对齐到同一个值。实际实验里注意力融合的提升在某些样本上非常明显——比如文本内容偏正面但语气冷淡时注意力会把音频权重调高综合结果趋于中性偏消极。这是早期融合硬拼接很难自动实现的。5. 常见问题排查模态对齐、特征尺度与过拟合的三个现场5.1 现象跑第一个实验模型不收敛或 Loss 直接变成 NaN原因三路特征的数值尺度差异过大。BERT 输出的向量值范围大约在 -1 到 1 之间openSMILE 的响度特征可能是小数但如果用了未归一化的原始音高值或者视频帧像素值数值范围可能达到几百甚至上千。拼接后的大数值特征会在梯度更新时碾压其他模态。解决对每个模态的特征分别做标准化而不是拼接后统一标准化。按模态分别计算均值和标准差各自减均值除标准差。对文本模态可以不做标准化但音频和视觉特征强制做。检查方式是在训练前打印三路特征的均值方差做一次目检差异超过两个数量级就要处理。5.2 现象验证集效果尚可但测试集明显退化原因多模态模型容易过拟合到某一个强模态最常见的是文本模态。因为 BERT 的语义表征本身已经很强模型学到“只看文本就够了”的捷径视觉和音频分支沦为摆设。这不算模型坏了但显然没有实现多模态融合的初衷。解决一个有效手段是模态丢弃训练modality dropout。训练时随机把某一路特征以一定概率置零强迫模型不能依赖固定模态。概率通常设为 0.2 到 0.3。另一个思路是像 CH-SIMS 数据集那样利用模态级标签做辅助监督让每个模态分支都独立分类一次总损失是融合损失和各模态损失的加权和。做对比实验时可以消融这个概率参数观察不同设置下测试集的表现。5.3 现象早停无效训练集损失持续下降但验证集早就不涨原因一个隐蔽的数据泄漏源头。很多视频数据集的训练、验证、测试划分是按视频片段做的但同一说话人的多个片段可能同时出现在不同划分里。模型会隐式记住说话人的语音特征和长相情感识别变成了身份识别。解决查阅数据集官方划分方式确认是否需要按说话人划分。原始数据里如果带说话人 ID建议自己重新划分确保同一说话人的所有样本只在同一个集合里出现。这个问题的隐蔽性在于训练过程非常正常只有测试结果暴露出问题——常见表现是测试集比验证集差一大截且无法解释。5.4 现象空窗口补零后融合层输出异常原因文本窗口为空时补零的向量参与归一化后会把均值向零拉而且全零向量经过 Linear 层后偏置项会导致输出非零语义上相当于向模型输入了一个“假特征”。解决不要用零向量占位用一个可学习的占位向量或者在 loss 里对空窗口样本做 mask。如果数据量足够直接把包含空窗口的样本丢掉简单有效。如果空窗口占比高就得重新考虑对齐窗口的大小——窗口加长会减少空窗口数量但也会降低时间分辨率。6. 验证与进阶用模态级指标倒逼融合质量入门阶段最容易犯的错是只看最终 ACC 或 F1却没弄清楚这个数字是从哪个评估口径里出来的。CMU-MOSI 这类连续情感分数可以同时做回归和分类两种评估。回归用 MAE 和 Corr分类则要指定阈值——常见做法是把情感分数大于 0 视为积极小于等于 0 视为消极这是二分类口径。三分类口径则会把分数切成积极、消极、中性三档。同一份预测结果在不同口径下的指标差异可以相当大论文对比时一定要确认对方用的是什么划分。我习惯用多标签数据集的模态级独立指标来检查融合是否真的有效。以 CH-SIMS 风格的数据为例训练好的模型可以分别输入单模态特征得到每个模态单独预测的指标再输入融合特征看融合是否优于最强的单模态。如果融合后指标反而低于最优单模态说明融合策略没学好优先排查特征尺度和模态丢弃概率。from sklearn.metrics import accuracy_score, f1_score, mean_absolute_error # 假设 pred_reg 是回归模型输出的连续分数labels 是真实分数 mae mean_absolute_error(labels, pred_reg) # 二分类口径0 为积极 pred_bin (pred_reg 0).astype(int) true_bin (labels 0).astype(int) acc accuracy_score(true_bin, pred_bin) f1 f1_score(true_bin, pred_bin) # 输出单模态与融合的对比结果 print(f[文本] MAE{text_mae:.3f} Acc{text_acc:.3f} F1{text_f1:.3f}) print(f[音频] MAE{audio_mae:.3f} Acc{audio_acc:.3f} F1{audio_f1:.3f}) print(f[视觉] MAE{video_mae:.3f} Acc{video_acc:.3f} F1{video_f1:.3f}) print(f[融合] MAE{fusion_mae:.3f} Acc{fusion_acc:.3f} F1{fusion_f1:.3f})这一组指标能直观暴露出模态不平衡的问题。我跑过的实验里文本单模态 Acc 往往在 75% 上下音频 65%视觉 60%融合后如果能到 78% 就是有效的融合如果融合只有 74%说明模型其实在退化成纯文本模型。进阶用法里还有一招把注意力融合层的权重可视化统计每个样本上三个模态的权重分布。当数据集偏向某种情绪表达时比如愤怒多靠音频和视觉讽刺多靠文本和音频权重分布会有明显规律。这个分析既能为论文提供分析图也能在工程上报给产品团队解释为什么某些样本被判成消极。多模态情感分析的项目我前后带过三轮每轮都会在模态对齐和特征标准化两个环节返工。从那以后我每次跑新数据集都强制走一遍流程“先看数据划分方式再做单模态基线最后才碰融合”这一套做完能省下不少后期排查的时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站