2.5.自动微分

2.5.1.一个简单的例子

追踪梯度意味着计算和记录张量在各个操作中的梯度信息,以便在反向传播时使用这些信息来更新模型参数

原地操作:指直接修改张量本身的内容,而不返回新的张量。在 PyTorch 中,原地操作通常会在方法名称后加上一个下划线 (_) 来标识,表示该操作会直接改变原始数据,而不生成新的张量。

一个标量函数关于向量 𝑥 的梯度是向量,并且与 𝑥 具有相同的形状

x.requires_grad_(True)    # 在原地修改张量,节省内存,无法恢复前状态
​
y =  torch.dot(x, x)       # 向量点积
y.backward()
x.grad              # 在执行反向传播后存储x的所有梯度

2.5.2.非标量变量的反向传播

对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self(指的就是你调用 .backward() 的那个张量本身(例如 y))的梯度

  • 为什么非标量 .backward() 需要 gradient 参数?

    • 答:输出 y 是向量(比如长度是 n),因为你有多个输出值,各自都能对输入求偏导PyTorch 必须知道你希望对这些输出怎么组合成一个标量,然后再求梯度

    • 这个“组合方式”就是你传给 .backward(gradient) 的 gradient 参数,它代表:∂L/∂y 这里的 𝐿 是你最终想优化的标量目标

    • # 等价于 y.backward(torch.ones(len(x)))
      y.sum().backward()

      传入全 1 向量(或 .sum())就是把所有输出加起来,相当于“求偏导数的和”


2.5.3.分离计算
x.grad.zero_()  # 清空梯度
y = x * x
u = y.detach()  # 保留 y 的数值结果,只是切断了梯度追踪,让 u 成为一个不带梯度信息的张量, 只是拿到了 y 的数值,被当作常量处理
z = u * x
z.sum().backward()
x.grad == u
​
x.grad.zero_()  # 清空梯度
y.sum().backward()
x.grad == 2 * x
  • 为什么计算二阶导数比一阶导数的开销要更大?

    • 二阶导数本质上是“对梯度再求导”,所以在第一次 .backward() 时,必须保留计算图(retain_graph=True),否则二次求导时找不到原来的计算依赖

  • 在运行反向传播函数之后,立即再次运行它,会发生什么?

    • 默认情况下,PyTorch 在 .backward() 完成后会释放计算图来节省内存。所以如果你不保留图,第二次运行 .backward() 会报错

    • 如果你确实需要多次 .backward(),必须在第一次调用时加:y.backward(retain_graph=True),这样 PyTorch 会保留计算图,允许后续的反向传播

  • 在控制流的例子中,我们计算d关于a的导数,如果将变量a更改为随机向量或矩阵,会发生什么?

    • 同一个函数,输入不同,梯度表达式也会不同(因为执行路径变了)

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐