Bỏ qua để đến nội dung

K-means

Sau bài này, bạn sẽ:

  • Hiểu bản chất của học không giám sát (unsupervised learning) và bài toán phân cụm (clustering).
  • Nắm vững hàm mục tiêu quán tính (inertia) JJ và thuật toán Lloyd với hai pha luân phiên: gán cụm (assignment) và cập nhật tâm (update).
  • Giải thích được vì sao khởi tạo ngẫu nhiên dễ rơi vào cực tiểu cục bộ và cách thuật toán K-means++ khắc phục nhược điểm này.
  • Biết cách chọn số cụm kk hợp lý bằng phương pháp khuỷu tay (Elbow method) và hệ số Silhouette (Silhouette score).

Hãy tưởng tượng bạn là chủ một chuỗi bán lẻ thời trang với hàng chục nghìn khách hàng. Mỗi khách hàng có hai thông tin: số lần mua sắm mỗi tháng và số tiền chi tiêu trung bình mỗi lần ghé tiệm. Bạn muốn chia họ thành các nhóm khách hàng để thiết kế chương trình ưu đãi phù hợp, nhưng trong tay bạn không hề có nhãn phân loại sẵn nào như “khách VIP”, “khách bình dân” hay “khách săn hàng giảm giá”.

Đó là lúc bạn cần đến học không giám sát (unsupervised learning) và bài toán phân cụm (clustering): tự động nhóm các điểm dữ liệu tương đồng lại gần nhau, sao cho các điểm trong cùng một nhóm thì gần nhau nhất, còn các nhóm khác nhau thì cách xa nhau nhất.

Cách tự nhiên nhất để định hình một nhóm là chọn ra một người đại diện đứng ở trung tâm nhóm — gọi là tâm cụm (centroid). Giả sử bạn muốn chia dữ liệu thành k=3k = 3 nhóm:

  1. Bạn đặt tạm 3 người đại diện vào 3 vị trí bất kỳ trong cửa hàng.
  2. Mỗi khách hàng nhìn quanh và bước về phía người đại diện gần mình nhất. Lúc này khách hàng đã tạm chia thành 3 cụm.
  3. Người đại diện của mỗi cụm bước tới vị trí trung bình cộng (trọng tâm) của tất cả khách hàng vừa chọn mình.
  4. Khi người đại diện đổi chỗ, một số khách hàng nhận ra người đại diện khác nay lại gần họ hơn, nên họ đổi sang nhóm mới.

Quá trình này lặp đi lặp lại. Sau vài lượt, không còn ai đổi nhóm nữa và các người đại diện cũng đứng yên tại chỗ. Thuật toán đã ổn định!

Dữ liệu của bài học này là 36 điểm trên mặt phẳng 2D, được tạo thành từ 3 đám mây điểm tự nhiên (mỗi đám 12 điểm) quanh các tọa độ (2,2)(2, 2), (8,3)(8, 3) và (5,8)(5, 8). Dưới đây là tọa độ của vài điểm tiêu biểu:

Chỉ số mẫuTọa độ (x1,x2)(x_1, x_2)Vị trí trực quan
0(1.39,1.86)(1.39, 1.86)Cụm dưới bên trái
1(1.78,2.45)(1.78, 2.45)Cụm dưới bên trái
12(8.85,3.21)(8.85, 3.21)Cụm dưới bên phải
24(5.20,7.82)(5.20, 7.82)Cụm phía trên

Mắt thường dễ dàng thấy 3 cụm tách biệt. Nhưng máy tính chỉ nhận được một mảng số thô gồm 36 hàng và 2 cột, không hề có nhãn gợi ý điểm nào thuộc cụm nào.

Bài toán của bài này là: chia mm điểm dữ liệu chưa có nhãn vào kk cụm sao cho tổng bình phương khoảng cách từ mỗi điểm tới tâm của cụm nó thuộc về đạt giá trị nhỏ nhất.

Cho tập dữ liệu gồm mm mẫu x(1),x(2),…,x(m)∈Rn\mathbf{x}^{(1)}, \mathbf{x}^{(2)}, \dots, \mathbf{x}^{(m)} \in \mathbb{R}^n, không có nhãn mục tiêu yy. Ta muốn phân hoạch mm mẫu này thành kk cụm không giao nhau:

C1∪C2∪⋯∪Ck={1,2,…,m},Ci∩Cj=∅  (∀i≠j)C_1 \cup C_2 \cup \dots \cup C_k = \{1, 2, \dots, m\}, \quad C_i \cap C_j = \emptyset \; (\forall i \ne j)

Mỗi cụm j∈{1,…,k}j \in \{1, \dots, k\} có một vectơ tâm μj∈Rn\boldsymbol{\mu}_j \in \mathbb{R}^n. Gọi c(i)∈{1,…,k}c^{(i)} \in \{1, \dots, k\} là chỉ số cụm được gán cho mẫu thứ ii.

Đại lượng cần làm nhỏ nhất là tổng bình phương khoảng cách từ mỗi điểm tới tâm cụm của nó, thường gọi là quán tính (inertia) hoặc độ biến dạng (distortion):

J(c,μ)=∑i=1m∥x(i)−μc(i)∥2J(\mathbf{c}, \boldsymbol{\mu}) = \sum_{i=1}^m \lVert \mathbf{x}^{(i)} - \boldsymbol{\mu}_{c^{(i)}} \rVert^2

Trong đó:

  • c=[c(1),c(2),…,c(m)]⊤\mathbf{c} = [c^{(1)}, c^{(2)}, \dots, c^{(m)}]^\top là tập hợp các chỉ số gán cụm.
  • μ={μ1,…,μk}\boldsymbol{\mu} = \{\boldsymbol{\mu}_1, \dots, \boldsymbol{\mu}_k\} là tập hợp các tâm cụm.

Bài toán tối ưu hóa đồng thời cả c\mathbf{c} và μ\boldsymbol{\mu} là một bài toán NP-khó (NP-hard). Thay vì tìm nghiệm tối ưu toàn cục bằng cách duyệt mọi cách phân hoạch, ta dùng một thuật toán lặp xấp xỉ cục bộ rất hiệu quả: thuật toán Lloyd (thường gọi đơn giản là thuật toán K-means).

Thuật toán Lloyd giải quyết việc tối ưu hai nhóm biến c\mathbf{c} và μ\boldsymbol{\mu} bằng cách cố định một nhóm rồi tối ưu nhóm còn lại, lặp đi lặp lại:

Khởi tạo k tâm cụm ban đầu μ_1, ..., μ_k
Lặp cho tới khi hội tụ:
Bước 1 (Gán cụm): Cố định μ, tìm gán cụm c tối ưu
Bước 2 (Cập nhật tâm): Cố định c, tìm tâm μ tối ưu

Giữ cố định các tâm cụm μ1,…,μk\boldsymbol{\mu}_1, \dots, \boldsymbol{\mu}_k. Để làm nhỏ nhất tổng JJ, mỗi số hạng ∥x(i)−μc(i)∥2\lVert \mathbf{x}^{(i)} - \boldsymbol{\mu}_{c^{(i)}} \rVert^2 phải nhỏ nhất độc lập với nhau. Do đó, ta gán mỗi mẫu x(i)\mathbf{x}^{(i)} vào cụm có tâm gần nó nhất:

c(i)=arg⁡min⁡j∈{1,…,k}∥x(i)−μj∥2c^{(i)} = \arg\min_{j \in \{1, \dots, k\}} \lVert \mathbf{x}^{(i)} - \boldsymbol{\mu}_j \rVert^2

Nếu có nhiều tâm cùng đạt khoảng cách nhỏ nhất bằng nhau, ta có thể chọn bất kỳ tâm nào trong số đó. Sau bước này, giá trị JJ chắc chắn giảm hoặc giữ nguyên: J(c(t+1),μ(t))≤J(c(t),μ(t))J(\mathbf{c}^{(t+1)}, \boldsymbol{\mu}^{(t)}) \le J(\mathbf{c}^{(t)}, \boldsymbol{\mu}^{(t)}).

Giữ cố định các chỉ số gán cụm c\mathbf{c}. Bây giờ ta cần tìm các tâm μj\boldsymbol{\mu}_j sao cho JJ nhỏ nhất:

J(c,μ)=∑j=1k∑i∈Cj∥x(i)−μj∥2J(\mathbf{c}, \boldsymbol{\mu}) = \sum_{j=1}^k \sum_{i \in C_j} \lVert \mathbf{x}^{(i)} - \boldsymbol{\mu}_j \rVert^2

Vì các cụm rời nhau, ta có thể cực tiểu hóa riêng rẽ từng cụm jj. Lấy đạo hàm riêng theo vectơ tâm μj\boldsymbol{\mu}_j:

∇μjJ=∇μj∑i∈Cj(x(i)−μj)⊤(x(i)−μj)=∑i∈Cj−2(x(i)−μj)=−2(∑i∈Cjx(i)−∣Cj∣μj)\begin{aligned} \nabla_{\boldsymbol{\mu}_j} J &= \nabla_{\boldsymbol{\mu}_j} \sum_{i \in C_j} (\mathbf{x}^{(i)} - \boldsymbol{\mu}_j)^\top (\mathbf{x}^{(i)} - \boldsymbol{\mu}_j) \\ &= \sum_{i \in C_j} -2(\mathbf{x}^{(i)} - \boldsymbol{\mu}_j) \\ &= -2 \left( \sum_{i \in C_j} \mathbf{x}^{(i)} - |C_j| \boldsymbol{\mu}_j \right) \end{aligned}

Cho đạo hàm bằng vectơ 0\mathbf{0} để tìm điểm cực tiểu:

∑i∈Cjx(i)−∣Cj∣μj=0  ⟹  μj=1∣Cj∣∑i∈Cjx(i)\sum_{i \in C_j} \mathbf{x}^{(i)} - |C_j| \boldsymbol{\mu}_j = \mathbf{0} \implies \boldsymbol{\mu}_j = \frac{1}{|C_j|} \sum_{i \in C_j} \mathbf{x}^{(i)}

Tâm mới chính là trung bình cộng (mean / centroid) của tất cả các điểm được gán vào cụm đó. Sau bước cập nhật này, giá trị JJ lại tiếp tục giảm hoặc giữ nguyên: J(c(t+1),μ(t+1))≤J(c(t+1),μ(t))J(\mathbf{c}^{(t+1)}, \boldsymbol{\mu}^{(t+1)}) \le J(\mathbf{c}^{(t+1)}, \boldsymbol{\mu}^{(t)}).

Mặc dù luôn hội tụ, thuật toán Lloyd chỉ hội tụ về một cực tiểu cục bộ (local minimum). Vị trí hội tụ phụ thuộc rất lớn vào vị trí khởi tạo của kk tâm ban đầu:

  • Nếu khởi tạo xấu (ví dụ hai tâm đặt quá gần nhau trong cùng một cụm tự nhiên), một cụm thật có thể bị xẻ đôi, trong khi hai cụm thật khác bị gộp chung làm một.
  • Nghiêm trọng hơn, có thể xuất hiện cụm rỗng (∣Cj∣=0|C_j| = 0) nếu một tâm không hút được điểm nào.

Chiến lược 1: Khởi tạo ngẫu nhiên nhiều lần (n_init)

Phần tiêu đề “Chiến lược 1: Khởi tạo ngẫu nhiên nhiều lần (n_init)”

Cách khắc phục đơn giản nhất là khởi tạo ngẫu nhiên nhiều lần độc lập (mặc định trong scikit-learn từng là n_init=10), chạy thuật toán Lloyd cho mỗi lần, rồi chọn nghiệm có quán tính JJ cuối cùng nhỏ nhất.

Năm 2007, David Arthur và Sergei Vassilvitskii đề xuất thuật toán K-means++ với nguyên lý trực quan: các tâm ban đầu nên nằm càng cách xa nhau càng tốt.

Các bước của K-means++:

  1. Chọn ngẫu nhiên tâm đầu tiên μ1\boldsymbol{\mu}_1 từ tập dữ liệu với phân phối đều.
  2. Với mỗi điểm x(i)\mathbf{x}^{(i)}, tính khoảng cách ngắn nhất D(x(i))D(\mathbf{x}^{(i)}) từ nó tới bất kỳ tâm nào đã được chọn: D(x(i))=min⁡j∥x(i)−μj∥D(\mathbf{x}^{(i)}) = \min_{j} \lVert \mathbf{x}^{(i)} - \boldsymbol{\mu}_j \rVert
  3. Chọn điểm tiếp theo làm tâm mới với xác suất tỉ lệ thuận với bình phương khoảng cách: P(x(i))=D(x(i))2∑l=1mD(x(l))2P(\mathbf{x}^{(i)}) = \frac{D(\mathbf{x}^{(i)})^2}{\sum_{l=1}^m D(\mathbf{x}^{(l)})^2}
  4. Lặp lại bước 2 và 3 cho tới khi chọn đủ kk tâm.

Điểm nào nằm càng xa các tâm đã có thì D(x)2D(\mathbf{x})^2 càng lớn, xác suất được chọn làm tâm tiếp theo càng cao. K-means++ giúp thuật toán gần như không bao giờ bị kẹt ở các cấu hình tâm tồi và tăng tốc độ hội tụ đáng kể. Đây là phương pháp khởi tạo mặc định trong mọi thư viện học máy hiện đại (init='k-means++').

Trong thực tế, bạn thường không biết trước số cụm kk thực sự của dữ liệu. Vì sao ta không thể đơn giản chọn kk có quán tính JJ nhỏ nhất?

Bởi vì khi kk tăng, JJ luôn giảm. Nếu đặt k=mk = m, mỗi điểm dữ liệu là một tâm, và J=0J = 0 tuyệt đối! Con số J=0J = 0 đó hoàn toàn vô nghĩa vì không còn ý nghĩa gom nhóm. Để chọn kk hợp lý, ta cần hai công cụ định lượng:

Ta vẽ đồ thị quán tính JJ theo số cụm k∈{1,2,… }k \in \{1, 2, \dots\}. Khi kk tăng từ 1 lên giá trị số cụm thực sự, JJ giảm rất nhanh. Khi vượt qua số cụm thực sự, việc thêm tâm mới chỉ chia nhỏ các cụm vốn đã khép kín, nên JJ giảm chậm lại rõ rệt.

Đồ thị tạo thành một khúc gập giống như khuỷu tay (elbow). Giá trị kk tại “khuỷu tay” chính là lựa chọn cân bằng tối ưu giữa việc giảm sai số và giữ số cụm tinh gọn.

Phương pháp Elbow đôi khi cho điểm gập không rõ ràng. Hệ số Silhouette (Peter Rousseeuw, 1987) đánh giá chất lượng phân cụm trực tiếp cho từng điểm dữ liệu dựa trên hai yếu tố: độ gắn kết nội cụm và độ tách biệt ngoại cụm.

Với mỗi mẫu x(i)∈CI\mathbf{x}^{(i)} \in C_I:

  1. Tính a(i)a^{(i)}: khoảng cách trung bình từ x(i)\mathbf{x}^{(i)} tới tất cả các điểm khác trong cùng cụm CIC_I (mức độ gắn kết nội bộ, càng nhỏ càng tốt): a(i)=1∣CI∣−1∑j∈CI,j≠i∥x(i)−x(j)∥a^{(i)} = \frac{1}{|C_I| - 1} \sum_{j \in C_I, j \ne i} \lVert \mathbf{x}^{(i)} - \mathbf{x}^{(j)} \rVert
  2. Tính b(i)b^{(i)}: khoảng cách trung bình nhỏ nhất từ x(i)\mathbf{x}^{(i)} tới các điểm của một cụm khác CJ≠CIC_J \ne C_I (khoảng cách tới cụm lân cận gần nhất, càng lớn càng tốt): b(i)=min⁡J≠I1∣CJ∣∑j∈CJ∥x(i)−x(j)∥b^{(i)} = \min_{J \ne I} \frac{1}{|C_J|} \sum_{j \in C_J} \lVert \mathbf{x}^{(i)} - \mathbf{x}^{(j)} \rVert
  3. Hệ số Silhouette của mẫu ii là: s(i)=b(i)−a(i)max⁡(a(i),b(i))∈[−1,1]s^{(i)} = \frac{b^{(i)} - a^{(i)}}{\max(a^{(i)}, b^{(i)})} \in [-1, 1]

Ý nghĩa của s(i)s^{(i)}:

  • s(i)≈1s^{(i)} \approx 1: a(i)≪b(i)a^{(i)} \ll b^{(i)}, mẫu nằm rất sâu bên trong cụm của mình, cách rất xa cụm khác (phân cụm hoàn hảo).
  • s(i)≈0s^{(i)} \approx 0: a(i)≈b(i)a^{(i)} \approx b^{(i)}, mẫu nằm ngay sát ranh giới giữa hai cụm.
  • s(i)<0s^{(i)} < 0: a(i)>b(i)a^{(i)} > b^{(i)}, mẫu gần cụm hàng xóm hơn cụm được gán (gán nhầm cụm).

Điểm số Silhouette trung bình trên toàn bộ dữ liệu sˉ=1m∑i=1ms(i)\bar{s} = \frac{1}{m} \sum_{i=1}^m s^{(i)} đạt đỉnh cực đại tại giá trị kk phù hợp nhất.

  • Phải xác định trước số cụm kk. K-means không thể tự phát hiện có bao nhiêu cụm tự nhiên trong dữ liệu.
  • Giả định cụm có dạng hình cầu lồi (spherical / convex). Vì dùng khoảng cách Euclid, ranh giới giữa các cụm tạo bởi K-means luôn là các đa giác phẳng (đa giác Voronoi). K-means hoàn toàn thất bại khi các cụm có hình dạng phức tạp (như hình trăng khuyết lồng nhau, dải xoắn) hoặc kích thước, mật độ chênh lệch lớn.
  • Nhạy cảm với giá trị ngoại lai (outliers). Vì tối ưu tổng bình phương khoảng cách, một điểm ngoại lai nằm rất xa có thể kéo lệch hẳn tâm cụm về phía nó.
  • Bắt buộc phải chuẩn hóa đặc trưng. Nếu một đặc trưng có biên độ đo đạc lớn hơn các đặc trưng khác hàng nghìn lần (ví dụ thu nhập theo triệu đồng so với số con theo người), khoảng cách Euclid sẽ bị chi phối hoàn toàn bởi đặc trưng đó.

Dưới đây là bảng theo dõi từng bước lặp của thuật toán Lloyd trên 36 điểm dữ liệu mẫu 2D (3 cụm thực sự) khi khởi tạo 3 tâm ban đầu tại 3 điểm dữ liệu số 2, 20 và 1:

Vòng lặp ttPhaQuán tính JJNhận xét diễn biến
0Khởi tạo—Chọn 3 điểm số 2, 20 và 1 làm 3 tâm ban đầu μ1,μ2,μ3\boldsymbol{\mu}_1, \boldsymbol{\mu}_2, \boldsymbol{\mu}_3
1Gán cụm406.70Mọi điểm chọn tâm gần nhất trong 3 tâm ban đầu
1Cập nhật241.673 tâm chuyển về trọng tâm các điểm vừa gán, JJ giảm mạnh
2Gán cụm230.88Nhiều điểm ở biên đổi sang cụm có tâm mới gần hơn
2Cập nhật200.73Các tâm tiếp tục trôi dần về tâm của 3 đám mây
3Gán cụm139.13Điểm đổi cụm tạo bước nhảy lớn về cấu trúc
3Cập nhật83.62Quán tính giảm sâu, cấu trúc 3 cụm bắt đầu tách bạch
4Gán cụm58.49Chỉ còn vài điểm biên dịch chuyển
4Cập nhật52.123 tâm đạt vị trí tối ưu: (1.92,2.19)(1.92, 2.19), (8.20,3.09)(8.20, 3.09), (5.41,7.90)(5.41, 7.90)
5Gán cụm52.12Không còn bất kỳ điểm nào đổi cụm →\to Thuật toán dừng lại

Hãy để ý hai đặc điểm quan trọng:

  1. Giá trị JJ giảm liên tục sau mỗi pha: từ 406.70 xuống 241.67, 230.88, 200.73, 139.13, 83.62, 58.49 và dừng lại ở 52.12.
  2. Tại vòng 5, kết quả gán cụm giống hệt vòng 4 nên thuật toán công nhận hội tụ và dừng lại sau đúng 4 bước cập nhật. Kích thước 3 cụm lúc này chia đều hoàn hảo: 12, 12, 12 điểm.

Ba ô code dưới đây chạy độc lập. Ô chạy đầu tiên mất vài giây để trình duyệt tải thư viện scikit-learn qua WebAssembly (Pyodide). Cả ba ô đều sử dụng chung bộ dữ liệu 36 điểm 2D được làm tròn 2 chữ số thập phân sinh từ make_blobs với 3 tâm chuẩn (2,2)(2, 2), (8,3)(8, 3), (5,8)(5, 8).

Ô đầu tiên cài đặt nguyên bản thuật toán Lloyd bằng NumPy: tính ma trận bình phương khoảng cách giữa các điểm và tâm bằng mảng đa chiều (broadcasting), cập nhật tâm bằng trung bình cộng và dừng khi mảng gán cụm cc không còn thay đổi.

Thuật toán Lloyd cài đặt từng bước bằng NumPy

import numpy as np
from sklearn.datasets import make_blobs
# 36 điểm 2D thành 3 cụm, làm tròn 2 chữ số.
X, _ = make_blobs(n_samples=36, centers=[[2, 2], [8, 3], [5, 8]],
cluster_std=1.0, random_state=10)
X = np.round(X, 2)
k = 3
mu = X[[2, 20, 1]].copy() # 3 tâm ban đầu: điểm số 2, 20 và 1
c = np.full(len(X), -1) # chưa điểm nào được gán
for t in range(1, 101):
# Pha gán: bình phương khoảng cách từ mỗi điểm tới mỗi tâm.
d2 = ((X[:, None, :] - mu[None, :, :]) ** 2).sum(axis=2)
c_new = d2.argmin(axis=1) # chỉ số tâm gần nhất
if np.array_equal(c_new, c): # không điểm nào đổi cụm: dừng
break
c = c_new
J = d2[np.arange(len(X)), c].sum()
print(f"iter {t}: assign J = {J:7.2f}", end=" ")
# Pha cập nhật: mỗi tâm thành trung bình các điểm của cụm.
for j in range(k):
if np.any(c == j): # bỏ qua cụm rỗng
mu[j] = X[c == j].mean(axis=0)
J = ((X - mu[c]) ** 2).sum()
print(f"update J = {J:7.2f}")
print(f"converged, sizes = {np.bincount(c, minlength=k)}")
print("centroids:")
print(np.round(mu, 2))

Lần chạy đầu cần tải Python, khoảng 10 MB.

Kết quả in ra tái hiện chính xác bảng theo dõi ở mục Minh họa. Sau 4 bước lặp, thuật toán hội tụ tại J=52.12J = 52.12. Ba cụm có kích thước đồng đều [12,12,12][12, 12, 12] và ba tâm hội tụ tại (1.92,2.19)(1.92, 2.19), (8.20,3.09)(8.20, 3.09) và (5.41,7.90)(5.41, 7.90) — rất sát với ba tâm gốc (2,2)(2, 2), (8,3)(8, 3), (5,8)(5, 8) dùng để sinh dữ liệu.

Ô thứ hai dùng lớp KMeans của scikit-learn để kiểm chứng sự nguy hiểm của cực tiểu cục bộ: chạy thử nghiệm 50 seed ngẫu nhiên với init="random", so sánh với init="k-means++", và in ra chi tiết một lần bị kẹt ở seed 13.

Khởi tạo ngẫu nhiên, K-means++ và bẫy cực tiểu cục bộ

import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
X, _ = make_blobs(n_samples=36, centers=[[2, 2], [8, 3], [5, 8]],
cluster_std=1.0, random_state=10)
X = np.round(X, 2)
def run(init, n_init):
# Chạy KMeans với 50 seed khác nhau, ghi lại inertia (tức J).
J = [KMeans(3, init=init, n_init=n_init, random_state=s).fit(X).inertia_
for s in range(50)]
J = np.round(J, 2)
stuck = np.sum(J > 52.12) # số lần dừng ở J lớn hơn 52.12
print(f"{init:>9}, n_init={n_init:>2}: min J = {J.min():6.2f}, "
f"max J = {J.max():6.2f}, stuck {stuck}/50")
run("random", 1) # 3 điểm ngẫu nhiên làm tâm, chạy 1 lần
run("k-means++", 1) # k-means++, chạy 1 lần
run("random", 10) # ngẫu nhiên, chạy 10 lần, giữ lần có J nhỏ nhất
# Một lần bị kẹt: J, cỡ các cụm và tọa độ tâm.
km = KMeans(3, init="random", n_init=1, random_state=13).fit(X)
print(f"seed 13: J = {km.inertia_:.2f}, sizes = {np.bincount(km.labels_)}")
print(np.round(km.cluster_centers_, 2))
# Mặc định: init='k-means++', n_init='auto' (1 lần với k-means++).
km = KMeans(3, random_state=0).fit(X)
print(f"default: J = {km.inertia_:.2f}")

Lần chạy đầu cần tải Python, khoảng 10 MB.

Dòng đầu tiên cho thấy: nếu chỉ chọn 3 điểm ngẫu nhiên làm tâm (init="random", n_init=1), có 3 trong số 50 lần thử (6%) thuật toán bị kẹt ở nghiệm xấu với quán tính JJ lên tới 230.40230.40 (thay vì 52.1252.12). Ngược lại, K-means++ (init="k-means++", n_init=1) đạt kết quả tối ưu 52.1252.12 ở cả 50/50 lần chạy mà không một lần bị kẹt. Nếu buộc phải dùng khởi tạo ngẫu nhiên, ta cần tăng n_init=10 để lấy giá trị tốt nhất trong 10 lần chạy.

Ở lần kẹt tại seed 13, kích thước các cụm bị lệch nặng thành [3,24,9][3, 24, 9]: hai tâm bị hút vào cùng một đám mây phía dưới (2.04,0.82)(2.04, 0.82) và (1.87,2.65)(1.87, 2.65), khiến hai đám mây còn lại bị gộp chung vào một tâm duy nhất (6.80,5.49)(6.80, 5.49) với J=230.04J = 230.04.

Ô thứ ba khảo sát số cụm kk từ 1 đến 6 bằng scikit-learn, tính quán tính JJ và hệ số Silhouette, rồi vẽ biểu đồ Elbow và Silhouette.

Chọn số cụm k bằng phương pháp Elbow và Silhouette

import matplotlib.pyplot as plt
import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
from sklearn.metrics import silhouette_score
X, _ = make_blobs(n_samples=36, centers=[[2, 2], [8, 3], [5, 8]],
cluster_std=1.0, random_state=10)
X = np.round(X, 2)
ks = range(1, 7)
inertia, sil = [], []
for k in ks:
km = KMeans(k, n_init=10, random_state=0).fit(X)
inertia.append(km.inertia_)
# Silhouette cần ít nhất 2 cụm.
sil.append(silhouette_score(X, km.labels_) if k > 1 else np.nan)
print(f"k = {k}: J = {km.inertia_:6.2f}, silhouette = {sil[-1]:.3f}")
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 3.5))
ax1.plot(ks, inertia, "o-")
ax1.set_xlabel("k")
ax1.set_ylabel("inertia J")
ax1.set_title("Elbow")
ax2.plot(ks, sil, "o-", color="tab:orange")
ax2.set_xlabel("k")
ax2.set_ylabel("mean silhouette")
ax2.set_title("Silhouette")
fig.tight_layout()
plt.show()

Lần chạy đầu cần tải Python, khoảng 10 MB.

Cả hai đồ thị đều chỉ rõ ràng đáp án tối ưu là k=3k = 3:

  1. Đồ thị Elbow bên trái gập góc dốc nhất tại k=3k = 3, nơi JJ rơi từ 237.64237.64 xuống 52.1252.12. Sau k=3k = 3, đường cong gần như đi thoai thoải.
  2. Đồ thị Silhouette bên phải đạt đỉnh cực đại duy nhất tại k=3k = 3 với giá trị rất cao 0.7280.728, cao hơn hẳn k=2k = 2 (0.5340.534) hay k=4k = 4 (0.6190.619).
Câu 1/4 Bạn chạy K-means trên cùng một tập dữ liệu với k=3k = 3 và k=4k = 4. So sánh hàm mục tiêu quán tính (inertia) JJ giữa hai mô hình?
Câu 2/4 Trong bước cập nhật (update step) của thuật toán Lloyd, vì sao tâm cụm mới μj\boldsymbol{\mu}_j luôn được tính bằng trung bình cộng các điểm trong cụm?
Câu 3/4 Thuật toán khởi tạo K-means++ chọn tâm cụm tiếp theo như thế nào?
Câu 4/4 Một điểm dữ liệu có hệ số Silhouette s≈−0.6s \approx -0.6. Con số này phản ánh điều gì?

Ý chính:

  • K-means là thuật toán học không giám sát kinh điển, phân hoạch mm mẫu dữ liệu vào kk cụm nhằm cực tiểu hóa hàm quán tính J=∑i=1m∥x(i)−μc(i)∥2J = \sum_{i=1}^m \lVert \mathbf{x}^{(i)} - \boldsymbol{\mu}_{c^{(i)}} \rVert^2.
  • Thuật toán Lloyd lặp lại hai pha luân phiên: gán mỗi điểm vào tâm gần nhất, rồi cập nhật tâm thành trung bình cộng của các điểm trong cụm. Thuật toán luôn hội tụ về một cực tiểu cục bộ.
  • Khởi tạo K-means++ chọn các tâm ban đầu cách xa nhau với xác suất tỉ lệ thuận với bình phương khoảng cách D(x)2D(\mathbf{x})^2, giải quyết triệt để vấn đề kẹt ở cực tiểu cục bộ xấu.
  • Số cụm kk được lựa chọn tối ưu thông qua điểm gập trên đồ thị Elbow và điểm cực đại của hệ số Silhouette. Cần chuẩn hóa đặc trưng trước khi chạy thuật toán.

Đọc thêm:

Nội dung theo giấy phép CC BY-NC-SA 4.0. Mã nguồn theo giấy phép MIT.Giới thiệu