Unsupervised Learning - การจัดกลุ่มข้อมูล (Clustering)

1. บทนำสู่ Unsupervised Learning และ Clustering

1.1 ความหมายของ Unsupervised Learning

Unsupervised Learning (การเรียนรู้แบบไม่มีผู้สอน) คือกระบวนการที่อัลกอริทึมเรียนรู้จากข้อมูลที่ไม่มีป้ายกำกับ (unlabeled data) โดยไม่มีคำตอบที่ถูกต้องกำหนดไว้ล่วงหน้า เป้าหมายคือการค้นหาโครงสร้าง รูปแบบ หรือความสัมพันธ์ที่ซ่อนอยู่ในข้อมูลเอง

ความแตกต่างระหว่าง Supervised และ Unsupervised Learning:

คุณสมบัติ Supervised Learning Unsupervised Learning
ข้อมูล Training มี Label กำกับ ไม่มี Label
เป้าหมาย ทำนาย Output ค้นหาโครงสร้างในข้อมูล
การประเมินผล เปรียบเทียบกับ Ground Truth ใช้ Internal Metrics
ตัวอย่าง Classification, Regression Clustering, Dimensionality Reduction
ความยากในการเก็บข้อมูล สูง (ต้อง Label) ต่ำ (ไม่ต้อง Label)

1.2 ความหมายของ Clustering

Clustering (การจัดกลุ่ม) คือกระบวนการแบ่งกลุ่มข้อมูลออกเป็น Cluster โดยข้อมูลภายในกลุ่มเดียวกันควรมีความคล้ายคลึงกัน (high intra-cluster similarity) ขณะที่ข้อมูลต่างกลุ่มควรมีความแตกต่างกัน (low inter-cluster similarity)

คุณสมบัติที่ดีของ Clustering:

1.3 ประเภทของ Clustering Algorithms

flowchart TD
    A["🔵 Clustering Algorithms - อัลกอริทึมการจัดกลุ่ม"]:::main --> B["Partitional - การแบ่งพาร์ทิชัน"]:::partition
    A --> C["Hierarchical - แบบลำดับชั้น"]:::hier
    A --> D["Density-Based - แบบความหนาแน่น"]:::density
    A --> E["Model-Based - แบบโมเดล"]:::model

    B --> B1["K-Means"]:::algo
    B --> B2["K-Medoids"]:::algo

    C --> C1["Agglomerative - แบบรวมจากล่างขึ้นบน"]:::algo
    C --> C2["Divisive - แบบแบ่งจากบนลงล่าง"]:::algo

    D --> D1["DBSCAN"]:::algo
    D --> D2["OPTICS"]:::algo

    E --> E1["Hidden Markov - Model (HMM)"]:::algo

    classDef main fill:#282828,stroke:#fabd2f,color:#ebdbb2,font-weight:bold
    classDef partition fill:#458588,stroke:#83a598,color:#ebdbb2
    classDef hier fill:#98971a,stroke:#b8bb26,color:#ebdbb2
    classDef density fill:#d79921,stroke:#fabd2f,color:#282828
    classDef model fill:#cc241d,stroke:#fb4934,color:#ebdbb2
    classDef algo fill:#3c3836,stroke:#928374,color:#ebdbb2

1.4 ประวัติความเป็นมาของ Clustering

flowchart LR
    subgraph era1["ยุคที่ 1: รากฐาน (1950s-1960s)"]
        A1["1957: Ward's Method - Hierarchical Clustering"]
        A2["1967: K-Means - MacQueen"]
    end

    subgraph era2["ยุคที่ 2: พัฒนาการ (1970s-1980s)"]
        B1["1973: ISODATA - Dunn's Index"]
        B2["1984: Fuzzy C-Means - Bezdek"]
    end

    subgraph era3["ยุคที่ 3: ยุคทันสมัย (1990s-2000s)"]
        C1["1996: DBSCAN - Ester et al."]
        C3["2002: Spectral Clustering"]
    end

    subgraph era4["ยุคที่ 4: Deep Learning Era (2010s-ปัจจุบัน)"]
        D1["2010: Deep Clustering"]
        D2["2020s: Self-Supervised - Clustering"]
    end

    era1 --> era2 --> era3 --> era4

    style era1 fill:#282828,stroke:#fabd2f,color:#ebdbb2
    style era2 fill:#3c3836,stroke:#83a598,color:#ebdbb2
    style era3 fill:#282828,stroke:#b8bb26,color:#ebdbb2
    style era4 fill:#3c3836,stroke:#fb4934,color:#ebdbb2

2. K-Means Clustering

2.1 แนวคิดหลักของ K-Means

K-Means Clustering คืออัลกอริทึมการจัดกลุ่มแบบ Partitional ที่แบ่งข้อมูล N จุด ออกเป็น K กลุ่ม โดยแต่ละกลุ่มมี Centroid (จุดศูนย์กลาง) และข้อมูลแต่ละจุดจะถูกกำหนดให้อยู่ใน Cluster ที่มี Centroid ใกล้ที่สุด

Objective Function ของ K-Means (Within-Cluster Sum of Squares - WCSS):

J = k=1 K xiCk xi - μk 2

โดยที่:

2.2 ขั้นตอนอัลกอริทึม K-Means

flowchart TD
    S["🟡 เริ่มต้น (Start) - กำหนดจำนวน K"]:::start
    S --> I["สุ่มเลือก K Centroids - Initialize K centroids randomly"]:::step
    I --> A["กำหนด Cluster - (Assignment Step) - แต่ละจุดข้อมูล → Centroid ที่ใกล้ที่สุด"]:::step
    A --> U["อัปเดต Centroid - (Update Step) - คำนวณค่าเฉลี่ยใหม่ของแต่ละ Cluster"]:::step
    U --> C{"Centroids - เปลี่ยนแปลงหรือไม่?"}:::decision
    C -->|"ใช่ (Yes)"| A
    C -->|"ไม่ (No) - Converged"| E["🟢 สิ้นสุด (End) - ผลลัพธ์: K Clusters"]:::end

    classDef start fill:#d79921,stroke:#fabd2f,color:#282828,font-weight:bold
    classDef step fill:#458588,stroke:#83a598,color:#ebdbb2
    classDef decision fill:#98971a,stroke:#b8bb26,color:#ebdbb2
    classDef end fill:#689d6a,stroke:#8ec07c,color:#282828,font-weight:bold

2.3 ตัวอย่างการคำนวณ K-Means แบบ Step-by-Step

ข้อมูลตัวอย่าง

สมมติเรามีข้อมูล 6 จุด ในพื้นที่ 2 มิติ (x, y):

จุดข้อมูล x y
A 1 1
B 1.5 2
C 3 4
D 5 7
E 3.5 5
F 4.5 5

กำหนด K = 2


Iteration 1:

สุ่มเลือก Centroid เริ่มต้น:

Assignment Step — คำนวณระยะทางจากทุกจุดไปยัง Centroid แต่ละตัว:

ระยะทางยูคลิด: d(x, μ) = √[(x₁-μ₁)² + (x₂-μ₂)²]

จุด d(จุด, μ₁) d(จุด, μ₂) Cluster
A(1,1) √[(1-1)²+(1-1)²] = 0.00 √[(1-5)²+(1-7)²] = 7.21 C₁
B(1.5,2) √[(1.5-1)²+(2-1)²] = 1.12 √[(1.5-5)²+(2-7)²] = 6.10 C₁
C(3,4) √[(3-1)²+(4-1)²] = 3.61 √[(3-5)²+(4-7)²] = 3.61 C₁*
D(5,7) √[(5-1)²+(7-1)²] = 7.21 √[(5-5)²+(7-7)²] = 0.00 C₂
E(3.5,5) √[(3.5-1)²+(5-1)²] = 4.72 √[(3.5-5)²+(5-7)²] = 2.50 C₂
F(4.5,5) √[(4.5-1)²+(5-1)²] = 5.32 √[(4.5-5)²+(5-7)²] = 2.06 C₂

*หมายเหตุ: กรณีระยะทางเท่ากัน กำหนด C ไปยัง C₁ (สามารถเลือกได้ตามกฎที่กำหนด)

Update Step — คำนวณ Centroid ใหม่:

μk = 1 |Ck| xiCk xi

Iteration 2: ทำ Assignment ใหม่ด้วย Centroid ที่อัปเดต — เมื่อ Assignment ไม่เปลี่ยน → Converged!

2.4 ตัวอย่าง Code Python (K-Means ด้วย scikit-learn)

"""
K-Means Clustering ด้วย scikit-learn
=====================================
สาธิตการจัดกลุ่มข้อมูลด้วย K-Means Algorithm
รวมถึงการหาจำนวน K ที่เหมาะสมด้วย Elbow Method
"""

import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_blobs
import warnings
warnings.filterwarnings('ignore')

# ─── 1. สร้างข้อมูลตัวอย่าง ─────────────────────────────────────────────
np.random.seed(42)
X, y_true = make_blobs(
    n_samples=300,
    centers=4,           # จำนวนกลุ่มที่แท้จริง
    cluster_std=0.8,
    random_state=42
)
print(f"ขนาดข้อมูล: {X.shape}")  # (300, 2)

# ─── 2. ปรับขนาดข้อมูล (Feature Scaling) ────────────────────────────────
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# ─── 3. Elbow Method: หาจำนวน K ที่เหมาะสม ──────────────────────────────
inertia_values = []  # WCSS สำหรับแต่ละค่า K
K_range = range(1, 11)

for k in K_range:
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    kmeans.fit(X_scaled)
    inertia_values.append(kmeans.inertia_)
    print(f"K={k:2d} | WCSS (Inertia) = {kmeans.inertia_:.2f}")

# ─── 4. สร้างโมเดล K-Means ด้วย K ที่เลือก ──────────────────────────────
best_k = 4
kmeans = KMeans(
    n_clusters=best_k,
    init='k-means++',   # ใช้ K-Means++ แทนการสุ่มธรรมดา
    n_init=10,          # ทดลอง 10 รอบ เลือก WCSS ต่ำสุด
    max_iter=300,       # จำนวน Iteration สูงสุด
    random_state=42
)
labels = kmeans.fit_predict(X_scaled)

# ─── 5. ดูผลลัพธ์ ─────────────────────────────────────────────────────────
print(f" - จำนวน Cluster: {best_k}")
print(f"Centroids: - {kmeans.cluster_centers_}")
print(f"WCSS สุดท้าย (Inertia): {kmeans.inertia_:.4f}")
print(f"จำนวน Iteration ที่ใช้: {kmeans.n_iter_}")

# นับสมาชิกในแต่ละกลุ่ม
for i in range(best_k):
    count = np.sum(labels == i)
    print(f"Cluster {i}: {count} จุดข้อมูล")

# ─── 6. Visualization ────────────────────────────────────────────────────
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# กราฟ 1: ข้อมูลดิบ
axes[0].scatter(X_scaled[:, 0], X_scaled[:, 1], c='gray', alpha=0.5, s=30)
axes[0].set_title('ข้อมูลดิบ (Raw Data)', fontsize=12)
axes[0].set_xlabel('Feature 1')
axes[0].set_ylabel('Feature 2')

# กราฟ 2: Elbow Method
axes[1].plot(K_range, inertia_values, 'bo-', linewidth=2, markersize=8)
axes[1].axvline(x=best_k, color='red', linestyle='--', label=f'K={best_k} (เลือก)')
axes[1].set_title('Elbow Method', fontsize=12)
axes[1].set_xlabel('จำนวน Cluster (K)')
axes[1].set_ylabel('WCSS (Inertia)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

# กราฟ 3: ผลการ Clustering
colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24']
for i in range(best_k):
    mask = labels == i
    axes[2].scatter(X_scaled[mask, 0], X_scaled[mask, 1],
                   c=colors[i], label=f'Cluster {i}', alpha=0.7, s=30)
# วาด Centroid
axes[2].scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
               c='black', marker='*', s=300, zorder=5, label='Centroids')
axes[2].set_title(f'K-Means Clustering (K={best_k})', fontsize=12)
axes[2].set_xlabel('Feature 1')
axes[2].set_ylabel('Feature 2')
axes[2].legend()

plt.tight_layout()
plt.savefig('kmeans_result.png', dpi=150, bbox_inches='tight')
plt.show()

2.5 K-Means++ การปรับปรุงการเลือก Centroid เริ่มต้น

ปัญหาของ K-Means ธรรมดา: การสุ่ม Centroid เริ่มต้นแบบ Random อาจนำไปสู่ผลลัพธ์ที่ไม่ดี

K-Means++ แก้ปัญหาโดย:

  1. เลือก Centroid แรกแบบสุ่ม
  2. Centroid ถัดไปถูกเลือกด้วยความน่าจะเป็นที่แปรผันตรงกับ ระยะทาง จาก Centroid ที่มีอยู่
P ( xi ) = D (xi)2 xjX D (xj)2

โดยที่ D(x) คือระยะทางของจุด x ไปยัง Centroid ที่ใกล้ที่สุดที่เลือกไว้แล้ว

2.6 ข้อดี ข้อเสีย และข้อจำกัดของ K-Means

ด้าน รายละเอียด
✅ ข้อดี ง่าย รวดเร็ว Scalable สำหรับข้อมูลขนาดใหญ่
✅ ข้อดี Convergence รับประกัน (ไม่จำเป็นต้อง Global Optimum)
❌ ข้อเสีย ต้องกำหนดจำนวน K ล่วงหน้า
❌ ข้อเสีย ไวต่อ Outlier และ Centroid เริ่มต้น
❌ ข้อเสีย สมมุติว่า Cluster มีรูปร่างเป็น Spherical (กลม)
❌ ข้อเสีย ไม่เหมาะกับ Cluster ที่มีขนาด/รูปร่างแตกต่างกัน

3. Hierarchical Clustering

3.1 แนวคิดหลักของ Hierarchical Clustering

Hierarchical Clustering (การจัดกลุ่มแบบลำดับชั้น) สร้างโครงสร้างลำดับชั้นของ Cluster ที่เรียกว่า Dendrogram มี 2 แนวทางหลัก:

3.2 Linkage Methods (วิธีวัดระยะทางระหว่าง Cluster)

flowchart TD
    L["Linkage Methods - วิธีวัดระยะห่างระหว่าง Cluster"]:::main
    L --> SL["Single Linkage - (Minimum) - ระยะห่างจุดใกล้สุด"]:::link
    L --> CL["Complete Linkage - (Maximum) - ระยะห่างจุดไกลสุด"]:::link
    L --> AL["Average Linkage - (UPGMA) - ค่าเฉลี่ยระยะห่างทุกคู่"]:::link
    L --> WL["Ward's Linkage - ลดค่า WCSS รวม"]:::link

    SL --> SD["⚠️ Chain Effect - สร้าง Cluster แบบโซ่"]:::note
    CL --> CD["✅ Compact Clusters - ทนต่อ Outlier ดีกว่า"]:::note
    AL --> AD["✅ สมดุลระหว่าง - Single และ Complete"]:::note
    WL --> WD["⭐ นิยมใช้มากที่สุด - เหมาะกับ Euclidean Space"]:::note

    classDef main fill:#282828,stroke:#fabd2f,color:#ebdbb2,font-weight:bold
    classDef link fill:#458588,stroke:#83a598,color:#ebdbb2
    classDef note fill:#3c3836,stroke:#928374,color:#a89984,font-size:11px

สูตรวัดระยะห่างระหว่าง Cluster A และ B:

Single Linkage: d (A,B) = min aiA,bjB d(ai,bj)

Complete Linkage: d (A,B) = max aiA,bjB d(ai,bj)

Ward's Linkage (ผลต่างของ WCSS หลังและก่อนรวม): d (A,B) = |A||B| |A|+|B| μA-μB 2

3.3 ตัวอย่างการคำนวณ Agglomerative Clustering

ข้อมูลตัวอย่าง (5 จุด)

จุด x y
P1 0 0
P2 1 0
P3 0 1
P4 5 5
P5 6 5

Distance Matrix เริ่มต้น (Euclidean Distance):

P1 P2 P3 P4 P5
P1 0 1.00 1.00 7.07 7.81
P2 1.00 0 1.41 5.66 6.40
P3 1.00 1.41 0 6.40 7.07
P4 7.07 5.66 6.40 0 1.00
P5 7.81 6.40 7.07 1.00 0

ขั้นตอน Agglomerative (Single Linkage):

  1. Step 1: ระยะทางน้อยสุด = d(P1,P2) = d(P1,P3) = d(P4,P5) = 1.00
    → รวม P1 กับ P2 ก่อน → Cluster {P1,P2}
  2. Step 2: อัปเดต Distance Matrix → รวม {P1,P2} กับ P3 → Cluster {P1,P2,P3}
  3. Step 3: รวม P4 กับ P5 → Cluster {P4,P5}
  4. Step 4: รวม {P1,P2,P3} กับ {P4,P5} → Cluster เดียวใหญ่

3.4 ตัวอย่าง Code Python (Hierarchical Clustering)

"""
Hierarchical Clustering ด้วย scikit-learn และ scipy
=====================================================
สาธิตการสร้าง Dendrogram และการตัด Cluster ที่จำนวนต่าง ๆ
"""

import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import AgglomerativeClustering
from sklearn.datasets import make_blobs
from sklearn.preprocessing import StandardScaler
from scipy.cluster.hierarchy import dendrogram, linkage
from scipy.spatial.distance import pdist

# ─── 1. สร้างข้อมูลตัวอย่าง ─────────────────────────────────────────────
np.random.seed(42)
X, _ = make_blobs(n_samples=50, centers=3, cluster_std=1.0, random_state=42)
X_scaled = StandardScaler().fit_transform(X)

# ─── 2. คำนวณ Linkage Matrix สำหรับ Dendrogram ──────────────────────────
# ลอง 4 แบบ: single, complete, average, ward
linkage_methods = ['single', 'complete', 'average', 'ward']

fig, axes = plt.subplots(2, 2, figsize=(14, 10))
axes = axes.flatten()

for idx, method in enumerate(linkage_methods):
    # คำนวณ linkage matrix
    Z = linkage(X_scaled, method=method)

    # วาด Dendrogram
    dendrogram(
        Z,
        ax=axes[idx],
        truncate_mode='lastp',   # แสดงเฉพาะ 12 Cluster สุดท้าย
        p=12,
        color_threshold=0.7 * max(Z[:, 2]),
        above_threshold_color='gray'
    )
    axes[idx].set_title(f'Dendrogram: {method.capitalize()} Linkage', fontsize=12)
    axes[idx].set_xlabel('จุดข้อมูล (Data Points)')
    axes[idx].set_ylabel('ระยะทาง (Distance)')
    axes[idx].axhline(y=2.5, color='red', linestyle='--',
                      alpha=0.7, label='เส้นตัด (Cut)')
    axes[idx].legend()

plt.suptitle('เปรียบเทียบ Linkage Methods', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.savefig('hierarchical_dendrograms.png', dpi=150)
plt.show()

# ─── 3. สร้างโมเดล Agglomerative Clustering ──────────────────────────────
model = AgglomerativeClustering(
    n_clusters=3,
    linkage='ward',        # ใช้ Ward's Linkage
    metric='euclidean'     # วัดระยะทางด้วย Euclidean
)
labels = model.fit_predict(X_scaled)

# ─── 4. แสดงผลลัพธ์ ───────────────────────────────────────────────────────
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# กราฟ Clustering Result
colors = ['#ff6b6b', '#4ecdc4', '#45b7d1']
for i in range(3):
    mask = labels == i
    axes[0].scatter(X_scaled[mask, 0], X_scaled[mask, 1],
                   c=colors[i], label=f'Cluster {i}', alpha=0.7, s=50)
axes[0].set_title('Agglomerative Clustering (Ward, K=3)')
axes[0].legend()

# กราฟแสดงจำนวนสมาชิกแต่ละ Cluster
unique, counts = np.unique(labels, return_counts=True)
axes[1].bar([f'Cluster {i}' for i in unique], counts,
           color=colors[:len(unique)], edgecolor='black')
axes[1].set_title('จำนวนสมาชิกในแต่ละ Cluster')
axes[1].set_ylabel('จำนวนจุดข้อมูล')

for i, count in enumerate(counts):
    axes[1].text(i, count + 0.3, str(count), ha='center', fontweight='bold')

plt.tight_layout()
plt.savefig('hierarchical_result.png', dpi=150)
plt.show()

print(f"การแจกแจงสมาชิก: {dict(zip(unique, counts))}")

3.5 เปรียบเทียบ Linkage Methods

Linkage ลักษณะ Cluster ข้อดี ข้อเสีย
Single ยาว แคบ (Chaining) เร็ว ตรวจ Non-convex ได้ Chain Effect
Complete กลม กะทัดรัด ทนต่อ Outlier Sensitive to Outliers
Average กลาง ๆ สมดุล คำนวณช้ากว่า
Ward กลม ขนาดใกล้เคียงกัน ผลดีที่สุดทั่วไป เฉพาะ Euclidean

4. DBSCAN

4.1 แนวคิดหลักของ DBSCAN

DBSCAN (Density-Based Spatial Clustering of Applications with Noise) เป็น Clustering Algorithm แบบ Density-Based ที่ค้นหา Cluster โดยใช้แนวคิดว่า Cluster คือพื้นที่ที่มีความหนาแน่นของจุดข้อมูลสูง แยกจากกันด้วยพื้นที่ที่มีความหนาแน่นต่ำ

ข้อได้เปรียบสำคัญ:

4.2 พารามิเตอร์และนิยามสำคัญของ DBSCAN

พารามิเตอร์:

ประเภทของจุดข้อมูล:

flowchart LR
    subgraph classification["การจำแนกประเภทจุดข้อมูลใน DBSCAN"]
        direction TB
        CP["🔴 Core Point - (จุดแกนกลาง) - |N_ε(x)| ≥ MinPts"]
        BP["🟡 Border Point - (จุดขอบ) - อยู่ใน N_ε ของ Core Point - แต่ตัวเองไม่ใช่ Core"]
        NP["⚫ Noise Point - (จุดรบกวน/Outlier) - ไม่ใช่ทั้ง Core และ Border"]
    end

    CP -->|"Directly Reachable"| BP
    CP -->|"Density-Connected ผ่าน Core Points"| CP
    NP -->|"ไม่อยู่ใน Cluster ใด ๆ"| NP

    style classification fill:#282828,stroke:#fabd2f,color:#ebdbb2
    style CP fill:#cc241d,stroke:#fb4934,color:#ebdbb2
    style BP fill:#d79921,stroke:#fabd2f,color:#282828
    style NP fill:#3c3836,stroke:#928374,color:#a89984

นิยาม: จุด q สามารถ Density-Reachable จาก p ถ้า:

4.3 ขั้นตอนอัลกอริทึม DBSCAN

  1. สำหรับทุกจุด x ที่ยังไม่ถูก Visit:
  2. ขยาย Cluster โดยเพิ่มจุดที่ Density-Reachable ทั้งหมด
  3. ทำซ้ำจนครบทุกจุด

4.4 ตัวอย่างการคำนวณ DBSCAN ด้วยมือ

ข้อมูลตัวอย่าง: กำหนด ε = 1.5, MinPts = 3

จุด x y
A 1 1
B 1.5 1.5
C 2 1
D 8 8
E 1.2 0.8
F 9 8

คำนวณ N_ε สำหรับแต่ละจุด (ε = 1.5):

ผลลัพธ์:

4.5 ตัวอย่าง Code Python (DBSCAN ด้วย scikit-learn)

"""
DBSCAN Clustering ด้วย scikit-learn
=====================================
สาธิตความสามารถของ DBSCAN ในการตรวจจับ Cluster รูปร่างซับซ้อน
และการตรวจจับ Outlier
พร้อมการหาพารามิเตอร์ ε ที่เหมาะสมด้วย k-distance Graph
"""

import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import DBSCAN, KMeans
from sklearn.datasets import make_moons, make_circles, make_blobs
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import NearestNeighbors

# ─── 1. สร้างข้อมูลรูปร่างต่าง ๆ ─────────────────────────────────────────
np.random.seed(42)

datasets = {
    'Moons (พระจันทร์)': make_moons(n_samples=200, noise=0.1, random_state=42),
    'Circles (วงกลม)': make_circles(n_samples=200, noise=0.1, factor=0.5, random_state=42),
    'Blobs + Noise (กลุ่มทั่วไป)': (
        np.vstack([
            make_blobs(n_samples=150, centers=3, cluster_std=0.5, random_state=42)[0],
            np.random.uniform(-5, 5, size=(30, 2))  # Noise สุ่ม
        ]),
        None
    )
}

fig, axes = plt.subplots(3, 3, figsize=(15, 12))

dbscan_params = [
    {'eps': 0.3, 'min_samples': 5},    # สำหรับ Moons
    {'eps': 0.3, 'min_samples': 5},    # สำหรับ Circles
    {'eps': 0.8, 'min_samples': 5},    # สำหรับ Blobs
]

kmeans_k = [2, 2, 3]

for row, (name, (X, _)) in enumerate(datasets.items()):
    X_scaled = StandardScaler().fit_transform(X)

    # ─ ข้อมูลดิบ ─
    axes[row, 0].scatter(X_scaled[:, 0], X_scaled[:, 1], c='gray', s=15, alpha=0.7)
    axes[row, 0].set_title(f'{name} - (ข้อมูลดิบ)')

    # ─ K-Means ─
    km = KMeans(n_clusters=kmeans_k[row], random_state=42, n_init=10)
    km_labels = km.fit_predict(X_scaled)
    colors_km = ['#ff6b6b', '#4ecdc4', '#45b7d1']
    for i in np.unique(km_labels):
        mask = km_labels == i
        axes[row, 1].scatter(X_scaled[mask, 0], X_scaled[mask, 1],
                            c=colors_km[i % 3], s=15, alpha=0.7)
    axes[row, 1].set_title(f'K-Means (K={kmeans_k[row]})')

    # ─ DBSCAN ─
    params = dbscan_params[row]
    db = DBSCAN(eps=params['eps'], min_samples=params['min_samples'])
    db_labels = db.fit_predict(X_scaled)

    n_clusters = len(set(db_labels)) - (1 if -1 in db_labels else 0)
    n_noise = np.sum(db_labels == -1)

    # วาดผลลัพธ์ DBSCAN
    unique_labels = set(db_labels)
    colors = plt.cm.Set1(np.linspace(0, 1, len(unique_labels)))

    for label, color in zip(unique_labels, colors):
        if label == -1:
            # Noise Points = สีดำ, รูป X
            mask = db_labels == -1
            axes[row, 2].scatter(X_scaled[mask, 0], X_scaled[mask, 1],
                                c='black', marker='x', s=50,
                                label='Noise/Outlier', zorder=5)
        else:
            mask = db_labels == label
            axes[row, 2].scatter(X_scaled[mask, 0], X_scaled[mask, 1],
                                c=[color], s=15, alpha=0.7,
                                label=f'Cluster {label}')

    axes[row, 2].set_title(
        f'DBSCAN (ε={params["eps"]}, MinPts={params["min_samples"]}) - '
        f'{n_clusters} clusters, {n_noise} noise points'
    )
    axes[row, 2].legend(fontsize=7)

axes[0, 0].set_ylabel('Moon Shape', fontsize=10)
axes[1, 0].set_ylabel('Circle Shape', fontsize=10)
axes[2, 0].set_ylabel('Blob + Noise', fontsize=10)

plt.suptitle('เปรียบเทียบ K-Means vs DBSCAN บนข้อมูลรูปร่างต่าง ๆ',
             fontsize=14, fontweight='bold')
plt.tight_layout()
plt.savefig('dbscan_comparison.png', dpi=150)
plt.show()

# ─── 2. วิธีหาค่า ε ที่เหมาะสม (k-distance Graph) ───────────────────────
print(" - === การหาค่า ε ด้วย k-distance Graph ===")
X_demo, _ = make_moons(n_samples=200, noise=0.1, random_state=42)
X_demo = StandardScaler().fit_transform(X_demo)

# คำนวณระยะทางไปยัง k-th Nearest Neighbor
k = 5  # = MinPts
nbrs = NearestNeighbors(n_neighbors=k).fit(X_demo)
distances, _ = nbrs.kneighbors(X_demo)

# เรียงลำดับ k-th distance
k_distances = distances[:, -1]
k_distances_sorted = np.sort(k_distances)[::-1]

plt.figure(figsize=(8, 5))
plt.plot(k_distances_sorted, 'b-', linewidth=2)
plt.axhline(y=0.3, color='red', linestyle='--', label='ε = 0.3 (จุดหัก Elbow)')
plt.xlabel('จุดข้อมูล (เรียงตามระยะทาง)')
plt.ylabel(f'{k}-th Nearest Neighbor Distance')
plt.title(f'k-Distance Graph (k={k}) - ค้นหาค่า ε ที่เหมาะสม')
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig('dbscan_epsilon.png', dpi=150)
plt.show()
print("ดูจุดหักงอ (Elbow) ของกราฟ → ค่า ε ที่เหมาะสม")

4.6 การเลือกพารามิเตอร์ DBSCAN

วิธีหาค่า ε:

วิธีเลือก MinPts:


5. การประเมินผลการจัดกลุ่ม

5.1 ประเภทของ Evaluation Metrics

flowchart TD
    EM["Clustering Evaluation Metrics - การวัดประสิทธิภาพการจัดกลุ่ม"]:::main

    EM --> INT["Internal Metrics - (ไม่ต้องการ Ground Truth) - ประเมินจากโครงสร้างภายใน"]:::cat
    EM --> EXT["External Metrics - (ต้องการ Ground Truth) - เปรียบเทียบกับ Labels จริง"]:::cat
    EM --> REL["Relative Metrics - เปรียบเทียบผลต่าง ๆ - ของอัลกอริทึมเดียวกัน"]:::cat

    INT --> SC["Silhouette Score - [-1, 1] → ยิ่งสูงยิ่งดี"]:::metric
    INT --> DBI["Davies-Bouldin Index - ≥ 0 → ยิ่งต่ำยิ่งดี"]:::metric
    INT --> CHI["Calinski-Harabasz Index - ≥ 0 → ยิ่งสูงยิ่งดี"]:::metric

    EXT --> ARI["Adjusted Rand Index (ARI) - [-1, 1] → ยิ่งใกล้ 1 ยิ่งดี"]:::metric
    EXT --> NMI["Normalized Mutual Info - [0, 1] → ยิ่งสูงยิ่งดี"]:::metric
    EXT --> HO["Homogeneity & - Completeness"]:::metric

    REL --> GAP["Gap Statistic"]:::metric
    REL --> ELB["Elbow Method - (WCSS)"]:::metric

    classDef main fill:#282828,stroke:#fabd2f,color:#ebdbb2,font-weight:bold
    classDef cat fill:#458588,stroke:#83a598,color:#ebdbb2
    classDef metric fill:#3c3836,stroke:#928374,color:#ebdbb2

5.2 Silhouette Score

Silhouette Score วัดว่าข้อมูลแต่ละจุดถูกจัดอยู่ใน Cluster ที่เหมาะสมแค่ไหน

s (i) = b(i)-a(i) max{a(i),b(i)}

โดยที่:

การแปลผล:

ตัวอย่างการคำนวณ Silhouette Score ด้วยมือ

ข้อมูล (3 จุด, 2 Cluster):

คำนวณสำหรับ P1:

5.3 Davies-Bouldin Index (DBI)

Davies-Bouldin Index วัดอัตราส่วนระหว่าง Scatter ภายใน Cluster กับ Separation ระหว่าง Cluster

DB = 1K i=1 K max ji si+sj d(ci,cj)

โดยที่:

DBI ต่ำ = Clustering ดี (Cluster กะทัดรัด ห่างกัน)

5.4 ตัวอย่าง Code Python (Evaluation Metrics)

"""
การประเมินผล Clustering ด้วย Metrics ต่าง ๆ
============================================
เปรียบเทียบ K-Means, Hierarchical, DBSCAN
ด้วย Internal และ External Metrics
"""

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans, AgglomerativeClustering, DBSCAN
from sklearn.metrics import (
    silhouette_score,
    davies_bouldin_score,
    calinski_harabasz_score,
    adjusted_rand_score,
    normalized_mutual_info_score
)
from sklearn.datasets import make_blobs
from sklearn.preprocessing import StandardScaler

# ─── 1. เตรียมข้อมูล ──────────────────────────────────────────────────────
np.random.seed(42)
X, y_true = make_blobs(n_samples=300, centers=4,
                       cluster_std=0.8, random_state=42)
X_scaled = StandardScaler().fit_transform(X)
true_clusters = 4

print(f"ข้อมูล: {X.shape}, True Clusters: {true_clusters}")

# ─── 2. รัน Clustering Algorithms ───────────────────────────────────────
algorithms = {
    'K-Means': KMeans(n_clusters=true_clusters, random_state=42, n_init=10),
    'Agglomerative - (Ward)': AgglomerativeClustering(n_clusters=true_clusters,
                                                     linkage='ward'),
    'DBSCAN': DBSCAN(eps=0.5, min_samples=5)
}

results = {}
for name, algo in algorithms.items():
    labels = algo.fit_predict(X_scaled)

    # กรองกรณี DBSCAN ที่มี Noise Points (-1)
    mask = labels != -1
    n_clusters = len(set(labels[mask]))

    if n_clusters < 2:
        print(f"⚠️  {name}: พบน้อยกว่า 2 Cluster — ข้ามการคำนวณ Metrics")
        continue

    results[name] = {
        'labels': labels,
        'n_clusters': n_clusters,
        'n_noise': np.sum(labels == -1),
        # Internal Metrics
        'Silhouette': silhouette_score(X_scaled[mask], labels[mask]),
        'Davies-Bouldin': davies_bouldin_score(X_scaled[mask], labels[mask]),
        'Calinski-Harabasz': calinski_harabasz_score(X_scaled[mask], labels[mask]),
        # External Metrics (ถ้ามี Ground Truth)
        'ARI': adjusted_rand_score(y_true[mask], labels[mask]),
        'NMI': normalized_mutual_info_score(y_true[mask], labels[mask])
    }

# ─── 3. สรุปผล ────────────────────────────────────────────────────────────
print(" - " + "="*75)
print(f"{'Algorithm':<22} | {'Clusters':>8} | {'Silhouette':>10} | "
      f"{'DB Index':>10} | {'CH Index':>10} | {'ARI':>6} | {'NMI':>6}")
print("="*75)

for name, r in results.items():
    alg_name = name.replace(' - ', ' ')
    print(f"{alg_name:<22} | {r['n_clusters']:>8} | "
          f"{r['Silhouette']:>10.4f} | "
          f"{r['Davies-Bouldin']:>10.4f} | "
          f"{r['Calinski-Harabasz']:>10.1f} | "
          f"{r['ARI']:>6.4f} | "
          f"{r['NMI']:>6.4f}")

print("="*75)
print("Silhouette: ↑ (สูงดี) | DB Index: ↓ (ต่ำดี) | CH Index: ↑ (สูงดี)")
print("ARI, NMI: ↑ (สูงดี, ต้องมี Ground Truth)")

# ─── 4. Silhouette Plot ──────────────────────────────────────────────────
from sklearn.metrics import silhouette_samples

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# Silhouette Plot สำหรับ K-Means
km = KMeans(n_clusters=true_clusters, random_state=42, n_init=10)
km_labels = km.fit_predict(X_scaled)
silhouette_vals = silhouette_samples(X_scaled, km_labels)
avg_silhouette = np.mean(silhouette_vals)

y_lower = 10
colors = plt.cm.Set2(np.linspace(0, 1, true_clusters))

for i in range(true_clusters):
    cluster_sil = np.sort(silhouette_vals[km_labels == i])
    size = cluster_sil.shape[0]
    y_upper = y_lower + size

    axes[0].fill_betweenx(np.arange(y_lower, y_upper),
                         0, cluster_sil, alpha=0.7, color=colors[i])
    axes[0].text(-0.05, y_lower + 0.5 * size, f'C{i}')
    y_lower = y_upper + 10

axes[0].axvline(x=avg_silhouette, color='red', linestyle='--',
               label=f'Avg = {avg_silhouette:.3f}')
axes[0].set_xlabel('Silhouette Coefficient')
axes[0].set_ylabel('Cluster')
axes[0].set_title(f'Silhouette Plot - K-Means (K={true_clusters})')
axes[0].legend()

# เปรียบเทียบ Silhouette สำหรับหลาย K
sil_scores = []
for k in range(2, 9):
    km_temp = KMeans(n_clusters=k, random_state=42, n_init=10)
    labels_temp = km_temp.fit_predict(X_scaled)
    sil_scores.append(silhouette_score(X_scaled, labels_temp))

axes[1].plot(range(2, 9), sil_scores, 'bo-', linewidth=2, markersize=8)
axes[1].axvline(x=true_clusters, color='red', linestyle='--',
               label=f'K={true_clusters} (เลือก)')
axes[1].set_xlabel('จำนวน Cluster (K)')
axes[1].set_ylabel('Silhouette Score')
axes[1].set_title('Silhouette Score vs K')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('clustering_evaluation.png', dpi=150)
plt.show()

5.5 สรุป Evaluation Metrics

Metric ต้องการ Ground Truth ช่วงค่า ค่าที่ดี ใช้กับ
Silhouette Score [-1, 1] → 1 ทุกอัลกอริทึม
Davies-Bouldin [0, ∞) → 0 ทุกอัลกอริทึม
Calinski-Harabasz [0, ∞) → ∞ ทุกอัลกอริทึม
Adjusted Rand Index [-1, 1] → 1 ทุกอัลกอริทึม
NMI [0, 1] → 1 ทุกอัลกอริทึม

6. สรุปและการเปรียบเทียบ

6.1 เปรียบเทียบ Clustering Algorithms

คุณสมบัติ K-Means Hierarchical DBSCAN
กำหนด K ล่วงหน้า ✅ ต้อง ✅ ต้อง (ตัด) ❌ ไม่ต้อง
รูปร่าง Cluster Spherical ขึ้นกับ Linkage Arbitrary
ตรวจ Outlier ✅ อัตโนมัติ

| Scalability | ✅ สูง | ❌ ต่ำ (O(n²)) | กลาง | | พารามิเตอร์ sensitiv | กลาง | ต่ำ | สูง | | ความซับซ้อน | O(nKt) | O(n² log n) | O(n log n)* | | ผลลัพธ์ซ้ำได้ | บางส่วน | ✅ | ✅ | | เหมาะกับ | ข้อมูลใหญ่, กลม | ข้อมูลเล็ก, ลำดับชั้น | ข้อมูลมี Noise |

*DBSCAN: O(n²) กรณีเลวร้าย ถ้าไม่ใช้ Index

6.2 แนวทางเลือก Clustering Algorithm

flowchart TD
    Q0["🤔 จะเลือก Clustering Algorithm ใด?"]:::question

    Q0 --> Q1{"มี Ground Truth - Labels หรือไม่?"}:::q
    Q1 -->|"ไม่มี (Unsupervised)"| Q2{"รู้จำนวน - Cluster หรือไม่?"}:::q
    Q1 -->|"มี (เพื่อ Eval)"| E["ใช้ ARI, NMI - ประเมินทุกอัลกอริทึม"]:::info

    Q2 -->|"ไม่รู้"| Q3{"ข้อมูลมี - Noise/Outlier มาก?"}:::q
    Q2 -->|"รู้จำนวน K"| Q5{"ข้อมูลมีรูปร่าง - ซับซ้อน?"}:::q

    Q3 -->|"ใช่"| R1["✅ DBSCAN - ตรวจ Outlier ได้"]:::result
    Q3 -->|"ไม่มาก"| R2["✅ Hierarchical Clustering - ดู Dendrogram หาจำนวน K"]:::result

    Q5 -->|"ซับซ้อน"| R4["✅ DBSCAN หรือ - Spectral Clustering"]:::result
    Q5 -->|"เป็น Spherical"| R5["✅ K-Means - เร็ว ง่าย ดีสำหรับ - ข้อมูลขนาดใหญ่"]:::result

    classDef question fill:#d79921,stroke:#fabd2f,color:#282828,font-weight:bold
    classDef q fill:#458588,stroke:#83a598,color:#ebdbb2
    classDef result fill:#98971a,stroke:#b8bb26,color:#ebdbb2,font-weight:bold
    classDef info fill:#b16286,stroke:#d3869b,color:#ebdbb2

6.3 Best Practices และข้อแนะนำ

ก่อนทำ Clustering:

ระหว่างทำ Clustering:

หลังทำ Clustering:

6.4 ตัวอย่างการใช้งานจริง

Domain Use Case Algorithm แนะนำ
Marketing Customer Segmentation K-Means
Biology Gene Expression Analysis Hierarchical
Cybersecurity Network Anomaly Detection DBSCAN
Image Processing Image Segmentation K-Means
NLP Document Clustering K-Means, Hierarchical
Astronomy Galaxy Classification DBSCAN
Finance Market Regime Detection HMM

7. เอกสารอ้างอิง

หนังสือและตำราเรียน

  1. Bishop, C. M. (2006). Pattern Recognition and Machine Learning. Springer. [บทที่ 9: Mixture Models and EM]
  2. Murphy, K. P. (2022). Probabilistic Machine Learning: An Introduction. MIT Press. [บทที่ 21: Clustering]
  3. Hastie, T., Tibshirani, R., & Friedman, J. (2009). The Elements of Statistical Learning (2nd ed.). Springer. [บทที่ 14]
  4. Han, J., Kamber, M., & Pei, J. (2011). Data Mining: Concepts and Techniques (3rd ed.). Morgan Kaufmann.

งานวิจัยสำคัญ

  1. MacQueen, J. (1967). Some methods for classification and analysis of multivariate observations. Proceedings of the Fifth Berkeley Symposium, 1, 281-297. [ต้นกำเนิด K-Means]
  2. Ester, M., Kriegel, H. P., Sander, J., & Xu, X. (1996). A density-based algorithm for discovering clusters in large spatial databases with noise. KDD, 96(34), 226-231. [DBSCAN]
  3. Dempster, A. P., Laird, N. M., & Rubin, D. B. (1977). Maximum likelihood from incomplete data via the EM algorithm. Journal of the Royal Statistical Society: Series B, 39(1), 1-22. [EM Algorithm]
  4. Ward, J. H. (1963). Hierarchical grouping to optimize an objective function. Journal of the American Statistical Association, 58(301), 236-244. [Ward's Linkage]

เอกสารออนไลน์

  1. scikit-learn Documentation — Clustering: https://scikit-learn.org/stable/modules/clustering.html
  2. scikit-learn — Clustering performance evaluation: https://scikit-learn.org/stable/modules/clustering.html#clustering-evaluation