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

الوحدة 6 — ResNet والوصلات المتبقّية

انتهت VGG عند حاجزٍ صعب: تكديس أكثر من 19 طبقة يجعل الدقّة تتراجع، حتى على بيانات التدريب. هذه ملاحظة مربكة: نموذج أعمق يجب أن يستطيع نظريًا محاكاة نموذج أقلّ عمقًا (بجعل الطبقات الإضافية هويّة)، فلماذا لا يفعل؟ اكتشف Kaiming He وفريقه في 2015 أنّ المشكلة في الاستمثال، لا في السعة، واقترحوا الحلّ الأنيق الذي أعاد تشكيل الرؤية الحاسوبية: الوصلات المتبقّية.

مشكلة التدهور مع العمق

جرّب الفريق تدريب شبكة بـ56 طبقة على CIFAR-10، ثم قارنها بشبكة أقلّ عمقًا بـ20 طبقة. النتيجة المفارقة:

العمقخطأ التدريبخطأ التقييم
20 طبقة8.8 %9.2 %
56 طبقة11.5 %12.1 %

الشبكة الأعمق أسوأ على التدريب نفسه. المشكلة ليست فرط تخصيص. إنّها عجز عن الاستمثال. لماذا؟

الجواب في الانتشار الخلفي. عند العمق 50، يمرّ التدرّج عبر خمسين مصفوفة قبل بلوغ الطبقات الأولى. حاصل ضرب مصفوفات بأوزان أصغر من الواحد يتلاشى أُسّيًا. حتى مع تهيئة He (دورة الأساسيات، الوحدة 6)، يظلّ الأمر صعبًا في الأعماق الكبيرة.

البلوك المتبقّي: الحلّ في سطر واحد

بدلًا من أن نطلب من طبقة أن تتعلّم دالّة F(x)F(x)، نطلب منها أن تتعلّم الفرق F(x)xF(x) - x، ثم نضيف xx في النهاية. النتيجة:

y=F(x)+xy = F(x) + x

يُسمّى xx الوصلة المختصرة (skip connection)، ويُسمّى F(x)F(x) البواقي (residual). لِمَ هذا التبديل الطفيف يُغيّر كلّ شيء؟

  • إذا كانت الطبقة المثلى قريبة من الهوية، فإنّ F(x)=0F(x) = 0 حلّ سهل: يكفي أن تصير الأوزان صغيرة. تعلّم أن تكون قريبًا من الصفر أسهل بكثير من تعلّم أن تكون قريبًا من الهوية.
  • التدرّج الآن يتدفّق عبر مسارَين. المسار الرئيسي عبر FF، والمسار الجانبي (الوصلة المختصرة) مباشر بلا تحويل. مشتقّ الوصلة المختصرة yx=1\frac{\partial y}{\partial x} = 1، فيمرّ التدرّج بلا تلاش.

هذه بصيرة هندسية بسيطة تحلّ مشكلة عميقة، وهي الأساس الرياضي لكلّ الشبكات الحديثة.

تنفيذ البلوك المتبقّي

في Keras، نستخدم API الوظيفية لأنّ الوصلة المختصرة تُنشئ فرعًا لا تدعمه Sequential:

from tensorflow.keras import layers

def bloc_residuel(x, canaux, strides=1):
residuel = x
# المسار الرئيسي
x = layers.Conv2D(canaux, 3, strides=strides, padding="same")(x)
x = layers.BatchNormalization()(x)
x = layers.Activation("relu")(x)
x = layers.Conv2D(canaux, 3, padding="same")(x)
x = layers.BatchNormalization()(x)

# الوصلة المختصرة تحتاج إلى التكيّف إذا تغيّرت الأبعاد
if strides != 1 or residuel.shape[-1] != canaux:
residuel = layers.Conv2D(canaux, 1, strides=strides, padding="same")(residuel)
residuel = layers.BatchNormalization()(residuel)

x = layers.Add()([x, residuel])
x = layers.Activation("relu")(x)
return x

ثلاث ملاحظات مهمّة:

  • التطبيع بالحزم بعد كلّ التفاف: ResNet كان أوّل من عمّم هذا النمط، وأصبح معيارًا.
  • الجمع قبل التنشيط الأخير: تُضاف البواقي إلى الوصلة، ثم يُطبَّق ReLU على المجموع.
  • التكيّف بمرشّح 1×1: عندما تختلف الأبعاد بين F(x)F(x) وxx (بسبب الخطوة 2 أو تغيّر عدد القنوات)، نُغيّر شكل xx بالتفاف 1×1 حتى نستطيع جمعهما.

كتلة العنق الزجاجي (Bottleneck)

للشبكات الأعمق (ResNet50 وما فوق)، اقترح المؤلّفون بديلًا أكثر كفاءة يُسمّى العنق الزجاجي:

def bloc_bottleneck(x, canaux, strides=1):
residuel = x
# 1×1 يُقلّص العمق
x = layers.Conv2D(canaux, 1)(x)
x = layers.BatchNormalization()(x)
x = layers.Activation("relu")(x)
# 3×3 يعمل على تمثيل مضغوط
x = layers.Conv2D(canaux, 3, strides=strides, padding="same")(x)
x = layers.BatchNormalization()(x)
x = layers.Activation("relu")(x)
# 1×1 يعيد التوسّع
x = layers.Conv2D(canaux * 4, 1)(x)
x = layers.BatchNormalization()(x)

if strides != 1 or residuel.shape[-1] != canaux * 4:
residuel = layers.Conv2D(canaux * 4, 1, strides=strides)(residuel)
residuel = layers.BatchNormalization()(residuel)

x = layers.Add()([x, residuel])
x = layers.Activation("relu")(x)
return x

الحيلة: التفاف 1×1 يُقلّص عدد القنوات إلى الرُبع، ثم تُطبَّق التفاف 3×3 المكلف على تمثيل مضغوط، ثم يعيد 1×1 التوسّع إلى الأبعاد الأصلية. النتيجة: القدرة التمثيلية للبلوك تبقى شبه ثابتة، بينما التكلفة الحسابية تنكمش بأربع أضعاف. هذا هو المفتاح الذي جعل ResNet152 قابلًا للتدريب.

عائلة ResNet

النموذجعدد الكتلنوع الكتلةمعاملاتTop-1 على ImageNet
ResNet188متبقّية بسيطة11.7 مليون69.8 %
ResNet3416متبقّية بسيطة21.8 مليون73.3 %
ResNet5016عنق زجاجي25.6 مليون76.1 %
ResNet10133عنق زجاجي44.5 مليون77.4 %
ResNet15250عنق زجاجي60.2 مليون78.3 %

انتبه: ResNet50 يحمل معاملات أقلّ من ResNet34، رغم أنّه أعمق. سرّ ذلك هو العنق الزجاجي الذي يُقلّص التكلفة. وهذا يُوضّح لماذا صار ResNet50 خيار النقل المفضّل في السنوات التالية: كفاءة أعلى مقابل الدقّة.

التنشيط المسبق (Pre-activation)

في 2016، عاد الفريق نفسه على ResNet ليقترح تعديلًا بسيطًا: نقل التطبيع بالحزم وReLU قبل الالتفاف بدل بعده:

# الترتيب الأصلي: Conv → BN → ReLU
# الترتيب ما بعد التنشيط: BN → ReLU → Conv

هذا التعديل يبدو تفصيلًا، لكنّه يُحسّن التدفّق التدرّجي: الوصلة المختصرة تصبح متطابقة تمامًا بلا أيّ تدخّل، وتمرّ التدرّجات بمشتقّ 1 صرف. تدفع هذه التغييرة الشبكات إلى عمق 200 و1000 طبقة بلا تدهور. المعماريات المعاصرة (EfficientNet وConvNeXt) تعتمد كلّها على هذا النمط.

قاعدة عملية: متى تختار أيّ نسخة
  • CIFAR-10 وصور صغيرة: ResNet18 أو ResNet34 من الصفر يعطي نتائج ممتازة.
  • مهمّة نقل قياسية: ResNet50 من keras.applications هو الخيار المتوازن الأشهر.
  • مهمّة صعبة وميزانية GPU كبيرة: ResNet101 أو ResNet152.
  • مقيّد بالحساب: مرّ إلى MobileNet أو EfficientNet (الوحدة 7).

عملية على CIFAR-10 مع ResNet-صغير

نبني نسخة مصغّرة من ResNet مناسبة لصور 32×32:

import tensorflow as tf
from tensorflow.keras import layers, models

def resnet_cifar():
entree = tf.keras.Input(shape=(32, 32, 3))
x = layers.Conv2D(64, 3, padding="same")(entree)
x = layers.BatchNormalization()(x)
x = layers.Activation("relu")(x)

# مرحلة 1: 32×32×64
for _ in range(3):
x = bloc_residuel(x, 64)

# مرحلة 2: 16×16×128
x = bloc_residuel(x, 128, strides=2)
for _ in range(3):
x = bloc_residuel(x, 128)

# مرحلة 3: 8×8×256
x = bloc_residuel(x, 256, strides=2)
for _ in range(3):
x = bloc_residuel(x, 256)

x = layers.GlobalAveragePooling2D()(x)
sortie = layers.Dense(10, activation="softmax")(x)

return tf.keras.Model(entree, sortie)

modele = resnet_cifar()
modele.compile(optimizer="adam",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])

هذه شبكة بـ21 كتلة متبقّية تحمل نحو 4.5 مليون معامل. تصل إلى دقّة 92 % على CIFAR-10 مع تكثير بيانات مناسب (الوحدة 8). فرق ثلاث نقاط عن VGG-صغير بمعاملات أقلّ. الوصلات المتبقّية تدفع.

التطبيع بالحزم في وقت التقييم

اعتاد المبتدئون أن ينسوا وضع النموذج في وضع eval (أو training=False) عند التقييم. طبقات BatchNormalization في ResNet تستخدم إحصائيات الدفعة أثناء التدريب، والمتوسّطات الجارية أثناء الاستدلال. النسيان يُنتج توقّعات تتغيّر حسب حجم الدفعة، وهو من أشيع الأخطاء في الرؤية. سنعود إلى ذلك في الوحدة 9 عند النقل.

في الخلاصة

  • شبكة أعمق قد تُنتج خطأ تدريب أكبر، وهذا ليس فرط تخصيص بل مشكلة استمثال ناتجة عن تلاشي التدرّج عبر الطبقات.
  • البلوك المتبقّي y=F(x)+xy = F(x) + x يُوفّر مسارًا مباشرًا للتدرّج بمشتقّ 1، ويجعل تعلّم الهويّة سهلًا؛ هذا الحلّ البسيط أطلق عصر الشبكات العميقة جدًّا.
  • العنق الزجاجي (1×1 → 3×3 → 1×1) يُقلّص التكلفة الحسابية بأربع أضعاف، ويجعل ResNet50 يحمل معاملات أقلّ من ResNet34.
  • التنشيط المسبق (BN → ReLU → Conv) يُبقي الوصلة المختصرة متطابقة تمامًا، ويسمح بشبكات إلى عمق 1000 طبقة.

الوحدة التالية: Inception وMobileNet، حيث نستكشف معماريات أخرى تُقدّم بدائل عبقرية للكفاءة الحسابية.