适合计算机视觉的神经网络架构:
- 平移不变性:不管检测对象出现在图像中的哪个位置,神经网络前几层应该对相同图像区域有相似的反应。
- 局部性:神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系。
严格来说,卷积层所表达的运算其实是互相关运算。
不同颜色所选的区域与同一个卷积核做互相关运算,最后得到输出。
同理,卷积核滑动进行互相关运算。最终得到高度为2,宽度为2的输出。
"""
定义corr2d函数:
1、该函数接受输入张量X和卷积核张量K,并返回输出张量Y
2、输出大小 = 输入大小n(k)×n(w) - 卷积核大小k(h)×k(w)
3、即:(n(k)-k(h)+1) × (n(w)-k(w)+1 )
"""
import torch
from torch import nn
from d2l import torch as d2l
def corr2d(X, K): #@save
"""计算二维互相关运算"""
# 卷积核的高度h和宽度w,K指卷积核Kernel
h, w = K.shape
# 设置输出Y的大小,用0进行填充
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
定义卷积层Conv2D:
- 卷积层对输入和卷积核权重进行互相关运算;
- 并在添加标量偏置之后产生输出。
"""
定义卷积层Conv2D:
1、卷积层对输入和卷积核权重进行互相关运算;
2、并在添加标量偏置之后产生输出。
"""
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))
# corr2d(X, K)
def forward(self, x):
return corr2d(x, self.weight) + self.bias
# 卷积层的一个简单应用:通过找到像素变化的位置,来检测图像中不同颜色的边缘。
# 1、构造一个6×8像素的黑白图像
X = torch.ones((6, 8))
X[:, 2:6] = 0
X
# 2、我们构造一个高度为1、宽度为2的卷积核K(水平相邻元素相同输出为0)
K = torch.tensor([[1.0, -1.0]])
Y = corr2d(X, K)
Y
"""
由X生成Y的卷积核:
1、构造一个卷积层,并将其卷积核初始化为随机张量;
2、在每次迭代中,比较Y与卷积层输出的平方误差,然后计算梯度来更新卷积核;
3、使用内置的二维卷积层,并忽略偏置。
"""
# 构造一个二维卷积层,它具有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 # 学习率
# 进行训练,轮数为10。计算梯度并进行更新,输出loss
for i in range(10):
# 定义损失函数:交叉熵损失
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
# 梯度置零
conv2d.zero_grad()
l.sum().backward()
# 迭代卷积核
# 梯度更新:w = w - lr * w'
conv2d.weight.data[:] -= lr * conv2d.weight.grad
# 每隔2轮输出一次
if(i + 1) % 2 ==0:
print(f'epoch{i+1}, loss{l.sum():.3f}')
- 问题一:应用了连续卷积,最终得到的输出远小于输入大小,使得原始图像的边界丢失了许多有用信息,我们希望输入大小和输出大小相同?
解决:填充:在输入图像的边界填充元素(通常填充元素是0)- 问题二:有时原始的输入分辨率十分冗余,我们可能希望大幅降低图像的宽度和高度?
解决:步幅:设置卷积核滑动的步幅来减少采样次数- 问题三:卷积核为什么一般选择奇数?
解决:保持空间维度的同时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列。
填充(padding):在输入图像的边界填充元素(通常填充元素是0)
"""
填充:
1、输入给定8×8,输出要求8×8
2、卷积核的大小为3×3,所有侧边填充1个像素
"""
import torch
from torch import nn
# 为了方便起见,我们定义了一个计算卷积层的函数。
# 此函数初始化卷积层权重,并对输入和输出提高和缩减相应的维数
def comp_conv2d(conv2d, X):
# 这里的(1,1)表示批量大小和通道数都是1
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
# 省略前两个维度:批量大小和通道(1, 1, 8, 8)
return Y.reshape(Y.shape[2:])
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
print(X)
print(conv2d)
print(comp_conv2d(conv2d, X))
comp_conv2d(conv2d, X).shape
# 卷积核为5×3时,为了使输入和输出相同,高度填充2,宽度填充1
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape
步幅(stride):每次滑动元素的数量
"""
步幅:
1、每次滑动元素的数量;
2、为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。
"""
# 将高度和宽度的步幅设置为2,从而将输入的高度和宽度减半
# (8 + 2 - 3) / 2 = 4
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape
当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。
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和核张量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_muti_in(X, K)
多输出通道:
- 在最流行的神经网络架构中,随着神经网络层数的加深,我们常会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。
- 将每个通道看作对不同特征的响应。
# 实现一个计算多个通道的输出的互相关函数
def corr2d_multi_in_out(X, K):
# 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
# 最后将所有结果都叠加在一起
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
# 通过将核张量K与K+1(K中每个元素加1)和K+2连接起来,构造了一个具有3个输出通道的卷积核。
K = torch.stack((K, K + 1, K + 2), 0)
# (输出通道数,输入通道数,高度,宽度)
K.shape
- 1×1卷积核被经常用来改变通道,相当于全连接层
- 可以对输入和输出的形状进行调整
# 使用全连接层实现1×1卷积
def corr2d_multi_in_out_1x1(X, K):
c_i, h, w = X.shape
# K:(输出通道,输入通道,高度,宽度)
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
print(Y1)
print(Y2)
通过逐渐聚合信息,生成越来越粗糙的映射,最终实现学习全局表示的目标,同时将卷积图层的所有优势保留在中间层。
汇聚(pooling)层(也叫做池化层):
- 降低卷积层对位置的敏感性
- 降低对空间降采样表示的敏感性
汇聚层与卷积层的原理大体相似,只不过把互相关运算换成求最大值或者求平均值
# 最大汇聚层和平均汇聚层
"""
定义汇聚层:
1、设置汇聚层与输出的大小
2、设置模式:大汇聚层和平均汇聚层
"""
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的高度和宽度
Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
# 进行遍历,相当于对矩阵的局部区域[i:i+p_h, j:j+p_w]求最大值/平均值
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()
if mode == 'avg':
Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
return Y
# 构建输入张量X,验证二维最大汇聚层输出
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
print(X)
print('最大汇聚层:\n', pool2d(X, (2, 2)))
print('平均汇聚层:\n', pool2d(X, (2, 2), 'avg'))
# print(f'最大汇聚层:{'\n'+pool2d(X, (2, 2))}')
# print(f'平均汇聚层:{pool2d(X, (2, 2), 'avg')}')
与卷积层一样,汇聚层也可以改变输出形状
# 构造了一个输入张量X,它有四个维度,其中样本数和通道数都是1
X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
X
多个通道:
- 在处理多通道输入数据时,汇聚层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总
- 汇聚层的输出通道数与输入通道数相同。
"""
多个通道:
1、在处理多通道输入数据时,汇聚层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。
2、汇聚层的输出通道数与输入通道数相同。
"""
X = torch.cat((X, X + 1), 1)
X
pool2d = nn.MaxPool2d(3, padding=1, stride=2)
pool2d(X)