NNDL 12 卷积神经网络(4)ResNet18实现MNIST

1.模型构建

1.1残差单元

残差单元包裹的非线性层的输入和输出形状大小应该一致。如果一个卷积层的输入特征图和输出特征图的通道数不一致,则其输出与输入特征图无法直接相加。为了解决上述问题,我们可以使用1×11×1大小的卷积将输入特征图的通道数映射为与级联卷积输出特征图的一致通道数。

1×11×1卷积:与标准卷积完全一样,唯一的特殊点在于卷积核的尺寸是1×11×1,也就是不去考虑输入数据局部信息之间的关系,而把关注点放在不同通道间。通过使用1×11×1卷积,可以起到如下作用:

  • 实现信息的跨通道交互与整合。考虑到卷积运算的输入输出都是3个维度(宽、高、多通道),所以1×11×1卷积实际上就是对每个像素点,在不同的通道上进行线性组合,从而整合不同通道的信息;
  • 对卷积核通道数进行降维和升维,减少参数量。经过1×11×1卷积后的输出保留了输入数据的原有平面结构,通过调控通道数,从而完成升维或降维的作用;
  • 利用1×11×1卷积后的非线性激活函数,在保持特征图尺寸不变的前提下,大幅增加非线性。
import torch
import torch.nn as nn
import torch.nn.functional as F


class ResBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1, use_residual=True):
        """
        残差单元
        输入:
            - in_channels:输入通道数
            - out_channels:输出通道数
            - stride:残差单元的步长,通过调整残差单元中第一个卷积层的步长来控制
            - use_residual:用于控制是否使用残差连接
        """
        super(ResBlock, self).__init__()
        self.stride = stride
        self.use_residual = use_residual
        # 第一个卷积层,卷积核大小为3×3,可以设置不同输出通道数以及步长
        self.conv1 = nn.Conv2d(in_channels, out_channels, 3, padding=1, stride=self.stride, bias=False)
        # 第二个卷积层,卷积核大小为3×3,不改变输入特征图的形状,步长为1
        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1, bias=False)

        # 如果conv2的输出和此残差块的输入数据形状不一致,则use_1x1conv = True
        # 当use_1x1conv = True,添加1个1x1的卷积作用在输入数据上,使其形状变成跟conv2一致
        if in_channels != out_channels or stride != 1:
            self.use_1x1conv = True
        else:
            self.use_1x1conv = False
        # 当残差单元包裹的非线性层输入和输出通道数不一致时,需要用1×1卷积调整通道数后再进行相加运算
        if self.use_1x1conv:
            self.shortcut = nn.Conv2d(in_channels, out_channels, 1, stride=self.stride, bias=False)

        # 每个卷积层后会接一个批量规范化层,批量规范化的内容在7.5.1中会进行详细介绍
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.bn2 = nn.BatchNorm2d(out_channels)
        if self.use_1x1conv:
            self.bn3 = nn.BatchNorm2d(out_channels)

    def forward(self, inputs):
        y = F.relu(self.bn1(self.conv1(inputs)))
        y = self.bn2(self.conv2(y))
        if self.use_residual:
            if self.use_1x1conv:  # 如果为真,对inputs进行1×1卷积,将形状调整成跟conv2的输出y一致
                shortcut = self.shortcut(inputs)
                shortcut = self.bn3(shortcut)
            else:  # 否则直接将inputs和conv2的输出y相加
                shortcut = inputs
            y = torch.add(shortcut, y)
        out = F.relu(y)
        return out

1.2 残差网络的整体结构

其中为了便于理解,可以将ResNet18网络划分为6个模块:

第一模块:包含了一个步长为2,大小为7×7的卷积层,卷积层的输出通道数为64,卷积层的输出经过批量归一化、ReLU激活函数的处理后,接了一个步长为2的3×3的最大汇聚层;
第二模块:包含了两个残差单元,经过运算后,输出通道数为64,特征图的尺寸保持不变;
第三模块:包含了两个残差单元,经过运算后,输出通道数为128,特征图的尺寸缩小一半;
第四模块:包含了两个残差单元,经过运算后,输出通道数为256,特征图的尺寸缩小一半;
第五模块:包含了两个残差单元,经过运算后,输出通道数为512,特征图的尺寸缩小一半;
第六模块:包含了一个全局平均汇聚层,将特征图变为1×1的大小,最终经过全连接层计算出最后的输出。
 

模块一

#模块一
def make_first_module(in_channels):
    # 模块一:7*7卷积、批量规范化、汇聚
    m1=nn.Sequential(nn.Conv2d(in_channels,64,7,stride=2,padding=3),
                     nn.BatchNorm2d(64),nn.ReLU(),
                     nn.MaxPool2d(kernel_size=3,stride=2,padding=1))
    return m1

模块二至五

#模块二至五
def resnet_module(input_channels, out_channels, num_res_blocks, stride=1, use_residual=True):
    blk = []
    # 根据num_res_blocks,循环生成残差单元
    for i in range(num_res_blocks):
        if i == 0: # 创建模块中的第一个残差单元
            blk.append(ResBlock(input_channels, out_channels,
                                stride=stride, use_residual=use_residual))
        else:      # 创建模块中的其他残差单元
            blk.append(ResBlock(out_channels, out_channels, use_residual=use_residual))
    return blk

封装模块

#封装模块二到五
def make_modules(use_residual):
    # 模块二:包含两个残差单元,输入通道数为64,输出通道数为64,步长为1,特征图大小保持不变
    m2 = nn.Sequential(*resnet_module(64, 64, 2, stride=1, use_residual=use_residual))
    # 模块三:包含两个残差单元,输入通道数为64,输出通道数为128,步长为2,特征图大小缩小一半。
    m3 = nn.Sequential(*resnet_module(64, 128, 2, stride=2, use_residual=use_residual))
    # 模块四:包含两个残差单元,输入通道数为128,输出通道数为256,步长为2,特征图大小缩小一半。
    m4 = nn.Sequential(*resnet_module(128, 256, 2, stride=2, use_residual=use_residual))
    # 模块五:包含两个残差单元,输入通道数为256,输出通道数为512,步长为2,特征图大小缩小一半。
    m5 = nn.Sequential(*resnet_module(256, 512, 2, stride=2, use_residual=use_residual))
    return m2, m3, m4, m5

定义完整网络

# 定义完整网络
class Model_ResNet18(nn.Module):
    def __init__(self, in_channels=3, num_classes=10, use_residual=True):
        super(Model_ResNet18, self).__init__()
        m1 = make_first_module(in_channels)
        m2, m3, m4, m5 = make_modules(use_residual)
        # 封装模块一到模块6
        self.net = nn.Sequential(m1, m2, m3, m4, m5,
                                 # 模块六:汇聚层、全连接层
                                 nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(512, num_classes))

    def forward(self, x):
        return self.net(x)

 使用torch.summary统计模型的参数量

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = Model_ResNet18(in_channels=1, num_classes=10, use_residual=True).to(device)
torchsummary.summary(model, (1, 32, 32))

 

使用torchstat统计模型的计算量

from torchstat import stat
model = Model_ResNet18(in_channels=1, num_classes=10, use_residual=True)
stat(model, (1, 32, 32))

 

2.模型训练

5.4.2.1 模型训练

import plot
from torch.utils.data import DataLoader,Dataset
import json
import gzip
import torchvision.transforms as transforms
import numpy as np
from PIL import Image
import torch.optim as opt
from Runner import RunnerV3
from metric import Accuracy
# 打印并观察数据集分布情况
train_set, dev_set, test_set = json.load(gzip.open('./mnist.json.gz'))
train_images, train_labels = train_set[0][:1000], train_set[1][:1000]
dev_images, dev_labels = dev_set[0][:200], dev_set[1][:200]
test_images, test_labels = test_set[0][:200], test_set[1][:200]
train_set, dev_set, test_set = [train_images, train_labels], [dev_images, dev_labels], [test_images, test_labels]
 
# 数据预处理
transforms = transforms.Compose([transforms.Resize(32),transforms.ToTensor(), transforms.Normalize(mean=[0.5], std=[0.5])])
 
 
class MNIST_dataset(Dataset):
    def __init__(self, dataset, transforms, mode='train'):
        self.mode = mode
        self.transforms = transforms
        self.dataset = dataset
 
    def __getitem__(self, idx):
        # 获取图像和标签
        image, label = self.dataset[0][idx], self.dataset[1][idx]
        image, label = np.array(image).astype('float32'), int(label)
        image = np.reshape(image, [28, 28])
        image = Image.fromarray(image.astype('uint8'), mode='L')
        image = self.transforms(image)
 
        return image, label
 
    def __len__(self):
        return len(self.dataset[0])
 
 
 
# 加载 mnist 数据集
train_dataset = MNIST_dataset(dataset=train_set, transforms=transforms, mode='train')
test_dataset = MNIST_dataset(dataset=test_set, transforms=transforms, mode='test')
dev_dataset = MNIST_dataset(dataset=dev_set, transforms=transforms, mode='dev')
 
# 学习率大小
lr = 0.005
# 批次大小
batch_size = 64
# 加载数据
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
dev_loader = DataLoader(dev_dataset, batch_size=batch_size)
test_loader = DataLoader(test_dataset, batch_size=batch_size)
# 定义网络,不使用残差结构的深层网络
model = Model_ResNet18(in_channels=1, num_classes=10, use_residual=False)
# 定义优化器
optimizer = opt.SGD(model.parameters(), lr)
loss_fn = F.cross_entropy
# 定义评价指标
metric = Accuracy()
# 实例化RunnerV3
runner = RunnerV3(model, optimizer, loss_fn, metric)
# 启动训练
log_steps = 15
eval_steps = 15
runner.train(train_loader, dev_loader, num_epochs=5, log_steps=log_steps,
            eval_steps=eval_steps, save_path="best_model.pdparams")
# 可视化观察训练集与验证集的Loss变化情况
plot.plot(runner, 'cnn-loss2.pdf')

其中RuunnerV3和Accuracy两个是之前写过的类之类把它变成.py文件直接调用。 

  • 17
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ResNet-50是一个经典的深度卷神经网络模型,其中50代表网络的层数。而MNIST是一个经典的手写数字识别数据集,包含了60000个训练样本和10000个测试样本,每个样本都是大小为28x28的灰度图像。 将ResNet-50应用于MNIST数据集时,需要对两者进行适配。首先,MNIST数据集是一个灰度图像数据集,而ResNet-50通常用于彩色图像识别,因此需要将MNIST数据集的图像转为RGB格式。方法是将MNIST图像的每个像素值复制三次,形成一个具有三个通道(RGB)的图像。此外,MNIST数据集中的图像尺寸为28x28,而ResNet-50要求输入图像的尺寸为224x224,因此需要对图像进行缩放。 在使用ResNet-50训练MNIST数据集时,可以使用预训练的ResNet-50模型进行迁移学习。通过在模型的最后一层添加一个全连接层,并将其输出节点数设置为10(对应MNIST数据集中0到9的数字类别),然后初始化全连接层的权重参数。接着,使用MNIST数据集进行训练,通常会使用交叉熵损失函数和随机梯度下降等优化算法。 通过这样的适配和训练,可以使得ResNet-50模型在MNIST数据集上学习到更好的特征表示,从而提高手写数字识别的准确率。然而,由于MNIST数据集相对简单,ResNet-50这样复杂的模型可能会导致过拟合问题,因此可能需要适当的正则化方法(如L1或L2正则化)来缓解过拟合。另外,还可以采用一些数据增强的技术,如随机旋转、平移或缩放等,来增加训练样本的多样性,提高模型的鲁棒性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值