采用残差网络 ResNet18 或 ResNet34 深度神经网络对CIFAR-10图像数据集实现分类,计算模型预测性能(top1-ACC,top3-ACC),并以友好的方式图示化结果。

目录

1.定义resnet模型resnet.py

2.模型训练

# 代码


开发环境:win11 + Python 3.8.8 + Pytorch 1.10.0 + Pycharm

运算设备:cpu

本文采用残差网络ResNet18深度神经网络对CIFAR-10图像实现分类。本文由三个.py文件实现, 分别为resnet.py,Resnet_18.py,acc_loss.py文件。其中resnet.py为神经网络模型;Resnet_18.py用于训练测试模型并计算模型对CIFAR-10图像数据分类的准确率和损失率;acc_loss.py负责画模型的损失图和精度图。

1.定义resnet模型resnet.py

本文件主要是resnet18的主要架构,可以分为实现残差块ResBlock的基模块部分,和实现ResNet18两个class。

pytorch 残差 python残差神经网络_pytorch 残差

残差块ResBlock:定义了残差块内连续的两个卷积层和一个捷径shortcut,定义前向输出函数,将两个卷积层的输出和经过shortcut处理过的图像相加,是组成ResNet的基本结构。

1. Conv1:也就是第一层卷积,没有shortcut机制。

2. Conv2:第一个残差块,一共有2个。

3. Conv3:第二个残差块,一共有2个。

4. Conv4:第三个残差块,一共有2个。

5. Conv5:第四个残差块,一共有2个。

6. fc:全连阶层

ResNet: 将第一层卷积与四层不同通道数的ResBlock和全连接层连接构成一个resnet18网络。

2.模型训练

本模型训练所用计算设备为cpu。

创建了一个训练函数,主要用来进行循环训练,并在每轮训练结束后,打印本轮的训练集精度与测试集精度等训练信息。

设置参数,训练回合epoch=10;batch_size=128,分批训练样本,每批次样本数量为128;学习率lr=0.01。

pytorch 残差 python残差神经网络_网络_02

pytorch 残差 python残差神经网络_深度学习_03

图1 10_EPOCH train loss_acc 

图2 20_EPOCH train loaa_acc

图1 一共循环了10个回合,损失函数已经几乎不再减小了,训练集精度也趋于恒定。可以从测试准确度test acc上看到,该模型的饱和测试集训练精度可以勉强达到0.9,满足了实验要求。

从图1图2的训练损失率和准确率的plot中可以观察到:(1)在同等迭代次数下,top3acc要明显高于投票1acc的值;(2)在迭代次数达到391次后,即epoch=1,也就是模型刚好完成第一回合的训练后,模型的训练精度top1acc、top3acc显著提高,训练损失率明显降低;(3)loss、acc、top3acc这三条曲线均在EPOCH交替处出现明显波动。

从图1图2对比来看,图2训练epoch为20次,为图1训练epoch的2倍,最佳top1acc为96.6%,top3acc99.9%接近100%,明显高于图1的90%和98%;loss控制在0.185到0.11之间明显低于图1的0.35。

 

pytorch 残差 python残差神经网络_深度学习_04

pytorch 残差 python残差神经网络_网络_05

 图3 10_EPOCH test_acc

图4 20_EPOCH test_acc

从图3、4中可以看到测试精度随着迭代次数的升高而有升高的趋势,最大测试精度约为88.10%,迭代次数约为7046次(18EPOCH)。

由于时间限制,本次实验epoch进行20次为止,可满足cifar_10分类要求。

# 代码

resnet.py

import torch
import torch.nn as nn
import torch.nn.functional as F

# 定义残差块ResBlock
class ResBlock(nn.Module):
    def __init__(self, inchannel, outchannel, stride=1):
        super().__init__()
        # 这里定义了残差块内连续的2个卷积层
        # nn.Sequential一个有序的容器,神经网络模块将按照在传入构造器的顺序依次被添加到计算图中执行,
        # 同时以神经网络模块为元素的有序字典也可以作为传入参数。
        self.left = nn.Sequential(
            # 定义第一个卷积,默认卷积前后图像大小不变但可修改stride使其变化,通道可能改变
            nn.Conv2d(inchannel, outchannel, kernel_size=3, stride=stride, padding=1, bias=False),
            nn.BatchNorm2d(outchannel),  # 数据的归一化处理
            nn.ReLU(inplace=True),
            # 定义第二个卷积,卷积前后图像大小不变,通道数不变
            nn.Conv2d(outchannel, outchannel, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(outchannel)
        )
        # 定义一条捷径,若两个卷积前后的图像尺寸有变化(stride不为1导致图像大小变化或通道数改变),捷径通过1×1卷积用stride修改大小
        # 以及用expansion修改通道数,以便于捷径输出和两个卷积的输出尺寸匹配相加
        self.shortcut = nn.Sequential()
        if stride != 1 or inchannel != outchannel:
            # shortcut,这里为了跟2个卷积层的结果结构一致,要做处理
            self.shortcut = nn.Sequential(
                nn.Conv2d(inchannel, outchannel, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(outchannel)
            )

    # 定义前向传播函数,输入图像为x,输出图像为out
    def forward(self, x):
        out = self.left(x)
        # 将2个卷积层的输出跟处理过的x相加,实现ResNet的基本结构
        out = out + self.shortcut(x)
        out = F.relu(out)

        return out


class ResNet(nn.Module):
    # 定义初始函数,输入参数为残差块,默认参数为分类数10
    def __init__(self, ResBlock, num_classes=10):
        super().__init__()
        # 设置第一层的输入通道数
        self.inchannel = 64
        self.conv1 = nn.Sequential(
            # 定义输入图片先进行一次卷积与批归一化,使图像大小不变,通道数由3变为64得两个操作
            nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU()
        )
        # 定义第一层,输入通道数64,有num_blocks[0]=2个残差块,残差块中第一个卷积步长自定义为1
        self.layer1 = self.make_layer(ResBlock, 64, 2, stride=1)
        # 定义第二层,输入通道数128,有num_blocks[1]=2个残差块,残差块中第一个卷积步长自定义为2
        self.layer2 = self.make_layer(ResBlock, 128, 2, stride=2)
        self.layer3 = self.make_layer(ResBlock, 256, 2, stride=2)
        self.layer4 = self.make_layer(ResBlock, 512, 2, stride=2)
        # 定义全连接层,输入512个神经元,输出10个分类神经元
        self.fc = nn.Linear(512, num_classes)

    # 这个函数主要是用来,重复同一个残差块
    # 定义创造层的函数,在同一层中通道数相同,输入参数为残差块,通道数,残差块数量,步长
    def make_layer(self, block, channels, num_blocks, stride):
        # strides列表第一个元素stride表示第一个残差块第一个卷积步长,其余元素表示其他残差块第一个卷积步长为1
        strides = [stride] + [1] * (num_blocks - 1)
        # 创建一个空列表用于放置层
        layers = []
        # 遍历strides列表,对本层不同的残差块设置不同的stride
        for stride in strides:
            layers.append(block(self.inchannel, channels, stride))  # 创建残差块添加进本层
            self.inchannel = channels  # 更新本层下一个残差块的输入通道数或本层遍历结束后作为下一层的输入通道数
        return nn.Sequential(*layers)  # 返回层列表

    # 定义前向传播函数,输入图像为x,输出预测数据
    def forward(self, x):
        # 在这里,整个ResNet18的结构就很清晰了
        out = self.conv1(x)
        out = self.layer1(out)
        out = self.layer2(out)
        out = self.layer3(out)
        out = self.layer4(out)
        out = F.avg_pool2d(out, 4)  # 经过一次4×4的平均池化
        out = out.view(out.size(0), -1)  # 将数据flatten平坦化
        out = self.fc(out)  # 全连接传播
        return out

def ResNet18():

    return ResNet(ResBlock)

 Resnet_18.py 用于训练和测试cifar-10数据

from resnet import ResNet18
# Use the ResNet18 on Cifar-10
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import torch
import torch.nn as nn
import argparse
import os


# check gpu
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 参数设置,使得我们能够手动输入命令行参数,就是让风格变得和Linux命令行差不多
parser = argparse.ArgumentParser(description='PyTorch CIFAR10 Training')
parser.add_argument('--outf', default='./model/', help='folder to output images and model checkpoints') #输出结果保存路径
args = parser.parse_args()

# set hyperparameter
EPOCH = 20
pre_epoch = 0
BATCH_SIZE = 128
LR = 0.01

# prepare dataset and preprocessing
transform_train = transforms.Compose([
    transforms.RandomCrop(32, padding=4),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])

transform_test = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])

trainset = torchvision.datasets.CIFAR10(root='../data', train=True, download=True, transform=transform_train)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=BATCH_SIZE, shuffle=True, num_workers=2)

testset = torchvision.datasets.CIFAR10(root='../data', train=False, download=True, transform=transform_test)
testloader = torch.utils.data.DataLoader(testset, batch_size=100, shuffle=False, num_workers=2)

# labels in CIFAR10
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

# define ResNet18 模型
net = ResNet18().to(device)

# define loss funtion & optimizer # 定义损失函数和优化方式
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=LR, momentum=0.9, weight_decay=5e-4)

# train
if __name__=='__main__':
    if not os.path.exists(args.outf):
        os.makedirs(args.outf)
    best_acc = 85  # 2 初始化best test accuracy
    print("Start Training, Resnet-18!")  # 定义遍历数据集的次数
    with open("acc.txt", "w") as f:
        with open("log.txt", "w") as f2:
            for epoch in range(pre_epoch, EPOCH):  # 循环训练回合,每回合会以批量为单位训练完整个训练集,一共训练EPOCH个回合
                print('\nEpoch: %d' % (epoch + 1))
                # 每一训练回合初始化累计训练损失函数为0.0,累计训练正确样本数为0.0,训练样本总数为0//start为开始计时的时间点
                net.train()
                sum_loss = 0.0
                correct = 0.0
                correct_3 = 0.0
                total = 0.0

                for i, data in enumerate(trainloader, 0):  # 循环每次取一批量的图像与标签
                    # prepare dataset
                    length = len(trainloader)
                    inputs, labels = data
                    inputs, labels = inputs.to(device), labels.to(device) # 将图像和标签分别搬移至指定设备上
                    optimizer.zero_grad()  # 优化器梯度清零

                    # forward & backward
                    outputs = net(inputs)  # 将批量图像数据inputs输入网络模型net,得到输出批量预测数据ouputs
                    loss = criterion(outputs, labels)  # 计算批量预测标签outputs与批量真实标签labels之间的损失函数loss
                    loss.backward()  # 对批量损失函数loss进行反向传播计算梯度
                    optimizer.step()  # 优化器的梯度进行更新,训练所得参数也更新

                    # print ac & loss in each batch
                    sum_loss += loss.item()  # 将本批量损失函数loss加至训练损失函数累计sum_loss中
                    # top_1
                    # 返回输出的最大值(不care)和最大值对应的索引,dim=1表示输出所在行 的最大值,
                    # 10分类问题对应1行由10个和为1的概率构成,返回概率最大的作为预测索引
                    _, predicted = torch.max(outputs.data, 1)
                    correct += predicted.eq(labels.data).cpu().sum()  # 将本批量预测正确的样本数加至累计预测正确样本数correct中
                    # top_3
                    maxk = max((1, 3))
                    y_resize = labels.view(-1, 1)
                    _, pred = outputs.topk(maxk, 1, True, True)
                    correct_3 += torch.eq(pred, y_resize).sum().float().item()
                    total += labels.size(0)  # 将本批量训练的样本数,加至训练样本总数
                    Train_epoch_loss = sum_loss / (i + 1)
                    Train_epoch_acc = 100. * correct / total
                    Train_epoch_acc_3 = 100. * correct_3 / total
                    print('epoch:%d | iter:%d | Loss: %.03f | top1Acc: %.3f%% | top3Acc: %.3f%%'
                          % (epoch + 1, (i + 1 + epoch * length), sum_loss / (i + 1), 100. * correct / total, 100. * correct_3 / total))
                    f2.write('epoch:%03d | iter:%05d | Loss: %.03f | top1Acc: %.3f%% | top3Acc: %.3f%%'
                             % (epoch + 1, (i + 1 + epoch * length), sum_loss / (i + 1), 100. * correct / total, 100. * correct_3 / total))
                    f2.write('\n')
                    f2.flush()

                    
                # get the ac with testdataset in each epoch # 每训练完一个epoch测试一下准确率
                print('Waiting Test...')
                with torch.no_grad():
                    correct = 0
                    total = 0
                    for data in testloader:
                        net.eval()
                        images, labels = data
                        images, labels = images.to(device), labels.to(device)
                        outputs = net(images)
                        """
                        loss = criterion(outputs, labels)  # 计算批量预测标签outputs与批量真实标签labels之间的损失函数loss
                        # loss.requires_grad_(True)  # 传入一个参数requires_grad=True, 这个参数表示是否对这个变量求梯度, 默认的是False, 也就是不对这个变量求梯度。
                        loss.backward()  # 对批量损失函数loss进行反向传播计算梯度
                        optimizer.step()  # 优化器的梯度进行更新,训练所得参数也更新
                        # print ac & loss in each batch
                        sum_loss += loss.item()  # 将本批量损失函数loss加至训练损失函数累计sum_loss中
                        """
                        _, predicted = torch.max(outputs.data, 1)
                        total += labels.size(0)
                        correct += (predicted == labels).sum()
                    print('test acc: %.3f%%' % (100 * correct / total))
                    top1Acc = 100. * correct / total

                    # 将每次测试结果实时写入acc.txt文件中
                    print('Saving model......')
                    torch.save(net.state_dict(), '%s/net_%03d.pth' % (args.outf, epoch + 1))
                    f.write("EPOCH=%03d,Accuracy= %.3f%%" % (epoch + 1, top1Acc))
                    f.write('\n')
                    f.flush()

                   
                    # 记录最佳测试分类准确率并写入best_acc.txt文件中
                    if top1Acc > best_acc:
                        f3 = open("best_acc.txt", "w")
                        f3.write("EPOCH=%d,best_acc= %.3f%%" % (epoch + 1, top1Acc))
                        f3.close()
                        best_acc = top1Acc
            print("Training Finished, TotalEPOCH=%d" % EPOCH)


FromLogTxt_draw_LossAccuracy.py 把log日志的acc和loss数据可视化为曲线


import matplotlib.pyplot as plt
from matplotlib import ticker
from matplotlib.ticker import MultipleLocator
x = []
loss_y = []
acc_y = []
Top3acc_y = []

with open("log.txt") as f:
    for line in f:
        line = line.strip()  # str
        # print(line)
        if len(line.split(" | ")) == 5:
            x.append(float(line.split(" | ")[1].split(':')[1]))  # iter
            loss_y.append(float(line.split(" | ")[2].split(':')[1]))  # Loss
            acc_y.append(float(line.split(" | ")[3].split(':')[1].split('%')[0]))  # acc
            Top3acc_y.append(float(line.split(" | ")[4].split(':')[1].split('%')[0]))  # top3acc
'''
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111)
plt.title('loss_acc')
plt.xlabel('iter')
plt.ylabel('loss')
tick_spacing = 0.25        # 通过修改tick_spacing的值可以修改y轴的密度
ax.yaxis.set_major_locator(ticker.MultipleLocator(tick_spacing ))
plt.plot(x, loss_y, label="loss")
plt.legend(loc='upper right')
plt.show()
'''

fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x, loss_y, '-', label='loss', color='g' )
ax2 = ax.twinx()  # 使用twinx(),得到与ax1 对称的ax2,共用一个x轴,y轴对称(坐标不对称)
ax2.plot(x, acc_y, '-', label='acc', color='r')
ax2.plot(x, Top3acc_y, '-r', label='top3acc', color='y')
ax.legend(loc='upper left')
ax.grid()
plt.title('loss_acc')
ax.set_xlabel("iter")
ax.set_ylabel("loss")
ax2.set_ylabel("acc")
ax.set_ylim(0,2.5)
ax2.set_ylim(0, 100)
ax2.legend(loc='upper right')
plt.savefig('loss_acc.png')
plt.show()

epoch = []
acc = []

with open("acc.txt") as f:
    for line in f:
        line = line.strip()  # str
        # print(line)
        epoch.append(float(line.split("=")[1].split(',')[0]))  # epoch
        acc.append(float(line.split("=",2)[2].split('%')[0]))  # acc

fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111)
plt.title('test_acc')
plt.xlabel('epoch')
plt.ylabel('acc')

plt.plot(epoch, acc, label="acc")
plt.legend(loc='upper right')
plt.savefig('test_acc.png')
plt.show()