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

الوحدة 7 — Inception وMobileNet وكفاءة الحساب

بعد ResNet، طرح الباحثون سؤالًا جديدًا: كيف نحصل على الدقّة نفسها بحساب أقلّ؟ الجواب على هذا السؤال حاسم عندما يجب أن تعمل الشبكة على هاتف محمول أو طرف حافّي أو خدمة تُخدَم لملايين الطلبات في الثانية. تعرض هذه الوحدة معماريتين رائدتين للكفاءة: Inception بفروعها المتوازية، وMobileNet بالتفافها المفصول بالعمق. ونختم بلمحة عن EfficientNet الذي يجمع الفكرتين.

Inception: عدّة أحجام مرشّحات في نفس الوقت

جاء اختراع Inception (Szegedy وآخرون 2014) من ملاحظة بسيطة: عند تصميم شبكة، نُجبَر على اختيار حجم مرشّح لكلّ طبقة. مرشّح 3×3 يلتقط تفاصيل صغيرة، 5×5 يلتقط سياقًا أوسع، 7×7 يلتقط منطقة كبيرة. لماذا الاختيار؟ لماذا لا تُطبَّق كلّ الأحجام في نفس الوقت وتترك للشبكة أن تختار من التمثيلات الأكثر إفادة؟

هذا هو بلوك Inception: أربعة فروع متوازية بدخل مشترك، تُدمج نتائجها بالتلاصق (concatenation) على محور القنوات:

from tensorflow.keras import layers

def bloc_inception(x, c1x1, c3x3_r, c3x3, c5x5_r, c5x5, cpp):
# فرع 1: التفاف 1×1
branche1 = layers.Conv2D(c1x1, 1, padding="same", activation="relu")(x)

# فرع 2: 1×1 يخفض العمق ثم 3×3
branche2 = layers.Conv2D(c3x3_r, 1, padding="same", activation="relu")(x)
branche2 = layers.Conv2D(c3x3, 3, padding="same", activation="relu")(branche2)

# فرع 3: 1×1 يخفض العمق ثم 5×5
branche3 = layers.Conv2D(c5x5_r, 1, padding="same", activation="relu")(x)
branche3 = layers.Conv2D(c5x5, 5, padding="same", activation="relu")(branche3)

# فرع 4: تجميع أقصى ثم 1×1
branche4 = layers.MaxPooling2D(3, strides=1, padding="same")(x)
branche4 = layers.Conv2D(cpp, 1, padding="same", activation="relu")(branche4)

# دمج الفروع على محور القنوات
return layers.Concatenate(axis=-1)([branche1, branche2, branche3, branche4])

الفكرة الذهنية: التفاف 1×1 يُقلّص العمق (كما في العنق الزجاجي) قبل تطبيق التفاف أكبر، مما يُوفّر حسابًا كبيرًا. كلّ فرع يتخصّص، والتلاصق يترك للطبقة التالية اختيار المُهمّ. GoogLeNet (Inception-v1) هزمت VGG في ImageNet 2014 بربع عدد معاملاتها، وذلك دليل أنّ الذكاء المعماري يفوق الحجم الخام.

MobileNet: الالتفاف المفصول بالعمق

MobileNet (Howard وآخرون 2017) يذهب أبعد. الفكرة الرئيسية: فصل الالتفاف إلى عمليتَين.

الالتفاف الكلاسيكي بمرشّح K×KK \times K على دخل بعمق CinC_{\text{in}} وخرج بعمق CoutC_{\text{out}} يحمل K×K×Cin×CoutK \times K \times C_{\text{in}} \times C_{\text{out}} معاملًا، ويستهلك H×W×K2×Cin×CoutH \times W \times K^2 \times C_{\text{in}} \times C_{\text{out}} عملية ضرب-جمع.

الالتفاف المفصول بالعمق يُقسّم هذا العمل إلى خطوتين:

  1. الالتفاف العمقي (depthwise convolution): مرشّح K×KK \times K مستقلّ لكلّ قناة دخل. يُنتج CinC_{\text{in}} خريطة ميزات. لا يخلط القنوات.
  2. الالتفاف النقطي (pointwise convolution): التفاف 1×1 يُطبَّق على CinC_{\text{in}} قنوات لإنتاج CoutC_{\text{out}} قنوات. يخلط القنوات ولا يعمل مكانيًا.

عدد المعاملات: K×K×Cin+Cin×CoutK \times K \times C_{\text{in}} + C_{\text{in}} \times C_{\text{out}}.

على مثال ملموس: مرشّح 3×3 على 128 → 256 قناة. الالتفاف العادي: 3×3×128×256=2949123 \times 3 \times 128 \times 256 = 294912 معاملًا. المفصول بالعمق: 3×3×128+128×256=339203 \times 3 \times 128 + 128 \times 256 = 33920 معاملًا. توفير 88 %.

from tensorflow.keras import layers

def bloc_separable(x, canaux, strides=1):
# التفاف عمقي 3×3
x = layers.DepthwiseConv2D(3, strides=strides, padding="same")(x)
x = layers.BatchNormalization()(x)
x = layers.ReLU(max_value=6.0)(x) # ReLU6 كما في MobileNet

# التفاف نقطي 1×1
x = layers.Conv2D(canaux, 1)(x)
x = layers.BatchNormalization()(x)
x = layers.ReLU(max_value=6.0)(x)
return x

DepthwiseConv2D في Keras هو تحديدًا هذا: مرشّح 3×3 مستقلّ لكلّ قناة. الفكرة عبقرية ببساطتها: الالتفاف الكامل يفعل شيئين متزامنَين — تركيب مكاني وخلط قنوات — بينما لا حاجة إلى الاثنَين معًا في كلّ مرّة. الفصل يعطي كلّ عملية معاملات مخصّصة لها.

MobileNet-v2 والوصلات المتبقّية المقلوبة

MobileNet-v2 (2018) يضيف تعديلين:

  • الوصلات المتبقّية كما في ResNet.
  • الوصلات المقلوبة (inverted residuals): بدلًا من تقليل العمق ثم توسيعه، توسيع العمق ثم تقليصه. الحدس: DepthwiseConv2D تعمل على تمثيل ضيّق فتفقد المعلومة، والتوسّع يُصلح ذلك.
def bloc_inverted_residual(x, canaux, expansion=6, strides=1):
entree = x
canaux_expansion = x.shape[-1] * expansion

# توسيع بـ1×1
x = layers.Conv2D(canaux_expansion, 1)(x)
x = layers.BatchNormalization()(x)
x = layers.ReLU(max_value=6.0)(x)

# التفاف عمقي على التمثيل الموسَّع
x = layers.DepthwiseConv2D(3, strides=strides, padding="same")(x)
x = layers.BatchNormalization()(x)
x = layers.ReLU(max_value=6.0)(x)

# تخفيض خطّي (بلا ReLU) لأنّ التمثيل المضغوط حسّاس
x = layers.Conv2D(canaux, 1)(x)
x = layers.BatchNormalization()(x)

if strides == 1 and entree.shape[-1] == canaux:
x = layers.Add()([x, entree])
return x

ملاحظة مهمّة: عدم وضع تنشيط ReLU في نهاية البلوك. لماذا؟ لأنّ التمثيل هنا ذو أبعاد قليلة، وReLU تُقصّ نصف القيم على الصفر، فتُفقد معلومات ثمينة. هذا يُسمّى «الطبقة الخطّية الضيّقة»، وهو تفصيل يُغيّر الأداء ملحوظًا.

FLOPs مقابل زمن الاستدلال الحقيقي

كثيرًا ما تُقارَن المعماريات بعدد عمليات الفاصلة العائمة (FLOPs). المقياس مفيد لكن مضلّل:

النموذجFLOPsزمن على GPU (نسبي)زمن على CPU (نسبي)
ResNet504.1G1.0×1.0×
MobileNet-v20.3G0.7×0.4×
EfficientNet-B00.4G0.9×0.5×

MobileNet-v2 يستهلك 14 مرّة أقلّ من FLOPs، لكنّه ليس 14 مرّة أسرع. لماذا؟ لأنّ الالتفاف المفصول بالعمق ليس صديقًا لتوازي GPU: الالتفاف العمقي يفتقر إلى كفاءة تنفيذ الالتفاف الكلاسيكي على أنوية GPU المُحسَّنة. على CPU وعلى شرائح الأجهزة المحمولة (حيث لا يوجد توازي واسع)، يعمل MobileNet جيّدًا. على GPU كبيرة (V100)، ResNet50 قد يفوز رغم عدد FLOPs الأكبر.

FLOPs مقياس غير موثوق للزمن الحقيقي

لا تخترْ معمارية على أساس FLOPs وحدها. المقياس الوحيد الموثوق هو الاستدلال على العتاد المستهدَف بحجم الدفعة الفعلي. الإطار (TensorFlow Lite أو ONNX Runtime أو TensorRT) وشكل التسريع (كمّي أو نصفي) يُغيّران الرتبة أحيانًا.

EfficientNet: التوسّع المركّب

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

depth=αϕ,width=βϕ,resolution=γϕ\text{depth} = \alpha^{\phi}, \quad \text{width} = \beta^{\phi}, \quad \text{resolution} = \gamma^{\phi}

مع قيود αβ2γ22\alpha \cdot \beta^2 \cdot \gamma^2 \approx 2 لضمان تضاعف FLOPs عند زيادة ϕ\phi بواحد. اختيار المعاملات α\alpha وβ\beta وγ\gamma يتمّ ببحث معماري (NAS) على شبكة أساسية صغيرة.

النتيجة: عائلة من ثمانية نماذج B0 إلى B7 مرتبطة بمعامل توسّع واحد. B0 يعمل على هاتف، B7 يحطّم أرقام ImageNet. المرونة المذهلة، وهي السبب في هيمنة EfficientNet على معايير النقل بين 2019 و2021.

import tensorflow as tf
from tensorflow.keras.applications import EfficientNetB0

modele_base = EfficientNetB0(
weights="imagenet",
include_top=False,
input_shape=(224, 224, 3),
)

مقارنة عملية: أيّ معمارية أختار؟

السياقالمعمارية الموصى بها
نقل قياسي، GPU، دقّة عاليةResNet50 أو EfficientNet-B4
ميزانية GPU محدودة، مهمّة سهلةResNet18 أو ResNet34
هاتف محمول أو طرف حافّيMobileNet-v2 أو MobileNet-v3
تصنيف صعب مع بيانات كثيرةEfficientNet-B5 إلى B7
زمن استدلال حرج على CPUMobileNet مع تكميم
قبل التصميم من الصفر، جرّب النقل

معماريات الوحدات 4 إلى 7 كلّها متوفّرة كنماذج مُدرَّبة مسبقًا في keras.applications. في 95 % من مسائل الرؤية العملية، نقل ResNet50 مُدرَّب على ImageNet أفضل من أي معمارية تُبنى من الصفر لمهمّتك. سنُفصّل ذلك في الوحدتين القادمتين.

في الخلاصة

  • Inception يُطبّق مرشّحات بأحجام متعدّدة في فروع متوازية يدمجها بالتلاصق، ويسمح للشبكة باختيار المستوى الحبيبي المناسب.
  • الالتفاف المفصول بالعمق يُقسّم الالتفاف إلى عمقي (مكاني، مستقلّ لكلّ قناة) ونقطي (خلط القنوات بـ1×1)، ويُوفّر ما يقارب 90 % من المعاملات.
  • FLOPs ليست الزمن: MobileNet أقلّ FLOPs لكنّه ليس دائمًا أسرع على GPU، لأنّ الالتفاف العمقي أقلّ صداقة للتوازي؛ قِسْ على العتاد المستهدَف.
  • EfficientNet يُطبّق توسّعًا مركّبًا للعمق والعرض والدقّة معًا، ويُنتج عائلة نماذج من B0 (على الهاتف) إلى B7 (لأعلى دقّة).

الوحدة التالية: تكثير البيانات للصور، حيث نُضاعف قسمة بياناتنا بدون جمع صورة إضافية، مع تجنّب التكثيرات التي تخرّب المهمّة.