Bootstrap

pytorch网络冻结的三种方法区别:detach、requires_grad、with_no_grad

pytorch网络冻结的三种方法区别:detach、requires_grad、with_no_grad

1、requires_grad

requires_grad=True # 要求计算梯度;
requires_grad=False # 不要求计算梯度;

在pytorch中,tensor有一个 requires_grad参数,如果设置为True,那么它会追踪对于该张量的所有操作。在完成计算时可以通过调用backward()自动计算所有的梯度,并且,该张量的所有梯度会自动累加到张量的.grad属性;反之,如果设置为False,则不会记录这些操作过程,自然而然就不会进行计算梯度的工作 。 tensorrequires_grad的属性默认为False.

x = torch.tensor([1.0, 2.0])
x.requires_grad

"""
结果:
False
"""

我们可以先看一下requires_grad参数设置分别为True和False时的情况。

# 设置好requires_grad的值为True
import torch

x = torch.tensor([1.0, 2.0], requires_grad=True)
y = torch.tensor([3.0, 4.0], requires_grad=False)
y1 = 2.0 * x + 2.0 * y

print(x, x.requires_grad)
print(y, y.requires_grad)
print(y1, y1.requires_grad)

y1.backward(torch.tensor([1.0, 1.0]))  
print(x.grad)
print(y.grad)

"""
结果:
tensor([1., 2.], requires_grad=True) True
tensor([3., 4.]) False
tensor([ 8., 12.], grad_fn=<AddBackward0>) True
tensor([2., 2.])
None
"""

在上面的实验中,发现在计算中如果存在tensor张量xrequires_gradTrue的情况,那么计算之后的结果y1requires_grad也为True,且计算梯度时仅会计算x的梯度,因为前面设置了y张量的requires_gradFalse,所以最后y张量的grad属性值为None

关于张量tensor的梯度计算可以参考另一篇博客:Tensor及其梯度

所以在深度学习训练时,要冻结部分权值参数不进行参数更新的话,可以在优化器初始化之前将参数组进行筛选,将不想进行训练的参数的requires_grad设置为False代码示例参考如下

cnn = CNN() #构建网络
 
for n,p in cnn.named_parameters():
    print(n,p.requires_grad)
    if n=="conv1.0.weight":
        p.requires_grad = False
 
optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad,cnn.parameters()), lr=learning_rate)

也可以把requires_grad属性置为 False这个操作放在optimizer之后,参数都不会进行更新。但是区别在于,先进行requires_grad属性置为False的操作,再optimizer初始化,不会将该层的参数放进优化器中更新,而先进行optimizer初始化,再进行requires_grad属性置为False的操作,会将所有的参数放进优化器中,但不更新该指定层参数,只更新剩下的参数。对比看来,optimizer中的参数量会相比前者会更大一点。

所以一般最好是将requires_grad属性置为 False这个操作放在optimizer之前。

注意事项:

1、requires_grad属性置为 False或者默认时,不能在对该tensor计算梯度,否则会进行报错。因为并没有追踪到任何计算历史,所以就不存在梯度的计算了。

import torch

x = torch.tensor([1.0, 2.0], requires_grad=True)
y = torch.tensor([3.0, 4.0], requires_grad=False)
y1 = 2.0 * x + 2.0 * y
# y1.backward(torch.tensor([1.0, 1.0])) 
# print(x.grad)
y.backward(torch.tensor([1.0, 1.0]))

"""
结果:
RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn
"""

2、整数型的tensor并没有requires_grad这个属性,只有浮点类型的tensor可以计算梯度

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
d:\test.ipynb Cell 9 in <cell line: 2>()
      1 a = torch.tensor([1,2])
----> 2 b = torch.tensor([3,4], requires_grad=True)
      3 c = a+b
      4 print(a.requires_grad)

RuntimeError: Only Tensors of floating point and complex dtype can require gradients

2、detach()

detach方法就是返回了一个新的张量,该张量与当前计算图完全分离,且该张量的计算将不会记录到梯度当中。

import torch

x = torch.tensor([1.0, 2.0], requires_grad=True)
y = torch.tensor([3.0, 4.0], requires_grad=True)
z = torch.tensor([3.0, 2.0], requires_grad=True)
x = x * 2
z1 = z.detach()
x1 = x.detach() 
y1 = 2.0 * x1 + 2.0 * y + 3 * z1
y1.backward(torch.tensor([1.0, 1.0])) 
print(x.requires_grad)
print(x.grad)
print(y.requires_grad)
print(y.grad)
print(z.requires_grad)
print(z.grad)
print(z1.requires_grad)
print(z1.grad)

"""
结果:
True
None
True
tensor([2., 2.])
True
None
False
None
C:\Users\26973\AppData\Local\Temp\ipykernel_37516\3652236761.py:12: UserWarning: The .grad attribute of a Tensor that is not a leaf Tensor is being accessed. Its .grad attribute won't be populated during autograd.backward(). If you indeed want the gradient for a non-leaf Tensor, use .retain_grad() on the non-leaf Tensor. If you access the non-leaf Tensor by mistake, make sure you access the leaf Tensor instead. See github.com/pytorch/pytorch/pull/30531 for more informations.
  print(x.grad)
"""

从上面实验可以看到,使用detach()方法后,可以截断反向传播的梯度流,其作用有点类似于将requires_grad属性置为False的情况。

requires_grad_()requires_grad属性置为False不同的是 detach()函数会返回一个新的Tensor对象b , 并且新Tensor是与当前的计算图分离的,其requires_grad属性为False,反向传播时不会计算其梯度。 ba共享数据的存储空间,二者指向同一块内存。

requires_grad_()函数会改变Tensorrequires_grad属性并返回Tensor修改requires_grad的操作是原位操作(in place)。其默认参数为requires_grad=Truerequires_grad=True时,自动求导会记录对Tensor的操作,requires_grad_()的主要用途是告诉自动求导开始记录对Tensor的操作。

关于detach()返回的张量与原张量共享数据的存储空间,二者指向同一块内存可以由以下代码看出:

z1[0] = 5.0
print(z)
print(z1)

"""
结果:
tensor([5., 2.], requires_grad=True)
tensor([5., 2.])
"""

当我们修改z1中的数据时,z中的数据也随之修改。

**总结:**当我们在计算到某一步时,不需要在记录某一个张量的梯度时,就可以使用detach()将其从追踪记录当中分离出来,这样一来该张量对应计算产生的梯度就不会被考虑了。比较常见的就是在GAN生成模型中,当训练一次生成器后,再训练判别器时,需要对生成器生成的fake进行损失计算,但是又不希望这部分损失对生成器进行权值的更新,这个时候需要冻结生成器那部分的权值,因此通常将生成器生成的fake张量使用fetch()进行阶段,再输入到判别器进行运算,这样最后使用loss.backward()时仅会对判别器部分的梯度进行计算

import torch
import numpy as np

y = torch.tensor([3.0, 4.0], requires_grad=True)
z = torch.tensor([3.0, 2.0], requires_grad=True)
z1 = z.detach()
z2 = z1 + y
y1 = torch.sum(3 * z2)
y1.backward() 
print(z2, z2.requires_grad)
print(y.grad)
print(z2.grad)
print(z1.grad)

"""
结果:
tensor([6., 6.], grad_fn=<AddBackward0>) True
tensor([3., 3.])
None
None
"""

这里假设z是生成器生成的图片,z1表示的是使用detch()截断后的张量,y表示的判别器内部的一些运算张量,z2表示经过判别器后的结果,y1假设是计算loss的损失函数,我们可以看到,使用y1.backward() 后,不会对生成器生成的z产生任何的梯度,在优化器优化时自然而然不会对其进行优化。而对于后面的步骤仍然会跟踪记录其所有的计算过程,比如对于z1在判别器中进行运算仍然会记录其过程,并仅会对判别器内部的参数y进行梯度计算,从而进行优化

(注:这里z2.grad之所以也为None,是因为z2节点不是叶子节点,它是由z1y进行累加而来的,所以在z2处不会有grad属性,这部分可以看其给出的警告)

UserWarning: The .grad attribute of a Tensor that is not a leaf Tensor is being accessed. Its .grad attribute won't be populated during autograd.backward(). If you indeed want the gradient for a non-leaf Tensor, use .retain_grad() on the non-leaf Tensor. If you access the non-leaf Tensor by mistake, make sure you access the leaf Tensor instead. See github.com/pytorch/pytorch/pull/30531 for more informations.

3、with_no_grad

torch.no_grad()是一个上下文管理器,用来禁止梯度的计算,通常用来网络推断中,它可以减少计算内存的使用量。

# 设置好requires_grad的值为True
import torch

x = torch.tensor([1.0, 2.0], requires_grad=True)
y1 = x ** 2

with torch.no_grad():  # 这里使用了no_grad()包裹不需要被追踪的计算过程
    y2 = y1 * 2
    y3 = x ** 5
    
y4 = y1 + y2 + y3

print(y1, y1.requires_grad)
print(y2, y2.requires_grad)
print(y3, y3.requires_grad)
print(y4, y4.requires_grad)

y4.backward(torch.ones(y4.shape))  # y1.backward() y2.backward()
print(x.grad)


"""
结果:
tensor([1., 4.], grad_fn=<PowBackward0>) True
tensor([2., 8.]) False
tensor([ 1., 32.]) False
tensor([ 4., 44.], grad_fn=<AddBackward0>) True
tensor([2., 4.])
"""

可以看出,其实使用with torch.no_grad()这个后,被其包裹的所有运算都是不计算梯度的,其效果与detach()类似,所以使用下列代码的运行结果是一样的:

# 设置好requires_grad的值为True
import torch

x = torch.tensor([1.0, 2.0], requires_grad=True)
y1 = x ** 2

# with torch.no_grad():  # 这里使用了no_grad()包裹不需要被追踪的计算过程
y2 = y1 * 2
y3 = x ** 5
y2 = y2.detach()
y3 = y3.detach()
 
y4 = y1 + y2 + y3

print(y1, y1.requires_grad)
print(y2, y2.requires_grad)
print(y3, y3.requires_grad)
print(y4, y4.requires_grad)

y4.backward(torch.ones(y4.shape))  # y1.backward() y2.backward()
print(x.grad)
"""
结果:
tensor([1., 4.], grad_fn=<PowBackward0>) True
tensor([2., 8.]) False
tensor([ 1., 32.]) False
tensor([ 4., 44.], grad_fn=<AddBackward0>) True
tensor([2., 4.])
"""

detach()是考虑将单个张量从追踪记录当中脱离出来;
torch.no_grad()是一个warper,可以将多个计算步骤的张量计算脱离出去,本质上没啥区别。

4、总结:

  1. requires_grad:在最开始创建Tensor时候可以设置的属性,用于表明是否追踪当前Tensor的计算操作。后面也可以通过requires_grad_()方法设置该参数,但是只有叶子节点才可以设置该参数。
  2. detach()方法:则是用于将某一个Tensor从计算图中分离出来。返回的是一个内存共享的Tensor,一变都变。
  3. torch.no_grad():对所有包裹的计算操作进行分离。但是torch.no_grad()将会使用更少的内存,因为从包裹的开始,就表明不需要计算梯度了,因此就不需要保存中间结果

参考博客:

https://blog.csdn.net/qq_37344125/article/details/107426741(写的较好,建议大家也可以看下这篇博客)
https://blog.csdn.net/m0_56413813/article/details/121542920
https://blog.csdn.net/qq_39208832/article/details/117415229
https://blog.csdn.net/weixin_43145941/article/details/114757673
https://blog.csdn.net/qq_33188180/article/details/112369276
https://blog.csdn.net/sinat_33486980/article/details/117952661

;