Como projetar uniformemente um hash para um número fixo de buckets

11

Olá colegas estatísticos,

Eu tenho uma fonte gerando hashes (por exemplo, computando uma string com um carimbo de data e hora e outras informações e hash com md5) e quero projetá-la em um número fixo de buckets (digamos 100).

hash de amostra: 0fb916f0b174c66fd35ef078d861a367

O que eu pensei inicialmente era usar apenas o primeiro caractere do hash para escolher um balde, mas isso leva a uma projeção descontroladamente uniforme (ou seja, algumas letras aparecem muito raramente e outras com muita freqüência)

Então, tentei converter essa string hexa em um número inteiro usando a soma dos valores de char e, em seguida, pegue o módulo para escolher um bucket:

import sys

for line in sys.stdin:
    i = 0
    for c in line:
        i += ord(c)
    print i%100

Parece funcionar na prática, mas não sei se há algum senso comum ou resultados teóricos que possam explicar por que e até que ponto isso é verdade?

[Edit] Após algumas considerações, cheguei à seguinte conclusão: Em teoria, você pode converter o hash em um número inteiro (muito grande) interpretando-o como um número: i = h [0] + 16 * h [1] + 16 * 16 * h [2] ... + 16 ^ 31 * h [31] (cada letra representa um número hexadecimal). Então você pode modular esse grande número para projetá-lo no espaço do balde. [/Editar]

Obrigado !

oDDsKooL
fonte
3
Um hash real não deve fornecer resultados não uniformes. Tem certeza de que o algoritmo de hash está implementado corretamente?
whuber
Duvido que exista um erro no próprio algoritmo de hash. Mas suspeito que os caracteres do resumo hexadecimal não sejam estritamente uniformes e distribuídos independentemente.
ODDsKooL # 13/12
1
É isso que acho duvidoso: um hash "criptograficamente seguro" como o MD5 deve ter distribuições uniformes de todos os dígitos, a menos que exista algo muito especial na distribuição da entrada ("especial" significa intimamente vinculado ao algoritmo MD5). Sua solução proposta equivale a re-hash do hash, o que não deve ser necessário.
whuber
1
O primeiro caractere do hash Md5 deve ser uniforme. Mas você obteria apenas 16 valores (é uma codificação hexadecimal)
leonbloy
1
Obrigado por insistir nesse ponto, repito minha contagem na primeira letra dos hashes e parece de fato ~ uniformemente distribuída: {'a': 789, 'c': 769, 'b': 755, 'e': 730, 'd': 804, 'f': 749, '1': 716, '0': 758, '3': 734, '2': 735, '5': 787, '4': 756, '7': 771, '6': 721, '9': 764, '8': 765}. Portanto, minha pergunta é mais ou menos respondida, pois eu só preciso projetar esse gerador aleatório de 16 estados em um espaço de 100 estados, o que pode ser feito usando as 2 primeiras letras do hash para gerar um número inteiro de intervalo [0,16+ 16 * 16] e modulo para 100. Importa-se de responder minha própria pergunta;)?
oDDsKooL

Respostas:

13

NB: colocando em forma a resposta que surgiu da discussão nos comentários para facilitar a leitura para as pessoas interessadas

(versão atualizada)

Suponha que tenhamos uma fonte gerando eventos independentes que queremos distribuir uniformemente em B baldes.

Os principais passos são:

  1. hash cada evento e para um inteiro i de tamanho 2N
  2. projeto para R×[0,1[ Como p=i2N
  3. encontrar balde correspondente bi de modo a biBp<bi+1B

Para 1. uma solução popular é usar o MurmurHash para gerar um número inteiro de 64 ou 128 bits.

Para 3. uma solução simples é iterar em j=1..B e verifique se p é em [bjB,bj+1B[

No pseudo-código (python), o procedimento geral pode ser:

def hash_to_bucket(e, B):
    i = murmurhash3.to_long128(str(e))
    p = i / float(2**128)
    for j in range(0, B):
        if j/float(B) <= p and (j+1)/float(B) > p:
            return j+1
    return B

(versão anterior, realmente não ideal)

A primeira observação é que a n- ésima letra do hash deve ser distribuída uniformemente em relação ao alfabeto (que tem aqui 16 letras - obrigado a @leonbloy por apontar isso).

Então, para projetá-lo para um intervalo de [0,100 [, o truque é pegar 2 letras do hash (por exemplo, 1ª e 2ª posições) e gerar um número inteiro com isso:

int_value = int(hash[0])+16*int(hash[1])

Esse valor vive no intervalo [0,16+ (16-1) * 16 [, ​​portanto, apenas precisamos modulá- lo para 100 para gerar um intervalo no intervalo [0, 100 [: Como apontado nos comentários, fazer portanto, impacte a uniformidade da distribuição, pois a primeira letra é mais influente que a segunda.

bucket = int_value % 100

Em teoria, você pode converter o hash inteiro em um número inteiro (muito grande) interpretando-o como um número: i = h [0] + 16 * h [1] + 16 * 16 * h [2] ... + 16 ^ 31 * h [31] (cada letra representa um número hexadecimal). Então você pode modular esse grande número para projetá-lo no espaço do balde. Pode-se notar, então, que o módulo i pode ser decomposto em uma operação distributiva e aditiva:

imodN=((h0modN)+(16modN×h1modN)+...+(1631modN×h31modN))modN
oDDsKooL
fonte
Quaisquer melhorias nesta resposta são bem-vindas.
ODDsKooL
Isso não parece uma boa solução, porque quando "duas letras" são "distribuídas uniformemente", os baldes de 0 através 55 normalmente obtém 50% mais ocorrências por depósito do que os depósitos de 56 através 99. Com efeito, você está usando uma terrível função de hash na tentativa de misturar o próprio hash em 100 buckets. Por que não usar apenas uma função hash válida conhecida para esse fim?
whuber
Concordo. Uma solução melhor rolada à mão seria pegar um pedaço da cadeia hexadecimal que poderia se traduzir em um número inteiro de espaço de 16 bits. Em seguida, divida o valor real pelo valor inteiro máximo de 16 bits, multiplique por cento e arredondado.
Spdrnl
Se você usar vários baldes na forma de 2n, você pode tirar apenas a última nbits do hash (e é equivalente em caracteres hexadecimais). Dessa forma, o resultado da operação do módulo será exatamente o mesmo que no cálculo da conversão completa em número inteiro. Também pode funcionar bem se você usar vários baldes que não são uma potência de2.
alesc
@whuber Concordo que isso não é o ideal e projetar para um contínuo [0,1 [intervalo é muito melhor. Eu verifiquei isso experimentalmente também. Vou editar a resposta para refletir essa visão.
ODDsKooL 19/05
0

Eu tive um problema semelhante e criei uma solução diferente, que pode ser mais rápida e facilmente implementada em qualquer idioma.

Meu primeiro pensamento foi despachar itens de maneira rápida e uniforme em um número fixo de baldes e, para ser escalável, eu deveria imitar a aleatoriedade.

Então, eu codifiquei essa pequena função retornando um número flutuante em [0, 1 [dada uma string (ou qualquer tipo de dado de fato).

Aqui em Python:

import math
def pseudo_random_checksum(s, precision=10000):
    x = sum([ord(c) * math.sin(i + 1) for i,c in enumerate(s)]) * precision
    return x - math.floor(x)

É claro que não é aleatório, na verdade nem sequer é pseudo-aleatório; os mesmos dados sempre retornam a mesma soma de verificação. Mas funciona como aleatório e é bem rápido.

Você pode despachar e recuperar itens facilmente em N buckets, atribuindo simplesmente cada item ao número do bucket math.floor (N * pseudo_random_checksum (item)).

fbparis
fonte
Você tem uma intuição ou prova de que as amostras serão colocadas uniformemente em [0,1]?
sud_
@sud_ Esta função é discutido aqui: stackoverflow.com/a/19303725/1608467
fbparis
@sud_ Além disso, eu executei alguns testes para compará-lo com um gerador de números aleatórios legítimos e foi bom em todos os casos que testei.
Fbparis