التحسين في التعلم العميق
“من SGD إلى Adam — الحيل التي تجعل الشبكات العميقة تتدرب فعلاً”
SGD مقابل Adam مقابل AdamW وإحماء معدل التعلم والجدولة التقوسية وتطبيع الدُّفعة والـDropout وقطع التدرج والتدريب بالدقة المختلطة.
المتطلبات الأساسية
المفاهيم المغطاة
∑الصيغ الرئيسية
SGD + الزخم
تراكم السرعة في اتجاه التدرج — يخرج من الحدود الدنيا الضحلة ويُخمِّد التذبذبات
آدم
معدل تعلم تكيفي لكل معامل: m̂_t = متوسط التدرج المصحَّح، v̂_t = تباين التدرج
تطبيع الدفعة
يُطبِّع التنشيطات لكل ميني-دفعة؛ γ,β قابلة للتعلم تستعيد القدرة التمثيلية
جدولة lr بالجيب التمام
تناقص سلس لمعدل التعلم من ηmax إلى ηmin على T خطوة — أفضل من الانخفاض بالخطوات
▶محاكاة تفاعلية
نموذج جيد مع تحسين سيئ لا قيمة له
نفس الشبكة مع SGD البسيط والتهيئة الجيدة والجدولة المناسبة يمكنها التفوق على شبكة أكبر مدربة باهمال. خدع التحسين هي الفرق بين 'يعمل في الورقة' و'يعمل على GPU في الإنتاج'. التاريخ: فشلت شبكات الأعماق المبكرة بسبب التدرجات المتلاشية والتهيئة السيئة. اختراق ImageNet 2012 (AlexNet) استخدم ReLU + dropout + weight decay. أضافت ResNets (2015) اتصالات قافزة لحل تدفق التدرج عند عمق 100+. تتدرب المحولات الحديثة بثبات عند عمق 1000+ مع تطبيع دقيق وإحماء معدل التعلم وقطع التدرج. كل خدعة حلّت نمط فشل محدد.
معدل التعلم هو المعامل الأهم. معدل تعلم خاطئ بمقدار 10× غالباً يصنع الفرق بين نموذج يتدرب وآخر يتباعد — قبل تجربة أي شيء آخر.
لماذا توجد كل حيلة
**الزخم (μ=0.9):** ينزلق الانحدار التدرجي على وعاء خسارة مستطيل بتعرج عبر البعد الضيق. يخمّد الزخم هذه التذبذبات بمتوسطة التدرجات عبر الزمن — محولاً تعرجاً بطيئاً إلى منحنى سلس نحو الحد الأدنى. **آدم:** لدى معاملات مختلفة قيم تدرج مختلفة جداً. يُطبِّع آدم كل معامل بحجم تدرجه التاريخي — الميزات النادرة تحصل على معدلات تعلم فعلية أكبر. **Batch Normalization:** يُرغم التحول في التوزيع الداخلي الطبقات اللاحقة على التعديل المستمر. يُعيد BatchNorm توسيط التنشيطات في كل طبقة مستقرةً التدريب ومتيحةً معدلات تعلم أعلى 10×. **Dropout (p=0.5):** يُصفِّر نصف التنشيطات عشوائياً أثناء التدريب — يُجبر الشبكة على تعلم تمثيلات زائدة مانعاً التكيف المشترك للخلايا.
حجم الدفعة ومعدل التعلم مرتبطان: مضاعفة حجم الدفعة له أثر مشابه لتقسيم معدل التعلم على اثنين. قاعدة التوسع الخطي (Goyal et al. 2017): ضبط lr بالتناسب مع حجم الدفعة، إضافة 5 إبوكات إحماء.
وصفة التدريب الحديثة للتعلم العميق
تهيئة الأوزان: He init لطبقات ReLU (σ=√(2/fan_in))، Xavier لـtanh/sigmoid (σ=√(2/(fan_in+fan_out))).
اختيار المحسِّن: Adam (β₁=0.9، β₂=0.999، ε=1e-8، lr=3e-4) لمعظم المهام. SGD+زخم للضبط الدقيق على ImageNet.
إضافة إحماء معدل التعلم: رفع خطي من 0 إلى lr المستهدف على 5% من الخطوات الكلية — يمنع خطوات التدرج الكبيرة قبل استقرار النموذج.
التبريد بالجيب التمام (أو ReduceLROnPlateau): تقليل lr بسلاسة إلى 1e-6. OneCycleLR بديل قوي.
قطع التدرج (max_norm=1.0): تحديد معيار التدرج قبل التحديث — ضروري لـRNN والمحولات. torch.nn.utils.clip_grad_norm_().
التنظيم: تضاؤل الأوزان (L2، λ=1e-4 إلى 1e-2). Dropout (p=0.1–0.5). تمهيد التسميات (ε=0.1) للتصنيف.
الدقة المختلطة (torch.cuda.amp): float16 للمرور الأمامي، float32 للخسارة — سرعة مضاعفة وكفاءة ذاكرة مضاعفة على GPU الحديثة.
حلقة التدريب الحديثة في PyTorch
import torch import torch.nn as nn import torch.optim as optim from torch.cuda.amp import GradScaler, autocast from torch.utils.data import TensorDataset, DataLoader class="tok-comment"># ── Minimal model + dataloader for the demo ──────────────────────────── class MyModel(nn.Module): def __init__(self): super().__init__() self.net = nn.Sequential(nn.Linear(class="tok-num">16, class="tok-num">64), nn.ReLU(), nn.Linear(class="tok-num">64, class="tok-num">10)) def forward(self, x): return self.net(x) X_data = torch.randn(class="tok-num">512, class="tok-num">16) y_data = torch.randint(class="tok-num">0, class="tok-num">10, (class="tok-num">512,)) train_loader = DataLoader(TensorDataset(X_data, y_data), batch_size=class="tok-num">32, shuffle=True) def train_one_epoch(model, loader, optimizer, scaler, scheduler, device, clip_norm=class="tok-num">1.0): model.train() total_loss = class="tok-num">0.0 for batch_idx, (X, y) in enumerate(loader): X, y = X.to(device), y.to(device) optimizer.zero_grad(set_to_none=True) class="tok-comment"># faster than zero_grad() class="tok-comment"># ── Mixed precision forward pass ────────────────────────────────────── with autocast(): logits = model(X) loss = nn.functional.cross_entropy(logits, y, label_smoothing=class="tok-num">0.1) class="tok-comment"># ── Scaled backward + gradient clipping ────────────────────────────── scaler.scale(loss).backward() scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), clip_norm) scaler.step(optimizer) scaler.update() scheduler.step() class="tok-comment"># step per batch for OneCycleLR total_loss += loss.item() return total_loss / len(loader) class="tok-comment"># ── Setup ───────────────────────────────────────────────────────────────────── device = torch.device(class="tok-str">"cuda" if torch.cuda.is_available() else class="tok-str">"cpu") model = MyModel().to(device) class="tok-comment"># AdamW (Adam with decoupled weight decay — better than Adam+L2) optimizer = optim.AdamW(model.parameters(), lr=class="tok-num">3e-4, weight_decay=class="tok-num">1e-2) class="tok-comment"># OneCycle LR: warmup + cosine anneal in one schedule scheduler = optim.lr_scheduler.OneCycleLR( optimizer, max_lr=class="tok-num">3e-4, total_steps=class="tok-num">100 * len(train_loader), class="tok-comment"># epochs × steps_per_epoch pct_start=class="tok-num">0.05, class="tok-comment"># class="tok-num">5% warmup anneal_strategy=class="tok-str">"cos", ) scaler = GradScaler() class="tok-comment"># for mixed precision class="tok-comment"># ── Optimizer comparison ────────────────────────────────────────────────────── import torch.optim as optim class="tok-comment"># SGD + Momentum (strong for fine-tuning pretrained CNNs) sgd = optim.SGD(model.parameters(), lr=class="tok-num">0.01, momentum=class="tok-num">0.9, weight_decay=class="tok-num">1e-4, nesterov=True) class="tok-comment"># Adam (default for most deep learning tasks) adam = optim.Adam(model.parameters(), lr=class="tok-num">3e-4, betas=(class="tok-num">0.9, class="tok-num">0.999)) class="tok-comment"># AdamW (Adam with properly decoupled weight decay — recommended by modern best practices) adamw = optim.AdamW(model.parameters(), lr=class="tok-num">3e-4, weight_decay=class="tok-num">0.01) class="tok-comment"># ── Batch Normalization example ─────────────────────────────────────────────── class ConvBlock(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.block = nn.Sequential( nn.Conv2d(in_ch, out_ch, class="tok-num">3, padding=class="tok-num">1, bias=False), nn.BatchNorm2d(out_ch), class="tok-comment"># bias=False because BN has its own β nn.ReLU(inplace=True), ) def forward(self, x): return self.block(x) class="tok-comment"># ── Layer Normalization (Transformers prefer LayerNorm) ─────────────────────── class TransformerBlock(nn.Module): def __init__(self, d_model, n_heads): super().__init__() self.attn = nn.MultiheadAttention(d_model, n_heads, batch_first=True) self.ff = nn.Sequential(nn.Linear(d_model, d_model*class="tok-num">4), nn.GELU(), nn.Linear(d_model*class="tok-num">4, d_model)) self.norm1 = nn.LayerNorm(d_model) class="tok-comment"># pre-norm (more stable than post-norm) self.norm2 = nn.LayerNorm(d_model) self.drop = nn.Dropout(class="tok-num">0.1) def forward(self, x): class="tok-comment"># Pre-norm architecture (used in GPT-class="tok-num">2+, better gradient flow) x = x + self.drop(self.attn(self.norm1(x), self.norm1(x), self.norm1(x))[class="tok-num">0]) x = x + self.drop(self.ff(self.norm2(x))) return x class="tok-comment"># ── Learning rate finder ────────────────────────────────────────────────────── class="tok-comment"># pip install torch-lr-finder from torch_lr_finder import LRFinder finder = LRFinder(model, optimizer, nn.CrossEntropyLoss(), device=device) finder.range_test(train_loader, end_lr=class="tok-num">10, num_iter=class="tok-num">100) finder.plot() class="tok-comment"># look for the steepest descent — that's your max_lr
أوضاع فشل BatchNorm الخفية
BatchNorm قوي لكنه يمتلك عدة أوضاع فشل خفية: (1) **أحجام دفعات صغيرة:** يحسب BatchNorm إحصائيات على الدفعة — مع batch_size < 8 تكون التقديرات صاخبة جداً. استخدم GroupNorm (group_size=32) أو LayerNorm بدلاً. (2) **model.eval() حيوي:** في وضع التقييم يستخدم BatchNorm الإحصائيات الجارية المحسوبة أثناء التدريب. نسيان استدعاء model.eval() قبل الاستدلال يسبب تنبؤات مختلفة تماماً. (3) **الضبط الدقيق على توزيعات مختلفة:** قد لا تتطابق المتوسط/التباين الجاريان مع بياناتك. فكر في track_running_stats=False أو معدل تعلم منخفض لطبقات BN. (4) **RNNs:** لا يعمل BatchNorm مع التسلسلات ذات الأطوال المتغيرة — استخدم LayerNorm بدلاً.
الخطأ الثاني الأكثر شيوعاً في PyTorch (بعد أبعاد التنسور الخاطئة) هو نسيان model.eval() — يتصرف BatchNorm وDropout بشكل مختلف في وضعي التدريب والتقييم.
?اختبار المعرفة
يتم حفظ التقدم في متصفحك — لا حاجة لحساب.