Thuật toán Ứng dụng — DP, String, Big Data & hơn nữa/Count-Min Sketch — đếm tần suất xấp xỉ
24/38
Bài 24 / 38~15 phútBig data & streaming — Khi RAM không đủMiễn phí lượt xem

Count-Min Sketch — đếm tần suất xấp xỉ

Ước lượng tần suất phần tử trên stream với bộ nhớ cố định, dùng nhiều hàm băm + ma trận đếm. Luôn over-estimate; ứng dụng heavy hitters, rate limiting.

TL;DR: Bài toán: trên một luồng hàng triệu sự kiện, đếm xem mỗi phần tử xuất hiện bao nhiêu lần — ví dụ sản phẩm nào đang được thêm vào giỏ nhiều nhất. Đếm chính xác phải nhớ từng key, key nhiều thì tốn hàng GB. Count-Min Sketch đếm gần đúng mà chỉ tốn vài chục KB cố định, nhờ một mẹo: thay vì cho mỗi phần tử một ô đếm riêng, ta ép nhiều phần tử dùng chung một số ít ô (đụng nhau) nên số đếm hơi dư; rồi dùng nhiều bộ ô, mỗi bộ trộn các phần tử theo một cách khác, và khi cần biết số đếm của một phần tử thì lấy con số nhỏ nhất trong các bộ — vì đụng độ chỉ làm dư, số nhỏ nhất luôn gần con số thật nhất. Kết quả vì thế không bao giờ thấp hơn thật, chỉ có thể cao hơn một chút. Redis dùng đúng cách này (lệnh CMS.INCRBY/CMS.QUERY).

Ngày 11/11 năm ngoái, Shopee xử lý hơn 87 triệu đơn hàng trong 24 giờ — tương đương ~1.000 đơn/giây. Với mỗi sự kiện add-to-cart, service cần biết sản phẩm nào đang hot để đẩy lên "trending". Nếu dùng một từ điển đếm productId → số lần, từ điển sẽ phình ra hàng triệu key; riêng phần bộ nhớ đã vài GB. Khi dữ liệu không vừa RAM, đẩy hết xuống DB lại tạo nghẽn cổ chai.

Count-Min Sketch giải quyết bài này: ước lượng tần suất với bộ nhớ vài chục KB cố định, chấp nhận đếm dư một chút (over-count) kiểm soát được, và không bao giờ đếm thiếu (under-count).

1. Trực giác — trò bỏ phiếu vào ít hộp

Trước khi đụng công thức, hãy hình dung một trò. Bạn tổ chức bỏ phiếu cho rất nhiều ứng viên (hàng triệu), nhưng chỉ có vài cái hộp (ít hơn ứng viên rất nhiều). Quy tắc: mỗi ứng viên được gán cố định vào một hộp; ai bỏ phiếu cho ứng viên đó thì thả phiếu vào đúng cái hộp ấy. Vì hộp ít mà ứng viên nhiều, nhiều ứng viên buộc phải dùng chung một hộp. Khi muốn biết một ứng viên được bao nhiêu phiếu, bạn đọc số phiếu trong hộp của ứng viên đó — nhưng con số này hơi dư, vì trong hộp còn lẫn phiếu của những ứng viên khác cùng hộp.

Hai phần tử đụng nhau (chung một hộp) chỉ làm số đếm dư lên, không bao giờ làm thiếu — đây là chìa khoá. Để con số bớt dư, ta dùng một mẹo: thay vì một bộ hộp, dùng nhiều bộ hộp, mỗi bộ xếp ứng viên vào hộp theo một cách khác nhau. Hai ứng viên đụng nhau ở bộ này thì thường không đụng nhau ở bộ kia. Khi cần số phiếu của một ứng viên, ta đọc số của ứng viên đó ở mọi bộ rồi lấy con số nhỏ nhất — vì đụng độ chỉ làm dư, bộ nào dư ít nhất sẽ cho con số nhỏ nhất và gần thật nhất.

Count-Min Sketch làm đúng trò này trên dữ liệu:

Trò bỏ phiếuCount-Min Sketch
Cái hộpMột ô đếm (một cột trong bảng)
Số phiếu trong hộpSố nguyên trong ô
Gán ứng viên cố định vào một hộpHàm băm (hash) ánh xạ phần tử về một cột
Một bộ hộp (một cách xếp)Một hàng + một hàm băm riêng
Nhiều ứng viên chung một hộpĐụng độ (collision) — số đếm dư
Lấy số nhỏ nhất qua các bộPhép "Min" — chỗ chữ "Min" trong tên

Điểm mấu chốt: dùng chung hộp chỉ cộng thêm, chẳng bao giờ trừ bớt — nên mọi bộ đều cho con số không nhỏ hơn số thật. Lấy con số nhỏ nhất là cách chọn ước lượng sát nhất mà vẫn không bao giờ thấp hơn thật. Vì thế tên gọi là Count-Min (đếm rồi lấy nhỏ nhất).

💡 Cách nhớ

"Chung hộp thì đếm dư, không đếm thiếu → lấy số nhỏ nhất qua nhiều bộ là sát nhất." Một phần tử không bao giờ bị đếm thiếu vì chẳng có thao tác nào lấy phiếu ra khỏi hộp.

2. Cấu trúc và tham số

2.1 Bài toán và cấu trúc

Cho một luồng (stream) các phần tử x_1, x_2, ..., x_t chảy qua. Ta cần trả lời frequency(x) = số lần x đã xuất hiện cho tới hiện tại. Đếm chính xác phải giữ một từ điển key → số lần; với log truy cập của một sàn thương mại điện tử, số key khác nhau có thể tới hàng chục triệu — từ điển chiếm hàng GB.

Count-Min Sketch (CMS) hiện thực hoá trò bỏ phiếu ở trên bằng một bảng số nguyên kích thước d hàng × w cột (khởi tạo toàn 0). Mỗi hàng là một "bộ hộp" và đi kèm một hàm băm riêng h_1, h_2, ..., h_d; mỗi hàm băm một phần tử về một cột trong khoảng [0, w-1] (một cái "hộp"). Tổng cộng có d hàm băm độc lập.

function CMS_init(d, w):
    -- Tạo bảng d hàng × w cột, khởi tạo 0
    for i <- 0 to d-1:
        for j <- 0 to w-1:
            table[i][j] <- 0
    return table
// Space: O(d * w)

Hai tham số ta tự chọn:

  • w (số cột mỗi hàng) — càng nhiều cột thì mỗi hộp gánh ít ứng viên hơn, số đếm dư ít hơn.
  • d (số hàng) — càng nhiều bộ hộp thì xác suất bị dư nhiều ở cả mọi bộ càng nhỏ (vì ta lấy con nhỏ nhất).

Phần đặt số cụ thể cho wd theo mức sai số mong muốn để ở mục công thức (§4) — ở đây chỉ cần nhớ: thêm cột để bớt dư, thêm hàng để bớt rủi ro.

2.2 Một lần cập nhật minh hoạ

Bảng ba hàng tám cột với ba ô được băm tới; ô của hàng hai phồng lên 12 vì bị một key khác dùng chung

Lấy min không phải để "cho chắc" — nó là cách chọn hàng ít ô nhiễm nhất. Không hàng nào đếm thiếu được, vì chẳng có thao tác nào lấy phiếu ra khỏi hộp; nên con nhỏ nhất luôn là con gần sự thật nhất, và thêm hàng chính là giảm rủi ro cả ba cùng bẩn.

3. Thao tác update và query

3.1 Update — tăng đếm

function CMS_update(x):
    -- Tăng ô tương ứng trên mỗi hàng
    for i <- 0 to d-1:
        j <- h_i(x) mod w
        table[i][j] <- table[i][j] + 1
// Time: O(d)  Space: O(1) thêm

3.2 Query — ước lượng tần suất

function CMS_query(x):
    -- Lấy min qua tất cả hàng
    result <- +∞
    for i <- 0 to d-1:
        j <- h_i(x) mod w
        result <- min(result, table[i][j])
    return result
// Time: O(d)  Space: O(1)

Vì sao lấy số nhỏ nhất (Min)? Mỗi ô table[i][j] là một cái hộp dùng chung — nó cộng dồn cả số lần của những phần tử khác cùng băm về đó (đụng độ). Vì dùng chung hộp chỉ cộng thêm chứ không trừ bớt, mọi ô liên quan tới x đều có giá trị không nhỏ hơn số lần thật của x. Lấy con nhỏ nhất qua các hàng tức là chọn cái hộp "sạch" nhất (ít lẫn phiếu người khác nhất) — đó là ước lượng sát nhất mà vẫn không bao giờ thấp hơn thật. Với d hàm băm khác nhau, khả năng một phần tử xui xẻo bị đụng nặng ở cả d hàng là rất nhỏ, nên con nhỏ nhất thường rất sát.

3.3 Trace ví dụ nhỏ — d=2, w=4

Stream: phone, laptop, phone, tablet, phone

Giả sử: h_1(phone)=0, h_1(laptop)=2, h_1(tablet)=0; h_2(phone)=1, h_2(laptop)=3, h_2(tablet)=3.

Thử đoán

Trước khi xem bảng trace: sau khi xử lý hết stream, query(tablet) sẽ trả về bao nhiêu? Để ý tabletphone cùng băm về bucket 0 ở hàng h_1. Đoán xong rồi đối chiếu với dòng cuối bảng.

BướcPhần tửtable[0] (h_1)table[1] (h_2)
Khởi tạo[0, 0, 0, 0][0, 0, 0, 0]
1phone[1, 0, 0, 0][0, 1, 0, 0]
2laptop[1, 0, 1, 0][0, 1, 0, 1]
3phone[2, 0, 1, 0][0, 2, 0, 1]
4tablet[3, 0, 1, 0][0, 2, 0, 2]
5phone[4, 0, 1, 0][0, 3, 0, 2]

query(phone): min(table[0][0], table[1][1]) = min(4, 3) = 3. Thật là 3. ✓

query(tablet): min(table[0][0], table[1][3]) = min(4, 2) = 2. Thật là 1. Sai số +1 do collision tabletphone cùng bucket 0 trên hàng 0.

Đây đúng là tính chất sai số một chiều: kết quả ≥ giá trị thật, không bao giờ dưới.

4. Phân tích sai số (phần công thức)

Giờ mới tới công thức, để biết đặt wd bao nhiêu cho mức dư mong muốn. Hai ký hiệu cần làm quen: ε (epsilon) là mức dư tương đối ta chấp nhận (ví dụ dư không quá 0,1% tổng lượt), và δ (delta) là xác suất hiếm hoi mà kết quả vẫn dư quá mức đó. Đặt thêm N = tổng số phần tử đã chảy qua stream.

Định lý: Với CMS kích thước d × w, query(x) trả về f̂(x) thoả:

f̂(x) ≥ f(x) luôn luôn (không bao giờ under-count).

P[f̂(x) > f(x) + ε·N] ≤ δ, với ε = e/wδ = e^(-d) (e ≈ 2,718, là hằng số e tự nhiên — đừng nhầm với ε epsilon ở trên).

N là tổng số phần tử trên stream. Khi chọn w = ⌈e/ε⌉d = ⌈ln(1/δ)⌉, ta đảm bảo sai số tương đối ≤ ε với xác suất ≥ 1 - δ.

Tham sốÝ nghĩaCông thức chọn
w (width)Kiểm soát sai số tuyệt đốiw = ⌈e/ε⌉
d (depth)Kiểm soát xác suất vượt sai sốd = ⌈ln(1/δ)⌉
Bộ nhớCố định bất kể số keyd × w × kích_thước_ô

Ví dụ thực tế: muốn sai số ≤ 0.1% tổng lượt (ε=0.001) với xác suất ≥ 99% (δ=0.01):

  • w = ⌈e/0.001⌉ = ⌈2718.28⌉ = 2719
  • d = ⌈ln(100)⌉ = ⌈4.6⌉ = 5
  • Bộ nhớ: 5 × 2719 × 8 = 108,760 byte ≈ 106 KB

So với HashMap lưu 10 triệu key (mỗi entry ~50 byte) = ~500 MB — CMS tiết kiệm hơn 4.500 lần.

5. Count-Min Sketch khác Bloom Filter thế nào?

Cả hai đều là cấu trúc xác suất dùng multiple hash functions + bit/ô đếm. Điểm khác biệt then chốt:

Tiêu chíBloom FilterCount-Min Sketch
Câu hỏi trả lời"x có trong tập chưa?" (membership)"x xuất hiện bao nhiêu lần?" (frequency)
Ô lưu trữBit (0/1)Số nguyên đếm
Loại sai sốFalse positive (nói có khi không)Over-count (f̂ ≥ f thật)
Hỗ trợ deleteKhông (bit không giảm được)Chỉ với biến thể counter có dấu; delete phá vỡ bảo đảm over-estimate một chiều của CMS chuẩn
Ứng dụng chínhCache miss avoidance, spam filterFrequency estimation, heavy hitters

Cả hai đều KHÔNG bao giờ false negative: Bloom Filter không nói "không có" khi thật ra có; CMS không nói "ít hơn" khi thật ra nhiều hơn — đây là tính chất bảo toàn quan trọng cho ứng dụng thực tế.

Xem thêm về Bloom Filter tại bài 09 module tìm kiếm nhanh.

6. Ứng dụng thực tế

Tìm phần tử nóng (heavy hitters — những phần tử xuất hiện nhiều nhất, ví dụ top-K sản phẩm bán chạy): chạy CMS song song với một min-heap giữ K phần tử nhiều nhất (heap "min-size-K" — cấu trúc luôn bỏ ra phần tử nhỏ nhất khi đầy, nên giữ lại đúng K phần tử lớn nhất). Mỗi lần cập nhật một phần tử, nếu số đếm ước lượng của nó vượt ngưỡng thì đẩy vào heap. Đây là ý tưởng cơ sở cho mini-challenge bài 06.

Giới hạn tốc độ truy cập (rate limiting — chặn bớt khi một nguồn gửi quá nhiều yêu cầu): thay vì lưu một bộ đếm riêng cho mỗi địa chỉ IP trong Redis, dùng CMS để ước lượng số yêu cầu từ một IP trong cửa sổ thời gian. Vì CMS chỉ đếm dư, sai số chỉ làm việc chặn gắt hơn mức cần: hệ thống có thể chặn nhầm một IP bình thường (false throttle — chặn oan) nhưng không bao giờ thả lọt một IP đáng chặn (false pass — thả oan). Hướng sai lệch này an toàn hơn cho bảo mật.

Giám sát lưu lượng mạng: một router cần biết luồng dữ liệu nào chiếm băng thông lớn nhất. Với hàng triệu luồng mỗi giây, chỉ CMS mới đủ nhanh và đủ nhỏ để chạy trên một con chip chuyên dụng gắn trong router (ASIC — chip thiết kế riêng cho một việc, không phải CPU đa dụng).

7. Pitfall

Pitfall 1 — Nhầm CMS trả về giá trị chính xác

-- SAI: tin query() trả về đúng → dùng để billing
if CMS_query(userId) > 1000:
    charge(userId)     -- Lỗi: over-count có thể charge oan
-- DUNG: dùng CMS chỉ để lọc ứng viên, verify bằng nguồn chính xác
candidates <- items where CMS_query(x) > threshold
for x in candidates:
    exact_count <- DB.count(x)  -- verify chính xác trước khi hành động
    if exact_count > 1000: charge(x)

CMS phù hợp để sàng lọctheo dõi, không phải để tính tiền hay kiểm toán — những việc cần con số chính xác tuyệt đối.

Pitfall 2 — Chọn dw không dựa trên ràng buộc bài toán

-- SAI: đoán mò d=3, w=100 mà không kiểm tra ε và δ
-- ε = e/100 ≈ 2.7% — quá lớn nếu N = 10 triệu, sai số tuyệt đối ~270.000!
-- DUNG: tính ngược từ ràng buộc
epsilon <- 0.001   -- chấp nhận sai số 0.1% * N
delta   <- 0.01    -- xác suất vượt sai số ≤ 1%
w <- ceil(e / epsilon)        -- = 2719
d <- ceil(ln(1 / delta))      -- = 5

Luôn xuất phát từ εδ chấp nhận được, sau đó tính wd. Ngược lại sẽ cho kết quả sai quá ngưỡng cho phép.

Pitfall 3 — Dùng hàm băm không độc lập hoặc dùng chung seed

-- SAI: dùng cùng 1 hàm băm với d offset khác nhau
h_i(x) = hash(x) + i   -- Không độc lập: cùng collision pattern!
-- DUNG: d hàm băm thực sự độc lập (khác seed hoặc thuật toán khác nhau)
h_i(x) = murmur3(x, seed=i)  -- seed độc lập → độc lập thống kê

Các hàm băm phải độc lập từng cặp (pairwise independent — nghĩa là hai hàm băm thực sự khác nhau: biết kết quả của hàm này không giúp đoán được kết quả của hàm kia) thì bảo đảm sai số lý thuyết mới đúng. Dùng phép cộng offset đơn giản không đủ độc lập: các hàm vẫn đụng độ theo cùng một kiểu, nên sai số thực tế tệ hơn mức lý thuyết hứa hẹn.

8. Liên hệ các bài khác

  • Hash function — nền tảng băm: hàm băm độc lập là viên gạch nền của CMS. Bài đó giải thích thế nào là độc lập từng cặp và hiệu ứng tuyết lở (avalanche — đổi một chút đầu vào thì kết quả băm đổi hẳn) — hai tính chất ảnh hưởng trực tiếp tới chất lượng CMS.
  • Bloom Filter: anh em họ của CMS — cùng dùng nhiều hàm băm trên một mảng, nhưng trả lời câu hỏi "phần tử có trong tập chưa" thay vì "xuất hiện bao nhiêu lần". Đọc bài đó để thấy rõ điểm giống và khác về cách dựng.
  • HyperLogLog: cùng họ xác suất — HyperLogLog ước lượng số phần tử khác nhau, còn CMS ước lượng số lần xuất hiện của từng phần tử. Hai bài bổ trợ cho nhau khi xử lý luồng dữ liệu.
  • Mini-challenge top-K: áp dụng CMS kết hợp min-heap để tìm các phần tử nóng — bài thực hành trực tiếp của bài này.
  • Amortized analysis: phân tích khấu hao giải thích vì sao mỗi thao tác chỉ chạm d ô (số hàng), gộp lại trên toàn bộ luồng vẫn cho tốc độ xử lý cao trong thực tế.

📚 Deep Dive

📚 Deep Dive — Nguồn gốc & tham khảo

Bài báo gốc:

  • Cormode & Muthukrishnan (2005) — "An Improved Data Stream Summary: The Count-Min Sketch and its Applications", Journal of Algorithms 55(1). Đây là bài báo giới thiệu CMS với đầy đủ chứng minh sai số.

Sách:

  • Mining of Massive Datasets (Leskovec, Rajaraman, Ullman), Chương 4 "Mining Data Streams" — trình bày CMS trong bối cảnh streaming algorithms, kèm so sánh với AMS sketch.
  • Probabilistic Data Structures and Algorithms (Gakhov, 2019) — chương Count-Min Sketch có ví dụ code Python và Redis.

Triển khai thực tế:

  • Redis module RedisBloom có lệnh CMS.INCRBY / CMS.QUERY — dùng được ngay trong production.
  • Apache Flink, Spark Streaming dùng CMS trong các window aggregation operator để tính approximate top-K.

Biến thể:

  • Count-Mean-Min Sketch: thay vì MIN, dùng median để giảm bias — tốt hơn khi distribution skewed mạnh.
  • Conservative Update (CU): khi update x, chỉ tăng ô nào có giá trị nhỏ hơn query(x) + 1 — giảm over-count đáng kể với chi phí thêm 1 lần query.

Tóm tắt

  • Đụng độ → chỉ đếm dư, không thiếu; lấy min qua d hàng → ước lượng sát nhất mà vẫn ≥ thật.
  • Chọn w = ⌈e/ε⌉ để sai số ≤ ε×N; d = ⌈ln(1/δ)⌉ để xác suất vượt ngưỡng ≤ δ — bộ nhớ cố định d×w ô.
  • Bloom Filter hỏi "có trong tập chưa?" (bit, false positive); CMS hỏi "xuất hiện bao nhiêu lần?" (số nguyên, over-count) — cả hai không bao giờ false negative.
  • Dùng để sàng lọc và theo dõi (heavy hitters, rate limiting); cần số chính xác tuyệt đối thì verify bằng nguồn đếm thật.

Tự kiểm tra

Tự kiểm tra
Q1
Vì sao query() lấy số nhỏ nhất qua các hàng thay vì lấy trung bình hay lớn nhất? Tính chất nào của đụng độ đảm bảo số nhỏ nhất là cận trên sát nhất?

Đụng độ trong một hàng chỉ làm tăng giá trị ô — một ô table[i][j] chứa tổng số đếm của mọi phần tử băm về cột j ở hàng i. Do đó mọi ô đều có giá trị không nhỏ hơn số lần thật của x. Lấy số lớn nhất sẽ chọn đúng hàng bị đụng độ nặng nhất — sai số lớn nhất. Lấy trung bình thì trộn lẫn các mức đụng độ khác nhau, không có bảo đảm lý thuyết rõ ràng.

Lấy số nhỏ nhất tức là chọn hàng bị đụng độ ít nhất (ước lượng sát nhất). Mọi ô đều không nhỏ hơn số thật, nên số nhỏ nhất qua các hàng vẫn không nhỏ hơn số thật — đó là một cận trên (upper bound: con số chặn trên, kết quả không vượt qua nó theo hướng thiếu). Với d hàm băm độc lập, xác suất một hàng vượt ngưỡng f(x) + ε·N bị chặn ở mức 1/e. Con số này suy ra từ bất đẳng thức Markov (quy tắc: xác suất một đại lượng không âm vượt một ngưỡng nào đó không lớn hơn giá trị trung bình chia cho ngưỡng đó); ở đây mức nhiễu đụng độ trung bình mỗi ô là N/w, chia cho ngưỡng ε·N = (e/w)·N ra đúng 1/e. Qua d hàng độc lập, xác suất số nhỏ nhất vẫn vượt ngưỡng là (1/e)^d = e^(-d) — đúng bằng δ ở mục 4, và rất nhỏ khi d vừa phải. Vì thế lấy số nhỏ nhất vừa hợp trực giác vừa có bảo đảm lý thuyết.

Q2
Tại sao sai số của CMS luôn là đếm dư, không bao giờ đếm thiếu? Điều này ảnh hưởng thế nào đến cách thiết kế ứng dụng dùng CMS?

Cơ chế: mỗi lần cập nhật một phần tử thì tăng đúng 1 ô trên mỗi hàng — không bao giờ giảm ô nào. Khi truy vấn, số nhỏ nhất qua các ô đó vẫn không nhỏ hơn số lần x xuất hiện thật (vì ô còn cộng thêm số đếm của các phần tử khác bị đụng độ). Không có thao tác nào làm giảm ô của x, nên đếm thiếu là không thể xảy ra.

Hệ quả thiết kế: nên dùng CMS cho quyết định "mềm" (chọn ra ứng viên đáng chú ý, bật cảnh báo) chứ không phải quyết định "cứng" (tính tiền, cam kết dịch vụ ràng buộc theo hợp đồng). Cách làm an toàn: dùng CMS lọc các ứng viên vượt ngưỡng, rồi đối chiếu lại với nguồn đếm chính xác (cơ sở dữ liệu, log) trước khi hành động. Với giới hạn tốc độ truy cập, đếm dư chỉ làm việc chặn gắt hơn — thường chấp nhận được về mặt bảo mật.

Q3
Cho ε=0.5% và δ=1%. Tính w và d cần thiết. Bộ nhớ tổng là bao nhiêu nếu mỗi ô dùng 8 byte?

ε = 0.005, δ = 0.01.

w = ⌈e / ε⌉ = ⌈2.718 / 0.005⌉ = ⌈543.6⌉ = 544 cột.

d = ⌈ln(1/δ)⌉ = ⌈ln(100)⌉ = ⌈4.605⌉ = 5 hàng.

Bộ nhớ: 5 × 544 × 8 = 21.760 byte ≈ 21 KB. Chỉ 21 KB để ước lượng tần suất với sai số không quá 0,5% của N, ở xác suất 99% — bất kể luồng có bao nhiêu phần tử khác nhau.

Q4
Bloom Filter và Count-Min Sketch đều dùng nhiều hàm băm trên một mảng. Chỉ ra 2 điểm khác biệt về cách dựng và giải thích vì sao mỗi điểm dẫn đến câu trả lời khác nhau.

Điểm 1 — Kiểu ô lưu trữ: Bloom Filter dùng bit (0 hoặc 1); CMS dùng số nguyên đếm. Bit chỉ nói "đã thấy hay chưa"; số nguyên nói "đã thấy bao nhiêu lần". Kiểu ô quyết định câu hỏi trả lời được — "có trong tập chưa" hay "xuất hiện bao nhiêu lần".

Điểm 2 — Cách đọc kết quả: Bloom Filter chỉ trả lời "có" khi tất cả các bit liên quan đều bằng 1; CMS lấy số nhỏ nhất qua các ô đếm. Ở Bloom Filter, sai "báo có nhầm" xảy ra khi mọi bit liên quan tình cờ đều bằng 1; ở CMS, đếm dư xảy ra khi cả mọi hàng đều bị đụng độ — hai cơ chế đọc khác nhau dẫn tới hai kiểu sai số khác nhau.

Q5
Conservative Update (cập nhật dè dặt — gọi tắt CU) cải tiến CMS bằng cách chỉ tăng những ô có giá trị nhỏ hơn query(x)+1, thay vì tăng tất cả d ô. Giải thích tại sao kỹ thuật này giảm đếm dư mà không làm kết quả trở thành đếm thiếu.

Ý tưởng: trước khi cập nhật, chạy truy vấn để biết ước lượng hiện tại . Chỉ tăng ô table[i][h_i(x)] nếu nó nhỏ hơn f̂ + 1. Những ô đã bằng hoặc lớn hơn f̂ + 1 là đang đếm dư vì đụng độ — tăng thêm cũng vô ích.

Vì sao không đếm thiếu: phép truy vấn vẫn lấy số nhỏ nhất. Sau mỗi lần cập nhật dè dặt, có ít nhất 1 trong số d ô được tăng (ô đang nhỏ nhất, bằng , sẽ tăng lên f̂+1). Số nhỏ nhất qua các ô vẫn không nhỏ hơn số thật — tính chất cận trên được giữ nguyên. CU làm chậm tốc độ tăng của những ô bị đụng độ nặng, nên về lâu dài số nhỏ nhất sát giá trị thật hơn.

Q6
Luồng gồm 1 triệu sự kiện, 90% là cùng một phần tử 'A'. CMS với w=100, d=3 sẽ cho kết quả query('A') như thế nào? Điều này dạy ta bài học gì về việc chọn w?

Với w=100, mỗi hàng có 100 ô. 'A' xuất hiện 900.000 lần; 100.000 lần còn lại trải đều qua khoảng 100.000 phần tử khác. Trung bình mỗi ô bị đụng độ thêm 1.000 lần. Ô của 'A' trong mỗi hàng vào khoảng 900.000 + 1.000 = 901.000, nên query(A) ≈ 901.000, sai số khoảng 1.000 trên 1.000.000, tức 0,1%.

Tuy nhiên, với ε = e/w = 2,718/100 ≈ 2,7% và N = 1.000.000, bảo đảm lý thuyết cho phép sai số tới 27.000 — lớn hơn sai số thực tế rất nhiều. Bài học: khi phân bố dữ liệu lệch mạnh (distribution skewed — một phần tử áp đảo, chiếm phần lớn lượt), CMS thực tế tốt hơn nhiều so với mức bảo đảm lý thuyết cho phần tử áp đảo đó; nhưng bảo đảm lý thuyết vẫn là cam kết đúng trong mọi trường hợp. Muốn sai số chặt hơn thì phải tăng w — không thể trông cậy vào việc "dữ liệu sẽ luôn lệch" vì dữ liệu thật thay đổi theo mùa.

Q7
Trong ứng dụng giới hạn tốc độ truy cập bằng CMS: mỗi IP là một phần tử, cửa sổ 1 phút. Giải thích tại sao đếm dư an toàn hơn đếm thiếu trong bài toán này, và đề xuất 1 vấn đề tiềm ẩn cần xử lý.

Đếm dư nghĩa là ta chặn một IP sớm hơn mức cần thiết (false throttle — chặn oan). Đếm thiếu nghĩa là ta cho qua khi đáng lẽ phải chặn (false pass — thả oan). Về bảo mật, thả oan nguy hiểm hơn chặn oan — một IP tấn công gây nghẽn (DDoS) lách được giới hạn thì hại hơn nhiều so với một IP bình thường bị chặn nhầm vài giây. CMS phù hợp vì sai số luôn nghiêng về hướng an toàn.

Vấn đề tiềm ẩn: CMS không có cơ chế tự xoá đếm theo cửa sổ thời gian. Cần kết hợp với cửa sổ trượt (sliding window — cửa sổ thời gian trượt dần, chỉ tính các sự kiện trong khoảng gần nhất), hoặc dùng hai CMS luân phiên (mỗi nửa cửa sổ lại đổi vai, một cái đang đếm thì cái kia được xoá để chờ dùng lại). Nếu không, số đếm cứ tích luỹ mãi và mọi IP đều bị chặn sau đủ lâu. Cách dùng hai CMS luân phiên là kỹ thuật chuẩn trong giới hạn tốc độ truy cập thực tế.

Bài tiếp theo: Sliding window — thống kê trên cửa sổ trượt

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

Sliding window — thống kê trên cửa sổ trượt