深度学习高手笔记 卷1:基础算法

978-7-115-59631-4
作者: 刘岩(@大师兄)
译者:
编辑: 孙喆思

图书目录:

详情

本书通过扎实、详细的内容和清晰的结构,从算法理论、算法源码、实验结果等方面对深度学习算法进行分析和介绍。本书共三篇,第一篇主要介绍深度学习在计算机视觉方向的一些卷积神经网络,从基础骨干网络、轻量级 CNN、模型架构搜索 3 个方向展开,介绍计算机视觉方向的里程碑算法;第二篇主要介绍深度学习在自然语言处理方向的重要突破,包括基础序列模型和模型预训练;第三篇主要介绍深度学习在模型优化上的进展,包括模型优化方法。 通过阅读本书,读者可以深入理解主流的深度学习基础算法,搭建起自己的知识体系,领会算法的本质,学习模型优化方法。无论是从事深度学习科研的教师及学生,还是从事算法落地实践的工作人员,都能从本书中获益。

图书摘要

版权信息

书名:深度学习高手笔记——卷1:基础算法

ISBN:978-7-115-59631-4

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

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

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

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


版  权

著    刘岩(@大师兄)

责任编辑 孙喆思

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

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

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

读者服务热线:(010)81055410

反盗版热线:(010)81055315

读者服务:

微信扫码关注【异步社区】微信公众号,回复“e59609”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。

内 容 提 要

本书通过扎实、详细的内容和清晰的结构,从算法理论、算法源码、实验结果等方面对深度学习算法进行分析和介绍。本书共三篇,第一篇主要介绍深度学习在计算机视觉方向的一些卷积神经网络,从基础骨干网络、轻量级CNN、模型架构搜索3个方向展开,介绍计算机视觉方向的里程碑算法;第二篇主要介绍深度学习在自然语言处理方向的重要突破,包括基础序列模型和模型预训练;第三篇主要介绍深度学习在模型优化上的进展,包括模型优化方法。

通过阅读本书,读者可以深入理解主流的深度学习基础算法,搭建起自己的知识体系,领会算法的本质,学习模型优化方法。无论是从事深度学习科研的教师及学生,还是从事算法落地实践的工作人员,都能从本书中获益。

序1

假如问近10年来计算机技术领域最热门的方向是什么,人工智能一定是候选之一。从1969年马文•明斯基成为第一位人工智能方向的图灵奖获得者,到2018年3位学者因在深度学习方向的贡献共同获得图灵奖,人工智能方向已经七获图灵奖。近年来,在人工智能上升为国家战略,并被广泛使用的大背景下,众多高校开设了诸如大数据、深度学习、数据挖掘等人工智能学科;诸多企业也开始使用人工智能赋能企业运营,为企业提供智能化支撑,助力企业实现降本增效,人工智能技术能力俨然成为衡量一个企业的核心实力的重要指标之一。

我们正处在一个信息化和智能化交互的时代,人工智能、物联网、区块链、元宇宙等技术创新,既是技术发展的阶段性成果,也是开启智能化时代的重要助推器。更重要的是,它们正在相互促进,共同发展。人工智能的发展经历了机器定理、专家系统的两次热潮和低谷。如今,我们正处在以深度学习为代表的第三次人工智能热潮中,并且人工智能正深刻改变着我们的生活。创新工场CEO李开复先生曾提出过著名的“五秒钟原则”:一项本来由人从事的工作,如果人可以在5秒以内对工作中需要思考和决策的问题作出相应决定,那么这项工作就有非常大的可能被人工智能技术全部或部分取代。人工智能为经济生活带来了颠覆性改变,这可能会造成部分岗位的消失,但它更多的是引发了工作性质的变革,所以能否掌握这门技术,在第三次人工智能浪潮中占得先机,决定了一个企业和个人的实力与前景。在人工智能的步步紧逼下,你究竟是在焦虑还是已经看到了其中潜在的机遇,并积极地接受变革呢?

如果你已经准备好迎接到来的第三次人工智能浪潮,那么本书是你不能错过的一本读物。本书全面且系统地梳理了近10年来的深度学习算法,并集结成册。本书结构清晰,内容丰富,包含了作者对深度学习深刻且独到的见解。在本书中,作者将深度学习的几十篇具有里程碑意义的论文整理成卷积神经网络、自然语言处理和模型优化3个主要方向,又对每个方向的重要算法做了深入浅出的讲解和分析。对比业内同类书籍,本书将深度学习算法的讲解提升到了一个新的高度,是你深入了解深度学习的不二之选。总之,本书极具价值,值得每一位深度学习方向的从业者、研究者和在校学生阅读和学习。

颜伟鹏

京东集团副总裁、京东零售技术委员会主席

序2

在古希腊时期,人类就梦想着创造能自主思考的机器。如今,人工智能已经成为一个活跃的研究课题和一门在诸多场景落地的技术。在人工智能发展的早期阶段,它更擅长解决可具象化为数学规则的问题,而人工智能的真正挑战在于解决那些对人来说很容易执行但非常难以描述为具体规则的问题,这就是深度学习的诞生动机。深度学习是人工智能的一个重要分支,它以大数据为基础,以数理统计为理论框架,涵盖了计算机视觉、自然语言处理、语音识别、图深度学习、强化学习等不同方向。于2012年提出的AlexNet开启了深度学习蓬勃发展的10年。2022年的1月3日,著名论文预印本平台arXiv的论文数量突破了200万篇,其中不乏残差网络、Transformer、GAN等引用量达到数万乃至数十万的经典算法论文。深度学习崛起近10年,我们有必要对深度学习近10年的发展做一些梳理和总结。

深度学习的发展日新月异,从使用基础算法的人脸识别、机器翻译、语音识别、AlphaGo等,再到综合各类算法的智能客服、推荐搜索、虚拟现实等,这些基于深度学习的技术和产品正在以惊人的速度改变着我们的工作与生活。除此之外,深度学习在智联网、无人驾驶、智能医疗等诸多领域的发展中也起到了中流砥柱的作用。即使你是一个和深度学习无关的其他行业从业者,你一定也在不知不觉中被深度学习影响着,而且你也可以借助简单、易用的深度学习框架快速使用这一前沿技术。

本书有别于以卷积神经网络、循环神经网络等基础概念为核心的同类书籍,主要以近10年来深度学习方向诞生的经典算法为基础,重点讨论深度学习在卷积神经网络、自然语言处理、模型归一化等方向上的发展历程以及各个算法的优缺点,介绍各个算法是如何分析先前算法的若干问题并提出解决方案的。本书包含作者对深度学习的独特见解和全新思考,知识丰富、架构清晰、重点突出、可读性好。此外,作者借助代码、图示、公式等手段,对晦涩难懂的算法进行深入浅出的剖析。相信每位读者都能够从本书中汲取相应的知识并得到启发。

包勇军

京东集团副总裁,京东零售技术委员会数据算法通道会长

前  言

目前人工智能(artificial intelligence,AI)在计算机界非常火热,而其中深度学习(deep learning,DL)无疑是更为火热的一个领域,它在计算机视觉、自然语言处理、语音识别、跨模态分析、风控建模等领域均取得了突破性的进展。而且近年来该领域的优秀论文、落地项目也层出不穷。密切关注深度学习领域的进展是每个深度学习工作者必不可少的工作内容之一,不仅为了找工作、升职加薪,还为了更好地跟随前沿科技,汲取算法奥妙。

2014年是深度学习蓬勃发展的一年,这一年计算机视觉方向诞生的算法有VGG、GoogLeNet、R-CNN、DeepLab,自然语言处理方向诞生的有注意力机制、神经图灵机、编码器-解码器架构。也就是在这一年,我开始了自己的研究生生涯,由此与人工智能和深度学习结下了不解之缘。度过了3年的求学生涯和4年的工作生涯,时间很快来到了2022年,我也有了8年的人工智能相关的科研与工作经历。在这8年的科研及工作中,我既见证了SVM、决策树、ELM等传统机器学习方法的没落,也了解了深度学习在各个方向的突破性进展。我既发表过使用传统机器学习方法解决神经机器翻译或者细胞检测问题的论文,也使用深度学习技术在OCR、公式识别、人像抠图、文本分类等方向实现了业务落地。在这8年的时间里,我读了很多论文和源码,也做了很多项目和实验。

在机缘巧合下,我听从朋友的建议将几篇学习笔记上传到了知乎,没想到得到了大量的收藏和关注,因此开通了本书同名专栏。截稿时,我在知乎上已更新了一百多篇文章,也有了几百万的阅读量和过万的粉丝数。为了能帮助更多的读者,我将知乎专栏下的文章经过整理、修改、精校、勘误之后完成了本套图书。

本套图书共两卷,分别是卷1基础算法和卷2经典应用。卷1由三篇组成,第一篇介绍深度学习在计算机视觉方向的一些卷积神经网络,从基础骨干网络(第1章)、轻量级CNN(第2章)、模型架构搜索(第3章)3个方向展开,介绍计算机视觉方向的30余个里程碑算法。第二篇主要介绍深度学习在自然语言处理方向的重要突破,主要介绍几个基础序列模型,如LSTM、注意力机制、Transformer等(第4章),以及近年来以BERT为代表的10余个预训练语言模型(第5章)。第三篇(第6章)将介绍模型优化的经典策略,分为两个方向,一个方向是Dropout及其衍生算法,另一个方向是以批归一化、层归一化为代表的归一化算法。

卷2会对专栏中的经典或者前沿应用进行总结,同样由三篇组成。第一篇介绍的应用是目标检测与分割,其中会介绍双阶段的R-CNN系列、单阶段的YOLO系列,以及Anchor-Free的CornerNet系列这3个方向的目标检测算法,也会介绍目标检测在特征融合和损失函数方向的迭代优化,最后会介绍与目标检测非常类似的分割算法。第二篇介绍深度学习中的OCR系列算法,用于场景文字检测、文字识别两个方向。第三篇会介绍其他深度学习经典或者前沿的应用,例如生成模型、图神经网络、二维信息识别、图像描述、人像抠图等。

阅读本书时有以下两点注意事项:本书的内容以经典和前沿的深度学习算法为主,并没有过多地介绍深度学习的基础知识,如果你在阅读本书时发现一些概念晦涩难懂,请移步其他基础类图书查阅相关知识点;本书源于一系列算法或者论文的读书笔记,不同章节的知识点存在相互依赖的关系,因此知识点并不是顺序展开的。为了帮助读者提前感知先验知识,本书会在每一节的开始给出相关算法依赖的重要章节,并在配套资源中给出两卷书整体的知识拓扑图。

我对本书有以下3个阅读建议。

如果你的深度学习基础较为薄弱,那么可以结合本书提供的知识拓扑图和章节先验知识,优先阅读拓扑图中无先验知识的章节,读懂该章节后便可以将这个章节在拓扑图中划掉,然后逐步将拓扑图清空。

如果你有一定的深度学习基础,对一些经典的算法(如VGG、残差网络、LSTM、Transformer、Dropout、BN等)都比较熟悉,那么你可以按顺序阅读本书,并在遇到陌生的概念时根据每一节提供的先验知识去阅读相关章节。

如果你只想了解某些特定的算法,你可以直接跳到相关章节,因为本书章节的内容都比较独立,而且会对重要的先验知识进行复盘,所以单独地阅读任何特定章节也不会有任何障碍。

本书是我编写的第一本图书,这是一个开始,但远不是一个结束。首先,由于个人的精力和能力有限,图书覆盖的知识点难免有所欠缺,甚至可能因为我的理解偏差导致编写错误,在此欢迎各位读者前去知乎专栏对应的文章下积极地指正,我也将在后续的版本中对本书进行修正和维护。随着深度学习的发展,无疑会有更多的算法被提出,甚至会有其他经典的算法被再次使用,我会在个人的知乎专栏继续对这些算法进行总结和分析。

本书的完成离不开我在求学、工作和生活中遇到的诸多“贵人”。首先感谢我在求学的时候遇到的诸位导师,他们带领我打开了人工智能的“大门”。其次感谢我在工作中遇到的诸位领导和同事,他们对我的工作给予了巨大的帮助和支持。最后感谢我的亲人和朋友,没有他们的支持和鼓励,本书是不可能完成的。

刘岩(@大师兄)

2022年5月28日

资源与支持

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

配套资源

本书提供源代码、知识拓扑图等免费配套资源。

要获得相关配套资源,请在异步社区本书页面中单击,跳转到下载界面,按提示进行操作即可。

您还可以扫码右侧二维码, 关注【异步社区】微信公众号,回复“e59631”直接获取,同时可以获得异步社区15天VIP会员卡,近千本电子书免费畅读。

提交勘误

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

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

扫码关注本书

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

与我们联系

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

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

如果您有兴趣出版图书、录制教学视频,或者参与图书技术审校等工作,可以发邮件给本书的责任编辑(sunzhesi@ptpress.com.cn)。

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

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

关于异步社区和异步图书

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

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

异步社区

微信服务号

第一篇 卷积神经网络

“虽然没有人这样说,但我认为人工智能几乎是一门人文学科,是一种试图理解人类智力和人类认知的尝试。”

——Sebastian Thrun

第1章 基础骨干网络

物体分类是计算机视觉(computer vision,CV)中最经典的、也是目前研究得最为透彻的一个领域,该领域的开创者也是深度学习领域的“名人”级别的人物,例如Geoffrey Hinton、Yoshua Bengio等。物体分类常用的数据集有手写数字识别数据集MNIST、物体识别数据集CIFAR-10(10类)和类别更多的CIFAR-100(100类),以及超大数据集ImageNet。ImageNet是由李飞飞教授主导的ILSVRC(ImageNet Large Scale Visual Recognition Challenge)中使用的数据集,每年的ILSVRC(此处指ILSVRC的物体分类任务)中产生的网络也指引了分类网络的发展方向。

2012年,第三届ILSVRC的冠军作品Hinton团队的AlexNet,将2011年的top-5错误率从25.8%降低到16.4%。他们的最大贡献在于验证了卷积操作在大数据集上的有效性,从此物体分类进入了深度学习时代。

2013年,ILSVRC已被深度学习算法“霸榜”,冠军作品ZFNet使用了更深的深度,并且其论文给出了卷积神经网络(CNN)的有效性的初步解释。

2014年是深度学习领域分类算法“井喷式”发展的一年,在物体检测方向也是如此。这一届ILSVRC物体分类任务的冠军作品是Google团队提出的GoogLeNet(top-5错误率:7.3%),亚军作品则是牛津大学的VGG(top-5错误率:8.0%),但是在物体检测任务中VGG击败了GoogLeNet。VGG利用的搭建CNN的思想现在来看依旧具有指导性,例如按照降采样的分布对网络进行分块,使用小卷积核,每次降采样之后特征图(feature map)的数量加倍,等等。另外VGG使用了当初贾扬清提出的Caffe作为深度学习框架并开源了其模型,凭借比GoogLeNet更快的特性,VGG很快占有了大量的市场,尤其是在物体检测领域。VGG也凭借增加深度来提升精度的思想将CNN推上了“最高峰”。GoogLeNet则从特征多样性的角度研究了CNN结构,GoogLeNet的特征多样性是基于一种并行的、使用了多个不同尺寸的卷积核的Inception单元来实现的。GoogLeNet的最大贡献在于指出CNN精度的增加不仅仅可以依靠深度实现,增加网络的复杂性也是一种有效的策略。

2015年的ILSVRC的冠军作品是何恺明等人提出的残差网络(top-5错误率:3.57%)。他们指出CNN的精度并不会随着深度的增加而增加,导致此问题的原因是网络的退化问题。残差网络的核心思想是通过向网络中添加直接映射(跳跃连接)的方式解决退化问题,进而使构建更深的CNN成为可能。残差网络的简单易用的特征使其成为目前使用最为广泛的网络结构之一。

2016年ILSVRC的前几名作品都是通过模型集成实现的,CNN的结构创新陷入了短暂的停滞。当年的冠军作品是商汤公司和香港中文大学联合推出的CUImage,它是6个模型的集成,并无创新性,此处不赘述。2017年是ILSVRC的最后一届,这一届的冠军是Momenta团队,他们提出了基于注意力机制的SENet(top-5错误率:2.21%),其通过自注意力(self-attention)机制为每个特征图计算出一个权重。另外一个非常重要的网络是黄高团队于CVPR 2017提出的DenseNet,本质上是各个单元相互连接的密集连接结构。

除了ILSVRC中各个冠军作品,在提升网络精度方面还有一些值得我们学习的算法,例如Inception的几个变种、结合了DenseNet和残差网络的DPN。

由于Transformer在自然语言处理(natural language processing,NLP)任务上取得的突破性进展,将Transformer应用到分类网络成为近年来非常火热的研究方向,比较有代表性的包括iGPT、ViT、Swin Transformer,以及混合使用CNN和Transformer的CSWin Transformer。

1.1 起源:LeNet-5和AlexNet

在本节中,先验知识包括:

BN(6.2节);

Dropout(6.1节)。

1.1.1 从LeNet-5开始

使用CNN解决图像分类问题可以往前追溯到1998年LeCun发表的论文[1],其中提出了用于解决手写数字识别问题的LeNet。LeNet又名LeNet-5,是因为在LeNet中使用的均是的卷积核。LeNet-5的网络结构如图1.1所示。

[1] 参见Yann LeCun、Léon Bottou、Yoshua Bengio等人的论文“Gradient-based learning applied to document recognition”。

图1.1 LeNet-5的网络结构

LeNet-5中使用的结构直接影响了其后的几乎所有CNN,卷积层 + 降采样层 + 全连接层至今仍然是最主流的结构。卷积操作使网络可以响应和卷积核形状类似的特征,而降采样操作则使网络拥有了一定程度的不变性。下面我们简单分析一下LeNet-5的网络结构。

输入:的手写数字(数据集中共10类)的黑白图片。

C1:C1层使用了6个卷积核,每个卷积核的大小均是,pad = 0,stride = 1(有效卷积,与有效卷积对应的是same卷积),激活函数使用的是tanh(双曲正切),表达式为式(1.1),tanh激活函数的值域是。所以在第一次卷积之后,特征图的大小变为,该层共有个神经元。加上偏置,该层共有个参数。

  (1.1)

S2:S2层是CNN常使用的降采样层。在LeNet-5中,降采样的过程是将窗口内的3个输入相加,乘一个可训练参数再加上一个偏置。LeNet-5 的这种带参数的降采样方式已经被淘汰,目前主流使用最大池化或平均池化。经过S2层,特征图的大小缩小,变成。该层共有个神经元,参数数量是

C3:C3层跟S2层并不是密集连接的,具体连接方式是,C3层的前6个特征图以S2层中3个相邻的特征图子集为输入,接下来6个特征图以S2层中4个相邻特征图子集为输入,然后的3个特征图以不相邻的4个特征图子集为输入,最后一个特征图以S2层中所有特征图为输入,如图1.2所示。这两个层采用的稀疏连接的方式已被抛弃,目前普遍使用的是密集连接,或轻量级网络中使用的深度可分离卷积、分组卷积。

图1.2 LeNet-5中C3层和S2层的连接方式

C3层包括16个大小为、通道数为6的same卷积,pad = 0,stride = 1,激活函数同样为tanh。一次卷积后,特征图的大小是,神经元数量为,可训练参数数量为

S4:与S2层的计算方法类似,该层使特征图的大小变成,共有个神经元,可训练参数数量是

C5:节点数为120的全连接层,激活函数是tanh,参数数量是

F6:节点数为84的全连接层,激活函数是tanh,参数数量是

输出:10个分类的输出层,使用的是softmax激活函数,如式(1.2)所示,参数数量是。softmax用于分类有如下优点:

ex使所有样本的值均大于0,且指数的性质使样本的区分度尽量高;

softmax所有可能值的和为1,反映出分类为该类别的概率,输出概率最高的类别即可。

  (1.2)

使用Keras搭建LeNet-5网络的核心代码如下,其是基于LeNet-5网络,在MNIST手写数字识别数据集上的实现。完整的LeNet-5在MNIST上的训练过程见随书资料。

注意,这里使用的都是密集连接,没有复现C3层和S2层之间的稀疏连接。

# 构建LeNet-5网络
model = Sequential()
model.add(Conv2D(input_shape = (28,28,1), filters=6, kernel_size=(5,5), 
          padding='valid', activation='tanh'))
model.add(MaxPool2D(pool_size=(2,2), strides=2))
model.add(Conv2D(input_shape=(14,14,6), filters=16, kernel_size=(5,5), 
          padding='valid', activation='tanh'))
model.add(MaxPool2D(pool_size=(2,2), strides=2))
model.add(Flatten())
model.add(Dense(120, activation='tanh'))
model.add(Dense(84, activation='tanh'))
model.add(Dense(10, activation='softmax'))

如图1.3所示,经过10个epoch后,LeNet-5基本收敛。

图1.3 LeNet-5在MNIST数据集上的收敛情况

1.1.2 觉醒:AlexNet

LeNet-5之后,CNN沉寂了约14年。直到2012年,AlexNet在ILSVRC中一举夺魁,直接把在ImageNet数据集上的精度提升了约10个百分点,它将CNN的深度和宽度都提升到了传统算法无法企及的新高度。从此,深度学习开始在CV的各个领域“披荆斩棘”,至今深度学习仍是人工智能最热门的话题。AlexNet作为教科书式的网络,值得每个学习深度学习的人深入研究。

AlexNet的名字取自该模型的第一作者Alex Krizhevsky。AlexNet在ImageNet中的120万张图片的1 000类分类任务上的top-1错误率是37.5%,top-5错误率则是15.3%(直接比第二名的26.2%低了约10个百分点)。AlexNet如此成功的原因是其使网络的宽度和深度达到了前所未有的高度,而该模型也使网络的可学习参数达到了58 322 314个。为了学习这些参数,AlexNet并行使用了两块GTX 580,大幅提升了训练速度。

笔记 AlexNet当初使用分组卷积是因为硬件资源有限,不得不将模型分到两块GPU上运行。相关研究者并没有给出分组卷积的概念,而且没有对分组卷积的性能进行深入探讨。ResNeXt的相关研究者则明确给出了分组卷积的定义,并证明和验证了分组卷积有接近普通卷积的精度。

当想要使用机器学习解决非常复杂的问题时,我们必须使用容量足够大的模型。在深度学习中,增加网络的宽度和深度会提升网络的容量,但是提升容量的同时也会带来两个问题:

计算资源的消耗;

模型容易过拟合。

计算资源是当时限制深度学习发展的瓶颈,2011年Ciresan等人提出了使用GPU部署CNN的技术框架[2],由此深度学习得到了可以解决其计算瓶颈问题的硬件支持。

[2] 参见Dan C. Ciresan、Ueli Meier、Jonathan Masci等人的论文“Flexible, High Performance Convolutional Neural Networks for Image Classification”。

下面来详细分析一下AlexNet。AlexNet的网络结构如图1.4所示。

图1.4 AlexNet的网络结构

AlexNet基于Keras的实现代码如下。

# 构建AlexNet网络
model = Sequential()
model.add(Conv2D(input_shape = (227,227,3), strides = 4, filters=96, kernel_size=(11,11),
          padding='valid', activation='relu'))
model.add(BatchNormalization())
model.add(MaxPool2D(pool_size=(3,3), strides=2))
model.add(Conv2D(filters=256, kernel_size=(5,5), padding='same', activation='relu'))
model.add(BatchNormalization())
model.add(MaxPool2D(pool_size=(3,3), strides=2))
model.add(Conv2D(filters=384, kernel_size=(3,3), padding='same', activation='relu'))
model.add(BatchNormalization())
model.add(Conv2D(filters=384, kernel_size=(3,3), padding='same', activation='relu'))
model.add(BatchNormalization())
model.add(Conv2D(filters=256, kernel_size=(3,3), padding='same', activation='relu'))
model.add(BatchNormalization())
model.add(MaxPool2D(pool_size=(2,2), strides=2))
model.add(Flatten())
model.add(Dense(4096, activation='tanh'))
model.add(Dropout(0.5))
model.add(Dense(4096, activation='tanh'))
model.add(Dropout(0.5))
model.add(Dense(10, activation='softmax'))
model.summary()

根据Keras提供的summary()函数,可以得到图1.5所示的AlexNet的参数数量的统计结果[3],计算方法参照LeNet-5,不赘述。

[3] 这里参数数量不同是因为代码没有将模型部署在两块显卡上。

图1.5 通过Keras的summary()函数得到的AlexNet参数数量

1.多GPU训练

首先对比图1.1和图1.4,我们发现AlexNet将网络分成了两个部分。由于当时显卡的显存大小有限,因此作者使用了两块GPU并行训练模型,例如第二个卷积(图1.4中通道数为128的卷积)只使用一个GPU自身显存中的特征图,而第三个卷积需要使用另外一个GPU显存中的特征图。不过得益于TensorFlow等开源框架对多机多卡的支持和显卡显存的提升,AlexNet部署在单块GPU上已毫无压力,所以这一部分就不赘述。

2.ReLU

在LeNet-5中,使用了tanh作为激活函数,tanh的函数曲线如图1.6所示。tanh是一个以原点为中心点、值域为(-1,1)的激活函数。在反向传播过程中,局部梯度会与整个损失函数关于该局部输出的梯度相乘。当tanh(x)中的x的绝对值比较大的时候,该局部的梯度会非常接近于0,在深度学习中,该现象叫作“饱和”。同样,另一个常用的sigmoid激活函数也存在饱和的现象。sigmoid的函数如式(1.3)所示,函数曲线如图1.7所示。

  (1.3)

图1.6 tanh的函数曲线

图1.7 sigmoid的函数曲线

饱和现象带来了一个深度学习中非常严重的问题,那便是梯度消失。梯度消失是由反向传播中链式法则的乘法特性导致的,反映在深度学习的训练过程中便是越接近损失函数的参数梯度越大,从而使得这一部分参数成为主要学习的参数,而远离损失函数的参数的梯度则非常接近0,导致几乎没有梯度传到这一部分参数,从而使得这一部分参数很难学习到。

为了解决这个问题,AlexNet引入了ReLU激活函数,如式(1.4)所示。

  (1.4)

ReLU的函数曲线如图1.8所示。

图1.8 ReLU的函数曲线

在ReLU中,无论x的取值有多大,f(x)的导数都是1,也就不存在导数小于1导致的梯度消失的现象了。图1.9所示的是我们在MNIST数据集上,根据LeNet-5使用tanh和ReLU两个激活函数得到的不同模型的收敛情况,旨在对比两个不同的激活函数的模型效果。

图1.9 LeNet-5使用不同激活函数的收敛情况

此外,由于ReLU将小于0的部分全部置0,因此ReLU的另外一个特点就是具有稀疏性,不仅可以优化网络的性能,还可以缓解过拟合现象。

虽然使用ReLU的节点不会有饱和问题,但是会“死掉”,即大部分甚至所有的值为负值,从而导致该层的梯度都为0。“死神经元”是由进入网络的负值引起的(例如在大规模的梯度更新之后可能出现),减小学习率能缓解该现象。

3.LRN

局部响应归一化(local response normalization,LRN)模拟的是动物神经中的横向抑制效应,是一个已经被淘汰的算法。在VGG[4]的相关论文中已经指出,LRN并没有什么效果。在现在的网络中,LRN已经被其他归一化方法所替代,例如在上面代码中使用的批归一化(batch normalization,BN)[5]。LRN是使用同一位置临近的特征图来归一化当前特征图的值的一种方法,其表达式如式(1.5)所示:

[4] 参见Karen Simonyan、Andrew Zisserman的论文“Very Deep Convolutional Networks for Large-Scale Image Recognition”。

[5] 参见Sergey Ioffe、Christian Szegedy的论文“Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift”。

  (1.5)

其中,N表示特征图的数量,a是输入特征图,b是输出特征图,(x, y)是特征图上的坐标ij 点在批次维度的索引,这些值均由验证集得出。

另外,AlexNet把LRN放在池化层之前,这在计算上是非常不经济的,一种更好的做法是把LRN放在池化层之后。

4.覆盖池化

当进行池化的时候,如果步长(stride)小于池化核的尺寸,相邻的池化核会相互覆盖,这种方式叫作覆盖池化(overlap pooling)。AlexNet的论文中指出这种方式可以缓解过拟合。

5.Dropout

在AlexNet的前两层,作者使用了Dropout[6]来缓解容量高的模型容易发生过拟合的现象。Dropout的使用方法是在训练过程中随机将一定比例的隐层节点置0。Dropout能够缓解过拟合的原因是每次训练都会采样一个不同的网络结构,但是这些架构是共享权值的。这种技术减轻了节点之间的耦合性,因为一个节点不能依赖网络的其他节点。因此,节点能够学习到更健壮的特征。只有这样,节点才能适应每次采样得到的不同的网络结构。注意在测试时,我们是不对节点进行丢弃的。

[6] 参见Nitish Srivastava、Geoffrey Hinton、Alex Krizhevsky等人的论文“Dropout: A Simple Way to Prevent Neural Networks from Overfitting”。

虽然Dropout会减慢收敛速度,但其在缓解过拟合方面的优异表现仍旧使其在当前的网络中得到广泛的使用。

图1.10所示的是LeNet-5中加入Dropout之后模型的训练损失曲线。从图1.10中我们可以看出,加入Dropout之后,训练速度放缓了一些。20个epoch之后,训练集的损失函数曲线仍高于没有Dropout的。加入Dropout之后,虽然损失值为0.073 5,远高于没有Dropout的0.015 5,但是测试集的准确率从0.982 6上升到0.984 1。具体的实验数据见随书代码。可见Dropout对于缓解过拟合还是非常有帮助的。

图1.10 有Dropout与没有Dropout对比

1.2 更深:VGG

在本节中,先验知识包括:

AlexNet(1.1节)。

2014年,随着AlexNet在ImageNet数据集上大放异彩,使用深度学习探寻针对ImageNet数据集的最优网络成为提升在该数据集上精度的优先级最高的做法。牛津大学视觉几何组(Visual Geometry Group)的这篇论文[7]便提出了对CNN的深度和其性能进行探索的网络,该网络被命名为VGG。

[7] 参见Karen Simonyan、Andrew Zisserman 的论文“Very Deep Convolutional Networks for Large-Scale Image Recognition”。

VGG的结构非常清晰:

按照的池化层,网络可以分成若干个块(block);

每个块包含若干个same卷积,块内的特征图数量固定不变;

特征图的通道数按块以2倍的速度逐渐递增,第四块和第五块内特征图的通道数都是512(即64、128、256、512、512)。

VGG非常容易应用到其他数据集。在VGG中,块数每增加1,特征图的尺寸缩小一半,这么做是为了保证每一块的参数数量不会剧烈变化。通过减少块的数目也可以将网络应用到如MNIST、CIFAR等图像尺寸更小的数据集。块内的卷积数量是可变的,因为卷积的数量并不会影响特征图的尺寸,我们可以根据任务的复杂度自行调整块内的卷积数量。

VGG的表现效果也非常好,在2014年的ILSVRC物体分类任务中排名第二(第一名是GoogLeNet[8]),在物体检测任务中排名第一。

[8] 参见Christian Szegedy、Wei Liu 、Yangqing Jia 等人的论文“Going Deeper with Convolutions”。

VGG的模型开源在其官方网站上,为其他任务提供了非常好的迁移学习的材料,这使得VGG占有了大量商业市场。关于不同框架的VGG开源模型,读者可自行在网上搜索。

1.2.1 VGG介绍

“VGG家族”等各个类型的实现见随书资料,关于VGG家族的参数的具体设置可以总结为图1.11,图中包含大量信息,接下来我们一一进行分析。

图1.11 VGG家族

1.家族的特征

我们来看看VGG家族的共同特征:

输入图像均是的RGB彩色图像;

均采用5层最大池化,使用的均是same卷积,表示最终均会产生大小为的特征图,这是一个比较合适的大小;

特征层之后是两个隐层节点数目为4096的全连接层,最后是一个1000类softmax分类器;

所有VGG模型均可以表示为

VGG在卷积核方向的最大改进是将卷积核全部换成更小的或者的卷积核,而性能最好的VGG-16和VGG-19由且仅由的卷积核构成,原因有如下3点。

根据感受野的计算式,其中stride为模型的步长,ksize为卷积核的大小。我们知道一层的卷积核和3层的卷积核具有相同的感受野,但是由于3层感受野具有更深的深度,因此可以构建出更具判别性的决策函数。

假设特征图的数量都是C,3层卷积核的参数数量是,1层卷积核的参数数量是,3层卷积核具有更少的参数。

由于神经元数量和层数的增多,训练速度会变得更慢。

图1.12反映了VGG家族的各个模型的性能。

图1.12 VGG家族的各个模型的性能对比

图1.13展示了把LeNet-5的单层卷积换成两层卷积在MNIST上的收敛表现。论文中的实验表明两层卷积的网络确实比单层卷积的网络表现好,但是训练速度慢了二分之一。

另外,作者在前两层的全连接处使用丢失率为0.5的Dropout,然而并没有在图1.11中反映出来。

2.VGG-A vs VGG-A-LRN

VGG-A-LRN比VGG-A多了一个AlexNet介绍的LRN层,但是实验数据表明加入了LRN的VGG-A-LRN的错误率反而更高了,而且LRN的加入会更加占用内存,增加训练时间。

图1.13 单层卷积的LeNet与两层 卷积的LeNet对比

3.VGG-A、VGG-B、VGG-D和VGG-E

对比VGG-A(11层)、VGG-B(13层)、VGG-D(16层)、VGG-E(19层)的错误率,我们发现随着网络深度的增加,分类的错误率逐渐降低,当然深度越深表示需要的训练时间越长。但是当模型(VGG-D和VGG-E)到达一定深度时,网络的错误率趋于收敛,甚至偶尔会发生深层网络的错误率高于浅层网络的情况,这就是后面我们要介绍的退化问题。同时考虑网络的训练时间,我们需要折中考虑选择合适的网络深度。我相信作者一定探索了比VGG-E更深的网络,但是由于表现不理想并没有将其列在论文中。后面介绍的残差网络则通过残差机制将网络的深度从理论上扩展到了无限大。在后面的应用中,VGG-D和VGG-E得到了最为广泛的应用,它们更多的时候被叫作VGG-16和VGG-19。

4.VGG-B和VGG-C

VGG-C在VGG-B的基础上添加了3个的卷积。的卷积是在NIN[9]中率先使用的。由于卷积在不影响感受野的前提下提升了决策函数的容量,并且有通道融合的作用,因此实现了错误率的下降。

[9] 参见Min Lin 、Qiang Chen、Shwicheng Yan的论文“Network In Network”。

5.VGG-C和VGG-D

VGG-D将VGG-C中的卷积换成了卷积,该组对比表明卷积的提升效果要优于卷积。

6.VGG-D和VGG-E

当网络层数增加到16层时,网络的损失函数趋于收敛。当网络提升到19层时,虽然精度有了些许的提升,但需要的训练时间也大幅增加。

1.2.2 VGG的训练和测试

1.训练

VGG的训练分为单尺度训练(single-scale training)和多尺度训练(multi-scale training)。在单尺度训练中,原图的短边长度为一个固定值S(实验中S被固定为256或384),然后等比例缩放图片,再从缩放的图片中裁剪的子图用于训练模型。在多尺度训练中,每张图的短边长度为256到512之间的一个随机值,然后从缩放的图片中裁剪的子图。

2.测试

测试时可以使用和训练时相同的图片裁剪方法,然后通过若干不同裁剪的图片投票的方式选择最后的分类。

但测试的时候图片是单张输入的,使用裁剪的方式可能会漏掉图片的重要信息。在OverFeat[10]的论文中,提出了将整幅图作为输入的方式,过程如下。

[10] 参见Pierre Sermanet、David Eigen、Xiang Zhang等人的论文“OverFeat: Integrated Recognition, Localization and Detection using Convolutional Networks”。

(1)将测试图片的短边长度固定为QQ可以不等于S

(2)将Q输入VGG,在卷积网络的最后一个卷积,得到的特征向量,WH一般不等于7。

(3)将第一个全连接层看成的卷积层(原本需要先进行Flattern()操作,再进行全连接操作),对比随书资料中的VGG-E和使用全卷积的VGG-E-test,可以发现两者具有相同的参数数量。

(4)将第二个、第三个全连接层看成(numClasses指的是类别数)的卷积层。

(5)如果输入图片大小为,则输出的大小为,因为图片大小可以不一致,所以可以将输出看作某张图片多个切片的预测结果。最终经过加和池化,对每个通道求和,将得到的结果作为最终输出,即取所有切片的平均数作为最终输出。

1.3 更宽:GoogLeNet

在本节中,先验知识包括:

AlexNet(1.1节)。

2012年之后,CNN的研究分成了两大流派,并且两大流派都在2014年有重要的研究成果发表。一个流派的研究方向是增加CNN的深度和宽度,经典的网络有2013年ILSVRC的冠军作品ZFNet和我们在1.2节中介绍的VGG系列。另外一个流派的研究方向是增加卷积核的拟合能力,或者说是增加网络的多样性,典型的网络有可以拟合任意凸函数的Maxout网络[11]、可以拟合任意函数的NIN,以及本节要解析的基于Inception的GoogLeNet。为了能更透彻地了解GoogLeNet的思想,我们首先需要了解Maxout和NIN两种结构。

[11] 参见Ian J. Goodfellow、David Warde-Farley、Mehdi Mirza等人的论文“Maxout Networks”。

1.3.1 背景知识

1.Maxout网络

在之前介绍的AlexNet中,它引入了Dropout来减轻模型的过拟合问题。Dropout可以看作一种集成模型,在训练的每步中,Dropout会将网络的隐层节点以概率P置0。Dropout和传统的装袋(bagging)方法主要有以下两个方面不同:

Dropout的每个子模型的权值是共享的;

在训练的每步中,Dropout会使用不同的样本子集训练不同的子网络。

这样在训练的每步中都会有不同的节点参与训练,可以减轻节点之间的耦合性。在测试时,Dropout使用的是整个网络的所有节点,只是节点的输出值要乘p。因为在测试时,我们不会进行Dropout操作。为了避免Dropout丢失节点带来的缩放问题,我们会将该层节点值乘p来达到Dropout引起的缩放效果。

作者认为,与其像Dropout这样平均地选择,不如有条件地选择节点来生成网络。在传统的神经网络中,第i个隐层的计算方式(暂时不考虑激活函数)如式(1.6)所示:

  (1.6)

假设第个隐层和第i个隐层的节点数分别是dm,那么W是一个的二维矩阵。而在Maxout网络中,W是一个三维矩阵,矩阵的维度是,其中k表示Maxout网络的通道数,是Maxout网络唯一的参数。Maxout网络的数学表达式如式(1.7)所示:

  (1.7)

其中

下面我们通过一个简单的例子来说明Maxout网络的工作方式。对于一个传统的网络,假设第i个隐层有两个节点,第i + 1个隐层有1个节点,那么多层感知机(multi-layer perceptron,MLP)的计算方式如式(1.8)所示:

  (1.8)

其中是激活函数,如tanh、ReLU等,X是输入数据的集合。从图1.14可以看出,传统神经网络的输出节点是由两个输入节点计算得到的。

图1.14 传统神经网络

如果我们将Maxout的参数k设置为5,Maxout网络可以展开成图1.15所示的形式:

图1.15 Maxout网络

其中z = max(z1,z2,z3,z4,z5)。z1z5为线性函数,所以z可以看作分段线性的激活函数。Maxout网络的论文中给出了证明,当k足够大时,Maxout单元可以以任意小的精度逼近任何凸函数,如图1.16所示,图中每条直线代表一个输出节点zi

图1.16 Maxout单元的凸函数无限逼近性

在Keras 2.0之前的版本中,我们可以找到Maxout网络的实现,其核心代码只有一行。

output = K.max(K.dot(X, self.W) + self.b, axis=1)

Maxout网络存在的最大的一个问题是网络的参数数量是传统神经网络的k倍,而k倍的参数数量并没有带来等价的精度提升,所以现在Maxout网络基本已被工业界淘汰。

2.NIN

Maxout单元可以逼近任何凸函数,而NIN的节点理论上可以逼近任何函数。在NIN中,作者也采用整图滑窗的形式,只是将CNN的卷积核替换成了一个小型MLP网络,如图1.17所示。

图1.17 NIN网络结构

在卷积操作中,一次卷积操作仅相当于卷积核和滑窗的一次矩阵乘法,其拟合能力有限。而MLP替代卷积操作增加了每次滑窗的拟合能力。图1.18展示了将LeNet-5改造成NIN在MNIST上的训练过程收敛曲线。通过实验,我们根据实验结果得到了3个重要信息:

NIN的参数数量远大于同类型的CNN;

NIN的收敛速度快于经典网络;

NIN的训练速度慢于经典网络。

图1.18 NIN与LeNet-5对比

通过Keras实现NIN的代码片段如下,全部实验内容见随书资料。

NIN = Sequential()
NIN.add(Conv2D(input_shape=(28,28,1), filters= 8, kernel_size = (5,5),
 padding = 'same',activation = 'relu'))
NIN.add(Conv2D(input_shape=(28,28,1), filters= 8, kernel_size = (1,1),
 padding = 'same',activation = 'relu'))
NIN.add(Flatten())
NIN.add(Dense(196,activation = 'relu'))
NIN.add(Reshape((14,14,1),input_shape = (196,1)))
NIN.add(Conv2D(16,(5,5),padding = 'same',activation = 'relu'))
NIN.add(Conv2D(16,(1,1),padding = 'same',activation = 'relu'))
NIN.add(Flatten())
NIN.add(Dense(120,activation = 'relu'))
NIN.add(Dense(84,activation = 'relu'))
NIN.add(Dense(10))
NIN.add(Activation('softmax'))
NIN.summary()

对比全连接,NIN中的卷积操作保存了网络隐层节点和输入图像的位置关系,卷积的这个特点使其在物体检测和分割任务上得到了更广泛的应用。除了保存特征图的位置关系,卷积还有两个用途:

实现特征图的升维和降维;

实现跨特征图的交互。

另外,NIN提出了使用全局平均池化(global average pooling)来减轻全连接层的过拟合问题,即在卷积的最后一层直接对每个特征图求均值,然后执行softmax操作。

1.3.2 Inception v1

GoogLeNet的核心部件叫作Inception。根据感受野的递推公式,不同大小的卷积核对应不同大小的感受野。例如在VGG的最后一层,卷积核的感受野分别是196、228、260。我们根据感受野的计算公式也可以知道,网络的层数越多,不同大小的卷积核对应在原图的感受野的大小差距越大,这也就是Inception通常在越深的层次中效果越明显的原因。在每个Inception模块中,作者并行使用了这3个不同大小的卷积核。同时,考虑到池化一直在CNN中扮演着积极的作用,所以作者建议Inception中也要加入一个并行的步长为1的最大池化。至此,一个朴素版本的Inception便诞生了,如图1.19所示。

图1.19 朴素版本的Inception

但是这个朴素版本的Inception会使网络的特征图的数量乘4。随着Inception数量的增长,特征图的数量会呈指数级增长,这意味着大量计算资源被消耗。为了提升运算速度,Inception使用了NIN中介绍的卷积在卷积操作之前进行降采样,由此便诞生了Inception v1,如图1.20所示。

图1.20 Inception v1结构

Inception的代码也比较容易实现,建立4个并行的分支并在最后将其合并到一起即可。为了在MNIST数据集上使用Inception,我使用了更窄的网络(特征图的数量均为4,官方特征图的数量已注释在代码中)。

def inception(x):
    inception_1x1 = Conv2D(4,(1,1), padding='same', activation='relu')(x) #64
    inception_3x3_reduce = Conv2D(4,(1,1), padding='same', activation='relu')(x) #96
    inception_3x3 = Conv2D(4,(3,3), padding='same', activation='relu')
        (inception_3x3_reduce) #128
    inception_5x5_reduce = Conv2D(4,(1,1), padding='same', activation='relu')(x) #16
    inception_5x5 = Conv2D(4,(5,5), padding='same', activation='relu')
        (inception_5x5_reduce) #32
    inception_pool = MaxPool2D(pool_size=(3,3), strides=(1,1), padding='same')(x) #192
    inception_pool_proj = Conv2D(4,(1,1), padding='same', activation='relu')
        (inception_pool) #32
    inception_output = merge([inception_1x1, inception_3x3, inception_5x5, 
                              inception_pool_proj], mode='concat', concat_axis=3)
    return inception_output

图1.21展示了使用相同通道数的卷积核的Inception在MNIST数据集上收敛速度的对比。从实验结果可以看出,对于比较小的数据集,Inception的提升非常有限。对比两个网络的容量,我们发现Inception和采用相同特征图的卷积拥有相同数量的参数,实验内容见随书资料。

图1.21 Inception与CNN对比

1.3.3 GoogLeNet

GoogLeNet的命名方式是为了致敬第一代深度卷积网络LeNet-5,作者通过堆叠Inception的方法构造了一个包含9个Inception模块、共22层的网络,并一举拿下了2014年ILSVRC的物体分类任务的冠军。GoogLeNet的网络结构如图1.22所示,高清大图参考其论文。

图1.22 GoogLeNet的网络结构

对比其他网络,GoogLeNet的一个最大的不同是在中间多了两个softmax分支作为辅助损失(auxiliary loss)函数。在训练时,这两个softmax分支的损失会以0.3的比例添加到损失函数上。根据论文的解释,该分支有如下两个作用:

保证较低层提取的特征也有分类的能力;

具有提供正则化并解决梯度消失问题的能力。

需要注意的是,在测试的时候,这两个softmax分支会被移除。

辅助损失函数的提出,是为了遵循信息论中的数据处理不等式(data processing inequality, DPI)原则。所谓数据处理不等式,是指数据处理的步骤越多,则丢失的信息也会越多,其数学建模方式如式(1.9)所示。

  (1.9)

式(1.9)表明,在数据传输的过程中,信息有可能消失,但绝对不会凭空增加。反映到反向传播中,也就是在计算梯度的时候,梯度包含信息的损失会逐层减少,所以GoogLeNet的中间层添加了两组损失函数以防止信息的过度丢失。

1.3.4 Inception v2

我们知道,一个的卷积核与两个的卷积核拥有相同大小的感受野,但是两个的卷积核拥有更强的拟合能力,所以在Inception v2[12]的版本中,作者将的卷积核替换为两个的卷积核,如下面代码所示。Inception v2如图1.23所示。

[12] 参见G. E. Hinton、N. Srivastava、A. Krizhevsky等人的论文“Improving neural networks by preventing co-adaptation of feature detectors”。

def inception_v2(x):
    inception_1x1 = Conv2D(4,(1,1), padding='same', activation='relu')(x)
    inception_3x3_reduce = Conv2D(4,(1,1), padding='same', activation='relu')(x)
    inception_3x3 = Conv2D(4,(3,3), padding='same', activation='relu')
        (inception_3x3_reduce)
    inception_5x5_reduce = Conv2D(4,(1,1), padding='same', activation='relu')(x)
    inception_5x5_1 = Conv2D(4,(3,3), padding='same', activation='relu')
        (inception_5x5_reduce)
    inception_5x5_2 = Conv2D(4,(3,3), padding='same', activation='relu')
        (inception_5x5_1)
    inception_pool = MaxPool2D(pool_size=(3,3), strides=(1,1), padding='same')(x)
    inception_pool_proj = Conv2D(4,(1,1), padding='same', activation='relu')
        (inception_pool)
    inception_output = merge([inception_1x1, inception_3x3, inception_5x5_2, 
                              inception_pool_proj], mode='concat', concat_axis=3)
    return inception_output

图1.23 Inception v2

1.3.5 Inception v3

Inception v3[13]将Inception v1和Inception v2中的卷积换成一个和一个的卷积,如图1.24所示。这样做带来的好处有如下几点:

[13] 参见Christian Szegedy、Vincent Vanhoucke、Sergey Ioffe等人的论文“Rethinking the Inception Architecture for Computer Vision”。

(1)节约了大量参数,提升了训练速度,减轻了过拟合的问题;

(2)多层卷积增加了模型的拟合能力;

(3)非对称卷积核的使用增加了特征的多样性。

def inception_v3(x):
    inception_1x1 = Conv2D(4,(1,1), padding='same', activation='relu')(x)
    inception_3x3_reduce = Conv2D(4,(1,1), padding='same', activation='relu')(x)
    inception_3x1 = Conv2D(4,(3,1), padding='same', activation='relu')
        (inception_3x3_reduce)
    inception_1x3 = Conv2D(4,(1,3), padding='same', activation='relu')(inception_3x1)
    inception_5x5_reduce = Conv2D(4,(1,1), padding='same', activation='relu')(x)
    inception_5x1 = Conv2D(4,(5,1), padding='same', activation='relu')
        (inception_5x5_reduce)
    inception_1x5 = Conv2D(4,(1,5), padding='same', activation='relu')(inception_5x1)
    inception_pool = MaxPool2D(pool_size=(3,3), strides=(1,1), padding='same')(x)
    inception_pool_proj = Conv2D(4,(1,1), padding='same', activation='relu')
        (inception_pool)
    inception_output = merge([inception_1x1, inception_1x3, inception_1x5, 
                              inception_pool_proj], mode='concat', concat_axis=3)
    return inception_output

图1.24 Inception v3

1.3.6 Inception v4

Inception v4[14]的论文中提出了Inception v4、Inception-ResNet v1和Inception-ResNet v2共3个模型架构。其中Inception v4延续了Inception v2和Inception v3的思想,而Inception-ResNet v1和Inception-ResNet v2则将Inception和残差网络进行了结合。

[14] 参见Christian Szegedy、Sergey Ioffe、Vincent Vanhoucke等人的论文“Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning”。

Inception v4的整体结构如图1.25所示,它的核心模块是一个骨干(Stem)模块、3个不同的Inception和两个不同的缩减(Reduction)模块。

图1.25 Inception v4的整体结构

图1.26所示的是Inception v4和Inception-ResNet v2的骨干模块,它由线性结构和包含两路分支的Inception结构组成。特征图的降采样通过步长为2的卷积来完成。图1.26中带有“V”符号的表示padding = 0的有效卷积。

图1.26 Inception v4和Inception-ResNet v2的骨干模块

图1.27所示的是Inception v4的3个Inception模块,这3个Inception模块并不会改变输入特征图的尺寸。从3个Inception模块的结构中我们可以看出它基本沿用了Inception v2和Inception v3的思想,即使用多层小卷积核代替单层大卷积核和变形卷积核。

图1.27 Inception v4的3个Inception模块

图1.28所示的是Inception v4的缩减模块,它也沿用了Inception v2和Inception v3的思想,并使用步长为2的卷积或者池化来进行降采样,其中Reduction-A也复用到了1.3.7节介绍的Inception-ResNet模块中。

图1.28 Inception v4的缩减模块

1.3.7 Inception-ResNet

Inception-ResNet共有v1和v2两个版本,它们都将Inception和残差网络的思想进行了整合,并且拥有相同的流程框架,如图1.29所示。

图1.29 Inception-ResNet v1和Inception-ResNet v2的流程框架

1.Inception-ResNet v1

Inception-ResNet v1的骨干模块并没有使用并行结构,仅仅由不同类型(填充、步长和卷积核尺寸)的卷积操作组成,如图1.30所示。

图1.30 Inception-ResNet v1的骨干模块

Inception-ResNet v1在Inception模块中插入了一条捷径,也就是将Inception和残差网络的思想进行了结合。它的3个Inception模块的结构如图1.31所示。

图1.31 Inception-ResNet v1的3个Inception模块的结构

Inception-ResNet v1的Reduction-A复用了Inception v4的Reduction-A,它的Reduction-B的结构如图1.32所示。

图1.32 Inception-ResNet v1的Reduction-B的结构

2.Inception-ResNet v2

Inception-ResNet v2采用了Inception v4的骨干模块,它的Reduction-A则和其他两个模块保持相同,剩下的3个Inception模块和Reduction-B的结构分别如图1.33和图1.34所示。

图1.33 Inception-ResNet v2的3个Inception模块的结构

从图1.33中可以看出,Inception-ResNet v2的3个Inception模块分别和Inception-ResNet v1的3个Inception模块保持了相同的网络结构,不同的仅有通道数。图1.34体现了Inception-ResNet v2的Reduction-B和图1.32的Inception-ResNet v1的Reduction-B一样具有相同架构、不同通道数的特点。

图1.34 Inception-ResNet v2的Reduction-B的结构

3.残差的缩放

作者重新研究了残差连接的作用,指出残差连接并不会明显提升模型精度,而是会加快训练收敛速度。另外,引入残差连接以后,网络太深了,不稳定,不太好训练,到后面模型的参数可能全变为0了,可通过引入尺度变量(scale)来使得网络更加稳定,如图1.35所示,其中Inception可以用任意其他子网络替代,将其输出乘一个很小的缩放系数(通常在0.1到0.3内),激活缩放之后执行单位加和ReLU激活。

图1.35 Inception v4中提出的残差缩放

1.4 跳跃连接:ResNet

在本节中,先验知识包括:

VGG(1.2节);

GoogLeNet(1.3节);

BN(6.2节);

LSTM(4.1节);

Dropout(6.1节)。

在VGG中,网络深度达到了19层。在GoogLeNet中,网络史无前例地达到了22层。那么,网络的精度会随着网络的层数增多而提高吗?在深度学习中,网络层数增多一般会伴随下面几个问题:

(1)计算资源过度消耗;

(2)模型容易过拟合;

(3)产生梯度消失或梯度爆炸问题。

问题(1)可以通过GPU集群来解决,对一个企业来说,资源并不是很大的问题;问题(2)可以通过采集海量数据,并配合Dropout正则化等方法有效避免;问题(3)可以通过BN避免。

貌似我们只要“无脑”地增加网络的层数,就能从中获益,但实验数据给了我们当头一棒。作者发现,随着网络层数的增加,网络发生了退化(degradation)的现象:随着网络层数的增多,训练集损失值逐渐下降,然后趋于饱和,当我们再增加网络深度时,训练集损失值反而会增大。注意这并不是过拟合,因为在过拟合中训练集损失值是一直减小的。

当网络退化时,浅层网络能够实现比深层网络更好的训练效果,这时如果我们把低层的特征传到高层,那么效果应该至少不比浅层网络的效果差,或者说如果一个VGG-100网络在第98层使用的是和VGG-16第14层一模一样的特征,那么VGG-100的效果应该会和VGG-16的效果相同。所以,我们可以通过在VGG-100的第98层和VGG-16的第14层之间添加一个直接映射(identity mapping)来实现此效果。

从信息论的角度讲,由于DPI(数据处理不等式,见1.3.3节)的存在,在前向传输的过程中,随着层数的加深,特征图包含的图像信息会逐层减少,而残差网络加入直接映射,保证了l + 1层的网络一定比l层的网络包含更多的图像信息。基于这种使用直接映射来连接网络不同层的思想,ResNet(残差网络)[15]应运而生。

[15] 参见Kaiming He、Xiangyu Zhang、Shaoqing Ren等人的论文“Deep Residual Learning for Image Recognition”。

1.4.1 残差网络

1.残差块

残差网络是由一系列残差块组成的,残差块的结构如图1.36所示,其中,weight在CNN中是指卷积操作,addition是指单位加操作。一个残差块可以用式(1.10)表示:

  (1.10)

图1.36 残差块的结构

残差块分成两部分:直接映射部分和残差部分。h(xl)是直接映射部分,即图1.36的左侧;是残差部分,一般由2个或者3个卷积操作构成,即图1.36中右侧包含卷积的部分。在CNN中,xl可能和xl+1的特征图的数量不一样,这时候就需要使用卷积进行升维或者降维,如图1.37所示。这时,残差块表示为式(1.11):

  (1.11)

图1.37 加入卷积的残差块

其中,的卷积核,实验结果表明卷积对模型性能提升作用有限,所以一般在升维或者降维时才会使用。

一般这种版本的残差块叫作resnet_v1,Keras代码实现如下:

def res_block_v1(x, input_filter, output_filter):
    res_x = Conv2D(kernel_size=(3,3), filters=output_filter, strides=1, padding='same')(x)
    res_x = BatchNormalization()(res_x)
    res_x = Activation('relu')(res_x)
    res_x = Conv2D(kernel_size=(3,3), filters=output_filter, strides=1, padding='same')(res_x)
    res_x = BatchNormalization()(res_x)
    if input_filter == output_filter:
        identity = x
    else: #需要升维或者降维
        identity = Conv2D(kernel_size=(1,1), filters=output_filter, strides=1, 
                          padding='same')(x)
    x = keras.layers.add([identity, res_x])
    output = Activation('relu')(x)
    return output

2.残差网络

残差网络的搭建分为两步:

(1)按照VGG的架构搭建一个普通的VGG网络;

(2)在普通的VGG的CNN之间插入单位映射,注意需要升维或者降维的时候可加入卷积。

在实现过程中,一般采用直接堆叠残差块的方式。

def resnet_v1(x):
    x = Conv2D(kernel_size=(3,3), filters=16, strides=1, padding='same', 
               activation='relu')(x)
    x = res_block_v1(x, 16, 16)
    x = res_block_v1(x, 16, 32)
    x = Flatten()(x)
    outputs = Dense(10, activation='softmax', kernel_initializer='he_normal')(x)
    return outputs

3.为什么叫残差网络

在统计学中,残差和误差是非常容易混淆的两个概念。误差衡量的是观测值和真实值之间的差距,残差是指预测值和观测值之间的差距。对于残差网络的命名,作者给出的解释是,网络的一层通常可以看作,而残差网络的一个残差块可以表示为,也就是,在单位映射中,y = x便是观测值,而H(x)是预测值,所以F(x)便对应着残差,因此叫作残差网络。

笔记 比如水位线的高度,模型预测为10m,你测量的是10.4m,但真实值为10.5m。通常你认为10.4m为真实值,其实它并不是。

1.4.2 残差网络背后的原理

残差块一个更通用的表示方式如式(1.12)所示:

  (1.12)

现在我们先不考虑升维或者降维的情况。在式(1.12)中,是直接映射,是激活函数,一般使用ReLU。我们首先给出如下两个假设。

假设1:是直接映射。

假设2:是直接眏射。

那么这时候残差块可以表示为式(1.13):

  (1.13)

对于一个更深的层L,其与l层的关系可以表示为式(1.14):

  (1.14)

式(1.14)反映了残差网络的两个属性:

L层可以表示为任意一个比它浅的l层和它们之间的残差部分之和;

L层是各个残差块特征的单位累加,而残差网络中通常使用单位加组合残差块。

根据反向传播中使用的导数的链式法则,损失函数关于xl的梯度可以表示为式(1.15):

  (1.15)

式(1.15)反映了残差网络的两个属性:

在整个训练过程中,不可能一直为 − 1,也就是说在残差网络中不会出现梯度消失的问题;

表示L层的梯度可以直接传递到任何一个比它浅的l层。

通过分析残差网络的正向和反向两个过程,我们发现当残差块满足上面两个假设时,信息可以非常畅通地在高层和低层之间传导,说明这两个假设是让残差网络可以训练深度模型的充分条件。那么这两个假设是必要条件吗?

1.直接映射是最好的选择

对于假设1,我们采用反证法,假设,那么这时候残差块(见图1.38(b))表示为式(1.16)。

  (1.16)

图1.38 直接映射的变异模型

对于更深的L层,残差块表示为式(1.17)。

  (1.17)

为了简化问题,我们只考虑式(1.17)的左半部分,损失函数xl求偏微分得式(1.18):

  (1.18)

式(1.18)反映了两个属性:

时,很有可能发生梯度爆炸;

时,梯度变成0,会阻碍残差网络信息的反向传递,从而影响残差网络的训练。

所以必须等于1。同理,其他常见的激活函数都会产生和上面的例子类似的阻碍信息反向传播的问题。

对于其他不影响梯度的,例如LSTM中的门机制(见图1.38(c)、图1.38(d))或者Dropout(见图1.38(f))以及图1.37中用于降维的卷积(见图1.38(e)),也许会有效果。作者采用了实验的方法进行验证,实验结果如表1.1所示。

表1.1 残差网络的变异模型(均为110层)在CIFAR-10数据集上的表现

变异情况

捷径类型

F

错误率(%)

备注

原始的

图1.38(a)

1

1

6.61

常数缩放

图1.38(b)

0

1

失败

普通网格

0.5

1

失败

0.5

0.5

12.35

冻结门

排他门

图1.38(c)

1−g(x)

g(x)

失败

初始化 bg = − 0 到 − 5

1−g(x)

g(x)

8.79

初始化 bg = − 6

1−g(x)

g(x)

9.81

初始化 bg = − 7

捷径门

图1.38(d)

1−g(x)

1

12.86

初始化 bg = 0

1−g(x)

1

6.91

初始化 bg = − 6

卷积捷径

图1.38(e)

卷积

1

12.22

Dropout捷径

图1.38(f)

Dropout 0.5

1

失败

从表1.1的实验结果中我们可以看出,在所有的变异模型中,直接映射依旧是效果最好的策略。下面是我们对图1.38中的各种变异模型的分析。

排他门(exclusive gating):在LSTM的门机制中,绝大多数门的值为0或者1,很难落到0.5附近。当g(x)→0时,残差块只由直接映射组成,阻碍卷积部分特征的传播;当g(x)→1时,直接映射失效,残差块退化为普通CNN。

捷径门(short-only gating):当g(x)→0时,网络便是图1.38(a)展示的由直接映射组成的残差网络;当g(x)→1时,残差块退化为普通CNN。

Dropout捷径:类似于将直接映射乘1 − p,所以会影响梯度的反向传播。

卷积捷径:卷积比直接映射拥有更强的表示能力,但是实验效果不如直接映射,这更可能是优化问题而非模型容量问题。

所以我们可以得出结论:假设1成立,即式(1.19)成立。

  (1.19)

2.激活函数的位置

原始的残差网络中提出的残差块可以扩展为更多形式,如图1.39(a)所示,即在卷积之后使用了BN,然后在和直接映射单位加之后使用了ReLU作为激活函数。

在前文中,我们得出假设“直接映射是最好的选择”,所以我们希望构造一种结构能够满足直接映射要求,即定义一个新的残差结构,如式(1.20)所示。

  (1.20)

式(1.20)反映到网络里就是将激活函数移到残差部分使用,即图1.39(c)所示的网络,这种在卷积之后使用激活函数的方法叫作后激活(post-activation)。然后,作者通过调整ReLU和BN的使用位置得到了几个变异模型,即图1.39(d)中的只有ReLU的预激活和图1.39(e)中的全部预激活。作者通过对照实验对比了这几种变异模型,结果如表1.2所示。

图1.39 激活函数在残差网络中的使用

表1.2 基于激活函数位置的变异模型在CIFAR-10上的实验结果

情况

ResNet-110

ResNet-164

传统残差单元

图1.39(a)

6.61

5.93

BN在单位加之后

图1.39(b)

8.17

6.50

ReLU在单位加之前

图1.39(c)

7.84

6.14

只有ReLU的预激活

图1.39(d)

6.71

5.91

全部预激活

图1.39(e)

6.37

5.46

实验结果表明将激活函数移动到残差部分可以提高模型的精度。该网络一般叫作残差网络v2,Keras实现如下:

def res_block_v2(x, input_filter, output_filter):
 res_x = BatchNormalization()(x)
 res_x = Activation('relu')(res_x)
 res_x = Conv2D(kernel_size=(3,3), filters=output_filter, strides=1, 
 padding='same')(res_x)
 res_x = BatchNormalization()(res_x)
 res_x = Activation('relu')(res_x)
 res_x = Conv2D(kernel_size=(3,3), filters=output_filter, strides=1, 
 padding='same')(res_x)
 if input_filter == output_filter:
 identity = x
 else: #需要升维或者降维
 identity = Conv2D(kernel_size=(1,1), filters=output_filter, strides=1, 
 padding='same')(x)
 output= keras.layers.add([identity, res_x])
 return output
 
def resnet_v2(x):
 x = Conv2D(kernel_size=(3,3), filters=16 , strides=1, padding='same', 
 activation='relu')(x)
 x = res_block_v2(x, 16, 16)
 x = res_block_v2(x, 16, 32)
 x = BatchNormalization()(x)
 y = Flatten()(x)
 outputs = Dense(10, activation='softmax', kernel_initializer='he_normal')(y)
 return outputs

一个残差网络的搭建也采用堆叠残差块的方式,在选择是否降维的时候可选择不同的残差块。残差网络v1的网络结构如图1.40所示。

图1.40 残差网络v1的网络结构

1.4.3 残差网络与模型集成

Andreas Veit等人的论文[16]指出残差网络可以从模型集成的角度理解。如图1.41所示,一个3层的残差网络可以展开成一棵含有8个节点的二叉树,而最终的输出便是这8个节点的集成。而他们的实验也验证了这一点:随机删除残差网络的一些节点,网络的性能变化较为平缓,而对VGG等堆叠到一起的网络来说,随机删除一些节点后,网络的输出将完全随机。

[16] 参见Andreas Veit、Michael Wilber、Serge Belongie的论文“Residual Networks Behave Like Ensembles of Relatively Shallow Networks”。

图1.41 残差网络展开成二叉树

1.5 注意力:SENet

在本节中,先验知识包括:

注意力机制(4.2节);

残差网络(1.4节)。

SENet[17]的提出动机非常简单。传统的方法是将网络的特征图的值直接传到下一层,而SENet的核心思想在于建模通道之间的依赖关系,通过网络的全局损失函数自适应地重新矫正通道之间特征的相应强度。简单地讲,SENet通过注意力机制为每一个通道学习一个权值。

[17] 参见Jie Hu、Li Shen、Gang Sun的论文“Squeeze-and-Excitation Networks”。

SENet由一系列连续的SE块组成,一个SE块包括压缩(squeeze)和激发(excitation)两个步骤。其中,压缩通过在特征图上执行全局平均池化得到当前特征图的全局压缩特征向量,特征图通过两层全连接得到特征图中每个通道的权值,并将加权后的特征图作为下一层网络的输入。从上面的分析中我们可以看出,SE块只依赖于当前的一组特征图,因此可以非常容易地嵌入几乎现在所有的CNN中。论文中给出了在当时最优的Inception插入SE块后的实验结果,提升效果显著。

SENet虽然引入了更多的操作,但是其带来的性能下降尚在可以接受的范围之内,从十亿次浮点运算数每秒(giga floating-point operations per second,GFLOPS)、参数数量以及运行时间的实验结果上来看,SENet带来的时间损失并不是非常显著。

1.5.1 SE块

SE块的结构如图1.42所示。

图1.42 SE块的结构

网络的左半部分包含一个传统的卷积变换,忽略这一部分并不会影响我们对SENet的理解。我们直接看一下右半部分,其中U是一个的特征图,(W,H)是图像的尺寸,C是图像的通道数。

经过(压缩操作)后,图像变成了一个的特征向量,特征向量的值由U确定。经过后,特征向量的维度没有变,但是向量值变成了新的值。这些值会通过和U得到加权后的U的维度是相同的。

1.压缩模块

压缩模块的作用是获得特征图U的每个通道的全局信息嵌入(特征向量)。在SE块中,这一步通过VGG中引入的全局平均池化实现。也就是通过求每个通道的特征图的平均值zc实现,如式(1.21)所示。

  (1.21)

通过全局平均池化得到的特征值是全局的(虽然比较粗糙)。另外,zc也可以通过其他方法得到,要求只有一个,得到的特征向量具有全局性。

2.激发模块

激发模块的部分作用是通过zc学习C中每个通道的特征权值,要求有两点:

要足够适配,这样能保证学习到的权值比较具有代表性;

要足够简单,这样不至于添加SE块之后网络的训练速度大幅降低。

通道之间的关系是非排他(non-exclusive)的,也就是说学习到的特征能够激励重要的特征,抑制不重要的特征。

根据上面的要求,SE块使用了两层全连接构成的门机制(gate mechanism)。门控单元s(即图1.42中的特征向量)的计算方式表示为式(1.22):

  (1.22)

其中,表示ReLU激活函数,表示sigmoid激活函数。分别是两个全连接层的权值矩阵。r则是中间层的隐层节点数,论文中指出这个值是16。

得到门控单元s后,最后的输出表示为sU的向量积,即图1.42中的操作如式(1.23)所示:

  (1.23)

其中,的一个特征通道的一个特征图,sc是门控单元s(向量)中的一个标量值。

以上就是SE块算法的全部内容,SE块可以从两个角度理解:

SE块学习了每个特征图的动态先验;

SE块可以看作在特征图维度的自注意力,因为注意力机制的本质也是学习一组权值。

1.5.2 SE-Inception和SE-ResNet

SE块的特性使其能够非常容易地和目前主流的卷积结构结合,例如论文中给出的Inception结构和残差网络结构,如图1.43所示。它们的结合方式也非常简单,只需要在Inception块或者残差块之后直接接上SE块即可。

图1.43 SE-Inception和SE-ResNet

1.5.3 SENet的复杂性分析

SENet的本质是使用自注意力机制根据特征图的值学习每个特征图的权值。U往往是一个由几万个节点值组成的三维矩阵,但是我们得到的s却只有C个值,这种程度的压缩具有非常高的可操作性。例如将U展开成的特征向量,然后通过全连接得到s,这也是目前主流的特征图到全连接的连接方式(Flatten()操作)。而且采用这种方式得到的s的效果往往优于采用SE块的策略得到的。但是SENet没这么做,原因是SENet是可以添加到网络中的任意一层之后的,而全连接操作往往是整个网络结构的性能瓶颈,尤其是当网络的节点数非常大时。

论文中主要对比了ResNet-50以及在ResNet-50中的每一层后添加SE块的网络在运行性能各方面的指标。

从计算性能的方向分析,ResNet-50需要约3.86GFLOPS,而SE-ResNet-50仅仅多了约0.01GFLOPS。

从预测速度上来看,ResNet-50的运行时间约190ms,SE-ResNet-50的运行时间约209ms,多了10%。

从参数数量上来看,SE-ResNet-50的参数数量比ResNet-50的参数数量(2 500万个参数)多了约250万个,约10%。而且作者发现ResNet-50最后几层的SE块可以省掉,且对性能影响并不大,这样的网络参数仅多了4%。

1.5.4 小结

SENet的思想非常简单,即通过特征图为自身的每个通道学习一个特征权值,通过单位乘的方式得到一组加权后的新的特征权值。SENet计算特征权值的方式是使用全局平均池化得到每个特征图的一维表示,再使用两层全连接层得到最终的结果。这个方法虽然简单,但是非常实用,并且SENet在2017年的ILSVRC上取得了非常优异的成绩。1.5.3节对SENet复杂性的分析引发了我们对SE块的进一步联想:如何在计算量和性能之间进行权衡?下面是我的几点思考。

(1)先通过感兴趣区域池化(region of interest pooling,ROI池化)得到更小(如)的特征图,再将其展开作为全连接的输入。其中,ROI池化指任意大小的特征图都可以等分为某一固定格式的池化窗口的操作。

(2)在网络的深度和隐层节点的数目之间进行权衡,究竟是更深的网络效果更好还是更宽的网络效果更好。

(3)每一层的SE块是否一定要相同?比如作者发现浅层更需要SE块,那么我们能否给浅层网络使用一个计算量更大但是性能更好的SE块,而给深层网络使用更为简单、高效的SE块,如单层全连接等。

1.6 更密:DenseNet

在本节中,先验知识包括:

残差网络(1.4节)。

通过残差网络的论文,我们知道残差网络能够应用在特别深的网络中的一个重要原因是无论正向计算精度还是反向计算梯度,信息都能毫无损失地从一层传到另一层。如果我们的目的是保证信息毫无阻碍地传播,那么残差网络的堆叠残差块便不是信息流通最合适的结构。

基于信息流通的原理,一个最简单的思想便是在网络的每个卷积操作中,将其低层的所有特征作为该网络的输入,也就是在一个层数为L的网络中加入个捷径。DenseNet中一个密集块(dense block)的设计如图1.44所示。为了更好地保存低层网络的特征,DenseNet[18]是将不同层的输出拼接在一起,而不是残差网络中的单位加操作。

[18] 参见Gao Huang、Zhuang Liu、Laurens van der Maaten等人的论文“Densely Connected Convolutional Networks”。

图1.44 DenseNet中一个密集块的设计

1.6.1 DenseNet算法解析及源码实现

在DenseNet中,如果全部采用图1.44所示的设计的话,第L层的输入是之前所有的特征图拼接到一起的结果。考虑到现今内存/显存空间的问题,该设计显然是无法应用到网络比较深的模型中的,故而DenseNet采用了图1.45所示的堆积密集块的网络结构。下面我们针对图1.45详细介绍DenseNet算法。

图1.45 DenseNet网络结构

1.密集块

在密集块中,第l层的输入xl是这个块中前面所有层的输出拼接后的结果,表示为式(1.24):

  (1.24)

其中,方括号表示拼接操作,即按照特征图将l − 1个输入拼接成一个张量(tensor)。表示合成函数。在实现时,我使用了stored_features变量存储每个合成函数的输出。

def dense_block(x, depth=5, growth_rate = 3):
 nb_input_feature_map = x.shape[3].value
    stored_features = x
 for i in range(depth):
 feature = composite_function(stored_features, growth_rate = growth_rate)
 stored_features = concatenate([stored_features, feature], axis=3)
 return stored_features

2.合成函数

合成函数(composite function)位于密集块的每一个节点中,其输入是拼接在一起的特征图,输出则是这些特征图经过卷积得到的结果,其中卷积的特征图的数量被定义为成长率k。在DenseNet中,成长率k一般是比较小的整数,在论文中,k = 12。为了更高效地使用浅层的特征图,DenseNet使用了拼接操作,但是拼接在一起的特征图的数量一般比较大。为了提高网络的计算性能,DenseNet先使用卷积将输入数据降维到4k,再使用卷积提取特征,作者将这一过程标准化为卷积卷积,这种结构被定义为DenseNet-B。

def composite_function(x, growth_rate):
 if DenseNetB: #使用DenseNet-B时加入1×1卷积
 x = BatchNormalization()(x)
 x = Activation('relu')(x)
 x = Conv2D(kernel_size=(1, 1), strides=1, filters=4 * growth_rate, 
 padding='same')(x)
 x = BatchNormalization()(x)
 x = Activation('relu')(x)
 output = Conv2D(kernel_size=(3, 3), strides=1, filters = growth_rate, 
 padding='same')(x)
 return output

3.成长率

成长率(growth rate)k是DenseNet的一个超参数,反映的是密集块中每个节点的输入数据的增长速度。在密集块中,每个节点的输出均是一个k维的特征向量。假设整个密集块的输入数据是k0维的,那么第l个节点的输入便是维的。作者通过实验验证,k一般取一个比较小的值,作者在实验中将k设置为12。

1.6.2 压缩层

在图1.45中,密集块之间的结构叫作压缩层(compression layer)。压缩层有降维和降采样两个作用。假设密集块的输出是m维的特征向量,那么下一个密集块的输入是,其中是压缩因子(compression factor),是一个用户自行设置的超参数。当等于1时,密集块的输入和输出的维度相同;当时,网络叫作DenseNet-C,在论文中,。包含瓶颈层和压缩层的DenseNet叫作DenseNet-BC,其中,瓶颈层的作用是在卷积网络中间加入一个通道数特别少的卷积核来减小通道数量,从而提高计算效率。池化层使用的是的平均池化层。

下面是在MNIST数据集上的DenseNet的核心代码,完整代码见随书资料。

def dense_net(input_image, nb_blocks = 2):
    x = Conv2D(kernel_size=(3,3), filters=8, strides=1, padding='same', 
               activation='relu')(input_image)
    for block in range(nb_blocks):
        x = dense_block(x, depth=NB_DEPTH, growth_rate = GROWTH_RATE)
        if not block == nb_blocks-1:
            if DenseNetC:
                theta = COMPRESSION_FACTOR
            nb_transition_filter =  int(x.shape[3].value * theta)
            x = Conv2D(kernel_size=(1,1), filters=nb_transition_filter, strides=1, 
                       padding='same', activation='relu')(x)
        x = AveragePooling2D(pool_size=(2,2), strides=2)(x)
    x = Flatten()(x)
    x = Dense(100, activation='relu')(x)
    outputs = Dense(10, activation='softmax', kernel_initializer='he_normal')(x)
    return outputs

1.6.3 小结

DenseNet具有如下优点:

信息流通更为顺畅;

支持特征重用;

网络更窄。

由于DenseNet需要在内存中保存密集块的每个节点的输出,此时需要极大的显存才能支持较大规模的DenseNet,这也导致了现在工业界主流的算法依旧是残差网络。

1.7 模型集成:DPN

在本节中,先验知识包括:

残差网络(1.4节);

DenseNet(1.6节);

RNN(4.1节)。

残差网络和DenseNet是捷径系列网络的最为经典的两个基础网络,其中残差网络通过单位加的方式直接将输入加到输出的卷积上,DenseNet则通过拼接的方式将输出与之后的每一层的输入进行拼接。本节介绍的双路网络(dual path network,DPN)[19]则通过高阶RNN(high order RNN,HORNN)[20]将残差网络和DenseNet进行了融合。所谓“双路”,即一条路是残差网络,另一条路是DenseNet。论文的动机是通过对残差网络和DenseNet的分解,证明残差网络更侧重于特征的复用,而DenseNet则更侧重于特征的生成,通过分析两个模型的优劣,将两个模型有针对性地组合起来。论文提出了拥有两个模型优点的DPN,并一举获得了2017年ILSVRC物体定位任务的冠军。

[19] 参见Yunpeng Chen、Jianan Li、Huaxin xiao等人的论文“Dual Path Networks”。

[20] 参见Rohollah Soltani、Hui Jiang的论文“Higher Order Recurrent Neural Networks”。

1.7.1 高阶RNN、DenseNet和残差网络

1.高阶RNN

假设ht是第t个时间片的隐层节点状态,xt是第t个时间片的输入数据,第0个时间片的隐层状态为x0,即h0 = x0。假设k是当前计算到的时间片,对于每个时间片,表示特征提取函数,用于将输入函数中的ht转换为对应的特征;则表示用于将之前所有时间片的特征进行聚合的函数,则高阶RNN可以抽象为式(1.25)。

  (1.25)

2.DenseNet与高阶RNN

DenseNet采用的是拼接的方式,而高阶RNN则采用的是单位加操作,欲联系DenseNet和高阶RNN,我们需要将DenseNet的拼接操作转换为高阶RNN的单位加操作。DenseNet的核心便是通过跨层的拼接实现信息流的捷径,表示为式(1.26):

  (1.26)

即第k层的输入由前k − 1层的输出拼接之后经过一个卷积得到,gk为合成函数,由BN、激活函数和卷积组成。我们这里忽略BN和激活函数,那么卷积操作可以表示为式(1.27)。

  (1.27)

从式(1.27)中我们可以看出,对若干组卷积来说,如果它们输出的特征图的通道数是相同的,对输出进行拼接再进行卷积等价于分别对每组特征单独进行卷积再求和的操作。

对高阶RNN来说,fg的权值是共享的,即对于所有的tk,满足以及。但是对DenseNet来说,每一层都有自己的参数,这也就意味着对DenseNet来说,fg是不共享的。因为在DenseNet中,当使用之前层的特征时,都对其进行了变换操作,这也就意味着DenseNet具有产生新的特征的能力。DenseNet和高阶RNN的关系如图1.46所示,左图是原始的DenseNet结构,右图显示了DenseNet和高阶RNN的关系,其中z1是时延(time delay)单元,⊕是单位加操作。

图1.46 DenseNet和高阶RNN的关系

综上所述,DenseNet是不满足时的特殊的高阶RNN,且DenseNet具有侧重于新特征生成的能力。

3.DenseNet与残差网络

假设:如果对于所有的tk,均满足,此时DenseNet将退化为残差网络,也就是说残差网络是一种特殊形式的DenseNet。

证明:我们这里给式(1.25)添加一个中间变量rkr0 = 0,则式(1.25)可以写成式(1.28)和式(1.29)的形式。

  (1.28)

  (1.29)

将式(1.28)和式(1.29)组合在一起便有了式(1.30)。

  (1.30)

式(1.30)中。可以看出,式(1.30)展示了一个非常明显的残差结构。

证毕!

从式(1.30)中我们可以看出,当之间存在参数共享时,即残差网络退化为一个传统的RNN,如图1.47所示。图1.47中,是RNN的激活函数,是单位映射,是RNN的时延单元。

图1.47 残差网络和RNN的关系

图1.48更形象地展示了残差网络与DenseNet之间的关系。图1.48(a)展示了一个标准的残差网络。图1.48(b)是将DenseNet的拼接改变成单位加之后的表示,其中绿色箭头和橙色箭头表示的卷积分别表示式(1.25)中的,这两个卷积都是有独立系数的,当图1.48(b)中的时便变成了图1.48(c),即图1.48(c)是满足的DenseNet,也就是一个残差网络。

在图1.48(b)和图1.48(c)中可以看到带下画线的卷积,这些卷积用于让图1.48(c)和图1.48(a)对应,目的是证明共享参数后DenseNet会退化为残差网络,并无其他的作用。

图1.48 残差网络是DenseNet的一种特殊形式

通过对比残差网络和DenseNet的原理,我们来分析一下两个网络的优缺点。从上面的描述中我们可以看出,残差网络复用了前面网络提取的特征,而每一层的特征都会原封不动地传到下一层,这样每一层提取的特征都有其不同点,因此特征的冗余度比较低。而DenseNet的每个卷积的参数都不同,前面的层不再被后面的层直接使用,而是被重新加工后生成了新的特征,这种结构可能会造成后面的层提取的特征是前面的层已经提取过的特征,所以说DenseNet是一个冗余度比较高的网络。通过分析可以看出,残差网络的特征复用率高,但是冗余度低,而DenseNet则可以创造新的特征,但是其冗余度高。基于此,作者结合了两个网络共同的优点,创造了DPN。

1.7.2 DPN详解

1.双路架构

基于上面的分析,双路架构(dual path architecture,DPA)以残差网络为主要框架,保证了特征的低冗余度,并在其基础上添加了一个非常小的DenseNet分支,用于生成新的特征。DPA的结构可以使用式(1.31)到式(1.34)来表示。

  (1.31)

  (1.32)

  (1.33)

     (1.34)

式(1.31)中的xk表示一个DenseNet分支,式(1.32)中的yk表示一个残差网络分支。式(1.33)将两个网络通过单位加的方式进行了合并,式(1.34)使用了转换函数gk得到了一个新的特征。DPA的结构如图1.49所示,其左侧是一个DenseNet,右侧是一个残差网络,表示拆分操作,⊕表示单位加操作。

图1.49 DPA的结构

2.双路网络

图1.50展示了真正的DPN结构,其和图1.49的最大不同在于残差网络和DenseNet共享了第一个卷积。在实际计算卷积时,DPN使用了分组卷积来提升网络的性能。在设计网络的超参数时,残差网络的通道数也比DenseNet的通道数多,防止DenseNet随着层数的增加引发显存消耗速度过快的问题。

图1.50 DPN的结构

和其他网络一样,我们也可以通过堆叠网络块的方式来提升模型的容量。

1.7.3 小结

作者通过一系列非常精彩的推导分析出了残差网络和DenseNet各自的优缺点,通过将CNN抽象化为高阶RNN,得出了残差网络具有低冗余度的优点但是存在特征重用的缺点,也得出了DenseNet具有可以生成新特征的优点但是存在冗余度过高的缺点,因此提出了结合残差网络和DenseNet的DPN。

DPN融合残差网络和DenseNet是基于投票的模型集成的方式实现的。基于这个方式,我们也许可以从下面几个角度进行进一步的优化:

采用更多种类的网络分支,如SENet、NAS等;

采用更好的集成方式,例如加上一个注意力机制为不同的网络结构分支学习不同的权值,因为极有可能不同的网络结构在不同的深度起着不同的作用。

1.8 像素向量:iGPT

在本节中,先验知识包括:

Transformer(4.3节);

BERT(5.4节);

GPT-1、GPT-2、GPT-3(5.3节);

层归一化(6.3节)。

GPT系列证明了其在NLP方向强大的学习能力。GPT的训练不需要人工标注数据,借助于语言模型构建损失函数,可以提取到泛化能力非常强的预训练语言模型。那么能否将这种无监督学习的思想迁移到图像分类中呢?本节要介绍的图像GPT[21](iGPT)便使用GPT-2[22]的网络结构进行图像特征的建模,然后将特征直接应用到下游的分类任务中的算法。实验结果表明,iGPT拥有强大的图像理解能力,不仅在诸多分类数据集上取得了非常好的分类效果,更惊艳的是它在图像补全上的表现,如图1.51[23]所示,在实验中,输入图像的下半部分会被遮住,iGPT使用上半部分的输入来预测下半部分的内容。

[21] 参见Mark Chen、Alec Radford、Rewon Child等人的论文“Generative Pretraining from Pixels”。

[22] 参见Alec Radford、Jeffrey Wu、Rewon Child等人的论文“Language Models are Unsupervised Multitask Learners”。

[23] 图片来源:OpenAI官网。

图1.51 iGPT的图像补全效果

因为GPT-2使用的是且仅是Transformer[24],所以iGPT也是一个完全无卷积或者池化的神经网络,引领了使用Transformer完成CV任务的浪潮。和其他的OpenAI的论文非常类似,iGPT的论文并没有提出新颖的模型架构或者算法思想,甚至连网络都直接照搬GPT-2。这篇论文的最大贡献在于突破了使用CNN解决图像问题的思维困境,赋予了图像数据一种新的特征表示方式,使得CV和NLP领域之间的差距缩到了几乎为0。基于这一点,OpenAI又推出了用于图像分类的零样本模型CLIP[25]和用于图像生成的DALL-e[26],这两个模型的效果依旧非常惊艳。

[24] 参见Ashish Vaswani、Noam Shazeer、Niki Parmar等人的论文“Attention Is All You Need”。

[25] 参见Alec Radford、Jong Wook Kim、Chris Hallacy等人的论文“Learning Transferable Visual Models From Natural Language Supervision”。

[26] 参见Aditya Ramesh、Mikhail Pavlov、Gabriel Goh等人的论文“Zero-Shot Text-to-Image Generation”。

1.8.1 iGPT详解

iGPT包含预训练和微调两个阶段,其中在预训练阶段,作者对比了自回归(auto regressive,AR)的预测下一个像素的任务和类似BERT(Bidirectional Encoder Representations from Transformers)的掩码语言模型(masked language model,MLM)任务,即预测被替换为掩码的像素。掩码语言模型任务的逻辑是使用上下文来预测被替换为掩码符号的像素的内容。为了衡量iGPT的提取图像特征的能力,作者使用了线性探测(linear probe)进行验证,基于的原理是:如果模型能够比较好地提取特征,那么在这个特征上直接进行分类,分类任务应该会取得非常好的效果。因为在线性探测中,下一个阶段的分类任务只知道上一个阶段模型产生的特征,而上一个阶段的模型的结果对下一个阶段的分类任务来说是一个“黑盒子”,因此它能不受模型架构的影响而更精确地衡量特征的质量。和线性探测不同的是,微调是指使用带标签的数据对包含分类层和特征提取部分的整个网络在无监督训练的基础上进行参数值的有监督微调。iGPT的核心内容可以概括为图1.52,下面我们对其进行详细介绍。

图1.52 iGPT的3个核心部分

1.从CV到NLP

众所周知,语言模型的输入是1维的文本数据,而图像是2维的栅格数据,如果想要将Transformer应用到图像中,第一步便是将图像转换为1维的结构。

Transformer的核心是注意力机制,其中涉及了大量的矩阵运算,随着序列长度的增加,这些运算所涉及的计算量呈指数级增长。表1.3所示的是iGPT论文中使用的3个数据集和其图像样本的分辨率,它们会分别将缩放到不同的IR中来得到模型的输入图像。对CIFAR-10/CIFAR-100的图像来说,其展开之后的序列长度是3 072,注意力机制尚且有能力处理。但是对ImageNet的图像来说,其展开之后的序列长度是150 528,这对Transformer来说就有些力不从心了。为了解决这个问题,作者首先将输入图像进行降采样,这里将输入图像的大小叫作输入分辨率(input resolution,IR),论文中的IR有3组,分别是

表1.3 iGPT中3个数据集的图像分辨率

数据集

图像分辨率

CIFAR-10/CIFAR-100

STL-10

ImageNet

对最小的的IR来说,其计算量依旧是极其大的,但是如果再降低分辨率的话,图像将变得人工不可分。为了进一步缓解计算压力,作者对标准的(R, G, B)图像数据进行了k = 512的k均值聚类,由此得到的图像仍能保持颜色信息,但是长度比(R, G, B)图像的短了。上面3组IR转化后的值分别是322、482、642。作者将这个分辨率叫作模型分辨率(model resolution,MR)。

在得到Transformer能够处理的缩小了分辨率的图像之后,便要将图像展开成1维结构,iGPT采用的是光栅扫描顺序,或者叫作滑窗扫描顺序,如图1.52(a)所示。

2.预训练

具体地讲,给定一个由n个无标签图像组成的批次样本,对于其中的任意一个图像像素顺序,iGPT使用了自回归模型对其概率密度进行建模,如式(1.35)所示:

  (1.35)

其中,图像像素顺序π是单位排列的,也就是按上面说的光栅扫描顺序排列的。参数的优化是通过最小化数据的负对数似然训练的,如式(1.36)所示。

  (1.36)

除了自回归模型,论文中另外一个预训练任务是类似BERT的MLM。在MLM中,每个像素的索引会有0.15的概率出现在BERT掩码M中(被替换为掩码),我们的目标便是使用未被替换为掩码的像素预测被替换为掩码的像素,如式(1.37)所示,其中x[1,n]\M表示未被替换为掩码字符的部分。

  (1.37)

这一部分如图1.52(b)所示。

当训练CIFAR-10/CIFAR-100、STL-10时,使用的预训练数据集是ImageNet。在训练ImageNet时,使用的预训练数据集是从网上爬取到的1亿张图片。

3.网络结构

对于一个输入序列,首先将每个位置的标志变成d维的嵌入向量。iGPT的解码器由L个块组成,对于第l + 1个块,它的输入是nd维的嵌入向量,输出是nd维的嵌入向量。iGPT的解码块使用GPT-2的网络结构,如式(1.38)所示。

  (1.38)

这里层归一化(layer normalization,LN)[27]既作用于注意力部分,又作用于MLP部分。

[27] 参见Jimmy Lei Ba、Jamie Ryan Kiros、Geoffrey E.Hinton的论文“Layer Normalization”。

def block(x, scope, *, past, hparams):
 with tf.variable_scope(scope):
 nx = x.shape[-1].value
        a, present = attn(norm(x, 'ln_1'), 'attn', nx, past=past, hparams=hparams)
 x = x + a
        m = mlp(norm(x, 'ln_2'), 'mlp', nx*4, hparams=hparams)
 x = x + m
        return x, present

在进行Transformer的自注意力的计算时,作者在原生的自注意力的基础上加入了上三角掩码,原生的自注意力(self-attention)的计算方式如式(1.39)所示:

  (1.39)

其中,QKV分别是基于输入内容得到的3个不同的特征矩阵(详见Transformer部分),dk是特征K的特征数。加入上三角掩码后的自注意力的计算方式如式(1.40)所示:

  (1.40)

假设上三角矩阵为b,mask_attention的计算方式如式(1.41)所示:

  (1.41)

其中,是一个非常小的浮点数。上三角掩码b的生成方式和mask_attention的核心代码如下。

def attention_mask(nd, ns, *, dtype):
    i = tf.range(nd)[:, None]
    j = tf.range(ns)
    m = i >= j - ns + nd
    return tf.cast(m, dtype)
 
 
def mask_attn_weights(w):
    _, _, nd, ns = shape_list(w)
    b = attention_mask(nd, ns, dtype=w.dtype)
    b = tf.reshape(b, [1, 1, nd, ns])
    w = w * b - tf.cast(1e10, w.dtype) * (1 - b)
    return w
 
 
def multihead_attn(q, k, v):
    # q、 k、 v 的形状为[batch, heads, sequence, features]
    w = tf.matmul(q, k, transpose_b=True)
    w = w * tf.rsqrt(tf.cast(v.shape[-1].value, w.dtype))
 
    if not hparams.bert:
        w = mask_attn_weights(w)
    w = softmax(w)
    a = tf.matmul(w, v)
    return a

如上面代码所示,在训练BERT的MLM时没有使用mask_attention(if not hparams.bert)。最后,通过LN得到解码器的最终输出。注意在iGPT中,并没有加入位置编码,而是希望模型能够自行学到这种空间位置关系。但是自回归的模型(例如ELMo)并不是全部需要自行学习,因为它的光栅扫描顺序在一定程度对输入数据的顺序进行了建模。对比传统的CNN方法,iGPT的一个特殊点是它具有排列不变性,而CNN的预测位置的值更容易受到其临近位置的值的影响。

iGPT采用了GPT-2的多层Transformer的架构,和GPT-2的一个不同点是使用了稀疏Transformer[28]提出的初始化的方法。

[28] 参见Rewon Child、Scott Gray、Alec Radford等人的论文“Generating Long Sequences with Sparse Transformers”。

iGPT提供了4个不同容量的模型,分别是iGPT-S、iGPT-M、iGPT-L以及iGPT-XL,它们的不同点取决于网络的层数L和嵌入向量的维度d,它们具体的值和参数数量如表1.4所示。

表1.4 iGPT中3个数据集的图像分辨率

模型

L

d

参数数量

iGPT-S

24

512

0.76亿

iGPT-M

36

1 024

4.55亿

iGPT-L

48

1 536

13.62亿

iGPT-XL

60

3 072

68.01亿

4.微调

在进行微调(fine-tuning)时,首先通过序列尺度上的平均池化将每个样本的特征nL变成d维的特征向量,如式(1.42)所示。

  (1.42)

然后在f L之上再添加一个全连接层得分类的logits,而微调的目标则是最小化交叉熵损失LCLF

当同时优化生成损失LGEN和分类损失时,优化目标为。这样可以得到更好的结果,其中

5.线性探测

iGPT通过将平均池化作用到模型的每一层,对比不同层不同的表征能力,如式(1.43)所示。

  (1.43)

传统的线性探测用于提取最后一层的特征,但是iGPT中通过语言模型训练的模型中最后一层并不是线性探测效果最好的一层,效果最好的是中间几层,如图1.53所示。不同于传统的监督模型,iGPT的中间层具有更强的表征能力,可能是因为在CNN中,浅层的网络更侧重于提取图像的表层信息,例如颜色、纹理等,而深层的网络更侧重于提取目标值的信息。在iGPT中会预测像素点可能的值,因此不管深层网络或者浅层网络都不太适用于分类,而中间的层反而会有更多的图像信息,因此得到的线性探测的准确率更高。

图1.53 iGPT中不同层的性能表现

1.8.2 实验结果分析

图1.54展示了不同容量的模型在不同的线性探测下的准确率,从中我们可以得出几条重要的结论:

对比3个模型的准确率,可以看出容量越大,线性探测的准确率越高;

对比模型准确率和验证损失可以看出,线性探测的准确率和训练的验证损失呈负相关;

对比相同损失值下的不同模型的准确率,我们可以看出模型容量越大,它的泛化能力越强;

线性探测准确率的上升并没有出现明显放缓的趋势,说明随着模型容量的增大,准确率还有继续提升的空间。

图1.54 iGPT的3个模型的线性探测的准确率和验证损失之间的关系

图1.55则展示了自回归和BERT分别在线性探测和微调,以及在单独训练和集成训练下的对比结果,可以得出如下结论:

基于自回归的训练方式要优于基于BERT的训练方式;

在ImageNet数据集中,加上微调之后的基于BERT的训练方式要优于基于自回归的训练方式;

集成了BERT和自回归的方法的效果是最优的。

图1.55 自回归(下)和BERT(上)的效果对比,线性探测(蓝色)和微调(红色)的效果对比,单独训练(浅色)和集成训练(深色)的效果对比

1.8.3 小结

iGPT打破了使用卷积操作进行图像处理的传统,创新性地使用Transformer进行图像处理,并且通过类似构建语言模型的方式得到了泛化能力非常强的特征,在线性探测或者微调方法上都取得了可以匹敌先进的有监督方法的效果。iGPT更惊艳的是在图像补全方向的效果,从补全的效果上来看,iGPT似乎已经学到了图像的本质信息。最为重要的是,我们没有看到iGPT的性能上限,通过增大数据量和模型容量,iGPT可以到达一个新的高度。

作为一个使用Transformer解决图像问题的基石性模型,iGPT也有很多的缺点。

iGPT对计算资源的要求是非常高的,iGPT-L在Tesla V100上的训练约需2 500天,而同性能的MoCo[29]模型大概仅需要70天。iGPT的参数数量也是同性能CNN的2~3倍。

[29] 参见Kaiming He、Haoqi Fan、Yuxin Wu等人的论文“Momentum Contrast for Unsupervised Visual Representation Learning”。

iGPT目前还只能处理低分辨率图片,把ImageNet的图片的分辨率降低到无疑将损失很多信息,而基于CNN的方法可以通过滑窗的方式轻松处理大分辨率图片,这个思想是值得iGPT借鉴的。对比iGPT中使用的Transformer,Transformer-XL则拥有对长序列更强的建模能力和更快的预测速度,也许Transformer-XL才是更适合iGPT使用的网络结构。

iGPT生成的补全图像还是依赖于输入数据的分布的,而这种数据偏差也是需要解决的问题之一。

1.9 Visual Transformer之Swin Transformer

在本节中,先验知识包括:

Transformer(4.3节);

残差网络(1.4节);

LN(6.3节);

iGPT(1.8节)。

自从Transformer在NLP任务上取得突破性进展之后,业内一直尝试着把Transformer用于CV领域。之前的若干方法,如iGPT、ViT[30]等,都将Transformer用在了图像分类领域,这些方法有两个非常严峻的问题:

[30] 参见Alexey Dosovitskiy、Lucas Beyer、Alexander Kolesnikov等人的论文“An image is worth 16 × 16 words: Transformers for image recognition at scale”。

(1)受限于图像的矩阵性质,一个能表达信息的图像往往至少需要几百个像素点,而建模这种包含几百个长序列的数据恰恰是Transformer的“天生”缺陷;

(2)目前多利用Transformer框架来进行图像分类,理论上来讲利用其解决检测问题应该也比较容易,但是对于分割这种密集预测的场景,Transformer并不擅长解决。

本节提出的Swin(Shift window)Transformer[31]解决了这两个问题,并且在分类、检测、分割任务上都取得了非常好的效果。Swin Transformer的最大贡献是提出了一个可以广泛应用到所有CV领域的骨干网络,并且大多数CNN中常见的参数在Swin Transformer中也是可以人工调整的,例如可以调整网络块数、每一块的层数以及输入图像的大小等。该网络的架构设计非常巧妙,是一个非常精彩的将Transformer应用到图像领域的架构,值得我们去学习。

[31] 参见Ze Liu、Yutong Lin、Yue Cao等人的论文“Swin Transformer: Hierarchical Vision Transformer using Shifted Windows”。

在Swin Transformer之前的ViT和iGPT,都使用了小尺寸的图像作为输入,这种直接调整大小的策略无疑会损失很多信息。与它们不同的是,Swin Transformer的输入是原始尺寸的图像,例如ImageNet的的图像。另外Swin Transformer使用的是CNN中最常用的多层次的网络结构,在CNN中一个特别重要的特征是随着网络层次的加深,节点的感受野在不断扩大,这个特征在Swin Transformer中也是满足的。Swin Transformer的这种层次结构,使得它可以像FPN[32]、U-Net[33]等一样完成分割或者检测的任务。Swin Transformer和ViT的对比如图1.56所示。

[32] 参见Tsung-Yi Lin、Piotr Dollár、Ross Girshtick等人的论文“Feature Pyramid Networks for Object Detection”。

[33] 参见Olaf Ronneberger、Philipp Fischer、Thomas Brox等人的论文“U-Net: Convolutional Networks for Biomedical Image Segmentation”。

图1.56 Swin Transformer和ViT的对比

本节将结合Swin Transformer的PyTorch源码对Swin Transformer论文中的算法细节以及代码实现展开介绍,并对该论文中解释模糊的点进行具体分析。学习完本节后,你将更了解Swin Transformer的结构细节和设计动机,现在我们开始吧!

1.9.1 网络结构详解

1.基础结构

Swin Transformer共提出了4个网络结构,从小到大依次是Swin-T、Swin-S、Swin-B和Swin-L,为了绘图简单,本节以最简单的Swin-T作为示例来讲解。Swin-T的网络结构如图1.57所示。Swin Transformer最核心的部分便是4个阶段中的Swin Transformer块,它的具体结构如图1.58所示,这一部分的源码如下。

class SwinTransformer(nn.Module):
    def __init__(self, *, hidden_dim, layers, heads, channels=3, num_classes=1000, 
                 head_dim=32, window_size=7, downscaling_factors=(4, 2, 2, 2), 
                 relative_pos_embedding=True):
        super().__init__()
 
        self.stage1 = StageModule(in_channels=channels, hidden_dimension=hidden_dim, 
                                  layers=layers[0],
                                  downscaling_factor=downscaling_factors[0], 
                                  num_heads=heads[0], head_dim=head_dim,
                                  window_size=window_size, 
                                  relative_pos_embedding=relative_pos_embedding)
 
        self.stage2 = StageModule(in_channels=hidden_dim, 
                                  hidden_dimension=hidden_dim * 2, layers=layers[1],
                                  downscaling_factor=downscaling_factors[1], 
                                  num_heads=heads[1], head_dim=head_dim,
                                  window_size=window_size, 
                                  relative_pos_embedding=relative_pos_embedding)
 
        self.stage3 = StageModule(in_channels=hidden_dim * 2, 
                                  hidden_dimension=hidden_dim * 4, layers=layers[2],
                                  downscaling_factor=downscaling_factors[2], 
                                  num_heads=heads[2], head_dim=head_dim,
                                  window_size=window_size, 
                                  relative_pos_embedding=relative_pos_embedding)
 
        self.stage4 = StageModule(in_channels=hidden_dim * 4, 
                                  hidden_dimension=hidden_dim * 8, layers=layers[3],
                                  downscaling_factor=downscaling_factors[3], 
                                  num_heads=heads[3], head_dim=head_dim,
                                  window_size=window_size, 
                                  relative_pos_embedding=relative_pos_embedding)
 
        self.mlp_head = nn.Sequential(
            nn.LayerNorm(hidden_dim * 8),
            nn.Linear(hidden_dim * 8, num_classes)
        )
 
    def forward(self, img):
        x = self.stage1(img)
        x = self.stage2(x)
        x = self.stage3(x)
        x = self.stage4(x)  # (1, 768, 7, 7)
        x = x.mean(dim=[2, 3])  # (1, 768)
        return self.mlp_head(x)

图1.57 Swin-T的网络结构

从源码中我们可以看出Swin Transformer的网络结构非常简单,由4个阶段和一个输出组成,非常容易扩展。Swin Transformer的4个阶段的网络结构是一样的,每个阶段仅对几个基本的超参数进行调整,包括隐层节点个数、网络层数、多头自注意的头的个数、降采样的尺度等,这些超参数在源码中的具体值如下所示,本节也会以这组超参数对网络结构进行详细讲解。

net = SwinTransformer(
    hidden_dim=96,
    layers=(2, 2, 6, 2),
    heads=(3, 6, 12, 24),
    channels=3,
    num_classes=3,
    head_dim=32,
    window_size=7,
    downscaling_factors=(4, 2, 2, 2),
    relative_pos_embedding=True
)

2.块分裂/块合并

在图1.57中图像之后是1个块分裂(patch partition),再之后是1个线性嵌入(linear embedding),二者加在一起就表示1个块合并(patch merging)。块合并部分的源码如下:

class PatchMerging(nn.Module):
    def __init__(self, in_channels, out_channels, downscaling_factor):
        super().__init__()
        self.downscaling_factor = downscaling_factor
        self.patch_merge = nn.Unfold(kernel_size=downscaling_factor, 
                                     stride=downscaling_factor, padding=0)
        self.linear = nn.Linear(in_channels * downscaling_factor ** 2, out_channels)
 
    def forward(self, x):
        b, c, h, w = x.shape
        new_h, new_w = h // self.downscaling_factor, w // self.downscaling_factor
        x = self.patch_merge(x) # (1, 48, 3136)
        x = x.view(b, -1, new_h, new_w).permute(0, 2, 3, 1) # (1, 56, 56, 48)
        x = self.linear(x) # (1, 56, 56, 96)
        return x

块合并的作用是对图像进行降采样,类似于CNN中的池化层。块合并主要是通过nn.Unfold()函数实现降采样的,nn.Unfold()的功能是对图像进行滑窗,相当于卷积操作的第一步,因此它的参数包括窗口的大小和滑窗的步长。根据源码中给出的超参数我们知道这一步降采样的比例是4,因此经过nn.Unfold()之后会得到个长度为的特征向量,其中3是输入阶段1的特征图的通道数,阶段1的输入是RGB图像,因此通道数为3,表示为式(1.44)。

  (1.44)

接着viewpermute将得到的向量序列还原为的二维矩阵,linear将长度是48的特征向量映射到out_channels的长度,因此阶段1的块合并的输出向量维度是(1,56,56,96)。

可以看出块分裂/块合并起到的作用类似CNN中通过带有步长的滑窗来降低分辨率,再通过卷积来调整通道数。不同的是,在CNN中最常使用的用于降采样的最大池化或者平均池化往往会丢弃一些信息,例如最大池化会丢弃窗口内的低响应值,而采用块合并的策略并不会丢弃其他响应,但它的缺点是带来运算量的增加。在一些需要提升模型容量的场景中,我们可以考虑使用块合并来替代CNN中的池化。

3.Swin Transformer的阶段n

如我们上面分析的,图1.57中的块分裂 + 线性嵌入就表示块合并,因此Swin Transformer的一个阶段便可以看作由块合并和Swin Transformer块组成,源码如下。

class StageModule(nn.Module):
    def __init__(self, in_channels, hidden_dimension, layers, downscaling_factor, 
                 num_heads, head_dim, window_size, relative_pos_embedding):
        super().__init__()
        assert layers % 2 == 0, # 为了确保同时包含窗口自注意力和位移窗口自注意力,我们需要确保
                                  总层数是2的整数倍
 
        self.patch_partition = PatchMerging(in_channels=in_channels, 
                                            out_channels=hidden_dimension,
                                            downscaling_factor=downscaling_factor)
 
        self.layers = nn.ModuleList([])
        for _ in range(layers // 2):
            self.layers.append(nn.ModuleList([
                SwinBlock(dim=hidden_dimension, heads=num_heads, 
                          head_dim=head_dim, mlp_dim=hidden_dimension * 4, 
                          shifted=False, window_size=window_size, 
                          relative_pos_embedding=relative_pos_embedding),
                SwinBlock(dim=hidden_dimension, heads=num_heads, 
                          head_dim=head_dim, mlp_dim=hidden_dimension * 4, 
                          shifted=True, window_size=window_size, 
                          relative_pos_embedding=relative_pos_embedding),
            ]))
 
    def forward(self, x):
        x = self.patch_partition(x)
        for regular_block, shifted_block in self.layers:
            x = regular_block(x)
            x = shifted_block(x)
        return x.permute(0, 3, 1, 2)

4.Swin Transformer块

Swin Transformer块是该算法的核心点,它由窗口多头自注意力(window multi-head self-attention,W-MSA)和移位窗口多头自注意力(shifted-window multi-head self-attention,SW-MSA)组成,如图1.58所示。出于这个结构,Swin Transformer的层数要为2的整数倍,一层提供给W-MSA,一层提供给SW-MSA。

图1.58 Swin Transformer块的具体结构

从图1.58中我们可以看出输入该阶段的特征先经过LN进行归一化,再经过W-MSA进行特征的学习,接着通过残差操作得到。接着通过LN、MLP和残差操作,得到这一层的输出特征。SW-MSA层的结构和W-MSA层的类似,不同的是计算特征部分分别使用了SW-MSA和W-MSA。可以从上面的源码中看出它们除了shifted的bool值不同,其他值是完全一致的。这一部分可以表示为式(1.45)。

  (1.45)

Swin Transformer块的源码如下所示,和论文中不同的是,LN操作(PerNorm()函数)从自注意力之前移到了自注意力之后。

class Residual(nn.Module):
    def __init__(self, fn):
        super().__init__()
        self.fn = fn
 
    def forward(self, x, **kwargs):
        return self.fn(x, **kwargs) + x
 
class PreNorm(nn.Module):
    def __init__(self, dim, fn):
        super().__init__()
        self.norm = nn.LayerNorm(dim)
        self.fn = fn
 
    def forward(self, x, **kwargs):
        return self.fn(self.norm(x), **kwargs)
 
class SwinBlock(nn.Module):
    def __init__(self, dim, heads, head_dim, mlp_dim, shifted, window_size, 
                 relative_pos_embedding):
        super().__init__()
        self.attention_block = Residual(PreNorm(dim, WindowAttention(dim=dim, 
                                        heads=heads, head_dim=head_dim,
                                        shifted=shifted, window_size=window_size,
                                        relative_pos_embedding=relative_pos_embedding)))
        self.mlp_block = Residual(PreNorm(dim, FeedForward(dim=dim, hidden_dim=mlp_dim)))
 
    def forward(self, x):
        x = self.attention_block(x)
        x = self.mlp_block(x)
        return x

5.W-MSA

W-MSA,顾名思义,就是按窗口的尺寸进行自注意力计算,与SW-MSA不同的是,它不会进行窗口移位,源码如下。我们这里先忽略shiftedTrue的情况。

class WindowAttention(nn.Module):
    def __init__(self, dim, heads, head_dim, shifted, window_size, 
                 relative_pos_embedding):
        super().__init__()
        inner_dim = head_dim * heads
        self.heads = heads
        self.scale = head_dim ** -0.5
        self.window_size = window_size
        self.relative_pos_embedding = relative_pos_embedding # (13, 13)
        self.shifted = shifted
 
        if self.shifted:
            displacement = window_size // 2
            self.cyclic_shift = CyclicShift(-displacement)
            self.cyclic_back_shift = CyclicShift(displacement)
            self.upper_lower_mask = nn.Parameter(create_mask(window_size=window_size,
                                                 displacement=displacement, 
                                                 upper_lower=True, left_right=False),
                                                 requires_grad=False) # (49, 49)
            self.left_right_mask = nn.Parameter(create_mask(window_size=window_size,
                                                 displacement=displacement,
                                                 pper_lower=False, left_right=True),
                                                 requires_grad=False) # (49, 49)
 
        self.to_qkv = nn.Linear(dim, inner_dim * 3, bias=False)
        if self.relative_pos_embedding:
            self.relative_indices = get_relative_distances(window_size) + window_size - 1
            self.pos_embedding = nn.Parameter(torch.randn(2 * window_size - 1, 
                                              2 * window_size - 1))
        else:
            self.pos_embedding = nn.Parameter(torch.randn(window_size ** 2, 
                                              window_size ** 2))
 
        self.to_out = nn.Linear(inner_dim, dim)
 
    def forward(self, x):
        if self.shifted:
            x = self.cyclic_shift(x)
 
        b, n_h, n_w, _, h = *x.shape, self.heads # [1, 56, 56, _, 3]
        qkv = self.to_qkv(x).chunk(3, dim=-1) # [(1,56,56,96), (1,56,56,96), (1,56,56,96)]
        nw_h = n_h // self.window_size # 8
        nw_w = n_w // self.window_size # 8
        # 分成 h/M * w/M 窗口
        q, k, v = map(lambda t: rearrange(t,  'b (nw_h w_h) (nw_w w_w) (h d) -> 
                      b h (nw_h nw_w) (w_h w_w) d', h=h, w_h=self.window_size, 
                      w_w=self.window_size), qkv)
        # q、 k、 v : (1, 3, 64, 49, 32)
        # 按窗口的尺寸逐个进行自注意力计算
        dots = einsum('b h w i d, b h w j d -> b h w i j', q, k) * self.scale # (1,3,64,49,49)
 
        if self.relative_pos_embedding:
            dots += self.pos_embedding[self.relative_indices[:, :, 0], 
                                       self.relative_indices[:, :, 1]]
        else:
            dots += self.pos_embedding
 
        if self.shifted:
            dots[:, :, -nw_w:] += self.upper_lower_mask
            dots[:, :, nw_w - 1::nw_w] += self.left_right_mask
        attn = dots.softmax(dim=-1) # (1,3,64,49,49)
        out = einsum('b h w i j, b h w j d -> b h w i d', attn, v)
        out = rearrange(out, 'b h (nw_h nw_w) (w_h w_w) d -> b (nw_h w_h) (nw_w w_w) (h d)',
                        h=h, w_h=self.window_size, w_w=self.window_size, nw_h=nw_h,
                        nw_w=nw_w) # (1, 56, 56, 96), 窗口合并
        out = self.to_out(out)
        if self.shifted:
            out = self.cyclic_back_shift(out)
        return out

forward()函数中首先计算的是Transformer中介绍的QKV这3个特征向量,所以to_qkv()函数进行的是线性变换。这里使用了一个实现小技巧,即只使用了一个隐层节点数为inner_dim*3的线性变换,然后使用chunk(3)操作将这3个特征向量切开,因此qkv是一个长度为3的张量,每个张量的维度是(56,56,96)。

之后的map( )函数是实现W-MSA中的W最核心的代码,该函数是通过einopsrearrange实现的。einops是一个可读性非常高的实现常见矩阵操作的Python包,它可以实现矩阵转置、矩阵复制、矩阵重塑等操作。最终通过rearrange得到了3个独立窗口的权值矩阵,它们的维度均是(3,64,49,32),这4个值的意思如下。

3:多头自注意力的头的个数。

64:窗口的个数,首先通过块合并将图像的尺寸降到,因为窗口的大小为7,所以总共剩下个窗口。

49:窗口的像素的个数。

32:隐层节点的个数。

Swin Transformer让计算区域以窗口为单位的策略极大地减小了网络的计算量,将复杂度降低到了图像尺寸的线性比例。传统的MSA和W-MSA的复杂度如式(1.46)所示。

  (1.46)

式(1.46)的计算省略了softmax占用的计算量,这里以Ω(MSA)为例,它的具体构成如下。

代码中的to_qkv()函数,用于生成QKV这3个特征向量:,其中x是输入数据,W是用于计算3个特征向量的权值向量。假设权值矩阵的特征数是Cx的维度是(hw,C ),W的维度是(C,C ),那么这3项的复杂度是3hwC2

计算QKTQKV的维度均是(hw,C ),因此它的复杂度是(hw)2C

softmax之后乘V得到Z(即源码中的out):因为QKT的维度是(hw,hw),所以它的时间复杂度是(hw)2C

Z乘矩阵WZ得到最终输出,即代码中的to_out()函数的结果,它的时间复杂度是hwC 2

通过Transformer的计算式(1.47),我们可以有更直观的理解:在Transformer部分中我们介绍自注意力是通过点乘的方式得到查询向量和键向量的相似度的,即式(1.47)中的QKT。然后通过这个相似度匹配值向量。因此这个相似度是通过逐个元素进行点乘计算得到的。如果比较的范围是一幅图像,那么计算的瓶颈就在于整幅图像的逐像素比较,因此复杂度是(hw)2。而W-MSA是在窗口内进行逐像素比较的,因此复杂度是M2hw,其中M是W-MSA的窗口的大小。式(1.47)中dk是特征向量QKV的长度。

  (1.47)

回到代码,接下来的dots变量便是我们刚刚介绍的QKT。关于加入相对位置编码,我们放到最后介绍。attneinsum完成了式(1.47)的整个流程,然后再次使用rearrange将维度调整回(56,56,96),最后通过to_out()将维度调整为超参数设置的输出维度的值。

这里我们介绍一下W-MSA的相对位置编码B。首先这个相对位置编码是加在乘归一化尺度之后的dots变量上的,因此Z的计算方式如式(1.48)所示。因为W-MSA是以窗口为单位进行特征匹配的,所以相对位置编码的范围也应该以窗口为单位,它的具体实现见如下代码。相对位置编码的具体思想参考UniLMv2[34]

[34] 参见Hangbo Bao、Li Dong、Furu Wei等人的论文“UniLMv2: Pseudo-Masked Language Models for Unified Language Model Pre-Training”。

  (1.48)

def get_relative_distances(window_size):
    indices = torch.tensor(np.array([[x, y] for x in range(window_size) 
                           for y in range(window_size)]))
    distances = indices[None, :, :] - indices[:, None, :]
    return distances

单独使用W-MSA得到的网络的建模能力是非常差的,因为它将每个窗口当作一个独立区域进行处理而忽略了窗口之间交互的必要性。为了解决这个问题,Swin Transformer提出了SW-MSA。

6.SW-MSA

SW-MSA接在W-MSA之后,因此只要我们提供一种和W-MSA不同的窗口切分方式便可以实现跨窗口的通信。SW-MSA的窗口切分方式如图1.59所示。我们之前说过,输入阶段1的图像的尺寸是(见图1.59(a)),W-MSA的窗口切分的结果如图1.59(b)所示。那么我们如何得到和W-MSA不同的切分方式呢?SW-MSA的思想很简单,将图像循环上移和循环左移半个窗口的大小,那么图1.59(c)中的蓝色和红色区域将分别被移动到图像的下侧和右侧,如图1.59(d)所示。如果在移位的基础上再按照W-MSA切分窗口,就会得到和W-MSA不同的窗口切分方式。图1.59(d)中红色框和蓝色框分别是W-MSA和SW-MSA的切分窗口的结果。这一部分可以通过PyTorch的roll()函数实现,源码中是CyclicShift()函数:

class CyclicShift(nn.Module):
    def __init__(self, displacement):
        super().__init__()
        self.displacement = displacement
 
    def forward(self, x):
        return torch.roll(x, shifts=(self.displacement, self.displacement), dims=(1, 2))

其中,displacement的值是窗口宽度除以2。

图1.59 SW-MSA的窗口切分方式

这种窗口切分方式引入了一个新的问题,即在移位图像的最后一行和最后一列各引入了一块移位过来的区域,如图1.59(d)所示。因为位移图像的最右边是由原始图像的左右两个边拼接而成,位移图像的最下边由原始图像的上下两个边拼接而成。因为图像的两个边不具备明显的语义相关性,所以计算位移图像的右边和下边是没有意义的,即只需要对比图1.59(d)所示的一个窗口中相同颜色的区域。我们以图1.59(d)左下角的区域(1)和右上角的区域(2)为例来说明SW-MSA是怎么解决这个问题的。

区域(1)移位行的计算方式如图1.60所示。首先一个大小的窗口通过线性运算得到QKV这3个特征向量的权值,如我们介绍的,它的维度是(49,32)。在这49行中,前28行是按照滑窗的方式遍历区域(1)的上半部分得到的,后21行则是遍历区域(1)的下半部分得到的,此时它们对应的位置关系依旧保持上黄下蓝。

图1.60 SW-MSA的区域(1)移位行的计算方式

接着便计算QKT,根据分块矩阵的矩阵乘法,我们知道在图中相同颜色区域相互计算后会保持颜色不变,而黄色和蓝色区域计算后会变成绿色,绿色部分表示相似度无意义。在论文中使用了upper_lower_mask将其替换为掩码,upper_lower_mask是由0和无穷大(Inf)组成的二值矩阵,最后通过单位加得到最终的dots变量。

upper_lower_mask的计算方式如下。

mask = torch.zeros(window_size ** 2, window_size ** 2)
mask[-displacement*window_size:, :-displacement*window_size] = float('-inf')
mask[:-displacement*window_size, -displacement*window_size:] = float('-inf')

区域(2)移位行的计算方式和区域(1)的类似,不同的是区域(2)是图像循环左移之后的结果,如图1.61所示。因为区域(2)是左右排列的,所以它得到的QKV是条纹状的,即先逐行遍历。在这7行中,都会先遍历4个黄色区域,再遍历3个红色区域。两个条纹状的矩阵相乘后,得到的相似度矩阵是网络状的,其中橙色区域表示无效区域,因此需要网格状的掩码left_right_mask来进行覆盖。

left_right_mask的生成方式如下面代码所示。

mask = torch.zeros(window_size ** 2, window_size ** 2)
mask = rearrange(mask, '(h1 w1) (h2 w2) -> h1 w1 h2 w2', h1=window_size, h2=window_size)
mask[:, -displacement:, :, :-displacement] = float('-inf')
mask[:, :-displacement, :, -displacement:] = float('-inf')
mask = rearrange(mask, 'h1 w1 h2 w2 -> (h1 w1) (h2 w2)')

图1.61 SW-MSA的区域(2)移位行的计算方式

关于upper_lower_maskleft_right_mask这两个掩码的值,读者可以自己代入一些值来验证,可以设置window_size的值,然后将displacement的值设为window_size的一半即可。

窗口移位和掩码的计算是在WindowAttention类的第一个if中实现的,掩码的相加是在第二个if中实现的,最后一个if则将图像复原。

截至目前,我们对Swin-T的阶段1进行了完整的梳理,后面3个阶段除了几个超参数和图像的尺寸与阶段1不同,其他的结构均保持一致,这里不赘述。

7.输出层

最后我们介绍一下Swin Transformer的输出层。在阶段4完成计算后,特征的维度是(768,7,7)。Swin Transformer先通过全局平均池化得到长度为768的特征向量,再通过LN和全连接得到最终的预测结果,如式(1.49)所示。

  (1.49)

1.9.2 Swin Transformer家族

Swin Transformer共提出了4个不同尺寸的模型,它们的区别在于隐层节点的长度、每个阶段的网络层数、多头自注意力机制的头的个数,具体值见下面的代码。

def swin_t(hidden_dim=96, layers=(2, 2, 6, 2), heads=(3,6,12, 24), **kwargs):
    return SwinTransformer(hidden_dim=hidden_dim, layers=layers, heads=heads, **kwargs)
 
def swin_s(hidden_dim=96, layers=(2, 2, 18, 2), heads=(3,6,12,24), **kwargs):
    return SwinTransformer(hidden_dim=hidden_dim, layers=layers, heads=heads, **kwargs)
 
def swin_b(hidden_dim=128, layers=(2, 2, 18, 2), heads=(4,8,16,32), **kwargs):
    return SwinTransformer(hidden_dim=hidden_dim, layers=layers, heads=heads, **kwargs)
 
def swin_l(hidden_dim=192, layers=(2, 2, 18, 2), heads=(6,12,24,48), **kwargs):
    return SwinTransformer(hidden_dim=hidden_dim, layers=layers, heads=heads, **kwargs)

因为Swin Transformer是一个多阶段的网络结构,而且每一个阶段的输出都是一组特征图,所以我们可以非常方便地将其迁移到几乎所有CV任务中。作者的实验结果也表明,Swin Transformer在检测和分割领域达到了先进的CNN分类模型的水平。

1.9.3 小结

Swin Transformer是近年来为数不多的让人兴奋的算法,它让人兴奋的原因有3个。

解决了长期困扰业界的将Transformer应用到CV领域时出现的速度慢的问题。

设计非常巧妙,具有新颖又紧扣CNN的优点,充分考虑CNN的位移不变性、尺寸不变性、感受野与层次的关系、分阶段降低分辨率以增加通道数等特点。没了这些特点,Swin Transformer是无法被称为一个骨干网络的;

在诸多CV领域有先进的表现。

当然,我们对Swin Transformer还是要站在一个客观的角度来评价的。虽然论文中说Swin Transformer是一个骨干网络,但是这样评价还为时尚早,原因如下。

Swin Transformer并没有提供一个像反卷积那样的上采样的算法,因此对于这个问题,并不能直接使用Swin Transformer替换骨干网络,也许可以采用双线性插值来实现,但效果如何还需要评估。

从1.9.1节中我们可以看出W-MSA每个窗口都有一组独立的QKT,因此Swin Transformer并不具有CNN一个特别重要的特性:权值共享。这也造成了Swin Transformer在速度上和同级别的CNN仍有不小的差距。所以就目前来看,在嵌入式平台上CNN还有着不可撼动的地位。

1.10 Vision Transformer之CSWin Transformer

在本节中,先验知识包括:

Swin Transformer(1.9节);

Transformer(4.3节);

Xception(2.3节);

残差网络(1.4节)。

感受野是影响CV模型效果至关重要的属性之一,因为模型是无法对它感知不到的区域建模的。在DeepLab系列算法中,空洞卷积在不增加参数数量的同时可以快速增加感受野。之前介绍的Swin Transformer仅仅通过移动窗口来增加感受野的方式仍然过于缓慢,因为这个算法需要通过大量堆叠网络块的方式来增加感受野。本节要介绍的CSWin(cross-shape window)Transformer[35]是Swin Transformer的改进版,它提出了通过十字形的窗口来实现自注意力机制,不仅计算效率非常高,而且能够通过两层计算获得全局的感受野。CSWin Transformer还提出了新的编码方式——局部加强位置编码,进一步提高了模型的准确率。

[35] 参见Xiaoyi Dong、Jianmin Bao、Dongdong Chen等人的论文“CSWin Transformer: A General Vision Transformer Backbone with Cross-Shaped Windows”。

1.10.1 CSWin Transformer概述

CSWin Transformer的网络结构如图1.62所示。它的输入是一幅3通道彩色图像,尺寸为,图像首先经过一组步长为4的卷积,得到特征图的尺寸为。这一点相比之前的直接无重叠的拆分是要有所提升的。之后CSWin Transformer分成4个阶段,每个阶段之间通过步长为2的卷积来降采样,这一点就和VGG等CNN结构很像了。

图1.62 CSWin Transformer的网络结构

1.10.2 十字形窗口自注意力机制

本节的核心是十字形窗口自注意力(cross-shaped window self-attention)机制,它由并行的横向自注意力和纵向自注意力组成。对于一个多头的自注意力模型,CSWin Transformer块将头的一半分给横向自注意力,另一半分给纵向自注意力,然后将这两个特征拼接起来,如图1.63所示。假设网络有K个头,其中1, …, K/2用于横向自注意力的计算,K/2 + 1, …, K用于纵向自注意力的计算。

图1.63 十字形窗口自注意力模型

具体地讲,我们假设模型的输入特征图是X,为了计算它在横向上的自注意力,首先将它拆分成个横条的数据。其中sw是横条的宽度,在这4个不同的阶段中取不同的值,实验结果表明[1,2,7,7]这组值在速度和精度上取得了比较好的均衡。

对于每个条状特征,使用Transformer可以得到它的特征Y i,最后将这M个特征拼接到一起便得到了这个头的输入。我们假设它属于第k个头,那么横向自注意力H-Attentionk(X )的计算方式如式(1.50)所示。

  (1.50)

其中,QKV这3个向量的映射矩阵。,它的作用是保证经过十字形窗口自注意力模型之后特征图的通道数保持不变。

纵向自注意力和横向自注意力的计算方式类似,不同的是纵向自注意力取的是宽度为sw的竖条,表示为式(1.51)。

  (1.51)

最终,这个网络块的输出表示为式(1.52):

  (1.52)

其中,O表示输出,用来调整特征图的通道数,并可以将两个不同方向的自注意力特征进行融合。

十字形窗口自注意力的一个非常重要的属性是它只需要两层就可以得到全局感受野,对于图像中的一点pi, j,它的当前层的感受野是它同行和同列的像素点:。对于pi, :上任意一点,它的感受野为,所以仅需要两层就可以将感受野扩充到全图。

1.10.3 局部加强位置编码

因为Transformer与输入顺序无关,所以需要向其中加入位置编码。在Transformer的论文中提出的绝对位置编码(absolute position encoding,APE)和条件位置编码(conditional position encoding,CPE)[36]的位置编码是直接加到输入数据X之上的,如图1.64(a)所示。相对位置编码(relative position encoding,RPE)[37]将位置编码加入自注意力内部,即直接加入softmax,如图1.64(b)所示。

[36] 参见Xiangxiang Chu、Zhi Tian、Bo Zhang等人的论文“Conditional Positional Encodings for Vision Transformers”。

[37] 参见Peter Shaw、Jakob Uszkoreit、Ashish Vaswani的论文“Self-Attention with Relative Position Representations”。

图1.64 Transformer常见的编码方式

本节提出的局部加强位置编码(Local enhanced Position Encoding,LePE)直接将位置编码添加到了值向量上,该添加操作是通过将位置编码EV相乘完成的。然后通过一个捷径将添加了位置编码的V和通过自注意力加权的V单位加到一起,如图1.64(c)所示,它的计算方式如式(1.53)所示。

  (1.53)

位置编码E是一个深度卷积,深度卷积在位置编码中的作用是捕获当前位置的像素和它周围邻居之间的位置关系,如式(1.54)所示。从另一个角度看,CSWin Transformer块是一个由十字形窗口自注意力和CNN组成的多分支的结构。

  (1.54)

1.10.4 CSWin Transformer块

CSWin Transformer块的网络结构如图1.65所示,它最显著的特点是添加了两个捷径,并使用LN对特征进行归一化,计算方式如式(1.55)所示。

  (1.55)

图1.65 CSWin Transformer块的网络结构

1.10.5 CSWin Transformer的复杂度

最后我们讨论一下CSWin Transformer的复杂度,它的计算方式如下。

对于横向自注意力:,其中WQKV共3个特征向量,XM组,自注意力机制共有K/2个头,因此这一部分的复杂度如式(1.56)所示。

  (1.56)

,因此QKT的复杂度如式(1.57)所示。

  (1.57)

,它们的积的复杂度如式(1.58)所示。

  (1.58)

同理对于纵向自注意力,前面3项的复杂度依次为

最后一项是拼接头之后的值乘W O,这一部分的复杂度是。综上,CSWin-Attention的复杂度为前面7项之和,最终结果如式(1.59)所示。

  (1.59)

从式(1.59)中可以看出,在比较浅的层中,HW的值比较大,出于速度方面的考虑,这时候建议使用比较小的sw;随着层数的增加,HW可能成比例缩小,这时候就可以使用感受野更大的sw了。

1.10.6 小结

CSWin Transformer披着Transformer的“外衣”,但的确是Transformer和卷积的混合算法。在模型的最开始便是一个有重叠的卷积,接着每个阶段可以看作由十字形窗口自注意力和深度卷积组成的双分支结构,在每个阶段中又添加了CNN最为经典的残差结构,而每个阶段之间又使用步长为2的卷积进行降采样。至于CSWin Transformer最大的创新点——十字形窗口,其实在CNN领域,2019年出现的CCNet[38]就提出过的类似的思想。

[38] 参见Zilong Huang、Xinggang Wang、Lichao Huang等人的论文“CCNet: Criss-Cross Attention for Semantic Segmentation”。

1.11 MLP? :MLP-Mixer

在本节中,先验知识包括:

LN(6.3节);

MobileNet(2.2节)。

这里介绍一个争议非常大的号称全部由MLP组成的图像分类模型:MLP-Mixer[39]。MLP-Mixer诞生后,很多微信公众号文章中宣称“CNN的时代”已经过去了,那么MLP-Mixer真的有这么神奇吗?下面我们来一步步揭开它的“神秘面纱”。

[39] 参见Ilya Tolstikhin、Neil Houlsby、Alexander Kolesnikov等人的论文“MLP-Mixer: An all-MLP Architecture for Vision”。

1.11.1 网络结构

MLP-Mixer的网络结构如图1.66所示,它由3个核心模块组成:

在每个块上的全连接;

通道混合的全连接;

像素点混合的全连接。

它的实现如下:

class MlpMixer(nn.Module): 
  num_classes: int
  num_blocks: int 
  patch_size: int 
  hidden_dim: int 
  tokens_mlp_dim: int 
  channels_mlp_dim: int 
  @nn.compact
    def __call__(self, x):
    s = self.patch_size
    x = nn.Conv(self.hidden_dim , (s,s), strides=(s,s), name='stem')(x)
    x = einops.rearrange(x, 'n h w c -> n (h w) c') 
  for _ in range(self.num_blocks):
        x = MixerBlock(self.tokens_mlp_dim , self.channels_mlp_dim)(x)
  x = nn.LayerNorm(name='pre_head_layer_norm')(x)
    x = jnp.mean(x, axis=1)
    return nn.Dense(self.num_classes , name='head', kernel_init=nn.initializers.zeros)(x)

图1.66 MLP-Mixer的网络结构

1.在每个块上的全连接

如图1.66所示,MLP-Mixer首先将图像按照滑窗的方式转换成一个长度为S的图像块序列,假设每个图像块的大小为P,序列的长度,然后在这个序列上使用一个共享的全连接,将其编码为长度为C的特征向量

这时大家应该已经看出这一部分实际上就是一个步长为P、卷积核的大小也是P的CNN,这里也是使用卷积实现这个操作的。

2.混合层

MLP-Mixer的骨干网络是由N个混合层(mixer layer)组成的,每个混合层的网络结构如图1.67所示。混合层的核心结构是图1.67中的两个MLP,其中MLP1用于标志混合(token-mixing)全连接块(红框内),MLP2用于通道混合(channel-mixing)全连接块(蓝框内),它们的实现如下:

class MixerBlock(nn.Module): 
  tokens_mlp_dim: int 
  channels_mlp_dim: int 
  @nn.compact
    def __call__(self, x):
        y = nn.LayerNorm()(x) 
    y = jnp.swapaxes(y, 1, 2) 
    y = MlpBlock(self.tokens_mlp_dim , name='token_mixing')(y)
    y = jnp.swapaxes(y, 1, 2)
        x = x+y
        y = nn.LayerNorm()(x)
return x+MlpBlock(self.channels_mlp_dim , name='channel_mixing')(y)

图1.67 混合层的网络结构

在标志混合中,混合层先对输入的X使用LN进行归一化处理,然后对其进行转置,将输入数据的格式由通道为宽、图像块为高的矩阵变成图像块为宽、通道为高的矩阵。接着在每个通道上使用一个权值共享的MLP进行标志之间的特征加工,再使用转置将矩阵还原,最后使用一个残差结构将处理前后的两个特征进行拼接。由于这一部分实现的是同一通道不同标志之间的混合,因此叫作标志混合。

可以看出,图1.67中的MLP1其实就是MobileNet[40]中介绍的深度卷积(depthwise convolution),其中卷积核和步长的大小均是P。这里的LN则相当于对整个特征图进行了一次白化(whitening)。图像白化是传统的计算机视觉中最常用的归一化手段,处理的方式就是将图像的像素平均值变为0,方差变为1。

[40] 参见Andrew G.Howard、Menglong Zhu、Bo Chen等人的论文“MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications”。

在通道混合中,混合层也先使用LN对特征进行归一化,然后直接使用一个共享的MLP对特征图的通道特征进行混合计算。这里的MLP2其实就是卷积。通道混合中还使用了残差结构进行特征的拼接。

在混合层中每个MLP是由两个全连接和GELU激活函数[41]组成的。因此混合层本质上还是一个由连续两个深度卷积和连续两个点卷积组成的深度可分离卷积。

[41] 参见Dan Hendrycks、Kevin Gimpel的论文“Gaussian Error Linear Units (GELUs)”。

class MlpBlock(nn.Module): 
  mlp_dim: int
    @nn.compact
    def __call__(self, x):
        y = nn.Dense(self.mlp_dim)(x)
        y = nn.gelu(y)
        return nn.Dense(x.shape[-1])(y)

3.输出层

MLP-Mixer的输出层使用的是CNN中最常使用的输出结构:一个全局平均池化和一个全连接。

1.11.2 讨论

1.结构

MLP-Mixer从本质上来说就是一个特殊形式的CNN,无论是在每个块上的全连接,还是混合层中的标志混合和通道混合,都是一种特殊的卷积形式。虽然说MLP-Mixer是一个全部由MLP组成的模型,但是最终还是没有脱离CNN的范畴,更别说“MLP is all you need”(你只需要一个MLP结构就够了)这种耸人听闻的报道了。这么看来,LeCun说MLP-Mixer是一个“挂羊头,卖狗肉”的算法也就不奇怪了。

MLP-Mixer不仅没有继承CNN的优点,还失去了CNN的灵活性,例如全卷积对输入图像尺寸的自由度等。MLP-Mixer的最大贡献可能是给出了CNN的一种全连接实现。

2.效果

MLP-Mixer虽然在ImageNet上取得了不错的分类效果,但是对比主流的CNN或者基于Transformer的方法仍有一些差距,且它的效果还依赖于JFT-300M数据集作为预训练数据集。当我们发现MLP-Mixer就是一种特殊的CNN之后,对它的效果也就不意外了。

所以各位CV领域的同行们完全不必惊慌,也没必要被一些微信公众号影响,继续放心地研究CNN吧!

读者服务:

微信扫码关注【异步社区】微信公众号,回复“e59631”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。

相关图书

ChatGPT原理与应用开发
ChatGPT原理与应用开发
深度学习的数学——使用Python语言
深度学习的数学——使用Python语言
深度学习:从基础到实践(上、下册)
深度学习:从基础到实践(上、下册)
动手学深度学习(PyTorch版)
动手学深度学习(PyTorch版)
深度学习与医学图像处理
深度学习与医学图像处理
深度强化学习实战:用OpenAI Gym构建智能体
深度强化学习实战:用OpenAI Gym构建智能体

相关文章

相关课程