首页人工智能Pytorch【深度学习(PyTorch...

【深度学习(PyTorch篇)】19.autograd的关闭以及获取非叶子节点的梯度

本系列文章配套代码获取有以下两种途径:

  • 通过百度网盘获取:
链接:https://pan.baidu.com/s/1XuxKa9_G00NznvSK0cr5qw?pwd=mnsj 提取码:mnsj
  • 前往GitHub获取
https://github.com/returu/PyTorch





01

autograd的关闭:


Pytorch中,autograd引擎是用于自动计算梯度的系统,这对于网络模型的训练至关重要。然而,在某些情况下,你可能需要关闭autograd以节省计算资源或避免不必要的计算。
使用 torch.no_grad() 上下文管理器来临时关闭autograd。当进入这个上下文时,所有在这个上下文中创建的张量都不会被autograd跟踪,这意味着不会为它们计算梯度。例如:
>>> x = torch.ones(1)
>>> w = torch.rand(1 , requires_grad=True)
>>> y = w * x

# y依赖于w,因此其requires_grad属性会被自动设置为True
>>> x.requires_grad , w.requires_grad , y.requires_grad
(False, True, True)

# 关闭autograd
>>> with torch.no_grad():
...     x = torch.ones(1)
...     w = torch.rand(1 , requires_grad=True)
...     y = w * x
...

# y依赖于w,即使w的requires_grad=True,但是y的requires_grad仍为False
>>> x.requires_grad , w.requires_grad , y.requires_grad
(False, True, False)
这对于模型推理评估阶段非常有用,因为在这些阶段中,我们通常不需要计算梯度。例如:
import torch  

model = ...  # some pre-trained model  
data = ...   # some input data  

# Evaluate the model  
with torch.no_grad():  
    output = model(data)

02

获取非叶子节点的梯度:


反向传播过程中,梯度主要被保存在叶子节点上,而非叶子节点的梯度默认不会被保存。
之所以这样设计有以下几个好处:
  • 内存效率:深度学习模型通常包含大量的参数和中间计算结果。如果保存所有节点的梯度,将会消耗大量的内存,这在大规模模型或有限资源环境中是不可接受的。只保存叶子节点的梯度可以显著降低内存占用。
  • 计算效率:在大多数情况下,我们只对模型的参数(即叶子节点)感兴趣,因为它们是优化过程中需要更新的部分。非叶子节点的梯度通常只是为了计算叶子节点梯度而存在的中间结果,没有直接用途。
  • 灵活性:只保存叶子节点的梯度为开发者提供了更大的灵活性。例如,我们可以选择只更新部分参数,或者在需要时手动保留某些非叶子节点的梯度。
>>> x = torch.ones(1)
>>> w = torch.rand(1 , requires_grad=True)
>>> b = torch.rand(1 , requires_grad=True)

>>> y = w * x
>>> z = y**2 + b
>>> z.backward()

# 在反向传播过程中,非叶子节点的梯度不会被保存
>>> x.grad , w.grad , b.grad , y.grad
(None, tensor([1.1053]), tensor([1.]), None)
如果需要查看非叶子节点的梯度,可以使用以下两种方法:
  • autograd.grad函数

PyTorchautograd模块提供了grad函数,可以用来计算并返回非叶子节点相对于某个标量函数的梯度。

>>> x = torch.ones(1)
>>> w = torch.rand(1 , requires_grad=True)
>>> b = torch.rand(1 , requires_grad=True)
>>> y = w * x
>>> z = y**2 + b

# z对y的梯度,隐式调用backward()
>>> torch.autograd.grad(z,y)
(tensor([0.8635]),)

# z = y**2 + b ,因此z对y的梯度 = 2y = w * x *2 ,与autograd.grad()结果一致
>>> w * x *2
tensor([0.8635], grad_fn=<MulBackward0>)

  • hook方法:
另外,还可以通过注册hook的方式来实现查看非叶子节点的梯度。具体操作时在非叶子节点上注册一个hook函数,该函数会在反向传播过程中被调用,并传入该节点的梯度。这样,就可以在hook函数中获取并保存非叶子节点的梯度。这种方法的好处是可以灵活地处理梯度,但需要注意避免修改梯度的值。
# hook方法的输入是梯度,没有返回值
>>def variable_hook(grad):
...     print("y的梯度:" , grad)
...

>>> x = torch.ones(1)
>>> w = torch.rand(1 , requires_grad=True)
>>> b = torch.rand(1 , requires_grad=True)
>>> y = w * x
>>> z = y**2 + b

# 注册hook
>>> hook_handle = y.register_hook(variable_hook)
>>> z.backward()
y的梯度:tensor([0.9646])

# z = y**2 + b ,因此z对y的梯度 = 2y = w * x *2 ,与hook方法结果一致
>>> w * x *2
tensor([0.9646], grad_fn=<MulBackward0>)

# 除非每次都要使用hook,否则要在用完之后移除hook
>>> hook_handle.remove()

03

实现线性回归:


上一篇文章中利用自动求导实现线性回归。本次将在参数更新时使用torch.no_grad() 上下文管理器并且使用hook方法获取中间损失值对模型预测值y_p的梯度信息。
生成数据以及定义函数部分与之前相同,就不做过多介绍:
  • 生成数据:

# 设置随机数种子,保证运行结果一致
torch.manual_seed(2024)

def get_fake_data(num):
    """
    生成随机数据,y = 0.5 * x +30并加上一些随机噪声
    """

    x = torch.randint(low = -5, high=30, size=(num,)).to(dtype=torch.float32)
    y = 0.5 * x +30 + torch.randn(num,)
    return x,y

x , y = get_fake_data(num=11)

# 对输入数据做归一化处理
x_new = x * 0.1
  • 定义函数:
# 定义模型:
def model(x , w , b):
    return w * x + b

# 定义损失函数:
def loss_fn(y_p , y):
    squared_diffs = (y_p - y)**2
    return squared_diffs.mean()
  • 循环训练,求解参数值:
y_p = model(x,*params)后注册一个hook函数,以便获取losst_p的梯度信息。
然后与之前手动计算梯度时创建的计算导数的函数进行对比,可以看出两者结果一致,说明hook方法操作正确。
另外,参数更新放置在torch.no_grad() 上下文管理器中。
# 损失对其输入(模型预测值)的导数
def dloss_fn(y_p , y):
    dsq_diffs = 2*(y_p - y)/y_p.shape[0]
    return print("梯度:" , dsq_diffs)

# 使用hook方法:
def variable_hook(grad):
    print("y_p的梯度:" , grad)

def training_loop(n_epochs , learning_rate , params , x , y):
    for epoch in range(1 , n_epochs+1):

        #前向传播
        y_p = model(x,*params)
        print("=====")
        # 两者计算的梯度结果一致
        dloss_fn(y_p , y)
        hook_handle = y_p.register_hook(variable_hook)
        loss = loss_fn(y_p ,y)

        # 梯度清零,可以在循环调用backward()之前任何时间完成
        if params.grad is not None:
            params.grad.zero_()

        # 反向传播:自动计算梯度
        loss.backward()

        # 将更新封装在非梯度上下文中
        with torch.no_grad():
            params -= learning_rate * params.grad


        if epoch % 10 ==0:
            print(f"Epoch : {epoch} , Loss : {loss}")
            print(f"Params : {params} , Grad : {params.grad}")
            print("-----")

#         # 梯度清零设置在此处也可以
#         if params.grad is not None:
#             params.grad.zero_()

    return params
设置相关参数值并运行,可以看到结果与之前一致,说明操作成功。
training_loop(n_epochs=1000 , learning_rate=1e-2 , params=torch.tensor([1.0 , 0.0],requires_grad=True) , x=x_new , y=y)

更多内容可以前往官网查看

https://pytorch.org/


本篇文章来源于微信公众号: 码农设计师

RELATED ARTICLES

欢迎留下您的宝贵建议

Please enter your comment!
Please enter your name here

- Advertisment -

Most Popular

Recent Comments