0502-计算图

pytorch完整教程目录:https://www.cnblogs.com/nickchen121/p/14662511.html

一、手动计算梯度

torch 的 autograd 的底层采用了计算图,它是一种特殊的有向无环图,记录算子和变量的关系,如下图所示:

其中 MUL 和 ADD 都是算子,用矩形表示;w、x、b 都是变量,用椭圆形表示。上述所示的变量 w、x、b 都是叶子节点,一般由用户创立,不依赖于其他变量。z 称为根结点,是计算图的最终目标。

其中各个叶子节点的梯度可以用链式法则求出为:

  1. (frac{partial{z}}{partial{y}}=1, frac{partial{z}}{partial{b}}=1)
  2. (frac{partial{z}}{partial{w}}=frac{partial{z}}{partial{y}}frac{partial{y}}{partial{w}}=1*x,frac{partial{z}}{partial{x}}=frac{partial{z}}{partial{y}}frac{partial{z}}{partial{x}}=1*w)

二、利用 torch 进行反向传播求梯度

对于上述的计算梯度,如果有了计算图,则可以通过 torch 的 autograd 反向传播自动完成。前面说到 autograd 会随着用户的操作,记录生产当前的 variable 的所有操作,并由此建立一个有向无环图,随着用户的每一个操作,相应的计算图就会发生改变,更底层的操作就是在图中记录了 Function,如下图的 addBackward。上述所说的有向无环图如下图所示:

由于torch 把每一个操作都写入了计算图中,也由此每一个变量在图中的位置可通过 它的grad_fn 属性在图中的位置推测得到。

进而在反向传播过程中,autograd 沿着这个图从根节点 z 溯源,然后利用链式求导法则计算所有叶子节点的梯度。每一个前向传播操作的函数都会有与之对应的反向传播函数用来计算输入的各个 variable 的梯度,这些函数的函数名通常以 Backward 结尾。

x = V(t.ones(1))
b = V(t.rand(1), requires_grad=True)
w = V(t.rand(1), requires_grad=True)
y = w * x  # 等价于 y=w.mul(x)
z = y + b  # 等价于 z=y.add(b)
# 由于 y 依赖于求导的 w,故而即使 y 没有指定requires_grad=True,也为 True;z 同理
x.requires_grad, b.requires_grad, w.requires_grad, y.requires_grad, z.requires_grad
(False, True, True, True, True)
x.is_leaf, b.is_leaf, w.is_leaf, y.is_leaf, z.is_leaf
(True, True, True, False, False)
# grad_fn 可以查看这个 variable 的反向传播函数
# 由于 z 是 add 函数的输出,所以它的反向传播函数是 AddBackward
z.grad_fn
<AddBackward0 at 0x7fdd9bbbe470>
# next_functions 保存 grad_fn 的输入,grad_fn 是一个元组
# 第一个是 y,它是乘法的输出,所以对应的反向传播函数 y.grad_fn 是 MulBackward
# 第二个是 b,它是叶子节点,由用户创建,grad_fn 为 None,但是有 z.grad_fn.next_functions
z.grad_fn.next_functions
((<MulBackward0 at 0x7fdd9bbbe908>, 0),
 (<AccumulateGrad at 0x7fdd9bbbe940>, 0))
z.grad_fn.next_functions[0][0] == y.grad_fn  # 证明上述所说
True
# 第一个是 w,叶子节点,但代码中规定需要求导,梯度是累加的
# 第二个是 x,叶子节点,单不需要求导,所以为 None
y.grad_fn.next_functions
((<AccumulateGrad at 0x7fdd9bbc50f0>, 0), (None, 0))
# 虽然 w 规定了需要求导,但是叶子节点的 grad_fn 都是 None
w.grad_fn, x.grad_fn
(None, None)

计算 w 的梯度时需要用到 x 的数值((frac{partial{y}}{partial{w}}=x)),这些数值在前向过程中会被保存为 buffer,但是在梯度计算完之后清空。为了能够多次反向传播,可以指定 retain_graph 来保留这些 buffer。

# error 的原因是版本问题,PyTorch0.3 中把许多python的操作转移到了C++中
# saved_variables 现在是一个c++的对象,无法通过python访问。
try:
    y.grad_fn.saved_variables
except Exception as e:
    print(f'error: {e}')
error: 'MulBackward0' object has no attribute 'saved_variables'
# 使用 retain_graph 保存 buffer
z.backward(retain_graph=True)
w.grad
tensor([1.])
# 多次反向传播,梯度累加,这也就是 w 中 AccumulateGrad 标识的含义
# 如果第一次 backward 没有 retain_graph=True 参数,再次反向传播则会报错
z.backward()
w.grad
tensor([2.])
# 由于反向传播没有保存 buffer,前向过程中保存的 buffer 都被清空,无法在进行正常的反向传播
z.backward()  # 会报错
y.grad_fn.saved_variables  # 会报错

三、在前向传播中利用动态图特性构建计算图

由于 torch 使用的是动态图,它的计算图在每次前向传播开始都是从头开始构建的,因此可以用使用 Python 的控制语句按照需求构建计算图。这意味着你不需要事先构建所有可能用到的图的路径,图可以在运行的时候构建。

def abs(x):
    if x.data[0] > 0: return x
    else: return -x


x = V(t.ones(1), requires_grad=True)
y = abs(x)  # 相当于 y = x
y.backward()
x.grad
tensor([1.])
x = V(-1 * t.ones(1), requires_grad=True)
y = abs(x)  # 相当于 y = -x
y.backward()
x.grad
tensor([-1.])
t.arange(-2, 4)
tensor([-2, -1,  0,  1,  2,  3])
def f(x):
    result = 1
    for i in x:
        if i.data > 0:
            result = i * result
    return result


x = V(t.arange(-2, 4, dtype=t.float), requires_grad=True)
y = f(x)  # 相当于 y = x[3]*x[4]*x[5]
y.backward()
x.grad
tensor([0., 0., 0., 6., 3., 2.])

在某些场景下,有些节点不需要反向传播,也不需计算图的生成,因此可以使用一个上下文管理器with torch.no_grad()

x = V(t.ones(1), requires_grad=True)
y = 2 * x
x.requires_grad, y.requires_grad
(True, True)
x = V(t.ones(1), requires_grad=True)
with t.no_grad():
    y = 2 * x
x.requires_grad, y.requires_grad
(True, False)

反向传播过程中非叶子节点的导数计算完之后将会被清空,可以通过以下三种方法查看这边变量的梯度:

  • 使用retain_grad保存梯度
  • 使用 autograd.grad 函数
  • 使用 hook

上述两个方法都是很强大的工具,具体的方法可以查看官网 api,这里只给出基础的用法。

x = V(t.ones(3), requires_grad=True)
w = V(t.rand(3), requires_grad=True)

y = x * w
z = y.sum()

z.backward()
y.retain_grad()
# 非叶子节点grad 计算完后清空,y 是 None
x.grad, w.grad, y.grad
(tensor([0.6035, 0.5587, 0.7389]), tensor([1., 1., 1.]), None)
# 第一种方法,保存梯度
x = V(t.ones(3), requires_grad=True)
w = V(t.rand(3), requires_grad=True)

y = x * w
z = y.sum()

y.retain_grad()
z.backward()
# 非叶子节点grad 计算完后清空,y 是 None
y.grad
tensor([1., 1., 1.])
# 第二种方法,使用 grad 获取中间变量的梯度
x = V(t.ones(3), requires_grad=True)
w = V(t.rand(3), requires_grad=True)

y = x * w
z = y.sum()

# z 对 y 的梯度,隐式调用 backward()
t.autograd.grad(z, y)
(tensor([1., 1., 1.]),)
# 第三种方法,使用 hook
# hook 是一个函数,输入是梯度,不应该有返回值
def variable_hook(grad):
    print(f'y 的梯度:{grad}')


x = V(t.ones(3), requires_grad=True)
w = V(t.rand(3), requires_grad=True)

y = x * w
z = y.sum()

# 注册 hook
hook_handle = y.register_hook(variable_hook)
z = y.sum()
z.backward()

# 除非你每次都要使用 hook,否则使用后应该移除 hook
hook_handle.remove()
y 的梯度:tensor([1., 1., 1.])

四、variable 的 grad 属性和 backward函数 的 grad_variables 参数的区别

  • variable X 的梯度是目标函数 f(x) 对 X 的梯度,(frac{partial{f(X)}}{partial{X}}=(frac{partial{f(X)}}{partial{x_0}},frac{partial{f(X)}}{partial{x_1}},cdots,frac{partial{f(X)}}{partial{x_n}}))
  • y.backward(grad_variables)中的 grad_variables 相当于链式求导法则中的 (frac{partial{z}}{partial{x}}=frac{partial{z}}{partial{y}}frac{partial{y}}{partial{x}}) 中的 (frac{partial{z}}{partial{y}})。z 是目标函数,一般是一个标量,因此 (frac{partial{z}}{partial{y}}) 的形状和 y 的形状一致。z.backward()等价于 y.backward(grad_y)。z.backward()省略了 grad_variables 参数,因为 z 是一个标量,并且 (frac{partial{z}}{partial{z}}=1)
x = V(t.arange(0, 3, dtype=t.float32), requires_grad=True)
y = x**2 + x * 2  # dy/dz = 2 * x + 2
z = y.sum()
z.backward()  # 从 z 开始反向传播
x.grad
tensor([2., 4., 6.])
x = V(t.arange(0, 3, dtype=t.float32), requires_grad=True)
y = x**2 + x * 2  # dy/dz = 2 * x + 2
z = y.sum()
y_grad_variables = V(t.Tensor([1, 1, 1]))  # dz/dy
y.backward(y_grad_variables)
x.grad
tensor([2., 4., 6.])

五、计算图特点小结

在反向传播时,需要注意的是,只有对 variable 操作才能使用 autograd,如果对 variable 的 data 操作,无法使用反向传播,并且除了参数初始化,一般不会修改 varaible.data 值。

讲了这么多,在 torch 中计算图的特点可总结如下:

  • autograd 根据用户对 variable 的操作构建计算图。对 variable 的操作抽象为 Function。
  • 由用户创建的结点称作叶子节点,叶子节点的 grad_fn 为 None。并且叶子节点中需要求导的 variable,具有 AccumulateGrad 标识,因为它的梯度是累加的。
  • variable 默认是不需要求导的。如果某一个节点的 requeires_grad=True,那么所有依赖它的节点都为 True。
  • 多次反向传播时,梯度是累加的。反向传播的中间缓存会被清空,为进行多次反向传播需要指定 retain_graph=True 来保存这些缓存。
  • 非叶子节点的梯度计算完之后就会被清空,可以使用 autograd.grad 和 hook 技术获取非叶子节点梯度的值,也可以通过 retain_grad 保存它的梯度值。
  • varibale 的 grad 和 data 形状一致,应该避免直接修改 variable.data,因为对 data 值的修改无法进行反向传播。
  • 反向传播函数 backward 的参数 grad_variables 可以看成是链式求导的中间结果,如果是标量,可以省略,默认为 1。
  • torch 采用动态图的设计,可以很方便的查看中间层的输出,动态地设计计算图的结构。
内容来源于网络如有侵权请私信删除