الدرس 4 من 8

أول موديل بـ PyTorch

نرجع لعملاء شركة الاتصالات، بس بشبكة عصبية.

تعريف الموديل

كل موديل في PyTorch عبارة عن class بيورث من nn.Module (فاكر الوراثة في المرحلة ٢؟). في __init__ بتعرّف الطبقات، وفي forward بتقول الداتا تمشي فيهم إزاي.

model_def.py
import torch
from torch import nn

class ChurnNet(nn.Module):
    def __init__(self, n_features):
        super().__init__()
        self.layers = nn.Sequential(
            nn.Linear(n_features, 32),   # 5 inputs -> 32 neurons
            nn.ReLU(),
            nn.Linear(32, 16),
            nn.ReLU(),
            nn.Linear(16, 1),            # one output: churn score (logit)
        )

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

model = ChurnNet(n_features=5)
print(model)
print("parameters:", sum(p.numel() for p in model.parameters()))

الـ Training Loop

ده الكود اللي هتكتبه في كل مشروع تقريباً، بنفس الخطوات الأربعة اللي عملناها بـ NumPy:

train_churn.py
import pandas as pd
import torch
from torch import nn
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import roc_auc_score

torch.manual_seed(42)

# ---- data (same churn.csv from phase 5) ----
df = pd.read_csv("churn.csv")
df["yearly"] = (df["contract"] == "yearly").astype(int)
features = ["tenure", "bill", "complaints", "data_gb", "yearly"]
X_train, X_test, y_train, y_test = train_test_split(
    df[features], df["churned"], test_size=0.2,
    random_state=42, stratify=df["churned"])

scaler = StandardScaler().fit(X_train)          # neural nets NEED scaling
X_train = torch.tensor(scaler.transform(X_train), dtype=torch.float32)
X_test = torch.tensor(scaler.transform(X_test), dtype=torch.float32)
y_train = torch.tensor(y_train.values, dtype=torch.float32).unsqueeze(1)

# ---- model, loss, optimizer ----
model = nn.Sequential(nn.Linear(5, 32), nn.ReLU(),
                      nn.Linear(32, 16), nn.ReLU(), nn.Linear(16, 1))
loss_fn = nn.BCEWithLogitsLoss()                # binary classification loss
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

# ---- training loop ----
losses = []
for epoch in range(150):
    logits = model(X_train)                     # 1. forward
    loss = loss_fn(logits, y_train)             # 2. loss
    optimizer.zero_grad()                       #    clear old gradients
    loss.backward()                             # 3. backward
    optimizer.step()                            # 4. update weights
    losses.append(loss.item())

# ---- evaluate ----
with torch.no_grad():                           # no gradients needed here
    probs = torch.sigmoid(model(X_test)).squeeze().numpy()
print(f"final train loss: {losses[-1]:.3f}")
print(f"test AUC: {roc_auc_score(y_test, probs):.3f}")

plt.figure(figsize=(7, 3.2))
plt.plot(losses, color="#3776AB", linewidth=2)
plt.xlabel("epoch"); plt.ylabel("loss"); plt.title("Training loss")
plt.grid(alpha=0.3)
plt.show()
BCEWithLogitsLoss loss للتصنيف الثنائي، وبتعمل الـ sigmoid جواها.
Adam optimizer ذكي بيظبط حجم الخطوة لكل وزن لوحده. الاختيار الافتراضي الكويس.
zero_grad() بتمسح الـ gradients القديمة، لأن PyTorch بيجمعها فوق بعض.
no_grad() بتقول لـ PyTorch ميسجلش عمليات، فالتوقع يبقى أسرع وأخف.

الشبكة اشتغلت، والـ loss نزل بشكل سليم. بس لو قارنت بالمرحلة ٥، الـ Logistic Regression جاب AUC أعلى على نفس الداتا! ده مش فشل، ده بالظبط اللي قلناه: داتا جدولية صغيرة وعلاقاتها بسيطة مش محتاجة شبكة عصبية. فايدة الدرس ده إنك تتقن الـ training loop، لأنه هو هو اللي هتستخدمه مع الصور والنصوص حيث الشبكات مالهاش منافس.

⚠ أشهر ٣ غلطاتنسيان optimizer.zero_grad() (الـ gradients بتتراكم والتدريب يبوظ). ونسيان الـ scaling (الشبكات حساسة جداً للمقاييس). وإنك تحسب الـ scaler على الداتا كلها بدل الـ train بس (leakage زي ما اتعلمنا).
الدرس اللي فات