• 浙大人工智能算法与系统课程作业指南系列(一)续:口罩识别的神经网络训练


    浙大人工智能算法与系统课程作业指南系列(一)续:口罩识别的神经网络训练

    好了好了,各位萌新小伙伴们,小板凳做好,我们开始继续讲这个作业里的坑了。上一篇我们花了整整一篇的空间,为大家简单介绍了Pytorch的有关数据处理的函数以及作业的数据处理部分的各种令人头皮发麻的操作,这其实对各位小伙伴未来的AI科研也是有点帮助的······吧(假装自己信了,┓( ´∀` )┏)。下面就继续聊聊关于神经网络的训练部分的坑吧。

    在数据处理部分下面,如果我没记错的话,应该有这样的一行代码(我写这个的时候作业网页已经关了,没办法┓( ´∀` )┏)

    device = torch.device("cuda:0") if torch.cuda.is_available() else torch.device("cpu")
    

    这个代码就是告诉你,能用GPU就用GPU,不能用再用CPU,因为虽然给的示例代码的模型很小,但是架不住在notebook上面跑得慢啊,大概如果训练1代就要一分多钟?反正要收敛的话要好久。所以能用GPU就用GPU。实际上平台的服务器是提供GPU跑模型的,但是要自己提交作业上去,具体咋操作到时候上课应该会讲,这里就不多BB了。

    接下来我们建立数据集,然后初始化一个我们的示例代码里面的神经网络模型:

    train_data_loader, valid_data_loader = processing_data(data_path=data_path, height=160, width=160, batch_size=32, test_split=0.1)
    
    model = MobileNetV1(classes = 2).to(device)
    

    关于数据处理这个函数,我上一篇已经说得很清楚了,还没看懂的话直接打死,╭(╯^╰)╮。然后这里建立模型就用了我们上面指定的那个运行设备,notebook的环境上是不能使用GPU的(至少我这一届是),所以会默认在CPU上跑(所以你们这些倒霉蛋们就认命吧23333)。说句题外话,虽然不知道未来这门课会变成什么样,反正我这一届是有1300+的人上这门课,然后所有人一开始都在这个平台的服务器上跑模型,有一次我一个可怜的室友,在排队把模型放到服务器的GPU准备跑跑看,结果他前面有48个人排队23333,好不容易排到他以后,他代码有一个地方写错了结果没运行成功,改完错以后再提交结果又有40多个人排队(噗哈哈哈哈),所以大家尽量在自己的本地电脑上跑吧,device的指定其实就是方便你在本地跑的。然后关于MobileNetV1,因为这个我写的这个系列的文章只是从代码功能层面上帮各位萌新小伙伴排下坑,所以关于这部分的具体原理嘛,emmmmm,自己找文章好咯~

    接下来,notebook上应该是给了一个优化器和一个学习率的下降策略,这两块我觉得notebook上面解释还是蛮清楚的,而且参数也基本不用改,就这么着吧(敲字敲得手指痛,摸鱼它不香么,滑稽.jpg)

    然后有一个隐藏的大坑,就是对损失函数的设定,我先把对应的代码放下来,至于为什么是个大坑,之后再解释:

    criterion = nn.CrossEntropyLoss()
    

    这个是我们在分类问题中常用的交叉熵函数,具体的表达式太长了,实在懒得打,有兴趣的小伙伴自己查一查哟。但是这里面是隐藏着一个巨坑的,尤其是对于之前没用过Pytorch的萌新来说,因为这个简单的损失函数中包含着两个内容,一个是取对数的LogSoftmax操作,一个是衡量概率差距的损失函数NLLLoss()函数,这一块等到训练模型的部分再和大家细说,小伙伴们别着急哟~

    终于要进入训练模型的部分啦~才怪!其实notebook上的代码还有一个巨坑。就是下面的一个小小的句子:

    loss_list = []
    

    小伙伴们可能会问啦,欸?这有啥坑的?就这?害,单纯这个代码段本身并没有什么问题,但是如果放到notebook上面,可就出大问题了。不知道小伙伴们在notebook上跑代码的时候有没有出现过这个问题:当我把前面notebook代码里面的epochs = 2的代码改了,把数值设得高一点,程序一般都会在读条读到第三代的时候突然断了,然后过了一会儿直接中断停了,是不是一脸懵逼?

    问题就出在这个loss_list = []里面。因为这个在训练的时候每一个epoch里面的每一个batch他都会存储一次loss,而notebook后台服务器提供的内存又不够,所以训练到第三代的时候,内存空间就不够了,不够了······,所以这个部分可以留着,毕竟你后面要画损失函数曲线,但是不要每一个batch就储存一次,这个等到后面的训练模型的代码里面再和大家详细说(小小的句子,伤害辣么大)。

    好啦好啦,小伙伴们坐好了,终于到了激动人心的模型训练阶段了,大家鼓掌~
    在训练模型的部分,代码大致上长这个样子(因为我稍微改过一点,原来notebook上的忘备份了,所以,凑合看咯┓( ´∀` )┏)

    for epoch in range(epochs):
        model.train()
        for batch_idx, (x, y) in enumerate(train_data_loader):
    
            x = x.to(device)
            y = y.to(device)
            pred_y = model(x)
    
            loss = criterion(pred_y, y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
    
            if loss < best_loss:
                best_model_weights = copy.deepcopy(model.state_dict())
                best_loss = loss
            loss_list.append(loss.item())
    

    先不看别的,就单单看这个鬼句子:

    loss_list.append(loss.item())
    

    你瞅瞅它在哪里,它每个batch就存一次,不爆炸才怪呢(真想一刀捅死这个给代码的人)。然后呢关于这个代码有一个和notebook不太一样的地方,就是在迭代的部分:

    for batch_idx, (x, y) in enumerate(train_data_loader):
    ...
    

    原来的代码里是用了一个tqdm把enumerate部分包起来了,其实就是显示一个读条进度,因为这也是程序在notebook里奔溃的原因之一,所以我一气之下就给删了┓( ´∀` )┏

    好了接下来我们详细分析一下这个部分的代码。在内层循环里面的前两句,就是将x和y放到指定的device上面进行运行,没有什么难度,很容易读懂。然后就是接下来的句子:

    pred_y = model(x)
    

    这个代码的实际功能,是给定一个批次的输入图片x进入我们的模型model中,然后输出一个tensor,这个tensor的维度是(batch_size, classes = 2),因为我们是一个二分类问题嘛。batch_size就是这一批里面有几张图片,然后classes这个维度实际上是表征分类的一个one-hot向量。one-hot向量是啥?emmmmm,因为我们是一个二分类问题,然后之前我们在ImageFolder这个函数中给出了当标签为mask时值为0,对应的one-hot向量就是[1, 0];nomask时值为1,对应的one-hot向量就是[0, 1]。关于这个model是怎么定义的,我们先不管,我们先把整个训练的函数搞清楚再说,因为接下来一步就是我们之前说的关于损失函数nn.CrossEntropyLoss()的大坑了。

    我们来看下一段十分重要的代码:

    loss = criterion(pred_y, y)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    

    虽然只有四行,但是这里面的坑可能和上一篇的图片处理的那个transforms = T.Compose······里面的东西还要多,我们还是一点一点拆开来看。为了让大家能够看得更清楚,我们将这四行代码拆成两块:

    loss = criterion(pred_y, y)
    loss.backward()
    
    optimizer.zero_grad()
    optimizer.step()
    

    欸?为啥没按照顺序拆出来看呀?别急别急,因为这样拆的话从代码原理和功能上看是比较容易理解的。

    首先是上面的关于loss的两行。大家需要了解的是,在机器学习还有这个深度学习中,要想将我们模型的参数进行更新学习的话,大多数情况需要使用的一个方法叫做“梯度下降”,具体公式就不贴在这里了,总之核心就是你要求出损失函数的值对你的参数的偏导数作为梯度,然后用这个梯度去更新参数。那么这个loss的两行里面,第一行就是用我们定义的损失函数criterion(也就是那个CrossEntropyLoss)来求出我们的输出和实际标签之间的损失函数值。

    求梯度的话一个最直观的方式就是,你知道模型损失函数的表达式,然后人为写一个函数进行求导,然后把得到的导数拿出来更新参数。简单的线性模型可能还好,那你自己去试试推导一下3层的全连接神经网络试一哈,狗头都给你锤爆┓( ´∀` )┏。为了简化编程者的负担,在Pytorch中有一个机制叫做自动求导。Pytorch中构建出来的神经网络,会将所有中间你用到的模型里面的参数构造出一个叫做计算图的东西,当你对损失函数值调用backward()方法的时候,Pytorch会自动沿着计算图,将沿途的能求导的tensor参数全都求梯度。这也就是这一块的第二行loss.backward()所做的工作,是不是很亲民吖~

    接下来是下面的关于optimizer的两行代码,同样地我们一行一行来看。在看第一行之前,我们还要对前一个块的那个backward()进行进一步的说明。在Pytorch的tensor中有一个成员叫做grad,每次backward()沿着计算图求解出来梯度之后,会把求得的梯度累加到grad里面。也就是说,如果你不在每一个batch训练结束之后对参数的grad进行清零操作,那么你这一轮的梯度就会被累计到你下一轮的训练里面,形象地说,就是有种接盘侠的感觉(误)。为了不当接盘侠,就需要在梯度更新之前把参数的梯度先清一下零(好快的车车www),这也就是optimizer.zero_grad()做的事情。

    梯度清零了,也又重新求出来了,该更新了吧,接下来的代码optimizer.step()就帮你做了梯度更新。当然了,我们实际这个示例代码给的训练方式是Adam,和正常的梯度下降还是有一些区别的,但是用梯度下降来帮助大家解释原理是比较清楚的。啥?Adam是啥?懒鬼自己去查!哼╭(╯^╰)╮。

    在理解这两个块的代码功能之后,我们再来重新看拆分之前的代码:先求loss,然后梯度清零,再求梯度,最后梯度更新。是不是一目了然?还不快谢谢我o( ̄ヘ ̄o)。训练部分就这样结束······那是不可能的,因为还有个之前一直在提的大坑没填呢,就是这个CrossEntropyLoss()。(可能我要是不提,萌新小伙伴们都要忘了)为啥说这个是个大坑,这还要从盘古开天辟地之日······啊不,从每一个图片的标签还有不同的损失函数说起。

    如果大家对最小二乘法有一定的了解的话,那么你们实际上就已经对一个叫做MSELoss的损失函数有了深刻了解了。实际上MSELoss函数就是求我们预测值和实际值的差值的平方和作为损失,然后最小二乘就是让这个损失最小化。但是这个放在分类问题里就不太合适了,假设我们神经网络输出的是[0.3, 0.7],就可以认为类别是和[0, 1],也就是nomask一样了,但是如果你用MSELoss的话,他就会强行学习参数,让你的输出不断向[0, 1]靠近,可能非要让你学成[0.01, 0.99]才算完事。

    这河狸吗,这不河狸。而且之前我们也提到过,虽然我们神经网络的输出是一个(batch_size, classes = 2)的一个tensor,而且classes那一维就是one-hot的向量,但是我们实际上给每一个图片的标签就是一个数0或者1啊,这咋搞?所以我们迫切需要一个新的损失函数计算方式:首先对于一个one-hot向量,我们先对变量进行一下Softmax操作······

    等等,Softmax是啥?实际上,分类问题得到的one-hot向量我们更倾向于把每一个分类看做一个该类别的概率,比如说,如果输出是[0.3, 0.7],我们就认为有30%的概率是mask,70%的概率是nomask。既然是概率,我们就需要概率的值大于0,概率的和为1。但是很不幸的是,神经网络的输出,这两点,都保证不了(垃圾AI毁我青春,误)。所以我们需要手动的让数据满足上面两点,这就是Softmax做的事情。

    在做完Softmax之后,我们还没有解决损失函数的事情呢,别急吖,对于已经Softmax的one-hot向量,我们会使用NLLLoss()函数,对每个类的概率以及类别实际的标签进行一个损失值的计算,具体的原理和公式······阿巴阿巴(打死),我推荐大家看一下《Deep Learning with Pytorch》这本书,上面说的蛮清楚的,绝对不是我懒得打字啊,绝不是!!!

    但是呢,说了这么多,我们代码里面这两个东西一个都没见到啊,咋回事?害,实际上为了更加方便编程者的模型构造,Pytorch很贴心地把Softmax和NLLLoss()整合到一起,叫做CrossEntropyLoss()函数,哇好棒!有事Pytorch干,没事干······(喂110吗,这里有个变态)。这也是为什么有些文章里面会说,Pytorch的交叉熵函数的实际计算方法和交叉熵的概念定义不大一样,确实是啊,没毛病。

    然后关于训练模型部分,后面还有一个判断是不是最好的模型,然后把loss的值保存到之前那个坑人的loss_list里面,这个很好理解啊,就不多说了,然后示例代码里面的训练模型部分除了后面的那个模型保存,就没有了。啊啊,亲爱的各位小伙伴们,我们终于训练出一个模型啦啊啊啊啊啊!······虽然我们现在连模型长什么样子都还不知道······本来呢我是想接着和大家聊聊示例代码里面神经网络结构的相关代码的,但是我一看文章长度,好家伙又4000+了,再写我估计你们看着都会觉得头疼,所以这一篇就先到这里啦,下一篇我会讲讲课程的示例代码里面的神经网络结构的相关代码,然后给大家顺带介绍一下有关Pytorch的相关操作。如果还有剩的话呢,再和大家介绍一下做这个作业的时候还要注意的大坑,篇幅太多的话就再分出去一篇,反正肯定会带着大家把这个作业做完的(如果你们这一届还是这个作业的话)。好啦我们下一篇再见辣~

  • 相关阅读:
    leetcode刷题-54螺旋矩阵
    leetcode刷题-53最大子序和
    leetcode刷题-52N皇后2
    leetcode刷题-51N皇后
    leetcode刷题-50Pow(x, n)
    leetcode刷题-37解数独
    leetcode刷题-49字母异位词分组
    leetcode刷题-48旋转图像
    数据结构—B树、B+树、B*树
    LeetCode–旋转数组的最小数字
  • 原文地址:https://www.cnblogs.com/JacobDale-TechLearning/p/14138077.html
Copyright © 2020-2023  润新知