TensorFlow 深度学习入门指南(三)
七、反向传播算法
回顾的时间越长,就能向前看的越远。
—丘吉尔
在第六章中,我们已经系统地介绍了基本的神经网络算法:从输入输出的表示出发;介绍感知器模型、多输入多输出全连接层;然后扩展到多层神经网络。我们还介绍了不同场景下输出层的设计和常用的损失函数及其实现。
本章我们将从理论层面学习神经网络中的核心算法之一:误差反向传播(BP)。实际上,反向传播算法早在 60 年代初就已经提出,但一直没有引起业界的重视。1970 年,Seppo Linnainmaa 在其硕士论文中提出了自动链推导方法,并实现了反向传播算法。1974 年,Paul Werbos 在其博士论文中首次提出了将反向传播算法应用于神经网络的可能性,但遗憾的是,Paul Werbos 并未发表后续的相关研究。事实上,Paul Werbos 认为这种研究思路对于解决感知机问题是有意义的,但由于人工智能的寒冬,社区普遍失去了解决那些问题的信念。直到大约 10 年后的 1986 年,Geoffrey Hinton 等人将反向传播算法应用于神经网络[1],使得反向传播算法在神经网络界轰轰烈烈。
通过深度学习框架的自动推导和自动参数更新功能,算法设计人员无需深入了解反向传播算法就可以构建复杂的模型和网络,并可以通过调用优化工具轻松训练网络模型。然而,反向传播算法和梯度下降算法是神经网络的核心,深入理解其原理非常重要。我们先回顾导数、梯度等数学概念,然后推导常用的激活函数和损失函数的梯度形式,开始逐步推导感知器和多层神经网络的梯度传播方法。如果你想刷新你的记忆或者学习更多的线性代数和微积分知识,[2]和[3]有更多的细节。
7.1 导数和梯度
高中的时候我们接触到了导数的概念,定义为当自变量 x 产生轻微扰动时∈x趋近于零时函数输出值的增量∈y与自变量 x 的增量∈x之比的极限:
函数 f ( x )的导数可以写成f’(x)或者。从几何的角度来说,一元函数的导数就是这里函数的切线的斜率,也就是函数值沿着 x 方向的变化率。考虑一个物理学上的例子,比如自由落体运动的位移函数的表达式
。对时间的导数是
。考虑到速度 v 定义为位移的变化率,那么位移对时间的导数就是速度,即 v = gt 。
事实上,导数是一个非常宽泛的概念。因为我们之前遇到的函数大多是一元函数,所以自变量只有两个方向: x + 和x-。当函数的自变量个数大于 1 时,函数导数的概念推广到函数值在任意方向的变化率。导数本身是标量,没有方向,但是导数表征了函数值在某个方向上的变化率。在这些任意方向中,沿着坐标轴的几个方向比较特殊,也叫偏导数。对于一元函数,导数写成。对于多元函数的偏导数,记为
。偏导数是导数的特例,没有方向。
考虑一个本质上是多元函数的神经网络模型,比如 shape [784,256]的一个权重矩阵 W ,其中包含 784 × 256 的连接权重,我们需要求 784 × 256 的偏导数。需要注意的是,在数学表达习惯中,要讨论的自变量一般记为 x ,但在神经网络中,一般用来表示输入,如图片、文本、语音数据等。网络的自变量是网络参数集 θ = { w 1 、 b 1 、 w 2 、 b 2 、⋯}.当使用梯度下降算法优化网络时,需要请求网络的所有偏导数。所以我们也关注误差函数 L 沿自变量 θ i 方向输出的导数,即。用向量形式写出函数的所有偏导数:
梯度下降算法可以以矢量的形式更新:
η是学习率。梯度下降算法一般是求函数 L 的最小值,有时也希望求函数的最大值,这就需要按如下方式更新梯度:
这种更新方法称为梯度上升算法。梯度下降算法和梯度上升算法在原理上是相同的。一个是向渐变的反方向更新,一个是向渐变的方向更新。都需要解偏导数。这里向量称为函数的梯度,由所有偏导数组成,代表方向。梯度的方向表示函数值上升最快的方向,梯度的反向表示函数值下降最快的方向。
梯度下降算法不能保证全局最优解,这主要是由于目标函数的非凸性造成的。考虑图 7-1 中的非凸函数。深蓝色区域是最小区域。不同的优化轨迹可能获得不同的最优数值解。这些数值解不一定是全局最优解。
图 7-1
非凸函数示例
神经网络模型表达式通常非常复杂,模型参数可达数千万或上亿级。几乎所有的神经网络优化问题都依赖于深度学习框架来自动计算网络参数的梯度,然后使用梯度下降来迭代优化网络参数,直到性能满足要求。深度学习框架中实现的主要算法是反向传播和梯度下降算法。所以了解这两种算法的原理有助于理解深度学习框架的作用。
在介绍多层神经网络的反向传播算法之前,我们首先介绍导数的公共属性、公共激活函数的梯度导数和损失函数,然后推导多层神经网络的梯度传播规律。
7.2 衍生产品的共同属性
本节介绍常用函数的求导规则和示例说明,为神经网络相关函数的求导做铺垫。
7.2.1 常见衍生工具
-
常数函数 c 的导数为 0。比如 y = 2 的导数就是
。
-
线性函数 y = ax + c 的导数为 a 。比如 y = 2 x + 1 的导数就是
。
-
函数xT3a的导数为axa—1。比如y=x2的导数就是
。
-
指数函数的导数ax是axln ln a。比如y=ex的导数就是
-
对数函数 x 的导数为
。比如 y = lnln x 的导数就是
7.2.2 衍生产品的共同属性
-
(f+g)’=f’+g’
-
(fg)=【F5】【g】+【f】
-
、g0
-
Consider function of function f (g(x)), let u = g(x), the derivative is:
7.2.3 实践衍生产品发现
考虑目标函数l=x⋅w2+b2,其导数为:
考虑目标函数l=x⋅ew+eb,其导数为:
考虑到目标函数L=y-(xw+b)2=[(xw+b)-y*-2,设g=xw+b*
考虑目标函数L*=AlN(xw+b),设g=xw+b,导数为:
![
7.3 激活函数的导数
这里我们介绍神经网络中常用的激活函数的推导。
7 . 3 . 1 Sigmoid 函数的导数
乙状结肠函数的表达式为:
让我们推导出 Sigmoid 函数的导数表达式:
可以看出,Sigmoid 函数的导数表达式最终可以表示为激活函数输出值的简单运算。利用这个性质,我们可以在神经网络的梯度计算中,通过缓存各层 Sigmoid 函数的输出值来计算它的导数。Sigmoid 函数的导函数如图 7-2 所示。
图 7-2
Sigmoid 函数及其导数
为了帮助理解反向传播算法的实现细节,本章选择不使用 TensorFlow 的自动求导功能。本章使用 Numpy 实现了一个由反向传播算法优化的多层神经网络。这里,Sigmoid 函数的导数由 Numpy 实现:
import numpy as np # import numpy library
def sigmoid(x): # implement sigmoid function
return 1 / (1 + np.exp(-x))
def derivative(x): # calculate derivative of sigmoid
# Using the derived expression of the derivatives
return sigmoid(x)*(1-sigmoid(x))
ReLU 函数的导数
回忆一下 ReLU 函数的表达式:
其导数的推导很简单:
可以看出 ReLU 函数的导数计算比较简单。当 x 大于或等于零时,导数值总是 1。在反向传播过程中,既不会放大梯度,造成梯度爆炸,也不会缩小梯度,造成梯度消失现象。ReLU 函数的导数曲线如图 7-3 所示。
图 7-3
ReLU 函数及其导数
在 ReLU 函数被广泛使用之前,神经网络中的激活函数大多是 Sigmoid。然而,Sigmoid 函数倾向于梯度分散。当网络的层数增加时,由于梯度值变得很小,网络的参数不能有效地更新。导致无法训练更深层次的神经网络,导致神经网络的研究停留在浅层次。ReLU 函数的引入,很好地缓解了梯度分散现象,神经网络的层数可以达到更深的层。比如 AlexNet 中使用 ReLU 激活功能,层数达到八层。一些超过 100 层的卷积神经网络也大多使用 ReLU 激活函数。
通过 Numpy,我们可以很容易地实现 ReLU 函数的求导,代码如下:
def derivative(x): # Derivative of ReLU
d = np.array(x, copy=True)
d[x < 0] = 0
d[x >= 0] = 1
return d
7 . 3 . 3 leaky relu 函数的导数
回忆 LeakyReLU 函数的表达式:
其导数可以推导为:
它与 ReLU 函数不同是因为当 x 小于零时,LeakyReLU 函数的导数值不是 0,而是一个常数 p,一般设置为较小的值,如 0.01 或 0.02。LeakyReLU 函数的导数曲线如图 7-4 所示。
图 7-4
LeakyReLU 函数及其导数
LeakyReLU 函数有效地克服了 ReLU 函数的缺陷,也得到广泛的应用。我们可以通过 Numpy 实现 LeakyReLU 函数的导数如下:
def derivative(x, p): # p is the slope of negative part of LeakyReLU
dx = np.ones_like(x) # initialize a vector with 1
dx[x < 0] = p # set negative part to p
return dx
7.3.4 双曲正切函数的导数
回忆一下 Tanh 函数的表达式:
其衍生表达式为:
双曲正切函数及其导数曲线如图 7-5 所示。
图 7-5
双曲正切函数及其导数
在 Numpy 中,双曲正切函数的导数通过 Sigmoid 函数实现,如下所示:
def sigmoid(x): # sigmoid function
return 1 / (1 + np.exp(-x))
def tanh(x): # tanh function
return 2*sigmoid(2*x) - 1
def derivative(x): # derivative of tanh
return 1-tanh(x)**2
7.4 损失函数的梯度
前面已经介绍了常见的损失函数。这里我们主要推导均方误差损失函数和交叉熵损失函数的梯度表达式。
7.4.1 均方误差函数的梯度
均方误差损失函数表达式为:
上式中的项是为了简化计算,也可以用
来取平均值来代替。这些缩放操作都不会改变渐变方向。那么它的偏导数
就可以展开为:
由复合函数的导数定律分解:
即:
考虑到其他情况下 k = i 和为 0 时
为 1,即偏导数
只与第 i 个节点有关,所以可以去掉上式中的求和符号。均方误差函数的导数可以表示为:
7.4.2 交叉熵函数的梯度
在计算交叉熵损失函数时,Softmax 函数和交叉熵函数一般以统一的方式实现。我们首先导出 Softmax 函数的梯度,然后导出交叉熵函数的梯度。
Softmax 的渐变回忆 soft max 的表情:
其作用是将输出节点的值转换成概率,并保证概率之和为 1,如图 7-6 所示。
图 7-6
Softmax 图解
回忆:
函数的导数是:
对于 Softmax 功能,,
。我们将在两种情况下推导其梯度: i = j 和I≦j。
-
i = j. The derivative of Softmax
is:
前面的表达式是 p i 和 1pj和pI=pj的乘积。所以当 i = j 时,Softmax 的导数为:
-
i ≠ j. Extend the Softmax function:
也就是:
可以看出,虽然 Softmax 函数的梯度求导过程略显复杂,但最终的表达式还是非常简洁的。偏导数表达式如下:
交叉熵函数的梯度考虑交叉熵损失函数的表达式:
这里我们直接推导出最终损耗值 L 对网络输出的 logits 变量zI的偏导数,展开为:
将复合函数 log log h 分解为:
也就是:
其中是我们已经推导出的 Softmax 函数的偏导数。
将求和符号拆分为两种情况: k = i 和k≦I,代入的表达式,我们可以得到:
也就是:
具体来说,分类问题中标签的一键编码方式有如下关系:
因此,交叉熵的偏导数可以进一步简化为:
7.5 全连接层的坡度
在介绍了梯度的基本知识之后,我们正式进入了神经网络的反向传播算法的推导。神经网络的结构是多样的,不可能一一分析梯度表达式。我们将使用具有全连接层网络的神经网络,使用 Sigmoid 函数作为激活函数,使用 softmax + MSE 损失函数作为误差函数来导出梯度传播定律。
7.5.1 单一神经元的梯度
对于一个使用 Sigmoid 激活函数的神经元模型,其数学模型可以写成:
变量的上标代表层数。例如, o (1) 代表第一层的输出, x 为网络的输入。我们以权重参数 w j 1 的偏导数求导为例。为了便于演示,我们绘制了如图 7-7 所示的神经元模型。图中未示出 Bias b ,输入节点数为 j,从第 j 个节点的输入到输出 o (1) 的权重连接记为
,其中上标表示权重参数所属的层数,下标表示当前连接的起始节点数和结束节点数。例如,下标 j 1 表示上一层的第 j 个节点到当前层的第一个节点。激活函数 σ 之前的变量称为
,激活函数 σ 之后的变量称为
。因为只有一个输出节点,所以
。误差值 L 由输出和实际标签之间的误差函数计算。
图 7-7
神经元模型
如果使用均方误差函数,考虑到单个神经元只有一个输出,那么损耗可以表示为:
其中, t 为真实标签值。加入不影响梯度的方向,计算更简单。我们以Jth(J∈【1, J )节点的权重变量 w * j * 1 为例,考虑损失函数 L :
的偏导数
考虑到o1=σ(z1)以及 Sigmoid 函数的导数为σ′=σ(1-σ),我们有:
把σ ( z 1 )写成o1:
考虑,我们有:
从上式可以看出,误差对权重的偏导数wj1只与输出值 o 1 ,真值 t ,以及连接到当前权重的输入 x j 有关。
7.5.2 全连接层的坡度
我们将单神经元模型推广为全连接层的单层网络,如图 7-8 所示。输入层通过全连接层获得输出向量 o (1) ,并计算与真实标签向量 t 的均方误差。输入节点数为 J,输出节点数为 K 。
图 7-8
全连接层
多输出全连接网络层模型与单神经元模型的区别在于它有更多的输出节点,每个输出节点对应一个真实的标签t1、t2、…、 t * K * 。wJK是第 j 个输入节点和第 k 个输出节点的连接权重。均方差可以表示为:
由于只与节点
关联,所以可以去掉前面公式中的求和符号,即 i = * k * :
代入ok=σ(zk):
考虑 Sigmoid 函数的导数σ’=σ(1σ):
将σ(z【k】)写成**【k】**
*考虑 :
可以看出wJK的偏导数只与当前连接的输出节点相关,对应 true 的标签
,对应输入节点 x * j * 。
让【k】=(
*变量 δ k 表征了连线末端节点误差梯度传播的某种特性。使用表示法 δ k 后,偏导数只与当前连接的起始节点 x j 和结束节点 δ k 相关。后面我们会看到 δ k * 在循环推导梯度中的作用。
现在已经导出了单层神经网络(即输出层)的梯度传播法,接下来我们尝试导出倒数第二层的梯度传播法。在完成倒数第二层的传播推导后,类似地,可以循环推导所有隐层的梯度传播模式,得到所有层参数的梯度计算表达式。
在介绍反向传播算法之前,我们先学习导数传播的一个核心规则——链式规则。
7.6 链式法则
前面我们介绍了输出图层的渐变计算方法。我们现在引入链规则,这是一个核心公式,可以逐层推导梯度,而不需要显式推导神经网络的数学表达式。
事实上,在推导梯度的过程中,或多或少地使用了链式法则。考虑到复合函数 y = f ( u ), u = g ( x ),我们可以从和
:
推导出
考虑两个变量的复合函数 z = f ( x , y ),其中x=g(t),y=h(t),那么导数可以由
和
:
比如,设 x = 2 * t * + 1, y = * t * 2 ,那么z=x2+ey。利用前面的公式,我们有:
设 x = 2 t + 1、y=t2:
也就是:
神经网络的损失函数 L 来自每个输出节点,如图 7-9 所示,其中输出节点
与隐含层的输出节点
关联,因此链式法则非常适合神经网络的梯度求导。让我们考虑如何将链式法则应用于损失函数。
图 7-9
梯度传播插图
在前向传播中,数据通过到达倒数第二层的节点
,然后传播到输出层的节点
。当每层只有一个节点时,可以利用链式法则将
逐层分解为:
在哪里中可以直接从误差函数中导出,而
可以从全连接层公式中导出。导数
就是输入
。可以看出,通过链式法则,对于
的导数,我们不需要具体的数学表达式;而是可以直接分解偏导数,逐层迭代求导。
这里我们简单用 TensorFlow 自动求导功能来体验一下链式法则的魅力。
import tensorflow as tf
# Create vectors
x = tf.constant(1.)
w1 = tf.constant(2.)
b1 = tf.constant(1.)
w2 = tf.constant(2.)
b2 = tf.constant(1.)
# Create gradient recorder
with tf.GradientTape(persistent=True) as tape:
# Manually record gradient info for non-tf.Variable variables
tape.watch([w1, b1, w2, b2])
# Create two layer neural network
y1 = x * w1 + b1
y2 = y1 * w2 + b2
# Solve partial derivatives
dy2_dy1 = tape.gradient(y2, [y1])[0]
dy1_dw1 = tape.gradient(y1, [w1])[0]
dy2_dw1 = tape.gradient(y2, [w1])[0]
# Valdiate chain rule
print(dy2_dy1 * dy1_dw1)
print(dy2_dw1)
在前面的代码中,我们通过 Tensorflow 中的自动梯度计算和链式法则计算了
,
,和,我们知道
和
应该等于
.,它们的结果如下:
tf.Tensor(2.0, shape=(), dtype=float32)
tf.Tensor(2.0, shape=(), dtype=float32)
7.7 反向传播算法
现在我们来推导一下隐藏层的渐变传播规律。简单回顾一下输出层的偏导数公式:
考虑倒数第二层的偏导数,如图 7-10 所示。输出层节点数为 K,输出为
。倒数第二层有 J 个节点,输出是
。倒数第二层有 I 个节点,输出为
。
图 7-10
反向传播算法
为了表达简洁,一些变量的上标有时被省略。首先,扩展均方误差函数:
因为 L 通过各个输出节点 o k 与wij关联,这里不能去掉求和符号,可以用链式法则分解均方误差函数:
代入ok=σ(zk):
Sigmoid 函数的导数是σ’=σ(1—σ,所以:
把σ(zk写成 o k ,再考虑链式法则,我们有:
其中,所以:
因为与 k 没有关联,所以我们有:
因为oj=σ(zj)和σ’=(1σ*,我们有:*
其中是 o i * ,所以:
其中,所以:
类似于的格式,将
定义为:
此时,可以写成当前连接的起始节点的输出值 o * i * 和结束节点的梯度变量信息
的简单相乘
可以看出,通过定义变量 δ ,各层的梯度表达式变得更加清晰简洁,其中 δ 可以简单理解为当前权重 w ij 对误差函数的贡献值。
我们来总结一下各层偏导数的传播规律。
输出图层:
倒数第二层:
倒数第二层:
其中 o n 为倒数第二层的输入。
根据这一规律,只需迭代计算每层各节点的、
、
值,即可得到当前层的偏导数,从而得到每层权重矩阵 W 的梯度,再通过梯度下降算法迭代优化网络参数。
至此,反向传播算法介绍完毕。
接下来我们将进行两个动手案例:第一个案例是使用 TensorFlow 提供的自动求导来优化 Himmelblau 函数的极值。第二种情况是实现基于 Numpy 的反向传播算法,完成针对二分类问题的多层神经网络训练。
7.8 Himmelblau 的实际优化
Himmelblau 函数是测试优化算法的常用示例函数之一。包含两个自变量 x 和 y ,数学表达式为:
首先,我们通过以下代码实现 Himmelblau 函数的表达式:
def himmelblau(x):
# Himmelblau function implementation. Input x is a list with 2 elements.
return (x[0] ** 2 + x[1] - 11) ** 2 + (x[0] + x[1] ** 2 - 7) ** 2
然后我们完成 Himmelblau 函数的可视化。使用 np.meshgrid 函数(TensorFlow 中也有 meshgrid 函数)生成二维平面网格点坐标如下:
x = np.arange(-6, 6, 0.1) # x-axis
y = np.arange(-6, 6, 0.1) # y-axis
print('x,y range:', x.shape, y.shape)
X, Y = np.meshgrid(x, y)
print('X,Y maps:', X.shape, Y.shape)
Z = himmelblau([X, Y])
使用 Matplotlib 库可视化 Himmelblau 函数,如图 7-11 所示:
图 7-11
天蓝色功能
# Plot the Himmelblau function
fig = plt.figure('himmelblau')
ax = fig.gca(projection='3d')
ax.plot_surface(X, Y, Z)
ax.view_init(60, -30)
ax.set_xlabel('x')
ax.set_ylabel('y')
plt.show()
图 7-12 是 Himmelblau 功能的等高线图。大致可以看出它有四个局部极小点,局部极小值都是 0,所以这四个局部极小值也是全局极小值。我们可以通过分析方法计算局部最小值的精确坐标;他们是:
知道了极值的解析解,我们现在使用梯度下降算法来优化 Himmelblau 函数的最小数值解。
图 7-12
Himmelblau 函数等高线图
我们可以使用 TensorFlow 自动求导来查找函数和的偏导数,并迭代更新和值,如下所示:
# The influence of the initialization value of the parameter on the optimization cannot be ignored, you can try different initialization values # Test the minimum value of function optimization
# [1., 0.], [-4, 0.], [4, 0.]
x = tf.constant([4., 0.]) # Initialization
for step in range(200):# Loop 200 times
with tf.GradientTape() as tape: #record gradient
tape.watch([x]) # Add to the gradient recording list
y = himmelblau(x) # forward propagation
# backward propagration
grads = tape.gradient(y, [x])[0]
# update paramaters with learning rate of 0.01
x -= 0.01*grads
# print info
if step % 20 == 19:
print ('step {}: x = {}, f(x) = {}'
.format(step, x.numpy(), y.numpy()))
经过 200 次迭代更新,程序可以找到一个最小解,此时函数值接近于零。数值解是
step 199: x = [ 3.584428 -1.8481264], f(x) = 1.1368684856363775e-12
这几乎与其中一个解析解(3.584,1.848)相同。
实际上,通过改变网络参数的初始化状态,程序可以获得多种最小数值解。参数的初始化状态可能会影响梯度下降算法的搜索轨迹,甚至可能会搜索出完全不同的数值解,如表 7-1 所示。这个例子解释了不同初始状态对梯度下降算法的影响。
表 7-1
初始值对优化结果的影响
|x 的初始值
|
数值解
|
解析解
| | --- | --- | --- | | ( 4 , 0 ) | (3.58,-1.84) | (3.58,-1.84) | | (1,0) | (3,1.99) | (3,2) | | (-4,0) | (-3.77,-3.28) | (-3.77,-3.28) | | (-2,2) | (-2.80,3.13) | (-2.80,3.13) |
7.9 实际操作反向传播算法
本节我们将利用前面介绍的多层全连通网络的梯度推导结果,直接用 Python 计算各层的梯度,根据梯度下降算法手动更新。由于 TensorFlow 有自动求导功能,所以我们选择没有自动求导功能的 Numpy 来实现网络,使用 Numpy 手动计算梯度,手动更新网络参数。
需要注意的是,本章推导的梯度传播公式是针对只有 Sigmoid 函数的多个全连通层的,损失函数是网络类型的均方误差函数。对于其他类型的网络,如具有 ReLU 激活函数和交叉熵损失函数的网络,需要重新推导梯度传播表达式,但方法类似。正是因为手动推导梯度的方法比较有限,所以在实际中很少使用。
我们将实现一个四层全连接网络来完成二进制分类任务。网络输入节点数为 2,隐层节点数设计为 20、50、25。输出层的两个节点分别代表属于类别 1 和类别 2 的概率,如图 7-13 所示。这里,Softmax 函数不用于约束网络输出概率值的总和。相反,均方误差函数被直接用于计算预测和独热码编码的真实标签之间的误差。所有激活函数都是 Sigmoid。这个设计是直接用我们的梯度传播公式。
图 7-13
网络结构
数据集
通过 scikit-learn 库提供的便利工具,生成了 2000 个线性不可分的 2 类数据集。数据的特征长度是 2。采样数据分布如图 7-14 所示。红点属于一类,蓝点属于另一类。每个类别的分布是新月形的,并且是线性不可分的,这意味着不能使用线性网络来获得好的结果。为了测试网络的性能,我们按照 7:3 的比例划分训练集和测试集。两千。0 s3 = 600 个样本点用于测试,不参与训练。剩下的 1400 分用于网络培训。
图 7-14
数据集分布
数据集的集合使用 scikit-learn 提供的 make_moons 函数直接生成,采样点数和测试比设置如下:
N_SAMPLES = 2000 # number of sampling points
TEST_SIZE = 0.3 # testing ratio
# Use make_moons function to generate data set
X, y = make_moons(n_samples = N_SAMPLES, noise=0.2, random_state=100)
# Split traning and testing data set
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=TEST_SIZE, random_state=42)
print(X.shape, y.shape)
数据集的分布可以通过下面的可视化代码绘制出来,如图 7-14 所示。
# Make a plot
def make_plot(X, y, plot_name, file_name=None, XX=None, YY=None, preds=None, dark=False):
if (dark):
plt.style.use('dark_background')
else:
sns.set_style("whitegrid")
plt.figure(figsize=(16,12))
axes = plt.gca()
axes.set(xlabel="$x_1$", ylabel="$x_2$")
plt.title(plot_name, fontsize=30)
plt.subplots_adjust(left=0.20)
plt.subplots_adjust(right=0.80)
if(XX is not None and YY is not None and preds is not None):
plt.contourf(XX, YY, preds.reshape(XX.shape), 25, alpha = 1, cmap=cm.Spectral)
plt.contour(XX, YY, preds.reshape(XX.shape), levels=[.5], cmap="Greys", vmin=0, vmax=.6)
# Use color to distinguish labels
plt.scatter(X[:, 0], X[:, 1], c=y.ravel(), s=40, cmap=plt.cm.Spectral, edgecolors='none')
plt.savefig('dataset.svg')
plt.close()
# Make distribution plot
make_plot(X, y, "Classification Dataset Visualization ")
plt.show()
网络层
一个新的层类用于实现网络层。输入节点的数量、输出节点的数量和激活函数的类型等参数被传递到网络层。权重和偏差张量偏差是基于初始化期间输入和输出节点的数量自动生成的,如下所示:
class Layer:
# Fully connected layer
def __init__(self, n_input, n_neurons, activation=None, weights=None, bias=None):
"""
:param int n_input: input nodes
:param int n_neurons: output nodes
:param str activation: activation function
:param weights: weight vectors
:param bias: bias vectors
"""
# Initialize weights through Normal distribution
self.weights = weights if weights is not None else np.random.randn(n_input, n_neurons) * np.sqrt(1 / n_neurons)
self.bias = bias if bias is not None else np.random.rand(n_neurons) * 0.1
self.activation = activation # activation function, e.g. ’sigmoid’
self.last_activation = None # output of activation function o
self.error = None
self.delta = None
网络层的前向传播函数实现如下,其中 last_activation 变量用于保存当前层的输出值:
def activate(self, x):
# Forward propagation function
r = np.dot(x, self.weights) + self.bias # X@W+b
# Get output through activation function
self.last_activation = self._apply_activation(r)
return self.last_activation
自我。前面代码中的 _apply_activation 函数实现了不同类型激活函数的正向计算过程,虽然这里我们只使用 Sigmoid 激活函数。
def _apply_activation(self, r):
# Calculate output of activation function
if self.activation is None:
return r # No activation function
# ReLU
elif self.activation == 'relu':
return np.maximum(r, 0)
# tanh
elif self.activation == 'tanh':
return np.tanh(r)
# sigmoid
elif self.activation == 'sigmoid':
return 1 / (1 + np.exp(-r))
return r
对于不同类型的激活函数,它们的导数计算如下:
def apply_activation_derivative(self, r):
# Calculate the derivative of activation functions
# If no activation function, derivative is 1
if self.activation is None:
return np.ones_like(r)
# ReLU
elif self.activation == 'relu':
grad = np.array(r, copy=True)
grad[r > 0] = 1.
grad[r <= 0] = 0.
return grad
# tanh
elif self.activation == 'tanh':
return 1 - r ** 2
# Sigmoid
elif self.activation == 'sigmoid':
return r * (1 - r)
return r
可以看出,Sigmoid 函数的导数实现为r(1—r,其中 r 为 σ ( z )。
网络模型
在创建了单层网络类之后,我们实现了网络模型的 NeuralNetwork 类,在内部维护各层的网络层对象。您可以通过 add_layer 函数添加网络层,以达到创建不同结构的网络模型的目的,如下所示:
class NeuralNetwork:
# Neural Network Class
def __init__(self):
self._layers = [] # list of network class
def add_layer(self, layer):
# Add layers
self._layers.append(layer)
网络的前向传播只需要循环调整每个网络层对象的前向计算函数。代码如下:
def feed_forward(self, X):
# Forward calculation
for layer in self._layers:
# Loop through every layer
X = layer.activate(X)
return X
根据图 7-13 中的网络结构配置,我们使用 NeuralNetwork 类创建一个网络对象,添加一个四层全连通网络。代码如下:
nn = NeuralNetwork()
nn.add_layer(Layer(2, 25, 'sigmoid')) # Hidden layer 1, 2=>25
nn.add_layer(Layer(25, 50, 'sigmoid')) # Hidden layer 2, 25=>50
nn.add_layer(Layer(50, 25, 'sigmoid')) # Hidden layer 3, 50=>25
nn.add_layer(Layer(25, 2, 'sigmoid')) # Hidden layer, 25=>2
网络模型的反向传播稍微复杂一些。我们需要从最后一层开始,计算每一层的变量 δ ,然后将计算出来的变量 δ 按照下面推导出的梯度公式存储在 layer 类的 delta 变量中:
def backpropagation(self, X, y, learning_rate):
# Back propagation
# Get result of forward calculation
output = self.feed_forward(X)
for i in reversed(range(len(self._layers))): # reverse loop
layer = self._layers[i] # get current layer
# If it’s output layer
if layer == self._layers[-1]: # output layer
layer.error = y - output
# calculate delta
layer.delta = layer.error * layer.apply_activation_derivative(output)
else: # For hidden layer
next_layer = self._layers[i + 1]
layer.error = np.dot(next_layer.weights, next_layer.delta)
# Calculate delta
layer.delta = layer.error * layer.apply_activation_derivative(layer.last_activation)
... # See following code
在反算出每层的变量 δ 后,只需要根据公式计算出每层参数的梯度,更新网络参数即可。因为代码中的δ实际计算为 δ ,所以更新时使用加号。代码如下:
def backpropagation(self, X, y, learning_rate):
... # Continue above code
# Update weights
for i in range(len(self._layers)):
layer = self._layers[i]
# o_i is output of previous layer
o_i = np.atleast_2d(X if i == 0 else self._layers[i - 1].last_activation)
# Gradient descent
layer.weights += layer.delta * o_i.T * learning_rate
因此,在反向传播函数中,反向计算每层的变量 δ ,根据梯度公式计算每层参数的梯度值,根据梯度下降算法完成参数更新。
7.9.4 网络培训
这里的二进制分类网络设计有两个输出节点,所以真正的标签需要一热编码。代码如下:
def train(self, X_train, X_test, y_train, y_test, learning_rate, max_epochs):
# Train network
# one-hot encoding
y_onehot = np.zeros((y_train.shape[0], 2))
y_onehot[np.arange(y_train.shape[0]), y_train] = 1
计算一热编码实标签和网络输出的均方误差,调用反向传播函数更新网络参数,迭代训练集 1000 次,如下:
mses = []
for i in range(max_epochs): # Train 1000 epoches
for j in range(len(X_train)): # Train one sample per time
self.backpropagation(X_train[j], y_onehot[j], learning_rate)
if i % 10 == 0:
# Print MSE Loss
mse = np.mean(np.square(y_onehot - self.feed_forward(X_train)))
mses.append(mse)
print('Epoch: #%s, MSE: %f' % (i, float(mse)))
# Print accuracy
print('Accuracy: %.2f%%' % (self.accuracy(self.predict(X_test), y_test.flatten()) * 100))
return mses
网络性能
我们记录每个历元的训练损失值 L 并绘制成曲线,如图 7-15 所示。
图 7-15
训练误差图
在训练 1000 个时期后,在测试集中的 600 个样本上获得的准确率为:
Epoch: #990, MSE: 0.024335
Accuracy: 97.67%
可以看出,通过手动计算梯度公式和手动更新网络参数,对于简单的二元分类任务,我们也可以获得更低的错误率。通过微调网络超参数和其他技术,您还可以获得更好的网络性能。
在每个历元中,我们在测试集上完成一次精度测试,并将其绘制成曲线,如图 7-16 所示。可以看出,随着 Epoch 的进步,模型的精度得到了稳步提升,初始阶段更快,后续的提升也比较顺利。
图 7-16
测试准确度
通过这种基于 Numpy 手动计算梯度的全连通网络的二元分类,相信读者可以更深刻地体会到深度学习框架在算法实现中的作用。没有 TensorFlow 这样的框架,我们也可以实现复杂的神经网络,但是灵活性、稳定性、开发效率、计算效率都很差。基于这些深度学习框架的算法设计和训练将大大提高算法开发者的工作效率。同时,我们也可以认识到,框架只是一个工具。更重要的是,我们对算法本身的理解是算法开发者最重要的能力。
7.10 参考文献
-
D.E. Rumelhart,G. E. Hinton 和 R. J. Williams,“通过反向传播错误学习表征”,《自然》, 323,6088,第 533-536 页,1986 年。
-
辛格库尔迪普。线性代数:循序渐进。第一版。英国牛津:牛津大学出版社,2013 年。
-
斯图尔特,詹姆斯。微积分:早期先验论。第八版。美国马萨诸塞州波士顿:Cengage Learning,2015。*****
八、Keras 高级接口
人工智能的问题不仅是计算机科学的问题,也是数学、认知科学和哲学的问题。
弗朗索瓦·乔列特
Keras 是一个主要用 Python 语言开发的开源神经网络计算库。它最初是由弗朗索瓦·乔莱写的。它被设计为高度模块化和可扩展的高级神经网络接口,使用户无需过多的专业知识就能快速完成模型的建立和训练。Keras 库分为前端和后端。后端一般调用已有的深度学习框架来实现底层操作,如 Theano、CNTK、TensorFlow 等。前端接口是由 Keras 抽象出来的一组统一的接口函数。用户可以通过 Keras 轻松切换不同的后端操作。由于 Keras 的高度抽象和易用性,根据 KDnuggets 的数据,截至 2019 年,Keras 的市场份额达到 26.6%,增长了 19.7%,在深度学习框架中仅次于 TensorFlow。
TensorFlow 和 Keras 之间是一种交错的关系,既竞争又合作。甚至 Keras 的创始人也在谷歌工作。早在 2015 年 11 月,Keras 后端支持中就加入了 TensorFlow。自 2017 年以来,Keras 的大部分组件都已集成到 TensorFlow 框架中。2019 年,Keras 被正式确定为 TensorFlow 2 唯一的高级接口 API,取代 TensorFlow 1 中包含的 tf.layers 等高级接口。换句话说,现在你只能使用 Keras 接口来完成 TensorFlow 图层模型的建立和训练。在 TensorFlow 2 中,Keras 是在 tf.keras 子模块中实现的。
Keras 和 tf.keras 有什么区别和联系?实际上,Keras 可以理解为一组用于构建和训练神经网络的高级 API 协议。Keras 本身已经实现了这个协议。安装标准的 Keras 库可以轻松调用 TensorFlow、CNTK 等后端完成加速计算。在 TensorFlow 中,也通过 tf.keras 实现了一套 Keras 协议,与 TensorFlow 深度融合,只基于 TensorFlow 后端操作,更完美的支持 TensorFlow。对于使用 TensorFlow 的开发者来说,tf.keras 可以理解为一个普通的子模块,与 tf.math、tf.data 等其他子模块没有区别,除非特别说明,否则以下章节中 keras 指的是 tf.keras 而不是标准的 Keras 库。
8.1 常用功能模块
Keras 提供了一系列与神经网络相关的高级类和函数,如经典数据集加载函数、网络层类、模型容器、损失函数类、优化器类和经典模型类。
对于经典数据集,一行代码就可以下载、管理和加载数据集。这些数据集包括波士顿房价预测数据集、CIFAR 图片数据集、MNIST/FashionMNIST 手写数字图片数据集和 IMDB 文本数据集。我们已经在前几章中介绍了其中的一些。
8.1.1 常见网络层类别
对于常见的神经网络层,我们可以使用张量模式的底层接口函数来实现,这些函数一般都包含在 tf.nn 模块中。对于常见的网络层,我们一般采用层的方法来完成模型的构建。tf.keras.layers 命名空间中提供了大量常见的网络层(下文中使用层来指代 tf.keras.layers),例如全连接层、激活功能层、池层、卷积层和循环神经网络层。对于这些网络层类,只需要在创建时指定网络层的相关参数,使用 call 方法完成正向计算即可。使用 call 方法时,Keras 会自动调用各层的正向传播逻辑,一般在类的 call 函数中实现。
以 Softmax 层为例,它可以使用 tf.nn.softmax 函数来完成正向传播中的 softmax 操作,也可以通过层来构建 Softmax 网络层。Softmax(轴)类,其中轴参数指定 Softmax 操作的尺寸。首先,导入相关的子模块,如下所示:
import tensorflow as tf
# Do not use "import keras" which will import the standard Keras, not the one in Tensorflow
from tensorflow import keras
from tensorflow.keras import layers # import common layer class
然后创建一个 Softmax 图层,并使用 call 方法完成正向计算:
In [1]:
x = tf.constant([2.,1.,0.1]) # create input tensor
layer = layers.Softmax(axis=-1) # create Softmax layer
out = layer(x) # forward propagation
通过 Softmax 网络层后,概率分布输出为:
Out[1]:
<tf.Tensor: id=2, shape=(3,), dtype=float32, numpy=array([0.6590012, 0.242433 , 0.0985659], dtype=float32)>
当然,我们也可以通过 tf.nn.softmax()函数直接完成计算,如下:
out = tf.nn.softmax(x)
网络容器
对于常见的网络,我们需要手动调用各层的类实例来完成正向传播操作。当网络层越深入,这部分代码就显得非常臃肿。通过 Keras 提供的网络容器 Sequential,可以将多个网络层封装成一个大型网络模型。只需要调用一次网络模型的实例,就可以完成数据从第一层到最后一层的顺序传播操作。
例如,具有独立激活功能层的两层全连接网络可以通过顺序容器封装为一个网络。
from tensorflow.keras import layers, Sequential
network = Sequential([
layers.Dense(3, activation=None), # Fully-connected layer without activation function
layers.ReLU(),# activation function layer
layers.Dense(2, activation=None), # Fully-connected layer without activation function
layers.ReLU() # activation function layer
])
x = tf.random.normal([4,3])
out = network(x)
顺序容器还可以通过 add()方法继续添加新的网络层,以动态创建网络:
In [2]:
layers_num = 2
network = Sequential([]) # Create an empty container
for _ in range(layers_num):
network.add(layers.Dense(3)) # add fully-connected layer
network.add(layers.ReLU())# add activation layer
network.build(input_shape=(4, 4))
network.summary()
前面的代码可以创建一个网络结构,其层数由 layers_num 参数指定。网络创建完成后,网络层类不会创建成员变量,如内部权重张量。使用 build 方法,可以指定输入大小,这将自动为所有层创建内部张量。通过 summary()函数,可以方便地打印出网络结构和参数。结果如下:
Out[2]:
Model: "sequential_2"
_______________________________________________________________
Layer (type) Output Shape Param Number
===============================================================
dense_2 (Dense) multiple 15
_______________________________________________________________
re_lu_2 (ReLU) multiple 0
_______________________________________________________________
dense_3 (Dense) multiple 12
_______________________________________________________________
re_lu_3 (ReLU) multiple 0
===============================================================
Total params: 27
Trainable params: 27
Non-trainable params: 0
_______________________________________________________________
图层列包括由 TensorFlow 内部维护的每个图层的名称,与 Python 的对象名称不同。“输出形状”列指示每个图层的输出形状。请注意,“输出形状”列的值都是“多个”,因为我们此时仅构建或编译了网络,并未真正训练或执行网络。在我们使用真实输入调用网络后,每个层的真实输出形状将反映在输出形状列中。Param number 列是每层的参数数。Total params 统计参数的总数。可训练参数是要优化的参数总数。不可训练参数是不需要优化的参数总数。
当我们通过顺序容器封装多个网络层时,每一层的参数表都会自动合并到顺序容器中。序列对象的可训练变量和变量包含要优化的张量列表和所有层的张量,例如:
In [3]: # print name and shape of trainable variables
for p in network.trainable_variables:
print(p.name, p.shape)
Out[3]:
dense_2/kernel:0 (4, 3)
dense_2/bias:0 (3,)
dense_3/kernel:0 (3, 3)
dense_3/bias:0 (3,)
顺序容器是最常用的类之一。这对快速建立多层神经网络非常有用。应该尽可能地使用它来简化网络模型的实现。
8.2 模型配置、培训和测试
在训练网络时,一般的流程是通过正向计算得到网络的输出值,然后通过损失函数计算网络误差,再通过自动微分工具计算并更新梯度,不定期测试网络性能。对于这种常用的训练逻辑,可以通过 Keras 提供的高层接口直接实现。
8.2.1 型号配置
在 keras,有两个特殊的阶层:Keras。Model 和 keras . layers . Layer . Layer . Layer 层类是网络层的父类,它定义了网络层的一些常用功能,比如添加权重、管理权重列表等。模型类是网络的父类。除了 layer 类的功能之外,还增加了保存模型、加载模型、训练测试模型等方便的功能。Sequential 也是 model 的子类,所以它拥有 model 类的所有功能。
下面介绍一下模型类及其子类的模型配置和训练功能。以序列容器封装的网络为例,我们首先为 MNIST 手写数字图像识别建立一个五层全连通网络。代码如下:
# Create a 5-layer fully connected network
network = Sequential([layers.Dense(256, activation='relu'),
layers.Dense(128, activation='relu'),
layers.Dense(64, activation='relu'),
layers.Dense(32, activation='relu'),
layers.Dense(10)])
network.build(input_shape=(4, 28*28))
network.summary()
网络创建后,正常的流程是在数据集中迭代多个历元,批量生成训练数据,做前向传播计算,然后通过损失函数计算误差值,通过反向传播自动计算梯度,更新网络参数。因为这部分逻辑非常通用,所以 Keras 中提供了 compile()和 fit()函数来简化逻辑。我们可以通过 compile 函数直接指定网络使用的优化器、损失函数、评估指标和其他设置。这一步称为配置。
# Import optimizer, loss function module
from tensorflow.keras import optimizers,losses
# Use Adam optimizer with learning rate of 0.01
# Use cross-entropy loss function with Softmax
network.compile(optimizer=optimizers.Adam(lr=0.01),
loss=losses.CategoricalCrossentropy(from_logits=True),
metrics=['accuracy'] # Set accuracy as evaluation metric
)
compile()函数中指定的优化器、损失函数和其他参数也是我们在自己的训练中需要设置的参数。Keras 在内部实现了这部分通用逻辑,以提高开发效率。
模型培训
模型配置完成后,可以通过 fit()函数发送用于训练和验证的数据集。这一步叫做模型训练。
# Training dataset is train_db, and validation dataset is val_db
# Train 5 epochs and validate every 2 epoch
# Training record and history is saved in history variable
history = network.fit(train_db, epochs=5, validation_data=val_db, validation_freq=2)
train_db 可以是 tf.data.Dataset 对象或 Numpy 数组。Epochs 参数指定训练迭代的时期数。validation_data 参数指定用于验证的数据集,验证频率由 validation_freq 控制。
前面的代码可以实现网络训练和验证的功能。fit 函数将返回训练过程数据记录的历史,其中 history.history 是字典对象,包括训练过程的损失、评估度量和其他记录,例如:
In [4]: history.history # print training record
Out[4]:
{'loss': [0.31980024444262184, # training loss
0.1123824894875288,
0.07620834542314212,
0.05487803366283576,
0.041726120284820596], # training accuracy
'accuracy': [0.904, 0.96638334, 0.97678334, 0.9830833, 0.9870667],
'val_loss': [0.09901347314302303, 0.09504951824009701], # validation loss
'val_accuracy': [0.9688, 0.9703]} # validation accuracy
fit()函数的操作代表了网络的训练过程,所以会消耗相当多的训练时间,训练完成后返回。训练时生成的历史数据可以通过返回值对象获得。可以看出,通过 Compile&Fit 方法实现的代码非常简洁高效,大大减少了开发时间。但是因为界面很高级,灵活性也降低了,要不要用还是由用户自己决定。
模型测试
该模型类不仅可以方便地完成网络的配置、训练和验证,还可以非常方便地进行预测和测试。我们将在过拟合一章中阐述验证和测试的区别。在这里,验证和测试可以理解为模型评估的一种方式。
Model.predict(x)方法可以完成模型预测,例如:
# Load one batch of test dataset
x,y = next(iter(db_test))
print('predict x:', x.shape) # print the batch shape
out = network.predict(x) # prediction
print(out)
其中 out 是网络的输出。通过前面的代码,训练好的模型可以用来预测新样本的标签信息。
如果只需要测试模型的性能,可以使用 Model.evaluate(db)来测试 db 数据集上的所有样本,并打印出性能指标,例如:
network.evaluate(db_test)
8.3 模型保存和加载
模型训练完成后,需要将模型保存到文件系统中,以便于后续的模型测试和部署。事实上,在训练时保存模型状态也是一个好习惯,这对于训练大规模网络尤为重要。一般大规模的网络需要几天甚至几周的训练。一旦训练过程中断或发生意外,之前的训练进度就会丢失。如果模型状态能够间歇性地保存到文件系统中,那么即使发生了宕机等意外,也可以从最新的网络状态文件中恢复,从而避免浪费大量的训练时间和计算资源。因此,模型的保存和加载非常重要。
在 Keras 中,有三种保存和加载模型的常用方法。
张量方法
网络的状态主要体现在网络的结构和网络层内的张量数据上。因此,在拥有网络结构源文件的情况下,将网络张量参数直接保存到文件系统是最轻量级的方式。以 MNIST 手写数字图片识别模型为例,可以通过调用 Model.save_weights(path)方法保存当前的网络参数。代码如下:
network.save_weights('weights.ckpt') # Save tensor data of the model
上述代码将网络模型保存到 weights.ckpt 文件中。需要时,我们先创建一个网络对象,然后调用网络对象的 load_weights(path)方法,将指定模型文件中保存的张量值加载到当前网络参数中,例如:
# Save tensor data of the model
network.save_weights('weights.ckpt')
print('saved weights.')
del network # delete network object
# Create similar network
network = Sequential([layers.Dense(256, activation='relu'),
layers.Dense(128, activation='relu'),
layers.Dense(64, activation='relu'),
layers.Dense(32, activation='relu'),
layers.Dense(10)])
network.compile(optimizer=optimizers.Adam(lr=0.01),
loss=tf.losses.CategoricalCrossentropy(from_logits=True),
metrics=['accuracy']
)
# Load weights from file
network.load_weights('weights.ckpt')
print('loaded weights!')
这种保存和加载网络的方法是最轻量级的。该文件只保存张量参数的值,没有其他额外的结构参数。但它需要使用相同的网络结构才能正确还原网络状态,所以一般在有网络源文件的情况下使用。
网络方法
下面介绍一种不需要网络源文件,只需要模型参数文件就可以恢复网络模型的方法。模型结构和模型参数可以通过 Model.save(path)函数保存到路径文件中,网络结构和网络参数可以通过 keras.models.load_model(path)恢复,不需要网络源文件。
首先,将 MNIST 手写数字图片识别模型保存到一个文件中,并删除网络对象:
# Save model and parameters to a file
network.save('model.h5')
print('saved total model.')
del network # Delete the network
网络的结构和状态可以通过 model.h5 文件恢复,不需要事先创建网络对象。代码如下:
# Recover the model and parameters from a file
network = keras.models.load_model('model.h5')
如您所见,除了存储模型参数,model.h5 文件还应该保存网络结构信息。您可以直接从文件中恢复网络对象,而无需事先创建模型。
8.3.3 保存模型方法
TensorFlow 之所以被业界看好,不仅是因为出色的神经网络层 API 支持,还因为它拥有强大的生态系统,包括移动端和 web 端的支持。当模型需要部署到其他平台时,TensorFlow 提出的 SavedModel 方法是平台无关的。
通过 tf.saved_model.save(network,path),可以将模型保存到路径目录中,如下所示:
# Save model and parameters to a file
tf.saved_model.save(network, 'model-savedmodel')
print('saving savedmodel.')
del network # Delete network object
以下网络文件出现在文件系统 model-savedmodel 目录中,如图 8-1 所示:
图 8-1
保存模型方法目录
用户不需要关心文件保存格式,只需要通过 tf.saved_model.load 函数还原模型对象即可。在恢复模型实例后,我们完成了测试准确率的计算,并实现了以下内容:
print('load savedmodel from file.')
# Recover network and parameter from files
network = tf.saved_model.load('model-savedmodel')
# Accuracy metrics
acc_meter = metrics.CategoricalAccuracy()
for x,y in ds_val: # Loop through test dataset
pred = network(x) # Forward calculation
acc_meter.update_state(y_true=y, y_pred=pred) # Update stats
# Print accuracy
print("Test Accuracy:%f" % acc_meter.result())
8.4 定制网络
尽管 Keras 提供了许多常见的网络层类,但用于深度学习的网络远不止这些。研究人员通常自己实现相对较新的网络层。因此,掌握自定义网络层和网络的实现非常重要。
对于需要创建自定义逻辑的网络层,可以通过自定义类来实现。创建自定义的网络层类时,需要从层中继承。层基类。创建自定义网络类时,需要从 keras 继承。模型基类,所以用这种方式创建的自定义类可以很容易地使用层/模型基类。该类提供的参数管理和其他功能也可以与其他标准网络层类交互使用。
8.4.1 自定义网络层
对于自定义网络层,我们至少需要实现初始化(init)方法和正向传播逻辑。我们以一个具体的自定义网络层为例,假设需要一个没有偏置向量的全连通层,即偏置为 0,固定激活函数为 ReLU。尽管这可以通过标准的密集层来创建,我们仍然解释如何通过实现这个“特殊的”网络层类来实现一个定制的网络层。
首先,创建一个类,并从基础层类继承。创建一个初始化方法,调用父类的初始化函数。因为是全连通层,所以需要设置两个参数:输入特征 inp_dim 的长度和输出特征 outp_dim 的长度,形状大小由 self.add_variable(name,shape)创建。名张量 W 被设置为优化。
class MyDense(layers.Layer):
# Custom layer
def __init__(self, inp_dim, outp_dim):
super(MyDense, self).__init__()
# Create weight tensor and set to be trainable
self.kernel = self.add_variable('w', [inp_dim, outp_dim], trainable=True)
需要注意的是,self.add_variable 会返回一个对张量 W 的 Python 引用,变量名由 TensorFlow 内部维护,使用频率较低。我们实例化 MyDense 类并查看其参数列表,例如:
In [5]: net = MyDense(4,3) # Input dimension is 4 and output dimension is 3.
net.variables,net.trainable_variables # Check the trainable parameters
Out[5]:
# All parameters
([<tf.Variable 'w:0' shape=(4, 3) dtype=float32, numpy=...
# Trainable parameters
[<tf.Variable 'w:0' shape=(4, 3) dtype=float32, numpy=...
可以看到张量 W 自动包含在参数表中。
通过修改为 self . kernel = self . add _ variable(' W ',[inp_dim,outp_dim],trainable = False),我们可以设置张量 W 不可训练,然后观察张量的管理状态:
([<tf.Variable 'w:0' shape=(4, 3) dtype=float32, numpy=...], # All parameters
[])# Trainable parameters
如你所见,张量此时不由 trainable _ variables 管理。另外,创建为 tf 的类成员变量。类初始化中变量也自动包含在张量管理中,例如:
self.kernel = tf.Variable(tf.random.normal([inp_dim, outp_dim]), trainable=False)
托管张量列表打印如下:
# All parameters
([<tf.Variable 'Variable:0' shape=(4, 3) dtype=float32, numpy=...],
[])# Trainable parameters
在自定义类初始化之后,我们将设计正向计算逻辑。对于这个例子,只需要完成矩阵运算 O = X @ W 就可以使用固定的 ReLU 激活函数。代码如下:
def call(self, inputs, training=None):
# Forward calculation
# X@W
out = inputs @ self.kernel
# Run activation function
out = tf.nn.relu(out)
return out
如上所述,正向计算逻辑是在 call(inputs,training = None)函数中实现的,其中 inputs 参数表示输入并由用户传入。training 参数用于指定模型的状态:True 表示训练模式,False 表示测试模式,默认值为 None,即测试模式。因为全连接层的训练和测试模式在逻辑上是一致的,所以这里不需要额外的处理。对于测试和训练模式不一致的网络层,需要根据训练参数设计要执行的逻辑。
定制网络
在完成自定义全连接层类实现后,我们基于前面描述的“无偏全连接层”创建了 MNIST 手写数字图片模型。
自定义网络类可以像其他标准类一样,通过顺序容器轻松封装到网络模型中:
network = Sequential([MyDense(784, 256), # Use custom layer
MyDense(256, 128),
MyDense(128, 64),
MyDense(64, 32),
MyDense(32, 10)])
network.build(input_shape=(None, 28*28))
network.summary()
可以看出,通过堆叠我们自定义的网络层类,也可以实现五层全连通的层网络。全连通层的每一层都没有偏置张量,激活函数使用 ReLU 函数。
顺序容器适用于这样的网络模型,其中数据按顺序从第一层传播到第二层,然后从第二层传播到第三层,并且以这种方式传播。例如,对于复杂的网络结构,第三层的输入不仅是第二层的输出,也是第一层的输出。这时,使用定制的网络更加灵活。首先创建一个从模型基类继承的类,然后分别创建相应的网络层对象,如下所示:
class MyModel(keras.Model):
# Custom network class
def __init__(self):
super(MyModel, self).__init__()
# Create the network
self.fc1 = MyDense(28*28, 256)
self.fc2 = MyDense(256, 128)
self.fc3 = MyDense(128, 64)
self.fc4 = MyDense(64, 32)
self.fc5 = MyDense(32, 10)
然后实现定制网络的转发操作逻辑,如下所示:
def call(self, inputs, training=None):
# Forward calculation
x = self.fc1(inputs)
x = self.fc2(x)
x = self.fc3(x)
x = self.fc4(x)
x = self.fc5(x)
return x
这个例子可以使用顺序容器方法直接实现。但是定制网络的正向计算逻辑可以自由定义,更加通用。我们将在卷积神经网络一章中看到定制网络的优越性。
8.5 模型动物园
对于常用的网络模型,如 ResNet 和 VGG,您不需要手动创建它们。它们可以通过 keras.applications 子模块用一行代码直接实现。同时,还可以通过设置权重参数来加载预先训练好的模型。
负载模型
以 ResNet50 网络模型为例,去除 ResNet50 最后一层后的网络一般作为新任务的特征提取子网,即利用 ImageNet 数据集上预先训练好的网络参数,根据任务的类别,初始化并追加一个与数据类别数相对应的全连通层,从而在预先训练好的网络基础上快速高效地学习新任务。
首先使用 Keras model zoo 加载 ImageNet 预先训练好的 ResNet50 网络。代码如下:
# Load ImageNet pre-trained network. Exclude the last layer.
resnet = keras.applications.ResNet50(weights='imagenet',include_top=False)
resnet.summary()
# test the output
x = tf.random.normal([4,224,224,3])
out = resnet(x) # get output
out.shape
上述代码自动从服务器下载 ImageNet 数据集的模型结构和预训练网络参数。通过将 include_top 参数设置为 False,我们选择移除 ResNet50 的最后一层。网络输出特征图的大小为[ b ,7,7,2048]。对于特定的任务,我们需要设置自定义数量的输出节点。以 100 个分类任务为例,基于 ResNet50 重建一个新的网络。创建一个新的池层(此处的池层可以理解为在高维度和宽维度中向下采样的函数),并将特征维度从[b,7,7,2048]减少到[b,2048],如下所示。
In [6]:
# New pooling layer
global_average_layer = layers.GlobalAveragePooling2D()
# Use last layer's output as this layer's input
x = tf.random.normal([4,7,7,2048])
# Use pooling layer to reduce dimension from [4,7,7,2048] to [4,1,1,2048],and squeeze to [4,2048]
out = global_average_layer(x)
print(out.shape)
Out[6]: (4, 2048)
最后,创建一个新的完全连接层,并将输出节点数设置为 100。代码如下:
In [7]:
# New fully connected layer
fc = layers.Dense(100)
# Use last layer's output as this layer's input
x = tf.random.normal([4,2048])
out = fc(x)
print(out.shape)
Out[7]: (4, 100)
在创建了预训练的 ResNet50 功能子网、新的池层和全连接层之后,我们重新使用顺序容器来封装新的网络:
# Build a new network using previous layers
mynet = Sequential([resnet, global_average_layer, fc])
mynet.summary()
可以看到新网络模型的结构信息是:
Layer (type) Output Shape Param Number
===============================================================
resnet50 (Model) (None, None, None, 2048) 23587712
_______________________________________________________________
global_average_pooling2d (Gl (None, 2048) 0
_______________________________________________________________
dense_4 (Dense) (None, 100) 204900
===============================================================
Total params: 23,792,612
Trainable params: 23,739,492
Non-trainable params: 53,120
通过设置 resnet.trainable = False,可以选择冻结 resnet 部分的网络参数,只训练新创建的网络层,从而快速高效地完成网络模型训练。当然,你也可以更新网络的所有参数。
8.6 指标
在网络的训练过程中,往往需要准确率和召回率等指标。Keras 在 keras.metrics 模块中提供了一些常用的指标。
使用 Keras 度量有四个主要步骤:创建新的度量容器、写入数据、读取统计数据和清除度量容器。
8.6.1 创建指标容器
在 keras.metrics 模块中,它提供了许多常用的度量类,如均值、精度和余弦相似度。下面,我们以平均误差为例。
loss_meter = metrics.Mean()
写入数据
可以通过 update_state 函数写入新数据,度量会按照自己的逻辑记录和处理采样的数据。例如,损失值在每个步骤结束时收集一次:
# Record the sampled data, and convert the tensor to an ordinary value through the float() function
loss_meter.update_state(float(loss))
在每个批处理操作结束时放置前面的采样代码后,血糖仪将根据采样数据自动计算平均值。
读取统计数据
采样多次数据后,可以选择调用测量器的 result()函数来获取统计值。例如,区间统计平均损失如下:
# Print the average loss during the statistical period
print(step, 'loss:', loss_meter.result())
清理容器
由于度量容器将记录所有历史数据,因此在开始新一轮统计时,有必要清除历史状态。可以通过 reset_states()函数来实现。例如,每次读取平均误差后,清除统计信息以开始下一轮统计,如下所示:
if step % 100 == 0:
# Print the average loss
print(step, 'loss:', loss_meter.result())
loss_meter.reset_states() # reset the state
8.6.5 实际操作准确度指标
根据使用度量工具的方法,我们在训练过程中使用准确度度量来统计准确率。首先,创建一个新的准确度测量容器,如下所示:
acc_meter = metrics.Accuracy()
每次正向计算完成后,记录训练准确率。需要注意的是,精度类的 update_state 函数的参数是预测值和真值,而不是当前批次的准确率。我们将当前批次样本的标签和预测结果写入度量,如下所示:
# [b, 784] => [b, 10, network output
out = network(x)
# [b, 10] => [b], feed into argmax()
pred = tf.argmax(out, axis=1)
pred = tf.cast(pred, dtype=tf.int32)
# record the accuracy
acc_meter.update_state(y, pred)
对测试集中所有批次的预测值进行计数后,打印统计数据的平均准确度,并清除指标容器。代码如下:
print(step, 'Evaluate Acc:', acc_meter.result().numpy())
acc_meter.reset_states() # reset metric
8.7 可视化
在网络培训过程中,通过 web 终端监控网络的培训进度,并可视化培训结果,对于提高开发效率非常重要。TensorFlow 提供了一个名为 TensorBoard 的特殊可视化工具,通过 TensorFlow 将监控数据写入文件系统,并使用 web 后端监控相应的文件目录,从而允许用户查看网络监控数据。
TensorBoard 的使用需要模型代码和浏览器的配合。使用 TensorBoard 之前,需要安装 TensorBoard 库。安装命令如下:
# Install TensorBoard
pip install tensorboard
接下来介绍如何使用 TensorBoard 工具在模型端和浏览器端监控网络训练进度。
模型侧
在模型方面,您需要创建一个汇总类,在需要时写入监控数据。首先通过 tf.summary.create_file_writer 创建一个监控对象类的实例,并指定监控数据写入的目录。代码如下:
# Create a monitoring class, the monitoring data will be written to the log_dir directory
summary_writer = tf.summary.create_file_writer(log_dir)
我们以监控误差和可视图像数据为例,介绍如何编写监控数据。正演计算完成后,对于误差等标量数据,我们通过 tf.summary.scalar 函数记录监控数据,并指定时间戳步长参数。这里的步长参数类似于每个数据对应的时标信息,也可以理解为数据曲线的坐标,不做赘述。每种类型的数据通过字符串的名称来区分,相似的数据需要用相同的名称写入数据库。例如:
with summary_writer.as_default():
# write the current loss to train-loss database
tf.summary.scalar('train-loss', float(loss), step=step)
TensorBoard 通过字符串 ID 区分不同类型的监测数据,所以对于错误数据,我们命名为“train-loss”;不能写入其他类型的数据,以防止数据污染。
对于图片类型的数据,可以通过 tf.summary.image 函数写入监控图片数据。例如,在训练期间,可以通过 tf.summary.image 函数来可视化样本图像。由于 TensorFlow 中的张量通常包含多个样本,因此 tf.summary.image 函数接受多个图片的张量数据,并设置 max_outputs 参数来选择显示图片的最大数量。代码如下:
with summary_writer.as_default():
# log accuracy
tf.summary.scalar('test-acc', float(total_correct/total), step=step)
# log images
tf.summary.image("val-onebyone-images:", val_images, max_outputs=9, step=step)
运行模型程序,相应的数据会实时写入指定的文件目录。
8.7.2 浏览器端
运行程序时,监控数据被写入指定的文件目录。如果您想要远程实时查看和可视化这些数据,您还需要使用浏览器和 web 后端。第一步是打开 web 后端。在终端中运行“tensorboard - logdir path”命令,指定 web 后端监控的文件目录路径,即可打开 web 后端监控流程,如图 8-2 所示:
图 8-2
打开 web 服务器
打开浏览器,输入网址 http://localhost: 6006(也可以通过 IP 地址远程访问,具体端口号可能会根据命令行提示有所变化)监控网络训练的进度。TensorBoard 可以同时显示多条监控记录。在监控页面的左侧,可以选择监控记录,如图 8-3 所示:
图 8-3
张量板快照
在监控页面的上端,您可以选择不同类型的数据监控页面,例如标量监控页面标量和图片可视化页面图像。对于这个例子,我们需要监控标量数据的训练误差和测试准确率,其曲线可以在 SCALARS 页面查看,如图 8-4 和图 8-5 所示。
图 8-5
训练准确度曲线
图 8-4
训练损失曲线
在图像页面,您可以查看每个步骤的图像,如图 8-6 所示。
图 8-6
每一步的图片
除了监控标量数据和图像数据,TensorBoard 还支持通过 tf.summary.histogram 查看张量数据的直方图分布、通过 tf.summary.text 打印文本信息等功能,例如:
with summary_writer.as_default():
tf.summary.scalar('train-loss', float(loss), step=step)
tf.summary.histogram('y-hist',y, step=step)
tf.summary.text('loss-text',str(float(loss)))
在直方图页面可以查看张量的直方图,如图 8-7 所示,在文本页面可以查看文本信息,如图 8-8 所示。
图 8-8
张量板文本可视化
图 8-7
张量板直方图
其实除了 TensorBoard,脸书开发的 Visdom 工具也可以方便的实现数据的可视化,实时支持多种可视化方式,使用起来更加方便。图 8-9 显示了 Visdom 数据的可视化。Visdom 可以直接接受 PyTorch 的张量类型数据,但不能直接接受 TensorFlow 的张量类型数据。需要将其转换为 Numpy 数组。对于追求丰富的可视化方法和实时监控的读者来说,Visdom 可能是更好的选择。
图 8-9
智慧快照〔??〕〔??〕〔??〕〔??〕
8.8 摘要
在这一章中,我们介绍了 Keras 高级 API 的使用,它可以节省我们在网络开发过程中的大量时间。我们可以很容易地用容器法来构造网络。使用 Keras 内置函数可以快速实现神经网络的训练和测试。在对网络进行训练和测试之后,我们还可以保存训练好的模型,并在将来使用 Keras 重新加载模型。除了常见的网络层,Keras 还提供了为不同用例构建定制网络层的功能。我们还讨论了如何使用 Keras 加载流行的网络模型,以及使用 TensorBoard 设置评估指标和可视化模型性能。我们通过本章学习的工具可以帮助我们显著提高网络开发效率。
Footnotes 1图片来源: https://github.com/facebookresearch/visdom
九、过拟合
一切都要尽量简单,但不能更简单。
—爱因斯坦
机器学习的主要目的是从训练集中学习数据的真实模型,使其能够在看不见的测试集上表现良好。我们称之为概括能力。一般来说,训练集和测试集是从相同的数据分布中采样的。抽样样本彼此独立,但来自相同的分布。我们称这种假设为独立同分布(i.i.d .)假设。
模型的表现力前面已经提到了,也称为模型的容量。当模型的表达能力较弱时,如单一线性层,只能学习一个线性模型,不能很好地近似非线性模型。当模型的表达能力太强时,可能会减少训练集的噪声模态,但会导致测试集的性能较差(泛化能力较弱)。因此,对于不同的任务,设计一个容量合适的模型可以获得更好的泛化性能。
9.1 模型容量
通俗地说,模型的容量或表达能力是指模型对复杂函数的拟合能力。反映模型能力的一个指标是模型假设空间的大小,即模型所能表示的函数集的大小。假设空间越大、越完整,就越有可能从假设空间中搜索接近真实模型的函数。反之,如果假设空间非常有限,就很难找到一个近似真实模型的函数。
考虑从真实分布中抽样:
从真实分布中抽取少量点组成训练集,训练集包含观测误差 ϵ ,如图 9-1 中的小点所示。如果我们只搜索所有一次多项式的模型空间,并将偏差设为 0,即 y = ax ,如图 9-1 中一次多项式的直线所示。那么就很难找到一条非常接近真实数据分布的直线。稍微增加假设空间,使假设空间都是三次多项式函数,即y=ax3+bx2+CX,很明显这个假设空间明显大于一次多项式的假设空间,我们可以找到一条曲线(如图 9-1 所示)反映了再次增加假设空间使可搜索函数为 5 次多项式,即y=ax5+bx4+CX+3+dx2+ex。在这个假设空间中,可以搜索到更好的函数,如图 9-1 中的 5 次多项式所示。再次增大假设空间后,如图 9-1 中 7、9、11、13、15、17 的多项式曲线所示,函数的假设空间越大,越有可能找到更接近真实分布的函数模型。
图 9-1
多项式能力
然而,过大的假设空间无疑会增加搜索难度和计算成本。事实上,在有限计算资源的约束下,更大的假设空间不一定能搜索到更好的模型。由于观测误差的存在,更大的假设空间可能包含更多的表达能力太强的函数,这些函数也可以学习训练样本的观测误差,从而伤害模型的泛化能力。选择合适的模型容量是一个难题。
9.2 过度配合和不足配合
由于真实数据的分布往往是未知且复杂的,因此无法推导出分布函数的类型及相关参数。因此,在选择学习模型的容量时,人们往往根据经验值选择稍大的模型容量。然而,当模型的容量过大时,它可能在训练集上表现得更好,但在测试集上表现得更差,如图 9-2 所示。当模型的容量过小时,可能在训练集和测试集的性能都很差,如图 9-2 中红色竖线左侧区域所示。
图 9-2
模型容量与误差的关系[1]
当模型的容量过大时,网络模型除了学习训练集数据的模态外,还会学习额外的观测误差,导致学习后的模型在训练集上表现较好,但在看不见的样本上表现较差,即模型的泛化能力较弱。我们称这种现象为过拟合。当模型的容量太小时,模型不能很好地学习训练集数据的模态,导致训练集和看不见的样本的性能都很差。我们称这种现象为欠拟合。
这里有一个简单的例子来解释模型的容量和数据分布之间的关系。图 9-3 描绘了某些数据的分布。可以粗略推测,数据可能属于某个 2 次多项式分布。如果使用简单的线性函数进行学习,会发现很难学习到更好的函数,导致训练集和测试集表现不佳的欠拟合现象,如图 9-3 (a) 。但如果使用更复杂的函数模型进行学习,有可能学习到的函数会过度“拟合”训练集样本,而导致在测试集上表现不佳,即过拟合,如图 9-3 (c) 。只有当学习到的模型与真实模型的容量大致匹配时,模型才能具有良好的泛化能力,如图 9-3 (b) 所示。
图 9-3
过拟合和欠拟合
考虑数据点的分布 p 数据(x, y ),其中
采样时加入随机高斯噪声,得到 120 个点的数据集,如图 9-4 所示。图中曲线为真实模型函数,黑色圆点为训练样本,绿色矩阵点为测试样本。
图 9-4
数据集和实函数
在已知真实模型的情况下,设计一个具有适当容量的函数空间来获得一个好的学习模型是很自然的。如图 9-5 所示,我们假设模型为二次多项式模型,学习到的函数曲线近似真实模型。然而,在实际场景中,真实的模型往往是未知的,因此如果设计假设空间太小,将无法搜索到合适的学习模型。如果设计假设空间过大,会导致模型泛化能力差。
图 9-5
适当的模型能力
那么如何选择机型的容量呢?统计学习理论为我们提供了一些思路。VC 维(Vapnik-Chervonenkis 维)是一种广泛使用的度量函数容量的方法。虽然这些方法为机器学习提供了一定程度的理论保障,但是这些方法很少应用于深度学习。部分原因是神经网络过于复杂,无法确定网络结构背后数学模型的 VC 维数。
虽然统计学习理论很难给出一个神经网络所需的最小容量,但它可以用来指导一个基于奥卡姆剃刀的神经网络的设计和训练。奥卡姆剃刀原则是由奥卡姆的威廉提出的解决规则,他是 14 世纪的逻辑学家和方济各会的方济各会修士。他在书中声明“不要浪费更多的东西,做那些用更少的东西就能做好的事情。”换句话说,如果两层神经网络结构可以很好地表达真实模型,那么三层神经网络也可以很好地表达,但我们应该更喜欢使用更简单的两层神经网络,因为它的参数数量更小,更容易训练,更容易通过较少的训练样本获得良好的泛化误差。
装配不足
让我们考虑一下拟合不足的现象。如图 9-6 所示,黑点和绿色矩形是从抛物线函数的分布中独立采样的。因为我们已经知道真实的模型,如果用一个比真实模型容量低的线性函数来拟合数据,模型很难有好的表现。具体表现为学习出来的线性模型在训练集上的误差(如均方差)较大,在测试集上的误差也较大。
图 9-6
典型的欠拟合模型
当我们发现当前模型在训练集上一直保持着较高的误差,难以优化和降低误差,在测试集上也表现不佳时,就可以考虑是否存在欠拟合的现象。可以通过增加神经网络的层数或增加中间维度的大小来解决欠拟合的问题。然而,因为现代深度神经网络模型可以容易地到达更深的层,所以用于学习的模型的容量通常是足够的。在实际应用中,会出现更多的过拟合现象。
过度装配
考虑同一个问题,训练集的黑点和测试机的绿色矩形分别从一个分布相同的抛物线模型中独立采样。当我们将模型的假设空间设置为 25 次多项式时,它远大于真实模型的功能容量。发现学习后的模型很可能会过拟合训练样本,导致学习模型对训练样本的误差很小,甚至小于真实模型对训练集的误差。但对于测试样本,模型性能急剧下降,泛化能力很差,如图 9-7 。
图 9-7
典型的过拟合模型
现代深度神经网络很容易出现过拟合现象,主要是因为神经网络具有非常强的表达能力,而训练集中的样本数量不够,很容易出现神经网络容量过大的情况。那么如何有效地检测和减少过拟合呢?
接下来,我们将介绍一系列有助于检测和抑制过拟合的方法。
9.3 数据集划分
前面我们介绍过,数据集需要分为训练集和测试集。为了选择模型超参数和检测过拟合,通常需要将原始训练集分成新的训练集和验证集,即需要将数据集分成三个子集:训练集、验证集和测试集。
9.3.1 验证集和超参数
前面已经介绍了训练集和测试集之间的区别。训练集 D train 用于训练模型参数,测试集 D test 用于测试模型的泛化能力。测试集中的样本不能参与模型训练,妨碍了模型对数据特征的“记忆”,损害了模型的泛化能力。训练集和测试集都是从相同的数据分布中采样的。例如,MNIST 手写数字图片集共有 70,000 幅样本图片,其中 60,000 幅图片用作训练集,其余 10,000 幅图片用于测试集。用户可以定义训练集和测试集的分离比。比如 80%的数据用于训练,剩下的 20%用于测试。当数据集规模较小时,为了更准确地检验模型的泛化能力,可以适当增加测试集的比例。图 9-8 展示了 MNIST 手写数字图片集的划分:80%用于训练,剩下的 20%用于测试。
图 9-8
训练和测试数据集部门
但是,仅将数据集分为训练集和测试集是不够的。因为测试集的性能不能用作模型训练的反馈,所以我们需要能够在模型训练期间挑选出更合适的模型超参数,以确定模型是否过拟合。因此,我们需要将训练集分为训练集和验证集,如图 9-9 所示。划分的训练集具有与原始训练集相同的功能,并且用于训练模型的参数,而验证集用于选择模型的超参数。其职能包括:
图 9-9
培训、验证和测试数据集
-
调整学习率、权重衰减系数、训练次数等。根据验证集的性能来设置。
-
根据验证集的性能重新调整网络拓扑。
-
根据验证集的性能,确定它是过拟合还是欠拟合。
类似于训练集-测试集的划分,训练集、验证集和测试集可以根据自定义的比例来划分,例如常见的 60%-20%-20%划分。图 9-9 为该分部的 MNIST 笔迹数据集示意图。
验证集和测试集的区别在于,算法设计者可以根据验证集的性能来调整模型的各种超参数的设置,以提高模型的泛化能力,但不能用测试集的性能来调整模型。否则,测试集和验证集的功能会重叠,因此测试集上的性能不会代表模型的泛化能力。
事实上,一些开发人员会错误地使用测试集来选择最佳模型,然后将其作为模型泛化性能报告。对于那些情况,测试集实际上是验证集,所以报告的“泛化性能”本质上是验证集上的性能,而不是真正的泛化性能。为了防止这种“作弊”,可以选择生成多个测试集,这样即使开发者使用其中一个测试集来选择模型,我们也可以使用其他测试集来评估模型,这也是 Kaggle 比赛中常用的方法。
提前停止
一般我们把训练集中的一次批量更新称为一步,把训练集中的所有样本迭代一次称为一个历元。在几个步骤或时期之后,可以使用验证集来计算模型的验证性能。如果验证步骤过于频繁,它可以准确地观察模型的训练状态,但也会引入额外的计算成本。通常建议在几个时期后执行验证操作。
以分类任务为例,训练性能指标包括训练误差、训练精度等。相应的,验证过程中也有验证误差和验证精度,测试过程中也有测试误差和测试精度。训练精度和验证精度可以大致推断出模型是过拟合还是欠拟合。如果模型的训练误差低,训练精度高,但验证误差高,验证准确率低,就可能出现过拟合。如果训练集和验证集的误差都很高,而精度很低,则可能出现欠拟合。
当观察到过拟合时,可以重新设计网络模型的容量,如减少网络的层数,减少网络的参数个数,增加正则化方法,增加对假设空间的约束,使模型的实际容量减少来解决过拟合现象。当观察到欠拟合现象时,可以尝试增加网络的容量,比如加深网络的层数,增加网络参数的个数,尝试更复杂的网络结构。
事实上,由于网络的实际容量可以随着训练的进行而改变,即使具有相同的网络设置,也可以观察到不同的过拟合和欠拟合情况。图 9-10 显示了分类问题的典型训练曲线。红色曲线是训练精度,蓝色曲线是测试精度。从图中可以看出,随着训练前期训练的进行,模型的训练精度和测试精度都呈现出不断提高的趋势,此时并没有出现过拟合现象。在训练后期,即使是同样的网络结构,由于模型实际容量的变化,我们观察到了过拟合的现象。即训练精度不断提高,但泛化能力变弱(测试精度下降)。
这意味着,对于神经网络,即使网络超参数数量保持不变(即,网络的最大容量是固定的),该模型仍可能看起来过拟合,因为神经网络的有效容量与网络参数的状态密切相关。神经网络的有效容量可以非常大,并且也可以通过稀疏参数和正则化来降低有效容量。在训练的早中期,没有出现过拟合的现象。随着训练次数的增加,过拟合现象越来越严重。在图 9-10 中,垂直虚线处于网络的最佳状态,没有明显的过拟合现象,网络的泛化能力最好。
图 9-10
培训流程图
那么如何选择合适的纪元提前停止训练(提前停止)以避免过拟合呢?我们可以通过观察验证度量的变化来预测最合适的历元的可能位置。具体来说,对于分类问题,我们可以记录模型的验证精度,并监控其变化。当发现验证精度对于连续的历元没有降低时,我们可以预测最合适的历元可能已经到达,因此我们可以停止训练。图 9-11 绘制了特定训练过程中训练和验证精度随训练时期的变化曲线。可以观察到,当 Epoch 在 30 左右时,模型达到最优状态,我们可以提前停止训练。
图 9-11
训练曲线示例
算法 1 是使用早期停止模型训练算法的伪代码。
| **算法 1:提前停止的网络训练** | | **初始化参数***θ***重复****为** ***步=*** **1** ***,*** … ***,N*** **做****随机选择批**{**、 ***和***}**~ d12】**** *****【θ】*******【l12】********结束
如果每隔第 n 个纪元做
计算验证集 {( x ,y)}~ D**val性能
如果某些连续步骤的验证性能没有提高,则执行
保存网络并停止训练
结束******** | | 做直到训练达到最大历元使用保存的网络计算测试集 {( x ,y)}~ D测试 性能****输出:网络参数 θ 和测试精度 |
9.4 模型设计
验证集可以确定网络模型是过拟合还是欠拟合,这为调整网络模型的容量提供了基础。对于神经网络来说,网络的层数和参数是网络容量非常重要的参考指标。通过减少层数和减少每层网络参数的大小,可以有效地降低网络容量。相反,如果发现模型欠拟合,我们可以通过增加层数和每层中的参数数量来增加网络的容量。
为了演示网络层数对网络容量的影响,我们可视化了分类任务的决策边界。图 [9-12 ,图 9-13 ,图 9-14 ,图 9-15 分别展示了不同网络层下训练两类分类任务的决策边界图,其中红色矩形块和蓝色圆形块分别代表训练集上的两类样本。在保持其他超参数一致的情况下,只调整网络的层数。如图,可以看到随着网络层数的增加,学习到的模型决策边界越来越接近训练样本,表示过拟合。对于这个任务,两层神经网络可以获得良好的泛化能力。网络的更深层不会提高整体模型性能。反而会导致过拟合,泛化能力变差,计算成本也更高。
图 9-15
六层
图 9-14
四层
图 9-13
三层
图 9-12
两层
9.5 正规化
通过设计具有不同层和大小的网络模型,可以为优化算法提供初始函数假设空间,但是模型的实际容量可以随着网络参数的优化和更新而改变。以多项式函数模型为例:
前一个型号的容量可以简单的通过 n 来衡量。在训练过程中,如果网络参数 β k + 1 、⋯、 β n 都为 0,则网络的实际容量退化为 kth 多项式的函数容量。因此,通过限制网络参数的稀疏性,可以约束网络的实际容量。
这种约束通常通过向损失函数添加额外的参数稀疏惩罚来实现。添加约束前的优化目标是:
在给模型的参数添加附加约束后,优化的目标变成:
其中ω(θ)表示网络参数 θ 上的稀疏约束函数。一般情况下,参数 θ 的稀疏性约束是通过约束参数的 L 范数来实现的,即:
其中‖θIT5l代表参数 θ i 的 l 范数。
新的优化目标除了最小化原损失函数 L ( x , y ),还需要约束网络参数的稀疏性ω(θ)。优化算法将在降低 L ( x , y )的同时,尽可能降低网络参数稀疏度ω(θ)。这里 λ 是平衡 L ( x , y )和ω(θ)重要性的权重参数。更大的 λ 意味着网络的稀疏性更重要;更小的 λ 意味着网络的训练误差更重要。通过选择合适的 λ ,可以获得更好的训练性能,同时保证网络的稀疏性,从而获得良好的泛化能力。
常用的正则化方法有 L0、L1 和 L2 正则化。
L0 正则化
L0 正则化是指以 L0 范数为稀疏罚项的正则化计算方法ω(θ),即:
L0 范数‖θI‖0定义为 θ i 中非零元素的个数。的约束可以强制网络中的连接权重大部分为 0,从而减少网络参数的实际数量和网络容量。然而,由于 L0 范数不可导,梯度下降算法不能用于优化。L0 范数在神经网络中不常用。
L1 正规化
将 L1 范数作为稀疏惩罚项ω(θ)的正则化计算方法称为 L1 正则化,即:
L1 范数‖θI‖1定义为张量 θ i 中所有元素的绝对值之和。L1 正则化也称为 Lasso 正则化,它是连续可导的,广泛应用于神经网络中。
L1 正则化可以如下实现:
# Create weights w1,w2
w1 = tf.random.normal([4,3])
w2 = tf.random.normal([4,2])
# Calculate L1 regularization term
loss_reg = tf.reduce_sum(tf.math.abs(w1))\
+ tf.reduce_sum(tf.math.abs(w2))
L2 正规化
将 L2 范数作为稀疏惩罚项ω(θ)的正则化计算方法称为 L2 正则化,即:
L2 范数‖θIT52定义为张量 θ i 中所有元素的平方和。L2 正则化也叫岭正则化,和 L1 正则化一样是连续可导的,在神经网络中有着广泛的应用。
L2 正则化项实现如下:
# Create weights w1,w2
w1 = tf.random.normal([4,3])
w2 = tf.random.normal([4,2])
# Calculate L2 regularization term
loss_reg = tf.reduce_sum(tf.square(w1))\
+ tf.reduce_sum(tf.square(w2))
规则化效应
继续以月牙形二类数据为例。在网络结构等其他超参数不变的情况下,在损失函数中加入 L2 正则项,使用不同的正则化超参数 λ 获得不同程度的正则化效果。
经过 500 个历元的训练,我们得到学习模型的分类决策边界,如图 9-16 ,图 9-17 ,图 9-18 ,图 9-19 所示。该分布表示当使用正则化系数 λ = 0.00001,0.001,0.1,和 0.13 时的分类效果。可以看出,随着正则化系数的增加,参数稀疏的网络惩罚变得更大,从而迫使优化算法搜索使网络容量更小的模型。当 λ = 0.00001 时,正则化效果相对较弱,网络过拟合。然而,当在 λ = 0.1,网络已经优化到合适的容量,没有明显的过拟合或欠拟合。
在实际训练中,一般倾向于尝试较小的正则化系数来观察网络是否过拟合。然后尝试逐渐增加参数 λ 来增加网络参数的稀疏性,提高泛化能力。但是过大的 λ 可能会导致网络不收敛,需要根据实际任务进行调整。
图 9-19
正则化参数:0.13
图 9-18
正则化参数:0.1
图 9-17
正则化参数:0.001
图 9-16
正则化参数:0.00001
在不同的正则化系数下,统计了网络中各连接权的取值范围。考虑网络第二层的权重矩阵 W ,其形状为【256,256】,即把一个输入长度为 256 的向量转换成一个输出长度为 256 的向量。从全连接层的权重连接来看,权重 W 包含 256 条连接线。我们将它们对应到图 9-20 ,图 9-21 ,图 9-22 ,图 9-23 中的 XY 网格,其中 X 轴范围为【0,255】,Y 轴范围为【0,255】。XY 网格的所有整数点分别代表形状【256,256】的权重张量 W 的每个位置,每个网格点表示当前连接的权重。从图中可以看出不同程度的正则化约束对网络权值的影响。当 λ = 0.00001 时,正则化的效果相对较弱,网络中的权值相对较大,主要分布在区间[1.6088,1.1599]内。将值增加到 λ = 0.13 后,网络权重值被限制在一个较小的范围内(0.1104,0.0785)。如表 9-1 所示,也可以观察到正则化后权重的稀疏性。
图 9-23
正则化参数:0.13
图 9-22
正则化参数:0.1
图 9-21
正则化参数:0.001
图 9-20
正则化参数:0.00001
表 9-1
正则化后的权重变化
| |min(W
|
max(WT7)
|
意为 ( W )
| | --- | --- | --- | --- | | 0.00001 | -1.6088 | 1.1599 | 0.0026 | | Zero point zero zero one | -0.1393 | 0.3168 | 0.0003 | | Zero point one | -0.0969 | 0.0832 | Zero | | Zero point one three | -0.1104 | 0.0785 | Zero |
9.6 辍学
2012 年,Hinton 等人在他们的论文“通过防止特征检测器的共同适应来改善神经网络”中使用了 dropout 方法来改善模型性能。Dropout 法通过随机断开神经网络,减少每次训练时模型实际参与计算的参数数量。但是,在测试期间,dropout 方法将恢复所有连接,以确保模型测试期间的最佳性能。
图 9-24 是某次正向计算时全连通层网络的连接状态示意图。图 9-24(a) 是一个标准的全连接神经网络。当前节点连接到前一层中的所有输入节点。在添加了丢包函数的网络层中,如图 9-24(b) 所示,每条连接是否断开都符合某种预设的概率分布,比如带有断开概率的伯努利分布图 9-24(b) 所示为具体的采样结果。虚线表示采样结果是断开的线,实线表示采样结果没有断开。
图 9-24
漏失图
在 TensorFlow 中,可以通过 tf.nn.dropout(x,rate)函数实现 dropout 函数,其中 rate 参数设置断开连接的概率 p 。例如:
# Add dropout operation with disconnection rate of 0.5
x = tf.nn.dropout(x, rate=0.5)
也可以使用 dropout 作为网络层,并在网络中间插入 Dropout 层。例如:
# Add Dropout layer with disconnection rate of 0.5
model.add(layers.Dropout(rate=0.5))
为了探究脱落层对网络训练的影响,我们保持网络层数等超参数不变,通过在 5 个全连通层中插入不同数量的脱落层,观察脱落对网络训练的影响。如图 9-25 、图 9-26 、图 9-27 、图 9-28 所示,分布绘制了不加脱落层、加一层、二层、四层脱落层的网络模型的决策边界效应。可以看出,当不添加漏失层时,网络模型与前面的观察结果相同。随着脱落层的增加,网络模型在训练时的实际容量减小,泛化能力变强。
图 9-28
具有四个脱落层
图 9-27
具有两个脱落层
图 9-26
有一个脱落层
图 9-25
无脱落层
9.7 数据增长
除了前面描述的可以有效检测和抑制过拟合的方法之外,增加数据集的大小是解决过拟合问题的最重要的方法。然而,收集样本数据和标签通常成本高昂。对于有限的数据集,可以通过数据扩充技术增加训练样本的数量,以获得一定程度的性能提升。数据扩充是指在保持样本标签不变的情况下,基于先验知识改变样本的特征,使新生成的样本也符合或近似符合数据的真实分布。
以图像数据为例,我们来介绍一下如何做数据增广。数据集中图片的大小经常不一致。为了便于神经网络的处理,需要将图片重新缩放到固定大小,如图 9-29 所示,这是重新缩放后固定大小的 224 × 224 图片。对于图片中的人,根据先验知识,我们知道旋转、缩放、平移、裁剪、改变视角、遮挡某个局部区域都不会改变图片的主类别标签,所以对于图片数据,有多种数据增强方法。
图 9-29
重新调整为 224 × 224 像素后的图片
TensorFlow 提供常见的图像处理功能,位于 tf.image 子模块中。通过 tf.image.resize 函数,我们可以对图片进行缩放。我们通常在预处理步骤中实现数据扩充。在从文件系统中读取图片之后,可以执行图像数据扩充操作。例如:
def preprocess(x,y):
# Preprocess function
# x: picture path, y:picture label
x = tf.io.read_file(x)
x = tf.image.decode_jpeg(x, channels=3) # RGBA
# rescale pictures to 244x244
x = tf.image.resize(x, [244, 244])
旋转
旋转图片是扩充图片数据的一种非常常见的方式。将原图片旋转一定角度,可以得到不同角度的新图片,这些图片的标签信息保持不变,如图 9-30 所示。
图 9-30
图像旋转
通过 tf.image.rot90(x,k = 1)可以将图片逆时针旋转 90 度 k 次,例如:
# Picture rotates 180 degrees counterclockwise
x = tf.image.rot90(x,2)
翻转
画面的翻转分为沿水平轴翻转和沿垂直轴翻转,分别如图 9-31 和图 9-32 所示。在 TensorFlow 中,可以使用 TF . image . random _ flip _ left _ right 和 tf.image.random_flip_up_down 在水平和垂直方向随机翻转图像,例如:
图 9-32
垂直翻转
图 9-31
水平翻转
# Random horizontal flip
x = tf.image.random_flip_left_right(x)
# Random vertical flip
x = tf.image.random_flip_up_down(x)
9.7.3 种植
通过去除原始图像的左、右或上下方向的部分边缘像素,可以保持图像的主体不变,同时可以获得新的图像样本。实际裁剪时,图片一般会缩放到比网络输入尺寸稍大的尺寸,然后再裁剪到合适的尺寸。比如网络的输入尺寸是 224 × 224,那么你可以使用 resize 函数将图片重新缩放到 244 × 244,然后随机裁剪到 224 × 224 的尺寸。代码实现如下:
# Rescale picture to larger size
x = tf.image.resize(x, [244, 244])
# Then randomly crop the picture to the desired size
x = tf.image.random_crop(x, [224,224,3])
图 9-33 是缩放到 244 × 244 的图片,图 9-34 是随机裁剪到 244 × 244 的例子,图 9-35 也是随机裁剪的例子。
图 9-35
裁剪和重缩放后-2
图 9-34
裁剪和重缩放后-1
图 9-33
裁剪前
生成数据
通过在原始数据上训练生成模型并学习真实数据的分布,生成模型可用于获得新样本。这种方法也可以在一定程度上提高网络性能。比如条件生成对抗网络(conditional generation adversive network,简称 CGAN)可以生成标记样本数据,如图 9-36 所示。
图 9-36
CGAN 生成的数字
其他方法
除了先前描述的典型图片数据扩充方法之外,图片数据可以被任意变换以基于先验知识获得新的图片,而不改变图片标签信息。图 9-37 展示的是在原图片上叠加高斯噪声后的图片数据,图 9-38 展示的是通过改变图片的视角得到的新图片,图 9-39 展示的是对原图片的部分进行随机分块得到的新图片。
图 9-39
随机阻塞零件
图 9-38
改变视角
图 9-37
添加高斯噪声
9.8 手工过度装配
之前,我们使用了大量月牙形的两类数据集来演示网络模型在各种防止过拟合措施下的性能。在本节中,我们将基于月牙形的两个分类数据集的过拟合和欠拟合模型来完成练习。
9.8.1 构建数据集
我们使用的样本数据集的特征向量长度为 2,标签为 0 或 1,代表两个类别。在 scikit-learn 库中提供的 make_moons 工具的帮助下,我们可以生成任意数量数据的训练集。首先打开 cmd 命令终端并安装 scikit-learn 库。该命令如下所示:
# Install scikit-learn library
pip install -U scikit-learn
为了证明过拟合现象,我们仅采样了 1000 个样本,并添加了标准偏差为 0.25 的高斯噪声,如下所示:
# Import libraries
from sklearn.datasets import make_moons
# Randomly choose 1000 samples, and split them into training and testing sets
X, y = make_moons(n_samples = N_SAMPLES, noise=0.25, random_state=100)
X_train, X_test, y_train, y_test = train_test_split(X, y,
test_size = TEST_SIZE, random_state=42)
make_plot 函数可以根据样本的坐标 X 和样本的标签 y 方便地绘制出数据的分布图:
def make_plot(X, y, plot_name, file_name, XX=None, YY=None, preds=None):
plt.figure()
# sns.set_style("whitegrid")
axes = plt.gca()
axes.set_xlim([x_min,x_max])
axes.set_ylim([y_min,y_max])
axes.set(xlabel="$x_1$", ylabel="$x_2$")
# Plot prediction surface
if(XX is not None and YY is not None and preds is not None):
plt.contourf(XX, YY, preds.reshape(XX.shape), 25, alpha = 0.08, cmap=cm.Spectral)
plt.contour(XX, YY, preds.reshape(XX.shape), levels=[.5], cmap="Greys", vmin=0, vmax=.6)
# Plot samples
markers = ['o' if i == 1 else 's' for i in y.ravel()]
mscatter(X[:, 0], X[:, 1], c=y.ravel(), s=20,
cmap=plt.cm.Spectral, edgecolors='none', m=markers)
# Save the figure
plt.savefig(OUTPUT_DIR+'/'+file_name)
画出抽样的 1000 个样本的分布,如图 9-40 ,红色方块点为一类,蓝色圆圈为另一类。
图 9-40
月亮形两类数据点
# Plot data points
make_plot(X, y, None, "dataset.svg")
9.8.2 网络层数的影响
为了探索不同网络深度下的过拟合程度,我们一共进行了五个训练实验。当n∈【0,4】时,构建具有 n + 2 层的全连通层网络,通过 Adam 优化器训练 500 个历元,得到网络在训练集上的分离曲线,如图 9.12、9.13、9.14、9.15 所示。
for n in range(5): # Create 5 different network with different layers
model = Sequential()
# Create 1st layer
model.add(Dense(8, input_dim=2,activation='relu'))
for _ in range(n): # Add nth layer
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid')) # Add last layer
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) # Configure and train
history = model.fit(X_train, y_train, epochs=N_EPOCHS, verbose=1)
# Plot boundaries for different network
preds = model.predict_classes(np.c_[XX.ravel(), YY.ravel()])
title = "Network layer ({})".format(n)
file = "NetworkCapacity%f.png"%(2+n*1)
make_plot(X_train, y_train, title, file, XX, YY, preds)
9.8.3 辍学影响
为了探讨辍学层对网络训练的影响,我们一共进行了五个实验。每个实验使用七层全连接层网络进行训练,但在全连接层中间隔插入 0~4 个漏层,并通过 Adam 优化器训练 500 个历元。网络训练结果如图 9.25、9.26、9.27 和 9.28 所示。
for n in range(5): # Create 5 different networks with different number of Dropout layers
model = Sequential()
# Create 1st layer
model.add(Dense(8, input_dim=2,activation='relu'))
counter = 0
for _ in range(5): # Total number of layers is 5
model.add(Dense(64, activation='relu'))
if counter < n: # Add n Dropout layers
counter += 1
model.add(layers.Dropout(rate=0.5))
model.add(Dense(1, activation='sigmoid')) # Output layer
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) # Configure and train
# Train
history = model.fit(X_train, y_train, epochs=N_EPOCHS, verbose=1)
# Plot decision boundaries for different number of Dropout layers
preds = model.predict_classes(np.c_[XX.ravel(), YY.ravel()])
title = "Dropout({})".format(n)
file = "Dropout%f.png"%(n)
make_plot(X_train, y_train, title, file, XX, YY, preds)
9.8.4 正规化的影响
为了探讨正则化系数对网络模型训练的影响,我们采用 L2 正则化方法构建了一个五层神经网络,其中第二、三、四层神经网络的权张量 W 加入了 L2 正则化约束项,如下所示:
def build_model_with_regularization(_lambda):
# Create networks with regularization terms
model = Sequential()
model.add(Dense(8, input_dim=2,activation='relu')) # without regularization
model.add(Dense(256, activation='relu', # With L2 regularization
kernel_regularizer=regularizers.l2(_lambda)))
model.add(Dense(256, activation='relu', # With L2 regularization
kernel_regularizer=regularizers.l2(_lambda)))
model.add(Dense(256, activation='relu', # With L2 regularization
kernel_regularizer=regularizers.l2(_lambda)))
# Output
model.add(Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) # Configure and train
return model
在保持网络结构不变的情况下,我们调整正则化系数 λ = 0.00001,0.001,0.1,0.12,0.13 来测试网络的训练效果,并在训练集上绘制学习模型的决策边界曲线,如图 9-16 ,图 9-17 ,图 9-18 ,图 9-19 所示
for _lambda in [1e-5,1e-3,1e-1,0.12,0.13]:
# Create model with regularization term
model = build_model_with_regularization(_lambda)
# Train model
history = model.fit(X_train, y_train, epochs=N_EPOCHS, verbose=1)
# Plot weight range
layer_index = 2
plot_title = "Regularization-[lambda = {}]".format(str(_lambda))
file_name = " Regularization _" + str(_lambda)
# Plot weight ranges
plot_weights_matrix(model, layer_index, plot_title, file_name)
# Plot decision boundaries
preds = model.predict_classes(np.c_[XX.ravel(), YY.ravel()])
title = " regularization ".format(_lambda)
file = " regularization %f.svg"%_lambda
make_plot(X_train, y_train, title, file, XX, YY, preds)
矩阵 3D 绘图功能的 plot_weights_matrix 代码如下:
def plot_weights_matrix(model, layer_index, plot_name, file_name):
# Plot weight ranges
# Get weights for certain layers
weights = model.layers[LAYER_INDEX].get_weights()[0]
# Get minimum, maximum and mean values
min_val = round(weights.min(), 4)
max_val = round(weights.max(), 4)
mean_val = round(weights.mean(), 4)
shape = weights.shape
# Generate grids
X = np.array(range(shape[1]))
Y = np.array(range(shape[0]))
X, Y = np.meshgrid(X, Y)
print(file_name, min_val, max_val,mean_val)
# Plot 3D figures
fig = plt.figure()
ax = fig.gca(projection='3d')
ax.xaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
ax.yaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
ax.zaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
# Plot weight ranges
surf = ax.plot_surface(X, Y, weights, cmap=plt.get_cmap('rainbow'), linewidth=0)
ax.set_xlabel('x', fontsize=16, rotation = 0)
ax.set_ylabel('y', fontsize=16, rotation = 0)
ax.set_zlabel('weight', fontsize=16, rotation = 90)
# save figure
plt.savefig("./" + OUTPUT_DIR + "/" + file_name + ".svg")
9.9 参考文献
- I. Goodfellow,Y. Bengio 和 a .库维尔,《深度学习》,麻省理工学院出版社,2016 年。