本人主页

三个阶段:

emoji集合~

[toc]

前言

在基本的GAN熟知了之后,我们先聊一聊上次的第一部分做了什么:首先,我们有一堆按照某个规律分布的数据,但是我们完全不知道它的特点,比如在这里我们就在这两条线之间生成了样例数据,显然,这个样例数据似乎也长着一个二次曲线的样子。

在这里插入图片描述

经过GAN的对抗之后,生成器生成的数据已经很接近二次曲线形状了。并且当我把样例数据的分布改变——比如改成在$y=x+1$附近的时候,也能够生成差不多对应的直线。

那么,如果我们的输入,不再是简单的线性分布,而是一张图片呢?看起来其实也就是把真实样本数据的输入维数64×15变成了64×15×3(rgb),但此时就应用到另一个针对图片的工具,CNN,所以就诞生了DCGAN。

数据集来源:scraped from www.getchu.com, which are then cropped using the anime face detection algorithm in https://github.com/nagadomi/lbpcascade_animeface.

代码中的小知识点

feature map是什么

在代码中,有ngfnz两个变量,一开始不知道其作用的时候很是头疼。

在每个卷积层,数据都是以三维形式存在的。
你可以把它看成许多个二维图片叠在一起,其中每一个称为一个feature map。
在输入层,如果是灰度图片,那就只有一个feature map;如果是彩色图片,一般就是3个feature map(红绿蓝)。
层与层之间会有若干个卷积核(kernel),上一层和每个feature map跟每个卷积核做卷积,都会产生下一层的一个feature map。

不同的Filter (不同的 weight, bias) ,卷积以后得到不同的 feature map,提取不同的特征(得到对应 specialized neuro)

举例:同一层:

Fliter1 的w1,b1 运算后提取的是 形状边缘的特征: feature map1

Fliter2 的w2,b2 运算后提取的是 颜色深浅的特征: feature map2

下一层:

Fliter3 的w3,b3 运算后提取的是 直线形状的特征: feature map3

Fliter4 的w4,b4 运算后提取的是 弧线形状的特征: feature map4

Fliter5 的w5,b5 运算后提取的是 红色深浅的特征: feature map5

Fliter6 的w6,b6 运算后提取的是 绿色深浅的特征: feature map6

作者:花花儿
链接:https://www.zhihu.com/question/31318081/answer/182488143
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

在卷积进行特征提取的时候,使用多个卷积核进行卷积再组合在一起,也会形成feature map

一些函数

1、nn.Conv2d和nn.ConvTranspose2d参数说明及区别

nn.Conv2d(in_channels,out_channels,kernel_size,stride=1,padding=0,dilation=1,groups=1,bias=True)

in_channels:输入维度
out_channels:输出维度
kernel_size:卷积核大小
stride:步长大小
padding:补0
dilation:kernel间距
groups(int, optional) : 从输入通道到输出通道的阻塞连接数

nn.Conv2d的功能是:对由多个输入平面组成的输入信号进行二维卷积。

经过卷积后的图像尺寸计算公式:N = (W − F + 2P )/S+1

w为原始图像大小、F为卷积核尺寸、P为padding大小、S是步长

2、pytorch中反卷积的函数为:

1
2
>class torch.nn.ConvTranspose2d(in_channels, out_channels, kernel_size, stride=1, padding=0, 
output_padding=0, groups=1, bias=True, dilation=1)

参数的含义如下:

  • in_channels(int) – 输入信号的通道数
  • out_channels(int) – 卷积产生的通道数
  • kerner_size(int or tuple) - 卷积核的大小
  • stride(int or tuple,optional) - 卷积步长,即要将输入扩大的倍数。
  • padding(int or tuple, optional) - 输入的每一条边补充0的层数,高宽都增加2*padding
  • output_padding(int or tuple, optional) - 输出边补充0的层数,高宽都增加padding
  • groups(int, optional) – 从输入通道到输出通道的阻塞连接数
  • bias(bool, optional) - 如果bias=True,添加偏置
  • dilation(int or tuple, optional) – 卷积核元素之间的间距

对于每一条边输入输出的尺寸的公式如下:

$output=(input-1)stride+output_padding-2padding+kernel_size$

重要:关于卷积中几个参数的数学定义

很简单的例子:

1
nn.ConvTranspose2d(opt.nz, ngf* 8, 4, 1, 0, bias=False)

其中输入的opt.nz是输入为nz维度(输入通道数为nz)的噪声:nz*1*1,那么这条语句实际上就是把一个nz个通道的1×1的像素,扩展到了有nz×8个通道,4×4的feature map

3、nn.BatchNorm2d()函数

机器学习中,进行模型训练之前,需对数据做归一化处理,使其分布一致。在深度神经网络训练过程中,通常一次训练是一个batch,而非全体数据。每个batch具有不同的分布产生了internal covarivate shift问题——在训练过程中,数据分布会发生变化,对下一层网络的学习带来困难。Batch Normalization将数据拉回到均值为0,方差为1的正态分布上(归一化),一方面使得数据分布一致,另一方面避免梯度消失、梯度爆炸。
————————————————
版权声明:本文为CSDN博主「宁静致远*」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_40522801/article/details/106185263

1
class torch.nn.BatchNorm2d(num_features, eps=1e-05, momentum=0.1, affine=True)

其中num_features是指输入的channel数。

4、解决一个小报错

参考

在使用errorg_meter.add(error_g.data[0])时会报错,经检查,是因为pytorch版本更新的原因,改成errorg_meter.add(error_g.item())就好了。:kissing_heart:

5、LeakyRelu()函数

LeakrReLU和ReLU的区别就在于小于0的部分,仍有微小的梯度(下图右者)

img

参考,讲的很详细

Fire是什么

Google的python fire文档

Fire,是为了更好在命令行输入参数来控制程序的,具体内容参考另一篇博客:

Adam优化器是什么

简单的来说,就是“动量+自适应学习率”——以下参考李宏毅的课

动量momentum

如下图所示,我们的梯度下降过程,就好像是一个小球滚落山谷最后达到最低点的过程,但是呢,就很容易被暂时的花花风景迷了眼(掉入局部最优中)。

想一想现实中是怎么做的,没错,小球的运动不仅收到当前梯度的影响,还存在着惯性,而动量momentum的思想就是赋予小球“冲过”局部最优的能力。

img img

按照上图所示,在某一点的最终更新梯度是梯度矢量和动量矢量的和,动量矢量描述为当前的“速度”,而其中的动量:
$$
\begin{gather}
m^0=0\
m^1=-\eta g^0\
m^2=-\lambda\eta g^0-\eta g^1
\end{gather}
$$
从形式上看,动量的大小其实也是过去每一步的梯度的加权和,所以最终的效果就是梯度下降的方向不仅由$\partial L/\partial W$决定啦

自适应学习率

自适应学习率这一块,也有个很简单的原理:我们希望在平坦的路上步子迈大点,在陡峭的山坡上步子小一点

img

可以看到,有了$\sigma_i^t$的存在可以进行调整,第一步的时候,分子分母抵消,按照$\eta$进行下降,然后在第二步,可以看到,是对前面的梯度的模进行了一个平方平均。因此,在整体梯度比较小的时候,分子就小了,那么学习率就上去了,步长增大;反之步长减小。下图很形象;

img

代码详解

遇到两个大问题:

  • import numpy出错——应该是你的问题

  • visdom出错

读取图片之torchvision

官方文档

1.transforms.Scale(size):改变图片的大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
crop = transforms.Scale(12)
img = Image.open('1.png')

print(type(img))
print(img.size)

croped_img = crop(img)
print(type(croped_img))
print(croped_img.size)

<class 'PIL.PngImagePlugin.PngImageFile'>
(64, 64)
<class 'PIL.Image.Image'>
(12, 12)

​ 可见把64*64的图片变成了12*12的。

2.transforms.CenterCrop(size):中心切割

​ 将给定的PIL.Image进行中心切割,得到给定的sizesize可以是tuple(元组)(target_height, target_width)size也可以是一个Integer,在这种情况下,切出来的图片的形状是正方形。

3.transforms.ToTensor():转化成tensor

​ 把一个取值范围是[0,255]PIL.Image或者shape(H,W,C)numpy.ndarray,转换成形状为[C,H,W],取值范围是[0,1.0]torch.FloadTensor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
data = np.random.randint(0, 255, size=75)
img = data.reshape(5, 5, 3)
print(img.shape)
img_tensor = transforms.ToTensor()(img) # 转换成tensor
print(img_tensor)

(5, 5, 3)
tensor([[[ 97, 218, 4, 139, 32],
[ 65, 231, 181, 86, 197],
[242, 10, 185, 27, 158],
[ 95, 0, 47, 88, 46],
[172, 210, 41, 220, 25]],

[[176, 225, 14, 189, 164],
[ 40, 107, 231, 110, 171],
[247, 128, 161, 128, 43],
[ 50, 51, 36, 103, 81],
[107, 25, 66, 171, 202]],

[[115, 194, 103, 128, 105],
[ 78, 75, 181, 2, 87],
[ 57, 35, 96, 35, 168],
[191, 143, 235, 86, 139],
[ 52, 60, 57, 103, 174]]], dtype=torch.int32)

4.transforms.Normalize(mean, std):将Tensor正则化

​ 给定均值:(R,G,B) 方差:(R,G,B),将会把Tensor正则化。即Normalized_image=(image-mean)/std

代码太长了,不管了,直接上吧,写到啥是啥

网络构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from torch import nn
class NetG(nn.Module):
'''
生成器定义
'''
def __init__(self, opt):
super(NetG, self).__init__()
ngf = opt.ngf # 生成器feature map数,opt是参数对象

self.main = nn.Sequential(
# 输入是一个nz维度的噪声,我们可以认为它是一个1*1*nz的feature map
nn.ConvTranspose2d(opt.nz, ngf * 8, 4, 1, 0, bias=False),
nn.BatchNorm2d(ngf * 8),
nn.ReLU(True),
# 上一步的输出形状:(ngf*8) x 4 x 4

nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 4),
nn.ReLU(True),
# 上一步的输出形状: (ngf*4) x 8 x 8

nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 2),
nn.ReLU(True),
# 上一步的输出形状: (ngf*2) x 16 x 16

nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf),
nn.ReLU(True),
# 上一步的输出形状:(ngf) x 32 x 32

# nn.ConvTranspose2d(ngf, 3, 5, 3, 1, bias=False),
nn.ConvTranspose2d(ngf, 3, 4, 2, 1, bias=False),
nn.Tanh() # 输出范围 -1~1 故而采用Tanh,如果要0`1则用Sigmod
# 输出形状:3 x 96 x 96
# 这一层的kernel需要变,符合图片64*64的大小
)
def forward(self, input):
return self.main(input)

​ 在这里,做出了一个重要变化:原来的最后一个层的kernel是(5,3,1),这样就能根据公式$H_{out}=(H_{in}-1)stride-2padding+kernel_size$使得图片从32×32变成96×96,但是我们用的图片数据是64×64,所以调整kernel为(4,2,1),最后发现修改正确!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class NetD(nn.Module):
'''
判别器定义
'''
def __init__(self, opt):
super(NetD, self).__init__()
ndf = opt.ndf
self.main = nn.Sequential(
# 输入 3 x 96 x 96
# nn.Conv2d(3, ndf, 5, 3, 1, bias=False),
nn.Conv2d(3, ndf, 4, 2, 1, bias=False),

nn.LeakyReLU(0.2, inplace=True),
# 输出 (ndf) x 32 x 32

nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 2),
nn.LeakyReLU(0.2, inplace=True),
# 输出 (ndf*2) x 16 x 16

nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 4),
nn.LeakyReLU(0.2, inplace=True),
# 输出 (ndf*4) x 8 x 8

nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf * 8),
nn.LeakyReLU(0.2, inplace=True),
# 输出 (ndf*8) x 4 x 4

nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
nn.Sigmoid() # 输出一个数(判断是真图片的概率)
)

def forward(self, input):
return self.main(input).view(-1)

​ 其实生成器和判别器的构造都比较简单的,生成器把一个nz*1*1的噪声,逐步上卷积到3*64*64,但是注意最后生成的矩阵每一个元素都是-1~1的,所以并不是直接的图像。

​ 判别器则是不断卷积,并使用LeakyReLU代替的ReLU,最后是输出一个数,即判断是真图片的概率。

配置所需参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Config(object):
data_path = 'data/' # 数据集存放路径
num_workers = 4 # 多进程加载数据所用的进程数
image_size = 64 # 图片尺寸
batch_size = 256
max_epoch = 200
lr1 = 2e-4 # 生成器的学习率
lr2 = 2e-4 # 判别器的学习率
beta1 = 0.5 # Adam优化器的beta1参数
gpu = True # 是否使用GPU
nz = 100 # 噪声维度
ngf = 64 # 生成器feature map数
ndf = 64 # 判别器feature map数

save_path = 'imgs/' # 生成图片保存路径

vis = True # 是否使用visdom可视化
env = 'GAN' # visdom的env
plot_every = 20 # 每间隔20 batch,visdom画图一次

debug_file = '/tmp/debuggan' # 存在该文件则进入debug模式
d_every = 1 # 每1个batch训练一次判别器
g_every = 5 # 每5个batch训练一次生成器
decay_every = 10 # 没10个epoch保存一次模型
netd_path = None # 'checkpoints/netd_.pth' #预训练模型
netg_path = None # 'checkpoints/netg_211.pth'

# 只测试不训练
gen_img = 'result.png'
# 从512张生成的图片中保存最好的64张
gen_num = 64
gen_search_num = 512
gen_mean = 0 # 噪声的均值
gen_std = 1 # 噪声的方差

opt = Config()#实例化

以上超参数设置就先这样吧,放在这后面再说。

开始训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def train(**kwargs):
for k_, v_ in kwargs.items():
setattr(opt, k_, v_)
'''
将对象opt的属性k_改变为v_,这是一个很实用的写法!!!!!
'''
if opt.vis: # 使用visdom进行显示
from visualize import Visualizer
vis = Visualizer(opt.env)#这个环境和咱们python的环境没关系,这里是'GAN'

transforms = tv.transforms.Compose([
tv.transforms.Scale(opt.image_size),
tv.transforms.CenterCrop(opt.image_size),
tv.transforms.ToTensor(),
tv.transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])#根据上面的介绍,将图片进行裁剪整理成Tensor

dataset = tv.datasets.ImageFolder(opt.data_path, transform=transforms)#ImageFolder只是一个通用的数据加载器,咱们的图片已经按照了规定放置,就没问题了
dataloader = t.utils.data.DataLoader(dataset,
batch_size=opt.batch_size,
shuffle=True,
num_workers=opt.num_workers,#4个子进程
drop_last=True
)

torch.utils.data的官方文档

​ 我们用已经用tv.datasets.ImageFolder加载好了数据集,而classtorch.utils.data.DataLoader是一个数据加载器。组合数据集和采样器,并在数据集上提供单进程或多进程迭代器。

  • dataset (Dataset) – 加载数据的数据集。
  • batch_size (int, optional) – 每个batch加载多少个样本(默认: 1)。
  • shuffle (bool, optional) – 设置为True时会在每个epoch重新打乱数据(默认: False).
  • num_workers (int, optional) – 用多少个子进程加载数据。0表示数据将在主进程中加载(默认: 0)
  • drop_last (bool, optional) – 如果数据集大小不能被batch size整除,则设置为True后可删除最后一个不完整的batch。如果设为False并且数据集的大小不能被batch size整除,则最后一个batch将更小。(默认: False)
1
2
3
4
5
6
7
# 定义网络
netg, netd = NetG(opt), NetD(opt)#实例化
def map_location(storage, loc): return storage
if opt.netd_path:
netd.load_state_dict(t.load(opt.netd_path, map_location=map_location))
if opt.netg_path:
netg.load_state_dict(t.load(opt.netg_path, map_location=map_location))

​ 这是在加载预训练模型,我们在构造好了一个模型后,可能要加载一些训练好的模型参数,这时就可以。官方文档位置,很值得看的一个博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 定义优化器和损失
optimizer_g = t.optim.Adam(
netg.parameters(), opt.lr1, betas=(opt.beta1, 0.999))
optimizer_d = t.optim.Adam(
netd.parameters(), opt.lr2, betas=(opt.beta1, 0.999))
criterion = t.nn.BCELoss() # 损失函数:二值交叉熵

# 真图片label为1,假图片label为0
# noises为生成网络的输入
true_labels = Variable(t.ones(opt.batch_size))
fake_labels = Variable(t.zeros(opt.batch_size))
fix_noises = Variable(t.randn(opt.batch_size, opt.nz, 1, 1))
noises = Variable(t.randn(opt.batch_size, opt.nz, 1, 1))

errord_meter = AverageValueMeter()
errorg_meter = AverageValueMeter()

顾名思义,Variable就是 变量 的意思。实质上也就是可以变化的量,区别于int变量,它是一种可以变化的变量,这正好就符合了反向传播,参数更新的属性。

具体来说,在pytorch中的Variable就是一个存放会变化值的地理位置,里面的值会不停发生片花,就像一个装鸡蛋的篮子,鸡蛋数会不断发生变化。那谁是里面的鸡蛋呢,自然就是pytorch中的tensor了。(也就是说,pytorch都是有tensor计算的,而tensor里面的参数都是Variable的形式)。如果用Variable计算的话,那返回的也是一个同类型的Variable。

https://www.jb51.net/article/177996.htm

AverageValueMeter(self)

tnt.AverageValueMeter返回的应该是统计量的均值。测量一组示例的平均损失很有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
if opt.gpu:  # 如果使用GPU,数据移到GPU上
netd.cuda()
netg.cuda()
criterion.cuda()
true_labels, fake_labels = true_labels.cuda(), fake_labels.cuda()
fix_noises, noises = fix_noises.cuda(), noises.cuda()

epochs = range(opt.max_epoch)
for epoch in iter(epochs):#生成一个迭代器
for ii, (img, _) in tqdm.tqdm(enumerate(dataloader)):
real_img = Variable(img)
if opt.gpu:
real_img = real_img.cuda()
if ii % opt.d_every == 0:# 每1个batch训练一次判别器
# 训练判别器
optimizer_d.zero_grad()
# 尽可能的把真图片判别为正确
output = netd(real_img) # 前向传播
error_d_real = criterion(output, true_labels) # 计算损失,二值交叉熵函数损失
error_d_real.backward() # 反向传播

# 尽可能把假图片判别为错误
noises.data.copy_(t.randn(opt.batch_size, opt.nz, 1, 1))
fake_img = netg(noises).detach() # 根据噪声生成假图
output = netd(fake_img)
error_d_fake = criterion(output, fake_labels)
error_d_fake.backward()
optimizer_d.step() # 更新权重
'''
!!!!!!!!!!!!!!!!!!!
注意看这里!先对网络进行了zero_grad()梯度清零,
然后netd(real_img)进行前向传播计算参数,
然后backward()反向传播计算梯度
最后两个反向传播都结束了,梯度都更新了,
optimizer_d.step()权重更新
!!!!!!!!!!!!!!!!!!!!
'''
error_d = error_d_fake + error_d_real

# errord_meter.add(error_d.data[0])
errord_meter.add(error_d.item())

if ii % opt.g_every == 0:
# 训练生成器
optimizer_g.zero_grad()
noises.data.copy_(t.randn(opt.batch_size, opt.nz, 1, 1))
fake_img = netg(noises)
output = netd(fake_img)
error_g = criterion(output, true_labels)
error_g.backward()
optimizer_g.step()
# errorg_meter.add(error_g.data[0])
errorg_meter.add(error_g.item())

可视化部分

visdom官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if opt.vis and ii % opt.plot_every == opt.plot_every-1:
# 可视化
if os.path.exists(opt.debug_file):
ipdb.set_trace()
fix_fake_imgs = netg(fix_noises)
vis.images(fix_fake_imgs.data.cpu().numpy()
[:64]*0.5+0.5, win='fixfake')
vis.images(real_img.data.cpu().numpy()
[:64]*0.5+0.5, win='real')
vis.plot('errord', errord_meter.value()[0])
vis.plot('errorg', errorg_meter.value()[0])

if epoch % opt.decay_every == 0:
# 保存模型、图片
tv.utils.save_image(fix_fake_imgs.data[:64], '%s/%s.png' % (
opt.save_path, epoch), normalize=True, range=(-1, 1))
t.save(netd.state_dict(), 'checkpoints/netd_%s.pth' % epoch)
t.save(netg.state_dict(), 'checkpoints/netg_%s.pth' % epoch)
errord_meter.reset()
errorg_meter.reset()
optimizer_g = t.optim.Adam(
netg.parameters(), opt.lr1, betas=(opt.beta1, 0.999))
optimizer_d = t.optim.Adam(
netd.parameters(), opt.lr2, betas=(opt.beta1, 0.999))

生成结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def generate(**kwargs):
'''
随机生成动漫头像,并根据netd的分数选择较好的
'''
for k_, v_ in kwargs.items():
setattr(opt, k_, v_)#从控制台的输入拿参数,可以进行默认参数的修改

netg, netd = NetG(opt).eval(), NetD(opt).eval()
noises = t.randn(opt.gen_search_num, opt.nz, 1,
1).normal_(opt.gen_mean, opt.gen_std)
noises = Variable(noises, volatile=True)

def map_location(storage, loc): return storage
netd.load_state_dict(t.load(opt.netd_path, map_location=map_location))
netg.load_state_dict(t.load(opt.netg_path, map_location=map_location))

if opt.gpu:
netd.cuda()
netg.cuda()
noises = noises.cuda()

# 生成图片,并计算图片在判别器的分数
fake_img = netg(noises)
scores = netd(fake_img).data

# 挑选最好的某几张
indexs = scores.topk(opt.gen_num)[1]
result = []
for ii in indexs:
result.append(fake_img.data[ii])
# 保存图片
tv.utils.save_image(t.stack(result), opt.gen_img,
normalize=True, range=(-1, 1))


if __name__ == '__main__':
import fire
fire.Fire()

最后结果

在这里插入图片描述在这里插入图片描述在这里插入图片描述

在这里插入图片描述在这里插入图片描述

如图,我训练了100个eopch,可以看到生成的图片逐渐有了动漫头像的感觉,相信如果训练的更多最后能够达到比较满意的结果~

2021.8.5