简介:基于AVEC2014数据集与ResNet网络的抑郁症诊断Python源码,面向具备一定Python基础、希望入门深度学习医疗应用的开发者,完整覆盖从视频帧提取、人脸预处理、模型设计到训练测试的端到端流程,代码结构清晰,便于二次开发。压缩包内共11个文件,以9个Python脚本为核心,分别承担视频均匀抽帧、MTCNN人脸对齐裁剪、标签合并生成CSV、模型网络结构定义、DataLoader迭代器封装、Tensorboard训练过程记录以及训练/验证/测试等环节;另含1个依赖清单txt和1个README运行说明md,整体仅8KB,结构紧凑,便于按模块对照学习。目前已有313人浏览学习。资源附有AVEC2014数据集下载地址及提取码,预处理阶段采用每视频取100帧并保留原始标签的策略,同时给出MTCNN工具的人脸对齐裁剪思路;各脚本功能划分清晰,既可直接复现抑郁症识别实验,也可作为课程设计、毕业设计或科研基线,帮助读者掌握ResNet在时序视觉任务中的调参与数据处理方法,并可将这套流程拓展到情绪识别、疲劳检测等类似场景,降低复现与实际部署门槛。
1. 用AVEC2014和ResNet把抑郁症诊断做成可复现的深度学习项目
抑郁评分靠访谈和量表,又依赖医生主观判断,换个环境结果可能就变样。AVEC2014数据集把真人访谈的音视频放出来,配合BDI-II量表分数,等于给了一个用深度学习量化抑郁程度的公开基准。ResNet在这类任务里很能打,它不像传统手工特征那样需要先设计“什么指标代表情绪”,而是直接从声谱和视觉帧里学表征,迁移预训练权重后效果通常优于手工特征。这里就按一套可运行的python源码路径来讲,怎么把AVEC2014数据接进来、用ResNet做回归、把训练和推理流程跑通。适合刚接触医疗AI但不想只会调包的研究生和工程师。
2. AVEC2014数据集结构与ResNet在抑郁症识别中的选型理由
2.1 AVEC2014的抑郁评估任务和数据模态
AVEC2014是Audio/Visual Emotion Challenge的年度benchmark,抑郁子任务给的是真人访谈视频,每段几十秒到几分钟不等,分成训练集、验证集和测试集。标签是BDI-II问卷总分,从0到63,越高抑郁越重。官方要求参赛者做的是“连续分数预测”,最后用平均绝对误差(MAE)和均方根误差(RMSE)来评估预测值和真实值之间的偏差。因为标签不是简单的好/坏二分类,所以模型最后应该是一个回归头,而不是常见的softmax分类器。
数据本身有三个模态:视频帧,原始音频(以及从音频提取的底层特征),还有口头内容转录。很多早期工作是把音频的MFCC、视频的LBP或Gabor特征拼起来,再上SVR或随机森林。这类特征的麻烦在于,特征提取和模型训练是两套独立流程,任何一步的参数变了,结果就得从头调一遍。深度学习方法直接学原始信号表示,把“特征工程”变成“表示学习”,这也是AVEC2014之后主流方案转向深度学习的主要原因,也正因如此,才开始出现大量基于预训练模型和开源代码的复现工作。
我在设计这套源码时,第一份消融实验就是“单模态 vs 多模态”。只拿音频做出来的模型已经能达到在验证集上可用的精度,但加上视觉帧后,MAE会再降一点。注意AVEC2014数据集下载需要从官网登记使用协议,解压后的原始目录通常是recordings和transcripts。拿到手建议先不要急着训练,把数据目录结构理清楚,因为后续的Dataset类会依赖这个结构。如果你手里拿到的压缩包目录名不对,宁可在代码里加一层路径映射,也不要把文件全部铺散到同一个文件夹里。
下面是一个把音频片段转成Mel频谱图的片段,这是喂给ResNet之前最常见的编码方式:
import librosa import numpy as np import soundfile as sf def audio_to_mel(path, sr=16000, n_mels=64, fmax=8000, hop_length=160, duration=None): # 读取音频,统一采样率到16kHz y, _ = sf.read(path, dtype='float32', always_2d=True) y = y.mean(axis=1) if duration is not None: y = y[:int(sr * duration)] # 计算对数Mel频谱,形状为 (n_mels, 帧数) mel = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=n_mels, hop_length=hop_length, fmax=fmax) log_mel = librosa.power_to_db(mel) return log_mel.astype(np.float32)这段代码直接把双声道取均值转单声道,再切成一定时长。hop_length=160在16kHz采样率下对应10ms一帧,视觉帧率通常也是10fps左右,后面如果要音视频对齐时间轴,这个粒度会比较方便。n_mels=64是平衡分辨率与计算量的常用值,AVEC2014官方特征包里的尺寸也接近这个范围。Mel频谱本质上把声音按人耳频率感知做了压缩,ResNet的2D卷积可以把它当成灰度图处理,只是通道数从1扩展到3后效果不一定更好,后续会讲到。
把几种模态可用的表示方式放在一起看,能帮助决定训练时数据管线的输入结构:
| 模态 | 表示方式 | 适合的网络输入 | 常见做法 |
|---|---|---|---|
| 音频 | Mel频谱 / 原始波形 | 2D:64xT / 1D:T | ResNet2D / WaveNet |
| 视频 | RGB帧序列 / 光流 | 2D:单帧或3帧堆叠 | ResNet2D + 时序池化 |
| 文本 | 词向量 | 1D序列 | LSTM / Transformer |
这里的“适合”不是绝对的,工程上优先选择能直接用预训练模型的表示,能少走很多弯路。如果你有现成的官方声学特征,也可以把它们当作额外输入拼在Mel频谱旁边,但要注意特征源采样率是否一致。
2.2 为什么用ResNet而不是传统手工特征
在AVEC2014这样的规模下,从头训练一个很深的CNN非常容易过拟合。ResNet最大的贡献是残差连接,把目标映射从“恒等函数”变成“残差值”,网络加深时梯度可以顺着skip connection直接回流,所以50层、101层都能在合理数据量下训练稳定。用公式看就是y = F(x) + x,当F(x)学不到东西时,输出至少还能保持输入,这让退化问题基本消失。
用ResNet的另一个理由是预训练模型非常容易获得。torchvision里就有resnet18、resnet50的ImageNet权重,虽然ImageNet是自然图像,不是声谱图,但前几层学到的边缘、纹理结构对Mel频谱一样有效。我试过直接加载预训练权重做特征提取器,冻结前几层只微调后面几层,收敛速度比随机初始化快很多,最终MAE也更低。不过在全文搜索“resnet预训练模型”时,会看到很多只改了最后一层的教程,放在这个项目里是不够的,因为输入通道和输出维度都变了。
要注意,ImageNet预训练是为了1000类分类,最后一层全连接是1000维,我们换成1维回归之后,预训练权重里的FC层用不上。一般做法是保留卷积主干,丢掉fc,替换成Linear(2048, 512) + ReLU + Dropout + Linear(512, 1)。这个改动虽然简单,但回归头怎么设计对结果影响不小。我建议中间层的维度不要超过256,因为AVEC2014的样本量也就是几百段视频,回归头太宽反而会放大噪声。加Dropout也很有必要,BDI标签本身存在评分者主观噪声,模型很容易记住训练集里的异常值。
2.3 从音视频到ResNet输入的最小流程度
音视频原始数据不能直接进ResNet,需要先做采样和重排。常见流程是:先把原始视频按时间轴切成长度相等的片段,比如每段2秒;对音频算Mel频谱,对视频每2秒抽1帧或3帧。然后把Mel频谱转成三通道(重复三次或与RGB图像拼接),或者把视频帧直接resize到224x224。这样每个样本就是一个(B, 3, 224, 224)的张量,正好是ResNet的标准输入。
注意这里有个容易忽略的地方:ResNet的预处理(normalize)用的是ImageNet的均值和方差,但Mel频谱的数值范围通常是-80到0,分布完全不同。强烈建议用你自己数据集的全局均值方差做标准化,或者至少把每个样本的数值缩放到0-1再喂模型。我曾经在图省事直接套ImageNet标准化,结果前几轮loss下降很慢,后来发现是输入分布不一致造成的。另外,如果使用官方提取的特征,注意它的数据格式可能是float64,转成float32能省一半显存,精度损失可以忽略。
3. Python源码解析:数据加载、ResNet特征提取与诊断模型搭建
3.1 工程目录结构与运行入口
拿到这套源码后,先看目录结构。常见的整理方式如下:
depression_avec2014/ ├── data/ │ ├── raw/ # 从AVEC2014官网下载的原始视频和转录 │ ├── processed/ # 预处理后的频谱、帧特征、标签csv │ └── splits/ # 官方划分的train/dev名单 ├── models/ │ ├── backbone.py # 封装ResNet加载和回归头 │ └── classifier.py # 抑郁症诊断模型整体定义 ├── datasets/ │ └── avec_dataset.py # Dataset类和预处理流程 ├── scripts/ │ ├── train.py # 训练入口 │ ├── evaluate.py # 验证和测试入口 │ └── predict.py # 单个视频推理 └── configs/ └── default.yaml # 超参数与路径配置train.py是唯一需要直接运行的脚本,其他都是被它调用的模块。configs/default.yaml里维护数据路径和所有超参数,避免每次改参数都去翻代码。运行前,先确认data/splits里有三个csv:train_split.csv、dev_split.csv、test_split.csv,它们的列名要保持一致,至少包含participant_id和bdi。如果你手头的标签文件不是这个格式,写一个几行的转换脚本统一处理,不要直接改动训练代码去适配脏数据。
3.2 自定义Dataset把AVEC2014数据喂给ResNet
深度学习框架中数据管线写得好不好,直接影响训练稳定性。AVEC2014每个样本是一个视频,但视频长度不一,不能整个塞进batch。最常用的是在训练时随机切片段,在验证时取中间片段,这样既做了数据增强又保证每轮计算量一致。下面是AVECDataset的核心代码,我略去了视频帧抽取细节,聚焦结构:
import torch import pandas as pd from torch.utils.data import Dataset from torchvision import transforms as T class AVECDataset(Dataset): def __init__(self, root, split_file, transform=None, clip_len=32, modality='audio'): self.root = root self.modality = modality self.clip_len = clip_len df = pd.read_csv(split_file) # 每行是 participant_id, bdi self.df = df self.transform = transform or T.Compose([ T.Resize((224, 224)), T.ToTensor(), T.Normalize(mean=[0.5], std=[0.5]) # 用在自己的频谱域 ]) def __len__(self): return len(self.df) def __getitem__(self, idx): row = self.df.iloc[idx] sid, label = row['participant_id'], row['bdi'] if self.modality == 'audio': feat = self.load_audio_feat(sid) # 返回 (64, T) feat = torch.FloatTensor(feat).unsqueeze(0) # 变成 (1,64,T) feat = self.transform(feat) else: feat = self.load_video_frames(sid) feat = torch.stack([self.transform(f) for f in feat]) # (clip_len,3,224,224) return feat, torch.FloatTensor([label])这个类加载CSV中的BDI分数,modality参数控制加载音频还是视频数据。注意对音频特征做unsqueeze(0)变成单通道图像,然后复用同一个transform。Normalize(mean=[0.5], std=[0.5])是我针对0-1归一化数据常用的一组数,如果你的预处理不是0-1范围,要同步修改。如果音频特征已经是三通道图,那就去掉unsqueeze(0),并确保transform里没有Resize改变频率轴比例。
这个Dataset模块是所有实验的基础,建议在正式训练前先单独写一行for x, y in dataloader: print(x.shape, y.shape); break验证张量形状,避免把问题留到训练循环里。另一个容易踩的坑是返回的标签类型,torch.FloatTensor([label])会带上一个额外的维度,正好对应模型输出(B,1),可以少做一次squeeze操作。
3.3 ResNet模型改造与预训练权重加载
模型定义的核心是把ResNet的全连接层换成回归头,同时保留残差结构来提取特征。代码里用torchvision的resnet50,把最后的平均池化结果拿出来做回归:
import torch.nn as nn from torchvision import models class DepResNet(nn.Module): def __init__(self, backbone='resnet50', num_classes=1, dropout=0.5, pretrained=True): super().__init__() if backbone == 'resnet50': self.backbone = models.resnet50(pretrained=pretrained) elif backbone == 'resnet18': self.backbone = models.resnet18(pretrained=pretrained) else: raise ValueError(f'unsupported backbone {backbone}') # 先取出特征维度 in_features = self.backbone.fc.in_features self.backbone.fc = nn.Sequential( nn.Dropout(dropout), nn.Linear(in_features, 256), nn.ReLU(inplace=True), nn.Dropout(dropout), nn.Linear(256, num_classes) ) # 如果是视频多帧,需要先逐帧提取再聚合 self.modality = 'video' def forward(self, x): if x.dim() == 5: # (B, T, C, H, W) B, T, C, H, W = x.shape x = x.view(B*T, C, H, W) feat = self.backbone(x) feat = feat.view(B, T, -1) feat = feat.mean(dim=1) # 时序平均池化 return feat else: # (B, C, H, W) return self.backbone(x)这里的要点是,当输入是视频多帧时,先将时间维和空间维合并送进ResNet,每个帧独立得到一维分数后取平均。这种“mean pooling”虽然简单,但对短片段很有效。如果你观察训练时波动大,可以换成attention池化,但那样训练周期会变长。我一般会在做视频实验时优先用平均池化,跑通流程后再决定是否升级。回归头里的Dropout参数可以先用0.5,如果训练集很小就加大到0.7,配合早停。
在初始化模型时,如果设pretrained=True,会自动下载ImageNet权重。很多国内网络环境下载慢,常见做法是手工把权重文件放到~/.cache/torch/hub/checkpoints下,文件名要和torchvision预期一致,否则会重复下载。下载后可以用下面的代码快速验证权重是否加载成功:
import torch from models.backbone import DepResNet model = DepResNet(backbone='resnet50', pretrained=True) print(model.backbone.fc)这行代码会打印出替换后的回归头结构,同时能看到ResNet50的卷积层是否正常加载。如果输出里出现Downloading字样,说明还在走网络,等它结束再继续。
3.4 训练脚本关键部分与超参数建议
训练脚本里要处理三件事:loss函数、优化器、验证指标。AVEC2014评估标准是MAE和RMSE,所以既要用MSE作为优化目标,也要在验证时计算MAE。下面是训练循环的核心代码:
def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss = 0.0 preds, labels = [], [] for x, y in loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() out = model(x) loss = criterion(out, y) loss.backward() optimizer.step() total_loss += loss.item() * x.size(0) preds.append(out.detach().cpu()) labels.append(y.cpu()) preds = torch.cat(preds) labels = torch.cat(labels) mae = (preds - labels).abs().mean().item() return total_loss / len(loader.dataset), maecriterion是nn.MSELoss,优化器用Adam。这里损失没有做任何加权,因为BDI分数在数据集里近似正态分布。如果你发现预测值总是集中在均值附近,就要考虑是不是类别不平衡导致的,那时可以在loss里对高分段样本加权,或者换成SmoothL1Loss减少离群点影响。注意训练集返回的y形状是(B,1),模型的out也是(B,1),这样在算loss时不需要手动对齐维度。
我把常用超参数整理成一张表,方便直接照抄:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 输入图像分辨率 | 224x224 | ResNet原生尺寸,改动需要重新调整预训练位置编码 |
| 片段长度 | 2秒 audio / 32帧 video | 太短信息不足,太长显存压力大 |
| batch_size | 16(单卡) | 50层网络在这个尺寸下约需8GB显存 |
| 初始学习率 | 1e-4(Adam) | 比分类任务低一个数量级,回归更敏感 |
| 训练轮数 | 30~50 | 配合early stopping |
| 验证间隔 | 每1个epoch | 小数据集下非常值得 |
| 音频输入通道 | 1,重复成3也可以 | 用ImageNet权重时,3通道需要复制或使用Gray2RGB |
| 回归头中间维度 | 256 | 大于数据集规模容易过拟合 |
这些参数沿用自torchvision的ImageNet训练经验,但针对回归任务把学习率调低了。原因是回归任务输出是连续值,过大的学习率会让包含严重离群样本的梯度主导更新,导致训练震荡。如果你用的是resnet18,可以把batch_size提到32,但学习率保持在1e-4左右比较稳妥。
4. 本地运行与调试:从数据下载到训练收敛的完整流程
4.1 数据集下载与目录规范
AVEC2014数据集不是开箱即用的,需要到AVEC官网的下载区注册教育或研究邮箱,同意数据使用协议后获得下载链接。压缩包中的原始视频命名通常是participant_序号.mp4,转录文件是文本格式。解压后不要修改文件名,严格按照下列结构放置:
data/raw/ ├── audio/ # 从视频抽出的音频,命名与视频文件名一致 ├── video/ # 原始视频文件 ├── transcript/ # 转录文本,用于扩展 └── label/ └── train_split.csv # participant_id, bdi官网给的标签文件和官方划分要原样保留。很多人在这一步踩坑:标签是Excel格式,直接用pd.read_excel读,但里头可能有空行或字符串型的数字,要强制转成float。我会在读取时做一次pd.to_numeric(..., errors='coerce'),然后丢弃NaN行。验证划分是否正确的方式是打印训练集和验证集的subject id交集,交集应该为空:
python -c " import pandas as pd tr = pd.read_csv('data/splits/train_split.csv')['participant_id'] va = pd.read_csv('data/splits/dev_split.csv')['participant_id'] print('overlap:', len(set(tr) & set(va))) "如果输出不是0,说明你把官方划分文件搞混了,需要回到下载页重新确认。这里不要自己随意划分,保持和官方一致才能和其他论文的结果做横向对比。
4.2 环境配置快速复现
源码基于Python 3.8+,核心依赖是PyTorch 1.8以上、torchvision、librosa、pandas、PyYAML。在conda环境里一次性装好:
conda create -n avec python=3.9 -y conda activate avec pip install torch==1.12.1 torchvision==0.13.1 --extra-index-url https://download.pytorch.org/whl/cu116 pip install librosa soundfile pandas scikit-learn pyyaml注意PyTorch版本不用追新,1.12和2.x在本项目中没有明显差异。librosa版本如果过高可能会和soundfile有兼容问题,我一般固定librosa==0.9.2,这个版本对Mel谱参数支持稳定。环境装完后,建议先运行一个只读取数据的脚本,确认音频能正常解码,再进入训练步骤。如果你的机器没有CUDA,把device设为cpu也能跑通,只是训练时间会多一个量级。
4.3 用命令行参数控制实验
源码的train.py会用argparse覆盖yaml中的配置,方便做实验矩阵。常用命令如下:
python scripts/train.py \ --config configs/default.yaml \ --modality audio \ --backbone resnet50 \ --lr 1e-4 \ --epochs 40 \ --batch_size 16 \ --device cuda:0运行时,程序会先打印数据集大小和样本维度,然后进入训练循环。这里--modality audio表示跑单模态实验;如果想看视频效果,改成video即可。但如果选了video,还要确认--clip_len 32在配置里的路径指向视频帧缓存目录,否则Dataset会在__getitem__里实时抽帧,训练速度会慢上几十倍。我一般会写一个preprocess.py先把所有视频帧抽出来存成npy,训练时只做随机片段索引。
如果你想做网格搜索,最简单的办法是写一个shell循环,把每次实验的日志输出到不同文件:
for LR in 1e-4 5e-5 3e-5; do python scripts/train.py --lr $LR --modality audio >> logs/lr_$LR.log 2>&1 done每次实验结束后,程序要把验证集的MAE和RMSE追加到同一个metrics.csv里,这样后续直接读表就能看到各参数的效果,不用手动翻日志。
4.4 常见运行错误与排错表
训练过程中遇到最多的错误其实跟模型没关系,都是数据管线和环境问题。下面表格列了常见的错误以及对策:
| 错误信息 | 原因 | 解决方法 |
|---|---|---|
FileNotFoundError: .../participant_1.mp4 | 原始文件路径和label表ID对不上 | 检查文件大小写和目录层级,确保用绝对路径 |
ValueError: zero-size array to reduction operation | 音频文件为空或读取失败 | 用soundfile尝试重新解码,删除坏文件 |
RuntimeError: CUDA out of memory | 视频帧堆叠后batch太大 | 缩小batch_size,或减少clip_len |
AssertionError: batch_size < num_classes | 回归头写成分类头 | 确认num_classes=1,且不再使用CrossEntropyLoss |
KeyError: 'bdi' | CSV列名不一致 | 打印DataFrame列名,根据官网schema调整 |
| 验证集MAE不下降 | 标签标准化缺失 | 对BDI减均值除标准差,预测后再反变换 |
最后这条“MAE不下降”值得展开讲。BDI分数范围大,直接MSE会让样本间的方差很大,模型很容易偷懒输出训练集均值。一个简单有效的做法是对标签做z-score标准化,训练结束后再把预测值映射回原始分数区间,这样MAE能在期望范围内波动。在Dataset里,我会在load时读取全局label均值和标准差,存到csv描述文件中,避免每跑一次重复计算。注意z-score只用训练集的均值方差,验证集和测试集都要沿用训练集的统计量,不能重新计算。
5. 从跑通到有效:模型评估、特征融合与调参技巧
把流程跑通只是第一步,要拿到能对外汇报的数字,至少要做三件事:早停和模型选择、单模态消融、多模态融合。小数据集的实验,验证集上最好的模型和最后一个epoch的模型差距可能很大,所以不能用“训练完保存最后一个”这种粗放方式。
第一个技巧是早停加EMA。由于训练集很小,最好的模型往往不是最后一个epoch。我用下面这段代码在验证MAE连续5轮没有变好的时候回滚最优权重:
best_mae = float('inf') bad_epochs = 0 for epoch in range(epochs): train_loss, train_mae = train_one_epoch(...) val_mae = evaluate(model, val_loader, device) if val_mae < best_mae: best_mae = val_mae torch.save(model.state_dict(), 'best_model.pt') bad_epochs = 0 else: bad_epochs += 1 if bad_epochs >= 5: print('early stop at epoch', epoch) break第二个技巧是不要把音频和视频在输入层就拼到一起。ResNet的卷积核擅长处理局部相关性,而音视频是两种完全不同的分布,强行拼接通道会互相干扰。常见做法是两路ResNet分别提取特征,再在倒数第二层做concatenate,然后过一层全连接。这个方案在AVEC2014上的效果优于输入层融合。融合时注意两个分支的特征维度要对齐,比如都取平均池化后的2048维,拼接成4096维再过回归头。
第三个技巧适用于音频单模态:用time masking做数据增强。把Mel频谱的时间轴随机遮挡一段,或频率轴上随机遮挡几个波段,不容易过拟合。实现上可以借用SpecAugment的思路,但参数要调小一点,因为AVEC2014本来就短,遮挡幅度太大会把有效的语音信息抹掉。视频方面,随机水平翻转与RandomCrop都能用,但要注意翻转会让左右手坐标混乱,对情绪识别影响不大,对动作分析就要慎重。
如果你验证集的MAE稳定在7到9分之间,对AVEC2014任务来说就已经是可用水平。不要轻信论文中动辄MAE小于6的结果,实验设置中很多人做了额外预训练或用到了其他数据,你的目的是跑通一个干净、可扩展的基线,而不是复现某个SOTA。最后,把best_model.pt和最终的预测csv保存下来,和评估脚本放在同一目录,这样后续做对比实验时能快速复现。
本文还有配套的精品资源,点击获取