Skip to main content
  1. Posts/

Reverse-Mode Automatic Differentiation

·1040 words·3 mins
Note: This article is available in Chinese only. 本文暂无英文版本。 View original

背景
#

怎么算微分。。通常有三种方法:

  • Symbolic Differentiation
  • Numerical Differentiation
  • Automatic Differentiation (auto diff)
求导方法对比

auto diff 中两种主流的方式分别是 forward-mode 和 reverse-mode。 由于 forward-mode 的方法中,计算的时间复杂度是 O(n),n 是输入的参数个数;而 reverse-mode 中,计算的时间复杂度是 O(m),m 是输出节点的个数。在 DNN 中,n 往往很大,远大于 m,因此这里主要介绍 reverse-mode auto diff 方法。

backprop 和 reverse mode auto diff 的区别
#

看了 reverse mode auto diff 的过程,感觉和 backprop 是一回事呀。 实际上,backprop 指的是训练神经网络时根据 loss 的 gradient 来更新 weight 的过程,而 auto diff 是 backprop 使用的一种用来计算 gradient 的 technique。

Backpropagation refers to the whole process of training an artificial neural network using multiple backpropagation steps, each of which computes gradients and uses them to perform a Gradient Descent step. In contrast, reverse-mode auto diff is simply a technique used to compute gradients efficiently and it happens to be used by backpropagation.

chain rule
#

chain rule..也就是微积分里的链式法则,更准确地说,是多变量的 chain rule。 简单来说,就是同一条路径相乘,不同路径相加。

可以参考 MULTI-VARIABLE CHAIN RULE

实现
#

放一个 CSE 599W Systems for ML 课程的 assignment 1,比较简单,只有 add、mul、matmul 这几个算子(因为重点并不在于支持算子的种类)。 需要实现几个算子的 forward 和梯度的计算,这部分比较好写。 然后需要实现 executor 的 run 函数,做个拓扑排序然后计算即可。

 1def run(self, feed_dict):
 2    """Computes values of nodes in eval_node_list given computation graph.
 3    Parameters
 4    ----------
 5    feed_dict: list of variable nodes whose values are supplied by user.
 6
 7    Returns
 8    -------
 9    A list of values for nodes in eval_node_list.
10    """
11    node_to_val_map = dict(feed_dict)
12    print("self.eval_node_list={}".format(self.eval_node_list))
13    # Traverse graph in topological sort order and compute values for all nodes.
14    topo_order = find_topo_sort(self.eval_node_list)
15    # 按照拓扑排序的顺序来计算,保证计算当前节点的值时,其依赖的值都计算出来了。
16    """TODO: Your code here"""
17    for node in topo_order:
18        if isinstance(node.op, PlaceholderOp):
19            continue
20        # 在实际计算的时候,要用具体的值来替代node
21        input_vals = [node_to_val_map[x] for x in node.inputs]
22        res = node.op.compute(node, input_vals)
23        node_to_val_map[node] = res
24
25    # Collect node values.
26    # 因为是按照topo order计算的,最后再变为和输入相同的顺序去输出
27    node_val_results = [node_to_val_map[node] for node in self.eval_node_list]
28    return node_val_results

然后是计算梯度这部分。 需要注意 node_to_output_grads_list 是一个 dict,key 是 node,val 其实是一个 list,表示 node 对哪些后续节点的 gradient 有作用。 然后 input_grads 表示的是 node 的 input 节点相对 node 的 gradient。

听起来有些让人费解。。看个具体的例子。

对于下面这个计算图,相关变量的值如下:

计算图前向求值与反向伴随传播
详细代码
 1def test_multi_var_chain_rule():
 2    x1 = ad.Variable(name="x1")
 3    x2 = x1+3
 4    x3 = x1+5
 5    y = x2*x3
 6
 7    grad_x1, grad_x2, grad_x3 = ad.gradients(y, [x1, x2, x3])
 8
 9    executor = ad.Executor([y, grad_x1, grad_x2, grad_x3])
10    x1_val = 1 * np.ones(3)
11    y_val, grad_x1_val, grad_x2_val, grad_x3_val = executor.run(feed_dict = {x1 : x1_val})
12
13
14
15nosetests -s  -v  autodiff_test.py                                                                                                      130 
16autodiff_test.custom_test ... output_node=(x1+((x2*x3)*x1))
17node.name=(x1+((x2*x3)*x1))
18node_to_output_grads_list[node]=[Oneslike((x1+((x2*x3)*x1)))]
19grad=Oneslike((x1+((x2*x3)*x1)))
20input_grads=[Oneslike((x1+((x2*x3)*x1))), Oneslike((x1+((x2*x3)*x1)))]
21
22node.name=((x2*x3)*x1)
23node_to_output_grads_list[node]=[Oneslike((x1+((x2*x3)*x1)))]
24grad=Oneslike((x1+((x2*x3)*x1)))
25input_grads=[(Oneslike((x1+((x2*x3)*x1)))*x1), (Oneslike((x1+((x2*x3)*x1)))*(x2*x3))]
26
27node.name=(x2*x3)
28node_to_output_grads_list[node]=[(Oneslike((x1+((x2*x3)*x1)))*x1)]
29grad=(Oneslike((x1+((x2*x3)*x1)))*x1)
30input_grads=[((Oneslike((x1+((x2*x3)*x1)))*x1)*x3), ((Oneslike((x1+((x2*x3)*x1)))*x1)*x2)]
31
32node.name=x3
33node_to_output_grads_list[node]=[((Oneslike((x1+((x2*x3)*x1)))*x1)*x2)]
34grad=((Oneslike((x1+((x2*x3)*x1)))*x1)*x2)
35input_grads=None
36
37node.name=x2
38node_to_output_grads_list[node]=[((Oneslike((x1+((x2*x3)*x1)))*x1)*x3)]
39grad=((Oneslike((x1+((x2*x3)*x1)))*x1)*x3)
40input_grads=None
41
42node.name=x1
43node_to_output_grads_list[node]=[Oneslike((x1+((x2*x3)*x1))), (Oneslike((x1+((x2*x3)*x1)))*(x2*x3))]
44grad=(Oneslike((x1+((x2*x3)*x1)))+(Oneslike((x1+((x2*x3)*x1)))*(x2*x3)))
45input_grads=None
46
47length of node_to_output_grads_list = {(x1+((x2*x3)*x1)): [Oneslike((x1+((x2*x3)*x1)))], x1: [Oneslike((x1+((x2*x3)*x1))), (Oneslike((x1+((x2*x3)*x1)))*(x2*x3))], ((x2*x3)*x1): [Oneslike((x1+((x2*x3)*x1)))], (x2*x3): [(Oneslike((x1+((x2*x3)*x1)))*x1)], x2: [((Oneslike((x1+((x2*x3)*x1)))*x1)*x3)], x3: [((Oneslike((x1+((x2*x3)*x1)))*x1)*x2)]}
48self.eval_node_list=[(x1+((x2*x3)*x1)), (Oneslike((x1+((x2*x3)*x1)))+(Oneslike((x1+((x2*x3)*x1)))*(x2*x3))), ((Oneslike((x1+((x2*x3)*x1)))*x1)*x3), ((Oneslike((x1+((x2*x3)*x1)))*x1)*x2)]
49
50
51
52
53for node in reverse_topo_order:
54    # print("node.name={} op={}".format(node.name, type(node.op)))
55    grad = sum_node_list(node_to_output_grads_list[node])
56    # print("grad={}".format(grad))
57    input_grads = node.op.gradient(node, grad)
58    # input_grads表示的node的input节点相对node的gradient
59    # print("input_grads={}".format(input_grads))
60    node_to_output_grad[node] = grad
61    for idx, inp in enumerate(node.inputs):
62        node_to_output_grads_list[inp] = node_to_output_grads_list.get(inp, [])
63        node_to_output_grads_list[inp].append(input_grads[idx])

参考链接
#

Related

[施工完成] CSAPP Cachelab

·1870 words·4 mins
背景 # CSAPP:3e 的配套实验 地址 分成了两个部分,第一部分是模拟一下 cache 的 miss、hit、evict 的规则,第二部分是优化一个矩阵的转置,使得 miss 尽可能少。

【施工完成】CSAPP archlab

·3893 words·8 mins
背景 # CSAPP:3e 第四章配套的实验。第四章是讲处理器架构的,章节的重点是实现一个六阶段流水线。

【施工完成】CSAPP bomb lab

·5867 words·12 mins
背景 # 疫情肆虐,在家百无聊赖,于是开始拆炸弹。 炸弹分为 6 个阶段,每个阶段必须输入一个特定的字符串,否则炸弹就会爆炸。 提供给我们的是一个 .c 文件和一个 linux 可执行文件 bomb。