Python والخوارزميات لـ ML

معالجة المصفوفات

5 دقيقة للقراءة

لماذا هذا مهم

بيانات ML هي أساساً متعددة الأبعاد. كل مقابلة ML ستختبر قدرتك على معالجة المصفوفات بكفاءة. أتقن هذه الأنماط وستحل 60% من مشاكل برمجة ML بشكل أسرع.

عمليات المصفوفة الأساسية

1. التبديل وإعادة التشكيل

التبديل - تبديل الصفوف والأعمدة:

import numpy as np

# التبديل يبدل الأبعاد
X = np.array([[1, 2, 3],
              [4, 5, 6]])  # الشكل: (2, 3)

X_T = X.T                   # الشكل: (3, 2)
# [[1, 4],
#  [2, 5],
#  [3, 6]]

# استخدام شائع: ضرب المصفوفات
# إذا كان X هو (n_samples, n_features)، فإن X.T هو (n_features, n_samples)
covariance = X.T @ X  # (n_features, n_features)

إعادة التشكيل - تغيير الأبعاد:

# التسطيح إلى أحادي البعد
matrix = np.array([[1, 2], [3, 4], [5, 6]])  # (3, 2)
flat = matrix.reshape(-1)                    # (6,) أو matrix.flatten()
# [1, 2, 3, 4, 5, 6]

# إعادة التشكيل لأبعاد مختلفة
arr = np.arange(12)              # (12,)
matrix_3x4 = arr.reshape(3, 4)   # (3, 4)
matrix_2x6 = arr.reshape(2, 6)   # (2, 6)

# استخدم -1 لاستنتاج البعد
matrix_auto = arr.reshape(3, -1)  # (3, 4) - يستنتج 4

سؤال المقابلة:

"لديك مصفوفة مسطحة أحادية البعد من 784 عنصر تمثل صورة 28x28. حوّلها مرة أخرى إلى شكل صورة."

def array_to_image(flat_arr):
    """
    تحويل مصفوفة 784 عنصر إلى صورة 28x28

    الوقت: O(1) - إعادة التشكيل هي عرض، لا نسخ
    المساحة: O(1)
    """
    return flat_arr.reshape(28, 28)

# اختبار
flat = np.arange(784)
image = array_to_image(flat)
print(image.shape)  # (28, 28)

2. التجميع والإحصائيات

العمليات الواعية بالمحور:

X = np.array([[1, 2, 3],
              [4, 5, 6],
              [7, 8, 9]])

# axis=0: لأسفل الأعمدة (عمودياً)
col_sums = X.sum(axis=0)     # [12, 15, 18]
col_means = X.mean(axis=0)   # [4., 5., 6.]

# axis=1: عبر الصفوف (أفقياً)
row_sums = X.sum(axis=1)     # [6, 15, 24]
row_means = X.mean(axis=1)   # [2., 5., 8.]

# بدون محور: المصفوفة بأكملها
total = X.sum()              # 45

احتفظ بالأبعاد للبث:

# مشكلة: طرح متوسطات الصفوف من كل صف
X = np.array([[1, 2, 3], [4, 5, 6]])

# خطأ: هذا ينشئ مصفوفة أحادية البعد
row_means = X.mean(axis=1)          # الشكل: (2,)
# centered = X - row_means           # خطأ عدم تطابق الشكل!

# صحيح: احتفظ بالأبعاد
row_means = X.mean(axis=1, keepdims=True)  # الشكل: (2, 1)
centered = X - row_means                    # البث يعمل!
# [[-1,  0,  1],
#  [-1,  0,  1]]

دوال إحصائية شائعة:

X = np.array([[1, 2, 3], [4, 5, 6]])

# إحصائيات أساسية
X.min()            # 1
X.max()            # 6
X.mean()           # 3.5
X.std()            # الانحراف المعياري
X.var()            # التباين

# النسب المئوية
np.percentile(X, 50)   # الوسيط
np.percentile(X, [25, 75])  # الأرباع

# على طول المحور
X.argmax(axis=0)   # فهرس الأعلى في كل عمود
X.argmin(axis=1)   # فهرس الأدنى في كل صف

3. التقطيع والفهرسة

التقطيع الأساسي:

X = np.array([[1, 2, 3, 4],
              [5, 6, 7, 8],
              [9, 10, 11, 12]])

# احصل على أول صفين
first_two = X[:2, :]          # [[1,2,3,4], [5,6,7,8]]

# احصل على آخر عمودين
last_two_cols = X[:, -2:]     # [[3,4], [7,8], [11,12]]

# احصل على صف ونطاق عمود محدد
sub = X[1:, 1:3]              # [[6,7], [10,11]]

الفهرسة المنطقية:

X = np.array([[1, -2, 3], [-4, 5, -6]])

# حدد القيم الموجبة
positive = X[X > 0]           # [1, 3, 5]

# استبدل القيم السالبة بـ 0
X[X < 0] = 0
# [[1, 0, 3], [0, 5, 0]]

# تصفية صفية: احتفظ بالصفوف حيث العمود الأول > 0
condition = X[:, 0] > 0
filtered = X[condition]

الفهرسة الفاخرة:

X = np.array([[1, 2, 3],
              [4, 5, 6],
              [7, 8, 9]])

# حدد صفوف وأعمدة محددة
rows = [0, 2]
cols = [1, 2]
sub = X[rows, :][:, cols]     # [[2, 3], [8, 9]]

# متقدم: حدد القطر
indices = np.arange(3)
diagonal = X[indices, indices]  # [1, 5, 9]

سؤال المقابلة:

"معطى مصفوفة ثنائية الأبعاد من درجات الاختبار (الطلاب × المواد)، أرجع الطلاب الذين حصلوا على درجات أعلى من المتوسط في جميع المواد."

def high_performers(scores):
    """
    scores: مصفوفة (n_students, n_subjects)
    يُرجع: فهارس الطلاب الذين تجاوزوا المتوسط في جميع المواد

    الوقت: O(n * m) حيث n=الطلاب، m=المواد
    المساحة: O(n * m) لمصفوفة منطقية
    """
    # احسب المتوسط لكل مادة (عمود)
    subject_means = scores.mean(axis=0, keepdims=True)  # (1, n_subjects)

    # تحقق مما إذا كان كل طالب يتجاوز المتوسط في كل مادة
    above_avg = scores > subject_means  # (n_students, n_subjects)

    # احتفظ بالطلاب الذين تجاوزوا المتوسط في جميع المواد
    all_above = above_avg.all(axis=1)   # (n_students,)

    return np.where(all_above)[0]

# اختبار
scores = np.array([[85, 90, 88],  # الطالب 0: فوق المتوسط في جميع
                   [70, 75, 72],  # الطالب 1: ليس كل فوق المتوسط
                   [95, 92, 94]]) # الطالب 2: فوق المتوسط في جميع
print(high_performers(scores))  # [0, 2]

4. ضرب المصفوفات

الضرب القياسي وضرب المصفوفات:

# المتجهات: الضرب القياسي
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
dot = np.dot(a, b)      # 1*4 + 2*5 + 3*6 = 32

# المصفوفات: ضرب المصفوفات
A = np.array([[1, 2], [3, 4]])    # (2, 2)
B = np.array([[5, 6], [7, 8]])    # (2, 2)

# الطريقة 1: np.dot()
C = np.dot(A, B)

# الطريقة 2: مشغل @ (Python 3.5+)
C = A @ B

# النتيجة: [[19, 22], [43, 50]]

البث في عمليات المصفوفة:

# أضف تحيزاً لكل عينة
X = np.array([[1, 2], [3, 4], [5, 6]])  # (3, 2) - 3 عينات، 2 ميزة
bias = np.array([10, 20])                # (2,)

result = X + bias  # البث: (3, 2) + (2,) = (3, 2)
# [[11, 22], [13, 24], [15, 26]]

الانحدار الخطي مع المصفوفات:

def fit_linear_regression(X, y):
    """
    ملاءمة الانحدار الخطي باستخدام المعادلة العادية

    X: (n_samples, n_features)
    y: (n_samples,)
    يُرجع: الأوزان (n_features,)

    الصيغة: w = (X^T X)^{-1} X^T y
    """
    # أضف حد التقاطع
    ones = np.ones((X.shape[0], 1))
    X_with_intercept = np.hstack([ones, X])

    # المعادلة العادية
    XTX = X_with_intercept.T @ X_with_intercept
    XTy = X_with_intercept.T @ y
    weights = np.linalg.inv(XTX) @ XTy

    return weights

# اختبار
X = np.array([[1], [2], [3], [4]])
y = np.array([2, 4, 6, 8])
weights = fit_linear_regression(X, y)
print(weights)  # [~0, ~2] - التقاطع، الميل

5. التسلسل والتكديس

التكديس الأفقي والعمودي:

A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])

# تكديس عمودي (إضافة صفوف)
V = np.vstack([A, B])  # [[1,2], [3,4], [5,6], [7,8]] - الشكل: (4, 2)

# تكديس أفقي (إضافة أعمدة)
H = np.hstack([A, B])  # [[1,2,5,6], [3,4,7,8]] - الشكل: (2, 4)

# التسلسل العام
cat_rows = np.concatenate([A, B], axis=0)  # نفس vstack
cat_cols = np.concatenate([A, B], axis=1)  # نفس hstack

استخدام عملي: تقسيم التدريب/الاختبار:

def create_batches(X, y, batch_size):
    """
    تقسيم البيانات إلى دفعات

    X: (n_samples, n_features)
    y: (n_samples,)
    يُرجع: قائمة من أزواج (X_batch, y_batch)
    """
    n_samples = X.shape[0]
    batches = []

    for i in range(0, n_samples, batch_size):
        end = min(i + batch_size, n_samples)
        X_batch = X[i:end]
        y_batch = y[i:end]
        batches.append((X_batch, y_batch))

    return batches

# اختبار
X = np.arange(20).reshape(10, 2)
y = np.arange(10)
batches = create_batches(X, y, batch_size=3)
print(len(batches))  # 4 دفعات (3+3+3+1)

6. أنماط المعالجة المسبقة الشائعة لـ ML

تطبيع Min-Max:

def min_max_normalize(X):
    """
    قياس الميزات إلى نطاق [0, 1]

    X: (n_samples, n_features)
    يُرجع: X مطبّع
    """
    min_val = X.min(axis=0, keepdims=True)
    max_val = X.max(axis=0, keepdims=True)
    return (X - min_val) / (max_val - min_val)

# اختبار
X = np.array([[1, 200], [2, 300], [3, 400]])
normalized = min_max_normalize(X)
# [[0., 0.], [0.5, 0.5], [1., 1.]]

الترميز الساخن الواحد:

def one_hot_encode(labels, num_classes):
    """
    تحويل تسميات الفئة إلى متجهات ساخنة واحدة

    labels: مصفوفة (n_samples,) من فهارس الفئة
    num_classes: إجمالي عدد الفئات
    يُرجع: مصفوفة (n_samples, num_classes)
    """
    n_samples = len(labels)
    one_hot = np.zeros((n_samples, num_classes))
    one_hot[np.arange(n_samples), labels] = 1
    return one_hot

# اختبار
labels = np.array([0, 2, 1, 0])
one_hot = one_hot_encode(labels, num_classes=3)
# [[1, 0, 0],
#  [0, 0, 1],
#  [0, 1, 0],
#  [1, 0, 0]]

تعويض القيم المفقودة:

def impute_mean(X):
    """
    استبدال قيم NaN بمتوسط العمود

    X: مصفوفة (n_samples, n_features) بقيم NaN
    يُرجع: X مع استبدال NaN بمتوسطات الأعمدة
    """
    # إنشاء نسخة لتجنب تعديل الأصل
    X_imputed = X.copy()

    # لكل عمود
    for col in range(X.shape[1]):
        # احصل على القيم غير NaN
        col_data = X[:, col]
        mask = ~np.isnan(col_data)

        # احسب متوسط القيم غير NaN
        col_mean = col_data[mask].mean()

        # استبدل NaN بالمتوسط
        X_imputed[~mask, col] = col_mean

    return X_imputed

# اختبار
X = np.array([[1, 2, np.nan],
              [3, np.nan, 6],
              [5, 4, 7]])
imputed = impute_mean(X)
# [[1, 2, 6.5],
#  [3, 3, 6],
#  [5, 4, 7]]

مشكلة المقابلة: مصفوفة المسافة

المشكلة: معطى N نقطة في فضاء D-بُعد، احسب مصفوفة المسافة الإقليدية الزوجية.

def pairwise_distances(X):
    """
    حساب المسافات الإقليدية الزوجية

    X: (n_samples, n_features)
    يُرجع: مصفوفة المسافة (n_samples, n_samples)

    المسافة بين i وj: sqrt(sum((X[i] - X[j])^2))

    طريقة فعالة باستخدام البث:
    ||x - y||^2 = ||x||^2 + ||y||^2 - 2*x·y
    """
    # احسب المعايير المربعة لكل نقطة
    # X^2 مجموعة عبر الميزات: (n_samples, 1)
    X_squared = (X ** 2).sum(axis=1, keepdims=True)

    # مصفوفة الضرب القياسي: (n_samples, n_samples)
    XXT = X @ X.T

    # استخدم الصيغة: ||xi - xj||^2 = ||xi||^2 + ||xj||^2 - 2*xi·xj
    distances_squared = X_squared + X_squared.T - 2 * XXT

    # خذ الجذر التربيعي (استخدم maximum لتجنب القيم السالبة من الأخطاء العددية)
    distances = np.sqrt(np.maximum(distances_squared, 0))

    return distances

# اختبار
X = np.array([[0, 0], [3, 4], [1, 1]])
D = pairwise_distances(X)
# [[0., 5., 1.414],
#  [5., 0., 3.606],
#  [1.414, 3.606, 0.]]

# تحقق: المسافة بين [0,0] و[3,4] = sqrt(9+16) = 5 ✓

نصائح الأداء

1. استخدم المتجهات بدلاً من الحلقات:

# بطيء: حلقة
result = []
for i in range(len(arr)):
    result.append(arr[i] * 2)

# سريع: متجه
result = arr * 2

2. تجنب النسخ غير الضرورية:

# ينشئ نسخة
arr_copy = arr.reshape(-1)

# يُرجع عرضاً (لا نسخ)
arr_view = arr.ravel()

3. استخدم معلمات المحور:

# بطيء: حلقة على الصفوف
sums = [row.sum() for row in X]

# سريع: متجه
sums = X.sum(axis=1)

النقاط الرئيسية

  1. أتقن معلمة المحور - افهم axis=0 (الأعمدة) مقابل axis=1 (الصفوف)
  2. استخدم keepdims=True - يمنع عدم تطابق الأبعاد في البث
  3. الفهرسة المنطقية قوية - رشّح البيانات بدون حلقات
  4. استخدم المتجهات للعمليات - أسرع 10-100 مرة من الحلقات
  5. افهم العروض مقابل النسخ - تجنب تخصيص الذاكرة غير الضروري

ما التالي؟

في الدرس التالي، سنغطي أنماط الخوارزميات الشائعة (مؤشران، نافذة منزلقة، بحث ثنائي) المُكيفة لسياقات مقابلة ML.

:::