iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Saving Models (joblib)

Persisting trained models lets you serve, version, and ship without retraining. joblib for sklearn, pickle for plain Python (carefully), torch.save for PyTorch, SavedModel/keras.save for TensorFlow, ONNX for cross-framework deployment. Always pin versions and bundle the preprocessing pipeline.

joblib, ONNX, pipelines, versioning

EXAMPLE
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
import joblib
import pickle
import json

# 1) Train a pipeline (not just the model)
X, y = load_breast_cancer(return_X_y=True, as_frame=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

pipe = Pipeline([
    ('scale', StandardScaler()),
    ('clf',   LogisticRegression(max_iter=1000)),
]).fit(X_train, y_train)
print(pipe.score(X_test, y_test))

# 2) joblib — sklearn's preferred format
joblib.dump(pipe, 'model.joblib', compress=3)
loaded = joblib.load('model.joblib')
print(loaded.predict(X_test[:3]))

# joblib compresses big NumPy arrays better than vanilla pickle.
# compress=3 → balance speed + size; compress=9 → smallest

# 3) pickle — generic Python
with open('model.pkl', 'wb') as f:
    pickle.dump(pipe, f, protocol=pickle.HIGHEST_PROTOCOL)
with open('model.pkl', 'rb') as f:
    model = pickle.load(f)

# CAVEAT: pickle is NOT a security boundary. Loading an untrusted .pkl executes arbitrary code.
# Only load files YOU produced or fully trust. Sign + verify in production pipelines.

# 4) Save the whole training context
import sklearn, joblib, sys, platform
metadata = {
    'sklearn_version': sklearn.__version__,
    'python_version':  sys.version,
    'platform':        platform.platform(),
    'feature_names':   list(X.columns),
    'target_classes':  ['benign', 'malignant'],
    'training_score':  pipe.score(X_train, y_train),
    'test_score':      pipe.score(X_test, y_test),
    'trained_at':      '2024-01-15T03:00:00Z',
}
with open('model.json', 'w') as f:
    json.dump(metadata, f, indent=2)

# Ship model.joblib + model.json together.

# 5) Load + validate version
meta = json.load(open('model.json'))
assert meta['sklearn_version'] == sklearn.__version__, 'sklearn version mismatch'

# 6) Pipeline with mixed columns (numeric + categorical)
numeric_cols = ['age', 'tenure']
cat_cols     = ['country', 'plan']

preprocess = ColumnTransformer([
    ('num', StandardScaler(),                                       numeric_cols),
    ('cat', OneHotEncoder(handle_unknown='ignore', sparse_output=False), cat_cols),
])

pipe = Pipeline([
    ('pre', preprocess),
    ('clf', LogisticRegression(max_iter=1000)),
]).fit(X_train, y_train)

joblib.dump(pipe, 'model.joblib')
# The fitted ColumnTransformer remembers the schema; serving code only needs to pass a DataFrame with the same cols.

# 7) Pickle compatibility caveats
# • Pickled sklearn models tied to scikit-learn version — load with the SAME version
# • Persist in train env, retrain in serve env if minor version differs
# • For cross-version portability, use ONNX or PMML

# 8) ONNX — cross-framework deployment
# pip install skl2onnx onnxruntime
import onnx
import onnxruntime as ort
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType

initial_type = [('float_input', FloatTensorType([None, X_train.shape[1]]))]
onx = convert_sklearn(pipe, initial_types=initial_type, target_opset=15)
with open('model.onnx', 'wb') as f:
    f.write(onx.SerializeToString())

# Load + predict
sess = ort.InferenceSession('model.onnx')
import numpy as np
proba = sess.run(['probabilities'], { 'float_input': X_test.values.astype(np.float32) })

# ONNX runs anywhere — Python, C++, .NET, mobile, browser via onnx-web.
# Convert from sklearn, PyTorch, TensorFlow, XGBoost, LightGBM.

# 9) PyTorch — torch.save with state_dict
import torch
import torch.nn as nn

model = MyNet()
torch.save(model.state_dict(), 'model.pt')

# Loading
model = MyNet()
model.load_state_dict(torch.load('model.pt', map_location='cpu'))
model.eval()

# Save with metadata
checkpoint = {
    'state_dict': model.state_dict(),
    'optimizer':  opt.state_dict(),
    'epoch':      10,
    'best_acc':   0.94,
}
torch.save(checkpoint, 'ckpt.pt')

# TorchScript — portable to C++ / mobile
scripted = torch.jit.script(model)
scripted.save('model.ts.pt')

# 10) TensorFlow / Keras
import tensorflow as tf
model.save('saved_model')                         # SavedModel (directory)
model.save('model.keras')                          # single file (newer)
model.save('model.h5')                             # legacy HDF5

loaded = tf.keras.models.load_model('model.keras')

# 11) Cloud + registry options
# • MLflow — tracking + model registry; one line to log + serve
# • Vertex AI Model Registry / SageMaker Model Registry
# • Weights & Biases artifacts
# • DVC for git-tracked model files
# Always store: model artefact + metrics + code commit + data snapshot

import mlflow
mlflow.sklearn.log_model(pipe, 'model')

# 12) Versioning strategies
# • SemVer per model: 1.0.0 → 1.0.1 (data drift), 1.1.0 (feature added), 2.0.0 (breaking schema)
# • Tag artefacts with git SHA + dataset hash
# • Keep last N versions deployable for instant rollback

# 13) Quantisation + pruning for serving
# • sklearn — usually doesn't matter; sklearn models are already small
# • PyTorch quantization — int8 cuts size 4x with minimal accuracy loss
# • TensorFlow Lite — mobile-ready conversions
# • ONNX Runtime + GraphOptimizationLevel

# 14) Inference services
# Wrap the model in:
#   • FastAPI + uvicorn — simple REST service
#   • BentoML — model serving framework
//   • TF Serving / TorchServe — production-grade
//   • Triton Inference Server — multi-framework + GPU
//   • SageMaker / Vertex AI endpoints — managed

# 15) Common bugs
# • Pickling the model only, not the preprocessing → inference output garbage; pickle the Pipeline
# • Loading pickle from an UNTRUSTED source → arbitrary code execution; sign + verify or use ONNX
# • sklearn version drift between train + serve → unpickling fails; pin versions in serve image
# • Forgetting to call model.eval() on PyTorch → dropout / batchnorm still active in inference
# • Serving without GPU → batch size matters; tune for CPU latency
# • No metadata → can't reproduce; embed feature names + classes
# • model.joblib size > 100 MB committed to git → use git LFS or model registry
# • Caching old model across deployments → versioned URLs + cache-bust
# • Mismatched feature ORDER between train + inference → silent wrong predictions; pass DataFrame with named columns

Why it matters

Always persist the full Pipeline (preprocessing + estimator), pair it with metadata (sklearn version, feature names, training scores), and pin versions on the serving side. Use joblib for sklearn, state_dict + torch.save for PyTorch, model.keras for TF, and ONNX when you need cross-framework or cross-platform serving.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
import joblib
joblib.dump(pipe, 'model.joblib')
pipe2 = joblib.load('model.joblib')
print(pipe2.predict(Xte[:5]))
Try it Yourself »

Exercise

Save a fitted model to disk.

joblib. (pipe, 'model.joblib')

Discussion

Loading…