图像卷积
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)
填充
如果我们添加行填充(大约一半在顶部,一半在底部)和列填充(大约一半在左侧,一半在右侧),则输出形状为
也就是输出的高度和宽度分别增加和。
在许多情况下,我们需要设置和,使得输出和输出具有相同的高度和宽度。
假如是奇数,我们将在高度的两侧填充行;
假如是偶数,我们将在高度的上侧填充行;
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)
多输出通道
多输出通道并不仅是学习多个单通道的检测器
用和分别表示输入和输出通道的数量,并用和分别表示卷积核的高度和宽度
我们可以为每个输出通道创建一个形状为的卷积核张量,这样卷积核的形状为。每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果
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)