lw2333

lw2333

Keep calm and study.

第六章-卷积神经网络

为什么要有所谓卷积神经网络#

初衷:图像中本就拥有丰富的结构,而这些结构可以被人类和机器学习模型使用。通过使用这些结构,可以降低训练的参数量,提高模型的准确性。

譬如在一张图片里做物体寻找的任务,一个比较合理的假设是物体位置具有空间不变性。卷积神经网络正是将空间不变性(spatial invariance)的这一概念系统化,从而基于这个模型使用较少的参数来学习有用的表示。

Screenshot 2024-02-21 093549

可以归纳有以下两个特点:

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

数学表示#

H\mathbf{H} 是图像矩阵 X\mathbf{X} 的隐藏表示。U\mathbf{U} 是偏置。W\mathbf{W} 是四阶权重张量。

索引 (i,j)(i, j) 处表示在此位置的像素。于是全连接层中的隐藏表示可以推导如下:

[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.\begin{aligned} \left[\mathbf{H}\right]_{i, j} &= [\mathbf{U}]_{i, j} + \sum_k \sum_l[\mathsf{W}]_{i, j, k, l} [\mathbf{X}]_{k, l}\\ &= [\mathbf{U}]_{i, j} + \sum_a \sum_b [\mathsf{V}]_{i, j, a, b} [\mathbf{X}]_{i+a, j+b}.\end{aligned}

其中 W\mathsf{W}V\mathsf{V} 只不过是把求和 “中心” 移动到了 (i,j)(i, j) 处而已。


平移不变性 意味着 U\mathsf{U}V\mathsf{V} 不依赖于 (i,j)(i, j) 的值。 这点很好理解,在不同的 (i,j)(i, j) 处,若 X\mathbf{X} 相同(物品一样),那么得到的隐藏表示也应该一样。 故:

[H]i,j=u+ab[V]a,b[X]i+a,j+b.[\mathbf{H}]_{i, j} = u + \sum_a\sum_b [\mathbf{V}]_{a, b} [\mathbf{X}]_{i+a, j+b}.

局部性 意味着我们可以把在某个范围之外的信息屏蔽,即在 [a,b][a,b] 之外的 V\mathbf{V} 设置为 0

[H]i,j=u+a=Δ1Δ1b=Δ2Δ2[V]a,b[X]i+a,j+b.[\mathbf{H}]_{i, j} = u + \sum_{a = -\Delta_1}^{\Delta_1} \sum_{b = -\Delta_2}^{\Delta_2} [\mathbf{V}]_{a, b} [\mathbf{X}]_{i+a, j+b}.

称上式为一个卷积层。V\mathbf{V} 被称为卷积核(convolution kernel)或者滤波器(filter)。


简单地喵两句卷积层。
卷积层所表达的运算其实是互相关运算(cross-correlation)
在卷积层中,输入张量和核张量通过互相关运算产生输出张量。
真正的卷积运算长这样(但其实无所谓):

(fg)(x)=f(z)g(xz)dz.(fg)(i)=af(a)g(ia).(fg)(i,j)=abf(a,b)g(ia,jb).\begin{aligned} (f * g)(\mathbf{x}) &= \int f(\mathbf{z}) g(\mathbf{x}-\mathbf{z}) d\mathbf{z}.\\ (f * g)(i) &= \sum_a f(a) g(i-a).\\ (f * g)(i, j) &= \sum_a\sum_b f(a, b) g(i-a, j-b). \end{aligned}

Screenshot 2024-02-21 093524

实际上,图像是一个由高度、宽度和颜色组成的三维张量,比如包含1024×1024×31024 \times 1024 \times 3个像素。
[X]i,j[X]i,j,k[V]a,b[V]a,b,c[\mathsf{X}]_{i, j}\to[\mathsf{X}]_{i, j, k}\quad [\mathbf{V}]_{a,b}\to [\mathsf{V}]_{a,b,c}

H\mathsf{H} 也采用三维张量。
换句话说,对于每一个空间位置,我们想要采用一组而不是一个隐藏表示。这样一组隐藏表示可以想象成一些互相堆叠的二维网格。
因此,我们可以把隐藏表示想象为一系列具有二维张量的 通道(channel)。
这些通道有时也被称为 特征映射(feature maps),因为每个通道都向后续层提供一组空间化的学习特征。
直观上可以想象在靠近输入的底层,一些通道专门识别边缘,而一些通道专门识别纹理。

为了支持输入 X\mathsf{X} 和隐藏表示 H\mathsf{H} 中的多个通道,我们可以在 V\mathsf{V} 中添加第四个坐标,即 [V]a,b,c,d[\mathsf{V}]_{a, b, c, d}。综上所述,

[H]i,j,d=a=Δ1Δ1b=Δ2Δ2c[V]a,b,c,d[X]i+a,j+b,c,[\mathsf{H}]_{i,j,d} = \sum_{a = -\Delta_1}^{\Delta_1} \sum_{b = -\Delta_2}^{\Delta_2} \sum_c [\mathsf{V}]_{a, b, c, d} [\mathsf{X}]_{i+a, j+b, c},

其中隐藏表示 H\mathsf{H} 中的索引 dd 表示输出通道,而随后的输出将继续以三维张量 H\mathsf{H} 作为输入进入下一个卷积层。
于是可以定义具有多个通道的卷积层,而其中 V\mathsf{V} 是该卷积层的权重。


(nhkh+1)×(nwkw+1).(n_h-k_h+1) \times (n_w-k_w+1).

代码练习#

def corr2d(X, K):  
    """二维互相关运算

    Args:
        X (Tensor): 二维矩阵
        K (Tensor): Kernel

    Returns:
        Tensor: 二维互相关运算结果
    """
    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]):
          # Hadamard product(Schur product, entrywise)
            Y[i, j] = (X[i:i + h, j:j + w] * K).sum() 
    return Y

二维卷积层

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

简单的示例如下

import torch
import torch.nn as nn
import torch.optim as optim

conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)
X = X.reshape((1, 1, 10, 12))
Y = Y.reshape((1, 1, 10, 11))

lr = 1e-1  

optimizer = optim.SGD([conv2d.weight], lr=lr)

for epoch in range(100):  
    Y_hat = conv2d(X)
    l = nn.MSELoss()(Y_hat, Y)

    optimizer.zero_grad()
    l.backward()
    optimizer.step()

    print(f"epoch {epoch+1}, loss {l.item():.3f}")

在卷积神经网络中,对于某一层的任意元素
,其感受野(receptive field)是指在前向传播期间可能影响
计算的所有元素(来自所有先前层)。

感受野可能大于输入的实际大小。当一个特征图中的任意元素需要检测更广区域的输入特征时,我们可以构建一个更深的网络。

填充和步幅#

卷积的输出形状取决于输入形状和卷积核的形状。连续应用多次卷积之后得到的输出远小于输入。这样的副作用就是图像边缘的信息丢失。
可在边界填充(冗余)信息,来影响。
另外,对于分辨率过高的冗余信息,可通过步幅来改进(一次走多步)。

Screenshot 2024-02-21 093454

这样,填充之后输出维度 (nhkh+1)×(nwkw+1).(n_h-k_h+1) \times (n_w-k_w+1). 变成
(nhkh+ph+1)×(nwkw+pw+1)(n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1)。

卷积神经网络中卷积核的高度和宽度通常为奇数,例如 1、3、5 或 7。 选择奇数的好处是,保持空间维度的同时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列。

常常设 ph=kh1,pw=kw1p_h=k_h-1, \,p_w=k_w-1,使输入和输出具有相同的高度和宽度。
这样可以在构建网络时更容易地预测每个图层的输出形状。假设 khk_h 是奇数,我们将在高度的两侧填充 ph/2p_h/2 行。
如果 khk_h 是偶数,则一种可能性是在输入顶部填充 ph/2\lceil p_h/2\rceil 行,在底部填充 ph/2\lfloor p_h/2\rfloor 行。同理,我们填充宽度的两侧。


步幅

每次滑动元素的数量称为步幅(stride)。
通常,当垂直步幅为 shs_h、水平步幅为 sws_w 时,输出形状为
(nhkh+ph+sh)/sh×(nwkw+pw+sw)/sw.\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.

特别地,ph=kh1p_h=k_h-1pw=kw1p_w=k_w-1 时,输出形状为
(nh+sh1)/sh×(nw+sw1)/sw\lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor

更进一步,如果输入的高度和宽度可以被垂直和水平步幅整除,则输出形状将为 (nh/sh)×(nw/sw)(n_h/s_h) \times (n_w/s_w)

代码练习#

def comp_conv2d(conv2d, X):
    X = X.reshape((1, 1) + X.shape)
    Y = conv2d(X)
    return Y.reshape(Y.shape[2:])

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

注意到 (parameter) padding: _size_2_t | strpadding 是高和宽的两侧分别填充的元素数量。


另外,nn 的 padding 默认是除后向上取整。

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

多输入多输出通道#

例如,每个 RGB 输入图像具有 3×h×w3\times h\times w 的形状。我们将这个大小为 3 的轴称为 通道(channel)维度。

几个通道一般就要构造几个卷积核。得到一个 ci×kh×kwc_i\times k_h\times k_w 的卷积核,互相关之后再求和。

def corr2d_multi_in(X, K):
    """多通道二维卷积

    Args:
        X (Tensor): 输入
        K (Tensor): 核

    Returns:
        Tensor: 结果
    """
    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)
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。