Phần 6 kết thúc với một hạn chế của Count-Min Sketch: nó đếm được tần suất khi bạn hỏi về một phần tử cụ thể, nhưng không tự biết phần tử nào nóng nhất. Mà trong thực tế, câu hỏi thường gặp lại chính là "phần tử nào nóng nhất?" — top 10 từ khóa tìm nhiều nhất, top IP gọi API nhiều nhất (dấu hiệu DDoS), sản phẩm trending. Đây gọi là bài toán heavy hitters (top-k). Lưu cả Counter đầy đủ thì tốn RAM theo số phần tử khác nhau. Giải pháp thanh lịch: ghép CMS (đếm tần suất bằng bộ nhớ cố định) với một min-heap nhỏ (theo dõi K ứng viên nóng nhất). Bài này (phần 7 loạt Xác suất) tự cài và đo thật độ chính xác của top-k tìm được.

Cơ chế: CMS đếm, min-heap giữ ứng viên

Ý tưởng kết hợp: CMS trả lời "tần suất ước lượng của x", và một min-heap kích thước K giữ K phần tử có tần suất cao nhất thấy được cho tới giờ. Với mỗi phần tử đến trong luồng:

  1. Cập nhật CMS (tăng d ô như phần 6).
  2. Hỏi CMS tần suất ước lượng của phần tử đó.
  3. So với phần tử nhỏ nhất trong heap (heap[0]): nếu lớn hơn, đẩy ứng viên yếu nhất ra và thêm phần tử này vào.

Min-heap cho phép luôn biết "ứng viên yếu nhất hiện tại" trong O(1) và thay nó trong O(log K). Bộ nhớ = CMS (cố định) + K phần tử (nhỏ) — không phụ thuộc số phần tử khác nhau trong luồng.

cms.add(tok); est = cms.count(tok)
if tok in heap: cập_nhật_est_trong_heap
elif len(heap) < K: heapq.heappush(heap, (est, tok))
elif est > heap[0][0]:                 # lớn hơn phần tử nhỏ nhất
    heapq.heapreplace(heap, (est, tok))  # đẩy ứng viên yếu nhất ra

Ảnh chụp đoạn mã nền tối minh hoạ heavy hitters tìm top-k phần tử nóng nhất trong luồng Count-Min Sketch đếm tần suất cộng min-heap giữ ứng viên top-k, vấn đề K phần tử nóng nhất trong luồng khổng lồ top 10 từ khóa tìm nhiều nhất top IP gọi API nhiều nhất DDoS sản phẩm trending lưu Counter đầy đủ tốn RAM theo số phần tử khác nhau cần tìm top-k mà bộ nhớ cố định kết hợp CMS bài 06 cộng min-heap, CMS đếm nhưng không tự liệt kê cần min-heap CMS trả lời tần suất của x khi hỏi nhưng không biết x nào nóng giải pháp giữ một min-heap kích thước K các ứng viên top-k mỗi phần tử tới cập nhật CMS hỏi tần suất ước lượng so với phần tử nhỏ nhất trong heap heap 0 lớn hơn thì thay vào, tự cài CMS cộng heapq kích thước K cms add tok est bằng cms count tok if tok trong heap cập nhật est trong heap elif len heap nhỏ hơn K heapq heappush heap est tok elif est lớn hơn heap 0 0 lớn hơn phần tử nhỏ nhất heapq heapreplace heap est tok đẩy ứng viên yếu nhất ra, vì sao top-k thường đúng CMS chính xác ở tần suất cao bài 06 heavy hitter sai số khoảng 1 phần trăm nên các phần tử nóng nhất được ước lượng đúng xếp hàng đúng phần tử biên vị trí khoảng K tần suất sát nhau nhiều over-estimate có thể đảo thứ tự recall nhỏ hơn 100 phần trăm ở ranh giới ứng dụng trending DDoS log realtime

Hình 1: Heavy hitters = CMS (đếm tần suất bằng bộ nhớ cố định) + min-heap kích thước K (giữ ứng viên nóng nhất); mỗi phần tử đến cập nhật CMS, hỏi tần suất, so với phần tử nhỏ nhất trong heap và thay nếu lớn hơn; top-k thường đúng vì CMS chính xác ở tần suất cao.

Đo thật: recall 90%, chỉ lệch ở ranh giới

Mình tạo luồng 300.000 sự kiện với phân phối lệch (15.000 phần tử khác nhau), tìm top-10 bằng CMS+heap, và so với top-10 thật (tính từ Counter đầy đủ để kiểm chứng):

Ảnh chụp bảng kết quả chạy thật heavy hitters output thật go-lab python CMS 5x2000 cộng min-heap 10 luồng 300k sự kiện 15k phần tử, top-10 thật Counter đầy đủ vs top-10 tìm được CMS cộng heap số top-k thật that top-k tìm được CMS est 1 kw-0 12191 kw-0 12241 2 kw-1 3082 kw-1 3120 3 kw-2 2224 kw-2 2258 4 kw-3 1751 kw-3 1788 5 kw-4 1529 kw-4 1560 6 kw-5 1341 kw-5 1446 7 kw-6 1204 kw-6 1303 8 kw-7 1096 kw-7 1239 9 kw-8 975 kw-8 1050 10 kw-9 884 kw-10 988 lệch ở ranh giới, recall và sai số tần suất recall top-10 bằng 90 phần trăm 9 trên 10 phần tử nóng tìm đúng 9 phần tử nóng nhất tìm đúng hết chỉ vị trí 10 lệch kw-9 884 và kw-10 khoảng 880 tần suất sát nhau over-estimate nhiều đảo thứ tự tần suất CMS của top-k est lớn hơn hoặc bằng that lệch nhỏ vì tần suất cao kw-0 that 12191 CMS 12241 lệch 0.41 phần trăm kw-1 that 3082 CMS 3120 lệch 1.23 phần trăm kw-2 that 2224 CMS 2258 lệch 1.53 phần trăm heavy hitter CMS chính xác xếp hàng top-k đúng bộ nhớ vẫn cố định

Hình 2: Chạy thật — top-10 thật (kw-0..kw-9) vs top-10 tìm được: chín vị trí đầu (kw-0..kw-8) khớp hoàn toàn, chỉ vị trí 10 lệch (tìm kw-10 thay vì kw-9, hai phần tử có tần suất sát nhau ~880); recall 90%; CMS over-estimate nhỏ cho heavy hitter (kw-0 lệch 0.41%, kw-1 1.23%, kw-2 1.53%).

  • Chín heavy hitter nóng nhất: tìm đúng hết: các vị trí 1-9 (kw-0 đến kw-8) khớp chính xác giữa top-k tìm được và top-k thật, đúng thứ tự. Đây là điều CMS+heap làm tốt: những phần tử thực sự nóng được nhận diện và xếp hạng đúng.
  • Vì sao đúng: CMS chính xác ở tần suất cao: nhớ phần 6 — CMS over-estimate tương đối nhỏ với phần tử tần suất cao. Ở đây kw-0 (thật 12.191) được ước lượng 12.241 (lệch chỉ 0.41%), kw-1 lệch 1.23%, kw-2 lệch 1.53%. Vì các heavy hitter được đếm gần đúng, thứ tự của chúng được giữ đúng. Đây chính là lý do CMS hợp với bài toán heavy hitters (và không hợp đếm phần tử hiếm).
  • Chỉ lệch ở ranh giới (recall 90%): vị trí thứ 10 tìm được kw-10 thay vì kw-9. Lý do trung thực: hai phần tử này có tần suất rất sát nhau (884 vs ~880), và lượng over-estimate của CMS (vốn nhiễu vài chục) đủ để đảo thứ tự của hai phần tử gần bằng nhau ở ranh giới top-k. Đây là hạn chế cố hữu: top-k xấp xỉ luôn có thể sai ở đường biên nơi tần suất các phần tử gần nhau — nhưng các phần tử rõ ràng nóng thì luôn đúng.

Đánh đổi cần cân nhắc

Recall cao ở đỉnh, kém ở biên — tăng K nội bộ để chắc chắn. Vì lỗi tập trung ở ranh giới top-k, một mẹo thực tế là theo dõi nhiều hơn K ứng viên (ví dụ giữ heap kích thước 2K hoặc 3K) rồi lấy top-K cuối cùng. Điều này giảm khả năng một phần tử thực sự thuộc top-K bị đẩy ra sớm do nhiễu. Đổi lại một chút bộ nhớ heap (vẫn nhỏ so với CMS) để tăng recall ở biên. Nếu bạn cần chính xác tuyệt đối ở biên thì heavy hitters xấp xỉ không phải công cụ đúng.

CMS over-estimate làm tần suất báo cao hơn thật — nhớ khi hiển thị. Con số tần suất mà bạn lấy ra cho top-k là ước lượng vượt (kw-0 báo 12.241 thay vì 12.191). Với hiển thị "xu hướng" thì không sao, nhưng nếu bạn dùng con số này cho tính toán tiếp (ví dụ tính tỉ lệ phần trăm), nhớ rằng nó hơi cao. Muốn con số chính xác hơn cho riêng top-k (ít phần tử), có thể duy trì một Counter thật chỉ cho các ứng viên trong heap — bộ nhớ vẫn nhỏ vì heap nhỏ.

Có các thuật toán heavy-hitter chuyên dụng tốt hơn cho một số ca. CMS+heap là cách phổ biến và đơn giản, nhưng có những thuật toán chuyên cho heavy hitters với đảm bảo tốt hơn: Space-Saving (Metwally et al.) đảm bảo tìm đủ mọi phần tử vượt ngưỡng tần suất với bộ nhớ nhỏ hơn, và Misra-Gries là thuật toán cổ điển đơn giản. Nếu heavy hitters là nhu cầu chính (không chỉ đếm tần suất chung), cân nhắc Space-Saving — nó thường chính xác hơn CMS+heap cho cùng bộ nhớ.

Ba ý mang về

  1. Heavy hitters = CMS + min-heap: đo thật trên luồng 300.000 sự kiện, CMS (bộ nhớ cố định) đếm tần suất và min-heap kích thước K giữ ứng viên nóng nhất — mỗi phần tử đến, cập nhật CMS, so với phần tử nhỏ nhất trong heap và thay nếu lớn hơn.
  2. Top-k đúng vì CMS chính xác ở tần suất cao: đo thật 9/10 heavy hitter tìm đúng (recall 90%), các vị trí 1-9 khớp hoàn toàn với over-estimate nhỏ (0.41-1.53%); lỗi chỉ ở ranh giới nơi tần suất các phần tử sát nhau (kw-9 vs kw-10).
  3. Biết giới hạn và cách cải thiện: lỗi tập trung ở biên top-k (tăng heap lên 2K-3K để tăng recall); tần suất báo là over-estimate (duy trì Counter thật cho riêng ứng viên nếu cần chính xác); và cân nhắc Space-Saving/Misra-Gries khi heavy hitters là nhu cầu chính.

Nguồn

Phần sau ta chuyển sang bài toán độ tương đồng: MinHash ước lượng hai tập giống nhau bao nhiêu (Jaccard similarity) bằng vài chữ ký nhỏ thay vì so từng phần tử — nền của phát hiện trùng lặp và gợi ý ở quy mô lớn.