【深度学习】实验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
            ############################################################################
    
    
  • 交叉熵损失函数:

        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
            ############################################################################
    
    

正向/反向传播

  • 全连接层:

    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)
            ############################################################################
    
  • 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
            ############################################################################
    
    
    
  • 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)
            ############################################################################
    
    

参数更新

这里不知道是不是写的公式有问题,我也没有搜到 “带权重衰减的动量梯度” 真正公式是什么, 当我指定梯度为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

报告问题

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))

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

请添加图片描述

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%, 请添加图片描述

猜你喜欢

转载自blog.csdn.net/Can__er/article/details/127859970