Logo

المعمل الثاني: الخرائط ذاتية التنظيم (خرائط Kohonen)

12 دقيقة قراءة
شرائح الدرس
1 / 14

المعمل رقم 2: الخرائط ذاتية التنظيم (Self-Organizing Maps - خرائط كوهونين)

التعلّم التنافسي (Competitive Learning) للتجميع، وتقليل الأبعاد، والتصور البصري

المواد الدراسية - التعلم غير الموجه (Unsupervised Learning)


1. مقدمة عن الخرائط ذاتية التنظيم

الخريطة ذاتية التنظيم (Self-Organizing Map - SOM) أو خريطة كوهونين (Kohonen Map) هي خوارزمية شبكة عصبية غير موجهة (unsupervised neural network) تعتمد على نماذج عصبية بيولوجية تعود إلى سبعينيات القرن الماضي. تستخدم أسلوب التعلم التنافسي (competitive learning)، وهي مصممة أساساً من أجل:

  • التجميع (Clustering): تجميع نقاط البيانات المتشابهة معاً
  • تقليل الأبعاد (Dimensionality Reduction): تحويل البيانات عالية الأبعاد إلى فضاء أقل أبعاداً
  • التصور البصري (Visualization): تسهيل تفسير مجموعات البيانات المعقدة

البنية (Architecture)

تتكون الخريطة ذاتية التنظيم من طبقتين رئيسيتين:

  1. طبقة الإدخال (Input Layer): تمثل خصائص (features) البيانات
  2. طبقة الإخراج (Output Layer): مرتبة في شبكة ثنائية الأبعاد (2D grid) من الخلايا العصبية (neurons)، بحيث تمثل كل خلية عصبية عنقوداً (cluster) من البيانات

2. كيف تعمل الخرائط ذاتية التنظيم

تتبع خوارزمية SOM الخطوات الأساسية التالية:

الخطوة 1: التهيئة (Initialization)

  • تُهيَّأ أوزان (weights) خلايا الإخراج العصبية بشكل عشوائي
  • تمثل هذه الأوزان خصائص كل خلية عصبية
  • سيتم تعديل الأوزان أثناء التدريب

الخطوة 2: التنافس (Competition)

  • لكل متجه إدخال (input vector)، تحسب SOM المسافة الإقليدية (Euclidean distance) بين الإدخال ومتجهات الأوزان لكل الخلايا العصبية
  • الخلية العصبية ذات أقل مسافة هي الخلية الفائزة (winning neuron) (وحدة أفضل تطابق - Best Matching Unit - BMU)

الصيغة الرياضية: D(j)=i=1n(wijxi)2D(j) = \sum_{i=1}^{n} (w_{ij} - x_i)^2

حيث:

  • D(j)D(j) هي المسافة الخاصة بالخلية العصبية jj
  • nn هو عدد الخصائص (features)
  • wijw_{ij} هو وزن الخلية العصبية jj للخاصية ii
  • xix_i هي خاصية الإدخال ii

الخطوة 3: تحديث الأوزان (Weight Update)

  • تُحدَّث أوزان الخلية العصبية الفائزة لتقترب أكثر من متجه الإدخال
  • تُعدَّل أيضاً الخلايا العصبية المجاورة (بتغييرات أصغر)

الصيغة الرياضية: wij(new)=wij(old)+α(xiwij(old))w_{ij}^{(new)} = w_{ij}^{(old)} + \alpha \cdot (x_i - w_{ij}^{(old)})

حيث:

  • α\alpha هو معدل التعلم (learning rate)
  • xix_i هي خاصية الإدخال

الخطوة 4: تناقص معدل التعلم (Learning Rate Decay)

  • يتناقص معدل التعلم α\alpha مع مرور الوقت
  • هذا يسمح للخريطة بالتقارب (converge) نحو قيم مستقرة

الصيغة الرياضية: α(t+1)=0.5α(t)\alpha(t+1) = 0.5 \cdot \alpha(t)

الخطوة 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) يحتوي على دالتين رئيسيتين:

  1. winner(): تجد الخلية العصبية الفائزة (العنقود) عبر حساب المسافات الإقليدية
  2. 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

خطوات عملية التدريب:

  1. التكرار عبر كل حقبة (epoch)
  2. لكل عينة تدريب:
    • إيجاد الخلية العصبية الفائزة
    • تحديث أوزان الخلية العصبية الفائزة
  3. تخفيض معدل التعلم
  4. تخزين الأوزان لأغراض التصور البصري
# 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 تجميع البيانات من خلال رسمين بيانيين:

  1. تقارب الأوزان (Weight Convergence): يوضح كيف يتطور كل مكوّن من مكونات الأوزان أثناء التدريب
  2. العناقيد النهائية (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. التحليل والتفسير

فهم النتائج

  1. تقارب الأوزان (Weight Convergence):

    • لاحظ كيف تستقر الأوزان مع مرور الحقب (epochs)
    • تناقص معدل التعلم يساعد على تقارب أكثر انسيابية
    • قد تتقارب الخصائص المختلفة بمعدلات مختلفة
  2. تشكّل العناقيد (Cluster Formation):

    • تعلمت SOM تمثيل البيانات بمركزَي عنقود (cluster centers)
    • تمثل أوزان كل عنقود نمطاً نموذجياً (prototypical pattern) في البيانات
    • تُوضَّع عينات الإدخال المتشابهة في نفس العنقود
  3. تصنيف عينة الاختبار:

    • تُصنَّف عينة الاختبار [0, 0, 0, 1] بناءً على أقل مسافة إقليدية
    • هذا يُظهر قدرة SOM على التعميم (generalize) على بيانات جديدة

6. أسئلة تدريبية

جرّب هذه التمارين لترسيخ فهمك:

  1. تعديل تناقص معدل التعلم: غيّر صيغة التناقص من alpha *= 0.9 إلى alpha *= 0.5. كيف يؤثر ذلك على التقارب؟

  2. إضافة المزيد من العناقيد: وسّع صنف SOM ليدعم 3 أو 4 عناقيد بدلاً من عنقودين.

  3. تغيير الأوزان الابتدائية: جرّب تهيئات عشوائية مختلفة. هل ما زالت SOM تتقارب نحو عناقيد مشابهة؟

  4. إضافة عينات تدريب جديدة: أضف عينات مثل [1, 1, 1, 1] أو [0, 0, 0, 0]. كيف يغيّر ذلك تشكّل العناقيد؟

  5. تنفيذ دالة الجوار (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