Training pipeline with callbacks#

  • VQCClassifier trained with EarlyStopping and ModelCheckpoint.

  • Both run on both backends, unlike a Lightning callback.

  • Inspect what each callback recorded, resume the run from its last checkpoint, then reload the fitted preprocessing for new rows.

  • Pennylane backend throughout.

1. Data#

  • make_moons, 200 samples.

  • normalize="minmax", fit on train only.

  • AngleEmbedding prescaling maps it to [0, pi] in setup().

[1]:
from sklearn.datasets import make_moons

import pyqit
from pyqit import DataModule, Trainer
from pyqit.ansatzes import SELAnsatz
from pyqit.core import AngleEmbedding, EarlyStopping, ModelCheckpoint
from pyqit.models import VQCClassifier

pyqit.set_seed(42)

X, y = make_moons(n_samples=200, noise=0.1, random_state=0)
dm = DataModule(X, y, normalize="minmax", batch_size=16, seed=42)

2. Trainer with both callbacks#

  • EarlyStopping on val_loss, patience 5. Stops once it stops improving.

  • ModelCheckpoint saves the best and last epoch, restores best weights after training.

  • Kept as named variables, not inlined, to read their state after fit.

  • loss_fn="cross_entropy".

[2]:
model = VQCClassifier(n_qubits=4, n_layers=3, ansatz=SELAnsatz, encoder=AngleEmbedding)

early_stop = EarlyStopping(monitor="val_loss", patience=5)
checkpoint = ModelCheckpoint(dirpath="ckpts", save_best=True, save_last=True)

trainer = Trainer(
    max_epochs=60,
    loss_fn="cross_entropy",
    callbacks=[early_stop, checkpoint],
    verbose=1,
)
history = trainer.fit(model, dm)
[Trainer] Starting pennylane backend | 60 epochs | lr=0.01

[EarlyStopping] Stopped at epoch 39 -- val_loss did not improve for 5 epoch(s) (best val_loss: 0.3256)
[Checkpoint] Last epoch -> ckpts/last.npz
[Checkpoint] Restored best weights from epoch 34 (val_loss: 0.3256)
[Trainer] Training complete.

3. What each callback recorded#

  • early_stop.stopped_epoch and early_stop.stopping_reason, set once training stops early.

  • checkpoint.best_epoch, checkpoint.best_path, checkpoint.last_path, the epoch and files ModelCheckpoint wrote.

  • history.best_epoch, history.best_score, history.best_metric, the same best epoch from the run’s own record.

[3]:
print("stopped at:", early_stop.stopped_epoch, "|", early_stop.stopping_reason)
print("best checkpoint:", checkpoint.best_epoch, "->", checkpoint.best_path)
print("last checkpoint:", checkpoint.last_path)
print("history best:", history.best_epoch, history.best_score, history.best_metric)
stopped at: 39 | val_loss did not improve for 5 epoch(s) (best val_loss: 0.3256)
best checkpoint: 34 -> ckpts/best.npz
last checkpoint: ckpts/last.npz
history best: 34 0.32560367617711666 val_loss

4. Loss curve#

[4]:
import matplotlib.pyplot as plt

plt.plot(history.train_loss, label="train_loss")
plt.plot(history.val_loss, label="val_loss")
plt.xlabel("epoch")
plt.ylabel("loss")
plt.legend()
plt.show()
../_images/tutorials_callbacks_8_0.png

5. Resuming from a checkpoint#

  • A checkpoint holds the weights, the optimizer state and the history to that epoch, .npz on pennylane and .ckpt on torch.

  • ModelCheckpoint(resume_from=...) loads all three before the first epoch, so a fresh model continues where last.npz stopped.

  • max_epochs counts from zero: the resumed run trains the epochs the file does not already hold.

[5]:
n_done = len(history.train_loss)

fresh_model = VQCClassifier(
    n_qubits=4, n_layers=3, ansatz=SELAnsatz, encoder=AngleEmbedding
)
resume = ModelCheckpoint(
    dirpath="ckpts", save_best=False, resume_from=checkpoint.last_path
)

resumed = Trainer(
    max_epochs=n_done + 5, loss_fn="cross_entropy", callbacks=[resume], verbose=1
).fit(fresh_model, dm)

assert resumed.train_loss[:n_done] == history.train_loss
print(f"{n_done} epochs from the file, {len(resumed.train_loss) - n_done} trained now")
[Trainer] Starting pennylane backend | 45 epochs | lr=0.01

[Checkpoint] Resumed from ckpts/last.npz at epoch 40
[Trainer] Training complete.
40 epochs from the file, 5 trained now

6. Saving the DataModule#

  • The checkpoint holds the model. The fitted normalizer is the DataModule’s, saved on request with dm.save.

  • DataModule.load(path, X_new) attaches new rows to the saved settings, like dm.for_prediction, so predicting needs neither the training data nor a refit.

[6]:
X_new, _ = make_moons(n_samples=5, noise=0.1, random_state=1)

dm.save("ckpts/datamodule.pkl")
dm_new = DataModule.load("ckpts/datamodule.pkl", X_new)

print(trainer.predict(fresh_model, dm_new))
[0 0 1 1 1]