
简介这份资源面向计算机、人工智能、通信工程等专业的学生与科研开发者提供一套基于Python的多模态情感识别完整实现将语音与文本两种模态结合并借助大模型微调思路完成情感分类任务可直接用于毕业设计、课程设计或项目初期立项演示。压缩包共6个文件以4个py源码为主体涵盖BERT与wav2vec2特征融合训练、模型定义及音频编码与文本分词工具另附1个md说明文档和1个txt环境配置记录整体约12KB结构精简便于快速上手。已有58人学习关注。代码经过测试可正常运行读者能从中掌握多模态特征对齐、语音文本联合建模与微调流程并参考环境说明完成本地部署基础较好的同学还可在此基础上修改扩展实现更多情感识别功能。1. 语音加文本做情感识别这套毕设源码到底能跑出什么结果答辩前一周导师问“你的多模态融合到底比单模态高几个点”很多人答不上来。单靠文本情感分析遇到“我没事”这种反讽就翻车单靠语音遇到平静语调念出负面歌词也会误判。这套基于 Python 的多模态情感识别毕设源码走的是语音加文本双通道路线文本侧用 BERT 提语义语音侧用 Wav2Vec2 提声学特征再拼在一起做分类并且带了大模型 finetune 的微调脚本。它解决的不是“从零训一个多模态大模型”这种重活而是给你一套能跑通、能改、能写进论文实验章节的完整流程。适合计算机、人工智能、通信工程方向的学生也适合想快速摸清多模态融合代码结构的从业者。下面我按实际拆包顺序把环境、数据、训练、融合、排错一条条讲清楚。2. 拆开压缩包先看什么文件结构与技术栈对应关系2.1 目录里每个文件在流程里的位置拿到毕设基于Python实现多模态语音和文本结合的情感识别大模型finetune.zip别急着装依赖。先解压对照下面这张表把文件角色认全后面调参才知道改哪个。文件/目录作用改动频率说明.md环境说明、运行顺序、数据集放置位置先读不改我的编程完整环境.txt记录作者验证过的包版本照抄别乱升BERT_w2v2_train.py训练主入口含数据加载、前向、反向、保存高频改BERT_w2v2.py双塔模型定义BERT 分支 Wav2Vec2 分支 融合层中频改utils_5_wavEnc_textTok.py音频编码、文本分词、对齐、Dataset 封装中频改models/__init__.py模型包初始化暴露类名低频改这套结构是典型的“脚本式毕设”写法训练逻辑、模型定义、数据处理分三个文件没有过度工程化。好处是新手能顺着 import 一路读下去坏处是耦合点集中在utils_5_wavEnc_textTok.py数据格式一变就得动它。2.2 技术栈版本为什么不能随便升我的编程完整环境.txt是这套代码的“后悔药”。多模态项目最怕版本漂移transformers从 4.30 升到 4.40Wav2Vec2Model的返回结构可能从 tuple 变成对象BERT_w2v2.py里取outputs[0]的写法直接报错。常见做法是建独立虚拟环境按记录装# 创建独立环境避免污染全局 conda create -n mm_emotion python3.8 -y conda activate mm_emotion # 按环境记录安装核心包版本以你包内txt为准这里给典型组合 pip install torch1.13.1cu117 -f https://download.pytorch.org/whl/torch_stable.html pip install transformers4.30.2 pip install torchaudio0.13.1 pip install librosa soundfile scikit-learn pandas numpy tqdm逻辑说明torch和torchaudio版本必须匹配否则加载 wav 时会报RuntimeError: Expected all tensors on same device之外的底层符号错误。transformers锁在 4.30 附近是因为 Wav2Vec2 的feature_extractor接口在这个区间稳定。参数上CUDA 版本按你显卡驱动选30 系卡用 cu117 比较稳40 系卡建议 cu118 起步。提示如果pip install torch装成 CPU 版训练时model.to(device)不会报错但一个 epoch 要跑几小时先torch.cuda.is_available()确认再开训。3. 数据怎么喂进去音频与文本的对齐处理3.1 多模态最容易被忽略的对齐问题多模态情感识别的精度一半取决于融合结构另一半取决于样本对齐。文本和语音必须来自同一个说话人、同一段话、同一个情感标签。常见翻车是文本用了一条评论语音用了另一条录音标签还是拼凑的训练 loss 能降但验证集乱跳。utils_5_wavEnc_textTok.py里的 Dataset 就是干这件事的核心是保证(wav_path, text, label)三元组一一对应。我一般会先写个检查脚本把数据清单过一遍确认没有空音频、没有超长文本、标签分布不极端import os, soundfile as sf import pandas as pd # 假设数据清单是 csv含 wav_path / text / label 三列 df pd.read_csv(data/labels.csv) bad [] for i, row in df.iterrows(): # 检查音频能否正常读取、时长是否过短 try: info sf.info(row[wav_path]) if info.duration 0.5: bad.append((i, too_short, info.duration)) except Exception as e: bad.append((i, read_error, str(e))) # 检查文本是否为空 if not isinstance(row[text], str) or len(row[text].strip()) 0: bad.append((i, empty_text, )) print(问题样本数:, len(bad)) for b in bad[:10]: print(b)逻辑说明sf.info只读头信息比sf.read快得多适合先筛一遍。时长阈值 0.5 秒是经验值低于这个 Wav2Vec2 提不出有效帧。参数上labels.csv的列名要和你utils里的读取字段一致不一致就在 Dataset 里改映射别去改 csv。3.2 文本分词与音频特征提取的参数文本侧走 BERT 的BertTokenizer音频侧走 Wav2Vec2 的Wav2Vec2Processor。两个处理器采样率要求不同BERT 无所谓Wav2Vec2 默认要 16kHz。如果你的音频是 44.1kHz必须在 Dataset 里重采样否则特征全错。import torchaudio from transformers import BertTokenizer, Wav2Vec2Processor tokenizer BertTokenizer.from_pretrained(bert-base-chinese) processor Wav2Vec2Processor.from_pretrained(facebook/wav2vec2-base-960h) def load_audio(path, target_sr16000): wav, sr torchaudio.load(path) # 多声道转单声道 if wav.shape[0] 1: wav wav.mean(dim0, keepdimTrue) # 重采样到 16k if sr ! target_sr: wav torchaudio.functional.resample(wav, sr, target_sr) return wav.squeeze(0) def encode_text(text, max_len128): # truncation 防止超长文本撑爆显存 return tokenizer(text, paddingmax_length, truncationTrue, max_lengthmax_len, return_tensorspt)逻辑说明resample放在 Dataset 的__getitem__里做不要预处理时全量转存否则数据量翻倍。max_len128对中文情感短句够用长文本可到 256但显存占用线性涨。音频侧如果显存紧张可以在Wav2Vec2Processor调用时加sampling_rate16000并截断到固定长度比如 5 秒。注意facebook/wav2vec2-base-960h是英文预训练权重中文语音场景下常见做法是换成中文语音预训练模型或者至少在下游 finetune 时把学习率调小让声学层慢慢适配。直接拿来冻结用中文情感区分度会打折。4. 双塔模型与 finetuneBERT 加 Wav2Vec2 怎么拼4.1 融合层的三种接法与选型理由BERT_w2v2.py里的融合方式决定了这套代码的上限。常见有三种拼接后接全连接、交叉注意力、门控融合。毕设场景我推荐先跑通拼接融合因为稳定、好解释、论文里画图清楚。import torch import torch.nn as nn from transformers import BertModel, Wav2Vec2Model class MultiModalEmotion(nn.Module): def __init__(self, num_classes4, hidden256): super().__init__() self.bert BertModel.from_pretrained(bert-base-chinese) self.w2v2 Wav2Vec2Model.from_pretrained(facebook/wav2vec2-base-960h) # BERT 输出 768 维Wav2Vec2 base 输出 768 维 self.fusion nn.Sequential( nn.Linear(768 * 2, hidden), nn.ReLU(), nn.Dropout(0.3), nn.Linear(hidden, num_classes) ) def forward(self, input_ids, attention_mask, audio_values): # 文本分支取 [CLS] 向量 text_out self.bert(input_idsinput_ids, attention_maskattention_mask).pooler_output # 音频分支对时间维取平均 audio_out self.w2v2(audio_values).last_hidden_state.mean(dim1) fused torch.cat([text_out, audio_out], dim-1) return self.fusion(fused)逻辑说明pooler_output是 BERT 的句向量比取last_hidden_state[:,0]更稳。音频侧mean(dim1)是最简单的时序聚合想提升可以换注意力池化。Dropout(0.3)是防过拟合的第一道闸数据少于 5000 条时别低于 0.2。num_classes按你数据集标签数改二分类就写 2。4.2 finetune 时哪些层该冻、学习率怎么设大模型 finetune 的核心矛盾是全量微调显存不够全冻又学不到领域特征。常见做法是分层学习率底层小、顶层大。这套代码如果直接optimizer Adam(model.parameters(), lr2e-5)BERT 和 Wav2Vec2 一起动小数据集上很容易过拟合。# 分层设置学习率预训练底层小融合层大 bert_params list(model.bert.named_parameters()) w2v2_params list(model.w2v2.named_parameters()) head_params list(model.fusion.named_parameters()) optimizer torch.optim.AdamW([ {params: [p for n, p in bert_params], lr: 1e-5}, {params: [p for n, p in w2v2_params], lr: 5e-6}, {params: [p for n, p in head_params], lr: 1e-3}, ], weight_decay0.01) # 前 2 个 epoch 冻结声学层让融合层先收敛 for epoch in range(epochs): if epoch 2: for p in model.w2v2.parameters(): p.requires_grad False else: for p in model.w2v2.parameters(): p.requires_grad True逻辑说明Wav2Vec2 学习率给到 BERT 的一半是因为声学特征对情感任务更敏感动大了会破坏预训练表征。融合层用 1e-3 是因为它随机初始化需要快速下降。weight_decay0.01配合 AdamW 是标配。前两轮冻结声学层是血泪经验不冻的话小数据集上 loss 前几个 batch 就炸到 nan。提示如果显存只够跑 batch_size4可以开梯度累积loss loss / 4; loss.backward()累积 4 次再optimizer.step()等效 batch 16。5. 训练跑不起来时先查这几处常见问题排查5.1 现象loss 一直是 nan 或突然爆掉原因通常有三个学习率过大、音频输入含 nan、标签越界。先查数据里有没有全零或超长音频再查学习率。解决把融合层学习率降到 1e-4声学层冻 2 轮并在forward后加torch.nan_to_num兜底。如果还炸打印一个 batch 的audio_values.max()超过 10 就说明没归一化。5.2 现象验证集准确率卡在 50% 不动二分类卡 50%、四分类卡 25%基本等于随机猜。原因多半是标签和输出维度不匹配或者文本和音频没对齐。解决先单独跑文本分支看能不能到 70% 以上再单独跑音频分支。哪个分支不 work 就先修哪个别一上来就调融合。5.3 现象报错Expected input batch_size (X) to match target batch_size (Y)这是 DataLoader 的collate_fn没处理好变长音频导致的。原因默认 collate 对不等长 tensor 直接 stack 失败或错位。解决在 Dataset 里把音频截断或填充到固定长度比如统一 5 秒 16kHz 即 80000 采样点再交给 DataLoader。5.4 现象训练正常但推理时结果全一样原因通常是推理时忘了model.eval()和torch.no_grad()Dropout 还在随机丢或者输入没做和训练一致的预处理。解决推理前固定走eval()音频重采样和分词参数必须和训练时完全一致差一个采样率结果就全偏。5.5 现象显存够但速度极慢原因数据在 CPU 上做重采样和分词GPU 空等。解决把重采样放到__getitem__里用 torchaudio 的 GPU 版或者预先把音频统一转成 16kHz 存盘。分词没法上 GPU但可以开DataLoader(num_workers4)并行。6. 把毕设做出区分度消融实验与融合权重调优想让论文实验章节有东西写光跑通不够得做消融。最直接的三组对比纯文本、纯语音、双模态融合。这套代码改起来不难把forward里torch.cat换成只取一个分支即可。我一般会固定随机种子跑三遍取平均避免单次玄学波动。# 消融开关mode 取 text / audio / both def forward(self, input_ids, attention_mask, audio_values, modeboth): text_out self.bert(input_idsinput_ids, attention_maskattention_mask).pooler_output audio_out self.w2v2(audio_values).last_hidden_state.mean(dim1) if mode text: fused text_out elif mode audio: fused audio_out else: fused torch.cat([text_out, audio_out], dim-1) return self.fusion(fused) if mode both else self.head_only(fused)逻辑说明消融时分类头维度要跟着变纯文本是 768 进 num_classes融合是 1536 进。别用同一个 head 硬套否则对比不公平。跑完三组通常融合比单文本高 3 到 8 个点这个差值就是论文里最值钱的一句话。再进一步是融合权重。拼接是等权但语音和文本的贡献不一定对等。可以加一个可学习权重# 可学习融合权重初始各 0.5 self.alpha nn.Parameter(torch.tensor(0.5)) fused torch.cat([self.alpha * text_out, (1 - self.alpha) * audio_out], dim-1)训练完看alpha收敛到多少如果偏向文本说明你的语音数据质量或预训练模型拖了后腿这本身就是一条可写进论文的分析结论。验证方法上除了准确率建议加 F1 和混淆矩阵情感识别类别不均衡时准确率会骗人。从那以后我每次拿到多模态代码都强制先单独跑通每个分支再动融合不然出了问题根本不知道是哪条路塌的。希望帮到你。本文还有配套的精品资源点击获取