前言:

        第2.4节的微积分只是高等数学的基础,比如求导,偏导,链式法则等。只是比高等数学多出了个梯度一词,书上写的也很清楚,连结一个多元函数对其所有变量的偏导数,以得到该函数的梯度向量。
        接下来我们开始第2.5节,自动微分。

一、为什么要有自动微分?

        求导是几乎所有深度学习优化算法的关键步骤,做高数题的时候,求导很简单。 但对于复杂的模型,手工进行更新是一件很痛苦的事情。手动求导是一件很不健康的事。
        于是,深度学习框架通过自动计算导数,即自动微分来加快求导。

二、书上的例子

import torch
x = torch.arange(4.0,requires_grad=True)
print(x.grad)#输出为:None

y = 2 * torch.dot(x, x)
print(y)#输出为:tensor(28., grad_fn=<MulBackward0>)

y.backward()
print(x.grad)#输出为:tensor([ 0.,  4.,  8., 12.])

        首先创建一个x向量 。后面的requires_grad=True,是记录每一步怎么算的,以便后面求导数。print(x.grad)这个时候还没有进行任何激素按,所以输出为None。
        y = 2 * torch.dot(x, x),是对x向量做了个点积,y是关于x的函数。grad_fn 就像是这个计算的"记忆",Mul:表示这个张量是通过乘法(Multiplication) 运算得到的Backward:表示这个函数用于反向传播(计算梯度); 0:只是一个版本标识
        y.backward()就是本节的重点,自动计算所有相关变量的梯度,(x1, x2, x3, x4)四个变量的梯度。
        后面可以看到x.grad发生了变化,我们知道y = x * x 进行求导的结果是2x, 那么对y = 2 * torch.dot(x, x)进行求导就是 4 *(x1, x2, x3, x4),和代码结果相同。

三、非标量变量的反向传播

# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
# 本例只想求偏导数的和,所以传递一个1的梯度是合适的
import torch

x = torch.arange(3.0)
x += 1
x.requires_grad_(True)  
print(x)#  tensor([1., 2., 3.], requires_grad=True)

y = x * x
print(y)#  tensor([1., 4., 9.], grad_fn=<MulBackward0>)

y.sum().backward()# 等价于y.backward(torch.ones(len(x)))
print(x.grad)#  tensor([2., 4., 6.])

        对于y.sum().backward()这行代码,有如下数学解释:

        y = [y₁, y₂, y₃] = [x₁², x₂², x₃²]
        y.sum() = y₁ + y₂ + y₃ = x₁² + x₂² + x₃²

        对 y.sum() 求导:
        d(y.sum())/dx₁ = 2x₁
        d(y.sum())/dx₂ = 2x₂  
        d(y.sum())/dx₃ = 2x₃
        所以梯度是 [2x₁, 2x₂, 2x₃] = [2, 4, 6]

        与之等价的代码:x=torch.arange(4.0,requires_grad=True)

        在 PyTorch 中,backward() 函数用于计算梯度,但当你有一个向量(非标量) 需要反向传播时,需要指定一个 gradient 参数。
        那么为什么需要这个参数?

        对于向量函数 y = f(x),其中:

        x = [x₁, x₂, x₃]

        y = [y₁, y₂, y₃] = [x₁², x₂², x₃²]

        当我们计算 dy/dx 时,实际上得到的是雅可比矩阵:

        ∂y₁/∂x₁  ∂y₁/∂x₂  ∂y₁/∂x₃
        ∂y₂/∂x₁  ∂y₂/∂x₂  ∂y₂/∂x₃  
        ∂y₃/∂x₁  ∂y₃/∂x₂  ∂y₃/∂x₃

        但 PyTorch 的 backward() 期望得到一个标量的梯度。torch.ones(len(x)) 的作用就是提供一个权重向量,将向量函数转换为标量函数。

  torch.ones(len(x)) 是一个常用的 PyTorch 函数,用于创建一个全为1的张量。
        y.backward(torch.ones(len(x))) 等价于:
        1. 先计算加权和:loss == sum(y * torch.ones(len(x))) == sum(y)
        2. 再对 loss 求导:d(loss)/dx
        注意,这里的loss是标量,可以用backward()方法。

四、分离计算

        应用场景,需要断开梯度连接,或者需要共享数据但不共享梯度时。

#下面的反向传播函数计算z=u*x关于x的偏导数,同时将u作为常数处理,而不是z=x*x*x关于x的偏导数。
import torch

x = torch.arange(3.0)
x += 1
x.requires_grad_(True)  

y = x * x

u = y.detach() #创建一个新的张量,这个张量与原始张量共享数据,但不再参与梯度计算。
z = u * x

z.sum().backward()
print(x.grad == u) #tensor([True, True, True, True])

        这段代码把y单独分离出来,创建成为一个新的张量u,u不参与梯度运算。并且由于记录了y的计算结果,我们可以随后在y上调用反向传播, 得到y=x*x关于的x的导数,即2*x。

五、Python控制流的梯度计算

        这是使用自动微分的好处,在条件、循环或函数调用的时候,任然可以得到梯度。
        以下代码是一个动态计算图的示例,它展示了 PyTorch 如何自动跟踪复杂的控制流操作。

import torch

def f(a):# a 是一个 PyTorch 张量,通常 requires_grad=True
    b = a * 2
    while b.norm() < 1000:# 只要 b 的范数小于 1000
        b = b * 2
    if b.sum() > 0:
        c = b
    else:
        c = 100 * b
    return c

a = torch.randn(size=(), requires_grad=True)
d = f(a)
d.backward()

print(a.grad == d / a)# tensor(True)

        观察上面函数f(a)代码,最后的输出结果c,一定是a的整数倍,所以可以这样写f(a) == k*a。
        下面把f(a)传给了d,所以d = k*a. k的值完全取决于a。
        所以可以用k的值验证梯度是否正确,k = d / a,a.grad == d / a

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐