前言
学习不能半途而废,现在重刷一遍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
|
注意,使用DataLoader和DataSet其实是有好处的,因为本质上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的训练模式
训练模式会启用一些优化策略,比如说DropOut和Batch 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)
|
也就是将训练数据传递给model,model会根据训练数据得到一个输出,也就是标签,我们将得到的标签和真实的标签来进行误差计算
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. 测试与预测
训练完一个模型后,如果在训练集上的精度还不错,那么我们就可以在验证集上测试其准确率。
测试也是分下面几个步骤:
开启评估模式
在训练阶段,一些优化方法如Dropout和Batch Normalization等其实是不需要开启的,而通过
就可以很方便的一键关闭。
关闭梯度计算
这里使用的是一个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()
|
总结
总的来说,训练并测试神经网络就可以分为上面几步。希望上面的代码可以作为参考,以后不时之需来看看!