Участник:Warmte

Материал из Викиконспекты
Перейти к: навигация, поиск

HyperLogLog — это вероятностный алгоритм, оценивающий количество различных элементов в больших потоках данных. Является стриминговым алгоритмом (англ. streaming algorithm), то есть обрабатывает последовательно поступающие данные в один проход.

Подобные алгоритмы используются в тех случаях, когда объемы обрабатываемых данных настолько велики, что получение точного ответа затребует пропорционально слишком большого объёма памяти, в то время как вероятностный алгоритм может дать близкий к точному ответ, будучи с точки зрения памяти намного более оптимальным.

Точность

При использовании [math]m[/math] единиц дополнительной памяти алгоритм HyperLogLog оценивает количество различных элементов со стандартной ошибкой около [math]\frac{1.04}{\sqrt{m}}[/math], то есть при оценке данных > 109 этому алгоритму потребуется всего 1.5 kB памяти для получения ответа с точностью 2%.

Предыдущий алгоритм LogLog, использовавшийся для решения этой задачи, достигал сравнимой точности ответа при использовании 64% от оригинальных объемов памяти.

Идея алгоритма

В основе алгоритма HyperLogLog лежит наблюдение, что мощность набора равномерно распределенных случайных чисел можно оценить, вычислив максимальное количество ведущих нулей в двоичном представлении каждого числа в наборе. Таким образом, если максимальное наблюдаемое количество начальных нулей равно [math]n[/math], то оценка количества различных элементов в наборе будет [math]2^n[/math].

Суть алгоритма заключается в следующем: к каждому элемента исходного набора применяется хеш-функция для получения набора равномерно распределенных случайных чисел с той же мощностью, что и исходный набор. Затем мощность этого случайно распределенного набора оценивается с помощью описанного выше алгоритма.

Но при таком подходе могут возникать различные проблемы за счёт большой дисперсии получаемой величины, а также некоторых крайних случаев: к примеру, если хэш некоторого элемента будет равен [math]0[/math], то максимальное количество ведущих нулей сразу станет равным [math]log_2{m}[/math], где [math]m[/math] — максимальное значение выбранной хэш-функции. Чтобы избежать подобных проблем и минимизировать дисперсию, модифицируем алгоритм следующим образом: разделим исходный поток элементов на [math]2^r[/math] корзин, для каждой из них вычислим максимальное наблюдаемое количество начальных нулей среди элементов в этой корзине, а затем на основе полученных для всех корзин значений вычислим итоговый ответ на задачу.

Описание алгоритма

  • [math]\mathcal{M}[/math] — исходный набор элементов [math]a_1 ... a_n[/math].
  • [math]h : \mathcal{U} \to {0, 1 ... 2^k - 1}[/math] — выбранная хэш-функция с возможными значениями от [math]0[/math] до [math]m - 1[/math], [math]k = log_2{m}[/math].
  • [math]r[/math] — количество бит исходного хэша, характеризующих номер корзины, в которую будет отправлен соответствующий элемент.
  • [math]z_j[/math] — максимальное наблюдаемое количество начальных нулей в корзине под номером [math]j[/math].

Разобьём исходный набор [math]\mathcal{M}[/math] на наборы [math]\mathcal{M}_0..\mathcal{M}_{2^r - 1}[/math] следующим образом: для каждого поступающего элемента [math]a_i[/math] вычислим его хэш [math]h(a_i)[/math] и представим его в двоичном виде. Тогда первые [math]r[/math] бит этого двоичного представления будут характеризовать номер корзины [math]j[/math], а оставшиеся биты сформируют остаточный хэш [math]h'(a_i)[/math], который и будет использоваться для поиска максимального количества начальных нулей в корзине [math]\mathcal{M}_j[/math].

Hll bins.jpg

Для каждой корзины вычислим соответствующее [math]z_j[/math], равное максимальному количеству ведущих нулей среди элементов этой корзины. Тогда оценкой для числа различных элементов в корзине будет [math]2^{z_j}[/math].

Логично было бы предположить, что в таком случае итоговым ответом на задачу будет [math] \sum\limits_{j=0}^{2^{r - 1}} 2^{z_j} [/math], но такой подход приводит не к самому выгодному результату. Выгоднее всего будет пересчитывать результат при помощи среднего гармонического всех полученных оценок:

[math]result \approx \alpha \cdot (2^r)^2 \cdot I[/math]

  • [math]I[/math] — это индикатор, вычисляемый по формуле [math]I = (\sum\limits_{j=0}^{2^{r - 1}} \frac{1}{2^{z_j}})^{-1} [/math]
  • [math]\alpha[/math] — корректирующий множитель, вычисляемый по формуле [math]\alpha = (2^r \int\limits_{0}^{\infty} (log_2{\frac{2 + u}{1 + u}})^{2^r} \,du )^{-1}[/math].

Поскольку множитель [math]\alpha[/math] может быть достаточно сложным для вычисления, можно подобрать его примерное значение в зависимости от [math]r[/math]:

[math] \alpha_{r}\approx\begin{cases} r=4 & 0.673\\ r=5 & 0.697\\ r=6 & 0.709\\ r\ge7 & \frac{0.7213}{1+\frac{1.079}{2^r}} \end{cases} [/math]

Асимптотика

Оценка времени работы: [math]\mathcal{O}(n)[/math], где [math]n[/math] — количество элементов в исходном наборе.

Оценка дополнительной памяти: [math]2^r \cdot log_2(k) = 2^r \cdot log_2 log_2 m [/math], где [math]m[/math] — количество возможных значений хэш-функции.


Источники информации