CI8 : Recherche d'informations
Implémenter un moteur de recherche simple.
Recherche d'informations
Dans cet exercice, nous allons créer un moteur de recherche. Pour cela, nous allons continuer le code commencé pendant le TP sur l'introduction au traitement du texte.
Nous allons commencer par créer notre index inversé. Pour cela, créez une classe InvertedIndex. Dans
cette classe, ajoutez un constructeur qui initialise trois dictionnaires : un premier transformant un terme en une
liste ordonnée de documents et le deuxième qui contient pour chaque terme le nombre de documents dans lequel il
apparaît, et le troisième qui contient l'identifiant unique de chaque terme.
Ajoutez également une liste vide qui contiendra tous les documents. L'identifiant d'un document sera
l'index du document dans cette liste.
Ajoutez également un champs nlp contenant les opérations en anglais de Spacy.
Ajoutez également un champs nlp contenant les opérations en anglais de Spacy.
class InvertedIndex:
def __init__(self):
self.inverted_index = {}
self.document_frequency = {}
self.documents = []
self.term_to_id = dict()
self.nlp = spacy.load('en_core_web_sm')
Reprenez la fonction normalize(text) du TP sur l'introduction au traitement du langage naturel et
ajoutez-la à la classe InvertedIndex. Si ce n'est pas déjà le cas, ajoutez le nettoyage par regex qui permettait
d'enlever les videos et les sources des images.
def normalize(self, text):
text = RE_VIDEO.sub("", text)
text = RE_SOURCES.sub("", text)
text = text.replace("\n", " ").replace(" ", " ")
doc = self.nlp(text)
return [token.lemma_.lower() for token in doc if not token.is_stop and not token.is_punct]
Écrivez une fonction add_document(document) qui ajoute un document à l'index. Pour cela, il
faut :
- Ajouter le document à la liste de documents et récupérer son index
- Normaliser le document
- Prendre le set des termes
- Pour chaque terme, ajouter le document aux deux dictionnaires (dans une liste pour l'un, en incrémentant le compteur pour l'autre)
- Pour chaque terme, s'il n'a pas été rencontré avant, lui attribuer un identifiant unique (en commençant à 0)
def add_document(self, document):
self.documents.append(document)
index = len(self.documents) - 1
terms = set(self.normalize(document, self.nlp))
for term in terms:
if term not in self.inverted_index:
self.inverted_index[term] = []
self.document_frequency[term] = 0
self.term_to_id[term] = len(self.inverted_index) - 1
self.inverted_index[term].append(index)
self.document_frequency[term] += 1
Écrivez une méthode save(self, filename) et une fonction load(filename) qui respectivement
enregistrent et charge l'index dans un fichier. Pour cela, on pourra enregistrer les attributs sous la forme d'un
fichier JSON.
def save(self, filename):
to_save = {
"inverted_index": self.inverted_index,
"document_frequency": self.document_frequency,
"documents": self.documents,
"term_to_id": self.term_to_id,
}
with open(filename, "w") as f:
json.dump(to_save, f)
@staticmethod
def load(filename):
index = InvertedIndex()
with open(filename, "r") as f:
to_load = json.load(f)
index.inverted_index = to_load["inverted_index"]
index.document_frequency = to_load["document_frequency"]
index.documents = to_load["documents"]
index.term_to_id = to_load["term_to_id"]
return index
Écrivez une fonction search_conjunction(words) qui retourne tous les documents contenants tous les
mots dans words.
On pourra écrire une fonction annexe merge_conjunction(l1, l2) qui fusionne les deux
listes de manière adéquate.
def merge_conjunction(l1, l2):
idx1 = 0
idx2 = 0
res = []
while idx1 l2[idx2]:
idx2 += 1
else:
res.append(l1[idx1])
idx1 += 1
idx2 += 1
return res
def search_conjunction(self, words):
first_word = words[0]
if first_word not in self.inverted_index:
return []
res = self.inverted_index[first_word]
if len(words) == 1:
return [self.documents[i] for i in res]
for word in words[1:]:
if word not in self.inverted_index:
return []
res = self.merge_conjunction(res, self.inverted_index[word])
return [self.documents[i] for i in res]
Écrivez une fonction search_disjunction(words) qui retourne tous les documents contenants au moins
un mot de words.
On pourra écrire une fonction annexe merge_disjunction(l1, l2) qui fusionne les deux
listes de manière adéquate.
def merge_disjunction(l1, l2):
idx1 = 0
idx2 = 0
res = []
while idx1 = len(l2) or (idx1 = len(l1) or l1[idx1] > l2[idx2]:
res.append(l2[idx2])
idx2 += 1
else:
res.append(l1[idx1])
idx1 += 1
idx2 += 1
return res
def search_disjunction(self, words):
res = []
for word in words:
if word not in self.inverted_index:
continue
res = self.merge_disjunction(res, self.inverted_index[word])
return [self.documents[i] for i in res]
(Optionnel) Adaptez votre code pour faire de l'optimisation de requête comme vu en cours.
(Optionnel) Vous pouvez rajouter la possibilité de recherche d'autres opérations booléennes.
Écrivez une fonction get_cosine_similarity(u, v) calculant la similarité cosinus entre les vecteurs
u et v supposés de même taille.
@staticmethod
def get_cosine_similarity(u, v):
norm_u = 0
norm_v = 0
dot_product = 0
for x, y in zip(u, v):
norm_u += x * x
norm_v += y * y
dot_product += x * y
return dot_product / math.sqrt(norm_u * norm_v)
Importez et adaptez les fonctions get_bow(text) et get_tfidf(text) du dernier TP sur le
TAL dans la classe InvertedIndex.
def get_bow(self, text):
res = [0] * len(self.inverted_index)
for word in text:
res[self.term_to_id[word]] += 1
return res
def get_tfidf(self, text):
res = [0] * len(self.inverted_index)
text = self.normalize(text)
bow = self.get_bow(text)
for term, i in self.term_to_id.items():
res[i] = bow[i] * math.log(len(self.documents) / self.document_frequency[term])
return res
Écrivez une fonction get_top_k(query, k, vocab, to_idx, n_words_in_documents, n_documents) qui
retourne les top k meilleurs documents pour la requête query. Pour cela :
- Calculez le vecteur tfidf de la requête
- Normalisez la requête et récupérer le set des termes
- Récupérez tous les documents contenant au moins un terme de la requête
- Pour chaque document trouvé, calculez sa similarité cosinus avec le vecteur de la requête
- Retournez les top k documents avec la similarité la plus faible
Les top k meilleurs résultats peuvent être obtenu en utilisant la fonction de tri de Python
sorted avec le bon paramètre de key (allez voir la documentation)
def get_top_k(self, query, k):
query_vector = self.get_tfidf(query)
terms_query = list(set(self.normalize(query)))
search_results = self.search_disjunction(terms_query)
similarities = []
for document in search_results:
vector = self.get_tfidf(document)
similarities.append(self.get_cosine_similarity(query_vector, vector))
ranked_results = sorted(zip(similarities, search_results), key=lambda x: -x[0])
return ranked_results[:k]
if __name__ == '__main__':
filename = "index.json"
save = False
if save:
inverted_index = InvertedIndex()
articles = json.load(open('../intro_nlp/all_articles.json'))
for article in articles:
inverted_index.add_document(article)
inverted_index.save(filename)
else:
inverted_index = InvertedIndex.load(filename)
search_results = inverted_index.search_conjunction(["elon", "musk", "twitter"])
print("Number of results:", len(search_results))
if search_results:
print(search_results[0])
search_results = inverted_index.search_conjunction(["microsoft", "gates"])
print("Number of results:", len(search_results))
if search_results:
print("Article", search_results[0])
search_results = inverted_index.search_disjunction(["elon", "musk", "twitter"])
print("Number of results:", len(search_results))
print(inverted_index.get_top_k("elon musk and twitter", 1))