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

الوحدة 6 — المُحسِّنات وجدولة معدّل التعلّم

معدّل التعلّم يهيمن على كلّ ما سواه، كما ذكرت الدورة السابقة. في هذه الوحدة نتعرّف إلى المُحسِّنات المتاحة في PyTorch، نُبرِّر خيار كلّ واحد، ثم نتناول الجدولة: كيف نغيّر معدّل التعلّم أثناء التدريب كي نستفيد من الحقب الأولى ومن الحقب الأخيرة معًا.

المُحسِّن في PyTorch: واجهة موحّدة

كلّ المُحسِّنات في torch.optim تتلقّى قائمة الأوزان وlr كحدّ أدنى:

import torch.optim as optim

optimiseur = optim.SGD(modele.parameters(), lr=0.1, momentum=0.9)

وكلّها تُستعمل بنفس الأسلوب في الحلقة: optimiseur.zero_grad() ثم perte.backward() ثم optimiseur.step(). المُختلِف بينها هو قاعدة التحديث، أي كيف يُترجَم التدرّج إلى تعديل على الأوزان.

SGD مع الاندفاع: البسيط الذي يعمل غالبًا

الانحدار العشوائي بالحزم (SGD) هو الجدّ الأكبر. بحدّ ذاته يعاني من تذبذب في الوديان الضيّقة، ولذلك يُضاف إليه الاندفاع الذي يمرّر جزءًا من التدرّج السابق:

opt = optim.SGD(
modele.parameters(),
lr=0.1,
momentum=0.9,
weight_decay=5e-4,
)

قِيَم افتراضية معقولة على مسائل الرؤية: lr=0.1 مع اندفاع 0.9. weight_decay هو تنظيم L2 مُدمَج، يمنع نموّ الأوزان دون حدّ. SGD هو الاختيار السائد في التدريبات الطويلة على شبكات ImageNet لأنّه يُنتج مُدرَّبات أكثر قابلية للتعميم — رأي مدعوم بعدد كبير من التجارب المنشورة.

Adam وAdamW: تكيّف لكلّ وزن

Adam يحمل ذاكرة لكلّ وزن (اللحظة الأولى والثانية للتدرّج) ويستنبط منها معدّل تعلّم فعليًا مختلفًا لكلّ معلمة. هذا يُلغي الحاجة إلى البحث الدقيق عن lr، ويجعل التقارب سريعًا في الحقب الأولى.

عيبه هو أنّ صيغته الأصلية تخلط weight_decay مع اللحظات، فتُدهور التنظيم. AdamW يفصل الاثنين وهو الاختيار الافتراضي اليوم في معظم مسائل التعلّم العميق:

opt = optim.AdamW(
modele.parameters(),
lr=3e-4,
betas=(0.9, 0.999),
weight_decay=1e-2,
)

القاعدة العملية: AdamW بمعدّل تعلّم بين 10410^{-4} و3×1043\times 10^{-4} تصلح كنقطة انطلاق لأغلب المسائل، بما فيها Fashion-MNIST.

SGD أم AdamW؟ حكم مختصر

السياقاختيار مستحسن
تدريب طويل جدًّا على شبكة قياسية (ResNet)SGD + Momentum + جدولة
تدريب متوسّط، أو أوّل نموذج على مسألة جديدةAdamW
محوّلات (BERT، GPT)AdamW حصريًّا تقريبًا
موارد محدودة، لا وقت لضبط lrAdamW

الفارق ليس كارثيًّا في الاتّجاهين، والمشروع النموذجي يبدأ عادةً بـAdamW لتقييم سريع، ثم يُقارن بـSGD مع جدولة كوسيلة تحسين محتملة.

جدولة معدّل التعلّم: لماذا نحتاجها

معدّل تعلّم ثابت لا يوفّي بالحاجات المتغيّرة: في البداية نريد تقدّمًا كبيرًا، في النهاية نحتاج ضبطًا دقيقًا. الجدولة تُغيّر lr عبر الحقب. أربع سياسات شائعة في PyTorch، لكلّ منها استعمال مميّز.

StepLR: التبسيط الأقصى

يخفض lr بمُعامل ثابت كلّ عدد ثابت من الحقب:

plan = optim.lr_scheduler.StepLR(opt, step_size=10, gamma=0.5)
for epoque in range(30):
entrainer_une_epoque()
plan.step() # مرّة واحدة في الحقبة

بعد كلّ 10 حقب، lr يُصبح نصف ما كان. مناسب لتدريبات قصيرة يُعرَف جدولها الزمنيّ سلفًا. عيبه القفزات الحادّة، التي تربك النموذج أحيانًا.

CosineAnnealingLR: تناقص جيبيّ ناعم

يتبع lr نصف موجة جيب تمام من قيمة البداية إلى قيمة نهائية:

lr(t)=lrmin+12(lrmaxlrmin)(1+cos ⁣(tTπ))lr(t) = lr_{\min} + \tfrac{1}{2}(lr_{\max} - lr_{\min})\left(1 + \cos\!\left(\tfrac{t}{T}\pi\right)\right)
plan = optim.lr_scheduler.CosineAnnealingLR(opt, T_max=30, eta_min=1e-6)

هذا الجدول اليوم من الأكثر استعمالاً، ويُنتج نتائج ممتازة على مسائل الرؤية. البدء بمعدّل عالٍ ثم التناقص التدريجي يُشبِه «تسخين ثم شحذ»، وهي حركة تُشبِه ما يقوم به عادةً مُدرِّب بشريّ.

OneCycleLR: من صفر إلى القمّة ثم إلى صفر

هذه سياسة أحدث، اقترحها ليزلي سميث. يبدأ lr منخفضًا، يرتفع إلى قيمة قصوى قبل منتصف التدريب، ثم يهبط إلى قيمة صغيرة جدًّا:

plan = optim.lr_scheduler.OneCycleLR(
opt,
max_lr=1e-3,
steps_per_epoch=len(charg_tr),
epochs=20,
)
for lot_x, lot_y in charg_tr:
...
optimiseur.step()
plan.step() # لكلّ حزمة، لا لكلّ حقبة!

ملاحظتان مهمّتان. الأولى: OneCycleLR يتقدّم لكلّ خطوة، لا لكلّ حقبة، خلافًا لسابقيه. الثانية: يستدعي معرفة epochs مسبقًا، وليس مناسبًا لتدريبات بطول متغيّر.

ReduceLROnPlateau: يقيس ثم يقرّر

هذا الجدول لا يتّبع صيغة مسبقة، بل يُراقب مقياسًا ما (عادةً خسارة التحقّق) ويُخفض lr عندما يتوقّف عن التحسّن:

plan = optim.lr_scheduler.ReduceLROnPlateau(
opt, mode="min", factor=0.5, patience=3, min_lr=1e-6
)
for epoque in range(50):
entrainer_une_epoque()
perte_val = evaluer()
plan.step(perte_val) # ملاحظ المقياس هنا

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

البحث عن معدّل التعلّم: خدعة سميث

قبل تدريب طويل، اختبار سريع يوفّر ساعات: نبدأ lr صغيرًا جدًّا، نُضاعِفه في كلّ حزمة، ونرسم الخسارة مقابل lr. المنطقة التي تنخفض فيها الخسارة بأقصى سرعة هي مجال الاختيار المناسب:

# تخطيطي: للمنتَجات الجاهزة استعمل مكتبة torch_lr_finder
lrs, pertes = [], []
lr = 1e-7
for lot_x, lot_y in charg_tr:
for g in opt.param_groups:
g["lr"] = lr
...
lrs.append(lr); pertes.append(perte.item())
lr *= 1.1
if lr > 1.0: break

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

مثال متكامل مع AdamW وCosine

نُلحق الجدولة بحلقة الوحدة الخامسة:

opt = optim.AdamW(modele.parameters(), lr=3e-4, weight_decay=1e-2)
plan = optim.lr_scheduler.CosineAnnealingLR(opt, T_max=20, eta_min=1e-6)

for epoque in range(1, 21):
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()
plan.step() # مرّة واحدة في الحقبة
perte_val, exact_val = evaluer(modele, charg_val, critere, appareil)
lr_actuel = opt.param_groups[0]["lr"]
print(f"حقبة {epoque:2d} | lr {lr_actuel:.6f} | تحقّق {perte_val:.4f} | دقّة {exact_val:.3f}")

طباعة lr في كلّ حقبة عادة نافعة: تتأكّد من أنّ الجدولة تعمل كما تتوقّع، وتُلاحظ التسخين والتناقص فورًا.

قصّ التدرّج: صمام أمان في المسائل الحسّاسة

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

perte.backward()
torch.nn.utils.clip_grad_norm_(modele.parameters(), max_norm=1.0)
opt.step()

هذه الحيلة قليلة الأثر على Fashion-MNIST، لكنّها تصير ضرورية على المهامّ التسلسلية. أضِفها كصمام أمان في المشاريع الجديدة، وأزلها إن تأكّدت من عدم الحاجة.

جدولة تُنادى في المكان الخطأ

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

البذرة والقياس قبل الحكم

لا تُصدر حكمًا على مُحسِّن أو جدولة بناءً على تدريب واحد. اختلاف بذور torch.manual_seed قد يُنتج فروقًا تعادل الفروق بين الخوارزميّتين نفسها. لتقارن SGD بـAdamW بنيّةً علميّة، شغِّل ثلاث تجارب بكلّ بذرة، واحسب المتوسّط والانحراف المعياري لخسارة التحقّق النهائية. سترى أحيانًا أنّ الفرق ليس دلاليًّا.

في الخلاصة

  • AdamW خيار افتراضي ممتاز في lr ∈ [10⁻⁴, 3×10⁻⁴]؛ SGD مع الاندفاع خيار مُنافِس في التدريبات الطويلة.
  • weight_decay هو تنظيم L2 مدمَج في المُحسِّن؛ AdamW يُطبّقه بشكل صحيح، بينما Adam الأصلي يخلطه مع اللحظات.
  • الجدولة تُعدّل lr عبر التدريب: CosineAnnealingLR افتراض جيّد؛ OneCycleLR سريع لكنّه يتقدّم لكلّ حزمة؛ ReduceLROnPlateau ملائم للتدريبات المتغيّرة الطول.
  • قصّ التدرّج صمّام أمان في الشبكات الحسّاسة؛ لا يُغيّر شيئًا في التدريبات السليمة لكنّه يمنع الانفجار في السيئة.

الوحدة التالية: نتحوّل إلى GPU ونُدخل الدقّة المختلطة لتسريع التدريب دون فقدان الجودة.