简介:面向需要完成毕业设计、课程设计或项目开发的计算机专业学生与开发者,提供一套基于Python/TensorFlow的多模态融合情感分析完整方案。项目输入覆盖文本(含emoji)、语音、图片和视频,采用分层策略从单模态特征逐步扩展为双模态与三模态特征,最终由softmax层将结果划分为喜、怒、哀、其他四类情感,比常规正负中性分类更能支撑用户情绪分层与目标人群分析。资源包含21个文件,以pickle数据文件、Python脚本、数据集压缩包及PDF/Markdown说明文档为主,整体约58.48MB,并附带环境说明(Python 3.6+、TensorFlow 1.7,支持CPU/GPU)。项目结构围绕数据准备、模型构建、训练运行等模块展开,便于按步骤理解多模态融合流程。源码已经过严格测试,已有680人学习使用;下载后既可作为课程设计或毕业设计的完整参考,也可在其基础上替换数据集、增加新模态或调整情感分类策略,方便二次开发。
1. 基于 python 开发的多模态融合情感分析:不是把三个模型拼一起就叫融合
做情感分析的同学大概都有个直觉:文本情感分类已经卷到没边了,换个方向做多模态似乎更有搞头。但你如果把文本、语音、图像各跑一个模型,最后把分数加一加,那不叫多模态融合,那叫投票。这个基于 python 开发的多模态融合情感分析项目,真正值得拆的地方在于它把单模态向量按层级拼成双模态、三模态向量,再统一过 softmax 分类成喜、怒、哀、其他四类,而不是常见的正负中性三分类。这个设计思路比代码本身更有参考价值,适合正在做毕业设计、课程设计,或者想往用户画像方向延伸的项目开发场景。项目自带源码、项目文档和预处理后的数据集,环境要求是 Python 3.6+ 和 TensorFlow 1.7,CPU 或 GPU 都能跑,下面从数据准备到模型训练一条线拆开讲。
2. 项目骨架与数据流:先读懂 pickle 缓存,再谈改模型
2.1 从文件清单看项目设计:四个 pickle 文件决定了你训练时读什么
把压缩包解压之后,核心文件我按用途分了三组,先看第一组:unimodal_mosi_2way.pickle、unimodal_iemocap_6way.pickle、unimodal_iemocap_4way.pickle。这三个是已经抽取好的单模态特征缓存。mosi是 CMU-MOSI 数据集,iemocap是 IEMOCAP 数据集,后面的2way、4way、6way指的是情绪类别数量。换句话说,作者把原始视频、音频、文本已经跑过一遍特征提取,结果存在 pickle 里,训练时直接加载,不用你重新去处理原始视频帧,这对没有 GPU 的同学非常友好。
第二组是处理脚本:data_prep.py、create_data.py、datasets.py。注意压缩包里还有一个名字很怪的“新建 文本文档.txt”,这个通常是作者整理数据目录时留下的说明残留,实际跑流程用不到,别被它干扰。
第三组是模型与训练入口:model.py、run.py。model.py定义了分层融合结构,run.py负责加载数据、构建图、启动训练。Canonical code.pdf是项目文档,result.png是训练曲线截图,README.md里有环境安装说明,iemocap-data.zip是原始数据集的打包。
2.2 数据准备链路:raw 数据如何变成模型能吃的 pickle 特征
原始iemocap-data.zip解压后,里面是音频、视频转写的文本标注、以及每一段对话的情绪标签。data_prep.py干的事是把这些原始录音按句切分,然后提取特征。常见做法是文本走词向量,语音走 openSMILE 这类工具抽帧级特征,图像或视频帧走预训练的 CNN 抽向量,最后统一保存成 numpy 数组,再序列化进 pickle。
create_data.py的作用更像是组装器。它读入data_prep.py产生的中间结果,把文本向量、语音向量、视觉向量对齐到同一个时间窗口,然后按训练集、验证集、测试集切分,最终生成unimodal_*_*.pickle文件。这里有个关键点:对齐是按句子粒度做的,不是按帧粒度。IEMOCAP 的原始数据里有说话人停顿、重叠语音,这些在预处理阶段都会被清洗掉。
datasets.py是加载器,里面的类会把 pickle 读进来,同时处理样本随机打乱和 batch 划分。你可以把这三个文件理解成一条流水线:data_prep负责原料粗加工,create_data负责精切与打包,datasets负责在训练时按需读取。
2.3 两个数据集的差异:为什么 mosi 是 2way 而 iemocap 是 6way
很多第一次看这个项目的人会卡在这里:unimodal_mosi_2way.pickle是两类,unimodal_iemocap_6way.pickle是六类,unimodal_iemocap_4way.pickle又变成四类,那我的模型输出维度到底设多少?
答案取决于你加载哪个 pickle。CMU-MOSI 的原始标注是 [-3, 3] 的连续情感分数,作者把它二值化成积极/消极两类,所以是 2way。IEMOCAP 原始标注有 anger、happy、sad、neutral 等多种,6way 是保留六个完整类别,4way 是把近似情绪合并成喜、怒、哀、其他四类,对应项目简介里说的分类标准。
| 数据集 | pickle 文件 | 类别数 | 分类含义 |
|---|---|---|---|
| CMU-MOSI | unimodal_mosi_2way.pickle | 2 | 积极、消极 |
| IEMOCAP | unimodal_iemocap_6way.pickle | 6 | 六个原始情绪 |
| IEMOCAP | unimodal_iemocap_4way.pickle | 4 | 喜、怒、哀、其他 |
如果你想复现项目文档里那个“四分类情感分析”的效果,就直接用 4way 的 pickle。如果只是想先跑通代码,用 2way 的 mosi 数据更快,因为类别少、收敛快、对 CPU 训练更友好。
2.4 跑通流程的最小步骤与预期输出
第一次跑,建议顺序是:解压iemocap-data.zip→ 打开run.py确认 pickle 路径变量 → 设置数据集为unimodal_iemocap_4way.pickle→ 直接运行。如果run.py里写死了某一个 pickle 的名字,你只需要改一处字符串。
pickle_path = './unimodal_iemocap_4way.pickle' # 换成你想用的数据 with open(pickle_path, 'rb') as f: data = pickle.load(f, encoding='latin1') # py3 加载 py2 pickle 必备参数这里有个非常重要的细节:encoding='latin1'。这些 pickle 文件如果是在 Python 2 环境下序列化的,Python 3 直接pickle.load(f)会报UnicodeDecodeError,加encoding='latin1'是最省事的解法。加载成功后,data里通常是 dict 结构,包含训练、验证、测试三个 split,每个 split 下有 text、audio、video 三个模态的特征数组和 label 数组。
3. 训练管线与模型结构:model.py 的层级融合到底怎么设计
3.1 单模态向量到三模态向量的拼接逻辑
项目简介里提到的“分层方法”是核心创新点,也是答辩时最容易被追问的地方。传统的 early fusion 是把三个模态的特征直接 concat,late fusion 是三个模态各出一个分数再平均,这个项目走的是中间路线:先两两融合,再三模态融合。
def hierarchical_fusion(text_vec, audio_vec, video_vec): # 单模态投影到同一维度 text_h = tf.layers.dense(text_vec, 128, activation=tf.nn.relu) audio_h = tf.layers.dense(audio_vec, 128, activation=tf.nn.relu) video_h = tf.layers.dense(video_vec, 128, activation=tf.nn.relu) # 双模态融合 text_audio = tf.concat([text_h, audio_h], axis=1) text_video = tf.concat([text_h, video_h], axis=1) audio_video = tf.concat([audio_h, video_h], axis=1) # 三模态融合 tri = tf.concat([text_audio, text_video, audio_video], axis=1) tri = tf.layers.dense(tri, 256, activation=tf.nn.relu) # 分类层 logits = tf.layers.dense(tri, num_classes) return logits这段代码的思路是:先将三个模态分别过一层全连接,映射到 128 维的公共空间,目的是消除三个模态特征维度不一致的问题。比如文本向量是 300 维 GloVe,语音特征是 256 维,视觉特征是 512 维,不投影直接 concat 会让高维模态主导梯度更新。投影之后再两两拼接,等于显式建模每两个模态之间的交互关系,最后把三组双模态特征再拼起来过一层全连接,得到三模态高层表示。
这种设计的直观解释是:情感判断往往不依赖所有模态同时生效。比如语气平淡但画面愤怒,语音和视觉的矛盾信号需要模型先学到“视频主导”的权重,双模态层就是给这种权重留了学习空间。实际代码里model.py会复杂一些,可能加了 dropout 和 batch normalization,但骨干结构就是上面这个三段式。
3.2 run.py 的核心参数与实测调整
run.py是整个项目的入口。里面的超参数直接决定训练效果,最重要的四个:learning_rate、batch_size、num_epochs、num_classes。
flags = tf.app.flags flags.DEFINE_float('learning_rate', 0.001, '初始学习率') flags.DEFINE_integer('batch_size', 32, '每个batch的样本数') flags.DEFINE_integer('num_epochs', 100, '最大训练轮数') flags.DEFINE_integer('num_classes', 4, '分类类别数,与pickle匹配') FLAGS = flags.FLAGS学习率 0.001 用的是 Adam 优化器的常见默认值,适合小规模多模态数据。如果训练 loss 震荡不降,我一般会降到 0.0003;如果收敛太慢,先确认是不是类别数设错了。batch_size建议保持 32,因为 pickle 里缓存的特征其实是内存中的 numpy 数组,数据量不大,batch 太大会让梯度方向单一,太小会放大噪声。
训练启动命令在 TensorFlow 1.7 下是:
python run.py --learning_rate=0.001 --batch_size=32 --num_epochs=100 --num_classes=4如果run.py里已经写死了参数,你也可以直接python run.py开跑。TF 1.7 没有 Keras 那种model.fit的友好输出,你会在终端看到每个 step 的 loss 和 accuracy 逐行打印,result.png里画的就是这个日志的曲线。
3.3 训练日志里该盯什么:loss 曲线比准确率数字更诚实
多模态模型一个典型毛病是过拟合得悄无声息。因为特征维度高、样本量小,训练 accuracy 可能很快冲到 95%,但验证 accuracy 卡在 70% 甚至往下掉。这时候盯住训练日志里的验证集 loss,如果它在第 30 轮开始反升,而训练 loss 还在降,就是典型的过拟合信号,应该提前停掉,然后调大 dropout 或者减小模型容量。
项目里如果提供了验证集 accuracy 打印,判断标准很简单:训练 accuracy 与验证 accuracy 差距超过 15 个百分点,基本可以断定过拟合。这时优先动模型代码里的 dropout 层,而不是加数据,因为多模态数据集的标注成本非常高,你很难快速扩充样本。
另外,TensorFlow 1.7 的日志里经常会出现WARNING:tensorflow:开头的一堆提示,大部分是参数初始化或设备分配相关的警告,不影响训练结果。真正的错误特征是进程直接退出,或者 loss 变成 NaN。loss 变 NaN 的常见原因是学习率过大或输入数据里有 NaN 值——预处理阶段漏掉的空音频帧会导致特征数组里出现 NaN,这种问题在后面避坑章节专门展开。
4. 避坑与排查:TensorFlow 1.7 环境下最常见的 5 个翻车现场
4.1 pickle 加载报 UnicodeDecodeError
现象:pickle.load(f)执行时抛出UnicodeDecodeError: 'ascii' codec can't decode byte 0x8b in position 0。
原因:pickle 是 Python 2 序列化生成的,字符串默认是 byte 类型,Python 3 默认按 Unicode 处理,两者不兼容。
解决:加载时强制指定编码。
with open('unimodal_iemocap_4way.pickle', 'rb') as f: data = pickle.load(f, encoding='latin1')latin1是 Python 2 和 3 都能无损互转的编码,比bytes更省事。如果你加载后取特征时发现数组维度不对,八成是同一个文件里混了不同编码,逐个层查看data.keys()去定位。
4.2 TensorFlow 1.7 装不上:Python 3.8 以上直接报错
现象:pip install tensorflow==1.7在 Python 3.8 环境下提示找不到匹配版本,或者装上了但 import 时崩溃。
原因:TensorFlow 1.7 是 2018 年的版本,官方只支持到 Python 3.6。Python 3.8 的 ABI 不兼容。
解决:用 conda 建一个 Python 3.6 环境再装。
conda create -n tf1 python=3.6 conda activate tf1 pip install tensorflow==1.7别在这上面浪费时间硬刚。用 Python 3.6 虚拟环境是这个项目最省心的做法,装好后python -c "import tensorflow as tf; print(tf.__version__)"输出1.7.0才算通过。
4.3 训练时 OOM 显存不足,但项目明明支持 GPU
现象:ResourceExhaustedError: OOM when allocating tensor with shape [...],进程直接退出。
原因:TensorFlow 1.7 默认申请全部 GPU 显存,训练和验证集一次性全部加载到显存也会放大占用。
解决:在run.py开头限制 GPU 显存按需分配。
config = tf.ConfigProto() config.gpu_options.allow_growth = True sess = tf.Session(config=config)加上之后训练过程会边用边申请显存,不再一次性吃满。如果还是 OOM,把batch_size从 32 降到 16,多模态特征 concat 之后占用确实比单模态高不少。
4.4 iemocap 解压后路径对不上,run.py 找不到数据
现象:报FileNotFoundError: ./iemocap-data/xxx.wav,但文件明明存在。
原因:iemocap-data.zip解压后,内层文件夹层级和data_prep.py里写死的路径不一致,最常见的是多套了一层目录。
解决:解压后先看一眼目录结构,把data_prep.py里路径变量改成实际路径。懒得改脚本的话,也可以直接把解压出来的文件挪到脚本预期位置。
unzip iemocap-data.zip -d ./ # 如果生成 ./iemocap-data/iemocap-data/xxx,就手动把内层提到外层 mv ./iemocap-data/iemocap-data/* ./iemocap-data/4.5 中文文本带 emoji 预处理后全部变成乱码
现象:训练 accuracy 极低,检查数据发现文本向量都是同一个值。
原因:项目原始数据处理是面向英文的,emoji 和中文在分词、编码阶段的处理逻辑不一致;如果你替换成自己的中文数据集,没做字形归一化就直接过词向量,绝大多数 token 会映射成 OOV。
解决:预处理阶段先统一编码,再决定是否保留 emoji。
# 常见做法:emoji 转成占位 token,中文按字切分 import re text = re.sub(r'[\U00010000-\U0010ffff]', '<emoji>', text) text = ' '.join(list(text)) # 按字切分中文情感分析里按字切分比按词切分更稳,因为分词错误会直接污染后续情感极性判断。这个项目本身是英文数据集,如果你要套自己的中文数据,必须走这一步,否则后面全是白训。
5. 把预计算特征接到自己的数据上:最小改动替换 pickle 的完整方法
到这一步,你已经能跑通项目自带的 iemocap 四分类。但毕业设计答辩时老师大概率会问:“你能不能处理自己的数据?” 这里给一个经过验证的替换方案:不重写模型结构,只把create_data.py的输出格式对齐到项目期望的 pickle 结构上。
首先明确模型需要什么:一个字典,包含train、valid、test三个键,每个键下又有text、audio、video、label四个数组,其中三个模态的特征数组第一维必须长度一致,label 是独热编码或整数索引。你的自制数据只要做成这个结构,模型代码一行不用改。
import numpy as np import pickle def build_pickle_from_custom(text_feats, audio_feats, video_feats, labels): assert text_feats.shape[0] == audio_feats.shape[0] == video_feats.shape[0] == len(labels) data = { 'train': {'text': text_feats[:400], 'audio': audio_feats[:400], 'video': video_feats[:400], 'label': labels[:400]}, 'valid': {'text': text_feats[400:500], 'audio': audio_feats[400:500], 'video': video_feats[400:500], 'label': labels[400:500]}, 'test': {'text': text_feats[500:], 'audio': audio_feats[500:], 'video': video_feats[500:], 'label': labels[500:]} } with open('./custom_data.pickle', 'wb') as f: pickle.dump(data, f, protocol=2)几个参数要点:protocol=2是为了兼容 Python 2 加载器,如果你确定只在 Python 3 环境用,protocol=4也可以,但建议保守一点。训练集、验证集、测试集的划分比例,常见做法是 8:1:1,上面代码里 400/500 是个硬编码示例,实际应该按你的样本总量算索引,我给一个通用写法:train_end = int(len(labels) * 0.8),valid_end = int(len(labels) * 0.9)。
换好数据之后,训练命令跟第 3 章完全一致,唯一改动是把run.py里的 pickle 路径指向custom_data.pickle,同时确认num_classes和你的 label 数量对应。如果你手里的数据还没有模态特征,也就是只有原始音频文件和文本,那需要先做特征提取:文本可以用 GloVe 或 BERT embedding,语音建议用 openSMILE 抽 IS13 协议的标准特征集,视频帧用预训练 ResNet 抽最后一层池化特征。这一步大家基本都是各用各的工具链,没有统一标准,但保证输出是形状为(样本数, 特征维度)的 numpy 数组就行。
这里有个我一直强调的习惯:替换完数据集后,第一件事不是看 accuracy,而是把 pickle 加载出来打印特征数组的 shape 和 label 的取值分布。
with open('./custom_data.pickle', 'rb') as f: check = pickle.load(f, encoding='latin1') print(check['train']['text'].shape) print(np.unique(check['train']['label']))如果 shape 里四个模态长度不一致,或者 label 的取值数量超过num_classes,训练再有耐心也是白费。从那以后我每次换数据都强制走一遍这个检查,五分钟能省一晚上的排查时间,希望帮到你。
本文还有配套的精品资源,点击获取