Skip to main content
  1. Posts/

(CSE 599W)Reverse Mode Autodiff

Table of Contents
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.

Bakpropagation 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

可以参考 MULTI-VARIABLE CHAIN RULE

实现
#

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

 1  def 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
53 for 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            ])
64            node_to_output_grads_list[inp].append(input_grads[idx])

参考链接
#

Related

[施工完成] CSAPP Cachelab

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

【施工中】torch2trt 学习笔记

前言 # 偶然发现了 torch2trt 的模型转换方案,思路是直接将pytorch op映射到TensorRT的python api. 在pytorch进行每个op forward的时候,tensorrt也相应往network上添加op. 这里会先涉及torch2trt的使用,后面会补充这个转换工具的代码学习

Jetson Nano踩坑记录

·8 mins
写在前面 # 主要是需要在jetson nano做模型转换,来记录下踩的坑 目前有两条路径,一条是我们现有的转换路径,也就是pytorch->onnx(->caffe)->trt的路径 在这条路径上踩了比较多的坑,最终暂时放弃,最直接的原因是cudnn8.0升级接口发生改动,编译caffe遇到较多问题 这里其实仍然采用了两条平行的路径,一条是直接在nano上构建环境,另外一种是基于docker(包括构建交叉编译环境用于加快编译速度)