实现基本自动微分操作(Basic Automatic Differentiation)是一种常用的自动微分方法,用于计算函数的导数。 所谓的自动计算微分,就是通过计算图,从输入到输出,反向传播,计算每个节点的导数。而这个计算图,通俗点来说就是高中所学到的链式法则需要画的函数关系图,也可以理解为深度学习中神经网络的结构图。 举个例子来说:假设有函数,那么其计算图可以大致表示为以下几层:

  1. 第一层为输入值x
  2. 第二层为两个节点,分别为
  3. 第三层为一个节点,为
  4. 第四层为一个节点,为,也即是 那么在前向传播中,从输入开始,根据计算图的结构,就可以算出来每一个节点的值;在反向传播中,从输出开始,根据计算图的结构,即每条边对应的函数的导数是可知的,就可以算出来每一个节点的导数。这里不再赘述。 在本题中,Value类的前驱节点通过children参数初始化为一个集合,则是因为在实际上,对于一个节点来说,并不需要知道前驱节点的顺序,只需要知道前驱节点是谁即可。而对于整个计算图来说,并不需要知道每一层的连接方式具体是什么,只需要知道每一层有哪些节点即可。

总的来说,整个流程可以归结为以下几个步骤:

  1. 构建计算图(神经网络),这在main函数中已经给出。
  2. 前向传播,计算每个节点的值 从输入开始,根据计算图的结构,计算每个节点的值。
  3. 反向传播,计算每个节点的导数 从输出开始,根据计算图的结构,计算每个节点的导数,这里用到的最关键的技术就是链式法则,此处不再赘述。由于选择集合作为前驱节点的存储结构,你需要通过拓扑排序来计算每个节点的导数。具体细节可参考图论的算法。

值得注意的是,在本题的代码中,你需要将梯度进行累加,而不是直接赋值。其原因在于,一个节点往往会被多个节点所使用,因此需要将所有使用该节点的节点的梯度进行累加。

标准代码如下

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})"