从头开始实现多层人工神经网络

本文详细介绍了多层人工神经网络(MLP)的结构和工作原理,包括前向传播、反向传播算法以及训练过程中的误差反向传播。通过实例解释了批量梯度下降、随机梯度下降在训练过程中的应用,同时探讨了训练过程中如何避免过拟合和优化学习速率。最后,展示了如何实现一个简单的多层感知机(MLP)模型,并在MNIST数据集上进行手写数字分类任务。
摘要由CSDN通过智能技术生成

从头开始实现多层人工神经网络

from IPython.display import Image
%matplotlib inline

1.使用人工神经网络建模复杂函数

神经网络中的Epoch、Iteration、Batchsize

神经网络中epoch与iteration是不相等的

  • batchsize:中文翻译为批大小(批尺寸)。在深度学习中,一般采用SGD训练,即每次训练在训练集中取batchsize个样本训练;

  • iteration:中文翻译为迭代,1个iteration等于使用batchsize个样本训练一次;一个迭代 = 一个正向通过+一个反向通过

  • epoch:迭代次数,1个epoch等于使用训练集中的全部样本训练一次;一个epoch = 所有训练样本的一个正向传递和一个反向传递

  • 举个例子,训练集有1000个样本,batchsize=10,那么:训练完整个样本集需要:100次iteration,1次epoch。

1.1单隐层神经网络综述

回顾前面的自适应神经元算法。在

前面,通过梯度下降优化算法来学习模型的权重系数。在每个epoch中(传递训练数据集),权重更新过程如下:

w : = w + Δ w ,  where  Δ w = − η ∇ J ( w ) \boldsymbol{w}:=\boldsymbol{w}+\Delta \boldsymbol{w}, \quad \text { where } \Delta \boldsymbol{w}=-\eta \nabla J(\boldsymbol{w}) w:=w+Δw, where Δw=ηJ(w)

这里实际上使用的是批量梯度下降,即计算的梯度是基于整个训练集,同时基于梯度的负方向更新模型的权值。其中,定义目标

函数为误差平方和SSE,并记作 J ( W ) J\boldsymbol(W) J(W)。更进一步,通过将 − ∇ J ( W ) -\nabla J\boldsymbol(W) J(W)乘上学习率 η \eta η,用以控制

下降步伐,从而避免越过了代价函数的全局最小值。

基于上述优化方式,我们同时更新所有的权重系数,定义每个权重的偏导数如下:
∂ ∂ w j J ( w ) = − ∑ i ( y ( i ) − a ( i ) ) x j ( i ) \frac{\partial}{\partial w_{j}} J(\boldsymbol{w})=-\sum_{i}\left(y^{(i)}-a^{(i)}\right) x_{j}^{(i)} wjJ(w)=i(y(i)a(i))xj(i)

这里的 y ( i ) y^{(i)} y(i)代表的是特定样本 x ( i ) x^{(i)} x(i)的类别标签, a ( i ) a^{(i)} a(i)代表的是神经元的激活函数,在自适应神经元中是一个线性

函数。形式如下:

ϕ ( z ) = z = a \phi(z)=z=a ϕ(z)=z=a

其中,上式的 z z z为连接输入层和输出层的权值线性组合:
z = ∑ j w j x j = w T x z=\sum_{j} w_{j} x_{j}=\boldsymbol{w}^{T} \boldsymbol{x} z=jwjxj=wTx

使用上述激活函数计算梯度更新,进一步实现一个阈值函数,将连续值输出压缩成二进制类别标签:
y ^ = { 1  if  g ( z ) ≥ 0 − 1  otherwise  \hat{y}=\left\{\begin{aligned} 1 & \text { if } g(z) \geq 0 \\ -1 & \text { otherwise } \end{aligned}\right. y^={11 if g(z)0 otherwise 

Image(filename='images/12_01.png', width=600) 

在这里插入图片描述

如上图,尽管Adaline由两个层组成,一个输入一个输出。但是由于输入层和输出增之间仅仅有一条链路,依然称其为单层网络。

另外一种加速模型学习的优化方式为随机梯度下降**Stochastic gradient descent SGD**,SGD 近似于单个训练样本(online learning),或者近似于使用一小部分训练样本(小批量学习)。

SGD相较于批量梯度下降,由于权重更新更加频繁,因此学习速度更快。同时噪声特性也使得其在训练具有非线性激活函数的

多层神经网络(其不具有凸代价函数)的时候,也很有益。

这里引入的噪声可以促进优化目标 避免陷入局部最小。



1.2介绍多层神经网络结构

将多个 单个的神经元连接到多层前馈神经网络,这种特殊类型的全连接网络也称为MLP。

# 三个层次的MLP几何表示
Image(filename='images/12_02.png', width=600) 

在这里插入图片描述

标记:第 L L L层的第 i i i个激活单元为 a i ( l ) a_i^{(l)} ai(l),则上图输入层的激活单元加上偏置单元后,表示如下:

a ( i n ) = [ a 0 ( i n ) a 1 ( i n ) ⋮ a m ( i n ) ] = [ 1 x 1 ( i n ) ⋮ x m ( i n ) ] \boldsymbol{a}^{(i n)}=\left[\begin{array}{c} a_{0}^{(i n)} \\ a_{1}^{(i n)} \\ \vdots \\ a_{m}^{(i n)} \end{array}\right]=\left[\begin{array}{c} 1 \\ x_{1}^{(i n)} \\ \vdots \\ x_{m}^{(i n)} \end{array}\right] a(in)=a0(in)a1(in)am(in)=1x1(in)xm(in)

对上述的网络结构,如果具有一个以上的隐含层,则称之为深层人工神经网络。

Image(filename='images/12_03.png', width=500) 

在这里插入图片描述



1.3通过前向传播激活神经网络

MLP学习过程总结

1.从输入层开始,通过网络前向传播训练数据的模式,从而生成输出;

2.基于网络的输出,使用代价函数来计算我们想要最小化的误差;

3.对误差进行反向传播,求出误差相对于网络中每个权重的导数,同时对模型进行更新;

最后,在对多个epoch重复这三个步骤并学习MLP的权重之后,使用前向传播来计算网络输出,并应用阈值函数来获得在独热编码表示中预测的类别标签。

由于隐含层中的每个单元都与输入层中的所有单元相连,这里先计算隐含层的激活单元 a 1 ( h ) a_1^{(h)} a1(h):

z 1 ( h ) = a 0 ( i n ) w 0 , 1 ( h ) + a 1 ( i n ) w 1 , 1 ( h ) + ⋯ + a m ( i n ) w m , 1 ( h ) a 1 ( h ) = ϕ ( z 1 ( h ) ) \begin{array}{l} z_{1}^{(h)}=a_{0}^{(i n)} w_{0,1}^{(h)}+a_{1}^{(i n)} w_{1,1}^{(h)}+\cdots+a_{m}^{(i n)} w_{m, 1}^{(h)} \\ a_{1}^{(h)}=\phi\left(z_{1}^{(h)}\right) \end{array} z1(h)=a0(in)w0,1(h)+a1(in)w1,1(h)++am(in)wm,1(h)a1(h)=ϕ(z1(h))

其中, z 1 ( h ) z_{1}^{(h)} z1(h)是网络输入, ϕ ( ⋅ ) \phi{(\cdot)} ϕ()是激活函数,其必须可微,这样才可以基于梯度的学习方法学习连接神经元的权重。

为了能够解决图像分类这样的复杂问题,MLP中需要使用非线性激活函数,例如sigmoid函数:

ϕ ( z ) = 1 1 + e − z \phi(z)=\frac{1}{1+e^{-z}} ϕ(z)=1+ez1

# sigmoid函数几何表示如下
Image(filename='images/12_04.png', width=500) 

在这里插入图片描述

MLP是前馈人工神经网络的一个典型例子。前馈代表的是:每一层都作为下一层的输入,而没有循环,这与循环神经网络RNN不同。

multilayer perceptron多层感知机实际上有一定的混淆视听,其网络架构中的人工神经元是典型的sigmoid单元,而不是感知机。

我们可以把MLP中的神经元看做是逻辑回归单元,返回值在0到1之间的连续范围内。

为了表示简化,上式简写为:

z ( h ) = a ( i n ) W ( h ) a ( h ) = ϕ ( z ( h ) ) \begin{array}{l} \boldsymbol{z}^{(h)}=\boldsymbol{a}^{(i n)} \boldsymbol{W}^{(h)} \\ \boldsymbol{a}^{(h)}=\phi\left(\mathbf{z}^{(h)}\right) \end{array} z(h)=a(in)W(h)a(h)=ϕ(z(h))

这里的 a ( i n ) \boldsymbol{a}^{(i n)} a(in) 1 × m 1 \times m 1×m的特征向量,还加上了一个偏置单元,样本为 x i n \boldsymbol{x}^{i n} xin

W ( h ) \boldsymbol{W}^{(h)} W(h) m × d m \times d m×d维的权重矩阵,其中 d d d为隐含层神经元的个数。

经过矩阵-向量乘法之后,得到 1 × d 1 \times d 1×d维的网络净输入(净活性值) z ( h ) \boldsymbol{z}^{(h)} z(h),以此来计算活性值 a ( h ) \boldsymbol{a}^{(h)} a(h),其中 a ( h ) ∈ R 1 × d \boldsymbol{a}^{(h)} \in \mathbb{R}^{1 \times d} a(h)R1×d

净输入在经过非线性激活函数之后,得到网络的活性值。净输入为输入信息 x x x的加权和,非线性函数也叫作激活函数

更进一步,将上述计算推广到所有的样本上:
Z ( h ) = A ( i n ) W ( h ) \boldsymbol{Z}^{(h)}=\boldsymbol{A}^{(i n)} \boldsymbol{W}^{(h)} Z(h)=A(in)W(h)

这里的 A ( i n ) \boldsymbol{A}^{(i n)} A(in)是一个矩阵,形状为 n × m n \times m n×m,则在矩阵乘法之后,得到网络的输入矩阵 Z ( h ) \boldsymbol{Z}^{(h)} Z(h),形状为 n × d n \times d n×d

然后通过激活函数的计算得到网络的活性值,这里的输出层:
A ( h ) = ϕ ( Z ( h ) ) \boldsymbol{A}^{(h)}=\phi\left(\boldsymbol{Z}^{(h)}\right) A(h)=ϕ(Z(h))

类似的,将输出层的活性值写成向量形式如下:
Z ( out  ) = A ( h ) W ( out  ) \boldsymbol{Z}^{(\text {out })}=\boldsymbol{A}^{(h)} \boldsymbol{W}^{(\text {out })} Z(out )=A(h)W(out )

这里乘上 d × t d \times t d×t维度的矩阵 W ( out  ) \boldsymbol{W}^{(\text {out })} W(out ), t t t是输出单元的个数, A ( h ) \boldsymbol{A}^{(h)} A(h)size为 n × d n \times d n×d,最终得到 n × t n \times t n×t维的 Z ( out  ) \boldsymbol{Z}^{(\text {out })} Z(out )

最后,添加上激活函数,得到网络的输出:
A ( o u t ) = ϕ ( Z ( o u t ) ) , A ( o u t ) ∈ R n × t \boldsymbol{A}^{(o u t)}=\phi\left(\boldsymbol{Z}^{(o u t)}\right), \quad \boldsymbol{A}^{(o u t)} \in \mathbb{R}^{n \times t} A(out)=ϕ(Z(out)),A(out)Rn×t

如果想通过sklearn API获取MNIST数据集,执行下述代码:

"""
from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split


X, y = fetch_openml('mnist_784', version=1, return_X_y=True)
y = y.astype(int)
X = ((X / 255.) - .5) * 2
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=10000, random_state=123, stratify=y)
"""
"\nfrom sklearn.datasets import fetch_openml\nfrom sklearn.model_selection import train_test_split\n\n\nX, y = fetch_openml('mnist_784', version=1, return_X_y=True)\ny = y.astype(int)\nX = ((X / 255.) - .5) * 2\nX_train, X_test, y_train, y_test = train_test_split(\n    X, y, test_size=10000, random_state=123, stratify=y)\n"


2.手写数字分类

2.1获取并准备MNIST数据集

MNIST数据集获取地址 ,其由以下几部分组成:

  • Training set images: train-images-idx3-ubyte.gz (9.9 MB, 47 MB unzipped, 60,000 examples)
  • Training set labels: train-labels-idx1-ubyte.gz (29 KB, 60 KB unzipped, 60,000 labels)
  • Test set images: t10k-images-idx3-ubyte.gz (1.6 MB, 7.8 MB, 10,000 examples)
  • Test set labels: t10k-labels-idx1-ubyte.gz (5 KB, 10 KB unzipped, 10,000 labels)
#  mnist数据解压代码

import sys
import gzip
import shutil
import os

if (sys.version_info > (3, 0)):
    writemode = 'wb'
else:
    writemode = 'w'

zipped_mnist = [f for f in os.listdir() if f.endswith('ubyte.gz')]
for z in zipped_mnist:
    with gzip.GzipFile(z, mode='rb') as decompressed, open(z[:-3], writemode) as outfile:
        outfile.write(decompressed.read()) 

如果没有使用gzip格式,需要确保文件命名形式如下:

  • train-images-idx3-ubyte
  • train-labels-idx1-ubyte
  • t10k-images-idx3-ubyte
  • t10k-labels-idx1-ubyte

# 图像数据以字节码的形式进行存储,将其读入为Numpy数组形式

import os
import struct
import numpy as np
 
def load_mnist(path, kind='train'):
    """Load MNIST data from `path`"""
    labels_path = os.path.join(path, 
                               '%s-labels-idx1-ubyte' % kind)
    images_path = os.path.join(path, 
                               '%s-images-idx3-ubyte' % kind)
        
    with open(labels_path, 'rb') as lbpath:
        magic, n = struct.unpack('>II', 
                                 lbpath.read(8))
        labels = np.fromfile(lbpath, 
                             dtype=np.uint8)

    with open(images_path, 'rb') as imgpath:
        magic, num, rows, cols = struct.unpack(">IIII", 
                                               imgpath.read(16))
        images = np.fromfile(imgpath, 
                             dtype=np.uint8).reshape(len(labels), 784)
        images = ((images / 255.) - .5) * 2  # 原始像素值在0-255之间,归一化之后缩放到正负一之间;
 
    return images, labels
# load_mnist函数返回两个数组,第一个为nxm维的NumPy数组(图像),其中n为样本个数,m为特征个数。
# 训练数据集含60000个训练样本,测试数据含10000个样本。
# MNIST数据集中的图像为28x28维的像素点,每个像素点代表一个灰度强度值。
# 这里将28x28的图像拉伸为1维的行向量,其代表输入图像矩阵的行,对应784/行,或者/图像
# 上述函数返回的第二个数组为目标变量,即类别标签(这里为整数值0-9)
X_train, y_train = load_mnist('', kind='train')
print('Rows: %d, columns: %d' % (X_train.shape[0], X_train.shape[1]))
Rows: 60000, columns: 784
X_test, y_test = load_mnist('', kind='t10k')
print('Rows: %d, columns: %d' % (X_test.shape[0], X_test.shape[1]))
Rows: 10000, columns: 784
# 基于梯度的优化中,使用批量归一化是实现更好收敛性的一个技巧  Batch normalization

这里先通过imshow函数将特征矩阵从784像素的向量reshape成原始的28x28维度的图像,从而实现可视化

import matplotlib.pyplot as plt

fig, ax = plt.subplots(nrows=2, ncols=5, sharex=True, sharey=True)
ax = ax.flatten()

for i in range(10):
    img = X_train[y_train == i][0].reshape(28, 28)
    ax[i].imshow(img, cmap='Greys')

ax[0].set_xticks([])
ax[0].set_yticks([])
plt.tight_layout()
# plt.savefig('images/12_5.png', dpi=300)
plt.show()

在这里插入图片描述

看看同一个数字的不同写法

# 数字7的25中不同变体
fig, ax = plt.subplots(nrows=5, ncols=5, sharex=True, sharey=True,)
ax = ax.flatten()
for i in range(25):
    img = X_train[y_train == 7][i].reshape(28, 28)
    ax[i].imshow(img, cmap='Greys')

ax[0].set_xticks([])
ax[0].set_yticks([])
plt.tight_layout()
# plt.savefig('images/12_6.png', dpi=300)
plt.show()

在这里插入图片描述

在完成上述处理步骤之后,这里将处理过后的数据进行保存。通过使用NumPy的savez函数实现将多维数组保存到磁盘。

savez函数类似于python的pickle模块,其将创建压缩后的数据存档,生成包含.npy格式文件的.npz文件;此外,这里使用savez_compressed而不是

savez函数,因为它使用与savez相同的语法,但可以进一步将输出文件压缩到更小的文件体积。(从原本的400M到压缩之后的20M)。

# 将训练集和测试集保存到归档文件
import numpy as np

np.savez_compressed('mnist_scaled.npz', 
                    X_train=X_train,
                    y_train=y_train,
                    X_test=X_test,
                    y_test=y_test)
# 创建了压缩文件之后,可以使用Numpy的load方法进行加载
mnist = np.load('mnist_scaled.npz')
# mnist变量现在指向一个对象,该对象可以访问savez_compressed函数作为关键字参数提供的四个数据数组。通过mnist.files属性访问。
mnist.files
['X_train', 'y_train', 'X_test', 'y_test']
# 例如
# X_train = mnist['X_train']
# 使用列表生成式,可以检索所有四个数据数组
X_train, y_train, X_test, y_test = [mnist[f] for f in ['X_train', 'y_train', 
                                    'X_test', 'y_test']]

del mnist

X_train.shape
(60000, 784)


2.2实现多层感知机

import numpy as np
import sys


class NeuralNetMLP(object):
    """ Feedforward neural network / Multi-layer perceptron classifier.

    Parameters
    ------------
    n_hidden : int (default: 30)
        Number of hidden units.
    l2 : float (default: 0.)
        Lambda value for L2-regularization.
        No regularization if l2=0. (default)
    epochs : int (default: 100)
        Number of passes over the training set.
    eta : float (default: 0.001)
        Learning rate.
    shuffle : bool (default: True)
        Shuffles training data every epoch if True to prevent circles.
    minibatch_size : int (default: 1)
        Number of training examples per minibatch.
    seed : int (default: None)
        Random seed for initializing weights and shuffling.

    Attributes
    -----------
    eval_ : dict
      Dictionary collecting the cost, training accuracy,
      and validation accuracy for each epoch during training.

    """
    def __init__(self, n_hidden=30,
                 l2=0., epochs=100, eta=0.001,
                 shuffle=True, minibatch_size=1, seed=None):

        self.random = np.random.RandomState(seed)
        self.n_hidden = n_hidden
        self.l2 = l2
        self.epochs = epochs
        self.eta = eta
        self.shuffle = shuffle
        self.minibatch_size = minibatch_size

    def _onehot(self, y, n_classes):
        """Encode labels into one-hot representation

        Parameters
        ------------
        y : array, shape = [n_examples]
            Target values.
        n_classes : int
            Number of classes

        Returns
        -----------
        onehot : array, shape = (n_examples, n_labels)

        """
        onehot = np.zeros((n_classes, y.shape[0]))
        for idx, val in enumerate(y.astype(int)):
            onehot[val, idx] = 1.
        return onehot.T

    def _sigmoid(self, z):
        """Compute logistic function (sigmoid)"""
        return 1. / (1. + np.exp(-np.clip(z, -250, 250)))

    def _forward(self, X):
        """Compute forward propagation step"""

        # step 1: net input of hidden layer
        # [n_examples, n_features] dot [n_features, n_hidden]
        # -> [n_examples, n_hidden]
        z_h = np.dot(X, self.w_h) + self.b_h

        # step 2: activation of hidden layer
        a_h = self._sigmoid(z_h)

        # step 3: net input of output layer
        # [n_examples, n_hidden] dot [n_hidden, n_classlabels]
        # -> [n_examples, n_classlabels]

        z_out = np.dot(a_h, self.w_out) + self.b_out

        # step 4: activation output layer
        a_out = self._sigmoid(z_out)

        return z_h, a_h, z_out, a_out

    def _compute_cost(self, y_enc, output):
        """Compute cost function.

        Parameters
        ----------
        y_enc : array, shape = (n_examples, n_labels)
            one-hot encoded class labels.
        output : array, shape = [n_examples, n_output_units]
            Activation of the output layer (forward propagation)

        Returns
        ---------
        cost : float
            Regularized cost

        """
        L2_term = (self.l2 *
                   (np.sum(self.w_h ** 2.) +
                    np.sum(self.w_out ** 2.)))

        term1 = -y_enc * (np.log(output))
        term2 = (1. - y_enc) * np.log(1. - output)
        cost = np.sum(term1 - term2) + L2_term
        
        # If you are applying this cost function to other
        # datasets where activation
        # values maybe become more extreme (closer to zero or 1)
        # you may encounter "ZeroDivisionError"s due to numerical
        # instabilities in Python & NumPy for the current implementation.
        # I.e., the code tries to evaluate log(0), which is undefined.
        # To address this issue, you could add a small constant to the
        # activation values that are passed to the log function.
        #
        # For example:
        #
        # term1 = -y_enc * (np.log(output + 1e-5))
        # term2 = (1. - y_enc) * np.log(1. - output + 1e-5)
        
        return cost

    def predict(self, X):
        """Predict class labels

        Parameters
        -----------
        X : array, shape = [n_examples, n_features]
            Input layer with original features.

        Returns:
        ----------
        y_pred : array, shape = [n_examples]
            Predicted class labels.

        """
        z_h, a_h, z_out, a_out = self._forward(X)
        y_pred = np.argmax(z_out, axis=1)
        return y_pred

    def fit(self, X_train, y_train, X_valid, y_valid):
        """ Learn weights from training data.

        Parameters
        -----------
        X_train : array, shape = [n_examples, n_features]
            Input layer with original features.
        y_train : array, shape = [n_examples]
            Target class labels.
        X_valid : array, shape = [n_examples, n_features]
            Sample features for validation during training
        y_valid : array, shape = [n_examples]
            Sample labels for validation during training

        Returns:
        ----------
        self

        """
        n_output = np.unique(y_train).shape[0]  # number of class labels
        n_features = X_train.shape[1]

        ########################
        # Weight initialization
        ########################

        # weights for input -> hidden
        self.b_h = np.zeros(self.n_hidden)
        self.w_h = self.random.normal(loc=0.0, scale=0.1,
                                      size=(n_features, self.n_hidden))

        # weights for hidden -> output
        self.b_out = np.zeros(n_output)
        self.w_out = self.random.normal(loc=0.0, scale=0.1,
                                        size=(self.n_hidden, n_output))

        epoch_strlen = len(str(self.epochs))  # for progress formatting
        self.eval_ = {'cost': [], 'train_acc': [], 'valid_acc': []}

        y_train_enc = self._onehot(y_train, n_output)

        # iterate over training epochs
        for i in range(self.epochs):

            # iterate over minibatches
            indices = np.arange(X_train.shape[0])

            if self.shuffle:
                self.random.shuffle(indices)

            for start_idx in range(0, indices.shape[0] - self.minibatch_size +
                                   1, self.minibatch_size):
                batch_idx = indices[start_idx:start_idx + self.minibatch_size]

                # forward propagation
                z_h, a_h, z_out, a_out = self._forward(X_train[batch_idx])

                ##################
                # Backpropagation
                ##################

                # [n_examples, n_classlabels]
                delta_out = a_out - y_train_enc[batch_idx]

                # [n_examples, n_hidden]
                sigmoid_derivative_h = a_h * (1. - a_h)

                # [n_examples, n_classlabels] dot [n_classlabels, n_hidden]
                # -> [n_examples, n_hidden]
                delta_h = (np.dot(delta_out, self.w_out.T) *
                           sigmoid_derivative_h)

                # [n_features, n_examples] dot [n_examples, n_hidden]
                # -> [n_features, n_hidden]
                grad_w_h = np.dot(X_train[batch_idx].T, delta_h)
                grad_b_h = np.sum(delta_h, axis=0)

                # [n_hidden, n_examples] dot [n_examples, n_classlabels]
                # -> [n_hidden, n_classlabels]
                grad_w_out = np.dot(a_h.T, delta_out)
                grad_b_out = np.sum(delta_out, axis=0)

                # Regularization and weight updates
                delta_w_h = (grad_w_h + self.l2*self.w_h)
                delta_b_h = grad_b_h # bias is not regularized
                self.w_h -= self.eta * delta_w_h
                self.b_h -= self.eta * delta_b_h

                delta_w_out = (grad_w_out + self.l2*self.w_out)
                delta_b_out = grad_b_out  # bias is not regularized
                self.w_out -= self.eta * delta_w_out
                self.b_out -= self.eta * delta_b_out

            #############
            # Evaluation
            #############

            # Evaluation after each epoch during training
            z_h, a_h, z_out, a_out = self._forward(X_train)
            
            cost = self._compute_cost(y_enc=y_train_enc,
                                      output=a_out)

            y_train_pred = self.predict(X_train)
            y_valid_pred = self.predict(X_valid)

            train_acc = ((np.sum(y_train == y_train_pred)).astype(np.float) /
                         X_train.shape[0])
            valid_acc = ((np.sum(y_valid == y_valid_pred)).astype(np.float) /
                         X_valid.shape[0])

            sys.stderr.write('\r%0*d/%d | Cost: %.2f '
                             '| Train/Valid Acc.: %.2f%%/%.2f%% ' %
                             (epoch_strlen, i+1, self.epochs, cost,
                              train_acc*100, valid_acc*100))
            sys.stderr.flush()

            self.eval_['cost'].append(cost)
            self.eval_['train_acc'].append(train_acc)
            self.eval_['valid_acc'].append(valid_acc)

        return self
n_epochs = 200

## @Readers: PLEASE IGNORE IF-STATEMENT BELOW
##
## This cell is meant to run fewer epochs when
## the notebook is run on the Travis Continuous Integration
## platform to test the code on a smaller dataset
## to prevent timeout errors; it just serves a debugging tool

if 'TRAVIS' in os.environ:
    n_epochs = 20
# 执行代码之后,初始化了一个784-100-10的MLP。其具有784个输入单元,对应n_features为784,100个隐层单元(n_hidden),以及10个输出单元

使用55000个样本训练MLP,这些样本来自于已经打乱的MNIST训练数据集,并且在训练过程中使用剩下的5000个样本进行验证。

nn = NeuralNetMLP(n_hidden=100, 
                  l2=0.01, 
                  epochs=n_epochs, 
                  eta=0.0005,
                  minibatch_size=100, 
                  shuffle=True,
                  seed=1)

nn.fit(X_train=X_train[:55000], 
       y_train=y_train[:55000],
       X_valid=X_train[55000:],
       y_valid=y_train[55000:])
200/200 | Cost: 5065.78 | Train/Valid Acc.: 99.28%/97.98%  




<__main__.NeuralNetMLP at 0x1c20891a988>
import matplotlib.pyplot as plt


plt.plot(range(nn.epochs), nn.eval_['cost'])
plt.ylabel('Cost')
plt.xlabel('Epochs')
#plt.savefig('images/12_07.png', dpi=300)
plt.show()

在这里插入图片描述

可以看到,代价在前100个epoch期间大幅度下降,且貌似在100个epoch时缓慢收敛。但在175到200个epoch之间,下降缓慢,但依然呈现出下降趋势

# 可视化训练和验证的Accuracy变化情况
plt.plot(range(nn.epochs), nn.eval_['train_acc'], 
         label='Training')
plt.plot(range(nn.epochs), nn.eval_['valid_acc'], 
         label='Validation', linestyle='--')
plt.ylabel('Accuracy')
plt.xlabel('Epochs')
plt.legend(loc='lower right')
plt.savefig('images/12_08.png', dpi=300)
plt.show()

在这里插入图片描述

上图显示出,当epoch为50的时候,训练和验证accuracy相等,并在此之后,网络出现了过拟合情况。降低过拟合的有效措施可以是增加正则化强度,也可以使用丢弃法,即随机丢弃一部分神经元。and so on

# 通过计算模型在测试数据上的预测精度来评估模型的泛化性能
y_test_pred = nn.predict(X_test)
acc = (np.sum(y_test == y_test_pred)
       .astype(np.float) / X_test.shape[0])

print('Test accuracy: %.2f%%' % (acc * 100))
Test accuracy: 97.54%

尽管模型在训练数据集上具有轻微的过拟合情况,但相较于验证数据集的精度97.98%,模型依然在测试集上取得了不错的性能。

为了进一步微调模型,我们可以改变隐藏单元的数量,正则化参数的值,以及学习速率等;

自适应学习率、更加复杂的SGD、批量归一化、丢弃法

miscl_img = X_test[y_test != y_test_pred][:25]
correct_lab = y_test[y_test != y_test_pred][:25]
miscl_lab = y_test_pred[y_test != y_test_pred][:25]

fig, ax = plt.subplots(nrows=5, ncols=5, sharex=True, sharey=True)
ax = ax.flatten()
for i in range(25):
    img = miscl_img[i].reshape(28, 28)
    ax[i].imshow(img, cmap='Greys', interpolation='nearest')
    ax[i].set_title('%d) t: %d p: %d' % (i+1, correct_lab[i], miscl_lab[i]))

ax[0].set_xticks([])
ax[0].set_yticks([])
plt.tight_layout()
#plt.savefig('images/12_09.png', dpi=300)
plt.show()

在这里插入图片描述



3.训练一个人工神经网络

3.1计算Logistic损失函数

Logistic损失函数定义如下:
J ( w ) = − ∑ i = 1 n y [ i ] log ⁡ ( a [ i ] ) + ( 1 − y [ i ] ) log ⁡ ( 1 − a [ i ] ) J(\boldsymbol{w})=-\sum_{i=1}^{n} y^{[i]} \log \left(a^{[i]}\right)+\left(1-y^{[i]}\right) \log \left(1-a^{[i]}\right) J(w)=i=1ny[i]log(a[i])+(1y[i])log(1a[i])

这里的 a [ i ] a^{[i]} a[i]是在前向传播过程中,计算的数据集中第 i i i个样本的sigmoid激活:上标 [ i ] [i] [i]代表的是样本索引。

a [ i ] = ϕ ( z [ i ] ) a^{[i]}=\phi\left(z^{[i]}\right) a[i]=ϕ(z[i])

通过在上述损失函数上增加一项损失函数,来降低模型过拟合程度,如L2正则化:
L 2 = λ ∥ w ∥ 2 2 = λ ∑ j = 1 m w j 2 L 2=\lambda\|w\|_{2}^{2}=\lambda \sum_{j=1}^{m} w_{j}^{2} L2=λw22=λj=1mwj2

这样一来,就得到了如下的等式:
J ( w ) = − [ ∑ i = 1 n y [ i ] log ⁡ ( a [ i ] ) + ( 1 − y [ i ] ) log ⁡ ( 1 − a [ i ] ) ] + λ 2 ∥ w ∥ 2 2 J(\boldsymbol{w})=-\left[\sum_{i=1}^{n} y^{[i]} \log \left(a^{[i]}\right)+\left(1-y^{[i]}\right) \log \left(1-a^{[i]}\right)\right]+\frac{\lambda}{2}\|\boldsymbol{w}\|_{2}^{2} J(w)=[i=1ny[i]log(a[i])+(1y[i])log(1a[i])]+2λw22

在此之前,我们为多类分类实现了一个mlp,它返回t个元素的输出向量,我们需要将其与独热码编码表示中的𝑡×1维目标向量进行比较。如果我们使用此MLP预测具有类别标签2的输入图像的类别标签,则第三层和目标的活性值可能如下所示:
a ( o u t ) = [ 0.1 0.9 ⋮ 0.3 ] , y = [ 0 1 ⋮ 0 ] a^{(o u t)}=\left[\begin{array}{c} 0.1 \\ 0.9 \\ \vdots \\ 0.3 \end{array}\right], \quad y=\left[\begin{array}{c} 0 \\ 1 \\ \vdots \\ 0 \end{array}\right] a(out)=0.10.90.3,y=010

将上述损失函数推广到一般形式:
J ( W ) = − ∑ i = 1 n ∑ j = 1 t y j [ i ] log ⁡ ( a j [ i ] ) + ( 1 − y j [ i ] ) log ⁡ ( 1 − a j [ i ] ) J(\boldsymbol{W})=-\sum_{i=1}^{n} \sum_{j=1}^{t} y_{j}^{[i]} \log \left(a_{j}^{[i]}\right)+\left(1-y_{j}^{[i]}\right) \log \left(1-a_{j}^{[i]}\right) J(W)=i=1nj=1tyj[i]log(aj[i])+(1yj[i])log(1aj[i])

对应的,增加了正则项:
J ( W ) = − [ ∑ i = 1 n ∑ j = 1 t y j [ i ] log ⁡ ( a j [ i ] ) + ( 1 − y j [ i ] ) log ⁡ ( 1 − a j [ i ] ) ] + λ 2 ∑ l = 1 L − 1 ∑ i = 1 u l ∑ j = 1 u l + 1 ( w j , i ( l ) ) 2 J(\boldsymbol{W})=-\left[\sum_{i=1}^{n} \sum_{j=1}^{t} y_{j}^{[i]} \log \left(a_{j}^{[i]}\right)+\left(1-y_{j}^{[i]}\right) \log \left(1-a_{j}^{[i]}\right)\right]+\frac{\lambda}{2} \sum_{l=1}^{L-1} \sum_{i=1}^{u_{l}} \sum_{j=1}^{u_{l+1}}\left(w_{j, i}^{(l)}\right)^{2} J(W)=[i=1nj=1tyj[i]log(aj[i])+(1yj[i])log(1aj[i])]+2λl=1L1i=1ulj=1ul+1(wj,i(l))2

其中, u l u_{l} ul代表的是第 l l l层神经元个数,同时正则项表示如下:
λ 2 ∑ l = 1 L − 1 ∑ i = 1 u l ∑ j = 1 u l + 1 ( w j , i ( l ) ) 2 \frac{\lambda}{2} \sum_{l=1}^{L-1} \sum_{i=1}^{u_{l}} \sum_{j=1}^{u_{l+1}}\left(w_{j, i}^{(l)}\right)^{2} 2λl=1L1i=1ulj=1ul+1(wj,i(l))2

这里的目标是最小化 J ( W ) J(\boldsymbol{W}) J(W),因此需要计算损失函数关于 W \boldsymbol{W} W的偏导数:

∂ ∂ w j , i ( l ) J ( W ) \frac{\partial}{\partial w_{j, i}^{(l)}} J(\boldsymbol{W}) wj,i(l)J(W)

这里的 W \boldsymbol{W} W实际上是由很多矩阵组成,譬如: W ( h ) \boldsymbol{W}^{(h)} W(h) W ( o u t ) \boldsymbol{W}^{(out)} W(out),对于具有一个隐层的MLP,

其三维的张量 W \boldsymbol{W} W可视化即如果如下图所示:

# 几何表示形式
Image(filename='images/12_10.png', width=300) 

在这里插入图片描述



3.2反向传播算法backpropagation

在反向传播算法中,很常用的方法为链式求导法则,如对于嵌套函数 [ f ( g ( x ) ) ] [f(g(x))] [f(g(x))]

d d x [ f ( g ( x ) ) ] = d f d g ⋅ d g d x \frac{d}{d x}[f(g(x))]=\frac{d f}{d g} \cdot \frac{d g}{d x} dxd[f(g(x))]=dgdfdxdg

类似的,对于更长的复合函数 f ( g ( h ( u ( v ( x ) ) ) ) ) f(g(h(u(v(x))))) f(g(h(u(v(x)))))

d F d x = d d x F ( x ) = d d x f ( g ( h ( u ( v ( x ) ) ) ) ) = d f d g ⋅ d g d h ⋅ d h d u ⋅ d u d v ⋅ d v d x \frac{d F}{d x}=\frac{d}{d x} F(x)=\frac{d}{d x} f(g(h(u(v(x)))))=\frac{d f}{d g} \cdot \frac{d g}{d h} \cdot \frac{d h}{d u} \cdot \frac{d u}{d v} \cdot \frac{d v}{d x} dxdF=dxdF(x)=dxdf(g(h(u(v(x)))))=dgdfdhdgdudhdvdudxdv

逆向自动微分的诀窍是从右往左进行,将一个矩阵乘以一个向量,将产生一个向量,该向量再乘以下一个矩阵,以此类推。

矩阵-向量乘法在计算上比矩阵乘法代价小很多,这就是反向传播算法在神经网络训练中最常用的原因之一。

3.3利用反向传播训练神经网络

使用误差反向传播算法,首先需要经过前向传播计算得到输出层的活性值。计算过程如下:
Z ( h ) = A ( i n ) W ( h )  (net input of the hidden layer)  A ( h ) = ϕ ( Z ( h ) )  (activation of the hidden layer)  Z ( out  ) = A ( h ) W ( out  )  (net input of the output layer)  A ( out  ) = ϕ ( Z ( out  ) )  (activation of the output layer)  \begin{array}{l} \boldsymbol{Z}^{(h)}=\boldsymbol{A}^{(i n)} \boldsymbol{W}^{(h)} \quad \text { (net input of the hidden layer) }\\ \boldsymbol{A}^{(h)}=\phi\left(\boldsymbol{Z}^{(h)}\right) \quad \text { (activation of the hidden layer) }\\ \boldsymbol{Z}^{(\text {out })}=\boldsymbol{A}^{(h)} \boldsymbol{W}^{(\text {out })} \quad \text { (net input of the output layer) }\\ \boldsymbol{A}^{(\text {out })}=\phi\left(\boldsymbol{Z}^{(\text {out })}\right) \quad \text { (activation of the output layer) } \end{array} Z(h)=A(in)W(h) (net input of the hidden layer) A(h)=ϕ(Z(h)) (activation of the hidden layer) Z(out )=A(h)W(out ) (net input of the output layer) A(out )=ϕ(Z(out )) (activation of the output layer) 

更具体地,通过网络中的连接前向传播输入特征,几何表示如下:

Image(filename='./images/12_11.png', width=400) 

在这里插入图片描述

在反向传播中,我们从右到左传播错误。首先计算输出层的误差向量:
δ ( o u t ) = a ( o u t ) − y \boldsymbol{\delta}^{(o u t)}=\boldsymbol{a}^{(o u t)}-\boldsymbol{y} δ(out)=a(out)y

这里的 y \boldsymbol{y} y为真实类别标签的向量。

接下来,计算隐藏层的误差项:

δ ( h ) = δ ( out  ) ( W ( o u t ) ) T ⊙ ∂ ϕ ( z ( h ) ) ∂ z ( h ) \boldsymbol{\delta}^{(h)}=\boldsymbol{\delta}^{(\text {out })}\left(\boldsymbol{W}^{(o u t)}\right)^{T} \odot \frac{\partial \phi\left(z^{(h)}\right)}{\partial z^{(h)}} δ(h)=δ(out )(W(out))Tz(h)ϕ(z(h))

这里的 ∂ ϕ ( z ( h ) ) ∂ z ( h ) \frac{\partial \phi\left(z^{(h)}\right)}{\partial z^{(h)}} z(h)ϕ(z(h))简单来说,就是激活函数的导数:

∂ ϕ ( z ) ∂ z = ( a ( h ) ⊙ ( 1 − a ( h ) ) ) \frac{\partial \phi(z)}{\partial z}=\left(a^{(h)} \odot\left(1-a^{(h)}\right)\right) zϕ(z)=(a(h)(1a(h)))

这里的 ⊙ \odot 代表的是元素级别的乘法。

激活函数的导数
ϕ ′ ( z ) = ∂ ∂ z ( 1 1 + e − z ) = e − z ( 1 + e − z ) 2 = 1 + e − z ( 1 + e − z ) 2 − ( 1 1 + e − z ) 2 = 1 ( 1 + e − z ) − ( 1 1 + e − z ) 2 = ϕ ( z ) − ( ϕ ( z ) ) 2 = ϕ ( z ) ( 1 − ϕ ( z ) ) = a ( 1 − a ) \begin{aligned} \phi^{\prime}(z) &=\frac{\partial}{\partial z}\left(\frac{1}{1+e^{-z}}\right) \\ &=\frac{e^{-z}}{\left(1+e^{-z}\right)^{2}} \\ &=\frac{1+e^{-z}}{\left(1+e^{-z}\right)^{2}}-\left(\frac{1}{1+e^{-z}}\right)^{2} \\ &=\frac{1}{\left(1+e^{-z}\right)}-\left(\frac{1}{1+e^{-z}}\right)^{2} \\ &=\phi(z)-(\phi(z))^{2} \\ &=\phi(z)(1-\phi(z)) \\ &=a(1-a) \end{aligned} ϕ(z)=z(1+ez1)=(1+ez)2ez=(1+ez)21+ez(1+ez1)2=(1+ez)1(1+ez1)2=ϕ(z)(ϕ(z))2=ϕ(z)(1ϕ(z))=a(1a)

接下来,计算 δ \boldsymbol{\delta} δ层的误差矩阵:

δ ( h ) = δ ( o u t ) ( W ( o u t ) ) T ⊙ ( a ( h ) ⊙ ( 1 − a ( h ) ) ) \delta^{(h)}=\delta^{(o u t)}\left(\boldsymbol{W}^{(o u t)}\right)^{T} \odot\left(a^{(h)} \odot\left(1-a^{(h)}\right)\right) δ(h)=δ(out)(W(out))T(a(h)(1a(h)))

上面的等式,使用的矩阵转置 ( W ( o u t ) ) T \left(\boldsymbol{W}^{(o u t)}\right)^{T} (W(out))T,其中 ( W ( o u t ) ) \left(\boldsymbol{W}^{(o u t)}\right) (W(out)) h × t h \times t h×t的矩阵,

这里的 t t t为输出类别标签的个数, h h h为隐藏层单元的个数

最终在得到 δ \delta δ之后,可以将损失函数的导数写为如下形式:
∂ ∂ w i , j ( o u t ) J ( W ) = a j ( h ) δ i ( out  ) ∂ ∂ w i , j ( h ) J ( W ) = a j ( i n ) δ i ( h ) \begin{aligned} \frac{\partial}{\partial w_{i, j}^{(o u t)}} J(\boldsymbol{W}) &=a_{j}^{(h)} \delta_{i}^{(\text {out })} \\ \frac{\partial}{\partial w_{i, j}^{(h)}} J(\boldsymbol{W}) &=a_{j}^{(i n)} \delta_{i}^{(h)} \end{aligned} wi,j(out)J(W)wi,j(h)J(W)=aj(h)δi(out )=aj(in)δi(h)

Δ ( h ) = ( A ( i n ) ) T δ ( h ) Δ ( o u t ) = ( A ( h ) ) T δ ( o u t ) \begin{array}{l} \Delta^{(h)}=\left(\boldsymbol{A}^{(i n)}\right)^{T} \delta^{(h)} \\ \Delta^{(o u t)}=\left(\boldsymbol{A}^{(h)}\right)^{T} \delta^{(o u t)} \end{array} Δ(h)=(A(in))Tδ(h)Δ(out)=(A(h))Tδ(out)

继而添加正则项:
Δ ( l ) : = Δ ( l ) + λ ( l ) W ( l ) \Delta^{(l)}:=\Delta^{(l)}+\lambda^{(l)} \boldsymbol{W}^{(l)} Δ(l):=Δ(l)+λ(l)W(l)

最后,权重更新过程为:

W ( l ) : = W ( l ) − η Δ ( l ) \boldsymbol{W}^{(l)}:=\boldsymbol{W}^{(l)}-\eta \Delta^{(l)} W(l):=W(l)ηΔ(l)

几何形式表示如下:

Image(filename='images/12_12.png', width=500) 

在这里插入图片描述



4.神经网络的收敛性

使用小批量梯度下降,这是一种关于随机梯度下降和批量梯度下降的一种中和方法,

虽然其依然是一种随机方法,但它通常可以得到非常精确的解,而且比常规梯度下降算法收敛更快;

如下图所示,优化算法很容易陷入局部极小;

通过提高学习率,可以较容易地摆脱局部极小,但也增加了超过全局最优值的概率;

Image(filename='images/12_13.png', width=500) 

在这里插入图片描述

  • 7
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值