基于pytorch搭建CNN

时间:2024-10-25 09:36:29

先上代码

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import matplotlib

matplotlib.use('tkAgg')

# 设置图形配置
config = {
    "font.family": 'serif',
    "mathtext.fontset": 'stix',  # matplotlib渲染数学字体时使用的字体,和Times New Roman差别不大
    "font.serif": ['SimSun'],  # 宋体
    'axes.unicode_minus': False  # 处理负号,即-号
}
matplotlib.rcParams.update(config)

# 定义超参数
input_size = 28  # 图像的尺寸为28*28*1
num_classes = 10  # 一共有10个类别的结果
num_epochs = 3
batch_size = 64  # 一个批次训练64张图片
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

train_dataset = datasets.MNIST(root='./data', train=True, transform=transforms.ToTensor(), download=True)

test_dataset = datasets.MNIST(root='./data', train=False, transform=transforms.ToTensor())

# 构建batch数据集
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)

test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=True)


class CNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Sequential(  # 输入大小(1,28,28)
            nn.Conv2d(
                in_channels=1,  # 灰度图
                out_channels=16,  # 得到的特征图的个数(也是使用卷积核的个数)
                kernel_size=5,  # 卷积核的大小
                stride=1,  # 步长
                padding=2,  # 边缘填充,如果希望得到的特征图大小和原来一样,那么padding=(kernel_size-1)/2 if stride = 1
            ),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2),  # 池化层操作区域(2*2),输出结果为(16,14,14)
        )
        self.conv2 = nn.Sequential(
            nn.Conv2d(16, 32, 5, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(2),  # 输出(32,7,7)
        )
        self.out = nn.Linear(32 * 7 * 7, 10)  # 全连接层

    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = x.view(x.size(0), -1)
        output = self.out(x)
        return output


# 计算准确率
def accuracy(predictions, labels):
    pred = torch.max(predictions.data, 1)[1]
    rights = pred.eq(labels.data.view_as(pred)).sum()
    return rights, len(labels)


# 具体实例化
net = CNN().to(device)

# 损失函数
criterion = nn.CrossEntropyLoss()

# 优化器
optimizer = optim.Adam(net.parameters(), lr=0.001)

for epoch in range(num_epochs):
    train_rights = []

    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)  # 数据和标签移动到 GPU
        net.train()
        output = net(data)
        loss = criterion(output, target)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        right = accuracy(output, target)
        train_rights.append(right)

        if batch_idx % 100 == 0:
            net.eval()
            val_rights = []
            with torch.no_grad():  # 测试时不计算梯度以节省内存
                for (data, target) in test_loader:
                    data, target = data.to(device), target.to(device)  # 测试数据也要移动到 GPU
                    output = net(data)
                    right = accuracy(output, target)
                    val_rights.append(right)

            train_r = (sum([tup[0] for tup in train_rights]), sum([tup[1] for tup in train_rights]))
            val_r = (sum([tup[0] for tup in val_rights]), sum([tup[1] for tup in val_rights]))

            print('当前epoch:{}[{}/{} ({:.0f}%)]\t损失:{:.6f}\t 训练集准确率:{:.2f}%\t测试集正确率:{:.2f}%'.format(
                epoch, batch_idx * batch_size, len(train_loader.dataset),
                100 * batch_idx / len(train_loader),
                loss.data,
                100 * train_r[0].item() / train_r[1],  # 使用 item() 获取标量
                100 * val_r[0].item() / val_r[1]
            ))

详细解释

数据准备

train_dataset = datasets.MNIST(root='./data', train=True, transform=transforms.ToTensor(), download=True)

test_dataset = datasets.MNIST(root='./data', train=False, transform=transforms.ToTensor())

# 构建batch数据集
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)

test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=True)

train_dataset=datasets.MNIST(root='./data',train=True,transform=transforms.ToTensor(), download=True)

  • datasets.MNIST: 这是PyTorch提供的一个数据集类,用于加载MNIST手写数字数据集。MNIST数据集包含60,000张训练图像和10,000张测试图像,每张图像都是28x28像素的灰度图像,表示0到9之间的数字。

  • root='./data': 这是数据集下载和存储的根目录。在这个例子中,数据集将被下载到当前目录下的./data文件夹中。

  • train=True: 这个参数指定加载的是训练数据集。如果设置为False,则会加载测试数据集。

  • transform=transforms.ToTensor(): 这是数据预处理的一个步骤。transforms.ToTensor()将PIL Image或numpy数组转换为PyTorch张量(Tensor),并且将像素值从[0, 255]范围归一化到[0, 1]范围。

  • download=True: 如果数据集尚未下载到指定的root目录,这个参数会自动下载数据集。

test_dataset=datasets.MNIST(root='./data',train=False,transform=transforms.ToTensor())

  • 这行代码与第一行类似,但train=False表示加载的是MNIST的测试数据集。测试数据集包含10,000张图像,用于评估模型的性能。

  • download=True没有在这里出现,因为测试数据集通常在下载训练数据集时一同下载。

train_loader=torch.utils.data.DataLoader(dataset=train_dataset,batch_size=batch_size, shuffle=True)

  • torch.utils.data.DataLoader: 这是PyTorch中用于构建数据加载器的类。DataLoader可以将数据集打包成mini-batch的形式,便于模型训练。

  • dataset=train_dataset: 这是我们之前创建的训练数据集对象。DataLoader将从这个数据集中抽取数据。

  • batch_size=batch_size: 这是每个mini-batch的大小。batch_size是一个预定义的变量,通常在代码的其他地方定义。

  • shuffle=True: 这个参数表示在每次迭代时是否打乱数据集。打乱数据可以避免模型学习到数据顺序的偏差,从而提高训练效果。

test_loader=torch.utils.data.DataLoader(dataset=test_dataset,batch_size=batch_size, shuffle=True)

  • 这行代码与第三行类似,但使用的是测试数据集。测试数据集通常不需要在每次迭代时打乱数据,但在这个例子中,shuffle=True表示在每次测试时也会打乱数据集。这在某些情况下可能并不必要,但在代码中默认了这种设置。

数据的结构

这样讲可能只能知道一些概念的东西,不知道这个数据的具体结构,下面介绍一下数据的结构:

train_dataset结构

train_dataset 是一个 torchvision.datasets.MNIST 对象,专门用于处理 MNIST 数据集。它的主要属性和方法如下:

  • 数据结构:

    • train_dataset.data: 用于存储图像数据,是一个形状为 (N, 28, 28) 的张量,其中 N 是训练样本的数量(60,000)。每个图像都是28x28的灰度图。
    • train_dataset.targets: 用于存储对应的标签,是一个一维的张量,形状为 (N,),存放着每个图像的数字标签(0-9)。
  • 常用方法:

    • __getitem__(index): 用于获取指定索引(index)的样本和标签。返回的是一个元组 (image, label),其中 image 是转换为张量后的图像,label 是相应的数字标签。
    • __len__(): 返回数据集中样本的总数,这里是60,000。

train_loader结构

train_loader 是一个 torch.utils.data.DataLoader 对象,它为 train_dataset 提供划分和迭代的方式。主要特点包括:

  • 数据结构:

    • train_loader 本身并不存储数据。它是在遍历 train_dataset 时提供数据的工具。每次迭代都将返回一个 mini-batch 的数据
    • train_loader 将生成 mini-batch 的元组,通常每次迭代返回的是 (images, labels)
      • images: 一个形状为 (batch_size, 1, 28, 28) 的张量,其中 batch_size 是指定的每个 mini-batch 的大小。
      • labels: 一个形状为 (batch_size,) 的张量,存放当前 mini-batch 中每张图像的标签。
  • 常用方法:

    • __iter__(): 使得 train_loader 可以被用在 for 循环中,每次迭代都会返回一个新的 mini-batch。
    • __len__(): 返回 DataLoader 中的总 mini-batch 数量。这通常是数据集样本数除以 batch_size。

例如,假设 batch_size 的值为 64,那么在使用 train_loader 进行迭代时,每次迭代将得到:

  • images:

    • 形状为 (64, 1, 28, 28),表示64张28x28的灰度图像。每张图像的通道数为1,因为它是灰度图。
  • labels:

    • 形状为 (64,),表示这64张图像的标签,值在0到9之间。

DataLoader生成什么?

再来介绍一下torch.utils.data.DataLoader

DataLoader 生成的是可以迭代的 mini-batch 数据。具体来说,每次迭代时,DataLoader 会返回一个 mini-batch 的数据。这个 mini-batch 通常是一个元组 (images, labels),其中:

  • images 是一个形状为 (batch_size, C, H, W) 的张量,batch_size 是每次迭代的大小,C 是图像的通道数,H 和 W 是图像的高度和宽度。
  • labels 是一个形状为 (batch_size,) 的张量,表示相应图像的标签。

如何使用enumerate?

那么我们如何遍历这个元组呢?其实python不像C/C++,遍历需要用下标遍历,比如我们像便利一个列表我们直接是for idx in list:,这个idx得到的直接是list中的值,但是有时候我们希望知道遍历的下标(用于指示此次遍历到多少遍了)。这样就可以用"enumerate"

1. enumerate 的作用

enumerate 的主要作用是在遍历一个可迭代对象时,返回一个包含索引的元组。具体来说,它会对可迭代对象中的每一个元素配上一个索引值,从 0 开始(默认),然后逐个返回索引和元素。

2. 基本语法
enumerate(iterable, start=0)

  • iterable: 任何可迭代的对象,比如列表、元组、字符串等。
  • start: 索引的起始值,默认为 0,但你可以指定其他起始值。
3. 如何使用 enumerate
示例 1:基本用法
# 一个简单的列表
fruits = ['apple', 'banana', 'mango']

# 使用 enumerate 遍历列表
for index, value in enumerate(fruits):
    print(f"Index: {index}, Value: {value}")
输出:
Index: 0, Value: apple
Index: 1, Value: banana
Index: 2, Value: mango

在这个例子中,enumerate 为每个列表元素提供了一个索引,从 0 开始。我们通过 for 循环同时获取了索引和元素值。

示例 2:指定起始索引

你可以通过 enumerate 的第二个参数指定索引的起始值。例如,如果你想让索引从 1 开始:

fruits = ['apple', 'banana', 'mango']

# 使用 enumerate 遍历列表,索引从 1 开始
for index, value in enumerate(fruits, start=1):
    print(f"Index: {index}, Value: {value}")

输出:

Index: 1, Value: apple
Index: 2, Value: banana
Index: 3, Value: mango

4. 在 DataLoader 中使用 enumerate

在 PyTorch 中,我们通常使用 DataLoader 来加载数据集。在训练神经网络时,我们不仅需要遍历每个 mini-batch,还需要知道当前遍历到了第几个 batch。这时,enumerate 就非常有用。

示例:在 DataLoader 中使用 enumerate

假设我们有一个 DataLoader 加载了 MNIST 数据集,我们可以使用 enumerate 来同时获取 batch 的索引和数据。

import torch
from torchvision import datasets, transforms

# 定义数据集和 DataLoader
train_dataset = datasets.MNIST(root='./data', train=True, transform=transforms.ToTensor(), download=True)
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)

# 使用 enumerate 遍历 DataLoader
for batch_idx, (images, labels) in enumerate(train_loader):
    print(f"Batch Index: {batch_idx}, Images shape = {images.shape}, Labels shape = {labels.shape}")
    
    # 如果需要在第一个 batch 后停止,可以加一个条件判断
    if batch_idx == 0:
        break

输出:

Batch Index: 0, Images shape = torch.Size([64, 1, 28, 28]), Labels shape = torch.Size([64])

在这个例子中:

  • batch_idx 是当前 mini-batch 的索引。
  • images 和 labels 是当前 mini-batch 的数据和标签。
  • enumerate(train_loader) 返回的 batch_idx 是从 0 开始的批次索引,而 images 和 labels 是对应的批次数据。

CNN的定义

class CNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Sequential(  # 输入大小(1,28,28)
            nn.Conv2d(
                in_channels=1,  # 灰度图
                out_channels=16,  # 得到的特征图的个数(也是使用卷积核的个数)
                kernel_size=5,  # 卷积核的大小
                stride=1,  # 步长
                padding=2,  # 边缘填充,如果希望得到的特征图大小和原来一样,那么padding=(kernel_size-1)/2 if stride = 1
            ),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2),  # 池化层操作区域(2*2),输出结果为(16,14,14)
        )
        self.conv2 = nn.Sequential(
            nn.Conv2d(16, 32, 5, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(2),  # 输出(32,7,7)
        )
        self.out = nn.Linear(32 * 7 * 7, 10)  # 全连接层

    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = x.view(x.size(0), -1)
        output = self.out(x)
        return output

什么是类

为了讲清楚这个结构,我们从最基本的地方讲起。

1. 类和对象

在 Python 中,类是一种用于创建对象的蓝图或模板。类定义了一组属性和方法,这些属性和方法可以被实例化后的对象所使用。

定义类
class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def bark(self):
        return "Woof!"

  • __init__ 方法: 这是类的构造函数,用于初始化对象的属性。
  • self: 表示类的实例对象本身。
创建对象
my_dog = Dog("Buddy", 3)
print(my_dog.name)  # 输出: Buddy
print(my_dog.bark())  # 输出: Woof!

2. 继承

继承是面向对象编程的一个重要特性,它允许一个类(子类)继承另一个类(父类)的属性和方法。子类可以重用父类的代码,并且可以在不修改父类的情况下添加新的功能。

定义父类
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "I am an animal."

定义子类
class Dog(Animal):
    def __init__(self, name, age):
        super().__init__(name)  # 调用父类的构造函数
        self.age = age

    def speak(self):
        return "Woof!"

  • super(): 用于调用父类的方法。
  • super().__init__(name): 调用父类 Animal 的构造函数,初始化 name 属性。
创建子类对象
my_dog = Dog("Buddy", 3)
print(my_dog.name)  # 输出: Buddy
print(my_dog.age)  # 输出: 3
print(my_dog.speak())  # 输出: Woof!

3. super() 函数

super() 函数用于调用父类的方法。它在多重继承中特别有用,因为它可以确保正确的方法解析顺序(Method Resolution Order, MRO)。

super() 的基本用法
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "I am an animal."

class Dog(Animal):
    def __init__(self, name, age):
        super().__init__(name)  # 调用父类的构造函数
        self.age = age

    def speak(self):
        return "Woof!"

  • super().__init__(name): 调用父类 Animal 的构造函数,初始化 name 属性。
super() 的多重继承
class Animal:
    def speak(self):
        return "I am an animal."

class Mammal(Animal):
    def speak(self):
        return "I am a mammal."

class Dog(Mammal):
    def speak(self):
        return super().speak() + " And I bark."

my_dog = Dog()
print(my_dog.speak())  # 输出: I am a mammal. And I bark.

在这个例子中,super().speak() 首先调用 Mammal 类的 speak() 方法,然后在其基础上添加新的内容。

代码整体结构解释

1. 类定义与父类初始化
class CNN(nn.Module):
    def __init__(self):
        super().__init__()

  • 类定义CNN 类继承自 nn.Module
  • 父类初始化super().__init__() 调用父类 nn.Module 的构造函数确保 nn.Module 中的所有属性和方法被正确初始化
2. 第一个卷积层 (self.conv1)
self.conv1 = nn.Sequential(  # 输入大小(1,28,28)
    nn.Conv2d(
        in_channels=1,  # 灰度图
        out_channels=16,  # 得到的特征图的个数(也是使用卷积核的个数)
        kernel_size=5,  # 卷积核的大小
        stride=1,  # 步长
        padding=2,  # 边缘填充,如果希望得到的特征图大小和原来一样,那么padding=(kernel_size-1)/2 if stride = 1
    ),
    nn.ReLU(),
    nn.MaxPool2d(kernel_size=2),  # 池化层操作区域(2*2),输出结果为(16,14,14)
)

  • nn.Sequential: 是一个容器,允许我们将多个层按顺序组合起来。
  • nn.Conv2d: 2D 卷积层,用于提取图像特征。
    • in_channels=1: 输入通道数为 1(灰度图像)。
    • out_channels=16: 输出通道数为 16,即使用 16 个卷积核,得到的特征图有 16 个。
    • kernel_size=5: 卷积核大小为 5x5。
    • stride=1: 卷积操作的步长为 1。
    • padding=2: 边缘填充 2 个像素,确保输出特征图大小与输入一致。
  • nn.ReLU: 激活函数,引入非线性。
  • nn.MaxPool2d: 最大池化层,用于减少特征图的空间尺寸。
    • kernel_size=2: 池化窗口大小为 2x2,步长默认为 2,输出特征图大小为 (16, 14, 14)。
3. 第二个卷积层 (self.conv2)
self.conv2 = nn.Sequential(
    nn.Conv2d(16, 32, 5, 1, 2),
    nn.ReLU(),
    nn.MaxPool2d(2),  # 输出(32,7,7)
)

  • nn.Conv2d: 第二个卷积层,输入通道数为 16(来自第一个卷积层的输出),输出通道数为 32。
    • in_channels=16: 输入通道数为 16。
    • out_channels=32: 输出通道数为 32。
    • kernel_size=5: 卷积核大小为 5x5。
    • stride=1: 卷积操作的步长为 1。
    • padding=2: 边缘填充 2 个像素,确保输出特征图大小与输入一致。
  • nn.ReLU: 激活函数,引入非线性。
  • nn.MaxPool2d: 最大池化层,用于减少特征图的空间尺寸。
    • kernel_size=2: 池化窗口大小为 2x2,步长默认为 2,输出特征图大小为 (32, 7, 7)。
4. 全连接层 (self.out)
self.out = nn.Linear(32 * 7 * 7, 10)  # 全连接层

  • nn.Linear: 全连接层,将卷积层输出的特征图转换为最终的分类结果。
    • 32 * 7 * 7: 输入特征的大小,32 个特征图,每个大小为 7x7。
    • 10: 输出大小为 10,对应 10 个类别的分类任务。
5. 前向传播函数 (forward)
def forward(self, x):
    x = self.conv1(x)
    x = self.conv2(x)
    x = x.view(x.size(0), -1)
    output = self.out(x)
    return output

  • forward: 前向传播函数,定义了数据在神经网络中的流动路径。
    • self.conv1(x): 将输入数据 x 通过第一个卷积层 conv1
    • self.conv2(x): 将经过 conv1 处理后的数据通过第二个卷积层 conv2
    • x.view(x.size(0), -1): 将卷积层的输出展平为一维向量,x.size(0) 是批量大小,-1 表示自动计算展平后的长度。
    • self.out(x): 将展平后的数据通过全连接层 out,得到最终的输出结果。
    • return output: 返回输出结果。

各个方法的具体解释

1. nn.Conv2d 的参数

nn.Conv2d 用于创建二维卷积层,它的构造函数有以下参数:

nn.Conv2d(
    in_channels,      # 输入通道数
    out_channels,     # 输出通道数
    kernel_size,      # 卷积核的大小
    stride=1,         # 步长,默认为1
    padding=0,        # 填充,默认为0
    dilation=1,       # 空洞卷积的膨胀系数,默认为1
    groups=1,         # 分组卷积的数量,默认为1
    bias=True,        # 是否使用偏置
)

  • in_channels (int): 输入的通道数。对于灰度图像一般为 1,对于 RGB 图像一般为 3
  • out_channels (int): 输出的通道数,即卷积核的个数
  • kernel_size (int 或 tuple): 卷积核的尺寸。可以是一个整数(表示方形卷积核),也可以是一个二元组,表示长和宽,例如 (3, 5)
  • stride (int 或 tuple): 步长,默认为 1。可以是一个整数(表示在两个方向上的步长相同),也可以是一个二元组,例如 (1, 2)(表示在两个方向上不同)
  • padding (int 或 tuple): 在输入的边缘补充的零的数量(加几圈0)。也可以是一个整数或二元组,类似于 stride 的情况。
  • dilation (int 或 tuple): 控制卷积核元素之间的间隔,默认为 1。
  • groups (int): 控制卷积的分组。默认为 1 谷歌母公司的 MobileNet 等模型使用分组卷积时将其设为 2 或更多。
  • bias (bool): 是否使用偏置,默认为 True。
2. nn.ReLU 的参数
nn.ReLU(inplace=False)  # 'inplace' 是一个可选参数

  • inplace (bool): 是否进行原地操作。如果为 True,ReLU 会直接改变输入的值,可以节省内存,但不适合某些场景。默认为 False。
3. nn.MaxPool2d 的参数
nn.MaxPool2d(
    kernel_size,      # 池化窗口的大小
    stride=None,      # 步长,默认为 kernel_size
    padding=0,        # 填充大小,默认为0
    dilation=1,       # 空洞卷积的膨胀系数,默认为1
    return_indices=False,  # 是否返回池化 indices
    ceil_mode=False   # 是否向上取整
)

  • kernel_size (int 或 tuple): 池化窗口的大小,可以是一个整数或二元组
  • stride (int 或 tuple): 步长,默认为 kernel_size
  • padding (int 或 tuple): 在输入的边缘补充的零的数量。
  • dilation (int 或 tuple): 控制池化的间隔,默认为 1。
  • return_indices (bool): 如果为 True,返回每个池化区域的索引。默认为 False。
  • ceil_mode (bool): 若为 True,使用向上取整。在输入图像尺寸非常小的情况下可能有帮助。默认为 False。
4. nn.Linear 的参数
nn.Linear(in_features, out_features, bias=True)

  • in_features (int): 输入的特征数量,即上一层的输出维度。
  • out_features (int): 输出的特征数量,即本层的输出维度。
  • bias (bool): 是否使用偏置,默认为 True。
5. view 方法的参数
x.view(size)  # size 可以是一个整数或元组

  • size (int 或 tuple): 用于重新定义张量的形状,第一个维度通常是 batch size,有时你可以使用 -1 来自动推导某个维度的大小。例如,x.view(x.size(0), -1) 中的 -1 表示根据其他维度的大小自动计算它的值。

在PyTorch中,张量的 .size() 方法返回一个 torch.Size 对象,它包含了张量的形状信息。具体来说,.size(0) 是用来获取张量的第一维度的大小。

假设你有一个形状为 (batch_size, channels, height, width) 的四维张量 x,那么在不同维度下使用 .size() 会返回不同的值:

  • x.size(0):返回第一个维度的大小,即 batch_size
  • x.size(1):返回第二个维度的大小,即 channels
  • x.size(2):返回第三个维度的大小,即 height
  • x.size(3):返回第四个维度的大小,即 width
import torch

# 创建一个四维张量
x = torch.randn(32, 3, 64, 64)  # 批量大小为 32,通道数为 3,高度和宽度均为 64

# 获取各维度的大小
batch_size = x.size(0)  # 32
channels = x.size(1)    # 3
height = x.size(2)      # 64
width = x.size(3)       # 64

print(f"Batch Size: {batch_size}")
print(f"Channels: {channels}")
print(f"Height: {height}")
print(f"Width: {width}")

输出:::Batch Size: 32 Channels: 3 Height: 64 Width: 64

训练过程

训练前准备

# 具体实例化
net = CNN().to(device)

# 损失函数
criterion = nn.CrossEntropyLoss()

# 优化器
optimizer = optim.Adam(net.parameters(), lr=0.001)

训练过程


for epoch in range(num_epochs):
    train_rights = []

    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)  # 数据和标签移动到 GPU
        net.train()
        output = net(data)
        loss = criterion(output, target)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        right = accuracy(output, target)
        train_rights.append(right)

        if batch_idx % 100 == 0:
            net.eval()
            val_rights = []
            with torch.no_grad():  # 测试时不计算梯度以节省内存
                for (data, target) in test_loader:
                    data, target = data.to(device), target.to(device)  # 测试数据也要移动到 GPU
                    output = net(data)
                    right = accuracy(output, target)
                    val_rights.append(right)

            train_r = (sum([tup[0] for tup in train_rights]), sum([tup[1] for tup in train_rights]))
            val_r = (sum([tup[0] for tup in val_rights]), sum([tup[1] for tup in val_rights]))

            print('当前epoch:{}[{}/{} ({:.0f}%)]\t损失:{:.6f}\t 训练集准确率:{:.2f}%\t测试集正确率:{:.2f}%'.format(
                epoch, batch_idx * batch_size, len(train_loader.dataset),
                100 * batch_idx / len(train_loader),
                loss.data,
                100 * train_r[0].item() / train_r[1],  # 使用 item() 获取标量
                100 * val_r[0].item() / val_r[1]
            ))

1. 主要循环 for epoch in range(num_epochs)

这个循环遍历所有的训练周期(epoch)。每个 epoch 代表了一次完整的训练数据集遍历。

2. 初始化 train_rights 列表

train_rights = []

train_rights 用于存储每个批次(batch)的准确率信息。准确率信息通常以元组的形式存储,比如 (正确预测的数量, 总样本数量)

3. 训练数据加载与处理

for batch_idx, (data, target) in enumerate(train_loader):
    data, target = data.to(device), target.to(device)

  • train_loader: 一个 PyTorch DataLoader 对象,用于加载训练数据批次。
  • batch_idx: 当前批次的索引。
  • data: 输入数据(如图像)。
  • target: 目标标签(如分类标签)。
  • data.to(device) 和 target.to(device): 将数据和标签移动到指定的设备(如 GPU)。

4. 设置模型为训练模式

net.train()

net.train() 将模型设置为训练模式。这会影响某些层的行为,如 Dropout 和 BatchNorm。在训练模式下,Dropout 会随机丢弃神经元,BatchNorm 会更新其内部统计量。

5. 前向传播

output = net(data)

使用输入数据进行前向传播,得到模型的输出结果 output

6. 计算损失

loss = criterion(output, target)

使用预定义的损失函数 criterion 计算模型输出 output 和目标标签 target 之间的损失值。

7. 清零梯度

optimizer.zero_grad()

在每次反向传播之前,需要清零梯度,以避免梯度累积。

8. 反向传播

loss.backward()

调用 backward() 方法进行反向传播,计算损失函数对模型参数的梯度。

9. 更新模型参数

optimizer.step()

使用优化器更新模型参数,根据计算出的梯度调整参数。

10. 计算训练准确率

right = accuracy(output, target)
train_rights.append(right)

计算当前批次的准确率 right,并将其添加到 train_rights 列表中。

11. 验证模型性能

if batch_idx % 100 == 0:
    net.eval()
    val_rights = []
    with torch.no_grad():
        for (data, target) in test_loader:
            data, target = data.to(device), target.to(device)
            output = net(data)
            right = accuracy(output, target)
            val_rights.append(right)

  • net.eval(): 将模型设置为评估模式。在评估模式下,Dropout 和 BatchNorm 等层的行为会有所不同。
  • torch.no_grad(): 在评估时禁用梯度计算,以节省内存和计算资源。
  • test_loader: 用于加载验证数据批次。
  • val_rights: 用于存储验证批次(batch)的准确率信息。

12. 计算并打印训练和验证的准确率

train_r = (sum([tup[0] for tup in train_rights]), sum([tup[1] for tup in train_rights]))
val_r = (sum([tup[0] for tup in val_rights]), sum([tup[1] for tup in val_rights]))

print('当前epoch:{}[{}/{} ({:.0f}%)]\t损失:{:.6f}\t 训练集准确率:{:.2f}%\t测试集正确率:{:.2f}%'.format(
    epoch, batch_idx * batch_size, len(train_loader.dataset),
    100 * batch_idx / len(train_loader),
    loss.data,
    100 * train_r[0].item() / train_r[1],  # 使用 item() 获取标量
    100 * val_r[0].item() / val_r[1]
))

  • train_r: 计算训练集的总正确预测数量和总样本数量。
  • val_r: 计算验证集的总正确预测数量和总样本数量。
  • print: 输出当前 epoch、批次数、损失值、训练集准确率和验证集准确率。