序列化是指将一个数据对象(例如,Python 对象、TensorFlow 模型)转换为一种格式,使其能够存储或传输,并在需要时通过反向的“反序列化”过程来重新创建该对象。
数据序列化有多种格式,如 JSON、XML、HDF5 和 Python 的 pickle,用于不同的目的。例如,JSON 返回人类可读的字符串形式,而 Python 的 pickle 库可以返回字节数组。
在本文中,您将了解如何在 Python 中使用两个常见的序列化库(pickle 和 HDF5)来序列化 Python 中的数据对象(如字典和 TensorFlow 模型),以便进行存储和传输。
完成本教程后,您将了解:
- Python 中的序列化库,例如 pickle 和 h5py
- 序列化 Python 中的字典和 TensorFlow 模型等对象
- 如何使用序列化进行记忆化以减少函数调用
开始您的项目,阅读我的新书《Python for Machine Learning》,书中包含分步教程以及所有示例的Python 源代码文件。
让我们开始吧!
Python 序列化入门指南。图片来自 little plant。部分权利保留
概述
本教程分为四个部分;它们是:
- 什么是序列化,我们为什么要序列化?
- 使用 Python 的 pickle 库
- 在 Python 中使用 HDF5
- 不同序列化方法的比较
什么是序列化,我们为什么需要关心它?
想想如何存储一个整数;您会如何将其存储在文件或传输它?这很简单!我们可以将整数写入文件,然后存储或传输该文件。
但是,如果我们考虑存储一个 Python 对象(例如,一个 Python 字典或 Pandas DataFrame),它具有复杂的结构和许多属性(例如,DataFrame 的列和索引,以及每列的数据类型)呢?您会如何将其存储为文件或传输到另一台计算机?
这就是序列化的用武之地!
序列化是将对象转换为可存储或传输的格式的过程。在传输或存储序列化数据后,我们能够稍后重建对象并获得完全相同的结构/对象,这使我们能够非常方便地在以后继续使用存储的对象,而无需从头开始重建对象。
在 Python 中,有许多不同的序列化格式。哈希表(Python 字典)的一个常见示例是 JSON 文件格式,它跨多种语言通用,人类可读,并允许我们存储字典并以相同的结构重新创建它。但是 JSON 只能存储基本结构,例如列表和字典,并且只能保留字符串和数字。我们无法让 JSON 记住数据类型(例如,numpy float32 与 float64)。它也无法区分 Python 元组和列表。
存在更强大的序列化格式。接下来,我们将探讨 Python 中两个常见的序列化库,即 pickle 和 h5py。
使用 Python 的 Pickle 库
pickle
模块是 Python 标准库的一部分,它实现了序列化(pickling)和反序列化(unpickling)Python 对象的方法。
要开始使用 pickle
,请在 Python 中导入它
1 |
import pickle |
之后,要序列化一个 Python 对象(如字典)并将字节流存储为文件,我们可以使用 pickle 的 dump()
方法。
1 2 3 4 |
test_dict = {"Hello": "World!"} with open("test.pickle", "wb") as outfile: # "wb" 参数以二进制模式打开文件 pickle.dump(test_dict, outfile) |
表示 test_dict
的字节流现在已存储在文件 “test.pickle
” 中!
要恢复原始对象,我们使用 pickle 的 load()
方法从文件中读取序列化字节流。
1 2 |
with open("test.pickle", "rb") as infile: test_dict_reconstructed = pickle.load(infile) |
警告:只反序列化您信任来源的数据,因为在反序列化过程中可能会执行任意恶意代码。
将它们放在一起,以下代码可以帮助您验证 pickle 是否可以恢复相同的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import pickle # 测试对象 test_dict = {"Hello": "World!"} # 序列化 with open("test.pickle", "wb") as outfile: pickle.dump(test_dict, outfile) print("已写入对象", test_dict) # 反序列化 with open("test.pickle", "rb") as infile: test_dict_reconstructed = pickle.load(infile) print("已恢复对象", test_dict_reconstructed) if test_dict == test_dict_reconstructed: print("恢复成功") |
除了将序列化对象写入 pickle 文件外,我们还可以使用 pickle 的 dumps()
函数在 Python 中获取对象序列化为字节数组类型。
1 |
test_dict_ba = pickle.dumps(test_dict) # b'\x80\x04\x95\x15… |
同样,我们可以使用 pickle 的 load 方法将字节数组类型转换回原始对象。
1 |
test_dict_reconstructed_ba = pickle.loads(test_dict_ba) |
pickle 的一个有用之处在于它可以序列化几乎所有 Python 对象,包括用户自定义对象,例如以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import pickle class NewClass: def __init__(self, data): print(data) self.data = data # 创建 NewClass 的一个对象 new_class = NewClass(1) # 序列化和反序列化 pickled_data = pickle.dumps(new_class) reconstructed = pickle.loads(pickled_data) # 验证 print("来自恢复对象的 {}:", reconstructed.data) |
上面的代码将打印以下内容:
1 2 |
1 来自 恢复 对象: 1 |
请注意,类构造函数中的打印语句在调用 pickle.loads()
时不会执行。这是因为它是恢复了对象,而不是重新创建了它。
Pickle 甚至可以序列化 Python 函数,因为函数是 Python 中的一等对象。
1 2 3 4 5 6 7 8 9 10 11 |
import pickle def test(): return "Hello world!" # 序列化和反序列化 pickled_function = pickle.dumps(test) reconstructed_function = pickle.loads(pickled_function) # 验证 print (reconstructed_function()) # 打印 “Hello, world!” |
因此,我们可以利用 pickle 来保存我们的工作。例如,Keras 或 scikit-learn 的训练模型可以通过 pickle 序列化并在以后加载,而不是每次使用时都重新训练模型。以下展示了如何使用 Keras 构建一个 LeNet5 模型来识别 MNIST 手写数字,然后使用 pickle 序列化训练好的模型。之后,我们可以重建模型而无需再次训练,它应该能产生与原始模型完全相同的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
import pickle import numpy as np import tensorflow as tf from tensorflow.keras.datasets import mnist from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, Dense, AveragePooling2D, Dropout, Flatten from tensorflow.keras.utils import to_categorical from tensorflow.keras.callbacks import EarlyStopping # 加载 MNIST 数字 (X_train, y_train), (X_test, y_test) = mnist.load_data() # 重塑数据为 (n_samples, height, width, n_channel) X_train = np.expand_dims(X_train, axis=3).astype("float32") X_test = np.expand_dims(X_test, axis=3).astype("float32") # 对输出进行独热编码 y_train = to_categorical(y_train) y_test = to_categorical(y_test) # LeNet5 模型 model = Sequential([ Conv2D(6, (5,5), input_shape=(28,28,1), padding="same", activation="tanh"), AveragePooling2D((2,2), strides=2), Conv2D(16, (5,5), activation="tanh"), AveragePooling2D((2,2), strides=2), Conv2D(120, (5,5), activation="tanh"), Flatten(), Dense(84, activation="tanh"), Dense(10, activation="softmax") ]) # 训练模型 model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"]) earlystopping = EarlyStopping(monitor="val_loss", patience=4, restore_best_weights=True) model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=100, batch_size=32, callbacks=[earlystopping]) # 评估模型 print(model.evaluate(X_test, y_test, verbose=0)) # 使用 pickle 进行序列化和反序列化 pickled_model = pickle.dumps(model) reconstructed = pickle.loads(pickled_model) # 再次评估 print(reconstructed.evaluate(X_test, y_test, verbose=0)) |
上面的代码将产生如下输出。请注意,原始模型和恢复模型的评估分数在最后两行中完全匹配。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
时期 1/100 1875/1875 [==============================] - 15s 7ms/step - loss: 0.1517 - accuracy: 0.9541 - val_loss: 0.0958 - val_accuracy: 0.9661 时期 2/100 1875/1875 [==============================] - 15s 8ms/step - loss: 0.0616 - accuracy: 0.9814 - val_loss: 0.0597 - val_accuracy: 0.9822 时期 3/100 1875/1875 [==============================] - 16s 8ms/step - loss: 0.0493 - accuracy: 0.9846 - val_loss: 0.0449 - val_accuracy: 0.9853 时期 4/100 1875/1875 [==============================] - 17s 9ms/step - loss: 0.0394 - accuracy: 0.9876 - val_loss: 0.0496 - val_accuracy: 0.9838 Epoch 5/100 1875/1875 [==============================] - 17s 9ms/step - loss: 0.0320 - accuracy: 0.9898 - val_loss: 0.0394 - val_accuracy: 0.9870 Epoch 6/100 1875/1875 [==============================] - 16s 9ms/step - loss: 0.0294 - accuracy: 0.9908 - val_loss: 0.0373 - val_accuracy: 0.9872 Epoch 7/100 1875/1875 [==============================] - 21s 11ms/step - loss: 0.0252 - accuracy: 0.9921 - val_loss: 0.0370 - val_accuracy: 0.9879 Epoch 8/100 1875/1875 [==============================] - 18s 10ms/step - loss: 0.0223 - accuracy: 0.9931 - val_loss: 0.0386 - val_accuracy: 0.9880 Epoch 9/100 1875/1875 [==============================] - 15s 8ms/step - loss: 0.0219 - accuracy: 0.9930 - val_loss: 0.0418 - val_accuracy: 0.9871 Epoch 10/100 1875/1875 [==============================] - 15s 8ms/step - loss: 0.0162 - accuracy: 0.9950 - val_loss: 0.0531 - val_accuracy: 0.9853 Epoch 11/100 1875/1875 [==============================] - 15s 8ms/step - loss: 0.0169 - accuracy: 0.9941 - val_loss: 0.0340 - val_accuracy: 0.9895 Epoch 12/100 1875/1875 [==============================] - 15s 8ms/step - loss: 0.0165 - accuracy: 0.9944 - val_loss: 0.0457 - val_accuracy: 0.9874 Epoch 13/100 1875/1875 [==============================] - 15s 8ms/step - loss: 0.0137 - accuracy: 0.9955 - val_loss: 0.0407 - val_accuracy: 0.9879 Epoch 14/100 1875/1875 [==============================] - 16s 8ms/step - loss: 0.0159 - accuracy: 0.9945 - val_loss: 0.0442 - val_accuracy: 0.9871 Epoch 15/100 1875/1875 [==============================] - 16s 8ms/step - loss: 0.0125 - accuracy: 0.9956 - val_loss: 0.0434 - val_accuracy: 0.9882 [0.0340442918241024, 0.9894999861717224] [0.0340442918241024, 0.9894999861717224] |
虽然 pickle 是一个强大的库,但它仍然有一些限制,无法进行 pickle。例如,实时连接(如数据库连接和打开的文件句柄)不能进行 pickle。这个问题出现的原因是,重建这些对象需要 pickle 重新建立与数据库/文件的连接,而 pickle 无法为您完成这项工作(因为它需要适当的凭据,并且超出了 pickle 的预期范围)。
想开始学习机器学习 Python 吗?
立即参加我为期7天的免费电子邮件速成课程(附示例代码)。
点击注册,同时获得该课程的免费PDF电子书版本。
在 Python 中使用 HDF5
分层数据格式 5 (HDF5) 是一种二进制数据格式。h5py
包是一个 Python 库,它提供了 HDF5 格式的接口。根据 h5py
文档,HDF5 “允许您存储海量数值数据,并轻松地从 Numpy 中操作这些数据。”
HDF5 比其他序列化格式更优越的地方在于它可以将数据存储在类似文件系统的层次结构中。您可以在 HDF5 中存储多个对象或数据集,就像在文件系统中保存多个文件一样。您也可以从 HDF5 中读取特定数据集,就像从文件系统中读取一个文件一样,而无需考虑其他文件。如果您使用 pickle 来完成此操作,则每次加载或创建 pickle 文件时都需要读取和写入所有内容。因此,HDF5 对于无法完全放入内存的大量数据非常有优势。
要开始使用 h5py
,您首先需要安装 h5py
库,可以通过以下方式安装:
1 |
pip install h5py |
或者,如果您使用的是 conda 环境:
1 |
conda install h5py |
然后我们就可以开始创建我们的第一个数据集了!
1 2 3 4 |
import h5py with h5py.File("test.hdf5", "w") as file: dataset = file.create_dataset("test_dataset", (100,), type="i4") |
这会在文件 test.hdf5
中创建一个名为 “test_dataset
” 的新数据集,形状为 (100, ),类型为 int32。h5py
数据集遵循 Numpy 语法,因此您可以进行切片、检索、获取形状等操作,与 Numpy 数组类似。
要检索特定索引
1 |
dataset[0] #检索数据集索引为0的元素 |
要获取数据集从索引 0 到索引 10 的切片
1 |
dataset[:10] |
如果您在 with
语句之外初始化了 h5py
文件对象,请记住也要关闭文件!
要从以前创建的 HDF5 文件中读取,您可以以 “r
”(读取模式)或 “r+
”(读写模式)打开文件。
1 2 3 |
with h5py.File("test.hdf5", "r") as file: print (file.keys()) # 获取文件中数据集的名称 dataset = file["test_dataset"] |
要组织您的 HDF5 文件,您可以使用组。
1 2 3 4 5 6 7 8 |
with h5py.File("test.hdf5", "w") as file: # 在文件中创建新的 group_1 file.create_group("group_1") group1 = file["group_1"] # 在 group1 中创建数据集 group1.create_dataset("dataset1", shape=(10,)) # 访问数据集 dataset = file["group_1"]["dataset1"] |
创建组和文件的另一种方法是指定要创建的数据集的路径,h5py
也会在该路径上创建组(如果它们不存在)。
1 2 3 |
with h5py.File("test.hdf5", "w") as file: # 在 group1 中创建数据集 file.create_dataset("group1/dataset1", shape=(10,)) |
这两个代码片段都会在 group1
尚未创建的情况下创建它,然后在 group1
中创建一个 dataset1
。
HDF5 在 TensorFlow 中
要使用 HDF5 格式在 TensorFlow Keras 中保存模型,我们可以使用模型的 save()
函数,并指定扩展名为 .h5
的文件名,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from tensorflow import keras # 创建模型 model = keras.models.Sequential([ keras.layers.Input(shape=(10,)), keras.layers.Dense(1) ]) model.compile(optimizer="adam", loss="mse") # 使用 .h5 扩展名指定模型 # 应以 HDF5 格式保存 model.save("my_model.h5") |
要加载存储的 HDF5 模型,我们也可以直接使用 Keras 中的函数
1 2 3 4 5 |
... model = keras.models.load_model("my_model.h5") # 检查模型是否已成功重建 print(model.summary) |
我们不希望对 Keras 模型使用 pickle 的一个原因是,我们需要一个更灵活的格式,它不与特定版本的 Keras 绑定。如果升级了我们的 TensorFlow 版本,模型对象可能会发生变化,而 pickle 可能无法提供一个可用的模型。另一个原因是只保留我们模型的基本数据。例如,如果我们查看上面创建的 HDF5 文件 my_model.h5
,我们会看到存储了以下内容:
1 2 3 4 5 6 7 |
/ /model_weights /model_weights/dense /model_weights/dense/dense /model_weights/dense/dense/bias:0 /model_weights/dense/dense/kernel:0 /model_weights/top_level_model_weights |
因此,Keras 只选择了重建模型所必需的数据。一个训练好的模型将包含更多的数据集,即除了 /model_weights/
之外,还有 /optimizer_weights/
。Keras 将相应地重建模型并恢复权重,从而为我们提供一个功能相同的模型。
以上面的例子为例。我们的模型保存在 my_model.h5
中。我们的模型是一个单一的密集层,我们可以通过以下方式提取该层的核(kernel):
1 2 3 4 |
import h5py with h5py.File("my_model.h5", "r") as infile: print(infile["/model_weights/dense/dense/kernel:0"][:]) |
由于我们没有对网络进行任何训练,它将给出初始化该层的随机矩阵。
1 2 3 4 5 6 7 8 9 10 |
[[ 0.6872471 ] [-0.51016176] [-0.5604881 ] [ 0.3387223 ] [ 0.52146655] [-0.6960067 ] [ 0.38258582] [-0.05564564] [ 0.1450575 ] [-0.3391946 ]] |
在 HDF5 中,元数据与数据一起存储。Keras 以 JSON 格式将网络的架构存储在元数据中。因此,我们可以按如下方式重现我们的网络架构:
1 2 3 4 5 6 7 8 9 |
import json import h5py with h5py.File("my_model.h5", "r") as infile: for key in infile.attrs.keys(): formatted = infile.attrs[key] if key.endswith("_config"): formatted = json.dumps(json.loads(formatted), indent=4) print(f"{key}: {formatted}") |
这将输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
backend: tensorflow keras_version: 2.7.0 model_config: { "class_name": "Sequential", "config": { "name": "sequential", "layers": [ { "class_name": "InputLayer", "config": { "batch_input_shape": [ null, 10 ], "dtype": "float32", "sparse": false, "ragged": false, "name": "input_1" } }, { "class_name": "Dense", "config": { "name": "dense", "trainable": true, "dtype": "float32", "units": 1, "activation": "linear", "use_bias": true, "kernel_initializer": { "class_name": "GlorotUniform", "config": { "seed": null } }, "bias_initializer": { "class_name": "Zeros", "config": {} }, "kernel_regularizer": null, "bias_regularizer": null, "activity_regularizer": null, "kernel_constraint": null, "bias_constraint": null } } ] } } training_config: { "loss": "mse", "metrics": null, "weighted_metrics": null, "loss_weights": null, "optimizer_config": { "class_name": "Adam", "config": { "name": "Adam", "learning_rate": 0.001, "decay": 0.0, "beta_1": 0.9, "beta_2": 0.999, "epsilon": 1e-07, "amsgrad": false } } } |
模型配置(即神经网络的架构)和训练配置(即传递给 compile()
函数的参数)以 JSON 字符串形式存储。在上面的代码中,我们使用 json
模块对其进行格式化,以便于阅读。建议将模型保存为 HDF5 而不是仅保存 Python 代码,因为如上所示,它包含了比代码更多关于网络构建方式的详细信息。
不同序列化方法的比较
上面我们看到了 pickle 和 h5py 如何帮助序列化我们的 Python 数据。
我们可以使用 pickle 序列化几乎任何 Python 对象,包括用户定义的对象和函数。但是 pickle 不是语言无关的。你无法在 Python 之外反序列化它。到目前为止,pickle 已经开发了 6 个版本,而较旧的 Python 可能无法使用较新版本的 pickle 数据。
相反,HDF5 是跨平台的,并且可以与其他语言(如 Java 和 C++)很好地配合使用。在 Python 中,h5py
库实现了 Numpy 接口,使其更容易操作数据。数据可以在不同的语言中访问,因为 HDF5 格式仅支持 Numpy 数据类型,如浮点数和字符串。我们不能将 Python 函数等任意对象存储到 HDF5 中。
进一步阅读
如果您想深入了解,本节提供了更多关于该主题的资源。
文章
- C# 编程指南中的序列化,https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/serialization/
- 保存和加载 Keras 模型,https://tensorflowcn.cn/guide/keras/save_and_serialize
库
API
- Tensorflow tf.keras.layers.serialize,https://tensorflowcn.cn/api_docs/python/tf/keras/layers/serialize
- Tensorflow tf.keras.models.load_model,https://tensorflowcn.cn/api_docs/python/tf/keras/models/load_model
- Tensorflow tf.keras.models.save_model,https://tensorflowcn.cn/api_docs/python/tf/keras/models/save_model
总结
在本帖中,您了解了什么是序列化,以及如何在 Python 中使用库来序列化 Python 对象,例如字典和 TensorFlow Keras 模型。您还了解了用于序列化的两个 Python 库(pickle、h5py)的优缺点。
具体来说,你学到了:
- 什么是序列化,以及它有什么用
- 如何在 Python 中开始使用 pickle 和 h5py 序列化库
- 不同序列化方法的优缺点
暂无评论。