跟李沐学AI-动手学深度学习-卷积神经网络

从全连接层到卷积

引例:分类猫和狗的图片
不变性:平移不变性、局部性。

  • 图像的平移不变性使我们以相同的方式处理局部图像,而不在乎它的位置。
  • 局部性意味着计算相应的隐藏表示只需要一小部分局部图像像素。

对全连接层使用平移不变性和局部性得到卷积层!!(具体数学推导没理解清楚)
二维交叉相关。

二维卷积层

在这里插入图片描述
总结:

  • 卷积层将输入和核矩阵进行交叉相关,加上偏移后得到输出。
  • 核矩阵和偏移是可学习的参数。
  • 核矩阵的大小是超参数。

代码实现

图像卷积
互相关运算

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

实现二维卷积层

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

卷积层的一个简单应用:检测图像中不同颜色的边缘

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

输出Y的1代表从白色到黑色的边缘,-1代表从黑色到宝色的边缘


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

Y = corr2d(X, K)
Y

当前使用的卷积核K只可以检测垂直边缘

corr2d(X.t(), K)

学习由X生成Y的卷积核

# 构造⼀个⼆维卷积层,它具有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))
for i in range(10):
  Y_hat = conv2d(X)
  l = (Y_hat - Y) ** 2
  conv2d.zero_grad()
  l.sum().backward()
  # 迭代卷积核
  conv2d.weight.data[:] -= 3e-2 * conv2d.weight.grad
  if (i + 1) % 2 == 0:
    print(f'batch {i+1}, loss {l.sum():.3f}')

所学的卷积核的权重张量

conv2d.weight.data.reshape((1, 2))

卷积层里的填充和步幅

填充

在这里插入图片描述
在输入周围添加额外的行/列
在这里插入图片描述

步幅

在这里插入图片描述
步幅是指行/列的滑动步长。
在这里插入图片描述
总结:

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

代码实现

填充和步幅
在所有侧边填充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)
  # 省略前两个维度:批量⼤⼩和通道
  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=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape

将高度和宽度的步幅设置为2

conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
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

多个输入和输出通道

多个输入通道

  • 彩色图像可能有RGB三个通道。
  • 转换为灰度会丢失信息。

每个通道都有一个卷积核,结果是所有通道卷积结果的和。
在这里插入图片描述

多个输出通道

无论有多少输入通道,到目前为止我们只用到了单输出通道。
我们可以有多个三维卷积核,每个核生成一个输出通道。
在这里插入图片描述

多个输入和输出通道

每个输出通道可以识别特定模式。
输入通道核识别并组合输入中的模式。

1*1卷积层

kh=kw=1是一个受欢迎的选择。它不识别空间模式,只是融合通道。
相当于输入形状为nhnw X Ci,权重为c0 X ci的全连接层 。

二维卷积层

在这里插入图片描述

总结

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

多输入和多输出的代码实现

实现一下多输入通道互相关运算

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

计算多个通道的输出的互相关函数

def corr2d_multi_in_out(X, K):
  # 迭代“K”的第0个维度,每次都对输⼊“X”执⾏互相关运算。
  # 最后将所有结果都叠加在⼀起
  return torch.stack([corr2d_multi_in(X, k) for k in K], 0)

K = torch.stack((K, K + 1, K + 2), 0)
K.shape

corr2d_multi_in_out(X, K)

1 X 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))

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

总结:

  • 多输入输出通道可以用来拓展卷积层的模型。
  • 当以每像素为基础应用时,1 X 1卷积层相当于全连接层。
  • 1 X 1卷积层通常用于调整网络层的通道数量和控制模型复杂性。

池化层

卷积层对于位置敏感,像是前边提及到的检测垂直边缘的卷积核,只能处理垂直的边缘信息。
它需要一定程度的平移不变性。

二维最大池化层

返回滑动窗口中的最大值。(可容1像素移位)。

填充、步幅和多个通道

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

平均池化层

最大池化层:每个窗口中最强的模式信号。
平均池化层:将最大池化层中的“最大”操作替换为“平均”。

总结

  • 池化层返回窗口中最大或平均值。
  • 缓解卷积层会对位置的敏感性。
  • 同样有窗口大小、填充和步幅作为超参数。

池化层代码实现

实现池化层的正向传播

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

填充和步幅

X = torch.arange(16, dtype=torch.float32).reshape((1, 1, 4, 4))
X

深度学习框架中的步幅与池化窗口的大小相同

pool2d = nn.MaxPool2d(3)
pool2d(X)

填充和步幅可以手动设定

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

设定一个任意大小的矩形池化窗口,并分别设定填充和步幅的高度和宽度

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

池化层在每个输入通道上单独运算

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

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

LeNet

最初应用于手写数字识别。识别邮政编码。识别支票。
MNIST
50000个训练数据,10000个测试数据,图像大小为28*28,10类。

在这里插入图片描述
总结:

  • LeNet是早期成功的神经网络。
  • 先使用卷积层来学习图片空间信息。
  • 然后使用全连接层来转换到类别空间。

卷积神经网络(LeNet)代码实现

LeNet(LeNet-5)由两个部分组成:卷积编码器和全连接层密集块。

!pip install d2l==0.14
import torch
from torch import nn
from d2l import torch as d2l
class Reshape(torch.nn.Module):
  def forward(self, x):
    return x.view(-1, 1, 28, 28)
net = torch.nn.Sequential(
  Reshape(),
  nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),#二维卷积层,输入通道数1,输出通道数6,卷积核大小5*5,零填充为2,步长为1,使用sigmoid激活函数,输出的特征图大小仍是28*28
  nn.AvgPool2d(kernel_size=2, stride=2),#平均池化层,大小为2*2,步长为2,输出的特征图大小为6@14*14
  nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),#二维卷积层,输入通道数为6,输出通道数为16,卷积核大小为5*5,使用sigmoid激活函数,输出的特征图大小为10*10
  nn.AvgPool2d(kernel_size=2, stride=2),#平均池化层,大小为2*2,步长为2,输出的特征图大小为16@5*5
  nn.Flatten(),#展成一维向量
  nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),#MLP输入层,输入16*5*5,输出120
  nn.Linear(120, 84), nn.Sigmoid(),#MLP隐藏层,输入120,输出84
  nn.Linear(84, 10))#MLP输出层,输入84,输出10

检查模型

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)

在这里插入图片描述

LeNet在Fashion-MNIST数据集上的表现

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)

对evaluate_accuracy函数进行轻微的修改

def evaluate_accuracy_gpu(net, data_iter, device=None): 
  """使⽤GPU计算模型在数据集上的精度。"""
  if isinstance(net, torch.nn.Module):
    net.eval() # 设置为评估模式
    if not device:
      device = next(iter(net.parameters())).device
  # 正确预测的数量,总预测的数量
  metric = d2l.Accumulator(2)
  for X, y in data_iter:
    if isinstance(X, list):
      # BERT微调所需的(之后将介绍)
      X = [x.to(device) for x in X]
    else:
      X = X.to(device)
    y = y.to(device)
    metric.add(d2l.accuracy(net(X), y), y.numel())
  return metric[0] / metric[1]

为了使用gpu,我们还需要一点小修改

def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
  """⽤GPU训练模型(在第六章定义)。"""
  def init_weights(m):
    if type(m) == nn.Linear or type(m) == nn.Conv2d:
      nn.init.xavier_uniform_(m.weight)
  net.apply(init_weights)
  print('training on', device)
  net.to(device)
  optimizer = torch.optim.SGD(net.parameters(), lr=lr)
  loss = nn.CrossEntropyLoss()
  animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
        legend=['train loss', 'train acc', 'test acc'])
  timer, num_batches = d2l.Timer(), len(train_iter)
  for epoch in range(num_epochs):
    # 训练损失之和,训练准确率之和,范例数
    metric = d2l.Accumulator(3)
    net.train()
    for i, (X, y) in enumerate(train_iter):
      timer.start()
      optimizer.zero_grad()
      X, y = X.to(device), y.to(device)
      y_hat = net(X)
      l = loss(y_hat, y)
      l.backward()
      optimizer.step()
      with torch.no_grad():
        metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
      timer.stop()
      train_l = metric[0] / metric[2]
      train_acc = metric[1] / metric[2]
      if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
        animator.add(epoch + (i + 1) / num_batches,
          (train_l, train_acc, None))
    test_acc = evaluate_accuracy_gpu(net, test_iter)
    animator.add(epoch + 1, (None, None, test_acc))
  print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
      f'test acc {test_acc:.3f}')
  print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
      f'on {str(device)}')

训练和评估LeNet-5模型

lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

在这里插入图片描述

总结

  1. 理解了关于模型的通道数,卷积核,池化层等细节信息。
  2. 关于如何推导出卷积核还没有思考明白。
  3. 学习并了解了LeNet网络模型的构建。
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值