الالتفاف
درس 2 من 7
2الالتفاف
الصورة الكبيرةالالتفاف فكرة واحدة: مرشّح صغير بحجم 3 في 3 ينزلق على الصورة ويسأل في كل موضع «هل هنا النمط الذي أبحث عنه؟». نفس المرشّح في كل مكان، ولهذا تحتاج الشبكة الالتفافية آلاف المعاملات بدل الملايين.
ماذا تشاهد في الرسم: المرشّح ينزلق على الصورة ويسأل في كل موضع: هل النمط هنا؟ لا يستطيع الجلوس فوق الحافة، فيخرج ناتج 2 × 4 من صورة 4 × 6. والأوزان التسعة نفسها تُستخدم في كل موضع، وهذا هو تقاسم الأوزان.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
الالتفاف مثل عدسة مكبّرة صغيرة تمرّ على الصورة مربعًا مربعًا وتبحث عن شكل واحد: حافة، أو زاوية، أو بقعة لون. الطبقات الأولى ترى خطوطًا بسيطة، والتي بعدها تجمع الخطوط إلى عيون وعجلات، وفي الأعلى ترى وجوهًا وسيارات.
المرشّح kernel مصفوفة أوزان صغيرة (مثل 3×3) تنزلق على الصورة وتحسب حاصل ضرب نقطي في كل موقع. الخطوة stride تحدد المسافة بين المواقع، والحشو padding يحفظ الحجم، والتجميع pooling يصغّر الخريطة. المشاركة في الأوزان تجعلها أقل معاملات من الطبقة الكاملة.
قبل أن تبدأ
🖼️الصورة كموتر
تعرف أن الصورة مصفوفات أرقام بترتيب (C, H, W)، فالمرشّح يمرّ على هذه الأرقام.
🧱الطبقات والتطبيع
تعرف الطبقة الخطية وكيف تعدّ معاملاتها، فستقارنها هنا بطبقة الالتفاف.
الفكرة الجوهرية
الالتفاف مرشّح صغير ينزلق على الصورة. تبني هنا المرشّح بيدك، ثم تفهم لماذا يصغر الناتج وكيف تتحكّم بحجمه، ثم التجميع، ثم سبب نجاح الفكرة كلها: تقاسم الأوزان.
1المرشّح: اضرب واجمع
المرشّح مصفوفة أوزان صغيرة، 3 في 3 غالبًا. يقف فوق رقعة من الصورة، يضرب كل رقم في الوزن المقابل، ويجمع الناتج في رقم واحد.
انظر إلى الرسم: صف واحد من الناتج. الرقم كبير حيث تتغيّر الإضاءة، وصفر حيث لا تتغيّر.
import numpy as np
image = np.array([
[0, 0, 0, 9, 9, 9],
[0, 0, 0, 9, 9, 9],
[0, 0, 0, 9, 9, 9],
[0, 0, 0, 9, 9, 9],
], dtype=float)
kernel = np.array([[-1, 0, 1],
[-1, 0, 1],
[-1, 0, 1]], dtype=float)
def conv2d(img, k, stride=1):
kh, kw = k.shape
out_h = (img.shape[0] - kh) // stride + 1
out_w = (img.shape[1] - kw) // stride + 1
out = np.zeros((out_h, out_w))
for i in range(out_h):
for j in range(out_w):
patch = img[i * stride:i * stride + kh, j * stride:j * stride + kw]
out[i, j] = (patch * k).sum()
return out
print(conv2d(image, kernel))
العمودان في المنتصف 27، والأطراف صفر. المرشّح وجد الحافة بالضبط حيث هي. هذا كل ما تفعله الطبقة الالتفافية الأولى في أي شبكة: عشرات المرشّحات مثل هذا، لكنها تتعلّم أرقامها بنفسها بدل أن نكتبها نحن.
2لماذا يصغر الناتج: الحشو
المرشّح لا يخرج عن حدود الصورة، فعدد المواضع التي يقف فيها أقل من عدد البكسلات.
صورة 4 في 6 ومرشّح 3 في 3 أعطيا ناتجًا 2 في 4، لأن المرشّح لا يستطيع الوقوف على الحافة. الحشو (Padding) يضيف إطارًا من الأصفار حتى يبقى الحجم كما هو، والخطوة (Stride) تقفز بالمرشّح أكثر من بكسل فيصغر الناتج عمدًا.
مع مرشّح 3 في 3، الإعداد padding=1 يحافظ على الحجم كما هو. هذا أكثر إعداد شيوعًا في الشبكات الحديثة.
3الخطوة وقانون الحجم
الخطوة stride=2 تقفز بالمرشّح بكسلين كل مرة، فيصغر الناتج للنصف. وكل ذلك يجمعه قانون واحد: (n + 2p - k) / s + 1.
انظر إلى الرسم: نفس الصورة 32 بثلاثة إعدادات.
def out_size(n, k, stride=1, padding=0):
return (n + 2 * padding - k) // stride + 1
print("32 k=3 ->", out_size(32, 3))
print("32 k=3 padding=1 ->", out_size(32, 3, padding=1))
print("32 k=3 stride=2 pad=1 ->", out_size(32, 3, stride=2, padding=1))
print("224 k=7 stride=2 pad=3 ->", out_size(224, 7, stride=2, padding=3))
السطر الأخير هو الطبقة الأولى في ResNet بالضبط: مرشّح 7 في 7 بخطوة 2 يحوّل 224 إلى 112. احفظ هذه المعادلة، فستحتاجها في كل مرة تصمّم فيها شبكة وتريد معرفة حجم ما يدخل الطبقة الخطية في النهاية.
4التجميع: أكبر قيمة في كل مربع
التجميع يصغّر الصورة دون أي أوزان يتعلّمها. MaxPool2d(2) يأخذ أكبر قيمة في كل مربع 2 في 2.
انظر إلى الرسم: 16 رقمًا تصبح 4.
import numpy as np
x = np.array([[1, 3, 2, 0],
[4, 6, 1, 1],
[0, 2, 9, 7],
[1, 0, 5, 8]])
pooled = x.reshape(2, 2, 2, 2).max(axis=(1, 3))
print(pooled)
كل مربع 2 في 2 صار رقمًا واحدًا هو أكبر ما فيه. لو تحرّك الرقم 9 بكسلًا واحدًا داخل مربعه، لبقي الناتج كما هو، وهذا معنى «أقل حساسية للموضع».
5تقاسم الأوزان
الالتفاف يفترض أمرين عن الصور: النمط المهم صغير ومحلي، ونفس النمط مهم أينما ظهر. لذلك نستخدم نفس المرشّح في كل موضع.
أوزان الطبقة عددها out × in × k × k فقط، مهما كبرت الصورة. قارنها الآن بطبقة خطية تعطي نفس حجم الناتج.
هذا الكود يحتاج PyTorch على جهازك، فالناتج المعروض مسجَّل من تشغيل حقيقي. ثبّته بـpip install torch وشغّله بنفسك.
from torch import nn
conv = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, padding=1)
dense = nn.Linear(3 * 32 * 32, 16 * 32 * 32)
print("conv weights: ", tuple(conv.weight.shape), sum(p.numel() for p in conv.parameters()))
print("dense weights:", sum(p.numel() for p in dense.parameters()))
طبقة التفافية بـ16 مرشّحًا: 448 معاملًا. طبقة خطية تعطي نفس حجم الناتج من نفس المدخل: أكثر من خمسين مليونًا. فرق مئة ألف ضعف، لهذا الشبكات الالتفافية ممكنة أصلًا، ولهذا تحتاج بيانات أقل لتتعلّم.
كيف يعمل، خطوة بخطوة: كيف يعمل الالتفاف على صورة
ضع المرشّح 3 × 3 فوق أول رقعة من الصورة.
اضرب كل وزن في البكسل تحته واجمع الحواصل التسعة في رقم واحد: كبير إن وُجد النمط وصفر إن لم يوجد.
حرّك المرشّح بمقدار الخطوة وكرّر حتى تغطّي الصورة، فيكون حجم الناتج (n + 2p − k) / s + 1.
كدّس طبقات كثيرة مع التجميع، فتجمع الطبقات العميقة الحواف إلى زوايا وقوام ثم أجزاء أشياء.
مثال محلول
صورة نصفها الأيسر أسود (0) والأيمن أبيض (9)، أي فيها حافة عمودية واحدة في المنتصف. ومرشّح يطرح العمود الأيسر من الأيمن: سيعطي رقمًا كبيرًا حيث تتغيّر الإضاءة، وصفرًا حيث لا تتغيّر.
mat1 and mat2 shapes cannot be multiplied. احسب بالمعادلة، أو أسهل: مرّر موترًا عشوائيًا بالحجم الصحيح عبر الجزء الالتفافي واطبع شكله.
جرّب بلا كود
MaxPool2d(2). ما الحجم في الآخر؟ استخدم (n + 2p - k) / s + 1 مع تقريب للأسفل. الجواب: (64 + 2 - 3) / 2 + 1 = 32، ثم التجميع يعطي 16.خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| أوزان المرشّح يكتبها المهندس بيده. | كتبنا مرشّح الحافة في الدرس للشرح فقط. في الشبكة الحقيقية تتعلّم المرشّحات أرقامها بالتدريب. |
| عدد معاملات طبقة الالتفاف يكبر مع حجم الصورة. | لا يعتمد على حجم الصورة أبدًا. Conv2d(16, 32, 3) لها 4640 معاملًا على أي صورة، وهذه فائدة تقاسم الأوزان. |
| مرشّح 3 × 3 لا يرى إلا 3 × 3 بكسل مهما كانت الشبكة. | كل طبقة ترى 3 × 3 من الطبقة قبلها، فيتّسع مجال الرؤية مع العمق، والتجميع والخطوات تسرّع اتّساعه. |
جرّب بنفسك
صورة 64 في 64، ومرشّح 5 في 5، وخطوة 1، وبلا حشو. ما حجم الناتج؟
(64 + 0 − 5) / 1 + 1 = 60. المرشّح يفقد بكسلين من كل جانب لأنه لا يستطيع الوقوف على الحافة. مع padding=2 سيبقى 64.
ما عدد معاملات nn.Conv2d(16, 32, kernel_size=3)؟
كل مرشّح من الـ32 يغطّي كل القنوات الـ16 بحجم 3 في 3، وله انحياز واحد. والعدد لا يعتمد على حجم الصورة إطلاقًا، وهذه بالضبط فائدة مشاركة الأوزان.
بطاقات مراجعة سريعة
(n + 2p − k) / s + 1.تحقق من فهمك
كيف تستطيع شبكة بمرشّحات 3 في 3 فقط أن «ترى» شيئًا يغطّي نصف الصورة؟
بالتكديس. كل طبقة ترى 3 في 3 من الطبقة التي قبلها، والتي رأت بدورها 3 في 3 من قبلها، فيكبر «مجال الرؤية» مع العمق. التجميع والخطوات تسرّع ذلك لأن كل بكسل بعد التصغير يمثّل منطقة أكبر من الأصل. بعد عشر طبقات مع بعض التصغير، كل خلية في العمق ترى أغلب الصورة.
صورة 32 في 32 ومرشّح 3 × 3 بخطوة 2 وحشو 1. ما حجم الناتج؟
(32 + 2 − 3) // 2 + 1 = 16، أي 16 في 16: الخطوة 2 نصّفت الصورة.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي2
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.