5 误差反向传播法

我们已经知道了如何通过数值微分的方式来计算相关参数的梯度,但是实际上而言,计算比较消耗时间。实际上,我们利用微积分的链式法则知识,可以更为快速地计算梯度。

5.1 计算图

由于使用链式法则,为了更好地运算,我们可以将一个数学表达式拆分为多个部分的组合并以图的形式表示出来。每个结点代表一个操作,边权对应输入结点的值。
例如:q= x + y, f = qz可以用下图表示:
在这里插入图片描述
计算结果只需从左至右依次计算即可,通常也称为“正向传播”。

5.2 反向传播

有了正向传播的概念,反向传播自然对应着从右至左的计算,而反向传播计算的值是梯度值。
对于一个结点来说,只需要根据自身的输入,计算自身的输出即可,称为局部计算。对梯度而言,计算下游梯度只需将上游输入的梯度乘以局部计算的梯度即可。
以上图为例,可以知道 df/df = 1。
对于乘法结点,输入结点的梯度值为1,又df/dq = z, df/dz = q.由链式法则,df / dq = df/df * df/dq = z = -4.
df / dz = df/df * df/dz = q = 3.
对于加法结点, 输入节点的梯度值为-4, 又dq/dx = dq/dy = 1.由链式法则,df / dx = df/dq * dq/dx = -4.同理df/dy = -4.
因而得到了各参数的梯度值。通过计算图,我们可以将复杂的数学表达式拆分成若干加减乘除运算,然后简单地应用链式法则得到各个梯度,即完成“反向传播”。
编写为代码为:

x = -2; y = 5; z = -4

# perform the forward pass
q = x + y # q becomes 3
f = q * z # f becomes -12

# perform the backward pass (backpropagation) in reverse order:
# first backprop through f = q * z
dfdz = q # df/dz = q, so gradient on z becomes 3
dfdq = z # df/dq = z, so gradient on q becomes -4
# now backprop through q = x + y
dfdx = 1.0 * dfdq # dq/dx = 1. And the multiplication here is the chain rule!
dfdy = 1.0 * dfdq # dq/dy = 1

6 卷积神经网络

知道了正向传播与反向转播的相关知识,也有了SGD优化方法,我们只需要再了解卷积神经网络的一些结构,便可以实现一个卷积神经网络并投入训练了。

6.1 全连接层(Full Connected)

顾名思义,全连接层的所有神经元和前一层的所有神经元都有连接。图像化为
在这里插入图片描述
具体实现时,只需将权重与上一层的输出矩阵相乘,加上偏置即可。
但是全连接层存在一定的局限性:忽略了数据的形状。以输入为图像为例,图像通常有高,长,通道三维形状,当应用全连接层时,必须将三维数据“压扁”为一维数据。这就使得原本包含的空间信息因“压扁”操作而被破坏,使得一些重要的特征无法得到利用。
因而需要一种操作使得这种空间信息能够得到保留和利用。

6.2 卷积层(Convolution)

卷积层可以保持数据的形状不发生改变而处理图像,因而可以利用输入数据的空间特征。
卷积层进行的处理相当于图像处理中的“滤波器运算”,拥有一个固定形状的滤波器,通过不断“滑动”来不断进行卷积操作。而对于一次卷积,将各个位置上的滤波器的元素和输入的对应元素相乘再求和,然后将结果保存到输出的对应位置即完成了一次操作。图示如下:
在这里插入图片描述
滤波器中的参数对应于权重,自然也可以加入偏置项。
通过上述图片发现,经过卷积层后,数据的形状缩小了,而有时我们需要保证卷积后的数据能与卷积前保持一致,由此我们需要对原数据进行”填充(padding)”,通常在四周填入多余的0来保证卷积后的形状不发生改变。
滤波器的滑动自然需要考虑到“步长(stride)”的问题,步长是一个超参数。
综合上述考虑,可以得到关于卷积层输出图像尺寸的公式:(假设输入大小为(H,W),滤波器大小为(FH, FW),输出大小为(OH, OW),填充为P,步长为S,则
OH = (H + 2P - FH) / S + 1
OW = (W + 2P - FW) / S + 1

6.3 池化层(Pool)

池化是缩小高,长方向上的空间的运算,将一个区域里的多个值集约成1个值,缩小空间的大小。典型池化有两种:
最大值池化(Max Pool):选取区域内的最大值作为“代表元素”
均值池化(Average Pool):选取区域元素的均值作为“代表元素”
在这里插入图片描述
如图为核大小为2×2,步长为2的最大值池化操作。
池化层有三个特点:

  1. 没有需要学习的参数
  2. 通道数不发生变化,即对输入数据的各个通道分别池化
  3. 对于微小的位置变化具有鲁棒性,即输入数据发生微小的变化池化层结果也不会发生较大变化。

6.4 CNN

实现了卷积层和池化层后,便可以通过组合来搭建卷积神经网络。
在这里插入图片描述
如图为AlexNet的基本结构图。可以看出其有多个卷积层和池化层,最后由全连接层输出结果。通过各个层的叠加,我们就可以实现一个简单的卷积神经网络。

7 优化技巧

上面我们已经知道了如何搭建一个简单的卷积神经网络,对于一些较为简单的数据集可能已经有很好的结果,但是我们还可以进一步进行优化来提升网络的性能,一些常用的优化技巧是非常重要且有效的。

7.1 参数的更新

7.1.1 SGD(随机梯度下降):

上一篇博客已经介绍过SGD。在进行参数更新时,只需将梯度的负值乘上学习率后加到参数上即可完成一次更新。SGD较为简单,容易实现,但对于某些问题效率不高,容易局限于局部最优点而难以达到全局最优点。
在这里插入图片描述

7.1.2 Momentum

Momentum可以理解为带“动量”的梯度下降方法,通过引进“动量”这一物理概念来改进梯度下降方法,使得损失函数不那么容易停留于局部最优点。用数学式表达为:
在这里插入图片描述
其中W表示要更新的权重,v对应于物理上的“速度”,αv的存在使得W存在一定的“动量”,从而不能够突变,减缓了梯度下降时的反复横跳,同时也使得损失函数不易于“卡”在局部最低点。Python实现为:

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]

7.1.3 AdaGrad

在上述两种更新策略中,学习率是一个不变的常数,但实际上,我们可以随着学习的进行而使学习率逐步衰减,这也与实际生活中的感受相切合。
而AdaGrad实现了“因材施教”的想法,针对“一个一个”的值,赋予其“定制”的值。即AdaGrad会为参数的每个元素适当地调整学习率,与此同时进行学习。数学表达式为:
在这里插入图片描述
新变量h保存了之前所有梯度值的平方和。在更新参数时,乘上根号h分之一,就可以调节学习的尺度,随着学习的进行,h不断增大,从而实际的学习率不断减小,且变动大的元素对应的学习率更小。
Python实现如下:

class AdaGrad:

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

细节:最后一排加上了一个极小值1e-7是为了防止分母为0的情况出现。

7.1.4 Adam

Adam的基本思路就是结合Momentum和AdaGrad两种方法的思想。通过组合这两种方法的优点来高效地实现参数空间的搜索。Python实现为:

class Adam:

    """Adam (http://arxiv.org/abs/1412.6980v8)"""

    def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.iter = 0
        self.m = None
        self.v = None
        
    def update(self, params, grads):
        if self.m is None:
            self.m, self.v = {}, {}
            for key, val in params.items():
                self.m[key] = np.zeros_like(val)
                self.v[key] = np.zeros_like(val)
        
        self.iter += 1
        lr_t  = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)         
        
        for key in params.keys():
            self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
            self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])   
            params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
     

7.2 权重的初始值

在神经网络的学习过程中,权重的初始值十分重要,影响到网络是否能够学习成果,因而需要一定的技巧来初始化权重值。

7.2.1 权重不能设置为相同的值

假设对于一个2层神经网络,权重初始为相同的值,那么在正向传播中,由于输入层的权重全部相同,那么第2层的神经元全部被传递相同的值,这也意味着反向传播时第二层的权重全部进行相同的更新。因此权重被更新为相同的值,这使得神经网络拥有许多不同的权重的意义丧失了,因而在初始化权值时,必须要随机生成初始值。

7.2.2 各层激活值需要具有适当的广度

为了能够得到良好的训练效果,就需要让各层的激活值的分布具有一定的广度。
以激活函数为sigmoid函数,权重初始值使用均值为0的高斯分布为例,
倘若权重广度过大,则会导致激活值呈偏向0和1的分布,随着层级加深,输出不断地向0和1靠近,导数值将会越来越小最后消失,即“梯度消失”。
若权重广度过小,则会使得激活值集中于0.5附近,导致了“表现力受限”问题。

7.2.3 Xavier初始值

为了使各层激活值具有适当的广度,Xavier推导出合适的权重尺度:若前一层的节点数为n,则初始值使用标准差为1/√n的分布。

7.2.4 He初始值

当激活函数为ReLU函数值时,更推荐使用He初始值,即:当前一层节点数位n,初始值使用标准差为√(2/n)的分布。可以理解为ReLU函数负值区域值为0,因此需要2倍系数来保持广度。

7.3 Batch Normalization

上面我们通过赋予合适的权重初始值来保证激活值的合适广度,而我们也可以“强制”调整激活值来使其具有合适的广度,而Batch Normalization基于此想法产生。
顾名思义,Batch Normalization以进行学习时的batch为单位进行正则归一化,数学式表达为:
在这里插入图片描述
(其实很像利用概率论的中心极限定理来正则化数据分布)
然后会对正规化后的数据进行伸缩与平移的变换,数学式表示为:
在这里插入图片描述
其中的γ与β是可以学习的值,分别初始为1和0。即完成了Batch Normalization。
Batch Normalization主要有三个优点:

  1. 可以增大学习率,加快学习进程
  2. 不必对权重的初始值过分依赖
  3. 抑制了过拟合

7.4 Dropout

为了抑制过拟合,增强训练模型的泛用度,通常会使用Dropout方法。
在训练时,按一定的比例随机地选取隐藏层的神经元,将其删除。在测试时,传递所有的神经元信号,但每个输出需要乘上训练时的删除比例后再输出。
在这里插入图片描述

Python实现为:

class Dropout:
    """
    http://arxiv.org/abs/1207.0580
    """
    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
        else:
            return x * (1.0 - self.dropout_ratio)

    def backward(self, dout):
        return dout * self.mask

不过,若在训练时未被删除的输出除以删除比例,那么在测试时也就不需要再乘上删除比例,这种处理在实际中也运用广泛,称为“inverted dropout”。

参考资料:
《深度学习入门——基于Python的理论与实现》
斯坦福大学课程CS231n:https://zhuanlan.zhihu.com/p/21930884

Logo

更多推荐