深度学习利器之自动微分(1)
深度学习利器之自动微分(1)
0x00 摘要
本文和下文以 Automatic Differentiation in Machine Learning: a Survey 这篇论文为基础,逐步分析自动微分这个机器学习的基础利器。
0.1 缘起
笔者计划分析 PyTorch 的分布式实现,但是在分析分布式autograd 时发现,如果不把自动微分以及普通 autograd 引擎梳理清楚,分布式autograd的分析就是寸步难行,因此返回头来学习,遂有此文。
0.2 自动微分
我们知道,深度学习框架训练模型的基本流程是:
- 依据模型搭建计算图。
- 依据输入计算损失函数。
- 计算损失函数对模型参数的导数。
- 根据得出的导数,利用梯度下降法等方法来反向更新模型参数,让损失函数最小化。
搭建计算图(依赖关系)和计算损失函数的过程,称为"正向传播”,这个是依据用户模型完成,本质上是用户自己处理。而依据损失函数求导的过程,称为"反向传播”,这个对于用户来说太繁重,所以各种深度学习框架都提供了自动求导功能。深度学习框架帮助我们解决的核心问题就是两个:
- 反向传播时的自动梯度计算和更新。
- 使用 GPU 进行计算。
于是这就牵扯出来自动梯度计算这个概念。
在数学与计算代数学中,自动微分或者自动求导(Automatic Differentiation,简称AD)也被称为微分算法或数值微分。它是一种数值计算的方式,其功能是计算复杂函数(多层复合函数)在某一点处的导数,梯度,Hessian矩阵值等等。
0x01 基本概念
为了行文完整,我们首先要介绍一些基本概念或者思想,可能部分概念大家已经熟知,请直接跳到第二章。
1.1 机器学习
赫伯特·西蒙(Herbert Simon,1975年图灵奖获得者、1978年诺贝尔经济学奖获得者)对"学习”下过一个定义:"如果一个系统,能够通过执行某个过程,就此改进了它的性能,那么这个过程就是学习”。
所以说,机器学习就是从经验数据中学习,提取数据中的重要的模式和趋势,从而改进预估函数(有关特定输入和预期输出的功能函数)的性能。
比如:一个函数可以用来区分猫和狗,我们需要使用大量的训练数据来挖掘培养这个函数,改进其性能。
1.2 深度学习
传统机器学习使用知识和经验从原始数据中提取各种特征进行训练。提取特征就是机器学习的重要组成部分:特征工程。因为原始数据涉及到的特征数目太庞大,而且特征种类千差万别,所以特征工程是个极具挑战的部分。深度学习则是让神经网络自己学习/提取数据的各种特征,或者通过组合各种底层特征来形成一些高层特征。
1.3 损失函数
对于机器学习的功能函数,我们给定一个输入,会得到一个输出,比如输入猫,输出"是否为猫”。但是这个实际输出值可能与我们的预期值不一样。因此,我们需要构建一个评估体系,来辨别函数的好坏,这就引出了损失函数。
损失函数(loss function) 或者是代价函数(cost function)就是用来度量预期输出(真实值)和实际输出(预测值)的"落差”程度 或者说是精确度。
损失函数可以把模型拟合程度量化成一个函数值,如果我们选取不同的模型参数,则损失函数值会做相应的改变。损失函数值越小,说明 实际输出 和预期输出 的差值就越小,也就表明构建的模型精确度就越高。比如常见的均方误差(Mean Squared Error)损失函数,从几何意义上来说,它可以看成预测值和实际值的平均距离的平方。
1.4 权重和偏置
损失函数里一般有两种参数:
-
我们把神经元与神经元之间的影响程度叫作为权重(weight)。权重的大小就是连接的强弱。它通知下一层相邻神经元更应该关注哪些输入信号量。
-
除了连接权值,神经元内部还有一个施加于自身的特殊权值,叫偏置(bias)。偏置用来调整函数与真实值距离的偏差。偏置能使辅助神经元是否更容易被激活。也就是说,它决定神经元的连接加权和得有多大,才能让激发变得有意义。
神经网络结构的设计目的在于,让神经网络以"更佳”的性能来学习。而这里的所谓"学习”,就是不断调整权重和偏置,从而找到神经元之间最合适的权重和偏置,让损失函数的值达到最小。
1.5 导数和梯度
神经网络的特征之一,就是从数据样本中学习。也就是说,可以由训练数据来自动确定网络权值参数的值。
既然我们有了损失函数这个评估体系,那么就可以利用其来反向调整网络中权重,使得损失最小,即如果某些权重使得损失函数达到最小值,这些权重就是我们寻找的最理想参数。
假设损失函数为 y = f(x),我们寻找其最小值,就是求这个函数的极值点,那么就是求其一阶导数 f'(x) = 0 这个微分方程的解。但是计算机不擅长求微分方程,所以只能通过插值等方法进行海量尝试,把函数的极值点求出来。
什么是导数呢?
所谓导数,就是用来分析函数"变化率”的一种度量。针对函数中的某个特定点 x0,该点的导数就是x0点的"瞬间斜率”,也即切线斜率。
什么是梯度呢?
梯度的本意是一个向量(矢量),表示某一函数在该点处的方向导数沿着该方向取得最大值,即函数在该点处沿着该方向(此梯度的方向)变化最快,变化率最大(为该梯度的模)。
在单变量的实值函数中,对于函数的某个特定点,它的梯度方向就表示从该点出发,函数值增长最为迅猛的方向或者说是函数导数变化率最大的方向。
对于机器学习/深度学习来说,梯度方向就是损失函数变化最快的方向,因为我们希望损失最小,所以我们就通常利用数值微分来计算神经网络权值参数的梯度,按照梯度下降来确定调参的方向,按照这个方向来优化。
1.6 梯度下降
梯度下降的大致思路是:首先给参数 w, b 随机设定一些初值,然后采用迭代的算法,计算当前网络的输出,然后根据网络输出与预期输出之间的差值,反方向地去改变前面各层的参数,直至网络收敛稳定。
具体来说,就是:
- 给参数 w, b 随机设定一些初值。
- for i = 0 to 训练数据的个数:
- 根据参数 w, b 这些初值来计算当前网络对于第 i 个训练数据的输出
- 根据网络输出与预期输出之间的差值,得到一个权重 w 和偏差 b 相对于损失函数的梯度。
- 最终,针对每一个训练数据,都得到一个权重和偏差的梯度值。
- 把各个样本数据的权重梯度加起来,计算所有样本的权重梯度的平均值 \(\nabla w\)。
- 把各个样本数据的偏差梯度加起来,计算所有样本的偏差梯度的平均值 \(\nabla b\)。
- 更新权重值和偏差值:
- w = w - \(\nabla w\)
- b = b - \(\nabla b\)
- 返回 2,继续迭代,直至网络收敛。
当然,梯度下降有很多优化方法,具体逻辑各有不同。
1.7 反向传播
说到back propagation算法,我们通常强调的是反向传播。其实在本质上,它是一个双向算法。也就是说,它其实是分两大步走:
前向传播:把批量数据送入网络,计算&正向传播输入信息(就是把一系列矩阵通过激活函数的加工,一层一层的向前"蔓延”,直到抵达输出层),最终输出的预测值与真实label比较之后,用损失函数计算出此次迭代的损失,其关注点是输入怎么影响到每一层。
反向传播:反向传播误差从网络最后端开始进入到网络模型中之前的每一层,根据链式求导,调整网络权重和偏置,最终逐步调整权重,使得最终输出与期望输出的差距达到最小,其关注点是每一层怎么影响到最终结果。
具体如下图:
我们可以看到,这个图中涉及到了大量的梯度计算,于是又涉及到一个问题:这些梯度如何计算?深度学习框架,帮助我们解决的核心问题就是两个:
- 反向传播时的自动梯度计算和更新,也被称作自动微分。
- 使用 GPU 进行计算。
1.8 可微分编程
1.8.1 可微分编程永生
Yann Lecun在其博文 "深度学习已死,可微分编程永生” 之中提到:
"深度学习本质上是一种新的编程方式——可微分编程——而且这个领域正试图用这种方式来制定可重用的结构。目前我们已经有:卷积,池化,LSTM,GAN,VAE,memory单元,routing单元,等等。”
但重要的一点是,人们现在正在将各种参数化函数模块的网络组装起来,构建一种新的软件,并且使用某种基于梯度的优化来训练这些软件。
越来越多的人正在以一种依赖于数据的方式(循环和条件)来程序化地定义网络,让它们随着输入数据的动态变化而变化。这与是普通的程序非常类似,除了前者是参数化的、可以自动可微分,并且可以训练和优化。动态网络变得越来越流行(尤其是对于NLP而言),这要归功于PyTorch和Chainer等深度学习框架(注意:早在1994年,以前的深度学习框架Lush,就能处理一种称为Graph Transformer Networks的特殊动态网络,用于文本识别)。
1.8.2 深度学习成功的关键
MIT媒体实验室的David Dalrymple 也介绍过可微分编程。Dalrymple认为,深度学习的成功有两大关键,一是反向传播,二是权重相关(weight-tying),这两大特性与函数编程(functional programing)中调用可重用函数十分相同。可微分编程有成为"timeless”的潜力。
反向传播以非常优雅的方式应用了链式规则(一个简单的微积分技巧),从而把连续数学和离散数学进行了深度整合,使复杂的潜在解决方案族可以通过向量微积分自主改进。
反向传播的关键是将潜在解决方案的模式(template)组织为一个有向图。通过反向遍历这个图,算法能够自动计算"梯度向量”,而这个"梯度向量" 能引导算法寻找越来越好的解决方案。
权重相关(weight-tying)是第二个关键之处,它使得同一个权重相关的网络组件可以同时在多个地方被使用,组件的每个副本都保持一致。Weight-tying 会使网络学习到更加泛化的能力,因为单词或者物体可能出现在文本块或图像的多个位置。权重相关(weight-tied)的组件,实际上与编程中可重用函数的概念相同(就类似于你编写一个函数,然后在程序中多个地方都进行调用),而且对组件的重用方式也与函数编程中通用的"高阶函数”生成的方式完全一致。
1.8.3 可微分编程
可微分编程是一个比较新的概念,是反向传播和weight-tying的延伸。用户仅指定了函数的结构以及其调用顺序,函数程序实际上被编译成类似于反向传播所需的计算图。图的各个组成部分也必须是可微的,可微分编程把实现/部署的细节留给优化器——语言会使用反向传播根据整个程序的目标自动学习细节,基于梯度进行优化,就像优化深度学习中的权重一样。
特斯拉人工智能部门主管Andrej Karpathy也提出过一个"软件2.0”概念。
软件1.0(Software 1.0)是用Python、C++等语言编写,由对计算机的明确指令组成。通过编写每行代码,程序员可以确定程序空间中的某个特定点。
Software 2.0 是用神经网络权重编写的。没有人参与这段代码的编写。在软件2.0的情况下,人类对一个理想程序的行为指定一些约束(例如,输入输出数据集),并依据可用的计算资源来搜索程序空间中满足约束条件的程序。在这个空间中,搜索过程可以利用反向传播和随机梯度下降满足要求。
Karpathy认为,在现实世界中,大部分问题都是收集数据比明确地编写程序更容易。未来,大部分程序员不再需要维护复杂的软件库,编写复杂的程序,或者分析程序运行时间。他们需要做的是收集、整理、操作、标记、分析和可视化提供给神经网络的数据。
综上,既然知道了自动计算梯度的重要性,我们下面就来借助一篇论文 Automatic Differentiation in Machine Learning: a Survey 来具体学习一下。
0x02 微分方法
2.1 常见方法
我们首先看看微分的几种比较常用的方法:
- 手动求解法(Manual Differentiation) : 完全手动完成,依据链式法则解出梯度公式,带入数值,得到梯度。
- 数值微分法(Numerical Differentiation) :利用导数的原始定义,直接求解微分值。
- 符号微分法(Symbolic Differentiation) : 利用求导规则对表达式进行自动计算,其计算结果是导函数的表达式而非具体的数值。即,先求解析解,然后转换为程序,再通过程序计算出函数的梯度。
- 自动微分法(Automatic Differentiation) :介于数值微分和符号微分之间的方法,采用类似有向图的计算来求解微分值。
具体如下图:
2.2 手动微分
手动微分就是对每一个目标函数都需要利用求导公式手动写出求导公式,然后依照公式编写代码,带入数值,求出最终梯度。
这种方法准确有效,但是不适合工程实现,因为通用性和灵活性很差,每一次我们修改算法模型,都要修改对应的梯度求解算法。如果模型复杂或者项目频繁反复迭代,那么算法工程师 别说 996 了,就是 365 x 24 也顶不住。
2.3 数值微分
数值微分方式应该是最直接而且简单的一种自动求导方式。从导数的原始定义中,我们可以直观看到前向差分公式为:
当h取很小的数值,比如0.000001 时,导数是可以利用差分来近似计算出来的。只需要给出函数值以及自变量的差值,数值微分算法就可计算出导数值。单侧差分公式根据导数的定义直接近似计算某一点处的导数值。
数值微分的优点是:
- 上面的计算式几乎适用所有情况,除非该点不可导,
- 实现简单。
- 对用户隐藏求解过程。
但是,数值微分有几个问题:
- 计算量太大,求解速度是这几种方法中最慢的,尤其是当参数多的时候,因为因为每计算一个参数的导数,你都需要重新计算f(x+h)。
- 因为是数值逼近,所有会不可靠,不稳定的情况,无法获得一个相对准确的导数值。如果 h 选取不当,可能会得到与符号相反的结果,导致误差增大。尤其是两个严重问题:
- 截断错误(Truncation error):在数值计算中 h 无法真正取零导致的近似误差。
- 舍入误差(Roundoff Error):在计算过程中出现的对小数位数的不断舍入会导致求导过程中的误差不断累积。
为了缓解截断错误,人们提出了中心微分近似(center difference approximation),这方法仍然无法解决舍入误差,只是减少误差,但是它比单侧差分公式有更小的误差和更好的稳定性。具体公式如下:
虽然数值微分有一些缺点,但是好处是简单实现,所以可以用来校验其他算法所得到梯度的正确性,比如"gradient check"就是利用数值微分法。
2.4 符号微分
符号微分(Symbolic Differentiation)属符号计算的范畴,利用求导规则对表达式进行自动计算,其计算结果是导函数的表达式。符号计算用于求解数学中的公式解(也称解析解),得到的是 解的表达式而非具体的数值。
符号微分适合符号表达式的自动求导,符号微分的原理是用下面的简单求导规则替代手动微分:
符号微分利用代数软件,实现微分的一些公式,然后根据基本函数的求导公式以及四则运算、复合函数的求导法则,将公式的计算过程转化成微分过程,这样就可以对用户提供的具有closed form的数学表达式进行"自动微分"求解。就是先求解析解,然后转换为程序,再通过程序计算出函数的梯度。
符号微分计算出的表达式需要用字符串或其他数据结构存储,如表达式树。数学软件如Mathematica,Maple,matlab中实现了这种技术。python语言的符号计算库也提供了这类算法。
符号微分的问题是:
- 表达式必须是closed form的数学表达式,也就是必须能写成完整数学表达式的,不能有编程语言中的循环结构,条件结构等。这样才能将整个问题转换为一个纯数学符号问题,从而利用一些代数软件进行符号微分求解。
- 表达式复杂时候(深层复合函数,如神经网络的映射函数),因为计算机也许并不能进行智能的简化,所以容易出现"表达式膨胀”(expression swell)的问题。
表达式膨胀如下图所示,稍不注意,符号微分求解就会如下中间列所示,表达式急剧膨胀,导致问题求解也随着变慢,计算上的冗余且成本高昂:
其实,对于机器学习中的应用,不需要得到导数的表达式,而只需计算函数在某一点处的导数值。
2.5 自动微分
2.5.1 中间方法
自动微分是介于数值微分和符号微分之间的方法,采用类似有向图的计算来求解微分值。
-
数值微分:一开始就直接代入数值近似求解。
-
符号微分:直接对代数表达式求解析解,最后才代入数值进行计算。
-
自动微分:首先对基本算子(函数)应用符号微分方法,其次带入数值进行计算,保留中间结果,最后通过链式求导法将中间结果应用于整个函数,这样可以做到完全向用户隐藏微分求解过程,也可以灵活于编程语言的循环结构、条件结构等结合起来。
关于解析解我们还要做一些说明。几乎所有机器学习算法在训练或预测时都可以归结为求解最优化问题,如果目标函数可导,则问题就变为求训练函数的驻点。但是通常情况下我们无法得到驻点的解析解,因此只能采用数值优化算法,如梯度下降法,牛顿法,拟牛顿法等等。这些数值优化算法都依赖于函数的一阶导数值或二阶导数值(包括梯度与Hessian矩阵)。因此需要解决如何求一个复杂函数的导数问题,自动微分技术是解决此问题的一种通用方法。
由于自动微分法只对基本函数或常数运用符号微分法则,所以它可以灵活结合编程语言的循环结构,条件结构等。使用自动微分和不使用自动微分对代码总体改动非常小,由于它实际是一种图计算,可以对其做很多优化,所以该方法在现代深度学习系统中得到广泛应用。
2.5.2 数学基础
自动微分 (AD)是用程序来自动化推导Jacobian矩阵或者其中的一部分,是计算因变量对某个自变量导数的一种数值计算方式,所以其数学基础是链式求导法则和雅克比矩阵。
2.5.2.1 链式求导
在计算链式法则之前,我们先回顾一下复合函数。复合函数在本质上就是有关函数的函数(function of functions)。它将一个函数的返回值作为参数传递给另一个函数,并且将另一个函数的返回值作为参数再传递给下一个函数,也就是 函数套函数,把几个简单的函数复合为一个较为复杂的函数。
链式法则是微积分中的求导法则,用于求一个复合函数的导数,是在微积分的求导运算中一种常用的方法。复合函数的导数将是构成复合这有限个函数在相应点的 导数的乘积,就像锁链一样一环套一环,故称链式法则。
比如求导:
链式求导,令:
则
即可求导。
2.5.2.2 雅克比矩阵
在向量微积分中,雅可比矩阵是一阶偏导数以一定方式排列成的矩阵,其行列式称为雅可比行列式。雅可比矩阵的重要性在于它体现了一个可微方程与给出点的最优线性逼近。
雅可比矩阵表示两个向量所有可能的偏导数。它是一个向量相对于另一个向量的梯度,其实现的是 n维向量 到 m 维向量的映射。
在矢量运算中,雅克比矩阵是基于函数对所有变量一阶偏导数的数值矩阵,当输入个数 = 输出个数时又称为雅克比行列式。
假设输入向量 \(x∈Rn\),而输出向量 \(y∈Rm\),则Jacobian矩阵定义为:
0xFF 参考
https://en.wikipedia.org/wiki/Automatic_differentiation
自动微分(Automatic Differentiation)
自动微分(Automatic Differentiation)简介——tensorflow核心原理
【深度学习理论】一文搞透梯度下降Gradient descent
梯度下降算法(Gradient Descent)的原理和实现步骤
【深度学习理论】纯公式手推+代码撸——神经网络的反向传播+梯度下降
Automatic Differentiation in Machine Learning: a Survey
自动微分(Automatic Differentiation)简介
Automatic Differentiation in Machine Learning: a Survey
自动微分(Automatic Differentiation)简介
PyTorch 的 backward 为什么有一个 grad_variables 参数?
BACKPACK: PACKING MORE INTO BACKPROP
【深度学习理论】一文搞透pytorch中的tensor、autograd、反向传播和计算图
[PyTorch 学习笔记] 1.5 autograd 与逻辑回归
OpenMMLab:PyTorch 源码解读之 torch.autograd:梯度计算详解
https://zhuanlan.zhihu.com/p/348555597)