Python والخوارزميات لـ ML
معالجة المصفوفات
لماذا هذا مهم
بيانات 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)
النقاط الرئيسية
- أتقن معلمة المحور - افهم axis=0 (الأعمدة) مقابل axis=1 (الصفوف)
- استخدم keepdims=True - يمنع عدم تطابق الأبعاد في البث
- الفهرسة المنطقية قوية - رشّح البيانات بدون حلقات
- استخدم المتجهات للعمليات - أسرع 10-100 مرة من الحلقات
- افهم العروض مقابل النسخ - تجنب تخصيص الذاكرة غير الضروري
ما التالي؟
في الدرس التالي، سنغطي أنماط الخوارزميات الشائعة (مؤشران، نافذة منزلقة، بحث ثنائي) المُكيفة لسياقات مقابلة ML.
:::