TensorFlow-深度学习入门指南-二-

162 阅读1小时+

TensorFlow 深度学习入门指南(二)

原文:Beginning Deep Learning with TensorFlow

协议:CC BY-NC-SA 4.0

五、高级 TensorFlow

人工智能将是谷歌的终极版本。理解网络上一切的终极搜索引擎。它会准确理解你想要什么,并给你正确的东西。

—拉里·佩奇

在介绍了基本的张量运算后,让我们进一步探讨高级运算,如张量合并和分割、范数统计、张量填充和裁剪。我们也将再次使用 MNIST 数据集来增强我们对 TensorFlow 中张量运算的理解。

5.1 合并和拆分

合并

归并就是将多个张量合并成某一维的一个张量。以某学校的成绩册数据为例,张量 A 用于保存 1-4 班的成绩册。每个班有 35 名学生,共有 8 门课程。张量 A 的形状为【4,35,8】。类似地,张量 B 保存其他六个类的成绩册,形状为【6,35,8】。将这两个成绩册合并,就可以得到学校所有班级的成绩册,记为张量 C ,对应的形状应该是【10,35,8】,其中 10 代表十个班级,35 代表 35 个学生,8 代表八个科目。

张量可以使用连接和堆栈操作来合并。串联操作不会生成新的维度。它仅沿现有维度合并。但是堆栈操作会创建新的维度。是否使用连接或堆叠操作来合并张量取决于是否需要为特定场景创建新的维度。我们将在下一节课中讨论这两个问题。

串联。在 TensorFlow 中,可以使用 tf.concat(tensors,axis)函数连接张量,其中第一个参数包含需要合并的张量列表,第二个参数指定要合并的维度索引。回到前面的例子,我们合并班级维度中的年级册。这里,类维的索引号是 0,即 axis = 0。合并 AB 的代码如下:

In [1]:
a = tf.random.normal([4,35,8]) # Create gradebook A
b = tf.random.normal([6,35,8]) # Create gradebook B
tf.concat([a,b],axis=0) # Merge gradebooks
Out[1]:
<tf.Tensor: id=13, shape=(10, 35, 8), dtype=float32, numpy=
array([[[ 1.95299834e-01,  6.87859178e-01, -5.80048323e-01, ...,
          1.29430830e+00,  2.56610274e-01, -1.27798581e+00],
        [ 4.29753691e-01,  9.11329567e-01, -4.47975427e-01, ...,

除了类维度,我们还可以合并其他维度的张量。考虑张量 A 用 shape [10,35,4]保存所有班级所有学生的前四科成绩,张量 B 用 shape [10,35,4]保存其余 4 科成绩。我们可以通过合并 AB 得到总的年级簿张量,如下所示:

In [2]:
a = tf.random.normal([10,35,4])
b = tf.random.normal([10,35,4])
tf.concat([a,b],axis=2) # Merge along the last dimension
Out[2]:
<tf.Tensor: id=28, shape=(10, 35, 8), dtype=float32, numpy=
array([[[-5.13509691e-01, -1.79707789e+00,  6.50747120e-01, ...,
          2.58447856e-01,  8.47878829e-02,  4.13468748e-01],
        [-1.17108583e+00,  1.93961406e+00,  1.27830813e-02, ...,

从语法上讲,concatenate 操作可以在任何维度上执行。唯一的限制是非合并维度的长度必须相同。例如,shape [4,32,8]和 shape [6,35,8]的张量不能在班级维度中直接合并,因为学生人数维度的长度不一样——一个是 32,一个是 35,例如:

In [3]:
a = tf.random.normal([4,32,8])
b = tf.random.normal([6,35,8])
tf.concat([a,b],axis=0) # Illegal merge. Second dimension is different.
Out[3]:
InvalidArgumentError: ConcatOp : Dimensions of inputs should match: shape[0] = [4,32,8] vs. shape[1] = [6,35,8] [Op:ConcatV2] name: concat

堆栈。concatenate 操作直接合并现有维度上的数据,并且不创建新维度。如果我们想在合并数据时创建一个新的维度,我们需要使用 tf.stack 操作。考虑张量 A 以【35,8】的形状保存一个班级的成绩册,张量 B 以【35,8】的形状保存另一个班级的成绩册。当合并这两个类的数据时,我们需要创建一个新的维度,定义为类维度。新尺寸可以放置在任何位置。一般把班级维度放在学生维度之前,也就是合并张量的新形状应该是[2,35,8]。

tf.stack(tensors,axis)函数可用于合并多个张量。第一个参数表示要合并的张量列表,第二个参数指定插入新维度的位置。axis 的用法与 tf.expand_dims 函数的用法相同。当 ≥ 0 时,在轴前插入一个新尺寸。当 <为 0 时,我们在轴后插入一个新的尺寸。图 5-1 显示了形状为 bchw 的张量对应不同轴参数设置的新维度位置。

img/515226_1_En_5_Fig1_HTML.png

图 5-1

具有不同轴值的堆栈操作的新尺寸插入位置

使用堆栈操作合并两个班级的成绩册,并在 axis = 0 位置插入班级维度。代码如下:

In [4]:
a = tf.random.normal([35,8])
b = tf.random.normal([35,8])
tf.stack([a,b],axis=0) # Stack a and b and insert new dimension at axis=0
Out[4]:
<tf.Tensor: id=55, shape=(2, 35, 8), dtype=float32, numpy=
array([[[ 3.68728966e-01, -8.54765773e-01, -4.77824420e-01,
         -3.83714020e-01, -1.73216307e+00,  2.03872994e-02,
          2.63810277e+00, -1.12998331e+00],...

我们也可以选择在其他地方插入新的维度。例如,在末尾插入类维度:

In [5]:
a = tf.random.normal([35,8])
b = tf.random.normal([35,8])
tf.stack([a,b],axis=-1) # Insert new dimension at the end
Out[5]:
<tf.Tensor: id=69, shape=(35, 8, 2), dtype=float32, numpy=
array([[[ 0.3456724 , -1.7037214 ],
        [ 0.41140947, -1.1554345 ],
        [ 1.8998919 ,  0.56994915],...

现在类维度在 axis = 2 上,我们需要根据最新维度顺序代表的视图来理解数据。如果我们选择使用 tf.concat 来合并前面的脚本,那么它将是

In [6]:
a = tf.random.normal([35,8])
b = tf.random.normal([35,8])
tf.concat([a,b],axis=0) # No class dimension
Out[6]:
<tf.Tensor: id=108, shape=(70, 8), dtype=float32, numpy=
array([[-0.5516891 , -1.5031327 , -0.35369992,  0.31304857,  0.13965549,
         0.6696881 , -0.50115544,  0.15550546],
       [ 0.8622069 ,  1.0188094 ,  0.18977325,  0.6353301 ,  0.05809061,...

可以看出 tf.concat 也可以平滑地合并数据,但是我们需要按照前 35 个学生来自第一节课,后 35 个学生来自第二节课的方式来理解张量数据,这不是很直观。对于这个例子,通过 tf.stack 方法创建一个新的维度显然更合理。

tf.stack 函数也需要满足一定的条件才能使用。它需要所有的张量合并成相同的形状。让我们来看看当堆叠两个不同形状的张量时会发生什么:

In [7]:
a = tf.random.normal([35,4])
b = tf.random.normal([35,8])
tf.stack([a,b],axis=-1) # Illegal use of stack function. Different shapes.
Out[7]:
InvalidArgumentError: Shapes of all inputs must match: values[0].shape = [35,4] != values[1].shape = [35,8] [Op:Pack] name: stack

前面的操作试图合并两个形状分别为[35,4]和[35,8]的张量。因为两个张量的形状不一样,合并操作无法完成。

拆分

合并操作的逆过程是拆分,即将一个张量拆分成多个张量。让我们继续学习成绩册的例子。我们得到全校形状为[10,35,8]的年级册张量。现在我们需要将数据在班级维度上切割成十个张量,每个张量保存对应班级的年级簿数据。tf.split(x,num_or_size_splits,axis)可以用来完成张量分裂运算。函数中参数的含义如下:

  • x:要分割的张量。

  • 数量 _ 或 _ 大小 _ 分割:切割方案。当 num_or_size_splits 为单值时,如 10,则表示张量 x 被切成等长的十份。当 num_or_size_splits 是一个列表时,列表中的每个元素代表每个部分的长度。比如 num_or_size_splits=[2,4,2,2]表示张量被切割成四个部分,每个部分的长度为 2,4,2,2。

  • 轴:指定分割的尺寸索引。

现在,我们将总成绩册张量分成十份,如下所示:

In [8]:
x = tf.random.normal([10,35,8])
# Cut into 10 pieces with equal length
result = tf.split(x, num_or_size_splits=10, axis=0)
len(result)  # Return a list with 10 tensors of equal length
Out[8]: 10

我们可以查看一个张量切割后的形状,应该是形状为[1,35,8]的一个类的所有年级书数据:

In [9]: result[0] # Check the first class gradebook
Out[9]: <tf.Tensor: id=136, shape=(1, 35, 8), dtype=float32, numpy=
array([[[-1.7786729 ,  0.2970506 ,  0.02983334,  1.3970423 ,
          1.315918  , -0.79110134, -0.8501629 , -1.5549672 ],
        [ 0.5398711 ,  0.21478991, -0.08685189,  0.7730989 ,...

可以看出,第一类张量的形状为[1,35,8],其中仍然具有类维数。让我们进行不等长切割。例如,将数据分成四部分,每个部分的长度为[4,2,2,2]:

In [10]: x = tf.random.normal([10,35,8])
# Split tensor into 4 parts
result = tf.split(x, num_or_size_splits=[4,2,2,2] ,axis=0)
len(result)
Out[10]: 4

检查第一个分裂张量的形状。根据我们的拆分方案,它应该包含四个班的成绩册。形状应该是[4,35,8]:

In [10]: result[0]
Out[10]: <tf.Tensor: id=155, shape=(4, 35, 8), dtype=float32, numpy=
array([[[-6.95693314e-01,  3.01393479e-01,  1.33964568e-01, ...,

特别是,如果我们想将某个维度除以长度 1,我们可以使用 tf.unstack(x,axis)函数。这个方法是 tf.split 的一个特例,拆分长度固定为 1。我们只需要指定拆分维度的索引号。例如,在班级维度中拆分总成绩簿张量:

In [11]: x = tf.random.normal([10,35,8])
result = tf.unstack(x,axis=0)
len(result) # Return a list with 10 tensors
Out[11]: 10

查看分割张量的形状:

In [12]: result[0] # The first class tensor
Out[12]: <tf.Tensor: id=166, shape=(35, 8), dtype=float32, numpy=
array([[-0.2034383 ,  1.1851563 ,  0.25327438, -0.10160723,  2.094969  ,
        -0.8571669 , -0.48985648,  0.55798006],...

可以看到,通过 tf.unstack 分裂后,分裂张量形状变成了[35,8],即类维消失,这与 tf.split 不同。

5.2 常见统计数据

在神经网络计算期间,需要计算各种统计属性,例如最大值、最小值、平均值和范数。由于张量通常包含大量的数据,通过获取这些张量的统计信息,更容易推断出张量值的分布。

5.2.1 规范

范数是向量“长度”的度量。可以推广到张量。在神经网络中,它通常用于表示张量权重和梯度大小。常用的规范有:

  • L1 norm, defined as the sum of the absolute values of all the elements of the vector:

    {\left\Vert x\right\Vert}_1={\sum}_i\left|{x}_i\right|

  • L2 norm, defined as the root sum of the squares of all the elements of the vector:

    {\left\Vert x\right\Vert}_2=\sqrt{\sum_i{\left|{x}_i\right|}²}

  • ∞ norm, defined as the maximum of the absolute values of all elements of a vector:

    {\left\Vert x\right\Vert}_{\infty }={\mathit{\max}}_i\left(\left|{x}_i\right|\right)

对于矩阵和张量,在将矩阵和张量展平成向量后,也可以使用前面的公式。在 TensorFlow 中,tf.norm(x,ord)函数可用于求解 L1、L2 和∞范数,其中 L1、L2 和∞范数的参数 ord 分别指定为 1、1 和 np.inf:

In [13]: x = tf.ones([2,2])
tf.norm(x,ord=1) # L1 norm
Out[13]: <tf.Tensor: id=183, shape=(), dtype=float32, numpy=4.0>
In [14]: tf.norm(x,ord=2) # L2 norm
Out[14]: <tf.Tensor: id=189, shape=(), dtype=float32, numpy=2.0>
In [15]: import numpy as np
tf.norm(x,ord=np.inf) # ∞ norm
Out[15]: <tf.Tensor: id=194, shape=(), dtype=float32, numpy=1.0>

5.2.2 最大值、最小值、平均值和总和

tf.reduce_max、tf.reduce_min、tf.reduce_mean 和 tf.reduce_sum 函数可用于获取某维或所有维中张量的最大值、最小值、平均值和总和。

考虑形状为[4,10]的张量,其中第一维表示样本的数量,第二维表示当前样本属于十个类别中的每一个的概率。每个样本概率的最大值可以通过 tf.reduce_max 函数获得:

In [16]: x = tf.random.normal([4,10])
tf.reduce_max(x,axis=1) # get maximum value at 2nd dimension
Out[16]:<tf.Tensor: id=203, shape=(4,), dtype=float32, numpy=array([1.2410722 , 0.88495886, 1.4170984 , 0.9550192 ], dtype=float32)>

前面的代码返回一个长度为 4 的向量,它表示每个样本的最大概率值。类似地,我们可以找到每个样本的概率最小值,如下所示:

In [17]: tf.reduce_min(x,axis=1) # get the minimum value at 2nd dimension
Out[17]:<tf.Tensor: id=206, shape=(4,), dtype=float32, numpy=array([-0.27862206, -2.4480672 , -1.9983795 , -1.5287997 ], dtype=float32)>

求每个样本的平均概率:

In [18]: tf.reduce_mean(x,axis=1)
Out[18]:<tf.Tensor: id=209, shape=(4,), dtype=float32, numpy=array([ 0.39526337, -0.17684573, -0.148988  , -0.43544054], dtype=float32)>

当未指定轴参数时,tf.reduce_*函数将查找所有数据的最大值、最小值、平均值和总和:

In [19]:x = tf.random.normal([4,10])
tf.reduce_max(x),tf.reduce_min(x),tf.reduce_mean(x)
Out [19]: (<tf.Tensor: id=218, shape=(), dtype=float32, numpy=1.8653786>,
 <tf.Tensor: id=220, shape=(), dtype=float32, numpy=-1.9751656>,
 <tf.Tensor: id=222, shape=(), dtype=float32, numpy=0.014772797>)

在求解误差函数时,可以通过 MSE 函数得到每个样本的误差,需要计算样本的平均误差。这里我们可以使用 tf.reduce_mean 函数如下:

In [20]:
out = tf.random.normal([4,10]) # Simulate output
y = tf.constant([1,2,2,0]) # Real labels
y = tf.one_hot(y,depth=10) # One-hot encoding
loss = keras.losses.mse(y,out) # Calculate loss of each sample
loss = tf.reduce_mean(loss) # Calculate mean loss
loss
Out[20]:
<tf.Tensor: id=241, shape=(), dtype=float32, numpy=1.1921183>

与 tf.reduce_mean 函数类似,sum 函数 tf.reduce_sum(x,axis)可以计算张量在相应轴上的所有特征的和:

In [21]:out = tf.random.normal([4,10])
tf.reduce_sum(out,axis=-1) # Calculate sum along the last dimension
Out[21]:<tf.Tensor: id=303, shape=(4,), dtype=float32, numpy=array([-0.588144 ,  2.2382064,  2.1582587,  4.962141 ], dtype=float32)>

另外,为了获得张量的最大值或最小值,我们有时也想获得相应的位置指数。例如,对于分类任务,我们需要知道最大概率的位置索引,这通常被用作预测类别。考虑具有十个类别的分类问题,我们得到形状为[2,10]的输出张量,其中 2 表示两个样本,10 表示属于十个类别的概率。由于元素的位置索引代表了当前样本属于该类别的概率,所以我们经常使用最大概率对应的索引作为预测类别。

In [22]:out = tf.random.normal([2,10])
out = tf.nn.softmax(out, axis=1) # Use softmax to convert to probability
out
Out[22]:<tf.Tensor: id=257, shape=(2, 10), dtype=float32, numpy=
array([[0.18773547, 0.1510464 , 0.09431915, 0.13652141, 0.06579739,
        0.02033597, 0.06067333, 0.0666793 , 0.14594753, 0.07094406],
       [0.5092072 , 0.03887136, 0.0390687 , 0.01911005, 0.03850609,
        0.03442522, 0.08060656, 0.10171875, 0.08244187, 0.05604421]],
       dtype=float32)>

以第一个样本为例,可以看出概率最高(0.1877)的指数为 0。因为每个指标上的概率代表样本属于该类别的概率,所以第一个样本属于 0 类的概率最大。因此,第一个样本最有可能属于类别 0。这是一个典型的应用,其中需要求解最大值的指数。

我们可以用 tf.argmax(x,axis)和 tf.argmin(x,axis)来求 x 在轴参数上的最大值和最小值的索引。例如:

In [23]:pred = tf.argmax(out, axis=1)
pred
Out[23]:<tf.Tensor: id=262, shape=(2,), dtype=int64, numpy=array([0, 0], dtype=int64)>

可以看出,两个样本的最大概率出现在索引 0 上,因此最有可能的是,它们都属于类别 0。我们可以使用类别 0 作为两个样本的预测类别。

5.3 张量比较

为了得到准确率等分类度量,一般需要将预测结果与真实标签进行比较。考虑 100 个样本的预测结果,可以通过 tf.argmax 得到预测的类别。

In [24]:out = tf.random.normal([100,10])
out = tf.nn.softmax(out, axis=1) # Convert to probability
pred = tf.argmax(out, axis=1) # Find corresponding category
Out[24]:<tf.Tensor: id=272, shape=(100,), dtype=int64, numpy=
array([0, 6, 4, 3, 6, 8, 6, 3, 7, 9, 5, 7, 3, 7, 1, 5, 6, 1, 2, 9, 0, 6,
       5, 4, 9, 5, 6, 4, 6, 0, 8, 4, 7, 3, 4, 7, 4, 1, 2, 4, 9, 4,...

pred 变量保存 100 个样本的预测类别。我们将它们与真实标签进行比较,以获得一个布尔张量,表示每个样本是否预测了正确的样本。tf.equal(a,b)(或 tf.math.equal(a,b),二者等价)函数可以比较两个张量是否相等,例如:

In [25]: # Simiulate the true labels
y = tf.random.uniform([100],dtype=tf.int64,maxval=10)
Out[25]:<tf.Tensor: id=281, shape=(100,), dtype=int64, numpy=
array([0, 9, 8, 4, 9, 7, 2, 7, 6, 7, 3, 4, 2, 6, 5, 0, 9, 4, 5, 8, 4, 2,
       5, 5, 5, 3, 8, 5, 2, 0, 3, 6, 0, 7, 1, 1, 7, 0, 6, 1, 2, 1, 3, ...
In [26]:out = tf.equal(pred,y) # Compare true and prediction
Out[26]:<tf.Tensor: id=288, shape=(100,), dtype=bool, numpy=
array([False, False, False, False, True, False, False, False, False,
       False, False, False, False, False, True, False, False, True,...

tf.equal 函数将比较结果作为布尔张量返回。我们只需要计算真实元素的数量,就可以得到正确的预测数量。为了实现这一点,我们先将布尔类型转换为整数张量,即 True 对应 1,False 对应 0,然后将 1 的个数求和,得到比较结果中 True 元素的个数:

In [27]:out = tf.cast(out, dtype=tf.float32) # convert to int type
correct = tf.reduce_sum(out) # get the number of True elements
Out[27]:<tf.Tensor: id=293, shape=(), dtype=float32, numpy=12.0>

可以看出,我们随机生成的预测数据中,正确预测的数量是 12 个,所以其准确率为:

accuracy=\frac{12}{100}=12\%

这是随机预测模型的正常水平。

除 tf.equal 函数外,其他常用的比较函数如表 5-1 所示。

表 5-1

常见比较函数

|

功能

|

比较逻辑

| | --- | --- | | tf.math.greater | > | | tf.math.less | < | | tf.math.greater_equal | a**b | | tf.math.less_equal | a≤【b】 | | tf.math.not_equal | | | tf.math.is_nan | = |

5.4 填写和复制

填充

图像的高度和宽度以及序列信号的长度可以不同。为了便于网络的并行计算,需要将不同长度的数据扩展到相同的长度。我们之前介绍过,可以通过复制来增加数据的长度。但是,重复复制数据会破坏原有的数据结构,不适合某些情况。常见的做法是在数据的开头或结尾填入足够数量的特定值。这些特定值(例如,0)通常表示无效的含义。这种操作称为填充。

考虑一个两句话的张量,每个单词用一个数字代码表示,比如 1 代表 I,2 代表 like,等等。第一句是“我喜欢今天的天气。”我们假设句号编码为[1,2,3,4,5,6]。第二句是“我也是”,编码为[7,8,1,6]。为了将这两个句子存储在一个张量中,我们需要保持这两个句子的长度一致,即需要将第二个句子的长度扩展为 6。常见的填充方案是在第二句话的末尾填充若干个零,即[7,8,1,6,0,0]。现在这两个句子可以堆叠起来,组合成一个形状为[2,6]的张量。

填充操作可以通过 tf.pad(x,paddings)函数实现。参数 paddings 是多个嵌套方案的列表,格式为[ 左填充右填充。例如, paddings = [[0,0],[2,1],[1,2]]表示第一维度不填充,第二维度左边(开头)填充两个单位,第二维度右边(结尾)填充一个单位,第三维度左边填充一个单位,第三维度右边填充两个单位。考虑到前面两个句子的例子,第二个句子的第一维右边需要填充两个单位,paddings 方案为[[0,2]]:

In [28]:a = tf.constant([1,2,3,4,5,6]) # 1st sentence
b = tf.constant([7,8,1,6]) # 2nd sentence
b = tf.pad(b, [[0,2]]) # Pad two 0's in the end of 2nd sentence
b
Out[28]:<tf.Tensor: id=3, shape=(6,), dtype=int32, numpy=array([7, 8, 1, 6, 0, 0])>

填充后,两个张量的形状是一致的,我们可以把它们叠加在一起。代码如下:

In [29]:tf.stack([a,b],axis=0) # Stack a and b
Out[29]:<tf.Tensor: id=5, shape=(2, 6), dtype=int32, numpy=
array([[1, 2, 3, 4, 5, 6],
       [7, 8, 1, 6, 0, 0]])>

在自然语言处理中,需要加载不同长度的句子。有的句子比较短,比如只有十个字,有的句子比较长,比如 100 多个字。为了能够保存在同一个张量中,一般选择一个能够覆盖大部分句子长度的阈值,比如 80 个单词。对于少于 80 个单词的句子,我们在句尾用 0 填充。对于超过 80 个单词的句子,我们通过删除结尾的一些单词将句子截短为 80 个单词。我们将以 IMDB 数据集为例,演示如何将长度不等的句子转换成长度相等的结构。代码如下:

In [30]:total_words = 10000 # Set word number
max_review_len = 80 # Maximum length for each sentence
embedding_len = 100 # Word vector length
# Load IMDB dataset
(x_train, y_train), (x_test, y_test) = keras.datasets.imdb.load_data(num_words=total_words)
# Pad or truncate sentences to the same length with end padding and truncation
x_train = keras.preprocessing.sequence.pad_sequences(x_train, maxlen=max_review_len,truncating='post',padding='post')
x_test = keras.preprocessing.sequence.pad_sequences(x_test, maxlen=max_review_len,truncating='post',padding='post')
print(x_train.shape, x_test.shape)
Out[30]: (25000, 80) (25000, 80)

在前面的代码中,我们将语句 max_review_len 的最大长度设置为 80 个单词。通过 keras . preprocessing . sequence . pad _ sequences 函数,我们可以快速完成填充和截断实现。以其中一个句子为例,变换后的向量是这样的:

[   1  778  128   74   12  630  163   15    4 1766 7982 1051    2   32
   85  156   45   40  148  139  121  664  665   10   10 1361  173    4
  749    2   16 3804    8    4  226   65   12   43  127   24    2   10
   10    0    0    0    0    0    0    0    0    0    0    0    0    0
    0    0    0    0    0    0    0    0    0    0    0    0    0    0
    0    0    0    0    0    0    0    0    0    0]

我们可以看到,句子的最后一部分用 0 填充,因此句子的长度正好是 80。其实在句子长度不够的情况下,我们也可以选择填充句子的开头部分。经过处理后,所有句子长度都变成 80,这样训练集可以统一存储在 shape [25000,80]的张量中,测试集可以存储在 shape [25000,80]的张量中。

下面介绍一个同时填写多个维度的例子。考虑填充图像的高度和宽度尺寸。如果我们有尺寸为 28 × 28 的图片,神经网络的输入层形状为 32 × 32,我们需要填充图像以获得 32 × 32 的形状。我们可以选择在图像矩阵的上、下、左、右各填充 2 个单元,如图 5-2 所示。

img/515226_1_En_5_Fig2_HTML.png

图 5-2

图像填充示例

前述填充方案可以如下实现:

In [31]:
x = tf.random.normal([4,28,28,1])
# Pad two units at each edge of the image
tf.pad(x,[[0,0],[2,2],[2,2],[0,0]])
Out[31]:
<tf.Tensor: id=16, shape=(4, 32, 32, 1), dtype=float32, numpy=
array([[[[ 0\.        ],
         [ 0\.        ],
         [ 0\.        ],...

经过填充操作后,图片大小变为 32 × 32,满足了神经网络的输入要求。

副本

在维度转换一节中,我们介绍了复制长度为 1 的维度的 tf.tile 函数。实际上,tf.tile 函数可用于在任何维度上重复复制多个数据副本。例如,对于形状为[4,32,32,3]的图像数据,如果复制方案为 multiples=[2,3,3,1],则表示通道维度不复制,高度和宽度维度复制三份,图像编号维度复制两份。实现如下:

In [32]:x = tf.random.normal([4,32,32,3])
tf.tile(x,[2,3,3,1])
Out[32]:<tf.Tensor: id=25, shape=(8, 96, 96, 3), dtype=float32, numpy=
array([[[[ 1.20957184e+00,  2.82766962e+00,  1.65782201e+00],
         [ 3.85402292e-01,  2.00732923e+00, -2.79068202e-01],
         [-2.52583921e-01,  7.82584965e-01,  7.56870627e-01],...

5.5 数据限制

考虑如何实现非线性激活函数 ReLU。事实上,它可以通过简单的数据限制操作来实现,其中元素的范围被限制为 x ∈ [0,+∞)。

在 TensorFlow 中,可以通过 tf.maximum (x,a)设置数据的下限,也就是可以通过 tf.minimum (x,a)设置数据的上限。

In [33]:x = tf.range(9)
tf.maximum(x,2) # Set lower limit of x to 2
Out[33]:<tf.Tensor: id=48, shape=(9,), dtype=int32, numpy=array([2, 2, 2, 3, 4, 5, 6, 7, 8])>
In [34]:tf.minimum(x,7) # Set x upper limit to 7
Out[34]:<tf.Tensor: id=41, shape=(9,), dtype=int32, numpy=array([0, 1, 2, 3, 4, 5, 6, 7, 7])>

基于 tf.maximum 函数,我们可以如下实现 ReLU:

def relu(x): # ReLU function
    return tf.maximum(x,0.) # Set lower limit of x to be 0

通过组合 tf.maximum(x,a)和 tf.minimum(x,b),可以同时限定数据的上下边界,即 x ∈ [ ab

In [35]:x = tf.range(9)
tf.minimum(tf.maximum(x,2),7) # Set x range to be [2, 7]
Out[35]:<tf.Tensor: id=57, shape=(9,), dtype=int32, numpy=array([2, 2, 2, 3, 4, 5, 6, 7, 7])>

更方便的是,我们可以使用 tf.clip_by_value 函数来实现上下限幅:

In [36]:x = tf.range(9)
tf.clip_by_value(x,2,7) # Set x range to be [2, 7]
Out[36]:<tf.Tensor: id=66, shape=(9,), dtype=int32, numpy=array([2, 2, 2, 3, 4, 5, 6, 7, 7])>

5.6 高级操作

前面的大多数函数都很常见,很容易理解。接下来,我们将介绍一些常用但稍微复杂一些的函数。

收集 tf

tf.gather 函数可以根据索引号收集数据。考虑年级书的例子。假设有四个班,每个班 35 个学生,共八个科目,年级书的张量形状为[4,35,8]。

x = tf.random.uniform([4,35,8],maxval=100,dtype=tf.int32)

现在需要收集一、二班的年级书。我们可以给出想要收集的类的索引号(例如[0,1]),并指定类的维数(例如 axis = 0)。然后通过 tf.gather 函数收集数据。

In [38]:tf.gather(x,[0,1],axis=0) # Collect data for 1st and 2nd classes
Out[38]:<tf.Tensor: id=83, shape=(2, 35, 8), dtype=int32, numpy=
array([[[43, 10, 93, 85, 75, 87, 28, 19],
        [52, 17, 44, 88, 82, 54, 16, 65],
        [98, 26,  1, 47, 59,  3, 59, 70],...

事实上,通过切片可以更方便地实现前面的要求。但是对于不规则的索引方式,比如需要抽查 1、4、9、12、13、27 名学生的年级数据,切片方式就不适合了。tf.gather 函数就是针对这种情况设计的,使用起来更方便。实现如下:

In [39]: # Collect the grade of students 1,4,9,12,13 and 27
tf.gather(x,[0,3,8,11,12,26],axis=1)
Out[39]:<tf.Tensor: id=87, shape=(4, 6, 8), dtype=int32, numpy=
array([[[43, 10, 93, 85, 75, 87, 28, 19],
        [74, 11, 25, 64, 84, 89, 79, 85],...

如果需要汇总所有学生的第三、第五科成绩,可以指定科目维度 axis = 2,实现如下:

# Collect the grades of the 3rd and 5th subjects of all students
In [40]:tf.gather(x,[2,4],axis=2)
Out[40]:<tf.Tensor: id=91, shape=(4, 35, 2), dtype=int32, numpy=
array([[[93, 75],
        [44, 82],
        [ 1, 59],...

可以看出 tf.gather 非常适合索引号没有规律的情况。索引号可以不按顺序排列,收集的数据也将按相应的顺序排列。例如:

In [41]:a=tf.range(8)
a=tf.reshape(a,[4,2])
Out[41]:<tf.Tensor: id=115, shape=(4, 2), dtype=int32, numpy=
array([[0, 1],
       [2, 3],
       [4, 5],
       [6, 7]])>
In [42]:tf.gather(a,[3,1,0,2],axis=0) # Collect element 4,2,1,3
Out[42]:<tf.Tensor: id=119, shape=(4, 2), dtype=int32, numpy=
array([[6, 7],
       [2, 3],
       [0, 1],
       [4, 5]])>

我们会把问题变得复杂一点。如果我们想检查[2,3]班[3,4,6,27]学生的学科成绩,可以通过组合多个 tf.gather 操作来实现。首先提取类[2,3]的数据:

In [43]:
students=tf.gather(x,[1,2],axis=0) # Collect data for class 2 and 3
Out[43]:<tf.Tensor: id=227, shape=(2, 35, 8), dtype=int32, numpy=
array([[[ 0, 62, 99,  7, 66, 56, 95, 98],...

然后,我们提取所选学生的相应数据:

In [44]:
tf.gather(students,[2,3,5,26],axis=1) # Collect data for students 3,4,6,27
Out[44]:<tf.Tensor: id=231, shape=(2, 4, 8), dtype=int32, numpy=
array([[[69, 67, 93,  2, 31,  5, 66, 65], ...

现在我们得到了形状为[2,4,8]的选定张量。

这次要抽查二班第二同学的所有科目,三班第三同学的所有科目,四班第四同学的所有科目。那么它是如何工作的呢?可以用笨拙的方式逐个手动提取数据。先提取第一个采样点的数据:x【1,1】。

In [45]: x[1,1]
Out[45]:<tf.Tensor: id=236, shape=(8,), dtype=int32, numpy=array([45, 34, 99, 17,  3,  1, 43, 86])>

然后提取第二个采样点x【2,2】的数据和第三个采样点x【3,3】的数据,最后将采样结果合并在一起。

In [46]: tf.stack([x[1,1],x[2,2],x[3,3]],axis=0)
Out[46]:<tf.Tensor: id=250, shape=(3, 8), dtype=int32, numpy=
array([[45, 34, 99, 17,  3,  1, 43, 86],
       [11, 25, 84, 95, 97, 95, 69, 69],
       [ 0, 89, 52, 29, 76,  7,  2, 98]])>

使用前面的方法,我们可以正确地获得 shape [3,8]的结果,其中 3 代表采样点数,4 代表每个采样点的数据。最大的问题是采样是手工串行进行的,计算效率极低。有没有更好的方法来实现这一点?

5.6.2 tf.gather_nd

使用 tf.gather_nd 函数,我们可以通过指定每个采样点的多维坐标来采样多个点。回到前面的挑战,我们要抽查二班第二个同学的所有科目,三班第三个同学的所有科目,四班第四个同学的所有科目。那么三个采样点的索引坐标就可以记录为[1,1],[2,2], [3,3],我们就可以把这个采样方案组合成一个列表[[1,1],[2,2],[3,3]]。

In [47]:
tf.gather_nd(x,[[1,1],[2,2],[3,3]])
Out[47]:<tf.Tensor: id=256, shape=(3, 8), dtype=int32, numpy=
array([[45, 34, 99, 17,  3,  1, 43, 86],
       [11, 25, 84, 95, 97, 95, 69, 69],
       [ 0, 89, 52, 29, 76,  7,  2, 98]])>

结果与串行采样方法一致,且实现更加简洁高效。

一般来说,当使用 tf.gather_nd 对多个样本进行采样时,例如,如果我们想要对类 i ,学生 j ,主题 k 进行采样,我们可以使用表达式[...,[ ijk ,...].内部列表包含每个采样点的相应索引坐标,例如:

In [48]:
tf.gather_nd(x,[[1,1,2],[2,2,3],[3,3,4]])
Out[48]:<tf.Tensor: id=259, shape=(3,), dtype=int32, numpy=array([99, 95, 76])>

在前面的代码中,我们提取了 1 班学生 2 的科目 1、2 班学生 3 的科目 2 和 3 班学生 3 的科目 4 的成绩。总共有三个年级的数据,结果总结成一个张量,形状为[3]。

5.6.3 tf.boolean_mask

除了通过给定的索引号进行采样之外,还可以通过给定的掩码进行采样。继续以形状为[4,35,8]的年级书张量为例;这次我们使用掩码方法进行数据提取。

考虑类维度中的采样,设置对应的掩码为:

mask=\left[ True, False, False, True\right]

即采样第一类和第四类。使用函数 tf.boolean_mask(x,mask,axis),可以根据掩码方案在相应的轴上执行采样,具体实现如下:

In [49]:
tf.boolean_mask(x,mask=[True, False,False,True],axis=0)
Out[49]:<tf.Tensor: id=288, shape=(2, 35, 8), dtype=int32, numpy=
array([[[43, 10, 93, 85, 75, 87, 28, 19],...

请注意,遮罩的长度必须与相应尺寸的长度相同。如果我们在类维中采样,我们必须指定长度为 4 的掩码,以指定四个类是否在采样。

如果对八个对象进行掩码采样,我们需要将掩码采样方案设置为

mask=\left[ True, False, False, True, True, False, False, True\right]

也就是说,对第一、第四、第五和第八个受试者进行采样:

In [50]:
tf.boolean_mask(x,mask=[True,False,False,True,True,False,False,True],axis=2)
Out[50]:<tf.Tensor: id=318, shape=(4, 35, 4), dtype=int32, numpy=
array([[[43, 85, 75, 19],...

不难发现,这里 tf.boolean_mask 的用法其实和 tf.gather 很像,只不过一个是用 mask 方法采样,另一个是直接给索引号。

现在让我们考虑一个类似于 tf.gather_nd 的多维掩码采样方法。为了便于演示,我们将班级数量减少到两个,学生数量减少到三个。即一个班只有三个学生,张量形状为[2,3,8]。如果我们想对第一个班的学生 1 到 2 和第二个班的学生 2 到 3 进行采样,我们可以使用 tf.gather_nd:

In [51]:x = tf.random.uniform([2,3,8],maxval=100,dtype=tf.int32)
tf.gather_nd(x,[[0,0],[0,1],[1,1],[1,2]])
Out[51]:<tf.Tensor: id=325, shape=(4, 8), dtype=int32, numpy=
array([[52, 81, 78, 21, 50,  6, 68, 19],
       [53, 70, 62, 12,  7, 68, 36, 84],
       [62, 30, 52, 60, 10, 93, 33,  6],
       [97, 92, 59, 87, 86, 49, 47, 11]])>

总共有四个学生的结果被取样,形状为[4,8]。

如果我们使用面具,我们如何表达它?表 5-2 表示相应位置的采样:

表 5-2

使用掩模法采样

|   |

学生 0

|

学生 1

|

学生 2

| | --- | --- | --- | --- | | 0 类 | 真实的 | 真实的 | 错误的 | | 1 类 | 错误的 | 真实的 | 真实的 |

因此,通过该表,可以很好地表达采用掩膜法的采样方案。代码实现如下:

In [52]:
tf.boolean_mask(x,[[True,True,False],[False,True,True]])
Out[52]:<tf.Tensor: id=354, shape=(4, 8), dtype=int32, numpy=
array([[52, 81, 78, 21, 50,  6, 68, 19],
       [53, 70, 62, 12,  7, 68, 36, 84],
       [62, 30, 52, 60, 10, 93, 33,  6],
       [97, 92, 59, 87, 86, 49, 47, 11]])>

结果和 tf.gather_nd 方法完全一样。可以看出,tf.boolean_mask 方法可用于一维和多维采样。

前面三个操作比较常用,尤其是 tf.gather 和 tf.gather_nd。下面添加了三个额外的高级操作。

在哪里

通过 tf.where(cond,a,b)函数,我们可以根据 cond 条件的真假情况从参数 a 或 b 中读取数据。条件确定规则如下:

img/515226_1_En_5_Figa_HTML.png

其中 i 是张量的元素索引。返回的张量大小与 a 和 b 一致,当condI对应位置为真时,数据从 a i 复制到 o i 。否则,将数据从 b i 复制到 o i 。考虑从所有 1 和 0 的两个张量 AB 中提取数据,其中中condIA 的对应位置提取元素 1,否则从 B 的对应位置提取元素 0。代码如下:

In [53]:
a = tf.ones([3,3])  # Tensor A
b = tf.zeros([3,3]) # Tensor B
# Create condition matrix
cond = tf.constant([[True,False,False],[False,True,False],[True,True,False]])
tf.where(cond,a,b)
Out[53]:<tf.Tensor: id=384, shape=(3, 3), dtype=float32, numpy=
array([[1., 0., 0.],
       [0., 1., 0.],
       [1., 1., 0.]], dtype=float32)>

可以看出,返回张量中 1 的位置都来自张量 A ,返回张量中 0 的位置来自张量 B

当参数 a=b=None 时,即不指定 a 和 b 参数;tf.where 返回 cond 张量中所有真元素的索引坐标。考虑下面的 cond 张量:

In [54]: cond
Out[54]:<tf.Tensor: id=383, shape=(3, 3), dtype=bool, numpy=
array([[ True, False, False],
       [False,  True, False],
       [ True,  True, False]])>

True 总共出现四次,每个 True 元素所在位置的索引分别为[0,0]、[1,1]、[2,0]和[2,1]。这些元素的索引坐标可以通过 tf.where(cond)的形式直接获得,如下所示:

In [55]:tf.where(cond)
Out[55]:<tf.Tensor: id=387, shape=(4, 2), dtype=int64, numpy=
array([[0, 0],
       [1, 1],
       [2, 0],
       [2, 1]], dtype=int64)>

那么这个有什么用呢?考虑一个场景,我们需要提取一个张量中所有的正数据和索引。首先构造张量 a,通过比较运算得到所有正数的位置掩码:

In [56]:x = tf.random.normal([3,3]) # Create tensor a
Out[56]:<tf.Tensor: id=403, shape=(3, 3), dtype=float32, numpy=
array([[-2.2946844 ,  0.6708417 , -0.5222212 ],
       [-0.6919401 , -1.9418817 ,  0.3559235 ],
       [-0.8005251 ,  1.0603906 , -0.68819374]], dtype=float32)>

通过比较运算,我们得到所有正数的掩码:

In [57]:mask=x>0 # equivalent to tf.math.greater()
mask
Out[57]:<tf.Tensor: id=405, shape=(3, 3), dtype=bool, numpy=
array([[False,  True, False],
       [False, False,  True],
       [False,  True, False]])>

通过 tf 提取掩膜张量中真元素的索引坐标,其中:

In [58]:indices=tf.where(mask) # Extract all element greater than 0
Out[58]:<tf.Tensor: id=407, shape=(3, 2), dtype=int64, numpy=
array([[0, 1],
       [1, 2],
       [2, 1]], dtype=int64)>

拿到索引后,我们可以通过 tf.gather_nd 恢复所有的正元素:

In [59]:tf.gather_nd(x,indices) # Extract all positive elements
Out[59]:<tf.Tensor: id=410, shape=(3,), dtype=float32, numpy=array([0.6708417, 0.3559235, 1.0603906], dtype=float32)>

其实在我们得到了 mask 之后,也可以直接通过 tf.boolean_mask 得到所有的正元素:

In [60]:tf.boolean_mask(x,mask) # Extract all positive elements
Out[60]:<tf.Tensor: id=439, shape=(3,), dtype=float32, numpy=array([0.6708417, 0.3559235, 1.0603906], dtype=float32)>

通过前面的一系列比较,我们可以直观地感受到这个函数有很大的实际应用价值,也可以深入了解它们的本质,以便能够以更灵活、简单、高效的方式实现我们的目的。

分散 _nd

TF . scatter _ nd(indexes,updates,shape)函数可以高效地刷新部分张量数据,但该函数只能对所有 0 张量进行刷新操作,因此可能需要结合其他操作来实现非零张量的数据刷新功能。

图 5-3 给出了一维全零张量的刷新计算原理。白板的形状由 shape 参数表示,要刷新的数据的索引号由 indexes 表示,updates 参数包含新数据。TF . scatter _ nd(indexes,updates,shape)函数根据 indexes 给出的索引位置将新数据写入全零张量,并返回更新后的结果张量。

img/515226_1_En_5_Fig3_HTML.png

图 5-3

用于刷新数据的 scatter_nd 函数

我们实现了图 5-3 中张量的刷新示例,如下所示:

In [61]: # Create indices for refreshing data
indices = tf.constant([[4], [3], [1], [7]])
# Create data for filling the indices
updates = tf.constant([4.4, 3.3, 1.1, 7.7])
# Refresh data for all 0 vector of length 8
tf.scatter_nd(indices, updates, [8])
Out[61]:<tf.Tensor: id=467, shape=(8,), dtype=float32, numpy=array([0\. , 1.1, 0\. , 3.3, 4.4, 0\. , 0\. , 7.7], dtype=float32)>

可以看出,在长度为 8 的全零张量上,相应位置的数据用来自更新的值填充。

考虑一个三维张量的例子。如图 5-4 所示,全零张量的形状是一个共有四个通道的特征图,每个通道的大小为 4 × 4。新的数据更新有一个形状[2,4,4],需要写入索引[1,3]。

img/515226_1_En_5_Fig4_HTML.png

图 5-4

3D 张量数据刷新

我们将新的特征映射写入现有张量,如下所示:

In [62]:
indices = tf.constant([[1],[3]])
updates = tf.constant([
    [[5,5,5,5],[6,6,6,6],[7,7,7,7],[8,8,8,8]],
    [[1,1,1,1],[2,2,2,2],[3,3,3,3],[4,4,4,4]]
])
tf.scatter_nd(indices,updates,[4,4,4])
Out[62]:<tf.Tensor: id=477, shape=(4, 4, 4), dtype=int32, numpy=
array([[[0, 0, 0, 0],
        [0, 0, 0, 0],
        [0, 0, 0, 0],
        [0, 0, 0, 0]],
       [[5, 5, 5, 5], # New data 1
        [6, 6, 6, 6],
        [7, 7, 7, 7],
        [8, 8, 8, 8]],
       [[0, 0, 0, 0],
        [0, 0, 0, 0],
        [0, 0, 0, 0],
        [0, 0, 0, 0]],
       [[1, 1, 1, 1], # New data 2
        [2, 2, 2, 2],
        [3, 3, 3, 3],
        [4, 4, 4, 4]]])>

可以看出,数据被刷新到第二和第四通道特征图上。

5.6.6 tf.网栅

tf.meshgrid 函数可以方便地生成二维网格的采样点坐标,方便可视化等应用。考虑两个自变量 x 和 y 的 Sinc 函数为:

z=\frac{sinsin\ \left({x}²+{y}²\right)\ }{x²+{y}²}

如果我们需要绘制区间x∈[8,8],y∈[8,8]的 Sinc 函数的 3D 曲面,如图 5-5 所示,我们首先需要生成 x 轴和 y 轴的网格点坐标集,这样就可以通过 Sinc 函数 z 的表达式计算出函数在每个位置的输出值,我们可以通过下式生成 10000 个坐标采样点:

points = []
for x in range(-8,8,100): # Loop to generate 100 sampling point for x-axis
for y in range(-8,8,100): # Loop to generate 100 sampling point for y-axis
        z = sinc(x,y)
        points.append([x,y,z])

显然,这种串行采样方法效率极低。有没有简单高效的生成网格坐标的方法?答案是 tf.meshgrid 函数。

img/515226_1_En_5_Fig5_HTML.jpg

图 5-5

正弦函数

通过分别在 x 轴和 y 轴上采样 100 个数据点,可以使用 tf.meshgrid(x,y)来生成这 10,000 个数据点的张量数据,并将它们保存在形状为[100,100,2]的张量中。为了计算方便,tf.meshgrid 在轴=二维切割后会返回两个张量,其中张量 A 包含所有点的 x 坐标,张量 B 包含所有点的 y 坐标。

In [63]:
x = tf.linspace(-8.,8,100) # x-axis
y = tf.linspace(-8.,8,100) # y-axis
x,y = tf.meshgrid(x,y)
x.shape,y.shape
Out[63]: (TensorShape([100, 100]), TensorShape([100, 100]))

使用生成的网格点坐标张量,Sinc 函数在 TensorFlow 中实现如下:

z = tf.sqrt(x**2+y**2)
z = tf.sin(z)/z  # sinc function

matplotlib 库可以用来绘制函数的 3D 曲面,如图 5-5 所示。

import matplotlib
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure()
ax = Axes3D(fig)
# Plot Sinc function
ax.contour3D(x.numpy(), y.numpy(), z.numpy(), 50)
plt.show()

5.7 加载经典数据集

到目前为止,我们已经学习了常见的张量运算,并准备实现大部分深度网络。最后,我们将用一个以张量形式实现的分类网络模型来完成这一章。在此之前,我们先正式介绍一下,对于常用的经典数据集,如何使用 TensorFlow 提供的工具方便地加载数据集。对于加载自定义数据集,我们将在后续章节中介绍。

在 TensorFlow 中,keras.datasets 模块提供了常用经典数据集的自动下载、管理、加载和转换功能,以及相应的数据集对象,这有助于多线程、预处理、混排和批量训练。

一些常用的经典数据集:

  • 波士顿住房:波士顿住房价格趋势数据集,用于训练和测试回归模型。

  • CIFAR10/100:用于图片分类任务的真实图片数据集。

  • MNIST/时尚 _MNIST:一个手写的数字图片数据集,用于图片分类任务。

  • IMDB:情感分类任务数据集,用于文本分类任务。

这些数据集在机器学习或深度学习中使用非常频繁。对于新提出的算法,一般倾向于在经典数据集上测试,然后尝试迁移到更大更复杂的数据集。

我们可以使用 datasets.xxx.load_data()函数自动加载经典数据集,其中 xxx 代表具体的数据集名称,如“CIFAR10”和“MNIST”。TensorFlow 将数据缓存在。keras/datasets 文件夹默认在用户目录下,如图 5-6 所示。用户不需要关心数据集是如何保存的。如果当前数据集不在缓存中,将自动从网络下载、解压缩和加载该数据集。如果它已经在缓存中,加载将自动完成。例如,要自动加载 MNIST 数据集:

In [66]:
import  tensorflow as tf
from    tensorflow import keras
from    tensorflow.keras import datasets # Load dataset loading module
# Load MNIST dataset
(x, y), (x_test, y_test) = datasets.mnist.load_data()
print('x:', x.shape, 'y:', y.shape, 'x test:', x_test.shape, 'y test:', y_test)
Out [66]:
x: (60000, 28, 28) y: (60000,) x test: (10000, 28, 28) y test: [7 2 1 ... 4 5 6]

函数将以相应的格式返回数据。对于图像数据集 MNIST 和 CIFAR10,将返回两个元组。第一个元组保存训练数据 x 和 y 对象;第二个元组是测试数据 x_test 和 y_test 对象。所有数据都存储在 Numpy 数组容器中。

img/515226_1_En_5_Fig6_HTML.jpg

图 5-6

TensorFlow 经典数据集保存目录

将数据加载到内存后,需要将其转换为 Dataset 对象,以便利用 TensorFlow 提供的各种便利功能。Dataset.from_tensor_slices 可用于将训练数据图像 x 和标签 y 转换成数据集对象:

# Convert to Dataset objects
train_db = tf.data.Dataset.from_tensor_slices((x, y))

将数据转换成 Dataset 对象后,我们一般需要为数据集添加一系列标准的处理步骤,比如随机洗牌、预处理、批量加载等。

洗牌

使用 Dataset.shuffle(buffer_size)函数,我们可以随机地对 Dataset 对象进行混排,以防止在每次训练过程中按照固定的顺序生成数据,这样模型就不会“记住”标签信息。代码实现如下:

train_db = train_db.shuffle(10000)

这里,buffer_size 参数指定缓冲池的大小,它通常被设置为一个较大的常数。调用数据集提供的这些实用函数将返回一个新的数据集对象。

db= db. step1\left(\right). step2\left(\right). step3.\left(\right)

这种方法按顺序完成所有的数据处理步骤,实现起来非常方便。

批量培训

为了利用 GPU 的并行计算能力,网络计算过程中一般会同时计算多个样本。我们把这种训练方法叫做批量训练,一个批量的样本数叫做批量。为了从数据集一次性生成批量样本,需要将数据集设置为批量训练模式。实现如下:

train_db = train_db.batch(128) # batch size is 128

这里 128 是批量参数,即一次并行计算 128 个样本。批量 sis 一般根据用户的 GPU 内存资源来设置。当 GPU 内存不足时,可以适当减小批量。

预处理

从 keras.datasets 加载的数据集格式在大多数情况下无法满足模型输入要求,因此需要根据用户的逻辑实现预处理步骤。Dataset 对象通过提供 map(func)实用函数可以非常方便地调用用户自定义的预处理逻辑,而预处理逻辑是在 func 函数中实现的。例如,下面的代码调用名为 preprocess 的函数来完成每个样本的预处理:

# Preprocessing is implemented in the preprocess function
train_db = train_db.map(preprocess)

考虑到 MNIST 手写数字图片数据集,图像 x 从 keras.datasets 加载后。batch()操作有 shape [ b ,28,28],其中像素用 0 到 255 的整数表示,标签形状为[ b ,数字编码。实际的神经网络输入通常需要将图像数据归一化到 0 附近的区间[0,1]或[1,1]。同时,根据网络设置,需要将 shape [28,28]的输入视图调整为合适的格式。对于标签信息,我们可以在预处理或误差计算期间选择一键编码。

这里,我们将 MNIST 图像数据映射到区间[0,1],并将视图调整为[b,28∫28]。对于标签数据,我们选择在预处理函数中执行一键编码。预处理功能实现如下:

def preprocess(x, y): # Customized preprocessing function
    x = tf.cast(x, dtype=tf.float32) / 255.
    x = tf.reshape(x, [-1, 28*28])     # flatten
    y = tf.cast(y, dtype=tf.int32)    # convert to int
    y = tf.one_hot(y, depth=10)    # one-hot encoding
    return x,y

5.7.4 新纪元培训

对于数据集对象,我们可以通过以下方式进行迭代:

   for step, (x,y) in enumerate(train_db): # Iterate with step
or
    for x,y in train_db: # Iterate without step

每次返回的 x 和 y 对象是批量样本和标签。当对 train_db 的所有样本完成一次迭代时,for 循环终止。完成一批数据训练称为一个步骤,通过多个步骤完成整个训练集的一次迭代称为一个历元。在训练中,通常需要在数据集上迭代多个历元,以获得更好的训练结果。例如,20 个历元的固定训练实现如下:

    for epoch in range(20): # Epoch number
        for step, (x,y) in enumerate(train_db): # Iteration step number
            # training...

此外,我们还可以设置一个数据集对象,以便数据集在退出之前将遍历多次,例如:

train_db = train_db.repeat(20) # Dataset iteration 20 times

上述代码使 train_db 中的 for x,y 在退出前迭代 20 个历元。无论采用这几种方法中的哪一种,都能达到同样的效果。由于上一章已经完成了正向计算的实际计算,这里就跳过了。

5.8 动手操作 MNIST 数据集

我们已经介绍并实现了前向传播和数据集。现在让我们完成剩下的分类任务逻辑。在训练过程中,通过几个步骤后打印出来,可以有效地监控错误数据。代码如下:

        # Print training error every 100 steps
        if step % 100 == 0:
            print(step, 'loss:', float(loss))

由于 loss 是张量类型的 TensorFlow,因此可以通过 float()函数将其转换为标准的 Python 浮点数。在几个步骤或几个历元训练之后,可以执行测试(验证)以获得模型的当前性能,例如:

        if step % 500 == 0: # Do a test every 500 steps
            # evaluate/test

现在让我们用张量运算函数来完成精度的实际计算。首先考虑一个批量样本 x。网络的预测值可以通过如下正向计算获得:

            for x, y in test_db: # Iterate through test dataset
                h1 = x @ w1 + b1 # 1st layer
                h1 = tf.nn.relu(h1) # Activation function
                h2 = h1 @ w2 + b2 # 2nd layer
                h2 = tf.nn.relu(h2) # Activation function
                out = h2 @ w3 + b3 # Output layer

预测值的形状是[ b ,10]。它表示样本属于每个类别的概率。我们根据 tf.argmax 函数选择出现最大概率的索引号,这是样本最可能的类别号:

                # Select the max probability category
                pred = tf.argmax(out, axis=1)

由于 y 已经在预处理中被一键编码,我们可以类似地得到 y 的类别号:

                y = tf.argmax(y, axis=1)

使用 tf.equal,我们可以比较两个结果是否相等:

                correct = tf.equal(pred, y)

对结果中所有 True(转换为 1)元素的数量求和,这是正确的预测数量:

                total_correct += tf.reduce_sum(tf.cast(correct, dtype=tf.int32)).numpy()

将正确的预测数除以测试总数,得到准确度,并将其打印出来,如下所示:

             # Calcualte accuracy
            print(step, 'Evaluate Acc:', total_correct/total)

在用 20 个历元训练一个简单的三层神经网络后,我们在测试集上取得了 87.25%的准确率。如果我们使用复杂的神经网络模型并微调网络超参数,我们可以获得更好的精度。训练误差曲线如图 5-7 所示,测试精度曲线如图 5-8 所示。

img/515226_1_En_5_Fig8_HTML.jpg

图 5-8

MNIST 测试精度

img/515226_1_En_5_Fig7_HTML.jpg

图 5-7

MNIST 培训损失

六、神经网络

很难想象哪个大行业不会被人工智能改变。人工智能将在这些行业中发挥主要作用,这一趋势非常明显。

—吴恩达

机器学习的最终目的是找到一组好的参数,让模型从训练集中学习映射关系fθ:xyxyDtrain,利用训练好的关系预测新的样本。神经网络属于机器学习研究的一个分支。具体指用多个神经元对映射函数 f θ 进行参数化的模型。

6.1 感知机

1943 年,美国神经科学家沃伦·斯特吉斯·麦卡洛克和数学逻辑学家沃尔特·皮茨受到生物神经元结构的启发,提出了人工神经元的数学模型,美国神经物理学家弗兰克·罗森布拉特进一步发展并提出了这一模型,被称为感知器模型。1957 年,Frank Rosenblatt 在 IBM-704 计算机上实现了感知器模型。该模型可以完成一些简单的视觉分类任务,如区分三角形、圆形和矩形[1]。

感知器模型的结构如图 6-1 所示。它接受一个长度为 nx = [ x 1x 2 ,…,xn]的一维向量,每个输入节点通过一个权重连接 w i

z={w}_1\;{x}_1+{w}_2\;{x}_2+\cdots +{w}_n\;{x}_n+b

其中, b 称为感知器的偏差,一维向量w=【w1w 2 ,…, w n 称为感知器的权重,而 z 称为感知器的净激活值。

img/515226_1_En_6_Fig1_HTML.png

图 6-1

感知模型

前面的公式可以写成向量形式:

z={w}^Tx+b

感知器是线性模型,不能处理线性不可分性。激活值通过在线性模型之后添加激活函数来获得:

a=\sigma (z)=\sigma \left({w}^Tx+b\right)

激活函数可以是阶跃函数。如图 6-2 所示,阶跃函数的输出只有 0/1。当z0 时,则输出 0,代表类别 0;当 z ≥ 0 时,1 为输出,代表类别 1,即:

a=\Big\{1\ {w}^Tx+b\ge 0\ 0\ {w}^Tx+b&lt;0

也可以是如图 6-3 所示的符号函数,表达式为:

a=\Big\{1\ {w}^Tx+b\ge 0-1\ {w}^Tx+b&lt;0

img/515226_1_En_6_Fig3_HTML.jpg

图 6-3

符号函数

img/515226_1_En_6_Fig2_HTML.jpg

图 6-2

阶跃函数

加入激活函数后,感知器模型可以用来完成二元分类任务。阶跃函数和符号函数在 z = 0 处不连续,因此梯度下降算法不能用于优化参数。

为了使感知器模型能够自动从数据中学习,Frank Rosenblatt 提出了一种感知器学习算法,如算法 1 所示。

| 算法 1:感知器训练算法 | | `Initialize`***w =***0***,b =*** **0**重复从训练集中随机选择一个样本(***x******I***,***y******I***)计算输出 ***a =符号***(***w******T******x******I******+b***)`If`***a≥y******【I】*****w*****;【w+y】******【I】*****【b】*****;【b+ y】******【I】***直到达到所需的步数`Output:parameters`***w***`and`*b* |

这里 η 是学习率。

虽然感知器模型已经被提出,具有很好的发展潜力,但是马文·李·闵斯基和西蒙·派珀特在 1969 年的《感知器》一书中证明了以感知器为代表的线性模型无法解决线性不可分性问题(XOR ),这直接导致了当时神经网络研究出现谷底。虽然感知器模型不能解决线性不可分问题,但书中也提到可以通过嵌套多层神经网络来解决。

6.2 全连接层

感知器模型的不可驱动性严重限制了它的潜力,使它只能解决极其简单的任务。事实上,现代深度学习模型的参数规模有几百万甚至上亿,但核心结构与感知器模型并无太大区别。在感知器模型的基础上,他们用其他光滑连续可导的激活函数代替不连续的阶跃激活函数,并堆叠多个网络层以增强网络的表达能力。

在本节中,我们替换感知器模型的激活函数,并并行堆叠多个神经元,以实现多输入多输出的网络层结构。如图 6-4 所示,两个神经元并行堆叠,即两个激活函数被替换的感知器,形成三个输入节点和两个输出节点的网络层。第一个输出节点是:

{o}_1=\sigma \left({w}_{11}\bullet {x}_1+{w}_{21}\bullet {x}_2+{w}_{31}\bullet {x}_3+{b}_1\right)

第二个节点的输出是:

{o}_2=\sigma \left({w}_{12}\bullet {x}_1+{w}_{22}\bullet {x}_2+{w}_{32}\bullet {x}_3+{b}_2\right)

把它们放在一起,输出向量就是 o = [ o 1o 2 。整个网络层可以用矩阵关系来表示:

\left[{o}_1\ {o}_2\ \right]=\left[{x}_1\ {x}_2\ {x}_3\ \right]@\left[{w}_{11}\ {w}_{12}\ {w}_{21}\ {w}_{22}\ {w}_{31}\ {w}_{32}\ \right]+\left[{b}_1\ {b}_2\ \right]

(6-1)

那就是:

O=X@W+b

输入矩阵 X 的形状定义为 中的 bd ,而 中的样本数为 b 和输入节点数为 d 。权重矩阵 W 的形状定义为[dindout],而输出节点数为d*out,偏移向量 b 的形状为[dout]。*

考虑两个样本{x}^{(1)}=\left[{x}_1^{(1)},{x}_2^{(1)},{x}_3^{(1)}\right]{x}^{(2)}=\left[{x}_1^{(2)},{x}_2^{(2)},{x}_3^{(2)}\right],前面的等式也可以写成:

\left[{o}_1^{(1)}\ {o}_2^{(1)}\ {o}_1^{(2)}\ {o}_2^{(2)}\ \right]=\left[{x}_1^{(1)}\ {x}_2^{(1)}\ {x}_3^{(1)}\ {x}_1^{(2)}\ {x}_2^{(2)}\ {x}_3^{(2)}\ \right]@\left[{w}_{11}\ {w}_{12}\ {w}_{21}\ {w}_{22}\ {w}_{31}\ {w}_{32}\ \right]+\left[{b}_1\ {b}_2\ \right]

其中输出矩阵 O 包含了 b 样本的输出,形状为[ bd out ]。由于每个输出节点都连接到所有输入节点,所以这个网络层称为全连接层,或密集层,其中 W 为权重矩阵, b 为偏置向量。

img/515226_1_En_6_Fig4_HTML.png

图 6-4

全连接层

张量模式实施

在 TensorFlow 中,要实现全连通层,只需要定义权重张量 W 和偏置张量 b 并使用 TensorFlow 提供的批量矩阵乘法函数 tf.matmul()完成网络层的计算即可。例如,对于一个输入矩阵 X 有两个样本,每个样本的输入特征长度din= 784,输出节点数dout= 256,则权重矩阵 W 的形状为【784,256】。偏置向量的形状 b 是【256】。相加后,输出层的形状为[2,256],即每个特征长度为 256 的两个样本的特征。代码实现如下:

In [1]:
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  # linear transformation
o1 = tf.nn.relu(o1)  # activation function
Out[1]:
   <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,...

事实上,我们已经多次使用上述代码来实现网络层。

层实现

全连接层本质上是矩阵乘法和加法运算。但作为最常用的网络层之一,TensorFlow 有一个更方便的实现方法:layers。密集(单位,激活)。穿过这层。密集类,只需要指定输出节点数(单位)和激活函数类型(activation)。需要注意的是,在第一次操作时,输入节点的数量将根据输入形状来确定,权重张量和偏置张量将根据输入和输出节点的数量自动创建和初始化。由于延迟评估,权重张量和偏差张量不会立即创建。需要构建函数或直接计算来完成网络参数的创建。激活参数指定当前层的激活函数,可以是常用激活函数,也可以是自定义激活函数,也可以指定为 none,即没有激活函数。

In [2]:
x = tf.random.normal([4,28*28])
from tensorflow.keras import  layers
# Create fully-connected layer with output nodes and activation function
fc = layers.Dense(512, activation=tf.nn.relu)
h1 = fc(x)  # calculate and return a new tensor
Out[2]:
<tf.Tensor: id=72, shape=(4, 512), dtype=float32, numpy=
array([[0.63339347, 0.21663809, 0\.        , ..., 1.7361937 , 0.39962345, 2.4346168 ],...

我们可以用前面代码中的一行代码创建一个全连接层 fc,输出节点数为 512,输入节点数在计算过程中自动获得。代码也自动创建内部权重张量和偏差张量。我们可以通过类内的类成员核和偏差来获得权重和偏差张量对象:

In [3]: fc.kernel # Get the weight tensor
Out[3]:
<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],
In [4]: fc.bias # Get the bias tensor
Out[4]:
<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 来完成。

In [5]: fc.trainable_variables
Out[5]:  # Return all parameters to be optimized
 [<tf.Variable 'dense_1/kernel:0' shape=(784, 512) dtype=float32,...,
 <tf.Variable 'dense_1/bias:0' shape=(512,) dtype=float32, numpy=...]

事实上,网络层不仅保存要优化的可训练变量列表,还保存不参与梯度优化的张量。例如,批处理规范化层可以通过 non _ trainable _ variables 成员返回所有不需要优化的参数列表。如果你想得到所有参数的列表,你可以通过类的 variables 成员得到所有内部张量,例如:

In [6]: fc.variables # Get all parameters
Out[6]:
[<tf.Variable 'dense_1/kernel:0' shape=(784, 512) dtype=float32,...,
 <tf.Variable 'dense_1/bias:0' shape=(512,) dtype=float32, numpy=...]

对于全连通层,所有内部张量都参与梯度优化,因此变量返回的列表与 trainable _ variables 相同。

在使用网络层类对象进行正向计算时,只需要调用该类的 call 方法,即以 fc(x)模式编写,它会自动调用 call 方法。该设置由 TensorFlow 框架自动完成。对于一个全连接的层类,在 call 方法中实现的操作逻辑非常简单。

6.3 神经网络

通过堆叠图 6-4 中完全连接的层,并确保前一层的输出节点数与当前层的输入节点数相匹配,可以创建任意层数的网络,这就是所谓的神经网络。如图 6-5 所示,通过堆叠四个全连接层,可以得到一个四层的神经网络。由于每一层都是全连通层,所以称为全连通网络。其中,第一至第三个全连接层称为隐层,最后一个全连接层的输出称为网络的输出层。隐藏层的输出节点数分别为[256,128,64],输出层的节点数为 10。

在设计全连通网络时,可以根据经验法则自由设置网络的配置等超参数,只需要遵循一些约束条件。例如,第一个隐藏层中输入节点的数量需要与数据的实际特征长度相匹配。每层中输入层的数量与前一层中输出节点的数量相匹配。输出层的激活函数和节点数需要根据所需输出的具体设置来设置。一般来说,神经网络模型的设计具有更大的自由度。如图 6-5 所示,每层输出节点数不一定非得是【256,128,64,10】可以自由搭配,比如【256,256,64,10】或者【512,64,32,10】。至于哪组超参数是最优的,需要大量的野外经验和实验。

img/515226_1_En_6_Fig5_HTML.png

图 6-5

四层神经网络

张量模式实施

对于图 6-5 这样的多层神经网络,需要分别定义每层的权矩阵和偏置向量。每层的参数只能用于对应的层,不能混用。图 6-5 中的网络模型实现如下:

# Hidden layer 1
w1 = tf.Variable(tf.random.truncated_normal([784, 256], stddev=0.1))
b1 = tf.Variable(tf.zeros([256]))
# Hidden layer 2
w2 = tf.Variable(tf.random.truncated_normal([256, 128], stddev=0.1))
b2 = tf.Variable(tf.zeros([128]))
# Hidden layer 3
w3 = tf.Variable(tf.random.truncated_normal([128, 64], stddev=0.1))
b3 = tf.Variable(tf.zeros([64]))
# Hidden layer 4
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]
            #  Hidden layer 1 forward calculation, [b, 28*28] => [b, 256]
            h1 = x@w1 + tf.broadcast_to(b1, [x.shape[0], 256])
            h1 = tf.nn.relu(h1)
            # Hidden layer 2 forward calculation, [b, 256] => [b, 128]
            h2 = h1@w2 + b2
            h2 = tf.nn.relu(h2)
            # Hidden layer 3 forward calculation, [b, 128] => [b, 64]
            h3 = h2@w3 + b3
            h3 = tf.nn.relu(h3)
            # Output layer forward calculation, [b, 64] => [b, 10]
            h4 = h3@w4 + b4

最后一层是否需要加入激活功能,通常取决于具体的任务。

使用 TensorFlow 自动求导功能计算梯度时,正演计算过程需要放在 tf 中。GradientTape()环境,这样就可以使用 GradientTape 对象的 gradient()方法自动求解参数的渐变,参数由 optimizers 对象更新。

层模式实现

对于常规的网络层,通过层的方法实现更加简洁高效。首先,创建新的网络层类,并指定每层的激活功能类型:

#  Import layers modules
from tensorflow.keras import layers,Sequential

fc1 = layers.Dense(256, activation=tf.nn.relu) #  Hidden layer 1
fc2 = layers.Dense(128, activation=tf.nn.relu) #  Hidden layer 2
fc3 = layers.Dense(64, activation=tf.nn.relu) #  Hidden layer 3
fc4 = layers.Dense(10, activation=None) #  Output layer
x = tf.random.normal([4,28*28])
h1 = fc1(x)  #  Get output of hidden layer 1
h2 = fc2(h1) #  Get output of hidden layer 2
h3 = fc3(h2) #  Get output of hidden layer 3
h4 = fc4(h3) #  Get the network output

对于这样一个数据依次转发的网络,也可以通过顺序容器封装成一个网络类对象,调用一次类的转发计算函数,完成所有层的转发计算。使用起来更方便,实现如下:

from tensorflow.keras import layers,Sequential

#  Encapsulate a neural network through Sequential container
model = Sequential([
    layers.Dense(256, activation=tf.nn.relu) , # Hidden layer 1
    layers.Dense(128, activation=tf.nn.relu) , # Hidden layer 2
    layers.Dense(64, activation=tf.nn.relu) , # Hidden layer 3
    layers.Dense(10, activation=None) , # Output layer
])

在正演计算中,只需要调用一次大型网络对象就可以完成所有图层的顺序计算:

out = model(x)

优化

我们把神经网络从输入到输出的计算过程称为前向传播。神经网络的前向传播过程也是数据张量从第一层流向输出层的过程。即从输入数据开始,通过每个隐层传递张量,直到得到输出并计算误差,这也是 TensorFlow 框架名称的由来。

正向传播的最后一步是完成误差计算:

L=g\left({f}_{\theta }(x),y\right)

在前面的公式中,fθT5(。)表示参数为 θ 的神经网络模型。 g (。)称为误差函数,用于描述当前网络的预测值fθ(x)与真实标签 y 的差距,如常用的均方误差函数。 L 称为网络的误差或损耗,一般为标量。我们希望通过学习训练集Dtrain上的一组参数来最小化训练误差 L :

前面的最小化问题通常使用反向传播算法来求解,并使用梯度下降算法来迭代更新参数:

{\theta}^{\prime }=\theta -\eta \bullet {\nabla}_{\theta }L

其中 η 为学习率。

从另一个角度来理解神经网络,它完成特征维数变换的功能,如四层 MNIST 手写数字图像识别全连通网络,依次完成 784 → 256 → 128 → 64 → 10 的特征降维过程。原始特征通常具有更高的维度,并且包含许多低级特征和无用信息。通过逐层的特征变换,较高的维度被降低到较低的维度,其中通常产生与任务高度相关的高级抽象特征信息,并且通过这些特征的简单逻辑确定,例如图片的分类,可以完成特定的任务。

网络参数的数量是衡量网络规模的重要指标。那么如何计算全连通层的参数量呢?考虑一个网络层,权重矩阵 W ,偏置向量 b ,输入特征长度 d in ,输出特征长度doutW 的参数个数为dindout。加上 bias 参数,参数总数为dindout+dout。对于多层全连接神经网络,例如 784 → 256 → 128 → 64 → 10,总参数量的表达式为:

256\bullet 784+256+128\bullet 256+128+64\bullet 128+64+10\bullet 64+10=242762

全连接层是最基本的神经网络类型。这对后续神经网络模型的研究非常重要,如卷积神经网络和循环神经网络。通过学习其他网络类型,我们会发现它们或多或少都源于全连接层网络的思想。由于 Geoffrey Hinton、Yoshua Bengio、Yann LeCun 坚持在神经网络前沿研究,为人工智能的发展做出了突出贡献,获得了 2018 年图灵奖(图 6-6 ,从右起分别为 Yann LeCun、Geoffrey Hinton、Yoshua Bengio)。

img/515226_1_En_6_Fig6_HTML.jpg

图 6-6

2018 图灵奖获奖名单 1

6.4 激活功能

下面,我们介绍神经网络中常见的激活函数。与阶跃函数和符号函数不同,这些函数是光滑的和可导的,并且适用于梯度下降算法。

乙状结肠

Sigmoid 函数也称为逻辑函数,定义如下:

Sigmoid(x)\triangleq \frac{1}{1+{e}^{-x}}

它的一个优秀特性是能够将输入 xR 压缩到一个区间 x ∈ (0,1)。这个区间的值在机器学习中常用来表示以下含义:

  • 概率分布区间(0,1)的输出匹配概率的分布范围。输出可以通过 Sigmoid 函数转换成概率

  • 信号强度通常 0~1 可以理解为某个信号的强度,比如像素的颜色强度:1 代表当前通道颜色最强,0 代表当前通道没有颜色。也可以用来表示当前闸门状态,即 1 表示打开,0 表示关闭。

Sigmoid 函数是连续可导的,如图 6-7 所示。梯度下降算法可直接用于优化网络参数。

img/515226_1_En_6_Fig7_HTML.jpg

图 6-7

Sigmoid 函数

在 TensorFlow 中,Sigmoid 函数可以通过 tf.nn.sigmoid 函数实现,如下所示:

In [7]:x = tf.linspace(-6.,6.,10)
x # Create input vector -6~6
Out[7]:
<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\.       ]...
In [8]:tf.nn.sigmoid(x) # Pass x to Sigmoid function
Out[8]:
<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)。

6.4.2 ReLU

在 ReLU(整流线性单元)之前,提出了激活函数;Sigmoid 函数通常是神经网络激活函数的首选。但是,当 Sigmoid 函数的输入值过大或过小时,梯度值接近于 0,这就是所谓的梯度弥散现象。出现这种现象时,网络参数会长时间不更新,导致训练不收敛的现象。梯度分散现象更可能发生在较深的网络模型中。2012 年提出的八层 AlexNet 模型使用了一个名为 ReLU 的激活函数,使得网络层数达到 8 层。此后,ReLU 函数的应用越来越广泛。ReLU 函数定义为:

ReLU(x)\triangleq \mathit{\max}\left(0,x\right)

功能曲线如图 6-8 所示。可以看出,ReLU 抑制所有小于 0 到 0 的值;对于正数,它直接输出。这种单方面的抑制特征来自于生物学。2001 年,神经科学家 Dayan 和 Abott 模拟了一个更精确的大脑神经元激活模型,如图 6-9 所示。具有单边压制、激发边界相对宽松等特点。ReLU 函数的设计与其非常相似[2]。

img/515226_1_En_6_Fig9_HTML.jpg

图 6-9

人脑的激活功能[2]

img/515226_1_En_6_Fig8_HTML.png

图 6-8

ReLU 函数

在 TensorFlow 中,ReLU 函数可以通过 tf.nn.relu 函数实现如下:

In [9]:tf.nn.relu(x)
Out[9]:
<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)>

可以看到,ReLU 激活函数后,负数全部被抑制为 0,正数被保留。

除了使用函数接口 tf.nn.relu 实现 relu 函数外,ReLU 函数还可以像密集层一样作为网络层添加到网络中。对应的类是层。ReLU()。一般来说,激活函数类不是主要的网络计算层,不计入网络层数。

ReLU 函数的设计源自神经科学。函数值和导数值的计算非常简单。同时具有优秀的梯度特性。在大量深度学习应用中已经被验证非常有效。

6.4.3 泄漏事故

x <为 0 时,ReLU 函数的导数始终为 0,这也可能造成梯度分散。为了克服这个问题,提出了 LeakyReLU 功能(图 6-10 )。

LeakyReLU\triangleq \Big\{x\kern1em x\ge 0\kern0.5em px\ x&lt;0

其中 p 是用户设定的小值,比如 0.02。当 p = 0 时,LeakyReLU 函数退化为 ReLU 函数。当 p ≠ 0 时,可以在 x < 0 处得到一个小的导数值,从而避免了梯度分散的现象。

img/515226_1_En_6_Fig10_HTML.png

图 6-10

LeakyReLU 函数

在 TensorFlow 中,LeakyReLU 函数可以通过 tf.nn.leaky_relu 实现如下:

In [10]:tf.nn.leaky_relu(x, alpha=0.1)
Out[10]:
<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)>

alpha 参数代表 p 。tf.nn.leaky_relu 对应的类是 layers.LeakyReLU,可以通过 LeakyReLU(alpha)创建一个 LeakyReLU 网络层,设置参数 p 。和密集层一样,LeakyReLU 层可以放在网络上合适的位置。

6.4.4 谭

双曲正切函数可以将输入 xR 压缩到一个区间(1,1),定义如下:

tanhtanh\ (x)=\frac{\left({e}^x-{e}^{-x}\right)}{\left({e}^x+{e}^{-x}\right)}

=2\bullet sigmoid(2x)-1

可以看出,通过 Sigmoid 函数缩放平移后可以实现 Tanh 激活功能,如图 6-11 所示。

img/515226_1_En_6_Fig11_HTML.png

图 6-11

Tanh 函数

在 Tensorflow 中,可以使用 tf.nn.tanh 实现 Tanh 函数,如下所示:

In [11]:tf.nn.tanh(x)
Out[11]:
<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)>

可以看到,向量元素值的范围映射到(1,1)。

6.5 输出层的设计

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

我们将根据输出值的范围对讨论进行分类。常见的输出类型包括:

  • oIRd输出属于整个实数空间,或实数空间的某一部分,如函数值趋势预测、年龄预测问题。

  • o

  • oI∈【0,1】,∈IoI= 1 输出值落在区间[0,1]内,所有输出值之和为 1。常见的问题包括多分类问题,如 MNIST 手写数字图片识别,该图片属于十类的概率之和应为 1。

  • oI∈[1,1]输出值在-1 和 1 之间。

6.5.1 常见实数空间

这类问题比较常见。比如正弦函数曲线、年龄预测、股票走势预测都属于连续实数空间的整体或部分,输出层可能没有激活函数。误差的计算直接基于最后一层的输出 o 和真值 y 。例如,均方误差函数用于测量输出值 o 和真实值 y 之间的距离:

L=g\left(o,y\right)

其中 g 代表特定的误差计算函数,如 MSE。

[0,1]区间

输出值属于区间[0,1]也很常见,比如图像生成,二值分类问题。在机器学习中,图像像素值一般归一化为区间[0,1]。如果直接使用输出图层的值,像素值范围将分布在整个实数空间中。为了将像素值映射到有效实数空间[0,1],需要在输出层之后添加合适的激活函数。在这里,Sigmoid 函数是一个很好的选择。

同样,对于二进制分类问题,比如硬币的正面和反面的预测,输出层只能是一个节点就是一个事件 A 发生的概率 P ( x )给网络输入 x,如果我们用网络的输出标量 o 来表示正面事件发生的概率,那么负面事件发生的概率就是 1o。网络结构如图 6-12 所示。

P(x)=o

P(x)=1-o

img/515226_1_En_6_Fig12_HTML.png

图 6-12

具有单个输出节点的二元分类网络

在这种情况下,只需在输出图层的值后添加 Sigmoid 函数,即可将输出转换为概率值。对于二进制分类问题,除了用单个输出节点来表示事件 A P ( x )的发生概率外,还可以分别预测 P ( x )和 P ( x ),并满足约束条件:

P(x)+P(x)=1

其中\underset{\_}{A}表示事件 a 的相反事件,如图 6-13 所示,二元分类网络的输出层为两个节点。第一个节点的输出值代表事件 A * P * ( x )发生的概率,第二个节点的输出值代表相反事件 P ( x )发生的概率。该函数只能将单个值压缩到区间(0,1),并且不考虑两个节点值之间的关系。我们希望它们除了满足oI∈【0,1】之外,还能满足概率之和为 1:

{\sum}_i{o}_i=1

这种情况就是下一节要介绍的问题设置。

img/515226_1_En_6_Fig13_HTML.png

图 6-13

具有两个输出的二元分类网络

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

对于输出值oI∈【0,1】,且所有输出值之和为 1 的情况,是多分类最常见的问题。如图 6-15 所示,输出层的每个输出节点代表一个类别。图中的网络结构用于处理三种分类任务。三个节点的输出值分布代表当前样本属于类别 A、B、C 的概率: P ( x )、P(B|x)、P(C|x)。因为多分类问题中的样本只能属于其中一个类别,所以所有类别的概率之和应该是 1。

如何实现这个约束逻辑?这可以通过向输出层添加 Softmax 函数来实现。Softmax 函数定义为:

Softmax\left({z}_i\right)\triangleq \frac{e^{z_i}}{\sum_{j=1}^{d_{out}}{e}^{z_j}}

Softmax 函数不仅可以将输出值映射到区间[0,1],还可以满足所有输出值之和为 1 的特性。如图 6-14 中的例子所示,输出层的输出为[2.0,1.0,0.1]。通过 Softmax 函数后,输出变为[0.7,0.2,0.1]。每个值代表当前样本属于每个类别的概率,概率值之和为 1。输出层的输出可以通过 Softmax 函数转换为类别概率,该函数在分类问题中非常常用。

img/515226_1_En_6_Fig15_HTML.png

图 6-15

多分类网络结构

img/515226_1_En_6_Fig14_HTML.png

图 6-14

Softmax 函数示例

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

In [12]: z = tf.constant([2.,1.,0.1])
tf.nn.softmax(z)
Out[12]:
<tf.Tensor: id=19, shape=(3,), dtype=float32, numpy=array([0.6590012, 0.242433 , 0.0985659], dtype=float32)>

与密集图层类似,Softmax 函数也可以用作网络层类。通过图层添加 Softmax 图层很方便。Softmax (axis = -1)类,其中 axis 参数指定要计算的维度。

在 Softmax 函数的数值计算过程中,由于输入值较大,容易出现数值溢出现象。当计算交叉熵时,可能发生类似的问题。对于数值计算的稳定性,TensorFlow 提供了一个统一的接口,同时实现 Softmax 和交叉熵损失函数,并处理数值不稳定的异常。通常建议使用这些接口函数。函数接口为 TF . keras . loss . categorial _ cross entropy(y_true,y_pred,from_logits = False),其中 y _ true 表示独热编码真标签,y_pred 表示网络的预测值。当 from_logits 设置为 True 时,y_pred 表示没有经过 Softmax 函数的变量 z。当 from_logits 设置为 False 时,y_pred 表示为 Softmax 函数的输出。对于数值计算稳定性,一般将 from_logits 设置为 True,这样 TF . keras . loss . category _ cross entropy 会在内部进行 Softmax 函数计算,不需要在模型中显式显式调用 Softmax 函数。例如:

In [13]:
z = tf.random.normal([2,10]) # Create output of the output layer
y_onehot = tf.constant([1,3]) # Create real label
y_onehot = tf.one_hot(y_onehot, depth=10) # one-hot encoding
# The Softmax function is not explicitly used in output layer, so
# from_logits=True. categorical_cross-entropy function will use Softmax
# function first in this case.
loss = keras.losses.categorical_crossentropy(y_onehot,z,from_logits=True)
loss = tf.reduce_mean(loss) # calculate the loss
loss
Out[13]:
<tf.Tensor: id=210, shape=(), dtype=float32, numpy= 2.4201946>

除了功能界面,你还可以使用损耗。CategoricalCrossentropy(from _ logits)类方法来同时计算 Softmax 和交叉熵损失函数。例如:

In [14]:
criteon = keras.losses.CategoricalCrossentropy(from_logits=True)
loss = criteon(y_onehot,z)
loss
Out[14]:
<tf.Tensor: id=258, shape=(), dtype=float32, numpy= 2.4201946>

区间(-1,1)

如果希望输出值的范围以区间(1,1)分布,只需使用双曲正切函数即可:

In [15]:
x = tf.linspace(-6.,6.,10)
tf.tanh(x)
Out[15]:
<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 散度和铰链损耗。其中,均方误差函数和交叉熵函数在深度学习中较为常见。均方差函数主要用于回归问题,交叉熵函数主要用于分类问题。

均方差函数

均方误差(MSE)函数将输出向量和真实向量映射到笛卡尔坐标系中的两个点,通过计算这两个点之间的欧几里德距离(准确地说,欧几里德距离的平方)来测量这两个向量之间的差异:

MSE\left(y,o\right)\triangleq \frac{1}{d_{out}}{\sum}_{i=1}^{d_{out}}{\left({y}_i-{o}_i\right)}²

MSE 的值总是大于或等于 0。当 MSE 函数达到最小值 0 时,输出等于真实标签,神经网络的参数达到最优状态。

MSE 函数广泛用于回归问题。实际上,MSE 函数也可以用于分类问题。在 TensorFlow 中,MSE 计算可以以函数或层的方式实现。例如,使用如下函数实现 MSE 计算:

In [16]:
o = tf.random.normal([2,10]) # Network output
y_onehot = tf.constant([1,3]) # Real label
y_onehot = tf.one_hot(y_onehot, depth=10)
loss = keras.losses.MSE(y_onehot, o) # Calculate MSE
loss
Out[16]:
<tf.Tensor: id=27, shape=(2,), dtype=float32, numpy=array([0.779179 , 1.6585705], dtype=float32)>

特别是,MSE 函数返回每个样本的均方误差。您需要在样本维度上再次求平均值,以获得平均样本的均方误差。实现如下:

In [17]:
loss = tf.reduce_mean(loss)
loss
Out[17]:
<tf.Tensor: id=30, shape=(), dtype=float32, numpy=1.2188747>

它也可以在层模式下实现。对应的类是 keras . loss . meansquadererror()。和其他类一样,可以调用 call 函数来完成正向计算。代码如下:

In [18]:
criteon = keras.losses.MeanSquaredError()
loss = criteon(y_onehot,o)
loss
Out[18]:
<tf.Tensor: id=54, shape=(), dtype=float32, numpy=1.2188747>

6.6.2 交叉熵误差函数

在引入交叉熵损失函数之前,我们先介绍一下信息学中熵的概念。1948 年,Claude Shannon 将热力学中熵的概念引入信息论,用来度量信息的不确定性。熵在信息科学中也叫信息熵或香农熵。熵越大,不确定性越大,信息量越大。分布的熵 P ( i )定义为:

H(P)\triangleq -{\sum}_iP(i)P(i)

事实上,也可以使用其他基函数。例如,对于四类分类问题,如果样本的真实标签是类别 4,那么标签的一键编码就是[0,0,0,1]。即这张图片的分类是唯一确定的,属于不确定度为 0 的类别 4,其熵可以简单计算为:

-0\bullet 0-0\bullet 0-0\bullet 0-1\bullet 1=0

也就是说,对于某个分布,熵为 0,不确定性最低。

如果预测的概率分布是[0.1,0.1,0.1,0.7],那么它的熵可以计算为:

-0.1\bullet 0.1-0.1\bullet 0.1-0.1\bullet 0.1-0.7\bullet 0.7\approx 1.356

考虑一个随机分类器,它对每个类别的预测概率是相等的:[0.25,0.25,0.25,0.25]。同理,可以计算出它的熵约为 2,这种情况下的不确定性略大于前一种情况。

因为,熵总是大于等于 0。当熵达到最小值 0 时,不确定度为 0。分类问题的一热码分布就是熵为 0 的典型例子。在 TensorFlow 中,我们可以用 tf.math.log 来计算熵。

在引入熵的概念后,我们将基于熵推导出交叉熵的定义:

H\left(p\Big\Vert q\right)\triangleq -{\sum}_ip(i)q(i)

通过变换,交叉熵可以分解为熵和 KL 散度之和(Kullback-Leibler 散度):

H\left(p\Big\Vert q\right)=H(p)+{D}_{KL}\left(p\Big\Vert q\right)

其中 KL 散度为:

{D}_{KL}\left(p\Big\Vert q\right)={\sum}_ip(i)\mathit{\log}\left(\frac{p(i)}{q(i)}\right)

KL 散度是 Solomon Kullback 和 Richard A. Leibler 在 1951 年使用的一个指标,用来衡量两个分布之间的距离。当 p = q 时,DKL(pq)的最小值为 0。 pq 相差越大,DKL(pq)越大。应当注意,交叉熵和 KL 散度都不是对称的,即:

H\left(p\Big\Vert q\right)\ne H\left(q\Big\Vert p\right)

{D}_{KL}\left(p\Big\Vert q\right)\ne {D}_{KL}\left(q\Big\Vert p\right)

交叉熵是两个分布之间“距离”的一个很好的度量。特别是当分类问题中 y 的分布采用一热编码时, H ( p ) = 0。然后,

H\left(p\Big\Vert q\right)=H(p)+{D}_{KL}\left(p\Big\Vert q\right)={D}_{KL}\left(p\Big\Vert q\right)

也就是说,交叉熵退化为真实标签分布和输出概率分布之间的 KL 散度。

根据 KL 散度的定义,我们推导出分类问题中交叉熵的计算表达式:

H\left(p\Big\Vert q\right)={D}_{KL}\left(p\Big\Vert q\right)={\sum}_j{y}_j\mathit{\log}\left(\frac{y_j}{o_j}\right)

=1\bullet \mathit{\log}\frac{1}{o_i}+{\sum}_{j\ne i}0\bullet \mathit{\log}\left(\frac{0}{o_j}\right)

=-\mathit{\log}{o}_i

其中 I 是独热编码中 1 的索引号,也是实范畴。可以看出,交叉熵只与实范畴上的概率oI有关,对应的概率 o i 越大,H(pq越小。当相应类别上的概率为 1 时,交叉熵达到最小值 0。此时网络输出与真实标签完全一致,神经网络获得最优状态。

因此,最小化交叉熵损失函数的过程也是最大化正确类别预测概率的过程。从这个角度来看,理解交叉熵损失函数是非常直观和容易的。

6.7 神经网络的类型

全连接层是神经网络最基本的类型,它为神经网络的后续研究做出了巨大的贡献。全连通层的正演计算过程比较简单,梯度求导也比较简单,但是有一个最大的缺陷。在处理特征长度较大的数据时,全连通层的参数量往往较大,使得全连通网络的参数数量庞大,难以训练。近年来,社交媒体的发展产生了大量的图片、视频、文本等数字资源,极大地推动了神经网络在计算机视觉和自然语言处理领域的研究,并相继提出了一系列神经网络类型。

6.7.1 卷积神经网络

如何识别、分析和理解图片、视频等数据是计算机视觉的一个核心问题。全连接层在处理高维图片和视频数据时,往往存在网络参数庞大、训练非常困难等问题。Yann Lecun 于 1986 年提出了卷积神经网络(CNN)。随着深度学习的繁荣,卷积神经网络在计算机视觉中的性能已经大大超越了其他算法,呈现出统治计算机视觉领域的趋势。用于图像分类的流行模型包括 AlexNet、VGG、GoogLeNet、ResNet 和 DenseNet。对于客观识别,有 RCNN、快速 RCNN、更快 RCNN、掩蔽 RCNN、YOLO 和 SSD。我们将在第十章中详细介绍卷积神经网络的原理。

6.7.2 循环神经网络

除了具有空间结构的图片、视频等数据,序列信号也是一种非常常见的数据类型。最有代表性的序列信号之一是文本。如何处理和理解文本数据是自然语言处理的一个核心问题。由于缺乏记忆机制和处理不定长信号的能力,卷积神经网络不擅长处理序列信号。在 Yoshua Bengio,Jürgen Schmidhuber 等人的不断研究下,循环神经网络(RNN)被证明在处理序列信号方面非常出色。1997 年,Jürgen Schmidhuber 提出了 LSTM 网络。作为 RNN 的变体,它更好地克服了 RNN 缺乏长期记忆和不擅长处理长序列的问题。LSTM 在自然语言处理中得到了广泛应用。基于 LSTM 模型,Google 提出了机器翻译的 Seq2Seq 模型,并成功应用于 Google 神经机器翻译系统(GNMT)中。其他 RNN 变体包括 GRU 和双向 RNN。我们将在第十一章中详细介绍循环神经网络的原理。

6.7.3 注意机制网络

RNN 不是自然语言处理的最终解决方案。近年来,注意力机制的提出,克服了 RNN 的不足,如训练不稳定、难以并行化等。在自然语言处理、图像生成等领域逐渐崭露头角。注意机制最初是在图像分类任务上提出的,但逐渐开始在自然语言处理中变得更加有效。2017 年,谷歌提出了第一个使用纯注意力机制的网络模型 Transformer,随后基于 Transformer 模型,又相继提出了 GPT、伯特、GPT-2 等一系列用于机器翻译的注意力网络模型。在其他领域,基于注意机制尤其是自我注意机制的网络也取得了不错的效果,比如 BigGAN 模型。

6.7.4 图形卷积神经网络

图片和文本等数据具有规则的空间或时间结构,称为欧几里德数据。卷积神经网络和循环神经网络非常擅长处理这种类型的数据。对于像一系列不规则的空间拓扑、社交网络、通讯网络、蛋白质分子结构这样的数据,那些网络似乎无能为力。2016 年,Thomas Kipf 等人提出了基于一阶近似谱卷积算法的图卷积网络(GCN)模型。GCN 算法实现简单,可以从空间一阶邻居信息聚合的角度直观地理解,因此在半监督任务上取得了良好的效果。随后,一系列网络模型被提出,如 GAT、EdgeConv 和 DeepGCN。

6.8 汽车油耗预测实践

在本节中,我们将使用全连接网络模型来完成汽车 MPG(每加仑英里数)的预测。

数据集

我们使用 auto MPG 数据集,其中包括各种车辆性能指标的真实数据和其他因素,如气缸数量、重量和马力。数据集的前五项如表 6-1 所示。此外,数字字段 origin 表示类别,其他字段都是数字类型。对于原产地,1 表示美国,2 表示欧洲,3 表示日本。

表 6-1

自动 MPG 数据集的前五项

|

每加仑行驶英里数

|

圆筒

|

排水量

|

马力

|

重量

|

加速

|

年型

|

起源

| | --- | --- | --- | --- | --- | --- | --- | --- | | Eighteen | eight | Three hundred and seven | One hundred and thirty | Three thousand five hundred and four | Twelve | Seventy | one | | Fifteen | eight | Three hundred and fifty | One hundred and sixty-five | Three thousand six hundred and ninety-three | Eleven point five | Seventy | one | | Eighteen | eight | Three hundred and eighteen | One hundred and fifty | Three thousand four hundred and thirty-six | Eleven | Seventy | one | | Sixteen | eight | Three hundred and four | One hundred and fifty | Three thousand four hundred and thirty-three | Twelve | Seventy | one | | Seventeen | eight | Three hundred and two | One hundred and forty | Three thousand four hundred and forty-nine | Ten point five | Seventy | one |

自动 MPG 数据集总共包括 398 条记录。我们将数据集从 UCI 服务器下载并读取到 DataFrame 对象中。代码如下:

# Download the dataset online
dataset_path = keras.utils.get_file("auto-mpg.data", "http://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data")
# Use Pandas library to read the dataset
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()
# Show some data
dataset.head()

原始表中的数据可能包含丢失的值。这些记录项目需要清除:

dataset.isna().sum() # Calculate the number of missing values
dataset = dataset.dropna() # Drop missing value records
dataset.isna().sum() # Calculate the number of missing values again

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

由于 origin 字段是分类数据,我们首先将其删除,然后将其转换为三个新字段,USA、Europe 和 Japan,这三个字段表示它们是否来自该原点:

origin = dataset.pop('Origin')
dataset['USA'] = (origin == 1)*1.0
dataset['Europe'] = (origin == 2)*1.0
dataset['Japan'] = (origin == 3)*1.0
dataset.tail()

将数据分为训练(80%)和测试(20%)数据集:

train_dataset = dataset.sample(frac=0.8,random_state=0)
test_dataset = dataset.drop(train_dataset.index)

将 MPG 移出并使用其真正的标签:

train_labels = train_dataset.pop('MPG')
test_labels = test_dataset.pop('MPG')

计算训练集每个字段值的均值和标准差,完成数据的标准化,通过 norm()函数;代码如下:

train_stats = train_dataset.describe()
train_stats.pop("MPG")
train_stats = train_stats.transpose()
# Normalize the data
def norm(x): # minus mean and divide by std
  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 records in training dataset with 9 features.
(78, 9) (78,) # 78 records in training dataset with 9 features.

创建 TensorFlow 数据集:

train_db = tf.data.Dataset.from_tensor_slices((normed_train_data.values, train_labels.values))
train_db = train_db.shuffle(100).batch(32) # Shuffle and batch

我们可以通过简单观察数据集中各场之间的分布来观察各场对 MPG 的影响,如图 6-16 所示。大致可以观察到,汽车排量、重量、MPG 之间的关系比较简单。随着排量或重量的增加,汽车的 MPG 降低,能耗增加;缸数越少,MPG 就能越好,符合我们的生活经验。

img/515226_1_En_6_Fig16_HTML.jpg

图 6-16

特征之间的关系

6.8.2 创建网络

考虑到自动 MPG 数据集的规模较小,我们仅创建一个三层全连接网络来完成 MPG 预测任务。有九个输入要素,因此第一层的输入节点数为 9。第一层和第二层的输出节点数设计为 64 和 64。由于预测值只有一种,所以输出层的输出节点设计为 1。因为 MPG 属于实数空间,所以可以不增加输出层的激活功能。

我们将网络实现为自定义的网络类。我们只需要在初始化函数中创建每个子网络层,在正向计算函数中实现自定义网络类的计算逻辑。自定义网络类继承自 keras。模型类,这也是自定义网络类的标准编写方法,为了方便使用 keras 提供的 trainable _ variables、save_weights 等各种方便的函数。模特班。网络模型类的实现如下:

class Network(keras.Model):
    # regression network
    def __init__(self):
        super(Network, self).__init__()
        # create 3 fully-connected layers
        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):
        # pass through the 3 layers sequentially
        x = self.fc1(inputs)
        x = self.fc2(x)
        x = self.fc3(x)

        return x

培训和测试

创建主网络模型类后,让我们实例化网络对象并创建优化器,如下所示:

model = Network() # Instantiate the network
# Build the model with 4 batch and 9 features
model.build(input_shape=(4, 9))
model.summary() # Print the network
# Create the optimizer with learning rate 0.001
optimizer = tf.keras.optimizers.RMSprop(0.001)

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

for epoch in range(200): # 200 Epoch
    for step, (x,y) in enumerate(train_db): # Loop through training set once
        # Set gradient tape
        with tf.GradientTape() as tape:
            out = model(x) # Get network output
            loss = tf.reduce_mean(losses.MSE(y, out)) # Calculate MSE
            mae_loss = tf.reduce_mean(losses.MAE(y, out)) # Calculate MAE

        if step % 10 == 0: # Print training loss every 10 steps
            print(epoch, step, float(loss))
        # Calculate and update gradients
        grads = tape.gradient(loss, model.trainable_variables)
        optimizer.apply_gradients(zip(grads, model.trainable_variables))

对于回归问题,除了均方误差(MSE)之外,平均绝对误差(MAE)也可以用来衡量模型的性能。

mae\triangleq \frac{1}{d_{out}}{\sum}_i\left|{y}_i-{o}_i\right|

我们可以在训练和测试数据集的每个历元结束时记录 MAE,并绘制如图 6-17 所示的变化曲线。

img/515226_1_En_6_Fig17_HTML.jpg

图 6-17

MAE 曲线

可以看出,当训练达到大约第 25 个历元时,MAE 的下降变得较慢,其中训练集的 MAE 继续缓慢下降,但测试集的 MAE 几乎保持不变,所以我们可以在第 25 个历元左右结束训练,利用此时的网络参数来预测新的输入。

6.9 参考文献

  1. 尼克,2017。人工智能简史。

  2. X.Glorot,A. Bordes 和 Y. Bengio,“深度稀疏整流器神经网络”,第十四届人工智能和统计国际会议论文集,美国佛罗里达州劳德代尔堡,2011 年。

  3. J.Mizera-Pietraszko 和 P. Pichappan,《实时智能系统讲义》,施普林格国际出版公司,2017 年。

Footnotes 1

图片来源:www.theverge.com/2019/3/27/18280665/ai-godfathers-turing-award-2018-yoshua-bengio-geoffrey-hinton-yann-lecun