基于Resnet18的minist手写数字分类

该项目详细介绍了如何使用ResNet-18模型对MNIST数据集进行图像分类。首先准备数据集,将其分为训练集和测试集,然后选择ResNet-18模型并搭建网络结构。接着进行数据预处理,包括归一化和转换为RGB。使用交叉熵损失函数和随机梯度下降优化器训练模型,并通过测试集评估模型性能。最后,代码示例展示了模型定义、训练和测试过程。
摘要由CSDN通过智能技术生成

项目大体实现功能:

  • 数据集准备:准备MNIST数据集。这个数据集包含了手写数字的灰度图像,每张图像的大小为28x28像素。将数据集分为训练集和测试集,通常使用60,000张图像进行训练,10,000张图像用于测试。

  • 模型选择:由于MNIST是一个相对简单的图像分类问题,可以选择使用较小的ResNet模型,如ResNet-18或ResNet-34。这些模型具有较少的层和参数,适合处理小规模图像。

  • 模型搭建:根据选择的ResNet模型架构,搭建网络结构。ResNet的核心是残差块(residual block),可以根据网络深度使用多个残差块堆叠起来构建整个网络。

  • 数据预处理:对于MNIST数据集,通常进行简单的预处理操作,如将像素值归一化到0到1之间,并将图像从灰度转换为RGB格式(因为ResNet通常接受3通道输入)。还可以添加一些数据增强技术,如随机旋转、平移和翻转,以增加数据的多样性。

  • 损失函数和优化器:对于MNIST的多类别分类任务,可以选择交叉熵损失函数作为模型的损失函数。针对优化器,常见的选择是使用随机梯度下降(SGD)或Adam优化器。

  • 训练模型:使用训练集对模型进行训练。需要定义适当的训练循环,包括前向传播、计算损失、反向传播和参数更新。通过调整超参数(如学习率、批次大小和训练迭代次数),优化模型的性能。

  • 模型评估:使用测试集评估训练得到的模型的性能。计算模型在测试集上的准确率、精确率、召回率等指标,了解模型在未见过的数据上的表现。

    (If possible)

  • 超参数调优:尝试不同的超参数组合,如学习率、批次大小、正则化参数等,通过验证集的性能来选择最佳的超参数设置。这有助于提高模型的泛化能力。

  • 结果分析和改进:分析模型在训练和测试集上的表现,并尝试改进模型性能。

项目总架构:

main.py:这是项目的主要入口文件,用于执行整个训练和测试流程

model.py:这个文件包含了ResNet模型的定义。在这里定义ResNet的网络结构、残差块等

data_loader.py:这个文件负责数据集的加载和预处理。

train.py:这个文件包含训练过程的代码。编写训练循环,包括前向传播、计算损失、反向传播和参数更新等

test.py:这个文件包含测试过程的代码。在这里编写测试循环,用于评估训练好的模型在测试集上的性能。

utils.py:这个文件可以包含一些辅助函数或工具函数

config.py:配置文件 ,用于保存和管理项目的超参数、路径、模型配置。

代码(附注释)

moudel.py

实现了一个ResNet-18模型,用于图像分类。ResNet模型采用残差连接解决深层网络中的梯度消失和信息丢失问题,通过堆叠残差块构建深层网络。模型包含卷积、池化、批归一化和全连接层,能够对输入图像进行特征提取和分类预测。

from utils import *
from data_loader import *
import torch.nn as nn
import torch.nn.functional as F

class BasicBlock(nn.Module):
    expansion = 1  # 基本块的扩展系数。在ResNet中,基本块的输出通道数是输入通道数的expansion倍

    def __init__(self, in_channels, out_channels, stride=1, downsample=None):  # downsample下采样层
        super(BasicBlock, self).__init__()
        self.conv1 = conv3x3(in_channels, out_channels, stride)  # 3x3卷积层,它将输入特征图转换为具有out_channels通道数的特征图。步幅由stride参数指定
        self.bn1 = nn.BatchNorm2d(out_channels)  # 2D批归一化层,用于归一化卷积层的输出
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = conv3x3(out_channels, out_channels)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.downsample = downsample  # 可选的下采样层,用于匹配输入和输出的维度,以便能够进行残差连接
        self.stride = stride

    def forward(self, x):
        residual = x  # 将输入x保存为residual,以便后续将其添加到卷积块的输出上,形成残差连接

        y = self.conv1(x)
        y = self.bn1(y)
        y = self.relu(y)

        y = self.conv2(y)
        y = self.bn2(y)

        if self.downsample is not None:  # 检查是否存在下采样操作
            residual = self.downsample(x)  # 果存在下采样操作,对输入x进行下采样,得到residual作为残差

        y += residual  # 将残差residual与经过卷积和批归一化后的特征图y相加,实现残差连接
        y = self.relu(y)

        return y


class ResNet(nn.Module):

    def __init__(self, block, layers, num_classes, grayscale):
        self.in_channels = 64  # 初始输入通道数为64
        if grayscale:  # 如果grayscale为True,则将输入通道数in_dim设置为1,表示灰度图像;
            in_dim = 1
        else:  # 否则,设置为3,表示RGB图像
            in_dim = 3
        super(ResNet, self).__init__()  # 调用父类nn.Module的初始化方法
        self.conv1 = nn.Conv2d(in_dim, 64, kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)  # inplace=True表示在原地执行操作,节省内存
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.layer1 = self._make_layer(block, 64, layers[0])  # layers->残差快数量
        self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
        self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
        self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
        self.avgpool = nn.AvgPool2d(7, stride=1)
        self.fc = nn.Linear(512 * block.expansion, num_classes)  # 定义全连接层fc,输入特征数量为512 * block.expansion,输出特征数量为num_classes,用于分类任务

        for m in self.modules():  # 遍历ResNet模型的所有模块
            if isinstance(m, nn.Conv2d):  # 如果当前模块是nn.Conv2d类型的
                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels  # 计算卷积核的参数数量
                m.weight.data.normal_(0, (2. / n) ** .5)  # 使用计算得到的卷积核参数数量(n)来对卷积核权重进行初始化,采用的是均值为0,标准差为(2. / n) ** .5的正态分布
            elif isinstance(m, nn.BatchNorm2d):  # 如果当前模块是nn.BatchNorm2d类型的
                m.weight.data.fill_(1)  # 将批归一化层的权重初始化为1
                m.bias.data.zero_()  # 将批归一化层的偏置项初始化为0

    def _make_layer(self, block, out_channels, blocks, stride=1):  # 定义构建残差层的过程。它接受块类型block、输出通道数out_channels、块的数量blocks和步幅stride作为参数
        downsample = None
        if stride != 1 or self.in_channels != out_channels * block.expansion:  # 如果步幅不为1或输入通道数与输出通道数不匹配
            downsample = nn.Sequential(                                        # 创建下采样层downsample,由一个1x1卷积层和一个批归一化层组成(为了解决在残差连接中输入和输出尺寸不匹配的问题)
                nn.Conv2d(self.in_channels, out_channels * block.expansion,
                          kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_channels * block.expansion),
            )

        layers = []  # 初始化层列表
        layers.append(block(self.in_channels, out_channels, stride, downsample))  # 将第一个残差块添加到层列表中,并更新输入通道数
        self.in_channels = out_channels * block.expansion  # 更新输入通道数为当前输出通道数乘以块的扩展系数
        '''
           为什么第一颗残差快单独考虑:

           第一个残差块的输入是经过初始卷积操作得到的特征图,输入尺寸与后续的残差块的输入尺寸不同。

           第一个残差块之后的残差块(layer1,layer2,layer3,layer4)的输入通道数与输出通道数是一致的,因为在每个残差块内部已经通过卷积操作将输入通道数进行了调整。

           而第一个残差块的输入通道数则由初始卷积操作的输出通道数决定,通常为64。

           因此,为了适应这个特殊情况,需要单独将第一个残差块添加到层列表中,并更新self.inchannels变量,使其与第一个残差块的输出通道数保持一致。

           这样在后续的残差块中,self.inchannels的值就会与输出通道数自动匹配,确保网络的连续性和正确性。
        '''
        for i in range(1, blocks):  # 迭代构建剩余的残差块
            layers.append(block(self.in_channels, out_channels))

        return nn.Sequential(*layers)  # 将层列表转换为顺序容器nn.Sequential并返回

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)
    # 以下通过残差层向前传播
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        # because MNIST is already 1x1 here:
        # disable avg pooling
        # x = self.avgpool(x)

        x = x.view(x.size(0), -1)  # 将特征图展平为向量
        logits = self.fc(x)  # 通过全连接层计算预测的logits
        probas = F.softmax(logits, dim=1)  # 对logits进行softmax操作得到预测概率
        return logits, probas


def resnet18(num_classes):
    model = ResNet(block=BasicBlock,
                   layers=[2, 2, 2, 2],  # 有四个残差层,每个残差层都由两个块组成
                   num_classes=NUM_CLASSES,
                   grayscale=GRAYSCALE)
    return model

train.py

训练给定模型,更新模型的权重,计算损失和准确率,并在训练过程中进行打印和记录

from model import *
import time
import torch
import torch.nn.functional as F

def trainModel(model, optimizer, scheduler):

    start_time = time.time()
    for epoch in range(NUM_EPOCHS):

        model.train()  #  将模型设置为训练模式,启用Batch Normalization和Dropout层
        for batch_idx, (features, targets) in enumerate(train_loader):  # 迭代训练数据集,使用enumerate(train_loader)函数返回一个可迭代的对象,
                                                                        # 其中每次迭代返回一个包含两个元素的元组(batch_idx, (features, targets))
            features = features.to(DEVICE)
            targets = targets.to(DEVICE)

            logits, probas = model(features)  # 将输入特征features作为模型的输入,调用模型对象model进行前向传播计算
            cost = F.cross_entropy(logits, targets)
            # 清零优化器的梯度缓存
            optimizer.zero_grad()
            # 反向传播并计算梯度
            cost.backward()
            # 更新权重
            optimizer.step()

            if not batch_idx % 50:
                print('Epoch: %03d/%03d | Batch %04d/%04d | Cost: %.4f' % (epoch+1, NUM_EPOCHS, batch_idx, len(train_loader), cost))

        model.eval()  # 将模型设置为评估模式,禁用Batch Normalization和Dropout层
        with torch.set_grad_enabled(False):  # 关闭梯度计算,节省内存
            print('Epoch: %03d/%03d | Train: %.3f%%' % (
                epoch + 1, NUM_EPOCHS,
                compute_accuracy(model, train_loader, device=DEVICE)))
        scheduler.step()
        print('Time elapsed: %.2f min' % ((time.time() - start_time) / 60))

    print('Total Training Time: %.2f min' % ((time.time() - start_time) / 60))

test.py

定义了一个测试和展示模型结果的函数test_and_show。测试给定模型在测试数据集上的准确率,并展示其中第一个样本(7)的图像和模型对该样本的预测结果

from model import *
import numpy as np
import torch
import matplotlib.pyplot as plt

def test_and_show(model):
    with torch.set_grad_enabled(False):  # 关闭梯度计算。这可以节省内存,因为在测试阶段不需要计算梯度
        print('Test accuracy: %.2f%%' % (compute_accuracy(model, test_loader, device=DEVICE)))
    for batch_idx, (features, targets) in enumerate(test_loader):

        features = features
        targets = targets
        break

    nhwc_img = np.transpose(features[0], axes=(1, 2, 0))
    nhw_img = np.squeeze(nhwc_img.numpy(), axis=2)  # 从张量中挤压(squeeze)掉大小为1的维度。在这里,我们将挤压掉通道维度,得到一个形状为(H, W)的灰度图像
    plt.imshow(nhw_img, cmap='Greys')
    plt.show()

    model.eval()

    logits, probas = model(features.to(DEVICE)[0, None])  # 对第一个样本进行模型的前向传播,得到预测的logits和概率
    print('Probability 7 =  %.2f%%' % (probas[0][7] * 100))  # 获取类别7的概率值,[0] 表示取出第一个样本的预测结果,[7] 表示取出预测结果中的第 8 个元素(0在识别范围)


utils.py

第一个函数conv3x3用于创建一个3x3的卷积层。该函数接受输入通道数in_channels、输出通道数out_channels和步幅Mystride作为参数,并返回一个卷积层对象。

第二个函数compute_accuracy用于计算模型在给定数据加载器data_loader上的准确率

import torch
import torch.nn as nn


def conv3x3(in_channels, out_channels, Mystride=1):
    return nn.Conv2d(in_channels, out_channels, kernel_size=3,
                     stride=Mystride,padding=1,bias=False)


def compute_accuracy(model, data_loader, device):
    correct_pred, num_examples = 0, 0  # 记录正确预测的样本数和总样本数
    for i, (features, targets) in enumerate(data_loader):  # 使用enumerate遍历data_loader,以获取索引(i)和数据批次(features,targets)
        features = features.to(device)
        targets = targets.to(device)

        logits, probas = model(features)  # 对输入特征进行模型的前向传播,得到预测的logits和概率
        _, predicted_labels = torch.max(probas, 1)  # 在probas张量的第二个维度(dim=1)上找到最大概率的索引,表示预测的类标签。最大概率的实际值被舍弃(用_表示)
        num_examples += targets.size(0)  # 将当前批次的大小(即批次中的样本数)添加到num_examples变量中,增加总样本数
        correct_pred += (predicted_labels == targets).sum()  # 过将预测的标签与目标标签进行比较,并计算匹配的数量,统计正确预测的样本数
    return correct_pred.float() / num_examples * 100  # 通过将正确预测的样本数除以总样本数,计算准确率,并乘以100转换为百分比

config.py

一些参数放里面,方便调整

import torch

# Hyperparameters
RANDOM_SEED = 1  # 设置随机数生成器的种子
LEARNING_RATE = 0.001
BATCH_SIZE = 128
NUM_EPOCHS = 100

# Architecture
NUM_FEATURES = 28*28  # 输入数据的特征数量,MNIST数据集中的图像大小为28x28,所以特征数量为28x28=784
NUM_CLASSES = 10

# Other
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
GRAYSCALE = True  # 输入数据是否为灰度图像

dataloader.py

用PyTorch提供的torchvision.datasetstorch.utils.data.DataLoader模块加载和处理数据集

from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision import transforms
from config import *   # 引用config里全部参数

# 定义数据增强的转换操作
train_transform = transforms.Compose([
    transforms.RandomResizedCrop(32),  # 对图像进行随机裁剪,将其大小调整为指定的尺寸,随机裁剪可以提取图像的不同部分,增加数据的多样性。
    transforms.RandomHorizontalFlip(),  # 以一定的概率对图像进行随机水平翻转。通过翻转图像,可以增加数据的多样性,使模型更具鲁棒性
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.5], std=[0.5])  # 对图像进行标准化处理,使其像素值在均值为0,标准差为1的范围内。标准化可以帮助模型更好地学习数据的分布,加速训练过程
])

train_dataset = datasets.MNIST(root='data',  # 指定数据集的保存路径
                               train=True,  # 加载训练集
                               transform=train_transform,  # 应用数据增强
                               download=True)  # 如果数据集不存在,则自动从网上下载数据集

test_dataset = datasets.MNIST(root='data',
                              train=False,  # 加载测试集
                              transform=transforms.ToTensor())  # 将数据转换为Tensor类型

# 数据加载器
train_loader = DataLoader(dataset=train_dataset,
                          batch_size=BATCH_SIZE,
                          shuffle=True)  # 每个epoch开始时,对数据进行洗牌,以增加训练的随机性

test_loader = DataLoader(dataset=test_dataset,
                         batch_size=BATCH_SIZE,
                         shuffle=False)

#检查数据集
'''
for images, labels in train_loader:
    print('Image batch dimensions: ', images.shape)
    print('Image label dimensions: ', labels.shape)
    break
'''

main.py

from train import *
from test import *

device = torch.device(DEVICE)
torch.manual_seed(RANDOM_SEED)

model = resnet18(NUM_CLASSES)
model.to(DEVICE)

# optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)  如果用adam优化器就不需要再设置学习率调度器了(adam自带)
# p.s用adam效果好的多..但是为了多运用一点优化技巧就手动加优化了😭

optimizer = torch.optim.SGD(model.parameters(), lr=LEARNING_RATE)  # 优化器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)  # 学习率调度器

print("training on ", device)
trainModel(model=model, optimizer=optimizer, scheduler=scheduler)

test_and_show(model=model)

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值