动手学深度学习笔记day9

卷积神经网络(convolutional neural networks,CNN)

图像不是二维张量,而是一个由高度、宽度和颜色组成的三维张量,比如包含个像素。 前两个轴与像素的空间位置有关,而第三个轴可以看作是每个像素的多维表示。

互相关运算

互相关运算(cross-correlation):在卷积层中,输入张量和核张量通过互相关运算产生输出张量。
在 图中,输入是高度为3、宽度为3的二维张量(即形状为3×3)。卷积核的高度和宽度都是2,而卷积核窗口(或卷积窗口)的形状由内核的高度和宽度决定(即2×2)。
动手学深度学习笔记day9_第1张图片

阴影部分是第一个输出元素,以及用于计算输出的输入张量元素和核张量元素:0×0+1×1+3×2+4×3=19。卷积窗口从输入张量的左上角开始,从左到右、从上到下滑动。

输出大小略小于输入大小。这是因为卷积核的宽度和高度大于1, 而卷积核只与图像中每个大小完全适合的位置进行互相关运算。 所以,输出大小等于输入大小nh×nw减去卷积核大小kh×kw,即:
在这里插入图片描述
接下来,我们在corr2d函数中实现如上过程,该函数接受输入张量X和卷积核张量K,并返回输出张量Y。

import torch
from torch import nn
from d2l import torch as d2l

def corr2d(X, K):  #@save
    """计算二维互相关运算"""
    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.]])

卷积层

卷积层对输入和卷积核权重进行互相关运算,并在添加标量偏置之后产生输出。 所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。 就像我们之前随机初始化全连接层一样,在训练基于卷积层的模型时,我们也随机初始化卷积核权重。

class Conv2D(nn.Module):
    def __init__(self, kernel_size):
        super().__init__()
        self.weight = nn.Parameter(torch.rand(kernel_size))
        self.bias = nn.Parameter(torch.zeros(1))

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

图像中目标的边缘检测
通过找到像素变化的位置,来检测图像中不同颜色的边缘。 首先,我们构造一个6×8像素的黑白图像。中间四列为黑色(0),其余像素为白色(1)。

X = torch.ones((6, 8))
X[:, 2:6] = 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只可以检测垂直边缘,无法检测水平边缘。

学习卷积核

那么我们是否可以学习由X生成Y的卷积核呢?
现在让我们看看是否可以通过仅查看“输入-输出”对来学习由X生成Y的卷积核。 我们先构造一个卷积层,并将其卷积核初始化为随机张量。接下来,在每次迭代中,我们比较Y与卷积层输出的平方误差,然后计算梯度来更新卷积核。为了简单起见,我们在此使用内置的二维卷积层,并忽略偏置。

那么以上面的[1, -1]的边缘检测器为例子

# 构造一个(内置函数)二维卷积层,它具有1个输入通道,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}')

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

输出是tensor([[ 0.9879, -0.9993]]),我们学习到的卷积核权重非常接近我们之前定义的卷积核K。

互相关和卷积运算之间的对应关系

之前:这个卷积核K只可以检测垂直边缘,无法检测水平边缘。
它们差别不大,我们只需水平和垂直翻转二维卷积核张量,然后对输入张量执行互相关运算。
由于卷积核是从数据中学习到的,因此无论这些层执行严格的卷积运算还是互相关运算,卷积层的输出都不会受到影响。我们将继续把“互相关运算”称为卷积运算。

填充和步幅

填充

如上所述,在应用多层卷积时,我们常常丢失边缘像素。 由于我们通常使用小卷积核,因此对于任何单个卷积,我们可能只会丢失几个像素。 但随着我们应用许多连续卷积层,累积丢失的像素数就多了。 解决这个问题的简单方法即为填充(padding):在输入图像的边界填充元素(通常填充元素是0)。
我们将输入3×3填充到5×5,那么它的输出就增加为4×4。阴影部分是第一个输出元素以及用于输出计算的输入和核张量元素:
0×0+0×1+0×2+0×3=0 。
动手学深度学习笔记day9_第2张图片
如果我们添加ph行(一共)填充(大约一半在顶部,一半在底部)和pw列填充(左侧大约一半,右侧一半),则输出形状将为(4-2+2+1)×(4-2+2+1)


这意味着输出的高度和宽度将分别增加和。
在许多情况下,我们需要设置ph=kh-1和pw=kw-1,使输入和输出具有相同的高度和宽度。 这样可以在构建网络时更容易地预测每个图层的输出形状。假设是kh奇数,我们将在高度的两侧填充ph/2行。 如果是kh偶数,则一种可能性是在输入顶部填充┏ph/2┒行,在底部填充┗ph/2┚行。同理,我们填充宽度的两侧。
卷积神经网络中卷积核的高度和宽度通常为奇数,例如1、3、5或7。

在下面的例子中,我们创建一个高度和宽度为3的二维卷积层,并在所有侧边填充1个像素。给定高度和宽度为8的输入,则输出的高度和宽度也是8。

import torch
from torch import nn


# 为了方便起见,我们定义了一个计算卷积层的函数。
# 此函数初始化卷积层权重,并对输入和输出提高和缩减相应的维数
def comp_conv2d(conv2d, X):
    # 这里的(1,1)表示批量大小和通道数都是1
    X = X.reshape((1, 1) + X.shape)	
    # X.shape为[8,8]		(1, 1) + X.shape为[1,1,8,8]
    Y = conv2d(X)  #Y.shape为[1,1,8,8]
    # 省略前两个维度:批量大小和通道
    return Y.reshape(Y.shape[2:])		 #为[8,8]	

# 请注意,这里每边都填充了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])

小结: padding=1上下左右都填充了1行,总共增加了2行2列
padding=(2, 1)上下都填充了2行,总共增加了4行,左右都填充了1行,总共增加了2列

步幅

我们将每次滑动元素的数量称为步幅(stride)。到目前为止,我们只使用过高度或宽度为1的步幅,那么如何使用较大的步幅呢? 图是垂直步幅为3,水平步幅为2的二维互相关运算。 着色部分是输出元素以及用于输出计算的输入和内核张量元素:0×1+1×2+2×3=8、0×1+6×1+0×2+0×3=6。
动手学深度学习笔记day9_第3张图片
可以看到,为了计算输出中第一列的第二个元素和第一行的第二个元素,卷积窗口分别向下滑动三行和向右滑动两列。但是,当卷积窗口继续向右滑动两列时,没有输出,因为输入元素无法填充窗口(除非我们添加另一列填充)。
动手学深度学习笔记day9_第4张图片

conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape
#torch.Size([2, 2])

默认情况下,填充为0,步幅为1。在实践中,我们很少使用不一致的步幅或填充,也就是说,我们通常有ph=pw和sh=sw。

小结
填充可以增加输出的高度和宽度。这常用来使输出与输入具有相同的高和宽。
步幅可以减小输出的高和宽,例如输出的高和宽仅为输入的高和宽的1/n(n是一个大于的整数)。
填充和步幅可用于有效地调整数据的维度。

多输入多输出通道

多输入通道

当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。假设输入的通道数为ci,那么卷积核的输入通道数也需要为ci。

当ci>1时,我们卷积核的每个输入通道将包含形状为kh×kw的张量。将这些张量连结在一起可以得到形状为的卷积核ci×kh×kw。由于输入和卷积核都有ci个通道,我们可以对每个通道输入的二维张量和卷积核的二维张量进行互相关运算,再对通道求和(将ci的结果相加)得到二维张量。这是多通道输入和多输入通道卷积核之间进行二维互相关运算的结果。

在 图中,我们演示了一个具有两个输入通道的二维互相关运算的示例。阴影部分是第一个输出元素以及用于计算这个输出的输入和核张量元素:
(1×1+2×2+4×3+5×4)+(0×0+1×1+3×2+4×3)=56
动手学深度学习笔记day9_第5张图片

import torch
from d2l import torch as d2l

def corr2d_multi_in(X, K):
    # zip(X, K) 先遍历“X”和“K”的第0个维度(通道维度),再把它们按元素求和加
    return sum(d2l.corr2d(x, k) for x, k in zip(X, K))
   
 #X.shape为[2, 3, 3],K.shape为[2, 2, 2],
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.]])

多输出通道

用ci和co分别表示输入和输出通道的数目,并让kh和kw为卷积核的高度和宽度。为了获得多个通道的输出,我们可以为每个输出通道创建一个形状为ci×kh×kw的卷积核张量,这样卷积核的形状是co×ci×kh×kw。在互相关运算中,每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果。

我们可以有多个三维卷积核,每个核生成一个输出通道
动手学深度学习笔记day9_第6张图片

动手学深度学习笔记day9_第7张图片

理解参考于多输出通道

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

#通过将核张量K与K+1(K中每个元素加1)和K+2连接起来,构造了一个具3有个输出通道的卷积核。
K = torch.stack((K, K + 1, K + 2), 0)
#k原本为tensor([[[0., 1.],[2., 3.]], [[1., 2.],  [3., 4.]]])
#构造k为tensor([[[[0., 1.], [2., 3.]],		 [[1., 2.], [3., 4.]]],		  [[[1., 2.],  [3., 4.]],	 
# [[2., 3.], [4., 5.]]],	 	 [[[2., 3.], [4., 5.]],		 [[3., 4.], [5., 6.]]]])

corr2d_multi_in_out(X, K)
#tensor([[[ 56.,  72.], [104., 120.]],
        [[ 76., 100.], [148., 172.]],
        [[ 96., 128.], [192., 224.]]])

1×1卷积层

1×1卷积,即kh=1,kw=1。1×1卷积失去了卷积层的特有能力——在高度和宽度维度上,识别相邻元素间相互作用的能力。 其实1×1卷积的唯一计算发生在通道上。

图展示了使用1×1卷积核与3个输入通道和2个输出通道的互相关计算。 这里输入和输出具有相同的高度和宽度,输出中的每个元素都是从输入图像中同一位置的元素的线性组合。 我们可以将卷积层看作是在每个像素位置应用的全连接层,以ci个输入值转换为co个输出值。
动手学深度学习笔记day9_第8张图片
使用全连接层实现1×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)
bool(float(torch.abs(Y1 - Y2).sum()) < 1e-6)		#True

小结
多输入多输出通道可以用来扩展卷积层的模型。
当以每像素为基础应用时,1×1)卷积层相当于全连接层。
1×1)卷积层通常用于调整网络层的通道数量和控制模型复杂性。

最大汇聚层(maximum pooling)和平均汇聚层(average pooling)

汇聚层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口(有时称为汇聚窗口)遍历的每个位置计算一个输出。 然而,不同于卷积层中的输入与卷积核之间的互相关计算,汇聚层不包含参数。 相反,池运算是确定性的,我们通常计算汇聚窗口中所有元素的最大值或平均值。这些操作分别称为最大汇聚层(maximum pooling)和平均汇聚层(average pooling)。
max(0,1,3,4)=4
动手学深度学习笔记day9_第9张图片
这里我们没有卷积核,输出为输入中每个区域的最大值或平均值。

import torch
from torch import nn
from d2l import torch as d2l

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.]])

填充和步幅

与卷积层一样,汇聚层也可以改变输出形状。和以前一样,我们可以通过填充和步幅以获得所需的输出形状。

默认情况下,深度学习框架中的步幅与汇聚窗口的大小相同。 因此,如果我们使用形状为(3, 3)的汇聚窗口,那么默认情况下,我们得到的步幅形状为(3, 3)。

X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
pool2d = nn.MaxPool2d(3)
pool2d(X)		#tensor([[[[10.]]]])

填充和步幅可以手动设定。

pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)		#tensor([[[[ 5.,  7.], [13., 15.]]]])

pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1))
pool2d(X)		#tensor([[[[ 5.,  7.], [13., 15.]]]])

多个通道

在处理多通道输入数据时,汇聚层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。 这意味着汇聚层的输出通道数与输入通道数相同。

X = torch.randn([1,2,4,4])
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
#tensor([[[[0.6247, 0.4828], [2.1504, 0.6395]], [[1.5552, 1.5552],  [1.5656, 1.5656]]]])
 m.shape  #torch.Size([1, 2, 2, 2])

小结
对于给定输入元素,最大汇聚层会输出该窗口内的最大值,平均汇聚层会输出该窗口内的平均值。
汇聚层的主要优点之一是减轻卷积层对位置的过度敏感。
我们可以指定汇聚层的填充和步幅。
使用最大汇聚层以及大于1的步幅,可减少空间维度(如高度和宽度)。
汇聚层的输出通道数与输入通道数相同。

笔记来源:动手学深度学习

你可能感兴趣的:(深度学习,神经网络,cnn)