Что нужно хранить для того, чтобы была возможность ответить на этот вопрос?
Для точного ответа нужно через равные интервалы времени сохранять множество посетителей сайта (пусть это для простоты будут IP-адреса), которых мы за прошедший интервал увидели. Понятное дело, что такой объём информации хранить нереально, а даже, если получится, придётся объединять большое количество множеств и считать элементы в том множестве, которое получилось в итоге. Это очень долго. Не спасает ситуацию даже переход от точных алгоритмов к приблизительным: гарантировать точность либо не получится, либо придётся использовать объём памяти и вычислительные ресурсы, сопоставимые с точным алгоритмом.
В 80-х годах появились первые вероятностные алгоритмы для приблизительной оценки количества элементов в множестве. При большом количестве уникальных элементов эти алгоритмы дают приблизительную оценку, которая отличается от истинного значения в (1±e), e<1>0.5. То есть они могут вернуть оценку, которая сильно отличается от истинного значения с некоторой вероятностью (1-p). Чем больше требуется точность, и чем меньше нужна вероятность ошибки, тем больше ресурсов требуют алгоритмы. Сохраняя внутреннее состояние одного из таких алгоритмов через равные промежутки времени в базе данных, мы можем оценить приблизительное количество уникальных посетителей не только за произвольный интервал времени, но и за произвольное объединение любых интервалов времени, например, мы можем посчитать общее количество уникальных IP, которых мы наблюдали в промежутке времени с 17:00 до 18:00 в течение последней недели.
В 2000-ные в научном сообществе велась активная работа по достижению теоретически оптимальных характеристик (т.е. потребление памяти, сложность добавления нового элемента, сложность запроса) вероятностных приблизительных алгоритмов для оценки кардинальности (количества элементов в множестве), разрабатывался необходимый инструментарий.
Первый такой алгоритм был предложен в 2010 году. О нём-то мы и поговорим.
Similar to Хочу знать, сколько уникальных посетителей было на моём сайте за произвольный интервал времени в прошлом / Константин Игнатов (Qrator Labs) (20)
2. ∙ Имеем поток элементов
∙
Хотим знать, сколько их было, без повторов
∙ Не хотим хранить поток целиком
Что ищем
2
3. SELECT COUNT DISTINCT . . .
∙ при оптимизации и планировании запросов к БД
∙ при оценке количества уникальных пар SRC-DST
в роутере
∙
при поиске паттернов в больших графах или
массивах данных
∙ при web-аналитике
Где нужно
3
4. U множество всех возможных элементов, n = |U|
S ⊂ U множество уникальных элементов в потоке
F0 = |S| количество элементов в этом множестве
ut ∈ S элемент на позиции t в потоке
Ut ⊂ S множество уникальных элементов, которые мы видели к моменту t
Считаем точно
4
5. ∙
Сколько стоит добавить один элемент в
множество?
∙ сортированный список
∙ битовая маска из n = |U| бит
∙ Сколько стоит посчитать количество элементов?
∙
А если элементов много? Очень?
Считаем точно
4
6. ∙ ˜F0 = (1 ± 𝜀) F0, 𝜀 > 0, 𝜀 ≪ 1
∙
Как гарантировать, что ошибка будет меньше 𝜀?
∙ Насколько меньше памяти можно использовать?
∙
Как быть?
Считаем приблизительно
5
7. ∙ ˜F0 = (1 ± 𝜀) F0, 𝜀 > 0, 𝜀 ≪ 1
∙
Как гарантировать, что ошибка будет меньше 𝜀?
∙ Насколько меньше памяти можно использовать?
∙ Можно добиться линейного уменьшения
∙ Ценой экспоненциального увеличения ошибки
∙
Как быть?
Считаем приблизительно
5
8. 1983 Flajolet, Martin Probabilistic Counting Algorithms for Data Base
Applications
1996 Alon, Matias, Szegedy The Space Complexity of Approximating the
Frequency Moments
2002 Bar-Yossef, Jayram, Kumar, Sivakumar, Trevisan Counting Distinct
Elements in a Data Stream
2003 Durand, Flajolet LogLog Counting of Large Cardinalities
2003 Indyk, Woodruff Tight Lower Bounds for the Distinct Elements Problem
State of art
6
9. 2007 Flajolet, Fusy, Gandouet, Meunier HyperLogLog: the analysis of a
near-optimal cardinality estimation algorithm
2010 Kane, Nelson, Woodruff Optimal Algorithm for the Distinct Elements
Problem
2012 Helmi, Lumbroso, Martinez, Viola Data Streams as Random Permutations:
the Distinct Element Problem
State of art
6
10. ∙ Кратко об этапах работы алгоритма
∙ Подробно об элементах алгоритмов
∙ Хэш-функции
∙
Разделение потока
∙
Статистики
∙
Подробно о том, почему всё это работает
План
7
11. bloom filter: встречался ли этот элемент в потоке?
heavy hiters: какие элементы встречались чаще всего?
F∞moment: как часто встречался самый распространённый элемент
online pattern discovery: какие элементы чаще всего встречались
вместе?
Связанные задачи
8
19. Идея: храним не всю информацию, а только то, что необходимо для ответа
∙ В случае вероятностного алгоритма (вариант):
∙ Храним и обновляем несколько значений x,
вычисленных из увиденного потока
∙
E [f (x)] асимптотически равно оцениваемой
величине
∙ Var [f (x)] должно быть мало
Sketch
10
21. Инициализация
(для 𝜀 = 0.32)
Выбрать большое простое число p, например 5247972723348490517
Выбрать три функции, например:
h(x) = (3813295331020233847x + 2061310418014472126) mod p
g1(x) = (4905134244493838961x + 339751972700966608) mod p mod 1000
g2(x) = (3462845419123084402x3+2920301559511116865x2+4398447080453503003x+
1489413524815015911) mod p mod 10
Создать сжатый массив C из m = 10 элементов, равных 0.
Вариант алгоритма
11
22. Для каждого объекта x
1 r = lsb (h(x)) — количество нулей справа в
бинарном представлении хеша + 1
2 i = g(x)
3 C[i] = max (C[i], r)
Вариант алгоритма
11
23. Получение результата
1 r = min(C)
2 T — количество элементов в C, которые больше r
3 Ответ: 2r+1log(1−T
m )
log(1− 1
m )
Вариант алгоритма
11
24. ∙ Будем рассматривать алгоритм, который:
∙
Отвечает с некоторой точностью 𝜀
F0,𝜀 = F0 · (1 ± 𝜀)
∙ Может ответить с большей ошибкой с
вероятностью (1 − 𝛿) < 1
2
(𝜀, 𝛿)-аппроксимация
Вероятностные алгоритмы
12
25. ∙
Вероятность ошибки: 𝛿 < 1
2
∙
Вероятность не допустить ошибку: 1 − 𝛿 > 1
2
∙ При R повторах:
∙ Биноминальное распределение
∙
Нужно R
2 + 1 или больше верных ответов
∙
R∑︀
i=R
2 +1
(︀R
i
)︀
(1 − 𝛿)i
𝛿R−i
Уменьшение вероятности ошибки
13
26. h : U → {0, 1, 2, 3, . . . , m − 1}
или
h : U →
{︂
0,
1
m
,
2
m
,
3
m
, . . . , 1
}︂
как правило, m |U|
∙
В современных реализациях потоковых
алгоритмов, как правило:
∙
log m = 64 (64-битные хэш-функции).
Хэш-функции
14
27. ∙ Теоретически нам нужно, чтобы вероятность
увидеть любой элемент была одинаковой
∙
На практике так не бывает
∙ Используем хэш-функции (теория):
∙ вероятность встретить определённый хэш 1
m
∙ даже если исходные объекты «скучкованны»
Хэш-функции и случайные величины
15
28. ∙ Абсолютно случайная хэш-функция:
∙ Для каждого элемента в U генерируем
случайное число от 0 до m
∙
Храним n = |U| хэшей в массиве (векторе)
∙
O (n log m) места в памяти
∙
Часто нам нужно несколько хэш-функций...
Память для хэш-функций
16
29. ∙ На практике:
∙ используется быстрая детерминистическая
функция от нескольких переменных
∙
фиксируются все переменные кроме одной
∙ нужно хранить только параметры функции
∙ как правило,O (log m)
Память для хэш-функций
16
43. ∙ При абсолютно случайной хэш-функции все
значения не зависят друг от друга:
Pr (h (x1) = c1, h (x2) = c2, . . . , h (xm) = cm) = 1
mm
∀
{︀
ci ∈ 0, m − 1
}︀
Семейства хэш-функций
18
44. ∙ На практике нам достаточно независимости
только для небольшого числа хэшей k
∙ Выбираем хэш-функцию случайно из
некоторого семейства H
∙ например, для k = 2,(ax + b) mod p mod m,
0 < a < p, 0 b < p:
Pr
h∈H
(h (x1) = c1 ∧ h (x2) = c2) = 1
m2
∀x1, x2 ∈ U, ∀c1, c2 ∈ 0, m − 1
Семейства хэш-функций
18
45. ∙ Для произвольного k
h (x) =
[︂(︂k−1∑︀
i=0
aixi
)︂
mod p
]︂
mod m
Pr (h (x1) = c1 ∧ h (x2) = c2 ∧ . . . ∧ h (xk) = ck) = 1
mk
∀x1 . . . xk ∈ U, ∀c1, . . . ck ∈ 0, m − 1
Семейства хэш-функций
18
46. ∙ Вероятность верного ответа (1 − 𝛿) и память
∙ Вероятность и сложность (время выполнения)
∙ Точность 𝜀 и память:
∙ теоретически 𝜀 = 1√
M
∙
M — количество используемых хэш-функций
Компромиссы
19
47. ∙ В некоторый момент переключаем выполнение
алгоритма с одного исполнителя (Алиса) к
другому (Боб)
∙ Алиса при этом должна отправить Бобу
сообщение. Дальше поток видит только Боб
∙ В конце Боб должен сказать, сколько было
элементов в потоке с (𝜀, 𝛿) аппроксимацией
∙ Размер сообщения и есть потребление памяти
Теоретическое потребление памяти
20
48. ∙
Теоретический предел: O
(︀ 1
𝜀2 + log n
)︀
∙
Наиболее распространённый на практике
алгоритм: O
(︀ 1
𝜀2 log log n
)︀
n = |U| — количество возможных объектов (e.g. 264)
log n — количество бит в бинарном представлении n (e.g. 64)
log log n — количество бит, нужных для записи количества бит в n (e.g. 6)
Теоретическое потребление памяти
20
56. ∙ Сколько чисел из 0, m имеют
∙ k нулей в конце бинарного представления?
∙
k нулей в начале?
∙ k нулей или единиц подряд?
Вероятность «редкой птицы»
23
60. ∙ Итого: в «сломанном» bloom-фильтре примерно
2r
𝜑 элементов
∙ r — номер первого бита 1 справа
∙
𝜑 — константа
∙ Нужно объединить результаты нескольких
экспериментов (иначе будут ответы, кратные
степеням 2)
Вероятность «редкой птицы»
23
61. 𝜑 = 2−1
2 e 𝛾 2
3
∞∏︁
𝜌=1
[︂
(4𝜌 + 1)(4𝜌 + 2)
(4𝜌)(4𝜌 + 3)
]︂(−1) 𝜈(𝜌)
≈ 0.77351
𝜈(𝜌) — количество 1-бит в бинарном представлении 𝜌
𝛾 = −
∞´
0
ln x
ex dx — постоянная Эйлера
Интересная константа
24
62. ∙ Плохо: ответы, кратные степеням 2
∙
Плохо: высокий разброс ошибки
∙ Хотим провести M независимых экспериментов,
не дублируя поток
∙
Заводим M корзинок и раскладываем элементы в
них при помощи ещё одной хэш-функции
∙ как при балансировке нагрузки
∙ одинаковые элементы всегда попадают в один
«подпоток»
Разделение потока
25
63. ∙ NB: Первые элементы (K ∼ m) потока:
∙ NB: Сколько останется пустых корзинок?
∙ Pr (bi) = 1
m Pr (∼ bi) =
(︀
1 − 1
m
)︀
∙ Pr (∼ bi after K) =
(︀
1 − 1
m
)︀K
∙ Pr (bi after K) = 1 −
(︀
1 − 1
m
)︀K
∙ Все корзинки одинаковые:
E (emty) = m
(︁
1 −
(︀
1 − 1
m
)︀K
)︁
Разделение потока
25
64. ∙ применяем «сломанный bloom-фильтр» для
каждой корзинки, считаем среднее
∙ Точность: 𝜀 = 0.78√
M
∙
Память: O
(︀ 1
𝜀2 log n
)︀
∙ Заменив арифметическое среднее на
гармоническое, получим 𝜀 = 1.02√
M
Разделение потока
25
65. ∙ Нам не нужно хранить сами значения в
«регистрах»
∙ Достаточно индекса нужного бита
∙
вот тут и получается O
(︀ 1
𝜀2 log log n
)︀
Наблюдение 1
26
66. ∙ В получившемся массиве почти все числа будут
одинаковыми
∙
Можно хранить минимальное значение отдельно,
а остальное — сжать
∙ Существуют компактные структуры данных с
быстрым обновлением и запросами
∙ Память: O (sum (C)) (на практике: 3–4 1
𝜀2 )
∙ E (#bucket ≈ log F0) = m
(︁
1 −
(︀
1 − 1
m
)︀F0
)︁
∙
остаётся выразить F0
Наблюдение 2
27
67. ∙
Храним sketch за каждые несколько секунд
∙
При запросе объединяем все затронутые
интервалы
∙ учитываем, что min хранится отдельно
∙
Запускаем последний шаг алгоритма
∙ Можем получить оценку не только за
произвольный интервал времени,
∙ но и за объединение любых произвольных
интервалов
Я всё-таки хочу знать...
28
68. Redis
∙ Специальная структура данных
∙
Специальные команды для этой
структуры
∙ Удобно для работы с небольшим
количеством множеств
Реализации
29
69. Clickhouse
∙ Умеет делать запросы с использованием
вероятностных алгоритмов
∙
По-умолчанию uniq() использует
вероятностный алгоритм
∙ Понятие состояния алгоритма и
специальный тип данных
Реализации
29