|Home|
<<Prev<<
<-Back--
>>Next>>
Data mining : streams et similarités
1. Combiner des "streams" de données à partir de plusieurs collections
1.1. Intro
En général dans une collection de documents, on
indexe les documents selon leur contenu -- donc on note pour chaque document
lesquels sont les mots qui y apparaissent (et combien de fois). Ensuite, lors
du traitement d'une requête (qui peut contenir plusieurs mots) on
cherche les documents les contenant (pour les sélectionner plus
finement, en calculant certaines quantités, etc.). Par exemple :
- la requête contient les mots A, B et C
- la collection contient des documents ainsi: (on
désigne par M... des mots autres que A, B et C)
D1 : A,M...
D2 : B,M...
D3 : C,M...
D4 : A,B,M...
D5 : B,C,M...
D6 : A,B,C....
D7 : M....
D8 : M....
autrement dit, il y a de documents qui ont tous les mots de la requête,
ou bien seulement une partie, ou bien aucun mot.
Les questions qu'on se pose sont comment trouver les documents ayant au moins
un mot de la requête dedans, et comment faire ceci en un temps
raisonnable.
1.2 Algo naif
Un algorithme simple et naif ferait deux boucles, imbriquées :
- pour chaque mot r de la requête (donc ici, pour A, puis B, puis C)
- pour chaque document d de la collection
- vérifier si d contient r,
- si oui, faire les
calculs, sélectionner d, etc.
- fin pour
- fin pour
De combien de pas a besoin cet algorithme? Autrement dit, combien de fois on
fait la vérification "si d contient r" ? Ce sera le nombre
de mots de la requête multiplié par le nombre total de
documents. Donc avec l'exemple ci-dessus, 8*3=24. Par contre, si au lieu de 8
nous avons 80000000 documents, une telle exécution risque de prendre
beaucoup de temps, même avec les ordinateurs puissants d'aujourd'hui.
1.2 Algo plus efficace
1.2.1 Idée
Une idée pour améliorer cet algo serait de pouvoir
éliminer les documents qui n'ont aucun mot en commun avec la
requête, donc dans notre exemple, D7 et D8. Autrement dit,
lorsqu'on commence à examiner les documents "candidats", qu'on ne
"touche pas du tout" à D7 et D8, c'est-à-dire
qu'au moment où on reçoit la requête à traiter,
qu'on "sache" que "ce n'est pas la peine" d'examiner D7 et D8, et
qu'on sache ceci juste en regardant la requête.
1.2.2 Indices
Vouloir aboutir à ceci avant même d'examiner les
documents un par un semble impossible. Mais souvenons-nous qu'il y a un moment
où chaque document "nous passe par la main" forcément : le moment
d'insertion dans la collection. Donc ce qu'on peut faire c'est
(à ce moment-là, ou bien au moment de ce qu'on appelle la
clôture de la session d'insertion, donc quand on n'écrit plus
(pour un bout de temps) dedans), préparer des listes d'indices.
1.2.3 Outil
Donc la collection est construite "intelligemment", et nous offre la
possibilité d'énumérer un par un tous les documents
contenant un mot donné et seulement ceux-ci, et dans l'ordre.
Appelons TD(r).nextDoc() la manière d'obtenir,
un par un (lors de chaque appel, et rapidement), tous les documents contenant le
mot r dans l'ordre (croissant de leur docId par
exemple), et seulement ces documents.
1.2.4 Cas particulier pour un seul mot dans la requête
Considérons, de plus, pour l'instant que la requête n'a qu'un
seul mot r. Réécrivons notre algorithme avec cet outil.
- tant que (d = TD(r).nextDoc()) est un document valide
- vérifier si d contient r,
- si oui, faire les
calculs, sélectionner d, etc.
- fin tant que
Maintenant on voit qu'on ne touche plus qu'au documents nécessaires.
1.2.5 Idée du cas général (plusieurs mots dans la requête)
Comment "élargir" le balayage pour "toucher" tous les documents qui
contiennent au moins un mot de la requête ? Souvenons-nous que les
appels successifs de TD(r).nextDoc() donnent les ids des documents dans
l'ordre croissant. Considérons une requête avec les
mots A, B, C
comme plus haut, et regardons l'ensemble des résultats du premier
appel pour chacun (car ils sont indépendants)
E[0] = {TD(A).nextDoc(), TD(B).nextDoc(), TD(C).nextDoc()}
Cet ensemble peut contenir un ou plusieurs documents, au hasard de leur contenu
et de leur ordonnancement. Peu importe. Considérons le document avec le
plus petit id dans E[0] -- notons-le Dmin[0].
Si on décide de traiter
tout d'abord Dmin[0], alors on peut "le mettre de
côté" et ne plus y revenir. Autrement dit, non
seulement Dmin[0] est le minimum de E[0], mais il est
également le minimum de tous les documents ayant au moins un mot de la
requête et qui ne sont pas encore traités. Donc une
fois Dmin[0] traité, on sait que tout autre document
candidat légitime a un id plus grand. Souvenons-nous qu'à chaque
appel, TD().nextDoc() nous donne des id de plus en plus grands (pas
forcément consécutifs -- mais ceci n'a pas d'importance). Donc,
une fois Dmin[0] traité, on appelle le (ou les)
TD().nextDoc() seulement pour ceux qui étaient égaux
à Dmin[0]. Comme cela on est sûr qu'on ne
rate aucun document candidat légitime, et on ne passe pas plus de temps
que nécessaire.
1.2.6 Algorithme du cas général (plusieurs mots dans la requête)
On donne ici l'algo général, qui utilise plusieurs "fonctions",
qu'on détaille également
- initialiser un vecteur de "streams" E=[TD(ri).nextDoc()]
pour chaque mot ri de la requête
- tant que qEnd(E) n'est pas vrai
- soit d = trouverMinIdDocDans(E)
- vérifier si d contient r,
- si oui, faire les
calculs, sélectionner d, etc.
- nextDocs(E)
- fin tant que
- fonction nextDocs(E)
- m = trouverMinIdDocDans(E)
- pour chaque stream-élément i de E
- s'il est égal à m, appeler TD(ri).nextDoc()
- fin fonction
La fonction qEnd(E) utilise une constante spéciale "fin-de-stream" qui
est rendue par un appel TD().nextDoc() quand il a fini tous les documents qu'il
aurait pu rendre. Si tous les streams sont épuisés, alors qEnd(E)
rend vrai, autrement non.
1.3 Lucene
Lucene contient du code qui crée les collections indexées de
documents, ainsi que du code qui nous permet de rechercher dedans, comme on l'a
déjà vu. Ce code a une certaine structure bien définie --
les différentes étapes du travail sont réparties entre
plusieurs classes spécifiques.
L'algorithme de balayage qui a été discuté plus haut est
exécuté principalement par des classes dérivées
de la classe Scorer. Une fois une instance construite, le "point
d'entrée" en est la méthode d'instance
Scorer.score(Collector c), et c'est donc cette méthode qui
exécute les étapes principales de notre algorithme.
Le Collector est une classe "seau", qui recueille (et trie, si
on prend la bonne classe dérivée de Collector)
les documents.
Le Scorer est créé (et mis au travail) par
l'IndexSearcher, qui est le "maître" --
créé depuis le point d'entrée principal du programme.
Le "maître" se sert également d'IndexReader, qui
donne l'accès effectif à la collection de documents, et qui
nous met à disposition entre autres les accesseurs de stream
utilisés par TD().nextDoc().
1.3.1 Fonctionnalités à réaliser
Le travail est à faire dans le
fichier similarity/SimScorer.java
que vous devez compléter avec du code
-- les méthodes SimScorer.TermData.nextDoc() et
SimScorer.nextDocs().
La classe TermData correspond au TD() de la notation
algorithmique -- elle gère un stream pour un mot de la
requête donné.
public class TermData {
public Term queryTerm = null;
public TermDocs termDocs = null;
public int currentDoc = -1;
private final int[] docBuff = new int[32];
private final int[] freqBuff = new int[32];
private int pointer = 0;
private int pointerMax = 0;
public TermData() {}
private int nextDoc() throws IOException {
// a faire
}
La classe SimScorer en construit un
tableau
private TermData[] termData;
qu'elle initialise dans le constructeur. Celui-ci est appelé par la
méthode search de l'IndexSearcher.
À partir du programme principal (on peut le voir
dans SimpleLuceneSearcher.java), les
étapes principales pour faire une recherche dans la collection de
documents indexée sont :
- on crée un objet (dérivé) de la
classe IndexSearcher
- on crée un objet (dérivé) de la classe Query,
qui représente notre requête, à l'aide d'un analyseur
lexical de requêtes
- on crée un objet (dérivé) de la
classe Collector, qui nous servira à recueillir un par un les
numéros d'identification des documents apparié à leur
"score" (c'est-à-dire mesure de similarité du document
correspondant avec notre requête -- pour représenter avec
quelle qualité chaque document satisfait notre requête).
- on appelle la méthode search de l'IndexSearcher.
- puisque le Collector utilisé trie ("par lui-même") les
(id des) documents selon l'ordre décroissant du score, à la fin
on rend le résultat en énumérant tout simplement
les X premiers documents, avec X donné initialement sur
la ligne de commande.
1.3.2 Classes et outils disponibles et nécessaires pour notre travail
La classe Lucene TermDocs est celle qui permet
d'énumérer les documents, comme on le fait en
notation algorithmique TD(). On en obtient une instance avec la méthode
d'instance IndexReader.termDocs(Term t). La
classe Term sert à gérer les termes.
On utilise la classe TermDocs en appelant sa méthode
d'instance TermDocs . read(int[] docBuff, int[] freqBuff) qui
remplit les tampons. Ensuite, on lit depuis ces tampons, et lorsqu'ils sont
"consommés", on rappelle TermDocs . read(int[] docBuff, int[]
freqBuff) un coup, pour les re-remplir. Cette méthode rend
combien elle a réussi à lire.
La
constante NO_MORE_DOCS (prédéfinie) servira
à signaler la fin d'un stream.
C'est dans la méthode TermData . nextDoc() que tout ceci
est appelé. Elle gère, au moyen des
données-membres pointer et pointerMax,
le remplissage et la consommation du tampon de docIDs, ainsi que l'avancement
de currentDoc de la manière suivante :
private int nextDoc() throws IOException {
// on incremente le pointeur
// si le pointeur a atteint ou dépassé le pointeur maximal
//////// re-remplir les tampons avec termDocs.read()
//////// et réassigner le pointeur maximal à ce qu'on vient de lire
//////// et remettre pointeur à zéro
//////// si on n'a en fait rien lu, c'est la fin, donc
////////////// on met currentDoc à NO_MORE_DOCS, et on le rend
//////// finsi
// finsi
// on met currentDoc à ce qui est pointé par pointer dans le tampon docBuff
// on rend currentDoc
}
Dans la classe SimScore, c'est la
méthode nextDocs() qui appelle
soigneusement TermData.nextDoc(), ainsi:
public void nextDocs(TermData[] t) throws IOException {
// trouver le docId minimal parmi les currentDoc des elements de t
// pour tous les entiers de 0 a termCount (excluant termCount lui-meme)
/////// si le currentDoc de l'element courant de t est minimal
///////////////// lui appeler nextDoc() dessus
}
Travail à faire
- Télécharger
l'archive archTp4.tar.gz, et
ouvrez-là dans un répertoire dédié à ce
TP, et lancez make une fois
- Écrivez le code complet de la
méthode TermData.nextDoc() dans similarity/SimScorer.java
- Écrivez le code complet de la
méthode Scorer.nextDocs() dans similarity/SimScorer.java
- Testez avec une commande comme
java -jar similarity.jar search testOk3/luceneIndex 'armstrong marathon york' 5 _
- Expliquez en détail la sortie écran, en lisant dans le code, en
regardant dans data avec grep, etc.
2. Généralités sur les mesures de similarité
2.1 Utilité de la similarité
Calculer la similarité entre deux documents, ou bien entre un document
et une
requête, est une manière de voir combien proches les documents
(respectivement le document et la requête) le sont. Autrement dit, plus
la distance entre les deux est petite, plus la similarité est grande
-- donc plus ils sont similaires.
Si nous sommes en train de passer en revue un nombre de documents pour en
sélectionner ceux qui répondent à une question
posé, alors on peut prendre comme méthode générale
- calculer les similarités entre la requête et chaque
document
- trier les documents dans l'ordre décroissant
- donner comme réponse la liste des N premiers documents,
où N est par exemple fixé par l'utilisateur.
2.2 Définitions préliminaires
Considérons une collection de documents (indexée).
Soit V l'ensemble ordonné de tous les mots apparaissant dans nos
documents,
ainsi que dans les requêtes possibles. (Ces mots sont "purifiés",
c'est-à-dire qu'on élimine les articles, prépositions,
etc., on ne conjugue pas les verbes, on ne met pas les pluriels, etc, et dans
la littérature on les appelle également termes (et en
anglais, "terms").
Nous représentons alors chaque document (et requête) par un vecteur
de dimension |V|, contenant, pour chaque coordonnée, le nombre de
fois où le terme correspondant apparaît dans le
document. (Habituellement, donc, les requêtes auront plutôt
seulement des 1 et des 0).
Exemple
Soit les documents suivants
The Prime Minister decided to make a public statement on TV in the evening.
Document U
This week's macroeconomical indices have been commented upon by the
Minister of Economy this morning and are going to also be commented upon in the
evening by the Prime Minister.
et la requête R
minister statemement economy
Leurs termes sont
comment,decide,economy,evening,index,macroeconomical,make,minister,morning,prime,public,statement,TV,week.
et les vecteurs de ces documents et de la requête seront
T = (0,1,0,1,0,0,1,1,0,1,1,1,1,0)
U = (2,0,1,1,1,1,0,2,1,1,0,0,0,1)
R = (0,0,1,0,0,0,0,1,0,0,0,1,0,0)
2.3 Définitions des similarités
Nous donnons les définitions suivantes pour deux vecteurs T
et U, chacun ayant la dimension n
2.3.1 Similarité du cosinus
Pour calculer la similarité du cosinus on doit calculer le produit
scalaire des deux vecteurs et le diviser par le produit des normes des deux
vecteurs.
Concrètement :
- norme d'un vecteur T de dimension n :
|T| = sqrt(T1 * T1 + T2 * T2 + ... + Tn * Tn)
produit scalaire de deux vecteurs T et U de
dimension n :
T . U = T1 * U1 + T2 * U2 + ... + Tn * Un
similarité du cosinus de T et U
simCos(T,U) = (T . U) / (|T| * |U|)
2.3.2 Similarité basée sur la distance euclidienne
La distance euclidienne entre deux vecteurs T et U de dimension
n est
DE(T,U) = sqrt((T1 - U1)2 + (T2 - U2)2 + ... + (Tn - Un)2)
Pour en faire une similarité, on peut considérer par exemple
1/DE(T,U)
en "faisant attention" au cas ou
DE(T,U) = 0
Par "faire attention" on entend s'assurer que ce cas est proprement pris en compte par exemple dans un programme qui calcule ces valeurs. On ne peut pas travailler avec des nombres infinis, donc on peut utiliser par exemple le plus grand float représentable, ou codifier ce cas autrement, selon le but dans lequel on le traite (trier les résultats, etc.).
2.3.3 Coefficient de Jaccard
Le coefficient de Jaccard est défini comme étant le quotient du cardinal de l'intersection par celui de l'union, et on peut (dans un premier temps) négliger les fréquences (on les prendra en compte dans le coeffcient de Jaccard généralisé).
Reprenant l'exemple donné ci-avant (avec les deux documents et la
requête), le cardinal de l'intersection de la requête avec le
premier document est 2, parce que le premier document contient bien les termes
'minister' et 'statement' (et non pas 'economy'). Le cardinal de l'union de
l'ensemble de termes de la requêete avec celui du premier document est
9 (8 du document plus 'economy' de la requêete). Donc le coefficient est
2/9.
2.3.4 Coefficient de Jaccard généralisé
Pour ce calcul, on prend en compte les fréquences également, et
alors on a comme définition
simJaccG(T,U) = (T . U) / (|T|2 + |U|2 - (T . U))
2.4 Exercices
- Calculez les quatre similarités pour chacune des paires
(T,U) (T,R) (U,R) (U,T) (R,T) (R,U)
- Que pouvez vous observer, par exemple au sujet de (T,U) et
(U,T) ? Ceci semble-t-il être vrai pour d'autres paires
du même type, comme par exemple (R,T) et (T,R) ?
- Pouvez-vous justifier formellement ce que vous venez d'observer au point
précédent ?
3. Modification de la similiarité par défaut de Lucene
Nous allons nous servir de la base de code Lucene et des bouts de code
existants, à savoir la partie recherche dans un index.
3.1 But de ce travail
Le but de ce travail est de vous faire implémenter les quatre formules
de calcul de similarité, tout en intégrant ce code avec la base
existante, afin de permettre le traitement de nos requêtes à
l'aide justement d'une de ces quatre mesures de similarités --
choisie sur la ligne de commande. On travaille maintenant dans la
méthode SimScorer.score() -- celle qui ne prend
aucun argument, et la donnée-membre SimScorer.scoreDoc
nous dit quel est le docId du document pour lequel on doit calculer le score.
(Souvenez-vous de toute la
partie algorithmique que nous avons traitée au début de ce TP --
le but en ayant été d'arriver à énumérer
efficacement tous les documents "intéressants" -- pour lesquelles on doit
calculer le score. Souvenez-vous également de ce qui est dit plus haut
dans ce TP, à savoir comment se déroulent les choses, qui appelle
qui, etc. -- le tout est dans la section 1.3.1, et dans le code).
3.2 Détails de la base de code existante
Le but du jeu étant de calculer de quantités à partir des
termes de la requête et du document courant et de leur
fréquences, nous devons savoir comment obtenir ces renseignements
(à l'aide de l'IndexReader (donné au constructeur
de SimScorer), d'autres classes Lucene
auxiliaires, ainsi que lesquelles des données-membre de SimScorer
vont nous aider.
3.2.1 Termes d'un document
À partir de n'importe quel id d'un document, à l'aide de
l'instance d'IndexReader, on peut obtenir tous les termes
présents dans le document, ainsi que leur fréquences, avec un
appel à la méthode d'instance
IndexReader . getTermFreqVectors():
TermFreqVector[] docTermFreqVect = docReader . getTermFreqVectors(scoreDoc);
car scoreDoc est la donnée-membre
de SimScorer qui contient le docId du document courant, qui est
donc celui en train d'être "noté".
Donc, nous avons ainsi accès aux termes d'un document, pour tous les
champs (Field) initialement indexés de la sorte dans la
collection. Souvenons-nous que dans notre cas, nous avions choisi d'indexer seulement
le champ content. Donc, pour notre requête on a besoin d'un
seul TermFreqVector -- celui correspondant à ce champ. On
doit par contre trouver son indice dans le tableau de TermFreqVector rendu
par la méthode getTermFreqVectors(). Ainsi, on cherche un par un
celui dont le nom de champ (obtenable avec la méthode getField())
est 'content':
TermFreqVector[] docTermFreqVect = docReader . getTermFreqVectors(scoreDoc);
final int howManyTermFreqVects = docTermFreqVect . length;
int contentFieldNdx = -1;
for(int kField = 0; kField < howManyTermFreqVects; kField++) {
if(docTermFreqVect[kField] . getField() . equal("content")) {
contentFieldNdx = kField;
}
}
if(contentFieldNdx < 0) {
throw new IOException("Cannot find the 'content' field among the ones with term frequencies");
}
Donc nous par la suite utiliser l'élément
docTermFreqVect[contentFieldNdx], qui est un TermFreqVector,
lequel est une autre classe Lucene, qui nous donne le String[]
des termes, ainsi que le int[] de leur fréquences:
final int docTermCount = docTermFreqVect[contentFieldNdx] . size();
final String[] docTerms = docTermFreqVect[contentFieldNdx] . getTerms();
final int[] docFreqs = docTermFreqVect[contentFieldNdx] . getTermFrequencies();
(On utilise le premier élément car c'est celui-ci, ici, dans
notre cadre,
Enfin un dernier mot d'algorithme et structures de données: en regardant
les définitions des mesures de similarités, on s'aperçoit
qu'il faut pouvoir pour tout terme trouver sa fréquence, et que
docTerms ensemble avec docFreqs nous offrent
cet accès plutôt en les énumérant un par un et non
pas directement. Donc nous devons nous prendre une table de
hachage docTermMap dans laquelle, pour chaque terme du
document, on met l'indice dans ces
tableaux. Ainsi, on remplit d'abord docTermMap utilisant une
boucle. Ensuite, chaque fois qu'on a besoin, pour un terme String
toto quelconque de connaître rapidement sa fréquence dans le
document courant scoreDoc, on pourra utiliser
docFreqs[(Integer)docTermMap . get(toto)]
De même, juste pour savoir si le document contient bien le terme String
toto, on peut faire
if(docTermMap . containsKey(toto)) { // oui, le document le contient bien
3.2.2 Termes de la requête
Les termes de la requête sont déjà disponibles car ils ont
été extraits dans le constructeur de SimScorer
(dont le code source vous est entièrement fourni). On y a accès
depuis le tableau SimScorer . termData, ainsi
termData[kTerm] . queryTerm . text()
(pour obtenir l'élément de rang kTerm). On peut
bien entendu également les mettre dans une table de hachage,
similairement à ce qui est expliqué plus haut pour les termes du
document, ainsi :
Map<String,Integer> queryTermMap = new HashMap<String,Integer>();
for(int kTerm = 0; kTerm < termCount; kTerm++) {
queryTermMap . put(termData[kTerm] . queryTerm . text(),kTerm);
}
et ensuite, si on a besoin de savoir donc si un certain terme String
thisTerm se trouve parmi les termes de la requête, on fait
if(queryTermMap . containsKey(thisTerm)) { // oui, la requête le contient bien
Pour ce qui est de leur fréquences, on les a toutes déjà dans
la donnée-membre
final float[] queryVect;
de SimScorer (c'est fait dans le constructeur -- elles sont simplement mises à 1
ici, pour simplifier).
Lors des calculs des mesures de similarité on va donc
utiliser queryVect pour obtenir les coordonnées non-nulles
du vecteur de la requête.
Donc revenant à queryTermMap, pour obtenir la valeur du bon élément de
queryVect pour un terme donné String
thisTerm, on fait
final int queryNdx = (Integer)queryTermMap . get(thisTerm);
Enfin, la norme du vecteur de
termes de la requête est déjà calculée (toujours dans le
constructeur), et est donc disponible dans la donnée-membre queryNorm.
3.2.3 Corps de la méthode SimScorer.score()
Vous avez ici la vue d'ensemble du corps entier de la
méthode SimScorer.score().
public float score() throws IOException {
System.out.print("#" + scoreDoc + " ");
assert scoreDoc != -1;
float scoreValue = 0.0f;
/// 1. Préparatifs initiaux
if(similarityType . equals("EuclideanSimilarity")) {
// 2. Calcul similarité basée sur la distance euclidienne
}
else if(similarityType . equals("CosineSimilarity")) {
// 3. Calcul similarité du cosinus
}
else if(similarityType . equals("JaccardSimilarity")) {
// 4. Calcul coefficient de Jaccard
}
else if(similarityType . equals("GenJaccardSimilarity")) {
// 5. Calcul coefficient de Jaccard généralisé
}
else {
throw new IOException("Unknown similarity type " + similarityType);
}
System.out.println(similarityType + " score=" + scoreValue);
return scoreValue;
}
3.3 Détails algorithmiques pour les préparatifs initiaux et pour
le calcul de chaque mesure de similarité
Maintenant nous allons regarder comment nous prendre pour calculer chaque
mesure de similarité selon leurs formules données plus haut, et
utilisant les outils exposés ci-avant.
3.3.1 Préparatifs initiaux
Avant de passer aux étapes précises, signalons que le gros du
code Java à écrire pour les préparatifs initiaux vous a
été déjà fourni en "pièces
détachées" dans la section 3.2, donc il faudra principalement
aller les chercher là-dedans et les mettre ensemble.
on récupère le TermFreqVector[] de scoreDoc depuis le reader
on récupère la longueur du tableau de TermFreqVectors qu'on vient de récupèrer
on se prend le contentFieldNdx à -1
boucle pour chaque valeur entière de 0 au nombre égal à la longueur du tableau de TermFreqVectors
si getField() . equals("content") appliquée à l'élé,ent courant du tableau de TermFreqVectors
alors on a trouvée l'indice recherché, donc on le met dans contentFieldNdx
finboucle
si contentFieldNdx n'a pas de bonne valeur, on lève une exception
on se prend une Map<String,Integer> docTermMap qu'on instancie avec new HashMap<String,Integer>();
on se prend dans docTermCount le nombre d'éléments du TermFreqVector choisi (c'est-à-dire docTermFreqVect[contentFieldNdx])
on se prend le String[] docTerms depuis, avec getTerms();
boucle pour chaque valeur entière de 0 à docTermCount // pour remplir la docTermMap
on met dans docTermMap l'élément courant de docTerms ainsi que son indice
finboucle
3.3.2 Calcul similarité basée sur la distance euclidienne
on récupère dans int[] docFreqs les fréquences du document courant
float sumOfWeightDiffSq = 0;
int qAllQueryTermsInDoc = 1;
on déclare, instancie et remplit la table de hachage queryTermMap comme expliqué au 3.2.2
boucle pour chaque valeur entière de 0 à docTermCount
soit thisTermW la valeur de l'élément courant de docFreqs
si la queryTermMap contient le terme courant du document (on les a tous dans docTerms -- voir 3.3.1) alors
on récupère son indice queryNdx depuis la queryTermMap (voir 3.2.2)
on rajoute a sumOfWeightDiffSq la quantité (thisTermW - queryVect[queryNdx]) * (thisTermW - queryVect[queryNdx])
si thisTermW n'est pas égale à queryVect[queryNdx], alors
qAllQueryTermsInDoc est mise à z´ro
finsi
sinon
on rajoute a sumOfWeightDiffSq la quantité thisTermW * thisTermW
qAllQueryTermsInDoc est mise à z´ro
finsi
finboucle
si docTermCount et termCount sont égaux et que qAllQueryTermsInDoc vaut 1 alors
scoreValue = Float . MAX_VALUE;
sinon
scoreValue prend la valeur de 1 divisée par sumOfWeightDiffSq
finsi
3.3.3 Calcul similarité du cosinus
on récupère dans int[] docFreqs les fréquences du document courant
on calcule docNorm (la norme du vecteur de frequences du document: boucle pour la somme des carrés, et sqrt() après)
on se prend float dotProd = 0; pour calculer le produit scalaire ("the dot product" en anglais)
boucle pour chaque entier de 0 à termCount (c'est-à-dire pour les terme de la requête)
on se prend final String queryTerm le terme courant (voir 3.2.2)
si le document contient également queryTerm parmi ses termes (utilisez docTermMap, voir la fin de 3.2.1)
récupérez l'indice docTermCoord de queryTerm dans docFreqs (à partir de la docTermMap, voir la fin de 3.2.1)
dotProd prend la valeur dotProd plus le produit queryVect[kTerm] * docFreqs[docTermCoord]
finsi
finboucle // (on a donc ainsi accumule dans dotProd les produits des bonnes coordonnees pour le produit scalaire)
scoreValue prend la valeur selon la formule (qutient de dotProd par le produit des deux normes, qui sont queryNorm et docNorm)
3.3.4 Calcul coefficient de Jaccard
on initialise intersectSize (qui est float) et queryTermNotInDocCount a zero
boucle pour chaque entier de 0 à termCount
si la docTermMap contient le terme courant de la requete (voir 3.2.2 pour l'obtenir)
intersectSize++
sinon
queryTermNotInDocCount++
finsi
finboucle
on se prend alors float unionSize comme étant docTermFreqVect[contentFieldNdx] . size() + queryTermNotInDocCount
la scoreValue est le quotient de intersectSize et de unionSize
3.3.5 Calcul coefficient de Jaccard généralisé
Vous avez tous les éléments nécessaires pour son calcul
depuis la similarité du cosinus : le produit scalaire ainsi que les normes. Faites attention à
utiliser les normes au carré.
Travail à faire
- Implémentez les préparatifs initiaux et le calcul des quatres
mesures de similarité dans la méthode SimScorer.score() (celle qui ne prend
aucun argument), selon les indications, dans le
fichier similarity/SimScorer.java (même fichier que
là où vous avez travaillé au point précédent).
- Compilez et testez
java -jar similarity.jar search testOk3/luceneIndex 'armstrong marathon york' 5 EuclideanSimilarity
java -jar similarity.jar search testOk3/luceneIndex 'armstrong marathon york' 5 CosineSimilarity
java -jar similarity.jar search testOk3/luceneIndex 'armstrong marathon york' 5 JaccardSimilarity
java -jar similarity.jar search testOk3/luceneIndex 'armstrong marathon york' 5 GenJaccardSimilarity