政安晨:【掌握AI的深度学习工具Keras API】(一)—— 【构建Keras模型的不同方法】(万字长文)

慈云数据 1年前 (2024-04-08) 技术支持 52 0

目录

前言

开始

Keras工作流程

构建Keras模型的不同方法

序贯模型

函数式API

模型子类化

混合使用不同的组件

用正确的工具完成工作


政安晨的个人主页:政安晨

欢迎 👍点赞✍评论⭐收藏

收录专栏: TensorFlow与Keras机器学习实战

希望政安晨的博客能够对您有所裨益,如有不足之处,欢迎在评论区提出指正!

我们开始一个全新的系列——Keras API。

通过这个系列的文章,您将了解到下面这几部分内容:

1)构建Keras模型的3种方法,即序贯模型、函数式API和模型子类化

2)使用Keras内置的训练循环和评估循环

3) 使用Keras回调函数来自定义训练

4) 使用TensorBoard监控训练指标和评估指标

5) 从头开始编写训练循环和评估循环


前言

TensorFlow的Keras API是一个高层次的神经网络API,用于构建和训练深度学习模型。Keras是一个开源的深度学习库,最初是作为一个独立的项目开发的,后来被集成到TensorFlow中。

Keras API提供了一组高级的函数和类,使得构建神经网络模型变得非常简单和直观。

使用Keras,可以通过几行代码定义一个神经网络模型,并使用各种模型层、激活函数和优化器来配置模型。

Keras API的设计目标是易用性和灵活性。

它提供了丰富的模型层,包括全连接层、卷积层、池化层、循环层等。此外,Keras还提供了各种激活函数、损失函数和优化器的选择。

用户可以根据自己的需求选择适合的模型层和配置参数。

Keras API还提供了方便的功能,使得模型的训练和评估变得非常简单。

用户可以使用fit()函数来训练模型,并使用evaluate()函数来评估模型在测试数据上的性能。Keras还支持自定义回调函数,可以在训练过程中执行各种操作,如保存模型、动态调整学习率等。

总之,TensorFlow的Keras API提供了一个简单而强大的工具来构建和训练深度学习模型。它的易用性和灵活性使得深度学习变得更加容易上手,并且可以满足各种复杂的需求。

开始

通过作者政安晨以前的文章,相信聪明如您已经掌握了一些Keras使用经验,已经熟悉序贯模型、Dense层,以及用于训练、评估和推断的内置API——compile()、fit()、evaluate()和predict()。

您还学习了如何通过继承Layer类来创建自定义层,以及如何使用TensorFlow的GradientTape来逐步实现一个训练循环。

在开始咱们这篇演绎之前,咱们先准备好环境,可以参考我在这篇文章里提到的搭建环境的部分:

政安晨的机器学习笔记——跟着演练快速理解TensorFlow(适合新手入门)icon-default.png?t=N7T8https://blog.csdn.net/snowdenkeke/article/details/135950931这篇文章里的演绎,我将在Windows系统中构建的Anaconda虚拟环境中的TensorFlow中进行。

Keras工作流程

Keras API的设计原则是渐进式呈现复杂性(progressive disclosure of complexity):

易于上手,同时又可以处理非常复杂的用例,只需逐步渐进式学习。

简单的工作流程应该简单易懂,同时还有高级的工作流程:无论你的问题多么罕见、多么复杂,应该都有一条清晰的解决路径。这条路径是建立在简单工作流程的基础之上的。也就是说,你从初学者成长为专家,可以一直使用相同的工具,只是使用方式不同。

因此,Keras没有唯一“正确”的使用方式。

相反,Keras提供了一系列工作流程,既有非常简单的工作流程,也有非常灵活的工作流程。

构建和训练Keras模型都有许多种方法,可以满足不同的需求。

所有这些工作流程都基于共享API,比如Layer和Model,所以任何一种工作流程的组件都可以用于其他工作流程,它们之间可以互相通信。

构建Keras模型的不同方法

在Keras中,构建模型可以使用以下3个API,如下图所示:

序贯模型(sequential model):这是最容易理解的API。它本质上是Python列表,仅限于层的简单堆叠。

函数式API(functional API):它专注于类似图的模型结构。它在可用性和灵活性之间找到了很好的平衡点,因此是构建模型最常用的API。

模型子类化(model subclassing):它是一个底层选项,你可以从头开始自己编写所有内容。如果你想控制每一个小细节,那么它是理想的选择。但是这样就无法使用Keras内置的许多特性,而且更容易犯错误。

(对于构建模型,渐进式呈现复杂性)

序贯模型

要构建Keras模型,最简单的方法就是使用序贯模型(Sequential类),代码如下所示:

先看一下Sequential类:

from tensorflow import keras
from tensorflow.keras import layers
model = keras.Sequential([
    layers.Dense(64, activation="relu"),
    layers.Dense(10, activation="softmax")
])

请注意,还可以使用add()方法逐步构建相同的模型,代码如下所示:

(它类似于Python列表的append()方法)

逐步构建序贯模型:

model = keras.Sequential()
model.add(layers.Dense(64, activation="relu"))
model.add(layers.Dense(10, activation="softmax"))

只有在第一次调用层时,层才会被构建(创建层权重)。这是因为层权重的形状取决于输入形状,只有知道输入形状之后才能创建权重。

因此,前面的序贯模型是没有权重的,演绎如下所示:

(这时模型还没有完成构建)

只有在数据上调用模型,或者调用模型的build()方法并给定输入形状时,模型才具有权重,代码如下所示(通过第一次调用模型来完成构建):

# 现在可以检索模型权重
model.build(input_shape=(None, 3))
# 调用模型的build()方法。模型样本形状应该是(3,)。输入形状中的None表示批量可以是任意大小
model.weights

演绎如下:

模型构建完成之后,可以用summary()方法显示模型内容,这对调试很有帮助,演绎如下:

可以看到,这个模型刚好被命名为sequential_1。你可以对Keras中的所有对象命名,包括每个模型和每一层,代码如下所示(利用name参数命名模型和层):

model = keras.Sequential(name="my_example_model")
model.add(layers.Dense(64, activation="relu", name="my_first_layer"))
model.add(layers.Dense(10, activation="softmax", name="my_last_layer"))
model.build((None, 3))
model.summary()

演绎如下:

逐步构建序贯模型时,每添加一层就打印出当前模型的概述信息,这是非常有用的。但在模型构建完成之前是无法打印概述信息的。

有一种方法可以实时构建序贯模型:只需提前声明模型的输入形状。你可以通过Input类来做到这一点,代码如下所示(提前声明模型的输入形状):

model = keras.Sequential()
# 利用Input声明输入形状。请注意,shape参数应该是单个样本的形状,而不是批量的形状
model.add(keras.Input(shape=(3,)))
model.add(layers.Dense(64, activation="relu"))

现在你可以使用summary()来跟踪观察,添加更多层之后模型的输出形状是如何变化的,代码如下(使用summary()跟踪模型输出形状的变化):

这是一种常用的调试工作流程,用于处理那些对输入进行复杂变换的层。

函数式API

序贯模型易于使用,但适用范围非常有限:它只能表示具有单一输入和单一输出的模型,按顺序逐层进行处理。

我们在实践中经常会遇到其他类型的模型,比如多输入模型(如图像及其元数据)、多输出模型(预测数据的不同方面)或具有非线性拓扑结构的模型。

在这种情况下,你可以使用函数式API构建模型。

你在现实世界中遇到的大多数Keras模型属于这种类型。它很强大,也很有趣,就像拼乐高积木一样。

简单示例

我们先来看一个简单的示例,即上面提到的两层堆叠。这个例子也可以用函数式API来实现,代码如下所示:

(带有两个Dense层的简单函数式模型)

inputs = keras.Input(shape=(3,), name="my_input")
features = layers.Dense(64, activation="relu")(inputs)
outputs = layers.Dense(10, activation="softmax")(features)
model = keras.Model(inputs=inputs, outputs=outputs)

我们来逐行解释一下。

首先声明一个Input(注意,你也可以对输入对象命名,就像对其他对象一样)。

inputs = keras.Input(shape=(3,), name="my_input")

这个inputs对象保存了关于模型将处理的数据的形状和数据类型的信息。

对于这个模型处理的批量,每个样本的形状为(3,),每个批量的样本数量是可变的(代码中的批量大小为None),数据批量的数据类型为float32 float32

我们将这样的对象叫作符号张量(symbolic tensor)。它不包含任何实际数据,但编码了调用模型时实际数据张量的详细信息。它代表的是未来的数据张量。

接下来,我们创建了一个层,并在输入上调用该层。

features = layers.Dense(64, activation="relu")(inputs)

所有Keras层都可以在真实的数据张量与这种符号张量上调用。对于后一种情况,层返回的是一个新的符号张量,其中包含更新后的形状和数据类型信息。

得到最终输出之后,我们在Model构造函数中指定输入和输出,将模型实例化。

outputs = layers.Dense(10, activation="softmax")(features)
model = keras.Model(inputs=inputs, outputs=outputs)

演绎如下:

多输入、多输出模型

与上述简单模型不同,大多数深度学习模型看起来不像列表,而像图。比如,模型可能有多个输入或多个输出。正是对于这种模型,函数式API才真正表现出色。

假设你要构建一个系统,按优先级对客户支持工单进行排序,并将工单转给相应的部门。

这个模型有3个输入:

工单标题(文本输入)

工单的文本正文(文本输入)

用户添加的标签(分类输入,假定为one-hot编码)

我们可以将文本输入编码为由1和0组成的数组,数组大小为vocabulary_size

模型还有2个输出:

工单的优先级分数,它是介于0和1之间的标量(sigmoid输出)

应处理工单的部门(对所有部门做softmax)

利用函数式API,仅凭几行代码就可以构建这个模型,代码如下所示:

(多输入、多输出的函数式模型)

vocabulary_size = 10000
num_tags = 100
num_departments = 4
# (本行及以下2行)定义模型输入
title = keras.Input(shape=(vocabulary_size,), name="title")
text_body = keras.Input(shape=(vocabulary_size,), name="text_body")
tags = keras.Input(shape=(num_tags,), name="tags")
# 通过拼接将输入特征组合成张量features
features = layers.Concatenate()([title, text_body, tags])
# 利用中间层,将输入特征重组为更加丰富的表示
features = layers.Dense(64, activation="relu")(features)
# 定义模型输出
priority = layers.Dense(1, activation="sigmoid", name="priority")(features)
department = layers.Dense(
    # 定义模型输出
    num_departments, activation="softmax", name="department")(features)
# (本行及以下1行)通过指定输入和输出来创建模型
model = keras.Model(inputs=[title, text_body, tags],
                    outputs=[priority, department])

函数式API很简单,就像拼乐高积木一样,可以非常灵活地定义由层构成的图。

训练一个多输入、多输出模型

这种模型的训练方法与序贯模型相同,都是对输入数据和输出数据组成的列表调用fit()。

这些数据列表的顺序应该与传入Model构造函数的inputs的顺序相同,代码如下所示:

(通过给定输入和目标组成的列表来训练模型)

import numpy as np
num_samples = 1280
# (本行及以下2行)虚构的输入数据
title_data = np.random.randint(0, 2, size=(num_samples, vocabulary_size))
text_body_data = np.random.randint(0, 2, size=(num_samples, vocabulary_size))
tags_data = np.random.randint(0, 2, size=(num_samples, num_tags))
# (本行及以下1行)虚构的目标数据
priority_data = np.random.random(size=(num_samples, 1))
department_data = np.random.randint(0, 2, size=(num_samples, num_departments))
model.compile(optimizer="rmsprop",
              loss=["mean_squared_error", "categorical_crossentropy"],
              metrics=[["mean_absolute_error"], ["accuracy"]])
model.fit([title_data, text_body_data, tags_data],
          [priority_data, department_data],
          epochs=1)
model.evaluate([title_data, text_body_data, tags_data],
               [priority_data, department_data])
priority_preds, department_preds = model.predict(
    [title_data, text_body_data, tags_data])

如果不想依赖输入顺序(比如有多个输入或输出),你也可以为Input对象和输出层指定名称,通过字典传递数据,代码如下所示(通过给定输入和目标组成的字典来训练模型):

model.compile(optimizer="rmsprop",
              loss={"priority": "mean_squared_error", "department":
                    "categorical_crossentropy"},
              metrics={"priority": ["mean_absolute_error"], "department":
                       ["accuracy"]})
model.fit({"title": title_data, "text_body": text_body_data,
           "tags": tags_data},
          {"priority": priority_data, "department": department_data},
          epochs=1)
model.evaluate({"title": title_data, "text_body": text_body_data,
                "tags": tags_data},
               {"priority": priority_data, "department": department_data})
priority_preds, department_preds = model.predict(
    {"title": title_data, "text_body": text_body_data, "tags": tags_data})

函数式API的强大之处:获取层的连接方式

函数式模型是一种图数据结构。

这便于我们查看层与层之间是如何连接的,并重复使用之前的图节点(层输出)作为新模型的一部分。它也很适合作为大多数研究人员在思考深度神经网络时使用的“思维模型”:由层构成的图。它有两个重要的用处:模型可视化与特征提取

我们来可视化上述模型的连接方式(模型的拓扑结构)。你可以用plot_model()将函数式模型绘制成图,如下图所示:

keras.utils.plot_model(model, "ticket_classifier.png")

上图为将plot_model()应用于工单分类模型生成的图。

你可以将模型每一层的输入形状和输出形状添加到这张图中,这对调试很有帮助,如下图所示:

keras.utils.plot_model(
    model, "ticket_classifier_with_shape_info.png", show_shapes=True)

上图为添加形状信息后的模型图。

张量形状中的None表示批量大小,也就是说,该模型接收任意大小的批量。

获取层的连接方式,意味着你可以查看并重复使用图中的节点(层调用)。模型属性model.layers给出了构成模型的层的列表。对于每一层,你都可以查询layer.input和layer. output,代码如下所示(检索函数式模型某一层的输入或输出):

model.layers
model.layers[3].input

这样一来,我们就可以进行特征提取,重复使用模型的中间特征来创建新模型。

假设你想对前一个模型增加一个输出——估算某个问题工单的解决时长,这是一种难度评分。实现方法是利用包含3个类别的分类层,这3个类别分别是“快速”“中等”和“困难”。你无须从头开始重新创建和训练模型。你可以从前一个模型的中间特征开始(这些中间特征是可以访问的),代码如下所示:(重复使用中间层的输出,创建一个新模型)

# layers[4]是中间的Dense层
features = model.layers[4].output
difficulty = layers.Dense(3, activation="softmax", name="difficulty")(features)
new_model = keras.Model(
    inputs=[title, text_body, tags],
    outputs=[priority, department, difficulty])

我们来绘制新模型的图,如下图所示:

keras.utils.plot_model(
    new_model, "updated_ticket_classifier.png", show_shapes=True)

上图为新模型的图:

模型子类化

最后一种构建模型的方法是最高级的方法:模型子类化,也就是将Model类子类化。

咱们以前的文章中介绍过如何通过将Layer类子类化来创建自定义层,将Model类子类化的方法与其非常相似:

在__init__()方法中,定义模型将使用的层;

在call()方法中,定义模型的前向传播,重复使用之前创建的层;

将子类实例化,并在数据上调用,从而创建权重。

将前一个例子重新实现为Model子类

我们看一个简单的例子:使用Model子类重新实现客户支持工单管理模型,代码清单如下所示

(简单的子类化模型)

class CustomerTicketModel(keras.Model):
    def __init__(self, num_departments):
        # 不要忘记调用super()构造函数!
        super().__init__()
        # (本行及以下3行)在构造函数中定义子层
        self.concat_layer = layers.Concatenate()
        self.mixing_layer = layers.Dense(64, activation="relu")
        self.priority_scorer = layers.Dense(1, activation="sigmoid")
        self.department_classifier = layers.Dense(
            num_departments, activation="softmax")
    #  在call()方法中定义前向传播
    def call(self, inputs):
        title = inputs["title"]
        text_body = inputs["text_body"]
        tags = inputs["tags"]
        features = self.concat_layer([title, text_body, tags])
        features = self.mixing_layer(features)
        priority = self.priority_scorer(features)
        department = self.department_classifier(features)
        return priority, department

定义好模型之后,就可以将模型实例化。

请注意,只有第一次在数据上调用模型时,模型才会创建权重,就像Layer子类一样。

model = CustomerTicketModel(num_departments=4)
priority, department = model(
    {"title": title_data, "text_body": text_body_data, "tags": tags_data})

到目前为止,一切看起来都与Layer子类化非常相似,咱们以前已经讲过。那么,Layer子类和Model子类之间有什么区别呢?

答案很简单:“层”是用来创建模型的组件,而“模型”是高阶对象,用于训练、导出进行推理等。简而言之,Model有fit()、evaluate()和predict()等方法,而Layer则没有。除此之外,这两个类几乎相同。

你可以编译和训练Model子类,就像序贯模型或函数式模型一样。

model.compile(optimizer="rmsprop",
# (本行及以下1行)参数loss和metrics的结构必须与call()返回的内容完全匹配——这里是两个元素组成的列表
              loss=["mean_squared_error", "categorical_crossentropy"],
              metrics=[["mean_absolute_error"], ["accuracy"]])
# (本行及以下2行)输入数据的结构必须与call()方法的输入完全匹配——这里是一个字典,字典的键是title、text_body和tags
model.fit({"title": title_data,
           "text_body": text_body_data,
           "tags": tags_data},
# 目标数据的结构必须与call()方法返回的内容完全匹配——这里是两个元素组成的列表
          [priority_data, department_data],
          epochs=1)
model.evaluate({"title": title_data,
                "text_body": text_body_data,
                "tags": tags_data},
               [priority_data, department_data])
priority_preds, department_preds = model.predict({"title": title_data,
   "text_body": text_body_data,
   "tags": tags_data})

模型子类化是最灵活的模型构建方法。

它可以构建那些无法表示为层的有向无环图的模型,比如这样一个模型,其call()方法在for循环中使用层,甚至递归调用这些层。

注意:子类化模型不能做什么

这种自由是有代价的:对于子类化模型,你需要负责更多的模型逻辑,也就是说,你犯错的可能性会更大。因此,你需要做更多的调试工作。

你开发的是一个新的Python对象,而不仅仅是将乐高积木拼在一起。

函数式模型和子类化模型在本质上有很大区别。

函数式模型是一种数据结构——它是由层构成的图,你可以查看、检查和修改它。子类化模型是一段字节码——它是带有call()方法的Python类,其中包含原始代码。这是子类化工作流程具有灵活性的原因——你可以编写任何想要的功能,但它引入了新的限制。

举例来说,由于层与层之间的连接方式隐藏在call()方法中,因此你无法获取这些信息。调用summary()无法显示层的连接方式,利用plot_model()也无法绘制模型拓扑结构。同样,对于子类化模型,你也不能通过访问图的节点来做特征提取,因为根本就没有图。将模型实例化之后,前向传播就完全变成了黑盒子。

混合使用不同的组件

至关重要的是,选择序贯模型、函数式API和模型子类化中的某一种方法,并不会妨碍你使用其他方法。Keras API的所有模型之间都可以顺畅地交互,无论是序贯模型、函数式模型,还是从头编写的子类化模型。它们都是一系列工作流程的一部分。

举例来说,你可以在函数式模型中使用子类化的层或模型,创建一个包含子类化模型的函数式模型,代码如下:

class Classifier(keras.Model):
    def __init__(self, num_classes=2):
        super().__init__()
        if num_classes == 2:
            num_units = 1
            activation = "sigmoid"
        else:
            num_units = num_classes
            activation = "softmax"
        self.dense = layers.Dense(num_units, activation=activation)
    def call(self, inputs):
        return self.dense(inputs)
inputs = keras.Input(shape=(3,))
features = layers.Dense(64, activation="relu")(inputs)
outputs = Classifier(num_classes=10)(features)
model = keras.Model(inputs=inputs, outputs=outputs)

反过来,你也可以将函数式模型作为子类化层或模型的一部分,代码如下所示:

(创建一个包含函数式模型的子类化模型)

inputs = keras.Input(shape=(64,))
outputs = layers.Dense(1, activation="sigmoid")(inputs)
binary_classifier = keras.Model(inputs=inputs, outputs=outputs)
class MyModel(keras.Model):
    def __init__(self, num_classes=2):
        super().__init__()
        self.dense = layers.Dense(64, activation="relu")
        self.classifier = binary_classifier
    def call(self, inputs):
        features = self.dense(inputs)
        return self.classifier(features)
model = MyModel()

用正确的工具完成工作

你已经了解了构建Keras模型的一系列工作流程,从最简单的工作流程(序贯模型)到最高级的工作流程(模型子类化)。应该选择哪一种呢?每一种都有自己的优缺点,你应该选择最适合手头工作的那一种。

一般来说,函数式API在易用性和灵活性之间实现了很好的平衡。它还可以直接获取层的连接方式,非常适合进行模型可视化或特征提取。如果你能够使用函数式API,也就是说,你的模型可以表示为层的有向无环图,那么我建议使用函数式API而不是模型子类化。

咱们这个系列的文章,后续所有示例都将使用函数式API,因为书中所有模型都可以表示为由层构成的图。但我们也会经常使用子类化的层。一般来说,使用包含子类化的层的函数式模型,可以实现两全其美的效果:既保留函数式API的优点,又具有较强的开发灵活性。


微信扫一扫加客服

微信扫一扫加客服

简体中文English