الشبكات العصبية من الصفر: دليل المطور

٢٨ مارس ٢٠٢٦

Neural Networks From Scratch: A Developer's Guide

ملخص

  • يرشدك هذا الدليل خلال بناء شبكة عصبية كاملة وتعمل من الصفر في Python باستخدام NumPy فقط.
  • ستقوم بتنفيذ الانتشار العكسي (backpropagation)، وفهم كيفية تدفق التدرجات (gradients)، ورؤية كل سطر في حلقة التدريب.
  • نحن ندرج كودًا يعمل لمحسن Adam، وتنظيم L2، والـ dropout، والتدريب بنظام الدفعات الصغيرة (mini-batch).
  • ستتعلم تقييم النماذج باستخدام الدقة (precision)، والاستدعاء (recall)، ودرجة F1، وROC AUC — وليس فقط الدقة الإجمالية (accuracy).
  • فهم هذه التفاصيل الداخلية يجعلك أكثر فعالية بكثير عندما تنتقل إلى أطر عمل مثل PyTorch أو TensorFlow.

ما ستتعلمه

  1. تنفيذ عصبون واحد وشبكة عصبية متعددة الطبقات من الصفر في Python.
  2. فهم الرياضيات وراء الانتشار العكسي وتنفيذها خطوة بخطوة.
  3. تنفيذ محسن Adam ومقارنته بالانحدار التدرجي (gradient descent) التقليدي.
  4. تطبيق تنظيم L2 والـ dropout لمنع الإفراط في التجهيز (overfitting).
  5. تنفيذ الانحدار التدرجي بالدفعات الصغيرة للتدريب الفعال.
  6. تقييم أداء النموذج باستخدام الدقة، والاستدعاء، ودرجة F1، وROC AUC.
  7. تتبع قيم التدرج عبر الشبكة لتصحيح أخطاء التدرجات المتلاشية أو المتفجرة.

المتطلبات الأساسية

قبل البدء، يجب أن تكون مرتاحًا مع:

  • برمجة Python: القواعد الأساسية، هياكل البيانات (القوائم، القواميس)، الدوال، والأصناف (classes).
  • NumPy: عمليات المصفوفات، والـ broadcasting، وضرب المصفوفات (np.dot).
  • الجبر الخطي: المتجهات، المصفوفات، الضرب القياسي، وعمليات التبديل (transpose).
  • التفاضل والتكامل: المشتقات، قاعدة السلسلة (chain rule)، ومفهوم التدرج (gradient).

تدعم الشبكات العصبية التعرف على الصور، ومعالجة اللغات الطبيعية، وتوليد الكود، والمزيد. تجعل مكتبات مثل PyTorch و TensorFlow بناءها أمرًا مباشرًا، لكنها تخفي أيضًا الآليات الداخلية. عندما لا يتقارب نموذجك، أو عندما تنفجر التدرجات، أو عندما تتصرف طبقة مخصصة بشكل غير متوقع — هنا تبرز أهمية فهم التفاصيل الداخلية.

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

يوفر لك هذا الدليل تنفيذًا يعمل بالكامل يمكنك تشغيله وتعديله والتعلم منه.

بناء الأساس: عصبون واحد

وحدة البناء الأساسية للشبكة العصبية هي العصبون. يستقبل العصبون مدخلات، ويطبق أوزانًا على تلك المدخلات، ويضيف انحيازًا (bias)، ويمرر النتيجة عبر دالة تنشيط.

رياضيًا:

output = activation_function(Σ(weight_i * input_i) + bias)

دعنا ننفذ هذا في Python:

import numpy as np

def sigmoid(x):
    """The sigmoid activation function."""
    return 1 / (1 + np.exp(-np.clip(x, -500, 500)))

def sigmoid_derivative(output):
    """Derivative of sigmoid, given the sigmoid output."""
    return output * (1 - output)

def neuron(inputs, weights, bias):
    """Calculates the output of a single neuron."""
    weighted_sum = np.dot(inputs, weights) + bias
    return sigmoid(weighted_sum)

# Example
inputs = np.array([0.5, 0.2, 0.9])
weights = np.array([0.1, 0.3, 0.2])
bias = 0.1

output = neuron(inputs, weights, bias)
print(f"Neuron output: {output:.4f}")  # 0.5624

لاحظ استخدام np.clip في دالة sigmoid — هذا يمنع الطفح العددي (numerical overflow) مع القيم الكبيرة جدًا أو الصغيرة جدًا، وهي مشكلة حقيقية ستواجهها في الممارسة العملية.

دوال التنشيط: اختيار الدالة الصحيحة

ليست Sigmoid هي الخيار الوحيد. تناسب دوال التنشيط المختلفة مشكلات مختلفة:

def relu(x):
    """ReLU — most common for hidden layers."""
    return np.maximum(0, x)

def relu_derivative(x):
    """Derivative of ReLU."""
    return (x > 0).astype(float)

def softmax(x):
    """Softmax — used for multi-class output layers."""
    exp_x = np.exp(x - np.max(x axis=-1 keepdims=True))
    return exp_x / np.sum(exp_x axis=-1 keepdims=True)

متى تستخدم ماذا:

  • ReLU للطبقات المخفية — سريعة الحساب، وتتجنب مشكلة التدرج المتلاشي التي تعاني منها sigmoid/tanh في الشبكات العميقة.
  • Sigmoid لطبقات الإخراج في التصنيف الثنائي (تخرج احتمالية بين 0 و 1).
  • Softmax لطبقات الإخراج في التصنيف متعدد الفئات (مجموع المخرجات يساوي 1).
  • Tanh تُستخدم أحيانًا في الطبقات المخفية ولكن تم استبدالها إلى حد كبير بمتغيرات ReLU.

مشكلة التدرج المتلاشي هي السبب في تراجع شعبية sigmoid للطبقات المخفية: عندما تكون المدخلات كبيرة جدًا أو صغيرة جدًا، تقترب مشتقة sigmoid من الصفر، مما يقتل تدفق التدرج في الشبكات العميقة.

شبكة عصبية كاملة متعددة الطبقات

الآن دعنا نبني صنفًا كاملاً للشبكة العصبية مع التمرير الأمامي، والانتشار العكسي، والتدريب:

import numpy as np

class NeuralNetwork:
    def __init__(self, layer_sizes, learning_rate=0.01, l2_lambda=0.0, dropout_rate=0.0):
        """
        layer_sizes: list of ints, e.g. [784, 128, 64, 10]
        learning_rate: step size for weight updates
        l2_lambda: L2 regularization strength (0 = no regularization)
        dropout_rate: fraction of neurons to drop during training (0 = no dropout)
        """
        self.layer_sizes = layer_sizes
        self.learning_rate = learning_rate
        self.l2_lambda = l2_lambda
        self.dropout_rate = dropout_rate
        self.num_layers = len(layer_sizes) - 1

        # He initialization — better than random for ReLU networks
        self.weights = []
        self.biases = []
        for i in range(self.num_layers):
            scale = np.sqrt(2.0 / layer_sizes[i])  # He init
            w = np.random.randn(layer_sizes[i] layer_sizes[i + 1]) * scale
            b = np.zeros((1 layer_sizes[i + 1]))
            self.weights.append(w)
            self.biases.append(b)

    def forward(self X training=False):
        """Forward pass through the network."""
        self.activations = [X]
        self.z_values = []
        self.dropout_masks = []

        current = X
        for i in range(self.num_layers):
            z = current @ self.weights[i] + self.biases[i]
            self.z_values.append(z)

            # ReLU for hidden layers, sigmoid for output
            if i < self.num_layers - 1:
                a = relu(z)
                # Apply dropout during training only
                if training and self.dropout_rate > 0:
                    mask = (np.random.rand(*a) > self)(float)
                    a *= mask / (1 - self)  # Inverted dropout scaling
                    self.dropout_masks(mask)
                else:
                    self.dropout_masks(np.ones_like(a))
            else:
                a = sigmoid(z)

            self.activations(a)
            current = a

        return current

    def backward(self X y):
        """Backpropagation — compute gradients for all weights and biases."""
        m = X[0]  # batch size
        output = self.activations[-1]

        # Output layer error (binary cross-entropy derivative with sigmoid)
        delta = output - y

        weight_grads = []
        bias_grads = []

        for i in range(self- 1-1-1):
            # Gradients for this layer
            dw = (self.activations[i]) / m
            db = npsum(delta axis=0 keepdims=True) / m

            # Add L2 regularization gradient
            if self> 0:
                dw += (self/ m) * self[i]

            weight_grads(0 dw)
            bias_grads(0 db)

            # Propagate error to previous layer (skip for input layer)
            if i > 0:
                delta = (delta @ self[i]) * relu_derivative(self[i - 1])
                # Apply dropout mask
                if self> 0:
                    delta *= self[i - 1] / (1 - self)

        return weight_grads bias_grads

    def compute_loss(self y_true y_pred):
        """Binary cross-entropy loss with optional L2 regularization."""
        m = y_true[0]
        # Clip to avoid log(0)
        y_pred = np(y_pred 1e-8 1 - 1e-8)
        bce = -np(y_true * np(y_pred) + (1 - y_true) * np(1 - y_pred))

        # L2 penalty
        if self> 0:
            l2_penalty = sum(npsum(w ** 2) for w in self)
            bce += (self/ (2 * m)) * l2_penalty

        return bce

    def train_step(self X y):
        """One training step: forward, backward, update."""
        output = self(X training=True)
        weight_grads bias_grads = self(X y)

        # Update weights with vanilla gradient descent
        for i in range(self):
            self[i] -= self* weight_grads[i]
            self[i] -= self* bias_grads[i]

        return self(y output)

    def predict(self X threshold=0.5):
        """Forward pass without dropout, returns class predictions."""
        probs = self(X training=False)
        return (probs >= threshold)(int)

تفاصيل التنفيذ الرئيسية:

  • تهيئة He (np.sqrt(2.0 / fan_in)) — استخدام قيم عشوائية عادية يسبب تقاربًا ضعيفًا في الشبكات العميقة. تم تصميم تهيئة He لتنشيطات ReLU وتحافظ على استقرار التباين عبر الطبقات.
  • الـ dropout المقلوب (Inverted dropout) — نقوم بتوسيع التنشيطات بمقدار 1 / (1 - dropout_rate) أثناء التدريب حتى لا نحتاج إلى التوسيع أثناء الاستنتاج (inference). هذا هو نفس النهج الذي يستخدمه PyTorch.
  • قص التدرج (Gradient clipping) عبر np.clip في sigmoid يمنع قيم NaN أثناء التدريب.

الانتشار العكسي: خطوة بخطوة

يحسب الانتشار العكسي تدرج دالة الخسارة بالنسبة لكل وزن وانحياز في الشبكة باستخدام قاعدة السلسلة. دعنا نتتبع كيفية عملها بالضبط.

لشبكة ذات طبقات [input → hidden → output]:

1. التمرير الأمامي — حساب مخرجات كل طبقة:

z_hidden = X @ W1 + b1
a_hidden = relu(z_hidden)
z_output = a_hidden @ W2 + b2
a_output = sigmoid(z_output)

2. تدرج طبقة الإخراج — لخسارة الإنتروبيا المتقاطعة الثنائية (binary cross-entropy) مع sigmoid، تتبسط المشتقة إلى:

delta_output = a_output - y_true

3. تدرجات الأوزان لطبقة الإخراج:

dW2 = (a_hidden.T @ delta_output) / batch_size
db2 = mean(delta_output)

4. نشر الخطأ للخلف:

delta_hidden = (delta_output @ W2.T) * relu_derivative(z_hidden)

5. تدرجات الأوزان للطبقة المخفية:

dW1 = (X.T @ delta_hidden) / batch_size
db1 = mean(delta_hidden)

6. تحديث جميع الأوزان:

W1 -= learning_rate * dW1
W2 -= learning_rate * dW2

قاعدة السلسلة هي ما يجعل هذا يعمل. يعتمد التدرج لـ W1 على كيفية تأثير W1 على z_hidden، وكيفية تأثير z_hidden على a_hidden، وكيفية تأثير a_hidden على z_output، وكيفية تأثير z_output على الخسارة. يتم ضرب كل من هذه المشتقات الجزئية معًا — وهذا هو تطبيق قاعدة السلسلة.

التحسين: محسن Adam

يستخدم الانحدار التدرجي التقليدي معدل تعلم ثابت لجميع المعاملات. هذا غير فعال — فبعض الأوزان تحتاج إلى تحديثات كبيرة بينما يحتاج البعض الآخر إلى تحديثات صغيرة. يحل Adam (تقدير العزم التكيفي) 1 هذه المشكلة من خلال الحفاظ على معدلات تعلم لكل معامل بناءً على تقديرات العزم الأول والثاني للتدرجات.

class AdamOptimizer:
    def __init__(self learning_rate=0.001 beta1=0.9 beta2=0.999 epsilon=1e-8):
        """
        Default hyperparameters from the original Adam paper.
        These defaults work well for most problems — start here.
        """
        self.lr = learning_rate
        self.beta1 = beta1   # Exponential decay rate for first moment (mean)
        self.beta2 = beta2   # Exponential decay rate for second moment (variance)
        self.epsilon = epsilon  # Prevents division by zero
        self.t = 0  # Timestep counter
        self.m_w = None  # First moment estimates for weights
        self.v_w = None  # Second moment estimates for weights
        self.m_b = None
        self.v_b = None

    def initialize(self weights biases):
        """Initialize moment estimates to zeros matching weight shapes."""
        self.m_w = [np(w) for w in weights]
        self.v_w = [np(w) for w in weights]
        self.m_b = [np(b) for b in biases]
        self.v_b = [np(b) for b in biases]

    def update(self weights biases weight_grads bias_grads):
        """Perform one Adam update step."""
        if self.m_w is None:
            self(weights biases)

        self.t += 1

        for i in range(len(weights)):
            # Update first moment (mean of gradients)
            self.m_w[i] = self* self[i] + (1 - self) * weight_grads[i]
            self[i] = self* self[i] + (1 - self) * bias_grads[i]

            # Update second moment (mean of squared gradients)
            self[i] = self* self[i] + (1 - self) * (weight_grads[i] ** 2)
            self[i] = self* self[i] + (1 - self) * (bias_grads[i] ** 2)

            # Bias correction — critical in early steps when moments are near zero
            m_w_hat = self[i] / (1 - self** self)
            v_w_hat = self[i] / (1 - self** self)
            m_b_hat = self[i] / (1 - self** self)
            v_b_hat = self[i] / (1 - self** self)

            # Update parameters
            weights[i] -= self* m_w_hat / (np(v_w_hat) + self)
            biases[i] -= self* m_b_hat / (np(v_b_hat) + self)

لماذا يعمل Adam بشكل جيد: يعاني الانحدار التدرجي التقليدي مع تضاريس الخسارة التي لها انحناءات مختلفة في اتجاهات مختلفة (وهو أمر شائع في الشبكات العصبية). يقوم Adam بتكييف معدل التعلم الفعال لكل معامل على حدة. المعاملات ذات التدرجات الكبيرة باستمرار تحصل على معدلات تعلم فعالة أصغر، والعكس صحيح. شروط تصحيح الانحياز (bias correction) مهمة لأنه بدونها، تكون تقديرات العزم منحازة نحو الصفر في التكرارات القليلة الأولى.

ملاحظة حول "النقاط الصغرى المحلية": ستسمع غالبًا أن المحسنات تساعد في "الهروب من النقاط الصغرى المحلية". في الممارسة العملية، أظهرت الأبحاث أن المشكلة الأكبر في الشبكات العصبية عالية الأبعاد هي نقاط السرج (saddle points) — وهي الأماكن التي يكون فيها التدرج صفرًا ولكن النقطة ليست صغرى ولا عظمى 2. يساعد Adam والطرق القائمة على الزخم (momentum) في التنقل عبر نقاط السرج بكفاءة.

التنظيم: منع الإفراط في التجهيز

عندما يحقق نموذجك دقة تدريب 99% ولكن 70% فقط على بيانات الاختبار، فإنه يعاني من الإفراط في التجهيز (overfitting) — أي حفظ بيانات التدريب بدلاً من تعلم أنماط قابلة للتعميم.

تنظيم L2 يضيف عقوبة إلى دالة الخسارة تتناسب مع مربع حجم الأوزان:

total_loss = cross_entropy_loss + (lambda / 2m) * Σ(w²)

هذا يمنع أي وزن منفرد من أن يصبح كبيرًا جدًا. مساهمة التدرج هي ببساطة (lambda / m) * w، وهي مدرجة بالفعل في صنف NeuralNetwork أعلاه.

يقوم الـ Dropout بتصفير جزء من تنشيطات الخلايا العصبية عشوائيًا خلال كل خطوة تدريب. هذا يجبر الشبكة على تعلم تمثيلات احتياطية — فلا يمكن لخلية عصبية واحدة أن تصبح نقطة فشل حرجة. في وقت الاستدلال (inference)، تكون جميع الخلايا العصبية نشطة ولكننا لا نحتاج إلى تغيير المقياس لأننا استخدمنا inverted dropout أثناء التدريب.

كيفية اختيار قوة التنظيم (Regularization):

  • ابدأ بـ l2_lambda=0.001 و dropout_rate=0.2
  • إذا كان هناك لا يزال فرط تخصيص (overfitting)، فقم بزيادتهما تدريجيًا
  • إذا كان هناك نقص في التخصيص (underfitting) (دقة تدريب منخفضة)، فقم بتقليلهما
  • راقب الفجوة بين خسارة التدريب وخسارة التحقق — يجب أن يقلل التنظيم هذه الفجوة

التدريب بالدفعات الصغيرة (Mini-Batch Training)

التدريب على كامل مجموعة البيانات لكل تحديث (batch gradient descent) بطيء. والتحديث بعد كل عينة واحدة (stochastic gradient descent) مشوش. الاشتقاق المتدرج بالدفعات الصغيرة (Mini-batch gradient descent) هو الحل الوسط العملي:

def train(network, X, y, epochs=100, batch_size=32, optimizer=None):
    """Train with mini-batches and optional Adam optimizer."""
    m = X.shape[0]
    history = []

    for epoch in range(epochs):
        # Shuffle data each epoch
        indices = np.random.permutation(m)
        X_shuffled = X[indices]
        y_shuffled = y[indices]

        epoch_loss = 0
        num_batches = 0

        for start in range(0, m, batch_size):
            end = min(start + batch_size, m)
            X_batch = X_shuffled[start:end]
            y_batch = y_shuffled[start:end]

            # Forward and backward pass
            output = network.forward(X_batch, training=True)
            weight_grads, bias_grads = network.backward(X_batch, y_batch)

            # Update weights
            if optimizer:
                optimizer.update(network.weights, network.biases, weight_grads, bias_grads)
            else:
                for i in range(network.num_layers):
                    network.weights[i] -= network.learning_rate * weight_grads[i]
                    network.biases[i] -= network.learning_rate * bias_grads[i]

            epoch_loss += network.compute_loss(y_batch, output)
            num_batches += 1

        avg_loss = epoch_loss / num_batches
        history.append(avg_loss)

        if epoch % 10 == 0:
            predictions = network.predict(X)
            accuracy = np.mean(predictions == y)
            print(f"Epoch {epoch:4d} | Loss: {avg_loss:.4f} | Accuracy: {accuracy:.4f}")

    return history

إرشادات حجم الدفعة (Batch size):

  • 32 — قيمة افتراضية جيدة لمعظم المشاكل
  • 64–128 — تدريب أسرع قليلاً، وغالباً ما يعمل بنفس الكفاءة
  • 256+ — قد يحتاج إلى إحماء معدل التعلم (learning rate warmup) للتقارب بشكل صحيح
  • قوى العدد 2 هي التقليدية (لمحاذاة ذاكرة GPU) ولكنها ليست مطلوبة للتدريب على CPU

تجميع كل شيء معًا

إليك مثال كامل يعمل على تدريب مجموعة بيانات اصطناعية:

# Generate synthetic dataset — two interleaving half-moons
from sklearn.datasets import make_moons

X, y = make_moons(n_samples=1000, noise=0.2, random_state=42)
y = y.reshape(-1, 1).astype(float)

# Split into train/test
split = int(0.8 * len(X))
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]

# Create network: 2 inputs → 64 hidden → 32 hidden → 1 output
network = NeuralNetwork(
    layer_sizes=[2, 64, 32, 1],
    learning_rate=0.001,
    l2_lambda=0.001,
    dropout_rate=0.1
)

optimizer = AdamOptimizer(learning_rate=0.001)

# Train
history = train(network, X_train, y_train, epochs=200, batch_size=32, optimizer=optimizer)

# Evaluate
test_preds = network.predict(X_test)
test_accuracy = np.mean(test_preds == y_test)
print(f"\nTest Accuracy: {test_accuracy:.4f}")

تقييم أداء النموذج: ما وراء الدقة

الدقة وحدها قد تكون مضللة. إذا كانت 95% من بياناتك تنتمي للفئة A، فإن النموذج الذي يتوقع دائمًا الفئة A سيحصل على دقة 95% بينما هو عديم الفائدة فعليًا.

def evaluate(y_true, y_pred, y_probs=None):
    """Compute classification metrics."""
    tp = np.sum((y_pred == 1) & (y_true == 1))
    fp = np.sum((y_pred == 1) & (y_true == 0))
    fn = np.sum((y_pred == 0) & (y_true == 1))
    tn = np.sum((y_pred == 0) & (y_true == 0))

    accuracy = (tp + tn) / (tp + fp + fn + tn)
    precision = tp / (tp + fp) if (tp + fp) > 0 else 0
    recall = tp / (tp + fn) if (tp + fn) > 0 else 0
    f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0

    print(f"Accuracy:  {accuracy:.4f}")
    print(f"Precision: {precision:.4f}")
    print(f"Recall:    {recall:.4f}")
    print(f"F1-score:  {f1:.4f}")

    # ROC AUC (simplified — requires probability scores)
    if y_probs is not None:
        thresholds = np.linspace(0, 1, 100)
        tpr_list, fpr_list = [], []
        for t in thresholds:
            preds = (y_probs >= t).astype(int)
            tp_t = np.sum((preds == 1) & (y_true == 1))
            fp_t = np.sum((preds == 1) & (y_true == 0))
            fn_t = np.sum((preds == 0) & (y_true == 1))
            tn_t = np.sum((preds == 0) & (y_true == 0))
            tpr_list.append(tp_t / (tp_t + fn_t) if (tp_t + fn_t) > 0 else 0)
            fpr_list.append(fp_t / (fp_t + tn_t) if (fp_t + tn_t) > 0 else 0)
        auc = -np.trapz(tpr_list, fpr_list)  # Numerical integration
        print(f"ROC AUC:   {auc:.4f}")

# Usage
test_probs = network.forward(X_test, training=False)
test_preds = (test_probs >= 0.5).astype(int)
evaluate(y_test, test_preds, test_probs)
  • الدقة (Precision) — من بين جميع التوقعات الإيجابية، كم منها كان إيجابيًا بالفعل؟ مهمة عندما تكون التوقعات الإيجابية الخاطئة مكلفة (كشف البريد العشوائي).
  • الاستدعاء (Recall) — من بين جميع الحالات الإيجابية الفعلية، كم منها استطعنا التقاطه؟ مهمة عندما تكون التوقعات السلبية الخاطئة مكلفة (كشف الأمراض).
  • مقياس F1 (F1-score) — المتوسط التوافقي للدقة والاستدعاء. مفيد عندما تحتاج إلى الموازنة بينهما.
  • ROC AUC — يقيس مدى قدرة النموذج على فصل الفئات عبر جميع العتبات (thresholds). 0.5 = تخمين عشوائي، 1.0 = فصل مثالي.

تصحيح الأخطاء: تتبع الاشتقاقات عبر الشبكة

عندما لا تتدرب شبكتك، تكون المشكلة دائمًا تقريبًا في الاشتقاقات (gradients). إليك كيفية فحصها:

def debug_gradients(network, X_sample, y_sample):
    """Print gradient statistics per layer to diagnose training issues."""
    network.forward(X_sample, training=True)
    weight_grads, bias_grads = network.backward(X_sample, y_sample)

    print("Layer | Weight Grad Mean | Weight Grad Std | Max Abs Grad")
    print("-" * 62)
    for i, (wg, bg) in enumerate(zip(weight_grads, bias_grads)):
        print(f"  {i:2d}  | {np.mean(np.abs(wg)):14.8f} | {np.std(wg):14.8f} | {np.max(np.abs(wg)):12.8f}")

    # Warnings
    for i, wg in enumerate(weight_grads):
        mean_abs = np.mean(np.abs(wg))
        if mean_abs < 1e-7:
            print(f"\n  WARNING: Layer {i} gradients are near zero (vanishing gradient).")
            print(f"  Try: ReLU activation, He initialization, or fewer layers.")
        elif mean_abs > 100:
            print(f"\n  WARNING: Layer {i} gradients are very large (exploding gradient).")
            print(f"  Try: gradient clipping, lower learning rate, or batch normalization.")

مشاكل الاشتقاق الشائعة:

  • تلاشي الاشتقاقات (Vanishing gradients) (كلها قريبة من الصفر) — تتقلص الاشتقاقات أثناء تدفقها للخلف عبر العديد من الطبقات. الحل: استخدم ReLU بدلاً من sigmoid للطبقات المخفية، واستخدم He initialization.
  • انفجار الاشتقاقات (Exploding gradients) (قيم كبيرة جدًا) — تنمو الاشتقاقات بشكل أسي. الحل: قص الاشتقاق (gradient clipping)، أو تقليل معدل التعلم، أو استخدام batch normalization.
  • خلايا ReLU الميتة (Dead ReLU neurons) (الاشتقاق صفر تمامًا للعديد من الخلايا) — يحدث عندما تخرج الخلايا صفرًا لجميع المدخلات. الحل: استخدم Leaky ReLU أو قلل معدل التعلم.

البناء من الصفر مقابل المكتبات: متى نستخدم ماذا؟

البناء من الصفر يعلمك الآليات. للإنتاج الفعلي، استخدم إطار عمل:

الجانب من الصفر PyTorch / TensorFlow
تسريع GPU يدوي (صعب) تلقائي
التفاضل التلقائي انتشار خلفي يدوي autograd مدمج
طبقات جاهزة تكتب كل شيء Conv, LSTM, Transformer، إلخ.
سرعة التدريب بطيئة (NumPy محدود بـ CPU) أسرع بـ 10-100 مرة مع GPU
فهم تصحيح الأخطاء ممتاز يتطلب معرفة بإطار العمل
عمليات مخصصة تحكم كامل ممكن ولكن أكثر تعقيدًا

المعرفة من هذا الدليل تنتقل مباشرة. عندما يتم تشغيل loss.backward() في PyTorch، فإنه يفعل بالضبط ما تفعله دالة backward() الخاصة بنا — فقط بشكل أسرع ومع تفاضل تلقائي بدلاً من حساب الاشتقاق اليدوي.

الخلاصة

لديك الآن شبكة عصبية تعمل تم بناؤها بالكامل من الصفر — بدون سحر أطر العمل، فقط NumPy والرياضيات. لقد قمت بتنفيذ:

  • الانتشار الأمامي والخلفي باستخدام قاعدة السلسلة (chain rule)
  • محسن Adam مع تصحيح الانحياز (bias correction)
  • تنظيم L2 و inverted dropout
  • التدريب بالدفعات الصغيرة مع التبديل العشوائي (shuffling)
  • مقاييس تقييم تتجاوز الدقة
  • أدوات تصحيح أخطاء الاشتقاق

الخطوة التالية هي نقل هذا الفهم إلى PyTorch أو TensorFlow. ستجد أن كل مفهوم — دوال الخسارة، المحسنات، التنظيم، التدريب بالدفعات — يرتبط مباشرة بواجهات برمجة تطبيقات (APIs) أطر العمل. الفرق هو أنك ستفهم الآن ما يحدث في الداخل.

Footnotes

  1. Kingma, D.P. & Ba, J. (2015). Adam: A Method for Stochastic Optimization. Proceedings of the 3rd International Conference on Learning Representations (ICLR).

  2. Dauphin, Y. et al. (2014). Identifying and attacking the saddle point problem in high-dimensional non-convex optimization. Advances in Neural Information Processing Systems (NeurIPS).


نشرة أسبوعية مجانية

ابقَ على مسار النيرد

بريد واحد أسبوعياً — دورات، مقالات معمّقة، أدوات، وتجارب ذكاء اصطناعي.

بدون إزعاج. إلغاء الاشتراك في أي وقت.