La regression linéaire est une méthode statistique pour modéliser la relation entre une variable `y` appelée observation cible et une liste de variables `x_1,x_2,...,x_n` appelée observation. L'équation du modèle est une combinaison linéaire avec un terme constant `a_0` :
`y = a_0"+"a_1x_1"+"a_2x_2"+"..."+"a_nx_n - epsilon`
`hat y = a_0"+"a_1x_1"+"a_2x_2"+"..."+"a_nx_n`
En notation matricielle, cela peut s'écrire comme suit, où on ajoute à toutes les observations la composante `x_0"="1` :
`Y = XA+E`
`hatY = XA`
`X= ((1,x_(1,1),x_(1,2),...,x_(1,n)),(1, x_(2,1),x_(2,2),...,x_(2,n)),(1,⫶,⫶,⋱,⫶),(1, x_(m,1),x_(m,2),...,x_(m,n)))` `A=((a_0),(a_1),(a_2),(⫶),(a_n))` `Y=((y_1),(y_2),(⫶),(y_m))` `hat Y=((hat y_1),(hat y_2),(⫶),(hat y_m))` `E=hatY - Y`
`Y` est le vecteur des observations cibles,
`X` est la matrice des observations avec comme première colone la constante `1`,
`A` est le vecteur des paramètres,
`hatY` est le vecteur des prédictions,
`E` est le vecteur des erreurs de prédictions.
Comme on a ajouter la composante `x_0=1`, l'indices `j` commence à zéro, `j in {0,1,2,3,...,n}`
La somme `sum_j` signifie `sum_(j=0)^(j=n)`
La somme `sum_i` signifie `sum_(i=1)^(i=N)`
L'estimation des paramètres `a_0, a_1, a_2 ,..., a_n` dans la regression linéaire se fait souvent par la méthode des moindres carrés ordinaires (MCO), qui consiste à minimiser la somme des carrés des erreurs noté `J` :
`J = sum_i(haty_i - y_i)^2 = sum_i(a_0x_(i,0)+a_1x_(i,1)+a_2x_(i,2)+...+a_nx_(i,n)-y_i)^2`
`XA = hatY`
`((1,x_(1,1),x_(1,2),...,x_(1,n)),(1, x_(2,1),x_(2,2),...,x_(2,n)),(1,⫶,⫶,⋱,⫶),(1, x_(m,1),x_(m,2),...,x_(m,n)))((a_0),(a_1),(a_2),(⫶),(a_n)) = ( (a_0+a_1x_(1,1)+a_2x_(1,2)+...+a_nx_(i,n)),(a_0+a_1x_(2,1)+a_2x_(2,2)+...+a_nx_(2,n)),(⫶),(a_0+a_1x_(N,1)+a_2x_(N,2)+...+a_nx_(N,n)) ) = ((hat y_1),(hat y_2),(⫶),(hat y_N))`
La valeur de `J` se met sous forme de composition matricielle, où `"·"` désigne l'opération de produit scalaire et où l'exposant `""^t` désigne l'opération de transposition :
`J = (XA-Y)"·"(XA-Y)`
`J = (XA-Y)^t(XA-Y)`
La solution au problème de minimisation est obtenue en dérivant la fonction de coût par rapport au vecteur `A` puis en résolvant pour zéro :
`(delJ)/(delA) = (del(XA-Y))/(delA) = vec0`
`(delJ)/(dela_0) = (del(XA-Y))/(delA) = vec0`
Le calcul matriciel sait développer cette équation en :
`X^tXA=X^tY`
La matrice des produits croisées `X^tX` qui est une matrice carré `(n"+"1)"×"(n"+"1)` symétrique, et le vecteur `X^tY` de dimension `n"+"1`, jouent un rôle centrale :
`X^tX = (sum_i x_(i,j) x_(i,k))_j^k = sum_i ((1,x_(i,1),x_(i,2),x_(i,3),...,x_(i,n)),(x_(i,1),x_(i,1)^2, x_(i,1)x_(i,2), x_(i,1)x_(i,3),...,x_(i,1)x_(i,n)),(x_(i,2),x_(i,2)x_(i,1), x_(i,2)^2, x_(i,2)x_(i,3),..., x_(i,2)x_(i,n)), (x_(i,3),x_(i,3)x_(i,1), x_(i,3)x_(i,2), x_(i,3)^2,..., x_(i,3)x_(i,n)),(⫶,⫶,⫶,⫶,⋱,⫶),(x_(i,n), x_(i,n)x_(i,1), x_(i,n)x_(i,2), x_(i,n)x_(i,3),..., x_(i,n)^2))`
`X^tY = (sum_i x_(i,j) y_(i))_j = sum_i ((y_i),(x_(i,1)y_i),(x_(i,2)y_i),(x_(i,3)y_i),(⫶),(x_(i,n)y_i))`
La somme `sum_i` mise à l'extérieur de la matrice ou du vecteur s'applique distinctement sur chaque composante de la matrice ou du vecteur, ce sont là les règles d'addition de matrices et les règles d'addition de vecteurs communément utilisées.
Lorsque `X^tX` est inversible, la solution est :
`A = (X^tX)^-1X^tY`
La méthode de la régression linéaire devient trop consommatrice en temps de calcul lorsque le nombre de variables `n` devient très grand, car elle nécessite d'inverser une matrice `n"×"n`. Si on ne souhait pas procéder à cette inversion, on peut alors utiliser une autre méthode plus légère qu'est la descente de gradient.
L'algorithme de descente de gradient est une méthode d'optimisation itérative pour trouver les minima d'une fonction différentiable. Dans le cas d'un seul paramètre `a` le gradient selon `a`, noté `"grad"` ou `nabla` correspond à la dérivée selon `a` :
`"grad" = nabla = d/(da)`
Etant donné une fonction de coût `J(a)`, c'est une valeur que l'on veut minimiser en faisant évoluer `a`. On précise formellement les notations afin de lever les ambiguités. On déclare que `J` dépend de `a` en notant le neurone `J"←"a`. Après cette déclaration, `J` est une fonction qui s'applique par défaut à l'argument `a` (qui constitue un système de coordonnée par défaut pour la fonction `J`), faisant que si l'on rencontre l'expression `J` dans une équation où l'on attend une valeur réel alors celle-ci est égale à `J(a)`. En résumer `J"="J(a)`.
le gradient de `J` selon `a`, noté `"grad"(J)` ou `"∇"J` ou `(dJ)/(da)`, est utilisé pour mettre à jour le paramètre `a` en le déplaçant dans le sens qui réduit `J`. La mise à jour du paramètre est définie comme suit :
`J←a`
`a := a - eta (dJ)/(da)`
Ce qui correspond à une variation du paramètre `a` noté `deltaa = "-" eta dJ"/"da`. Remarquez que si on choisit une variation `deltaa` telle que multipliée par la pente de `J`, elle comble complètement `J`, c'est à dire telle que `deltaa dJ"/"da = "-"J` et donc où `deltaa = "-"J(dJ"/"da)^-1`, on obtient alors la méthode de Newton, et l'itération devient alors :
`a := a -J ((dJ)/(da))^-1`
Cette méthode cherche les racines `{a "/" J(a)"="0}` et ne convient pas pour chercher les minima de `J`. C'est pourquoi on l'utilise non pas sur `J` mais sur la dérivée `dJ"/"da`, car les racines de `dJ"/"da` correspondent aux minima et maxima selon `a` de `J`. L'itération devient alors :
`a := a -((dJ)/(da))((d^2J)/(da^2))^-1`
Dans le cas générale il y a `n` paramètres `vec a=(a_1,a_2,...,a_n)`. L'algorithme est vectorialisé. Le gradient selon `vec a`, noté `vec"grad"` ou `vec nabla` correspond à l'opération de dérivation suivante :
`vec "grad" = vec nabla = ((del/(dela_1)),(del/(dela_2)),(⫶),(del/(dela_n)))`
Etant donné une fonction de coût `J(a_1,a_2,...,a_n)` que l'on note `J(vec a)`, le gradient de `J` selon `vec a`, noté `vec"grad"(J)` ou `vec∇J` ou `vec((dJ)/(d vec a))`, est utilisé pour mettre à jour le paramètre vectoriel `vec a` en le déplaçant dans la direction qui réduit `J`. La mise à jour du paramètre est définie comme suit :
`J←vec a`
`vec a := vec a - eta vec nabla J`
`((a_1),(a_2),(⫶),(a_n)) := ((a_1),(a_2),(⫶),(a_n)) - eta( ((dJ)/(da_1)), ((dJ)/(da_2)),(⫶),((dJ)/(da_n)) )`
Ce qui s'écrit :
`AAi, a_i := a_i-eta (delJ)/(del a_i)`
`vec a := vac a -eta(delJ)/(del vec a)`
(del(XA-Y))/(delA)
L'évaluation de la performance d'un modèle de regression linéaire est souvent exprimée à l'aide du coefficient de détermination `R` :
`R^2=1- (sum_i (hat y_i - y_i)^2)/(sum_i (bar y - y_i)^2)`
Une variable statistique centrée réduite est la variable transformée par une application affine (combinaison linéaire avec un terme constant), de telle sorte que sa moyenne soit nulle et que son écart type soit égal à un. Si on centre et si on réduit chaque variables préalablement, la régression linéaire n'a plus besoin du teme constant, et la résolution matriciel réduit les erreurs de calcul dû à la manipulation de valeurs d'ordre de grandeur différents.
On note souvent la moyenne de `x` par `bar x`, et l'écart type de `x` par `sigma_x` :
`bar x = 1/N sum_i x_i`
`sigma_x = sqrt(1/Nsum_i (barx-x_i)^2)`
La variable `x` centrée réduite est : `(x-barx)/(sigma_x)`
On propose un exemple en simulant des données. On utilise ChatGPT pour programmer. Il convient préalablement de créer un environement python (voir python), d'activer cet environnement et d'installer les bibliothèques utilisées à l'aide des commandes suivantes (une fois pour toute pour cet environnement) :
pip install math
pip install random
pip install numpy
pip install scikit-learn
Vous copier le programme python dans un fichier prog.py, et pour exécuter le programe, il suffit d'exécuter la commande suivante :
python prog.py
Ou bien, si vous voulez ouvrir l'interpréteur python en lançant le programme, il suffit d'exécuter la commande suivante :
python -i prog.py
Vous :
Programme en python le calcul des `100` premières valeurs (`i"="0` à `99`) des fonctions `f(i),g(i),h(i),c(i)` suivantes que tu mettra dans des listes portant le nom de la fonction : `f(i) = sin(i) + "rand"()`
où `"rand"()` est un nombre à chaque fois au hasard compris entre `0` et `1`
`g(i) = 2"*"sin(i"*"0.3) + "rand"()`
`h(i) = sin(i"*"0.03) + 2"*""rand"()`
`c(i) = 7"*"sin(i)-3*sin(i"*"0.3)+5"*"sin(i"*"0.03)+"rand"()`
ChapGPT :
Voici un programme Python qui calcule les 100 premières valeurs des fonctions demandées et les stocke dans des listes distinctes : import math import random# Initialisation des listes f = [] g = [] h = [] c = []# Boucle de calcul pour i = 0 à 99 for i in range(100): rand_val = random.random() # nombre aléatoire dans [0,1)f_val = math.sin(i) + random.random() g_val = 2 * math.sin(i * 0.3) + random.random() h_val = math.sin(i * 0.03) + 2 * random.random() c_val = (7 * math.sin(i) - 3 * math.sin(i * 0.3) + 5 * math.sin(i * 0.03) + random.random())f.append(f_val) g.append(g_val) h.append(h_val) c.append(c_val)# Exemple d'affichage des 10 premières valeurs print("f[:10] =", f[:10]) print("g[:10] =", g[:10]) print("h[:10] =", h[:10]) print("c[:10] =", c[:10])
Vous :
Programme la régression linéaire pour prédire `c` à partir de `f,g,h`. Utilise pour cela la bibliothèque sklearn. Puis calcule le coefficient de détermination.
ChatGPT :
Parfait
On va utiliser scikit-learn (sklearn.linear_model.LinearRegression) pour entraîner un modèle qui prédit `c(i)` à partir de `f(i),g(i),h(i)`. Ensuite on calcule le coefficient de détermination `R^2`Voici un script complet :
import math import random import numpy as np from sklearn.linear_model import LinearRegression from sklearn.metrics import r2_score# Initialisation des listes f, g, h, c = [], [], [], []# Calcul des données pour i = 0 à 99 for i in range(100): f_val = math.sin(i) + random.random() g_val = 2 * math.sin(i * 0.3) + random.random() h_val = math.sin(i * 0.03) + 2 * random.random() c_val = (7 * math.sin(i) - 3 * math.sin(i * 0.3) + 5 * math.sin(i * 0.03) + random.random()) f.append(f_val) g.append(g_val) h.append(h_val) c.append(c_val)# Conversion en arrays numpy X = np.column_stack([f, g, h]) # variables explicatives y = np.array(c) # variable cible# Création et entraînement du modèle model = LinearRegression() model.fit(X, y)# Prédiction y_pred = model.predict(X)# Coefficient de détermination R^2 r2 = r2_score(y, y_pred)print("Coefficients :", model.coef_) print("Intercept :", model.intercept_) print("Coefficient de détermination R^2 :", r2)
On exécute le programme et cela affiche :
(env1) dmabboux@bb:~/A$ python -i r.py
Coefficients : [ 6.19383379 -1.24577475 0.78220824]
Intercept : 0.14654725339310204
Coefficient de détermination R^2 : 0.8445136460729791
L'intersept est la prédiction de `c(i)` lorsque toutes les composantes sont nulles c'est à dire lorsque `f(i)=g(i)=h(i)=0`. Il correspond au terme affine constant décrit prédédément comme le paramètre d'indice zéro, mais l'informatique démarrant habituelement ses tableaux par l'indice zéro, il est ici nommé différemment comme l'intersept. Le modèle appris est :
`hat c(i) = "Coefficients"[0]"*"f(i)+"Coefficients"[1]*g(i)+"Coefficients"[2]*h(i)+"Intersept"`
ChatGPT peut montrer les données d'entraînement :
ChatGPT peut comparer les prédictions et les valeurs réels :
On calcul les coefficients `vec p` correspondant au minimum de `sum_i(haty_i-y)^2+alpha sum_i |p_i|`
Le lasso peut fonctionner dans les cas où le nombre d'observations est inférieur au nombre de variables (`N < n`). Le lasso permet de sélectionner un sous-ensemble restreint de variables (dépendant du paramètre `alpha`) ayant un réel apport sur l'observation cible.
Reprogramme la régression linéaire pour prédire `c` à partir de `f,g,h`, mais en utilisant la méthode Lasso.
Parfait
On peut remplacer la régression linéaire classique par la régression Lasso de scikit-learn (sklearn.linear_model.Lasso).
Cette méthode ajoute une pénalisation `L_1` qui favorise la sélection de variables (certains coefficients peuvent devenir exactement `0`).Voici le code :
import math
import random
import numpy as np
from sklearn.linear_model import Lasso
from sklearn.metrics import r2_score# --- Génération des données --- f, g, h, c = [], [], [], []for i in range(100): f_val = math.sin(i) + random.random() g_val = 2 * math.sin(i * 0.3) + random.random() h_val = math.sin(i * 0.03) + 2 * random.random() c_val = (7 * math.sin(i) - 3 * math.sin(i * 0.3) + 5 * math.sin(i * 0.03) + random.random()) f.append(f_val) g.append(g_val) h.append(h_val) c.append(c_val)# Conversion en arrays numpy X = np.column_stack([f, g, h]) y = np.array(c)# --- Régression Lasso --- # alpha contrôle la force de régularisation (par défaut ici 0.1) model = Lasso(alpha=0.1) model.fit(X, y)# Prédictions y_pred = model.predict(X)# Coefficient de détermination R^2 r2 = r2_score(y, y_pred)print("Coefficients :", model.coef_) print("Intercept :", model.intercept_) print("Coefficient de détermination R^2 :", r2)Ici :
- alpha=0.1 règle la pénalisation (plus grand = coefficients plus "rabotés").
- coef_ donne les coefficients associés à `f,g,h`
- intercept_ est la constante.
- r2 mesure la qualité de l’ajustement.
On calcul les coefficients `vec p` correspondant au minimum de `sum_i(haty_i-y)^2+alpha sum_i p_i^2`
Le Ridge permet de réduire le risque de surapprentissage
Reprogramme la régression linéaire pour prédire `c` à partir de `f,g,h`, mais en utilisant la méthode Ridge.
Très bien
On peut utiliser la régression Ridge (sklearn.linear_model.Ridge) qui applique une régularisation `L_2`.
Contrairement à Lasso, elle réduit les coefficients mais sans les annuler complètement.Voici le code :
import math
import random
import numpy as np
from sklearn.linear_model import Ridge
from sklearn.metrics import r2_score# --- Génération des données --- f, g, h, c = [], [], [], []for i in range(100): f_val = math.sin(i) + random.random() g_val = 2 * math.sin(i * 0.3) + random.random() h_val = math.sin(i * 0.03) + 2 * random.random() c_val = (7 * math.sin(i) - 3 * math.sin(i * 0.3) + 5 * math.sin(i * 0.03) + random.random()) f.append(f_val) g.append(g_val) h.append(h_val) c.append(c_val)# Conversion en arrays numpy X = np.column_stack([f, g, h]) y = np.array(c)# --- Régression Ridge --- # alpha contrôle la régularisation (plus grand = coefficients plus petits) model = Ridge(alpha=1.0) model.fit(X, y)# Prédictions y_pred = model.predict(X)# Coefficient de détermination R^2 r2 = r2_score(y, y_pred)print("Coefficients :", model.coef_) print("Intercept :", model.intercept_) print("Coefficient de détermination R^2 :", r2)alpha=1.0 est le paramètre de régularisation (tu peux l’augmenter ou le diminuer pour voir l’effet).
L'algorithme de descente de gradient est une méthode d'optimisation itérative pour trouver les minima d'une fonction différentiable. Dans le cas d'un seul paramètre `a` le gradient selon `a`, noté `"grad"` ou `nabla` correspond à la dérivée selon `a` :
`"grad" = nabla = d/(da)`
Etant donné une fonction de coût `J(a)`, c'est une valeur que l'on veut minimiser en faisant évoluer `a`. On précise formellement les notations afin de lever les ambiguités. On déclare que `J` dépend de `a` en notant le neurone `J"←"a`. Après cette déclaration, `J` est une fonction qui s'applique par défaut à l'argument `a` (qui constitue un système de coordonnée par défaut pour la fonction `J`), faisant que si l'on rencontre l'expression `J` dans une équation où l'on attend une valeur réel alors celle-ci est égale à `J(a)`. En résumer `J"="J(a)`.
le gradient de `J` selon `a`, noté `"grad"(J)` ou `"∇"J` ou `(dJ)/(da)`, est utilisé pour mettre à jour le paramètre `a` en le déplaçant dans le sens qui réduit `J`. La mise à jour du paramètre est définie comme suit :
`J←a`
`a := a - eta (dJ)/(da)`
Ce qui correspond à une variation du paramètre `a` noté `deltaa = "-" eta dJ"/"da`. Remarquez que si on choisit une variation `deltaa` telle que multipliée par la pente de `J`, elle comble complètement `J`, c'est à dire telle que `deltaa dJ"/"da = "-"J` et donc où `deltaa = "-"J(dJ"/"da)^-1`, on obtient alors la méthode de Newton, et l'itération devient alors :
`a := a -J ((dJ)/(da))^-1`
Cette méthode cherche les racines `{a "/" J(a)"="0}` et ne convient pas pour chercher les minima de `J`. C'est pourquoi on l'utilise non pas sur `J` mais sur la dérivée `dJ"/"da`, car les racines de `dJ"/"da` correspondent aux minima et maxima selon `a` de `J`. L'itération devient alors :
`a := a -((dJ)/(da))((d^2J)/(da^2))^-1`
Dans le cas générale il y a `n` paramètres `vec a=(a_1,a_2,...,a_n)`. L'algorithme est vectorialisé. Le gradient selon `vec a`, noté `vec"grad"` ou `vec nabla` correspond à l'opération de dérivation suivante :
`vec "grad" = vec nabla = ((del/(dela_1)),(del/(dela_2)),(⫶),(del/(dela_n)))`
Etant donné une fonction de coût `J(a_1,a_2,...,a_n)` que l'on note `J(vec a)`, le gradient de `J` selon `vec a`, noté `vec"grad"(J)` ou `vec∇J` ou `vec((dJ)/(d vec a))`, est utilisé pour mettre à jour le paramètre vectoriel `vec a` en le déplaçant dans la direction qui réduit `J`. La mise à jour du paramètre est définie comme suit :
`J←vec a`
`vec a := vec a - eta vec nabla J`
`((a_1),(a_2),(⫶),(a_n)) := ((a_1),(a_2),(⫶),(a_n)) - eta( ((dJ)/(da_1)), ((dJ)/(da_2)),(⫶),((dJ)/(da_n)) )`
Ce qui s'écrit :
`AAi, a_i := a_i-eta (delJ)/(del a_i)`
On peut utiliser cette méthode pour l'apprentissage automatique. On considère une liste de variables `vec x =(x_1,x_2,x_3,...,x_n)` et une variable cible `y`, et on souhaite prédire `y` en fonction de vec `x`. La fonction de prédiction `F(vecx,vec a)` comprend les paramètres `veca = (a_1,a_2,a_3,...,a_d)` que l'on veut ajuster. L'équation du modèle est
`y = F(vecx,veca) - epsilon`
On utilise un jeux de données d'entrainement de `N` observations. On note les vecteurs `vec x_1, vec x_2, vec x_3,....vecx_n` et `vec y` comprenant toutes les observations :
`vec x_1 = ((x_(1,1)),(x_(2,1)),(⫶),(x_(N,1))), vec x_2 = ((x_(1,2)),(x_(2,2)),(⫶),(x_(N,2))), vec x_3 = ((x_(1,3)),(x_(2,3)),(⫶),(x_(N,3))), vec y = ((y_1),(y_2),(⫶),(y_N))`
Pour chaque observation n°`i`, la prédiction de `y_i` se note `haty_i = F(vecx_i,vec a)`, et l'erreur de prédiction se note `epsilon_i = F(vecx_i,veca)-y_i` Donc pour l'ensemble des observations nous avons :
`vec epsilon = vec haty- vec y`
L'apprentissage automatique supervisé va, à partir du jeu de données, `vec x_1, vec x_2, vec x_3, ..., vecx_n, vec y`, ajuster les paramètres `vec a` pour obtenir un vecteur d'erreur `vec epsilon` minimal. Mais, il faut définir ce qu'on entend par vecteur d'erreur minimal. Cela se fait en définissant une métrique, et on choisie comme métrique la norme classique d'un vecteur. L'apprentissage va ajuster les paramètres `veca =(a_1,a_2,...,a_d)` afin que le vecteur `vec epsilon=(epsilon_1,epsilon_2,...,epsilon_N)` possède une norme minimale, ce qui revient à posséder un carré minimum (méthode des moindres carrés). La fonction de coût est donc :
`J←vec a`
`J = vec epsilon"·"vec epsilon`
`J = vec epsilon^t vec epsilon`
Le calcul du gradient se développe comme suit :
`vec nablaJ = (((delJ)/(dela_1)),((delJ)/(dela_2)),(⫶),((delJ)/(dela_n)))`
`(delJ)/(dela_i)= del `
On considère un exemple comprenant 3 variables `x_1, x_2, x_3` et une variable cible `y`. On note les vecteurs `vec x_1, vec x_2, vec x_3, vec y` comprenant toutes les observations :
`vec x_1 = ((x_(1,1)),(x_(2,1)),(⫶),(x_(N,1))), vec x_2 = ((x_(1,2)),(x_(2,2)),(⫶),(x_(N,2))), vec x_3 = ((x_(1,3)),(x_(2,3)),(⫶),(x_(N,3))), vec y = ((y_1),(y_2),(⫶),(y_N))`