【深度学习】卷积神经网络

卷积神经网络

1.从全连接层到卷积

1.1不变性

想象一下,假设我们想从一张图片中找到某个物体。 合理的假设是:无论哪种方法找到这个物体,都应该和物体的位置无关。 理想情况下,我们的系统应该能够利用常识:猪通常不在天上飞,飞机通常不在水里游泳。 但是,如果一只猪出现在图片顶部,我们还是应该认出它。卷积神经网络正是将空间不变性(spatial invariance)的这一概念系统化,从而基于这个模型使用较少的参数来学习有用的表示。

  • 平移不变性(translation invariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应。
  • 局部性(locality):神经网络的前几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系。最终聚合这些局部特征,以在整个图像级别进行预测。

1.2 多层感知机的限制

首先,多层感知机的输入是二维图像X,其隐藏表示H在数学上是一个矩阵,在代码中表示为二维张量。其中X和H具有相同的形状,即二者拥有空间结构。
使用 [ X ] i , j [X]_{i,j} [X]i,j [ H ] i , j [H]_{i,j} [H]i,j分别表示输入图像和隐藏表示中位置为 ( i , j ) (i,j) (i,j)处的像素。为了使每个隐藏神经元都能接收到每个输入像素的信息,将参数从权重矩阵替换为四阶权重张量W。假设U包含偏置参数,可以将全连接层形式化地表示为:
[ H ] i , j = [ U ] i , j + ∑ k ∑ l [ W ] i , j , k , l [ X ] k , l = [ U ] i , j + ∑ a ∑ b [ V ] i , j , a , b [ X ] i + a , j + b \begin{aligned} {[\mathbf{H}]_{i, j} } & =[\mathbf{U}]_{i, j}+\sum_{k} \sum_{l}[\mathbf{W}]_{i, j, k, l}[\mathbf{X}]_{k, l} \\ & =[\mathbf{U}]_{i, j}+\sum_{a} \sum_{b}[\mathbf{V}]_{i, j, a, b}[\mathbf{X}]_{i+a, j+b} \end{aligned} [H]i,j=[U]i,j+kl[W]i,j,k,l[X]k,l=[U]i,j+ab[V]i,j,a,b[X]i+a,j+b
其中,从W到V的转换只是形式上的转换,因为在这两个四阶张量的元素之间存在一一对应的关系。此时只需重新索引下标 ( k , l ) (k,l) (k,l),使得 k = i + a , l = j + b k=i+a, l=j+b k=i+a,l=j+b,由此可得 [ V ] i , j , a , b = [ W ] i , j , i + a , j + b [V]_{i,j,a,b}=[W]_{i,j,i+a,j+b} [V]i,j,a,b=[W]i,j,i+a,j+b。索引a和b通过在正偏移和负偏移之间移动覆盖了整个图像。对于隐藏表示中任意给定位置 ( i , j ) (i,j) (i,j)处的像素 [ H ] i , j [H]_{i,j} [H]i,j,可以通过在x中以 ( i , j ) (i,j) (i,j)为中心对像素进行加权求和得到,加权使用的权重为 [ V ] i , j , a , b [V]_{i,j,a,b} [V]i,j,a,b

1.2.1 平移不变性

现在引用上述的第一个原则:平移不变性。这意味着检测对象在输入X中的平移,应该仅导致隐藏表示H中的平移。也就是说,V和U实际上不依赖于 ( i , j ) (i,j) (i,j)的值,即 [ V ] i , j , a , b = [ V ] a , b [V]_{i,j,a,b}=[V]_{a,b} [V]i,j,a,b=[V]a,b。并且U是一个常数,比如u。因此可以简化H定义为:
[ H ] i , j = u + ∑ a ∑ b [ V ] a , b [ X ] i + a , j + b \begin{aligned} {[\mathbf{H}]_{i, j} } & =u+\sum_{a} \sum_{b}[\mathbf{V}]_{a,b}[\mathbf{X}]_{i+a,j+b} \end{aligned} [H]i,j=u+ab[V]a,b[X]i+a,j+b
这就是卷积。

1.2.2 局部性

现在引用上述的第二个原则:局部性。如上所述,为了收集用来训练参数 [ H ] i , j [H]_{i,j} [H]i,j的相关信息,我们不应偏离到距 ( i , j ) (i,j) (i,j)很远的地方。这意味着在$|a|> \Delta 或 或 |b|> \Delta 的范围之外,我们可以设置 的范围之外,我们可以设置 的范围之外,我们可以设置[V]{a,b}=0 。因此,可以将 。因此,可以将 。因此,可以将[H]{i,j}$重写为:
[ H ] i , j = u + ∑ a = − Δ Δ ∑ b = − Δ Δ [ V ] a , b [ X ] i + a , j + b \begin{aligned} {[\mathbf{H}]_{i, j} } & =u+\sum_{a=- \Delta}^{\Delta} \sum_{b=- \Delta}^{\Delta }[\mathbf{V}]_{a,b}[\mathbf{X}]_{i+a,j+b} \end{aligned} [H]i,j=u+a=ΔΔb=ΔΔ[V]a,b[X]i+a,j+b
这就是一个卷积层(convolutional layer)。卷积神经网络就是包含卷积层的一类特殊的神经网络。在深度学习研究社区中,V被称为卷积核(convolution kernal)或者滤波器(filter),亦或简单的被称之为该卷积层的权重,通常该权重是可学习的参数。当图像处理的局部区域很小时,卷积神经网络与多层感知机的训练差异可能是巨大的:以前,多层感知机可能需要数十亿个参数来表示网络中的一层,而现在卷积神经网络通常只需要几百个参数,而且不需要改变输入或隐藏表示的维数。 参数大幅减少的代价是,我们的特征现在是平移不变的,并且当确定每个隐藏活性值时,每一层只包含局部的信息。 以上所有的权重学习都将依赖于归纳偏置。当这种偏置与现实相符时,我们就能得到样本有效的模型,并且这些模型能很好地泛化到未知数据中。 但如果这偏置与现实不符时,比如当图像不满足平移不变时,我们的模型可能难以拟合我们的训练数据。

1.3 卷积

在数学中,两个函数( f , g f,g f,g)之间的卷积被定义为:
( f ∗ g ) ( x ) = ∫ f ( z ) g ( x − z ) d z (f * g)(\mathbf{x})=\int f(\mathbf{z}) g(\mathbf{x}-\mathbf{z}) d \mathbf{z} (fg)(x)=f(z)g(xz)dz
也就是说,卷积是当把一个函数“翻转”并移位x时,测量f和g之间的重叠。 当为离散对象时,积分就变成求和。例如,对于由索引为Z的、平方可和的、无限维向量集合中抽取的向量,我们得到以下定义:
( f ∗ g ) ( i ) = ∑ a f ( a ) g ( i − z ) (f * g)({i})=\sum_a f({a}) g({i-z}) (fg)(i)=af(a)g(iz)
对于二维张量,则为f的索引 ( a , b ) (a,b) (a,b)和g的索引 ( i = a , j − b ) (i=a,j-b) (i=a,jb)上的对应加和:
( f ∗ g ) ( i , j ) = ∑ a ∑ b f ( a , b ) g ( i − a , j − b ) (f * g)({i,j})=\sum_a \sum_b f({a,b}) g({i-a,j-b}) (fg)(i,j)=abf(a,b)g(ia,jb)

1.4 通道

但是一张图像一般包括三个通道(红绿蓝),因此实际上的图像并不是二维张量,而是一个由高度、宽度和颜色组成的三维张量,比如 1024 × 1024 × 3 1024\times1024\times 3 1024×1024×3个像素。前两个轴与像素的空间位置有关,第三个轴可以看作每个像素的多维表示。因此需要将X索引的 [ X ] i , j , k [X]_{i,j,k} [X]i,j,k相应的调整为 [ V ] a , b , c [V]_{a,b,c} [V]a,b,c,而非之前所说的 [ V ] a , b [V]_{a,b} [V]a,b
此外,由于输入图像是三维的,隐藏表示H最好也采用三维张量。这样一组隐藏表示可以想象成一些互相堆叠的二维网格。因此我们可以把隐藏表示想象为一系列具有二维张量的通道(channel),也被称为特征映射(feature maps),因为每个通道都向后续层提供一组空间化的学习特征。直观上可以想象在靠近输入的底层,一些通道专门识别边缘,而一些通道专门识别纹理。
为了支持输入X和隐藏表示H的多个通道,可以在V中增加第四个坐标,即 [ V ] a , b , c , d [V]_{a,b,c,d} [V]a,b,c,d。综上,此时有:
[ H ] i , j , d = u + ∑ a = − Δ Δ ∑ b = − Δ Δ ∑ c [ V ] a , b , c , d [ X ] i + a , j + b , c \begin{aligned} {[\mathbf{H}]_{i, j,d} } & =u+\sum_{a=- \Delta}^{\Delta} \sum_{b=- \Delta}^{\Delta } \sum_c[\mathbf{V}]_{a,b,c,d}[\mathbf{X}]_{i+a,j+b,c} \end{aligned} [H]i,j,d=u+a=ΔΔb=ΔΔc[V]a,b,c,d[X]i+a,j+b,c
其中隐藏表示H中的索引d表示输出通道,而随后的输出将继续以三维张量H作为输入进入下一个卷积层,所以以上公式可以定义具有多个通道的卷积层,其中V是该卷积层的权重。

2.图像卷积

2.1 互相关运算

严格来说,卷积层是个错误的叫法,因为它所表达的运算其实是互相关运算(cross-correlation),而不是卷积运算。 根据上节中的描述,在卷积层中,输入张量和核张量通过互相关运算产生输出张量。
首先,我们暂时忽略通道(第三维)这一情况,看看如何处理二维图像数据和隐藏表示。在下图中,输入是高度为3、宽度为3的二维张量(即形状为3 * 3)。卷积核的高度和宽度都是2,而卷积核窗口(或卷积窗口)的形状由内核的高度和宽度决定(即2 * 2)。
在这里插入图片描述

在二维互相关运算中,卷积窗口从输入张量的左上角开始,从左到右、从上到下滑动。 当卷积窗口滑动到新一个位置时,包含在该窗口中的部分张量与卷积核张量进行按元素相乘,得到的张量再求和得到一个单一的标量值,由此我们得出了这一位置的输出张量值。 在如上例子中,输出张量的四个元素由二维互相关运算得到,这个输出高度为2
、宽度为2.
注意,输出大小略小于输入大小。这是因为卷积核的宽度和高度大于1, 而卷积核只与图像中每个大小完全适合的位置进行互相关运算。 所以,输出大小等于输入大小 n h × n ω n_h \times n_{\omega} nh×nω减去 k h × k ω k_h \times k_{\omega} kh×kω,即:
( n h − k h + 1 ) × ( n ω − k ω + 1 ) (n_h-k_h+1) \times (n_{\omega}-k_{\omega}+1) (nhkh+1)×(nωkω+1)这是因为我们需要足够的空间在图像上“移动”卷积核。接下来,我们在corr2d函数中实现如上过程,该函数接受输入张量X和卷积核张量K,并返回输出张量Y。

import torch
from torch import nn

def corr2d(X, K):
    #计算二维互相关运算
    h, w = K.shape
    Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w +1))
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            Y[i,j] = (X[i:i + h, j:j + w] * K).sum()
    return Y

X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
corr2d(X, K)

输出结果为:

tensor([[19., 25.],
        [37., 43.]])

2.2 卷积层

卷积层对输入和卷积核权重进行互相关运算,并在添加标量偏置后产生输出。所以卷积层中的两个被训练的参数是卷积核权重标量偏置。在训练基于卷积层的模型时,随机初始化卷积核权重。
基于上面定义的corr2d函数实现二维卷积层。在__init__构造函数中,将weight和bias声明为两个模型参数。前向传播函数调用corr2d函数并添加偏置。

#卷积层
class Conv2D(nn.Module):
    def __init__(self, kernal_size):
        super().__init__()
        self.weight = nn.Parameter(torch.rand(kernal_size))
        self.bias = nn.Parameter(torch.zeros(1))

    def forward(self, x):
        return corr2d(x, self.weight) + self.bias

高度和宽度分别为 h h h ω \omega ω的卷积核可以被称为 h × ω h\times \omega h×ω卷积。

2.3 图像中目标的边缘检测

如下是卷积层的一个简单应用:通过找到像素变化的位置,来检测图像中不同颜色的边缘。 首先,我们构造一个像素的黑白图像。中间四列为黑色(0),其余像素为白色(1)。

X = torch.ones((6, 8))
X[:, 2:6] = 0
X

输出结果为:

tensor([[1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.],
        [1., 1., 0., 0., 0., 0., 1., 1.]])

接下来我们构造一个高度为1,宽度为2的卷积核k,当进行互相关运算时,如果水平相邻的两元素相同,则输出为零,否则输出为非零。随后对参数X(输入)和K(卷积核)执行互相关运算。 如下所示,输出Y中的1代表从白色到黑色的边缘,-1代表从黑色到白色的边缘,其他情况的输出为0。

K = torch.tensor([[1.0, -1.0]])

Y = corr2d(X, K)
Y

输出结果为:

tensor([[ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.],
        [ 0.,  1.,  0.,  0.,  0., -1.,  0.]])

现在我们将输入的二维图像转置,再进行如上的互相关运算。 其输出如下,之前检测到的垂直边缘消失了。 不出所料,这个卷积核K只可以检测垂直边缘,无法检测水平边缘。

corr2d(X.t(), K)

输出结果为:

tensor([[0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.]])

2.4 卷积核

如果我们只需寻找黑白边缘,那么以上[1, -1]的边缘检测器足以。然而,当有了更复杂数值的卷积核,或者连续的卷积层时,我们不可能手动设计滤波器。因此可以尝试由X生成Y的卷积核。**首先构造一个卷积层,并将其卷积核初始化为随机张量。**接下来,在每次迭代中,**比较Y与卷积层输出的平方误差,然后计算梯度来更新卷积核。**为了简单起见,在此使用内置的二维卷积层,并忽略偏置。

#构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias = False)

# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1

X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
lr = 3e-2  # 学习率

for i in range(10):
    Y_hat = conv2d(X)
    l = (Y_hat - Y) ** 2
    conv2d.zero_grad()
    l.sum().backward()
    #迭代卷积核
    conv2d.weight.data[:] -= lr * conv2d.weight.grad
    if (i+1) % 2 ==0:
        print(f'epoch {i+1}, loss{l.sum():.3f}')

输出结果为:

epoch 2, loss7.652
epoch 4, loss1.294
epoch 6, loss0.222
epoch 8, loss0.039
epoch 10, loss0.007

在10次迭代之后,误差已经降到足够低。此时查看卷积核的权重张量。

conv2d.weight.data.reshape((1, 2))

输出结果为:

tensor([[ 0.9820, -0.9893]])

可以看到,此时卷积核的权重非常接近之前定义的卷积核K。

3.填充和步幅

由上节内容可知,卷积的输出形状取决于输入形状和卷积核的形状。事实上,填充(padding)和步幅(stride)也会影响输出的大小。

3.1 填充

在应用多层卷积时,我们常常丢失边缘像素。 由于我们通常使用小卷积核,因此对于任何单个卷积,我们可能只会丢失几个像素。 但随着我们应用许多连续卷积层,累积丢失的像素数就多了。 解决这个问题的简单方法即为填充(padding):在输入图像的边界填充元素(通常填充元素是0)。 例如,在下图中,将3 * 3输入填充到5 * 5,此时的输出就增加为4 * 4.阴影部分是第一个输出元素以及用于计算的输入和核张量元素: 0 × 0 + 0 × 1 + 0 × 2 + 0 × 3 = 0 0 \times 0 + 0 \times 1 + 0 \times 2 + 0 \times 3 = 0 0×0+0×1+0×2+0×3=0
在这里插入图片描述

卷积神经网络中卷积核的高度和宽度通常为奇数,例如1、3、5或7。 选择奇数的好处是,保持空间维度的同时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列。
此外,使用奇数的核大小和填充大小也提供了书写上的便利。对于任何二维张量X,当满足:

    1. 卷积核的大小是奇数;
    1. 所有边的填充行数和列数相同;
    1. 输出与输入具有相同高度和宽度

则可以得出:输出Y[i, j]是通过以输入X[i, j]为中心,与卷积核进行互相关计算得到的。
比如,在下面的例子中,我们创建一个高度和宽度为3的二维卷积层,并在所有侧边填充1个像素。给定高度和宽度为8的输入,则输出的高度和宽度也是8。

# 为了方便起见,我们定义了一个计算卷积层的函数。
# 此函数初始化卷积层权重,并对输入和输出提高和缩减相应的维数

def comp_conv2d(conv2d, X):
    # 这里的(1,1)表示批量大小和通道数都是1
    X = X.reshape((1, 1) + X.shape)
    Y = conv2d(X)
    # 省略前两个维度:批量大小和通道
    return Y.reshape(Y.shape[2:])

# 请注意,这里每边都填充了1行或1列,因此总共添加了2行或2列
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
comp_conv2d(conv2d, X).shape

输出结果为:

torch.Size([8, 8])

当卷积核的高度和宽度不同时,我们可以填充不同的高度和宽度,使输出和输入具有相同的高度和宽度。在如下示例中,我们使用高度为5,宽度为3的卷积核,高度和宽度两边的填充分别为2和1。

conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape

输出结果为:

torch.Size([8, 8])

3.2 步幅

在计算互相关时,卷积窗口从输入张量的左上角开始,向下、向右滑动。 在前面的例子中,我们默认每次滑动一个元素。 但是,有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。将每次滑动元素的数量称为步幅(stride)。下图即展示了一个垂直步幅为3,水平步幅为2的二维互相关运算。
在这里插入图片描述

conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape

输出结果为:

torch.Size([4, 4])

4.多输入多输出通道

到目前为止,我们仅展示了单个输入和单个输出通道的简化例子。 这使得我们可以将输入、卷积核和输出看作二维张量。当我们添加通道时,我们的输入和隐藏的表示都变成了三维张量。例如,每个RGB输入图像具有 3 × g × ω 3\times g \times \omega 3×g×ω的形状。我们将这个大小为3的轴称为通道(channel)维度

4.1 多输入通道

当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。假设输入的通道数为 c i c_i ci,那么卷积核的输入通道数也需要为 c i c_i ci
如果卷积核的窗口形状是 k h × k ω k_h \times k_{\omega} kh×kω,那么当 c i = 1 c_i=1 ci=1时,我们可以把卷积核看作形状为 k h × k ω k_h \times k_{\omega} kh×kω的二维张量。
c i > 1 c_i>1 ci>1时,卷积核的每个输入通道将包含形状为 k h × k ω k_h \times k_{\omega} kh×kω的张量,将这些张量 c i c_i ci连结在一起可以得到形状为 c i × k h × k ω c_i \times k_h \times k_{\omega} ci×kh×kω的卷积核。由于输入和卷积核都有 c i c_i ci个通道,此时可以对每个通道输入的二维张量和卷积核的二维张量进行互相关运算,再对通道求和(将 c i c_i ci的结果相加)得到二维张量。这是多通道输入和多输入通道卷积核之间进行二维互相关运算的结果。下图即是一个具有两个输入通道的二维互相关运算的示例。
在这里插入图片描述

示例代码如下:

import torch

def corr2d(x, k):
    h, w = k.shape
    y = torch.zeros((x.shape[0] - h + 1, x.shape[1] - w + 1))
    for i in range(y.shape[0]):
        for j in range(y.shape[1]):
            y[i, j] = torch.sum(x[i:i + h, j:j + w] * k)
    return y

def corr2d_multi_in(X, K):
    # 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起
    return sum(corr2d(x, k) for x, k in zip(X, K))
    
X = torch.tensor([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
               [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])

corr2d_multi_in(X, K)

输出结果为:

tensor([[ 56.,  72.],
        [104., 120.]])

4.2 多输出通道

在最流行的神经网络架构中,随着神经网络层数的加深,常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。直观地说,可以将每个通道看作对不同特征的响应。而现实可能更为复杂一些,因为每个通道不是独立学习的,而是为了共同使用而优化的。因此,多输出通道并不仅是学习多个单通道的检测器。
c i c_i ci c o c_o co分别表示输入和输出通道的数目,并让 k h k_h kh k ω k_{\omega} kω为卷积核的高度和宽度。为获得多个通道的输出,为每个输出通道创建一个形状为 c i × k h × k ω c_i \times k_h \times k_{\omega} ci×kh×kω的卷积核张量,这样卷积核的形状为 c o × c i × k h × k ω c_o \times c_i \times k_h \times k_{\omega} co×ci×kh×kω。在互相关运算中,每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果。
如下代码所示,实现一个计算多个通道的输出的互相关函数:

#计算多个通道的输出的互相关函数。
def corr2d_multi_in_out(X, K):
     # 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
    # 最后将所有结果都叠加在一起
    return np.stack([corr2d_multi_in(X, k) for k in K], 0)

K = torch.stack((K, K + 1, K + 2), 0)
K.shape

输出结果为:

torch.Size([3, 2, 2, 2])

然后对输入张量X与卷积核张量K执行互相关运算。现在的输出包含3个通道,第一个通道的结果与先前输入张量X和多输入单输出通道的结果一致。

corr2d_multi_in_out(X, K)

输出结果为:

tensor([[[ 56.,  72.],
         [104., 120.]],

        [[ 76., 100.],
         [148., 172.]],

        [[ 96., 128.],
         [192., 224.]]])

4.3 1 × 1 1 \times 1 1×1卷积

1 × 1 1 \times 1 1×1卷积,即 k h = k ω = 1 k_h=k_{\omega}=1 kh=kω=1。此时由于使用了最小窗口, 1 × 1 1 \times 1 1×1卷积失去了卷积层的特有能力(即在高度和宽度维度上,识别相邻元素间相互作用的能力。)其实 1 × 1 1 \times 1 1×1卷积的唯一计算发生在通道上。
下图展示了使用 1 × 1 1 \times 1 1×1卷积核与3个输入通道和2个输出通道的互相关运算。
在这里插入图片描述

这里输入和输出具有相同的高度和宽度,输出中的每个元素都是从输入图像中同一位置的元素的线性组合。可以将 1 × 1 1 \times 1 1×1卷积层看作在每个像素位置应用的全连接层,以 c i c_i ci个输入值转换为 c o c_o co个输出值。因为这仍是一个卷积层,所以跨像素的权重是一致的。同时 1 × 1 1 \times 1 1×1卷积层需要的权重维度为 c o × c i c_o \times c_i co×ci,再额外加上一个偏置。
下面是用全连接层实现 1 × 1 1 \times 1 1×1卷积,此时需要对输入和输出的数据形状进行调整。

def corr2d_multi_in_out_1x1(X, K):
    c_i, h, w = X.shape
    c_o = K.shape[0]
    X = X.reshape((c_i, h * w))
    K = K.reshape((c_o, c_i))
    #全连接层中的矩阵乘法
    Y = torch.matmul(K, X)
    return Y.reshape((c_o, h, w))

X = torch.normal(0 ,1, (3, 3, 3))
K = torch.normal(0, 1, (2, 3, 1, 1))

Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
assert float(torch.abs(Y1 - Y2).sum()) < 1e-6

5.池化层

通常当我们处理图像时,我们希望逐渐降低隐藏表示的空间分辨率、聚集信息,这样随着我们在神经网络中层叠的上升,每个神经元对其敏感的感受野(输入)就越大。而我们的机器学习任务通常会跟全局图像的问题有关(例如,“图像是否包含一只猫呢?”),所以我们最后一层的神经元应该对整个输入的全局敏感。通过逐渐聚合信息,生成越来越粗糙的映射,最终实现学习全局表示的目标,同时将卷积图层的所有优势保留在中间层。此外,当检测较底层的特征时,我们通常希望这些特征保持某种程度上的平移不变性。由此引入汇聚(pooling)层,也叫池化层,它具有双重目的:降低卷积层对位置的敏感性,同时降低对空间采样表示的敏感性。

5.1 最大池化层和平均池化层

与卷积层类似,池化层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口(有时称为池化窗口)遍历的每个位置计算一个输出。 然而,不同于卷积层中的输入与卷积核之间的互相关计算,池化层不包含参数。 相反,池运算是确定性的,我们通常计算池化窗口中所有元素的最大值或平均值。这些操作分别称为最大池化层(maximum pooling)和平均池化层(average pooling)。
在这两种情况下,与互相关运算符一样,池化窗口从输入张量的左上角开始,从左往右、从上往下的在输入张量内滑动。在池化窗口到达的每个位置,它计算该窗口中输入子张量的最大值或平均值。计算最大值或平均值是取决于使用了最大池化层还是平均池化层。
在这里插入图片描述

在下面的代码中的pool2d函数,我们实现池化层的前向传播。 但这里没有卷积核,输出为输入中每个区域的最大值或平均值。

def pool2d(X, pool_size, mode='max'):
    p_h, p_w = pool_size
    Y = torch.zeros((X.shape[0] - p_h + 1), X.shape[1] - p_w + 1)
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            if mode == 'max':
                Y[i, j] = X[i: i+ p_h, j: j + p_w].max()
            elif mode == 'avg':
                Y[i, j] = X[i: i +p_h, j: j + p_w].mean()
    return Y

#验证最大池化层的输出
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))

输出结果为:

tensor([[4., 5.],
        [7., 8.]])

验证平均池化层为:

pool2d(X, (2, 2), 'avg')

输出得到:

tensor([[2., 3.],
        [5., 6.]])

5.2 填充和步幅

与卷积层一样,池化层也可以改变输出形状。可以通过填充和步幅以获得所需的输出形状。 下面,用深度学习框架中内置的二维最大池化层,来演示池化层中填充和步幅的使用。

#构造一个输入张量X,它有四个维度,其中样本数和通道数都是1
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))

#默认情况下,深度学习框架中的步幅与汇聚窗口的大小相同
#使用形状为(3, 3)的池化窗口
pool2d = nn.MaxPool2d(3)

#填充和步幅可以手动设定
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)

最后得到:

tensor([[[[ 5.,  7.],
          [13., 15.]]]])

5.3 多个通道

在处理多通道输入数据时,池化层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。 这意味着池化层的输出通道数与输入通道数相同。 下面,将在通道维度上连结张量X和X + 1,以构建具有2个通道的输入。

X = torch.cat((X, X + 1), 1)
X

得到:

tensor([[[[ 0.,  1.,  2.,  3.],
          [ 4.,  5.,  6.,  7.],
          [ 8.,  9., 10., 11.],
          [12., 13., 14., 15.]],

         [[ 1.,  2.,  3.,  4.],
          [ 5.,  6.,  7.,  8.],
          [ 9., 10., 11., 12.],
          [13., 14., 15., 16.]]]])

进行池化:

pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)

可以发现输出通道的数量仍然是2:

tensor([[[[ 5.,  7.],
          [13., 15.]],

         [[ 6.,  8.],
          [14., 16.]]]])

6.卷积神经网络(LeNet)

LeNet是最早发布的卷积神经网络之一,因其在计算机视觉任务中的高效性能而受到广泛关注。这个模型是由AT&T贝尔实验室的研究员Yann LeCun在1989年提出的(并以其命名),目的是识别图像中的手写数字。 当时,Yann LeCun发表了第一篇通过反向传播成功训练卷积神经网络的研究,这项工作代表了十多年来神经网络研究开发的成果。

6.1 模型框架

总体来看,LeNet(LeNet-5)由两个部分组成:

  • 卷积编码器:由两个卷积层组成;
  • 全连接层密集块:由三个全连接层组成
    具体框架如下图所示:
    在这里插入图片描述

**每个卷积块中的基本单元是一个卷积层、一个sigmoid激活函数和平均池化层。**虽然ReLU和最大池化层更有效,但它们在20世纪90年代还没有出现。每个卷积层使用 5 × 5 5\times 5 5×5卷积核 和一个 sigmoid激活函数。这些层将输入映射到多个二维特征输出,通常同时增加通道的数量。第一卷积层有6个输出通道,而第二个卷积层有16个输出通道。每个 2 × 2 2 \times 2 2×2池操作(步幅2)通过空间下采样将维数减少4倍。卷积的输出形状由批量大小、通道数、高度、宽度决定。
为了将卷积块的输出传递给稠密块,必须在小批量中展平每个样本。换言之,将这个四维输入转换成全连接层所期望的二位输入,这里的二维表示的第一个维度索引小批量中的样本,第二个维度给出每个样本的平面向量展示。LeNet的稠密块有三个全连接层,分别有120、84、10个输出。在分类任务中,输出层的10维对应于最后输出结果的数量。
以下为LeNet的示例代码,需要实例化一个Sequential块并将需要的层连接在一起。同时将一个大小为28 * 28的单通道(黑白)图像通过LeNet。操作思想如下:
在这里插入图片描述

import torch
from torch import nn

net = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Flatten(),
    nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
    nn.Linear(120, 84), nn.Sigmoid(),
    nn.Linear(84, 10))

X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
for layer in net:
    if isinstance(layer, nn.Sigmoid) or isinstance(layer, nn.AvgPool2d) or isinstance(layer, nn.Flatten):
        print(layer.__class__.__name__, 'output shape: \t', X.size())
    X = layer(X)
    if isinstance(layer, nn.Conv2d) or isinstance(layer, nn.Linear):
        print(layer.__class__.__name__, 'output shape: \t', X.size())

输出得到:

Conv2d output shape: 	 torch.Size([1, 6, 28, 28])
Sigmoid output shape: 	 torch.Size([1, 6, 28, 28])
AvgPool2d output shape:  torch.Size([1, 6, 28, 28])
Conv2d output shape: 	 torch.Size([1, 16, 10, 10])
Sigmoid output shape: 	 torch.Size([1, 16, 10, 10])
AvgPool2d output shape:  torch.Size([1, 16, 10, 10])
Flatten output shape: 	 torch.Size([1, 16, 5, 5])
Linear output shape: 	 torch.Size([1, 120])
Sigmoid output shape: 	 torch.Size([1, 120])
Linear output shape: 	 torch.Size([1, 84])
Sigmoid output shape: 	 torch.Size([1, 84])
Linear output shape: 	 torch.Size([1, 10])

可以看到,在整个卷积块中,与上一层相比,每一层特征的高度和宽度都减小了。第一个卷积层使用2个像素的填充,来补偿5 * 5卷积核导致的特征减少。相反,第二个卷积层没有填充,因此高度和宽度都减少了4个像素。随着层叠的上升,通道的数量从输入时的1个增加到第一个卷积层之后的6个,再到第二个卷积层之后的16个。同时,每个池化层的高度和宽度都减半。最后每个全连接层减少维数,最终输出一个维数与结果分类数相匹配的输出。

6.2 模型训练

现在我们已经实现了LeNet,让我们看看LeNet在Fashion-MNIST数据集上的表现。

import torch
import torchvision
from torch.utils import data
from torchvision import transforms
import matplotlib.pyplot as plt
import numpy as np
import time
from IPython import display

def get_dataloader_workers():
    return 4

def load_data_fashion_mnist(batch_size, resize=None):
    trans = [transforms.ToTensor()]
    if resize:
        trans.insert(0, transforms.Resize(resize))
    trans = transforms.Compose(trans)
    mnist_train = torchvision.datasets.FashionMNIST(
        root="F:/FFAXXTTY/diveintodl/chapter3", train=True, transform=trans, download=True)
    mnist_test = torchvision.datasets.FashionMNIST(
        root="F:/FFAXXTTY/diveintodl/chapter3", train=False, transform=trans, download=True)
    return (data.DataLoader(mnist_train, batch_size, shuffle=True,
                            num_workers=get_dataloader_workers()),
            data.DataLoader(mnist_test, batch_size, shuffle=False,
                            num_workers=get_dataloader_workers()))

class Accumulator:  #@save
    """在n个变量上累加"""
    def __init__(self, n):
        self.data = [0.0] * n

    def add(self, *args):
        self.data = [a + float(b) for a, b in zip(self.data, args)]

    def reset(self):
        self.data = [0.0] * len(self.data)

    def __getitem__(self, idx):
        return self.data[idx]
    
def accuracy(y_hat, y):
    if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
        y_hat = y_hat.argmax(axis = 1)
    cmp = y_hat.type(y.dtype) == y
    return float(cmp.type(y.dtype).sum())

class Animator:
    def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None, ylim=None, xscale='linear', yscale='linear',
                fmts=('-', 'm--', 'g-', 'r:'), nrows=1, ncols=1, figsize=(3.5, 2.5)):
        if legend is None:
            legend = []
            
        self.fig, self.axes = plt.subplots(nrows, ncols, figsize=figsize)
        if nrows * ncols == 1:
            self.axes = [self.axes,]

        # self.config_axes = lambda:self.set_axes(
        #     self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
        self.X, self.Y, self.fmts = None, None, fmts
        self.set_axes(xlabel, ylabel, xlim, ylim, xscale, yscale, legend)

    def set_axes(self, xlabel, ylabel, xlim, ylim, xscale, yscale, legend):
        for ax in self.axes:
            ax.set_xlabel(xlabel)
            ax.set_ylabel(ylabel)
            ax.set_xscale(xscale)
            ax.set_yscale(yscale)
            ax.set_xlim(xlim)
            ax.set_ylim(ylim)
            if legend:
                ax.legend(legend)
            ax.grid()
            
    def add(self, x, y):
        if not hasattr(y, '__len__'):
            y = [y]
        n = len(y)
        if not hasattr(x, "__len__"):
            x = [x] * n
        if not self.X:
            self.X = [[] for _ in range(n)]
        if not self.Y:
            self.Y = [[] for _ in range(n)]
        for i, (a, b) in enumerate(zip(x, y)):
            if a is not None and b is not None:
                self.X[i].append(a)
                self.Y[i].append(b)

        for ax in self.axes:
            ax.cla()
            for x, y, fmt in zip(self.X, self.Y, self.fmts):
                ax.plot(x, y, fmt)
        display.display(self.fig)
        display.clear_output(wait=True)

class Timer:
    def __init__(self):
        self.times = []
        self.start()

    def start(self):
        self.tik = time.time()

    def stop(self):
        self.times.append(time.time() - self.tik)
        return self.times[-1]
    
    def avg(self):
        return sum(self.times) / len(self.times)
    
    def sum(self):
        return sum(self.times)
    
    def cumsum(self):
        return np.array(self.times).cumsum().tolist()
    
def try_gpu(i=0):
    if torch.cuda.device_count() >= i + 1:
        return torch.device(f'cuda:{i}')
    return torch.device('cpu')

#模型训练
batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size=batch_size)

def evaluate_accuracy_gpu(net, data_iter, device=None):
    if isinstance(net, nn.Module):
        net.eval() #s设置为评估模式
        if not device:
            device = next(iter(net.parameters())).device
    #正确预测的数量,总预测的数量
    metric  = Accumulator(2)
    with torch.no_grad():
        for X, y in data_iter:
            if isinstance(X, list):
                X = [x.to(device) for x in X]
            else:
                X = X.to(device)
            y = y.to(device)
            metric.add(accuracy(net(X), y), y.numel())
    return metric[0] / metric[1]

def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
    def init_weights(m):
        if type(m) == nn.Linear or type(m) == nn.Conv2d:
            nn.init.xavier_uniform_(m.weight)
    net.apply(init_weights)
    print('training on', device)
    net.to(device)
    optimizer = torch.optim.SGD(net.parameters(), lr = lr)
    loss = nn.CrossEntropyLoss()
    animator = Animator(xlabel='epoch', xlim=[1, num_epochs], legend=['train loss', 'train acc', 'test acc'])

    timer, num_batches = Timer(), len(train_iter)
    for epoch in range(num_epochs):
        metric = Accumulator(3)
        net.train()
        for i, (X, y) in enumerate(train_iter):
            timer.start()
            optimizer.zero_grad()
            X, y = X.to(device), y.to(device)
            y_hat = net(X)
            l = loss(y_hat, y)
            l.backward()
            optimizer.step()
            with torch.no_grad():
                metric.add(l * X.shape[0], accuracy(y_hat, y), X.shape[0])
            timer.stop()
            train_l = metric[0] / metric[2]
            train_acc = metric[1] / metric[2]
            if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
                animator.add(epoch + (i + 1) / num_batches, (train_l, train_acc, None))
        test_acc = evaluate_accuracy_gpu(net, test_iter)
        animator.add(epoch + 1, (None, None, test_acc))
    print(f'loss {train_l: .3f}, train_acc{train_acc: .3f},' f'test acc{test_acc:.3f}')
    print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec 'f'on {str(device)}')

lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, try_gpu())

最终输出结果为:

loss  0.465, train_acc 0.826,test acc0.810
49495.8 examples/sec on cuda:0

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值