动手学深度学习-卷积

卷积基础

全链接到卷积

当直接用MLP处理图片时,数据量会过大

比如36mb的图片可能有3600万的像素,即便是单隐层也需要14GB的显存才能处理。

  • 平移不变性:这意味着即使输入图像中的对象位置发生了变化,网络仍然能够识别出这个对象。
  • 局部性:指的是模型对输入数据的局部信息或临近像素的关系的敏感和依赖性。在图像处理中基于一个观察:图像中的相邻像素通常是高度相关的。

重新考察全连接层

将输入和输出变形为矩阵(宽度,高度)

将权重信息变为4-D张量(h,w)到(h',w')。因为这里是输入的高宽到输出的高宽的变化。

以下公式,展示了如何将一个输入矩阵(通常是图像)与一个卷积核(filter或kernel)相乘以生成输出矩阵(特征图)。

这里的$h_{i,j}$就是输出,位于第i行,第j列。x是输入,w是全连接层的权重。$w_{i,j,k,l}$表示当卷积核位于输出特征图的第 i 行第 j 列时,对应的输入图像位置是第 k行第 l列的权重。a和b指的是相对于卷积核最新偏移的位置。这里是输入的高宽到输出的高宽的变化所以reshape成4d的矩阵。

等式中间为便利俩个维度进行求和,

$$ h_{i,j}=\sum_{k,l}w_{i,j,k,l}x_{k,l}=\sum_{a,b}\nu_{i,j,a,b}x_{i+a,j+b} $$

V是W的重新索引使得$\nu_{i,j,a,b}=w_{i,j,i+a,j+b}$。

平移不变性

x的平移导致h的平移$h_{i,j}=\sum_{a,b}\nu_{i,j,a,b}x_{i+a,j+b}$(即便只有一点点偏移都会导致)

v不应该依赖于(i,j)

解决方案为$\nu_{i,j,a,b}=\nu_{a,b}$

$$ h_{i,j}=\sum_{a,b}\nu_{a,b}x_{i+a,j+b} $$

这就是2维卷积(交叉相关)

局部性

$$ h_{i,j}=\sum_{a,b}\nu_{a,b}x_{i+a,j+b} $$

当评估$h_{i,j}$时,我们不应该用原理$x_{i,j}$的参数

解决方案:当|a|,|b|>$\Delta$时,使得$v_{a,b}=0$,就是去看远离卷积核的元素

$$ h_{i,j}=\sum_{a=-\Delta}^\Delta\sum_{b=-\Delta}^\Delta\nu_{a,b}x_{i+a,j+b} $$

总结:对全连接层使用平移不变性和局部性,得到卷积层

$$ \begin{aligned}&h_{i,j}=\sum_{a,b}\nu_{i,j,a,b}\chi_{i+a,j+b}\\&h_{i,j}=\sum_{a=-\Delta}^\Delta\sum_{b=- \Delta}^\Delta\nu_{a,b}x_{i+a,j+b}\end{aligned} $$

卷积

其实就是对四个位置做矩阵乘法。

二维卷积

输入$\mathbf{X}:n_h\times n_w$

核$\mathbf{W}:k_h\times k_w$ · 偏差$b\in\mathbb{R}$

输出$\mathbf{Y}:(n_h-k_h+1)\times(n_w-k_w+1)$:这里写的是输出的规模
$$\mathbf{Y}=\mathbf{X}\star\mathbf{W}+b$$

w和b是可以学习的参数

可以通过卷积值来 进行边缘检测,锐化,高斯模糊。高斯模糊就是卷积核是高斯分布的

二维交叉相关

$$ y_{i,j}=\sum_{a=1}^h\sum_{b=1}^ww_{a,b}x_{i+a,j+b} $$

二维卷积(这才是卷积的严格表示)

$$ y_{i,j}=\sum_{a=1}^h\sum_{b=1}^ww_{-a,-b}x_{i+a,j+b} $$

由于对称性,使用中没有区别。

一维和三位交叉相关

1维的有文本语言时间序列

$$ y_i=\sum_{a=1}^hw_ax_{i+a} $$

三维的有,视频,医学图像,气象地图

$$ y_{i,j,k}=\sum_{a=1}^h\sum_{b=1}^w\sum_{c=1}^dw_{a,b,c}x_{i+a,j+b,k+c} $$

代码

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


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


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]):
            # 这是python切片写法,从i开始到i+h行,从j到j+w行
            # 就是从原图上切片一块下来,然后进行矩阵乘法,将结果输入到新的图片上
            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)

# 检测图像中的边缘
X = torch.ones((6, 8))
X[:, 2:6] = 0
print(X)
# 这个1x2的卷积只能检测垂直的边缘
K = torch.tensor([[1.0, -1.0]])
Y = corr2d(X, K)
print(Y)

# 学习由X生成Y的卷积核(上面代码都是顶死的卷积核大小)
# 输入通道1,输出通道为1,黑白图片通道为1,彩色图片通道为3,卷积核大小为1,2,不需要bias
conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)
# 对所有conv来说输入都是4d的,前俩个参数分别是通道数批量大小数
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
# 将X通过卷积核变成y,这里是对卷积核进行学习
for i in range(10):
    # 输入矩阵得到输出向量
    Y_hat = conv2d(X)
    # 均方误差
    l = (Y_hat - Y) ** 2
    # 梯度设为0
    conv2d.zero_grad()
    # 求和并梯度下降
    l.sum().backward()
    # 3e-2是学习率,减去学习率乘以梯度
    conv2d.weight.data[:] -= 3e-2 * conv2d.weight.grad
    if (i + 1) % 2 == 0:
        # 每俩个bach输出一次loss
        print(f'batch{i + 1},loss{l.sum():.3f}')

# 所学的卷积核的权重张量
print(conv2d.weight.data.reshape((1, 2)))

卷积层将输入和核矩阵进行交叉相关,加上偏移后得到输出(局部性和平移不变性)

核矩阵和偏移是可以学习的参数

核矩阵的大小是超参数,卷积层可以理解为是一个特殊的全连接层

为什么卷积核不应该看那么远,是因为最终视野是一样的一般是3x3最多是5x5

主要的思想是用卷积核替代全连接层。比如隐藏层1000,输入层3000w像素就炸了。(可以看作某种数据共享的全链接)

填充和步幅

给定32x32输入图像

应用5x5大小的卷积核

  • 第一层得到输出大小28x28
  • 第七层得到输出大小4x4

更大的卷积核可以更快地减小输出

形状从$n_h \times n_w$减少到$\mathbf{Y}:(n_h-k_h+1)\times(n_w-k_w+1)$,n是原始,k是卷积核。

但是卷积核有个问题就是:他能够迭代的次数是有限的,比如32x32的图片多经过几次卷积核就会变得很小。

填充

在输入周围添加额外的行和列使得,最外层的一圈都为0。比如在4x4的卷积核中,我们的输出可以比输入还要来的更大。

如果填充$p_h$行和$p_w$列,输出形状为

$$ (n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1) $$

通常取

$$ p_h=k_h-1,\quad p_w=k_w-1 $$

这样带入上式就可以消为0,即输入规模和输出规模不会发生变化

  • 当$k_h$为奇数:在上下俩测填充$p_h$/2
  • 当$k_h$为偶数,在上侧填充$[p_h/2]$,在下侧填充$[p_h/2]$

步幅

大的卷积核会增加计算量和模型复杂度(多层小的效果一般比一层大的更好)

填充减小的输出大小于层数线性层相关

  • 给定输入大小224x224,在使用5x5卷积核的情况下,需要44层将输出降低到4x4
  • 需要大量计算才能得到较小输出。

步幅是指行/列的滑动步长,例:高度3宽度2的步幅就是卷积核每次移动的幅度纵向为3,横向为2

其实单行就是$(n_k-k_h+p_h)/s_h+1$线算出能迭代的位置最后加上自身的位置。

$$ \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 $$

如果$p_h=k_h-1,\quad p_w=k_w-1$(代表了填充一般取核数-1,方便计算)

$$ \lfloor(n_h+s_h-1)/s_h\rfloor\times\lfloor(n_w+s_w-1)/s_w\rfloor $$

如果输入高度和宽度可以被步幅整除$(n_h/s_h)\times(n_w/s_w)$

填充和步幅都是卷积层的超参数,填充在输入周围添加额外的行和列,来控制输出形状的减少量,步幅是每次华东核窗口时的行列步长,可以成本减少输出的形状。

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


def comp_conv2d(conv2d, X):
    # 在维度的前面加上一个通道数和批量大小数
    X = X.reshape((1, 1) + X.shape)
    Y = conv2d(X)
    # 删除前面2维
    return Y.reshape(Y.shape[2:])


conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
print(comp_conv2d(conv2d, X).shape)
# 填充高度和宽度
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
print(comp_conv2d(conv2d, X).shape)
# 将高度和步幅设置为2
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
# 完全不对称
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
print(comp_conv2d(conv2d, X).shape)#输出结果是2x2

虽然看起来3x3的卷积核视野很小,但是第二层就是9x9,理论上十层3x3卷积和6层5x5效果差不多,但是3x3来的更快

多个输入和输出通道

彩色图像有RGB三个通道,转化为灰度会丢失该信息

每个通道都有一个卷积核,结果是所有通道卷积结果的和

这里的输出是个单通道的

多输入通道

$$ \begin{aligned} &\bullet\text{ 输入 }\mathbf{X}:c_i\times n_h\times n_w \\ &\bullet\text{ 核 }\mathbf{W}:c_i\times k_h\times k_w \\ &\bullet\text{ 输出 }\mathbf{Y}:m_h\times m_w \\ &&\mathbf{Y}=\sum_{i=0}^{c_i}\mathbf{X}_{i,:,:}\star\mathbf{W}_{i,:,:} \end{aligned} $$

多输出通道

无论有多少输入通道,到目前为止我们只用到但输出通道

我们可以有多个三维卷积和,每个核生成一个输出通道(输出通道与卷积核的数量相关,与输入通道无关)

$$ \begin{aligned} &\bullet\text{ 输入 }\mathbf{X}:c_i\times n_h\times n_w \\ &\bullet\text{ 核 }\mathbf{W}:c_o\times c_i\times k_h\times k_w \\ &\bullet\text{ 输出 }\mathbf{Y}:c_o\times m_h\times m_w \\ &\mathbf{Y}_{i,:,:}=\mathbf{X}\star\mathbf{W}_{i,:,:,:}\quad\text{for }i=1,...,c_o \end{aligned} $$


每个输出通道可以识别特定的模型

底层通道识别的是原始细节,例如眼睛耳朵,高层可以识别整体信息比如整个头

输入通道三个别并组合输入中的模型

1*1卷积层

他是一个受欢迎的选择。它不识别空间模式,只是融合通道,每个1x1的卷积核都作用在三个通道上

相当于输入形状为$n_hn_w\times c_i$,权重为$c_o \times c_i$的全连接层(这里的co是output)

简单来说就是多做一个卷积层就是多一种输出

输出通道是卷积层的超参数,每一个输入通道都有独立的二维卷积核,所有通道结果相加得到一个输出通道的结果

每个输出通道有三维的卷积核(一般来说每一个层都是作用在之前全部维度上的)

多输入和多输出通道代码

import torch
from d2l import torch as d2l


# 实现多输入通道互相关运算
def corr2d_multi_in(X, K):
    # 每次拿出对应的维度,然后按元素求和
    return sum(d2l.corr2d(x, k) for x, k in zip(X, K))


# 计算多通道的互相关函数,K为输出通道
def corr2d_multi_in_out(X, K):
    # 在0的维度上进行累积,对每一个K进行支持多输入通道的互相关运算
    # 其实就是将维度累加(提升了一个维度)
    return torch.stack([corr2d_multi_in(X, k) for k in K], 0)


# 1x1卷积
def corr2d_multi_in_out_1x1(X, K):
    c_i, h, w = X.shape
    # 获取第0个维度
    c_o = K.spape[0]
    # 把高和宽拉成一条向量
    X = X.reshape((c_i, h * w))

    K = K.reshape((c_o, c_i))
    # 计算输出y,因为1x1卷积可以直接进行矩阵乘法,不需要卷积核去扫。
    Y = torch.matmul(K, X)
    # 在reshape回去,
    return Y.reshape(c_o, h, w)


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

print(corr2d_multi_in(X, K))

# K为输出通道
print("k的形状", K.shape)
# 第一个参数是tensor,这里是在第0个元素位置下插入tensor
# 用于在给定的新维度上对一系列张量进行堆叠,
K = torch.stack((K, K + 1, K + 2), 0)
print("k的值:", K)
print(corr2d_multi_in_out(X, K))

# 1x1卷积
# 生成一个形状为(3, 3, 3)的张量,其中的元素是从均值为0、标准差为1的正态分布中随机抽取的。
# 作为输入矩阵
X = torch.normal(0, 1, (3, 3, 3))
# 输出维度,输入维度,宽,高
K = torch.normal(0, 1, (2, 3, 1, 1))

Y1 = corr2d_multi_in_out(X, K)
Y2 = corr2d_multi_in_out(X, K)

一般来说不同通道的卷积核也是一样的,这样一个卷积核扫过的时候就可以同时作用于三个通道了。

池化层

卷积层对于位置十分敏感,需要一定程度的平移不变性,照明,物体位置,比例,外观等等因素因图像而异。

返回窗口中的最大值,可以理解为没有参数的卷积核直接输出最大参数

比如2x2的池化层可以容忍一个像素的偏移

  • 池化层于卷积层类似,都具有填充和步幅
  • 没有科学系参数
  • 在每个通道应用池化层以获得相应的输出通道
  • 输出通道=输入通道数

最大池化层输出每个窗口最强的信号

平均池化层:将最大池化层中的最大替换为平均

代码实现

import torch
from d2l import torch as d2l


def pool2d(X, pool_size, mode='max'):
    p_h, p_w = pool_size
    Y = torch.zeros(X.shape)
    for i in range(Y.shape[1]):
        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]])
print(pool2d(X, (2, 2)))
print(pool2d(X, (2, 2), 'avg'))

# 填充和步幅

# 创建4x4的矩阵,pytorch中步幅和池化窗口的大小是相同的
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
print(X)
pool2d = torch.nn.MaxPool2d(3)
print(pool2d)
# 填充和步幅可以手动指定,第一个矩阵大小也可以设置为(2,3)
pool2d = torch.nn.MaxPool2d(3, padding=1, stride=2)
print(pool2d(X))

# 池化层在每个输入通道上单独运算
# cat是在对应维度上对tensor进行拼接
X = torch.cat((X, X + 1), 1)
pool2d = torch.nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)

池化层和卷积层是可以任意嵌套的,现在池化层用的越来越少了,

Last modification:July 22, 2024
如果觉得我的文章对你有用,请随意赞赏