【课程笔记】深度学习网络 - 1 - AlexNet

【前注】本文记载的课程笔记来源于:B站up主霹雳啪啦 - 图像分类课程

目录

〇、AlexNet理论与背景

1、AlexNet在当时具有的几个优越性

2、AlexNet的提出对深度学习领域产生的重大影响

3、后续改进方案

4、网络特征与创新点

一、补充知识点

1、过拟合

2、Dropout随机失活机制

3、卷积层计算后的矩阵尺寸大小

二、模型零件与封装

1、特征层的封装(feature)

2、分类器的封装(classifier)

三、模型对输入的处理

四、配置GPU设备与输入图像预处理

五、训练集 / 测试集的获取(DataLoader)

六、将索引 / 分类的数据存储为文件

七、损失函数与优化器定义

八、准备阶段

九、训练测试环节

十、Model代码全览

十一、Train&Test代码全览



〇、AlexNet理论与背景

        AlexNet是由Alex Krizhevsky、Ilya Sutskever和Geoffrey Hinton于2012年提出的深度卷积神经网络模型,是第一个成功应用于大规模图像分类任务的深度学习模型。

       

1、AlexNet在当时具有的几个优越性

        (1)模型架构创新:AlexNet引入了深度卷积神经网络的概念,并通过堆叠多层卷积层和池化层来提取图像特征。它的网络结构包含了8个卷积层和3个全连接层,其中使用ReLU激活函数来缓解梯度消失问题。

        (2)数据集和计算资源:AlexNet在当时首次使用了大规模的图像数据集ImageNet,该数据集包含超过100万张高分辨率图像,通过在大规模数据上进行训练,可以有效提高模型的泛化能力。同时,AlexNet也是第一个在当时广泛使用GPU进行训练的深度学习模型,利用GPU并行计算能力加速了训练过程。

        (3)卷积神经网络的突破:相比传统的浅层神经网络,AlexNet的引入推动了卷积神经网络的发展。它证明了深度网络可以在大规模图像分类任务上取得比传统方法更好的性能。

        

2、AlexNet的提出对深度学习领域产生的重大影响

        (1)图像分类:AlexNet在ImageNet图像分类挑战赛(ILSVRC)中获得了显著的优势,将错误率降低到当时最低水平,引起了广泛的关注和研究。

        (2)深度学习热潮:AlexNet的成功引爆了深度学习的热潮,促进了对深度神经网络的研究兴趣,并推动了后续模型和算法的发展。

3、后续改进方案

        在AlexNet之后,研究者们对深度卷积神经网络进行了不断的改进和优化,一些后续的改进方案包括:

        (1)VGGNet:VGGNet于2014年提出,通过使用更小尺寸的卷积核和更深的网络结构来进一步提升性能。

        (2)GoogLeNet:GoogLeNet于2014年提出,引入了Inception模块,通过并行使用不同大小的卷积核来捕捉不同尺度的特征。

        (3)ResNet:ResNet于2015年提出,通过引入残差连接解决了深层网络训练中的梯度消失问题,并且在ILSVRC比赛中取得了突破性的性能。

        这些改进方案(后续会逐步讲述)的出现不断提升了深度神经网络的性能和效果,在图像分类等领域取得了巨大成功。

4、网络特征与创新点

         以下是AlexNet的简述图解:

【课程笔记】深度学习网络 - 1 - AlexNet_第1张图片

        图中关于各层的信息数据如下图所示:

【课程笔记】深度学习网络 - 1 - AlexNet_第2张图片

        1、整个网络是从输入x开始,一直连系到输出。特征层包括Conv1~5,Maxpool1~3,分类器包括FC1~FC3,激活层使用的是Relu。

        2、网络分类器的前两层使用了Dropout环节,即随机失活神经元机制,该机制可以减少网络训练过拟合的可能。

        3、传统的激活函数使用sigmoid或tanh函数,但AlexNet采用了ReLU(Rectified Linear Unit)作为非线性激活函数。ReLU在计算上更加高效,并且能够缓解梯度消失问题,加速模型训练。

一、补充知识点

1、过拟合

        由于模型的设计过于复杂,或者参数过多,训练数据过少,噪声过多等原因,造成模型特征维度过多的情况, 使得训练出来的拟合的函数完美的预测了训练集的数据,但是对新数据或者测试集的预测效果奇差的现象。因此,过拟合现象是过度的拟合了训练数据,而没有考虑泛化能力的现象。

        (下图从左到右分别是:欠拟合,正常拟合,过拟合)

【课程笔记】深度学习网络 - 1 - AlexNet_第3张图片

2、Dropout随机失活机制

        Dropout通过随机地在神经网络的训练过程中丢弃一部分神经元的输出来缓解过拟合问题。具体来说,它在每次训练迭代中以一定的概率丢弃隐藏层的神经元,也就是将其输出置为零。这样做的效果是,强制模型不依赖单个神经元或某些特定的组合来进行预测,而是鼓励网络的多个部分共同学习,增加了模型的鲁棒性。

【课程笔记】深度学习网络 - 1 - AlexNet_第4张图片

        Dropout的优点有以下:

        (1)正则化效果:Dropout可以看作是对神经网络进行随机正则化的一种方式。通过丢弃神经元,它限制了模型的复杂性,减少了过拟合的风险。

        (2)参数独立性:通过随机丢弃神经元,Dropout使得不同神经元之间的参数更加独立。这样可以减少参数之间的共同适应性,增加了模型的鲁棒性和泛化能力。

        (3)训练加速:Dropout在训练过程中相当于对不同的子网络进行训练,这相当于对模型进行集成学习。由于每个子网络较小,训练起来更快,可以有效减少训练时间。

        【注意】测试时通常不会应用Dropout,而是使用全部的网络参数进行预测。因为测试时不需要减少参数或随机性,而是希望获得模型的最佳性能。

3、卷积层计算后的矩阵尺寸大小

        公式:

N=(W-F+2*P)/S+1

        参数:

        N:输出矩阵尺寸(N*N)

        W:输入矩阵尺寸(W*W)

        F:卷积核尺寸(F*F)

        P:padding填充大小

        S:stride步幅大小

二、模型零件与封装

1、特征层的封装(feature)

class AlexNet(nn.Module):

    # 定义AlexNet需要哪些层

    # num_classes = 1000,表示该模型默认最后会进行类别数量为1000的分类任务,可手动调整
    def __init__(self, num_classes=1000):
        super(AlexNet, self).__init__()
        # 卷积层的定义:用于特征提取
        # nn.Sequential:各层的定义与属性设置,用于简化卷积层的语法定义,降低代码重复量,当模型层数较多时使用。
        # inplace=True,增加计算量,减少内存消耗
        self.features = nn.Sequential(
                # 存在stride情况下的计算规则:(原尺寸-ks+2*pd)/sr+1,向下取整数
                nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2),  # input[N, 3, 224, 224]  output[N, 48, 55, 55]
                nn.ReLU(inplace=True),

                nn.MaxPool2d(kernel_size=3, stride=2),  # output[N, 48, 27, 27],向下取整数

                nn.Conv2d(48, 128, kernel_size=5, padding=2),  # output[N, 128, 27, 27]
                nn.ReLU(inplace=True),

                nn.MaxPool2d(kernel_size=3, stride=2),  # output[N, 128, 13, 13]

                nn.Conv2d(128, 192, kernel_size=3, padding=1),  # output[N, 192, 13, 13]
                nn.ReLU(inplace=True),

                nn.Conv2d(192, 192, kernel_size=3, padding=1),  # output[N, 192, 13, 13]
                nn.ReLU(inplace=True),

                nn.Conv2d(192, 128, kernel_size=3, padding=1),  # output[N, 128, 13, 13]
                nn.ReLU(inplace=True),

                nn.MaxPool2d(kernel_size=3, stride=2),  # output[N, 128, 6, 6]
            )

        特征层的封装不再采用以前一个一个定义零件的方式,而是直接通过nn.Sequential()方法,在括号里写入想要创建的零件及其属性即可,用于简化卷积层的语法定义,降低代码重复量,当模型层数较多时使用。

        参考网络模型,特征层的执行顺序为:

        Cv1 - Ac1 - MaxP1 - Cv2 - Ac2 - MaxP2 - Cv3 - Ac3 - Cv4 - Ac4 - Cv5 - Ac5 - MaxP3

2、分类器的封装(classifier)

        # 全连接层(分类器)的定义与属性设置
        self.classifier = nn.Sequential(
                # nn.Dropout(p=?):表示随机失活p*100%的神经元,主要是用于降低参数量,减小过拟合概率
                nn.Dropout(p=0.5),

                # 经过卷积层后的输出是[N, 128, 6, 6],展平变为[N, 128*6*6]
                nn.Linear(128 * 6 * 6, 2048),
                nn.ReLU(inplace=True),

                nn.Dropout(p=0.5),

                nn.Linear(2048, 2048),
                nn.ReLU(inplace=True),

                nn.Linear(2048, num_classes),
            )

        1、Dropout(p=0.5):用于随机失活50%比例的神经元,在分类器前两层使用;

        2、nn.Linear(128*6*6,2048):前面的128*6*6是该线性层输入通道数,是输入x在经过特征层后,被flatten展平后得到的,更多的:设张量x的维度为N * M * A * B,并且按照start_dim=1展平,那么展平后的结果将是一个二维张量,形状为N * (M * A * B)。

        3、分类器的执行顺序:

        Dp1 - L1 - Ac1 - Dp2 - L2 - Ac2 - L3

三、模型对输入的处理

    def forward(self, x):
        x = self.features(x)
        x = torch.flatten(x, start_dim=1)
        y_pred = self.classifier(x)
        return y_pred

        非常简单的按照串行顺序处理输入x,先经过特征提取层,再展平为二维张量[BS, C*W*H],最后经过分类器得到预测输出。

四、配置GPU设备与输入图像预处理

# 一、配置设备
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"模型将使用{device}进行运算")

# 二、图像预处理,创建transform字典,根据后文的选项传输transform内的属性
data_transform = {
        # 用于训练集的transform:随机裁剪为224*224图像,随机水平翻转,ToTensor,标准化
        # 由于输入是3通道,所以在标准化参数配置中,均值和方差各要配置三个参数,以对应三个通道
        "train": transforms.Compose([transforms.RandomResizedCrop(224),
                                     transforms.RandomHorizontalFlip(),
                                     transforms.ToTensor(),
                                     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]),

        # 用于测试集的transform:裁剪为224*224,ToTensor,标准化
        "test": transforms.Compose([transforms.Resize((224, 224)),  # cannot 224, must (224, 224)
                                    transforms.ToTensor(),
                                    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
    }

        图像输入预处理步骤中,主要是构建了一个变量data_transform,内容是一个字典类型,通过键值对分别对应控制了训练集图像所预处理的配置,以及测试集图像所预处理的配置,到时候只需要在加载DataLoader的时候,往参数里面传入transform = dict[键名]就可以实现预处理操作,是一个非常简洁的操作。

五、训练集 / 测试集的获取(DataLoader)

# 三、获取训练集

# 1、先获取本文件根路径
# 2、再找到数据集路径
batch_size = 64
data_root = os.path.abspath(os.path.join(os.getcwd()))
image_path = os.path.join(data_root, "../DataSet/flower_data")
assert os.path.exists(image_path), "f{image_path} path does not exist."

# 3、获取到训练集,为其分配属于train集的transform
# 注意:datasets.ImageFolder是一个通用的数据加载器,它可以根据数据集文件夹的组织方式读取图像。
train_dataset = datasets.ImageFolder(root=os.path.join(image_path, "train"),
                                     transform=data_transform["train"])

train_loader = DataLoader(train_dataset, batch_size=batch_size,
                          shuffle=True,
                          num_workers=0)

# 四、获取测试集

# nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8])  # number of workers
# print('Using {} dataloader workers every process'.format(nw))

test_dataset = datasets.ImageFolder(root=os.path.join(image_path, "test"),
                                    transform=data_transform["test"])

test_loader = DataLoader(test_dataset,
                         batch_size=4, shuffle=False,
                         num_workers=0)

        windows操作模式下,num_workers要设置为0,否则报错。

六、将索引 / 分类的数据存储为文件

# 五、获取分类index和classes

# 1、train_dataset.class_to_idx返回一个字典,其中键是图像类别的名称(比如"daisy"、"dandelion"等),值是对应的类别索引(0、1、2等)
#   即:返回字典的形式为{'daisy': 0, 'dandelion': 1, 'roses': 2, 'sunflower': 3, 'tulips': 4}。
cls_idx_dict = train_dataset.class_to_idx

# 2、由于需求的目标字典内容应该是idx作为键,而cls作为值,所以将上述字典进行键值调换
idx_cls_dict = dict((val, key) for key, val in cls_idx_dict.items())

# 3、使用json.dumps()函数将cla_dict转换为格式化的JSON字符串,并使用indent=4参数使其按照缩进格式进行美化
json_str = json.dumps(idx_cls_dict, indent=4)

# 4、将JSON字符串写入该文件中
with open('class_indices.json', 'w') as json_file:
    json_file.write(json_str)

        这里就是写入一个json文件,文件内容是{'class_name0':0, 'class_name1':1, ..., 'class_name(i):i'}的类似于键值对的文件,主要是用于根据索引来查找对应的类别。

        在预测自定义新样本的时候加载index / class文件,从而让系统可以根据index来告知用户它的class预测答案是什么。

七、损失函数与优化器定义

# 六、创建AlexNet的实例化对象
net = AlexNet(num_classes=5)
net.to(device)

# 七、创建损失函数与优化器
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.0002)

       该数据集是一个五分类的数据集,内容是:{'daisy': 0, 'dandelion': 1, 'roses': 2, 'sunflower': 3, 'tulips': 4}。

八、准备阶段

# 八、准备阶段
# 这里使用到的best_accuracy表示会记录训练集中最高准确率,并把此时的训练参数记录下来用于测试,save_path是参数保存路径
epochs = 5
save_path = './best_accuracy_params/AlexNet.pth'

# len(train_loader) 返回训练数据加载器中的批次数量,即数据集划分为多少个批次进行训练。
train_steps = len(train_loader)

# 获取训练集的图片数量
train_num = len(train_dataset)

# 获得测试集中有多少图片
test_num = len(test_dataset)
print(f"使用了{train_num}张图片进行训练,使用了{test_num}张图片进行测试,一轮训练周期中需要进行{train_steps}训练批次")

        1、设置训练循环周期epoch;

        2、设置权重文件存储的路径save_path(保存的是使得测试集准确率最高的权重文件);

        3、获取每一轮周期中的训练的次数train_steps;

        4、获取训练集输入图像总数train_num;

        5、获取测试集输入图像总数test_num;

九、训练测试环节

# 九、边训练边测试,并记录最佳的参数,避免过拟合

def main():
    best_accuracy = 0.0
    correct_num = 0.0

    # 训练
    for epoch in range(epochs):
        # 该代码意为:训练模式,可以用于执行Dropout等操作
        net.train()

        # 初始化运行时误差
        running_loss = 0.0

        # 使用tqdm库创建一个进度条,在训练数据加载器train_loader上进行迭代,每次迭代获取一个batch的数据样本
        # file=sys.stdout 参数是为了将进度条输出到标准输出(即控制台),以便在实时监视训练进度。
        train_bar = tqdm(train_loader, file=sys.stdout)

        for step, data in enumerate(train_bar):
            train_inputs, train_labels = data
            train_inputs = train_inputs.to(device)
            train_labels = train_labels.to(device)

            optimizer.zero_grad()

            train_outputs = net(train_inputs)
            loss = loss_function(train_outputs, train_labels)

            loss.backward()
            optimizer.step()

            running_loss += loss.item()

            # 将进度条 train_bar 的描述信息(description)设置为一个格式化字符串,以显示当前训练周期的损失值。
            train_bar.desc = f"train epoch[{epoch + 1}/{epochs}] loss:{loss:.4f}"

        # 测试
        # 通过net.eval()将模型设置为评估模式,这将关闭Dropout等训练模式下的操作,以便进行准确的验证
        net.eval()
        with torch.no_grad():
            # 使用tqdm库创建一个进度条,在训练数据加载器validate_loader上进行迭代,每次迭代获取一个batch的数据样本
            test_bar = tqdm(test_loader, file=sys.stdout)

            for test_data in test_bar:
                test_inputs, test_labels = test_data
                test_labels = test_labels.to(device)
                test_inputs = test_inputs.to(device)

                test_outputs = net(test_inputs)

                _, predict_y = torch.max(test_outputs, dim=1)

                # 计算正确的数量
                correct_num += torch.eq(predict_y, test_labels).sum().item()

            # 计算测试准确度:正确预测的数量 / 需要预测的总量
            test_accuracy = correct_num / test_num
            print(f'[epoch {epoch + 1}] train_loss: {running_loss / train_steps}  val_accuracy: {test_accuracy}')
            correct_num = 0.0

            # 如果当前的测试准确率超过了(历史)最大准确率,则记录本次训练的权重信息到save_path
            if test_accuracy > best_accuracy:
                best_accuracy = test_accuracy
                torch.save(net.state_dict(), save_path)

        print('训练完毕')

    if __name__ == '__main__':
        main()

十、Model代码全览

import torch.nn as nn
import torch


class AlexNet(nn.Module):

    # 定义AlexNet需要哪些层

    # num_classes = 1000,表示该模型默认最后会进行类别数量为1000的分类任务,可手动调整
    def __init__(self, num_classes=1000):
        super(AlexNet, self).__init__()

        # 卷积层的定义:用于特征提取
        # nn.Sequential:各层的定义与属性设置,用于简化卷积层的语法定义,降低代码重复量,当模型层数较多时使用。
        # inplace=True,增加计算量,减少内存消耗
        self.features = nn.Sequential(
                # 存在stride情况下的计算规则:(原尺寸-ks+2*pd)/sr+1,向下取整数
                nn.Conv2d(3, 48, kernel_size=11, stride=4, padding=2),  # input[N, 3, 224, 224]  output[N, 48, 55, 55]
                nn.ReLU(inplace=True),

                nn.MaxPool2d(kernel_size=3, stride=2),  # output[N, 48, 27, 27],向下取整数

                nn.Conv2d(48, 128, kernel_size=5, padding=2),  # output[N, 128, 27, 27]
                nn.ReLU(inplace=True),

                nn.MaxPool2d(kernel_size=3, stride=2),  # output[N, 128, 13, 13]

                nn.Conv2d(128, 192, kernel_size=3, padding=1),  # output[N, 192, 13, 13]
                nn.ReLU(inplace=True),

                nn.Conv2d(192, 192, kernel_size=3, padding=1),  # output[N, 192, 13, 13]
                nn.ReLU(inplace=True),

                nn.Conv2d(192, 128, kernel_size=3, padding=1),  # output[N, 128, 13, 13]
                nn.ReLU(inplace=True),

                nn.MaxPool2d(kernel_size=3, stride=2),  # output[N, 128, 6, 6]
            )

        # 全连接层(分类器)的定义与属性设置
        self.classifier = nn.Sequential(
                # nn.Dropout(p=?):表示随机失活p*100%的神经元,主要是用于降低参数量,减小过拟合概率
                nn.Dropout(p=0.5),

                # 经过卷积层后的输出是[N, 128, 6, 6],展平变为[N, 128*6*6]
                nn.Linear(128 * 6 * 6, 2048),
                nn.ReLU(inplace=True),

                nn.Dropout(p=0.5),

                nn.Linear(2048, 2048),
                nn.ReLU(inplace=True),

                nn.Linear(2048, num_classes),
            )

    # if init_weights:
    #    self._initialize_weights()

    # 开始定义模型是如何处理输入数据的
    # 先经过卷积层处理,然后展平,再使用全连接层进行分类
    def forward(self, x):
        x = self.features(x)
        x = torch.flatten(x, start_dim=1)
        y_pred = self.classifier(x)
        return y_pred

    # def _initialize_weights(self):
    #     for m in self.modules():
    #         if isinstance(m, nn.Conv2d):
    #             nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
    #             if m.bias is not None:
    #                 nn.init.constant_(m.bias, 0)
    #         elif isinstance(m, nn.Linear):
    #             nn.init.normal_(m.weight, 0, 0.01)
    #             nn.init.constant_(m.bias, 0)

十一、Train&Test代码全览

import os
import sys
import json
import torch
import torch.nn as nn
from torchvision import transforms, datasets
import torch.optim as optim
from tqdm import tqdm
from torch.utils.data import DataLoader
from AN_01_Model import AlexNet

# 一、配置设备
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"模型将使用{device}进行运算")

# 二、图像预处理,创建transform字典,根据后文的选项传输transform内的属性
data_transform = {
        # 用于训练集的transform:随机裁剪为224*224图像,随机水平翻转,ToTensor,标准化
        # 由于输入是3通道,所以在标准化参数配置中,均值和方差各要配置三个参数,以对应三个通道
        "train": transforms.Compose([transforms.RandomResizedCrop(224),
                                     transforms.RandomHorizontalFlip(),
                                     transforms.ToTensor(),
                                     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]),

        # 用于测试集的transform:裁剪为224*224,ToTensor,标准化
        "test": transforms.Compose([transforms.Resize((224, 224)),  # cannot 224, must (224, 224)
                                    transforms.ToTensor(),
                                    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
    }

# 三、获取训练集

# 1、先获取本文件根路径
# 2、再找到数据集路径
batch_size = 64
data_root = os.path.abspath(os.path.join(os.getcwd()))
image_path = os.path.join(data_root, "../DataSet/flower_data")
assert os.path.exists(image_path), "f{image_path} path does not exist."

# 3、获取到训练集,为其分配属于train集的transform
# 注意:datasets.ImageFolder是一个通用的数据加载器,它可以根据数据集文件夹的组织方式读取图像。
train_dataset = datasets.ImageFolder(root=os.path.join(image_path, "train"),
                                     transform=data_transform["train"])

train_loader = DataLoader(train_dataset, batch_size=batch_size,
                          shuffle=True,
                          num_workers=0)

# 四、获取测试集

# nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8])  # number of workers
# print('Using {} dataloader workers every process'.format(nw))

test_dataset = datasets.ImageFolder(root=os.path.join(image_path, "test"),
                                    transform=data_transform["test"])

test_loader = DataLoader(test_dataset,
                         batch_size=4, shuffle=False,
                         num_workers=0)

# 五、获取分类index和classes

# 1、train_dataset.class_to_idx返回一个字典,其中键是图像类别的名称(比如"daisy"、"dandelion"等),值是对应的类别索引(0、1、2等)
#   即:返回字典的形式为{'daisy': 0, 'dandelion': 1, 'roses': 2, 'sunflower': 3, 'tulips': 4}。
cls_idx_dict = train_dataset.class_to_idx

# 2、由于需求的目标字典内容应该是idx作为键,而cls作为值,所以将上述字典进行键值调换
idx_cls_dict = dict((val, key) for key, val in cls_idx_dict.items())

# 3、使用json.dumps()函数将cla_dict转换为格式化的JSON字符串,并使用indent=4参数使其按照缩进格式进行美化
json_str = json.dumps(idx_cls_dict, indent=4)

# 4、将JSON字符串写入该文件中
with open('class_indices.json', 'w') as json_file:
    json_file.write(json_str)

# 六、创建AlexNet的实例化对象
net = AlexNet(num_classes=5)
net.to(device)

# 七、创建损失函数与优化器
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.0002)

# 八、准备阶段
# 这里使用到的best_accuracy表示会记录训练集中最高准确率,并把此时的训练参数记录下来用于测试,save_path是参数保存路径
epochs = 5
save_path = './best_accuracy_params/AlexNet.pth'

# len(train_loader) 返回训练数据加载器中的批次数量,即数据集划分为多少个批次进行训练。
train_steps = len(train_loader)

# 获取训练集的图片数量
train_num = len(train_dataset)

# 获得测试集中有多少图片
test_num = len(test_dataset)
print(f"使用了{train_num}张图片进行训练,使用了{test_num}张图片进行测试,一轮训练周期中需要进行{train_steps}训练批次")


# 九、边训练边测试,并记录最佳的参数,避免过拟合

def main():
    best_accuracy = 0.0
    correct_num = 0.0

    # 训练
    for epoch in range(epochs):
        # 该代码意为:训练模式,可以用于执行Dropout等操作
        net.train()

        # 初始化运行时误差
        running_loss = 0.0

        # 使用tqdm库创建一个进度条,在训练数据加载器train_loader上进行迭代,每次迭代获取一个batch的数据样本
        # file=sys.stdout 参数是为了将进度条输出到标准输出(即控制台),以便在实时监视训练进度。
        train_bar = tqdm(train_loader, file=sys.stdout)

        for step, data in enumerate(train_bar):
            train_inputs, train_labels = data
            train_inputs = train_inputs.to(device)
            train_labels = train_labels.to(device)

            optimizer.zero_grad()

            train_outputs = net(train_inputs)
            loss = loss_function(train_outputs, train_labels)

            loss.backward()
            optimizer.step()

            running_loss += loss.item()

            # 将进度条 train_bar 的描述信息(description)设置为一个格式化字符串,以显示当前训练周期的损失值。
            train_bar.desc = f"train epoch[{epoch + 1}/{epochs}] loss:{loss:.4f}"

        # 测试
        # 通过net.eval()将模型设置为评估模式,这将关闭Dropout等训练模式下的操作,以便进行准确的验证
        net.eval()
        with torch.no_grad():
            # 使用tqdm库创建一个进度条,在训练数据加载器validate_loader上进行迭代,每次迭代获取一个batch的数据样本
            test_bar = tqdm(test_loader, file=sys.stdout)

            for test_data in test_bar:
                test_inputs, test_labels = test_data
                test_labels = test_labels.to(device)
                test_inputs = test_inputs.to(device)

                test_outputs = net(test_inputs)

                _, predict_y = torch.max(test_outputs, dim=1)

                # 计算正确的数量
                correct_num += torch.eq(predict_y, test_labels).sum().item()

            # 计算测试准确度:正确预测的数量 / 需要预测的总量
            test_accuracy = correct_num / test_num
            print(f'[epoch {epoch + 1}] train_loss: {running_loss / train_steps}  val_accuracy: {test_accuracy}')
            correct_num = 0.0

            if test_accuracy > best_accuracy:
                best_accuracy = test_accuracy
                torch.save(net.state_dict(), save_path)

        print('训练完毕')

    if __name__ == '__main__':
        main()


# [epoch 9] train_loss: 0.8941145218335665  val_accuracy: 0.7142857142857143
# 这是本次训练记录到的最大准确率 - 71.43%

你可能感兴趣的:(Pilipala,笔记,深度学习,人工智能,python)