1、pytorch如何微调fine tuning?
在加载了预训练模型参数之后,需要finetuning 模型,可以使用不同方式finetune。
- model = torchvision.models.resnet18(pretrained=True)
- for param in model.parameters():
- param.requires_grad = False
- # 替换最后的全连接层, 改为训练100类
- # 新构造的模块的参数默认requires_grad为True
- model.fc = nn.Linear(512, 100)
-
- # 只优化最后的分类层
- optimizer = optim.SGD(model.fc.parameters(), lr=1e-2, momentum=0.9)
- ignored_params = list(map(id, model.fc.parameters()))
- base_params = filter(lambda p: id(p) not in ignored_params,
- model.parameters())
-
- optimizer = torch.optim.SGD([
- {'params': base_params},
- {'params': model.fc.parameters(), 'lr': 1e-3}
- ], lr=1e-2, momentum=0.9)
2、Pytorch如何使用多gpu?
3、Pytorch如何实现大部分layer?
pytorch可以实现大部分layer,这些层都继承于nn.Module。如nn.Conv2卷积层;AvgPool, Maxpool, AdaptiveAvgPool平均池化,最大池化; TransposeConv逆卷积; nn.Linear全连接层; nn.BatchNorm1d(1d,2d,3d)归一化层; nn.dropout; nn.ReLU; nn.Sequential。
- net1 = nn.Sequential()
- net1.add_module("conv1", nn.Conv2d(3,3,3))## add_module
- net1.add_module("BatchNormalization", nn.BatchNorm2d(3))
- net1.add_module('activation_layer', nn.ReLU())
-
- net2 = nn.Sequential(nn.Conv2d(3,3,3),
- nn.BatchNorm2d(3),
- nn.Relu())
-
- from collections import OrderedDict
- net3 = nn.Sequential(OrderedDict([("conv1", nn.Conv2d(3,3,3)),
- ("BatchNormalization", nn.BatchNorm2d(3)),
- ("activation_layer", nn.Relu())]))
-
4、nn.Module与autograd的区别。
5、inplace的理解
- a = torch.tensor([1.0, 3.0], requires_grad=True)
- b = a + 2
- print(b._version) # 0
-
- loss = (b * b).mean()
- b[0] = 1000.0
- print(b._version) # 1
-
- loss.backward()
- # RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation ...
6、nn.Functional和nn.Module
7、Pytorch数据
8、自定义层的步骤。
要实现一个自定义层大致分为以下几个主要步骤:
9、nn.ModuleList和nn.Sequential的不同
nn.Sequential 里面的顺序是你想要的,而且不需要再添加一些其他处理的函数 (比如nn.functional 里面的函数,nn 与 nn.functional 有什么区别? ),那么完全可以直接用 nn.Sequential。这么做的代价就是失去了部分灵活性,毕竟不能自己去定制 forward 函数里面的内容了。一般情况下 nn.Sequential 的用法是来组成卷积块 (block),然后像拼积木一样把不同的 block 拼成整个网络,让代码更简洁,更加结构化。
10、apply-参数初始化
Pytorch中对卷积层和批归一化层权重进行初始化,也就是weight和bias,主要会用到torch的apply()函数。
- def weight_init(m):
- classname = m.__class__.__name__ # 得到网络层的名字,如ConvTranspose2d
- if classname.find('Conv') != -1: # 使用了find函数,如果不存在返回值为-1,所以让其不等于-1
- m.weight.data.normal_(0.0, 0.02)
- elif classname.find('BatchNorm') != -1:
- m.weight.data.normal_(1.0, 0.02)
- m.bias.data.fill_(0)
-
- model = net()
- model.apply(weight_init)
11、Sequential三种实现方法
- import torch.nn as nn
- model = nn.Sequential(
- nn.Conv2d(1,20,5),
- nn.ReLU(),
- nn.Conv2d(20,64,5),
- nn.ReLU()
- )
-
- print(model)
- print(model[2]) # 通过索引获取第几个层
- '''运行结果为:
- Sequential(
- (0): Conv2d(1, 20, kernel_size=(5, 5), stride=(1, 1))
- (1): ReLU()
- (2): Conv2d(20, 64, kernel_size=(5, 5), stride=(1, 1))
- (3): ReLU()
- )
- Conv2d(20, 64, kernel_size=(5, 5), stride=(1, 1))
- '''
- import torch.nn as nn
- model = nn.Sequential(
- nn.Conv2d(1,20,5),
- nn.ReLU(),
- nn.Conv2d(20,64,5),
- nn.ReLU()
- )
-
- print(model)
- print(model[2]) # 通过索引获取第几个层
- '''运行结果为:
- Sequential(
- (0): Conv2d(1, 20, kernel_size=(5, 5), stride=(1, 1))
- (1): ReLU()
- (2): Conv2d(20, 64, kernel_size=(5, 5), stride=(1, 1))
- (3): ReLU()
- )
- Conv2d(20, 64, kernel_size=(5, 5), stride=(1, 1))
- '''
- import torch.nn as nn
- from collections import OrderedDict
- model = nn.Sequential()
- model.add_module("conv1",nn.Conv2d(1,20,5))
- model.add_module('relu1', nn.ReLU())
- model.add_module('conv2', nn.Conv2d(20,64,5))
- model.add_module('relu2', nn.ReLU())
-
- print(model)
- print(model[2]) # 通过索引获取第几个层
12、计算图
计算图通常包含两种元素,一个是tensor,另一个是Function。Function指的是计算图中某个节点(node)所进行的运算,比如加减乘除卷积等等。Function内部有forward()和backward()两个方法,分别应用于正向、反向传播。
- a = torch.tensor(2.0, requires_grad=True)
- b = a.exp()
- print(b)
- # tensor(7.3891, grad_fn=
)

- input = torch.ones([2, 2], requires_grad=False)
- w1 = torch.tensor(2.0, requires_grad=True)
- w2 = torch.tensor(3.0, requires_grad=True)
- w3 = torch.tensor(4.0, requires_grad=True)
-
- l1 = input * w1
- l2 = l1 + w2
- l3 = l1 * w3
- l4 = l2 * l3
- loss = l4.mean()
-
- print(w1.data, w1.grad, w1.grad_fn)
- # tensor(2.) None None
- print(l1.data, l1.grad, l1.grad_fn)
- # tensor([[2., 2.],
- # [2., 2.]]) None
- print(loss.data, loss.grad, loss.grad_fn)
- # tensor(40.) None
需要注意的是,我们给定的 w 们都是一个常数,利用了广播的机制实现和常数和矩阵的加法乘法,比如 w2 + l1,实际上我们的程序会自动把 w2 扩展成 [[3.0, 3.0], [3.0, 3.0]],和 l1 的形状一样之后,再进行加法计算,计算的导数结果实际上为 [[2.0, 2.0], [2.0, 2.0]],为了对应常数输入,所以最后 w2 的梯度返回为矩阵之和 8 。另外还有一个问题,虽然 w 开头的那些和我们的计算结果相符,但是为什么 l1,l2,l3,甚至其他的部分的求导结果都为空呢?
- a = torch.ones([2, 2], requires_grad=True)
- print(a.is_leaf)
- # True
-
- b = a + 2
- print(b.is_leaf)
- # False
- # 因为 b 不是用户创建的,是通过计算生成的
- # 和前边一样
- # ...
- loss = l4.mean()
-
- l1.retain_grad()
- l4.retain_grad()
- loss.retain_grad()
-
- loss.backward()
-
- print(loss.grad)
- # tensor(1.)
- print(l4.grad)
- # tensor([[0.2500, 0.2500],
- # [0.2500, 0.2500]])
- print(l1.grad)
- # tensor([[7., 7.],
- # [7., 7.]])
13、requires_grad
当我们创建一个张量 (tensor) 的时候,如果没有特殊指定的话,那么这个张量是默认是不需要求导的。我们可以通过 tensor.requires_grad 来检查一个张量是否需要求导。在张量间的计算过程中,如果在所有输入中,有一个输入需要求导,那么输出一定会需要求导;相反,只有当所有输入都不需要求导的时候,输出才会不需要 。
- input = torch.randn(8, 3, 50, 100)
- print(input.requires_grad)
- # False
-
- net = nn.Sequential(nn.Conv2d(3, 16, 3, 1),
- nn.Conv2d(16, 32, 3, 1))
- for param in net.named_parameters():
- print(param[0], param[1].requires_grad)
- # 0.weight True
- # 0.bias True
- # 1.weight True
- # 1.bias True
-
- output = net(input)
- print(output.requires_grad)
- # True
14、把网络参数的 requires_grad 设置为 False 会怎么样?
同样的网络在训练的过程中冻结部分网络,让这些层的参数不再更新,这在迁移学习中很有用处。
- input = torch.randn(8, 3, 50, 100)
- print(input.requires_grad)
- # False
-
- net = nn.Sequential(nn.Conv2d(3, 16, 3, 1),
- nn.Conv2d(16, 32, 3, 1))
- for param in net.named_parameters():
- param[1].requires_grad = False
- print(param[0], param[1].requires_grad)
- # 0.weight False
- # 0.bias False
- # 1.weight False
- # 1.bias False
-
- output = net(input)
- print(output.requires_grad)
- # False
只更新FC层:
- model = torchvision.models.resnet18(pretrained=True)
- for param in model.parameters():
- param.requires_grad = False
-
- # 用一个新的 fc 层来取代之前的全连接层
- # 因为新构建的 fc 层的参数默认 requires_grad=True
- model.fc = nn.Linear(512, 100)
-
- # 只更新 fc 层的参数
- optimizer = optim.SGD(model.fc.parameters(), lr=1e-2, momentum=0.9)
-
- # 通过这样,我们就冻结了 resnet 前边的所有层,
- # 在训练过程中只更新最后的 fc 层中的参数。
15、反向传播的流程
loss.backward();optimizer.step() 权重更新;optimizer.zero_grad() 导数清零–必须的
- class Simple(nn.Module):
- def __init__(self):
- super().__init__()
- self.conv1 = nn.Conv2d(3, 16, 3, 1, padding=1, bias=False)
- self.conv2 = nn.Conv2d(16, 32, 3, 1, padding=1, bias=False)
- self.linear = nn.Linear(32*10*10, 20, bias=False)
-
- def forward(self, x):
- x = self.conv1(x)
- x = self.conv2(x)
- x = self.linear(x.view(x.size(0), -1))
- return x
- # 创建一个很简单的网络:两个卷积层,一个全连接层
- model = Simple()
- # 为了方便观察数据变化,把所有网络参数都初始化为 0.1
- for m in model.parameters():
- m.data.fill_(0.1)
-
- criterion = nn.CrossEntropyLoss()
- optimizer = torch.optim.SGD(model.parameters(), lr=1.0)
-
- model.train()
- # 模拟输入8个 sample,每个的大小是 10x10,
- # 值都初始化为1,让每次输出结果都固定,方便观察
- images = torch.ones(8, 3, 10, 10)
- targets = torch.ones(8, dtype=torch.long)
- output = model(images)
- print(output.shape)
- # torch.Size([8, 20])
-
- loss = criterion(output, targets)
-
- print(model.conv1.weight.grad)
- # None
- loss.backward()###############################################################
- print(model.conv1.weight.grad[0][0][0])
- # tensor([-0.0782, -0.0842, -0.0782])
- # 通过一次反向传播,计算出网络参数的导数,
- # 因为篇幅原因,我们只观察一小部分结果
-
- print(model.conv1.weight[0][0][0])
- # tensor([0.1000, 0.1000, 0.1000], grad_fn=
) - # 我们知道网络参数的值一开始都初始化为 0.1 的
-
- optimizer.step()###########################################################
- print(model.conv1.weight[0][0][0])
- # tensor([0.1782, 0.1842, 0.1782], grad_fn=
) - # 回想刚才我们设置 learning rate 为 1,这样,
- # 更新后的结果,正好是 (原始权重 - 求导结果) !
-
- optimizer.zero_grad()############每次更新完权重之后,我们记得要把导数清零啊,
- # 不然下次会得到一个和上次计算一起累加的结果。
- print(model.conv1.weight.grad[0][0][0])
- # tensor([0., 0., 0.])
- # 每次更新完权重之后,我们记得要把导数清零啊,
- # 不然下次会得到一个和上次计算一起累加的结果。
- # 当然,zero_grad() 的位置,可以放到前边去,
- # 只要保证在计算导数前,参数的导数是清零的就好。
16、detach
多说无益,我们来看个例子吧:
这个导数计算的结果明显是错的,但没有任何提醒,之后再 Debug 会非常痛苦。所以,建议大家都用 tensor.detach()。
- a = torch.tensor([7., 0, 0], requires_grad=True)
- b = a + 2
- print(b)
- # tensor([9., 2., 2.], grad_fn=
) -
- loss = torch.mean(b * b)
-
- b_ = b.detach()
- b_.zero_()
- print(b)
- # tensor([0., 0., 0.], grad_fn=
) - # 储存空间共享,修改 b_ , b 的值也变了
-
- loss.backward()
- # RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation
这个例子中,b 是用来计算 loss 的一个变量,我们在计算完 loss 之后,进行反向传播之前,修改 b 的值。这么做会导致相关的导数的计算结果错误,因为我们在计算导数的过程中还会用到 b 的值,但是它已经变了(和正向传播过程中的值不一样了)。在这种情况下,PyTorch 选择报错来提醒我们。但是,如果我们使用 tensor.data 的时候,结果是这样的:
- a = torch.tensor([7., 0, 0], requires_grad=True)
- b = a + 2
- print(b)
- # tensor([9., 2., 2.], grad_fn=
) -
- loss = torch.mean(b * b)
-
- b_ = b.data
- b_.zero_()
- print(b)
- # tensor([0., 0., 0.], grad_fn=
) -
- loss.backward()
-
- print(a.grad)
- # tensor([0., 0., 0.])
-
- # 其实正确的结果应该是:
- # tensor([6.0000, 1.3333, 1.3333])
17、tensor-numpy
使用GPU还有一个点,在我们想要GPUtensor转换成Numpy变量的时候,需要先将tensor转换到CPU中去,因为Numpy是CPU-only。其次,如果tensor需要求导的话,还需要加一步detach,再转成Numpy。
18、tensor.item()
- x = torch.randn(1, requires_grad=True, device='cuda')
- print(x)
- # tensor([-0.4717], device='cuda:0', requires_grad=True)
-
- y = x.item()
- print(y, type(y))
- # -0.4717346727848053
-
- x = torch.randn([2, 2])
- y = x.tolist()
- print(y)
- # [[-1.3069953918457031, -0.2710231840610504], [-1.26217520236969, 0.5559719800949097]]
19、torch.backends.cudnn.benchmark=True
设置 torch.backends.cudnn.benchmark=True 将会让程序在开始时花费一点额外时间,为整个网络的**每个卷积层搜索最适合它的卷积实现算法,进而实现网络的加速。**适用场景是网络结构固定(不是动态变化的),网络的输入形状(包括 batch size,图片大小,输入的通道)是不变的,其实也就是一般情况下都比较适用。反之,如果卷积层的设置一直变化,将会导致程序不停地做优化,反而会耗费更多的时间。
20、静态图和动态图
- # 这是一个关于 PyTorch 是动态图的例子:
- a = torch.tensor([3.0, 1.0], requires_grad=True)
- b = a * a
- loss = b.mean()
-
- loss.backward() # 正常
- loss.backward() # RuntimeError
-
- # 第二次:从头再来一遍
- a = torch.tensor([3.0, 1.0], requires_grad=True)
- b = a * a
- loss = b.mean()
- loss.backward() # 正常