TensorFlow 实战(一)(3)https://developer.aliyun.com/article/1522666
3.1.3 函数式 API
现在是时候实现第二个模型(即,模型 B)了,该模型使用主成分作为额外的输入。希望这个额外输入(主成分)能为模型提供额外的特征,从而提高模型的性能。主成分是使用一种称为主成分分析(PCA)的算法提取出来的。PCA 是一种降维技术,它会将高维数据投影到一个较低维度的空间中,同时努力保留数据中存在的方差。现在你需要创建一个模型,该模型接受两个不同的输入特征集。
你不能再使用 Sequential API,因为它只能处理顺序模型(即,单输入层通过一系列层产生单输出)。在这里,我们有两个不同的输入:花卉的原始特征和 PCA 特征。这意味着两个层以并行方式工作,产生两个不同的隐藏表示,并将它们连接起来,最后为输入产生类别概率,如图 3.3 所示。函数式 API 对于这种类型的模型是一个很好的选择,因为它可以用于定义具有多个输入或多个输出的模型。
图 3.3 函数式 API 与其他 API 的对比(灰色块为无法使用的功能)
让我们开始吧。首先,我们需要导入以下层和模型对象,因为它们将成为我们模型的核心:
from tensorflow.keras.layers import Input, Dense, Concatenate from tensorflow.keras.models import Model
接下来,我们需要创建两个 Input 层(用于原始输入特征和 PCA 特征):
inp1 = Input(shape=(4,)) inp2 = Input(shape=(2,))
原始输入特征的 Input 层将有四个特征列,而 PCA 特征的 Input 层将只有两个特征列(因为我们只保留了前两个主成分)。如果回顾一下我们如何使用 Sequential API 定义模型,你会注意到我们没有使用 Input 层。但在使用函数式 API 时,我们需要明确指定我们需要包含在模型中的 Input 层。
定义了两个 Input 层后,我们现在可以计算这些层的单独隐藏表示:
out1 = Dense(16, activation='relu')(inp1) out2 = Dense(16, activation='relu')(inp2)
这里,out1 表示 inp1 的隐藏表示(即原始特征),out2 是 inp2 的隐藏表示(即 PCA 特征)。然后我们连接这两个隐藏表示:
out = Concatenate(axis=1)([out1,out2])
让我们更详细地了解在使用 Concatenate 层时会发生什么。Concatenate 层只是沿着给定的轴连接两个或多个输入。在此示例中,我们有两个输入传递给 Concatenate 层(即 [None, 16] 和 [None, 16]),并希望沿着第二个轴(即 axis=1)进行连接。请记住,当您指定 shape 参数时,Keras 会向输入/输出张量添加一个额外的批次维度。这个操作的结果是一个大小为 [None, 32] 的张量。从这一点开始,您只有一个序列的层。我们将定义一个具有 relu 激活函数的 16 节点 Dense 层,最后是一个具有 softmax 归一化的三节点输出层:
out = Dense(16, activation='relu')(out) out = Dense(3, activation='softmax')(out)
我们需要做一步额外的工作:创建一个 Model 对象,说明输入和输出是什么。现在,我们有一堆层和没有 Model 对象。最后,我们像之前一样编译模型。我们选择 categorical_crossentropy 作为损失函数,adam 作为优化器,像之前一样。我们还将监控训练准确率:
model = Model(inputs=[inp1, inp2], outputs=out) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])
这个模型的完整代码在以下清单中提供。
列表 3.2 使用 Keras 函数式 API 实现的模型 B
from tensorflow.keras.layers import Input, Dense, Concatenate from tensorflow.keras.models import Model import tensorflow.keras.backend as K K.clear_session() ❶ inp1 = Input(shape=(4,)) ❷ inp2 = Input(shape=(2,)) ❷ out1 = Dense(16, activation='relu')(inp1) ❸ out2 = Dense(16, activation='relu')(inp2) ❸ out = Concatenate(axis=1)([out1,out2]) ❹ out = Dense(16, activation='relu')(out) out = Dense(3, activation='softmax')(out) model = Model(inputs=[inp1, inp2], outputs=out) ❺ model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])❻
❶ 确保清除 TensorFlow 图
❷ 两个输入层。一个输入层具有四个特征,另一个输入层具有两个特征。
❸ 两个并行隐藏层
❹ 负责将两个并行输出 out1 和 out2 进行拼接的连接层
❺ 模型定义
❻ 使用损失函数、优化器和评估指标编译模型
现在你可以打印模型的摘要了
model.summary()
得到的结果为
Model: "model" _____________________________________________________________________________________ Layer (type) Output Shape Param # Connected to ===================================================================================== input_1 (InputLayer) [(None, 4)] 0 _____________________________________________________________________________________ input_2 (InputLayer) [(None, 2)] 0 _____________________________________________________________________________________ dense (Dense) (None, 16) 80 input_1[0][0] _____________________________________________________________________________________ dense_1 (Dense) (None, 16) 48 input_2[0][0] _____________________________________________________________________________________ concatenate (Concatenate) (None, 32) 0 dense[0][0] dense_1[0][0] _____________________________________________________________________________________ dense_2 (Dense) (None, 16) 528 concatenate[0][0] _____________________________________________________________________________________ dense_3 (Dense) (None, 3) 51 dense_2[0][0] ===================================================================================== Total params: 707 Trainable params: 707 Non-trainable params: 0 _____________________________________________________________________________________
对于这个摘要表示,你觉得怎么样?你能从这个摘要中推断出它是什么样的模型吗?很遗憾,不能。虽然我们的模型有并行层,但是摘要看起来似乎我们有一系列按顺序处理输入和输出的层。我们有没有办法获得比这更好的表示呢?是的,我们有!
Keras 还提供了以网络图的形式可视化模型的能力。您可以使用下面的代码实现:
tf.keras.utils.plot_model(model)
如果您在 Jupyter 笔记本上运行此命令,您将获得以下图形的内联输出(图 3.4)。现在我们的模型的运行情况更加清晰了。
图 3.4 使用函数式 API 创建的模型示例。可以在顶部看到并行的输入层和隐藏层。最终的输出层位于底部。
如果您需要将此图保存到文件中,只需执行以下操作:
tf.keras.utils.plot_model(model, to_file='model.png’)
如果您需要在层的名称和类型之外查看输入/输出大小,可以通过将 show_shapes 参数设置为 True 来实现
tf.keras.utils.plot_model(model, show_shapes=True)
这将返回图 3.5。
图 3.5 使用 show_shapes=True 绘制的 Keras 模型图
请记住我们有两个输入,原始特征(x)和 x 的前两个主成分(我们称之为 x_pca)。您可以如下计算前两个主成分(使用 scikit-learn 库):
from sklearn.decomposition import PCA pca_model = PCA(n_components=2, random_state=4321) x_pca = pca_model.fit_transform(x)
PCA 已经在 scikit-learn 中实现。你定义一个 PCA 对象,并将值 2 传递给 n_components 参数。你也固定了随机种子,以确保在各个试验中保持一致性。然后你可以调用 fit_transform(x) 方法来获得最终的 PCA 特征。你可以像之前一样训练这个模型,调用
model.fit([x, x_pca], y, batch_size=64, epochs=10)
遗憾的是,你不会看到很大的准确率提升。结果将与您之前达到的相当。在给定的代码示例中,使用这个模型时你会有大约 6% 的准确率提升。然而,你会发现,如果增加迭代次数,这个差距会变得越来越小。这主要是因为添加 PCA 特征并没有真正增加多少价值。我们将四个维度减少到两个,这不太可能产生比我们已经拥有的更好的特征。让我们在下一个练习中试试运气。
3.1.4 子类化 API
回到研究实验室,看到添加主成分并没有改善结果有点令人沮丧。然而,团队对于你对于在给定模型中使用哪个 API 的了解却印象深刻。一位团队成员建议了一个最终模型。当前,密集层是通过以下方式计算其输出的
h = α(xW + b)
其中 α 是某种非线性。你想看看是否通过添加另一个偏差(即,除了加性偏差外,我们添加了一个乘法偏差)可以改善结果,使得方程变为
h = α([xW + b] × b[mul])
这就是层子类化会拯救一切的地方,因为在 Keras 中没有预先构建的层能够提供这种功能。Keras 提供的最终 API 是子类化 API(见图 3.6),它将允许我们将所需的计算定义为一个计算单元(即,一个层),并在定义模型时轻松重用它。子类化来自软件工程概念中的继承。其思想是你有一个提供某种对象一般功能的超类(例如,一个 Layer 类),然后你从该层中派生(或继承),创建一个更具体的层,实现特定功能。
图 3.6 子类化 API 与其他 API 的比较(已灰显)
子类化 API 与顺序 API 和函数 API 有着截然不同的风格。在这里,你正在创建一个 Python 类,该类定义了层或模型的基本操作。在本书中,我们将专注于子类化层(即不包括模型)。在我看来,更多的情况下你会对层进行子类化而不是模型,因为层的子类化更加方便,可能在你只有一个模型或多个模型的情况下需要。然而,只有当你创建由许多较小模型组成的更大的复合模型时,才需要模型的子类化。值得注意的是,一旦你学会了层的子类化,扩展到模型的子类化相对容易。
当子类化层时,有三个重要的函数需要从你继承的 Layer 基类中重写:
- init() — 使用任何它接受的参数初始化层
- build() — 模型的参数将在这里创建
- call() — 定义了正向传播期间需要进行的计算
这是你会写的新层。我们将适当地称呼我们的自定义层为 MulBiasDense。注意这一层是如何继承自位于 tensorflow.keras.layers 子模块中的基础层 Layer 的。
列表 3.3 使用 Keras 子类化新层
from tensorflow.keras import layers class MulBiasDense(layers.Layer): def __init__(self, units=32, input_dim=32, activation=None): ❶ super(MulBiasDense, self).__init__() ❶ self.units = units ❶ self.activation = activation ❶ def build(self, input_shape): ❷ self.w = self.add_weight(shape=(input_shape[-1], self.units), ❷ initializer='glorot_uniform', trainable=True)❷ self.b = self.add_weight(shape=(self.units,), ❷ initializer='glorot_uniform', trainable=True)❷ self.b_mul = self.add_weight(shape=(self.units,), ❷ initializer='glorot_uniform', trainable=True)❷ def call(self, inputs): ❸ out = (tf.matmul(inputs, self.w) + self.b) * self.b_mul ❸ return layers.Activation(self.activation)(out) ❸
❶ 定义了定义层所需的各种超参数
❷ 将层中的所有参数定义为 tf.Variable 对象。self.b_mul 代表了乘法偏置。
❸ 定义了在向层馈送数据时需要进行的计算
首先,我们有 init() 函数。层有两个参数:隐藏单元的数量和激活类型。激活默认为 None,意味着如果未指定,则没有非线性激活(即仅进行线性转换):
def __init__(self, units=32, activation=None): super(MulBiasDense, self).__init__() self.units = units self.activation = activation
接下来,我们实现 build() 函数,这是子类化中的一个重要的拼图。所有参数(例如权重和偏置)都是在这个函数内创建的:
def build(self, input_shape): self.w = self.add_weight(shape=(input_shape[-1], self.units), initializer='glorot_uniform', trainable=True) self.b = self.add_weight(shape=(self.units,), initializer='glorot_uniform', trainable=True) self.b_mul = self.add_weight(shape=(self.units,), initializer='glorot_uniform', trainable=True)
在这里,参数 w、b 和 b_mul 分别指代方程中的 W、b 和 b[mul]。对于每个参数,我们提供了形状、初始化器和一个布尔值以指示可训练性。此处使用的初始化器 ‘glorot_uniform’(mng.bz/N6A7
)是一种流行的神经网络初始化器。最后,我们需要编写 call() 函数,它定义了输入将如何转换为输出:
def call(self, inputs): out = (tf.matmul(inputs, self.w) + self.b) * self.b_mul return layers.Activation(self.activation)(out)
就是这样:我们的第一个子类化层。值得注意的是,在子类化层时,你需要了解的其他几个函数还有:
- compute_output_shape() — 通常,Keras 会自动推断出层的输出形状。但是,如果你进行了太多复杂的转换,Keras 可能会迷失方向,你将需要使用这个函数明确地定义输出形状。
- get_config() - 如果您计划在训练后将模型保存到磁盘,则需要实现此函数,该函数返回由图层使用的参数的字典。
定义了新的层后,可以像以下清单展示的那样使用函数式 API 创建模型。
清单 3.4 使用 Keras 子类化 API 实现的模型 C
from tensorflow.keras.layers import Input, Dense, Concatenate ❶ from tensorflow.keras.models import Model ❶ import tensorflow.keras.backend as K ❶ import tensorflow as tf ❶ K.clear_session() ❷ inp = Input(shape=(4,)) ❸ out = MulBiasDense(units=32, activation='relu')(inp) ❹ out = MulBiasDense(units=16, activation='relu')(out) ❹ out = Dense(3, activation='softmax')(out) ❺ model = Model(inputs=inp, outputs=out) ❻ model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])❼
❶ 导入必要的模块和类
❷ 确保我们清除 TensorFlow 图
❸ 定义输入层
❹ 使用新的子类化层 MulBiasDense 定义两个层
❺ 定义 softmax 输出层
❻ 定义最终模型
❼ 使用损失函数、优化器和准确率作为指标编译模型
不幸的是,在我们的实验中,我们尝试的所有架构改进都没有带来明显的改进。但是,您通过知道针对哪个模型使用哪个 API 使同事们感到印象深刻,使小组能够在提交论文的截止日期前准备好结果。表 3.1 进一步总结了我们讨论的 API 的主要优缺点。
表 3.1 使用不同 Keras APIs 的优缺点
Sequential API | Pros | 使用顺序 API 实现的模型易于理解、简洁。 |
Cons | 无法实现具有多输入/输出等复杂架构特性的模型。 | |
Functional API | Pros | 可用于实现具有多输入/输出等复杂架构元素的模型。 |
Cons | 开发人员需要手动正确连接各种层并创建模型。 | |
Sub-classing API | Pros | 可以创建不作为标准层提供的自定义层和模型。 |
Cons | 需要深入理解 TensorFlow 提供的底层功能。 | |
由于用户定义的性质,可能会导致不稳定性和调试困难。 |
在下一节中,我们将讨论您可以在 TensorFlow 中导入和摄入数据的不同方式。
练习 1
假设您需要创建一个具有单个输入层和两个输出层的全连接神经网络。您认为哪个 API 最适合这项任务?
3.2 获取 TensorFlow/Keras 模型的数据
到目前为止,我们已经看过了如何使用不同的 Keras APIs 实现各种模型。此时,您应该已经知道何时使用哪种 API(有时甚至知道不该使用哪种 API)来查看模型的架构。接下来,我们将学习如何使用 TensorFlow/Keras 读取数据来训练这些模型。
假设您最近加入了一家初创公司,作为一名数据科学家,正在尝试使用包含机器学习模型的软件来识别花的品种(使用图像)。他们已经有一个可以接受一批图像和一批标签并训练模型的自定义数据管道。然而,这个数据管道相当隐晦且难以维护。您的任务是实现一个易于理解和维护的替代数据管道。这是一个通过使用 TensorFlow 快速原型设计数据管道来给您的老板留下深刻印象的绝佳机会。
除非经过数据训练,否则模型没有任何价值。更多(高质量)的数据意味着更好的性能,因此将数据以可伸缩和高效的方式提供给模型非常重要。现在是时候探索 TensorFlow 的特性,以创建实现此目的的输入管道。有两种流行的获取数据的替代方法:
- tf.data API
- Keras 数据生成器
您将要处理的数据集(从 mng.bz/DgVa
下载)包含一个包含文件名和标签的 CSV(逗号分隔值)文件以及一个包含 210 个花卉图像(.png 格式)的集合。
注意 还有第三种方法,那就是使用 Python 包访问流行的机器学习数据集。这个包被称为 tensorflow-datasets。这意味着只有当您想要使用包已支持的数据集时,此方法才有效。
现在是时候伸展一下手指,开始实现数据管道了。
TensorFlow 实战(一)(5)https://developer.aliyun.com/article/1522668