资讯动态

基于语音识别的阿尔兹海默症早期认知障碍预测:30秒语音分析

发布时间:2026/10/27 19:55:37 来源:尧图企业网站定制
简介基于语音识别的阿尔兹海默症早期认知障碍预测项目包面向机器学习、深度学习方向的毕业设计、课程设计及期末大作业场景覆盖从语音数据采集、特征提取到认知障碍风险预测的完整流程并提供SVM与深度学习两种建模思路。压缩包共85个文件、约4.89MB包含44个wav语音样本、20张物品图片素材、8个Python脚本特征提取、训练、验证、录音等、1个训练好的SVM模型文件以及特征数据表、可视化分析图和可运行笔记目录划分清晰便于复现与改造。资源还附带特征分布、特征与标签关系、模型效果对比等分析图及说明文档有助于理解模型性能与数据特征。已有22人学习适合完成相关课题或快速搭建语音认知障碍预测实验环境的学生参考可直接调参、扩展数据集或替换模型继续优化。1. 基于语音识别的阿尔兹海默症早期认知障碍预测从一段三十秒录音里找认知衰退信号最近接手一个社区认知筛查项目对方拿来几十条老人朗读录音问我能不能从中看出谁有早期认知障碍倾向。基于语音识别的阿尔兹海默症早期认知障碍预测.zip 里那套方案做的正是这件事用 ASR 把音频转写为文本同时从声学层与语义层抽特征交给序列模型输出风险概率。它解决的是个很具体的痛点——常规量表筛查耗时、老人配合度参差而一段 30 秒语音几乎无创、采集成本可忽略。适合手里有一批老年语音样本、却不知道从哪下手的医疗 AI 工程师也适合给基层卫生团队搭低成本预筛工具的开发者。2. 语音里藏着哪些认知信号AD 预测的特征原理与选型2.1 AD 言语退化的四个可测量维度从临床观察到特征选型阿尔兹海默症早期最先被侵蚀的常常不是“回忆往事”的能力而是“找词”和“组织语言”的执行功能。神经语言学里有个经典观察AD 患者的自发言语中语义空洞、句法简化、停顿异常这三类问题出现得远比记忆力主诉要早。落到可计算层面我一般把它拆成四个维度语义层找词困难具体名词被宽泛词替代“东西”“那个”出现频率升高语义重复一句话绕两遍才说清。句法层平均句长下降从句和连接词减少整体句法复杂度变低。韵律层语速变慢句内停顿增加尤其是“连填充词都填不上的尴尬停顿”。音素层早期并不显著但晚期会出现构音含糊、音节时长不稳定。做这个方向第一个要建立的认知是不要只盯着“他说了什么”更要盯着“他怎么说”。停顿的位置、填充词的密度、句法复杂度这些“副产物信息”往往比字面内容更稳定。更实际的一点这四个维度都不需要人工标注——语音识别直接给你转写文本声学处理直接给你停顿和语调参数整个特征管线可以全自动。特征选型时我的经验是别一上来就把能算的都算上。先做一组最小特征语速字数/总时长、填充词密度“嗯”“啊”“那个”等占总词数比例、平均句长、停顿次数、停顿时长占总时长比例再加上一段 MFCC 序列。这组特征只有十来个维度却已经能覆盖临床观察里最核心的语言学信号。import json, re with open(data/transcripts/session_001.json, r, encodingutf-8) as f: t json.load(f) text t[full_text] words re.findall(r[\u4e00-\u9fa5], text) # 只取中文词 filler [嗯, 啊, 那个, 就是, 然后, 这个] sentence_len [] for sent in re.split(r[。], text): tokens re.findall(r[\u4e00-\u9fa5], sent) if tokens: sentence_len.append(len(tokens)) features { total_words: len(words), duration_s: t[duration_s], speech_rate: len(words) / t[duration_s], # 字/秒 filler_density: sum(1 for w in words if w in filler) / max(len(words), 1), mean_sentence_len: sum(sentence_len) / max(len(sentence_len), 1), pause_count: t[pause_count], pause_ratio: t[pause_total_s] / max(t[duration_s], 1), }这段代码做的事很直白把 ASR 转写结果读进来用正则切出中文词统计语速、填充词密度、平均句长再结合时间戳里的停顿信息生成一条可训练的特征记录。参数说明filler列表里的词按数据集实际口吻调整北方老人常用“那个”“就是”南方老人会有“然后”“阿”需要先对一小批转写文本肉眼扫一遍再定total_words用中文字数而非英文 token 数是为了让语速单位统一成“字/秒”不同转录工具分词方式有差异标准化后好对比。2.2 为什么必须走语音识别链路声学特征与语言特征的融合逻辑有人会问我不用 ASR直接从音频算 MFCC 喂给 LSTM不也能预测能但容易翻车。纯声学模型在跨数据集时经常学到录音环境、麦克风、音量和年龄相关的泛衰老发音而不是认知障碍本身。医疗场景里尤其危险健康对照组录音条件往往比患者组好声学特征很容易拾取这种“组间偏差”模型拿着一张 AUC 0.9 的成绩单一到新数据就现原形。引入语音识别后模型多了一条语义通道。近年多语种语音识别的工程化成熟度已经足够应付中文、粤语、方言混杂的老年样本不再需要自己训练声学模型。常见做法是双支路融合声学支路负责“怎么说”转录文本支路负责“说什么”最后在分类头汇合。对比下来特征支路来源捕捉的信号翻车点声学特征MFCC、F0、停顿、语速语音流畅性、韵律退化录音环境、年龄与性别差异语言特征ASR 转写文本找词困难、句法简化、填充词老人口音、ASR 转写错误两条支路缺一不可但权重上我更偏向语言特征。原因简单认知障碍本质是“脑子组织语言”出了问题单靠发声器官的声学指标很难和普通衰老区分而填充词密度、句长这类语言特征与临床量表的相关性有大量文献支撑。这也意味着 ASR 的转写质量直接影响预测上限所以工程里不能随便用一个在线免费接口要选能本地部署、对中文口语鲁棒的模型。from faster_whisper import WhisperModel model WhisperModel(medium, devicecuda, compute_typefloat16) segments, info model.transcribe( audio/session_001.wav, languagezh, beam_size1, vad_filterFalse, condition_on_previous_textFalse, )这段是转写核心调用。参数说明vad_filterFalse是刻意关掉的因为 VAD 会切掉长停顿而停顿恰恰是我们的核心特征同理condition_on_previous_textFalse是为了防止长音频里模型“复读”上一段文字——老人说话零散时 Whisper 很容易自己接一段没有的内容关掉后更稳但要接受个别句子识别变差。2.3 zip 解压后的工程骨架一个 AD 语音预筛项目应该长什么样这类带着.zip的工程包我拿到手第一步不是看train.py而是看目录和配置。一个能直接跑的 AD 语音预筛项目目录预期是这样ad_speech_predict/ ├── config.yaml ├── data/ │ ├── raw_audio/ # 原始 wav按 subject_id 组织 │ ├── transcripts/ # ASR 输出 json含段级时间戳 │ └── features/ # 特征缓存 npz/csv ├── src/ │ ├── audio_preprocess.py # 重采样、降噪、静音段检测 │ ├── transcribe.py # ASR 转写入口 │ ├── extract_features.py # 特征抽取 │ ├── dataset.py # Dataset 与增强 │ ├── train.py # 训练入口 │ └── predict.py # 单条音频推理 ├── models/ # checkpoint └── evaluation/ # 按说话人分组的评估脚本config.yaml里的关键参数决定这个包能不能迁移到你的数据上audio: sample_rate: 16000 segment_s: 30 # 单条样本长度超过则切窗 use_vad: false # 预处理阶段关VAD否则丢掉停顿特征 asr: model_size: medium # 中文老年语音至少 medium device: cuda compute_type: float16 features: n_mfcc: 40 hop_length: 512 filler_ratio: true model: hidden_size: 128 num_layers: 2 dropout: 0.4 bidirectional: true参数说明sample_rate16000是 ASR 和声学特征的标准采样率低于 16k 会丢高频信息segment_s30是经验值太短语义不完整太长模型难收敛hop_length512配合 40 维 MFCC 能在 30 秒音频里产出约 900 帧序列这个长度 LSTM 能扛住Transformer 会稍吃力。拿到任何同类 zip先看这几项再动代码参数经常比模型结构更致命。3. 最小可复现管线从 wav 到认知障碍预测分数3.1 音频预处理重采样、响度对齐与静音段检测第一步永远是格式对齐。门诊录音设备五花八门手机、录音笔、远程问诊系统各来一套声道数、采样率、响度全不一样。我用librosa统一处理顺便把静音段时间戳独立算出来——注意这是独立于 ASR 的前面说过VAD 切掉的停顿时间戳在 ASR 结果里并没有得在原始音频上自己量。import librosa import numpy as np def preprocess_audio(path, sample_rate16000): y, sr librosa.load(path, srsample_rate, monoTrue) y librosa.util.normalize(y) # 计算静音段用短时能量包络找停顿 frame_len int(0.025 * sr) # 25ms 帧 hop int(0.010 * sr) # 10ms 步进 energy librosa.feature.rms(yy, frame_lengthframe_len, hop_lengthhop)[0] # 阈值取整体 RMS 的 10%低于则算停顿 thr energy.max() * 0.10 silence energy thr # 把帧序列转成时间段 pause_segments [] start None for i, is_sil in enumerate(silence): t0 i * hop / sample_rate if is_sil and start is None: start t0 elif not is_sil and start is not None: pause_segments.append((start, t0)) start None if start is not None: pause_segments.append((start, len(y) / sample_rate)) return y, pause_segments, len(y) / sample_rate逻辑说明先统一为 16k 单声道并做响度归一化然后用librosa.feature.rms算短时能量能量低于最大值的 10% 就判为停顿。这样算出来的停顿是“纯声学停顿”不需要 ASR 参与也不受转写错误影响。参数说明25ms 窗长是语音活动检测的常用选择既能捕捉到 200ms 以上的语义停顿又不会把音节内部的气音误判成停顿阈值 10% 偏保守如果录音里叹气、咳嗽多可以上调到 15%但要注意别把轻声尾音也切掉。3.2 本地 ASR 转写导出带时间戳的文本并清洗上一章给过 Whisper 的调用片段这里说完整流程。先转写再把结果清洗出两个关键东西全文本和段级时间戳。清洗时要特别注意两个现象一是老人口齿不清导致 Whisper 输出空串或乱码二是“嗯”“啊”这类填充词会被当成无意义内容直接吞掉。from faster_whisper import WhisperModel import json, re model WhisperModel(medium, devicecuda, compute_typefloat16) def transcribe_and_save(wav_path, out_path): segments, info model.transcribe( wav_path, languagezh, beam_size1, vad_filterFalse, condition_on_previous_textFalse, ) seg_list [] full_text [] for s in segments: text s.text.strip() # 清洗去掉标点、全角空格、重复语气词 text re.sub(r[^\u4e00-\u9fa5嗯啊], , text) if len(text) 2: # 避免单个语气词刷屏 seg_list.append({ start: round(s.start, 2), end: round(s.end, 2), text: text, }) full_text.append(text) result { full_text: .join(full_text), segments: seg_list, duration_s: round(info.duration, 2), pause_count: len(seg_list), } with open(out_path, w, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2) return result逻辑说明遍历segments时把每段的起止时间和文本存下来过滤掉太短的片段最后汇总成 json 落盘。参数说明len(text) 2这行是防止“嗯”“啊”等单字填充词被单独记成段落但它们仍然出现在full_text里后面算填充词密度时不会丢info.duration是 ASR 自己算的总时长比len(audio)/sr靠谱因为它去掉了前后静音。3.3 特征提取MFCC 序列加语言统计特征落盘成训练集预处理和转写都完成之后进入特征工程。这里要生成两个东西MFCC 序列定长时间窗和语言统计特征上一章那段代码的扩展版。MFCC 序列用于 LSTM 的时序建模语言特征用于 MLP 支路。两条支路最后在分类器里融合。import librosa import numpy as np import json, re def extract_all(audio_path, transcript_path, segment_s30, sr16000): y, _ librosa.load(audio_path, srsr, monoTrue) y librosa.util.normalize(y) # 定长切段长音频切成30秒窗不足则末尾补零 total_frames segment_s * sr if len(y) total_frames: y np.pad(y, (0, total_frames - len(y))) n_seg len(y) // total_frames features_all [] with open(transcript_path, r, encodingutf-8) as f: t json.load(f) for i in range(n_seg): seg y[i * total_frames:(i 1) * total_frames] mfcc librosa.feature.mfcc(yseg, srsr, n_mfcc40, n_fft2048, hop_length512) # 转成帧序列 (time, 40) mfcc mfcc.T.astype(np.float32) # 语言特征从整段文本里抽和MFCC共用同一个subject words re.findall(r[\u4e00-\u9fa5], t[full_text]) filler [嗯, 啊, 那个, 就是, 然后, 这个] lang_feat np.array([ len(words) / max(t[duration_s], 1), sum(1 for w in words if w in filler) / max(len(words), 1), t.get(pause_count, 0) / max(t[duration_s], 1), ], dtypenp.float32) features_all.append({mfcc: mfcc, lang: lang_feat}) return features_all逻辑说明extract_all把单个音频切成若干 30 秒窗每窗抽 40 维 MFCC转成(time, 40)的序列格式同时从转写 json 里抽出语速、填充词密度、停顿频率三个语言特征。参数说明n_fft2048在 16k 采样率下对应 128ms 窗能兼顾语音的时频分辨率hop_length512是 32ms 步进30 秒窗会得到约 937 帧这个帧数对 LSTM 比较友好如果训练爆显存可以调成hop_length1024帧数减半但短停顿的细节会受影响。4. 序列建模与融合训练把两类特征变成预测概率4.1 选型逻辑为什么从 LSTM 起步而不是一上来就上 Transformer很多人玩过“lstm 预测”那套时间序列例程比如正弦波、股价之类的拿到 AD 语音就想直接上 Transformer。我的建议相反样本量不到两千条时LSTM 更踏实。语音序列是长程依赖加局部噪声混合体Transformer 的全局注意力确实更强但需要大量数据把它喂饱否则会过拟合到录音噪音上。就像拿 transformer 预测正弦数据很容易可真实老人录音是非平稳、带口音、带环境噪声的小模型反而更容易学到稳定规律。模型结构上常见做法是双支路MFCC 序列进 LSTM输出最后时间步的隐状态语言特征进一个两层 MLP。两条支路的输出拼接再过一层 Dropout 加全连接输出二分类概率。标签上我做三分类而不是纯二分类健康、轻度认知障碍MCI、疑似阿尔兹海默症这样更贴合社区筛查的需求轻度认知障碍那个类别本身也有筛查价值。import torch import torch.nn as nn class ADSpeechModel(nn.Module): def __init__(self, n_mfcc40, hidden128, n_layers2, lang_dim3, n_classes3, dropout0.4): super().__init__() self.lstm nn.LSTM(n_mfcc, hidden, n_layers, batch_firstTrue, bidirectionalTrue, dropoutdropout) self.mlp nn.Sequential( nn.Linear(lang_dim, 32), nn.ReLU(), nn.Dropout(dropout), ) self.classifier nn.Sequential( nn.Linear(hidden * 2 32, 64), nn.ReLU(), nn.Dropout(dropout), nn.Linear(64, n_classes), ) def forward(self, mfcc, lang): out, _ self.lstm(mfcc) # (batch, time, hidden*2) # 取最后有效帧而不是固定最后一步避免padding噪声 out out[:, -1, :] lang self.mlp(lang) feat torch.cat([out, lang], dim-1) return self.classifier(feat)逻辑说明nn.LSTM用batch_firstTrue方便直接喂(batch, time, freq)bidirectionalTrue会让模型同时看到句子前后的停顿结构对填充词、语义重复这类特征更敏感。forward里 MFCC 过 LSTM 后取最后时间步的隐状态再和 MLP 处理过的语言特征拼接过分类头。参数说明hidden128对二维 LSTM 够用调大收益不大还容易过拟合n_layers2是折中一层欠拟合三层在样本量小时训练不稳定dropout0.4是血泪经验试过 0.3 到 0.50.4 在这个任务里泛化最稳。4.2 训练流程类别权重、早停与随机裁剪增强数据量小是 AD 语音数据的常态健康样本比患者样本多得多类别不平衡必须处理。我在损失函数里加权同时用早停避免过拟合。增强部分只做两件事随机裁剪 MFCC 的时间维度、给 MFCC 加小幅噪声。不做变速增强因为语速本身是重要特征变速会破坏它。import torch import torch.nn as nn import numpy as np def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0 for batch in loader: mfcc, lang, label [x.to(device) for x in batch] # 随机裁剪从937帧中随机截512帧 if mfcc.size(1) 512: start np.random.randint(0, mfcc.size(1) - 512) mfcc mfcc[:, start:start 512] optimizer.zero_grad() logits model(mfcc, lang) loss criterion(logits, label) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() total_loss loss.item() return total_loss / max(len(loader), 1)逻辑说明训练循环里给 MFCC 做了随机裁剪迫使模型不过度依赖某一时间窗的局部特征也算变相数据增强。clip_grad_norm_把梯度范数压到 5.0这是处理语音序列训练不稳定的常用手段LSTM 在长序列上梯度容易爆炸。参数说明裁剪窗口 512 帧约 16 秒仍然保留足够的句法上下文batch 大小我在DataLoader里设 16这个任务 16 比 32 收敛稳因为序列长、显存占用高。损失函数用带权重的交叉熵类别权重直接用训练集各类别样本数反比def get_criterion(labels): n len(labels) counts np.bincount(labels) weights n / (len(counts) * counts) return nn.CrossEntropyLoss(weighttorch.tensor(weights, dtypetorch.float32))参数说明类别权重计算方式为n / (K * count_c)避免某个类别权重过大模型输出 logits 直接进CrossEntropyLoss不需要手动过 softmax。4.3 按说话人分组阻止模型“记住身份”而不是“识别障碍”这是整个工程里最重要的一个坑。AD 语音数据集通常一个人录多段朗读、复述、自由对话各几段。如果做普通随机划分同一个人的多段语音会同时出现在训练和验证集里。LSTM 有很强的记忆能力它会直接记住说话人音色验证集上 AUC 虚高到 0.95毫无意义。解决办法是肯定能用上的from sklearn.model_selection import GroupKFold subjects np.array([item[subject_id] for item in dataset_meta]) groups np.array([item[group_id] for item in dataset_meta]) gkf GroupKFold(n_splits5) for train_idx, val_idx in gkf.split(np.zeros(len(subjects)), None, groups): # train_idx / val_idx 里不会出现同一个说话人 pass逻辑说明GroupKFold按group_id划分这里的group_id就是说话人 ID。划分后同一个老人的所有录音只会出现在训练或验证中的一侧。参数说明n_splits5对几十到几百人的数据量合适如果样本数很少可以降到 3关键是把group_id设成患者 ID 而不是录音 ID这是最容易写错的地方。5. 避坑与排查AD 语音预测里最容易翻车的五个细节5.1 现象AUC 0.95部署后直接崩盘原因随机划分数据同一说话人的不同录音同时出现在训练集和验证集模型学到的是“认出这个人的音色”而不是认知障碍信号。解决一律用GroupKFold按说话人分组评估结果只认分组后的指标。这条要写进项目规范里任何人来跑实验都先检查划分代码。5.2 现象ASR 把老人的停顿全吞了特征里没有 pause_count原因Whisper 默认的vad_filterTrue会把静音段全部切掉再转写停顿信息在第一层就被过滤了。解决转写时vad_filterFalse停顿统计独立在原始音频上进行不要依赖 ASR 的时间戳。如果转写结果噪声太多可以先用silero-vad做一次静音段检测但不把它作为转写的输入过滤条件。5.3 现象模型预测结果和年龄高度相关老年组全被判为高危原因MFCC 全局归一化把年龄差异和认知差异混在一起模型学到了“声音老认知障碍”。解决MFCC 按每段录音自身均值做归一化而不是按整个数据集。语音特征用“偏离个人基线”的方式表达比如语速除以该说话人健康时的平均语速。这个项目里我吃过亏后来所有特征都改成个体内标准化。5.4 现象类别不平衡把模型训练成“只会说没事”原因健康老人数量远大于患者模型发现全预测为阴性就能拿到很高准确率。解决训练时用类别权重损失函数按样本数反比加权评估指标不看 Accuracy看 PR-AUC 和敏感度。医疗筛查场景宁可把临界样本判为阳性也不能漏阈值可以往 0.4 压。5.5 现象推理时音频长度超过 30 秒模型直接报错原因训练时固定输入 30 秒推理时遇到 1 分钟的自由对话没有切窗逻辑。解决推理脚本里把长音频切成 30 秒重叠窗重叠 50%每窗独立预测最后对概率取平均。这个操作既能覆盖整段对话又能避免切到句中被破坏语义也是可解释性里“定位危险片段”的基础。6. 进阶技巧用校准曲线和语音片段提高预测可信度模型能跑了但要做临床预筛AUC 不够医生更关心两个问题这个概率值准不准凭什么说这一段录音有问题。校准是第一个技巧。直接看predict_proba输出的 0.7 是否真的意味着 70% 风险用可靠性曲线可视化。常见做法是训练后做 temperature scaling在验证集上用一维参数重新缩放 logits把概率校准到置信区间内。可解释性上用最朴素的办法把 SHAP 值里贡献最大的语言特征和声学特征找出来再回放对应时间段。我的做法是把 ASR 段级时间戳和 SHAP 权重对齐输出一个清单“第 12 到 18 秒出现 3 次超过 1.5 秒的停顿模型对此处的风险贡献最大”。这种输出给医生比任何可视化热力图都直观。阈值调整上社区筛查我会把阈值定在敏感度 0.85 对应的点而不是默认 0.5代价是特异性下降但把疑似者转到神经内科做量表确认比漏掉早期患者划算得多。这也是这个方向我踩过最深的坑一开始追求高准确率把阈值拉到 0.6结果漏掉两个轻度认知障碍样本被合作方要求返工。后来所有模型评估都附带“阈值-敏感度-特异性”三列对照阈值永远为召回目标服务——筛查工具不决策只报警。希望这个方向能帮你少走几段弯路也希望你手里的语音数据能真正替那些说不清话的老人争取到早一步干预的机会。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑