K-Nearest Neighbors — تعلم الآلة
أقرب الجيران (K-Nearest Neighbors)
أبسط فكرة في تعلم الآلة: قل لي من جيرانك، أقل لك من أنت. لتصنيف نقطة جديدة، ابحث عن أقرب K نقاط في بيانات التدريب، وخذ الفئة الأكثر بينها.
لا تدريب حقيقي
fit في KNN مجرد حفظ للبيانات. كل العمل يحدث عند predict: حساب المسافة لكل نقاط التدريب. لذلك التدريب فوري، والتنبؤ بطيء مع البيانات الكبيرة.
المسافة
import numpy as np
a = np.array([170, 65]) # الطول سم، الوزن كغ
b = np.array([160, 70])
print("إقليدية:", np.sqrt(((a - b) ** 2).sum()))
print("مانهاتن:", np.abs(a - b).sum())التقييس إلزامي
إذا كانت ميزة بالآلاف (الدخل) وأخرى بالعشرات (العمر)، فالمسافة يحددها الدخل وحده تقريباً. قيّس دائماً قبل KNN (StandardScaler)، مع fit على التدريب فقط.
اختيار K
| K | السلوك |
|---|---|
| صغير (1) | حساس للضوضاء: نقطة شاذة واحدة تغيّر القرار ← Overfitting |
| كبير جداً | يتجاهل الأنماط المحلية، ويميل للفئة الأكثر عدداً ← Underfitting |
- اختر K بـ Cross-Validation، لا بالتجربة على الاختبار.
- مع فئتين يُفضل K فردي لتجنب التعادل.
- K لا يمكن أن يتجاوز عدد عينات التدريب (المثال الأصلي كان يجرب 7 جيران على 6 عينات فيتوقف بخطأ).
- weights="distance": الجيران الأقرب صوتهم أعلى.
from sklearn.datasets import load_wine
from sklearn.model_selection import cross_val_score
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
X, y = load_wine(return_X_y=True)
for k in [1, 5, 15]:
raw = cross_val_score(KNeighborsClassifier(n_neighbors=k), X, y, cv=5).mean()
scaled = cross_val_score(make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=k)), X, y, cv=5).mean()
print(f"K={k}: بلا تقييس {raw:.2f} | مع تقييس {scaled:.2f}")لاحظ الفرق الكبير الذي يصنعه التقييس. (make_pipeline يضمن أن المقياس يتعلم من جزء التدريب فقط في كل دورة — تفاصيله في درس Pipelines.)
لعنة الأبعاد
مع مئات الميزات تصبح كل النقاط "بعيدة" عن بعضها بشكل متقارب، فيفقد مفهوم "الأقرب" معناه. KNN يعمل أفضل مع ميزات قليلة نسبياً ومفيدة.
استخداماته
خط أساس سريع، أنظمة توصية بسيطة ("منتجات مشابهة")، وملء القيم الناقصة (KNNImputer).
تمرين
في المثال الكامل استبدل حلقة K بـ GridSearchCV على قيم K من 1 إلى 15 مع خيار weights، واطبع أفضل إعداد.
import numpy as np
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
rng = np.random.default_rng(7)
# مجموعتان من العملاء: العمر والدخل الشهري (بالدولار) — مقياسان مختلفان جداً
young = np.column_stack([rng.normal(24, 3, 60), rng.normal(600, 150, 60)])
mature = np.column_stack([rng.normal(45, 6, 60), rng.normal(1400, 300, 60)])
X = np.vstack([young, mature])
y = np.array([0] * 60 + [1] * 60)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
print(f"عينات التدريب: {len(X_train)} — K يجب أن يكون أصغر من هذا العدد")
# اختيار K بـ Cross-Validation على التدريب فقط
for k in [1, 3, 5, 7, 15]:
pipe = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=k))
score = cross_val_score(pipe, X_train, y_train, cv=5).mean()
print(f"K={k:2d}: دقة CV = {score:.2%}")
best = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=5))
best.fit(X_train, y_train)
print(f"\nدقة الاختبار النهائية (K=5): {best.score(X_test, y_test):.2%}")
new_customer = [[30, 900]]
print(f"عميل عمره 30 ودخله 900 ← الفئة {best.predict(new_customer)[0]}")اختبر فهمك