TensorFlow 深度学习入门指南(一)
原文:Beginning Deep Learning with TensorFlow协议:CC BY-NC-SA 4.0一、人工智能导论我们想要的是一台能从经验中学习的机器。—艾伦·图灵1.1 人工智能在行动信息技术是人类历史上的第三次工业革命。计算机、互联网和智能家居技术的普及极大地方便了人们的日常生活。通过编程,人类可以将事先设计好的交互逻辑交给机器反复快速执行,从而将人类从简单繁琐的重复性劳
一、人工智能导论
我们想要的是一台能从经验中学习的机器。
—艾伦·图灵
1.1 人工智能在行动
信息技术是人类历史上的第三次工业革命。计算机、互联网和智能家居技术的普及极大地方便了人们的日常生活。通过编程,人类可以将事先设计好的交互逻辑交给机器反复快速执行,从而将人类从简单繁琐的重复性劳动中解放出来。然而,对于需要高智能水平的任务,如人脸识别、聊天机器人和自动驾驶,很难设计出清晰的逻辑规则。因此,传统的编程方法对这类任务无能为力,而人工智能作为解决这类问题的关键技术,是非常有前途的。
随着深度学习算法的兴起,AI 在一些任务上已经实现甚至超过了类似人类的智能。比如 AlphaGo 程序已经击败了人类围棋最强选手之一的柯洁,OpenAI Five 在 Dota 2 的比赛上击败了冠军战队 OG。在此期间,人脸识别、智能语音、机器翻译等实用技术进入了人们的日常生活。现在我们的生活其实已经被 AI 包围了。虽然目前所能达到的智能水平距离人工通用智能(AGI)还有一段距离,但我们仍然坚信 AI 的时代已经到来。
接下来,我们将介绍 AI、机器学习和深度学习的概念,以及它们之间的联系和区别。
1.1.1 人工智能解释
AI 是一种允许机器像人类一样获得智能和推理机制的技术。这个概念最早出现在 1956 年的达特茅斯会议上。这是一项非常具有挑战性的任务。人类目前还不能对人脑的工作机制有全面科学的认识。制造能达到人脑水平的智能机器,无疑难度更大。也就是说,在某些方面类似甚至超过人类智力的机器已经被证明是可行的。
如何实现 AI 是一个非常宽泛的问题。人工智能的发展主要经历了三个阶段,每个阶段都代表了人类试图从不同角度实现人工智能的探索足迹。在早期,人们试图通过总结和概括一些逻辑规则,并以计算机程序的形式实现它们来开发智能系统。但是这种显式规则往往过于简单,难以用来表达复杂抽象的概念和规则。这个阶段被称为推理期。
20 世纪 70 年代,科学家试图通过知识库和推理来实现 AI。他们建立了一个大型复杂的专家系统来模拟人类专家的智力水平。这些明确指定的规则的最大困难之一是,许多复杂、抽象的概念无法在具体的代码中实现。比如人类对图片的识别和对语言的理解过程,根本无法用既定的规则来模拟。为了解决这样的问题,一个允许机器从数据中自动学习规则的研究学科诞生了,称为机器学习。20 世纪 80 年代,机器学习成为人工智能领域的一个热门话题。这是第二阶段。
在机器学习中,有一个方向是通过神经网络学习复杂、抽象的逻辑。神经网络方向的研究经历了两次大起大落。自 2012 年以来,深度神经网络技术的应用在计算机视觉、自然语言处理(NLP)和机器人等领域取得了重大突破。有些任务甚至已经超过了人类的智力水平。这是人工智能的第三次复兴。深度神经网络最终有了一个新名字——深度学习。总的来说,神经网络和深度学习的本质区别并不大。深度学习是指基于深度神经网络的模型或算法。人工智能、机器学习、神经网络、深度学习之间的关系如图 1-1 所示。
图 1-1
人工智能、机器学习、神经网络和深度学习的关系
1.1.2 机器学习
机器学习可以分为有监督学习、无监督学习、强化学习,如图 1-2 。
图 1-2
机器学习的类别
监督学习。监督学习数据集包含样本 x 和样本标签 y 。算法需要学习映射关系fθ:x→y,其中 f θ 代表模型函数, θ 是模型的参数。在训练过程中,通过最小化模型预测与真实值 y 之间的误差来优化模型参数 θ ,使模型能够有更准确的预测。常见的监督学习模型包括线性回归、逻辑回归、支持向量机(SVMs)和随机森林。
无监督学习。收集带标签的数据通常更昂贵。对于只有样本的数据集,算法需要发现数据本身的模态。这种算法被称为无监督学习。无监督学习中的一类算法是将自身作为监督信号,即fθ:x→x,这就是所谓的自监督学习。在训练过程中,通过最小化模型预测值fθ(x)与其自身 x 之间的误差来优化参数。常见的无监督学习算法包括自编码器和生成对抗网络(GANs)。
强化学习。这是一种通过与环境交互来学习解决问题的策略的算法。与有监督和无监督学习不同,强化学习问题没有明确的“正确的”动作监督信号。该算法需要与环境进行交互,以从环境反馈中获得滞后的奖励信号。因此,不可能计算模型强化学习预测和“正确值”之间的误差来直接优化网络。常见的强化学习算法是深度 Q 网络(DQNs)和近似策略优化(PPO)。
1.1.3 神经网络和深度学习
神经网络算法是一类基于神经网络从数据中学习的算法。它们仍然属于机器学习的范畴。由于计算能力和数据量的限制,早期的神经网络很浅,通常有一到四层左右。因此,网络表达能力有限。随着计算能力的提升和大数据时代的到来,高度并行化的图形处理单元(GPU)和海量数据使得大规模神经网络的训练成为可能。
2006 年,Geoffrey Hinton 首次提出了深度学习的概念。2012 年,八层深度神经网络 AlexNet 发布,在图像识别比赛中取得了巨大的性能提升。此后,相继开发出几十层、几百层、甚至上千层的神经网络模型,显示出很强的学习能力。使用深度神经网络实现的算法通常被称为深度学习模型。本质上,神经网络和深度学习可以被认为是一样的。
我们简单对比一下深度学习和其他算法。如图 1-3 所示,基于规则的系统通常会编写显式逻辑,这种逻辑一般是为特定任务设计的,不适合其他任务。传统机器学习算法人为设计具有一定通用性的特征检测方法,如 SIFT、HOG 特征等。这些特性适用于某一类任务,具有一定的通用性。但是性能很大程度上取决于如何设计这些特性。神经网络的出现使得计算机可以通过神经网络自动设计那些功能,而无需人工干预。浅层神经网络通常具有有限的特征提取能力,而深层神经网络能够提取高级抽象特征,并且具有更好的性能。
图 1-3
深度学习与其他算法的比较
1.2 神经网络的历史
我们把神经网络的发展分为浅层神经网络阶段和深度学习阶段,以 2006 年为分界点。2006 年之前,深度学习以神经网络的名义发展,经历了两起两落。2006 年,Geoffrey Hinton 首次将深度神经网络命名为深度学习,开始了它的第三次复兴。
1.2.1 浅层神经网络
1943 年,心理学家沃伦·麦卡洛克和逻辑学家沃尔特·皮茨根据生物神经元的结构提出了最早的神经元数学模型,以他们的姓氏首字母命名为 MP 神经元模型。型号f(x)=h(g(x)),其中g(x)=∑IxI,xI如果 g ( x ) ≥ 0,输出为 1;如果 g ( x ) <为 0,输出为 0。MP 神经元模型没有学习能力,只能完成固定的逻辑判断。**
图 1-4
MP 神经元模型
1958 年,美国心理学家弗兰克·罗森布拉特(Frank Rosenblatt)提出了第一个可以自动学习权重的神经元模型,称为感知器(perceptron)。如图 1-5 所示,利用输出值 o 与真值 y 之间的误差来调整神经元的权重{ w 1 , w 2 ,…,wn}。Frank Rosenblatt 随后实现了基于“Mark 1 感知器”硬件的感知器模型。如图 1-6 和 1-7 所示,输入为 400 像素的图像传感器,输出有 8 个节点。它可以成功地识别一些英文字母。一般认为,1943–1969 年是人工智能发展的第一个繁荣期。
图 1-7
马克 1 感知器网络架构 2
图 1-6
弗兰克·罗森布拉特和马克 1 号感知器 1 号
图 1-5
感知器模型
1969 年,美国科学家马文·明斯基等人在《感知器一书中指出了感知器等线性模型的主要缺陷。他们发现,感知器不能处理简单的线性不可分问题,如异或。这直接导致了对神经网络感知机相关研究的低谷期。一般认为 1969–1982 年是人工智能的第一个冬天。
虽然处于 AI 的低谷期,但还是有很多意义重大的研究陆续发表。其中最重要的是反向传播(BP)算法,它仍然是现代深度学习算法的核心基础。事实上,BP 算法的数学思想早在 20 世纪 60 年代就已经衍生出来,但当时还没有应用到神经网络中。1974 年,美国科学家 Paul Werbos 在其博士论文中首次提出 BP 算法可以应用于神经网络。遗憾的是,这个结果没有得到足够的重视。1986 年,David Rumelhart 等人在 Nature 发表了一篇使用 BP 算法进行特征学习的论文。此后,BP 算法开始得到广泛关注。
1982 年,随着约翰·霍普菲尔德(John Hopfield)的循环连接霍普菲尔德网络(cyclical connected Hopfield network)的提出,从 1982 年到 1995 年开始了人工智能复兴的第二次浪潮。在此期间,卷积神经网络、循环神经网络和反向传播算法相继被开发出来。1986 年,David Rumelhart、Geoffrey Hinton 等人将 BP 算法应用于多层感知器。1989 年,Yann LeCun 等人将 BP 算法应用于手写数字图像识别,取得了巨大的成功,被称为 LeNet。LeNet 系统在邮政编码识别、银行支票识别和许多其他系统中成功商业化。1997 年,Jürgen Schmidhuber 提出了最广泛使用的循环神经网络变体之一,长短期记忆(LSTM)。同年,还提出了双向循环神经网络。
遗憾的是,随着以支持向量机(SVM)为代表的传统机器学习算法的兴起,神经网络的研究逐渐进入低谷,被称为人工智能的第二个冬天。支持向量机具有严谨的理论基础,需要的训练样本数量少,还具有良好的泛化能力。相比之下,神经网络缺乏理论基础,难以解释。深网难练,表现正常。图 1-8 显示了 1943 年到 2006 年间 AI 发展的重要时期。
图 1-8
浅层神经网络开发时间表
1.2.2 深度学习
2006 年,Geoffrey Hinton 等人发现多层神经网络可以通过逐层预训练得到更好的训练,并在 MNIST 手写数字图片数据集上取得了比 SVM 更好的错误率,开启了第三次人工智能复兴。在那篇论文中,Geoffrey Hinton 首次提出了深度学习的概念。2011 年,Xavier Glorot 提出了一种校正线性单位(ReLU)激活函数,这是目前应用最广泛的激活函数之一。2012 年,Alex Krizhevsky 提出了一个八层深度神经网络 AlexNet,它使用了 ReLU 激活函数和 Dropout 技术来防止过拟合。同时摒弃了逐层预训练的方式,直接在两个 NVIDIA GTX580 GPUs 上训练网络。AlexNet 在 ILSVRC-2012 图片识别比赛中获得第一名,显示前 5 名的错误率比第二名惊人地降低了 10.9%。
自 AlexNet 模型开发以来,各种模型相继问世,包括 VGG 系列、GoogleNet 系列、ResNet 系列和 DenseNet 系列。ResNet 系列型号将网络的层数增加到数百甚至数千层,同时保持相同甚至更好的性能。其算法简单通用,性能显著,是深度学习最具代表性的模型。
除了在监督学习方面取得惊人的成果,在无监督学习和强化学习方面也取得了巨大的成就。2014 年,Ian Goodfellow 提出了生成对抗网络(GANs),它通过对抗训练来学习样本的真实分布,以生成近似度更高的样本。此后,人们提出了大量的 GAN 模型。最新的图像生成模型可以生成达到肉眼难以辨别的保真度的图像。2016 年,DeepMind 将深度神经网络应用于强化学习领域,提出了 DQN 算法,在雅达利游戏平台的 49 款游戏中,达到了与人类相当甚至更高的水平。在围棋领域,来自 DeepMind 的 AlphaGo 和 AlphaGo Zero 智能程序先后战胜了人类顶级围棋选手李世石、柯洁等。在多智能体协作 Dota 2 游戏平台中,OpenAI 开发的 OpenAI 五大智能程序在受限游戏环境下击败 TI8 冠军战队 OG,展现了大量专业的高水平智能操作。图 1-9 列出了 2006 年到 2019 年 AI 发展的主要时间点。
图 1-9
深度学习发展的时间表
1.3 深度学习的特点
与传统的机器学习算法和浅层神经网络相比,现代深度学习算法通常具有以下特点。
数据量
早期的机器学习算法训练起来相对简单快速,所需数据集的规模也相对较小,比如英国统计学家罗纳德·费雪在 1936 年收集的鸢尾花数据集,只包含三类花,每类有 50 个样本。随着计算机技术的发展,设计的算法越来越复杂,对数据量的需求也越来越大。Yann LeCun 在 1998 年收集的 MNIST 手写数字图片数据集包含从 0 到 9 的总共 10 类数字,每类多达 7000 张图片。随着神经网络尤其是深度学习网络的兴起,网络层数普遍较大,模型参数数量可达百万、千万,甚至十亿。为了防止过拟合,训练数据集的大小通常很大。现代社交媒体的普及也使得收集海量数据成为可能。例如,2010 年发布的 ImageNet 数据集总共包括 14,197,122 张图片,整个数据集的压缩文件大小为 154GB。图 1-10 和 1-11 列出了一段时间内的样本数量和数据集大小。
虽然深度学习对大数据集的需求很高,但是收集数据,尤其是收集有标签的数据,往往是非常昂贵的。一个数据集的形成通常需要人工采集、爬取原始数据并清除无效样本,然后用人类的智能对数据样本进行标注,因此不可避免地会引入主观偏差和随机误差。因此,数据量要求小的算法是非常热门的话题。
图 1-11
数据集大小随时间变化
图 1-10
数据集样本大小随时间变化
1.3.2 计算能力
计算能力的提升是第三次人工智能复兴的重要因素。事实上,现代深度学习的基础理论早在 20 世纪 80 年代就已经提出,但直到 2012 年基于两个 GTX580 GPUs 上的训练的 AlexNet 发布,深度学习的真正潜力才得以实现。传统的机器学习算法不像深度学习那样对数据量和计算能力有苛刻的要求。通常情况下,在 CPU 上进行串行训练可以获得满意的效果。但是深度学习非常依赖并行加速计算设备。目前大多数神经网络使用并行加速芯片,如英伟达 GPU 和谷歌 TPU 来训练模型参数。比如 AlphaGo Zero 程序,需要从零开始在 64 个 GPU 上训练 40 天,才能超越所有 AlphaGo 历史版本。自动网络结构搜索算法使用 800 个 GPU 来优化更好的网络结构。
目前普通消费者可以使用的深度学习加速硬件设备主要来自 NVIDIA GPU 显卡。图 1-12 图解了 2008-2017 年 NVIDIA GPU 和 x86 CPU 的每秒十亿次浮点运算(GFLOPS)的变化。可以看到,x86 CPU 的曲线变化相对较慢,NVIDIA GPU 的浮点计算能力呈指数级增长,这主要是游戏和深度学习计算的业务不断增加推动的。
图 1-12
英伟达 GPU FLOPS 变化(数据来源:英伟达)
1.3.3 网络规模
早期的感知器模型和多层神经网络只有一层或两层到四层,网络参数也在几万左右。随着深度学习的发展和计算能力的提升,相继提出了 AlexNet (8 层)、VGG16 (16 层)、GoogleNet (22 层)、ResNet50 (50 层)、DenseNet121 (121 层)等模型,同时输入图片的大小也从 28×28 到 224×224 逐渐增大到 299×299 甚至更大。这些变化使得网络的参数总数达到千万级,如图 1-13 所示。
网络规模的增加相应地增强了神经网络的能力,使得网络可以学习更复杂的数据形态,并且模型性能可以相应地提高。另一方面,网络规模的增大也意味着我们需要更多的训练数据和计算能力来避免过拟合。
图 1-13
网络层的变化
一般情报
过去,为了提高算法在某项任务上的性能,往往需要利用先验知识,人工设计相应的特征来帮助算法更好地收敛到最优解。这种类型的特征提取方法通常与特定任务密切相关。一旦场景发生变化,这些人为设计的特征和先验设置就无法适应新的场景,人们往往需要重新设计算法。
设计一种能够像人脑一样自动学习、自我调整的通用智能机构,一直是人类共同的愿景。深度学习是最接近一般智能的算法之一。在计算机视觉领域,以前需要为特定任务设计特征并添加先验假设的方法已经被深度学习算法所抛弃。目前,几乎所有的图像识别、目标检测和语义分割算法都是基于端到端的深度学习模型,表现出良好的性能和较强的适应性。在 Atari 游戏平台上,DeepMind 设计的 DQN 算法在相同的算法、模型结构、超参数设置下,可以在 49 场游戏中达到人类同等水平,表现出一定程度的通用智能。图 1-14 是 DQN 算法的网络结构。它不是为某个游戏设计的,但可以控制 Atari 游戏平台上的 49 个游戏。
图 1-14
DQN 网络结构[1]
1.4 深度学习应用
深度学习算法已经广泛应用于我们的日常生活中,比如手机中的语音助手、汽车中的智能辅助驾驶、刷脸支付等。我们将从计算机视觉、自然语言处理和强化学习开始,介绍深度学习的一些主流应用。
计算机视觉
图像分类是一个常见的分类问题。神经网络的输入是图片,输出值是当前样本属于每个类别的概率。通常,选择概率最高的类别作为样本的预测类别。图像识别是深度学习最早的成功应用之一。经典的神经网络模型包括 VGG 系列、Inception 系列和 ResNet 系列。
物体检测是指通过算法自动检测出图片中常见物体的大概位置。通常用一个包围盒来表示,对包围盒中对象的类别信息进行分类,如图 1-15 所示。常见的对象检测算法有 RCNN、快速 RCNN、更快 RCNN、掩模 RCNN、SSD 和 YOLO 系列。
语义分割是对图片中的内容进行自动分割和识别的算法。我们可以把语义分割理解为对每个像素的分类,分析每个像素的类别信息,如图 1-16 。常见的语义分割模型有 FCN、U-net、SegNet、DeepLab 系列等。
图 1-16
语义分割示例
图 1-15
对象检测示例
视频了解。随着深度学习在 2D 图片相关任务上取得更好的效果,具有时间维度信息(第三维是帧序列)的 3D 视频理解任务受到越来越多的关注。常见的视频理解任务包括视频分类、行为检测和视频主题提取。常见的机型有 C3D、TSN、DOVF、TS_LSTM。
图像生成从学习到的分布中学习真实图片和样本的分布,获得高度逼真的生成图片。目前常见的图像生成模型有系列和 GAN 系列。其中,GAN 系列算法近年来取得了长足的进步。最新的 GAN 模型产生的画面效果已经到了肉眼难以辨别真伪的程度,如图 1-17 。
除了前面的应用,深度学习在其他领域也取得了显著的成果,例如艺术风格转移(图 1-18 )、超分辨率、图片去噪/模糊、灰度图片着色等等。
图 1-18
艺术风格转移图像
图 1-17
模型生成的图像
1.4.2 自然语言处理
机器翻译。在过去,机器翻译算法通常基于统计机器翻译模型,这也是谷歌翻译系统在 2016 年之前使用的技术。2016 年 11 月,谷歌推出了基于 Seq2Seq 模型的谷歌神经机器翻译(GNMT)系统。首次实现了从源语言到目标语言的直接翻译技术,在多项任务上提高了 50–90%。常用的机器翻译模型有 Seq2Seq、BERT、GPT 和 GPT-2。其中,OpenAI 提出的 GPT-2 模型约有 15 亿个参数。起初,OpenAI 出于技术安全原因拒绝开源 GPT-2 模型。
聊天机器人也是自然语言处理的一个主流任务。机器自动学习与人类对话,对简单的人类需求提供满意的自动响应,提高客户服务效率和服务质量。聊天机器人通常用于咨询系统、娱乐系统和智能家居。
强化学习
虚拟游戏。与真实环境相比,虚拟游戏平台既可以训练和测试强化学习算法,又可以避免无关因素的干扰,同时还可以最小化实验成本。目前,常用的虚拟游戏平台包括 OpenAI Gym、OpenAI Universe、OpenAI Roboschool、DeepMind OpenSpiel 和 MuJoCo,常用的强化学习算法包括 DQN、A3C、A2C 和 PPO。在围棋领域,DeepMind AlphaGo 程序已经超越了人类围棋专家。在 Dota 2 和星际争霸游戏中,OpenAI 和 DeepMind 开发的智能程序也曾在限制规则下击败过职业队伍。
机器人。在现实环境中,对机器人的控制也取得了一些进展。例如,加州大学伯克利分校实验室在机器人领域的模仿学习、元学习、少射学习等领域取得了很多进展。波士顿动力公司在机器人应用方面取得了可喜的成绩。它制造的机器人在复杂地形行走和多智能体协作等任务上表现出色(图 1-19 )。
自动驾驶被认为是短期内强化学习的一个应用方向。许多公司在自动驾驶方面投入了大量资源,如百度、优步和谷歌。来自百度的 Apollo 已经开始在北京、雄安、武汉等地试运营。图 1-20 显示的是百度的自动驾驶汽车 Apollo。
图 1-20
百度的自动驾驶汽车阿波罗 4
图 1-19
来自波士顿动力公司的机器人 3
1.5 深度学习框架
工欲善其事,必先利其器。了解了深度学习的基础知识之后,我们来挑选一下用来实现深度学习算法的工具。
主要框架
-
Theano 是最早的深度学习框架之一。它是由 Yoshua Bengio 和 Ian Goodfellow 开发的。它是一个基于 Python 的计算库,用于定位底层操作。Theano 同时支持 GPU 和 CPU 操作。由于 Theano 开发效率低,模型编译时间长,开发者改用 TensorFlow,目前 Theano 已经停止维护。
-
Scikit-learn 是一个完整的机器学习算法计算库。它内置了对常见的传统机器学习算法的支持,并且拥有丰富的文档和示例。然而,scikit-learn 并不是专门为神经网络设计的。不支持 GPU 加速,神经网络相关层的实现也有欠缺。
-
咖啡由贾于 2013 年创立。它主要用于使用卷积神经网络的应用,不适用于其他类型的神经网络。Caffe 的主要开发语言是 C ++,同时也为 Python 等其他语言提供接口。它还支持 GPU 和 CPU。由于发展时间较早,在业内知名度较高,2017 年脸书推出了 Caffe 的升级版——Caffe 2。Caffe2 现在已经集成到 PyTorch 库中。
-
Torch 是一个非常好的科学计算库,基于不太流行的编程语言 Lua 开发。Torch 灵活性很高,很容易实现自定义网络层,这也是 PyTorch 继承的优秀基因。但由于 Lua 语言用户较少,Torch 一直无法获得主流应用。
-
MXNet 由陈天琦和李牧开发,是亚马逊官方深度学习框架。它采用命令式编程和符号式编程的混合方法,灵活性高,运行速度快,文档和实例丰富。
-
PyTorch 是脸书推出的深度学习框架,基于原始的 Torch 框架,使用 Python 作为主要开发语言。PyTorch 借鉴了 Chainer 的设计风格,采用命令式编程,使得网络的搭建和调试非常方便。虽然 PyTorch 在 2017 年才发布,但由于其精致小巧的界面设计,PyTorch 在学术界获得了广泛好评。1.0 版本后,原有的 PyTorch 和 Caffe2 合并,弥补 PyTorch 在工业部署上的不足。总体来说,PyTorch 是一个优秀的深度学习框架。
-
Keras 是基于 Theano 和 TensorFlow 等框架提供的底层操作实现的高层框架。它为快速训练和测试提供了大量的高级接口。对于常见的应用程序,用 Keras 开发是非常高效的。但因为没有底层实现,需要抽象底层框架,所以运行效率不高,灵活性一般。
-
TensorFlow 是 Google 在 2015 年发布的深度学习框架。最初的版本只支持符号编程。由于其较早的发布和谷歌在深度学习领域的影响力,TensorFlow 迅速成为最受欢迎的深度学习框架。但由于界面设计变化频繁、功能设计冗余、符号编程开发调试困难等原因,TensorFlow 1.x 一度被业界诟病。2019 年,Google 推出了 TensorFlow 2 正式版,运行于动态图优先模式,可以避免 TensorFlow 1.x 版本的诸多缺陷。TensorFlow 2 得到了业界的广泛认可。
目前,TensorFlow 和 PyTorch 是业界应用最广泛的两个深度学习框架。TensorFlow 在行业内拥有完整的解决方案和用户基础。得益于其精简灵活的接口设计,PyTorch 可以快速构建和调试网络,在学术界好评如潮。TensorFlow 2 发布后,让用户更容易学习 TensorFlow,将模型无缝部署到生产中。这本书使用 TensorFlow 2 作为主要框架来实现深度学习算法。
下面是 TensorFlow 和 Keras 的联系和区别。Keras 可以理解为一套高级别的 API 设计规范。Keras 本身有规范的官方实现。TensorFlow 中也实现了相同的规范,称为 tf.keras 模块,tf.keras 将作为唯一的高层接口,避免接口冗余。除非特别说明,本书中的 Keras 均指 tf.keras。
1.5.2 TensorFlow 2 和 1.x
TensorFlow 2 在用户体验上和 TensorFlow 1.x 是完全不同的框架。TensorFlow 2 与 TensorFlow 1.x 代码不兼容。同时在编程风格和功能界面设计上也大相径庭。TensorFlow 1.x 代码需要依靠人工迁移,自动化迁移方式不太靠谱。Google 即将停止更新 TensorFlow 1.x,不建议现在学习 TensorFlow 1.x。
TensorFlow 2 支持动态图形优先级模式。在计算过程中,您可以获得计算图形和数值结果。您可以调试代码并实时打印数据。网络像积木一样搭建,一层一层堆叠,符合软件开发思维。
以简单加法 2.0 + 4.0 为例,在 TensorFlow 1.x 中,我们需要先创建一个计算图,如下:
import tensorflow as tf
# 1\. Create computation graph with tf 1.x
# Create 2 input variables with fixed name and type
a_ph = tf.placeholder(tf.float32, name='variable_a')
b_ph = tf.placeholder(tf.float32, name='variable_b')
# Create output operation and name
c_op = tf.add(a_ph, b_ph, name='variable_c')
创建计算图的过程类似于通过符号建立公式 c = a + b 的过程。它只记录公式的计算步骤,并不实际计算数值结果。数值结果只能通过运行输出 c 并赋值 a = 2.0 和 b = 4.0 来获得,如下所示:
# 2.Run computational graph with tf 1.x
# Create running environment
sess = tf.InteractiveSession()
# Initialization
init = tf.global_variables_initializer()
sess.run(init) # Run the initialization
# Run the computation graph and return value to c_numpy
c_numpy = sess.run(c_op, feed_dict={a_ph: 2., b_ph: 4.})
# print out the output
print('a+b=',c_numpy)
可见在 TensorFlow 1 中进行简单的加法运算都是如此繁琐,更不用说创建复杂的神经网络算法了。这种创建计算图并在以后运行它的编程方法被称为符号编程。
接下来,我们使用 TensorFlow 2 完成相同的操作,如下所示:
import tensorflow as tf
# Use TensorFlow 2 to run
# 1.Create and initialize variable
a = tf.constant(2.)
b = tf.constant(4.)
# 2.Run and get result directly
print('a+b=',a+b)
可以看到,计算过程非常简单,没有额外的计算步骤。
同时获得计算图形和数值结果的方法称为命令式编程,也称为动态图形模式。TensorFlow 2 和 PyTorch 都是使用动态图优先模式开发的,很容易调试。一般来说,动态图模式对于开发来说效率很高,但是对于运行来说可能没有静态图模式效率高。TensorFlow 2 还支持通过 tf.function 将动态图模式转换为静态图模式,实现开发和运营效率的双赢。在本书的剩余部分,我们使用 TensorFlow 来表示一般的 TensorFlow 2。
演示
深度学习的核心是算法的设计思想,深度学习框架只是我们实现算法的工具。在下文中,我们将演示 TensorFlow 深度学习框架的三个核心功能,以帮助我们理解框架在算法设计中的作用。
- 加速计算
神经网络本质上是由大量的矩阵乘法和加法等基本数学运算组成的。TensorFlow 的一个重要功能就是利用 GPU 方便地实现并行计算加速功能。为了演示 GPU 的加速效果,我们可以比较 CPU 和 GPU 上多个矩阵乘法的平均运行时间,如下所示。
我们分别创建形状为[1,n]和[n,1]的两个矩阵 A 和 B。可以使用参数 n 调整矩阵的大小,代码如下:
# Create two matrices running on CPU
with tf.device('/cpu:0'):
cpu_a = tf.random.normal([1, n])
cpu_b = tf.random.normal([n, 1])
print(cpu_a.device, cpu_b.device)
# Create two matrices running on GPU
with tf.device('/gpu:0'):
gpu_a = tf.random.normal([1, n])
gpu_b = tf.random.normal([n, 1])
print(gpu_a.device, gpu_b.device)
让我们实现 CPU 和 GPU 操作的函数,并通过 timeit.timeit()函数测量这两个函数的计算时间。需要注意的是,第一次计算一般需要额外的环境初始化工作,所以这个时间不能算。我们通过预热阶段去除此时间,然后测量计算时间,如下所示:
def cpu_run(): # CPU function
with tf.device('/cpu:0'):
c = tf.matmul(cpu_a, cpu_b)
return c
def gpu_run():# GPU function
with tf.device('/gpu:0'):
c = tf.matmul(gpu_a, gpu_b)
return c
# First calculation needs warm-up
cpu_time = timeit.timeit(cpu_run, number=10)
gpu_time = timeit.timeit(gpu_run, number=10)
print('warmup:', cpu_time, gpu_time)
# Calculate and print mean running time
cpu_time = timeit.timeit(cpu_run, number=10)
gpu_time = timeit.timeit(gpu_run, number=10)
print('run time:', cpu_time, gpu_time)
我们绘制了不同矩阵大小的 CPU 和 GPU 环境下的计算时间,如图 1-21 所示。可以看出,当矩阵规模较小时,CPU 和 GPU 时间相差无几,体现不出 GPU 并行计算的优势。当矩阵规模较大时,CPU 计算时间显著增加,GPU 充分利用并行计算,而计算时间几乎没有任何变化。
图 1-21
CPU/GPU 矩阵乘法时间
- 自动梯度计算
在使用 TensorFlow 构建正演计算过程时,TensorFlow 除了能够获得数值结果外,还会自动构建计算图。TensorFlow 提供自动微分功能,无需手动求导即可计算网络参数输出的导数。考虑以下函数的表达式:
)
输出 y 对变量 w 的导数关系为
)
考虑在( a 、 b 、 c 、 w ) = (1,2,3,4)处的导数。我们可以得到)
使用 TensorFlow,我们可以直接计算给定函数表达式的导数,而无需手动推导导数的表达式。TensorFlow 可以自动导出。代码实现如下:
import tensorflow as tf
# Create 4 tensors
a = tf.constant(1.)
b = tf.constant(2.)
c = tf.constant(3.)
w = tf.constant(4.)
with tf.GradientTape() as tape:# Track derivative
tape.watch([w]) # Add w to derivative watch list
# Design the function
y = a * w**2 + b * w + c
# Auto derivative calculation
[dy_dw] = tape.gradient(y, [w])
print(dy_dw) # print the derivative
程序的结果是
tf.Tensor(10.0, shape=(), dtype=float32)
可以看出 TensorFlow 自动微分的结果与手工计算的结果是一致的。
- 通用神经网络接口
除了矩阵乘法、加法等底层数学功能,TensorFlow 还具有常用神经网络运算功能、常用网络层、网络训练、模型保存、加载、部署等一系列深度学习系统的便捷功能。使用 TensorFlow,可以轻松使用这些函数完成常见的生产流程,高效稳定。
1.6 开发环境安装
在了解了深度学习框架带来的便利后,我们现在准备在本地桌面安装最新版本的 TensorFlow。TensorFlow 支持多种常用操作系统,如 Windows 10、Ubuntu 18.04、Mac OS 等。它支持在 NVIDIA GPU 上运行的 GPU 版本和仅使用 CPU 进行计算的 CPU 版本。我们以最常见的操作系统 Windows 10、NVIDIA GPU、Python 为例,介绍如何安装 TensorFlow 框架等开发软件。
一般来说,开发环境安装分为四个主要步骤:Python 解释器 Anaconda、CUDA 加速库、TensorFlow 框架和常用编辑器。
1.6.1 Anaconda 安装
Python 解释器是让用 Python 写的代码被 CPU 执行的桥梁,是 Python 语言的核心软件。用户可以从 www.python.org/
下载合适版本(此处使用 Python 3.7)的解释器。安装完成后,可以调用 python.exe 程序来执行用 Python(.py 文件)。
这里我们选择安装 Anaconda 软件,该软件集成了 Python 解释器、包管理、虚拟环境等一系列辅助功能。我们可以从 www.anaconda.com/distribution/#download-section
下载 Anaconda,选择最新版本的 Python 下载安装。如图 1-22 所示,勾选“将 Anaconda 添加到我的 PATH 环境变量”选项,这样就可以通过命令行调用 Anaconda 程序了。如图 1-23 所示,安装人员询问是否一起安装 VS 代码软件。选择跳过。整个安装过程大约 5 分钟,具体时间视电脑性能而定。
图 1-23
蟒蛇装置 2
图 1-22
蟒蛇装置 1
安装完成后,我们如何验证 Anaconda 是否安装成功?按键盘上的 Windows+R 组合键,可以调出正在运行的程序对话框,输入“cmd”,按 enter 键打开 Windows 自带的命令行程序“cmd.exe”。或者点击开始菜单,输入“cmd”找到“cmd.exe”程序,打开。输入“conda list”命令查看 Python 环境中已安装的库。如果是新安装的 Python 环境,列出的库都是 Anaconda 自带的库,如图 1-24 所示。如果“conda list”能正常弹出一系列库列表信息,则 Anaconda 软件安装成功。否则,安装失败,您需要重新安装。
图 1-24
Anaconda 安装测试
CUDA 安装
目前的深度学习框架大多基于英伟达的 GPU 显卡进行加速计算,所以你需要安装英伟达提供的 GPU 加速库 CUDA。在安装 CUDA 之前,请确保您的计算机具有支持 CUDA 程序的 NVIDIA 图形设备。如果你的电脑没有 NVIDIA 显卡——比如有些电脑显卡厂商是 AMD 或者 Intel——CUDA 程序就不行,你可以跳过这一步直接安装 TensorFlow CPU 版本。
CUDA 的安装分为三步:CUDA 软件安装、cuDNN 深度神经网络加速库安装、环境变量配置。安装过程有点繁琐。我们将以 Windows 10 系统为例,一步一步地介绍它们。
CUDA 软件安装打开 CUDA 程序官方下载网站: https://developer.nvidia.com/cuda-10.0-download-archive
。这里我们用的是 CUDA 10.0 版本:选择 Windows 平台,x86_64 架构,10 系统,exe(本地)安装包,然后选择“下载”,下载 CUDA 安装软件。下载完成后,打开软件。如图 1-25 所示,选择“自定义”选项,点击“下一步”按钮,进入如图 1-26 所示的安装程序选择列表。在这里,您可以选择需要安装的组件,取消选择不需要安装的组件。在“CUDA”类别下,取消选择“Visual Studio 集成”项。在“驱动程序组件”类别下,在“显示驱动程序”行比较“当前版本”和“新版本”的版本号。如果“当前版本”大于“新版本”,您需要取消选中“显示驱动程序”如果“当前版本”小于或等于“新版本”,则勾选“显示驱动程序”,如图 1-27 所示。安装完成后,您可以单击“下一步”并按照说明进行安装。
图 1-26
CUDA 安装 2
图 1-25
CUDA 安装 1
安装完成后,我们来测试一下 CUDA 软件是否安装成功。打开“cmd”终端,输入“nvcc -V”打印当前 CUDA 版本信息,如图 1-28 所示。如果无法识别该命令,则安装失败。我们可以从 CUDA 安装路径“C:\ Program Files \ NVIDIA GPU Computing Toolkit \ CUDA \ v 10.0 \ bin”中找到“nvcc.exe”程序,如图 1-29 所示。
图 1-29
CUDA 安装测试 2
图 1-28
CUDA 安装测试 1
图 1-27
CUDA 安装 3
cuDNN 神经网络加速库安装。CUDA 不是专门针对神经网络的 GPU 加速库;它是为各种需要并行计算的应用而设计的。如果你想为神经网络应用加速,你需要安装一个额外的 cuDNN 库。需要注意的是,cuDNN 库不是一个可执行程序。您只需要下载并解压缩 cuDNN 文件,并配置 Path 环境变量。
打开 https://developer.nvidia.com/cudnn
网站,选择“下载 cuDNN”由于 NVIDIA 的规定,用户需要登录或创建一个新用户才能继续下载。登录后进入 cuDNN 下载界面,勾选“我同意 cuDNN 软件许可协议的条款”,会弹出 cuDNN 版本下载选项。选择与 CUDA 10.0 匹配的 cuDNN 版本,点击“cuDNN Library for Windows 10”链接下载 cuDNN 文件,如图 1-30 。需要注意的是,cuDNN 本身是有版本号的,同样需要匹配 CUDA 版本号。
图 1-30
cuDNN 版本选择界面
下载完 cuDNN 文件后,将其解压缩,并将文件夹“cuda”重命名为“cudnn765”。然后将“cudnn765”文件夹复制到 CUDA 安装路径“C:\ Program Files \ NVIDIA GPU Computing Toolkit \ CUDA \ v 10.0”(图 1-31 )。这里可能会弹出一个需要管理员权限的对话框。选择继续粘贴。
图 1-31
cuDNN 安装路径
环境变量配置。我们已经完成了 cuDNN 的安装,但是为了让系统知道 cuDNN 文件的位置,我们需要如下配置 Path 环境变量。打开文件浏览器,右键“我的电脑”,选择“属性”,选择“高级系统设置”,选择“环境变量”,如图 1-32 所示。在“系统变量”栏中选择“Path”环境变量,选择“编辑”,如图 1-33 所示。选择“新建”,输入 cuDNN 安装路径“C:\ Program Files \ NVIDIA GPU Computing Toolkit \ CUDA \ v 10.0 \ cud nn 765 \ bin”,使用“上移”按钮将此项移动到顶部。
图 1-33
环境变量配置 2
图 1-32
环境变量配置 1
CUDA 安装完成后,环境变量应该包括“C:\ Program Files \ NVIDIA GPU Computing Toolkit \ CUDA \ v 10.0 \ bin”、“C:\ Program Files \ NVIDIA GPU Computing Toolkit \ CUDA \ v 10.0 \ libnvvp”、“C:\ Program Files \ NVIDIA GPU Computing Toolkit \ CUDA \ v 10.0 \ cud nn 765 \ bin”。根据实际路径,前面的路径可能略有不同,如图 1-34 所示。确认后,单击“确定”关闭所有对话框。
图 1-34
与 CUDA 相关的环境变量
1 . 6 . 3 tensorflow 安装
TensorFlow 和其他 Python 库一样,可以使用 Python 包管理工具“pip install”命令进行安装。在安装 TensorFlow 时,你需要根据你的电脑是否有 NVIDIA GPU 显卡来确定是安装更强大的 GPU 版本还是一般性能的 CPU 版本。
# Install numpy
pip install numpy
使用前面的命令,您应该能够自动下载并安装 numpy 库。现在让我们安装 TensorFlow 的最新 GPU 版本。该命令如下所示:
# Install TensorFlow GPU version
pip install -U tensorflow
前面的命令应该会自动下载并安装 TensorFlow GPU 版本,该版本目前是 TensorFlow 2.x 的正式版本。“-U”参数指定如果安装了此软件包,则执行升级命令。
现在来测试一下 TensorFlow 的 GPU 版本是否安装成功。在“cmd”命令行输入“ipython”进入 ipython 交互终端,然后输入“import tensorflow as tf”命令。如果没有出现错误,继续输入“tf.test.is_gpu_available()”测试 gpu 是否可用。该命令将打印一系列信息。以“I”(信息)开头的信息包含了可用的 GPU 图形设备的信息,最后会返回“真”或“假”,表示 GPU 设备是否可用,如图 1-35 所示。如果为真,则 TensorFlow GPU 版本安装成功;如果为 False,安装将失败。您可能需要再次检查 CUDA、cuDNN 和环境变量配置的步骤,或者复制错误并向搜索引擎寻求帮助。
图 1-35
tensorflow gpu 安装测试
如果没有 GPU,可以安装 CPU 版本。CPU 版本无法使用 GPU 加速计算,计算速度相对较慢。但是,因为本书中作为学习目的介绍的模型一般计算量不大,所以也可以使用 CPU 版本。将来对深度学习有了更好的理解后,也可以添加 NVIDIA GPU 设备。如果 TensorFlow GPU 版本安装失败,我们也可以直接使用 CPU 版本。安装 CPU 版本的命令是
# Install TensorFlow CPU version
pip install -U tensorflow-cpu
安装完成后,在 ipython 终端输入“import tensorflow as tf”命令,验证 CPU 版本安装成功。TensorFlow 安装完成后,可以通过“tf。__ 版本 __"。图 1-36 给出了一个例子。注意,即使是代码也适用于所有 TensorFlow 2.x 版本。
图 1-36
tensorflow 版本测试
前面手动安装 CUDA 和 cuDNN、配置 Path 环境变量以及安装 TensorFlow 的过程是标准的安装方法。虽然步骤繁琐,但对理解每个库的功能作用有很大帮助。事实上,对于新手来说,您可以通过如下两个命令来完成前面的步骤:
# Create virtual environment tf2 with tensorflow-gpu setup required
# to automatically install CUDA,cuDNN,and TensorFlow GPU
conda create -n tf2 tensorflow-gpu
# Activate tf2 environment
conda activate tf2
这种快速安装方法称为最小安装方法。这也是使用 Anaconda 发行版的便利之处。通过极简版安装的 TensorFlow 在使用前需要激活相应的虚拟环境,需要与标准版区分开来。标准版本安装在 Anaconda 的默认环境基础中,通常不需要手动激活基础环境。
默认情况下也可以安装常见的 Python 库。该命令如下所示:
# Install common python libraries
pip install -U ipython numpy matplotlib pillow pandas
TensorFlow 在运行的时候,会默认消耗所有的 GPU 资源,这在计算上是非常不友好的,尤其是当计算机有多个用户或者程序同时使用 GPU 资源的时候。占用所有的 GPU 资源会让其他程序无法运行。所以一般建议将 TensorFlow 的 GPU 内存使用设置为增长模式,即根据实际模型大小申请 GPU 内存资源。代码实现如下:
# Set GPU resource usage method
# Get GPU device list
gpus = tf.config.experimental.list_physical_devices('GPU')
if gpus:
try:
# Set GPU usage to growth mode
for gpu in gpus:
tf.config.experimental.set_memory_growth(gpu, True)
except RuntimeError as e:
# print error
print(e)
通用编辑器安装
用 Python 写程序有很多种方法。你可以使用 IPython 或者 Jupyter Notebook 来交互式地编写代码。还可以使用 Sublime Text、PyCharm 和 VS 代码来开发大中型项目。本书推荐使用 PyCharm 编写和调试代码,使用 VS 代码进行交互式项目开发。他们两个都是免费的。用户可以自行下载安装。
接下来,让我们开始深度学习之旅吧!
1.7 摘要
1.8 参考
- 动词 (verb 的缩写)Mnih、K. Kavukcuoglu、D. Silver、A. A .鲁苏、J. Veness、M. G. Bellemare、A. Graves、M. Riedmiller、A. K. Fidjeland、G. Ostrovski、S. Petersen、C. Beattie、A. Sadik、I. Antonoglou、H. King、D. Kumaran、D. Wierstra、S. Legg 和 D. Hassabis,“通过深度强化学习实现人类水平的控制”,*《自然》,*511
图片来源:https://slideplayer.com/slide/12771753/
2
图片来源:www.glass-bead.org/article/machines-that-morph-logic/?lang=enview
3
4
二、回归
有些人担心人工智能会让我们感到自卑,但话说回来,任何一个头脑正常的人每次看到一朵花都会有一种自卑感。
—艾伦·凯
2.1 神经元模型
一个成年人的大脑包含大约 1000 亿个神经元。每个神经元通过树突获得输入信号,通过轴突传递输出信号。神经元相互连接形成了一个巨大的神经网络,从而形成了人类的大脑,感知和意识的基础。图 2-1 是典型的生物神经元结构。1943 年,心理学家沃伦·麦卡洛克和数学逻辑学家沃尔特·皮茨提出了人工神经网络的数学模型来模拟生物神经元的机制[1]。这项研究由美国神经学家 Frank Rosenblatt 进一步发展为感知器模型[2],这也是现代深度学习的基石。
图 2-1
典型的生物神经元结构 1
从生物神经元的结构出发,重温科学先驱的探索,逐步揭开自动学习机的神秘面纱。
首先,我们可以将神经元模型抽象成如图 2-2 (a)所示的数学结构。神经元输入向量x=x1, x 2 , x 3 ,…,xnT通过函数 f 映射到 y 考虑一个简化的情况,比如线性变换:f(x)=wTx+b。扩展的形式是
图 2-2
数学神经元模型
参数 θ = { w 1 、 w 、 2 、 w 、 3 、…、 w 、??n、??b}决定了神经元的状态,通过固定这些参数可以确定该神经元的处理逻辑。当输入节点数 n = 1(单输入)时,神经元模型可进一步简化为
)
然后我们可以将 y 的变化绘制成 x 的函数,如图 2-3 所示。随着输入信号 x 增加,输出 y 也线性增加。这里参数 w 可以理解为直线的斜率,b 为直线的偏置。
图 2-3
单输入线性神经元模型
对于某个神经元来说, x 和 y 之间的映射关系fw, b 未知但固定。两点可以确定一条直线。为了估计 w 和 b 的值,我们只需要从图t】中的直线上采样任意两个数据点(x(1), y (1) , x (2) , y (2) )
)
)
如果( x (1) ,y(1))≦(x(2), y (2) ),我们就可以求解前面的方程组得到 w 和 b 的值。我们来考虑一个具体的例子:x(1)= 1, y (1) = 1.567, x (2) = 2, y (2) = 3.043。代入前面公式中的数字,得到
)
)
这是我们初高中学过的二元线性方程组。利用消元法可以很容易地计算出解析解,即 w = 1.477, b = 0.089。
你可以看到,我们只需要两个不同的数据点就可以完美地求解一个单输入线性神经元模型的参数。对于输入为 N 的线性神经元模型,我们只需要采样 N + 1 个不同的数据点。似乎线性神经元模型可以被完美地解析。那么前面的方法有什么问题呢?考虑到任何采样点都可能存在观测误差,我们假设观测误差变量 ϵ 服从正态分布),均值为 μ ,方差为 σ 2 。然后示例如下:
)
一旦引入观测误差,即使是简单的线性模型,如果只采样两个数据点,也可能带来较大的估计偏差。如图 2-4 所示,数据点都存在观测误差。如果估计基于两个蓝色矩形数据点,则估计的蓝色虚线将与真正的橙色直线有较大偏差。为了减少观测误差引入的估计偏差,我们可以对多个数据点)进行采样,然后寻找一条“最佳”的直线,使其最小化所有采样点与该直线之间的误差之和。
图 2-4
有观测误差的模型
由于观测误差的存在,可能不存在完美通过所有采样点)的直线。因此,我们希望找到一条接近所有采样点的“好”直线。如何衡量「好」与「坏」?一个自然的想法是用所有采样点的预测值wx??(I)+b与真实值y(I)之间的均方误差(MSE)作为总误差,即
)
然后搜索一组参数w∫和b∫使总误差最小)总误差最小对应的直线就是我们要找的最优直线,也就是
)
这里 n 表示采样点数。
2.2 优化方法
现在我们来总结一下前面的解法:我们需要找到最优参数w∫和b∫,使输入输出满足一个线性关系y(I)=wx(I)+b,但是,由于观测误差 ϵ 的存在,需要对一个由足够数量的数据样本组成的数据集)进行采样,以找到一组最优的参数w*∑和b∑,使均方误差
)最小。*
对于单输入神经元模型,通过消去法只需要两个样本就可以得到方程的精确解。这种由严格公式导出的精确解称为解析解。然而,在多个数据点( n ≫ 2)的情况下,很可能没有解析解。我们只能用数值优化的方法来获得一个近似的数值解。为什么叫优化?这是因为计算机的计算速度非常快。我们可以利用强大的计算能力进行多次“搜索”和“尝试”,从而逐步减少错误)。最简单的优化方法就是蛮力搜索或者随机实验。比如为了找到最合适的w∫和b∫,我们可以从实数空间中随机抽取任意一个 w 和 b ,计算出对应模型的误差值
)。从所有实验
)中挑出误差最小的
),其对应的w∑和b∑就是我们要找的最优参数。
这种强力算法简单明了,但对于大规模、高维优化问题效率极低。梯度下降是神经网络训练中最常用的优化算法。凭借强大的图形处理单元(GPU)芯片的并行加速能力,非常适合优化具有海量数据的神经网络模型。自然,它也适用于优化我们简单的线性神经元模型。由于梯度下降算法是深度学习的核心算法,我们将首先应用梯度下降算法来解决简单的神经元模型,然后在第七章中详细介绍其在神经网络中的应用。
有了导数的概念,如果要求解一个函数的最大值和最小值,可以简单地将导函数设为 0,找到对应的自变量数值,也就是驻点,然后检查驻点类型。以函数f(x)=x2sin(x)为例,我们可以在区间x∈【10,10】内绘制函数及其导数,其中蓝色实线为 f ( x ),黄色虚线为),如图所示可以看出,导数(虚线)为 0 的点就是驻点, f ( x )的最大值和最小值都出现在驻点。
图 2-5
函数f(x)=x2∙罪 ( x )及其衍生
函数的梯度被定义为函数对每个独立变量的偏导数的向量。考虑一个三维函数 z = f ( x , y ),函数对自变量 x 的偏导数为),函数对自变量 y 的偏导数记为
),梯度∇ f 为向量
)。我们来看一个具体的函数 f ( x ,y)=(cos2x+cos2y)2。如图 2-6 所示,平面中红色箭头的长度代表梯度向量的模,箭头的方向代表梯度向量的方向。可以看出,箭头的方向始终指向函数值增加的方向。函数曲面越陡,箭头的长度越长,梯度的模数越大。
图 2-6
一个函数及其梯度2
通过前面的例子,我们可以直观的感受到,函数的梯度方向总是指向函数值增加的方向。那么梯度的反方向应该指向函数值减小的方向。
)
(2.1)
为了利用这个特性,我们只需要按照前面的等式迭代更新x’。然后我们可以得到越来越小的函数值。 η 用于缩放梯度向量,称为学习率,一般设置为较小的值,如 0.01 或 0.001。特别地,对于一维函数,前面的向量形式可以写成标量形式:
)
通过前面的公式多次迭代更新x’,则x??’处的函数值y??’总是比 x 处的函数值小的可能性更大。
用公式( 2.1 )优化参数的方法称为梯度下降算法。它计算函数 f 的梯度∇ f 并迭代更新参数 θ 以获得当函数 f 达到其最小值时参数 θ 的最优数值解。需要注意的是,深度学习中的模型输入一般表示为 x ,需要优化的参数一般表示为 θ 、 w 、 b 。
现在,我们将在本次会议开始时应用梯度下降算法来计算最佳参数w∑和b∑。这里,均方误差函数被最小化:
)
需要优化的模型参数是 w 和 b ,因此我们使用以下等式迭代更新它们:
)
)
2.3 运行中的线性模型
让我们使用梯度下降算法实际训练一个单输入线性神经元模型。首先,我们需要对多个数据点进行采样。对于具有已知模型的玩具示例,我们直接从指定的真实模型中取样:
)
- 采样数据
为了模拟观测误差,我们在模型中增加了一个独立的误差变量 ϵ ,其中 ϵ 服从高斯分布,平均值为 0,标准差为 0.01(即方差为 0.01 2 ):
)
通过随机采样 n = 100 次,我们使用以下代码获得一个训练数据集):
data = [] # A list to save data samples
for i in range(100): # repeat 100 times
# Randomly sample x from a uniform distribution
x = np.random.uniform(-10., 10.)
# Randomly sample from Gaussian distribution
eps = np.random.normal(0., 0.01)
# Calculate model output with random errors
y = 1.477 * x + 0.089 + eps
data.append([x, y]) # save to data list
data = np.array(data) # convert to 2D Numpy array
在前面的代码中,我们在一个循环中执行 100 个样本,每次我们从均匀分布u(10,10)中随机采样一个数据点 x ,然后从高斯分布)中随机采样噪声 ϵ 。最后,我们使用真实模型和随机噪声 ϵ 生成数据,并将其保存为 Numpy 数组。
- 计算均方误差
现在,让我们通过平均每个数据点的预测值和真实值之间的平方差来计算训练集的均方误差。我们可以使用以下函数来实现这一点:
- 计算坡度
def mse(b, w, points):
# Calculate MSE based on current w and b
totalError = 0
# Loop through all points
for i in range(0, len(points)):
x = points[i, 0] # Get ith input
y = points[i, 1] # Get ith output
# Calculate the total squared error
totalError += (y - (w * x + b)) ** 2
# Calculate the mean of the total squared error
return totalError / float(len(points))
根据梯度下降算法,我们需要计算每个数据点)的梯度。首先,考虑扩展均方误差函数
):
)
因为
)
我们有
)
)
)
(2.2)
如果很难理解前面的推导,可以复习数学中与梯度相关的课程。详细内容也会在本书第七章介绍。我们可以暂时记住)的最终表情。同样的,我们可以推导出偏导数的表达式
):
)
)
)
)
(2.3)
根据表达式( 2.2 )和( 2.3 ),我们只需要计算出(wx(I)+b—y(I))x(I)的平均值实现如下:
- 渐变更新
def step_gradient(b_current, w_current, points, lr):
# Calculate gradient and update w and b.
b_gradient = 0
w_gradient = 0
M = float(len(points)) # total number of samples
for i in range(0, len(points)):
x = points[i, 0]
y = points[i, 1]
# dL/db:grad_b = 2(wx+b-y) from equation (2.3)
b_gradient += (2/M) * ((w_current * x + b_current) - y)
# dL/dw:grad_w = 2(wx+b-y)*x from equation (2.2)
w_gradient += (2/M) * x * ((w_current * x + b_current) - y)
# Update w',b' according to gradient descent algorithm
# lr is learning rate
new_b = b_current - (lr * b_gradient)
new_w = w_current - (lr * w_gradient)
return [new_b, new_w]
在计算出误差函数在 w 和 b 的梯度后,我们可以根据方程( 2.1 )更新 w 和 b 的值。训练数据集的所有样本一次被称为一个时期。我们可以使用之前定义的函数迭代多个时期。实现如下:
def gradient_descent(points, starting_b, starting_w, lr, num_iterations):
# Update w, b multiple times
b = starting_b # initial value for b
w = starting_w # initial value for w
# Iterate num_iterations time
for step in range(num_iterations):
# Update w, b once
b, w = step_gradient(b, w, np.array(points), lr)
# Calculate current loss
loss = mse(b, w, points)
if step%50 == 0: # print loss and w, b
print(f"iteration:{step}, loss:{loss}, w:{w}, b:{b}")
return [b, w] # return the final value of w and b
主要培训功能定义如下:
def main():
# Load training dataset
data = []
for i in range(100):
x = np.random.uniform(3., 12.)
# mean=0, std=0.1
eps = np.random.normal(0., 0.1)
y = 1.477 * x + 0.089 + eps
data.append([x, y])
data = np.array(data)
lr = 0.01 # learning rate
initial_b = 0 # initialize b
initial_w = 0 # initialize w
num_iterations = 1000
# Train 1000 times and return optimal w*,b* and corresponding loss
[b, w]= gradient_descent(data, initial_b, initial_w, lr, num_iterations)
loss = mse(b, w, data) # Calculate MSE
print(f'Final loss:{loss}, w:{w}, b:{b}')
经过 1000 次迭代更新,最终的 w 和 b 就是我们要找的“最优”解。结果如下:
iteration:0, loss:11.437586448749, w:0.88955725981925, b:0.02661765516748428
iteration:50, loss:0.111323083882350, w:1.48132089048970, b:0.58389075913875
iteration:100, loss:0.02436449474995, w:1.479296279074, b:0.78524532356388
...
iteration:950, loss:0.01097700897880, w:1.478131231919, b:0.901113267769968
Final loss:0.010977008978805611, w:1.4781312318924746, b:0.901113270434582
可以看出,在第 100 次迭代时, w 和 b 的值已经接近真实模型值。经过 1000 次更新后得到的 w 和 b 非常接近真实模型。训练过程的均方误差如图 2-7 所示。
图 2-7
训练过程中的 MSE 变化
前面的示例显示了梯度下降算法在求解模型参数方面的强大功能。需要注意的是,对于复杂的非线性模型,梯度下降算法求解的参数可能是局部最小解而不是全局最小解,这是由函数非凸性决定的。然而,我们在实践中发现,通过梯度下降算法获得的数值解的性能通常可以被很好地优化,并且相应的解可以直接用于近似最优解。
2.4 总结
简单回顾一下我们的探索:我们先假设输入为 n 的神经元模型是线性模型,然后通过 n + 1 个样本可以计算出 w 和 b 的精确解。引入观测误差后,可以对多组数据点进行采样,通过梯度下降算法进行优化,得到 w 和 b 的数值解。
如果从另一个角度来看这个问题,其实可以理解为一组连续值(向量)预测问题。给定一个数据集),我们需要从数据集学习一个模型,以便预测一个未知样本的输出值。在假设模型的类型之后,学习过程变成了搜索模型参数的问题。比如我们假设神经元是线性模型,那么训练过程就是搜索线性模型参数 *** w *** 和 *** b *** 的过程。训练之后,我们可以使用模型输出值作为任何新输入的真实值的近似值。从这个角度来说,是一个连续值预测问题。
在现实生活中,连续值预测问题非常常见,比如股票价格趋势的预测、天气预报中的温湿度预测、年龄的预测、交通流量的预测等等。如果它的预测在一个连续的实数范围内,或者属于某个连续的实数范围,我们称之为回归问题。特别是如果用线性模型来近似真实模型,那么我们称之为线性回归,这是回归问题的一种具体实现。
除了连续值预测问题,还有离散值预测问题吗?比如硬币正反面的预测,只能有正反面两种预测。给定一张图片,这张图片中的物体类型只能是一些离散的类别比如猫或者狗。像这样的问题被称为分类问题,这将在下一章介绍。
2.5 参考文献
-
W.s .麦卡洛克和 w .皮茨,“神经活动内在思想的逻辑演算”,《数学生物物理学通报》, 5,第 115-133 页,1943 年 12 月 1 日。
-
F.罗森布拉特,感知机,一个感知和识别自动机项目,康奈尔航空实验室,1957 年。
来源: https://commons.wikimedia.org/wiki/File:Neuron_Hand-tuned.svg
2
图片来源:https://en.wikipedia.org/wiki/Gradient?oldid=747127712
三、分类
花在人工智能上的一年时间,足以让一个人相信上帝。
—艾伦·珀利斯
前面已经介绍了用于连续变量预测的线性回归模型。现在让我们深入分类问题。分类问题的一个典型应用是教计算机如何自动识别图像中的对象。我们来考虑一个图像分类中最简单的任务:0–9 数字图片识别,相对简单,也有非常广泛的应用,比如邮政编码、快递单号、手机号识别。我们将以 0–9 数字图片识别为例,探讨如何利用机器学习解决分类问题。
3.1 手写数字图片数据集
机器学习需要从数据中学习,所以首先需要收集大量的真实数据。以手写数字图片识别为例,如图 3-1 所示,我们需要收集大量真人书写的 0–9 数字图片。为了便于存储和计算,采集到的图片一般会缩放到固定的大小,比如 224 × 224 或者 96 × 96 像素。这些图片将作为输入数据 x 。同时,我们需要给每张图片贴上标签,这些标签将作为图片的真实价值。此标签指示图像属于哪个特定类别。对于手写数字图片识别,标签是数字 0-9,代表 0-9 的图片。
图 3-1
手写数字图片
如果我们希望模型在新样本上表现良好,即实现良好的模型泛化能力,那么我们需要尽可能地增加数据集的大小和多样性,使训练数据集尽可能接近真实的人口分布,并且模型也可以在看不见的样本上表现良好。
为了便于算法评估,Lecun 等人[1]发布了一个名为 MNIST 的手写数字图片数据集,其中包含了数字 0–9 的真实手写图片。每个数字总共有 7000 张图片,收集自不同的写作风格。总图数 7 万。其中 6 万张图片用于训练,剩下的 1 万张图片作为测试集。
因为手写数字图片中的信息相对简单,所以每张图片在只保留灰度信息的情况下,缩放为同样大小的 28 × 28 像素,如图 3-2 所示。这些图片由真人书写,包含丰富的字体大小、书写风格、线条粗细等信息,保证这些图片的分布尽可能接近真实手写数字图片的人口分布,从而保证模型泛化能力。
图 3-2
MNIST 数据集示例
现在让我们来看一幅画的表现。图片包含 h 行和 w 列,像素值为 h×w。通常,像素值是从 0 到 255 范围内的整数,以表示颜色强度信息。例如,0 表示最低强度,255 表示最高强度。如果是彩色图片,每个像素包含三个通道 R、G 和 B 的强度信息,这三个通道分别代表红色、绿色和蓝色的颜色强度。因此,与灰度图像不同,彩色图片的每个像素由具有三个元素的一维向量表示,这三个元素表示 R、G 和 B 颜色的强度。这样一来,彩色图像被保存为维数为[h,w,3]的张量,而灰度图片只需要形状为[h,w]的二维矩阵或形状为[h,w,1]的三维张量来表示其信息。图 3-3 显示了 8 号图片的矩阵内容。可以看出,图片中的黑色像素用 0 表示,灰度信息用 0–255 表示。图片中较白的像素对应于矩阵中较大的值。
图 3-3
一幅画是如何表现的 1
像 TensorFlow 和 PyTorch 这样的深度学习框架可以通过几行代码轻松下载、管理和加载 MNIST 数据集。这里,我们使用 TensorFlow 自动下载 MNIST 数据集,并将其转换为 Numpy 数组格式:
import os
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, optimizers, datasets
# load MNIST dataset
(x, y), (x_val, y_val) = datasets.mnist.load_data()
# convert to float type and rescale to [-1, 1]
x = 2*tf.convert_to_tensor(x, dtype=tf.float32)/255.-1
# convert to integer tensor
y = tf.convert_to_tensor(y, dtype=tf.int32)
# one-hot encoding
y = tf.one_hot(y, depth=10)
print(x.shape, y.shape)
# create training dataset
train_dataset = tf.data.Dataset.from_tensor_slices((x, y))
# train in batch
train_dataset = train_dataset.batch(512)
load_data()函数返回两个元组对象:第一个是训练集,第二个是测试集。第一个元组的第一个元素是训练图片数据 X ,第二个元素是对应的类别号 Y 。与图 3-3 类似,训练集 X 中的每幅图像由 28×28 像素组成,训练集 X 中有 60000 幅图像,因此 X 的最终维数为(60000,28,28)。 Y 的大小为(60000),代表 0-9 的 60000 个数字。类似地,测试集包含 10,000 个测试图片和相应的数字编号,其维数分别为(10000,28,28)和(10,000)。
从 TensorFlow 加载的 MNIST 数据集包含值从 0 到 255 的图像。在机器学习中,一般希望数据的范围分布在 0 左右的小范围内。因此,我们将像素范围重新调整为区间[1,1],这将有利于模型优化过程。
每张图的计算过程都是通用的。所以我们可以一次计算多张图片,充分利用 CPU 或者 GPU 的并行计算能力。我们用一个形状矩阵[ h , w 来表示一张图片。对于多张图,我们可以在前面多加一个维度,用一个形状张量[ b , h , w 来表示。这里 b 代表批量。彩色图片可以用一个形状为[ b , h , w , c ]的张量来表示,其中 c 表示通道数,对于彩色图片为 3。TensorFlow 的 Dataset 对象可用于使用 batch()函数方便地将数据集转换为批处理。
3.2 建立模型
回想一下我们在上一章讨论的生物神经元结构。我们将输入向量)简化为单个输入标量 x,模型可以表示为 y = * xw * + b 。如果是多输入单输出的模型结构,我们需要使用向量形式:
)
更一般地,通过组合多个多输入单输出神经元模型,我们可以构建一个多输入多输出模型:
)
其中)、
)、
)、
)。
对于多输出和批量训练,我们以批量形式编写模型:
)
(3.1)
其中 中的)、
)、
)、
)、 d 表示输入维度,dout表示输出维度。 X 有形状 中 b , d , b 为样本数d**为每个样本的长度。 W 有形状 d 在 , d 出 ,包含 d 在∵d**出 参数。偏置向量 b 有形状 d 出 。@符号表示矩阵乘法。由于运算结果 X @ W 是一个形状为[ b ,dout]的矩阵,所以不能直接加到向量 b 上。所以批量形式的+号需要支持广播,即通过复制 b 将向量 b 展开成形状为 b ,dout的矩阵。
考虑两个样本,其中 d in = 3,dout= 2。方程式 3.1 展开如下:
)
其中上标如(1)和(2)表示样本索引,下标如 1 和 2 表示某个样本向量的元素。相应的模型结构如图 3-4 所示。
图 3-4
具有三个输入和两个输出的神经网络
可以看出,矩阵形式更加简洁明了,同时可以充分发挥矩阵计算的并行加速能力。那么如何将图像识别任务的输入输出转化为张量形式呢?
使用形状为[ h 、 w 、 b 的矩阵存储灰度图像,使用形状为[ b 、 h 、 w 的张量存储图片。但是我们的模型只能接受向量,所以我们需要将[ h , w ]矩阵展平成一个长度为 h ⋅ w 的向量,如图 [3-5 所示,其中输入特征的长度din=h⋅w
图 3-5
展平矩阵
对于输出标签 y ,之前已经介绍了数字编码。它可以用一个数字来表示标签信息。输出只需要一个数字来表示网络的预测类别值,比如 1 号代表猫,3 号代表鱼。然而,数字编码的一个最大问题是,数字之间存在自然的顺序关系。比如 1、2、3 对应的标签是猫、狗、鱼,它们之间没有顺序关系,而是 1 < 2 < 3。因此,如果使用数字编码,将迫使模型学习这种不必要的约束。换句话说,数字编码会将标称标度(即,没有特定顺序)改变为序数标度(即,具有特定顺序),这不适合这种情况。
那么如何解决这个问题呢?输出实际上可以设置为一组长度为 d out 的向量,其中 d out 与类别的数量相同。例如,如果输出属于第一类,则相应的索引被设置为 1,其他位置被设置为 0。这种编码方法称为一键编码。以图 3-6 中的“猫、狗、鱼、鸟”识别系统为例,所有样本只属于“猫、狗、鱼、鸟”四类中的一类我们使用索引位置来分别表示猫、狗、鱼和鸟的类别。对于猫的所有图片,它们的一热编码是[1,0,0,0];对于所有的狗图片,它们的一热编码是[0,1,0,0];诸如此类。一键编码广泛应用于分类问题。
图 3-6
独热编码示例
手写数字图片的总类别数为十,即 d out = 10。对于一个样本,假设它属于一个类别 i ,即编号 i 。使用 one-hot 编码,我们可以使用长度为 10 的向量 y 来表示它,其中这个向量中的第 I 个元素是 1,其余的是 0。比如图片 0 的一热编码是[1,0,0,…,0],图片 2 的一热编码是[0,0,1,…,0],图片 9 的一热编码是[0,0,0,…,1]。独热编码非常稀疏。与数字编码相比,它需要更多的存储空间,所以一般采用数字编码进行存储。在计算过程中,数字编码被转换为一位热码编码,这可以通过 tf.one_hot()函数实现,如下所示:
y = tf.constant([0,1,2,3]) # digits 0-3
y = tf.one_hot(y, depth=10) # one-hot encoding with length 10
print(y)
Out[1]:
tf.Tensor(
[[1\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0.] # one-hot encoding of number 0
[0\. 1\. 0\. 0\. 0\. 0\. 0\. 0\. 0\. 0.] # one-hot encoding of number 1
[0\. 0\. 1\. 0\. 0\. 0\. 0\. 0\. 0\. 0.] # one-hot encoding of number 2
[0\. 0\. 0\. 1\. 0\. 0\. 0\. 0\. 0\. 0.]], shape=(4, 10), dtype=float32)
现在让我们回到手写数字图像识别的任务。输入是一个扁平化的图片向量 x ∈ R 784 ,输出是一个长度为 10o∈R10对应某个数的一键编码的向量,形成一个多输入多输出的线性模型o=WT我们希望模型输出更接近真实标签。
3.3 误差计算
对于分类问题,我们的目标是最大化某个性能指标,比如准确率。但是当精度被用作损失函数时,它实际上是不可微的。因此,梯度下降算法不能用于优化模型参数。一般的方法是建立一个平滑的和可导的代理目标函数,例如优化模型的输出和独热编码的真实标签之间的距离。通过优化代理目标函数获得的模型通常在测试数据集上也表现良好。与回归问题相比,分类问题的优化和评价目标函数是不一致的。训练一个模型的目标是通过优化损失函数 L 找到最优数值解W∫和b∫:
对于一个分类问题的误差计算,更常见的是使用交叉熵损失函数,而不是回归问题中引入的均方误差损失函数。我们将在以后的章节中介绍交叉熵损失函数。为了简单起见,这里我们仍然使用均方误差损失函数来解决手写数字图片识别问题。 n 个样本的均方误差损失函数可以表示为
)
现在我们只需要用梯度下降算法优化损失函数得到最优解 W 和 b 然后用得到的模型预测未知的手写数字图片 x ∈ D 测试 。
3.4 我们真的解决了问题吗?
根据前面的解决方案,手写数字图片识别的问题真的完美解决了吗?至少有两个主要问题:
-
一个线性模型是机器学习中最简单的模型之一。它只有几个参数,只能表达线性关系。复杂大脑的感知和决策远比一个线性模型复杂。因此,线性模型显然是不够的。
-
复杂性是模型近似复杂分布的能力。前述解决方案仅使用由少量神经元组成的单层神经网络模型。相比人脑中的 1000 亿个神经元互联结构,其泛化能力明显较弱。
图 3-7 显示了模型复杂性和数据分布的示例。绘制了带有观测误差的采样点分布图。实际分布可以是二次抛物线模型。如图 3-7 (a)如果用线性模型拟合数据,很难学习到好的模型;如果使用合适的多项式函数模型进行学习,比如二次多项式,就可以学习到如图 3-7 (b)所示的合适模型。但当模型过于复杂时,比如一个十次多项式,很可能会过拟合,伤害模型的泛化能力,如图 3-7 ©。
图 3-7
模型复杂性
我们目前使用的多神经元模型仍然是线性模型,泛化能力较弱。接下来,我们将尝试解决这两个问题。
3.5 非线性模型
由于线性模型不可行,我们可以在线性模型中嵌入非线性函数,将其转换为非线性模型。我们称这个非线性函数为激活函数,用 σ 表示:
)
这里 σ 代表一个特定的非线性激活函数,比如 Sigmoid 函数(图 3-8 (a))和 ReLU 函数(图 3-8 (b))。
图 3-8
常见激活功能
ReLU 函数只保留函数 y = x 的正部分,并将负部分设置为零。它具有单边抑制特性。虽然简单,但 ReLU 函数具有极好的非线性特性、容易的梯度计算和稳定的训练过程。它是深度学习模型中使用最广泛的激活函数之一。这里,我们通过嵌入 ReLU 函数将模型转换为非线性模型:
)
3.6 模型复杂性
为了增加模型的复杂性,我们可以重复堆叠多个转换,例如
)
)
)
在前面的等式中,我们将第一层神经元的输出值 h 1 作为第二层神经元的输入,然后将第二层神经元的输出 h 2 作为第三层神经元的输入,最后一层神经元的输出为模型输出。
如图 3-9 所示,函数嵌入以一个接一个的连接网络出现。我们称输入节点 x 所在的层为输入层。每个非线性模块 h i 的输出及其参数 W i 和 b i 称为一个网络层。特别是网络中间的那一层叫隐藏层,最后一层叫输出层。这种由大量神经元连接而成的网络结构称为神经网络。每层的节点数和层数决定了神经网络的复杂程度。
图 3-9
三层神经网络体系结构
现在我们的网络模型已经升级为三层神经网络,具有下降的复杂度和良好的非线性泛化能力。接下来,我们来讨论如何优化网络参数。
3.7 优化方法
我们已经在第二章介绍了回归问题的详细优化过程。实际上,类似的优化方法也可以用来解决分类问题。对于只有一层的网络模型,我们可以直接导出)和
)的偏导数表达式,然后计算每一步的梯度,并使用梯度下降算法更新参数 w 和 b 。然而,随着复杂非线性函数的嵌入,网络层数和数据特征长度也增加,模型变得非常复杂,并且难以手动导出梯度表达式。此外,一旦网络结构发生变化,模型函数和相应的梯度表达式也会发生变化。因此,依靠人工计算梯度显然是不可行的。
这就是为什么我们发明了深度学习框架。在自动微分技术的帮助下,深度学习框架可以在计算每一层的输出和相应的损失函数时建立神经网络的计算图,然后自动计算任意参数 θ 的梯度)。用户只需要设置好网络结构,梯度就会自动计算更新,使用起来非常方便高效。
3.8 动手手写数字图像识别
在本节中,我们将体验神经网络的乐趣,而不会介绍太多 TensorFlow 的细节。本节的主要目的不是讲授每一个细节,而是让读者对神经网络算法有一个全面直观的体验。让我们开始体验神奇的图像识别算法吧!
构建网络
对于第一层,输入是 x ∈ R 784 ,输出h1∈R256是一个长度为 256 的向量。我们不需要明确写出h1=ReLU(W1x+b1)的计算逻辑。它可以在 TensorFlow 中用一行代码实现:
# Create one layer with 256 output dimension and ReLU activation function
layers.Dense(256, activation='relu')
使用 TensorFlow 的序列函数,我们可以很容易地建立一个多层网络。对于三层网络,可以按如下方式实现:
# Build a 3-layer network. The output of 1st layer is the input of 2nd layer.
model = keras.Sequential([
layers.Dense(256, activation='relu'),
layers.Dense(128, activation='relu'),
layers.Dense(10)])
三层中的输出节点数分别为 256、128 和 10。调用 model (x)可以直接返回最后一层的输出。
模型培训
构建好三层神经网络后,给定输入 x ,我们可以调用 model( x )得到模型输出 o 并计算电流损耗 L :
with tf.GradientTape() as tape: # Record the gradient calculation
# Flatten x, [b, 28, 28] => [b, 784]
x = tf.reshape(x, (-1, 28*28))
# Step1\. get output [b, 784] => [b, 10]
out = model(x)
# [b] => [b, 10]
y_onehot = tf.one_hot(y, depth=10)
# Calculate squared error, [b, 10]
loss = tf.square(out-y_onehot)
# Calculate the mean squared error, [b]
loss = tf.reduce_sum(loss) / x.shape[0]
然后我们使用 TensorFlow tape.gradient(loss,model.trainable _ variables)的自动微分函数来计算所有的梯度)。:
# Step3\. Calculate gradients w1, w2, w3, b1, b2, b3
grads = tape.gradient(loss, model.trainable_variables)
使用梯度列表变量保存梯度结果。然后我们使用优化器对象根据梯度更新规则自动更新模型参数 θ 。
)
代码如下:
# Auto gradient calculation
grads = tape.gradient(loss, model.trainable_variables)
# w' = w - lr * grad, update parameters
optimizer.apply_gradients(zip(grads, model.trainable_variables))
经过多次迭代后,学习到的模型 f θ 可以用来预测未知图片的分类概率。这里暂时不讨论模型测试部分。
MNIST 数据集的训练误差曲线如图 3-10 所示。由于三层神经网络具有较强的泛化能力,手写数字图像识别任务相对简单,训练误差下降较快。在图 3-10 中,x 轴代表迭代所有训练样本的次数,称为历元。迭代所有训练样本一次被称为一个时期。我们可以在几个时期后测试模型的准确性和其他指标,以监控模型的训练效果。
图 3-10
MNIST 数据集的训练误差
3.9 摘要
本章通过将一层线性回归模型类比于分类问题,提出了一个三层非线性神经网络模型来解决手写数字图像识别问题。学完这一章,大家应该对(浅显的)神经网络算法有了很好的理解。除了数字图像识别,分类模型还有各种各样的应用。例如,分类模型用于区分垃圾邮件和非垃圾邮件,对非结构化文本进行情感分析,以及处理图像以进行分割。我们将在以后的章节中遇到更多的分类问题和应用。
接下来学习 TensorFlow 的一些基础知识,为后续学习和实现深度学习算法打下坚实的基础。
3.10 参考
- Y.Lecun、L. Bottou、Y. Bengio 和 P. Haffner,“基于梯度的学习应用于文档识别”,《IEEE 学报,1998 年。
四、基本 TensorFlow
我设想在未来,我们可能相当于机器人宠物狗,到那时我也会支持机器人。
—克劳德·香农
TensorFlow 是深度学习算法的科学计算库。所有操作都是基于张量对象执行的。复杂的神经网络算法本质上是张量的乘、加等基本运算的组合。因此,熟悉 TensorFlow 中的基本张量运算非常重要。只有掌握了这些操作,才能随意实现各种复杂新颖的网络模型,理解各种模型和算法的本质。
4.1 数据类型
TensorFlow 中的基本数据类型包括数值、字符串和布尔。
数字
数值张量是 TensorFlow 的主要数据格式。根据维度,可以分为
-
标量:单个实数,如 1.2 和 3.4,维数为 0,形状为[]。
-
向量:实数的有序集合,用方括号包裹,如【1.2】【1.2,3.4】,维数为 1,形状根据长度不同为【n】。
** 矩阵: n 行、 m 列的实数有序集合,如[[1,2],[3,4]],维数为 2,形状为[ n , m 。
* 张量:维数大于 2 的数组。张量的每个维度也称为轴。通常,每个维度代表特定的物理意义。例如,形状为[2,32,32,3]的张量有四个维度。如果表示图像数据,每个维度或轴表示图像的数量、图像高度、图像宽度和颜色通道的数量,即 2 表示两个图片,图像高度和宽度都是 32,3 表示总共三个颜色通道,即 RGB。张量的维数和每个维数所代表的具体物理意义需要用户定义。*
*在 TensorFlow 中,标量、向量和矩阵也不加区分地统称为张量。你需要根据张量的维数或者形状来做出自己的判断。同样的惯例也适用于这本书。
首先,让我们在 TensorFlow 中创建一个标量。实现如下:
In [1]:
a = 1.2 # Create a scalar in Python
aa = tf.constant(1.2) # Create a scalar in TensorFlow
type(a), type(aa), tf.is_tensor(aa)
Out[1]:
(float, tensorflow.python.framework.ops.EagerTensor, True)
如果要使用 TensorFlow 提供的函数,就必须按照 TensorFlow 指定的方式创建张量,而不是标准的 Python 语言。我们可以通过 print (x)或者 x 把张量 x 的相关信息打印出来,代码如下:
In [2]: x = tf.constant([1,2.,3.3])
x # print out x
Out[2]:
<tf.Tensor: id=165, shape=(3,), dtype=float32, numpy=array([1\. , 2\. , 3.3], dtype=float32)>
在输出中,id 是 TensorFlow 中内部对象的索引,shape 表示张量的形状,dtype 表示张量的数值精度。numpy()方法可以返回 Numpy.array 类型的数据,方便将数据导出到系统中的其他模块。
In [3]: x.numpy() # Convert TensorFlow (TF) tensor to numpy array
Out[3]:
array([1\. , 2\. , 3.3], dtype=float32)
与标量不同,向量的定义必须通过一个列表容器传递给 tf.constant()函数。例如,下面是如何创建一个向量:
In [4]:
a = tf.constant([1.2]) # Create a vector with one element
a, a.shape
Out[4]:
(<tf.Tensor: id=8, shape=(1,), dtype=float32, numpy=array([1.2], dtype=float32)>,
TensorShape([1]))
创建一个包含三个元素的向量:
In [5]:
a = tf.constant([1,2, 3.])
a, a.shape
Out[5]:
(<tf.Tensor: id=11, shape=(3,), dtype=float32, numpy=array([1., 2., 3.], dtype=float32)>,
TensorShape([3]))
类似地,矩阵的实现如下:
In [6]:
a = tf.constant([[1,2],[3,4]]) # Create a 2x2 matrix
a, a.shape
Out[6]:
(<tf.Tensor: id=13, shape=(2, 2), dtype=int32, numpy=
array([[1, 2],
[3, 4]])>, TensorShape([2, 2]))
三维张量可以定义为
In [7]:
a = tf.constant([[[1,2],[3,4]],[[5,6],[7,8]]])
Out[7]:
<tf.Tensor: id=15, shape=(2, 2, 2), dtype=int32, numpy=
array([[[1, 2],
[3, 4]],
[[5, 6],
[7, 8]]])>
字符串
除了数值类型,TensorFlow 还支持字符串类型。例如,在处理图像数据时,我们可以先记录图像的路径串,然后通过预处理函数根据路径读取图像张量。字符串张量可以通过传入字符串对象来创建,例如:
In [8]:
a = tf.constant('Hello, Deep Learning.')
a
Out[8]:
<tf.Tensor: id=17, shape=(), dtype=string, numpy=b'Hello, Deep Learning.'>
tf.strings 模块为字符串提供了常见的实用函数,如 lower()、join()、length()和 split()。例如,我们可以将所有字符串转换成小写:
In [9]:
tf.strings.lower(a) # Convert string a to lowercase
Out[9]:
<tf.Tensor: id=19, shape=(), dtype=string, numpy=b'hello, deep learning.'>
深度学习算法主要基于数值张量运算,字符串数据使用频率较低,这里不做过多赘述。
布尔型
为了方便比较操作,TensorFlow 还支持布尔张量。我们可以轻松地将 Python 标准布尔数据转换为 TensorFlow 内部布尔数据,如下所示:
In [10]: a = tf.constant(True)
a
Out[10]:
<tf.Tensor: id=22, shape=(), dtype=bool, numpy=True>
同样,我们可以创建一个布尔向量,如下所示:
In [1]:
a = tf.constant([True, False])
Out[1]:
<tf.Tensor: id=25, shape=(2,), dtype=bool, numpy=array([ True, False])>
请注意,Tensorflow 和标准 Python 布尔类型并不总是等价的,也不能通用,例如:
In [1]:
a = tf.constant(True) # Create TF Boolean data
a is True # Whether a is a Python Boolean
Out[1]:
False # TF Boolean is not a Python Boolean
In [2]:
a == True # Are they numerically the same?
Out[2]:
<tf.Tensor: id=8, shape=(), dtype=bool, numpy=True> # Yes, numerically, they are equal.
4.2 数值精度
对于数值张量,可以用对应于不同精度的不同字节长度来保存。例如,浮点数 3.14 可以以 16 位、32 位或 64 位精度保存。位越长,精度越高,当然,数字占用的存储空间也越大。TensorFlow 中常用的精度类型有 tf.int16、tf.int32、tf.int64、tf.float16、tf.float32 和 tf.float64,其中 tf.float64 称为 tf.double。
当创建一个张量时,我们可以指定它的精度,例如:
In [12]:
tf.constant(123456789, dtype=tf.int16)
tf.constant(123456789, dtype=tf.int32)
Out[12]:
<tf.Tensor: id=33, shape=(), dtype=int16, numpy=-13035>
<tf.Tensor: id=35, shape=(), dtype=int32, numpy=123456789>
注意,当精度太低时,数据 123456789 溢出,并返回错误的结果。通常,tf.int32 和 tf.int64 精度更常用于整数。对于浮点数,高精度张量可以更准确地表示数据。比如 tf.float32 用于 π 时,实际保存的数据是 3.1415927:
In [1]:
import numpy as np
tf.constant(np.pi, dtype=tf.float32) # Save pi with 32 byte
Out[1]:
<tf.Tensor: id=29, shape=(), dtype=float32, numpy=3.1415927>
如果我们使用 tf.float64,我们可以得到更高的精度:
In [2]:
tf.constant(np.pi, dtype=tf.float64) # Save pi with 64 byte
Out[2]:
<tf.Tensor: id=31, shape=(), dtype=float64, numpy=3.141592653589793>
对于大多数深度学习算法,tf.int32 和 tf.float32 一般能够满足精度要求。一些精度要求比较高的算法,比如强化学习,可以用 tf.int64 和 tf.float64。
张量精度可以通过 dtype 属性来访问。对于一些只能处理指定精度类型的运算,需要事先检查输入张量的精度类型,不符合要求的张量要用 tf.cast 函数转换成合适的类型,例如:
In [3]:
a = tf.constant(3.14, dtype=tf.float16)
print('before:',a.dtype) # Get a's precision
if a.dtype != tf.float32: # If a is not tf.float32, convert it to tf.float32.
a = tf.cast(a,tf.float32) # Convert a to tf.float32
print('after :',a.dtype) # Get a's current precision
Out[3]:
before: <dtype: 'float16'>
after : <dtype: 'float32'>
在执行类型转换时,需要确保转换操作的合法性。例如,将高精度张量转换为低精度张量时,可能会出现隐藏的数据溢出风险:
In [4]:
a = tf.constant(123456789, dtype=tf.int32)
tf.cast(a, tf.int16) # Convert a to lower precision and we have overflow
Out[4]:
<tf.Tensor: id=38, shape=(), dtype=int16, numpy=-13035>
布尔类型和整数类型之间的转换也是合法且常见的:
In [5]:
a = tf.constant([True, False])
tf.cast(a, tf.int32) # Convert boolean to integers
Out[5]:
<tf.Tensor: id=48, shape=(2,), dtype=int32, numpy=array([1, 0])>
一般来说,在类型转换期间,0 表示 False,1 表示 True。在 TensorFlow 中,非零数字被视为真,例如:
In [6]:
a = tf.constant([-1, 0, 1, 2])
tf.cast(a, tf.bool) # Convert integers to booleans
Out[6]:
<tf.Tensor: id=51, shape=(4,), dtype=bool, numpy=array([ True, False, True, True])>
4.3 待优化的张量
为了区分需要计算梯度信息的张量和不需要计算梯度信息的张量,TensorFlow 增加了一个特殊的数据类型来支持梯度信息的记录:tf.Variable. tf。变量在普通张量的基础上增加了名称、可训练性等属性,支持计算图的构造。由于梯度运算消耗大量计算资源并自动更新相关参数,tf。对于不需要梯度信息的张量,如神经网络的输入 X ,不需要封装变量。而是需要计算梯度的张量,比如神经网络层的 W 和 b ,需要用 tf 进行包裹。变量,以便 TensorFlow 跟踪相关的梯度信息。
特遣部队。Variable()函数可用于将普通张量转换成具有梯度信息的张量,例如:
In [20]:
a = tf.constant([-1, 0, 1, 2]) # Create TF tensor
aa = tf.Variable(a) # Convert to tf.Variable type
aa.name, aa.trainable # Get tf.Variable properties
Out[20]:
('Variable:0', True)
名称和可训练属性是特定于 tf 的。可变类型。name 属性用于命名计算图形中的变量。这个命名系统由 TensorFlow 内部维护,一般不需要用户做任何事情。可训练属性指示是否需要为张量记录梯度信息。创建变量对象时,默认启用可训练标志。可以将可训练属性设置为 False,以避免记录渐变信息。
除了创造 tf。可变张量通过普通张量,也可以直接创建,例如:
In [21]:
a = tf.Variable([[1,2],[3,4]]) # Directly create Variable type tensor
a
Out[21]:
<tf.Variable 'Variable:0' shape=(2, 2) dtype=int32, numpy=
array([[1, 2],
[3, 4]])>
特遣部队。可变张量可以认为是普通张量的一种特殊类型。实际上,为了支持自动微分功能,也可以通过 GradientTape.watch()方法将普通张量临时添加到跟踪梯度信息的列表中。
4.4 创建张量
在 TensorFlow 中,您可以通过多种方式创建张量,例如从 Python 列表、从 Numpy 数组或从已知的分布中创建。
4.4.1 从数组和列表创建张量
Numpy 数组和 Python 列表是 Python 中非常重要的数据容器。许多数据在转换为张量之前被加载到数组或列表中。TensorFlow 的输出数据通常也导出到数组或列表中,这使得它们很容易用于其他模块。
tf.convert_to_tensor 函数可用于从 Python 列表或 Numpy 数组创建新的张量,例如:
In [22]:
# Create a tensor from a Python list
tf.convert_to_tensor([1,2.])
Out[22]:
<tf.Tensor: id=86, shape=(2,), dtype=float32, numpy=array([1., 2.], dtype=float32)>
In [23]:
# Create a tensor from a Numpy array
tf.convert_to_tensor(np.array([[1,2.],[3,4]]))
Out[23]:
<tf.Tensor: id=88, shape=(2, 2), dtype=float64, numpy=
array([[1., 2.],
[3., 4.]])>
请注意,默认情况下,Numpy 浮点数组以 64 位精度存储数据。转换为张量类型时,精度为 tf.float64,需要时可以转换为 tf.float32。事实上,tf.constant()和 tf.convert_to_tensor()都可以自动将 Numpy 数组或 Python 列表转换为张量类型。
4.4.2 创建全 0 或全 1 张量
创建全 0 或全 1 的张量是一种非常常见的张量初始化方法。考虑线性变换 y = Wx + b 。权重矩阵 W 可以用全 1 的矩阵初始化, b 可以用全 0 的向量初始化。于是线性变换变为 y = x 。我们可以使用 tf.zeros()或 tf.ones()创建任意形状的全零或全一张量:
In [24]: tf.zeros([]),tf.ones([])
Out[24]:
(<tf.Tensor: id=90, shape=(), dtype=float32, numpy=0.0>,
<tf.Tensor: id=91, shape=(), dtype=float32, numpy=1.0>)
创建一个全 0 和全 1 的向量:
In [25]: tf.zeros([1]),tf.ones([1])
Out[25]:
(<tf.Tensor: id=96, shape=(1,), dtype=float32, numpy=array([0.], dtype=float32)>,
<tf.Tensor: id=99, shape=(1,), dtype=float32, numpy=array([1.], dtype=float32)>)
创建一个全零矩阵:
In [26]: tf.zeros([2,2])
Out[26]:
<tf.Tensor: id=104, shape=(2, 2), dtype=float32, numpy=
array([[0., 0.],
[0., 0.]], dtype=float32)>
创建一个全 1 矩阵:
In [27]: tf.ones([3,2])
Out[27]:
<tf.Tensor: id=108, shape=(3, 2), dtype=float32, numpy=
array([[1., 1.],
[1., 1.],
[1., 1.]], dtype=float32)>
使用 tf.zeros_like 和 tf.ones_like,您可以轻松地创建一个全为 0 或 1 的张量,它与另一个张量的形状一致。例如,下面是如何创建一个与张量 a 形状相同的全零张量:
In [28]: a = tf.ones([2,3]) # Create a 2x3 tensor with all 1s
tf.zeros_like(a) # Create a all zero tensor with the same shape of a
Out[28]:
<tf.Tensor: id=113, shape=(2, 3), dtype=float32, numpy=
array([[0., 0., 0.],
[0., 0., 0.]], dtype=float32)>
创建一个与张量 a 形状相同的全 1 张量:
In [29]: a = tf.zeros([3,2]) # Create a 3x2 tensor with all 0s tf.ones_like(a) # Create a all 1 tensor with the same shape of a
Out[29]:
<tf.Tensor: id=120, shape=(3, 2), dtype=float32, numpy=
array([[1., 1.],
[1., 1.],
[1., 1.]], dtype=float32)>
4.4.3 创建定制的数值张量
除了用全 0 或全 1 初始化张量之外,有时还需要用特定的值初始化张量,比如–1。使用 tf.fill(shape,value),我们可以创建一个具有特定数值的张量,其中维度由 shape 参数指定。例如,下面是如何用 element–1 创建一个标量:
In [30]:tf.fill([], -1) #
Out[30]:
<tf.Tensor: id=124, shape=(), dtype=int32, numpy=-1>
创建一个包含所有元素的向量–1:
In [31]:tf.fill([1], -1)
Out[31]:
<tf.Tensor: id=128, shape=(1,), dtype=int32, numpy=array([-1])>
创建一个包含所有元素的矩阵 99:
In [32]:tf.fill([2,2], 99) # Create a 2x2 matrix with all 99s
Out[32]:
<tf.Tensor: id=136, shape=(2, 2), dtype=int32, numpy=
array([[99, 99],
[99, 99]])>
4.4.4 根据已知分布创建张量
有时,创建从普通分布(如正态(或高斯)和均匀分布)采样的张量非常有用。例如,在卷积神经网络中,卷积核 W 通常从正态分布初始化,以便于训练过程。在敌对网络中,隐藏变量 z 通常从均匀分布中取样。
使用 tf.random.normal(shape,mean=0.0,stddev=1.0),我们可以创建一个张量,其维数由形状参数和从正态分布 N ( mean , stddev 2 )中采样的值定义。例如,以下是如何从均值为 0、标准差为 1 的正态分布创建张量:
In [33]: tf.random.normal([2,2]) # Create a 2x2 tensor from a normal distribution
Out[33]:
<tf.Tensor: id=143, shape=(2, 2), dtype=float32, numpy=
array([[-0.4307344 , 0.44147003],
[-0.6563149 , -0.30100572]], dtype=float32)>
根据均值为 1、标准差为 2 的正态分布创建张量:
In [34]: tf.random.normal([2,2], mean=1,stddev=2)
Out[34]:
<tf.Tensor: id=150, shape=(2, 2), dtype=float32, numpy=
array([[-2.2687864, -0.7248812],
[ 1.2752185, 2.8625617]], dtype=float32)>
利用 tf.random.uniform(shape,minval=0,maxval=None,dtype=tf.float32),我们可以创建一个从区间[ minval ,maxval]采样的均匀分布张量。例如,下面是如何创建一个从区间[0,1]均匀采样的矩阵,其形状为[2,2]:
In [35]: tf.random.uniform([2,2])
Out[35]:
<tf.Tensor: id=158, shape=(2, 2), dtype=float32, numpy=
array([[0.65483284, 0.63064325],
[0.008816 , 0.81437767]], dtype=float32)>
创建一个从区间[0,10]均匀采样的矩阵,形状为[2,2]:
In [36]: tf.random.uniform([2,2],maxval=10)
Out[36]:
<tf.Tensor: id=166, shape=(2, 2), dtype=float32, numpy=
array([[4.541913 , 0.26521802],
[2.578913 , 5.126876 ]], dtype=float32)>
如果我们需要对整数进行统一采样,我们必须指定 maxval 参数,并将数据类型设置为 tf.int*:
In [37]:
# Create a integer tensor from a uniform distribution with interval [0,100)
tf.random.uniform([2,2],maxval=100,dtype=tf.int32)
Out[37]:
<tf.Tensor: id=171, shape=(2, 2), dtype=int32, numpy=
array([[61, 21],
[95, 75]])>
请注意,来自所有随机函数的这些输出可能是不同的。但是,这并不影响这些功能的使用。
创建一个序列
当循环或索引一个张量时,经常需要创建一个连续的整数序列,这可以通过 tf.range()函数来实现。函数 tf.range(limit,delta=1)可以创建步长为 delta 且在区间[0, limit 内的整数序列。例如,以下是如何创建步长为 1 的 0–10 的整数序列:
In [38]: tf.range(10) # 0~10, 10 is not included
Out[38]:
<tf.Tensor: id=180, shape=(10,), dtype=int32, numpy=array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])>
创建一个 0 到 10 之间的整数序列,步长为 2:
In [39]: tf.range(10,delta=2) # 10 is not included
Out[39]:
<tf.Tensor: id=185, shape=(5,), dtype=int32, numpy=array([0, 2, 4, 6, 8])>
利用 tf.range(start,limit,delta=1),我们可以在区间[ start ,limit]内创建一个整数序列,步长为 delta:
In [40]: tf.range(1,10,delta=2) # 1~10, 10 is not included
Out[40]:
<tf.Tensor: id=190, shape=(5,), dtype=int32, numpy=array([1, 3, 5, 7, 9])>
4.5 张量的典型应用
在介绍了张量的性质和创建方法之后,下面将介绍张量在各个维度的典型应用,让读者直观地想到它们的主要物理意义和用途,为后续张量的维度变换等一系列抽象运算的学习奠定基础。
这一节不可避免的会提到以后章节要学的网络模型或者算法。你现在不需要完全理解他们,但可以有一个初步的印象。
标量
在 TensorFlow 中,标量是最容易理解的。它是一个维数为 0 的简单数字,形状为[]。标量的典型用途是表示错误值和各种度量,如准确度、精确度和召回率。
考虑模型的训练曲线。如图 4-1 所示,x 轴为训练步数,y 轴为每查询图像误差变化损失(图 4-1 (a))和准确度变化(图 4-1 (b)),其中损失值和准确度为张量计算生成的标量。
图 4-1
损耗和精度曲线
以均方误差函数为例。tf.keras.losses.mse(或 tf.keras.losses.MSE,同一个函数)返回每个样本的误差值,最后取误差的平均值作为当前批次的误差后,自动变成标量:
In [41]:
out = tf.random.uniform([4,10]) # Create a model output example
y = tf.constant([2,3,2,0]) # Create a real observation
y = tf.one_hot(y, depth=10) # one-hot encoding
loss = tf.keras.losses.mse(y, out) # Calculate MSE for each sample
loss = tf.reduce_mean(loss) # Calculate the mean of MSE
print(loss)
Out[41]:
tf.Tensor(0.19950335, shape=(), dtype=float32)
4.5.2 矢量
向量在神经网络中非常常见。比如在全连接网络和卷积神经网络中,偏置张量 b 用向量来表示。如图 4-2 所示,在每个全连接层的输出节点上加一个偏置值,所有输出节点的偏置用向量形式表示b=b1,b2T:
![img/515226_1_En_4_Fig2_HTML.png
图 4-2
偏置向量的应用
考虑两个输出节点的网络层,我们创建长度为 2 的偏置向量,并在每个输出节点上加回:
In [42]:
# Suppose z is the output of an activation function
z = tf.random.normal([4,2])
b = tf.zeros([2]) # Create a bias vector
z = z + b
Out[42]:
<tf.Tensor: id=245, shape=(4, 2), dtype=float32, numpy=
array([[ 0.6941646 , 0.4764454 ],
[-0.34862405, -0.26460952],
[ 1.5081744 , -0.6493869 ],
[-0.26224667, -0.78742725]], dtype=float32)>
注意,形状为[4,2]的张量 z 和形状为[2]的向量 b 可以直接相加。这是为什么呢?我们将在稍后的“广播”部分揭示它。
对于通过高级接口类 Dense()创建的网络层,张量 W 和 b 由类内部自动创建和管理。偏置变量 b 可以通过全连接层的偏置成员访问。例如,如果创建了具有四个输入节点和三个输出节点的线性网络层,则它的偏置向量 b 应该具有长度 3,如下所示:
In [43]:
fc = layers.Dense(3) # Create a dense layer with output length of 3
# Create W and b through build function with input nodes of 4
fc.build(input_shape=(2,4))
fc.bias # Print bias vector
Out[43]:
<tf.Variable 'bias:0' shape=(3,) dtype=float32, numpy=array([0., 0., 0.], dtype=float32)>
可以看出,类的 bias 成员是一个长度为 3 的向量,初始化为全 0。这也是 bias b 的默认初始化方案。此外,偏置向量的类型是可变的,因为对于 W 和 b 都需要梯度信息。
矩阵
矩阵也是一种非常常见的张量。例如,一个全连通层的批量输入张量 X 的形状为 中的 b , d ,其中 b 表示输入样本的个数,即批量大小, 中的 d 表示输入特征的长度。例如,特征长度 4 和包含总共两个样本的输入可以表示为矩阵:
x = tf.random.normal([2,4]) # A tensor with 2 samples and 4 features
设全连通层的输出节点数为三,则它的权张量形状W【4,3】。我们可以使用张量 X 、 W 和向量 b 直接实现一个网络层。代码如下:
In [44]:
w = tf.ones([4,3])
b = tf.zeros([3])
o = x@w+b # @ means matrix multiplication
Out[44]:
<tf.Tensor: id=291, shape=(2, 3), dtype=float32, numpy=
array([[ 2.3506963, 2.3506963, 2.3506963],
[-1.1724043, -1.1724043, -1.1724043]], dtype=float32)>
在前面的代码中, X 和 W 都是矩阵。前面的代码实现了线性变换网络层,激活函数为空。一般来说,网络层σ(X@W+b)称为全连通层,可以直接用 TensorFlow 中的 Dense()类实现。特别地,当激活函数 σ 为空时,全连通层也称为线性层。我们可以通过 Dense()类创建一个具有四个输入节点和三个输出节点的网络层,并通过全连接层的内核成员查看其权重矩阵 W :
In [45]:
fc = layers.Dense(3) # Create fully-connected layer with 3 output nodes
fc.build(input_shape=(2,4)) # Define the input nodes to be 4
fc.kernel # Check kernel matrix W
Out[45]:
<tf.Variable 'kernel:0' shape=(4, 3) dtype=float32, numpy=
array([[ 0.06468129, -0.5146048 , -0.12036425],
[ 0.71618867, -0.01442951, -0.5891943 ],
[-0.03011459, 0.578704 , 0.7245046 ],
[ 0.73894167, -0.21171576, 0.4820758 ]], dtype=float32)>
4.5.4 三维张量
三维张量的典型应用是表示序列信号。其格式为
)
其中序列信号的个数为 b ,序列长度表示时间维度上采样点或步长的个数,特征长度表示每个点的特征长度。
考虑自然语言处理(NLP)中句子的表示,比如评价一个句子是否是正面情感的情感分类网络,如图 4-3 所示。为了便于神经网络对字符串的处理,一般通过嵌入层将单词编码成固定长度的向量。例如,“a”被编码为长度为 3 的向量。那么两个长度相等的句子(每个句子有五个单词)可以表示为一个形状为[2,5,3]的三维张量,其中 2 代表句子的数量,5 代表单词的数量,3 代表编码后的单词向量的长度。我们演示如何通过 IMDB 数据集来表示句子,如下所示:
In [46]: # Load IMDB dataset
from tensorflow import keras
(x_train,y_train),(x_test,y_test)=keras.datasets.imdb.load_data(num_words=10000)
# Convert each sentence to length of 80 words
x_train = keras.preprocessing.sequence.pad_sequences(x_train,maxlen=80)
x_train.shape
Out [46]: (25000, 80)
我们可以看到 x_train 的形状是[25000,80],其中 25000 代表句子的数量,80 代表每个句子总共 80 个单词,每个单词用一种数字编码的方式表示。接下来,我们使用层。嵌入函数将每个数字编码字转换为长度为 100 的向量:
In [47]: # Create Embedding layer with 100 output length
embedding=layers.Embedding(10000, 100)
# Convert numeric encoded words to word vectors
out = embedding(x_train)
out.shape
Out[47]: TensorShape([25000, 80, 100])
通过嵌入层,句子张量的形状变成了[25000,80,100],其中 100 表示每个单词都被编码为长度为 100 的向量。
图 4-3
情感分类网络
对于具有一个特征的序列信号,比如一个产品在 60 天内的价格,只需要一个标量来表示产品价格,那么两个产品的价格变化就可以用一个形状为[2,60]的张量来表示。为了便于格式统一,价格变化也可以表示为形状的张量[2,60,1],其中 1 表示特征长度为 1。
4.5.5 四维张量
大多数时候我们只使用维数小于 5 的张量。对于更高维张量,例如元学习中的五维张量表示,可以应用类似的原理。四维张量广泛应用于卷积神经网络中。它们用于保存要素地图。格式一般定义为
)
其中 b 表示输入样本的数量; h 和 w 分别代表特征图的高度和宽度;而 c 是通道数。有些深度学习框架也使用[ b , c , h , w ]的格式,比如 PyTorch。影像数据是一种特征地图。具有 RGB 三个通道的彩色图像包含像素的 h 行和 w 列。每个点需要三个值来表示 RGB 通道的颜色强度,因此可以使用形状为[ h , w ,3]的张量来表示一幅图片。如图 4-4 所示,上图代表原始图像,包含了三个下通道的亮度信息。
图 4-4
RGB 图像的特征图
在神经网络中,一般会并行计算多个输入以提高计算效率,所以 b 图片的张量可以表示为[ b 、 h 、 w ,3]:
In [48]:
# Create 4 32x32 color images
x = tf.random.normal([4,32,32,3])
# Create convolutional layer
layer = layers.Conv2D(16,kernel_size=3)
out = layer(x)
out.shape
Out[48]: TensorShape([4, 30, 30, 16])
卷积核张量也是一个四维张量,可以通过核成员变量来访问:
In [49]: layer.kernel.shape
Out[49]: TensorShape([3, 3, 3, 16])
4.6 索引和切片
张量数据的一部分可以通过索引和切片操作提取出来,这是非常常用的。
索引
在 TensorFlow 中,支持标准的 Python 索引方式,比如[ i ][ j ]以及逗号和“:”。考虑四张 32 × 32 大小的彩色图片(为方便起见,大部分张量由随机正态分布产生,下同)。相应的张量具有如下形状[4,32,32,3]:
x = tf.random.normal([4,32,32,3])
接下来,我们使用索引方法从张量中读取部分数据。
-
读取第一图像数据:
-
阅读第一幅图的第二行:
x = tf.random.normal ([4,32,32,3]) # Create a 4D tensor
In [51]: x[0] # Index 0 indicates the 1st element in Python
Out[51]:<tf.Tensor: id=379, shape=(32, 32, 3), dtype=float32, numpy=
array([[[ 1.3005302 , 1.5301839 , -0.32005513],
[-1.3020388 , 1.7837263 , -1.0747638 ], ...
[-1.1092019 , -1.045254 , -0.4980363 ],
[-0.9099222 , 0.3947732 , -0.10433522]]], dtype=float32)>
- 阅读第一幅图片的第二行第三列:
In [52]: x[0][1]
Out[52]:
<tf.Tensor: id=388, shape=(32, 3), dtype=float32, numpy=
array([[ 4.2904025e-01, 1.0574218e+00, 3.1540772e-01],
[ 1.5800388e+00, -8.1637271e-02, 6.3147342e-01], ...,
[ 2.8893018e-01, 5.8003378e-01, -1.1444757e+00],
[ 9.6100050e-01, -1.0985689e+00, 1.0827581e+00]], dtype=float32)>
- 选择第三张图片的第二行、第一列和第二(B)通道:
In [53]: x[0][1][2]
Out[53]:
<tf.Tensor: id=401, shape=(3,), dtype=float32, numpy=array([-0.55954427, 0.14497331, 0.46424514], dtype=float32)>
In [54]: x[2][1][0][1]
Out[54]:
<tf.Tensor: id=418, shape=(), dtype=float32, numpy=-0.84922135>
当维数较大时,使用[ i ][ j 的方式…【 k 不方便。相反,我们可以使用[ i , j ,…, k 进行分度。它们是等价的。
- 阅读第二幅图片的第十行第三列:
In [55]: x[1,9,2]
Out[55]:
<tf.Tensor: id=436, shape=(3,), dtype=float32, numpy=array([ 1.7487534 , -0.41491988, -0.2944692 ], dtype=float32)>
切片
使用格式start:end:step可以很容易地提取一段数据,其中 start 是起始位置的索引,end 是结束位置的索引(不包括),step 是采样步长。
以形状为[4,32,32,3]的图像张量为例,我们将说明如何使用切片来获得不同位置的数据。例如,如下阅读第二幅和第三幅图片:
In [56]: x[1:3]
Out[56]:
<tf.Tensor: id=441, shape=(2, 32, 32, 3), dtype=float32, numpy=
array([[[[ 0.6920027 , 0.18658352, 0.0568333 ],
[ 0.31422952, 0.75933754, 0.26853144],
[ 2.7898 , -0.4284912 , -0.26247284],...
开始 : 结束 : 步骤刀法有很多缩写。可以根据需要有选择地省略开始、结束和步进参数。当像::,都被省略时,表示读取是从开始到结束,步长为 1。比如 x [0,:]表示读取第一张图片的所有行,其中::表示行维度的所有行,相当于 x [0]:
In [57]: x[0,::] # Read 1st picture
Out[57]:
<tf.Tensor: id=446, shape=(32, 32, 3), dtype=float32, numpy=
array([[[ 1.3005302 , 1.5301839 , -0.32005513],
[-1.3020388 , 1.7837263 , -1.0747638 ],
[-1.1230233 , -0.35004002, 0.01514002],
...
为简洁起见,::可以缩写为单个冒号:,例如:
In [58]: x[:,0:28:2,0:28:2,:]
Out[58]:
<tf.Tensor: id=451, shape=(4, 14, 14, 3), dtype=float32, numpy=
array([[[[ 1.3005302 , 1.5301839 , -0.32005513],
[-1.1230233 , -0.35004002, 0.01514002],
[ 1.3474811 , 0.639334 , -1.0826371 ],
...
前面的代码表示读取所有图片,隔行采样,读取所有通道数据,相当于缩放图片原来高度和宽度的 50%。
我们来总结一下不同的切片方式,从第一个元素开始读取时可以省略“start”,即取最后一个元素时可以省略“start = 0”,取最后一个元素时可以省略“end”,步长为 1 时可以省略“step”。详情汇总在表 4-1 中。
表 4-1
切片方法概述
|方法
|
意义
|
| — | — |
| 开始:结束:步骤 | 从“开始”读到“结束”(不包括),步长为“步长” |
| 出发 | 从“开始”读到“结束”(不含),步长为 1。 |
| 开始: | 以步长 1 从“开始”读到对象的结尾。 |
| 开始::步骤 | 以“步长”从“起点”读取到对象的终点 |
| :结束:步骤 | 从第 0 项读到“end”(不含),步长为“step” |
| :结束 | 从第 0 项读到“end”(不含),步长为 1。 |
| *步骤 | 从第 0 项读取到最后一项,步长为“step” |
| :: | 阅读所有项目。 |
| : | 阅读所有项目。 |
特别地,步长可以是负的。例如,start:end:—1 表示从“start”开始,逆序读取,以“end”结束(不含),索引“end”小于“start”考虑一个从 0 到 9 的简单序列向量,以相反的顺序取第一个元素,不包括第一个元素:
In [59]: x = tf.range(9) # Create the vector
x[8:0:-1] # Reverse slicing
Out[59]:
<tf.Tensor: id=466, shape=(8,), dtype=int32, numpy=array([8, 7, 6, 5, 4, 3, 2, 1])>
按如下相反顺序提取所有元素:
In [60]: x[::-1]
Out[60]:
<tf.Tensor: id=471, shape=(9,), dtype=int32, numpy=array([8, 7, 6, 5, 4, 3, 2, 1, 0])>
每两个项目反向采样的实现方式如下:
In [61]: x[::-2]
Out[61]:
<tf.Tensor: id=476, shape=(5,), dtype=int32, numpy=array([8, 6, 4, 2, 0])>
读取每个图像的所有通道,其中行和列以相反的顺序每两个元素采样一次。实现如下:
In [62]: x = tf.random.normal([4,32,32,3])
x[0,::-2,::-2]
Out[62]:
<tf.Tensor: id=487, shape=(16, 16, 3), dtype=float32, numpy=
array([[[ 0.63320625, 0.0655185 , 0.19056146],
[-1.0078577 , -0.61400175, 0.61183935],
[ 0.9230892 , -0.6860094 , -0.01580668],
...
当张量维数较大时,不需要采样的维数一般用单冒号“:”表示所有元素都被选中。这样一来,可能会出现很多“:”。考虑形状为[4,32,32,3]的图像张量。当需要读取绿色通道上的数据时,前面的所有维度都被提取为
In [63]: x[:,:,:,1] # Read data on Green channel
Out[63]:
<tf.Tensor: id=492, shape=(4, 32, 32), dtype=float32, numpy=
array([[[ 0.575703 , 0.11028383, -0.9950867 , ..., 0.38083118, -0.11705163, -0.13746642],
...
为了避免出现像 x [:,:,:,1]冒号太多的情况,我们可以使用符号“⋯”来取多维度的所有数据,其中维度的个数需要根据规则自动推断:当符号⋯以切片方式出现时,“⋯”左边的维度会自动向最左边对齐。符号“⋯”右侧的尺寸将自动与最右侧对齐。系统将自动推断由符号“⋯".”表示的维数详情汇总在表 4-2 中。
表 4-2
"…"切片方法摘要
|方法
|
意义
|
| — | — |
| a, ⋯ ,b | 为维度 a 选择 0 到 a,为维度 b 选择 b 到结束,为其他维度选择所有元素。 |
| 一、 ⋯ | 为维度 a 选择 0 到 a,为其他维度选择所有元素。 |
| ⋯ ,b | 选择 b 以结束维 b 和其他维的所有元素。 |
| ⋯ | 读取所有元素。 |
我们列举更多的例子如下:
-
读取第一和第二图片的绿色和蓝色通道数据:
-
阅读最后两张图片:
In [64]: x[0:2,...,1:]
Out[64]:
<tf.Tensor: id=497, shape=(2, 32, 32, 2), dtype=float32, numpy=
array([[[[ 0.575703 , 0.8872789 ],
[ 0.11028383, -0.27128693],
[-0.9950867 , -1.7737272 ],
...
- 读取红色和绿色通道数据:
In [65]: x[2:,...] # equivalent to x[2:]
Out[65]:
<tf.Tensor: id=502, shape=(2, 32, 32, 3), dtype=float32, numpy=
array([[[[-8.10753584e-01, 1.10984087e+00, 2.71821529e-01],
[-6.10031188e-01, -6.47952318e-01, -4.07003373e-01],
[ 4.62206364e-01, -1.03655539e-01, -1.18086267e+00],
...
In [66]: x[...,:2]
Out[66]:
<tf.Tensor: id=507, shape=(4, 32, 32, 2), dtype=float32, numpy=
array([[[[-1.26881 , 0.575703 ],
[ 0.98697686, 0.11028383],
[-0.66420585, -0.9950867 ],
...
切片总结
张量索引和切片方法多种多样,尤其是切片操作,初学者很容易混淆。本质上,切片操作只有这个基本形式开始 : 结束 : 步骤。通过这种基本形式,有目的地省略一些默认参数,并派生出多个缩写方法。所以写起来更容易更快。由于深度学习一般处理的维数在四维以内,所以你会发现张量切片运算在深度学习中并没有那么复杂。
4.7 维度转换
在神经网络中,维度变换是最核心的张量运算。通过维度变换,数据可以任意切换,满足不同情况的计算需求。考虑线性图层的批处理形式:
)
假设两个样本,每个样本的特征长度为 4,包含在 X 中,形状为【2,4】。线性层的输出节点数为三,即 W 的形状为【4,3】,定义 b 的形状为【3】。那么 X @ W 的结果具有[2,3]的形状。注意,我们还需要添加形状为[3]的 b 。如何将两个不同形状的张量直接相加?
回想一下,我们要做的是给每层的每个输出节点增加一个偏置。这种偏差由每个节点的所有样本共享。换句话说,每个样本应该在每个节点增加相同的偏置,如图 4-5 所示。
图 4-5
线性层的偏差
因此,对于两个样本的输入 X ,我们需要复制偏倚
)
将样本数转化为如下矩阵形式
)
然后加上X’=X@W
)
因为此时它们的形状相同,这就满足了矩阵加法的要求:
)
这样既满足了矩阵加法需要形状一致的要求,又实现了每个输入样本的输出节点共享偏置向量的逻辑。为了实现这一点,我们向偏置向量 b 插入一个新的维度 batch,然后复制 batch 维度中的数据,以获得形状为[2,3]的转换版本B’。这一系列的操作称为维度变换。
每种算法对张量格式都有不同的逻辑要求。当现有的张量格式不满足算法要求时,需要通过量纲变换将张量调整到正确的格式。基本维度转换包括诸如改变视图(reshape())、插入新维度(expand_dims())、删除维度(squeeze())和交换维度(transpose())之类的功能。
重塑
在介绍整形操作之前,我们先来了解一下张量存储和视图的概念。张量的观点就是我们理解张量的方式。例如,形状[2,4,4,3]的张量在逻辑上被理解为两个图片,每个图片具有四行和四列,并且每个像素具有三个通道的 RGB 数据。张量的存储体现在张量在内存中是作为一个连续的区域存储的。对于同一个存储,我们可以有不同的看法。对于[2,4,4,3]张量,我们可以把它看作两个样本,每个样本的特征是一个长度为 48 的向量。同一个张量可以产生不同的视图。这就是存储和视图的关系。视图生成非常灵活,但需要合理。
我们可以通过 tf.range()生成一个向量,通过 tf.reshape()函数生成不同的视图,例如:
In [67]: x=tf.range(96)
x=tf.reshape(x,[2,4,4,3]) # Change view to [2,4,4,3] without change storage
Out[67]: # Data is not changed, only view is changed.
<tf.Tensor: id=11, shape=(2, 4, 4, 3), dtype=int32, numpy=
array([[[[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11]],...
存储数据时,内存不支持这种维度层次概念,数据只能以分块和顺序的方式写入内存。所以这种层次关系需要人工管理,即需要人工跟踪每个张量的存储顺序。为了便于表达,我们将张量形状列表左侧的维度称为大维度,将形状列表右侧的维度称为小维度。例如,在形状为[2,4,4,3]的张量中,图像 2 的数量被称为大维度,通道 3 的数量被称为小维度。在小维度优先写入的优先级设置下,前一个张量 x 的内存布局为
| one | Two | three | four | five | six | seven | eight | nine | ... | ... | ... | Ninety-three | Ninety-four | Ninety-five |改变张量的观点只会改变理解张量的方式。它不会改变存储顺序。因为写入大量数据会消耗更多的计算资源,所以这样做是为了提高计算效率。由于数据在存储时只有扁平化的结构,与逻辑结构是分离的,新的逻辑结构(视图)不需要改变数据存储方式,可以节省大量的计算资源。改变视图操作在提供便利的同时,也带来了很多逻辑上的危险。改变视图操作的默认前提是存储不变;否则,更改视图操作是非法的。我们首先介绍合法的视图转换操作,然后介绍一些非法的视图转换。
比如张量 A 按照 b 、 h 、 w 、 c 的初始视图写入内存。如果我们改变理解方式,它可以有以下格式:
-
张量 b , h ⋅ w , c 用 h ⋅ w 像素和 c 通道表示 b 图片。
-
张量[ b , h , w ⋅ c 用 h 线条表示 b 图片,每条线的特征长度为 w ⋅ c 。
-
张量[ b , h ⋅ w ⋅ c 代表 b 图片,每张图片的特征长度为h⋅w⋅c。
前面视图的存储不需要改变,所以都是正确的。
从语法上来说,视图转换只需要确保新视图的元素总数和存储区域的大小相等,即新视图的元素数等于
![ b ∙ h ∙ w ∙ c b\bullet h\bullet w\bullet c b∙h∙w∙c
正是因为视图设计的语法约束很少,完全由用户定义,所以在改变视图时容易出现逻辑风险。
现在让我们考虑非法的视图转换。例如,如果新视图被定义为[ b 、 w 、 h 、 c 、 b 、 c 、 h 、 w ,或者[ b 、 c 、 h 、w如果存储顺序没有同步更新,恢复的数据会与新视图不一致,造成数据混乱。这就需要用户了解数据,才能确定操作是否合法。我们将在“交换维度”一节中展示如何改变张量的存储。**
正确使用视图转换操作的一种技术是跟踪存储维度的顺序。比如初始视图中保存的“图片数-行-列-通道”的张量,存储也是按照“图片数-行-列-通道”的顺序写的。如果用“图片数-像素-通道”的方法恢复视图,与“图片数-行-列-通道”不冲突,所以可以得到正确的数据。但如果用“图片数-通道数-像素”的方法恢复数据,由于内存布局是按照“图片数-行-列-通道数”的顺序,视图维的顺序与存储维的顺序不一致,导致数据杂乱。
改变视图是神经网络中非常常见的操作。您可以通过串联多个整形操作来实现复杂的逻辑。但是,当通过 reshape 更改视图时,您必须始终记住张量的存储顺序。新视图的维度顺序必须与存储顺序相同。否则,您需要通过交换维度操作来同步存储顺序。例如,对于具有形状[4,32,32,3]的图像数据,可以通过整形操作将形状调整为[4,1024,3]。视图的维度顺序为b—像素—c,张量的存储顺序为[ b , h , w , c ]。形状为[4,1024,3]的张量可以恢复为以下形式:
-
当[ b , h , w , c ] = [4,32,32,3]时,新视图的维度顺序和存储顺序一致,可以恢复数据,不会出现紊乱。
-
当[ b , w , h , c ] = [4,32,32,3]时,新视图的维度顺序与存储顺序冲突。
-
当[h∷w∷c, b ] = [3072,4]时,新视图的维度顺序与存储顺序冲突。
在 TensorFlow 中,我们可以通过张量的 ndim 和 shape 属性获得张量的维数和形状:
In [68]: x.ndim,x.shape # Get the tensor's dimension and shape
Out[68]:(4, TensorShape([2, 4, 4, 3]))
使用 TF . shape(x,new_shape),我们可以合法地任意改变张量的视图,例如:
In [69]: tf.reshape(x,[2,-1])
Out[69]:<tf.Tensor: id=520, shape=(2, 48), dtype=int32, numpy=
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, 24, 25, 26, 27, 28, 29, 30, 31,...
80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95]])>
参数–1 表示当前轴上的长度需要根据张量的总元素不变的规则自动导出。例如,前面的–1 可以推导为
)
将数据视图再次更改为[2,4,12],如下所示:
In [70]: tf.reshape(x,[2,4,12])
Out[70]:<tf.Tensor: id=523, shape=(2, 4, 12), dtype=int32, numpy=
array([[[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],...
[36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]],
[[48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59], ...
[84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95]]])>
将数据视图再次更改为[2,16,3],如下所示:
In [71]: tf.reshape(x,[2,-1,3])
Out[71]:<tf.Tensor: id=526, shape=(2, 16, 3), dtype=int32, numpy=
array([[[ 0, 1, 2], ...
[45, 46, 47]],
[[48, 49, 50],...
[93, 94, 95]]])>
通过前面一系列连续的视图变换操作,我们需要知道张量的存储顺序没有改变,数据仍然按照 0,1,2,⋯,95 的初始顺序存储在内存中。
4.7.2 添加和删除尺寸
增加一个维度。添加一个长度为 1 的维度相当于在原始数据中添加一个新维度的概念。维度长度为 1,所以不需要改变数据;这只是观点的改变。
考虑一个具体的例子。大灰度图像的数据保存为 28 × 28 形状的张量。最后,一个新的维度被添加到张量中,它被定义为通道的数量。那么张量的形状变成[28,28,1]如下:
In [72]: # Generate a 28x28 matrix
x = tf.random.uniform([28,28],maxval=10,dtype=tf.int32)
Out[72]:
<tf.Tensor: id=11, shape=(28, 28), dtype=int32, numpy=
array([[6, 2, 0, 0, 6, 7, 3, 3, 6, 2, 6, 2, 9, 3, 0, 3, 2, 8, 1, 3, 6, 2, 3, 9, 3, 6, 1, 7],...
使用 tf.expand_dims (x,axis),我们可以在指定的轴之前插入一个新的维度:
In [73]: x = tf.expand_dims(x,axis=2)
Out[73]:
<tf.Tensor: id=13, shape=(28, 28, 1), dtype=int32, numpy=
array([[[6],
[2],
[0],
[0],
[6],
[7],
[3],...
可以看到,插入新维度后,数据的存储顺序并没有改变。插入新维度后,只有数据视图会发生变化。
同样,我们可以在前面插入一个新的维度,表示长度为 1 的图像数维度。这时张量的形状变成[1,28,28,1]:
In [74]: x = tf.expand_dims(x,axis=0) # Insert a dimension at the beginning
Out[74]:
<tf.Tensor: id=15, shape=(1, 28, 28, 1), dtype=int32, numpy=
array([[[[6],
[2],
[0],
[0],
[6],
[7],
[3],...
注意,当 tf.expand_dims 的轴为正时,意味着在当前维度之前插入了一个新维度;当它为负值时,表示在当前尺寸之后插入了一个新尺寸。以张量 b 、 h 、 w 、 c 为例,不同轴参数的实际插入位置如图 4-6 所示。
图 4-6
不同轴参数的插入位置
删除一个尺寸。删除维度是添加维度的反向操作。与添加维度一样,删除维度只能删除长度为 1 的维度,并且不会改变张量的存储顺序。继续考虑形状[1,28,28,1]的例子。如果我们想删除图片维数,我们可以使用 tf.squeeze (x,axis)函数。axis 参数是要删除的维度的索引号:
In [75]: x = tf.squeeze(x, axis=0) # Delete the image number dimension
Out[75]:
<tf.Tensor: id=586, shape=(28, 28, 1), dtype=int32, numpy=
array([[[8],
[2],
[2],
[0],...
继续删除频道号维度。由于删除了图像数维,此时 x 的形状为[28,28,1]。删除通道号维度时,我们应该指定 axis = 2,如下所示:
In [76]: x = tf.squeeze(x, axis=2) # Delete channel dimension
Out[76]:
<tf.Tensor: id=588, shape=(28, 28), dtype=int32, numpy=
array([[8, 2, 2, 0, 7, 0, 1, 4, 9, 1, 7, 4, 8, 2, 7, 4, 8, 2, 9, 8, 8, 0, 9, 9, 7, 5, 9, 7],
[3, 4, 9, 9, 0, 6, 5, 7, 1, 9, 9, 1, 2, 7, 2, 7, 5, 3, 3, 7, 2, 4, 5, 2, 7, 3, 8, 0],...
如果我们不指定尺寸参数 axis,即 tf.squeeze(x),它将默认删除所有长度为 1 的尺寸,例如:
In [77]:
x = tf.random.uniform([1,28,28,1],maxval=10,dtype=tf.int32)
tf.squeeze(x) # Delete all dimensions with length 1
Out[77]:
<tf.Tensor: id=594, shape=(28, 28), dtype=int32, numpy=
array([[9, 1, 4, 6, 4, 9, 0, 0, 1, 4, 0, 8, 5, 2, 5, 0, 0, 8, 9, 4, 5, 0, 1, 1, 4, 3, 9, 9],...
建议逐个指定要删除的维度参数,以防止 TensorFlow 意外删除某些长度为 1 的维度,导致计算结果无效。
交换尺寸
改变视图或添加或删除维度不会影响张量的存储。有时,仅仅改变对张量的理解而不改变量纲的顺序是不够的。也就是需要直接调整存储顺序。通过交换维度,张量的存储顺序和视图都发生了变化。
交换维度操作非常常见。例如,一个图像张量在 TensorFlow 中默认的存储格式是[ b 、 h 、 w 、 c 格式,但是有些库的图像格式是[ b 、 c 、 h 、 w 格式。我们以[ b , h , w , c 到[ b , c , h , w 的转换为例,介绍如何使用 tf.transpose(x,perm)函数完成维度交换操作,其中参数 perm 代表新维度的顺序。考虑形状为[2,32,32,3]的图像张量,“图片数,行数,列数,通道数”的维数指标分别为 0,1,2,3。如果新维度的顺序是“图片数、通道数、行数、列数”,那么对应的索引号就变成了[0,3,1,2],所以需要将参数 perm 设置为[0,3,1,2]。实现如下:
In [78]: x = tf.random.normal([2,32,32,3])
tf.transpose(x,perm=[0,3,1,2]) # Swap dimension
Out[78]:
<tf.Tensor: id=603, shape=(2, 3, 32, 32), dtype=float32, numpy=
array([[[[-1.93072677e+00, -4.80163872e-01, -8.85614634e-01, ...,
1.49124235e-01, 1.16427064e+00, -1.47740364e+00],
[-1.94761145e+00, 7.26879001e-01, -4.41877693e-01, ...
如果我们要将[ b 、 h 、 w 、 c 改为[ b 、 w 、 h 、 c ],即交换高度和宽度尺寸,则新的尺寸索引变为[0,2,1,3],如下所示:
In [79]:
x = tf.random.normal([2,32,32,3])
tf.transpose(x,perm=[0,2,1,3]) # Swap dimension
Out[79]:
<tf.Tensor: id=612, shape=(2, 32, 32, 3), dtype=float32, numpy=
array([[[[ 2.1266546 , -0.64206547, 0.01311932],
[ 0.918484 , 0.9528751 , 1.1346699 ],
...,
需要注意的是,通过 tf.transpose 完成维度交换后,张量的存储顺序发生了变化,视图也相应发生了变化。所有后续操作都必须基于新的订单和视图。与改变视图操作相比,维度交换操作的计算开销更大。
复制数据
插入新维度后,我们可能希望复制新维度上的数据,以满足后续计算的要求。考虑例子Y=X@W+b。在插入一个样本数为 b 的新维度后,我们需要复制新维度中的批量数据,并将 b 的形状改为与 X @ W 一致,以完成张量加法运算。
我们可以使用 tf.tile(x,倍数)函数来完成指定维度的数据复制操作。参数 multiples 分别指定每个维度的复制编号。例如,1 表示不会复制数据,2 表示新长度是原始长度的两倍。
以输入[2,4]和三输出节点线性变换层为例,偏差 b 定义为
)
通过 tf.expand_dims(b,axis = 0)插入一个新的维度,变成一个矩阵:
)
现在 B 的形状变成了【1,3】。我们需要根据输入样本的数量在轴= 0 的维度上复制数据。这里的批量是 2,也就是做了一份拷贝就变成了
)
通过 tf.tile(b,倍数= [2,1]),可以在 axis = 0 维复制一次,在 axis = 1 维不复制。首先,插入一个新维度如下:
In [80]:
b = tf.constant([1,2]) # Create tensor b
b = tf.expand_dims(b, axis=0) # Insert new dimension
b
Out[80]:
<tf.Tensor: id=645, shape=(1, 2), dtype=int32, numpy=array([[1, 2]])>
复制批处理维度中数据的一个副本,以实现以下目的:
In [81]: b = tf.tile(b, multiples=[2,1])
Out[81]:
<tf.Tensor: id=648, shape=(2, 2), dtype=int32, numpy=
array([[1, 2],
[1, 2]])>
现在 B 的形状变成了【2,3】, B 可以直接加到 X @ W 上。考虑另一个 2×2 矩阵的例子。实现如下:
In [82]: x = tf.range(4)
x=tf.reshape(x,[2,2]) # Create 2x2 matrix
Out[82]:
<tf.Tensor: id=655, shape=(2, 2), dtype=int32, numpy=
array([[0, 1],
[2, 3]])>
首先,复制列维度中数据的一个副本,如下所示:
In [83]: x = tf.tile(x,multiples=[1,2])
Out[83]:
<tf.Tensor: id=658, shape=(2, 4), dtype=int32, numpy=
array([[0, 1, 0, 1],
[2, 3, 2, 3]])>
然后复制行维度中数据的一个副本:
In [84]: x = tf.tile(x,multiples=[2,1])
Out[84]:
<tf.Tensor: id=672, shape=(4, 4), dtype=int32, numpy=
array([[0, 1, 0, 1],
[2, 3, 2, 3],
[0, 1, 0, 1],
[2, 3, 2, 3]])>
在二维复制操作之后,我们可以看到数据的形状已经翻倍。这个例子帮助我们更直观地理解数据复制的过程。
需要注意的是,tf.tile 会创建一个新的张量来保存复制的张量。由于复制操作涉及大量的数据读写操作,计算成本相对较高。神经网络中不同形状之间的张量运算很常见,那么有没有轻量级的复制运算呢?这就是接下来要介绍的广播操作。
4.8 广播
广播是一种轻量级的张量复制方法,它在逻辑上扩展了张量数据的形状,但只在需要时执行实际的存储复制操作。对于大多数场景,广播机制可以通过避免实际的数据复制来完成逻辑运算,从而与 tf.tile 函数相比减少了大量的计算开销。
对于长度为 1 的所有维度,广播的效果与 tf.tile 相同,不同的是 tf.tile 通过执行 copy IO 操作创建了一个新的张量。广播不会立即复制数据;相反,它会在逻辑上改变张量的形状,使视图成为复制的形状。广播将使用深度学习框架的优化方法来避免数据的实际复制,并完成逻辑运算。对于用户来说,广播和 tf.tile 复制的最终效果是一样的,但是广播机制节省了大量的计算资源。建议在计算过程中尽量使用广播,提高效率。
继续考虑前面的例子Y=X@W+b, X @ W 的形状为【2,3】, b 的形状为【3】。我们可以通过组合 tf.expand_dims 和 tf.tile 来手动完成复制数据的操作,即把 b 变换成 shape【2,3】然后加到 X @ W 。但其实用 shape [3]直接把 X @ W 加到 b 上也是正确的,比如:
x = tf.random.normal([2,4])
w = tf.random.normal([4,3])
b = tf.random.normal([3])
y = x@w+b # Add tensors with different shapes directly
前面的加法运算不会引发逻辑错误。这是因为它自动调用广播函数 tf.broadcast_to(x,new_shape),将 b 的形状展开为【2,3】。前面的操作等效于
y = x@w + tf.broadcast_to(b,[2,3])
换句话说,当运算符+遇到两个形状不一致的张量时,它会自动考虑将这两个张量展开成一致的形状,然后调用 tf.add 来完成张量加法运算。通过自动调用 tf.broadcast_to(b,[2,3]),既达到了增加维度的目的又避免了实际复制数据的昂贵计算成本。
广播机制的核心思想是普遍性。也就是说,相同的数据通常适用于其他位置。在验证普适性之前,我们需要先将张量形状向右对齐,然后进行普适性检查:对于长度为 1 的维度,默认情况下这个数据一般适用于当前维度中的其他位置;对于不存在的维度,增加一个新维度后,默认的当前数据也普遍适用于新维度,从而可以展开成任意维数的张量形状。
考虑到张量 A 具有形状[ w ,1],需要扩展到形状[ b , h , w , c ]。如图 4-7 所示,第一行为展开形状,第二行为现有形状。
图 4-7
广播示例 1
首先,将两个形状向右对齐。对于通道维度 c ,张量的当前长度为 1。默认情况下,该数据也适用于当前维度中的其他位置。数据被逻辑复制,长度变成 c;对于不存在的维度 b 和 h ,自动插入一个新的维度,新维度的长度为 1,同时当前数据一般适用于新维度中的其他位置,即对于其他图片和其他行,与当前行的数据完全一致。这将自动扩展相应的尺寸到 b 和 h ,如图 4-8 所示。
图 4-8
广播示例 2
tf.broadcast_to(x,new_shape)函数可用于显式执行自动扩展功能,将现有形状扩展为 new_shape。实现如下:
In [87]:
A = tf.random.normal([32,1]) # Create a matrix
tf.broadcast_to(A, [2,32,32,3]) # Expand to 4 dimensions
Out[87]:
<tf.Tensor: id=13, shape=(2, 32, 32, 3), dtype=float32, numpy=
array([[[[-1.7571245 , -1.7571245 , -1.7571245 ],
[ 1.580159 , 1.580159 , 1.580159 ],
[-1.5324328 , -1.5324328 , -1.5324328 ],...
可以看出,在普遍性原则的指导下,广播机制变得直观易懂。
让我们考虑一个不满足普遍性原则的例子,如图 4-9 所示。
图 4-9
传播坏榜样
在 c 维中,张量已经有两个特征,新形状对应维的长度为 c ( c ≠ 2,如 c = 3)。那么当前维度中的这两个特征就不能普遍适用于其他位置,所以不符合普遍性原则。如果我们应用广播,它将触发错误,例如
In [88]:
A = tf.random.normal([32,2])
tf.broadcast_to(A, [2,32,32,4])
Out[88]:
InvalidArgumentError: Incompatible shapes: [32,2] vs. [2,32,32,4] [Op:BroadcastTo]
在进行张量运算时,有些运算会在处理不同形状的张量时自动调用广播机制,比如+、-、*、和/,将对应的张量广播成一个共同的形状,然后做相应的计算。图 4-10 展示了三种不同形状的张量加法的一些例子。
图 4-10
自动广播示例
我们来测试一下基础运营商的自动播报机制,比如:
a = tf.random.normal([2,32,32,1])
b = tf.random.normal([32,32])
a+b,a-b,a*b,a/b # Test automatic broadcasting for operations +, -, *, and /
这些操作可以在实际计算之前广播成一个公共形状。使用广播机制可以使代码更加简洁高效。
4.9 数学运算
在前几章中,我们已经使用了一些基本的数学运算,如加、减、乘、除。本节将系统介绍 TensorFlow 中常见的数学运算。
4.9.1 加减乘除
加减乘除是最基本的数学运算。它们分别由 TensorFlow 中的 tf.add、tf.subtract、tf.multiply 和 tf.divide 函数实现。TensorFlow 具有重载运算符+、-、和/。一般建议直接使用那些运算符。底数除法和余数除法是另外两种常见的运算,分别由//和%运算符实现。让我们演示一下除法运算,例如:
In [89]:
a = tf.range(5)
b = tf.constant(2)
a//b # Floor dividing
Out[89]:
<tf.Tensor: id=115, shape=(5,), dtype=int32, numpy=array([0, 0, 1, 1, 2])>
In [90]: a%b # Remainder dividing
Out[90]:
<tf.Tensor: id=117, shape=(5,), dtype=int32, numpy=array([0, 1, 0, 1, 0])>
电源操作
通过 tf.pow(x,a)函数,或者运算符** as x**a,可以方便地完成幂运算:
In [91]:
x = tf.range(4)
tf.pow(x,3)
Out[91]:
<tf.Tensor: id=124, shape=(4,), dtype=int32, numpy=array([ 0, 1, 8, 27])>
In [92]: x**2
Out[92]:
<tf.Tensor: id=127, shape=(4,), dtype=int32, numpy=array([0, 1, 4, 9])>
将指数设置为)的形式来实现根运算
),例如:
In [93]: x=tf.constant([1.,4.,9.])
x**(0.5) # square root
Out[93]:
<tf.Tensor: id=139, shape=(3,), dtype=float32, numpy=array([1., 2., 3.], dtype=float32)>
特别是对于常见的平方和平方根运算,可以使用 tf.square(x)和 tf.sqrt(x)。平方运算的实现如下:
In [94]:x = tf.range(5)
x = tf.cast(x, dtype=tf.float32) # convert to float type
x = tf.square(x)
Out[94]:
<tf.Tensor: id=159, shape=(5,), dtype=float32, numpy=array([ 0., 1., 4., 9., 16.], dtype=float32)>
平方根运算实现如下:
In [95]:tf.sqrt(x)
Out[95]:
<tf.Tensor: id=161, shape=(5,), dtype=float32, numpy=array([0., 1., 2., 3., 4.], dtype=float32)>
指数和对数运算
使用 tf.pow(a,x)或**运算符也可以轻松实现指数运算,例如:
In [96]: x = tf.constant([1.,2.,3.])
2**x
Out[96]:
<tf.Tensor: id=179, shape=(3,), dtype=float32, numpy=array([2., 4., 8.], dtype=float32)>
具体来说,对于自然指数 e x ,这可以用 tf.exp(x)来实现,例如:
In [97]: tf.exp(1.)
Out[97]:
<tf.Tensor: id=182, shape=(), dtype=float32, numpy=2.7182817>
在 TensorFlow 中,自然对数 x 可以用 tf.math.log(x)实现,例如:
In [98]: x=tf.exp(3.)
tf.math.log(x)
Out[98]:
<tf.Tensor: id=186, shape=(), dtype=float32, numpy=3.0>
如果要计算其他底数的对数,可以使用对数换底公式:
)
例如,)的计算可以通过下式实现
In [99]: x = tf.constant([1.,2.])
x = 10**x
tf.math.log(x)/tf.math.log(10.)
Out[99]:
<tf.Tensor: id=6, shape=(2,), dtype=float32, numpy=array([1., 2.], dtype=float32)>
矩阵乘法
神经网络包含大量矩阵乘法运算。我们之前介绍过,矩阵乘法可以通过@运算符和 tf.matmul(a,b)函数轻松实现。需要注意的是,TensorFlow 中的矩阵乘法可以使用批处理方式,即张量 A 和 B 可以有大于 2 的维数。当维度大于 2 时,TensorFlow 选择 A 和 B 的最后两个维度进行矩阵乘法,前面的维度全部视为批量维度。
根据矩阵乘法的定义, A 能乘一个矩阵 B 的条件是 A 的倒数第二个维度(列)的长度和 B 的倒数第二个维度(行)的长度必须相等。例如,形状为[4,3,28,32]的张量 a 可以乘以形状为[4,3,32,2]的张量 b。代码如下:
In [100]:
a = tf.random.normal([4,3,28,32])
b = tf.random.normal([4,3,32,2])
a@b
Out[100]:
<tf.Tensor: id=236, shape=(4, 3, 28, 2), dtype=float32, numpy=
array([[[[-1.66706240e+00, -8.32602978e+00],
[ 9.83304405e+00, 8.15909767e+00],
[ 6.31014729e+00, 9.26124632e-01],...
矩阵乘法也支持自动广播机制,例如:
In [101]:
a = tf.random.normal([4,28,32])
b = tf.random.normal([32,16])
tf.matmul(a,b) # First broadcast b to shape [4, 32, 16] and then multiply a
Out[101]:
<tf.Tensor: id=264, shape=(4, 28, 16), dtype=float32, numpy=
array([[[-1.11323869e+00, -9.48194981e+00, 6.48123884e+00, ...,
6.53280640e+00, -3.10894990e+00, 1.53050375e+00],
[ 4.35898495e+00, -1.03704405e+01, 8.90656471e+00, ...,
前面的操作会自动将变量 b 展开为一个常见的形状[4,32,16],然后以批处理形式将变量 a 相乘,以获得形状为[4,28,16]的结果。
4.10 动手向前传播
到目前为止,我们已经介绍了张量创建、索引切片、维度转换和常见的数学运算。最后,我们将使用我们所学的知识来完成三层神经网络的实现:
out=ReLU{ReLU{ReLUX@W1+b1]@W2+b2} @W
我们使用的数据集是 MNIST 手写数字图片数据集。输入节点的数量是 784。第一、第二和第三层的输出节点数分别是 256、128 和 10。首先,让我们为每个非线性层创建张量参数 W 和 b 如下:
# Every layer's tensor needs to be optimized. Set initial bias to be 0s.
# w and b for first layer
w1 = tf.Variable(tf.random.truncated_normal([784, 256], stddev=0.1))
b1 = tf.Variable(tf.zeros([256]))
# w and b for second layer
w2 = tf.Variable(tf.random.truncated_normal([256, 128], stddev=0.1))
b2 = tf.Variable(tf.zeros([128]))
# w and b for third layer
w3 = tf.Variable(tf.random.truncated_normal([128, 10], stddev=0.1))
b3 = tf.Variable(tf.zeros([10]))
在正演计算中,首先将形状为[ b ,28,28]的输入张量的视图调整为形状为[ b ,784]的矩阵,使其适合网络的输入格式:
# Change view[b, 28, 28] => [b, 28*28]
x = tf.reshape(x, [-1, 28*28])
接下来,完成第一层的计算。我们在这里执行自动扩展操作:
# First layer calculation, [b, 784]@[784, 256] + [256] => [b, 256] + [256] => [b, 256] + [b, 256]
h1 = x@w1 + tf.broadcast_to(b1, [x.shape[0], 256])
h1 = tf.nn.relu(h1) # apply activation function
对第二和第三非线性功能层使用相同的方法。输出层可以使用 ReLU 激活函数:
# Second layer calculation, [b, 256] => [b, 128]
h2 = h1@w2 + b2
h2 = tf.nn.relu(h2)
# Output layer calculation, [b, 128] => [b, 10]
out = h2@w3 + b3
将实标记张量转换为一位热编码,并从 out 计算均方误差,如下所示:
# Calculate mean square error, mse = mean(sum(y-out)²)
# [b, 10]
loss = tf.square(y_onehot - out)
# Error metrics, mean: scalar
loss = tf.reduce_mean(loss)
前面的正向计算过程需要在“with tf”的上下文中进行包装。GradientTape() as tape”,以便可以在自动微分操作的正向计算期间保存计算图形信息。
使用 tape.gradient()函数获取网络参数的梯度信息。结果存储在梯度列表变量中,如下所示:
# Calculate gradients for [w1, b1, w2, b2, w3, b3]
grads = tape.gradient(loss, [w1, b1, w2, b2, w3, b3])
然后我们需要通过
)
来更新参数
# Update parameters using assign_sub (subtract the update and assign back to the original parameter)
w1.assign_sub(lr * grads[0])
b1.assign_sub(lr * grads[1])
w2.assign_sub(lr * grads[2])
b2.assign_sub(lr * grads[3])
w3.assign_sub(lr * grads[4])
b3.assign_sub(lr * grads[5])
其中,assign_sub()从给定的参数值中减去自身,实现就地更新操作。网络训练误差的变化如图 4-11 所示。
图 4-11
正向计算的训练误差*
更多推荐
所有评论(0)