header-includes | output | colorlinks | urlcolor | linkcolor | |||||||
---|---|---|---|---|---|---|---|---|---|---|---|
|
pdf_document |
true |
Blue |
Blue |
\intro{}
Dans ce document, nous approfondirons les techniques de "Régression logistique" et "Naive Bayes" comme outils d'apprentissage supervisés.
Dans le cadre de l'intelligence artificielle et de l'apprentissage supervisé, la compréhension et la classification précise des données revêtent une importance capitale. Parmi les diverses méthodologies existantes, la "Régression Logistique" et "Naive Bayes" se distinguent par leur efficacité et leur applicabilité dans de nombreux contextes. Ce document se propose d'étudier ces deux techniques, en mettant l'accent sur leur mise en œuvre pratique et leur efficacité comparative dans divers scénarios.
En statistiques, la régression logistique s'inscrit dans le cadre des modèles de régression pour les variables binaires.
Bien qu'elle soit quasiment exclusivement utilisée en tant que méthode de classification.
En effet, c'est l'ajout d'un seuil à la probabilité continue donnée par le modèle de régression qui nous permet de l'utiliser pour la classification.
Ce type de modèle vise à expliquer de manière optimale une variable binaire, qui représente la présence ou l'absence d'une caractéristique spécifique, à l'aide d'un ensemble conséquent de données réelles et d'un modèle mathématique.
Autrement dit, il s'agit de relier une variable aléatoire de Bernoulli,
généralement notée
La régression logistique s'appuie sur un classifeur linéaire \cite{ClassifieurLineaire2022}
i.e. un classifieur dont la sortie (pour un vecteur de feature
$$
g(x) = f(\scalproduct{w}{x} + b)
$$
où
Par exemple, dans le cas de la regression logistique binaire, on suppose le modèle suivant:
$$
y_i \sim Bernoulli(p_i),\quad p_i = \sigma(\scalproduct{\w}{\x_i} + b),\quad \sigma(z) = \frac{1}{1 + e^{-z}}
$$
où
Cependant, dans notre dataset (voir \href{#choix-du-dataset-outils-utilisuxe9s}{section 2.0}) nous avons 3 classes (3 espèces d'iris),
La sigmoide étant continue, nous avons testé 2 méthodes de prédiction:
- La première consistait simplement à modifier la manière dont nous appliquions le seuillage sur la fonction sigmoide, pour distinguer 3 cas au lieu de 2.
i.e. Au lieu de séparer le domaine en 2 (
$\sigma(z) \leq 0.5,\ \sigma(z) > 0.5$ ), nous l'avons séparé en$N$ (ici$N = 3$ ). On a donc que$y_i = k \Leftrightarrow \frac{k}{N} \leq \sigma(z) < \frac{k + 1}{N}$ , ce qui a donné des résultats assez satisfaisants comme nous le verrons en \href{#ruxe9gression-logistique-1}{section 2.2}. - La deuxième consistait évidement en l'application non pas de la régression logistique binaire, mais de la régression logistique multinomiale, fonctionnant avec plusieurs labels. Le principe de la régression logistique multinomiale est simplement de faire plusieurs régressions logistiques binaires. On possède donc un vecteur de poids et un biais par label, et on calcule à chaque fois la probabilité que l'élément appartienne à une certaine classe. La prédiction retournera la classe pour laquelle la probabilité que l'élément appartienne à la classe est la plus élevée.
"Naive Bayes" se présente comme une méthode de classification probabiliste basée sur le théorème de Bayes,
caractérisée par l'adoption d'une hypothèse d'indépendance forte entre les features (attributs), qualifiée de "naïve".
Plus simplement, le classifieur est considéré comme "naïf" car il part du principe que chaque feature (attribut) est indépendante des autres et a un poid égal quant à la probabilité qu'un point appartienne à une classe.
Ce modèle est dit génératif contrairement à la régression logistique, étant considéré comme "méthode discriminante" \cite{ClassifieurLineaire2022}, et consiste à modéliser les probabilités conditionnelles
En d'autres termes, le problème revient à trouver, pour des attributs
\newpage
Pour la suite de ce projet les outils suivants ont été utilisés dans chaque parties:
Le package ucmilrepo
a été utilisé pour charger les données de notre dataset depuis la base de donnée du UC Irvine Machine Learning Repository.
Le dataset que nous avons choisi est le fameux dataset "Iris" \cite{r.a.fisherIris1936}, un des plus anciens et connus dataset de classification. Il contient 150 observations de 3 espèces différentes d'iris (Iris setosa, Iris virginica et Iris versicolor) avec
Voici un aperçu des points-clés du dataset:
\begin{figure} \centering \includegraphics[width=0.8\textwidth,height=\textheight]{../res/iris_img.png} \includegraphics[width=0.8\textwidth,height=\textheight]{../res/iris_table.png} \caption{Iris descriptive table} \end{figure}
Le label que nous allons prédire sera donc class, i.e. l'espèce de l'iris.
\newpage
Dans cette section, une implémentation de la "descente en gradient" a été réalisée. La fonction a la signature suivante
\begin{lstlisting}
def gradient_descent(df, params: NDArray, alpha: float, num_iters: int) -> NDArray:
\end{lstlisting}
Elle calcule de manière itérative le(s) paramètre(s) \code{params} qui minimisent la fonction dont df
est le gradient avec un "taux de convergence" \code{alpha}.
La fonction a été testé avec la fonction \code{scipy.optimize.fmin} \cite{ScipyOptimizeFmin} de la librairie scipy
sur la fonction suivante:
$$
f(x) = x * \cos(\pi (x + 1))
$$
avec différents NDArray
avec D=0).
Les minimas locaux trouvés par les deux fonctions sont les suivants:
Ce résultat illustre bien 2 choses: la première est que l'implémentation de la descente en gradient fonctionne correctement puisque chaque points trouvé par notre fonction est confondu avec celui trouvé par la fonction de scipy (c'est ce qui donne cette teinte "grise"). La deuxième est que la "qualité" du minima local (i.e. la distance avec le minima globale) dépend fortement de la valeur initiale et ce pour les deux fonctions.
\newpage{}
Reprennons le modèle décrit dans la section 1.1.
Nous avons donc:
Notre but est de calculer
La densité de probabilité de cette fonction peut donc s'exprimer comme
Notre but est donc de maximiser cette fonction. Cependant, nous préférons une fonction à minimiser plutôt qu'à maximiser, car la descente en gradient permet de trouver un minimum et non pas un maximum...
Une solution habituelle est donc d'inverser la fonction, transformant ainsi le problème de maximisation en problème de minimisation, et de prendre le logarithme de l'inverse de cette fonction afin d'éviter des valeurs extrêmes lors de notre minimisation.
L'application de la fonction logarithme sur l'inverse de la fonction est correcte car la fonction logarithme est strictement croissante, donc elle n'aura pas d'impact sur la convexité de la fonction.
Cette solution est communément appelée Negative Logarithm Likelihood
.
Donc on cherchera à minimiser la fonction:
Comme nous voulons utiliser la descente en gradient, nous devons trouver la dérivée de la fonction à minimiser, donc de notre fonction de coût.
Tout d'abord, remarquons que nous pouvons écrire:
De plus, voici ce que nous donne la dérivée de la fonction sigmoide:
Donc nous pouvons facilement calculer la dérivée par rapport au poid
Voici ce que nous donne la dérivée partielle par rapport au poids
Or on a:
Donc:
Voici ce que nous donne la dérivée partielle par rapport au biais
On a:
Donc:
Donc pour
et:
Afin d'entraîner les paramètres de la régression logistique, il faut pouvoir comparer les résultats obtenus par la régression avec les résultats attendus.
On souhaite définir une fonction à minimiser permettant de trouver les paramètres optimaux de la régression logistique.
Notre classification se base sur la fonction sigmoïde
Comme la fonction exponnentielle est toujours positive, on a bien que
La fonction sigmoïde nous donne la probabilité que l'élément donné appartienne à un label.
Autrement dit, la fonction sigmoïde est la fonction de répartition de la régression logistique.
Soit
Pour plus de simplicité, on va considérer que le biais est compris dans les poids: au lieu d'écrire
Pour la suite, on va noter
Notre régression logistique binaire peut donc s'écrire comme:
On désire donc trouver une nouvelle distribution
On veut donc que pour une régression logistique binaire, on ait
On peut remarquer que:
On peut considérer que nous avons un vecteur de poids pour chaque label.
Ainsi, on a $\theta_0 = \begin{bmatrix} w_0 & b_0 \end{bmatrix}$ pour le label 0 et $\theta_1 = \begin{bmatrix} w_0 & b_0 \end{bmatrix}$ pour le label 1.
Comme on a besoin seulement d'un vecteur de poids pour déterminer le label de nouveaux éléments avec leurs caractéristiques, on peut considérer que $\theta_0 = \begin{bmatrix} 0 & \dots & 0 \end{bmatrix}$.
Ainsi, la formule précédente nous donne:
On peut donc généraliser cette formule pour
Cela nous donne:
Comme la fonction exponentielle est toujours positive, on a bien que:
De plus, on a que:
Donc la fonction
Cette fonction est courramment appelée fonction softmax
.
Notre objectif est donc de trouver une fonction de coût pour pouvoir entraîner les paramètres de la régression multinomiale.
On cherche à maximiser la vraisemblance des données.
Donc pour un label
Comme on a plusieurs couples de données
En maximisant cette fonction, on fait en sorte que le paramètre
Afin de pouvoir utiliser un algorithme comme la descente en gradient, il faut non pas maximiser une fonction, mais minimiser une fonction.
Tout d'abord, comme on travaille avec des exponentielles, on a intérêt à prendre un logarithme pour éviter d'avoir à travailler avec de trop grandes valeurs. Cette modification n'aura pas d'impact sur la convexité car la fonction logarithme est une fonction strictement croissante.
Enfin, comme on cherche une fonction à minimiser et non pas à maximiser pour pouvoir utiliser la descente en gradient, on va prendre l'inverse de la fonction.
Cela s'appelle courrament le negative logarithm likelihood
.
Cela nous donne une fonction de coût comme suit:
On peut minimiser cette fonction de coût grâce à une descente en gradient.
On va calculer la dérivée de la fonction de coût.
On a:
Donc:
On a donc:
car
Donc pour
Maintenant, on est prêt pour entraîner notre régression logistique multinomiale !
\newpage
Maitenant que nous avons une fonction de coût permettant de quantifier (en moyenne) à quel point un set de
En effet, lors de l'apprentissage, on va chercher de manière itérative les
Concrètement cela revient à appliquer l'algorithme suivant:
\begin{algorithm}
\caption{gradient descent}\label{alg:grad_desc}
\begin{algorithmic}
\Function {GradientDescent}{$f, \w_{init}, b_0, \alpha, \text{num_iters}$}
\State
En pratique, il est plus simple de passer directement la fonction qui calcule le gradient en argument, que d'essayer de le calculer dynamiquement, c'est pourquoi la signature de notre implémentation prend un df
en argument plutôt que la fonction de coût elle même.
Où le calcul des dérivées partielles a été definit comme ci-dessous (plus de détails de calculs sont présents dans le point 2.2.1).
Soit db
dans l'algorithme ci-dessus se réfère à la moyenne (pour tout i) de ces valeurs (i.e. distance moyenne classes prédites -- "vrai" classes).
On l'obtient donc comme suit: (la somme des dérivées est la dérivée de la somme, linéarité de la dérivée)
De même pour dw
:
\begin{align*}
\nabla_{\mathbf{w}} C & = -\frac{1}{N} \sum_{i = 1}^{N}(x_{ij}(y_i - p_i)){1 \leq j \leq k}
= \frac{1}{N} \sum{i=1}^N(\sigma(z_i) - y_i)\cdot (x_{ij}){1 \leq j\leq k} \
%
& =\frac{1}{N}\sum{i = 1}^N (\sigma(\mathbf{w}^T\mathbf{x_i} + b) - y_i)\ \mathbf{x_i}
\end{align*}
On retrouve ainsi, le calcul effectué dans la fonction \code{grad} de \code{log_reg.py} de signature suivante:
\begin{lstlisting} def grad(X: NDArray, y: NDArray, w: NDArray, b: float) -> tuple: \end{lstlisting}
Etant donné que pour le calcul du gradient il est nécessaire d'avoir un matrice de feature
\begin{lstlisting} def grad_desc_ml(features: NDArray, labels: NDArray, df, w: NDArray, b: float, alpha: float, num_iters: int) -> tuple[NDArray, float]: \end{lstlisting}
Cette fonction se comporte exactement de la même manière que celle décrite en \href{#gradient-descent}{section 2.1}. La seule différence est qu'elle passe features
et labels
comme X
et y
à la fonction df
(dans notre cas df
est toujours la fonction grad
), i.e. on a \code{df(features, labels, w, b)} au lieu de \code{df(params)}.
La régression logistique multinomiale est entraîné de manière similaire à la régression logistique binaire.
Cependant, comme expliqué précédement, on utilise la matrice de poids et biais
Le calcul du gradient (plus de détails dans la section 2.2.2) est effectué par la fonction \code{gradient_cost_function} de \code{softmax.py} de signature suivante:
\begin{lstlisting} def gradient_cost_function(X: np.ndarray, theta: np.ndarray, Y: np.ndarray) -> np.ndarray \end{lstlisting}
La descente en gradient aura de nouveau dû être modifiée pour être fonctionnelle: on a besoin de pouvoir passer les paramètres
\begin{lstlisting} def gradient_descent_softmax(df, X: np.ndarray, Y: np.ndarray, theta: np.ndarray, alpha: float, num_iters: int) -> np.ndarray: \end{lstlisting}
Pour la prédiction, nous avons utilisé la fonction suivante:
\begin{lstlisting} def predict_log_reg(X: NDArray, w: NDArray, b): \end{lstlisting}
qui prend simplement
Pour la prédiction de la régression logistique multinomiale, nous avons utilisé la fonction suivante:
\begin{lstlisting} def predict_log_reg_2(X: NDArray, theta: NDArray): \end{lstlisting}
qui est définie également dans \code{softmax.py}.
Cette fonction applique softmax sur les données d'entrée, puis retourne le label qui contient la valeur maximale obtenue par softmax.
\newpage{}
Suite à l'apprentissage , nous avons obtenu les résultats suivants: \begin{align*} w &= [0.53452349, 0.36463584, 1.16132476, 1.08204578]\ b &= 0.45146791 \end{align*}
N.B.:
L'apprentissage peut être ré-effectué de manière efficiente si besoin et à l'aide du jupyter notebook training_test.ipynb disponible sur la branche gpu-training du repository github. Le code de l'entraînement (uniquement sur cette branche) a été "porté" sur cuda / gpgpu à l'aide de la librairie cupy \cite{NumPySciPyGPU}.
A noter qu'il utilise des fonctions de sklearn alors que nous devions les implémenter nous mêmes, (telles que les metrics f1-score...). Ces fonctions ont bien été implentées mais pour une raison de simplicité, elles n'ont pas été utilisées pour l'entrainement. Le code de cette branche ne fera donc pas partie du rendu mais reste publiquement accessible sur github.
\vspace{0.3cm}
Comme dit en section 1.1, ces paramètres sont, en effet, très satisfaisant,
comme on peut le voir sur l'output de pytest
suivant:
\begin{lstlisting}
src/log_reg.py::test_log_reg_f1score
weights & biases: [0.53452349, 0.36463584, 1.16132476, 1.08204578], 0.45146791
{ 'accuracy': 1.0, 'f1_score': 1.0, 'precision': 1.0, 'recall': 1.0 }
PASSED
src/naive_bayes.py::test_predict_bayes_f1score_all
{ 'accuracy': 0.97, 'f1_score': 0.975, 'precision': 0.976, 'recall': 0.974 }
PASSED
\end{lstlisting}
NB: pour reproduire cette output, lancer \code{make test_model}.
Ce résultat a été obtenu avec une séparation 70/30 de training/test data. Lorsque l'on essaye de changer la portion qui est prise aléatoirement dans chaque catégorie, on obtient un F1-score qui varie entre 0.93 et 1.0 (avec, dans de rares exceptions 0.91 ou 0.89).
De plus, l'on voit que les performances que nous avons obtenus rentrent tout à fait dans le cadre de celles annoncées par le UCI ML Repository:
Ce résultat illustre bien que notre démarche est correcte et que nos 2 modèles sont efficaces, avec un penchant pour la régression logistique qui semble être plus efficace que Naive Bayes.
Suite à l'apprentissage du modèle multinomiale, on obtien les performances suivantes:
python3 main.py
Results:
Found theta (bias are last column and weights are the rest):
[[ 0.32626066 0.83466238 -1.21626121 -0.55348121 0.17005888]
[ 0.20948959 -0.28684921 0.15659967 -0.18787991 0.11458452]
[-0.53575025 -0.54781317 1.05966154 0.74136112 -0.2846434 ]]
Metrics obtained:
{'precision': 1.0, 'recall': 1.0, 'accuracy': 1.0, 'f1_score': 1.0}
On peut être très satisfait de ces résultats.
Pour les obtenir, nous avons fait le choix de ne pas initialiser aléatoirement la matrice learning_rate
de
Afin de reproduire ces résultats, il suffit de décommenter la ligne #softmax.main()
dans \code{main.py}
Ici, sur la matrice theta, nous avons chaque ligne qui représente les poids et biais pour chaque label, avec à chaque fois le dernier élément de la ligne qui est le biais et le reste qui est le poids.
\newpage{}
Dans cette section, une implémentation d'un classifieur linéaire bayesien (naive bayes) a été réalisée.
Dans cette implémentation, étant données que toutes nos features sont continues, nous avons considéré que sepal length, sepal width, petal length et petal width seront représenté comme 4 variables aléatoires
C'est à dire: $$ X_k \sim \mathcal{N}( \mu_k, \sigma_k) \qquad \qquad k \in \iitv{0, 3} $$
Elles peuvent être récupérées à l'aide de la fonction suivante:
\begin{lstlisting} def get_distrib_parameters(features: DataFrame, labels) -> dict[Any, list[tuple[fl, fl]]]: \end{lstlisting}
qui va retourner un dict mappant chaque classe à une liste contenant les paramètres des distributions conditionnelles (normales) des features pour cette classe.
Deux fonctions de prédictions ont été implémenté,
- Prennant un sample et prédisant sa classe
- Une deuxième qui prend tous les samples et applique, en parallèle, la première fonction à chacun d'eux.
Elles ont les signatures suivantes:
\begin{lstlisting} def predict_bayes(x, params_by_class: dict[Any, list[tuple[fl, fl]]]) -> Any: def predict_bayes_all(X: DataFrame, params_by_class: dict[Any, list[tuple[fl, fl]]] | None = None) -> list[Any]: \end{lstlisting}
Comme dit précédemment, pour pouvoir prédire la classe d'un sample, il faut calculer les probabilité conditionnelle
Cela revient à chercher le
Or
$$
P(\x | y) = P(x_1 | y) \prod_{i = 2}^{n}{P(x_i | x_{i-1}, \ldots, x_1, y)}
$$
Avec l'hypothèse que les
Donc
En conclusion:
$$ \tilde{y} = \text{arg}\max_{y \in \mathcal{Y}} \left[\ P(y) \prod_{k = 1}^K{P(x_k | y)}\ \right] $$
(où
Où au début on cherche à maximiser
qui est ce que calculent les fonctions dont on a donné la signature ci-dessus.
Dans cette section, nous allons simplement reprendre ce qui a été fait dit dans la \href{#ruxe9sultats}{section 2.2.4} et remontrer les mêmes tests.
Voici l'output du test pytest
pour les rapports de performances du model bayesien:
\begin{lstlisting}
src/log_reg.py::test_log_reg_f1score
weights & biases: [0.53452349, 0.36463584, 1.16132476, 1.08204578], 0.45146791
{ 'accuracy': 1.0, 'f1_score': 1.0, 'precision': 1.0, 'recall': 1.0 }
PASSED
src/naive_bayes.py::test_predict_bayes_f1score_all
{ 'accuracy': 0.97, 'f1_score': 0.975, 'precision': 0.976, 'recall': 0.974 }
PASSED
\end{lstlisting}
Ce résultat a été obtenu avec une séparation 70/30 de training/test data.
Ces résultats illustrent bien que notre démarche est correcte et que nos 2 modèles sont efficaces, avec un penchant pour la régression logistique qui semble être plus efficace que Naive Bayes.
Cependant, un f1-score de
\newpage{}
Pour chaque classe y, on peut tracer les fonctions de distribution de probabilité pour chaque donnée
Pour la classe Y=0, on obtient le graphe suivant :
On peut voir tout d'abord que pour cette classe, les pics des courbes bleue et rouge sont bien inférieurs aux pics des courbes vert et magenta. Ainsi, on en conclu que les variables
\newpage{}
Pour la classe Y=1, on obtient le graphe suivant :
On peut voir tout d'abord que pour cette classe, les pics des courbes bleus et verte sont bien inférieurs aux pics des courbes rouge et magenta. Ainsi, on en conclu que les variables
\newpage{}
Enfin pour la classe Y=2, on obtient le graphe suivant :
On observe que les pics des courbes rouge et magenta sont presque deux fois plus grand que ceux des courbes bleue et verte, de plus les courbes rouge se chevauchent fortement. Ainsi les variables
Ainsi on peut remarquer que globalement la variable
Dans cette partie, nous allons voir les problèmes de sur-apprentissage du modèle de régression logistique multinomial.
Le phénomène de sur-apprentissage est lorsque le modèle entraîné s'adapte trop bien aux données d'apprentissage, si bien qu'il s'adapte au bruit des données d'apprentissage. Cela a pour conséquences de produire de moins bonnes prédictions pour des données de test. En effet, le modèle est "trompé" par le bruit des données d'apprentissage et prédira ainsi de mauvais résultats.
Dans l'énoncé, on nous propose d'utiliser un volume de données réduit afin de montrer le phénomène de sur-apprentissage.
Nous étions alors perplexe et nous nous posions cette simple question: pourquoi utiliser moins de données pour montrer le phénomène engendré lorsqu'on entraîne trop un modèle ?
À cette question, nous n'avons hélas pas encore trouvé de réponse qui tranche.
En effet, peut-être qu'il faut montrer qu'avec moins de données, le modèle entraîné peut avoir de bonnes performances sur les données d'apprentissage, mais pas sur les données de test.
Mais cela ne serait-il pas plutôt un phénomène de sous-apprentissage ?
Ou alors faut-il essayer d'entrainer un maximum notre paramètre
Tout d'abord, nous avons testé et entrainé les modèles pour un volume de données réduit.
Voici 2 graphes montrant les résultats obtenus avec la régression logistique multinomiale et l'approche naive bayes. Notez que nous avons tenté de faire une courbe approximant les données obtenues afin de mieux visualiser ce qu'il se passe. Cette courbe est obtenue, en faisant un peu de bricolage, par le code suivant:
a, b = np.polyfit(x, log(y), 1)
plt.plot(x, a * log(y) + b)
Ce qui est une sorte de régression linéaire "adaptée" pour une courbe logarithmique.
\newpage
Sur le graphique donné par Naive Bayes, nous pouvons constater que les f1 scores varient beaucoup, aussi bien pour les données d'entrainement que pour les données de test. Cependant, on peut remarquer que pour un volume faible de données, la courbe verte, correspondant au f1 score obtenu avec les données d'entrainement, est légèrement au dessus de la courbe rouge, représentant le f1 score des données de test. Cela signifie que lorsqu'on n'a pas assez de données d'entrainement, le modèle entrainé fera ne fera pas de bonnes prédictions sur les données de test, car celui-ci n'a pas été suffisamment entraîner pour bien ajuster ses paramètres: ses paramètres sont uniquement ajustés pour les données d'entrainement.
Cependant, nous pouvons constater que les f1 scores obtenus par Naive Bayes ne convergent pas... C'est pourquoi, afin de montrer les phénomènes de sur-apprentissage, nous allons utiliser la régression logistique qui possède des f1 scores convergeant, comme nous pouvons l'observer sur le graphique.
Sur le graphique donné par la régression logistique, nous pouvons observer que les f1 scores sont très variés pour un volume faible de données, mais que ceux-ci convergent lorsque le volume de données augmente (On suppose que la même chose se produit avec Naive Bayes, mais qu'on n'a pas assez de données pour le remarquer...). C'est pour cette raison que nous allons préférer utiliser la régression logistique plutôt que Naive Bayes pour montrer les phénomènes de sur-apprentissage.
Nous pouvons également constater que les performances sur les données d'entrainement sont meilleures que les performances sur les données de test. Cela montre un "sur-ajustement" du modèle sur les données d'entrainement. En effet, comme le volume de données est faible, les paramètres de la régression logistique sont bien entraînés pour les données d'entrainement, mais pas pour les données de test.
Cependant, lorsque le volume de données augmente, on voit que les paramètres de la régression logistique s'ajustent correctement, et on a des performances sur les données de test qui deviennent meilleures que les performances sur les données d'entrainement.
Donc plus on a des données, mieux on pourra ajuster notre modèle. Dans notre cas, on ne peut pas tester ce que trop de données peuvent faire, car on n'a hélas pas un stock de données illimité.
Mais nous pouvons nous demander à quel point le bruit peut influencer la performance de notre modèle.
Afin d'éclaircir ce point, nous allons vous épargner la visualisation du graphique obtenu par Naive Bayes car celui-ci est très éparse: aucune donnée ne converge. Mais nous allons vous montrer le graphique obtenu par la régression logistique.
Tout d'abord, pour ajouter du bruit aux données, nous indiquons un pourcentage des données qu'on veut bruiter. Ensuite, nous prenons d'une manière aléatoire le pourcentage donné de données, et pour ces données sélectionnées, nous attribuons un label aléatoire parmi la liste de labels initiaux des données. Cela entraine forcément qu'une donnée qu'on voulait bruiter a repris le même label qu'elle avait, et n'est donc pas bruité. Donc le bruitage est approximatif.
Voici le graphique obtenu pour la régression logistique:
Nous pouvons constater que pour des données pas très bruitées, le modèle possède de bonnes performances. Mais plus le bruit introduit dans les données augmente, plus on obtient des résultats éparpillés et décroissant dans l'ensemble. Enfin, on peut observer pour 80 à 100% des données bruitées que le modèle possède de très mauvaises performances sur les données de test, qui sont moins bonnes que les performances obtenues sur les données d'entrainement. Cela est causé par le bruit introduit dans les données: il y a suffisamment de bruit dans les données pour que le modèle soit entrainné au bruit des données d'apprentissage, ce qui cause de mauvaises performances sur des données de test.
Donc trop de bruit dans les données d'apprentissage d'un modèle peut rendre un modèle avec de mauvaises performances, car trop entrainé au bruit des données d'apprentissages.
Enfin, nous avons tenté de faire varier le nombre d'itérations lors de la descente en gradient de la régression logistique et ajouté du bruit aux données pour que les paramètres de la régression logistique soient perturbés par le bruit des données. Nous avons mis 50% de bruit dans les données d'apprentissage, ce qui nous a donné le graphe suivant:
Les résultats nous plaisent car ils montrent que pour un faible nombre d'itérations, les performances du modèle sont bonnes, mais moins bonnes que pour un nombre moyen d'itérations, et pour un nombre trop grand d'itérations, les performances du modèle sont moins bonnes.
Cela signifie que si la descente en gradient est trop précise, on aura un modèle bien entrainé pour les données d'apprentissage, mais pas pour les données de test, car la performance du modèle sur les données de test diminue lorsqu'on a un nombre d'itérations élevé, tandis que la performance du modèle reste à peu près constante pour les données d'entrainement.
Ainsi, il faut également ajuster la précision de la descente en gradient afin d'obtenir non pas les paramètres optimaux pour les données d'apprentissage, mais pour obtenir des paramètres à la fois généraux et précis, permettant de donner des performances optimales sur des données de test.
Nous pouvons conclure que le sur-apprentissage ou sur-ajustement peut causer des problèmes dans les performances d'un modèle de classification, comme la régression logistique, car le modèle peut soit s'entrainer trop au bruit des données d'apprentissages, entrainant de mauvais résultats de test, soit ne pas avoir suffisamment de données d'apprentissage pour bien ajuster ses paramètres, soit trop s'entrainer aux données d'apprentissage, ce qui entraine une baisse de performance sur les données de tests, car le modèle est également entrainé au bruit des données d'apprentissage.
Enfin, il faut toujours s'assurer que le modèle donne les meilleures performances possibles sur les données de test, car les performances sur les données de test comptent plus que les performances sur les données d'apprentissage...
Pour bruiter les données, une fonction a été créée dans \code{overfitting.py}. Cette fonction possède la signature suivante:
\begin{lstlisting} def add_noise_to_data(labels: np.ndarray, percentage: int) -> np.ndarray: \end{lstlisting}
Elle prend des données aléatoirement et les bruite en réatribuant le label correspondant aux caractéristiques. On peut avoir le label qui est défini d'une manière aléatoire à sa valeur initiale.
Pour prendre seulement un pourçentage des données, une autre fonction a été créée dans \code{overfitting.py}. Cette fonction possède la signature suivante: \begin{lstlisting} def get_percentage_of_data(feat: np.ndarray, labels: np.ndarray, percentage: int) -> np.ndarray: \end{lstlisting}
Cette fonction prend un pourcentage des données qui ont été, au préalable, "mélangées" aléatoirement.
Les graphiques ont été obtenus grâce aux autres fonctions définies dans \code{overfitting.py}.
On peut les exécuter en décommentant les lignes suivantes de la toute fin de \code{overfitting.py} et en décommentant la ligne overfitting.main()
de main:
#overfitting_naive_bayes(FEAT, LABELS, FEAT_test, LABELS_test)
#overfitting_log_reg(FEAT, LABELS, FEAT_test, LABELS_test)
#overfitting(FEAT, LABELS, FEAT_test, LABELS_test)
Le code peut cependant prendre du temps à l'exécution: il prennait environ 2 minutes à tourner sur la machine d'un des étudiants.
\newpage{}
Une fois que les paramètres des classes sont obtenus en supposant l'indépendance des variables, on échantillone de nouvelles données afin de comparer les résultats obtenus avec les données d'origine.
L'échantillonage est fait dans le fichier sampling.py
.
On fait 50 échantillons pour chaque classe, à partir des paramètres des distributions obtenus dans la section précédente.
On obtient les résultats suivants (la moyenne et l'écart-type sont donnés pour chaque classe et chaque variable):
-
Pour la classe 0:
- Mean:
- réelle: 4.964516, 3.3612902, 1.467742, 0.2451613
- échantillon: 5.04191904, 3.33580458, 1.46340112, 0.23831319
- Ecart-type:
- réel: 0.34014544, 0.37654343, 0.18508933, 0.112067565
- échantillon: 0.32847194, 0.35161457, 0.16439592, 0.10696828
- Mean:
-
Pour la classe 1:
- Mean:
- réelle: 5.862162, 2.7243242, 4.2108107, 1.3027027
- échantillon: 5.89406553, 2.70037139, 4.28611674, 1.3473438
- Ecart-type:
- réel: 0.531952, 0.29944894, 0.49597478, 0.20613708
- échantillon: 0.51264886, 0.25024787, 0.40643571, 0.19599569
- Mean:
-
Pour la classe 2:
- Mean:
- réelle: 6.5594597, 2.9864864, 5.545946, 2.0054054
- échantillon: 6.52999239, 3.0324595, 5.57314614 2.00670609
- Ecart-type:
- réel: 0.65889615, 0.31460926, 0.54446435, 0.29715872
- échantillon: 0.63336966, 0.32560175, 0.68418903, 0.30524206
- Mean:
Les nombres et les graphiques montrent que les échantillons sont très proches des données réelles, donc on a bien la vraisemblance.
Notre implémentation de Naïve Bayes a été comparée avec celle de SKLearn dans le fichier sampling.py
, avec un split des données échantillonnées en 70% training et 30% test.
On obtient les résultats suivants:
Notre Naive Bayes
-
Precision: 0.9761904761904763
-
Recall: 0.9743589743589745
-
Accuracy: 0.9777777777777777
-
F1_score: 0.9752738654147106
Sklearn Naive Bayes
-
precision: 0.9761904761904763
-
recall: 0.9743589743589745
-
accuracy: 0.9777777777777777
-
f1_score: 0.9752738654147106
Notre implémentation de Régression Logistique a été comparée avec celle de SKLearn dans le fichier sampling.py
, avec un split des données échantillonnées en 70% training et 30% test.
Pour SKLearn, le modèle utilisé est lr = LogisticRegression(multi_class="multinomial")
, car on a 3 classes et donc il nous faut un modèle multinomial. \cite{sklearnLogReg}
Notre Logistic Regression
-
Precision: 0.8505050505050505
-
Recall: 0.8461538461538461
-
Accuracy: 0.8666666666666667
-
F1_score: 0.848323868840447
Sklearn Logistic Regression
-
Precision: 0.9761904761904763
-
Recall: 0.9743589743589745
-
Accuracy: 0.9777777777777777
-
F1_score: 0.9752738654147106
On a vu qu'on avait la vraisemblance, puisque les échantillons sont très proches des données réelles, comme le montrent les graphiques.
En ce qui concerne SKLearn, on peut voir que les métriques pour Naïve Bayes sont identiques car les 2 implémentations sont simplement une application du théorème de Bayes, donc on espère avoir les mêmes résultats.
Pour le logistic regression, SKLearn fournit de meilleurs métriques car il est certainement plus optimisé que notre implémentation.
Malgré cela, notre implémentation donne quand même des très bons résultats, avec chaque métrique tournant autour de 85%.
On a donc aussi la classification.
On peut donc conclure que les deux implémentations arrivent à bien classifier les données IRIS.
\newpage{}
- Mise en place du repos github
- Mise en place du code
- Mise en place du dataset
- Implémentation de \code{naive_bayes.py} et de \code{log_reg.py}
- Auteur des sections:
- 1
- 2.0
- 2.1
- 2.2.3
- 2.2.4.1
- 2.2.5.1
- 2.3
- Implémentation de \code{sampling.py}
- Auteur de la section 4
- Implémentation de \code{plot_util.py} ?
- Auteur de la section 3 (jusqu'à 3.1)
- Implémentation de \code{metrics.py}
- Implémentation de \code{softmax.py}, de \code{gradient_descent.py} et de \code{overfitting.py}
- Auteur des sections
- 2.2.1
- 2.2.2
- 2.2.4.2
- 2.2.5.2
- 3.1
\newpage
\printbibliography[heading=bibintoc, title={Références}]