pytorch 1.1.0
torchvision 0.3.0
cuda 9.0
数据集用的是COCO2014的train2014训练集,使用ImageNet也可以
需要用到在ImageNet上预训练好的VGG16,模型文件为vgg16-397923af.pth
首先需要明白一点,深度学习之所以被称为“深度”,就在于它采用了深层的网络结构,网络的不同层学到的是图像不同层面的特征信息。
研究表明,几乎所有神经网络的第一层学习到的都是关于线条和颜色的信息(直观理解就是像素组成色彩,点组成线)。再往上,神经网络开始关注一些复杂的特征(比如拐角或特殊形状等),这些特征可以看成是低层次的特征组合。随着深度的加深,神经网络关注的信息逐渐抽象(例如有的卷积核关注嘴巴,有的关注眼睛等,以及关注对象之间的空间关系等)。
在进行风格迁移时,并不要求生成图片的像素和输入图片中每一个像素都一样,而追求的是生成图片和输入图片具有相同的特征(比如原图是个人,我们希望风格迁移之后的图片依据能明显看出来是个人)。但这是不是就类似神经网络中最后的层的输出(差不多也就是最后的卷积核关注的特征)?但是这最后一层的特征的抽象程度太高,我们不可能只希望最后只从生成图片中看出来是个人,我们很是希望生成图片中能保存输入图片的部分细节信息(五官、动作等),这些信息相对来说抽象程度没那么高。因此我们使用中间某些层的特征作为目标,希望输入图片和风格迁移的生成图片在这些层的特征尽可能的相似,即将图片在深度模型中的中间某些层的输出作为图像的的知觉特征。
注:这就是下面要使用VGG-16中不同激活层(ReLU)输出的原因所在。
一般使用Gram矩阵来表示图像的风格特征。对于每一张图片,卷积层的输出形状为C×H×W,C是卷积核的通道数,一般称为有C个卷积核,每个卷积核学习图像的不同特征。每一个卷积核输出的H×W代表这张图像的一个feature map(特征图),最后所有的卷积和输出组合成feature maps。通过计算每个feature map之间的相似性,可以得到图像的风格特征。
对于一个C×H×W的feature maps F ,Gram Matrix的形状为C×C,其第i、j个元素Gi,j的计算方式定义如下:
G i , j = ∑ k F i k F j k G_{i,j} = \sum_kF_{ik}F_{jk} Gi,j=k∑FikFjk
其中Fik代表第i个feature map的第k个像素点。
关于Gram Matrix,要注意:
实践证明Gram Matrix的特点:注重风格纹理等特征,忽略空间信息。图像的空间信息在计算Gram Matrix时都被舍弃,但是纹理、色彩等风格信息被保存下来。Gram Matrix表征图像的风格特征在风格迁移、纹理合成等任务中的表现十分出众。
风格迁移的图片想要做到逼真。第一是要生成的图片在内容、细节上尽可能地与输入的内容图片相似;第二要生成的图片在风格上尽可能地与风格图片相似。因此,下面会用损失content loss和style loss来衡量这两个指标。
在下面我们会实现一种快速风格迁移算法——Fast Style Transfer,其更多被称为Fast Neural Style。
Fast Neural Style专门设计了一个网络用来进行风格迁移,输入原图片,网络将自动生成目标图片。这个网络会将输入的风格图片训练成一个相对应的风格网络,接下来输入我们想进行转换的图片,很快就能完成一次风格迁移。
在Fast Neural Style的网络结构中,x是输入图像。在风格迁移任务中yc=x,ys是风格图片,Image Transform Net fw是我们设计的风格迁移网络,针对输入的图像x,能够返回一张新的图像y**。**y在图像内容上与yc相似,但在风格上与ys相似。损失网络(Loss Network)不用训练,只用来计算知觉特征和风格特征。损失网络,采用在ImageNet上预训练好的VGG-16。
VGG-16网络,从上到下有5个卷积块,两个卷积块间通过MaxPooling层区分。每个卷积块有2~3个卷积层,每一个卷积层后面都跟着一个ReLU激活层。
Fast Neural Style的训练步骤:
(1)输入一张图片x 到 fw(风格迁移网络) 中得到 y^(生成的图片)。
(2)将 y^ 和 yc(就是x) 输入到Loss Network(VGG-16)中,计算它在relu3_3(表示第3个卷积块的第3个卷积层的,激活层)的输出,并计算它们之间的均方误差作为content loss(生成的图片在内容、细节上与输入的内容图片的相似程度)。
(3)将 y^ 和 ys(风格图片)输入到Loss Network中,计算它在 relu1_2、relu2_2、relu3_3和relu4_4 的输出,再计算它们的Gram Matrix 的均方误差作为 style loss(生成的图片在风格上与风格图片的相似程度)。
(4)两个损失相加,再反向传播。更新 fw 的参数,固定Loss Network不动(即不更新损失网络的参数)。
(5)重复(1)到(4),训练 fw。
VGG16的网络结构图如下。
PackedVGG.py
#coding:utf-8
import torch
import torch.nn as nn
from torchvision.models import vgg16
from collections import namedtuple
class Vgg16(nn.Module):
def __init__(self):
super(Vgg16, self).__init__()
features = list(vgg16(pretrained=True).features[:23]) # 把预训练的vgg16的前23层放在一个列表里,vgg16总共31层(除了全连接层)
# features的第3,8,15,22层分布是:relu1_2,relu2_2,relu3_3,relu4_3
self.features = nn.ModuleList(features).eval() # 不更新参数
def forward(self, x):
results = [] # 结果列表
for ii,model in enumerate(self.features): # 获取3,8,15,22层的relu的输出
x = model(x)
if ii in {3, 8, 15, 22}:
results.append(x)
# namedtuple创建一个具名元组,参数有,typename代表元组名称,field_names代表元组中元素的名称,rename当元素名称中有Python的关键字,则必须设置为True
vgg_outputs = namedtuple("VggOutputs",['relu1_2','relu2_2','relu3_3','relu4_3'])
# * + results表示把列表中的数据全部提出来,等于去掉最外层的[]
return vgg_outputs(*results) # 返回一个具名元组,元组的元素是3,8,15,22relu层的输出
上边我们在网络前向传播的过程中获得了中间层的输出,并保存下来,其他的层不再需要,节省了空间。
在torchvision中,VGG的实现由两个nn.Sequential对象组成。第一个是features,包括卷积、激活和池化等层,用来提取图像特征。另一个是classifier,包含全连接层,用来分类。上边通过vgg.features直接获得对应的nn.Sequential对象。这样在前向传播时,当计算完指定层的输出后,就将结果保存于一个list中,然后再使用namedtuple进行名称绑定,这样可以通过output.relu1_2访问第一个元素,方便又直观。当然也可以利用layer.register_forward_hook的方式来获取相应层的输出,但是在本例中相对比较麻烦。
风格迁移网络,参考了PyTorch的官方示例,其网络结构如下图所示。
图中(a)是网络的总体结构,左边(d)是一个残差单元的结构图,右边(b)和(c)分别是下采样和上采样单元的结构图。网络结构总结有以下几个特点。
transformer_net.py
#coding:utf-8
import torch as t
from torch import nn
import numpy as np
class TransformerNet(nn.Module):
"""
InstanceNorm:一个channel内做归一化,算H*W的均值,用在风格化迁移;
因为在图像风格化中,生成结果主要依赖于某个图像实例,所以对整个batch归一化不适合图像风格化中,因而对HW做归一化。可以加速模型收敛,并且保持每个图像实例之间的独立。
affine: 布尔值,当设为true,给该层添加可学习的仿射变换参数。
仿射变换即进行平移、旋转、转置、缩放等操作。
"""
def __init__(self):
super(TransformerNet, self).__init__()
# 下卷积层
self.initial_layers = nn.Sequential(
ConvLayer(3, 32, kernel_size=9, stride=1),
nn.InstanceNorm2d(32, affine=True),
nn.ReLU(True),
ConvLayer(32, 64, kernel_size=3, stride=2), # stride为2,使得特征图尺寸缩小1/2
nn.InstanceNorm2d(64, affine=True),
nn.ReLU(True),
ConvLayer(64, 128, kernel_size=3, stride=2),
nn.InstanceNorm2d(128, affine=True),
nn.ReLU(True)
)
# Residual layers(残差层)
self.res_layers = nn.Sequential(
ResidualBlock(128),
ResidualBlock(128),
ResidualBlock(128),
ResidualBlock(128),
ResidualBlock(128)
)
# Upsampling Layers(上卷积层)
self.upsample_layers = nn.Sequential(
UpsampleConvLayer(128, 64, kernel_size=3, stride=1, upsample=2), # upsample取2是因为stride取1,为了使特征图的尺寸扩大2倍
nn.InstanceNorm2d(64, affine=True),
nn.ReLU(True),
UpsampleConvLayer(64, 32, kernel_size=3, stride=1, upsample=2),
nn.InstanceNorm2d(32, affine=True),
nn.ReLU(True),
ConvLayer(32, 3, kernel_size=9, stride=1)
)
def forward(self, x):
out = self.initial_layers(x) # 下采样的卷积层
out = self.res_layers(out) # 深度残差层
out = self.upsample_layers(out) # 上采样的卷积层
return out
# 卷积模块
class ConvLayer(nn.Module):
"""
add ReflectionPad for Conv
这里使用了边界反射填充
"""
def __init__(self, in_channels, out_channels, kernel_size, stride):
super(ConvLayer, self).__init__()
# 计算反射填充的层数
reflection_padding = int(np.floor(kernel_size / 2)) # np.floor 返回不大于输入参数的最大整数。
# 定义反射填充层 H(out) = H(in) + paddingTop + paddingBottom
# 高加上下,宽加左右,平常我们的tensor是(B,C,H,W)
self.reflection_pad = nn.ReflectionPad2d(reflection_padding) # ReflectionPad2d的参数有四个,比如传一个元组(3,3,5,5) last right top bottom,只传一个数表示上下左右都是同一个数
# 定义卷积层
self.conv2d = nn.Conv2d(in_channels, out_channels, kernel_size, stride)
def forward(self, x):
# 在经过卷积层前先给x加个反射填充层,例如例如,(64,3,220,220) 过(3,3,5,5)得到(64,3,230,226),似乎我们一般都是上下左右加同一个数,做到镜像
out = self.reflection_pad(x) # 这里上下左右加同一个数,使得特征图的H和W增大了
out = self.conv2d(out)
return out
# 上采样模块
class UpsampleConvLayer(nn.Module):
"""
这里也使用了边界反射填充
先上采样,然后做一个卷积(Conv2d),而不是采用ConvTranspose2d,这种效果更好,为了避免棋盘效应
比如,这里的kernel_size取3,stride取1,upsample取2,输入x的尺寸为2×2
那么,经过nn.functional.interpolate()后特征图变成4×4
经过reflection_pad()后特征图变成6×6,再通过conv2d()后特征图恢复到4×4
"""
def __init__(self, in_channels, out_channels, kernel_size, stride, upsample=None):
super(UpsampleConvLayer, self).__init__()
self.upsample = upsample # 上采样的标志,用来表示特征图扩大的倍数
# 下面同上面的ConvLayer
reflection_padding = int(np.floor(kernel_size / 2)) # 计算反射填充的层数
self.reflection_pad = nn.ReflectionPad2d(reflection_padding)
self.conv2d = nn.Conv2d(in_channels, out_channels, kernel_size, stride)
def forward(self, x):
if self.upsample: # 判断是否上采样
# interpolate实现插值和上采样,插值算法取决于参数mode的设置,默认为最近邻nearest,scale_factor指定输出为输入的多少倍数
x = t.nn.functional.interpolate(x, scale_factor=self.upsample)
out = self.reflection_pad(x)
out = self.conv2d(out)
return out
# 残差层
class ResidualBlock(nn.Module):
"""
这里kernel_size取3,使得reflection_padding反射填充的层数取1,
通过reflection_pad层就会给原特征图H和C各增加2,
再经过一个(k=3,p=0,s=1)的卷积后特征图的H和C又各减去2,
因此维持了经过self.conv1层的特征图的尺寸不变。
由此,可以明白,这里的残差网络加深了网络,且没有改变原本特征图的尺寸。
"""
def __init__(self, channels):
super(ResidualBlock, self).__init__()
self.conv1 = ConvLayer(channels, channels, kernel_size=3, stride=1)
self.in1 = nn.InstanceNorm2d(channels, affine=True)
self.conv2 = ConvLayer(channels, channels, kernel_size=3, stride=1)
self.in2 = nn.InstanceNorm2d(channels, affine=True)
self.relu = nn.ReLU()
def forward(self, x):
identity = x # 先将本体保存下来
out = self.conv1(x)
out = self.in1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.in2(out)
out = out + identity # 将原本的x和最后的out相加在一次,实现残差
return out # 这里或许可以再通过一个激活层,这样self.relu(out),但我没有这样实验过
**nn.functional.interpolate()**在插值算法上可以使用的上采样算法,有’nearest’,‘linear’,‘bilinear’,‘bicubic’,‘trilinear’和’area’,默认采用’nearest’。
可以这样直观的观察interpolate()是如何使用的
input = torch.arange(1, 5, dtype=torch.float32).view(1, 1, 2, 2)
input
tensor([[[[1., 2.],
[3., 4.]]]])
x = F.interpolate(input, scale_factor=2, mode='nearest')
x
tensor([[[[1., 1., 2., 2.],
[1., 1., 2., 2.],
[3., 3., 4., 4.],
[3., 3., 4., 4.]]]])
x = F.interpolate(input, scale_factor=2, mode='bilinear', align_corners=True)
x
tensor([[[[1.0000, 1.3333, 1.6667, 2.0000],
[1.6667, 2.0000, 2.3333, 2.6667],
[2.3333, 2.6667, 3.0000, 3.3333],
[3.0000, 3.3333, 3.6667, 4.0000]]]])
想了解PyTorch实现上采样的方法和原理,可以参考这个博客。
工具包里实现了计算Gram Matrix、加载风格图片和batch数据标准化。
我们还可以在其中实现用于可视化的工具。
utils.py
#coding:utf-8
# 工具类
from itertools import chain
import torch as t
import torchvision as tv
import numpy as np
# 这里的均值和标准差不是0.5和0.5,而是使用他人专门计算的ImageNet上所有图片的均值和标准差
# 这更符合真实世界图片的分布,效果也比都为0.5好
IMAGENET_MEAN = [0.485,0.456,0.406] # 均值
IMAGENET_STD = [0.229,0.224,0.225] # 方差
def gram_matrix(y):
"""
使用Gram矩阵来表示图像的风格特征
输入B,C,H,W
输出B,C,C
"""
(b,ch,h,w) = y.size() # 比如1,8,2,2
features = y.view(b,ch,w*h) # 得到1,8,4
features_t = features.transpose(1,2) # 调换第二维和第三维的顺序,即矩阵的转置,得到1,4,8
gram = features.bmm(features_t) / (ch*h*w) # bmm()用来做矩阵乘法,及未转置的矩阵乘以转置后的矩阵,得到的就是1,8,8了
# 由于要对batch中的每一个样本都计算Gram Matrix,因此使用bmm()来计算矩阵乘法,而不是mm()
return gram
def get_style_data(path):
"""
加载风格图片,
输入: path, 文件路径
返回: tensor 形状为1*c*h*w, 分布大约在-2~2
"""
# 数据预处理
style_transform = tv.transforms.Compose([
tv.transforms.ToTensor(),
tv.transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
])
style_image = tv.datasets.folder.default_loader(path) # 加载数据
style_tensor = style_transform(style_image) # 图片的tensor形状为 c*h*w
return style_tensor.unsqueeze(0) # 增加一维,变成1*c*h*w
def normalize_batch(batch):
"""
batch数据标准化
输入: b,c,h,w 将分布在0~255的图片进行标准化
输出: b,c,h,w 大约-2~2
"""
mean = batch.data.new(IMAGENET_MEAN).view(1, -1, 1, 1) # .new()等于创建type和device与batch.data一样的tensor,不带参的new()无内容
std = batch.data.new(IMAGENET_STD).view(1, -1, 1, 1)
mean = (mean.expand_as(batch.data)) # 表示让mean变成和batch.data一样形状的tensor
std = (std.expand_as(batch.data))
return (batch / 255.0 - mean) / std # 返回标准化处理后的batch
上边new()中给参数,是在添加噪声,并且将数据维度变成(1,-1,1,1),比如64×64就变成了1×4096×1×1,其他维度变成1,那么-1位置就要增加其他维度的值
比如 (3,1).expand_as(3,4) 形状就从(3,1)变成了(3,4),不够的数据,就重复扩充,比如**[[1],[2],[3]] -> [[1,1,1,1],[2,2,2,2],[3,3,3,3]]**。这里是为了让单独的mean和std扩充成batch形状,方便之后batch中每张图片的标准化,这样矩阵的计算就省去了单独一张图片一张图片的标准化了。
main.py
#coding:utf-8
# 风格迁移的主程序
import torch as t
import torchvision as tv
from torch.utils import data
from torch.nn import functional as F
import tqdm
import time
import os
mean = [0.485,0.456,0.406] # 平均
std = [0.229,0.224,0.225] # 方差
# 定义各项参数类
class Config:
image_size = 256 # 图片大小
batch_size = 8 # 批处理数
data_root = 'data/coco2014/train2014/'
num_workers = 4 # 多线程加载数据
use_gpu = True # 默认使用GPU
style_path = 'style.jpeg' # 风格图片存放的路径
lr = 1e-3 # 学习率
plot_every = 100 # 每10个batch可视化一次
epoches = 2 # 训练轮数
content_weight = 1e5 # content_loss的权重
style_weight = 1e10 # style_loss的权重
model_path = None # 预训练模型的路径
save_path = 'imgs'
content_path = 'content.jpg' # 需要进行风格迁移的图片
result_path = 'output.png' # 风格迁移后结果的保存路径
# 定义训练过程
def train(**kwargs):
opt = Config() # 加载参数
# 加载数据和数据预处理
transforms = tv.transforms.Compose([
tv.transforms.Resize(opt.image_size),
tv.transforms.CenterCrop(opt.image_size),
tv.transforms.ToTensor(),
tv.transforms.Lambda(lambda x:x*255)
])
dataset = tv.datasets.ImageFolder(opt.data_root,transforms)
dataloader = data.DataLoader(dataset,opt.batch_size,num_workers=opt.num_workers,drop_last=True)
print("Dataset:",len(dataset),"Dataloader:",len(dataloader))
# Dataset: 82783 Dataloader: 10347
# 转换网络
transformer = TransformerNet().cuda()
if opt.model_path:
print("转换模型参加加载成功!")
transformer.load_state_dict(t.load(opt.model_path,map_location=lambda _s,_:_s))
# 损失网络Vgg16
vgg = Vgg16().cuda().eval()
for param in vgg.parameters():
param.requires_grad = False # 不反向传播更新vgg的参数
# 优化器
optimizer = t.optim.Adam(transformer.parameters(),opt.lr)
# 获取风格图片的数据
style = get_style_data(opt.style_path)
style = style.cuda()
# 风格图片的gram矩阵
with t.no_grad():
features_style = vgg(style) # vgg()只返回了 3,8,15,22的relu层的输出
gram_style = [gram_matrix(y) for y in features_style] # 分别计算这些层的Gram Matrix
now = time.time()
for epoch in range(opt.epoches):
c_loss = 0
s_loss = 0
for ii,(x,_) in tqdm.tqdm(enumerate(dataloader)):
# 训练
optimizer.zero_grad() # 梯度清零
x = x.cuda()
y = transformer(x) # 得到风格迁移后的图片
# 对x和y(即yc和y^)进行batch标准化处理
y = normalize_batch(y)
x = normalize_batch(x)
# 获得x和y在vgg上那四层的输出
features_y = vgg(y)
features_x = vgg(x)
# content loss 内容loss,只用到了relu3_3(在原理中有介绍)
content_loss = opt.content_weight * F.mse_loss(features_y.relu3_3,features_x.relu3_3)
# style loss 风格loss,这里四层relu都用到了
style_loss = 0
for ft_y,gm_s in zip(features_y,gram_style):
gram_y = gram_matrix(ft_y)
style_loss += F.mse_loss(gram_y,gm_s.expand_as(gram_y))
style_loss *= opt.style_weight
total_loss = content_loss + style_loss
total_loss.backward() # 反向传播
optimizer.step() # 更新参数
# 损失累加
c_loss += content_loss.item()
s_loss += style_loss.item()
if ii%opt.plot_every == 0:
# 每隔一定的batch保存风格迁移后的图片,也可以顺便把原图片也保存下来,方便对比
tv.utils.save_image((y.data.cpu()[0]*0.225+0.45).clamp(min=0,max=1),'%s/%s_output_%s.png'%(opt.save_path,epoch,ii),normalize=True,range=(-1,1))
# tv.utils.save_image((x.data.cpu()[0]*0.225+0.45).clamp(min=0,max=1),'%s/%s_input_%s.png'%(opt.save_path,epoch,ii),normalize=True,range=(-1,1))
if ii%1000 == 0: # 每隔一定batch打印一次训练信息
print("Epoch:{},C_Loss:{:.6f},S_Loss:{:.6f},Time:{:.4f}s".format(epoch,c_loss/8000,s_loss/8000,time.time()-now))
# 保存模型
t.save(transformer.state_dict(),'checkpoints/%s_style.pth'%epoch)
# 开始训练
train()
# 测试
def stylize(**kwargs):
with t.no_grad():
opt = Config()
# 加载要进行风格迁移的图片,并进行预处理
content_image = tv.datasets.folder.default_loader(opt.content_path)
content_transform = tv.transforms.Compose([
tv.transforms.ToTensor(),
tv.transforms.Lambda(lambda x:x.mul(255)) # 使用transforms.Lambda封装其为transforms策略,mul()也是矩阵的一种点乘操作,要求操作的两个矩阵维度必须一样
])
content_image = content_transform(content_image)
content_image = content_image.unsqueeze(0).cuda().detach() # upsqueeze(0)增加了一个维度,使c×h×w变成了1×c×h×w
# 模型
style_model = TransformerNet().eval() # map_location表示将GPU保存的模型加载到CPU上
style_model.load_state_dict(t.load(opt.model_path,map_location=lambda _s,_:_s))
style_model = style_model.cuda() # 或这样map_location=lambda storage, loc: storage
# 风格迁移与保存
output = style_model(content_image)
output_data = output.cpu().data[0]
tv.utils.save_image(((output_data/255)).clamp(min=0,max=1),opt.result_path)
stylize()
为上面代码说明几点:
另外除了直接调用train()和stylize()还可以通过终端的方式改变Config()类中的配置的值的方式来灵活运行训练和测试。
if __name__ == '__main__':
import fire
fire.Fire()
# 这样写需要先注释掉上面的train()和stylize()
# 然后通过终端来运行训练和测试过程,例如
python main.py train --style_path=XXX.jpeg
python main.py stylize --model_path='checkpoints/XXX.pth' --content_path=XXX.jpg --result_path=XXX.png --use_gpu=False # 这里字符串的引号是可以省略的
风格图片1
迁移后的图片
可以看到效果很是不错,风格图片的风格特征被很好的学习到,且原图的知觉特征也很明显。
下面换一个风格图片,梵高的风格
也还不错,把风格图片的颜色搭配学到了,不过艺术风格还差点意思。大家可以尝试训练其他风格的图片。
另外需要提出的一点,上面训练2个epoch花费了大约1.6个小时,是放在Kaggle上训练的,其用的GPU应该是Nvida Tesla P100。这不用我吹了,可以想想有多香把。
风格迁移非常的有趣,可以把我们的图片转换成我们喜欢的风格,且快速完成转换的过程。
我们上面实现的是Fast Neural Style,在其之前还有风格迁移开山之作Neural Style,但是Neural Style在进行风格迁移时需要花费几十分钟甚至几个小时的训练,但是其生成图片的效果会比Fast Neural Style好上一点。不过,速度是个大硬伤,我们总喜欢很快就能看到满意的结果。
除了Neural Style和Fast Neural Style,还有别的有趣又吸引人的风格迁移项目。
Adobe的图片风格深度迁移(Deep Photo Style Transfer),还有CycleGAN,它们在风格迁移上表现的尤为出色,比Fast Neural Style的效果要好上许多。其中,CycleGAN的网络结构和Fast Neural Style的transformer类似,但它采用的是GAN的训练方式,能够实现风格的双向转换(即原图片转换成风格图片,风格图片转换成原图片的风格)。这个我会在接下来抽空去接触并实践一下,有趣的东西还是要多取尝试的。
深度学习框架PyTorch入门与实践(https://github.com/chenyuntc/pytorch-book/tree/master/chapter8-%E9%A3%8E%E6%A0%BC%E8%BF%81%E7%A7%BB(Neural%20Style))
Deconvolution and Checkerboard Artifacts(https://www.jianshu.com/p/36ff39344de5)