公司动态
基于AVEC2014与ResNet的抑郁症自动诊断:语谱图+PyTorch工程实践
简介本资源是一套面向人工智能与医疗健康交叉领域初学者及研究者的抑郁症自动诊断实践项目基于AVEC2014多模态数据集含语音与面部视频与ResNet深度神经网络聚焦于利用面部表情图像实现抑郁症辅助判别。资源包含完整Python工程代码与配套说明共11个文件9个核心.py模块涵盖数据预处理、加载、模型构建、训练/验证/测试全流程、1个依赖清单requirements.txt及1份README.md使用指南总大小仅8KB轻量易部署。项目代码结构清晰模块职责分明覆盖从原始数据读取、ResNet特征提取、端到端训练到模型评估的全链路实现适合作为深度学习在精神健康领域落地的入门范例。目前已有76人下载学习可直接运行复现实验快速掌握医疗影像分类任务中数据预处理、残差网络调用及二分类评估的关键技术点。 做抑郁症自动诊断这个方向最麻烦的其实不是模型而是数据。AVEC2014数据集是少有的、带真实临床评分的精神健康公开数据配合ResNet做基线分类/回归是很多团队入门这个领域的第一站。这篇文章把我自己跑通这个项目时踩的坑、调参的思路和代码组织方式完整写出来希望能给正在做类似任务的你省点时间。项目其实不复杂输入是受访者的录音或视频里的音频轨我把它转成语谱图用ResNet18提取特征最后回归出PHQ-8抑郁评分。整套流程用Python PyTorch实现开发环境是Windows 11 CUDA 11.7 PyTorch 1.13单卡RTX 3060就能跑完训练和推理。下面从思路、数据处理、模型训练、调参经验四个部分展开全程按我实际操作的顺序来写。1. 项目思路拆解为什么是AVEC2014 ResNet1.1 这个项目实际解决什么问题先说结论这个任务本质上是多媒体特征到连续临床评分之间的映射问题。AVEC2014里面的录音不是随便找的语音而是访谈场景下的自然对话时长从几十秒到几分钟不等。这种数据有一个好处——它带有人工标注的PHQ-8分数这是临床上病人健康问卷的8项版本0到24分分数越高抑郁风险越大。所以任务天然是一个回归问题输入一段视频/音频输出一个0到24之间的分数。当然也可以把它转成二分类比如以10分为界大于等于10定义为有抑郁倾向但这样会丢掉中间地带的信息。我这次做的是回归为主、分类评估为辅既能输出严重程度也能算阈值指标。从工程角度看这个项目可以拆成四个环节数据读取与预处理、特征表示、模型训练、评估分析。每个环节都有不少容易踩的坑后面章节我会逐个说明。单看模型的话它其实就是一个普通的图像回归任务难点集中在数据侧和如何在小样本上控制过拟合。1.2 选型逻辑数据集和网络各自不可替代的原因AVEC2014在这个方向几乎算是事实标准。AVECAudio/Visual Emotion Challenge从2011年开始举办2014年的子挑战之一就是抑郁症检测数据由录音、录像和人工标注组成组织者统一提供了训练集、开发集和测试集的划分。相比自己采集数据它最大的价值是样本经过伦理审批、标签经过专业人员核验、实验条件相对一致。做研究或者做毕设拿它当基准数据集最省事。选ResNet是因为它把“图像”分类/回归任务的基线拔高了一截。直接用浅层CNN处理语谱图也不是不行但抑郁症相关的语音线索往往分散在多个频带和时间片段上需要网络有足够深的感受野去组合这些线索。ResNet通过残差连接解决了深层网络退化问题可以放心堆深度。而且ResNet18参数量只有约11M在样本量很小的医疗数据集上不容易一发不可收拾地过拟合训练速度也快属于“稳”字当头的选择。对比一下另外两个常见方案用LSTM/Transformer直接处理时序特征需要先手动提取MFCC、eGeMAPS等声学特征特征工程的工作量不小而且手工特征会丢掉一部分语谱图上的细节用VGG或DenseNet要么参数多要么在公开预训练权重上不如ResNet好找。综合来看ResNet18语谱图是性价比最高的入门路线后续想换更强的网络代码改动也小。2. 数据集处理从视频到224x224语谱图2.1 AVEC2014数据结构和标签说明拿到压缩包后先别急着训练把目录结构摸清楚。AVEC2014的原始数据一般是这样组织的训练集目录每个受访者一个子目录里面有视频文件通常还有对应的转录文本开发集目录结构同上测试集目录结构同上但标签不公开需要提交到官方平台评测每个受访者的ID是一个编号标签文件里记录了ID和对应的PHQ-8总分。注意AVEC2014用的是PHQ-8而不是PHQ-9因为第9项涉及自伤念头在公开数据集里通常被去掉所以总分范围是0到24。训练集和开发集各自大概有50名受访者加起来才100个左右。这个样本量在深度学习方法里算是非常小的所以在数据处理阶段就想好了两条对策一是切窗口增加样本数量二是不把官方训练集/开发集拆开使用而是合并之后做交叉验证。2.2 音频抽离与语谱图生成的完整流程附代码我采用的方案是只使用音频轨不处理视频画面。原因有几个视频数据量是音频的几十倍处理起来慢访谈场景下画面信息表情、姿态确实有用但提取起来需要额外的人脸检测、对齐流程对入门项目来说成本太高语音韵律、语速、能量变化本身就和抑郁症状高度相关纯音频能达到不错的效果。第一步从视频里抽音频。我习惯用ffmpeg统一转成16kHz单声道wav采样率统一很重要否则后面算Mel频谱时频率轴会错位。ffmpeg -i input.mp4 -ar 16000 -ac 1 -vn output.wav第二步把长音频切窗。这里有个策略问题每个视频访谈时长不一样短的几十秒长的几分钟。我按4秒窗口、2秒步长切平均每个受访者能切出几十个窗口样本量一下子从100左右涨到几千。切窗的目的是让模型看到不同时间片段学习“语速慢”“停顿多”这类局部线索。第三步把每个窗口转成Log-Mel语谱图。Mel刻度更接近人耳听觉感知Log压缩能让数值分布更稳定。我直接用torchaudio实现代码量很少import torch import torchaudio def load_audio(path, target_sr16000): waveform, sr torchaudio.load(path) if sr ! target_sr: waveform torchaudio.functional.resample(waveform, sr, target_sr) return waveform def wav_to_log_mel(waveform, n_mels224, target_len224, devicecpu): waveform waveform.to(device) mel_spec torchaudio.transforms.MelSpectrogram( sample_rate16000, n_fft1024, hop_length512, n_melsn_mels )(waveform) log_mel torch.log(mel_spec 1e-6) # 固定长度不足补零超出截断 if log_mel.size(-1) target_len: pad target_len - log_mel.size(-1) log_mel torch.nn.functional.pad(log_mel, (0, pad)) else: log_mel log_mel[:, :, :target_len] return log_mel这里把n_mels设成224是为了直接匹配ResNet的输入尺寸省得后面再resize。如果算出来的频谱时间维度不足224帧就补零超过就截断。补零不是最优做法但在样本量不足时比直接丢弃窗口要好。2.3 训练集/验证集划分要避开的坑样本划分是整个项目里最影响结果可信度的一步。AVEC2014官方虽然给了train/dev/test的划分但test标签不公开很多人就只在train上训练、在dev上调参。这样有两个问题一是样本只用了50个模型很容易过拟合二是固定划分的偶然性太大运气差点可能把高分样本全分到验证集里导致验证MAE虚低或者虚高。我自己的做法是把官方的train和dev合并然后做5折交叉验证。每个fold用80%训练、20%验证最终报告5折的平均MAE和标准差。官方test集只在最后全量训练后用来做一次推理并且不参与任何调参决策。这样得到的指标更接近模型的真实水平。需要注意切窗后同一受访者的多个窗口不能同时出现在训练集和验证集里。换句话说切窗是拿到某个受访者全部窗口之后才做的动作而划分必须在受访者ID维度上进行否则数据和标签会串扰评估结果虚高到没有参考价值。from sklearn.model_selection import KFold participant_ids sorted(set(all_labels[Participant_ID])) kf KFold(n_splits5, shuffleTrue, random_state42) for fold, (train_idx, val_idx) in enumerate(kf.split(participant_ids)): train_pids [participant_ids[i] for i in train_idx] val_pids [participant_ids[i] for i in val_idx] # 根据pid过滤出对应窗口 train_samples all_samples[all_samples[Participant_ID].isin(train_pids)] val_samples all_samples[all_samples[Participant_ID].isin(val_pids)]另一个容易被忽略的点标签要复制给该受访者的所有窗口。比如某位受访者PHQ-815他切出40个窗口这40个窗口的标签全部是15。同一个受访者的不同窗口之间高度相似如果这些窗口过多模型可能会偷懒去记“这个人说话慢所以分数高”而不是学真正的语音模式。为了缓解这个问题训练时每个epoch可以随机从每个受访者抽取固定数量的窗口参与训练而不是把全部窗口都喂进去。3. 模型搭建与训练从ResNet18到回归头3.1 网络结构改造与预训练权重复用我用的是torchvision里自带的ResNet18改动只有一小块把最后的全连接层从1000类改成单节点输出。因为任务是回归不是分类最后一层不需要Softmax直接输出一个0到24之间的连续值。import torch.nn as nn from torchvision.models import resnet18, ResNet18_Weights model resnet18(weightsResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, 1)这里有一个很关键的细节预训练模型的输入是三通道RGB图像而语谱图默认是单通道的二维矩阵。有两种处理方式。第一种是只把语谱图转成单通道同时修改第一个卷积层Conv2d(3,...)改为Conv2d(1,...)但这样会导致这层的预训练权重没法直接复用需要重新初始化前面的特征提取能力会弱很多。第二种是把单通道复制成三通道也就是沿通道维拼接三次不修改第一层结构直接用全套ImageNet预训练权重。我实测下来第二种收敛更快验证集MAE也更低所以最后采用了三通道输入。输入尺寸方面ResNet对输入没有硬性要求但224x224最稳妥。前面提到n_mels设成224时间维度也固定到224这样数据流从输入到输出完全没有尺寸转换训练速度也快。3.2 训练参数配置数据增强、优化器、损失函数训练配置是这类小样本项目的灵魂。先说损失函数回归任务最直接的是MSE Loss它对大误差样本的惩罚更重比较符合临床场景里“别差太远”的诉求。我同时也试过Smooth L1 Loss它在大误差处梯度更温和不容易被个别异常样本带偏但最终MAE没有明显改善所以还是用回MSE。优化器选了AdamW学习率1e-4weight_decay设1e-5。为什么不选SGDSGD配合momentum在小样本上也能工作但对学习率敏感需要手动调整的余地更大AdamW收敛稳对新手友好。ResNet18在这个任务上不需要太长的训练35个epoch左右就够了。学习率调度用余弦退火前3个epoch做线性warmup先把学习率从很低的1e-6升到1e-4。数据增强方面我做了四件事随机切窗位置在4秒窗口的基础上再随机偏移0.5秒、随机音量增益0.8到1.2倍、加高斯噪声信噪比约20dB、SpecAugment。SpecAugment是语音识别里常用的增强手段随机mask掉一小段频率或时间。用的时候注意mask范围别太大经验值是时间mask最多15帧、频率mask最多8个Mel bin太大容易把语谱图上重要的共振峰信息盖掉。train_transform nn.Sequential( # 频率掩码 torchaudio.transforms.FrequencyMasking(freq_mask_param8), # 时间掩码 torchaudio.transforms.TimeMasking(time_mask_param15), )优化器、调度器、损失函数组合起来以后每个epoch还要做一件事记录train MAE和val MAE并且只在val MAE创新低的时候保存模型权重。不要用最后一个epoch的权重因为训练后期的模型在验证集上不一定是最优的。early stopping阈值我设在12个epoch没有改善就停止。3.3 评估指标选哪个更合理回归任务最常用的三个指标MAE、RMSE、Pearson相关系数。MAE最直观它表示平均误差多少分比如MAE4.0说明平均预测值和真实分差4分。RMSE对大误差更敏感如果样本里有几个预测特别离谱的RMSE会明显大于MAE。相关系数衡量的是预测值和真实值的变化趋势是否一致但它对整体偏移不敏感比如预测值全部比真实值高2分相关系数可能依然很高。我的5折交叉验证结果大概在MAE 4.6左右RMSE 5.7相关系数0.5到0.6之间。这个水平能说明方案可行但离临床应用还差得远。在AVEC历年挑战赛里这个分数处于中间偏上的位置。除了回归指标我还会用PHQ-810作为阈值做一个二分类评估算出F1分数。这里有一个坑10分是临床上常用的中度抑郁分界但在这个数据集上最佳分类阈值不一定恰好是10。我一般会在验证集上扫描7到14之间的所有整数阈值选F1最高的那个阈值来评估。这样更公平也更能反映模型的实际分辨能力。4. 掉坑记录与精度提升实战4.1 六个高频问题排查速查表这个项目我在复现过程中遇到不少问题有些是数据相关的有些是代码相关的。整理成了一张表方便你对照排查现象可能原因解决办法ffmpeg抽音频失败压缩包里视频容器格式不常见先用ffprobe查看流信息确认编码后再指定-c:a pcm_s16le语谱图尺寸和标签数量对不上长音频切窗数量不同标签没对齐按Participant_ID填充标签确保每条样本都有唯一pid验证集MAE很低但测试集很差窗口级划分泄露了同受访者数据改成按受访者ID划分严禁同一pid同时出现在train/val训练后期val loss飙升学习率过大或过拟合降低学习率到3e-5增加weight_decay提前早停预测值集中在均值附近模型学不到信号输出被bias主导降低lr、加warmup、检查输入语谱图是否过暗/过亮加载预训练权重报key mismatch修改了fc或conv1层用load_state_dict(strictFalse)只加载backbone部分权重其中“预测值集中在均值附近”这个坑最容易误导人。我一开始见到这个现象以为是特征没提好来回换网络结构折腾了一周才发现是学习率太高。语谱图数据的数值范围和人脸图像数据差别很大预训练模型在ImageNet上学习的统计量不一定适应这里所以第一层的学习率需要特别小。后来我在优化器里给backbone和fc设置了不同学习率backbone用5e-5fc用1e-4效果明显好转。4.2 把MAE再压低一点的三个实用技巧技巧一多窗口预测取中位数。训练时模型见过很多4秒窗口推理时不要只取一个窗口的预测而是把一段录音切成若干个不重叠的窗口每个窗口单独预测最后取中位数。中位数比平均值更稳因为个别窗口的异常预测不会被拉进去太多。我试过把单窗口MAE从5.2降到多窗口的4.7提升明显。技巧二用多任务约束特征。在最后的回归头旁边加一个二分类分支输入同一个backbone的特征输出“PHQ-8是否10”的概率。回归分支用MSE Loss分类分支用CrossEntropy Loss两个loss加权相加我设的权重是回归1.0、分类0.5。这样分类任务给backbone提供了一个离散的、更稳定的监督信号回归结果反而更稳。技巧三测试时做简单的特征增强。推理时对同一段音频分别使用原始语谱图、频率稍微平移的语谱图、加了轻微噪声的语谱图得到多个预测结果再取平均。这种方式叫test-time augmentation虽然会多花一点推理时间但在医疗场景下稳定性比速度重要。三个增强版本平均下来MAE还能再降0.2到0.3。最后再分享一个小技巧把训练集里每个受访者的窗口数量控制在一个范围比如最少20个、最多50个。窗口太少会学不到特征窗口太多会让模型偏向记忆特定受访者。训练时对样本做加权采样让每个受访者的总样本数大致均衡。这个改动对最终的泛化能力有实打实的帮助。我在实际操作中最深的体会是这个项目真正的天花板不在模型结构而在数据处理方式和对小样本过拟合的控制。AVEC2014公开十几年了想靠它刷出一个惊人的精度不太现实但把它当作学习“怎样用深度学习解决医疗数据问题”的练习项目价值非常大。如果你也在跑这个方向的代码建议多花时间在特征可视化上——把语谱图画出来和音频一起听一听很多直觉就是这样培养出来的。本文还有配套的精品资源点击获取