K-means: phân nhóm khách hàng không cần nhãn
Xem thửLan ở phòng kinh doanh của Giao Hàng Anh Tí có ngân sách cho ba chương trình ưu đãi khác nhau trong quý tới. Cô cần biết nên gửi chương trình nào cho ai. Dữ liệu có 6.141 khách đã đặt từ 3 đơn trở lên trong sáu tháng đầu năm 2026, mỗi khách tóm bằng bốn con số: số đơn, số món trung bình mỗi đơn, giá trị đơn trung bình và tỉ lệ đơn trả tiền khi nhận hàng (COD).
Không có cột nào ghi "khách này thuộc nhóm nào". Hệ thống chưa từng gắn nhãn, và cũng không ai biết trước có mấy nhóm. Minh, lập trình viên backend, đề xuất chia bằng tay theo giá trị đơn: dưới 250.000 đồng, từ 250.000 tới 500.000 đồng, trên 500.000 đồng. Lan hỏi: "Ba nhóm theo giá tiền thì mỗi nhóm nhận ưu đãi gì khác nhau?".
Trước khi đọc tiếp, hãy ghi ra dự đoán của bạn: nếu để máy tự chia 6.141 khách thành 3 nhóm dựa trên bốn con số đó, máy sẽ tách khách theo cột nào? Và nếu quên chuẩn hóa thì kết quả có giống cách chia theo giá của Minh không?
Sau bài này, bạn có thể
- Giải thích k-means bằng hai bước lặp (gán về tâm gần nhất, dời tâm về trung bình) và tính tay được một vòng trên năm khách.
- Tự viết k-means bằng NumPy, rồi dùng
KMeanscủa scikit-learn trên 6.141 khách và đọc bảng hồ sơ từng cụm. - Nhận ra hai lỗi hay gặp: quên chuẩn hóa khiến cụm chỉ cắt theo một cột, và khởi tạo xấu khiến thuật toán kẹt ở nghiệm kém.
Cần biết trước: bài Vector: biến một đơn hàng và một câu thành dãy số (Chương 3, phần chuẩn hóa) và bài k-NN: dự đoán bằng hàng xóm gần nhất (Chương 7).
Mức độ: Cơ bản · Thời gian: khoảng 20 phút · Từ khóa: học không giám sát, k-means, tâm cụm, inertia, k-means++
Học không giám sát: tìm cấu trúc khi không có đáp án
Ở Chương 7, mỗi đơn hàng có sẵn đáp án: thời gian giao thật, hoặc giao trễ hay không. Mô hình học cách đoán đáp án đó. Đó là học có giám sát (supervised learning).
Bài toán của Lan khác hẳn. Không có đáp án nào để so. Ta chỉ có các điểm dữ liệu và muốn biết chúng có tự gom thành nhóm hay không. Đây là học không giám sát (unsupervised learning): tìm cấu trúc trong dữ liệu không có nhãn. Phân cụm (clustering) là dạng phổ biến nhất: chia các điểm thành nhóm sao cho điểm trong cùng nhóm giống nhau, điểm khác nhóm khác nhau.
"Giống nhau" ở đây đo bằng khoảng cách Euclid giữa hai vector, đúng như bài Vector ở Chương 3. Vì vậy mọi điều bạn đã học về thang đo vẫn áp dụng nguyên vẹn: cột có đơn vị lớn sẽ quyết định khoảng cách.
Mô hình tư duy: gán về tâm gần nhất, dời tâm, lặp lại
K-means chia dữ liệu thành đúng k cụm, k do bạn chọn. Mỗi cụm có một tâm (centroid), là một điểm trong cùng không gian với dữ liệu. Thuật toán lặp hai bước:
- Gán (assign): mỗi điểm thuộc về tâm gần nó nhất.
- Dời tâm (update): mỗi tâm dời tới trung bình của các điểm vừa được gán cho nó.
Lặp lại tới khi không điểm nào đổi cụm. Thuật toán này còn được gọi là thuật toán Lloyd. Để biết một cách chia tốt đến đâu, k-means đo inertia (còn gọi là tổng bình phương trong cụm):
inertia = Σ (khoảng cách từ điểm i tới tâm của cụm chứa i)²
Tổng chạy qua mọi điểm i. Inertia nhỏ nghĩa là các điểm nằm sát tâm của mình. Mỗi bước gán chọn tâm gần nhất nên inertia không tăng. Mỗi bước dời tâm đặt tâm ở trung bình, vị trí làm tổng bình phương nhỏ nhất, nên inertia cũng không tăng. Inertia chỉ giảm hoặc đứng yên, nên thuật toán chắc chắn dừng.
Ví dụ số với năm khách, mỗi khách một con số là số đơn mỗi tháng: An 2, Bình 4, Lan 6, Minh 15, Hà 19. Chọn k = 2, đặt hai tâm ngay tại An (2) và Bình (4).
| Bước | Tâm 1 | Tâm 2 | Cụm 1 | Cụm 2 | Inertia |
|---|---|---|---|---|---|
| Gán lần 1 | 2 | 4 | An | Bình, Lan, Minh, Hà | 350 |
| Dời tâm lần 1 | 2 | 11 | An | Bình, Lan, Minh, Hà | 154 |
| Gán lần 2 | 2 | 11 | An, Bình, Lan | Minh, Hà | 100 |
| Dời tâm lần 2 | 4 | 17 | An, Bình, Lan | Minh, Hà | 16 |
| Gán lần 3 | 4 | 17 | không ai đổi cụm | 16 |
Kiểm một dòng: ở lần gán 2, Bình cách tâm 1 là |4 - 2| = 2 và cách tâm 2 là |4 - 11| = 7, nên Bình chuyển sang cụm 1. Inertia cuối là (2-4)² + 0 + (6-4)² + (15-17)² + (19-17)² = 16.
Sơ đồ sau là toàn bộ vòng lặp, kể cả bước chuẩn hóa phải làm trước.
Đang tải sơ đồ…
Cách đọc hình: bắt đầu từ ô cyan là bảng vector khách hàng, mỗi khách một hàng. Hai ô tím là việc chuẩn bị: chuẩn hóa từng cột rồi chọn k tâm ban đầu. Hai ô xanh dương là vòng lặp chính, gán rồi dời tâm. Ô cam hỏi có điểm nào đổi cụm không: có thì quay lại bước gán, không thì dừng và trả về nhãn cụm của từng khách, vị trí các tâm và inertia (ô xanh lá).
Hình chưa cho thấy hai điều: inertia giảm thế nào qua từng nửa bước, và một khách quá khác thường làm gì với các tâm. Animation dưới đây chạy đúng năm khách ở trên. Bấm nút biến thể để thêm Tạp Hóa Cô Tấm, một cửa hàng đặt 60 đơn mỗi tháng.
Đang tải Animation…
Ở chế độ đầu, k-means dừng sau hai vòng với hai nhóm đúng như mắt thường thấy: khách ít đơn và khách nhiều đơn. Ở biến thể, tâm 2 bị cửa hàng 60 đơn kéo lên dần (20,8 rồi 31,33 rồi 39,5 rồi 60), từng khách một bị đẩy sang tâm 1, cuối cùng Cô Tấm ngồi một mình một cụm và An với Hà bị gộp chung. Tâm là trung bình, mà trung bình rất nhạy với giá trị ngoại lai (outlier). Bài 4 của chương sẽ học cách tìm những điểm như vậy trước.
Làm cùng mình
Code nằm trong thư mục m01/c08/examples của kho lab, chạy từ chính thư mục đó. Dữ liệu là orders.parquet sinh ở Chương 1.
Bước 1: chạy lại ví dụ năm khách bằng code, có và không có Cô Tấm.
def lloyd_1d(x: np.ndarray, centers: np.ndarray, max_iter: int = 10) -> np.ndarray:
labels = None
for it in range(1, max_iter + 1):
dist = np.abs(x[:, None] - centers[None, :]) # (n, k): khoảng cách tới từng tâm
new_labels = dist.argmin(axis=1)
if labels is not None and np.array_equal(new_labels, labels):
print(f"iter {it}: no customer changed cluster, stop")
break
labels = new_labels
inertia = ((x - centers[labels]) ** 2).sum()
print(f"iter {it}: assign {labels.tolist()} inertia {inertia:.1f}")
centers = np.array([x[labels == j].mean() for j in range(len(centers))])
inertia = ((x - centers[labels]) ** 2).sum()
print(f" move centers to {centers.round(2).tolist()} inertia {inertia:.1f}")
return labels
| Dòng code | Máy làm gì | Vì sao |
|---|---|---|
x[:, None] - centers[None, :] | Tạo bảng (n, k) hiệu giữa mỗi khách và mỗi tâm | Broadcasting thay cho hai vòng lặp lồng nhau |
dist.argmin(axis=1) | Lấy chỉ số tâm gần nhất cho mỗi khách | Đây là bước gán |
np.array_equal(new_labels, labels) | So nhãn mới với nhãn cũ | Không ai đổi cụm thì dừng |
x[labels == j].mean() | Trung bình các khách thuộc cụm j | Đây là bước dời tâm |
((x - centers[labels]) ** 2).sum() | Cộng bình phương khoảng cách tới tâm của mình | Đây là inertia |
Kết quả [Đã chạy] Python 3.12, NumPy 2.5, pandas 3.1, scikit-learn 1.9, CPU x86_64, ngày 04/11/2026:
customers: {'An': 2.0, 'Binh': 4.0, 'Lan': 6.0, 'Minh': 15.0, 'Ha': 19.0}
iter 1: assign [0, 1, 1, 1, 1] inertia 350.0
move centers to [2.0, 11.0] inertia 154.0
iter 2: assign [0, 0, 0, 1, 1] inertia 100.0
move centers to [4.0, 17.0] inertia 16.0
iter 3: no customer changed cluster, stop
final: {'An': 0, 'Binh': 0, 'Lan': 0, 'Minh': 1, 'Ha': 1}
add Tap Hoa Co Tam with 60 orders per month
iter 1: assign [0, 1, 1, 1, 1, 1] inertia 3486.0
move centers to [2.0, 20.8] inertia 2074.8
iter 2: assign [0, 0, 0, 1, 1, 1] inertia 1593.5
move centers to [4.0, 31.33] inertia 1248.7
iter 3: assign [0, 0, 0, 0, 1, 1] inertia 1102.9
move centers to [6.75, 39.5] inertia 939.2
iter 4: assign [0, 0, 0, 0, 0, 1] inertia 669.1
move centers to [9.2, 60.0] inertia 218.8
iter 5: no customer changed cluster, stop
final: {'An': 0, 'Binh': 0, 'Lan': 0, 'Minh': 0, 'Ha': 0, 'CoTam': 1}
Các con số khớp bảng tính tay ở trên. Phần sau cho thấy cái giá của một điểm ngoại lai: inertia cuối là 218,8, gấp hơn 13 lần so với 16, và nhóm "khách nhiều đơn" Minh, Hà biến mất vào cụm của An.
Bước 2: tự viết k-means cho dữ liệu thật, rồi so với scikit-learn.
Hàm load_profiles trong file trợ giúp _profiles.py dựng bảng hồ sơ khách từ đơn hàng bằng groupby như ở Chương 2:
def load_profiles(min_orders: int = 3) -> pd.DataFrame:
orders = pd.read_parquet(DATA / "orders.parquet")
orders = orders.assign(is_cod=orders["payment"] == "cod")
profiles = orders.groupby("customer_id").agg(
n_orders=("order_id", "size"),
avg_items=("items", "mean"),
avg_value=("order_value_vnd", "mean"),
cod_share=("is_cod", "mean"),
)
# cần ít nhất vài đơn thì tỉ lệ COD mới có nghĩa
return profiles[profiles["n_orders"] >= min_orders]
Điều kiện từ 3 đơn trở lên có lý do: một khách chỉ có 1 đơn thì tỉ lệ COD chỉ có thể là 0 hoặc 1, chưa nói được gì về thói quen. Hàm standardize trong cùng file trừ trung bình rồi chia độ lệch chuẩn theo từng cột, đúng như Chương 3.
def kmeans(Z: np.ndarray, k: int, seed: int = 0, max_iter: int = 50):
rng = np.random.default_rng(seed)
centers = Z[rng.choice(len(Z), size=k, replace=False)] # k khách ngẫu nhiên làm tâm
for it in range(max_iter):
d2 = ((Z[:, None, :] - centers[None, :, :]) ** 2).sum(axis=2) # (n, k)
labels = d2.argmin(axis=1)
new = np.array([Z[labels == j].mean(axis=0) for j in range(k)])
if np.allclose(new, centers):
break
centers = new
inertia = ((Z - centers[labels]) ** 2).sum()
return labels, centers, inertia, it + 1
| Dòng code | Máy làm gì | Vì sao |
|---|---|---|
rng.choice(len(Z), size=k, replace=False) | Chọn k khách ngẫu nhiên làm tâm ban đầu | Cách khởi tạo đơn giản nhất |
Z[:, None, :] - centers[None, :, :] | Hiệu shape (6141, 3, 4) | Mỗi khách với mỗi tâm, theo cả 4 chiều |
.sum(axis=2) | Cộng theo trục đặc trưng | Ra bình phương khoảng cách shape (6141, 3) |
np.allclose(new, centers) | Tâm gần như không dời nữa | Điều kiện dừng tương đương "không ai đổi cụm" |
Kết quả [Đã chạy] Python 3.12, NumPy 2.5, pandas 3.1, scikit-learn 1.9, CPU x86_64, ngày 04/11/2026:
Z shape: (6141, 4)
own k-means : 11 iterations, inertia 5417.9, sizes [1186, 3025, 1930]
scikit-learn: 6 iterations, inertia 5417.9, sizes [3025, 1186, 1930]
Bản tự viết và scikit-learn cho cùng inertia 5.417,9 và cùng kích thước ba cụm (1.186, 3.025, 1.930), chỉ khác thứ tự đánh số. Số thứ tự cụm là tùy ý: cụm 0 của bản này là cụm 1 của bản kia. scikit-learn hội tụ sau ít vòng hơn, một phần vì nó chọn tâm ban đầu thông minh hơn (k-means++, xem bước 4).
Bước 3: đọc hồ sơ từng cụm, và tình huống thất bại khi quên chuẩn hóa.
def profile_table(labels: np.ndarray) -> pd.DataFrame:
table = profiles.groupby(labels)[FEATURES].mean()
table.insert(0, "size", np.bincount(labels))
# sắp cụm theo giá trị đơn để hai bảng dễ so
return table.sort_values("avg_value").round({"n_orders": 1, "avg_items": 2, "avg_value": -3, "cod_share": 2})
for name, data in [("standardized", standardize(X)), ("raw units", X)]:
labels = KMeans(n_clusters=3, n_init=10, random_state=0).fit_predict(data)
print(f"--- k=3 on {name} ---")
print(profile_table(labels).to_string())
Kết quả [Đã chạy] Python 3.12, NumPy 2.5, pandas 3.1, scikit-learn 1.9, CPU x86_64, ngày 04/11/2026:
--- k=3 on standardized ---
size n_orders avg_items avg_value cod_share
0 3025 5.5 2.21 287000.0 0.32
2 1930 4.9 2.21 288000.0 0.75
1 1186 17.8 6.97 908000.0 0.45
--- k=3 on raw units ---
size n_orders avg_items avg_value cod_share
0 3518 5.3 2.06 229000.0 0.49
2 1496 5.9 2.79 435000.0 0.48
1 1127 17.5 6.90 928000.0 0.46
Đọc bảng trên (đã chuẩn hóa): một cụm 1.186 khách đặt 17,8 đơn, mỗi đơn 6,97 món, giá trị 908.000 đồng, rõ ràng là các cửa hàng đặt hàng đều đặn. Hai cụm còn lại có số món và giá trị gần như y hệt nhau (2,21 món, khoảng 287.000 đồng) nhưng khác hẳn ở tỉ lệ COD: 0,32 so với 0,75. Máy đã tự tìm ra một nhóm 1.930 khách thích trả tiền khi nhận hàng, điều không ai chỉ cho nó.
Bảng dưới (số thô) kể một câu chuyện khác. Ba cụm có giá trị đơn 229.000, 435.000 và 928.000 đồng, còn tỉ lệ COD gần như bằng nhau (0,49; 0,48; 0,46). Đây chính là cách chia theo giá của Minh, chỉ là do máy vẽ ranh giới. Lý do giống hệt Chương 3: độ lệch chuẩn của avg_value là khoảng 277.000 đồng, của cod_share chỉ khoảng 0,24, nên trong khoảng cách Euclid cột tiền lấn át mọi cột khác. Câu trả lời cho câu hỏi mở bài: có chuẩn hóa, máy tách theo quy mô đơn và thói quen trả tiền; quên chuẩn hóa, máy chỉ cắt theo giá trị đơn.
Bước 4: khởi tạo xấu làm k-means kẹt ở nghiệm kém.
runs = [KMeans(n_clusters=3, init="random", n_init=1, random_state=s).fit(Z) for s in range(10)]
print("init=random, n_init=1, 10 seeds:")
print(" inertia:", [round(r.inertia_) for r in runs])
worst = max(runs, key=lambda r: r.inertia_)
print(" worst run, mean n_orders per cluster:",
profiles.groupby(worst.labels_)["n_orders"].mean().round(1).tolist(),
"sizes", np.bincount(worst.labels_).tolist())
best = KMeans(n_clusters=3, init="k-means++", n_init=10, random_state=0).fit(Z)
print(f"init=k-means++, n_init=10: inertia {best.inertia_:.0f}")
Kết quả [Đã chạy] Python 3.12, NumPy 2.5, pandas 3.1, scikit-learn 1.9, CPU x86_64, ngày 04/11/2026:
init=random, n_init=1, 10 seeds:
inertia: [5491, 8828, 8825, 5418, 8825, 5491, 5418, 5418, 5491, 5418]
worst run, mean n_orders per cluster: [5.3, 14.8, 21.7] sizes [4953, 670, 518]
init=k-means++, n_init=10: inertia 5418
Mười lần chạy với tâm ban đầu chọn ngẫu nhiên cho ba mức inertia: 5.418 (tốt nhất), 5.491 và khoảng 8.825. Lần tệ nhất chia đôi nhóm cửa hàng (14,8 và 21,7 đơn) và nhét 4.953 khách còn lại vào một cụm. Mỗi lần lặp, inertia vẫn giảm, nhưng chỉ giảm tới đáy gần nhất: đó là cực tiểu cục bộ (local minimum). Cách sửa là k-means++ (tâm ban đầu được chọn xa nhau theo xác suất) cộng với chạy nhiều lần rồi giữ lần có inertia nhỏ nhất, chính là tham số n_init=10.
Áp dụng vào Giao Hàng Anh Tí
Với ba cụm từ dữ liệu đã chuẩn hóa, Lan có thể đặt tên tạm và gắn một hành động cho mỗi cụm:
| Cụm | Số khách | Đặc điểm | Hành động gợi ý |
|---|---|---|---|
| Cửa hàng đặt nhiều | 1.186 | 17,8 đơn, 6,97 món, 908.000 đồng | Gói giá theo sản lượng tháng |
| Khách ưa COD | 1.930 | 4,9 đơn, tỉ lệ COD 0,75 | Ưu đãi nhỏ khi chuyển sang trả trước |
| Khách lẻ trả trước | 3.025 | 5,5 đơn, tỉ lệ COD 0,32 | Mã giảm giá để tăng tần suất đặt |
Đây mới là bản nháp. Bài sau trả lời câu hỏi Lan chắc chắn sẽ hỏi: "Sao lại là 3 cụm mà không phải 2 hay 5?".
Hiểu lầm thường gặp
Hiểu lầm 1: "Số thứ tự cụm có ý nghĩa." Sai ở chỗ số thứ tự chỉ phụ thuộc thứ tự tâm được khởi tạo. Ở bước 2, cùng một cách chia nhưng cụm cửa hàng là số 0 ở bản tự viết và số 1 ở scikit-learn. Luôn đặt tên cụm bằng bảng hồ sơ, không bằng số.
Hiểu lầm 2: "K-means tìm ra các nhóm thật trong dữ liệu." Sai ở chỗ k-means luôn trả về đúng k cụm, kể cả khi dữ liệu không có nhóm nào. Nó chia theo khoảng cách, không biết khách là văn phòng hay gia đình. Bộ dữ liệu mô phỏng có nhóm ẩn segment_true, và hai nhóm văn phòng, gia đình chỉ khác nhau ở số đơn nên không cụm nào tách được chúng.
Hiểu lầm 3: "Chạy một lần là đủ." Sai ở chỗ k-means chỉ tìm cực tiểu cục bộ. Ở bước 4, 3 trên 10 lần chạy với init="random" cho inertia khoảng 8.825, cao hơn 63% so với nghiệm tốt nhất.
Hiểu lầm 4: "Có cột tỉ lệ nên không cần chuẩn hóa." Sai ở chỗ các cột tỉ lệ (0 đến 1) đứng cạnh cột tiền (hàng trăm nghìn) thì gần như không đóng góp gì vào khoảng cách. Ở bước 3, ba cụm trên số thô có tỉ lệ COD 0,49; 0,48; 0,46.
| Triệu chứng | Nguyên nhân | Cách kiểm chứng | Cách sửa |
|---|---|---|---|
| Các cụm chỉ khác nhau ở một cột | Cột đó có thang đo lớn nhất | In X.std(axis=0) | Chuẩn hóa trước khi phân cụm |
| Chạy lại ra kết quả khác hẳn | Khởi tạo ngẫu nhiên, kẹt cực tiểu cục bộ | Chạy với vài random_state, so inertia | init="k-means++", n_init=10, cố định random_state |
| Một cụm chỉ có vài khách | Điểm ngoại lai kéo tâm | Xem kích thước cụm bằng np.bincount(labels) | Xử lý ngoại lai trước (bài 4) |
| Báo cáo tuần này đổi tên cụm so với tuần trước | Số thứ tự cụm là tùy ý | So bảng hồ sơ, không so số | Đặt tên theo đặc điểm, lưu tâm cụm đã fit |
Tự kiểm tra (có đáp án)
Câu 1 (giải thích). Vì sao bước dời tâm không bao giờ làm inertia tăng?
Đáp án: với một nhóm điểm cố định, điểm làm tổng bình phương khoảng cách nhỏ nhất chính là trung bình của nhóm. Dời tâm tới trung bình chỉ có thể giữ nguyên hoặc giảm tổng đó, như từ 350 xuống 154 ở ví dụ năm khách.
Câu 2 (dự đoán). Năm khách An 2, Bình 4, Lan 6, Minh 15, Hà 19 đơn mỗi tháng. Nếu đặt hai tâm ban đầu ở 15 và 19, kết quả cuối có khác không?
Đáp án: không. Gán lần 1: An, Bình, Lan, Minh về tâm 15, Hà về tâm 19. Dời tâm: 6,75 và 19. Gán lần 2: Minh cách 6,75 là 8,25 nhưng cách 19 chỉ 4, nên sang cụm của Hà. Dời tâm: 4 và 17, đúng nghiệm cũ. Với dữ liệu đơn giản này chỉ có một nghiệm ổn định; dữ liệu 6.141 khách thì có nhiều.
Câu 3 (vận dụng). Minh muốn thêm cột avg_km (quãng đường trung bình) vào hồ sơ khách. Cần làm gì trước khi chạy lại k-means, và làm sao biết cột này có giúp tách khách không?
Đáp án: chuẩn hóa cả năm cột, chạy lại với n_init=10, rồi xem bảng hồ sơ: nếu avg_km gần như bằng nhau ở mọi cụm (dữ liệu mô phỏng cho khoảng 5,3 km ở mọi nhóm), cột này không mang thông tin về nhóm khách và chỉ thêm nhiễu.
Tự tay làm
Mở kmeans_customers.py, đổi n_clusters=3 thành n_clusters=2 ở cả hai lần chạy. Trước khi chạy, dự đoán: với dữ liệu đã chuẩn hóa, cụm nào sẽ bị gộp?
Kết quả mong đợi để tự kiểm: dữ liệu chuẩn hóa cho hai cụm 4.955 và 1.186 khách; cụm lớn có tỉ lệ COD khoảng 0,49 vì nhóm ưa COD và nhóm trả trước đã bị gộp. Sau đó thêm một dòng in np.bincount(labels) cho bản số thô và giải thích bằng một câu vì sao các cụm ở đó vẫn cắt theo avg_value.
Điều cần nhớ
- Học không giám sát tìm cấu trúc khi không có nhãn; phân cụm chia điểm sao cho cùng cụm thì gần nhau.
- K-means lặp hai bước: gán mỗi điểm về tâm gần nhất, dời mỗi tâm về trung bình cụm. Inertia không tăng qua từng bước nên thuật toán luôn dừng, nhưng có thể dừng ở cực tiểu cục bộ.
- Luôn chuẩn hóa trước, dùng k-means++ với
n_initlớn hơn 1, và đặt tên cụm bằng bảng hồ sơ chứ không bằng số thứ tự. - Tâm là trung bình nên rất nhạy với ngoại lai. Bài tiếp theo trả lời câu hỏi còn bỏ ngỏ: chọn k bằng số đo nào khi không có đáp án để so.
Nguồn tham khảo
- scikit-learn, Clustering, mục K-means: https://scikit-learn.org/stable/modules/clustering.html
- scikit-learn,
sklearn.cluster.KMeans: https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html - scikit-learn, Demonstration of k-means assumptions: https://scikit-learn.org/stable/auto_examples/cluster/plot_kmeans_assumptions.html
- Arthur và Vassilvitskii (2007), k-means++: The Advantages of Careful Seeding: https://theory.stanford.edu/~sergei/papers/kMeansPP-soda.pdf
- AI Engineering from Scratch (MIT), phase 02, Unsupervised learning: https://github.com/rohitg00/ai-engineering-from-scratch/tree/main/phases/02-ml-fundamentals/07-unsupervised-learning