深度学习的数学——使用Python语言

978-7-115-60777-5
作者: [美]罗纳德·T.纽塞尔(Ronald T. Kneusel)
译者: 辛愿
编辑: 郭泳泽

图书目录:

详情

深度学习是一门注重应用的学科。了解深度学习背后的数学原理的人,可以在应用深度学习解决实际问题时游刃有余。本书通过Python代码示例来讲解深度学习背后的关键数学知识,包括概率论、统计学、线性代数、微分等,并进一步解释神经网络、反向传播、梯度下降等深度学习领域关键知识背后的原理。 本书适合有一定深度学习基础、了解Pyho如编程语言的读者阅读,也可作为拓展深度学习理论的参考书。

图书摘要

版权信息

书名:深度学习的数学—使用Python语言

ISBN:978-7-115-60777-5

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

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

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

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

版  权

著    [美]罗纳德•T.纽塞尔(Ronald T. Kneusel)

译    辛 愿

责任编辑 郭泳泽

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

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

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

读者服务热线:(010)81055410

反盗版热线:(010)81055315

内容提要

深度学习是一门注重应用的学科。了解深度学习背后的数学原理的人,可以在应用深度学习解决实际问题时游刃有余。本书通过Python代码示例来讲解深度学习背后的关键数学知识,包括概率论、统计学、线性代数、微分等,并进一步解释神经网络、反向传播、梯度下降等深度学习领域关键知识背后的原理。

本书适合有一定深度学习基础、了解Python编程语言的读者阅读,也可作为拓展深度学习理论的参考书。

推荐序

在过去的十几年里,人工智能技术突飞猛进,在多个领域产生了深远的影响:语音识别技术已经达到大多数人觉得语音输入准确度足够高的地步;语音合成技术已经可以合成真假难辨的自然语音,并在人机对话、有声书制作、新闻播报等场景中获得广泛应用;图像识别技术在图像分类和图像识别任务上的性能超越了普通人;图像和视频合成技术则已经可以合成风格各异的虚拟数字人,成为元宇宙的重要组成部分。尤其让人印象深刻的是,人工智能软件AlphaGo在2016年战胜了围棋冠军,AlphaFold2在2022年解析了大量蛋白质的结构。这些任务之前一直被认为极其复杂,需要极高的智能和很多年的努力才能解决。而所有这些进展在很大程度上要归功于人工智能的一个重要分支——深度学习的发展。

今天,计算机和人工智能方向的从业者可能大多听说过深度学习,很多从业者或多或少使用过深度学习的技术或模型。不过,由于开源逐渐成为一种风气,越来越多的技术使用者广泛依赖于开源框架(比如TensorFlow和PyTorch)及各种开源算法。这一方面大大减少了“重复发明轮子”的情况,加速了技术的演进和应用推广,另一方面也使得大家更容易忽视底层技术。

本书就是为希望更了解深度学习底层数学基础的朋友们准备的。与其他数学书不同,本书围绕深度学习展开,阐述了深度学习背后的核心数学概念,包括统计学、线性代数、微分等,并且包含了很多人容易忽略的矩阵微分。另外,本书的示例是以Python代码而不是严格理论证明的形式展开的,这使得它们特别适合深度学习的从业者(特别是初学者)使用,尤其是那些希望通过学习底层数学知识来更好地了解深度学习原理,从而改进训练算法和模型的朋友。

本书的作者是机器学习方向的博士,有20年左右的工业界从业经验,曾经出版了多部与计算机和机器学习相关的图书。他的书大多从实用的角度出发,简单易懂。本书的译者也有丰富的人工智能从业经验,并曾翻译《贝叶斯方法》。他的译文忠实原文而又符合中文习惯,读起来非常顺畅。我相信本书一定可以帮到很多希望在深度学习领域深入学习而不是仅仅使用开源模型的朋友。

俞 栋    

2022年8月13日于西雅图

译者序

正如本书作者罗纳德·T. 纽塞尔博士所言,“人工智能已无处不在”。自从2011年微软提出的基于DNN的深度神经网络取得语音识别领域的重大突破,人工智能的发展便一发不可收:AlexNet在计算机视觉领域开启新纪元,RNN和Transformer重新定义了对自然语言的理解,强化学习模型击败围棋大师的新闻家喻户晓……这一切都离不开过去十多年深度学习的飞速发展。

想要在深度学习这一研究领域取得突破,理解其背后数学原理至关重要。2021年11月,纽塞尔博士出版了这本面向深度学习从业人员的数学基础教材。该书一经推出便广受好评。对于想要从事人工智能领域或使用深度学习技术解决手头问题,但缺乏数学基础,亦不知如何以最小代价补齐数学短板的人而言,这本书无疑能提供巨大的帮助。毕竟,实际任务的复杂性、数据分布的多样性、模型训练过程的不确定性、目标函数的灵活性等,都可能导致实际项目的失败。

纽塞尔博士拥有20年左右的机器学习从业经验,甚至在风靡一时的AlexNet提出之前就已经开始从事深度学习的研究。在翻译这本书的过程中,我能深刻体会到作者在这一领域拥有的扎实理论功底和丰富实战经验,这也使本书既不会过于深奥而导致门槛过高,又充分保留了重要细节以确保读者能真正理解关键内容。更难能可贵的是,本书中的代码示例设计非常精巧,既能让读者快速理解抽象的数学概念,又有极强的可用性,能直接在实际工作中参考。

本书的主要内容可分为两部分。第一部分主要介绍理解深度学习所必备的如下数学基础。

概率论与统计学的核心内容。实际上,传统机器学习就基于统计学的数据模型和求解算法,而现代深度学习中也保留了大量的机器学习思想,例如变分自编码器中关于变分推断的思想,以及生成对抗网络中关于分布拟合的技术等。

线性代数的关键知识点。这些内容是开启深度学习技术大门的一把钥匙。神经网络是一系列矩阵运算和非线性映射的组合,理解线性代数和矩阵分析的本质,才更容易洞察神经网络的工作机制。

关于微分尤其是函数求导和矩阵微分的知识。这些内容对于理解网络如何利用梯度的反向传播进行参数更新至关重要。理解了它们,才能够根据需要灵活对目标函数进行优化或设计。

本书第二部分介绍深度神经网络的工作机制以及网络得以成功训练的关键技术,包括神经网络数据流、反向传播机制和梯度下降算法。

神经网络数据流描述了输入网络的数据如何一步步到达输出。作者既阐述了传统网络的情况,也介绍了卷积网络中卷积层、池化层和全连接层的工作机制。

在关于反向传播机制的内容中,作者介绍了如何通过数据图模型,根据微分的链式法则,对目标函数进行参数优化。掌握这些内容后,读者甚至可以自己实现一个简单的底层网络。

关于梯度下降算法,作者介绍了SGD、Adam等深度学习中解决无约束优化问题的经典核心算法。深刻理解这些算法能帮助读者在实战中快速定位问题,让网络得以正常训练。

我非常感谢人民邮电出版社的王峰松先生邀请我负责本书的中文翻译,也非常感谢郭泳泽先生后期对本书的精心修改和编排。在翻译本书的过程中,我也时常在字里行间感受到作者的认真负责,以及对问题的深入思考,这些都促使我将本书真实、完整地呈现给中文读者。然而,凡事总有疏漏,如果读者发现本书的翻译未能准确传达作者本意,我恳请得到来自读者的更正,也会在未来的修订版本中不断完善,力求完美。

辛 愿       

2022年8月8日于深圳腾讯滨海大厦

人工智能(Artificial Intelligence,AI)已无处不在。不信的话,掏出你口袋里的智能手机,一切便不证自明——我们的手机能提供基于人脸识别的安全服务,能识别简单的语音指令,能在人像模式下自动模糊背景,还能偷偷学习我们的喜好以提供个性化服务。通过分析海量数据,AI模型可以创造疫苗,可以改进机械操作,可以创造自动驾驶车辆,可以利用量子计算的强大算力,甚至当你在网络上下棋的时候,可以自动匹配与你棋艺相当的对手与你对决。整个工业界都在转型,只为更好地让本领域的专业知识,能与最前沿的AI技术结合发挥价值。而学术界也不甘落后,如今各学位的课程,都会尽量涉及人工智能的相关概念。机器驱动的认知自主时代正在到来,此刻我们所有人都已是AI的消费者。你若还对AI的发展感兴趣,就有必要理解是哪项AI技术在过去数十年得到了显著提升,那就是深度学习。深度学习是机器学习的一个分支领域,它能够利用极深的神经网络对复杂问题建模,而这些复杂问题往往难以用传统的分析模型来解决。AI技术早在20世纪50年代就已经被艾伦·图灵提出,但正是人们对深度神经网络的改进才导致近来AI的重大发展。如果说深度学习是AI发展的关键引擎,那么深度学习本身的引擎是什么呢?

深度学习的核心概念涉及自然科学、工程技术和数学。各家公司一直在试图给出其正式定义,但难以涵盖方方面面,以至于当他们想招聘该领域头部人才的时候,只好将职位要求描述得非常宽泛。与此类似,这一领域的学术课程,往往需要跨不同学科,才能让学生习得所需的技能。尽管在实战中,运用深度学习技术需要跨不同领域的学科知识,但其核心仍建立在数学理论的基础上,包括概率论、统计学、线性代数和微分。至于对这些数学基础理论要掌握和理解到什么程度,就要看你希望对深度学习技术精通到何种程度了。

本书致力于为深度神经网络的工作人员在实施算法的过程中遇到的各种挑战提供解决方案。他们通常遇到的挑战在于如何有效地利用现有方案解决问题,比如去哪里找寻源代码、如何设置工作环境来运行代码、如何进行单元测试,以及最终如何用业务数据训练模型来解决实际问题。这些深度神经网络可能有数千万甚至上亿的参数需要学习,而且即便是精通算法的研究员,也需要在有充足训练样本的情况下,通过精细化的调参才能实现有效优化,达到对数据的良好表征。初次(第二次、第三次也一样)实现模型的时候,他们通常会经历痛苦的网络最优结构的搜索过程,而只有具备对底层数学原理的高水平理解的人才能胜任这些工作。

而当算法人员开始对整个方案进行整合的时候,他们就要进一步提高专业度,不仅要熟悉本领域的知识,也要理解深度学习的底层基础模块。此时,他们所面临的挑战将不只是简单的算法实现,而且需要运用核心概念对目标领域的问题建模。挑战再次降临!他们可能面临梯度爆炸的问题,也可能为了更好地对问题建模而不得不修改损失函数,却又发现损失函数不可微(也就无法进行梯度计算),抑或在训练模型的时候发现优化算法效率太低。本书为这些人填补了空白。通过清楚地阐述深度学习所需的核心数学概念,本书可以帮助他们解决这些困难。

对核心概念理解到一定程度以后,开发者不仅能对算法进行整合,也能对算法提出创新。一旦有了创新,往往就需要对成果进行传播。这时候,开发者可能要离开开发岗位,花时间去做一些宣传、展示甚至很多具有教育性质的工作。他们还可能需要一本随时可以参考的手册,以便时常温习AI领域能取得进展所依赖的核心理论基础,同时提升个人影响力。

由于以上方方面面的因素,深度学习的开发人员在工作中需要涉及的知识体系极为庞大,但每个知识点通常又只是解决特定问题,如果不加以约束,开发者将难以聚焦于需要关注的问题点。纽塞尔博士在使用机器学习和深度学习解决图像生成和分析问题方面拥有15年以上的从业经验,他想通过本书强调并巩固读者最需要掌握的核心技能——运用神经网络解决问题所必备的核心数学基础。当然,没有任何一本书可以包罗万象,本书着眼于运用 AI 技术所需数学技能的概述型描述,如果需要探究统计学、线性代数、微分等学科的更深层知识,请参考其他资料以获得所需内容。

德里克·J.瓦尔伍尔德(Derek J. Walvoord)

前  言

如今,数学的地位举足轻重,而深度学习也在日渐扮演重要角色。从无人驾驶汽车敢于给出的安全允诺,到比顶尖骨科医生还厉害的骨科诊断医疗系统,再到越发强大甚至引起担忧的语音助手,深度学习已经无处不在。

本书涵盖要理解深度学习所必须掌握的数学知识。当然,你确实可以利用现成的组件,在完成好相应设置并准备好Python代码以后,就对数据进行处理并完成模型训练任务,而无须理解自己在做什么,更不用理解背后的数学理论。而且,由于深度学习的强大,你往往能成功地训练一个模型。但是,你并不理解自己为什么能成功,也不该就此而满足。想搞明白原因,就需要学习数学。虽然用不着大量的数学知识,但一定的数学功底还是必需的。具体来说,你需要理解与概率论、统计学、线性代数和微分相关的一些理论知识,而这些知识刚好就是本书所要讨论的内容。

本书面向谁

本书不是一本深度学习的入门教材,也不会教你深度学习的基础知识,而是此类书籍的有效补充[你可以参考我的另一本书:Practical Deep Learning: A Python-Based Introduction(No Starch Press, 2021)]。虽然本书假设你对深度学习的核心概念已经比较熟悉,但我还是会在讲解的过程中做必要的解释说明。

此外,本书假设你具备一定的数学基础,包括高中数学知识,尤其是代数知识。本书还假设你对Python、R或其他类似编程语言较为熟悉。本书使用Python 3进行讲解,书中会用到它的一些主流组件,如NumPy、SciPy以及scikit-learn。

对于其他方面,本书就不作要求了,毕竟本书的目的就是给你提供成功使用深度学习所必备的知识。

关于本书

这虽然是一本关于数学的书,但其中不会有大量公式证明和练习题,我们主要通过代码来阐述各种概念。深度学习是一门应用学科,所以你需要在实践中理解其内涵。我们将用代码填补数学理论和应用实践之间的空白。

本书内容安排有序,首先介绍基础理论,然后引出更高级的数学内容,最后用实际的深度学习算法让你将之前掌握的内容融会贯通。建议你按照书中的内容顺序阅读,如果遇到已经非常熟悉的内容,你可以直接跳过。

1章:搭建舞台

该章对工作环境以及深度学习中的常用组件进行配置。

2章:概率论

概率论影响深度学习的方方面面,它是理解神经网络训练过程的关键。作为本书概率论的前半部分,该章介绍该领域的基础知识点。

3章:概率论进阶

单靠一章难以覆盖重要的概率论的全部内容,该章继续探索概率论中与深度学习相关的知识点,包括概率分布和贝叶斯定理。

4章:统计学

统计学对理解数据和评估模型非常重要,而且概率论也离不开统计学,要理解深度学习,就不得不理解统计学。

5章:线性代数

线性代数是一门关于向量和矩阵的学科,而深度学习就以线性代数为核心。实现神经网络本身就是在运用向量和矩阵进行运算,所以理解相关概念和运算方法非常关键。

6章:线性代数进阶

该章继续讨论线性代数知识,内容聚焦于矩阵的相关核心内容。

7章:微分

或许训练神经网络的最核心理论基础就是梯度。要想理解和使用梯度,就必须掌握如何对函数求导。该章介绍求导和梯度的理论基础。

8章:矩阵微分

在深度学习中,求导往往是针对向量和矩阵进行的。该章把导数的概念扩展到这些对象上。

9章:神经网络中的数据流

要想理解神经网络如何对向量和矩阵进行运算,就必须理解数据在神经网络中是如何流转的。该章讨论这些内容。

10章:反向传播

成功训练神经网络离不开两个关键算法:反向传播和梯度下降。该章通过介绍反向传播,帮助你对前面所学知识加以应用。

11章:梯度下降

梯度下降使用反向传播过程中计算得出的梯度来训练神经网络。该章从简单的一维函数开始探讨梯度下降,一步步讲到全连接网络的情况。除此之外,该章还会介绍并对比梯度下降的各种变体。

附录:学无止境

本书虽然略过了概率论、统计学、线性代数和微分中的很多知识点,但附录部分会给你提供进一步学习相关领域的资源。

本书的配套代码可以从GitHub网站的rkneusel9/MathForDeepLearning库中下载。现在就让我们开始学习吧!

资源与支持

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

配套资源

本书提供配套代码。

请在异步社区本书页面中单击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

提交勘误

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

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

扫码关注本书

扫描下方二维码,您将会在异步社区微信服务号中看到本书信息及相关的服务提示。

与我们联系

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

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

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

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

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

关于异步社区和异步图书

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

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

异步社区

微信服务号

第1章 搭建舞台

传统数学课本通过习题的方式让读者掌握知识点,本书则希望通过动手实验来达到这一目的。书中会提供足够多的实验内容。读者无须准备纸笔,但需要尝试实现代码。

本章帮读者配置工作环境。全书代码运行于Linux操作系统,具体采用的发行版为Ubuntu 20.04,不过大多数代码也能在新版Ubuntu和其他Linux发行版上运行。为了完整性考虑,我还会提供macOS和Windows环境下的配置方法。但我要指出,深度学习最理想的工作环境是Linux,当然大多数情况下用macOS也可以。不建议使用Windows,因为很多深度学习组件的移植版在Windows下的维护性较差,尽管最近这个问题逐渐开始好转。

我会首先介绍一下需要安装哪些软件包,然后带领读者快速浏览一下适用于Python 3的NumPy库,因为对几乎所有使用Python进行科学计算的人来说,NumPy都是基础工具。接下来我会介绍SciPy,它也是很多科学计算的必备工具,但我在此只做简要说明。最后我会谈一下有关scikit-learn的话题,它通常简称为sklearn。scikit-learn非常有用,它实现了很多传统的机器学习模型。

全书将借助可执行的示例代码来阐述概念。所有的例子都需要先执行以下代码:

import numpy as np

另外,有些示例会引用之前章节中出现过的代码。但是因为所有的例子都很简短,运行多个示例也不会占用多少资源,所以建议将每一章的各个示例都运行在单个Python会话中。当然,这也不是必需的。

1.1 组件安装

本节的最终目标是完成以下组件的安装,各个组件的版本最低要求如下:

Python 3.8.5;

NumPy 1.17.4;

SciPy 1.4.1;

matplotlib 3.1.2;

sklearn 0.23.2。

如果版本更新,通常也可以。

下面我们快速看一下在不同的操作系统中如何安装这些组件。

1.1.1 Linux

在以下代码中,$表示Linux终端的命令符,而>>>则表示Python解释器的命令符。

如果你的计算机上安装的是Ubuntu 20.04,那么系统将自带Python 3.8.5。

可执行以下代码来查看操作系统版本号:

$ cat /etc/os-release

执行python3将启动Python解释器,执行python将默认启动Python 2.7。

可执行以下代码来安装NumPy、SciPy、matplotlib和sklearn:

$ sudo apt-get install python3-pip
$ sudo apt-get install python3-numpy
$ sudo apt-get install python3-scipy
$ sudo pip3 install matplotlib
$ sudo pip3 install scikit-learn

启动Python 3,可通过导入numpy、scipy、matplotlib和sklearn模块来验证这些组件是否安装成功。然后调用__version__,确保安装的版本高于最低要求,例如:

>>> import numpy; numpy.__version__
'1.17.4'
>>> import scipy; scipy.__version__
'1.4.1'
>>> import matplotlib; matplotlib.__version__
'3.1.2'
>>> import sklearn; sklearn.__version__
'0.23.2'

1.1.2 macOS

在Mac上安装Python 3,需要去Python官方网站下载macOS对应的最新版Python 3。在我写本书的时候,Python 3的最新版本是3.9.2。下载后安装即可。

安装完成后,打开终端,确保安装成功:

$ python3 --version
Python 3.9.2

在安装完Python 3以后,就可以在终端直接用pip3来安装组件了。

$ pip3 install numpy --user
$ pip3 install scipy --user
$ pip3 install matplotlib --user
$ pip3 install scikit-learn --user

最后,你可以在Python 3中查看所安装组件的版本号。首先在终端执行python3以启动Python,然后导入NumPy、SciPy、matplotlib和sklearn模块并输出它们的版本号,以确保安装的版本符合最低要求。

1.1.3 Windows

在Windows 10中,可按照以下步骤安装Python 3和各个组件。

(1)访问Python官方网站并单击页面上的Downloads和Windows。

(2)在页面的底部选择x86-64对应的可执行程序。

(3)运行安装包,在安装过程中选择默认选项。

(4)选择Install for All Users和Add Python to the Windows PATH,这一点很重要。

在按照上面的步骤完成安装后,系统会自动添加Python到PATH环境变量中,因此你可以直接在命令行中运行Python。打开命令行(按Windows + R快捷键并输入cmd),执行python命令。如果一切顺利,你会看到Python的交互命令符>>>。我这里安装的版本是Python 3.8.2。注意在Windows中,退出Python对应的快捷键是Ctrl+Z而不是Ctrl+D。

Python安装程序会自动帮我们安装pip。这样我们就可以直接在命令行中使用pip来安装所依赖的组件了。可通过在提示符后执行以下命令来安装NumPy、SciPy、matplotlib和sklearn。

> pip install numpy
> pip install scipy
> pip install matplotlib
> pip install sklearn

在这里,各个组件的版本分别是NumPy 1.18.1、SciPy 1.4.1、matplotlib 3.2.1和sklearn 0.22.2,它们全部满足最低版本要求。

要验证安装是否成功,可在命令行中启动Python,尝试导入numpy、scipy、matplotlib和sklearn模块。如果没有报错,就说明安装成功。接下来就要写Python代码了,你可以选择任何你熟悉的编辑软件,或直接使用记事本。

在完成这些组件的安装后,我们就准备好进入下一步了。下面我们先来快速熟悉一下已经安装的这些组件。虽然全书有很多示例会用到这些组件,但阅读一下我建议的文档还是值得的。

1.2 NumPy

前面已经安装了NumPy。现在我来介绍一下NumPy的一些基本概念和运算方法。你如果有兴趣,也可以自行查找完整的技术手册。

启动Python,尝试执行以下代码:

>>> import numpy as np
>>> np.__version__
'1.16.2'

第一行代码导入numpy模块并将其重命名为np。这种用简称来重命名模块的方式虽然不是必需的,却几乎成了通用做法。第二行代码则输出版本号,以确保安装的NumPy版本满足前面所说的最低要求。

1.2.1 定义数组

NumPy以数组为运算对象,它可以方便地将列表转换为数组。想想看,与C和Java等语言中的数组类型相比,Python中的列表类型虽然使用起来非常优雅,但是当使用列表模拟数组进行科学计算的时候,效率还是非常低的。NumPy在这方面很有优势,使用NumPy的数组类型时实际上效率很高。下面的例子首先把列表转换为数组,然后展示了一些数组属性:

>>> a = np.array([1,2,3,4])
>>> a
array([1, 2, 3, 4])
>>> a.size
4
>>> a.shape
(4,)
>>> a.dtype
dtype('int64')

上面的例子将一个包含4个元素的列表传给了np.array函数,得到一个NumPy数组。数组最基本的属性包括size和shape。这个数组的size属性为4,表示包含4个元素;shape属性则是包含4的一个元组,表示这是一个包含4个元素的一维数组或者说一维向量。如果数组是二维的,那么其shape属性将包含两个元素,分别对应每一维的大小。在下面的例子中,数组b的shape属性为(2, 4),这表示它是一个2行4列的数组。

>>> b = np.array([[1,2,3,4],[5,6,7,8]])
>>> print(b)
[[1 2 3 4]
 [5 6 7 8]]
>>> b.shape
(2, 4)

1.2.2 数据类型

Python中的数据类型大体分为两种:取值几乎可以是任意大小的整型以及浮点型。NumPy数组则支持更多的数据类型。由于NumPy的底层是用C语言实现的,因此NumPy支持C语言中所有的数据类型。前面的例子向np.array函数传入了一个各元素为整数的列表,结果得到一个各元素为64位有符号整数的数组。表1-1展示了NumPy支持的数据类型。我们既可以让NumPy替我们选择数据类型,也可以显式指定数据类型。

表1-1 NumPy支持的数据类型,C语言中等价的数据类型以及取值范围

NumPy支持的数据类型

C语言中等价的数据类型

取值区间

float64

double

±[2.225×10–308, 1.798×10308]

float32

float

±[1.175×1038, 3.403×1038]

int64

long long

[–263, 263−1]

uint64

unsigned long long

[0, 264−1]

int32

long

[–231, 231−1]

uint32

unsigned long

[0, 232−1]

uint8

unsigned char

[0, 28−1]

我们来看一些为数组指定类型的例子:

>>> a = np.array([1,2,3,4], dtype="uint8")
>>> a.dtype
dtype('uint8')
>>> a = np.array([1,2,3,4], dtype="int16")
>>> a = np.array([1,2,3,4], dtype="uint32")
>>> b = np.array([1,2,3,4.0])
>>> b.dtype
dtype('float64')
>>> b = np.array([1,2,3,4.0], dtype="float32")
>>> c = np.array([111,222,333,444], dtype="uint8")
>>> c
array([111, 222, 77, 188], dtype=uint8)

在上面的例子中,数组a的元素为整型,而数组b的元素为浮点型。注意在第一个关于数组b的例子中,Python自动为数组b的元素选择了64位浮点型。之所以会这样,是因为输入的列表里有一个浮点数4.0。

关于数组c的例子看起来似乎是错误的,其实不然。如果给定的数据超出指定类型的表示范围,NumPy并不会报错。在这个例子中,我们指定的8位整型只能表示[0, 255]取值范围内的整数。前面的两个数111和222属于这个范围;但后面两个数333和444都太大,NumPy默认只保留这两个数的最后8位,分别是77和188。NumPy用这个例子给我们上了一课,让我们明白了该指定什么数据类型。虽然这类问题不常出现,但我们仍须牢记于心。

1.2.3 二维数组

如果说把列表转换为数组后得到的是一维向量,那么我们可以猜测,如果把一个列表的列表转换为数组,那么得到的将是一个二维向量。事实的确如此,我们猜对了。

>>> d = np.array([[1,2,3],[4,5,6],[7,8,9]])
>>> d.shape
(3, 3)
>>> d.size
9
>>> d
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

可以看到,一个由三个子列表构成的列表被映射成了一个3 × 3的向量(即矩阵)。由于NumPy数组从0开始对元素编号,因此引用d[1,2]返回的是6。

1.2.4 全0数组和全1数组

NumPy有两个非常有用的函数:np.zeros和np.ones。它们都用于定义指定大小的数组。前者用0作为数组全部元素的初始值,而后者则将数组元素全部初始化为1。这是NumPy从头创建数组的主要方式。

>>> a = np.zeros((3,4), dtype="uint32")
>>> a[0,3] = 42
>>> a[1,1] = 66
>>> a
array([[ 0,  0, 0, 42],
       [ 0, 66, 0,  0],
       [ 0,  0, 0,  0]], dtype=uint32)
>>> b = 11*np.ones((3,1))
>>> b
array([[11.],
       [11.],
       [11.]])

这两个函数的第一个参数都是元组,用于指定各个维度的大小。如果传入标量,那么默认定义的是一个一维向量。以数组b为例,它是初始值为1、大小为3 × 1的数组,通过与标量11相乘,可以使数组b的每个元素都为11。

1.2.5 高级索引

上面的例子介绍了访问单个元素的简单索引方式。NumPy支持更复杂的索引形式,常用的一种就是用单个索引查询整个子数组,举个例子:

>>> a = np.arange(12).reshape((3,4))
>>> a
array([[ 0, 1,  2,  3],
       [ 4, 5,  6,  7],
       [ 8, 9, 10, 11]])
>>> a[1]
array([4, 5, 6, 7])
>>> a[1] = [44,55,66,77]
>>> a
array([[ 0,  1,  2,  3],
       [44, 55, 66, 77],
       [ 8,  9, 10, 11]])

这个例子用到了np.arange函数,它等价于Python中的range函数。注意这里使用reshape方法将一个大小为12的一维向量转换成了一个3 × 4的矩阵。另外请注意,a[1]返回的是整个子数组,索引是从第一维开始进行的。a[1]其实是a[1, :]的简化形示,其中的“:”表示某一维的全部元素。这种简化形式也可以用于赋值操作。

NumPy还支持Python列表的所有切片索引方式,继续上面的例子:

>>> a[:2]
array([[ 0,  1,  2,  3],
       [44, 55, 66, 77]])
>>> a[:2,:]
array([[ 0,  1,  2,  3],
       [44, 55, 66, 77]])
>>> a[:2,:3]
array([[ 0,  1,  2],
       [44, 55, 66]])
>>> b = np.arange(12)
>>> b
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
>>> b[::2]
array([ 0, 2, 4, 6, 8, 10])
>>> b[::3]
array([0, 3, 6, 9])
>>> b[::-1]
array([11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0])

首先,a[:2]返回数组a中前两行的全部元素,这里隐含了用“:”索引第二维元素,a[:2]与a[:2,:]等价。关于数组a的第三个例子对两个维度都进行了索引,通过a[:2, :3]返回了数组a的前两行和前三列元素。关于数组b的例子展示了如何每隔两个或三个元素进行查询。最后一个例子非常有用,这个例子使用一个负的增量实现了索引的倒序。当增量为−1时,表示对所有元素倒序排列。如果增量为−2,则表示以倒序每隔一个元素进行一次查询。

NumPy使用“:”来表示查询某一维的全部元素。NumPy还支持用英文省略号来表示“尽可能多的‘:’符号”。为了举例说明,下面我们先定义一个三维数组:

>>> a = np.arange(24).reshape((4,3,2))
>>> a
array([[[ 0,  1],
        [ 2,  3],
        [ 4,  5]],
       [[ 6,  7],
        [ 8,  9],
        [10, 11]],
       [[12, 13],
        [14, 15],
        [16, 17]],
       [[18, 19],
        [20, 21],
        [22, 23]]])

可以把数组a看成4个3 × 2的子数组。如果要更新其中的第2个子数组,我们可以这么做:

>>> a[1,:,:] = [[11,22],[33,44],[55,66]]
>>> a
array([[[ 0,  1],
        [ 2,  3],
        [ 4,  5]],
       [[11, 22],
        [33, 44],
        [55, 66]],
       [[12, 13],
        [14, 15],
        [16, 17]],
       [[18, 19],
        [20, 21],
        [22, 23]]])

这里我们显式地用“:”进行各个维度的索引,可以看到“:”的使用让NumPy很有兼容性,NumPy能够自动将列表的列表识别为数组并且执行相应的更新操作。接下来我们可以看到,用英文省略号也可以实现同样的效果。

>>> a[2,...] = [[99,99],[99,99],[99,99]]
>>> a
array([[[ 0,  1],
        [ 2,  3],
        [ 4,  5]],
       [[11, 22],
        [33, 44],
        [55, 66]],
       [[99, 99],
        [99, 99],
        [99, 99]],
       [[18, 19],
        [20, 21],
        [22, 23]]])

这里对第3个3 × 2的子数组也进行了更新。

1.2.6 读写磁盘

NumPy数组可以通过调用np.save写到磁盘上,并通过调用np.load从磁盘上加载,比如:

>>> a = np.random.randint(0,5,(3,4))
>>> a
array([[4, 2, 1, 3],
       [4, 0, 2, 4],
       [0, 4, 3, 1]])
>>> np.save("random.npy",a)
>>> b = np.load("random.npy")
>>> b
array([[4, 2, 1, 3],
       [4, 0, 2, 4],
       [0, 4, 3, 1]])

我们先用np.random.randint创建了一个大小为3 × 4,元素取值在0和5之间(包含0和5)的随机整型数组(NumPy有很多关于随机数的函数)。然后我们将该数组写到磁盘上,命名为random.npy。数组文件必须以“.npy”为后缀,如果没有指定,NumPy会自动添加该后缀。最后,我们通过调用np.load从磁盘上加载了保存的NumPy数组。

本书还会涉及其他的NumPy函数,我会在遇到时详细介绍这些函数。接下来,我们快速了解一下SciPy。

1.3 SciPy

SciPy为Python添加了大量的函数库。由于SciPy在后端使用NumPy,因此通常这两个组件需要一起安装。关于SciPy的完整指南,可以查阅其官方文档。

在本书中,我们主要聚焦于scipy.stats模块。启动Python,执行如下代码:

>>> import scipy
>>> scipy.__version__
'1.2.1'

以上代码将加载SciPy并方便你确认其版本是否符合最低要求,通常更新的版本在本书中也适用。

下面做几个简单的小实验。

>>> from scipy.stats import ttest_ind
>>> a = np.random.normal(0,1,1000)
>>> b = np.random.normal(0,0.5,1000)
>>> c = np.random.normal(0.1,1,1000)
>>> ttest_ind(a,b)
Ttest_indResult(statistic=-0.027161815649563964, pvalue=0.9783333836992686)
>>> ttest_ind(a,c)
Ttest_indResult(statistic=-2.295584443456226, pvalue=0.021802794508002675)

我们首先加载NumPy,然后从SciPy的stats模块中导入ttest_ind函数。该函数能够接收两个集合作为参数,比如来自两个类别的数值集合,然后回答如下问题:这两个集合的均值是否相同?或者更准确地说,回答我们有多大把握相信这两组数据来自同样的生成过程。解决这个问题的一种经典算法就是t检验(t-test)。评估方式则是看结果中的p值,即pvalue一项。你可以把p值理解为,如果两组数据来自同样的生成过程,那么这两组数据的均值差有多大概率与我们观测的结果一致。如果概率接近1,则可以基本相信这两组数据的生成过程相同。

a、b和c是一维数组,其中的元素(有1000个值)都取自高斯曲线(也称为正态曲线)。我们会在后面详细说明数据的生成过程,这里你只需要理解每个数值都取自一条钟形曲线,这种曲线中间的部分相比边缘更有可能被取样。normal函数的前两个参数分别定义均值和标准差,后者用于衡量曲线的宽度。也就是说,标准差越大,曲线的形状越扁平,曲线越宽。

在这个例子中,我们预期a和b是十分相似的两组数据,因为它们的均值都是0.0,只不过它们取样的钟形曲线在形状上存在稍许差异。相比而言,c的均值为0.1。我们希望t检验算法的结果能够体现这两者之间的差异,也就是告诉我们a和c不大可能来自同样的生成过程。

函数ttest_ind执行t检验并输出包含pvalue一项的信息。正如我们所料,输入a和b,得到的p值约为0.98。这表示如果两组数据来自同一生成过程,那么这两组数据的均值差异有98%的概率与我们给出的数据一致。相比而言,当输入a和c时,得到的p值约为0.027。这表示如果两组数据的生成过程相同,那么它们的均值差异只有3%的概率符合我们给出的数据。因而我们可以推断a和c来自不同的生成过程。我们称这两组数据具有统计显著性差异。

传统上,我们认为p值小于0.05就具有统计显著性了。然而使用这个阈值有些武断,最近在复现一些实验,尤其是软科学中的实验时,大家发现这个阈值应该设置得更为严格。以p值等于0.05为阈值,这意味着每20次实验中就有1次结论错误(1/20 = 0.05),这样的阈值过于宽泛。应该说,p值接近0.05只能证明某个结论有可能是事实,值得我们投入更多精力(和更大规模的数据)去做进一步研究。

1.4 matplotlib

matplotlib用于绘图。这里我们展示一下matplotlib绘制二维图像和三维图像的能力。我们先来看一个二维图像的例子:

>>> import numpy as np
>>> import matplotlib.pylab as plt
>>> x = np.random.random(100)
>>> plt.plot(x)
>>> plt.show()

上述代码首先加载了numpy模块(matplotlib很适合处理NumPy数据),然后使用np.random. random生成了一个大小为100、取值区间为[0, 1)的随机数组x。最后使用plt.plot给数组x画图,并通过调用plt.show将绘制的图像输出。matplotlib的输出是交互式的。你可以动手试试绘图窗口的各项功能。例如,图1-1展示了Linux环境下绘图窗口的样子。由于画的是一个随机数组,因此每次图像的序列都是不同的,但绘图窗口的功能区是固定的。

图1-1 一个简单的matplotlib绘图窗口

下面我们再来试试三维数组:

>>> from mpl_toolkits.mplot3d import Axes3D
>>> import matplotlib.pylab as plt
>>> import numpy as np
>>> x = np.random.random(20)
>>> y = np.random.random(20)
>>> z = np.random.random(20)
>>> fig = plt.figure()
>>> ax = fig.add_subplot(111, projection='3d')
>>> ax.scatter(x,y,z)
>>> plt.show()

上述代码首先加载了三维坐标组件、matplotlib以及NumPy。然后用NumPy生成了3个取值区间为[0, 1)的随机数组,它们就是我们要画的三维数据点。接下来调用plt.figure和fig.add_subplot以设置三维投影图。参数111告诉matplotlib我们希望所有图片构成1 × 1的网格,并且把当前图片画到第一个格子里,所以111表明只有单张图片。参数projection用于设置三维绘图模式。最后用ax.scatter绘制散点图,并且通过调用plt.show将图片显示出来。使用matplotlib绘制的三维图像与二维图像一样,也是交互式的,可尝试用鼠标旋转图片。

1.5 scikit-learn

虽然本书的目标是覆盖深度学习的数学知识,而非实现深度学习模型本身,但我们时不时会发现,一两个简单的神经网络模型有助于我们理解问题。这时候我们就会用到sklearn中的MLPClassifier类。另外,sklearn还提供了很多有用的组件用于评估模型的性能和绘制高维图像。

下面我们构建一个简单的神经网络,对8 × 8的手写体灰度小图进行分类。sklearn内置了我们这里需要的数据集。以下是示例代码:

  import numpy as np
  from sklearn.datasets import load_digits
  from sklearn.neural_network import MLPClassifier
 
❶ d = load_digits()
  digits = d["data"]
  labels = d["target"]
 
  N = 200
❷ idx = np.argsort(np.random.random(len(labels)))
  x_test, y_test = digits[idx[:N]], labels[idx[:N]]
  x_train, y_train = digits[idx[N:]], labels[idx[N:]]
 
❸ clf = MLPClassifier(hidden_layer_sizes=(128,))
  clf.fit(x_train, y_train)
 
  score = clf.score(x_test, y_test)
  pred = clf.predict(x_test)
  err = np.where(y_test != pred)[0]
  print("score : ", score)
  print("errors:")
  print(" actual : ", y_test[err])
  print(" predicted: ", pred[err])

上述代码首先导入numpy模块,然后从sklearn中导入load_digits来返回图像数据集,并导入MLPClassifier来训练一个基于多层感知机的传统神经网络。接下来从返回的图像数据集中分别抽取图片和对应的数字标签❶。其中图片被存储为包含8 × 8 = 64个元素的向量,这表示将单张图片的二维像素矩阵按行展开,并且拼接成一行。由于整个图像数据集包含1797张图片,因此digits是一个1797行、64列的二维NumPy数组,labels则是一个包含1797个数字标签的一维向量。

随机地将图片的顺序打乱(这里需要注意保持图片和标签的对应关系❷),然后划分训练数据和测试数据(x_train、x_test)以及对应的标签(y_train、y_test)。拿出前200张图片作为测试样本,余下的1597张图片则作为训练样本。这样每个手写体数字就大约有160张图片用于训练,而有大约20张图片用于测试。

接下来创建一个MLPClassifier对象用于建模❸。指定单个隐层的大小为128个神经元,其他参数全部默认。由于输入为包含64个元素的向量,因此在通过隐层后,向量的大小将翻倍。我们不用指定输出层的大小,sklearn会根据y_train的标签自动进行推测。训练模型非常简单,只需要调用clf.fit并且传入训练图片(x_train)和标签(y_train)。

训练这么小的数据集只需要几分钟就够了。训练结束后,模型学到的权重和截距被保存在对象clf中。我们先获得模型的score,它表示模型总体的准确率,然后拿到模型在测试集上的预测结果(pred)。通过与真实标签(y_test)进行对比,我们可以得到预测错误的样本编号,将其保存到变量err中。最后把错误标签和对应的真实标签输出并进行对比。

每次执行上述代码时,我们都会重新将样本打乱,因而划分的训练集和测试集会发生变化。此外,神经网络在训练前都会随机初始化各个参数。因此,每次训练的结果都会不同。我第一次执行这段代码时,得到的总体准确率为0.97(97%)。如果是随机猜测,准确率大约是10%,所以我们可以说模型训练得还不错。

1.6 小结

在本章中,我首先介绍了如何设置工作环境,接下来概要性地介绍了各个Python组件,并且提供了用于深入学习的参考链接。有了让人安心和完善的工作环境后,我们将在第2章中一头扎进概率论。

相关图书

动手学自然语言处理
动手学自然语言处理
Python高性能编程(第2版)
Python高性能编程(第2版)
图像处理与计算机视觉实践——基于OpenCV和Python
图像处理与计算机视觉实践——基于OpenCV和Python
Python数据科学实战
Python数据科学实战
Web应用安全
Web应用安全
Python编程入门与实战(第3版)
Python编程入门与实战(第3版)

相关文章

相关课程