实现基本自动微分操作(Basic Automatic Differentiation)是一种常用的自动微分方法,用于计算函数的导数。
所谓的自动计算微分,就是通过计算图,从输入到输出,反向传播,计算每个节点的导数。而这个计算图,通俗点来说就是高中所学到的链式法则需要画的函数关系图,也可以理解为深度学习中神经网络的结构图。
举个例子来说:假设有函数,那么其计算图可以大致表示为以下几层:
- 第一层为输入值x
- 第二层为两个节点,分别为
和
- 第三层为一个节点,为
- 第四层为一个节点,为
,也即是
那么在前向传播中,从输入开始,根据计算图的结构,就可以算出来每一个节点的值;在反向传播中,从输出开始,根据计算图的结构,即每条边对应的函数的导数是可知的,就可以算出来每一个节点的导数。这里不再赘述。 在本题中,Value类的前驱节点通过children参数初始化为一个集合,则是因为在实际上,对于一个节点来说,并不需要知道前驱节点的顺序,只需要知道前驱节点是谁即可。而对于整个计算图来说,并不需要知道每一层的连接方式具体是什么,只需要知道每一层有哪些节点即可。
总的来说,整个流程可以归结为以下几个步骤:
- 构建计算图(神经网络),这在main函数中已经给出。
- 前向传播,计算每个节点的值 从输入开始,根据计算图的结构,计算每个节点的值。
- 反向传播,计算每个节点的导数 从输出开始,根据计算图的结构,计算每个节点的导数,这里用到的最关键的技术就是链式法则,此处不再赘述。由于选择集合作为前驱节点的存储结构,你需要通过拓扑排序来计算每个节点的导数。具体细节可参考图论的算法。
值得注意的是,在本题的代码中,你需要将梯度进行累加,而不是直接赋值。其原因在于,一个节点往往会被多个节点所使用,因此需要将所有使用该节点的节点的梯度进行累加。
标准代码如下
class Value:
def __init__(self, data, _children=(), _op=""):
self.data = data
self.grad = 0
self._backward = lambda: None
self._prev = set(_children)
self._op = _op
def __add__(self, other):
other = other if isinstance(other, Value) else Value(other)
out = Value(self.data + other.data, (self, other), "+")
def _backward():
self.grad += out.grad
other.grad += out.grad
out._backward = _backward
return out
def __mul__(self, other):
other = other if isinstance(other, Value) else Value(other)
out = Value(self.data * other.data, (self, other), "*")
def _backward():
self.grad += other.data * out.grad
other.grad += self.data * out.grad
out._backward = _backward
return out
def relu(self):
out = Value(0 if self.data < 0 else self.data, (self,), "ReLU")
def _backward():
self.grad += (out.data > 0) * out.grad
out._backward = _backward
return out
def backward(self):
topo = []
visited = set()
def build_topo(v):
if v not in visited:
visited.add(v)
for child in v._prev:
build_topo(child)
topo.append(v)
build_topo(self)
self.grad = 1
for v in reversed(topo):
v._backward()
def __repr__(self):
return f"Value(data={self.data}, grad={self.grad})"