用Python实现神经网络(附完整代码)!

2020-12-07 10:09:16 浏览数 (1)

作者:[美]霍布森·莱恩,科尔·霍华德

在学习神经网络之前,我们需要对神经网络底层先做一个基本的了解。我们将在本节介绍感知机、反向传播算法以及多种梯度下降法以给大家一个全面的认识。

一、感知机

数字感知机的本质是从数据集中选取一个样本(example),并将其展示给算法,然后让算法判断“是”或“不是”。一般而言,把单个特征表示为xi,其中i是整数。所有特征的集合表示为

X

,表示一个向量:

X = [x_1, x_2, ..., x_n]

类似地,每个特征的权重表示为

w_{i},

其中

i

对应于与该权重关联的特征

X

的下标,所有权重可统一表示为 一个向量

W

:

boldsymbol{W}=left[w_{1}, w_{2}, cdots, w_{i}, cdots, w_{n}right]
left(x_{1} w_{1}right) left(x_{2} w_{2}right) cdots left(x_{i} w_{i}right) cdots

这里有一个缺少的部分是是否激活神经元的阈值。一旦加权和超过某个阈值,感知机就输出1,否则输出0。我们可以使用一个简单的阶跃函数(在图5-2中标记为“激活函数”)来表示这个阈值。

一般而言我们还需要给上面的阈值表达式添加一个偏置项以确保神经元对全0的输入具有弹性,否则网络在输入全为0的情况下输出仍然为0。

注:所有神经网络的基本单位都是神经元,基本感知机是广义神经元的一个特例,从现在开始,我们将感知机称为一个神经元。

二、反向传播算法

2.1 代价函数

很多数据值之间的关系不是线性的,也没有好的线性回归或线性方程能够描述这些关系。许多数据集不能用直线或平面来线性分割。比如下图中左图为线性可分的数据,而右图为线性不可分的数据:

在这个线性可分数据集上对两类点做切分得到的误差可以收敛于0,而对于线性不可分的数据点集,我们无法做出一条直线使得两类点被完美分开,因此我们任意做一条分割线,可以认为在这里误差不为0,因此我们需要一个衡量误差的函数,通常称之为代价函数:

text{err}(x) = |y-f(x)|

而我们训练神经网络(感知机)的目标是最小化所有输入样本数据的代价函数

J(x) = text{min}sum_{i=1}^ntext{err}(x_i)

2.2 反向传播

权重

w_{1i}

通过下一层的权重(

w_{1j}

)和(

w_{2j}

)来影响误差,因此我们需要一种方法来计算对

w_{1i}

误差的贡献,这个方法就是反向传播

下图中展示的是一个全连接网络,图中没有展示出所有的连接,在全连接网络中,每个输入元素都与下一层的各个神经元相连,每个连接都有相应的权重。因此,在一个以四维向量为输入、有5个神经元的全连接神经网络中,一共有20个权重(5个神经元各连接4个权重)。

感知机的每个输入都有一个权重,第二层神经元的权重不是分配给原始输入的,而是分配给来自第一层的各个输出。从这里我们可以看到计算第一层权重对总体误差的影响的难度。第一层权重对误差的影响并不是只来自某个单独权重,而是通过下一层中每个神经元的权重来产生的。反向传播的推导过程较为复杂,这里仅简单展示其结果:

如果该层是输出层,借助于可微的激活函数,权重的更新比较简单, 对于第

j

个输出,误差的导数如下

Delta w_{i j}=-alpha frac{partial text {Error}}{w_{i j}}=-alpha y_{i}left(y_{j}-f(x)_{j}right) y_{j}left(1-y_{j}right)

如果要更新隐藏层的权重,则会稍微复杂一点儿:

Delta w_{i j}=-alpha frac{partial Error}{partial w_{i j}}=-alpha y_{i}left(sum_{l in L} delta_{l} w_{j l}right) y_{j}left(1-y_{j}right)

函数

f

表示实际结果向量,

f_j

表示该向量第

j

个位置上的值,

y_i

y_j

是倒数第二层第

i

个节点和输出第

j

个节点的输出,连接这两个节点的权重为

w_{ij}

,误差代价函数对

w_{ij}

求导的结果相当于用

alpha

(学习率)乘以前一层的输出再乘以后一层代价函数的导数。公式中

delta_l

表示

L

层第

l

个节点上的误差项,前一层第

j

个节点到

L

层所有的节点进行加权求和。

2.3 多种梯度下降法

到目前为止,我们一直是把所有训练样本的误差聚合起来然后再做梯度下降,这种训练方法称为批量学习(batch learning)。一批是训练数据的一个子集。但是在批量学习中误差曲面对于整个批是静态的,如果从一个随机的起始点开始,得到的很可能是某个局部极小值,从而无法看到其他的权重值的更优解。这里有两种方法来避开这个陷阱。

第一种方法是随机梯度下降法。在随机梯度下降中,不用去查看所有的训练样本,而是在输入每个训练样本后就去更新网络权重。在这个过程中,每次都会重新排列训练样本的顺序,这样将为每个样本重新绘制误差曲面,由于每个相异的输入都可能有不同的预期答案,因此大多数样本的误差曲面都不一样。对每个样本来说,仍然使用梯度下降法来调整权重。不过不用像之前那样在每个训练周期结束后聚合所有误差再做权重调整,而是针对每个样本都会去更新一次权重。其中的关键点是,每一步都在向假定的极小值前进(不是所有路径都通往假定的极小值)。

使用正确的数据和超参数,在向这个波动误差曲面的各个最小值前进时,可以更容易地得到全局极小值。如果模型没有进行适当的调优,或者训练数据不一致,将导致原地踏步,模型无法收敛,也学不会任何东西。不过在实际应用中,随机梯度下降法在大多数情况下都能有效地避免局部极小值。这种方法的缺点是计算速度比较慢。计算前向传播和反向传播,然后针对每个样本进行权重更新,这在本来已经很慢的计算过程的基础上又增加了很多时间开销。

第二种方法,也是更常见的方法,是小批量学习。在小批量学习中,会传入训练集的一个小的子集,并按照批量学习中的误差聚合方法对这个子集对应的误差进行聚合。然后对每个子集按批将其误差进行反向传播并更新权重。下一批会重复这个过程,直到训练集处理完成为止,这就重新构成了一个训练周期。这是一种折中的办法,它同时具有批量学习(快速)和随机梯度下降(具有弹性)的优点。

三、Keras:用Python实现神经网络

用原生Python来编写神经网络是一个非常有趣的尝试,而且可以帮助大家理解神经网络中的各种概念,但是Python在计算速度上有明显缺陷,即使对于中等规模的网络,计算量也会变得非常棘手。不过有许多Python库可以用来提高运算速度,包括PyTorch、Theano、TensorFlow和Lasagne等。本书中的例子使用Keras。

Keras是一个高级封装器,封装了面向Python的API。API接口可以与3个不同的后端库相兼容:Theano、谷歌的TensorFlow和微软的CNTK。这几个库都在底层实现了基本的神经网络单元和高度优化的线性代数库,可以用于处理点积,以支持高效的神经网络矩阵乘法运算。

我们以简单的异或问题为例,看看如何用Keras来训练这个网络。

代码语言:javascript复制
import numpy as np
from keras.models import Sequential  # Kera的基础模型类
from keras.layers import Dense, Activation  # Dense是神经元的全连接层
from keras.optimizers import SGD  # 随机梯度下降,Keras中还有一些其他优化器
# Our examples for an exclusive OR.
x_train = np.array([[0, 0],
                    [0, 1],
                    [1, 0],
                    [1, 1]])   # x_train是二维特征向量表示的训练样本列表
y_train = np.array([[0],
                    [1],
                    [1],
                    [0]])   # y_train是每个特征向量样本对应的目标输出值
model = Sequential()
num_neurons = 10  # 全连接隐藏层包含10个神经元
model.add(Dense(num_neurons, input_dim=2))   #  input_dim仅在第一层中使用,后面的其他层会自动计算前一层输出的形状,这个例子中输入的XOR样本是二维特征向量,因此input_dim设置为2
model.add(Activation('tanh'))
model.add(Dense(1))   # 输出层包含一个神经元,输出结果是二分类值(0或1)
model.add(Activation('sigmoid'))
model.summary()

可以看到模型的结构为:

代码语言:javascript复制
Layer (type)                 Output Shape                Param 
=================================================================
dense_18 (Dense)             (None, 10)                  30
_________________________________________________________________
activation_6 (Activation)    (None, 10)                  0
_________________________________________________________________
dense_19 (Dense)             (None, 1)                   11
_________________________________________________________________
activation_7 (Activation)    (None, 1)                   0
=================================================================
Total params: 41.0
Trainable params: 41.0
Non-trainable params: 0.0

model.summary()提供了网络参数及各阶段权重数(Param #)的概览。我们可以快速计算一下:10个神经元,每个神经元有3个权重,其中有两个是输入向量的权重(输入向量中的每个值对应一个权重),还有一个是偏置对应的权重,所以一共有30个权重需要学习。输出层中有10个权重,分别与第一层的10个神经元一一对应,再加上1个偏置权重,所以该层共有11个权重。

下面的代码可能有点儿不容易理解:

代码语言:javascript复制
sgd = SGD(lr=0.1)
model.compile(loss='binary_crossentropy', optimizer=sgd, metrics=['accuracy'])

SGD是之前导入的随机梯度下降优化器,模型用它来最小化误差或者损失lr是学习速率,与每个权重的误差的导数结合使用,数值越大模型的学习速度越快,但可能会使模型无法找到全局极小值,数值越小越精确,但会增加训练时间,并使模型更容易陷入局部极小值。损失函数本身也定义为一个参数,在这里用的是binary_crossentropymetrics参数是训练过程中输出流的选项列表。用compile方法进行编译,此时还未开始训练模型,只对权重进行了初始化,大家也可以尝试一下用这个随机初始状态来预测,当然得到的结果只是随机猜测:

代码语言:javascript复制
model.predict(x_train)
[[ 0.5       ]
 [ 0.43494844]
 [ 0.50295198]
 [ 0.42517585]]

predict方法将给出最后一层的原始输出,在这个例子中是由sigmoid函数生成的。

之后再没什么好写的了,但是这里还没有关于答案的任何知识,它只是对输入使用了随机权重。接下来可以试着进行训练。

代码语言:javascript复制
model.fit(x_train, y_train, epochs=100)   # 从这里开始训练模型
Epoch 1/100
4/4 [==============================] - 0s - loss: 0.6917 - acc: 0.7500
Epoch 2/100
4/4 [==============================] - 0s - loss: 0.6911 - acc: 0.5000
Epoch 3/100
4/4 [==============================] - 0s - loss: 0.6906 - acc: 0.5000
...
Epoch 100/100
4/4 [==============================] - 0s - loss: 0.6661 - acc: 1.0000

提示

在第一次训练时网络可能不会收敛。第一次编译可能以随机分布的参数结束,导致难以或者不能得到全局极小值。如果遇到这种情况,可以用相同的参数再次调用model.fit,或者添加更多训练周期,看看网络能否收敛。或者也可以用不同的随机起始点来重新初始化网络,然后再次尝试fit。如果使用后面这种方法,请确保没有设置随机种子,否则只会不断重复同样的实验结果。

当网络一遍又一遍地学习这个小数据集时,它终于弄明白了这是怎么回事。它从样本中“学会”了什么是异或!这就是神经网络的神奇之处。

代码语言:javascript复制
model.predict_classes(x_train)
4/4 [==============================] - 0s
[[0]
 [1]
 [1]
 [0]]
model.predict(x_train)
4/4 [==============================] - 0s
[[ 0.0035659 ]
 [ 0.99123639]
 [ 0.99285167]
 [ 0.00907462]]

在这个经过训练的模型上再次调用predict(和predict_classes)会产生更好的结果。它在这个小数据集上获得了 100%的精确度。当然,精确率并不是评估预测模型的最佳标准,但对这个小例子来说完全可以说明问题。接下来展示了如何保存这个异或模型:

代码语言:javascript复制
import h5py
model_structure = model.to_json()  # 用Keras的辅助方法将网络结构导出为JSON blob类型以备后用

with open("basic_model.json", "w") as json_file:
	json_file.write(model_structure)

model.save_weights("basic_weights.h5")  # 训练好的权重必须被单独保存。第一部分只保存网络结构。在后面重新加载网络结构时必须对其重新实例化

同样也有对应的方法来重新实例化模型,这样做预测时不必再去重新训练模型。虽然运行这个模型只需要几秒,但是在后面的章节中,模型的运行时间将会快速增长到以分钟、小时甚至天为单位,这取决于硬件性能和模型的复杂度,所以请准备好!

0 人点赞