Algoritmo di Min Hashing - Dipartimento di Matematica e Informatica

Algoritmo di Min Hashing
Progetto del corso di
”Data Analysis and Management”
Prof. A. Ferro
Dario Fiore
Corso di Laurea Specialistica in Informatica presso
Dipartimento di Matematica e Informatica, Università di Catania
October 25, 2005
Abstract
Il progetto realizzato ha riguardato l’implementazione dell’algoritmo di Min Hashing utile
per il calcolo della similarità di insiemi di elementi rappresentati come colonne di una matrice.
In particolare è stata apportata una ottimizzazione sulla rappresentazione dei dati di input per
eliminare informazioni ridondanti, riducendo in tal modo il tempo di computazione, nonchè lo
spazio necessario per immagazzinare i dati stessi.
1
Contents
1 Introduzione
1.1 Il problema della ricerca della similarità
1.2 Rappresentazione dei dati . . . . . . . .
1.3 Similarità . . . . . . . . . . . . . . . . .
1.4 L’algoritmo di Min Hashing . . . . . . .
1.4.1 Trucchi di implementazione . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3
3
3
3
3
3
2 Il progetto
2.1 I dati . . . . . . . . . . . . . . .
2.1.1 DB optbuilder . . . . .
2.2 Implementazione dell’algoritmo
2.2.1 Calcolo delle similarità .
2.2.2 Codice sorgente . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5
5
5
6
6
10
Min Hashing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Min Hashing (Implementation trick) . . . . . . . . . . . . . . . . . . . . . . . .
Union-Intersection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
4
9
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
List of Algorithms
1
2
3
2
1
1.1
Introduzione
Il problema della ricerca della similarità
Conoscere la similarità tra insiemi di elementi può risultare utile in molte applicazioni. Infatti
dato s valore di similarità di due insiemi A e B e una soglia t, allora possiamo definire correlati A
e B se s > t. Questa correlazione, a seconda dell’applicazione, pùo aiutare nell’interpretazione
dei dati.
1.2
Rappresentazione dei dati
Per questo genere di problemi solitamente gli insiemi di elementi sono rappresentati con una
matrice in cui ogni colonna corrisponde a un elemento e ogni riga a un insieme di elementi.
Data una matrice A = |ai,j |, allora in una riga ai,j = 1 indica la presenza dell’elemento j
nell’insieme i, altrimenti ai,j = 0. Queste matrici hanno una caratterizzazione sparsa e sono
tali che le colonne sono in un numero tale da poter entrare nella memoria principale mentre le
righe sono talmente tante da non potervi entrare.
1.3
Similarità
Data tale rappresentazione dei dati possiamo definire il concetto di similarità.
Date due colonne CT1 e C2 , si definisce similarità:
|C1 S C2 |
Sim(C1 , C2 ) = |C
C2 |
1
1.4
L’algoritmo di Min Hashing
Come si può vedere dalla definizione di similarità, per il suo calcolo è necessario conoscere la cardinalità della loro intersezione e della loro unione. Poichè il numero di righe è spropositamente
elevato, trovare questi valori può essere molto costoso in termini di tempo computazionale.
L’idea dell’algoritmo è quella di riuscire a sintetizzare il contenuto delle colonne in una piccola
quantità di dati chiamata segnatura Sig(C) in modo che due colonne C1 e C2 sono simili se e
solo se sono simili le loro segnature Sig(C1 ), Sig(C2 ).
Di seguito lo pseudo-codice dell’algoritmo.
Algorithm 1 Min Hashing
p a parameter that indicates signature length
for i = 0 to p do
generate a rows’ random permutation
for all rows following permutation order do
for each column c do
hi (c) =index of first row with 1 in column c
end for
end for
end for
1.4.1
Trucchi di implementazione
Tuttavia per l’implementazione dell’algoritmo si effettuano alcune modifiche per migliorarne
l’efficienza. Infatti poichè generare una permutazione delle righe è computazionalmente costoso,
3
anzichè generare p permutazioni se ne genera una sola e si utilizzano invece p diverse funzioni
hash hk , k = 1 · · · p. Poi per ogni colonna si mantiene il minimo valore di hash hk in cui essa è
1.
Algorithm 2 Min Hashing (Implementation trick)
let p be a parameter that indicates signature length
hk : {1 · · · n} → {1 · · · O(n)}
let slot(Ci , hk ) be a matrix for min-hash values initialized to +∞
generate a rows’ random permutation
for each row Rj following permutation order do
for each column Ci do
if Rj has 1 in column Ci ((Rj , Ci ) = 1) then
for k = 1 · · · p do
if hk (j) < slot(hk , Ci ) then
slot(hk , Ci ) ← hk (j)
end if
end for
end if
end for
end for
4
2
Il progetto
Il progetto è stato volto alla implementazione di questo algoritmo avendo come specifiche:
• dati immagazzinati in un database Berkeley DB ;
• linguaggio di programmazione: C++.
2.1
I dati
Il primo passo è stato la costruzione del database partendo dai dati di test che si trovavano su
file di testo. Tali dati si trovavano nella seguente forma. Ogni riga conteneva gli indici degli
elementi contenuti.
Esempio
25 52 164 240 274 328 368 448 538 561 630 687 730 775 825 834
39 120 124 205 401 581 704 814 825 834
35 249 674 712 733 759 854 950
39 422 449 704 825 857 895 937 954 964
15 229 262 283 294 352 381 708 738 766 853 883 966 978
26 104 143 320 569 620 798
7 185 214 350 529 658 682 782 809 849 883 947 970 979
227 390
L’algoritmo nella sua versione originale prevederebbe i dati in forma matriciale con colonne
di 0 e 1 tuttavia, date le dimensioni, si è deciso di mantenere nel database e quindi anche
nell’algoritmo questa rappresentazione ”compatta”. Per giustificare tale scelta sono stati fatti
dei test. Per un file di origine di 3,8MBytes si otteneva un file Berkeley DB in forma matriciale
di 392,9MBytes. Invece il file Berkeley DB in forma compatta era di soli 5,9MBytes.
Nella costruzione del database è stato aggiunto un valore al termine di ogni riga pari a
numerodicolonne + 1. Questo valore si è reso necessario come delimitatore per scorrere la
riga nell’algoritmo, dato che le righe non hanno una lunghezza fissa.
Inoltre ad ogni riga è stata associata una chiave pari alla sua posizione (1 alla prima riga, 2
alla seconda e cosı̀ via). Tale scelta è stata fatta perchè in Berkeley DB le righe possono essere
estratte in base a una chiave o iterativamente. E dato che nell’algoritmo bisogna selezionare le
righe nell’ordine della permutazione (quindi in posizioni diverse) è stata necessaria la presenza
di un tale indice.
2.1.1
DB optbuilder
Per costruire il database a partire da un file di testo della forma descritta si è realizzato un
apposito programma da utilizzare a linea di comando con la seguente sintassi:
DB_optbuilder filename dbname colsnumber
• filename: nome del file con i dati
• dbname: nome del file Berkeley DB in cui trasferire i dati
• colsnumber: numero di colonne, ovvero di elementi
Il programma si compone del programma DB optbuilder che per eseguire il lavoro crea un
oggetto della classe DB optbuild che ha la seguente intestazione.
class DB_optbuild
{
private:
5
//nome del database
std::string dbname;
//nome del file dati sorgente
std::string filename;
//il numero di colonne, ovvero di items
int ncols;
//il DB
Db db;
public:
DB_optbuild(std::string dbfname, std::string fname, int cols);
void build();
void error(const char* p);
};
2.2
Implementazione dell’algoritmo
L’algoritmo implementato prende in input i seguenti argomenti:
• il nome del file Berkeley DB;
• la lunghezza della segnatura p;
• il nome per un file di output in cui salvare le segnature delle colonne;
• il nome per un file di output in cui salvare le similarità tra colonne;
• il numero di colonne.
Fondamentalmente si ispira a Algorithm 2 con la differenza che quando estrae una riga dal
database invece di cercare le colonne con 1, trova direttamente gli indici di queste colonne.
Il tutto è realizzato per mezzo della classe opt Min Hashing che per completezza è allegata di
seguito.
Per i dettagli strettamente implementativi è possibile fare riferimento ai commenti del codice.
2.2.1
Calcolo delle similarità
Il progetto oltre a trovare le segnature delle colonne tramite la tecnica del Min Hashing esegue
anche il calcolo delle similarità. Per fare questo serve calcolare le cardinalità degli insiemi di
unione ed intersezione per ogni coppia di colonne, dato che
Sim(Ci , Cj ) = intersecval(Ci , Cj )/unionval(Ci , Cj )
Si illustra l’algoritmo realizzato per trovare tali valori unionval, intersecval poichè dalla semplice lettura del codice potrebbe non essere subito comprensibile.
Unione L’idea di base parte dalla definizione di unione e dal fatto che la cardinalità dell’insieme
risultante è al più 2∗p (p dimensione delle colonne). Valore dal quale vanno sottratti i duplicati.
Pertanto si fa una scansione delle due colonne Ci e Cj utilizzando gli indici k = 1 · · · p, l = 1 · · · p.
Si osserva che
p
X
(k)
(k)
unionval =
uniontempi + uniontempj
k=1
ove
(k)
(uniontempi
(k)
+ uniontempj )
∈ {0, 1, 2}. Fissata k, facendo iterare l = 1 · · · p si possono
(k)
presentare 3 casi, inizializzando uniontempi
(k)
= uniontempj
6
=1
1. Se l < k e Ci (k) = Ci (l), ovvero c’è un duplicato nella stessa colonna Ci .
(k)
uniontempi = 0.
Allora
2. Se l 6 k e Ci (k) = Cj (l), ovvero c’è un duplicato tra la colonna Ci e la colonna Cj . Allora
(k)
uniontempi = 0.
3. Se l < k e Cj (k) = Cj (l), ovvero c’è un duplicato nella stessa colonna Cj . Allora
(k)
uniontempj = 0.
Intersezione Anche in questo caso si fa una scansione delle due colonne Ci , Cj alla ricerca
dei duplicati però stavolta stando attenti ai duplicati nella stessa colonna. Si osserva che
intersecval =
p
X
intertemp(k)
k=1
ove intertemp(k) ∈ {0, 1}, infatti 0 6 intersecval 6 p. Fissata k, facendo iterare l = 1 · · · p:
• se Ci (k) = Cj (l) intertemp = 1;
• se per l < k troviamo un duplicato in Ci (cioè Ci (k) = Ci (l)) memorizziamo questa
informazione per non contare più i duplicati per tutte le iterazioni di l con tale k fissata.
Mostriamo in Algorithm 3 lo pseudocodice del risultante algoritmo per il calcolo delle similarità.
7
Algorithm 3 Union-Intersection
let n be number of columns
for i = 1 · · · n do
for j = 1 · · · n, j > i do
{scartiamo le ripetizioni}
union = 0
intersec = 0
for k = 1 · · · p do
uniontempi = uniontempj = 1
intertemp = 0
remdup = 1 {remember duplicates}
for l = 1 · · · p do
{case 1}
if l < k and Ci (k) = Ci (l) then
uniontempi = 0
end if
{case 2}
if l 6 k and Ci (k) = Cj (l) then
uniontempi = 0
end if
{case 3}
if l < k and Cj (k) = Cj (l) then
uniontempj = 0
end if
{intersection}
if Ci (k) = Cj (l) then
intertemp = 1 ∗ remdup
end if
if l < k and Ci (k) = Ci (l) then
intertemp = 0
remdup = 0
end if
end for
union = union + uniontempi + uniontempj
intersec = intersec + intertemp
end for
Sim(Ci , Cj ) = intersec/union
end for
end for
8
2.2.2
/*
*
*
*
*
*
*
*/
Codice sorgente
opt_Min_Hashing.cpp
MinHashing
Created by dario on 26/08/05.
Copyright 2005 __MyCompanyName__. All rights reserved.
#include "db_cxx.h"
#include "stdlib.h"
#include<fstream.h>
/*
* Classe opt_Min_Hashing
* Contiene l’implementazione dell’algoritmo e altre funzioni di
* utilit.
* L’ottimizzazione consiste nel lavorare con dati sotto forma di carrelli
* contenenti gli indici degli items.
*/
class opt_Min_Hashing
{
private:
//nome del file del DB
std::string dbname;
//nome del file di output con le segnature delle colonne
std::string sigfile;
//nome del file di output con la matrice delle similarit
std::string simfile;
int n; //numero di righe
int ncols; //numero di colonne
//parametro dell’algoritmo: la lunghezza della segnatura
int p;
//matrice con le segnature per colonna
int* res;
//matrice con i valori di similarit tra colonne
double *sims;
//il DB
Db db;
public:
//costruttore
opt_Min_Hashing(std::string database, int siglen, std::string sigfname, std::string simfname, int cols);
//l’algoritmo
int esegui();
9
void save_output();
int get_records_number();
void simscalc();
void rnd_permutation(int* perm);
int hash(int k, int r);
};
/*
* Costruttore
* Si occupa di inizializzare il DB, i parametri dell’algoritmo
* e le matrici di output.
*/
opt_Min_Hashing::opt_Min_Hashing(std::string database, int siglen, std::string sigfname, std::string
simfname, int cols) :
db(NULL, 0)
{ dbname = database;
sigfile = sigfname;
simfile = simfname;
p = siglen;
db.open(NULL, dbname.c_str(), NULL, DB_BTREE, DB_CREATE, 0);
ncols = cols;
//std::cout<<"ncols="<<ncols<<"\n";
//leggiamo il numero di righe
n = get_records_number();
//std::cout<<"n="<<n<<"\n";
/* Inizializza la matrice risultato
* Tale matrice dovrebbe essere inizializzata a +inf
* Per convenzione poniamo i suoi elementi pari a n+1
* che un sup del nostro insieme di valori.
*/
res = new int[p*ncols];
for(int i=0; i<p; i++)
for(int j=0; j<ncols; j++)
res[i*ncols+j]=n+1;
//inizializza la matrice delle similarit
sims = new double[ncols*ncols];
}
/*
* Implementazione dell’algoritmo di MinHashing
*/
int opt_Min_Hashing::esegui()
{
10
//generazione di una permutazione random delle righe
//vettore della permutazione
int* perm = new int[n];
//generiamo la permutazione
rnd_permutation(perm);
//leggiamo le righe nell’ordine della permutazione
for(int i=0; i<n; i++)
{ int r = perm[i];
//preleviamo la riga dal DB
Dbt key(&r, sizeof(int));
Dbt data;
memset(&data, 0, sizeof(Dbt));
//if(i%10000==0)
// std::cout<<r<<"i"<<i<<"\n";
int ret = db.get(NULL, &key, &data, 0);
int* columns = (int*)data.get_data();
//scorriamo la riga leggendo gli indici
for(int j=0; columns[j]!=ncols; j++)
{ int c = columns[j];
for(int k=0; k<p; k++)
{ //calcoliamo il valore hash
int hval = hash(k, r);
//se minore aggiorniamo la tabella delle segnature
if(hval<res[k*ncols+c])
res[k*ncols+c]=hval;
}
}
}
//calcoliamo le similarit
simscalc();
//salviamo sui file segnature e similarit
save_output();
//chiude la connessione al DB
db.close(0);
//ritorna l’esito
return 0;
}
void opt_Min_Hashing::save_output()
{ /////////salva le signature///////////
ofstream out(sigfile.c_str());
for(int i=0; i<p; i++)
{ for(int j=0; j<ncols; j++)
out<<res[i*ncols+j]<<"\t";
11
out<<"\n";
}
out<<"\n";
out.close();
///////////////////////////////////////
/////////salva le similarit///////////
ofstream outs(simfile.c_str());
for(int i=0; i<ncols; i++)
{ for(int j=0; j<ncols; j++)
outs<<sims[i*ncols+j]<<"\t";
outs<<"\n";
}
outs<<"\n";
outs.close();
///////////////////////////////////////
}
/////////CALCOLO DELLE SIMILARITA’///////////
void opt_Min_Hashing::simscalc()
{
for(int i=0; i<ncols; i++)
{ for(int j=0; j<ncols; j++)
{ //scartiamo le ripetizioni
if(j>i)
{ //calcoliamo le cardinalit di unione ed intersezione
//per ogni coppia di colonne (i,j)
int unione = 0;
int intersezione = 0;
//k ed l indici per scorrere le righe
for(int k=0; k<p; k++)
{ int unionetempi=1;
int unionetempj=1;
int intertemp = 0;
int intertempdup = 1;
//fissata la riga k, facciamo variare l=0...p-1
for(int l=0; l<p; l++)
{
//duplicato nella stessa colonna Ci
if(l<k && res[k*ncols+i]==res[l*ncols+i])
unionetempi = 0;
//duplicato nell’altra colonna Cj
if(l<=k && res[k*ncols+i]==res[l*ncols+j])
unionetempi = 0;
//duplicato nella stessa colonna Cj
if(l<k && res[k*ncols+j]==res[l*ncols+j])
unionetempj = 0;
//elemento in comune tra le 2 colonne
if(res[k*ncols+i]==res[l*ncols+j])
{ intertemp = 1*intertempdup;
12
}
//duplicato nella colonna Ci
//d’ora in poi non contiamo pi i duplicati nell’altra colonna
if(l<k && res[k*ncols+i]==res[l*ncols+i])
{ intertemp = 0;
intertempdup = 0;
}
}
//calcoliamo i valori finali per (Ci,Cj)
unione = unione+unionetempi+unionetempj;
intersezione = intersezione+intertemp;
}
//memorizziamo
//Sim(Ci,Cj)=|Ci A Cj|/|Ci U Cj|
sims[i*ncols+j] = (double)intersezione/unione;
//std::cout<<"Sim(C"<<i<<", C"<<j<<")="<<s<<"\n";
}
}
}
}
/*
* generazione di una permutazione random delle righe
*/
void opt_Min_Hashing::rnd_permutation(int* perm)
{ //settiamo il seed
srand((unsigned int)time((time_t*) NULL));
//vettore temporaneo per ricordare i numeri generati
bool* righe = new bool[n];
for(int i=0; i<n; )
{ int r = (rand()%n)+1;
if(righe[r-1]==false)
{ righe[r-1]=true;
perm[i]=r;
i++;
}
}
//possiamo cancellare il vettore di utilit
delete[] righe;
}
/*
* Funzione hash
* k indice della funzione hash
* r indice della riga
*/
int opt_Min_Hashing::hash(int k, int r)
{ return ((k+1)*r+k)%n;
}
13
/*
* Ricava il numero di record nel database usando
* un apposito metodo di BDB.
*/
int opt_Min_Hashing::get_records_number()
{ //istanziamo l’oggetto che raccoglie le statistiche
void* statp = 0;
DB_BTREE_STAT* bstp;
db.stat(NULL, &statp, 0);
bstp = (DB_BTREE_STAT*)statp;
int nrecords = bstp->bt_ndata;
return nrecords;
}
14