简介一套基于Python与TensorFlow实现的多模态情感分析项目资源输入涵盖文本、语音、图片和视频面向毕业设计、课程设计与项目开发。源码已通过严格测试可放心参考并二次扩展。项目采用分层融合思路先提取单模态特征再逐步构建双模态、三模态组合特征最后用softmax层输出喜、怒、哀、其他四类情感比常见的正负中性分类更适合用户分层与目标分析。压缩包共21个文件主要包括pickle数据文件、py源码、含iemocap数据集的zip、txt说明、pdf文档、md说明及结果示意图总大小约58.48MB目录结构清晰便于按模块查阅。环境要求Python 3.6和TensorFlow 1.7支持CPU或GPU。目前已有六百八十二人学习使用对于想理解多模态情感识别完整流程的开发者是一套可直接落地的实践资料。1. 多模态融合情感分析从文本单模态到四类情感的完整 Python 方案多模态融合情感分析这个 Python 项目解决的是纯文本情感分析的盲区。弹幕里一句“这也太厉害了吧”配上白眼表情包和嘲讽语气只看文本的模型十有八九判成喜实际上人家在骂。项目输入同时覆盖文本含 emojis、语音、图片和视频用 TensorFlow 搭了一套从单模态向量逐步融合到双模态、三模态的分层结构最后通过 softmax 输出喜、怒、哀、其他四类情感。自带源码、项目文档和 MOSI/IEMOCAP 数据集毕业设计、课程设计都能直接接跑通就是一份能演示、能写进论文的完整基线。2. 项目结构与融合原理单模态向量到三模态向量的分层设计2.1 为什么先做单模态编码而不是直接拼特征多模态数据最麻烦的地方是三个模态根本不在同一个度量空间里。文本特征来自词向量或 embedding维度几百语音特征来自 openSMILE 这类工具抽出的声学参数维度几十到上千不等图片和视频特征过完 CNN动辄上千维。如果直接把三段原始特征 concat 起来喂给全连接层维度高的模态会主导梯度方向模型基本学不到弱模态的信息。这是多模态项目里最常见的“单模态近视”也是很多人复现多模态论文发现准确率反而不如单模态的原因。项目采用的分层方案是先给每个模态单独做一层编码把不同维度的原始特征统一映射到相同的 hidden_dim再做两两组合的双模态融合最后做三模态融合接 softmax。每一步都有独立的激活函数和参数相当于先让模型分别理解“这句话在说什么”“这个语气什么情绪”“这个画面什么氛围”再学三者之间的互补关系。文本里的 emojis 也被当作语义信号保留在向量里不需要单独清洗掉这一点在预处理脚本里处理得很干净。我一般会拿这种分层结构和直接 concat 做对比实验后者准确率通常低 510 个点。原因是直接拼接时模型要同时学模态内表示和模态间关系优化难度大分层结构把任务拆成两段每段都更容易收敛。它属于 early fusion 和 late fusion 之间的折中——既不是把原始特征生拼也不是三个模型最后投票而是在中间层逐级对齐。2.2 双模态到三模态融合路径与向量对齐双模态层有三条路径文本语音、文本视频、语音视频三条路径的输出再接三模态层。为什么不把三条并成一条不同模态对的互补性不一样文本和语音都携带语言信息但文本是字面内容、语音是语调情绪两者结合能纠正“字面客气但语气轻蔑”的样本语音和视频结合能捕捉说话人表情与语调的一致程度比如皱着眉说“没事”和笑着说的情感完全不同。对齐指的是把每个模态的向量统一到相同维度再参与拼接。实现里编码层都输出 hidden_dim默认 128 维双模态拼接后是 256 维经过一层 relu 全连接保持 256 维三模态把三条双模态路径拼起来是 768 维再过一层全连接压到 512 维附近。逐层压缩的设计避免了特征维度爆炸也让最终分类层只面对一个紧凑表示。hidden_dim 不是越大越好IEMOCAP 这类几千条样本的数据集128 已经是比较稳的起点。2.3 四分类标签设计喜、怒、哀、其他常见文本情感分析是三分类正向、负向、中性。项目刻意改成喜、怒、哀、其他四类项目文档里写得很直接为了用户分层和目标用户分析。运营场景里把“负向”再拆成“怒”和“哀”非常实用——愤怒用户需要安抚和退款引导悲伤用户需要共情和陪伴话术完全两样只分正负根本不够用。标签来自数据集自带标注。IEMOCAP 原始是六类情绪项目给了 6way 和 4way 两份 pickleMOSI 原始是情感强度回归值2way pickle 把它二值化成正负。4way 的常见映射关系大致如下IEMOCAP 原始情绪映射到四分类happy、excited喜angry、frustrated怒sad哀neutral 及其他其他训练时不需要手动做这个映射pickle 里的 label 数组已经是映射后的整数索引03 对应四类。压缩包里的文件按下面这份清单对照检查文件作用model.py分层融合模型定义run.py训练、验证、推理入口data_prep.py数据加载、切分、归一化create_data.py从原始数据构造特征 pickleunimodal_mosi_2way.pickleMOSI 单模态特征二分类标签unimodal_iemocap_4way.pickleIEMOCAP 单模态特征四分类标签unimodal_iemocap_6way.pickleIEMOCAP 单模态特征六分类标签iemocap-data.zip原始 IEMOCAP 数据Canonical code.pdf项目文档与代码说明README.md环境部署与运行说明result.png作者跑出的训练结果截图新建 文本文档.txt作者备注草稿可忽略3. 数据准备MOSI、IEMOCAP 与三份 pickle 文件怎么读3.1 两份数据集各自的形态和适用场景MOSICMU 多模态观点情感数据集收录的是 YouTube 影评视频片段发言者边说边有画面文本由字幕对齐是学术圈多模态情感分析最常用的公开集之一。它的情感自带强度原本是回归任务范围在 [-3, 3]二值化后转成正负二分类样本量适中、类别干净适合用来快速验证模型能不能跑通。IEMOCAP 是交互式情感动态捕捉数据集由演员按脚本表演对话录制时长约 12 小时情绪标注比 MOSI 细带 angry、happy、sad、neutral、excited、frustrated 六类。如果你的毕业设计要强调细粒度情感分析主线用 IEMOCAP 的 4way 那份更有说服力如果只是做系统演示MOSI 2way 一个 epoch 跑得飞快演示过程不冷场。3.2 pickle 文件解析读取代码与数据结构项目把三个模态的特征预先提取好存成了 pickle。拿到文件第一件事不是跑训练而是先读进来看结构确认 key 名和维度再改 run.py 里的对应参数。下面这段代码可以做快速普查import pickle import numpy as np def load_unimodal_pickle(path): # 关键老 pickle 是 Python 2 生成的必须指定 latin1 编码 with open(path, rb) as f: data pickle.load(f, encodinglatin1) return data data load_unimodal_pickle(unimodal_mosi_2way.pickle) print(顶层类型:, type(data)) if isinstance(data, dict): for key, val in data.items(): if hasattr(val, shape): print(key, shape:, val.shape, dtype:, val.dtype) else: print(key, type(val))这段代码的逻辑分两块pickle.load 的 encoding 参数解决 Python 2 序列化文件的兼容问题很多人卡在这一步后面的循环按字典逐项打印 key 和 shape是确认特征维度最快的办法。常见的结构是 {text: ..., audio: ..., video: ..., label: ...}也可能按 {train: ..., valid: ..., test: ...} 分层组织具体以 print 结果为准。提示如果打印出来没有 label 这个 key先列出所有 key 找找常见命名还有 labels 或 y改一下脚本里的字段名即可。注意 label 的取值。2way 的 label 一般是 0/14way 是 036way 是 05。如果 run.py 里写死 num_classes4 却拿 6way 那份去训会报维度不匹配或准确率异常低这是我见过最多的用法错误。3.3 create_data.py 与 data_prep.py谁负责什么create_data.py 负责从原始数据构造特征 pickle输入是 iemocap-data.zip 这类原始录制数据输出是上面那种特征字典。常见做法是文本走预训练词向量或句向量语音用 openSMILE 提声学特征视频帧过预训练 CNN 取全连接层输出。工程上这一步最耗时所以作者把特征提前存成 pickle 发出来省掉重复抽特征的一两个小时。data_prep.py 负责把 pickle 里的特征变成模型能直接吃的数据划分训练/验证/测试集、归一化、把各模态维度对齐。切分有个容易踩的坑——三个模态来自同一条样本必须按下标同步切不能分别随机打乱。代码写法如下from sklearn.model_selection import train_test_split def split_multimodal(text, audio, video, labels, test_ratio0.1, valid_ratio0.1): # 用同一个 random_state 和同一组下标同步切四个数组 idx np.arange(len(labels)) train_idx, temp_idx train_test_split( idx, test_sizetest_ratio valid_ratio, stratifylabels, random_state42) valid_idx, test_idx train_test_split( temp_idx, test_sizevalid_ratio / (test_ratio valid_ratio), stratifylabels[temp_idx], random_state42) def pick(arr, ix): return arr[ix] return (pick(text, train_idx), pick(text, valid_idx), pick(text, test_idx), pick(audio, train_idx), pick(audio, valid_idx), pick(audio, test_idx), pick(video, train_idx), pick(video, valid_idx), pick(video, test_idx), pick(labels, train_idx), pick(labels, valid_idx), pick(labels, test_idx))train_test_split 的 stratify 参数保证训练/验证/测试里每类样本比例一致情感数据通常类不均衡这个参数能避免“验证集里全是多数类”的假象。random_state 固定成 42保证每次跑复现一致答辩时被问“实验可复现吗”直接报种子。先切出 temp 再二次切分是为了分别控制验证集和测试集比例常见配比是 8:1:1 或 6:2:2。4. 模型与训练model.py 与 run.py 的代码走读和参数调优4.1 model.py分层融合模型的核心实现model.py 是这套项目的主干接收三个模态的输入占位符依次经过单模态编码、双模态融合、三模态融合输出 logits 和 softmax 概率。一个可用的实现大致如下import tensorflow as tf def fused_model(text_feat, audio_feat, video_feat, hidden_dim128, num_classes4): # 单模态编码三个模态统一映射到 hidden_dim with tf.variable_scope(unimodal): t tf.layers.dense(text_feat, hidden_dim, activationtf.nn.relu, nametext_enc) a tf.layers.dense(audio_feat, hidden_dim, activationtf.nn.relu, nameaudio_enc) v tf.layers.dense(video_feat, hidden_dim, activationtf.nn.relu, namevideo_enc) # 双模态融合两两拼接后过一层全连接 with tf.variable_scope(bimodal): ta tf.concat([t, a], axis1) ta tf.layers.dense(ta, hidden_dim * 2, activationtf.nn.relu, nameta_fc) tv tf.concat([t, v], axis1) tv tf.layers.dense(tv, hidden_dim * 2, activationtf.nn.relu, nametv_fc) av tf.concat([a, v], axis1) av tf.layers.dense(av, hidden_dim * 2, activationtf.nn.relu, nameav_fc) # 三模态融合把三条双模态路径拼起来 with tf.variable_scope(trimodal): tri tf.concat([ta, tv, av], axis1) tri tf.layers.dense(tri, hidden_dim * 4, activationtf.nn.relu, nametri_fc) logits tf.layers.dense(tri, num_classes, namelogits) return tf.nn.softmax(logits), logits逻辑说明单模态层用三个独立的 variable_scope防止不同模态的 Dense 权重互相覆盖双模态层输出 hidden_dim2因为两个 128 维向量拼接后是 256 维容量不缩水三模态层输出 hidden_dim4 同理。最后一层不接激活函数直接出 logitssoftmax 交给损失函数内部处理数值上更稳定也比手动算 softmax 再算 cross entropy 少一次中间精度损失。参数说明hidden_dim 影响模型容量几千条样本用 128 足够数据量上十万可以调到 256num_classes 必须和 pickle 的 label 上界对齐用 4way 就传 4用 6way 就传 6。另一个容易忽略的点是 relu 之后可以加 dropout训练时 drop 0.5、预测时关掉对抑制过拟合很有效调参阶段建议加上。提示如果你装了 TensorFlow 1.15 而不是 1.7上面的代码无需改动两个版本对 tf.layers 和 tf.variable_scope 的支持一致。4.2 run.py训练循环与关键超参数run.py 负责把 model.py 定义的图真正跑起来核心是占位符、损失函数、优化器和会话循环text_ph tf.placeholder(tf.float32, [None, TEXT_DIM], nametext) audio_ph tf.placeholder(tf.float32, [None, AUDIO_DIM], nameaudio) video_ph tf.placeholder(tf.float32, [None, VIDEO_DIM], namevideo) label_ph tf.placeholder(tf.int32, [None], namelabel) _, logits fused_model(text_ph, audio_ph, video_ph) loss tf.reduce_mean( tf.nn.sparse_softmax_cross_entropy_with_logits( logitslogits, labelslabel_ph)) train_op tf.train.AdamOptimizer(learning_rate1e-3).minimize(loss) correct tf.equal(tf.argmax(logits, axis1), tf.cast(label_ph, tf.int64)) accuracy tf.reduce_mean(tf.cast(correct, tf.float32)) with tf.Session() as sess: sess.run(tf.global_variables_initializer()) for epoch in range(EPOCHS): for batch in make_batches(X_train, y_train, batch_size32): sess.run(train_op, feed_dict{ text_ph: batch[text], audio_ph: batch[audio], video_ph: batch[video], label_ph: batch[label], }) val_acc sess.run(accuracy, feed_dict{ text_ph: X_valid[text], audio_ph: X_valid[audio], video_ph: X_valid[video], label_ph: y_valid, }) print(epoch, epoch, val_acc, val_acc)逻辑说明sparse_softmax_cross_entropy_with_logits 要求 label 是整数索引而不是 one-hot正好对上 pickle 里的 03 标签省一步转换AdamOptimizer 配学习率 1e-3 是通用起点情感分类这种小数据任务很少需要超过 1e-2。验证集每个 epoch 滚一次用来判断收敛和过拟合。参数说明batch_size 32 在 4G 显存下比较稳如果用的是视频帧特征且每帧维度大降到 16 或 8EPOCHS 先设 50观察验证准确率不再上升就早停不用硬跑满。make_batches 可以自己写一个 generator按 batch_size 切片注意最后一次不足一个 batch 也要能跑别用取余把尾巴丢掉。4.3 怎么判断训练真的有效训练完别只盯着训练集准确率。第一步对比训练 loss 和验证 loss训练 loss 一直降、验证 loss 回升是过拟合加 dropout 或减 hidden_dim两个 loss 都平了说明模型容量到头。第二步做消融实验——分别只喂文本、只喂语音、只喂视频记录各自准确率再和多模态结果对比。如果融合后没比最好的单模态高大概率是特征没对齐或者数据量撑不起融合层回头查第 3 章的 pickle 维度是否真的匹配。result.png 是作者跑出的效果截图可以作为你的基准线。把自己训出的准确率和混淆矩阵截图存档写论文、做答辩 PPT 都用得上这也是为什么我建议每次实验固定随机种子、固定数据切分不然两份结果对不上答辩现场很难解释。5. 环境部署与常见问题排查TensorFlow 1.7 时代的五个坑5.1 环境版本配对Python 3.6 TensorFlow 1.7README 写得很明确Python 3.6、TensorFlow 1.7、支持 CPU 或 GPU。这套组合在当年是主流放到今天问题就来了——TensorFlow 1.7 是 2018 年的版本和现在的 Python 3.9/3.10、新版 CUDA 完全不兼容。如果机器上已经装了 TF 2.x直接拿项目代码跑几乎必翻车。CPU 版安装命令很简单关键是版本号必须锁死conda create -n msa python3.6 conda activate msa pip install tensorflow1.7.0 pip install numpy1.19.5 scikit-learnGPU 版先确认显卡驱动再装pip install tensorflow-gpu1.7.0逻辑说明TF 1.7 的 GPU 版对应 CUDA 9.0 和 cuDNN 7驱动太新反而可能跑不起来这是老框架的通病。CPU 版虽然慢但对情感分析这种几千条样本的小数据集完全够用一个 epoch 几十秒毕业设计演示不需要 GPU。装完用下面这行验证环境能打印版本号就算过了python -c import tensorflow as tf; print(tf.__version__)注意确认机器上没有其他项目依赖 TF 2.x别在同一个环境里强行降级新建 conda 环境最省事。5.2 五个高频踩坑记录以下五条是这个项目上最常见的真实翻车现场按出现频率排序。坑一TensorFlow 2.x 环境直接报 AttributeError现象运行 run.py 报 module tensorflow has no attribute placeholder或 has no attribute layers。原因机器装的是 TF 2.xtf.placeholder、tf.layers 在 2.0 里被移除项目代码按 1.x API 写的。解决新建 conda 环境装 Python 3.6然后 pip install tensorflow1.15.0。1.15 对 1.x 代码兼容最好支持到更晚的 Python 3.7比 1.7 省心。如果坚持用 2.x可以在代码头部加 import tensorflow.compat.v1 as tf 并调用 tf.disable_v2_behavior()但老代码里其他 op 还会陆续报错不建议。坑二加载 pickle 报 UnicodeDecodeError现象pickle.load 抛 UnicodeDecodeError: ascii codec cant decode byte。原因pickle 是作者用 Python 2 序列化的Python 3 默认按 ascii 解析字符串撞上非 ASCII 字符就炸。解决open 后加 encodinglatin1第 3 章代码已经写好了。这是老多模态项目最经典的兼容性坑踩的人特别多。坑三numpy 报 module numpy has no attribute float现象import 阶段报 AttributeError: module numpy has no attribute float。原因numpy 1.24 起删掉了 np.float、np.int 这些老别名data_prep.py 或依赖库还在用。解决把 numpy 降到 1.19.5。兼容 Python 3.6 的 numpy 最高就到 1.19.5和 TF 1.7 正好同一代装的时候务必锁版本。坑四GPU 训练 OOMResourceExhaustedError现象run.py 跑到某个 epoch 报 ResourceExhaustedError: OOM when allocating tensor with shape [...]。原因三个模态特征同时进 feed_dict视频特征每帧几千维时一个 batch 占用的显存会暴涨8G 以下显存尤其明显。解决batch_size 从 32 降到 16 或 8确认 feed_dict 里没有把整个验证集一次性塞进去再不行用编码层把输入先降维或者干脆跑 CPU 版。坑五模型只学到多数类准确率虚高现象训练准确率很高比如 80%但打印每一类的召回率发现模型几乎把所有样本都判成“其他”。原因情感类别天然不均衡IEMOCAP 里 neutral 和其他类样本多网络学了个偷懒策略。解决切分数据时用 stratifylabels给损失函数加类别权重对少数类放大梯度评估时看混淆矩阵而不是只看总准确率。6. 落地扩展把四分类模型改造成自己的数据集如果你手头有自己的数据而不是直接用项目自带的 pickle步骤其实不复杂核心就是把原始数据换成和 pickle 里一样的特征格式。特征提取按模态分开处理。文本部分量小用预训练词向量平均量大用 BERT 句向量输出维度按需裁剪语音部分用 openSMILE 提取声学特征拿到 CSV 再转 numpy图片/视频部分用预训练 CNN 去掉分类头把每帧过一遍网络取倒数第二层输出作为该模态特征多条视频帧可以平均成一个向量。特征到位后用第 3 章的 split_multimodal 同步切分再直接跑 run.py。这里有一个我反复强调的验证方法先用单模态特征分别训练拿到三条基线再打开融合。如果某一模态特征质量太差比如语音采集噪声大融合后反而被它拖累这时候在 data_prep.py 里给该模态特征做归一化或者干脆先剔除只做双模态。进阶技巧是用项目自带的 unimodal_iemocap_4way.pickle 训练出一个预训练权重再在自己的数据上微调只重新训练最后两层。做法是在 run.py 里把初始化换成加载 checkpoint并限定可训练变量saver tf.train.Saver() with tf.Session() as sess: saver.restore(sess, ./pretrained/iemocap_4way.ckpt) # 只微调三模态层和分类层前面层冻结 train_vars [v for v in tf.trainable_variables() if tri_fc in v.name or logits in v.name] train_op tf.train.AdamOptimizer(1e-4).minimize(loss, var_listtrain_vars)逻辑说明saver.restore 加载 IEMOCAP 4way 训练出的权重作为初始化var_list 限定只更新 tri_fc 和 logits 两层前面的模态编码层保持不动避免小数据量下把预训练表示冲掉。学习率从 1e-3 降到 1e-4微调阶段步子要小。验证是否成功的标准很简单项目自带的 pickle 跑出来的准确率与你自己数据的准确率放在同一张表里融合模型高于任意单模态基线说明多模态确实在起作用这个结果写进论文很有说服力。说句实在话我在多模态项目上栽过的跟头十有七八不在模型而在数据对齐和版本兼容。从那以后我每次拿到情感分析数据集都强制自己先走一遍“读 pickle → 查维度 → 单模态基线 → 融合对比”的流程十分钟的检查能省掉一晚上的排错。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?