- 教程
- 人工智能
- 机器学习
- 深度学习
【免费下载链接】AI-For-Beginners
12 Weeks, 24 Lessons, AI for All!
本篇技术指南基于 AI-For-Beginners 课程第 12 课《Segmentation》(分割)展开,讲解如何将计算机视觉任务从"框出目标"的目标检测推进到"逐像素分类"的分割,并结合仓库内的两个可运行 Notebook(PyTorch 与 TensorFlow 双版本)与课后实验,完整走通"语义分割 / 实例分割 → 编码器-解码器架构 → 医学图像(皮肤痣 PH² 数据集)分割"的实战链路。读完本篇,你将掌握分割网络的数据加载、模型搭建(SegNet、U-Net)、损失函数选择(交叉熵 / BCE)、训练与评估(像素准确率)全流程,并能独立完成一次人体分割实验。
一、分割:从边界框到像素级分类
在此前的目标检测(Object Detection)学习中,模型通过预测目标的边界框(bounding box)来定位物体。但很多任务并不止于"框出物体",而要求更精确的目标定位——比如把照片中的人物轮廓完整抠出来,这正是本节的主题:分割(Segmentation)。
分割可以被看作逐像素分类(pixel classification):对于图像中的每一个像素,模型都必须预测它的类别(背景也是其中一个类别)。也就是说,分割模型的输出不是几个坐标框,而是一张与输入同尺寸的"类别图"。
语义分割与实例分割
分割有两大主流算法:
- 语义分割(Semantic segmentation):只判断每个像素属于哪个类别,不区分同一类别中的不同物体。例如图像中有 10 只羊,语义分割把它们全部标为"羊"这一类。
- 实例分割(Instance segmentation):在给出像素类别的同时,把同一类别的不同个体拆分成不同实例,10 只羊会被标记为 10 个不同对象。
从结构上看,目标检测只输出若干矩形框;而分割任务中,模型要对图像里每一个像素作出判断。这种更强的粒度让分割在自动驾驶(道路、行人、车道线的像素级识别)、医学影像(器官、病灶区域勾画)等场景中成为关键能力。
二、分割网络的统一架构:编码器-解码器(Encoder-Decoder)
尽管用于分割的神经网络架构有很多种,但它们都具有相同的总体结构。从某种意义上说,这种结构与你在本课程第 9 课(Autoencoders)中学习的自编码器相似——区别在于:自编码器的目标是"重构原始图像",而分割网络的目标是重构一张**掩码(mask)**图。因此,一个分割网络包含以下两个部分:
- 编码器(Encoder):从输入图像中提取特征;
- 解码器(Decoder):把这些特征转换为掩码图像——输出与输入尺寸相同,通道数等于类别数。
这一结构也解释了为何"通道数等于类别数":对每个像素而言,模型要输出一个长度为类别数的向量(背景占其中一类),取 argmax 即得该像素的最终类别。当任务只有"目标 / 背景"两类时,解码器末尾只需输出 1 个通道(如仓库 Notebook 中out_channels=1的做法)。
SegNet:最朴素的编码器-解码器
仓库中的 SemanticSegmentationPytorch.ipynb 首先实现了一个最简架构SegNet:编码器使用带卷积和池化的标准 CNN,解码器使用包含卷积和上采样(upsampling)的反卷积 CNN,并通过批归一化(Batch Normalization)让多层网络训练更稳定。
PyTorch 版 SegNet 的核心代码(节选自SemanticSegmentationPytorch.ipynb):
class SegNet(nn.Module): def __init__(self): super().__init__() # 编码器:4 组 3x3 卷积 + ReLU + BatchNorm + 2x2 最大池化 self.enc_conv0 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=(3,3), padding=1) self.act0 = nn.ReLU() self.bn0 = nn.BatchNorm2d(16) self.pool0 = nn.MaxPool2d(kernel_size=(2,2)) # ... enc_conv1(16→32)、enc_conv2(32→64)、enc_conv3(64→128) 结构同理 # 瓶颈层:通道数提升到 256 self.bottleneck_conv = nn.Conv2d(in_channels=128, out_channels=256, kernel_size=(3,3), padding=1) # 解码器:4 组双线性上采样 + 卷积 + BatchNorm,通道逐级回降 self.upsample0 = nn.UpsamplingBilinear2d(scale_factor=2) self.dec_conv0 = nn.Conv2d(in_channels=256, out_channels=128, kernel_size=(3,3), padding=1) # ... dec_conv1(128→64)、dec_conv2(64→32) 结构同理 # 最终输出:1 个通道(二分类掩码) self.upsample3 = nn.UpsamplingBilinear2d(scale_factor=2) self.dec_conv3 = nn.Conv2d(in_channels=32, out_channels=1, kernel_size=(1,1)) self.sigmoid = nn.Sigmoid() def forward(self, x): e0 = self.pool0(self.bn0(self.act0(self.enc_conv0(x)))) # e1、e2、e3 同理 b = self.bottleneck_conv(e3) d0 = self.dec_bn0(self.dec_act0(self.dec_conv0(self.upsample0(b)))) # d1、d2 同理 d3 = self.sigmoid(self.dec_conv3(self.upsample3(d2))) return d3U-Net:用跳跃连接留住细节
SegNet 的架构很自然,但并不是最精确的:编码器先对原图施加金字塔式 CNN 结构,会降低图像特征的空间精度;当解码器重建图像时,无法正确还原像素位置。
这引出了**跳跃连接(skip connections)**的思想:在编码器和解码器对应的卷积层之间建立直连通道。该架构在语义分割中极为常用,被称为U-Net。每个卷积层上的跳跃连接帮助网络保留来自该层原始输入的特征信息,从而缓解下采样带来的细节丢失。
仓库的 Notebook 中使用了一个相对简单的 CNN 编码器,并指出 U-Net 也可以使用更复杂的特征提取骨干网络,例如 ResNet-50。U-Net 在 PyTorch 中的关键差异点在于解码器输入通道的拼接:
# 编码器每层"激活后、池化前"的特征图被保留用于拼接 cat0 = self.bn0(self.act0(self.enc_conv0(x))) cat1 = self.bn1(self.act1(self.enc_conv1(e0))) cat2 = self.bn2(self.act2(self.enc_conv2(e1))) cat3 = self.bn3(self.act3(self.enc_conv3(e2))) b = self.bottleneck_conv(e3) # 解码器每一层都与对应尺度的编码器特征做通道拼接(torch.cat) d0 = self.dec_bn0(self.dec_act0(self.dec_conv0(torch.cat((self.upsample0(b), cat3), dim=1)))) d1 = self.dec_bn1(self.dec_act1(self.dec_conv1(torch.cat((self.upsample1(d0), cat2), dim=1)))) d2 = self.dec_bn2(self.dec_act2(self.dec_conv2(torch.cat((self.upsample2(d1), cat1), dim=1)))) d3 = self.sigmoid(self.dec_conv3(torch.cat((self.upsample3(d2), cat0), dim=1))))可以看到 U-Net 解码器各层输入通道数(384 / 192 / 96 / 48)恰好是"上采样特征 + 跳跃连接特征"的叠加,这正是跳跃连接在实现层面的直接体现。TensorFlow 版(SemanticSegmentationTF.ipynb)用keras.Concatenate(axis=3)完成同样操作。
三、分割的损失函数:为什么不能只用 MSE
分割任务的损失函数值得特别关注。训练经典自编码器时,我们需要度量两张图像的相似度,可以用均方误差(MSE)。但在分割中,目标掩码图的每个像素代表一个类别编号(沿第三维做 one-hot 编码),因此必须使用专门用于分类的损失函数——交叉熵损失(cross-entropy loss),并对所有像素求平均。
- 当掩码为二分类(如"痣 / 背景")时,使用二分类交叉熵损失(Binary Cross-Entropy,BCE)。
✅One-hot 编码是将类别标签编码为长度等于类别数向量的方式,分割任务中通常沿通道维(第三维)展开。例如二分类时类别 0 编码为
[1, 0],类别 1 编码为[0, 1]。
仓库的 PyTorch Notebook 在搭建模型后直接配置了优化器与损失函数:
model = SegNet().to(device) optimizer = optim.Adam(model.parameters(), lr=lr, weight_decay=weight_decay) loss_fn = nn.BCEWithLogitsLoss() # 二分类交叉熵TensorFlow 版与之对应:
model = SegNet() # tf.keras.Model optimizer = optimizers.Adam(learning_rate=lr, decay=weight_decay) loss_fn = losses.BinaryCrossentropy(from_logits=True) # 使用 logits 形式的 BCE model.compile(loss=loss_fn, optimizer=optimizer)选择 BCE 而非 MSE 的直觉是:逐像素分类本质上是分类问题,而分类任务用交叉熵族损失收敛更稳、梯度更合理;MSE 是为回归 / 图像重构设计的,直接套用于像素类别预测会弱化分类边界的学习信号。
四、医学图像分割实战:PH² 皮肤镜数据集
本节课程的实战场景是:训练一个网络,在医学图像上识别人类的痣(nevus,俗称胎记/痦子)。数据来源为PH² 皮肤镜图像数据库(PH² Database of dermoscopy images)。
数据集概览
- 共200 张图像,分为三个类别:典型痣(typical nevus)、非典型痣(atypical nevus)、黑色素瘤(melanoma);
- 每张图像都配有一张对应的掩码(mask),标出痣的区域。
训练目标是:把任意一个痣从背景中分割出来(即二分类:痣 / 背景)。这种技术尤其适合此类医学成像任务——肿瘤、病灶的精确边界对诊断意义重大。下图展示了 PH² 数据集中的原始图像与掩码(图像与掩码均由 PH² 数据库提供):
数据集下载与预处理
Notebook 中通过命令行下载 PH² 数据集的 RAR 压缩包并解压(需要系统安装unrar工具,Linux 下可用包管理器安装):
wget https://www.dropbox.com/s/k88qukc20ljnbuo/PH2Dataset.rar unrar x -Y PH2Dataset.rar随后定义数据加载函数:遍历PH2Dataset/PH2 Dataset images目录,凡是目录名以_Dermoscopic_Image结尾的读取为原图,以_lesion结尾的读取为掩码;将所有图像缩放到256×256,并按比例切分训练集与测试集。
PyTorch 版(SemanticSegmentationPytorch.ipynb):
def load_dataset(train_part, root='PH2Dataset'): images = [] masks = [] for root, dirs, files in os.walk(os.path.join(root, 'PH2 Dataset images')): if root.endswith('_Dermoscopic_Image'): images.append(imread(os.path.join(root, files[0]))) if root.endswith('_lesion'): masks.append(imread(os.path.join(root, files[0]))) size = (256, 256) # 原图:双线性缩放(anti_aliasing=True) images = torch.permute(torch.FloatTensor( np.array([resize(image, size, mode='constant', anti_aliasing=True) for image in images])), (0, 3, 1, 2)) # 掩码:最近邻缩放后二值化(>0.5),并增加通道维 masks = torch.FloatTensor( np.array([resize(mask, size, mode='constant', anti_aliasing=False) > 0.5 for mask in masks]) ).unsqueeze(1) indices = np.random.permutation(range(len(images))) train_part = int(train_part * len(images)) train_ind, test_ind = indices[:train_part], indices[train_part:] return (images[train_ind], masks[train_ind]), (images[test_ind], masks[test_ind]) train_dataset, test_dataset = load_dataset(train_size)这里有两个值得注意的预处理细节:
- 掩码缩放使用
anti_aliasing=False并做> 0.5二值化:掩码是标注图而非自然图像,缩放后需要通过阈值重新二值化,避免插值产生模糊的中间值破坏标签语义; - PyTorch 版将张量置换为
(N, C, H, W)布局((0, 3, 1, 2)),以匹配卷积层的通道优先约定;TensorFlow 版则保持(N, H, W, C),两者与各自框架的数据约定一致。
超参数配置
PyTorch Notebook 给出的实验配置(用于 SegNet 与 U-Net 两套模型):
device = 'cuda:0' if torch.cuda.is_available() else 'cpu' train_size = 0.9 # 90% 数据用于训练 lr = 1e-3 # Adam 学习率 weight_decay = 1e-6 # L2 权重衰减 batch_size = 32 epochs = 30TensorFlow Notebook 的配置则按 80/20 划分数据并训练更多轮次:
train_size = 0.8 lr = 3e-4 weight_decay = 8e-9 batch_size = 64 epochs = 100五、训练循环与评估:两个可运行的 Notebook 实验
PyTorch 版训练循环
PyTorch Notebook 的训练函数遵循标准的"前向传播 → 计算损失 → 反向传播 → 更新参数"流程,并在每个 epoch 后于测试集上评估损失:
def train(dataloaders, model, loss_fn, optimizer, epochs, device): train_dataloader, test_dataloader = dataloaders[0], dataloaders[1] tqdm_iter = tqdm(range(epochs)) for epoch in tqdm_iter: model.train() train_loss = 0.0 for batch in train_dataloader: imgs, labels = batch imgs, labels = imgs.to(device), labels.to(device) preds = model(imgs) loss = loss_fn(preds, labels) optimizer.zero_grad() loss.backward() optimizer.step() train_loss += loss.item() model.eval() test_loss = 0.0 with torch.no_grad(): for batch in test_dataloader: imgs, labels = batch imgs, labels = imgs.to(device), labels.to(device) loss = loss_fn(model(imgs), labels) test_loss += loss.item() train_loss /= len(train_dataloader) test_loss /= len(test_dataloader) tqdm_iter.set_postfix({'train loss:': train_loss, 'test loss:': test_loss}, refresh=True)Notebook 中 SegNet 与 U-Net 各训练 30 个 epoch(运行耗时分别约 16 分钟与 29 分钟),通过tqdm实时观察训练/测试损失。TensorFlow 版则直接使用 Keras 的model.fit(..., validation_data=...)完成 100 个 epoch 的训练,两种框架的教学思路完全对齐。
评估:掩码可视化与像素准确率
评估时,将测试图像的模型输出与真实掩码并排绘制,直观对比分割效果。由于二分类输出是概率图,预测掩码通过对输出做> 0.5阈值得到:
# PyTorch 版 predictions.append((model(img).detach().cpu()[0] > 0.5).float())# TensorFlow 版 pred = np.array(model.predict(img)) predictions.append(pred[0, :, :, 0] > 0.5)除了可视化,分割模型还有正式的评价指标。最容易理解的指标是像素准确率(pixel accuracy):被正确分类的像素所占百分比,即
$$\text{pixel accuracy}=\frac{\text{分类正确的像素数}}{\text{总像素数}}$$
在课程实验的直观对比中,SegNet 与 U-Net 都能较好地将痣与背景分离,而 U-Net 借助跳跃连接在边缘细节上通常更具优势(实验日志显示 U-Net 的测试损失最终更低:约 0.15 vs SegNet 约 0.45,可分别在两个 Notebook 的训练输出中查看)。
运行环境依赖
仓库 binder/requirements.txt 列出了运行这些 Notebook 所需的环境,其中与本课直接相关的关键依赖包括:
- 深度学习框架:
tensorflow==2.13.1、keras==3.13.2(TF 版 Notebook),以及 PyTorch 生态(torchvision,Notebook 中与torch一并导入); - 图像处理:
scikit-image==0.21.0(imread、resize)、pillow==12.2.0; - 辅助工具:
tqdm==4.66.5(进度条)、matplotlib(可视化,Notebook 中使用plt)、numpy。
你可以在 binder 配置 与 requirements.txt 中查看完整的虚拟环境定义。
六、课后实验:人体分割(Assignment)
本课的课后作业(lab/README.md)要求完成人体分割(Human Body Segmentation),应用场景非常贴近现实:
在视频制作(例如天气预报)中,我们经常需要把人从摄像机画面中抠出来,叠加到其他背景上。传统做法是色度键(chroma key)技术——让人站在纯色背景前拍摄,再把背景替换掉。本次实验将训练一个神经网络模型来直接抠出人体剪影,从而摆脱对纯色背景的依赖。
实验要点:
- 数据集:使用 Kaggle 上的Segmentation Full Body MADS Dataset(人体全身分割数据集),从 Kaggle 手动下载后解压到当前目录;
- 起点 Notebook:打开 lab/BodySegmentation.ipynb,其中先通过
plt.imread读取segmentation_full_body_mads_dataset_1192_img/images目录下的图像与其对应的masks掩码,并可视化图像与掩码的配对效果; - 之后复用本课学习的编码器-解码器架构与 BCE 损失完成训练。
七、延伸挑战与自学方向
- 挑战(Challenge):人体分割只是针对"人像"的常见任务之一,其他重要任务还包括骨骼检测(skeleton detection)与姿态检测(pose detection)。可以尝试 OpenPose 库,了解姿态检测的实现方式。
- 回顾与自学(Review & Self Study):建议进一步了解分割的更多应用场景,并自主研究**实例分割(Instance Segmentation)与全景分割(Panoptic Segmentation)**两个子领域——实例分割已在本课开头介绍,全景分割则把"语义"与"实例"两类输出统一到一个框架中。
八、课程内相关资源汇总
- 本课主文档:12-Segmentation/README.md
- PyTorch 实验:SemanticSegmentationPytorch.ipynb
- TensorFlow 实验:SemanticSegmentationTF.ipynb
- 课后作业说明:lab/README.md 与 lab/BodySegmentation.ipynb
- 前置知识:目标检测(第 11 课)、自编码器(第 9 课)
- 计算机视觉模块总览:4-ComputerVision/README.md
- 运行环境:binder/requirements.txt、environment.yml
小结
分割是比边界框更精细的图像理解技术:它把任务从"框出目标"升级为"逐像素分类"。其通用骨架是编码器-解码器结构——SegNet 提供最朴素的对应对称设计,U-Net 通过跳跃连接显著改善分割边界的精度。训练时请牢记分割与重构的本质区别:目标掩码是类别标签而非图像,因此损失函数应选用(逐像素平均的)交叉熵,二分类场景下则是 BCE。通过 PH² 医学图像数据集(PyTorch / TensorFlow 双版本 Notebook)与人体分割课后实验,你可以完整复现"数据加载 → 模型搭建 → 训练 → 掩码可视化与像素准确率评估"的整套实战流程,并将这一能力迁移到更多像素级理解任务中。
- 教程
- 人工智能
- 机器学习
- 深度学习
【免费下载链接】AI-For-Beginners
12 Weeks, 24 Lessons, AI for All!
相关推荐
AI-For-Beginners 图像分割实战指南:从像素级分类到医学影像病变分割
AI For Beginners 图像分割实战指南:从像素级分类到医学影像病变分割 图像分割(Segmentation)是计算机视觉中比目标检测更进一步的任务:
教程人工智能机器学习深度学习AI-For-Beginners 图像分割实战:从像素级分类原理到医学图像与人体分割实现
AI For Beginners 图像分割实战:从像素级分类原理到医学图像与人体分割实现 导读 本篇文章基于开源课程 AI For Beginners 的 Co
教程人工智能机器学习深度学习AI-For-Beginners 图像分割实战:从像素级分类到语义分割的编码器-解码器架构与医学影像应用
AI For Beginners 图像分割实战:从像素级分类到语义分割的编码器 解码器架构与医学影像应用 导读 :本文围绕 AI For Beginners 课
教程人工智能机器学习深度学习
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考