深度学习导论

978-7-115-52991-6
作者: [美] 尤金·查尔尼克(Eugene Charniak)
译者: 沈磊郑春萍
编辑: 王峰松

图书目录:

详情

《深度学习导论》讲述了前馈神经网络、Tensorflow、卷积神经网络、词嵌入与循环神经网络、序列到序列学习、深度强化学习、无监督神经网络模型等深度学习领域的基本概念和技术,通过一系列的编程任务,向读者介绍了热门的人工智能应用,包括计算机视觉和自然语言处理等。 本书编写简明扼要,理论联系实践,每一章都包含习题以及补充阅读的参考文献。本书既可作为高校人工智能课程的教学用书,也可供从业者入门参考。 本书要求读者熟悉线性代数、多元微积分、概率论与数理统计知识,另外需要读者了解Python编程。

图书摘要

版权信息

书名:【抢鲜版】-深度学习导论

ISBN:978-7-115-52991-6

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    [美] 尤金·查尔尼克(Eugene Charniak)

译    沈磊 郑春萍

责任编辑 王峰松

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


Introduction to Deep Learning by Eugene Charniak

© 2018 The Massachusetts Institute of Technology.

Simplified Chinese translation copyright © 2020 by Posts & Telecom Press.

This edition published by arrangement with MIT Press through Bardon-Chinese Media Agency.

All rights reserved.

本书简体中文翻译版由Bardon-Chinese Media Agency代理MIT Press授权人民邮电出版社独家出版发行。未经出版者书面许可,不得以任何方式复制或节录本书中的任何内容。

版权所有,侵权必究。


本书讲述了前馈神经网络、Tensorflow、卷积神经网络、词嵌入与循环神经网络、序列到序列学习、深度强化学习、无监督神经网络模型等深度学习领域的基本概念和技术,通过一系列的编程任务,向读者介绍了热门的人工智能应用,包括计算机视觉和自然语言处理等。

本书要求读者熟悉线性代数、多元微积分、概率论与数理统计知识,另外需要读者了解Python编程。

本书编写简明扼要,理论联系实践,每一章都包含习题以及补充阅读的参考文献。本书既可作为高校人工智能课程的教学用书,也可供从业者入门参考。

谨以此书,感恩家人


人民邮电出版社邀请我为Eugene Charniak的《深度学习导论》中文版作序。坦率地说,我认为自己并非最佳人选,因为虽然我早年做过神经网络相关的工作,但是近年一直从事的是知识图谱研究,已经远离神经网络研究前线十来年了。不过,Charniak从学术上算是我的“祖父”——他是我在伦斯勒理工学院(RPI)的导师James Hendler的导师。从学术传承的角度,梳理一下近三十多年发生的一些事情,倒是我一直想做的事。另外,也许正是因为我现在是“局外人”,也经历过神经网络的上一次低谷,或许可以提供一些其他的观察角度。借这次机会,我认真通读了Charniak的书,把我的笔记写在这里,权且作为序。

Charniak是布朗大学的教授,生于1946年,从20世纪60年代末起就从事自然语言处理(NLP)有关的研究。人类的语言是一种非常复杂的处理对象,语言处理涉及规则、统计、常识、语言学、知识系统等非常多的领域,Charniak的学术谱系恰好涵盖了上述领域的方方面面。

在长达半个世纪的研究生涯中,Charniak曾做出非常多的开创性工作,2011年美国计算语言学会(ACL)为他颁发了终身成就奖。他最广为人知的是“查尔尼克解析器”(Charniak parser),一个依存文法解析器,相关论文获2015年AAAI经典论文奖。他是把统计方法运用于自然语言处理的先驱之一,在“概率上下文无关文法”(PCFG)上做了大量有深远影响的工作。此外,他在机器翻译、机器问答、知识表示等问题上也涉猎广泛。

和很多大学教授不同,Charniak一直保持着亲自动手写程序的习惯。他是一个极为重视通过实践来检验理论的人,所以他的这本书,才能通过简明清晰的代码一步步引导读者接触复杂的概念,而不是把读者淹没在公式推导或者代码接口的细节里。通过实践抓住本质,可能是Charniak学术风格的鲜明特点,也是本书(以及他之前写的其他4本教科书)的突出优点。

Charniak攻读博士学位时的导师是麻省理工学院(MIT)的Marvin Minsky(1972,毕业年份,下同)。Minsky是人工智能领域的创始人之一,贡献广泛,是1969年图灵奖得主。也是在1969年,Minsky出版了《感知器》(Perceptrons)这本书,为第一次神经网络高潮画上了句号(这件事后面还要详细说)。其实早在1956年“人工智能”(Artificial Intelligence)这个词被造出来之前,Minsky已经在做神经网络相关的工作了,所以他的批评是非常有分量的。

顺带说一句,Minsky攻读博士学位时的导师是普林斯顿大学的Albert Tucker(1954)。Tucker是大数学家(Tucker再往上的学术谱系都是数学家、哲学家了),是规划理论、博弈论的“大拿”,“囚徒困境”概念的提出者。后来获得了诺贝尔奖的John Nash也是他的学生。后来Minsky、Charniak、Hendler也都研究过规划理论。在人工智能历史上,不同分支的理论往往被相互借鉴,这些大宗师们也得益于不局限于某一狭隘视角。

之后在20世纪70年代,Charniak和Minsky工作的时候,Minsky发明了一种知识表示方法——“框架”(Frame)理论,Charniak在早年也从事过框架理论的研究。后来James Hendler在布朗大学师从Charniak攻读博士学位(1985),继承了知识表示这个方向的工作,主攻主体(agent)理论、规划和知识推理方面。到了20世纪90年代,Hendler和Tim Berners-Lee等人开创了语义网(Semantic Web)这个领域,其演化到今天被称为“知识图谱”(Knowledge Graph)。

其实Charniak和Hendler的工作都是关于知识的,只是侧重点不同:Charniak主攻用统计方法理解文本里的知识(“经验主义”),Hendler主攻如何获取人的头脑里的知识(“理性主义”或者“符号主义”)。两者都可以追溯到Minsky的一些奠基性工作,但在人工智能的发展史上,两种技术路线又相互竞争,一种的高潮往往是另一种的低谷,追溯一下这些高潮和低谷的发展历程可能也有助于我们理解未来。当然在这个序里,我们的讨论会主要聚焦在神经网络和深度学习这个分支,其他方法只在必要的时候提及。

神经网络的发展,迄今经历了三个周期,包括三次高潮和两次低谷。

以上的周期年份,主要是对于美国的学术界而言,而在中国,以前则会滞后几年,不过在最近一个周期里,中美两国已经基本同步发展了。

我最早接触神经网络是在1996年。那时候虽然在美国神经网络已经进入了冬天,但是在20世纪90年代的中国计算机界,神经网络依然是一门“显学”,虽然不能说言必称神经网络,但相信神经网络是一种“万灵药”的想法还是非常普遍的。我当时的同学,有学土木的、机械的、电气的、仪器的,听说我是做神经网络的,都跑过来要合作用神经网络发论文,大体也行得通。我们拿它来做电力负载预测、桥梁结构优化,效果都是非常好的。

到20世纪90年代末,发现BP网络、Hopfield网络有这样那样的问题,那时候便有一个想法,为什么不能进一步利用网络的层次性,做一种层次化学习的神经网络呢?2001年,我带着这个想法去Iowa State University向Vasant Honavar学习人工智能。Honavar也是神经网络专家,那时他在这个领域研究了十几年了,但是他和我说,研究神经网络是再也得不到资助了,你必须换一个方向。

后来也的确是这样。那时候即使是做神经网络的人,也必须套着其他的“马甲”发表论文。我记得在Geoffrey Hinton他们发表那篇经典论文“A fast learning algorithm for deep belief nets”的2006年,ICDM、ICML这些机器学习的主流会议上,几乎没有关于神经网络的文章。“ICML不应该接受关于神经网络的文章”还是一种潜规则。Hinton他们的这篇文章,现在回过头来看预示了神经网络的复兴,但是文章名字也没有直接提到神经网络,而用了“belief nets”这样“安全”的名字——这能让人去联想贝叶斯或者概率图,当时的显学。

当时神经网络衰到什么程度呢?去翻一下那年的“神经网络大本营”NIPS 2006的论文集,大部分文章都不是关于神经网络的了,而是贝叶斯、马尔可夫网络和支持向量机(SVM,神经网络的“对头”)之类的。甚至在NIPS 2012,Hinton都用“自黑”开场:“我今天想告诉大家,其实过去这些年大家没有必要来参加NIPS。”(戏指过去这些年没啥进展)。真是让人唏嘘,“一个人的命运当然要靠自我奋斗,但是也要考虑历史的进程”。

之所以有这样的低谷,来自于之前极高的期待。神经网络的发展史上,反复出现“极高的期待”—“极度的怀疑”这种震荡。比如下面两段话分别出现在前两次高潮时期的媒体上:

海军披露了一台尚处初期的电子计算机,期待这台电子计算机能行走、谈话、看和写,自己复制出自身存在意识……Rosenblatt博士,康奈尔航空实验室的一位心理学家说,感知机能作为机械太空探险者被发射到行星上。(《纽约时报》,1958)

现在已经可以采购到神经网络程序,可用于预测标普500的动向,或者诊断心脏病。诺贝尔奖得主Leon Cooper说,这种技术最终会比电话里的晶体管还普遍。DARPA的Jasper Lupo认为,我相信这种技术比原子弹更重要。(《科学家》,1988)

现在第三次高潮正在进行中,类似的话近年在媒体上天天有,就不必在这里举例了。

在高潮期,不管啥都要蹭神经网络的热点,比如在1991年(第二个高潮的巅峰)《终结者2》电影中,施瓦辛格扮演的“终结者”机器人也说:“我的CPU是一个神经网络处理器,一个会学习的计算机。”(My CPU is a neural-net processor...a learning computer.)那时候没人能想到,仅仅4年之后这个领域就凉了,神经网络遇到了自己的“终结者”。

这几年关于神经网络起起伏伏的历史的文章已经很多,这里不打算复述这段历史的细节。感兴趣的读者可以参考尼克的《人工智能简史》一书中“神经网络简史”一章,和Andrey Kurenkov的“A Brief History of Neural Nets and Deep Learning”(神经网络和深度学习简史)。我们仅把讨论限于核心方法衰落与兴起的技术原因。

1969年Minsky(和Seymour Papert)在《感知器》一书里给感知器判了“死刑”。感知器的具体技术细节,请参考本书第1章“前馈神经网络”。Minsky的逻辑是:

(1)一层感知器只能解决线性问题;

(2)要解决非线性问题(包括分段线性问题),比如异或(XOR)问题,我们需要多层感知器(MLP);

(3)但是,我们没有MLP可用的训练算法。

所以,神经网络是不够实用的。这是一本非常严谨的专著,影响力很大。一般的读者未必能理解书中的推理及其前提限制,可能就会得到一个简单的结论:神经网络都是骗人的。

这并不意味着Minsky本人看衰人工智能领域,实际上1967年他说:“一代人内……创建人工智能的问题就会被事实上解决掉。”这里可能还有另外一个因素:在那个时候,他是很看好与神经网络竞争的“符号主义”和“行为主义”的方法的,比如框架方法、微世界方法等,他后面也转向心智与主体理论的研究,所以《感知器》这本书观点的形成可能也有路线之间竞争的因素。但很不幸的是,无论是1967年他对AI过于乐观的展望,还是1969年他(事后看)对连接主义方法过于悲观的判断,都对1973年AI进入全面的冬天起到了推波助澜的作用。这是“极高的期待导致极度的怀疑”的第一次案例——当然并不是最后一次。

说1969年《感知器》的观点事后看过于悲观,是因为在Minsky写这本书的时候,问题的答案——误差反向传播(BP)算法——其实已经出现了,虽然直到1974年Paul Werbos在博士论文中才把它引入了神经网络。只是要再等十几年,这个算法才被几个小组再次独立发现并广为人知。无独有偶,1995年前后神经网络再次进入低谷的时候,后来深度学习的那些雏形在20世纪80年代末其实已经出现了,也同样需要再花二十年才能被主流认知。

不管是不是合理,神经网络与它的其他AI难兄难弟一起,进入了一个漫长的冬天。当时研究经费的主要来源是政府,但这之后十几年几乎没有政府资助再投入神经网络。20世纪70年代到80年代初,AI退守的阵地主要是“符号主义”的专家系统。

1986年,David Rumelhart、Geoffrey Hinton和Ronald Williams发表了著名的文章“Learning representations by back-propagating errors”(通过误差反向传播进行表示学习),回应了Minsky在1969年发出的挑战。尽管不是唯一得到这个发现的小组(其他人包括Parker,1985;LeCun,1985),但是这篇文章本身得益于其清晰的描述,开启了神经网络新一轮的高潮。

BP算法是基于一种“简单”的思路:不是(如感知器那样)用误差本身去调整权重,而是用误差的导数梯度)。具体的算法,请参考本书第1章和第2章。

如果我们有多层神经元(如非线性划分问题要求的),那只要逐层地做误差的“反向传播”,一层层求导,就可以把误差按权重“分配”到不同的连接上,这也即链式求导。为了能链式求导,神经元的输出要采用可微分的函数,如s形函数(sigmoid)。

在20世纪80年代的时候,一批新的生力军——物理学家也加入了神经网络的研究阵地,如John Hopfield、Hermann Haken等。在计算机科学家已经不怎么搞神经网络的20世纪80年代早期,这些物理学家反而更有热情。与第一周期中常见的生物学背景的科学家不同,物理学家给这些数学方法带来了新的物理学风格的解释,如“能量”“势函数”“吸引子”“吸引域”等。对于上述链式求导的梯度下降算法,物理学的解释是在一个误差构成的“能量函数”地形图上,我们沿着山坡最陡峭的路线下行,直到达到一个稳定的极小值,也即“收敛”点。

1989年,George Cybenko证明了“万能近似定理”(universal approximation theorem),从表达力的角度证明了,多层前馈网络可以近似任意函数(此处表述忽略了一些严谨的前提细节)。进一步的理论工作证明了,多层感知器是图灵完备的,即表达力和图灵机等价。这就从根本上消除了Minsky对神经网络表达力的质疑。后续的工作甚至表明,假如允许网络的权重是所谓“不可计算实数”的话,多层前馈网络还可以成为“超图灵机”——虽然这没有现实工程意义,不过足以说明神经网络强大的表达力。

BP算法大获成功,引起了人们对“连接主义”方法的极大兴趣。数以百计的新模型被提出来,比如Hopfield网络、自组织特征映射(SOM)网络、双向联想记忆(BAM)、卷积神经网络、循环神经网络、玻尔兹曼机等。物理学家也带来了很多新方法和新概念,如协同学、模拟退火、随机场、平均场和各种从统计物理学中借鉴过来的概念。其实后来深度学习复兴时代的很多算法,都是在那时候就已经被提出来了。

回看20世纪80年代,你也许会发现今天探索过的很多想法当时都探索过,诸如自动控制、股市预测、癌症诊断、支票识别、蛋白质分类、飞机识别,以及非常多的军事应用等,都有成功的案例——这是20世纪60年代那一波未曾见的。因为有了这些可商业落地的应用,大量风险投资也加入进来,从而摆脱了单纯依靠政府资助发展的模式。

可以说,在那个时代,神经网络已经是“大数据”驱动的了。相比美好的承诺,新一代神经网络速度慢的缺点(这来自于大量的求导计算)也就不算什么了。而且出现了大量用硬件加速的神经网络——正如今天专用于深度学习的“AI芯片”。大量的公司去设计并行计算的神经网络,IBM、TI都推出了并行神经计算机,还有ANZA、Odyssey、Delta等神经计算协处理器,基于光计算的光学神经网络,等等。甚至Minsky本人都创办了一家并行计算人工智能公司“Thinking Machines”,产品名也充满暗示地叫“连接机”(蹭“连接主义”的名气)。和今天一样,也几乎每天都有头条,每一天都看起来更加激动人心,眼前的困难都可以被克服。

短短几年之内,极度的怀疑反转为(又一次的)极高的期待,以至于在之前引用的《科学家》1988年文章“神经网络初创企业在美国激增”中也表达了对这种期待的担心:

神经网络在金融领域如此之热,以至于有些科学家担心人们会上当。斯坦福大学教授、有三十年神经网络经验的Bernard Widrow说:“一些商业神经网络公司的信誓旦旦可能会把这个行业带入另一个黑暗时代。”

Widrow也是在Minsky的影响下进入AI领域的,后来加入斯坦福大学任教。他在1960年提出了自适应线性单元(Adaline),一种和感知器类似的单层神经网络,用求导数方法来调整权重,所以说有“三十年神经网络经验”并不为过。不过,当时他认为神经网络乃至整个人工智能领域风险有点高,于是他转向了更稳妥的自适应滤波和自适应模式识别研究。

顺便说一句,自适应滤波的很多方法在数学上和神经网络方法是相通的,甚至只是换了个名字,比如Widrow著名的“最小均方误差”(LMS)方法在后来的神经网络研究中也广为应用。我们在神经网络的起起伏伏中经常看到这样的现象(后面还会举更多的例子):

回到1986—1995年这段时间,什么都要和神经网络沾边才好发表。比如,那时候CNN不是指卷积神经网络(Convolutional Neural Network,见本书第3章),而是细胞神经网络(Cellular Neural Network)——一种并行硬件实现的细胞自动机,尽管这种算法本来和神经网络没有太大关系。顺便提一句,它的发明人是“虎妈”(蔡美儿)的父亲蔡少棠。

神经网络从“飞龙在天”到“亢龙有悔”,也只花了几年时间,就又遇到了“第二代终结者”。有趣的是,第二代终结者的出现本身又是为了解决第一代终结者问题而导致的。

异或问题本质上是线性不可分问题。为了解决这个问题,在网络里引入非线性,以及将这些非线性函数组合的参数化学习方法(BP算法等)。但是这样复杂的高维非线性模型,在计算上遇到了很多挑战,基本上都是和链式求导的梯度算法相关的。

首先就是“慢”。训练一个规模不算很大的神经网络花上几天时间是很正常的,在中国就更艰苦了。1998年在读研究生时我得到的第一台计算机是一台“486”,在那上面运行MATLAB的神经网络程序,隐藏层节点都不敢超过20个。为什么这么慢呢?全连接的前馈网络,参数空间维数大幅增加,导致了维度灾难(The Curse of Dimensionality),参数组合的数量呈指数增长,而预测的精度与空间维数的增加反向相关,在20世纪90年代有限的算力支持下,规模稍大的问题就解决不了了。

“万能近似定理”虽然说明了我们可以逼近任意函数,但是并不保证有一个训练算法能够学习到这个函数。虽然后来我们知道,同样的神经元数量,多隐层会比单隐层收敛得更快(虽然单隐层和多隐层在表达力上对于连续函数没区别),但是那时候由于不能解决“梯度消失”的问题(后面还会讲到),很少人会用多隐层。所以,神经网络内在的结构性是不好的。那时候也有很多“打补丁”的方法,比如,通过进化神经网络来寻找最优节点结构,或者自适应步长迭代,等等,但事后看,都是些治标不治本的方法。

维度灾难的另一个后果是泛化问题。比如训练一个手写数字识别器,稍微变化一下图像可能就识别不了了。这个问题的原因是误差求导是在一个高维空间里,目标函数是一个多“峰值”和“谷底”的非线性函数,这就导致了梯度下降迭代终点(“吸引子”)往往不一定是希望找到的结果(全局最优解)。甚至,有些迭代终点根本不是任何目标模式,称为“伪模式”或者“伪状态”。

Hinton在2015年的一个教程里也总结了基于BP的前馈网络的问题。

(1)数据:带标签的数据集很小,只有现在(2015)的千分之一。

(2)算力:计算性能很低,只有现在(2015)的百万分之一。

(3)算法:权重的初始化方式和非线性模型错误。

后来,数据问题和算力问题被时间解决了,而算法问题早在2006年前后就被解决了(即深度学习革命)。

回到1995年,那时大家并没有Hinton在20年后的这些洞见,但是也能意识到神经网络的这些问题很难解决。再一次,“极高的期待导致极度的怀疑”,未能兑现的承诺导致了资金的快速撤离和学术热情的大幅下降。几乎所有的神经网络公司都关门了——至少有300家AI公司,包括Minsky的Thinking Machines(1994)也关门了。

这时候恰好出现了基于统计机器学习的其他竞争方法,导致大家逐渐抛弃了神经网络而转向统计机器学习,如支持向量机(SVM)、条件随机场(CRF)、逻辑回归(LR回归)等。其实这些方法也都和神经网络有千丝万缕的联系,可以证明与某些特定的网络等价,但是相对简单、快速,加上出现了一些成熟的工具,到20世纪90年代后期在美国就成为主流了。

这里只对SVM做一下分析。1963年SVM刚出现的时候,和单层感知器一样,都只能处理线性分类问题。两者后来能处理非线性问题,本质都是对原始的数据进行了一个空间变换,使其可以被线性分类,这样就又可以用线性分类器了,只是两者对如何做空间变换途径不同:

只是,SVM以牺牲了一点表达力灵活性(通常核函数不是任意的)为代价,换来了实践上的诸多方便。而神经网络在之后的17年里,逐渐从“主流”学术界消失了,直到跌到了“鄙视链”的最下面。据说Hinton从20世纪90年代到2006年大部分投稿都被会议拒掉,因为数学(相比统计机器学习)看起来不够“fancy”(新潮)。

20世纪90年代中期到2010年左右在整体上被认为是第二个AI冬天,神经网络无疑是其中最冷的一个分支。值得一提的是,这段时间内互联网兴起,连带导致机器学习方法和语义网方法的兴起,算是这个寒冬里两个小的局部春天。不过在这个神经网络“潜龙勿用”的第二个蛰伏期,有些学者依然顽强坚持着,转机又在慢慢酝酿。

Geoffrey Hinton、Yoshua Bengio和Yann LeCun获得2018年图灵奖是众望所归。在那漫长的神经网络的第二个冬天里,正是他们的坚持才迎来了第三周期的复兴,而且势头远远大于前面两次。其中,Hinton是1986年和2006年两次里程碑式论文的作者,也是BP算法和玻尔兹曼机的提出者;Bengio在词嵌入与注意力机制(见本书第4章)、生成式对抗网络(GAN,见本书7.4节)、序列概率模型上有贡献;LeCun独立发现并改进了BP算法,发明了卷积神经网络(见本书第3章)。

神经网络之所以翻身了,关键还是在一些经典的难问题上确实展示了实用性,把一些停滞了很久的问题向前推进了。先是体现在手写字符识别MNIST(2006)上,然后是在语音识别(2010)和图像分类ImageNet(2012)上。ImageNet ILSVRC 2012竞赛是神经网络方法第三次兴起的标志性事件。国内同步就有了报道,余凯当天在微博上说:

Deep learning令人吃惊!最近第三届ImageNet Challenge上,Hinton团队获得第一,Hit Rate@Top5 =84%,比第二名高出10%!第一届比赛,我带领的NEC团队获得第一,成绩是72%。去年第二届,Xerox Lab获得第一,但结果和我们前年的差不多,无实质进步。今年可是飞跃了。

之后另一个标志性事件是2016年AlphaGo击败围棋世界冠军李世石,背后基于深度强化学习方法(本书第5章)。这件事有极大的公众宣传效果,激发了一轮深度学习风险投资的狂潮。在机器阅读理解竞赛SQuAD上,自2018年以来,序列到序列学习模型(本书第6章)也取得了与人类匹敌的成绩。

效果是反驳一切怀疑的最好的武器。

为什么深度学习能战胜“第二代终结者”,取得这么好的成绩呢?我想从算法细节、算法哲学、工程成本三个角度谈一些个人看法。

1.从算法细节的角度分析

前面我们提到链式求导带来一系列问题。单隐层全连接造成收敛速度不够快,但是由于“梯度消失”(或对偶的“梯度爆炸”)问题,难以实现多隐层误差反向传播。而且,网络还有泛化能力不好、容易过拟合等问题。

它的解决方法其实并不复杂。首先,用分段线性函数ReLU: f(x)=max(0,x)取代sigmoid激活函数——这个函数甚至不是严格可微的。线性保证了它的导数不会趋近于零,分段线性则保证了我们可以分段逼近一个函数,尽管从理论上这个逼近不平滑,但是工程上够用。

实践表明,ReLU函数在训练多层神经网络时,更容易收敛,并且预测性能更好。这不是从理论推导出来的结果,而是有了实践之后,反过来总结出来的。我们发现单侧抑制一些神经元(ReLU的实际作用)会导致“表征稀疏”,而这反而是好事,既让表示更具有鲁棒性,又提高了计算效率。

这种丢弃信息反而提高效果的工程实践在深度学习的其他一些细节也有体现。比如,“丢弃”算法(dropout)通过每次训练让部分神经元“装死”来避免过拟合,卷积神经网络中引入“池化”(pooling)丢弃一些输入信息反而会指数级减小泛化误差。

以上种种工程技巧,基本原理并不复杂,一旦捅破窗户纸,不免给人“原来如此简单”的感觉。与当初战胜“第一代终结者”也颇有类似之处,就是并非依赖一个高深莫测的新理论,而是依赖一些朴素的“常识”,去从工程上想办法。

那为什么这些看似简单的方法,要过十几年才被接受呢?大概是因为学术界的遗忘周期是15年吧!三代博士过后大家基本就不记得从前了。正所谓“人心中的成见是一座大山”,直到连成见都被遗忘了,才会有新的开始。

2.从算法哲学的角度来分析

总的来说,神经网络的演进一直沿着“模块化+层次化”的方向,不断把多个承担相对简单任务的模块组合起来。BP网络是感知器的层次化,深度学习网络则是多个BP网络的层次化——当然后来也出现了多种非BP网络的深度层次化。Hinton最近提出的“胶囊”(capsule)网络就是要进一步模块化。层次化并不仅仅是网络的拓扑叠加,更重要的是学习算法的升级,例如,仅仅简单地加深层次会导致BP网络的梯度消失问题。

从本质上说,深度学习网络可以比经典的BP网络处理更复杂的任务,在于它的模块性,使得它可以对复杂问题“分而治之”(Divide and Conquer)。无论是多层前馈网络,还是循环神经网络,都体现了这种模块性。因为我们处理的问题(图像、语音、文字)往往都有天然的模块性,学习网络的模块性若匹配了问题本身内在的模块性,就能取得较好的效果。

这可以看成一种连接主义的“动态规划”,把原来全连接网络的训练这种单一决策过程,变成了多阶段决策过程。例如,在多层卷积网络对图像的处理中,会出现不同的层次依次“抽取”出了从基础特征到高层次模式的现象,每一层基于上一层的输入,就相当于很多子任务可以被重用了。所以这种方法也被称为表示学习(representation learning)方法。

这样的好处是多方面的,既极大提高了学习收敛的速度(解决了维度灾难),又可避免那些“不合理”的局部最优解(因为它们在模块性匹配的过程中被自然淘汰了)。

从这个角度去理解,深度神经网络是“优雅”的,在于它简洁而美。一个“好”的模型,通常是“优雅”的。这很难说是什么科学道理,但是就和物理学一样,一个计算机科学的算法,如果它是技术主干道上的一个有深远价值的东西,往往它是“美”的,简洁的,没有太多补丁。一个糟糕的算法,就好像托勒密的“本轮”,一个补丁套一个补丁,或者像在发明抗生素之前治疗肺结核的方法,神秘而不可解释。如之前给BP网络和Hopfield网络打各种补丁的方法,前置各种ad-hoc不变形特征提取器,用进化算法训练网络结构,用局部定位消除虚假吸引子,等等,数学上都高深莫测,但是效果并不好。现在回头看,那些模型都很“丑”。

深度学习把学习分层,不是个数学问题,而是个知识重用问题,每一层自然分解出不同等级的特征,从底层特征到高层特征。这样一下子就把原来打几千种补丁的必要性都消灭了。这个架构是优雅的,也同时解决了收敛速度问题和泛化问题,因为它触及了问题的本质。一个优雅的方法,基本的原理往往是特别好懂的,不用看公式就能懂。

这里多说一句,深度学习模型现在大火的同时,也出现了很多对它的“本轮”补丁,如一些几百层的神经网络模型。搞得这么复杂的模型,通常在技术演进上是旁支。

3.从工程成本角度分析

深度学习的成功,工具系统的可用性是很关键的因素。工具大大降低了运用这些方法的门槛。深度学习被采用,并不一定是因为它效果最好——许多场合可能就和传统方法的最好水平差不多。但是,发挥传统方法的最好水平需要一位有多年经验的“老中医”,而深度学习工具可以让一个刚出道的学生就达到相近或稍差的表现,在语音和图像场景上更可以超出传统方法。这从管理学和经济学上都带来了巨大的好处。

例如2006年Netflix推荐算法大赛,冠军团队利用集成算法,整合了107种算法,最后提高了10个百分点。而2016年,有人用Keras写了一段不到20行的深度神经网络程序就得到了类似的结果。又如基于深度学习的依存文法解析器senna和传统的Stanford parser相比,效果接近,略差一点,但是从建模复杂性上,senna就远远比Stanford parser简单了,senna只用了一个零头的代码量就达到了接近的效果。

以前需要“老中医”来做特征工程,现在交给深度学习来进行表示学习(representation learning),通过深度神经网络中的逐层加工,逐渐将低层的特征表示转化为高层的特征表示。

同样,以前也需要“老中医”来对核函数(kernel)、卷积模板(mask)等强烈依赖经验的计算单元进行选择或者构造,这限制了可能的学习种类。深度学习网络相当于可以从数据中学习kernel或者mask,大大提高了灵活性,降低了对经验的依赖。

又如,在深度学习中广泛采用预训练模型(如最近很火的BERT)。这个想法的本质是知识重用。可复用的预训练模型作为“工作母机”,可以被后续的工程再去针对特定的任务修正和调优。

综上所述,大量深度学习工具的出现,大大降低了神经网络的入门门槛,大大增加了神经网络工程师的供给总量,大大降低了领域专家介入成本,从而有利于控制工程总成本。

不过,现实的问题求解并不是单一工序。任何一个实际问题的解决,都需要工程上的细致的问题分解,并不总是存在“端到端”的方法,多种工具的组合运用是工程不可或缺的。随着深度学习的普及,最近几年毕业的学生,很多甚至不知道深度学习之外的方法了,连传统机器学习都丢掉了,更不用说规则方法了,这对实际解决问题将是有害的。

深度学习如今进入了本轮高潮的第7个年头,正如日中天,在前所未有的海量资金投入时,讨论是不是会有什么因素导致本轮高潮的结束似乎是杞人忧天。有人认为,这一次的神经网络复兴将是最后一次,因为神经网络将不可能再次进入低谷。不过,“一切伟大的世界历史事变……可以说都出现两次”。“极高的期待导致极度的怀疑”这件事已经发生两次了,如今正处在第三次“极高的期待”中,很多名人又开始担心人工智能威胁人类了。为了这一领域的健康发展,我们也应该审视深度学习是不是有其自身的边界,并提前想一想对这些边界的应对。

如之前的分析,第二代终结者问题(链式求导的副作用)恰恰是为了解决第一代终结者问题(非线性分类)而带来的新问题。那终结第三次神经网络高潮(深度学习)的会不会也是为了解决第二代终结者问题而导致的新问题呢?

暴力美学问题。当我们加深网络层次并引入模块性的时候,会带来什么副作用呢?现在深度学习反而变得越来越贵,层数越来越多,预训练模型也越来越昂贵,深度学习在很多场景下反而变成了“暴力美学”,成为拼数据、拼GPU的烧钱游戏。但是,其实去非巨头的企业走走就会知道,大多数的领域落地问题,还不能承担这种成本,尤其是很多机构组织的问题解决,必须从低成本小问题开始。“暴力美学”式的深度学习,就只能停留在“头部问题”(即存在大量数据和大量算力的问题)上,而难以解决大多数垂直领域问题。

炼丹问题。深度学习的结果越来越难以解释和定向优化,整个系统是个“炼丹”的黑箱。当然,这个问题不是深度学习独有的,是整个“连接主义”方法共同的问题。只是深度学习把这种“炼丹”推到了一个全新的高度,调参的效果往往不可理解,没法解释。但是非常多的应用问题,如医疗和自动驾驶,的确是需要可解释性和定向优化的,这就限制了应用的效果。

递归性序列问题。黑箱问题本身可能还不是致命的,但是它又带来了另一个问题:一些在人看起来很清晰的问题,基于海量的训练数据机器还是学习不好。这类问题通常是一种“递归性生成规则”,最简单的如数字的构成规则,基于这些规则可能生成无穷无尽的序列。基于纯语料对齐技术训练,就很难得到不出错的中英文数字翻译。类似的递归性序列不仅在语言中大量存在,在表格、篇章等结构中也广泛存在。深度学习到底能不能在工程上解决这类语法归纳(grammar induction)问题,还是个待实践的问题。

知识融合问题。这个问题也是近来学术界关注的热点。如何把先验知识或者“知识图谱”(即数据本身的结构性)融合进深度神经网络?各类的向量化方法被提出,语义并不依赖于把符号直接映射到模型世界的个体上,而取决于个体的统计特性。但是除了词向量,其他更复杂的知识结构(例如属性、二元关系和表达式)在工程上依然鲜有成功。在自然语言处理中,外源知识恐怕是难以避免的,目前的向量化方法,似乎还不足以独立完成这个 任务。

深度学习的这些问题(潜在的“第三代终结者”问题),也同样是难以仅仅用拓扑的改良来解决的,例如增加神经网络层数或者再提升数据的量级。可能需要我们进一步提出更先进的网络结构,或者融合其他的AI工具,而不仅是“打补丁”(和二十多年前一样)。读者可能也会得出自己的“终结者问题”。思考这些问题,并不意味着我们否定深度学习,而是有助于我们进一步拓宽思路。也许,如之前的两次复兴一样,答案并不复杂,需要的仅仅是从常识出发,去发现工程的技巧。

下面两句话都是Minsky说的。

1970:“三到八年后,我们就会有一个机器,达到普通人类的智能水平。”——三年之后AI进入冬天。

2003:“1970年代以来,AI就脑死亡了。”——三年之后Hinton悄然举起复兴的大旗。

可见,即使是AI创始人自己,对未来的预测,也常常是错误的。

从某种程度上说,也许我们不可能走出周期律,它会一直陪伴着这个学科。因为人工智能可能和所有其他的计算机分支都不一样,她会一次又一次让我们着迷,因为爱而被冲昏头脑,又因为爱而对她生恨。这恰恰是她的魅力所在。

神经网络(包括深度学习)是最好的第一种算法和最后一种算法。当你对一个问题一无所知,请用神经网络。当一个问题已经被明确可解,神经网络总是可以帮你达到已知的最优结果。在两者之间,神经网络和其他算法一样优秀或者糟糕。神经网络模型在实践有效后通常都会经历简化,甚至部分“白箱”化。这在工程上是几乎一定会发生的。总之,它是一个“最不坏的选择”。

我们可以做到的是实事求是,一切从实践出发,一切从工程出发,去理解约束,理解落地细节,抓住本质。我们也会经历知其然、知其所以然,到知未然的认识深化过程。对神经网络这样一种颇为复杂的工具,比较和实践都是不可或缺的。

Charniak的这本书就是可以帮助您达到这一点的优秀参考书。它是一本实事求是的教材,它也是一本以工程为导向的指南,以Python和Tensorflow为实践工具,可以带你以清晰的逻辑进入实战,去领略基础的深度学习算法如CNN、RNN、LSTM、GAN等。这本书只是一个开始,正如本文前面所述,神经网络是一个有深厚历史渊源的学科,在未来还有很多其他的进阶话题等着对此有兴趣的读者去探索。

我相信您会和我一样,愉快地阅读本书,并获得思考的乐趣。

文因互联 鲍捷

2019年9月于北京


笔者长期进行人工智能方面的研究,主要研究方向为自然语言处理,但深度学习的出现为这个领域带来了一场全新的革命。只是对于这件事,我过了太久才有所顿悟,这是因为神经网络发展至今,已历经三次浪潮,而第三次才是一场真正意义上的革命。我突然发现自己已远落后于时代,而要紧跟时代步伐又十分艰难。所以,我只是做了一个教授应该做的事:为促进自学,要求自己教授该领域课程,参加线上速成班,并向学生们不耻下问。最后一条可不是在开玩笑,尤其是我的学生Siddarth(Sidd)Karramcheti,作为该课程的本科助教组长,为我提供了很多帮助。

这些经历造就了这本书。首先,这本书的篇幅并不长,毕竟我学得很慢。其次,本书很大程度上采用项目驱动模式撰写。许多教材在计算机科学知识方面的讲解安排有些失衡,大多注重理论方面的讲解,而缺少对于特定项目的实例讲解。当然这两方面折中一下更好,不过对我而言,学习计算机科学的最好方法,就是坐下来写程序,所以本书很大程度上也反映了我的学习习惯。而最方便的方式就是把这些记下来,希望对一些预期受众有所帮助。

那么预期受众包括哪些读者呢?我当然希望这本书能够帮助到计算机科学领域的从业者,不过教师首先要服务学生,所以这本书主要是作为深度学习课程的教材。在布朗大学,我这门课是为研究生和本科生开设的,课程涵盖了本书所有内容,还添加了一些“文化”课程(研究生必须完成期终项目才能修满学分)。本课程需要一些线性代数和多元微积分的基础,虽然不需要精通线性代数,但学生们告诉我,如果你一点也不懂,那么理解多层网络和其所需的张量就非常困难。但在这之前,我们也要用到多元微积分。事实上,它只会明确地出现在第1章,当我们从头开始建立反向传播时,举办一次偏导数的讲座是有必要的。此外,概率论与数理统计也是这门课的先修课程,有助于简化一些复杂的理论解释,我非常鼓励学生选修这样的课程。另外,这门课还需要一些Python的基本编程知识,尽管书中未涉及,但在课程中有单独的Python基础实验课。

笔者一边写书,一边学习。因此,读者会发现每一章的拓展阅读部分,不仅参考了一些重要的研究论文,还参考了许多的二次文献—其他人的教育著作。如果没有它们,我不可能学到这些。

美国罗德岛州普罗维登斯市

2018年1月


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可,如下图所示。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。

如果您是学校、培训机构或企业用户,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、人工智能、软件测试、前端、网络技术等。

异步社区

微信服务号


对深度学习(或称神经网络)的探索通常从它在计算机视觉中的应用入手。计算机视觉属于人工智能领域,因深度学习技术而不断革新,并且计算机视觉的基础(光强度)是用实数来表示的,处理实数正是神经网络所擅长的。

以如何识别0到9的手写数字举例。如果从头解决这个问题,首先我们需要制作一个镜头来聚焦光线以成像,然后通过光传感器将光线转换成计算机可“感知”的电子脉冲,最后,由于使用的是数字计算机,需要将图片离散化,也就是说,将颜色和光强度通过二维数组表示出来。幸运的是,我们的在线数据集Mnist(发音为“em-nist”)已经帮我们完成了图片离散化工作。(这里的“nist”是指美国国家标准技术研究所,此数据集由nist提供。)如图1.1所示,每张图片可用28×28的整数数组表示。为了适应页面,已删除左右边界区域。

图1.1 Mnist中离散化的图片数据

图1.1中,0表示白色,255表示黑色,介于两者之间的数字表示灰色。这些数字被称为像素值,其中像素是计算机可处理图片的最小单位。像素所能展示的实际世界的“大小”取决于镜头,以及镜头与物体表面的距离等因素。但在这个例子里,我们不用考虑这些因素。图1.1中对应的黑白图片如图1.2所示。

图1.2 图1.1像素得出的黑白图片

仔细观察这张图片,我们会发现可以用一些简单的方法解决本例中的问题。例如,已知图片是数字“7”,则(8,8)处的像素点是暗的。同理,数字“7”的中间部分为白色,比如,(13,13)处的像素值为0。数字“1”则不同,由于数字“1”的标准书写不占用左上角位置,而是在正中间部分,所以这两处的像素值与数字“7”相反。稍加思考,我们就能想出许多启发式规则(大部分情况下是有效的),并利用这些规则编写分类程序。

然而,这不是我们要做的。本书的重点是机器学习,也就是通过给定的样本和对应的正确答案让计算机进行学习。在本例中,我们希望程序通过学习给出的样本和正确答案(或称标签),学会识别28×28像素图片中的数字。在机器学习中,这被称为监督学习问题,准确地说,是全监督学习问题,即每个学习样本都对应有正确答案。在后面的章节中,会出现没有正确答案的情况,如第6章中的半监督学习问题,第7章的无监督学习问题。在这些章节中我们会具体讲解它们的工作机制。

如果忽略处理光线和物体表面的细节问题,就只需解决分类问题,即给定一组输入(通常称为特征),将产生这些输入(或具有这些特征)的实体,识别(或分类)为有限类。在本例中,输入是像素,分为十类。定义有l个输入(像素)的向量为x = (x1, x2,…, xl),正确答案为a。通常输入的是实数,正负均可,在本例中,为正整数。

我们从一个更简单的问题入手,创建一个程序来判断图片中的数字是否为0。这是二分类问题,感知机是早期解决二分类问题的机器学习方法之一,如图1.3所示。

图1.3 感知机原理图

感知机是神经元的简单计算模型。单个神经元(见图1.4)通常由许多输入(树突)、一个细胞体和一个输出(轴突)组成。因此,感知机也有多个输入和一个输出。一个简单的感知机,在判断28×28像素图片中的数字是否为0时,需要有784个输入和1个输出,每个输入对应一个像素。为简化作图,图1.3中的感知机只有5个输入。

图1.4 一个典型的神经元

一个感知机由一个权重向量w = (w1,…, wl )和一个偏置项b组成,其中权重向量的每个权值对应一个输入。wb称为感知机的参数。通常,我们用Φ来表示参数集,ΦiΦΦi是第i个参数。对于感知机,Φ = {w U b}。

感知机利用这些参数计算下列函数。

(1.1)

即,将每个感知机输入乘其对应权重值,并加上偏置项,若结果大于0,返回1,否则返回0。感知机是二分类器,所以1表示x属于此类,0则表示不属于。

将长度为l的两个向量的点积定义为式(1.2)。

  (1.2)

因此,我们可以将感知机运算函数简化如下:

 

(1.3)

b+ w·x被称为线性单元,图1.3中,使用∑来标识。当涉及调整参数时,可将偏置项视为w中的一个权重,这个权重的特征值总是1。这样,只需讨论调整w的情况。

我们关注感知机,是因为感知机算法可以根据训练样本找到Φ,而且这种算法简单又鲁棒。我们用上标来区分样本,第k个样本的输入是xk = [x1k,…,xlk],对应的答案是ak。在感知机这种二分类器中,答案是1或0,表示是否属于该类。当分类有m类时,答案将是0到m−1的一个整数。

有时可以将机器学习描述为函数逼近问题。从这个角度来看,单个线性单元感知机定义了一类参数化的函数。而感知机权重的学习,就是挑选出该类中最逼近解的函数,作为“真”函数进行计算,当给定任何一组像素值时,就能正确地判断图片是否为数字0。

在机器学习研究中,假设我们至少有两组,当然最好是三组问题样本。第一组是训练集,它用于调整模型的参数。第二组是开发集(它也称为留出集或验证集),其在改进模型过程中用于测试模型。第三组是测试集,一旦模型固定了,(幸运的话)产生了良好的结果,我们就可以对测试集样本进行评估。测试集是为了防止在验证集上能够运行的程序在没见过的问题上不起作用。这些集合有时被称为语料库,比如“测试语料库”。我们使用的Mnist数据可以在网上获得,训练数据包括60,000幅图片及其对应的正确标签,验证集和测试集各有10,000幅图片和标签。

感知机算法的最大特点是,如果有一组参数值使感知机能够正确分类所有训练集,那么该算法一定会找到这组值。可惜的是对于大多数现实样本来说,并不存在这组参数值。但即使没有这组参数值,仍然有参数值可以使得样本识别正确率很高,就这一点来说,感知机的表现也极其出色。

该算法通过多次迭代训练集来调整参数以增加正确识别的样本数。如果训练时我们遍历了整个训练集发现不需要改变任何参数,那这组参数就是正确的,我们可以停止训练。然而,如果没有这组正确的参数,那么参数会一直改变。为了防止这种情况,我们在N次迭代后停止训练,其中N是程序员设置的系统参数。通常,N随着要学习的参数总数增加而增加。我们要谨慎区分参数Φ和其他与程序关联的参数,它们不属于Φ,例如训练集的迭代次数N,我们称后者为超参数。图1.5给出了该算法的伪代码,注意Δx一般是指“x的变化”。

图1.5中最关键的两行是2(a)i和2(a)ii,其中ak等于1或0,ak=1表示图片属于这一类,等于0则表示不属于。2(a)i行是指如果感知机的输出是正确的标签,就不用进行操作。2(a)ii行则指定了如何改变权重wi,如果我们在每一个参数wi上增加(akf (xk))xik,再次尝试这个样本,感知机误差会减少,甚至得出正确答案。

图1.5 感知机算法

我们需要尝试所有可能性,才能了解2(a)ii行的算法是如何增加正确率的。假设训练样本xk属于该类,这意味着它的标签ak = 1。如果我们分类错误,那f (xk )(感知机对第k个训练样本的输出)一定是0,所以( akf ( xk))= 1,且对于所有的i,Δwi = xik。因为所有像素值都≥0,所以算法会增加权重,下一次运算后f ( xk)会返回更大的值,这减少了误差。(读者练习:在样本实际不在类别里但感知机却认为它属于该类的情况下,使用这个公式达到减少误差目的。)

我们将偏置项b视为一个虚拟特征x0的权重,该特征值恒等于1,上述讨论仍能成立。

让我们举一个小例子。这里我们只查看(并调整)四个像素的权重,分别是像素(7,7)(左上角中心位置)、像素(7,14)(上部中心位置)、像素(14,7)和像素(4,14)。通常方便的做法是对像素值进行归一化,使其介于0和1之间。假设我们的图片中的数字是0,那么a = 1,而这四个位置的像素值分别是0.8、0.9、0.6和0。由于最初所有参数都为0,计算第一幅图片的f(x),运算w·x+b=0,得到f (x) = 0,所以我们的图片被错误分类,af (x) = 1。因此,对权重w7,7进行运算,结果变为(0 + 0.8×1) = 0.8。同样,接下来的两个权重w7,14w14,7变为0.9和0.6。而中心像素权重保持为0(因为那里的图像值为0),偏置项变为1.0。需要注意的是,如果我们第二次将同样的图片输入感知机,并使用新的权重,它会被正确分类。

假设下一幅图片不是数字0,而是数字1,且两个中心列的像素的值为1,其他为0。那么b+ w·x = 1 + 0.8×0+ 0.9×1 + 0.6×0 + 0×1 = 1.9,即f(x)>0,感知机会将样本错误分类为0,因此akf(xk) = 0−1 = −1。我们根据2(a)ii行调整每个权重,因为像素值为0,所以w7,7w14,7不变,而w7,14现在变成了0.9−0.9×1 = 0(前一个值减去权重乘以当前像素值)。bw4,14的新值留给读者计算。

我们多次在训练集上进行迭代。将训练集过一遍被称为一轮。此外,请注意,如果训练集数据以不同的顺序呈现给程序,我们学习的权重会有所不同,良好的做法是让训练集数据在每轮中随机呈现。在1.6节中我们会再谈到这一点,不过为了刚入门的学生,我们省略了这些细节。

如果我们不是只创建一个感知机,而是为我们想要识别的每个类别都创建一个感知机,这样就将感知机扩展到多类别决策问题。比如,对于最初的0~9数字识别问题,我们可以创建10个感知机,每个数字一个,然后输出感知机预测值最高的类别。在图1.6中,我们展示了三个感知机如何识别三类物体之一的图片。

虽然图1.6中三个感知机看起来关联紧密,但实际上它们是独立的,只共享相同的输入。多类别感知机输出的答案是输出最高值的线性单元的对应数字,所有感知机都是独立于其他感知机接受训练,使用的算法与图1.5所示完全相同。因此,给定图片和标签,我们对10个感知机运行感知机算法步骤2(a)共10次。如果标签是5,但是得出最高值的感知机对应数字6,那么对应数字0到4的感知机不会改变它们的参数(因为它们正确地给出了不是这一类或者是这一类的判断)。对应数字6到9的感知机也是如此。另一方面,由于感知机5和6报告了错误的判断,它们要改变参数。

图1.6 用于识别多个类别的多个感知机

在初期,对神经网络的讨论(以下简称为NN)伴随着类似于图1.6所示的图表,这类图表强调了各个计算元素(线性单元)。如今,我们预计这类元素的数量会很大,所以我们讨论的是以层为单位的计算。层即为一组存储或计算单元,各层并行工作并将值传递给另一层。图1.7所示是图1.6强调了层视图的版本,它显示了输入层到计算层的传递过程。

图1.7 神经网络层

神经网络可能有许多层,每个层的输出都是下一层的输入。层的堆叠结构便是“深度学习”中的“深度”由来。

然而,多层堆叠的感知机工作效果不佳,所以我们需要另一种方法学习如何改变权重。在本节中,我们探讨如何使用最简单的网络——前馈神经网络,和相对简单的学习技术——梯度下降,以达到改变权重的目的。一些研究人员将用梯度下降训练的前馈神经网络称为多层感知机。

在我们讨论梯度下降之前,我们首先需要讨论损失函数。损失函数是从模型输出得到输出结果有多“坏”的函数。在学习模型参数时,我们的目标是将损失最小化。如果对于训练样本我们得到了正确的结果,则感知机的损失函数值为零,如果不正确,则为1,这被称为0-1损失。0-1损失的优点让我们有足够的理由使用它,但它也有缺点,它不适用于梯度下降学习,因为梯度下降学习的基本思想是根据以下式子修改参数。

  

(1.4)

这里的是学习率,它是实数,用来衡量我们改变参数的程度。损失L相对于参数的偏导数是非常重要的,换句话说,如果我们能发现参数是如何影响损失的,就可以改变参数来减少损失(所以前有符号–)。在我们的感知机中,或者说在神经网络中,输出是由模型参数Φ所决定的,所以在此类模型中,损失是函数L(Φ)。

说得更形象一点,假设我们的感知机只有两个参数,我们可以想象一个具有轴Φ1Φ2的欧几里德平面,平面中每个点的上方(或下方)都标有损失函数值。假设参数的当前值分别为1.0和2.2,观察L在点(1,2.2)的行为。当Φ2=2.2时,图1.8的切面显示了作为Φ1函数的虚拟损失的运动轨迹。当Φ1=1时,切线的斜率约为–。如果学习率 = 0.5,那么根据式(1.4),加上(−0.5 ) ×(–) =0.125,即,向右移动约0.125个单位,可以减少损失。

图1.8 作为1函数的虚拟损失的运动轨迹

式(1.4)有效的前提是,损失必须是参数的可微函数,0-1损失却不是。假设将我们犯错误的数量作为某个参数Φ的函数,开始我们只在一个样本上评估我们的感知机,结果得到错误答案。如果我们继续增加Φ(或者减少Φ),并且重复足够多次,f(x)最终会改变它的值,我们就可以得到正确的结果。所以当我们看函数图像时,会看到一个阶跃函数,但是阶跃函数是不可微的。

当然,还有其他的损失函数。最常用的是交叉熵损失函数,它最接近“标准” 的损失函数。在本节中,我们将解释交叉熵损失函数是什么,以及我们的网络将如何计算这个函数。下一节使用它进行参数学习。

图1.6中的网络输出一个向量,向量中的每个元素值为一个线性单元的输出,我们选择最高输出值对应的类别,接着改变该网络,使得输出的数字是各类的(估计的)概率分布。在本例中,正确分类的概率是随机变量CC=c,其中c∈{0,1,2,…,9}。概率分布是一组总和为1的非负数,目前该网络可以输出数字,但它们一般同时包括正数和负数。幸运的是,softmax函数可以简单地将一组数字转换成概率分布,该函数公式如下:

  

(1.5)

softmax可以保证返回一个概率分布,因为即使xi是负数,也是正数,并且所有值加和为1,因为分母是所有可能值之和。例如,σ([−1,0,1])≈[0.09, 0.244,0.665 ]。要注意一个特殊情况是,神经网络到softmax的输入都为0。当e0= 1,如果有10个选项,所有选项的概率会是。推而广之,如果有n个选项,概率即为

“softmax”之所以得名,是因为它是“max”函数的“软”(soft)版本。max函数的输出完全由最大输入值决定,softmax的输出主要但不完全由最大值决定。很多机器学习函数以“softX”的形式命名,意味着X的输出“被软化”。

图1.9显示了添加了softmax层的网络。左边输入的数字是图片像素值,添加softmax层之后,右边输出的数字是类别概率。离开线性单元进入softmax函数的数字通常称为logit,这是术语,用来指即将使用softmax转化为概率的、没有标准化过的数字(“logit”有多种发音方式,最常用的发音是“LOW-git”)。我们用l来表示多个logit的向量(每个类别对应一个logit),公式如下:

  

(1.6)

  

(1.7)

图1.9 具有softmax层的简单网络

公式的第二行是指,由于softmax函数的分母是一个标准化常数,以确保这些数字总和为1,所以概率与softmax的分子成比例。

现在我们可以定义交叉熵损失函数X

  (1.8)

样本x的交叉熵损失是x对应标签的概率的负对数。换句话说,我们用softmax计算所有类别的概率,然后找出正确答案,损失是这个数字的概率的负对数。

该公式的合理性如下所述:它朝着正确的方向前进。如果X是一个损失函数,模型越差,函数结果应该越大,模型越好,结果越小。而一个改进过的模型应该为正确答案得到更高的概率,所以我们在前面加上一个减号,这样随着概率的增加,这个数字会变得越来越小。数字的对数随着数字的增加/减小而增加/减小。因此,相较于好参数,X(Φ, x )输入坏参数得到的结果会更大。

但是为什么要使用对数?一般认为对数可以缩小数字之间的距离,比如,log(10,000)和log(1,000)之间的差异是1。有人认为这是损失函数的一个缺点:它会让糟糕的情况看起来不那么糟糕。对数的这种特征是有误导性的,随着X越来越大,ln(X )却不会以同样的程度增加,但考虑图1.10中的–ln(X),当X变为0时,对数的变化比X的变化大得多。由于我们处理的是概率,这才是我们要关心的区域。

图1.10 –ln(x )图像

为什么这个函数被称为交叉熵损失函数?在信息论中,当概率分布在逼近某个真实分布时,这两个分布的交叉熵用于测量它们之间的差异,交叉熵损失是交叉熵的负值的近似值。这只是比较浅显的解释,因为信息论不是本书重点,所以不再深入讨论。

现在我们有了损失函数,可以用下面的公式来计算它。

 

(1.9)

  

(1.10)

   

(1.11)

我们首先根据式(1.11)计算logit l,然后根据式(1.10)将这些logit通过softmax层来计算概率,接着根据式(1.9)计算损失,即正确答案概率的负自然对数。请注意,之前线性单元的权重表示为w,但现在我们有许多这样的单元,所以wj是第j个单元的权重,bj是该单元的偏置项。

这个从输入到损失的计算过程被称为学习算法的前向传递,它计算在权重调整过程即反向传递中将要使用的值。该值可以使用多种方法进行计算,这里我们使用随机梯度下降法。“梯度下降”这个术语源于损失函数的斜率(它的梯度),然后使系统跟随梯度降低其损失(下降)。整个学习方法通常被称为反向传播。

我们先举个最简单的例子,对某个偏置项bj,如何进行梯度估计。从式 (1.9)~式(1.11)中可以看出,通过先后改变lj的值和概率,bj改变了损失。让我们分步做这件事。在本例中,我们只考虑由单个训练样本引起的错误,所以我们将X(Φ, x )写为X(Φ)。首先,

 

(1.12)

该公式使用链式法则来表达前述内容——bj的变化会导致X的变化,这是因为bj的变化会引起lj的变化。

现在看式(1.12)右边的一阶偏导数。事实上,它的值恒等于1。

  

(1.13)

其中,wi,j是第j个线性单元的第i个权重。因为在中,作为bj的函数变化的只有bj本身,所以导数是1。

接下来,我们考虑X作为lj的函数是如何变化的。

 

(1.14)

其中pi是网络分配给i类的概率。这说明,因为X只取决于正确答案的概率,所以lj只通过改变这个概率来影响X。反过来,

 

(1.15)

(来自基础微积分)。

还剩下一个项有待估值。

 

(1.16)

式(1.16)的第一个相等表示我们通过计算logit的softmax函数来获得概率,第二个相等来自维基百科。公式推导需要仔细梳理各项,所以在这里我们不做推导,但我们仍可以得出它的合理性。我们来看logit的向量值lj的变化如何影响softmax函数得出的概率,可以查看以下公式。

 

分为两种情况。假设logit正在变化的j不等于a,也就是说,假设这是一张数字6的图片,但是我们计算的是logit 8对应的偏置。在这种情况下,式中lj只出现在分母中,导数应该是负的(或0),因为lj越大,pa越小。这就对应式(1.16)中的第二种情况,当然,式(1.16)中会产生一个小于或等于0的数字,因为两个概率相乘不会为负。

另一方面,如果j = a,则式中lj同时出现在分子和分母中。它在分母中的出现会导致结果变小,但是在这种情况下,分子的增加会抵消掉结果的变小。因此,在这种情况下,我们期望一个正导数(或0),这是式(1.16)的第一种情况。

有了这个结果,我们现在可以推导出修正偏置参数bj的公式。将式(1.15)和式(1.16)代入式(1.14),我们可以得到

 

(1.17)

 

(1.18)

剩下的很简单。我们在式(1.12)中指出

右边第一个导数值为1。因此,损失相对于bj的导数可由式(1.14)算出。最后,使用改变权重的规则式(1.12),我们得到了更新神经网络偏置参数的规则。

 

(1.19)

改变权重参数的公式是式(1.19)的变体。与式(1.12)对应的权重公式为

 

(1.20)

首先,请注意最右边的导数与式(1.12)中的导数相同。这意味着,在权重调整阶段,当我们更改偏置时,应当保存结果以复用。右边两个导数的第一个导数估值为

  

(1.21)

如果我们牢记偏置仅仅是一个权重,其对应的特征值恒等于1,我们就可以推导出这个公式,运用这个新的“伪权重”就可以立即从式(1.21)推导出式(1.13)。

使用这个结果可以更新权重公式。

  

(1.22)

我们现在已经推导出如何根据一个训练样本调整我们模型的参数。梯度下降算法会让我们遍历所有的训练样本,记录每个样本对参数值修正的建议,但在我们完成遍历所有样本之前,不会实际改变参数。我们通过各个样本得到的修正量加和来修改每个参数。

这个算法的缺点是非常慢,尤其是当训练集特别大的时候。我们通常需要经常调整参数,因为根据特定样本的结果,每个参数有增有减时,它们会以不同的方式相互作用。因此,在实践中,我们几乎不使用梯度下降,而是使用随机梯度下降。随机梯度下降中,每m个样本更新一次参数,然而m个样本比训练集小得多。一个典型的m值是20。这被称为批大小。

通常,批越小,学习率应该设置得越小。因为任何一个样本都会以牺牲其他样本为代价,将权重推向其正确的分类。如果学习率低,不会有太大影响,因为对参数所做的改变相对较小。相反,对于较大批量样本,我们会在m个不同的样本上取平均值,所以参数不会向单个样本的特性过于倾斜,对参数的改变可以更大。

图1.11 使用简单的前馈网络进行数字识别的伪代码

我们已经全面了解了神经网络程序的内容。图1.11所示是伪代码,其中第一行内容是我们的首要操作——初始化模型参数。有些情况下我们可以将初始值设为0,就像我们在感知机算法中所做的那样。虽然我们在本例中可以这么设置,但可能不适用于其他情况。一般的做法是随机设置权重接近于0。还可以通过给Python随机数生成器一个种子,这样在调试时,参数可以设置为相同的初始值,就可以得到完全相同的输出。如果没有给种子,Python会使用环境中的数字作为种子,比如时间的最后几位数字。

请注意,在训练的每一次迭代中,我们首先修改参数,然后在验证集上运行模型来查看当前这组参数的表现。当在验证样本运行时,我们不会执行反向训练。如果打算将我们的程序用于某个实际用途(例如,读取邮件上的邮政编码),我们看到的样本并不是我们训练的样本,因此我们想知道程序的实际效果。我们的验证数据就是实际情况的近似。

一些经验可以派上用场。首先,像素值不要太偏离−1到1这个区间。在本例中,因为原始像素值是0到255,我们只需将它们除以255,这个过程叫作数据规范化。虽然不是硬性规定,但是将输入保持在−1到1或者0到1是有意义的,在式(1.22)中,我们可以看到这一点。观察式(1.22)可以发现,调整同一个输入对应的偏置项和权重的公式之间的区别在于后者有乘法项xi,即输入项的值。在上一节我们说过,如果我们假设偏置项只是一个权重项,其输入值总是1,那么用于更新偏置参数的方程就会从式(1.22)中消失。因此,如果我们不修改输入值,并且其中一个像素的值为255,那么我们修改权重值会是修改偏置参数的255倍。这种情况比较古怪,因为我们没有先验理由认定权重值比偏置参数需要更多的修正。

接下来要设定学习率,这是比较复杂的。这里,我们把学习率设定为0.0001。要注意,比起设置过小,设置得过大会产生更加糟糕的结果。如果设置值过大,softmax函数会得出一个数学溢出错误。再看式(1.5),首先应该想到的是分子和分母的指数。如果学习率过大,那么某个logit可能会变大,而e(≈2.7)的指数过高的时候一定会溢出。即使没有错误警告,过高的学习率也会导致程序在学习曲线的无意义区域徘徊。

因此,标准做法是在进行计算时,观察个别样本的损失。首先从第一张训练图片开始。这些数字通过神经网络输出到logit层。所有的权重和偏置都是接近0的数字(比如0.1)。这意味着所有的logit值都非常接近于0,所以所有的概率都非常接近(见式(1.5)的讨论)。损失是正确答案概率的负自然对数,即−ln () ≈2.3。我们预计总体趋势是,随着训练的样本增多,单个损失会下降。但是,也有一些图片并不是那么规范,神经网络分类的确定性更低。因此,单个损失既有上升的情况也有下降的情况,这种趋势可能难以辨别。所以我们不是一次打印一个损失,而是将所有损失相加,然后比如每100批打印一次平均值。应该能够观察到平均值明显降低,虽然可能会有抖动。

回到关于学习率和其设置过高的风险讨论,太低的学习率也会降低程序收敛到一组好参数的速度。所以开始时用较低的学习率,而后尝试更大的值通常是最好的做法。

由于很多参数都在同时变化,神经网络算法很难调试。想要完成调试,需要在程序错误出现之前尽可能少地改变一些元素。首先,当我们修改权重时,如果立即再次运行相同的训练样本,损失会更少。如果损失没有减少,可能是有以下两个原因:第一,程序出现错误或学习率设置得太高;第二,不需要改变所有的权重来减少损失,可以只改变其中一个,或者一组。例如,当你第一次运行算法时,只需改变偏置。然而,单层网络中的偏置很大程度上会捕捉到不同的类别以不同的频率出现。这在Mnist数据中并不多见,所以在这种情况下,我们仅通过学习偏置并不会有太大改善。

如果程序运行正常,可以获得约91%或92%的验证数据精度(accuracy)。这个结果并不是很好,但这是一个开始。在后面的章节中,我们将学习如何达到约99%的精度。

简单神经网络有一个好处,那就是有时我们可以直接解释各个参数的值,并判断它们是否合理。在我们讨论图1.1时,像素点(8,8)是暗的,它的像素值为254,这在某种程度上可以判断图片是数字7,而不是数字1,因为数字1通常不占据左上角的空间。我们可以将这一观察转化为对权重矩阵wi,j中值的预测,其中i是像素数,j是答案数。如果像素值是从0到784,那么位置(8,8)的像素值是8×28 + 8 = 232,并且连接像素值和数字7(正确答案)的权重将是w232,7,而连接像素值和数字1的权重是w232,1。也就是说,w232,7应该比w232,1大。我们用低方差随机初始化权重的方法分别运行了几次训练程序,每一次都得到前者为正数(比如0.25),而后者为负数(比如−0.17)。

线性代数提供了另一种表示神经网络中运算的方法:使用矩阵。矩阵是元素的二维数组。在我们的例子中,这些元素是实数。矩阵的维度分别是行数和列数,所以lm列矩阵如下所示:

(1.23)

矩阵的主要运算是加法和乘法。两个矩阵(必须具有相同维度)的相加是每个矩阵元素相加。也就是说,如果YZ两个矩阵相加,即X = Y + Z,那么xi,j = yi,j + zi,j

两个矩阵相乘X = YZ表示维度为l ×m的矩阵Y和维度为m × n的矩阵Z相乘,结果是维度为l×n的矩阵X,其中,

  

(1.24)

举一个例子,

 

我们可以使用矩阵乘法和加法的组合来定义线性单元的运算,特别是输入特征是一个1×l的矩阵X。在前述的数字识别问题中,l = 784,像素单元对应的权重是W,即wi, j是单元j的第i个权重。所以W的维度是像素数乘数字个数,即784×10。B是长度为10的偏置向量,并且

  

(1.25)

其中L是长度为10的logit向量。确保维度对应是使用该公式的前提。

现在,前馈Mnist模型的损失( L )可以用如下公式表示。

  

(1.26)

  

(1.27)

其中,第一个公式给出了可能类别A(x)上的概率分布,第二个公式确定了交叉熵损失。

我们也可以更简洁地表示反向传递。首先,我们引入梯度运算符。

 

(1.28)

倒三角形▽xf (x )表示通过对x中的所有值取f的偏导数而得到的向量。之前我们讨论了单个lj的偏导数,这里,我们将所有l的导数定义为单个导数的向量。此外,矩阵的转置是指矩阵行和列间的转换。

 

(1.29)

有了这些,我们可以将式(1.22)改写为

  

(1.30)

根据公式右半部,我们将784×1矩阵乘以1×10矩阵,得到一个784×10的矩阵,这就是784×10权重矩阵W的变化量。

矩阵表示法可以清楚地描述输入层进入线性单元层生成logit,以及损失导数传回参数变化量的运算过程。除此之外,使用矩阵符号还有一个更为实用的原因。当进行大量线性单元运算时,一般线性代数运算,特别是深度学习训练可能非常耗时。而很多问题可以用矩阵符号法表示,许多编程语言都有特殊的包,允许你使用线性代数结构进行编程,并且经过优化的包也比手动编码更有效率。特别是在Python中编程,使用NumPy包及其矩阵运算操作,可以得到一个数量级的加速。

此外,线性代数还可以应用于计算机图形学和游戏程序,这就产生了称为图形处理单元即GPU的专用硬件。GPU相对于CPU较慢,但GPU有大量处理器,以及用于并行线性代数计算的软件。神经网络的一些专用语言(例如Tensorflow)有内置软件,可以感知GPU的可用性,并在不改变代码的情况下使用GPU。这通常又会使速度提升一个数量级。

在本例中,采用矩阵符号还有第三个原因。如果我们并行处理若干训练样本,专用软件包(如NumPy)和硬件(GPU)效率会更高。此外,矩阵符号法符合我们之前提出的思想:在更新模型参数之前处理m个训练样本(批大小)。通常做法是将m个训练样本全都输入矩阵中,一起运行。在式(1.25)中,我们将图片x想象成大小为1×784的矩阵,这是一个训练样本,有784个像素点。现在我们将矩阵维度变为m×784,即使不改变处理方法(必要的更改已经内置到例如NumPy和Tensorflow等软件包和语言中),矩阵仍旧能够工作。让我们来看看为什么。

首先,矩阵乘法XW中的X现在已经从1行变为m行。1行是1×784,那么m行即为m×784。根据线性代数中矩阵乘法的定义,可以看作我们对每一行进行乘法,然后将它们叠在一起,得到m×784矩阵。

在公式中加上偏置项就不像这样可以直接计算。我们说过矩阵加法要求两个矩阵具有相同的维度,但式(1.25)中的两矩阵维度不相等了,XW现在的维度大小是m ×10,而偏置项B的维度大小为1 ×10,需要进行改变。

NumPy和Tensorflow都有广播机制。当一些矩阵的大小不符合算术运算要求的大小时,可以对矩阵大小进行调整。比如某个矩阵维度为1×n,而我们需要的是m×n维度的矩阵,矩阵会获得由其单行或单列组成的m−1份(虚拟)副本,使维度符合要求。这使得B的维度大小变为m ×10,这样我们就可以将偏置项加到乘法输出m×10中的所有项上。在数字识别问题中,回忆一下当维度是1×10的时候我们如何加偏置项。每一个数字都可能是正确答案,我们对每个数字的决策增加了偏置项。现在我们也要增加偏置项,但这次是针对所有的决策和m个样本,并行增加偏置项。

如果独立同分布(iid)假设成立,即我们的数据是独立同分布的,那么神经网络模型会收敛到正确的解。宇宙射线的测量是一个典型的例子,射入的光线和所涉及的过程是随机和不变的。

而我们的数据很少(几乎没有)是独立同分布的——除非国家标准协会提供源源不断的新样本。训练时第一轮的数据是独立同分布的,但从第二轮开始的数据就与第一轮完全相同,独立同分布假设不成立。有时我们的数据从第二个训练样本开始就无法做到独立同分布,这在深度强化学习(第6章)中很常见,因此,在深度强化学习中,网络经常出现不稳定,导致无法收敛到正确的解,有时甚至无法收敛到任何解。我们认为,如果一个较小的数据集以非随机顺序输入数据,就可能产生灾难性的结果。

假设对于每个Mnist图片,我们都添加了第二个形状相同但黑白相反的图片,即,如果原始图片的像素值为v,则反图片的像素值为−v。我们现在在这个新的语料库上训练Mnist感知机,但是使用了不同的输入顺序(我们假设批大小是某个偶数)。在第一个顺序中,每个原始Mnist数字图片后面紧跟着它的相反版本。我们认为(我们通过实验验证了这一点),简单Mnist神经网络不会比随机猜测的结果更好。这似乎是合理的。首先,反向传递修改了原始图片权重,然后处理第二个反图片。由于反图片的输入是原始图片的负数,且其他元素完全相同,所以对反图片权重的修改完全抵消了原始图片权重的修改。因此在训练结束时,所有权重都没有改变。这也意味着没有学习,参数和随机初始化的一样。

另一方面,学习独立处理每个数据集(原始数据集和相反数据集)真的不应该有太多的困难,而且即使是学习合起来的数据集的权重,难度也不应该太大。实际上,简单地随机化输入顺序就可以使性能恢复到接近原始问题的水平。如果有10,000个样本,在这10,000个样本之后,权重已经发生了很大的变化,所以相反的图片并不能完全抵消原来的学习。但如果我们有一个无穷无尽的图片来源,并且抛硬币来随机决定原始图片或反图片输入神经网络的顺序,那么这些抵消就会完全消失。

在本节和后续章节的“参考文献和补充阅读”部分,我会完成以下几点:(a)给学生指定该章节主题的后续阅读材料;(b)阐述该领域的一些重要贡献;(c)引用参考资料。我不能保证所有内容的完整性或客观性,特别针对(b)条目。在准备写这一部分时,我开始阅读神经网络的历史文章,特别是Andrey Kurenkov [Kur15][1]的一篇博客文章,以检查我的记忆是否准确(并丰富其内容)。

早期研究神经网络的一篇重要论文由McCulloch和Pitts [MP43]于1943年撰写,他们提出了本书所说的线性单元作为单个神经元的正式模型。然而,他们没有提出一个可以训练单个或多个神经元完成任务的学习算法,这类学习算法最早由Rosenblatt在其1958年的感知机论文[Ros58]中提出。然而,正如我们在文中指出的,他的算法只适用于单层神经网络。

下一个重大贡献是反向传播的发明,它适用于多层神经网络。当时,许多研究人员都在几年间独立得出了这个方法(只有当最初的论文没有引起足够的注意,其他人尚未发现问题已经解决时,这种情况才会发生)。Rumelhart、Hinton和Williams的论文结束了这一时期,他们明确指出三人的论文是对该发明的重新发现[RHW86]。三人的这篇论文来源于圣地亚哥大学的一个小组,该小组撰写了大量论文,推动了并行分布式处理(PDP)下神经网络的第二次研究热潮。这些论文组成了两卷合集,在神经网络领域颇有影响力[RMG+87]。

至于我是如何学习神经网络的,我在后面的章节中给出了更多的细节。在本章中,我要介绍一篇博客和两本书。Steven Miller [Mil15]的一篇博客用一个很棒的数字例子详细介绍了反向传播的前向传递和反向传递。还有两本我查阅过的神经网络教科书,一本是Ian Goodfellow、Yoshua Bengio和Aaron Courville的《深度学习》[GBC16],第二本是由Aurélien Géron [Gér17]编写的《机器学习实战:基于Scikit-Learn和Tensorflow》。

练习1.1 思考批大小为1的前馈Mnist程序。假设我们在第一个样本训练前后观察偏置变量。如果它们设置正确(即我们的程序中没有错误),请描述你在偏置值中看到的变化。

练习1.2 假设图片有两个像素,且为二值(像素值为0或1),没有偏置参数,讨论一个二分类问题。(a)当像素值为( 0,1 )且权重为

0.2 -0.3

-0.1 0.4

时,计算前向传递的logit和概率。这里w[i,j]是第i个像素和第j个单元之间连接的权重,例如,w[0,1]在这里等于−0.3。(b)假设正确答案是1(而不是0),学习率为0.1,损失是多少?并计算反向传递时的Δw0,0

练习1.3 当图片像素值是( 0,0 )时,回答练习1.2的问题。

练习1.4 一个同学问你:“在初等微积分中,我们通过对一个函数求微分,将结果表达式设置为0,并求解方程,找到它的最小值。既然我们的损失函数是可微的,为什么我们不用同样的方法,而使用梯度下降呢?”解释为什么不行。

练习1.5 计算以下内容。

  

(1.31)

你可以假设有广播机制,这样计算时维度就相符了。

练习1.6 在本章中,我们只讨论了分类问题,对于这类问题,交叉熵是通常会选择的损失函数。在某些问题中我们希望可以用神经网络预测特定的值。例如,许多人想要这样一个程序,给定今天某只股票的价格和世界上其他影响因素,输出明天股票的价格。如果我们训练一个单层神经网络来达到这个目的,通常会使用平方误差损失。

  

(1.32)

其中t是当天的实际价格,l(X,Φ)是单层神经网络Φ = {b,W}的输出。这也被称为二次损失。推导出损失对bi求导的公式。

[1] 说明:本书中的参考文献在正文中出现时,采用作者姓名首字母缩写加年份缩写的形式,如[Kur15]是指Kurenkov 于2015 年发表的文献,[MP43]是指McCulloch 和Pitts 于1943 年发表的文献,[BCP+88]是指Brown、Cocke 和Pietra 等人于1988 年发表的文献,其余类推。


本章讲述的主要内容包括:预备知识;Tensorflow程序;多层神经网络;检查点、Tensordot、TF变量的初始化和TF图创建的简化;参考文献和补充阅读;习题。

Tensorflow是谷歌开发的一种开源编程语言,旨在让深度学习程序编程变得更简单。我们首先从一个程序开始。

import tensorflow as tf
x = tf.constant("Hello World")
sess = tf.Session()
print(sess.run(x)) #will print out "Hello World"

该程序是否看起来像Python代码呢?它的确就是Python代码。事实上,Tensorflow(后称TF)是一组函数集合,可以使用不同的编程语言来调用它。最完整的接口是Python的,这就是我们在上述程序中使用的。

要注意的是,TF函数与其说是执行一个程序,不如说是定义一个只有在调用run命令时才执行的计算,就像上面程序的最后一行一样。更准确地说,第3行中的TF函数Session创建了一个会话,与该会话相关联的是定义计算的图。像constant这样的命令会将元素添加到计算中。在本例中,元素只是一个常量数据项,其值是Python字符串“Hello World”。第4行代码显示TF计算与会话sess相关联的图中的x指向的TF变量。最终结果是——打印输出“Hello World”。

我们可以将上例最后一行替换为print(x),进行对比。替换后输出

Tensor("Const:0", shape=(), dtype=string)

关键是Python变量x并不绑定到字符串,而是绑定到Tensorflow计算图的一部分。只有当通过执行sess.run(x)来计算图的这一部分时,我们才能访问TF常量的值。

图2.1 TF中的placeholder

所以,在上面的代码中,xsess是Python变量,可以根据我们的需要命名。importprint是Python函数,必须这样拼写,Python才能理解我们想要执行哪个函数。constantSessionrun是TF命令,拼写必须准确(包括Session中需要大写“S”)。此外,需要首先import tensorflow,这是固定的,我们在后文中不再提及。

在图2.1中的代码中,x仍是Python变量,其值是TF常量,在本例中是浮点数2.0。然后,z是Python变量,其值是TF placeholder。TF中的placeholder类似于编程语言函数中的变量。假设我们有以下Python代码。

x = 2.0
def sillyAdd(z):
   return z+x
print(sillyAdd(3))  # Prints out 5.0
print(sillyAdd(16)) # Prints out 18.0

这里zsillyAdd参数的名称,当我们调用sillyAdd(3)中的函数时,z被它的值3所取代。TF程序的工作方式类似,不同之处在于给TF placeholder赋值的方式不同,如图2.1的第5行所示。

print(sess.run(comp,feed_dict={z:3.0}))

这里的feed_dictrun的命名参数(因此它的名称必须拼写正确)。它接受Python字典这类值。在字典中,计算所需的每个placeholder都必须给定一个值。所以第一次sess.run打印输出为2.0和3.0的总和,第二次打印输出18.0。第三次调用sess.run时需要注意的是,如果计算不需要placeholder的值,则不必提供其值。另一方面,正如第4个打印输出语句后的注释所指出的,如果计算需要一个值,但没有提供该值,就会出现错误。

Tensorflow的命名源于其基本数据结构是张量型(tensor)多维数组。大约有十五种或更多张量类型。当我们定义上面的placeholder z时,我们给出了它的类型为float32。除了它的类型,张量也有形状。想象一个2×3的矩阵,它的形状就是[2, 3]。长度为4的向量形状为[4],它不同于形状为[1,4]的1×4矩阵,或者形状为[4,1]的4×1矩阵。一个3×17×6的数组形状为[3,17,6]。他们都是张量。标量(即数字)的形状是null,也属于张量。此外,请注意张量不像线性代数,它不需要区分行向量和列向量。有些张量的形状只有一个分量,例如[5]。我们如何在纸上画出这些张量对数学来说无关紧要。我们对数组张量进行图示时,总是遵循第零个维度垂直绘制,第一个维度水平绘制的规则。但这是我们为保持一致进行的限制。请注意,张量维数和下标都是从零开始的。

回到我们对placeholder的讨论:大多数placeholder不是前述例子中的简单标量,而是多维张量。2.2节从一个简单的用于Mnist数字识别的Tensorflow程序开始。其中将一张图片输入TF代码,并运行神经网络前向传递,以获得网络对数字的预测。此外,在训练阶段,运行反向传递并修改程序的参数。为了给程序传入图片输入,我们定义了一个placeholder。它是float32型,形状为[ 28,28],或者是[784],这取决于我们给它的是一个二维Python列表还是一维Python列表。例如,

img=tf.placeholder(tf.float32,shape=[28,28])

请注意,shapeplaceholder函数的命名参数。

在深入讨论真正的程序之前,我们先看TF数据结构。如前所述,神经网络模型由它们的参数和程序的结构来定义——如何将参数与输入值组合以产生答案。通常我们随机初始化参数(例如,连接输入图像和答案logit的权重w),神经网络会修改参数以在训练数据上最小化损失。创建TF参数有三个阶段。首先,用初始值创建张量,然后将张量转换为Variable(TF对参数的称谓),然后初始化变量或者说参数。我们来创建图1.11中前馈Mnist伪代码所需的参数。首先是偏置项b,然后是权重W

bt = tf.random_normal([10], stddev=.1)
b = tf.Variable(bt)
W = tf.Variable(tf.random_normal([784,10],stddev=.1))
sess=tf.Session()
sess.run(tf.global_variables_initializer())
print(sess.run(b))

第1行添加了创建形状为[10]的张量的指令,张量的十个值是从标准偏差为0.1的正态分布生成的随机数。正态分布,也称为高斯分布,是常见的钟形曲线。从正态分布中选取的数字将以平均值(µ)为中心,它们离平均值的距离由标准偏差(σ)决定。更具体地说,大约68 %的值处在平均值的一个标准偏差范围内,超出这个范围的数字出现概率会大大降低。

上面代码的第2行输入为bt,并添加了一段TF图,该图创建了一个与bt具有相同形状和值的变量。一旦我们创建了变量,我们就很少需要原始张量,所以通常会同时进行上述两个事件而不保存张量指针,就像创建参数W的第3行一样。在使用bW之前,我们需要在创建的会话中对它们进行初始化,这是第5行的工作。第6行是打印输出结果(结果如下,每次都会不同)。

[-0.05206999 0.08943175 -0.09178174 -0.13757218 0.15039739
  0.05112269 -0.02723283 -0.02022207 0.12535755 -0.12932496]

如果我们颠倒了最后两行的顺序,当尝试打印b所指的变量时,就会收到一条错误消息。

因此,在TF程序中,我们创建变量来存储模型参数。最初,参数的值是不含信息的,通常是标准偏差很小的随机值。根据之前的讨论,梯度下降的反向传递修改了它们。一旦被修改,sess指向的会话将保留新值,并在下次运行会话时使用它们。

图2.2是前馈神经网络Mnist程序的TF版本,它比较完整,应该可以运行。这里隐藏的关键元素是代码mnist.train.next_batch,它处理Mnist数据中的读取细节。先大体看一看图2.2,请注意虚线之前的所有内容都与设置TF计算图有关;虚线之后首先使用图来训练参数,然后运行程序来查看测试数据的准确性。现在我们逐行解读这个程序。

首先,是import tensorflow和Mnist数据的读取代码,然后在第5行和第6行定义了两组参数,这和刚才讨论的TF变量定义有一点小变化。接下来,我们为输入神经网络的数据定义placeholder。首先,在第8行,定义图像数据的placeholder,这是一个形状为[batchSz,784]的张量。在讨论线性代数为什么是表示神经网络计算的好方法时(1.5节),我们注意到,同时处理几个样本时,我们的计算速度会加快,而且,这与随机梯度下降中的批处理概念非常吻合。在图2.2中,我们可以看到这一点在TF中如何实现。也就是说,图片的placeholder不是一行784个像素,而是100行(这取于batchSz的值)。程序第9行与之类似,我们的程序一次性给出100张图片的预测。

图2.2 Mnist的前馈神经网络的Tensorflow代码

在第9行中还需注意一点。我们用包含10个数字的向量表示一个答案,所有数字值都为零,除了第a个,其中a是该图像对应的正确数字。例如,第1章中的数字7的图片(图1.1),正确答案的对应表示是( 0,0,0,0,0,0,0,1,0,0 )。这种形式的向量被称为独热(one-hot)向量,因为它们具有仅选择一个值作为激活值的特性。

截至第9行是程序的参数定义和输入,下面的代码是完成图中的计算。其中第11行开始显示TF用于神经网络计算的强大能力。它定义了模型的神经网络前向传递,将(一个批大小的)图片输入线性单元(由Wb定义),然后对所有结果应用softmax函数以得到一个概率向量。

我们建议在查看类似代码时,首先检查所涉及的张量的形状,以确保它们是合理的。这里隐藏最深的计算是矩阵乘法matmul,即输入图片[100,784]乘以W[784, 10]得到一个形状为[100,10]的矩阵。接着我们将偏置与矩阵相加,得到一个形状为[100,10]的矩阵,这是100张图片的批中的10个logit。然后,将结果通过softmax函数处理,最后会得到图片对应的[100, 10]大小的标签概率分配矩阵。

第12行并行计算100个样本的平均交叉熵损失。我们从里到外进行讲解。tf.log(x)返回一个张量,使得x的每个元素都被它的自然对数代替。图2.3展示了tf.log如何进行批操作,批大小为3,批中每个向量都包含5个概率分布。

图2.3 tf.log的批操作

接下来,ans * tf.log(prbs)中的标准乘法符号“*”代表两个张量的逐元素相乘。图2.4显示了在批运算中,每个标签的独热向量与负自然对数矩阵的逐元素相乘如何进行。结果中的每一行,除了正确答案概率对应的负对数之外,所有内容都被清零。

图2.4 答案乘概率的负对数的计算

此时,为了获得每张图片的交叉熵,我们只需要对数组中的所有值求和。求和的第一步操作是

tf.reduce_sum( A, reduction_indices = [ 1 ] )

它将A的各行相加,如图2.5所示。这里的一个关键部分是

reduction_indices = [ 1 ]

在我们之前对张量的介绍中,提到了张量的维数是从零开始的。reduce_sum可以对列求和,默认情况下,reduction_indices=[0],或者,如本例中,对行求和,reduction_indices=[1]。这将生成一个[100,1]的数组,每行中只有正确概率的对数作为唯一的条目。图2.5设批大小为3,并假设有5个类,而不是10个。作为交叉熵计算的最后一个部分,图2.2中第12行reduce_mean对所有列求和(同样reduction_indices是默认值),并返回平均值(1.1左右)。

图2.5 根据reduction_indices为[1]进行tf.reduce_sum计算

最后,我们可以转到图2.2中的第14行,在此TF真正展示了它的优点,这一行就实现了整个反向传递所需的全部内容。

tf.train.GradientDescentOptimizer(0.5).minimize(xEnt)

即,使用梯度下降来计算权重变化,并最小化由第12行和第13行定义的交叉熵损失函数。该行还指定了0.5的学习率。我们不必担心计算导数或其他元素,因为如果你在TF中定义了前向计算和损失,那么TF编译器会知道如何计算必要的导数,并按照正确的顺序将它们串在一起对权重进行修改。我们可以通过选择不同的学习率来修改这个函数调用,或者,如果我们使用不同的损失函数,可以用另一个TF计算的元素替换xEnt

当然,TF基于前向传递导出反向传递的能力是有限的。再强调一次,只有当所有前向传递计算都用TF函数完成时,它才能做到这一点。对于像我们这样的初学者来说,这并不是太大的限制,因为TF有各种各样的内置操作,它知道如何进行区分和连接。

第15行和第16行代码计算模型的accuracy(精度)。精度是模型计算正确答案的数量除以处理的图片数量。首先,关注标准数学函数argmax,如,它返回让f(x)最大化的x值。在这里,我们使用的tf.argmax(prbs, 1)有两个参数:第一个是张量,我们从中取argmax;第二个是取argmax的张量轴。张量轴的作用与我们用于reduce_sum的命名参数类似——它帮助我们在张量的不同轴上求和。举例来说,如果张量是( ( 0,2,4 ), ( 4,0,3 ) ),并且使用轴0(默认值),我们会得到( 1,0,0 )。我们先比较0和4,由于4更大,所以返回1。然后我们比较2和0,由于2更大,所以返回0。如果我们使用轴1,我们会返回( 2,0 )。第15行有一个批大小logit的数组。argmax函数返回批大小的最大logit所在位置的数组。接下来,我们应用tf.equal将最大logit与正确答案进行比较。tf.equal返回一个批向量的布尔值(如果它们相等,则为True),tf.cast(tensor,tf.float32)将该向量转换为浮点数,以便tf.reduce_mean将它们相加,得到正确率的百分比。请注意不要将布尔值转换成整数,因为取平均值时,它会返回一个整数,在这种情况下,该整数将始终为零。

定义了会话(第18行)并初始化参数值(第19行)之后,我们可以训练模型(第21行至第23行)。在这三行代码中,我们使用从TF Mnist库中获得的代码每次提取100张图片及其答案,然后通过调用sess.run在训练的计算图上运行程序。当这个循环结束时,我们共训练了1,000次,每次迭代有100张图片,或者说总共训练了100,000张测试图片。我的Mac Pro电脑具有四核处理器,完成这轮循环大约需要5秒(第一次将内容放入缓存中会花费较长时间)。提到“四核处理器”是因为TF会查看可用的计算能力,在没有指导时也能很好地使用电脑的计算能力。

你可能注意到了,第21行到第23行有一点奇怪——我们从来没有明确提到过要进行前向传递,而TF根据计算图(Computation graph)计算出了这一点。从GradientDescentOptimizer中,TF知道自己需要执行xEnt所需的计算(第12行),这需要计算prbs,而该计算又指向了第11行的前向传递计算。

最后,第25行到第29行计算测试数据的正确率(91%或92%)。首先,通过浏览计算图的组织结构可以发现,accuracy计算最终需要的是在前向传递中计算prbs,而不是反向传递的训练。因此,为了更好地测试数据,不对权重进行修改。

第1章中提到,在训练模型时打印输出错误率是良好的调试实践。一般来说,错误率会下降。为此,我们将第23行改为

acc,ignore= sess.run([accuracy,train],
                          feed_dict={img: imgs, ans: anss})

这里的语法是用于组合计算的普通Python语言。计算的第一个值(accuracy的值)分配给变量acc,计算的第二个值分配给ignore。Python的习惯做法是用下划线符号( _ )代替ignore,当语法要求变量接受一个值,但我们不需要记住它时,Python会使用下划线符号。当然,我们还需要添加一个命令来打印输出acc的值。

我们提到这一点是为了帮助读者避免一个常见的错误——无视第23行,反而自己新增了第23.5行(我和一些刚入门的学生都犯过这个错误)。

acc= sess.run(accuracy, feed_dict={img: imgs, ans: anss})

这种做法效率较低,因为TF在这种情况下需要进行两次前向传递,一次是在要进行训练时,另一次是在求accuracy时。更重要的是,第一次调用会修改权重,从而更有可能为该图片预测正确的标签。如果在此之后计算accuracy,程序的性能就会有所夸大。当我们调用一次sess.run,但同时求两个值时,就不会发生这种情况。

我们设计的程序,如第1章中的伪代码和第2章的TF代码,都是单层神经网络,只有一层线性单元。问题来了,多层线性单元表现会更好吗?早期神经网络研究人员认为答案是“否”,下面解释为什么。线性单元可以被看作线性代数矩阵,即我们看到一层前馈神经网络只是计算y = XW。在我们的Mnist模型中,为了将784个像素值转换成10个logit值,W的形状设置为[ 784,10 ],并增加额外的权重来替换偏置项。假设我们又添加了一层线性单元U,其形状为[ 784,784 ],输出到层V中,层VW形状一样,是[ 784,10 ],

(2.1)

(2.2)

其中第2行遵循矩阵乘法的结合律。这里的重点是,使用两层神经网络UV相乘得到的能力,都可以由W= UV的单层神经网络得到。

有一个简单的解决方案——在层与层之间添加一些非线性计算。最常用的一种是tf.nn.relu(或ρ),修正线性单元(rectified linear unit,以下简称relu),定义为

(2.3)

函数图像如图2.6所示。

图2.6 tf.nn.relu的行为

在深度学习中,置于各层之间的非线性函数称为激活函数(activation function)。除了常用的relu以外,其他一些激活函数也活跃于程序中,例如sigmoid函数,定义为

(2.4)

函数图像如图2.7所示。在所有情况下,激活函数都分别应用于张量参数中的各个实数。例如,ρ([1,17, −3 ] ) = [ 1,17,0]。

图2.7 sigmoid函数

在发现relu这种有效简单的非线性函数前,sigmoid函数非常受欢迎。但是sigmoid可以输出的值范围非常有限,只限于从0到1,而relu输出的值可以从0到无穷大。当我们进行反向传递计算梯度找出参数如何影响损失时,这一点非常关键。反向传播时,若使用sigmoid函数会使梯度为0——这个过程被称为梯度消失(vanishing gradient)问题。更简单的激活函数会极大改善这个问题,鉴于此,tf.nn.lrelu——带泄露修正线性单元(leaky relu)——使用非常频繁,因为它比relu可输出的值范围更大,如图2.8所示。

图2.8 lrelu函数

将多层神经网络放在一起,得出新模型。

(2.5)

其中σ是softmax函数,UV是第一层和第二层线性单元的权重,bubv是它们的偏置。

现在我们在TF中进行实现。我们将图2.2第5行和第6行中的Wb的定义替换为图2.9第1行到第4行的层UV,图2.2第11行prbs的计算替换为图2.9的第5行至第7行。这些替换将原代码转换成多层神经网络。此外,考虑到参数数量更多了,我们将学习率降低为。旧程序在100,000张图片上训练后得出的精度稳定在92%左右,新程序在100,000张图片上的精度会达到94%左右。另外,如果我们增加训练图片的数量,测试集的性能会一直提高到大约97%。注意,这个代码和没有非线性函数的代码之间的唯一区别是第6行。如果我们删除它,精度会下降到大约92%。这足以让你相信数学的力量!

图2.9 用于多层神经网络识别数字的TF图构造代码

还有一点需要注意,在具有数组参数W的单层神经网络中,W的形状由输入数量(784)和输出数量(10)固定。对于两层线性单元,我们则可以自由地选择隐藏层大小(hidden size)。所以U是输入大小×隐藏层大小,V是隐藏层大小×输出大小。在图2.9中,我们只是将隐藏层大小设定为784,与输入大小相同,但是这并不是必须的。通常,加大隐藏层会提高性能,但也会有极限。

在本节中,我们将介绍TF的其他方面——有助于完成本书其余部分中提出的编程任务的知识(例如,检查点),或者是在接下来的章节中会用到的知识。

在TF计算中添加检查点(checkpoint)通常很有用——将张量保存下来,以便可以在下一次恢复计算,或者在不同的程序中重新使用该张量。在TF中,我们通过创建和使用saver对象来实现这一点。

saveOb= tf.train.Saver()

如前节所述,saveOb是Python变量,你可以选择名称。在使用对象之前,可以在任意时间创建它,但是由于后文提到的原因,在初始化变量(调用global_variable_initialize)之前创建这个对象更为合理。然后在每n轮训练后,保存所有变量的当前值。

saveOb.save(sess, "mylatest.ckpt")

save函数有两个参数:要保存的会话以及文件名和位置。在上述语句的情况下,保存的目录与Python程序所在目录相同。如果这个参数是tmp/model.checkpt,它就会出现在tmp子目录中。

调用save函数创建了四个文件。最小的文件,名为checkpoint,是一个Ascii文件,指定了在该目录存储检查点的一些高级细节。名称checkpoint是固定的,如果你将某个文件命名为“checkpoint”,它将被覆盖。其他三个文件名会根据你提供的字符串来定义。在本例中,它们被命名为

mylatest.ckpt.data-00000-of-00001
mylatest.ckpt.index
mylatest.chpt.meta

第一个文件保存了参数值。另外两个文件包含TF导入这些值时使用的元信息(稍后将进行描述)。如果你的程序反复调用save,这些文件每次都会被覆盖。

接下来如果我们想在已经训练过的同一个神经网络模型上做进一步的训练,最简单的操作就是修改原来的训练程序。你保留了saver对象,现在我们想用保存的值初始化所有TF变量。因此,我们通常会移除global_variable_initialize,通过调用saver对象的“restore”方法来替换global_variable_initialize

saveOb.restore(sess, "mylatest.ckpt")

下次调用训练程序时,它会恢复训练,TF变量自动设置为上次训练中保存的值,其他一切都没有改变。因此,如果在训练代码时,打印轮数及其对应损失,它会从1开始打印轮数,除非你修改了代码。当然,如果你想调整打印输出,或者想让程序更加优雅,你可以修改代码,但是在这里编写更好的Python代码不是我们要关心的。

tensordot函数是TF中矩阵乘法在张量上的推广。我们对标准矩阵乘法非常熟悉,即前一章中的matmul。当AB具有相同的维度个数nA的最后一个维度与B的倒数第二个维度大小相同,并且前n−2个维度相同时,我们可以调用函数tf.matmul( A,B )。因此,如果A的维度是[ 2,3,4 ],B的维度是[ 2,4,6 ],那么乘积维度是[ 2,3,6 ]。矩阵乘法可以看作重复的点积。例如,矩阵乘法

(2.6)

可以通过将向量( 1,2,3)和( −1, −3, −5 )进行点积,并将答案放在结果矩阵的左上角位置来实现。以这种方式继续运算,第i行与第j列的点积,即为第i行第j列的结果。因此,设A是式(2.6)的第一个矩阵,B是第二个,这个计算也可以表示为

tf.tensordot(A, B, [[ 1 ], [ 0 ]])

前两个参数是进行运算的张量,第三个参数是一个双元素列表:第一个元素是来自第一个参数需要进行点积的维度列表,第二个元素是第二个参数的相应维度列表。这个双元素列表指导tensordot获取这两个维度的点积。当然,如果我们要取它们的点积,指定的维度大小必须相等。由于垂直绘制第0个维度,水平绘制第1个维度,这意味着取A的每一行和B的每一列的点积。tensordot的结果按照从左到右取维度,先取A的剩余的维度后取B的。也就是说,在本例中,输入张量维度为[2,3]和[3,2],在点积中指定的两个维度“消失”了(维度1和维度0),以得到维度为[2,2]的结果。

图2.10给出的例子更为复杂,导致matmul无法在一条指令中处理它。我们将此图从第5章拿过来作为例子(第5章中会解释变量的名字含义),但在本章中,我们只是通过它观察tensordot在做什么。不看数字,只看tensordot函数调用中的第三个参数 [ [ 1 ], [ 0 ] ],即取encOut的1维和wAT的0维的点积。因为他们大小都为4,所以这是可行的。也就是说,我们取两个维度分别为[2,4,4]和[4,3]的张量的点积(斜体数字是进行点积的维度)。由于这些维度在点积之后消失,因此得到的张量具有维度[ 2,4,3 ],当我们在例子最后打印输出时,该张量维度是正确的。简单地说一下实际的计算,我们对两个张量显示为列的维度取点积,即,第一个点积是对[ 1,1,1,−1 ]和[ 0.6,0.2,0.1,0.1 ]进行计算,得出的0.8作为结果张量中的第一个数值。

图2.10 tensordot实例

最后,tensordot不限于在每个张量中进行一维的点积。如果A的维度是[ 2,4,4 ],而B的维度是[ 4,4 ],那么运算tensordot ( A,B,[ [ 1,2 ],[ 0,1 ] ])会得到维度[ 2 ]的张量。

在1.4节中,我们说过,随机初始化神经网络参数(即TF变量)且保证其接近于0是个很好的实践。在第一个TF程序(图2.9)中,我们使用如下命令实现这一操作。

b = tf.Variable(tf.random normal([10], stddev=.1))

其中,我们假设0.1的标准偏差足够“接近0”。

然而,关于标准差的选择自有一套理论和实践体系。这里我们给出了一个名为“Xavier初始化”的规则,它通常用于在随机初始化变量时设置标准差。设ni为层的输入数,no为层的输出数,对于图2.9中的变量W,ni= 784,即像素的数量;no = 10,即备选分类的数量。针对Xavier初始化,设置标准差σ

(2.7)

例如,对于W将值784和10代入,标准差σ约为0.0502,四舍五入为0.1。通常,推荐将标准差设在0.3(10×10层)和0.03(1,000×1,000层)之间。输入和输出值越多,标准差越低。

Xavier初始化最初是为了与sigmoid激活函数一起使用而提出的(见图2.7)。如前所述,当x远低于−2或高于+2时,σ(x)对x几乎毫无反应。也就是说,如果输入sigmoid函数的值太高或太低,它们的变化可能对损失几乎没有影响。进行反向传递时,如果损失的变化被sigmoid函数抵消,那么它不会影响输入sigmoid函数的参数。实际上,我们希望一层的输入和输出之间的比率方差(variance)大约为1。这里我们使用技术意义上的方差:数值随机变量 值和其均值之间平方差的期望值。此外,随机变量X的期望值(expected value)(用E[X]表示)是其可能取值的概率平均值。

(2.8)

以六面骰子为例,滚动一个六面骰子的期望值计算如下:

(2.9)

因此,我们希望将输入方差与输出方差之比保持在1左右,该层不会由于sigmoid函数而对信号过度衰减。这限制了我们初始化的方式。我们传达了一个原始事实(你可以查看推导过程),对于一个权重矩阵为W的线性单元,前向传递的方差(Vf)和反向传递的方差(Vb)分别为

(2.10)

(2.11)

其中σW权重的标准偏差。(单个高斯的方差是(σ2),所以这说得通。)如果我们把VfVb都设为零,然后求解σ可得

(2.12)

(2.13)

除非输入的基数与输出的基数相同,否则这没有解。由于通常情况并非如此,所以我们在这两个值之间取一个“平均值”,得出Xavier规则。

(2.14)

对于其他激活函数,也有等价的方程。随着relu和其他激活函数的出现,而这些激活函数不像sigmoid那样容易饱和,因此这个问题不再像以前那么重要了。尽管如此,Xavier规则确实提供了很好地设置标准偏差的方法,它的TF程序版本和其他语言相关版本都十分常用。

回顾图2.9,可以看到需要7行代码来描述两层前馈网络。可以想想看,如果在没有TF的情况下,我们用Python编程描述这样少的内容会需要多少代码。如果我们用图2.9的方式创建一个8层网络——在本书结尾你需要完成这个任务——将需要大约24行代码。

TF有一组方便的函数,即layers模块,可以更紧凑地对常见的分层情形进行编码。在这里我们介绍

tf.contrib.layers.fully_connected.

如果一层的所有单元都连接到下一层的所有单元,则该层称为完全连接。我们在前两章中使用的层都是完全连接的,因此之前没有将它们和其他网络进行区分。定义这样一个层,我们会做以下工作:(a)创建权重W;(b)创建偏置b;(c)进行矩阵乘法并加和偏置;(d)应用激活函数。假设我们已经执行了import tensorflow.contrib.layers as layers,可以用下面的一行代码来完成定义工作。

layerOut=layers.fully_connected(layerIn,outSz,activeFn)

上述调用创建了一个用Xavier方法初始化的矩阵和一个以零初始化的偏置向量。它返回layerIn乘矩阵再加上偏置的结果,并将activeFn指定的激活函数应用于该结果。如果你没有指定激活函数,它会使用relu;如果你指定None作为激活函数,则不使用激活函数。

使用fully_connected,我们可以将图2.9中的7行代码写为

L1Output=layers.fully_connected(img,756)
prbs=layers.fully_connected(L1Output,10,tf.nn.softmax)

请注意,我们指定tf.nn.softmax作为第二层的激活函数,以作用于第二层的输出。

当然,如果我们有一个100层的神经网络,写出100个fully_connected的调用是非常冗长乏味的。幸运的是,我们可以使用Python或者TF API来定义我们的网络。举一个想象中的例子,假设我们想要创建100个隐藏层,每一层比前一层小1,其中第一层的大小是一个系统参数。我们可以写出

outpt = input
for i in range(100):
    outpt = layers.fully_connected(outpt, sysParam - i)}

这个例子很傻,但反映了很重要的一点:TF图的部分可以像列表或字典一样在Python中传递和操作。

Tensorflow起源于谷歌内部项目——谷歌大脑,这个项目由两名谷歌的研究人员Jeff Dean和Greg Corrado以及斯坦福大学教授Andrew Ng发起。开始时,该项目被称为“Distbelief”,当它的应用超越了初始项目时,谷歌正式接管了进一步的开发,并聘请了多伦多大学的Geoffrey Hinton,我们在第1章中提到了他对深度学习的开创性贡献。

Xavier初始化来源于Xavier Glorot的名字。他以第一作者的身份撰写了介绍Xavier初始化的文章[GB10]。

如今,Tensorflow只是深度学习的编程语言之一(参见文献[Var17])。就用户数量而言,Tensorflow是迄今为止最受欢迎的语言。第二位是Keras,一种建立在Tensorflow之上的高级语言。第三位是Caffe,最早是由加州大学伯克利分校开发的。Facebook现在支持Caffe的开源版本Caffe2。Pytorch是Torch的Python接口,它在深度学习自然语言处理社群十分受欢迎。

练习2.1 如果在图2.5中,我们计算tf.reduce_sum(A),其中A是图左侧的数组,结果会是怎样的?

练习2.2 从图2.2中取出第14行并将其插入第22行和第23行之间(循环如下),会产生什么问题?

for i in range(1000):
   imgs, anss = mnist.train.next_batch(batchSz)
   train = tf.train.GradientDescentOptimizer(0.5).minimize(xEnt)
   sess.run(train, feed_dict={img: imgs, ans: anss})

练习2.3 下面是图2.2中第21行到第23行代码的另一个变体,它有没有问题?如果有问题,是什么问题?

for i in range(1000):
   img, anss= mnist.test.next_batch(batchSz)
   sumAcc+=sess.run(accuracy, feed_dict={img:img, ans:anss})

练习2.4 在图2.10中,以下操作输出的张量形状是什么?

   tensordot(wAT, encOut, [[0],[1]])

并给出解释。

练习2.5 展开计算过程,确认图2.10的例子最后打印输出的张量中第一个数字( 0.8 )是正确的(精确到三位小数)。

练习2.6 假设input的形状为[50,10],以下代码创建了多少TF变量?

   O1 = layers.fully connected(input, 20, tf.sigmoid)

创建的矩阵中变量的标准偏差是多少?


相关图书

深度学习高手笔记 卷2:经典应用
深度学习高手笔记 卷2:经典应用
深度学习详解
深度学习详解
ChatGPT原理与应用开发
ChatGPT原理与应用开发
深度学习的数学——使用Python语言
深度学习的数学——使用Python语言
人工智能和深度学习导论
人工智能和深度学习导论
动手学深度学习(PyTorch版)
动手学深度学习(PyTorch版)

相关文章

相关课程