🎯要点
- 反向传播矢量化计算方式
- 前向传递和后向传递计算方式
- 图节点拓扑排序
- 一阶二阶前向和伴随模式计算
- 二元分类中生成系数高斯噪声和特征
- 二元二次方程有向无环计算图
- 超平面搜索前向梯度下降算法
- 快速傅里叶变换材料应力和切线算子
- GPU CUDA 神经网络算术微分
Python自动微分前向反向
自动微分不同于符号微分和数值微分。符号微分面临着将计算机程序转换为单一数学表达式的困难,并且可能导致代码效率低下。数值微分(有限差分法)会在离散化过程和取消过程中引入舍入误差。这两种经典方法在计算更高导数时都存在问题,复杂性和误差会增加。最后,这两种经典方法在计算函数对许多输入的偏导数时都很慢,而这是基于梯度的优化算法所需要的。自动微分解决了所有这些问题。
符号微分是我们将要解开的梯度计算的下一种方法。这是一个系统的过程,将由算术运算和符号组成的表达式转换为表示其导数的表达式。这是通过将微积分的导数规则(例如求和规则)应用于闭式表达式来实现的。
实际上,符号微分是计算机手动推导表达式导数的方式。例如下面的两个函数
f
f
f 和
g
g
g,我们可以使用微积分导出其导数的表达式。
g
(
x
)
=
cos
(
x
)
+
2
x
−
e
x
f
(
g
)
=
4
g
2
\begin{gathered} g(x)=\cos (x)+2 x-e^x \\ f(g)=4 g^2 \end{gathered}
g(x)=cos(x)+2x−exf(g)=4g2
f ( g ( x ) ) = 4 ( cos ( x ) + 2 x − e x ) 2 ( 4 ) \begin{aligned} &f(g(x))=4\left(\cos (x)+2 x-e^x\right)^2\qquad(4) \end{aligned} f(g(x))=4(cos(x)+2x−ex)2(4)
d f d x = d f d g ⋅ d g d x = 8 ( cos ( x ) + 2 x − e x ) ⋅ ( − sin ( x ) + 2 − e x ) ( 5 ) \frac{d f}{d x}=\frac{d f}{d g} \cdot \frac{d g}{d x}=8\left(\cos (x)+2 x-e^x\right) \cdot\left(-\sin (x)+2-e^x\right)\qquad(5) dxdf=dgdf⋅dxdg=8(cos(x)+2x−ex)⋅(−sin(x)+2−ex)(5)
要找到 f ( g ( x ) ) f(g(x)) f(g(x)) 输入的导数,我们只需将其插入上面的转换表达式中并对其求值即可。在实践中,我们以编程方式实现这个过程,并且所表示的变量将不仅仅是标量(例如向量、矩阵或张量)。下面是我们如何符号微分等式4得到等式 5 。
from sympy import symbols, cos, exp, diff
x = symbols("x")
fog = 4 * (cos(x) + 2 * x - exp(x)) ** 2
dfdx = diff(fog, x)
print(dfdx)
输出
4*(2*x - exp(x) + cos(x))*(-2*exp(x) - 2*sin(x) + 4)
这解决了数值微分中出现的数值不准确和不稳定的问题,因为我们有一个可以直接计算函数梯度的表达式。不过,我们仍面临限制其优化神经网络可行性的问题。
我们在符号微分中看到的主要问题是表达式膨胀。表达式膨胀导致导数表达式通过变换呈指数增长,这是系统地将导数规则应用于原始表达式的惩罚。以下面的乘法规则为例。
d
d
x
f
(
x
)
g
(
x
)
=
f
′
(
x
)
g
(
x
)
+
g
′
(
x
)
f
(
x
)
\frac{d}{d x} f(x) g(x)=f^{\prime}(x) g(x)+g^{\prime}(x) f(x)
dxdf(x)g(x)=f′(x)g(x)+g′(x)f(x)
导数表达式不仅在术语上有所增长,而且在计算上也有所增长。这甚至没有考虑到
f
f
f 或
g
g
g 本身可以是复杂的函数 - 可能会增加更多的表达式膨胀。
当我们导出
d
f
d
x
\frac{d f}{d x}
dxdf 时,我们看到了一些表达式膨胀,这是一个相对简单的函数。现在想象一下,尝试对许多可能一遍又一遍地应用导数规则的复合函数执行相同的操作,对于神经网络代表许多复杂的复合函数,是极其不切实际的。
f
(
x
)
=
e
w
x
+
b
+
e
−
(
w
x
+
b
)
e
w
x
+
b
−
e
−
(
w
x
+
b
)
∂
f
∂
w
=
(
−
x
e
−
b
−
w
x
−
x
e
b
+
w
x
)
(
e
−
b
−
w
x
+
e
b
+
w
x
)
(
−
e
−
b
−
w
x
+
e
b
+
w
x
)
2
+
−
x
e
−
b
−
w
x
+
x
e
b
+
w
x
−
e
−
b
−
w
x
+
e
b
+
w
x
\begin{gathered} f(x)=\frac{e^{w x+b}+e^{-(w x+b)}}{e^{w x+b}-e^{-(w x+b)}} \\ \frac{\partial f}{\partial w}=\frac{\left(-x e^{-b-w x}-x e^{b+w x}\right)\left(e^{-b-w x}+e^{b+w x}\right)}{\left(-e^{-b-w x}+e^{b+w x}\right)^2}+\frac{-x e^{-b-w x}+x e^{b+w x}}{-e^{-b-w x}+e^{b+w x}} \end{gathered}
f(x)=ewx+b−e−(wx+b)ewx+b+e−(wx+b)∂w∂f=(−e−b−wx+eb+wx)2(−xe−b−wx−xeb+wx)(e−b−wx+eb+wx)+−e−b−wx+eb+wx−xe−b−wx+xeb+wx
上式显示的是神经网络中看到的线性投影,后面是非线性激活函数 tanh。结果表明,在不进行简化和优化的情况下,寻找梯度来更新权重 w w w 可能会导致大量的表达式膨胀和重复计算。
面临的另一个缺点是符号微分仅限于闭式表达式。编程之所以有用,是因为它能够使用控制流根据程序的状态改变程序的行为方式,同样的原理也经常应用于神经网络。
无控制流:
from sympy import symbols, diff
def f(x):
if x > 2:
return x * 2 + 5
return x / 2 + 5
x = symbols("x")
dfdx = diff(f(x))
print(dfdx)
TypeError: cannot determine truth value of Relational
示例中暗示的最后一个缺点是我们可能会导致重复计算。在等式4 和 5 中,我们评估 e x e^x ex 三次:一次是在计算等式4 ,两次计算等式5。这可以在更大的范围内实现更复杂的功能,从而为符号微分创造更多的不切实际性。我们可以通过缓存结果来减少这个问题,但这不一定能解决表达式膨胀问题。
自动微分将复合函数表示为组成它们的变量和基本运算。所有数值计算都以这些运算为中心,由于我们知道它们的导数,我们可以将它们串联起来以得出整个函数的导数。简而言之,自动微分是数值计算的增强版本,它不仅可以评估数学函数,还可以计算它们的导数。
下面,我留下了一个示例,仅显示接受两个输入
x
1
x_1
x1 和
x
2
x_2
x2 的函数的评估跟踪的原始计算。
y
=
f
(
x
1
,
x
2
)
=
x
1
x
2
+
x
2
−
ln
(
x
1
)
x
1
=
2
,
x
2
=
4
(
6
)
\begin{gathered} y=f\left(x_1, x_2\right)=x_1 x_2+x_2-\ln \left(x_1\right) \\ x_1=2, x_2=4 \end{gathered}\qquad(6)
y=f(x1,x2)=x1x2+x2−ln(x1)x1=2,x2=4(6)
正向原始追踪 输出 v − 1 = x 1 2 v 0 = x 2 4 v 1 = v − 1 v 0 2 ( 4 ) = 8 v 2 = ln ( v − 1 ) ln ( 2 ) = 0.693 v 3 = v 1 + v 0 8 + 4 = 12 v 4 = v 3 − v 2 12 − 0.693 = 11.307 y = v 4 11.307 \begin{aligned} &\begin{array}{|c|c|} \hline \text { 正向原始追踪 }& \text { 输出 } \\ \hline v _{-1}= x _1 & 2 \\ \hline v _0= x _2 & 4 \\ \hline v _1= v _{-1} v _0 & 2(4)=8 \\ \hline v _2=\ln \left( v _{-1}\right) & \ln (2)=0.693 \\ \hline v_3=v_1+v_0 & 8+4=12 \\ \hline v _4= v _3- v _2 & 12-0.693=11.307 \\ \hline y=v_4 & 11.307 \\ \hline \end{array}\\ \end{aligned} 正向原始追踪 v−1=x1v0=x2v1=v−1v0v2=ln(v−1)v3=v1+v0v4=v3−v2y=v4 输出 242(4)=8ln(2)=0.6938+4=1212−0.693=11.30711.307
在评估轨迹之上,我们可以使用有向无环图作为数据结构,以算法方式表示评估轨迹。有向无环图中的节点表示输入变量、中间变量和输出变量,而边则描述输入到输出转换的计算层次结构。最后,该图必须是有向且无环的,以确保正确的计算流程。整体而言,这种类型的有向无环图通常称为计算图。
前向模式:
class Variable:
def __init__(self, primal, tangent):
self.primal = primal
self.tangent = tangent
def __add__(self, other):
primal = self.primal + other.primal
tangent = self.tangent + other.tangent
return Variable(primal, tangent)
def __sub__(self, other):
primal = self.primal - other.primal
tangent = self.tangent - other.tangent
return Variable(primal, tangent)
def __mul__(self, other):
primal = self.primal * other.primal
tangent = self.tangent * other.primal + other.tangent * self.primal
return Variable(primal, tangent)
def __truediv__(self, other):
primal = self.primal / other.primal
tangent = (self.tangent / other.primal) + (
-self.primal / other.primal**2
) * other.tangent
return Variable(primal, tangent)
def __repr__(self):
return f"primal: {self.primal}, tangent: {self.tangent}"
前向模式下自动微分计算
def mul_add(a, b, c):
return a * b + c * a
def div_sub(a, b, c):
return a / b - c
a, b, c = Variable(25.0, 1.0), Variable(4.0, 0.0), Variable(-5.0, 0.0)
print(f"{a = }, {b = }, {c = }")
print(f"{mul_add(a, b, c) = }")
a.tangent, b.tangent, c.tangent = 0.0, 1.0, 0.0
print(f"{div_sub(a, b, c) = }")
反向模式
class Variable:
def __init__(self, primal, adjoint=0.0):
self.primal = primal
self.adjoint = adjoint
def backward(self, adjoint):
self.adjoint += adjoint
def __add__(self, other):
variable = Variable(self.primal + other.primal)
def backward(adjoint):
variable.adjoint += adjoint
self_adjoint = adjoint * 1.0
other_adjoint = adjoint * 1.0
self.backward(self_adjoint)
other.backward(other_adjoint)
variable.backward = backward
return variable
def __sub__(self, other):
variable = Variable(self.primal - other.primal)
def backward(adjoint):
variable.adjoint += adjoint
self_adjoint = adjoint * 1.0
other_adjoint = adjoint * -1.0
self.backward(self_adjoint)
other.backward(other_adjoint)
variable.backward = backward
return variable
def __mul__(self, other):
variable = Variable(self.primal * other.primal)
def backward(adjoint):
variable.adjoint += adjoint
self_adjoint = adjoint * other.primal
other_adjoint = adjoint * self.primal
self.backward(self_adjoint)
other.backward(other_adjoint)
variable.backward = backward
return variable
def __truediv__(self, other):
variable = Variable(self.primal / other.primal)
def backward(adjoint):
variable.adjoint += adjoint
self_adjoint = adjoint * (1.0 / other.primal)
other_adjoint = adjoint * (-1.0 * self.primal / other.primal**2)
self.backward(self_adjoint)
other.backward(other_adjoint)
variable.backward = backward
return variable
def __repr__(self) -> str:
return f"primal: {self.primal}, adjoint: {self.adjoint}"
反向模式自动微分计算
def mul_add(a, b, c):
return a * b + c * a
def div_sub(a, b, c):
return a / b - c
a, b, c = Variable(25.0, 1.0), Variable(4.0, 0.0), Variable(-5.0, 0.0)
print(f"{a = }, {b = }, {c = }")
d = mul_add(a, b, c)
d.backward(1.0)
print(f"{d = }")
print(f"{a.adjoint = }, {b.adjoint = }, {c.adjoint = }")
a.adjoint, b.adjoint, c.adjoint = 0.0, 0.0, 0.0
e = div_sub(a, b, c)
e.backward(1.0)
print(f"{e = }")
print(f"{a.adjoint = }, {b.adjoint = }, {c.adjoint = }")