
简介这份资源面向医疗AI方向的学习者与研究者提供一套基于视觉变换网络ViT实现自闭症谱系障碍ASD儿童脸部分析检测的完整项目实战代码可用于理解如何将Transformer架构迁移到医学图像分类任务适合具备一定深度学习基础、希望积累医疗健康领域落地经验的中高级开发者。压缩包共39个文件约3.42MB以17个Python脚本和12个YAML配置为主前者覆盖模型定义、训练评估、数据加载与注意力可视化等核心逻辑后者对应不同规模与数据集的实验配置另含少量图片、缓存与说明文档。项目围绕面部图像数据标注、ViT模型训练与ASD特征识别展开借助自注意力机制自动捕捉表情、注视模式、头部姿态等关键线索并输出患病预测结果。目录中模型、配置、数据集与可视化模块划分清晰便于读者复现训练流程、调整超参并迁移到其他面部相关神经发育疾病的分析场景。目前已有158人学习关注。1. 自闭症谱系障碍脸部分析ViT 能读出什么信号儿童自闭症谱系障碍ASD的早期筛查临床上长期依赖 ADOS-2、M-CHAT 这类行为量表评估一次动辄四十分钟起步还高度依赖评估师经验。而自闭症儿童在社交注视、面部表情回应上的异常往往比语言发育迟缓出现得更早——这恰恰是视觉模型能切入的地方。ViTVision Transformer把图像切成 patch 序列后做全局自注意力相比 CNN 的局部感受野它更容易捕捉眉眼间距、注视方向、嘴角对称性这类跨区域的微弱关联。这个项目要做的就是拿 ASD 儿童与典型发育TD儿童的面部图像训练一个 ViT 分类器输出辅助筛查概率。适合有 PyTorch 基础、想跑通一个完整医学图像分类链路的同学也适合做儿童心理/行为研究的从业者了解模型边界。它不是诊断工具而是一个把「脸部分析」变成可复现实验的技术骨架。2. ViT 做 ASD 脸部分析为什么选它、数据怎么摆2.1 从 CNN 换到 ViT 的三个现实理由先说选型。ASD 脸部分析这个任务图像本身分辨率不高但判别信息分散——不是某一块局部纹理决定分类而是多个面部区域的几何关系共同作用。CNN 靠堆叠卷积核扩大感受野浅层只能看到局部要覆盖整张脸得堆很深ViT 从第一层就做全局 attention每个 patch 都能直接和其余 patch 交互。这是第一个理由全局依赖建模更直接。第二个理由是迁移成本。ViT 在 ImageNet-21k 上预训练后patch embedding 已经学到了通用的人脸结构先验微调时只需要少量样本就能收敛。ASD 公开数据集普遍偏小几百到几千张量级从头训 CNN 很容易过拟合而预训练 ViT 冻结前几层 只调分类头是更稳的起点。第三个理由是可解释性。ViT 的 attention map 能可视化出模型到底在看哪里——是眼睛、嘴巴还是脸颊轮廓。对医学辅助场景这个「黑匣子」能打开一条缝比 CNN 的 Grad-CAM 更贴近 patch 级的注意力分布。当然代价也明显ViT 吃数据、吃显存小数据集上如果不做强增强和正则翻车概率不低。2.2 数据集目录结构与标签约定常见做法是按train/val/test三段划分每段下按类别建子目录。标签用 0/1 二分类0 代表 TD典型发育1 代表 ASD。目录长这样dataset/ ├── train/ │ ├── ASD/ # 标签 1 │ └── TD/ # 标签 0 ├── val/ │ ├── ASD/ │ └── TD/ └── test/ ├── ASD/ └── TD/用torchvision.datasets.ImageFolder直接读它会按子目录名排序生成类别索引。注意ImageFolder的类别顺序是按字母排的ASD排在TD前面所以class_to_idx是{ASD: 0, TD: 1}。如果你在别处硬编码了 0TD这里就会标签反转训练 loss 看着降但指标全乱。我一般会在训练脚本开头打印一次dataset.class_to_idx确认。数据量参考公开的 ASD 面部数据集通常在千张级别train:val:test 按 7:1.5:1.5 切。如果原始数据只有 ASD 和 TD 混在一起没有划分先写个脚本按比例复制别在训练时用随机 split否则每次跑结果不可复现。2.3 图像预处理与增强参数怎么定ViT 的标准输入是 224×224。原始面部图如果分辨率更高先 resize 到 256 再 CenterCrop 到 224这是 timm 里 ViT 的惯用流程。归一化用 ImageNet 的 mean/std因为预训练权重就是在这个分布上学的。import torch from torchvision import transforms train_tf transforms.Compose([ transforms.Resize(256), # 先缩到 256保留更多边缘信息 transforms.RandomResizedCrop(224, # 随机裁剪再缩到 224模拟不同拍摄距离 scale(0.7, 1.0)), transforms.RandomHorizontalFlip(0.5),# 面部左右对称翻转是安全增强 transforms.RandomRotation(10), # 轻微旋转容忍拍摄角度偏差 transforms.ColorJitter(0.2, 0.2, 0.2), # 亮度/对比度/饱和度抖动 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), # 验证集不做随机增强保证可复现 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])参数说明RandomResizedCrop的scale(0.7, 1.0)是关键。下限设太低比如 0.5会把脸裁掉一半模型学到的就是残缺特征设太高0.9 以上增强效果几乎为零。0.7 是我在面部数据上比较稳的取值。RandomRotation(10)别超过 15 度面部朝向本身是判别信息之一转太多等于把信号抹掉。ColorJitter的强度 0.2 是保守值医学图像不建议做太激进的色彩扰动否则肤色相关的微弱特征会被破坏。注意验证集和测试集绝对不能用RandomResizedCrop或RandomHorizontalFlip否则每次评估结果都在变你没法判断模型到底有没有进步。3. 用 timm 加载 ViT 并跑通训练最小可复现命令3.1 环境与依赖安装先确认 CUDA 可用然后装 timm。timm 封装了 ViT 的预训练权重加载比手写 patch embedding 省事得多。pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install timm scikit-learn matplotlib python -c import torch; print(torch.cuda.is_available())最后一行应该输出True。如果是False先排查驱动和 CUDA 版本匹配别急着往下跑——CPU 上训 ViT 基本等于放弃。3.2 加载预训练 ViT 并替换分类头import timm import torch.nn as nn def build_model(num_classes2, drop_rate0.1): # vit_base_patch16_22412 层768 维12 头patch 16 model timm.create_model( vit_base_patch16_224, pretrainedTrue, # 加载 ImageNet 预训练权重 num_classesnum_classes, # 直接替换分类头为 2 类 drop_ratedrop_rate, # 分类头前的 dropout小数据集必备 ) return model逻辑说明timm.create_model的pretrainedTrue会自动下载并加载权重num_classes2会把原来的 1000 类头换成 2 类线性层。drop_rate0.1是分类头的 dropout不是 attention dropout。小数据集上这个值很关键设 0 容易过拟合设 0.3 以上又欠拟合0.1~0.2 是常见区间。如果想进一步省显存可以冻结前 8 层for name, param in model.named_parameters(): if blocks.0 in name or blocks.1 in name or \ blocks.2 in name or blocks.3 in name or \ blocks.4 in name or blocks.5 in name or \ blocks.6 in name or blocks.7 in name: param.requires_grad False冻结底层是因为浅层学的是边缘、纹理这类通用特征ASD 任务不需要重学。只微调后 4 层 分类头训练更快过拟合风险更低。3.3 训练循环与关键超参import torch from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR train_ds ImageFolder(dataset/train, transformtrain_tf) val_ds ImageFolder(dataset/val, transformval_tf) print(class_to_idx:, train_ds.class_to_idx) # 务必确认标签映射 train_loader DataLoader(train_ds, batch_size16, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size16, shuffleFalse, num_workers4) device torch.device(cuda) model build_model().to(device) criterion torch.nn.CrossEntropyLoss() optimizer AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr2e-5, weight_decay0.05) scheduler CosineAnnealingLR(optimizer, T_max30) for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() loss criterion(model(imgs), labels) loss.backward() optimizer.step() scheduler.step() # 验证 model.eval() correct total 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) pred model(imgs).argmax(1) correct (pred labels).sum().item() total labels.size(0) print(fepoch {epoch}: val_acc{correct/total:.4f})参数说明lr2e-5是微调 ViT 的典型学习率比从头训小两个数量级。weight_decay0.05配合 AdamW 是 ViT 微调的标准组合别用 0.01那个值对 Transformer 偏小。batch_size16是 8G 显存下的保守值显存够可以上 32。CosineAnnealingLR的T_max30要和总 epoch 数一致让学习率平滑降到接近 0。提示如果 val_acc 在前 5 个 epoch 就冲到 0.95 以上然后不动了大概率是数据泄漏——train 和 val 里有同一张图的副本或者划分时没打乱。先查数据别调模型。4. 评估指标与 attention 可视化别只看准确率4.1 类别不平衡下的指标选择ASD 数据集里 TD 样本往往多于 ASD如果直接看 accuracy模型全预测 TD 也能有 60%。必须看 sensitivity召回 ASD和 specificity召回 TD以及 AUC。from sklearn.metrics import classification_report, roc_auc_score import numpy as np model.eval() all_probs, all_labels [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs imgs.to(device) probs torch.softmax(model(imgs), dim1)[:, 1] # ASD 概率 all_probs.extend(probs.cpu().numpy()) all_labels.extend(labels.numpy()) all_probs np.array(all_probs) all_labels np.array(all_labels) preds (all_probs 0.5).astype(int) print(classification_report(all_labels, preds, target_names[TD, ASD])) print(AUC:, roc_auc_score(all_labels, all_probs))classification_report会给出每个类别的 precision/recall/f1。重点看 ASD 那一行的 recall——这是筛查场景最不能漏的指标。AUC 低于 0.7 基本说明模型没学到有效信号别急着部署先回去查数据和增强。4.2 用 attention map 看模型关注区域ViT 的最后一层 attention 可以 reshape 回 patch 网格叠加到原图上。import matplotlib.pyplot as plt def visualize_attention(model, img_tensor, patch_size16, img_size224): model.eval() with torch.no_grad(): # 取最后一个 block 的 attention attn model.blocks[-1].attn x model.patch_embed(img_tensor.unsqueeze(0).to(device)) B, N, C x.shape # 手动前向到最后一层拿 attention 权重 # 简化写法直接 hook # 实际用 register_forward_hook 抓 attn.attn_drop 前的权重 # 这里给出 reshape 逻辑 grid int((img_size // patch_size)) # 14 # attn_weights: [B, heads, N, N]取 CLS token 对所有 patch 的注意力 # cls_attn attn_weights[:, :, 0, 1:].mean(1) # [B, N-1] # cls_attn cls_attn.reshape(grid, grid) # plt.imshow(cls_attn.cpu(), cmapjet); plt.show()这段代码给的是 reshape 逻辑框架实际抓 attention 权重需要用register_forward_hook挂在model.blocks[-1].attn.attn_drop上。核心思路取 CLS token 对所有 patch 的注意力平均多头后 reshape 成 14×14 的热力图。如果热力图集中在眼睛和嘴巴区域说明模型学到了合理的面部特征如果散在背景上说明数据里背景干扰太大需要做人脸对齐或裁剪。注意attention map 好看不等于模型可靠。它只是「模型在看哪里」不是「模型为什么这么判断」。别把可视化当成医学解释。5. 避坑与排查ASD 脸部分析里最容易翻车的五件事5.1 验证集准确率异常高测试集崩了现象val_acc 0.95test_acc 0.55。原因train/val/test 划分时同一受试者的多张照片被分到了不同集合模型记住了人脸身份而不是 ASD 特征。解决按受试者 ID 划分同一个人所有照片只进一个集合。如果数据没有 ID 信息至少用图像哈希去重后再切分。5.2 loss 不降梯度全是 NaN现象训练几个 step 后 loss 变 nan。原因ViT 微调学习率设太大或者weight_decay和 AdamW 配合不当导致梯度爆炸。解决lr 降到 1e-5 甚至 5e-6加torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)做梯度裁剪。另外检查输入归一化有没有漏未归一化的像素值直接进 patch embedding 也会炸。5.3 模型只预测一个类别现象所有样本都输出 TDrecall(ASD)0。原因类别不平衡 没有加权损失。解决CrossEntropyLoss(weighttorch.tensor([1.0, 3.0]).to(device))给少数类更高权重。或者用WeightedRandomSampler在 DataLoader 层面做重采样。权重比例按类别频率的倒数来定别拍脑袋。5.4 显存不够batch_size 降到 4 还是 OOM现象CUDA out of memory。原因ViT-base 在 224 分辨率下batch 16 大约吃 6-8G 显存如果还开了 attention 可视化或保留了中间激活占用更高。解决先冻结更多层减少激活存储用torch.cuda.amp混合精度训练实在不行换vit_small_patch16_224参数量少一半精度掉得不多。5.5 换了数据集后指标全乱现象在自己数据上跑AUC 只有 0.5。原因新数据集的拍摄设备、光照、人脸角度和预训练分布差异太大或者标签映射反了。解决先打印class_to_idx确认标签再可视化几张图确认人脸检测框正常最后用model.eval()跑一遍预训练权重直接推理看输出概率分布是不是接近均匀——如果是说明特征完全没对上需要先在人脸检测对齐上做工作。6. 把 ViT 推到可用分层学习率与早停的实操技巧走到这一步模型能跑通但离「可用」还差一截。我踩过最大的坑是所有层用同一个学习率结果底层预训练特征被破坏顶层还没学好。后来改成分层学习率效果立竿见影。def get_layer_lr_params(model, base_lr2e-5, decay0.75): 底层小学习率顶层大学习率 params [] num_blocks len(model.blocks) # vit_base 是 12 for i, block in enumerate(model.blocks): lr base_lr * (decay ** (num_blocks - i)) params.append({params: block.parameters(), lr: lr}) # 分类头和 patch_embed 单独设 params.append({params: model.patch_embed.parameters(), lr: base_lr * 0.5}) params.append({params: model.head.parameters(), lr: base_lr * 2}) return params optimizer AdamW(get_layer_lr_params(model), weight_decay0.05)逻辑第 0 层学习率是2e-5 * 0.75^12 ≈ 6e-7几乎不动第 11 层是2e-5 * 0.75 ≈ 1.5e-5分类头是4e-5。这样底层通用特征被保护顶层任务特征快速适应。decay0.75是我试出来的值0.5 太激进底层学不动0.9 又和统一学习率没区别。早停策略也重要。ASD 数据集小过拟合来得快。我一般监控 val AUC连续 7 个 epoch 不涨就停同时保存 AUC 最高的那个 checkpoint。best_auc, patience, wait 0, 7, 0 for epoch in range(50): # ... 训练 ... auc roc_auc_score(all_labels, all_probs) if auc best_auc: best_auc auc torch.save(model.state_dict(), best_vit_asd.pth) wait 0 else: wait 1 if wait patience: print(fearly stop at epoch {epoch}, best AUC{best_auc:.4f}) break最后说一个验证方法拿测试集跑一遍把预测概率在 0.4~0.6 之间的样本单独拎出来看。这些是模型的「犹豫区」如果里面混了大量标注错误的样本说明数据质量有问题不是模型不行。我习惯把这个区间叫「后悔药区」——早看一眼能省掉几天的调参。这套流程跑下来在千张级 ASD 面部数据上AUC 做到 0.80 左右是合理预期。再往上瓶颈通常不在模型而在数据量和标注一致性。如果你打算往这个方向投入先把数据划分和标签核对做扎实比换更大的 ViT 有用得多。希望帮到你。本文还有配套的精品资源点击获取