简介:面向生物医学工程、计算机及相关专业学生的阿尔兹海默症早期认知障碍预测项目,基于语音识别与机器学习/深度学习技术,通过分析言语特征构建认知风险评估模型,适用于毕业设计、课程设计与期末大作业等场景。压缩包共85个文件约4.89MB,包含44个wav语音样本、20张测试图片、Python源码、Jupyter Notebook、特征数据CSV、训练好的SVM模型、可视化图表及shell脚本,覆盖数据采集、预处理、特征提取、模型训练与评估全链路。已有22人学习。资源提供可运行的预测框架,按数据、模型、脚本、文档分层组织;数据切分、处理与特征提取模块配合一键运行脚本和依赖清单,便于快速复现实验,训练好的SVM模型也可直接用于后续预测。分布分析图、特征-标签关系图与散点对比图可帮助判断特征区分度和模型效果,配套说明与工程配置兼顾项目规范,适合作为医学AI方向毕业设计或综合实训的完整参考。
1. 为什么语音识别里被“清洗”掉的内容,反而是轻度认知障碍预测的关键
在记忆门诊或社区筛查现场,最常用的采集方式其实很简单:让老人描述一幅图,录音笔录上一分钟。传统语音识别把这段音频转成文字后,模型会把口语修正得通顺、流畅,而这些“纠正”恰好把最值钱的诊断信号洗掉了——停顿变长、词内拉音、重复、语速下降,这些往往比文字内容更早反映轻度认知障碍(MCI)。标题里的“基于语音识别的阿尔兹海默症早期认知障碍预测.zip”指向的就是这样一类横跨语音特征、ASR中间结果和分类建模的工程包:它把音频切分、特征提取、时序建模、评估脚本放在一个可复现的目录里。这篇就按这个方向,把整个实现路径拆开讲,适合语音算法工程师、医学信息研究人员,以及想给数字疗法产品加认知评估模块的团队。
2. 从音频到特征:轻度认知障碍预测里最值得先落地的三个特征维度
预测类任务的最前一步不是选模型,而是决定“拿什么去预测”。对认知障碍语音来说,转录文本只是其中一个维度,并且往往不是最好用的那个。下面按我实际落地的顺序,把三个维度逐一讲清楚。
2.1 先把原始录音统一成 16k 单声道,再做静音切段
语音特征对采样率和声道是敏感的。门诊录音来自不同设备,有的手机是 48k 双声道,有的录音笔是 32k 单声道。直接混着提特征,模型会先去学习设备差异,而不是认知差异。所以第一步永远是统一格式。
import librosa import soundfile as sf def to_mono_16k(src_path: str, dst_path: str) -> None: """统一到 16k 单声道,去掉重采样带来的音质差异""" y, sr = librosa.load(src_path, sr=16000, mono=True) sf.write(dst_path, y, 16000, subtype="PCM_16") # 用法示例:先把输入录音转成 wav 再进后续特征管线 to_mono_16k("case_001_raw.m4a", "case_001.wav")这里librosa.load的sr=16000参数会强制重采样到 16k,mono=True会把多声道混成单声道。之所以固定 16k,是因为后面如果接语音识别模型,绝大多数开源模型的输入采样率都是 16k;同时 16k 对基频和共振峰的保留也够用。转完格式再做静音切段,常用的做法是用能量阈值去掉首尾静音,避免后续计算语速时把静音也算成“慢”。
2.2 MFCC、韵律特征和 ASR 置信度,三者各有分工
认知障碍语音特征大致分三类,这也是压缩包里最常出现的三个特征子集。MFCC 描述声道和发音器官的运动模式,对发音模糊、构音困难敏感;韵律特征包括基频 F0、语速、停顿次数、发声时长比,对“说话变慢、停顿变多”这类时间维度上的退化敏感;ASR 中间特征则是语音识别模型在输出文本之前产生的置信度、帧级嵌入等,老人语音识别置信度普遍偏低,但“低多少”本身就是一个特征。
import librosa import numpy as np import pandas as pd def extract_acoustic_features(wav_path: str, sr: int = 16000) -> pd.DataFrame: y, _ = librosa.load(wav_path, sr=sr) mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13, n_fft=512, hop_length=160) f0, voiced_flag, _ = librosa.pyin(y, fmin=80, fmax=400) # 说话停顿:按短时能量找静音段,统计停顿次数 frame_energy = librosa.feature.rms(y=y, frame_length=512, hop_length=160)[0] silent_frames = frame_energy < (frame_energy.mean() * 0.3) pause_count = np.sum(np.diff(silent_frames.astype(int)) > 0) features = { "mfcc_mean": mfcc.mean(axis=1).tolist(), "mfcc_std": mfcc.std(axis=1).tolist(), "f0_mean": np.nanmean(f0), "f0_std": np.nanstd(f0), "pause_count": pause_count, "speech_rate": np.sum(~silent_frames) / (len(y) / sr), } return pd.DataFrame([features])n_fft=512和hop_length=160是语音情感和认知任务里常用的组合:窗长 32ms,帧移 10ms,能保留清晰的短时变化;MFCC 取 13 维而不是 40 维,是因为样本量通常不大,维度太多会让分类器很快过拟合。pyin估计基频时fmin=80, fmax=400覆盖了老年人说话的常见音高范围。算出来的pause_count和speech_rate必须和 MFCC 一起进模型,单独丢给模型的文本特征会丢掉这些时序信息。
2.3 多语种语音识别场景下,特征要跟着预训练模型走
如果面对的是多语种或方言场景,不能直接把中文训练好的特征统计量套到英文或方言数据上。常见做法是换用多语种预训练模型做帧级特征抽取,再统计成固定维度向量。预训练模型输出的是每帧一个高维嵌入,需要做时序池化,比如均值池化或按分位数池化。和手工 MFCC 比,这类嵌入能捕获更多发音细节,但它需要 GPU 和更大的显存,样本量低于两三百条时,手工特征加简单分类器通常反而更稳。我的经验是:第一版先用手工特征跑出基线,再用嵌入特征替换 MFCC 块做对比。
3. 搭建第一版分类流程:标签定义、分组切分和最小训练配置
特征有了,接下来最容易被轻视的两个问题:标签怎么定,数据怎么切。这两步出的错,往往比模型选型出错更致命,而且错误会一路污染到评估结果。
3.1 标签尽量落到二分类:正常对照对MCI,而不是直接分三分类
不少研究想直接分“正常 / 轻度认知障碍 / 阿尔兹海默症”三类,但其实临床诊断里 MCI 与早期 AD 的边界本身就模糊,医生之间的判定一致性也不高。我一般会把标签归并为二分类:认知正常对照(CN)与认知障碍组(MCID),后者可以包含 MCI 和早期 AD,只要放的语音采样与 MMSE/MoCA 分数来自同一个时间窗口。如果样本量充足,再另跑一个三分类做探索。
标签来源建议用一个 CSV 维护,至少包含:被试编号、音频路径、诊断分组、MMSE 分数、年龄段、性别、录音设备。诊断时间到录音时间间隔超过 3 个月的数据,我宁可不收— 认知状态会变化,标签和语音对不上,模型学到的是噪声。
3.2 以被试为单位做分组切分,绝不能按句子切分
这是语音认知预测里最容易踩的数据泄漏来源。一个被试可能录了 3 段音频,按句子打乱进训练集和测试集,模型会记住说话人身份而不是疾病状态。测试时同一人的另一段话被放进训练集,模型“见过这个人”,分数会虚高一大截。
用GroupKFold或StratifiedGroupKFold按被试 ID 分组是正确做法:
import pandas as pd from sklearn.model_selection import StratifiedGroupKFold df = pd.read_csv("manifest.csv") X = df[["mfcc_mean", "pause_count", "speech_rate"]] # 特征矩阵示意 y = df["label"] # 0=正常对照, 1=认知障碍 groups = df["subject_id"] sgkf = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=42) for train_idx, test_idx in sgkf.split(X, y, groups): train_df = df.iloc[train_idx] test_df = df.iloc[test_idx] # 确保同一个 subject_id 不会同时出现在两边 assert set(train_df["subject_id"]).isdisjoint(set(test_df["subject_id"]))n_splits=5在样本量极小(比如 100 人)时可以改成 5 折或留一被试交叉验证(LOSO)。StratifiedGroupKFold同时兼顾标签类别平衡和组别隔离:它先按组分组,再尽量让每一折里的正负样本比例接近总体分布。如果样本量少到连分组都困难,就按“每个被试最多出现两个会话”收集音频,会话间隔至少一个月。
3.3 先跑一个 XGBoost/LightGBM 基线,再决定要不要上深度模型
对于几百条级别的样本,树模型往往比深度模型更实用。原因是特征维度不高、样本量小,树的非线性足够拟合,同时正则参数直观可调。先把基线跑通,后续换 LSTM 才有对比基准。
from xgboost import XGBClassifier from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler model = Pipeline([ ("scaler", StandardScaler()), ("clf", XGBClassifier( n_estimators=200, max_depth=3, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, eval_metric="logloss", early_stopping_rounds=10, random_state=42, )), ])树模型不需要归一化,但这里的StandardScaler是给后面接 LSTM 做准备的,放在基线上先验证特征量纲对结果的影响。max_depth=3是防过拟合的主要旋钮,样本少于 300 时不要超过 4。subsample=0.8让每棵树只看到 80% 样本,colsample_bytree=0.8每棵树只用 80% 特征,这两个参数对这种小数据是价比最高的正则项。
训练时我会先用全部特征的均值向量做输入,得到一组指标;再去掉f0_std或pause_count做特征消融,看哪个特征对结果贡献最大。这样做的意义是:后续给临床解释模型时,能说清楚模型主要依赖停顿次数还是音高变化。
4. 把时序信息用起来:从帧级特征到 LSTM 与轻量级序列建模
上一章的基线模型把每个样本压缩成一个向量,但语音本质上是时间序列。停顿出现的位置、基频下降的轨迹、MFCC 在语句边界的变化——这些是统计均值带不走的。如果特征管线里已经保留了帧级数据,就可以进入序列建模。
4.1 帧级特征如何组织成样本序列
在手工特征阶段,每个样本是一行;在序列阶段,每个样本变成了“时间帧 × 特征维度”的二维矩阵。常见做法是:以 10ms 帧移提取 13 维 MFCC 和基频拼接,每 2 秒一段滑窗切分成片段,再对每个片段做预测并投票。
import numpy as np import librosa def frame_sequence(wav_path: str, sr: int = 16000, frame_len: int = 200, hop_len: int = 80): """返回 (样本数, 时间帧, 特征维度) 的序列特征 frame_len=200 对应 2 秒窗口,hop_len=80 对应 1 秒步长 """ y, _ = librosa.load(wav_path, sr=sr) mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13, n_fft=512, hop_length=160) # 10ms 帧移 f0, voiced, _ = librosa.pyin(y, fmin=80, fmax=400) f0 = np.nan_to_num(f0, nan=0.0) feats = np.vstack([mfcc, f0[::1][: mfcc.shape[1]]]) feats = feats.T # 形状变成 (时间帧, 特征维度) seqs = [] for start in range(0, len(feats) - frame_len + 1, hop_len): chunk = feats[start : start + frame_len] seqs.append(chunk) return np.array(seqs)这里mfcc提取的 hop_length=160 对应 10ms,所以frame_len=200恰好是 2 秒;hop_len=80取帧单位,80 帧等于 0.8 秒。重叠窗口能缓解“切段把词切碎”的问题。注意每段音频切出的序列数量不相同,后续要按段做预测再对被试投票,不能用简单拼接。
4.2 LSTM 的参数量要到“刚好能拟合”而不是“足够大”
临床语音样本量少,LSTM 的 hidden_size 不宜大。我见到最多的问题是把 NLP 任务里 512 维的 hidden 搬过来,三百条样本直接过拟合。我常用的配置是 64 维、单层双向 LSTM,配合较大的 dropout。
import torch import torch.nn as nn class MciLSTM(nn.Module): def __init__(self, input_dim: int, hidden_dim: int = 64, num_layers: int = 1, num_classes: int = 2): super().__init__() self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers=num_layers, batch_first=True, bidirectional=True) self.dropout = nn.Dropout(0.4) self.classifier = nn.Linear(hidden_dim * 2, num_classes) def forward(self, x): # x 形状: (batch, time_steps, features) out, _ = self.lstm(x) out = out[:, -1, :] # 取最后时间步的隐状态 out = self.dropout(out) return self.classifier(out)bidirectional=True会让输出维度翻倍,所以分类器输入是hidden_dim * 2。取out[:, -1, :]相当于只看序列末端状态;如果停顿特征集中在序列后段,这种取法会有效,但会丢掉前面所有位置的完整信息。替代方案是取所有时间步的均值池化。我的习惯是同时在代码里保留两套 pooling,用验证集决定用哪一个。
4.3 不要把“序列预测”任务里的 Transformer 配置搬过来
“预测”这个词在不同任务里含义差别很大:视频流量预测、光伏功率预测是自回归预测下一时刻的数值,训练数据通常几万条起步;而认知障碍语音分类是段级分类,样本量往往只有一两百人。在这种情况下,Transformer 的注意力层很容易拟合训练集的说话人特征,验证集掉点明显。如果一定要用,建议在 LSTM 基线上只做一层 TransformerEncoder,hidden 控制在 32 或 64,并加标签平滑和早停。先把 LSTM 结果作为对照组,再看 Transformer 带来的增量值不值那个调参成本。
5. 常见问题与避坑:语音认知预测项目里翻车最多的 4 个位置
从特征到评估,这个方向最容易出错的地方不在 PyTorch 训练代码,而在数据处理和验证逻辑。下面四条是按“现象 → 原因 → 解决”整理的踩坑记录。
坑 1:训练集和验证集出现了同一个人,准确率虚高到 0.9 以上。
现象:五折交叉验证平均准确率超过 0.92,比自己预期高太多,换到外部小样本验证时立刻掉到 0.6。
原因:同一被试的多段音频被随机切分进了训练集和测试集。模型学到的不是 MCI 的语音模式,而是特定说话人的声纹和习惯。GroupKFold 解决的就是这个问题,但很多人会忽略这段逻辑直接train_test_split。
解决:所有与训练相关的切分都按subject_id分组,用StratifiedGroupKFold。评估时报告“被试级别”的 AUC,不要报告“音频段级别”的准确率,后者永远偏乐观。
坑 2:同一批音频用不同设备录,模型先学会了设备特征。
现象:换录音笔后,模型在新数据上的表现明显下降;查看特征分布,不同设备的音频在 MFCC 第一维上聚合成分团。
原因:手机录音和录音笔的频率响应不同,空调底噪、增益差异全部被 MFCC 捕捉。模型学到的是谁的声音更“干净”。
解决:做简单的音频增强,或至少做短时能量归一化;在训练脚本里把“设备 ID”作为交叉验证的分组变量之一,验证模型换设备后的鲁棒性。如果预算允许,每个被试尽量用同型号设备录制。
坑 3:停顿特征里混进了“录音开始前”的静音。
现象:pause_count 这个特征重要性排名异常高,但查看错误样本发现,有些停顿集中在音频首尾的静音段。
原因:VAD 只做了简单阈值过滤,或者完全没做。长段静音被当成一次长停顿,模型利用了这个假信号。
解决:在做语音活动检测时,把短时能量低于阈值的连续帧长度达到 300ms 以上才算一次停顿;同时砍掉音频首尾各 200ms 的保护段,再统计 pause_count。
坑 4:类别不平衡下只看准确率,模型全预测“正常”也能有 0.7。
现象:测试集里正负样本比是 2:8,模型输出全为 0,准确率 0.8,看起来不错,但召回率是 0。
原因:分类器在类别不平衡时会被多数类带偏,而准确率指标掩盖了这个问题。
解决:后续评估以 AUC、敏感性、特异性为主,分类阈值不要用默认 0.5,而是用验证集上 Youden 指数选出来的阈值。
坑 5:MMSE 分数和录音时间跨度超过半年,标签“不新鲜”。
现象:标签是去年门诊的 MMSE 分数,语音是今年采集的,模型怎么调都稳定不过 0.6。
原因:半年内轻度认知障碍患者的认知状态可能已经变化,语音特征对应的“真实标签”和 CSV 里的标签不一致。
解决:标签与录音采集日期必须来自同一时间窗口,超过 3 个月的直接丢弃。项目数据采集文档里要写明这一条硬约束。
6. 验证模型的一个更实用的技巧:被试内交叉验证加校准曲线,别只看准确率
最后一章分享一个我每次交付认知语音评估项目都会固定加上的验证模块:校准分析。它对临床场景的意义是,医生拿到模型的输出概率后,要给一个可以直接理解的数值,比如“这个人的异常概率是 0.73”,而不是“类别 1”。如果模型输出的概率普遍偏大或偏小,阈值怎么调都不好用。
做法是:在 GroupKFold 的每一折里,除了记录预测类别,还要保存predict_proba输出的正类概率,然后用sklearn.calibration.calibration_curve画校准曲线,同时计算 Brier 分数。
from sklearn.calibration import calibration_curve import numpy as np # y_true 为折内真实标签,y_prob 为模型输出的正类概率 prob_true, prob_pred = calibration_curve(y_true, y_prob, n_bins=5, strategy="quantile") brier = np.mean((y_prob - y_true) ** 2) for i in range(len(prob_true)): print(f"预测概率区间 {prob_pred[i]:.2f} -> 实际阳性率 {prob_true[i]:.2f}")n_bins=5是因为临床语音样本量通常不大,分太多箱各箱样本太少,曲线会抖动。strategy="quantile"按分位数划分区间,保证每个箱里有足够样本。Brier 分数越小越好,它同时惩罚“过度自信的错”和“不够自信的对”,比准确率更能反映概率输出的质量。
如果校准曲线呈明显的 S 形,说明模型输出偏保守,可以加一次 Platt 缩放;如果曲线靠近对角线但 Brier 分数仍然高,说明个别样本贡献了巨大误差,去检查这些样本的音频质量,通常能找到环境噪声或截断错误。
我现在的固定习惯是:任何语音认知评估项目交付时,除了模型文件和训练脚本,必须附上一份含校准曲线的评估报告和一份错误样例清单。错误样例按“被错分的音频 + 人工标注的语音特征描述”整理,这样临床的人才能真正判断模型是“漏了”还是“误报”,也方便下一轮迭代定位问题。这条路不能只看最终分数,把错误样本摊开看,往往比调参更能救命。希望帮到你。
本文还有配套的精品资源,点击获取