深度卷积神经网络理论回顾

卷积与相关操作计算的关系

  卷积运算的一个重要的特点就是,通过卷积运算,可以使原信号增强,并且降低噪音。在卷积(convolution)层中通常使用的是更加直观的互相关(cross-correlation)运算。在二维卷积层中,一个二维输入数组和一个二维核(kernel)数组通过互相关运算输出一个二维数组。我们用一个具体例子来解释二维互相关运算的含义。如下图所示,输入是一个高和宽均为3的二维数组。我们将该数组的形状记为 3 × 3 3 \times 3 3×3或(3,3)。核数组的高和宽分别为2。该数组在卷积计算中又称卷积核或过滤器(filter)。卷积核窗口(又称卷积窗口)的形状取决于卷积核的高和宽,即 2 × 2 2 \times 2 2×2。下图中阴影部分为第一个输出元素及其计算所使用的输入和核数组元素: 0 × 0 + 1 × 1 + 3 × 2 + 4 × 3 = 19 0\times0+1\times1+3\times2+4\times3=19 0×0+1×1+3×2+4×3=19
Figure
  在二维互相关运算中,卷积窗口从输入数组的最左上方开始,按从左往右、从上入下的顺序,依次在输入数组上滑动。当卷积窗口滑动到某一位置时,窗口中的输入子数组与核数组按元素相乘并求和,得到输出数组中相应位置的元素。
  二维卷积层将输入和卷积核做互相关运算,并加上一个标量偏差来得到输出。卷积层的模型参数包括卷积核和标量偏差。在训练模型的时候,通常我们先对卷积核随机初始化,然后不断迭代卷积核和偏差。卷积窗口形状为 p × q p\times q p×q的卷积层称为 p × q p\times q p×q p × q p\times q p×q卷积或 q × p q\times p q×p卷积核也说明卷积核的高和宽分别为p和q。卷积层可通过重复使用卷积核有效地表征局部空间。
  卷积运算与互相关运算类似。为了得到卷积运算的输出,我们只需将核数组左右翻转并上下翻转,再与输入数组做互相关运算。 可见,卷积运算和互相关运算虽然类似,但如果他们使用相同的核数组,对于同一个输入,输出往往并不相同。在卷积层为何能使用互相关运算代码卷积运算呢?其实,在深度学习中核数组都是学出来的:卷积层无论使用互相关运算或卷积运算都不影响模型预测时的输出。
  PyTorch中,使用Conv2d来实现二维卷积层。
class torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)
二维卷积层,输入的尺度是(N, C_in,H,W),输出尺度(N,C_out,H_out,W_out)

卷积与反卷积

TyTorch里卷积与反卷积

  • 2D卷积函数
    class torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)
    卷积层中的参数是:
    weight(tensor) - 卷积的权重,大小是(out_channels, in_channels,kernel_size)
    bias(tensor) - 卷积的偏置系数,大小是(out_channel)

  • 2D反卷积函数
    class torch.nn.ConvTranspose2d(in_channels, out_channels, kernel_size, stride=1, padding=0, output_padding=0, groups=1, bias=True)

Pooling操作的定义与作用

  利用图像局部相关性的原理,对图像进行下采样,可以减少数据处理量同时保留有用信息。池化层(pooling) ,它的提出是为了缓解卷积层对位置的过度敏感性。池化一般有最大池化或平均池化。在最大二维池化中,池化窗口从输入数组的最左上方开始,按从左往右、从上往下的顺序,依次在输入数组上滑动。当池化窗口滑动到某一位置时,窗口中的输入子数组的最大值(平均值)即输出数组中相应益的元素。池化窗口形状为 p × q p\times q p×q的池化层称为 p × q p\times q p×q池化层,其中的池化运算叫作 p × q p\times q p×q池化。在处理多通道输入数据时,池化层对每个输入通道分别池化,而不是像卷积层那样将各通道的输入按通道相加,这意味着池化层的输出通道数与输入通道数相等
在这里插入图片描述
pytorch实现
class torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)

特征图与感受野

Figure
  二维卷积层输出的二维数组可以看作是输入空间维度(宽和高)上某一级的表征,也叫特征图(feature map)。影响元素 x x x的前向计算的所有可能输入区域(可能大于输入的实际尺寸)叫做 x x x的感受野(receptive field)。以上图为例,输入中阴影部分的四个元素是输出中阴影部分元素的感受野。我们将上图中 2 × 2 2\times 2 2×2的输出记为 Y Y Y,并考虑一个更深的卷积神经网络:将Y与另一个形状为 2 × 2 2\times 2 2×2的核数组做互相关运算,输出单个元素 z z z,那么, z z z Y Y Y上的感受野包括 Y Y Y的全部四个元素,在输入上的感受野包括其中全部9个元素。我们可以通过更深的卷积神经网络使用特征图中的单个元素(单元)的感受野变得更加广阔,从而捕捉输入上更大尺寸的特征特征图中元素的感受野是与特定的层相对而言的,比如上面例子中,最后一层输出的单个元素 z z z,对于上一层特征图 Y Y Y而言,其感受野是 Y Y Y中的4个元素,而对于第一层输入图像来说,在输入上的感受里包括全部的9个元素(这9个元素都会影响这个最终的输出元素)。

卷积网络中的计算,填充与步幅

  假设输入形状是 n h × n w n_h\times n_w nh×nw,卷积核窗口形状是 k h × k w k_h\times k_w kh×kw,那么输出形状将会是

( n h − k h + 1 ) × ( n w − k w + 1 ) . (n_h-k_h+1) \times (n_w-k_w+1). (nhkh+1)×(nwkw+1).
所以卷积层的输出形状由输入形状和卷积核窗口形状决定。卷积层还有两个超参数,填充和步幅,它们可以对给定形状的输入和卷积核改变输出形状。
  填充(padding) 是指在输入高和宽的两侧填充元素(通常是0元素)。以下图为例,我们在原输入高和宽的两侧分别添加了值为0的元素,使得输入高和宽从3变成了5,并导致输出高和宽从2增加到4。一般来说,如果在高的两侧一共填充 p h p_h ph行,在宽的两侧一共填充 p w p_w pw列,那么输出形状将会是 ( n h − k h + p h + 1 ) × ( n w − k w + p w + 1 ) (n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1) (nhkh+ph+1)×(nwkw+pw+1),也就是说输出的高和宽会分别增加 p h p_h ph p w p_w pw。很多情况下,我们会设置 p h = k h − 1 p_h=k_h-1 ph=kh1 p w = k w − 1 p_w=k_w-1 pw=kw1来使输入和输出具有相同的高和宽。这样会方便在构造网络时推测每个层的输出形状。假设这里 k h k_h kh是奇数,我们会在高的两侧分别填充 p h / 2 p_h/2 ph/2行。如果 k h k_h kh是偶数,一种可能的输入是在输入的顶端一侧填充 ⌈ p h / 2 ⌉ \lceil p_h/2\rceil ph/2行,而在底端一侧填充 ⌊ p h / 2 ⌋ \lfloor p_h/2\rfloor ph/2行。在宽的两侧填充同理。卷积神经网络经常使用奇数高宽的卷积核,如1,3,5和7,所以两端上的填充个数相等。
Figure
  卷积窗口从输入数组的最左上方开始,按从从往右、从上往下的顺序,依次在输入数组上滑动。将每次滑动的行数和列数称为步幅(stride)。在下面的例子中,展示了在高上步幅为3、在宽上步幅为2的二维互相关运算。可以看到,输出第一列第二个元素时,卷积窗口向下滑动了3行,而在输出第一行第二个元素时卷积窗口向右滑动了2列。当卷积窗口在输入上再向右滑动2列时,由于输出元素无法填满窗口,无结果输出。一般来说,当高上步幅为 s h s_h sh,宽上步幅为 s w s_w sw,输出形状为 ⌊ ( n h − k h + p h + s h ) / s h ⌋ × ⌊ ( n w − k w + p w + s w ) / s w ⌋ \lfloor(n_h-k_h+p_h+s_h)/s_h\rfloor \times \lfloor(n_w-k_w+p_w+s_w)/s_w\rfloor (nhkh+ph+sh)/sh×(nwkw+pw+sw)/sw
如果设置 p h = k h − 1 p_h=k_h-1 ph=kh1 p w = k w − 1 p_w=k_w-1 pw=kw1,那么输出形状将简化为 ⌊ ( n h + s h − 1 ) / s h ⌋ × ⌊ ( n w + s w − 1 ) / s w ⌋ \lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor (nh+sh1)/sh×(nw+sw1)/sw。更进一步,如果输入的高和宽能分别被高和宽上的步幅整除,那么输出形状将是 ( n h / s h ) × ( n w / s w ) (n_h/s_h) \times (n_w/s_w) (nh/sh)×(nw/sw)。当输入的高和宽两侧填充数分别这 p h p_h ph p w p_w pw时,我们称填充为( p h p_h ph, p w p_w pw)。特别地,当 p h = p w = p p_h = p_w = p ph=pw=p时,填充为 p p p。当在高和宽上的步幅分别为 s h s_h sh s w s_w sw时,我们称步幅为 ( s h , s w ) (s_h, s_w) (sh,sw)。特别地,当 s h = s w = s s_h = s_w = s sh=sw=s时,步幅为 s s s。在默认情况下,填充为0,步幅为1。

Figure

多输入通道和多输出通道

  多输入通道。 当输入数据含多个通道时,我们需要构造一个输入通道数与输入数据的通道数相同的卷积核,从而能够与含多通道的输入数据做互相关运算。假设输入数据的通道数为 c i c_i ci,那么卷积核的输入通道数同样为 c i c_i ci,设卷积核窗口形状为 k h × k w k_h\times k_w kh×kw。当 c i = 1 c_i=1 ci=1时,卷积核只包含一个形状为 k h × k w k_h\times k_w kh×kw的二维数组。当 c i > 1 c_i>1 ci>1时,将会为每个输入通道各分配一个形状为 k h × k w k_h\times k_w kh×kw的核数组。把这 c i c_i ci个数组在输入通道维上连结,即得到一个形状为 c i × k h × k w c_i\times k_h \times k_w ci×kh×kw的卷积核。由于输入和卷积核各有 c ∣ i c|i ci个通道,我们可以在各个通道上对输入的二维数组和卷积核的二维核数组做互相关运算,再将这 c i c_i ci个互相关运算的二维输出按通道相加,得到一个二维数组。这就是含多个通道的输入数据与多输入通道的卷积核做二维互相关运算的输出。下图中展示了含2个输入通道的二维互相关计算的例子。在每个通道上,二维输入数组与二维核数组做互相关运算,再按通道相加即得到输出。图中阴影部分为第一个输出元素及其计算所使用的输入和核数组元素: ( 1 × 1 + 2 × 2 + 4 × 3 + 5 × 4 ) + ( 0 × 0 + 1 × 1 + 3 × 2 + 4 × 3 ) = 56 (1\times1+2\times2+4\times3+5\times4)+(0\times0+1\times1+3\times2+4\times3)=56 (1×1+2×2+4×3+5×4)+(0×0+1×1+3×2+4×3)=56
在这里插入图片描述
  多输出通道 。当输入通道有多个时,因为我们对各个通道的结果做了累加,所以不论输入通道数是多少,输出通道数问题是1。设卷积核通道和输出通道数分别为 c i c_i ci c o c_o co,高和宽分别为 k h k_h kh k w k_w kw。如果希望得到含多个通道的输出,我们可以为每个输出通道分别创建形状为 c i × k h × k w c_i\times k_h \times k_w ci×kh×kw的核数组。将它们在输出通道维上连结,卷积核形状为 c o × c i × k h × k w c_o \times c_i\times k_h \times k_w co×ci×kh×kw。在做互相关运算时,每个输出通道上的结果由卷积核在该输出通道上的核数组与整个输入数组计算而来。
   1 × 1 1\times 1 1×1卷积层。 现在讨论卷积窗口形状为 1 × 1 ( k h = k w = 1 ) 1\times 1(k_h = k_w = 1) 1×1kh=kw=1的多通道卷积层,它通道被称为 1 × 1 1\times 1 1×1卷积层,并将其中的卷积运算称为 1 × 1 1\times 1 1×1卷积。因为使用了最小窗口, 1 × 1 1\times 1 1×1卷积失去了卷积层可以识别高和宽维度上相邻元素构成的模式识别功能。实际上, 1 × 1 1\times 1 1×1卷积的主要计算发生在通道维上。下图展示了一个 1 × 1 1\times 1 1×1卷积的例子。输出中的每个元素来自输入中在高和宽上相同位置的元素在不同通道之间的按权重累加。假设我们将通道维当作特征维,将高和宽维度上的元素当成数据样本,那么 1 × 1 1\times 1 1×1卷积层的作用与全连接层等价。
在这里插入图片描述

卷积网络计算复杂度与参数量分析

LeNet 5
以C1层为例来分析这一层需要学习的参数数量。C1层是一个卷积层,由6个Feature Map构成,特征图中每个神经元与输入中 5 × 5 5\times 5 5×5的邻域相连,特征图的大小为 28 × 28 28\times 28 28×28。每个卷积核有 5 × 5 = 25 5\times 5=25 5×5=25个weight参数和一个bias参数,一共6个卷积核,共有 ( 5 × 5 ) × 6 = 156 (5\times 5)\times 6=156 (5×5)×6=156个参数。C1层与输入图像共 156 × ( 28 × 28 ) = 122304 156\times (28\times 28)=122304 156×(28×28)=122304个连接。

深度卷积网络各层的特点(深、浅)

  在深度卷积网络中,浅层网络能更好的提取位置特征,深层网络能更好地提取语义特征。
  CNN的强大之处在于它的多层结构能自动学习特征,并且可以学习到多个层次的特征:较浅的卷积层感知域较小,学习到一些局部区域的特征;较深的卷积层具有较大的感知域,能够学习到更加抽象一些的特征。这些抽象特征对物体的大小、位置和方向等敏感性更低,从而有助于识别性能的提高。CNN的强大之处在于它的多层结构能自动学习特征,并且可以学习到多个层次的特征:较浅的卷积层感知域较小,学习到一些局部区域的特征;较深的卷积层具有较大的感知域,能够学习到更加抽象一些的特征。这些抽象特征对物体的大小、位置和方向等敏感性更低,从而有助于识别性能的提高。这些抽象的特征对分类很有帮助,可以很好地判断出一幅图像中包含什么类别的物体,但是因为丢失了一些物体的细节,不能很好地给出物体的具体轮廓、指出每个像素具体属于哪个物体,因此做到精确的分割就很有难度。

卷积层、全连接层、全连接卷积

  大多数神经网络中高层网络通常会采用全连接层(Global Connected Layer),通过多对多的连接方式对特征进行全局汇总,可以有效地提取全局信息。但是全连接的方式需要大量参数,是神经网络中最占资源的部分之一,因此就需要由局部连接(Local Connected Layer),仅在局部区域范围内产生神经元连接,能够有效地减少参数量。
  根据卷积操作的作用范围可以分为全卷积(Global Convolution)和局部卷积(Local Convolution)。全卷积就是标准卷积,在整个输入特征维度范围内采用相同的卷积核参数进行运算,全局共享参数的连接方式可以使神经元之间的连接参数大大减少。局部卷积又叫平铺卷积(Tiled Convolution)或非共享卷积(Unshared Convolution),是局部连接与全卷积的折衷。
  通常CNN网络在卷积层之后会接上若干个全连接层,将卷积层产生的特征图(feature map)映射成一个固定长度的特征向量。FCN对图像进行像素级的分类,它可以接受任意尺寸的输入图像,采用反卷积对最后一个卷积层的feature map进行上采样,使它恢复到输入图像相同的尺寸,从而可以对每个像素都产生了一个预测,同时保留了原始输入图像中的空间信息,最后在上采样的特征图上进行逐像素分类。最后逐个像素计算softmax分类的损失,相当于每一个像素对应一个训练样本。
  FCN将传统CNN中的全连接层转化成卷积层,对应CNN网络FCN把最后三层全连接层转换成为三层卷积层。在传统的CNN结构中,前5层是卷积层,第6层和第7层分别是一个长度为4096的一维向量,第8层是长度为1000的一维向量,分别对应1000个不同类别的概率。FCN将这3层表示为卷积层,卷积核的大小 (通道数,宽,高) 分别为 (4096,1,1)、(4096,1,1)、(1000,1,1)。看上去数字上并没有什么差别,但是卷积跟全连接是不一样的概念和计算过程,使用的是之前CNN已经训练好的权值和偏置,但是不一样的在于权值和偏置是有自己的范围,属于自己的一个卷积核。因此FCN网络中所有的层都是卷积层,故称为全卷积网络

全连接层 ↔ \leftrightarrow 卷积层

  全连接层和卷积层之间唯一的不同就是卷积层中的神经元只与输入数据中的一个局部区域连接,并且在卷积列中的神经元共享参数。在两类层中,神经元都是计算点积,所以它们的函数形式是一样的,因此,将此两者相互转化是可能的:

  • 卷积层 → \rightarrow 全连接层。对于任一个卷积层,都存在一个能实现和它一样的前向传播函数的全连接层。权重矩阵是一个巨大的矩阵,除了某些特定块,其余部分都是零,而在其中大部分块中,元素都是相等的。
  • 全连接层 ← \leftarrow 全连接层。任何全连接层都可以转化为卷积层。比如,一个 K = 4096 K=4096 K=4096的全连接层,输入数据的形状是 7 × 7 × 512 7\times 7\times 512 7×7×512,这个全连接层可以被等效地看作一个 F = 7 , P = 0 , S = 1 , K = 4096 F=7,P=0,S=1,K=4096 F=7,P=0,S=1,K=4096的卷积层。换句话说,就是将卷积核的尺寸设置为输入数据尺寸一致。因为只有一个单独的深度列覆盖并滑过输入数据体,所以输出将变成 1 × 1 × 4096 1\times1\times4096 1×1×4096,这个结果就和全连接层是一样的了。

写了一段测试代码

import torch
from torch import nn


class Conv(nn.Module):
    def __init__(self):
        super(Conv, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(in_channels=512, out_channels=4096, kernel_size=7),
            nn.Conv2d(in_channels=4096, out_channels=4096, kernel_size=1),
            nn.Conv2d(in_channels=4096, out_channels=1000, kernel_size=1)
        )

    def forward(self, x):
        return self.conv(x)


class Full_Connect(nn.Module):
    def __init__(self):
        super(Full_Connect, self).__init__()
        self.Full = nn.Sequential(
            nn.Linear(in_features=7*7*512, out_features=4096),
            nn.Linear(in_features=4096, out_features=4096),
            nn.Linear(in_features=4096, out_features=1000)
        )

    def forward(self, x):
        return self.Full(x)


x = torch.rand(1, 512, 7, 7)
net1 = Conv()
net2 = Full_Connect()
print(net1)
print(net2)

y1 = net1(x)
print(y1.view(-1).shape)
y2 = net2(x.view(-1))
print(y2.shape)

输出:

Conv(
  (conv): Sequential(
    (0): Conv2d(512, 4096, kernel_size=(7, 7), stride=(1, 1))
    (1): Conv2d(4096, 4096, kernel_size=(1, 1), stride=(1, 1))
    (2): Conv2d(4096, 1000, kernel_size=(1, 1), stride=(1, 1))
  )
)
Full_Connect(
  (Full): Sequential(
    (0): Linear(in_features=25088, out_features=4096, bias=True)
    (1): Linear(in_features=4096, out_features=4096, bias=True)
    (2): Linear(in_features=4096, out_features=1000, bias=True)
  )
)
torch.Size([1000])
torch.Size([1000])

典型的深度卷积网络

LeNet的PyTorch实现

LeNet

class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5),
            nn.Sigmoid(),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5),
            nn.Sigmoid(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        self.fc = nn.Sequential(
            nn.Linear(in_channels=16*4*4, out_channels=120),
            nn.Sigmoid(),
            nn.Linear(in_channels=120, out_channels=84),
            nn.Sigmoid(),
            nn.Linear(in_channels=84, out_channels=10)
        )

    def forward(self, img):
        feature = self.conv(img)
        output = self.fc(feature.view(img.shape[0], -1))
        return output

网络结构打印出来的结构。

LeNet(
  (conv): Sequential(
    (0): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
    (1): Sigmoid()
    (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (3): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
    (4): Sigmoid()
    (5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (fc): Sequential(
    (0): Linear(in_features=256, out_features=120, bias=True)
    (1): Sigmoid()
    (2): Linear(in_features=120, out_features=84, bias=True)
    (3): Sigmoid()
    (4): Linear(in_features=84, out_features=10, bias=True)
  )
)

alexnet的PyTorch实现

AlexNet
简化版的AlexNet。

class AlexNet(nn.Module):
    def __init__(self):
        super(AlexNet, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(1, 96, 11, 4), # in_channels, out_channels, kernel_size, stride, padding
            nn.ReLU(),
            nn.MaxPool2d(3, 2), # kernel_size, stride
            # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
            nn.Conv2d(96, 256, 5, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(3, 2),
            # 连续3个卷积层,且使用更小的卷积窗口。除了最后的卷积层外,进一步增大了输出通道数。
            # 前两个卷积层后不使用池化层来减小输入的高和宽
            nn.Conv2d(256, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 384, 3, 1, 1),
            nn.ReLU(),
            nn.Conv2d(384, 256, 3, 1, 1),
            nn.ReLU(),
            nn.MaxPool2d(3, 2)
        )
         # 这里全连接层的输出个数比LeNet中的大数倍。使用丢弃层来缓解过拟合
        self.fc = nn.Sequential(
            nn.Linear(256*5*5, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(),
            nn.Dropout(0.5),
            # 输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
            nn.Linear(4096, 10),
        )

    def forward(self, img):
        feature = self.conv(img)
        output = self.fc(feature.view(img.shape[0], -1))
        return output

google的PyTorch实现

Inception

class Inception(nn.Module):
    # c1 - c4为每条线路里的层的输出通道数
    def __init__(self, in_c, c1, c2, c3, c4):
        super(Inception, self).__init__()
        # 线路1,单1 x 1卷积层
        self.p1_1 = nn.Conv2d(in_c, c1, kernel_size=1)
        # 线路2,1 x 1卷积层后接3 x 3卷积层
        self.p2_1 = nn.Conv2d(in_c, c2[0], kernel_size=1)
        self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
        # 线路3,1 x 1卷积层后接5 x 5卷积层
        self.p3_1 = nn.Conv2d(in_c, c3[0], kernel_size=1)
        self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
        # 线路4,3 x 3最大池化层后接1 x 1卷积层
        self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
        self.p4_2 = nn.Conv2d(in_c, c4, kernel_size=1)

    def forward(self, x):
        p1 = F.relu(self.p1_1(x))
        p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
        p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
        p4 = F.relu(self.p4_2(self.p4_1(x)))
        return torch.cat((p1, p2, p3, p4), dim=1)  # 在通道维上连结输出

b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                   nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),
                   nn.Conv2d(64, 192, kernel_size=3, padding=1),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32),
                   Inception(256, 128, (128, 192), (32, 96), 64),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64),
                   Inception(512, 160, (112, 224), (24, 64), 64),
                   Inception(512, 128, (128, 256), (24, 64), 64),
                   Inception(512, 112, (144, 288), (32, 64), 64),
                   Inception(528, 256, (160, 320), (32, 128), 128),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
                   b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
                   Inception(832, 384, (192, 384), (48, 128), 128),
                   d2l.GlobalAvgPool2d())
net = nn.Sequential(b1, b2, b3, b4, b5, 
                    d2l.FlattenLayer(), nn.Linear(1024, 10))

resnet的PyTorch实现

在含有这种跳跃连接的网络结构的具体实现时,是通过中间量的计算存储,然后再输入到后面的操作中完成的,具体看代码中的foward部分,很有借鉴意义。
res_block

class Residual(nn.Module): 
    def __init__(self, in_channels, out_channels, use_1x1conv=False, stride=1):
        super(Residual, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, stride=stride)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
        if use_1x1conv:
            self.conv3 = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride)
        else:
            self.conv3 = None
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.bn2 = nn.BatchNorm2d(out_channels)

    def forward(self, X):
        Y = F.relu(self.bn1(self.conv1(X)))
        Y = self.bn2(self.conv2(Y))
        if self.conv3:
            X = self.conv3(X)
        return F.relu(Y + X)

ResNet中Bottleneck的官方实现

class Bottleneck(nn.Module):
    expansion = 4
 
    def __init__(self, inplanes, planes, stride=1, downsample=None):
        super(Bottleneck, self).__init__()
        self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)
        self.bn1 = nn.BatchNorm2d(planes)
        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride,
                               padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(planes)
        self.conv3 = nn.Conv2d(planes, planes * self.expansion, kernel_size=1, bias=False)
        self.bn3 = nn.BatchNorm2d(planes * self.expansion)
        self.relu = nn.ReLU(inplace=True)
        self.downsample = downsample
        self.stride = stride
 
    def forward(self, x):
        residual = x
 
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
 
        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)
 
        out = self.conv3(out)
        out = self.bn3(out)
 
        if self.downsample is not None:
            residual = self.downsample(x)
 
        out += residual
        out = self.relu(out)
 
        return out

densenet的PyTorch实现

DenseNet模块
与ResNet的主要区别在于,DenseNet里模块 B B B的输出不是像ResNet那样和模块 A A A的输出相加,而是在通道维上连结。这样模块 A A A的输出可以直接传入模块 B B B后面的层。在这个设计里,模块 A A A直接跟模块 B B B后面的所有层连接在了一起。这也是它被称为“稠密连接”的原因。DenseNet的主要构建模块是稠密块(dense block)和过渡层(transition layer)。前者定义了输入和输出是如何连结的,后者则用来控制通道数,使之不过大。

def conv_block(in_channels, out_channels):
    blk = nn.Sequential(nn.BatchNorm2d(in_channels), 
                        nn.ReLU(),
                        nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))
    return blk
class DenseBlock(nn.Module):
    def __init__(self, num_convs, in_channels, out_channels):
        super(DenseBlock, self).__init__()
        net = []
        for i in range(num_convs):
            in_c = in_channels + i * out_channels
            net.append(conv_block(in_c, out_channels))
        self.net = nn.ModuleList(net)
        self.out_channels = in_channels + num_convs * out_channels # 计算输出通道数

    def forward(self, X):
        for blk in self.net:
            Y = blk(X)
            X = torch.cat((X, Y), dim=1)  # 在通道维上将输入和输出连结
        return X
def transition_block(in_channels, out_channels):
    blk = nn.Sequential(
            nn.BatchNorm2d(in_channels), 
            nn.ReLU(),
            nn.Conv2d(in_channels, out_channels, kernel_size=1),
            nn.AvgPool2d(kernel_size=2, stride=2))
    return blk
    
net = nn.Sequential(
        nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
        nn.BatchNorm2d(64), 
        nn.ReLU(),
        nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

学习参考资料

动手学深度学习
卷积神经网络中感受野的详细介绍
PyTorch中文文档
深度卷积网络的详细解释
解读 pytorch对resnet的官方实现
全连接、局部连接、全卷积与局部卷积

猜你喜欢

转载自blog.csdn.net/PAN_Andy/article/details/102761142
今日推荐