神经网络学习_反向传播与梯度下降(1)

前言

本节内容较多,但是大部分是简单可操作的例子,用以理解相关概念。

反向传播的简单例子

我们以一个简单的例子开头:

第一个例子

当 k0 = 1, m0 = 3时,z0 = 459。

如果想使得z1 = 450,分别只考虑 k, m,它们的值分别该如何变化?

很自然的想法就是在 z = xy = f(k,m) 研究其增减性。

只考虑 k,则:

z = xy = f(k,m0)

∂z/∂k = (∂z/∂x) * (∂x/∂k) = 3y

那么在 k0 = 1,m0 = 3附近,函数 f(k,m0) 关于 k 的导数为:

∂z/∂k|k=1, m=3 = 51

根据上式可以有如下计算:

∴ limΔk→0(Δz/Δk) = 51,即(Δk→0)Δz = 51 Δk

∵ Δk = (z1 - z0) / 51 ≈ -0.1765 = k1 - k0

∴ k1 = 0.8235, x = 3k1 + 8m0 = 26.4705, z = xy = 449.9985

emmmm,好像我选的值比较好,第一次计算就达到了比较好的精度,但是很多情况下,是无法一次达到的,因为我们使用的导数是在原来点附近的极小范围内起作用。

第二个例子

如果同时改变 k, m 呢?

  1. 不妨设 Wk = 0.5, Wm = 0.5,即二者的权重分别为0.5,0.5;
  2. 计算:∂z/∂k, ∂z/∂m;
  3. 则Δk=ΔzWk / (∂z/∂k), Δm = ΔzWm / (∂z/∂m)
  4. 更新 k, m:k = k + Δk, m = m + Δm(这里的Δ指新的值减去旧的值);
  5. 计算 z 和目标值之间的误差,若超过误差范围,则返回步骤 2。

如果函数是非线性的,同样使用偏导数计算,将误差一层一层反向传递。

反向传播与梯度下降

通俗地讲,神经网络的训练过程就是根据自己原有的方法,先预测一个值,然后拿到真实值进行比较。它一看,欸,我跟真实值的误差有这么多,然后就根据这个误差,运用梯度下降的原理,调整自己的计算方法,再进行计算。

这跟猜数字的游戏很像,你报一个数,我猜一个数,你告诉我差距是多少,然后我再对猜数的方法进行调整……

梯度下降法

基本思想

就跟下山一样,选择“视野”内最“陡”的一条路下山。

它是求解非线性规划问题的一种方法,用于求可微函数$f(X)$的最值(极值)。

我们的目标是使得通过误差函数计算出来的误差值最小。

我们的基本思想是:

  • 依照某种规则选择”更好的X(k),使得 f(X(k)) < f(X(0))
  • 得到解序列 {X(k)}:limk→+∞|X(k) - X| = 0 (X 为 f(X) 极(最)小值)

所以,根据函数在该点的负梯度方向是该点的函数值下降最快的方向,我们可以采用如下规则选择更好的 X:

X(k+1) = X(k) - λ∇f(Xk)

由于在神经网络中,调整的是各层之间的权重,因此我们的目标为:选取合适的权重,使得损失函数最小。

当神经网络的权重变成 W(k+1) 时,选取合适的步长 λ,就能够使损失函数值 f(W(k+1)) < f(W(k)) (损失函数的选取一般使得 f(W) 的非负),也就是与标签值差距更小了。

这样,我们就可以根据 W(k+1) 去反向修改权重了。

λ 如何确定

还有一个问题是,λ 该如何确定

λ决定了每次往最速下降方向走多少距离,这是个问题,不恰当的话可能会震荡:

LRLH.png

或者在两个取值间反复横跳,无法下降:

LRH.png

这一个学习率在极值点附近会产生震荡:

LRL.png

这个看起来很合适,就是需要更多的迭代:

LRP.png

其实这里的步长 λ 就是接下来的学习率,学习率并不是越高越好。

如果设定过高,数学上来讲梯度下降算法可能难以收敛,发生梯度爆炸;过小则容易被困在“更差”的局部最小值里,收敛很慢。

“...较高的学习速率... 表示系统含有太多的动能,参数向量在处于混沌状态下,不断来回反弹,无法稳定到损失函数的一个较深且较窄的最优值”cs231n

我们可以通过代码模拟一下:见文末附录

Python构造神经网络_1

经过这两篇的说明,我们可以尝试使用python自己写一个超简单的三层神经网络(输入层、隐藏层、输出层)。

笔者不会TensorFlow、PyTorch、Keras、PaddlePaddle等框架,但是基于可持续发展的理念,选择深入学习内在机理。这里分享一下自己的学习经历。

准备

Python环境:Windows, Python3.7, IDLE

依赖:numpy, scipy, matplotlib, csv,使用pip安装

说明

根据第一节内容可以知道:

  • 我们需要有三层,每层若干个神经元结点。
  • 第一层称作输入层,用于接收输入的数据;第二层称为隐藏层,用于抽象化输入数据的特征,以便更好线性分类;第三层称为输出层,输出判断结果。
  • 除输入节点外,每个结点都需要经过激活函数再输出。
  • 上一层的输出作为下一层的输入。
  • 相邻两层之间,存在权重矩阵。

代码

#!\usr\bin\env python3是为了兼容Linux;由于采用了中文注释,最好指定编码方式#-*- encoding: utf-8 -*-;最后说明代码作者。

#!\usr\bin\env python3
#-*- encoding: utf-8 -*-
__author__ = 'QCF'

import csv
import numpy as np
import scipy.special
import matplotlib.pyplot as plt

class neuralNetwork:
    # 初始化设定
    def __init__(self, InputNodes, HiddenNodes, OutputNodes, LearningRate):
        # 设定三层神经元的数量
        self.in_nodes = InputNodes
        self.hid_nodes = HiddenNodes
        self.out_nodes = OutputNodes
        # 设定学习率
        self.LR = LearningRate
        # 设定初始权重矩阵W_ih与W_ho,表示输入层到隐藏层、隐藏层到输出层的权重矩阵,其构成如下:
        # w_11 w_21
        # w_12 w_22
        # W_ij表示上一层的第i结点到下一层的第就j结点的权重值。
        # 初始权重服从于均值为0,方差为1/sqrt(num_hid_nodes)的正态分布
        self.W_ih = np.random.normal(0.0, pow(self.hid_nodes, -0.5), (self.hid_nodes, self.in_nodes))
        self.W_ho = np.random.normal(0.0, pow(self.out_nodes, -0.5), (self.out_nodes, self.hid_nodes))
        # 使用Sigmoid函数作为激活函数
        self.activate_func = lambda x: scipy.special.expit(x)
        self.inv_activate_func = lambda x: scipy.special.logit(x)

小小的总结

至此,我们大致了解了一下神经网络的基本原理以及梯度下降法。限于篇幅,损失函数以及如何计算反向传播误差将在下篇说明,并且将附上矩阵运算的说明。

附录

梯度下降模拟代码

以下的代码测试了 0.1~1.0的学习率,在初始点为 x = -1.8 的情况下,我们是如何一步一步使用梯度下降逼近目标函数的极小值点。目标函数的形式无任何要求,“+ 0.1”是为了使曲线不与坐标轴靠太近,影响观看效果。

#!usr/bin/env python3
#-*- encoding: utf-8 -*-
__author__ = "QCF"

import numpy as np
import matplotlib.pyplot as plt

# 目标函数
def Fun(x):
    y = pow(x, 2) + 0.1
    return y

# 目标函数的导数
def dFun(x):
    y = 2*x
    return y

# 模拟不同学习率LR下的梯度下降,迭代10次
def Generate_d(LR):
    # 初始点x,P储存每次得到的新点坐标
    x = -1.8
    P = [[0,0] for i in range(10)]
    for i in range(10):
        P[i][0] = x
        P[i][1] = Fun(x)
        dx = dFun(x)
        x = x - LR * dx
    return P

# 绘出函数
def Draw_Fun(LR):
    x = np.linspace(-2, 2, num = 400)
    y = Fun(x)
    plt.plot(x, y, '-')
    P = Generate_d(LR)
    for i in range(10):
        plt.plot(P[i][0], P[i][1], 'x')
    for i in range(9):
        plt.plot([P[i][0], P[i+1][0]], [P[i][1], P[i+1][1]], '-')
    plt.title("LR = {:.2f}".format(LR))
    plt.show()

if __name__ == "__main__":
    LR = [1.0, 0.8, 0.6, 0.4, 0.2, 0.1]
    for r in LR:
        Draw_Fun(r)

说些什么好呢?笔者很多时候写代码总是感觉在重复造轮子,然而这又是学习的一部分。。。还是算法好啊 ~(大雾)~,总是可以体会到思想的光辉 (/▽\)

     

2 Comments

  1. 前排姿瓷!!

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

购物车
There are no products in the cart!
Subtotal
¥0.00
Total
¥0.00
Continue Shopping
0