机器学习系统 UW CSE 599W: Systems for ML 笔记 (上篇)
这是一门由陈天奇大佬讲的机器学习系统课程,UW的网站上放出了这么课的相关信息,本文是这门课的课堂笔记。
这门课,上对接的是Tensorflow, Pytorch, Caffe, MXNet这类机器学习计算框架,下对接的是CPU/显卡/手机/单片机。。。形态各异的硬件设备。
但是UW只放出了课件和作业,所以自学起来可能比较凌乱,这里参考了各位大佬的其他材料。
1. 课程概览 (lecture 3)
Fig1.1 课程概览 |
机器学习系统就处在这两者之间。
这门课程把中间地带划分成三大块:
- User API
- System Components
- Architecture
1.1 User API
第一层User API 指的是运算框架比如tensorflow或者pytorch之类的把运算进行封装,使得DL开发者不需要从自动求导写起,并且把用户build的模型转化成计算图(computational graph)。
Fig1.2 Logistic Regression的计算图 |
计算图,这个概念在接触过tensorflow以及pytorch应该或多或少听说过他俩的一个区别是tf是静态计算图,pytorch是动态计算图
Fig1.3 静态vs动态计算图 |
1.2 System Components
到了第二层系统组件做的事情就是对于计算图的优化。
比如说会做的有
deadcode检测,内存规划和优化,并行调度,
Fig1.4 并行调度优化 |
1.3 Architecture
第三层就是把优化好的计算图部署到对应硬件上的操作。
现在的做法是各个厂商自己出一套对于model serving的支持,比如Intel的OpenVINO,ARM的ARM NN,NV的TensorRT等。但是各自对于计算图中算子的支持度不同,也无法迁移。
相比TensorFlow等这样的框架孱弱的Inference性能,各大设备厂商的Inference框架性能都比较不错),比如Intel的OpenVINO,ARM的ARM NN,NV的TensorRT等,但是这里面有一个问题,各大设备厂商的框架并不具备通用性,比如对训练框架模型产生的算子支持不全(尤其是像TensorFlow这种算子很多的),通常在一个设备厂商的Inference框架能跑,但是不一定在另外一个设备厂商的Inference框架上能跑。
类比自编程语言到不同硬件的历史,最终这个问题在编译器层面得到了解决,于是乎TVM所做的核心工作也是成为计算图到硬件的编译器。
对于TVM的整体工作可以看这篇: 手把手带你遨游TVM
课程接下来的部分就是按照上面说的三个层面详细展开。
2. Automatic Differentiation (Lecture 4)
微分之于Deep learning是非常重要的一个运算,然而计算机自动实现微分不是一件很直接的事情,实现自动求微分也有很多不同的途径。
2.1 Symbolic Differentiation
符号微分的做法是模拟人类计算微分的做法,计算出对应的表达式,然后把数值放进变量,得到最后的值。
比如要实现对于函数$g(x,y) = 5 + xy$对x的偏导,那就先算出$\frac{\partial g(x,y)}{\partial x} = 0 + (0*x + y * 1)$ 体现在计算机中也就是会生成对应的导数计算图。
Fig2.1 符号微分 |
这样做有两个缺点,一个是原式必须写成closed-form也就是必须直接的写成确定的表达式,不能自动处理自定义的函数(虽然可能是内建函数的组合);另一个缺点在于,这样做会有表达式膨胀的问题:可能原表达式并不复杂但是对其求导时候生成的计算图会变得非常复杂。并且我们要的只是最后的数值,中间过程的存储并没有意义
Fig2.2 表达式膨胀问题 |
2.2 Numerical Differentiation
根据导数的定义,$f(\boldsymbol{x})$在$x_i$处的导数定义为
为了防止truncation error,可以改进为
然而这样做的问题在于无法避免rounding error,并且计算时参数量也很大性能并不好(需要控制变量,对每个入参跑一边计算)
2.3 自动微分(正向)
所有数值计算归根结底都是一系列有限的可微算子的组合 — <An introduction to automatic differentiation>
**在看了很多相关的文章以后发现这篇博客讲的最为清楚,mark之
自动微分可以理解为是公式微分和数值微分的结合,他用到了一个dual number的数学性质。使得在计算前向的时候通过加入对偶数,可以得到他的导数。
(关于dual number 和自动微分的关系可以见wiki)
简言之就是把原来的变量$x$, 替换为$x+ x'\epsilon$
其中$x'$是一个实数,$\epsilon$是一个特殊的符号,他有$\epsilon^2 = 0$的性质,在此基础上,这些运算成立:
$(x+x'\epsilon) + (y+y'\epsilon) = x + y + (x'+y')\epsilon$
$(x+x'\epsilon)\cdot(y+y'\epsilon) = xy + xy'\epsilon + x'y\epsilon + x'y'\epsilon^2 = xy + (xy' + x'y)\epsilon$
进一步,对于多项式$P(x) = p_0 + p_1x + p_2x^2 + \cdots + p_nx^n$
其中$P^{(1)}$表示$P$对第一个参数的导数,$x'$这里并不是导数,而是一个可以任意选择的实数。
并且可以证得:
$h(a+b\epsilon) = h(a) + b \cdot h'(a)\epsilon$, 所以计算一次$h(a+\epsilon)$就可以得到$h(a)$和$h'(a)$
下图表示了通过这种自动微分怎么在计算出$f(3,4)$的同时计算$\frac{\partial f}{\partial x}(3,4)$
Fig2.3 正向自动微分例子 |
从下往上看,叶子结点层是函数的输入,$x = 3+\epsilon, y = 4$然后沿箭头往上运算,在到达root的时候算出了$f(3,4) = 42$的同时也计算出了$\frac{\partial f}{\partial x}(3,4) = 24$
这时候虽然比起之前的方式已经有了很大的进步,但是对于一个1000维输入的函数,我们如果要对其中每个自变量求偏导的话我们就要计算1000遍。
2.3.2 反向自动微分
这种方式也是Tensorflow采用的求微分方案,达成的效果是,计算所有的偏导只需要跑2遍计算图,并且没有用到上述的dual number这样深奥的数学性质,这里用到的只有求导链式法则。
Fig2.4 反向自动微分例子 |
它记录了每个运算的中间结果,然后根据它们可以从根到叶自上而下的算出$f$对每个中间过程的偏导,到了叶节点一层也就算出了对所有自变量的偏导。
这个自动求导方式非常适合于很多输入一个输出的函数,很多DL任务就是这样的情况。另一个优势在于,他并不要求这个函数处处可导,只要在对应的输入点可导就能计算。
2.4 AutoDiff Algorithm
然而能不能做得更好的?
缺点1:这里其实需要在正向传播的时候在内存中保留所有中间过程,以待反向传播时候用到。
缺点2:缺乏灵活性,比如如果要求二阶导呢。
这门课中给出的做法是给反向传播建立一个新的计算图。
Fig2.5 构建求偏导计算图 |
根据黑色的正向传播的计算图,配合上链式法则和基础函数的求导规则,我们可以构建出红色的计算图,用于求所有自变量的偏导。
因为这里构建了计算图,所以在正向求$f$的时候不需要保存所有的中间数值,只需要把最后的函数输出丢给红色计算图。
并且作为计算图(DAG),它也可以进行一些computional graph optimization(变量值共享等等)
Fig2.6 两种自动求导的比较 |
之于这个的代码实现其实也比较直观:
从正向计算图的拓扑排序的逆序作为起点开始构建,利用链式法则完成整体构建。
忘了拓扑排序的刷一下Leetcode207 Course schedule
Fig2.7 AutoDiff 伪代码 |
这门课的Assignment 1 也是手动实现Audo-diff
Comments
Post a Comment