用 Keras/TensorFlow 2.9 创建深度学习模型的方法总结


前言

Keras 中有 3 种方法可以创建深度学习模型:1. keras.Sequential 模型。 2.函数式 API。3.子类法 subclassing keras.Model。
因为可用的方法很多,灵活性大,实际使用时容易出错。
但是如果建模的目标,是能够表达任意结构的深度学习模型,并且能够看到模型的完整内部结构,那么在创建模型时,有 3 个简单的原则可以使用。


创建模型时的 3 个原则

  1. 使用函数式 functional API 的方法来创建模型,即 model = keras.Model(inputs=…)。
  2. 不要使用 Numpy 函数。
  3. 对个别的自定义层,使用子类法 subclassing keras.layers.Layer 来创建。

下面详细讨论这 3 个原则。


1. 使用函数式 API 的方法创建模型

使用函数式 API 创建的模型,能够看到完整的模型结构。而用子类法 subclassing keras.Model 创建的模型,则无法看到内部结构,因为它相当于是创建了一个黑盒子。至于用 keras.Sequential 创建的模型,因为是一个线性结构,无法用于表达复杂的模型。
所以绝大多数情况下,使用函数式 API 创建模型即可。

使用函数式 API 建模的形式为:model = keras.Model(inputs=…)。 一个例子如下。

inputs = keras.Input(shape=(608, 608, 3))

x = keras.layers.Conv2D(16, 3)(inputs)
x = keras.layers.BatchNormalization()(x)
outputs  = keras.layers.LeakyReLU()(x)

funtional_api_model = keras.Model(inputs=inputs, outputs=outputs, 
                                  name='demo_funtional_api_model')

2. 不要使用 Numpy 函数

用函数式 API 创建模型时,不要使用 Numpy 函数。如果要用到一些 Numpy 的功能,可以用 TF 的函数来代替,比如用 tf.reduce_max 代替 np.amax,用 tf.concat 代替 np.concatenate 等。
而不能使用 Numpy 函数的原因,则是因为:
Keras 默认以静态计算图的方式运行深度学习模型。在创建计算图时,要用到 KerasTensor,这是 TensorFlow 独有的一种张量类型,Numpy 并不知道如何处理 KerasTensor。
KerasTensor 也叫符号张量 symbolic tensor,它的第 0 维度大小为 None。如下图。
在这里插入图片描述
如果在建模时使用了 Numpy 函数,会出现以下报错:
"You are passing KerasTensor ..., an intermediate Keras symbolic input/output, to a TF API that does not allow registering custom dispatchers ... Other APIs cannot be called directly on symbolic Kerasinputs/outputs ..."
有时会出现另外一种报错:
"NotImplementedError: Cannot convert a symbolic tf.Tensor (Placeholder:0) to a numpy array ..."

3. 使用子类法 subclassing keras.layers.Layer 创建自定义层

创建模型时,优先使用 TF 的内置层,包括 keras.layers 和 tfa.layers 。比如使用 keras.layers.Reshape 层而非使用 tf.reshape,使用 tfa.layers.GELU 而非 tf.nn.gelu。使用 TF 内置层的好处是:便于对各层进行命名,并且用 keras.utils.plot_model 画出的结构图也更好看。

但在某些情况下,需要使用自定义的层。比较常见的 2 种情况如下。

3.1 把复杂的 block 封装到自定义层中

创建深度学习模型,就像是搭积木,会用到各种的积木块 block,比如 Conv2D–Batch Normalization–Mish 就是一个常用的 block。
如果把 block 做成函数,则可以在模型中看到 block 的完整结构。但是如果想隐藏 block 的内部结构,做成一个黑盒子,就可以用自定义层来实现。一个例子如下。

class DemoSubclassingLayer(keras.layers.Layer):
    """一个黑盒子形式的 block。仅用于演示 keras.layers.Layer 的用法。
	
	Attributes:
        conv_filters_1: 一个整数,是第 1 个卷积层的过滤器数量。
        conv_filters_2: 一个整数,是第 2 个卷积层的过滤器数量。
        dropout_rate: 一个浮点数,是 Dropout 层的 dropout 比例。
        conv_1: 是 block 中的第 1 个卷积层。
        conv_2: 是 block 中的第 2 个卷积层。
        batch_norm : 是 block 中的 BatchNormalization 层。
	"""
	
    def __init__(self, filters_1, filters_2, dropout_rate, **kwargs):
        super().__init__(**kwargs)
        self.conv_filters_1 = filters_1
        self.conv_filters_2 = filters_2
        self.dropout_rate = dropout_rate
        # 把所有包含可训练参数的层定义为属性。
        self.conv_1 = keras.layers.Conv2D(filters=self.conv_filters_1, kernel_size=3)
        self.conv_2 = keras.layers.Conv2D(filters=self.conv_filters_2, kernel_size=3)
        self.batch_norm = keras.layers.BatchNormalization()
    
    def call(self, inputs, training=None):
        # 在 call 部分定义网络的前向传播过程。
        x = self.conv_1(inputs)
        x = self.conv_2(x)
        
        # 在目前的深度学习模型中,Dropout 和 BatchNormalization,是仅有的 2 种需要用到 training 参数的层。training 是一个布尔值,被用来 	
        # 区分训练模式和推理模式 inference mode。Dropout 和 BatchNormalization 在训练模式下和推理模式下会有不同的表现 behaviour。
        x = self.batch_norm(x, training=training)
        
        x = tfa.activations.mish(x)  # mish,ReLU 等没有可训练参数,所以不需要在 __init__ 中设为属性。
        
        # Dropout 层没有可训练参数,所以不需要在 __init__ 中设为属性。
        x = keras.layers.Dropout(rate=self.dropout_rate)(x, training=training)  
        
        return x  # 必须把计算结果用 return 返回。
    
    def get_config(self):
        config = super().get_config()
        # 在 get_config 部分,把自定义的数值,字符串等加入到字典 config 中,而 TF 内置的函数和层不需要加进来。
        # 注意字典 config 的 key 应该是 __init__ 中的参数名称,而 config 的 value 则是该参数所对应的属性。
        config.update({
            'filters_1': self.conv_filters_1,
            'filters_2': self.conv_filters_2,
            'dropout_rate': self.dropout_rate,
        })
        # 必须要有下面的 return config 语句,否则后续加载模型时,无法得到当前类 DemoSubclassingLayer 的参数值。
        return config

使用自定义层时,主要用到 3 个方法:init(),call() 和 get_config()。另外 2 个方法 build() 和 from_config(),一般不太需要用到。

  1. init() 部分,必须把所有可训练的参数 parameters,定义为属性。
    可训练的参数有 2 个来源,第一个来源是一些有参数的层,包括 BatchNormalization, Conv2D 等。第二个来源是可训练的变量 tf.Variable(trainable=True)。
    这样做的目的,是把这些可训练的参数记录下来,在训练过程中 TensorFlow 会计算它们的梯度,并进行反向传播,不断更新这些参数。

    尤其要注意 2 点:
    1.1 如果模型中多次用到某一类的层,则该层必须被定义为多个属性。
    如上面例子中用到 2 次卷积层,就被定义为了 2 个属性 self.conv_1 和 self.conv_2 。
    1.2 没有参数的层,不需要设置为属性。如上面例子中的 mish、ReLU 等。

  2. call() 部分。实现网络的前向传播,也就是各种层的叠加。

  3. get_config() 部分,把自定义的数值,字符串等加入到字典 config 中,而 TF 内置的函数和层不需要加进来。
    get_config 主要是为了获取当前自定义层的参数值,并将和模型一起保存,即 serialization。

    如果之前已经把模型保存在硬盘上,那么加载模型时,可以用下面的方法。

    saved_model_path = 'saved_model.h5'  # 模型保存路径。
    
    # 把所有的自定义层,自定义的损失函数和自定义指标等,都放到 custom_objects 中。
    custom_objects = {'DemoSubclassingLayer': DemoSubclassingLayer}
    # 下面加载模型时,不需要提供 DemoSubclassingLayer 的参数值,因为这些参数值已经被 get_config() 自动保存在了硬盘上,
    # 并且 Keras 会自动把这些参数值提供给 DemoSubclassingLayer。
    saved_model = keras.models.load_model(saved_model_path, custom_objects=custom_objects)
    

在创建自定义层时,如果在 call() 部分创建了新的参数(例如创建了 Conv2D 等各种有参数的层,或是创建了 tf.Variable(Trainable=True…) 变量),而没有把它们作为属性放到 init 部分,在训练模型时,将会出现报错信息:
tf.function only supports singleton tf.Variables created on the first call. Make sure the tf.Variable is only created once or created outside tf.function.
有时是另外一种报错信息:
tf.function-decorated function tried to create variables on non-first call.
这两种报错都是同一个意思:即不要在 call() 部分创建可训练的参数。否则在训练时, 每个 epoch 都会调用 call() 一次,而 call() 部分的参数也被一次又一次地新建,导致反向传播无法对 call() 部分的参数进行更新。

如果用自定义层 DemoSubclassingLayer 创建模型,再用 plot_model 画出结构:

inputs = keras.Input(shape=(608, 608, 3))
outputs = DemoSubclassingLayer(filters_1=8, filters_2=32, dropout_rate=0.3, name='black_box_layer')(inputs)
demo_model_subclassing_layer = keras.Model(inputs=inputs, outputs=outputs, name='demo_model_subclassing_layer')

keras.utils.plot_model(demo_model_subclassing_layer, show_shapes=True, to_file='demo_model_subclassing_layer.png')

得到的结构图如下。可以看到自定义层 DemoSubclassingLayer 完全是一个黑盒子,虽然其中有多个层,但是所有的层结构都是看不到的。
在这里插入图片描述

3.2 把 eager tensor 封装到自定义层中

创建模型时,如果用到 eager tensor(例如 tf.range() 等情况),应该将其封装到 keras.layers.Layer 里面。否则将无法生成模型结构,也无法用 keras.utils.plot_model 画出结构图,并且在建模时产生报错:
AttributeError: 'tensorflow.python.framework.ops.EagerTensor' object has no attribute '_keras_history'

举例来说:在 Vision Transformer 模型中,需要用到 position encoding,通常会使用 tf.range() 来生成 position encoding。此时,这个 tf.range() 就会生成一个 eager tensor,需要把 tf.range() 封装到 keras.layers.Layer 的 call 部分。示例代码如下:

    # 这里仅展示自定义层 PositionEncoding 的 call 部分。
    def call(self, inputs):
        # positions 形状为 (41209,)。
        positions = tf.range(self.patches_quantity)
        # positions 形状为 (1, 41209)。必须用 tf.newaxis 转为 2D 张量,后续给 Embedding 层才能得到 3D 张量。
        position_encoding = positions[tf.newaxis, :]
        # embedded_positions 形状为 (1, 41209, output_dim)。
        embedded_positions = self.position_embeddings(position_encoding)

        return embedded_positions

以上是 2022-06-04 的内容。
下面为 2022-08-27 新增加的部分。

4. 特殊情况:在 callback 中创建模型

还有一种特殊情况,是在 keras.callbacks.Callback 中创建模型。这种情况的应用场景是:
在物体探测 object detection 任务中,训练模型本身在图模式下进行;但是评价 AP 指标时,需要在 eager 模式下进行。此时就可以在 callback 中单独创建一个模型,专门用于评价 AP 指标。也因此可以把 callback 中的这个模型叫做评价模型。

创建评价模型,主要有如下 3 个步骤:

  1. 在训练开始时,创建一个新的评价模型。
  2. 需要评价指标时,提取当前正在训练模型的权重,然后把该权重赋给评价模型。
  3. 用评价模型计算 AP 指标。

这个 keras.callbacks.Callback 的示例代码如下,3 个大步骤的子步骤 1.1, 1.2, 1.3, 2.1 等等,均写在代码中。

class SaveModelHighestAP(keras.callbacks.Callback):
    """指标 AP 达到最高时,保存模型。

    因为计算 AP 指标花的时间很长,并且 AP 指标的计算图太大,个人 PC 上无法构建计算图,所以单独创
    建一个评价模型,在运行一定 epochs 次数之后,才计算一次 AP 指标,并且是在 eager 模式下计算。
    而训练模型本身则始终在图模式下进行,这样既能保证模型运行速度,又能实现 AP 指标的计算。
    具体 4 个操作如下:
    1. 先创建一个专用的模型 evaluation_ap,用于计算指标 AP。
    2. 当训练次数 epochs 满足 2 个条件时,把当前训练模型 self.model 的权重提取出来,可以
        用 get_weights。
        epochs 需要满足的 2 个条件是:
        a. epochs ≥ epochs_warm_up,epochs_warm_up 是一个整数,表示经过一定数量的训
        练之后才保存权重。
        b. epochs % skip_epochs == 0,表示每经过 skip_epochs 个 epochs 之后,
        提取权重。如果设置 skip_epochs = 1,则表示每个 epoch 都会提取权重。
    3. 把提取到的权重,用 set_weights 加载到指标测试模型 evaluation_ap 上,然后用模型
        evaluation_ap 来测试指标。
    4. 如果指标 AP 大于最好的 AP 记录,且提供了保存模型的路径,则把该模型保存为
        highest_ap_model。

    Attributes:
        evaluation_data: 用来计算 AP 的输入数据,一般应该使用验证集数据。
        highest_ap_model_path: 一个字符串,是保存最高 AP 指标模型的路径。如果为 None,
            则不保存 AP 指标最高的模型。
        evaluation_model: 一个 Keras 模型,专门用于计算 AP 指标。
        coco_average_precision: 是自定义类 MeanAveragePrecision 的个体 instance,用于计算 AP 指标。
        epochs_warm_up: 一个整数,表示从多少个 epochs 训练之后,开始计算 AP 指标。
        skip_epochs: 一个整数,表示每经过多少个 epochs,计算一次 AP 指标。
        ap_record: 一个列表,用于记录所有的 AP 指标。
    """

    def __init__(self, evaluation_data, highest_ap_model_path=None,
                 epochs_warm_up=100, skip_epochs=50):
        super().__init__()
        self.evaluation_data = evaluation_data
        self.highest_ap_model_path = highest_ap_model_path
        self.coco_average_precision = MeanAveragePrecision()
        self.epochs_warm_up = epochs_warm_up
        self.skip_epochs = skip_epochs

    # 1. 在训练开始时,创建一个新的评价模型。
    # noinspection PyUnusedLocal, PyAttributeOutsideInit
    def on_train_begin(self, logs=None):
        """因为每一次新的训练开始时,可能使用了不同的损失函数和超参,所以重新创建评价模型,
        并对其进行编译。还要把 ap_record 清零,从头开始一次新的记录。
        """
        custom_objects = {'MishActivation': MishActivation,
                          ...  # 1.1 把各种自定义的类加进来。
                          'PositionEncoding': PositionEncoding}
        # 1.2 使用 with 语句,以自定义对象作为 context manager。
        with keras.utils.custom_object_scope(custom_objects): 
            # 1.3 获取当前正在训练模型的各种参数值 config。
            config = self.model.get_config()  # 当前正在训练的模型,可以用 self.model 获得。
            # 1.4 创建评价模型。直接使用当前正在训练模型的 config。
            self.evaluation_model = keras.Model.from_config(config)

        # 1.5 编译模型。
        # 虽然损失函数是自定义的,只要它不是类 class,就可以直接用 self.model.loss
        # 来调用这个损失函数,并且会把它的参数值自动带过来。而其它自定义的类,则必须放到
        # custom_objects 中。
        self.evaluation_model.compile(
            run_eagerly=True,  # 1.6 在 eager 模式下计算 AP 指标。
            metrics=[self.coco_average_precision],
            loss=self.model.loss,
            optimizer=self.model.optimizer)  # self.model.optimizer 是当前训练模型的优化器。

        self.ap_record = []  # 记录 AP 指标。每次新的训练开始时,都要进行一次清零。

    # noinspection PyUnusedLocal
    def on_epoch_end(self, epoch, logs=None):
        """在一个 epoch 结束之后,计算 AP,并保存最高 AP 对应的模型。"""        
        # 2. 需要评价指标时,提取当前正在训练模型的权重,然后把该权重赋给评价模型。
        # 下面使用 (epoch + 1), 是因为 epoch 是从 0 开始计算的,但实际上 
        # epoch = 0 时,就已经是第一次迭代了。
        if tf.logical_and(
            ((epoch + 1) >= self.epochs_warm_up),
                ((epoch + 1 - self.epochs_warm_up) % self.skip_epochs == 0)):

            # 2.1 先获取当前训练模型 self.model 的权重。
            current_weights = self.model.get_weights()
            # 2.2 把当前模型的权重,应用到评价模型 evaluation_ap 上。
            self.evaluation_model.set_weights(current_weights)

            # 2.3 对 AP 指标进行复位。
            # 因为没有对 self.evaluation_model 进行训练,所以 AP 指标不会在每个 epoch 之后
            # 自动复位,指标的状态量还存储着上一个 epoch 的状态,必须手动用
            #  reset_state() 对指标进行复位,否则会发生计算错误。
            self.coco_average_precision.reset_state()

            # 3. 用评价模型计算 AP 指标。
            evaluation = self.evaluation_model.evaluate(self.evaluation_data)
            # evaluation 是一个列表,包括 1 个损失值和 1 个 AP。
            current_ap = evaluation[-1]

            if not self.ap_record:
                max_ap_record = 0  # 列表为空时,无法使用 amax 函数,所以直接赋值为 0.
            else:
                max_ap_record = np.amax(self.ap_record)

            print(f'\nChecking the AP at epoch {epoch + 1}. The highest AP is: '
                  f'{max_ap_record:.2%}')

            if current_ap > max_ap_record:
                print(f'The highest AP changed to: {current_ap:.2%}')
                if self.highest_ap_model_path is not None:
                    self.model.save(self.highest_ap_model_path)  # 保存 AP 最高的模型。
                    print(f'The highest AP model is saved.')

            # 在上面取得 max_ap_record 之后,才把指标加到列表 ap_record 中去,
            # 否则程序逻辑会出错。
            self.ap_record.append(current_ap)

————————本文结束————————

  • 8
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值