Algoritmos avanzados bajo big data: hiperloglog, contando el número de elementos diferentes bajo datos masivos

Si lo entrevistan para redis, por lo general, la otra parte le preguntará qué estructura de datos ha utilizado. Si dice que ha utilizado hyperloglog, definitivamente es un elemento positivo, porque la otra parte sabe que está lidiando con problemas basados ​​en datos masivos y alta concurrencia. En la sección anterior, usamos el algoritmo min-count-sketch para contar el número de repeticiones de un elemento dado bajo datos masivos, mientras que hyperloglog es todo lo contrario, cuenta el número de elementos diferentes en todo el conjunto de datos.

En los escenarios de aplicaciones tradicionales, la forma común de lograr este objetivo es usar una tabla hash. Recorremos todos los elementos una vez, luego verificamos si la tabla hash ya tiene un elemento correspondiente y, finalmente, recorremos la tabla hash nuevamente para obtener los diferentes elementos. número El problema con este enfoque es que, en el caso de datos masivos, es probable que la tabla hash almacene una gran cantidad de datos, especialmente cuando hay pocos elementos repetidos, la memoria ocupada por la tabla hash es grande y los elementos de datos son estructuras complejas En este caso, la memoria ocupada aumentará aún más.

Al igual que en la sección anterior, los algoritmos en escenarios de big data siguen una rutina, que consiste en intercambiar precisión por ahorro de memoria. Cuanta más memoria se guarde, la precisión disminuirá en consecuencia. Por lo general, el algoritmo utilizará docenas de G La memoria se reduce a unos pocos M, y la precisión se controla en torno al 99 %, en el caso de datos masivos, esta precisión es completamente aceptable. Echemos un vistazo al flujo de algoritmo específico.

Pensemos primero en una pregunta. Supongamos que hay una moneda justa, es decir, la probabilidad de que salgan caras y caracteres es de 0,5. Supongamos que lanzamos la moneda N veces y encontramos que salen caras 100 veces. ¿Cuál es el valor de N? Como la probabilidad de cara es 0,5, podemos estimar que el valor de N es 100 / 0,5 = 200. El primer paso de HyperLogLog se diseñó a partir de esta idea. Supongamos que tenemos un conjunto de datos con n elementos, que contiene k elementos diferentes. Si queremos lograr el efecto de "lanzamiento de moneda" mencionado anteriormente, podemos usar una función hash, cuya salida es una longitud L La cadena binaria de , es decir, la cadena contiene L caracteres y los caracteres son 0 o 1. Si el valor de L es lo suficientemente grande, entonces podemos hacer hash de diferentes datos de entrada para diferentes resultados de salida.Si el conjunto de datos contiene k datos diferentes, entonces el resultado de salida tendrá k valores diferentes.

El problema con el enfoque anterior es que aún necesitamos almacenar todos los resultados. Si el valor de L es mayor que el espacio de almacenamiento requerido por los elementos del conjunto, el algoritmo necesita más espacio, por lo que debemos optimizarlo. Lo siguiente Introducimos un método de optimización llamado conteo de probabilidad. Su principio es: después de obtener el resultado hash, contamos el número de 0 de derecha a izquierda, luego agregamos 1 al resultado y usamos p para marcar el resultado. Por ejemplo, "1100", hay 2 0 de derecha a izquierda, por lo que el valor p es 2+1=3, para "0111", es 1 desde la primera posición, por lo que el valor p correspondiente es 0+1= 1, para "0000", hay 4 0 de derecha a izquierda, por lo que el valor p correspondiente es 4 + 1 = 5. Calculamos el valor hash de todos los elementos y luego calculamos el valor p más grande, que denotamos como p (max), entonces la estimación del número de elementos diferentes es 2^p(max). Debe recordarse de antemano que el resultado obtenido por este método no es exacto, y continuaremos mejorando su precisión más adelante.

Veamos la lógica básica del algoritmo. Suponiendo que el resultado del cálculo de la función hash es lo suficientemente aleatorio, si hay k elementos diferentes en una matriz que contiene n elementos, entonces esperamos que entre los k elementos diferentes, la mitad de los elementos más a la derecha del resultado hash tomen el valor 0 , la otra mitad de los elementos más a la derecha toman el valor 1. A continuación, pasaremos al siguiente paso para la parte con el valor 0 (incluidos k/2 elementos). En esta parte de elementos, el penúltimo elemento del resultado hash tiene un valor de 0 y la mitad del cual tiene un valor de 1, es decir, el número de elementos en cada parte es k/4, es decir, el más a la derecha elemento del resultado hash El número de elementos con ambos valores de 0 es k/4, y así sucesivamente, el número de elementos con el valor 0 en el elemento i más a la derecha del resultado hash es k/(2^i ), por lo que de acuerdo con nuestro anterior La lógica de predecir el número de lanzamientos de monedas es la misma. Si el número máximo de 0 desde la derecha es p_max, entonces podemos estimar que el número de elementos diferentes es 2^p_max. Aquí debemos enfatizar nuevamente que esto es solo una estimación de probabilidad, y el resultado no es lo suficientemente preciso.Echemos un vistazo a su implementación de código;

import hashlib
import random


def convert_hash_to_binary(hash_hex):
    # 将哈希字符串转换为包含0和1的字符串
    return bin(int(hash_hex, 16))


def num_trailing_zeros(hash_bin):
    # 从右到左统计0的个数一直到遇见1停止
    reverse = hash_bin[::-1]
    count = 0
    for i in range(len(reverse)):
        if reverse[i] == '0':
            count += 1
        else:
            break

    return count + 1


def probability_counting(array):
    # 估算给定数组中不同元素的个数
    p_max = 0
    for a in array:
        hash_str = hashlib.sha256(str(a).encode()).hexdigest()
        bin_str = convert_hash_to_binary(hash_str)
        p = num_trailing_zeros(bin_str)
        if p > p_max:
            p_max = p
            # print(f"str: {bin_str} with trailing zeros: {p_max}")

    return 2 ** p_max


ELEMENT_COUNT = 100000  # 随机创建给定个取值位于(0, 10000)之间的整数


def generate_random_array():
    count = 0
    array = []
    diff_count = 0
    diff_map = {
    
    }
    same_count = 0
    while count < ELEMENT_COUNT:
        num = random.randint(0, 10000)
        array.append(num)
        if num not in diff_map:
            diff_map[num] = True
            diff_count += 1
        else:
            same_count += 1
        count += 1

    # print(f"diff count: {diff_count}, same count : {same_count}")
    return array, diff_count


array, differ_count = generate_random_array()

probability_count = probability_counting(array)

print(f"different elements cont: {
      
      differ_count}")
print(f"probability count: {
      
      probability_count}")

En el código anterior, generamos 100,000 elementos, cada elemento toma un valor entre (0,10000), luego registramos la cantidad de elementos diferentes y finalmente usamos la estimación de probabilidad para predecir la cantidad de elementos diferentes. Los resultados de la ejecución del código son los siguientes :

different elements cont: 10000
probability count: 8192

Se puede ver que los resultados estimados no son precisos, pero esta idea es el comienzo del algoritmo hyperloglog. A continuación, mejoramos el algoritmo descrito anteriormente. Este algoritmo mejorado se llama promedio aleatorio. Su método es sacar los b bits en el extremo derecho del resultado hash y colocarlos en m = 2^b "cubos" de acuerdo con el resultados. ", y luego para cada elemento en el "cubo", calcule su p_max correspondiente a su vez, y luego sume estos p_max y divídalo por m, es decir, A = p_1_max + p_2_max +... + p_m_max, donde p_i_max representa el i-th El p_max correspondiente a cada cubo, el valor estimado final es m * (2 ^ A), veamos la implementación del código:

def get_bits_val(bin_str, b):
    # 例如 1001011, 取前边4个比特位,1001,其对应数值就是9
    b_bit_str = bin_str[0:b]
    b_bit_val = int(b_bit_str, 2)
    return b_bit_val


def first_bits(h, b):
    # h 将h对应的哈希值转换为只包含0,1的二进制字符串,然后去最右边b个比特位,并计算他们形成的数值
    bin_str = convert_hash_to_binary(h)
    bit_val = get_bits_val(bin_str, b)
    return bit_val


# 下面代码打印结果为9
#print("fist 4  bits of 1001111: ", get_bits_val("1001011", 4))

b = 14  # 桶的个数为2 ^ 14 
bucket_map = {
    
    }


def stochastic_average(array):
    bucket_count = 0
    for a in array:
        hash_str = hashlib.sha256(str(a).encode()).hexdigest()
        bin_str = convert_hash_to_binary(hash_str)
        p = num_trailing_zeros(bin_str)

        # 将哈希结果转换为二进制,取最左边b个比特值计算当前元素哈希值所在的桶
        bucket = first_bits(hash_str, b)
        if bucket in bucket_map:
            #记录每个桶元素从右边算起0做多的个数
            if p > bucket_map[bucket]:
                bucket_map[bucket] = p
        else:
            bucket_map[bucket] = p
            bucket_count += 1

    p_max_sum = 0
    m = 2 ** b

    for i in range(m):
        if i in bucket_map:
            p_max_sum += bucket_map[i]

    avg = p_max_sum / bucket_count 
    return bucket_count * (2 ** avg)


stochastic_count = stochastic_average(array)
print(f"result of stochastic_average is {
      
      stochastic_count}")

Después de ejecutar el código anterior, el resultado es el siguiente:

different elements cont: 10001
result of stochastic_average is 25158.04266464522

Para ser honesto, no parece que las mejoras vayan allí. Por lo tanto, haremos más mejoras. El algoritmo para completar este paso se llama loglog. Esta mejora se basa principalmente en el resultado del paso anterior multiplicado por un parámetro. El valor de este parámetro está relacionado con la m en el algoritmo anterior, que es el número de cubetas. Estos Los parámetros son:
a(m) = 0.39701 - (2*(pi^2) + (ln(2)) ^ 2) / 28m

Si el valor de m es mayor a 64, entonces a(m) puede tomar directamente el valor de 0.39701, multiplicamos el resultado anterior por este valor y el resultado es 9887.99, que es aproximadamente igual a 9888. Se puede ver que está muy cerca. Desde la perspectiva de las estadísticas matemáticas, después de multiplicar por el parámetro a(m), la tasa de error es 1/sqrt(m), cuando b=14, este valor es de alrededor del 1%. Según el algoritmo actual, la ocupación de la memoria está principalmente en el "cubo". Si establecemos el tamaño de un cubo en 8 bytes, entonces cuando el número de cubos se establece en 2^14, la memoria debe ser de aproximadamente 130 kb, y al algoritmo no le importa la cantidad de datos que desea procesar, no importa cuán grande sea, la tasa de error puede permanecer sin cambios.

Además, si podemos confirmar que la cantidad de elementos diferentes en el conjunto de datos no excede k-max como máximo, entonces solo necesitamos bits log(k-max) para el resultado dado por la función hash (por ejemplo, solo se necesitan 5 bits para 32 correspondientes a binario Además, dado que cada depósito se usa para almacenar el número de ceros contados de derecha a izquierda después de que el resultado hash se convierte a binario, el tamaño de memoria requerido para un depósito es log(log(k -max)) Bits, esto puede ser un poco confuso, específicamente, suponiendo que el resultado del hash se convierte a formato binario y no supera los 64 bits como máximo, lo que significa que el número de 0 de derecha a izquierda no supera los 64, entonces el conteo solo usa 6 bits, porque 2 ^ 6 = 64, que es de donde proviene el nombre del algoritmo loglog. Por lo tanto, si determinamos que la cantidad de elementos diferentes en los datos no excede k-max = 2 ^ 64, si la cantidad de cubos se establece en 2 ^ 14, entonces la memoria total requerida es (2 ^ 14) * log (log(2 ^ 64)) es aproximadamente igual a 12kb,

Finalmente, mejoramos el algoritmo LogLog anterior nuevamente para obtener el algoritmo SuperLogLog. La mejora es que el promedio aleatorio anterior es la suma de los valores máximos de cada cubo y luego se promedia. Aquí usamos el llamado promedio armónico para calcular La fórmula es: E_bucket =
m / (2 ^ -p_1_max + 2 ^ -p_2_max +... + 2 ^ -p_m_max)
y luego multiplicar el resultado del cálculo anterior por el parámetro a(m) y el número de cubos m, pero aquí el parámetro a(m) es diferente al anterior, tomará diferentes valores de acuerdo al número de baldes, la situación básica es la siguiente:
a(16)=0.673, a(32)=0.697, a (64) = 0,709, a(m) = 0,7213/(1+1,079/m ) m >= 128, es decir
, si el número de cubos no supera los 16, entonces a(m) toma el valor de 0,673, y si es mayor a 16 pero menor a 32, toma un valor de 0.697 y así sucesivamente.Veamos la implementación del código:


bucket_map = {
    
    }


def get_alpha(m):
    if m <= 16:
        return 0.673
    elif 16 < m <= 32:
        return 0.697
    elif 32 < m <= 64:
        return 0.709
    else:
        return 0.7213 / (1 + 1.079 / m)

b = 11 #不同取值对结果影响较大,原因在于我们的实验数据没能达到"海量"标准
def hyper_log_log(array):
    bucket_count = 0
    for a in array:
        hash_str = hashlib.sha256(str(a).encode()).hexdigest()
        bin_str = convert_hash_to_binary(hash_str)
        p = num_trailing_zeros(bin_str)

        # 将哈希结果转换为二进制,取最左边b个比特值计算当前元素哈希值所在的桶
        bucket = first_bits(hash_str, b)
        if bucket in bucket_map:
            # 记录每个桶元素从右边算起0做多的个数
            if p > bucket_map[bucket]:
                bucket_map[bucket] = p
        else:
            bucket_map[bucket] = p
            bucket_count += 1

    compute_sum = 0
    # 计算调和平均数
    for key in bucket_map:
        compute_sum += (2 ** (-1 * bucket_map[key]))

    harmonic_avg = bucket_count / compute_sum
    result = get_alpha(bucket_count) * bucket_count * harmonic_avg
    '''
    最后还需要根据结果做一些调整,这些调整主要基于比较复杂的数理统计推导,我们暂时忽略
    '''
    if result < 5 * bucket_count / 2:
        print(f"small correction")
    if result > 2 ** 32 / 30:
        result = -2 ** 32 * math.log(1 - result / 2 ** 32)

    return result


print(f"result of hyperloglog {
      
      hyper_log_log(array)}")

El resultado después de ejecutar el código anterior es el siguiente:

different elements cont: 9999
alpha : 0.7182725932495458
result of hyperloglog 9945.058986524531

Del experimento de código, descubrí que los diferentes valores de b tienen un mayor impacto en los resultados. Personalmente, creo que la razón es que la cantidad de datos utilizados en el experimento de código no puede cumplir con los requisitos "masivos". Después de todo , la memoria y el poder de cómputo de las computadoras personales son muy limitados. Para obtener más contenido, busque la codificación de Disney en la estación b

Supongo que te gusta

Origin blog.csdn.net/tyler_download/article/details/128652032
Recomendado
Clasificación