🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
文章目录
介绍
风格转移
它是如何工作的?
使用 VGG-19 网络架构实现风格迁移
输入——加载和显示
练习 5.01:加载和显示图像
加载模型
练习 5.02:在 PyTorch 中加载预训练模型
提取特征
练习 5.03:设置特征提取过程
优化算法、损失和参数更新
内容丢失
风格损失
总体损耗
练习 5.04:创建目标图像
活动5.01:执行风格迁移
概括
本章介绍了使用预训练模型创建或使用性能良好的算法而无需收集大量数据的过程。在本章中,您将学习如何从 PyTorch 加载预训练模型以创建风格迁移模型。到本章结束时,您将能够通过使用预训练模型执行风格迁移。
介绍
上一章解释了传统 CNN 的不同构建模块,以及一些提高其性能和减少训练时间的技术。那里解释的架构虽然很典型,但并不是一成不变的,并且出现了大量的 CNN 架构来解决不同的数据问题,最常见的是计算机视觉领域。
这些架构在配置和学习任务上各不相同。当今非常流行的一种是视觉几何组( VGG ) 架构,由牛津机器人研究所的 Karen Simonyan 和 Andrew Zisserman 创建。它是为物体识别而开发的,由于网络依赖的大量参数,它实现了最先进的性能。它在数据科学家中流行的主要原因之一是训练模型的参数(权重和偏差)的可用性,这使得研究人员无需训练即可使用它,以及模型的出色性能。
在本章中,我们将使用这个预训练模型来解决一个计算机视觉问题,该问题因专门用于共享图像的社交媒体渠道的流行而特别出名。它包括执行风格转换,以创建具有一张图像的风格(颜色和纹理)和另一张图像的内容(形状和对象)的新图像。
当在常规图像上应用过滤器以提高其质量和在社交媒体配置文件上发布时的吸引力时,每天都会执行此任务数百万次。虽然这看起来是一项简单的任务,但本章将解释这些图像编辑应用程序幕后发生的神奇之处。
本章中的所有代码都可以在以下位置找到:https ://packt.live/2yiR97z 。
风格转移
简而言之,风格转换包括修改图像的风格,同时仍保留其内容。一个例子是拍摄一张动物的图像并将风格转换为莫奈风格的绘画,如下图所示:
图 5.1:风格迁移的输入和输出——本章最后练习的结果
笔记
此图像可在 GitHub 上找到,网址为https://packt.live/2XEykpL。
根据上图,模型有两个输入:内容图像和风格图像。内容是指图像的对象,而风格是指颜色和纹理。因此,模型的输出应该是包含内容图像中的对象和风格图像的艺术外观的图像。
它是如何工作的?
与解决上一章中解释的传统计算机视觉问题相反,风格转换需要一组不同的步骤来有效地将两个图像作为输入并创建一个新图像作为输出。
以下是解决风格迁移问题时所遵循的步骤的简要说明:
- 提供输入:内容和样式图像都将被提供给模型,并且它们需要具有相同的形状。这里的一个常见做法是调整样式图像的大小,使其与内容图像的形状相同。
- 加载模型:Oxford 的 VGG 创建了一个模型架构,在风格转换问题上表现出色,称为 VGG 网络。他们还使模型的参数对任何人都可用,以便可以缩短或跳过模型的训练过程(这就是预训练模型的用途)。
笔记
VGG 网络有不同的版本,并且都使用不同的层数。为了区分不同的版本,命名法中首字母缩写词末尾的破折号和数字代表该特定架构的层数。对于本章,我们将使用网络的 19 层版本,称为 VGG-19。
正因为如此,可以使用 PyTorch 的模型子包加载预训练模型来执行风格迁移任务,而无需使用大量图像训练网络。
- 确定层的功能:鉴于手头有两个主要任务(识别图像的内容和区分另一个图像的风格),不同的层将具有不同的功能来提取不同的特征。对于样式图像,重点应放在颜色和纹理上,而对于内容图像,重点应放在边缘和形式上。在这一步中,不同的层被分成不同的任务。
- 定义优化问题:与任何其他监督问题一样,有必要定义一个损失函数,它将负责衡量输出和输入之间的差异。与其他监督问题不同,风格迁移任务要求您定义三种不同的损失函数,所有这些损失函数都应在训练过程中最小化。三个损失函数解释如下:
内容损失:这衡量内容图像和输出之间的距离,同时只考虑与内容相关的特征。
风格损失:衡量风格图像和输出之间的距离,同时只考虑与风格相关的特征。
总损失:这结合了内容和风格损失。两种损失都有一个与之相关的权重,用于确定它们参与总损失的计算。
- 参数更新:这一步使用梯度来更新模型的不同参数。
使用 VGG-19 网络架构实现风格迁移
VGG-19 是由 19 层组成的 CNN。它使用来自 ImageNet 数据库的数百万张图像进行训练。该网络能够将图像分类为 1,000 个不同的类别标签,包括大量的动物和不同的工具。
笔记
要探索 ImageNet 数据库,请访问以下 URL:http ://www.image-net.org/ 。
考虑到它的深度,该网络能够从各种各样的图像中识别出复杂的特征,这使得它特别适合风格转换问题,其中特征提取在不同阶段和不同目的中至关重要。
本节将重点介绍如何使用预训练的 VGG-19 模型进行风格迁移。本章的最终目标是使用动物或风景图像(作为内容图像)和知名艺术家的一幅画作(作为风格图像)来创建常规对象的新图像具有艺术风格。
然而,在深入这个过程之前,以下是导入列表及其使用的简要说明:
- NumPy:这将用于转换要显示的图像。
- torch、torch.nn和torch.optim:它们将实现神经网络并定义优化算法。
- PIL.Image:这将根据以下代码片段加载图像:
image = Image.open(image_name) image = transformation(image).unsqueeze(0)
可以看出,第一步包括打开图像(此处,image_name应替换为图像的路径)。接下来,可以将之前定义的任何变换应用于图像。
笔记
有关如何定义图像变换的提示,请重温第 4 章,卷积神经网络。
unsqueeze ()函数用于根据将图像馈送到 VGG-19 模型的要求为图像添加额外的维度。
- matplotlib.pyplot:这将显示图像。
- torchvision.transforms和torchvision.models:它们会将图像转换为张量并加载预训练模型。
输入——加载和显示
执行风格转换的第一步包括加载内容和风格图像。在此步骤中,将处理基本的预处理,其中图像的大小必须相等(最好是用于训练预训练模型的图像大小),这也将是输出图像的大小。此外,图像被转换为 PyTorch 张量,并且可以根据需要进行归一化。
显示已加载的图像以确保它们符合要求始终是一种很好的做法。考虑到此时图像已经转换为张量并归一化,应该克隆张量,并需要执行一组新的转换,以便我们可以使用 Matplotlib 显示它们。这意味着应该将张量转换回Python 成像库( PIL ) 图像,并且必须恢复规范化过程,如以下示例所示:
image = tensor.clone()
image = image.squeeze(0)
img_display = \
transforms.Compose([transforms.Normalize((-0.5/0.25, \
-0.5/0.25, -0.5/0.25), \
(1/0.25, 1/0.25, \
1/0.25)), \
transforms.ToPILImage()])
首先,克隆张量,并删除额外的维度。接下来,定义转换。
要了解恢复归一化的过程,请考虑对所有维度使用 0.5 作为均值和 0.25 作为标准差进行归一化的图像。恢复归一化的方法是使用均值的负值除以标准差作为均值(-0.5 除以 0.25)。新的标准偏差应等于一除以标准偏差(1 除以 0.25)。定义加载和显示图像的函数有助于节省时间并确保对内容图像和样式图像执行相同的过程。这个过程将在下面的练习中得到扩展。
笔记
本章的所有练习都将在同一个笔记本中编码,组合代码将一起执行风格迁移任务。
练习 5.01:加载和显示图像
这是执行风格迁移的四个步骤中的第一步。本练习的目的是加载和显示将在进一步练习中使用的图像(包括内容和样式图像)。请按照以下步骤完成此练习:
笔记
对于本章中的练习和活动,您需要安装 Python 3.7、Jupyter 6.0、Matplotlib 3.1、NumPy 1.17、Pillow 6.2 和 PyTorch 1.3+(最好是 PyTorch 1.4,带或不带 CUDA)(如前言中所述) ).
在本书的 GitHub 存储库 ( https://packt.live/2yiR97z ) 中,您将能够找到将在本章的不同练习和活动中使用的不同图像。
- 导入执行样式转换所需的所有包:
import numpy as np import torch from torch import nn, optim from PIL import Image import matplotlib.pyplot as plt from torchvision import transforms, models
如果您有可用的 GPU,请定义一个名为device等于cuda的变量,它将用于将一些变量分配给您机器的 GPU:
device = "cuda" device
- 设置要用于两个图像的图像大小。此外,设置要对图像执行的转换,其中应包括调整图像大小、将它们转换为张量并对其进行归一化:
imsize = 224 loader = transforms.Compose([\ transforms.Resize(imsize), \ transforms.ToTensor(), \ transforms.Normalize((0.485, 0.456, 0.406), \ (0.229, 0.224, 0.225))])
使用此代码,将图像调整为与最初用于训练 VGG-19 模型的图像相同的大小。归一化也是使用用于归一化训练图像的相同值完成的。
笔记
VGG 网络使用归一化图像进行训练,其中每个通道的平均值分别为 0.485、0.456 和 0.406,标准差分别为 0.229、0.224 和 0.225。
- 定义一个接收图像路径作为输入并使用PIL打开图像的函数。接下来,它应该将转换应用于图像:
def image_loader(image_name): image = Image.open(image_name) image = loader(image).unsqueeze(0) return image
- 调用函数加载内容和样式图像。使用狗图像作为内容图像和 Matisse 图像作为样式图像,这两种图像都可以在本书的 GitHub 存储库中找到:
content_img = image_loader("images/dog.jpg") style_img = image_loader("images/matisse.jpg")
如果您的机器有可用的 GPU,请改用以下代码片段来获得相同的结果:
content_img = image_loader("images/dog.jpg").to(device) style_img = image_loader("images/matisse.jpg").to(device)
前面的代码片段将保存图像的变量分配给 GPU,以便使用这些变量的所有操作都由 GPU 处理。
- 要显示图像,请将它们转换回 PIL 图像并恢复规范化过程。在变量中定义这些转换:
unloader = transforms.Compose([\ transforms.Normalize((-0.485/0.229, \ -0.456/0.224, \ -0.406/0.225), \ (1/0.229, 1/0.224, 1/0.225)),\ transforms.ToPILImage()])
- 创建一个克隆张量、压缩它并将上一步中定义的转换应用于张量的函数:
def tensor2image(tensor): image = tensor.clone() image = image.squeeze(0) image = unloader(image) return image
如果您的机器有可用的 GPU,请改用以下等效代码片段:
def tensor2image(tensor): image = tensor.to('cpu').clone() image = image.squeeze(0) image = unloader(image) return image
前面的代码片段将图像分配回 CPU,以便我们可以绘制它们。
- 调用两个图像的函数并绘制结果:
plt.figure() plt.imshow(tensor2image(content_img)) plt.title("Content Image") plt.show() plt.figure() plt.imshow(tensor2image(style_img)) plt.title("Style Image") plt.show()
生成的图像应如下所示:
图 5.2:内容图像
图 5.3:样式图像
这样,您就成功加载并显示了用于样式转换的内容和样式图像。
加载模型
与许多其他框架一样,PyTorch 有一个子包,其中包含不同的模型,这些模型之前已经过训练并可供公众使用。考虑到从头开始训练神经网络非常耗时,这一点很重要;从预训练模型开始可以帮助减少训练时间。这意味着可以加载预训练模型,这样我们就可以使用它们的最终参数(应该是那些最小化损失函数的参数),而无需经过迭代过程。
正如我们之前提到的,用于执行风格转换任务的架构是 19 层的 VGG 网络,也称为 VGG-19。预训练模型在torchvision的模型子包下可用。PyTorch 中保存的模型分为两部分:
- vgg19.features:它由网络的所有卷积层和池化层以及参数组成。这些层负责从图像中提取特征;有些层专注于样式特征,例如颜色,而其他层则专注于内容特征,例如边缘。
- vgg19.classifier:这是指位于网络末端的线性层(也称为全连接层),包括它们的参数。这些层是将图像分类为标签类别之一的层,例如,识别图像中的动物类型。
笔记
要探索 PyTorch 中可用的其他预训练模型,请访问https://pytorch.org/docs/stable/torchvision/models.html。
根据前面的信息,只应加载模型的特征部分,以便提取内容和风格图像的必要特征。加载模型包括调用模型子包,后跟模型名称,确保预训练参数设置为True(以便从先前的训练过程中加载参数),并且只有特征层被加载按照以下代码段加载:
model = models.vgg19(pretrained=True).features
考虑到那些有助于检测所需特征的参数,每一层中的参数(权重和偏差)应保持不变。这可以通过定义模型不需要计算任何这些层的梯度来实现,如下所示:
for param in model.parameters():
param.requires_grad_(False)
在这里,对于之前加载的模型的每个参数,requires_grad_方法都设置为False以避免计算梯度,因为目标是使用预训练参数,而不更新它们。
练习 5.02:在 PyTorch 中加载预训练模型
使用与上一个练习相同的笔记本,本练习旨在加载将在后续练习中使用的预训练模型,以使用我们之前加载的图像执行样式转换任务。
- 打开上一个练习的笔记本。
- 从 PyTorch 加载 VGG-19 预训练模型:
model = models.vgg19(pretrained=True).features
如前所述,选择模型的特征部分。这将使您能够访问模型的所有卷积层和池化层,这些层将用于执行本章后续练习中的特征提取。
- 通过先前加载的模型的参数执行for循环。设置每个参数,使其不需要梯度计算:
for param in model.parameters(): param.requires_grad_(False)
通过将梯度计算设置为False,我们确保在训练过程中不计算梯度。
如果您的机器有可用的 GPU,请将以下代码片段添加到前面的代码片段中,以便将模型分配给 GPU:
model.to(device)
这样,您就成功加载了预训练模型。
提取特征
正如我们之前提到的,VGG-19 网络包含 19 个不同的层,包括卷积层、池化层和全连接层。卷积层在每个池化层之前以堆栈的形式出现,五个是整个架构中堆栈的数量。
在风格迁移领域,已经有不同的论文确定了那些对于识别内容和风格图像上的相关特征至关重要的层。因此,通常认为每个堆栈的第一个卷积层都能够提取风格特征,而只有第四个堆栈的第二个卷积层应该用于提取内容特征。
从现在开始,我们将提取风格特征的层称为conv1_1、conv2_1、conv3_1、conv4_1和conv5_1,而负责提取内容特征的层将称为conv4_2。
笔记
用作本章指南的论文可通过以下 URL 访问:https ://www.cv-foundation.org/openaccess/content_cvpr_2016/papers/Gatys_Image_Style_Transfer_CVPR_2016_paper.pdf 。
这意味着风格图像的特征是从五个不同的层获得的,而内容图像的特征仅从一层获得。每个层的输出用于比较输出图像和输入图像,目标是修改目标图像的参数,使它们类似于内容图像的内容和风格图像的风格,这可以通过优化三种不同的损失函数来实现(这将在本章中进一步解释)。
要提取每一层的特征,可以使用以下代码片段:
layers = {'0': 'conv1_1', '5': 'conv2_1', '10': 'conv3_1', \
'19': 'conv4_1', '21': 'conv4_2', '28': 'conv5_1'}
features = {}
x = image
for index, layer in model._modules.items():
x = layer(image)
if index in layers:
features[layers[index]] = x
在前面的代码片段中,layers是一个字典,它将所有相关层的位置(在网络中)映射到将用于识别它们的名称,而model._modules包含一个包含网络每一层的字典。
通过在不同的层执行for循环,我们将图像传递到不同的层,并将感兴趣层(我们之前创建的层字典中的层)的输出保存到特征字典中。输出字典由包含层名称的键和包含该层输出特征的值组成。
为了确定目标图像是否包含与内容图像相同的内容,我们需要检查两个图像中是否存在某些特征。然而,要检查目标图像和风格图像的风格表示,有必要检查相关性,而不是检查两个图像的特征是否严格存在。这是因为两幅图像的风格特征都不是精确的,而是近似值。
克矩阵用于检查这些相关性。它包括创建一个矩阵,该矩阵查看给定层中不同样式特征的相关性。这是通过将卷积层的矢量化输出乘以相同的转置矢量化输出来完成的,如下图所示:
图 5.4:gram 矩阵的计算
在上图中,A 指的是具有 4x4 维度(高和宽)的输入样式图像,而 B 表示图像通过具有五个过滤器的卷积层后的输出。最后,C 指的是 gram 矩阵的计算,其中左边的图像表示 B 的矢量化版本,右边的图像是其转置版本。从向量化输出的乘法中,创建了一个 5x5 gram 矩阵,其值表示不同通道(过滤器)在风格特征方面的相似性(相关性)。
这些相关性可用于确定与图像的风格表示相关的那些特征,然后可用于改变目标图像。考虑到风格特征是在五个不同的层中获得的,考虑到必须为每一层创建一个 gram 矩阵,可以安全地假设网络能够从风格图像中检测大小特征。
练习 5.03:设置特征提取过程
使用上一个练习中的网络架构和本章第一个练习中的图像,我们将创建几个能够从输入图像中提取特征并为风格特征创建 gram 矩阵的函数。
- 打开上一个练习的笔记本。
- 打印我们在上一个练习中加载的模型的架构。这将帮助我们识别相关层,以便我们可以执行样式转换任务:
print(model)
- 创建一个字典,用于将相关层(键)的索引映射到名称(值)。这样方便以后调用相关层的过程:
relevant_layers = {'0': 'conv1_1', '5': 'conv2_1', '10': \ 'conv3_1', '19': 'conv4_1', '21': \ 'conv4_2', '28': 'conv5_1'}
为了创建字典,我们使用了上一步的输出,它显示了网络中的每一层。在那里,可以观察到第一个堆栈的第一层标记为0,而第二个堆栈的第一层标记为5,依此类推。
- 创建一个将从输入图像中提取相关特征(仅从相关层提取的特征)的函数。将其命名为features_extractor并确保它将我们之前创建的图像、模型和字典作为输入:
def features_extractor(x, model, layers): features = {} for index, layer in model._modules.items(): x = layer(x) if index in layers: features[layers[index]] = x return features
输出应该是一个字典,键是层的名称,值是该层的输出特征。
- 对我们在本章第一个练习中加载的内容和样式图像调用features_extractor函数:
content_features = features_extractor(content_img, model, \ relevant_layers) style_features = features_extractor(style_img, model, \ relevant_layers)
- 对样式特征执行克矩阵计算。考虑到风格特征是从不同的层获得的,这就是为什么应该创建不同的 gram 矩阵,每个层的输出一个:
style_grams = {} for i in style_features: layer = style_features[i] _, d1, d2, d3 = layer.shape features = layer.view(d1, d2 * d3) gram = torch.mm(features, features.t()) style_grams[i] = gram
对于每一层,获得风格特征矩阵的形状以对其进行矢量化。接下来,通过将矢量化输出乘以其转置版本来创建 gram 矩阵。
- 创建初始目标图像。该图像稍后将与内容和样式图像进行比较并进行更改,直到达到所需的相似度:
target_img = content_img.clone().requires_grad_(True)
将初始目标图像创建为内容图像的副本是一种很好的做法。此外,必须将其设置为需要计算梯度,考虑到我们希望能够在迭代过程中对其进行修改,直到内容与内容图像相似,风格与风格图像相似。
同样,如果您的机器有可用的 GPU,请确保您也将目标图像分配给 GPU,而是使用以下代码片段:
target_img = content_img.clone().requires_grad_(True).to(device)
- 使用我们在本章第一个练习中创建的tensor2image函数绘制目标图像,它看起来应该与内容图像相同:
plt.figure() plt.imshow(tensor2image(target_img)) plt.title("Target Image") plt.show()
输出图像如下:
图 5.5:目标图像
这样,您就成功地进行了特征提取并计算了克矩阵以进行风格迁移。
优化算法、损失和参数更新
尽管风格转移是使用参数保持不变的预训练网络执行的,但创建目标图像包含一个迭代过程,其中计算三个不同的损失函数,并通过仅更新与目标图像相关的参数来最小化。
为了实现目标图像的创建,计算了两个不同的损失函数(内容损失和样式损失),然后将它们放在一起以计算要优化以获得适当目标图像的总损失函数。然而,考虑到在内容和风格方面的测量精度有很大不同,下面是对内容和风格损失函数的计算的解释,以及如何计算总损失的描述。
内容丢失
这包含一个函数,该函数基于给定层获得的特征图,计算内容图像和目标图像之间的距离。对于 VGG-19 网络,仅根据conv4_2层的输出计算内容损失。
内容损失函数背后的主要思想是最小化内容图像和目标图像之间的距离,使后者在内容方面与前者高度相似。
内容损失可以计算为相关层 ( conv4_2 ) 的内容和目标图像的特征图之间的均方差,可以使用以下等式实现:
图 5.6:内容损失函数
风格损失
与内容损失类似,风格损失是通过计算均方差来衡量风格与目标图像在风格特征(例如,颜色和纹理)方面的距离的函数。
与内容丢失的情况相反,它不是比较从不同层派生的特征图,而是比较基于风格和目标图像的特征图计算的 gram 矩阵。
必须使用for循环为所有相关层(在本例中为五层)计算样式损失。这将导致损失函数考虑来自两个图像的简单和复杂样式表示。
此外,最好在 0 到 1 之间权衡这些层中每一层的样式表示,以便比提取非常复杂特征的层更加强调提取更大和更简单特征的层。这是通过为较早的层(conv1_1和conv2_1)赋予更高的权重来实现的,这些层从样式图像中提取更通用的特征。
对于每个相关层,可以使用以下等式计算样式损失:
图 5.7:风格损失计算
总体损耗
最后,总损失函数由内容损失和风格损失的组合组成。在通过更新目标图像的参数创建目标图像的迭代过程中,它的值被最小化。
同样,建议您为内容和风格损失分配权重,以确定它们对最终输出的参与。这有助于确定目标图像的程式化程度,同时让内容仍然可见。最好将内容损失的权重设置为 1,而风格损失的权重必须高得多才能达到您偏好的比例。
分配给内容损失的权重通常称为 alpha,而分配给样式损失的权重称为 beta。
计算总损失的最终方程如下:
图 5.8:总损失计算
一旦定义了损失的权重,就可以设置迭代步数以及优化算法,它应该只影响目标图像。这意味着,在每个迭代步骤中,将计算所有三个损失,以便我们可以使用梯度来优化与目标图像相关的参数,直到损失函数最小化并获得具有所需外观的目标图像。
与之前神经网络的优化一样,以下是每次迭代中可以观察到的步骤:
- 从目标图像中获取内容和样式方面的特征。在初始迭代中,此图像将是内容图像的精确副本。
- 计算内容损失。这是通过比较内容和目标图像的内容特征图来完成的。
- 计算所有相关层的平均样式损失。这是通过比较风格和目标图像的所有层的克矩阵来实现的。
- 计算总损失。
- 计算总损失函数相对于目标图像的参数(权重和偏差)的偏导数。
- 重复此操作,直到达到所需的迭代次数。
最终输出将是一张内容与内容图像相似,风格与风格图像相似的图像。
练习 5.04:创建目标图像
在本章的最后一个练习中,您将执行风格迁移的任务。本练习包括对负责执行不同迭代的部分进行编码,同时优化损失函数以获得理想的目标图像。为此,使用我们在本章前面的练习中编写的代码位至关重要:
笔记
在 GPU 上运行此代码时,需要进行一些更改。请访问本书的 GitHub 存储库以修改此代码的 GPU 版本。
- 打开上一个练习的笔记本。
- 定义一个字典,其中包含负责提取样式特征的每个层的权重:
style_weights = {'conv1_1': 1., 'conv2_1': 0.8, 'conv3_1': 0.6, \ 'conv4_1': 0.4, 'conv5_1': 0.2}
请务必使用您在上一个练习中为图层指定的相同名称作为键。
- 定义与内容和风格损失相关的权重:
alpha = 1 beta = 1e5
- 定义迭代步数,以及优化算法。如果我们想查看此时创建的图像的绘图,我们还可以设置迭代次数:
print_statement = 200 optimizer = torch.optim.Adam([target_img], lr=0.001) iterations = 2000
该优化算法要更新的参数应该是目标图像的参数。
笔记
运行 2,000 次迭代(如本练习中的示例所示)将花费相当长的时间,具体取决于您的资源。然而,要获得出色的目标图像,可能需要更多的迭代。
为了让您了解每次迭代中目标图像发生的变化,几次迭代就足够了,但我们鼓励您尝试更长时间的训练。
- 定义for循环,其中将计算所有三个损失函数并执行优化过程:
for i in range(1, iterations+1): # 提取所有相关层的特征 target_features = features_extractor(target_img, model, \ relevant_layers) # 计算内容损失 content_loss = torch.mean((target_features['conv4_2'] \ - content_features['conv4_2'])**2) # 遍历所有样式层 style_losses = 0 for layer in style_weights: # 为该层创建gram矩阵 target_feature = target_features[layer] _, d1, d2, d3 = target_feature.shape target_reshaped = target_feature.view(d1, d2 * d3) target_gram = torch.mm(target_reshaped, \ target_reshaped.t()) style_gram = style_grams[layer] # 计算该层的样式损失 style_loss = style_weights[layer] \ * torch.mean((target_gram - style_gram)**2) # 计算所有层的样式损失 style_losses += style_loss / (d1 * d2 * d3) # 计算总损失 total_loss = alpha * content_loss + beta * style_losses # 执行反向传播 optimizer.zero_grad() total_loss.backward() optimizer.step() # 打印目标图像 if i % print_statement == 0 or i == 1: print('Total loss: ', total_loss.item()) plt.imshow(tensor2image(target_img)) plt.show()
- 绘制内容图像和目标图像以比较结果。这可以通过使用我们在前面的练习中创建的tensor2image函数来实现,以便将张量转换为可以使用matplotlib打印的 PIL 图像:
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(20, 10)) ax1.imshow(tensor2image(content_img)) ax2.imshow(tensor2image(target_img)) ax3.imshow(tensor2image(style_img)) plt.show()
最终图像应类似于以下内容:
图 5.9:内容、风格、目标图片对比
笔记
要查看高质量的彩色图像,请访问本书的 GitHub 存储库,网址为https://packt.live/2VBZA5E。
这样,您就成功地进行了风格迁移。
笔记
要访问此特定部分的源代码,请参阅https://packt.live/2VyKJtK。
该部分目前没有在线交互示例,需要在本地运行。
要访问此源代码的 GPU 版本,请参考https://packt.live/2YMcdhh。此版本的源代码不可用作在线交互式示例,需要使用 GPU 设置在本地运行。
活动5.01:执行风格迁移
在本活动中,我们将执行风格迁移。为此,我们将对本章中学到的所有概念进行编码。让我们看看下面的场景。
您有一些想要更改的图像,使它们具有艺术气息,为了实现这一目标,您决定创建一些代码,使用预训练的神经网络来执行风格转换。按照以下步骤完成此活动:
- 导入所需的库。
- 指定要对输入图像执行的转换。务必将它们调整到相同的大小,将它们转换为张量,并将它们归一化。
- 定义图像加载器函数。这应该打开图像并对其进行转换。调用图像加载器函数来加载两个输入图像。
- 为了能够显示图像,定义一组新的转换以恢复图像的规范化并将张量转换为 PIL 图像。
- 创建一个能够对张量执行先前转换的函数 ( tensor2image )。为两个图像调用该函数并绘制结果。
- 加载 VGG-19 模型。
- 创建一个字典,将相关层(键)的索引映射到名称(值)。然后,创建一个函数来提取相关层的特征图。使用它们来提取两个输入图像的特征。
- 计算样式特征的克矩阵。此外,创建初始目标图像。
- 设置不同样式层的权重,以及内容和样式损失的权重。
- 运行模型 500 次迭代。在开始训练模型之前定义 Adam 优化算法,使用 0.001 作为学习率。
笔记
根据您的资源,培训过程可能需要几个小时。因此,要获得出色的结果,建议您训练数千次迭代。如果您希望查看培训过程的进度,添加打印语句是一种很好的做法。
本章显示的结果是通过运行大约 5,000 次迭代获得的,如果没有 GPU,这将需要很长时间才能运行(我们使用 GPU 的这个活动的解决方案也可以在本书的 GitHub 存储库中找到)。但是,为了只看到一些微小的变化,按照本活动 (500) 中的建议运行几百次迭代就足够了。
- 绘制内容、样式和目标图像以比较结果。
5,000 次迭代后的输出应如下所示:
图 5.10:绘制内容和目标图像
笔记
可以通过此链接找到此活动的解决方案。
要查看图 5.10的高质量彩色图像,请访问https://packt.live/2KcORcw。
概括
本章介绍了风格迁移,这是当今流行的任务,可以使用 CNN 执行。它包括将内容图像和样式图像作为输入并返回新创建的图像作为输出,该图像保留其中一个图像的内容和另一个图像的样式。它通常用于通过将随机的常规图像与伟大艺术家的画作相结合来赋予图像艺术感。
虽然风格迁移是使用 CNN 执行的,但创建目标图像的过程并不是通过传统的网络训练来实现的。本章解释了如何使用预训练网络来考虑一些特别擅长识别某些特征的相关层的输出。
本章解释了开发能够执行样式转换任务的代码所需的每个步骤,其中第一步包括加载和显示输入。正如我们之前提到的,模型有两个输入(内容和样式图像)。每张图像都要经过一系列变换,目的是将图像调整为相同大小,将它们转换为张量,并对它们进行归一化,以便它们能够被网络正确处理。
接下来,加载预训练模型。正如我们在本章中提到的,VGG-19 是解决此类任务最常用的架构之一。它由 19 层组成,包括卷积层、池化层和全连接层,其中,对于所讨论的任务,只使用部分卷积层。考虑到 PyTorch 提供了一个包含多个预训练网络架构的子包,加载预训练模型的过程相当简单。
加载网络后,网络的某些层将被识别为在检测对风格转换至关重要的某些特征方面表现出色。虽然五个不同的层能够提取与图像风格相关的特征,例如颜色和纹理,但只有其中一个层非常擅长提取内容特征,例如边缘和形状。因此,至关重要的是定义那些将用于从输入图像中提取信息以创建所需目标图像的层。
最后,是时候对迭代过程进行编码,以用于创建具有所需特征的目标图像。为此,计算了三种不同的损失。一种用于比较内容图像与目标图像在内容方面的差异(内容损失),另一种用于比较风格图像与目标图像在风格方面的差异(风格损失),其中是通过计算gram矩阵来实现的。最后,有一个结合了内容和风格损失(总损失)。
目标图像是通过最小化总损失值来创建的,这可以通过更新与目标图像相关的参数来完成。尽管可以使用预训练的网络,但达到理想目标图像的过程可能需要数千次迭代和相当长的时间。
在下一章中,将解释一种不同的网络架构,以便使用一系列文本数据来解决数据问题。RNN 是具有记忆的神经网络架构,这使它们能够处理顺序数据。它们通常用于解决与理解人类语言相关的问题。