
简介面向抑郁症自动诊断的深度学习项目以AVEC2014为基准数据集采用残差网络构建识别模型为计算机视觉与医学图像处理方向的开发者提供完整可运行的示例。项目共包含十一个文件其中九个是Python源码模块分别负责视频抽帧、人脸检测与对齐、数据集封装、网络定义、训练验证与测试等流程另含环境依赖说明与运行文档整体大小仅为八KB非常轻量。截至目前已有三百一十四人学习下载。模块间划分清晰从数据预处理到模型训练链路完整项目还借助Tensorboard记录训练损失便于观察收敛情况开发者既可基于此复现实验也可将其迁移至其他面部情感识别任务中进一步改进。该方案工程完整适合作为课程设计或科研复现的参考。1. 当“检测抑郁”变成图像分类问题AVEC2014与Resnet的组合凭什么能落地听说“用深度学习诊断抑郁”很多人第一反应是要上LSTM、Transformer这类时间序列模型。但真正在AVEC2014数据集上最容易复现出结果的路线其实是把音视频片段转成频谱图当成图像任务交给Resnet去拟合一个抑郁量表得分。这个标题给出的方案就是一套完整闭环AVEC2014官方数据集做输入Resnet做骨干网络用python源码实现训练与评估流程运行说明里还带数据集的下载渠道。它能解决的具体问题包括快速建立一个抑郁倾向评分的baseline对比语音语调和视觉线索对量表得分的预测能力也适合学术研究与辅助筛查场景。想跑医疗AI方向的新人可以用它入门迁移学习熟手则能拿它做官方数据的对比实验。先说清楚边界这类系统只能做研究参考和辅助评估不能替代临床诊断。2. 把AVEC2014整理成Resnet能吃的输入预处理路径与标签设计2.1 AVEC2014抑郁子挑战的原始输入到底是什么AVEC2014是2014年国际音视觉情感挑战赛的项目名称抑郁子挑战使用的语料来自Freeform访谈。受试者在指定场景中和系统进行开放对话整个过程的音频和视频被同时记录下来。官方提供的标签是贝克抑郁量表BDI-II的自评得分范围0到63得分越高说明抑郁症状越严重。训练、验证、测试三个分区按受试者划分同一人的任何片段不会同时出现在训练集和测试集里。这个划分方式要求工程实现时格外留意——如果自己重新切数据集很容易把一个人切成多段后拆到两边造成身份泄漏。这个数据集适合做深度学习是因为抑郁线索在音调、语速、停顿、面部表情上都有体现。常见做法是分别处理音轨和画面两个模态独立建模后再做融合也可以只先跑音频一条线。官方还发布过一批预提取特征但用Resnet的路线通常不依赖它们而是直接从原始数据里学习。有一点要说清楚严格意义讲Resnet不是时序模型它对“一帧”特征敏感在一段语音里语速、停顿、能量变化能体现在频谱纹理上一张log-mel频谱图包含一到两秒的短时频谱结构足够Resnet捕捉到与量表得分相关的线索。数据集本体需要按官方流程申请压缩包里的运行说明会给出渠道和授权方式申请下来后才能放到本地目录开跑。2.2 预处理路径从audio/video到固定尺寸张量音频侧的标准做法是把原始wav转成log-mel频谱图。mel频谱把频率轴映射到近似人耳感知的刻度log压缩让能量分布更均匀得到的二维矩阵可以直接看成一张灰度图。下面的函数把任意时长的音频统一处理成224x224的log-mel图正好匹配ImageNet预训练Resnet的输入尺寸。import librosa import numpy as np import cv2 def audio_to_logmel(audio_path, sr16000, n_mels128, target_frames128): # 读取音频统一重采样到16kHz y, _ librosa.load(audio_path, srsr) # 转log-mel频谱得到 [n_mels, T] mel librosa.feature.melspectrogram(yy, srsr, n_melsn_mels, fmax8000) log_mel librosa.power_to_db(mel) # 统一时间长度不足补零超出截断 if log_mel.shape[1] target_frames: pad target_frames - log_mel.shape[1] log_mel np.pad(log_mel, ((0, 0), (0, pad)), modeconstant) else: log_mel log_mel[:, :target_frames] # 缩放到224x224 log_mel cv2.resize(log_mel, (224, 224)) # 标准化到零均值单位方差 mean, std log_mel.mean(), log_mel.std() log_mel (log_mel - mean) / (std 1e-6) # 复制成三通道匹配预训练权重 log_mel np.stack([log_mel] * 3, axis-1) return log_mel.astype(np.float32)逻辑说明librosa.load里指定sr16000是为了统一采样率语音任务里16kHz是常用值8kHz信息损失太大44.1kHz又没必要。melspectrogram把一维波形变成[n_mels, T]的二维结构power_to_db再做对数压缩。时间长度用target_frames128固定之后cv2.resize到224x224这一步让不同时长的音频最终shape一致。最后复制成三通道很关键Resnet第一层卷积的in_channels是3如果用单通道图要么自己改卷积层要么就得按这个方式复制复制通常更省事也不会明显改变特征分布。视频侧的baseline更简单用OpenCV逐帧读取视频按固定间隔取帧直接resize到224x224。整帧作为输入是成本最低的做法实际工程里更常见的是先做人脸检测只保留人脸区域再缩放。import cv2 def extract_frames(video_path, interval30, target_size(224, 224)): cap cv2.VideoCapture(video_path) frames [] idx 0 while True: ret, frame cap.read() if not ret: break if idx % interval 0: frame cv2.resize(frame, target_size) frames.append(frame) idx 1 cap.release() return np.stack(frames).astype(np.float32) / 255.0参数说明interval30表示每30帧取一帧在25到30fps的视频里大约每秒取一帧避免相邻帧高度相关导致训练集冗余。target_size224和音频侧保持一致。直接取整帧在背景干扰大的样本上效果有限后面可以做MTCNN或OpenCV自带的人脸检测器但第一步先用这个版本跑通数据管线再逐步加检测逻辑。2.3 标签处理回归、阈值、归一化AVEC2014抑郁子挑战的官方标签是连续分数但工程上通常同时跑两个设定连续回归和二分类。两者的标签形式、损失函数和评估指标都不一样做之前先想清楚要什么。任务设定标签形式损失函数评估指标常见用途连续回归BDI-II得分 / 63 归一化到0-1SmoothL1Loss或MSEMAE、RMSE官方评测、严重程度排序二分类BDI-II 14为阳性否则阴性CrossEntropyLossF1、ACC辅助筛查、倾向识别不要把0到63的原始得分直接丢给网络。归一化到0-1之后输出范围可控收敛更快推理时把预测值乘回63即可还原。二分类的阈值14是文献里常用的“具有抑郁症状”切分但它只是研究参考阈值不等于临床确诊标准。类别不均衡在二分类里很常见训练时用加权采样或者给CrossEntropyLoss加weight不然模型容易全部预测为多数类。预处理完成后整个数据集会被整理成两个部分图片张量和对应标签。下载到的原始数据一般要按运行说明放到固定目录训练脚本才能找到具体目录结构以你拿到的说明文档为准不要自己随意改路径。3. 用Resnet搭抑郁诊断模型深度选择、预训练权重与损失函数设计3.1 为什么是Resnet而不是时序模型动手深度学习做图像分类的人对残差结构都不会陌生。Resnet的核心贡献是解决深层网络的退化问题层数加深时训练准确率反而下降而残差连接让梯度可以绕过中间层直接回传所以网络能放心堆到几十层。在AVEC2014这个任务里输入是频谱图或视频帧本质上是图像特征提取Resnet天然适配。时序模型被很多人第一直觉选中是因为语音本身是时间序列。但实际跑起来会发现两个痛点一是音频被转成log-mel图后已经变成二维结构再用LSTM去处理需要重新展平等于放弃空间纹理信息二是LSTM对变长序列和帧对齐很敏感训练不稳定。Resnet做的是局部感受野上的卷积和池化频谱图中的纹理模式、能量带分布都能被卷积核捕捉到配合ImageNet预训练权重特征迁移效果比随机初始化的LSTM稳定得多。预训练权重在迁移学习里的价值在于网络前几层学到的是边缘、纹理、形状这类通用特征。音频频谱图在视觉上也有类似的纹理结构所以ImageNet上训出来的Resnet骨干可以直接复用在频谱图上后面的任务只需重新学最后的分类层或回归层。对AVEC2014这种受试者数量不算多的数据集从头训练一个深层网络很容易过拟合预训练加微调是更可靠的路径。3.2 ResNet18/34/50怎么选预训练模型加载与冻结策略模型深度特点适合什么情况ResNet18层数浅、参数量小、训练快、不易过拟合数据量小先跑通baselineResNet34中间档结构稍厚数据适中想多试几种学习率ResNet50bottleneck结构、表达能力最强数据量足够、显存充裕追求更高精度我的习惯是先上ResNet18把所有流程跑通确认预处理、数据划分、评估逻辑都正常再换ResNet50做精度对比。AVEC2014的样本规模撑不起一上来就训练ResNet50预训练特征在小数据上已经够用。换模型时只需要改一行代码后面的训练循环和评估函数完全不用动。预训练权重下载是个容易被忽略的环节。torchvision加载权重时会从官方源下载过程可能比较慢建议先把权重文件下载到本地缓存目录之后每次实验都从缓存加载。加载时要注意预训练模型最后的fc层是1000类输出和抑郁得分任务不匹配必须在建模时替换掉。冻结策略对AVEC2014这类小数据集很重要。常见做法是前两到三个epoch只训练新加的fc层把backbone的requires_grad设为False学习率可以稍高到1e-3两三个epoch后解冻backbone学习率降到1e-4做全模型微调。这样做的目的是先让新头适配任务再让预训练特征在很小的学习率下慢慢调整避免一上来就全量微调把预训练权重冲坏。3.3 把Resnet输出改造成抑郁分数回归模型定义是整个源码的核心部分。下面是一个完整的DepressionResnet实现支持快速切换模型深度和任务类型。import torch import torch.nn as nn import torchvision.models as models class DepressionResnet(nn.Module): def __init__(self, num_classes1, pretrainedTrue, model_nameresnet18): super().__init__() # 按名称加载主干网络 if model_name resnet18: self.backbone models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT if pretrained else None) elif model_name resnet50: self.backbone models.resnet50(weightsmodels.ResNet50_Weights.DEFAULT if pretrained else None) # 替换最后的全连接层 in_features self.backbone.fc.in_features self.backbone.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, 64), nn.ReLU(), nn.Linear(64, num_classes) ) def forward(self, x): # x: [B, 3, 224, 224] return self.backbone(x)逻辑说明model_name控制使用ResNet18还是ResNet50实验对比时只改参数不换代码。pretrainedTrue时加载ImageNet预训练权重这是迁移学习的关键。原来fc层的输入维度从in_features取出然后替换成Dropout Linear ReLU Linear的结构。加Dropout是因为样本量小新任务头只有两个全连接层也容易过拟合num_classes1对应连续回归num_classes2对应二分类改一个参数就能切换。在forward里直接返回backbone输出回归模式下输出是[B, 1]训练时用squeeze(1)去掉多余维度。接入数据管线时只需要把第2章的预处理函数放在Dataset的__getitem__里返回(image_tensor, label_tensor)即可训练和验证共用同一套数据管道。损失函数方面回归任务优先用nn.SmoothL1Loss()也就是Huber loss。它在误差绝对值小于1时按平方惩罚大于1时退化为线性惩罚对BDI分数这种天然存在大误差样本的回归问题比MSE更稳。分类任务用nn.CrossEntropyLoss()。L2正则化在PyTorch里通常通过优化器的weight_decay参数控制不需要手动往loss里加额外项这和使用weight_decay的目的是同一个防止权重过大导致过拟合。4. 把训练与评估跑通最小python源码脚本与运行环境说明4.1 训练循环的标准骨架数据加载、loss、优化器、评估数据准备好了模型也定义好了训练脚本就可以写得非常短。核心训练循环如下假设train_dataset和val_dataset已经按第2章的方式构建好。from torch.utils.data import DataLoader import torch.nn as nn train_loader DataLoader(train_dataset, batch_size16, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size16, shuffleFalse, num_workers4) model DepressionResnet(pretrainedTrue, model_nameresnet18).cuda() criterion nn.SmoothL1Loss() optimizer torch.optim.SGD(model.parameters(), lr1e-4, momentum0.9, weight_decay1e-4) for epoch in range(30): model.train() total_loss 0.0 for images, labels in train_loader: images, labels images.cuda(), labels.cuda() optimizer.zero_grad() preds model(images).squeeze(1) loss criterion(preds, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) val_mae, val_rmse, val_f1 evaluate(model, val_loader) print(fepoch {epoch 1}: loss{total_loss / len(train_loader.dataset):.4f}, fmae{val_mae:.3f}, rmse{val_rmse:.3f}, f1{val_f1:.3f})参数说明batch_size16是默认值显存不够就先降到8再降4。num_workers4让数据加载在子进程并行但Windows上如果一直卡死或报错先把它改成0再排查。优化器用SGD加momentum小数据上训练比Adam更稳学习率1e-4适合微调预训练权重如果只训练新fc层学习率可以放到1e-3。每个epoch结束在验证集上算一次MAE、RMSE和F1顺便看loss是否还在下降这个输出信息足够判断模型状态。如果遇到训练不收敛优先检查的不是调参而是数据和标签对不对。把预处理函数单独跑一遍打印出一个batch的shape和标签范围确认维度是[16, 3, 224, 224]、标签在0到1之间再回来调学习率。这个检查只需要五分钟能省几个小时排错。4.2 评估指标别只看lossMAE、RMSE、F1的组合用法AVEC2014官方很看重MAE和RMSE但既然标题叫“诊断系统”分类指标就不能丢。回归指标好不代表能区分患者和非患者所以每次评估要同时算三个指标。import numpy as np from sklearn.metrics import f1_score def evaluate(model, loader, threshold14.0): model.eval() preds_all, labels_all [], [] with torch.no_grad(): for images, labels in loader: pred model(images.cuda()).squeeze(1).cpu() preds_all.append(pred) labels_all.append(labels) preds_all torch.cat(preds_all).numpy() * 63.0 # 还原真实分数 labels_all torch.cat(labels_all).numpy() * 63.0 mae np.mean(np.abs(preds_all - labels_all)) rmse np.sqrt(np.mean((preds_all - labels_all) ** 2)) pred_cls preds_all threshold true_cls labels_all threshold f1 f1_score(true_cls, pred_cls) return mae, rmse, f1逻辑说明模型输出是归一化后的0到1分数所以评估时统一乘回63再用threshold14.0做阳性判定。三个指标一起看能暴露很多问题MAE低但RMSE高说明存在个别预测偏差很大的样本MAE和RMSE都好但F1差说明预测分数围绕在阈值附近分类边界不稳定模型排序能力弱。f1_score来自scikit-learn二分类情况下直接传两个数组即可。注意所有样本要集中在同一个数组里算不能每个batch单独算再平均那样会把分布信息抹掉。分类标签不均衡时F1比ACC有意义。AVEC2014的样本分布里正常组和抑郁组比例并不是均衡的只看准确率会被多数类带偏F1能反映少数类的检出能力。4.3 python环境与深度学习环境配置依赖清单和三个检查点源码包里的运行说明通常会列依赖但不同人的机器环境差异很大。一份可复用的requirements.txt大概是这样的python3.8 torch1.13 torchvision0.14 opencv-python4.5 librosa0.10 numpy1.21 pandas1.3 scikit-learn1.0torch和torchvision的版本必须匹配建议用官方安装命令装GPU版如果机器只有CPU也能跑通流程只是训练慢不少batch_size要相应调小。librosa依赖ffmpeg装好后先执行import librosa确认没有报错。opencv-python在部分Linux服务器上需要额外的系统库解压后运行import cv2验证即可。跑通前做三个检查。第一步用一个batch的数据跑forward确认输出shape是[B]而不是[B, 1, 1]第二步跑一个完整epoch看到loss有下降趋势第三步在训练前单独调一次evaluate确认标签还原逻辑没有写错。这三个检查做完剩下的就是等训练结束看指标了。5. 避坑与排查AVEC2014跑抑郁诊断最常见的5个翻车点AVEC2014看起来链路简单但每一环都有隐蔽的坑。下面5条是根据我实际调试经验整理的高频问题按“现象、原因、解决”三层说透。5.1 音频长短不一batch在训练中途直接崩掉现象训练到第二个epoch突然报tensor shape mismatch错误堆栈指向某个具体样本。原因不同受试者的访谈录音长度差异极大有的30秒有的好几分钟。预处理函数虽然写了定长逻辑但实际执行时可能有某个样本走了不同分支或者cv2.resize的输入不是预期的二维数组导致shape错乱。解决把预处理函数统一放在Dataset的__getitem__里保证每个样本都经过同一个函数处理。训练前先遍历一遍整个数据集打印每张图的shape确认没有漏网之鱼。我习惯在数据加载完、训练开始前加一行断言assert img.shape (3, 224, 224)防止运行中途才发现问题。5.2 标签归一化不一致训练用0-1评估却还原错现象训练loss降得很漂亮验证MAE却高得离谱甚至大于63。原因训练时把标签除以63评估时忘记乘回63或者乘了两次63。两处逻辑分散在不同函数里改了一处忘了另一处。解决把归一化和还原封装成同一个模块的成对函数比如normalize_label(y)和denormalize_label(y)用一个SCALE 63.0常量统一管理。训练和评估都只调用对应函数绝不手写乘除。5.3 回归到均值陷阱MAE不高但预测分数全挤在一起现象验证MAE在6到8之间看起来还行画出预测分布发现几乎所有样本都被预测到20到30之间低分组完全出不来。原因AVEC2014的标签本身有均值回归效应。样本量小、噪声大网络学到一个“预测中间值总比冒险猜两端划算”的loss局部最优于是退化成均值预测。SmoothL1对中误差的惩罚本来就温和进一步助长了这个倾向。解决最有效的是把回归和分类结合起来。额外加一个二分类头让网络同时学“分数高低”和“是否阳性”分类头能强迫网络拉开低分组和高分组的特征分布。另一个做法是重采样把BDI分数分箱后对少数段做过采样避免训练集被中间分数主导。还有一种比较直接的办法对预测分数做校准看散点图而不是只看MAE。5.4 预训练权重加载失败size mismatch报错现象model.load_state_dict(state_dict)报size mismatch for backbone.fc.weight。原因加载的是完整state_dict但自己改了fc层的结构原来1000类输出的权重和新head的64维输入对不上。torchvision的预训练权重文件既包含卷积层也包含fc层直接整体加载必然报错。解决加载时只取backbone部分的卷积层权重把fc过滤掉。常见做法是用strictFalse或者手动过滤key。state_dict torch.load(resnet18_weight.pth, map_locationcpu) new_state {k: v for k, v in state_dict.items() if k.startswith(backbone.) and fc not in k} model.backbone.load_state_dict(new_state, strictFalse)说明过滤条件里k.startswith(backbone.)是匹配模型里的主干网络参数fc not in k排除掉最后一层全连接。这样加载后前面卷积层用预训练权重新head保持随机初始化正好配合冻结微调策略。5.5 受试者身份泄漏验证指标虚高换人测试崩掉现象自己随机划分训练集和验证集验证F1能冲到0.75以上换成官方划分后F1掉到0.55。原因AVEC2014每个受试者有多个片段随机切分时同一人的片段被分到两边。模型学到的是“记住这个人”而不是“识别抑郁症状”身份泄漏让验证集变得毫无意义。解决按subject_id分组划分数据集。用GroupKFold按人分组做交叉验证或者直接用官方给出的train/dev划分。训练开始前检查一下train和val的subject_id集合交集为空才算合格。这个检查应该写进数据划分函数里用一行assert set(train_subjects).isdisjoint(set(val_subjects))兜底。6. 别让模型“盲猜均值”预测分布检查与分数可信度验证模型训练完MAE低不能直接欢呼。第一步是检查预测分数的分布是否和真实标签分布一致。把测试集所有预测值收集起来看均值、标准差、相关系数这个操作比任何loss都更能说明模型学到了什么。import numpy as np from scipy.stats import pearsonr preds np.array(preds_all) labels np.array(labels_all) print(pred mean/std:, preds.mean().round(2), preds.std().round(2)) print(label mean/std:, labels.mean().round(2), labels.std().round(2)) print(corr:, pearsonr(preds, labels)[0].round(3))一个有效模型的预测标准差应该接近真实标签标准差相关系数至少要有明显的正相关性。如果预测均值接近标签均值但预测标准差只有标签标准差的四分之一那基本可以判定模型在回归到均值预测分数没有区分度MAE再低也只是数字游戏。分数可信度还可以结合推理时的多片段一致性来验证。训练时如果采用切段策略推理时同一受试者的多个片段会得到多个预测分数这些分数的方差能反映模型对这个人的稳定程度。多片段预测情况建议处理方差小且远离阈值输出高置信度可直接辅助参考方差小但靠近阈值输出低置信度标记为临界样本方差大输出低置信度建议人工复核或重新采样这个做法在工程上很有价值。抑郁倾向筛查最重要的不是把所有样本都预测准而是把置信度低的样本识别出来交给人工判断。模型输出一个分数加一个可信度等级比单纯输出分数更实用。我最早跑通这套流程时验证集MAE降到了6以下一度以为模型已经能用了。后来把预测分数画成直方图才发现几乎所有样本都被压到20到30之间真实低分组完全出不来。从那次之后分布对比图和多片段一致性检查成了我每次训练结束后的第一步没有通过分布检查的模型即使loss好看也不会继续调。这个习惯帮我避免了很多次虚假的“实验成功”希望帮到你。本文还有配套的精品资源点击获取