0%

1

自动摘要: Torchvision对象检测微调教程 为了充分利用本教程,建议使用此[Colab](https://colab.research.google.com/github/pytorch/tuto ……..

Torchvision对象检测微调教程

为了充分利用本教程,建议使用此 Colab 版本, 这将可以尝试以下信息。

在本教程将对 Penn-Fudan 数据库中的行人检测和分割,使用预训练的 Mask R-CNN 模型进行微调。 此数据库包含 170 个图像和 345 个行人实例,本次将用它来说明如何在torchvision中使用新功能,以便在自定义数据集上训练实例细分模型。

定义数据集

用于训练目标检测、实例分割和人员关键点检测的参考脚本可以方便地支持添加新的自定义数据集。 数据集应继承自标准torch.utils.data.Dataset类,并实现__len__和__getitem__。

唯一需要的特异性是数据集__getitem__应该返回:

  • 图像:大小为(H, W)的 PIL 图像
  • 目标:包含以下字段的字典
    • boxes (FloatTensor[N, 4]):[x0, y0, x1, y1]格式的N边界框的坐标,范围从0至W,从0至H;
    • labels (Int64Tensor[N]):每个边界框的标签。 0始终代表背景类;
    • image_id (Int64Tensor[1]):图像标识符。 它在数据集中的所有图像之间应该是唯一的,并在评估过程中使用;
    • area (Tensor[N]):边界框的区域。 在使用 COCO 度量进行评估时,可使用此值来区分小盒子,中盒子和大盒子之间的度量得分。
    • iscrowd (UInt8Tensor[N]):在评估期间,将忽略具有 iscrowd= True 的实例。
    • (可选)masks (UInt8Tensor[N, H, W]):每个对象的分割蒙版
    • (可选)keypoints (FloatTensor[N, K, 3]):对于 N 个对象中的每一个,它包含[x, y, visibility]格式的 K 个关键点,用于定义对象。 可见性为 0 (visibility=0)表示关键点不可见。 请注意,对于数据扩充,翻转关键点的概念取决于数据表示形式,您可能应该将references/detection/transforms.py修改为新的关键点表示形式

如果您的模型返回上述方法,他们将使其既可用于培训,也可用于评估,并将使用 pycotools 中的评估脚本,这些脚本可以与 pip install pycotools 一起安装。

注意:

  • 安装pycotools之前先要安装vs_BuildTools,如图
    。网址:https://visualstudio.microsoft.com/zh-hans/visual-cpp-build-tools/,安装的时候选择C++编辑界面就好。
  • 对于Windows, 请使用gautamchitnis安装pycocotools,命令如下
    1
    pip install git+https://github.com/gautamchitnis/cocoapi.git@cocodataset-master#subdirectory=PythonAPI

关于labels的注解。 该模型将0类作为背景。 如果您的数据集不包含背景类,则labels中不应包含0。 例如,假设您只有猫和狗两类,则可以定义1来表示猫和0代表狗。 因此,例如,如果其中一个图像同时具有两个类,则您的labels张量应类似于[1,2]。

此外,如果要在训练过程中使用宽高比分组(以便每个批量仅包含具有相似长宽比的图像),则建议您还实现get_height_and_width方法,该方法返回图像的高度和宽度。 如果未提供此方法,我们将通过__getitem__查询数据集的所有元素,这会将图像加载到内存中,并且比提供自定义方法慢。

为 PennFudan 编写自定义数据集

让我们为 PennFudan 数据集编写一个数据集。 在下载并解压缩zip文件之后,我们具有以下文件夹结构:

1
2
3
4
5
6
7
8
9
10
11
12
PennFudanPed/
PedMasks/
FudanPed00001_mask.png
FudanPed00002_mask.png
FudanPed00003_mask.png
FudanPed00004_mask.png
...
PNGImages/
FudanPed00001.png
FudanPed00002.png
FudanPed00003.png
FudanPed00004.png

这是一对图像和分割掩码的一个示例:
因此,每个图像都有一个对应的分割蒙版,其中每个颜色对应一个不同的实例。 让我们为此数据集编写一个torch.utils.data.Dataset类。代码下面有注意事项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import os
import numpy as np
import torch
from PIL import Image

class PennFudanDataset(torch.utils.data.Dataset):
def __init__(self,root,transforms):
self.root = root
self.transforms = transforms

#加载所有图像文件,对它们进行排序以确保它们对齐
self.imgs = list(sorted(os.listdir(os.path.join(root,"PNGImages"))))
self.masks = list(sorted(os.listdir(os.path.join(root,"PedMasks"))))

def __getitem__(self, idx):
#获取图片和掩码
img_path = os.path.join(self.root,"PNGImages",self.imgs[idx])
mask_path = os.path.join(self.root,"PedMasks",self.masks[idx])
img = Image.open(img_path).convert("RGB")


#请注意,我们还没有将掩码转换为 RGB,因为每种颜色对应一个不同的实例,其中 0 是背景
mask = Image.open(mask_path)
#将 PIL 图像转换成一个numpy数组
mask = np.array(mask)

#实例被编码为不同的颜色
obj_ids = np.unique(mask)
#第一个 id 是背景,所以删除它
obj_ids = obj_ids[1:]

#将颜色编码的掩码拆分为一组二进制掩码
masks = mask == obj_ids[:,None,None]

#得到每个掩码的边界框坐标
num_objs = len(obj_ids)
boxes = []
for i in range(num_objs):
pos = np.where(masks[i])
xmin = np.min(pos[1])
xmax = np.max(pos[1])
ymin = np.min(pos[0])
ymax = np.max(pos[0])
boxes.append([xmin,ymin,xmax,ymax])

#把所有东西都变成pytorch张量
boxes = torch.as_tensor(boxes,dtype=torch.float32)
#只有一个类
labels = torch.ones((num_objs,),dtype=torch.int64)
masks = torch.as_tensor(masks,dtype=torch.uint8)

image_id = torch.tensor([idx])
area = (boxes[:,3] - boxes[:,1]) * (boxes[:,2] - boxes[:,0])

#假设所有的实例都不是人群
iscrowd = torch.zeros((num_objs,),dtype=torch.int64)

target = {}
target["boxes"] = boxes
target["labels"] = labels
target["image_id"] = mask
target["area"] = area
target["iscrowd"] = iscrowd

if self.transforms is not None:
img,target = self.transforms(img,target)

return img, target

def __len__(self):
return len(self.imgs)

注意事项:代码中的root的格式为:root = ‘C:\Users\sy\Desktop\PennFudanPed’

代码中会用到PIL库,这个库只支持到python 2.7版本,创建了兼容python 3的版本,叫Pillow,可以通过安装Pillow来使用PIL。Pillow这个库一般在安装Anacanda3的时候就安装了,但是调用的时候依旧用不了,以下是解决办法:

1
2
3
4
5
#卸载已经安装好的pillow
pip uninstall pillow

#安装Pillow(此处注意,Pillow开头的P要大写!!!)
pip install Pillow

这就是数据集的全部内容。 现在,定义一个可以对该数据集执行预测的模型。

定义模型

在本教程中,我们将基于 Faster R-CNN 使用 Mask R-CNN 。 Faster R-CNN 是可预测图像中潜在对象的边界框和类分数的模型。

Mask R-CNN 在 Faster R-CNN 中增加了一个分支,该分支还可以预测每个实例的分割掩码。

将预训练模型用于微调

假设您要从在 COCO 上经过预训练的模型开始,并希望针对您的特定类对其进行微调。 这是一种可行的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
import torchvision
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor

#装载一个在 COCO 上预先训练好的模型
model = torchvision.models.detection.fasterrcnn_resnet50_fpn(weights = "DEFAULT")

#用一个新的分类器替换分类器,它具有用户定义的 num_classes
num_classes = 2 #1 class(person) + 背景

#获取输入特征数给分类器
in_features = model.roi_heads.box_predictor.cls_score.in_features

model.roi_heads.box_predictor = FastRCNNPredictor(in_features,num_classes)

修改模型以添加其他主干

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import torchvision
from torchvision.models.detection import FasterRCNN
from torchvision.models.detection.rpn import AnchorGenerator

#加载预训练模型进行分类并仅返回特征
backbone = torchvision.models.mobilenet_v2(weights='DEFAULT').features

#FasterRCNN 需要知道骨干网中输出通道的数量。对于mobilenet_v2,它是1280,所以我们需要在这里添加它
backbone.out_channels = 1280

#让我们让 RPN 每个空间位置生成 5 x 3 个锚点,具有 5 个不同的大小和 3 个不同的纵横比。我们有一个 Tuple[Tuple[int]] 因为每个特征图可能有不同的大小和纵横比
anchor_generator = AnchorGenerator(sizes=((32,64,128,256,512),),aspect_ratios=((0.5,1.0,2.0),))

#让我们定义我们将用于执行感兴趣区域裁剪的特征图,以及重新缩放后裁剪的大小。如果你的主干返回一个张量,featmap_names 应该是 [0]。
# 更一般地说,主干应该返回一个 OrderedDict[Tensor],并且在 featmap_names 中您可以选择要使用的特征图。
roi_pooler = torchvision.ops.MultiScaleRoIAlign(featmap_names=['0'],output_size=7,sampling_ratio=2)

#在 FasterRCNN 模型中把这些碎片组合在一起
model = FasterRCNN(backbone,num_classes=2,rpn_anchor_generator=anchor_generator,box_roi_pool=roi_pooler)

第一次运行的输出结果,再次运行就没有了:

1
2
Downloading: "https://download.pytorch.org/models/mobilenet_v2-7ebf99e0.pth" to C:\Users\sy/.cache\torch\hub\checkpoints\mobilenet_v2-7ebf99e0.pth
100%|██████████| 13.6M/13.6M [00:01<00:00, 7.73MB/s]

PennFudan数据集的实例细分模型

在此案例中,由于数据集非常小,希望从预训练模型中进行微调,因此本次将遵循方法1。这里将使用Mask R-CNN计算实例分割掩码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torchvision
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
from torchvision.models.detection.mask_rcnn import MaskRCNNPredictor

def get_model_instance_segmentation(num_classes):
#加载一个在 COCO 上预训练的实例分割模型
model = torchvision.models.detection.maskrcnn_resnet50_fpn(weight="DEFAULT")

#获取分类器的输入特征数
in_features = model.roi_heads.box_predictor.cls_score.in_features
print(in_features)
#用一个新的pre-trained替换预先训练过的pre-trained
model.roi_heads.box.mask_predictor = FastRCNNPredictor(in_features,num_classes)

#获取输入掩码分类器的特征数
in_features_mask = model.roi_heads.mask_predictor.conv5_mask.in_channels
hidden_layer = 256

#用一个新的替换掩码预测值
model.roi_heas.mask_predictor = MaskRCNNPredictor(in_features_mask,
hidden_layer,
num_classes)
return model

Putting everything together

在references/detection/中,我们提供了许多帮助程序功能来简化训练和评估检测模型。 在这里,我们将使用references/detection/engine.py,references/detection/utils.py和references/detection/transforms.py。 只需将它们复制到您的文件夹中,然后在此处使用它们即可。网址:https://github.com/pytorch/vision/tree/main/references/detection

写一些辅助函数来进行数据扩充/转换:

1
2
3
4
5
6
7
8
9
10
import torch
from torchvision import transforms as T

def get_transform(train):
transforms = []
transforms.append(T.PILToTensor())
transforms.append(T.ConvertImageDtype(torch.float))
if train:
transforms.append(T.RandomHorizontalFlip(0.5))
return T.Compose(transforms)

测试forward()方法(可选)

在遍历数据集之前,最好先查看模型在训练过程中的期望值以及对样本数据的推断时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import torchvision


model = torchvision.models.detection.fasterrcnn_resnet50_fpn(weights="DEFAULT")
dataset = PennFudanDataset('PennFudanPed',get_transform(train=True))
data_loader = torch.utils.data.DataLoader(
dataset, batch_size=2, shuffle=True, num_workers=4,
collate_fn=torch.utils.collate_fn)

images,targets = next(iter(data_loader))
images = list(image for image in images)
targets = [{k: v for k,v in t.items()} for t in targets]
output = model(images,targets)

model.eval()
x = [torch.rand(3,300,400), torch.rand(3,500,400)]
predictions = model(x)

现在,编写执行训练和验证的main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from engine import train_one_epoch, evaluate
import utils


def main():
# train on the GPU or on the CPU, if a GPU is not available
device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')

# our dataset has two classes only - background and person
num_classes = 2
# use our dataset and defined transformations
dataset = PennFudanDataset('PennFudanPed', get_transform(train=True))
dataset_test = PennFudanDataset('PennFudanPed', get_transform(train=False))

# split the dataset in train and test set
indices = torch.randperm(len(dataset)).tolist()
dataset = torch.utils.data.Subset(dataset, indices[:-50])
dataset_test = torch.utils.data.Subset(dataset_test, indices[-50:])

# define training and validation data loaders
data_loader = torch.utils.data.DataLoader(
dataset, batch_size=2, shuffle=True, num_workers=4,
collate_fn=utils.collate_fn)

data_loader_test = torch.utils.data.DataLoader(
dataset_test, batch_size=1, shuffle=False, num_workers=4,
collate_fn=utils.collate_fn)

# get the model using our helper function
model = get_model_instance_segmentation(num_classes)

# move model to the right device
model.to(device)

# construct an optimizer
params = [p for p in model.parameters() if p.requires_grad]
optimizer = torch.optim.SGD(params, lr=0.005,
momentum=0.9, weight_decay=0.0005)
# and a learning rate scheduler
lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer,
step_size=3,
gamma=0.1)

# let's train it for 10 epochs
num_epochs = 10

for epoch in range(num_epochs):
# train for one epoch, printing every 10 iterations
train_one_epoch(model, optimizer, data_loader, device, epoch, print_freq=10)
# update the learning rate
lr_scheduler.step()
# evaluate on the test dataset
evaluate(model, data_loader_test, device=device)

print("That's it!")

结束语

在此处下载本教程的完整源文件

计算机视觉的迁移学习教程

本教程将学习如何使用迁移学习训练卷积神经网络进行图像分类。 您可以在cs231n 笔记中阅读有关迁移学习的更多信息。

引用注解:实际上,很少有人从头开始训练整个卷积网络(使用随机初始化),因为拥有足够大小的数据集相对很少。 相反,通常在非常大的数据集上对 ConvNet 进行预训练(例如 ImageNet,其中包含 120 万个具有 1000 个类别的图像),然后将 ConvNet 用作初始化或固定特征提取器以完成感兴趣的任务。

两个主要的迁移学习方案如下所示:

  • 卷积网络的微调:代替随机初始化,我们使用经过预训练的网络初始化网络,例如在 imagenet 1000 数据集上进行训练的网络。 其余的训练照常进行。
  • 作为固定特征提取器的 ConvNet:在这里,我们将冻结除最终全连接层之外的所有网络的权重。 最后一个全连接层将替换为具有随机权重的新层,并且仅训练该层。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from __future__ import print_function,division

import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim import lr_scheduler
import torch.backends.cudnn as cudnn
import numpy as np
import torchvision
from torchvision import datasets,models,transforms
import matplotlib.pyplot as plt
import time
import os
import copy

print(plt.ion()) #交互式

输出结果:

1
<matplotlib.pyplot._IonContext object at 0x000001E0FBBAE640>

加载数据

本次将使用torchvision和torch.utils.data包来加载数据,需要解决的问题是训练一个模型来对蚂蚁和蜜蜂进行分类。我们有大约120张蚂蚁和蜜蜂的训练图像。每个类有 75 个验证图像。通常,这是一个非常小的数据集,如果从头开始训练,则可以对其进行概括。由于我们使用的是迁移学习,我们应该能够很好地泛化。

此次使用的数据集是图像网格的一个非常小的子集。从此处下载数据集并解压到当前目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
data_transforms = {
'train': transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])
]),
'val': transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])
]),
}

data_dir = 'data/hymenoptera_data'

image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
data_transforms[x])
for x in ['train','val']}
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x],batch_size=4,shuffle=True,num_workers=4)
for x in ['train','val']}

dataset_sizes = {x:len(image_datasets[x]) for x in ['train','val']}
class_names = image_datasets['train'].classes

device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

可视化一些图像

函数imshow()代码块一定要放在 ‘main‘函数内调用否则会出问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def imshow(inp, title=None):
inp = inp.numpy().transpose((1, 2, 0))
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
inp = std * inp + mean
inp = np.clip(inp, 0, 1)
plt.imshow(inp)
if title is not None:
plt.title(title)
plt.pause(0.001) # 暂停一会儿更新
if __name__ == '__main__':
# 得到一批训练数据
input,classes = next(iter(dataloaders['train']))
out = torchvision.utils.make_grid(input)
imshow(out,title=[class_names[x] for x in classes])

输出结果是一张图片:

训练模型

现在编写一个常规函数来训练模型,说明:

  • 安排学习率
  • 保存最佳模型

以下,参数scheduler是来自torch.optim.lr_scheduler的 LR 调度器对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def train_model(model,criterion,optimizer,scheduler,num_epochs=25):
since = time.time()

bast_model_wts = copy.deepcopy(model.state_dict())
best_acc = 0.0

for epoch in range(num_epochs):
print(f'Epoch {epoch}/{num_epochs - 1}')
print('-' * 10)

#每个epoch都有一个训练和验证阶段
for phase in ['train','val']:
if phase == 'train':
model.train() #将模型设置为训练模式
else:
model.eval() #将模型设置为验证模式

running_loss = 0.0
running_corrects = 0

#遍历数据
for input, labels in dataloaders[phase]:
inputs = input.to(device)
labels = labels.to(device)

optimizer.zero_grad()

with torch.set_grad_enabled(phase == 'train'):
outputs = model(inputs)
_, preds = torch.max(outputs,1) #这里的_表示这个值不重要用_表示
loss = criterion(outputs,labels)

#仅在训练阶段进行逆向 + 优化
if phase == 'train':
loss.backward()
optimizer.step()

#统计
running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds == labels.data)
if phase == 'train':
scheduler.step()

epoch_loss = running_loss / dataset_sizes[phase]
epoch_acc = running_corrects.double() / dataset_sizes[phase]

print(f'{phase} Loss: {epoch_loss:.4f} Acc:{epoch_acc:.4f}')

# deep copy the model
if phase == 'val' and epoch_acc > best_acc:
best_acc = epoch_acc
best_model_wts = copy.deepcopy(model.state_dict())

print()

time_elapsed = time.time() - since
print(f'training complete in {time_elapsed // 60:.0f}m {time_elapsed % 60:.0f}s')
print(f'bast val Acc: {best_acc:4f}')

#获取最佳模型权重
model.load_state_dict(best_model_wts)
return model

可视化模型预测

通用函数,显示一些图像的预测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def visualize_model(model,num_images=6):
was_training = model.training
model.eval()
images_so_far = 0
fig = plt.figure()

with torch.no_grad():
for i,(inputs,labels) in enumerate(dataloaders['val']):
inputs = inputs.to(device)
labels = labels.to(device)

outputs = model(inputs)
_,preds = torch.max(outputs,1)

for j in range(inputs.size()[0]):
images_so_far += 1
ax = plt.subplot(num_images//2, 2, images_so_far)
ax.axis('off')
ax.set_title(f'predicted: {class_names[preds[j]]}')
imshow(inputs.cpu().data[j])

if images_so_far == num_images:
model.train(mode=was_training)
return
model.train(model=was_training)

微调 ConvNet

加载预训练的模型并重置最终的全连接层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
model_ft = models.resnet18(pretrained=True)
num_ftrs = model_ft.fc.in_features

#这里每个输出样本的大小被设置为2。
#另外,它可以通用于nn.Linear(num_ftrs, len(class_names))

model_ft.fc = nn.Linear(num_ftrs,2)

model_ft = model_ft.to(device)

criterion = nn.CrossEntropyLoss()

#所有参数都在进行优化
optimizer_ft = optim.SGD(model_ft.parameters(),lr=0.001,momentum=0.9)

#每7个纪元衰减 LR 的因子为0.1
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft,step_size=7,gamma=0.1)

输出结果,只有第一次运行时会下载:

1
2
3
4
5
6
C:\ProgramData\Anaconda3\lib\site-packages\torchvision\models\_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and will be removed in 0.15, please use 'weights' instead.
warnings.warn(
C:\ProgramData\Anaconda3\lib\site-packages\torchvision\models\_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and will be removed in 0.15. The current behavior is equivalent to passing `weights=ResNet18_Weights.IMAGENET1K_V1`. You can also use `weights=ResNet18_Weights.DEFAULT` to get the most up-to-date weights.
warnings.warn(msg)
Downloading: "https://download.pytorch.org/models/resnet18-f37072fd.pth" to C:\Users\sy/.cache\torch\hub\checkpoints\resnet18-f37072fd.pth
100%|██████████| 44.7M/44.7M [00:04<00:00, 11.5MB/s]

欢迎关注我的其它发布渠道