Keras 3 架构解析(二):神经网络层——从 Layer 抽象到网络组件设计
Keras 3 架构解析(二):神经网络层——从 Layer 抽象到网络组件设计
引言
在上一篇文章中,我们介绍了 Keras 3 的基础计算层,包括多后端计算架构、Keras Ops、自动微分、JIT 编译以及数值精度管理等核心机制。
基础计算层解决了数学运算如何执行的问题,但仅有张量运算并不足以构建完整的神经网络。
一个实际的深度学习模型通常由多个计算单元组成。这些计算单元不仅需要执行数学变换,还必须管理可训练参数、维护内部状态、处理输入与输出的维度关系,并能够与其他计算单元组合。
Keras 将这些职责统一封装在神经网络层(Layer)这一抽象中。
在 Keras 3 中,Layer 不仅是 Dense、Conv2D、LSTM 等常见网络组件的基础类,也是连接底层张量运算与上层模型构建系统的核心接口。
本文从软件架构与数学计算两个角度出发,介绍 Keras 3 的神经网络层系统,包括 Layer 基类、权重与状态管理、层的生命周期、主要网络层类型,以及自定义层的实现机制。
一、Layer:神经网络的基本抽象
1.1 Layer 的本质
在数学意义上,神经网络层通常可以表示为一个参数化函数:
[
Y=f(X;\theta)
]
其中:
- (X):输入张量;
- (Y):输出张量;
- (\theta):该层所拥有的参数;
- (f):由张量运算构成的计算过程。
例如,全连接层的数学表达式为:
[
Y=\sigma(XW+b)
]
其中,(W) 为权重矩阵,(b) 为偏置向量,(\sigma) 为激活函数。
这一表达式包含两个不同的组成部分。
首先是计算逻辑,即矩阵乘法、向量加法和激活函数等操作。
其次是状态,即参与计算的权重矩阵与偏置向量。
Keras 的 Layer 类将两者结合起来,使神经网络层成为一个同时管理计算与状态的对象。
需要强调的是,并非所有网络层都拥有可训练参数。例如,Dropout、Reshape 和部分池化层通常不包含可训练权重。
因此,更严格的定义是:
Layer 是一个能够接收张量、执行变换并根据需要管理内部状态的可组合计算单元。
1.2 Layer 的主要职责
Keras 的 Layer 基类承担以下几个方面的职责。
| 职责 | 主要内容 |
|---|---|
| 计算定义 | 通过 call() 定义前向计算过程 |
| 参数创建 | 通过 add_weight() 创建权重 |
| 状态管理 | 跟踪可训练与不可训练变量 |
| 生命周期管理 | 通过 build() 初始化依赖输入形状的状态 |
| 输入管理 | 处理输入形状、数据类型和输入约束 |
| 训练行为 | 支持训练模式、推理模式及参数冻结 |
| 组合能力 | 支持将其他 Layer 嵌套为子组件 |
| 持久化支持 | 提供配置序列化及权重保存所需的信息 |
Keras 正是通过统一这些基础职责,避免每一种网络层都重复实现参数注册、状态管理与执行调度等通用逻辑。
1.3 Layer 与普通函数的区别
考虑以下两个实现。
第一个是普通张量计算函数:
from keras import ops
def linear(x, w, b):
return ops.matmul(x, w) + b
该函数完成一个线性变换,但权重需要由外部提供。
第二个使用 Keras Layer:
import keras
from keras import ops
class Linear(keras.layers.Layer):
def __init__(self, units, **kwargs):
super().__init__(**kwargs)
self.units = units
def build(self, input_shape):
self.w = self.add_weight(
shape=(input_shape[-1], self.units),
initializer="glorot_uniform"
)
self.b = self.add_weight(
shape=(self.units,),
initializer="zeros"
)
def call(self, inputs):
return ops.matmul(inputs, self.w) + self.b
两者实现的数学变换基本相同,但管理方式不同。
普通函数只描述如何计算,权重的创建、注册和生命周期均由外部负责。
Layer 则将权重与计算逻辑封装在同一个对象内,并交由 Keras 统一管理。
因此,在深度学习框架的架构中,Layer 不仅是一种数学计算封装,也是一种资源管理与组件组合机制。
二、Layer 的生命周期:__init__()、build() 与 call()
Keras Layer 的重要设计之一,是将对象初始化、参数构建和前向计算划分为不同阶段。
这三个阶段分别由 __init__()、build() 和 call() 承担。
2.1 __init__():定义层的配置
__init__() 是 Python 对象的初始化方法。
对于 Keras Layer,它通常用于保存配置参数、创建子层,以及初始化不依赖输入形状的状态。
例如:
class Linear(keras.layers.Layer):
def __init__(self, units, **kwargs):
super().__init__(**kwargs)
self.units = units
此时,units 表示输出特征维度。
由于尚未得到输入张量,无法确定输入特征数量,因此还不能确定权重矩阵的完整形状。
需要注意的是,这并不意味着 Keras 禁止在 __init__() 中创建权重。如果权重形状已知,可以直接通过 add_weight() 创建。
但对于依赖输入维度的权重,通常应当推迟到 build() 阶段创建。
2.2 build():创建参数与状态
build() 负责根据输入形状创建相应的权重和状态。
例如:
def build(self, input_shape):
input_dim = input_shape[-1]
self.w = self.add_weight(
name="kernel",
shape=(input_dim, self.units),
initializer="glorot_uniform"
)
self.b = self.add_weight(
name="bias",
shape=(self.units,),
initializer="zeros"
)
假设输入张量形状为:
[
X\in\mathbb{R}^{B\times16}
]
其中 (B) 为 Batch Size。
如果设置 units=32,那么:
[
W\in\mathbb{R}^{16\times32}
]
[
b\in\mathbb{R}^{32}
]
该层拥有的可训练参数总数为:
[
16\times32+32=544
]
build() 的意义在于,将参数创建与实际输入结构联系起来。
这一机制通常被称为延迟构建(Lazy Building)。
它允许开发者先定义层的结构,再在首次使用时根据输入形状确定参数规模。
2.3 call():定义前向计算
call() 定义网络层执行的数学操作。
例如:
def call(self, inputs):
return ops.matmul(inputs, self.w) + self.b
与 build() 不同,call() 会在每次使用该层时执行。
因此,二者具有不同的生命周期。
build() 通常只需要执行一次,而 call() 可以执行多次。
对于同一个层对象,每次调用通常复用此前创建的权重,而不会重新初始化参数。
2.4 __call__():实际的执行入口
Keras Layer 对象可以像函数一样调用:
layer = Linear(units=32)
x = ops.ones((8, 16))
y = layer(x)
print(y.shape)
# (8, 32)
这里真正由 Python 调用的是 layer.__call__(),而不是直接调用 layer.call()。
Keras 的 __call__() 不只是简单转发,它还承担层生命周期管理、输入检查、数据类型处理以及相关框架状态管理等职责。
对于尚未构建的层,它会根据输入情况触发构建过程。
简化后的执行关系为:
创建 Layer 对象
│
▼
__init__()
│
▼
首次调用 layer(x)
│
▼
__call__()
│
├── 输入检查及必要的转换
│
├── 检查是否已经构建
│
├── 必要时执行 build()
│
└── 执行 call()
│
▼
输出张量
需要指出,这只是概念上的执行流程,不代表 Keras 内部实现的严格调用顺序。
对于后续调用,如果输入结构与已经构建的参数兼容,通常可以直接复用现有状态完成计算。
2.5 为什么不应在 call() 中随意创建权重
Keras 3 对状态创建的时机提出了更明确的要求。
尤其是在 JAX 后端中,模型可能需要在编译前确定完整的状态结构。
如果在 call() 内根据输入数据的运行结果动态创建权重,就可能导致状态无法提前确定,并引发构建或编译错误。
因此,自定义层应当遵循一个基本原则:
在 __init__() 或 build() 中创建状态,在 call() 中定义计算。
这一原则不仅有助于提高跨后端兼容性,也能降低模型生命周期管理的复杂度。
三、权重与状态管理
3.1 可训练权重与不可训练权重
Keras 将层中的权重划分为两类。
第一类是可训练权重(Trainable Weights)。
这类权重通常通过反向传播计算梯度,并由优化器在训练过程中更新。
第二类是不可训练权重(Non-trainable Weights)。
这类权重虽然不会被常规梯度优化过程更新,但仍然属于层的内部状态,可以根据特定规则进行修改。
例如,Batch Normalization 层中的移动均值和移动方差,就是典型的不可训练状态。
Keras 提供三个主要属性:
layer.weights
layer.trainable_weights
layer.non_trainable_weights
其中,weights 包含该层跟踪的全部权重,而后两者分别对应可训练与不可训练权重。
3.2 使用 add_weight() 管理参数
开发者应当优先使用 Keras 提供的 add_weight() 方法创建层权重。
例如:
self.counter = self.add_weight(
name="counter",
shape=(),
initializer="zeros",
trainable=False
)
这样创建的变量会被 Keras 自动跟踪,并纳入相应的状态管理系统。
相比之下,如果在自定义层中直接使用后端原生变量构造函数,例如某些情况下直接创建 tf.Variable,Keras 3 并不保证自动将其注册为层权重。
通过 add_weight() 创建变量,可以明确其形状、初始化方式、训练属性、数据类型、正则化和约束等信息。
因此,它不仅是变量创建接口,也是 Keras 参数管理体系的重要组成部分。
3.3 参数冻结与训练模式
Keras 使用 trainable 属性控制某个层的权重是否参与训练。
例如:
layer.trainable = False
这会使该层的权重不再作为可训练权重交给常规优化过程。
但必须区分两个概念:
trainable:是否允许权重参与训练更新;training:当前执行的是训练模式还是推理模式。
对于大多数层,两者是不同的控制维度。
例如,Dropout 层没有可训练权重,因此设置 trainable=False 并不会关闭 Dropout 的随机失活行为。
真正控制 Dropout 行为的是 training 参数。
不过,Batch Normalization 是一个需要特别注意的例外。在 Keras 中,将其设置为 trainable=False,通常也会使该层按推理模式执行,不再更新其移动统计量。
正确理解这两类状态,对于迁移学习、参数冻结与模型微调十分重要。
四、主要神经网络层
Keras 3 提供了多种预定义网络层。
这些层遵循统一的 Layer 抽象,但各自实现不同的数学结构。
4.1 全连接层:Dense
Dense 是最基础的参数化神经网络层之一。
它执行线性变换,并可以选择性地应用非线性激活函数:
[
Y=\sigma(XW+b)
]
Keras 实现:
from keras import layers
dense = layers.Dense(
units=64,
activation="relu"
)
Dense 的输入并不局限于二维张量。
例如,当输入形状为:
[
(B,T,D)
]
其中,(B) 表示批大小,(T) 表示序列长度,(D) 表示特征维度时,Dense 会对最后一个维度执行线性变换。
若输出维度为 (H),输出形状为:
[
(B,T,H)
]
因此,Dense 可以直接用于时间序列、文本序列及 Transformer 的特征映射。
4.2 卷积层:Convolution
卷积层通过局部感受野和权重共享机制处理输入数据。
与 Dense 对每个输入位置应用相同的特征变换不同,卷积层还会在特定维度上聚合邻近位置的信息。
Keras 提供多种卷积层:
| 网络层 | 典型应用 |
|---|---|
Conv1D |
时间序列、一维信号 |
Conv2D |
图像、二维空间数据 |
Conv3D |
视频、三维空间数据 |
DepthwiseConv2D |
逐通道空间卷积 |
SeparableConv2D |
深度可分离卷积 |
以一维卷积为例:
conv = layers.Conv1D(
filters=64,
kernel_size=3,
padding="same",
activation="relu"
)
假设输入采用 channels_last 格式,形状为:
[
(B,60,16)
]
其中序列长度为 60,输入特征数为 16。
在步长为 1、使用 same 填充的情况下,输出形状为:
[
(B,60,64)
]
卷积层的核心特点是局部信息聚合与参数共享。
由于卷积核的权重在不同位置复用,因此卷积层通常能够以相对较少的参数提取局部模式。
4.3 循环神经网络层:RNN、LSTM 与 GRU
循环神经网络通过递归状态描述序列数据之间的依赖关系。
其一般形式可以表示为:
[
h_t=f(x_t,h_{t-1};\theta)
]
其中,(x_t) 是当前时间步的输入,(h_{t-1}) 是上一时间步的隐藏状态。
Keras 提供的主要循环层包括:
SimpleRNNLSTMGRU
其中,LSTM 与 GRU 通过门控机制改善传统循环神经网络在长序列训练中的部分问题。
例如:
lstm = layers.LSTM(
units=64,
return_sequences=True
)
当输入形状为:
[
(B,T,D)
]
且设置 return_sequences=True 时,输出形状为:
[
(B,T,64)
]
如果设置为 False,则通常只返回最后一个时间步对应的输出:
[
(B,64)
]
此外,循环层还可以根据参数配置返回内部状态,或在连续批次之间保留状态。
这些机制使其适合需要显式处理序列状态的任务。
4.4 注意力层:Attention
注意力机制通过计算不同位置之间的相关性,实现跨位置的信息聚合。
在 Transformer 中,典型的缩放点积注意力可以表示为:
[
\operatorname{Attention}(Q,K,V)
\operatorname{softmax}
\left(
\frac{QK^T}{\sqrt{d_k}}
\right)V
]
其中:
- (Q):Query;
- (K):Key;
- (V):Value;
- (d_k):Key 的特征维度。
Keras 提供了多种注意力层,其中最常见的是 MultiHeadAttention。
attention = layers.MultiHeadAttention(
num_heads=4,
key_dim=16
)
对于自注意力,可以将同一张量同时作为 Query 和 Value 输入:
output = attention(
query=x,
value=x
)
当 key 未显式指定时,它默认使用 value。
多头注意力会将输入投影到多个不同的注意力子空间中,再组合各个注意力头的计算结果。
需要注意,key_dim 表示每个注意力头的 Key/Query 维度,而不是整个注意力层的总输出维度。
当未指定 output_shape 时,MultiHeadAttention 通常会将最终结果投影回 Query 的最后一维特征空间。
例如,输入为:
[
(B,T,64)
]
使用 4 个注意力头、每个头的 Key 维度为 16,并不意味着最终输出必然扩展为 256 维。在默认输出投影配置下,输出仍为:
[
(B,T,64)
]
还应注意,注意力层与完整 Transformer Block 不是同一个概念。
一个典型的 Transformer Block 除了多头注意力外,还包含前馈网络、残差连接及归一化等组件。
这些组件需要通过多个 Keras Layer 进行组合。
五、辅助网络层
除了主要的特征变换层,神经网络通常还需要归一化、正则化、池化及张量结构变换等辅助组件。
虽然这些组件未必承担主要的特征提取任务,但对于模型稳定性、表达能力及计算组织同样重要。
5.1 归一化层
归一化层用于调整中间特征的数值分布。
Keras 主要提供 BatchNormalization、LayerNormalization、GroupNormalization 等组件。
BatchNormalization
Batch Normalization 在训练过程中使用当前批次的统计信息对输入进行归一化。
其基本形式为:
[
y=\gamma
\frac{x-\mu}{\sqrt{\sigma^2+\epsilon}}
+\beta
]
其中,(\mu) 和 (\sigma^2) 是归一化时使用的统计量,(\gamma) 和 (\beta) 是可学习的缩放与平移参数。
BatchNormalization 在训练和推理阶段通常采用不同的统计量:训练时使用当前批次统计量,推理时使用训练期间积累的移动统计量。
LayerNormalization
Layer Normalization 对每个样本沿指定的特征维度独立计算统计量,而不依赖批次内其他样本的统计数据。
norm = layers.LayerNormalization(
axis=-1,
epsilon=1e-5
)
LayerNormalization 广泛用于 Transformer 等神经网络结构。
选择归一化方法时,需要考虑输入张量的结构、批大小以及模型的训练方式,而不是仅根据网络类型作简单判断。
5.2 正则化层
正则化的主要目的是降低模型过拟合风险。
Dropout 是 Keras 中最常见的正则化层之一。
dropout = layers.Dropout(0.2)
在训练模式下,Dropout 会以指定概率随机将部分输入单元置零,并对剩余单元进行相应缩放。
在正常推理模式下,Dropout 不执行随机失活。
此外,Keras 还提供 GaussianNoise、SpatialDropout 等其他正则化组件。
这些层的主要特征是修改训练过程中的数据或中间表示,而不是直接增加可训练参数。
5.3 池化层
池化层用于对局部区域或整个特征维度执行聚合。
常见组件包括:
| 层 | 功能 |
|---|---|
MaxPooling1D |
一维局部最大值池化 |
AveragePooling1D |
一维局部平均池化 |
MaxPooling2D |
二维局部最大值池化 |
GlobalAveragePooling1D |
沿一维序列进行全局平均聚合 |
GlobalAveragePooling2D |
对二维空间维度进行全局平均聚合 |
例如:
pool = layers.GlobalAveragePooling1D()
当输入为:
[
(B,T,D)
]
且采用默认设置时,输出为:
[
(B,D)
]
即在时间维度上对特征进行聚合。
5.4 张量形状变换层
许多神经网络结构需要调整张量形状,而不是改变其数值内容。
Keras 提供了 Reshape、Flatten、Permute 等组件。
例如:
reshape = layers.Reshape((8, 16))
这一操作可以将每个样本中的 128 个元素重新组织成 (8\times16) 的结构。
又如:
permute = layers.Permute((2, 1))
对于输入:
[
(B,T,D)
]
该操作将得到:
[
(B,D,T)
]
这些操作在时间序列模型中尤其常见。
不同架构对于时间维度与特征维度的处理方式可能不同,因此经常需要显式调整张量维度。
需要注意,改变形状并不等于改变数据的实际含义。Reshape 仅重新组织元素的维度结构,而 Permute 会调整指定维度的顺序。二者不能随意互换。
5.5 合并层与残差连接
复杂神经网络并不总是由单一路径顺序构成。
例如,ResNet 和 Transformer 都大量使用残差连接。
残差结构的一般形式为:
[
Y=X+F(X)
]
Keras 可以通过 Add 层实现:
output = layers.Add()([x, residual])
除加法外,Keras 还支持 Concatenate、Multiply、Average 等合并操作。
对于需要多分支计算的模型,这些层提供了统一的张量组合接口。
不过,合并操作需要满足相应的形状约束。例如,Add 要求输入张量具有兼容的相同形状;如果残差分支与主分支的维度不同,就需要先进行必要的投影或变换。
六、参数初始化、正则化与约束
神经网络层不仅需要决定执行什么计算,还必须确定参数如何初始化、如何受到训练目标约束,以及更新后需要满足哪些条件。
Keras 通过三个相互独立的机制管理这些行为。
6.1 参数初始化(Initializer)
参数初始化决定模型权重在训练开始前的初始数值。
例如:
layer = layers.Dense(
64,
kernel_initializer="glorot_uniform",
bias_initializer="zeros"
)
常见初始化方法包括 Glorot、He、LeCun、RandomNormal 和 Orthogonal 等。
初始化方法通常与激活函数、网络结构以及输入输出特征规模有关。
以 Glorot 初始化为例,它通过考虑输入和输出维度来控制初始权重的方差,以降低深层网络在初始前向传播和反向传播过程中出现数值尺度不合理的风险。
参数初始化发生在权重创建阶段,而不是每次执行前向计算时重新进行。
6.2 正则化(Regularizer)
正则化通过向训练目标增加额外惩罚项,约束模型参数或激活值。
例如,L2 正则化通常可以表示为:
[
L_{\text{total}}
L_{\text{task}}
+
\lambda\sum_i w_i^2
]
其中,(L_{\text{task}}) 为原始任务损失,第二项为正则化惩罚。
Keras 提供:
from keras import regularizers
layer = layers.Dense(
64,
kernel_regularizer=regularizers.L2(1e-4)
)
正则化产生的附加损失可以通过层的 losses 属性获取,并在合适的训练流程中纳入总损失。
需要区分的是,L2 正则化与优化器中的 Weight Decay 虽然在某些条件下具有相关的数学效果,但并不总是等价,尤其是在使用自适应优化器时。
6.3 参数约束(Constraint)
参数约束则直接限制权重更新后的取值范围或结构。
例如:
from keras import constraints
layer = layers.Dense(
64,
kernel_constraint=constraints.MaxNorm(2.0)
)
这种约束可以在优化器完成权重更新后,将参数投影到满足指定条件的集合中。
因此,三者的职责存在明确区别:
| 机制 | 生效阶段 | 主要作用 |
|---|---|---|
| Initializer | 参数创建时 | 设置初始权重 |
| Regularizer | 训练目标计算时 | 增加惩罚项 |
| Constraint | 参数更新后 | 限制参数取值 |
理解这种区分,有助于避免将初始化策略、损失惩罚和更新约束混为一谈。
七、自定义 Layer:从数学结构到可复用组件
预定义网络层能够覆盖大量常见模型结构,但在研究新模型时,开发者经常需要定义自己的计算单元。
Keras 提供的 Layer 子类机制,使开发者能够在保留框架状态管理能力的同时,自由定义数学计算过程。
7.1 实现一个门控全连接层
下面实现一个简单的门控全连接层(Gated Dense Layer)。
该层包含两个并行的线性变换:
[
H=\tanh(XW_h+b)
]
[
G=\sigma(XW_g)
]
然后将两个结果逐元素相乘:
[
Y=H\odot G
]
其中,(\sigma) 表示 Sigmoid 函数,(\odot) 表示逐元素乘法。
这种设计使网络能够通过门控分支调节不同特征的输出强度。
以下为完整实现:
import keras
from keras import ops
@keras.saving.register_keras_serializable(
package="Custom"
)
class GatedDense(keras.layers.Layer):
def __init__(self, units, **kwargs):
super().__init__(**kwargs)
self.units = units
def build(self, input_shape):
input_dim = input_shape[-1]
self.kernel = self.add_weight(
name="kernel",
shape=(input_dim, self.units),
initializer="glorot_uniform"
)
self.gate_kernel = self.add_weight(
name="gate_kernel",
shape=(input_dim, self.units),
initializer="glorot_uniform"
)
self.bias = self.add_weight(
name="bias",
shape=(self.units,),
initializer="zeros"
)
def call(self, inputs):
hidden = ops.tanh(
ops.matmul(inputs, self.kernel)
+ self.bias
)
gate = ops.sigmoid(
ops.matmul(inputs, self.gate_kernel)
)
return hidden * gate
def get_config(self):
config = super().get_config()
config.update({
"units": self.units
})
return config
该实现具有以下特点。
首先,参数创建集中在 build() 中,避免在前向计算期间动态创建权重。
其次,所有数学运算均使用 keras.ops,因此不直接依赖某个后端的原生张量 API。
再次,get_config() 保存必要的初始化配置,使自定义层能够参与 Keras 的配置序列化机制。
最后,通过 register_keras_serializable() 注册类名,可以简化后续模型反序列化时对自定义对象的识别。加载时仍需确保相应的自定义层实现已经被正确注册或提供。
7.2 使用自定义层
自定义 Layer 可以像普通网络层一样使用:
inputs = keras.Input(shape=(16,))
outputs = GatedDense(32)(inputs)
model = keras.Model(
inputs=inputs,
outputs=outputs
)
假设输入为:
[
X\in\mathbb{R}^{B\times16}
]
输出为:
[
Y\in\mathbb{R}^{B\times32}
]
该层拥有两组 (16\times32) 的权重矩阵,以及一个长度为 32 的偏置向量。
参数总数为:
[
16\times32\times2+32=1056
]
如果将相同层应用于更高维的输入,例如形状为 ((B,T,16)) 的序列张量,矩阵运算也可以作用于最后一个特征维度,从而得到 ((B,T,32)) 的输出。
这体现了 Layer 抽象的另一个优点:开发者只需要定义局部计算规则,而不必针对每一种批大小或序列长度重新实现相同的变换。
7.3 Layer 的组合与嵌套
Keras Layer 支持递归组合。
一个 Layer 可以包含多个子 Layer,而外层对象会自动跟踪相应的子组件及其权重。
例如:
class ResidualMLP(keras.layers.Layer):
def __init__(self, width, **kwargs):
super().__init__(**kwargs)
self.dense1 = keras.layers.Dense(
width,
activation="gelu"
)
self.dense2 = keras.layers.Dense(
width
)
self.dropout = keras.layers.Dropout(0.1)
def call(self, inputs, training=None):
x = self.dense1(inputs)
x = self.dropout(
x,
training=training
)
x = self.dense2(x)
return inputs + x
这一自定义层同时包含两个 Dense 层和一个 Dropout 层。
其计算结构可以概括为:
[
Y=X+W_2
\left(
\operatorname{Dropout}
\left(
\operatorname{GELU}(XW_1+b_1)
\right)
\right)+b_2
]
在这个示例中,输入特征维度必须与 width 一致,才能直接执行残差加法。
更一般地,如果输入与输出维度不同,需要额外增加投影层,使残差分支与主分支具有相同的形状。
需要注意,这种嵌套并不要求开发者手动收集每个子层的权重。Keras 会将作为属性注册的子 Layer 纳入外层对象的权重管理体系。
由此,Layer 可以逐级组合出更复杂的网络结构。
例如:
基础张量运算
│
▼
Dense / Conv / Attention
│
▼
自定义计算单元
│
▼
Residual Block
│
▼
Transformer Block
│
▼
完整神经网络模型
这种分层组合能力是 Keras 实现复杂模型结构的重要基础。
八、Layer 的输入输出、Mask 与执行行为
8.1 输入与输出形状
网络层之间通过张量连接,因此输入与输出形状必须满足相应的计算条件。
例如,Dense 对输入张量最后一个维度执行线性变换,而 Conv1D 通常要求输入至少包含序列维度和通道维度。
Keras 可以在层构建期间根据输入形状推断权重维度,并在部分情况下进行输入约束检查。
开发者还可以通过 InputSpec 声明层对输入维度、形状或数据类型的要求。
例如,某个自定义层可以规定只接受三维张量,从而避免不符合预期的输入直接进入计算过程。
但应当注意,维度合法并不代表语义正确。
例如,形状同为 ((B,T,D)) 的两个张量,可能分别表示时间序列与空间特征序列。Keras 不会自动理解其中的业务含义。
8.2 Mask:无效位置的处理
在自然语言处理和时间序列任务中,不同样本可能拥有不同的有效序列长度。
为了组成统一批次,通常需要对较短序列进行填充。
但填充位置不应被直接视为真实数据。
Keras 通过 Mask 机制标记需要忽略的位置。
例如:
inputs = keras.Input(shape=(None, 8))
x = keras.layers.Masking(
mask_value=0.0
)(inputs)
outputs = keras.layers.LSTM(32)(x)
这一示例中,如果某个时间步的所有特征都等于 0,该时间步将被标记为无效位置。
支持 Mask 的后续层可以依据这些标记处理数据。
Keras 的 Mask 约定通常使用布尔值表示有效性:
True:有效位置;False:需要屏蔽的位置。
需要注意,并不是所有网络层都支持 Mask。对于没有实现相应处理逻辑的自定义层,开发者可能需要显式接收、传播或重新计算 Mask,否则可能出现错误或丢失有效性信息。
8.3 training 参数
某些 Layer 在训练和推理阶段具有不同的执行逻辑。
自定义层可以通过 call() 中的 training 参数表达这种区别:
def call(self, inputs, training=None):
x = self.dense(inputs)
return self.dropout(
x,
training=training
)
这样可以让外部训练系统在调用该层时,将训练或推理状态传递给内部子层。
需要注意,training 控制的是前向执行行为,不应将其与参数是否允许更新的 trainable 属性混淆。
九、Layer 与 Model 的架构边界
在 Keras 中,Layer 和 Model 之间存在紧密联系。
从抽象关系上看,Model 本身也是 Layer 的一种扩展。因此,Model 同样可以被调用、拥有权重并包含其他 Layer。
但两者的主要职责不同。
Layer 更关注局部计算逻辑与内部状态管理。
Model 则在此基础上提供完整网络的组织能力,并支持模型级训练、评估、推理及相关管理接口。
| 特征 | Layer | Model |
|---|---|---|
| 定义张量变换 | 支持 | 支持 |
| 管理内部权重 | 支持 | 支持 |
| 组合子层 | 支持 | 支持 |
| 参与自动微分 | 支持 | 支持 |
直接提供 fit() |
不提供 | 提供 |
直接提供 evaluate() |
不提供 | 提供 |
直接提供 predict() |
不提供 | 提供 |
| 表示完整模型 | 可以作为计算主体,但不是主要职责 | 主要职责 |
因此,Layer 与 Model 不是两个完全独立的系统,而是处于不同的抽象层次。
Layer 构成可复用的计算组件,Model 则将这些组件组织成可训练和可部署的整体。
理解这种区别,能够帮助开发者在构建复杂模型时保持合理的模块边界。
对于一个包含多个 Transformer Block 的神经网络而言,通常可以将单个 Transformer Block 实现为自定义 Layer,再通过 Model 组织整个网络及其训练接口。
这样能够将数学计算组件与模型级职责相互分离,避免把所有逻辑集中在单个类中。
十、Keras Layer 设计的架构意义
从软件架构角度看,Keras Layer 系统体现了四项重要的设计原则。
10.1 计算与状态封装
Layer 将数学变换与相应参数组织在同一组件内。
这使开发者能够独立定义、复用和测试特定计算结构,而不必在模型其他部分重复处理权重管理问题。
10.2 参数构建与计算执行分离
通过 build() 与 call() 的分工,Keras 将参数创建与前向执行划分为不同阶段。
这一设计使网络层能够根据输入结构延迟创建参数,同时保证后续调用复用已有状态。
在需要编译或进行函数式执行的后端中,明确的状态边界尤其重要。
10.3 组件化与递归组合
Layer 可以包含其他 Layer,并自动跟踪其内部权重。
因此,复杂网络不必作为一个不可分割的大型计算函数实现,而可以被拆解为多级、可复用的计算单元。
这为卷积网络、残差网络和 Transformer 等复杂结构提供了统一的组织方式。
10.4 计算实现与后端解耦
Keras 3 允许自定义 Layer 使用 keras.ops 描述数学运算。
在使用跨后端兼容 API、遵循状态创建规则,并且没有依赖后端特定能力的情况下,同一个 Layer 可以运行在不同计算后端上。
但这并不意味着 Keras 能够保证所有后端具有完全一致的执行方式或性能。
神经网络层定义了计算结构,底层实际算子、编译行为和硬件执行策略仍由所选后端负责。
因此,Layer 的跨后端能力应被理解为一种软件接口兼容机制,而不是对底层计算系统差异的彻底消除。
结语
Keras 3 的神经网络层系统,是整个框架中连接数学计算与模型构建的关键环节。
其核心设计可以概括为:通过 Layer 抽象统一封装计算逻辑、权重和内部状态;通过 build() 与 call() 明确参数创建和前向执行的边界;通过标准网络层提供常见神经网络结构;通过子类化与递归组合支持复杂模型开发。
从底层计算的角度看,Layer 是一组由张量运算构成的数学变换。
从软件工程的角度看,Layer 则是一个具有明确生命周期、状态管理能力和组合接口的计算组件。
这两种视角共同构成了 Keras 神经网络层系统的设计基础。
理解 Layer 之后,就能够进一步理解 Keras 如何将多个网络层连接成完整模型,以及如何管理模型的输入输出、网络拓扑和参数集合。
在下一篇文章中,我们将介绍 Keras 的模型构建层,重点分析 Model 基类、Sequential API、Functional API 与 Model Subclassing 三种建模方式,并讨论不同建模方式在模型结构表达、复用能力及工程维护方面的差异。
- 点赞
- 收藏
- 关注作者
评论(0)