• 【深度学习】实验2答案:构建自己的多层感知机


    DL_class

    学堂在线《深度学习》实验课代码+报告(其中实验1和实验6有配套PPT),授课老师为胡晓林老师。课程链接:https://www.xuetangx.com/training/DP080910033751/619488?channel=i.area.manual_search

    持续更新中。
    所有代码为作者所写,并非最后的“标准答案”,只有实验6被扣了1分,其余皆是满分。仓库链接:https://github.com/W-caner/DL_classs。 此外,欢迎关注我的CSDN:https://blog.csdn.net/Can__er?type=blog
    部分数据集由于过大无法上传,我会在博客中给出下载链接。如果对代码有疑问,有更好的思路等,也非常欢迎在评论区与我交流~

    实验2:构建自己的多层感知机

    实现代码

    保留了实验一中的学习率衰减部分,在Q1~Q4的过程中为了绘图方便,没有使用早停法。分别使用numpy将不同层中的前向传播和反向传播方法实现即可。

    损失函数

    • 欧氏距离:

          def forward(self, logit, gt):
              """
                输入: (minibatch)
                - logit: 最后一个全连接层的输出结果, 尺寸(batch_size, 10)
                - gt: 真实标签, 尺寸(batch_size, 10)
              """
              ############################################################################
              # TODO:
              # 在minibatch内计算平均准确率和损失
              # 分别保存在self.accu和self.loss里(将在solver.py里自动使用)
              # 只需要返回self.loss
              # self.y_: (batch_size, 10)
              self.y_ = logit
              self.y = gt
              # loss: (batch_size, 1)
              L = np.sqrt(np.sum(np.square(self.y_ - self.y), axis=1))
              self.loss = np.average(L)
              # pre: (batch_size, )
              # pre = np.argmax(self.y_, axis=1)
              self.accu = accuracy_score(np.argmax(self.y_, axis=1), np.argmax(self.y,axis=1))
              ############################################################################
              return self.loss
      
          def backward(self):
              ############################################################################
              # TODO:
              # 计算并返回梯度(与logit具有同样的尺寸)
              return self.y_-self.y
              ############################################################################
      
      
      • 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
    • 交叉熵损失函数:

          def forward(self, logit, gt):
              """
                输入: (minibatch)
                - logit: 最后一个全连接层的输出结果, 尺寸(batch_size, 10)
                - gt: 真实标签, 尺寸(batch_size, 10)
              """
      
              ############################################################################
              # TODO: 
              # 在minibatch内计算平均准确率和损失,分别保存在self.accu和self.loss里(将在solver.py里自动使用)
              # 只需要返回self.loss
              # self.y_: (batch_size, 10)
              self.y_ = np.exp(logit) / np.sum(np.exp(logit), axis=1, keepdims=True)
              self.y = gt
              # L: (batch_size, 1)
              L = -np.sum(np.log(self.y_)*self.y, axis=1)
              self.loss = np.average(L)
              # pre: (batch_size, )
              # pre = np.argmax(self.y_, axis=1)
              self.accu = accuracy_score(np.argmax(self.y_, axis=1), np.argmax(self.y, axis=1))
              ############################################################################
              return self.loss
      
      
          def backward(self):
      
              ############################################################################
              # TODO: 
              # 计算并返回梯度(与logit具有同样的尺寸)
              return self.y_-self.y
              ############################################################################
      
      
      • 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

    正向/反向传播

    • 全连接层:

      def forward(self, Input):
      
              ###########################################################################
              # TODO: 
              # 对输入计算Wx+b并返回结果.
              self.Input = Input
              self.batch_zise = Input.shape[0]
              return np.dot(Input, self.W) + self.b
              ############################################################################
      
      
          def backward(self, delta):
              # 输入的delta由下一层计算得到
              ############################################################################
              # TODO: 
              # 根据delta计算梯度
              # grad_W: (128, batch_size)*(batch_size,10) -> (128, 10)
              self.grad_W = np.dot(self.Input.T, delta)/self.batch_zise
              # grad_b: (batch_size, 10) -> (1, 10)
              self.grad_b = np.average(delta, axis=0)
              # delta: (batch_size, 10)*(10,128) -> (batch_size, 128)
              return np.dot(delta, self.W.T)
              ############################################################################
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
    • relu激活层:

          def forward(self, Input):
      
              ############################################################################
              # TODO: 
              # 对输入应用ReLU激活函数并返回结果
              self.Input = Input
              return np.maximum(Input, 0)
      
              ############################################################################
      
      
          def backward(self, delta):
      
              ############################################################################
              # TODO: 
              # 根据delta计算梯度
              delta[self.Input<0]=0
              return delta
              ############################################################################
      
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
    • Sigmoid激活层:

          def forward(self, Input):
      
              ############################################################################
              # TODO: 
              # 对输入应用Sigmoid激活函数并返回结果
              self.out =  1 / (1+np.exp(-Input))
              return self.out
              ############################################################################
      
          def backward(self, delta):
      
              ############################################################################
              # TODO: 
              # 根据delta计算梯度
              return delta * self.out *(1-self.out)
              ############################################################################
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17

    参数更新

    这里不知道是不是写的公式有问题,我也没有搜到 “带权重衰减的动量梯度” 真正公式是什么, 当我指定梯度为0.8的时候,训练到后期反而准确率会下降。默认梯度0.0即不会出现问题。

        # 一步反向传播,逐层更新参数
        def step(self, model):
            layers = model.layerList
            for layer in layers:
                if layer.trainable:
                    ############################################################################
                    # TODO:
                    # 使用layer.grad_W和layer.grad_b计算diff_W and diff_b.
                    # 注意weightDecay项.
                    if self.momentum:
                        layer.W_velocity = self.momentum * layer.W_velocity + \
                            self.learningRate * \
                            (layer.grad_W + self.weightDecay*layer.W)
                        layer.b_velocity = self.momentum * layer.b_velocity + \
                            self.learningRate*layer.grad_b
                        layer.diff_W = -layer.W_velocity
                        layer.diff_b = -layer.b_velocity
                    else:
                        # Weight update without momentum
                        layer.diff_W = -self.learningRate * \
                            (layer.grad_W+self.weightDecay* layer.W)
                        layer.diff_b = -self.learningRate * layer.grad_b
                    ############################################################################
                    # Weight update
                    layer.W += layer.diff_W
                    layer.b += layer.diff_b
    
    • 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

    报告问题

    Q1

    记录训练和测试准确率,绘制损失函数和准确率曲线图;

    下图是使用原始参数,两种方法训练了30个周期的loss和accuracy的结果。

    欧式距离损失:

    请添加图片描述

    softmax交叉熵损失:

    请添加图片描述

    Q2

    比较分别使用 Sigmoid 和 ReLU 激活函数时的结果,可以从收敛情况、准确率等方面比较。

    • 收敛情况:无论使用哪个作为激活函数,只要模型正确,在足够的周期下都可以收敛,但是使用relu收敛速度(斜率)更快,推测是因为relu函数大于0的时候,导数为恒定值计算较快。
    • 准确率:在每个周期纵向对比,使用sigmoid准确率更高,且最后能达到的效果也更好,推测是因为relu导致了网络的稀疏性,对于这种简单数据集而言丢失了一些参数。但同时能看出,relu对于复杂数据集过拟合给出了比较好的解决方案。

    Q3

    比较分别使用欧式距离损失和交叉熵损失时的结果;

    在使用同一种激活函数时,交叉熵损失函数在测试集上有着更好的表现,并且收敛的周期数少(速度快),而在训练集上的准确率来看则是欧氏距离更胜一筹。

    感觉损失函数的影响并不绝对,需要搭配恰到好处的应用实例,恰好的激活函数和网络结构才能得到更好的表现。比如在模型输出与真实值的误差服从高斯分布的假设下,最小化均方差损失函数与极大似然估计本质上是一致的,因此在这个假设能被满足的场景中(比如回归),均方差损失是一个很好的损失函数选择;而交叉熵损失可能会更适用于本例(分类问题)。

    Q4

    构造具有两个隐含层的多层感知机,自行选取合适的激活函数和损失函数,与只有一个隐含层的结果相比较;

    这里尝试了sigmoid+crossentropy,这二者的梯度有较为天然的结合,仿照该数据集上的经典模型“LeNet5”的结构,搭建了一个类似的网络(728,128,30,10)。以同样的参数训练和测试,得到对比图:

    请添加图片描述

    发现多层网络甚至不如一层网络效果好,推测是因为多层网络较为复杂,又都是全连接,随着学习率的衰减导致陷入局部最优的问题,所以更换激活函数为relu,网络结构如下所示:

    multMLP = Network()
    # 使用FCLayer和ReLULayer构建多层感知机
    # 128, 30为隐含层的神经元数目
    multMLP.add(FCLayer(784, 128))
    multMLP.add(ReLULayer())
    multMLP.add(FCLayer(128, 30))
    multMLP.add(ReLULayer())
    multMLP.add(FCLayer(30, 10))
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    对比图如下所示,无论是在收敛速度上,还是训练的准确率上,多个隐含层的网络结构有着更好的表现:

    请添加图片描述

    Q5

    本案例中给定的超参数可能表现不佳,请自行调整超参数尝试取得更好的结果,记录下每组超参数的结果,并作比较和分析。

    从上面的训练过程中明显可以看出,存在较为严重的局部最优和过拟合现象,所以调大学习率,加入早停法,还是按照先批次,再学习率,最后调节权重衰减和周期的方法进行调节,对比结果基本和实验一报告中的情况(就是过大会怎么样,过小会怎么样,为什么最终选择这个参数)吻合,这里不再给出相同参数具体分析,只给对比图了。

    • Batch_size:选择100即可
      请添加图片描述

    • learning_rate:收敛较快,可以直接从0.05开始衰减,每周期衰减1.1~1.5都可以。

    • weight_decay:

    请添加图片描述

    可以发现,正则项的加入一定要适度,其起到的是“调节作用”,在超过0.1的时候已经对模型效果产生了负面影响,也就是稍微加一点正则化(甚至这样的简单模型可以不加)为最优。

    最终使用batch_size为100,learning_rate从0.08开始,每周期衰减1.2,权重衰减为0.005,早停法进行25个周期的训练,基本上训练5~6个周期即可到达饱和。

    此时得到测试集上的最好的结果为96.61%, 请添加图片描述

  • 相关阅读:
    JSP基本概念
    某款服务器插上4张TDP功耗75瓦PCIE卡无法开机的调试过程
    零基础新手也能会的H5邀请函制作教程
    web3之女巫(sybil)
    LyScript 获取上一条与下一条指令
    【图解设计模式】适配器模式
    UVM如何处理out-of-order乱序传输
    python基础--函数的应用、lambda表达式以及高阶函数
    Apache Log4j Server (CVE-2017-5645) 反序列化命令执行漏洞
    js中的instance,isPrototype和getPrototypeOf的使用,来判断类的关系
  • 原文地址:https://blog.csdn.net/Can__er/article/details/127859970