المعمل الثاني: الخرائط ذاتية التنظيم (خرائط Kohonen)
المواد الدراسية - التعلم غير الموجه (Unsupervised Learning)
1. مقدمة عن الخرائط ذاتية التنظيم
الخريطة ذاتية التنظيم (Self-Organizing Map - SOM) أو خريطة كوهونين (Kohonen Map) هي خوارزمية شبكة عصبية غير موجهة (unsupervised neural network) تعتمد على نماذج عصبية بيولوجية تعود إلى سبعينيات القرن الماضي. تستخدم أسلوب التعلم التنافسي (competitive learning)، وهي مصممة أساساً من أجل:
- التجميع (Clustering): تجميع نقاط البيانات المتشابهة معاً
- تقليل الأبعاد (Dimensionality Reduction): تحويل البيانات عالية الأبعاد إلى فضاء أقل أبعاداً
- التصور البصري (Visualization): تسهيل تفسير مجموعات البيانات المعقدة
البنية (Architecture)
تتكون الخريطة ذاتية التنظيم من طبقتين رئيسيتين:
- طبقة الإدخال (Input Layer): تمثل خصائص (features) البيانات
- طبقة الإخراج (Output Layer): مرتبة في شبكة ثنائية الأبعاد (2D grid) من الخلايا العصبية (neurons)، بحيث تمثل كل خلية عصبية عنقوداً (cluster) من البيانات
2. كيف تعمل الخرائط ذاتية التنظيم
تتبع خوارزمية SOM الخطوات الأساسية التالية:
الخطوة 1: التهيئة (Initialization)
- تُهيَّأ أوزان (weights) خلايا الإخراج العصبية بشكل عشوائي
- تمثل هذه الأوزان خصائص كل خلية عصبية
- سيتم تعديل الأوزان أثناء التدريب
الخطوة 2: التنافس (Competition)
- لكل متجه إدخال (input vector)، تحسب SOM المسافة الإقليدية (Euclidean distance) بين الإدخال ومتجهات الأوزان لكل الخلايا العصبية
- الخلية العصبية ذات أقل مسافة هي الخلية الفائزة (winning neuron) (وحدة أفضل تطابق - Best Matching Unit - BMU)
الصيغة الرياضية:
حيث:
- هي المسافة الخاصة بالخلية العصبية
- هو عدد الخصائص (features)
- هو وزن الخلية العصبية للخاصية
- هي خاصية الإدخال
الخطوة 3: تحديث الأوزان (Weight Update)
- تُحدَّث أوزان الخلية العصبية الفائزة لتقترب أكثر من متجه الإدخال
- تُعدَّل أيضاً الخلايا العصبية المجاورة (بتغييرات أصغر)
الصيغة الرياضية:
حيث:
- هو معدل التعلم (learning rate)
- هي خاصية الإدخال
الخطوة 4: تناقص معدل التعلم (Learning Rate Decay)
- يتناقص معدل التعلم مع مرور الوقت
- هذا يسمح للخريطة بالتقارب (converge) نحو قيم مستقرة
الصيغة الرياضية:
الخطوة 5: شرط التوقف (Stopping Condition)
- يتوقف التدريب عندما:
- يُصل إلى العدد الأقصى من الحقب (epochs)، أو
- تتقارب الأوزان (تصبح التغييرات ضئيلة جداً)
3. التنفيذ بلغة Python
لننفذ خوارزمية SOM خطوة بخطوة.
الخطوة 1: استيراد المكتبات المطلوبة
# Import necessary libraries
import math
import matplotlib.pyplot as plt
import numpy as np
print("Libraries imported successfully!")Libraries imported successfully!الخطوة 2: تعريف صنف SOM
سننشئ صنفاً (class) يحتوي على دالتين رئيسيتين:
winner(): تجد الخلية العصبية الفائزة (العنقود) عبر حساب المسافات الإقليديةupdate(): تُحدِّث متجهات الأوزان الخاصة بالخلية العصبية الفائزة
class SOM:
"""
Self-Organizing Map implementation with two clusters
"""
def winner(self, weights, sample):
"""
Find the winning neuron (Best Matching Unit)
Parameters:
- weights: Current weight vectors for all neurons
- sample: Input sample to classify
Returns:
- Index of the winning neuron (0 or 1)
"""
D0 = 0 # Distance to cluster 0
D1 = 0 # Distance to cluster 1
# Calculate Euclidean distance to each cluster
for i in range(len(sample)):
D0 += math.pow((sample[i] - weights[0][i]), 2)
D1 += math.pow((sample[i] - weights[1][i]), 2)
# Return the cluster with minimum distance
return 0 if D0 < D1 else 1
def update(self, weights, sample, J, alpha):
"""
Update weights of the winning neuron
Parameters:
- weights: Current weight vectors
- sample: Input sample
- J: Index of winning neuron
- alpha: Learning rate
Returns:
- Updated weight vectors
"""
for i in range(len(weights[0])):
# Apply weight update formula
weights[J][i] = weights[J][i] + alpha * (sample[i] - weights[J][i])
return weights
print("SOM class defined successfully!")SOM class defined successfully!الخطوة 3: تحضير بيانات التدريب
سنعرّف:
- بيانات التدريب (T): 4 عينات، كل عينة بها 4 خصائص
- الأوزان الابتدائية: تهيئة عشوائية لعنقودين (clusters)
- المعاملات الفائقة (Hyperparameters): عدد الحقب (epochs) ومعدل التعلم
# Training data: 4 samples with 4 features each
T = [[1, 1, 0, 0],
[0, 0, 0, 1],
[1, 0, 0, 0],
[0, 0, 1, 1]]
# Get dimensions
m, n = len(T), len(T[0]) # m=4 samples, n=4 features
# Initialize weights for 2 clusters randomly
weights = [[0.2, 0.6, 0.5, 0.9], # Cluster 0 initial weights
[0.8, 0.4, 0.7, 0.3]] # Cluster 1 initial weights
# Create SOM object
som = SOM()
# Hyperparameters
epochs = 10 # Number of training iterations
alpha = 0.5 # Initial learning rate
print(f"Training data shape: {m} samples × {n} features")
print(f"Number of clusters: {len(weights)}")
print(f"Training for {epochs} epochs with initial learning rate α = {alpha}")Training data shape: 4 samples × 4 features
Number of clusters: 2
Training for 10 epochs with initial learning rate α = 0.5الخطوة 4: تدريب شبكة SOM
خطوات عملية التدريب:
- التكرار عبر كل حقبة (epoch)
- لكل عينة تدريب:
- إيجاد الخلية العصبية الفائزة
- تحديث أوزان الخلية العصبية الفائزة
- تخفيض معدل التعلم
- تخزين الأوزان لأغراض التصور البصري
# Store weight history for visualization
weight_history_0 = []
weight_history_1 = []
# Training loop
for epoch in range(epochs):
print(f"\n--- Epoch {epoch + 1}/{epochs} (α = {alpha:.4f}) ---")
for j in range(m):
sample = T[j]
# Find winning neuron
J = som.winner(weights, sample)
# Update weights
weights = som.update(weights, sample, J, alpha)
print(f" Sample {sample} → Cluster {J}")
# Store weights after each epoch
weight_history_0.append(list(weights[0]))
weight_history_1.append(list(weights[1]))
# Decay learning rate
alpha *= 0.9
print("\n" + "="*50)
print("Training completed!")
print("="*50)
--- Epoch 1/10 (α = 0.5000) ---
Sample [1, 1, 0, 0] → Cluster 1
Sample [0, 0, 0, 1] → Cluster 0
Sample [1, 0, 0, 0] → Cluster 1
Sample [0, 0, 1, 1] → Cluster 0
--- Epoch 2/10 (α = 0.4500) ---
Sample [1, 1, 0, 0] → Cluster 1
Sample [0, 0, 0, 1] → Cluster 0
Sample [1, 0, 0, 0] → Cluster 1
Sample [0, 0, 1, 1] → Cluster 0
--- Epoch 3/10 (α = 0.4050) ---
Sample [1, 1, 0, 0] → Cluster 1
Sample [0, 0, 0, 1] → Cluster 0
Sample [1, 0, 0, 0] → Cluster 1
Sample [0, 0, 1, 1] → Cluster 0
--- Epoch 4/10 (α = 0.3645) ---
Sample [1, 1, 0, 0] → Cluster 1
Sample [0, 0, 0, 1] → Cluster 0
Sample [1, 0, 0, 0] → Cluster 1
Sample [0, 0, 1, 1] → Cluster 0
--- Epoch 5/10 (α = 0.3281) ---
Sample [1, 1, 0, 0] → Cluster 1
Sample [0, 0, 0, 1] → Cluster 0
Sample [1, 0, 0, 0] → Cluster 1
Sample [0, 0, 1, 1] → Cluster 0
--- Epoch 6/10 (α = 0.2952) ---
Sample [1, 1, 0, 0] → Cluster 1
Sample [0, 0, 0, 1] → Cluster 0
Sample [1, 0, 0, 0] → Cluster 1
Sample [0, 0, 1, 1] → Cluster 0
--- Epoch 7/10 (α = 0.2657) ---
Sample [1, 1, 0, 0] → Cluster 1
Sample [0, 0, 0, 1] → Cluster 0
Sample [1, 0, 0, 0] → Cluster 1
Sample [0, 0, 1, 1] → Cluster 0
--- Epoch 8/10 (α = 0.2391) ---
Sample [1, 1, 0, 0] → Cluster 1
Sample [0, 0, 0, 1] → Cluster 0
Sample [1, 0, 0, 0] → Cluster 1
Sample [0, 0, 1, 1] → Cluster 0
--- Epoch 9/10 (α = 0.2152) ---
Sample [1, 1, 0, 0] → Cluster 1
Sample [0, 0, 0, 1] → Cluster 0
Sample [1, 0, 0, 0] → Cluster 1
Sample [0, 0, 1, 1] → Cluster 0
--- Epoch 10/10 (α = 0.1937) ---
Sample [1, 1, 0, 0] → Cluster 1
Sample [0, 0, 0, 1] → Cluster 0
Sample [1, 0, 0, 0] → Cluster 1
Sample [0, 0, 1, 1] → Cluster 0
==================================================
Training completed!
==================================================الخطوة 5: اختبار شبكة SOM المدربة
الآن لنصنّف عينة اختبار باستخدام الشبكة المدربة.
# Test sample
test_sample = [0, 0, 0, 1]
# Find which cluster it belongs to
predicted_cluster = som.winner(weights, test_sample)
print(f"Test Sample: {test_sample}")
print(f"Predicted Cluster: {predicted_cluster}")
print(f"\nFinal Trained Weights:")
print(f" Cluster 0: {weights[0]}")
print(f" Cluster 1: {weights[1]}")Test Sample: [0, 0, 0, 1]
Predicted Cluster: 0
Final Trained Weights:
Cluster 0: [6.060332016352902e-05, 0.00018180996049058703, 0.5655848232229601, 0.9999696983399182]
Cluster 1: [0.9999393966798364, 0.4343848751169582, 0.00021211162057235155, 9.090498024529351e-05]4. التصور البصري (Visualization)
لنتصور بصرياً كيف تعلمت SOM تجميع البيانات من خلال رسمين بيانيين:
- تقارب الأوزان (Weight Convergence): يوضح كيف يتطور كل مكوّن من مكونات الأوزان أثناء التدريب
- العناقيد النهائية (Final Clusters): إسقاط ثنائي الأبعاد لعينات الإدخال ومراكز العناقيد النهائية
# Create figure with 2 subplots
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# ============= PLOT 1: Weight Convergence =============
features = [f"Feature {i+1}" for i in range(n)]
epochs_range = range(1, epochs + 1)
for i in range(n):
# Extract weight values for this feature across all epochs
feature_data_0 = [w[i] for w in weight_history_0]
feature_data_1 = [w[i] for w in weight_history_1]
# Plot for both clusters
axes[0].plot(epochs_range, feature_data_0,
label=f'Cluster 0 - {features[i]}',
linestyle='--', marker='o', markersize=4)
axes[0].plot(epochs_range, feature_data_1,
label=f'Cluster 1 - {features[i]}',
linestyle='-', marker='s', markersize=4)
axes[0].set_title('SOM Weight Convergence Over Epochs', fontsize=12, fontweight='bold')
axes[0].set_xlabel('Epoch', fontsize=10)
axes[0].set_ylabel('Weight Value', fontsize=10)
axes[0].legend(fontsize=8, loc='best')
axes[0].grid(True, alpha=0.3)
# ============= PLOT 2: Final Weights vs. Input Samples =============
# Using first two features for 2D visualization
# Extract first two features from input samples
input_x = [sample[0] for sample in T]
input_y = [sample[1] for sample in T]
# Plot input samples
axes[1].scatter(input_x, input_y,
color='blue', marker='o', s=150,
label='Input Samples',
edgecolors='black', linewidths=1.5, alpha=0.7)
# Extract first two features from final weights
final_weight_0_x = weights[0][0]
final_weight_0_y = weights[0][1]
final_weight_1_x = weights[1][0]
final_weight_1_y = weights[1][1]
# Plot final weight vectors (cluster centers)
axes[1].scatter(final_weight_0_x, final_weight_0_y,
color='red', marker='X', s=300,
label='Cluster 0 (Final Weight)',
edgecolors='darkred', linewidths=2)
axes[1].scatter(final_weight_1_x, final_weight_1_y,
color='green', marker='X', s=300,
label='Cluster 1 (Final Weight)',
edgecolors='darkgreen', linewidths=2)
# Annotate input samples
for i, (x, y) in enumerate(zip(input_x, input_y)):
axes[1].annotate(f'S{i+1}', (x, y),
textcoords="offset points",
xytext=(0, 10), ha='center', fontsize=9)
axes[1].set_title('SOM Final Weights vs. Input Samples\n(Features 1 & 2 Projection)',
fontsize=12, fontweight='bold')
axes[1].set_xlabel('Feature 1', fontsize=10)
axes[1].set_ylabel('Feature 2', fontsize=10)
axes[1].set_xlim(-0.1, 1.1)
axes[1].set_ylim(-0.1, 1.1)
axes[1].legend(fontsize=9, loc='best')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("Visualizations created successfully!")Visualizations created successfully!5. التحليل والتفسير
فهم النتائج
-
تقارب الأوزان (Weight Convergence):
- لاحظ كيف تستقر الأوزان مع مرور الحقب (epochs)
- تناقص معدل التعلم يساعد على تقارب أكثر انسيابية
- قد تتقارب الخصائص المختلفة بمعدلات مختلفة
-
تشكّل العناقيد (Cluster Formation):
- تعلمت SOM تمثيل البيانات بمركزَي عنقود (cluster centers)
- تمثل أوزان كل عنقود نمطاً نموذجياً (prototypical pattern) في البيانات
- تُوضَّع عينات الإدخال المتشابهة في نفس العنقود
-
تصنيف عينة الاختبار:
- تُصنَّف عينة الاختبار
[0, 0, 0, 1]بناءً على أقل مسافة إقليدية - هذا يُظهر قدرة SOM على التعميم (generalize) على بيانات جديدة
- تُصنَّف عينة الاختبار
6. أسئلة تدريبية
جرّب هذه التمارين لترسيخ فهمك:
-
تعديل تناقص معدل التعلم: غيّر صيغة التناقص من
alpha *= 0.9إلىalpha *= 0.5. كيف يؤثر ذلك على التقارب؟ -
إضافة المزيد من العناقيد: وسّع صنف SOM ليدعم 3 أو 4 عناقيد بدلاً من عنقودين.
-
تغيير الأوزان الابتدائية: جرّب تهيئات عشوائية مختلفة. هل ما زالت SOM تتقارب نحو عناقيد مشابهة؟
-
إضافة عينات تدريب جديدة: أضف عينات مثل
[1, 1, 1, 1]أو[0, 0, 0, 0]. كيف يغيّر ذلك تشكّل العناقيد؟ -
تنفيذ دالة الجوار (Neighborhood Function): في خرائط SOM الكلاسيكية، تُحدَّث الخلايا العصبية المجاورة أيضاً (وليس الفائزة فقط). نفّذ هذا الامتداد.
7. الكود الكامل (في خلية واحدة)
إليك التنفيذ الكامل كمرجع:
# Complete Self-Organizing Map Implementation
import math
import matplotlib.pyplot as plt
import numpy as np
class SOM:
def winner(self, weights, sample):
D0 = 0
D1 = 0
for i in range(len(sample)):
D0 += math.pow((sample[i] - weights[0][i]), 2)
D1 += math.pow((sample[i] - weights[1][i]), 2)
return 0 if D0 < D1 else 1
def update(self, weights, sample, J, alpha):
for i in range(len(weights[0])):
weights[J][i] = weights[J][i] + alpha * (sample[i] - weights[J][i])
return weights
def main():
# Training data
T = [[1, 1, 0, 0], [0, 0, 0, 1], [1, 0, 0, 0], [0, 0, 1, 1]]
m, n = len(T), len(T[0])
# Initialize weights
weights = [[0.2, 0.6, 0.5, 0.9], [0.8, 0.4, 0.7, 0.3]]
som = SOM()
# Hyperparameters
epochs = 10
alpha = 0.5
# Store weights for visualization
weight_history_0 = []
weight_history_1 = []
# Training
for epoch in range(epochs):
for j in range(m):
sample = T[j]
J = som.winner(weights, sample)
weights = som.update(weights, sample, J, alpha)
weight_history_0.append(list(weights[0]))
weight_history_1.append(list(weights[1]))
alpha *= 0.9
# Test
s = [0, 0, 0, 1]
J = som.winner(weights, s)
print("Test Sample s belongs to Cluster:", J)
print("Trained weights:", weights)
# Visualization
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Plot 1: Weight convergence
features = [f"Feature {i+1}" for i in range(n)]
epochs_range = range(1, epochs + 1)
for i in range(n):
feature_data_0 = [w[i] for w in weight_history_0]
feature_data_1 = [w[i] for w in weight_history_1]
axes[0].plot(epochs_range, feature_data_0, label=f'Weight 0 - {features[i]}', linestyle='--')
axes[0].plot(epochs_range, feature_data_1, label=f'Weight 1 - {features[i]}', linestyle='-')
axes[0].set_title('SOM Weight Convergence Over Epochs')
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('Weight Value')
axes[0].legend()
axes[0].grid(True)
# Plot 2: Final weights vs input samples
input_x = [sample[0] for sample in T]
input_y = [sample[1] for sample in T]
axes[1].scatter(input_x, input_y, color='blue', marker='o', s=100, label='Input Samples (Features 1 & 2)')
axes[1].scatter(weights[0][0], weights[0][1], color='red', marker='X', s=200, label='Final Weight Vector 0')
axes[1].scatter(weights[1][0], weights[1][1], color='green', marker='X', s=200, label='Final Weight Vector 1')
axes[1].set_title('SOM Final Weights vs. Input Samples (Features 1 & 2)')
axes[1].set_xlabel('Feature 1')
axes[1].set_ylabel('Feature 2')
axes[1].set_xlim(-0.1, 1.1)
axes[1].set_ylim(-0.1, 1.1)
axes[1].legend()
axes[1].grid(True)
plt.tight_layout()
plt.show()
if __name__ == "__main__":
main()Test Sample s belongs to Cluster: 0
Trained weights: [[6.060332016352902e-05, 0.00018180996049058703, 0.5655848232229601, 0.9999696983399182], [0.9999393966798364, 0.4343848751169582, 0.00021211162057235155, 9.090498024529351e-05]]8. الخلاصة والنقاط الأساسية
✅ الخرائط ذاتية التنظيم خوارزميات تعلم غير موجه (unsupervised) تُستخدم للتجميع (clustering) وتقليل الأبعاد (dimensionality reduction)
✅ التعلم التنافسي (Competitive Learning): تُحدَّث فقط الخلية العصبية الفائزة (وربما جيرانها)
✅ الحفاظ على الطوبولوجيا (Topology Preservation): تُوضَّع المدخلات المتشابهة عند خلايا عصبية متجاورة في شبكة الإخراج
✅ تناقص معدل التعلم (Learning Rate Decay): أساسي للتقارب (convergence) والاستقرار
✅ التطبيقات: تصور البيانات، التعرف على الأنماط، التحليل الاستكشافي للبيانات
المراجع
- Kohonen, T. (1982). "Self-organized formation of topologically correct feature maps"
- GeeksforGeeks - Self-Organising Maps
- Haykin, S. (1999). "Neural Networks: A Comprehensive Foundation"
آخر تحديث: أكتوبر 2025