神经网络实战:ResNet 医学影像分类全流程解析
前言在医学影像领域处理高分辨率图像往往耗时耗力。本次项目采用 MedMNIST 风格的简化数据集即28×28像素的小尺寸医学图像重点完成医学影像的多分类任务并拆解深度学习中非常经典的网络结构——ResNet也就是深度残差网络。一、环境准备与数据加载医学数据集通常包含多种类别例如结肠癌切片、皮肤病变、肺部 X 光等。由于不同数据集的类别数、通道数和样本数量可能不同因此项目中通过一个 JSON 配置文件来统一管理数据集的基本信息。1.1 数据预处理代码在dataset.py中我们需要定义数据的读取方式以及标准化操作。import torch from torch.utils.data import Dataset from torchvision import transforms class MedicalDataset(Dataset): def __init__(self, data_array, labels, transformNone): self.data data_array self.labels labels self.transform transform def __getitem__(self, index): # 1. 提取图像和标签 img, target self.data[index], self.labels[index] # 2. 预处理ToTensor 转为 TensorNormalize 进行标准化 if self.transform is not None: img self.transform(img) return img, target def __len__(self): return len(self.data) # 定义预处理流程 data_transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize( mean[0.5, 0.5, 0.5], std[0.5, 0.5, 0.5] ) ])这里需要注意如果当前数据集是三通道 RGB 图像可以使用mean[0.5, 0.5, 0.5] std[0.5, 0.5, 0.5]如果是单通道灰度图像则应改为mean[0.5] std[0.5]二、核心原理残差块 BasicBlockResNet 的核心思想是解决深层网络的退化问题。普通网络层数不断加深后训练效果不一定变好甚至可能变差。ResNet 通过 Shortcut Connection也就是短路连接让输入信息可以直接传到后面从而缓解这个问题。简单来说残差结构可以表示为out F(x) x其中F(x)表示卷积层学习到的特征x表示原始输入。这样一来如果新增层学得好就能提升效果如果新增层学得不好shortcut 也能保留原始信息避免模型效果明显下降。2.1 BasicBlock 代码实现下面是 ResNet 的最小构建单元 BasicBlock。import torch.nn as nn class BasicBlock(nn.Module): def __init__(self, in_channels, out_channels, stride1): super(BasicBlock, self).__init__() # 第一层卷积负责特征提取或降采样 self.conv1 nn.Conv2d( in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse ) self.bn1 nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) # 第二层卷积 self.conv2 nn.Conv2d( out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse ) self.bn2 nn.BatchNorm2d(out_channels) # Shortcut 路径 self.shortcut nn.Sequential() # 如果维度不匹配需要用 1×1 卷积调整 if stride ! 1 or in_channels ! out_channels: self.shortcut nn.Sequential( nn.Conv2d( in_channels, out_channels, kernel_size1, stridestride, biasFalse ), nn.BatchNorm2d(out_channels) ) def forward(self, x): identity x out self.conv1(x) out self.bn1(out) out self.relu(out) out self.conv2(out) out self.bn2(out) # 核心步骤主分支输出与 shortcut 分支相加 out self.shortcut(identity) out self.relu(out) return out这里最关键的是 shortcut 路径。如果输入和输出维度一致shortcut 不需要做任何操作直接相加即可如果输入和输出维度不一致例如通道数变化或特征图尺寸变化就需要使用1×1卷积进行调整。可以简单理解为shape 一样直接相加shape 不一样先用1×1卷积调整后再相加。三、搭建 ResNet 网络架构通过_make_layer函数我们可以像搭积木一样重复使用 BasicBlock从而搭建完整的 ResNet 网络。3.1 完整网络代码class ResNet(nn.Module): def __init__(self, block, num_blocks, num_classes9, input_channels3): super(ResNet, self).__init__() self.in_channels 64 # 第一步初始卷积层 self.conv1 nn.Conv2d( input_channels, 64, kernel_size3, stride1, padding1, biasFalse ) self.bn1 nn.BatchNorm2d(64) self.relu nn.ReLU(inplaceTrue) # 第二步堆叠 4 个 layer self.layer1 self._make_layer(block, 64, num_blocks[0], stride1) self.layer2 self._make_layer(block, 128, num_blocks[1], stride2) self.layer3 self._make_layer(block, 256, num_blocks[2], stride2) self.layer4 self._make_layer(block, 512, num_blocks[3], stride2) # 第三步平均池化 全连接分类 self.avgpool nn.AdaptiveAvgPool2d((1, 1)) self.fc nn.Linear(512, num_classes) def _make_layer(self, block, out_channels, num_blocks, stride): # 第一个 block 可能需要降采样其余 block 保持 stride1 strides [stride] [1] * (num_blocks - 1) layers [] for s in strides: layers.append(block(self.in_channels, out_channels, s)) self.in_channels out_channels return nn.Sequential(*layers) def forward(self, x): # 初始输入[Batch, 3, 28, 28] out self.relu(self.bn1(self.conv1(x))) out self.layer1(out) # [Batch, 64, 28, 28] out self.layer2(out) # [Batch, 128, 14, 14] out self.layer3(out) # [Batch, 256, 7, 7] out self.layer4(out) # [Batch, 512, 4, 4] out self.avgpool(out) # [Batch, 512, 1, 1] out out.view(out.size(0), -1) # [Batch, 512] out self.fc(out) # [Batch, num_classes] return out def resnet18(num_classes, input_channels): return ResNet(BasicBlock, [2, 2, 2, 2], num_classes, input_channels)这里的[2, 2, 2, 2]表示 ResNet-18 中四个 layer 分别包含 2 个 BasicBlock。整体 shape 变化可以理解为[Batch, 3, 28, 28] → [Batch, 64, 28, 28] → [Batch, 128, 14, 14] → [Batch, 256, 7, 7] → [Batch, 512, 4, 4] → [Batch, 512] → [Batch, num_classes]随着网络逐渐加深特征图的通道数不断增加高和宽逐渐减小模型提取到的特征也越来越抽象。四、训练与评估最后将数据喂入模型完成前向传播、损失计算和反向传播。4.1 训练循环代码# 初始化模型、优化器和损失函数 model resnet18(num_classes9, input_channels3).to(device) optimizer torch.optim.Adam(model.parameters(), lr0.001) criterion nn.CrossEntropyLoss() def train(model, train_loader, optimizer, criterion, epoch): model.train() for batch_idx, (inputs, targets) in enumerate(train_loader): inputs, targets inputs.to(device), targets.to(device).long() # 1. 梯度清零 optimizer.zero_grad() # 2. 前向传播 outputs model(inputs) # 3. 计算损失 loss criterion(outputs, targets) # 4. 反向传播 loss.backward() # 5. 更新参数 optimizer.step() if batch_idx % 10 0: print( fEpoch: {epoch} | fBatch: {batch_idx} | fLoss: {loss.item():.4f} ) # 训练启动 for epoch in range(1, 101): train(model, train_loader, optimizer, criterion, epoch)训练流程可以概括为读取 batch 数据 → 输入模型 → 得到预测结果 → 计算交叉熵损失 → 反向传播 → 更新参数验证阶段和训练阶段类似但不需要反向传播和参数更新只需要计算 loss、accuracy、AUC 等指标。五、文章总结本项目基于简化版医学影像数据集完成了一个 ResNet-18 图像分类流程。通过将医学图像统一处理为28×28可以降低训练成本方便快速跑通整个深度学习实验。ResNet 的核心在于残差连接。它通过 shortcut 保留原始输入信息从而缓解深层网络退化问题。尤其是在 layer 切换时如果输入输出维度不一致代码中会通过stride2进行下采样同时利用1×1卷积在 shortcut 路径上调整通道数和特征图尺寸保证两条分支可以正常相加。在实际调试时建议在forward()中打印每一层的out.shape观察张量从[Batch, 3, 28, 28]逐步变为[Batch, 512]特征向量再变为[Batch, num_classes]分类结果的过程。通过以上流程我们完成了从医学图像输入、ResNet 特征提取到最终分类输出的完整实战项目。这个项目的重点不在于追求最高准确率而在于理解 ResNet 的网络结构、shortcut 的作用以及深度学习分类任务的完整训练流程。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2575647.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!