【CV with Pytorch】第 4 章 :构建图像分割模型

我们周围的图像有不同的纹理、图案、形状和大小。它们携带着大量的信息,这些信息很容易被人眼和大脑理解,但计算机却不太容易理解。图像分割是一个问题集,我们试图训练计算机理解图像,以便它们可以分离不同的对象并将相似的对象分组。这可以是类似像素强度或类似纹理和形状的形式。

已经开发了许多算法并已用于分割图像。就像对象检测将对象分开一样,图像分割从不太相似的对象中识别出更多相似的对象。如果我们考虑基本聚类方法(例如 k-means)中使用的概念,我们就会知道数据点如何在相似数据附近对齐。

例如,假设碗里有两种苹果和两种橙子。如果我们查看特征,我们可以从对可食用数据点进行排序开始。当我们考虑纹理或颜色时,我们可以将数据点分成两组。当我们将成本作为另一个特征添加时,我们可以将数据点分成四个集群。同样,这个序列可以帮助我们定位数据点的不同点或相似点,并按集群分组。这对生物医学领域和自动驾驶汽车非常有帮助。这仍然是一个活跃的研究领域,并且主要与对象检测框架相结合。我们将介绍基础知识,然后考虑一些示例。让我们开始吧。

图像分割

分割的主观性质是基于我们正在处理的领域类型。有两种类型的分割——语义分割和实例分割。当我们进行语义分割时,来自相似对象的像素被认为是一类,但对象内部没有分离。如果我们想象一下实时场景,当高速公路上有多辆汽车的图像时,分割会将所有汽车分组并将这些组与路边或风景分开。

让我们考虑一个例子。图4-1a显示了一条有汽车的高速公路。在多辆汽车旁边,高速公路的一侧有草地和一些树木。

【CV with Pytorch】第 4 章 :构建图像分割模型_第1张图片

图 4-1a 原始输入图像

现在考虑从原始输入中提取并从能够对输入中的对象进行分类的卷积神经网络传递的像素块(见图4-1b)。这将为我们提供类似于图中属于汽车的补丁的输出。接下来,我们尝试将中心像素映射到汽车并像这样遍历整个图像。这将为我们提供图像中的分割分离(语义)。它将汽车与树木和道路分开。这里需要注意的一件重要事情是所有汽车都属于同一类。

【CV with Pytorch】第 4 章 :构建图像分割模型_第2张图片

图 4-1b 从输入中获取补丁

此类问题的另一种解决方法是运行一个没有任何下采样的卷积神经网络分类器,并使用它对每个像素进行分类,从而将相似的对象聚集在一起。

在我们想要区分类别之前,这些都很好。例如,假设有多辆汽车,我们想对每辆汽车分别进行分类。在这种情况下,实例分割出现了,其中每个像素都被映射到特定的类,并且通过用适当的类标记像素来分离对象。语义分割的思想可以追溯到图像处理中使用的不可学习的技术,而实例分割是一个相当新的概念。

当我们开始进行实例分割时出现的一种基本方法是 R-CNN 方法的近似复制品。但我们预测的是细分而不是区域。

请看图4-2中的过程图。图像被传递到一个分段建议网络,该网络给出图像的分段。一方面,这些片段可以形成一个边界框并传递到框卷积神经网络以生成特征。另一方面,片段被接受并应用背景掩蔽变换。它只接受图像的平均值并将对象的背景转换为黑色。一旦片段被屏蔽,它就会被传递到区域卷积神经网络以获得不同的特征集。

【CV with Pytorch】第 4 章 :构建图像分割模型_第3张图片

图 4-2 实例分割流程

【CV with Pytorch】第 4 章 :构建图像分割模型_第4张图片

图 4-3 自定义数据输出

我们这里拥有的是框图像和网络拉取的区域的组合。这些将被组合,然后根据它们包含的对象实例进一步分类。当它细化分割区域时,还添加了一个次要步骤。

这些只是实验性的分割技术设置。还进行了其他方法上的改进,包括类似于 Faster R-CNN 的级联网络、超列等。

这些是语义分割和实例分割之间的多重差异。

使用语义分割:

  • 所有像素都被分类。

  • 使用完全卷积模型。

  • 下采样用于各种方法,然后使用可学习的上采样技术来重新创建图像。

  • 如果使用类似 ResNet 的架构,则使用跳过连接。

使用实例分割:

  • 不仅每个像素都被分类,而且实例也被检测到。

  • 该过程几乎遵循对象检测体系结构。

来自 PyTorch 的预训练支持

PyTorch的发展速度比任何其他框架都快得多。它有大量的模块和类。由于接近Python,所以更容易适应框架。一般而言,在深度学习中选择 PyTorch 框架并利用大量资源做出有影响力的改变是有推动力的。

与对象检测一样,分割也是架构中较重的一面。从头开始训练这些模型并不总是那么容易或理想。CPU 的训练时间相当长,尽管 GPU 在这种情况下有所帮助,但帮助不大。由于所有训练过程的限制,我们可能会选择迁移学习技术。这有助于我们利用已经提取到作品中的丰富信息。这些模型在多样化的数据集中进行训练,并被泛化以处理我们将遇到的大多数问题的变化。让我们深入了解torch存储库中的一些令人惊叹的模型。

语义分割

  • 全卷积神经网络。正如这篇论文所提出的,一个全卷积网络在语义分割任务上进行了端到端的训练。它是一个卷积神经网络块,后面跟着一个像素预测。

  • 使用空洞卷积进行语义分割 (DeepLavV3)。在该体系结构中,并行堆叠的空洞卷积用于通过改变感受野来捕获多尺度上下文。

  • Lite 减少空洞空间金字塔池化 (LR-ASPP)。MobileNetV3 的高级版本,它是在 NAS(神经架构搜索)的帮助下创建的。

我们将使用一个预训练模型来评估图像上的模型。该模型有很多参数,因此在 CPU 或低配置基础设施上运行推理会比较慢。如果我们使用 Colab,我们可以打开 GPU 作为基础设施支持并运行代码。

让我们从配置所需的基本import开始。该模型经过预训练并放置在 Torchvision 中。我们也将导入模型。

import numpy as np
import torch
import matplotlib.pyplot as plt
## torchvision 相关导入
import torchvision.transforms.functional as F
from torchvision.io import read_image
from torchvision.utils import draw_bounding_boxes
from torchvision.utils import make_grid
## 模型和转换
from torchvision.transforms.functional import convert_image_dtype
from torchvision.models.segmentation import fcn_resnet50

至此我们导入了 Torch 和 Torchvision 相关的函数。我们需要构建所有可以在整个代码中重用的实用函数。这是重构代码并删除不必要重复的有效方法。在这种情况下,我们需要显示图像,以便我们可以使用图像可视化工具。

## 多个图像的实用程序
def img_show(images):
    if not isinstance(images, list):
        ## 将投射图像概括为列表
        images = [images]
    fig, axis = plt.subplots(ncols=len(images), squeeze=False)
    for i, image in enumerate(images):
        image = image.detach() # 与当前 DAG 分离,无梯度
        image = F.to_pil_image(image)
        axis[0, i].imshow(np.asarray(image))
        axis[0, i].set(xticklabels=[], yticklabels=[], xticks=[], yticks=[])

此代码接受多个图像或单个图像。它检查对象是否为列表,如果不是,则将其转换为列表。轴是根据可迭代的图像分配的。图像与 DAG 分离,并且不计算这些变量的梯度。

在效用函数之后,让我们获取示例图像并将其配置为在分割过程中工作。

## 获取需要进行分割的图像
img1 = read_image("/content/semantic_example_highway.jpg")
box_car = torch.tensor([ [170, 70, 220, 120]], dtype=torch.float) ## (xmin,ymin,xmax,ymax)
colors = ["blue"]
check_box = draw_bounding_boxes(img1, box_car, colors=colors, width=2)
img_show(check_box)
## 图像批处理
batch_imgs = torch.stack([img1])
batch_torch = convert_image_dtype(batch_imgs, dtype=torch.float)

图片需要上传并放置在可访问的位置。该图像包括多辆汽车,截至目前,我们将一个盒子放在它上面(X min, Y min, X max和 Y max)。需要针对全卷积网络调整这些值,以了解对象的存在。最终,这批图像在为模型堆叠之前被转换为张量。

现在,让我们加载模型并准备好进行评估。

model = fcn_resnet50(pretrained=True, progress=False)
## 开启评估模式
model = model.eval()
# 基于训练配置的标准归一化
normalized_batch_torch = F.normalize(batch_torch, mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225))
result = model(normalized_batch_torch)['out']

如前所述,fcn_resnet50是从已经过训练的存储库中下载的。模型设置为评估步骤。在前面的步骤中创建的批处理现在根据经过训练的模型配置进行规范化。

现在是时候通过模型传递我们的图像了。

classes = [
    '__background__', 'aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus',
    'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike',
    'person', 'pottedplant', 'sheep', 'sofa', 'train', ’tvmonitor’
]
class_to_idx = {cls: idx for (idx, cls) in enumerate(classes)}
normalized_out_masks = torch.nn.functional.softmax(result, dim=1)
car_mask = [
    normalized_out_masks[img_idx, class_to_idx[cls]]
    for img_idx in range(batch_torch.shape[0])
    for cls in ('car', 'pottedplant','bus')
]
img_show(car_mask)

我们正在定义包含所有可能类别的列表,并让批处理图像结果通过 softmax 层。然后绘制掩码。这个例子为我们提供了我们之前讨论的语义分割的流程。它显示了我们如何获取任何数据并根据模型进行准备。我们加载一个模型并对模型运行推理以获得掩码。

实例分割

我们正在研究语义分割以生成对象的蒙版,然后将它们叠加在原始图像上。但是,实例分割呢?我们现在将看看一些我们可以用来生成掩码的预训练模型。

检测和掩蔽模型:

  • Faster R-CNN。本研究引入了一个区域提议网络,它同时预测对象边界框和与边界框对应的objectness core。它解决了与早期论文相关的瓶颈。

  • Mask R-CNN。这个过程扩展了 Faster R-CNN 并在图像上使用对象检测和生成掩码。

  • RetinaNet。这篇论文在精度和速度方面对两级检测器做了一些惊人的改进。它使用focal loss的新概念来处理所有这些问题。

  • Single Shot Detector。该论文解释说,为默认边界框生成一个 objectness 分数,并根据对象进行细化。

这些模型主要在 COCO 数据集上训练,能够处理预测。

对于faster R-CNN:

x = [torch.rand(3, 300, 400), torch.rand(3, 500, 400)]
faster_rcnn_model = torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrained=True)
faster_rcnn_model.eval()
result = faster_rcnn_model (x)

对于MobileNet:

x = [torch.rand(3, 300, 400), torch.rand(3, 500, 400)]
mobilenet_model = torchvision.models.detection.fasterrcnn_mobilenet_v3_large_fpn(pretrained=True)
mobilenet_model.eval()
result = mobilenet_model(x)

对于 RetinaNet:

x = [torch.rand(3, 300, 400), torch.rand(3, 500, 400)]
retinanet_model = torchvision.models.detection.retinanet_resnet50_fpn(pretrained=True)
retinanet_model.eval()
result = retinanet_model(x)

对于Single Shot Detection:

x = [torch.rand(3, 300, 400), torch.rand(3, 500, 400)]
ssd_model = torchvision.models.detection.ssd300_vgg16(pretrained=True)
ssd_model.eval()
result = ssd_model(x)

对于所有这些实例,我们从 PyTorch 存储库中提取模型并使用它来运行推理。

微调模型

如果我们工作的领域中有研究人员标记和训练的类,则使用预训练模型可以获得预测。如果它密切相关但我们没有确切的类别,我们可以期待变化。这是为我们的项目进行训练和分类的最重要原因之一。

在本节中,我们探索代码并详细解释微调现有模型的必要步骤,以增强模型的预测能力以满足我们的目的。正如我们已经建立的那样,分割包括可以识别图像中的类别的附加功能。

我们将使用的问题集是在https://www.cis.upenn.edu/~jshi/ped_html/PennFudanPed.zip上找到的开源数据集。

该数据集包含行人数据,我们将对其进行微调。各种用例使用分段作为输出来确定决策步骤。识别行人是自动驾驶汽车的一个重要用例,可以在瞬间决定他们需要向哪个方向移动。由于这些原因,这些模型的准确度也需要足够高。

让我们看一下流程的基本导入。

项目设置是任何项目中最具决定性的部分之一。在此项目中,我们可以使用 Jupyter notebook 实例或 Colab notebook 进行训练。

首先,我们使用wget命令从源下载数据集。

## 提取流量数据
!wget https://www.cis.upenn.edu/~jshi/ped_html/PennFudanPed.zip 。
!unzip PennFudanPed.zip

感叹号 ( ! ) 帮助 Colab 单元将它们识别为 shell 脚本。下载后,unzip 命令会解压缩包。需要注意的一件事是这些命令是基于 Linux 的,并且后端操作系统也假定为 Linux。

一旦我们的系统中有了数据集,我们就可以运行项目所需的基本导入。

## 基本导入
import os
import numpy as np
## torch 导入
import torch
import torch.utils.data
from torch.utils.data import Dataset
## torchvision 导入
import torchvision
import torchvision.transforms as T
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
from torchvision.models.detection.mask_rcnn import MaskRCNNPredictor
## 图像实用程序
from PIL import Image
import matplotlib.pyplot as plt
## 代码实用程序
import random
import cv2

请注意,我们正在导入 Torch 和 Torchvision 相关的包。我们还获得了 MaskRCNN 和 FasterRCNN 预训练模型。现在让我们导入 PyTorch 基础训练框架。这将有助于我们扩展功能并避免重写复杂的代码。

## 克隆 PyTorch 存储库以设置与原始训练的精确目录结构
!git clone https://github.com/pytorch/vision.git
%cd vision
!git checkout v0.3.0
!cp references/detection/engine.py ../
!cp references/detection/transforms.py ../
!cp references/detection/utils.py ../
!cp references/detection/coco_utils.py ../
!cp references/detection/coco_eval.py ../

准备好基本框架后,我们将复制我们将使用的重要Python 脚本,例如引擎、转换、实用程序、coco_utils和coco_eval。

一旦这些导入完成并且我们已经验证文件位于我们运行代码的同一基础架构上,我们就可以再运行几个导入。

## imports from the PyTorch repo
import utils
import transforms as T
from engine import train_one_epoch, evaluate

这些导入基于 PyTorch 训练框架和脚本中的代码。一旦完成,让我们看看微调模型所需的自定义数据集类的创建。

class CustomDataset(Dataset):
    def __init__(self, dir_path, transforms=None):
        ## 初始化对象属性
        self.transforms = transforms
        self.dir_path = dir_path
        ## 来自目录路径
        ## 添加了 PedMasks 目录中的面具列表
        self.mask_list = list(sorted(os.listdir(os.path.join(dir_path, "PedMasks"))))
        ## 添加目录列表中的实际图像列表
        self.image_list = list(sorted(os.listdir(os.path.join(dir_path, "PNGImages"))))
    def __getitem__(self, idx):
        # 获取图像和掩码
        img_path = os.path.join(self.dir_path, "PNGImages", self.image_list[idx])
        mask_path = os.path.join(self.dir_path, "PedMasks", self.mask_list[idx])
        image_obj = Image.open(img_path).convert("RGB")
        mask_obj = Image.open(mask_path)
        mask_obj = np.array(mask_obj)
        obj_ids = np.unique(mask_obj)
        # background 有第一个 id 所以不包括那个
        obj_ids = obj_ids[1:]
        # 将掩码拆分为二进制文件
        masks_obj = mask_obj == obj_ids[:, None, None]
        # 边界框
        num_objs = len(obj_ids)
        bboxes = []
        for i in range(num_objs):
            pos = np.where(masks_obj[i])
            xmax = np.max(pos[1])
            xmin = np.min(pos[1])
            ymax = np.max(pos[0])
            ymin = np.min(pos[0])
            bboxes.append([xmin, ymin, xmax, ymax])
        image_id = torch.tensor([idx])
        masks_obj = torch.as_tensor(masks_obj, dtype=torch.uint8)
        bboxes = torch.as_tensor(bboxes, dtype=torch.float32)
        labels = torch.ones((num_objs,), dtype=torch.int64)
        area = (bboxes[:, 3] - bboxes[:, 1]) * (bboxes[:, 2] - bboxes[:, 0])
        iscrowd = torch.zeros((num_objs,), dtype=torch.int64)
        target = {}
        target["image_id"] = image_id
        target["masks"] = masks_obj
        target["boxes"] = bboxes
        target["labels"] = labels
        target["area"] = area
        target["iscrowd"] = iscrowd
        if self.transforms is not None:
            image_obj, target = self.transforms(image_obj, target)
        return image_obj, target
    def __len__(self):
        return len(self.image_list)

自定义数据集创建是将较新的数据集合并到训练管道中的一种标准技术。要在代码中寻找的要点如下:

  • 我们正在从 PyTorch 扩展Dataset类。

  • 我们为该类定义了三个重要函数——初始化、get_item和len。

  • 我们正在初始化转换,这对于测试、验证和训练来说可能是不同的。

  • 我们正在定义边界框。

  • 我们正在定义目标。

完成数据集创建后,我们必须根据新数据修改模型。让我们看看它的代码。

def modify_model(classes_num):
    # 模型已经在从 PyTorch 存储库加载的 COCO 上训练
    maskrcnn_model = torchvision.models.detection.maskrcnn_resnet50_fpn(pretrained=True)
    # 输入特征识别个数
    in_features = maskrcnn_model.roi_heads.box_predictor.cls_score.in_features
    # head 改变了
    maskrcnn_model.roi_heads.box_predictor = FastRCNNPredictor(in_features, classes_num)
    in_features_mask = maskrcnn_model.roi_heads.mask_predictor.conv5_mask.in_channels
    hidden_layer = 256
    maskrcnn_model.roi_heads.mask_predictor = MaskRCNNPredictor(in_features_mask,
 hidden_layer,
num_classes)
    return maskrcnn_model

这些步骤改变了模型的头部配置。在此之后,我们将尝试对数据进行转换。这将为训练准备数据。

def get_transform_data(train):
    transforms = []
    # PIL 图像到 PyTorch 模型的张量
    transforms.append(T.ToTensor())
    if train:
        # 基本图像增强技术
        ## 可以添加更多的实验
        transforms.append(T.RandomHorizontalFlip(0.5))
    return T.Compose(transforms)
# 获取要转换的流量数据
train_dataset = CustomDataset('/content/PennFudanPed', get_transform_data(train=True))
test_dataset = CustomDataset('/content/PennFudanPed', get_transform_data(train=False))
# 训练测试拆分
torch.manual_seed(1)
indices = torch.randperm(len(train_dataset)).tolist()
train_dataset = torch.utils.data.Subset(train_dataset, indices[:-50])
test_dataset = torch.utils.data.Subset(test_dataset, indices[-50:])
# 定义训练和验证数据加载器
train_data_loader = torch.utils.data.DataLoader(
    train_dataset, batch_size=2, shuffle=True, num_workers=4,
    collate_fn=utils.collate_fn)
test_data_loader = torch.utils.data.DataLoader(
    test_dataset, batch_size=1, shuffle=False, num_workers=4,
    collate_fn=utils.collate_fn)

在转换数据中寻找的要点是:

  • 将数据转换为张量,以便它可以在 PyTorch DAG 中使用。

  • 对于训练,我们正在使用转换或增强技术。对于所有其他目的,例如测试和验证步骤,我们将不使用任何增强技术。

  • 我们从我们在早期阶段构建的自定义数据集类创建火车和测试数据集。

  • 一旦定义了自定义数据,我们将使用它来创建一个可迭代对象,这将直接帮助我们进行训练。

  • 可迭代对象也称为数据加载器

创建数据加载器后,我们可以转到训练部分。我们将定义需要进行培训的设备。我们将定义优化器和学习率调度器。

device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
# 因为我们处理的是人,所以背景类数变为 2
num_classes = 2
final_model = modify_model(num_classes)
# 如果 GPU 不可用,则模型到 GPU 或 CPU
final_model.to(device)
## 获取 SGD 优化器
params = [p for p in final_model.parameters() if p.requires_grad]
optimizer = torch.optim.SGD(params,
                            lr=0.005,
                            momentum=0.9,
                            weight_decay=0.0005)
# 设置学习率
lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer,
                                               step_size=2,
                                               gamma=0.1)

从代码中需要注意的关键点如下:

  • 模型所在的设备应该与训练它的数据相同。模型和数据之间不应有任何跨基础设施的转换。如果数据太大而无法放在一个 GPU 上,则成批数据应与模型驻留在同一系统中。

  • 设置优化器和学习率调度器。

  • 需要注意的是,当我们训练复杂网络时,固定的学习率并不能帮助我们训练得更快或更有效。学习率本身就是我们训练过程中最重要的超参数之一,我们需要小心处理。

现在我们已经设置了模型参数,让我们运行几个时期来检查训练过程。

# 设置 epochs
num_epochs = 5
for epoch in range(num_epochs):
    ## 使用 pytorch 辅助函数本身的 train_one_epoch
    # 习惯微调框架
    train_one_epoch(final_model, optimizer, train_data_loader, device, epoch, print_freq=10)
    # 更新权重和学习率
    lr_scheduler.step()
    # 根据权重的变化得到评估结果
    evaluate(final_model, test_data_loader, device=device)

为了简单起见,我们将模型运行了五个时期,但为了获得更好的结果,您应该运行更长时间。培训过程中最重要的方面之一是检查生成的日志。他们应该让我们对数据如何通过模型运行以及训练过程如何建立有一个公平的理解。让我们快速浏览一下 epoch 生成的日志。

Epoch: [0]  [ 0/60]  eta: 0:02:17  lr: 0.000090  loss: 2.7890 (2.7890)  loss_classifier: 0.7472 (0.7472)  loss_box_reg: 0.3405 (0.3405)  loss_mask: 1.6637 (1.6637)  loss_objectness: 0.0351 (0.0351)  loss_rpn_box_reg: 0.0025 (0.0025)  time: 2.2894  data: 0.4357  max mem: 2161
Epoch: [0]  [10/60]  eta: 0:01:26  lr: 0.000936  loss: 1.3992 (1.7301)  loss_classifier: 0.5175 (0.4831)  loss_box_reg: 0.2951 (0.2971)  loss_mask: 0.7160 (0.9201)  loss_objectness: 0.0279 (0.0249)  loss_rpn_box_reg: 0.0045 (0.0048)  time: 1.7208  data: 0.0469  max mem: 3316
Epoch: [0]  [20/60]  eta: 0:01:05  lr: 0.001783  loss: 1.0006 (1.2323)  loss_classifier: 0.2196 (0.3358)  loss_box_reg: 0.2905 (0.2854)  loss_mask: 0.3228 (0.5877)  loss_objectness: 0.0172 (0.0188)  loss_rpn_box_reg: 0.0042 (0.0045)  time: 1.6055  data: 0.0096  max mem: 3316
Epoch: [0]  [30/60]  eta: 0:00:49  lr: 0.002629  loss: 0.5668 (1.0164)  loss_classifier: 0.0936 (0.2558)  loss_box_reg: 0.2643 (0.2860)  loss_mask: 0.1797 (0.4540)  loss_objectness: 0.0056 (0.0156)  loss_rpn_box_reg: 0.0045 (0.0050)  time: 1.6322  data: 0.0108  max mem: 3316
Epoch: [0]  [40/60]  eta: 0:00:33  lr: 0.003476  loss: 0.4461 (0.8835)  loss_classifier: 0.0639 (0.2070)  loss_box_reg: 0.2200 (0.2681)  loss_mask: 0.1693 (0.3904)  loss_objectness: 0.0028 (0.0126)  loss_rpn_box_reg: 0.0057 (0.0054)  time: 1.6640  data: 0.0107  max mem: 3316
Epoch: [0]  [50/60]  eta: 0:00:16  lr: 0.004323  loss: 0.3779 (0.7842)  loss_classifier: 0.0396 (0.1749)  loss_box_reg: 0.1619 (0.2452)  loss_mask: 0.1670 (0.3476)  loss_objectness: 0.0014 (0.0107)  loss_rpn_box_reg: 0.0051 (0.0058)  time: 1.5650  data: 0.0107  max mem: 3316
Epoch: [0]  [59/60]  eta: 0:00:01  lr: 0.005000  loss: 0.3066 (0.7143)  loss_classifier: 0.0329 (0.1549)  loss_box_reg: 0.1074 (0.2265)  loss_mask: 0.1508 (0.3172)  loss_objectness: 0.0022 (0.0097)  loss_rpn_box_reg: 0.0052 (0.0059)  time: 1.5627  data: 0.0109  max mem: 3316
Epoch: [0] Total time: 0:01:37 (1.6202 s / it)
creating index...
index created!
Test:  [ 0/50]  eta: 0:00:27  model_time: 0.3958 (0.3958)  evaluator_time: 0.0052 (0.0052)  time: 0.5474  data: 0.1449  max mem: 3316
Test:  [49/50]  eta: 0:00:00  model_time: 0.3451 (0.3489)  evaluator_time: 0.0061 (0.0110)  time: 0.3666  data: 0.0055  max mem: 3316
Test: Total time: 0:00:18 (0.3715 s / it)
Averaged stats: model_time: 0.3451 (0.3489)  evaluator_time: 0.0061 (0.0110)
Accumulating evaluation results...
DONE (t=0.01s).
Accumulating evaluation results...
DONE (t=0.01s).
IoU metric: bbox
 Average Precision  (AP) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.690
 Average Precision  (AP) @[ IoU=0.50      | area=   all | maxDets=100 ] = 0.976
 Average Precision  (AP) @[ IoU=0.75      | area=   all | maxDets=100 ] = 0.863
 Average Precision  (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = -1.000
 Average Precision  (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.363
 Average Precision  (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.708
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=  1 ] = 0.311
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets= 10 ] = 0.747
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.747
 Average Recall     (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = -1.000
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.637
 Average Recall     (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.755
IoU metric: segm
 Average Precision  (AP) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.722
 Average Precision  (AP) @[ IoU=0.50      | area=   all | maxDets=100 ] = 0.976
 Average Precision  (AP) @[ IoU=0.75      | area=   all | maxDets=100 ] = 0.886
 Average Precision  (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = -1.000
 Average Precision  (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.448
 Average Precision  (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.740
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=  1 ] = 0.325
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets= 10 ] = 0.760
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.761
 Average Recall     (AR) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = -1.000
 Average Recall     (AR) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.675
 Average Recall     (AR) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.767

以下是检查任何训练网络的日志时需要注意的一些要点:

  • 由于我们使用了逐步学习率变化,因此可以很容易地在日志冗长中注意到它。

  • 可以记录每个批次的分类器损失和客观性损失。

  • 我们应该检查的其他重要方面是平均精度和平均召回率。

  • ETA 和内存分配可以帮助我们估计计算量,以便对模型进行更大规模的评估。

现在我们已经训练了模型,我们可以选择使用torch save 命令来保存模型。我们还可以使用 Save Dictionary 选项,它比只保存 pickled 形式更有优势。当我们保存字典时,我们基本上可以在需要时更改字典,但当我们以 pickle 的形式存储它时,情况可能并非如此。pickle 存储目录路径和模型参数,很难更改或改变。

## 保存模型完整版
## 可以选择状态字典版本的保存
torch.save(final_model, 'mask-rcnn-fine_tuned.pt')

由于我们现在有了一个训练有素的模型,我们可以开始我们的推理了。它将从eval mode开始,它将模型切换到评估模式。不计算梯度。

# pytorch 帮助将模型设置为评估模式
final_model.eval()
CLASSES = ['__background__', 'pedestrian']
device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
final_model.to(device)

这也将帮助我们描述将用于推理的模型。

MaskRCNN(
  (transform): GeneralizedRCNNTransform(
      Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
      Resize(min_size=(800,), max_size=1333, mode=’bilinear’)
  )
  (backbone): BackboneWithFPN(
    (body): IntermediateLayerGetter(
      (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
      (bn1): FrozenBatchNorm2d(64, eps=0.0)
      (relu): ReLU(inplace=True)
      (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
      (layer1): Sequential(
        (0): Bottleneck(
          (conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(64, eps=0.0)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(64, eps=0.0)
          (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(256, eps=0.0)
          (relu): ReLU(inplace=True)
          (downsample): Sequential(
            (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
            (1): FrozenBatchNorm2d(256, eps=0.0)
          )
        )
        (1): Bottleneck(
          (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(64, eps=0.0)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(64, eps=0.0)
          (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(256, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (2): Bottleneck(
          (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(64, eps=0.0)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(64, eps=0.0)
          (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(256, eps=0.0)
          (relu): ReLU(inplace=True)
        )
      )
      (layer2): Sequential(
        (0): Bottleneck(
          (conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(128, eps=0.0)
          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(128, eps=0.0)
          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(512, eps=0.0)
          (relu): ReLU(inplace=True)
          (downsample): Sequential(
            (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
            (1): FrozenBatchNorm2d(512, eps=0.0)
          )
        )
        (1): Bottleneck(
          (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(128, eps=0.0)
          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(128, eps=0.0)
          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(512, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (2): Bottleneck(
          (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(128, eps=0.0)
          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(128, eps=0.0)
          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(512, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (3): Bottleneck(
          (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(128, eps=0.0)
          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(128, eps=0.0)
          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(512, eps=0.0)
          (relu): ReLU(inplace=True)
        )
      )
      (layer3): Sequential(
        (0): Bottleneck(
          (conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
          (downsample): Sequential(
            (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)
            (1): FrozenBatchNorm2d(1024, eps=0.0)
          )
        )
        (1): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (2): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (3): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (4): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (5): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
      )
      (layer4): Sequential(
        (0): Bottleneck(
          (conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(512, eps=0.0)
          (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(512, eps=0.0)
          (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(2048, eps=0.0)
          (relu): ReLU(inplace=True)
          (downsample): Sequential(
            (0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)
            (1): FrozenBatchNorm2d(2048, eps=0.0)
          )
        )
        (1): Bottleneck(
          (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(512, eps=0.0)
          (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(512, eps=0.0)
          (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(2048, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (2): Bottleneck(
          (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(512, eps=0.0)
          (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(512, eps=0.0)
          (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(2048, eps=0.0)
          (relu): ReLU(inplace=True)
        )
      )
    )
    (fpn): FeaturePyramidNetwork(
      (inner_blocks): ModuleList(
        (0): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1))
        (1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1))
        (2): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1))
        (3): Conv2d(2048, 256, kernel_size=(1, 1), stride=(1, 1))
      )
      (layer_blocks): ModuleList(
        (0): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        (1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        (2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        (3): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
      (extra_blocks): LastLevelMaxPool()
    )
  )
  (rpn): RegionProposalNetwork(
    (anchor_generator): AnchorGenerator()
    (head): RPNHead(
      (conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (cls_logits): Conv2d(256, 3, kernel_size=(1, 1), stride=(1, 1))
      (bbox_pred): Conv2d(256, 12, kernel_size=(1, 1), stride=(1, 1))
    )
  )
  (roi_heads): RoIHeads(
    (box_roi_pool): MultiScaleRoIAlign(featmap_names=['0', '1', '2', '3'], output_size=(7, 7), sampling_ratio=2)
    (box_head): TwoMLPHead(
      (fc6): Linear(in_features=12544, out_features=1024, bias=True)
      (fc7): Linear(in_features=1024, out_features=1024, bias=True)
    )
    (box_predictor): FastRCNNPredictor(
      (cls_score): Linear(in_features=1024, out_features=2, bias=True)
      (bbox_pred): Linear(in_features=1024, out_features=8, bias=True)
    )
    (mask_roi_pool): MultiScaleRoIAlign(featmap_names=['0', '1', '2', '3'], output_size=(14, 14), sampling_ratio=2)
    (mask_head): MaskRCNNHeads(
      (mask_fcn1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (relu1): ReLU(inplace=True)
      (mask_fcn2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (relu2): ReLU(inplace=True)
      (mask_fcn3): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (relu3): ReLU(inplace=True)
      (mask_fcn4): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (relu4): ReLU(inplace=True)
    )
    (mask_predictor): MaskRCNNPredictor(
      (conv5_mask): ConvTranspose2d(256, 256, kernel_size=(2, 2), stride=(2, 2))
      (relu): ReLU(inplace=True)
      (mask_fcn_logits): Conv2d(256, 2, kernel_size=(1, 1), stride=(1, 1))
    )
  )
)

这个模型定义可以帮助我们建立我们当前使用的架构以及我们可以围绕它做的改变。完成后让我们再写几行代码来显示掩码。

def get_mask_color(mask_conf):
    ## 生成掩码的辅助函数
    colour_option = [[0, 250, 0],[0, 0, 250],[250, 0, 0],[0, 250, 250],[250, 250, 0],[250, 0, 250],[75, 65, 170],[230, 75, 180],[235, 130, 40],[60, 140, 240],[40, 180, 180]]
    blue = np.zeros_like(mask_conf).astype(np.uint8)
    green = np.zeros_like(mask_conf).astype(np.uint8)
    red = np.zeros_like(mask_conf).astype(np.uint8)
    red[mask_conf == 1], green[mask_conf == 1], blue[mask_conf == 1] = colour_option[random.randrange(0,10)]
    mask_color = np.stack([red, green, blue], axis=2)
    return mask_color
def generate_prediction(image_path, conf):
    ## 生成预测的辅助函数
    image = Image.open(image_path)
    transform = T.Compose([T.ToTensor()])
    image = transform(image)
    image = image.to(device)
    predicted = final_model([image])
    predicted_score = list(predicted[0][’scores’].detach().cpu().numpy())
    predicted_temp = [predicted_score.index(x) for x in predicted_score if x>conf][-1]
    masks = (predicted[0][’masks’]>0.5).squeeze().detach().cpu().numpy()
    # print(pred[0][’labels’].numpy().max())
    predicted_class_val = [CLASSES[i] for i in list(predicted[0]['labels'].cpu().numpy())]
    predicted_box_val = [[(i[0], i[1]), (i[2], i[3])] for i in list(predicted[0]['boxes'].detach().cpu().numpy())]
    masks = masks[:predicted_temp+1]
    predicted_class_name = predicted_class_val[:predicted_temp+1]
    predicted_box_score = predicted_box_val[:predicted_temp+1]
    return masks, predicted_box_score, predicted_class_name
def segment_image(image_path, confidence=0.5, rect_thickness=2, text_size=2, text_thickness=2):
    masks_conf, box_conf, predicted_class = generate_prediction(image_path, confidence)
    image = cv2.imread(image_path)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    for i in range(len(masks_conf)):
      rgb_mask = get_mask_color(masks_conf[i])
      image = cv2.addWeighted(image, 1, rgb_mask, 0.5, 0)
      cv2.rectangle(image, box_conf[i][0], box_conf[i][1],color=(0, 255, 0), thickness=rect_thickness)
      cv2.putText(image,predicted_class[i], box_conf[i][0], cv2.FONT_HERSHEY_SIMPLEX, text_size, (0,255,0),thickness=text_thickness)
    plt.figure(figsize=(20,30))
    plt.imshow(image)
    plt.xticks([])
    plt.yticks([])
    plt.show()
segment_image('/content/pedestrian_img.jpg', confidence=0.7)

概括

本章讨论了图像分割的工作原理及其在市场上的变化。这是解决图像分割问题的一个重要方面。这也通过一个例子建立了微调的概念。展望未来,这里学到的概念将有助于理解计算机视觉的概念。

下一章将着眼于我们如何构建管道来帮助解决涉及图像相似性的业务问题。

你可能感兴趣的:(使用,PyTorch,的计算机视觉项目,pytorch,深度学习)