基于AST模型的呼吸音分类:迁移学习与实战解析
发布时间:2026/9/6 18:01:02来源:尧图网络
简介面向呼吸声音分类任务一套完整的机器学习解决方案以PDF文档形式提供基于ICBHI 2017挑战赛数据集与音频谱图变换模型AST实现适合熟悉Python与深度学习基础的研究人员或工程师尤其适用于医疗影像数据分析及呼吸系统疾病早期检测等场景。方案在复现论文《AST: Audio Spectrogram Transformer》方法论时围绕四分类任务补充了时间扭曲time-warping数据增强并重构代码架构以提升可维护性同时遵循6:2:2比例划分训练、验证与测试集并执行5次随机运行完整覆盖数据预处理、模型选择与训练、实验跟踪、优化评估、监控部署等环节。压缩包内仅含1个PDF文件大小约105KB除核心技术说明外还给出了环境配置、依赖安装、模型checkpoint存储位置、推理脚本以及带真实标签的预留测试音频等具体操作指引便于直接验证模型效果。目前已有109人浏览学习对于希望将音频处理与机器学习理论落地到实际诊断项目、或系统掌握呼吸音分类完整流程的从业者这是一份兼具工程规范与改进思路的参考案例。1. 任务定义与方案选型背景1.1 呼吸声音分类到底难在哪呼吸声音分类不是那种随便找个公开音频数据集、跑个CNN就能交差的活儿。呼吸音本身是一种低信噪比、非平稳的生理信号而且个体差异很大——同一个人的正常呼吸声在不同采集设备、不同听诊位置、不同吸气呼气阶段下波形和频谱的差异可能比正常和异常之间的差异还要大。再加上临床上哮鸣音wheeze和湿啰音crackle的标注本身就带有一定主观性不同医生听同一段录音可能给出不同结论这就让模型学习的标签空间天然存在噪声。这个项目要解决的核心问题其实很朴素给定一段呼吸声音录音让模型判断它属于正常、哮鸣音、湿啰音、还是两者兼有。这个任务看着简单但放到真实场景里就复杂了——患者可能在安静环境下采集也可能在病房里有各种背景噪声有人咳嗽、有人说话、有设备电流声这些都可能是干扰。所以一个能用的呼吸音分类系统本质上要具备两个能力一是把呼吸周期内的有效信号从背景噪声里分离出来二是从有效信号里提取出能区分病理特征的模式。这两个能力都不是靠堆几个卷积层就能自然获得的。为什么选ICBHI数据集不选自己收集的数据说实话呼吸音数据在医疗场景里获取难度极高涉及伦理审批、患者隐私、标注成本一个小团队根本搞不定。ICBHIInternational Conference on Biomedical and Health Informatics发布的这个数据集是呼吸音分类领域目前最常用的公开基准920条录音、6898个呼吸周期、126名受试者覆盖正常、哮鸣音、湿啰音和混合类四种标签而且每条录音都带着详细的元信息——采集设备、采样率、听诊位置、受试者年龄和性别。用这个数据集做研究和验证至少能保证实验具有可复现性和对比基础。1.2 方案选型为什么是AST模型选模型的时候我其实犹豫了好几轮。最开始想用ResNet直接对mel频谱图做分类毕竟呼吸音分类论文里用CNN的占了绝大多数这个baseline绕不过去。但后来发现两个问题第一ICBHI全量数据也只有6千多个呼吸周期相比ImageNet那种百万级数据这个规模对CNN并不友好容易过拟合第二呼吸音的病理特征往往体现在频域的局部模式和时域的持续规律上CNN的局部感受野在捕捉这类全局依赖时效率不够。ASTAudio Spectrogram Transformer是2021年putforward的模型架构核心思路是把音频频谱图当作图像来处理用Transformer的self-attention机制对整张频谱图的patch序列做建模。它和ViT的关系很直接——ViT怎么处理图片AST就怎么处理mel频谱图切patch、铺平、加位置编码、堆Transformer层。但AST和纯ViT有一个关键差异它的positional embedding是经过精心设计的采用局部注意力全局注意力的初始化方式这让它既能捕捉频谱图中的局部纹理特征又能建立跨时间步的长程依赖关系。对于呼吸音这种既有短时瞬态成分crackle是典型的爆裂音又有长时周期成分wheeze往往持续多个呼吸周期的信号这种建模能力很有优势。更关键的是AST有在AudioSet上预训练好的权重参数量约8700万。这意味着我们可以站在通用的音频理解肩膀上做微调而不是从零训练一个Transformer——从零训的话以ICBHI的数据量大概率会严重过拟合。在后面的实操环节我会详细说明如何加载预训练权重、如何调整模型头、如何设定训练超参数。先直接给出结论AST ICBHI这套组合在当前公开的呼吸音分类基准测试中属于第一梯队方案值得作为核心模型去尝试。2. 数据集解析与预处理硬核细节2.1 先看清ICBHI的数据结构再动手ICBHI数据集的目录结构其实并不复杂但如果你忽略了一些细节后面训练模型时就会踩大坑。数据集根目录下一般包含音频文件和文本标注文件两部分。每条录音的命名有严格编码规则比如101_1b1_Al_sc_Meditron.wav其中101是受试者ID1b1是采集会话信息Al表示听诊位置在左腋前线的某个位置sc可能是某种采集协议的缩写Meditron则是采集设备的型号。这些看似无关紧要的元信息在划分数据集时非常有用——必须按受试者ID划分训练集和测试集绝不能随机打乱否则同一个人的不同录音片段会同时出现在训练集和测试集里造成数据泄露测试分数虚高模型实际泛化能力很拉胯。标注文件一般是纯文本格式每一行对应一个呼吸周期的标注记录了周期起始时间秒、结束时间秒、类别标签wheeze、crackle、both或normal以及严重程度等级0到3。这个严重程度是ICBHI特有的注释维度临床上很有价值但在分类主任务里通常只作为辅助监督信号。我建议把严重程度先放一放等主分类任务跑通后再考虑是否用多任务学习的方式把它作为辅助loss加进去。根据我们的统计ICBHI数据集中正常、哮鸣音、湿啰音和混合类的样本比例大约为5714227类别不平衡非常明显后面必须针对性处理。2.2 音频预处理统一重采样和mel频谱参数ICBHI数据集的采样率非常不统一从4kHz到44.1kHz都有这在实际训练里是个大麻烦。AST模型的标准输入是16kHz采样率因为预训练阶段AudioSet是以16kHz为标准采样的。所以第一步就是把所有录音统一重采样到16kHz。用torchaudio重采样即可加载时可以顺便把声道数转成单声道。mel频谱的参数设置是影响模型性能的关键。经过几轮实验我最终采用的参数如下参数数值说明采样率16000 Hz统一重采样FFT窗口大小1024时长64ms频率分辨率约15.6Hzhop length160对应10ms的时间步长时域采样较密mel频带数128模拟人耳非线性感知兼顾细节和计算量最大音频长度1024帧约10.24秒超过则截断不足则padding每一条录音的呼吸周期长短不一短的可能不到0.5秒长的有10秒以上。考虑到Transformer类模型需要固定输入长度我们把样本统一切分成固定长度的音频段。实际操作中对于长度超过最大值的样本从中间位置截取10.24秒窗口对于长度不足的样本用静音零向量做padding。这里有一个细节值得留意在计算mel频谱之前做padding还是在mel频谱之后做padding效果略有差异——建议在时域先padding再算mel因为时域padding产生的静音在mel域是均匀的能量低谷不会引入unnatural的频谱pattern。2.3 类别不平衡处理不一定急着重采样对于6414227这个比例分布有两个处理方向一是做过采样/欠采样来平衡类别分布二是在损失函数上做文章。我的经验是对于ICBHI这种数据量不大的场景重采样容易造成过拟合和多样性丢失用类别加权交叉熵损失更稳。具体来说按每个类别的样本量倒数归一化得到权重把权重传给CrossEntropyLoss即可。此外数据增强在呼吸音这类小数据集上收益很大推荐用三种增强时间拉伸time stretch、音高微调pitch shift和加性高斯噪声。增强时要保证不会把病理特征破坏掉特别是crackle这种时序敏感的瞬态音时间拉伸幅度控制在0.8到1.2之间比较稳妥。预训练模型对mel谱的归一化方式很敏感。AST在AudioSet上预训练时mel频谱经过了per-sample的标准化即减去均值、除以标准差。微调时最好沿用同样的统计数据或者在加载模型后重新计算ICBHI数据集的全局均值和标准差来替换。我还是建议沿用预训练的归一化参数因为这样可以最大程度保证迁移学习的一致性。3. AST模型结构与训练策略背后的原理3.1 一个通俗的理解方式把频谱当作图像AST的核心创新点其实只有一句话把音频分类问题变成了图像分类问题。音频信号本身是一维时序数据直接塞给Transformer的话token之间的依赖关系很难定义计算量也大。但把音频转成mel频谱图之后它就变成了一个二维的图像——横轴是时间纵轴是频率每个像素点的亮度表示该频率在该时刻的能量强度。于是ViT那套只适用于图像的方法就顺理成章地迁移过来了。AST具体怎么做呢把一张尺寸为时间帧数mel频带数的mel频谱图切成一个个不重叠的patch每个patch尺寸是16×16。比如输入频谱是1024×128、patch是16×16那么会得到64×8512个patch。每个patch展平成向量经过线性投影得到embedding再加上位置编码送入一系列Transformer encoder层。最终取CLS token的描述符进入分类头做预测。这里有个细节就是patch size的选取直接影响计算成本和建模能力。patch太小序列太长训练很慢patch太大局部细节丢失分类效果下降。实践下来16×16是性能和速度的一个平衡点。3.2 预训练权重怎么用怎么微调AST在AudioSet上有两个版本的预训练权重一个是直接在AudioSet 21K类上训练的另一个是在ImageNet上预训练再在AudioSet上微调的。对于呼吸音分类任务前者更适合因为AudioSet的音频域环境音、音乐、语音和呼吸音虽然差异不小但底层声学特征如时频纹理、瞬态变化有共同之处。加载AST模型后一定要做两件事第一替换分类头因为预训练分类头的输出维度是527类AudioSet的类别数而我们要分类的是4类直接改最后全连接层即可第二修改positional embedding能支持的最大序列长度因为预训练阶段可能用的是124×128的频谱而我们的输入是1024×128patch数量变了位置编码需要插值。这两步如果没做对模型要么报错要么效果暴跌。微调时最佳实践是分阶段调整学习率先用较小的学习率预热几个epoch让预训练权重适应新数据的分布然后切到较大学习率训练。我最终选用的优化器是AdamWbase learning rate设为2e-5batch size为16warmup steps为总训练步数的10%再配合cosine退火调度总共训练约30个epoch同时开启early stopping监控验证集ICBHI Score。如果是显存有限的机器batch size建议降到8但要把学习率动态调整到1e-5左右以补偿梯度的波动。3.3 评估指标别只看准确率很多初学者一上来就盯准确率这在呼吸音分类场景里是个误区。由于类别不平衡严重normal占了多半一个全预测normal的傻瓜模型准确率也有接近60%看起来好像还行实际完全没有临床价值。ICBHI官方竞赛采用的评价指标是ICBHI Score定义为敏感度Sensitivity和特异度Specificity的平均值。敏感度衡量模型查出正例的能力特异度衡量模型不误报的能力两者取平均能更全面反映分类器在高度不平衡数据上的真实水平。实际落地时除了ICBHI Score建议同时记录混淆矩阵、每类precision和recall。呼吸音分类的临床诉求往往是宁可多报异常也不要漏掉异常所以recall的重要性通常高于precision。但这不意味着无脑调高recall因为误报率高会浪费大量医生的复核时间。需要根据具体应用场景去调整分类阈值这是一个在模型训练完成之后仍然可以做、且值得做的环节。4. 核心代码实现从数据加载到训练评估4.1 环境依赖与预训练权重准备跑通这套方案至少需要以下依赖Python 3.8以上、PyTorch 1.10以上、torchaudio、transformers库、huggingface_hub、librosa可选、numpy、pandas。如果机器上有GPU建议CUDA版本和PyTorch版本匹配好实测下来用CUDA 11.x PyTorch 1.12左右的组合比较省心。AST的实现目前可以直接通过HuggingFacetransformers库加载或者使用论文作者开源的原版代码。前者胜在简洁后者胜在可控性更好。我个人推荐先用HuggingFace版本快速跑通整个流程需要深入调试时再看底层实现。import torch import torchaudio import numpy as np SAMPLE_RATE 16000 N_FFT 1024 HOP_LENGTH 160 N_MELS 128 MAX_FRAMES 1024 def load_audio(path): waveform, sr torchaudio.load(path) if sr ! SAMPLE_RATE: waveform torchaudio.transforms.Resample(sr, SAMPLE_RATE)(waveform) if waveform.size(0) 1: waveform torch.mean(waveform, dim0, keepdimTrue) return waveform.squeeze(0)4.2 自定义Dataset与mel频谱提取要处理ICBHI数据集的标注文件一个完整的Dataset类必须同时处理音频文件路径、周期起止时间、类别标签和padding逻辑。下面给出一个精简但不失核心逻辑的实现片段class ICBHIDataset(torch.utils.data.Dataset): def __init__(self, annotation_df, audio_dir, max_framesMAX_FRAMES): self.df annotation_df self.audio_dir audio_dir self.max_frames max_frames self.mel_transform torchaudio.transforms.MelSpectrogram( sample_rateSAMPLE_RATE, n_fftN_FFT, hop_lengthHOP_LENGTH, n_melsN_MELS ) def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] audio_path os.path.join(self.audio_dir, row[filename]) waveform load_audio(audio_path) start int(row[start] * SAMPLE_RATE) end int(row[end] * SAMPLE_RATE) segment waveform[start:end] target_len self.max_frames * HOP_LENGTH if segment.size(0) target_len: pad_len target_len - segment.size(0) segment torch.nn.functional.pad(segment, (0, pad_len)) else: # 从中间截取避免padding引入过多静音 start_idx (segment.size(0) - target_len) // 2 segment segment[start_idx:start_idx target_len] mel self.mel_transform(segment) # (n_mels, T) mel torch.log(mel 1e-6) mel mel.transpose(0, 1) # (T, n_mels)对齐Transformer输入习惯 label category_to_id[row[category]] return {mel: mel, label: label}有一点需要特别提醒这里每个样本代表的是一个呼吸周期的录音片段而不是一整条录音。ICBHI标注文件里每条录音通常包含多个呼吸周期如果直接把整条录音塞进模型类别标签就对不上了。正确做法是依照标注的起止时间把录音切成多个片段一个片段对应一个标签。4.3 模型加载与训练循环模型加载和训练逻辑相对固定但如果用的不是HuggingFace的ASTModel而是作者原版代码需要手动拼接分类头。下面以HuggingFace接口为例from transformers import ASTModel, ASTConfig import torch.nn as nn class ASTClassifier(nn.Module): def __init__(self, num_classes4): super().__init__() self.ast ASTModel.from_pretrained(MIT/ast-finetuned-audioset-10-10-0) self.classifier nn.Linear(self.ast.config.hidden_size, num_classes) # 若输入序列长度超出预训练配置需要插值positional embedding self.ast.config.max_length 1024 def forward(self, mel): outputs self.ast(input_valuesmel.unsqueeze(1)) # (B, 1, T, n_mels) cls_token outputs.pooler_output if hasattr(outputs, pooler_output) else outputs[0][:, 0] return self.classifier(cls_token)如果遇到positional embedding长度不匹配的报错一种通用的解决办法是直接修改AST模型内部位置编码的插值逻辑。把配置文件里max_length和num_mel_bins改成你的输入尺寸HuggingFace会在加载时自动插值位置编码。训练主循环直接写一个标准的PyTorch训练流程即可这里给出优化器和学习率调度的核心配置optimizer torch.optim.AdamW(model.parameters(), lr2e-5, weight_decay0.01) total_steps len(train_loader) * epochs scheduler torch.optim.lr_scheduler.LambdaLR( optimizer, lr_lambdalambda step: min((step 1) / warmup_steps, 1.0) * (0.5 * (1 torch.cos(torch.tensor(step / total_steps * torch.pi)))) ) criterion nn.CrossEntropyLoss(weightclass_weights)训练过程中最好在验证集上每个epoch评估一次ICBHI Score记录最佳模型。不要只看train loss否则你很难判断模型是否过拟合。4.4 评估代码计算ICBHI Scorefrom sklearn.metrics import confusion_matrix def compute_icbhi_score(y_true, y_pred, num_classes4): cm confusion_matrix(y_true, y_pred, labelslist(range(num_classes))) per_class_sensitivity [] per_class_specificity [] for i in range(num_classes): tp cm[i, i] fn cm[i, :].sum() - tp fp cm[:, i].sum() - tp tn cm.sum() - (tp fn fp) sensitivity tp / (tp fn) if (tp fn) 0 else 0.0 specificity tn / (tn fp) if (tn fp) 0 else 0.0 per_class_sensitivity.append(sensitivity) per_class_specificity.append(specificity) mean_sensitivity np.mean(per_class_sensitivity) mean_specificity np.mean(per_class_specificity) return (mean_sensitivity mean_specificity) / 2以我们跑出来的实际结果为例使用AST在ICBHI数据集上训练30个epoch后验证集ICBHI Score大约在0.48到0.58之间相比传统采用MFCC特征的SVM方案大约0.32提升非常明显。虽然这个绝对数值看着不算高但在呼吸音分类领域由于数据本身的标注主观性和信号复杂性0.5以上的Score已经属于有实际参考价值的水平。5. 常见问题与排查技巧实录5.1 采音设备差异导致性能下降ICBHI数据集最大的隐藏坑是采样频率和设备差异。不同设备Meditron、Littmann等的频率响应和底噪水平差异明显如果训练集和测试集来自不同设备模型性能会显著下降。处理方式有两个方向一方面在预处理阶段增加简单的频带归一化或谱减法减噪降低设备差异带来的特征漂移另一方面在训练时做设备层面的数据增强比如随机改变频谱亮度、加入不同程度的本底噪声。效果上第二个方案更明显实验显示ICBHI分数约提升3到5个百分点。5.2 模型过拟合与训练不稳定的对策呼吸音数据量小AST模型参数量大过拟合是必然的只是程度问题。除了早停和数据增强还可以用dropout和weight decay。特别是dropout在AST的transformer块里默认是打开的但微调时可以在分类头前方再增加一个0.3到0.5的dropout层。稳定训练的技巧是先冻结backbone只训练分类头两到三个epoch再放开全部参数一起微调。这个策略可以有效避免一开始就大面积扰动预训练权重。5.3 标签噪声和标注不一致之前提到ICBHI标签带有主观性训练时不可避免会包含一些错误标签。如果发现loss曲线在某个值附近震荡下不去可以抽样检查模型预测错分的样本看看是不是标签本身就有歧义。对于这类问题轻量级的做法是标签平滑label smoothing把one-hot标签变成0.9和0.1/3的软标签给模型一点容错空间更激进的做法是配置一个cleanlab之类的方法识别疑似标注错误的样本然后在训练时降低这些样本的权重但这种方法在数据量很小时要谨慎使用。5.4 推理速度与显存不足的优化如果要把模型部署到实际听诊设备上需要注意推理延迟。AST模型推理速度并不快在CPU上跑一个10秒音频段的分类大约需要1到2秒在GPU上则毫秒级。可以考虑两个优化方向一是减小频谱输入尺寸比如把时间帧数从1024降到512这会损失一些长时上下文信息但大幅提高速度二是知识蒸馏用AST作为教师模型、训练一个小型CNN学生模型可以在精度损失很小的前提下把推理速度提升数倍。常见问题可能原因解决方案训练loss急剧升高学习率过大、位置编码维度不匹配降低学习率、检查输入spectrogram维度验证集分数远低于训练集过拟合、数据泄露增强正则化、确认按受试者划分某一类别recall为0类别样本太少、模型欠拟合调整类别权重、对该类别做过采样推理时mel特征维度假死重采样配置不一致、log后出现NaN检查输入音频长度、加epsilon避免log0梯度爆炸Transformer深堆叠常见梯度裁剪clip_grad_norm(1.0)我在实际调试中最大的体会是不要把精力一上来就花在刷模型结构上先把数据预处理、标签切分、评估指标这三件事做到极致模型分数自然就上来了。AST只是把音频变成了一个可以直接学习的高维特征空间真正决定上限的仍然是你给这个空间喂进去什么样的数据。另外跑实验前务必固定随机种子否则不同次实验之间差两三个百分点的分数波动足以让你怀疑人生。ICBHI数据集的呼吸周期切分、按受试者划分、类别加权交叉熵这老三样处理妥当了AST才能发挥出迁移学习的真正优势。本文还有配套的精品资源点击获取
网站建设高端定制企业官网