简介:PDF文献《基于深度学习的音频抑郁症识别》聚焦抑郁症的智能化识别,面向深度学习、语音信号处理及医学交叉领域的研究者与从业者,提出基于卷积神经网络和长短期记忆网络的音频抑郁回归模型 DR AudioNet。文献同时给出多尺度音频差分归一化(MADN)特征提取算法,用于刻画非个性化讲话特性,并结合前后相邻音频段的 MADN 特征设计了两种优化网络结构,实验表明该方法能有效识别抑郁程度。资源共 1 个 PDF 文件,压缩包约 856KB,内容涵盖模型架构、特征设计、实验细节与分析,适合作为论文写作参考和算法复现依据。目前已有 852 人学习,对关注语音情感计算、抑郁自动诊断及音频深度特征提取的读者尤其有参考价值。
1. 音频抑郁症识别:先从"换个说法"讲起
做语音情感识别的人大多遇到过这种尴尬:同一句话用平静语气说和用沮丧语气说,MFCC 特征分布的重叠区域远大于不同说话人之间的差异。换句话说,模型很容易记住"这个人说话的声音",而不是"这个人说话的状态"。抑郁症语音识别(Automatic Speech Depression Detection)也卡在这个问题上——2016 年 AVEC 竞赛里基于 MFCC 和 CNN 的方案不少,但换一个说话人,准确率立刻掉下来。这篇论文提出的核心思路很直接:既然静态特征里混着说话人的音色、音调等个性化信息,那就用相邻帧之间的差分代替原始特征,再对不同特征用不同尺度的滑动窗口做归一化,把"这个人是谁"的信息尽量剥掉,剩下的才是与抑郁程度相关的时序变化。这个思路用一个叫 MADN(多尺度音频差分归一化)的算法实现,再配合 CNN 加 LSTM 的 DR AudioNet 网络做 BDI-II 分值回归。下面从特征到网络逐步拆开。
2. 语音特征从哪儿来:MFCC、短时能量与共振峰的选取逻辑
2.1 AVEC 2014 数据集的结构和预处理边界
论文实验用的是 AVEC 2014 的音频子集。数据分成 NORTHWIND 和 FREEFORM 两部分:前者是照着固定文章朗读,后者是回答工作人员的问题,比如"你最喜欢的菜是什么"。第一部分的好处是文本内容一致,特征差异主要来自发音方式;第二部分更接近自然交谈。两部分各 150 对音频,按 50/50/50 划分训练、验证、测试集。每个样本的标签是 BDI-II 量表得分,范围 0~63,不同分数段对应无抑郁、轻度、中度、重度。
预处理有几个值得注意的参数。原始采样率 44.1kHz,每帧 1024 个采样点,帧移 512(也就是 50% 重叠),每 60 帧组成一个音频片段。这样单个片段覆盖的时间长度是 (60+1)×512/44100 ≈ 0.708 秒,一个 4 分钟的音频大约能切出 300 多个片段。这里的关键决策是去除长时间静音段后再切分,否则沉默部分会贡献大量零能量帧,干扰后续差分计算。整批数据预处理后得到 7548 个片段,其中训练集 5100、测试集 2448。
2.2 MFCC 参数选择:从 Mel 刻度到倒谱系数
MFCC 的计算过程在论文里有完整的公式。先把普通频率转换为 Mel 刻度:
import librosa import numpy as np y, sr = librosa.load("depression_audio.wav", sr=44100) # 去除首尾静音,保留有效语音段 y_trimmed, _ = librosa.effects.trim(y, top_db=20) # 1024 点 FFT,hop_length 512,即 50% 重叠 mfcc = librosa.feature.mfcc( y=y_trimmed, sr=sr, n_mfcc=12, n_fft=1024, hop_length=512, n_mels=40, fmin=0, fmax=None, ) # mfcc 形状:(12, 帧数),帧数由音频长度决定Mel 频率和普通频率的换算关系是 f_mel = 2595 * log10(1 + f_Hz / 700)。这个非线性映射模拟了人耳对低频敏感、高频迟钝的特性。参数上论文取了 12 个 MFCC 系数和 40 个 Mel 滤波器组,这是语音识别里比较保守的配置。常见做法是取 13 个系数(含第 0 阶能量),但论文实验显示 12 阶在抑郁回归任务上结果更稳。
2.3 共振峰、短时能量和过零率为什么有必要
MFCC 描述的是频谱包络,而抑郁症患者的声道肌肉紧张程度会导致共振峰位置偏移。共振峰反映的是声道(共鸣腔)的物理特征,发音器官紧张时,共振峰频率会变化,因此把它加进来能捕捉 MFCC 不容易体现的发音生理差异。短时能量用来区分有声段和无声段,抑郁症患者往往有停顿变长、吐字不清的现象,这些会直接体现在能量分布的时序变化上。过零率则可以辅助判断清音和浊音的比例。
提取这几个特征可以用 librosa 配合 scipy 实现:
import librosa from scipy.signal import lfilter # 短时能量:每帧信号的平方和 frame_len = 1024 hop = 512 energy = librosa.feature.rms( y=y_trimmed, frame_length=frame_len, hop_length=hop )[0] # 形状:(帧数,) # 过零率:每帧内信号符号变化的次数 zcr = librosa.feature.zero_crossing_rate( y_trimmed, frame_length=frame_len, hop_length=hop )[0] # 共振峰:用 LPC 线性预测估计声道传输函数,再找峰值 # 这里用 12 阶 LPC,采样率 44.1kHz 下能覆盖 4 个左右的共振峰 def lpc_resonance(frame, order=12): # 预加重 pre_emph = lfilter([1, -0.97], [1], frame) autocorr = np.correlate(pre_emph, pre_emph, "full") autocorr = autocorr[len(pre_emph) - 1 :] # Levinson-Durbin 递推求解 LPC 系数 from scipy.linalg import solve_toeplitz r = autocorr[: order + 1] a = solve_toeplitz(r[:-1], -r[1:]) # 由 LPC 系数构造频谱并找峰值 w, h = np.polynomial.chebyshev.chebfit( # 实际简化操作:用 scipy.signal.freqz 求频响 ) return None共振峰部分的完整实现比较繁琐,工程上更省事的做法是用 parselmouth(Praat 的 Python 接口)的to_formant_burg方法。LPC 阶数设置需要根据采样率调整:44.1kHz 下 12 阶能分辨 3~4 个共振峰,16kHz 采样时 10 阶就够了。特征全部提取完后,拼接成一个 17×60 的矩阵——17 是 12 维 MFCC 加能量、过零率、3 个共振峰的合计维度,60 是帧数。
3. MADN 差分归一化:把说话人个性从特征里剥掉
3.1 差分特征为什么能削弱个性化信息
假设两个人说同一句话,一个人天生嗓门大,另一个人声音低沉。他们的静态 MFCC 第一维和第二维的绝对数值差异明显,但相邻两帧之间的变化趋势往往相似——因为抑扬顿挫、语速变化这些韵律特征受发音器官物理结构的影响较小。MADN 的第一步就是做相邻帧差分:D(n, f) = V(n, f+1) - V(n, f)。这一步直接去掉了特征的直流分量,也就是说话人相对固定的音色基线。
但差分之后的数值范围仍受说话人音量影响。一个人说话大声,他的差分值整体也会偏大。所以在差分之后必须做归一化,把每个特征维度的动态范围压到 [0, 1]。
3.2 滑动窗口归一化的实现
归一化不是对整段音频算全局 min-max,那样会把整段音频的动态范围拉平,丢失局部突变信息。MADN 的核心是按滑动窗口计算局部最小值和最大值:
import numpy as np def madn_normalize(D, window_sizes): """ D: 差分特征矩阵,形状 (n_features, n_frames) window_sizes: 每个特征维度对应的窗口半径列表,长度等于 n_features 返回归一化后的特征矩阵 F """ n_features, n_frames = D.shape F = np.zeros_like(D, dtype=np.float32) for n in range(n_features): radius = window_sizes[n] for f in range(n_frames): # 窗口边界裁剪 start = max(0, f - radius) end = min(n_frames, f + radius + 1) # 取出窗口内的局部差分值 local = D[n, start:end] d_min = np.min(local) d_max = np.max(local) if d_max - d_min > 1e-8: F[n, f] = (D[n, f] - d_min) / (d_max - d_min) else: # 差分恒定则设为零,避免除零 F[n, f] = 0.0 return F # 每个特征维度的窗口半径(帧数) # 前12维 MFCC 用半径 5 # 第13、14维(能量、过零率)用半径 10,因为数值变化更平缓 # 第15~17维(共振峰)用半径 15,共振峰漂移是慢变过程 window_sizes = [5] * 12 + [10] * 2 + [15] * 3 madn_feat = madn_normalize(diff_features, window_sizes)窗口半径的设计逻辑要从特征的物理意义理解。MFCC 反映的是频谱包络的短时变化,说话时元音和辅音交替较快,局部窗口取小一些能保留韵母过渡的细节;短时能量受响度波动影响,变化相对平缓,窗口取 10;共振峰反映声道形状,转动速度有限,窗口取 15 帧(约 0.35 秒)比较合理。
窗口边界处理有个细节要特别注意。max(0, f - radius)在音频段开头几帧会把窗口截短,这会导致前几帧的归一化结果偏大(因为局部范围小、分母小)。常见的做法是给每段音频前后各补 radius 帧的填充值再算差分,工程上通常用"镜像填充"或者直接丢弃边缘的 5~10 帧特征。实测丢弃边缘帧对 AVEC 2014 数据的 MAE 指标影响不大,因为每个样本被切成大量片段,边缘损失占比很小。
3.3 用 numpy 向量化替代逐帧循环
上面的实现每帧做一次切片和 min/max 操作,numpy 循环在特征维度大、帧数过万时效率偏低。实际训练可以改成基于膨胀卷积的滑动窗口量化解法:
from numpy.lib.stride_tricks import sliding_window_view def madn_normalize_fast(D, window_sizes): n_features, n_frames = D.shape F = np.zeros_like(D, dtype=np.float32) for n in range(n_features): radius = window_sizes[n] win = 2 * radius + 1 # 镜像填充,保持开头和结尾的窗口完整 padded = np.pad(D[n], radius, mode="reflect") # 构造滑窗视图,形状:(n_frames, win) windows = sliding_window_view(padded, win) local_min = windows.min(axis=1) local_max = windows.max(axis=1) denom = local_max - local_min denom[denom < 1e-8] = 1.0 # 防止除零 F[n] = (D[n] - local_min) / denom return Fsliding_window_view不复制数据,只在内存上做视图映射,比循环快一个数量级。mode="reflect"是镜像填充,保证每个位置都有完整的 2r+1 窗口,也避免边缘特殊处理。
操作完成后得到的 MADN 特征 F 在时间轴上是非个性化的"变化量"表示,和原始静态特征 V1 互为补充。论文中的模型二使用前一段音频的 MADN 特征 V2,模型三使用后一段的 MADN 特征 V3,本质上就是让网络同时看"当前说了什么"和"说话状态怎么变化"两组信息。
4. 构建 DR AudioNet:3x1 卷积为什么比方形卷积更合理
4.1 把二分类改成回归的原因
抑郁量表 BDI-II 的得分是连续分值。分类模型只能回答"有没有抑郁",而临床治疗需要知道抑郁的严重程度——轻度、中度、重度的干预方案完全不同。论文将网络输出从二分类的 softmax 改为单节点回归,用 MAE 和 RMSE 作为评估指标,预测目标是 0~63 的实数值。直觉上回归任务的优化更平滑,模型不会为了硬性分类边界牺牲数值上的精度。
4.2 输入矩阵的几何含义
输入是一个 17×60 的矩阵,行维度是 17 种特征,列维度是 60 帧时间。这样排列后,水平轴是时间、垂直轴是特征类型。类似的频谱图处理问题中,方形卷积核(比如 3×3)会同时混叠时间和特征维度——不同特征之间的相关性远弱于同一特征在相邻帧之间的相关性,卷积核窗口会把这些物理含义完全不同的维度做了无差别混合,削弱特征的判别力。DR AudioNet 采用 3×1 卷积核,即每次只沿时间轴滑动 3 帧,垂直方向不做卷积。这样做有两个好处:每个特征维度保持独立的时间卷积,不会引入跨维度的虚假耦合;参数数量也减少到方形卷积的三分之一。
4.3 完整的 PyTorch 实现
import torch import torch.nn as nn class DRAudioNet(nn.Module): def __init__(self, n_features=17, n_frames=60): super().__init__() # 两层 3x1 卷积,沿时间轴提取局部变化模式 self.conv1 = nn.Conv2d( in_channels=1, out_channels=64, kernel_size=(3, 1), padding=(1, 0), ) self.bn1 = nn.BatchNorm2d(64) self.conv2 = nn.Conv2d( in_channels=64, out_channels=64, kernel_size=(3, 1), padding=(1, 0), ) self.bn2 = nn.BatchNorm2d(64) self.pool = nn.MaxPool2d(kernel_size=(2, 1)) self.relu = nn.ReLU() # LSTM 层,编码时序依赖 self.lstm = nn.LSTM( input_size=64, hidden_size=128, num_layers=1, batch_first=True, ) # 回归头,输出 BDI-II 预测分值 self.fc1 = nn.Linear(128, 128) self.fc2 = nn.Linear(128, 1) def forward(self, x): # x 形状: (batch, 17, 60),先补通道维 x = x.unsqueeze(1) # (batch, 1, 17, 60) x = self.relu(self.bn1(self.conv1(x))) x = self.relu(self.bn2(self.conv2(x))) x = self.pool(x) # 池化后时间维从 60 缩减到 30 # 转成 LSTM 输入: (batch, seq_len, features) x = x.squeeze(2) # (batch, 30, 64) x = x.permute(0, 2, 1) # (batch, 30, 64) out, _ = self.lstm(x) # out: (batch, 30, 128) # 取最后一个时间步的输出 out = out[:, -1, :] # (batch, 128) out = self.relu(self.fc1(out)) pred = self.fc2(out) # (batch, 1) return pred.squeeze(1)卷积层的作用是提取局部时序变化模式,也就是"邻近几帧之间怎么变";池化层把时间维从 60 降到 30,减少 LSTM 的计算量,同时对帧位置的小偏移引入一定的鲁棒性,说话人语速快慢不同时,抑郁相关的特征在时间轴上可能出现几个帧的错位,池化恰好容忍这种位移。LSTM 层的作用则是捕捉长距离依赖——一个人在几分钟的对话里音频状态的缓慢漂移,这种趋势 CNN 感受野覆盖不了。
训练参数按论文实验设置:batch size 32,损失函数用均方误差(MSE),优化器常见做法用 Adam,学习率从 1e-4 起,验证集 loss 连续 5 个 epoch 不下降就减半。层数上,两层卷积加一层 LSTM 是这个任务的经验配置,再加深容易在小数据集上过拟合——AVEC 2014 音频段虽然有 5100 条,但都来自 50 个说话人,独立样本数其实不多。
5. 相邻片段串行微调与 MAE/RMSE 验证技巧
5.1 模型串联而不是特征拼接
论文没有简单地把三组特征拼在一起喂给网络,而是采用串行微调的训练方式:先在静态特征 V1 上训练 DR AudioNet,得到模型一;再用前一段音频的 MADN 特征 V2 在模型一的基础上继续训练,得到模型二;最后用后一段的 MADN 特征 V3 在模型二的基础上微调得到模型三。每个后续模型都在前一个模型已经收敛的基础上继续用新的特征调整权重,相当于让网络先理解音频的静态内容,再学习局部变化特征,最后扩展认知到更长期的上下文变化。
# 三个阶段训练伪代码 classifier = DRAudioNet() # Stage 1: 在静态特征上从头训练 static_feat, bdi_label = load_segment_features(feature_type="static") train(classifier, static_feat, bdi_label, epochs=30) # Stage 2: 用前一段 MADN 特征微调 prev_madn = load_segment_features(feature_type="madn_prev") train(classifier, prev_madn, bdi_label, epochs=10, lr=1e-5) # Stage 3: 用后一段 MADN 特征继续微调 next_madn = load_segment_features(feature_type="madn_next") train(classifier, next_madn, bdi_label, epochs=10, lr=5e-6)微调阶段学习率要比第一阶段小一个数量级,因为预训练权重已经编码了有效的静态音频特征,过大的学习率会破坏这些信息。Stage 2 和 Stage 3 的音频片段在时间上与当前片段相邻,共享同一个 BDI-II 标签。这种设计不是简单的数据增强,而是让模型在推理时能把相邻片段的信息也纳入考量,缓解单个片段时长不足导致的信息缺失问题。
5.2 MAE 与 RMSE 在抑郁分值上的解读
评估指标用 MAE 和 RMSE。MAE 是所有预测误差绝对值的平均,计算方式为 MAE = 1/N * Σ|yᵢ - ŷᵢ|,对异常值不敏感;RMSE 是先平方再平均开根,对偏离较大的预测施以更重的惩罚。论文在测试集上得到的 RMSE 和 MAE 约 9.70。BDI-II 总分 63,预测误差在 10 分以内意味着模型能大致区分抑郁程度等级(无抑郁、轻度、中度、重度之间的分值边界是 13/19/28),但还不足以精确到个体差异的临床级别。这个数值对应的实际表现是:模型能判断一个语音样本偏向哪个严重等级区间,但同一受试者两周内的复测分数波动也可能达到这个量级。
5.3 验证差分特征有效性的快速方法
不训练完整网络,也能快速验证 MADN 特征是否真的剥离了个性化信息。做法是:从数据集中选同一说话人两段不同抑郁分值的录音,分别计算静态特征和 MADN 特征的均值向量,再比较这两个向量在说话人内部的差别和说话人之间的差别。
import numpy as np def feature_separation_score(features, speaker_ids, labels): """ 计算组间方差与组内方差的比值 值越大说明特征区分说话人状态的能力越强 """ speakers = np.unique(speaker_ids) between_var = 0.0 within_var = 0.0 global_mean = features.mean(axis=0) for spk in speakers: spk_feat = features[speaker_ids == spk] spk_mean = spk_feat.mean(axis=0) # 组间方差:说话人均值相对全局均值的偏移 between_var += ( len(spk_feat) * np.sum((spk_mean - global_mean) ** 2) ) # 组内方差:特征本身相对说话人均值的离散度 within_var += np.sum((spk_feat - spk_mean) ** 2) return between_var / (within_var + 1e-8) # static_feats 和 madn_feats 分别取 100 段语音 score_static = feature_separation_score(static_feats, speakers, labels) score_madn = feature_separation_score(madn_feats, speakers, labels) print(f"Static separation: {score_static:.3f}") print(f"MADN separation: {score_madn:.3f}")如果 MADN 有效,speaker 维度上的分离度分数会显著下降,说明说话人之间的特征差异缩小了;而标签维度(高低分组)上的分离度分数应该保持或上升。实际论文中 MADN 对 MAE 的改善幅度大约在 5%~10%,这个验证方法在动手训练前能给到明确的方向信号。检查时注意差分后的特征维度会少一帧,因为最后一个点没有后续帧可以做差分,代码里要对齐标签长度。
本文还有配套的精品资源,点击获取