1. 说明

现在使用深度学习算法都以调库为主,但在使用库之前,先用python写一个最基本的神经网络的程序,也非常必要,它让我们对一些关键参数:学习率,批尺寸,激活函数,代价函数的功能和用法有一个直观的了解。

2. 原理

1) BP神经网络


BP神经网络是一种按照误差逆向传播算法训练的多层前馈神经网络.这又前馈又逆向的把人绕晕了.先看看什么是前馈神经网络,回顾一下《深度学习_简介》中的图示:

图片.png
图片.png

这是一个典型的前馈神经网络,数据按箭头方向数据从输入层经过隐藏层流入输出层,因此叫做前馈.前馈网络在模型的输出和模型之间没有反馈,如果也包含反馈,则是循环神经网络,将在后续的RNN部分介绍.
前向网络和循环网络的用途不同,举个例子,比如做玩具狗,前馈是用不同材料和规格训练N次,各次训练之间都没什么关系,只是随着训练,工人越来越熟练.而循环网络中,要求每次做出来的狗都是前次的加强版,因此前次的结果也作为一种输入参与到本次的训练之中.可把循环网络理解成前馈网络的升级版.本篇讲到的BP神经网络,以及处理图像常用的卷积神经网络都是前馈网络,而处理自然语言常用的RNN则是循环网络.
误差逆向传播是指通过工人做出的狗(预测结果)与玩具狗规格(实际结果)的误差来调整各个工人的操作(权重w),这个例子具体见前篇《简介》,¬由于误差的传播的顺序是:输出层->隐藏层2->隐藏层1,所以叫逆向传播.
综上,前馈指的是数据流向,逆向指的是误差流向.

2) 训练过程

简单回忆一下(详见《简介》篇)训练过程:对于每个训练样本,BP算法先将输入样例提供给给输入神经元,然后逐层将信号向前传播,直到产生输出层的结果,然后对照实际结果计算输出层的误差,再将误差逆向传播到隐层神经元,然后根据神经元的误差来对连接权值和与偏置进行调整优化。向前传数据很简单,只包含加法乘法和激活函数(具体计算见代码),相对的难点在于逆向传误差,当得到了输出层的误差后,调整w3中各个w的具体方法是什么呢?这里用到了梯度下降算法.此处也是代码中的最理解的部分.
下面先看一下代码,梯度下降算法见之后的"关键概念"部分.

3. 代码分析

1) 程序说明

程序实现了通过MNIST数据集中60000个实例训练对手写数字的识别,使用一个输入层,一个隐藏层,一个输出层的方式构建BP神经网络.
因代码较长,把它分成两块:算法实现和处部调用(运行程序时把它们粘在一起即可)。注释有点多哈:p

2) 算法实现

# -*- coding: utf-8 -*-

import numpy as np
import random
import os, struct
from array import array as pyarray
from numpy import append, array, int8, uint8, zeros
from keras.datasets import mnist
 
class NeuralNet(object):
    # 初始化神经网络,sizes包含了神经网络的层数和每层神经元个数
    def __init__(self, sizes):
        self.sizes_ = sizes
        self.num_layers_ = len(sizes)  # 三层:输入层,一个隐藏层(8个节点), 输出层
        # zip 函数同时遍历两个等长数组的方法
        self.w_ = [np.random.randn(y, x) for x, y in zip(sizes[:-1], sizes[1:])]  # w_、b_初始化为随机数
        self.b_ = [np.random.randn(y, 1) for y in sizes[1:]]
        # w_是二维数组,w_[0].shape=(8,784), w_[1].shape=(10, 8),权值, 供矩阵乘
        # b_是二维数组,b_[0].shape=(8, 1), b_[1].shape=(10, 1),偏移, 每层间转换的偏移
 
    # Sigmoid函数,激活函数的一种, 把正负无穷间的值映射到0-1之间
    def sigmoid(self, z):
        return 1.0/(1.0+np.exp(-z))

    # Sigmoid函数的导函数, 不同激活函数导函数不同
    def sigmoid_prime(self, z):
        return self.sigmoid(z)*(1-self.sigmoid(z))
 
    # 向前传播:已知input,根据w,b算output,用于预测
    def feedforward(self, x):
        for b, w in zip(self.b_, self.w_):
            x = self.sigmoid(np.dot(w, x)+b)
        return x # 此处的x是0-9每个数字的可能性
 
    # 单次训练函数,x是本次训练的输入,y是本次训练的实际输出
    # 返回的是需调整的w,b值
    def backprop(self, x, y):
        # 存放待调整的w,b值,nabla是微分算符
        nabla_b = [np.zeros(b.shape) for b in self.b_] # 与b_大小一样,初值为0
        nabla_w = [np.zeros(w.shape) for w in self.w_] # 与w_大小一样,初值为0
 
        activation = x # 存放层的具体值, 供下层计算 
        activations = [x] # 存储每层激活函数之后的值
        zs = [] # 存放每层激活函数之前的值
        for b, w in zip(self.b_, self.w_):
            z = np.dot(w, activation)+b # dot是矩阵乘法, w是权值,b是偏移
            zs.append(z)
            activation = self.sigmoid(z) # 激活函数
            activations.append(activation)
 
        # 计算输出层的误差,cost_derivative为代价函数的导数
        delta = self.cost_derivative(activations[-1], y) * \
            self.sigmoid_prime(zs[-1]) #原理见梯度下降部分
        nabla_b[-1] = delta 
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())
 
        # 计算隐藏层的误差
        for l in range(2, self.num_layers_):
            z = zs[-l]
            sp = self.sigmoid_prime(z)
            delta = np.dot(self.w_[-l+1].transpose(), delta) * sp
            nabla_b[-l] = delta
            nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
        return (nabla_b, nabla_w)
 
    # 对每批中的len(mini_batch)个实例,按学习率eta调整一次w,b
    def update_mini_batch(self, mini_batch, eta):
        # 累计调整值
        nabla_b = [np.zeros(b.shape) for b in self.b_] # 与b_大小一样,值为0
        nabla_w = [np.zeros(w.shape) for w in self.w_] # 与w_大小一样,值为0
        for x, y in mini_batch: # 100个值,分别训练
            delta_nabla_b, delta_nabla_w = self.backprop(x, y)
            nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
            nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
        # eta是预设的学习率(learning rate),用来调节学习的速度. eta越大,调整越大
        # 用新计算出的nable_w调整旧的w_, b_同理
        self.w_ = [w-(eta/len(mini_batch))*nw for w, nw in zip(self.w_, nabla_w)]
        self.b_ = [b-(eta/len(mini_batch))*nb for b, nb in zip(self.b_, nabla_b)]
 
    # 训练的接口函数
    # training_data是训练数据(x, y);epochs是训练次数;
    # mini_batch_size是每次训练样本数; eta是学习率learning rate
    def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None):
        if test_data:
            n_test = len(test_data)
 
        n = len(training_data)
        for j in range(epochs): # 用同样数据,训练多次
            random.shuffle(training_data) # 打乱顺序
            mini_batches = [training_data[k:k+mini_batch_size] for k in range(0, n, mini_batch_size)]
            # 把所有训练数据60000个分成每100个/组(mini_batch_size=100)
            for mini_batch in mini_batches:
                self.update_mini_batch(mini_batch, eta) # 分批训练
            if test_data:
                print("Epoch {0}: {1} / {2}".format(j, self.evaluate(test_data), n_test))
            else:
                print("Epoch {0} complete".format(j))
 
    # 计算预测的正确率
    def evaluate(self, test_data):
        # argmax(f(x))是使得 f(x)取得最大值所对应的变量x
        test_results = [(np.argmax(self.feedforward(x)), y) for (x, y) in test_data]
        return sum(int(x == y) for (x, y) in test_results)
 
    # 代价函数的导数, 对比实际输出与模拟输出的差异, 此时y也是个数组
    def cost_derivative(self, output_activations, y):
        return (output_activations-y)
 
    # 预测
    def predict(self, data):
        value = self.feedforward(data)
        return value.tolist().index(max(value))

3) 外部调用

外部调用主要实现了主函数,load数据,以及调用神经网络的接口

# 将输入数据转换为神经网络能处理的格式
def load_samples(image, label, dataset="training_data"):
    X = [np.reshape(x,(28*28, 1)) for x in image] # 手写图分辨率28x28
    X = [x/255.0 for x in X]   # 灰度值范围(0-255),转换为(0-1)
 
    # 把y从一个值转成一个数组,对应输出层0-9每个数字出现的概率
    # 5 -> [0,0,0,0,0,1.0,0,0,0];  1 -> [0,1.0,0,0,0,0,0,0,0]
    def vectorized_Y(y):  
        e = np.zeros((10, 1))
        e[y] = 1.0
        return e
 
    if dataset == "training_data":
        Y = [vectorized_Y(y) for y in label]
        pair = list(zip(X, Y))
        return pair
    elif dataset == 'testing_data':
        pair = list(zip(X, label))
        return pair
    else:
        print('Something wrong')
 
 
if __name__ == '__main__':
    INPUT = 28*28 # 每张图像28x28个像素
    OUTPUT = 10 # 0-9十个分类
    net = NeuralNet([INPUT, 8, OUTPUT])

    # 从mnist提供的库中装载数据
    (x_train, y_train), (x_test, y_test) = mnist.load_data()
    # 格式转换
    test_set = load_samples(x_test, y_test, dataset='testing_data')
    train_set = load_samples(x_train, y_train, dataset='training_data')

    #训练
    net.SGD(train_set, 13, 100, 3.0, test_data=test_set)
 
    #计算准确率
    correct = 0;
    for test_feature in test_set:  
        if net.predict(test_feature[0]) == test_feature[1]:  
            correct += 1  
    print("percent: ", correct/len(test_set))

4. 关键概念

1) 误差函数

误差函数也叫代价函数或损失函数,它计算的是实际结果和预测结果之间的差异,误差函数记作L(Y, f(x)).
上例中代价函数用的是方差再取二分之一的方法(SSE):(1/2)*(o-y)^2,它的导数是o-y,即output_activations-y,其中output_activations为预测结果,y为实际结果。上面没有直接写误差函数,而是给出了它的导数(cost_derivative).
误差函数还有均方误差,绝对值均差等,具体请见参考中的《目标函数objectives》。

2) 梯度下降

回顾一下导数的概念,假设我们有一个函数 y = f (x),这个函数的导数记为f’(x) ,它描述了如何改变x,能在输出获得相应的变化:
f (x +ε) ≈ f (x) +εf’(x)
此时我们希望f()向小的方向变化(等号左侧),则需要对f(x)加一个负数,即ε
f’(x)<=0,那么ε与f’(x)符号不同。换言之,可以将 x 往导数的反方向移动一小步ε来减小 f (x)。这种技术被称为梯度下降.可通过下图,获得更直观的理解.

(图片引自《深度学习》"花书")
(图片引自《深度学习》"花书")

梯度下降算法在上例的backprop()部分实现,我们想知道如何改变权重w,能使误差函数L变小,于是求L对于w的导数,然后将w向导数的反方向移动一小步,即可使L变小.
损失函数的计算由下式得出:
L = (1/2)*(g(wx+b) – y)^2,其中y是实际结果,g()是激活函数,wx+b是对上一步x的线性变换,对L求导用到了复合函数的链试法则,因此有程序中分别使用了激活函数的导数(sigmoid_prime),损失函数的导数(cost_derivative),再乘以上一步的x(activations).以上就是求权重w变化的原理,偏置b同理.
另外,需要注意的是这里求出的nable_w是权重的梯度,并不是具体的权重值.

3) 批尺寸

批尺寸是每训练多少个数据调整一次权重.上例中由mini_batch指定为每次100个实例;如果每训练一次就调整一次,不但会增加运算量,还会使w变化波动加俱,使其难以收敛;如果一次训练太多,则会占用较大内存,有时正负波相互抵消,最终使w无法改进.因此选择批尺寸时,需要在考虑内存和运算量的情况下尽量加大批尺寸.

4) 学习率

学习率也叫学习因子,简单地说,就是每次算出来的梯度对权值的影响的大小。学习率大,影响就大。学习率决定了参数移动到最优值的速度快慢。如果学习率过大,很可能会越过最优值;反而如果学习率过小,优化的效率可能过低,使得长时间算法无法收敛。
学习率在上例中是eta数值.
学习率的选择与误差函数相关,上例中使用SSE作为误差函数,批尺寸越大,梯度数据累加后越大,因此在计算时除以了批尺寸大小.我们可以选择一个不被训练集样本个数影响的误差函数(比如MSE).另外,输入特征的大小也对学习率有影响,所以处理前最好先归一化.
还可以在学习中动态地调整学习率,常用的有学习率有sgd, adadelta等.具体见参考中的《各种优化方法总结比较》

5) 激活函数

激活函数也叫激励函数,它的功能是将线性变换转成非线性变换,以提供非线性问题的解决方法.
上例中使用了sigmoid函数作为激活函数.常用的激活函数还有tanh,RelU等.其中ReLU是当前流行的激活函数y=max(0,x),它将大于0的值留下,否则一律为0。常用于处理大多数元素为0的稀疏矩阵。具体请见参考中的《神经网络之激活函数》

5. 参考

1) 神经网络入门之bp算法,梯度下降
http://blog.csdn.net/u013230651/article/details/75909596

2) 使用Python实现神经网络
http://blog.csdn.net/u014365862/article/details/53868414

3) 目标函数objectives
http://keras-cn.readthedocs.io/en/latest/other/objectives/

4) 各种优化方法总结比较
http://blog.csdn.net/luo123n/article/details/48239963

5) 机器学习中的损失函数
http://blog.csdn.net/shenxiaoming77/article/details/51614601

6) Deep Learning 学习随记(七)Convolution and Pooling --卷积和池化
http://blog.csdn.net/zhoubl668/article/details/24801103

7) 神经网络之激活函数(sigmoid、tanh、ReLU)
http://blog.csdn.net/suixinsuiyuan33/article/details/69062894?locationNum=4&fps=1

posted on 2017-11-02 15:36  xieyan0811  阅读(95)  评论(0编辑  收藏  举报