NormanZyq
发布于 2020-10-21 / 309 阅读
0
0

PyTorch 学习笔记 第6课图像风格迁移和GAN-Part1

Neural Style Transfer 图像风格迁移

这个主题好像无法显示公式,所以本文公式的显示可能存在问题

给一张图片,和另一种风格的图片(例如真人和二次元),希望把风格迁移到另一张上。

有什么问题:

  1. 如何定义图片的“内容”和图片的“风格”

    • 可以用 VGG Network 表示:
      • 内容图片
      • 风格图片
      • 风格迁移后的新图片
  2. 损失函数如何定义

    • Content Loss
      L_{content} (\vec{p}, \vec{x}, l) = \frac{1}{2} \sum_{i, j} (F^{l}_{ij} - P^{l}_{ij})^2
    • Gram Matrix 可以用来表示两张图片的texture相似度
      G^{l}_{ij} = \sum_{k} (F^{l}_{ik}F^{l}_{jk})
    • Style Loss
      F_{l} = \frac{1}{4N^2_{l}M^2_{l}}\sum_{i, j} (G^l_{ij} - A^l_{ij})^2

准备工作

引入和device

%matplotlib inline
# 由于 %matplotlib inline 的存在,当输入plt.plot(x,y_1)后,不必再输入 plt.show(),图像将自动显示出来

from __future__ import division
from torchvision import models, transforms
import torchvision
from PIL import Image
import argparse
import torch
import torch.nn as nn
import numpy as np

import matplotlib.pyplot as plt

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

定义一些辅助的东西

  • 读取图片
def load_image(image_path, transform=None, max_size=None, shape=None):
    image = Image.open(image_path)
    if max_size:
        scale = max_size / max(image.size)
        size = np.array(image.size) * scale
        image = image.resize(size.astyle(int), Image.ANTIALIAS)

    if shape:
        image = image.resize(shape, Image.LANCZOS)

    if transform:
        image = transform(image).unsqueeze(0)

    return image.to(device)
  • transform

这里的Normalize为什么均值和标准差已经填上了具体的数?因为是老师从ImageNet里面查到的,所以就直接用了。

transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
  • 读取两张图片试试看(一张是内容,一张是风格)
content = load_image('png/content.png', transform, max_size=400)
style = load_image('png/style/png', transform, shape=[content.size(2), content.size(3)])
  • unloader(用于将Tensor转换回图片格式,方便展示)
unloader = transforms.ToPILImage()  # reconvert into PIL image
plt.ion()
def imshow(tensor, title=None):
    image = tensor.cpu().clone()
    image = image.squeeze(0)
    image = unloader(image)
    plt.imshow(image)
    if title is not None:
        plt.title(title)
    plt.pause(0.001)
plt.figure()
imshow(style[0], title='Image')

直接这样展示出来的是被标准化过的图片,如果需要查看原图,需要把上面的transform中的Normalize给删掉。

模型定义

接下来会用到models.vgg19(),所以首先看一下这是个什么东西。

因为使用预训练的模型需要下载500多M,我没下载,所以整份代码就没有跑,只是抄下来了代码,加上了一点自己的理解。

vgg = models.vgg19(pretrained=True)	# True的话是使用预训练过的模型,可能需要下载
print(vgg)

可以看到这个模型包含features、avgpool、classifier,如果我们只要features,就可以加上.features即可

vgg.features

下面定义的VGGNet模型并不是我们最重要训练的模型,仅仅是我们的特征提取器。

class VGGNet(nn.Module):
    def __init__(self):
        super(VGGNet, self).__init__()
        self.select = ['0', '5', '10', '19', '28']
        self.vgg = models.vgg19(pretrained=True).features

    def forward(self, x):
        features = []
        for name, layer in self.vgg._modules.items():
            x = layer(x)
            if name in self.select:
                features.append(x)
        return features

vgg = VGGNet().to(device).eval()	# 因为不是要训练这个东西,所以调到eval模式
target = content.clone().requies_grad_(True)
optimizer = torch.optim.Adam([target], lr=0.003, betas=[0.5, 0.999])

我们要做的是优化这个target图片(Tensor),并非是上面的模型。

训练模型

total_step = 2000

for step in range(total_step):
    target_features = vgg(target)
    content_features = vgg(content)
    style_features = vgg(style)
    content_loss = style_loss = 0.
    for f1, f2, f3 in zip(target_features, content_features, style_features):
        content_loss += torch.mean((f1 - f2) ** 2)
        _, c, h, w = f1.size()
        f1 = f1.view(c, h * w)
        f3 = f3.view(c, h * w)

        # 作点积
        f1 = torch.mm(f1, f1.t())
        f3 = torch.mm(f3, f3.t())
        style_loss += torch.mean((f1 - f3) ** 2) / (c * h * w)

    loss = content_loss + style_loss * 100.

    # 更新target image的Tensor
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if step % 100 == 0:
        print('Step [{}/{}], Content Loss: {:.4f}, Style Loss: {:.4f}'
              .format(step, total_step, content_loss.item(), style_loss.item()))

显示图片

现在得到了风格迁移的图片,显示出来的话需要先denormalization。

# 这些参数怎么来的我也不知道,老师说从ImageNet抄过来的,可以实现之前标准化的反操作
denorm = transforms.Normalize([-2.12, -2.04, -1.80], [4.37, 4.46, 4.44])
img = target.clone().squeeze()
img = denorm(img).clamp_(0, 1)
# plt.figure()
imshow(img, title='Target Image')

因为我没有准备数据集,所以这部分的代码我没有跑。

总结

为了完成图像风格迁移,我们做了些什么?(把老师说的话写了下来,但是我还是没太能理解)

  1. 准备一张图像内容的图像和一张图像风格的图像
  2. 为了定义图片的内容,就使用了VGGNet将图片的一些关键的feature提取了出来;图片的内容就是直接用层的vector表示,所以这个图片要跟原图片的L2 Loss越小越好;
  3. 风格表示使用Gram Matrix,跟风格图片的L2 Loss越小越好

Generative Adversarial Network 生成式对抗网络

  • Generator 生成器:目标是让生成的数据更接近真实数据
  • Discriminator 分类器:目的是能够鉴别真实数据和生成的假数据

现在有一些真实的图片给Discriminator训练,同时Generator用一些东西生成假的数据,而Discriminator是用来判断假数据,Generator生成假数据,希望能够“骗过”Discriminator,其中形成对抗关系

定义模型

batch_size = 32
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize([0.5],[0.5])
])

mnist_data = torchvision.datasets.MNIST('./mnist_date',
                                        train=True,
                                        download=True,
                                        transform=transform)

dataloader = torch.utils.data.DataLoader(dataset=mnist_data,
                                        batch_size=batch_size,
                                        shuffle=True)

plt.imshow(next(iter(dataloader))[0][0][0])

这里的dataloader是一个list,每个元素是一个batch,下面用一小段代码解释dataloader的格式:

temp = next(iter(dataloader))[0]
temp.shape 	# torch.Size([32, 1, 28, 28])

这里的32就是刚刚设置的btach_size,后面三个元素是图片的大小,也就是说,图片都是1 * 28 * 28的(黑白和28 * 28)。

判别器、生成器、optimizer

ReLU()在x小于0的时候会变成0,大于0才是x,但是LeakyReLU(0.2)会使小于0的变成0.2 * x

image_size = 28 * 28
hidden_size = 256

# 判别器
D = nn.Sequential(
    nn.Linear(image_size, hidden_size),
    nn.LeakyReLU(0.2),  #
    nn.Linear(hidden_size, hidden_size),
    nn.LeakyReLU(0.2),
    nn.Linear(hidden_size, 1),
    nn.Sigmoid()
)

# 生成器
latent_size = 64
G = nn.Sequential(
    nn.Linear(latent_size, hidden_size),
    nn.ReLU(),
    nn.Linear(hidden_size, hidden_size),
    nn.ReLU(),
    nn.Linear(hidden_size, image_size),
    nn.Tanh()
)

D = D.to(device)
G = G.to(device)

loss_fn = nn.BCELoss()
d_optimizer = torch.optim.Adam(D.parameters(), lr=0.0002)
g_optimizer = torch.optim.Adam(G.parameters(), lr=0.0002)

训练模型

total_steps = len(dataloader)
num_epochs = 30
for epoch in range(num_epochs):
    for i, (images, _) in enumerate(dataloader):
        batch_size = images.shape[0]    # 就是前面那个32
        images = images.reshape(batch_size, image_size).to(device)

        real_labels = torch.ones(batch_size, 1).to(device)
        fake_labels = torch.zeros(batch_size, 1).to(device)

        outputs = D(images)     # 这是真实的图片
        d_loss_read = loss_fn(outputs, real_labels)     # 所以我希望分类器能够分类到真实的上面
        real_score = outputs    # 对于D来说,越大越好

        # 随机生成假图片
        z = torch.randn(batch_size, latent_size).to(device)  # latent variable 隐变量
        fake_images = G(z)
        outputs = D(fake_images.detach())   # 为什么要detach?
        d_loss_fake = loss_fn(outputs, fake_labels)
        fake_score = outputs    # 对于D来说,越小越好;对G来说越大越好

        # 优化discriminator
        d_loss = d_loss_read + d_loss_fake
        d_optimizer.zero_grad()
        d_loss.backward()
        d_optimizer.step()

        # 优化generator
        outputs = D(fake_images)
        g_loss = loss_fn(outputs, real_labels)  # 这里想表示的是把假图片作为输入以后,D的输出距离真实值有多远

        d_optimizer.zero_grad()
        g_optimizer.zero_grad()
        g_loss.backward()
        g_optimizer.step()

        if i % 200 == 0:
            print('Epoch [{}/{}], Step [{}/{}], d_loss: {:.4f}, g_loss: {:.4f}, D(x): {:.2f}, D(G(z)): {:.2f}'
                  .format(epoch, num_epochs, i, total_steps, d_loss.item(), g_loss.item(),
                          real_score.mean().item(), fake_score.mean().item()))

调用detach返回一个新的tensor,从当前计算图中分离下来的,但是仍指向原变量的存放位置,不同之处只是requires_grad为false,得到的这个tensor永远不需要计算其梯度,不具有grad。

训练的输出应该是一个互相“对抗”的过程,所以两个loss都是时高时低,如果一个突然降低很多的话,另一个模型的效果肯定不好。下面是我训练的部分输出:

Epoch [0/30], Step [0/1875], d_loss: 1.4368, g_loss: 0.7075, D(x): 0.47, D(G(z)): 0.49
Epoch [0/30], Step [200/1875], d_loss: 0.0947, g_loss: 3.7515, D(x): 0.98, D(G(z)): 0.07
Epoch [0/30], Step [400/1875], d_loss: 0.6226, g_loss: 4.6492, D(x): 0.72, D(G(z)): 0.16
Epoch [0/30], Step [600/1875], d_loss: 0.0493, g_loss: 5.2923, D(x): 0.98, D(G(z)): 0.03
Epoch [0/30], Step [800/1875], d_loss: 0.0356, g_loss: 5.4317, D(x): 0.99, D(G(z)): 0.03
Epoch [0/30], Step [1000/1875], d_loss: 0.0553, g_loss: 7.3380, D(x): 0.96, D(G(z)): 0.01
Epoch [0/30], Step [1200/1875], d_loss: 0.1294, g_loss: 3.1940, D(x): 0.97, D(G(z)): 0.09
Epoch [0/30], Step [1400/1875], d_loss: 0.0399, g_loss: 4.7674, D(x): 0.99, D(G(z)): 0.03
Epoch [0/30], Step [1600/1875], d_loss: 0.2040, g_loss: 3.8322, D(x): 0.92, D(G(z)): 0.07
Epoch [0/30], Step [1800/1875], d_loss: 0.3724, g_loss: 3.6350, D(x): 0.85, D(G(z)): 0.02

看看效果

生成一些假图片

z = torch.randn(batch_size, latent_size).to(device)
fake_images = G(z)
fake_images = fake_images.view(batch_size, 28, 28).data.cpu().numpy()

plt.imshow(fake_images[0], cmap=plt.cm.gray)

image-20201020163510925

效果还不错,可以看出来是个数字6。


评论