الدرس 08
التعلّم بالنقل (Transfer Learning)
CIFAR-10: التجميد (Freeze) مقابل الضبط الدقيق (Fine-Tune) مقابل التدريب من الصفر
في هذا المعمل سنقوم بما يلي:
- فحص نموذج أساسي (Base Model) مُدرَّب مسبقًا وهو DenseNet121.
- تجميد (Freeze) ذلك النموذج الأساسي، وإضافة رأس (Head) جديد، وتدريب الرأس فقط.
- إلغاء تجميد (Unfreeze) النموذج بالكامل والضبط الدقيق (Fine-Tune).
- تجميد مخصَّص (Custom-Freeze) لأول N طبقة، وتدريب الباقي.
- بناء وتدريب شبكة CNN مخصَّصة صغيرة من الصفر.
- مقارنة دقة التحقق (Validation Accuracy) عبر جميع التجارب الخمس.
import tensorflow as tf
from tensorflow.keras import layers, models
import matplotlib.pyplot as plt
print("TF version:", tf.__version__)TF version: 2.18.0# Load CIFAR-10 Dataset
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
# Convert labels to categorical
y_train = tf.keras.utils.to_categorical(y_train, 10)
y_test = tf.keras.utils.to_categorical(y_test, 10)
print("Shapes:", x_train.shape, "→", y_train.shape)Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
[1m170498071/170498071[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m4s[0m 0us/step
Shapes: (50000, 32, 32, 3) → (50000, 10)y_testarray([[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 1., 0.],
[0., 0., 0., ..., 0., 1., 0.],
...,
[0., 0., 0., ..., 0., 0., 0.],
[0., 1., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 1., 0., 0.]])def build_tl_model(base_cls):
"""Build a transfer-learning model using the given base."""
base = base_cls(include_top=False, weights='imagenet',
input_shape=(32, 32, 3))
# 2) Freeze only the base
base.trainable = False
model = models.Sequential([
base,
layers.GlobalAveragePooling2D(),
layers.Dense(128, activation='relu'),
layers.Dense(10, activation='softmax')
])
return model# Freeze all layers and train head only
model_fe = build_tl_model(tf.keras.applications.DenseNet121)
model_fe.summary()
# Verify trainable counts
print("Total layers:", len(model_fe.layers))
print("Trainable layers:", sum(l.trainable for l in model_fe.layers))
model_fe.compile(tf.keras.optimizers.Adam(1e-3),
loss='categorical_crossentropy',
metrics=['accuracy'])
history_fe = model_fe.fit(
x_train, y_train,
epochs=5,
validation_data=(x_test, y_test)
)Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/densenet/densenet121_weights_tf_dim_ordering_tf_kernels_notop.h5
[1m29084464/29084464[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m0s[0m 0us/step
[1mModel: "sequential"[0m
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃[1m [0m[1mLayer (type) [0m[1m [0m┃[1m [0m[1mOutput Shape [0m[1m [0m┃[1m [0m[1m Param #[0m[1m [0m┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ densenet121 ([38;5;33mFunctional[0m) │ ([38;5;45mNone[0m, [38;5;34m1[0m, [38;5;34m1[0m, [38;5;34m1024[0m) │ [38;5;34m7,037,504[0m │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ global_average_pooling2d │ ([38;5;45mNone[0m, [38;5;34m1024[0m) │ [38;5;34m0[0m │
│ ([38;5;33mGlobalAveragePooling2D[0m) │ │ │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense ([38;5;33mDense[0m) │ ([38;5;45mNone[0m, [38;5;34m128[0m) │ [38;5;34m131,200[0m │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_1 ([38;5;33mDense[0m) │ ([38;5;45mNone[0m, [38;5;34m10[0m) │ [38;5;34m1,290[0m │
└─────────────────────────────────┴────────────────────────┴───────────────┘
[1m Total params: [0m[38;5;34m7,169,994[0m (27.35 MB)
[1m Trainable params: [0m[38;5;34m132,490[0m (517.54 KB)
[1m Non-trainable params: [0m[38;5;34m7,037,504[0m (26.85 MB)
Total layers: 4
Trainable layers: 3
Epoch 1/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m74s[0m 32ms/step - accuracy: 0.5107 - loss: 1.4042 - val_accuracy: 0.6103 - val_loss: 1.1084
Epoch 2/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m21s[0m 13ms/step - accuracy: 0.6358 - loss: 1.0320 - val_accuracy: 0.6266 - val_loss: 1.0702
Epoch 3/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m39s[0m 12ms/step - accuracy: 0.6628 - loss: 0.9555 - val_accuracy: 0.6348 - val_loss: 1.0587
Epoch 4/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m19s[0m 12ms/step - accuracy: 0.6812 - loss: 0.9009 - val_accuracy: 0.6363 - val_loss: 1.0593
Epoch 5/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m22s[0m 14ms/step - accuracy: 0.6942 - loss: 0.8605 - val_accuracy: 0.6441 - val_loss: 1.0433# Unfreeze entire model and fine-tune
model_ft = model_fe
for layer in model_ft.layers:
layer.trainable = True
model_ft.summary()
model_ft.compile(
optimizer=tf.keras.optimizers.Adam(1e-5),
loss='categorical_crossentropy',
metrics=['accuracy']
)
history_ft = model_ft.fit(
x_train, y_train,
epochs=5,
validation_data=(x_test, y_test)
)[1mModel: "sequential"[0m
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃[1m [0m[1mLayer (type) [0m[1m [0m┃[1m [0m[1mOutput Shape [0m[1m [0m┃[1m [0m[1m Param #[0m[1m [0m┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ densenet121 ([38;5;33mFunctional[0m) │ ([38;5;45mNone[0m, [38;5;34m1[0m, [38;5;34m1[0m, [38;5;34m1024[0m) │ [38;5;34m7,037,504[0m │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ global_average_pooling2d │ ([38;5;45mNone[0m, [38;5;34m1024[0m) │ [38;5;34m0[0m │
│ ([38;5;33mGlobalAveragePooling2D[0m) │ │ │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense ([38;5;33mDense[0m) │ ([38;5;45mNone[0m, [38;5;34m128[0m) │ [38;5;34m131,200[0m │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_1 ([38;5;33mDense[0m) │ ([38;5;45mNone[0m, [38;5;34m10[0m) │ [38;5;34m1,290[0m │
└─────────────────────────────────┴────────────────────────┴───────────────┘
[1m Total params: [0m[38;5;34m7,434,976[0m (28.36 MB)
[1m Trainable params: [0m[38;5;34m7,086,346[0m (27.03 MB)
[1m Non-trainable params: [0m[38;5;34m83,648[0m (326.75 KB)
[1m Optimizer params: [0m[38;5;34m264,982[0m (1.01 MB)
Epoch 1/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m270s[0m 91ms/step - accuracy: 0.3725 - loss: 1.9739 - val_accuracy: 0.5274 - val_loss: 1.4738
Epoch 2/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m111s[0m 35ms/step - accuracy: 0.5563 - loss: 1.2795 - val_accuracy: 0.6106 - val_loss: 1.1553
Epoch 3/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m57s[0m 36ms/step - accuracy: 0.6302 - loss: 1.0570 - val_accuracy: 0.6590 - val_loss: 1.0132
Epoch 4/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m80s[0m 35ms/step - accuracy: 0.6819 - loss: 0.9129 - val_accuracy: 0.6949 - val_loss: 0.8927
Epoch 5/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m82s[0m 35ms/step - accuracy: 0.7194 - loss: 0.8038 - val_accuracy: 0.7166 - val_loss: 0.8386# Fine-tune all layers from scratch (no initial freeze)
model_nf = build_tl_model(tf.keras.applications.DenseNet121)
model_nf.trainable = True
model_nf.compile(
optimizer=tf.keras.optimizers.Adam(1e-4),
loss='categorical_crossentropy',
metrics=['accuracy']
)
print("=== Fine-tune all layers (no initial freeze) ===")
history_nf = model_nf.fit(
x_train, y_train,
epochs=5,
validation_data=(x_test, y_test)
)=== Fine-tune all layers (no initial freeze) ===
Epoch 1/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m66s[0m 29ms/step - accuracy: 0.4023 - loss: 1.7199 - val_accuracy: 0.5715 - val_loss: 1.2335
Epoch 2/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m50s[0m 13ms/step - accuracy: 0.5916 - loss: 1.1855 - val_accuracy: 0.6095 - val_loss: 1.1351
Epoch 3/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m42s[0m 14ms/step - accuracy: 0.6234 - loss: 1.0827 - val_accuracy: 0.6246 - val_loss: 1.0891
Epoch 4/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m42s[0m 15ms/step - accuracy: 0.6415 - loss: 1.0328 - val_accuracy: 0.6336 - val_loss: 1.0665
Epoch 5/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m37s[0m 12ms/step - accuracy: 0.6540 - loss: 1.0008 - val_accuracy: 0.6310 - val_loss: 1.0648# Custom-freeze first 100 layers
# 1) Instantiate the backbone first
base = tf.keras.applications.DenseNet121(
include_top=False,
weights="imagenet",
input_shape=(32, 32, 3)
)
# 2) Freeze the first 100 layers of the backbone
for layer in base.layers[:100]:
layer.trainable = False
for layer in base.layers[100:]:
layer.trainable = True
model_cf = models.Sequential([
base,
layers.GlobalAveragePooling2D(),
layers.Dense(128, activation='relu'),
layers.Dense(10, activation='softmax')
])
# 4) Compile & verify
model_cf.compile(
optimizer=tf.keras.optimizers.Adam(1e-4),
loss="categorical_crossentropy",
metrics=["accuracy"]
)
# Count how many layers are trainable
trainable = sum(l.trainable for l in model_cf.layers + base.layers)
total = sum(1 for _ in model_cf.layers + base.layers)
print(f"Trainable layers: {trainable} / {total}")
# 5) Train on your resized CIFAR inputs
history_cf = model_cf.fit(
x_train, y_train,
epochs=5,
validation_data=(x_test, y_test)
)Trainable layers: 331 / 431
Epoch 1/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m224s[0m 76ms/step - accuracy: 0.4830 - loss: 1.5048 - val_accuracy: 0.7306 - val_loss: 0.9113
Epoch 2/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m82s[0m 33ms/step - accuracy: 0.7554 - loss: 0.7078 - val_accuracy: 0.7814 - val_loss: 0.6363
Epoch 3/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m81s[0m 33ms/step - accuracy: 0.8206 - loss: 0.5185 - val_accuracy: 0.7978 - val_loss: 0.5982
Epoch 4/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m78s[0m 31ms/step - accuracy: 0.8685 - loss: 0.3845 - val_accuracy: 0.7995 - val_loss: 0.6192
Epoch 5/5
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m82s[0m 31ms/step - accuracy: 0.9037 - loss: 0.2825 - val_accuracy: 0.8102 - val_loss: 0.6099model_cf.summary()[1mModel: "sequential_2"[0m
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃[1m [0m[1mLayer (type) [0m[1m [0m┃[1m [0m[1mOutput Shape [0m[1m [0m┃[1m [0m[1m Param #[0m[1m [0m┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ densenet121 ([38;5;33mFunctional[0m) │ ([38;5;45mNone[0m, [38;5;34m1[0m, [38;5;34m1[0m, [38;5;34m1024[0m) │ [38;5;34m7,037,504[0m │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ global_average_pooling2d_2 │ ([38;5;45mNone[0m, [38;5;34m1024[0m) │ [38;5;34m0[0m │
│ ([38;5;33mGlobalAveragePooling2D[0m) │ │ │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_4 ([38;5;33mDense[0m) │ ([38;5;45mNone[0m, [38;5;34m128[0m) │ [38;5;34m131,200[0m │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_5 ([38;5;33mDense[0m) │ ([38;5;45mNone[0m, [38;5;34m10[0m) │ [38;5;34m1,290[0m │
└─────────────────────────────────┴────────────────────────┴───────────────┘
[1m Total params: [0m[38;5;34m19,733,344[0m (75.28 MB)
[1m Trainable params: [0m[38;5;34m6,281,674[0m (23.96 MB)
[1m Non-trainable params: [0m[38;5;34m888,320[0m (3.39 MB)
[1m Optimizer params: [0m[38;5;34m12,563,350[0m (47.93 MB)# Train a small custom CNN from scratch on 32×32 images
scratch = models.Sequential([
layers.Input((32,32,3)),
layers.Conv2D(32,3,activation='relu',padding='same'), layers.MaxPool2D(),
layers.Conv2D(64,3,activation='relu',padding='same'), layers.MaxPool2D(),
layers.GlobalAveragePooling2D(),
layers.Dropout(0.2),
layers.Dense(10, activation='softmax')
])
scratch.compile('adam', loss="categorical_crossentropy", metrics=["accuracy"])
history_s = scratch.fit(
x_train, y_train,
epochs=10,
validation_data=(x_test, y_test)
)Epoch 1/10
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m11s[0m 5ms/step - accuracy: 0.2228 - loss: 2.0600 - val_accuracy: 0.3457 - val_loss: 1.7556
Epoch 2/10
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m6s[0m 4ms/step - accuracy: 0.3406 - loss: 1.7654 - val_accuracy: 0.3715 - val_loss: 1.6826
Epoch 3/10
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m6s[0m 4ms/step - accuracy: 0.3744 - loss: 1.6866 - val_accuracy: 0.4266 - val_loss: 1.6087
Epoch 4/10
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m6s[0m 4ms/step - accuracy: 0.4002 - loss: 1.6309 - val_accuracy: 0.4587 - val_loss: 1.5145
Epoch 5/10
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m6s[0m 4ms/step - accuracy: 0.4266 - loss: 1.5724 - val_accuracy: 0.4719 - val_loss: 1.4802
Epoch 6/10
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m10s[0m 4ms/step - accuracy: 0.4404 - loss: 1.5419 - val_accuracy: 0.4861 - val_loss: 1.4414
Epoch 7/10
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m6s[0m 4ms/step - accuracy: 0.4551 - loss: 1.5068 - val_accuracy: 0.4906 - val_loss: 1.4343
Epoch 8/10
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m6s[0m 4ms/step - accuracy: 0.4637 - loss: 1.4824 - val_accuracy: 0.5124 - val_loss: 1.3748
Epoch 9/10
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m10s[0m 4ms/step - accuracy: 0.4706 - loss: 1.4597 - val_accuracy: 0.5100 - val_loss: 1.3677
Epoch 10/10
[1m1563/1563[0m [32m━━━━━━━━━━━━━━━━━━━━[0m[37m[0m [1m5s[0m 3ms/step - accuracy: 0.4793 - loss: 1.4402 - val_accuracy: 0.5181 - val_loss: 1.3403# Compare all runs
plt.figure(figsize=(10,6))
plt.plot(history_fe.history['val_accuracy'], 'o-', label='Freeze+TrainHead')
plt.plot(history_ft.history['val_accuracy'], 'o-', label='Unfreeze+FineTune')
plt.plot(history_nf.history['val_accuracy'], 'o-', label='No Freeze→Fine-tuneAll')
plt.plot(history_cf.history['val_accuracy'], 'o-', label='Custom-Freeze')
plt.plot(history_s.history['val_accuracy'], 'o-', label='Scratch CNN')
plt.title("Validation Accuracy Comparison")
plt.xlabel("Epoch")
plt.ylabel("Accuracy")
plt.legend()
plt.show()تمارين للطلاب: توسيع معمل التعلّم بالنقل
استبدال العمود الفقري (Backbone) المُدرَّب مسبقًا بآخر جديد
استبدل DenseNet121 بـEfficientNetB0، أو Xception في خط أنابيب التعلّم بالنقل (Transfer-Learning Pipeline) الخاص بك.
لكل نموذج، اضبط حجم الإدخال (على سبيل المثال يتوقّع EfficientNetB0 حجم 224×224؛ وVGG16 حجم 224×224؛ وXception حجم 71×71 على الأقل).
استخدم tf.image.resize لإعادة تشكيل صور CIFAR-10 الخاصة بك وفقًا لذلك.
درّب الرأس الجديد فقط (5 حِقَب)، ثم اضبطه بدقة (Fine-Tune) (10 حِقَب بمعدل تعلّم = 1e-5).
حاول زيادة عدد الحِقَب وتغيير معدل التعلّم
دراسة ذاتية
متى تستخدم كل نمط:
تجميد الكل، تدريب الرأس فقط
- مجموعة بياناتك صغيرة (أقل من 10 آلاف صورة)
- الفئات مشابهة نسبيًا لفئات ImageNet (أجسام طبيعية، حيوانات)
الضبط الدقيق بعد تدريب الرأس (إلغاء تجميد الكل)
- مجموعة بياناتك متوسطة الحجم (10–100 ألف صورة)
- تلاحظ ارتفاع دقة التحقق (Validation Accuracy) لبضع حِقَب ثم تستقر - ولا تتحسن بعد ذلك
الضبط الدقيق من الصفر (بدون تجميد أولي)
- لديك مجموعة بيانات كبيرة (100 ألف صورة أو أكثر)
- بياناتك مختلفة جدًا عن ImageNet (مثل الصور الطبية)
التجميد المخصَّص (تجميد أول N طبقة، وتدريب الباقي)
- لديك حجم بيانات متوسط (10–50 ألف)
- تريد تقليل الحوسبة مقارنةً بالضبط الدقيق الكامل
قد ينجح التجميد المخصَّص للأسباب التالية:
-
الاستفادة من الخصائص العامة منخفضة المستوى تتعلّم الطبقات الالتفافية الأولى كاشفات حواف بسيطة، وبُقع لونية، ومرشِّحات نسيجية (Texture Filters). هذه خصائص عامة ونادرًا ما تحتاج للتغيير. تجميدها يحافظ على هذه اللبنات الأساسية الموثوقة.
-
تركيز قدرة التعلّم على الطبقات الخاصة بالمهمة بإلغاء تجميد الطبقات الأعمق فقط، تخصّص بياناتك المحدودة وميزانية التدرّج (Gradient) لتعلّم الخصائص الدلالية "عالية المستوى" التي تميّز فعليًا فئات CIFAR الخاصة بك، بدلًا من إعادة تعلّم الحواف.