Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions _quarto-en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ project:
- content/modelisation/index.qmd
- content/modelisation/0_preprocessing.qmd
- content/modelisation/1_modelevaluation.qmd
- content/modelisation/2_classification.qmd
- content/modelisation/3_regression.qmd


website:
Expand Down
228 changes: 189 additions & 39 deletions content/modelisation/2_classification.qmd
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
---
title: "Découverte de la classification avec la technique des SVM"
title-en: "Discovering classification with the SVM technique"
categories:
- Modélisation
description: |
La classification permet d'attribuer une classe d'appartenance (_label_
dans la terminologie du _machine learning_)
discrète à des données à partir de certaines variables explicatives
(_features_ dans la même terminologie).
Les algorithmes de classification sont nombreux. L'un des plus intuitifs et
les plus fréquemment rencontrés sont les _SVM_ (*Support Vector Machine*).
Ce chapitre illustre les enjeux de la classification à partir de
ce modèle sur les données de vote aux élections présidentielles US de 2020.
La classification permet d'attribuer une classe d'appartenance (_label_ dans la terminologie du _machine learning_) discrète à des données à partir de certaines variables explicatives (_features_ dans la même terminologie). Les algorithmes de classification sont nombreux. L'un des plus intuitifs et les plus fréquemment rencontrés sont les _SVM_ (*Support Vector Machine*). Ce chapitre illustre les enjeux de la classification à partir de ce modèle sur les données de vote aux élections présidentielles US de 2020.
description-en: |
Classification enables us to assign a discrete membership class (_label_ in machine learning terminology) to data, based on certain explanatory variables (_features_ in the same terminology). Classification algorithms are numerous. One of the most intuitive and frequently encountered is _SVM_ (*Support Vector Machine*). This chapter illustrates the challenges of using this model to classify model on voting data for the 2020 US presidential elections.
echo: false
---

Expand All @@ -19,21 +15,31 @@ echo: false
printMessage="true"
>}}

::: {.content-visible when-profile="fr"}
# Introduction

Ce chapitre vise à présenter de manière très succincte le principe de l'entraînement de modèles dans un cadre de classification. L'objectif est d'illustrer la démarche à partir d'un algorithme dont le principe est assez intuitif. Il s'agit d'illustrer quelques uns des concepts évoqués dans les chapitres précédents, notamment ceux relatifs à l'entraînement d'un modèle. D'autres cours de votre scolarité vous permettront de découvrir d'autres algorithmes de classification et les limites de chaque technique.

Ce chapitre vise à présenter de manière très succincte le principe de l'entraînement de modèles dans un cadre de classification. L'objectif est d'illustrer la démarche à partir d'un algorithme dont le principe est assez intuitif. Il s'agit d'illustrer quelques uns des concepts évoqués dans les chapitres précédents, notamment ceux relatifs à l'entraînement d'un modèle. D'autres cours de votre scolarité vous permettront de découvrir d'autres algorithmes de classification et les limites de chaque technique.

## Données
:::

::: {.content-visible when-profile="en"}
# Introduction

This chapter aims to very briefly introduce the principle of training models in a classification context. The goal is to illustrate the process using an algorithm with an intuitive principle. It seeks to demonstrate some of the concepts discussed in previous chapters, particularly those related to model training. Other courses in your curriculum will allow you to explore additional classification algorithms and the limitations of each technique.

## Data
:::


{{< include _import_data_ml.qmd >}}

:::: {.content-visible when-profile="fr"}
## La méthode des _SVM_ (_Support Vector Machines_)

Les SVM (_Support Vector Machines_) font partie de la boîte à outil traditionnelle des _data scientists_.
Le principe de cette technique est relativement intuitif grâce à son interprétation géométrique.
Il s'agit de trouver une droite, avec des marges (les supports) qui discrimine au mieux le nuage de point de nos données.
Il s'agit de trouver une droite, avec des marges (les supports) qui discrimine au mieux le nuage de points de nos données.
Bien-sûr, dans la vraie vie, il est rare d'avoir des nuages de points bien ordonnés pour pouvoir les séparer par une droite. Mais une projection adéquate (un noyau ou _kernel_) peut arranger des données pour permettre de discriminer les données.

```{=html}
Expand Down Expand Up @@ -61,12 +67,52 @@ des _labels_ homogènes.
On peut, sans perdre de généralité,
supposer que le problème consiste à supposer l'existence d'une loi de probabilité $\mathbb{P}(x,y)$ ($\mathbb{P} \to \{-1,1\}$) qui est inconnue. Le problème de discrimination
vise à construire un estimateur de la fonction de décision idéale qui minimise la probabilité d'erreur. Autrement dit
:::

::::

:::: {.content-visible when-profile="en"}
## The SVM Method (_Support Vector Machines_)

SVM (_Support Vector Machines_) is part of the traditional toolkit for _data scientists_.
The principle of this technique is relatively intuitive thanks to its geometric interpretation.
The goal is to find a line, with margins (supports), that best separates the point cloud in our data.
Of course, in real life, it is rare to have well-organized point clouds that can be separated by a line. However, an appropriate projection (a kernel) can transform the data to enable separation.

```{=html}
<img src="https://scikit-learn.org/stable/_images/sphx_glr_plot_iris_svc_001.png" alt="Iris SVC Plot" width="300px">
```

::: {.tip}
## Mathematical formalization

SVM is one of the most intuitive _machine learning_ methods
due to its simple geometric interpretation. It is also
one of the least complex _machine learning_ algorithms in terms of formalization
for practitioners familiar with traditional statistics. This note provides an overview, though it is not essential for understanding this chapter.
In _machine learning_, more than the mathematical details, the key is to build intuitions.

The goal of SVM, let us recall, is to find a hyperplane that
best separates the different classes. For example, in a two-dimensional space,
it aims to find a line with margins that best divides the space into regions
with homogeneous _labels_.

Without loss of generality, we can assume the problem involves a probability distribution $\mathbb{P}(x,y)$ ($\mathbb{P} \to \{-1,1\}$) that is unknown. The goal of classification is to build an estimator of the ideal decision function that minimizes the probability of error. In other words
:::


$$
\theta = \arg\min_\Theta \mathbb{P}(h_\theta(X) \neq y |x)
$$

::: {.content-visible when-profile="fr"}
Les SVM les plus simples sont les SVM linéaires. Dans ce cas, on suppose qu'il existe un séparateur linéaire qui permet d'associer chaque classe à son signe:
:::

::: {.content-visible when-profile="en"}
The simplest SVMs are linear SVMs. In this case, it is assumed that a linear separator exists that can assign each class based on its sign:
:::


$$
h_\theta(x) = \text{signe}(f_\theta(x)) ; \text{ avec } f_\theta(x) = \theta^T x + b
Expand All @@ -77,20 +123,34 @@ avec $\theta \in \mathbb{R}^p$ et $w \in \mathbb{R}$.
<img src="https://upload.wikimedia.org/wikipedia/commons/7/72/SVM_margin.png" alt="Les SVM dans le cas linéaire" style="width: 40%;">
```

Lorsque des observations sont linéairement séparables,
il existe une infinité de frontières de décision linéaire séparant les deux classes. Le _"meilleur"_ choix est de prendre la marge maximale permettant de séparer les données. La distance entre les deux marges est $\frac{2}{||\theta||}$. Donc maximiser cette distance entre deux hyperplans revient à minimiser $||\theta||^2$ sous la contrainte $y_i(\theta^Tx_i + b) \geq 1$.
::: {.content-visible when-profile="fr"}
Lorsque des observations sont linéairement séparables, il existe une infinité de frontières de décision linéaire séparant les deux classes. Le _"meilleur"_ choix est de prendre la marge maximale permettant de séparer les données. La distance entre les deux marges est $\frac{2}{||\theta||}$. Donc maximiser cette distance entre deux hyperplans revient à minimiser $||\theta||^2$ sous la contrainte $y_i(\theta^Tx_i + b) \geq 1$.

Dans le cas non linéairement séparable, la *hinge loss* $\max\big(0,y_i(\theta^Tx_i + b)\big)$ permet de linéariser la fonction de perte, ce qui donne le programme d'optimisation suivant :
:::

::: {.content-visible when-profile="en"}
When observations are linearly separable, there is an infinite number of linear decision boundaries separating the two classes. The _"best"_ choice is to select the maximum margin that separates the data. The distance between the two margins is $\frac{2}{||\theta||}$. Thus, maximizing this distance between two hyperplanes is equivalent to minimizing $||\theta||^2$ under the constraint $y_i(\theta^Tx_i + b) \geq 1$.

In the non-linearly separable case, the *hinge loss* $\max\big(0,y_i(\theta^Tx_i + b)\big)$ allows for linearizing the loss function, resulting in the following optimization problem:
:::

$$
\frac{1}{n} \sum_{i=1}^n \max\big(0,y_i(\theta^Tx_i + b)\big) + \lambda ||\theta||^2
$$

::: {.content-visible when-profile="fr"}
La généralisation au cas non linéaire implique d'introduire des noyaux transformant l'espace de coordonnées des observations.
:::

::: {.content-visible when-profile="en"}
Generalization to the non-linear case involves introducing kernels that transform the coordinate space of the observations.
:::

::::


::: {.content-visible when-profile="fr"}
# Application

Pour appliquer un modèle de classification, il nous faut
Expand All @@ -101,6 +161,16 @@ défaite d'un des partis.
Même si les Républicains ont perdu en 2020, ils l'ont emporté
dans plus de comtés (moins peuplés). Nous allons considérer
que la victoire des Républicains est notre _label_ 1 et la défaite _0_.
:::

::: {.content-visible when-profile="en"}
# Application

To apply a classification model, we need to find a dichotomous variable. The natural choice is to use the dichotomous variable of a party's victory or defeat.

Even though the Republicans lost in 2020, they won in more counties (less populated ones). We will consider a Republican victory as our _label_ 1 and a defeat as _0_.
:::


```{python}
#| echo: true
Expand All @@ -111,7 +181,10 @@ from sklearn.model_selection import cross_val_score
import matplotlib.pyplot as plt
```

::: {.exercise}

::: {.content-visible when-profile="fr"}

:::: {.exercise}
## Exercice 1 : Premier algorithme de classification

1. Créer une variable *dummy* appelée `y` dont la valeur vaut 1 quand les républicains l'emportent.
Expand All @@ -133,7 +206,48 @@ créer des échantillons de test (20 % des observations) et d'estimation (80 %)
6. Changer de variables *x*. Utiliser uniquement le résultat passé du vote démocrate (année 2016) et le revenu. Les variables en question sont `share_2016_republican` et `Median_Household_Income_2019`. Regarder les résultats, notamment la matrice de confusion.

7. [OPTIONNEL] Faire une 5-fold validation croisée pour déterminer le paramètre *C* idéal.
::::

:::


:::: {.content-visible when-profile="en"}
::: {.exercise}

## Exercise 1: First classification algorithm

1. Create a *dummy* variable called `y` with a value of 1 when the Republicans win.
2. Using the ready-to-use function `train_test_split` from the `sklearn.model_selection` library,
create test samples (20% of the observations) and training samples (80%) with the following *features*:

```python
vars = [
"Unemployment_rate_2019", "Median_Household_Income_2019",
"Percent of adults with less than a high school diploma, 2015-19",
"Percent of adults with a bachelor's degree or higher, 2015-19"
]
```

and use the variable `y` as the *label*.

*Note: You may encounter the following warning:*

> A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel()

*Note: To avoid this warning every time you train your model, you can use `DataFrame[['y']].values.ravel()` instead of `DataFrame[['y']]` when preparing your samples.*

3. Train an SVM classifier with a regularization parameter `C = 1`. Examine the following performance metrics: `accuracy`, `f1`, `recall`, and `precision`.

4. Check the confusion matrix: despite seemingly reasonable scores, you should notice a significant issue.

5. Repeat the previous steps using normalized variables. Are the results different?

6. Change the *x* variables. Use only the previous Democratic vote result (2016) and income. The variables in question are `share_2016_republican` and `Median_Household_Income_2019`. Examine the results, particularly the confusion matrix.

7. [OPTIONAL] Perform 5-fold cross-validation to determine the ideal *C* parameter.

:::
::::


```{python}
Expand All @@ -144,7 +258,11 @@ votes['y'] = (votes['votes_gop'] > votes['votes_dem']).astype(int)

```{python}
#2. Création des échantillons d'entraînement et de validation
xvars = ['Unemployment_rate_2019', 'Median_Household_Income_2019', 'Percent of adults with less than a high school diploma, 2015-19', "Percent of adults with a bachelor's degree or higher, 2015-19"]
xvars = [
'Unemployment_rate_2019', 'Median_Household_Income_2019',
'Percent of adults with less than a high school diploma, 2015-19',
"Percent of adults with a bachelor's degree or higher, 2015-19"
]

df = votes.loc[:, ["y"] + xvars]

Expand All @@ -154,13 +272,25 @@ X_train, X_test, y_train, y_test = train_test_split(
)
```

::: {.content-visible when-profile="fr"}
On obtient donc un ensemble de _features_ d'entraînement ayant cette forme:
:::

::: {.content-visible when-profile="en"}
We thus obtain a set of training _features_ with the following structure:
:::

```{python}
X_train.head()
```

::: {.content-visible when-profile="fr"}
Et les _labels_ associés sont les suivants:
:::

::: {.content-visible when-profile="en"}
And the associated _labels_ are as follows:
:::

```{python}
y_test
Expand All @@ -179,11 +309,22 @@ sc_precision = sklearn.metrics.precision_score(y_pred, y_test)
```

```{python}
stats_perf = pd.DataFrame.from_dict({"Accuracy": [sc_accuracy], "Recall": [sc_recall],
"Precision": [sc_precision], "F1": [sc_f1]}, orient = "index", columns = ["Score"])
stats_perf = pd.DataFrame.from_dict(
{
"Accuracy": [sc_accuracy], "Recall": [sc_recall],
"Precision": [sc_precision], "F1": [sc_f1]
}, orient = "index", columns = ["Score"]
)
```

A l'issue de la question 3, notre classifieur manque totalement les labels 0, qui sont minoritaires. Parmi les raisons possibles: l'échelle des variables. Le revenu, notamment, a une distribution qui peut écraser celle des autres variables, dans un modèle linéaire. Il faut donc, a minima, standardiser les variables, ce qui est l'objet de la question 4

::: {.content-visible when-profile="fr"}
A l'issue de la question 3, notre classifieur manque totalement les labels 0, qui sont minoritaires. Parmi les raisons possibles : l'échelle des variables. Le revenu, notamment, a une distribution qui peut écraser celle des autres variables, dans un modèle linéaire. Il faut donc, a minima, standardiser les variables, ce qui est l'objet de la question 4.
:::

::: {.content-visible when-profile="en"}
At the end of question 3, our classifier completely misses the 0 labels, which are in the minority. One possible reason is the scale of the variables. Income, in particular, has a distribution that can dominate the others in a linear model. Therefore, at a minimum, it is necessary to standardize the variables, which is the focus of question 4.
:::



Expand All @@ -201,15 +342,21 @@ disp.plot()
plt.show()
```

Standardiser les variables n'apporte finalement pas de gain:
::: {.content-visible when-profile="fr"}
Standardiser les variables n'apporte finalement pas de gain :
:::

::: {.content-visible when-profile="en"}
Standardizing the variables ultimately does not bring any improvement:
:::

```{python}
import sklearn.preprocessing as preprocessing

X = df.loc[:, xvars]
y = df[['y']]
scaler = preprocessing.StandardScaler().fit(X) #Ici on estime
X = scaler.transform(X) #Ici on standardise
scaler = preprocessing.StandardScaler().fit(X)
X = scaler.transform(X)

X_train, X_test, y_train, y_test = train_test_split(
X,
Expand All @@ -220,30 +367,39 @@ clf = svm.SVC(kernel='linear', C=1).fit(X_train, y_train)
predictions = clf.predict(X_test)
cm = sklearn.metrics.confusion_matrix(y_test, predictions, labels=clf.classes_)
disp = sklearn.metrics.ConfusionMatrixDisplay(
confusion_matrix=cm,
display_labels=clf.classes_
)
confusion_matrix=cm,
display_labels=clf.classes_
)
disp.plot()
plt.show()
```

Il faut donc aller plus loin : le problème ne vient pas de l'échelle mais du choix des variables. C'est pour cette raison que l'étape de sélection de variable est cruciale et qu'un chapitre y est consacré.
::: {.content-visible when-profile="fr"}
Il faut donc aller plus loin : le problème ne vient pas de l'échelle mais du choix des variables. C'est pour cette raison que l'étape de sélection de variables est cruciale et qu'un chapitre y est consacré.

A l'issue de la question 6,
le nouveau classifieur avec devrait avoir les performances suivantes :
À l'issue de la question 6, le nouveau classifieur devrait avoir les performances suivantes :
:::

```{python}
#| output: asis
::: {.content-visible when-profile="en"}
It is therefore necessary to go further: the problem does not lie in the scale but in the choice of variables. This is why the step of variable selection is crucial and why a chapter is dedicated to it.

out = pd.DataFrame.from_dict({"Accuracy": [sc_accuracy], "Recall": [sc_recall],
"Precision": [sc_precision], "F1": [sc_f1]}, orient = "index", columns = ["Score"])
```
At the end of question 6, the new classifier should have the following performance:
:::


```{python}
#| output: asis

out = pd.DataFrame.from_dict(
{
"Accuracy": [sc_accuracy], "Recall": [sc_recall],
"Precision": [sc_precision], "F1": [sc_f1]
}, orient = "index", columns = ["Score"]
)
```

```{python}
# 6. Refaire les questions en changeant la variable X.
# Question 6
votes['y'] = (votes['votes_gop'] > votes['votes_dem']).astype(int)
df = votes[["y", "share_2016_republican", 'Median_Household_Income_2019']]
tempdf = df.dropna(how = "any")
Expand All @@ -266,19 +422,13 @@ sc_f1 = sklearn.metrics.f1_score(y_pred, y_test)
sc_recall = sklearn.metrics.recall_score(y_pred, y_test)
sc_precision = sklearn.metrics.precision_score(y_pred, y_test)

#print(sc_accuracy)
#print(sc_f1)
#print(sc_recall)
#print(sc_precision)

predictions = clf.predict(X_test)
cm = sklearn.metrics.confusion_matrix(y_test, predictions, labels=clf.classes_)
disp = sklearn.metrics.ConfusionMatrixDisplay(
confusion_matrix=cm,
display_labels=clf.classes_
)
disp.plot()
# On obtient un résultat beaucoup plus cohérent.

plt.savefig("confusion_matrix3.png")
```
Expand Down
Loading
Loading