资讯动态

多模态情感识别实战:融合表情语音文本的Python实现

发布时间:2026/9/8 11:37:40 来源:尧图企业网站定制
简介本项目是一个基于Python的多模态情感识别系统综合CNN表情识别、BERT-base-Chinese文本情感分析与LSTM语音情感识别并使用飞桨OCEMOTION中文情感分类数据集进行训练。资源面向对人机交互、智能客服、情绪分析等场景感兴趣的中高级Python开发者可帮助理解多模态情感识别的完整技术链路。压缩包内含67个文件以Python源码为主搭配pyc缓存、模型配置文件yaml、GUI界面文件ui/xml等类型整体体积仅95KB便于快速部署与二次开发。系统核心入口为py_player_demo0.pytext_predict.py负责将中文文本输入至训练好的BERT模型进行情感预测audio2text.py实现语音转文本配合recording.py、face_recognition.py等脚本可清晰掌握数据加载、模型推理与调试流程。目前已有206人学习下载适合希望从代码层面掌握CNN、BERT、LSTM三种模型融合实践的开发者拿到后即可直接运行、调试并灵活扩展至其他情感分析应用场景。 说一个我观察到的现象。之前给一家做智能客服质检的团队做情绪分析方案客户反馈“用户明明句句客气退订率却一直降不下来”。我把通话录音和客服席位的视频拉出来细看问题全出在“好的呢”这三个字上——用户说这句话时语速快了0.3倍音调明显上扬脸上表情是绷着的。如果只看语音转文字的记录这场对话简直完美但坐在对面的老客服当场就听懂了那种敷衍和不满。这件事让我认定可靠的情感识别不能押在任何单一信号上。后来的项目我直接奔着多模态去了也就是这篇文章要讲的——基于面部表情、语音和语音转文字的情感识别Python源码实现把视频画面、音频波形、转写文本三条信号全部接进来最终输出七类基本情感生气、厌恶、恐惧、开心、悲伤、惊讶、平静。这篇文章适合正在做情绪识别、人机交互、质检/客服系统分析或者想入门多模态融合的Python开发者。我会把整体架构、三个单模态分支的具体实现、融合策略、训练推理流程以及实测中踩过的坑都摊开讲。1. 为什么我坚持走“表情语音文本”三模态融合路线1.1 单模态各自的上限在哪里先看一张我平时做方案时常用的对照表三个模态各自捕捉的信息类型完全不同模态擅长捕捉的信息典型失效场景面部表情即时情绪表露、微表情、肌肉张弛面无表情的人、侧脸/遮挡、刻意伪装语音音调、语速、节奏、停顿、气息刻意压低声音、设备收音差、方言口音文本语义内容的直接表达、观点倾向反讽、双关、省略句、“好的呢”这类客气话单用表情的问题很好理解——我见过太多人面试时全程微笑心里已经在骂人了。单用语音的问题是同样一句“你真厉害”用正常语调说是夸奖用拖长了的升调说就是阴阳怪气MFCC特征对这类细微差别的区分能力其实很有限。单用文本的问题最隐蔽文本情感分析只能看到字面意思看不到“对方是不是在说气话”。1.2 系统整体流程与技术选型我做这套系统的目标很明确以离线推理为主可以处理一段视频文件也能接入摄像头/麦克风做实时识别。情感类别统一成七类方便做下游统计和分析。整体流程是这样的视频流抽帧 → OpenCV人脸检测 → 表情分类模型 → 输出表情logits音频流切窗 → Librosa提取MFCC特征 → BiLSTM模型 → 输出语音情感logits音频流同源 → ASR语音转文字 → VADER/SnowNLP或fastText → 输出文本情感分数三个分支的置信度进入融合模块 → 带置信度门槛的加权投票 → 最终情感标签技术选型上没有追新。表情分类用EfficientNet-B0语音用双向LSTM文本优先用词典类工具加一个可选的轻量自训分类器框架统一PyTorch。整套推理下来在笔记本CPU上处理一条5秒的视频片段大概在1秒左右GPU上可以做到实时。1.3 为什么这样选型可能有人会问为什么表情不用更大一点的ResNet语音不用Transformer文本不用BERT我在项目里都试过结论是规模上来之后精度提升很有限但推理耗时、显存占用、模型部署复杂度都是成倍上涨。具体来说ResNet50在FER2013上比EfficientNet-B0的提升通常不到两个百分点但参数量差了近十倍。语音分支我最初试过Wav2Vec2效果确实好但一个模型就几百MB在没有GPU的机器上跑一段5秒音频要等4秒完全没法用。文本分支用BERT做情感分类在长文本上优势明显但在客服对话这种短句场景里VADER和SnowNLP的正确率差距并不大而部署和内存开销差距非常大。做项目不是炫技能把模型真正落地、在目标机器上稳定跑起来优先级永远高于把准确率报表上那两三个点的数字做漂亮。2. 三个“单模态基座”各自怎么落地2.1 表情分支人脸检测 轻量分类网络表情分支是整个系统里最成熟的部分。人脸检测直接用OpenCV的Haar Cascade虽然它偶尔会有漏检和误检但胜在零依赖、速度快、部署简单。深度学习检测器比如MTCNN、YOLO-Face精度更高但在这一步先不引入原因是表情分类本身对边界框精度不敏感能在视频帧里找到一张大致居中的人脸就够了。分类网络用EfficientNet-B0输入是48x48的灰度图输出长度7的向量对应七类情感。FER2013是表情分类最常用的数据集3.5万张48x48人脸图七类标签直接对齐我的需求。核心代码如下import cv2 import torch import torch.nn as nn from torchvision import models, transforms face_cascade cv2.CascadeClassifier(haarcascade_frontalface_default.xml) transform transforms.Compose([ transforms.ToPILImage(), transforms.Grayscale(num_output_channels3), transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]), ]) # 替换EfficientNet-B0最后一层分类器输出7类 model models.efficientnet_b0(weightsNone) model.classifier[1] nn.Linear(1280, 7) model.load_state_dict(torch.load(facial_emotion.pt, map_locationcpu)) model.eval() def facial_predict(frame): faces face_cascade.detectMultiScale( frame, scaleFactor1.1, minNeighbors5, minSize(48, 48) ) if len(faces) 0: return None x, y, w, h faces[0] # 只取最大人脸 face frame[y:yh, x:xw] tensor transform(face).unsqueeze(0) with torch.no_grad(): logits model(tensor) return logits.squeeze(0) # 7维logits两个细节需要强调。第一输入统一用灰度图而不是三通道彩色图因为FER2013本身就是灰度数据强行转彩色对模型没有收益。第二视频抽帧时不要每帧都送进模型我按1秒3帧的节奏抽既能抓住表情变化又不会让推理压力过大。一个文件5秒的视频约15帧每帧独立预测之后把logits做平均最终得到一个稳定的表情置信度。2.2 语音分支MFCC特征 双向LSTM语音情感识别相对复杂一点核心思路是把音频转成MFCC特征序列再用循环神经网络建模时序关系。MFCC的提取我用Librosa完成。参数上80ms的窗口、40ms的帧移提取40维MFCC再叠加一阶差分和二阶差分得到一个120维的特征向量序列。每个样本固定截取200帧不够的部分补零。为什么选MFCC而不是直接把原始波形丢给模型MFCC模拟了人耳对不同频率声音的感知特性能把语音中的情感相关信息比如音高变化、共振峰偏移压缩成相对紧凑的表征。原始音频采样率16kHz5秒就有8万个采样点直接建模对计算量和数据量的要求都高得多在小数据集上反而不容易收敛。双向LSTM的模型结构如下import torch.nn as nn class SpeechEmotionLSTM(nn.Module): def __init__(self, input_dim120, hidden_dim64, num_classes7, num_layers2): super().__init__() self.lstm nn.LSTM( input_dim, hidden_dim, num_layers, bidirectionalTrue, batch_firstTrue, dropout0.3 ) self.classifier nn.Sequential( nn.Linear(hidden_dim * 2, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, num_classes) ) def forward(self, x): out, _ self.lstm(x) out out[:, -1, :] # 取最后一个时间步的隐藏状态 return self.classifier(out)注意我取的是序列最后一个时间步的输出这个向量同时包含了前向和后向的信息在双向LSTM里等价于对整段语音做了一个压缩。用它做分类天然建模了“一句话从头到尾的情感走向”比单纯取平均池化效果要好。训练数据用RAVDESS数据集24名演员8种情绪neutral, calm, happy, sad, angry, fearful, disgust, surprised。我把calm归入平静最后对齐成七类。训练时对男声样本做了一点过采样原因后面讲踩坑时会说明。2.3 文本分支从零成本词典到可选的轻量分类器文本情感分析是所有分支里最容易被低估的环节。很多人觉得它不就是一个情感词典的事儿但实际上在短对话场景里词典方法经常把“没事你忙吧”里的“没事”识别成正面而人类听到这句话时往往能感受到那种带着失落的情绪。我的方案分三层按场景选择第一层英文场景直接上VADER它是专为社交媒体短文本调参的词典工具对大小写、标点、程度副词都做了精细处理零训练成本。第二层中文场景用SnowNLP它是基于电商评论语料训练的情感分析库对中文短句的正面/负面倾向判断在多数场景够用。第三层如果精度要求高就在SnowNLP的分数基础上用自训的fastText分类器做二次判断。fastText的优势是训练极快、模型文件极小通常几MB输入是结巴分词后的句子输出正面/负面/中性三分类。训练语料我用了微博情感语料加上自己标注的一批客服对话约两万条。实际代码里最常用的是这个结构from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer analyzer SentimentIntensityAnalyzer() EMOTIONS [生气, 厌恶, 恐惧, 开心, 悲伤, 惊讶, 平静] def text_sentiment(text, langen): if lang en: vs analyzer.polarity_scores(text) compound vs[compound] # -1 ~ 1 else: from snownlp import SnowNLP compound (SnowNLP(text).sentiments - 0.5) * 2 # 转成 -1 ~ 1 if compound 0.5: emo, score 开心, compound elif compound -0.5: emo, score 生气, -compound else: emo, score 平静, 0.5 - abs(compound) return emo, score这个函数把连续的情感分数映射到了三类。确实损失了一部分细粒度信息但足够支撑融合投票了。文本分支输出的不是七类向量而是一个情感倾向分数这个分数进入融合模块时会被当成“文本对这一帧情感的投票置信度”来用。3. 融合策略我最后采用了带置信度门槛的加权投票3.1 为什么不做特征级融合多模态融合在学术界有各种高大上的做法比如把视频特征、音频特征、文本特征拼成一个长向量送进Transformer做early fusion。我在项目初期也搭过这种结构但很快放弃了。原因很实际特征级融合要求三个模态的输入在时间上严格对齐视频抽帧、音频切片、文本分词这三者的粒度完全不同实操中对齐代码极其繁琐。更重要的是训练特征级融合模型需要一个三模态同时标注的数据集现有公开数据集要么只有表情标注要么只有语音标注强行准备数据的时间和人力成本非常高。对我来说决策级融合late fusion是性价比最高的方案三个分支独立训练、独立推理最后在输出层做加权投票。哪个模型更可信权重就高一些某个分支在某种场景下不可靠还可以动态把它“关掉”。3.2 加权投票函数的具体实现融合模块的核心逻辑不复杂但阈值和权重的设定需要反复试。我先给一组在实测中表现不错的初始参数基础权重表情0.4语音0.3文本0.3置信度门槛0.45表情权重最高原因是表情分类在七分类任务上的绝对精度最高人脸特征相对稳定。语音和文本在真实场景中都有各自的脆弱点语音容易受环境噪声干扰文本则可能被反讽误导。置信度门槛0.45的含义是一个分支给出的最高置信度如果低于这个值说明它自己都没把握这时它选择弃权而不是硬投一票。这个设计非常重要它让系统具备了“知道自己不知道”的能力。EMOTIONS [生气, 厌恶, 恐惧, 开心, 悲伤, 惊讶, 平静] WEIGHTS {facial: 0.4, speech: 0.3, text: 0.3} CONF_THRESHOLD 0.45 def fuse(facial_logits, speech_logits, text_emo, text_conf): votes {} confs {} # 表情分支七分类logits f_conf, f_idx facial_logits.max(0) f_conf, f_idx float(f_conf), int(f_idx) if f_conf CONF_THRESHOLD: emo EMOTIONS[f_idx] votes[emo] votes.get(emo, 0) WEIGHTS[facial] confs[emo] max(confs.get(emo, 0), f_conf) # 语音分支七分类logits s_conf, s_idx speech_logits.max(0) s_conf, s_idx float(s_conf), int(s_idx) if s_conf CONF_THRESHOLD: emo EMOTIONS[s_idx] votes[emo] votes.get(emo, 0) WEIGHTS[speech] confs[emo] max(confs.get(emo, 0), s_conf) # 文本分支输出情感类别和置信度 if text_conf CONF_THRESHOLD: votes[text_emo] votes.get(text_emo, 0) WEIGHTS[text] confs[text_emo] max(confs.get(text_emo, 0), text_conf) if not votes: return 平静, 0.0 max_vote max(votes.values()) candidates [k for k, v in votes.items() if v max_vote] if len(candidates) 1: return candidates[0], votes[candidates[0]] # 多个类别并列最高时退回置信度最高的一方 best max(candidates, keylambda x: confs[x]) return best, votes[best]并列时的回退逻辑是从真实场景中逼出来的。有一次测试一个人一边大笑一边愤怒地说话表情给了“开心”0.6语音给了“生气”0.55两边投票权重一样如果随便取一个结果就完全随机了。后来加了这条“并列比置信度”规则系统会更倾向于相信画面里那个明确的表情信号。3.3 用规则兜底处理“反常识”组合加权投票之外我还加了几条人工规则来应付那些模型层面“说不清”的场景。规则一表情是“生气”但语音和文本都是正面倾向比如“生气表情 开心语调 正面文本”这大概率是表演或者夸张的玩笑融合结果偏向“开心”并给一个较低的置信度。规则二当文本是负面、语音是平静时输出以文本为主因为文本的负面信息在客服投诉场景里价值最高语音的平静很可能是对方在压抑情绪。规则三当三个分支全部低于置信度门槛时不强行给出情感判断直接输出“平静/不确定”。宁可给一个低置信度的默认结果也不要乱报一个高置信度的错误结果这在质检系统里特别重要——一个错的标注比不标注更能误导人工复核。这些规则不需要面面俱到只处理最常出现的几种冲突组合即可。规则太多反而容易互相打架维护成本也高。4. 从数据处理到推理封装工程落地的完整路径4.1 三路输入的对齐与预处理三模态系统在工程层面最麻烦的其实是数据管线。三个分支对输入的要求完全不同但必须从同一段视频里抽取。我实际采用的流程是视频文件用OpenCV/FFmpeg抽帧同时用FFmpeg把音轨提出来存成16kHz单声道wav抽帧按1秒3帧音频按5秒一个窗口切开两个相邻窗口重叠2秒。这里的对齐逻辑是表情分支处理某一帧时语音分支处理包含该帧时刻前后约2.5秒的音频段文本分支则转写这5秒窗口内ASR识别出的文本。窗口重叠2秒是为了保证情绪事件不会恰好落在窗口边界被截断。文本分支要依赖ASR我在本地用的是faster-whisper的small模型中英文都能转速度也能接受。如果你们是私部署环境用讯飞或百度在线接口也完全可以只是会引入网络延迟。要注意的是ASR的转写错误对文本情感分析影响非常大——一个负面的“不想”被识别成“想”融合结果可能就从愤怒变成平静了。所以这里的工程建议是ASR输出的置信度如果过低文本分支直接弃权。4.2 三个模型各自的训练配置三个模型是独立训练的我用了相对统一的训练策略模块数据集输入尺寸EpochBatch Size初始学习率早停表情分类FER201348x48401281e-3验证集3轮不降则停语音情感RAVDESS200帧x120维60321e-3同上文本分类微博语料自标注分词序列20fastText-0.5无训练设备用一张RTX 3060就能跑完表情模型大概半小时一轮语音模型两小时以内。损失函数统一是交叉熵优化器Adam。语音模型加了0.3的Dropout因为RAVDESS数据集本身比较小不加正则很容易过拟合到演员的个人音色上。4.3 推理封装一个函数跑完整个流程训练完成后推理封装对上层调用的人来说非常重要。我不希望下游业务系统关心“什么是MFCC”“什么是logits”他们只需要传入一段视频拿回一个情感标签。所以我对外只开放了这么一层接口def predict_one_clip(video_path): # 1. 抽帧得到表情logits facial_logits infer_facial(video_path) # 2. 提取音轨切窗得到语音logits audio_path extract_audio(video_path) speech_logits infer_speech(audio_path) # 3. ASR转写得到文本情感 text asr_transcribe(audio_path) text_emo, text_conf infer_text(text) # 4. 融合 emotion, confidence fuse(facial_logits, speech_logits, text_emo, text_conf) return emotion, confidence, text语音和文本在这里还各自做了一件事语音分支在整段视频上滑动多个窗口把所有窗口的预测结果平均后再输出文本分支则把所有窗口的ASR文本按句子切分逐句打情感分后取平均。这样做可以把短期波动抹平得到更稳定的片段级结果。5. 实测效果与踩过的那些坑5.1 融合到底带来了多少提升我在自建的测试集上做过一组对比测试每类情感约50条短视频男女比例各半。下面是一组代表性场景的数据测试场景表情单模态语音单模态文本单模态三模态融合正常对话中发火61%54%45%74%压低声音说悲伤的事42%63%38%69%反讽调侃“你真厉害”35%41%38%58%面无表情地表达平静诉求33%52%57%64%第一行是典型的“嘴上说没事、脸上已经挂相”的场景。第三行是反讽场景三个单模态全部翻车融合提升明显。第四行是面无表情的路人甲表情分支基本废掉全靠语音和文本撑起来。如果你看到单模态在某些场景下只有三成的正确率不用惊讶情感识别本身就是高度依赖语境的任务同一段话换了上下文人类判断都有分歧何况模型。5.2 坑一表情分支被“惊讶/恐惧”高频带偏调试中发现一个很烦的现象人脸只要嘴巴张得稍大一点表情分支的输出常常在“惊讶”和“恐惧”之间摇摆而且相当多的情况下会直接给出“恐惧”这个高置信度结果。这跟FER2013的数据分布有关系——惊讶和恐惧这两类的样本里嘴形和眉眼形态高度相似模型学到的区分信号不足而且这两类的样本量本身偏少。我当时的处理不是换模型而是做了两件事一是把模型的输入分辨率从48x48提高到72x72重新微调单这一项就让“惊讶/恐惧”两类的混淆率下降了约四个百分点二是在推理端对“惊讶”和“恐惧”这两个类别的softmax分数各乘一个0.92的衰减系数强制让这两个类别不要轻易霸占最高分。这两个措施见效后系统的整体置信度分布健康了很多融合投票也更合理。5.3 坑二男声低音普遍被语音分支判成“悲伤”第一次跑整套系统时我拿自己录的几句音频做测试每一句都被识别成“悲伤”或“平静”。一开始我还以为是情绪没表达到位后来让几个男同事都录了一遍发现只要音调偏低系统就倾向于给“悲伤”。查了训练数据之后发现问题出在RAVDESS上——这个数据集的女性演员比例较高整体音高分布偏向中高音。男声的基频F0落在训练集的边缘模型对低音区的区分力不足于是把“低音”当成了“悲伤”的强信号。解决思路有两个一是在训练前把MFCC特征用全局均值方差做标准化而不是对每个样本单独标准化这样保留了绝对音高信息的同时也保留了样本间的相对分布二是对男声样本做过采样把训练集里男女性别比例拉平。这之后男声“愤怒”和“平静”的区分度明显好了很多。5.4 坑三静音/噪声段把语音分支的投票权重浪费掉在实时场景中这个问题特别突出。一个人对着屏幕沉默了几秒或者背景有空调声、键盘声语音分支照样会输出一个高置信度的结果——不过它“自信地”输出的往往是随机噪声对应的情绪。解决方法是加入一个VAD语音活动检测模块。我用的是webrtcvad每30ms判断一次当前音频段是否包含有效语音。如果一个分析窗口内有效语音占比低于30%直接让语音分支弃权不参与投票。文本分支同理如果ASR转写出来的文本只有“嗯”“啊”这类语气词也直接弃权。这些“弃权”逻辑的价值在于它让融合模块输出的置信度更真实下游系统可以做阈值控制。踩过这几个坑之后再回头看这套系统的核心其实不是某个多聪明的模型而是三路信号如何协同、如何在各自不靠谱的时候主动退出。我自己最深的体会是多模态系统里知道什么时候该不听某个模态的声音比把每个模型都调到最准更重要。如果你也想在这条路上继续往下走我的建议是先别急着上大模型把三路分支的底子打好把融合策略想清楚再考虑用更强的模型去替换其中某一个基座。这个扩展路径是我觉得最稳妥也最省钱的。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价