Keras 3 架构解析(五):数据处理系统——从数据加载到高性能输入管道
Keras 3 架构解析(五):数据处理系统——从数据加载到高性能输入管道
引言
在前四篇文章中,我们分别介绍了 Keras 3 的基础计算层、神经网络层、模型构建层与训练系统。
基础计算层负责提供统一的张量运算能力;神经网络层将数学变换与参数状态封装为可复用的计算组件;模型构建层将这些组件组织成完整的神经网络;训练系统则通过损失函数、优化器和训练循环,实现模型参数的迭代更新。
至此,我们已经建立了一套能够定义和训练神经网络的基本框架。
然而,模型训练的实际执行效率并不只取决于神经网络本身。
一个深度学习系统需要持续从存储设备读取原始数据,将其转换为符合模型要求的数值表示,再按照一定的顺序组织成批次,最终传递给计算设备。
如果这一过程无法跟上模型的计算速度,那么即使使用性能很高的 GPU 或 TPU,计算设备也可能因为等待数据而处于空闲状态。
与此同时,数据处理过程还直接影响模型的统计性质。
例如,数据标准化是否使用了验证集的统计信息,时间序列窗口是否包含未来数据,训练样本的随机采样方式是否改变了原始数据分布,都可能对模型训练结果产生实质性影响。
因此,数据处理不仅是将数据读取到内存中的辅助过程,也是深度学习系统中具有独立职责的重要组成部分。
Keras 3 在这一领域延续了其多后端架构设计。
它没有要求开发者必须使用某一种数据加载框架,而是通过统一的数据适配机制,使训练系统能够接收 NumPy 数组、TensorFlow Dataset、PyTorch DataLoader 和 Keras PyDataset 等多种数据来源。
这一设计使模型计算后端与数据加载框架之间形成相对独立的关系。
本文将从软件架构与计算执行两个角度出发,系统介绍 Keras 3 的数据处理系统,包括数据加载、数据预处理、批处理、随机采样、时间序列构建以及高性能输入管道等核心机制。
一、数据处理系统的基本架构
1.1 数据处理系统的职责
在深度学习框架中,数据处理系统(Data Pipeline)负责将原始数据转换为训练系统能够消费的输入。
其基本流程可以概括为:
原始数据
│
▼
数据加载
│
▼
数据解析
│
▼
数据预处理
│
▼
样本构建
│
▼
随机采样与排序
│
▼
批次构建
│
▼
预取与数据传输
│
▼
Keras Model
│
▼
Training Loop
这里的流程是一种概念性划分,并不意味着所有数据管道都必须严格按照上述顺序执行。
例如,某些数据管道会先将样本组织成 Batch,再执行向量化预处理。另一些数据管道则会在解码之后立即缓存数据,以避免重复执行昂贵的转换操作。
实际的操作顺序需要根据数据类型、计算成本和内存约束确定。
从职责上看,数据处理系统主要解决五个问题。
第一,数据从哪里来。
原始数据可能位于内存、磁盘文件、数据库、对象存储或网络服务中。
第二,如何转换数据。
原始数据通常需要经过解码、类型转换、数值标准化、特征提取或其他转换过程,才能成为模型输入。
第三,如何组织样本。
对于时间序列,需要构建滑动窗口;对于文本,需要处理不同长度的序列;对于图像,需要统一空间维度。
第四,如何组织训练批次。
模型通常以 Batch 为单位执行训练,因此需要确定采样顺序、Batch Size 和不完整批次的处理方式。
第五,如何保证数据供应效率。
数据处理过程应尽可能与模型计算并行,避免计算设备因等待数据而闲置。
这五个问题共同构成数据处理系统的主要职责。
1.2 数据处理与模型计算的边界
一个重要的架构原则是:数据管道负责准备训练样本,模型负责定义样本上的数学计算。
例如,假设有一个时间序列预测模型:
[
\hat{y}=f_\theta(X)
]
其中:
[
X\in\mathbb{R}^{B\times T\times F}
]
这里:
- (B):批大小;
- (T):时间序列长度;
- (F):每个时间步的特征数量。
数据管道负责将原始记录组织成形状为 ((B,T,F)) 的张量。
模型则负责对这些张量进行卷积、注意力计算或其他神经网络运算。
这种划分使模型无需了解底层数据究竟来自 CSV、Parquet、数据库还是远程对象存储。
只要最终生成的张量满足模型的输入契约,就可以复用相同的模型结构。
但这种分离并不是绝对的。
例如,数值标准化既可以在数据管道中完成,也可以通过 Keras 的预处理层放入模型内部。
因此,数据预处理与模型计算之间存在一个需要主动设计的边界。
二、Keras 3 的数据输入抽象
2.1 Keras 并不要求统一的数据加载引擎
Keras 3 的核心设计,是将高层训练接口与具体数据框架解耦。
其内置训练接口主要支持以下数据来源:
| 数据来源 | 主要特点 | 典型用途 |
|---|---|---|
| NumPy Array | 简单、直接、支持内存访问 | 小型数据集、原型实验 |
tf.data.Dataset |
支持数据流变换、缓存、并行映射与预取 | 大规模训练、复杂输入管道 |
| PyTorch DataLoader | 支持自定义 Dataset、采样器和多进程加载 | 自定义文件读取、PyTorch 数据生态 |
keras.utils.PyDataset |
以 Batch 为单位定义数据访问 | Python 自定义批处理逻辑 |
Keras 通过内部的数据适配机制,将这些不同形式的数据接入统一的训练流程。
例如:
model.fit(train_data)
这里的 train_data 可以是 NumPy 数组与标签的组合,也可以是支持的数据集迭代对象。
Keras 会根据数据来源选择对应的处理方式。
这一机制使 Model 不需要直接依赖数据加载框架的实现细节。
2.2 数据加载框架与计算后端可以不同
Keras 3 的多后端能力不仅体现在模型计算层,也体现在数据输入层。
例如,以下组合在 Keras 3 中是受支持的:
| Keras 计算后端 | 数据管道 | 支持情况 |
|---|---|---|
| TensorFlow | tf.data.Dataset |
支持 |
| TensorFlow | PyTorch DataLoader | 支持 |
| JAX | tf.data.Dataset |
支持 |
| JAX | PyTorch DataLoader | 支持 |
| PyTorch | tf.data.Dataset |
支持 |
| PyTorch | PyTorch DataLoader | 支持 |
例如,可以使用 JAX 作为模型计算后端:
import os
os.environ["KERAS_BACKEND"] = "jax"
import keras
然后使用 TensorFlow 构建数据管道:
import tensorflow as tf
dataset = tf.data.Dataset.from_tensor_slices(
(x_train, y_train)
)
dataset = dataset.batch(64)
model.fit(dataset, epochs=10)
只要运行环境安装了所需依赖,Keras 就可以将这个 TensorFlow Dataset 提供的数据交给使用 JAX 后端的模型训练。
但需要特别强调:
数据输入兼容不意味着不同框架之间的张量可以在所有情况下直接混合运算。
例如,在非 TensorFlow 后端中,不能假定任意 Keras Layer 都可以直接作为 tf.data.Dataset.map() 的转换函数。
数据适配接口负责将数据交给模型,并不意味着底层计算框架已经被统一。
此外,跨框架数据适配可能带来数据转换与内存管理开销。因此,数据框架的选择仍然需要根据实际性能测试决定。
2.3 数据批次的标准结构
在常规监督学习中,Keras 通常接收三种形式的数据批次。
第一种仅包含输入:
x_batch
主要适用于模型推理,或由自定义训练逻辑负责其他目标的场景。
第二种包含输入和标签:
(x_batch, y_batch)
这是监督学习中最常见的形式。
第三种包含输入、标签和样本权重:
(
x_batch,
y_batch,
sample_weight_batch
)
其中,sample_weight_batch 用于控制不同样本对训练损失的贡献。
对于多输入模型,x_batch 还可以是列表、元组或字典等嵌套结构。
例如:
x_batch = {
"time_features": time_data,
"static_features": static_data
}
这一设计使数据管道能够直接匹配 Functional API 构建的多输入模型。
三、NumPy:最简单的数据输入方式
3.1 NumPy 数组的基本使用
对于能够全部存放在内存中的数据集,NumPy 是最简单的输入方式。
例如:
import numpy as np
rng = np.random.default_rng(42)
x_train = rng.normal(
size=(1000, 16)
).astype("float32")
y_train = rng.normal(
size=(1000, 1)
).astype("float32")
这里:
[
X\in\mathbb{R}^{1000\times16}
]
[
Y\in\mathbb{R}^{1000\times1}
]
然后直接执行:
model.fit(
x_train,
y_train,
batch_size=64,
epochs=10,
shuffle=True
)
在这种模式下,Keras 负责根据数组生成训练批次。
开发者不需要创建额外的数据迭代器。
3.2 NumPy 的优势与局限
NumPy 的主要优势是简单。
数据已经存储在连续或可访问的内存结构中,便于检查形状、数据类型及具体数值。
对于原型实验、小规模数据集或调试工作,它能够显著降低系统复杂度。
但当数据规模较大时,这种方式会遇到明显限制。
首先,整个数据集可能无法放入内存。
其次,如果训练样本需要复杂的实时解码或数据增强,提前生成全部数据可能导致较高的存储和内存开销。
最后,NumPy 数组本身不提供完整的异步文件读取、并行解码和后台预取体系。
因此,在数据量较大或预处理成本较高时,通常需要采用更加完善的数据管道。
3.3 validation_split 的行为
对于 NumPy 等支持直接索引的数据,Keras 可以使用:
model.fit(
x_train,
y_train,
validation_split=0.2,
epochs=10
)
自动划分一部分数据作为验证集。
但这里存在一个重要细节。
Keras 的 validation_split 会在训练数据被打乱之前,从数据末尾划分指定比例的样本。
因此,它并不等价于随机分层抽样,也不保证验证集具有与训练集相同的类别分布。
对于按时间排列的数据,这种行为可能符合顺序划分的直觉,但仍需要检查标签构造、时间窗口和特征统计是否存在数据泄漏。
在正式实验中,更推荐显式构建训练集和验证集,再通过 validation_data 传入。
四、tf.data:声明式数据处理管道
4.1 tf.data 的基本思想
tf.data 是 TensorFlow 提供的数据处理系统。
它允许开发者通过组合多个数据转换操作,构建能够持续输出训练样本的数据管道。
虽然 tf.data 属于 TensorFlow 生态,但 Keras 3 可以在不同计算后端下接收 tf.data.Dataset 对象。
其核心思想是,将数据加载与转换过程组织为一系列可组合的操作。
常见操作包括:
| 操作 | 作用 |
|---|---|
from_tensor_slices() |
根据数组或张量构建 Dataset |
map() |
对元素执行转换 |
filter() |
过滤不符合条件的元素 |
shuffle() |
通过缓冲区打乱元素顺序 |
batch() |
将多个元素组成批次 |
repeat() |
重复数据流 |
take() |
截取指定数量元素 |
cache() |
缓存部分计算结果 |
prefetch() |
提前准备后续元素 |
interleave() |
交错读取多个数据来源 |
这些操作通常以链式方式组合。
4.2 构建基本数据管道
例如:
import tensorflow as tf
train_ds = tf.data.Dataset.from_tensor_slices(
(x_train, y_train)
)
train_ds = (
train_ds
.shuffle(
buffer_size=1000,
seed=42
)
.batch(64)
.prefetch(tf.data.AUTOTUNE)
)
这段代码依次完成三个操作。
首先,通过 shuffle() 打乱样本顺序。
其次,通过 batch() 将样本组织为批次。
最后,通过 prefetch() 提前准备后续批次。
模型可以直接使用:
model.fit(
train_ds,
epochs=10
)
由于 Dataset 已经完成批处理,因此通常不需要再次向 fit() 传入 batch_size。
4.3 map():数据转换
map() 用于对 Dataset 中的元素应用转换函数。
例如:
def preprocess(x, y):
x = tf.cast(x, tf.float32)
x = x / 255.0
return x, y
然后:
train_ds = train_ds.map(
preprocess,
num_parallel_calls=tf.data.AUTOTUNE
)
这种方式适合执行图像归一化、特征类型转换和其他可以表达为 TensorFlow 操作的数据预处理。
其中,num_parallel_calls 用于控制转换操作的并行执行。
设置为 tf.data.AUTOTUNE 后,TensorFlow 可以根据运行情况自动选择相应的并行度。
但需要注意,自动调节并不保证在所有情况下都具有最优性能。
实际效果仍然取决于 CPU 数量、数据大小、转换操作的复杂度及其他系统负载。
4.4 cache():避免重复处理
如果某些数据转换成本较高,但其结果在不同 Epoch 中保持不变,可以考虑缓存转换结果。
例如:
train_ds = (
train_ds
.map(
preprocess,
num_parallel_calls=tf.data.AUTOTUNE
)
.cache()
.shuffle(1000)
.batch(64)
.prefetch(tf.data.AUTOTUNE)
)
这种方式使确定性的预处理结果能够被重复使用。
但缓存会消耗内存或磁盘空间。
如果数据集过大,直接使用内存缓存可能造成内存不足。
此外,缓存位置也会影响数据行为。
如果在随机增强之后进行缓存,就可能将某一次生成的随机增强结果固定下来,导致后续 Epoch 无法获得新的增强样本。
因此,通常应将确定性且重复计算成本较高的操作放在缓存之前,而将需要每轮变化的随机操作放在缓存之后。
这是一种常见设计原则,而不是适用于所有任务的固定规则。
4.5 prefetch():数据供应与模型计算重叠
prefetch() 是高性能数据管道的重要组成部分。
在没有预取时,训练过程可能表现为:
准备 Batch 1
│
▼
计算 Batch 1
│
▼
准备 Batch 2
│
▼
计算 Batch 2
此时,数据准备与模型计算基本串行执行。
如果引入预取机制,则可以形成类似的流水线:
时间 ─────────────────────────────►
数据准备: Batch 1 | Batch 2 | Batch 3
──────────────────
模型计算: Batch 1 | Batch 2 | Batch 3
在理想情况下,数据准备与模型计算可以部分重叠。
假设:
[
T_{\text{data}}
]
表示准备一个 Batch 的耗时,
[
T_{\text{compute}}
]
表示模型处理一个 Batch 的耗时。
如果两者完全串行:
[
T_{\text{step}}
\approx
T_{\text{data}}+T_{\text{compute}}
]
如果能够充分重叠:
[
T_{\text{step}}
\approx
\max(T_{\text{data}},T_{\text{compute}})
]
实际系统通常介于两者之间。
因此,预取并不能消除数据处理成本,而是通过并行化减少模型计算对数据准备的等待。
五、PyTorch Dataset 与 DataLoader
5.1 Dataset 与 DataLoader 的职责
PyTorch 将数据系统划分为两个主要组件。
Dataset 负责定义如何访问数据。
DataLoader 则负责组织数据采样、批处理和并行加载。
这种分工与 Keras PyDataset 的设计存在明显区别。
在典型的 PyTorch Dataset 中:
dataset[index]
返回的是单个样本。
而 DataLoader 负责将多个样本组合成 Batch。
5.2 自定义 Dataset
例如:
import torch
class CustomDataset(torch.utils.data.Dataset):
def __init__(self, x, y):
self.x = torch.as_tensor(x)
self.y = torch.as_tensor(y)
def __len__(self):
return len(self.x)
def __getitem__(self, index):
return (
self.x[index],
self.y[index]
)
这里:
__len__()返回样本数量;__getitem__()返回指定索引对应的样本。
该类只负责定义数据访问规则,不负责模型训练。
5.3 使用 DataLoader 构建批次
train_dataset = CustomDataset(
x_train,
y_train
)
train_loader = torch.utils.data.DataLoader(
train_dataset,
batch_size=64,
shuffle=True,
num_workers=0
)
之后可以直接用于 Keras:
model.fit(
train_loader,
epochs=10
)
即使当前 Keras 使用的是 JAX 或 TensorFlow 后端,该 DataLoader 仍可以作为输入来源。
这体现了 Keras 3 对数据框架与计算后端的解耦。
5.4 多进程加载
对于文件读取或预处理成本较高的数据集,可以配置:
train_loader = torch.utils.data.DataLoader(
train_dataset,
batch_size=64,
shuffle=True,
num_workers=4,
persistent_workers=True,
prefetch_factor=2
)
其中:
num_workers:数据加载进程数量;persistent_workers:是否在迭代结束后保留工作进程;prefetch_factor:每个工作进程提前准备的批次数量。
在使用多进程加载时,开发者还需要考虑操作系统的进程启动方式、Dataset 对象的可序列化性以及多进程资源开销。
例如,在 Windows 环境中使用多进程 DataLoader,通常需要遵循 Python 多进程的入口保护要求。
此外,更多的 Worker 并不必然带来更高吞吐量。
如果数据已经驻留在内存中,而且转换操作非常简单,多进程通信和数据复制产生的开销可能超过并行处理带来的收益。
因此,Worker 数量应通过实际测试确定。
5.5 内存固定与 GPU 数据传输
PyTorch DataLoader 还提供:
pin_memory=True
该参数用于将支持的数据放置在页锁定内存中,从而改善部分 CPU 到 CUDA GPU 的数据传输行为。
但这并不是一个对所有后端都有效的通用加速开关。
对于使用 Keras 3 跨后端数据适配的场景,数据可能仍需经过额外的转换步骤。
因此,是否启用 pin_memory,需要根据实际设备、计算后端和数据传输路径进行判断。
六、Keras PyDataset:以 Batch 为中心的数据接口
6.1 PyDataset 的设计目标
除了使用外部数据框架,Keras 还提供了自己的数据集抽象:
keras.utils.PyDataset
它允许开发者使用 Python 定义数据访问方式,并通过 Keras 内置训练接口消费这些数据。
与 PyTorch Dataset 不同,PyDataset 的核心抽象是 Batch。
其 __getitem__() 应当返回一个完整批次,而不是单个样本。
同样,__len__() 应当返回批次数量,而不是样本数量。
6.2 自定义 PyDataset
下面实现一个基于 NumPy 数组的数据集。
import math
import numpy as np
import keras
class ArrayDataset(keras.utils.PyDataset):
def __init__(
self,
x,
y,
batch_size=64,
shuffle=True,
seed=42,
**kwargs
):
super().__init__(**kwargs)
self.x = x
self.y = y
self.batch_size = batch_size
self.shuffle = shuffle
self.rng = np.random.default_rng(seed)
self.indices = np.arange(len(x))
self.on_epoch_end()
def __len__(self):
return math.ceil(
len(self.x) / self.batch_size
)
def __getitem__(self, index):
start = index * self.batch_size
end = start + self.batch_size
batch_indices = self.indices[start:end]
return (
self.x[batch_indices],
self.y[batch_indices]
)
def on_epoch_end(self):
if self.shuffle:
self.rng.shuffle(self.indices)
该实现具有几个重要特点。
首先,数据访问以 Batch 为单位。
其次,样本顺序通过内部索引数组控制,而不需要直接修改原始数据。
再次,on_epoch_end() 可以在每轮训练结束后重新打乱样本顺序。
最后,Batch Size 由 Dataset 自身管理。
6.3 接入 Keras 训练系统
train_dataset = ArrayDataset(
x_train,
y_train,
batch_size=64,
shuffle=True,
workers=4
)
model.fit(
train_dataset,
epochs=10,
shuffle=False
)
这里,数据批次顺序由 PyDataset 的内部逻辑负责,因此在 fit() 中设置 shuffle=False,避免引入另一层不必要的批次顺序控制。
需要注意,workers、use_multiprocessing 和 max_queue_size 主要影响 Keras 在 fit() 中消费 PyDataset 的方式。
直接通过 Python 循环访问 PyDataset,并不会因为设置了多个 Worker 就自动获得相同的并行执行能力。
6.4 PyDataset 与 DataLoader 的区别
| 特征 | Keras PyDataset | PyTorch Dataset + DataLoader |
|---|---|---|
| 基本访问单位 | Batch | 通常是单个样本 |
| 批处理逻辑 | 在 PyDataset 内定义 | 通常由 DataLoader 负责 |
| 自定义采样 | 通过索引和相关方法实现 | 可通过 Sampler 等机制实现 |
| 并行加载 | Keras 管理相关 Worker | DataLoader 管理 Worker |
| 与 Keras 集成 | 原生支持 | 通过数据适配接口支持 |
| 适合场景 | 自定义批次生成 | 自定义样本读取与采样流程 |
二者都能用于复杂数据加载任务。
选择哪一种,主要取决于现有代码生态和数据访问方式,而不是某一种接口在所有场景下具有绝对优势。
七、数据预处理系统
7.1 为什么需要预处理
模型通常要求输入数据满足明确的数值范围、数据类型和张量形状。
但原始数据往往不满足这些要求。
例如:
- 图像像素可能是 0 至 255 的整数;
- 数值特征可能具有不同的量纲;
- 类别特征可能是字符串;
- 文本可能包含可变长度的字符序列;
- 时间序列可能存在缺失值、异常值和不同采样频率。
因此,在训练之前,需要将这些原始表示转换为模型能够使用的特征。
Keras 通过预处理层(Preprocessing Layers)提供了部分标准化能力。
常见组件包括:
| 预处理类型 | 代表组件 |
|---|---|
| 数值标准化 | Normalization |
| 数值离散化 | Discretization |
| 类别编码 | CategoryEncoding |
| 字符串映射 | StringLookup |
| 整数类别映射 | IntegerLookup |
| 文本向量化 | TextVectorization |
| 图像缩放 | Rescaling |
| 图像尺寸调整 | Resizing |
| 图像随机增强 | RandomFlip、RandomRotation 等 |
这些组件属于 Keras Layer 体系,但其用途主要是数据转换,而不是一般意义上的可训练特征提取。
7.2 数值标准化
数值标准化是常见的预处理操作。
一种典型方法是 Z-score 标准化:
[
z=\frac{x-\mu}{\sigma}
]
其中:
- (x):原始特征;
- (\mu):该特征的均值;
- (\sigma):该特征的标准差。
Keras 提供:
normalizer = keras.layers.Normalization(
axis=-1
)
然后使用:
normalizer.adapt(x_train)
估计训练数据的均值和方差。
最后:
x_normalized = normalizer(x_train)
即可得到标准化后的数据。
adapt() 会计算并保存所需要的统计信息,之后可以在训练和推理阶段重复使用。
7.3 数据标准化与信息泄漏
标准化存在一个容易被忽视的问题。
假设数据集包含训练集、验证集和测试集。
如果先在整个数据集上计算均值和标准差,再进行数据划分,就可能使训练阶段间接利用验证集和测试集的信息。
即使标准化本身没有直接使用目标标签,这种处理方式也可能造成评估偏差。
因此,通常应遵循:
使用训练集估计预处理统计量,再将相同的转换规则应用于验证集和测试集。
例如:
normalizer = keras.layers.Normalization()
# 仅根据训练集计算统计量
normalizer.adapt(x_train)
# 使用同一个预处理层
train_x = normalizer(x_train)
val_x = normalizer(x_val)
test_x = normalizer(x_test)
对于时间序列任务,这一要求尤其重要。
如果模型在历史时间点进行预测,那么标准化参数不应依赖未来时期才能观察到的数据。
7.4 类别编码
某些特征并不是连续数值,而是类别。
例如:
red
blue
green
神经网络通常不能直接对这些字符串执行常规数值计算。
Keras 提供 StringLookup,将字符串映射为整数索引。
lookup = keras.layers.StringLookup(
vocabulary=[
"red",
"green",
"blue"
]
)
之后可以将类别转换为索引。
如果需要进一步生成 One-Hot 或 Multi-Hot 表示,则可以结合其他类别编码组件。
需要指出,类别编码方案会影响模型输入的统计含义。
例如,将无序类别直接映射为整数后作为连续数值输入,可能使模型错误地将类别编号之间的大小关系视为具有实际意义。
因此,类别编码应根据模型架构和变量语义进行设计。
7.5 文本向量化
文本数据需要从字符或单词序列转换为数值表示。
Keras 提供:
vectorizer = keras.layers.TextVectorization(
max_tokens=10000,
output_mode="int",
output_sequence_length=128
)
该层可以执行文本标准化、分词、词表映射与序列长度处理等操作。
词表既可以由外部直接提供,也可以通过 adapt() 从训练文本中学习。
但需要注意,TextVectorization 内部使用 TensorFlow。
因此,在非 TensorFlow 后端中,不能将其视为能够无条件嵌入已编译模型计算图的普通跨后端 Layer。
官方更推荐在需要跨后端使用时,将其放在模型外部的输入预处理管道中。
这一限制说明:Keras 的统一接口并不代表所有预处理组件都具有完全相同的跨后端执行能力。
八、预处理应该放在模型内部还是外部
8.1 模型内部预处理
一种方式是直接将预处理层作为模型的一部分。
例如:
normalizer = keras.layers.Normalization()
normalizer.adapt(x_train)
model = keras.Sequential([
keras.Input(shape=(16,)),
normalizer,
keras.layers.Dense(
64,
activation="relu"
),
keras.layers.Dense(1)
])
这种方式的优势是,数据标准化规则与模型一同组织。
训练和推理可以复用相同的转换逻辑,从而降低预处理不一致的风险。
在模型保存与恢复时,支持序列化的预处理层也可以与模型结构共同保存。
但这种方式意味着预处理操作会成为模型前向计算的一部分。
对于某些计算成本较高、可以预先完成的预处理操作,这未必是性能最优的选择。
8.2 模型外部预处理
另一种方式是在数据管道中完成预处理。
例如:
train_ds = train_ds.map(
preprocess,
num_parallel_calls=tf.data.AUTOTUNE
)
这种方式的优势是,可以利用数据管道的并行处理、缓存和预取机制。
例如,CPU 可以在模型执行 GPU 计算时,提前处理下一批输入数据。
但需要注意,如果预处理规则完全存在于模型外部,那么在部署模型时,也必须同时保存和复现这些转换步骤。
否则,训练输入与推理输入可能不一致。
8.3 两种方式的比较
| 维度 | 模型内部预处理 | 模型外部预处理 |
|---|---|---|
| 训练与推理一致性 | 较容易保证 | 需要额外管理 |
| 模型整体封装 | 较完整 | 较依赖外部流程 |
| CPU 并行预处理 | 取决于后端和执行方式 | 更容易利用专用管道 |
| 数据缓存 | 需要额外设计 | 可直接使用管道缓存 |
| 部署复杂度 | 可能较低 | 需要复现预处理 |
| 跨后端限制 | 需要关注 Layer 兼容性 | 可使用独立数据处理工具 |
实际工程中,不必强行选择某一种模式。
例如,可以将轻量级且需要严格保持一致的标准化操作放入模型内部,而将昂贵的文件解码与数据清洗放在外部数据管道中。
这种分工往往更符合实际系统需求。
九、Batching:批处理与张量组织
9.1 Batch 的数学意义
在深度学习训练中,模型通常不会每次只处理一个样本,而是同时处理多个样本。
设单个输入样本形状为:
[
X_i\in\mathbb{R}^{T\times F}
]
将 (B) 个样本组成一个批次后:
[
X_{\text{batch}}
\in
\mathbb{R}^{B\times T\times F}
]
其中,(B) 是 Batch Size。
这样,模型就可以利用矩阵和张量运算,同时处理多个样本。
对适合并行执行的模型而言,批处理能够提高计算设备的利用效率,并减少逐样本调度带来的开销。
9.2 Batch Size 与训练性能
Batch Size 会同时影响计算性能与优化行为。
较大的 Batch 可能具有以下特点:
- 更充分利用矩阵计算单元;
- 减少每个样本对应的调度开销;
- 提高部分硬件上的计算吞吐量;
- 增加输入张量和中间激活值的内存需求。
较小的 Batch 则可能降低单次计算的内存需求,并改变梯度估计的统计性质。
因此,Batch Size 不是纯粹的数据加载参数。
它同时影响神经网络的优化过程。
在实际训练中,需要结合模型结构、设备内存、计算效率和训练收敛行为进行选择。
9.3 不完整 Batch
假设数据集包含 1,000 个样本,Batch Size 为 64。
那么:
[
1000=15\times64+40
]
数据集可以形成 15 个完整批次和 1 个包含 40 个样本的批次。
对于 tf.data,可以通过:
dataset.batch(
64,
drop_remainder=False
)
保留最后一个不完整批次。
如果设置:
dataset.batch(
64,
drop_remainder=True
)
则最后的 40 个样本会被丢弃。
保留不完整批次能够利用更多数据,但最后一个 Batch 的形状会有所不同。
对于某些依赖静态形状的编译或分布式执行场景,固定 Batch 大小可能更有利于简化计算图和设备调度。
但直接丢弃样本也会改变每轮训练使用的数据集合。
因此,是否丢弃不完整批次,应根据训练需求和执行环境决定。
9.4 可变长度数据的批处理
并非所有样本都具有相同形状。
例如,自然语言文本可能包含不同数量的 Token。
时间序列也可能具有不同的有效长度。
为了形成统一的 Batch,通常需要执行填充(Padding)。
例如:
序列 A:1 2 3 4 5
序列 B:6 7 8
可以填充为:
序列 A:1 2 3 4 5
序列 B:6 7 8 0 0
但填充值不能被无条件当作真实数据。
因此,许多模型还需要使用 Mask 标记无效位置。
例如:
序列 A:1 1 1 1 1
序列 B:1 1 1 0 0
其中,1 表示有效位置,0 表示填充位置。
Keras 的部分网络层支持 Mask 传播和处理机制。
但如果模型包含自定义 Layer,就需要确认这些 Layer 是否正确处理了 Mask。
否则,模型可能将填充数据纳入注意力计算、序列聚合或损失统计,导致结果偏差。
十、随机采样与数据顺序
10.1 为什么需要 Shuffle
对于许多独立同分布的监督学习任务,随机打乱训练样本有助于降低原始数据排列顺序对梯度更新的影响。
例如,如果分类数据按照类别连续存储:
AAAAAA...
BBBBBB...
CCCCCC...
并按原始顺序生成 Batch,那么相邻批次可能包含高度集中的类别分布。
通过随机打乱,能够使不同类别更充分地混合在训练批次中。
但随机打乱并不是所有任务都适用的固定要求。
例如,对于具有因果或时间顺序约束的序列预测任务,必须区分样本顺序与样本内部的时间顺序。
10.2 tf.data.shuffle() 的缓冲区机制
TensorFlow 提供:
dataset.shuffle(
buffer_size=1000,
seed=42
)
这里的 buffer_size 控制随机打乱缓冲区的大小。
需要注意,shuffle() 并不总是将整个数据集一次性加载并执行全局随机排列。
当缓冲区小于数据集规模时,它通常只能在有限范围内随机选择输出元素。
因此:
dataset.shuffle(buffer_size=100)
与:
dataset.shuffle(buffer_size=100000)
可能产生不同程度的随机混合效果。
更大的缓冲区通常能够提高随机混合程度,但也需要更多内存。
如果数据集非常大,就需要在随机性与资源消耗之间进行权衡。
10.3 采样与数据分布
随机打乱与随机采样并不是同一个概念。
Shuffle 主要改变现有样本的排列顺序。
Sampling 则可能改变样本被选中的概率。
例如,对于类别不平衡数据,可以使用加权采样,使某些少数类别样本更频繁地出现。
但这种策略也会改变训练过程中观察到的类别分布。
如果训练数据分布与实际部署数据分布不同,就需要进一步考虑模型输出概率的校准问题。
因此,采样策略应被视为训练算法的一部分,而不只是数据加载的实现细节。
十一、时间序列数据管道
11.1 时间序列与普通独立样本的区别
时间序列任务具有明确的时间依赖关系。
对于一个按时间排列的数据集:
[
X={x_0,x_1,\ldots,x_{N-1}}
]
模型可能需要根据过去 (T) 个时间步预测未来的目标。
例如:
[
X_i=
[x_i,x_{i+1},\ldots,x_{i+T-1}]
]
对应目标:
[
y_i=x_{i+T}
]
因此,原始序列必须经过窗口化(Windowing),才能形成监督学习样本。
11.2 Keras 的时间序列工具
Keras 提供:
keras.utils.timeseries_dataset_from_array()
用于从数组构建时间序列滑动窗口。
例如:
import numpy as np
import keras
values = np.arange(
100,
dtype="float32"
).reshape(-1, 1)
sequence_length = 12
dataset = keras.utils.timeseries_dataset_from_array(
data=values[:-1],
targets=values[sequence_length:],
sequence_length=sequence_length,
sequence_stride=1,
batch_size=16,
shuffle=False
)
该示例中,第一个训练样本为:
[
X_0=[0,1,\ldots,11]
]
对应目标:
[
y_0=12
]
第二个训练样本为:
[
X_1=[1,2,\ldots,12]
]
对应目标:
[
y_1=13
]
这里,targets[i] 对应从位置 (i) 开始的输入窗口所需的预测目标。
因此,构造数据时必须明确目标数组与输入窗口之间的索引对应关系。
如果预测的是未来第 (H) 个时间步,而不是紧接着窗口之后的一个时间步,就需要进一步调整目标的偏移。
11.3 时间序列窗口与数据泄漏
时间序列任务中,一个常见错误是先对整个数据集构建全部窗口,再随机划分训练集和验证集。
由于相邻窗口可能共享大量时间步,这种划分方式可能导致训练集与验证集之间出现高度重叠。
例如:
[
X_0=[x_0,\ldots,x_{11}]
]
[
X_1=[x_1,\ldots,x_{12}]
]
两者共享 11 个时间步。
如果随机将其中一个窗口放入训练集,将另一个放入验证集,那么验证集就不再代表严格的未来时期预测。
更严重的是,如果窗口构建没有正确约束目标时间,训练样本还可能直接包含未来信息。
因此,对于时间序列预测,通常应优先根据时间划分训练期、验证期与测试期,再按照相应的信息边界构建样本。
例如:
历史时间 ─────────────────────────────►
训练期 验证期 测试期
─────────────│──────────│──────────
│ │
Train │ Val │ Test
其中,预处理统计量、特征计算和标签构造都必须遵守相同的信息可用性约束。
11.4 多实体时间序列
对于包含多个独立实体的数据,例如不同设备、不同用户或不同股票,时间序列窗口还需要遵守实体边界。
假设数据按以下方式排列:
A:t0 t1 t2 ... t99
B:t0 t1 t2 ... t99
如果直接将两段数据拼接,再使用滑动窗口函数,可能产生跨实体窗口:
A:t97 t98 t99
B:t0 t1 t2
这样的窗口通常不具有正确的时间序列语义。
因此,应当先根据实体分组,再分别生成窗口,最后将有效窗口组合为训练数据集。
如果模型还需要处理交易日、会话边界或设备重启等特殊分段,也应将这些边界纳入窗口构建规则。
Keras 的通用时间序列工具负责窗口化操作,但不会自动理解这些业务约束。
十二、高性能数据管道的设计原则
12.1 数据管道可能成为训练瓶颈
一个训练步骤的整体耗时可以概念性地表示为:
[
T_{\text{step}}
T_{\text{data}}
+
T_{\text{compute}}
+
T_{\text{overhead}}
]
其中:
- (T_{\text{data}}):数据读取和处理耗时;
- (T_{\text{compute}}):模型计算耗时;
- (T_{\text{overhead}}):调度与其他系统开销。
这一表达式不考虑并行重叠。
在实际系统中,数据准备与计算可能同时发生,因此各部分耗时并不总能简单相加。
如果数据处理速度低于模型消耗数据的速度,那么计算设备就会等待后续 Batch。
因此,训练性能优化需要同时分析数据管道和模型计算。
12.2 并行文件读取
当训练数据分布在大量文件中时,逐个串行读取文件可能成为瓶颈。
tf.data 可以通过 interleave() 组织多个数据源的交错读取。
PyTorch DataLoader 则可以利用多个 Worker 并行读取样本。
但并行读取的收益取决于底层存储系统。
例如,对于高延迟远程对象存储,适当提高读取并发可能显著增加吞吐量。
但对于单块已经达到吞吐上限的本地磁盘,继续提高并发数量可能只会增加 I/O 竞争。
因此,Worker 数量与文件布局应结合存储特征进行优化。
12.3 向量化预处理
对于简单的数值转换,逐个样本调用 Python 函数可能产生较高的解释器与函数调用开销。
例如,对每个样本独立执行标准化:
for sample in samples:
sample = (sample - mean) / std
可能不如对整个批次直接执行数组运算:
batch = (batch - mean) / std
后者可以更充分地利用 NumPy、TensorFlow 或其他底层计算库的向量化能力。
因此,对于可向量化的预处理操作,通常应尽量减少 Python 层逐元素循环。
这也是为什么在某些场景中,先 Batch 再执行 map(),可能比逐样本转换具有更低的调度成本。
但对于需要独立文件解码或无法合并处理的操作,仍可能需要逐样本执行。
12.4 缓存与预取的内存成本
缓存、Shuffle 和 Prefetch 都可能维护内部缓冲区。
假设每个 Batch 的平均大小为 (S),预取队列包含 (Q) 个 Batch。
那么,仅预取数据就可能需要近似:
[
M_{\text{prefetch}}\approx Q\times S
]
的缓冲内存。
例如,每个 Batch 占用 32 MiB,如果预取 8 个 Batch,那么仅这些批次的数据就可能占用约 256 MiB。
此外,还需要考虑解码缓冲区、Worker 进程、数据转换中间结果和模型本身的内存使用。
因此,增加预取深度和 Worker 数量并不总是合理。
对于内存受限的训练环境,过多的预取数据可能造成频繁内存回收、交换或进程终止,反而降低整体效率。
12.5 数据管道性能的衡量方式
在性能分析中,应当区分三个指标。
第一,原始数据读取吞吐量。
例如:
[
\text{MB/s}
]
衡量单位时间内从存储系统读取的数据量。
第二,样本处理吞吐量。
例如:
[
\text{Samples/s}
]
衡量单位时间内生成多少个可供模型使用的样本。
第三,训练吞吐量。
例如:
[
\text{Training Samples/s}
]
衡量模型实际完成多少个样本的训练计算。
这三个指标并不一定相同。
例如,某个数据管道可能具有很高的磁盘读取吞吐量,但由于大量时间消耗在解码或特征构建阶段,最终提供给模型的样本数量仍然较少。
因此,只测量磁盘读取速度不足以判断整个数据管道是否高效。
更合理的方式是分别测量数据读取、预处理、Batch 构建和模型训练,并对比是否存在明显的等待阶段。
十三、完整示例:使用 tf.data 训练 Keras 回归模型
下面通过一个完整示例,将数据生成、数据划分、预处理、批处理和模型训练连接起来。
为了使示例能够独立运行,使用人工生成的回归数据。
13.1 环境与数据生成
import os
# 在导入 Keras 之前设置后端
os.environ["KERAS_BACKEND"] = "tensorflow"
import numpy as np
import tensorflow as tf
import keras
from keras import layers
rng = np.random.default_rng(42)
# 构建 3000 个样本,每个样本 12 个特征
x = rng.normal(
size=(3000, 12)
).astype("float32")
# 生成真实参数
true_w = rng.normal(
size=(12, 1)
).astype("float32")
# 构建回归目标
noise = rng.normal(
scale=0.1,
size=(3000, 1)
).astype("float32")
y = x @ true_w + noise
# 显式划分训练与验证集
x_train = x[:2400]
y_train = y[:2400]
x_val = x[2400:]
y_val = y[2400:]
这里使用 TensorFlow 后端,是为了使示例的依赖关系更加直接。
如果使用 JAX 后端,只要运行环境具备对应的框架及设备依赖,也可以将相同的 tf.data.Dataset 接入 Keras 训练系统。
13.2 构建数据管道
BATCH_SIZE = 64
train_ds = tf.data.Dataset.from_tensor_slices(
(x_train, y_train)
)
train_ds = (
train_ds
.shuffle(
buffer_size=len(x_train),
seed=42,
reshuffle_each_iteration=True
)
.batch(BATCH_SIZE)
.prefetch(tf.data.AUTOTUNE)
)
val_ds = tf.data.Dataset.from_tensor_slices(
(x_val, y_val)
)
val_ds = (
val_ds
.batch(BATCH_SIZE)
.prefetch(tf.data.AUTOTUNE)
)
训练数据使用 Shuffle,而验证数据保持确定性的顺序。
虽然对于某些简单的逐样本评价指标而言,验证顺序不影响最终结果,但保持验证集输入稳定通常更有利于实验复现与问题排查。
13.3 构建预处理层
normalizer = layers.Normalization(
axis=-1
)
# 只使用训练集估计统计量
normalizer.adapt(x_train)
该层根据训练集计算每个特征的均值与方差。
随后将其作为模型的一部分。
13.4 构建模型
model = keras.Sequential([
keras.Input(shape=(12,)),
normalizer,
layers.Dense(
64,
activation="relu"
),
layers.Dense(
32,
activation="relu"
),
layers.Dense(1)
])
这种结构使标准化操作成为模型计算流程的一部分。
因此,训练和验证阶段都会使用相同的预处理统计量。
13.5 配置并执行训练
model.compile(
optimizer=keras.optimizers.Adam(
learning_rate=1e-3
),
loss=keras.losses.MeanSquaredError(),
metrics=[
keras.metrics.MeanAbsoluteError()
]
)
history = model.fit(
train_ds,
validation_data=val_ds,
epochs=20,
verbose=2
)
在这个示例中,完整执行流程为:
NumPy 原始数据
│
▼
训练数据划分
│
┌────────┴────────┐
│ │
▼ ▼
Train Data Val Data
│ │
▼ │
Normalization.adapt() │
│ │
▼ ▼
tf.data tf.data
│ │
▼ ▼
Shuffle Batch
│ │
▼ ▼
Batch Prefetch
│ │
▼ │
Prefetch │
│ │
└────────┬────────┘
▼
Keras Model
│
▼
Normalization
│
▼
Dense
│
▼
Training
这里,adapt() 只在训练开始前执行一次,用于估计标准化统计量。
训练和验证时,模型会使用已经确定的标准化参数,而不是重新从当前 Batch 中估计均值和方差。
13.6 评估与模型保存
训练完成后:
results = model.evaluate(
val_ds,
return_dict=True
)
print(results)
可以通过相同的数据管道执行评估。
如果需要保存模型:
model.save("regression_model.keras")
由于 Normalization 是模型的一部分,其相关配置与统计状态可以随模型一起保存。
这样,在恢复模型时,就可以复用相同的预处理规则。
不过,原始文件解码、缺失值处理和其他尚未纳入模型的外部数据转换,仍然需要由部署系统自行保证一致性。
十四、数据处理系统中的常见设计问题
14.1 将数据加载与训练逻辑耦合
一个常见的工程问题,是将文件读取、特征转换、批处理、损失计算和优化器更新全部写入同一个训练循环。
这种方式虽然能够工作,但会使数据系统与模型训练系统高度耦合。
结果是,只要数据来源发生变化,就可能需要修改训练代码。
更合理的架构,是让数据管道向训练系统提供具有明确格式的 Batch,而不暴露底层存储细节。
14.2 在训练和推理时使用不同的预处理规则
如果训练时使用某一组均值和方差进行标准化,而推理时重新估计统计量,就会改变模型实际接收到的数据分布。
类似的问题也可能发生在类别编码、缺失值填充及文本词表构建中。
因此,预处理参数应作为模型实验配置的一部分进行持久化管理。
14.3 忽视数据泄漏
数据泄漏不一定表现为直接使用未来标签。
它还可能来自:
- 在完整数据集上拟合标准化参数;
- 在训练前使用未来时间信息生成特征;
- 将高度重叠的时间序列窗口随机分配到不同数据集;
- 在特征选择过程中反复参考测试集表现;
- 在多实体数据中错误地跨越实体或时间边界。
这些问题可能使模型验证结果显著优于真实部署表现。
因此,数据处理系统不仅需要保证计算正确,还必须保证信息可用性符合实际预测场景。
14.4 盲目增加并行度
数据管道中的并行度并不是越高越好。
更多 Worker 可能增加:
- CPU 资源竞争;
- 多进程通信成本;
- 内存占用;
- 文件系统并发压力;
- 数据顺序与复现控制的复杂性。
如果瓶颈不是数据处理,而是模型计算,那么进一步增加数据 Worker 通常无法提高整体吞吐量。
优化前应首先确定实际瓶颈。
14.5 过度依赖高层数据工具
Keras 提供的 image_dataset_from_directory()、text_dataset_from_directory() 和 timeseries_dataset_from_array() 能够简化常见数据集的构建。
例如:
dataset = keras.utils.image_dataset_from_directory(
"images/",
image_size=(224, 224),
batch_size=32
)
可以根据目录结构加载图像分类数据。
但这些工具主要解决通用数据组织问题。
对于复杂业务数据,它们未必能表达所有约束。
例如,多实体时间序列、带有严格时间边界的窗口生成或需要特殊采样规则的数据集,可能更适合通过自定义 Dataset 实现。
因此,使用高层工具的前提,应当是其抽象与任务需求相匹配。
十五、Keras 数据处理系统的架构意义
从整个 Keras 3 框架来看,数据处理系统与基础计算层、模型构建层和训练系统具有相对独立的职责。
可以将它们概括为以下结构:
外部数据系统
┌────────────────────────┐
│ 文件 / 数据库 / 对象存储 │
└────────────┬───────────┘
│
▼
数据加载层
┌────────────────────────┐
│ NumPy │
│ tf.data │
│ PyTorch DataLoader │
│ Keras PyDataset │
└────────────┬───────────┘
│
▼
数据处理层
┌────────────────────────┐
│ Decode / Transform │
│ Window / Sampling │
│ Batch / Prefetch │
└────────────┬───────────┘
│
▼
Keras Model
┌────────────────────────┐
│ Layers / Ops │
│ Forward Computation │
└────────────┬───────────┘
│
▼
Training Loop
┌────────────────────────┐
│ Loss / Gradient │
│ Optimizer / Metrics │
└────────────┬───────────┘
│
▼
计算后端与硬件
┌────────────────────────┐
│ JAX / TensorFlow │
│ PyTorch │
│ CPU / GPU / TPU │
└────────────────────────┘
该图表示逻辑职责关系,而不是严格的执行调用顺序。
数据管道可能使用 TensorFlow 或 PyTorch 等框架完成一部分数值计算,同时由另一个计算后端执行模型训练。
从软件架构角度看,这种设计具有三个重要意义。
第一,数据来源与模型结构解耦。
无论训练数据来自内存数组、本地文件还是远程对象存储,只要数据管道能够提供满足模型输入要求的 Batch,就可以复用相同的 Model。
第二,数据处理与训练算法解耦。
数据管道可以独立优化文件读取、预处理、缓存和并行执行,而不需要直接修改模型的损失函数或优化器。
第三,数据处理性能成为独立的优化对象。
开发者可以分别分析数据准备吞吐量与模型计算吞吐量,识别真正制约系统效率的环节。
但需要强调,数据管道与训练系统在性能上仍然存在紧密联系。
Batch Size、张量形状、采样顺序和数据精度等因素,不仅影响数据处理,也可能改变模型的计算效率与训练行为。
因此,架构上的职责分离,并不代表性能优化可以完全孤立进行。
更合理的方式,是在保持接口边界清晰的同时,对整个训练链路进行统一测量。
结语
Keras 3 的数据处理系统,主要解决原始数据如何被组织为可供神经网络高效使用的训练输入。
与基础计算层不同,Keras 并不试图将所有数据处理操作统一为某个独立的底层执行引擎。
相反,它通过数据适配接口,允许开发者根据需求选择 NumPy、TensorFlow Dataset、PyTorch DataLoader 或 Keras PyDataset。
这种架构保留了不同数据生态系统的能力,同时使上层 Model 和 Training Loop 能够使用相对统一的接口。
从功能上看,数据处理系统主要包含数据加载、预处理、样本构建、随机采样、批处理和预取等环节。
从性能上看,它决定了计算设备能否持续获得足够的数据,从而影响整体训练吞吐量。
从统计学角度看,它决定了模型实际观察到的数据分布,也关系到验证结果是否具有可信度。
因此,数据管道不应仅仅被视为训练之前的准备工作,而应当被视为深度学习系统中需要独立设计、测试和维护的重要组成部分。
对于较小的数据集,直接使用 NumPy 可能是最简单且足够有效的方案。
对于复杂的数据转换和高吞吐量训练,可以考虑使用 tf.data 或 PyTorch DataLoader。
对于需要完全控制批次生成逻辑的任务,则可以使用 Keras PyDataset 或其他自定义数据系统。
最终选择应由数据规模、处理复杂度、硬件环境和实际性能测试决定,而不是单纯根据框架名称判断。
至此,我们已经介绍了 Keras 3 的基础计算层、神经网络层、模型构建层、训练系统和数据处理系统。
这五个部分共同构成了从原始数据到模型参数更新的完整技术链路。
但在实际训练过程中,还需要对训练生命周期进行进一步控制。
例如,如何在验证指标不再改善时停止训练,如何定期保存检查点,如何动态调整学习率,以及如何将训练日志输出到监控系统。
这些功能并不直接属于模型计算或数据处理,而是由训练控制与监控系统负责。
在下一篇文章中,我们将介绍 Keras 的 Callback 机制,分析训练事件、生命周期管理、EarlyStopping、ModelCheckpoint、LearningRateScheduler 及自定义 Callback 的实现方式,并讨论它们如何与训练循环协同工作。
- 点赞
- 收藏
- 关注作者
评论(0)