明月深度学习实践002:关于模型训练你应该关注的内容

2021-10-28 11:31:47 浏览数 (1)

前面我们已经建立了一个简单的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是最基础的模型之一,是我们很好的入门模型。这次我们讲了几个方面的内容:

  1. GPU的几个常用函数
  2. 模型参数量计算
  3. 模型计算量计算
  4. 模型训练指标记录
  5. 卷积层特征图查看

PS:后面希望自己能保持每周更新一篇的节奏吧,写文章的过程其实更多是自己总结学习的过程。

0 人点赞