机器学习系列2 BP神经网络+代码实现

神经网络简单的说,就是用一种层次化的方式将一堆简单的函数在顶层堆叠在一起,形成一个复杂的非线性函数,以此表达输入与输出之间的关系。

本文结构:

              1.介绍构成神经网络的基本单元:神经元

              2.介绍三层神经网络的实现过程:前向传播、损失函数的计算、反向传播、梯度下降算法,并使用python分步实现

一、神经网络的基本单元:神经元

        神经元是构成神经网络的基本单元,图1.1展示了一个最简单的神经元结构

       

                                                                                图1.1 神经元结构

       图中\alpha称作激活函数,神经元的输出就是所有输入的加权和经过激活函数的处理得到的值。激活函数的作用可以理解为将输入非线性化从而能够得到更复杂的函数来处理非线性分布的数据。输入数据的权重就是神经网络的参数,神经网络的训练过程就是不断优化神经元中参数取值的过程。

二、三层bp神经网络的实现

       图2.1 为一个三层全链接bp神经网络

                                                                             图2.1 三层神经网络

      网络结构:D*H*C,  D为输入层神经元的个数,H为中间层神经元个数,C为输出层神经元的个数,如在多分类问题中,C代表类别个数。

     X: 输入,大小为N*D,N为输入样本个数,D为每个样本的属性个数。

     W、b:为神经网络的参数,是要在神经网络训练过程中不断的优化的。W1 W2分别是隐藏层的权重和输出层的权重矩阵,大小分别为D*HH*C,b1 b2 分别为隐藏层和输出层的偏置

     Y^:神经网络的输出,大小为N*C,表示着所有样本在C个类别上的得分

   (建议大家可以了解下线性分类器,对于理解很有帮助:https://zhuanlan.zhihu.com/p/20918580?refer=intelligentunit)       

      前面大概介绍了神经网络的结构和参数,下面我们来实现一个三层神经网络,需要准备的工具:python3 、jupyter、numpy,这些只要安装了anaconda就都有啦!!

      实现一个神经网络的第一步,便是准备数据,定义超参(神经网络的结构等训练过程中不会改变的参数),初始化参数(W b),数据方面使用的是mnist手写数字识别,比较经典的例子了,数据部分就不多介绍了,以下是准备数据的代码。

from sklearn import datasets
from sklearn.model_selection import train_test_split
# 加载sklearn自带的mnist数据
digits = datasets.load_digits()
# 数据集包含1797个手写体数字的图片,图片大小为8*8
# 数字大小0~9,也就是说有这是个10分类问题
images = digits.images
targets = digits.target
print(("dataset shape is: "), images.shape)
# 将数据分为训练数据和测试数据(20%)
X_train,X_test,y_train,y_test = train_test_split(images , targets , test_size=0.2 , random_state=0)
num_training = 1137
num_validation = 300
num_test = y_test.shape[0]
# 将训练集再分为训练集和验证集
mask = list(range(num_training, num_training + num_validation))
X_val = X_train[mask]
y_val = y_train[mask]
mask = list(range(num_training))
X_train = X_train[mask]
y_train = y_train[mask]
mask = list(range(num_test))
X_test = X_test[mask]
y_test = y_test[mask]
print("the number of train: ", num_training)
print("the number of test: ", num_test)
print("the number of validation: ", num_validation)
# 将每个数字8*8的像素矩阵转化为64*1的向量
X_train = X_train.reshape(num_training, -1)
X_val = X_val.reshape(num_validation, -1)
X_test = X_test.reshape(num_test, -1)
print("training data shape: ", X_train.shape)
print("validation data shape: ", X_val.shape)
print("test data shape: ", X_test.shape)

     输出结果:dataset shape is: (1797, 8, 8) the number of train: 1137 the number of test: 360 the number of validation: 300 training data shape: (1137, 64) validation data shape: (300, 64) test data shape: (360, 64)

     这样,根据前面所讲,我们的神经网络输入层神经元个数D为64,输出神经元个数C为10,N为1137,隐藏层神经元个数H我们设为30(这样随意制定其实是没有根据的,实际上我们可以进行多次试验,选择不同的中间层个数看最后哪个模型的结果更好,以此来选择H的大小)。知道了这些,我们就来定义神经网络的参数吧,代码如下。

# 定义神经网络的参数
# 定义超参
input_size = 64
hidden_size = 30
num_classes = 10
# 为了之后使用的方便,我将参数初始化,计算loss,训练,预测的过程都定义在一个名为network的类中
import numpy as np
import matplotlib.pyplot as plt
class network(object):
    # 初始化参数,将W,b保存在名为params的字典中
    # W随机初始化,b初始化为零
    def __init__(self, input_size, hidden_size, output_size, std=1e-4):
        self.params = {}
        self.params['W1'] = std * np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = std * np.random.randn(hidden_size, output_size)
        self.params['b2'] = np.zeros(output_size)

       到这里准备工作就搞定啦,下面要完成神经网络的核心部分,我会先讲述各个部分的知识,之后用代码实现~        

1.前向传播(inference)     

      前面说过神经元是构成神经网络的基本单元,而前向传播,我的理解就是由输入层和输入层与下一层的权重算出下一层的输出,之后根据下一层的输出,和下一层与下下一层之间的权重,算出下下一层的输出,……这样一直到输出层Y^,得到输出层的输出,我们就可以那它与真实的label共同计算出损失函数,通过反向传播算出各个参数的梯度,之后使用梯度下降等优化算法不断的优化参数,不断的迭代这个过程,最终达到使损失函数最小化的目的。扯远了,还是先讲回前向传播算法。

      前面讲神经元的时候,我们说过每个神经元都是先计算输入的加权和之后经过激活函数得到的输出   ,有矩阵基础的我们很容易就可以推出中间层的输出为         

                                                                              hidden = ReLU(XW1)      

                                                                              hiddden.shape = (N*D)*(D*H) = N*H

     什么?没有矩阵基础,那我把这个过程写出来,我估计你就懂啦

     y的算法与中间层的算法类似

                                                                               y = hidden*W2 +b2

                                                                               y.shape = N*C

    y就是N个输入样本在C个类别上的分数,分数越大,分成该类别的可能性也就越大,当然最后我们其实是选了y的softmax将其转为概率来决定最后分成那一类的,这个先暂且不谈。下面,就用我们学到的知识实现一个前向传播吧。

     之后还要实现损失函数,要用到前向传播,这里为了方便,我们将前向传播的代码写在loss函数中。

    # 定义损失函数,里面包含了前向传播的实现过程
    def loss(self, X, y=None, reg=0.0):
        # 先讲各个参数都提取出来
        W1 = self.params['W1']
        b1 = self.params['b1']
        W2 = self.params['W2']
        b2 = self.params['b2']
        N, D = X.shape
        # 前向传播
        # hidden的实现
        hidden = np.dot(X, W1) + b1
        # relu:max(0, x)
        hidden = np.maximum(0,hidden)
        # 算输出y
        y = np.dot(hidden, W2) + b2
        return y

       查看y的大小和y的第一行

bp = network(input_size, hidden_size, num_classes, std=1e-4)
y = bp.loss(X_train, y=None, reg=0.0)
print(y.shape)
print(y[0])
(1137, 10)
[-1.91580890e-06  3.81388898e-06  7.80884551e-07 -2.15404182e-06
 -6.46812857e-07  5.68680275e-07  2.99659642e-07  1.52743669e-06
  3.46021564e-07  1.94189152e-07]

      是不是我们预想的一样呢~

2. 损失函数

    损失函数的实现方法有很多种,比较常用的如交叉熵损失函数,svm算法使用的hing loss, 多分类问题常用的损失函数是softmax loss,

介绍softmax loss之前,我们要先了解什么是softmax

2.1 softmax

    softmax我们可以理解成能使前向传播得到的样本在各个类别上的得分转化为概率的一种函数,其表达式如下:

                                                                     P(y=k|X=x_{i}) = e^{y_{k}}/\sum_{j=1}^{C}e^{y_{j}}

    式中k为分类的第k个类别,代表了x样本被分为第k个类别的概率值。代码实现y[0]的softmax,你就会更直观的明白softmax的作用。

def softmax(y):
    # 首先算y中每个元素的指数
    exp = np.exp(y)
    # 算取指数之后的y的各个元素的和
    sum = np.sum(exp)
    # 根据softmax公式exp除上sum就等于该样本在各个类别上的概率啦
    per = exp/sum
    return per
per = softmax(y[0])
print(per)
    
    

  运行程序我们得到:

[0.0999998  0.10000007 0.09999971 0.10000025 0.10000011 0.10000023
 0.09999987 0.10000018 0.09999984 0.09999994]

2.2 softmax loss

   由于 log 运算符不会影响函数的单调性,我们先对第i个样本分类为正确的概率取对数

                                                           L_{i} = log P(y=y_{i}|X=x_{i})

   y_{i}就是第i个样本的label,也就是正确的分类。我们希望神经网络将样本分正确的概率越大越好,因此我们希望L_{i}越大越好,但通常来说我们是要最小化损失函数,因此我们对L_{i}取负号。因此我们的softmax loss可以定义为:

                                                          L = -(1/N)*\sum _{i=1}^{N}L_{i}

  到这里我们的损失函数其实还是不够完美,因为我们还没有加上正则化项,根据著名的奥卡姆剃刀原则,我们应该让模型在能达到效果的同时保证模型的简单化,否则就容易过拟合,泛化性差。

                                                         L = -(1/N)*\sum _{i=1}^{N}L_{i} + 1/2 *\lambda*W^{2}

  这样就得到了一个完整的softmax loss了。那就让我们完成loss函数吧~

 def loss(self, X, y=None, reg=0.0):
        # 先讲各个参数都提取出来
        W1 = self.params['W1']
        b1 = self.params['b1']
        W2 = self.params['W2']
        b2 = self.params['b2']
        N, D = X.shape
        # 前向传播
        # hidden的实现
        hidden = np.dot(X, W1) + b1
        # relu:max(0, x)
        hidden = np.maximum(0,hidden)
        # 算输出y
        y2 = np.dot(hidden, W2) + b2
        if y = None:
            return y2
        # loss 计算
        loss = None
        loss = -y2[range(N), y].sum() + np.log(np.exp(f).sum(axis = 1)).sum()
        loss = loss / N + 0.5 * reg * (np.sum(W1 * W1) + np.sum(W2 * W2))
        return loss
        

  还是算一下y[0]的loss

bp = network(input_size, hidden_size, num_classes, std=1e-4)
loss = bp.loss(X_train, y=y_train, reg=0.0)
print(loss[0])

 运行便可得到y[0]的softmax loss值

[-1.56893717e-06 -4.97653927e-07  3.76788810e-06  2.38449041e-06
  3.00933122e-06  2.42926917e-06 -3.31616168e-06 -7.18269769e-06
  3.00740161e-06  2.50132419e-07]

 对于softmax 和softmax loss这里有很多细节没有展示,大家可以参照下面这篇博文进行进一步的学习。

 https://blog.csdn.net/red_stone1/article/details/80687921

 下面就进入我个人认为最难理解的部分了,也就是反向传播算法,我会以计算图的角度来讲如何计算loss在各个参数上的梯度。

 在这之前建议大家先预习一下矩阵求导  https://zhuanlan.zhihu.com/p/25063314

3. 反向传播算法

  当我们使用前馈神经网络接受输入x并产生输出y^时,信息通过网络向前流动,输入x提供初始信息,然后传播到每一层的隐藏单元,并最终产生输出y,这个过程称之为前向传播。在训练神经网络的过程中,前向传播可以持续向前直到它产生一个标量损失函数反向传播则是让损失函数的信息通过网络向后传动,从而算出损失函数关于各个可训练参数的梯度。需要注意的是,反向传播是一种计算梯度的方法,不仅仅只可以用在神经网络中,更不仅仅是可以用在损失函数上,只是恰巧神经网络要这样用罢了。这种在网络中传播信息来计算导数的想法非常普遍。

3.1 计算图

 了解过tensorflow的人应该都知道,计算图是tensorflow的最基本的概念, 每一个计算都可用计算图中一个节点来表示,而节点之间的边则是描述计算之间的依赖关系。举一个简单的例子:y = 3x +5

  一旦我们能使用一个计算图来表示一个函数,我们就能够很方便的使用“反向传播算法”递归地调用链式求导法则来计算计算图中每个变量的梯度。

3.1 如何用计算图实施反向传播

  最终输出(这里最终输出是损失函数)关于输入的梯度 = 上游传播回来的梯度 * 本地的梯度值(指的是连接输入的节点的输出关于输入的梯度)

  拿一个单独的节点来举例,f表示某种运算,可以是乘、加、甚至是relu激活函数……

  z = f(x,y)

 

表达式:  

   若f为乘法运算,那么z关于x的本地梯度就是y,z关于y的本地梯度就是x;若f为加法运算,那么z关于x和y的本地梯度就都为1。这里我们可以假设:f为乘法运算,x输入值为1,y输入值为2,上游传回的损失函数关于z的梯度为1,根据前面的公式,我们可以很轻松的就算出损失函数关于x的梯度值为 1*2=2。到这里有人可能就有疑惑,这不就是链式求导法则么?为什么还要搞计算图这一套?其实是这样的,若函数特别复杂,直接求解会很困难,很容易就搞错,而使用计算图就是帮我们理清思路的。

3.2 三层神经网络的反向传播

  让我们先来理清思路,我们首先要通过前向传播算法算出神经网络的输出y^; 之后通过输出y^与真实的label y算出损失函数loss;最后通过反向传播算法算出损失函数关于可训练参数(W1 W2 b1 b2)的梯度。我相信大家已经对这个思路非常了解了,下面把公式列出,方便大家用画出计算图计算反向传播。

                                                         y_{hat} = (max(0, XW1)+b1)W2 +b2

                                                         L_{i} = log P(y=y_{i}|X=x_{i})

                                                         L = -(1/N)*\sum _{i=1}^{N}L_{i} + 1/2 *\lambda*W1^{2} +1/2 *\lambda*W2^{2}

   需要注意的是,我们的输入和输出都是矩阵,而不是单个数字,所以我们要时刻记得可训练参数的梯度与它自身的大小要一致!那么现在可以试着画一下计算图,算可训练参数的梯度啦。每个画的计算图可能都会有所不同,下面是我绘制的计算图,和推导出的梯度,可供参考。

  

  

                 (是不是治好了你多年的劲椎病??)

   上图没有看懂没关系,先自己把计算图画下来,代码中的注释我写的很详细,你可以和计算图对应起来慢慢消化。下面我就把反向传播的梯度计算代码写在loss函数中,致此loss函数写完。

# 反向传播
        # 首先定义一个grads的字典,存放各个可训练参数的梯度
        grads = {}
        # 按照计算图,先计算dscore
        # 先对y2取对数
        exp = np.exp(y2)
        # 求每行个元素的和,之后用每行各个元素除上该行的和
        dscore = exp / exp.sum(axis = 1, keepdims = True)
        # 对label(即y)对应的元素减1
        dscore[range(N),y] -= 1
        # 别忘了还要除输入样本的个数
        dscore = dscore/N 
        grads['b2'] = np.sum(dscore, axis=0)
        grads['W2'] = np.dot(hidden.T, dscore)+ reg * W2
        # dhidden
        dhidden = np.dot(dscore, W2.T)
        # 因为加了relu激活函数,随意要讲XW1 + b1 <0对应的dihidden元素归0
        dhidden_0 = dhidden[(np.dot(X, W1)+b1)<0] = 0
        grads['b1'] = np.sum(dhidden_0, axis=0)
        grads['W1'] = np.dot(X.T, dhidden_0) + reg * W1
        return loss, grads
    

   对于反向传播,大家可以看深度学习之父Goodfellow的经典书籍:《深度学习》(p126~p139)你一定户会有很多收获。

4.梯度下降算法

  这里就属于最优化的部分了,我们用梯度下降来更新可训练参数,以使得损失函数达到最小,这样就可以达到训练的目的,这部分比较简单,我就不详细的讲了,只给出公式。

                                                              W1 = W1-learningrate*grad[W1]

                                                              W2 = W2 - learningrate*grad[W2]

                                                              b1 = b1-learningrate*gard[b1]

                                                              b2 = b2-learningrate*grad[b2]

  训练代码的实现,我使用的随机梯度下降的方法,简单来说就是并非直接使用全部训练样本来计算损失函数的梯度,而是在每次迭代中,选取一个batch来训练样本,batch 的大小我们通常取2的倍数。

def train(self, X, y, X_val, y_val,
            learning_rate=1e-3, learning_rate_decay=0.95,
            reg=5e-6, num_iters=100,
            batch_size=200, verbose=False):
        # 查看有有多少个训练样本,并检查按照设定的batch大小每个epoch需要迭代多少次
        num_train = X.shape[0]
        iterations_per_epoch = max(num_train / batch_size, 1)

        # 使用随机梯度下降优化可训练参数
        # 把训练过程中得到的loss和准确率信息存起来方便查看并解决问题
        loss_history = []
        train_acc_history = []
        val_acc_history = []
        # 迭代numz_iters次,每次只随机选择一个batch来训练样本
        for it in range(num_iters):
            X_batch = None
            y_batch = None
            indices = np.random.choice(num_train, batch_size, replace=True)
            X_batch = X[indices]
            y_batch = y[indices]
            # 用当前的batch训练数据来得到loss 和grad
            loss, grads = self.loss(X_batch, y=y_batch, reg=reg)
            # 记录这次迭代的损失大小
            loss_history.append(loss)
            self.params['W1'] -=  learning_rate * grads['W1']
            self.params['b1'] -=  learning_rate * grads['b1']
            self.params['W2'] -=  learning_rate * grads['W2']
            self.params['b2'] -=  learning_rate * grads['b2']
            # 如果你选择了可视化训练过程,那么会显示每次迭代产生的loss
            if verbose and it % 100 == 0:
                print('iteration %d / %d: loss %f' % (it, num_iters, loss))

           # Every epoch, check train and val accuracy and decay learning rate.
            if it % iterations_per_epoch == 0:
                # Check accuracy
                train_acc = (self.predict(X_batch) == y_batch).mean()
                val_acc = (self.predict(X_val) == y_val).mean()
                train_acc_history.append(train_acc)
                val_acc_history.append(val_acc)

                # 每个epoch结束,衰减一下学习率
                learning_rate *= learning_rate_decay

        return {
            'loss_history': loss_history,
            'train_acc_history': train_acc_history,
            'val_acc_history': val_acc_history,
        }

  让我们运行训练程序看看结果吧

net = network(input_size, hidden_size, num_classes)
stats = net.train(X_train, y_train, X_val, y_val,
            num_iters=5000, batch_size=1000,
            learning_rate=0.01, learning_rate_decay=0.95,
            reg=0.25, verbose=True)
val_acc = (net.predict(X_val) == y_val).mean()
print('Validation accuracy: ', val_acc)

iteration 0 / 5000: loss 2.302588
iteration 100 / 5000: loss 2.301742
iteration 200 / 5000: loss 2.157397
iteration 300 / 5000: loss 1.291627
iteration 400 / 5000: loss 0.992755
iteration 500 / 5000: loss 0.853052
iteration 600 / 5000: loss 0.808615
iteration 700 / 5000: loss 0.815295
iteration 800 / 5000: loss 0.798135
iteration 900 / 5000: loss 0.776097
iteration 1000 / 5000: loss 0.789725
iteration 1100 / 5000: loss 0.781372
iteration 1200 / 5000: loss 0.768215
iteration 1300 / 5000: loss 0.741990
iteration 1400 / 5000: loss 0.761446
iteration 1500 / 5000: loss 0.758429
iteration 1600 / 5000: loss 0.744926
iteration 1700 / 5000: loss 0.725760
iteration 1800 / 5000: loss 0.739801
iteration 1900 / 5000: loss 0.739610
iteration 2000 / 5000: loss 0.744809
iteration 2100 / 5000: loss 0.746988
iteration 2200 / 5000: loss 0.752954
iteration 2300 / 5000: loss 0.766363
iteration 2400 / 5000: loss 0.733834
iteration 2500 / 5000: loss 0.738579
iteration 2600 / 5000: loss 0.757790
iteration 2700 / 5000: loss 0.756696
iteration 2800 / 5000: loss 0.756156
iteration 2900 / 5000: loss 0.749059
iteration 3000 / 5000: loss 0.723457
iteration 3100 / 5000: loss 0.743312
iteration 3200 / 5000: loss 0.721808
iteration 3300 / 5000: loss 0.743211
iteration 3400 / 5000: loss 0.729517
iteration 3500 / 5000: loss 0.737294
iteration 3600 / 5000: loss 0.745131
iteration 3700 / 5000: loss 0.744178
iteration 3800 / 5000: loss 0.728137
iteration 3900 / 5000: loss 0.731122
iteration 4000 / 5000: loss 0.730373
iteration 4100 / 5000: loss 0.733699
iteration 4200 / 5000: loss 0.757392
iteration 4300 / 5000: loss 0.746430
iteration 4400 / 5000: loss 0.754329
iteration 4500 / 5000: loss 0.750048
iteration 4600 / 5000: loss 0.740220
iteration 4700 / 5000: loss 0.763828
iteration 4800 / 5000: loss 0.765284
iteration 4900 / 5000: loss 0.754324
Validation accuracy:  0.96

  最后在验证集上的准确率达到96%是不是很有成就感呢。

  好啦,至此,bp神经网络我就讲完啦,其实还有很多很多细节没有讲,如什么随机梯度下降,还有各种更强的优化方法,在这学习率衰减我也没有在这里讲,因为毕竟本片博文只是讲解bp神经网络,更细节的东西有机会再慢慢说吧。

完整代码,我会整理好放在我的github上,大家可以做参考:https://github.com/zhengmingzhang/bp-csdn

总结:

  机器学习、深度学习最近真的很火,我认为也必将是未来的方向,听在找工作的学长学姐说,面试时候问的都是算法背后的数学推导,而不是让你简单实现一下就好,其实也很好理解,使用sklearn包当然很快就能实现一个算法,但是当你不理解算法本身,你真的很难知道该怎么样提升算法的效果来更好的解决你面对的问题,所以静下心来自己推导就很重要啦~

猜你喜欢

转载自blog.csdn.net/weixin_41722370/article/details/83590276
今日推荐