انتقل إلى المحتوى الرئيسي

الوحدة 9 — التعلّم بالنقل والضبط الدقيق التدريجي

في الوحدة السابقة، وصلنا إلى دقّة 71 % على مجموعة النفايات بشبكة صغيرة مدرَّبة من الصفر مع تكثير. اليوم سنُضاعف الأداء تقريبًا في وقت أقلّ، بأخذ ResNet50 مُدرَّبًا مسبقًا على ImageNet وضبطه على مهمّتنا. هذه هي مقاربة النقل، وهي في الغالبية الساحقة من مسائل الرؤية العملية الخيار الأوّل قبل أيّ شيء آخر.

لماذا يعمل النقل

الشبكة التي دُرِّبت على ImageNet رأت 1.2 مليون صورة من 1000 صنف. طبقاتها الأولى تعلّمت مرشّحات عامّة: كواشف الحواف، والقوامات، والنماذج البسيطة. هذه المرشّحات لا تتغيّر جوهريًا بين صور القطط والزجاجات: فحواف الزجاج تبقى حوافًا، والقوامات المعدنية تبقى قوامات.

لكن الطبقات العميقة تصير أكثر تخصّصًا: تكشف عن أجزاء ذات معنى في سياق ImageNet (وجوه القطط، عجلات السيّارات، أزهار). هذا التخصّص أقلّ فائدة لمهمّتنا، لكنّ بقايا التمثيلات الأعمق لا تزال تحمل معلومة قابلة للاستخدام.

النتيجة الاستراتيجية: نُبقي الشبكة الأصلية، ونستبدل رأسها بمصنّف جديد يناسب أصنافنا الستّة. ثم نُدرّب في مرحلتين: استخراج الميزات أوّلًا، ثمّ التذويب التدريجي.

المرحلة 1: استخراج الميزات

نحمّل ResNet50 بأوزانه من ImageNet، ونُلغي رأس التصنيف الأصلي، ونضيف رأسًا خاصًّا بمهمّتنا:

import tensorflow as tf
from tensorflow.keras import layers

# تحميل الشبكة المُدرَّبة مسبقًا
base = tf.keras.applications.ResNet50(
weights="imagenet",
include_top=False, # لا نريد رأس ImageNet ذي 1000 صنف
input_shape=(224, 224, 3),
)
base.trainable = False # نجمّد الشبكة الأساسية

# بناء النموذج مع رأس جديد
entree = tf.keras.Input(shape=(224, 224, 3))
x = tkathir(entree) # تكثير من الوحدة 8
x = tf.keras.applications.resnet50.preprocess_input(x) # طبيعي إلى ResNet50
x = base(x, training=False) # هام جدًّا: training=False
x = layers.GlobalAveragePooling2D()(x)
x = layers.Dropout(0.3)(x)
sortie = layers.Dense(6, activation="softmax")(x)

modele = tf.keras.Model(entree, sortie)
modele.compile(
optimizer=tf.keras.optimizers.Adam(1e-3),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)

modele.fit(donnees_train, validation_data=donnees_val, epochs=10)

في هذه المرحلة، فقط رأس التصنيف يتعلّم. تُستخدم الشبكة الأساسية كمُستخرج ميزات ثابت. الميزة: تدريب سريع (نصف دقيقة لكلّ حقبة على GPU متوسّطة) وأقلّ خطرًا لفرط التخصيص. النتيجة بعد 10 حقبات: نحو 84 % على مجموعة النفايات.

الفخّ الكبير: preprocess_input

كلّ عائلة شبكات في Keras لها دالّة preprocess_input خاصّة بها. تعمل ResNet50 مع دخل مطبَّع كأنّه من ImageNet: تُطرَح متوسّطات القنوات (RGB = [123.68, 116.78, 103.94])، وتُقلَب القنوات إلى BGR. لا تفعل ذلك بيدك، استعمل resnet50.preprocess_input — لكلّ نموذج معالجته المسبقة الخاصّة.

إذا نسيت هذه الخطوة، سيبدو النموذج «يعمل» ويعطي دقّة قليلة (نحو 40-50 %)، لكنّ الأداء الحقيقي مفقود. لن تصدر أيّ رسالة خطأ.

preprocess_input مختلف بين النماذج
  • resnet50.preprocess_input يعمل بـBGR ويطرح المتوسّطات.
  • efficientnet.preprocess_input يوحّد إلى [0, 1] (لا شيء يُفعل عمليًا).
  • mobilenet_v2.preprocess_input يوحّد إلى [-1, 1]. تحقّق دائمًا من نوع النموذج المُستخدَم واستدعِ الدالّة الصحيحة، ولا تخترع نمطًا عامًّا.

المرحلة 2: التذويب التدريجي

بعد أن يستقرّ رأس التصنيف على أوزان معقولة، نبدأ تذويب الطبقات العميقة من الشبكة الأساسية لتتكيّف مع مهمّتنا:

base.trainable = True

# جمّد الطبقات الأولى، ذوّب فقط الطبقات العميقة
for layer in base.layers[:100]:
layer.trainable = False

# مهمّ جدًّا: أعِد التركيب بمعدّل تعلّم أصغر جدًّا
modele.compile(
optimizer=tf.keras.optimizers.Adam(1e-5), # 100 مرّة أصغر
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)

modele.fit(donnees_train, validation_data=donnees_val, epochs=10)

ثلاث نقاط حرجة:

  • الجمود الجزئي: نُذوِّب فقط الطبقات العميقة (الأخيرة). الطبقات الأولى تحمل ميزات عامّة يجب أن نحافظ عليها. عدد الطبقات التي تُذاب متغيّر عمليّ: 30 إلى 100 طبقة تُذاب عادةً في ResNet50 (المجموع 175).
  • معدّل تعلّم أصغر: نضبط بـ10510^{-5} بدلًا من 10310^{-3}. لماذا؟ لأنّ الأوزان قريبة من حلّها الأمثل، ومعدّل كبير سيدمّرها. القاعدة الشائعة: قسّم معدّل التعلّم على مئة عند التذويب.
  • إعادة التركيب: إلزامية بعد تغيير trainable. إن نسيت، ستستمرّ Keras في استعمال التركيب السابق، ولن يُطبَّق أيّ تغيير.

بعد هذه المرحلة، نصل إلى دقّة 92-93 % على النفايات. عشرون نقطة فوق التدريب من الصفر بنفس الشبكة.

معدّلات التعلّم المتمايزة

مقاربة أدقّ: بدلًا من 1e-5 موحّد، نستخدم معدّلات مختلفة حسب عمق الطبقة. الفكرة: الطبقات العميقة (قرب الرأس) تحتاج تكيّفًا أكبر، الطبقات الأولى أقلّ:

# الطريقة اليدوية في TensorFlow
optimizeurs = []
for i, layer in enumerate(base.layers):
if not layer.trainable:
continue
lr = 1e-5 * (i / len(base.layers)) # أكبر للأعمق
layer._optimizer = tf.keras.optimizers.Adam(lr)
# ...

هذا يزيد تعقيد الشيفرة. Keras لا يدعم هذا الأسلوب أصليًّا؛ يجب كتابته يدويًا أو استعمال حلقة تدريب مخصّصة. في التطبيقات، يفضّل كثيرون التبسيط: معدّل موحّد صغير، مع تذويب على مراحل (مثلًا: أوّل 5 حقبات نذوّب 30 طبقة، ثم 5 أخرى نذوّب 60 طبقة).

فخّ التطبيع بالحزم المُجمَّد

هذا هو الخطأ الأكثر شيوعًا وأشدّها ضررًا في النقل.

طبقات BatchNormalization في ResNet50 تحفظ نوعَين من الحالة:

  • معاملات متعلَّمة (γ\gamma وβ\beta): تُجمَّد عندما نضع layer.trainable = False.
  • إحصائيات جارية (المتوسّط والتباين المتراكمان): لا تُجمَّد بالطريقة نفسها. تُحدَّث في كلّ تمرير أمامي إذا كانت الطبقة في وضع «التدريب».

بمجرّد أن نستدعي modele.fit()، تنتقل كلّ الطبقات إلى وضع التدريب، حتى المُجمَّدة. BatchNormalization تبدأ في تحديث إحصائياتها الجارية استنادًا إلى دفعاتنا الجديدة (النفايات)، بينما γ\gamma وβ\beta محفوظتان من ImageNet. النتيجة: عدم تطابق داخلي، أداء منهار.

الحلّ: مرّر training=False صراحةً عند استدعاء الشبكة الأساسية:

x = base(x, training=False)   # يُبقي BN في وضع الاستدلال حتى في التدريب

هذا يُجبر كلّ طبقة على استعمال إحصائياتها الجارية الأصلية، لا الجديدة. سطر واحد يفصل بين 92 % و65 %.

training=False ضروري ولو كنّا في fit()

عندما نستدعي base(x, training=False)، هذا لا يعني «لا نُدرّب»، بل يعني «تصرّف كأنّك في الاستدلال، لا في التدريب». الفرق يهمّ فقط لطبقات BatchNormalization وDropout. عندما نذوّب الطبقات لاحقًا للضبط الدقيق، نضع training=None (أو نُبقي False حسب مذهبنا) — لكن لا تنسَ إعادة التركيب.

متى يفشل النقل

النقل ليس سحرًا. يفشل في حالتَين رئيستَين:

البُعد بين الميدانَين: ImageNet صور طبيعية، بألوان RGB، بأحجام يومية. صور الأشعة السينية أو الأقمار الاصطناعية أو المسح المجهري بعيدة جدًّا: طبقاتها الأولى تحتاج مرشّحات مختلفة (لا حواف من ImageNet). في هذه الحالات، النقل مفيد جزئيًا لكنّه ليس علاجًا سحريًا. حتى قد يكون التدريب من الصفر أفضل.

قِلّة البيانات الشديدة: أقلّ من 100 صورة لكلّ صنف. حتى مع تكثير، الضبط الدقيق يُنتج فرط تخصيص شديد. الحلّ: تجميد الشبكة كلّها ثم استخراج الميزات فقط، مع مصنّف بسيط (Logistic Regression أو SVM) فوق الميزات.

التنوّع داخل الصنف كبير جدًّا: إذا كانت أصنافك تحتوي أشياء بأشكال مختلفة جوهريًا (سيّارة يمكن أن تكون شاحنة أو دراجة نارية)، النقل يعطي حدًّا فوقيًا معقولًا لكنّه لن يتفوّق. تحتاج إلى بيانات أكثر أو تعريفات أصناف أدقّ.

نتيجة على مجموعة النفايات

جدول مقارنة عملي:

الطريقةدقّةزمن التدريب
شبكة صغيرة من الصفر بدون تكثير62 %45 دقيقة
شبكة صغيرة من الصفر مع تكثير71 %55 دقيقة
ResNet50 استخراج ميزات (رأس فقط)84 %8 دقائق
ResNet50 ضبط دقيق (30 طبقة)90 %25 دقيقة
ResNet50 ضبط دقيق (100 طبقة)93 %40 دقيقة

النقل ليس فقط أدقّ، بل أسرع بكثير كذلك. القاعدة العملية: قبل تصميم شبكة من الصفر، جرّب النقل. في 90 % من الحالات، النقل يفوز.

بروتوكول قياسي للنقل
  1. تحميل نموذج مُدرَّب مسبقًا (ResNet50 مثلًا).
  2. include_top=False والتجميد الكامل.
  3. رأس تصنيف جديد يناسب أصنافك.
  4. تدريب الرأس فقط لـ5-10 حقبات مع معدّل تعلّم 1e-3.
  5. تذويب الطبقات العميقة (30-100 طبقة).
  6. إعادة تركيب بمعدّل 1e-5.
  7. تدريب لـ10-20 حقبة إضافية مع مراقبة التقييم.

في الخلاصة

  • النقل يعيد استخدام شبكة مُدرَّبة مسبقًا على ImageNet كأساس، ويُحلّ في 95 % من مسائل الرؤية أسرع وأدقّ من التدريب من الصفر.
  • مرحلتان: استخراج الميزات (رأس فقط) مع معدّل 1e-3، ثم الضبط الدقيق (تذويب الطبقات العميقة) مع معدّل 1e-5 وإعادة تركيب إلزامية.
  • الفخّ الأخطر هو التطبيع بالحزم: إحصائياته الجارية تُحدَّث في وضع التدريب حتى ولو جُمّدت معاملاته؛ استعمل training=False عند استدعاء الشبكة الأساسية.
  • النقل يفشل عند البُعد الكبير بين الميدانَين (طبّ، أقمار اصطناعية) أو قِلّة البيانات الشديدة أو التنوّع الكبير داخل الأصناف.

الوحدة التالية: التفسير بواسطة Grad-CAM، حيث نُظهر أين ينظر النموذج عند اتّخاذ قراره، ونكتشف نموذجًا يبدو دقيقًا لكنّه ينظر إلى الخلفية بدل الغرض.