尧图精选

病理图像深度学习工程实践:基于PyTorch的WSI切片分类

🕒 发布时间:2026/9/1 0:35:06 📁 来源:尧图网络
简介本资源是一套面向生物信息学与医学图像分析方向研究者的Python深度学习实践代码包聚焦于组织病理学图像与空间转录组数据的跨模态建模任务解决HE染色切片到基因表达谱的预测难题。资源共40个文件以33个Python脚本为核心涵盖数据预处理、模型训练、可视化及交叉验证等全流程辅以2个配置文件.cfg、1个Shell脚本TIFF格式转换、1个README说明文档及若干开发辅助文件如.pylintrc、.gitignore整体压缩包仅64KB轻量但结构完整。已有711人学习下载适合具备基础PyTorch与图像处理能力的中高级用户快速复现ST-Net模型。读者可直接运行prepare spatial命令缓存空间计数与肿瘤标签调用create_tifs.sh批量生成分块TIFF再通过main.py端到端训练模型并借助visualize.py、plot_model.py等模块完成结果解析与图表生成配套example.cfg支持灵活路径配置显著降低环境部署门槛。 组织病理学图像深度学习项目简单来说就是用Python和深度学习模型分析医院病理科扫描出来的组织切片自动判断组织是良性还是恶性、有没有肿瘤浸润甚至进一步做癌变分级和细胞检测。这个方向最近临床和科研都缺人难点不在模型结构上而在数据处理和工程落地这一圈。我第一次拿到病理切片数据的时候心态比较天真以为把图像缩一缩扔进一个训练好的CNN就行了。结果打开一份svs文件就沉默了一张几GB的全切片图缩略图就已经几千万像素直接训练显存绝对爆掉。后来我才真正意识到这个方向真正核心的代码在“如何把一张巨幅切片变成模型能吃的样本”然后才是网络结构和训练策略。这篇文章记录的就是我从零开始搭建的一个最小可运行的病理图像深度学习工程骨架代码全部用Python实现PyTorch做训练框架下载后可以直接改参数复现。从结果看这个工程既能做patch级别的良恶性分类也可以作为后续弱监督分割、多实例学习的基础。如果你正在做病理AI、医学图像分析方向或者需要一个可运行的baseline来快速验证数据质量本文的内容应该能帮你省下至少一两天搭环境的时间。1. 项目定位先想清楚病理图像深度学习要解决什么问题1.1 病理图像和自然图像有哪些本质差异在写代码之前先要搞清楚为什么不能照搬通用图像分类方案。组织病理学图像和自然图像相比有四个非常明显的特点直接影响技术选型。首先是一张切片图的尺寸极其夸张。医院常用的全切片图像Whole Slide Image简称WSI分辨率动辄十万乘十万像素很多中心扫描出来的文件几个GB甚至十几个GB显卡根本不可能一次性读入。代码里必须先做切片采样把WSI拆成小patch再进入训练流程。其次是染色差异。不同医院、不同批次、不同染液浓度的切片颜色深浅差别非常大。同一个模型在A医院的切片上表现很好换到B医院可能直接掉十几个点。这是病理图像里面最经典的域偏移问题后面数据增强部分必须处理。第三个问题是样本不平衡。一张完整切片里恶性肿瘤往往只占很小一部分区域绝大多数区域是正常组织或者空白背景。如果把整张切片扫出来做patch级分类阴性样本可能是阳性样本的几十倍训练出来的模型很容易“偷懒”把所有patch都预测为阴性。最后是标签成本高。每个patch的真实类别需要病理医生逐块标注时间成本和人力成本都很高数据集通常比自然图像数据集小得多。这也是“用ImageNet预训练权重做初始化”比“从头开始训练”更值得做的主要原因。1.2 为什么这套方案最终选择Python PyTorch组织病理学图像深度学习在工程实现层面基本绕不开Python。不是Python本身性能有多强而是整个医学图像生态都长在它上面换了语言就得不到这些现成的东西。OpenSlide是读取WSI最主流的库它可以读svs、ndpi、mirax等几乎所有病理切片格式。虽然底层是C但Python封装用起来非常顺手几行代码就能拿到指定位置的图像块。在数据预处理阶段OpenSlide几乎是必选项。深度学习框架我选了PyTorch理由是病理AI社区现在大多用PyTorch。医学图像领域最新的论文、最常用的预训练模型、一些开源的病理分析工具箱都是PyTorch版本优先。训练调试时PyTorch的动态图和灵活的hooks机制也方便观察中间特征对反复实验很有用。另外配套的还有opencv、numpy、pandas、albumentations负责图像读写、数据组织和数据增强。这套组合的好处是代码量少、迭代快适合一个研究型工程快速跑起来。我在项目里用的环境是Python 3.8 PyTorch 1.12 CUDA 11.3显卡是RTX 3090 24G。如果你显卡显存小一点也完全能跑后面第4节会讲怎么降内存。1.3 任务选型第一版先做Patch级分类很多新手一上来就直奔全切片分割这是我在这个项目里踩过的第一个认知坑。分割任务需要像素级的标签标注成本极高训练难度、显存占用、后处理复杂度都会明显上升。在数据不充裕、标签不完整的情况下直接上分割很容易陷入“调了无数个版本结果还是不如预期”的状态。我给的建议是第一版代码先做Patch级二分类也就是先判断每个256x256或者512x512的小图像块是不是肿瘤区域。这个任务足够简单能快速验证数据质量、染色差异和baseline指标。等整个流程跑通AUC曲线出来了再根据业务需求向分割或弱监督方向扩展。模型架构方面我用ResNet34作为骨干。ResNet34在病理patch分类里是一个性价比很高的选择比ResNet50参数量小、训练快在小数据集上泛化反而更好。EfficientNet和Vision Transformer确实性能上限更高但baseline阶段不建议一上来就跟风换大模型先把训练链路跑通更重要。后面第3节会给出对应的代码和替换思路。2. 环境准备与病理切片预处理最花时间的环节2.1 依赖安装PyTorch和OpenSlide的坑环境安装看起来简单实际容易卡住。我这里把安装步骤整理成可以直接复制执行的方式。创建conda环境并安装PyTorch GPU版conda create -n patho python3.8 conda activate patho pip install torch1.12.1cu113 torchvision0.13.1cu113 -f https://download.pytorch.org/whl/torch_stable.html接着安装配套依赖pip install openslide-python opencv-python numpy pandas tqdm albumentations scikit-learn matplotlib这里要特别提醒一个坑pip安装的openslide-python只是Python绑定底层C库需要单独装。Linux系统下比如Ubuntu需要执行sudo apt-get install libopenslide0Windows系统则要去OpenSlide官网下载预编译的OpenSlide包把解压目录加入环境变量否则代码跑起来会一直报“找不到OpenSlide”或者“unable to load openslide”之类的错误。这个问题非常常见很多人以为pip安装成功就完事了结果一读图片就报错。另外如果PyTorch版本和CUDA版本对不上训练时会出现“Torch not compiled with CUDA enabled”这时候大概率是装了CPU版的torch。建议安装前确认一下GPU驱动支持的CUDA版本再选择对应的PyTorch wheel。2.2 WSI切Patch把全切片图像变成训练样本整个预处理阶段最核心的代码是把一张巨型WSI切成小块小块的patch。这里我给出一个可以直接跑的切分脚本它会在低倍率缩略图上标出组织区域再只在组织区域采样避免把大量白色背景切进去。import openslide import cv2 import numpy as np import os from tqdm import tqdm def save_patches_from_wsi(slide_path, output_dir, patch_size256, bg_threshold0.8): slide openslide.OpenSlide(slide_path) w, h slide.dimensions slide_id os.path.splitext(os.path.basename(slide_path))[0] patch_dir os.path.join(output_dir, slide_id) os.makedirs(patch_dir, exist_okTrue) # 在低倍率缩略图上做背景过滤 slide_thumbnail slide.get_thumbnail((w // 8, h // 8)) thumb_np np.array(slide_thumbnail.convert(RGB)) thumb_gray cv2.cvtColor(thumb_np, cv2.COLOR_RGB2GRAY) _, binary cv2.threshold(thumb_gray, 220, 255, cv2.THRESH_BINARY_INV) tile_size patch_size stride patch_size scale_x w / thumb_np.shape[1] scale_y h / thumb_np.shape[0] patch_list [] for y in range(0, binary.shape[0], tile_size // 8): for x in range(0, binary.shape[1], tile_size // 8): # 判断这个低倍率小块是否是组织区域 b binary[y:y tile_size // 8, x:x tile_size // 8] if b.size 0: continue if np.mean(b) 255 * (1 - bg_threshold): continue # 换算到原分辨率坐标并读取patch origin_x int(x * scale_x) origin_y int(y * scale_y) size_x min(patch_size, w - origin_x) size_y min(patch_size, h - origin_y) if size_x patch_size or size_y patch_size: continue patch slide.read_region((origin_x, origin_y), 0, (patch_size, patch_size)) patch_rgb np.array(patch.convert(RGB)) patch_path os.path.join(patch_dir, f{origin_x}_{origin_y}.png) cv2.imwrite(patch_path, cv2.cvtColor(patch_rgb, cv2.COLOR_RGB2BGR)) patch_list.append(patch_path) return patch_list为什么要先在低倍率缩略图上计算前景掩膜因为如果直接在原始分辨率上逐块采样并判断背景每次都要读一大块高分辨率数据不仅慢还非常耗IO。缩略图上先算组织区域然后只对前景区域按原分辨率采样速度能快很多倍数据处理时间可以缩短到原来的五分之一左右。实际使用中关于patch_size的选择我建议先统一用256x256。patch越小能保留的位置信息越精细但显存占用会因样本数量增加而变大patch越大推理速度越快但需要的显存也越大而且模型很难捕捉局部细节。先用256跑通再慢慢加大看指标变化。2.3 样本组织与Dataset加载训练时不需要把整个数据集读进内存只需要保存patch的路径和标签。我建议把样本信息统一放在一个CSV文件里格式如下字段含义patch_pathpatch文件的相对路径label标签0代表良性1代表恶性slide_id来源切片编号split划分train/val/testCSV文件可以用pandas生成也可以手动构建。有了CSV之后PyTorch的Dataset就很好写了。import cv2 import pandas as pd from torch.utils.data import Dataset class PatchDataset(Dataset): def __init__(self, df, transformNone): self.df df self.transform transform def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img cv2.imread(row[patch_path]) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) if self.transform: img self.transform(imageimg)[image] label int(row[label]) return img, label注意在读取图像时一定要把BGR转换成RGB。OpenCV读出来的是BGR通道顺序直接塞进PyTorch预训练模型颜色通道会被反转模型的性能会莫名其妙下降。2.4 数据增强染色是最大的变量通用数据增强翻转、旋转、缩放、裁剪都适合病理图像。病理场景的特殊点在于染色差异不同切片之间的颜色变化非常大所以我在增强管线里加入了颜色扰动。import albumentations as A train_transform A.Compose([ A.RandomResizedCrop(224, 224, scale(0.8, 1.0)), A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomBrightnessContrast(brightness_limit0.15, contrast_limit0.15, p0.3), A.HueSaturationValue(hue_shift_limit10, sat_shift_limit20, val_shift_limit20, p0.5), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ]) val_transform A.Compose([ A.Resize(224, 224), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ])这里使用ImageNet的均值和标准差做标准化是为了匹配预训练权重初始时的数据分布。如果完全不用预训练权重用自算的均值和标准差也完全可以但从实际经验看ImageNet预训练权重的收益非常明显。有一点需要注意增强强度不要开太大。病理图像的组织结构有方向性过强的缩放、旋转或形变会破坏组织结构模型反而不容易学到真实特征。我在实际项目里旋转角度一般不超过90度缩放范围控制在0.8到1.0之间效果比较稳。3. 核心模型代码实现与训练流程3.1 模型结构一个改了两行的ResNet34模型架构部分我直接用torchvision提供的ResNet34把最后一层全连接替换成二分类输出就行。这个思路简单但实际效果好尤其适合数据量不大的场景。import torch import torch.nn as nn import torchvision.models as models class PatchClassifier(nn.Module): def __init__(self, num_classes2, frozen_stages2): super().__init__() self.backbone models.resnet34(weightsmodels.ResNet34_Weights.IMAGENET1K_V1) count 0 for name, module in self.backbone.named_children(): if count frozen_stages: break for p in module.parameters(): p.requires_grad False count 1 in_features self.backbone.fc.in_features self.backbone.fc nn.Linear(in_features, num_classes) def forward(self, x): return self.backbone(x)如果你安装的torchvision版本是0.13的上面的weights参数可能不识别可以把代码改成models.resnet34(pretrainedTrue)。两种写法本质一样都是加载ImageNet预训练权重。关于frozen_stages这个参数它表示前几个stage的权重不更新。在小数据集上前两层学到的是非常基本的边缘、纹理特征这些特征和ImageNet上学的差不多冻结它们能让训练更快也能降低过拟合风险。等模型在验证集上的指标提升不明显时再解冻全部层做完整微调。这个“两阶段训练”的方法是处理医学小数据集的经典策略。3.2 训练循环和关键参数解释训练循环我直接给一个定义了完整epoch的函数。在病理图像这种大样本场景下我建议训练时使用混合精度速度和显存都有明显改善。def train_one_epoch(model, loader, criterion, optimizer, scaler): model.train() total_loss 0 correct 0 total 0 for images, labels in loader: images images.cuda(non_blockingTrue) labels labels.cuda(non_blockingTrue) optimizer.zero_grad() with torch.cuda.amp.autocast(): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() total_loss loss.item() * images.size(0) preds outputs.argmax(dim1) correct (preds labels).sum().item() total labels.size(0) return total_loss / total, correct / total优化器我推荐AdamW学习率设置3e-4weight_decay设置1e-4。如果数据集特别小学习率可以降到1e-4。相比SGDAdamW对初学阶段更友好不用花太多时间调学习率SGD后期调优效果好但前期收敛慢不适合快速迭代。训练过程的损失函数用CrossEntropyLoss。这里有个关键的类别不平衡问题。如果Patch数据中恶性样本占比很低直接在原始分布上训练会导致模型严重偏置。常用的解决办法是在DataLoader里用WeightedRandomSampler给少数类加大采样权重from torch.utils.data import WeightedRandomSampler labels_count df[label].value_counts() weights [1.0 / labels_count[label] for label in df[label]] sampler WeightedRandomSampler(weights, num_sampleslen(weights), replacementTrue)使用加权采样后每个batch里良性和恶性的比例会均衡很多模型的recall会在不牺牲太多precision的情况下明显提升。3.3 评估指标病理场景不能只看准确率病理图像分类有一个很容易犯的错误只看准确率。在类别极不平衡的情况下模型只要全部预测为阴性准确率就能超过90%看起来好看实际毫无用处。所以我的评估指标以ROC-AUC和敏感性/特异性为主再加一个混淆矩阵辅助分析。验证集评估代码from sklearn.metrics import roc_auc_score, confusion_matrix def evaluate(model, loader): model.eval() all_labels [] all_probs [] with torch.no_grad(): for images, labels in loader: images images.cuda() outputs model(images) probs torch.softmax(outputs, dim1)[:, 1].cpu().numpy() all_probs.extend(probs) all_labels.extend(labels.numpy()) auc roc_auc_score(all_labels, all_probs) tn, fp, fn, tp confusion_matrix(all_labels, [1 if p 0.5 else 0 for p in all_probs]).ravel() sensitivity tp / (tp fn 1e-8) specificity tn / (tn fp 1e-8) return auc, sensitivity, specificity训练过程中每轮epoch结束都记录这几个指标观察验证集AUC的变化趋势。正常的训练曲线应该是训练loss持续下降验证AUC逐步上升最后趋于平稳。如果验证AUC突然下跌说明模型开始过拟合需要提前停止训练或者增加正则化。4. 实操记录训练参数、踩坑与调参心得4.1 参数配置与实验依据我从实际实验里整理了一份比较稳定的参数配置可以直接作为不同显存条件下的起点。参数推荐值说明patch_size256显存小用224精度可以再尝试512batch_size3224G显存可设328G显存建议8base_lr3e-4AdamW默认学习率weight_decay1e-4防止过拟合epochs20看验证集AUC早停optimizerAdamW比SGD好调lossCrossEntropyLoss配合WeightedRandomSampler这个配置对应的是单个二分类任务。如果你改成多分类比如癌变分级Grade 1/2/3可以保存同样的结构只改num_classes和第3节的损失函数部分。4.2 染色归一化和数据增强的取舍染色差异是整个项目中影响最隐蔽的因素。我一开始只加了通用颜色增强模型在原始测试集上AUC能到0.93但换了一批来自不同医院的切片后AUC直接掉到0.81这就是染色差异导致的域偏移。后来我在预处理阶段加了一个简单的染色归一化对每个patch的RGB通道做标准化使其均值和方差对齐到训练集的总体分布。这个操作不需要复杂的颜色空间转换但效果不错可以明显提升跨中心的稳定性。当然如果数据来自同一家医院不做染色归一化影响不大。从增强策略上看我最终保留的增强组合是随机裁剪、水平翻转、垂直翻转、亮度对比度扰动、HSV扰动。没有用很激进的光学变形因为组织结构在临床上是有方向意义的过度扭曲反而会误导模型。这个平衡需要反复实验不同数据集的敏感点不一样。4.3 显存不够怎么办混合精度与更小的patch很多读者可能没有24G显存的高端显卡。如果你遇到CUDA out of memory按照下面的顺序优化。第一步是开启混合精度训练我前面代码里的torch.cuda.amp已经默认开启。混合精度会把模型中的部分算子用FP16计算显存占用能减少大约40%速度还能提升30%左右。第二步是调小patch_size。从256降到224显存占用减少大概20%对精度的影响通常很小。最低可以降到192但不建议再低了否则patch里的组织形态可能不够完整模型很难学到有区分度的特征。第三步是调小batch_size。如果batch_size从32降到16显存占用减半但训练时梯度噪声变大可能要稍微加大学习率来补偿。另一个可选操作是把输入图像尺寸降为128但这样会丢失细节只建议在数据本身分辨率较高、使用弱监督标注的场景下尝试。5. 常见问题与排查技巧遇到报错先查这里5.1 高频报错速查表我把整个流程中经常遇到的报错整理成了一张表遇到问题时先对照查一遍。报错信息可能原因解决办法OpenSlide file not found底层库未安装Linux安装libopenslide0Windows安装OpenSlide预编译包Torch not compiled with CUDA enabled安装了CPU版PyTorch重新安装GPU版确认CUDA版本CUDA out of memory显存不足减小batch_size开启混合精度降低patch尺寸No module named dataset项目路径不对在项目根目录运行程序或设置PYTHONPATH训练loss一直不变学习率太大或太小调低或调高base_lr检查数据标签是否正确验证集AUC接近0.5标签出错或采样策略异常优先检查CSV标签和patch路径对应关系启动失败代码2程序缺少依赖或路径问题按报错顺序检查依赖库和系统路径这些报错里最容易被忽略的是“启动失败代码2”。我遇到过两次一次是Windows下OpenSlide环境变量没配好另一次是项目路径里包含中文导致路径解析异常。遇到这个错误先检查路径和依赖不要盲目去改代码。5.2 模型效果差时按什么顺序排查模型训练完如果发现效果不理想我一般不会马上换模型而是按照下面这个顺序排查。第一步看训练集的loss能否下降。如果训练loss一直卡在某个值附近不掉说明模型欠拟合问题大概率是学习率设置不合适、数据标签混乱或者模型结构有问题。如果训练loss能降但验证集AUC上不去那要么是过拟合要么是数据集分布差异太大。第二步看patch质量。我在项目早期换了很多模型结构AUC都上不去后来发现是切patch时背景过滤阈值设低了很多白色背景块被当成组织送进了模型。这类数据在训练时会让模型学到“有白色边界的都是阳性”这种错误特征表现就是验证集上规律性出错。第三步查看预测错误的样本。把验证集上预测错的patch单独挑出来批量存成图片看一眼。这一步实际做下来经常能发现染色异常、部分组织碎片、盖章划痕等情况。数据问题解决了模型往往很快就能涨点。6. 代码下载、项目结构与使用方式6.1 完整项目文件结构整个工程代码我整理成了下面这个结构下载解压后按顺序运行两个脚本就能完成训练。patho_dl/ ├── data/ │ ├── patches/ │ ├── train.csv │ └── val.csv ├── src/ │ ├── config.py │ ├── dataset.py │ ├── model.py │ ├── train.py │ ├── evaluate.py │ └── utils.py ├── checkpoints/ └── README.mdconfig.py里集中存放所有可调参数包括patch_size、batch_size、learning_rate、epochs、数据路径和模型保存路径。我习惯把所有参数集中在一个文件里这样每次实验不用改多个地方的代码改掉一个config就能跑一组新实验。6.2 从0到1运行流程拿到代码后按照下面步骤操作就能跑通整个流程。第一步把WSI数据放到data目录下可以用任意标准的病理切片格式svs、ndpi都可以。第二步运行预处理脚本将WSI切成patch并生成CSV标注文件。首次运行建议先用一张切片测试确认生成的patch数量和路径没问题再全量处理。第三步修改config.py里的数据路径和训练参数运行train.py开始训练。训练过程中终端会打印每个epoch的train loss、验证集AUC、敏感性、特异性同时在checkpoints目录下保存最优模型。第四步运行evaluate.py在测试集上输出最终指标和混淆矩阵。如果要做推理predict.py会加载保存的模型对新的切片或patch输出预测概率。我在使用工程时有个习惯每调整一次数据预处理或增强策略都会重新更新CSV文件里的patch列表并保留一个训练日志记录每个版本的AUC。这样整个迭代过程有章可循出了效果波动能快速定位到是数据、增强还是模型参数的问题。最后再分享一个个人体会组织病理学图像深度学习看起来是深度学习算法问题真正做下来会发现模型是最好解决的部分数据预处理、组织区域筛选、染色归一化和合理的训练策略反而决定了项目的上限。把这个最小工程跑通之后后面无论是换成Transformer模型还是加入多实例学习做全切片预测都有了稳固的基地。你在复现过程中如果遇到我上面没提到的怪问题建议先从数据路径和标签对应关系查起那是我踩坑最多的地方大概率也是你会踩坑的地方。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →