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

الوحدة 10 — أوّل شبكة مُدرَّبة من طرف إلى طرف

تسع وحدات من الآليات. وتجمعها هذه الوحدة في مشروع كامل قابل للتنفيذ: مُصنِّف على بيانات جدولية، من المعالجة القبلية إلى الحفظ. وكلّ قرار في الكود يُحيل إلى الوحدة التي تُبرّره.

معمارية المشروع

ترتيب الخطوات غير قابل للتفاوض، وهو يستأنف مباشرة مكتسبات الدورة السابقة: التقسيم قبل المعالجة القبلية، وضبط المعالجة القبلية على التدريب وحده، ثم بناء الشبكة.

import torch
import torch.nn as nn
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# 1. نقسم أولا، ثم نعالج: قاعدة الدورة السابقة.
X_tr, X_val, y_tr, y_val = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42
)

# 2. المسوي يضبط على التدريب وحده، وإلا حدث تسرب.
echelle = StandardScaler().fit(X_tr)
X_tr = echelle.transform(X_tr)
X_val = echelle.transform(X_val)

# 3. موترات ثم محملات. نخلط التدريب، ولا نخلط التحقق أبدا.
def en_tenseurs(X, y):
return torch.utils.data.TensorDataset(
torch.tensor(X, dtype=torch.float32),
torch.tensor(y, dtype=torch.long),
)

charg_tr = torch.utils.data.DataLoader(en_tenseurs(X_tr, y_tr), batch_size=64, shuffle=True)
charg_val = torch.utils.data.DataLoader(en_tenseurs(X_val, y_val), batch_size=256)

الشبكة

class Classifieur(nn.Module):
def __init__(self, n_entrees, n_classes, largeur=128, abandon=0.3):
super().__init__()
self.reseau = nn.Sequential(
# لا انحياز: وسيط بيتا في التطبيع يحل محله.
nn.Linear(n_entrees, largeur, bias=False),
nn.BatchNorm1d(largeur),
nn.ReLU(),
nn.Dropout(abandon),

nn.Linear(largeur, largeur // 2, bias=False),
nn.BatchNorm1d(largeur // 2),
nn.ReLU(),
nn.Dropout(abandon),

# مخرج بنتائج خام: الخسارة تطبق سوفت ماكس بنفسها.
nn.Linear(largeur // 2, n_classes),
)

def forward(self, x):
return self.reseau(x)


appareil = "cuda" if torch.cuda.is_available() else "cpu"
modele = Classifieur(X_tr.shape[1], len(np.unique(y))).to(appareil)

وثلاثة قرارات في هذه الكتلة تستحقّ التوضيح. فترتيب خطّي، تطبيع، تنشيط، إسقاط يتبع عُرف الوحدة 8. ويُفسَّر bias=False بالوسيط β\beta في التطبيع، الذي يؤدّي هذا الدور سلفًا. والطبقة الأخيرة لا تحمل أي تنشيط: فـCrossEntropyLoss تنتظر النتائج الخام وتُطبّق سوفت‌ماكس تطبيقًا مستقرًّا عدديًا، كما شرحت الوحدة 3.

حلقة التدريب

critere = nn.CrossEntropyLoss()
optimiseur = torch.optim.AdamW(modele.parameters(), lr=3e-4, weight_decay=0.01)
planificateur = torch.optim.lr_scheduler.CosineAnnealingLR(optimiseur, T_max=100)

meilleure_perte, patience, attente = float("inf"), 10, 0
historique = {"train": [], "val": []}

for epoque in range(100):
# --- التدريب: الإسقاط نشط، إحصائيات الحزمة. ---
modele.train()
perte_tr = 0.0
for X_lot, y_lot in charg_tr:
X_lot, y_lot = X_lot.to(appareil), y_lot.to(appareil)
optimiseur.zero_grad() # وإلا تراكمت التدرجات
perte = critere(modele(X_lot), y_lot)
perte.backward()
nn.utils.clip_grad_norm_(modele.parameters(), 1.0) # الوحدة 6
optimiseur.step()
perte_tr += perte.item() * X_lot.size(0)

# --- التحقق: الإسقاط معطل، المتوسطات الجارية. ---
modele.eval()
perte_val, justes = 0.0, 0
with torch.no_grad():
for X_lot, y_lot in charg_val:
X_lot, y_lot = X_lot.to(appareil), y_lot.to(appareil)
sorties = modele(X_lot)
perte_val += critere(sorties, y_lot).item() * X_lot.size(0)
justes += (sorties.argmax(1) == y_lot).sum().item()

perte_tr /= len(charg_tr.dataset)
perte_val /= len(charg_val.dataset)
historique["train"].append(perte_tr)
historique["val"].append(perte_val)
planificateur.step()

# --- التوقف المبكر: نحفظ أوزان أفضل مرور. ---
if perte_val < meilleure_perte:
meilleure_perte, attente = perte_val, 0
torch.save(modele.state_dict(), "meilleur_modele.pt")
else:
attente += 1
if attente >= patience:
print(f"توقف مبكر عند الحقبة {epoque}")
break

if epoque % 10 == 0:
exactitude = justes / len(charg_val.dataset)
print(f"epoque {epoque:3d} | train {perte_tr:.4f} | val {perte_val:.4f} | exactitude {exactitude:.3f}")

أربع نقاط انتباه مُركَّزة في هذه الحلقة، وكلّها من الوحدات السابقة. فالتحويل بين train() وeval() يشترط سلوك الإسقاط والتطبيع (الوحدتان 7 و8). وzero_grad() يمنع تراكم التدرّجات من تكرار إلى آخر. وtorch.no_grad() في التحقّق يوفّر الذاكرة والحساب بعدم تسجيل المخطّط. والمُخطِّط يتقدّم مرّة واحدة في كلّ حقبة، لا لكلّ حزمة.

نتحقّق، ثم نحفظ

قبل إطلاق هذه المئة حقبة، طبّق اختبار الوحدة 9: الإفراط في تعلّم عشر مشاهدات. فإن فشل، صحّح قبل المضيّ.

ومن أجل النشر، نُعيد تحميل أفضل الأوزان ونحفظ كلّ ما يلزم للاستنساخ:

modele.load_state_dict(torch.load("meilleur_modele.pt"))
modele.eval()

torch.save({
"poids": modele.state_dict(),
"architecture": {"n_entrees": X_tr.shape[1], "largeur": 128, "abandon": 0.3},
"echelle": {"moyenne": echelle.mean_, "ecart_type": echelle.scale_},
"version_torch": torch.__version__,
}, "modele_complet.pt")

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

مسار تقدّم واقعي

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

الخلاصة

  • التقسيم قبل المعالجة القبلية، وضبط المُسوّي على التدريب وحده: قاعدة الدورة السابقة تنطبق كما هي.
  • ترتيب خطّي، تطبيع، تنشيط، إسقاط، مع bias=False قبل تطبيع وبلا أي تنشيط في المُخرَج.
  • تستلزم الحلقة train() وeval() في موضعهما، وzero_grad()، وno_grad() في التحقّق، ومُخطِّطًا يتقدّم لكلّ حقبة.
  • احفظ الأوزان والمُسوّي معًا؛ فبغير إحصائيات السلّم يكون النموذج غير قابل للاستخدام في الإنتاج.

الخطوة الأخيرة: الخلاصة وامتحان الأربعين سؤالًا.