
简介这份资源面向计算机、人工智能、通信工程等专业的学生与科研人员提供一套基于Python的多模态情感识别完整实现将语音与文本两种模态结合并借助大模型微调思路完成情感分类任务可作为毕业设计、课程设计或项目立项的参考方案。压缩包共6个文件约12KB包含4个Python脚本、1个说明文档与1个环境配置文本脚本分别承担BERT与wav2vec2特征融合、模型训练及音频编码与文本分词等模块结构紧凑、便于按模块阅读与二次修改。目前已有58人学习关注适合希望理解多模态融合流程、掌握大模型微调基本套路的读者借鉴也能在此基础上替换数据集或调整网络结构以扩展功能。1. 多模态情感识别把语音和文本塞进同一个模型到底怎么跑通一段客服录音里用户说“没事我能接受”语气却明显发颤、语速变快。只做文本情感分析模型大概率判成中性甚至正向只做语音情感识别又拿不到“能接受”这个语义反讽。多模态情感识别要解决的就是这类单模态翻车的场景把语音信号和转写文本对齐后送进同一个模型让两路信息互相校正。这个方向在毕设里非常常见因为它同时踩中了 Python、多模态、语音、文本、情感识别这几个高频检索词又不像纯算法课题那样只能跑公开数据集。适合有 Python 基础、想做大模型 finetune 落地、但不知道从哪一步开始搭 pipeline 的人。下面按我实际搭过的一套方案从数据对齐讲到 finetune 参数再到推理验证把能复现的路径写清楚。2. 语音与文本怎么对齐多模态情感识别的数据层设计多模态项目翻车十次有八次不是模型问题是数据层没对齐。语音和文本的时间粒度天然不同一段 5 秒音频对应一句话但这句话里可能只有“还行吧”三个字带情感前面全是语气词。如果直接把整段音频的梅尔频谱和整句文本硬拼模型学到的是噪声。我一般会先做一层“情感单元”切分再谈融合。2.1 采样率、帧长与文本切片的对齐规则语音侧统一到 16kHz 单声道这是绝大多数预训练语音模型的输入标准。帧长 25ms、帧移 10ms用 40 维梅尔滤波器组得到每 10ms 一帧的特征序列。文本侧不要按字符切按标点和停顿切成语义片段每个片段记录起止时间戳。对齐规则是一个文本片段覆盖的时间窗内取对应帧的均值池化作为该片段的声学表示。这样每个文本 token 都能拿到一段语音特征而不是整段音频一个向量。import librosa import numpy as np def extract_mel(audio_path, sr16000, n_mels40, frame_len0.025, frame_shift0.010): # 统一采样率强制单声道 y, _ librosa.load(audio_path, srsr, monoTrue) # 预加重补偿高频衰减 y np.append(y[0], y[1:] - 0.97 * y[:-1]) # 提取梅尔频谱返回形状 (n_mels, time_frames) mel librosa.feature.melspectrogram( yy, srsr, n_melsn_mels, n_fftint(sr * frame_len), hop_lengthint(sr * frame_shift) ) # 转对数刻度动态范围压缩 log_mel librosa.power_to_db(mel, refnp.max) return log_mel.T # 转成 (time_frames, n_mels) 方便后续对齐这段代码的关键参数是n_fft和hop_length它们决定了时间分辨率。hop_length160对应 10ms 帧移和文本时间戳对齐时误差能控制在 20ms 内。power_to_db这步别省线性梅尔谱的动态范围太大直接送进网络会让梯度爆炸。如果音频里有长静音先做 VAD 切掉否则静音帧会稀释情感特征。2.2 文本转写与情感标签的弱监督构造毕设场景下逐句人工标注情感成本太高。常见做法是用一个开源语音转文本模型先出转写再用一个文本情感分类器打弱标签最后人工抽检修正。转写模型选中文效果稳定的即可不用追最新。情感标签分四类中性、正向、负向、混合。混合类专门留给“话里有话”的样本比如文本正向但语音负向。弱标签构造完后按 8:1:1 切训练、验证、测试切分时保证同一说话人的数据不跨集否则验证集准确率会虚高。import pandas as pd from sklearn.model_selection import train_test_split def split_by_speaker(df, test_size0.1, val_size0.1, seed42): # 先按说话人分组避免同一人数据泄漏到验证集 speakers df[speaker_id].unique() train_spk, test_spk train_test_split(speakers, test_sizetest_size, random_stateseed) train_spk, val_spk train_test_split(train_spk, test_sizeval_size/(1-test_size), random_stateseed) train df[df[speaker_id].isin(train_spk)] val df[df[speaker_id].isin(val_spk)] test df[df[speaker_id].isin(test_spk)] return train, val, testspeaker_id这列必须有没有就先用声纹聚类补一个。按说话人切分是多模态情感识别的血泪经验随机切分能让测试集准确率虚高 10 个点以上答辩时被问到泛化能力就露馅了。3. 融合架构选型早期融合、晚期融合还是交叉注意力数据对齐之后下一个决策点是两路特征在哪一层合并。早期融合把语音和文本特征直接拼接送进一个编码器实现简单但要求两路维度严格对齐且语音帧数远多于文本 token 数拼接后文本信息会被淹没。晚期融合各自过编码器最后在分类层做加权平均鲁棒但学不到跨模态交互。交叉注意力是当前主流让文本 token 去查询语音帧自动学出“哪个词对应哪段语气”。3.1 交叉注意力融合层的 PyTorch 实现我一般用文本侧做 query、语音侧做 key 和 value因为文本 token 数少、语义密度高让它主动去语音里找证据更合理。语音特征先过一个线性层降到和文本同维度再送进多头注意力。注意加 mask语音帧里有 padding 的部分要屏蔽掉否则注意力会跑到填充位上。import torch import torch.nn as nn class CrossModalFusion(nn.Module): def __init__(self, text_dim768, audio_dim40, hidden_dim256, n_heads4): super().__init__() # 语音特征升维到和文本同空间 self.audio_proj nn.Linear(audio_dim, hidden_dim) self.text_proj nn.Linear(text_dim, hidden_dim) # 文本做 query语音做 key/value self.cross_attn nn.MultiheadAttention( embed_dimhidden_dim, num_headsn_heads, batch_firstTrue ) self.norm nn.LayerNorm(hidden_dim) self.classifier nn.Linear(hidden_dim, 4) # 四分类情感 def forward(self, text_feat, audio_feat, audio_maskNone): # text_feat: (B, T_text, 768) audio_feat: (B, T_audio, 40) q self.text_proj(text_feat) kv self.audio_proj(audio_feat) # audio_mask 为 True 的位置是 padding需要屏蔽 attn_out, _ self.cross_attn(q, kv, kv, key_padding_maskaudio_mask) fused self.norm(q attn_out) # 残差连接稳定训练 # 对文本维度做均值池化得到整句表示 pooled fused.mean(dim1) return self.classifier(pooled)hidden_dim设 256 是显存和效果的折中毕设级显卡8G 显存跑 batch size 16 没问题。n_heads4够用再多在小数据集上容易过拟合。残差连接那行别删去掉后训练损失震荡明显。key_padding_mask的语义是 True 表示屏蔽和 PyTorch 官方文档一致别搞反。3.2 大模型 finetune 的接入方式冻结底层还是全量微调如果标题里提到大模型 finetune通常指用预训练语言模型做文本编码器再外挂语音分支。两种做法冻结语言模型底层只训融合层和分类头或者全量微调。毕设数据量一般几千到几万条全量微调容易灾难性遗忘我一般先冻结底层 8 层只微调顶层 4 层加融合模块。学习率设 2e-5融合层用 1e-4分层设学习率能稳住训练。from transformers import AutoModel def build_model(pretrained_namebert-base-chinese, freeze_layers8): text_encoder AutoModel.from_pretrained(pretrained_name) # 冻结底层参数只保留顶层可训练 for i, layer in enumerate(text_encoder.encoder.layer): if i freeze_layers: for p in layer.parameters(): p.requires_grad False return text_encoderfreeze_layers8是针对 12 层 BERT 的常见设置数据量过万可以降到 6。冻结后训练显存占用能降三分之一训练轮数 5 到 8 轮就收敛。如果验证集 F1 连续两轮不升就停别硬跑。4. 训练与推理避坑多模态情感识别最常见的五类翻车这一章按现象、原因、解决三段式写都是我在实际跑通这个 pipeline 时踩过的。4.1 验证集准确率 95%测试集掉到 60%现象训练时验证集 F1 一路涨到 0.95换测试集直接腰斩。原因按随机切分而不是按说话人切分同一人的语音片段同时出现在训练和验证集模型记住了说话人音色而非情感。解决切分前先按speaker_id分组用GroupShuffleSplit或手动按说话人划分确保同一人只出现在一个集合。4.2 语音分支梯度爆炸损失变成 NaN现象训练几个 batch 后 loss 变 NaN梯度范数飙到几千。原因梅尔频谱没做对数压缩或者音频没归一化幅值范围差异太大。解决power_to_db之后再做一次标准化减均值除标准差同时在融合层前加LayerNorm梯度裁剪设max_norm1.0。4.3 文本转写错误率高情感标签跟着错现象转写结果里“还行”变成“还星”弱标签直接判成中性。原因转写模型对口语、方言、背景噪声鲁棒性差。解决转写前先做降噪用noisereduce库或简单谱减法转写后加一层纠错用拼音相似度把明显错字映射回常见词。人工抽检 10% 样本错得多的说话人直接剔除。4.4 交叉注意力学不到东西融合后效果不如单模态现象融合模型 F1 比纯文本模型还低 2 个点。原因语音帧数远多于文本 token注意力被大量静音帧分散或者 mask 没加对padding 位参与了计算。解决先做 VAD 切掉静音控制语音帧数在文本 token 数的 3 到 5 倍检查key_padding_mask的 True/False 语义用一个小 batch 打印注意力权重确认。4.5 推理时音频长度不一致batch 拼不起来现象单条推理正常凑 batch 就报维度错误。原因不同音频帧数不同直接 stack 会失败。解决用torch.nn.utils.rnn.pad_sequence按 batch 内最大长度补齐同时生成 mask 传给融合层。文本侧同理用 tokenizer 的paddingTrue。5. 把方案跑成可复现结果验证指标与进阶技巧训练跑通只是第一步毕设答辩时老师会问“你怎么证明多模态比单模态好”。我一般做三组对照纯文本模型、纯语音模型、多模态融合模型在同一测试集上比 F1 和准确率。多模态通常比最好的单模态高 3 到 8 个点如果没高先查数据对齐和 mask别急着换模型。验证时别只看整体准确率按情感类别看混淆矩阵。混合类样本少容易被模型忽略可以给混合类加类别权重CrossEntropyLoss(weight...)里把混合类权重设成其他类的 2 倍。另外做一个消融实验去掉交叉注意力换成简单拼接看 F1 掉多少这个数字就是融合模块的贡献。from sklearn.metrics import f1_score, confusion_matrix import numpy as np def evaluate(model, dataloader, device): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for batch in dataloader: text batch[text_feat].to(device) audio batch[audio_feat].to(device) mask batch[audio_mask].to(device) logits model(text, audio, mask) preds logits.argmax(dim-1).cpu().numpy() all_preds.extend(preds) all_labels.extend(batch[label].numpy()) # 宏平均 F1对类别不均衡更敏感 f1 f1_score(all_labels, all_preds, averagemacro) cm confusion_matrix(all_labels, all_preds) return f1, cmaveragemacro这步很关键毕设数据里中性样本往往占一半以上用微平均会被中性类拉高宏平均才能暴露混合类的短板。混淆矩阵打出来看混合类和负向类有没有互相混混得多说明语音分支没学到语气信息回去检查梅尔特征和融合层。最后一个技巧推理阶段做测试时增强。对音频做轻微时间拉伸和加性噪声对文本做同义词替换各生成 3 个增强版本取平均 logits 再 argmax。这个操作不加训练成本通常能再涨 1 到 2 个点。我自己的习惯是每次改完融合结构先跑一版基线把 F1 记在表格里改一个变量跑一次别一次改三处否则涨了跌了都不知道是谁的功劳。希望帮到你。本文还有配套的精品资源点击获取