développement des métriques pour la question 3 et suite de la rédaction du rapport

This commit is contained in:
François Pelletier 2019-09-28 21:54:59 -04:00
parent 4d2b030782
commit 8c567aa307
3 changed files with 122 additions and 15 deletions

View file

@ -1,6 +1,6 @@
# Travail pratique 1
IFT7022 —Traitement automatique de la langue naturelle
IFT-7022 — Traitement automatique de la langue naturelle
Remis par François Pelletier
@ -79,9 +79,26 @@ Situation 2. `Q : Combien de cœurs a une pieuvre?`
## Origine du nom de famille
Dans cette section, nous allons créer des modèles pour les noms de famille provenant de plusieurs origines. Nous disposons de jeux d'entrainement et de test étiquetés, ce qui nous permettra d'analyser la performance de nos différents modèles d'origine.
Dans cette section, nous allons créer des modèles pour les noms de famille provenant de 18 langues d'origine. Nous disposons de jeux d'entrainement et de test étiquetés, ce qui nous permettra d'analyser la performance de nos différents modèles d'origine.
Pour chaque origine, nous allons normaliser les noms de famille en les mettant en minuscules et les convertissant en encodage ASCII. Nous allons ensuite construire des unigrammes pour chacun des noms de familles. Ceci nous permettra tout d'abord de disposer de listes de lettres au lieu de mots, ce qui facilitera la création des bigrammes et trigrammes. Celà nous permet aussi de mesurer la taille $V$ du vocabulaire, soit le nombre de caractères différents, incluant un caractère virtuel pour représenter la fin de mot.
### Création des k-grammes
Pour convertir les mots en unigrammes, on utilise la fonction `list` qui retourne simplement une liste des caractères d'un mot. Puis, pour créer des k-grammes, on ajoute $k-1$ caractères de début de mot au début de la liste et $k-1$ caractères de fin de mot à la fin de cette liste. Ensuite, pour créer la liste des k-grammes, on en prend les éléments $k$ à la fois et on les joint ensemble dans une seule chaîne de caractères. Soit $U$ la liste des unigrammes, on sélectionne
$$U[i] \ldots U[i+k-1], \forall i \in [0,|U|-k]$$
### Création des modèles
On crée un modèle différent pour chacune des origines. Pour chaque nom présent dans la liste associée à une origine, on applique l'opération de normalisation, puis on extrait les unigrammes dans une liste. On accumule ceux-ci dans une liste de listes.
Sur chacun des mots transformés en unigrammes, on développe les k-grammes pour $k \in [1,4]$. On effectue le compte de tous les k-grammes pour une origine, qu'on stocke dans un dictionnaire, puis on enregistre cette information (qui est notre modèle de langue) dans un dictionnaire de modèles dont la clé est de la forme (origine,$k$). Ceci nous permettra d'accéder aux modèles pour différentes valeurs de $k$ simultanément.
### Origine la plus probable
Pour chaque origine, nous allons normaliser les noms de famille en les mettant en minuscules et les convertissant en encodage ASCII. Nous allons ensuite construire des unigrammes pour chacun des noms de familles. Ceci nous permettra tout d'abord de disposer de listes de lettres au lieu de mots, ce qui facilitera la création des bigrammes et trigrammes. Celà nous permet aussi de mesurer la taille `V` du vocabulaire, soit le nombre de caractères différents, incluant un caractère virtuel pour représenter la fin de mot.
### Unigrammes
@ -89,3 +106,73 @@ Pour chaque origine, nous allons normaliser les noms de famille en les mettant e
### Trigrammes
## Classification de textes
### Description des données
Ce problème consiste à classifier un ensemble de questions en anglais à l'aide d'un ensemble de catégories de réponses. Nous disposons d'un échantillon d'entraînement composé de 5556 questions annotées et d'un échantillon de test composé de 500 questions.
La distribution des catégories de questions dans l'échantillon d'entraînement est présentée dans le tableau suivant:
![](images/3_sn_plot_count_category_train.png)
On peut remarquer immédiatement que ce n'est pas un jeu de données équilibré: il y a trois catégories qui sont relativement sous-représentées: `ABBREVIATION`, `ORGANIZATION` et `TEMPORAL`.
Voici maintenant la distribution pour l'échantillon test
![](images/3_sn_plot_count_category_test.png)
On remarque immédiatement que la distribution de l'échantillon de test diffère énormément de celle de l'échantillon d'entrainement. On doit s'attendre à ce que l'exactitude du modèle soit inférieure pour le test.
### Algorithme de vectorisation
Afin de classifier les questions, nous allons utiliser des vecteurs de fréquences de mots. Comme l'utilisation de mots rares pourrait créer une forme de surentrainement, et de mots trop fréquents, une instabilité des modèles, nous devons trouver où tronquer la distribution de mots. De plus, nous pouvons nous poser la question sur l'inclusion ou non des mot vides.
Pour ce faire, on effectue une recherche sur une grille composée de ces trois paramètres:
- fréquence minimale dans le corpus (1 à 20)
- proportion maximale dans le corpus (0,005 à 0,100)
- indicateur booléen d'exclusion des mots vides.
Pour chacune des combinaisons, une vectorisation est effectuée, puis un modèle de Bayes naïf est entraîné avec une validation croisée, en conservant le paramètre $\alpha=1$. Le score moyen est ensuite conservé. La combinaison qui présente le meilleur score moyen est conservée et est utilisée pour créer un CountVectorizer. Il est important de noter que l'on considère le vocabulaire comme étant fermé au contenu du corpus d'entraînement.
Pour créer ce vocabulaire depuis l'échantillon d'entraînement, on utilise la méthode `fit_transform`. Pour créer les vecteurs de fréquences de l'échantillon de test, il faut éviter de recréer le vocabulaire en utilisant la méthode `transform`.
### Apprentissage
Les données sont maintenant préparées pour entraîner le modèle de Bayes Naïf. Afin de déterminer le paramètre $\alpha$ optimal, nous allons utiliser recherche sur grille et la validation croisée avec 5 sous-échantillons. Comme nous cherchons un paramètre d'un modèle d'apprentissage, nous pouvons effectuer ces étapes à l'aide d'un objet `GridSearchCV` et de sa méthode `fit`. Avec un score de 0.6834, nous conservons la valeur $\alpha=1$.
### Résultats
Le modèle a une exactitude de 0.8362 pour l'échantillon d'entraînement et de 0.6220 pour l'échantillon de test. Nous sommes donc en présence d'une forme de surapprentissage. Considérons aussi que les proportions de chaque étiquettes varient grandement entre les deux échantillons.
Les résultats détaillés sont présentés ci-dessous sous forme de matrices de confusion.
![](images/3_seaborn_df_cm_predict_train.png)
![](images/3_seaborn_df_cm_predict_test.png)
On remarque immédiatement que le modèle ne fais pas la disctinction entre les deux catégories `DEFINITION` et `ENTITY`. Comme ce sont respectivement les catégories les plus fréquentes de l'échantillon d'entraînement et de test respectivement, on explique une bonne partie de la perte de performance.
## Notes
J'ai utilisé quelques librairies additionnelles pour produire les figures présentes dans ce rapport directement depuis les fonctions du problème. Ces librairies sont:
```python
import matplotlib.pyplot as plt
import seaborn as sn
from pandas import DataFrame
```
## Références
- [Documentation de Scikit-Learn](https://scikit-learn.org/stable/documentation.html)
- [Documentation de Seaborn](https://seaborn.pydata.org)
- [Documentation de Python 3.7: The Python Standard Library](https://docs.python.org/3.7/library/)
- Stack Overflow:
- https://stackoverflow.com/a/7716358
- https://stackoverflow.com/a/280156

View file

@ -213,7 +213,7 @@ def evaluate_models(filename, n=3, use_logprob=True):
results = {}
compteur_succes = {}
for org, name_list in test_data.items():
compteur_succes[org] = {}
compteur_succes[org] = {"succes": 0, "echec": 0}
results[org] = {}
for name in name_list:
mp_origin = most_probable_origin(name, n, use_logprob)

View file

@ -14,7 +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)
@ -36,8 +36,10 @@ def custom_count_vectorize_questions(min_df, max_df, use_stop_words):
def train_and_test_classifier(training_fn, test_fn):
questions, labels = load_dataset(training_fn)
# questions, labels = load_dataset(training_questions_fn)
print("Nb questions d'entraînement:", len(questions))
test_questions, test_labels = load_dataset(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.
@ -78,7 +80,7 @@ def train_and_test_classifier(training_fn, test_fn):
mnb_fit = gvc.fit(count_questions_train_opt, labels)
print("Score: " + str(mnb_fit.best_score_))
print("Score: {0:.4f}".format(str(mnb_fit.best_score_)))
print("Paramètres: " + str(mnb_fit.get_params()))
@ -90,26 +92,48 @@ def train_and_test_classifier(training_fn, test_fn):
accuracy_train = accuracy_score(y_true=labels, y_pred=labels_predict_train)
accuracy_test = accuracy_score(y_true=test_labels, y_pred=labels_predict_test)
# Section pour le rapport
# Section pour produire des images pour le rapport
# Décompte des étiquettes du jeu d'entrainement
df_count_labels = DataFrame(labels, columns=['category'])
f, ax3 = plt.subplots(figsize=(9, 9))
sn.set(style="darkgrid")
sn_plot_count_category = sn.countplot(y='category', data=df_count_labels, palette="deep", ax=ax3, order=u_labels)
sn_plot_count_category.set(title="Fréquence des catégories de questions\ndans l'échantillon d'entraînement",
xlabel="Fréquence",
ylabel="Catégorie")
plt.tight_layout()
sn_plot_count_category.get_figure().savefig("images/3_sn_plot_count_category_train.png")
# Décompte des étiquettes du jeu de test
df_count_test_labels = DataFrame(test_labels, columns=['category'])
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.set(title="Fréquence des catégories de questions\ndans l'échantillon de test",
xlabel="Fréquence",
ylabel="Catégorie")
plt.tight_layout()
sn_plot_count_category_test.get_figure().savefig("images/3_sn_plot_count_category_test.png")
# Matrice de confusion pour l'entrainement
cm_predict_train = confusion_matrix(y_true=labels, y_pred=labels_predict_train, labels=u_labels)
df_cm_predict_train = DataFrame(cm_predict_train, index=u_labels, columns=u_labels)
f, ax = plt.subplots(figsize=(9, 9))
plt.subplots_adjust(top=0.961,
bottom=0.169,
left=0.169,
right=0.983,
hspace=0.2,
wspace=0.2)
sn_plot_cm_predict_train = sn.heatmap(df_cm_predict_train,
cmap='Oranges',
annot=True,
fmt="d",
ax=ax)
sn_plot_cm_predict_train.set(title="Performance du modèle NB sur l'échantillon d'entrainement",
xlabel="Prédit",
ylabel="Observé")
@ -118,22 +142,18 @@ def train_and_test_classifier(training_fn, test_fn):
# Matrice de confusion pour le test
cm_predict_test = confusion_matrix(y_true=test_labels, y_pred=labels_predict_test, labels=u_labels)
df_cm_predict_test = DataFrame(cm_predict_test, index=u_labels, columns=u_labels)
f, ax2 = plt.subplots(figsize=(9, 9))
plt.subplots_adjust(top=0.961,
bottom=0.169,
left=0.169,
right=0.983,
hspace=0.2,
wspace=0.2)
sn_plot_cm_predict_test = sn.heatmap(df_cm_predict_test,
cmap='Oranges',
annot=True,
fmt="d",
ax=ax2)
sn_plot_cm_predict_test.set(title="Performance du modèle NB sur l'échantillon test",
xlabel="Prédit",
ylabel="Observé")