前面我们已经建立了一个简单的LeNet模型,已经训练它了来做手写数字识别,基于mnist数据集上的效果还是不错的。今天接着写一些模型训练相关的内容。
说明:代码基于Pytorch。
0x01 GPU的几个常用函数
现在深度学习没有GPU,那基本是寸步难行。关于GPU在使用上,相比CPU确实坑多很多,有时莫名其妙就挂了。这里介绍几个常用的函数:
代码语言:javascript复制# 判断cuda是否可用
torch.cuda.is_available()
# 定义device
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 在数据准备好之后,应该统一转移到device
images = images.to(device)
# 查看显存的占用,除以2个1024,单位位MB
torch.cuda.max_memory_allocated()//(1024*1024)
# 释放显存占用
# empty_cache()来释放所有未使用的缓存的内存,以便其它 GPU 应用能够使用. 但是并不能释放 tensors 所占用的 GPU 显存
torch.cuda.empty_cache()
0x02 模型各层参数量查询
模型的参数会直接影响显存大小是否足够的问题,而显存往往又是相对昂贵的 计算资源,因此非常重要。Pytorch提供了查看模型参数的方法:
把这个对应到我们的模型代码:
代码语言:javascript复制 self.conv1 = nn.Sequential( # input: 28*28*1
# filter 大小 5×5,filter 深度(个数)为 6,padding为2,卷积步长s=1
# 输出矩阵大小为 28×28×6
nn.Conv2d(1, 6, 5, 1, 2),
nn.ReLU(),
# filter 大小 2×2(即 f=2),步长 s=2,no padding
# 输出:14*14*6
nn.MaxPool2d(kernel_size=2, stride=2),
)
self.conv2 = nn.Sequential(
# filter 大小 5×5,filter 个数为 16,padding 为 0, 卷积步长 s=1
# 输出矩阵大小为 10×10×16
nn.Conv2d(6, 16, 5),
nn.ReLU(),
# 输出:5*5*16
nn.MaxPool2d(2, 2)
)
# 全连接层
self.fc1 = nn.Sequential(
nn.Linear(16 * 5 * 5, 120),
nn.ReLU()
)
# 全连接层
self.fc2 = nn.Sequential(
nn.Linear(120, 84),
nn.ReLU()
)
# 全连接层,输出神经元数量为 10,代表 0~9 十个数字类别
self.fc3 = nn.Linear(84, 10)
从这里我们能看得更清楚了,对于卷积层的参数量:
输出通道数 * 输入通道数 * 卷积核size 输出通道数
其中输出通道数也等于卷积核数量,bias和输出通道数相等。
而对于全连接层的参数量计算,则更加简单:
输入神经元数量 * 输出神经元数量 输出神经元数量
其中bias等于输出神经元数量。
0x03 模型计算量
模型的计算量会直接模型的训练时长,在Pytorch上有一个thop的包可以进行计算(这个包需要安装: pip install thop),使用也非常简单:
这里flops是计算量,params是参数量。
0x04 记录模型评估指标
对应pytorch官方提供有一个可视化工具visdom,不过个人觉得这个东西不是太好用,如果只是记录指标的话,而如果使用tensorboardx,却只能自己使用,很难在团队之间分享结果,所以还是选择熟悉的mlflow,简单却好用。先初始化mlflow:
我们内部部署了一个独立的mlflow服务,只要往这里写数据,就能很方便的在团队之间进行分享。
代码语言:javascript复制import time
import torch.optim as optim
EPOCH = 10 # 遍历数据集次数
LR = 0.001 # 学习率
# 设置mlflow及记录参数
mlflow.set_experiment('LeNet测试实验')
mlflow.start_run()
mlflow.log_param('EPOCH', EPOCH)
mlflow.log_param('LR', LR)
# 定义是否使用GPU
print("cuda" if torch.cuda.is_available() else "cpu")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 定义损失函数loss function 和优化方式(采用SGD)
net = LeNet().to(device)
criterion = nn.CrossEntropyLoss() # 交叉熵损失函数,通常用于多分类问题上
optimizer = optim.SGD(net.parameters(), lr=LR, momentum=0.9)
start = time.time()
for epoch in range(EPOCH):
sum_loss = 0.0
# 数据读取
for i, data in enumerate(trainloader):
inputs, labels = data
inputs, labels = inputs.to(device), labels.to(device)
# 梯度清零
optimizer.zero_grad()
# forward backward
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# 每训练100个batch打印一次平均loss
sum_loss = loss.item()
if i % 100 == 99:
print('[%d, %d] loss: %.03f' % (epoch 1, i 1, sum_loss / 100))
sum_loss = 0.0
# 每跑完一次epoch测试一下准确率
with torch.no_grad():
correct = 0
total = 0
for data in testloader:
images, labels = data
images, labels = images.to(device), labels.to(device)
outputs = net(images)
# 取得分最高的那个类
_, predicted = torch.max(outputs.data, 1)
total = labels.size(0)
correct = (predicted == labels).sum()
print('第%d个epoch的识别准确率为:%.2f%%' % (epoch 1, (100 * int(correct) / total)))
mlflow.log_metric('Accuracy', 100 * int(correct) / total, step=epoch)
# 保存模型
torch.save(net.state_dict(), 'models/lenet/lenet_d.pth' % epoch)
# 记录运行时间与显存占用
# 这两个值对于非常重要,记录下来
mlflow.log_metric('gpu', torch.cuda.max_memory_allocated()//(1024*1024))
mlflow.log_metric('run_time', time.time()-start)
# 结束记录
mlflow.end_run()
# 释放显存占用
torch.cuda.empty_cache()
训练过程跟原来是一样的,只是增加了参数记录。当训练完成之后,会有一条记录:
最后准确率97.77%,显存占用6M,运行时间65.03秒。
点击进去,我们就能看到对应的准确率随着训练次数的增多而变化的趋势:
简单直观。
0x05 特征图
在训练完的时候,我们可能想知道一下模型卷积层输出的特征图是什么呢?我们可以对它们进行查看。
首先,我们随便加载一个图像,并执行 第一个卷积层:
我们可以看到,我们加载的这个图像应该是一个28*28数字8,在我们执行第一个卷积层之前,我们需要先讲这个图像的像素点归一化为0到1之间的值,除以255即可。Size中第一个元素1表示我们输入的是一个图像。
img_conv1是第一个卷积层输出的值,注意该值的shape,该值实际有6个通道,我们逐一将其展示出来看看:
代码语言:javascript复制print(torch.max(img_conv1), torch.min(img_conv1))
max_val = torch.max(img_conv1).to('cpu')
for i in range(img_conv1.shape[1]):
_img = img_conv1[0][i]
_img = _img.to('cpu').clone()
_img = (_img/max_val)*255
img_pil = transforms.ToPILImage()(_img)
img_pil = img_pil.resize((112, 112))
display(img_pil)
其输入大概如下:
事实上共有6个小图像,这里只是展示前2个,看形状长得跟原图差不多,应该是将原图的特征提取了出来,其他4个小图像也是类似。
这种特征图应该类似一种激活图的意思,就是说特征图中白色的像素表示该点的值越大,也该也意味着该特征越明显。
接着,我们把卷积层1的输出结果输入到卷积层2中:
这跟我们在模型的forward方法中的调用方式是非常接近的,输入的是6通道,输出的是16通道。而可视化也类似:
这里也是省略了剩下的14个小图像。这些特征激活图其实很难理解的。
0x06 关于LeNet后记
LeNet是最基础的模型之一,是我们很好的入门模型。这次我们讲了几个方面的内容:
- GPU的几个常用函数
- 模型参数量计算
- 模型计算量计算
- 模型训练指标记录
- 卷积层特征图查看
PS:后面希望自己能保持每周更新一篇的节奏吧,写文章的过程其实更多是自己总结学习的过程。