当前位置: 首页 > news >正文

Python数据持久化:pickle与npy格式的读写指南与实战

1. 项目概述:为什么我们需要pickle和npy?

在Python的数据处理、机器学习或者科学计算项目中,我们经常面临一个核心问题:如何高效地保存和加载中间数据或最终模型?你可能会说,用文本文件(如.txt.csv.json)不就行了?确实,文本文件通用性强,可读性好,但在处理大规模数值数组(比如一个包含百万个浮点数的矩阵)或复杂的Python对象(比如一个自定义的类实例,里面包含了方法、属性和其他嵌套对象)时,文本格式就显得力不从心了。效率低下、存储空间浪费、无法保存对象状态是文本格式的硬伤。

这时,pickle.npy格式就闪亮登场了,它们是Python生态中专为高效序列化而生的“利器”。简单来说,pickle是Python的“通用储蓄罐”,几乎能保存任何Python对象的状态;而.npy是NumPy库的“专用集装箱”,专门为存储多维数组数据而优化,速度极快,空间占用小。

我见过不少新手朋友,在需要保存一个训练好的Scikit-learn模型或者一个庞大的NumPy数组时,还在纠结于如何用循环把数据写入文本文件,或者尝试用json去序列化一个不支持的类型,结果到处报错。其实,掌握pickle.npy的读写,是Python数据科学工作流中一项非常基础且关键的技能,能让你在数据持久化这一步上既省心又高效。

2. 核心工具解析:pickle与npy的定位与差异

在动手写代码之前,我们必须先搞清楚这两个工具各自擅长什么,以及它们之间的核心区别。混用或者选错工具,可能会导致数据丢失、加载失败或者性能瓶颈。

2.1 pickle:Python对象的通用序列化器

pickle模块是Python标准库的一部分,它的设计目标非常明确:将内存中的Python对象转换(序列化)为一个字节流,并可以反向将这个字节流还原(反序列化)为原来的对象。这个过程可以理解为将对象“腌制”起来存放,之后再“解腌”恢复原样。

它的核心优势在于“通用性”:

  • 几乎支持所有类型:列表、字典、集合、元组、自定义类的实例、函数(仅序列化函数名和模块引用,而非字节码)、甚至打开的文件句柄(不推荐)等。
  • 保存对象状态:对于自定义类的实例,pickle会保存实例的属性值。如果类定义了__getstate____setstate__方法,你还可以自定义序列化的过程。
  • 递归处理:对象内部嵌套的其他对象也会被自动序列化。

但它也有明显的局限性:

  • Python专属pickle格式是Python特有的,其他编程语言(如Java、C++)无法直接读取。这意味着它不适合作为跨语言交换数据的格式。
  • 安全风险:永远不要反序列化来自不受信任来源的pickle数据!因为pickle在反序列化时会执行字节码,恶意构造的数据可能导致任意代码执行。
  • 版本依赖:不同Python版本间序列化的数据可能不兼容。用Python 3.8pickle的数据,不一定能用Python 3.12完美加载,尤其是涉及底层C扩展的对象时。

2.2 npy:NumPy数组的高效存储格式

.npy格式是NumPy库原生提供的二进制格式,专门用于存储单个NumPy数组(ndarray)。它的设计哲学是“简单、高效、专一”。

它的核心优势在于“性能”和“精确”:

  • 读写速度极快:因为是二进制格式,且针对数组的内存布局进行了优化,读写速度远超文本格式,也通常比pickle存储纯数组要快。
  • 存储空间小:直接保存数组的二进制数据,没有额外的格式开销(如逗号、引号、缩进)。
  • 自描述性.npy文件头包含了数组的元数据,如数据类型(dtype)、形状(shape)、字节顺序(endianness)等。这意味着你不需要额外文件来说明数组的结构,加载时一切信息都已就位。
  • 跨平台兼容性好:只要使用NumPy,在不同操作系统和架构上都能正确读取,解决了字节序等问题。

它的局限性也很明显:

  • 仅支持NumPy数组:它只能存ndarray。如果你想存一个字典,里面包含几个数组和一些字符串参数,单靠.npy就不行了。不过NumPy提供了.npz格式来存储多个数组(可以理解为多个.npy的压缩包)。
  • 功能单一:它就是为存数组而生,没有pickle那样复杂的对象序列化能力。

选择指南:

  • 当你需要保存一个或多个纯粹的NumPy数组,并且追求极致的I/O性能时,首选.npy.npz这是机器学习中保存特征矩阵、权重参数的黄金标准。
  • 当你需要保存一个复杂的、包含多种非数组类型(如字典、列表、自定义模型对象)的Python对象时,必须使用pickle例如,保存一个完整的Scikit-learn模型(包含预处理器、估计器、超参数等)。

注意:对于机器学习模型,pickle是通用方案,但许多库(如joblib, Scikit-learn推荐)在pickle基础上做了优化,更适合存储包含大量NumPy数组的对象(如大型模型参数)。但原理相通。

3. 实战代码详解:pickle的读取与写入

理论说清楚了,我们直接上代码。我会逐行添加注释,并穿插在实际项目中积累的经验和容易踩的坑。

3.1 使用pickle写入文件

假设我们有一个复杂的实验配置和结果需要保存。

import pickle import numpy as np # 假设我们有一个自定义的简单类,模拟一个训练配置 class TrainingConfig: def __init__(self, model_name, lr, epochs): self.model_name = model_name self.learning_rate = lr self.epochs = epochs def display(self): print(f"Model: {self.model_name}, LR: {self.learning_rate}, Epochs: {self.epochs}") # 创建一些要保存的数据 config = TrainingConfig("ResNet50", 0.001, 100) # 一个NumPy数组,可以是特征、标签或模型权重 important_array = np.random.randn(100, 50) # 一个普通的Python字典,记录一些元信息 metadata = {'dataset': 'ImageNet', 'author': 'John', 'date': '2023-10-27'} # 我们甚至可以把它们打包进一个字典或列表里 data_to_save = { 'config': config, 'data_array': important_array, 'meta': metadata } # 方法1:使用 pickle.dump 直接写入文件 (最常用) # 以二进制写入模式打开文件,'wb' 中的 'b' 至关重要,pickle操作的是字节 with open('experiment_data.pkl', 'wb') as f: # 将对象 data_to_save 序列化并写入文件对象 f # protocol 参数指定pickle协议版本,版本越高通常越高效,但兼容的Python版本可能越新 # protocol=4 (Python 3.4+) 支持大对象, protocol=5 (Python 3.8+) 支持带外数据(如内存映射数组) pickle.dump(data_to_save, f, protocol=pickle.HIGHEST_PROTOCOL) print("数据已使用 pickle.dump 保存至 experiment_data.pkl") # 方法2:使用 pickle.dumps 序列化为字节对象,再自行写入 # 这在需要将pickle数据存入数据库、通过网络发送或进行其他内存操作时有用 serialized_bytes = pickle.dumps(data_to_save, protocol=pickle.HIGHEST_PROTOCOL) # 例如,我们可以把这些字节存到文件 with open('experiment_data_bytes.pkl', 'wb') as f: f.write(serialized_bytes) print("数据已序列化为字节并保存。")

关键点与避坑指南:

  1. 文件模式必须是二进制(‘wb’/‘rb’):这是新手最容易出错的地方。用文本模式(‘w’/‘r’)打开会导致编码错误,因为pickle生成的是字节,不是文本。
  2. 协议版本(protocol)pickle.HIGHEST_PROTOCOL通常是最佳选择,它会自动使用你当前Python版本支持的最高效协议。如果你需要确保数据能被旧版Python读取,可以指定一个较低的协议(如protocol=2兼容Python 2.3+和3.0+),但会牺牲一些性能和存储效率。
  3. 处理大对象:如果你要序列化的对象非常大(比如一个巨大的字典列表),可能会遇到内存问题。pickle本身会一次性将对象读入内存。对于超大对象,可以考虑使用picklePicklerUnpickler进行流式处理,或者将大对象(如数组)分离出来用.npy存储。
  4. 自定义类的序列化:确保你的自定义类在反序列化的环境中是可导入的(即类的定义必须存在)。pickle不存储类本身的代码,只存储类名、模块名和实例属性。如果反序列化时找不到类定义,会引发AttributeError

3.2 使用pickle读取文件

现在,我们来加载刚才保存的数据。

import pickle import sys import os # 在尝试加载前,确保之前定义的 TrainingConfig 类在当前作用域可用。 # 如果这段代码在另一个文件运行,你需要重新定义或导入这个类。 # 这是pickle反序列化自定义类的关键前提! try: from __main__ import TrainingConfig # 假设类定义在同一个文件的全局作用域 except ImportError: # 如果是从另一个模块运行,可能需要直接定义或从模块导入 # 这里我们简单处理,因为本例中类已定义 pass # 方法1:使用 pickle.load 直接从文件加载 file_path = 'experiment_data.pkl' if not os.path.exists(file_path): print(f"错误:文件 {file_path} 不存在!") sys.exit(1) with open(file_path, 'rb') as f: # 注意是 'rb' 模式 # 从文件对象 f 中反序列化出对象 loaded_data = pickle.load(f) print("=== 从文件直接加载的数据 ===") print(f"数据类型: {type(loaded_data)}") print(f"数据键: {loaded_data.keys() if isinstance(loaded_data, dict) else 'N/A'}") # 访问并验证数据 if 'config' in loaded_data: loaded_config = loaded_data['config'] # 检查是否成功恢复了类实例 print(f"加载的 config 类型: {type(loaded_config)}") if hasattr(loaded_config, 'display'): loaded_config.display() # 调用对象的方法 if 'data_array' in loaded_data: print(f"加载的数组形状: {loaded_data['data_array'].shape}") print(f"数组前三个元素: {loaded_data['data_array'][0, :3]}") if 'meta' in loaded_data: print(f"元数据: {loaded_data['meta']}") # 方法2:从字节加载 with open('experiment_data_bytes.pkl', 'rb') as f: file_bytes = f.read() loaded_data_from_bytes = pickle.loads(file_bytes) print("\n=== 从字节加载的数据 ===") # 可以进行类似的验证... print("验证通过,数据一致。")

关键点与避坑指南:

  1. 类定义必须存在:这是加载包含自定义类实例的pickle文件时最常见的错误。错误信息通常是AttributeError: Can‘t get attribute ‘TrainingConfig’ on <module ‘__main__’ from ...>。解决方法有两种:
    • (推荐)将类定义放在独立的模块中:在序列化和反序列化的代码中都通过import导入。这样pickle记录的模块名就是真实的模块名(如mymodule.TrainingConfig),而不是__main__
    • 在反序列化前重新定义类:确保类定义的代码在pickle.load()之前被执行。对于简单的脚本可以,但对于复杂项目不推荐,容易导致定义不一致。
  2. 安全警告再强调pickle.load()pickle.loads()永远不要用于加载来源不明、不受信任的数据。这等同于执行未知代码。
  3. 处理版本兼容性:如果数据是用更高版本的pickle协议保存的,而你的Python环境较旧,加载时会报错。通常的解决方案是升级Python环境,或者在保存时使用兼容性更好的低版本协议。

4. 实战代码详解:npy的读取与写入

对于纯粹的NumPy数组操作,.npy格式更加简洁高效。NumPy提供了两个主要函数:np.savenp.load

4.1 使用np.save写入.npy文件

import numpy as np # 创建一些示例数组 # 一个浮点型二维数组 matrix = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]], dtype=np.float32) # 一个一维整数数组 vector = np.arange(10, dtype=np.int64) # 一个三维随机数组 tensor = np.random.rand(4, 5, 6) print("原始数组信息:") print(f"matrix - dtype: {matrix.dtype}, shape: {matrix.shape}") print(f"vector - dtype: {vector.dtype}, shape: {vector.shape}") print(f"tensor - dtype: {tensor.dtype}, shape: {tensor.shape}") # 保存单个数组到 .npy 文件 # np.save 会自动添加 .npy 扩展名,如果文件名没有的话 np.save('my_matrix.npy', matrix) np.save('my_vector', vector) # 保存为 my_vector.npy print("单个数组已保存。") # 保存多个数组到单个 .npz 文件(压缩存档) # .npz 文件内部包含多个独立的 .npy 文件 np.savez('my_archive.npz', mat=matrix, vec=vector, ten=tensor) # 你也可以使用压缩格式以节省磁盘空间,尤其适合稀疏或重复数据多的数组 np.savez_compressed('my_compressed_archive.npz', mat=matrix, vec=vector, ten=tensor) print("多个数组已保存至 .npz 文件(包含普通和压缩版本)。")

关键点与避坑指南:

  1. 自动扩展名np.save(‘filename‘, arr)会保存为filename.npy。如果你传入‘filename.npy‘,它也会正确保存。但为了清晰,建议统一加上.npy扩展名。
  2. 数据类型(dtype)保持.npy格式会精确保存数组的dtype。一个np.float32的数组加载回来依然是np.float32,不会像某些文本读取方式那样默认转为float64。这对于内存敏感的应用(如嵌入式或移动端部署)非常重要。
  3. .npz 文件np.saveznp.savez_compressed用于保存多个数组。它们通过关键字参数来命名数组(如mat=matrix)。加载时,这些名字就是键。压缩版本可以显著减小文件体积,特别是对于稀疏矩阵或有很多重复值的数组,但写入和读取时会稍微增加一些CPU时间。
  4. 内存映射(memmap):对于远超内存大小的巨型数组,可以使用np.save保存后,再用np.load(‘file.npy‘, mmap_mode=‘r‘)进行内存映射式读取。这样你可以像操作普通数组一样操作磁盘上的数据,而无需一次性全部加载到内存。

4.2 使用np.load读取.npy文件

读取操作非常简单直观。

import numpy as np # 1. 加载单个 .npy 文件 loaded_matrix = np.load('my_matrix.npy') print("=== 加载的单个数组 ===") print(f"数据: \n{loaded_matrix}") print(f"类型: {type(loaded_matrix)}") print(f"dtype: {loaded_matrix.dtype}, shape: {loaded_matrix.shape}") # 验证数据一致性 print(f"与原始数据是否完全一致: {np.array_equal(loaded_matrix, matrix)}") # 2. 加载 .npz 文件 # np.load 对于 .npz 文件返回的是一个类似字典的 NpzFile 对象 archive = np.load('my_archive.npz') compressed_archive = np.load('my_compressed_archive.npz') print("\n=== 加载 .npz 存档(普通)===") # 查看存档中包含哪些数组 print(f"存档中的键: {list(archive.keys())}") # 通过键名访问数组 loaded_mat_from_archive = archive['mat'] loaded_vec_from_archive = archive['vec'] loaded_ten_from_archive = archive['ten'] print(f"mat shape: {loaded_mat_from_archive.shape}") print(f"vec shape: {loaded_vec_from_archive.shape}") print(f"ten shape: {loaded_ten_from_archive.shape}") # 验证压缩存档的数据 print("\n=== 验证压缩存档数据一致性 ===") print(f"mat 是否一致: {np.array_equal(archive['mat'], compressed_archive['mat'])}") print(f"vec 是否一致: {np.array_equal(archive['vec'], compressed_archive['vec'])}") # 3. 使用内存映射模式加载大文件(示例) # 假设 ‘large_array.npy‘ 是一个非常大的文件 # loaded_large = np.load(‘large_array.npy‘, mmap_mode=‘r‘) # ‘r‘ 只读, ‘r+‘ 读写, ‘c‘ 拷贝时写 # print(loaded_large[0:100]) # 只读取前100个元素到内存 # 4. 重要:关闭 .npz 文件对象 # 对于 .npz 文件,使用完毕后显式关闭是一个好习惯,尤其是在写入后立即读取的场景。 archive.close() compressed_archive.close() print("\n.npz 文件已关闭。")

关键点与避坑指南:

  1. .npz 文件的行为np.load(‘.npz‘)返回的不是直接的数组,而是一个NpzFile对象。你需要像字典一样通过键(即保存时用的名字)来访问具体的数组。这个对象在关闭前,数组数据可能还保存在磁盘缓存中。
  2. 关闭 .npz 文件:虽然Python的垃圾回收最终会处理,但显式调用.close()方法或在with语句中使用是更好的实践,可以及时释放系统资源。注意,对于单个的.npy文件,np.load直接返回数组,不存在关闭的问题。
    # 使用 with 语句自动管理 .npz 文件资源 with np.load('my_archive.npz') as data: my_array = data['mat'] # 离开 with 块后,文件会自动关闭
  3. 路径问题:确保文件路径正确。如果文件不在当前工作目录,需要使用绝对路径或相对路径。
  4. 数据类型确认:加载后,务必检查数组的dtypeshape是否符合预期,这是数据流水线中防止错误传播的重要一步。

5. 混合使用与高级场景

在实际项目中,我们经常需要混合使用这两种格式,以达到最佳效果。

5.1 场景:保存和加载机器学习模型与数据

一个典型的场景是保存一个训练好的机器学习模型。模型本身可能是一个复杂的对象(如Scikit-learn的Pipeline),而特征矩阵和标签是大的NumPy数组。

策略:

  • 模型对象:使用picklejoblib.dump保存。
  • 大型数据数组:使用.npy.npz保存。
import pickle import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.datasets import make_classification # 1. 生成模拟数据并训练一个简单模型 X, y = make_classification(n_samples=1000, n_features=20, random_state=42) model = LogisticRegression(max_iter=200) model.fit(X, y) # 2. 分别保存模型和数据集 # 保存模型对象 (复杂对象,用pickle) with open('trained_logistic_model.pkl', 'wb') as f: pickle.dump(model, f, protocol=pickle.HIGHEST_PROTOCOL) # 保存训练数据 (大型数组,用.npy/.npz) np.savez('training_dataset.npz', features=X, labels=y) print("模型和数据已分别保存。") # 3. 加载并验证 with open('trained_logistic_model.pkl', 'rb') as f: loaded_model = pickle.load(f) with np.load('training_dataset.npz') as data: loaded_X = data['features'] loaded_y = data['labels'] # 使用加载的模型对加载的数据进行预测(验证) predictions = loaded_model.predict(loaded_X[:5]) print(f"加载模型对前5个样本的预测: {predictions}") print(f"实际标签: {loaded_y[:5]}") accuracy = loaded_model.score(loaded_X, loaded_y) print(f"模型在完整数据上的准确率: {accuracy:.4f}")

5.2 场景:自定义类中包含NumPy数组

如果你的自定义类实例属性里有大的NumPy数组,直接对整个实例进行pickle可能会效率较低。你可以通过定义__getstate____setstate__方法来优化。

import pickle import numpy as np class LargeDataContainer: def __init__(self, metadata, large_array): self.metadata = metadata # 字典或字符串 self.large_array = large_array # 巨大的NumPy数组 def __getstate__(self): """定义对象如何被序列化。""" # 我们可以选择将数组保存为单独的文件,只保存路径 # 但这里演示另一种思路:返回状态字典,pickle会处理它。 # 对于真的非常大的数组,更好的方法是分离存储。 state = self.__dict__.copy() # 如果需要对数组做特殊处理(如压缩),可以在这里进行 # 例如:state['large_array'] = compress_array(self.large_array) return state def __setstate__(self, state): """定义对象如何被反序列化。""" # 如果序列化时做了处理,这里需要反向操作 # 例如:state['large_array'] = decompress_array(state['large_array']) self.__dict__.update(state) # 使用 big_array = np.random.rand(10000, 10000) # 假设这是一个大数组 container = LargeDataContainer({'desc': 'big data'}, big_array) with open('container.pkl', 'wb') as f: pickle.dump(container, f, protocol=pickle.HIGHEST_PROTOCOL) # 加载时,__setstate__ 会被自动调用 with open('container.pkl', 'rb') as f: loaded_container = pickle.load(f) print(f"元数据: {loaded_container.metadata}") print(f"数组形状: {loaded_container.large_array.shape}")

实操心得:对于极其巨大的数组,更优的架构是将数组本身用np.save保存为.npy文件,而在类中只存储文件路径。在__getstate__中保存路径,在__setstate__或类的方法中惰性加载数组。这避免了pickle文件本身变得巨大。

6. 常见问题与排查技巧实录

在实际使用中,你肯定会遇到各种问题。下面是我总结的一些典型错误和解决方法。

6.1 pickle相关错误

问题1:ModuleNotFoundErrorAttributeError(找不到类)

  • 错误信息ModuleNotFoundError: No module named ‘mymodule‘AttributeError: Can‘t get attribute ‘MyClass‘ on <module ‘__main__’ from ...>
  • 原因:反序列化时,Python找不到对象所属类的定义。常见于自定义类,且序列化与反序列化环境不一致(例如,类定义在脚本中,以__main__执行)。
  • 解决方案
    1. (治本)将类定义放在独立的模块文件中:例如,创建mymodels.py,定义MyClass。在序列化和反序列化的脚本中都使用from mymodels import MyClass。这样pickle存储的模块名是mymodels,而非__main__
    2. 确保类定义在作用域内:在调用pickle.load()之前,确保类的定义代码已经执行。对于简单的单脚本项目,确保类定义在脚本顶部。
    3. 使用dill等第三方库(它比pickle更强大,可以序列化更多类型的对象,但仍有环境依赖)。

问题2:PicklingErrorTypeError(无法序列化的对象)

  • 错误信息PicklingError: Can‘t pickle <function ... at 0x...>: it‘s not the same object as ...TypeError: cannot pickle ‘_thread.lock‘ object
  • 原因:你尝试pickle一个不支持序列化的对象,例如lambda函数(在某些情况下)、文件句柄、线程锁、数据库连接等。
  • 解决方案
    1. 避免序列化这类对象:在序列化前,将这些属性设置为None或可序列化的替代值。例如,在__getstate__方法中排除文件句柄。
    2. 使用__getstate____setstate__:在这两个方法中手动定义哪些状态需要保存/恢复,跳过不可序列化的部分,并在__setstate__后重新初始化它们。

问题3: 文件损坏或版本不兼容

  • 错误信息EOFError,UnpicklingError, 或关于协议版本的警告。
  • 原因:文件被截断、以文本模式打开并修改、或用更高版本的协议保存却用低版本Python加载。
  • 解决方案
    1. 检查文件是否完整。
    2. 确认始终使用二进制模式(‘rb‘/’wb‘)。
    3. 统一序列化和反序列化环境的Python版本和库版本。如果必须跨版本,保存时使用较低的协议(如protocol=2)。

6.2 npy相关错误

问题1:ValueError: Cannot load file containing pickled data

  • 错误信息:当你用np.load去加载一个.pkl文件时。
  • 原因:文件扩展名混淆或文件实质内容错误。np.load期望的是NumPy格式的二进制文件,而不是pickle格式。
  • 解决方案:使用正确的函数。如果是pickle文件,用pickle.load()。确保文件扩展名和内容匹配。

问题2:OSError: Failed to interpret file ‘...‘ as a pickle

  • 错误信息:文件损坏或根本不是有效的.npy/.npz文件。
  • 原因:文件被其他程序修改、下载不完整、或以文本编辑器保存导致编码错误。
  • 解决方案:重新生成或获取原始文件。确保文件传输过程完整。

问题3: 内存不足 (MemoryError)

  • 错误信息:尝试加载一个非常大的.npy文件时。
  • 原因:数组大小超过可用内存。
  • 解决方案:使用内存映射模式加载。
    large_data = np.load(‘huge_array.npy‘, mmap_mode=‘r‘) # 现在 large_data 是一个 memmap 对象,行为类似数组 # 你可以切片操作,只有被访问的部分才会加载到内存 chunk = large_data[0:1000]

6.3 通用最佳实践与技巧

  1. 明确的文件扩展名:使用.pkl.pickle表示pickle文件,用.npy表示单个NumPy数组,用.npz表示多个NumPy数组的存档。这有助于你和其他人一眼识别文件类型。
  2. 版本控制:对于重要的、需要长期保存的数据,在文件内部或通过文件名记录生成该数据的软件版本(如Python版本、NumPy版本、模型版本)。例如:model_v1.2_py38.pkl
  3. 数据校验:保存后,立即进行一次加载验证,比较加载的数据与原始数据是否一致(使用np.array_equal==)。对于关键数据,可以计算并保存一个校验和(如MD5)。
  4. 处理路径:使用os.path.join来构建文件路径,以提高代码的跨平台兼容性。
  5. 异常处理:在读写文件时使用try...except块来捕获可能的IOErrorPermissionError等,使程序更健壮。
  6. 对于生产环境模型部署:考虑使用更专业、跨语言可能性更高的格式,如ONNX(用于模型交换)或HDF5(用于复杂科学数据)。pickle更适合Python内部的、短期的数据持久化。

最后,我个人在实际项目中的习惯是:小型配置、复杂对象用pickle;纯数值型大数据数组用.npy/.npz。在保存模型时,我会同时保存模型的pickle文件和一个包含关键训练指标和超参数的JSON配置文件,这样既保证了模型的完整性,又有了人类可读的元信息。记住,没有一种格式是万能的,根据你的数据特点和用途选择最合适的工具,才是高效编程的关键。

http://www.jsqmd.com/news/1330267/

相关文章:

  • 如何用Compressor.js实现浏览器端图像压缩:完整指南
  • SpringBoot+Vue校园电动车租赁系统开发实战
  • Clockwork for Dynamo:450+节点如何彻底改变你的BIM参数化设计工作流?
  • 抖音去水印教程2026:怎么把视频无水印保存下来?最新方法整理 - 免费软件工具方法教程
  • Godot信号与函数实战:7天打通游戏逻辑的任督二脉
  • 河北园区安保派遣公司哪家好选对团队与调度机制是关键 - 热点品牌推荐
  • 基于MCP协议与OpenClaw构建可插拔AI Agent工具生态的实战指南
  • 基于 Boost MPPT与双PI闭环双向DC/DC的离网光伏储能系统动力学建模及稳态特性分析(Simulink仿真实现)
  • 杭州做 AI-GEO 优化选哪家?本土公司能力分级与避坑指南(2026年8月版) - 品牌测评网
  • 026、BraAttention上下文锚注意力机制在YOLOv12中的即插即用复现——基于PKINet的上下文先验建模与mAP涨点实验
  • NCM文件解密终极指南:3种方法快速解锁网易云音乐加密文件
  • Sass编译全攻略:从命令行到构建工具的四种实战方案
  • 蛋白多因子【抗体芯片】检测技术全解析:原理、优势与应用场景
  • 公众号如何发起投票活动,365 评选免费投票小程序完整操作指南 - 投票评选制作软件系统
  • Linux进程CPU绑定优化:taskset命令详解与实践
  • 2026年Word转PDF全攻略:从电脑自带、免费小程序到在线网站,一文说清 - 软件小管家
  • Windows下搭建JMeter+InfluxDB+Grafana实时性能监控平台
  • CAS单点登录系统:原理、实践与优化指南
  • Godot资源提取工具全解析:从PCK文件结构到实战应用
  • UE5 GAS实战:构建带冷却与消耗的主动技能系统
  • 2026 年惠州靠谱的MJS 工法公司推荐,明明是能解决深基坑难题的硬技能,为啥总被人忽略? - 企业官方推荐【认证】
  • PyAutoGUI截图函数深度解析:从基础使用到异常处理与性能优化
  • Unity3D零基础入门:从C#脚本到3D滚球游戏实战开发
  • Unity游戏模组开发入门:基于MelonLoader的C#注入与Harmony补丁实战
  • UE5 Niagara GPU粒子边界剔除优化:解决视角切换时特效消失问题
  • 浙江光固化陶瓷3D打印机品牌厂商怎么选才靠谱 - 热点品牌推荐
  • 《骑在银龙的背上》歌词深度解析与罗马音跟唱指南
  • 深入解析URP逐对象光照机制:从原理到实战解决灯光消失Bug
  • 2026实测:抖音禁止保存视频怎么下载 无水印教程,附抖音无法直接保存的视频保存方法 - 免费软件工具方法教程
  • Xenomai 3双内核实时Linux系统构建指南:从原理到实践