投稿:Intelligent Software Development
团队介绍:团队成员来自一线互联网公司,工作在架构设计与优化、工程方法研究与实践的最前线,曾参与搜索、互联网广告、共有云/私有云等大型产品的设计、开发和技术优化工作。目前主要专注在机器学习、微服务架构设计、虚拟化/容器化、持续交付/DevOps等领域,希望通过先进技术和工程方法最大化提升软件和服务的竞争力。
无论即将到来的是大数据时代还是人工智能时代,亦或是传统行业使用人工智能在云上处理大数据的时代,作为一个有理想有追求的程序员,不懂深度学习(Deep Learning)这个超热的技术,会不会感觉马上就out了?现在救命稻草来了,《零基础入门深度学习》系列文章旨在帮助爱编程的你从零基础达到入门级水平。
零基础意味着你不需要太多的数学知识,只要会写程序就行了,没错,这是专门为程序员写的文章。虽然文中会有很多公式你也许看不懂,但同时也会有更多的代码,程序员的你一定能看懂的(我周围是一群狂热的Clean Code程序员,所以我写的代码也不会很差)。
1深度学习是啥
在人工智能领域,有一个方法叫机器学习。在机器学习这个方法里,有一类算法叫神经网络。神经网络如下图所示:
上图中每个圆圈都是一个神经元,每条线表示神经元之间的连接。我们可以看到,上面的神经元被分成了多层,层与层之间的神经元有连接,而层内之间的神经元没有连接。最左边的层叫做输入层,这层负责接收输入数据;最右边的层叫输出层,我们可以从这层获取神经网络输出数据。输入层和输出层之间的层叫做隐藏层。
隐藏层比较多(大于2)的神经网络叫做深度神经网络。而深度学习,就是使用深层架构(比如,深度神经网络)的机器学习方法。
那么深层网络和浅层网络相比有什么优势呢?简单来说深层网络能够表达力更强。事实上,一个仅有一个隐藏层的神经网络就能拟合任何一个函数,但是它需要很多很多的神经元。而深层网络用少得多的神经元就能拟合同样的函数。也就是为了拟合一个函数,要么使用一个浅而宽的网络,要么使用一个深而窄的网络。而后者往往更节约资源。
深层网络也有劣势,就是它不太容易训练。简单的说,你需要大量的数据,很多的技巧才能训练好一个深层网络。这是个手艺活。
2感知器
看到这里,如果你还是一头雾水,那也是很正常的。为了理解神经网络,我们应该先理解神经网络的组成单元——神经元。神经元也叫做感知器。感知器算法在上个世纪50-70年代很流行,也成功解决了很多问题。并且,感知器算法也是非常简单的。
- 感知器的定义
下图是一个感知器:
可以看到,一个感知器有如下组成部分:
如果看完上面的公式一下子就晕了,不要紧,我们用一个简单的例子来帮助理解。
例子:用感知器实现and函数
我们设计一个感知器,让它来实现and运算。程序员都知道,and是一个二元函数(带有两个参数和),下面是它的真值表:
为了计算方便,我们用0表示false,用1表示true。这没什么难理解的,对于C语言程序员来说,这是天经地义的。
我们令,而激活函数就是前面写出来的阶跃函数,这时,感知器就相当于and函数。不明白?我们验算一下:
输入上面真值表的第一行,即,那么根据公式(1),计算输出:
也就是当X1X2都为0的时候,为0,这就是真值表的第一行。读者可以自行验证上述真值表的第二、三、四行。
例子:用感知器实现or函数
同样,我们也可以用感知器来实现or运算。仅仅需要把偏置项的值设置为-0.3就可以了。我们验算一下,下面是or运算的真值表:
我们来验算第二行,这时的输入是,带入公式(1):
也就是当时X1=0,X2=为1,即or真值表第二行。读者可以自行验证其它行。
- 感知器还能做什么
事实上,感知器不仅仅能实现简单的布尔运算。它可以拟合任何的线性函数,任何线性分类或线性回归问题都可以用感知器来解决。前面的布尔运算可以看作是二分类问题,即给定一个输入,输出0(属于分类0)或1(属于分类1)。如下面所示,and运算是一个线性分类问题,即可以用一条直线把分类0(false,红叉表示)和分类1(true,绿点表示)分开。
然而,感知器却不能实现异或运算,如下图所示,异或运算不是线性的,你无法用一条直线把分类0和分类1分开。
- 感知器的训练
现在,你可能困惑前面的权重项和偏置项的值是如何获得的呢?这就要用到感知器训练算法:将权重项和偏置项初始化为0,然后,利用下面的感知器规则迭代的修改和,直到训练完成。
其中:
Wi是与输入Xi对应的权重项,b是偏置项。事实上,可以把看作是值永远为1的输入Xb所对应的权重。是训练样本的实际值,一般称之为label。而是感知器的输出值,它是根据公式(1)计算得出。是一个称为学习速率的常数,其作用是控制每一步调整权的幅度。
每次从训练数据中取出一个样本的输入向量,使用感知器计算其输出,再根据上面的规则来调整权重。每处理一个样本就调整一次权重。经过多轮迭代后(即全部的训练数据被反复处理多轮),就可以训练出感知器的权重,使之实现目标函数。
- 编程实战:实现感知器
对于程序员来说,没有什么比亲自动手实现学得更快了,而且,很多时候一行代码抵得上千言万语。接下来我们就将实现一个感知器。
下面是一些说明:
- 使用python语言。python在机器学习领域用的很广泛,而且,写python程序真的很轻松。
- 面向对象编程。面向对象是特别好的管理复杂度的工具,应对复杂问题时,用面向对象设计方法很容易将复杂问题拆解为多个简单问题,从而解救我们的大脑。
- 没有使用numpy。numpy实现了很多基础算法,对于实现机器学习算法来说是个必备的工具。但为了降低读者理解的难度,下面的代码只用到了基本的python(省去您去学习numpy的时间)。
下面是感知器类的实现,非常简单。去掉注释只有27行,而且还包括为了美观(每行不超过60个字符)而增加的很多换行。
接下来,我们利用这个感知器类去实现and函数。
将上述程序保存为perceptron.py文件,通过命令行执行这个程序,其运行结果为:
神奇吧!感知器竟然完全实现了and函数。读者可以尝试一下利用感知器实现其它函数。
从上面部分我们已经学会了编写一个简单的感知器,并用它来实现一个线性分类器。大家应该还记得用来训练感知器的『感知器规则』把。然而,我们并没有关心这个规则是怎么得到的。下面部门将通过介绍另外一种『感知器』,也就是『线性单元』,来说明关于机器学习一些基本的概念,比如模型、目标函数、优化算法等等。这些概念对于所有的机器学习算法来说都是通用的,掌握了这些概念,就掌握了机器学习的基本套路。
3线性单元是啥
感知器有一个问题,当面对的数据集不是线性可分的时候,『感知器规则』可能无法收敛,这意味着我们永远也无法完成一个感知器的训练。为了解决这个问题,我们使用一个可导的线性函数来替代感知器的阶跃函数,这种感知器就叫做线性单元。线性单元在面对线性不可分的数据集时,会收敛到一个最佳的近似上。
为了简单起见,我们可以设置线性单元的激活函数f为
这样的线性单元如下图所示
对比此前我们讲过的感知器
这样替换了激活函数f之后,线性单元将返回一个实数值而不是0,1分类。因此线性单元用来解决回归问题而不是分类问题。
- 线性单元的模型
当我们说模型时,我们实际上在谈论根据输入x预测输出y的算法。比如,x可以是一个人的工作年限,y可以是他的月薪,我们可以用某种算法来根据一个人的工作年限来预测他的收入。比如:
函数h(x)叫做假设,而w、b是它的参数。我们假设参数w=100,参数y=500,如果一个人的工作年限是5年的话,我们的模型会预测他的月薪为
你也许会说,这个模型太不靠谱了。是这样的,因为我们考虑的因素太少了,仅仅包含了工作年限。如果考虑更多的因素,比如所处的行业、公司、职级等等,可能预测就会靠谱的多。我们把工作年限、行业、公司、职级这些信息,称之为特征。对于一个工作了5年,在IT行业,百度工作,职级T6这样的人,我们可以用这样的一个特征向量来表示它:x = (5, IT, 百度, T6)。
既然输入x变成了一个具备四个特征的向量,相对应的,仅仅一个参数就不够用了,我们应该使用4个参数W1,W2,W3,W4,每个特征对应一个。这样,我们的模型就变成
其中,X 1对应工作年限,X2对应行业,X3对应公司,X4对应职级。
为了书写和计算方便,我们可以令W0等于b,同时令W0对应于特征X0。由于X0其实并不存在,我们可以令它的值永远为1。也就是说
这样上面的式子就可以写成
我们还可以把上式写成向量的形式
长成这种样子模型就叫做线性模型,因为输出就是输入特征的线性组合。
4监督学习和无监督学习
接下来,我们需要关心的是这个模型如何训练,也就是参数取什么值最合适。
机器学习有一类学习方法叫做监督学习,它是说为了训练一个模型,我们要提供这样一堆训练样本:每个训练样本即包括输入特征X,也包括对应的输出(也叫做标记,label)。也就是说,我们要找到很多人,我们既知道他们的特征(工作年限,行业...),也知道他们的收入。我们用这样的样本去训练模型,让模型既看到我们提出的每个问题(输入特征X),也看到对应问题的答案(标记Y)。当模型看到足够多的样本之后,它就能总结出其中的一些规律。然后,就可以预测那些它没看过的输入所对应的答案了。
另外一类学习方法叫做无监督学习,这种方法的训练样本中只有而没有。模型可以总结出特征的一些规律,但是无法知道其对应的答案。
很多时候,既有X又有Y的训练样本是很少的,大部分样本都只有。比如在语音到文本(STT)的识别任务中,X是语音,Y是这段语音对应的文本。我们很容易获取大量的语音录音,然而把语音一段一段切分好并标注上对应文字则是非常费力气的事情。这种情况下,为了弥补带标注样本的不足,我们可以用无监督学习方法先做一些聚类,让模型总结出哪些音节是相似的,然后再用少量的带标注的训练样本,告诉模型其中一些音节对应的文字。这样模型就可以把相似的音节都对应到相应文字上,完成模型的训练。
- 线性单元的目标函数
现在,让我们只考虑监督学习。
在监督学习下,对于一个样本,我们知道它的特征x,以及标记y。同时,我们还可以根据模型h(x)计算得到输出y的平均值。注意这里面我们用y表示训练样本里面的标记,也就是实际值;用带上划线的表示模型计算的出来的预测值。我们当然希望模型计算出来的y的平均值和y越接近约好。
数学上有很多方法来表示的和的接近程度,比如我们可以用和的差的平方的来表示它们的接近程度
我们把叫做单个样本的误差。至于为什么前面要乘1/2,是为了后面计算方便。
训练数据中会有很多样本,比如N个,我们可以用训练数据中所有样本的误差的和,来表示模型的误差E,也就是
上式的e(1)表示第一个样本的误差,e(2)表示第二个样本的误差......。
我们还可以把上面的式子写成和式的形式。使用和式,不光书写起来简单,逼格也跟着暴涨,一举两得。所以一定要写成下面这样
其中
(式2)中,X(i)表示第i个训练样本的特征,Y(i)表示第个样本的标记,我们也可以用元组(X(i),Y(i))表示第训练样本。Y(i)平均值的则是模型对第i个样本的预测值。
我们当然希望对于一个训练数据集来说,误差最小越好,也就是(式2)的值越小越好。对于特定的训练数据集来说,(X(i),Y(i))的值都是已知的,所以(式2)其实是参数W的函数。
由此可见,模型的训练,实际上就是求取到合适的w,使(式2)取得最小值。这在数学上称作优化问题,而E(w)就是我们优化的目标,称之为目标函数。
5梯度下降优化算法
大学时我们学过怎样求函数的极值。函数y=f(x)的极值点,就是它的导数f’(x)=0的那个点。因此我们可以通过解方程f’(x)=0,求得函数的极值点(x0,y0)。
不过对于计算机来说,它可不会解方程。但是它可以凭借强大的计算能力,一步一步的去把函数的极值点『试』出来。如下图所示:
首先,我们随便选择一个点开始,比如上图的点。接下来,每次迭代修改x的为x1,x2,x3,经过数次迭代后最终达到函数最小值点。
你可能要问了,为啥每次修改的值,都能往函数最小值那个方向前进呢?这里的奥秘在于,我们每次都是向函数y=f(x)的梯度的相反方向来修改。什么是梯度呢?翻开大学高数课的课本,我们会发现梯度是一个向量,它指向函数值上升最快的方向。显然,梯度的反方向当然就是函数值下降最快的方向了。我们每次沿着梯度相反方向去修改的值,当然就能走到函数的最小值附近。之所以是最小值附近而不是最小值那个点,是因为我们每次移动的步长不会那么恰到好处,有可能最后一次迭代走远了越过了最小值那个点。步长的选择是门手艺,如果选择小了,那么就会迭代很多轮才能走到最小值附近;如果选择大了,那可能就会越过最小值很远,收敛不到一个好的点上。
按照上面的讨论,我们就可以写出梯度下降算法的公式
其中,▽是梯度算子,▽f(x)就是指f(x)的梯度。n是步长,也称作学习速率。
对于上一节列出的目标函数(式2)
梯度下降算法可以写成
聪明的你应该能想到,如果要求目标函数的最大值,那么我们就应该用梯度上升算法,它的参数修改规则是
下面,请先做几次深呼吸,让你的大脑补充足够的新鲜的氧气,我们要来求取▽E(w),然后带入上式,就能得到线性单元的参数修改规则。
关于▽E(w)的推导过程,我单独把它们放到一节中。您既可以选择慢慢看,也可以选择无视。在这里,您只需要知道,经过一大串推导,目标函数E(w)的梯度是
因此,线性单元的参数修改规则最后是这个样子
有了上面这个式子,我们就可以根据它来写出训练线性单元的代码了。
需要说明的是,如果样本有N个特征,则上式中的x,w都是N+1维向量(因为我们加上了一个恒为1的虚拟特征,参考前面的内容),而y是标量。用高逼格的数学符号表示,就是
为了让您看明白说的是啥,我吐血写下下面这个解释(写这种公式可累可累了)。因为是N+1维列向量,所以(式3)可以写成
如果您还是没看明白,建议您也吐血再看一下大学时学过的《线性代数》吧。
- ▽E(w)的推导
这一节你尽可以跳过它,并不太会影响到全文的理解。当然如果你非要弄明白每个细节,那恭喜你骚年,机器学习的未来一定是属于你的。
首先,我们先做一个简单的前戏。我们知道函数的梯度的定义就是它相对于各个变量的偏导数,所以我们写下下面的式子
可接下来怎么办呢?我们知道和的导数等于导数的和,所以我们可以先把求和符号里面的导数求出来,然后再把它们加在一起就行了,也就是
现在我们可以不管高大上的了,先专心把里面的导数求出来。
我们知道,y是与w无关的常数,而y的均值=wTx,下面我们根据链式求导法则来求导(上大学时好像叫复合函数求导法则)
我们分别计算上式等号右边的两个偏导数
代入,我们求得里面的偏导数是
最后代入,求得
至此,大功告成。
- 随机梯度下降算法(Stochastic Gradient Descent, SGD)
如果我们根据(式3)来训练模型,那么我们每次更新的迭代,要遍历训练数据中所有的样本进行计算,我们称这种算法叫做批梯度下降(Batch Gradient Descent)。如果我们的样本非常大,比如数百万到数亿,那么计算量异常巨大。因此,实用的算法是SGD算法。在SGD算法中,每次更新w的迭代,只计算一个样本。这样对于一个具有数百万样本的训练数据,完成一次遍历就会对w更新数百万次,效率大大提升。由于样本的噪音和随机性,每次更新并不一定按照减少E的方向。然而,虽然存在一定随机性,大量的更新总体上沿着减少E的方向前进的,因此最后也能收敛到最小值附近。下图展示了SGD和BGD的区别
如上图,椭圆表示的是函数值的等高线,椭圆中心是函数的最小值点。红色是BGD的逼近曲线,而紫色是SGD的逼近曲线。我们可以看到BGD是一直向着最低点前进的,而SGD明显躁动了许多,但总体上仍然是向最低点逼近的。
最后需要说明的是,SGD不仅仅效率高,而且随机性有时候反而是好事。今天的目标函数是一个『凸函数』,沿着梯度反方向就能找到全局唯一的最小值。然而对于非凸函数来说,存在许多局部最小值。随机性有助于我们逃离某些很糟糕的局部最小值,从而获得一个更好的模型。
- 实现线性单元
接下来,让我们撸一把代码。
因为我们已经写了感知器的代码,因此我们先比较一下感知器模型和线性单元模型,看看哪些代码能够复用。
比较的结果令人震惊,原来除了激活函数f不同之外,两者的模型和训练规则是一样的(在上表中,线性单元的优化算法是SGD算法)。那么,我们只需要把感知器的激活函数进行替换即可。对于一个养成良好习惯的程序员来说,重复代码是不可忍受的。大家应该把代码保存在一个代码库中(比如git)。
通过继承Perceptron,我们仅用几行代码就实现了线性单元。这再次证明了面向对象编程范式的强大。
接下来,我们用简单的数据进行一下测试。
程序运行结果如下图
拟合的直线如下图
6小结
事实上,一个机器学习算法其实只有两部分:
- 模型:从输入特征预测输入的那个函数
- 目标函数:目标函数取最小(最大)值时所对应的参数值,就是模型的参数的最优值。很多时候我们只能获得目标函数的局部最小(最大)值,因此也只能得到模型参数的局部最优值。
因此,如果你想最简洁的介绍一个算法,列出这两个函数就行了。
接下来,你会用优化算法去求取目标函数的最小(最大)值。[随机]梯度{下降|上升}算法就是一个优化算法。针对同一个目标函数,不同的优化算法会推导出不同的训练规则。我们后面还会讲其它的优化算法。
其实在机器学习中,算法往往并不是关键,真正的关键之处在于选取特征。选取特征需要我们人类对问题的深刻理解,经验、以及思考。而神经网络算法的一个优势,就在于它能够自动学习到应该提取什么特征,从而使算法不再那么依赖人类,而这也是神经网络之所以吸引人的一个方面。
现在,经过漫长的烧脑,你已经具备了学习神经网络的必备知识。下一篇文章,我们将介绍本系列文章的主角:神经网络,以及用来训练神经网络的大名鼎鼎的算法:反向传播算法。