Bagaimana memproyeksikan secara seragam hash ke sejumlah bucket yang tetap

11

Hai Rekan Statistik,

Saya memiliki hash penghasil sumber (mis., Menghitung string dengan stempel waktu dan informasi lainnya dan hashing dengan md5) dan saya ingin memproyeksikannya ke sejumlah bucket (katakan 100).

contoh hash: 0fb916f0b174c66fd35ef078d861a367

Apa yang saya pikirkan pada awalnya adalah menggunakan hanya karakter pertama dari hash untuk memilih ember, tetapi ini mengarah pada proyeksi liar yang tidak seragam (yaitu beberapa huruf terlihat sangat jarang dan lainnya sangat sering)

Kemudian, saya mencoba mengubah string hexa ini menjadi integer menggunakan jumlah nilai char, kemudian mengambil modulo untuk memilih sebuah bucket:

import sys

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

Tampaknya berhasil dalam praktiknya, tetapi saya tidak tahu apakah ada akal sehat atau hasil teoretis yang dapat menjelaskan mengapa dan sejauh mana ini benar?

[Sunting] Setelah beberapa pemikiran saya sampai pada kesimpulan berikut: Secara teori Anda dapat mengubah hash menjadi integer (sangat besar) dengan menafsirkannya sebagai angka: i = h [0] + 16 * h [1] + 16 * 16 * h [2] ... + 16 ^ 31 * h [31] (setiap huruf mewakili angka heksadesimal). Kemudian Anda bisa memodulasi angka besar ini untuk memproyeksikannya ke ruang bucket. [/ Edit]

Terima kasih!

oDDsKooL
sumber
3
Hash yang asli seharusnya tidak memberikan hasil yang tidak seragam. Apakah Anda yakin algoritma hash diimplementasikan dengan benar?
whuber
Saya ragu ada bug dalam algoritma hashing itu sendiri. Tapi saya curiga karakter hex digest tidak seragam dan didistribusikan secara independen.
oDDsKooL
1
Itulah yang saya temukan ragu: hash "aman secara kriptografis" seperti MD5 harus memiliki distribusi seragam dari semua digit, kecuali ada sesuatu yang sangat istimewa tentang distribusi input ("khusus" berarti terkait erat dengan algoritma MD5). Solusi yang Anda usulkan berjumlah untuk hashing ulang hash, yang seharusnya tidak perlu sama sekali.
whuber
1
Karakter pertama dari hash Md5 harus seragam. Tetapi Anda hanya akan mendapatkan 16 nilai (ini merupakan pengkodean heksadesimal)
leonbloy
1
Terima kasih telah bersikeras pada titik itu, saya menjalankan kembali penghitungan saya pada huruf pertama hash dan tampaknya memang ~ didistribusikan secara seragam: {'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}. Oleh karena itu pertanyaan saya kurang lebih dijawab karena saya hanya perlu memproyeksikan generator acak 16-state ke ruang 100-state, yang dapat dilakukan dengan menggunakan 2 huruf pertama dari hash untuk menghasilkan bilangan bulat kisaran [0,16+ 16 * 16] dan modo ke 100. Keberatan jika saya menjawab pertanyaan saya sendiri;)?
oDDsKooL

Jawaban:

13

NB: memberikan jawaban yang muncul dari diskusi dalam komentar sehingga lebih mudah dibaca untuk orang yang tertarik

(versi terbaru)

Misalkan kita memiliki sumber yang menghasilkan acara independen yang ingin kita distribusikan secara seragam ke dalam emberB

Langkah kuncinya adalah:

  1. hash setiap peristiwa ke integer ukuranei2N
  2. memproyeksikan ke asR×[0,1[p=i2N
  3. temukan ember yang cocok sehinggabibiBp<bi+1B

Untuk 1. solusi yang populer adalah menggunakan MurmurHash untuk menghasilkan integer 64 atau 128 bit.

Untuk 3. solusi sederhana adalah dengan beralih pada dan periksa bahwa ada dij=1..Bp[bjB,bj+1B[

Dalam (python) pseudo-code prosedur keseluruhan bisa menjadi:

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

(versi sebelumnya, benar-benar tidak optimal)

Pengamatan pertama adalah bahwa huruf ke- n dari hash harus didistribusikan secara seragam sehubungan dengan alfabet (yang panjangnya di sini 16 huruf - terima kasih kepada @leonbloy karena menunjukkannya).

Kemudian, untuk memproyeksikannya ke rentang [0,100 [, triknya adalah mengambil 2 huruf dari hash (mis. Posisi 1 dan 2) dan menghasilkan bilangan bulat dengan itu:

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

Nilai ini hidup dalam kisaran [0,16+ (16-1) * 16 [, ​​maka kita hanya perlu memodulnya hingga 100 untuk menghasilkan ember di kisaran [0, 100 [kisaran: Seperti yang ditunjukkan dalam komentar, melakukan jadi dampak keseragaman distribusi karena huruf pertama lebih berpengaruh daripada yang kedua.

bucket = int_value % 100

Secara teori Anda dapat mengubah seluruh hash menjadi bilangan bulat (sangat besar) dengan menafsirkannya sebagai angka: i = h [0] + 16 * h [1] + 16 * 16 * h [2] ... + 16 ^ 31 * h [31] (setiap huruf mewakili angka heksadesimal). Kemudian Anda bisa memodulasi angka besar ini untuk memproyeksikannya ke ruang bucket. Seseorang kemudian dapat mencatat bahwa mengambil modulo dari i dapat diuraikan menjadi operasi distributif dan aditif:

imodN=((h0modN)+(16modN×h1modN)+...+(1631modN×h31modN))modN
oDDsKooL
sumber
Setiap perbaikan untuk jawaban ini dipersilakan.
oDDsKooL
Ini tidak terlihat sebagai solusi yang baik karena ketika "dua huruf" didistribusikan "secara seragam," ember dari hingga biasanya akan mendapatkan 50% lebih banyak hit per ember daripada ember dari hingga . Akibatnya, Anda menggunakan fungsi hash yang mengerikan dalam upaya untuk hash hash itu sendiri menjadi 100 ember. Mengapa tidak menggunakan fungsi hash baik yang diketahui untuk tujuan itu? 0555699
whuber
Saya setuju. Solusi linting tangan yang lebih baik adalah dengan mengambil sepotong string hex yang dapat diterjemahkan menjadi bilangan bulat dari ruang 16 bit. Kemudian bagi nilai aktual dengan nilai integer 16 bit maksimal, kalikan dengan seratus dan bulat.
spdrnl
Jika Anda menggunakan sejumlah kotak dalam bentuk , Anda hanya dapat mengambil bit terakhir dari hash (dan itu setara dalam karakter hex). Dengan cara ini hasil operasi modulo akan persis sama seperti ketika menghitungnya pada konversi penuh ke integer. Ini juga bisa berfungsi jika Anda menggunakan sejumlah ember yang bukan kekuatan . 2nn2
alesc
@whuber Saya setuju ini tidak cukup optimal dan memproyeksikan ke interval [0,1 [terus menerus jauh lebih baik. Saya sudah memverifikasi itu secara eksperimental juga. Saya akan mengedit jawaban untuk mencerminkan pandangan itu.
oDDsKooL
0

Saya memiliki masalah yang sama dan muncul dengan solusi berbeda yang mungkin lebih cepat dan lebih mudah diimplementasikan dalam bahasa apa pun.

Pikiran pertama saya adalah untuk mengirimkan barang dengan cepat dan seragam dalam jumlah yang tetap, dan agar dapat diukur, saya harus meniru keacakan.

Jadi saya mengkodekan fungsi kecil ini mengembalikan angka float di [0, 1 [diberikan string (atau segala jenis data sebenarnya).

Di sini, di 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)

Tentu saja itu tidak acak, bahkan itu bukan pseudo acak, data yang sama akan selalu mengembalikan checksum yang sama. Tapi itu bertindak seperti acak dan cukup cepat.

Anda dapat dengan mudah mengirim dan kemudian mengambil item dalam ember N dengan hanya menetapkan setiap item ke nomor bucket math.floor (N * pseudo_random_checksum (item)).

fbparis
sumber
Apakah Anda memiliki intuisi atau bukti bahwa sampel akan ditempatkan secara seragam di [0,1]?
sud_
@sud_ Fungsi ini dibahas di sini: stackoverflow.com/a/19303725/1608467
fbparis
@sud_ Juga, saya telah menjalankan beberapa tes untuk membandingkannya dengan generator nomor acak yang sah dan itu OK dalam setiap kasus yang saya uji.
fbparis