GridSearchCV指定n_jobs报PicklingError并行异常求解
M1 Mac下GridSearchCV调用Keras模型多进程报PicklingError的解决方案
问题根源
GridSearchCV的并行能力基于joblib实现,默认使用标准库pickle做任务序列化,手动导入dill、pathos不会自动替换其序列化后端,因此无法解决序列化失败问题。- TensorFlow内部大量使用
_LazyLoader延迟加载对象,这类对象不支持标准pickle序列化。M1芯片Mac上Python多进程默认启动模式为spawn,该模式需要将所有任务对象完整序列化后传递给子进程,因此会触发can't pickle _LazyLoader objects报错;x86架构Mac默认使用fork模式,子进程直接继承父进程内存,不会触发这类序列化问题。 tensorflow.keras.wrappers.scikit_learn.KerasClassifier属于TensorFlow已弃用的API,本身对多进程并行场景兼容性较差。
方案1:使用Scikeras替换弃用的Keras封装(推荐,稳定性最高)
Scikeras是scikit-learn官方认可的Keras模型封装库,原生解决了Keras对象的序列化问题,对M1架构适配良好,不需要额外修改多进程配置。
- 先在当前Anaconda环境安装依赖:
conda install -c conda-forge scikeras
如果conda安装慢也可以用pip:pip install scikeras - 替换代码中的对应部分,核心修改点如下:
# 替换原有的KerasClassifier导入 from scikeras.wrappers import KerasClassifier from sklearn.model_selection import GridSearchCV import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, BatchNormalization, MaxPooling2D, Flatten, Dense, Dropout import numpy as np # 模型创建函数不要依赖全局变量,把固定维度作为参数传入 def create_model_SGD(neurons = 1, learning_rate=0.1, momentum=0.0, input_shape=None, no_classes=None): # 如果用TensorFlow-Metal做GPU加速,加这一段避免子进程显存冲突 gpus = tf.config.experimental.list_physical_devices('GPU') if gpus: try: for gpu in gpus: tf.config.experimental.set_memory_growth(gpu, True) except RuntimeError: pass model = Sequential() model.add(Conv2D(32, kernel_size = (3, 3), activation = 'relu', input_shape = input_shape, padding = 'same')) model.add(BatchNormalization()) model.add(MaxPooling2D(pool_size = (2, 2))) model.add(BatchNormalization()) model.add(Conv2D(64, kernel_size = (3, 3), activation = 'relu', padding = 'same')) model.add(BatchNormalization()) model.add(MaxPooling2D(pool_size = (2, 2))) model.add(BatchNormalization()) model.add(Flatten()) model.add(Dense(neurons, activation = 'relu')) model.add(Dropout(rate = 0.2)) model.add(BatchNormalization()) model.add(Dense(no_classes, activation = 'softmax')) model.compile(loss=tf.keras.losses.sparse_categorical_crossentropy, optimizer=tf.keras.optimizers.SGD(learning_rate=learning_rate, momentum=momentum), metrics=['accuracy']) return model seed = 7 tf.random.set_seed(seed) # 初始化封装器时传入固定参数,避免子进程找不到全局变量 model = KerasClassifier( model=create_model_SGD, verbose=0, input_shape=input_shape, # 替换为实际的输入形状 no_classes=no_classes # 替换为实际分类数 ) # 参数网格部分不需要修改 learn_rate = [0.001, 0.01, 0.1] momentum = [0.0, 0.5, 0.9] neurons = [256, 512, 1024] batch_size = [100, 250, 350] epochs = [10, 25, 50] param_grid = dict(neurons=neurons, learning_rate=learn_rate, momentum=momentum, batch_size=batch_size, epochs=epochs) # 直接设置n_jobs=-1即可正常并行 grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3, verbose = 1) grid_result = grid.fit(input_train, target_train)
方案2:保留原有KerasClassifier的兼容方案
如果因项目限制必须使用TensorFlow自带的旧KerasClassifier,可以通过修改多进程启动模式+替换序列化后端解决:
- 在所有代码逻辑最开头(所有import语句之前)强制设置多进程启动模式为fork,绕开spawn的序列化要求:
import multiprocessing multiprocessing.set_start_method('fork', force=True)
- 安装dill序列化库,然后通过joblib指定使用dill作为序列化后端执行fit:
# 先安装dill: pip install dill from joblib import parallel_backend # 原有GridSearchCV初始化逻辑不变 grid = GridSearchCV(estimator=model, param_grid=param_grid, n_jobs=-1, cv=3, verbose = 1) # 指定并行后端和序列化工具 with parallel_backend('loky', serializer='dill'): grid_result = grid.fit(input_train, target_train)
*注意:该方案在部分高版本TensorFlow-Metal环境下可能出现子进程GPU初始化失败的问题,稳定性不如方案1。
避坑提示
- 不要在Jupyter Notebook的交互单元格中直接定义
create_model_SGD函数,spawn模式下子进程无法序列化交互环境定义的函数,最好将模型创建函数写到独立的.py文件中,再在Notebook中导入使用,能大幅降低序列化报错概率。 - 并行数不要设置过高,M1芯片统一内存带宽有限,n_jobs设置为和CPU性能核数一致即可,设置为-1调用所有核心(包括能效核)反而可能因为内存争抢降低运行速度。
内容的提问来源于stack exchange,提问作者user8400607
相关产品推荐
相关产品推荐

