Keras自定义Sequential子类:重写fit/predict、保存加载及标签编码实现
我继承了keras.models.Sequential类,重写了fit()和predict()方法,目的是封装sklearn的LabelEncoder,这样调用fit()和predict()时可以直接传入任意标签组成的y数组,无需先将标签转换为[0,1,...,num_classes-1]范围内的整数。
我的实现代码如下:
import numpy as np from keras.models import Sequential from keras.utils import to_categorical from sklearn.preprocessing import LabelEncoder class SuperSequential(Sequential): def __init__(self, **kwargs): super().__init__(**kwargs) self.encoder = LabelEncoder() def fit(self, X: np.ndarray, y: np.ndarray, **kwargs) -> Sequential: y_enc = self.encoder.fit_transform(y) y_enc = to_categorical(y_enc, len(np.unique(y_enc))) return super().fit(X, y_enc) def predict(self, X: np.ndarray) -> np.ndarray: y_pred = super().predict(X) y_pred = np.argmax(y_pred , axis=1) return self.label_encoder.inverse_transform(y_pred)
但这个实现存在问题:我想用keras.models.save_model()保存训练好的模型,再通过keras.models.load_model()加载,但加载后的模型总是基类Sequential,不包含我重写的fit()和predict()方法。
更新:后来发现加载时传入custom_objects参数后,加载出来的确实是SuperSequential类型,但LabelEncoder的拟合状态丢失了:
keras.models.load_model("model_path", custom_objects={"SuperSequential": SuperSequential})
另外我注意到Keras有keras.layers.IntegerLookup这类预处理层,似乎能实现我的需求,但不知道如何将其作为Sequential模型的一部分完成标签编码。
我的具体问题:
- 能否保存并加载
Sequential的子类?如果可以,该怎么做? - 如何用Keras的预处理层(比如
keras.layers.IntegerLookup)实现和子类相同的效果? - 由于相关参考资料较少,我不确定当前思路是否合理,有没有更好的方法实现我的需求?
1. 保存并加载Sequential子类的方法
可以实现,但默认模型保存机制不会自动保存自定义属性(比如你的self.encoder),需要手动实现序列化与反序列化逻辑:
步骤1:重写get_config()方法
在SuperSequential类中添加该方法,保存encoder的拟合状态:
def get_config(self): config = super().get_config() # 保存LabelEncoder的类标签 config["encoder_classes"] = self.encoder.classes_.tolist() return config
步骤2:添加类方法from_config()
从加载的配置中恢复encoder的状态:
@classmethod def from_config(cls, config): # 先恢复父类模型配置 model = super().from_config(config) # 重建LabelEncoder并恢复类标签 model.encoder = LabelEncoder() model.encoder.classes_ = np.array(config["encoder_classes"]) return model
步骤3:加载模型
现在使用load_model时传入custom_objects,即可完整恢复模型与encoder的状态:
keras.models.load_model("model_path", custom_objects={"SuperSequential": SuperSequential})
注意:如果LabelEncoder还有其他自定义状态,需在get_config中一并保存。
2. 用Keras预处理层实现标签编码
使用IntegerLookup和CategoryEncoding层可以将标签处理逻辑整合到模型中,保存/加载时会自动携带预处理逻辑,无需额外处理:
完整代码示例
import numpy as np import keras from keras import layers # 创建包含标签预处理的模型 def build_model(num_classes, label_vocab): # 特征输入分支 input_features = layers.Input(shape=(10,)) # 假设输入特征为10维 x = layers.Dense(32, activation='relu')(input_features) output = layers.Dense(num_classes, activation='softmax')(x) # 标签预处理分支(训练时使用) input_labels = layers.Input(shape=(), dtype='string') # 映射标签为整数索引 label_lookup = layers.IntegerLookup(output_mode='int') label_lookup.adapt(np.array(label_vocab)) encoded_labels = label_lookup(input_labels) # 转换为独热编码 one_hot_labels = layers.CategoryEncoding(num_tokens=num_classes, output_mode='one_hot')(encoded_labels) # 训练模型:输入为特征+标签,输出为模型预测 train_model = keras.Model(inputs=[input_features, input_labels], outputs=output) train_model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy']) # 预测模型:输入特征,输出原始标签 pred_output = label_lookup.inverse(layers.Argmax(axis=1)(output)) pred_model = keras.Model(inputs=input_features, outputs=pred_output) return train_model, pred_model # 使用示例 label_vocab = ["cat", "dog", "bird"] train_model, pred_model = build_model(3, label_vocab) # 准备数据 X = np.random.rand(100, 10) y = np.random.choice(label_vocab, size=100) # 训练模型 train_model.fit([X, y], epochs=5) # 预测 predictions = pred_model.predict(X[:5]) print(predictions) # 保存与加载预测模型 pred_model.save("pred_model.h5") loaded_pred_model = keras.models.load_model("pred_model.h5") loaded_predictions = loaded_pred_model.predict(X[:5]) print(loaded_predictions)
说明
- 该方案将标签编码/解码逻辑完全整合到模型中,保存与加载时自动携带预处理层的状态(如
IntegerLookup的类标签) - 训练时可直接传入原始标签,模型内部完成编码;预测时直接输出原始标签,无需额外处理
3. 思路合理性与更优方案
你的原始子类化思路是可行的,但Keras预处理层方案更符合框架设计理念:
- 预处理层可与模型一起保存、部署,避免训练与推理阶段的预处理不一致
- 可利用Keras的
adapt()方法自动拟合标签,无需手动维护LabelEncoder - 无额外sklearn依赖,更适合生产环境部署
更轻量的替代方案:
若不需要将预处理层整合到模型中,可单独使用keras.utils.StringLookup处理标签,再保存其状态:
from keras.utils import StringLookup # 拟合标签 lookup = StringLookup(output_mode='int') lookup.adapt(y) # 编码标签并训练模型 y_enc = lookup(y) model = Sequential([layers.Dense(32, activation='relu'), layers.Dense(3, activation='softmax')]) model.compile(optimizer='adam', loss='categorical_crossentropy') model.fit(X, keras.utils.to_categorical(y_enc), epochs=5) # 保存模型与lookup状态 model.save("model.h5") np.save("lookup_vocab.npy", lookup.get_vocabulary()) # 加载与预测 loaded_model = keras.models.load_model("model.h5") loaded_lookup = StringLookup(output_mode='int') loaded_lookup.set_vocabulary(np.load("lookup_vocab.npy")) y_pred = loaded_model.predict(X) y_pred = loaded_lookup.inverse(np.argmax(y_pred, axis=1))
该方案比子类化更简洁,避免了子类化带来的序列化问题,同时保持了逻辑清晰。
内容的提问来源于stack exchange,提问作者fortune_pickle

