Khóa học
K-means là một kỹ thuật học máy không giám sát phổ biến, cho phép xác định các cụm (nhóm điểm dữ liệu tương đồng) trong dữ liệu. Trong hướng dẫn này, bạn sẽ tìm hiểu về phân cụm k-means trong R bằng tidymodels, ggplot2 và ggmap. Chúng ta sẽ cùng tìm hiểu:
- cách thuật toán phân cụm k-means hoạt động
- cách trực quan hóa dữ liệu để xác định liệu nó có phù hợp cho phân cụm hay không
- một nghiên cứu tình huống về huấn luyện và tinh chỉnh mô hình k-means bằng bộ dữ liệu đánh giá Airbnb
Trước khi đi sâu hơn, chúng tôi khuyến nghị bạn ôn lại những kiến thức cơ bản về Lập trình R, như vector và data frame. Nếu bạn đã sẵn sàng, hãy bỏ qua và bắt đầu ngay!
Tổng quan về Phân cụm k-Means
Các mô hình phân cụm nhằm nhóm dữ liệu thành các “cụm” hay nhóm riêng biệt. Điều này có thể được dùng như một phân tích độc lập, hoặc dùng làm đặc trưng trong một thuật toán học có giám sát.

Ở phía bên trái của sơ đồ trên, ta thấy 2 tập điểm riêng biệt chưa có nhãn và được tô màu như những điểm dữ liệu tương tự nhau. Khớp một mô hình k-means cho dữ liệu này (phía bên phải) có thể làm lộ ra 2 nhóm riêng (thể hiện bằng cả các vòng tròn và màu sắc khác nhau).
Trong không gian hai chiều, con người dễ dàng tách các cụm này, nhưng với nhiều chiều hơn, bạn cần dùng mô hình.
Thuật toán k-means
Hãy tưởng tượng bạn muốn làm vài món salad trái cây, mỗi món gồm các loại trái cây tương tự nhau.
- Bạn quyết định số món salad trái cây muốn làm (k). Ví dụ, bạn quyết định làm 3 món.
- Bạn chọn ngẫu nhiên 3 loại trái cây trên bàn làm các "đại diện" ban đầu cho mỗi món salad.
- Giờ bạn đi quanh bàn và gán mỗi loại trái cây vào món salad có đại diện giống nó nhất. Ví dụ, táo có thể đi với lê, cam với quýt, và chuối với chuối dầu.
- Khi đã gán xong, bạn tìm "trung tâm" mới của mỗi món salad bằng cách tính trung bình các đặc trưng trái cây trong mỗi nhóm (ví dụ: kích thước, màu sắc, vị). Những trung tâm mới này trở thành các đại diện được cập nhật.
- Bạn lặp lại bước 3 và 4 vài lần, gán trái cây cho đại diện giống nhất và cập nhật đại diện mỗi lần cho đến khi các nhóm không còn thay đổi.

Một cách tổng quát, bạn:
- Chọn số cụm, k.
- Chọn ngẫu nhiên k điểm (cụm kích thước 1).
- Tính khoảng cách giữa mỗi điểm và trọng tâm rồi gán mỗi điểm dữ liệu vào cụm gần nhất.
- Tính trọng tâm (vị trí trung bình) cho mỗi cụm.
- Tiếp tục lặp lại bước 3–4 cho đến khi các cụm không đổi hoặc đạt số vòng lặp tối đa.
Vì k-means chọn ngẫu nhiên các tâm cụm, thuật toán cần được chạy một số lần do người dùng đặt. Bằng cách này, ta sẽ đạt được nghiệm tốt nhất bằng cách tối thiểu hóa thước đo chất lượng mô hình. Thước đo này gọi là tổng bình phương sai số trong cụm (WCSS), tức tổng khoảng cách giữa các điểm dữ liệu và trọng tâm tương ứng của mỗi cụm. Thực vậy, thước đo chất lượng mô hình càng nhỏ thì ta càng gần với mô hình tốt nhất.
Bộ dữ liệu Danh sách Airbnb
Tất cả nội dung của hướng dẫn này sẽ xoay quanh các danh sách cho thuê trên Airbnb ở Cape Town, có sẵn trên DataLab. Dữ liệu chứa nhiều loại thông tin khác nhau, như chủ nhà, giá, số lượng đánh giá, tọa độ, v.v.
Ngay cả khi bộ dữ liệu có vẻ cung cấp thông tin sâu về chỗ nghỉ, vẫn có những câu hỏi rõ ràng có thể trả lời. Ví dụ, hãy tưởng tượng Airbnb yêu cầu các nhà khoa học dữ liệu của mình nghiên cứu phân khúc các danh sách trên nền tảng này tại Cape Town.
Mỗi thành phố đều khác nhau và có những nhu cầu riêng tùy thuộc vào văn hóa của cư dân. Việc xác định các cụm ở thành phố này có thể hữu ích để khai thác những hiểu biết mới, giúp tăng mức độ hài lòng của khách hàng hiện tại với các Chiến lược Trung thành Khách hàng phù hợp, tránh rời bỏ khách hàng. Đồng thời, việc thu hút người dùng mới với các chương trình khuyến mãi phù hợp cũng rất quan trọng.
Trực quan hóa dữ liệu Airbnb
Trước khi áp dụng k-means, chúng ta muốn tìm hiểu thêm về mối quan hệ giữa các biến bằng cách xem ma trận tương quan. Để tiện hiển thị, chúng ta làm tròn đến hai chữ số thập phân.
library(dplyr)
airbnb |>
select(price, minimum_nights, number_of_reviews) |>
cor(use = "pairwise.complete.obs") |>
round(2)

Từ đầu ra, có vẻ tồn tại mối quan hệ âm nhẹ giữa giá và số lượng đánh giá: Giá càng cao thì số đánh giá càng thấp. Tương tự với số đêm tối thiểu và số lượng đánh giá. Vì số đêm tối thiểu không ảnh hưởng lớn đến giá, chúng ta sẽ chỉ muốn tìm hiểu thêm về cách giá và số lượng đánh giá liên quan với nhau:
library(ggplot2)
ggplot(data, aes(number_of_reviews, price, color = room_type, shape = room_type)) +
geom_point(alpha = 0.25) +
xlab("Number of reviews") +
ylab("Price")

Biểu đồ phân tán cho thấy chi phí chỗ ở, đặc biệt là nhà nguyên căn, cao hơn khi số lượng đánh giá ít, và có xu hướng giảm khi số đánh giá tăng.
Chuẩn hóa
Trước khi khớp mô hình, còn một bước nữa. k-means nhạy với các biến có đơn vị không tương đồng, dẫn đến kết quả gây hiểu lầm. Trong ví dụ này, số lượng đánh giá là hàng chục hoặc hàng trăm, nhưng giá là hàng chục nghìn. Nếu không xử lý dữ liệu, khác biệt về giá sẽ trông lớn hơn khác biệt về đánh giá, trong khi chúng ta muốn các biến này được đối xử công bằng.
Để tránh vấn đề này, các biến cần được biến đổi về cùng một thang đo. Như vậy, chúng có thể được so sánh đúng đắn bằng số đo khoảng cách.
Có nhiều phương pháp để xử lý. Phổ biến và thường dùng nhất là chuẩn hóa, gồm trừ đi giá trị trung bình khỏi giá trị đặc trưng và sau đó chia cho độ lệch chuẩn. Kỹ thuật này giúp thu được các đặc trưng có trung bình 0 và độ lệch chuẩn 1.
Bạn có thể scale các biến bằng hàm scale(). Vì hàm này trả về ma trận, mã sẽ gọn hơn khi dùng phong cách base R thay vì tidyverse.
airbnb[, c("price", "number_of_reviews")] = scale(airbnb[, c("price", "number_of_reviews")])
Khớp và đánh giá mô hình
Cuối cùng chúng ta có thể xác định các cụm danh sách bằng k-means. Để bắt đầu, hãy thử thực hiện k-means với 3 cụm và nstart bằng 20. Tham số cuối này dùng để chạy k-means với 20 khởi tạo ngẫu nhiên khác nhau và sau đó R sẽ tự động chọn kết quả tốt nhất theo tổng bình phương sai số trong cụm. Chúng ta cũng đặt seed để tái lập cùng kết quả mỗi lần chạy mã.
# Get the two columns of interest
airbnb_2cols <- data[, c("price", "number_of_reviews")]
set.seed(123)
km.out <- kmeans(airbnb_2cols, centers = 3, nstart = 20)
km.out
Đầu ra:
K-means clustering with 3 clusters of sizes 785, 37, 16069
Cluster means:
price number_of_reviews
1 14264.102 5.9401274
2 83051.541 0.6756757
3 1589.879 18.2649200
Clustering vector:
[1] 3 3 3 3 3 3 3 3 1 3 3 3 3 3 3 3 3 1 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
[37] 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
[73] 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
[109] 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 1 3 3 3 3 3 3 3 3 3 3 3 3 3
[145] 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 1 1 3 3
...
[16777] 3 3 3 3 3 3 3 3 3 3 1 3 3 1 3 1 3 3 3 3 3 1 3 3 3 3 3 3 3 3 1 3 3 3 3 3
[16813] 3 1 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 1 3
[16849] 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 1 3 3 3 3 3 3 3 3 3 3 3
[16885] 3 3 3 3 3 3 3
Within cluster sum of squares by cluster:
[1] 41529148852 49002793251 33286433394
(between_SS / total_SS = 74.5 %)
Available components:
[1] "cluster" "centers" "totss" "withinss" "tot.withinss"
[6] "betweenss" "size" "iter" "ifault"
Từ đầu ra, ta thấy ba cụm khác nhau đã được tìm thấy với kích thước 785, 37 và 16069. Với mỗi cụm, các khoảng cách bình phương giữa các quan sát và trọng tâm được tính. Vì vậy, mỗi quan sát sẽ được gán vào một trong ba cụm.
Dù có vẻ là kết quả tốt, cách tốt nhất để tìm mô hình tối ưu là thử các mô hình với số cụm khác nhau. Ta cần bắt đầu từ mô hình có một cụm, sau đó thử mô hình có hai cụm, v.v. Toàn bộ quy trình này cần được theo dõi bằng một biểu đồ, gọi là scree plot, trong đó số cụm trên trục x, còn WCSS trên trục y.
Trong nghiên cứu tình huống này, chúng ta xây 10 mô hình k-means, mỗi mô hình sẽ có số cụm khác nhau, tối đa 10 cụm. Thêm vào đó, chúng ta chỉ dùng một phần của bộ dữ liệu: chỉ gồm giá và số lượng đánh giá. Để vẽ scree plot, ta cần lưu tổng bình phương sai số trong cụm của tất cả mô hình vào biến wss.
# Decide how many clusters to look at
n_clusters <- 10
# Initialize total within sum of squares error: wss
wss <- numeric(n_clusters)
set.seed(123)
# Look over 1 to n possible clusters
for (i in 1:n) {
# Fit the model: km.out
km.out <- kmeans(airbnb_2cols, centers = i, nstart = 20)
# Save the within cluster sum of squares
wss[i] <- km.out$tot.withinss
}
# Produce a scree plot
wss_df <- tibble(clusters = 1:n, wss = wss)
scree_plot <- ggplot(wss_df, aes(x = clusters, y = wss, group = 1)) +
geom_point(size = 4)+
geom_line() +
scale_x_continuous(breaks = c(2, 4, 6, 8, 10)) +
xlab('Number of clusters')
scree_plot

Quan sát scree plot, ta thấy tổng bình phương sai số trong cụm giảm khi số cụm tăng. Tiêu chí chọn số cụm là tìm "khuỷu tay" sao cho bạn xác định được điểm mà sau khi thêm một cụm nữa thì WCSS giảm chậm hơn nhiều. Trong trường hợp này chưa rõ ràng, nên ta sẽ thêm các đường ngang để có cái nhìn rõ hơn:
scree_plot +
geom_hline(
yintercept = wss,
linetype = 'dashed',
col = c(rep('#000000',4),'#FF0000', rep('#000000', 5))
)

Nếu nhìn lại, quyết định có vẻ rõ ràng hơn nhiều, phải không? Từ trực quan này, ta có thể nói lựa chọn tốt nhất là đặt số cụm bằng 5. Sau k=5, mức cải thiện của các mô hình giảm đáng kể.
# Select number of clusters
k <- 5
set.seed(123)
# Build model with k clusters: km.out
km.out <- kmeans(airbnb_2cols, centers = k, nstart = 20)
Ta có thể lại trực quan hóa biểu đồ phân tán giữa giá và số lượng đánh giá. Chúng ta cũng tô màu các điểm theo id cụm:
data$cluster_id <- factor(km.out$cluster)
ggplot(data, aes(number_of_reviews, price, color = cluster_id)) +
geom_point(alpha = 0.25) +
xlab("Number of reviews") +
ylab("Price")

Ta có thể quan sát rằng:
- Có một vài danh sách giá cao và ít đánh giá, tô màu xanh lá và hồng ánh tím
- Cũng có một nhóm điểm dữ liệu vừa giá thấp vừa ít đánh giá, đánh dấu màu đỏ
- Sau đó, giá dường như ổn định ngay khi số lượng đánh giá tăng.
Khi nào K-Means thất bại?
Lưu ý rằng k-means đơn giản và dễ áp dụng, nhưng không phải lúc nào cũng là lựa chọn tốt để phân nhóm dữ liệu vì có thể thất bại. Thuật toán giả định các cụm có dạng cầu và vì vậy hoạt động tốt với các trường hợp này, trong khi các nhóm có kích thước và mật độ khác nhau thường không được thuật toán này bắt tốt.
Khi các điều kiện này không thỏa mãn, nên cân nhắc các phương pháp thay thế khác như DBSCAN và BIRCH. Clustering in Machine Learning: 5 Essential Clustering Algorithms cung cấp một cái nhìn tổng quan tuyệt vời về các phương pháp phân cụm nếu bạn muốn đào sâu.
Nâng tầm kiến thức
Chúng ta có thể kết luận rằng k-means vẫn là một trong những thuật toán phân cụm được dùng nhiều nhất để xác định các nhóm con riêng biệt, dù không phải lúc nào cũng hoàn hảo trong mọi tình huống. Giờ đây, bạn đã có kiến thức để áp dụng nó với R trong các nghiên cứu tình huống khác
Nếu bạn muốn học sâu hơn về phương pháp này, hãy xem khóa học Unsupervised Learning in R. Cũng có Cluster Analysis in R và An Introduction to Hierarchical Clustering in Python để có cái nhìn đầy đủ về các phương pháp phân cụm hiện có, hữu ích khi k-means chưa đủ để mang lại insight có ý nghĩa từ dữ liệu của bạn. Trong trường hợp bạn cũng muốn khám phá các mô hình giám sát với R, khóa học này được khuyến nghị!
