【Datawhale X 李宏毅苹果书 AI夏令营】Task2 Chapter3.3&4%5

【Datawhale X 李宏毅苹果书 AI夏令营】Task1 Chapter3.1&3.2:https://blog.csdn.net/qq_45516068/article/details/141615276?spm=1001.2014.3001.5501
本文将基于两个实践来学习夏令营中的内容

1.从0.5开始入门

  • 本blog仅作为夏令营过程中学习出现的问题和任务进行记录和学习留档,针对苹果书内的知识点不作详细记录(例如完全复制公式,思维导图等形式)。

  • 本文根据夏令营进度,从3.1开始。

  • 网络上有众多的学习资料,如果你恰好点了进来,对机器学习、深度学习或者仅对Datawhale感兴趣,可以点击下面的链接来详细了解。

    • **李宏毅深度学习教程LeeDL-Tutorial(苹果书)**开源地址:https://github.com/datawhalechina/leedl-tutorial
    • 李宏毅《机器学习/深度学习》2021课程B站视频:https://www.bilibili.com/video/BV1JA411c7VT?p=1&vd_source=7f728b80e21aaffa0f2781c650cbe2ce
    • 本人参与的**Datawhale AI夏令营(第五期)**地址(虽然不能报名,但仍可以进入学习说明和Task文件进行自学):https://linklearner.com/activity/16
    • 本人自己之前接触过的一篇实践性学习框架Approaching (Almost) Any Machine Learning Problem(简称AAAMLP):
      • 原文github地址(作者Abhishek Thakur):https://github.com/abhishekkrthakur/approachingalmost
      • 中译版出处:https://ytzfhqs.github.io/AAAMLP-CN/

2.实践一:HOG+SVM实现CIFAR-10数据集的图像分类

采取HOG+SVM的特征提取和分类器,选择CIFAR-10数据集,解决图像分类问题,步骤要求如下:

1.数据准备:

​ 下载 CIFAR-10 数据集。该数据集包含 10 个类别共 60000 张 32x32 彩色图像,每个类别有 6000 张图像。

2.特征提取:

​ 使用所选的特征提取方法(如 HOG,SIFT 等)从训练集图像中提取特征。对每个图像提取特征,并将其转换成适合分类器的形式。

3.分类器训练:

​ 选择一个适当的分类器(如 SVM,KNN 等)。使用训练集中的特征和相应的标签来训练分类器。

4.模型评估:

​ 使用测试集中的图像提取特征,并使用训练好的分类器进行分类。计算分类器在测试集上的准确率、精确率、召回率和 F1 分数等指标,以评估模型性能。

注意事项:

​ 在实现过程中,可以使用现有的库或者自己实现特征提取和分类器。并确保代码结构清晰,注释清楚,以便理解。

二、实验原理分析

2.1 CIFAR-10

​ 根据官网CIFAR-10 and CIFAR-100 datasets (toronto.edu)所述,该数据集是一个更接近普适物体的彩色图像数据集,用于识别普适物体的小型数据集。一共包含10 个类别的RGB 彩色图片:飞机、汽车、鸟类、猫、鹿、狗、蛙类、马、船和卡车。
​ 每个图片的尺寸为32 × 32 ,每个类别有6000个图像,数据集中一共有50000 张训练图片和10000 张测试图片。
在这里插入图片描述

本实验基于此数据集完成图像分类问题,通过该网页CIFAR-10 Benchmark (Image Classification) | Papers With Code可以了解目前算法实现正确率排名,并讲论文与github代码对应。在这里插入图片描述

可以看到目前准确率最高的ViT-H/14,正确率为99.5%

2.2 HOG特征提取

HOG:Histogram of Oriented Gradients:定向梯度直方图是计算机视觉和图像处理中一种流行的特征描述符技术。 **它分析对象内边缘方向的分布以描述其形状和外观。 HOG 方法涉及计算图像中每个像素的梯度大小和方向,然后将图像划分为小单元。**

​ 主要步骤如下:

​ **1、图像预处理:**灰度化(将图像看做一个x,y,z(灰度)的三维图像)

​ **2、计算梯度图:**采用Gamma校正法对输入图像进行颜色空间的标准化(归一化);目的是调节图像的对比度,降低图像局部的阴影和光照变化所造成的影响,同时可以抑制噪音的干扰

​ **3、计算梯度直方图:**计算图像每个像素的梯度(包括大小和方向);主要是为了捕获轮廓信息,同时进一步弱化光照的干扰。

​ **4、Block 归一化:**将图像划分成小cells(例如66像素/cell);统计每个cell的梯度直方图(不同梯度的个数),即可形成每个cell的descriptor;将每几个cell组成一个block(例如33个cell/block),一个block内所有cell的特征descriptor串联起来便得到该block的HOG特征descriptor

​ **5、计算HOG特征向量:**将图像image内的所有block的HOG特征descriptor串联起来就可以得到该image(你要检测的目标)的HOG特征descriptor了。这个就是最终的可供分类使用的特征向量了。

​ 参考使用代码如下:

fd = hog(gray, 9, [1, 1], [2, 2])	#此为调用hog的api。	参数分别为orientations,pixels_per_cell和cells_per_block

#下列是hog函数的自写形式:
def get_hog_feat(self, image, stride=8, orientations=8, pixels_per_cell=(8, 8), cells_per_block=(2, 2)):
    cx, cy = pixels_per_cell
    bx, by = cells_per_block
    sx, sy = image.shape
    n_cellsx = int(np.floor(sx // cx))
    n_cellsy = int(np.floor(sy // cy))
    n_blocksx = (n_cellsx - bx) + 1
    n_blocksy = (n_cellsy - by) + 1
    gx = np.zeros((sx, sy), dtype=np.float32)
    gy = np.zeros((sx, sy), dtype=np.float32)
    eps = 1e-5
    grad = np.zeros((sx, sy, 2), dtype=np.float32)
    for i in range(1, sx-1):
        for j in range(1, sy-1):
            gx[i, j] = image[i, j-1] - image[i, j+1]
            gy[i, j] = image[i+1, j] - image[i-1, j]
            grad[i, j, 0] = np.arctan(gy[i, j] / (gx[i, j] + eps)) * 180 / math.pi
            if gx[i, j] < 0:
                grad[i, j, 0] += 180
            grad[i, j, 0] = (grad[i, j, 0] + 360) % 360
            grad[i, j, 1] = np.sqrt(gy[i, j] ** 2 + gx[i, j] ** 2)
    normalised_blocks = np.zeros((n_blocksy, n_blocksx, by * bx * orientations))
    for y in range(n_blocksy):
        for x in range(n_blocksx):
            block = grad[y*stride:y*stride+16, x*stride:x*stride+16]
            hist_block = np.zeros(32, dtype=np.float32)
            eps = 1e-5
            for k in range(by):
                for m in range(bx):
                    cell = block[k*8:(k+1)*8, m*8:(m+1)*8]
                    hist_cell = np.zeros(8, dtype=np.float32)
                    for i in range(cy):
                        for j in range(cx):
                            n = int(cell[i, j, 0] / 45)
                            hist_cell[n] += cell[i, j, 1]
                    hist_block[(k * bx + m) * orientations:(k * bx + m + 1) * orientations] = hist_cell[:]
            normalised_blocks[y, x, :] = hist_block / np.sqrt(hist_block.sum() ** 2 + eps)
    return normalised_blocks.ravel()

2.3 SVM

支持向量机(support vector machines, SVM)是一种二分类模型,它的基本模型是定义在特征空间上的间隔最大的线性分类器,间隔最大使它有别于感知机;SVM还包括核技巧,这使它成为实质上的非线性分类器。SVM的的学习策略就是间隔最大化,可形式化为一个求解凸二次规划的问题,也等价于正则化的合页损失函数的最小化问题。SVM的的学习算法就是求解凸二次规划的最优化算法。

​ 本次我们采用python中的sklearn库中的sklearn.svm.SVC()函数来使用svm方法。

2.4 评估模型性能

​ 在本次实验中,CIFAR-10已经提前准备好训练集和测试集,所以我们根据要求,来进行准确率、精确率、召回率和 F1 分数的输出。

​ 定义如下:

准确率/召回率(查全率)
在这里插入图片描述
准确率更care的是在已经预测为真的结果中,预测正确的比例,这时候我们可以发现如果我们预测为真的个体数越少,准确率越高的可能性就会越大,即如果我们只预测最可能为真的那一个个体为真,其余的都为假,那么这时候我们的准确率很可能为100%,但此时召回率就会很低;而召回率care的是在所有为真的个体中,被预测正确的个体所占的比例,那么可以看到如果我们预测为真的个体越多,那么召回率更高的可能性就会越大,即如果我们把所有的个体都预测为真,那么此时的召回率必然为100%,但是准确率此时就会很低。因此这两个度量往往是相互对立的,即准确率高则召回率通常比较低,召回率高则准确率往往会很低。因此我们分别用准确率或召回率对模型的预测结果进行评价会有片面性。

错误率/精度(accuracy)

​ 假设我们拥有m个样本个体,那么该样本的错误率为:
e = 1 m ∑ i = 1 m I ( f ( x i ) ≠ y i ) e=\frac{1}{m} \sum_{i=1}^{m} I\left(f\left(x_{i}\right) \neq y_{i}\right) e=m1i=1mI(f(xi)=yi)
​ 即分类错误的样本数占总样本数的个数,精度即为 1−e
F1分数与P-R曲线

​ P-R曲线是以召回率R为横轴,准确率P为纵轴,然后根据模型的预测结果对样本进行排序,把最有可能是正样本的个体排在前面,而后面的则是模型认为最不可能为正例的样本,再按此顺序逐个把样本作为正例进行预测并计算出当前的准确率和召回率得到的曲线。

在这里插入图片描述

​ 通过上图我们可以看到,当我们只把最可能为正例的个体预测为正样本时,其准确率最高位1.0,而此时的召回率则几乎为0,而我们如果把所有的个体都预测为正样本的时候,召回率为1.0,此时准确率则最低。当我们使用F1分数来进行度量时:
F 1 = 2 ∗ P ∗ R P + R F1=\frac{2*P*R}{P+R} F1=P+R2PR
​ F1的值越大越好,可以发现,F1是一个准确率和召回率的调和平均数,其更care较小值,因此如果P与R中一个值太小会对F1产生更大的影响,但是这样的判断都是以准确率和召回率同等重要为基础的,但是对于很多问题,其会更care其中的一个指标,例如癌症的判断,其更关注的是召回率而不是准确率,因为如果我们更关注准确率,就会使得很多的癌症患者被误诊为不是癌症,从而造成患者的死亡率会更高。

三、实验过程

​ 本实验在不同block的情况下,分别训练以及进行了图像的分类。最后结果如图。在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

四、实验结果及分析

​ 由图可以发现,4x4block的准确率最高,其次是8x8,2x2的准确率反而很低。根据搜集资料可知,该数据集由于是32x32的图片,在分块过少时,特征值反而会重复,最终导致低准确率。

接下来会参考如何使CIFAR-10测试集的分类准确率从40%提升到90% - 知乎 (zhihu.com)该文章进行训练网络的迭代和优化。

3.实践二:Task2.3(实践任务):HW3(CNN)卷积神经网络-图像分类

通过利用卷积神经网络架构,通过一个较小的10种食物的图像的数据集训练一个模型完成图像分类的任务

由于阿里云的券之前用过,所以正在使用自己电脑(intel iRISx)跑程序,比较慢,后续补充结构

代码解析:

3.1导入必要的库

import numpy as np
import pandas as pd
import torch
import os
import torch.nn as nn
import torchvision.transforms as transforms
from PIL import Image
# “ConcatDataset” 和 “Subset” 在进行半监督学习时可能是有用的。
from torch.utils.data import ConcatDataset, DataLoader, Subset, Dataset
from torchvision.datasets import DatasetFolder, VisionDataset
# 这个是用来显示进度条的。
from tqdm.auto import tqdm
import random

为了确保实验的可重复性,设置随机种子,并对CUDA进行配置以确保确定性:

# 设置随机种子以确保实验结果的可重复性
myseed = 6666

# 确保在使用CUDA时,卷积运算具有确定性,以增强实验结果的可重复性
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False

# 为numpy和pytorch设置随机种子
np.random.seed(myseed)
torch.manual_seed(myseed)

# 如果使用CUDA,为所有GPU设置随机种子
if torch.cuda.is_available():
    torch.cuda.manual_seed_all(myseed)

3.2图像预处理变换

使用torchvision.transforms库来进行图像预处理和变换

# 在测试和验证阶段,通常不需要图像增强。
# 我们所需要的只是调整PIL图像的大小并将其转换为Tensor。
test_tfm = transforms.Compose([
   transforms.Resize((128, 128)),
   transforms.ToTensor(),
])

# 不过,在测试阶段使用图像增强也是有可能的。
# 你可以使用train_tfm生成多种图像,然后使用集成方法进行测试。
train_tfm = transforms.Compose([
   # 将图像调整为固定大小(高度和宽度均为128)
   transforms.Resize((128, 128)),
   # 你可以在这里添加一些图像增强的操作。

   # ToTensor()应该是所有变换中的最后一个。
   transforms.ToTensor(),
])

3.3提供数据集

class FoodDataset(Dataset):
    """
    用于加载食品图像数据集的类。

    该类继承自Dataset,提供了对食品图像数据集的加载和预处理功能。
    它可以自动从指定路径加载所有的jpg图像,并对这些图像应用给定的变换。
    """

    def __init__(self, path, tfm=test_tfm, files=None):
        """
        初始化FoodDataset实例。

        参数:
        - path: 图像数据所在的目录路径。
        - tfm: 应用于图像的变换方法(默认为测试变换)。
        - files: 可选参数,用于直接指定图像文件的路径列表(默认为None)。
        """
        super(FoodDataset).__init__()
        self.path = path
        # 列出目录下所有jpg文件,并按顺序排序
        self.files = sorted([os.path.join(path, x) for x in os.listdir(path) if x.endswith(".jpg")])
        if files is not None:
            self.files = files  # 如果提供了文件列表,则使用该列表
        self.transform = tfm  # 图像变换方法

    def __len__(self):
        """返回数据集中图像的数量。"""
        return len(self.files)

    def __getitem__(self, idx):
        """
        获取给定索引的图像及其标签。

        参数:
            idx: 图像在数据集中的索引。

        返回:
            im: 应用了变换后的图像。
            label: 图像对应的标签(如果可用)。
        """
        fname = self.files[idx]
        im = Image.open(fname)
        im = self.transform(im)  # 应用图像变换

        # 尝试从文件名中提取标签
        try:
            label = int(fname.split("/")[-1].split("_")[0])
        except:
            label = -1  # 如果无法提取标签,则设置为-1(测试数据无标签)

        return im, label

3.4设置模型

class Classifier(nn.Module):
    """
    定义一个图像分类器类,继承自PyTorch的nn.Module。
    该分类器包含卷积层和全连接层,用于对图像进行分类。
    """
    def __init__(self):
        """
        初始化函数,构建卷积神经网络的结构。
        包含一系列的卷积层、批归一化层、激活函数和池化层。
        """
        super(Classifier, self).__init__()
        # 定义卷积神经网络的序列结构
        self.cnn = nn.Sequential(
            nn.Conv2d(3, 64, 3, 1, 1),  # 输入通道3,输出通道64,卷积核大小3,步长1,填充1
            nn.BatchNorm2d(64),        # 批归一化,作用于64个通道
            nn.ReLU(),                 # ReLU激活函数
            nn.MaxPool2d(2, 2, 0),      # 最大池化,池化窗口大小2,步长2,填充0
            
            nn.Conv2d(64, 128, 3, 1, 1), # 输入通道64,输出通道128,卷积核大小3,步长1,填充1
            nn.BatchNorm2d(128),        # 批归一化,作用于128个通道
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),      # 最大池化,池化窗口大小2,步长2,填充0
            
            nn.Conv2d(128, 256, 3, 1, 1), # 输入通道128,输出通道256,卷积核大小3,步长1,填充1
            nn.BatchNorm2d(256),        # 批归一化,作用于256个通道
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),      # 最大池化,池化窗口大小2,步长2,填充0
            
            nn.Conv2d(256, 512, 3, 1, 1), # 输入通道256,输出通道512,卷积核大小3,步长1,填充1
            nn.BatchNorm2d(512),        # 批归一化,作用于512个通道
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),       # 最大池化,池化窗口大小2,步长2,填充0
            
            nn.Conv2d(512, 512, 3, 1, 1), # 输入通道512,输出通道512,卷积核大小3,步长1,填充1
            nn.BatchNorm2d(512),        # 批归一化,作用于512个通道
            nn.ReLU(),
            nn.MaxPool2d(2, 2, 0),       # 最大池化,池化窗口大小2,步长2,填充0
        )
        # 定义全连接神经网络的序列结构
        self.fc = nn.Sequential(
            nn.Linear(512*4*4, 1024),    # 输入大小512*4*4,输出大小1024
            nn.ReLU(),
            nn.Linear(1024, 512),        # 输入大小1024,输出大小512
            nn.ReLU(),
            nn.Linear(512, 11)           # 输入大小512,输出大小11,最终输出11个类别的概率
        )

    def forward(self, x):
        """
        前向传播函数,对输入进行处理。
        
        参数:
        x -- 输入的图像数据,形状为(batch_size, 3, 128, 128)
        
        返回:
        输出的分类结果,形状为(batch_size, 11)
        """
        out = self.cnn(x)               # 通过卷积神经网络处理输入
        out = out.view(out.size()[0], -1)  # 展平输出,以适配全连接层的输入要求
        return self.fc(out)             # 通过全连接神经网络得到最终输出

3.5配置模型训练的参数(与前一个实例类似)

# 根据GPU是否可用选择设备类型
device = "cuda" if torch.cuda.is_available() else "cpu"

# 初始化模型,并将其放置在指定的设备上
model = Classifier().to(device)

# 定义批量大小
batch_size = 64

# 定义训练轮数
n_epochs = 8

# 如果在'patience'轮中没有改进,则提前停止
patience = 5

# 对于分类任务,我们使用交叉熵作为性能衡量标准
criterion = nn.CrossEntropyLoss()

# 初始化优化器,您可以自行调整一些超参数,如学习率
optimizer = torch.optim.Adam(model.parameters(), lr=0.0003, weight_decay=1e-5)

# 初始化追踪器,这些不是参数,不应该被更改
stale = 0
best_acc = 0

for epoch in range(n_epochs):
  # ---------- 训练阶段 ----------
  # 确保模型处于训练模式
  model.train()

  # 这些用于记录训练过程中的信息
  train_loss = []
  train_accs = []

  for batch in tqdm(train_loader):
      # 每个批次包含图像数据及其对应的标签
      imgs, labels = batch
      # imgs = imgs.half()
      # print(imgs.shape,labels.shape)

      # 前向传播数据。(确保数据和模型位于同一设备上)
      logits = model(imgs.to(device))

      # 计算交叉熵损失。
      # 在计算交叉熵之前不需要应用softmax,因为它会自动完成。
      loss = criterion(logits, labels.to(device))

      # 清除上一步中参数中存储的梯度
      optimizer.zero_grad()

      # 计算参数的梯度
      loss.backward()

      # 为了稳定训练,限制梯度范数
      grad_norm = nn.utils.clip_grad_norm_(model.parameters(), max_norm=10)

      # 使用计算出的梯度更新参数
      optimizer.step()

      # 计算当前批次的准确率
      acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()

      # 记录损失和准确率
      train_loss.append(loss.item())
      train_accs.append(acc)

  train_loss = sum(train_loss) / len(train_loss)
  train_acc = sum(train_accs) / len(train_accs)

  # 打印信息
  print(f"[ 训练 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {train_loss:.5f}, acc = {train_acc:.5f}")

  # ---------- 验证阶段 ----------
  # 确保模型处于评估模式,以便某些模块如dropout被禁用并能够正常工作
  model.eval()

  # 这些用于记录验证过程中的信息
  valid_loss = []
  valid_accs = []

  # 按批次迭代验证集
  for batch in tqdm(valid_loader):
      # 每个批次包含图像数据及其对应的标签
      imgs, labels = batch
      # imgs = imgs.half()

      # 我们在验证阶段不需要梯度。
      # 使用 torch.no_grad() 加速前向传播过程。
      with torch.no_grad():
          logits = model(imgs.to(device))

      # 我们仍然可以计算损失(但不计算梯度)。
      loss = criterion(logits, labels.to(device))

      # 计算当前批次的准确率
      acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()

      # 记录损失和准确率
      valid_loss.append(loss.item())
      valid_accs.append(acc)
      # break

  # 整个验证集的平均损失和准确率是所记录值的平均
  valid_loss = sum(valid_loss) / len(valid_loss)
  valid_acc = sum(valid_accs) / len(valid_accs)

  # 打印信息
  print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")

  # 更新日志(输出)
  if valid_acc > best_acc:
      with open(f"./{_exp_name}_log.txt", "a"):
          print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f} -> 最佳")
  else:
      with open(f"./{_exp_name}_log.txt", "a"):
          print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")

  # 保存模型
  if valid_acc > best_acc:
      print(f"在第 {epoch} 轮找到最佳模型,正在保存模型")
      torch.save(model.state_dict(), f"{_exp_name}_best.ckpt")  # 只保存最佳模型以防止输出内存超出错误
      best_acc = valid_acc
      stale = 0
  else:
      stale += 1
      if stale > patience:
          print(f"连续 {patience} 轮没有改进,提前停止")
          break

由于本实例通过代码来实现训练、测试和模型参数的配置,所以此处不详细解释代码(也没学懂TAT)

3.6训练

# 初始化追踪器,这些不是参数,不应该被更改
stale = 0
best_acc = 0

for epoch in range(n_epochs):
    # ---------- 训练阶段 ----------
    # 确保模型处于训练模式
    model.train()

    # 这些用于记录训练过程中的信息
    train_loss = []
    train_accs = []

    for batch in tqdm(train_loader):
        # 每个批次包含图像数据及其对应的标签
        imgs, labels = batch
        # imgs = imgs.half()
        # print(imgs.shape,labels.shape)

        # 前向传播数据。(确保数据和模型位于同一设备上)
        logits = model(imgs.to(device))

        # 计算交叉熵损失。
        # 在计算交叉熵之前不需要应用softmax,因为它会自动完成。
        loss = criterion(logits, labels.to(device))

        # 清除上一步中参数中存储的梯度
        optimizer.zero_grad()

        # 计算参数的梯度
        loss.backward()

        # 为了稳定训练,限制梯度范数
        grad_norm = nn.utils.clip_grad_norm_(model.parameters(), max_norm=10)

        # 使用计算出的梯度更新参数
        optimizer.step()

        # 计算当前批次的准确率
        acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()

        # 记录损失和准确率
        train_loss.append(loss.item())
        train_accs.append(acc)

    train_loss = sum(train_loss) / len(train_loss)
    train_acc = sum(train_accs) / len(train_accs)

    # 打印信息
    print(f"[ 训练 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {train_loss:.5f}, acc = {train_acc:.5f}")

    # ---------- 验证阶段 ----------
    # 确保模型处于评估模式,以便某些模块如dropout被禁用并能够正常工作
    model.eval()

    # 这些用于记录验证过程中的信息
    valid_loss = []
    valid_accs = []

    # 按批次迭代验证集
    for batch in tqdm(valid_loader):
        # 每个批次包含图像数据及其对应的标签
        imgs, labels = batch
        # imgs = imgs.half()

        # 我们在验证阶段不需要梯度。
        # 使用 torch.no_grad() 加速前向传播过程。
        with torch.no_grad():
            logits = model(imgs.to(device))

        # 我们仍然可以计算损失(但不计算梯度)。
        loss = criterion(logits, labels.to(device))

        # 计算当前批次的准确率
        acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()

        # 记录损失和准确率
        valid_loss.append(loss.item())
        valid_accs.append(acc)
        # break

    # 整个验证集的平均损失和准确率是所记录值的平均
    valid_loss = sum(valid_loss) / len(valid_loss)
    valid_acc = sum(valid_accs) / len(valid_accs)

    # 打印信息
    print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")

    # 更新日志(输出)
    if valid_acc > best_acc:
        with open(f"./{_exp_name}_log.txt", "a"):
            print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f} -> 最佳")
    else:
        with open(f"./{_exp_name}_log.txt", "a"):
            print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")

    # 保存模型
    if valid_acc > best_acc:
        print(f"在第 {epoch} 轮找到最佳模型,正在保存模型")
        torch.save(model.state_dict(), f"{_exp_name}_best.ckpt")  # 只保存最佳模型以防止输出内存超出错误
        best_acc = valid_acc
        stale = 0
    else:
        stale += 1
        if stale > patience:
            print(f"连续 {patience} 轮没有改进,提前停止")
            break

3.7测试

# 构建测试数据集
# "loader"参数指定了torchvision如何读取数据
test_set = FoodDataset("./hw3_data/test", tfm=test_tfm)
# 创建测试数据加载器,批量大小为batch_size,不打乱数据顺序,不使用多线程,启用pin_memory以提高数据加载效率
test_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False, num_workers=0, pin_memory=True)

# 实例化分类器模型,并将其转移到指定的设备上
model_best = Classifier().to(device)

# 加载模型的最优状态字典
model_best.load_state_dict(torch.load(f"{_exp_name}_best.ckpt"))

# 将模型设置为评估模式
model_best.eval()

# 初始化一个空列表,用于存储所有预测标签
prediction = []

# 使用torch.no_grad()上下文管理器,禁用梯度计算
with torch.no_grad():
    # 遍历测试数据加载器
    for data, _ in tqdm(test_loader):
        # 将数据转移到指定设备上,并获得模型的预测结果
        test_pred = model_best(data.to(device))
        # 选择具有最高分数的类别作为预测标签
        test_label = np.argmax(test_pred.cpu().data.numpy(), axis=1)
        # 将预测标签添加到结果列表中
        prediction += test_label.squeeze().tolist()


# 创建测试csv文件
def pad4(i):
    """
    将输入数字i转换为长度为4的字符串,如果长度不足4,则在前面补0。
    :param i: 需要转换的数字
    :return: 补0后的字符串
    """
    return "0" * (4 - len(str(i))) + str(i)

# 创建一个空的DataFrame对象
df = pd.DataFrame()
# 使用列表推导式生成Id列,列表长度等于测试集的长度
df["Id"] = [pad4(i) for i in range(len(test_set))]
# 将预测结果赋值给Category列
df["Category"] = prediction
# 将DataFrame对象保存为submission.csv文件,不保存索引
df.to_csv("submission.csv", index=False)

结果待补充,购买了AutoDL正在测试(8.31更新,开学了)
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值