自动求导

I dont quite understand what Question 3 wants us to answer. When I change the original a to a random vector or matrix, nothing strange happens. Can anyone help me? What is the situation supposed to be?

  1. Because it has to invoke ‘backward()’ twice which definitely consumes more resources.
  2. RuntimeError occurs:

RuntimeError: Trying to backward through the graph a second time (or directly access saved tensors after they have already been freed). Saved intermediate values of the graph are freed when you call .backward() or autograd.grad(). Specify retain_graph=True if you need to backward through the graph a second time or if you need to access saved tensors after calling backward.

  1. RuntimeError occurs:

RuntimeError: grad can be implicitly created only for scalar outputs

def f(x):
    if torch.min(x) < 0:
        y = x * x
    else:
        y = torch.mean(x)
    return y
x = torch.randn(1, 5, requires_grad=True)
print(x)
y = f(x)
if y.dim() > 0:
    y = y.sum()
y.backward()
print(x.grad)
print(x.grad==2*x if torch.min(x)<0 else x.grad==1/x.shape[1]*torch.ones_like(x))
%run calculus.ipynb
x = torch.arange(-2*torch.pi,2*torch.pi+0.1,0.1).requires_grad_()
f = lambda x: torch.sin(x)
f(x).sum().backward()
plot(x.detach(), [f(x).detach(), x.grad], 'x', 'y', legend=["sin(x)","D(sinx)"])

image

交作业:

  1. 为什么计算二阶导数比一阶导数的开销要更大?

我认为计算二阶导数需要更多的内存和计算资源。
内存:计算二阶导数时,需要存储更多的中间结果和梯度信息。
计算资源:计算二阶导数时,需要进行更多的计算操作,这增加了计算时间和资源消耗。

  1. 在运行反向传播函数之后,立即再次运行它,看看会发生什么。

发生了错误,因为在第一次backward()之后,梯度已经被清空了,所以在第二次backward()时,梯度为None
但我们可以通过设置retain_graph=True来保留计算图,这样就可以多次调用backward()了

# 课程的例1

import torch

x = torch.arange(4.0, requires_grad=True)

y = x * x # y 是矢量

y.sum().backward(retain_graph=True) # 由于 backward() 函数只能对标量调用,所以需要用 sum() 函数将 y 转换为标量

print(x.grad)

y.sum().backward()

print(x.grad)

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

def f(a):
b = a * 2
while b.norm() < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c

a = torch.randn(size=(7,), requires_grad=True) # a 现在是向量
d = f(a)
d.sum().backward()

print(a)
print(a.grad)

发生对应的长度变化。

梯度的本质是偏导数,而 PyTorch 会根据自动微分机制对每个分量求导。因此,

a 的梯度结构总是和 a 的结构一致:

标量:梯度是标量。ex: size=()

向量:梯度是每个分量的偏导数,形成向量。ex: size=(7,)

矩阵:梯度是每个分量的偏导数,形成矩阵。ex: size=(7,12)

  1. 重新设计一个求控制流梯度的例子,运行并分析结果。

def f(a):
if a > 0:
b = a * 2
else:
b = a * 3
while b.norm() < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c

# 创建一个随机标量 a,并设置 requires_grad=True

a = torch.randn(size=(), requires_grad=True)

# 计算 d = f(a)

d = f(a)

# 进行反向传播,计算梯度

d.sum().backward()

# 打印 a 的梯度

print("a 的值:", a)

print("a 的梯度:", a.grad)

运行结果:

a 的值: tensor(1.0775, requires_grad=True)

a 的梯度: tensor(1024.)

1 a 是一个随机标量,值为 1.0775

2 因为 a > 0,所以 b = a * 2,即 b = 1.0775 * 2 = 2.155。

进入 while 循环,不断将 b 乘以 2,直到 b.norm() 大于或等于 1000。

3 因为 b.sum() > 0,所以 c = b

4 d = c = b = 1103.36

进行反向传播,计算 a 的梯度。

因为 b 是通过不断乘以 2 得到的,所以 b 相对于 a 的梯度是 2^p,其中 p 是循环的次数。

在这个例子中,循环执行了 9 次,所以 p = 9,梯度是 2^9 = 512。

但是因为初始时 b = a * 2,所以总的梯度是 2 * 2^9 = 2^10 = 1024。

  1. 使f(x)=sin⁡(x),绘制f(x)和df(x)/dx的图像,其中后者不使用f′(x)=cos⁡(x)。

from math import pi

import matplotlib.pyplot as plt

def f(x):
y = torch.sin(x)
return y

x = torch.arange(0, 2*pi, 2*pi/100, requires_grad=True)
y = f(x)

y.sum().backward()

# 将张量从计算图中分离出来,然后转换为 numpy 数组并绘制 y = sin(x) 和 dy/dx 的图形
plt.plot(x.detach().numpy(), y.detach().numpy(), label='y = sin(x)')
plt.plot(x.detach().numpy(), x.grad.detach().numpy(), label='dy/dx')

# 添加标签和图例
plt.xlabel('x')
plt.ylabel('y / dy/dx')
plt.legend()
plt.grid(True)

plt.show()

踩了两个坑,关于只能对标量使用反向传播
1、这里的x和f(x)=sinx都是一个张量,画图的时候要用detach()方法看成标量,再进行画图。

d2l.plot(x.detach(), [f(x).detach(), x.grad], 'x', 'f(x)', figsize=(10, 2.5))

2、先对sinx求和得到张量,就可以得到x的梯度了。

f(x).sum().backward()