Небольшая предыстория. Этот пост я написал для двух целей. Во-первых, обкатать конвертор разметки Markdown + inline_formula в хабрачитаемый вид. Во-вторых, рассказать об интересной задаче из data streaming. К концу написания, я обнаружил пост про LogLog четырехлетней давности. На мою удачу автор предыдущего поста делал упор на реализацию. Я же, полагаясь на inline_formula, расскажу больше о математике.

Давайте представим, что у нас есть роутер. Через роутер проходит много пакетов по разным адресам. Нам интересно получить статистику, как много адресов задействовано в коммуникации. Есть пара проблем.

  • Пакетов так много, что запомнить их все нельзя. Сказать ушедшему пакету «Вернись! Я все прощу,» — тоже.
  • Всех возможных адресов inline_formula. Столько памяти на роутере нет.

some title

Задача. Есть последовательность целых чисел inline_formula, все числа принимают значения от inline_formula до inline_formula. Требуется в один проход посчитать количество различных чисел, используя inline_formula памяти.

Я расскажу вероятностный приближенный алгоритм Флажолета-Мартина. ТТХ алгоритма:

  • использует inline_formula памяти!
  • работает на любом входе;
  • находит ответ, который отличается от точного не более чем в 3 раза с вероятностью inline_formula:


    вероятность берется по случайным битам алгоритма.

В конце поста я расскажу, почему точные детерминированные алгоритмы требуют inline_formula памяти.

Алгоритм Флажолета-Мартина


Представим, что у нас есть отрезок действительных чисел inline_formula. На отрезок мы независимо случайно кидаем inline_formula точек согласно равномерному распределению. Какое будет расстояние между крайней левой точкой и нулем?

Можно предположить, что точки разделят отрезок на inline_formula меньших подотрезков примерно одинаковой длины. Если аккуратно записать матожидание расстояния и взять интеграл, мы получим


Пусть кто-то кинул случайно несколько точек на отрезок, и inline_formula — расстояние от нуля до крайней левой точки. Можно прикинуть, что всего точек порядка inline_formula.

Идея алгоритма Флажолета-Мартина — случайно бросить все числа последовательности inline_formula на отрезок inline_formula, а затем найти расстояние от нуля до крайней левой точки. Если одинаковые числа будут всегда отображаться в одну точку, а разные независимо распределяться по отрезку, мы сможем прикинуть ответ.

2-независимые хэш-функции


Бросать числа на отрезок мы будем с помощью случайной хэш-функции.

Определение. Семейство хэш-функций inline_formula называется 2-независимым, если для любых inline_formula и inline_formula


Смысл определения в следующем. Зафиксируем два каких угодно ключа inline_formula и inline_formula.
Ключи — различные. Посмотрим на случайные величины inline_formula и inline_formula. Случайность задается выбором функции inline_formula. Тогда, по определению, величины inline_formula и inline_formula будут вести себя как независимые.

Как следствие, если взять всего один ключ inline_formula, то величина inline_formula будет равномерна распределена по inline_formula.

Для примера возьмем простое число inline_formula. Пусть inline_formula. inline_formula — это семейство всех линейных отображений по модулю inline_formula:


для inline_formula. Тогда


Поскольку inline_formula, система имеет ровно одно решение среди inline_formula возможных:


Поймем два важных момента. Во-первых, хранение такой функции обходится в inline_formula памяти, что очень немного. Во-вторых, если внимательно приглядеться, то можно понять, что вычисления проходят в поле inline_formula, и могут быть обобщены для любого конечного поля.

В тестовом коде я буду использовать поле Галуа inline_formula. В описании далее можно считать, что у нас есть семейство хэш-функций inline_formula, где inline_formula — степень двойки. Хранение одной функции занимает inline_formula памяти.

Алгоритм


Пусть inline_formula — степень двойки.
Перед стартом, алгоритм случайно выбирает хэш-функцию inline_formula из 2-независимого семейства.

Будем бросать элементы последовательности на отрезок inline_formula. Берем очередное значение inline_formula и записываем: ноль, точка, inline_formula в двоичном виде. Например, если inline_formula, то получится число inline_formula.

Обозначим через inline_formula число лидирующих нулей в двоичной записи inline_formula. Пусть inline_formula. Мы знаем, что минимальное значение лежит между inline_formula и inline_formula.

Ответ алгоритма: inline_formula.

def init():
	h = H.sample()
	z = 0
	
def process(a):
	z = max(z, zero(h(a))
	
def answer():
	return 2**(z + 0.5)

Анализ



Я планирую показать, что ответ алгоритма будет в 3 раза больше верного с вероятностью меньшей inline_formula. Аналогично, алгоритм вернет ответ в 3 раза меньше верного вероятностью меньшей inline_formula. Если вы не планируете углубляться в математику, смело переходите к следующей части.

Обозначим через inline_formula множество всех различных чисел последовательности inline_formula, а inline_formula — их количество.

Для анализа алгоритма, нам потребуются два набора случайных величин.

  • inline_formula. Принимает значение 1, если количество лидирующих нулей в двоичной записи inline_formula хотя бы inline_formula; иначе — inline_formula.
  • inline_formula. Величина inline_formula больше нуля, если переменная inline_formula по завершении алгоритма оказалась не меньше inline_formula.

Заметим, что вероятность inline_formula: величина inline_formula равномерно распределена по отрезку inline_formula; inline_formula — степень двойки; есть всего inline_formula чисел с inline_formula лидирующими нулями.

Значит, матожидание inline_formula. Ограничим сверху дисперсию


Заметим, что дисперсия по величинам inline_formula линейна. Для любых двух inline_formula и inline_formula




Поскольку inline_formula и inline_formula независимы, то inline_formula. Значит,


Более того, inline_formula, поскольку величины inline_formula — 2-независимы.

Теперь рассмотрим величину inline_formula.

  • inline_formula по линейности матожидания.
  • inline_formula по линейности дисперсии для 2-независимых величин.

Пусть inline_formula — минимальное число такое, что inline_formula. Событие «алгоритм выдал ответ в 3 раза больше нужного» равносильно событию inline_formula и равносильно событию inline_formula. Применяя неравенство Маркова, ограничим вероятность


Пусть inline_formula — максимальное число такое, что inline_formula. Аналогично, событие «алгоритм выдал ответ в 3 раза меньше нужного» равносильно событию inline_formula и равносильно событию inline_formula. Применяя неравенство Чебышёва, получим


Финальный аккорд: медиана


Осталось понять как понизить ошибку. Возьмем случай, когда алгоритм выдает слишком большой ответ. Запустим алгоритм параллельно inline_formula раз и вернем медиану среди ответов. Я утверждаю, что если inline_formula, то алгоритм ошибется с вероятностью не больше inline_formula. Аналогично, ограничив ошибку в другую сторону, получим


Почему медиана так работает? По неравенству Чернова. Заведем случайную величину inline_formula. Величина inline_formula равна единице, если ответ алгоритма на inline_formula запуске меньше inline_formula. Вероятность этого события не меньше 0.52.

Если медиана inline_formula запусков алгоритма больше inline_formula, то это значит, что алгоритм хотя бы половину раз выдал ответ больший inline_formula и inline_formula. Тогда по неравенству Хефдинга-Чернова


где inline_formula — некоторая константа. Другой случай показывается аналогично.

Нижняя оценка для точного алгоритма


Давайте представим, что кто-то действительно придумал детерминированный алгоритм, который в один проход находит точное число различных элементов в один проход. Мы покажем, что такой алгоритм должен использовать inline_formula памяти.

Возьмем множество inline_formula размера inline_formula и положим его в качестве начала последовательности. Скормим эту часть алгоритму и посмотрим на его память.

Из одной только памяти алгоритма можно извлечь все множество inline_formula. Если скормить в текущем состоянии число inline_formula, ответ алгоритма не изменится; если inline_formula, то увеличится на 1. Значит, каждому множеству inline_formula должно соответствовать свое уникальное состояние памяти.

Различных подмножеств из inline_formula размера inline_formula примерно inline_formula. Если мы хотим каждому множеству сопоставить битовую строку, нам потребуется inline_formula

Что почитать


  1. «Probabilistic Counting Algorithms for Data Base Applications», Flajolet, Martin, 1983, link.
  2. «The space complexity of approximating the frequency moments», Alon, Matias, Szegedy, 1999, link.

Комментарии (12)


  1. EighthMayer
    28.07.2015 15:39

    Прошу прощения, вопрос скорее всего очень глупый (учитывая что я не понял почти ничего из того, что ниже хабраката), но если исходить из «пары проблем», то почему-бы не использовать битовую маску на 2^32 (4294967296) бита (512 мегабайт), где каждый бит соответствует одному адресу?

    Если адреса в массиве не уникальны, то можно использовать ещё одну такую-же маску (+512 мегабайт) чтобы полностью обойти это ограничение.


    1. ripatti
      28.07.2015 15:43
      +3

      Представьте, что у вас нет 512 Мб, а есть только 1 Кб.


    1. SeptiM Автор
      28.07.2015 15:44

      Да, можно, но такой метод требует слишком много памяти. Идея в том, как пожертвовать немного точностью, чтобы получить алгоритм, который использует всего пару килобайт. Кроме того, если длина адреса вырастет до 64 бит, первый метод совсем отвалится.


  1. impwx
    28.07.2015 16:09
    +13

    Из постановки задачи никак не следует, что для нее допустимо решение с погрешностью 300%.


    1. SeptiM Автор
      28.07.2015 16:16

      Не следует. Задача определяет цель, к которой мы стремимся. В следующем абзаце я анонсирую, что будет рассказано. В частности, что в точной формулировке задача не имеет решения.

      В конце поста я расскажу, почему точные детерминированные алгоритмы требуют inline_formula памяти.


    1. SeptiM Автор
      28.07.2015 16:18

      Немного оффтоп, но… Чем для вас является точность? Абсолютная цель или ресурс, которым можно пожертвовать?


      1. impwx
        28.07.2015 16:41
        +3

        Не поймите меня неправильно, я ни в коем случае не оспариваю необходимость вероятностных алгоритмов, но чем является точность — зависит не от моего личного мнения, а только от задачи. Если нужно показать красивый график — конечно, она не так важна. А если, например, по результатам этого подсчета будет составляться план модернизации сети и закупаться оборудование — то сами понимаете, троекратная переплата может оказаться неприемлемой.


        1. SeptiM Автор
          28.07.2015 16:53
          +4

          Ага, т.е. вас больше всего беспокоит то, что алгоритм гуляет с 300% ошибкой, которую еще и нельзя регулировать. Это действительно проблема, но решаемая.

          Есть усиление этого алгоритма, которое для произвольного inline_formula возвращает ответ в пределах inline_formula с вероятностью inline_formula и использует памяти примерно inline_formula. Можно посмотреть тут. Я думал про неё написать, но пост будет перегруженным.

          Если будет большой интерес, могу написать пост про усиление.


          1. datacompboy
            28.07.2015 17:32
            +2

            Надавата будет.


  1. ishevchuk
    28.07.2015 23:14

    Спасибо за статью!
    Совершенно случайно не знаете, используют ли какие-то сетевые железки именно этот алгоритм для подсчета количества соединений?
    И если да, то какие?


    1. SeptiM Автор
      29.07.2015 00:35

      Спасибо вам! :)

      У меня нет знакомых железячников, но в целом, когда речь заходит о поиске аномалий в сетевом траффике на этот результат ссылаются. Причем ссылаются скорее как на первичную идею, которую потом активно допиливали. Обычно используют HyperLogLog или что-то близкое к нему.

      Знаю соц. сеть, которая считает лайки HyperLogLog-ом. =)


      1. AlexWinner
        29.07.2015 06:50

        Считаем им уникальных посетителей (правда, сами не реализовывали, просто грузим лог в ElasticSearch, по примерно 1млрд запросов в сутки).