Pytorch训练神经网络的流程

前言

学习不能半途而废,现在重刷一遍eecs498,接着学习深度学习的内容

训练一个神经网络

无论是何种架构的网络,其训练流程大致可以分为:

  • 准备数据
  • 定义网络结构
  • 开始训练
  • 对模型进行测试评估
  • 保存模型(可选)

今天我们就根据上面这几步来尝试构建一个CNN网络,数据集就是MNIST 手写体识别

1. 准备数据

pytorch中,我们一般使用DataLoader来构建数据

1
from torch.utils.data import DataLoader

对于“手写体识别”这个例子来说,我们可以使用官方工具来获取和构建数据集

1
2
3
4
5
6
7
8
9
from torchvision import datasets, transforms
# 数据预处理:转为Tensor并标准化
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, ), (0.5, ))
])
# 下载训练集和测试集
train_dataset = datasets.MNIST(root= './data', train= True, download= True, transform= transform)
test_dataset = datasets.MNIST(root= './data', train= False, download= True, transform= transform)

当然,在很多情况下我们需要构建我们自己的数据集,这个时候我们就需要自己实现一个DataLoader,其实现过程也比较固定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from torch.utils.data import Dataset

class MyDataset(Dataset):
    def __init__(self, ...):          # 初始化:只存引用,不加载
        pass
    
    def __getitem__(self, idx):       # 索引→样本:核心逻辑
        return x, y                   # 必须返回 (特征, 标签)
    
    def __len__(self):                # 返回数据集大小
        return total_size

注意,使用DataLoaderDataSet其实是有好处的,因为本质上DataLoader是一个迭代器,也就是说在加载数据时是“按需”加载的,而不是把数据一股脑的全部读进内存。

2. 构建CNN模型

我们使用网络结构较为简单的CNN来说明一下如何自定义一个网络。在torch.nn这个包中有许多已经定义好的神经网络工具,借助这个工具,我们就可以较为简单的去定义每一层网络的结构和连接。

这是一个简单的模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class SimpleCNN(nn.Module):
    def __init__(self):
        super().__init__()
        
        # 第一个卷积块: 1通道输入 -> 32通道输出
        self.conv1 = nn.Sequential(
            nn.Conv2d(1, 32, kernel_size= 3, padding=1), # 28x28x1 -> 28x28x32
            nn.ReLU(),
            nn.MaxPool2d(kernel_size= 2) # 14 x 14 x 32
        )
        # 32通道输入 -> 64通道
        self.conv2 = nn.Sequential(
            nn.Conv2d(32, 64, kernel_size= 3, padding= 1), # 14 x 14 x 32 -> 14 x 14 x 64
            nn.ReLU(),
            nn.MaxPool2d(kernel_size= 2) # 14 x 14 x 64 -> 7 x 7 x 64
        )
        
        # 全连接层
        self.fc = nn.Sequential(
            nn.Flatten(), # 7 x7 x 64 = 3136
            nn.Linear(64 * 7 * 7, 128),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(128, 10)
        )
        
    # 前向传播
    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = self.fc(x)
        return x

需要注意的是,我们必须重写父类的方法

1
2
3
4
5
6
7
8
9
class Model(nn.Module):
        def __init__(self) -> None:
            super().__init__()
            self.conv1 = nn.Conv2d(1, 20, 5)
            self.conv2 = nn.Conv2d(20, 20, 5)

        def forward(self, x):
            x = F.relu(self.conv1(x))
            return F.relu(self.conv2(x))

也就是__init__()forward()方法,前者定义了整个网络的结构,而后者定义了数据流动的整个过程。

实例化模型

在定义好网络结构后,我们就可以去实例化一个模型。

1
2
model = SimpleCNN().to(device)
print(model)

以下是这个网络的结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
SimpleCNN(
  (conv1): Sequential(
    (0): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU()
    (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (conv2): Sequential(
    (0): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU()
    (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (fc): Sequential(
    (0): Flatten(start_dim=1, end_dim=-1)
    (1): Linear(in_features=3136, out_features=128, bias=True)
    (2): ReLU()
    (3): Dropout(p=0.5, inplace=False)
    (4): Linear(in_features=128, out_features=10, bias=True)
  )
)

计算模型总的参数

1
2
3
4
# 计算模型参数量
total_params = sum(p.numel() for p in model.parameters())
print(f'模型总参数量:{total_params:,}')   
模型总参数量421,642

3. 训练模型

定义好一个模型后,我们就可以去训练这个模型。一般的训练模型的过程就是迭代优化的一个逻辑。下面是一些我们需要记住的必备参数

  • 迭代次数
  • 学习率
  • 损失函数
  • 优化器

显然,后面三个参数都是在梯度下降算法中使用的。一个适度的学习率可以使模型快速收敛,而根据不同的任务可以选择合理的损失函数,最后就是选择合适的梯度下降算法。

1
2
3
4
5
6
7
import torch.optim as optim

# 设置超参数
n_epochs = 10
learning_rate = 0.001
criterion = nn.CrossEntropyLoss() # 交叉熵损失函数
optimizer = optim.Adam(model.parameters(), lr= learning_rate) # 传递模型的参数和学习率

设置完以上超参数后就可以开始训练模型了,有很多流程都是固定的。

设置训练模式

注意,我们说的训练是迭代训练的,因此下面的处理指的都是单次训练。

首先开启model的训练模式

1
model.train()

训练模式会启用一些优化策略,比如说DropOutBatch Normalization等优化,待会我们会在预测中关闭训练模式。

除此之外,我们还应该收集一些辅助信息以来查看训练过程中是否一切正常

1
2
3
running_loss = 0.0 # 训练误差
correct = 0 # 准确率
total = 0

开始训练

需要从训练集中获取一个训练数据和标签。

1
for i, (image, labels) in enumerate(train_loader):

也就是从上文中的DataLoader中获取。然后将数据迁移到CPU

1
2
3
# 将数据移动到GPU上
images = image.to(device)
labels = labels.to(device)

执行前向传播

1
2
# 前向传播
outputs = model(images)

也就是将训练数据传递给modelmodel会根据训练数据得到一个输出,也就是标签,我们将得到的标签和真实的标签来进行误差计算

1
loss = criterion(outputs, labels)

得到误差损失后,我们就必须进行梯度更新,梯度更新的过程就是进行反向传播的过程

1
2
3
4
# 反向传播
optimizer.zero_grad() # 清零梯度
loss.backward() # 计算梯度
optimizer.step() # 更新权重

训练模型也有几个关键的固定步骤,所以我把训练过程的代码全部贴出来

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 训练循环
train_losses = []
train_accuracies = []
for epoch in range(n_epochs): 											# n次迭代
    # 设置训练模式				
    model.train() 														# 打开训练模式
    running_loss = 0.0
    correct = 0
    total = 0
    
    for i, (image, labels) in enumerate(train_loader): 			# 通过迭代器获取数据
        # 将数据移动到GPU上
        images = image.to(device)								# 移动数据到对应计算设备
        labels = labels.to(device)
        
        # 前向传播
        outputs = model(images)									# 前向传播
        loss = criterion(outputs, labels)						# 计算损失
        
        # 反向传播
        optimizer.zero_grad() # 清零梯度						# 清零梯度
        loss.backward() # 计算梯度								# 反向传播
        optimizer.step() # 更新权重								# 更新权重
        
        # 统计准确率
        running_loss += loss.item()
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
        
    # 计算epoch统计
    epoch_loss = running_loss / len(train_loader)
    epoch_acc = 100 * correct / total
    train_losses.append(epoch_loss)
    train_accuracies.append(epoch_acc)
    
    print(f"Epoch [{epoch+1}/{n_epochs}], "
          f"Loss: {epoch_loss:.4f}, "
          f"Accuracy: {epoch_acc:.2f}%")
print("训练完成!")

import matplotlib.pyplot as plt
# 训练结束后,一次性绘制
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
# 绘制损失曲线
ax1.plot(train_losses, 'b-', label='Training Loss')
ax1.set_title('Training Loss')
ax1.set_xlabel('Epoch')
ax1.set_ylabel('Loss')
ax1.legend()
ax1.grid(True)
# 绘制准确率曲线
ax2.plot(train_accuracies, 'r-', label='Training Accuracy')
ax2.set_title('Training Accuracy')
ax2.set_xlabel('Epoch')
ax2.set_ylabel('Accuracy (%)')
ax2.legend()
ax2.grid(True)
plt.tight_layout()
plt.show()

4. 测试与预测

训练完一个模型后,如果在训练集上的精度还不错,那么我们就可以在验证集上测试其准确率。

测试也是分下面几个步骤:

开启评估模式

在训练阶段,一些优化方法如DropoutBatch Normalization等其实是不需要开启的,而通过

1
model.eval()

就可以很方便的一键关闭。

关闭梯度计算

这里使用的是一个torch自带的上下文管理器

1
2
with torch.no_grad():
    ...

迭代测试集进行计算

1
2
3
4
5
6
7
8
9
for images, labels in test_loader:
    images = images.to(device)
    labels = labels.to(device)

    outputs = model(images)
    _, predicted = torch.max(outputs.data, 1)

    test_total += labels.size(0)
    test_correct += (predicted == labels).sum().item()

总结

总的来说,训练并测试神经网络就可以分为上面几步。希望上面的代码可以作为参考,以后不时之需来看看!

Licensed under CC BY-NC-SA 4.0
花有重开日,人无再少年
使用 Hugo 构建
主题 StackJimmy 设计