1 前言
本文主要是针对陈云的PyTorch入门与实践的第八章的内容进行复现,准确地说,是看着他写的代码,自己再实现一遍,所以更多地是在讲解实现过程中遇到的问题或者看到的好的方法,而不是针对论文的原理的进行讲解。对于原理,也只是会一笔带过。原理篇暂时不准备留坑,因为原理是个玄学。
这是我的代码
大神链接:(https://github.com/anishathalye/neural-style)
2 问题及其解决
我在第六章和第七章的时候还是基于pytorch 0.4.0,而第八章的时候我开始基于pytorch 0.4.1,所以以下的内容介绍都是基于0.4.1
2.1 文件组织形式
1 | ├─checkpoints/ |
其中,上半部分是对数据和模型的保存组织形式,我们只需要能对应起来即可,其中,checkpoints是为了保存模型,content_img中的style.jpg是训练时候的风格图片,input.jpg是测试的输入,output.jpg是测试的输出,data中的数据是训练数据,主要是因为这个训练数据太整齐,是用ImageFolder读取的,为了避免麻烦,也为了在测试的时候方便观察图片,所以style.jpg我们暂时放在了content中。
下半部分是重点,我们需要写的代码,每次都是先从dataset.py和models开始写起,然后导入visualize.py,这个文件基本不会发生改变,然后同时写main.py和config.py,边写边扩展utils中的其他文件,例如main中用到的函数等等。
2.2 models
PackedVGG.py
这里我们主要是取已有的网络,得到中间层的输出
models.named_parameters():返回的是一个生成器,每次返回一个参数的关键字和值
models.state_dict():返回的是一个字典,记录了参数的关键字和值
models.parameters():返回的是变量,没有名字,可以在requires_grad中用到
models.features返回的是相对应的模型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
29In [7]: from torchvision.models import vgg16
In [8]: models = vgg16(pretrained=True)
In [9]: model = models.features[:1]
In [10]: model
Out[10]:
Sequential(
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
)
In [11]: models.parameters()
Out[11]: <generator object Module.parameters at 0x7f8fad26b3b8>
In [12]: models.named_parameters()
Out[12]: <generator object Module.named_parameters at 0x7f8f29e99d58>
In [13]: model.named_parameters()
Out[13]: <generator object Module.named_parameters at 0x7f8fad26b2b0>
In [14]: model.parameters()
Out[14]: <generator object Module.parameters at 0x7f8fad26b4c0>
In [15]: model.state_dict()
Out[15]:
OrderedDict([('0.weight', tensor([[[[-0.5537, 0.1427, 0.5290],
[-0.5831, 0.3566, 0.7657],
[-0.6902, -0.0480, 0.4841]],
1 | from torchvision.models import vgg16 |
sequencial是支持索引操作的
list(module)会变成一个list,可以通过索引来获取层,注意,nn.ModuleList, nn.Sequential, nn.Conv等都是Module,都可以通过named_parameters来获取参数。
为了能够提取出中间层的输出,作者换了一个方法,用的nn.ModuleList,nn.ModuleList和nn.Sequential的区别在此才真正显现,nn.Sequential更有利于直接把输入传给Module,计算是一个整体,写起来更方便,而nn.Modulist则不能直接把输入传给Module,需要用循环传输入,更有利于在层中做一些保留,提取中间层的输出。后面我们会讲到hook。或者说提取中间层的输出我们可以选择在定义网络的forward中进行,另外,就是需要注意的是,这里的输入是一个batch_size大小的矩阵,所以即便像作者这样,用一个列表保存输出,但实际输出的列表中的元素都是(b,n,h,w)大小的。后面我会验证。
提取中间层的输出有两种方法:
第二种方法参考链接:https://www.jianshu.com/p/0a23db1df55a1
2
3
4
5
6
7
8
9
10
11# 第一种方法,这种方法是在前向网络中提取输出,好像也是在反向传播网络中,但这种提取中间层是永久性的,也适合用这些层的做其他运算,这些运算是计算在整体网络框架中的
def forward(self, x):
results = []
for ii, model in enumerate(self.features):
x = model(x)
if ii in {3, 8, 15, 22}:
results.append(x)
vgg_outputs = namedtuple("VggOutputs", ['relu1_2', 'relu2_2', 'relu3_3', 'relu4_3'])
return vgg_outputs(*results)
1 | # 第二种方法,适合在在不影响整体网络的情况下拿出一个分支进行单独计算,现在还不清楚这样子会不会影响backward,个人感觉会,因为也是相当于一个变量对其进行计算,导数为1。 |
transformer.py
可参考链接
- padding的操作是边界反射补充
- 放大方法是双线性插值,而不是ConvTransposed2d,即unsample或者说是interpolate, 但是其中的一个参数align_corners一直没有理解,既然是双线性插值,那结果就是固定的,怎么还会因为其他参数发生变化。
其中,写的时候必要的时候可以写写子网络
这里我对residualblock提出了疑问,事实上left+right后面可以没有relu层,这一点我们可以从以下链接找到说明。
https://github.com/abhiskk/fast-neural-style/blob/master/neural_style/transformer_net.py
http://torch.ch/blog/2016/02/04/resnets.htmlThe above result seems to suggest that it’s important to avoid changing data that passes through identity connections only. We can take this philosophy one step further: should we remove the ReLU layers at the end of each residual block? ReLU layers also perturb data that flows through identity connections, but unlike batch normalization, ReLU’s idempotence means that it doesn’t matter if data passes through one ReLU or thirty ReLUs. When we remove ReLU layers at the end of each building block, we observe a small improvement in test performance compared to the paper’s suggested ReLU placement after the addition. However, the effect is fairly minor. More exploration is needed.
对于其他的出现的网络架构,其实都是有理可循的,但暂时不是本篇的重点,所以只做一个记录。上卷积简单地看了看这篇论文,unsample要比ConvTransposed2D要好,但是没有看懂。留作后续。
dataset.py & visualize.py
因为加载数据是用的tv.datasets.ImageFolder,所以dataset.py不需要写,
visualize.py是第六章的时候写好的,这里只写几个改进的
- self.vis = Visdom(env=env,use_incoming_socket=False, **kwargs),这里的use_incoming_socket是不需要从浏览器接受数据到软件中,如果没有的话会提示 ‘>’ not supported between instances of ‘float’ and ‘NoneType’
- 在一个函数前提示输入的大小和类型是一件很重要的事情,必要的时候需要输入分布,
- 这里的plot用了一个很巧的方法,用字典记录不同的点其他的细节可以看代码中的记录,应该比较清晰了。
1
2
3self.index = {}
x = self.index.get(win,0)
self.index[win] = x+1
main.py & utils.py & config.py
其中utils主要为main提供一些用到的函数,config提供参数,
main作为主函数,里面主要就是train(),val(),test(),help(),下面记录一些写main函数的一些疑问。
cuda
这里写几种怎么从cpu到gpu的方法以及应用场景。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22# 第一种
device = t.device('cuda') if opt.use_gpu else t.device('cpu')
models.to(device)
tensor = tensor.to(device)
此时使用默认的cuda,一般是cuda:0,适用于全局
# 第二种
torch.cuda.current_device() # 查询当前GPU
torch.cuda.set_device(1)
device = torch.device('cuda')
models.to(device)
此时用的是cuda:1,使用于全局
#第三种
#上下文管理器
with torch.cuda.device(1):
models.to(device)
#第四种
import os
os.environ["CUDA_VISIBLE_DEVICES"]="2"
没用过
tqdm
https://blog.csdn.net/langb2014/article/details/54798823?locationnum=8&fps=1
进度条,但是只在jupyter和终端中用的时候效果很明显,在代码中用的效果没有那么好,tqdm试了试,用在enumerate()中时,需要写成这样:1
2
3
4
5
6elements = ('a', 'b', 'c')
for count, ele in tqdm(enumerate(elements)):
print(count, i)
# two arguments
for count, ele in tqdm(enumerate(elements), total=len(train_ids), leave=False):
print(count, i)
包括zip也是一样,因为他们返回的是一个生成器,并不知道长度。
tqdm的进一步用法
1 | from tqdm import tqdm |
反向传播和梯度下降
参考链接https://blog.csdn.net/qq_16234613/article/details/80025832
这里主要是针对第七章和第八章出现的反向传播和梯度下降出现的问题进行记录。
在第七章,是这么实现分别训练的1
2
3
4
5
6
7
8
9
10
11
12
13
14fake_img = netg(noises).detach()
fake_output = netd(fake_img)
error_d_fake = criterion(fake_output, fake_labels)
error_d_fake.backward()
optimizer_d.step()
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()
y = x.detach():表示将生成一个新的叶子节点,值与当前节点的值相同,但是y.requires_grad = False, y.grad_fn=None,此时x和y共享内存,对y数据的操作也会影响x,可以理解为冻结了通过y进行反向传播的路。如果在网络的输出detach,即y= models(x).detach(),可以理解成,models只进行前向传播,grad=None。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20In [17]: a = torch.ones(3,3)
In [18]: a.requires_grad=True
In [19]: b = a*2
In [20]: b.requires_grad
Out[20]: True
In [21]: b.grad_fn
Out[21]: <MulBackward at 0x7f8fac6e40f0>
In [22]: c = b.detach()
In [23]: c.requires_grad
Out[23]: False
In [24]: print(c.grad_fn)
None
In [25]: c.is_leaf1
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
39In [2]: a = torch.ones(3,3)
In [14]: b
Out[14]:
tensor([[2., 2., 2.],
[2., 2., 2.],
[2., 2., 2.]], grad_fn=<MulBackward>)
In [15]: c = b.detach()
In [16]: c
Out[16]:
tensor([[2., 2., 2.],
[2., 2., 2.],
[2., 2., 2.]])
In [17]: c[0,0]=1
In [18]: c
Out[18]:
tensor([[1., 2., 2.],
[2., 2., 2.],
[2., 2., 2.]])
In [19]: b
Out[19]:
tensor([[1., 2., 2.],
[2., 2., 2.],
[2., 2., 2.]], grad_fn=<MulBackward>)
In [20]: c.requires_grad
Out[20]: False
In [21]: b.grad_fn
Out[21]: <MulBackward at 0x7f764429ffd0>
In [22]: b.grad_fn.next_functions
Out[22]: ((<AccumulateGrad at 0x7f7644428358>, 0),)
In [23]: a.grad_fn
在第八章,是这么表示的1
2for param in vgg16.parameters():
param.requires_grad = False
这种表示可以使得某一个网络不参与梯度下降这个过程,但是对于网络的输入和输出还是支持梯度下降的。
requires_grad只是表示当前的变量不再需要梯度下降,
综上所述,对于中间变量,需要使用x.detach(),使其变成默认的叶子节点,对于叶子节点,使用x.requires_grad。并且对于中间变量使用requires_grad会报错。
在第八章,还有一种表示方法:1
2
3
4
5
6
7with t.no_grad():
features = vgg16(style_img)
gram_style = [gram_matrix(feature) for feature in features]
def stylize(**kwargs):
pass
这种方法会使得任何计算得到的结果都是requires_grad = False,暂时不清楚和detach()的区别。也是一种表示只前向传播的方法,不参与反向传播和梯度下降。
train()
图片分为两种:风格图片,只需要一张,内容图片,很多,用于训练,这一点没有暂时没有理解为什么这么设置。其中,对输入的图片进行了乘以255,我觉得是因为为了使模型的输出直接就是255,不需要再进行处理,没有验证。
ensor.item()
tensor.tolist()
content_image = tv.datasets.folder.default_loader(opt.content_path)
在训练过程中,会发现对于整个训练过程,不仅有神经网络,而且还有自己定义的函数,nn.functional,还有两个损失函数,这是之前没有预料到的。
保存图片
1 | # 保存图片的几种方法,第七章的是 |
utils.py
这里的疑问是得到gram矩阵的时候,为什么要除以c*h*w,而不是h*w,虽然源码都是这么写的。
写到这里也还是还要很多疑问,暂时保留。
昨天发现训练的过程不对,今天在对比代码的过程中,发现了自己写代码的一些漏洞,主要有
- 命名不规范:表示同一个东西出现了两个命名,导致了自己在写代码的过程中传参出现了问题,或者是一类东西没有一个规则进行命名,导致自己在写代码的过程中用到之前的变量的时候必须返回去去查找这个变量,效率低且容易出错。
- 对源码的修改不是很恰当,导致在写上卷积层的输出和源码完全不一致,这个是自己之前没有遇到的。
- visdom的运用,我用不同的environment导致结果也不一样,default是之前一直用的,这次换成了test1之后显示的结果就对了。这个暂时还不清楚原因,如果是会保留信息的话,但是plot是重新开始画的,等会测试测试vis的问题。是网络的问题。但是vis.save()的介绍是序列化信息,暂时还没有理解。
对单张图片进行加载验证
content_image = tv.datasets.folder.default_loader(opt.content_path)
可以理解成Image.open,看源码就可以知道的
贴两个成果图看看效果。
遗留的问题
Gram矩阵为什么可以代表图片风格,这里有个解释(https://arxiv.org/pdf/1701.01036.pdf),还没来得及看。