Phần 4 giới thiệu ý tưởng đẹp của HyperLogLog: rank lớn nhất báo hiệu cardinality, chia register để giảm phương sai. Nhưng giữa một ý tưởng đẹp và một thuật toán chạy được là một khoảng cách đầy chi tiết kỹ thuật — và chính những chi tiết đó phân biệt HLL của sách giáo khoa với HLL trong Redis hay BigQuery. Bài này (phần 5 loạt Xác suất) đào vào ba hiệu chỉnh quan trọng nhất, và đo thật để thấy vì sao thiếu chúng thì thuật toán sai thảm hại: tại sao phải dùng trung bình điều hòa, hằng số alpha làm gì, và cách xử lý thiên lệch ở cardinality nhỏ và lớn.

Vì sao trung bình điều hòa, không phải trung bình thường

Mỗi register giữ một rank, và 2^rank là "ước lượng thô" cho số phần tử mà register đó thấy. Để gộp m register, câu hỏi là: lấy trung bình kiểu gì? Trực giác đầu tiên là trung bình cộng (arithmetic mean). Nhưng đây là bẫy: register có rank cao (băm ra chuỗi nhiều số 0 dẫn đầu — hiếm) có 2^rank lớn gấp bội, và nó là một ngoại lai kéo lệch trung bình cộng lên cao. Một register may mắn thấy rank 20 (2^20 ≈ 1 triệu) sẽ áp đảo hàng nghìn register khác.

Trung bình điều hòa (harmonic mean) giải quyết điều này: nó bị chi phối bởi các giá trị nhỏ, nên giảm mạnh ảnh hưởng của ngoại lai lớn → phương sai thấp hơn, ước lượng ổn định hơn. Công thức HLL chính là harmonic mean cải trang:

E = alpha · m² / Σ 2^(-reg[i])      # = alpha · m · harmonic_mean(2^reg)

Ảnh chụp đoạn mã nền tối minh hoạ HyperLogLog sâu harmonic mean alpha và hiệu chỉnh hai đầu những chi tiết biến ý tưởng đẹp thành thuật toán chạy được thật, vì sao trung bình điều hòa không phải trung bình thường mỗi register giữ 2 mũ rank register có rank cao băm ra chuỗi nhiều 0 hiếm là ngoại lai 2 mũ rank của nó lớn gấp bội kéo lệch trung bình thường lên trung bình điều hòa harmonic bị chi phối bởi giá trị nhỏ giảm ảnh hưởng ngoại lai phương sai thấp hơn ước lượng ổn định E bằng alpha nhân m bình phương chia tổng 2 mũ trừ reg i bằng alpha nhân m nhân harmonic mean 2 mũ reg, hằng số alpha hiệu chỉnh thiên lệch hệ thống alpha bằng 0.7213 chia 1 cộng 1.079 chia m m lớn m nhỏ có giá trị khác công thức harmonic thô vẫn thiên lệch một chút có hệ thống tính được alpha là hệ số nhân để bù đúng lệch đó ước lượng không thiên lệch, thiên lệch hai đầu cardinality nhỏ và rất lớn nhỏ ít phần tử phần lớn register còn rỗng bằng 0 công thức harmonic sai nặng dùng linear counting E bằng m nhân ln m chia V V bằng số register rỗng if E nhỏ hơn hoặc bằng 2.5 m và V lớn hơn 0 E bằng m nhân ln m chia V vùng nhỏ rất lớn gần 2 mũ 32 với hash 32 bit đụng va chạm hash hiệu chỉnh, đo so ba ước lượng trên cùng dữ liệu est harmonic công thức HLL harmonic cộng alpha cộng linear counting est arithmetic trung bình thường của rank rồi 2 mũ ngây thơ để so est no lc harmonic nhưng tắt linear counting để thấy vùng nhỏ hỏng các hiệu chỉnh này chính là thứ HyperLogLog cộng cộng Google 2013 hoàn thiện

Hình 1: Ba hiệu chỉnh của HLL — trung bình điều hòa thay trung bình thường (giảm ảnh hưởng register rank cao ngoại lai); hằng số alpha = 0.7213/(1+1.079/m) bù thiên lệch hệ thống; và linear counting E = m·ln(m/V) cho cardinality nhỏ (V = số register rỗng) — chính là những gì HyperLogLog++ hoàn thiện.

Đo thật: thiếu hiệu chỉnh thì sai thảm hại

Mình tự cài HLL (m=2^12) và so các cách ước lượng trên cùng dữ liệu:

Ảnh chụp bảng kết quả chạy thật HyperLogLog sâu output thật go-lab python HLL tự cài m bằng 2 mũ 12, a harmonic mean vs trung bình thường cardinality lớn N thật 50000 harmonic 50415 sai số 0.83 phần trăm arithmetic 124985 sai số 149.97 phần trăm N thật 200000 harmonic 197923 sai số 1.04 phần trăm arithmetic 498504 sai số 149.25 phần trăm N thật 500000 harmonic 493316 sai số 1.34 phần trăm arithmetic 1233124 sai số 146.62 phần trăm trung bình thường bị register rank cao ngoại lai kéo lệch khoảng 2.5x harmonic mean giảm ảnh hưởng ngoại lai sai số khoảng 1 phần trăm đó là lý do HLL dùng nó, c cardinality nhỏ có vs không linear counting m bằng 4096 N thật 100 có LC 97 sai số 2.86 phần trăm không LC 3002 sai số 2902.28 phần trăm register phần lớn rỗng N thật 500 có LC 499 sai số 0.16 phần trăm không LC 3206 sai số 541.24 phần trăm N thật 2000 có LC 2008 sai số 0.38 phần trăm không LC 4031 sai số 101.56 phần trăm N thật 5000 có LC 5059 sai số 1.19 phần trăm không LC 6104 sai số 22.08 phần trăm vùng nhỏ công thức harmonic hỏng linear counting đếm register rỗng V E bằng m nhân ln m chia V chính xác hơn hẳn cài đặt thật phải có bước này, kết ba hiệu chỉnh làm nên HyperLogLog chạy được 1 harmonic mean thay trung bình thường giảm phương sai do ngoại lai 2 hằng số alpha bù thiên lệch hệ thống của công thức harmonic 3 linear counting ở vùng nhỏ cộng hiệu chỉnh vùng lớn HyperLogLog cộng cộng Google

Hình 2: Chạy thật — (a) harmonic mean cho sai số 0.83-1.34% trong khi trung bình thường sai ~150% (ngoại lai kéo lệch ~2.5x); (c) cardinality nhỏ: có linear counting sai 0.16-2.86%, không có sai từ 22% tới 2902% (N=100).

  • (a) Harmonic thắng trung bình thường một trời một vực: trên cardinality lớn (50k-500k), harmonic mean (công thức HLL) cho sai số 0.83-1.34%, còn một ước lượng ngây thơ dùng trung bình thường của rank sai ~150% — vượt ~2.5 lần ở mọi quy mô. Register rank cao là ngoại lai kéo trung bình cộng lên, và harmonic mean chính là liều thuốc. Đây không phải chi tiết nhỏ — nó là lý do trung tâm HLL hoạt động (LogLog cũ dùng geometric mean, HLL cải tiến bằng harmonic mean, giảm hằng số sai số).
  • (c) Cardinality nhỏ: linear counting là bắt buộc: đây là hiệu chỉnh dễ bị bỏ quên nhất. Với ít phần tử (N=100 trên m=4096 register), phần lớn register còn rỗng, và công thức harmonic hỏng hoàn toàn: không có linear counting, N=100 bị ước lượng thành 3002 — sai 2902%! Nhưng dùng linear counting (E = m·ln(m/V) với V = số register rỗng) cho 97 — sai chỉ 2.86%. Cải thiện cũng dramatic ở N=500 (541% → 0.16%) và N=2000 (101% → 0.38%). Mọi cài đặt HLL nghiêm túc phải chuyển sang linear counting ở vùng cardinality nhỏ.

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

Hằng số alpha nhỏ nhưng cần thiết — và khác nhau theo m. Công thức harmonic thô vẫn thiên lệch có hệ thống một chút (ước lượng lệch một hằng số), và alpha là hệ số nhân bù đúng lệch đó. Giá trị alpha = 0.7213/(1+1.079/m) cho m lớn, nhưng m nhỏ (16, 32, 64) có các hằng số riêng được tính trong bài báo gốc. Bỏ qua alpha làm ước lượng lệch ~2-3% có hệ thống — không thảm họa như hai lỗi trên, nhưng đủ để làm sai số vượt mức lý thuyết. Đừng tự chế hằng số; dùng đúng giá trị của paper.

Vùng chuyển tiếp giữa linear counting và HLL cần xử lý mượt. Cài đặt đơn giản (như demo) chuyển đột ngột từ linear counting sang harmonic ở ngưỡng E ≤ 2.5m. Nhưng ở đúng vùng chuyển tiếp, cả hai công thức đều kém chính xác, tạo một "vết lồi" sai số. HyperLogLog++ (Google, 2013) giải quyết bằng bias correction dựa trên dữ liệu thực nghiệm (bảng tra cứu độ lệch đo được) thay vì công thức cứng — cho đường cong sai số mượt trên toàn dải. Nếu dùng thư viện, chọn cái cài HLL++ chứ không phải HLL gốc.

Hash phải đủ rộng và tốt — 64 bit là chuẩn hiện nay. HLL gốc dùng hash 32 bit, nên với cardinality rất lớn (gần 2^32 ≈ 4 tỉ) xảy ra đụng độ hash (hai phần tử khác nhau băm trùng), làm ước lượng lệch xuống — cần hiệu chỉnh vùng lớn. HLL++ chuyển sang hash 64 bit, đẩy ngưỡng đụng độ lên 2^64 (thực tế không bao giờ chạm), loại bỏ nhu cầu hiệu chỉnh vùng lớn. Dùng hash 64 bit chất lượng (như xxHash, MurmurHash3) là điều kiện tiên quyết cho HLL chính xác ở quy mô lớn.

Ba ý mang về

  1. Trung bình điều hòa là trái tim của HLL: đo thật harmonic mean cho sai số ~1% trong khi trung bình thường sai ~150% — vì register rank cao là ngoại lai kéo lệch trung bình cộng, và harmonic mean giảm ảnh hưởng ngoại lai; đây là cải tiến chính của HLL so với LogLog cũ.
  2. Linear counting cứu vùng cardinality nhỏ: đo thật không có linear counting, N=100 bị ước lượng 3002 (sai 2902%); có linear counting (m·ln(m/V)) cho 97 (sai 2.86%) — mọi cài đặt nghiêm túc phải chuyển sang linear counting khi nhiều register còn rỗng.
  3. Hằng số alpha, hash 64 bit, và bias correction làm nên HLL++: alpha bù thiên lệch hệ thống của công thức harmonic; hash 64 bit loại đụng độ ở cardinality lớn; và HyperLogLog++ (Google) thêm bias correction thực nghiệm cho đường cong sai số mượt — dùng thư viện cài HLL++ chứ không phải HLL gốc.

Nguồn

Phần sau ta chuyển sang bài toán thứ ba: không phải "có trong tập không" hay "bao nhiêu phần tử khác nhau", mà "phần tử này xuất hiện bao nhiêu lần" trong một luồng — Count-Min Sketch đếm tần suất bằng bộ nhớ nhỏ, và ta đo thật mức ước lượng vượt của nó.