简介:面向深度学习多模态情感分析方向的完整算法源码与说明包,适合计算机、数学、电子信息等专业学生用于课程设计、期末大作业或毕业设计,也适合对情感分析感兴趣的初学者开展实战演练与算法复现。资源共36个文件,核心为Python源码,覆盖模型结构定义、层次模块实现、参数配置、训练与测试等环节;同时配有Markdown格式的多模态情感分析论文笔记和算法说明,并附29张运行过程、模型结构或结果可视化截图,便于对照文字理解代码实现思路。整体压缩包仅6.7MB,目录结构清晰,下载后可直接运行调试。目前已有153人浏览学习,参考价值得到初步验证。借助源码、笔记与截图,读者可获得完整的多模态情感分析项目方案,掌握文本与图像等模态特征的融合处理方法,梳理从数据处理、模型搭建到效果评估的完整流程,为课程项目或毕业设计的深化改进提供扎实基础。
1. 多模态情感分析:从模态割裂走向统一建模的那一步
一段客服录音里,用户嘴上说着“好的,谢谢你”,声音却发紧,视频画面里眉头还拧着。单模态模型会给出三个互相矛盾的结论——文本判正面,语音判紧张,人脸判不悦。多模态情感分析就是把这些信号放进同一个模型,用深度学习统一建模,输出一个更接近人直觉的综合判断。它不执着于把每个模态单独做到极致,而是吃透模态间的互补与冲突,这正是它与情感分析最本质的差别。这个方向适合做短视频内容审核、直播情绪预警、智能客服质检和人机交互体验分析的人。标题里的 LW 不是某个框架缩写,而是 LightWeight——最终方案要轻量、可落地、能压进边缘设备。下文按三模态特征提取、融合策略、训练调参、翻车避坑、轻量化部署一路讲到底,每一步都按可复现的工程方式拆开。
2. 三模态特征提取:把文本、音频、人脸变成可融合的向量
融合的前提是每个模态都有稳定的数值表示,而不是把原始波形、像素直接塞给模型。这一章分别讲文本、音频、人脸三个模态的常见工程做法和取舍。文本侧要处理中文语境里的反讽和转折,音频侧要在手工特征和预训练特征之间选边,人脸侧要解决抽帧、检测、对齐这一串前置问题。
2.1 文本模态:轻量预训练编码器与截断策略
文本是情感分析里信息密度最高的模态,显式情绪词(“气死我了”)和反讽表达(“你可真棒”)都需要上下文建模。词向量加 CNN 的做法对反讽基本无能为力,常见做法是直接用中文预训练模型,比如 hfl/chinese-roberta-wwm-ext 这类 BERT 家族模型。LW 场景下可以换成蒸馏版本或者 sentence-transformers 里的小型多语言模型,输出维度从 768 降到 384,融合层参数也随之变少。
下面这段代码把一条文本编码成 768 维向量,核心是 mean pooling 而不是直接取 CLS 向量。
from transformers import AutoTokenizer, AutoModel import torch model_name = "hfl/chinese-roberta-wwm-ext" # 中文预训练,鲁棒性好 tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModel.from_pretrained(model_name) model.eval() def text_to_vector(text: str, max_len: int = 128): inputs = tokenizer( text, max_length=max_len, padding="max_length", truncation=True, return_tensors="pt", ) with torch.no_grad(): outputs = model(**inputs) # 用 last_hidden_state 做 mean pooling,比直接取 CLS 对短文本更稳 mask = inputs["attention_mask"].unsqueeze(-1).float() vec = (outputs.last_hidden_state * mask).sum(dim=1) / mask.sum(dim=1) return vec.squeeze(0) # 768 维mean pooling 把整句话的语义均匀地压进一个向量,不会出现 CLS 向量在某些任务上对首 token 过敏感的问题。max_len 按实际任务调整:短视频弹幕和客服短句 128 够用,长评论建议提到 256。这里有个容易忽略的点:如果文本来自 ASR,必须保留每句话的时间戳,后面与音频、视频帧对齐时靠它定位。截断策略也很关键,默认 truncation 会直接砍掉句子尾部,长句里的转折词一旦被截掉,情感极性可能反转,具体现象在避坑章节里展开。
2.2 音频模态:eGeMAPS 低层描述符为什么仍是落地首选
音频情感特征主要有两条路线。一条是 openSMILE 提取的 eGeMAPS 低层描述符,包含音高、能量、抖动、闪烁等 88 维统计量,提取速度快、不依赖 GPU、在端侧也能跑,缺点是缺少对语义语境的理解。另一条是 wav2vec2、HuBERT 这类预训练模型的深层特征,表征能力强但对算力要求高,LW 项目里通常只做对比实验,不直接上生产。实际落地时,我会先用 eGeMAPS 跑通整套流程,如果准确率不够再替换预训练特征,这样每一层的调试成本都可控。
openSMILE 的提取命令很直接:
SMILExtract -C config/eGeMAPSv02.conf \ -I sample.wav \ -O sample.csv \ -appendcsv 0 \ -csvSeparator ;-C 指定配置文件,eGeMAPSv02 是官方标准配置;-I 和 -O 分别指定输入音频和输出 CSV;-appendcsv 0 表示每次覆盖而不是追加;-csvSeparator 用分号分隔列,避免和特征值里的逗号冲突。默认配置下窗口是 25ms、步长 10ms,输出按帧排列。这个 25ms 窗口和视频常用的 25fps 帧率天然对齐,一帧窗口正好对应一帧画面,后面做多模态特征文件就会轻松很多。如果录音环境嘈杂,建议在提取前先加一步 VAD 和人声增强,否则音量、噪底这些统计量会被环境音带偏,情感特征被污染之后融合层怎么调都救不回来。
提取出的 CSV 在 Python 里读进来后,先做 z-score 归一化再存成 npy。注意 openSMILE 原始输出里可能带非数值标记行,读取时要按行过滤。
2.3 人脸模态:用 MTCNN 加轻量表情模型提取视频帧特征
人脸模态的核心流程是抽帧、检测、对齐、特征提取。视频里表情变化比语音慢,不需要每帧都处理,常见做法是每秒钟抽 5 帧,既保留表情轮廓又避免大量重复计算。每一步都要考虑失败兜底,尤其在低质量视频里,侧脸、遮挡、运动模糊都会让检测器失灵。下面这段代码用 MTCNN 从单帧里取出人脸区域。
import cv2 from mtcnn import MTCNN detector = MTCNN(min_face_size=20, steps_threshold=[0.6, 0.7, 0.7], scale_factor=0.709) def extract_face_roi(frame, cache=None): dets = detector.detect_faces(frame) if not dets: return cache, None # 检测失败,复用上一帧结果 box = dets[0]["box"] x, y, w, h = box # 扩边 10% 避免把额头/下巴裁掉 margin = int(0.1 * max(w, h)) x, y = max(0, x - margin), max(0, y - margin) x2, y2 = min(frame.shape[1], x + w + margin), min(frame.shape[0], y + h + margin) roi = frame[y:y2, x:x2] roi = cv2.resize(roi, (112, 112)) # 模型输入尺寸 return roi, {"box": box}这里只取置信度最高的人脸框。多说话人场景下可以按画面面积和中心距离排序,挑最主要的那个人;cache 参数用于连续丢帧时复用上一帧的人脸结果,避免特征序列出现空洞。扩边 10% 是为了不让检测框把额头或下巴切掉,这看起来是细节,但对表情识别的影响很明显。112x112 是 MobileFaceNet 一类轻量表情模型的标准输入尺寸,输出 embedding 通常是 128 或 512 维。另一个可用方案是 CLIP 的图像编码器提取通用视觉特征,但通用特征对表情的细粒度差异不敏感,做视频人物情感分析时我更倾向用专门的表情模型。
2.4 三模态特征对齐:时间戳统一与特征文件格式
三个模态单独提完之后,必须落在同一个时间轴上才能进融合层。常见做法是以视频帧时间戳为基准:音频特征按 25ms hop 对应到每一帧,文本按 ASR 的 start 和 end 落到帧区间,人脸特征随抽帧位置落位。下面是按 clip 对齐的示例。
import numpy as np def align_features(clip_start, clip_end, text_vec, audio_feat, face_emb, fps=25): # audio_feat 按 25ms hop 提取,face_emb 对应每一视频帧 n_frames = int((clip_end - clip_start) * fps) audio_frames = audio_feat[clip_start * 40:clip_end * 40] # 每秒 40 帧音频特征 face_resampled = np.repeat(face_emb, max(1, n_frames // len(face_emb)), axis=0)[:n_frames] return { "text": text_vec, "audio": audio_frames.mean(axis=0), # 展平到 clip 级 "face": face_resampled.mean(axis=0), }对齐后的特征以 clip 为单位保存成 npy 或 h5 文件,每条记录包含 text、audio、face 三个向量以及缺失标记。这里把音频和人脸都做了均值池化,好处是实现简单,坏处是会把瞬间的愤怒、惊慌抹平,第 5 章会专门讲这个问题。特征文件命名建议带视频 ID、起始时间戳和说话人 ID,后面做验证集划分时按说话人分组,能避免数据泄漏。
3. 融合策略选型:早期拼接、晚期平均与跨模态注意力谁更稳
特征提完之后,融合层直接决定多模态情感分析的天花板。常见做法有三种:早期把三个向量拼在一起过分类器、晚期对三个模态的预测做平均或投票、中间用注意力机制让模态之间先做交互再分类。三者没有绝对优劣,但适用场景完全不同。
3.1 早期拼接为什么不推荐直接 concat
直接把文本 768 维、音频 88 维、人脸 512 维拼起来是最快的方案,但至少有三个问题。第一,向量维度和量纲差距悬殊,文本那一路在梯度里占绝对主导,音频和人脸几乎成了摆设。第二,三个模态的关系不是简单相加,文本说“不错”但音频语气讽刺,这种冲突需要模态间显式交互才能建模,直接拼接学不到。第三,在小数据集上直接 concat 高维向量极易过拟合,验证集准确率虚高,上线就翻车。
改进方式是先做投影对齐,把三个模态映射到同一个低维空间再拼接。投影层可以用单层线性加 LayerNorm,也可以用后面要讲的非对称注意力。这里给一个最小可跑的早期拼接基线,作为后续实验的对照:
import torch.nn as nn class EarlyConcat(nn.Module): def __init__(self, dims=[768, 88, 512], proj_dim=256, num_classes=7): super().__init__() self.proj = nn.ModuleList([ nn.Sequential(nn.Linear(d, proj_dim), nn.LayerNorm(proj_dim)) for d in dims ]) self.classifier = nn.Linear(proj_dim * 3, num_classes) def forward(self, text_vec, audio_vec, face_vec): x = torch.cat([ self.proj[0](text_vec), self.proj[1](audio_vec), self.proj[2](face_vec) ], dim=-1) return self.classifier(x)proj_dim 取 256 是为了和后面的跨模态注意力共用一套维度,方便做消融对比。这个基线的作用不是拿来做最终模型,而是验证融合层是否真的带来了增益——如果跨模态注意力在自建验证集上打不过这个基线,说明问题出在特征侧或数据侧,而不是融合结构不够高级。
3.2 跨模态注意力融合的 PyTorch 实现
跨模态注意力的思路是用一个模态的特征去查询其他模态的特征,让文本能“看到”音频和视频里和它矛盾的信息。落实到工程上,可以做成一个轻量 transformer 层,query 来自主模态,key 和 value 来自被查询模态。下面是按这个思路实现的单层跨模态注意力模块。
import torch import torch.nn as nn import torch.nn.functional as F class CrossModalAttention(nn.Module): def __init__(self, d_model=256, nhead=4, dropout=0.1): super().__init__() self.q_proj = nn.Linear(d_model, d_model) self.k_proj = nn.Linear(d_model, d_model) self.v_proj = nn.Linear(d_model, d_model) self.norm = nn.LayerNorm(d_model) self.dropout = nn.Dropout(dropout) self.nhead = nhead def forward(self, query, kv, mask=None): B, D = query.shape # 投影并拆头 q = self.q_proj(query).view(B, self.nhead, D // self.nhead) k = self.k_proj(kv).view(B, self.nhead, D // self.nhead) v = self.v_proj(kv).view(B, self.nhead, D // self.nhead) scores = (q * k).sum(-1) / (D ** 0.5) # 点积注意力 if mask is not None: scores = scores.masked_fill(mask, -1e9) attn = F.softmax(scores, dim=-1) out = (attn.unsqueeze(-1) * v).sum(dim=1) return self.norm(query + self.dropout(out))使用时可以对每个模态各查一次其他两个模态,例如 h_text_attn = CrossModalAttention()(text, concat(audio, face)),然后把三个注意力输出和原始投影向量在特征维度上拼接,再过一层 FFN。mask 参数用来屏蔽缺失模态,缺失模态对应位置的 scores 直接置为极小负值,这样注意力不会把权重分给它。nhead 取 4 而不是 8,是因为三模态特征维度只有 256,头数太多会把每个头的维度切得太碎,学不到稳定的交互模式。残差连接在这里不是可选项——去掉之后 attention 输出的分布偏移会让后面的分类层很难收敛。
3.3 门控动态融合与模态缺失兜底
跨模态注意力解决的是“模态间如何交互”,门控融合解决的是“这个样本该信谁”。不同视频的质量差异很大,有的画面全程清晰,有的后半段人脸完全失焦;有的音频干净,有的背景音乐盖过人声。门控机制让模型根据输入特征动态分配三个模态的权重,缺失模态的置信度天然会低,门控会自动忽略它。下面是一个带门控的分类头实现。
import torch import torch.nn as nn class GatedFusion(nn.Module): def __init__(self, d_model=256, num_classes=7): super().__init__() self.gate = nn.Linear(d_model * 3, 3) self.classifier = nn.Linear(d_model * 3, num_classes) self.temperature = 1.0 # 可调,控制门控分布的锐利程度 def forward(self, h_text, h_audio, h_face, mask=None): h = torch.cat([h_text, h_audio, h_face], dim=-1) gate_logits = self.gate(h) / self.temperature if mask is not None: # mask 为 0 表示该模态缺失 gate_logits = gate_logits.masked_fill(mask == 0, -1e9) gate_weight = torch.softmax(gate_logits, dim=-1) weighted = gate_weight[:, 0:1] * h_text + gate_weight[:, 1:2] * h_audio + gate_weight[:, 2:3] * h_face return self.classifier(torch.cat([weighted, h], dim=-1))temperature 初始设为 1.0,训练后期可以衰减到 0.7,让门控决策更自信。注意门控和分类器共用输入 h,但门控只用三个模态的投影向量做注意力打分。如果某个模态整体质量差,门控权重会趋向均匀分布,这时要检查特征侧而不是怪融合层——门控没有能力凭空创造信息,它只能压制噪声。多模态融合论文里经常把这套叫统一处理框架,落到工程上就是一套投影、交互、门控的分类头,复杂度不高,但每一步的细节都影响最终性能。
4. 训练调参:损失函数、采样策略与三个必调超参数
融合模型搭好之后,训练策略往往决定它能发挥几成实力。多模态情感分析的数据集天然存在两个麻烦:类别不均衡和样本总量小。公开数据集如视频情感分析基准多数只有几千条,特定领域的数据像 Bird1445 这类公开数据集又很难直接迁移到人脸情感场景,自采数据仍是主流。在这样的数据条件下,损失函数和采样策略比模型结构更能稳定带来涨点。
4.1 损失函数设计:Focal Loss 处理长尾情感分布
中文视频弹幕和客服录音里,“中性”和“开心”占比很高,“厌恶”“恐惧”这类样本常年只有前者的十分之一。交叉熵损失在长尾分布下会把绝大多数梯度分配给头部类别,导致尾部类别的召回率极低。Focal Loss 通过给易分类样本降权来解决这个问题,实现很简洁。
import torch import torch.nn as nn import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, alpha=None, gamma=2.0): super().__init__() self.alpha = alpha # 类别权重,一维 tensor self.gamma = gamma def forward(self, logits, targets): ce = F.cross_entropy(logits, targets, reduction="none") pt = torch.exp(-ce) # 模型对正确类别的置信度 focal_weight = (1 - pt) ** self.gamma if self.alpha is not None: alpha_w = self.alpha[targets] focal_weight = focal_weight * alpha_w return focal_weight.mean()gamma=2.0 是默认推荐值,太大容易让训练不稳定,太小就退化成普通交叉熵。alpha 可以从训练集统计得到:alpha_i = N / (C * n_i),N 是总样本数,C 是类别数,n_i 是第 i 类的样本数。这里有一个血泪经验:alpha 和 gamma 要一起调,只调 alpha 不调 gamma 会让头部类别的准确率明显下降,因为降权太凶狠了。调参时先固定 gamma=2.0 把 alpha 跑三组,再固定最优 alpha 把 gamma 从 1.0 到 3.0 扫一遍,总共四五个实验就能定下来。
4.2 类别不均衡的采样策略:不换模型也能涨点
除了在损失函数上做文章,采样策略也是常见手段。WeightedRandomSampler 让每个 batch 里小类别的出现频率提升,实现简单,效果直接。下面这段代码按类别数量反比计算采样权重。
from torch.utils.data import WeightedRandomSampler, DataLoader def make_sampler(labels, multiplier=2.0): class_counts = torch.bincount(labels, minlength=7).float() weights = 1.0 / (class_counts + 1e-6) sample_weights = weights[labels] ** multiplier return WeightedRandomSampler( weights=sample_weights, num_samples=len(labels), replacement=True, )multiplier 控制采样权重的放大倍数,取 1.0 是最温和的版本,取 2.0 会明显拉高低资源类别的出现频率。replacement=True 意味着同一个样本可能在同一个 epoch 里多次出现,配合数据增强一起用效果更好,否则模型会对重复样本过拟合。文本增强可以用同义词替换,音频增强加随机噪声和音量扰动,人脸增强做亮度、对比度和水平翻转。增强强度不要开太高,音频加噪过猛会让模型把噪声当成情感特征,验证集上不掉点、上线后一遇真实噪声就崩。
4.3 三个必调的超参数
多模态情感分析训练里,下面这三个参数对结果影响最大,建议每次实验都记录下来。
| 超参数 | 推荐范围 | 实验方向 |
|---|---|---|
| 学习率 | 融合层 1e-3 到 3e-3,预训练部分 1e-5 到 5e-5 | 融合层和预训练部分必须分开设,统一用 3e-5 会让融合层学不动 |
| Batch size | 16 到 32,显存不够时用梯度累积到等价 batch | 小 batch 配合高学习率容易震荡,多模态数据本身噪声大,稳妥为上 |
| 融合层 dropout | 0.1 到 0.3 | 0.1 起步,过拟合明显再往上加,加太多会让模态交互被抹平 |
学习率设置是最容易踩坑的地方:预训练模型部分确实要用小学习率微调,但融合层是随机初始化的,给它也设 1e-5 会让整个模型几个月都练不上去。常见做法是用两个参数组,预训练部分挂 warmup 后线性衰减,融合层用恒定小学习率,或者用一个小的 warmup 阶段。warmup_ratio 取 0.1,weight_decay 取 0.01,fusion dropout 从 0.1 开始试。如果训练曲线显示验证 loss 一路走高而训练 loss 还在降,那基本就是 dropout 不够或数据增强强度不够,先改这两个再动模型结构。
5. 避坑指南:多模态情感分析里最容易翻车的 5 个现场
这个章节按“现象、原因、解决”三条线写,都属于项目里真实发生过的翻车现场,希望下面的经验能帮你省掉几周的调试时间。
5.1 文本截断丢掉转折词:长句被砍掉“但是”之后情感反转
现象:验证集上模型把“电影不错,但是节奏拖沓,看得我快睡着了”判成正面,而且相当自信。检查文本预处理后发现,max_len=128 的截断策略直接砍掉了句子后半段,转折词和负面评价全在截断范围里。
原因:默认 truncation=True 是保留头部截断尾部,对长文本情感分析来说,情感极性经常藏在句子后半段,截断等于把关键证据提前删掉。
解决:把 max_len 提到 256,同时改成 head+tail 的截断策略,保留前 64 个 token 和后 128 个 token,中间部分直接丢弃。这个改动在许多长句样本上能立刻看到效果,融合层的输入文本特征不再被系统性误导。如果还要处理更长的文本,可以按句切分后对每句单独编码,再对整段的句子向量做 mean pooling,但那样计算量会成倍上涨,LW 场景不推荐。
5.2 音频特征把瞬时爆发抹平:全段均值让“愤怒”变成“平静”
现象:一段视频里人物前两秒平静、后两秒爆发怒吼,模型却给出“中性”的预测。检查对齐后的音频特征,发现整段六秒的 eGeMAPS 特征被平均成一个向量,瞬间的愤怒能量在均值里被稀释得无影无踪。
原因:特征对齐时为了省事,对整段做 mean pooling,这个操作把时间维度上的情绪变化全部压缩掉了。情感不是静态属性,它是随时间波动的信号,均值池化等于人为制造信息丢失。
解决:把 clip 切得更细,比如切成 1 秒的小窗口,每个窗口保留一组统计特征,模型按时间步做融合,让注意力机制自己决定哪一帧更重要。窗口切太小也不行,0.5 秒窗口里的音高统计不够稳定,抖动值会失真。短视频项目里 1 秒窗口是折中之后比较稳的选择,音频特征按 25ms 帧抽取后,每 40 帧一组统计量,刚好对应 1 秒。
5.3 人脸检测失败时硬填零向量:缺失模态污染主模态输出
现象:视频后半段人物侧脸,MTCNN 检测不到人脸,特征提取返回了 512 维的零向量。融合模型直接把这堆零向量参与 attention 计算,门控机制被打乱,原本清晰的文本负面信号也被拖累,预测结果飘忽不定。
原因:零向量在 attention 里会产生一个虚假的“匹配信号”,模型不知道该不该信任它。门控虽然理论上能学会忽略缺失模态,但零向量还是有数值梯度,训练时容易被带偏。
解决:缺失模态不传零向量,而是传一个可学习的 [MISSING] 嵌入,同时把对应的 mask 置 0,参与 attention 和门控计算时直接屏蔽。第 3.2 节的 CrossModalAttention 里 mask 参数就是这个用途。另外可以在检测失败时复用上一帧的人脸特征,但要加一个时效上限,超过 2 秒的旧特征宁可标记缺失也不要用,否则人脸模态等于在胡说八道。
5.4 类别不均衡导致愤怒样本全被预测成中性
现象:验证集整体准确率 82%,看起来不错,但细看混淆矩阵,“愤怒”类别的召回率只有 21%,几乎全被预测成了“中性”。进一步统计训练集发现中性样本占比超过 60%,普通交叉熵在训练时完全被头部类别主导。
原因:交叉熵对每个样本同等对待,头部类别的梯度累积效应会让决策边界整体偏向多数类。这个现象在情感分析里比图像分类更严重,因为情感类别之间的边界本身就很模糊,中性处在所有极性类别中间,容易吞掉邻近类别。
解决:先在数据层面动手,用 WeightedRandomSampler 把每个 batch 里的类别比例拉到接近均衡;再加 Focal Loss 对难样本重点学习。两个方法叠加后,愤怒类别的召回率通常能提升 15 到 25 个百分点,代价是中性类别的精确率会有小幅度下降。如果对精确率指标有硬性要求,可以在门控融合输出后加一个人为的阈值偏移,但这个做法是在掩盖数据问题,不推荐作为最终方案。
5.5 验证集划分泄漏:同一说话人同时出现在训练和验证集
现象:训练时验证 F1 一直稳定在 85% 以上,准备上线时拿了一批新主播的录音测试,F1 直接掉到 58%。回头检查数据划分,发现当时按文件随机切分,同一个主播的视频片段被同时分进了训练和验证集,模型其实是在“认人”而不是“认情绪”。
原因:多模态数据里同一说话人的语气、音色、表情习惯高度一致,随机切分会让验证集严重高估模型对新人的泛化能力。音频特征尤其明显,音色这个跟情感无关的维度反而变成了模型的最强信号。
解决:按说话人 ID 或者按视频 ID 做 GroupKFold 划分,保证同一个人的所有片段只在训练集或只在验证集。这个改动会让验证集指标瞬间掉几个点,但它是真实泛化能力的体现。如果项目里多人同框,还需要按时间窗口去重,避免相邻帧的画面相似度过高造成的隐式泄漏。新手最容易在这个环节翻车,因为训练曲线看起来一切正常,坑全埋在下游上线阶段。
6. LW 轻量化收尾:把融合模型压到能部署的状态
LW 不能只停留在口头上。模型在 GPU 上跑得再好,压不到目标硬件上就是白做。下面从导出格式、量化精度和推理缓存三个方向给出落地路径。
6.1 ONNX 导出与 INT8 量化
PyTorch 模型先导出为 ONNX,再交给 ONNX Runtime 做 INT8 量化,是成本最低的轻量化路线。
torch.onnx.export( model, (text_vec, audio_vec, face_vec), "mma_model.onnx", opset_version=17, input_names=["text", "audio", "face"], output_names=["logits"], dynamic_axes={"text": {0: "batch"}, "audio": {0: "batch"}, "face": {0: "batch"}} )opset 版本建议 17 以上,对 LayerNorm 和 Attention 算子的支持更完整;dynamic_axes 按 batch 维度设置动态轴,方便流式推理时单条请求也能跑。导出后用 onnxruntime quantization 工具做 INT8 量化,量化后模型体积通常能压缩到原来的四分之一到三分之一,推理时间在 CPU 上也有明显下降。INT8 会让最终准确率掉 1 到 2 个点,这是在轻量化目标下能够接受的代价。
6.2 流式推理缓存:避免重复提取特征
实时场景里,每 250ms 出一次预测结果,如果每次都重新提取三模态特征,算力会被浪费在重复计算上。常见做法是维护一个特征缓存队列:人脸特征按帧存,音频特征按 100ms 窗口存,文本按句存;新预测只对新增时间片的特征做提取,历史缓存直接复用。这个设计能让推理耗时从几百毫秒降到几十毫秒,是 LW 落地里最关键的一步。
6.3 用混淆矩阵决定下一次迭代方向
我一般会在交付前跑一次全量测试集,把混淆矩阵打印出来逐类看。有一次发现所有“惊讶”都被分到了“恐惧”,后来查标注记录才发现是数据标注标准本身就混淆了这两种情绪,遇到这种情况我不会去调模型结构,而是先回看数据。多模态情感分析里模型结构能提供的增益远小于数据质量带来的影响,把每一个误分类样例翻出来看,比换更花哨的注意力机制有用得多。希望这些踩过的坑能帮你少走一段弯路。
本文还有配套的精品资源,点击获取