Tensorflow使用subclassing构建模型时为什么必须需要定义call方法

1. Keras模型构建的三种方式

在使用Tensorflow.keras构建深度学习模型时,可以有以下三种方式:
• tf.keras.Sequential 方式
• Keras Functional API 方式
• Keras Model Subclassing 方式
其中tf.keras.Sequential 方式是最为常用的,而Keras Functional API 方式使用的频率次之,Keras Model Subclassing 方式是使用频率最少的一种方式。
但是tf.keras.Sequential 方式最简便,主要适用于较为基本的深度学习的模型构建,例如卷积神经网络。当进入以Transformer为基础的大模型阶段,继续使用tf.keras.Sequential 方式已经很难实现相应的模型构建需求,需要采用Keras Model Subclassing这种方式了。如何使用这种方式进行模型定义,很多人还存在比较大的误区,下面详细说明这种方式的使用要点。

2. subclassing建模的方式

2.1 一个完整的采用subclassing的模型实例

import tensorflow as tf
import matplotlib.pyplot as plt
import numpy as np

class SimpleDenseNet(tf.keras.Model):
    def __init__(self):
        super(SimpleDenseNet, self).__init__()
        # 定义层
        self.dense1 = tf.keras.layers.Dense(64, activation='relu')
        self.dense2 = tf.keras.layers.Dense(32, activation='relu')
        self.dense3 = tf.keras.layers.Dense(10)  # 假设是10个类别的分类问题
 
    def call(self, inputs):
        # 定义前向传播逻辑
        x = self.dense1(inputs)
        x = self.dense2(x)
        return self.dense3(x)

model = SimpleDenseNet()
 
# 准备数据(这里仅为示例)
x_train = np.random.random((100, 784))  # 假设输入特征维度为784
y_train = np.random.randint(0, 10, (100,))  # 假设有10个类别
 
# 编译模型(如果需要自定义训练过程)
model.compile(optimizer='adam', loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), metrics=['accuracy'])
 
# 训练模型
model_trained = model.fit(x_train, y_train, validation_data=(x_train,y_train),epochs=10)
accuracy = model_trained.history['accuracy']
val_accuracy = model_trained.history['val_accuracy']
plt.plot(model_trained.history['accuracy'])
plt.plot(model_trained.history['val_accuracy'])
plt.xlabel('Epoch')
plt.ylabel('Accuracy')
plt.legend(['accuacy','val_accuracy'])
plt.show()

这里是一个完整的使用subclassing建模的实例。
在这里插入图片描述
这个图是模型构建的完整过程,其中I部分是类的声明,II部分是类的构造函数部分,III部分是类的call方法部分。

2.2 subclassing建模的主要步骤

上面这个实例完整的展示了通过subclassing建模的方式,也就是主要分为3个主要步骤:
(1) 通过类的声明,这里必须是继承自tf.keras.Model类
(2) 模型类的构造函数部分,在这一部分中主要将使用到的layer作为模型类的属性
(3) 模型类的call方法,在这里可以看到除了使用属性中定义的layer之外还使用了input,这里input实际上代表了输入模型的数据,在这里实际上通过数据的输入输出关系实现了层与层之间的拓扑关系。另外需要说明一点,在构造函数中,虽然每一层都定义为模型类的属性,但是层与层之间并未形成关联关系,也就是各个层并没有组织起来,而call方法中通过数据输入输出关系将各个层组织起来,完成了模型中层与层之间拓扑关系的定义。从这里可以看出通过subclassing方式可以灵活定义模型中层与层之间的拓扑关系,而不再局限于线性串联关系。

2.3 关于subclassing构建模型的几个疑问

(1) 在模型类中定义了call方法,但是却看不到在代码中调用这个call方法,那么这个call方法有没有真正被使用,如果被使用了那又是通过什么方式进行使用的?
(2) 这里的call方法肯定不是魔术方法,那么可不可以随便命名,是不是不一定要叫做call?
(3) 在这个实例中,模型类并未定义input属性,那为什么call方法中又是怎么传递进input的呢?

3. 关于subclassing定义模型的进一步解释

要想真正搞清楚上面的那3个问题,需要对keras中使用subclassing方式定义模型进行进一步的剖析,才能真正把这种方式背后的原理与机制搞明白。
从I部分的类模型声明过程中,模型类是继承自tf.keras.Model类,这些隐藏模型定义过程中的问题应该都与这个继承有关联。下面首先就从剖析这个继承关系开始。

3.1 tf.keras.Model类

tf.keras.Model类的定义是在’\tensorflow\python\keras\engine\training.py’中定义的,其声明的过程如下图所示:
在这里插入图片描述
在这个类中定义了call方法,如下图所示
在这里插入图片描述
需要注意的是,在这个类中并没有定义__call__方法,至少从目前来说Model类
来说,不是可调用的,而采用subclassing构建的模型类中也没有定义__call__方法,因此至少从目前来讲采用subclassing构建的模型类不是可调用的,也就是没法实现对call方法自动使用。而在Model类中的call方法的解释中,表示call是通过__call__方法间接实现了调用。
要搞清楚这个问题,就要看一下:Model类继承自base_layer.Layer, version_utils.ModelVersionSelector这2个类。
下面这个截图是base_layer.Layer类的声明
在这里插入图片描述
下面这个截图是Layer类中call方法的定义
在这里插入图片描述
从这个定义过程中可以看出,在call方法的定义中,实际上没有对input做任何改变。
下面是Layer类中__call__方法的定义

def __call__(self, *args, **kwargs):
    """Wraps `call`, applying pre- and post-processing steps.

    Args:
      *args: Positional arguments to be passed to `self.call`.
      **kwargs: Keyword arguments to be passed to `self.call`.

    Returns:
      Output tensor(s).

    Note:
      - The following optional keyword arguments are reserved for specific uses:
        * `training`: Boolean scalar tensor of Python boolean indicating
          whether the `call` is meant for training or inference.
        * `mask`: Boolean input mask.
      - If the layer's `call` method takes a `mask` argument (as some Keras
        layers do), its default value will be set to the mask generated
        for `inputs` by the previous layer (if `input` did come from
        a layer that generated a corresponding mask, i.e. if it came from
        a Keras layer with masking support.
      - If the layer is not built, the method will call `build`.

    Raises:
      ValueError: if the layer's `call` method returns None (an invalid value).
      RuntimeError: if `super().__init__()` was not called in the constructor.
    """
    if not hasattr(self, '_thread_local'):
      raise RuntimeError(
          'You must call `super().__init__()` in the layer constructor.')

    # `inputs` (the first arg in the method spec) is special cased in
    # layer call due to historical reasons.
    # This special casing currently takes the form of:
    # - 'inputs' must be explicitly passed. A layer cannot have zero arguments,
    #   and inputs cannot have been provided via the default value of a kwarg.
    # - numpy/scalar values in `inputs` get converted to tensors
    # - implicit masks / mask metadata are only collected from 'inputs`
    # - Layers are built using shape info from 'inputs' only
    # - input_spec compatibility is only checked against `inputs`
    # - mixed precision casting (autocast) is only applied to `inputs`,
    #   not to any other argument.
    # - setting the SavedModel saving spec.
    inputs, args, kwargs = self._split_out_first_arg(args, kwargs)
    input_list = nest.flatten(inputs)

    # Functional Model construction mode is invoked when `Layer`s are called on
    # symbolic `KerasTensor`s, i.e.:
    # >> inputs = tf.keras.Input(10)
    # >> outputs = MyLayer()(inputs)  # Functional construction mode.
    # >> model = tf.keras.Model(inputs, outputs)
    if _in_functional_construction_mode(self, inputs, args, kwargs, input_list):
      return self._functional_construction_call(inputs, args, kwargs,
                                                input_list)

    # Maintains info about the `Layer.call` stack.
    call_context = base_layer_utils.call_context()

    # Accept NumPy and scalar inputs by converting to Tensors.
    if any(isinstance(x, (
        np_arrays.ndarray, np.ndarray, float, int)) for x in input_list):
      inputs = nest.map_structure(_convert_numpy_or_python_types, inputs)
      input_list = nest.flatten(inputs)

    # Handle `mask` propagation from previous layer to current layer. Masks can
    # be propagated explicitly via the `mask` argument, or implicitly via
    # setting the `_keras_mask` attribute on the inputs to a Layer. Masks passed
    # explicitly take priority.
    input_masks, mask_is_implicit = self._get_input_masks(
        inputs, input_list, args, kwargs)
    if self._expects_mask_arg and mask_is_implicit:
      kwargs['mask'] = input_masks

    # Training mode for `Layer.call` is set via (in order of priority):
    # (1) The `training` argument passed to this `Layer.call`, if it is not None
    # (2) The training mode of an outer `Layer.call`.
    # (3) The default mode set by `tf.keras.backend.set_learning_phase` (if set)
    # (4) Any non-None default value for `training` specified in the call
    #  signature
    # (5) False (treating the layer as if it's in inference)
    args, kwargs, training_mode = self._set_training_mode(
        args, kwargs, call_context)

    # Losses are cleared for all sublayers on the outermost `Layer.call`.
    # Losses are not cleared on inner `Layer.call`s, because sublayers can be
    # called multiple times.
    if not call_context.in_call:
      self._clear_losses()

    eager = context.executing_eagerly()
    with call_context.enter(
        layer=self,
        inputs=inputs,
        build_graph=not eager,
        training=training_mode):

      input_spec.assert_input_compatibility(self.input_spec, inputs, self.name)
      if eager:
        call_fn = self.call
        name_scope = self._name
      else:
        name_scope = self._name_scope()  # Avoid autoincrementing.  # pylint: disable=not-callable
        call_fn = self._autographed_call()

      with ops.name_scope_v2(name_scope):
        if not self.built:
          self._maybe_build(inputs)

        if self._autocast:
          inputs = self._maybe_cast_inputs(inputs, input_list)

        with autocast_variable.enable_auto_cast_variables(
            self._compute_dtype_object):
          outputs = call_fn(inputs, *args, **kwargs)

        if self._activity_regularizer:
          self._handle_activity_regularization(inputs, outputs)
        if self._supports_masking:
          self._set_mask_metadata(inputs, outputs, input_masks, not eager)
        if self._saved_model_inputs_spec is None:
          self._set_save_spec(inputs)

        return outputs

从下面这个截图中可以看出,__call__方法实际上调用了call方法
在这里插入图片描述
至此call方法实际过程已经比较清晰了,用户自己定义的模型类User_Model继承自\tensorflow\python\keras\engine\training.py中的Model类,这样使得User_Model中定义的call方法覆盖了Model类中的方法,而training.py中的Model类继承自base_layer.Layer,这样又使得training.py中的Model类中的call方法覆盖了base_layer.Layer中的call方法,而base_layer.Layer中的__call__方法又调用了call方法,这使得在使用User_Model时,通过base_layer.Layer中的__call__方法调用了User_Model中的call方法,而base_layer.Layer定义了__call__方法,User_Model又通过training.py中的Model类继承了base_layer.py中的Layer类,所以User_Model也是可调用的了。
在这里插入图片描述
通过这个图,使用subclassing方法构建模型类的过程中,call方法在定义过程中是通过怎样的途径发挥作用的,就变得比较清晰了。

3.2 关于疑问的解答

3.2 关于疑问的解答
在2.3节中提出了几个问题,通过上面的剖析,这3个问题的答案就很容易得到了:
(1) 在模型类中定义了call方法,但是却看不到在代码中调用这个call方法,那么这个call方法有没有真正被使用,如果被使用了那又是通过什么方式进行使用的?
Tensorflow中base_layer.py的Layer类实现__call__方法对call方法的调用,然后通过继承关系,使得User_Model定义的call方法可以被__call__方法进行调用,并且因为base_layer.py的Layer类中定义了__call__方法,使得User_Model也是可调用的了。通过下面这个方式
User_Object = User_Model
User_Object(args)
使得User_Model中定义的call方法得到执行
(2) 这里的call方法肯定不是魔术方法,那么可不可以随便命名,是不是不一定要叫做call?
虽然call方法不是魔术方法,但是base_layer.py的Layer类中在__call__方法已经指定了call名称,因此这个名称是不能随意命名的。
(3) 在这个实例中,模型类并未定义input属性,那为什么call方法中又是怎么传递进input的呢?
这里的input就是User_Object(args)中的args,在Python中并没有规定类中的形参一定是类中的属性,因此这里args也可以不是类中的属性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值