Hệ khuyến nghị
Ý tưởng cốt lõi
Netflix có khoảng 17 nghìn phim, Spotify có hàng trăm triệu bài hát, Amazon có hàng trăm triệu sản phẩm. Người dùng không thể duyệt qua tất cả, họ cần ai đó gợi ý cái phù hợp với mình. Hệ khuyến nghị (recommender system) là thuật toán dự đoán người dùng sẽ thích cái gì, dựa trên hành vi quá khứ và hồ sơ của họ.
Đây là một trong những ứng dụng thương mại thành công nhất của học máy. Một vài con số đáng chú ý: khoảng 80 phần trăm nội dung xem trên Netflix đến từ gợi ý, khoảng 35 phần trăm doanh thu của Amazon đến từ mục "khách hàng đã mua món này cũng mua", và gần như toàn bộ thời gian dùng TikTok đến từ feed "For You" do hệ thống gợi ý.
Ma trận người dùng và vật phẩm
Dữ liệu nền tảng của hầu hết hệ khuyến nghị là ma trận đánh giá, thường gọi là ma trận người dùng và vật phẩm (user-item matrix). Mỗi hàng là một người dùng, mỗi cột là một vật phẩm, mỗi ô là điểm đánh giá mà người dùng đó dành cho vật phẩm đó (ví dụ thang điểm 1 tới 5 sao).
| Người dùng | Phim A | Phim B | Phim C | Phim D |
|---|---|---|---|---|
| An | 5 | 3 | ? | 1 |
| Bình | 4 | ? | ? | 1 |
| Châu | 1 | ? | 5 | 4 |
| Dũng | ? | 2 | 4 | ? |
Mục tiêu của hệ khuyến nghị là điền vào các ô đang để trống (dấu hỏi), tức dự đoán điểm cho những cặp người dùng và vật phẩm chưa quan sát được, rồi gợi ý những vật phẩm có điểm dự đoán cao nhất.
Bài toán này khó vì vài lý do:
- Ma trận rất thưa. Với Netflix, khoảng 99 phần trăm số ô là trống.
- Quy mô lớn. Số người dùng và số vật phẩm có thể lên tới hàng trăm triệu.
- Khởi đầu lạnh (cold-start). Người dùng mới chưa đánh giá gì, vật phẩm mới chưa ai đánh giá.
- Thiên lệch dữ liệu. Người dùng chỉ đánh giá những thứ họ chủ động chọn xem, không phải một mẫu ngẫu nhiên.
Độ tương tự cosine
Hầu hết các phương pháp dưới đây đều cần đo "hai thứ giống nhau bao nhiêu". Công cụ phổ biến nhất là độ tương tự cosine (cosine similarity). Với hai vector a và b, cosine bằng tích vô hướng của chúng chia cho tích độ dài hai vector. Viết bằng lời: cos(a, b) = (a chấm b) / (độ dài a nhân độ dài b).
Giá trị nằm trong khoảng từ trừ 1 tới 1. Với các vector không âm (ví dụ đặc trưng nhị phân, đếm từ, hay TF-IDF) thì cosine nằm trong khoảng từ 0 tới 1, càng gần 1 nghĩa là hai vector càng giống nhau về hướng. Ý nghĩa hình học: cosine đo góc giữa hai vector, không quan tâm độ dài, nên nó so sánh "xu hướng" thay vì "độ lớn".
Thử ngay: gợi ý phim từ ma trận đánh giá
Hãy thử trực tiếp. Bấm chọn một người dùng trong ma trận dưới đây để xem hệ tính cosine với những người còn lại (chỉ trên các phim cả hai đã đánh giá), chọn người tương đồng nhất, rồi gợi ý một phim mà người đó chưa xem. Chọn người dùng mới chưa có đánh giá nào để thấy vì sao lối gợi ý dựa trên người giống mình, tên gọi là lọc cộng tác, chịu chết khi người dùng chưa để lại dấu vết nào, tình huống gọi là khởi đầu lạnh.
| Người / Phim | Interstellar | Avengers | Titanic | La La Land | Inception |
|---|---|---|---|---|---|
| An | 5 | 4 | – | 2 | – |
| Bình | 4 | 5 | 1 | – | 5 |
| Châu | 1 | 2 | 5 | 4 | – |
| Dũng | 2 | – | 4 | 5 | 3 |
| Emi (mới) | – | – | – | – | – |
Lọc cộng tác (collaborative filtering)
Lọc cộng tác không cần biết gì về nội dung vật phẩm. Nó chỉ cần ma trận đánh giá. Ý tưởng cốt lõi: dùng hành vi của số đông để suy ra sở thích cá nhân. Có hai biến thể.
Lọc cộng tác theo người dùng
Lọc theo người dùng (user-based CF) tìm những người dùng giống với người mục tiêu, rồi gợi ý những vật phẩm mà nhóm người giống đó thích.
- Tính độ tương tự giữa người mục tiêu và mọi người dùng khác đã đánh giá vật phẩm đang xét, dùng cosine trên hai vector đánh giá của họ.
- Chọn ra
kngười giống nhất. - Dự đoán điểm bằng trung bình có trọng số: điểm của từng người hàng xóm được nhân với độ tương tự của họ, rồi chia cho tổng các độ tương tự.
Diễn nôm: "những người giống bạn cũng thích X, vậy chắc bạn cũng sẽ thích X".
Lọc cộng tác theo vật phẩm
Lọc theo vật phẩm (item-based CF) đảo lại góc nhìn: tìm những vật phẩm giống nhau, dựa trên việc chúng được nhiều người chung đánh giá tương tự nhau.
- Để dự đoán điểm của người dùng cho vật phẩm
i, tìm những vật phẩm mà người đó đã đánh giá và giống vớii. - Độ tương tự giữa hai vật phẩm tính bằng cosine trên hai cột tương ứng của ma trận.
- Dự đoán bằng trung bình có trọng số điểm của người dùng trên các vật phẩm giống
i.
Diễn nôm: "bạn đã chấm cao phim X, mà phim Y rất giống X, vậy chắc bạn cũng thích Y".
Khi nào dùng cái nào? Lọc theo người dùng hợp khi số người dùng nhỏ hơn số vật phẩm. Lọc theo vật phẩm hợp khi số vật phẩm nhỏ hơn số người dùng, đây là trường hợp phổ biến trong thương mại điện tử. Ngoài ra, độ giống giữa các vật phẩm ổn định hơn theo thời gian (sản phẩm không đổi) so với độ giống giữa người dùng (sở thích thay đổi), nên Amazon lịch sử dùng lọc theo vật phẩm.
Lọc theo nội dung (content-based)
Lọc theo nội dung dùng đặc trưng nội tại của vật phẩm (thể loại phim, tác giả sách, năm phát hành, từ khóa) để gợi ý những vật phẩm giống với những gì người dùng đã thích. Quy trình ba bước:
- Biểu diễn mỗi vật phẩm thành một vector đặc trưng.
- Xây hồ sơ người dùng bằng trung bình (có thể có trọng số) các đặc trưng của những vật phẩm người đó đã thích.
- Gợi ý những vật phẩm có độ tương tự cao nhất với hồ sơ người dùng.
Ví dụ với phim, mỗi phim được mã hóa thành một vector nhị phân theo thể loại:
| Phim | Hành động | Hài | Lãng mạn | Khoa học viễn tưởng |
|---|---|---|---|---|
| The Matrix | 1 | 0 | 0 | 1 |
| Ông bà Smith | 1 | 1 | 1 | 0 |
| Inception | 1 | 0 | 0 | 1 |
| Notebook | 0 | 0 | 1 | 0 |
Giả sử người dùng đã thích The Matrix nên hồ sơ của họ là vector (1, 0, 0, 1). Tính cosine giữa hồ sơ này với các phim còn lại cho kết quả: Inception bằng 1.00 (giống hệt), Ông bà Smith khoảng 0.41, Notebook bằng 0. Hệ thống sẽ gợi ý Inception, hợp lý vì cả hai đều thuộc hành động và khoa học viễn tưởng.
Ưu điểm của lọc theo nội dung: không cần dữ liệu của người khác nên xử lý tốt khởi đầu lạnh cho người dùng mới, có thể giải thích gợi ý ("vì bạn thích phim hành động"), và khám phá được nội dung dài đuôi. Nhược điểm: chất lượng phụ thuộc vào đặc trưng (phải thiết kế thủ công hoặc trích tự động), khó vượt ra ngoài sở thích đã biết, và không tận dụng được tri thức từ cộng đồng người dùng.
So sánh hai cách lọc
| Tiêu chí | Lọc cộng tác | Lọc theo nội dung |
|---|---|---|
| Dữ liệu cần | Ma trận đánh giá của nhiều người | Đặc trưng nội tại của vật phẩm |
| Cần người dùng khác | Có, dựa vào cộng đồng | Không, chỉ cần lịch sử của chính người đó |
| Khởi đầu lạnh người dùng mới | Yếu, chưa có đánh giá nào | Tốt nếu biết một vài sở thích |
| Khởi đầu lạnh vật phẩm mới | Yếu, chưa ai đánh giá | Tốt, chỉ cần đặc trưng vật phẩm |
| Khám phá sở thích mới | Tốt, gợi ý bất ngờ từ người giống mình | Hạn chế, dễ tạo bong bóng lọc |
| Khả năng giải thích | Khó ("người giống bạn cũng thích") | Dễ ("vì bạn thích thể loại này") |
| Phụ thuộc chất lượng đặc trưng | Không | Có, rất nhiều |
Trong thực tế, các hệ thống thương mại lớn thường dùng cách lai (hybrid), kết hợp cả hai để bù trừ điểm yếu cho nhau.
Vấn đề khởi đầu lạnh
Khởi đầu lạnh (cold-start) là tình huống hệ thống chưa có đủ dữ liệu để gợi ý tốt, xảy ra ở ba dạng:
- Người dùng mới: vừa đăng ký, chưa đánh giá gì. Lọc cộng tác bó tay vì không biết người này giống ai.
- Vật phẩm mới: vừa thêm vào, chưa ai đánh giá. Lọc cộng tác không thể tính độ giống cho nó.
- Hệ thống mới: cả nền tảng còn ít dữ liệu.
Một vài cách giảm nhẹ thường gặp: dùng lọc theo nội dung cho người dùng và vật phẩm mới (vì chỉ cần đặc trưng, không cần lịch sử đánh giá); hỏi sở thích ban đầu khi đăng ký (chọn vài thể loại yêu thích); gợi ý theo độ phổ biến trong giai đoạn đầu; và dùng cách lai để chuyển dần từ nội dung sang cộng tác khi dữ liệu tích lũy nhiều hơn.
Đánh giá hệ khuyến nghị
Xây được mô hình rồi, làm sao biết nó gợi ý tốt hay dở? Có hai nhóm cách đánh giá. Cách thứ nhất là đánh giá ngoại tuyến trên dữ liệu lịch sử: che đi một phần đánh giá đã biết, cho mô hình dự đoán lại, rồi so với giá trị thật đã che. Nếu bài toán là dự đoán điểm số, ta thường đo sai số bằng RMSE hoặc MAE, tức trung bình độ lệch giữa điểm dự đoán và điểm thật. Nhưng người dùng hiếm khi quan tâm điểm dự đoán chính xác tới đâu; họ chỉ nhìn vài vật phẩm đầu danh sách. Vì thế các độ đo theo thứ hạng thường sát thực tế hơn: precision tại k đếm tỉ lệ vật phẩm trong top k gợi ý mà người dùng thực sự thích, còn recall tại k đếm tỉ lệ vật phẩm người dùng thích được lọt vào top k.
Cách thứ hai là đánh giá trực tuyến bằng thử nghiệm A/B: chia người dùng thật thành hai nhóm, một nhóm dùng hệ cũ, một nhóm dùng hệ mới, rồi so các chỉ số như tỉ lệ nhấp, thời gian xem hay doanh thu. Đánh giá trực tuyến đáng tin nhất vì đo trực tiếp hành vi thật, nhưng tốn kém và có rủi ro làm hỏng trải nghiệm, nên người ta thường sàng lọc bằng đánh giá ngoại tuyến trước rồi mới đem vài ứng viên tốt nhất ra thử A/B.
Cũng cần cảnh giác với thiên lệch phản hồi ngay khi đánh giá. Dữ liệu ta có chỉ gồm những vật phẩm người dùng chủ động chọn tương tác, vốn không phải một mẫu ngẫu nhiên: một phim bị chấm điểm thấp thường là phim người dùng đã lỡ xem rồi thất vọng, còn vô số phim họ chắc chắn sẽ ghét thì đơn giản chưa bao giờ được xem nên không hề có trong dữ liệu. Hơn nữa chính hệ khuyến nghị cũ đã định hình dữ liệu mới, vì người dùng chỉ đánh giá được những thứ nó từng đẩy ra. Đánh giá ngoại tuyến trên dữ liệu lệch như vậy dễ cho điểm quá lạc quan, đó là thêm một lý do vì sao thử nghiệm trực tuyến vẫn được coi là chuẩn cuối cùng.
Một điều quan trọng nữa: độ chính xác không phải tất cả. Một hệ chỉ toàn gợi ý thứ an toàn mà người dùng chắc chắn thích sẽ đạt điểm chính xác cao nhưng nhàm chán và bó hẹp. Các nền tảng còn quan tâm độ đa dạng của danh sách, tính bất ngờ thú vị (gợi đúng thứ người dùng thích mà họ không ngờ tới), và độ phủ (tỉ lệ kho vật phẩm từng được đem ra gợi ý), để tránh cứ đẩy đi đẩy lại vài vật phẩm nổi tiếng.
Điểm chính xác cao trên dữ liệu lịch sử không đồng nghĩa với một hệ khuyến nghị tốt. Mô hình có thể đạt RMSE thấp bằng cách học thuộc thiên lệch của dữ liệu cũ, rồi chỉ gợi đi gợi lại vài vật phẩm phổ biến ai cũng thích, khiến trải nghiệm nghèo nàn và không giúp người dùng khám phá cái mới. Một hệ tốt phải cân bằng giữa trúng ý và đa dạng, và bằng chứng thuyết phục nhất luôn là hành vi người dùng thật đo qua thử nghiệm trực tuyến, chứ không phải một con số sai số đẹp trên tập kiểm tra.
Mã nguồn Python
Ví dụ thuần Python (không cần thư viện ngoài) minh họa độ tương tự cosine và một bộ gợi ý vật phẩm đơn giản trên ma trận đánh giá nhỏ.
from math import sqrt
# Ma tran danh gia: nguoi dung -> {vat pham: diem}
# Diem tu 1 toi 5, vat pham khong co nghia la chua danh gia
ratings = {
"An": {"Phim A": 5, "Phim B": 3, "Phim D": 1},
"Binh": {"Phim A": 4, "Phim D": 1},
"Chau": {"Phim A": 1, "Phim C": 5, "Phim D": 4},
"Dung": {"Phim B": 2, "Phim C": 4},
}
def cosine(vec_a, vec_b):
# vec_a, vec_b la dict {vat pham: diem}
# Numerator: dot product over the co-rated items only (missing scores count as 0).
# Denominator: norms of the FULL rating vectors, not just the co-rated part.
common = set(vec_a) & set(vec_b)
if not common:
return 0.0
dot = sum(vec_a[k] * vec_b[k] for k in common)
norm_a = sqrt(sum(v * v for v in vec_a.values()))
norm_b = sqrt(sum(v * v for v in vec_b.values()))
if norm_a == 0 or norm_b == 0:
return 0.0
return dot / (norm_a * norm_b)
def predict(target_user, target_item, k=2):
# Loc cong tac theo nguoi dung: du doan diem cho mot vat pham
target_vec = ratings[target_user]
sims = []
for other_user, vec in ratings.items():
if other_user == target_user:
continue
if target_item in vec: # nguoi nay da danh gia vat pham can du doan
sims.append((cosine(target_vec, vec), other_user))
sims.sort(reverse=True) # sap xep theo do tuong tu giam dan
top_k = sims[:k]
numerator = sum(sim * ratings[u][target_item] for sim, u in top_k)
denominator = sum(abs(sim) for sim, u in top_k)
if denominator == 0:
return None
return numerator / denominator
def recommend(target_user, k=2, top_n=3):
# Goi y cac vat pham nguoi dung chua danh gia, sap theo diem du doan
seen = set(ratings[target_user])
all_items = set()
for vec in ratings.values():
all_items |= set(vec)
candidates = all_items - seen
scored = []
for item in candidates:
score = predict(target_user, item, k)
if score is not None:
scored.append((score, item))
scored.sort(reverse=True)
return scored[:top_n]
# Vi du do tuong tu cosine giua hai nguoi dung
print("Cosine An - Binh:", round(cosine(ratings["An"], ratings["Binh"]), 3))
print("Cosine An - Chau:", round(cosine(ratings["An"], ratings["Chau"]), 3))
# Goi y cho An nhung vat pham An chua danh gia
print("Goi y cho An:")
for score, item in recommend("An"):
print(" ", item, "->", round(score, 2))
Chạy đoạn mã trên, kết quả in ra đúng như sau:
Cosine An - Binh: 0.861
Cosine An - Chau: 0.235
Goi y cho An:
Phim C -> 4.51
An và Bình giống nhau hơn hẳn vì cùng chấm cao Phim A và cùng chấm thấp Phim D, trong khi Châu chấm Phim A rất thấp nên gần như ngược ý An. Bạn có thể đổi ratings thành ma trận của riêng mình để thử nghiệm.
Hàm cosine ở trên và công cụ tương tác ở đầu bài không tính cùng một công thức, nên đừng ngạc nhiên khi hai bên cho số lệch nhau.
- Hàm
cosinetrong mã lấy tử số trên các vật phẩm chung nhưng lấy chuẩn (norm_a,norm_b) trên toàn bộ vector. Điều này tương đương coi mọi ô trống là điểm 0, tức mặc định "chưa đánh giá thì xem như không thích". - Công cụ tương tác thì cắt cả tử số lẫn mẫu số về chỉ các phim cả hai đã đánh giá, tức bỏ qua hoàn toàn phần không chung.
Trên đúng ma trận này, khác biệt rất lớn:
| Cặp người dùng | Mã trong bài (chuẩn toàn vector) | Công cụ (chỉ vật phẩm chung) |
|---|---|---|
| An và Bình | 0.861 | 0.999 |
| An và Châu | 0.235 | 0.428 |
| An và Dũng | 0.227 | 1.000 |
Chỗ An và Dũng lộ ra điểm yếu của biến thể chỉ-vật-phẩm-chung: hai người chỉ trùng nhau đúng một phim (Phim B), mà cosine của hai vector một chiều dương thì luôn bằng 1, nên công cụ kết luận họ giống nhau tuyệt đối chỉ từ một điểm dữ liệu. Biến thể toàn vector tránh cái bẫy đó nhưng lại phải trả giá bằng giả định "ô trống nghĩa là điểm 0", vốn cũng sai vì chưa xem không đồng nghĩa với không thích. Không biến thể nào đúng tuyệt đối; trong thực tế người ta thường trừ trung bình của từng người trước khi tính (tương quan Pearson, xem bài tập 2) và bắt buộc số vật phẩm chung phải đủ lớn thì độ tương tự mới được tin.
Bài tập thực hành
Bài 1. Cài đặt thêm hàm cosine_item tính độ tương tự giữa hai vật phẩm (lọc theo vật phẩm) thay vì giữa hai người dùng. Gợi ý dự đoán điểm của An cho một phim dựa trên các phim giống mà An đã đánh giá.
Gợi ý
Trước tiên xây "vector cột" cho mỗi vật phẩm: duyệt toàn bộ ratings, gom thành dict dạng {nguoi_dung: diem} cho từng phim. Sau đó tái dùng đúng hàm cosine ở trên (vì nó nhận hai dict bất kỳ). Để dự đoán điểm của An cho phim i, lấy các phim An đã đánh giá, tính độ giống của chúng với i, rồi làm trung bình có trọng số giống như hàm predict.
Bài 2. Thêm chuẩn hóa kiểu Pearson (cosine có trừ trung bình). Trước khi tính cosine, trừ điểm trung bình của mỗi người dùng khỏi từng điểm của họ. Giải thích vì sao việc này giúp giảm thiên lệch với "người dễ tính" (người chấm cao mọi thứ).
Gợi ý
Tính mean_u bằng trung bình các giá trị trong vector người dùng, rồi tạo vector mới với mỗi giá trị là diem - mean_u. Người dễ tính có trung bình cao nên sau khi trừ, điểm 5 của họ trở thành gần 0 thay vì lớn, khiến họ không còn "giống mọi người" một cách giả tạo. Cosine trên vector đã trừ trung bình chính là tương quan Pearson.
Bài 3. Mô phỏng khởi đầu lạnh. Thêm một người dùng mới chưa có đánh giá nào và một phim mới chưa ai đánh giá vào ratings. Quan sát điều gì xảy ra khi gọi recommend và predict cho chúng, rồi đề xuất một cách xử lý.
Gợi ý
Với người dùng mới, predict sẽ không tìm được ai giống (cosine bằng 0 với mọi người do không có vật phẩm chung), nên trả về None. Với phim mới, không ai trong ratings từng chấm nó nên không bao giờ được gợi ý. Cách xử lý đơn giản: dùng lọc theo nội dung dựa trên đặc trưng thể loại, hoặc tạm thời gợi ý theo phim phổ biến nhất (nhiều lượt đánh giá nhất) cho tới khi có đủ dữ liệu.
Tự kiểm tra
Vì sao lọc theo nội dung xử lý khởi đầu lạnh của vật phẩm mới tốt hơn lọc cộng tác?
Lọc theo nội dung chỉ cần đặc trưng nội tại của vật phẩm (thể loại, từ khóa, tác giả) để so độ tương tự, nên một phim vừa thêm vào vẫn được gợi ý ngay nếu nó giống thứ người dùng đã thích. Lọc cộng tác lại cần vật phẩm đó đã được nhiều người đánh giá thì mới tính được độ giống với các vật phẩm khác, nên phim mới chưa ai chấm sẽ không bao giờ xuất hiện trong gợi ý.
Khi nào nên ưu tiên lọc theo vật phẩm thay vì lọc theo người dùng?
Khi số vật phẩm nhỏ hơn nhiều so với số người dùng (thường gặp trong thương mại điện tử), vì ma trận độ giống giữa các vật phẩm gọn hơn và rẻ hơn để tính. Ngoài ra, độ giống giữa các vật phẩm ổn định theo thời gian vì bản chất sản phẩm không đổi, trong khi sở thích người dùng thay đổi liên tục, nên có thể tính trước và lưu lại để dùng dần.
Câu hỏi tự kiểm
- 1Lọc cộng tác cần dữ liệu gì để hoạt động?
- 2Vì sao lọc theo nội dung xử lý khởi đầu lạnh của vật phẩm mới tốt hơn lọc cộng tác?
- 3Độ tương tự cosine đo điều gì giữa hai vector?
- 4Vì sao điểm chính xác cao trên dữ liệu lịch sử chưa chắc là một hệ khuyến nghị tốt?
Hệ khuyến nghị dự đoán những ô trống trong ma trận người dùng và vật phẩm. Lọc cộng tác chỉ cần ma trận đánh giá và dùng độ tương tự cosine để tìm người giống nhau (theo người dùng) hoặc vật phẩm giống nhau (theo vật phẩm). Lọc theo nội dung dùng đặc trưng của vật phẩm và xử lý tốt khởi đầu lạnh. Cosine đo góc giữa hai vector, càng gần 1 càng giống. Các hệ thống thực tế thường lai cả hai cách để bù trừ điểm yếu.