CIFAR-10 Transfer Learning with Keras 3#
This complete workflow freezes an ImageNet-pretrained VGG16 feature extractor and trains a CIFAR-10 classifier. The split, head intent, ten-epoch maximum, and evaluation are aligned across frameworks.
A network connection is required the first time the pretrained weights are
cached. To run the example more quickly, set EPOCHS to 1 or 2 in the data cell.
Runtime dependency check#
import importlib.util
import subprocess
import sys
from pathlib import Path
REQUIRED_RUNTIME = {'keras': 'keras', 'torch': 'torch'}
COLAB_EXTRAS = {}
missing_required = [
package for module, package in REQUIRED_RUNTIME.items()
if importlib.util.find_spec(module) is None
]
missing_extras = [
package for module, package in COLAB_EXTRAS.items()
if importlib.util.find_spec(module) is None
]
if missing_extras and "google.colab" in sys.modules:
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "-q", *missing_extras]
)
missing_extras = []
if missing_required or missing_extras:
missing = ", ".join(missing_required + missing_extras)
raise RuntimeError(
f"Missing notebook dependencies: {missing}. Locally run "
"`uv sync --group notebooks`; in Colab restart the runtime if an "
"installation cell just changed the environment."
)
print("runtime dependency check passed")
runtime dependency check passed
%matplotlib inline
Keras-to-PyTorch crosswalk#
This optional implementation uses the same Torch runtime as the canonical
native PyTorch path. Keras compile() selects the optimizer and loss,
fit() owns the explicit batch and epoch loop, and callbacks provide
high-level training control. Data, splits, budgets, evidence, and scientific
conclusions remain aligned with the canonical PyTorch workflow.
Inspect class coverage#
training_counts = np.bincount(y_train, minlength=10)
fig, ax = plt.subplots(figsize=(8, 3.2))
ax.bar(np.arange(10), training_counts)
ax.set(
title="Training-set class distribution",
xlabel="Class",
ylabel="Samples",
xticks=np.arange(10),
)
plt.show()
Preprocess for VGG16 and define the trainable head#
x_train = keras.applications.vgg16.preprocess_input(x_train * 255.0)
x_validation = keras.applications.vgg16.preprocess_input(x_validation * 255.0)
x_test_images = x_test.copy()
x_test = keras.applications.vgg16.preprocess_input(x_test * 255.0)
base_model = keras.applications.VGG16(
include_top=False, weights="imagenet", input_shape=(32, 32, 3)
)
base_model.trainable = False
print("extracting frozen VGG16 features once per split")
x_train = base_model.predict(
x_train, batch_size=BATCH_SIZE, verbose=1
).reshape(len(x_train), -1)
x_validation = base_model.predict(
x_validation, batch_size=BATCH_SIZE, verbose=1
).reshape(len(x_validation), -1)
x_test = base_model.predict(
x_test, batch_size=BATCH_SIZE, verbose=1
).reshape(len(x_test), -1)
model = keras.Sequential(
[
keras.Input(shape=(x_train.shape[1],)),
layers.Dense(512, activation='relu'), layers.Dropout(0.25), layers.Dense(256, activation='relu'), layers.Dropout(0.25),
layers.Dense(10),
],
name="vgg16_frozen_features",
)
model.compile(
optimizer=keras.optimizers.Adam(1e-3),
loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=["accuracy"],
)
print(f"trainable parameters: {sum(np.prod(v.shape) for v in model.trainable_weights):,}")
model.summary()
extracting frozen VGG16 features once per split
1/176 ━━━━━━━━━━━━━━━━━━━━ 52s 298ms/step
3/176 ━━━━━━━━━━━━━━━━━━━━ 7s 45ms/step
5/176 ━━━━━━━━━━━━━━━━━━━━ 7s 45ms/step
7/176 ━━━━━━━━━━━━━━━━━━━━ 7s 45ms/step
9/176 ━━━━━━━━━━━━━━━━━━━━ 7s 46ms/step
10/176 ━━━━━━━━━━━━━━━━━━━━ 7s 47ms/step
11/176 ━━━━━━━━━━━━━━━━━━━━ 7s 47ms/step
13/176 ━━━━━━━━━━━━━━━━━━━━ 7s 48ms/step
15/176 ━━━━━━━━━━━━━━━━━━━━ 7s 48ms/step
17/176 ━━━━━━━━━━━━━━━━━━━━ 7s 47ms/step
19/176 ━━━━━━━━━━━━━━━━━━━━ 7s 48ms/step
21/176 ━━━━━━━━━━━━━━━━━━━━ 7s 47ms/step
22/176 ━━━━━━━━━━━━━━━━━━━━ 7s 48ms/step
24/176 ━━━━━━━━━━━━━━━━━━━━ 7s 48ms/step
26/176 ━━━━━━━━━━━━━━━━━━━━ 7s 47ms/step
28/176 ━━━━━━━━━━━━━━━━━━━━ 6s 47ms/step
30/176 ━━━━━━━━━━━━━━━━━━━━ 6s 47ms/step
32/176 ━━━━━━━━━━━━━━━━━━━━ 6s 47ms/step
34/176 ━━━━━━━━━━━━━━━━━━━━ 6s 48ms/step
36/176 ━━━━━━━━━━━━━━━━━━━━ 6s 48ms/step
38/176 ━━━━━━━━━━━━━━━━━━━━ 6s 47ms/step
40/176 ━━━━━━━━━━━━━━━━━━━━ 6s 47ms/step
42/176 ━━━━━━━━━━━━━━━━━━━━ 6s 47ms/step
44/176 ━━━━━━━━━━━━━━━━━━━━ 6s 47ms/step
45/176 ━━━━━━━━━━━━━━━━━━━━ 6s 47ms/step
47/176 ━━━━━━━━━━━━━━━━━━━━ 6s 47ms/step
48/176 ━━━━━━━━━━━━━━━━━━━━ 6s 48ms/step
50/176 ━━━━━━━━━━━━━━━━━━━━ 5s 47ms/step
52/176 ━━━━━━━━━━━━━━━━━━━━ 5s 47ms/step
54/176 ━━━━━━━━━━━━━━━━━━━━ 5s 47ms/step
56/176 ━━━━━━━━━━━━━━━━━━━━ 5s 47ms/step
58/176 ━━━━━━━━━━━━━━━━━━━━ 5s 47ms/step
60/176 ━━━━━━━━━━━━━━━━━━━━ 5s 47ms/step
62/176 ━━━━━━━━━━━━━━━━━━━━ 5s 47ms/step
64/176 ━━━━━━━━━━━━━━━━━━━━ 5s 47ms/step
66/176 ━━━━━━━━━━━━━━━━━━━━ 5s 47ms/step
68/176 ━━━━━━━━━━━━━━━━━━━━ 5s 47ms/step
70/176 ━━━━━━━━━━━━━━━━━━━━ 4s 47ms/step
72/176 ━━━━━━━━━━━━━━━━━━━━ 4s 47ms/step
74/176 ━━━━━━━━━━━━━━━━━━━━ 4s 47ms/step
76/176 ━━━━━━━━━━━━━━━━━━━━ 4s 47ms/step
78/176 ━━━━━━━━━━━━━━━━━━━━ 4s 47ms/step
80/176 ━━━━━━━━━━━━━━━━━━━━ 4s 47ms/step
82/176 ━━━━━━━━━━━━━━━━━━━━ 4s 47ms/step
84/176 ━━━━━━━━━━━━━━━━━━━━ 4s 47ms/step
86/176 ━━━━━━━━━━━━━━━━━━━━ 4s 47ms/step
88/176 ━━━━━━━━━━━━━━━━━━━━ 4s 47ms/step
90/176 ━━━━━━━━━━━━━━━━━━━━ 4s 47ms/step
91/176 ━━━━━━━━━━━━━━━━━━━━ 3s 47ms/step
93/176 ━━━━━━━━━━━━━━━━━━━━ 3s 47ms/step
95/176 ━━━━━━━━━━━━━━━━━━━━ 3s 47ms/step
97/176 ━━━━━━━━━━━━━━━━━━━━ 3s 47ms/step
99/176 ━━━━━━━━━━━━━━━━━━━━ 3s 47ms/step
101/176 ━━━━━━━━━━━━━━━━━━━━ 3s 47ms/step
103/176 ━━━━━━━━━━━━━━━━━━━━ 3s 47ms/step
105/176 ━━━━━━━━━━━━━━━━━━━━ 3s 47ms/step
107/176 ━━━━━━━━━━━━━━━━━━━━ 3s 47ms/step
109/176 ━━━━━━━━━━━━━━━━━━━━ 3s 47ms/step
111/176 ━━━━━━━━━━━━━━━━━━━━ 3s 47ms/step
113/176 ━━━━━━━━━━━━━━━━━━━━ 2s 47ms/step
115/176 ━━━━━━━━━━━━━━━━━━━━ 2s 47ms/step
117/176 ━━━━━━━━━━━━━━━━━━━━ 2s 47ms/step
119/176 ━━━━━━━━━━━━━━━━━━━━ 2s 47ms/step
121/176 ━━━━━━━━━━━━━━━━━━━━ 2s 47ms/step
123/176 ━━━━━━━━━━━━━━━━━━━━ 2s 47ms/step
125/176 ━━━━━━━━━━━━━━━━━━━━ 2s 47ms/step
127/176 ━━━━━━━━━━━━━━━━━━━━ 2s 47ms/step
129/176 ━━━━━━━━━━━━━━━━━━━━ 2s 47ms/step
130/176 ━━━━━━━━━━━━━━━━━━━━ 2s 47ms/step
132/176 ━━━━━━━━━━━━━━━━━━━━ 2s 47ms/step
134/176 ━━━━━━━━━━━━━━━━━━━━ 1s 47ms/step
136/176 ━━━━━━━━━━━━━━━━━━━━ 1s 47ms/step
138/176 ━━━━━━━━━━━━━━━━━━━━ 1s 47ms/step
140/176 ━━━━━━━━━━━━━━━━━━━━ 1s 47ms/step
142/176 ━━━━━━━━━━━━━━━━━━━━ 1s 47ms/step
144/176 ━━━━━━━━━━━━━━━━━━━━ 1s 47ms/step
146/176 ━━━━━━━━━━━━━━━━━━━━ 1s 47ms/step
148/176 ━━━━━━━━━━━━━━━━━━━━ 1s 47ms/step
150/176 ━━━━━━━━━━━━━━━━━━━━ 1s 47ms/step
152/176 ━━━━━━━━━━━━━━━━━━━━ 1s 47ms/step
154/176 ━━━━━━━━━━━━━━━━━━━━ 1s 47ms/step
156/176 ━━━━━━━━━━━━━━━━━━━━ 0s 47ms/step
158/176 ━━━━━━━━━━━━━━━━━━━━ 0s 47ms/step
160/176 ━━━━━━━━━━━━━━━━━━━━ 0s 47ms/step
162/176 ━━━━━━━━━━━━━━━━━━━━ 0s 47ms/step
164/176 ━━━━━━━━━━━━━━━━━━━━ 0s 47ms/step
166/176 ━━━━━━━━━━━━━━━━━━━━ 0s 47ms/step
168/176 ━━━━━━━━━━━━━━━━━━━━ 0s 47ms/step
170/176 ━━━━━━━━━━━━━━━━━━━━ 0s 47ms/step
172/176 ━━━━━━━━━━━━━━━━━━━━ 0s 47ms/step
174/176 ━━━━━━━━━━━━━━━━━━━━ 0s 47ms/step
176/176 ━━━━━━━━━━━━━━━━━━━━ 0s 47ms/step
176/176 ━━━━━━━━━━━━━━━━━━━━ 8s 47ms/step
1/20 ━━━━━━━━━━━━━━━━━━━━ 0s 45ms/step
3/20 ━━━━━━━━━━━━━━━━━━━━ 0s 44ms/step
5/20 ━━━━━━━━━━━━━━━━━━━━ 0s 44ms/step
7/20 ━━━━━━━━━━━━━━━━━━━━ 0s 45ms/step
9/20 ━━━━━━━━━━━━━━━━━━━━ 0s 45ms/step
11/20 ━━━━━━━━━━━━━━━━━━━━ 0s 45ms/step
13/20 ━━━━━━━━━━━━━━━━━━━━ 0s 45ms/step
15/20 ━━━━━━━━━━━━━━━━━━━━ 0s 46ms/step
17/20 ━━━━━━━━━━━━━━━━━━━━ 0s 46ms/step
19/20 ━━━━━━━━━━━━━━━━━━━━ 0s 46ms/step
20/20 ━━━━━━━━━━━━━━━━━━━━ 0s 46ms/step
20/20 ━━━━━━━━━━━━━━━━━━━━ 1s 46ms/step
1/40 ━━━━━━━━━━━━━━━━━━━━ 1s 45ms/step
3/40 ━━━━━━━━━━━━━━━━━━━━ 1s 44ms/step
4/40 ━━━━━━━━━━━━━━━━━━━━ 1s 55ms/step
6/40 ━━━━━━━━━━━━━━━━━━━━ 1s 51ms/step
8/40 ━━━━━━━━━━━━━━━━━━━━ 1s 49ms/step
10/40 ━━━━━━━━━━━━━━━━━━━━ 1s 48ms/step
12/40 ━━━━━━━━━━━━━━━━━━━━ 1s 47ms/step
14/40 ━━━━━━━━━━━━━━━━━━━━ 1s 47ms/step
16/40 ━━━━━━━━━━━━━━━━━━━━ 1s 46ms/step
18/40 ━━━━━━━━━━━━━━━━━━━━ 1s 46ms/step
20/40 ━━━━━━━━━━━━━━━━━━━━ 0s 46ms/step
22/40 ━━━━━━━━━━━━━━━━━━━━ 0s 46ms/step
24/40 ━━━━━━━━━━━━━━━━━━━━ 0s 46ms/step
26/40 ━━━━━━━━━━━━━━━━━━━━ 0s 46ms/step
28/40 ━━━━━━━━━━━━━━━━━━━━ 0s 46ms/step
29/40 ━━━━━━━━━━━━━━━━━━━━ 0s 47ms/step
31/40 ━━━━━━━━━━━━━━━━━━━━ 0s 47ms/step
33/40 ━━━━━━━━━━━━━━━━━━━━ 0s 47ms/step
35/40 ━━━━━━━━━━━━━━━━━━━━ 0s 46ms/step
37/40 ━━━━━━━━━━━━━━━━━━━━ 0s 46ms/step
39/40 ━━━━━━━━━━━━━━━━━━━━ 0s 47ms/step
40/40 ━━━━━━━━━━━━━━━━━━━━ 2s 46ms/step
trainable parameters: 396,554
Model: "vgg16_frozen_features"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ dense (Dense) │ (None, 512) │ 262,656 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dropout (Dropout) │ (None, 512) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_1 (Dense) │ (None, 256) │ 131,328 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dropout_1 (Dropout) │ (None, 256) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_2 (Dense) │ (None, 10) │ 2,570 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 396,554 (1.51 MB)
Trainable params: 396,554 (1.51 MB)
Non-trainable params: 0 (0.00 B)
Train with validation-based early stopping#
history = model.fit(
x_train,
y_train,
validation_data=(x_validation, y_validation),
epochs=EPOCHS,
batch_size=BATCH_SIZE,
callbacks=[
keras.callbacks.EarlyStopping(
monitor="val_loss", patience=2, restore_best_weights=True
)
],
verbose=2,
)
fig, axes = plt.subplots(1, 2, figsize=(10, 3.5))
axes[0].plot(history.history["loss"], label="training")
axes[0].plot(history.history["val_loss"], label="validation")
axes[0].set(title="Cross-entropy", xlabel="Epoch")
axes[1].plot(history.history["accuracy"], label="training")
axes[1].plot(history.history["val_accuracy"], label="validation")
axes[1].set(title="Accuracy", xlabel="Epoch")
for axis in axes:
axis.legend()
plt.tight_layout()
plt.show()
Epoch 1/10
176/176 - 1s - 7ms/step - accuracy: 0.4539 - loss: 2.1779 - val_accuracy: 0.5932 - val_loss: 1.1905
Epoch 2/10
176/176 - 1s - 7ms/step - accuracy: 0.5726 - loss: 1.2374 - val_accuracy: 0.6316 - val_loss: 1.0662
Epoch 3/10
176/176 - 1s - 6ms/step - accuracy: 0.6172 - loss: 1.0987 - val_accuracy: 0.6418 - val_loss: 1.0283
Epoch 4/10
176/176 - 1s - 6ms/step - accuracy: 0.6478 - loss: 1.0073 - val_accuracy: 0.6584 - val_loss: 0.9859
Epoch 5/10
176/176 - 1s - 6ms/step - accuracy: 0.6704 - loss: 0.9387 - val_accuracy: 0.6688 - val_loss: 0.9578
Epoch 6/10
176/176 - 1s - 6ms/step - accuracy: 0.6871 - loss: 0.8803 - val_accuracy: 0.6752 - val_loss: 0.9432
Epoch 7/10
176/176 - 1s - 6ms/step - accuracy: 0.7048 - loss: 0.8276 - val_accuracy: 0.6798 - val_loss: 0.9405
Epoch 8/10
176/176 - 1s - 6ms/step - accuracy: 0.7222 - loss: 0.7740 - val_accuracy: 0.6856 - val_loss: 0.9285
Epoch 9/10
176/176 - 1s - 6ms/step - accuracy: 0.7365 - loss: 0.7370 - val_accuracy: 0.6800 - val_loss: 0.9406
Epoch 10/10
176/176 - 1s - 6ms/step - accuracy: 0.7518 - loss: 0.6928 - val_accuracy: 0.6796 - val_loss: 0.9555
Final test evidence#
test_logits = model.predict(x_test, batch_size=BATCH_SIZE, verbose=0)
test_predictions = test_logits.argmax(axis=1)
test_accuracy = accuracy_score(y_test, test_predictions)
cm = confusion_matrix(y_test, test_predictions, labels=np.arange(10))
print(f"test accuracy: {test_accuracy:.4f}")
print(
classification_report(
y_test,
test_predictions,
labels=np.arange(10),
digits=3,
zero_division=0,
)
)
print(
"HELIO_RESULT "
+ json.dumps(
{
"split_signature": split_signature,
"test_accuracy": float(test_accuracy),
"confusion_shape": list(cm.shape),
},
sort_keys=True,
)
)
assert cm.shape == (10, 10)
fig, ax = plt.subplots(figsize=(7, 6))
ConfusionMatrixDisplay(cm, display_labels=np.arange(10)).plot(
ax=ax, colorbar=False, values_format="d"
)
ax.set_title("Test confusion matrix")
plt.show()
mistakes = np.flatnonzero(test_predictions != y_test)[:12]
if len(mistakes):
fig, axes = plt.subplots(3, 4, figsize=(9, 7))
for axis, index in zip(axes.flat, mistakes):
image = x_test_images[index]
if image.shape[0] in (1, 3):
image = np.transpose(image, (1, 2, 0))
axis.imshow(image.squeeze(), cmap="gray" if image.squeeze().ndim == 2 else None)
axis.set_title(f"true={y_test[index]}, pred={test_predictions[index]}")
axis.axis("off")
plt.tight_layout()
plt.show()
test accuracy: 0.6738
precision recall f1-score support
0 0.743 0.733 0.738 1000
1 0.758 0.751 0.754 1000
2 0.635 0.504 0.562 1000
3 0.505 0.512 0.509 1000
4 0.595 0.630 0.612 1000
5 0.639 0.601 0.619 1000
6 0.655 0.798 0.720 1000
7 0.739 0.689 0.713 1000
8 0.778 0.762 0.770 1000
9 0.697 0.758 0.726 1000
accuracy 0.674 10000
macro avg 0.674 0.674 0.672 10000
weighted avg 0.674 0.674 0.672 10000
HELIO_RESULT {"confusion_shape": [10, 10], "split_signature": "bfa7941b2b58be94", "test_accuracy": 0.6738}
Try it yourself in Keras#
Change one choice at a time and keep the data split and evaluation unchanged:
replace the 512→256 classifier with one 256-unit layer.
change the classifier dropout while keeping VGG16 frozen.
unfreeze only the final VGG16 block and use a smaller learning rate.