资讯动态

多模态融合情感分析:从文本、语音到四分类的可复现实践

发布时间:2026/10/19 16:11:38 来源:尧图企业网站定制
简介这是一套基于TensorFlow的多模态情感分析项目输入涵盖文本含表情符号、语音、图片与视频通过单模态到双模态、三模态的逐层融合最终将情感划分为喜、怒、哀和其他四类相比常规正负中三分类更细粒度适用于毕业设计、课程设计及实际项目二次开发尤其适合中高级Python开发者参考实践。资源包共21个文件以Python源码.py、预处理后的pickle数据、原始数据集zip及项目文档PDF、MD、TXT为主整体大小约58.48MB压缩包内目录结构清晰便于按模块逐步学习。目前已有682人浏览学习。源码经过严格测试可直接运行并在此基础上扩展配套的data_prep.py、model.py、run.py等脚本覆盖数据准备、模型构建与训练推理全流程result.png直观展示分类效果搭配README与PDF说明可帮助读者快速掌握多模态情感识别的实现思路与实验方法。1. 多模态融合情感分析从文本、语音、视频到四分类一份可复现的资源毕业设计选题只要是“基于python的多模态融合情感分析”就绕不开三件事数据集怎么来、特征怎么对齐、融合模型怎么设计。这份资源包把这三件事都压进了源码和预提取特征文件里文本、语音、图片和视频统一转成单模态向量再逐层融合成双模态、三模态向量最后过softmax分出喜、怒、哀、其他。与常见的正负情感二分类不同它按四类情感输出更适合做用户分层和目标用户分析。适合三类人毕设需要完整流程的学生、课程设计想快速跑通的开发者、想对比特征融合方法的入门研究者。建议把源码当实验基线而不是黑匣子先跑通再改参数最后换数据集。2. 文件结构藏着流程从pickle特征到四分类标签的闭环拆包后的第一件事不是急着找入口脚本而是先让文件名跟流程对上号。这个压缩包里的代码非常集中create_data.py、data_prep.py、model.py、run.py再加三个 unimodal 开头的 pickle 特征文件、原始数据压缩包、README、算法说明文档和一张结果图。里面那个“新建 文本文档.txt”其实值得先打开通常是打包前随手记的参数备注记录了当时跑通用的Python版本、batch size或者某次调试时的坑比README更直接。2.1 文件职责表每个文件在流程里干什么整个数据处理链路是固定的create_data.py 从原始数据生成 pickle 特征data_prep.py 负责加载和预处理model.py 定义模型run.py 发起训练。四个脚本首尾衔接刚好对应四个处理环节。文件 / 目录在流程里的作用create_data.py读取原始数据生成单模态特征 pickle 文件data_prep.py加载 pickle做对齐、标准化、划分训练/验证集model.py定义单模态支路、双模态融合、三模态融合与分类头run.py主入口配置参数、启动训练、打印评估结果unimodal_mosi_2way.pickleMOSI 数据集的预提取单模态特征标签为正负二分类unimodal_iemocap_4way.pickleIEMOCAP 数据集的预提取单模态特征四分类版本unimodal_iemocap_6way.pickleIEMOCAP 数据集的预提取单模态特征六分类原版iemocap-data.zip原始数据集压缩包解压后供 create_data.py 读取dataset 目录存放中间数据或按脚本划分后的数据Canonical code.pdf算法与代码说明跑通之后再看result.png训练过程可视化结果可作为自己的基准对照我拆包时的判断是这个项目把最费时间的特征抽取做成了“离线产物”你拿到的 pickle 文件就是已经抽取好的特征数组。所以跑通 run.py 不需要音频解码库也不需要视频解码工具这对毕设环境非常友好。2.2 两套数据集、三种分类头MOSI 与 IEMOCAP 的差异资源里出现了两套公开数据集分类头的数量不一样。MOSI 是视频评论数据集一条样本包含人脸图像、语音和字幕文本标签是连续情感打分范围一般在 -3 到 3 之间。unimodal_mosi_2way.pickle 里的 2way 表示把连续打分映射成正负两种极性用于二分类。IEMOCAP 是对话语音数据集自带六类情绪标注生气、开心、悲伤、中性、沮丧、兴奋。unimodal_iemocap_6way.pickle 保留原始六分类而 4way 版本大概率是把六类映射到了本项目需要的“喜、怒、哀、其他”四类。这个映射关系决定了你改分类头时的代价如果只用四分类直接加载 4way pickle如果想在六分类上跑把 run.py 里的 n_class 从 4 改成 6 即可模型结构不用动。这里容易忽略一个细节三种分类头的输出维度不同但 picke 里的单模态特征结构是一样的。也就是说模型的主体可以完全复用只有一个数字需要改。后面第 6 章会专门说切换方法。2.3 pickle 里到底存了什么动手前先看一眼text、audio、video 三种模态的特征到底是什么形状决定了你写 data_prep.py 时对齐函数怎么设计。很多报错都来自“我以为 pickle 里存的是 [batch, 50, 128] 的张量实际存的是 [batch, 变长, 40]”所以第一步就是查看结构。import pickle # 加载预提取好的单模态特征 # 注意 encodinglatin1这是 Python2 生成的 pickle 在 Python3 下加载的通用解法 with open(unimodal_iemocap_4way.pickle, rb) as f: data pickle.load(f, encodinglatin1) # 打印前两个样本的结构 for key, value in list(data.items())[:2]: print(sample id:, key) for item in value: if hasattr(item, shape): print( shape:, item.shape, dtype:, item.dtype) else: print( value:, item)这段代码负责两件事确认 pickle 是字典结构还是列表结构确认每个模态特征张量是几维、特征维度是多少。常见结构是样本 id 为键值为一个列表里面依次是文本特征、语音特征、视觉特征和标签。如果你的打印结果显示维度是三维那说明已经带了时间步如果是二维说明发布者把序列压缩成了固定向量后面的融合模型就要按向量拼接来做而不是走 LSTM。unimodal 前缀是另一个关键提醒这些 pickle 不是原始音频和视频而是已经抽好的特征数组。特征抽取这一步被完整前置了你的代码只需要负责加载、对齐、融合。对毕业设计来说这省掉了大量环境依赖不至于一上来就栽在音频解码库的安装上。3. 特征工程文本、语音与视觉帧的抽取、对齐与批处理多模态情感分析里最影响最终效果的不是模型结构而是特征是否对齐到了同一个时间语义上。文档里专门提到输入包含文本、语音、图片和视频所以特征工程要同时处理三种不同采样率的数据文本一个词对应一个向量语音按帧计算视频按帧抽取。它们的长度天然不一致必须做统一。3.1 文本与 emoji不要忽略表情符号这一维项目文档特意标注了“含 emojis”这通常意味着文本分支不是简单的词向量平均。emoji 在情感表达里往往承担了语气词的作用一句话的文本关键词是中性的但带上笑哭表情之后情感倾向完全反转。处理时如果直接把 emoji 当成未知词丢掉相当于主动放弃了一部分强特征。常见做法是给 emoji 单独建一个索引表把每个表情映射为一个可训练向量再和词向量拼进同一个序列。我一般会复用词向量表的维度这样后续 LSTM 的输入维度不用分叉处理。import numpy as np def tokenize(text): # 按空格分词保留emoji占位符 return [t for t in text.split() if t] def build_text_feature(text, emoji_index, word_index, word_dim300, max_len30): 把一句文本转成 [max_len, word_dim] 的向量序列。 emoji 与普通词使用同一套向量空间找不到就补零向量。 tokens tokenize(text)[:max_len] seq np.zeros((max_len, word_dim), dtypenp.float32) for i, tok in enumerate(tokens): if tok in emoji_index: vec emoji_index[tok] elif tok in word_index: vec word_index[tok] else: vec np.zeros(word_dim, dtypenp.float32) seq[i] vec return seq这段代码的边界在于max_len 截断了超长句子训练时所有样本都要在同一个长度上否则 LSTM 的 time step 对不上。如果数据集里的句子普遍在 10 到 20 个词之间max_len 取 30 是安全的如果测试集出现超长文本宁可做截断也不要动态长度动态长度会让 batch 内的张量维度不一致直接触发 TensorFlow 的 shape 报错。3.2 语音与视觉帧从原始信号到固定维度语音特征最常见的做法是分帧后提取 MFCC一帧对应一个特征向量帧长通常取 25ms帧移 10ms输出 40 维左右的系数。视频特征则是均匀抽帧用预训练 CNN 逐帧提特征每帧输出 128 到 512 维的向量。图片在这个流程里更简单当作单帧视频处理直接抽取一次 CNN 特征即可。语音和视频的长度天然不同一段 5 秒的音频会产出约 500 帧而视觉抽帧可能只保留 20 到 30 帧。所以不能直接拼接必须先压缩到同一个尺度。常见做法是对语音的帧序列做“窗口池化”把 500 帧压缩成 20 帧视频则均匀抽 20 帧。这一步的结果是三种模态在时间维上都有同样的长度之后才能进融合层。3.3 data_prep.py 里的对齐函数均匀采样而不是开头截断对齐是整个工程里最容易翻车的位置。对齐函数的目标是把任意长度的特征序列统一成 target_len 帧。最简单的思路是直接截断或者末尾补零但效果都不好直接截断会把句尾情感信息丢掉因为情感通常在结尾爆发末尾补零会让模型学到一堆无意义的零向量。我推荐的方案是长序列均匀采样短序列重复末尾帧。import numpy as np def align_feature(feat, target_len20): 把 [seq_len, feat_dim] 的特征序列对齐到固定长度。 长序列均匀采样保留时间结构短序列用末尾帧补齐。 seq_len feat.shape[0] if seq_len target_len: # 均匀采样避免直接掐头去尾 idx np.linspace(0, seq_len - 1, target_len).astype(np.int) return feat[idx] elif seq_len target_len: # 用最后一帧重复补齐不引入不存在的中间值 pad np.repeat(feat[-1:], target_len - seq_len, axis0) return np.concatenate([feat, pad], axis0) return feat参数上target_len 选 20 是一个平衡点太小会丢失语音的语调变化太大会拖慢训练。这里要提一个很多人忽略的细节均匀采样用的索引是 np.linspace 生成的浮点索引转整数时要小心边界如果 seq_len 恰好等于 target_len 会走最后一分支如果 seq_len 比 target_len 大很多linspace 采样不会越界安全。data_prep.py 里随后要做标准化和批处理。我一般会在划分训练集和验证集之后再做标准化统计量的计算用训练集的均值和方差去归一化验证集避免数据泄漏。批大小推荐 32显存吃紧就降到 16。验证曲线可以参考资源包附带的 result.png如果你跑出来的 loss 曲线形状差异太大优先怀疑对齐函数而不是模型结构。4. 模型设计单模态到双模态再到三模态的层次融合为什么先单后合项目的核心卖点是“分层方法”从单模态向量逐步扩展到双模态和三模态向量最后过 softmax 分类。这个设计和大多数人一开始设想的“直接把三种特征 concat 成一个长向量”不一样背后有实际的训练收益。4.1 层次融合的动机一次 concat 为什么不行早期拼接的做法是把文本、语音、视觉三个特征在输入层直接拼成一个长向量然后丢进全连接网络。问题在于三种模态的分布差异太大文本向量是离散语义空间语音是声学空间视觉是像素语义空间。直接拼接等于让第一层网络同时学会三种空间的度量衡训练难度成倍增加。层次融合的思路是让每个单模态先用自己的 LSTM 压缩成一个语义向量再做两两拼接最后三模态拼接。每一步的输入输出都是同一语义空间里的向量网络只需要学“哪个模态组合对情感判断贡献更大”而不是被迫同时做特征空间转换。这也是文档里强调“从单模态向量逐步扩展到双模态和三模态向量”的原因。4.2 model.py 核心结构LSTM 支路加两层融合下面是我拆包后重构的模型结构和文档“单模态→双模态→三模态→softmax”的描述一致。每个单模态先用 LSTM 取最后隐藏状态作为向量双模态层是三组两两拼接三模态层是三个向量一起拼接每个融合层都接了一个辅助分类头。import tensorflow as tf def hierarchical_model(text_x, audio_x, video_x, n_class4, hidden64): # 单模态支路把序列压成一个语义向量 def branch(x, scope): with tf.variable_scope(scope): cell tf.nn.rnn_cell.LSTMCell(hidden) _, state tf.nn.dynamic_rnn(cell, x, dtypetf.float32) return state.h # LSTMStateTuple 里的隐状态 with tf.variable_scope(unimodal): t branch(text_x, text) a branch(audio_x, audio) v branch(video_x, video) # 双模态层两两拼接后再过全连接 with tf.variable_scope(bimodal): ta tf.layers.dense(tf.concat([t, a], axis1), hidden, tf.nn.relu) av tf.layers.dense(tf.concat([a, v], axis1), hidden, tf.nn.relu) tv tf.layers.dense(tf.concat([t, v], axis1), hidden, tf.nn.relu) # 三模态层 with tf.variable_scope(trimodal): tav tf.layers.dense(tf.concat([t, a, v], axis1), hidden, tf.nn.relu) # 辅助分类头与最终融合 logits_ta tf.layers.dense(ta, n_class) logits_av tf.layers.dense(av, n_class) logits_tv tf.layers.dense(tv, n_class) logits_tav tf.layers.dense(tav, n_class) fusion tf.concat([ tf.nn.softmax(logits_ta), tf.nn.softmax(logits_av), tf.nn.softmax(logits_tv), tf.nn.softmax(logits_tav) ], axis1) logits tf.layers.dense(fusion, n_class) return logits这段代码里值得细看的是辅助分类头。双模态层的三个 softmax 和三模态层的一个 softmax 并不直接作为最终输出而是把概率拼接后再过一层映射。这样做的实际效果是中间层被迫各自学到有意义的情感判别能力最终分类器只需要学习“信任哪个模态组合”而不是从头学三模态特征。hidden64 是对比实验里比较稳妥的起点。如果数据集规模大可以调到 128但注意 LSTM 的参数量会翻两番。我建议你先用 64 跑通确认 loss 下降后再放大而不是一上来就追求大模型。4.3 损失函数、优化器与不均衡标签分类头用的是 sparse softmax 交叉熵优化器用 Adam初始学习率 0.001。四分类的标签如果分布不均“喜”占了绝大多数而“怒”很少训练会偏向多数类。资源包里没有内置类别权重实际动手时可以给少数类加权。# 按类别比例手工设定权重 class_weights [2.0, 1.0, 1.0, 0.5] weights tf.gather(class_weights, tf_label) loss tf.reduce_mean( tf.nn.sparse_softmax_cross_entropy_with_logits(logitslogits, labelstf_label) * weights ) train_op tf.train.AdamOptimizer(0.001).minimize(loss)类别权重的设置原则是数量少的类权重更高但不宜过高2 到 3 倍是常见范围。权重太大会让模型把大量样本判成少数类准确率反而下降。我一般会在训练前先打印标签分布用 np.bincount 统计每个类别的样本数再决定权重具体值。5. 高频错误与训练排查TensorFlow 1.7 下的环境坑、维度坑与 loss 卡死这个项目锁定了 TensorFlow 1.7 和 Python 3.6在现在的机器上跑通需要一点版本管理经验。很多人卡住不是因为模型代码而是环境版本组合不对。这一章把最容易遇到的坑按现象、原因、解决拆开讲。5.1 环境部署先把版本锁死TensorFlow 1.7 发布于 Python 3.6 时代直接 pip install tensorflow1.7.0 之前建议用 conda 建好隔离环境。Python 版本超过 3.7 时1.7 的预编译包大概率装不上。# 建议使用 py3.6 隔离环境 conda create -n tf17 python3.6 conda activate tf17 pip install tensorflow1.7.0 numpy1.16.6如果是 GPU 机器tensorflow-gpu1.7.0 对应的是 CUDA 9.0 和 cuDNN 7.0 的组合新版 CUDA 无法直接兼容。CPU 模式更省心在脚本开头强制指定设备即可import os os.environ[CUDA_VISIBLE_DEVICES] -1注意这个环境变量需要在 import tensorflow 之前设置否则 GPU 设备已经被初始化设置了也不生效。5.2 高频运行错误现象、原因、解决错误 1pickle 加载时出现 UnicodeDecodeError: ascii codec cant decode byte。原因pickle 文件由 Python2 生成内部字符串需要按 Latin1 解码。解决加载时指定 encodinglatin1也就是第 2.3 节代码里的写法。如果不用这个参数在 Python3 下必现这个错误。错误 2run.py 报 ValueError提示 Cannot feed value of shape (32, 25, 40) for Tensor Placeholder:0期望 shape 是 (?, 25, 128)。原因占位符的第三维和实际输入特征维度不匹配。文本、语音、视觉三个输入的维度不同我在第 4.2 节代码里直接用 text_x 变量名占位实际实现时需要在 run.py 里为三个占位符显式声明第三维。解决打印 pickle 里每类特征的 shape把第三个数字写到占位符定义里或者在 data_prep.py 里对特征做维度统一。错误 3AttributeError: module numpy has no attribute bool。原因numpy 新版移除了 np.boolTensorFlow 1.7 内部老代码还在用。解决把 numpy 降到 1.16.6这是最省事的方案不要手动改源码。错误 4GPU 机器上训练时报 ResourceExhaustedError: OOM。原因batch_size32、hidden64、加三模态拼接显存占用很容易超。解决batch_size 降到 16 或 8也可以把 target_len 从 30 降到 20。验证集和训练集不要同时开两个 session跑完一个再跑另一个。这个错误列表可以存在本地当排查手册。我自己的习惯是遇到 shape 类报错第一反应不是看堆栈而是先打印 batch 内所有张量的 shape这个习惯能节省大量时间。5.3 训练表现异常loss 不降与准确率卡在 25%四分类任务的随机准确率是 25%。如果训练了 5 个 epoch 后准确率还在 30% 以下先检查标签分布而不是盲目调学习率。我见过的最常见原因不是模型问题而是 4way 和 6way 的标签没做映射直接加载 6way pickle 后保留了 6 个类别但输出层还是 4 个节点导致标签值 5 超出了有效范围训练过程一片混乱。# 检查一个 batch 里标签是否均衡 import numpy as np batch_labels [sample[-1] for sample in batch] print(np.bincount(batch_labels, minlengthn_class))如果某个类别在 1000 步训练里从未出现准确率必然虚高或虚低。另一个常见问题是学习率过大导致 loss 震荡不降检查 Adam 初始学习率是否设置在 0.001 附近。dropout 设为 0.8 以上也可能让训练变慢但这类现象通常会在 epoch 增大后缓解不要因为前 3 个 epoch 不降就马上判定模型坏了。注意看 result.png 里记录的训练曲线资源文档里通常会保留正常训练的 loss 下降速度。如果自己的曲线前几十步就剧烈震荡优先怀疑学习率如果平稳但下降极慢优先怀疑特征对齐的长度设置。6. 换数据集实战四分类改六分类再改成回归打分这套资源的价值不在于跑通一个 demo而在于能快速换标签体系。下面两个方向的改动量都不大但每一处都踩过坑。6.1 从 4way 切到 6way改两个数字加载 unimodal_iemocap_6way.pickle把 run.py 里的 n_class 改成 6data_prep.py 里的标签变量就不再需要做四分类映射。如果原脚本里写死了四分类的标签转换逻辑必须删掉否则标签值在 4 到 5 的样本会被映射到错误类别。# run.py 里只需要动这两处 DATA_PICKLE unimodal_iemocap_6way.pickle N_CLASS 64way 版本的“喜、怒、哀、其他”和 6way 版本的“生气、开心、悲伤、中性、沮丧、兴奋”之间没有一一对应关系后者更细。切换后验证集准确率通常会先下降因为分类任务变难了这是正常现象不代表代码改错。6.2 从分类改成回归打分MOSI 的 2way pickle 里实际存的是连续情感打分加载后如果想要回归输出把输出层从 n_class 个节点改成 1 个节点损失函数从交叉熵改成均方误差即可。with tf.variable_scope(regression_head): pred tf.layers.dense(fusion, 1) loss tf.reduce_mean(tf.square(pred - tf_label_float))评估指标也要换分类任务看准确率回归任务看皮尔逊相关系数和平均绝对误差。如果直接沿用准确率做判断你会发现模型“预测不准”但相关系数其实已经不错了。从那以后我每次换数据集都会强制走一遍完整检查先看 pickle 标签分布再打印三个模态的 shape确认分类头节点数最后才动 run.py。这套流程帮我滤掉了九成以上的低级错误希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑