深度学习模型——经典卷积神经网络(从概念到实现)【上】

参考来源:

动手深度学习(第二版)

视频推荐

【从“卷积”、到“图像卷积操作”、再到“卷积神经网络”,“卷积”意义的3次改变-哔哩哔哩】 https://b23.tv/ppuGbfa

这个b站up主将自己思考卷积网络的整个过程展示了出来,同时用吃饭后的消化过程来形象的说明了卷积公式的基本原理。无论对卷积的了解是多是少,总能从中得到收获。

科普:

图像一般包含三个通道/三种原色(红色、绿色和蓝色)。 实际上,图像不是二维张量,而是一个由高度、宽度和颜色组成的三维张量,比如包含1024×1024×3个像素。 前两个轴与像素的空间位置有关,而第三个轴可以看作每个像素的多维表示。 

一、从全连接层到卷积

我们在学习多层感知机后,已经可以实现特征的非线性变化,找到不同特征共同决定的结果函数。但是,这种模型的复杂度使得多层感知机在处理大规模的输入特征时会使用极大的GPU运行资源,存储时也将占用相当大的空间。例如我们有一个足够充分的照片数据集,数据集中是拥有标注的照片,每张照片具有百万级像素,这意味着网络的每次输入都有一百万个维度。 即使将隐藏层维度降低到1000,这个全连接层也将有10的6次方×10的3次方=10的9次方个参数。毫无疑问,这个代价是我们所不希望看到的。

所以,我们希望找到一种新模型可以减轻这种代价,卷积神经网络(convolutional neural networks,CNN)便因此诞生。卷积的理论基础建立在空间不变性(spatial invariance)这一基础概念上。例如我们想在一组照片中找到一个人的位置,我们的目光从左上角横着一行行找到右下角,最终我们能在照片的某一位置看到他。而找到他的依据只和他的样貌有关系,他的样子并不取决于他潜藏的地方,也就使得人们更进一步总结出两个规律:

  1. 平移不变性(translation invariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为“平移不变性”。

  2. 局部性(locality):神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。最终,可以聚合这些局部特征,以在整个图像级别进行预测

    用数学表达式我们可能会更直观得感受出来:

使用[X]i,j和[H]i,j分别表示输入图像和隐藏表示中位置(i,j)处的像素。为了使每个隐藏神经元都能接收到每个输入像素的信息,我们将参数从权重矩阵(如同我们先前在多层感知机中所做的那样)替换为四阶权重张量W。假设U包含偏置参数,我们可以将全连接层形式化地表示为

                   

从W到V的转换只是形式上的转换,因为在这两个四阶张量的元素之间存在一一对应的关系。 我们只需重新索引下标(k,l),使k=i+a、l=j+b,由此可得[V]i,j,a,b=[W]i,j,i+a,j+b。 索引a和b通过在正偏移和负偏移之间移动覆盖了整个图像。 对于隐藏表示中任意给定位置(i,j)处的像素值[H]i,j,可以通过在x中以(i,j)为中心对像素进行加权求和得到,加权使用的权重为[V]i,j,a,b。     

笔者认为,这里的对应与全连接层的实现有关系,无论是线性变换还是非线性变换,最后都可以通过用隐藏层的坐标(i,j)逆行工程加上中途中变换的数值变成刚输入时(k,l)的位置坐标,也就是i,j与k,j存在相关性。 

实现平移不变性:检测对象在输入X中的平移,应该仅导致隐藏表示H中的平移。也就是说,V和U实际上不依赖于(i,j)的值,即[V]i,j,a,b=[V]a,b。并且U是一个常数,比如u。因此,我们可以简化H定义为:

                          

这里去掉了新权重V的两个维度,代表了图像的识别与否与在H中的位置(i,j)无关。

实现局部性。如上所述,为了收集用来训练参数[H]i,j的相关信息,我们不应偏离到距(i,j)很远的地方。这意味着在|a|>Δ或|b|>Δ的范围之外,我们可以设置[V]a,b=0。因此,我们可以将[H]i,j重写为

                          

简而言之,局部性的实现就是给值设置了约束,这里的约束是反着约束的,所以称为卷积。

 上面的式子是一个卷积层(convolutional layer),而卷积神经网络是包含卷积层的一类特殊的神经网络。V被称为卷积核(convolution kernel)或者滤波器(filter),亦或简单地称之为该卷积层的权重,通常该权重是可学习的参数。 当图像处理的局部区域很小时,卷积神经网络与多层感知机的训练差异可能是巨大的:以前,多层感知机可能需要数十亿个参数来表示网络中的一层,而现在卷积神经网络通常只需要几百个参数,而且不需要改变输入或隐藏表示的维数。 参数大幅减少的代价是,我们的特征现在是平移不变的,并且当确定每个隐藏活性值时,每一层只包含局部的信息。 以上所有的权重学习都将依赖于归纳偏置。当这种偏置与现实相符时,我们就能得到样本有效的模型,并且这些模型能很好地泛化到未知数据中。 但如果这偏置与现实不符时,比如当图像不满足平移不变时,我们的模型可能难以拟合我们的训练数据。

上面的公式只是单层的卷积层,在扩展到神经网络时,我们就需要通过积分来合在一起 

                           

二、卷积神经网络的优缺点

优点:

  1. 卷积神经网络架构设计合理,能够从复杂的原始数据中提取出特征,减少了学习参数和计算量;
  2. 卷积神经网络对图像平移、缩放、旋转等表现出较强的不变性,在图像识别中表现出优异的性能;
  3. 卷积神经网络逐层处理,在每一层实现数据的抽象表示和特征提取,充分发挥了深度学习的优势;
  4. 卷积神经网络可以通过训练来学习数据的特征,避免了手工特征提取需要大量人工参与的问题。

缺点:

  1. 卷积神经网络很容易出现过度拟合的问题,对于小样本情况需要进行正则化或者数据增强等方法;
  2. 对于一些文本、序列等数据类型的处理,传统的卷积神经网络并不适合,需要使用特殊的结构进行处理;
  3. 卷积神经网络并不能完全替代人类直觉,在一些决策需求较强的场景仍需要人工介入。

三、图像卷积

卷积层表达的运算其实是互相关运算(cross-correlation),而不是卷积运算。

首先,我们暂时忽略通道(第三维)这一情况,看看如何处理二维图像数据和隐藏表示。在下图中,输入是高度为3、宽度为3的二维张量(即形状为3×3)。卷积核的高度和宽度都是2,而卷积核窗口(或卷积窗口)的形状由内核的高度和宽度决定(即2×2)。

                 

在二维互相关运算中,卷积窗口从输入张量的左上角开始,从左到右、从上到下滑动。 当卷积窗口滑动到新一个位置时,包含在该窗口中的部分张量与卷积核张量进行按元素相乘,得到的张量再求和得到一个单一的标量值,由此我们得出了这一位置的输出张量值。结果算式如下:

                         

输出大小等于输入大小nh×nw减去卷积核大小kh×kw,即:

                                

代码实现:

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)

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

 给出如下矩阵:

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

                      

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

Y = corr2d(X, K)
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}')

                                           

四、图像填充和步幅

在学习了基本的图像卷积后,我们可以预想,原有的图像每经过一次卷积操作后,就会缩小一圈,为一次操作后得到的新图像大小,图像变得更加平滑或者凸显出边缘。

但是,若是我们把图像进行多次卷积操作后,得到的数组可能会小一大圈,导致只剩下最关键的一些特征,大部分微笑特征全部被丢弃,这也不是我们希望看到的。这时,我们可以通过填充图像的大小并有意识的调节步幅的大小。

1、填充

下图中,我们将3×3输入填充到5×5,那么它的输出就增加为4×4。阴影部分是第一个输出元素以及用于输出计算的输入和核张量元素: 0×0+0×1+0×2+0×3=0。

通常,如果我们添加ph行填充(大约一半在顶部,一半在底部)和pw列填充(左侧大约一半,右侧一半),则输出形状将为

                       

在许多情况下,我们需要设置ph=kh−1和pw=kw−1,使输入和输出具有相同的高度和宽度。 这样可以在构建网络时更容易地预测每个图层的输出形状。假设kh是奇数,我们将在高度的两侧填充ph/2行。 如果kh是偶数,则一种可能性是在输入顶部填充⌈ph/2⌉行,在底部填充⌊ph/2⌋行。同理,我们填充宽度的两侧。

比如,在下面的例子中,我们创建一个高度和宽度为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)
    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

更普遍化的情况:

 结果:

行8=8+2+2-5+1=8

列8=8+1+1-3+1=8

2、步幅

 在计算互相关时,卷积窗口从输入张量的左上角开始,向下、向右滑动。 在前面的例子中,我们默认每次滑动一个元素。 但是,有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。我们将每次滑动元素的数量称为步幅(stride)。

着色部分的计算步骤为:

0×0+0×1+1×2+2×3=8、0×0+6×1+0×2+0×3=6。

          

此时,当垂直步幅为sh、水平步幅为sw时,输出形状为:

             

如果我们设置了ph=kh−1和pw=kw−1,则输出形状将简化为⌊(nh+sh−1)/sh⌋×⌊(nw+sw−1)/sw⌋。 更进一步,如果输入的高度和宽度可以被垂直和水平步幅整除,则输出形状将为(nh/sh)×(nw/sw)。

代码样例:

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

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

 第一个样例结果:

向下取整:

4=(8+2-3+2)/2

4=(8+2-3+2)/2

第二个样例结果:

2=(8-3+0+3)/3

2=(8-5+1+4)/4

五、多输入多输出通道

 

当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。假设输入的通道数为ci,那么卷积核的输入通道数也需要为ci。如果卷积核的窗口形状是kh×kw,那么当ci=1时,我们可以把卷积核看作形状为kh×kw的二维张量。

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

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

import torch
from d2l import torch as d2l

def corr2d_multi_in(X, K):
    # 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起
    return sum(d2l.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)

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

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

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

corr2d_multi_in_out(X, K)

最后的结果:   
 

 PS:特殊卷积层——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)
assert float(torch.abs(Y1 - Y2).sum()) < 1e-6

 六、汇聚层(池化层)

汇聚层(Pooling layer)是深度学习中常用的一种层次结构,通常被用于将输入的高维特征图降维成低维特征,以减少参数数量和计算量。其主要作用是对输入数据进行降采样,从而减小特征图的大小,同时保留最重要的特征。

池化层通常在卷积神经网络的卷积层之后使用,通过不同的方式压缩序列或图像,比如最大池化、平均池化、L2池化等方法,来获取局部区域内的最大值、平均值等特征,并生成下一层的输入。它们可以有效地降低数据维度,提高模型的泛化能力和防止过拟合。

需要注意的是,随着池化大小增大,信息的损失也会增加,因此在策略选择上需要根据具体应用场景进行取舍。

它具有双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。

1、最大汇聚层和平均汇聚层

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

在这两种情况下,与互相关运算符一样,汇聚窗口从输入张量的左上角开始,从左往右、从上往下的在输入张量内滑动。在汇聚窗口到达的每个位置,它计算该窗口中输入子张量的最大值或平均值。计算最大值或平均值是取决于使用了最大汇聚层还是平均汇聚层。

其他几个元素的计算公式对应为:

                                                  

实现代码和卷积层实现相似:

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

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

返回最大池化和平均池化

 

2、汇聚层的填充和步幅

 需要注意的是,当只传入x时,会默认步幅和池化窗口大小一致,所以尽量还是自己去设置padding和stride

X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
pool2d = nn.MaxPool2d(3)
pool2d(X)
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)
pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1))
pool2d(X)

结果:

 

3、多通道情况:

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

 在处理多通道输入数据时,汇聚层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。所以结果为:

                              

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

朝闻夕逝752

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值