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) |
Clustering (การจัดกลุ่ม) คือกระบวนการแบ่งกลุ่มข้อมูลออกเป็น Cluster โดยข้อมูลภายในกลุ่มเดียวกันควรมีความคล้ายคลึงกัน (high intra-cluster similarity) ขณะที่ข้อมูลต่างกลุ่มควรมีความแตกต่างกัน (low inter-cluster similarity)
คุณสมบัติที่ดีของ Clustering:
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
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
K-Means Clustering คืออัลกอริทึมการจัดกลุ่มแบบ Partitional ที่แบ่งข้อมูล N จุด ออกเป็น K กลุ่ม โดยแต่ละกลุ่มมี Centroid (จุดศูนย์กลาง) และข้อมูลแต่ละจุดจะถูกกำหนดให้อยู่ใน Cluster ที่มี Centroid ใกล้ที่สุด
Objective Function ของ K-Means (Within-Cluster Sum of Squares - WCSS):
โดยที่:
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
สมมติเรามีข้อมูล 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 ใหม่:
Iteration 2: ทำ Assignment ใหม่ด้วย Centroid ที่อัปเดต — เมื่อ Assignment ไม่เปลี่ยน → Converged!
"""
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()
ปัญหาของ K-Means ธรรมดา: การสุ่ม Centroid เริ่มต้นแบบ Random อาจนำไปสู่ผลลัพธ์ที่ไม่ดี
K-Means++ แก้ปัญหาโดย:
โดยที่ D(x) คือระยะทางของจุด x ไปยัง Centroid ที่ใกล้ที่สุดที่เลือกไว้แล้ว
| ด้าน | รายละเอียด |
|---|---|
| ✅ ข้อดี | ง่าย รวดเร็ว Scalable สำหรับข้อมูลขนาดใหญ่ |
| ✅ ข้อดี | Convergence รับประกัน (ไม่จำเป็นต้อง Global Optimum) |
| ❌ ข้อเสีย | ต้องกำหนดจำนวน K ล่วงหน้า |
| ❌ ข้อเสีย | ไวต่อ Outlier และ Centroid เริ่มต้น |
| ❌ ข้อเสีย | สมมุติว่า Cluster มีรูปร่างเป็น Spherical (กลม) |
| ❌ ข้อเสีย | ไม่เหมาะกับ Cluster ที่มีขนาด/รูปร่างแตกต่างกัน |
Hierarchical Clustering (การจัดกลุ่มแบบลำดับชั้น) สร้างโครงสร้างลำดับชั้นของ Cluster ที่เรียกว่า Dendrogram มี 2 แนวทางหลัก:
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:
Complete Linkage:
Ward's Linkage (ผลต่างของ WCSS หลังและก่อนรวม):
| จุด | 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):
"""
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))}")
| Linkage | ลักษณะ Cluster | ข้อดี | ข้อเสีย |
|---|---|---|---|
| Single | ยาว แคบ (Chaining) | เร็ว ตรวจ Non-convex ได้ | Chain Effect |
| Complete | กลม กะทัดรัด | ทนต่อ Outlier | Sensitive to Outliers |
| Average | กลาง ๆ | สมดุล | คำนวณช้ากว่า |
| Ward | กลม ขนาดใกล้เคียงกัน | ผลดีที่สุดทั่วไป | เฉพาะ Euclidean |
DBSCAN (Density-Based Spatial Clustering of Applications with Noise) เป็น Clustering Algorithm แบบ Density-Based ที่ค้นหา Cluster โดยใช้แนวคิดว่า Cluster คือพื้นที่ที่มีความหนาแน่นของจุดข้อมูลสูง แยกจากกันด้วยพื้นที่ที่มีความหนาแน่นต่ำ
ข้อได้เปรียบสำคัญ:
พารามิเตอร์:
ประเภทของจุดข้อมูล:
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 ถ้า:
ข้อมูลตัวอย่าง: กำหนด ε = 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):
ผลลัพธ์:
"""
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) ของกราฟ → ค่า ε ที่เหมาะสม")
วิธีหาค่า ε:
วิธีเลือก MinPts:
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
Silhouette Score วัดว่าข้อมูลแต่ละจุดถูกจัดอยู่ใน Cluster ที่เหมาะสมแค่ไหน
โดยที่:
การแปลผล:
ข้อมูล (3 จุด, 2 Cluster):
คำนวณสำหรับ P1:
Davies-Bouldin Index วัดอัตราส่วนระหว่าง Scatter ภายใน Cluster กับ Separation ระหว่าง Cluster
โดยที่:
DBI ต่ำ = Clustering ดี (Cluster กะทัดรัด ห่างกัน)
"""
การประเมินผล 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()
| 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 | ทุกอัลกอริทึม |
| คุณสมบัติ | 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
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
ก่อนทำ Clustering:
ระหว่างทำ Clustering:
หลังทำ Clustering:
| 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 |