卷积神经网络-LeNet5、AlexNet、VGGNet、GoogleNet、ResNet原理及tensorflow2实现

卷积神经网络

1. 预备知识

1.1 神经网络中为什么要标准化

  • 原因在于神经网络学习过程本质就是为了学习数据分布,一旦训练数据与测试数据的分布不同,那么网络的泛化能力也大大降低;另外一方面,一旦每批训练数据的分布各不相同(batch 梯度下降),那么网络就要在每次迭代都去学习适应不同的分布,这样将会大大降低网络的训练速度,这也正是为什么我们需要对数据都要做一个归一化预处理的原因。
  • 对于深度网络的训练是一个复杂的过程,只要网络的前面几层发生微小的改变,那么后面几层就会被累积放大下去。一旦网络某一层的输入数据的分布发生改变,那么这一层网络就需要去适应学习这个新的数据分布,所以如果训练过程中,训练数据的分布一直在发生变化,那么将会影响网络的训练速度。

1.2 什么是 LRN

  • Local Response Normalization的简称是LRN,即局部响应归一化层。说到为什么要使用LRN就不得不提到神经生物学中的一个概念叫做 lateral inhibition(横向抑制),简单来讲就是兴奋的神经细胞抑制周围神经细胞的能力。应用到深度神经网络中,这种横向抑制的目的是进行局部对比度增强,以便将局部特征在下一层得到表达。LRN是一个非训练层,即该层中不存在可训练的参数。假设某一个CNN层的输出为一个 WxHxC 的张量,要对该张量做LRN我们应该怎么做呢。假设我们取n的大小为3,现在我们要计算(x, y, i)位置上 LRN 后的值,我们便可以以 (x,y, i)点位中心,在channel维度范围内取一个1xn大小的网格,此时我们便可以通过(x, y, i)位置的数值和其周围的数值对(x, y, i)位置进行正则化。

1.3 BN

  • BN的基本思想其实相当直观:因为深层神经网络在做非线性变换前的激活输入值(就是那个x=WU+B,U是输入)随着网络深度加深或者在训练过程中,其分布逐渐发生偏移或者变动,之所以训练收敛慢,一般是整体分布逐渐往非线性函数的取值区间的上下限两端靠近(对于Sigmoid函数来说,意味着激活输入值WU+B是大的负值或正值),所以这导致反向传播时低层神经网络的梯度消失,这是训练深层神经网络收敛越来越慢的本质原因,而BN就是通过一定的规范化手段,把每层神经网络任意神经元这个输入值的分布强行拉回到均值为0方差为1的标准正态分布,其实就是把越来越偏的分布强制拉回比较标准的分布,这样使得激活输入值落在非线性函数对输入比较敏感的区域,这样输入的小变化就会导致损失函数较大的变化,意思是这样让梯度变大,避免梯度消失问题产生,而且梯度变大意味着学习收敛速度快,能大大加快训练速度。

1.4 1*1卷积的作用

  • 降维,降低了计算复杂度。当某个卷积层输入的特征数较多,对这个输入进行卷积运算将产生巨大的计算量;如果对输入先进行降维,减少特征数后再做卷积计算量就会显著减少。比如,一张500 * 500* 100 的图片,在20个filter上做1 * 1的卷积,那么结果的大小为500* 500* 20。

  • 加入非线性,提升网络的表达能力。比如先进行一次普通的卷积(比如3x3),紧跟再进行一次1x1的卷积,对于某个像素点来说1x1卷积等效于该像素点在所有特征上进行一次全连接的计算,无论是第一个3x3卷积还是新增的1x1卷积,后面都紧跟着激活函数(比如relu)。将两个卷积串联,就能组合出更多的非线性特征。

2. 经典卷积神经网络原理

2.1 LeNet5

  • LeNet5 诞生于 1994 年,是最早的卷积神经网络之一,并且推动了深度学习领域的发展。LeNet5 的架构基于这样的观点:图像的特征分布在整张图像上,以及带有可学习参数的卷积是一种用少量参数在多个位置上提取相似特征的有效方式。在那时候,没有GPU帮助训练,甚至CPU的速度也很慢。因此,能够保存参数以及计算过程是一个关键进展,这和将每个像素用作一个大型多层神经网络的单独输入相反。LeNet5 阐述了哪些像素不应该被使用在第一层,因为图像具有很强的空间相关性,而使用图像中独立的像素作为不同的输入特征则利用不到这些相关性。LeNet5网络结构如下:
    LeNet5网络结构
  • LeNet5网络结构,主要有8层组成:输入层、C1层、S2层、C3层、S4层、C5层、F6层、输出层。
    • 输入层(input):这是一个输入32* 32的单通道的图像。
    • C1层:该层为卷积层。输入:3232,卷积核:6@55,输出:6@28*28,步长为1, padding为VALID。
    • S2层:该层为下采样层,也叫池化层,卷积核:6@22,输出:6@1414。
    • C3层:该层为卷积层,卷积核:6@55,输出:16@1010,步长为1, padding为VALID。
    • S4层:该层为池化层,卷积核:16@22,输出:16@55。
    • C5层:该层为卷积层,卷积核:120@55,输出:120@11,步长为1, padding为VALID。
    • F6层: 该层为全连接层,采用Sigmoid 函数进行压缩,输出84个神经元。
    • 输出层:该层由径向基函数(RBF)输出10个神经元。
  • LeNet5 网络能够总结为如下几点:
    • 使用卷积。参数较少(局部连接和共享权重),在卷积运算中,能够使原信息的特征增强,并且降低噪声。
    • 下采样(subsample)。将每个相邻区域的四个像素求和变为一个像素,残生一个缩小1/4的特征映射图。
    • 多层神经网络(MLP)作为最后的分类器,层与层之间的稀疏连接矩阵避免大的计算成本。

2.2 AlexNet

  • 2012年,Alex Krizhevsky发表了Alexet,它是LeNet的一种更深更宽的版本,并以显著优势赢得了困难的 ImageNet 竞赛。AlexNet将LeNet的思想扩展到了更大的能学习到远远更复杂的对象与对象层次的神经网络上。

  • AlexNet的特点,首次在CNN中应用ReLU、Dropout和LRN等Trick,同时使用GPU进行运算加速。

    • (1)成功使用ReLU作为CNN的激活函数,并验证其效果在较深的网络超过了Sigmoid,成功解决了Sigmoid在网络较深时的梯度弥散问题。基于ReLU的深度卷积网络比基于tanh和sigmoid的网络训练快数倍。使用ReLU后,值域大于等于0,值域是无界的,而tanh、sigmoid函数值域区间[0,1],值域是有界的。所以一般在ReLU之后会做一个normalization。虽然ReLU激活函数在很久之前就被提出了,但是直到AlexNet的出现才将其发扬光大。
    • (2)使用Dropout。相对于一般如线性模型使用正则的方法来防止模型过拟合,而在神经网络中Dropout通过修改神经网络本身结构来实现。Dropout虽有单独的论文论述,但是AlexNet将其实用化,通过实践证实了它的效果。在AlexNet中主要是最后几个全连接层使用了Dropout。
    • (3)使用覆盖最大池化。此前CNN中普遍使用平均池化,AlexNet全部使用最大池化,避免平均池化的模糊化效果。并且AlexNet中提出让步长比池化核的尺寸小,这样池化层的输出之间会有重叠和覆盖,提升了特征的丰富性。
    • (4)提出了LRN层,对局部神经元的活动创建竞争机制,使得其中响应比较大的值变得相对更大,并抑制其他反馈较小的神经元,增强了模型的泛化能力。
    • (5)使用CUDA加速深度卷积网络的训练,利用GPU强大的并行计算能力,处理神经网络训练时大量的矩阵运算。AlexNet使用了两块GTX 580 GPU进行训练,单个GTX 580只有3GB显存,这限制了可训练的网络的最大规模。因此作者将AlexNet分布在两个GPU上,在每个GPU的显存中储存一半的神经元的参数。因为GPU之间通信方便,可以互相访问显存,而不需要通过主机内存,所以同时使用多块GPU也是非常高效的。同时,AlexNet的设计让GPU之间的通信只在网络的某些层进行,控制了通信的性能损耗。
    • (6)数据增强,随机地从256*256的原始图像中截取224*224大小的区域(以及水平翻转的镜像),相当于增加了2*(256-224)^2=2048倍的数据量。如果没有数据增强,仅靠原始的数据量,参数众多的CNN会陷入过拟合中,使用了数据增强后可以大大减轻过拟合,提升泛化能力。预测时,则取图片的四个角加中间共5个位置,并进行左右翻转,一共获得10张图片,对他们进行预测并对10次结果求均值。同时,AlexNet论文中提到了会对图像的RGB数据进行PCA处理,并对主成分做一个标准差为0.1的高斯扰动,增加一些噪声,这个Trick可以让错误率再下降1%。
  • 网络结构


    AlexNet网络结构
AlexNet网络详细结构

2.3 VGG

  • VGG是Oxford的Visual Geometry Group的组提出的(大家应该能看出VGG名字的由来了)。VGG模型是2014年ILSVRC竞赛的第二名,第一名是GoogLeNet。但是VGG模型在多个迁移学习任务中的表现要优于googLeNet。而且,从图像中提取CNN特征,VGG模型是首选算法。它的缺点在于,参数量有140M之多,需要更大的存储空间。它证明了增加网络的深度能够在一定程度上影响网络最终的性能。VGG有两种结构,分别是VGG16和VGG19,两者并没有本质上的区别,只是网络深度不一样。参考文章

  • VGG16包含了16个隐藏层(13个卷积层和3个全连接层),VGG19包含了19个隐藏层(16个卷积层和3个全连接层)。VGG网络的结构非常一致,从头到尾全部使用的是3x3的卷积和2x2的max pooling。

  • VGG16相比AlexNet的一个改进是采用使用了3个3x3卷积核来代替7x7卷积核,使用了2个3x3卷积核来代替5* 5卷积核(AlexNet的9×9或11×11过滤器)。对于给定的感受野(与输出有关的输入图片的局部大小),采用堆积的小卷积核是优于采用大的卷积核,因为多层非线性层可以增加网络深度来保证学习更复杂的模式,而且代价还比较小(参数更少)。

  • VGGNet则清一色使用3x3卷积。因为卷积不仅涉及到计算量,还影响到感受野。前者关系到是否方便部署到移动端、是否能满足实时处理、是否易于训练等,后者关系到参数更新、特征图的大小、特征是否提取的足够多、模型的复杂度和参数量等等。作者认为两个3x3的卷积堆叠获得的感受野大小,相当一个5x5的卷积;而3个3x3卷积的堆叠获取到的感受野相当于一个7x7的卷积。

  • feature map维度的整体变化过程是:先将local信息压缩,并分摊到channel层级,然后无视channel和local,通过fc这个变换再进一步压缩为稠密的feature map,这样对于分类器而言有好处也有坏处,好处是将local信息隐藏于/压缩到feature map中,坏处是信息压缩都是有损失的,相当于local信息被破坏了(分类器没有考虑到,其实对于图像任务而言,单张feature map上的local信息还是有用的)。

  • 关于池化。特征信息从一开始输入的224x224x3被变换到7x7x512,从原本较为local的信息逐渐分摊到不同channel上,随着每次的conv和pool操作打散到channel层级上。不难发现,卷积只增加feature map的通道数,而池化只减少feature map的宽高。如今也有不少做法用大stride卷积去替代池化,未来可能没有池化。

  • 关于全连接。维度在最后一个卷积后达到7x7x512=25088,紧接着压缩到4096维,可能是作者认为这个过程太急,又接一个fc4096作为缓冲,同时两个fc4096后的relu又接dropout0.5去过渡这个过程,因为最后即将给1k-way softmax,所以又接了一个fc1000去降低softmax的学习压力。VGG最后三个全连接层在形式上完全平移AlexNet的最后三层,超参数上只有最后一层fc有变化:bias的初始值,由AlexNet的0变为0.1,该层初始化高斯分布的标准差,由AlexNet的0.01变为0.005。超参数的变化,提出者自己的感性理解指导认为,以贡献bias来降低标准差,相当于标准差和bias间trade-off,或许提出者实验validate发现这个值比之前AlexNet设置的(std=0.01,bias=0)要更好。

  • 关于全连接转卷积。作者在测试阶段把网络中原本的三个全连接层依次变为1个conv7x7,2个conv1x1,也就是三个卷积层。改变之后,整个网络由于没有了全连接层,网络中间的feature map不会固定,所以网络对任意大小的输入都可以处理,因而作者在紧接着的后一句说到: The resulting fully-convolutional net is then applied to the whole (uncropped) image。

  • 1x1卷积核。VGG在最后的三个阶段都用到了1x1卷积核,选用1x1卷积核的最直接原因是在维度上继承全连接,然而作者首先认为1x1卷积可以增加决策函数(decision function,这里的决策函数就是softmax)的非线性能力,非线性是由激活函数ReLU决定的,本身1x1卷积则是线性映射,即将输入的feature map映射到同样维度的feature map。

  • 同样stride下,不同卷积核大小的特征图和卷积参数差别不大;越大的卷积核计算量越大。

  • VGG网络结构如下:


    VGG网络结构
  • VGG16 各层的结构和参数如下:

    • C1-1层:卷积层
      • 输入:224 x 224 x 3;滤波器:64@3 x 3 x 3;输出:224 x 224 x 64
    • C1-2层:卷积层
      • 输入:224 x 224 x 64;滤波器:64@3 x 3 x 3;输出:224 x 224 x 64
    • P1层:池化层
      • 输入:224 x 224 x 64; 滤波器:64@2 x 2;输出:112 x 112 x 64
    • C2-1层:卷积层
      • 输入:112 x 112 x 64; 滤波器:128@3 x 3 x 64;输出:112 x 112 x 128
    • C2-2层:卷积层
      • 输入:112 x 112 x 64; 滤波器:128@3 x 3 x 64; 输出:112 x 112 x 128
    • P2层:池化层
      • 输入:112 x 112 x 128; 滤波器:128@2 x 2; 输出:56 x 56 x 128
    • C3-1层:卷积层
      • 输入:56 x 56 x 128; 滤波器:256@3 x 3 x 128; 输出:56 x 56 x 256
    • C3-2层:卷积层
      • 输入:56 x 56 x 128; 滤波器:256@3 x 3 x 256; 输出:56 x 56 x 256
    • C3-3层:卷积层
      • 输入:56 x 56 x 256; 滤波器:256@3 x 3 x 256; 输出:56 x 56 x 256
    • P3层:池化层
      • 输入:56 x 56 x 256; 滤波器:256@2 x 2; 输出:28 x 28 x 256
    • C4-1层:卷积层
      • 输入:28 x 28 x 256; 滤波器:512@3 x 3 x 256; 输出:28 x 28 x 512
    • C4-2层:卷积层
      • 输入:28 x 28 x 512; 滤波器:512@3 x 3 x 256; 输出:28 x 28 x 512
    • C4-3层:卷积层
      • 输入:28 x 28 x 512; 滤波器:512@3 x 3 x 256; 输出:28 x 28 x 512
    • P4层:池化层
      • 输入:28 x 28 x 512; 滤波器:512@2 x 2; 输出:14 x 14 x 512
    • C5-1层:卷积层
      • 输入:14 x 14 x 512; 滤波器:512@3 x 3 x 512; 输出:14 x 14 x 512
    • C5-2层:卷积层
      • 输入:14 x 14 x 512; 滤波器:512@3 x 3 x 512; 输出:14 x 14 x 512
    • C5-3层:卷积层
      • 输入:14 x 14 x 512; 滤波器:512@3 x 3 x 512; 输出:14 x 14 x 512
    • P5层:池化层
      • 输入:14 x 14 x 512; 滤波器:512@2 x 2; 输出:7 x 7 x 512
    • F6层:全连接层,高斯分布初始化(std=0.005),bias常数初始化(0.1)
      • 输入:25088;输出:4096
    • F7层是个全连接层,高斯分布初始化(std=0.005),bias常数初始化(0.1)
      • 输入:4096;输出:4096
    • F8层:全连接层,高斯分布初始化(std=0.005),bias常数初始化(0.1)
      • 输入:4096;输出:1000
    • 输出层:
      • soft_max
  • VGG优点

    • 小卷积核。将卷积核全部替换为3x3(极少用了1x1)。几个小滤波器(3x3)卷积层的组合比一个大滤波器(5x5或7x7)卷积层好。
    • 小池化核。相比AlexNet的3x3的池化核,VGG全部为2x2的池化核;
    • 层数更深,特征图更宽。由于卷积核专注于扩大通道数、池化专注于缩小宽和高,使得模型架构上更深更宽的同时,计算量的增加放缓;
    • 全连接转卷积。网络测试阶段将训练阶段的三个全连接替换为三个卷积,测试重用训练时的参数,使得测试得到的全卷积网络因为没有全连接的限制,因而可以接收任意宽或高为的输入。
  • VGG缺点

    • VGG耗费更多计算资源,其中绝大多数的参数都是来自于第一个全连接层,VGG有3个全连接层,使用了更多的参数导致更多的内存占用。

2.4 GoogLeNet

  • GoogLeNet是2014年Christian Szegedy提出的一种全新的深度学习结构,在这之前的AlexNet、VGG等结构都是通过增大网络的深度(层数)来获得更好的训练效果,但层数的增加会带来很多负作用,比如overfit、梯度消失、梯度爆炸等。因此设计出 GoogLeNet——第一个Inception架构,inception的提出追求减少深度来提升训练结果,能更高效的利用计算资源,在相同的计算量下能提取到更多的特征,从而提升训练结果。模型结构如下,
    InceptionNet-v1
  • inception结构的主要贡献有两个:一是使用1x1的卷积来进行升降维和增加信息;二是在多个尺寸上同时进行卷积再聚合。

  • 多个尺寸上进行卷积再聚合。InceptionNet-v1网络结构可以看到对输入做了4个分支,分别用不同尺寸的filter进行卷积或池化,最后再在特征维度上拼接到一起。这种全新的结构有什么好处呢?Szegedy从多个角度进行了解释:

    • 解释1:在直观感觉上在多个尺度上同时进行卷积,能提取到不同尺度的特征。特征更为丰富也意味着最后分类判断时更加准确。

    • 解释2:利用稀疏矩阵分解成密集矩阵计算的原理来加快收敛速度。举个例子图四左侧是个稀疏矩阵(很多元素都为0,不均匀分布在矩阵中),和一个2x2的矩阵进行卷积,需要对稀疏矩阵中的每一个元素进行计算;如果像右图那样把稀疏矩阵分解成2个子密集矩阵,再和2x2矩阵进行卷积,稀疏矩阵中0较多的区域就可以不用计算,计算量就大大降低。这个原理应用到inception上就是要在特征维度上进行分解!传统的卷积层的输入数据只和一种尺度(比如3x3)的卷积核进行卷积,输出固定维度(比如256个特征)的数据,所有256个输出特征基本上是均匀分布在3x3尺度范围上,这可以理解成输出了一个稀疏分布的特征集;而inception模块在多个尺度上提取特征(比如1x1,3x3,5x5),输出的256个特征就不再是均匀分布,而是相关性强的特征聚集在一起(比如1x1的的96个特征聚集在一起,3x3的96个特征聚集在一起,5x5的64个特征聚集在一起),这可以理解成多个密集分布的子特征集。这样的特征集中因为相关性较强的特征聚集在了一起,不相关的非关键特征就被弱化,同样是输出256个特征,inception方法输出的特征“冗余”的信息较少。用这样的“纯”的特征集层层传递最后作为反向计算的输入,自然收敛的速度更快。
      稀疏矩阵分解
    • 解释3:Hebbin赫布原理。Hebbin原理是神经科学上的一个理论,解释了在学习的过程中脑中的神经元所发生的变化,用一句话概括就是fire togethter, wire together。赫布认为“两个神经元或者神经元系统,如果总是同时兴奋,就会形成一种‘组合’,其中一个神经元的兴奋会促进另一个的兴奋”。比如狗看到肉会流口水,反复刺激后,脑中识别肉的神经元会和掌管唾液分泌的神经元会相互促进,“缠绕”在一起,以后再看到肉就会更快流出口水。用在inception结构中就是要把相关性强的特征汇聚到一起。这有点类似上面的解释2,把1x1,3x3,5x5的特征分开。因为训练收敛的最终目的就是要提取出独立的特征,所以预先把相关性强的特征汇聚,就能起到加速收敛的作用。

  • 关于v1,v2,v3,v4版本,请访问我以前写的这篇文章,不确定设置的是不是隐秘哈哈哈哈哈啊哈

2.5 ResNet

  • 残差网络是由来自Microsoft Research的4位学者提出的卷积神经网络,在2015年的ImageNet大规模视觉识别竞赛(ILSVRC)中获得了图像分类和物体识别的优胜。残差网络的特点是容易优化,并且能够通过增加相当的深度来提高准确率。其内部的残差块使用了跳跃连接,缓解了在深度神经网络中增加深度带来的梯度消失问题。

2.5.1 背景

  • 我们都知道增加网络的宽度和深度可以很好的提高网络的性能,深的网络一般都比浅的的网络效果好,比如说一个深的网络A和一个浅的网络B,那A的性能至少都能跟B一样,为什么呢?因为就算我们把B的网络参数全部迁移到A的前面几层,而A后面的层只是做一个等价的映射,就达到了B网络的一样的效果。在深度学习中,网络层数增多一般会伴着下面几个问题?
    • 梯度弥散或梯度爆炸。对于原来的网络,如果简单地增加深度,会导致梯度弥散或梯度爆炸,对于该问题的解决方法是正则化初始化和中间的正则化层(Batch Normalization)。
    • 计算资源的消耗。通过GPU集群来解决。
    • 过拟合。过拟合通过采集海量数据,并配合Dropout正则化等方法可以有效避免;
    • 退化问题。网络层数增加,但是在训练集上的准确率却饱和甚至下降了。这个不能解释为overfitting,因为overfit应该表现为在训练集上表现更好才对。退化问题说明了深度网络不能很简单地被很好地优化。
  • 作者通过实验:通过浅层网络等同映射构造深层模型,结果深层模型并没有比浅层网络有等同或更低的错误率,推断退化问题可能是因为深层的网络并不是那么好训练,也就是求解器很难去利用多层网络拟合同等函数。

2.5.3 网络结构

  • 下图展示不同layer的ResNet.

    ResNet 不同层数的网络配置

    在使用ResNet网络结构时,发现层数不断加深导致训练集上误差在增大的现象被消除了,并且在测试集上的表现也变好了。在2015年ResNet推出不久后,Google就借鉴了ResNet的精髓,2016年提出了InceptionV4的Inception-ResNet-V2。在2016年作者有提出了ResNet-V2,V1和V2的主要区别在于,作者发现残差单元的传播方式中,前馈和后馈的信息可以直接传输,因此将skip connection的非线性激活函数替换为Identity Mappings(y=x),同时v2在每一层都使用了Batch Normalization,新的残差单元比以前的泛化性能更强了。有学者认为,ResNet类似于一个没有gates的LSTM网络,输入X传递到后面层的过程一直发生,而不是学习出来的。也有人认为ResNet的效果类似于在多层网络间的集成(ensemble)。The Power of Depth for Feedforward Neural Networks证明了加深网络比加宽网络更有效。

  • 通过同样是34层的卷积神经网络,下图展示VGGNet-19、普通的卷积神经网络和ResNet的对比,可以看到ResNet和普通神经网络的最大区别在于,ResNet有很多支线连接到后面的层,使得后面的层可以直接学习残差,这种结构被称为shortcut或skip connections。
    VGGNet-19、普通的卷积神经网络、ResNet
  • ResNet使用了一个非常深的CNN,有50,101,152,200层等。能够训练如此深的网络的关键是使用跳过连接(skip connection,也称为快捷连接),一个层的输入信号也被添加到位于下一层的输出。当训练一个神经网络时,目标是使其模拟一个目标函数h(x)。如果将输入x添加到网络的输出中(即添加跳过连接),那么网络将被迫模拟f(x)=h(x)-x而不是h(x)。这被称为残留学习(见下图,右图是2016年改进后的resnet)。

    RESNET短链接

  • 作者还提出了两层、三层的残差学习单元。两层的残差学习单元中包含两个相同输出通道数的3*3卷积;而三层的残差单元,第二层使用了3*3的卷积,第一和第三层使用了NetworkInNetwork 和 InceptionNet中的1*1卷积,有先降维再升维的操作。此外,如果网络的输入和输出维度不同,需要对x做一个映射。再连接到后面的维度。

    两层、三层的ResNet残差模块

优点

  • (1)减少信息丢失。ResNet通过直接将输入信息短连接到输出,保护信息的完整性。
  • (2)简化学习目标和难度。整个网络只需要学习输入、输出差别的那一部分。

tensorflow2 代码实现

关于网络的实现,网上有很多代码,大家可以根据不同的框架和编码习惯,挑选一篇。

# TensorFlow实现LeNet

import tensorflow as tf
def inference(inputs):
    # input shape: [batch, height, width, 1]
    with tf.variable_scope('conv1'):
        weights = tf.Variable(tf.truncated_normal([5, 5, 1, 6], stddev=0.1))
        biases = tf.Variable(tf.zeros([6]))
        conv1 = tf.nn.conv2d(inputs, weights, strides=[1, 1, 1, 1], padding='VALID')
        conv1 = tf.nn.relu(tf.nn.bias_add(conv1, biases))
        maxpool2 = tf.nn.max_pool(conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1])
    with tf.variable_scope('conv3'):
        weights = tf.Variable(tf.truncated_normal([5, 5, 6, 16], stddev=0.1))
        biases = tf.Variable(tf.zeros([16]))
        conv3 = tf.nn.conv2d(maxpool2, weights, strides=[1, 1, 1, 1])
        conv3 = tf.nn.relu(tf.nn.bias_add(conv3, biases))
        maxpool4 = tf.nn.max_pool(conv3, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1])
    with tf.variable_scope('conv5'):
        weights = tf.Variable(tf.truncated_normal([5, 5, 6, 16], stddev=0.1))
        biases = tf.Variable(tf.zeros([16]))
        conv5 = tf.nn.conv2d(maxpool4, weights, strides=[1, 1, 1, 1])
        conv5 = tf.nn.relu(tf.nn.bias_add(conv5, biases))
    with tf.variable_scope('fc6'):
        flat = tf.reshape(conv5, [-1, 120])
        weights = tf.Variable(tf.truncated_normal([120, 84], stddev=0.1))
        biases = tf.Variable(tf.zeros([84]))
        fc6 = tf.nn.matmul(flat, weights) + biases
        fc6 = tf.nn.relu(fc6)
    with tf.variable_scope('fc7'):
        weights = tf.Variable(tf.truncated_normal([84, 10], stddev=0.1))
        biases = tf.Variable(tf.zeros([10]))
        fc7 = tf.nn.matmul(fc6, weights) + biases
        fc7 = tf.nn.softmax(fc7)
    return fc7
"""
tf2 搭建ResNet AlexNet 实战
"""

import tensorflow as tf
from tensorflow.keras.datasets import fashion_mnist
from tensorflow import data as tfdata
import numpy as np

# 将 GPU 的显存使用策略设置为 “仅在需要时申请显存空间”。
for gpu in tf.config.experimental.list_physical_devices('GPU'):
    tf.config.experimental.set_memory_growth(gpu, True)

# 1、读取数据
'''由于Imagenet数据集是一个比较庞大的数据集,且网络的输入为224*224,所以,我们定义一个方法,
来读取数据集并将数据resize到224*224的大小'''

class Data_load():
    def __init__(self):
        fashion_mnist = tf.keras.datasets.fashion_mnist
        (self.train_images, self.train_labels), (self.test_images, self.test_labels)\
            = fashion_mnist.load_data()
        # 数据维度扩充成[n,h,w,c]的模式
        self.train_images = np.expand_dims(self.train_images.astype(np.float32) / 255.0, axis=-1)
        self.test_images = np.expand_dims(self.test_images.astype(np.float32)/255.0,axis=-1)
        # 标签
        self.train_labels = self.train_labels.astype(np.int32)
        self.test_labels = self.test_labels.astype(np.int32)
        # 训练和测试的数据个数
        self.num_train, self.num_test = self.train_images.shape[0], self.test_images.shape[0]
    def get_train_batch(self,batch_size):
        # 随机取batch_size个索引
        index = np.random.randint(0, np.shape(self.train_images)[0], batch_size)
        # resize
        resized_images = tf.image.resize_with_pad(self.train_images[index], 224, 224 )
        return resized_images.numpy(), self.train_labels[index]
    def get_test_batch(self,batch_size):
        index = np.random.randint(0, np.shape(self.test_images)[0], batch_size)
        # resize
        resized_images = tf.image.resize_with_pad(self.test_images[index], 224, 224 )
        return resized_images.numpy(), self.test_labels[index]

# 2、定义模型
def MyAlexNet():
    net=tf.keras.Sequential()
    net.add(tf.keras.layers.Conv2D(96,11,(4,4),"same",activation="relu"))
    net.add(tf.keras.layers.MaxPool2D(pool_size=3, strides=2))
    net.add(tf.keras.layers.Conv2D(filters=256, kernel_size=5, padding='same', activation='relu'))
    net.add(tf.keras.layers.MaxPool2D(pool_size=3, strides=2))
    net.add(tf.keras.layers.Conv2D(filters=384, kernel_size=3, padding='same', activation='relu'))
    net.add(tf.keras.layers.Conv2D(filters=384, kernel_size=3, padding='same', activation='relu'))
    net.add(tf.keras.layers.Conv2D(filters=256, kernel_size=3, padding='same', activation='relu'))
    net.add(tf.keras.layers.MaxPool2D(pool_size=3, strides=2))
    net.add(tf.keras.layers.Flatten())
    net.add(tf.keras.layers.Dense(512, activation='relu'))# 为了方便训练 神经元个数改小,原来是1024
    net.add(tf.keras.layers.Dropout(0.5))
    net.add(tf.keras.layers.Dense(256, activation='relu'))# 为了方便训练 神经元个数改小,原来是1024
    net.add(tf.keras.layers.Dropout(0.5))
    net.add(tf.keras.layers.Dense(10, activation='sigmoid'))

    return net


def train(num_epoches,net):
    optimizer = tf.keras.optimizers.SGD(learning_rate=0.01, momentum=0.0, nesterov=False)
    net.compile(optimizer=optimizer,
                  loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])

    num_iter = data_load.num_train // batch_size
    for e in range(num_epoches):
        for n in range(num_iter):
            x_batch, y_batch = data_load.get_train_batch(batch_size)
            test_x_batch, test_y_batch = data_load.get_test_batch(batch_size)
            net.fit(x_batch, y_batch,validation_data=(test_x_batch, test_y_batch))


if __name__ == '__main__':
    # 加载数据
    batch_size=64
    data_load=Data_load()
    x_train_batch,y_train_batch=data_load.get_train_batch(batch_size)
    print("x_batch shape:",x_train_batch.shape,"y_batch shape:", y_train_batch.shape)
    
    # 加载网络结构
    net=MyAlexNet()
    X = tf.random.uniform((1,224,224,1))
    for layer in net.layers:
        X = layer(X)
        print(layer.name, 'output shape\t', X.shape)
        
    # 训练
    num_epoches = 1
    train(num_epoches, net)

"""
tf2   VGG13 实战
"""

import tensorflow as tf
from tensorflow.keras import layers, optimizers, datasets, Sequential
import os

os.environ['TF_CPP_MIN_LOG_LEVEL']='2'
tf.random.set_seed(1)

# 定义卷积层
conv_layers = [ # 5 units of conv + max pooling
    # unit 1
    layers.Conv2D(64, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.Conv2D(64, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),

    # unit 2
    layers.Conv2D(128, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.Conv2D(128, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),

    # unit 3
    layers.Conv2D(256, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.Conv2D(256, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),

    # unit 4
    layers.Conv2D(512, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.Conv2D(512, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same'),

    # unit 5
    layers.Conv2D(512, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.Conv2D(512, kernel_size=[3, 3], padding="same", activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2, 2], strides=2, padding='same')
]

# 定义全连接层
fc_layers = [
    layers.Dense(256, activation=tf.nn.relu),
    layers.Dense(128, activation=tf.nn.relu),
    layers.Dense(100, activation=None),
]

def preprocess(x, y):
    # [0~1]
    x = tf.cast(x, dtype=tf.float32) / 255.
    y = tf.cast(y, dtype=tf.int32)
    return x,y


def main():

    # [b, 32, 32, 3] => [b, 1, 1, 512]
    conv_net = Sequential(conv_layers)
    fc_net = Sequential(fc_layers)

    conv_net.build(input_shape=[None, 32, 32, 3])
    fc_net.build(input_shape=[None, 512])
    optimizer = optimizers.Adam(lr=1e-4)

    # [1, 2] + [3, 4] => [1, 2, 3, 4]
    variables = conv_net.trainable_variables + fc_net.trainable_variables

    for epoch in range(2):

        for step, (x,y) in enumerate(train_db):

            with tf.GradientTape() as tape:
                
                out = conv_net(x)   # [b, 32, 32, 3] => [b, 1, 1, 512]
                out = tf.reshape(out, [-1, 512])  # flatten, => [b, 512]
                logits = fc_net(out)  # [b, 512] => [b, 100]
                y_onehot = tf.one_hot(y, depth=100)   # [b] => [b, 100]
                # compute loss
                loss = tf.losses.categorical_crossentropy(y_onehot, logits, from_logits=True)
                loss = tf.reduce_mean(loss)

            grads = tape.gradient(loss, variables)
            optimizer.apply_gradients(zip(grads, variables))

            if step %100 == 0:
                print(epoch, step, 'loss:', float(loss))



        total_num = 0
        total_correct = 0
        for x,y in test_db:

            out = conv_net(x)
            out = tf.reshape(out, [-1, 512])
            logits = fc_net(out)
            prob = tf.nn.softmax(logits, axis=1)
            pred = tf.argmax(prob, axis=1)
            pred = tf.cast(pred, dtype=tf.int32)

            correct = tf.cast(tf.equal(pred, y), dtype=tf.int32)
            correct = tf.reduce_sum(correct)

            total_num += x.shape[0]
            total_correct += int(correct)
 
        acc = total_correct / total_num
        print(epoch, 'acc:', acc)



if __name__ == '__main__':
    
    (x,y), (x_test, y_test) = datasets.cifar100.load_data()
    y = tf.squeeze(y, axis=1)
    y_test = tf.squeeze(y_test, axis=1)
    print(x.shape, y.shape, x_test.shape, y_test.shape)


    train_db = tf.data.Dataset.from_tensor_slices((x,y))
    train_db = train_db.shuffle(1000).map(preprocess).batch(128)

    test_db = tf.data.Dataset.from_tensor_slices((x_test,y_test))
    test_db = test_db.map(preprocess).batch(64)

    sample = next(iter(train_db))
    print('sample: \n', sample[0].shape, sample[1].shape,
          tf.reduce_min(sample[0]), tf.reduce_max(sample[0]))


    main()


'''
tf2 搭建ResNet
'''

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, Sequential



class BasicBlock(layers.Layer):

    def __init__(self, filter_num, stride=1):
        super(BasicBlock, self).__init__()

        self.conv1 = layers.Conv2D(filter_num, (3, 3), strides=stride, padding='same')
        self.bn1 = layers.BatchNormalization()
        self.relu = layers.Activation('relu')

        self.conv2 = layers.Conv2D(filter_num, (3, 3), strides=1, padding='same')
        self.bn2 = layers.BatchNormalization()

        if stride != 1:
            self.downsample = Sequential()
            self.downsample.add(layers.Conv2D(filter_num, (1, 1), strides=stride))
        else:
            self.downsample = lambda x:x



    def call(self, inputs, training=None):

        # [b, h, w, c]
        out = self.conv1(inputs)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)

        identity = self.downsample(inputs)

        output = layers.add([out, identity])
        output = tf.nn.relu(output)

        return output

# Res Block 模块。继承keras.Model或者keras.Layer都可以
class ResNet(keras.Model):
    # 第一个参数layer_dims:[2, 2, 2, 2] 4个Res Block,每个包含2个Basic Block
    # 第二个参数num_classes:我们的全连接输出,取决于输出有多少类。
    def __init__(self, layer_dims, num_classes=100): # [2, 2, 2, 2]
        super(ResNet, self).__init__()
        
        # 预处理层;实现起来比较灵活可以加 MAXPool2D,
        # 从头到尾的顺序,对多个网络层的线性堆叠。使用.add()方法将各层添加到模型中
        self.stem = Sequential([layers.Conv2D(64, (3, 3), strides=(1, 1), padding='valid'),
                                layers.BatchNormalization(),
                                layers.Activation('relu'),
                                layers.MaxPool2D(pool_size=(2, 2), strides=(1, 1), padding='same')
                                ])
        # 创建4个Res Block;
        self.layer1 = self.build_resblock(64,  layer_dims[0])
        self.layer2 = self.build_resblock(128, layer_dims[1], stride=2)
        self.layer3 = self.build_resblock(256, layer_dims[2], stride=2)
        self.layer4 = self.build_resblock(512, layer_dims[3], stride=2)

        # gap:减少参数
        self.gap = layers.GlobalAveragePooling2D()
        self.fc = layers.Dense(num_classes)

    def call(self, inputs, training=None):
        # 前向运算
        x = self.stem(inputs)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.gap(x) # [b, c]
        x = self.fc(x) # [b, 100]

        return x

    def build_resblock(self, filter_num, blocks, stride=1):

        res_blocks = Sequential()
        res_blocks.add(BasicBlock(filter_num, stride))

        for _ in range(1, blocks):
            res_blocks.add(BasicBlock(filter_num, stride=1))

        return res_blocks


def resnet18():
    return ResNet([2, 2, 2, 2])

推荐阅读更多精彩内容