简介:面向计算机相关专业学生、教师及医学图像分析初学者,这份基于Python的机器学习资源针对脑PET图像分析与疾病预测任务,提供了一套完整可运行的源码实现,可直接用于课程设计、毕业设计或实训实验。资源包仅6个文件,大小约10KB,以Python脚本为主体(4个py文件),辅以介绍文档、依赖清单,具备清晰的模块划分:训练脚本负责模型训练与参数调优,预测脚本用于加载新图像输出诊断结果,裁剪脚本可自动提取感兴趣区域,配置文件则集中管理路径与模型参数。项目内置详细介绍与运行说明,代码结构简洁,便于快速复现和二次开发。目前已有84人学习下载,适合希望从零构建脑部疾病预测流程的入门者和进阶者,遇到问题可通过私信交流并获得远程教学指导,能够帮助节省环境配置与代码调试时间,将精力聚焦于算法创新与实验验证。
1. 脑 PET 图像分析和疾病预测:这份源码包到底能解决什么问题
拿到一份基于 Python 机器学习的脑 PET 图像分析和疾病预测源码包,先别急着解压跑训练,得先搞清楚它解决的是什么场景下的问题。脑 PET(正电子发射断层扫描)在阿尔茨海默病、帕金森病、癫痫灶定位这些神经退行性疾病的诊断里,是很有价值的影像手段——它不是看解剖结构,而是看葡萄糖代谢的活跃区域,病灶区往往表现为代谢减低或异常增高。机器学习在这类任务里做的,就是从大量标注好的 PET 图像里学会哪些代谢模式对应哪类疾病,然后对新扫出来的 PET 图像做预测。这份资源最直接的用处,是把「图像读取 → 裁剪 ROI → 特征提取 → 模型训练 → 新样本预测」这条链路完整串起来,适合做课程设计、毕业设计、实训作业,也适合刚入门医学图像方向的同学拿它作为二次开发的地基,而不是从零开始堆环境。
这套代码的核心并不玄乎,关键就三件事:怎么样把原始 PET 影像裁出有效脑区、怎么样训练一个分类器、怎么样把训练好的模型用到新图像上。源码包里的五个文件刚好对应这些环节,下面我按实际执行顺序,把这几个文件和它们之间的依赖关系拆开说清楚。
2. 源码包文件结构和数据流:五个文件是怎么协作的
拿到这个资源包后,第一件事不是运行,而是先把文件之间的调用关系捋清楚。这个包里没有复杂到需要一个框架级的工程结构,它就是最直接的、适合教学和单机运行的一套程序,但正是因为简单,很多人上来就在没装依赖、没改路径的情况下硬跑,结果连报错都看不懂。
2.1 文件清单和职责边界
看一下解压后的文件,核心是这几个:
| 文件 | 职责 | 运行时机 |
|---|---|---|
crop.py | 图像裁剪,提取脑部 ROI 区域 | 训练前预处理 |
1_train.py | 训练机器学习模型 | 第一次运行 |
2_predict.py | 用训练好的模型对新 PET 图做预测 | 训练完成后 |
config.py | 统一存放路径、参数、模型超参数 | 每次运行前修改 |
requirements.txt | 记录 Python 依赖库 | 搭建环境时 |
介绍.md | 项目说明、运行步骤、注意事项 | 通读一遍 |
这个顺序其实就是在告诉你数据流的方向。第一次运行项目时,你大概率是先通读介绍.md,然后按 requirements.txt 装依赖,接着改 config.py 里的路径,跑 crop.py 生成裁剪后的数据,再跑 1_train.py 得模型文件,最后用 2_predict.py 对一张新 PET 图做预测。五个文件之间不是孤立的,config.py 是所有脚本的公共入口,它被另外三个脚本反复读取。
从工程上看,这种结构是有它的合理性的:把参数集中到一个配置文件里,训练脚本和预测脚本就不需要写死任何路径或超参数,换数据集或者换模型时只改 config.py 就行。这也是为什么你在改代码之前必须先把这个文件读一遍——很多人的翻车现场不是算法出问题,而是路径对不上。
2.2 数据从哪里来、到哪里去
PET 图像数据源,可以是你自备的医学影像数据集,也可以是公开的脑影像数据库。这类任务里,ADNI(阿尔茨海默症神经影像计划)数据集是最常被用来做实验的公开数据之一,里面包含大量 PET、MRI 图像和临床诊断标签。处理医学图像时要注意一个常见问题:原始 PET 图像通常是三维体积数据,存储格式多为 NIfTI(.nii)或 DICOM 系列,而这份源码里用到的图像处理流程,大概率是把三维数据从某个轴切出二维切片再做分析,或者是对某个轴向做最大密度投影得到二维图像。
在我的使用习惯里,这一步是整个项目里最需要你亲自动手的部分,因为不同来源的数据集目录结构差别很大。常见的做法是建一个清晰的文件目录——原始数据放在data/raw/,裁剪后的图放在data/processed/,模型输出放在models/。这份资源里的 config.py 主要就是干这个事的,它为你留了一个集中配置的入口。注意,第一次拿到源码包时,data 目录是空的或者只有样例数据,需要你自己把数据集放进去,并同步修改配置。
3. 数据预处理与配置改造:crop.py 和 config.py 里的关键参数
如果说训练是正餐,那数据预处理就是备菜。备菜没做好,再好的菜谱也白搭。这一章我们专门看 crop.py 和 config.py,因为绝大多数运行报错和低准确率问题都出在这一步。
3.1 crop.py:为什么必须裁剪、怎么裁剪
PET 图像不是每一寸都有诊断价值。颅骨外的背景区域、扫描床的伪影、颅骨本身的信号,这些都会干扰模型训练,让模型去学习一些不该学的模式。裁剪的目的就是把脑实质区域或者你关心的 ROI 提取出来,减少无关像素的干扰,同时把图像缩放到统一尺寸,方便模型输入。
crop.py脚本做的事情大致是这三步:读取原始图像 → 确定裁剪边界 → 输出裁剪后的图像并保存。如果是二维切片,边界通常是通过像素阈值来确定的——非脑区在 PET 图像上通常是低值背景,而脑组织因为有代谢摄取而呈现较高的像素值。
import os import numpy as np from nibabel import load as load_nii # 处理医学影像格式 from PIL import Image from config import DATA_ROOT, CROP_OUTPUT_DIR, IMAGE_SIZE def crop_pet_slice(nii_path, threshold_ratio=0.35): """ 读取一张PET的nii文件,按强度阈值确定脑区边界,裁剪并缩放 threshold_ratio: 低于全图最大强度*该比例的像素视为背景,不参与边界计算 """ img = load_nii(nii_path).get_fdata() # 取中间层作为切片,PET体积数据的中间轴通常包含完整脑区 mid_slice = img[:, :, img.shape[2] // 2] max_val = mid_slice.max() # 生成二值掩膜:大于阈值的像素视为脑区 mask = mid_slice > (max_val * threshold_ratio) # 找到掩膜的非零坐标,确定包围盒 coords = np.argwhere(mask) y_min, x_min = coords.min(axis=0) y_max, x_max = coords.max(axis=0) # 加上边距,避免裁剪掉边缘灰质区域 pad = 5 y_min = max(0, y_min - pad) x_min = max(0, x_min - pad) y_max = min(mid_slice.shape[0], y_max + pad) x_max = min(mid_slice.shape[1], x_max + pad) cropped = mid_slice[y_min:y_max, x_min:x_max] # 归一化并缩放到统一尺寸 cropped = (cropped - cropped.min()) / (cropped.max() - cropped.min() + 1e-8) pil_img = Image.fromarray((cropped * 255).astype(np.uint8)) resized = pil_img.resize((IMAGE_SIZE, IMAGE_SIZE), Image.Resampling.BILINEAR) return resized # 批量处理示例 for file_name in os.listdir(DATA_ROOT): if file_name.endswith('.nii') or file_name.endswith('.nii.gz'): out_img = crop_pet_slice(os.path.join(DATA_ROOT, file_name)) out_path = os.path.join(CROP_OUTPUT_DIR, file_name.replace('.nii.gz', '.png').replace('.nii', '.png')) out_img.save(out_path)这段代码里有两个参数值得你花时间调:threshold_ratio和pad。threshold_ratio设大了,会把低代谢的皮层区域当成背景切掉;设小了,扫描床和头颅周围的散射信号会被圈进来。我一般会对数据集中三到五张图先跑一遍裁剪,把中间结果可视化地看一遍再定这个值,别直接用默认值,这个参数跟扫描设备的采集协议强相关,不同中心的数据差异很大。pad是包围盒加边距的像素数,防止阈值分割把脑回边缘的像素恰好切掉。
这段代码是示意写法,实际包里的 crop.py 可能用 OpenCV 或者纯 NumPy 实现,但逻辑是一样的。关键点是:你看源码时重点看它判断边界的规则,搞清楚它是在哪个轴向做的裁剪,这是后面调试的基础。
3.2 config.py:集中配置的诀窍和典型参数
config.py 的设计思路很朴素——把散落在各处的魔法数字集中到一个文件里。这样做的好处在于:当你换一个数据集或者换一批参数重跑实验时,不需要翻遍每个脚本去找那个写死的数值。
常见配置是这样的:
import os # 路径配置 DATA_ROOT = "data/raw" # 原始PET图像的目录 CROP_OUTPUT_DIR = "data/processed" # 裁剪后图像的输出目录 MODEL_SAVE_PATH = "models/model.pth" # 训练好的模型权重保存位置 # 图像参数 IMAGE_SIZE = 224 # 裁剪后统一缩放到224x224 THRESHOLD_RATIO = 0.35 # 裁剪阈值,与crop.py保持同步 # 训练参数 BATCH_SIZE = 16 EPOCHS = 50 LEARNING_RATE = 1e-4 NUM_CLASSES = 2 # 数据处理 TRAIN_VAL_SPLIT = 0.8 # 训练集占80% SEED = 42路径全部用相对路径或者基于项目根目录拼接的路径,不要写绝对路径,绝对路径换机器就废。SEED这个参数很多人忽略,但它特别重要,固定随机种子后,你和别人跑出来的结果才能对比,否则每次运行结果都飘,你说不清是模型问题还是随机性问题。
训练参数这里,BATCH_SIZE受显存限制,如果你用的是 8GB 显存的卡,224x224 的灰度图批量大小设 32 也能跑,但设 16 更稳。LEARNING_RATE是这批代码里对结果影响最大的一个数,后面训练章节会单独讲。需要提醒的是:如果是初学者,建议先从默认参数跑,把流程跑通后再去动这些参数,别一上来就一顿乱改,最后都不知道是谁造成的。
4. 训练流程详解:1_train.py 里的模型选型与调试
训练脚本是整个资源包里最核心的代码,它把数据加载、模型构建、训练循环、模型保存这四件事串在一起。这一章我们重点讲两件事:代码的逻辑顺序是什么,以及训练过程中你该盯哪些指标。
4.1 训练脚本的骨架和模型选择
从教学资源的角度看,这个训练脚本的模型选型不会太复杂,常见做法是用一个 ResNet 或 VGG 的预训练模型做特征提取,然后把最后一层全连接层替换成二分类输出。这么做有它的道理:脑 PET 图像训练数据量通常不大,动辄几万张的自然图像数据集对医学图像来说是很奢侈的,从头训练一个深层 CNN 极易过拟合;用 ImageNet 预训练模型做迁移学习,相当于模型已经学会通用图像特征了,我们只需要微调后面几层去适应医学图像的分布。
import torch import torch.nn as nn from torchvision import models, transforms from torch.utils.data import Dataset, DataLoader from PIL import Image import os from config import DATA_ROOT, IMAGE_SIZE, BATCH_SIZE, EPOCHS, LEARNING_RATE # 标准图像增强与归一化:PET灰度图转三通道后送入预训练模型 transform = transforms.Compose([ transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) class PetDataset(Dataset): def __init__(self, root_dir, is_train=True, split=0.8, seed=42): self.file_list = [os.path.join(root_dir, f) for f in os.listdir(root_dir) if f.endswith('.png')] # 按固定随机种子切分训练集和验证集 n_train = int(len(self.file_list) * split) # 在实际代码里这里会用随机划分并做好标签映射 self.samples = self.file_list if is_train else self.file_list[n_train:] def __len__(self): return len(self.samples) def __getitem__(self, idx): img = Image.open(self.samples[idx]).convert('RGB') label = 1 if "dementia" in self.samples[idx] else 0 # 标签规则示例 return transform(img), label # 迁移学习:加载预训练ResNet18,替换最后一层为二分类 def build_model(num_classes=2): model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) return model这里的标签规则是示意性的——实际资源包里数据集如果是按子目录区分类别,那__getitem__里会从子目录名推断标签;如果是 CSV 标注格式,则从 CSV 中读标签。你拿到代码后第一件事就是搞清楚标签是以哪种方式关联到图像上的,这一点直接决定数据加载代码是否需要改。
模型选型这里,ResNet18 是这类任务里性价比很高的选择:参数量小,训练快,在医学图像小数据集上不容易过拟合。如果你发现验证集准确率一直上不去,可以尝试换成 ResNet50 或 EfficientNet 看有没有提升,但要注意换模型后学习率可能需要同步调整。
4.2 训练循环、学习率和过拟合的三个信号
训练循环本身是一个标准的监督学习流程:前向传播算损失、反向传播算梯度、优化器更新权重、记录指标。这一部分代码你不用改,但要知道日志里每个指标的含义。
学习率被认为是训练里面最具玄学色彩的超参数。设大了,损失震荡不收敛,准确率像过山车;设小了,Loss 下降跟蜗牛爬一样,五十个 epoch 过去了还跟没训似的。冰冻三尺非一日之寒,为了把这个参数设好,我一般会跑一个小实验探一下路:
from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR optimizer = AdamW(model.parameters(), lr=LEARNING_RATE, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=EPOCHS) for epoch in range(EPOCHS): running_loss = 0.0 for inputs, labels in train_loader: optimizer.zero_grad() outputs = model(inputs) loss = nn.CrossEntropyLoss()(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() scheduler.step() val_acc = evaluate(model, val_loader) print(f"Epoch {epoch+1}/{EPOCHS} | Loss: {running_loss/len(train_loader):.4f} | Val Acc: {val_acc:.4f}")这个代码里weight_decay是另一个容易被忽略但很有用的参数,它做的是 L2 正则化,惩罚过大的权重,是缓解过拟合的基本手段。CosineAnnealingLR是学习率调度器,它让学习率按余弦曲线从初始值下降到接近零,很多情况下能在训练末期给准确率带来一点点额外提升。
训练过程中侵入性比较强的信号有三个。第一个是训练 Loss 一直降、验证 Loss 不降甚至升高——这是过拟合最经典的信号,解决思路是增加数据量或数据增强手段;第二个是训练和验证准确率都非常低——大概率是数据加载标签对不上,模型在乱学关联规则;第三个是训练 Loss 在某个数值附近震荡下不去——学习率太大或者模型容量不够。
另外,强烈建议训练时顺手把每个 epoch 的验证准确率输出到日志文件而不是只输出到控制台,因为你可能要跑十几轮去比不同参数的效果,翻日志比翻终端记录方便得多。跑一次训练,把python 1_train.py >> train_log.txt 2>&1这个习惯建立起来,后面做实验对比能省很多事。
5. 预测的完整流程与常见问题排查:2_predict.py 的四个典型坑
模型训练完成只是走完了一半路,把模型用起来才是延续性关键。预测脚本读入模型权重,对新图像完成和训练时一模一样的预处理流程,然后输出类别和置信度。这一步看似简单,但实际跑起来踩坑的概率最大。这一章我们专门做排查。
5.1 预测脚本的避坑:预处理一致性
预测时最隐蔽的一个坑就是「预测阶段预处理和训练阶段不一致」。训练时做了裁剪和归一化,预测时如果忘了做,输入分布就变了,模型的准确率会断崖式下跌,这种情况通常不会报错,而是默默给出一个错误结果。
import torch from torchvision import transforms from PIL import Image from config import MODEL_SAVE_PATH # 和训练完全相同的预处理管道 transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) def predict_single_image(image_path): model = build_model() model.load_state_dict(torch.load(MODEL_SAVE_PATH, map_location='cpu')) model.eval() # 关键:切换到eval模式,关闭dropout和batch norm统计 img = Image.open(image_path).convert('RGB') inputs = transform(img).unsqueeze(0) with torch.no_grad(): logits = model(inputs) probs = torch.softmax(logits, dim=1) pred_class = torch.argmax(probs, dim=1).item() confidence = probs[0, pred_class].item() return pred_class, confidence注意model.eval()这一行,全篇最容易被忽略的一行。如果遗忘,模型里如果含 dropout 层或 batch normalization 层,它们会继续按训练模式运行,预测结果就不稳定。同一张图每次跑出来概率都不一样的「灵异事件」,八成就是这一行没写。
另一个细节是map_location='cpu',这是给没有 GPU 的机器准备的。如果训练在 GPU 上跑,你自己测试时不一定有同型号的卡,这个参数让模型权重能加载到 CPU 上进行推理。
5.2 四类高频问题的现象、原因与解决路径
我在跑类似项目时遇到过的、以及后台经常被问到的四类问题,按「现象 → 原因 → 解决」的格式写在下面,这些就是这份资源落地时的血泪经验。
问题一:运行 1_train.py 报错ModuleNotFoundError: No module named 'torch'
现象:脚本启动即崩,提示找不到 torch 或其他依赖库。原因:直接由缺少依赖引起,但深层原因是安装依赖的 Python 环境和你运行脚本的不是同一个,最常见的是用系统自带的 python 命令去跑一个在虚拟环境里装的依赖。解决:先确认自己是不是在正确环境里——用which python看看当前解释器路径,再用pip install -r requirements.txt重装一遍所有依赖。以 PyTorch 为例,如果机器有 NVIDIA 显卡且需要 GPU 支持,推荐到 PyTorch 官网按 CUDA 版本选择对应的安装命令,而不是直接pip install torch装到 CPU 版。装完再验证:
python -c "import torch; print(torch.__version__)"问题二:图像裁剪后数据集是空的,或者裁剪结果全是黑的
现象:运行 crop.py 后输出目录为空,或者生成的图像是全黑的,看不到任何结构。原因:阈值设置得太高,整个脑区都被判为背景。另外,很多医学影像的像素值范围不是 0-255,有的 PET 数据是浮点数且值范围较大,直接像素值比较可能出问题。解决:先打印一下原始数据的像素值分布情况——最大值、最小值、均值、95 百分位数,再针对这个分布去设定阈值。做个中间结果可视化是排查这类问题最靠谱的方式:
import numpy as np import matplotlib.pyplot as plt # 打印像素值分布特征 slice_data = load_nii("data/raw/sample.nii").get_fdata()[:, :, 20] # 随便取一层 print(f"min={slice_data.min():.2f}, max={slice_data.max():.2f}, mean={slice_data.mean():.2f}") plt.imshow(slice_data, cmap='gray') plt.show()建议看一遍几十层切片,确认哪个层面最合适分析,再来调裁剪参数。
问题三:训练时 Loss 下降但验证准确率一直徘徊在 50% 附近(二分类任务的随机水平)
现象:训练 Loss 正常下降到了 0.3 以下,但验证准确率怎么都上不去。原因:数据划分可能出现了类别不均衡或者标签泄露等结构性问题,比如训练集和验证集来自同一受试者的不同切片——同一人的数据同时出现在训练集和验证集,模型等于提前见到了答案,泛化能力就是差的,但这一般会导致验证集虚高。方向反过来更常见:验证集和训练集分布不一致,比如不同扫描仪产生的数据被放到同一个数据集上。解决:对画像数据按受试者维度来切分——同一个人所有切片要么全进训练集,要么全进验证集,不能让切片级别混在一起;清洗验证集中不清晰的伪影图像。
问题四:预测结果和预期完全相反,比如健康人判断为患病
现象:模型训练时验证准确率有 90%,但拿到一张新图预测时结果离谱。原因:最可能是预处理不一致——新图没经过裁剪或者用了不同的 resize 参数。如果训练图是裁剪后缩放到 224,预测时直接拿原图 resize,输入的占比区域就完全不同。解决:把预测阶段的图像处理步骤完完整整地复现训练阶段的操作,包括裁剪、灰度归一化、resize,保证中间结果可视化后和训练集图像风格一致。
要声明的一点是,这个诊疗流程并不能避开标签错误或者标注主观性的问题,医学数据集的标注质量本身就需要花时间审查。如果发现训练集的标签本身就有一批存疑的,把它挑出来重新确认,让模型学习一个本来就有错的答案去获得高分数,是不值得的。
6. 进阶:从跑通到可信——离线验证、阈值调整与模型不可知分析
如果你已经成功跑通了整个流程,接下来值得花时间思考的是「怎么让结果更可靠」。这一章我们谈三个具体的进阶技巧,它们不需要改动架构,但能切切实实提升实验的说服力。
第一个值得做的是把「单一划分」改成「交叉验证」。训练脚本里的TRAIN_VAL_SPLIT默认是 0.8,这意味结果与数据如何切分有很大关系。尤其在小数据集上,某一次划分运气好验证集都是清晰的图,准确率就好看;运气差验证集里混入几张低质量图像,准确率就往下掉。如果数据集规模允许,建议做一个 5 折交叉验证——把数据分成五份,轮流留一份做验证,五次结果的平均值和方差才是更有说服力的指标。具体上手动改成KFold逻辑,代价很小但能让你对模型的稳定性心里有数。
第二个技巧是调整分类阈值。默认情况下argmax把输出超过 0.5 判为正类,但在医学筛查场景里,漏诊和误诊的代价是完全不同的。预测时模型的softmax输出是一个连续的概率分数,它告诉你模型把握多大,而不仅是「是或不是」。实操层面,在验证集上画出 ROC 曲线,选定一个让特异性和敏感性平衡的阈值,预测时用这个阈值替代默认的 0.5,在压线场景下往往能改善实际表现。
第三个是做一个简单的「模型不可知分析」——拿几张被正确分类和错误分类的 PET 图出来,排在一起看。用深度学习做医学图像分析,模型像一个黑匣子,但它在判别的时候可能依赖的是图像中你不希望它关注的区域,比如扫描床边缘的伪影或图像角标水印。把这些图可视化检查一遍,能帮你避开「模型学到了错误特征」的陷阱。具体操作就是在predict.py的基础上,让模型输出验证集中每张图的预测概率,按置信度从低到高排序,取头尾各十张图做一次主观审查。如果模型高置信度判错的图有共同视觉特征,那问题一大半在数据,而不是在模型。
当初我跑类似的医学图像项目时,也曾经对着一张高置信度但预测错误的图像愣了半天,检查之后才发现是预处理阶段漏了一步归一化。从那以后,每次做预测流程,我都强制自己先在训练集上抽十张图跑一遍验证,确认预处理一致后再上新数据。这份资源给你的是一个起点,剩下的质量把控,还是要靠你自己多动手试、多翻日志、多看中间结果,希望帮到你。
本文还有配套的精品资源,点击获取