写
使用 MNIST
数据集,训练一个识别手写数字的模型
导入必要的库
import torch
import torch.nn as nn # 用于构建神经网络
import torch.nn.functional as F # 各种函数,比如激活函数
import torch.optim as optim # 优化算法,比如 Adam
from torchvision import datasets, transforms # 视觉向量化
import torch.utils
import torch.utils.data
定义数据类型 & 加载数据集
这段代码定义了一个数据转换流程(transform
),目的是将输入的图像数据进行两步处理:
- 转换为Tensor:把图像数据变成一种适合深度学习模型处理的格式
- 标准化:调整图像数据的数值范围,让它的分布更适合模型训练
transforms.Compose
定义了一个标准化流程,将多个转换步骤结合在一起
# 转换图片类型为 Tensor,并且标准化,这里的标准化数值时针对 MNIST 数据集的,均值为0.1307,标准差为0.3081
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081))
])
下面加载数据集,由于 MNIST 数据集是在网上的,所以需要下载,这里使用 datasets.MNIST('./MNIST', train=True, download=True, transform=transform),
来指定下载,其中 ./MNIST
是指定了数据集的存储路径,train=
是指定了是作为训练集还是测试集,transform
是选择上面的预处理转换流程,将图片类型转换为 Tensor,并且进行标准化
torch.utils.data.DataLoader(...)
是 PyTorch 给的一个加载数据集的工具,dataset
传入的上面的 MNIST
数据集,batch_size
是每次传入模型的数据,shuffle
是是否打乱数据集,我们这里对训练集进行打乱,对测试集不打乱
# 下载并加载训练集
train_loader = torch.utils.data.DataLoader(
dataset=datasets.MNIST('./MNIST', train=True, download=True, transform=transform),
batch_size=64, shuffle=True
)
# 下载并且加载测试集
test_loader = torch.utils.data.DataLoader(
dataset=datasets.MNIST('./MNIST', train=False, download=True, transform=transform),
batch_size=1000, shuffle=False
)
定义神经网络结构
网络结构
这个神经网络有两层全连接层(nn.Linear
),也就是两个线性变换层:
self.fc1 = nn.Linear(28*28, 128)
:第一层(输入层到隐藏层)。输入是 28*28=784(因为 MNIST 图片是 28 x 28 像素,展平后是一个长度为 784 的向量),输出是 128 个神经元。这就像从 784 个输入点连接到 128 个中间点self.fc2 = nn.Linear(128, 10)
:第二层(隐藏层到输出层)。输入是 128 个神经元,输出是 10 个神经元,分别对应 0 到 9 这 10 个数字的分类分数
输入 (784维) --> 全连接层1 (784到128) --> ReLU激活 --> 全连接层2 (128到10) --> 输出 (10维得分)
前向传播(定义数据从输入到输出)
x = x.view(-1, 28*28)
:将数据进行降维,x
是从上面的dataLoader
拿到的数据,这时候的数据为tensor
类型,根据MNIST
数据集的定义,其中的tensor
数据形状应该为[batch_size, 1, 28, 28]
,这时候使用.view
方法对其进行降维,将其变成一维数据,即28*28
,其中-1
表示自动计算维度大小x = F.relu(self.fc1(x))
,这里对第一层神经网络的线性输出使用了 ReLU 激活函数x = self.fc2(x)
,最后进行输出
class Net(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(28*28, 128) # 输入层到隐藏层
self.fc2 = nn.Linear(128, 10) # 隐藏层到输出层,10个数字,故10个神经元
def forward(self, x):
x = x.view(-1, 28*28)
x = F.relu(self.fc1(x))
x= self.fc2(x)
return x
设置模型和优化器
对于使用 Apple silicon 或者 AMD GPUs 的 Mac 设备来说,可以使用 Metal Performance Shaders (MPS) 来代替 CUDA 进行 GPU 高性能训练
if torch.backends.mps.is_available():
device = torch.device("mps")
print("使用 MPS 后端")
else:
device = torch.device("cpu")
print("使用 CPU")
print(f"当前设备:{device}")
model = Net().to(device)
optimizer = optim.Adam(model.parameters(), lr=0.001)
定义训练函数
这里的 train
函数需要传入 5 个形参,之后设置模式为训练模式 model.train()
for batch_idx, (data, target) in enumerate(train_loader):
这个循环会分别从 train_loader
中拿出 batch
的数据以及编号
data, target = data.to(device), target.to(device)
会将数据移动到对应的设备中
optimizer.zero_grad()
对前面的梯度进行清空,防止影响这一批的训练
loss = F.cross_entropy(output=output, target=target)
使用交叉熵损失函数,进行计算,并且使用 loss.backward()
对损失函数进行反向传播
optimizer.step()
根据反向传播计算出来的梯度对其进行参数更新
def train(model, device, train_loader, optimizer, epoch):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = F.cross_entropy(output, target)
loss.backward()
optimizer.step()
if batch_idx % 100 == 0:
print(f'Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)}] Loss: {loss.item():.6f}')
测试函数
这里的 test
函数需要传入三个形参,model
是要评估的模型,device
是使用计算的设备,test_loader
是传入的测试集
torch.no_grad()
是设置不需要使用梯度
这里是将数据从测试集中拿出来,并且将其放入设备中,准备计算
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
是开始使用模型进行预测
test_loss += F.cross_entropy(output, target, reduction='sum').item()
使用交叉熵损失函数来计算预测和实际之间的差值,.item()
将 PyTorch 中的张量转化为普通的 Python 变量,以便于累加
pred = output.argmax(dim=1)
选择其中概率最高的作为预测结果
correct += pred.eq(target).sum().item()
将预测的和目标结果进行比较,返回一个布尔张量,并且将其累加,最终转化成 Python 数值,进行统计预测成功的
最后进行统计输出
test_loss /= len(test_loader.dataset)
accuracy = 100. * correct / len(test_loader.dataset)
print(f'\nTest set: Average loss: {test_loss:.4f}, Accuracy: {correct}/{len(test_loader.dataset)} ({accuracy:.2f}%)\n')
def test(model, device, test_loader):
model.eval() # 模型设置为评估模型
test_loss = 0 # 设置错误累计
corrent = 0 # 设置成功
# 在测试模式下不需要梯度
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
test_loss += F.cross_entropy(output, target, reduction='sum').item()
pred = output.argmax(dim=1)
corrent += pred.eq(target).sum().item()
test_loss /= len(test_loader.dataset)
accuracy = 100. * corrent / len(test_loader.dataset)
print(f'\nTest set: Average loss: {test_loss:.4f}, Accuracy: {corrent}/{len(test_loader.dataset)} ({accuracy:.2f}%)\n')
开始训练
这里选择迭代 6 次,在训练集训练结束后直接使用测试集进行检验
import time
all_time = time.time()
for epoch in range(1, 6):
start_time = time.time() # 记录开始时间
train(model, device, train_loader, optimizer, epoch)
test(model, device, test_loader)
elapsed = time.time() - start_time # 计算当前 epoch 用时
print(f"Epoch {epoch} 用时: {elapsed:.2f} 秒")
spend_time = time.time() - all_time
print(f'\n总耗时:{spend_time}')
训练结束后,可以选择将训练好的模型(参数)保存在文件中:
torch.save(model.state_dict(), 'mnist_model.pth')
随机打开一张图检验
import matplotlib.pyplot as plt
import numpy as np
# 获取一张图片
examples = enumerate(test_loader)
batch_idx, (example_data, example_targets) = next(examples)
# 取第0张
example = example_data[0].unsqueeze(0).to(device)
output = model(example)
pred = output.argmax(dim=1)
# 显示图像
plt.imshow(example.cpu().squeeze(), cmap='gray')
plt.title(f'pred result: {pred.item()}')
plt.axis('off')
plt.show()
使用 CPU 或者 GPU 训练的时长区别
笔者所使用的是 Macbook Air M2 16G 版本,在这里我们分别测试一下使用 Metal 和 CPU 训练的时间对比
训练的代码是这个,在这里我已经先定义好了
import time
all_time = time.time()
for epoch in range(1, 6):
start_time = time.time() # 记录开始时间
train(model, device, train_loader, optimizer, epoch)
test(model, device, test_loader)
elapsed = time.time() - start_time # 计算当前 epoch 用时
print(f"Epoch {epoch} 用时: {elapsed:.2f} 秒")
spend_time = time.time() - all_time
print(f'\n总耗时:{spend_time}')
使用 CPU
Epoch: 1 [0/60000] Loss: 2.277056
Epoch: 1 [6400/60000] Loss: 0.644652
Epoch: 1 [12800/60000] Loss: 0.222628
Epoch: 1 [19200/60000] Loss: 0.176244
Epoch: 1 [25600/60000] Loss: 0.378818
Epoch: 1 [32000/60000] Loss: 0.290092
Epoch: 1 [38400/60000] Loss: 0.162229
Epoch: 1 [44800/60000] Loss: 0.139718
Epoch: 1 [51200/60000] Loss: 0.346534
Epoch: 1 [57600/60000] Loss: 0.289447
Test set: Average loss: 0.1444, Accuracy: 9545/10000 (95.45%)
Epoch 1 用时: 3.59 秒
Epoch: 2 [0/60000] Loss: 0.144912
Epoch: 2 [6400/60000] Loss: 0.072045
Epoch: 2 [12800/60000] Loss: 0.135150
Epoch: 2 [19200/60000] Loss: 0.104258
Epoch: 2 [25600/60000] Loss: 0.049891
Epoch: 2 [32000/60000] Loss: 0.301743
Epoch: 2 [38400/60000] Loss: 0.177394
Epoch: 2 [44800/60000] Loss: 0.052172
Epoch: 2 [51200/60000] Loss: 0.280824
Epoch: 2 [57600/60000] Loss: 0.246844
...
Epoch 5 用时: 3.49 秒
总耗时:17.6266610622406
使用 GPU
Epoch: 1 [0/60000] Loss: 2.377831
Epoch: 1 [6400/60000] Loss: 0.408726
Epoch: 1 [12800/60000] Loss: 0.485179
Epoch: 1 [19200/60000] Loss: 0.278891
Epoch: 1 [25600/60000] Loss: 0.307072
Epoch: 1 [32000/60000] Loss: 0.113186
Epoch: 1 [38400/60000] Loss: 0.199770
Epoch: 1 [44800/60000] Loss: 0.225306
Epoch: 1 [51200/60000] Loss: 0.089396
Epoch: 1 [57600/60000] Loss: 0.151413
Test set: Average loss: 0.1318, Accuracy: 9604/10000 (96.04%)
Epoch 1 用时: 4.77 秒
Epoch: 2 [0/60000] Loss: 0.113682
Epoch: 2 [6400/60000] Loss: 0.044853
Epoch: 2 [12800/60000] Loss: 0.142270
Epoch: 2 [19200/60000] Loss: 0.122028
Epoch: 2 [25600/60000] Loss: 0.026501
Epoch: 2 [32000/60000] Loss: 0.095724
Epoch: 2 [38400/60000] Loss: 0.085834
Epoch: 2 [44800/60000] Loss: 0.090036
Epoch: 2 [51200/60000] Loss: 0.242773
Epoch: 2 [57600/60000] Loss: 0.036999
Test set: Average loss: 0.0993, Accuracy: 9718/10000 (97.18%)
Epoch 2 用时: 4.82 秒
Epoch: 3 [0/60000] Loss: 0.029983
Epoch: 3 [6400/60000] Loss: 0.091785
Epoch: 3 [12800/60000] Loss: 0.020735
Epoch: 3 [19200/60000] Loss: 0.058402
Epoch: 3 [25600/60000] Loss: 0.018810
Epoch: 3 [32000/60000] Loss: 0.218197
Epoch: 3 [38400/60000] Loss: 0.098756
Epoch: 3 [44800/60000] Loss: 0.098376
Epoch: 3 [51200/60000] Loss: 0.157909
Epoch: 3 [57600/60000] Loss: 0.023484
Test set: Average loss: 0.0871, Accuracy: 9737/10000 (97.37%)
Epoch 3 用时: 4.76 秒
Epoch: 4 [0/60000] Loss: 0.079279
Epoch: 4 [6400/60000] Loss: 0.013454
Epoch: 4 [12800/60000] Loss: 0.048985
Epoch: 4 [19200/60000] Loss: 0.022868
Epoch: 4 [25600/60000] Loss: 0.061240
Epoch: 4 [32000/60000] Loss: 0.007518
Epoch: 4 [38400/60000] Loss: 0.102460
Epoch: 4 [44800/60000] Loss: 0.040929
Epoch: 4 [51200/60000] Loss: 0.028633
Epoch: 4 [57600/60000] Loss: 0.163401
Test set: Average loss: 0.0762, Accuracy: 9758/10000 (97.58%)
Epoch 4 用时: 4.80 秒
Epoch: 5 [0/60000] Loss: 0.067033
Epoch: 5 [6400/60000] Loss: 0.032746
Epoch: 5 [12800/60000] Loss: 0.014985
Epoch: 5 [19200/60000] Loss: 0.072542
Epoch: 5 [25600/60000] Loss: 0.061535
Epoch: 5 [32000/60000] Loss: 0.034278
Epoch: 5 [38400/60000] Loss: 0.054543
Epoch: 5 [44800/60000] Loss: 0.005301
Epoch: 5 [51200/60000] Loss: 0.049687
Epoch: 5 [57600/60000] Loss: 0.048477
Test set: Average loss: 0.0788, Accuracy: 9755/10000 (97.55%)
Epoch 5 用时: 4.85 秒
总耗时:23.998369932174683
这里算下来,使用 CPU 推理的效果还不如 GPU,后来发现问题出在该数据集的 beach_size
上,beach_size
太小了,没办法发挥出 GPU 并行计算的优势
当笔者将 beach_size
提升到 128
后,效率有明显提升:
Epoch: 1 [0/60000] Loss: 0.002459
Epoch: 1 [12800/60000] Loss: 0.000959
Epoch: 1 [25600/60000] Loss: 0.001181
Epoch: 1 [38400/60000] Loss: 0.001985
Epoch: 1 [51200/60000] Loss: 0.002780
Test set: Average loss: 0.0803, Accuracy: 9799/10000 (97.99%)
Epoch 1 用时: 3.87 秒
Epoch: 2 [0/60000] Loss: 0.006955
Epoch: 2 [12800/60000] Loss: 0.002206
Epoch: 2 [25600/60000] Loss: 0.001883
Epoch: 2 [38400/60000] Loss: 0.003889
Epoch: 2 [51200/60000] Loss: 0.003672
Test set: Average loss: 0.1011, Accuracy: 9759/10000 (97.59%)
Epoch 2 用时: 3.64 秒
Epoch: 3 [0/60000] Loss: 0.005289
Epoch: 3 [12800/60000] Loss: 0.012927
Epoch: 3 [25600/60000] Loss: 0.028744
Epoch: 3 [38400/60000] Loss: 0.016574
Epoch: 3 [51200/60000] Loss: 0.019915
Test set: Average loss: 0.0929, Accuracy: 9788/10000 (97.88%)
Epoch 3 用时: 3.69 秒
Epoch: 4 [0/60000] Loss: 0.001103
Epoch: 4 [12800/60000] Loss: 0.002012
Epoch: 4 [25600/60000] Loss: 0.000979
Epoch: 4 [38400/60000] Loss: 0.009304
Epoch: 4 [51200/60000] Loss: 0.000931
Test set: Average loss: 0.0955, Accuracy: 9782/10000 (97.82%)
Epoch 4 用时: 3.64 秒
Epoch: 5 [0/60000] Loss: 0.000375
Epoch: 5 [12800/60000] Loss: 0.016383
Epoch: 5 [25600/60000] Loss: 0.003329
Epoch: 5 [38400/60000] Loss: 0.004040
Epoch: 5 [51200/60000] Loss: 0.012434
Test set: Average loss: 0.0964, Accuracy: 9784/10000 (97.84%)
Epoch 5 用时: 3.75 秒
总耗时:18.60126280784607