TensorFlow 2.0深度学习算法实战 第六章 神经网络

很难想象哪一个大行业不会被人工智能改变。人工智 能会在这些行业里发挥重大作用,这个走向非常明 显。−吴恩达

机器学习的最终目的是找到一组良好的参数𝜃,使得𝜃表示的数学模型能够很好地从训练集中学到映射关系 f θ : x → y , x , y ∈ D t r a i n f_{\theta}: x \rightarrow y, x, y \in \mathbb{D}^{t r a i n} fθ:xy,x,yDtrain,从而利用训练好的 f θ ( x ) , x ∈ D test f_{\theta}(x), x \in \mathbb{D}^{\text {test}} fθ(x),xDtest去预测新样本。神经网络属于机器学习的一个研究分支,它特指利用多个神经元去参数化映射函数 f θ f_{\theta} fθ的模型。

6.1 感知机

1943 年,美国神经科学家 Warren Sturgis McCulloch 和数理逻辑学家 Walter Pitts 从生物神经元的结构上得到启发,提出了人工神经元的数学模型,这进一步被美国神经物理学家 Frank Rosenblatt 发展并提出了感知机(Perceptron)模型。1957 年,Frank Rosenblatt 在一台 IBM-704 计算机上面模拟实现了他发明的感知机模型,这个网络模型可以完成一些简单的视觉分类任务,比如区分三角形、圆形、矩形等 (尼克, 2017),图 6.1 是 FrankRosenblatt 和硬件实现的感知机。

感知机模型的结构如图 6.1 所示,它接受长度为𝑛的一维向量𝒙 = [𝑥1, 𝑥2, … , 𝑥𝑛],每个输入节点通过权值为𝑤𝑖, 𝑖𝜖[1, 𝑛]的连接汇集为
z = w 1 x 1 + w 2 x 2 + ⋯ + w n x n + b z=w_{1} x_{1}+w_{2} x_{2}+\cdots+w_{n} x_{n}+b z=w1x1+w2x2++wnxn+b
其中 b 称为感知机的偏置(Bias),一维向量𝒘 = [w1,w2, … ,wn]称为感知机的权值(Weight),
在这里插入图片描述

z 称为感知机的净活性值(Net Activation)。写成向量形式:
z = w T x + b z=\boldsymbol{w}^{T} \boldsymbol{x}+b z=wTx+b
可以看到感知机是线性模型,并不能处理线性不可分问题。通过在线性模型后添加激活函数(Activation function)后得到活性值(Activation):
a = σ ( z ) = σ ( w T x + b ) a=\sigma(z)=\sigma\left(\boldsymbol{w}^{T} \boldsymbol{x}+b\right) a=σ(z)=σ(wTx+b)

其中激活函数可以是阶跃函数(Step function)。如图所示,阶跃函数的输出只有 0/1 两种数值,当𝑧 < 0时输出 0,代表类别 0;当𝑧 ≥ 0时输出 1,代表类别 1:
a = { 1 w T x + b ≥ 0 0 w T x + b ≤ 0 a=\left\{\begin{array}{ll} 1 & \boldsymbol{w}^{T} \boldsymbol{x}+b \geq 0 \\ 0 & \boldsymbol{w}^{T} \boldsymbol{x}+b \leq 0 \end{array}\right. a={10wTx+b0wTx+b0
也可以是符号函数(Sign function),如图 6.3 所示:
a = { 1 w T x + b ≥ 0 − 1 w T x + b ≤ 0 a=\left\{\begin{array}{ll} 1 & \boldsymbol{w}^{T} \boldsymbol{x}+b \geq 0 \\ -1 & \boldsymbol{w}^{T} \boldsymbol{x}+b \leq 0 \end{array}\right. a={11wTx+b0wTx+b0
在这里插入图片描述

添加激活函数后,感知机可以用来完成二分类任务的分类。阶跃函数和符号函数在𝑧 =0处是不连续的,其他位置导数为 0,无法利用梯度下降算法进行参数优化

为了能够让感知机模型能够从数据中间自动学习,Frank Rosenblatt 提出了感知机的学习算法,如算法 1 所示。
在这里插入图片描述其中𝜼为学习率。

虽然感知机提出之处被寄予了良好的发展潜力,但是 Marvin Lee Minsky 和 SeymourPapert 于 1969 年在《Perceptrons》书中证明了以感知机为代表的线性模型不能解决异或(XOR)等线性不可分问题,这直接导致了当时新兴地神经网络的研究进入了低谷期。尽管感知机模型不能解决线性不可分问题,但书中也提到通过嵌套多层神经网络可以解决。

6.2 全连接层

感知机模型的不可导特性严重约束了它的潜力,使得它只能解决极其简单的任务。实际上,现代深度学习动辄数百万甚至上亿的参数规模,它的核心结构与感知机并没有多大差别,它在感知机的基础上,将不连续的阶跃激活函数换成了其他平滑连续激活函数并通过堆叠多层网络层来增强网络的表达能力

本节我们通过替换感知机的激活函数,同时并行堆叠多个神经元来实现多输入、多输出的网络层。如图 6.4 所示,并行堆叠了 2 个神经元,即 2 个替换了激活函数的感知机,构成 3 输入节点,2 个输出节点的网络层。其中第一个输出节点的输出
o 1 = σ ( w 11 ∗ x 1 + w 21 ∗ x 2 + w 31 ∗ x 3 + b 0 ) o_{1}=\sigma\left(w_{11} * x_{1}+w_{21} * x_{2}+w_{31} * x_{3}+b_{0}\right) o1=σ(w11x1+w21x2+w31x3+b0)
第二个输出节点的输出
o 2 = σ ( w 12 ∗ x 1 + w 22 ∗ x 2 + w 32 ∗ x 3 + b 1 ) o_{2}=\sigma\left(w_{12} * x_{1}+w_{22} * x_{2}+w_{32} * x_{3}+b_{1}\right) o2=σ(w12x1+w22x2+w32x3+b1)
输出向量为𝒐 = [𝑜1, 𝑜2]。整个网络层可以通过一次矩阵运算完成:
[ o 1 o 2 ] = [ x 1 x 2 x 3 ] @ [ W 11 W 12 w 21 w 22 w 31 w 32 ] + [ b 0 b 1 ] \left[\begin{array}{lllll} o_{1} & o_{2} \end{array}\right]=\left[\begin{array}{lll} x_{1} & x_{2} & x_{3} \end{array}\right] @\left[\begin{array}{ll} W_{11} & W_{12} \\ w_{21} & w_{22} \\ w_{31} & w_{32} \end{array}\right]+\left[\begin{array}{ll} b_{0} & b_{1} \end{array}\right] [o1o2]=[x1x2x3]@W11w21w31W12w22w32+[b0b1]

0 = X @ W + b 0=X @ W+b 0=X@W+b
其中输入矩阵 X X X shape 定义为[𝑏, d i n d_{in} din],𝑏为样本数量,此处只有 1 个样本参与前向运算, d i n d_{in} din为输入节点数;权值矩阵 W W W shape 定义为[ d i n d_{in} din, d o u t d_{out} dout], d o u t d_{out} dout为输出节点数,偏置向量 b b b shape定义为[ d o u t d_{out} dout]。

考虑批量并行计算,例如 2 个样本, x 1 = [ x 1 1 , x 2 1 , x 3 1 ] , x 2 = [ x 1 2 , x 2 2 , x 3 2 ] \boldsymbol{x}^{1}=\left[x_{1}^{1}, x_{2}^{1}, x_{3}^{1}\right], \quad x^{2}=\left[x_{1}^{2}, x_{2}^{2}, x_{3}^{2}\right] x1=[x11,x21,x31],x2=[x12,x22,x32],则可以方便地将公式推广到批量形式:
[ o 1 1 o 2 1 o 1 2 o 2 2 ] = [ x 1 1 x 2 1 x 3 1 x 1 2 x 2 2 x 3 2 ]   @ [ w 11 w 12 w 21 w 22 w 31 w 32 ] + [ b 0 b 1 ] \left[\begin{array}{cc} o_{1}^{1} & o_{2}^{1} \\ o_{1}^{2} & o_{2}^{2} \end{array}\right]=\left[\begin{array}{ccc} x_{1}^{1} & x_{2}^{1} & x_{3}^{1} \\ x_{1}^{2} & x_{2}^{2} & x_{3}^{2} \end{array}\right] \text { } @\left[\begin{array}{cc} w_{11} & w_{12} \\ w_{21} & w_{22} \\ w_{31} & w_{32} \end{array}\right]+\left[\begin{array}{ll} b_{0} & b_{1} \end{array}\right] [o11o12o21o22]=[x11x12x21x22x31x32] @w11w21w31w12w22w32+[b0b1]
其中输出矩阵o包含了𝑏个样本的输出特征,shape 为[𝑏, d o u t d_{out} dout]。由于每个输出节点与全部的输入节点相连接,这种网络层称为全连接层(Fully-connected Layer),或者稠密连接层(Dense Layer),W 矩阵叫做全连接层的权值矩阵,𝒃向量叫做全连接层的偏置
在这里插入图片描述

6.2.1 张量方式实现

在 TensorFlow 中,要实现全连接层,只需要定义好权值张量 W 和偏置张量 b,并利用TensorFlow 提供的批量矩阵相乘函数 tf.matmul()即可完成网络层的计算。

例如,创建输入 X X X 矩阵为𝑏 = 2个样本,每个样本的输入特征长度为 d i n d_{in} din= 784,输出节点数为 d o u t d_{out} dout=256,故定义权值矩阵 W W W 的 shape 为[784,256],并采用正态分布初始化 W;偏置向量 b b b 的shape 定义为[256],在计算完 X @ W X@W X@W后相加即可,最终全连接层的输出 O O O 的 shape 为[2,256],即 2 个样本的特征,每个特征长度为 256。

# 创建 W,b 张量
x = tf.random.normal([2,784])
w1 = tf.Variable(tf.random.truncated_normal([784, 256], stddev=0.1))
b1 = tf.Variable(tf.zeros([256]))
o1 = tf.matmul(x,w1) + b1 # 线性变换
o1 = tf.nn.relu(o1) # 激活函数

<tf.Tensor: id=31, shape=(2, 256), dtype=float32, numpy=
array([[ 1.51279330e+00, 2.36286330e+00, 8.16453278e-01,
 1.80338228e+00, 4.58602428e+00, 2.54454136e+00,

6.2.2 层方式实现

全连接层本质上是矩阵的相乘相加运算,实现并不复杂。但是作为最常用的网络层之一,TensorFlow 中有更加高层、使用更方便的层实现方式:layers.Dense(units, activation)只需要指定输出节点数 Units 和激活函数类型即可。输入节点数将根据第一次运算时的输入 shape 确定,同时根据输入、输出节点数自动创建并初始化权值矩阵 W 和偏置向量 b,使用非常方便。其中 activation 参数指定当前层的激活函数,可以为常见的激活函数或自定义激活函数,也可以指定为 None 无激活函数。

from tensorflow.keras import layers # 导入层模块

x = tf.random.normal([4,28*28])
# 创建全连接层,指定输出节点数和激活函数
fc = layers.Dense(512, activation=tf.nn.relu)
h1 = fc(x) # 通过 fc 类完成一次全连接层的计算

<tf.Tensor: id=72, shape=(4, 512), dtype=float32, numpy=
array([[0.63339347, 0.2166
3809, 0. , ..., 1.7361937 , 0.39962345,
 2.4346168 ],

上述通过一行代码即可以创建一层全连接层 fc,并指定输出节点数为 512,输入的节点数在fc(x)计算时自动获取,并创建内部权值矩阵 W W W 和偏置 b b b。我们可以通过类内部的成员名kernelbias 来获取权值矩阵 W 和偏置 b:

fc.kernel # 获取 Dense 类的权值矩阵

<tf.Variable 'dense_1/kernel:0' shape=(784, 512) dtype=float32, numpy=
array([[-0.04067389, 0.05240148, 0.03931375, ..., -0.01595572,
 -0.01075954, -0.06222073],
 
fc.bias # 获取 Dense 类的偏置向量

<tf.Variable 'dense_1/bias:0' shape=(512,) dtype=float32, numpy=
array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,

在优化参数时,需要获得网络的所有待优化的参数张量列表,可以通过类的trainable_variables 来返回待优化参数列表:

fc.trainable_variables
# 返回待优化参数列表

[<tf.Variable 'dense_1/kernel:0' shape=(784, 512) dtype=float32,,
<tf.Variable 'dense_1/bias:0' shape=(512,) dtype=float32, numpy=]

实际上,网络层除了保存了待优化张量 trainable_variables,还有部分层包含了不参与梯度优化的张量,如后续介绍的 BatchNormalization 层,可以通过non_trainable_variables 成员返回所有不需要优化的参数列表。如果希望获得所有参数列表,可以通过类的 variables 返回所有内部张量列表:

fc.variables # 返回所有参数列表

[<tf.Variable 'dense_1/kernel:0' shape=(784, 512) dtype=float32,,
<tf.Variable 'dense_1/bias:0' shape=(512,) dtype=float32, numpy=]

对于全连接层,内部张量都参与梯度优化,故 variables 返回列表与 trainable_variables 一样。

利用网络层类对象进行前向计算时,只需要调用类的__call__方法即可,即写成 fc(x)方式,它会自动调用类的__call__方法,在__call__方法中自动调用 call 方法,全连接层类在 call 方法中实现了𝜎(𝑋@𝑊 + 𝒃)的运算逻辑,最后返回全连接层的输出张量。

6.3 神经网络

通过层层堆叠图 6.4 中的全连接层,保证前一层的输出节点数与当前层的输入节点数匹配,即可堆叠出任意层数的网络。我们把这种由神经元构成的网络叫做神经网络

如图6.5所示,通过堆叠 4 个全连接层,可以获得层数为 4 的神经网络,由于每层均为全连接层,称为全连接网络。其中第 1~3 个全连接层在网络中间,称之为隐藏层 1,2,3,最后一个全连接层的输出作为网络的输出,称为输出层。隐藏层 1,2,3 的输出节点数分别为[256,128,64],输出层的输出节点数为 10。
在这里插入图片描述

在设计全连接网络时,网络的结构配置等超参数可以按着经验法则自由设置,只需要遵循少量的约束即可。其中隐藏层 1 的输入节点数需和数据的实际特征长度匹配,每层的输入层节点数与上一层输出节点数匹配,输出层的激活函数和节点数需要根据任务的具体设定进行设计。

总的来说,神经网络结构的自由度较大,如图 6.5 层中每层的输出节点数不一定要设计为[256,128,64,10],可以自由搭配,如[256,256,64,10],或[512,64,32,10]等等,至于与哪一组超参数是最优的,这需要很多的领域经验知识和大量的实验尝试,或者可以通过 AutoML 技术搜索出较优设定。

6.3.1 张量方式实现

对于多层神经网络,以图 6.6 网络结构为例,分别定义各层的权值矩阵 W 和偏置向量b 如下:

# 隐藏层 1 张量
w1 = tf.Variable(tf.random.truncated_normal([784, 256], stddev=0.1))
b1 = tf.Variable(tf.zeros([256]))
# 隐藏层 2 张量
w2 = tf.Variable(tf.random.truncated_normal([256, 128], stddev=0.1))
b2 = tf.Variable(tf.zeros([128]))
# 隐藏层 3 张量
w3 = tf.Variable(tf.random.truncated_normal([128, 64], stddev=0.1))
b3 = tf.Variable(tf.zeros([64]))
# 输出层张量
w4 = tf.Variable(tf.random.truncated_normal([64, 10], stddev=0.1))
b4 = tf.Variable(tf.zeros([10]))

在计算时,只需要按照网络层的顺序,将上一层的输出送入当前层的输入即可,重复直至最后一层,将输出层的输出作为网络的输出:

with tf.GradientTape() as tape: # 梯度记录器
    # x: [b, 28*28]
    # 隐藏层 1 前向计算,[b, 28*28] => [b, 256]
    h1 = x@w1 + tf.broadcast_to(b1, [x.shape[0], 256])
    h1 = tf.nn.relu(h1)
    # 隐藏层 2 前向计算,[b, 256] => [b, 128]
    h2 = h1@w2 + b2
    h2 = tf.nn.relu(h2)
    # 隐藏层 3 前向计算,[b, 128] => [b, 64]
    h3 = h2@w3 + b3
    h3 = tf.nn.relu(h3)
    # 输出层前向计算,[b, 64] => [b, 10]
    h4 = h3@w4 + b4

最后一层是否需要添加激活函数通常视具体的任务而定,这里加不加都可以。在使用 TensorFlow 自动求导功能计算梯度时,需要将前向计算过程放置在tf.GradientTape()环境中,从而利用GradientTape对象的 gradient()方法自动求解参数的梯度,并利用 optimizers 对象更新参数。

6.3.2 层方式实现

通过层方式实现起来更加简洁,首先新建各个网络层,并指定各层的激活函数类型:

# 导入常用网络层 layers
from tensorflow.keras import layers,Sequential
# 隐藏层 1
fc1 = layers.Dense(256, activation=tf.nn.relu) 
# 隐藏层 2
fc2 = layers.Dense(128, activation=tf.nn.relu) 
# 隐藏层 3
fc3 = layers.Dense(64, activation=tf.nn.relu) 
# 输出层
fc4 = layers.Dense(10, activation=None) 

在前向计算时,依序通过各个网络层即可:

x = tf.random.normal([4,28*28])
# 通过隐藏层 1 得到输出
h1 = fc1(x) 
# 通过隐藏层 2 得到输出
h2 = fc2(h1) 
# 通过隐藏层 3 得到输出
h3 = fc3(h2) 
# 通过输出层得到网络输出
h4 = fc4(h3) 

对于这种数据依次向前传播的网络,也可以通过 Sequential 容器封装成一个网络大类对象,调用大类的前向计算函数即可完成所有层的前向计算:

# 导入 Sequential 容器
from tensorflow.keras import layers,Sequential
# 通过 Sequential 容器封装为一个网络类
model = Sequential([
    layers.Dense(256, activation=tf.nn.relu) , # 创建隐藏层 1
    layers.Dense(128, activation=tf.nn.relu) , # 创建隐藏层 2
    layers.Dense(64, activation=tf.nn.relu) , # 创建隐藏层 3
    layers.Dense(10, activation=None) , # 创建输出层
])

前向计算时只需要调用一次网络大类对象即可完成所有层的按序计算:`

out = model(x) # 前向计算得到输出

6.3.3 优化目标

我们把神经网络从输入到输出的计算过程叫做前向传播(Forward propagation)。神经网络的前向传播过程,也是数据张量(Tensor)从第一层流动(Flow)至输出层的过程:从输入数据开始,途径每个隐藏层,直至得到输出并计算误差,这也是 TensorFlow 框架名字意义所在

前向传播的最后一步就是完成误差的计算
L = g ( f θ ( x ) , y ) \mathcal{L}=g\left(f_{\theta}(\boldsymbol{x}), \boldsymbol{y}\right) L=g(fθ(x),y)
其中 f θ ( ⋅ ) f_{\theta}(\cdot) fθ()代表了利用𝜃参数化的神经网络模型,𝑔(∙)称之为误差函数,用来描述当前网络的预测值𝑓𝜃(𝒙)与真实标签𝒚之间的差距度量,比如常用的均方差误差函数,ℒ称为网络的误差(Error,或损失 Loss),一般为标量。我们希望通过在训练集 D t r a i n D^{train} Dtrain上面学习到一组参数𝜃使得误差的误差ℒ最小:
θ ∗ = argmin ⁡ θ ⏟ g ( f θ ( x ) , y ) , x ∈ D train \theta^{*}=\underbrace{\underset{\theta}{\operatorname{argmin}}} g\left(f_{\theta}(\boldsymbol{x}), \boldsymbol{y}\right), x \in \mathbb{D}^{\text {train}} θ= θargming(fθ(x),y),xDtrain
上述的 arg ⁡ min ⁡ ⏟ θ \underbrace{\arg \min }_{\theta} θ argmin优化问题一般采用误差反向传播算法求解网络参数𝜃的梯度信息,并利用梯度下降算法迭代更新:
θ ′ = θ − η ∗ ∇ θ L \theta^{\prime}=\theta-\eta * \nabla_{\theta} \mathcal{L} θ=θηθL

利用误差反向传播算法进行反向计算的过程也叫反向传播(Backward propagation)。

从另一个角度来理解神经网络,它完成的是特征的维度变换的功能,比如 4 层的MNIST 手写数字图片识别的全连接网络,它依次完成了784 → 256 → 128 → 64 → 10的特征降维过程。

原始的特征通常具有较高的维度,包含了很多底层特征及无用信息,通过神经网络的层层特征变换,将较高的维度降维到较低的维度,此时的特征一般包含了与任务强相关的高层特征信息,通过对这些特征进行简单的逻辑判定即可完成特定的任务,如图片的分类。

网络的参数量是衡量网络规模的重要指标。那么怎么计算全连接层的参数量呢?
考虑权值矩阵W,偏置 b,输入特征长度为 d i n d_{in} din,输出特征长度为 d o u t d_{out} dout的网络层,其参数量为 d i n d_{in} din d o u t d_{out} dout,再加上偏置 b 的参数,总参数量为 d i n d_{in} din d o u t d_{out} dout + d o u t d_{out} dout

对于多层的全连接神经网络,比如784 → 256 → 128 → 64 → 10,总参数量计算表达式:
256 ∗ 784 + 256 + 128 ∗ 256 + 128 + 64 ∗ 128 + 64 + 10 ∗ 64 + 10 = 242762约 242K 个参数量。

全连接层作为最基本的神经网络类型,对于后续的卷积神经网络循环神经网络等的研究具有十分重要的意义,通过对其他网络类型的学习,我们会发现它们或多或少地都源自全连接层网络的思想。由于 Geoffrey Hinton、Yoshua Bengio 和 Yann LeCun 三人长期坚持在神经网络的一线领域研究,为人工智能的发展做出了杰出贡献,2018 年计算机图灵奖颁给这 3 人(图 6.7,从左至右依次是 Yann LeCun、Geoffrey Hinton、Yoshua Bengio)。
在这里插入图片描述

6.4 激活函数

下面我们来介绍神经网络中的常见激活函数。

6.4.1 Sigmoid

Sigmoid 函数也叫 Logistic 函数,定义为
Sigmoid : = 1 1 + e − x \text {Sigmoid}:=\frac{1}{1+e^{-x}} Sigmoid:=1+ex1
它的一个优良特性就是能够把y ∈ 𝑅的输入“压缩”到y ∈ [0,1]区间,这个区间的数值在机器学习常用来表示以下意义:

概率分布 [0,1]区间的输出和概率的分布范围契合,可以通过 Sigmoid 函数将输出转译为概率输出

信号强度 一般可以将 0~1 理解为某种信号的强度,如像素的颜色强度,1 代表当前通道颜色最强,0 代表当前通道无颜色;抑或代表门控值(Gate)的强度,1 代表当前门控全部开放,0 代表门控关闭

Sigmoid 函数连续可导,如图 6.8 所示,相对于阶跃函数,可以直接利用梯度下降算法优化网络参数,应用的非常广泛。
在这里插入图片描述
在 TensorFlow 中,可以通过 tf.nn.sigmoid 实现 Sigmoid 函数:

tf.linspace(-6.,6.,10)
x # 构造-6~6 的输入向量

<tf.Tensor: id=5, shape=(10,), dtype=float32, numpy=
array([-6. , -4.6666665, -3.3333333, -2. , -0.6666665,
 0.666667 , 2. , 3.333334 , 4.666667 , 6. ]…
 
tf.nn.sigmoid(x) # 通过 Sigmoid 函数

<tf.Tensor: id=7, shape=(10,), dtype=float32, numpy=
array([0.00247264, 0.00931597, 0.03444517, 0.11920291, 0.33924365,
 0.6607564 , 0.8807971 , 0.96555483, 0.99068403, 0.9975274 ],
 dtype=float32)>

可以看到,向量的范围由[−6,6]映射到[0,1]的区间.

我们可以看下sigmoid的函数图像

def set_plt_ax():
    # get current axis 获得坐标轴对象
    ax = plt.gca()                                           

    ax.spines['right'].set_color('none') 
    # 将右边 上边的两条边颜色设置为空 其实就相当于抹掉这两条边
    ax.spines['top'].set_color('none')         

    ax.xaxis.set_ticks_position('bottom')   
    # 指定下边的边作为 x 轴,指定左边的边为 y 轴
    ax.yaxis.set_ticks_position('left') 

    # 指定 data  设置的bottom(也就是指定的x轴)绑定到y轴的0这个点上
    ax.spines['bottom'].set_position(('data', 0)) 
    ax.spines['left'].set_position(('data', 0))
set_plt_ax()
plt.plot(x, sigmoid_y, color='C4', label='Sigmoid')
plt.xlim(-6, 6)
plt.ylim(0, 1)
plt.legend(loc=2)
plt.show()

在这里插入图片描述

6.4.2 ReLU

在 ReLU(REctified Linear Unit,修正线性单元)激活函数提出之前,Sigmoid 函数通常是神经网络的激活函数首选。但是 Sigmoid 函数在输入值较大或较小时容易出现梯度值接近于 0 的现象,称为梯度弥散现象,网络参数长时间得不到更新,很难训练较深层次的网络模型。2012 年提出的 8 层AlexNet 采用了一种名叫 ReLU 的激活函数,使得网络层数达到了 8 层。ReLU 函数定义为

ReLU ⁡ ( x ) : = max ⁡ ( 0 , x ) \operatorname{ReLU}(x):=\max (0, x) ReLU(x):=max(0,x)
函数曲线如图 6.9 所示,可以看到,ReLU 对小于 0 的值全部抑制为 0;对于正数则直接输出,这种单边抑制特性来源于生物学。

在这里插入图片描述
在 2001 年,神经科学家 Dayan 和 Abott 模拟得出更加精确的脑神经元激活模型,如图 6.10 所示,它具有单侧抑制、相对宽松的兴奋边界等特性,ReLU 函数与之非常类似 (Glorot, Bordes, & Bengio, 2011)。
在这里插入图片描述
在 TensorFlow 中,可以通过 tf.nn.relu 实现 ReLU 函数:

tf.nn.relu(x) # 通过 ReLU 激活函数

<tf.Tensor: id=11, shape=(10,), dtype=float32, numpy=
array([0. , 0. , 0. , 0. , 0. , 0.666667,
 2. , 3.333334, 4.666667, 6. ], dtype=float32)>
set_plt_ax()

plt.plot(x, relu_y, color='C4', label='ReLU')
plt.xlim(-6, 6)
plt.ylim(0, 6)
plt.legend(loc=2)
plt.show()

在这里插入图片描述

可以看到,经过 ReLU 激活函数后,负数全部抑制为 0,正数得以保留。除了可以使用函数式接口 tf.nn.relu 实现 ReLU 函数外,还可以像 Dense 层一样将ReLU 函数作为一个网络层添加到网络中,对应的类为 layers.ReLU()类。一般来说,激活函数类并不是主要的网络运算层,不计入网络的层数。

ReLU 函数的设计源自神经科学,计算十分简单,同时有着优良的梯度特性,在大量的深度学习应用中被验证非常有效,是应用最广泛的激活函数之一。

6.4.3 LeakyReLU

ReLU 函数在𝑥 < 0时梯度值恒为 0,也可能会造成梯度弥散现象,为了克服这个问题,·LeakyReLU· 函数被提出,如图 6.11 所示,LeakyReLU 表达式为:
 LeakyReLU  = { x x ≥ 0 p ∗ x x < 0 \text { LeakyReLU }=\left\{\begin{array}{cc} x & x \geq 0 \\ p * x & x<0 \end{array}\right.  LeakyReLU ={xpxx0x<0
其中𝑝为用户自行设置的某较小数值的超参数,如 0.02 等。当𝑝 = 0时,LeayReLU 函数退化为 ReLU 函数;当𝑝 ≠ 0时,𝑥 < 0能够获得较小的梯度值𝑝,从而避免出现梯度弥散现象。
在这里插入图片描述
在 TensorFlow 中,可以通过 tf.nn.leaky_relu 实现 LeakyReLU 函数:

tf.nn.leaky_relu(x, alpha=0.1) # 通过 LeakyReLU 激活函数

<tf.Tensor: id=13, shape=(10,), dtype=float32, numpy=
array([-0.6 , -0.46666667, -0.33333334, -0.2 , -0.06666666,
 0.666667 , 2. , 3.333334 , 4.666667 , 6. ],
 dtype=float32)>
set_plt_ax()

plt.plot(x, leakyrelu_y, color='C4', label='LeakyReLU')
plt.xlim(-6, 6)
plt.ylim(-1, 6)
plt.legend(loc=2)
plt.show()

在这里插入图片描述

其中 alpha 参数即𝑝参数。tf.nn.leaky_relu 对应的类为 layers.LeakyReLU,可以通LeakyReLU(alpha)创建 LeakyReLU 网络层,并设置𝑝参数,像 Dense 层一样将 LeakyReLU层放置在网络的合适位置。

6.4.4 Tanh

Tanh 函数能够将y ∈ 𝑅的输入“压缩”到[−1,1]区间,定义为:
tanh ⁡ ( x ) = ( e x − e − x ) ( e x + e − x ) = 2 ∗ sig ⁡ moid ⁡ ( 2 x ) − 1 \begin{aligned} &\tanh (x)=\frac{\left(e^{x}-e^{-x}\right)}{\left(e^{x}+e^{-x}\right)}\\ &=2 * \operatorname{sig} \operatorname{moid}(2 x)-1 \end{aligned} tanh(x)=(ex+ex)(exex)=2sigmoid(2x)1
可以看到 tanh 激活函数可通过 Sigmoid 函数缩放平移后实现,函数曲线如图 6.12 所示。
在这里插入图片描述
在 TensorFlow 中,可以通过 tf.nn.tanh 实现 tanh 函数:

tf.nn.tanh(x) # 通过 tanh 激活函数

<tf.Tensor: id=15, shape=(10,), dtype=float32, numpy=
array([-0.9999877 , -0.99982315, -0.997458 , -0.9640276 , -0.58278286,
 0.5827831 , 0.9640276 , 0.997458 , 0.99982315, 0.9999877 ],
 dtype=float32)>
set_plt_ax()

plt.plot(x, tanh_y, color='C4', label='Tanh')
plt.xlim(-6, 6)
plt.ylim(-1.5, 1.5)
plt.legend(loc=2)
plt.show()

在这里插入图片描述

可以看到向量的范围被映射到[−1,1]之间。

6.5 输出层设计

我们来特别地讨论网络的最后一层的设计,它除了和所有的隐藏层一样,完成维度变换、特征提取的功能,还作为输出层使用,需要根据具体的任务场景来决定是否使用激活函数,以及使用什么类型的激活函数

我们将根据输出值的区间范围来分类讨论。常见的几种输出类型包括:

❑ 𝒐 ∈ R d R^{d} Rd输出属于整个实数空间,或者某段普通的实数空间,比如函数值趋势的预测,年龄的预测问题等

❑ 𝒐 ∈ [0,1] 输出值特别地落在[0, 1]的区间,如图片生成,图片像素值一般用[0, 1]表示;或者二分类问题的概率,如硬币正反面的概率预测问题

❑ 𝒐 ∈ [0,  1], ∑ i o i = 1 \sum_{i} o_{i}=1 ioi=1 输出值落在[0, 1]的区间,并且所有输出值之和为 1,常见的如多分类问题,如 MNIST 手写数字图片识别,图片属于 10 个类别的概率之和为 1

❑ 𝒐 ∈ [−1,  1] 输出值在[-1,1]之间。

6.5.1 普通实数空间

这一类问题比较普遍,像正弦函数曲线预测、年龄的预测、股票走势的预测等都属于整个或者部分连续的实数空间,输出层可以不加激活函数。误差的计算直接基于最后一层的输出𝒐和真实值 y 进行计算,如采用均方差误差函数度量输出值𝒐与真实值𝒚之间的距离:
L = g ( o , y ) \mathcal{L}=g(\boldsymbol{o}, \boldsymbol{y}) L=g(o,y)
其中𝑔代表了某个具体的误差计算函数。

6.5.2 [0, 1]区间

输出值属于[0,1]区间也比较常见,比如图片的生成,二分类问题等。在机器学习中,一般会将图片的像素值归一化到[0,1]区间,如果直接使用输出层的值,像素的值范围会分布在整个实数空间。为了让像素的值范围映射到[0,1]的有效实数空间,需要在输出层后添加某个合适的激活函数𝜎,其中 Sigmoid 函数刚好具有此功能。

同样的,对于二分类问题,如硬币的正反面的预测,输出层可以只需要一个节点,表示某个事件 A 发生的概率P(𝐴|𝑥)。如果我们把网络的输出o表示正面事件出现的概率,那么反面事件出现的概率即为1 − 𝑜,网络结构如图 6.13 所示。

P(正面|𝑥) = 𝑜
P(反面|𝑥) = 1 − 𝑜
在这里插入图片描述

只需要在输出层的净活性值 z z z 后添加 Sigmoid 函数即可将输出转译为概率值。

对于二分类问题,除了可以使用单个输出节点表示事件 A 发生的概率P(𝐴|𝑥)外,还可以分别预测P(𝐴|𝑥)和P(𝑛𝑜𝑡 𝐴|𝑥),并满足约束
P ( A ∣ x ) + P ( not  A ∣ x ) = 1 \mathrm{P}(A | x)+\mathrm{P}(\text {not } A | x)=1 P(Ax)+P(not Ax)=1
如图 6.13 所示,二分类网络的输出层设置为 2 个节点,第一个节点的输出值表示为事件 A发生的概率P(𝐴|𝑥),第二个节点的输出值表示非 A 即 B 事件发生的概率P(𝑛𝑜𝑡 𝐴|𝑥),考虑到 Sigmoid 函数只能将单个值压缩到[0,1]区间,并不会考虑 2 个节点值之间的关系。我们希望除了满足 o i o_{i} oi ∈ [0,1]之外,还希望他们能满足概率之和为 1 的约束:

∑ i o i = 1 \sum_{i} o_{i}=1 ioi=1
这种情况就是下一节要介绍的问题设定。
在这里插入图片描述

6.5.3 [0,1]区间,和为 1

输出值 o i o_{i} oi∈ [0,1],所有输出值之和为 1,这种设定以多分类问题最为常见。如图 6.15所示,输出层的每个输出节点代表了一种类别,图中网络结构用于处理 3 分类任务,3 个节点的输出值分布代表了前样本属于类别 A,类别 B,类别 C 的概率P(A|x), P(B|x), P(C|x),考虑多分类问题中的样本只可能属于所有类别中的某一种,因此满足所有类别概率之和为 1 的约束。
在这里插入图片描述

如何实现此约束逻辑呢?可以通过在输出层添加 Softmax 函数实现。Softmax 函数定义为
σ ( z i ) = e z i ∑ j = 1 d out  e z j \sigma\left(z_{i}\right)=\frac{e^{z_{i}}}{\sum_{j=1}^{d_{\text {out }}} e^{z_{j}}} σ(zi)=j=1dout ezjezi
Softmax 函数不仅可以将输出值映射到[0,1]区间,还满足所有的输出值之和为 1 的特性。如图 6.15 中的例子,输出层的输出为[2. ,1. ,0.1],经过 Softmax 函数计算后,得到输出为[0.7,0.2,0.1],可以看到每个值代表了当前样本属于每个类别的概率,概率值之和为 1。通过 Softmax 函数可以将输出层的输出转译为类别概率,在分类问题中使用的非常频繁。
在这里插入图片描述

在 TensorFlow 中,可以通过 tf.nn.softmax 实现 Softmax 函数:

z = tf.constant([2.,1.,0.1])
tf.nn.softmax(z) # 通过 Softmax 函数

<tf.Tensor: id=19, shape=(3,), dtype=float32, numpy=array([0.6590012,
0.242433 , 0.0985659], dtype=float32)>

与 Dense 层类似,Softmax 函数也可以作为网络层类使用,通过类 layers.Softmax(axis=-1)可以方便添加 Softmax 层,其中 axis 参数指定需要进行计算的维度。

在 Softmax 函数的数值计算过程中,容易因输入值偏大发生数值溢出现象;在计算交叉熵时,也会出现数值溢出的问题。为了数值计算的稳定性,TensorFlow 中提供了一个统一的接口,将 Softmax 与交叉熵损失函数同时实现,同时也处理了数值不稳定的异常,一般推荐使用,避免单独使用 Softmax 函数与交叉熵损失函数。

函数式接口为tf.keras.losses.categorical_crossentropy(y_true, y_pred, from_logits=False),其中 y_true 代表了one-hot 编码后的真实标签,y_pred 表示网络的预测值,当 from_logits 设置为 True 时,y_pred 表示须为未经过 Softmax 函数的变量 z;当 from_logits 设置为 False 时,y_pred 表示为经过 Softmax 函数的输出。

z = tf.random.normal([2,10]) # 构造输出层的输出
y_onehot = tf.constant([1,3]) # 构造真实值 1和3
y_onehot = tf.one_hot(y_onehot, depth=10) # one-hot 编码
# 输出层未使用 Softmax 函数,故 from_logits 设置为 True
loss = keras.losses.categorical_crossentropy(y_onehot,z,from_logits=True)
loss = tf.reduce_mean(loss) # 计算平均交叉熵损失
loss

<tf.Tensor: id=210, shape=(), dtype=float32, numpy= 2.4201946>

也可以利用 losses.CategoricalCrossentropy(from_logits)类方式同时实现 Softmax 与交叉熵损失函数的计算:

# 创建 Softmax 与交叉熵计算类,输出层的输出 z 未使用 softmax
criteon = keras.losses.CategoricalCrossentropy(from_logits=True)
loss = criteon(y_onehot,z) # 计算损失
loss

<tf.Tensor: id=258, shape=(), dtype=float32, numpy= 2.4201946

6.5.4 [-1, 1]

如果希望输出值的范围分布在[−1, 1],可以简单地使用 tanh 激活函数,实现如下

x = tf.linspace(-6.,6.,10)
tf.tanh(x) # tanh 激活函数

<tf.Tensor: id=264, shape=(10,), dtype=float32, numpy=
array([-0.9999877 , -0.99982315, -0.997458 , -0.9640276 , -0.58278286,
 0.5827831 , 0.9640276 , 0.997458 , 0.99982315, 0.9999877 ],
 dtype=float32)>

输出层的设计具有一定的灵活性,可以根据实际的应用场景自行设计。

6.6 误差计算

常见的误差计算函数有均方误差、交叉熵、KL 散度、Hinge Loss 函数等,其中均方误差函数和交叉熵函数在深度学习中比较常见,均方误差主要用于回归问题,交叉熵主要用于分类问题

6.6.1 均方误差

均方误差(Mean Squared Error, MSE)函数把输出向量和真实向量映射到笛卡尔坐标系的两个点上,通过计算这两个点之间的欧式距离(准确地说是欧式距离的平方)来衡量两个向量之间的差距:
M S E : = 1 d out ∑ i = 1 d out ( y i − o i ) 2 \mathrm{MSE}:=\frac{1}{d_{\text {out}}} \sum_{i=1}^{d_{\text {out}}}\left(y_{i}-o_{i}\right)^{2} MSE:=dout1i=1dout(yioi)2
MSE 误差函数的值总是大于等于 0,当 MSE 函数达到最小值 0 时,输出等于真实标签,此时神经网络的参数达到最优状态。

均方差广泛应用在回归问题中,在分类问题中也可以应用均方差误差。在 TensorFlow中,可以通过函数方式或层方式实现 MSE 误差计算:

o = tf.random.normal([2,10]) # 构造网络输出
y_onehot = tf.constant([1,3]) # 构造真实值
y_onehot = tf.one_hot(y_onehot, depth=10)
loss = keras.losses.MSE(y_onehot, o) # 计算均方差
loss

<tf.Tensor: id=27, shape=(2,), dtype=float32, numpy=array([0.779179 ,
1.6585705], dtype=float32)>

TensorFlow MSE 函数返回的是每个样本的均方误差,需要在样本数量上再次平均来获得batch 的均方差:

loss = tf.reduce_mean(loss) # 计算 batch 均方差
loss

<tf.Tensor: id=30, shape=(), dtype=float32, numpy=1.2188747>

也可以通过层方式实现,对应的类为 keras.losses.MeanSquaredError()

# 创建 MSE 类
criteon = keras.losses.MeanSquaredError()
loss = criteon(y_onehot,o) # 计算 batch 均方差
loss

<tf.Tensor: id=54, shape=(), dtype=float32, numpy=1.2188747>

6.6.2 交叉熵

在介绍交叉熵损失函数之前,我们首先来介绍信息学中(Entropy)的概念。1948 年,Claude Shannon 将热力学中的熵的概念引入到信息论中,用来衡量信息的不确定度。熵在信息学科中也叫信息熵,或者香农熵。熵越大,代表不确定性越大,信息量也就越大。某个分布 (𝑖)的熵定义为
H ( P ) : = − ∑ i P ( i ) log ⁡ 2 P ( i ) H(P):=-\sum_{i} P(i) \log _{2} P(i) H(P):=iP(i)log2P(i)
实际上,𝐻( P)也可以使用其他底数的𝑙𝑜𝑔函数计算。

举个例子,对于 4 分类问题,如果某个样本的真实标签是第 4 类,one-hot 编码为[0,0,0,1],即这张图片的分类是唯一确定的,它属于第 4 类的概率 (𝑦=4|𝑥) = 1,不确定性为 0,它的熵可以简单的计算为
− 0 ∗ log ⁡ 2 0 − 0 ∗ log ⁡ 2 0 − 0 ∗ log ⁡ 2 0 − 1 ∗ log ⁡ 2 1 = 0 -0 * \log _{2} 0-0 * \log _{2} 0-0 * \log _{2} 0-1 * \log _{2} 1=0 0log200log200log201log21=0
也就是,对于确定的分布,熵为 0,即不确定性最低。如果它预测的概率分布是[0.1,0.1,0.1,0.7],它的熵可以计算为
− 0.1 ∗ log ⁡ 2 0.1 − 0.1 ∗ log ⁡ 2 0.1 − 0.1 ∗ log ⁡ 2 0.1 − 0.7 ∗ log ⁡ 2 0.7 ≈ 1.356 -0.1 * \log _{2} 0.1-0.1 * \log _{2} 0.1-0.1 * \log _{2} 0.1-0.7 * \log _{2} 0.7 \approx 1.356 0.1log20.10.1log20.10.1log20.10.7log20.71.356
考虑随机分类器,它每个类别的预测概率是均等的:[0.25,0.25,0.25,0.25],这种情况下的熵约为 2。

由于P(𝑖) ∈ [0,1], log2 (𝑖) ≤ 0,因此熵总是大于等于 0。当熵取得最小值 0 时,不确定性为 0。分类问题的 One-hot 编码的分布就是熵为 0 的例子。在 TensorFlow 中间,我们可以利用 tf.math.log 来组合计算熵。

在介绍完熵的概念后,我们基于熵引出交叉熵(Cross Entropy)的定义:

H ( p , q ) : = − ∑ i = 0 p ( i ) log ⁡ 2 q ( i ) H(p, q):=-\sum_{i=0} p(i) \log _{2} q(i) H(p,q):=i=0p(i)log2q(i)
通过变换,交叉熵可以分解为𝑝的熵𝐻(𝑝)与 p,q 的 KL 散度(Kullback-Leibler Divergence)的和:
H ( p , q ) = H ( p ) + D K L ( p ∣ q ) H(p, q)=H(p)+D_{K L}(p | q) H(p,q)=H(p)+DKL(pq)

其中 KL 定义为
D K L ( p ∣ q ) = ∑ x ∈ X p ( x ) log ⁡ ( p ( x ) q ( x ) ) D_{K L}(p | q)=\sum_{x \in X} p(x) \log \left(\frac{p(x)}{q(x)}\right) DKL(pq)=xXp(x)log(q(x)p(x))
KL 散度是 Solomon Kullback 和 Richard A. Leibler 在 1951 年提出的用于衡量 2 个分布之间距离的指标,𝑝 = 𝑞时, D K L D_{KL} DKL(𝑝|𝑞)取得最小值 0。需要注意的是,交叉熵和 KL 散度都不是对称的
H ( p , q ) ≠ H ( q , p ) D K L ( p ∣ q ) ≠ D K L ( q ∣ p ) \begin{aligned} H(p, q) & \neq H(q, p) \\ D_{K L}(p | q) & \neq D_{K L}(q | p) \end{aligned} H(p,q)DKL(pq)=H(q,p)=DKL(qp)
交叉熵可以很好地衡量 2 个分布之间的差别,特别地,当分类问题中 y 的编码分布𝑝采用one-hot 编码时:𝐻(𝒚) = 0,此时
H ( y , o ) = H ( y ) + D K L ( y ∣ o ) = D K L ( y ∣ o ) H(\boldsymbol{y}, \boldsymbol{o})=H(\boldsymbol{y})+D_{K L}(\boldsymbol{y} | \boldsymbol{o})=D_{K L}(\boldsymbol{y} | \boldsymbol{o}) H(y,o)=H(y)+DKL(yo)=DKL(yo)
退化到真实标签分布 y 与输出概率分布 o 之间的 KL 散度上。

根据 KL 散度的定义,我们推导分类问题中交叉熵的计算表达式:
H ( y , o ) = D K L ( y ∣ o ) = ∑ j y j log ⁡ ( y j o j ) H(\boldsymbol{y}, \boldsymbol{o})=D_{K L}(\boldsymbol{y} | \boldsymbol{o})=\sum_{j} y_{j} \log \left(\frac{y_{j}}{o_{j}}\right) H(y,o)=DKL(yo)=jyjlog(ojyj)

= 1 ∗ log ⁡ 1 o i + ∑ j ≠ i 0 ∗ log ⁡ ( 0 o j ) = − log ⁡ o i \begin{array}{c} =1 * \log \frac{1}{o_{i}}+\sum_{j \neq i} 0 * \log \left(\frac{0}{o_{j}}\right) \\ =-\log o_{i} \end{array} =1logoi1+j=i0log(oj0)=logoi
其中𝑖为 One-hot 编码中为 1 的索引号,也是当前输入的真实类别。可以看到,ℒ只与真实类别𝑖上的概率𝑜𝑖有关,对应概率𝑜𝑖越大,𝐻(𝒚, 𝒐)越小,当对应概率为 1 时,交叉熵𝐻(𝒚, 𝒐)取得最小值 0,此时网络输出𝒐与真实标签𝒚完全一致,神经网络取得最优状态。最小化交叉熵的过程也是最大化正确类别的预测概率的过程。从这个角度去理解交叉熵损失函数,非常直观易懂。

6.7 神经网络类型

全连接层是神经网络最基本的网络类型,对后续神经网络类型的研究有巨大的贡献,全连接层前向计算简单,梯度求导也较简单,但是它有一个最大的缺陷,在处理较大特征长度的数据时,全连接层的参数量往往较大,使得训练深层数的全连接网络比较困难

近年来,社交媒体的发达产生了海量的图片、视频、文本等数字资源,极大地促进了神经网络在计算机视觉、自然语言处理等领域中的研究,相继提出了一系列的神经网络变种类型。

6.7.1 卷积神经网络

如何识别、分析并理解图片、视频等数据是计算机视觉的一个核心问题,全连接层在处理高维度的图片、视频数据时往往网络参数量巨大,训练非常困难。通过利用局部相关性和权值共享的思想,Yann Lecun 在 1986 年提出了卷积神经网络(Convolutional NeuralNetwork,CNN)。

随着深度学习的兴盛,卷积神经网络在计算机视觉中的表现几乎碾压了所有其他的模型算法,呈现统治计算机视觉领域之势。

这其中比较流行的模型有用于图片分类的 AlexNet,VGG,GoogLeNet,ResNet,DenseNet 等,用于目标识别的 RCNN,FastRCNN,Faster RCNN,Mask RCNN 等。

我们将在第 10 章详细介绍卷积神经网络。

6.7.2 循环神经网络

除了具有空间结构的图片、视频等数据之外,序列信号也是非常常见的一种数据类型其中一个最具代表性的序列信号就是文本数据。如何处理并理解文本数据是自然语言处理的一个核心问题。

卷积神经网络由于缺乏 Memory 机制和处理不定长序列信号的能力,并不擅长自然语言处理任务。循环神经网络(Recurrent Neural Network,RNN)在Yoshua Bengio, Jürgen Schmidhuber 等人的持续研究下,被证明非常擅长处理序列信号。

1997 年,Jürgen Schmidhuber 提出了 LSTM 网络,作为 RNN 的变种,它较好地克服了RNN 缺乏长期记忆、不擅长处理长序列的问题,在自然语言处理中得到了广泛的应用。基于 LSTM 模型,Google 提出了用于机器翻译的 Seq2Seq 模型,并成功商用于谷歌神经机器翻译系统(GNMT)。其他的 RNN 变种还有 GRU,双向 RNN 等。

我们将在第 11 章详细介绍循环神经网络。

6.7.3 注意力(机制)网络

RNN 并不是自然语言处理的最终解决方案,近年来随着注意力机制(Attention)的提出,克服了 RNN 训练不稳定、难以并行化等缺陷,在自然语言处理和图片生成等领域中逐渐崭露头角。

注意力机制最初在图片分类任务上提出,但逐渐开始侵蚀 NLP 各大任务。2017 年,Google 提出了第一个利用纯注意力机制实现的网络模型 Transformer,随后基于Transformer 相继提出了一系列的用于机器翻译的注意力网络模型,如 GPT,BERT,GPT-2等。在其他领域,基于注意力机制,尤其是自注意力(Self-attention)机制构建的网络也取得了不错的效果,比如基于自注意力机制的 BigGAN 模型等。

我们将在第 14 章介绍注意力机制及其网络模型。

6.7.4 图神经网络

图片、文本等数据具有规则的空间、时间结构,称之为 Euclidean Data。卷积神经网络和循环神经网络被证明非常擅长处理这种类型的数据。而像类似于社交网络、通信网络,蛋白质分子结构等一系列不规则的空间拓扑结构的数据,它们显得力不从心。

2016 年,Thomas Kipf 等人基于前人在一阶近似的谱卷积算法上提出了图卷积网(GraphConvolution Network,GCN)模型。GCN 算法实现简单,从空间一阶邻居信息聚合的角度也能直观理解,在半监督任务上取得了不错效果。随后,一系列的网络模型相继被提出,如 GAT,EdgeConv,DeepGCN 等。

6.8 油耗预测实战

本节我们将利用全连接网络完成汽车的效能指标 MPG 的回归问题预测。

6.8.1 数据集

我们采用 Auto MPG 数据集,它记录了各种汽车效能指标与气缸数、重量、马力等其他因子的真实数据,查看数据集的前 5 项,如表格 6.1 所示,其中每个字段的含义列在表格 6.2 中。除了产地的数字字段表示类别外,其他字段都是数值型。对于产地地段,1 表示美国,2 表示欧洲,3 表示日本。
在这里插入图片描述
在这里插入图片描述

Auto MPG 数据集一共记录了 398 项数据,我们从 UCI 服务器下载并读取数据集到DataFrame 对象中:

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, losses
# 在线下载汽车效能数据集
dataset_path = keras.utils.get_file("auto-mpg.data",
"http://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data")
# 利用 pandas 读取数据集,字段有效能(公里数每加仑),气缸数,排量,马力,重量
# 加速度,型号年份,产地
column_names = ['MPG','Cylinders','Displacement','Horsepower','Weight',
 'Acceleration', 'Model Year', 'Origin']
raw_dataset = pd.read_csv(dataset_path, names=column_names,na_values = "?", 
comment='\t',sep=" ", skipinitialspace=True)
dataset = raw_dataset.copy()
# 查看部分数据
dataset.head()

原始数据中的数据可能含有空字段(缺失值)的数据项,需要清除这些记录项:

dataset.isna().sum() # 统计空白数据
dataset = dataset.dropna() # 删除空白数据项
dataset.isna().sum() # 再次统计空白数据

清除后,数据集记录项减为 392 项。

由于 Origin 字段为类别数据,我们将其移动出来,并转换为新的 3 个字段:USA,Europe 和 Japan,分别代表是否来自此产地:

# 处理类别型数据,其中 origin 列代表了类别 1,2,3,分布代表产地:美国、欧洲、日本
# 先弹出(删除并返回)origin 这一列
origin = dataset.pop('Origin')
# 根据 origin 列来写入新的 3 个列
dataset['USA'] = (origin == 1)*1.0
dataset['Europe'] = (origin == 2)*1.0
dataset['Japan'] = (origin == 3)*1.0
dataset.tail()

按着 8:2 的比例切分训练集和测试集:

# 切分为训练集和测试集
train_dataset = dataset.sample(frac=0.8,random_state=0)
test_dataset = dataset.drop(train_dataset.index)

将 MPG 字段移出为标签数据:

# 移动 MPG 油耗效能这一列为真实标签 Y
train_labels = train_dataset.pop('MPG')
test_labels = test_dataset.pop('MPG')

统计训练集的各个字段数值的均值和标准差,并完成数据的标准化:

# 查看训练集的输入 X 的统计数据
train_stats = train_dataset.describe()
train_stats.pop("MPG")
train_stats = train_stats.transpose()
# 标准化数据
def norm(x):
    return (x - train_stats['mean']) / train_stats['std']

normed_train_data = norm(train_dataset)
normed_test_data = norm(test_dataset)

打印出训练集和测试集的大小:

print(normed_train_data.shape,train_labels.shape)
print(normed_test_data.shape, test_labels.shape)

(314, 9) (314,) # 训练集共 314 行,输入特征长度为 9,标签用一个标量表示
(78, 9) (78,) # 测试集共 78 行,输入特征长度为 9,标签用一个标量表示

利用切分的训练集数据构建数据集对象:

train_db = tf.data.Dataset.from_tensor_slices((normed_train_data.values,
train_labels.values)) # 构建 Dataset 对象
train_db = train_db.shuffle(100).batch(32) # 随机打散,以32为1个batch

我们可以通过简单地统计数据集中各字段之间的两两分布来观察各个字段对 MPG 的影响,如图 6.17 所示,可以大致观察到,其中汽车排量、重量与 MPG 的关系比较简单,随着排量或张量的增大,汽车的 MPG 降低,能耗增加,这也是符合我们的生活经验的。
在这里插入图片描述

6.8.2 创建网络

考虑到 Auto MPG 数据集规模较小,我们只创建一个 3 层的全连接层网络来完成 MPG值的预测任务。

输入 X X X 的特征共有 9 种,因此第一层的输入节点数为 9。第一层、第二层的输出节点数设计为64,64,由于只有一种预测值,输出层输出节点设计为 1。

考虑MPG ∈R+,因此最后一次的激活函数可以不加,也可以添加 ReLU 激活函数。

我们将网络实现为一个自定义网络类,只需要在初始化函数中创建各个子网络层,并在前向计算函数 call 中实现自定义网络类的计算逻辑即可。

自定义网络类继承自keras.Model基类,这也是自定义网络类的标准写法,以方便地利用 keras.Model 基类提供的 trainable_variables 等各种便捷功能:

class Network(keras.Model):
    #回归网络
    def __init__(self):
        super(Network,self).__init__()
        #创建3个全连接层
        self.fc1=layers.Dense(64,activation='relu')
        self.fc2=layers.Dense(64,activation='relu')
        self.fc3=layers.Dense(1)
    
    def call(self,inputs,training=None,mask=None):
        # 依次通过3个全连接层
        x=self.fc1(inputs)
        x=self.fc2(x)
        x=self.fc3(x)
        return x   

6.8.3 训练与测试

在完成主网络模型类的创建后,我们来实例化网络对象和创建优化器:

model = Network() # 创建网络类实例
# 通过 build 函数完成内部张量的创建,其中 4 为任意的 batch 数量,9 为输入特征长度
model.build(input_shape=(4, 9))
model.summary() # 打印网络信息
optimizer = tf.keras.optimizers.RMSprop(0.001) # 创建优化器,指定学习率

接下来实现网络训练部分。通过 Epoch 和 Step 的双层循环训练网络,共训练 200 个epoch:

我们一般把完成一个 batch 的数据训练,叫做一个 step通过多个 step 来完成整个训练集的一次迭代,叫做一个 epoch。在实际训练时,通常需要对数据集迭代多个 epoch 才能取得较好地训练效果:

for epoch in range(200):#200个epoch
    for step,(x,y) in enumerate(train_db):#遍历依次训练集
        # 梯度记录器
        with tf.GradientTape() as tape:
            out=model(x)#通过网络获得输出
            loss=tf.reduce_mean(keras.losses.MSE(y,out))#计算MSE
            mae_loss=tf.reduce_mean(keras.losses.MAE(y,out))#计算MAE

        if step%10==0:#打印训练误差
            # 计算梯度,并跟新
            print('step:{},epoch:{},loss:{}'.format(step,epoch,float(loss)))
        grads=tape.gradient(loss,model.trainable_variables)
        optimizer.apply_gradients(zip(grads,model.trainable_variables))

对于回归问题,除了 MSE 均方差可以用来测试模型的性能,还可以用平均绝对误差(Mean Absolute Error, MAE)来衡量模型的性能,它被定义为:
 mae  : = 1 d out  ∑ i ∣ y i − o i ∣ \text { mae }:=\frac{1}{d_{\text {out }}} \sum_{i}\left|y_{i}-o_{i}\right|  mae :=dout 1iyioi
我们记录每个 epoch 结束时的训练和测试 MAE 数据,并绘制变化曲线,如图 6.18 所示。
在这里插入图片描述

可以观察到,在训练到约第 25 个 epoch 时,MAE 的下降变得较缓慢,其中训练集的 MAE还在继续缓慢下降,但是测试集 MAE 几乎保持不变,因此可以在约第 25 个 epoch 时提前结束训练,并利用此时的网络参数来预测新的输入样本即可.

完整代码如下:

#!/usr/bin/env python
# encoding: utf-8

import os
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, losses

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'


def load_dataset():
    # 在线下载汽车效能数据集
    dataset_path = keras.utils.get_file("auto-mpg.data",
                                        "http://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data")

    # 效能(公里数每加仑),气缸数,排量,马力,重量
    # 加速度,型号年份,产地
    column_names = ['MPG', 'Cylinders', 'Displacement', 'Horsepower', 'Weight',
                    'Acceleration', 'Model Year', 'Origin']
    raw_dataset = pd.read_csv(dataset_path, names=column_names,
                              na_values="?", comment='\t',
                              sep=" ", skipinitialspace=True)

    dataset = raw_dataset.copy()
    return dataset


def preprocess_dataset(dataset):
    dataset = dataset.copy()
    # 统计空白数据,并清除
    dataset = dataset.dropna()

    # 处理类别型数据,其中origin列代表了类别1,2,3,分布代表产地:美国、欧洲、日本
    # 其弹出这一列
    origin = dataset.pop('Origin')
    # 根据origin列来写入新列
    dataset['USA'] = (origin == 1) * 1.0
    dataset['Europe'] = (origin == 2) * 1.0
    dataset['Japan'] = (origin == 3) * 1.0

    # 切分为训练集和测试集
    train_dataset = dataset.sample(frac=0.8, random_state=0)
    test_dataset = dataset.drop(train_dataset.index)

    return train_dataset, test_dataset


def get_train_stats(train_dataset):
    # 查看训练集的输入X的统计数据
    train_stats = train_dataset.describe()
    train_stats.pop("MPG")
    train_stats = train_stats.transpose()
    return train_stats


def norm(x, train_stats):
    """
    标准化数据
    :param x:
    :param train_stats: get_train_stats(train_dataset)
    :return:
    """
    return (x - train_stats['mean']) / train_stats['std']


class Network(keras.Model):
    # 回归网络
    def __init__(self):
        super(Network, self).__init__()
        # 创建3个全连接层
        self.fc1 = layers.Dense(64, activation='relu')
        self.fc2 = layers.Dense(64, activation='relu')
        self.fc3 = layers.Dense(1)

    def call(self, inputs):
        # 依次通过3个全连接层
        x = self.fc1(inputs)
        x = self.fc2(x)
        x = self.fc3(x)

        return x


def build_model():
    # 创建网络
    model = Network()
    model.build(input_shape=(4, 9))
    model.summary()
    return model


def train(model, train_db, optimizer, normed_test_data, test_labels):
    train_mae_losses = []
    test_mae_losses = []
    for epoch in range(200):
        for step, (x, y) in enumerate(train_db):

            with tf.GradientTape() as tape:
                out = model(x)
                loss = tf.reduce_mean(losses.MSE(y, out))
                mae_loss = tf.reduce_mean(losses.MAE(y, out))

            if step % 10 == 0:
                print(epoch, step, float(loss))

            grads = tape.gradient(loss, model.trainable_variables)
            optimizer.apply_gradients(zip(grads, model.trainable_variables))

        train_mae_losses.append(float(mae_loss))
        out = model(tf.constant(normed_test_data.values))
        test_mae_losses.append(tf.reduce_mean(losses.MAE(test_labels, out)))

    return train_mae_losses, test_mae_losses


def plot(train_mae_losses, test_mae_losses):
    plt.figure()
    plt.xlabel('Epoch')
    plt.ylabel('MAE')
    plt.plot(train_mae_losses, label='Train')
    plt.plot(test_mae_losses, label='Test')
    plt.legend()
    # plt.ylim([0,10])
    plt.legend()
    plt.savefig('MEA变化曲线.svg')


def main():
    dataset = load_dataset()
    train_dataset, test_dataset = preprocess_dataset(dataset)
    # 统计数据
    sns_plot = sns.pairplot(train_dataset[["Cylinders", "Displacement", "Weight", "MPG"]],
                            diag_kind="kde")
    sns_plot.savefig("特征之间的两两分布.svg")

    train_stats = get_train_stats(train_dataset)

    # 移动MPG油耗效能这一列为真实标签Y
    train_labels = train_dataset.pop('MPG')
    test_labels = test_dataset.pop('MPG')

    # 进行标准化
    normed_train_data = norm(train_dataset, train_stats)
    normed_test_data = norm(test_dataset, train_stats)

    model = build_model()
    optimizer = tf.keras.optimizers.RMSprop(0.001)
    train_db = tf.data.Dataset.from_tensor_slices((normed_train_data.values, train_labels.values))
    train_db = train_db.shuffle(100).batch(32)

    train_mae_losses, test_mae_losses = train(model, train_db, optimizer, normed_test_data, test_labels)
    plot(train_mae_losses, test_mae_losses)


if __name__ == '__main__':
    main()
  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

安替-AnTi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值