前言
前面我们讲解了CNN,这章我们主要说一些图像增广以及计算机视觉相关 的方向。
一、图像增广
图像增广(Image augmentation) 是一种在训练过程中扩充图像数据集的技术。它通过对原始图像应用一系列随机变换,生成新的训练样本,以增加数据的多样性和数量。
图像增广可以帮助解决训练数据不足的问题,扩展数据集的规模,有效提升模型的泛化能力和鲁棒性。
常见的图像增广操作包括:随机旋转、水平或垂直翻转、随机裁剪、缩放、平移、调整亮度和对比度、加入噪声等。
通过图像增广,可以提升模型的泛化能力、减少过拟合问题,并且对于目标检测、图像分类、语义分割等任务都是一种有效的数据扩充方法。
1.1 常用的图像增广
我们以一张1080x1439的图像作为示例:
%matplotlib inline
import torch
import torchvision
from torch import nn
from d2l import torch as d2l
d2l.set_figsize()
img = d2l.Image.open('./assets/dog.jpg')
print(img)
d2l.plt.imshow(img)
为了方便我们观察结果,我们定义一个函数,该函数可以对同一图像多次进行增广。
def apply(img, aug, num_rows=2, num_cols=4, scale=2):
Y = [aug(img) for _ in range(num_rows * num_cols)]
d2l.show_images(Y, num_rows, num_cols, scale=scale)
1.1.1 翻转和裁剪
torchvision.transforms.RandomHorizontalFlip()
可以对图像进行随机水平翻转
apply(img, torchvision.transforms.RandomHorizontalFlip())
torchvision.transforms.RandomVerticalFlip()
同理,随机垂直翻转。
apply(img, torchvision.transforms.RandomVerticalFlip())
torchvision.transforms.RandomResizedCrop
是PyTorch中的一个数据预处理函数,用于对图像进行随机裁剪和缩放的操作。
它的用法如下所示:
torchvision.transforms.RandomResizedCrop(size, scale=(0.08, 1.0), ratio=(0.75, 1.333), interpolation=2)
参数含义如下:
-
size
:裁剪后的图像大小,可以是一个整数或一个元组(height, width)
。 -
scale
:图像被裁剪后的面积相对于原图像面积的比例范围。 -
ratio
:裁剪后的宽高比范围。 -
interpolation
:插值方法,默认为PIL.Image.BILINEAR
。
当图像被输入时,RandomResizedCrop
首先会根据 scale
和 ratio
的范围选取一个随机的裁剪区域,然后将该区域进行缩放,使其大小与 size
参数指定的大小相匹配。最后,裁剪后的图像将会被返回。
shape_aug = torchvision.transforms.RandomResizedCrop(
(200, 200), scale=(0.1, 1), ratio=(0.5, 2)
)
apply(img, shape_aug)
1.1.2 变换颜色
我们可以改变图像颜色的四个方面:亮度、对比度、饱和度和色调。
torchvision.transforms.ColorJitter
是PyTorch中的一个数据预处理函数,用于对图像进行颜色增强的操作。
它的用法如下所示:
torchvision.transforms.ColorJitter(brightness=0, contrast=0, saturation=0, hue=0)
参数含义如下:
-
brightness
:亮度调整的范围,可以是一个浮点数或一个长度为2的元组(min, max)
,默认为0。 -
contrast
:对比度调整的范围,可以是一个浮点数或一个长度为2的元组(min, max)
,默认为0。 -
saturation
:饱和度调整的范围,可以是一个浮点数或一个长度为2的元组(min, max)
,默认为0。 -
hue
:色调调整的范围,可以是一个浮点数或一个长度为2的元组(-max, max)
,默认为0。
color_aug = torchvision.transforms.ColorJitter(
brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5)
apply(img, color_aug)
1.1.3 结合多种图像增广方法
我们可以通过使用一个Compose实例来综合上面定义的不同的图像增广方法,并将它们应用到每个图像。
augs = torchvision.transforms.Compose([
torchvision.transforms.RandomHorizontalFlip(), color_aug, shape_aug])
apply(img, augs)
二、微调
微调(fine-tuning)在计算机视觉中是一种常用的技术,用于通过调整已经在大型数据集上预训练的模型,使其适应特定任务或数据集。
2.1 微调的步骤
微调的步骤通常包括以下几个阶段:
-
预训练模型:首先,选择一个在大型数据集上预训练的模型作为基础。这样的预训练模型通常在庞大的图像数据集上进行了数十甚至上百万次的训练,并且已经学习到了一些普遍的视觉特征。
-
冻结部分层:在微调过程中,通常将预训练模型的一部分或全部层参数冻结(即保持其权重不变),以保留其学到的通用特征提取能力。一般来说,低层次的卷积层通常保持冻结,而较高层次的卷积层和全连接层则可以进行微调。 也可以不冻结,但采用较小的学习率以及迭代次数。
-
替换输出层:将原始的输出层(通常是针对预训练模型的分类任务设计的层)替换为适合特定任务的新输出层。新的输出层的结构和类别数目必须与目标任务相匹配。
-
重新训练:使用特定任务的标注数据集对模型进行重新训练。在这个阶段,只有新的输出层的权重会被更新,而冻结的层的权重保持不变。
-
调整权重:一旦对新的输出层进行了训练,可以逐步解冻其他卷积层和全连接层,并对它们的权重进行微调。这样可以利用目标任务的数据来优化这些层的特征表示。
微调的目的是通过在特定任务上进行有针对性的训练,改进预训练模型的性能。相对于从零开始训练一个完整的模型,微调可以更快地收敛并获得更好的结果。然而,微调可能需要更多的标注数据,并且需要根据具体任务进行适当的调整和实验,以获得最佳的性能。
2.2 具体案例
%matplotlib inline
import os
import torch
import torchvision
from torch import nn
from d2l import torch as d2l
下载 并解压数据集
#@save
d2l.DATA_HUB['hotdog'] = (d2l.DATA_URL + 'hotdog.zip',
'fba480ffa8aa7e0febbb511d181409f899b9baa5')
data_dir = d2l.download_extract('hotdog')
data_dir
'../data/hotdog'
构建数据集:
train_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'train'))
test_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'test'))
查看部分数据集:
hotdogs = [train_imgs[i][0] for i in range(8)]
not_hotdogs = [train_imgs[-i - 1][0] for i in range(8)]
d2l.show_images(hotdogs + not_hotdogs, 2, 8, scale=1.4);
构建增广以及正则化
# 使用RGB通道的均值和标准差,以标准化每个通道
normalize = torchvision.transforms.Normalize(
[0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
train_augs = torchvision.transforms.Compose([
torchvision.transforms.RandomResizedCrop(224),
torchvision.transforms.RandomHorizontalFlip(),
torchvision.transforms.ToTensor(),
normalize])
test_augs = torchvision.transforms.Compose([
torchvision.transforms.Resize([256, 256]),
torchvision.transforms.CenterCrop(224),
torchvision.transforms.ToTensor(),
normalize])
访问resnet18中的最后一层:
pretrained_net = torchvision.models.resnet18(pretrained=True)
pretrained_net.fc #最后一层
Linear(in_features=512, out_features=1000, bias=True)
'''修改最后一层 以实现二分类任务'''
finetune_net = torchvision.models.resnet18(pretrained=True)
finetune_net.fc = nn.Linear(finetune_net.fc.in_features, 2)
nn.init.xavier_uniform_(finetune_net.fc.weight)
Parameter containing:
tensor([[-0.0517, -0.1011, 0.0124, ..., 0.1000, -0.0545, 0.0062],
[ 0.0955, 0.0561, 0.0096, ..., 0.0597, 0.0575, 0.0123]],
requires_grad=True)
# 如果param_group=True,输出层中的模型参数将使用十倍的学习率
def train_fine_tuning(net, learning_rate, batch_size=128, num_epochs=5,
param_group=True):
train_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'train'), transform=train_augs),
batch_size=batch_size, shuffle=True)
test_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(
os.path.join(data_dir, 'test'), transform=test_augs),
batch_size=batch_size)
devices = d2l.try_all_gpus()
loss = nn.CrossEntropyLoss(reduction="none")
if param_group:
params_1x = [param for name, param in net.named_parameters() if name not in ["fc.weight", "fc.bias"]]
trainer = torch.optim.SGD([{'params': params_1x},
{'params': net.fc.parameters(),
'lr': learning_rate * 10}],
lr=learning_rate, weight_decay=0.001)
else:
trainer = torch.optim.SGD(net.parameters(), lr=learning_rate,
weight_decay=0.001)
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
devices)
train_fine_tuning(finetune_net, 5e-5)
loss 0.177, train acc 0.932, test acc 0.943
968.4 examples/sec on [device(type='cuda', index=0), device(type='cuda', index=1)]
训练时的注意事项:
- 是一个目标数据集上的正常训练任务,但使用更强的正则化
- 使用更小的学习率
- 使用更少的数据迭代
- 源数据集远复杂于目标数据集,通常微调效果更好
三、 目标检测和边界框
在图像分类任务中,我们假设图像中只有一个主要物体对象,我们只关注如何识别其类别。 然而,很多时候图像里有多个我们感兴趣的目标,我们不仅想知道它们的类别,还想得到它们在图像中的具体位置。 在计算机视觉里,我们将这类任务称为目标检测(object detection) 或目标识别(object recognition)。
首先引入一张多个目标的图片。
%matplotlib inline
import torch
from d2l import torch as d2l
d2l.set_figsize()
img = d2l.plt.imread('./assets/dog.jpg')
d2l.plt.imshow(img);
3.1 边界框
在目标检测中,我们通常使用边界框(bounding box) 来描述对象的空间位置。
有两张方式来定位边界框的位置:
- 给定矩形框的左上角与左下角坐标点。
- 给定矩形框的中心位置以及框的宽高。
可以定义两个函数,转换两种表示法:
#@save
def box_corner_to_center(boxes):
"""从(左上,右下)转换到(中间,宽度,高度)"""
x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
cx = (x1 + x2) / 2
cy = (y1 + y2) / 2
w = x2 - x1
h = y2 - y1
boxes = torch.stack((cx, cy, w, h), axis=-1)
return boxes
#@save
def box_center_to_corner(boxes):
"""从(中间,宽度,高度)转换到(左上,右下)"""
cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
x1 = cx - 0.5 * w
y1 = cy - 0.5 * h
x2 = cx + 0.5 * w
y2 = cy + 0.5 * h
boxes = torch.stack((x1, y1, x2, y2), axis=-1)
return boxes
我们来尝试画出狗与猫的位置。
# bbox是边界框的英文缩写
dog_bbox, cat_bbox = [60.0, 45.0, 378.0, 516.0], [400.0, 112.0, 655.0, 493.0]
#@save
def bbox_to_rect(bbox, color): #该函数能将边界框转成matplotlib格式
# 将边界框(左上x,左上y,右下x,右下y)格式转换成matplotlib格式:
# ((左上x,左上y),宽,高)
return d2l.plt.Rectangle(
xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1],
fill=False, edgecolor=color, linewidth=2)
fig = d2l.plt.imshow(img)
fig.axes.add_patch(bbox_to_rect(dog_bbox, 'blue'))
fig.axes.add_patch(bbox_to_rect(cat_bbox, 'red'));
四、锚框
目标检测算法通常会在输入图像中采样大量的区域,然后判断这些区域中是否包含我们感兴趣的目标,并调整区域边界从而更准确地预测目标的真实边界框(ground-truth bounding box)。 不同的模型使用的区域采样方法可能不同。 这里我们介绍其中的一种方法:以每个像素为中心,生成多个缩放比和宽高比(aspect ratio)不同的边界框。 这些边界框被称为锚框(anchor box)
五、多尺度目标检测
六、目标检测数据集
七、单发多框检测(SSD)
八、区域卷积神经网络(R-CNN)系列
区域卷积神经网络(Region-based Convolutional Neural Network,R-CNN) 是一个用于目标检测的计算机视觉算法。它通过将图像分割成不同的区域,然后对每个区域进行特征提取和分类,从而实现对图像中多个物体的检测和定位。
8.1 R-CNN
R-CNN首先从输入图像中选取若干(例如2000个)提议区域(如锚框也是一种选取方法),并标注它们的类别和边界框(如偏移量)。然后,用卷积神经网络对每个提议区域进行前向传播以抽取其特征。 接下来,我们用每个提议区域的特征来预测类别和边界框。
具体步骤如下:
-
对输入图像使用选择性搜索来选取多个高质量的提议区域。这些提议区域通常是在多个尺度下选取的,并具有不同的形状和大小。每个提议区域都将被标注类别和真实边界框;
-
选择一个预训练的卷积神经网络,并将其在输出层之前截断。将每个提议区域变形为网络需要的输入尺寸,并通过前向传播输出抽取的提议区域特征;
-
将每个提议区域的特征连同其标注的类别作为一个样本。训练多个支持向量机对目标分类,其中每个支持向量机用来判断样本是否属于某一个类别;
-
将每个提议区域的特征连同其标注的边界框作为一个样本,训练线性回归模型来预测真实边界框。
尽管R-CNN模型通过预训练的卷积神经网络有效地抽取了图像特征,但它的速度很慢。 想象一下,我们可能从一张图像中选出上千个提议区域,这需要上千次的卷积神经网络的前向传播来执行目标检测。 这种庞大的计算量使得R-CNN在现实世界中难以被广泛应用。
8.2 Fast R-CNN
R-CNN的主要性能瓶颈在于,对每个提议区域,卷积神经网络的前向传播是独立的,而没有共享计算。 由于这些区域通常有重叠,独立的特征抽取会导致重复的计算。
Fast R-CNN (Girshick, 2015)对R-CNN的主要改进之一,是仅在整张图象上执行卷积神经网络的前向传播。 然后再作选择性搜索和兴趣区域池化层。
具体步骤如下:
-
输入图像和候选区域生成:与R-CNN相同,首先对输入图像进行候选区域生成。可以使用选择性搜索或使用其他更快速的算法,例如候选区域网络(Region Proposal Network,RPN)。
-
特征提取和共享:对整个图像使用卷积神经网络(CNN)进行特征提取,通常可以使用预训练的CNN模型,如VGGNet或ResNet。不同于R-CNN,Fast R-CNN通过在整个图像上进行一次特征提取,而不是每个候选区域单独进行提取,从而实现特征共享。
-
ROIPooling层:对于每个候选区域,使用ROIPooling层将其映射到固定大小的特征图上。ROIPooling层根据候选区域的大小和特征图的大小,将其划分成固定大小的区域,并对每个区域进行最大池化,以获取固定长度的特征向量。
-
特征向量分类:将ROIPooling层输出的特征向量输入到全连接层,并通过softmax分类器对其进行分类。分类器通常由多个全连接层和一个softmax层构成,用于预测候选区域中是否存在目标物体以及目标物体的类别。
-
边界框回归:对于被分类为包含目标物体的候选区域,使用回归算法来调整该区域的边界框,以更准确地定位目标物体。
ROI汇聚层的运算规则如下:
例如
在4×4的输入中,我们选取了左上角3×3的兴趣区域。 对于该兴趣区域,我们通过2×2的兴趣区域汇聚层得到一个2×2的输出。 请注意,四个划分后的子窗口中分别含有元素0、1、4、5(5最大);2、6(6最大);8、9(9最大);以及10。
8.3 Faster R-CNN
Faster R-CNN是对Fast R-CNN进一步改进的目标检测算法,其主要改进在于引入了Region Proposal Network(RPN),从而实现了端到端的目标检测。
以下是Faster R-CNN模型的主要步骤:
-
输入图像和特征提取:与Fast R-CNN相同,首先对输入图像使用卷积神经网络(CNN)进行特征提取。
-
RPN:在特征提取的基础上,引入RPN网络用于生成候选的目标框(即候选区域),RPN网络通过卷积操作在不同尺度和长宽比的锚点上进行滑动窗口操作,根据锚点的特征提取结果预测目标框的位置和是否包含目标物体。
-
ROI Pooling:对于每个候选区域,使用ROI Pooling将其映射到固定大小的特征图上,得到固定长度的特征向量。
-
特征向量分类和边界框回归:将ROI Pooling层输出的特征向量输入到分类器和回归器中进行分类和位置微调。分类器由多个全连接层和一个softmax层构成,用于预测候选区域中是否存在目标物体以及目标物体的类别。回归器则用于调整候选区域的边界框以更准确地定位目标物体。
Faster R-CNN的特点在于引入了RPN网络,使得目标检测过程可以端到端地训练,从而减少了额外的计算开销和多个独立组件的耦合。相比于Fast R-CNN,Faster R-CNN具有更快的速度和更准确的检测效果。
8.4 Mask R-CNN
Mask R-CNN是一种对Faster R-CNN的进一步改进,它在目标检测的基础上添加了对目标实例的像素级分割能力,即生成目标的精确掩码。
以下是Mask R-CNN模型的主要步骤:
-
输入图像和特征提取:与Faster R-CNN相同,首先对输入图像使用卷积神经网络(CNN)进行特征提取。
-
RPN:在特征提取的基础上,使用Region Proposal Network(RPN)生成候选的目标框(候选区域)。
-
ROIAlign:对于每个候选区域,使用ROIAlign层将其映射到固定大小的特征图上,得到固定尺寸的候选区域特征。与传统的ROIPooling不同,ROIAlign通过双线性插值的方式更精确地对齐像素级别的特征。
-
分类和边界框回归:将ROIAlign层输出的特征向量分别输入到分类器和回归器中进行分类和位置微调,得到候选区域的类别和位置信息。
-
分割掩码生成:在特征提取的基础上添加一个分割头(segmentation head),通过附加的卷积神经网络对每个候选区域的特征进行预测,生成目标实例的像素级掩码。
Mask R-CNN允许在目标检测的同时,实现对目标实例的精确像素级分割,从而能够更准确地理解和定位图像中的目标物体。这使得Mask R-CNN在许多需要像素级别分割的任务上表现出色,如图像标注、语义分割、实例分割等。
九、语义分割和数据集
本节将探讨语义分割(semantic segmentation)问题,它重点关注于如何将图像分割成属于不同语义类别的区域。 与目标检测不同,语义分割可以识别并理解图像中每一个像素的内容:其语义区域的标注和预测是像素级的。 下图展示了语义分割中图像有关狗、猫和背景的标签。 与目标检测相比,语义分割标注的像素级的边框显然更加精细。
9.1 图像分割和实例分割
计算机视觉领域还有2个与语义分割相似的重要问题,即图像分割(image segmentation)和实例分割(instance segmentation)。 我们在这里将它们同语义分割简单区分一下
-
图像分割将图像划分为若干组成区域,这类问题的方法通常利用图像中像素之间的相关性。它在训练时不需要有关图像像素的标签信息,在预测时也无法保证分割出的区域具有我们希望得到的语义。以上图中的图像作为输入,图像分割可能会将狗分为两个区域:一个覆盖以黑色为主的嘴和眼睛,另一个覆盖以黄色为主的其余部分身体。
-
实例分割也叫同时检测并分割(simultaneous detection and segmentation),它研究如何识别图像中各个目标实例的像素级区域。与语义分割不同,实例分割不仅需要区分语义,还要区分不同的目标实例。例如,如果图像中有两条狗,则实例分割需要区分像素属于的两条狗中的哪一条。
9.2 Pascal VOC2012 语义分割数据集
%matplotlib inline
import os
import torch
import torchvision
from d2l import torch as d2l
下载数据集
#@save
d2l.DATA_HUB['voc2012'] = (d2l.DATA_URL + 'VOCtrainval_11-May-2012.tar',
'4e443f8a2eca6b1dac8a6c57641b67dd40621a49')
voc_dir = d2l.download_extract('voc2012', 'VOCdevkit/VOC2012')
我们使用read_voc_images函数把样本与标签通过编号进行对应读取。
#@save
def read_voc_images(voc_dir, is_train=True):
"""读取所有VOC图像并标注"""
txt_fname = os.path.join(voc_dir, 'ImageSets', 'Segmentation',
'train.txt' if is_train else 'val.txt')
mode = torchvision.io.image.ImageReadMode.RGB
with open(txt_fname, 'r') as f:
images = f.read().split()
features, labels = [], []
for i, fname in enumerate(images):
features.append(torchvision.io.read_image(os.path.join(
voc_dir, 'JPEGImages', f'{fname}.jpg')))
labels.append(torchvision.io.read_image(os.path.join(
voc_dir, 'SegmentationClass' ,f'{fname}.png'), mode))
return features, labels
train_features, train_labels = read_voc_images(voc_dir, True)
下面我们绘制前5个输入图像及其标签。 在标签图像中,白色和黑色分别表示边框和背景,而其他颜色则对应不同的类别。
n = 5
imgs = train_features[0:n] + train_labels[0:n]
imgs = [img.permute(1,2,0) for img in imgs] #把通道的顺序切换到后面 构成
d2l.show_images(imgs, 2, n);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yB77077E-1688608561566)(%E8%AF%AD%E4%B9%89%E5%88%86%E5%89%B2%E5%92%8C%E6%95%B0%E6%8D%AE%E9%9B%86_files/%E8%AF%AD%E4%B9%89%E5%88%86%E5%89%B2%E5%92%8C%E6%95%B0%E6%8D%AE%E9%9B%86_6_0.png)]
接下来,我们列举RGB颜色值和类名。
#@save
VOC_COLORMAP = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],
[0, 0, 128], [128, 0, 128], [0, 128, 128], [128, 128, 128],
[64, 0, 0], [192, 0, 0], [64, 128, 0], [192, 128, 0],
[64, 0, 128], [192, 0, 128], [64, 128, 128], [192, 128, 128],
[0, 64, 0], [128, 64, 0], [0, 192, 0], [128, 192, 0],
[0, 64, 128]]
#@save
VOC_CLASSES = ['background', 'aeroplane', 'bicycle', 'bird', 'boat',
'bottle', 'bus', 'car', 'cat', 'chair', 'cow',
'diningtable', 'dog', 'horse', 'motorbike', 'person',
'potted plant', 'sheep', 'sofa', 'train', 'tv/monitor']
通过上面定义的两个常量,我们可以方便地查找标签中每个像素的类索引。 我们定义了voc_colormap2label函数来构建从上述RGB颜色值到类别索引的映射,而voc_label_indices函数将RGB值映射到在Pascal VOC2012数据集中的类别索引。
#@save
def voc_colormap2label():
"""构建从RGB到VOC类别索引的映射"""
colormap2label = torch.zeros(256 ** 3, dtype=torch.long)
for i, colormap in enumerate(VOC_COLORMAP):
colormap2label[
(colormap[0] * 256 + colormap[1]) * 256 + colormap[2]] = i #相当于把RGB像素通过从左到右的一个256进制转成10进制 再跟下标映射起来----这里相当于是一个哈希函数
return colormap2label
#@save
def voc_label_indices(colormap, colormap2label):
"""将VOC标签中的RGB值映射到它们的类别索引"""
colormap = colormap.permute(1, 2, 0).numpy().astype('int32')
idx = ((colormap[:, :, 0] * 256 + colormap[:, :, 1]) * 256
+ colormap[:, :, 2])
return colormap2label[idx]
例如,在第一张样本图像中,飞机头部区域的类别索引为1,而背景索引为0。
y = voc_label_indices(train_labels[0], voc_colormap2label())
y[105:115, 130:140], VOC_CLASSES[1] #为1的位置都是飞机区域
(tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 1, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1]]),
'aeroplane')
预处理数据—图像增广
#@save
def voc_rand_crop(feature, label, height, width):
"""随机裁剪特征和标签图像"""
rect = torchvision.transforms.RandomCrop.get_params(
feature, (height, width)) ## 获取随机裁剪参数
feature = torchvision.transforms.functional.crop(feature, *rect)
label = torchvision.transforms.functional.crop(label, *rect)
return feature, label
imgs = []
for _ in range(n):
imgs += voc_rand_crop(train_features[0], train_labels[0], 200, 300)
imgs = [img.permute(1, 2, 0) for img in imgs]
d2l.show_images(imgs[::2] + imgs[1::2], 2, n);
自定义语义分割数据集类
我们通过继承高级API提供的Dataset类,自定义了一个语义分割数据集类VOCSegDataset。 通过实现__getitem__函数,我们可以任意访问数据集中索引为idx的输入图像及其每个像素的类别索引。 由于数据集中有些图像的尺寸可能小于随机裁剪所指定的输出尺寸,这些样本可以通过自定义的filter函数移除掉。 此外,我们还定义了normalize_image函数,从而对输入图像的RGB三个通道的值分别做标准化。
#@save
class VOCSegDataset(torch.utils.data.Dataset):
"""一个用于加载VOC数据集的自定义数据集"""
def __init__(self, is_train, crop_size, voc_dir):
self.transform = torchvision.transforms.Normalize(
mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
self.crop_size = crop_size
features, labels = read_voc_images(voc_dir, is_train=is_train)
self.features = [self.normalize_image(feature)
for feature in self.filter(features)]
self.labels = self.filter(labels)
self.colormap2label = voc_colormap2label()
print('read ' + str(len(self.features)) + ' examples')
def normalize_image(self, img):
return self.transform(img.float() / 255)
def filter(self, imgs): #只要大于裁剪大小的样本
return [img for img in imgs if (
img.shape[1] >= self.crop_size[0] and
img.shape[2] >= self.crop_size[1])]
def __getitem__(self, idx):
feature, label = voc_rand_crop(self.features[idx], self.labels[idx],
*self.crop_size)
return (feature, voc_label_indices(label, self.colormap2label))
def __len__(self):
return len(self.features)
crop_size = (320, 480)
voc_train = VOCSegDataset(True, crop_size, voc_dir)
voc_test = VOCSegDataset(False, crop_size, voc_dir)
read 1114 examples
read 1078 examples
batch_size = 64
train_iter = torch.utils.data.DataLoader(voc_train, batch_size, shuffle=True,
drop_last=True,
num_workers=d2l.get_dataloader_workers())
for X, Y in train_iter:
print(X.shape)
print(Y.shape)
break
torch.Size([64, 3, 320, 480])
torch.Size([64, 320, 480])
整合迭代器组件
#@save
def load_data_voc(batch_size, crop_size):
"""加载VOC语义分割数据集"""
voc_dir = d2l.download_extract('voc2012', os.path.join(
'VOCdevkit', 'VOC2012'))
num_workers = d2l.get_dataloader_workers()
train_iter = torch.utils.data.DataLoader(
VOCSegDataset(True, crop_size, voc_dir), batch_size,
shuffle=True, drop_last=True, num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(
VOCSegDataset(False, crop_size, voc_dir), batch_size,
drop_last=True, num_workers=num_workers)
return train_iter, test_iter
十、转置卷积
10.1 转置卷积的定义以及运算步骤
转置卷积(transposed convolution),又被称为反卷积(deconvolution),是一种对卷积操作的逆过程,但不是逆运算,转置卷积也是一种卷积。它在图像处理和深度学习中经常被用于上采样和图像重建等任务。
注:反卷积的命名是有一点歧义的,容易被误认为是卷积的逆运算,这个后面会讲,正式的命名还是转置卷积。
首先,我们直接给出转置卷积的运算步骤,后面再作解释:
假设,转置卷积运算给出的步幅为s,填充为p,则,
- 在输入特征图元素间填充s-1行和列的0元素。
- 在输入特征图的周围填充k-p-1行和列的0元素。
- 将卷积核参数上下、左右翻转。
- 做一次正常卷积运算(p=0,s=1,注:这里的p,s与转置的不一样)
上图为s=2,p=1,k=3的转置卷积运算。
输出的大小公式:
H
o
u
t
=
(
H
i
n
−
1
)
×
s
t
r
i
d
e
[
0
]
−
2
×
p
a
d
d
i
n
g
[
0
]
+
k
e
r
n
e
l
_
s
i
z
e
[
0
]
W
o
u
t
=
(
W
i
n
−
1
)
×
s
t
r
i
d
e
[
1
]
−
2
×
p
a
d
d
i
n
g
[
1
]
+
k
e
r
n
e
l
_
s
i
z
e
[
1
]
\begin{array}{l}\\\\H_{out}=(H_{in}-1)\times stride[0]-2\times padding[0]+kernel\_size[0]\\\\W_{out}=(W_{in}-1)\times stride[1]-2\times padding[1]+kernel\_size[1]\end{array}
Hout=(Hin−1)×stride[0]−2×padding[0]+kernel_size[0]Wout=(Win−1)×stride[1]−2×padding[1]+kernel_size[1]
10.2 pytorch给出的转置卷积接口
该类的主要参数包括:
-
in_channels
:输入张量的通道数。 -
out_channels
:输出张量的通道数。 -
kernel_size
:卷积核的尺寸。 -
stride
:卷积核的步幅(可以是一个数或者一个元组)。 -
padding
:输入的边缘填充大小。 -
output_padding
:输出的边缘填充大小。 -
dilation
:卷积核内元素之间的间距(默认为1)。 -
groups
:控制输入和输出通道之间的连接方式,通常设置为1。 -
bias
:是否使用偏置参数,默认为True。
官方公式:
其中,我们默认dilation为1,output_padding通常为0.
10.3 矩阵乘法角度理解转置卷积
对于正常卷积运算来说,我们首先忽略偏置。
实际上的计算并不会像我们手算那样使用滑动窗口,我们介绍一种矩阵运算。
首先,我们把卷积核进行等价:
然后我们把对应的矩阵进行展开:
矩阵乘法的公式为:
I
1
×
16
C
16
×
4
=
O
1
×
4
I^{1×16}C^{16×4} = O^{1×4}
I1×16C16×4=O1×4
在这里我们思考一个问题,我们能否根据矩阵C和O求出I,就是输入矩阵呢?
答案是显然不能的,首先这个的矩阵C就不是个方阵,也就提不上可逆了,而大多数情况来说这个卷积都是不可逆的,这就是前面说的那个反卷积可能引起的歧义。
那我们能否变形生成一个与输入矩阵形状相同的矩阵呢?
答案是显然可以的,并且我们把这种反着来扩大特征矩阵的运算叫做转置矩阵。
我们把公式变形:
O
1
×
4
C
T
=
P
1
×
16
O^{1×4}C^T = P^{1×16}
O1×4CT=P1×16
矩阵表示:
我们然后再把这种矩阵乘法,转回卷积运算
在我们使用等效卷积核与输入矩阵运算时,发现他们的结果与绿色的卷积核在这种新的输入矩阵上滑动的运算结果相同。
而这个绿色的卷积核刚好就等于之前的卷积核上下左右对调的卷积核。
十一、全卷积网络(FCN)
全卷积网络(Fully Convolutional Network,简称FCN)是一种基于卷积神经网络(Convolutional Neural Network,简称CNN)的架构,用于图像处理和计算机视觉任务中的像素级别的语义分割。
与我们之前在图像分类或目标检测部分介绍的卷积神经网络不同,全卷积网络将中间层特征图的高和宽变换回输入图像的尺寸:这是通过在前面引入的转置卷积(transposed convolution)实现的。 因此,输出的类别预测与输入图像在像素级别上具有一一对应关系:通道维的输出即该位置对应像素的类别预测。
接下来,手动实现一下:
%matplotlib inline
import torch
import torchvision
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
- 首先获取resnet18
pretrained_net = torchvision.models.resnet18(pretrained=True)
list(pretrained_net.children())[-3:] #展示一下后面三部分 一个块 一个平均池化层以及一个线性层
[Sequential(
(0): BasicBlock(
(conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
),
AdaptiveAvgPool2d(output_size=(1, 1)),
Linear(in_features=512, out_features=1000, bias=True)]
- 去掉最后两层
net = nn.Sequential(*list(pretrained_net.children())[:-2]) #最后两层不要
如果我们给定一个3×20×480的输入,经过网络后会变成512×10×15.
X = torch.rand(size=(1, 3, 320, 480))
net(X).shape
torch.Size([1, 512, 10, 15])
- 增加卷积层
我们分类的结果是21类,因此通道数应该要从512变成21,高宽应该从10×15变成320×480
因此我们应该使用一个卷积核为1*1使得通道数降为21,而高宽根据公式:
H
o
u
t
=
(
H
i
n
−
1
)
×
s
t
r
i
d
e
[
0
]
−
2
×
p
a
d
d
i
n
g
[
0
]
+
k
e
r
n
e
l
_
s
i
z
e
[
0
]
W
o
u
t
=
(
W
i
n
−
1
)
×
s
t
r
i
d
e
[
1
]
−
2
×
p
a
d
d
i
n
g
[
1
]
+
k
e
r
n
e
l
_
s
i
z
e
[
1
]
\begin{array}{l}\\\\H_{out}=(H_{in}-1)\times stride[0]-2\times padding[0]+kernel\_size[0]\\\\W_{out}=(W_{in}-1)\times stride[1]-2\times padding[1]+kernel\_size[1]\end{array}
Hout=(Hin−1)×stride[0]−2×padding[0]+kernel_size[0]Wout=(Win−1)×stride[1]−2×padding[1]+kernel_size[1]
根据约束,我们可以令k=64 p=16 s=32
num_classes = 21
net.add_module('final_conv', nn.Conv2d(512, num_classes, kernel_size=1))
net.add_module('transpose_conv', nn.ConvTranspose2d(num_classes, num_classes,
kernel_size=64, padding=16, stride=32))
-
使用双线性插值法初始化转置卷积的参数
我们需要初始化转置卷积的参数,这里我们采用双线性插值法来生成转置卷积层的初始化参数。
def bilinear_kernel(in_channels, out_channels, kernel_size):
factor = (kernel_size + 1) // 2
if kernel_size % 2 == 1:
center = factor - 1
else:
center = factor - 0.5
og = (torch.arange(kernel_size).reshape(-1, 1),
torch.arange(kernel_size).reshape(1, -1))
filt = (1 - torch.abs(og[0] - center) / factor) * \
(1 - torch.abs(og[1] - center) / factor)
weight = torch.zeros((in_channels, out_channels,
kernel_size, kernel_size))
weight[range(in_channels), range(out_channels), :, :] = filt
return weight
W = bilinear_kernel(num_classes, num_classes, 64)
net.transpose_conv.weight.data.copy_(W);
batch_size, crop_size = 32, (320, 480)
train_iter, test_iter = d2l.load_data_voc(batch_size, crop_size)
read 1114 examples
read 1078 examples
4.定义损失函数并进行训练
def loss(inputs, targets):
return F.cross_entropy(inputs, targets, reduction='none').mean(1).mean(1)
num_epochs, lr, wd, devices = 5, 0.001, 1e-3, d2l.try_all_gpus()
trainer = torch.optim.SGD(net.parameters(), lr=lr, weight_decay=wd)
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices)
loss 0.445, train acc 0.863, test acc 0.850
16.1 examples/sec on [device(type='cuda', index=0), device(type='cuda', index=1)]
- 预测
def predict(img):
X = test_iter.dataset.normalize_image(img).unsqueeze(0)
pred = net(X.to(devices[0])).argmax(dim=1)
return pred.reshape(pred.shape[1], pred.shape[2])
def label2image(pred):
colormap = torch.tensor(d2l.VOC_COLORMAP, device=devices[0])
X = pred.long()
return colormap[X, :]
voc_dir = d2l.download_extract('voc2012', 'VOCdevkit/VOC2012')
test_images, test_labels = d2l.read_voc_images(voc_dir, False)
n, imgs = 4, []
for i in range(n):
crop_rect = (0, 0, 320, 480)
X = torchvision.transforms.functional.crop(test_images[i], *crop_rect)
pred = label2image(predict(X))
imgs += [X.permute(1,2,0), pred.cpu(),
torchvision.transforms.functional.crop(
test_labels[i], *crop_rect).permute(1,2,0)]
d2l.show_images(imgs[::3] + imgs[1::3] + imgs[2::3], 3, n, scale=2);
这里注意一下语义分割中的交叉熵损失:
比如计算下面这个3分类的交叉熵损失,我们是在每个像素上对应的通道与label作交叉熵损失。
结果形状为【N,H,W】每个元素的值表示该位置的损失值,最后还要通过去平均值把损失转成标量。
十二、风格迁移
风格迁移算法的基本原理是将一幅图像的内容与另一幅图像的风格进行分离,然后将两者重新合成成新的图像。通常,它分为两个关键步骤:内容提取和风格提取。
-
内容提取:这一步骤旨在提取图像的内容信息,即图像中的物体和结构。常用的方法是使用卷积神经网络(CNN),将原始图像输入预训练的CNN模型中,选择适当的层作为内容特征表示。这些特征表示捕捉到了原始图像中的低级和高级视觉特征。
-
风格提取:这一步骤的目标是提取图像的风格信息,即图像的纹理和颜色分布。通常采用的方法是使用卷积神经网络,将已经经过预训练的CNN模型作为基础,在其中选择适当的层作为风格特征表示。这些特征表示捕捉到了图像中的纹理和颜色分布的统计特征。
一旦获得了内容和风格的特征表示,就可以使用任何适当的算法将它们结合起来生成新的图像。
12.1 方法
- 首先,我们初始化合成图像,例如将其初始化为内容图像,该合成图像是风格迁移过程中唯一需要更新的变量,即风格迁移所需迭代的模型参数。
- 然后,我们选择一个预训练的卷积神经网络来抽取三个图像的特征,其中的模型参数在训练中无须更新(即冻结抽取模型的参数)。
- 这个深度卷积神经网络凭借多个层逐级抽取图像的特征,我们可以选择其中某些层的输出作为内容特征或风格特征。
- 我们通过前向传播(实线箭头方向)计算风格迁移的损失函数,并通过反向传播(虚线箭头方向)迭代模型参数,即不断更新合成图像。
- 风格迁移常用的损失函数由3部分组成:
- 内容损失使合成图像与内容图像在内容特征上接近;
- 风格损失使合成图像与风格图像在风格特征上接近;
- 全变分损失则有助于减少合成图像中的噪点。
12.2 读取内容和风格图像
%matplotlib inline
import torch
import torchvision
from torch import nn
from d2l import torch as d2l
d2l.set_figsize()
content_img = d2l.Image.open('../img/rainier.jpg')
d2l.plt.imshow(content_img);
style_img = d2l.Image.open('../img/autumn-oak.jpg')
d2l.plt.imshow(style_img);
12.3 预处理和后处理
预处理函数preprocess对输入图像在RGB三个通道分别做标准化,并将结果变换成卷积神经网络接受的输入格式。
后处理函数postprocess则将输出图像中的像素值还原回标准化之前的值。于图像打印函数要求每个像素的浮点数值在0~1之间,我们对小于0和大于1的值分别取0和1。
rgb_mean = torch.tensor([0.485, 0.456, 0.406])
rgb_std = torch.tensor([0.229, 0.224, 0.225])
def preprocess(img, image_shape):
transforms = torchvision.transforms.Compose([
torchvision.transforms.Resize(image_shape),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(mean=rgb_mean, std=rgb_std)])
return transforms(img).unsqueeze(0)
def postprocess(img):
img = img[0].to(rgb_std.device)
img = torch.clamp(img.permute(1, 2, 0) * rgb_std + rgb_mean, 0, 1) #限制取值范围
return torchvision.transforms.ToPILImage()(img.permute(2, 0, 1))
12.4 抽取图像特征
我们使用基于ImageNet数据集预训练的VGG-19模型来抽取图像特征。
pretrained_net = torchvision.models.vgg19(pretrained=True)
一般来说,越靠近输入层,越容易抽取图像的细节信息,越靠近输出层,越容易抽取图像的全局信息。VGG网络使用了5个卷积块, 实验中,我们选择第四卷积块的最后一个卷积层作为内容层,选择每个卷积块的第一个卷积层作为风格层。
style_layers, content_layers = [0, 5, 10, 19, 28], [25]
下面我们构建一个新的网络net,它只保留到我们抽取最后一个层为止。
net = nn.Sequential(*[pretrained_net.features[i] for i in
range(max(content_layers + style_layers) + 1)])
extract_features
抽取指定图像在前向传播中的内容层与风格层:
def extract_features(X, content_layers, style_layers):
contents = []
styles = []
for i in range(len(net)):
X = net[i](X)
if i in style_layers:
styles.append(X)
if i in content_layers:
contents.append(X)
return contents, styles
get_contents
函数用来提取内容图像中的预处理后的内容图像以及抽取的内容层。get_styles
函数类似。
def get_contents(image_shape, device):
content_X = preprocess(content_img, image_shape).to(device)
contents_Y, _ = extract_features(content_X, content_layers, style_layers)
return content_X, contents_Y
def get_styles(image_shape, device):
style_X = preprocess(style_img, image_shape).to(device)
_, styles_Y = extract_features(style_X, content_layers, style_layers)
return style_X, styles_Y
12.5 定义损失函数
12.5.1 内容损失
内容损失采用均方差损失函数。
def content_loss(Y_hat, Y):
# 我们从动态计算梯度的树中分离目标:
# 这是一个规定的值,而不是一个变量。
return torch.square(Y_hat - Y.detach()).mean()
12.5.2 风格损失
风格损失与内容损失类似,也通过平方误差函数衡量合成图像与风格图像在风格上的差异。
为了表达风格层输出的风格,我们先通过extract_features函数计算风格层的输出。假设输出的形状为[1,c,h,w],我们将每个通道拉成行向量,那么就形成了一个c行,hw列的矩阵,我们记作
X
X
X,且称
X
X
T
XX^T
XXT为格拉姆矩阵。
其中格拉姆矩阵中的每个元素的值表示其行列坐标通道风格的相关性。为了避免格拉姆矩阵中出现较大的值,我们让矩阵除以元素的个数。
def gram(X):
num_channels, n = X.shape[1], X.numel() // X.shape[1]
X = X.reshape((num_channels, n))
return torch.matmul(X, X.T) / (num_channels * n)
自然地,风格损失的平方误差函数的两个格拉姆矩阵输入分别基于合成图像与风格图像的风格层输出。这里假设基于风格图像的格拉姆矩阵gram_Y已经预先计算好了。
def style_loss(Y_hat, gram_Y):
return torch.square(gram(Y_hat) - gram_Y.detach()).mean()
12.5.3 全变分损失函数
有时候,我们学到的合成图像里面有大量高频噪点,即有特别亮或者特别暗的颗粒像素。 一种常见的去噪方法是全变分去噪(total variation denoising): 假设 x i , j x_{i, j} xi,j表示坐标 ( i , j ) (i, j) (i,j)处的像素值,降低全变分损失 ∑ i , j ∣ x i , j − x i + 1 , j ∣ + ∣ x i , j − x i , j + 1 ∣ \sum_{i, j} \left|x_{i, j} - x_{i+1, j}\right| + \left|x_{i, j} - x_{i, j+1}\right| i,j∑∣xi,j−xi+1,j∣+∣xi,j−xi,j+1∣
def tv_loss(Y_hat):
return 0.5 * (torch.abs(Y_hat[:, :, 1:, :] - Y_hat[:, :, :-1, :]).mean() +
torch.abs(Y_hat[:, :, :, 1:] - Y_hat[:, :, :, :-1]).mean())
12.5.4 总的损失函数
风格转移的损失函数是内容损失、风格损失和总变化损失的加权和。 通过调节这些权重超参数,我们可以权衡合成图像在保留内容、迁移风格以及去噪三方面的相对重要性。
content_weight, style_weight, tv_weight = 1, 1e3, 10
def compute_loss(X, contents_Y_hat, styles_Y_hat, contents_Y, styles_Y_gram):
# 分别计算内容损失、风格损失和全变分损失
contents_l = [content_loss(Y_hat, Y) * content_weight for Y_hat, Y in zip(
contents_Y_hat, contents_Y)]
styles_l = [style_loss(Y_hat, Y) * style_weight for Y_hat, Y in zip(
styles_Y_hat, styles_Y_gram)]
tv_l = tv_loss(X) * tv_weight
# 对所有损失求和
l = sum(10 * styles_l + contents_l + [tv_l])
return contents_l, styles_l, tv_l, l
12.6 初始化合成图像
在风格迁移中,合成的图像是训练期间唯一需要更新的变量。因此,我们可以定义一个简单的模型SynthesizedImage,并将合成的图像视为模型参数。模型的前向传播只需返回模型参数即可。
class SynthesizedImage(nn.Module):
def __init__(self, img_shape, **kwargs):
super(SynthesizedImage, self).__init__(**kwargs)
self.weight = nn.Parameter(torch.rand(*img_shape))
def forward(self):
return self.weight
下面,我们定义get_inits函数。该函数创建了合成图像的模型实例,并将其初始化为图像X。风格图像在各个风格层的格拉姆矩阵styles_Y_gram将在训练前预先计算好。
def get_inits(X, device, lr, styles_Y):
gen_img = SynthesizedImage(X.shape).to(device)
gen_img.weight.data.copy_(X.data)
trainer = torch.optim.Adam(gen_img.parameters(), lr=lr)
styles_Y_gram = [gram(Y) for Y in styles_Y]
return gen_img(), styles_Y_gram, trainer
12.4 训练模型
在训练模型进行风格迁移时,我们不断抽取合成图像的内容特征和风格特征,然后计算损失函数。下面定义了训练循环。
def train(X, contents_Y, styles_Y, device, lr, num_epochs, lr_decay_epoch):
X, styles_Y_gram, trainer = get_inits(X, device, lr, styles_Y)
scheduler = torch.optim.lr_scheduler.StepLR(trainer, lr_decay_epoch, 0.8)
animator = d2l.Animator(xlabel='epoch', ylabel='loss',
xlim=[10, num_epochs],
legend=['content', 'style', 'TV'],
ncols=2, figsize=(7, 2.5))
for epoch in range(num_epochs):
trainer.zero_grad()
contents_Y_hat, styles_Y_hat = extract_features(
X, content_layers, style_layers)
contents_l, styles_l, tv_l, l = compute_loss(
X, contents_Y_hat, styles_Y_hat, contents_Y, styles_Y_gram)
l.backward()
trainer.step()
scheduler.step()
if (epoch + 1) % 10 == 0:
animator.axes[1].imshow(postprocess(X))
animator.add(epoch + 1, [float(sum(contents_l)),
float(sum(styles_l)), float(tv_l)])
return X
device, image_shape = d2l.try_gpu(), (300, 450)
net = net.to(device)
content_X, contents_Y = get_contents(image_shape, device)
_, styles_Y = get_styles(image_shape, device)
output = train(content_X, contents_Y, styles_Y, device, 0.3, 500, 50)
文章来源:https://www.toymoban.com/news/detail-528148.html
总结
在本章中,我们介绍了计算机视觉领域一些基础的方向,并讲解了一些基础的实现过程。文章来源地址https://www.toymoban.com/news/detail-528148.html
到了这里,关于六、计算机视觉相关内容的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!