数据集准备:准备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
:配置文件 ,用于保存和管理项目的超参数、路径、模型配置。
实现了一个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
训练给定模型,更新模型的权重,计算损失和准确率,并在训练过程中进行打印和记录
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_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在识别范围)
第一个函数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转换为百分比
一些参数放里面,方便调整
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 # 输入数据是否为灰度图像
用PyTorch提供的torchvision.datasets
和torch.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
'''
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)