ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PyTorch从零实现U-Net图像分割:编码器-解码器与跳跃连接实战

PyTorch从零实现U-Net图像分割:编码器-解码器与跳跃连接实战 在图像分割任务中单纯的分类网络往往“认得出来却画不出来”。比如给模型一张街景图分类网络能告诉你“图里有机动车、行人、建筑”却无法告诉你“机动车在哪个像素范围、行人在哪一片区域”。当业务需求从“这张图里有什么”变成“把这个物体的轮廓精确抠出来”时就需要 U-Net 这类像素级分割网络登场了。本文将以 PyTorch 为基础框架从零实现一个经典的 U-Net 图像分割模型并配有一套完整的合成数据集用于训练和验证。无论你是刚接触 PyTorch 入门的新手还是已经在分类任务上有一定经验、希望向分割方向拓展的开发者都可以跟着本文的步骤亲手完成“数据生成 → 模型搭建 → 训练验证 → 推理可视化”的完整链路。1. 图像分割任务与 U-Net 的核心思想1.1 图像分割在解决什么问题图像分割是计算机视觉中比分类和检测更精细的任务。它的输出不是类别标签也不是目标框而是一张和原图尺寸相同或略小的掩码图掩码图中的每个像素都对应一个类别标签。按精细化程度图像分割通常可以分成三类类型输出内容典型场景语义分割给每个像素分配一个类别不区分同一类别的不同个体自动驾驶道路区域分割、医学图像中的器官分割实例分割给每个像素分配类别同时区分同一类别的不同个体细胞计数、行人实例分割全景分割同时处理“stuff”背景类和“thing”前景实例类城市街景理解U-Net 最初是在医学图像分割任务中提出的后来被广泛应用到遥感影像分割、工业质检、广告牌图像分割系统等场景中。它的优势在于标注数据少时仍能获得不错的效果编码器-解码器结构非常容易扩展到新的数据集跳跃连接的设计让浅层空间细节和深层语义信息能够融合输出轮廓更精细。1.2 U-Net 架构的编码器-解码器与跳跃连接U-Net 的网络结构可以从名字上直观理解左侧是一条收缩路径右侧是一条扩张路径整体形状像字母“U”。左侧编码器部分由多个卷积块和下采样层组成。每一次下采样会把特征图尺寸缩小一半同时把通道数增加一倍。这个过程类似 VGG、ResNet 等分类网络中的特征提取阶段模型逐步学习从局部纹理到高层语义的特征。右侧解码器部分做的是相反操作把低分辨率的语义特征逐步上采样回原图尺寸并通过跳跃连接将编码器同尺寸的特征图拼接到一起。这一步非常关键因为单纯的上采样会丢失很多细节比如物体边缘、小结构而跳跃连接把下采样之前的细节特征直接传进来让解码器能够同时参考“全局语义”和“局部细节”。跳跃连接不仅是通道维度的拼接还起到了梯度传播的作用。这让 U-Net 在训练时收敛更快也更稳定。下面我们直接进入 PyTorch 实战环节先确定环境与项目结构。2. 环境准备与项目结构2.1 Python 与 PyTorch 环境本文示例代码不依赖复杂第三方库核心只需要 PyTorch 和基础图像处理库。Python 3.8 PyTorch 1.10 numpy Pillow matplotlib如果你还没有配置 PyTorch 环境可以参考官方提供的安装命令根据自己电脑是否有 NVIDIA 显卡选择 CPU 版或 CUDA 版。版本兼容问题比较常见建议在安装时通过以下方式确认python -c import torch; print(torch.__version__, torch.cuda.is_available())如果输出中torch.cuda.is_available()为False说明当前环境无法使用 GPU。本文训练的是合成数据数据量小、模型规模适中CPU 也能运行只是速度稍慢。需要特别提醒的是PyTorch 与 CUDA 版本存在配套关系安装时尽量避免直接使用最新版本先确认本机显卡驱动支持的 CUDA 版本再去 PyTorch 官网选择对应命令。2.2 合成数据集设计为了让大家能完整跑通流程同时又不需要提前下载动辄几个 GB 的真实分割数据集本文采用合成数据来完成实验。数据生成规则如下图像尺寸 256×256灰度图。背景为高斯噪声模拟简单纹理。每张图随机绘制 1~3 个圆形或矩形作为前景目标。掩码图中前景目标区域为白色255背景区域为黑色0。这个任务虽然比真实场景简单但已经包含了图像分割最核心的问题像素级二分类。模型能够在这个任务上达到较高 IoU 后再切换真实数据集只需要更换数据加载部分模型代码和训练流程可以完全复用。2.3 项目结构unet-segmentation/ ├── generate_data.py # 生成合成数据集 ├── dataset.py # Dataset 与数据增强 ├── model.py # U-Net 模型定义 ├── train.py # 训练与验证 ├── inference.py # 单张推理与可视化 ├── data/ │ ├── train/ │ │ ├── images/ │ │ └── masks/ │ └── val/ │ ├── images/ │ └── masks/建议新建一个专门的目录存放以上文件避免与其它项目混在一起。3. 从零实现 U-Net 模型3.1 卷积块与 Down 模块U-Net 中大量使用“两次卷积 批归一化 ReLU”的组合我们把它封装成DoubleConv模块。加入BatchNorm2d能有效缓解深层网络训练时的梯度分布问题也能加快收敛速度。# 文件路径model.py import torch import torch.nn as nn class DoubleConv(nn.Module): 两次卷积 BN ReLU def __init__(self, in_channels, out_channels, mid_channelsNone): super().__init__() if mid_channels is None: mid_channels out_channels self.double_conv nn.Sequential( nn.Conv2d(in_channels, mid_channels, kernel_size3, padding1, biasFalse), nn.BatchNorm2d(mid_channels), nn.ReLU(inplaceTrue), nn.Conv2d(mid_channels, out_channels, kernel_size3, padding1, biasFalse), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue), ) def forward(self, x): return self.double_conv(x)Down模块负责下采样这里使用最大池化将特征图宽高缩小一半然后接DoubleConv增加通道数。class Down(nn.Module): 下采样最大池化 双卷积 def __init__(self, in_channels, out_channels): super().__init__() self.maxpool_conv nn.Sequential( nn.MaxPool2d(kernel_size2, stride2), DoubleConv(in_channels, out_channels), ) def forward(self, x): return self.maxpool_conv(x)3.2 Up 模块与跳跃连接Up模块是 U-Net 的核心创新点。我们使用转置卷积ConvTranspose2d将特征图的宽高扩大一倍然后和编码器对应层的特征在通道维上拼接最后用DoubleConv将拼接后的特征融合输出。class Up(nn.Module): 上采样转置卷积 跳跃连接拼接 双卷积 def __init__(self, in_channels, out_channels): super().__init__() self.up nn.ConvTranspose2d(in_channels, out_channels, kernel_size2, stride2) self.conv DoubleConv(out_channels * 2, out_channels) def forward(self, x, skip): x self.up(x) x torch.cat([x, skip], dim1) return self.conv(x)这里有个容易忽略的细节为什么self.conv的输入通道是out_channels * 2因为转置卷积先把x的通道数从in_channels降到out_channels随后跳跃连接的skip恰好也是out_channels通道拼接后得到out_channels * 2再由DoubleConv处理回out_channels。3.3 完整 U-Net 网络有了基础模块后组装完整的 U-Net 就很简单了。整体结构如下输入 (1×256×256) ↓ DoubleConv(1 → 64) → skip1 ↓ Down(64 → 128) → skip2 ↓ Down(128 → 256) → skip3 ↓ Down(256 → 512) → skip4 ↓ Down(512 → 1024) → 最底层特征 ↓ Up(1024 → 512) 拼接 skip4 ↓ Up(512 → 256) 拼接 skip3 ↓ Up(256 → 128) 拼接 skip2 ↓ Up(128 → 64) 拼接 skip1 ↓ 输出卷积(64 → n_classes)完整代码如下class OutConv(nn.Module): def __init__(self, in_channels, out_channels): super().__init__() self.conv nn.Conv2d(in_channels, out_channels, kernel_size1) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, n_channels1, n_classes1): super().__init__() self.inc DoubleConv(n_channels, 64) self.down1 Down(64, 128) self.down2 Down(128, 256) self.down3 Down(256, 512) self.down4 Down(512, 1024) self.up1 Up(1024, 512) self.up2 Up(512, 256) self.up3 Up(256, 128) self.up4 Up(128, 64) self.outc OutConv(64, n_classes) def forward(self, x): x1 self.inc(x) x2 self.down1(x1) x3 self.down2(x2) x4 self.down3(x3) x5 self.down4(x4) x self.up1(x5, x4) x self.up2(x, x3) x self.up3(x, x2) x self.up4(x, x1) logits self.outc(x) return logits模型输出层的n_classes1最后没有接 Sigmoid而是输出 logits。这样是为了配合BCEWithLogitsLoss它在内部同时完成 Sigmoid 与损失计算数值上更稳定训练时强烈建议这样写。为了快速验证网络定义是否正确可以在model.py末尾加上一段自测代码if __name__ __main__: model UNet(n_channels1, n_classes1) dummy torch.randn(1, 1, 256, 256) out model(dummy) print(输出形状:, out.shape)运行后会输出torch.Size([1, 1, 256, 256])表示输出和输入分辨率一致每个像素都对应一个 logit 值。4. 数据生成与加载4.1 生成合成图像与掩码我们采用PIL.ImageDraw来绘制圆形和矩形。为了让生成的数据更有区分度前景区域使用 60~200 之间的随机灰度值背景使用高斯噪声这样模型不能仅凭“是否纯黑”来判断目标区域。# 文件路径generate_data.py import os import numpy as np from PIL import Image, ImageDraw def generate_sample(img_size256, seedNone): rng np.random.default_rng(seed) # 图像高斯噪声背景 arr rng.normal(128, 20, (img_size, img_size)) arr arr.clip(0, 255).astype(np.uint8) img Image.fromarray(arr, modeL) mask Image.new(L, (img_size, img_size), 0) draw_img ImageDraw.Draw(img) draw_mask ImageDraw.Draw(mask) for _ in range(int(rng.integers(1, 4))): shape rng.choice([circle, rect]) if shape circle: r int(rng.integers(15, 50)) x int(rng.integers(r, img_size - r)) y int(rng.integers(r, img_size - r)) draw_img.ellipse([x - r, y - r, x r, y r], fillint(rng.integers(60, 200))) draw_mask.ellipse([x - r, y - r, x r, y r], fill255) else: w int(rng.integers(30, 80)) h int(rng.integers(30, 80)) x0 int(rng.integers(0, img_size - w)) y0 int(rng.integers(0, img_size - h)) draw_img.rectangle([x0, y0, x0 w, y0 h], fillint(rng.integers(60, 200))) draw_mask.rectangle([x0, y0, x0 w, y0 h], fill255) return np.array(img, dtypenp.uint8), np.array(mask, dtypenp.uint8) def main(): base_dir data train_img_dir os.path.join(base_dir, train, images) train_mask_dir os.path.join(base_dir, train, masks) val_img_dir os.path.join(base_dir, val, images) val_mask_dir os.path.join(base_dir, val, masks) for d in [train_img_dir, train_mask_dir, val_img_dir, val_mask_dir]: os.makedirs(d, exist_okTrue) for i in range(60): img, mask generate_sample(seedi) if i 48: img.save(os.path.join(train_img_dir, fsample_{i:03d}.png)) Image.fromarray(mask).save(os.path.join(train_mask_dir, fsample_{i:03d}.png)) else: img.save(os.path.join(val_img_dir, fsample_{i:03d}.png)) Image.fromarray(mask).save(os.path.join(val_mask_dir, fsample_{i:03d}.png)) print(合成数据集生成完成 ftrain 48 张val 12 张) if __name__ __main__: main()执行后在data目录下生成train和val两个子集每个子集内部再分为images和masks两个目录。目录分离的优点是训练集和验证集天然独立不会发生数据泄露。4.2 自定义 Dataset 与数据增强PyTorch 中加载自定义数据需要继承torch.utils.data.Dataset实现__len__和__getitem__两个方法。由于本文是灰度图像输入张量形状是(1, H, W)如果你要处理三通道 RGB 图像只需要把convert(L)改成convert(RGB)并调整模型输入的n_channels即可。# 文件路径dataset.py import glob import os import numpy as np import torch from PIL import Image from torch.utils.data import Dataset class SegmentationDataset(Dataset): def __init__(self, image_dir, mask_dir, trainFalse): self.image_paths sorted(glob.glob(os.path.join(image_dir, *.png))) self.mask_paths sorted(glob.glob(os.path.join(mask_dir, *.png))) assert len(self.image_paths) len(self.mask_paths), 图像与掩码数量不一致 self.train train def __len__(self): return len(self.image_paths) def __getitem__(self, idx): image Image.open(self.image_paths[idx]).convert(L) mask Image.open(self.mask_paths[idx]).convert(L) image np.array(image, dtypenp.float32) / 255.0 mask np.array(mask, dtypenp.float32) / 255.0 # 训练集上做随机翻转增强验证集不做 if self.train: if np.random.rand() 0.5: image np.flip(image, axis1).copy() mask np.flip(mask, axis1).copy() if np.random.rand() 0.5: image np.flip(image, axis0).copy() mask np.flip(mask, axis0).copy() image_tensor torch.from_numpy(image).unsqueeze(0) mask_tensor torch.from_numpy(mask).unsqueeze(0) return image_tensor, mask_tensor这里有几个工程上需要注意的要点图像和掩码要使用完全相同的翻转操作否则训练数据的标注就错了。np.flip返回的是视图需要调用.copy()避免后续内存访问异常。灰度图归一化到[0, 1]掩码也归一化到[0, 1]这样标签变成 0 或 1。如果希望使用更丰富的数据增强比如随机旋转、缩放、亮度调整推荐引入albumentations库。它专门考虑了分割任务中“图像增强与掩码增强保持一致”的问题用法比手写更简洁。5. 训练与验证5.1 损失函数与评估指标本文是二分类分割问题最直接的损失函数是BCEWithLogitsLoss。它期望模型输出是未经过 Sigmoid 的 logits标签是 0 或 1数值上更稳定。不过实际工程中很多分割任务存在严重的类别不平衡问题比如肿瘤区域只占整幅图像的很小比例。这种情况下可以尝试DiceLoss或者把 Dice Loss 与 BCE Loss 按比例结合。下面给出一个通用的 Dice Loss 实现供后续替换使用import torch import torch.nn as nn class DiceLoss(nn.Module): def __init__(self, smooth1.0): super().__init__() self.smooth smooth def forward(self, logits, targets): probs torch.sigmoid(logits) intersection (probs * targets).sum() union probs.sum() targets.sum() dice (2.0 * intersection self.smooth) / (union self.smooth) return 1.0 - dice评估指标方面图像分割领域最常用的是 IoUIntersection over Union也叫 Jaccard 指数。它的计算方式是预测区域与真实区域的交集面积除以并集面积。def compute_iou(pred, target, eps1e-6): 计算每个样本的 IoU返回 batch 均值 pred_flat pred.view(pred.size(0), -1) target_flat target.view(target.size(0), -1) intersection (pred_flat * target_flat).sum(dim1) union pred_flat.sum(dim1) target_flat.sum(dim1) - intersection iou (intersection eps) / (union eps) return iou.mean().item()当一张图中完全没有前景目标时交集和并集都可能是 0加入eps可以避免除零错误。不过在本实验的合成数据中每张图都一定有前景目标这个问题并不明显。5.2 训练流程有了模型、数据和指标训练代码的结构就比较清晰了。我们用一个完整脚本来实现训练与验证# 文件路径train.py import os import numpy as np import torch import torch.nn as nn from torch.utils.data import DataLoader from dataset import SegmentationDataset from model import UNet def compute_iou(pred, target, eps1e-6): pred_flat pred.view(pred.size(0), -1) target_flat target.view(target.size(0), -1) intersection (pred_flat * target_flat).sum(dim1) union pred_flat.sum(dim1) target_flat.sum(dim1) - intersection iou (intersection eps) / (union eps) return iou.mean().item() def validate(model, dataloader, criterion, device): model.eval() total_loss 0.0 ious [] with torch.no_grad(): for images, masks in dataloader: images images.to(device) masks masks.to(device) logits model(images) loss criterion(logits, masks) total_loss loss.item() * images.size(0) probs torch.sigmoid(logits) preds (probs 0.5).float() ious.append(compute_iou(preds, masks)) avg_loss total_loss / len(dataloader.dataset) avg_iou float(np.mean(ious)) return avg_loss, avg_iou def main(): device torch.device(cuda if torch.cuda.is_available() else cpu) print(使用设备:, device) batch_size 4 epochs 20 lr 1e-3 train_ds SegmentationDataset(data/train/images, data/train/masks, trainTrue) val_ds SegmentationDataset(data/val/images, data/val/masks, trainFalse) train_loader DataLoader(train_ds, batch_sizebatch_size, shuffleTrue, num_workers0) val_loader DataLoader(val_ds, batch_sizebatch_size, shuffleFalse, num_workers0) model UNet(n_channels1, n_classes1).to(device) criterion nn.BCEWithLogitsLoss() optimizer torch.optim.Adam(model.parameters(), lrlr) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemax, patience3, factor0.5 ) best_iou 0.0 for epoch in range(1, epochs 1): model.train() total_loss 0.0 for images, masks in train_loader: images images.to(device) masks masks.to(device) optimizer.zero_grad() logits model(images) loss criterion(logits, masks) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) train_loss total_loss / len(train_ds) val_loss, val_iou validate(model, val_loader, criterion, device) scheduler.step(val_iou) if val_iou best_iou: best_iou val_iou torch.save(model.state_dict(), best_model.pth) print( fEpoch {epoch:02d} | train_loss{train_loss:.4f} | fval_loss{val_loss:.4f} | val_iou{val_iou:.4f} | 保存最佳模型 ) else: print( fEpoch {epoch:02d} | train_loss{train_loss:.4f} | fval_loss{val_loss:.4f} | val_iou{val_iou:.4f} ) print(训练完成最佳验证 IoU:, round(best_iou, 4)) if __name__ __main__: main()在默认参数下运行 20 个 epoch合成数据集的验证 IoU 很快就能达到 0.95 以上。如果你的运行环境是 CPU可以将epochs调小到 10或者将batch_size调整为 2减少等待时间。关于学习率和调度器这里使用ReduceLROnPlateau根据验证 IoU 自动调整学习率当指标连续多个 epoch 不上升时学习率减半。这样可以在接近收敛时避免震荡。6. 模型推理与可视化6.1 加载模型训练结束后当前目录下会生成best_model.pth。加载模型时要注意两点保存的是state_dict不是整个模型对象。加载时需要提前实例化网络结构且传入的参数要和训练时一致。device torch.device(cuda if torch.cuda.is_available() else cpu) model UNet(n_channels1, n_classes1).to(device) model.load_state_dict(torch.load(best_model.pth, map_locationdevice)) model.eval()map_location参数很重要。如果模型是在 GPU 上训练的而现在只有 CPU 环境不加这个参数会报位置不匹配的错误。6.2 预测结果可视化推理时输入图像要和训练过程保持一致先转成灰度图或 RGB再归一化最后变成(1, C, H, W)的张量。下面以验证集中的sample_048.png为例完成预测并可视化# 文件路径inference.py import numpy as np import torch import matplotlib.pyplot as plt from PIL import Image from model import UNet def main(): device torch.device(cuda if torch.cuda.is_available() else cpu) model UNet(n_channels1, n_classes1).to(device) model.load_state_dict(torch.load(best_model.pth, map_locationdevice)) model.eval() image_path data/val/images/sample_048.png mask_path data/val/masks/sample_048.png image_np np.array(Image.open(image_path).convert(L), dtypenp.float32) / 255.0 mask_np np.array(Image.open(mask_path).convert(L), dtypenp.float32) / 255.0 image_tensor torch.from_numpy(image_np).unsqueeze(0).unsqueeze(0).to(device) with torch.no_grad(): logits model(image_tensor) probs torch.sigmoid(logits) pred_np (probs.cpu().numpy().squeeze() 0.5) base np.stack([image_np * 255] * 3, axis-1).astype(np.uint8) overlay base.copy() overlay[pred_np] (255, 80, 80) fig, axes plt.subplots(1, 4, figsize(16, 4)) axes[0].imshow(image_np, cmapgray) axes[0].set_title(Original) axes[1].imshow(mask_np, cmapgray) axes[1].set_title(Ground Truth) axes[2].imshow(pred_np, cmapgray) axes[2].set_title(Prediction) axes[3].imshow(overlay) axes[3].set_title(Overlay) for ax in axes: ax.axis(off) plt.tight_layout() plt.show() if __name__ __main__: main()如果你是在服务器上运行没有图形界面可以把plt.show()换成plt.savefig(result.png)这样同样可以拿到可视化结果。从实际效果来看U-Net 在合成数据上的预测 mask 基本能覆盖完整目标区域。由于模型本身有池化和下采样过程目标边缘会有一两个像素的偏差但总体上 IoU 指标能保持很高水平。7. 常见问题与排查思路无论你是第一次跑分割任务还是准备把 U-Net 迁移到自己的数据集上下面这些高频问题都值得提前了解。问题现象常见原因解决思路训练时 CUDA out of memorybatch_size 太大或输入分辨率过高减小 batch_size、降低输入图像尺寸、使用梯度累积验证 IoU 始终为 0预测阈值设置不当或类别极度不平衡检查 Sigmoid 输出分布调整阈值换用 Dice Loss模型输出有棋盘格伪影转置卷积叠加产生重叠区域不均匀可换用双线性上采样 普通卷积的组合损失不下降学习率过大或过小特征归一化缺失初始化学习率设为 1e-3/1e-4检查是否加了 BatchNorm加载模型报键名错误网络参数与保存的 state_dict 不一致确认加载前实例化的 UNet 参数与训练时一致跳跃连接拼接尺寸不一致输入图片宽高不能被 16 整除将图片 resize 到 64 的整数倍或使用 padding 策略CPU 训练速度很慢模型参数量大数据预处理耗时降低输入分辨率、减少 epoch、或切换到 GPU其中“跳跃连接拼接尺寸不一致”是大图输入时最容易踩的坑。因为 U-Net 内部有 4 次下采样和 4 次上采样如果输入尺寸不是 16 的倍数上采样后的尺寸可能与编码器对应层的输出尺寸对不上直接拼接会报错。解决办法有两个一是将输入图片统一 resize 到能被 16 整除的尺寸二是在每个卷积层中根据尺寸差做适当裁剪。8. 工程实践建议与后续学习路线8.1 工程实践建议把 U-Net 从基础 Demo 迁移到真实项目时有几个方向值得重点打磨。第一损失函数不要只盯着 BCE。医学图像分割、遥感分割等场景中前景占比往往很小此时单独使用 BCE 会导致模型偏向预测背景。建议使用Dice Loss、Focal Loss或者将多个损失按权重组合。第二数据增强是分割任务的重要收益来源。随机翻转、随机缩放、随机旋转、颜色抖动都能提升模型的泛化能力。但要特别注意几何变换必须对图像和掩码同时做完全一致的变换否则模型会学到错误的标注。第三分割模型训练时批量大小不宜过小。BatchNorm在 batch 较小时统计量不稳定可能影响训练效果。如果显存有限可以降低分辨率而不是强行缩小 batch。第四要保存最佳模型而不是最后一个 epoch 的模型。本文的训练代码中已通过验证 IoU 监控val_iou并保存最优权重这项习惯在真实项目中能避免很多性能回退问题。第五推理阶段可以增加简单的 TTATest Time Augmentation。例如把输入图像水平翻转后分别预测再将两个预测结果取平均。这个技巧不需要重新训练通常能带来一点点的 IoU 提升。8.2 后续学习路线完成本文的 U-Net 图像分割实战后你可以继续往几个方向深入探索。最直接的扩展是把输入从灰度图换成 RGB 三通道然后在真实数据集上训练比如著名的CamVid道路分割数据集、VOC语义分割数据集或者医学影像分割中的LiTS肝脏数据集。真实数据的分布更复杂你会遇到类别不均衡、边界模糊、标注噪声等问题这些正是工程中真正需要处理的部分。模型结构方面可以对比尝试U-Net、Attention U-Net、DeepLabV3、SegNet等变体理解不同改进方案背后的动机。PyTorch 生态下也有一些现成的分割库但在熟悉底层实现之前建议先自己手写一遍这样后续换框架排查问题会更快。如果你的目标方向是医学图像分割需要额外关注图像灰度范围不一致、标注标准多样化、多模态影像配准等问题。这类任务往往要求较高的准确率且训练样本偏少因此数据增强和迁移学习的重要性会更加突出。总之图像分割不只是一套网络结构更是一套“数据构建 - 模型设计 - 损失优化 - 指标评估 - 边缘场景处理”的完整工程链路。希望本文这份完整的 PyTorch 入门实战能帮你把 U-Net 真正跑通、用好并在自己的项目中顺利落地。
返回列表