تخطَّ إلى المحتوى

التجميع: اكتشاف المجموعات بخوارزمية K-Means

الدرس 8 من 8 · تعلّم الآلة

في كل ما سبق كانت معك الإجابات الصحيحة: تسميات تتدرب عليها وتقيس بها. لكن بيانات كثيرة تأتي بلا أي تسميات: قائمة عملاء، أو مقالات، أو قراءات أجهزة. هنا يأتي التجميع (Clustering) الذي لمحته في درس أنواع التعلّم. في هذا الدرس الأخير ستفهم كيف تعمل خوارزمية K-Means، وتقع عمداً في أشهر أخطائها ثم تصلحه، وتتعلم كيف تختار عدد المجموعات.

الحرف K هو عدد المجموعات الذي تختاره أنت. ثم تكرر الخوارزمية خطوتين:

  1. تضع K نقطة عشوائية تسمى المراكز (Centroids).
  2. تُلحق كل عينة بأقرب مركز إليها.
  3. تنقل كل مركز إلى متوسط العينات التي التحقت به.
  4. تعيد الخطوتين 2 و 3 حتى تتوقف المراكز عن الحركة.

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

متجر سجّل لخمسة عشر عميلاً عدد الزيارات في الشهر ومتوسط الفاتورة:

import numpy as np
from sklearn.cluster import KMeans
# [عدد الزيارات في الشهر، متوسط الفاتورة بالجنيه]
customers = np.array([
[2, 60], [3, 80], [1, 50], [2, 90], [4, 70],
[12, 120], [14, 100], [11, 140], [13, 110], [15, 130],
[3, 600], [2, 750], [4, 680], [1, 820], [3, 700],
])

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

المحاولة الأولى وفخ المقاييس

Section titled “المحاولة الأولى وفخ المقاييس”
km = KMeans(n_clusters=3, n_init=10, random_state=42)
print(km.fit_predict(customers))
الناتج
[1 1 1 1 1 1 1 1 1 1 0 2 0 2 0]

نتيجة مخيّبة! العملاء العشرة الأوائل في مجموعة واحدة، مع أن نصفهم يزور 12 مرة أو أكثر، وأصحاب الفواتير الكبيرة انقسموا إلى مجموعتين بلا سبب مفهوم.

السبب أن K-Means تحسب المسافة، والفاتورة بالمئات والزيارات بالآحاد. فرق 100 جنيه يطغى تماماً على فرق 10 زيارات، فكأن عمود الزيارات غير موجود. هذه هي المشكلة نفسها التي رأيتها مع خوارزمية الجيران الأقرب، والعلاج نفسه: توحيد المقاييس.

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaled = scaler.fit_transform(customers)
km = KMeans(n_clusters=3, n_init=10, random_state=42)
labels = km.fit_predict(scaled)
print(labels)
الناتج
[2 2 2 2 2 0 0 0 0 0 1 1 1 1 1]

الآن وجد النموذج المجموعات الثلاث التي رأيناها بأعيننا. وتذكّر أن الأرقام 0 و 1 و 2 مجرد أسماء بلا ترتيب ولا معنى.

  • n_init=10: شغّل الخوارزمية عشر مرات ببدايات عشوائية مختلفة واحتفظ بأفضلها، فالبداية السيئة قد تعطي تقسيماً سيئاً.
  • random_state=42: يثبّت العشوائية لتحصل على النتيجة نفسها في كل تشغيل.

النموذج أعطاك أرقاماً، والمعنى مهمتك أنت. المراكز محسوبة بالمقاييس الموحّدة، فنعيدها إلى الوحدات الأصلية لنقرأها:

centers = scaler.inverse_transform(km.cluster_centers_)
print(centers.round(1))
الناتج
[[ 13. 120. ]
[ 2.6 710. ]
[ 2.4 70. ]]

كل سطر مركز مجموعة: متوسط الزيارات ثم متوسط الفاتورة.

المجموعة الزيارات الفاتورة اسم نقترحه فكرة تسويقية
0 13 120 العملاء الأوفياء برنامج نقاط
1 2.6 710 أصحاب المشتريات الكبيرة عروض على المنتجات الغالية
2 2.4 70 العملاء العابرون خصم يشجعهم على العودة

ولتصنيف عميل جديد، وحّد مقاييسه بالمحوِّل نفسه ثم اسأل النموذج:

new_customers = scaler.transform([[14, 110], [2, 650]])
print(km.predict(new_customers))
الناتج
[0 1]

الأول عميل وفيّ، والثاني من أصحاب المشتريات الكبيرة.

هنا عرفنا العدد بالنظر، لكن مع آلاف العملاء وعشر ميزات لن ترى شيئاً. المقياس inertia_ هو مجموع مربعات المسافات بين كل عينة ومركز مجموعتها، وكلما صغر كانت المجموعات أكثر تماسكاً:

for k in range(1, 7):
m = KMeans(n_clusters=k, n_init=10, random_state=42).fit(scaled)
print(k, round(m.inertia_, 2))
الناتج
1 30.0
2 12.05
3 1.12
4 0.75
5 0.46
6 0.3

الرقم ينخفض دائماً مع زيادة K، ولو جعلت كل عميل مجموعة وحده لوصل إلى الصفر، فلا تبحث عن أصغر قيمة. ابحث عن الكوع (Elbow): النقطة التي يتوقف بعدها الانخفاض الكبير. من 2 إلى 3 هبوط حاد (من 12.05 إلى 1.12)، وبعد 3 تحسّن ضئيل. الكوع عند 3.

الكوع ليس واضحاً دائماً. معامل الظل (Silhouette) يعطيك رقماً واحداً بين -1 و 1: كلما اقترب من 1 كانت كل عينة قريبة من مجموعتها وبعيدة عن غيرها.

from sklearn.metrics import silhouette_score
for k in range(2, 6):
m = KMeans(n_clusters=k, n_init=10, random_state=42).fit(scaled)
print(k, round(silhouette_score(scaled, m.labels_), 3))
الناتج
2 0.587
3 0.816
4 0.701
5 0.579

أعلى قيمة عند K يساوي 3، وهذا يوافق طريقة الكوع وما رأيناه بأعيننا.

لنجرّب على بيانات نعرف إجابتها: أزهار Iris، ونخفي أنواعها عن النموذج ثم نقارن:

import pandas as pd
from sklearn.datasets import load_iris
from sklearn.pipeline import make_pipeline
iris = load_iris()
pipe = make_pipeline(StandardScaler(), KMeans(n_clusters=3, n_init=10, random_state=42))
clusters = pipe.fit_predict(iris.data)
species = iris.target_names[iris.target]
print(pd.crosstab(species, clusters, rownames=['species'], colnames=['cluster']))
الناتج
cluster 0 1 2
species
setosa 0 50 0
versicolor 39 0 11
virginica 14 0 36

كل أزهار setosa الخمسين في مجموعة وحدها: نوع مميز فعلاً. لكن النوعين الآخرين اختلطا: 11 زهرة versicolor ذهبت مع virginica، و 14 زهرة virginica ذهبت مع versicolor. النموذج لم يخطئ، فهو لا يعرف الأنواع أصلاً. هو يجمع ما تتشابه قياساته، وهذان النوعان متقاربان في القياسات.

  1. أضف عميلاً يزور 14 مرة وفاتورته 700 جنيه، وأعد التجميع بثلاث مجموعات ثم بأربع. أين ذهب في كل مرة؟
  2. اطبع inertia_ للأعداد من 1 إلى 6 على البيانات غير الموحّدة، ولاحظ أين يظهر الكوع ولماذا يخدعك.
  3. احسب متوسط كل مجموعة بـ pandas: ضع البيانات في DataFrame، وأضف عموداً للمجموعة، ثم استخدم groupby('cluster').mean().
  4. تحدٍّ: جمّع أزهار Iris بدون توحيد المقاييس وقارن جدول crosstab بالجدول الذي في الدرس.
  • نسيان توحيد المقاييس: الميزة ذات الأرقام الكبيرة ستتحكم في النتيجة وحدها. وحّد المقاييس قبل أي خوارزمية تعتمد على المسافة.
  • اختيار K بأصغر inertia: القيمة تنخفض دائماً مع زيادة K. ابحث عن الكوع أو عن أعلى Silhouette.
  • اعتبار أرقام المجموعات تسميات ثابتة: المجموعة 0 اليوم قد تصبح المجموعة 2 عند إعادة التدريب. اعتمد على خصائص المجموعة لا على رقمها.
  • توحيد العميل الجديد بمحوِّل جديد: استخدم scaler.transform بالمحوِّل الذي تعلّم من بيانات التدريب، ولا تستدعِ fit مرة أخرى.

سؤال

ميزتان: العمر (من 18 إلى 70) والدخل السنوي (من 20000 إلى 900000). ماذا يحدث لو طبّقت K-Means بلا توحيد للمقاييس؟

سؤال

لماذا لا نختار عدد المجموعات الذي يعطي أصغر قيمة لـ inertia؟

  • K-Means تكرر خطوتين: إلحاق كل عينة بأقرب مركز، ثم نقل المركز إلى متوسط عيناته.
  • وحّد المقاييس أولاً، وإلا تحكمت الميزة ذات الأرقام الكبيرة في النتيجة.
  • اختر عدد المجموعات بطريقة الكوع أو بأعلى معامل Silhouette.
  • النموذج يعطيك المجموعات، وتفسيرها وتسميتها مهمتك أنت.
  • مبروك! أنهيت كورس تعلّم الآلة: من البيانات إلى نماذج تصنّف وتتوقع الأرقام وتكتشف المجموعات، مقيَّمة ومحسَّنة وجاهزة للاستخدام.