1、反向传播算法

反向传播(Backward Propagation 或 Back Propagation,BP算法)指的是计算神经网络参数梯度

的方法。简言之,该方法根据微积分中的链式法则,按相反的顺序从输出层到输入层遍历网络。该

算法存储了计算某些参数梯度时所需的任何中间变量。

反向传播目前来说就是一个更高效计算grad的方式,并不是一个更新参数的方法

1.1、链式法则

反向传播将局部导数向反方向传递,传递的原理基于链式法则。反向传播时将信号乘以节点的局部

导数然后传递给下一个节点。

1.2、加法和乘法节点

2、反向传播——激活层

这里理清一下思路,我们这里定义的反向传播的函数,里面有一个dy需要输入,其实这个时候我们

只是包装了一个函数而已,我们并没有在管这个dy到底是多少,这个需要用到的时候你自己再传进

来,然后输入dx其实就是当前这一层的导数,如果下面还要再继续往前传递的话,这个dx也有可能

成为前一层的dy也就是输入,下面的公式很重要,为什么是上一层乘下一层就是这个原理。

2.1、Relu

class Relu:
    def __init__(self):
        self.mask = None
    def forward(self, x):
        self.mask = ( x<=0 )
        y = x.copy()
        y[self.mask] = 0
        return y
    # 反向传播输入后面层传来的局部导数dy,输出dx
    def backward(self, dy):
        dx = dy.copy()
        dx[self.mask] = 0
        return dx
2.2、Sigmoid

from common.functions import *
class Sigmoid:
    def __init__(self):
        self.y = None
    def forward(self, x):
        y = sigmoid(x)
        self.y = y
        return y
    def backward(self, dy):
        dx = dy * self.y * (1.0 - self.y)
        return dx
2.3、Affine仿射变换

这里具体是左乘还是右乘就是看形状

这个代码还没有实现梯度下降的功能,暂时只是求梯度,然后返回一个可以往前传递的导数值而

已,这个return的dx相当于上一层的dy

class Affine:
    def __init__(self,W,b):
        self.W = W
        self.b = b
        self.X = None
        self.original_x_shape = None
        self.dw = None
        self.db = None
    def forward(self, X):
        self.original_x_shape = X.shape
        self.X = X.reshape(X.shape[0], -1)
        Y = self.X @ self.W + self.b
        return Y
    def backward(self, dY):
        dx = dY @ self.W.T
        self.dw = self.X.T @ dY
        self.db = np.sum(dY, axis=0)
        dx = dx.reshape(self.original_x_shape)
        return dx

3、反向传播——输出层

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None
        self.y = None
        self.t = None
    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)
        return self.loss
    def backward(self, dy=1):
        n = self.t.shape[0]
        if self.t.size == self.y.size:
            dx = self.y - self.t
        else:
            dx = self.y.copy()
            dx[np.arange(n),self.t] -= 1
        return dx * dy / n

4、改进之后的案例

gradient下面的这一步是先做一遍前向传播,保存反向传播要用到的数值

self.loss(x,t)
import numpy as np
from common.functions import  *
from common.gradient import numerical_gradient
from common.layers import *
from collections import OrderedDict

class TwoLayerNet():
    def __init__(self, input_size, hidden_size, output_size,weight_init_std=0.01):
        self.params = {}
        self.params['W1'] = np.random.randn(input_size, hidden_size) * weight_init_std
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = np.random.randn(hidden_size, output_size) * weight_init_std
        self.params['b2'] = np.zeros(output_size)

        self.layers = OrderedDict()
        self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
        self.layers['Relu1'] = Relu()
        self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])
        self.lastLayer = SoftmaxWithLoss()


    def forward(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        return x
    def loss(self,x,t):
        y = self.forward(x)
        return self.lastLayer.forward(y,t)
    def accuracy(self,x,t):
        y = self.forward(x)
        y_pred = np.argmax(y, axis=1)
        acc = np.sum(y_pred == t) / x.shape[0]
        return acc
    def numerical_gradient(self, x, t):
        loss_f = lambda _ : self.loss(x,t)
        grads = {}
        grads['W1'] = numerical_gradient(loss_f,self.params['W1'])
        grads['b1'] = numerical_gradient(loss_f,self.params['b1'])
        grads['W2'] = numerical_gradient(loss_f,self.params['W2'])
        grads['b2'] = numerical_gradient(loss_f,self.params['b2'])
        return grads
    def gradient(self, x, t):
        self.loss(x,t)
        layers = list(self.layers.values())
        layers.reverse()
        dy = self.lastLayer.backward()
        for layer in layers:
            dy = layer.forward(dy)
        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

5、深度学习学习技巧之梯度消失和梯度爆炸

梯度消失:在反向传播的时候,一开始梯度比较有效,越往前传播,梯度越来越小直至趋向于0,

这样意味着参数更新太慢,这种现象被称为“梯度消失”。

与之对应,如果我们进行一些特殊的调整(比如初始权重很大),可以让梯度反向传播时不会明显

减小,从而解决梯度消失的问题;然而这样一来,前面层的梯度又会变得非常大,引起网络不定,

无法再从训练数据中学习,这种现象被称为“梯度爆炸”。

怎么做:

改变神经网络结构(换激活函数,优化损失函数,ResNet,LSTM),增加限制(正则化,参数初始化,梯度裁剪设置阈值),优化学习算法(更新参数方法)

6、更新参数方法的优化

6.1、SGD的缺点

局部最优解:陷入局部最优,尤其在非凸函数中,难以找到全局最优解。

鞍点:陷入鞍点,梯度为 0,导致训练停滞(x ** 3的0点)。

收敛速度慢:高维或非凸函数中,收敛速度较慢。

学习率选择:学习率过大导致震荡或不收敛,过小则收敛速度慢。

#SGD
class SGD:
    def __init__(self,lr=0.01):
        self.lr = lr
    def update(self,params,grads):
        for key in params.keys():
            params[key] -= self.lr * grads[key]
6.2、动量法Momentum

动量法有时能够减缓优化过程中的震荡,加快优化的速度。因为其会累计历史梯度,也可以有效避

免鞍点问题。

import numpy as np
#Momentum
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):
        # 首先判断是否是第一次迭代,如果是,则对v做全0初始化
        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]
6.3、学习率衰减

学习率衰减是一种平衡策略,初期使用较大学习率快速接近最优解,后期逐渐减小学习率,使参数

更稳定地收敛到最优解。

6.3.1、等间隔衰减

每隔固定的训练周期(epoch),学习率按一定的比例下降,也称为“步长衰减”。例如,使学习率

每隔20个epoch衰减为之前的0.7:

6.3.2、指定间隔衰减

在指定的epoch,让学习率按照一定的系数衰减。例如,使学习率在epoch达到[10,50,200]时衰

减为之前的0.7:

6.3.3、指数衰减

学习率按照指数函数 f(𝑥) = ax , 𝑎 < 1 进行衰减。例如,使学习率以 0.99 为底数,epoch为指数衰

减:

6.4、AdaGrad自适应梯度

Momentum是利用历史梯度直接影响梯度更新,AdaGrad是利用历史梯度影响学习率,从而影响梯

度更新。

#AdaGrad
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-8)
6.5、RMSProp均方根传播

#RMSProp
class RMSProp:
    def __init__(self,lr=0.01,decay=0.9):
        self.lr = lr
        self.decay = decay
        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] = self.decay * self.h[key] + (1 - self.decay) * grads[key] * grads[key]
            # 也可以分两步走
            self.h[key] *= self.decay
            self.h[key] += (1 - self.decay) * grads[key] * grads[key]
            params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-8)
6.6、Adam自适应矩估计

Adam(Adaptive Moment Estimation,自适应矩估计)在工程应用中用的最多,融合了

Momentum 和 AdaGrad 的方法。

v是一阶矩,h是二阶矩。

阿尔法1 = 0.9、阿尔法2 = 0.999

class Adam:
    def __init__(self,lr=0.01,beta1=0.9,beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2
        self.t = 0
        self.v = None
        self.h = None
    def update(self,params,grads):
        if self.v is None:
            self.v ,self.h = {},{}
            for key,val in params.items():
                self.v[key] = np.zeros_like(val)
                self.h[key] = np.zeros_like(val)
        self.t += 1
        # 将t对公式的修正影响,直接添加到学习率上
        lr_t = self.lr * np.sqrt(1 - self.beta2 ** self.t) / (1 - self.beta1 ** self.t)
        for key in params.keys():
            # self.v[key] = self.beta1 * (self.v[key]) + (1 - self.beta1) * grads[key]
            # self.h[key] = self.beta2 * (self.h[key]) + (1 - self.beta2) * (grads[key] ** 2)
            # 化简版本
            self.v[key] += (1 - self.beta1) * (grads[key] - self.v[key])
            self.h[key] += (1 - self.beta2) * (grads[key] ** 2 - self.h[key])
            params[key] -= lr_t * self.v[key] / (np.sqrt(self.h[key]) + 1e-8)
6.7、各种方法对比——代码实现
import numpy as np
import matplotlib.pyplot as plt
from common.optimizer import *
from collections import OrderedDict # 使用有序字典保存不同的优化器

def f(x,y):
    return x ** 2 / 20 + y ** 2

def f_grad(x,y):
    return x / 10, 2 * y

init_pos = (-7.0,2.0)
params,grads = {},{}

optimizers = OrderedDict()
optimizers['SGD'] = SGD(lr = 0.9)
optimizers['Momentum'] = Momentum(lr = 0.11,momentum = 0.85)
optimizers['AdaGrad'] = AdaGrad(lr = 1.5)
optimizers['Adam'] = Adam(lr = 0.5,beta1 = 0.5, beta2 = 0.999)

# 定义子图序号
idx = 1

for key in optimizers: # 遍历字典是默认遍历key,所以这里写什么单词都可以,只要上下文能对应就可以
    optimizer = optimizers[key]
    params['x'],params['y'] = init_pos[0], init_pos[1]
    # 用列表保存参数变化轨迹
    x_history,y_history = [],[]

    for epoch in range(30):
        x_history.append(params['x'])
        y_history.append(params['y'])
        grads['x'],grads['y'] = f_grad(params['x'],params['y'])
        optimizer.update(params,grads)

    # 确定子图
    plt.subplot(2,2,idx)
    idx += 1
    # 绘制等高线
    x = np.arange(-10,10,0.01)
    y = np.arange(-5,5,0.01)
    X,Y = np.meshgrid(x,y)
    Z = f(X,Y)
    Z[Z > 7] = 0
    plt.contour(X,Y,Z)
    # 单独画出最低点
    plt.plot(0,0,'+')
    plt.plot(x_history,y_history,'o-',color='red',markersize=2,label = key)
    plt.legend(loc='best')
plt.show()

这里超参数要调节,是一个交叉验证的过程,beta1是往前冲一下的向量,是继承momentum的

7、参数初始化

我们选择哪个激活函数以及如何初始化参数,可以决定优化算法收敛的速度有多快;糟糕选择可能

会导致我们在训练时遇到梯度爆炸或梯度消失。

7.1、常数初始化

注意:将权重初始值设为 0 将无法正确进行学习。严格地说,不能将权重初始值设成一样的值。因

为这意味着反向传播时权重全部都会进行相同的更新,被更新为相同的值(对称的值)。这使得神

经网络拥有许多不同的权重的意义丧失了。为了防止“权重均一化”(瓦解权重的对称结构),必须

随机生成初始值。

7.2、秩初始化

7.3、正态分布初始化

权重参数按指定均值μ与标准差 σ 正态分布初始化。因为不能直接将权重初始化为相同的常数,所

以需要对参数进行随机初始化。最常见的随机分布就是正态分布(也叫高斯分布),记作X ~ N(μ,

σ2)。

7.4、均匀分布初始化

权重参数在指定区间内均匀分布初始化。均匀分布一般记作X ~ U(a, b)。

7.5、Xavier初始化(Glorot初始化)

7.6、He初始化(Kaiming初始化)

8、正则化

机器学习的问题中,过拟合 是一个很常见的问题。

过拟合指的是能较好拟合训练数据,但不能很好地拟合不包含在训练数据中的其他数据。机器学习

的目标是提高泛化能力,希望即便是不包含在训练数据里的未观测数据,模型也可以进行正确的预

测。因此可以通过 正则化 方法来抑制过拟合。

常用的正则化方法有 Batch Normalization、权值衰减、Dropout、早停法等。

8.1、Batch Normalization批量标准化

Batch Normalization 最重要的目的,其实是调整各层的激活值分布使其拥有适当的广度,BN 层通

常放在线性层(全连接层/卷积层)之后,激活函数之前。它有着以下优点:

可以使学习快速进行(允许更高的学习率)
不那么依赖初始值(对于初始值不用那么神经质)
抑制过拟合(降低 Dropout 等的必要性)

8.2、权值衰减

通过在学习的过程中对大的权重进行“惩罚”,可以有效地抑制过拟合,这种方法被称为权值衰减。

因为很多过拟合产生的原因,就是权重参数取值过大。

8.3、Dropout随机失活

训练时拟定一个超参数p,这个p是一个概率,以这个概率随机关闭一部分神经元,让我们的神经网

络节点变少,同时未被关闭的神经元的输出值以 1/(1 - p) 的比例进行缩放,以保持期望值不变;而

测试时通常不使用Dropout,即所有神经元保持激活状态并且不进行缩放。

Dropout 在全连接层和卷积层均适用,尤其对大规模网络效果显著。Dropout 通常放在激活函数之

后,线性层(全连接层/卷积层)之前,也就是说:Dropout 在“激活之后、下一层之前”

更多推荐