K-means
Mục tiêu
Phần tiêu đề “Mục tiêu”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) 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 hợp lý bằng phương pháp khuỷu tay (Elbow method) và hệ số Silhouette (Silhouette score).
Cần biết trước
Phần tiêu đề “Cần biết trước”- Tổng quan về Học máy, thuộc phần Học máy (Đã học)
- Đại số tuyến tính, thuộc phần Toán cho AI (Đã học)
Trực giác
Phần tiêu đề “Trực giác”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.
Gom cụm quanh các tâm
Phần tiêu đề “Gom cụm quanh các tâm”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 nhóm:
- Bạn đặt tạm 3 người đại diện vào 3 vị trí bất kỳ trong cửa hàng.
- 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.
- 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.
- 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 mẫu 36 điểm
Phần tiêu đề “Dữ liệu mẫu 36 điểm”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 độ , và . Dưới đây là tọa độ của vài điểm tiêu biểu:
| Chỉ số mẫu | Tọa độ | Vị trí trực quan |
|---|---|---|
| 0 | Cụm dưới bên trái | |
| 1 | Cụm dưới bên trái | |
| 12 | Cụm dưới bên phải | |
| 24 | 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 điểm dữ liệu chưa có nhãn vào 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.
Lý thuyết
Phần tiêu đề “Lý thuyết”Phát biểu bài toán
Phần tiêu đề “Phát biểu bài toán”Cho tập dữ liệu gồm mẫu , không có nhãn mục tiêu . Ta muốn phân hoạch mẫu này thành cụm không giao nhau:
Mỗi cụm có một vectơ tâm . Gọi là chỉ số cụm được gán cho mẫu thứ .
Đạ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):
Trong đó:
- là tập hợp các chỉ số gán cụm.
- là tập hợp các tâm cụm.
Bài toán tối ưu hóa đồng thời cả và 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: hai bước luân phiên
Phần tiêu đề “Thuật toán Lloyd: hai bước luân phiên”Thuật toán Lloyd giải quyết việc tối ưu hai nhóm biến và 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, ..., μ_kLặ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 ưuBước 1: Gán cụm (Assignment step)
Phần tiêu đề “Bước 1: Gán cụm (Assignment step)”Giữ cố định các tâm cụm . Để làm nhỏ nhất tổng , mỗi số hạng phải nhỏ nhất độc lập với nhau. Do đó, ta gán mỗi mẫu vào cụm có tâm gần nó nhất:
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ị chắc chắn giảm hoặc giữ nguyên: .
Bước 2: Cập nhật tâm (Update step)
Phần tiêu đề “Bước 2: Cập nhật tâm (Update step)”Giữ cố định các chỉ số gán cụm . Bây giờ ta cần tìm các tâm sao cho nhỏ nhất:
Vì các cụm rời nhau, ta có thể cực tiểu hóa riêng rẽ từng cụm . Lấy đạo hàm riêng theo vectơ tâm :
Cho đạo hàm bằng vectơ để tìm điểm cực tiểu:
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ị lại tiếp tục giảm hoặc giữ nguyên: .
Khởi tạo và thuật toán K-means++
Phần tiêu đề “Khởi tạo và thuật toán K-means++”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 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 () 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 cuối cùng nhỏ nhất.
Chiến lược 2: Thuật toán K-means++
Phần tiêu đề “Chiến lược 2: Thuật toán K-means++”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++:
- Chọn ngẫu nhiên tâm đầu tiên từ tập dữ liệu với phân phối đều.
- Với mỗi điểm , tính khoảng cách ngắn nhất từ nó tới bất kỳ tâm nào đã được chọn:
- 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:
- Lặp lại bước 2 và 3 cho tới khi chọn đủ tâm.
Điểm nào nằm càng xa các tâm đã có thì 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++').
Chọn số cụm k: Elbow và Silhouette
Phần tiêu đề “Chọn số cụm k: Elbow và Silhouette”Trong thực tế, bạn thường không biết trước số cụm thực sự của dữ liệu. Vì sao ta không thể đơn giản chọn có quán tính nhỏ nhất?
Bởi vì khi tăng, luôn giảm. Nếu đặt , mỗi điểm dữ liệu là một tâm, và tuyệt đối! Con số đó hoàn toàn vô nghĩa vì không còn ý nghĩa gom nhóm. Để chọn hợp lý, ta cần hai công cụ định lượng:
Phương pháp khuỷu tay (Elbow Method)
Phần tiêu đề “Phương pháp khuỷu tay (Elbow Method)”Ta vẽ đồ thị quán tính theo số cụm . Khi tăng từ 1 lên giá trị số cụm thực sự, 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 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ị 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.
Hệ số Silhouette (Silhouette Score)
Phần tiêu đề “Hệ số Silhouette (Silhouette Score)”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 :
- Tính : khoảng cách trung bình từ tới tất cả các điểm khác trong cùng cụm (mức độ gắn kết nội bộ, càng nhỏ càng tốt):
- Tính : khoảng cách trung bình nhỏ nhất từ tới các điểm của một cụm khác (khoảng cách tới cụm lân cận gần nhất, càng lớn càng tốt):
- Hệ số Silhouette của mẫu là:
Ý nghĩa của :
- : , 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).
- : , mẫu nằm ngay sát ranh giới giữa hai cụm.
- : , 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 đạt đỉnh cực đại tại giá trị phù hợp nhất.
Giới hạn
Phần tiêu đề “Giới hạn”- Phải xác định trước số cụm . 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 đó.
Minh họa
Phần tiêu đề “Minh họa”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 | Pha | Quán tính | Nhận xét diễn biến |
|---|---|---|---|
| 0 | Khởi tạo | — | Chọn 3 điểm số 2, 20 và 1 làm 3 tâm ban đầu |
| 1 | Gán cụm | 406.70 | Mọi điểm chọn tâm gần nhất trong 3 tâm ban đầu |
| 1 | Cập nhật | 241.67 | 3 tâm chuyển về trọng tâm các điểm vừa gán, giảm mạnh |
| 2 | Gán cụm | 230.88 | Nhiều điểm ở biên đổi sang cụm có tâm mới gần hơn |
| 2 | Cập nhật | 200.73 | Các tâm tiếp tục trôi dần về tâm của 3 đám mây |
| 3 | Gán cụm | 139.13 | Điểm đổi cụm tạo bước nhảy lớn về cấu trúc |
| 3 | Cập nhật | 83.62 | Quán tính giảm sâu, cấu trúc 3 cụm bắt đầu tách bạch |
| 4 | Gán cụm | 58.49 | Chỉ còn vài điểm biên dịch chuyển |
| 4 | Cập nhật | 52.12 | 3 tâm đạt vị trí tối ưu: , , |
| 5 | Gán cụm | 52.12 | Không còn bất kỳ điểm nào đổi cụm Thuật toán dừng lại |
Hãy để ý hai đặc điểm quan trọng:
- Giá trị 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.
- 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.
Ví dụ code
Phần tiêu đề “Ví dụ code”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 , , .
Ô đầ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 không còn thay đổi.
Thuật toán Lloyd cài đặt từng bước bằng NumPy
import numpy as npfrom 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 = 3mu = X[[2, 20, 1]].copy() # 3 tâm ban đầu: điểm số 2, 20 và 1c = 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 . Ba cụm có kích thước đồng đều và ba tâm hội tụ tại , và — rất sát với ba tâm gốc , , 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 npfrom sklearn.cluster import KMeansfrom 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ầnrun("k-means++", 1) # k-means++, chạy 1 lầnrun("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 lên tới (thay vì ). Ngược lại, K-means++ (init="k-means++", n_init=1) đạt kết quả tối ưu ở 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 : hai tâm bị hút vào cùng một đám mây phía dưới và , khiến hai đám mây còn lại bị gộp chung vào một tâm duy nhất với .
Ô thứ ba khảo sát số cụm từ 1 đến 6 bằng scikit-learn, tính quán tính 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 pltimport numpy as npfrom sklearn.cluster import KMeansfrom sklearn.datasets import make_blobsfrom 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à :
- Đồ thị Elbow bên trái gập góc dốc nhất tại , nơi rơi từ xuống . Sau , đường cong gần như đi thoai thoải.
- Đồ thị Silhouette bên phải đạt đỉnh cực đại duy nhất tại với giá trị rất cao , cao hơn hẳn () hay ().
Lỗi hay gặp
Phần tiêu đề “Lỗi hay gặp”Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Tóm tắt và đọc thêm
Phần tiêu đề “Tóm tắt và đọc thêm”Ý chính:
- K-means là thuật toán học không giám sát kinh điển, phân hoạch mẫu dữ liệu vào cụm nhằm cực tiểu hóa hàm quán tính .
- 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 , giải quyết triệt để vấn đề kẹt ở cực tiểu cục bộ xấu.
- Số cụm đượ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:
- Lloyd, S. (1982). Least squares quantization in PCM (PDF). IEEE Transactions on Information Theory, 28(2), 129–137. Bài báo gốc công bố thuật toán lặp phân cụm K-means.
- Arthur, D., & Vassilvitskii, S. (2007). k-means++: The advantages of careful seeding (PDF). Proceedings of the 18th Annual ACM-SIAM Symposium on Discrete Algorithms, 1027–1035. Bài báo gốc về phương pháp khởi tạo K-means++.
- Rousseeuw, P. J. (1987). Silhouettes: A graphical aid to the interpretation and validation of cluster analysis. Journal of Computational and Applied Mathematics, 20, 53–65. Đề xuất hệ số Silhouette đánh giá chất lượng phân cụm.
- Vũ Hữu Tiệp (2017). Bài 4: K-means Clustering trong Machine Learning cơ bản. Bài viết tiếng Việt chi tiết với hình vẽ trực quan từng bước thuật toán K-means.
- scikit-learn documentation. K-means. Tài liệu hướng dẫn sử dụng
sklearn.cluster.KMeans.
Trình duyệt đang chặn lưu dữ liệu. Tiến độ sẽ mất khi bạn tải lại trang.
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