Keras 3 架构解析(三):模型构建层——从 Model 抽象到计算图组织
Keras 3 架构解析(三):模型构建层——从 Model 抽象到计算图组织
引言
在前两篇文章中,我们分别讨论了 Keras 3 的基础计算层与神经网络层。
基础计算层通过 Keras Ops 和多后端架构,提供统一的张量运算接口,并利用 JAX、TensorFlow、PyTorch 等计算后端实现自动微分、计算编译和硬件执行。
神经网络层则通过 Layer 抽象,将数学变换、可训练参数和内部状态封装为能够独立使用和递归组合的计算组件。
然而,单独的网络层并不能完整描述一个深度学习模型。
实际的神经网络通常由数十个甚至数百个计算层构成。这些层之间可能存在顺序连接、残差连接、并行分支、共享参数,以及多输入、多输出等复杂关系。
为了组织这些计算结构,Keras 提供了模型构建层(Model Construction)。
模型构建层的核心是 keras.Model。它不仅负责将多个网络层组织为完整的计算系统,还统一管理模型的输入输出、网络拓扑、参数集合以及训练、评估和推理接口。
Keras 3 提供三种主要的模型构建方式:
- Sequential API:通过顺序堆叠网络层构建模型。
- Functional API:通过符号张量与层之间的连接关系构建计算图。
- Model Subclassing:通过继承
Model类,以 Python 程序定义模型的前向计算逻辑。
三种方式建立在同一套模型抽象之上,但在结构表达能力、灵活性、可检查性和序列化方式等方面存在重要差异。
本文将从软件架构与计算组织的角度,系统介绍 Keras 3 的模型构建机制。
一、Model:完整神经网络的基本抽象
1.1 Model 的本质
从数学角度看,一个深度学习模型可以表示为参数化函数:
[
Y=F(X;\Theta)
]
其中:
- (X):模型输入;
- (Y):模型输出;
- (\Theta):模型拥有的全部参数;
- (F):多个计算操作组合形成的整体映射关系。
例如,一个由三层全连接网络构成的模型,可以写为:
[
H_1=\sigma(XW_1+b_1)
]
[
H_2=\sigma(H_1W_2+b_2)
]
[
Y=H_2W_3+b_3
]
其中,每一层都实现局部数学变换,而整个模型负责将这些变换组织成一个完整的计算过程。
在 Keras 中,这种整体计算结构由 Model 表示。
但与普通数学函数不同,Keras Model 不仅描述输入与输出之间的映射,还承担参数跟踪、状态管理、训练集成和模型持久化等工程职责。
因此,可以将 Keras Model 理解为:
一个以神经网络计算结构为核心,统一封装模型状态、执行接口和生命周期管理的计算对象。
1.2 Model 与 Layer 的关系
在 Keras 的对象体系中,Model 继承自 Layer。
这意味着 Model 同样具有以下基本能力:
- 接收输入张量并返回输出张量;
- 管理可训练权重和不可训练权重;
- 维护内部状态;
- 包含其他 Layer 或 Model;
- 支持作为其他计算结构的组成部分。
然而,Model 在 Layer 的基础上增加了完整神经网络所需要的功能。
| 功能 | Layer | Model |
|---|---|---|
| 定义张量计算 | 支持 | 支持 |
| 管理可训练参数 | 支持 | 支持 |
| 管理内部状态 | 支持 | 支持 |
| 嵌套其他计算组件 | 支持 | 支持 |
| 作为完整网络的统一接口 | 非主要职责 | 主要职责 |
compile() |
不提供 | 提供 |
fit() |
不提供 | 提供 |
evaluate() |
不提供 | 提供 |
predict() |
不提供 | 提供 |
| 模型级结构查看 | 有限 | 支持 |
因此,Model 与 Layer 并不是两个相互独立的概念。
Layer 主要解决局部计算组件如何定义的问题,Model 则进一步解决这些组件如何形成完整神经网络的问题。
1.3 Model 的核心组成
从架构上看,一个 Keras Model 可以划分为四个主要部分。
第一部分:模型结构
模型结构描述神经网络由哪些计算层构成,以及这些层如何连接。
例如,模型可能采用顺序结构:
Input
│
▼
Dense
│
▼
LayerNormalization
│
▼
Dense
│
▼
Output
也可能采用包含多个分支的结构:
Input
│
┌────┴────┐
▼ ▼
Dense Conv1D
│ │
└────┬────┘
▼
Merge
│
▼
Output
上述第二种结构仅表示概念上的多分支连接。实际执行合并时,必须保证各分支输出满足相应的形状条件。
第二部分:模型状态
包括:
- 可训练权重;
- 不可训练权重;
- 子层状态;
- 其他由框架跟踪的变量。
模型参数并不需要集中存储在 Model 自己定义的变量中。
通常,各个 Layer 分别管理自身权重,而 Model 负责递归跟踪这些子组件的全部状态。
第三部分:执行接口
Model 可以通过 model(x) 执行前向计算。
此外,还提供训练、评估和推理所需要的统一接口。
第四部分:模型元信息
包括模型名称、输入输出结构、层配置及其他用于模型检查、序列化和重新构建的信息。
需要注意,模型配置与模型权重属于不同概念。
前者描述模型由什么构成,后者记录模型当前参数的实际数值。
二、模型构建的三种方式
Keras 提供 Sequential、Functional 和 Subclassing 三种主要建模方式。
它们不是三套完全独立的神经网络系统,而是同一个 Model 抽象的不同构建方式。
其核心区别在于:
模型的计算结构是通过线性列表、符号计算图,还是 Python 程序表达。
2.1 三种建模方式的基本差异
| 维度 | Sequential | Functional | Subclassing |
|---|---|---|---|
| 结构表达 | 线性层列表 | 符号计算图 | Python 前向程序 |
| 多输入、多输出 | 不适合 | 支持 | 支持 |
| 残差与并行连接 | 不直接支持 | 支持 | 支持 |
| 参数共享 | 不适合 | 支持 | 支持 |
| 数据依赖的动态控制流 | 不支持 | 受到静态图结构限制 | 更灵活 |
| 模型结构可检查性 | 较高 | 较高 | 取决于实现 |
| 自动序列化结构 | 支持 | 支持 | 通常需要额外实现 |
| 典型场景 | 简单顺序网络 | 复杂但拓扑相对固定的网络 | 自定义执行逻辑与动态模型 |
对于结构确定的常规神经网络,通常优先考虑 Sequential 或 Functional。
只有当模型结构难以用静态计算图清晰表达时,才更有必要使用 Model Subclassing。
这种选择不是基于三种 API 的功能等级,而是基于模型的实际结构需求。
三、Sequential API:顺序模型
3.1 Sequential 的基本结构
Sequential 是 Keras 中最简单的模型构建方式。
它将一组 Layer 按照指定顺序连接,使前一层的输出直接成为后一层的输入。
在数学上,可以表示为:
[
Y=f_n(f_{n-1}(\cdots f_2(f_1(X))))
]
每一层执行一个局部计算,而整个模型执行这些计算的复合函数。
例如:
import keras
from keras import layers
model = keras.Sequential([
keras.Input(shape=(16,)),
layers.Dense(
32,
activation="relu"
),
layers.Dense(
8,
activation="relu"
),
layers.Dense(1)
])
该模型的计算结构为:
Input(16)
│
▼
Dense(32, ReLU)
│
▼
Dense(8, ReLU)
│
▼
Dense(1)
│
▼
Output(1)
对于输入:
[
X\in\mathbb{R}^{B\times16}
]
模型输出为:
[
Y\in\mathbb{R}^{B\times1}
]
其中,(B) 为批大小。
3.2 参数数量计算
以上述模型为例,可以逐层计算其参数规模。
第一层 Dense:
[
P_1=16\times32+32=544
]
第二层 Dense:
[
P_2=32\times8+8=264
]
第三层 Dense:
[
P_3=8\times1+1=9
]
因此,整个模型的可训练参数数量为:
[
P_{\text{total}}=544+264+9=817
]
Keras 可以通过以下接口自动统计:
model.summary()
print(model.count_params())
在该示例中,count_params() 返回 817。
3.3 显式输入定义与延迟构建
Sequential 支持在初始化时显式定义输入形状。
model = keras.Sequential([
keras.Input(shape=(16,)),
layers.Dense(32),
layers.Dense(1)
])
由于 Keras 已经知道输入特征维度,因此可以在模型构建过程中确定各层权重形状。
另一种方式是不指定输入:
model = keras.Sequential([
layers.Dense(32),
layers.Dense(1)
])
在这种情况下,模型通常需要等到首次接收输入数据,或者显式调用 build(),才能完成参数初始化。
例如:
model.build((None, 16))
其中,None 表示批大小未被固定。
对于输入形状已经确定的模型,通常更适合在创建时显式指定输入。
这样能够更早发现维度错误,也便于使用 summary() 查看模型结构。
3.4 Sequential 的局限性
Sequential 要求每个连接位置都能够以单输入、单输出的形式顺序组织。
因此,当模型存在以下结构时,Sequential 通常不再适合:
- 多个独立输入分支;
- 多个输出分支;
- 残差连接;
- 跨层跳跃连接;
- 同一 Layer 的多次共享调用;
- 复杂的非线性网络拓扑。
例如,残差结构:
[
Y=X+F(X)
]
要求原始输入 (X) 与经过若干计算层的 (F(X)) 重新合并。
这种连接不是简单的顺序传递,因此无法仅依靠 Sequential 的线性堆叠机制直接描述。
需要注意,Sequential 仍然可以包含一个已经封装好的残差模块。如果残差逻辑被实现为自定义 Layer,外层模型依然可以使用 Sequential。
换言之,Sequential 的限制主要体现在外层拓扑表达能力,而不是禁止使用内部结构复杂的组件。
四、Functional API:符号计算图模型
4.1 Functional API 的核心思想
Functional API 是 Keras 中用于组织复杂神经网络拓扑的主要方式。
其基本思想是:
首先定义符号输入,再通过调用网络层建立张量之间的计算关系,最后根据输入和输出构建完整模型。
这种方式将模型表示为有向无环图(Directed Acyclic Graph,DAG)。
计算图中的节点表示张量或计算操作,连接关系则描述数据如何在不同计算组件之间流动。
例如:
import keras
from keras import layers
inputs = keras.Input(shape=(16,))
x = layers.Dense(
32,
activation="relu"
)(inputs)
x = layers.Dense(
8,
activation="relu"
)(x)
outputs = layers.Dense(1)(x)
model = keras.Model(
inputs=inputs,
outputs=outputs
)
这段代码与此前的 Sequential 示例表示相同的数学结构。
但它们构建模型的方式不同。
Sequential 将层作为有序集合进行组织。
Functional 则显式声明每个计算层的输入来源和输出去向。
因此,Functional 能够表达远比简单顺序结构复杂的网络拓扑。
4.2 Input 与 KerasTensor
Functional API 的起点通常是:
inputs = keras.Input(shape=(16,))
该操作创建的不是包含真实样本数据的普通张量,而是用于描述模型输入的符号张量。
Keras 使用 KerasTensor 表示这种符号结构。
它主要携带:
- 张量形状;
- 数据类型;
- 符号计算关系所需的元信息。
例如:
print(inputs.shape)
print(inputs.dtype)
输出通常为:
(None, 16)
float32
这里的 None 表示批大小不固定。
需要指出,keras.Input(shape=(16,)) 中的形状描述的是单个样本的特征结构,而不是整个批次的完整形状。
对于长度为 60、每个时间步具有 16 个特征的时间序列,应定义为:
inputs = keras.Input(
shape=(60, 16)
)
对应符号张量形状为:
[
(B,60,16)
]
其中,批大小通常表示为 None。
当一个 Layer 接收 KerasTensor 时,Keras 会建立符号计算关系,而不需要立即执行实际的数据运算。
例如:
x = layers.Dense(32)(inputs)
这一步定义了从输入到 Dense 层输出的计算连接。
Keras 可以由此推断输出形状:
[
(B,32)
]
这一过程通常称为静态形状推断。
4.3 计算图的构建机制
Functional API 的重要特征,是模型结构能够在真正处理训练数据之前确定。
考虑:
inputs = keras.Input(shape=(16,))
x = layers.Dense(32)(inputs)
x = layers.Dense(64)(x)
outputs = layers.Dense(1)(x)
model = keras.Model(inputs, outputs)
Keras 在构建过程中逐步记录层之间的连接关系。
最终,Model 根据指定的输入与输出,确定计算图中所需要的层及其依赖关系。
这样便形成了一个明确的模型拓扑。
但必须区分两个不同概念:
- Keras Functional 的符号计算图;
- JAX、TensorFlow 或 PyTorch 后端使用的执行图与编译图。
前者主要描述模型的结构关系。
后者负责将实际计算组织、转换或编译为能够在设备上执行的程序。
Functional API 创建了明确的高层计算结构,但并不意味着它本身完成所有底层编译优化。
五、复杂计算图:残差连接、分支与多输入输出
5.1 残差连接
残差连接是许多现代神经网络的重要结构。
其一般形式为:
[
Y=X+F(X)
]
其中,(F(X)) 由一个或多个神经网络层组成。
使用 Functional API,可以直接表示这一计算关系。
import keras
from keras import layers
inputs = keras.Input(shape=(32,))
x = layers.Dense(
64,
name="projection"
)(inputs)
residual = x
x = layers.Dense(
64,
activation="relu"
)(x)
x = layers.Dense(64)(x)
x = layers.Add()([x, residual])
x = layers.LayerNormalization(
name="residual_norm"
)(x)
outputs = layers.Dense(1)(x)
model = keras.Model(
inputs,
outputs,
name="residual_model"
)
其逻辑结构为:
Input(32)
│
▼
Dense(64)
│
┌─────┴─────┐
│ │
│ ▼
│ Dense(64)
│ │
│ ▼
│ Dense(64)
│ │
└─────┬─────┘
▼
Add
│
▼
LayerNormalization
│
▼
Dense(1)
│
▼
Output
在这个示例中,残差分支与主分支都具有 64 个特征,因此可以执行逐元素加法。
如果两个分支的形状不同,就必须进行必要的投影或维度调整。
Functional API 将这种非顺序拓扑直接体现在代码中,而不需要开发者手动管理计算图对象。
5.2 多输入模型
实际模型可能需要同时接收多种数据。
例如,一个时间序列预测模型可能同时使用:
- 历史序列特征;
- 当前时刻的静态特征。
假设历史序列的形状为:
[
X_{\text{time}}\in\mathbb{R}^{B\times60\times16}
]
静态特征为:
[
X_{\text{static}}\in\mathbb{R}^{B\times8}
]
可以使用 Functional API 分别定义两个输入分支:
import keras
from keras import layers
time_input = keras.Input(
shape=(60, 16),
name="time_features"
)
static_input = keras.Input(
shape=(8,),
name="static_features"
)
# 时间序列分支
time_features = layers.LSTM(32)(
time_input
)
# 静态特征分支
static_features = layers.Dense(
16,
activation="relu"
)(static_input)
# 特征融合
merged = layers.Concatenate()([
time_features,
static_features
])
x = layers.Dense(
32,
activation="relu"
)(merged)
outputs = layers.Dense(1)(x)
model = keras.Model(
inputs=[
time_input,
static_input
],
outputs=outputs
)
两条分支分别提取不同类型的数据特征,再通过 Concatenate 完成特征融合。
其融合结果形状为:
[
(B,48)
]
因为时间序列分支产生 32 维特征,静态分支产生 16 维特征。
这类模型无法用普通 Sequential 结构直接表达,但非常适合使用 Functional API 构建。
5.3 多输出模型
一个模型也可以具有多个预测目标。
例如,同一个特征提取网络可以同时执行二分类和回归任务。
inputs = keras.Input(shape=(32,))
x = layers.Dense(
64,
activation="relu"
)(inputs)
x = layers.Dense(
32,
activation="relu"
)(x)
classification = layers.Dense(
1,
activation="sigmoid",
name="classification"
)(x)
regression = layers.Dense(
1,
name="regression"
)(x)
model = keras.Model(
inputs=inputs,
outputs={
"classification": classification,
"regression": regression
}
)
该模型包含一个共享特征提取部分和两个独立的预测头。
数学上可以表示为:
[
H=F(X;\theta_s)
]
[
Y_c=G_c(H;\theta_c)
]
[
Y_r=G_r(H;\theta_r)
]
其中:
- (\theta_s):共享参数;
- (\theta_c):分类头参数;
- (\theta_r):回归头参数。
这种结构通常称为多任务学习模型。
需要注意,多输出模型在训练时通常还需要针对不同输出配置相应的损失函数与评价指标。
这属于训练系统的职责,将在后续文章中讨论。
六、参数共享与模型复用
6.1 共享 Layer 与重复创建 Layer
在 Keras 中,重复调用同一个 Layer 对象,与创建多个相同配置的 Layer 对象,具有不同含义。
例如:
shared = layers.Dense(16)
y1 = shared(x1)
y2 = shared(x2)
这里的 y1 与 y2 使用同一组权重。
其数学表达式为:
[
Y_1=X_1W+b
]
[
Y_2=X_2W+b
]
两个分支共享 (W) 和 (b)。
相反:
layer1 = layers.Dense(16)
layer2 = layers.Dense(16)
y1 = layer1(x1)
y2 = layer2(x2)
则创建两组相互独立的参数。
即使两个 Layer 的结构与初始化配置相同,它们仍然是两个独立的计算组件。
因此,参数共享的关键不是 Layer 类型是否相同,而是是否复用了同一个 Layer 实例。
这一机制常用于:
- 孪生网络;
- 对比学习模型;
- 双塔结构中的共享编码器;
- 多输入特征提取;
- 需要重复使用同一计算模块的网络。
6.2 子模型的构建
由于 Model 本身继承自 Layer,因此一个 Model 也可以作为另一个 Model 的组件。
例如,首先构建一个编码器:
encoder_input = keras.Input(
shape=(32,)
)
x = layers.Dense(
64,
activation="relu"
)(encoder_input)
encoded = layers.Dense(
16,
activation="relu"
)(x)
encoder = keras.Model(
encoder_input,
encoded,
name="encoder"
)
然后,将编码器作为另一个模型的组成部分:
inputs = keras.Input(shape=(32,))
features = encoder(inputs)
outputs = layers.Dense(1)(features)
model = keras.Model(
inputs,
outputs,
name="predictor"
)
此时,encoder 作为一个独立子模型被嵌套在 predictor 中。
整个结构可以表示为:
Predictor Model
│
├── Input
│
├── Encoder Model
│ ├── Dense(64)
│ └── Dense(16)
│
└── Dense(1)
这种机制有助于实现复杂模型的模块化组织。
例如,可以分别定义 Encoder、Backbone、Prediction Head 等模块,再将它们组合成完整模型。
需要注意,将同一个子模型对象复用于多个位置时,会复用其已有权重,而不是自动创建一套新参数。
6.3 从现有计算图提取子模型
Functional API 还有一个重要特性:可以利用已有计算图中的中间张量,创建新的模型。
例如,对于前面构建的残差模型:
feature_extractor = keras.Model(
inputs=model.inputs,
outputs=model.get_layer(
"residual_norm"
).output
)
这将构建一个输出残差归一化层结果的新模型。
新模型与原模型共享相应的层和权重。
因此,如果修改共享层的权重,相关模型的计算结果也会随之变化。
这种机制通常用于特征提取、模型调试、迁移学习及中间表示分析。
它体现了 Functional API 的一个重要优势:计算图不仅能够执行,也能够被检查和重新组织。
七、Model Subclassing:以 Python 程序定义模型
7.1 Subclassing 的核心思想
Sequential 和 Functional 都主要依靠 Keras 提供的结构化 API 描述模型。
Model Subclassing 则采用不同方式。
开发者直接继承 keras.Model,在 Python 类中定义子组件,并通过 call() 编写模型的前向计算逻辑。
例如:
import keras
from keras import layers
class CustomModel(keras.Model):
def __init__(self, width=64, **kwargs):
super().__init__(**kwargs)
self.width = width
self.projection = layers.Dense(
width
)
self.dense = layers.Dense(
width,
activation="relu"
)
self.dropout = layers.Dropout(0.1)
self.output_layer = layers.Dense(1)
def call(self, inputs, training=None):
x = self.projection(inputs)
residual = x
x = self.dense(x)
x = self.dropout(
x,
training=training
)
x = x + residual
return self.output_layer(x)
实例化:
model = CustomModel(width=64)
对于这个模型,前向计算结构由 call() 中的 Python 代码决定。
与 Functional API 不同,开发者不需要先通过符号张量声明完整的外部计算图。
7.2 Subclassing 的灵活性
Subclassing 最大的优势是能够以一般 Python 程序表达模型计算过程。
例如,模型可以包含循环、条件分支和其他程序控制逻辑。
这对于以下结构可能更加合适:
- 需要自定义执行步骤的复杂模型;
- 包含循环式计算模块的网络;
- 结构随输入条件改变的模型;
- 非标准神经网络架构;
- 难以表示为固定有向无环图的计算过程。
但这里需要区分 Python 控制流与张量控制流。
如果条件分支依赖普通 Python 配置参数,通常可以直接使用 if 语句。
如果条件分支依赖运行时张量中的数值,则在 JAX 编译或其他图执行模式下,普通 Python if 并不一定能够正确执行。
这类情况可能需要使用 keras.ops.cond() 等兼容的控制流操作,或采用后端特定的实现方式。
因此,Subclassing 提供了更自由的程序表达能力,但并不意味着任何 Python 代码都天然支持跨后端 JIT 编译。
7.3 Subclassing 的局限性
与 Functional API 相比,Subclassing 的主要代价是模型结构不一定能够被完整地表示为一个可直接检查的静态计算图。
例如,模型内部可能根据输入数据执行不同的计算分支。
在这种情况下,仅仅检查模型包含哪些 Layer,并不足以确定每次前向传播的实际执行路径。
这会影响:
- 自动生成完整计算图;
- 静态结构检查;
- 模型结构序列化;
- 中间计算节点提取;
- 部分编译与优化工具的适配。
Subclassing 模型仍然可以使用 summary()、权重管理及相关诊断工具,但其可见信息取决于模型的构建状态和具体实现,通常不具备 Functional 模型完整的静态拓扑信息。
因此,Subclassing 更适合确实需要动态计算逻辑的场景,而不应仅仅因为它更接近传统 Python 面向对象编程,就将所有模型都实现为自定义 Model 类。
八、模型检查与参数管理
8.1 summary():查看模型结构
Keras Model 提供 summary() 方法查看网络的主要结构信息。
例如:
model.summary()
输出通常包括:
- 层名称;
- 层类型;
- 输出形状;
- 参数数量;
- 可训练参数总数;
- 不可训练参数总数。
对于包含子模型的复杂结构,可以使用:
model.summary(
expand_nested=True,
show_trainable=True
)
其中:
expand_nested=True:展开嵌套模型;show_trainable=True:展示各层的可训练状态。
需要注意,summary() 一般要求模型已经完成构建。
对于没有指定输入结构的 Sequential 模型或尚未调用的 Subclassing 模型,可能需要先进行构建或执行一次前向计算。
8.2 模型参数与权重访问
Keras Model 提供统一的参数访问接口。
model.weights
model.trainable_weights
model.non_trainable_weights
这些属性递归收集模型及其子组件中的对应权重。
此外,可以使用:
model.count_params()
获取模型的参数总数。
需要注意,参数总数只是模型规模的一个维度。
实际训练内存还受到以下因素影响:
- 参数数据类型;
- 梯度存储;
- 优化器状态;
- 中间激活值;
- 输入批大小;
- 编译与运行时缓冲区。
因此,不能直接根据参数数量判断模型在 GPU 或 TPU 上的完整内存开销。
8.3 层访问与模型检查
对于已经构建的模型,可以通过:
model.layers
获取模型直接包含的网络层列表。
也可以通过名称查找特定层:
layer = model.get_layer(
name="residual_norm"
)
但应注意,model.layers 主要表示模型当前层级中可见的直接子层。
如果模型包含多级嵌套子模型,可能需要进一步访问对应子模型内部的 layers。
此外,Keras 还提供模型结构可视化功能:
keras.utils.plot_model(
model,
to_file="model.png",
show_shapes=True,
expand_nested=True
)
对于结构化的 Functional 模型,这种可视化能够直观展示层之间的连接关系。
对于 Subclassing 模型,则不能假定其所有动态执行路径都能够自动转换为完整的静态结构图。
九、模型配置、复制与序列化
9.1 模型配置与模型状态
模型的结构配置和参数状态属于不同层次。
例如,一个 Dense 层可以具有如下配置:
units = 64
activation = relu
use_bias = True
这些信息描述计算层如何构建。
但它们并不包含权重矩阵中的实际数值。
因此,一个训练前的模型和一个训练后的模型,可以具有完全相同的结构配置,但拥有不同的参数值。
在 Keras 中,这两类信息分别通过配置序列化和权重管理机制处理。
9.2 get_config() 与 from_config()
Keras 提供配置序列化接口:
config = model.get_config()
对于能够被正确配置序列化的模型,可以根据配置重新建立对象。
在构建自定义 Layer 或 Model 时,开发者通常需要实现 get_config(),保存构造对象所需要的参数。
例如:
def get_config(self):
config = super().get_config()
config.update({
"width": self.width
})
return config
当构造参数仅包含简单的 Python 基本类型,并且初始化方法能够直接接收这些配置时,继承的 from_config() 通常已经足够。
如果构造参数中包含其他 Layer、Model 或复杂对象,则可能需要进一步实现专门的反序列化逻辑。
需要特别强调,get_config() 主要保存模型配置,并不意味着它保存了训练完成的权重数值。
9.3 模型复制
Keras 提供:
new_model = keras.models.clone_model(model)
对于支持配置序列化的模型,该操作可以根据原模型的结构配置创建新的模型对象。
默认情况下,新模型会重新初始化权重,而不是复制原模型的参数数值。
因此,模型复制与权重复制不是同一个概念。
如果希望创建一个具有相同结构、相同参数的独立模型,还需要复制权重,例如在兼容的已构建模型之间使用 get_weights() 和 set_weights()。
这类操作对于模型对照实验、初始化一致性控制及模型副本管理具有实际意义。
9.4 完整模型保存
Keras 3 的原生模型保存格式使用 .keras 扩展名。
例如:
model.save("model.keras")
加载模型:
restored_model = keras.models.load_model(
"model.keras"
)
在适用情况下,.keras 文件可以包含:
- 模型结构配置;
- 模型权重;
- 编译配置;
- 优化器状态及其他相关信息。
这一机制使模型能够从磁盘恢复。
但对于包含自定义 Layer 或 Model 的模型,保存文件通常不会包含自定义 Python 类的源代码。
因此,加载时仍然需要能够访问相应的自定义类定义,通常可以通过注册自定义对象或显式提供自定义对象映射来解决。
对于 Subclassing 模型,还必须确保其配置和构建过程能够被正确恢复。
因此,不能认为只要继承了 keras.Model,任何自定义模型就必然能够无条件完整序列化。
9.5 模型保存与模型部署的区别
保存模型主要解决持久化和恢复问题。
模型部署则需要进一步考虑目标运行时、硬件环境、推理接口及模型导出格式。
Keras 3 提供相应的模型导出机制,但不同部署目标可能需要采用不同工具链。
例如,保存为 .keras 文件并不等价于生成可以被任何推理引擎直接执行的通用模型文件。
因此,模型持久化与部署应被视为相关但不同的工程阶段。
十、三种建模方式的工程选择
10.1 简单顺序模型:优先使用 Sequential
如果模型仅包含线性堆叠的网络层,例如:
Input
│
Dense
│
Dropout
│
Dense
│
Output
使用 Sequential 通常最为直接。
它减少了中间变量和显式连接代码,使模型结构容易阅读。
对于简单 MLP、顺序 CNN 和常规特征变换网络而言,Sequential 足以满足需求。
10.2 固定复杂拓扑:优先使用 Functional
如果模型包含残差连接、多个输入输出分支或共享参数,但整体拓扑能够在构建时确定,通常更适合使用 Functional API。
例如:
Time Input Static Input
│ │
▼ ▼
Encoder Dense
│ │
└──────┬───────┘
▼
Concatenate
│
▼
Shared MLP
│
┌──────┴──────┐
▼ ▼
Classifier Regressor
Functional 能够明确表示计算图中的所有连接关系。
这种显式结构通常便于:
- 查看输入与输出形状;
- 检查网络拓扑;
- 提取中间特征;
- 复用共享计算模块;
- 进行结构序列化;
- 调试连接错误。
对于 Transformer、ResNet 及多任务学习模型,Functional 通常是一种合理的组织方式。
10.3 动态结构与复杂逻辑:使用 Subclassing
如果模型的计算流程本身需要由 Python 程序控制,例如不同输入触发不同计算路径,或者需要执行无法简单表达为固定 DAG 的循环式计算,则可以考虑 Model Subclassing。
但使用 Subclassing 时,需要主动维护几个关键约束:
首先,应明确区分参数创建阶段与计算执行阶段。
其次,应尽量使用 Keras 提供的跨后端算子,避免在不必要的情况下引入某个后端的专用 API。
再次,需要显式设计模型的序列化能力,尤其是在模型需要长期保存和重复实验时。
最后,如果目标环境涉及 JIT 编译、TPU 或多设备执行,还应特别检查动态控制流与张量状态管理是否符合后端约束。
10.4 混合使用不同建模方式
三种建模方式并非互斥。
实际工程中,可以将它们组合使用。
例如:
- 使用自定义 Layer 实现特殊数学运算。
- 使用 Functional API 将多个计算模块组织成固定拓扑。
- 在需要额外程序控制的情况下,由 Model Subclassing 管理整体执行过程。
这种分层方式能够将复杂性限制在真正需要复杂实现的模块内部。
例如,一个 Transformer Block 可能具有复杂的注意力机制和残差结构,但只要其输入输出规则明确,就可以被封装为单个 Layer。
外层模型无须了解该模块的每个内部算子,而只需要根据其接口进行组合。
从软件工程角度看,这比将整个神经网络的计算细节全部集中在单个大型 Model 类中更有利于维护。
十一、模型构建中的常见设计问题
11.1 将 Model 误认为必须对应整个训练任务
虽然 Model 通常表示完整神经网络,但它并不一定必须对应最终预测任务。
一个特征编码器、一个解码器,甚至一个独立的 Backbone,都可以表示为 Model。
其原因是 Model 本身可以作为 Layer 使用。
因此,Model 的边界可以根据实际的计算模块划分,而不是只能以完整训练任务为边界。
11.2 将模型构建与模型训练混合
模型构建负责定义计算结构与参数。
模型训练负责计算损失、求取梯度并更新参数。
这两类职责应尽可能分离。
例如,对于常规网络,模型不需要在 call() 中直接执行优化器更新。
通常应将前向计算定义在模型内部,再通过 Keras 训练接口配置相应的损失函数和优化器。
只有在确实需要特殊训练算法时,才应扩展模型训练步骤或编写自定义训练循环。
11.3 在 Functional API 中直接混用后端原生张量操作
Functional API 使用 KerasTensor 表示符号输入。
因此,不能假定任意 TensorFlow、PyTorch 或 JAX 原生函数都能够直接接收这种符号对象。
在构建模型计算关系时,应优先使用:
- Keras Layer;
keras.ops;- 其他支持 Keras 符号张量的接口。
这样不仅能够维持符号计算图的一致性,也有利于模型在不同后端之间迁移。
11.4 忽视模型状态与对象身份
在 Keras 中,同一个 Layer 对象被多次调用,意味着相应参数被共享。
但两个拥有相同配置的 Layer 对象,通常具有独立参数。
因此,参数共享不能仅根据层名称或数学结构判断,而应根据具体对象的复用关系确定。
这在多分支模型、共享编码器和迁移学习中尤其重要。
11.5 以 Model 参数量替代完整性能评估
count_params() 能够统计模型参数规模,但无法直接表示模型的整体计算成本。
例如,两个拥有相同参数数量的模型,可能具有完全不同的计算复杂度。
典型影响因素包括:
- 矩阵运算规模;
- 序列长度;
- 注意力计算复杂度;
- 张量布局;
- 中间激活内存;
- 算子融合;
- 后端编译和硬件执行效率。
因此,模型结构设计不能仅根据参数数量进行评价。
参数规模、计算量、内存访问和设备执行效率应当分别分析。
十二、模型构建层的架构意义
从整个 Keras 3 框架来看,模型构建层位于基础计算层和训练系统之间。
其基本关系可以表示为:
训练系统
┌──────────────────────┐
│ Loss / Optimizer │
│ Metrics / Training │
└──────────┬───────────┘
│
▼
Model
┌──────────────────────┐
│ Sequential │
│ Functional │
│ Model Subclassing │
└──────────┬───────────┘
│
▼
Layer
┌──────────────────────┐
│ Dense / Conv / RNN │
│ Attention / Custom │
└──────────┬───────────┘
│
▼
Keras Ops
┌──────────────────────┐
│ Tensor Operations │
│ Mathematical Ops │
└──────────┬───────────┘
│
▼
计算后端与硬件
┌──────────────────────┐
│ JAX / TensorFlow │
│ PyTorch │
│ CPU / GPU / TPU │
└──────────────────────┘
这是概念性的架构关系,而不是底层系统严格的调用顺序。
在这套架构中,各层具有不同的职责。
基础计算层负责提供统一的计算操作。
神经网络层负责将计算操作封装为具备状态管理能力的组件。
模型构建层负责组织组件之间的计算关系,并提供模型级接口。
训练系统则基于模型执行前向计算、损失计算、梯度更新和效果评估。
这种分层结构的主要意义,是将数学计算、组件封装、模型组织与训练执行区分开来。
开发者可以在不修改训练系统的情况下调整模型拓扑,也可以在保持模型结构相对稳定的情况下更换损失函数、优化器或计算后端。
与此同时,模型构建层通过统一的 Model 接口,使不同复杂程度的神经网络能够接入同一套训练与管理系统。
需要强调的是,这种架构分层并不意味着各部分在执行时完全独立。
模型的实际运行仍然依赖网络层、计算后端和硬件执行环境之间的协同。
但从软件设计角度看,明确的抽象边界可以降低模块之间的耦合,提高模型开发的可维护性与复用能力。
结语
Keras 3 的模型构建层,主要解决如何将多个神经网络组件组织成完整计算系统的问题。
其核心设计可以概括为三个方面。
首先,Model 在 Layer 的基础上提供完整神经网络所需要的结构管理、状态管理和训练接口。
其次,Sequential、Functional 和 Subclassing 提供了不同层次的模型表达能力,使开发者能够根据网络拓扑和执行逻辑选择合适的构建方式。
最后,Keras 允许 Layer 与 Model 递归嵌套,通过参数共享、子模型复用和计算图重组,实现复杂神经网络的模块化组织。
对于常规深度学习开发,Functional API 通常能够在结构表达能力与工程可维护性之间取得较好的平衡。
对于简单顺序网络,Sequential 更加直接。
对于真正需要动态执行逻辑的模型,Subclassing 则提供了必要的自由度。
三种方式没有绝对的优劣之分。合理的选择应当以模型结构需求为基础,避免为了追求抽象能力而引入不必要的复杂性。
至此,我们已经介绍了 Keras 3 的基础计算层、神经网络层与模型构建层。
这三个部分共同解决了从数学运算、计算组件到完整模型组织的问题。
但一个已经构建完成的神经网络,仍然只是一个具有参数和前向计算能力的数学系统。
要使模型能够从数据中学习,还需要进一步引入损失函数、优化器、梯度计算及训练循环。
在下一篇文章中,我们将讨论 Keras 的训练系统,分析 compile()、fit()、evaluate() 和 predict() 的职责,以及 Loss、Optimizer、Metrics 与训练循环之间的工作关系。
- 点赞
- 收藏
- 关注作者
评论(0)