x=torch.randn(3,requires_grad=True)
y=x+2 # will create a computational graph
print(y)
z=y*y*2
print(z)
z.backward() #dz/dx
print(x.grad)
tensor([2.0864, 1.2528, 1.2987], grad_fn=<AddBackward0>)
tensor([8.7058, 3.1389, 3.3733], grad_fn=<MulBackward0>)
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
<ipython-input-6-3ff5c6c8f8ea> in <cell line: 8>()
6 print(z)
----> 8 z.backward() #dz/dx
9 print(x.grad)
2 frames
/usr/local/lib/python3.10/dist-packages/torch/autograd/__init__.py in _make_grads(outputs, grads, is_grads_batched)
115 if out.requires_grad:
116 if out.numel() != 1:
--> 117 raise RuntimeError(
118 "grad can be implicitly created only for scalar outputs"
119 )
RuntimeError: grad can be implicitly created only for scalar outputs
创建一个v,作为链式法则中的Jacobine矩阵
x=torch.randn(3,requires_grad=True)
y=x+2 # will create a computational graph
print(y)
z=y*y*2
print(z)
v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float32)
z.backward(v) #dz/dx
print(x.grad)
当执行y.backward(v)
时,实际上进行的操作是计算向量y
相对于其输入张量x
的向量-雅可比乘积(vector-Jacobian product, VJP)。这个过程可以更清晰地理解backward
方法的工作原理以及v
的作用。假设我们有向量函数
y
=
f
(
x
)
\mathbf{y} = f(\mathbf{x})
y=f(x),其中
x
\mathbf{x}
x和
y
\mathbf{y}
y都是向量,
x
∈
R
n
\mathbf{x} \in \mathbb{R}^n
x∈Rn,
y
∈
R
m
\mathbf{y} \in \mathbb{R}^m
y∈Rm。雅可比矩阵
J
\mathbf{J}
J是
f
f
f相对于
x
\mathbf{x}
x的导数的矩阵表示,其中每个元素
J
i
j
=
∂
y
i
∂
x
j
J_{ij} = \frac{\partial y_i}{\partial x_j}
Jij=∂xj∂yi。
向量-雅可比乘积
当对y.backward(v)
进行操作,其中
v
∈
R
m
\mathbf{v} \in \mathbb{R}^m
v∈Rm,实际上计算的是
v
⊤
J
\mathbf{v}^\top \mathbf{J}
v⊤J。这个操作的结果是一个长度为
n
n
n的向量,与
x
\mathbf{x}
x的维度相同。具体地说,如果
y
\mathbf{y}
y由函数
f
(
x
)
f(\mathbf{x})
f(x)得到,那么对于给定的向量
v
\mathbf{v}
v,向量-雅可比乘积
v
⊤
J
\mathbf{v}^\top \mathbf{J}
v⊤J给出了
v
⊤
J
\mathbf{v}^\top \mathbf{J}
v⊤J相对于
x
\mathbf{x}
x的梯度。
计算过程
-
雅可比矩阵 J \mathbf{J} J:
- 尺寸为 m × n m \times n m×n,其中 m m m是 y \mathbf{y} y的维度, n n n是 x \mathbf{x} x的维度。
- 元素定义为 J i j = ∂ y i ∂ x j J_{ij} = \frac{\partial y_i}{\partial x_j} Jij=∂xj∂yi,表示 y \mathbf{y} y的每个元素相对于 x \mathbf{x} x的每个元素的偏导数。
-
向量-雅可比乘积 v ⊤ J \mathbf{v}^\top \mathbf{J} v⊤J:
- v \mathbf{v} v是一个长度为 m m m的向量,与 y \mathbf{y} y的维度相同。
- 乘积的结果是一个长度为 n n n的向量,这个向量给出了加权和 ∑ i = 1 m v i ∂ y i ∂ x j \sum_{i=1}^{m}v_i \frac{\partial y_i}{\partial x_j} ∑i=1mvi∂xj∂yi相对于 x \mathbf{x} x每个元素的导数。
实际应用
在实际应用中,比如深度学习的反向传播过程,这种计算模式允许你计算非标量输出的梯度。通过选择合适的 v \mathbf{v} v,你可以控制反向传播过程中不同输出分量对最终梯度的贡献。这在多任务学习或当损失函数由多个部分组成时尤其有用,因为它允许对每个任务或损失部分的贡献进行加权。
例如,如果
y
\mathbf{y}
y是一个具有三个元素的向量,并且你想要计算
y
\mathbf{y}
y相对于
x
\mathbf{x}
x的梯度,那么通过选择
v
=
[
0.1
,
1.0
,
0.0001
]
\mathbf{v} = [0.1, 1.0, 0.0001]
v=[0.1,1.0,0.0001]作为权重,你实际上是在指定每个元素对最终梯度的贡献比重。这样,通过y.backward(v)
,你得到的是每个
x
\mathbf{x}
x的元素对加权输出
∑
i
v
i
y
i
\sum_{i}v_i y_i
∑iviyi的梯度,这对于控制和理解多目标优化过程非常有用。
链式法则
在自动微分和反向传播中,链式法则是计算导数的核心原理。它用于计算复合函数的导数,即一个函数的输出成为另一个函数的输入的情形。链式法则使得我们能够将复合函数的导数分解为各个简单函数导数的乘积。
假设我们有两个函数 y = f ( u ) y=f(u) y=f(u)和 u = g ( x ) u=g(x) u=g(x),它们组合成复合函数 y = f ( g ( x ) ) y=f(g(x)) y=f(g(x))。我们想要计算 y y y相对于 x x x的导数 d y d x \frac{dy}{dx} dxdy。
链式法则公式
链式法则表述为:
d y d x = d y d u ⋅ d u d x \frac{dy}{dx} = \frac{dy}{du} \cdot \frac{du}{dx} dxdy=dudy⋅dxdu
这里:
- d y d u \frac{dy}{du} dudy是外函数 f ( u ) f(u) f(u)相对于其输入 u u u的导数。
- d u d x \frac{du}{dx} dxdu是内函数 g ( x ) g(x) g(x)相对于其输入 x x x的导数。
向量-雅可比乘积的链式法则
当我们将这个原理扩展到向量和多变量函数时,我们会用到雅可比矩阵来表示这些导数,因为单个函数可能依赖于多个变量。假设 y = f ( u ) \mathbf{y}=f(\mathbf{u}) y=f(u)和 u = g ( x ) \mathbf{u}=g(\mathbf{x}) u=g(x),其中 x \mathbf{x} x、 u \mathbf{u} u和 y \mathbf{y} y都是向量。如果我们要计算 y \mathbf{y} y相对于 x \mathbf{x} x的导数,我们可以用雅可比矩阵将链式法则表示为:
d y d x = d y d u ⋅ d u d x \frac{d\mathbf{y}}{d\mathbf{x}} = \frac{d\mathbf{y}}{d\mathbf{u}} \cdot \frac{d\mathbf{u}}{d\mathbf{x}} dxdy=dudy⋅dxdu
这里:
- d y d u \frac{d\mathbf{y}}{d\mathbf{u}} dudy是 y \mathbf{y} y相对于 u \mathbf{u} u的雅可比矩阵。
- d u d x \frac{d\mathbf{u}}{d\mathbf{x}} dxdu是 u \mathbf{u} u相对于 x \mathbf{x} x的雅可比矩阵。
示例中的链式法则
在y.backward(v)
的上下文中,如果
y
=
f
(
x
)
\mathbf{y}=f(\mathbf{x})
y=f(x)并且我们有一个向量
v
\mathbf{v}
v,那么通过向量-雅可比乘积,我们实际上计算的是:
grad x = v ⊤ d y d x \text{grad}_{\mathbf{x}} = \mathbf{v}^\top \frac{d\mathbf{y}}{d\mathbf{x}} gradx=v⊤dxdy
这里的 d y d x \frac{d\mathbf{y}}{d\mathbf{x}} dxdy是通过应用链式法则获得的,而 v ⊤ \mathbf{v}^\top v⊤的作用是将多个梯度加权合并成一个向量,这样可以直接用于反向传播。
这种方法允许深度学习框架在非标量输出的情况下有效地计算梯度,并在复杂网络结构中传播这些梯度。
向量 v \mathbf{v} v 可以被理解为对输出向量 y \mathbf{y} y 的每个元素的梯度贡献进行加权的系数。如果 y \mathbf{y} y 是由函数 f ( x ) f(\mathbf{x}) f(x) 产生的,那么 v \mathbf{v} v 中的每个元素都对应于 y \mathbf{y} y 中每个元素的偏导数权重。
具体来说,假设 y = [ y 1 , y 2 , … , y m ] T \mathbf{y} = [y_1, y_2, \ldots, y_m]^T y=[y1,y2,…,ym]T 是一个有 m m m 个元素的向量,而 x = [ x 1 , x 2 , … , x n ] T \mathbf{x} = [x_1, x_2, \ldots, x_n]^T x=[x1,x2,…,xn]T 是输入向量。那么 v \mathbf{v} v 可以是任何形式的 [ v 1 , v 2 , … , v m ] T [v_1, v_2, \ldots, v_m]^T [v1,v2,…,vm]T,其中每个 v i v_i vi 是一个标量,用于加权 y \mathbf{y} y 的第 i i i 个元素的偏导数。
v \mathbf{v} v 中的元素是偏导数组成的向量
在计算向量-雅可比乘积 v ⊤ d y d x \mathbf{v}^\top \frac{d\mathbf{y}}{d\mathbf{x}} v⊤dxdy 时,向量 v \mathbf{v} v 的选择决定了想要加权强调的输出元素对最终梯度的贡献程度。换句话说, v \mathbf{v} v 中的每个元素 v i v_i vi 直接乘以 d y d x \frac{d\mathbf{y}}{d\mathbf{x}} dxdy 中相应的偏导数,这影响了每个输出元素对输入元素 x \mathbf{x} x 的梯度计算的贡献。
举个例子:
- 如果 y \mathbf{y} y 表示一个多任务学习场景中不同任务的损失,那么 v \mathbf{v} v 可以被设置为表示每个任务相对重要性的权重。
- 如果某些输出的梯度对最终目标更为关键,你可以通过在 v \mathbf{v} v 中增加这些元素的权重(即使其值更大)来实现。
- v \mathbf{v} v 中的元素也可以是根据某些外部标准或动态计算出的值,用以适应训练过程中变化的需求。
总之, v \mathbf{v} v 可以基于实际应用中的具体需求来设定,以控制不同输出元素在梯度计算中的贡献。
为什么要进行向量-雅可比乘积
进行向量-雅可比乘积 v ⊤ d y d x \mathbf{v}^\top \frac{d\mathbf{y}}{d\mathbf{x}} v⊤dxdy 的计算有几个关键原因
1. 非标量反向传播
在深度学习中,大多数损失函数会产生标量输出,例如交叉熵损失或均方误差损失,这使得计算梯度直接而简单。然而,在某些情况下,模型的输出可能是向量或高维张量,例如在多任务学习场景中,每个任务可能有自己的损失值,这些值被组合成一个向量。在这种情况下,直接对非标量进行反向传播需要一个明确的方法来处理多个偏导数的累积,这就是向量-雅可比乘积的用途。
2. 加权梯度
通过 v \mathbf{v} v 的选择,可以对不同的输出分量赋予不同的权重,这在实际应用中非常有用。例如,在多任务学习中,不同的任务可能具有不同的重要性。通过选择合适的 v \mathbf{v} v,可以在梯度更新过程中对某些任务给予更高的优先级,从而调整模型训练的焦点。
3. 计算效率
从计算的角度看,向量-雅可比乘积提供了一种高效计算非标量函数梯度的方法。在深度学习框架中,自动微分技术(如反向传播算法)利用这种乘法来高效地计算和传播梯度,无论输出是标量还是非标量。这种方法避免了直接计算雅可比矩阵的需要,后者在大多数情况下既不实用也不高效。
4. 灵活性和通用性
使用向量-雅可比乘积使得梯度计算更加灵活,能够适应各种不同的需求和场景。
总之,进行向量-雅可比乘积的计算是深度学习中一种重要的技术,它允许复杂模型的有效训练,提供了对模型训练过程的精细控制,并确保了计算的高效性。