学习笔记-深度学习-深入Keras

内容是对《Python深度学习》的摘录、理解、代码实践和遇到的问题。

本章内容

  • 构架Keras模型的3种方法,即序贯模型Sequential、函数式API和模型子类化
  • 使用Keras内置的训练循环和评估循环
  • 使用Keras回调函数来自定义训练
  • 使用TensorBoard监控训练指标和评估指标
  • 从头开始编写训练循环和评估循环

后面几章将深入介绍计算机视觉、时间序列预测、自然语言处理和生成式深度学习。要实现这些复杂的应用,需要的知识远不止Sequential架构和默认的fit()循环。本章将全面介绍使用Keras API的重要方法。

除了第一部分外,其他内容都在理解或代码实践上遇到了困难,之后有机会再补充。

构建Keras模型的不同方法

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

  • 序贯模型sequential model:这是最容易理解的API。它本质上是Python列表,仅限于层的简单堆叠。
  • 函数式API function API:它专注于类似图的模型结构。它在可用性和灵活性之间找到了很好的平衡点,因此是构建模型最常用的API。
  • 模型子类化model subclassing:它是一个底层选项,可以从头开始自己编写所有内容。如果想控制每一个小细节,那么它是理想的选择。但是这样就无法使用Keras内置的许多特性,而且更容易犯错误。

序贯模型

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

在处理复杂变换的层(比如第八章将介绍的卷积层)时,经常会用到summary( )方法显示模型内容以帮助调试。注意调用过一次模型,模型构建完成之后才能使用。

import tensorflow.keras as keras
from tensorflow.keras import layers

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

model.build(input_shape=(None, 3))  # 样本形状是(3,), None表示批量可以是任意大小
model.summary()

Model: "sequential"

_________________________________________________________________

Layer (type)                 Output Shape              Param #  

=================================================================

dense (Dense)                multiple                  256      

_________________________________________________________________

dense_1 (Dense)              multiple                  650      

=================================================================

Total params: 906

Trainable params: 906

Non-trainable params: 0

_________________________________________________________________

从第一行可以看到,模型被命名为"sequential"。可以对Keras中的所有对象命名,包括每个模型和每一层(使用name参数实现)。

import tensorflow.keras as keras

from tensorflow.keras import layers



model = keras.Sequential(name="example_model")

model.add(layers.Dense(64, activation="relu", name="first_layer"))

model.add(layers.Dense(10, activation="softmax", name="last_layer"))



model.build(input_shape=(None, 3))  # 样本形状是(3,), None表示批量可以是任意大小

model.summary()

Model: "example_model"

_________________________________________________________________

Layer (type)                 Output Shape              Param #  

=================================================================

first_layer (Dense)          multiple                  256      

_________________________________________________________________

last_layer (Dense)           multiple                  650      

=================================================================

Total params: 906

Trainable params: 906

Non-trainable params: 0

逐步构建序贯模型时,每添加一层就打印出当前模型的概述信息是很有用的。但在模型构建完成之前无法打印概述信息。为了实现实时构建序贯模型,可以通过Input类提前声明模型的输入形状。

import tensorflow.keras as keras

from tensorflow.keras import layers



model = keras.Sequential()

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

model.add(layers.Dense(64, activation="relu"))

model.summary()

Model: "sequential"

_________________________________________________________________

Layer (type)                 Output Shape              Param #  

=================================================================

dense (Dense)                (None, 64)                256      

=================================================================

Total params: 256

Trainable params: 256

Non-trainable params: 0

model.add(layers.Dense(10, activation="softmax"))

model.summary()

Model: "sequential"

_________________________________________________________________

Layer (type)                 Output Shape              Param #  

=================================================================

dense (Dense)                (None, 64)                256      

_________________________________________________________________

dense_1 (Dense)              (None, 10)                650      

=================================================================

Total params: 906

Trainable params: 906

Non-trainable params: 0

如上,就可以使用summary( )来跟踪观察,添加更多层之后模型的输出形状是如何变化的。对于处理那些对输入进行复杂变换的层(比如卷积层),这是一种常用堆叠调试工作流程。

函数式API

序贯模型易于使用,但适用范围非常有限:它只能表示具有单一输入和单一输出的模型,按顺序逐层进行处理。在实践中经常会遇到其他类型的模型,比如多输入模型(如图像及其元数据)、多输出模型(预测数据的不同方面)或具有非线性拓扑结构的模型。在这种情况下,可以使用函数式API构建模型。

简单示例

上面的两层堆叠模型,也可以用函数式API来实现。

import tensorflow.keras as keras

from tensorflow.keras import layers



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

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

outputs = layers.Dense(10, activation="relu")(features)

model = keras.Model(inputs=inputs, outputs=outputs)

接下来逐行解释。

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

这个inputs对象保存了关于模型将处理的数据的形状和数据类型的信息。这样的对象叫作符号张量symbolic tensor,它不包含任何实际数据,但编码了调用模型时实际张量数据的详细信息。

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

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

outputs = layers.Dense(10, activation="relu")(features)

model = keras.Model(inputs=inputs, outputs=outputs)

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

构建多输入多输出模型

与上述简单模型不同,大多数深度学习模型看起来不像列表,而像图。比如,模型可能有多个输入或多个输出。

假设要构建一个系统,按优先级对工单进行排序,并将工单转给相应的部门。这个模型有3个输入:

  • 工单标题(文本输入)
  • 工单的文本正文(文本输入)
  • 用户添加的标签(分类输入,假定为one-hot编码)

可以将文本输入编码为由1和0组成的数组,数组大小为vocabulary_size(11章将详细介绍文本编码方法)。

模型还有2个输出:

  • 工单的优先级分数,是介于0和1之间的标量(sigmoid输出)
  • 应处理工单的部门(对所有部门做softmax)

接下来利用函数式API构建这个模型。

import tensorflow.keras as keras

from tensorflow.keras import layers



vocabulary_size = 10000

num_tags = 100

num_departments = 4



#   定义模型输入

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 = layers.Concatenate()([title, text_body, tags])   # 通过拼接将输入特征组合成张量features

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)   # 定义模型输出





model = keras.Model(inputs=[title, text_body, tags], outputs=[priority, department])    # 通过指定输入和输出来创建模型

训练多输入多输出模型

这种模型的训练方式与序贯模型相同,都是对输入数据和输出数据组成的列表调用fit( )。这些数据列表的顺序应该与传入Model构造函数的inputs的顺序相同。

num_samples = 1280



#   虚构输入数据

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))

#   虚构目标数据

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_v2.RMSProp(),

    loss=["mean_square_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, title_data])

在有多个输入或输出的情况下,如果不想依赖输入输出顺序,也可以为Input对象和输出层指定名称,通过字典传递数据,如下所示:

model.compile(

    optimizer=rmsprop_v2.RMSProp(),

    loss={"priority": "mean_square_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( )将函数式模型绘制成图

补充:plot_model( )需要Graphviz和pydot这两个第三方包。需要先安装Graphviz再安装pydot。

在Anaconda下安装Graphviz应该使用:conda install python-graphviz

Anaconda下安装Graphviz - 知乎 (zhihu.com)

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

还可以将模型每一层的输入形状和输出形状添加到这张图中,这对调试很有帮助。

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

 

形状张量中的“?“应该是None,表示批量大小,也即该模型接收任意大小的张量。

获取层的连接方式,意味着可以查看并重复使用图中的节点(层调用)。模型属性model.layers给出了构成模型的层的列表。对于每一层,都可以查询layer.input和layer.output。

检索函数模式模型某一层的输入或输出:

model.layers

[<tensorflow.python.keras.engine.input_layer.InputLayer object at 0x00000202680EA988>, <tensorflow.python.keras.engine.input_layer.InputLayer object at 0x0000020268015AC8>, <tensorflow.python.keras.engine.input_layer.InputLayer object at 0x000002026813C708>, <tensorflow.python.keras.layers.merge.Concatenate object at 0x000002026814B6C8>, <tensorflow.python.keras.layers.core.Dense object at 0x000002026816C088>, <tensorflow.python.keras.layers.core.Dense object at 0x0000020268170DC8>, <tensorflow.python.keras.layers.core.Dense object at 0x000002025B90DC88>]

model.layers[3].input

[<tf.Tensor 'title:0' shape=(None, 10000) dtype=float32>,

<tf.Tensor 'text_body:0' shape=(None, 10000) dtype=float32>,

<tf.Tensor 'tags:0' shape=(None, 100) dtype=float32>]

model.layers[3].output

<tf.Tensor 'concatenate/Identity:0' shape=(None, 20100) dtype=float32>

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

例如,假设想对前一个模型增加一个输出——估算某个问题工单的解决时常,这是一种难度评分。实现方法是利用包含3个类别的分类层,这3个类别分别是“快速“、”中等“和”困难“。

无需从头开始重新创建和训练模型。可以从前一个模型的中间特征开始(这些中间特征是可以访问的)。

重复使用中间层的输出,创建一个新模型:

features = model.layers[4].output   # layer[4]是中间的Dense层

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类子类化。Model子类化的方法:

  • 在__init__( )方法中,定义模型将使用的层;
  • 在call()方法中,定义模型的前向传播,重复使用之前创建的层;
  • 将子类实例化,并在数据上调用,从而创建权重。

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

import tensorflow.keras as keras

import tensorflow.keras.layers as layers



class CustomerTicketModel(keras.Model):

    #   定义构造函数

    def __init__(self, num_departments):

        super.__init__()    # 调用父类的初始化方法

        #   在构造函数中定义子层

        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

定义好模型之后,就可以将模型实例化。注意,只有第一次在数据上调用模型时,模型才会创建权重。

model = CustomerTicketModel(num_departments=4)

priority, department = model(

    {"title": title_data},

    {"text_body": text_body_data},

    {"tags": tags_data}

)

编译和训练Model子类和序贯模型或函数式模型一样。

model.compile(

    optimizer=rmsprop_v2.RMSProp(),

    #   参数loss和metrics的结构必须与call()返回的内容完全匹配,在这个例子里是两个元素组成的列表

    loss=["mean_squared_error", "categorical_crossentropy"],

    metrics=[["mean_absolute_error"], ["accuracy"]]

)



model.fit(

    #   输入数据的结构必须与call()方法的输入完全一样,在这个例子里是一个字典,字典的键是"title","text_body"和"tags"

    {"title": title_data, "text_body": text_body_data, "tags": tags_data},

    #   目标数据的结构必须与call()方法返回的内容完全匹配,在这个例子里是两个元素组成的列表

    [priority_data, department_data],

    epochs=1

)



priority_preds, department_preds = model.predict({"title": title_data, "text_body": text_body_data, "tags": tags_data})

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

子类化模型不能做什么

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

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

混合使用不同的组件

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 Model1(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 = Model1()
 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值