ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

多模态情感识别实战:融合表情、语音与文本的实时情绪分析系统

多模态情感识别实战:融合表情、语音与文本的实时情绪分析系统 简介基于面部表情、语音和语音转文本的多模态情感识别Python源码面向有深度学习基础的学生与开发者提供一套集成CNN表情识别、BERT中文文本情感分析和LSTM语音情感识别的可运行项目覆盖从视频、语音到文本的完整情感判断流程。压缩包共67个文件包含28个Python脚本、23个pyc编译文件、6个XML界面配置和4个YAML模型参数等整体约95KB源码内部按GUI播放器、人脸检测、语音识别、文本预测等模块划分目录清晰便于阅读与二次开发。项目从主入口脚本启动配套文本预测脚本和语音转文本脚本分别调用BERT模型完成中文情感推理、通过语音识别转换为文本后进入情绪分析并以飞桨OCEMOTION中文情感分类数据集为基础训练验证附有多个配置文件和示例程序。目前已有206人学习或下载适合作为课程设计、毕业设计或情感分析方向的技术参考帮助快速掌握多模态情感识别工程实现。1. 项目概述与整体思路1.1 为什么做多模态情感识别这些年做情感识别相关项目的同学越来越多但只做单一模态很容易卡瓶颈。只看面部表情对方可以面无表情地说话表情识别直接失灵只听语音环境噪音一大声学特征全被污染只做语音转文字文字里的“还行吧”可能是真满意也可能是强忍不满。三路信号都接入互相补短板整体判断才稳定。这个项目的核心就是把这些融合起来摄像头捕捉面部表情麦克风采集说话语音同时把语音转成文字再把三个信号的结果做一次综合打分。输入是视频流加音频流输出是情绪标签比如开心、平静、生气、难过、反感、恐惧、惊讶。适合想走通多模态识别全流程的同学参考也适合做情绪辅助判断的原型验证。1.2 技术选型与模块架构我对这套系统的选型原则只有一条能离线跑的坚决不联网能轻量加载的坚决不上大模型。所以最终方案是这样的面部表情OpenCV做人脸检测然后用预训练好的 FER 表情分类模型输入64x64灰度图输出7类情感概率做表情识别全程不依赖显卡。语音情感用 librosa 提取音频特征MFCC、过零率、能量、基频等组合成特征向量后送入一个随机森林分类器。语音转文字用 Vosk 离线语音识别模型识别出文本后再做文本情感分析。融合决策在概率层做加权融合给表情、语音、文本分配不同权重最后得到综合结果。为什么选这几个模块首先Vosk 是纯离线的语音识别方案录音和识别都在本地完成不依赖外部接口也不存在日调用量限制。其次表情模型用 OpenCV 的 DNN 接口加载 caffemodel不需要装 TensorFlow 或 PyTorch 全家桶整个推理链路非常轻。语音情感这里我坚持用传统机器学习而不是深度模型因为公开的语音情感标注数据本身就不多随机森林在小样本上泛化性反而更好效果足够用调参也直观。这套架构有一个明显的优点模块之间是解耦的。你可以把表情模块换成更重的深度模型也可以把随机森林换成 MLP不影响整个流程。2. 环境准备与依赖安装2.1 Python 虚拟环境和依赖清单这套代码我建议用 Python 3.9 到 3.11 之间的版本。太新的 3.12 在某些依赖上编译容易出问题太旧的对 librosa 不友好。我自己的环境是 Python 3.10 venv在 Windows 和 Ubuntu 上都跑过效果一致。依赖清单如下直接用 pip 安装pip install opencv-python librosa soundfile pyaudio vosk numpy scikit-learn scipy如果你不需要麦克风实时录音只是想跑通流程pyaudio 可以先不装用 soundfile 读取 wav 文件代替。值得注意的是 librosa 和 numpy 之间的版本关联比较紧如果出现 numpy 版本不兼容的问题通常是 librosa 被升级到了新版而 numpy 还被旧版本锁着手动统一升级一下就行。2.2 模型文件准备这个项目需要三组模型文件我建议单独建一个models/目录统一管理models/ ├── haarcascade_frontalface_default.xml ├── fer_model/ │ ├── deploy.prototxt │ └── emotion_model.caffemodel └── vosk-model-small-cn人脸检测器直接用 OpenCV 自带的 Haar 级联文件在cv2.data.haarcascades里就能找到不用额外下载。表情模型是标准的 FER2013 训练结果prototxt 文件和 caffemodel 权重放在fer_model文件夹。Vosk 的中文语音模型去官网下载vosk-model-small-cn就行解压后大小大约 40MBCPU 也能跑得动。一个容易踩的坑模型路径不要带中文字符。Windows 下路径带中文很容易导致 OpenCV 的 DNN 读取失败报错还特别隐蔽我遇到过一次最后是逐行打印路径才发现问题。3. 核心模块实现与关键细节3.1 面部表情识别模块表情识别模块分两步先找脸再判情绪。找脸我用 Haar 级联虽然比不上当前主流的人脸检测模型但对表情识别这种场景足够用了而且速度极快。import cv2 import numpy as np face_cascade cv2.CascadeClassifier( models/haarcascade_frontalface_default.xml ) emotion_net cv2.dnn.readNetFromCaffe( models/fer_model/deploy.prototxt, models/fer_model/emotion_model.caffemodel ) EMOTIONS [angry, disgust, fear, happy, sad, surprise, neutral] def detect_face_emotion(frame): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, scaleFactor1.1, minNeighbors5) if len(faces) 0: return None, [] # 取最大的人脸框通常离镜头最近 faces sorted(faces, keylambda f: f[2] * f[3], reverseTrue) x, y, w, h faces[0] roi gray[y:y h, x:x w] roi cv2.resize(roi, (64, 64)) roi roi.astype(float32) / 255.0 blob cv2.dnn.blobFromImage(roi, 1.0, (64, 64), (0, 0, 0), swapRBFalse) emotion_net.setInput(blob) probs emotion_net.forward()[0] emotion EMOTIONS[int(np.argmax(probs))] return emotion, probs几个关键细节检测多张脸时只取面积最大的框这个假设在单人场景下是成立的如果要做多人识别就需要对每个脸单独跑一遍分类blobFromImage的均值参数这里设置成 0因为模型预处理时已经做了归一化不需要再减均值。实际测试中这个模型对“开心”和“惊讶”的识别率最高对“反感”和“害怕”经常混淆因为这两类在 FER2013 数据集里的样本量本身就少。如果你发现表情模块输出不稳定可以考虑加入简单的时序平滑连续 5 帧里出现次数最多的表情作为当前表情能明显减少单帧误判。3.2 语音情感识别模块语音情感识别不是去理解文本内容而是听说话的音调、语速、停顿、音量这些声学特征。所以这里核心工作是提取一组能代表语音“情绪色彩”的特征量。我用 librosa 提取了这几组特征40维 MFCC 的一阶差分、过零率、均方根能量、基频 F0 的均值和标准差、语速估计。全部拼接起来得到一个实验维度足够的特征向量。import librosa import numpy as np def extract_audio_features(file_path, sr16000): y, sr librosa.load(file_path, srsr, monoTrue) mfcc librosa.feature.mfcc(yy, srsr, n_mfcc40) mfcc_delta librosa.feature.delta(mfcc) zcr librosa.feature.zero_crossing_rate(y).mean() rms librosa.feature.rms(yy).mean() f0, voiced_flag, _ librosa.pyin(y, fmin80, fmax600) f0_clean f0[~np.isnan(f0)] feat np.concatenate([ mfcc.mean(axis1), mfcc_delta.mean(axis1), [zcr, rms, f0_clean.mean(), f0_clean.std()] ]) return feat.reshape(1, -1)pyin 基频提取要指定频率范围80Hz 到 600Hz 覆盖了绝大多数人的说话基频范围。男女声差异很大所以基频的方差也是一个有效特征它反映的是说话人音调的起伏程度起伏越大情绪通常越激动。分类器我用随机森林训练数据是自己标注的小数据集也可以用公开的语音情感数据集比如常见的四个基础情绪的样例集每类情绪几十条就能得到一个能用的模型。训练和保存代码比较常规这里不展开但有一点建议模型文件用joblib.dump保存成.pkl推理时加载后做同样的特征提取流程确保训练和推理的特征顺序完全一致。我见过太多人训练时特征顺序和推理时不一致结果准确率直接对半削。3.3 语音转文字与文本情感分析语音转文字我用 Vosk它的好处是纯离线、模型小、中文识别效果够用。加载模型后创建一个识别器然后持续往里面塞音频数据识别完成后取最后的final结果。from vosk import Model, KaldiRecognizer import json model Model(models/vosk-model-small-cn) rec KaldiRecognizer(model, 16000) def transcribe_frame(audio_frame): if rec.AcceptWaveform(audio_frame.tobytes()): result json.loads(rec.Result()) return result.get(text, ) return Vosk 输入必须是 16kHz 单声道 16bit 的 PCM 数据。如果音频来源是 librosa 加载的 wav 文件加载时直接用sr16000就行如果是麦克风实时采集需要保证 pyaudio 的采样率参数就是 16000。这个细节很关键采样率不匹配会导致识别结果全是乱码。文本情感分析这里我做了一个相对轻量的实现准备一份积极词表和消极词表对识别出来的文本做分词和词频统计计算一个情感得分。分数大于阈值判积极小于阈值判消极否则判中性。这样一个规则方案的优点是零训练成本缺点是对反讽、反问这类修辞基本无感。所以我在融合阶段给文本情感分配的权重是最低的它只是用来修正前两个模块的偏向而不是主导。3.4 多模态情感融合与决策融合这一步是整个项目的灵魂。表情、语音、文本三个信号各自得到的结果不是简单打平而是要有一个置信度加权的过程。def fuse_emotion(face_probs, voice_probs, text_score): # 如果语音模块没有有效输入只使用表情结果 if voice_probs is None: face_weight 1.0 voice_weight 0.0 else: face_weight 0.4 voice_weight 0.4 text_weight 0.2 final_scores np.zeros(len(EMOTIONS)) if face_probs is not None: final_scores face_weight * face_probs if voice_probs is not None: final_scores voice_weight * voice_probs # 文本情感只映射到积极、消极、中性三个维度 if text_score 0.3: final_scores[3] text_weight # happy elif text_score -0.3: final_scores[4] text_weight # sad else: final_scores[6] text_weight # neutral return EMOTIONS[int(np.argmax(final_scores))], final_scores为什么表情和语音权重一样是 0.4而文本只有 0.2从实际效果看面部表情和语气在大多数实时场景下比文本内容更诚实文本内容容易说些场面话。但文本又确实能提供语境信息所以保留 0.2 的比例。如果你想更严谨可以在自己标定的小样本上跑一遍用回归或者简单搜索法找到最合适的固定权重这个比拍脑袋定权重要靠谱得多。4. 主程序串联多线程与实时输出4.1 摄像头与麦克风并行采集既有音频又要有视频同时还要跑识别最简单可靠的方案就是多线程。摄像头线程负责读取帧并做表情识别麦克风线程负责持续的语音采集、断句和音频情感识别主线程负责把两个线程的最新结果汇总融合。import threading import queue import time class EmotionEngine: def __init__(self): self.face_result None self.voice_result None self.text_result None self.lock threading.Lock() def face_worker(self): cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: continue emotion, probs detect_face_emotion(frame) if emotion is not None: with self.lock: self.face_result (emotion, probs) time.sleep(0.1) def voice_worker(self): # pyaudio 采集 VAD 断句 特征提取 pass音频这边有个要点是断句。如果不做断句VAD 就会一直累积音频整个识别就卡住了。我用的方法是用能量阈值做端点检测当连续 0.5 秒静音时认为一段语音结束把这一段的音频分别送入 Vosk 和语音情感模型。这个方案比固定时间窗口更贴合真实说话习惯缺点是如果在嘈杂环境下静音阈值要调得高一些否则会把所有音频连成一大段。4.2 实时结果输出与可视化融合结果我用两种方式展示控制台打印情绪标签和置信度摄像头窗口里叠加显示当前识别到的表情类别。视频窗口叠加文字用 OpenCV 的putText关键是中文会显示成问号所以我在叠加时直接用英文标签中文只在终端打印。[15:32:08] 表情: happy (0.62) | 语音: happy (0.48) | 文本: 积极 [15:32:08] 融合结果 - happy (置信度 0.55)实际测试中有个很有意思的现象单帧表情偶尔会跳动但融合后整体输出平滑很多。这就是多模态融合的价值——单模块的波动会被其他模块平均掉最终结果更接近人的主观感受。5. 常见问题与排查实录5.1 环境与依赖踩坑最常遇到的问题是librosa和numpy版本冲突。如果你在安装完所有依赖后 import librosa 报numpy.ndarray size changed这类错误一般是 numpy 被其他包降级了pip install -U numpy就能解决。pyaudio 在 Windows 上安装有时会卡在编译阶段实际上装不到轮子。建议直接用预编译的 wheel或者改用sounddevice采集音频录入的数据转换一下格式就能喂给 Vosk效果完全没差。5.2 模型文件加载失败或识别不准表情模型加载失败绝大部分是 caffemodel 和 prototxt 版本不匹配。我的建议是固定使用 FER 原版配套的两个文件不要从不同来源混搭。Vosk 识别中文不准时先确认采样率是不是 16000再确认音频是不是单声道。如果都对了还是不准看看是不是录音音量太小Vosk 对低音量音频的容错率一般可以在送入识别器前做一次增益处理乘个 1.5 到 3 的系数效果立竿见影。5.3 实时同步与延迟问题摄像头线程和音频线程是独立的两个结果天然会有一点时间错位。我的做法是给每个结果打上时间戳主线程只取时间戳相差不超过 1 秒的结果做融合如果其中一个模块没有新结果就退化为单模块输出而不是卡住等待。延迟如果太大优先检查是不是表情模块每帧都在推理。摄像头一般是 30fps但表情识别没必要一帧一刷每隔 0.1 秒取一帧就够了。音频那边情绪识别至少要等一个完整语句结束才能算完所以整体输出大概会慢 1 到 2 秒这个在实际使用中可以接受。根据我个人经验这套系统跑通之后最有意思的调整阶段其实是调权重。我一开始把文本权重调得很高结果用户说“太棒了”但语气平淡时被判成开心实际是在敷衍把文本降下来之后系统对这类冷嘲热讽的识别明显准了很多。如果你也想做扩展可以试着把最后的加权融合换成一个小逻辑回归模型让系统自己在样本上学出最优权重或者给表情模块换成更大的预训练模型实时帧率会下降但识别细节会好一些。本文还有配套的精品资源点击获取
返回列表