卷积神经网络

发布于:2025-09-15 ⋅ 阅读:(25) ⋅ 点赞:(0)

互相关运算

卷积层实际进行的是互相关运算(cross-correlation),而非严格的数学卷积。

互相关运算中,输入张量与核张量按窗口逐位置相乘并求和,得到输出张量。

0 \times 0 + 1 \times 1 + 3 \times 2 + 4 \times 3 = 19

假设输入形状为n_h\times n_w,卷积核形状为k_h \times k_w,那么输出形状将是(n_h-k_h+1)\times(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.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

卷积层

卷积层对输入和卷积核权重进行互相关运算,并在添加标量偏置之后产生输出。 所以,卷积层中的两个被训练的参数是卷积核权重和标量偏置。

在训练基于卷积层的模型时,要随机初始化卷积核权重。

卷积层初始化代码

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

学习卷积核

先构造一个卷积层,并将其卷积核初始化为随机张量。接下来,在每次迭代中,比较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))
lr = 3e-2  # 学习率

for i in range(10):
    Y_hat = conv2d(X)
    l = (Y_hat - Y) ** 2
    conv2d.zero_grad()
    l.sum().backward()
    # 迭代卷积核
    conv2d.weight.data[:] -= lr * conv2d.weight.grad
    if (i + 1) % 2 == 0:
        print(f'epoch {i+1}, loss {l.sum():.3f}')

填充

  • 目的:防止卷积操作后图像尺寸缩小,尤其是边缘信息丢失。

  • 做法:在输入图像四周填充0(或其他值)。

填充 p_h 行,p_w 列之后输出尺寸变为:(n_h-k_h+1)\times(n_w-k_w+1)

若希望输入输出尺寸一致,通常设置:p_h=k_h-1,p_w=k_w-1

步幅

  • 目的:减少输出尺寸,降低计算量,降采样。

  • 做法:卷积核每次滑动跳过多个像素。

垂直步幅:s_h ,水平步幅:s_w,输出尺寸为:\left\lfloor\frac{n_h-k_h+p_h+s_h}{s_h}\right\rfloor \times \left\lfloor\frac{n_w-k_w+p_w+s_w}{s_w}\right\rfloor

若输入尺寸能被步幅整除,则输出尺寸为:\frac{n_h}{s_h}\times\frac{n_w}{s_w}

多输入通道

当输入包含多个通道时,需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。

多输入通道互相关运算:简而言之,我们所做的就是对每个通道执行互相关操作,然后将结果相加。

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

多输出通道

直观地说,我们可以将每个通道看作对不同特征的响应。而现实可能更为复杂一些,因为每个通道不是独立学习的,而是为了共同使用而优化的。因此,多输出通道并不仅是学习多个单通道的检测器。

用  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 corr2d_multi_in_out(X, K):
    # 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
    # 最后将所有结果都叠加在一起
    return torch.stack([corr2d_multi_in(X, k) for k in K], 0)

1×1卷积层

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

汇聚层

它具有双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。

与卷积层类似,汇聚层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口(有时称为汇聚窗口)遍历的每个位置计算一个输出。 然而,不同于卷积层中的输入与卷积核之间的互相关计算,汇聚层不包含参数。 相反,池运算是确定性的,我们通常计算汇聚窗口中所有元素的最大值或平均值。这些操作分别称为最大汇聚层(maximum pooling)和平均汇聚层(average pooling)。

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
  • 与卷积层一样,汇聚层也可以改变输出形状。可以通过填充和步幅以获得所需的输出形状。
  • 在处理多通道输入数据时,汇聚层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。 这意味着汇聚层的输出通道数与输入通道数相同。 

LeNet

用深度学习框架实现LeNet

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

net = nn.Sequential(
    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))

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

def evaluate_accuracy_gpu(net, data_iter, device=None): #@save
    """使用GPU计算模型在数据集上的精度"""
    if isinstance(net, nn.Module):
        net.eval()  # 设置为评估模式
        if not device:
            device = next(iter(net.parameters())).device
    # 正确预测的数量,总预测的数量
    metric = d2l.Accumulator(2)
    with torch.no_grad():
        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]

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

lr, num_epochs = 0.9, 10
train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_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)

  • 卷积层全连接层 用 Xavier 均匀分布初始化权重。

  • net.apply(...) 会递归遍历所有子模块。

net.to(device)
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss()

  • 把模型搬至 GPU/CPU。

  • 优化器:SGD。

  • 损失:交叉熵(适合多类分类)。

animator = d2l.Animator(xlabel='epoch',
                        legend=['train loss', 'train acc', 'test acc'])

  • 实时绘制三条曲线:训练损失、训练准确率、测试准确率。

metric = d2l.Accumulator(3)   # 累加器:loss_sum, acc_sum, sample_count
net.train()                   #  dropout/batch-norm 切换到训练模式
for X, y in train_iter:
    X, y = X.to(device), y.to(device)
    optimizer.zero_grad()
    y_hat = net(X)
    l = loss(y_hat, y)
    l.backward()
    optimizer.step()

  • 标准前向-反向-更新流程。

  • metric.add(...) 把当前批的 总损失、总正确数、总样本数 累加。


网站公告

今日签到

点亮在社区的每一天
去签到