开发者指南 / 保存、序列化和导出模型

保存、序列化和导出模型

作者:Neel Kovelamudi,Francois Chollet
创建日期 2023/06/14
上次修改日期 2023/06/30
描述:保存、序列化和导出模型的完整指南。

在 Colab 中查看 GitHub 源代码


简介

Keras 模型包含多个组件

  • 架构或配置,指定模型包含哪些层以及它们是如何连接的。
  • 一组权重值(“模型的状态”)。
  • 优化器(通过编译模型定义)。
  • 一组损失和指标(通过编译模型定义)。

Keras API 将所有这些部分一起保存到统一的格式中,并以 .keras 扩展名标记。这是一个包含以下内容的 zip 归档文件

  • 基于 JSON 的配置文件(config.json):模型、层和其他可跟踪对象的配置记录。
  • 基于 H5 的状态文件,例如 model.weights.h5(对于整个模型),其中包含层及其权重的目录键。
  • 一个 JSON 格式的元数据文件,存储诸如当前 Keras 版本等信息。

让我们看看它是如何工作的。


如何保存和加载模型

如果您只有 10 秒钟时间阅读本指南,以下是要了解的内容。

保存 Keras 模型

model = ...  # Get model (Sequential, Functional Model, or Model subclass)
model.save('path/to/location.keras')  # The file needs to end with the .keras extension

加载模型

model = keras.models.load_model('path/to/location.keras')

现在,让我们详细了解一下。


设置

import numpy as np
import keras
from keras import ops

保存

本节介绍如何将整个模型保存到单个文件中。该文件将包含

  • 模型的架构/配置
  • 模型的权重值(在训练期间学习到的)
  • 模型的编译信息(如果调用了 compile()
  • 优化器及其状态(如果有)(这使您可以从中断的地方重新开始训练)

API

您可以使用 model.save()keras.models.save_model()(两者等效)保存模型。您可以使用 keras.models.load_model() 加载它。

Keras 3 中唯一支持的格式是“Keras v3”格式,它使用 .keras 扩展名。

示例

def get_model():
    # Create a simple model.
    inputs = keras.Input(shape=(32,))
    outputs = keras.layers.Dense(1)(inputs)
    model = keras.Model(inputs, outputs)
    model.compile(optimizer=keras.optimizers.Adam(), loss="mean_squared_error")
    return model


model = get_model()

# Train the model.
test_input = np.random.random((128, 32))
test_target = np.random.random((128, 1))
model.fit(test_input, test_target)

# Calling `save('my_model.keras')` creates a zip archive `my_model.keras`.
model.save("my_model.keras")

# It can be used to reconstruct the model identically.
reconstructed_model = keras.models.load_model("my_model.keras")

# Let's check:
np.testing.assert_allclose(
    model.predict(test_input), reconstructed_model.predict(test_input)
)
 4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 0.4232  
 4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 281us/step
 4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 373us/step

自定义对象

本节介绍在 Keras 保存和重新加载中处理自定义层、函数和模型的基本工作流程。

保存包含自定义对象的模型(例如子类化的层)时,**必须**在对象类上定义 get_config() 方法。如果传递给自定义对象构造函数(__init__() 方法)的参数不是 Python 对象(除整数、字符串等基本类型之外的任何内容),那么**也必须**在 from_config() 类方法中显式地反序列化这些参数。

像这样

class CustomLayer(keras.layers.Layer):
    def __init__(self, sublayer, **kwargs):
        super().__init__(**kwargs)
        self.sublayer = sublayer

    def call(self, x):
        return self.sublayer(x)

    def get_config(self):
        base_config = super().get_config()
        config = {
            "sublayer": keras.saving.serialize_keras_object(self.sublayer),
        }
        return {**base_config, **config}

    @classmethod
    def from_config(cls, config):
        sublayer_config = config.pop("sublayer")
        sublayer = keras.saving.deserialize_keras_object(sublayer_config)
        return cls(sublayer, **config)

有关更多详细信息和示例,请参阅定义配置方法部分

保存的 .keras 文件很轻量级,不存储自定义对象的 Python 代码。因此,要重新加载模型,load_model 需要通过以下方法之一访问任何使用的自定义对象的定义

  1. 注册自定义对象(首选)
  2. 加载时直接传递自定义对象,或
  3. 使用自定义对象作用域

以下是每个工作流程的示例

注册自定义对象(首选

这是首选方法,因为自定义对象注册极大地简化了保存和加载代码。将 @keras.saving.register_keras_serializable 装饰器添加到自定义对象的类定义中,会在主列表中全局注册该对象,使 Keras 能够在加载模型时识别该对象。

让我们创建一个涉及自定义层和自定义激活函数的自定义模型来演示这一点。

示例

# Clear all previously registered custom objects
keras.saving.get_custom_objects().clear()


# Upon registration, you can optionally specify a package or a name.
# If left blank, the package defaults to `Custom` and the name defaults to
# the class name.
@keras.saving.register_keras_serializable(package="MyLayers")
class CustomLayer(keras.layers.Layer):
    def __init__(self, factor):
        super().__init__()
        self.factor = factor

    def call(self, x):
        return x * self.factor

    def get_config(self):
        return {"factor": self.factor}


@keras.saving.register_keras_serializable(package="my_package", name="custom_fn")
def custom_fn(x):
    return x**2


# Create the model.
def get_model():
    inputs = keras.Input(shape=(4,))
    mid = CustomLayer(0.5)(inputs)
    outputs = keras.layers.Dense(1, activation=custom_fn)(mid)
    model = keras.Model(inputs, outputs)
    model.compile(optimizer="rmsprop", loss="mean_squared_error")
    return model


# Train the model.
def train_model(model):
    input = np.random.random((4, 4))
    target = np.random.random((4, 1))
    model.fit(input, target)
    return model


test_input = np.random.random((4, 4))
test_target = np.random.random((4, 1))

model = get_model()
model = train_model(model)
model.save("custom_model.keras")

# Now, we can simply load without worrying about our custom objects.
reconstructed_model = keras.models.load_model("custom_model.keras")

# Let's check:
np.testing.assert_allclose(
    model.predict(test_input), reconstructed_model.predict(test_input)
)
 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 46ms/step - loss: 0.2571
 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step
 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step

将自定义对象传递给 load_model()

model = get_model()
model = train_model(model)

# Calling `save('my_model.keras')` creates a zip archive `my_model.keras`.
model.save("custom_model.keras")

# Upon loading, pass a dict containing the custom objects used in the
# `custom_objects` argument of `keras.models.load_model()`.
reconstructed_model = keras.models.load_model(
    "custom_model.keras",
    custom_objects={"CustomLayer": CustomLayer, "custom_fn": custom_fn},
)

# Let's check:
np.testing.assert_allclose(
    model.predict(test_input), reconstructed_model.predict(test_input)
)
 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 37ms/step - loss: 0.0535
 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step
 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 12ms/step

使用自定义对象作用域

自定义对象作用域内的任何代码都将能够识别传递给作用域参数的自定义对象。因此,在作用域内加载模型将允许加载我们的自定义对象。

示例

model = get_model()
model = train_model(model)
model.save("custom_model.keras")

# Pass the custom objects dictionary to a custom object scope and place
# the `keras.models.load_model()` call within the scope.
custom_objects = {"CustomLayer": CustomLayer, "custom_fn": custom_fn}

with keras.saving.custom_object_scope(custom_objects):
    reconstructed_model = keras.models.load_model("custom_model.keras")

# Let's check:
np.testing.assert_allclose(
    model.predict(test_input), reconstructed_model.predict(test_input)
)
 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 40ms/step - loss: 0.0868
 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step
 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step

模型序列化

本节介绍仅保存模型的配置,而不保存其状态。模型的配置(或架构)指定模型包含哪些层以及这些层是如何连接的。如果您拥有模型的配置,则可以使用新初始化的状态(没有权重或编译信息)创建模型。

API

以下序列化 API 可用

  • keras.models.clone_model(model):创建模型的(随机初始化的)副本。
  • get_config()cls.from_config():分别检索层或模型的配置,并从其配置重新创建模型实例。
  • keras.models.model_to_json()keras.models.model_from_json():类似,但作为 JSON 字符串。
  • keras.saving.serialize_keras_object():检索任何任意 Keras 对象的配置。
  • keras.saving.deserialize_keras_object():从其配置重新创建对象实例。

内存中模型克隆

您可以通过 keras.models.clone_model() 对模型进行内存中克隆。这等效于获取配置然后从其配置重新创建模型(因此它不保留编译信息或层权重值)。

示例

new_model = keras.models.clone_model(model)

get_config()from_config()

调用 model.get_config()layer.get_config() 将分别返回一个包含模型或层配置的 Python 字典。您应该定义 get_config() 以包含模型或层的 __init__() 方法所需的的参数。在加载时,from_config(config) 方法将使用这些参数调用 __init__() 以重建模型或层。

层示例

layer = keras.layers.Dense(3, activation="relu")
layer_config = layer.get_config()
print(layer_config)
{'name': 'dense_4', 'trainable': True, 'dtype': 'float32', 'units': 3, 'activation': 'relu', 'use_bias': True, 'kernel_initializer': {'module': 'keras.src.initializers.random_initializers', 'class_name': 'GlorotUniform', 'config': {'seed': None}, 'registered_name': 'GlorotUniform'}, 'bias_initializer': {'module': 'keras.src.initializers.constant_initializers', 'class_name': 'Zeros', 'config': {}, 'registered_name': 'Zeros'}, 'kernel_regularizer': None, 'bias_regularizer': None, 'kernel_constraint': None, 'bias_constraint': None}

现在让我们使用 from_config() 方法重建层

new_layer = keras.layers.Dense.from_config(layer_config)

顺序模型示例

model = keras.Sequential([keras.Input((32,)), keras.layers.Dense(1)])
config = model.get_config()
new_model = keras.Sequential.from_config(config)

函数式模型示例

inputs = keras.Input((32,))
outputs = keras.layers.Dense(1)(inputs)
model = keras.Model(inputs, outputs)
config = model.get_config()
new_model = keras.Model.from_config(config)

to_json()keras.models.model_from_json()

这类似于 get_config / from_config,除了它将模型转换为 JSON 字符串,然后可以在没有原始模型类的情况下加载它。它也特定于模型,不适用于层。

示例

model = keras.Sequential([keras.Input((32,)), keras.layers.Dense(1)])
json_config = model.to_json()
new_model = keras.models.model_from_json(json_config)

任意对象序列化和反序列化

keras.saving.serialize_keras_object()keras.saving.deserialize_keras_object() API 是通用 API,可用于序列化或反序列化任何 Keras 对象和任何自定义对象。它是保存模型架构的基础,并且位于 keras 中所有 serialize()/deserialize() 调用的背后。

示例:

my_reg = keras.regularizers.L1(0.005)
config = keras.saving.serialize_keras_object(my_reg)
print(config)
{'module': 'keras.src.regularizers.regularizers', 'class_name': 'L1', 'config': {'l1': 0.004999999888241291}, 'registered_name': 'L1'}

请注意序列化格式包含所有必要的 信息以进行正确的重建

  • module 包含对象来自的 Keras 模块或其他识别模块的名称。
  • class_name 包含对象类的名称。
  • config 包含重建对象所需的所有信息
  • 自定义对象的 registered_name。请参阅此处

现在我们可以重建正则化器了。

new_reg = keras.saving.deserialize_keras_object(config)

模型权重保存

您可以选择仅保存和加载模型的权重。如果以下情况适用,这将很有用:

  • 您只需要模型进行推理:在这种情况下,您不需要重新开始训练,因此不需要编译信息或优化器状态。
  • 您正在进行迁移学习:在这种情况下,您将训练一个新的模型,重用先前模型的状态,因此不需要先前模型的编译信息。

用于内存中权重传输的 API

可以使用 get_weights()set_weights() 在不同的对象之间复制权重

  • keras.layers.Layer.get_weights():返回权重值的 NumPy 数组列表。
  • keras.layers.Layer.set_weights(weights):将模型权重设置为提供的值(作为 NumPy 数组)。

示例

在内存中将权重从一层传输到另一层

def create_layer():
    layer = keras.layers.Dense(64, activation="relu", name="dense_2")
    layer.build((None, 784))
    return layer


layer_1 = create_layer()
layer_2 = create_layer()

# Copy weights from layer 1 to layer 2
layer_2.set_weights(layer_1.get_weights())

在内存中将权重从一个模型传输到另一个具有兼容架构的模型

# Create a simple functional model
inputs = keras.Input(shape=(784,), name="digits")
x = keras.layers.Dense(64, activation="relu", name="dense_1")(inputs)
x = keras.layers.Dense(64, activation="relu", name="dense_2")(x)
outputs = keras.layers.Dense(10, name="predictions")(x)
functional_model = keras.Model(inputs=inputs, outputs=outputs, name="3_layer_mlp")


# Define a subclassed model with the same architecture
class SubclassedModel(keras.Model):
    def __init__(self, output_dim, name=None):
        super().__init__(name=name)
        self.output_dim = output_dim
        self.dense_1 = keras.layers.Dense(64, activation="relu", name="dense_1")
        self.dense_2 = keras.layers.Dense(64, activation="relu", name="dense_2")
        self.dense_3 = keras.layers.Dense(output_dim, name="predictions")

    def call(self, inputs):
        x = self.dense_1(inputs)
        x = self.dense_2(x)
        x = self.dense_3(x)
        return x

    def get_config(self):
        return {"output_dim": self.output_dim, "name": self.name}


subclassed_model = SubclassedModel(10)
# Call the subclassed model once to create the weights.
subclassed_model(np.ones((1, 784)))

# Copy weights from functional_model to subclassed_model.
subclassed_model.set_weights(functional_model.get_weights())

assert len(functional_model.weights) == len(subclassed_model.weights)
for a, b in zip(functional_model.weights, subclassed_model.weights):
    np.testing.assert_allclose(a.numpy(), b.numpy())

无状态层的情况

因为无状态层不会更改权重的顺序或数量,所以即使存在额外/缺少的无状态层,模型也可以具有兼容的架构。

inputs = keras.Input(shape=(784,), name="digits")
x = keras.layers.Dense(64, activation="relu", name="dense_1")(inputs)
x = keras.layers.Dense(64, activation="relu", name="dense_2")(x)
outputs = keras.layers.Dense(10, name="predictions")(x)
functional_model = keras.Model(inputs=inputs, outputs=outputs, name="3_layer_mlp")

inputs = keras.Input(shape=(784,), name="digits")
x = keras.layers.Dense(64, activation="relu", name="dense_1")(inputs)
x = keras.layers.Dense(64, activation="relu", name="dense_2")(x)

# Add a dropout layer, which does not contain any weights.
x = keras.layers.Dropout(0.5)(x)
outputs = keras.layers.Dense(10, name="predictions")(x)
functional_model_with_dropout = keras.Model(
    inputs=inputs, outputs=outputs, name="3_layer_mlp"
)

functional_model_with_dropout.set_weights(functional_model.get_weights())

用于将权重保存到磁盘并加载回磁盘的 API

可以通过调用 model.save_weights(filepath) 将权重保存到磁盘。文件名应以 .weights.h5 结尾。

示例

# Runnable example
sequential_model = keras.Sequential(
    [
        keras.Input(shape=(784,), name="digits"),
        keras.layers.Dense(64, activation="relu", name="dense_1"),
        keras.layers.Dense(64, activation="relu", name="dense_2"),
        keras.layers.Dense(10, name="predictions"),
    ]
)
sequential_model.save_weights("my_model.weights.h5")
sequential_model.load_weights("my_model.weights.h5")

请注意,更改 layer.trainable 可能会导致模型包含嵌套层时 layer.weights 的顺序不同。

class NestedDenseLayer(keras.layers.Layer):
    def __init__(self, units, name=None):
        super().__init__(name=name)
        self.dense_1 = keras.layers.Dense(units, name="dense_1")
        self.dense_2 = keras.layers.Dense(units, name="dense_2")

    def call(self, inputs):
        return self.dense_2(self.dense_1(inputs))


nested_model = keras.Sequential([keras.Input((784,)), NestedDenseLayer(10, "nested")])
variable_names = [v.name for v in nested_model.weights]
print("variables: {}".format(variable_names))

print("\nChanging trainable status of one of the nested layers...")
nested_model.get_layer("nested").dense_1.trainable = False

variable_names_2 = [v.name for v in nested_model.weights]
print("\nvariables: {}".format(variable_names_2))
print("variable ordering changed:", variable_names != variable_names_2)
variables: ['kernel', 'bias', 'kernel', 'bias']
Changing trainable status of one of the nested layers...
variables: ['kernel', 'bias', 'kernel', 'bias']
variable ordering changed: False
迁移学习示例

从权重文件加载预训练权重时,建议将权重加载到原始检查点模型中,然后将所需的权重/层提取到新模型中。

示例

def create_functional_model():
    inputs = keras.Input(shape=(784,), name="digits")
    x = keras.layers.Dense(64, activation="relu", name="dense_1")(inputs)
    x = keras.layers.Dense(64, activation="relu", name="dense_2")(x)
    outputs = keras.layers.Dense(10, name="predictions")(x)
    return keras.Model(inputs=inputs, outputs=outputs, name="3_layer_mlp")


functional_model = create_functional_model()
functional_model.save_weights("pretrained.weights.h5")

# In a separate program:
pretrained_model = create_functional_model()
pretrained_model.load_weights("pretrained.weights.h5")

# Create a new model by extracting layers from the original model:
extracted_layers = pretrained_model.layers[:-1]
extracted_layers.append(keras.layers.Dense(5, name="dense_3"))
model = keras.Sequential(extracted_layers)
model.summary()
Model: "sequential_4"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ dense_1 (Dense)                 │ (None, 64)                │     50,240 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ dense_2 (Dense)                 │ (None, 64)                │      4,160 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ dense_3 (Dense)                 │ (None, 5)                 │        325 │
└─────────────────────────────────┴───────────────────────────┴────────────┘
 Total params: 54,725 (213.77 KB)
 Trainable params: 54,725 (213.77 KB)
 Non-trainable params: 0 (0.00 B)

附录:处理自定义对象

定义配置方法

规格

  • get_config() 应返回一个 JSON 可序列化的字典,以便与 Keras 架构和模型保存 API 兼容。
  • from_config(config)(一个 classmethod)应返回一个从配置创建的新层或模型对象。默认实现返回 cls(**config)

注意:如果所有构造函数参数都已可序列化,例如字符串和整数,或非自定义 Keras 对象,则无需覆盖 from_config。但是,对于更复杂的对象(例如传递给 __init__ 的层或模型),必须在 __init__ 本身或覆盖 from_config() 方法中显式处理反序列化。

示例

@keras.saving.register_keras_serializable(package="MyLayers", name="KernelMult")
class MyDense(keras.layers.Layer):
    def __init__(
        self,
        units,
        *,
        kernel_regularizer=None,
        kernel_initializer=None,
        nested_model=None,
        **kwargs
    ):
        super().__init__(**kwargs)
        self.hidden_units = units
        self.kernel_regularizer = kernel_regularizer
        self.kernel_initializer = kernel_initializer
        self.nested_model = nested_model

    def get_config(self):
        config = super().get_config()
        # Update the config with the custom layer's parameters
        config.update(
            {
                "units": self.hidden_units,
                "kernel_regularizer": self.kernel_regularizer,
                "kernel_initializer": self.kernel_initializer,
                "nested_model": self.nested_model,
            }
        )
        return config

    def build(self, input_shape):
        input_units = input_shape[-1]
        self.kernel = self.add_weight(
            name="kernel",
            shape=(input_units, self.hidden_units),
            regularizer=self.kernel_regularizer,
            initializer=self.kernel_initializer,
        )

    def call(self, inputs):
        return ops.matmul(inputs, self.kernel)


layer = MyDense(units=16, kernel_regularizer="l1", kernel_initializer="ones")
layer3 = MyDense(units=64, nested_model=layer)

config = keras.layers.serialize(layer3)

print(config)

new_layer = keras.layers.deserialize(config)

print(new_layer)
{'module': None, 'class_name': 'MyDense', 'config': {'name': 'my_dense_1', 'trainable': True, 'dtype': 'float32', 'units': 64, 'kernel_regularizer': None, 'kernel_initializer': None, 'nested_model': {'module': None, 'class_name': 'MyDense', 'config': {'name': 'my_dense', 'trainable': True, 'dtype': 'float32', 'units': 16, 'kernel_regularizer': 'l1', 'kernel_initializer': 'ones', 'nested_model': None}, 'registered_name': 'MyLayers>KernelMult'}}, 'registered_name': 'MyLayers>KernelMult'}
<MyDense name=my_dense_1, built=False>

请注意,对于 MyDense,上面无需覆盖 from_config,因为 hidden_unitskernel_initializerkernel_regularizer 分别是整数、字符串和内置 Keras 对象。这意味着 cls(**config) 的默认 from_config 实现将按预期工作。

对于更复杂的对象,例如传递给 __init__ 的层和模型,例如,您必须显式地反序列化这些对象。让我们来看一个需要覆盖 from_config 的模型示例。

示例:

@keras.saving.register_keras_serializable(package="ComplexModels")
class CustomModel(keras.layers.Layer):
    def __init__(self, first_layer, second_layer=None, **kwargs):
        super().__init__(**kwargs)
        self.first_layer = first_layer
        if second_layer is not None:
            self.second_layer = second_layer
        else:
            self.second_layer = keras.layers.Dense(8)

    def get_config(self):
        config = super().get_config()
        config.update(
            {
                "first_layer": self.first_layer,
                "second_layer": self.second_layer,
            }
        )
        return config

    @classmethod
    def from_config(cls, config):
        # Note that you can also use [`keras.saving.deserialize_keras_object`](/api/models/model_saving_apis/serialization_utils#deserializekerasobject-function) here
        config["first_layer"] = keras.layers.deserialize(config["first_layer"])
        config["second_layer"] = keras.layers.deserialize(config["second_layer"])
        return cls(**config)

    def call(self, inputs):
        return self.first_layer(self.second_layer(inputs))


# Let's make our first layer the custom layer from the previous example (MyDense)
inputs = keras.Input((32,))
outputs = CustomModel(first_layer=layer)(inputs)
model = keras.Model(inputs, outputs)

config = model.get_config()
new_model = keras.Model.from_config(config)

自定义对象如何序列化

序列化格式为通过@keras.saving.register_keras_serializable注册的自定义对象提供了一个特殊的键。这个registered_name键允许在加载/反序列化时轻松检索,同时也允许用户添加自定义命名。

让我们看一下上面定义的自定义层MyDense序列化后的配置。

示例:

layer = MyDense(
    units=16,
    kernel_regularizer=keras.regularizers.L1L2(l1=1e-5, l2=1e-4),
    kernel_initializer="ones",
)
config = keras.layers.serialize(layer)
print(config)
{'module': None, 'class_name': 'MyDense', 'config': {'name': 'my_dense_2', 'trainable': True, 'dtype': 'float32', 'units': 16, 'kernel_regularizer': {'module': 'keras.src.regularizers.regularizers', 'class_name': 'L1L2', 'config': {'l1': 1e-05, 'l2': 0.0001}, 'registered_name': 'L1L2'}, 'kernel_initializer': 'ones', 'nested_model': None}, 'registered_name': 'MyLayers>KernelMult'}

如所示,registered_name键包含Keras主列表的查找信息,包括包MyLayers和我们在@keras.saving.register_keras_serializable装饰器中提供的自定义名称KernelMult。请再次查看自定义类定义/注册此处

请注意,class_name键包含类的原始名称,允许在from_config中正确地重新初始化。

此外,请注意module键为None,因为这是一个自定义对象。