El objetivo de esta lección es entender como funcionan distintos algoritmos para minería de flujos de datos, es decir información que no está almacenada sino se va adquiriendo de forma incremental.
Nota: Esta lección está basada en el Capítulo 4 del libro Mining of Massive Datasets. Rajaraman, Leskovec and Ullman 2012 y en las notas del curso de Métodos Analíticos del 2013 por Felipe Gonzalez.
En esta lección analizaremos el problema de conteo de elementos distintos en un flujo datos, es dedir, calcular distintos momentos de frecuencia acerca de los elementos en un flujo de datos.
Sea $A = a_1, a_2, \ldots, a_m$ una secuencia de números naturales y sea $m_i = |j: a_j = i|$ el número de veces que $i$ ocurre en la secuencia $A$. El $k$-esimo momento de el flujo está dado por:
$$F_k=\sum^{n}_{i=1}m_i^k$$Cada uno de los momentos de frecuencia nos proporciona una estadística util acerca de los elementos de la secuencia.
Es claro que cuando el número de elementos a contar es demasiado grande para ser almacenados en memoria tenemos que buscar una alternativa.
Almacenar la información sobre los elementos observados en el disco duro haría la tarea sumamante lenta.
Una vez más utilizaremos estructuras de datos probabilísticas para estimar el número de elementos distintos en un flujo.
Para más referencias acerca de los momentos de frecuencia ver:
The space complexity of approximating the frequency moments http://www.tau.ac.il/~nogaa/PDFS/amsz4.pdf
La idea de este algoritmo es tomar una función hash $h$ que mapee los elementos a un número grande de cubetas (de manera uniforme)
La intuición detrás de este algoritmo es que entre más elementos observemos la probabilidad de que observemos un elemento cuya expansión binaria tenga muchos ceros es mayor.
Dado un elemento $a$ y $h(a)$
Es decir que debe tomar cerca de $2^r$ elementos antes de obervar un hash con $r$ ceros.
Dados $m$ elementos en el flujo:
In [386]:
%pylab inline
In [364]:
import random as rndm
NUM_ITEMS = 10000
rnd = rndm.Random(11)
M = 8388133
lsk = int(numpy.ceil(numpy.log2(M)))
p = 15485863
a = rnd.randint(0, p - 1)
b = rnd.randint(0, p - 1)
def hasher(x, va, vb):
return ((va * x + vb) % p) % M
def get_lsb(bits):
for i in range(lsk):
if (bits & (0x01 << i)) != 0:
return i + 1
def update(items):
for item in items:
lsb = get_pos(item)
def get_pos(item):
h = hasher(item, a, b)
return get_lsb(h)
In [365]:
items = [rnd.randint(0,NUM_ITEMS) for i in range(1000000)]
pos = [get_pos(item) for item in items]
plt = hist(pos)
Podemos notar de forma empírica que la probabilidad de observar un hash con un mayor número de ceros dismuye exponencialmente.
Sin embargo este método presenta dos problemas:
Si utilizamos, múltiples funciones hash $h_k(a)$ para cada valor $a$ podemos obtener una mejor estimación de $m$. Sin embargo no podemos promediar ya que el valor esperado de $2^R$ es infinito
El método de medianas de promedios tiene un mejor desempeño entre más grande sea el tamaño de los grupos.
In [405]:
import time
LONG_NUM_ITEMS = 1000000
total = set()
max_pos = 0
result = list()
error = list()
for l in range(1000):
items = [rnd.randint(0, LONG_NUM_ITEMS) for i in range(100)]
total.update(items)
pos = [get_pos(item) for item in items]
max_pos = max(max(pos), max_pos)
estimate = 2**max_pos
tot = len(total)
result.append((tot, estimate, tot - estimate))
error.append(((estimate / float(tot)) - 1) * 100)
In [406]:
subplot(2,1,1)
plt = plot(result)
subplot(2,1,2)
plt = plot(error)
En el ejemplo con un solo hash podemos ver como el estimado es bastante malo dado que no hay nada que equilibre una mala predicción.
A continuación utilizaremos múltiples hashes y la mediana de los promedios.
In [391]:
seeds = list()
NUM_HASHES = 2**8
GROUP_SIZE = 16
for _ in range(NUM_HASHES):
la = rnd.randint(0, p - 1)
lb = rnd.randint(0, p - 1)
seeds.append((la, lb))
def update_pos(item, ints):
for i in range(NUM_HASHES):
h = hasher(item, seeds[i][0], seeds[i][1])
ints[i] = max(get_lsb(h), max_ints[i])
def get_estimate(ints):
nh = NUM_HASHES / GROUP_SIZE
averages = list()
for group in range(nh):
averages.append(2**(sum(ints[group:group + GROUP_SIZE]) / GROUP_SIZE))
return numpy.median(averages)
In [407]:
total_2 = set()
result_2 = list()
error_2 = list()
max_ints = [0] * NUM_HASHES
for l in range(1000):
items = [rnd.randint(0, LONG_NUM_ITEMS) for i in range(100)]
total_2.update(items)
for item in items:
update_pos(item, max_ints)
estimate = get_estimate(max_ints)
tot = len(total_2)
result_2.append((tot, estimate, tot - estimate))
error_2.append(((estimate / float(tot)) - 1) * 100)
In [408]:
subplot(2,1,1)
plt = plot(result_2)
subplot(2,1,2)
plt = plot(error_2)
La idea es utilizar el mayor número de hashes posibles de tal forma que los promedios nos den un mejor aproximado a valor real.
El límite es que tanta capacidad de procesamiento tenemos para calcular los hashes.
Más información sobre el algoritmo de FM, está disponible en su paper: Probabilistic Counting Algorithms for Database Applications http://www.mathcs.emory.edu/~cheung/papers/StreamDB/Probab/1985-Flajolet-Probabilistic-counting.pdf
Otro algoritmo útil para aproximar los elementos distintos en un flujo es el algortitmo de K-Minimum Values.
La intuición es la siguiente, si los hashes están uniformemente distribuidos, es posible estimar el número de elementos observados a partir del espacio promedio entre los hashes.
Una manera de estimar el espacio entre los hashes es manteniendo referencia al valor más chico. De tal forma que si $X = min(S)$ podemos esperar que $n$ esté en orden de $1/X$. Sin embargo una vez más nos enfrentamos al probema de la varianza.
Para mejorar el aproximado mantenemos los $k$ valores más pequeños y estimamos utilizando $(k - 1)/max(\text{kmv})$
Podemos observar que este algoritmo funciona razonablemente bien y a un costo de procesamiento mucho menor.
Counting Distinct Elements in a Data Stream http://dl.acm.org/citation.cfm?id=711822
In [397]:
import heapq as hq
total_3 = set()
result_3 = list()
error_3= list()
min_items = []
kmvk = 3
for l in range(1000):
items = [rnd.randint(0, LONG_NUM_ITEMS) for i in range(100)]
total_3.update(items)
for item in items:
hv = hasher(item, a, b)
if len(min_items) < kmvk:
hq.heappush(min_items, -hv)
elif -hv > hq.nsmallest(1, min_items)[0]:
hq.heappop(min_items)
hq.heappush(min_items, -hv)
min_item = hq.nsmallest(1, min_items)
estimate = (kmvk - 1) / (-min_item[0] / float(M))
tot = len(total_3)
result_3.append((tot, estimate, tot - estimate))
error_3.append(((estimate / float(tot)) - 1) * 100)
In [398]:
subplot(2,1,1)
plt = plot(result_3)
subplot(2,1,2)
plt = plot(error_3)
Uno de lo algoritmos más populares para el conteo de elementos distintos es el algoritmo de HyperLogLog.
La idea principal del algoritmo es emular el efecto de realizar $m$ hashes utilizando un promedio estocástico a partir de una sola función hash.
Esto se hace dividiendo el flujo en sub-flujos que corresponden a una partición de la unidad.
Utilizando los primeros $k$ bits del hash para seleccionar la partición y el resto de los bits para calcular la cola de ceros para cada partición $R_j$.
La intución es la siguiente, si partimos el flujo en $n/m$ elementos entonces podemos utilizar el promedio de $\bar{R}$ en las $m$ cubetas para calcular la cardinalidad utilizando $m * 2^\bar{R}$
El estimado HLL corrige un sesgo importante utilizando la media armónica de $2^{R_j}$
$$E_{\text{HLL}} = \frac{\alpha_{m}m^2}{\sum_{j=1}^{m}{2^{-R_j}}}$$Donde $\alpha_m$ es una constante que corrige el sesgo para pra valores bajos de $m$.
Este algoritmo presenta un error estandar de $1.04/\sqrt{m}$. Por ejemplo para estimar cardinalidades de flujos arriba de $10^9$ con 2% de precisión solo se necesitan 1.5 KB de memoria.
Adicionalmente éste algoritmo es altamente paralelizable ya que múltiples máquinas pueden correr el algoritmo utilizando el mismo hash y $m$.
HyperLogLog: the analysis of a near-optimal cardinality estimation algorithm http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf
HyperLogLog in Practice: Algorithmic Engineering of a State of The Art Cardinality Estimation Algorithm http://static.googleusercontent.com/media/research.google.com/en/us/pubs/archive/40671.pdf