TP à rendre : Calcul du PageRank par la méthode de la

Transcription

TP à rendre : Calcul du PageRank par la méthode de la
TP à rendre : Calcul du PageRank par la méthode de la puissance
Charles Bouillaguet
23 septembre 2014
1
Introduction
Objectif Le but de ce TP est d’appliquer l’algorithme de calcul du PageRank aux 5 millions et quelques
de pages de Wikipédia (en anglais). Ceci nous permettra de trouver quelles sont les pages les plus « importantes ».
Une implémentation séquentielle, en C, vous est fournie. Elle est abondamment décrite dans ce document.
Votre travail consistera à en produire une version parallèle.
Le « PageRank » Pour identifier quelles pages sont vraiment importantes sur le web, les fondateurs de
Google ont utilisé un modèle simple. On considère le web comme un graphe orienté, dont les pages web sont
les noeuds, et où la page u a une arête vers la page v si elle contient un hyperlien vers v.
On modélise les internautes comme des processus aléatoires : quand ils sont sur une page u, ils cliquent
au hasard sur un des liens qu’elle contient. Plus précisément, si la page u contient k liens vers les pages
v1 , v2 , . . . , vk , alors l’internaute qui arrive sur u passe sur vi avec probabilité 1/k.
De la sorte, ils se « balladent » sur le graphe en effectuant une marche aléatoire. Comme le processus est sans
mémoire, on peut le représenter par une chaine de Markov discrète en temps discret. Le PageRank d’une page
donnée est simplement la probabilité de se trouver sur la page en question dans la distribution stationnaire
de la chaine de Markov.
On dispose pour ce TP de la matrice d’adjacence du graphe des hyperliens de Wikipédia en anglais. C’est une
matrice carrée G telle que G[i, j] vaut 1 si la page i pointe vers la page j, et zéro sinon. On note également
k(i) le nombre de liens qui partent de la page i :
k(i) =
n−1
X
G[i, `]
`=0
La matrice G n’est pas exactement la matrice de transition de la chaîne de Markov. On définit donc la
matrice G0 de la façon suivante :
G[i, j]
G0 [i, j] =
k(i)
Et cette fois, la somme des entrées d’une ligne de G0 vaut toujours 1 (ou zéro si la page i n’a aucun lien).
On représente une distribution de probabilité sur l’ensemble des n pages de Wikipédia par un vecteur-ligne
de taille n. Nécessairement, ses entrées sont des réels compris entre 0 et 1, et leur somme vaut 1. Si x est
une telle distribution de probabilité, alors la distribution obtenue en effectuant un pas de la marche aléatoire
est :
x0 = x × G0
Par conséquent, le vecteur de PageRank est la distribution stationnaire qui vérifie x = x × G0 .
1
Amélioration du modèle Cependant, on rajoute un petit ingrédient au modèle : de temp en temps, les
internautes, sous l’influence de la partie non-virtuelle de leur existence, attérissent sur n’importe quelle page
de manière complètement uniforme. Par exemple, alors qu’ils lisaient les pages sur les fonctions MPI, ils
décident de sortir, et donc de regarder la météo (aucun rapport !).
Plus précisément, on introduit un paramètre s. Quand ils sont sur la page u, alors :
– Soit la page u ne pointe vers aucune autre page, et alors ils vont sur une nouvelle page choisie uniformément
au hasard parmi les n pages Wikipédia.
– Soit la page u pointe vers d’autres pages, et alors :
– Avec probabilité 1 − s ils vont sur n’importe quelle autre page du web (uniformément au hasard).
– Avec probabilité s ils vont sur une des pages qui sont accessibles par un hyperlien depuis u (uniformément
au hasard).
Les concepteurs de Google, dans leur premier article scientifique, expliquent que s = 0.85 est un bon choix.
Avec ces nouvelles règles, calculer la matrice de transition de la chaine de Markov est un tout petit peu plus
compliqué, mais pas beaucoup.
Méthode de la puissance Pour calculer le PageRank de tout le web d’un seul coup, il suffit donc de calculer la distribution stationnaire d’une chaine de Markov, c’est-à-dire de calculer le vecteur propre dominant
de sa matrice de transition.
Pour ce faire, on part de la distribution uniforme (x0 [i] = 1/n), et on calcule des version successives de la
distribution. Si on note M la matrice de transition de la chaine de Markov, alors
xi+1 = xi × M
À chaque étape, on calcule error = kxi+1 − xi k. Lorsque error = 0, on a atteint la distribution stationnaire.
En pratique, on peut se contenter que error soit très faible (par exemple plus petit que 10−6 ).
2
Présentation du code séquentiel
Ce code a été écrit en « litterate programming ». La documentation est le code. Cela signifie donc que
et le code C et cette documentation sont extraites du même fichier. C’est le programme nuweb (http:
//nuweb.sourceforge.net/) qui se charge de produire le code et la documentation.
Pour améliorer la lisibilité le programme est découpé en « morceaux » avec des renvois indiqués entre h
grands chevrons i avec des numéros. Un petit exemple sera plus parlant qu’un grand discours. Voici donc la
structure générale du programme.
"sequential.c" 2≡
h Fichiers d’en-tête à inclure 3a i
h Définitions 3b i
h Fonction main 3c i
Pour ce qui est des fichiers d’en-tête, on a besoin de stdio.h pour printf, de stdlib.h pour malloc, de
math.h pour sqrt, de stdint.h pour avoir le type uint32_t et enfin de mpi.h. On écrit donc :
2
h Fichiers d’en-tête à inclure 3a i ≡
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <math.h>
#include <mpi.h>
Fragment referenced in 2.
La seule définition dont on ait besoin est celle de la valeur du paramètre s (plus il est proche de 1, plus le
calcul est difficile).
h Définitions 3b i ≡
#define PAGERANK_S
0.85
Fragment referenced in 2.
Et on peut passer sans plus attendre au corps du code :
h Fonction main 3c i ≡
int main(int argc, char **argv) {
h Variables locales de main 4a, . . . i
h Lecture du graphe de liens 4b i
h Allocation et initialisation des autres données 5c i
h Boucle de calcul du PageRank 5a i
h Affichage des résultats 6d i
h Libération de la mémoire 5d i
return 0;
}
Fragment referenced in 2.
3
Gestion du graphe de liens de wikipedia
Avec ce TP sont fournis deux fichiers :
titles.txt contient les titres des pages Wikipédia. Il y a un titre par ligne. L’idée, c’est que le titre de la
page numéro i se trouve ligne i. On suppose que la première ligne porte le numéro zéro.
links.bin contient le graphe des liens entre les pages, sous un format optimisé décrit ci-dessous.
On ne pourrait pas stocker complètement la matrice d’adjacence du graphe des liens de Wikidédia sous forme
dense. En effet, une matrice carrée de taille 5 millions avec un seul bit par entrée nécessiterait presque 3
tera-octets d’espace.
Par contre, le graphe est assez creux : il y a beaucoup plus de « zéro » que de « un » dans sa matrice
d’adjacence. L’astuce consiste à ne stocker que la position des entrées non-nulles.
Format du fichier Le fichier qui décrit le graphe des liens se lit par paquets de 32 bits. Chaque paquet
de 32 bits décrit un entier de type uint32_t. Ce fichier se compose de trois parties :
– Un entier n qui donne le nombre de noeuds du graphe (donc le nombre de pages Wikipédia).
– Un tableau row_ptr de n + 1 entiers.
– Un tableau col_ind de row_ptr[n] entiers.
3
Ces données permettent d’identifier tous les successeurs d’un noeud donné. Les numéros des successeurs du
noeud i se trouvent dans le tableau col_ind, des indices row_ptr[i] (inclu) à row_ptr[i+1] (exclu).
Procédure de chargement du fichier Aussi, la procédure de chargement du graphe fonctionne comme
suit :
h Variables locales de main 4a i ≡
FILE *f;
uint32_t n, nnz;
uint32_t *row_ptr, *col_ind;
Fragment defined by 4a, 5b, 7a.
Fragment referenced in 3c.
La variable nnz contient le nombre total d’entrées dans col_ind. C’est en fait le Nombre de Non-Zero dans
la matrice du d’adjacence du graphe.
h Lecture du graphe de liens 4b i ≡
f = fopen("links.bin", "r");
if (f == NULL) {
perror("impossible d’ouvrir links.bin");
exit(1);
}
fread(&n, sizeof(uint32_t), 1, f);
row_ptr = (uint32_t *) malloc((n + 1) * sizeof(uint32_t));
if (row_ptr == NULL) {
perror("impossible d’allouer row_ptr");
exit(1);
}
fread(row_ptr, sizeof(uint32_t), n + 1, f);
nnz = row_ptr[n];
col_ind = (uint32_t *) malloc(nnz * sizeof(uint32_t));
if (col_ind == NULL) {
perror("impossible d’allouer col_ind");
exit(1);
}
fread(col_ind, sizeof(uint32_t), nnz, f);
fclose(f);
Fragment referenced in 3c.
4
Boucle principale
La boucle principale de la méthode de la puissance est très simple :
4
h Boucle de calcul du PageRank 5a i ≡
while (error > 1e-6) {
h Affichage d’un petit message 6a i
h Calcul de y = x · M 7b i
h Calcul de error = ky − xk 6b i
h Copie de y dans x 6c i
n_iterations += 1;
}
Fragment referenced in 3c.
Mais avant de pouvoir se lancer là-dedans, il nous faut déclarer des variables et allouer de la mémoire pour
stocker les vecteurs x et y. Le vecteur x est initialisé avec la distribution uniforme.
h Variables locales de main 5b i ≡
int i, j, k, n_iterations;
double *x, *y;
double start_time, error;
Fragment defined by 4a, 5b, 7a.
Fragment referenced in 3c.
h Allocation et initialisation des autres données 5c i ≡
x = malloc(n * sizeof(double));
y = malloc(n * sizeof(double));
if ((x == NULL) || (y == NULL)) {
perror("impossible d’allouer les vecteur");
exit(1);
}
for (i = 0; i < n; i++) {
x[i] = 1.0 / n;
}
start_time = MPI_Wtime();
error = INFINITY;
n_iterations = 0;
Fragment referenced in 3c.
Lorsque tout sera fini, il ne faudra pas oublier de libérer la mémoire allouée :
h Libération de la mémoire 5d i ≡
free(row_ptr);
free(col_ind);
free(x);
free(y);
Fragment referenced in 3c.
Pour que l’utilisateur puisse suivre la progression du calcul, on se rappelle à son bon souvenir au début de
chaque itération :
5
h Affichage d’un petit message 6a i ≡
printf("iteration %d, erreur actuelle %g\n", n_iterations, error);
Fragment referenced in 5a.
La partie qui calcule le produit matrice-vecteur est la plus compliquée, donc on la garde pour la fin. Le calcul
de l’erreur, lui est très facile :
h Calcul de error = ky − xk 6b i ≡
error = 0;
for (i = 0; i < n; i++) {
double delta = x[i] - y[i];
error += delta * delta;
}
error = sqrt(error);
Fragment referenced in 5a.
Une fois l’erreur calculée, il faut mettre les données en place pour l’itération d’après :
h Copie de y dans x 6c i ≡
for (i = 0; i < n; i++) {
x[i] = y[i];
}
Fragment referenced in 5a.
h Affichage des résultats 6d i ≡
printf("erreur finale après %d iterations: %g\n", n_iterations, error);
double time = MPI_Wtime() - start_time;
printf("MFlops : %.1f \n", 1.0 * (n + nnz) * n_iterations / 1048576 / time);
double max = -1;
int max_i;
for( i = 0; i < n; i++ ) {
if ( x[i] > max ) {
max = x[i];
max_i = i;
}
}
printf("meilleur PageRank atteint page numéro %d\n", max_i + 1);
Fragment referenced in 3c.
5
Produit matrice-vecteur implicite
Il nous reste la partie compliquée : le calcul du produit matrice-vecteur. Ce qui est ennuyeux, c’est qu’on ne
doit surtout pas former complètement la matrice de transition de la chaine de Markov en RAM (on n’a pas
assez de RAM, et ça prendrait trop longtemps). On ne dispose que de la matrice G d’adjacence du graphe,
et il faut qu’on gère la « téléportation » des internautes.
On décompose la matrice de transition de la chaine de Markov en trois composantes :
6
– une qui correspond au graphe des liens
– une composante qui correspond à la téléportation « systématique »
– une dernière qui correspond à la téléportation « forcée » depuis les pages sans liens.
M = s · G0 +
La matrice H est la matrice :
s
1−s
·H + ·T
n
n

1
1

H = .
 ..
1
1
..
.
1
1
..
.
...
...
..
.

1
1

.. 
.
1
1
1
...
1
La matrice T est définie de la façon suivante : sa i-ème ligne est remplie de zéros si la page i a des liens vers
d’autres pages, et remplie de 1 dans le cas contraire.
Maintenant, la feinte consiste à réaliser que x × H est un vecteur dont toutes les coordonnées sont égales :
elles contiennent la somme des entrées de x. Or comme x est une distribution de probabilités, la somme de
ses entrées vaut un.
La valeur de x × T est un peu plus compliquée à trouver. D’abord, comme toutes les colonnes de T sont
identiques, alors toutes les coordonnées de x × T sont identiques. Leur valeur commune est en fait la somme
des x[i] pour tous les indices i qui correspondent à des pages sans liens.
On a donc :


n−1

1 − s
s X
+
x[i]
x × M = x × (s · G0 ) + 
· 1
 n
n i=0
1
1
...
1
k(i)=0
En gros, il faut calculer le produit x × G0 , puis ajouter à chaque coordonnée une constante qu’on va appeler
global_term (c’est le gros machin entre parenthèses). On peut donc se lancer :
h Variables locales de main 7a i ≡
double global_term;
int k_i;
Fragment defined by 4a, 5b, 7a.
Fragment referenced in 3c.
h Calcul de y = x · M 7b i ≡
h Mise à zéro de y 8a i
h Initialisation de global_term avec la composante « systématique » 8c i
for (i = 0; i < n; i++) {
h Calcul de k(i) 8e i
if (k_i > 0) {
h Calcul de x × G0 8f i
} else {
h Mise à jour de global_term avec la composante « forcée » 8d i
}
}
h Ajout de global_term à toutes les composantes de y 8b i
Fragment referenced in 5a.
7
Commençons par les parties faciles. Au début, on met y à zéro :
h Mise à zéro de y 8a i ≡
for (i = 0; i < n; i++) {
y[i] = 0;
}
Fragment referenced in 7b.
Et à la fin, on ajoute global_term à toutes ses composantes :
h Ajout de global_term à toutes les composantes de y 8b i ≡
for (i = 0; i < n; i++) {
y[i] += global_term;
}
Fragment referenced in 7b.
La valeur de global_term est initialement (1 − s)/n :
h Initialisation de global_term avec la composante « systématique » 8c i ≡
global_term = (1 - PAGERANK_S) / n;
Fragment referenced in 7b.
Chaque fois qu’on découvre une page sans lien, on augmente global_term en fonction de x[i] :
h Mise à jour de global_term avec la composante « forcée » 8d i ≡
global_term += PAGERANK_S * x[i] / n;
Fragment referenced in 7b.
Déterminer le nombre de liens présents sur la page i est facile grâce à notre représentation du graphe. En
effet, ces liens se trouvent dans le tableau col_ind entre les indices row_ptr[i] et row_ptr[i+1].
h Calcul de k(i) 8e i ≡
k_i = row_ptr[i + 1] - row_ptr[i];
Fragment referenced in 7b.
Le dernier détail qui reste est le produit avec la matrice creuse G0 . On examine la i-ème ligne, pour chaque
entrée non-nulle, on repère sa colonne (dans j) puis on met à jour y[j] en conséquence.
h Calcul de x × G0 8f i ≡
for (k = row_ptr[i]; k < row_ptr[i + 1]; k++) {
j = col_ind[k];
y[j] += PAGERANK_S * x[i] / k_i;
}
Fragment referenced in 7b.
8