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

الوحدة 9 — التعلّم بالنقل مع torchvision

بلغنا في الوحدة الثامنة نموذجًا يعمل من طرف إلى طرف على Fashion-MNIST، مبنيًّا من الصفر. الحقيقة أنّ التدريب من الصفر نادرًا ما يكون القرار الصحيح في الرؤية الحاسوبية اليوم. إعادة استعمال شبكة مُدرَّبة مسبقًا على ImageNet ثم ضبطها على مهمّتك تُعطي نتائج أفضل بكثير من الصفر، في وقت أقلّ. هذه الوحدة تشرح كيف نفعل ذلك في PyTorch عبر torchvision.models.

لماذا يعمل النقل: الطبقات الأولى عامّة

شبكة تلافيفية عميقة تُدرَّب على ImageNet تتعلّم أوّلاً كشف الحواف والزوايا، ثم القوامات والألوان، ثم أجزاء الأشياء، ثم الأشياء الكاملة. الطبقات الأولى لا تختصّ بأصناف ImageNet: كاشف الحواف مفيد لأيّ صورة، بما فيها صور الملابس في Fashion-MNIST. الطبقات الأخيرة وحدها هي المُتخصّصة، وهي التي نستبدل.

تحميل نموذج مُدرَّب مسبقًا

torchvision.models يوفّر عشرات المعماريّات مع أوزان جاهزة:

import torch
from torchvision import models

# الطريقة الحديثة (منذ torchvision 0.13)
poids = models.ResNet18_Weights.IMAGENET1K_V1
modele = models.resnet18(weights=poids)

الاستدعاء يُنزّل الأوزان أوّل مرّة (نحو 45 ميغابايت لـResNet18) ويحفظها في مجلّد داخليّ. الاستدعاءات اللاحقة تقرأ من الذاكرة المحلية. لا حاجة إلى تحضير أيّ ملفّ بنفسك.

استبدال الرأس

ResNet18 مُدرَّب على 1000 صنف؛ نريده على 10 أصناف. نستبدل الطبقة الأخيرة:

import torch.nn as nn

# الطبقة الأخيرة تُسمّى fc في ResNet
n_car = modele.fc.in_features # 512 في ResNet18
modele.fc = nn.Linear(n_car, 10) # رأس جديد لعشرة أصناف

نلاحظ ثلاث نقاط. أوّلًا، حفظ in_features قبل الاستبدال ضروريّ لأنّ الطبقة القديمة تختفي في السطر التالي. ثانيًا، الرأس الجديد يُهيَّأ عشوائيًّا: أوزانه لم تُدرَّب بعد. ثالثًا، اسم الطبقة الأخيرة يختلف حسب المعمارية: fc في ResNet، classifier في VGG، heads.head في Vision Transformer. لا اسم موحّد.

المرحلة الأولى: تجميد القاعدة

نجمّد كلّ الأوزان الأصلية، ونُدرّب الرأس وحده:

for parametre in modele.parameters():
parametre.requires_grad = False # تجميد كامل

# ثم رأس جديد يحمل requires_grad=True افتراضيًّا
modele.fc = nn.Linear(n_car, 10)

# لا نُمرِّر للمُحسِّن إلّا الأوزان التي تحتاج تدرّجًا
opt = torch.optim.AdamW(
[p for p in modele.parameters() if p.requires_grad],
lr=1e-3,
weight_decay=1e-4,
)

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

المرحلة الثانية: الضبط الدقيق بمعدّل صغير

بعد استقرار الرأس، نُذيب بعض الطبقات الأخيرة من القاعدة ونُتابع التدريب بمعدّل تعلّم أصغر بكثير:

# نُذيب آخر بلوك (layer4 في ResNet)
for parametre in modele.layer4.parameters():
parametre.requires_grad = True

# مُحسِّن جديد يشمل الجميع، بمعدّلَين متمايزَين
opt = torch.optim.AdamW([
{"params": modele.layer4.parameters(), "lr": 1e-5}, # قاعدة: ضبط دقيق
{"params": modele.fc.parameters(), "lr": 1e-3}, # رأس: تدريب عاديّ
], weight_decay=1e-4)

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

نسبة الطبقات المُذابة: كم من الشبكة يجب إذابتها

قاعدة تجريبية:

البيانات وحجم المسألةما نُذيب
مهمّة قريبة جدًّا من ImageNet وبيانات قليلةالرأس فقط
مهمّة قريبة وبيانات متوسّطةالرأس + آخر بلوك
مهمّة بعيدة وبيانات متوسّطةالرأس + آخر بلوكين
مهمّة بعيدة جدًّا أو بيانات كثيرةالشبكة كاملةً بضبط دقيق

Fashion-MNIST يقع في الخانة الأولى تقريبًا: صور صغيرة رمادية، بعيدة عن ImageNet شكليًّا. لكنّ الطبقات الأولى مفيدة رغم ذلك (كواشف الحواف). عمليًّا، الرأس وحده يكفي هنا، وإذابة بلوك أخير قد تُضيف قليلاً.

التطبيع الذي ينتظره النموذج

هذا هو الفخّ الأشيع في التعلّم بالنقل. كلّ شبكة على ImageNet مُدرَّبة على صور طُبِّعَت بمتوسّطات وانحرافات محدّدة:

from torchvision import transforms

# متوسّطات وانحرافات ImageNet: احفظها عن ظهر قلب
prep = transforms.Compose([
transforms.Resize(224),
transforms.CenterCrop(224),
transforms.Grayscale(num_output_channels=3), # Fashion-MNIST رمادية
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225],
),
])

تمرير صور بمقياس مختلف يُدهور الأداء بصمت. الطريقة الأنظف هي أخذ التحويل من الأوزان مباشرةً:

poids = models.ResNet18_Weights.IMAGENET1K_V1
prep = poids.transforms() # كلّ ما ينتظره النموذج

هذا يُلغي كلّ خطر النسيان أو الخطأ في القيم. اعتمده كلّما أمكن.

Fashion-MNIST مع ResNet18: المشروع الكامل

نُطبِّق كلّ ما سبق:

import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import datasets, models

appareil = "cuda" if torch.cuda.is_available() else "cpu"

# 1. البيانات مع التطبيع المناسب
poids = models.ResNet18_Weights.IMAGENET1K_V1
prep = poids.transforms()

jeu_tr = datasets.FashionMNIST("./donnees", train=True, download=True, transform=prep)
jeu_val = datasets.FashionMNIST("./donnees", train=False, download=True, transform=prep)
charg_tr = DataLoader(jeu_tr, batch_size=64, shuffle=True, num_workers=2, pin_memory=True)
charg_val = DataLoader(jeu_val, batch_size=128, shuffle=False, num_workers=2, pin_memory=True)

# 2. النموذج مع رأس جديد
modele = models.resnet18(weights=poids)
for p in modele.parameters():
p.requires_grad = False
modele.fc = nn.Linear(modele.fc.in_features, 10)
modele = modele.to(appareil)

# 3. المرحلة الأولى: الرأس فقط
opt = torch.optim.AdamW(modele.fc.parameters(), lr=1e-3)
critere = nn.CrossEntropyLoss()

for epoque in range(1, 6):
modele.train()
for lot_x, lot_y in charg_tr:
lot_x, lot_y = lot_x.to(appareil), lot_y.to(appareil)
opt.zero_grad()
perte = critere(modele(lot_x), lot_y)
perte.backward()
opt.step()

# 4. المرحلة الثانية: إذابة layer4 مع معدّل صغير
for p in modele.layer4.parameters():
p.requires_grad = True

opt = torch.optim.AdamW([
{"params": modele.layer4.parameters(), "lr": 1e-5},
{"params": modele.fc.parameters(), "lr": 1e-4},
])

for epoque in range(1, 6):
modele.train()
for lot_x, lot_y in charg_tr:
lot_x, lot_y = lot_x.to(appareil), lot_y.to(appareil)
opt.zero_grad()
perte = critere(modele(lot_x), lot_y)
perte.backward()
opt.step()

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

اختيار المعمارية: ليس ResNet18 دومًا

torchvision.models يقدّم عائلة واسعة:

العائلةجيّد لـحجم
resnet18/34/50/101خطّ أساس متين45 - 170 ميغابايت
efficientnet_b0/.../b7كفاءة أفضل لكلّ معلمة20 - 260 ميغابايت
mobilenet_v3الأجهزة الطرفية8 - 22 ميغابايت
vit_b_16 (Vision Transformer)الأداء الأعلى مع بيانات كثيرة340 ميغابايت
convnext_tiny/small/baseحداثة معمارية توافقيّة110 - 340 ميغابايت

قاعدة بسيطة: ResNet50 خطّ أساس ممتاز في معظم الحالات؛ ResNet18 كافٍ للتجارب السريعة؛ EfficientNet مناسب حين تكون الحوسبة محدودة والدقّة مطلوبة؛ ViT يستحقّ الاعتبار على البيانات الكثيرة والحوسبة الوفيرة.

تدفّق شكل غير متوقّع: ImageNet تنتظر 3 قنوات

Fashion-MNIST رمادية بقناة واحدة، وشبكات ImageNet تنتظر ثلاث قنوات. يوجد حلاّن. الأول رأيناه أعلاه: Grayscale(num_output_channels=3) يُكرّر القناة الوحيدة ثلاث مرّات. الثاني تعديل الطبقة الأولى للشبكة:

# تعديل conv1 من ResNet لقبول قناة واحدة
poids_conv1 = modele.conv1.weight.data.mean(dim=1, keepdim=True)
modele.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)
modele.conv1.weight.data = poids_conv1

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

تجميد BatchNorm ليس تلقائيًّا

عند requires_grad = False على معلمات BatchNorm، الأوزان القابلة للتعلّم تتجمّد، لكن الإحصائيات الجارية (المتوسّط والانحراف) تستمرّ في التحديث ما دام النموذج في وضع train(). هذا يُدهور الأداء حين تكون بياناتك مختلفة عن ImageNet — وهو حال Fashion-MNIST تمامًا. الحلّ: بعد التجميد، ضع النموذج في eval() أثناء المرحلة الأولى، أو عدّل BatchNorm صراحةً بإجباره على وضع الاستدلال. نفس الفخّ شرحته دورة 08 عن Keras.

اعرض الطبقات قبل التعديل

print(modele) يُظهر الشجرة الكاملة لأيّ نموذج جاهز: أسماء الطبقات، الأبعاد، الترتيب. قبل استبدال أيّ شيء، اقرأ الشجرة. اسم fc قد يكون classifier أو head حسب المعمارية، وتخمين الاسم يُنتج AttributeError محبطًا. القراءة قبل الكتابة تُوفّر دقائق كثيرة.

في الخلاصة

  • الطبقات الأولى من شبكة ImageNet عامّة وقابلة لإعادة الاستعمال؛ الرأس وحده يجب استبداله.
  • مرحلتان: الرأس أوّلاً بقاعدة مجمَّدة ومعدّل عادي، ثم إذابة آخر بلوك بمعدّل أصغر مئة مرّة.
  • التطبيع المتوقّع جزء من العقد؛ استعمل weights.transforms() لتفادي كلّ خطر خطأ.
  • عند تجميد الشبكة، تذكّر BatchNorm: إحصائياته الجارية تستمرّ في التحديث في وضع train؛ ضع النموذج في eval() أو عالِج الأمر صراحةً.

الوحدة التالية: تصدير النموذج بـTorchScript وONNX ونشره في خدمة تُقدّم توقّعاته عبر الشبكة.