卷积神经网络

图像卷积

1.多层感知机的输入是二维图像X,其隐藏表示H在数学上是一个矩阵,在代码中表示为二维张量

2.为了使每个隐藏神经元都能接受每个输入像素的信息,我们将参数从权重矩阵替换为四阶权重张量

3.平移不变性:检测对象在输入X中的平移,应该仅仅导致隐藏表示H中的平移。

4.局部性

5.由于输入图象是三维的,我们们的隐藏表示H也最好采用三维张量。对于每个空间位置,我们想要采用一组而不是一个隐藏表示。

6.这样一组隐藏可以想象成一些互相堆叠的二维网格,可以想象为一系列具有二维张量的通道,有时也称为特征映射

import torch
from torch import nn
from d2l import torch as d2l
def conv2d(x, k):       #@save
    # 二维互相关运算
    # k表示卷积核;获取卷积核 k 的高度 h 和宽度 w
    h, w = k.shape
    # 在每个位置 (i, j),从 x 中提取一个与卷积核 k 大小相同的子张量。
    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 的所有位置
            # 将提取的子张量与卷积核 k 进行逐元素相乘,并将结果相加,得到输出张量 y 在位置 (i, j) 的值
            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]])
conv2d(x, k)

卷积层

卷积层对输入和卷积核进行互相关运算,并在添加标量偏置之后产生输出。

所以,卷积层中的两个被训练的参数卷积核标量偏置

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 conv2d(x, self.weight) + self.bias

高度和宽度分别为h和w的卷积核可以称为h*w的卷积核,我们也将带有h*w卷积核的卷积层称为h*w卷积层

图像中目标的边缘检测

我们构造一个6x8像素的黑白图像。中间4列为黑色(0),其余像素为白色(1)

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

接下来,我们构造一个高度为1、长度为2的卷积核K;当进行卷积操作时,如果水平相邻的两元素相同,则输出为0。否则输出为非零

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

然后,我们对参数X(输入)和K(卷积核)执行互相关运算。输出y中的1代表白色到黑色的边缘。-1表示黑色到白色的边缘。

y = conv2d(x, k)

现在,我们将输入的二维图像转置,再加上之前的互相关运算。则之前检测的垂直边缘消失了,因此这个K只可以检测垂直边缘,不能检测水平边缘

学习卷积核

我们先构造一个卷积层,并将其卷积核初始化为随机张量。接下来,在每次迭代中,我们比较Y与卷积层输出的平方误差,然后计算梯度来更新卷积核

# 构造一个二维卷积层,它具有一个输入、输出通道和形状为(1,2)的卷积核
# 在卷积操作中不使用偏置项;     输入和输出通道都为1
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求和(因为l是一个二维张量),并执行反向传播,以计算卷积核参数的梯度
    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)

卷积操作中的特征映射(Feature Map)指的是一幅图像(或其它特征映射)在经过卷积提取到的特征,每个特征映射可以作为一类抽取的图像特征。特征映射就是某张图像经过卷积运算得到的特征值矩阵。

填充和步幅

输入形状为n*n,卷积核形状为k*k,那么输出形状将是(n-k+1)*(n-k+1)

 填充

如果我们添加p_{h}行填充(大约一半在顶部,一半在底部)和p_{w}列填充(大约一半在左侧,一半在右侧),则输出形状为

(n_{h}+k_{h}+p_{h}+1)\times (n_{w}+k_{w}+p_{w}+1)

也就是输出的高度和宽度分别增加p_{h}p_{w}

在许多情况下,我们需要设置p_{h}=k_{h}-1p_{w}=k_{w}-1,使得输出和输出具有相同的高度和宽度。

假如k_{h}是奇数,我们将在高度的两侧填充\frac{p_{h}}{2}行;

假如k_{h}是偶数,我们将在高度的上侧填充\left \lceil \frac{p_h}{2} \right \rceil行;

def comp_conv2d(conv2d, x):
    # 这里的(1, 1)表示批量大小和通道数都是1
    x = x.reshape((1, 1) + x.shape)
    y = conv2d(x)
    # y.shape[2:]返回y的形状元组中的最后两个元素,即(height', width')
    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

步幅

垂直步幅和水平步幅

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

多输入输出通道

多输入通道

import torch
from d2l import torch as d2l
def conv2d(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
def conv2d_multi_in(x, k):
    # 先遍历x和k的第0个维度(通道维度),再把它们加在一起
    return sum(conv2d(x, k) for x, k in zip(x, k))
x = torch.tensor([[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
                  [[1, 2, 3],[4, 5, 6], [7, 8, 9]]])
k = torch.tensor([[[1, 2], [3, 4]],[[0, 1], [2, 3]]])

conv2d_multi_in(x, k)

多输出通道

多输出通道并不仅是学习多个单通道的检测器

c_{i}c_{o}分别表示输入和输出通道的数量,并用k_{h}k_{w}分别表示卷积核的高度和宽度

我们可以为每个输出通道创建一个形状为c_{i}\times k_{h}\times k_{w}的卷积核张量,这样卷积核的形状为c_{o}\times c_{i}\times k_{h}\times k_{w}。每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果

def conv2d_multi_in_out(x, k):
    # 迭代k的第0个维度, 每次都对输入x执行互相关运算
    # 最后将所有结果都叠加在一起
    return torch.stack([conv2d_multi_in(x, k) for k in k], 0)
# 在第一个维度上堆叠了三个张量;构造了一个具有3个输出通道的卷积核
k = torch.stack((k, k+1, k+2), 0)

1*1卷积层

# 使用具有3个输入通道和2个输出通道的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))
# 当以像素为基础应用时,1*1卷积层相当于全连接层
# 均值为0,标准差为1
x = torch.normal(0, 1, (3, 3, 3))
# 核函数为2*3个(1,1)的卷积核
k = torch.normal(0, 1, (2, 3, 1, 1))

y1 = corr2d_multi_in_out_1x1(x, k)
y2 = conv2d_multi_in_out(x, k)
# 检查差异值是否小于1*10的-6次方
assert float(torch.abs(y1 - y2).sum()) < 1e-6

池化层

通常处理图像时,我们希望逐渐降低隐藏表示的空间分辨率、聚合信息,这样随着神经网络中层数的增加,每个神经元对其敏感的感受野(输入)就越大。

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

填充和步幅

# (1, 1, 4, 4)分别指批次大小、通道数、高度、宽度
x = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))

# 默认情况下,步幅与卷积核的大小是一致的。
# 如果使用形状为(3,3)的卷积核,我们的步幅形状为(3,3)
pool2d = nn.MaxPool2d(3)
print(pool2d.stride)
pool2d(x)

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

多个通道

在处理多通道输入数据时,池化层在每个输入通道上单独运算。池化层的输出通道数与输入通道数相同

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

卷积神经网络(LeNet)

LeNet-5由以下两部分组成:

①卷积编码器(两个卷积层组成);②全连接层稠密块(3个全连接层组成)

每个卷积块中的基本单元是一个卷积层、一个sigmoid激活函数和平均池化层。

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

net = nn.Sequential(
    # 1是输入通道数,6是输出通道数
    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)
)
# size=(1, 1, 28, 28)表示批次大小(同时处理的样本数量)为1,通道数为1,图像大小是28*28
x = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
for layer in net:
    x = layer(x)
    print(layer.__class__.__name__,'output.shape:\t', x.shape)



# 模型训练
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
上一篇:MySQL数据库的下载和安装以及命令行语法学习


下一篇:【目标检测】2. RCNN