From 1e59a902fb4b6c2169f706adc4f078e38371ec40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Pelletier?= Date: Mon, 30 Sep 2019 12:00:16 -0400 Subject: [PATCH] ajouts de commentaires --- rapport.md | 6 +-- t1_conversion_questions.py | 50 +++++++++++++------- t2_classification_noms.py | 83 +++++++++++++++++----------------- t3_classification_questions.py | 15 ++++-- 4 files changed, 87 insertions(+), 67 deletions(-) diff --git a/rapport.md b/rapport.md index 8d537ca..e7cd334 100644 --- a/rapport.md +++ b/rapport.md @@ -18,20 +18,20 @@ Comme on souhaite garder la forme la plus générale possible pour ces expressio L'approche utilisée a été de trouver d'abord les mots interrogatifs, ensuite la structure générale de la phrase, dont le verbe principal et les noms à remplacer, le cas échéant. Puis, on a identifié les différents types de réponses requises, et lorsqu'un même motif de question nécessitait deux types de réponses différentes, on a séparé en deux expressions consécutives. Ensuite, on a construit quelques règles qui appliquent des corrections mineures au niveau des signes et de la ponctuation. -Toutes ces règles sont enchainées les unes à la suite des autres. Comme la conversion élimine le point d'interrogation à la fin de la phrase, les motifs subséquents au premier qui a donné une correspondance sont ainsi ignorés. Il n'y a donc aucun traitement conditionnel en dehors de ceux inclus dans les expressions régulières. +Toutes ces règles sont enchainées les unes à la suite des autres. Comme la conversion élimine le point d'interrogation à la fin de la phrase, les motifs subséquents au premier qui a donné une correspondance sont ainsi ignorés. Il n'y a donc aucun traitement conditionnel en dehors de ceux inclus dans les expressions régulières. Si on n'est pas parvenu à transformer l'affirmation, on retourne une réponse par défaut. Ce qui ne se produit pas avec les questions de cet exercice. ### Description des motifs utilisés Les motifs suivent cet ordre. Sauf indication contraire, le groupe affirmatif à convertir est situé après le motif recherché : 1. Mot interrogatif **qui**, suivi d'un verbe auxiliaire, sous forme d'énumération partielle ; -2. Mot interrogatif **qu'est-ce que **; +2. Mot interrogatif **qu'est-ce que**; 3. Mot interrogatif **que** suivi d'un verbe d'état ou d'action. On a ici utilisé une énumération partielle pour identifier les verbes présents dans les questions en échantillon ; 4. Mot interrogatif **où** ou de la forme **dans quelle** suivi d'une catégorie géographique, sous forme d'énumération partielle. On a aussi capturé la forme particulière **quelle est la capitale** où on remplace le mot **quelle** par où, afin de correspondre au motif précédent. Ceci permet d'éviter une règle additionnelle ; 5. Mot interrogatif **quel** suivi soit du mot âge et du verbe avoir, soit du verbe auxiliaire et d'un nom représentant une quantité, sous forme d'énumération ; 6. Mot interrogatif **quel** suivi du verbe être (forme générique du motif précédent) ; 7. mot interrogatif **quand** ou groupe **en quelle année **; -8. mot interrogatif **pourquoi **; +8. mot interrogatif **pourquoi**; 9. mot interrogatif **combien** suivi du mot **de **; 10. mot interrogatif **combien** suivi de **y a-t-il**. La différence ici est que la réponse conserve l'ordre de la question et qu'il faut transformer **y a-t-il** en **il y a **; 11. Mot interrogatif **quel** à la fin de la question. Ce motif sert à attraper une forme moins fréquente de questions où la question est posée après le groupe affirmatif. diff --git a/t1_conversion_questions.py b/t1_conversion_questions.py index 68309f8..60139b5 100644 --- a/t1_conversion_questions.py +++ b/t1_conversion_questions.py @@ -7,6 +7,7 @@ results_filename = "./t1_results.txt" test_questions_fn = "./data/test-questions-t1.txt" test_results_filename = "./test-t1_results.txt" + # Mettre dans cette partie les expressions régulières # que vous utilisez pour analyser les questions # et en faire la conversion en affirmations @@ -19,10 +20,11 @@ def run_task1(filename): results = convert_all_questions(questions) save_results(results) -def run_task_test(filename, test_results_filename): + +def run_task_test(filename, res_filename): questions = load_questions(filename) results = convert_all_questions(questions) - save_results(results, test_results_filename) + save_results(results, res_filename) def convert_all_questions(questions): @@ -50,22 +52,25 @@ def convert(question): # En cas de doute, me consulter. # Votre code... + # Mot interrogatif **qui** regex_qui = re.compile(r"(qui)\s" r"(est|sont|était|étaient|sera|seront)\s" r"(.*)\?", re.IGNORECASE) modif1_0 = re.sub(regex_qui, - r"\3 \2 Luc Lamontagne.", - question) + r"\3 \2 Luc Lamontagne.", + question) + regex_qui_a = re.compile(r"(qui)\s" - r"(a)\s" - r"(.*)\?", - re.IGNORECASE) + r"(a)\s" + r"(.*)\?", + re.IGNORECASE) modif1 = re.sub(regex_qui_a, r"Luc Lamontagne \2 \3.", modif1_0) + regex_quel_nom = re.compile(r"quel\s" r"(est)\s" r"(le nom du chef)" @@ -75,6 +80,7 @@ def convert(question): r"\2 \3 \1 Luc Lamontagne.", modif1) + # Mot interrogatif ** qu'est-ce que** regex_questce = re.compile(r"(qu'est-ce\squ[ie'])\s?" r"(.*)\?", re.IGNORECASE) @@ -82,6 +88,7 @@ def convert(question): r"\2 est X.", modif1_1) + # Mot interrogatif **que** regex_quesont = re.compile(r"(qu[e'])\s" r"(est|sont|signifie|fait)-?(on)?" r"(.*)\?", @@ -90,6 +97,7 @@ def convert(question): r"\4 \3 \2 X.", modif2) + # Mot interrogatif **où** ou de la forme **dans quelle** regex_ou = re.compile(r"((dans\squel(le)?\s(ville|région|pays))|où)\s" r"(est|sont)\s" r"(.*)\?", @@ -101,8 +109,9 @@ def convert(question): regex_capitale = re.compile(r"(quel[le]*)\s(est)\s(la\scapitale|l'emplacement)(.*)\?", re.IGNORECASE) modif4_1 = re.sub(regex_capitale, - r"\3 \4 \2 Québec.", modif4) + r"\3 \4 \2 Québec.", modif4) + # Mot interrogatif **quel** regex_mesure = re.compile(r"([aà]\s)?qu[el]+\s(âge\s)?" r"(est|avait)?\s?" r"(l[a']\s?)?" @@ -117,18 +126,21 @@ def convert(question): re.IGNORECASE) modif6 = re.sub(regex_quelle_est, r"\1\3 \2 X.", modif5) + # mot interrogatif **quand** regex_quand = re.compile(r"(quand|en\squelle\sannée)\s(est\sné\s)?(.*)\?", re.IGNORECASE) modif7 = re.sub(regex_quand, r"\3 \2en 2000.", modif6) + # mot interrogatif **pourquoi ** regex_pourquoi = re.compile(r"pourquoi\s(.*)\?", re.IGNORECASE) modif8 = re.sub(regex_pourquoi, r"\1 parce que X.", modif7) + # mot interrogatif **combien** regex_combien1 = re.compile(r"combien\sde\s(\w+)(.*)\?", re.IGNORECASE) modif9 = re.sub(regex_combien1, @@ -143,27 +155,31 @@ def convert(question): r"\1 1000 \2.", modif9)) + # Mot interrogatif **quel** à la fin regex_quelle_fin = re.compile(r"(.*?)(qu\w+)\s(\w+)\?", re.IGNORECASE) modif11 = re.sub(regex_quelle_fin, r"\1 \3 X.", modif10) - # Modifications non liées aux mots + # Inversions de pronoms modif12 = re.sub(r"(\w+)(-t)?-(il|elle)", r"\1", modif11) - modif12 = re.sub(r"(\w+)(-t)?-(vous)", r"\3 \1", modif12) - modif12 = re.sub(r"\s{2,}", " ", modif12) - modif12 = re.sub(r"^\s", "", modif12) - modif12 = re.sub(r"\'\s", "'", modif12) + modif13 = re.sub(r"(\w+)(-t)?-(vous)", r"\3 \1", modif12) + # Suppression des espaces + modif14 = re.sub(r"\s{2,}", " ", modif13) + modif15 = re.sub(r"^\s", "", modif14) + modif16 = re.sub(r"\'\s", "'", modif15) + + # Retourner la question seulement si on a réussi à la modifier en affirmation if question != modif11: - return modif12 + return modif16 else: return "la terre est ronde." -def save_results(results, results_filename=results_filename): - with open(results_filename, 'w') as output_file: +def save_results(results, res_filename=results_filename): + with open(res_filename, 'w') as output_file: for question, sentence in results: output_file.write("Q: " + question + "\n") output_file.write("A: " + sentence + "\n") @@ -171,7 +187,7 @@ def save_results(results, results_filename=results_filename): if __name__ == '__main__': - # Vous pouvez modifier cette section + print("Conversion des questions du fichier {} ".format(questions_fn)) run_task1(questions_fn) run_task_test(test_questions_fn, test_results_filename) diff --git a/t2_classification_noms.py b/t2_classification_noms.py index c2c2ecc..7b072f2 100644 --- a/t2_classification_noms.py +++ b/t2_classification_noms.py @@ -21,11 +21,9 @@ all_origins = [] # la liste des 18 langues d'origines de noms BOS = "~" # character used to pad the beginning of a name EOS = "!" # character used to pad the end of a name - +v = 26 kgram_models = {} -V = 26 - def find_files(path): """Retourne le nom des fichiers contenus dans un répertoire. @@ -52,8 +50,8 @@ def unicode_to_ascii(s): def read_names(filename): """Retourne une liste de tous les noms contenus dans un fichier.""" - with open(filename, encoding='utf-8') as f: - names = f.read().strip().split('\n') + with open(filename, encoding='utf-8') as file_handle: + names = file_handle.read().strip().split('\n') return [unicode_to_ascii(name) for name in names] @@ -67,6 +65,7 @@ def load_names(): names_by_origin[origin] = names +# Cette fonction normalise les mots en les convertissant en ASCII, mettant en minuscules et supprimant les espaces def normalize_word(word): word = unicode_to_ascii(word) word = word.lower() @@ -74,10 +73,14 @@ def normalize_word(word): return word +# On génère les unigrammes à l'aide d'une liste def make_unigram(word): return list(word) +# On génère les k-grammes en ajoutant du padding et +# en traversant la chaîne de caractères de gauche à droite +# avec un pas de 1 et une largeur de k def make_kgram(unigram_word, k): # Ajouter padding unigram_with_padding = [BOS for i in range(1, k)] + unigram_word + [EOS for i in range(1, k)] @@ -85,6 +88,7 @@ def make_kgram(unigram_word, k): return kgrams +# Cette fonction crée un modèle de langue pour une origine en prenant en entrée des k-grammes def make_count_dict(kgrams_origin): kgram_count_dict = {} for kgram_name in kgrams_origin: @@ -100,10 +104,6 @@ def train_models(): # # Vous pouvez ajouter au fichier toutes les fonctions que vous jugerez nécessaire. # Merci de ne pas modifier les fonctions présentes dans ce fichier. - # - # À compléter - Fonction pour la construction des modèles unigrammes, bigrammes et trigrammes. - # - # Votre code à partir d'ici... # Construire un tableau de fréquence par langue. Chaque tableau est une entrée dans un dictionnaire for origin in names_by_origin.keys(): @@ -121,7 +121,6 @@ def train_models(): def most_probable_origin(name, n=3, use_logprob=True): # Retourne la langue d'origine la plus probable du nom. # n désigne la longueur des N-grammes. Par ex n=3 --> trigramme - # À compléter... # Calculer les log-prob et les perplexités log_prob_origin = {} @@ -143,25 +142,24 @@ def most_probable_origin(name, n=3, use_logprob=True): def logprob(name, origin, n=3): # Retourne la valeur du logprob d'un nom étant donné une origine # Utilisez une fonction logarithme en base 2. - # À compléter... unigram_name = make_unigram(name) kgram_name = make_kgram(unigram_name, n) k_counts = kgram_models[(origin, n)] logprob_name = 0 if n == 1: - N = sum(k_counts.values()) + prob_n = sum(k_counts.values()) for kgram in kgram_name: - C = k_counts.get(kgram, 0) - L = math.log((C + 1) / (N + V)) - logprob_name = logprob_name + L + prob_c = k_counts.get(kgram, 0) + prob_l = math.log((prob_c + 1) / (prob_n + v)) + logprob_name = logprob_name + prob_l else: k1_counts = kgram_models[(origin, n - 1)] for kgram in kgram_name: - N = k1_counts.get(kgram[:-1], 0) - C = k_counts.get(kgram, 0) - L = math.log((C + 1) / (N + V)) - logprob_name = logprob_name + L + prob_n = k1_counts.get(kgram[:-1], 0) + prob_c = k_counts.get(kgram, 0) + prob_l = math.log((prob_c + 1) / (prob_n + v)) + logprob_name = logprob_name + prob_l return logprob_name @@ -172,26 +170,24 @@ def prod(iterable): def perplexity(name, origin, n=3): # Retourne la valeur de perplexité d'un nom étant donné une origine # À compléter... - unigram_name = make_unigram(name) kgram_name = make_kgram(unigram_name, n) length_name = len(kgram_name) k_counts = kgram_models[(origin, n)] - V = 26 probs_name = [] if n == 1: - N = sum(k_counts.values()) + prob_n = sum(k_counts.values()) for kgram in kgram_name: - C = k_counts.get(kgram, 0) - P = (C + 1) / (N + V) - probs_name.append(P) + prob_c = k_counts.get(kgram, 0) + prob_p = (prob_c + 1) / (prob_n + v) + probs_name.append(prob_p) else: k1_counts = kgram_models[(origin, n - 1)] for kgram in kgram_name: - N = k1_counts.get(kgram[:-1], 0) - C = k_counts.get(kgram, 0) - P = (C + 1) / (N + V) - probs_name.append(P) + prob_n = k1_counts.get(kgram[:-1], 0) + prob_c = k_counts.get(kgram, 0) + prob_p = (prob_c + 1) / (prob_n + v) + probs_name.append(prob_p) perp = math.exp(-sum([math.log(x) for x in probs_name]) / length_name) @@ -229,14 +225,14 @@ def evaluate_models(filename, n=3, use_logprob=True): def compute_v(): - global V + global v vocab = {} for origin in all_origins: modele_courant = kgram_models[(origin, 1)] for k in modele_courant.keys(): vocab[k] = vocab.get(k, 0) + modele_courant.get(k, 0) - V = len(vocab.keys()) + 1 - print("la valeur de V est "+str(V)) + v = len(vocab.keys()) + 1 + print("la valeur de V est " + str(v)) if __name__ == '__main__': @@ -261,22 +257,25 @@ if __name__ == '__main__': # print("\nLes données pour tester vos modèles sont:") # for org, name_list in test_names.items(): # print("\t", org, name_list) - for n in range(1, 4): - res_logprob, succ_logprob = evaluate_models(test_filename, n, True) - res_perp, succ_perp = evaluate_models(test_filename, n, False) + for current_n in range(1, 4): + res_logprob, succes_logprob = evaluate_models(test_filename, current_n, True) + res_perp, succes_perp = evaluate_models(test_filename, current_n, False) print(res_logprob) # Résultats pour le rapport - df_succ_logprob = pd.DataFrame.from_dict(succ_logprob).fillna(0).transpose() - df_succ_logprob.columns = ['echec_logprob', 'succes_logprob'] - df_succ_perp = pd.DataFrame.from_dict(succ_perp).fillna(0).transpose() - df_succ_perp.columns = ['echec_perp', 'succes_perp'] - df_succ = df_succ_logprob.join(df_succ_perp) + df_succes_logprob = pd.DataFrame.from_dict(succes_logprob).fillna(0).transpose() + df_succes_logprob.columns = ['echec_logprob', 'succes_logprob'] + df_succes_perp = pd.DataFrame.from_dict(succes_perp).fillna(0).transpose() + df_succes_perp.columns = ['echec_perp', 'succes_perp'] + df_succ = df_succes_logprob.join(df_succes_perp) f, ax = plt.subplots(figsize=(9, 9)) hm_echec = sn.heatmap(df_succ, annot=True, fmt="d", linewidths=.5, ax=ax, cmap='Oranges') - hm_echec.set(title="Performance de modèles "+{1:"uni",2:"bi",3:"tri"}[n]+"grammes \nsur les données de test") + hm_echec.set( + title="Performance de modèles " + + {1: "uni", 2: "bi", 3: "tri"}[current_n] + + "grammes \nsur les données de test") plt.tight_layout() - hm_echec.get_figure().savefig("images/2_seaborn_hm_echec_"+str(n)+".png") + hm_echec.get_figure().savefig("images/2_seaborn_hm_echec_" + str(current_n) + ".png") diff --git a/t3_classification_questions.py b/t3_classification_questions.py index 16a50ab..f0ca0f6 100644 --- a/t3_classification_questions.py +++ b/t3_classification_questions.py @@ -14,6 +14,7 @@ from sklearn.utils.multiclass import unique_labels training_questions_fn = "./data/questions-t3.txt" test_questions_fn = "./data/test-questions-t3.txt" + # Cette fonction provient de https://stackoverflow.com/a/7716358 def mean(numbers): return float(sum(numbers)) / max(len(numbers), 1) @@ -25,6 +26,7 @@ def run_question_classification(training_fn, test_fn): print("Accuracy on test set: {0:.4f}".format(accuracy_test)) +# Fonction de décompte personnalisée avec les trois paramètres optimisés def custom_count_vectorize_questions(min_df, max_df, use_stop_words): stop_words = [None, 'english'][use_stop_words] count_vec = CountVectorizer(analyzer='word', @@ -42,9 +44,7 @@ def train_and_test_classifier(training_fn, test_fn): # test_questions, test_labels = load_dataset(test_questions_fn) print("Nb questions de test:", len(test_questions)) - # Insérer ici votre code pour la classification des questions. - # Votre code... - + # Multinomial Naive Bayes mnb = MultinomialNB() # Sélection des meilleurs paramètres pour le CountVectorizer @@ -74,18 +74,22 @@ def train_and_test_classifier(training_fn, test_fn): count_questions_train_opt = count_vec_opt.fit_transform(questions) count_questions_test_opt = count_vec_opt.transform(test_questions) + # Optimisation de l'hyperparamètre alpha + print("\nRecherche de l'hyperparamètre alpha:") gvc = GridSearchCV(mnb, param_grid={'alpha': [x / 10.0 for x in range(1, 21)]}, cv=5) mnb_fit = gvc.fit(count_questions_train_opt, labels) - print("Score: {0:.4f}".format(str(mnb_fit.best_score_))) + print("Score: {0:.4f}".format(mnb_fit.best_score_)) print("Paramètres: " + str(mnb_fit.get_params())) u_labels = unique_labels(labels, test_labels) + # Calcul des prédictions et de la performance + labels_predict_train = mnb_fit.predict(count_questions_train_opt) labels_predict_test = mnb_fit.predict(count_questions_test_opt) @@ -112,7 +116,8 @@ def train_and_test_classifier(training_fn, test_fn): f, ax4 = plt.subplots(figsize=(9, 9)) sn.reset_defaults() sn.set(style="darkgrid") - sn_plot_count_category_test = sn.countplot(y='category', data=df_count_test_labels, palette="deep", ax=ax4, order=u_labels) + sn_plot_count_category_test = sn.countplot(y='category', data=df_count_test_labels, palette="deep", ax=ax4, + order=u_labels) sn_plot_count_category_test.set(title="Fréquence des catégories de questions\ndans l'échantillon de test", xlabel="Fréquence", ylabel="Catégorie")