Pytorch基础知识(11)神经风格迁移

谁说深度学习只适用于日常任务?利用神经风格迁移,你可以把日常图像转换成艺术杰作。
作为一个例子,看看下面的截图:
在这里插入图片描述
左边的图像使用ErinLoree的风格图像转换后获得右边的图像。

在神经风格迁移中,我们分别取一个内容图像和一个风格图像。然后,生成具有内容图像的内容和风格图像的艺术风格的图像。这个算法很容易理解,并且遵循我们在本系列中一直学习的同样的概念,但是有一个附加的转折。我们将在本章看到,通过使用伟大艺术家的杰作作为风格图像,你可以产生非常有趣的图像。

在本章中,您将学习使用PyTorch实现神经风格迁移算法来生成具有艺术气息的图像。您将了解内容损失、风格损失、Gram矩阵,以及如何使用它们来实现算法。

本章包括以下教程:

  • 导入数据
  • 实现神经风格迁移

导入数据

如前所述,在神经样式转移算法中,我们至少需要一张内容图像和一张风格图像。在这个教程中,我们将首先以PIL对象的形式加载内容和风格图像。然后,我们将PIL对象转换为PyTorch张量,以便我们可以将它们输入模型。然后,我们将显示张量。

对于本章中的步骤,您将需要一张内容图像和一张风格图像。您可以使用本章脚本提供的图像,也可以使用您自己的任何其他图像。如果您选择后者,将您的图像重命名为content.jpg和style.jpg,并将它们放入名为data的文件夹中,该文件夹位于本章中使用的脚本的相同位置。内容图像可以是日常图片。对于风格图像,试着找到一个艺术图像

#1. 加载内容和风格图像
from PIL import Image
path2content="./data/content.jpg"
path2style="./data/style.jpg"
content_img=Image.open(path2content)
style_img=Image.open(path2style)

#2. 定义图像变换函数
import torchvision.transforms as transforms
h,w=256,384
mean_rgb=(0.485,0.456,0.406)
std_rgb=(0.229,0.224,0.225)
transformer=transforms.Compose([
			transforms.Resize((h,w)),
			transforms.ToTensor(),
			transforms.Normalize(mean_rgb,std_rgb)])
#3.将PIL图像转换为tensors
# 将content_img经过transformer
content_tensor=transformer(content_img)
print(content_tensor.shape, content_tensor.requires_grad)
# torch.Size([3,256,384]) False

# 将style_img经过transformer
style_tensor=transformer(style_img)
print(style_tensor.shape,style_tensor.requires_grad)
# torch.Size([3,256,384]) False

#4. 显示经过transformer后的内容图像
import torch
def imgtensor2pil(img_tensor):
	img_tensor_c=img_tensor.clone().detach()
	img_tensor_c*=torch.tensor(std_rgb).view(3,1,1)
	img_tensor_c+=torch.tensor(mean_rgb).view(3,1,1)
	img_tensor_c=img_tensor_c.clamp(0,1)
	img_pil=to_pil_image(img_tensor_c)
	return img_pil
# 显示content_tensor
import matplotlib.pylab as plt
form torchvision.transforms.functional import to_pil_image
plt.imshow(imgtensor2pil(content_tensor))
plt.title("content image")

# 显示经过transformer后的风格图像
plt.imshow(imgtensor2pil(style_tensor))
plt.title("style image")

内容图像
风格图像
正如我们观察到的,内容和样式图像都被调整为(256,384)。在下一个教程中,我们将加载预先训练好的模型。

代码解析:
在这个教程中,我们实现了加载风格和内容图像并将它们转换为张量的代码,以便将它们提供给模型。

在第1步中,我们将内容和风格图像作为PIL对象加载。我们使用了本章脚本中提供的两个图像。但是,您可以用自己的内容和风格图像替换它们。确保将图像复制到与脚本相同位置的名为data的文件夹中。

在步骤2中,我们将内容和风格图像转换为张量。为此,我们应用了torchvision包中的三个变换函数:Resize,ToTensor和Normalize。

我们使用Resize方法将图像的大小调整为(256,384)。理论上,您可以使用任何大小的图像作为内容和风格图像。实际上,这些大小受到计算机或CUDA设备上可用内存的限制。同时,图像尺寸越大,算法完成时间越长。

然后我们使用ToTensor方法将PIL对象转换为PyTorch张量。这个函数将把像素的值缩放到[0,1]的范围。接下来,我们通过减去RGB均值并除以RGB标准差(STD)来标准化张量。预定义的RGB平均值和标准偏差值取决于用于特征提取的预训练模型。在我们的案例中,VGG19模型在ImageNet数据集上被预先训练,对ImageNet数据集计算RGB均值和STD均值。

在步骤3中,我们使用步骤2中定义的变换函数将内容和格式图像转换为标准化张量。看看张量的形状。正如你所看到的,图像被调整大小为形状[3,256,384]的张量。另外,检查张量的requires_grad属性。因为没有对内容和格式图像进行优化,所以这个属性应该是False。

在第4步中,我们显示了内容和格式张量。为了能够可视化张量,我们定义了imgtensor2pil辅助函数来将张量转换回PIL图像。请注意,此辅助函数的定义仅用于可视化目的。辅助函数的输入如下:

  • img_tensor:形状为[3,height,width]的张量

在这个函数中,我们首先.clone()克隆了张量,以防止对原始张量做出任何改变。由于张量之前使用零均值-单位方差归一化方法进行了归一化,所以我们将张量反归一化回到其原始值。这是通过将其值乘以RGB标准差,然后加上RGB均值来实现的。接下来,我们使用.clamp()方法确保值在[0,1]范围内。最后,使用torchvision包中的to_pil_image函数将张量转换为PIL图像。

实现风格迁移

正如您在系列中所看到的,我们遵循了几个标准步骤:加载输入和目标数据,定义一个模型、一个目标函数和优化器,然后使用梯度下降算法更新模型参数来训练模型。在所有这些过去的案例中,在训练过程中对模型的输入保持不变,并且对模型进行更新。

现在,想象这样一种情况:我们保持模型参数不变,而在训练期间更新模型的输入。这种扭曲是神经风格迁移算法背后的直觉。

具体来说,神经风格迁移算法的工作原理如下:

  • 1.取一个预先训练的分类模型(例如,VGG19),去掉最后一层,保留剩下的层作为特征提取器。
  • 2.将内容图像提供给模型,并获得作为目标内容的选定特征。
  • 3.将风格图像输入模型,得到所选特征的Gram矩阵作为目标风格。
  • 4.将输入输入到模型中,得到所选特征的特征和Gram矩阵,分别作为预测的内容和风格。
  • 5.计算内容和样式损失,并使用此信息更新输入并减少损失。
  • 6.重复步骤4,直到损失最小化。

神经风格迁移算法的框图如下图所示:
神经风格迁移算法的框架图
在下面的部分中,我们将下载示例内容和样式图像,并实现算法。
我们将开发一个神经风格迁移算法。该算法有多个步骤,我们将介绍每个主要步骤。

加载预训练模型

# 我们将加载VGG19作为预训练模型,并冻结其参数:
#1. 加载预训练模型
import torchvision.models as models
device=torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model_vgg=models.vgg19(pretrained=True).features.to(device).eval()
print(model_vgg)

# Sequential(
# (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1,1))
# (1): ReLU(inplace)
# (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1),padding=(1, 1))
# ...
# (34): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1),padding=(1, 1))
# (35): ReLU(inplace)
# (36): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1,ceil_mode=False)
# )

#2. 冻结参数
for param in model_vgg.parameters():
	params.requires_grad_(False)

定义损失函数

我们将定义辅助函数来从模型中获得中间特征并计算Gram矩阵。然后,我们将定义内容损失和风格损失。

#1. 定义辅助函数获得中间特征
def get_features(x, model, layers):
	features={}
	for name, layer in enumerate(model.children()):
		x=layer(x)
		if str(name) in layers:
			features[layers[str(name)]]=x
	return features

#2. 定义计算tensor的Gram矩阵的辅助函数
def gram_matrix(x):
	n,c,h,w=x.size()
	x=x.view(n*c,h*w)
	gram=torch.mm(x, x.t())
	return gram
#3. 定义计算内容损失的辅助函数
import torch.nn.functional as F
def get_content_loss(pred_features, target_features, layer):
	target=target_features[layer]
	pred=pred_features[layer]
	loss=F.mse_loss(pred, target)
	return loss

#4. 定义计算风格损失的函数
def get_style_loss(pred_features, target_features, style_layers_dict):
	loss=0
	for layer in style_layers_dict:
		pred_fea=pred_features[layer]
		pred_gram=gram_matrix(pred_fea)
		n,c,h,w=pred_fea.shape
		target_gram=gram_matrix(target_features[layer])
		layer_loss=style_layers_dict[layer]*F.mse_loss(pred_gram,target_gram)
		loss += layer_loss/(n*c*h*w)
	return loss
#5. 获取内容和风格图像的特征
feature_layers={
	"0":"conv1_1",
	"5":"conv2_1",
	"10":"conv3_1",
	"19":"conv4_1",
	"21":"conv4_2",
	"28":"conv5_1"
}
con_tensor=content_tensor.unsqueeze(0).to(device)
sty_tensor=style_tensor.unsqueeze(0).to(device)

content_features=get_features(con_tensor, model_vgg, feature_layers)
style_features=get_features(style_tensor,model_vgg, feature_layers)

# 出于调试目的,让我们打印内容特征的形状:
for key in content_features.keys():
	print(content_features[key].shape)
# torch.Size([1, 64, 256, 384])
# torch.Size([1, 128, 128, 192])
# torch.Size([1, 256, 64, 96])
# torch.Size([1, 512, 32, 48])
# torch.Size([1, 512, 32, 48])
# torch.Size([1, 512, 16, 24])

定义优化器

首先,初始化输入张量和优化器:

#1. 初始化内容张量
input_tensor=con_tensor.clone().requires_grad_(True)
#2. 定义优化器
from torch import optim
optimizer=optim.Adam([input_tensor], lr=0.01)

运行算法

我们将运行神经风格秦迁移算法来生成一个艺术图像:

#1. 设置超参数
num_epochs=301
content_weight=1e1
style_weight=1e4
content_layer="conv5_1"
style_layers_dict={
	"conv1_1": 0.75,
	"conv2_1": 0.5,
	"conv3_1": 0.25,
	"conv4_1": 0.25,
	"conv5_1": 0.25
}
#2. 运行算法
for epoch in range(num_epochs):
	optimizer.zero_grad()
	input_features=get_features(intput_tensor, model_vgg, feature_layers)
	content_loss=get_content_loss(input_features,content_features,content_layer)
	style_loss=get_style_loss(input_features, style_features,style_layers_dict)
	neural_loss=content_weight*content_loss + style_weight*style_loss
	neural_loss.backward(retain_graph=True)
	optimizer.step()
	if epoch % 100==0:
		print("epoch {}, content loss: {:.2f}, style loss: {:.2f}".format(epoch, content_loss, style_loss))
# epoch 0, content loss: 0.0, style loss 3.3e+02
# epoch 100, content loss: 3.6, style loss 9e+01
# epoch 200, content loss: 3.9, style loss 5.6
# epoch 300, content loss: 4.0, style loss 3.3

#3. 显示结果
plt.imshow(imgtensor2pil(input_tensor[0].cpu()))

在这里插入图片描述
代码解析

(1)在加载预训练模型这一小节中,我们加载了一个预训练模型作为特征提取器。

正如在步骤1中观察到的,我们使用了在ImageNet数据集上预训练的torchvision包中的VGG19模型。该模型期望输入是形状(3,高度,宽度)的小批数据,并归一化。这就是我们在加载数据部分中变换内容和风格图像的原因。

在第二步中,我们使用requires_grad_方法冻结模型参数,以避免在算法优化过程中对模型的任何更改。

(2)在定义损失函数小节中,我们定义了内容和风格损失函数。为此,我们定义了几个辅助函数。

在第1步中,我们定义了get_features辅助函数。该辅助函数用于获取预训练模型的中间特征。这些特性将用于计算风格和内容损失值。辅助函数的输入如下:

  • x:形状为[1,3,height,width]的Pytorch张量
  • model:预训练模型对象
  • layers:一个Python字典,包含层名和索引

在辅助函数中,我们遍历模型层,将输入张量x传递到每一层,并得到它的输出。如果层名包含在layers字典中,我们将收集它的输出。辅助函数将收集到的特征作为字典返回。

在第2步中,我们定义了gram_matrix辅助函数来计算张量的Gram矩阵。Gram矩阵将用于计算风格损失值。函数输入如下:

  • x:形状为[1,c,h,w]的张量,c,h,w分别表示通道,高和宽

输入张量x来自模型的中间特征。在辅助函数中,我们将x从一个4D张量调整为一个2D张量,然后计算Gram矩阵。输出是一个形状为[c, c]的张量。

在第3步中,我们定义了get_content_loss辅助函数来计算内容损失值。辅助函数的输入如下:

  • pred_features:一个Python字典,包含给定输入张量的模型的中间特征
  • target_features:一个Python字典,包含给定内容张量的模型的中间特征
  • layer:包含层名的字符串

像任何典型的损失函数一样,在这里,我们想计算目标和预测值之间的距离。在辅助函数中,我们提取了参数layer指定的层的目标张量和预测张量。然后计算两个张量之间的均方误差(MSE)并返回其值。

在第4步中,我们定义了get_style_loss辅助函数来计算样风格损失。辅助函数的输入如下:

  • pred_features:一个Python字典,包含给定输入张量的模型的中间特征

  • target_features:一个Python字典,包含给定风格张量的模型的中间特征

  • style_layers_dict:包含风格损失中包含的层的名称和权重的Python字典

    在辅助函数中,我们迭代风格层,并提取每层的预测和目标张量。然后,我们计算了这两个张量的Gram矩阵,并用它们来计算MSE。每层计算损失值,乘以层权值,归一化后相加。该函数返回风格损失中包含的所有层的累计损失值。

在第5步中,我们调用get_features辅助函数来获得内容和风格特征。为了获得特征,我们将内容张量和样式张量传递给辅助函数。请注意,我们使用.unsqueeze方法为张量添加了一个维度,因为模型输入形状是[1,3,height, width]。层的名称和索引在Python字典feature_layers中定义。

仅出于调试目的,我们打印了每一层内容特征的形状。

(3)在定义优化器小节中,我们定义了输入张量和优化器,以便能够根据损失值更新输入。

在步骤1中,我们定义了输入张量。如果你还记得,神经风格迁移算法的目标是更新输入以最小化损失函数。输入可以随机初始化或使用内容图像。正如我们观察到的,我们将内容张量克隆为输入张量。注意,requires_grad方法应该设置为True,因为我们希望能够更新输入张量。

在步骤2中,我们定义了优化器。我们从torch.optim中选择了Adam优化器。注意,我们将input_tensor作为参数传递给优化器。您还可以使用optim包中可用的其他优化器,比如LBFGS。

(4)在“运行算法”小节中,我们将所有的代码拼接在一起,并执行了神经风格迁移算法。

在步骤1中,我们定义了超参数。查看参数content_weight和style_weight。这些参数定义了内容损失和风格损失在整体损失值中的贡献。与content_weight相比,通常需要更高的style_weight参数,但是您可以使用这些值来查看它们的作用。

content_layer参数定义了内容损失中包含的层的名称。这里,我们使用内容层中的conv5_1。您可以将此参数设置为不同的层,并查看对结果的影响。

style_layer_dict参数是一个字典,它定义了风格损失中包含的层的名称和权重。正如观察到的,包括了五个层。也可以根据需要修改此参数。您可以从字典中删除一层,或更改权重,从而获得略有不同的结果。

在步骤2中,所有的材料都准备好了,我们运行了完整的神经风格迁移算法。正如观察到的,输入张量被传递给get_features辅助函数,并获得input_features。注意,input_features是模型预测的特征。然后,我们调用get_content_loss辅助函数来获取content_loss。接下来,我们调用get_style_loss辅助函数来获取style_loss。然后结合内容损失和风格损失计算总损失。下一步,使用.backward方法,计算总损失相对于输入张量的梯度。最后,利用优化器的.step方法更新输入张量。

正如所观察到的,优化器随着优化的进行更新输入张量。由于我们用内容张量初始化输入张量,内容损失从零开始逐渐增加。风格损失从较高的值开始,随着输入张量的更新逐渐减少。

在第3步中,我们显示了优化完成后的结果(input_tensor)。注意,我们使用imgtensor2pil辅助函数将张量转换为PIL图像。正如我们观察到的,我们的神经风格迁移算法工作很好,得到的图像得到了风格图像的纹理信息。

您现在可以尝试使用不同的图像作为内容或样式图像,并生成新的艺术图像。

您可以使用其他优化器,如原始论文中提出的LBFGS,而不是Adam优化器。

试试torch.optim包中的LBFGS优化器,看看它如何影响输出。

同时,我们用内容张量初始化输入张量。您可以尝试用随机值初始化输入张量并重新运行算法。您可能需要调整超参数来获得您想要的结果。

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值