多模态情感分析实战:基于Jupyter与PyTorch的文本语音融合模型
发布时间:2026/10/2 8:32:23来源:尧图网络
简介面向计算机、通信、人工智能等专业学生的多模态情感分析项目基于Jupyter与Python实现完整整合源码、报告文档与模型可用于期末课程设计、大作业或毕业设计参考。压缩包共2000个文件以txt数据及输出文件为主另含1个Python主程序与1个Markdown说明文档整包约202.69MB目录安排便于检索。目前已有187人浏览学习答辩评审分达到98分代码均经过调试测试确保可以运行。读者可从中获得多模态情感分析的模型构建思路、完整运行流程与预测结果示例覆盖数据预处理、模型训练到情感预测的关键环节既适合小白循序渐进地学习也便于基础较好的使用者在此基础上修改调整实现更丰富的功能附带的报告文档则有助于梳理理论背景与实验设计提升学习效率。1. 多模态情感分析模型这份 Jupyter 大作业到底能跑通什么多模态情感分析近两年几乎是期末大作业和毕设里出现频率最高的选题因为只做文本情感分析太单薄而真正的视频/语音情感分析又要处理抽帧、对齐、降噪一整条管线工作量直接翻倍。这份基于 Jupyter Python 的多模态情感分析模型源码正好卡在一个务实的位置文本模态走词向量 序列模型语音模态用预提取的统计特征融合层做注意力加权main.py 跑完直接输出 test_with_predict.txt答辩拿了 98 分说明项目完整度经得起追问。适合赶期末大作业但不想糊弄的学生、第一次接触多模态想找可复现参考的新手也适合想确认文本与语音特征到底怎么拼的从业者。在 Jupyter 里跑还有个额外好处——每个中间张量的 shape 都摊在单元格里报错一眼定位。2. 源码结构与数据流从 main.py 到 test_with_predict.txt 的完整链路2.1 先看清单README、main.py 和一堆 txt 的分工拿到压缩包先别急着运行先把文件清单读一遍。这份资源的文件结构相当典型我拆过不少课程设计这个布局基本就是说明文档 主脚本 预测输出 语料样本的标配文件作用README.md环境依赖、安装步骤、运行方式、项目说明main.py训练/预测主入口数据加载、模型构建、评估输出都在这test_with_predict.txt模型对测试样本的预测结果每行一个 id 加一个标签4226.txt、2902.txt、4094.txt 等文本语料样本每行一条待分析文本README 是最先要读的它决定了你是不是要补装某个依赖库。main.py 是整包的核心后续所有分析都围绕它展开。test_with_predict.txt 是拿来和人工标注对比的产物验证模型是否真的学到了东西。那些数字命名的 txt 文件则是真正的数据本体注意它们的命名规律——数字文件名通常就是样本编号这个编号会和预测文件里的 id 对应上后面验证时会用到。这里有个容易忽略的点多模态里文本 语音组合音频原始波形一般不会放进来而是把每段音频预先转成一维统计特征向量以 npy 或 csv 形式存放在单独目录。你如果在压缩包里没找到 wav、mp3 文件不要觉得资源缺东西这是课程设计里最常见的取舍——省掉音频预处理管线把重心放在融合模型上但打开 README 确认音频特征目录的位置仍然值得花两分钟。2.2 main.py 主流程拆解从配置到预测输出main.py 的执行顺序并不复杂我一般会先找它的 main 入口然后沿着读取配置 → 加载样本 → 提取特征 → 构建模型 → 训练/预测 → 写出结果这条线往下读。很多课程设计的 main.py 都是这种直线结构好处是每一步的输出都能单独验证。一个典型的流程骨架长这样def main(): # 1. 配置集中在一个字典里方便在 Jupyter 里直接改 cfg { data_dir: ./data, # 存放 4226.txt 等语料的目录 audio_feat_dir: ./audio_feat,# 音频统计特征目录 text_dim: 128, # 文本向量维度word2vec 常用 128 或 300 audio_dim: 1582, # opensmile 标准特征集输出维度 hidden_dim: 128, # 融合层隐藏维度 num_classes: 3, # 情感类别数积极/消极/中性 mode: train, # train 或 predict epochs: 20, batch_size: 32, lr: 1e-3 } # 2. 加载文本样本 samples load_text_samples(cfg[data_dir]) # 3. 加载音频统计特征预提取不做波形处理 audio_feats load_audio_features(cfg[audio_feat_dir]) # 4. 构建多模态模型 model MultimodalSentimentModel(cfg) # 5. 训练或预测 if cfg[mode] train: train(model, samples, audio_feats, cfg) else: preds predict(model, samples, audio_feats, cfg) write_predictions(preds, test_with_predict.txt)这段代码里真正决定项目成败的是三个对齐点data_dir 里的 txt 文件顺序是否与 audio_feat 里的特征顺序一致text_dim、audio_dim 是否和实际特征输出一致num_classes 是否和真实标签类别一致。任何一个对不上模型都能跑但结果必然离谱而且这种错在 Jupyter 里不容易被直接发现因为不报错只出坏结果。audio_dim 那个 1582 是 opensmile 标准特征集的常见维度如果你用的是自己提取的特征这个值一定要改成实际维度不能照抄。hidden_dim 设 128 对课程设计体量是够用的再往上加对准确率提升有限反而增加过拟合风险。2.3 文本样本的读取规则每一行就是一条样本数字命名的 txt 文件打开后通常是每行一条文本。读取规则要在代码里写清楚否则很容易读进去一堆空行和非法字符。我习惯的写法是import os def load_text_samples(data_dir): samples [] for fname in sorted(os.listdir(data_dir)): if not fname.endswith(.txt): continue filepath os.path.join(data_dir, fname) with open(filepath, encodingutf-8, errorsignore) as f: for line in f: line line.strip() if not line: continue samples.append({ id: fname.replace(.txt, ), text: line }) return samples这里两个参数值得单独说明。encodingutf-8 是必须的Windows 下如果用默认编码读 UTF-8 文件会得到乱码这是新手最容易踩的坑。errorsignore 的作用是跳过语料里混入的非法字节比如某些网络爬来的文本里带了半个 emoji 的残片没有这个参数整个文件都会读不进来。sorted 保证文件按名称稳定排序这看起来不起眼实际上对多模态项目至关重要文本模态和音频特征模态是按样本一一对应的只要加载顺序乱了后面所有融合都是错位融合。这类错位不会报错但会让准确率变得像随机猜测一样——多模态项目里最怕的就是这种不报错的错。读完到这一步你应该已经能确认这份项目的文本输入长什么样、怎么加载、加载后每个样本有哪些字段。接下来就是特征提取的活了。3. 文本特征提取词向量、BiLSTM 与 Attention 的参数细节3.1 中文清洗与分词Jupyter 里的第一个翻车点多模态情感分析里的文本模态最忌讳的是把原始字符串直接喂进模型。中文尤其敏感——一句这部电影太烂了连演员都救不回来和这部电影烂到演员都救不回来但特效不错字面只差几个字情感倾向完全两回事清洗和分词的质量直接决定后面模型看到的是什么。分词我一般用 jieba它在课程设计场景里足够快、足够准而且不依赖外部服务import re import jieba def clean_text(text): text re.sub(rhttps?://\S, , text) # 去掉链接 text re.sub(r\w, , text) # 去掉 用户 text re.sub(r\s, , text).strip() # 压缩空白 return text def tokenize(text): words jieba.lcut(clean_text(text)) # 只保留中文、英文、数字 token纯符号过滤掉 return [w for w in words if re.search(r[\u4e00-\u9fa5A-Za-z0-9], w)]clean_text 里三个正则覆盖了最常见的数据噪声URL、用户、连续空白。tokenize 里的过滤条件我特意保留了数字因为情感语料里3 分一般般这类表达里的数字经常是情感信号的一部分不能简单丢掉。如果你处理的是英文语料把 jieba.lcut 换成 nltk.word_tokenize 就行其余逻辑不用动。这里有个容易被忽略的点如果后面接的是 BERT 之类的预训练模型分词要换成 transformers 的 tokenizerjieba 切出来的词对 BERT 来说是先切错再拼回去反而丢失信息。课程设计里最常见的是 word2vec BiLSTM 这套组合jieba 足够用。3.2 max_len 与 embedding_dim先看语料分布再定参数max_len 这个参数是很多项目翻车的重灾区。有人图省事直接定 200结果语料平均长度只有 20模型输入里 80% 是 padding学到的全是噪声有人怕截断信息定 500结果 batch_size 一大直接爆内存。我的习惯是先做一次长度分布统计再去定阈值lengths [len(tokenize(s[text])) for s in samples] # 取 95 分位作为 max_len兼顾信息保留和训练速度 max_len sorted(lengths)[int(len(lengths) * 0.95)] print(语料平均长度:, sum(lengths) / len(lengths)) print(95分位长度:, max_len)为什么取 95 分位而不是最大值因为个别超长文本比如有人拷贝了一整段影评会把 max_len 撑到 1000而模型根本学不到那么远的依赖白白消耗内存。取分位数相当于给极端样本一个截断上限保留绝大多数信息同时让 padding 比例可控。embedding_dim 的选择取决于你用什么词向量。如果项目用了预训练 word2vec维度一般是 128 或 300如果用的是随机初始化 embedding那这个维度就是你自己设定的超参数。三种常见做法的差异我整理了个对比方案维度特点适用场景预训练 word2vec128~300语义先验强训练快中小规模语料、课程设计首选BERT 静态特征768语义最丰富内存与耗时高有 GPU、文本语义复杂随机初始化 embedding自定义完全靠任务数据学习领域性强、通用词向量覆盖差从资源包的定位来看词向量方案是性价比最高的训练时间短Jupyter 里跑得动答辩时也容易解释。如果你机器配置一般别犹豫就用预训练 word2vec把省下来的时间花在融合层的调参上。3.3 文本编码器双向 LSTM 加注意力池化的完整代码文本模态的核心编码器我建议用 BiLSTM Attention这套结构在情感分析里是被验证过无数次的经典组合比纯 LSTM 多一个反向看一遍的能力比 Transformer 轻量得多在 CPU 上也能接受。代码可以直接抄到这个程度import torch class TextEncoder(torch.nn.Module): def __init__(self, vocab_size, embedding_dim128, hidden_dim128, num_layers1, dropout0.3): super().__init__() self.embedding torch.nn.Embedding( vocab_size, embedding_dim, padding_idx0 ) self.lstm torch.nn.LSTM( embedding_dim, hidden_dim, num_layersnum_layers, batch_firstTrue, bidirectionalTrue ) self.dropout torch.nn.Dropout(dropout) self.attn torch.nn.Linear(hidden_dim * 2, 1) def forward(self, input_ids, mask): emb self.dropout(self.embedding(input_ids)) out, _ self.lstm(emb) # 注意力权重先算每个位置的原始分数 scores self.attn(out).squeeze(-1) # padding 位置的分数置为负无穷softmax 后权重为 0 scores scores.masked_fill(mask 0, -1e9) attn_weights torch.softmax(scores, dim1) # 加权求和得到句向量 context torch.bmm(attn_weights.unsqueeze(1), out).squeeze(1) return context几个参数值得解释到位。双向 LSTM 的输出维度是 hidden_dim * 2因为前向和后向两个方向的结果要拼起来attn 层的输入维度必须对应改成 256。padding_idx0 让 embedding 层对 id 为 0 的位置输出全零向量这些零向量经过 LSTM 后会产生一些残余激活所以注意力层需要 mask 把 padding 位置强行屏蔽掉这是句向量质量的关键。dropout0.3 是情感分析里比较常用的值如果你发现训练集过拟合明显把 dropout 提到 0.5如果欠拟合降到 0.2 以内。num_layers 对这类任务 1 层就够加深到 2 层收益很小但显存和训练时间会明显上涨。这层输出的 context 向量就是后续与音频特征做融合的文本模态输入。4. 多模态融合与训练权重分配、损失函数与早停4.1 直接拼特征不够浅融合会把文本信息淹没多模态项目的核心问题不是怎么把两个模态的特征塞进一个模型而是怎么让模型在融合时不丢掉任何一个模态的信息。最常见的入门做法是直接在特征维度上拼接文本向量是 256 维双向 LSTM 输出音频向量是 1582 维拼出来一个 1838 维的大向量再过几层全连接。这个做法不是不能跑但它有很明显的结构性问题——音频维度是文本维度的 6 倍多全连接层在拟合时天然会更关注音频分量文本里的情感词几乎被淹没了。这在评委会眼里是非常容易被追问的点你的多模态是简单拼接还是融合如果回答是直接 concat很难拿高分。更稳妥的方案是让模型学一个权重自己决定在做情感判断时文本和音频各占多少分量。这个思路实现起来不复杂但解释力强很多。4.2 模态加权融合层让模型自己决定谁主导我一般用一层带 sigmoid 权重的特征融合来做它相当于给每个样本学出一个 0 到 1 的标量表示文本模态在这个样本里的主导程度音频的权重就是 1 减这个值。这样模型既能学会文本很关键的样本也能学会语气很重要的样本class MultimodalFusion(torch.nn.Module): def __init__(self, text_dim, audio_dim, hidden_dim128): super().__init__() # 先把两个模态映射到同一维度 self.text_proj torch.nn.Linear(text_dim, hidden_dim) self.audio_proj torch.nn.Linear(audio_dim, hidden_dim) # 拼接后计算模态权重 self.attn torch.nn.Linear(hidden_dim * 2, 1) def forward(self, text_vec, audio_vec): t torch.tanh(self.text_proj(text_vec)) a torch.tanh(self.audio_proj(audio_vec)) combined torch.cat([t, a], dim-1) # sigmoid 输出一个标量权重 weight torch.sigmoid(self.attn(combined)) fused weight * t (1 - weight) * a return fused为什么这里用 sigmoid 而不是 softmax因为 softmax 是对两个模态的二维权重做归一化数学上等价于一个权重等于另一个的互补其实用 sigmoid 就够了而且数值上更稳定梯度传递更直接。这个权重的含义在答辩时可以这样解释weight 接近 1 的样本模型主要依据文本内容做判断weight 接近 0 的样本模型认为语气和声学特征更重要。这个可解释性是纯拼接结构给不了的。如果项目体量够大还能升级成交叉注意力让文本的每个 token 去关注音频的整体特征但课程设计阶段做模态级加权融合已经足够再复杂反而容易在答辩时被追问得下不来台。4.3 类别不均衡loss 要加权重优化器要分两层设置情感语料几乎必然存在类别不均衡——中性样本通常少于积极和消极这在多模态场景里会放大成模型全预测成多数类的问题。处理方式我一般分两步第一步用 sklearn 根据训练集标签自动算类别权重传给 CrossEntropyLoss第二步优化器按参数组设置不同学习率预训练部分用小学习率随机初始化的融合层用大学习率import numpy as np from sklearn.utils.class_weight import compute_class_weight labels [s[label] for s in train_samples] classes np.unique(labels) class_weight compute_class_weight( class_weightbalanced, classesclasses, ylabels ) loss_fn torch.nn.CrossEntropyLoss( weighttorch.tensor(class_weight, dtypetorch.float32) ) optimizer torch.optim.Adam([ {params: model.text_encoder.parameters(), lr: 5e-5}, {params: model.fusion.parameters(), lr: 1e-3}, {params: model.classifier.parameters(), lr: 1e-3} ], weight_decay1e-5)这里的关键是学习率分层。如果整个模型都用 1e-3word2vec 的 embedding 层会被打乱——预训练词向量里的语义先验在几次迭代内就被冲掉了反过来如果整个模型都用 5e-5融合层和分类器又要几十个 epoch 才能收敛。所以常见做法是把文本编码器尤其是 embedding 层的学习率调到 5e-5 这个量级融合层和分类器保持 1e-3。weight_decay1e-5 是给大权重加一点 L2 惩罚对防止融合层过拟合有一点帮助。如果你的训练集很小还可以考虑把 dropout 提上去而不是盲目加正则项。4.4 训练循环与早停验证集不涨就停下来训练循环本身不复杂但有两个细节是课程设计里常常忽略的梯度裁剪和早停。梯度裁剪解决的是多模态梯度尺度不一致的问题文本模态和音频模态的梯度量级可能差几十倍不裁剪的话 loss 偶尔会跳到 NaN 这种玄学问题早停解决的是过拟合验证集准确率连续几个 epoch 不涨就停下来恢复最优模型best_acc 0.0 patience_count 0 for epoch in range(cfg[epochs]): model.train() for batch in train_loader: text_ids, audio_feat, labels batch logits model(text_ids, audio_feat) loss loss_fn(logits, labels) optimizer.zero_grad() loss.backward() # 梯度裁剪防止多模态梯度差异导致 loss 爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() # 每个 epoch 结束评估一次 model.eval() val_acc evaluate(model, val_loader) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) patience_count 0 else: patience_count 1 if patience_count cfg[patience]: # 通常设 3 print(fearly stopping at epoch {epoch}) breakclip_grad_norm_ 的最大范数设在 1.0 是经验值如果你发现 loss 还是不稳定可以降到 0.5。patience 设 3 比较平衡太小会在验证集波动时提前停太大又起不到防过拟合的作用。需要注意早停之后恢复的不是最后一次的模型而是 best_model.pth 里保存的最优权重所以每次验证集刷新最优准确率时一定要 save这是训练环节里唯一的后悔药。5. 避坑指南Jupyter 环境、编码与过拟合的 5 条实战记录这一章不带空话直接看问题。下面 5 条坑都是在这个项目场景里真实会碰到的每一条我都按现象 → 原因 → 解决写方便你对照排查。5.1 现象Jupyter 内核连不上网页版转圈转半天现象启动 jupyter notebook 后浏览器访问网页版登录入口但界面上内核一直显示连接中点代码单元格转圈无响应重装 Anaconda 也没解决。原因这类情况九成是端口或内核注册信息的问题不是环境本身坏了。常见的是上次异常退出后jupyter 的 kernelspec 注册信息和当前 python 环境对不上或者 8888 端口被其他进程占用。解决先关掉所有 jupyter 进程命令行执行 jupyter kernelspec list 查看内核列表如果项目用的是 conda 环境再跑一次 python -m ipykernel install --user --name 你的环境名 重新注册如果端口被占换 8899 启动jupyter notebook --port8899 --no-browser然后把输出的 token 复制到网页登录页面手动登录。5.2 现象txt 读进来全是乱码模型训练全废现象在 Windows 上用 Jupyter 打开项目里的 4226.txt 等文件显示中文全部变成乱码后续特征提取只能提取出一堆无效字符。原因保存这些 txt 时用的编码是 UTF-8而 Windows 上很多编辑器默认用 GBK 打开文件如果代码里 open() 没有指定 encodingpython 也会走系统默认编码去读。解决统一在 open() 里显式指定 encodingutf-8并加上 errorsignore。所有写出的文件也显式指定编码比如结果文件 write_predictions 里用 open(path, w, encodingutf-8)。这一步做了乱码问题从一个变成零个。5.3 现象训练集 95 分、测试集 30 分不是过拟合是泄漏现象训练过程中准确率一路冲到 95%但测试集只有 30%几乎是随机水平。第一反应是过拟合加 dropout、减模型复杂度都不管用。原因这类训练好、测试差到离谱的情况首先怀疑的不是过拟合而是数据划分错误或者标签泄漏。课程设计里最常见的是按行打乱划分训练/测试集导致同一个编号的样本被拆到了两边模型相当于见过测试答案另一种是文本里残留了好评差评这种标签词。解决严格按文件粒度划分数据一个 txt 文件属于训练集就全部进训练集不能按行切检查原始语料里是否混入了标签列有的话在预处理时删掉划分时固定 random seed保证每次复现结果一致。5.4 现象预测输出全是同一个类别模型像黑匣子现象main.py 跑完打开 test_with_predict.txt 发现所有样本的预测标签都是同一个比如全是neutral准确率只比随机猜测高一点。原因多数是类别不均衡加上训练不充分导致的偏置模型发现全预测成多数类就能获得一个不算难看的 loss不会再往下学了。另一个可能原因是音频特征加载出了问题某个模态全为零向量模型在退化状态下只靠一个模态的死信号做判断。解决先检查 test_with_predict.txt 的标签分布数一下各类别占比再回到训练状态打印每个 epoch 的验证集准确率和 loss看模型是不是从第一轮开始就偏向某个类最后单独抽查音频特征矩阵看是否有全零行或全零列。这三个检查做完基本能定位到原因必要时把 loss 里的 class_weight 权重调大。5.5 现象vscode 里跑 Jupyter导入模块时报 ModuleNotFoundError现象在 vscode 里打开项目的 .ipynb选择了一个 conda 环境作为内核import 某个库直接报 ModuleNotFoundError但在终端里 pip install 明明说已经安装。原因vscode 选择的 Jupyter 内核和实际运行代码的 python 解释器不是同一个环境。终端里 pip 装进了 A 环境notebook 内核却指向 B 环境两边互不承认这是 vscode conda 组合里最高频的坑之一。解决在 conda 里激活目标环境执行 python -m pip install ipykernel 然后 python -m ipykernel install --user --name 目标环境名回到 vscode 重新选择内核把这个环境名选中。也可以在 notebook 里先执行 !which python 确认当前内核的实际路径和终端里确认一致后再往下装包。6. 验证与扩展读懂 test_with_predict.txt 并换成自己的数据6.1 预测文件的前两列id 与标签的对应关系test_with_predict.txt 是这个项目交付的成品格式一般很简单每行两列第一列是样本 id第二列是预测标签。id 与源语料的文件名是同一个编号——4226.txt 里的样本预测结果里的 id 就是 4226。验证模型对不对第一件事就是核对 id 是否能一一对上。def load_predictions(path): preds {} with open(path, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 2: preds[parts[0]] parts[1] return preds # 如果测试集有真实标签直接算分类报告 from sklearn.metrics import classification_report pred load_predictions(test_with_predict.txt) truth load_predictions(true_labels.txt) keys [k for k in pred if k in truth] print(classification_report( [truth[k] for k in keys], [pred[k] for k in keys] ))两个文件 key 对齐的位置是极易出错的如果预测脚本里少了 sorted导致 id 顺序不稳定pred 和 truth 的字典键就会错位此时 classification_report 会因为 key 不一致而输出一堆零但这其实不是模型的问题是验证脚本的问题。6.2 换成自己的数据三处改动跑通整个流程把这份资源的文本语料换掉只需要改三处第一处是把 cfg[data_dir] 指向自己的目录第二处是确认自己的数据是按行存储、每行一条文本的格式。如果带标签就调整 load_text_samples 里的解析逻辑按 \t 切分文本和标签第三处是改 cfg[num_classes]比如你的二分类任务就改成 2五分类就改成 5。这三处改完训练流程和预测流程都不用动。格式上我有一个建议自定义数据目录里放文本文件时文件名保持纯数字编号最省事否则要在 load_text_samples 里额外加 id 生成的逻辑。原项目用数字命名不是随便来的它和预测输出的 id 体系是配套的这个习惯值得沿用。6.3 换数据的固定检查流程30 秒避免 90% 的翻车一开始我觉得换数据就是改改路径结果第一次换完自己数据集翻过一次车——训练集加载了 5000 条预测时只出了 300 条而且 id 对不上。从那以后我每次换数据都强制走一遍这个流程先读一行文本打印出来看有没有乱码再 print 前 5 个样本的 id、长度、标签确认读取链路没问题跑完 predict 后抽查 test_with_predict.txt 里 10 行和源语料逐条对比看 id 是否一一对应、文本情感倾向与预测标签是否直观一致。这套流程加起来不到 30 秒但对于多模态项目来说特征对齐和数据读取顺序是最容易翻车、也最不报错的地方。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网