☰
多模态情感分析入门:从三路特征融合到注意力机制实战
2026/10/17 14:47:57 网站建设 项目流程

简介:《多模态情感分析入门》是一本由清华大学出版社与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", sr=sr) num_windows = len(audio) // hop_length # 3. 对每个窗口提取音高和音量特征 def extract_audio_features(window_start): seg = audio[window_start*hop_length : (window_start+1)*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, padding=True, truncation=True, return_tensors="pt") 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_dim=300, hidden_dim=128): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim) self.lstm = nn.LSTM(embed_dim, hidden_dim, batch_first=True, bidirectional=True) 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, interval=0.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_dim=128, audio_dim=88, video_dim=512, hidden_dim=256, num_classes=1): 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_dim=128, audio_dim=88, video_dim=512, hidden_dim=128, num_classes=1): 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(dim=0)

早期融合的优点是实现简单,模型能自己学到模态间的交互。缺点是模态间尺度差异大时,数值大的特征会主导梯度,所以拼接前必须做归一化。晚期融合的每个模态独立建模,适合模态缺失场景(比如只有文本),但模型学不到跨模态的交互信息。

实际效果上,早期融合在 CMU-MOSI 上通常优于晚期融合,前提是特征做过标准化。晚期融合更适合部署环境不稳定的产品,因为某一路输入坏了,其他路照常能出结果。

4.2 中间融合:注意力机制是性价比最高的选择

介于两者之间的中间融合,对每个模态先做局部编码,再在融合层引入注意力机制。核心思想是让模型自己学习:当前说话场景里,到底哪个模态更值得信。

import torch.nn.functional as F class AttentionFusion(nn.Module): def __init__(self, feat_dim=128): 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%,说明模型其实在退化成纯文本模型。

进阶用法里还有一招:把注意力融合层的权重可视化,统计每个样本上三个模态的权重分布。当数据集偏向某种情绪表达时(比如愤怒多靠音频和视觉,讽刺多靠文本和音频),权重分布会有明显规律。这个分析既能为论文提供分析图,也能在工程上报给产品团队,解释为什么某些样本被判成消极。

多模态情感分析的项目我前后带过三轮,每轮都会在模态对齐和特征标准化两个环节返工。从那以后我每次跑新数据集都强制走一遍流程:“先看数据划分方式,再做单模态基线,最后才碰融合”,这一套做完能省下不少后期排查的时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询