深度学习

深度学习

深度学习第四天

今日心情

第六章:学习的一些技巧

6.1 参数的更新方法(也就是学习方法)

我们所要达成的目的是最优化:也就是使得损失函数的值最小的方法

  • 随机梯度下降法(SGD):
    • 优点:简单易懂
    • 缺点:如果函数的形状非均向(anisotropic),比如呈延伸状,搜索的路径就会非常低效
    • 代码实现:
    class SGD:
      def __init__(self, lr=0.01):
          self.lr = lr
      def update(self, params, grads):
          for key in params.keys():
      p       arams[key] -= self.lr * grads[key]
    • 具体调用形式
      optimizer = SGD()#初始化这样的optimizer,这样的好处是如果要使用其他的更新参数的方法,我们可以直接例化相同的名称来实现调用
      grads = network.gradient(x_batch, t_batch)
      params = network.params
      optimizer.update(params, grads)
          
      def gradient(self, x, t):
          # forward
          self.loss(x, t)#损失函数我们也有很多种,通常我们根据需要选
    
          # backward
          dout = 1
          dout = self.lastLayer.backward(dout)
          
          layers = list(self.layers.values())
          layers.reverse()
          for layer in layers:
              dout = layer.backward(dout)
    
          # 设定
          grads = {}
          grads['W1'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
          grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db
    
          return grads
    
  • Momentum(动量)
    • 为了解决传统的随机梯度下降法算面临的不足之处,即函数的形象非均相化,也就是说像如下这个图片的函数,x和y轴的梯度变化是不均匀的(拓展到n维很显然这样会很影响整体的搜索效率)
    • 而随机梯度下降法的失效的根本原因就是梯度的方向并没有指向最小值的方向,存在一个山谷,山谷中间的值大小是差不多的,但是存在某个最低点,这样的坏处就显而易见,如果使用随机梯度下降法,在y轴方向(梯度变化大的方向),参数的选择是来回跳跃的,也就是走出了之字这样的路线,这大大降低了搜索的效率
    • 在这里,我们引入动量的这样一个物理学领域的思想来解决问题,既然之前SGD的根本问题在于它对之前的路径没有感觉,来回横跳,那我们不妨给他加个惯性从而让其实现找到基本的路径

      聊到这里,突然就想到了实验室里面的曾经使用过的“蚁群算法”,这个算法本身是为了促进函数的收敛的,即快速找到最短路径(好的路上信息素多,所以快速收敛),但是在符号回归中为了防止收敛到局部,也就是为了寻找最优解而不是局部最优解,即跳出局部,我们使用了“当信息素比较多的路径上让蚂蚁别再搜索”,从而实现了搜索的广度的实现

    • 算法涉及的主要的公式
    • 怎么解释这个算法呢?我觉得不妨从下山这个角度来解释
      • 何为梯度:也就是山的坡度,也就是上山的坡度,那梯度取反相当于下山的坡度,在第一项中也就可以看成是推你下山的力,用来增加你下山的力量;而前面的系数a也就是一个摩擦力用来减小速度;
      • 通过这样的一个设计,我们就可以实现在照顾梯度的定义的时候让其保持原有的一些运动性质不变
      • 那么在相同的一个问题中(a = 0.9, n = 0.1)
        • 假设$v_0 = (0,0)$
        • 由于沿y轴方向比沿x轴方向的梯度更大,所以$v = (10,1)$
        • 到了对岸,此时$ v = (-1,2)$
        • 周而复始,从而慢慢就增大了x方向的变化,抵消了y轴方向的变化
    • 算法的实现
      class Momentum:
          def __init__(self, lr=0.01, momentum=0.9):
              self.lr = lr
              self.momentum = momentum
              self.v = None
          def update(self, params, grads):
              if self.v is None:
                  self.v = {}
                  for key, val in params.items():
                  self.v[key] = np.zeros_like(val)
              for key in params.keys():
                  self.v[key] = self.momentum*self.v[key] - self.lr*grads[key]
                  params[key] += self.v[key]  
  • AdaGrad
    • 实现的思路:在关于学习率的有效技巧中,有一种被称为学习率衰减(learning ratedecay)的方法,即随着学习的进行,使学习率逐渐减小。逐渐减小学习率的想法,相当于将“全体”参数的学习率值一起降低。而AdaGrad进一步发展了这个想法,针对“一个一个”的参数,赋予其“定制”的值,也就是不同的参数的减小的幅度是不一样的
    • 在更新参数时,通过乘以$\frac{1}{\sqrt{h}}$,就可以调整学习的尺度,也就是说如果某一个参数变化的非常快也就是损失函数关于该参数的梯度很大,那么我就让这个参数的变化幅度不那么大
    • 久而久之,也就是会随着更新次数的增多,会导致参数不再变化,这时候我们可以通过RMSProp方法,即并不是将过去所有的梯度一视同仁地相加,而是逐渐 地遗忘过去的梯度,在做加法运算时将新梯度的信息更多地反映出来,又称作称为“指数移动平均”,呈指数函数式地减小过去的梯度的尺度
    • 对于当前的问题来说,他会限制y轴方向的更新,从而实现快速的查找
    • 代码实现
      class AdaGrad:
          def __init__(self, lr=0.01):
              self.lr = lr
              self.h = None
          def update(self, params, grads):
              if self.h is None:
                  self.h = {}
              for key, val in params.items():
                  self.h[key] = np.zeros_like(val)
              for key in params.keys():
                  self.h[key] += grads[key] * grads[key]
                  params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
    • 效率展示
  • Adam
    • 定义以及算法思路:融合了Momentum和AdaGrad的方法
    • 效果展示:

6.2 初始权重的重要性

设定什么样的权重初始值,经常关系到神经网络的学习能否成功,介绍权重初始值的推荐值,并通过实验确认神经网络的学习是否会快速进行

抑制过拟合,提高模型泛化能力的,我们可以减小权重参数的值,权值衰减就是这样的一种方法,那我们可以选择一开始选择比较小的参数来进行初始化,我们通过选择这样的随机分布来初始化参数的值 那为什么不能全部设置为0呢,其实是,不妨假设第一层和第二层的权制都为0,那不管你的输入如何,第二层的输出一定是相同的,意味着反向传播回来带来的权值的更新是相同的,其实说白了就是没法产生有效的更新,并拥有了对称的值(重复的值) 那我们对于每一层的激活值有以下的一些要求

  • 首先就是不能完全偏向0和1,否则在梯度的反向传递过程中容易发生梯度消失
  • 激活值也不能有所偏向,否则表现力就会不够,如果100个神经元都输出几乎相同的值,那么也可以由1个神经元来表达基本相同的事情。因此,激活值在分布上有所偏向会出现“表现力受限”的问题。
  • 我们要求激活值的分布具有一定的广度 所以我们出现了合适的初始值的分布的选择
  • Xavier初始值:
    • 适用于线性函数,sigmoid函数和 tanh函数左右对称,且中央附近可以视作线性函数,所以适合使用Xavier初始值
    • 如果前一层的节点数为n,则初始值使用标准差为$\frac{1}{\sqrt{n}}$的分布 ,从这个结果可知,越是后面的层,图像变得越歪斜,但是呈现了比之前更有广度的分布。因为各层间传递的数据有适当的广度,所以sigmoid函数的表现力不受限制,有望进行高效的学习。
  • ReLU的权重初始值—He初始值
    • 激活函数使用ReLU
    • 当前一层的节点数为n时, He初始值使用标准差为$\sqrt{\frac{2}{{n}}}$ 的高斯分布
    • 当“std = 0.01”时,各层的激活值非常小 A。神经网络上传递的是非常小的值,说明逆向传播时权重的梯度也同样很小。这是很严重的问题,实际上学习基本上没有进展
    • 对于第二张我们很轻易可以发现随着层数的增加,越来越偏向于0分布,从而容易出现梯度消失
    • 对于第三张,各层中分布的广度相同。由于即便层加深,数据的广度也能保持不变,因此逆向传播时,也会传递合适的值 从而,我们在基于这三种来训练我的mini数据,我们可以得到以下的结果 我们很容易就会发现标准差为0,01的数据基本无法训练,如前文所述,无法进行有效的参数更新 其次是当权重初始值为Xavier初始值和He初始值时,学习进行得很顺利。并且,我们发现He初始值时的学习进度更快一些

6.3 Batch Normalization

在上一节中,我们通过选择合适的初始值来实现了让每一层的激活值尽可能的分布均匀

那么,一个自然而然的想法:有没有其他办法,不吃操作,还能让激活值尽可能的分布均匀呢

答案就是:Batch Normalization,即为了使各层拥有适当的广度,“强制性”地调整激活值的分布

综上,Batch Normalization的好处有以下的三点:

  • 可以不依赖于初始值实现激活值的广度

  • 可以提高学习率

  • 可以抑制过拟合

  • BatchNorm的实现:

    如前所述, Batch Norm的思路是调整各层的激活值分布使其拥有适当的广度。为此,要向神经网络中插入对数据分布进行正规化的层,即BatchNormalization层(下文简称Batch Norm层) Batch Norm,顾名思义,以进行学习时的mini-batch为单位,按minibatch进行正规化。具体而言,就是进行使数据分布的均值为0、方差为1的正规化,然后通过将这个处理插入到激活函数的前面(或者后面),可以减小数据分布的偏向 但插入之后数据的分布就会发生变化,比较乱,我们就需要对所有的数据进行正规化(均值为0,方差为1)的变化

    问题在于正规化之后的数据一半正数一半负数,如果送进relu层,那就会一半变成0,所以我们采用数据的变化的方法

    初始状态: 一开始,γ=1,β=0。也就是说,刚开始大家还是穿着标准的军训服(维持刚开始训练的稳定性)。

    学习与进化: 随着网络的训练,网络会自己去摸索:“欸?对于第 3 层的特征来说,均值为 0 方差为 1 好像会让我丢失很多图像的边缘信息。如果均值变成 5,方差变成 2,下一层学得会更好!”

    完美变形: 于是,网络通过反向传播,自动把这层的 γ 调整为 2,β 调整为 5。

  • BatchNormal层的好处展示

  • BatchNormal层的好处描述

    • 使用batchnormal可以加速学习的进度,即使用较大的学习率
    • 使用batchnormal可以使得训练对于初始权重具有健壮性,实际上,在不使用Batch Norm的情况下,如果不赋予一个尺度好的初始值,学习将完全无法进行。

如何抑制过拟合—正则化

过拟合产生的原因

何为过拟合,简单来说就是模型的泛化能力不强,即只认识训练过的数据,对于没有经过训练的数据的识别的准确性不高

  • 模型拥有大量的参数,表现力强
  • 训练数据小

思考:模型的能力强$\Leftarrow$参数量大,那么当选择训练大模型的时候,我们需要大量的训练数据 据此如果我们要构造一个过拟合的模型,其实很轻而易举的就可以想到我们可以选择让神经层数很多但是选择比较小的训练数据,即为此,要从MNIST数据集原本的60000个训练数据中只选定300个,并且,为了增加网络的复杂度,使用7层网络(每层有100个神经元,激活函数为ReLU)

如何抑制过拟合

我们通常有两种方法,其实我们先讲讲第一种方法

  • 权值衰减:

    • 很多过拟合原本就是因为权重参数取值过大才发生的,所以我们很自然的想法就是见底权制的取值
    • 回忆我们之前的调整参数的方法AdaGrad,就是通过减小对于损失函数的变化影响比较大的参数的变化从而实现更加有效的训练策略
    • 神经网络的学习目的是减小损失函数的值,我们可以采用$损失函数+加上权重的平方范数(L2范数)$,即如果将权重记为W, L2范数的权值衰减就是$\frac{1}{2}\lambda W$ ,

    不妨求个导看看,$\frac{\partial{LOSS}}{\partial{W}}+\lambda W$

    从而我们可以看到我们的,λ是控制正则化强度的超参数。 λ设置得越大,对大的权重施加的惩罚就越重,如下图所见

    • 结果展示:
    • 其实我们想要的不是百分之百,而是训练数据和测试数据的值基本接近
  • Dropout

    • 作为抑制过拟合的方法,前面我们介绍了为损失函数加上权重的L2范数的权值衰减方法该方法可以简单地实现,在某种程度上能够抑制过拟合。但是,如果网络的模型变得很复杂,只用权值衰减就难以应对了。在这种情况下,我们经常会使用Dropout方法。
    • Dropout的思想:
      • 训练时候:随机选出隐藏层的神经元,然后将其删除。被删除的神经元不再进行信号的传递,
      • 测试时,虽然会传递所有的神经元信号,但是对于各个神经元的输出,要乘上训练时的删除比例后再输出。
      • 综上,其核心思想其实就是为了防止有的神经元表现太好,所以通过随机的删除某些神经元来提高平均的表现能力
      • 代码实现:
          class Dropout:
              def __init__(self, dropout_ratio=0.5):
                  self.dropout_ratio = dropout_ratio
                  self.mask = None
          def forward(self, x, train_flg=True):
              if train_flg:
              self.mask = np.random.rand(*x.shape) > self.dropout_ratio
              return x * self.mask#我们来限制神经元是否发生作用的方式是通过对输出乘以掩码mask来实现
              else:
              return x * (1.0 - self.dropout_ratio)
          def backward(self, dout):
              return dout * self.mask
      
    • 效果展示
    • 一些思考:我们会有一种感觉,如果多个神经网络同时去预测,那么我们是不是通过取他们所预测的均值,我们就可以提高预测的准确性呢?答案是肯定的,这就是我们所说的集成学习。我们其实反过来想想,dropout的学习其实也是通过随机的删除神经元来实现部分神经元的训练来使得其均匀的从数据中学习,然后平均的输出,也就是说,可以理解成,Dropout将集成学习的效果(模拟地)通过一个网络实现了。

超参数的验证

一些前置知识

超参数的定义:各层的神经元数量、 batch大小、参数更新时的学习率或权值衰减,虽然超参数的取值非常重要,但是在决定超参数的过程中一般会伴随很多的试错。

那么怎么试错呢?

我们这里引入验证数据,那我们为什么不直接使用测试数据来直接试错呢?

  • 答案就是如果我们这样做,就会使得模型在特定的测试数据上表现良好但是不具备泛化性,可能就会得到不能拟合其他数据、泛化能力低的模型

  • 训练数据用于参数(权重和偏置)的学习,验证数据用于超参数的性能评估。为了确认泛化能力,要在最后使用(比较理想的是只用一次)测试数据。

然而对于不同的数据集,有的会事先分成训练数据、验证数据、测试数据三部分,有的只分成训练数据和测试数据两部分,有的则不进行分割。如果是MNIST数据集,获得验证数据的最简单的方法就是从训练数据中事先分割20%作为验证数据

在分割数据之前,我们应该随机打乱数据,防止原始数据是具有规律的

优化的核心——逐步缩小好值的范围
  • 我们在这里选用使用如下的寻找超参数的流程,其实和生物的梯度取样是相似的
    1. 先确定一个大概的范围
    2. 从数据中随机采样,(为什么选择随机采样,而不采取有规则性的网格式的搜索呢)

      在进行神经网络的超参数的最优化时,与网格搜索等有规律的搜索相比,随机采样的搜索方式效果更好。这是因为在多个超参数中,各个超参数对最终的识别精度的影响程度不同

    3. 使用第二步选择出来的参数,进行训练然后在验证数据集中进行效果的评估但是要将epoch设置得很小
    4. 不断的重复,大概100轮来筛选范围

    其实更像是草台班子的经验之谈罢了,如果真的要精确,我们可以选择贝叶斯优化

  • 代码实现:
    weight_decay = 10 ** np.random.uniform(-8, -4)
    lr = 10 ** np.random.uniform(-6, -2)

卷积神经网络

(写到这里感慨一下,以为其实不多的内容也写了将近9000字,来吧,最核心的出装:cnn)

7.1 CNN的整体结构

7.2 卷积层和池化层(Convolution 层 和 Pooling 层)

7.2.1

在引入卷积层之前,我们先分析一下全连接层的问题。

我们知道,全连接层在处理图像的时候,是先将数据展开成一维的,而这样子就会损失掉一些空间信息,比如某个点的周围是什么样的数据,所以我们引入卷积层(卷积核(滤波器)),可以理解为之前的“Affi ne - ReLU”连接被替换成了“Convolution -ReLU -(Pooling)”连接

比如,空间上邻近的像素为相似的值、 RBG的各个通道之间分别有密切的关联性、相距较远的像素之间没有什么关联等,因为全连接层会忽视形状,将全部的输入数据作为相同的神经元(同一维度的神经元)处理,所以无法利用与形状相关的信息。卷积层可以保持形状不变。当输入数据是图像时,卷积层会以3维数据的形式接收输入数据,并同样以3维数据的形式输出至下一层。因此,在CNN中,可以(有可能)正确理解图像等具有形状的数据。

7.2.2卷积运算

对于输入数据,卷积运算以一定间隔滑动滤波器的窗口并应用。这里所 说的窗口是指图7-4中灰色的3 × 3的部分。如图7-4所示,将各个位置上滤 波器的元素和输入的对应元素相乘,然后再求和(有时将这个计算称为乘积 累加运算)。然后,将这个结果保存到输出的对应位置。将这个过程在所有 位置都进行一遍,就可以得到卷积运算的输出。

7.2.3 填充

在周围填一圈数据,从而实现了调整输出的大小

7.2.4 步幅

卷积核每次走多少合适,应用滤波器的位置间隔称为步幅(stride)。

输出大小的计算

其实怎么记这个式子呢,输出的高度肯定和输入的高度成正比,和卷积核的高度成反比,和步幅成反比,和填充成正比。 】 而当除不尽的时候,根据深度学习的框架的不同,当值无法除尽时,有时会向最接近的整数四舍五入,不进行报错而继续运行。

7.2.5 3维数据的卷积运算

由于图像是3维数据,除了高、长方向之外,还需要处理通道方向,我们不得不引入多层的卷积核 对于三维的图像数据,书写顺序为(channel, height, width) 那我们可不可以输出的时候也是多通道的(多channel) 那我们要加偏置怎么做呢,我们使用广播机制 那我们能不能一下子处理多个数据呢,答案肯定的

7.3 池化层

池化是缩小高、长方向上的空间的运算。比如,如图7-14所示,进行将2 × 2的区域集约成1个元素的处理(取区域内的最大值,平均值,即Max池化之外,还有Average池化) 另外,一般来说,池化的窗口大小会和步幅设定成相同的值。比如, 3 × 3的窗口的步幅会设为3, 4 × 4的窗口的步幅会设为4等 池化层具有以下的特征

  • 对微小的位置变化具有鲁棒性(健壮
  • 没有需要学习的参数
  • 通道数不会发生变化

7.4实现方式

7.4.1 卷积层的实现方式

思路:从正常的思考角度来说,我们可以不断的取分片使用for循环来实现卷积的操作,但事实上矩阵的运算往往更有效率,所以我们选择使用im2col来将输入矩阵和卷积核展开来实现更快的操作 代码实现im2col的实现

    import sys, os
    sys.path.append(os.pardir)
    from common.util import im2col
    x1 = np.random.rand(1, 3, 7, 7)
    col1 = im2col(x1, 5, 5, stride=1, pad=0)
    print(col1.shape) # (9, 75)
    x2 = np.random.rand(10, 3, 7, 7) # 10个数据
    col2 = im2col(x2, 5, 5, stride=1, pad=0)
    print(col2.shape) # (90, 75)

代码对于

    class Convolution:
        def __init__(self, W, b, stride=1, pad=0):
            self.W = W
            self.b = b
            self.stride = stride
            self.pad = pad
        def forward(self, x):
            FN, C, FH, FW = self.W.shape
            N, C, H, W = x.shape
            out_h = int(1 + (H + 2*self.pad - FH) / self.stride)
            out_w = int(1 + (W + 2*self.pad - FW) / self.stride)
            col = im2col(x, FH, FW, self.stride, self.pad)#输入数据的展开
            col_W = self.W.reshape(FN, -1).T # 滤波器的展开
            out = np.dot(col, col_W) + self.b
            out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
            return out

7.4.2 池化层的实现方式

还是通过im2col方式来实现,与卷积层不同的是池化操作是每一层独立进行的,不会和卷积层一样进行加和操作

    class Pooling:
        def __init__(self, pool_h, pool_w, stride=1, pad=0):
            self.pool_h = pool_h
            self.pool_w = pool_w
            self.stride = stride
            self.pad = pad
        def forward(self, x):
            N, C, H, W = x.shape
            out_h = int(1 + (H - self.pool_h) / self.stride)
            out_w = int(1 + (W - self.pool_w) / self.stride)
            # 展开(1)
            col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
            col = col.reshape(-1, self.pool_h*self.pool_w)#-1表示根据你的第二维即“列”自动去计算行的个数
            # 最大值(2)
            out = np.max(col, axis=1)
            # 转换(3)
            out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
            return out

7.5 CNN的可视化

CNN究竟在学什么

其实对于cnn而言,我们观察卷积层的权重图像,可以发现其实cnn在观察边缘(颜色变化的分界线)和斑块(局部的块状区域) 为什么这样说呢 我们说某一个卷积核对某某场景可以识别,其实是说他遇到了一个可以使得他这个卷积核产生的输出很大的一个场景,:通过组合各种各样不同图案的滤波器(横的、竖的、斜的、圆的),网络就能把图像里所有关键的轮廓和细节全部“响应”出来!

层次越深的卷积核长什么呢

随着层次加深,神经元从简单的形状向“高级”信息变化,比如 可以看到卷积核的后面的识别越来越具体了,也就是愈来愈高级了

卷积核的存在其实是在之前的神经网络的基础上增加了空间维度的一些信息,然后再与正确的标签做损失函数梯度。 总而言之,卷积核负责提取特征,全连接层则负责最终的概率的输出