0%

遇到的坑

网络的组合

用列表的数据结构,存储Module。

使用 Python 的 list 添加的全连接层和它们的 parameters 并没有自动注册到我们的网络中。

解决方案:如果想要继承了nn.Module的网络组合多个子网络,需要通过nn.Sequential, nn.ModuleList对网络参数进行注册

计算Loss的两个Tensor形状不一致

由于通过网络经过各种矩阵相乘,最后得到的结果是一个二维的Tensor。自己定义的real_label是一个一维的Tensor。torch.Size([256]) 不等于 torch.Size([256, 1]).

1
g_loss = self.losser.get_loss(d_fake, real_label)

解决方案:在网络D最后的输出使用.squeeze()对维度值为1的维度进行挤压。

注意

对在每个View上的数据,使用list或tuple的数据结构进行存储,不必再转到Tensor上。

使用Processor再训练(train)过程中对数据进行处理,虽然可能会影响效率,但是是最方便的实现方法,在Pytorch的DataLoader只能返回Batch_size的数据,无法再加一个维度,保存View。


Reference

https://discuss.pytorch.org/t/when-should-i-use-nn-modulelist-and-when-should-i-use-nn-sequential/5463

对Batch_size与lr的认识

大Batch(Batch -> Full):

好处:

  • 更好的代表样本总体,更准确指向极值。
  • 不同权重梯度差别大,学习率不统一。

坏处:

  • 内存局限,无法一次性载入全部样本。
  • 每个Batch,由于采样性差异,梯度修正值相互抵消

小Batch(Batch -> 1):

好处:

  • 可以在一个epoch上迭代很多次,参数更新多次。

坏处:

  • 每次修正都向各自样本的梯度方向修正,难收敛

简介

Inplace操作就是将操作后的新值赋值到原变量地址上的操作。如x += 1.

Pytorch中的操作

带有_后缀对Tensor的操作都是Inplace操作。

.squeeze()不是Inplace操作,.squeeze_()是Inplace操作。

x += 1是Inplace操作,x = x + 1不是Inplace操作。

注意

在 pytorch 中, 有两种情况不能使用 inplace operation:

  1. 对于 requires_grad=True 的 叶子张量(leaf tensor) 不能使用 inplace operation
  2. 对于在 求梯度阶段需要用到的张量 不能使用 inplace operation

Reference

https://zhuanlan.zhihu.com/p/38475183

图片Img数组的取值范围及含义

Loader返回的值形式为[batch, channel, row, col]. (N * C * H * W)

channel:通道数,通道的维度上就保存了各个通道上的值,如RGB。

torchvision.transforms中的.toTensor()就是将图片的[0, 255]范围转换为了Tensor上的[0.0, 1.0]。因此mnist数据集中每个像素点上的值在[0, 1]区间上。

G 与 D 进行两次前向传播

两次的参数均独立生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# G的前向传播
noise = torch.autograd.Variable(torch.randn(imgs_batch, self.latent_dim)).cuda()
gen_imgs = self.G(noise)
d_fake = self.D(gen_imgs)
g_loss = sef.bce_loss(d_fake, real_label)
# 训练G
...
# D的前向传播
gen_imgs = self.G(noise)
d_fake = self.D(gen_imgs)
d_real = self.D(real_imgs)
d_loss_fake = self.bce_loss(d_fake, fake_label)
d_loss_real = self.bce_loss(d_real, real_label)
d_loss = d_loss_fake + d_loss_real
# 训练D
...

注意:

每一次进行前向计算的参数都是独立的参数,在生成计算图时都是独立的。因此,如果计算后一个网络的时候还传的是一开始的gen_imgs其实没有变化,指向的还是同一个参数。但是如果再次让noise经过G生成一个gen_imgs,因为G已经更新过了,此时的gen_imgs也发生了变化,因此再进行前向传播,生成的就是另外一个计算图了。

因此这种情况根本不需要再第一次反向传播的时候设置retain_graph=True.

G 与 D 只进行一次前向传播

共用一次前向计算的参数。

1
2
3
4
5
6
7
8
9
10
11
# 参数只进行一次前向传播
noise = torch.autograd.Variable(torch.randn(imgs_batch, self.latent_dim)).cuda()
gen_imgs = self.G(noise)
d_fake = self.D(gen_imgs)
d_real = self.D(real_imgs)

g_loss = self.bce_loss(d_fake, real_label)
d_loss_fake = self.bce_loss(d_fake, fake_label)
d_loss_real = self.bce_loss(d_real, real_label)
d_loss = d_loss_fake + d_loss_real
# 同时训练

方法一(错误的)

两个网络同时训练的参数的优化器同时梯度清零->损失反向传播->梯度下降,此时的网络无法得到优化。而且此时因为两个loss的反向传播有重合的部分,必须要retain_graph=True

1
2
3
4
5
6
self.optimizer_G.zero_grad()
self.optimizer_D.zero_grad()
g_loss.backward(retain_graph=True)
d_loss.backward()
self.optimizer_G.step()
self.optimizer_D.step()

注意:

此时根本不会对网络有优化的作用,并没有产生‘对抗’的效果。

方法二

两个网络先后训练,先优化G,后优化D。但是此时loss反向传播时,仍然会有参数重复计算梯度且没有更新的部分(网络D中的参数)。必须要在先进行反向传播的loss加上retain_graph=True.

1
2
3
4
5
6
7
self.optimizer_G.zero_grad()
g_loss.backward(retain_graph=True)
self.optimizer_G.step()

self.optimizer_D.zero_grad()
d_loss.backward()
self.optimizer_D.step()

注意:

这种方法更注重的是二次求导,也就是原计算图保存的是g_loss对网络D中参数的梯度,然后叠加上d_loss对网络D中参数的梯度。一并对网络D的参数进行更新。

Conv2d

1
nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)

in_channels: 输入通道数。

out_channels: 输出通道数。

kernel_size: 卷积核大小。(如果长宽不一样,传一个tuple)

stride: 卷积核移动步长。

padding: 图片上下左右的填充。

输入的shape应该是:[batch_size, channels, height, weight]

思考

GAN这种结构最大的问题就是虽然可以生成很相似的东西,但是如果生成的东西有很强的类别属性,无法指定生成的类别

Optimizer优化器就是训练网络,虽然进入网络的样本也被装入了Variable计算图中,其实际上不会被更新,如果没有设置requires_grad=False本质上还是会被计算梯度的,因此对非网络中的参数指定不计算梯度,可以提升运行效率。

retain_graph实质上是对计算图进行保留,由于训练基本上是不断生成一个计算图然后反向传播,默认在反向传播后就抛弃掉了这个计算图结构。

只进行一次前向计算就是同时训练两个网络(训练网络D并不是在更新了网络G的基础上);进行两次前向计算就是分别训练两个网络(训练网络D是在网络G更新的基础上)。

保留计算图或不保留计算图的本质就在于,对网络D的更新是否叠加上了网络G更新时反向传播的g_loss对网络D中参数的梯度。(不是计算二阶导数!

网络的最后一层要在[0, 1]上,由于没有用卷积,在数字意外部分多还是噪声的影响大,因此造成了数字以外区域黑白。

batch大小的调整直接影响到了权重的迭代。(问题就出在batch=10000忘记调回来了!!!):很大的一个作用是可以在一个epoch上迭代很多次!!!

问题

shape操作 reshape, view, squeeze, flatten…

共享内存 如(a.view(-1, 1)/ b = a.view(-1, 1)) a内存中的tensor都会变吗,还是只有b拿到了?inplace


Reference

https://www.zhihu.com/search?q=batch%20size&utm_content=search_suggestion&type=content

https://blog.csdn.net/weixin_43202635/article/details/84204180

改变Tensor形状

改变Tensor形状时,原Tensor中的数据是如何重构的。

Tensor数据形状的变化

同一维度

每一行往前或往后拼接,直到满足这个维度要求的值。

1
2
3
4
5
6
7
x = torch.autograd.Variable(torch.Tensor([[1, 2], [3, 4], [5, 6]]), requires_grad=True)
tensor([[1., 2.],
[3., 4.],
[5., 6.]], requires_grad=True)
x = x.view(2, 3)
tensor([[1., 2., 3.],
[4., 5., 6.]], grad_fn=<ViewBackward>)

跨维度

从最低维上的数值一直到高纬进行拼接,不同维度上不够的会跨维度进行拼接。

1
2
3
4
5
6
7
8
9
10
11
12
x = torch.autograd.Variable(torch.Tensor([[[1, 2], [3, 4], [5, 6]], [[7, 8], [9, 10], [11, 12]]]), requires_grad=True)
tensor([[[ 1., 2.],
[ 3., 4.],
[ 5., 6.]],

[[ 7., 8.],
[ 9., 10.],
[11., 12.]]], requires_grad=True)
x = x.view(3, 4)
tensor([[ 1., 2., 3., 4.],
[ 5., 6., 7., 8.],
[ 9., 10., 11., 12.]], grad_fn=<ViewBackward>)

查看形状的方式

1. shape

通过Tensor.shape可以获取该Tensor的形状信息。这里的shape属于Field,它是一个数组。

使用方法:想要获取某个维度的大小只需要.shape[0]就可以了。

2. size

通过Tensor.size()可以获取该Tensor某个维度上的信息。这里的size属于Method,它是一个方法,需要传dim信息。

使用方法:.size(0)

改变形状的方式

1. reshape

  • 不受该Tensor是否是连续的影响。

  • 返回的可能是Tensor的copy,也可能不是

2. view

  • 只能改变连续的Tensor(没有调用过transpose, permute等)。
  • 与原Tensor共享存储器(不是内存)。

3. squeeze/unsqueeze

对Tensor维度值为1的维度进行删除或增加。

1
2
3
4
5
6
7
8
a = Tensor(2, 1, 3, 1)
# torch.Size([2, 1, 3, 1])
a = a.squeeze(1)
# torch.Size([2, 3, 1])
a = a.unsqueeze(0)
# torch.Size([1, 2, 3, 1])
a = a.squeeze()
# torch.Size([2, 3])

注意:

  • 不传参删除所有维度值为1的维度。
  • 不在原本的存储空间上操作,只是显示改变样子,想要获取需要赋值。

Tensor的拼接

将多个Tensor进行拼接,使用torch.cat(inputs, dim=0).

将输入在dim维度上进行拼接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
x = Tensor([[1, 2, 3], [4, 5, 6]])
# x: tensor([[1., 2., 3.],
# [4., 5., 6.]], device='cuda:7')
y = Tensor([[4, 5, 6], [7, 8, 9]])
# y: tensor([[4., 5., 6.],
# [7., 8., 9.]], device='cuda:7')
z = torch.cat([x, y], dim=0)
# tensor([[1., 2., 3.],
# [4., 5., 6.],
# [4., 5., 6.],
# [7., 8., 9.]], device='cuda:7') torch.Size([4, 3])
z = torch.cat([x, y], dim=1)
# tensor([[1., 2., 3., 4., 5., 6.],
# [4., 5., 6., 7., 8., 9.]], device='cuda:7') torch.Size([2, 6])

注意:

  • 传入的inputs是一个序列,listtuple.
  • 传入的inputs在需要拼接的dim上shape必须一致。

Tensor的合并

torch.flatten(input, start_dim, end_dim)

start_dimend_dim维度之间的值合并。

好像torch下的方法,都可以通过Tensor.flatten()来调用。

Tensor的分解

1. torch.split()

torch.split(tensor, split_size, dim)

split_size: 间隔大小。

1
2
a = Tensor(3, 2)
x, y, z = a.split(1, 0)

注意

  • 最后不足的也会被分成一个块。
  • 注意类型,返回的是一个tuple,可以用多个变量来接收,tuple里装的是Tensor。

2. torch.chunk()

torch.chunk(tensor, chunks, dim)

chunks: 分块数。

1
2
a = Tensor(3, 2)
x, y, z = a.chunk(3, 0)

注意:最后不足的也会被分成一个块。


Reference

https://www.cnblogs.com/dilthey/p/12376179.html