Course2-Week2-神经网络的训练方法
文章目录

1. 神经网络的编译和训练
1.1 TensorFlow实现
上周演示了如何定义一个神经网络并使用其进行“推理”,本周就来学习剩余的的“编译”和“训练”:
# compile()函数所有的可选选项及其示例
model.compile(loss=tf.keras.losses.BinaryCrossentropy(),
optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
metrics='accuracy'
)
参考Keras官网说明文档:The compile()
method: specifying a loss, metrics, and an optimizer。
熟悉了上面的选项之后,回到简化的“手写数字识别”问题,其训练代码如下,主要看“编译model.compile()
”和“训练model.fit()
”:
图2-2-1 训练神经网络的代码-手写数字识别
1.2 损失函数和代价函数的数学公式
图2-2-2 训练神经网络的步骤
上一节介绍了“编译”和“训练”的实现代码,那“编译”中所配置的“二元交叉熵损失函数”loss=BinaryCrossentropy()
到底是什么呢?其实,其定义和“逻辑回归”中的损失函数相同。因为在统计学中,损失函数形式是“交叉熵”(如下式),而“二元”只是强调输出为二元分类,所以称之为“二元交叉熵损失函数”。下面是“手写数字识别”神经网络的损失函数和代价函数:
Loss Function:
L
(
f
(
x
⃗
)
,
y
)
=
−
y
l
o
g
(
f
(
x
⃗
)
)
−
(
1
−
y
)
l
o
g
(
1
−
f
(
x
⃗
)
)
Cost Function:
{
J
(
W
,
B
)
=
1
m
∑
i
=
1
m
L
(
f
(
x
⃗
)
,
y
)
,
W
=
[
W
[
1
]
,
W
[
2
]
,
W
[
3
]
]
,
B
=
[
b
⃗
[
1
]
,
b
⃗
[
2
]
,
b
⃗
[
3
]
]
.
\begin{aligned} \text{Loss Function:} & \; L(f(\vec{x}),y) = -ylog(f(\vec{x})) - (1-y)log(1-f(\vec{x})) \\ \text{Cost Function:} & \; \left\{\begin{aligned} & J(\bold{W},\bold{B}) = \frac{1}{m}\sum_{i=1}^{m}L(f(\vec{x}),y), \\ & \bold{W}=[\bold{W}^{[1]}, \bold{W}^{[2]}, \bold{W}^{[3]}], \\ & \bold{B}=[\vec{\bold{b}}^{[1]}, \vec{\bold{b}}^{[2]}, \vec{\bold{b}}^{[3]}]. \end{aligned}\right. \end{aligned}
Loss Function:Cost Function:L(f(x),y)=−ylog(f(x))−(1−y)log(1−f(x))⎩
⎨
⎧J(W,B)=m1i=1∑mL(f(x),y),W=[W[1],W[2],W[3]],B=[b[1],b[2],b[3]].
在代码实现中,Keras库最初是独立的数学函数库,后来被TensorFlow收录,所以TensorFlow使用Keras库来定义损失函数。这也是最初的代码中,从keras导入损失函数的原因。当然记住所有的损失函数名称并不现实,需要的时候问问ChatGPT就行。当然上述是对“分类问题”的代码,如果想编译“回归问题”,可以采用下面的代码(注意修改损失函数):
# 回归问题中,使用“平方差损失函数”编译模型
from tensorflow.keras.losses import MeanSquareError
model.compile(loss = MeanSquareError())
上面已经介绍了损失函数的数学形式和代码。现在还有最后一点就是在迭代“训练”中,怎么求解代价函数的偏导。显然“训练”的过程就是最小化代价函数,比如使用“梯度下降”,那么每一次迭代都要计算代价函数对每一层每个神经元的每个参数的偏导。这工作量想想就惊人!更别说神经网络很难求出显式的偏导表达式。所以实际上,神经网络的“训练”使用“反向传播(back propogation)”来进行迭代,可以大大节省计算量。TensorFlow将上述“反向传播”寻找最低点的过程全部封装在model.fit()
函数中,并迭代设定好的次数epochs
,“5.反向传播”一节将进行介绍其数学原理。
现在知道如何训练一个基本的神经网络,也被称为“多层感知器”。下面几节将继续介绍一些改进,使神经网络的性能更好。
2. 其他的激活函数
再继续介绍“编译”和“训练”之前,我们来继续看看其他的“激活函数”。这可以首先帮助我们优化整个神经网络。
2.1 Sigmoid激活函数的替代方案
由于前面介绍了“逻辑回归”,于是我们将每个神经元都看成一个小的逻辑回归单元,所以目前一直使用Sigmiod函数作为隐藏层和输出层所有神经元的激活函数。但Sigmoid函数显然也有其局限性。比如下面的“需求预测”问题,我们最开始假设隐藏层输出的有“心理预期价格”、“认可度”、“产品质量”,但实际上这些指标并不一定都为0~1之间的小数字,比如“认可度”可以有“不认可”、“一般认可”、“极度认可”、“完全传播开来”等。于是我们不妨将“认可度”的范围扩展为到所有“非负数”,也就是从0到非常大的数字。也就是ReLU函数(Rectified Linear Unit, 修正线性单元):
图2-2-3 ReLU函数-需求预测
于是,这启示我们,激活函数的形状应该和输出范围有关,下面就是三种常见的激活函数:
图2-2-4 常见的三种激活函数
合理使用上述激活函数可以进一步提升神经网络性能,进而构建各种各样的强大的神经网络。
2.2 如何选择激活函数
那该如何选择激活函数呢?首先我们从输出层开始。输出层的激活函数通常取决于“目标值”,比如二元分类问题就使用Sigmoid函数、如果预测股票涨跌的回归问题就使用线性激活函数(因为有正有负)、如果是“预测房价”这样的回归问题就使用ReLU函数(因为房价非负)。而对于隐藏层来说,若无特殊原因,所有隐藏层的神经元都应选择ReLU函数。最早人们都默认使用Sigmoid函数,但是现在逐渐演变成默认使用ReLU函数,主要有以下几个原因:
图2-2-5 改进输出层、隐藏层的激活函数——手写数字识别
对于大多数情况、大多数应用来说,上述介绍的几个激活函数已经够用了。当然还有其他激活函数,比如Tanh函数、Leaky ReLU函数、Swish函数等。每隔几年,研究人员都会提出新的激活函数,这些激活函数在某些方面的性能确实会更好。想了解的话可以自行查阅。
2.3 为什么需要激活函数
既然ReLU函数比Sigmoid函数计算更快,那能不能默认使用更简洁的“线性激活函数”呢?换句话说,能不能不使用激活函数呢?显然是不行的。由于“线性函数的线性组合还是线性函数”,若只使用线性激活函数,整个神经网络就会退化成“线性回归”,所以 尽量不在隐藏层使用线性激活函数。使用其他的非线性函数,比如ReLU函数,只要有足够多的神经元,通过调整参数就可以拟合出任意的曲线,进而实现神经网络想要的拟合效果。
下面是ChatGPT给出的激活函数的几个作用:
3. 多分类问题和Softmax函数
本节来进一步优化“编译compile()
”的“损失函数loss
”设置。在第一节中,由于“手写数字识别”已经被我们简化成只识别数字1和数字0,所以使用了“二元交叉熵损失函数loss=BinaryCrossentropy()
”,那如果要识别全部的数字0~数字9,又该如何设置损失函数呢?下面就来介绍。
3.1 多分类问题
显然,要识别全部的数字0~数字9,就需要将二元分类问题进行推广。于是,分类输出的目标值有多个可能的问题,便称为“多分类问题(multiclass classfication problem)”。比如“手写数字识别”要分类10个数字、“肿瘤检测”要分类多种恶化类型、流水线产品检测不同类型的缺陷等。
图2-2-6 二元分类推广到多分类
对于多分类问题,显然希望决策边界如上右图所示。于是下节介绍由二元分类的“逻辑回归算法”推广得到的“Softmax回归算法”,可以解决上述“多分类问题”,并将其应用到神经网络中。
3.2 逻辑回归的推广:Softmax回归
下面是Softmax函数的定义,也就是 g ( z ) g(z) g(z) 的定义:
图2-2-7 Softmax回归算法的模型
于是类似于“逻辑回归”的“二元交叉熵(Binary Crossen tropy)损失函数”,“Softmax回归”的损失函数为“稀疏分类交叉熵(Sparse Categorical Crossen tropy)”。同样也是,推理结果越接近真实结果,损失越小:
图2-2-8 Softmax回归算法的损失函数
3.3 Softmax神经网络的代码实现
显然,要创建解决多分类问题的神经网络,只需要更改输出层激活函数为“Softmax函数”、损失函数为“稀疏分类交叉熵”即可,代码如下:
图2-2-9 Softmax回归神经网络(实际不使用此代码)-10分类的手写数字识别
注意到,Softmax层,也被称为“Softmax激活函数”,和之前的激活函数都不同。因为Softmax函数的输出 a j a_j aj 不只取决于当前的输入 z j z_j zj,而是取决于所有的输入 z 1 , . . . , z N z_1,...,z_N z1,...,zN。所以Softmax层并不能逐个计算神经元的输出,而是需要“同时计算”该层所有神经元的输出。
虽然上述代码规定好了损失函数,但实际上并不采用上述代码。因为采用上面的代码,会要求TensorFlow保留损失函数计算的中间结果 a j , j = 1 , . . , N . a_j,j=1,..,N. aj,j=1,..,N.。但实际上,多计算一个变量就会引入更多的舍入误差,尤其是“Sigmoid函数 1 1 + e − z \frac{1}{1+e^{-z}} 1+e−z1”、“Softmax函数 e z j ∑ k = 1 N e z k \frac{e^{z_j}}{\sum_{k=1}^N e^{z_k}} ∑k=1Nezkezj”的计算结果中,其总会有些值非常小,进而使得计算精度不足导致 舍入误差。为了消除这部分的舍入误差,我们可以令TensorFlow不要再计算中间结果 z z z,而是直接将 z z z 的表达式代入到损失函数中。这样TensorFlow会先化简损失函数表达式(重排列),再进行计算,从而减少一步的舍入误差:
图2-2-10 更精确的的代码实现——二元分类问题
图2-2-11 更精确的的代码实现——多分类问题
3.4 多标签问题
最后,“多分类问题(multi-classfication problem)”和“多标签问题(multi-label problem)”非常容易混淆,本小节就是来区分一下这两个定义。“多标签分类”是一个输入,多个不同类型的分类。所以,“多标签问题”可以看成是多个“二元分类”的组合。比如下面的“多标签问题”就是由3个不同的“二元分类”组成的:
图2-2-12 多标签问题示意图
直观上,我们可能会想到针对不同的标签分类,创建各自独立的的神经网络,比如上图就创建三个独立的神经网络,但显然这看起来不聪明,所以实际上会创建一个神经网络同时解决这三个二元分类。创建网络时只要注意将最后的输出层调整为三个Sigmoid神经元即可。
下面来介绍更加高级的神经网络概念,包括比“梯度下降”更好的迭代算法。
4. 更加高级的神经网络概念
4.1 梯度下降法的改进:Adam算法
“梯度下降”广泛应用于机器学习算法中,如线性回归、逻辑回归、神经网络早期实现等,但是还有其他性能更好的最下滑代价函数的算法。比如“Adam算法(Adaptive Moment estimation, 自适应矩估计)”就可以自动调整“梯度下降”学习率 α \alpha α 的大小,从而加快“梯度下降”的收敛速度。下面是其算法逻辑和代码示例:
图2-2-13 Adam算法逻辑及实现代码
有兴趣可以试着调大全局学习率learning_rate
,看看Adam算法能否学的更快。
4.2 其他类型的神经网络层
目前为止学习到的所有神经网络都是“密集层类型(dense layer type)”,也就是层内的每一个神经元都会得到上一层的所有输入特征。虽然“密集层类型”的神经网络功能很强大,但是其计算量很大。于是,Yann LeCun 最早提出“卷积层(convolution layer)”,并将其应用到计算机视觉领域。“卷积层”中,每个神经元只关心输入图像的某个区域。如下左图中,对于输入图像,每个神经元只关心某个小区域(按照颜色对应)。“卷积层”的优点如下:
图2-2-14 卷积层、卷积神经网络示意图
如果神经网络有很多“卷积层”,就会称之为“卷积神经网络”(如上右图)。上右图便给出了“心电图监测”问题(本小节开始的说明)的卷积神经网络示意图。显然,改变每个神经元查看的窗口大小,每一个有多少神经元,有效的选择这些参数,可以构建比“密集层类型”更加有效的神经网络。
除了“卷积层”之外,当然还有其他的神经网络层类型,将不同类型的“层”结合在一起,组成更加强大的神经网络:
4.3 神经网络代码实例——手写数字识别
图2-2-15 训练集和神经网络模型-手写数字识别
到本节为止,本周与代码有关的部分就介绍完了。现在根据本周实验,给出完整的“手写数字识别”代码,并在后面给出了神经网络参数及预测结果。代码的整体逻辑是,使用训练集进行训练;训练完毕后,再从训练集中挑出一些图片,看看训练后的神经网络能否正确预测。所以最后的“训练错误汇总”是因为迭代次数过少导致:
# 导入头文件
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.activations import linear, relu, sigmoid
import matplotlib.pyplot as plt
plt.style.use('./deeplearning.mplstyle')
import logging
logging.getLogger("tensorflow").setLevel(logging.ERROR)
tf.autograph.set_verbosity(0)
# 加载训练集:5000张手写体图片,每张图片大小20x20
X = np.load("data/X.npy") # 5000x400
y = np.load("data/y.npy") # 5000x1
# 定义神经网络
model = Sequential(
[
tf.keras.Input(shape=(400,)), # 输入特征的长度
Dense(units=25, activation='relu', kernel_regularizer=tf.keras.regularizers.l2(0.1), name='layer1'),
Dense(units=15, activation='relu', kernel_regularizer=tf.keras.regularizers.l2(0.1), name='layer2'),
Dense(units=10, activation='linear', name='layer3'),
], name = "my_model"
)
# 编译模型
model.compile(
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
optimizer=tf.keras.optimizers.Adam(learning_rate=0.001)
)
# 训练模型
history = model.fit(X, y, epochs=40)
# 预测结果
image_new = X[1015]
prediction = model.predict(image_new.reshape(1,400)) # prediction
yhat = np.argmax(prediction)
prediction_p = tf.nn.softmax(prediction)
print(f" Largest Prediction index: {yhat}")
print(f"The Probability of Largest index: {prediction_p[0,yhat]:0.2f}")
图2-2-16 “定义神经网络”后的神经网络参数
图2-2-17 “预测”单个图片
图2-2-18 预测错误汇总
5. 反向传播(选修)
“反向传播”是“自动微分(auto-diff)算法”的一种。本节介绍TensorFlow如何使用“反向传播”,计算神经网络的代价函数对所有参数的偏导。
5.1 计算图和导数
显然,由于神经网络可以创建的相当庞大,所以很难显式的写出偏导表达式。此时,我们求解代价函数偏导的思路就转成,求解代价函数在该点切线的斜率,也就是使用很小的步长来近似当前点的切线斜率。但是神经网络通常又有很多层,于是对于每个神经元的每个参数都通过这种方式,直接迭代一次神经网络求解对一个参数的偏导,显然也不现实。于是,我们先通过“计算图(Computation Graph)”来研究一下神经网络的计算流程,找找灵感,比如下面对于单神经元网络的代价函数计算:
图2-2-19 计算代价函数的偏导-单神经元
J ( w , b ) = 1 2 ( ( w x + b ) − y ) 2 ⇒ ∂ J ∂ w = ∂ d ∂ w ∂ J ∂ d = ∂ a ∂ w ∂ d ∂ a ∂ J ∂ d = ∂ c ∂ w ∂ a ∂ c ∂ d ∂ a ∂ J ∂ d \begin{aligned} J(w,b) &= \frac{1}{2}((wx+b)-y)^2\\ \Rightarrow \frac{\partial J}{\partial w} &= \frac{\partial d}{\partial w} \frac{\partial J}{\partial d} = \frac{\partial a}{\partial w} \frac{\partial d}{\partial a} \frac{\partial J}{\partial d} = \frac{\partial c}{\partial w} \frac{\partial a}{\partial c} \frac{\partial d}{\partial a} \frac{\partial J}{\partial d} \end{aligned} J(w,b)⇒∂w∂J=21((wx+b)−y)2=∂w∂d∂d∂J=∂w∂a∂a∂d∂d∂J=∂w∂c∂c∂a∂a∂d∂d∂J
也就是,将整体的计算过程拆成一步一步的,就组成了“计算图”。从左到右,求解当前点的代价函数大小很简单。但注意到,如果我们想要求解代价函数的表达式时,还可以根据上述“计算图”写出求导的“链式法则(chain rule)”!!而且“链式法则”的顺序正好是从右向左的!!由于越靠右的节点也会被更多的参数共用,于是反向传播一次,便可以把依次把路径上的每一个参数的偏导都求解出来。而不是针对每个参数,都额外增加一点,通过从左到右计算代价函数的变化率来求得相应参数的偏导。于是,若“计算图”总共有 N N N 个节点、 P P P 个输入参数,要计算所有函数偏导:
5.2 神经网络中的反向传播
本节就来将上一节的想法应用到大型神经网络中。显然可以如下图所示,先“前向传播”计算神经网络中所有神经元的取值,然后一次“反向传播”遍历走完所有节点,即可计算出神经网络的代价函数对每个神经元的每个参数的偏导。显然,此时“计算图”实际上就相当于神经网络:
图2-2-20 计算代价函数的偏导-两层神经网络