0%
الرؤية الحاسوبية

الرؤية

كيف تستخلص الآلة المعلومات من صورة.

10-15 دقيقة

الرؤية الحاسوبية

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

الصعوبة ليست في نقص المعلومات، بل في وفرتها المفرطة. فصورة التقطها هاتف تحتوي على ملايين الأعداد، لا يعني أي منها شيئًا بمفرده. إن قيمة البكسل لا تخبرنا بشيء؛ بل التنظيم المحلي لآلاف البكسلات هو الذي يشكّل حافة، وتنظيم آلاف الحواف هو الذي يشكّل وجهًا. ويجب التعرّف على هذا التنظيم أيًّا كان موضع الجسم، وإضاءته، وحجمه، واتجاهه.

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

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

الصورة مصفوفة من الأعداد

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

from sklearn.datasets import load_digits

chiffres = load_digits()
print(chiffres.images.shape)   # (1797, 8, 8)
print(chiffres.images.min(), chiffres.images.max())   # 0.0 16.0
print(chiffres.target[0])      # 0

print(chiffres.images[0].astype(int))
# [[ 0  0  5 13  9  1  0  0]
#  [ 0  0 13 15 10 15  5  0]
#  [ 0  3 15  2  0 11  8  0]
#  [ 0  4 12  0  0  8  8  0]
#  [ 0  5  8  0  0  9  8  0]
#  [ 0  4 11  0  1 12  7  0]
#  [ 0  2 14  5 10 12  0  0]
#  [ 0  0  6 13 10  0  0  0]]

عند تضييق العينين قليلًا يمكن تخمين الصفر: عمودان من القيم المرتفعة يفصل بينهما فراغ مركزي من الأصفار. وهذا بالضبط ما تتلقاه الآلة، لا أكثر ولا أقل.

الحجم هو العقبة الأولى. تستخدم مجموعة البيانات هذه صورًا مصغّرة بأبعاد 8 × 8، أي 64 عددًا لكل صورة. أما الصورة المعتادة المدخلة إلى شبكة رؤية فأبعادها 224 × 224 بكسل على 3 قنوات:

print(224 * 224 * 3)   # 150528

150 528 عددًا لصورة واحدة. وكل ما يلي يتمحور حول معالجة هذا الحجم دون أن يتضخّم عدد المعاملات الواجب تعلّمها.

الالتفاف: اكتشاف نمط أينما كان

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

import numpy as np

# Une barre verticale claire sur fond sombre.
image = np.array([
    [0, 0, 9, 9, 0, 0],
    [0, 0, 9, 9, 0, 0],
    [0, 0, 9, 9, 0, 0],
    [0, 0, 9, 9, 0, 0],
], dtype=float)

# Noyau de Sobel simplifié : détecteur de contour vertical.
noyau = np.array([[1.0, 0.0, -1.0],
                  [1.0, 0.0, -1.0],
                  [1.0, 0.0, -1.0]])

def convoluer(image, noyau):
    h, w = image.shape
    kh, kw = noyau.shape
    sortie = np.zeros((h - kh + 1, w - kw + 1))
    for y in range(sortie.shape[0]):
        for x in range(sortie.shape[1]):
            fenetre = image[y:y + kh, x:x + kw]
            sortie[y, x] = float((fenetre * noyau).sum())
    return sortie

carte = convoluer(image, noyau)
print(carte)
# [[-27. -27.  27.  27.]
#  [-27. -27.  27.  27.]]

لنتحقق من قيمة واحدة يدويًا. في الأعلى جهة اليمين تغطي النافذة الأعمدة 3 و4 و5، أي القيم [[9,0,0],[9,0,0],[9,0,0]]. وناتج الضرب في النواة هو 9×1 + 0×0 + 0×(−1) مكرّرًا على ثلاثة أسطر، أي 27. تدل القيمة +27 على انتقال من الفاتح إلى الداكن. وفي الجهة اليسرى، يعطي الانتقال نفسه بالاتجاه المعاكس −27. وفي وسط النمط، ستعطي نواة متماثلة 0: لا شيء يستدعي الإبلاغ.

ثلاث خصائص تصنع القوة الكاملة لهذه العملية:

  • المحلية. كل مخرَج لا يعتمد إلا على جوار صغير، وهذا يوافق البنية الفعلية للصور: فالحافة ظاهرة محلية.
  • مشاركة الأوزان. الأوزان التسعة للنواة هي نفسها في كل المواضع. فالكاشف الذي يُتعلَّم مرة واحدة يعمل في كل مكان من الصورة.
  • الاقتصاد. وهو النتيجة الرقمية للنقطة السابقة، وهي نتيجة مذهلة.
# Une couche dense reliant tous les pixels d'une image 224×224×3 à 1000 neurones :
print(150528 * 1000 + 1000)      # 150529000

# Une couche convolutive de 64 noyaux de 3×3 sur 3 canaux :
print(3 * 3 * 3 * 64 + 64)       # 1792

150 مليون معامل مقابل 1 792. أي بفارق يقارب 84 000 ضعف. ومن دون مشاركة الأوزان ما كان التعلّم العميق على الصور ليصبح عمليًا، وليس ذلك مسألة صبر: فعدد الأمثلة اللازمة لضبط 150 مليون معامل في طبقة واحدة يفوق كل منال.

التصغير والتكديس ورؤية مجال أوسع

لا ينظر الالتفاف المنفرد إلا إلى جوار بأبعاد 3 × 3. ولكي نتعرّف على جسم كامل، يجب توسيع مجال الرؤية تدريجيًا. وتعمل على ذلك آليتان.

الأولى هي خفض العينات (sous-échantillonnage)، وأشهر أشكاله يحتفظ بالقيمة العظمى في كل كتلة:

def pooling_max(carte, taille=2):
    h, w = carte.shape
    return carte[:h - h % taille, :w - w % taille].reshape(
        h // taille, taille, w // taille, taille).max(axis=(1, 3))

print(pooling_max(np.abs(carte)))   # [[27. 27.]]

تتحوّل الخريطة ذات الأبعاد 2 × 4 إلى 1 × 2. نفقد الموضع الدقيق للحافة ونحتفظ بشدتها: وهذا بالضبط التسوية المطلوبة في التصنيف، حيث إن معرفة وجود حافة أهم من معرفة موضعها بدقة البكسل.

الآلية الثانية هي التكديس. فكل طبقة ترى مخرجات الطبقة السابقة، أي جوارًا أوسع فأوسع من الصورة الأصلية:

Couche 1  (3×3 pixels vus)    →  contours, taches de couleur
Couche 2  (~7×7 pixels vus)   →  coins, angles, courbes
Couche 3  (~15×15)            →  motifs : œil, roue, texture
Couche 4+ (une grande partie) →  parties d'objets, puis objets

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

وثمة تاريخان يحكمان هذه القصة: شبكة LeNet-5 التي نشرها يان لوكون وزملاؤه عام 1998 طبّقت هذا المخطط فعلًا على قراءة الشيكات؛ أما AlexNet عام 2012 فقد نقلته إلى مقياس ImageNet وأطلقت الموجة الحالية.

التصنيف والكشف والتجزئة

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

المهمةالسؤالالمخرَج لصورة 224×224 و20 فئة
التصنيفما الموجود في هذه الصورة؟20 عددًا (احتمال لكل فئة)
الكشفأين الأجسام، وما هي؟قائمة: الفئة والدرجة و4 إحداثيات لكل جسم
التجزئةإلى أي فئة ينتمي كل بكسل؟224 × 224 × 20 = 1 003 520 عدد
print(224 * 224 * 20)   # 1003520

الفارق بين 20 ومليون قيمة مخرَجة يفسّر الجزء الأكبر من الأمر: فالتجزئة تتطلب توسيمًا بدقة البكسل، وكلفة إنتاجه لا تُقارن بكلفة وسم صورة بتسمية واحدة.

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

def iou(a, b):
    """Intersection sur union de deux boîtes (x1, y1, x2, y2)."""
    x1 = max(a[0], b[0]); y1 = max(a[1], b[1])
    x2 = min(a[2], b[2]); y2 = min(a[3], b[3])
    inter = max(0, x2 - x1) * max(0, y2 - y1)
    aire_a = (a[2] - a[0]) * (a[3] - a[1])
    aire_b = (b[2] - b[0]) * (b[3] - b[1])
    return inter / (aire_a + aire_b - inter)

print(round(iou((10, 10, 50, 50), (30, 30, 70, 70)), 3))   # 0.143
print(round(iou((10, 10, 50, 50), (12, 12, 52, 52)), 3))   # 0.822

الحالة الأولى: صندوقان بأبعاد 40 × 40 مزاحان بمقدار 20 بكسلًا يتداخلان على 20 × 20 = 400 بكسل، والاتحاد 1600 + 1600 − 400 = 2800، أي 400 / 2800 ≈ 0.143. وتقضي الاصطلاحات المتّبعة باعتبار الكشف صحيحًا إذا تجاوزت القيمة 0.5. أما الصندوق الثاني المزاح بمقدار بكسلين فقط فيتداخل على 38 × 38 = 1 444 بكسلًا، والاتحاد 1 756، أي 0.822: كشف صحيح.

نقل التعلّم

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

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

ويمكن قياس هذا الاقتصاد رقميًا. فمع جذع ينتج متجهًا من 2048 خاصية و10 فئات مستهدفة، تضم الطبقة المراد تعلّمها 2048 × 10 + 10 = 20 490 معاملًا، بدلًا من عشرات الملايين في الشبكة الكاملة.

يبقى أن نعرف هل يتحقق المبدأ فعلًا. لنختبره بأمانة: سنُجري «تدريبًا مسبقًا» لتمثيل على الأرقام من 5 إلى 9، ثم نتعلّم التعرّف على الأرقام من 0 إلى 4 بخمسة أمثلة فقط لكل فئة.

import numpy as np
from sklearn.datasets import load_digits
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

X, y = load_digits(return_X_y=True)

source = X[y >= 5]                       # « pré-entraînement » : classes 5 à 9
cible_X, cible_y = X[y < 5], y[y < 5]    # tâche cible : classes 0 à 4

Xp, Xt, yp, yt = train_test_split(cible_X, cible_y, test_size=0.4,
                                  random_state=0, stratify=cible_y)

indices = np.concatenate([np.where(yp == c)[0][:5] for c in range(5)])

brut = LogisticRegression(max_iter=5000).fit(Xp[indices], yp[indices])
print(round(brut.score(Xt, yt), 3))                       # 0.936

# La représentation est apprise sur les classes 5-9 uniquement,
# sans jamais voir une étiquette de la tâche cible.
pca = PCA(n_components=30, random_state=0).fit(source)
tete = LogisticRegression(max_iter=5000).fit(pca.transform(Xp[indices]), yp[indices])
print(round(tete.score(pca.transform(Xt), yt), 3))         # 0.931

النتيجة تستحق تعليقًا صريحًا: التمثيل المنقول لا يتفوّق على البكسلات الخام (0.931 مقابل 0.936). فالتجربة لا تثبت فائدة النقل، ومن غير الأمين عرضها كما لو كانت تثبتها.

هذه النتيجة السلبية مفيدة. فالنقل لا يضيف شيئًا هنا لثلاثة أسباب يمكن تحديدها: الصورة المصغّرة 8 × 8 لا تحتوي إلا على 64 قيمة، وهي قابلة للاستغلال مباشرة؛ وتحليل المكوّنات الرئيسية تحويل خطي، عاجز عن بناء التسلسل الهرمي للأنماط الذي يمنح الجذع الالتفافي قيمته؛ و30 مكوّنًا من أصل 64 بُعدًا لا تضغط شيئًا تقريبًا. إن النقل يحقق مكاسبه الموثّقة حين يكون الجذع عميقًا وغير خطي ومدرَّبًا مسبقًا على صور أغنى بكثير من هذه الصور المصغّرة. أما الآلية المعروضة أعلاه، أي تجميد التمثيل وتعلّم الرأس وحده، فهي التي تُستخدم عمليًا؛ لكن الحجم هو الذي يجعلها مجدية، وليس الحجم المستخدم هنا.

خلاصة

  • الصورة مصفوفة من الأعداد، والمشكلة في كثرتها: 150 528 قيمة لصورة ملوّنة واحدة بأبعاد 224 × 224.
  • يمرّر الالتفاف نواة صغيرة ذات أوزان مشتركة على الصورة كلها. وهذه المشاركة هي التي تجعل الأمر ممكنًا: 1 792 معاملًا بدلًا من 150 مليونًا لطبقة كثيفة مكافئة.
  • إن خفض العينات وتكديس الطبقات يوسّعان مجال الرؤية تدريجيًا ويُنتجان تسلسلًا هرميًا (حواف، أنماط، أجزاء أجسام) لم يحدده أحد.
  • يختلف التصنيف والكشف والتجزئة في شكل المخرَج، من 20 عددًا إلى أكثر من مليون. وتتبع كلفة التوسيم التدرّج نفسه.
  • يقوم نقل التعلّم على تجميد جذع مدرَّب مسبقًا وإعادة تعلّم طبقة القرار وحدها، أي نحو 20 000 معامل بدلًا من ملايين. وتعتمد فائدته على غنى الجذع: فعلى صور مصغّرة 8 × 8 مع تحويل خطي، لا تُظهر التجربة أعلاه أي فائدة.

التعليقات

التعليقات مدعومة عبر GitHub Discussions

سجّل الدخول باستخدام GitHub للمشاركة في النقاش

تم نسخ الرابط!