Introduction au réseau de neurones : de la simple régression à l'économétrie¶

Le notebook : https://sergelhomme.fr/notebook/Intro_IA_reg_poly.ipynb

Pour s'initier aux réseaux de neurones artificiels, qui sont aujourd'hui au cœur de nombreuses avancées en deep learning et en intelligence artificielle, nous allons utiliser TensorFlow. Plus précisément, nous nous appuierons sur Keras, l'interface (API) de haut niveau intégrée à TensorFlow. On peut voir Keras comme un véritable « panneau de contrôle » permettant de construire, entraîner et évaluer des réseaux de neurones sans avoir à manipuler directement toute la complexité du moteur TensorFlow. En pratique, Keras simplifie considérablement l'écriture des modèles et permet de se concentrer sur les concepts essentiels plutôt que sur les détails techniques de l'implémentation.

In [1]:
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import Input

Régression linéaire simple et réseau de neurones¶

Une régression linéaire simple constitue un exemple classique de modèle d'apprentissage supervisé. Son objectif est d'apprendre une relation entre une ou plusieurs variables explicatives X et une variable à prédire Y. Pour cela, le modèle est entraîné sur un jeu de données d'apprentissage à partir duquel il estime les coefficients de la régression. Une fois ces coefficients déterminés, ils peuvent être utilisés pour prédire de nouvelles valeurs de Y à partir de nouvelles observations X. Le modèle est ensuite évalué sur un jeu de données distinct afin de vérifier sa capacité à généraliser ses prédictions (voir plus bas l'exemple de modélisation des prix immobiliers à Boston). En deep learning, les variables X sont généralement appelées variables d'entrée (explicatives, d'apprentissages, inputs) ou caractéristiques (features). Les variables Y correspondent aux sorties attendues du modèle. En classification, elles sont souvent appelées étiquettes (labels), tandis qu'en régression on parle plus volontiers de cibles (targets) ou de variable à prédire, de variable à expliquer. Les prédictions du modèle sont quant à elles notées Y' ou Y barre.

D'une certaine manière, les coefficients d'une régression jouent un rôle analogue aux poids d'un réseau de neurones : ce sont les paramètres que le modèle apprend à partir des données afin de réaliser des prédictions. Il est donc intéressant de commencer par un cas très simple : un réseau constitué d'un unique neurone. Nous verrons que ce neurone est capable d'apprendre lui aussi les paramètres nécessaires à la prédiction, exactement comme une régression apprend ses coefficients.

Avec un réseau de neurones, les coefficients d'une régression linéaire sont appris (calculés itérativement) par descente de gradient, plutôt qu’obtenus par la résolution analytique des équations (normales) de Legendre-Gauss, qui donnent une solution optimale mais ne sont pas adaptées aux modèles complexes (non linéaires) ou de grandes dimensions (dans ce cas, la descente de gradient est un algorithme heuristique permettant de trouver des solutions satisfaisantes en un temps raisonnable). La descente de gradient n'est pas un algorithme de résolution exacte, mais dans le cas d'une régression linéaire il converge vers (s'approche de) la solution optimale, car la fonction de coût est convexe. Plus de détails sur la descente de gradients ici : https://sergelhomme.fr/notebook/Intro_IA.html

Ainsi, en théorie, une régression linéaire simple est équivalente à l'utilisation d'un réseau de neurones à : 1 neurone ; sans fonction d’activation (plus précisément une fonction d'activation linéaire) ; 1 entrée ; 1 sortie. La solution proposée converge en théorie vers la solution optimale.

In [2]:
# Données (on construit des donnees suivant une régression du type y = 3x + 2)
np.random.seed(0)
X = np.linspace(0, 10, 100).reshape(-1, 1)
y = 3 * X + 2 + np.random.normal(0, 0.5, size=(100, 1))
In [3]:
plt.plot(X, y, 'o', markersize=3)
plt.show()
No description has been provided for this image

Après avoir créé nos données, on crée alors un réseau de neurones à l'aide de la fonction Sequential() de Keras. Sequential() crée un modèle de réseau de neurones simple (couche par couche). La fonction Dense() permet de préciser que tous les neurones sont connectés (ici, il n'y en a qu'un...). Le paramètre unit définit le nombre de neurones, tandis que le paramètre input_shape précise que l’entrée du modèle contient 1 variable (1 feature, x est de dimension 1).

In [4]:
# Modèle Keras : 1 neurone
model = Sequential([Dense(units=1, input_shape=(1,), activation='linear')])
C:\Users\Serge\anaconda3\Lib\site-packages\keras\src\layers\core\dense.py:87: UserWarning: Do not pass an `input_shape`/`input_dim` argument to a layer. When using Sequential models, prefer using an `Input(shape)` object as the first layer in the model instead.
  super().__init__(activity_regularizer=activity_regularizer, **kwargs)

La méthode compile() permet de déterminer la méthode d'optimisation et la métrique permettant d'évaluer la qualité de nos prédictions. Ici, on utilise la méthode de descente de gradient stochastique (c'est la méthode de descente de gradient, mais on utilise seulement une partie des données pour ajuster le fameux gradient, car prendre tout le jeu de données peut être très couteux). MSE c'est ce que l'on cherche à minimiser ici, l'erreur quadratique moyenne comme dans une régression linéaire classique.
La méthode fit() calcule le modèle, l'epoch c'est pour faire simple le nombre d'itération de l'algorithme et l'argument verbose permet de masquer ces epoch.

In [5]:
model.compile(optimizer='sgd', loss='mse')
# Entraînement
model.fit(X, y, epochs=200, verbose=0)
Out[5]:
<keras.src.callbacks.history.History at 0x1aa488b9190>

Le méthode layers[] permet d'avoir accès aux couches, donc aux neurones et get_weights plus précisément aux poids finaux du modèle et le biais associé. Ici ce sont les paramètres de notre régression.

In [6]:
# Récupération des coefficients
weights, bias = model.layers[0].get_weights()
a = weights[0][0]  # pente
b = bias[0]        # intercept
print(f"Pente (a) estimée (proche de 3) : {a:.4f}")
print(f"Intercept (b) estimé (proche de 2) : {b:.4f}")
Pente (a) estimée (proche de 3) : 2.9973
Intercept (b) estimé (proche de 2) : 2.0662

La méthode predict() permet d'obtenir des prédictions en se fondant sur les poids calculés. On peut alors reprendre les valeurs de X pour afficher la droite de régression.

In [7]:
predictions = model.predict( X )
plt.plot(X, y, 'o', markersize=3)
plt.plot(X, predictions)
plt.show()
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 15ms/step
No description has been provided for this image

Un réseau de neurones repose sur un principe relativement simple : il apprend les paramètres de combinaisons linéaires, à la manière d'une régression, puis applique à ces combinaisons des fonctions d'activation non linéaires. C'est l'enchaînement de ces transformations qui lui permet de modéliser des relations beaucoup plus complexes que celles capturées par une régression linéaire classique.

Un réseau de neurones n'est pas une rupture avec la régression ; c'est une généralisation de la logique de régression à des architectures multicouches et non linéaires.

Régression linéaire multiple et réseau de neurones¶

Pour une régression linéaire multiple, un seul neurone avec une entrée de dimension n est toujours suffisant. Ca reste une combinaison linéaire. Pas besoins d'empiler des neurones en fonction du nombre de paramètres de votre régression linéaire, du nombre de variables (features). Attention néanmoins, plus n augmente, plus la convergence de la solution sera difficile.

Utiliser plusieurs neurones n’apporte en théorie rien si : toutes les couches sont linéaires ; la sortie est une combinaison linéaire des entrées. Empiler plusieurs neurones linéaires est équivalent à un seul neurone linéaire. Autrement dit, une succession de transformations linéaires n'est rien d'autre qu'une seule grande transformation linéaire. De plus, plusieurs neurones détruisent le sens direct des coefficients. En gros, vous pourrez toujours aussi bien prédire, mais vous ne pourrez plus récupérer les coefficients de votre régression facilement (ils seront plus difficiles à obtenir, il faudra des traitements statistiques et mathématiques), votre modèle deviendra alors en quelque sorte une boite noire...

In [8]:
# Données (on construit des donnees suivant une régression du type y = 3x1 + 6x2 + 2)
np.random.seed(0)
x1 = np.linspace(0, 10, 100).reshape(-1, 1)
x2 = np.random.rand(100,1) * 10
x2 = x2.reshape(-1,1)
X = np.hstack([x1,x2])
y = 3 * x1 + 6 * x2 + 2 + np.random.normal(0, 0.5, size=(100, 1))

Le code est donc quasiment identique à celui de la régression linéaire simple, car un seul neurone sans activation suffit. Il faut juste changer la dimension de l'entrée, donc ici de notre neurone.

In [7]:
# Modèle Keras : 1 neurone
model = Sequential([Dense(units=1, input_shape=(2,), activation='linear')]) #input_shape passe a 2
model.compile(optimizer='sgd', loss='mse')
model.fit(X, y, epochs=200, verbose=0)
weights, bias = model.layers[0].get_weights()
In [8]:
a = weights[0][0]  # pente 1
b = weights[1][0]  # pente 2
c = bias[0]        # intercept
print(f"Pente (a) estimée (proche de 3) : {a:.4f}")
print(f"Pente (b) estimée (proche de 6) : {b:.4f}")
print(f"Intercept (c) estimé (proche de 2) : {c:.4f}")
Pente (a) estimée (proche de 3) : 3.0141
Pente (b) estimée (proche de 6) : 6.0573
Intercept (c) estimé (proche de 2) : 2.0957

Polynôme et réseau de neurones¶

Pour un polynôme, la problématique devient plus subtile : un polynôme est non linéaire, mais est linéaire en ses coefficients si on connait le modèle du polynôme. Ainsi, si le polynôme est explicitement construit, si on connait sa forme, alors un seul neurone suffit. Si on sait qu'un polynôme pourrait résoudre le problème posé, mais que l'on ignore sa forme, alors dans ce cas le problème n'est plus une simple combinaison linéaire de coefficients. Lorsque l'on ignore la forme exacte de la relation entre les variables, il devient nécessaire de disposer d'un modèle capable d'apprendre automatiquement des transformations non linéaires pertinentes à partir des données. C'est précisément l'un des intérêts des réseaux de neurones multicouches.

Imaginons que l'on cherche à modéliser un problème à l'aide d'un polynôme de degrés 3. En théorie, on peut alors utiliser un seul neurone et on peut s'intéresser aux valeurs des paramètres...

In [9]:
# Données (polynôme degré 3 :  y = 1 + 2x - 0.5x^2 + 0.1x^3 + bruit)
np.random.seed(0)

X = np.linspace(-3, 3, 200).reshape(-1, 1)

y = (
    1
    + 2 * X
    - 0.5 * X**2
    + 0.1 * X**3
    + np.random.normal(0, 0.5, size=(200, 1))
)

# Construction des features polynomiales
X_poly = np.hstack([
    X,
    X**2,
    X**3
])
In [10]:
# Modèle : 1 neurone linéaire
model = Sequential([Dense(1, input_shape=(3,), activation='linear')])
model.compile(optimizer='sgd', loss='mse')
# Entraînement
model.fit(X_poly, y, epochs=100, verbose=0)
# Extraction des coefficients
weights, bias = model.layers[0].get_weights()

beta_1, beta_2, beta_3 = weights.flatten()
beta_0 = bias[0]

# Affichage
print(f"β0 (intercept) : {beta_0:.3f}")
print(f"β1 (x)         : {beta_1:.3f}")
print(f"β2 (x²)        : {beta_2:.3f}")
print(f"β3 (x³)        : {beta_3:.3f}")
β0 (intercept) : -108987176.000
β1 (x)         : 158831024.000
β2 (x²)        : 20081772.000
β3 (x³)        : 282371104.000
In [11]:
loss = model.evaluate(X_poly, y, verbose=0)
print(loss)
1.0119843449399149e+19

Comme on le voit ici, ça ne marche pas toujours très bien. On voit que la descente de gradient peut être un algorithme capricieux. On va donc passer à plusieurs neurones et surtout à plusieurs couches avec des fonctions d'activation non linéaires :

In [12]:
# Une couche d'entrée et une couche de sortie de dimension 1 pour les prédictions entre les deux deux couches cachées de 12 neurones non linéaires
model = Sequential()
model.add(Input((3,), name="InputLayer"))
model.add(Dense(12, activation='relu', name='Dense_n1'))
model.add(Dense(12, activation='relu', name='Dense_n2'))
model.add(Dense(1, name='Output'))

model.compile(optimizer = 'adam',
                loss      = 'mse',
                metrics   = ['mae', 'mse'] )

model.summary()
Model: "sequential_2"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Layer (type)                         ┃ Output Shape                ┃         Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ Dense_n1 (Dense)                     │ (None, 12)                  │              48 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ Dense_n2 (Dense)                     │ (None, 12)                  │             156 │
├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤
│ Output (Dense)                       │ (None, 1)                   │              13 │
└──────────────────────────────────────┴─────────────────────────────┴─────────────────┘
 Total params: 217 (868.00 B)
 Trainable params: 217 (868.00 B)
 Non-trainable params: 0 (0.00 B)
In [13]:
history = model.fit(X_poly, y, epochs = 100)
Epoch 1/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 1s 5ms/step - loss: 25.2928 - mae: 4.1509 - mse: 25.2928
Epoch 2/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 21.9829 - mae: 3.9203 - mse: 21.9829 
Epoch 3/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 21.2173 - mae: 3.8073 - mse: 21.2173 
Epoch 4/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 17.5155 - mae: 3.5191 - mse: 17.5155 
Epoch 5/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 14.4881 - mae: 3.2424 - mse: 14.4881 
Epoch 6/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 12.4418 - mae: 3.0053 - mse: 12.4418 
Epoch 7/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 10.0541 - mae: 2.7138 - mse: 10.0541 
Epoch 8/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 7.7314 - mae: 2.3690 - mse: 7.7314 
Epoch 9/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 6ms/step - loss: 7.0516 - mae: 2.3062 - mse: 7.0516 
Epoch 10/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 5.6099 - mae: 2.0678 - mse: 5.6099 
Epoch 11/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 6ms/step - loss: 4.6293 - mae: 1.9075 - mse: 4.6293 
Epoch 12/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 3.6078 - mae: 1.6967 - mse: 3.6078 
Epoch 13/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 6ms/step - loss: 3.5473 - mae: 1.6910 - mse: 3.5473 
Epoch 14/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 3.0583 - mae: 1.5588 - mse: 3.0583 
Epoch 15/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 2.6033 - mae: 1.4258 - mse: 2.6033 
Epoch 16/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 2.5768 - mae: 1.4067 - mse: 2.5768 
Epoch 17/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 2.4710 - mae: 1.3619 - mse: 2.4710 
Epoch 18/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 2.3982 - mae: 1.3427 - mse: 2.3982 
Epoch 19/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 2.4098 - mae: 1.3487 - mse: 2.4098 
Epoch 20/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 2.3917 - mae: 1.3450 - mse: 2.3917 
Epoch 21/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 2.3410 - mae: 1.3270 - mse: 2.3410 
Epoch 22/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 2.1362 - mae: 1.2775 - mse: 2.1362 
Epoch 23/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 2.3664 - mae: 1.3282 - mse: 2.3664 
Epoch 24/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 2.0407 - mae: 1.2207 - mse: 2.0407 
Epoch 25/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 2.1272 - mae: 1.2660 - mse: 2.1272 
Epoch 26/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 2.2095 - mae: 1.2968 - mse: 2.2095 
Epoch 27/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 2.2059 - mae: 1.2811 - mse: 2.2059 
Epoch 28/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.9728 - mae: 1.2259 - mse: 1.9728 
Epoch 29/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 2.2194 - mae: 1.2858 - mse: 2.2194 
Epoch 30/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.9897 - mae: 1.2342 - mse: 1.9897 
Epoch 31/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.9852 - mae: 1.2161 - mse: 1.9852 
Epoch 32/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 2.1159 - mae: 1.2689 - mse: 2.1159 
Epoch 33/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.8691 - mae: 1.1689 - mse: 1.8691 
Epoch 34/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 1.8858 - mae: 1.1728 - mse: 1.8858 
Epoch 35/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 2.0523 - mae: 1.2462 - mse: 2.0523 
Epoch 36/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.9878 - mae: 1.2328 - mse: 1.9878 
Epoch 37/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.9476 - mae: 1.2144 - mse: 1.9476 
Epoch 38/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.7155 - mae: 1.1068 - mse: 1.7155 
Epoch 39/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.5897 - mae: 1.0748 - mse: 1.5897 
Epoch 40/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.8115 - mae: 1.1636 - mse: 1.8115 
Epoch 41/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.7784 - mae: 1.1502 - mse: 1.7784 
Epoch 42/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 8ms/step - loss: 1.7353 - mae: 1.1366 - mse: 1.7353 
Epoch 43/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 6ms/step - loss: 1.8231 - mae: 1.1787 - mse: 1.8231 
Epoch 44/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 6ms/step - loss: 1.6079 - mae: 1.0843 - mse: 1.6079 
Epoch 45/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.7060 - mae: 1.1307 - mse: 1.7060 
Epoch 46/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.5163 - mae: 1.0600 - mse: 1.5163 
Epoch 47/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.7869 - mae: 1.1664 - mse: 1.7869 
Epoch 48/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.5355 - mae: 1.0727 - mse: 1.5355 
Epoch 49/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.7572 - mae: 1.1553 - mse: 1.7572 
Epoch 50/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.5269 - mae: 1.0578 - mse: 1.5269 
Epoch 51/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.6542 - mae: 1.1233 - mse: 1.6542 
Epoch 52/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.4206 - mae: 1.0210 - mse: 1.4206 
Epoch 53/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.4186 - mae: 1.0113 - mse: 1.4186 
Epoch 54/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.3353 - mae: 0.9878 - mse: 1.3353 
Epoch 55/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.3443 - mae: 1.0000 - mse: 1.3443 
Epoch 56/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.3308 - mae: 0.9780 - mse: 1.3308 
Epoch 57/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.3435 - mae: 0.9881 - mse: 1.3435 
Epoch 58/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.3493 - mae: 0.9980 - mse: 1.3493 
Epoch 59/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.3008 - mae: 0.9641 - mse: 1.3008 
Epoch 60/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.3148 - mae: 0.9708 - mse: 1.3148 
Epoch 61/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.1672 - mae: 0.8999 - mse: 1.1672 
Epoch 62/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.1868 - mae: 0.9143 - mse: 1.1868 
Epoch 63/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.2511 - mae: 0.9447 - mse: 1.2511 
Epoch 64/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 6ms/step - loss: 1.1430 - mae: 0.9140 - mse: 1.1430 
Epoch 65/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.2161 - mae: 0.9333 - mse: 1.2161 
Epoch 66/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 1.1499 - mae: 0.9064 - mse: 1.1499 
Epoch 67/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.0350 - mae: 0.8534 - mse: 1.0350 
Epoch 68/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.1660 - mae: 0.9172 - mse: 1.1660 
Epoch 69/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.0426 - mae: 0.8679 - mse: 1.0426 
Epoch 70/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.9786 - mae: 0.8186 - mse: 0.9786 
Epoch 71/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 1.0107 - mae: 0.8408 - mse: 1.0107 
Epoch 72/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.9536 - mae: 0.8109 - mse: 0.9536 
Epoch 73/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 1.0047 - mae: 0.8535 - mse: 1.0047 
Epoch 74/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.8960 - mae: 0.7950 - mse: 0.8960 
Epoch 75/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.9065 - mae: 0.7969 - mse: 0.9065 
Epoch 76/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.8279 - mae: 0.7514 - mse: 0.8279 
Epoch 77/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.8723 - mae: 0.7807 - mse: 0.8723 
Epoch 78/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.8547 - mae: 0.7795 - mse: 0.8547 
Epoch 79/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.8253 - mae: 0.7702 - mse: 0.8253 
Epoch 80/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.7657 - mae: 0.7323 - mse: 0.7657 
Epoch 81/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.6820 - mae: 0.6798 - mse: 0.6820 
Epoch 82/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.8273 - mae: 0.7605 - mse: 0.8273 
Epoch 83/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.7188 - mae: 0.7004 - mse: 0.7188 
Epoch 84/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.7165 - mae: 0.7017 - mse: 0.7165 
Epoch 85/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.6611 - mae: 0.6719 - mse: 0.6611 
Epoch 86/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.5889 - mae: 0.6346 - mse: 0.5889 
Epoch 87/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step - loss: 0.6923 - mae: 0.6765 - mse: 0.6923 
Epoch 88/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.5968 - mae: 0.6367 - mse: 0.5968 
Epoch 89/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.6324 - mae: 0.6669 - mse: 0.6324 
Epoch 90/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.5982 - mae: 0.6438 - mse: 0.5982 
Epoch 91/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.5485 - mae: 0.6107 - mse: 0.5485 
Epoch 92/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.5719 - mae: 0.6120 - mse: 0.5719 
Epoch 93/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.5381 - mae: 0.6000 - mse: 0.5381 
Epoch 94/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.5391 - mae: 0.5986 - mse: 0.5391 
Epoch 95/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.5115 - mae: 0.5823 - mse: 0.5115 
Epoch 96/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.5177 - mae: 0.5878 - mse: 0.5177 
Epoch 97/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.4677 - mae: 0.5556 - mse: 0.4677 
Epoch 98/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.4303 - mae: 0.5198 - mse: 0.4303 
Epoch 99/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.3980 - mae: 0.5144 - mse: 0.3980 
Epoch 100/100
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 5ms/step - loss: 0.4205 - mae: 0.5139 - mse: 0.4205 
In [14]:
score = model.evaluate(X_poly, y,  verbose=0)

print('loss      : {:5.4f}'.format(score[0]))
print('mae       : {:5.4f}'.format(score[1]))
print('mse       : {:5.4f}'.format(score[2]))
loss      : 0.3860
mae       : 0.4979
mse       : 0.3860
In [15]:
X1 = np.array([[-1.6342]]).reshape(-1, 1)
y1 = (1 + 2 * X1 - 0.5 * X1**2 + 0.1 * X1**3)
X_poly1 = np.hstack([X1, X1**2, X1**3])
predictions = model.predict( X_poly1 )

print("Prediction : " + str(predictions[0][0]))
print("Reality    : " + str(format(y1[0][0])))
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 76ms/step
Prediction : -3.5653694
Reality    : -4.0401358473688
In [16]:
vary = np.sum((y - np.mean(y))**2)
y_pred = model.predict( X_poly )

residuals = y - y_pred
error_moy = np.sum(np.abs(residuals)) / len(y)
rss = np.sum(residuals**2)

print("MAE : " + str(error_moy))
print("R2 : " + str(1 - (rss / vary)))
7/7 ━━━━━━━━━━━━━━━━━━━━ 0s 4ms/step 
MAE : 0.4979417879170137
R2 : 0.9820949951418474

Les prédictions obtenues sont alors plutôt bonnes avec plusieurs neurones non-linéaires, mais nous ne récupérons pas les paramètres du polynôme de départ, qui sont pourtant faciles à obtenir avec une régression linéaire classique.

In [17]:
z = np.polyfit(list(X.flatten()), list(y.flatten()), 3)
print("Les paramètres du polynome : " + str(z)) #Plus haut degres en premier
Les paramètres du polynome : [ 0.08049601 -0.49523311  2.09081018  1.02101087]
In [18]:
y_pred = z[3] + z[2] * X + z[1] * X**2 + z[0] * X**3

residuals = y - y_pred
error_moy = np.sum(np.abs(residuals)) / len(y)
rss = np.sum(residuals**2)

print(error_moy)
print("R2 : " + str(1 - (rss / vary)))
0.4155176840113402
R2 : 0.988246588509612

Exemple pratique : Prédiction des prix de l'immobilier à Boston¶

Pour appliquer nos premières connaissances en matière de construction de réseaux de neurones, on va chercher à construire un modèle de prédiction des prix de l'immobilier sur un jeu de données très connu, les prix de l'immobilier à Boston.
Le jeu de données initial peut être retrouvé ici : https://www.cs.toronto.edu/~delve/data/boston/bostonDetail.html
Un autre exemple connu de prix de l'immobilier peut etre obtenu ici : https://keras.io/api/datasets/california_housing/

In [19]:
adresse = "boston.csv"
In [20]:
import pandas as pd
df = pd.read_csv(adresse,sep =',')
df.head()
Out[20]:
crim zn indus chas nox rm age dis rad tax ptratio b lstat medv
0 0.00632 18.0 2.31 0 0.538 6.575 65.2 4.0900 1 296 15.3 396.90 4.98 24.0
1 0.02731 0.0 7.07 0 0.469 6.421 78.9 4.9671 2 242 17.8 396.90 9.14 21.6
2 0.02729 0.0 7.07 0 0.469 7.185 61.1 4.9671 2 242 17.8 392.83 4.03 34.7
3 0.03237 0.0 2.18 0 0.458 6.998 45.8 6.0622 3 222 18.7 394.63 2.94 33.4
4 0.06905 0.0 2.18 0 0.458 7.147 54.2 6.0622 3 222 18.7 396.90 5.33 36.2

CRIM: This is the per capita crime rate by town
ZN: This is the proportion of residential land zoned for lots larger than 25,000 sq.ft
INDUS: This is the proportion of non-retail business acres per town
CHAS: This is the Charles River dummy variable (this is equal to 1 if tract bounds river; 0 otherwise)
NOX: This is the nitric oxides concentration (parts per 10 million)
RM: This is the average number of rooms per dwelling
AGE: This is the proportion of owner-occupied units built prior to 1940
DIS: This is the weighted distances to five Boston employment centers
RAD: This is the index of accessibility to radial highways
TAX: This is the full-value property-tax rate per 10,000 dollars
PTRATIO: This is the pupil-teacher ratio by town
B: This is calculated as 1000(Bk — 0.63)^2, where Bk is the proportion of people of African American descent by town
LSTAT: This is the percentage lower status of the population
MEDV: This is the median value of owner-occupied homes in 1000 dollars

Premièrement, on va appliquer une régression linéaire multiple classique sur ces données. On doit alors clairement distinguer les données d'entrainement et les données de test.

In [21]:
#On decoupe les donnees en donnees d'entrainement et de test en mélangeant celles-ci
data = df.sample(frac=1., axis=0)
data_train = data.sample(frac=0.7, axis=0)
data_test  = data.drop(data_train.index)
#On separe x et y
x_train = data_train.drop('medv',  axis=1)
y_train = data_train['medv']
x_test  = data_test.drop('medv',   axis=1)
y_test  = data_test['medv']

On va normaliser les données, car elles sont très hétérogènes.

In [22]:
#On normalise X
mean = x_train.mean()
std  = x_train.std()
x_train = (x_train - mean) / std
x_test  = (x_test  - mean) / std
#On passe en array
x_train, y_train = np.array(x_train), np.array(y_train)
x_test,  y_test  = np.array(x_test),  np.array(y_test)
In [23]:
#Calcul de la variance a reconstituer
vary = np.sum((y_test - np.mean(y_test))**2)

On peut appliquer à ce jeu de données, plus précisément sur les données d'entrainement dans un premier temps (x_train, y_train), une régression linéaire classique :

In [24]:
from scipy.stats import t, f
from scipy.stats import norm

def reg(X, y, names, verbose=True) :
    # === 1. Ajouter une constante pour l’intercept ===
    X_ = np.c_[np.ones(len(X)), X]  # colonne de 1 pour le biais
    # === 2. Calcul des coefficients OLS : β̂ = (XᵀX)⁻¹ Xᵀy ===
    XtX_inv = np.linalg.inv(X_.T @ X_)
    beta_hat = XtX_inv @ X_.T @ y
    # === 3. Calcul des résidus ===
    y_pred = X_ @ beta_hat
    residuals = y - y_pred
    # === 4. Variance résiduelle : σ² = RSS / (n - p) ===
    n, p = X_.shape  # p = nb de paramètres (incluant l’intercept)
    RSS = np.sum(residuals**2)
    sigma2 = RSS / (n - p)
    # === 5. Variance des coefficients : Var(β̂) = σ² * (XᵀX)⁻¹ ===
    var_beta = sigma2 * XtX_inv
    se_beta = np.sqrt(np.diag(var_beta))  # erreurs standard
    # === 6. t-statistics : t = β̂ / SE(β̂) ===
    t_stats = beta_hat / se_beta
    # === 7. p-values (bilatérales) ===
    p_values = 2 * (1 - t.cdf(np.abs(t_stats), df=n - p))
    # === 8. R2 ===
    r2 = 1 - RSS / np.sum((y - np.mean(y))**2)
    k = p - 1
    F = (r2 / k) / ((1 - r2) / (n - k - 1))
    p_value = 1 - f.cdf(F, k, n - k - 1)
    # === Affichage ===
    if verbose :
        for i, (b, se, tval, pval) in enumerate(zip(beta_hat, se_beta, t_stats, p_values)):
            if i == 0 : print(f"cst: {b:.4f}, SE={se:.4f}, t={tval:.4f}, p={pval:.4f}")
            if i != 0 : print(f"{names[i-1]}: {b:.4f}, SE={se:.4f}, t={tval:.4f}, p={pval:.4f}")
        print(f"\nR² = {1 - RSS / np.sum((y - np.mean(y))**2):.4f}")
    return beta_hat, se_beta, t_stats, p_values, r2, p_value
In [25]:
noms = df.columns
res = reg(x_train, y_train, noms)
cst: 22.3819, SE=0.2577, t=86.8399, p=0.0000
crim: -0.9784, SE=0.3427, t=-2.8546, p=0.0046
zn: 0.8455, SE=0.3727, t=2.2683, p=0.0239
indus: 0.2121, SE=0.5103, t=0.4156, p=0.6780
chas: 0.8184, SE=0.2661, t=3.0757, p=0.0023
nox: -2.1003, SE=0.5356, t=-3.9212, p=0.0001
rm: 2.8640, SE=0.3526, t=8.1217, p=0.0000
age: 0.5534, SE=0.4417, t=1.2529, p=0.2111
dis: -2.7938, SE=0.4935, t=-5.6616, p=0.0000
rad: 2.7411, SE=0.7272, t=3.7693, p=0.0002
tax: -1.7197, SE=0.8028, t=-2.1421, p=0.0329
ptratio: -2.1600, SE=0.3370, t=-6.4093, p=0.0000
b: 1.0519, SE=0.3075, t=3.4214, p=0.0007
lstat: -4.0640, SE=0.4258, t=-9.5444, p=0.0000

R² = 0.7398

Le R2 nous apprend que la régression est plutôt de bonne qualité. La plupart des variables semblent significatives. Le jeu de données est donc plutôt bon à donner à un réseau de neurones. A priori il y a de la connaissance experte derrière. On peut alors faire des prédictions sur le jeu de données test en récupérant les coefficients calculés précédemment sur les données d'entrainement :

In [26]:
X_ = np.c_[np.ones(len(x_test)), x_test]
y_pred = X_ @ res[0]
ypred = res[-1]
print("Pred : ", y_pred[10], "Value : ", y_test[10])
print("Pred : ", y_pred[11], "Value : ", y_test[11])
Pred :  8.196582272310376 Value :  5.0
Pred :  31.389964640513373 Value :  31.1
In [27]:
residuals = y_test - y_pred
error_moy = np.sum(np.abs(residuals)) / len(y_test)
rss = np.sum(residuals**2)
print("MAE : " + str(error_moy))
print("R2 : " + str(1 - (rss / vary)))
MAE : 3.508086014387225
R2 : 0.7258450492857227

Les prédictions sont bonnes : le R2 est à la hauteur de celui du jeu d'entrainement. On précise ici le calcul de la MAE qui est davantage utilisé lorsque l'on utilise des réseaux de neurones. Maintenant, étudions ce que l'on peut obtenir en utilisant un réseau de neurones avec des activations non linéaires, puisque sinon on a vu que l'on retrouvera les résultats de la régression linéaire. Peut-on mieux faire en termes de prédictions ?

In [28]:
model = Sequential()
model.add(Input((13,), name="InputLayer"))
model.add(Dense(32, activation='relu', name='Dense_n1'))
model.add(Dense(64, activation='relu', name='Dense_n2'))
model.add(Dense(32, activation='relu', name='Dense_n3'))
model.add(Dense(1, name='Output'))
  
model.compile(optimizer = 'adam', loss = 'mse', metrics = ['mae', 'mse'] )
In [29]:
model.fit(x_train, y_train, epochs = 200, verbose = False, validation_data = (x_test, y_test))
Out[29]:
<keras.src.callbacks.history.History at 0x1aa4c2b6810>
In [30]:
score = model.evaluate(x_test, y_test, verbose=0)
print('loss      : {:5.4f}'.format(score[0]))
print('mae       : {:5.4f}'.format(score[1]))
print('mse       : {:5.4f}'.format(score[2]))
loss      : 13.8986
mae       : 2.7022
mse       : 13.8986
In [31]:
predictions = model( x_test )
print("Prediction : {:.2f}".format(predictions[0][0]))
print("Value : ", y_test[0])
print("Prediction : {:.2f}".format(predictions[1][0]))
print("Value : ", y_test[1])
Prediction : 18.34
Value :  22.0
Prediction : 11.83
Value :  13.4
In [32]:
residuals = np.array(np.array(predictions).flatten()) - y_test
error_moy = np.sum(np.abs(residuals))/len(y_test)
rss = np.sum(residuals**2)
print("MAE : " + str(error_moy))
print("R2 : " + str(1 - (rss / vary)))
MAE : 2.70217988804767
R2 : 0.8234363701493373

Le calcul du R2 que j'ai rajouté, mais aussi surtout celui de la MAE nous montre que le réseau de neurones arrive dans cet exemple à mieux prédire les prix de l'immobilier qu'une régression linéaire multiple. La prise en compte d'un modèle non linéaire semble donc plus appropriée ici. Il faut reconnaitre ici que c'est bien souvent le cas et qu'il est difficile battre les réseaux de neurones en matière de prédictions dans de nombreux domaines. Néanmoins, en matière de compréhension, pour exploiter les connaissances du réseau et donc ce qui influence les prix de l'immobilier, il faudra s'armer d'outils difficiles à présenter maintenant avec notre niveau de compétence. On a donc affaire à une boite noire pour l'instant.

L'alternative Scikit-learn¶

Pour ce type de problème, il n'est pas nécessaire d'utiliser des architectures profondes ou particulièrement complexes. L'utilisation de TensorFlow peut même sembler excessive, dans la mesure où des outils plus simples permettent déjà de construire des réseaux de neurones performants. La bibliothèque scikit-learn propose notamment la classe MLPRegressor, qui permet d'appliquer rapidement un réseau de neurones multicouche à un jeu de données tabulaire. Nous utilisons néanmoins TensorFlow ici car il offre un contrôle plus fin sur l'architecture du réseau et constitue aujourd'hui une référence pour l'apprentissage du deep learning. Ci-dessous l'utilisation de MLPRegressor :

In [33]:
from sklearn.neural_network import MLPRegressor
In [34]:
regr = MLPRegressor(random_state=1, max_iter=2000, tol=0.1) # Une couche de 100 neurones
regr.fit(x_train, y_train)
regr.predict(x_test)
regr.score(x_test, y_test)
Out[34]:
0.6221644268051829
In [35]:
# Deux couches de neurones sont en théorie plus adaptées à notre problème
regr = MLPRegressor(random_state=1, hidden_layer_sizes=(50,50), max_iter=2000, tol=0.1) 
regr.fit(x_train, y_train)
regr.predict(x_test)
regr.score(x_test, y_test)
Out[35]:
0.69868613458768

Les résultats sont moins bons qu'avec notre réseau "construit à la main". Néanmoins, la force de Scikit-learn est que cette bibliothèque permet d'utiliser d'autres algorithmes de prédictions issus du machine learning qui sont en théorie meilleurs pour ce type de prédictions :

In [36]:
from sklearn.ensemble import RandomForestRegressor
regr = RandomForestRegressor(max_depth=2, random_state=0)
regr.fit(x_train, y_train)
regr.predict(x_test)
regr.score(x_test, y_test)
Out[36]:
0.7812232553842731
In [37]:
from sklearn.ensemble import GradientBoostingRegressor
regr = GradientBoostingRegressor(random_state=0)
regr.fit(x_train, y_train)
regr.predict(x_test)
regr.score(x_test, y_test)
Out[37]:
0.868236377908265
In [38]:
from sklearn.ensemble import HistGradientBoostingRegressor #Pour de grands jeu de données
HistGradientBoostingRegressor().fit(x_train, y_train)
regr.predict(x_test)
regr.score(x_test, y_test)
Out[38]:
0.868236377908265
In [39]:
from sklearn.svm import SVR
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
regr = make_pipeline(StandardScaler(), SVR(C=1.0, epsilon=0.2))
regr.fit(x_train, y_train)
regr.predict(x_test)
regr.score(x_test, y_test)
Out[39]:
0.620450119313478

A ce jeu de prédictions, sur ce jeu de données, c'est l'algorithme GradientBoostingRegressor qui a gagné et pas le réseau de neurones !