
Tutoriel PyTorch pour les débutants : créer un modèle de régression multiple à partir de zéro
avant que les LLM ne deviennent à la mode, il y avait un presque visible doubler séparer les frameworks de Machine Learning des frameworks de Deep Learning.
La discussion s’est concentrée sur Scikit-Learn, XGBoost et similaires pour le ML, tandis que PyTorch et TensorFlow dominaient la scène lorsque le Deep Learning était en cause.
Cependant, après l’explosion de l’IA, j’ai vu PyTorch dominer la scène bien plus que TensorFlow. Les deux frameworks sont vraiment puissants, permettant aux Data Scientists de résoudre différents types de problèmes, le traitement du langage naturel étant l’un d’entre eux, augmentant ainsi une fois de plus la popularité du Deep Learning.
Eh bien, dans cet article, mon idée n’est pas de parler de PNL, mais plutôt de travailler sur un problème de régression linéaire multivariable avec deux objectifs en tête :
- Apprendre à créer un modèle à l’aide de PyTorch
- Partager des connaissances sur la régression linéaire que l’on ne retrouve pas toujours dans d’autres tutoriels.
Allons-y.
Préparation des données
Très bien, permettez-moi de vous épargner une définition sophistiquée de la régression linéaire. Vous l’avez probablement vu trop souvent dans d’innombrables tutoriels sur Internet. Donc, assez pour dire ça lorsque vous avez une variable Y que vous souhaitez prédire et une autre variable X qui peut expliquer la variation de Y à l’aide d’une ligne droite, c’est-à-dire essentiellement une régression linéaire.
Ensemble de données
Pour cet exercice, utilisons l’ensemble de données Abalone [1].
Nash, W., Sellers, T., Talbot, S., Cawthorn, A. et Ford, W. (1994). Ormeau [Dataset]. Référentiel d’apprentissage automatique UCI. https://doi.org/10.24432/C55C7W.
Selon la documentation de l’ensemble de données, l’âge de l’ormeau est déterminé en coupant la coquille à travers le cône, en la colorant et en comptant le nombre d’anneaux au microscope, une tâche ennuyeuse et longue. D’autres mesures, plus faciles à obtenir, sont utilisées pour prédire l’âge.
Alors, allons-y et chargeons les données. De plus, nous allons encoder à chaud la variable Sexpuisque c’est le seul catégorique.
# Data Load
from ucimlrepo import fetch_ucirepo
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style('darkgrid')
from feature_engine.encoding import OneHotEncoder
# fetch dataset
abalone = fetch_ucirepo(id=1)
# data (as pandas dataframes)
X = abalone.data.features
y = abalone.data.targets
# One Hot Encode Sex
ohe = OneHotEncoder(variables=['Sex'])
X = ohe.fit_transform(X)
# View
df = pd.concat([X,y], axis=1)
Voici l’ensemble de données.

Donc, afin de créer un meilleur modèle, explorons les données.
Explorer les données
Les premières étapes que j’aime effectuer lors de l’exploration d’un ensemble de données sont :
1. Vérification de la distribution de la variable cible.
# Looking at our Target variable
plt.hist(y)
plt.title('Rings [Target Variable] Distribution');
Le graphique montre que la variable cible n’est pas normalement distribuée. Cela peut avoir un impact sur la régression, mais peut généralement être corrigé avec une transformation de puissance, telle que Log ou Box-Cox.

2. Regardez la description statistique.
Les statistiques peuvent nous montrer des informations importantes telles que la moyenne, l’écart type et repérer facilement certains écarts en termes de valeurs minimales ou maximales. Les variables explicatives sont plutôt correctes, dans une plage plus petite et à la même échelle. La variable cible (Rings) est à une échelle différente.
# Statistical description
df.describe()

Ensuite, vérifions les corrélations.
# Looking at the correlations
(df
.drop(['Sex_M', 'Sex_I', 'Sex_F'],axis=1)
.corr()
.style
.background_gradient(cmap='coolwarm')
)

Les variables explicatives ont une corrélation modérée à forte avec Rings. Nous pouvons également constater qu’il existe une certaine colinéarité entre Whole_weight avec Shucked_weight, Viscera_weightet Shell_weight. Length et Diameter sont également colinéaires. Nous pouvons tester leur suppression plus tard.
sns.pairplot(df);
Lorsque nous traçons les nuages de points des paires et examinons la relation des variables avec Ringsnous pouvons rapidement identifier certains problèmes
- L’hypothèse d’homoscédasticité est violée. Cela signifie que la relation n’est pas homogène en termes de variance.
- Regardez comment les tracés forment une forme de cône, augmentant la variance de Y à mesure que les valeurs de X augmentent. Lors de l’estimation de la valeur de
Ringspour des valeurs plus élevées des variables X, l’estimation ne sera pas très précise. - La variable
Heighta au moins deux valeurs aberrantes qui sont très visibles lorsque Hauteur > 0,3.

La suppression des valeurs aberrantes et la transformation de la variable cible en logarithmes entraîneront le prochain tracé des paires. C’est mieux, mais cela ne résout toujours pas le problème de l’homoscédasticité.

Une autre exploration rapide que nous pouvons faire consiste à tracer des graphiques pour vérifier la relation des variables lorsqu’elles sont regroupées par Sex variable.
La variable Diameter a la relation la plus linéaire lorsque Sex=Imais c’est tout.
# Create a FacetGrid with scatterplots
sns.lmplot(x="Diameter", y="Rings", hue="Sex", col="Sex", order=2, data=df);

D’autre part, Shell_weight a trop de dispersion pour les valeurs élevées, ce qui fausse la relation linéaire.
# Create a FacetGrid with scatterplots
sns.lmplot(x="Shell_weight", y="Rings", hue="Sex", col="Sex", data=df);

Tout cela montre qu’un modèle de régression linéaire serait très difficile pour cet ensemble de données et échouerait probablement. Mais nous voulons quand même le faire.
Au fait, je ne me souviens pas avoir vu un article où nous expliquons réellement ce qui n’a pas fonctionné. Ainsi, en faisant cela, nous pouvons également tirer de précieuses leçons.
Modélisation : utilisation de Scikit-Learn
Exécutons le modèle sklearn et évaluons-le à l’aide de l’erreur quadratique moyenne.
from sklearn.linear_model import LinearRegression
from sklearn.metrics import root_mean_squared_error
df2 = df.query('Height < 0.3 and Rings > 2 ').copy()
X = df2.drop(['Rings'], axis=1)
y = np.log(df2['Rings'])
lr = LinearRegression()
lr.fit(X, y)
predictions = lr.predict(X)
df2['Predictions'] = np.exp(predictions)
print(root_mean_squared_error(df2['Rings'], df2['Predictions']))
2.2383762717104916
Si nous examinons l’en-tête, nous pouvons confirmer que le modèle peine à obtenir des estimations pour des valeurs plus élevées (par exemple, lignes 0, 6, 7 et 9).

Un pas en arrière : essayer d’autres transformations
Bien. Alors que pouvons-nous faire maintenant ?
Supprimez probablement d’autres valeurs aberrantes et réessayez. Essayons d’utiliser un algorithme non supervisé pour trouver d’autres valeurs aberrantes. Nous appliquerons le Facteur de valeur aberrante localelaissant tomber 5 % des valeurs aberrantes.
Nous supprimerons également la multicolinéarité, en supprimant Whole_weight et Length.
from sklearn.neighbors import LocalOutlierFactor
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
# fetch dataset
abalone = fetch_ucirepo(id=1)
# data (as pandas dataframes)
X = abalone.data.features
y = abalone.data.targets
# One Hot Encode Sex
ohe = OneHotEncoder(variables=['Sex'])
X = ohe.fit_transform(X)
# Drop Whole Weight and Length (multicolinearity)
X.drop(['Whole_weight', 'Length'], axis=1, inplace=True)
# View
df = pd.concat([X,y], axis=1)
# Let's create a Pipeline to scale the data and find outliers using KNN Classifier
steps = [
('scale', StandardScaler()),
('LOF', LocalOutlierFactor(contamination=0.05))
]
# Fit and predict
outliers = Pipeline(steps).fit_predict(X)
# Add column
df['outliers'] = outliers
# Modeling
df2 = df.query('Height < 0.3 and Rings > 2 and outliers != -1').copy()
X = df2.drop(['Rings', 'outliers'], axis=1)
y = np.log(df2['Rings'])
lr = LinearRegression()
lr.fit(X, y)
predictions = lr.predict(X)
df2['Predictions'] = np.exp(predictions)
print(root_mean_squared_error(df2['Rings'], df2['Predictions']))
2.238174395913869
Même résultat. Hmm….
D’accord. nous pouvons continuer à jouer avec les variables et l’ingénierie des fonctionnalités, et nous commencerons à voir quelques améliorations ici et là, comme lorsque nous ajoutons le carré de Height, Diameteret Shell_weight. Ceci ajouté au traitement des valeurs aberrantes fera baisser le RMSE à 2.196.
# Second Order Variables
X['Diameter_2'] = X['Diameter'] ** 2
X['Height_2'] = X['Height'] ** 2
X['Shell_2'] = X['Shell_weight'] ** 2
Certes, il est juste de noter que chaque variable ajoutée dans les modèles de régression linéaire aura un impact sur le R² et gonflera parfois le résultat, donnant la fausse idée que le modèle s’améliore, alors que ce n’est pas le cas. Dans ce cas, le modèle s’améliore réellement, puisque nous y ajoutons des composantes non linéaires avec les variables du second ordre. Nous pouvons le prouver en calculant le R² ajusté. Il est passé de 0,495 à 0,517.
# Adjusted R²
from sklearn.metrics import r2_score
r2 = r2_score(df2['Rings'], df2['Predictions'])
n= df2.shape[0]
p = df2.shape[1] - 1
adj_r2 = 1 - (1 - r2) * (n - 1) / (n - p - 1)
print(f'R²: {r2}')
print(f'Adjusted R²: {adj_r2}')
En revanche, ramener Whole_weight et Length peut améliorer un peu plus les chiffres, mais je ne le recommanderais pas. Si nous faisons cela, nous ajoutons de la multicolinéarité et gonflons l’importance des coefficients de certaines variables, conduisant à des erreurs d’estimation potentielles à l’avenir.
Modélisation : utilisation de PyTorch
D’accord. Maintenant que nous avons créé un modèle de base, l’idée est de créer un modèle linéaire en utilisant le Deep Learning et d’essayer de battre le RMSE de 2,196.
Droite. Pour commencer, permettez-moi de préciser ceci d’emblée : les modèles de Deep Learning fonctionnent mieux avec des données mises à l’échelle. Cependant, comme nos variables X sont toutes sur la même échelle, nous n’aurons pas à nous en préoccuper. Alors continuons d’avancer.
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
Nous devons préparer les données pour la modélisation avec PyTorch. Ici, nous avons besoin de quelques ajustements pour rendre les données acceptables par le framework PyTorch, car cela ne nécessitera pas de trames de données pandas classiques.
- Utilisons le même bloc de données de notre modèle de base.
- Diviser X et Y
- Transformer la variable Y en journal
- Transformez les deux en tableaux numpy, car PyTorch ne prendra pas de trames de données.
df2 = df.query('Height < 0.3 and Rings > 2 and outliers != -1').copy()
X = df2.drop(['Rings', 'outliers'], axis=1)
y = np.log(df2[['Rings']])
# X and Y to Numpy
X = X.to_numpy()
y = y.to_numpy()
Ensuite, en utilisant TensorDatasetnous faisons de X et Y un objet Tensor et imprimons le résultat.
# Prepare with TensorData
# TensorData helps us transforming the dataset to Tensor object
dataset = TensorDataset(torch.tensor(X).float(), torch.tensor(y).float())
input_sample, label_sample = dataset[0]
print(f'** Input sample: {input_sample}, \n** Label sample: {label_sample}')
** Input sample: tensor([0.3650, 0.0950, 0.2245, 0.1010, 0.1500, 1.0000,
0.0000, 0.0000, 0.1332, 0.0090, 0.0225]),
** Label sample: tensor([2.7081])
Ensuite, grâce à la fonction DataLoader, nous pouvons créer des lots de données. Cela signifie que le réseau neuronal traitera un batch_size quantité de données à la fois.
# Next, let's use DataLoader
batch_size = 500
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
Les modèles PyTorch sont mieux définis en tant que classes.
- Le cours est basé sur
nn.Modulequi est la classe de base de PyTorch pour les réseaux de neurones. - Nous définissons le calques de modèle que nous voulons utiliser dans la méthode init.
super().__init__()garantit que la classe se comportera comme un objet torche.
- Le
forwardla méthode décrit qu’arrive-t-il à l’entrée lorsqu’elle est transmise au modèle.
Ici, nous le passons à travers les couches linéaires que nous avons définies dans la méthode init et utilisons les fonctions d’activation ReLU pour ajouter une certaine non-linéarité au modèle lors de la passe avant.
# 2. Creating a class
class AbaloneModel(nn.Module):
def __init__(self):
super().__init__()
self.linear1 = nn.Linear(in_features=X.shape[1], out_features=128)
self.linear2 = nn.Linear(128, 64)
self.linear3 = nn.Linear(64, 32)
self.linear4 = nn.Linear(32, 1)
def forward(self, x):
x = self.linear1(x)
x = nn.functional.relu(x)
x = self.linear2(x)
x = nn.functional.relu(x)
x = self.linear3(x)
x = nn.functional.relu(x)
x = self.linear4(x)
return x
# Instantiate model
model = AbaloneModel()
Essayons ensuite le modèle pour la première fois en utilisant un script qui simule une recherche aléatoire.
- Créer un critère d’erreur pour l’évaluation du modèle
- Créez une liste pour contenir les données du meilleur modèle et configurez le
best_losscomme une valeur élevée, elle sera donc remplacée par de meilleurs chiffres de perte au cours de l’itération. - Configurez la plage pour le taux d’apprentissage. Nous utiliserons des facteurs de puissance de -2 à -4 (par exemple de 0,01 à 0,0001).
- Configurez une plage pour l’élan de 0,9 à 0,99.
- Obtenez les données
- Mettez à zéro le dégradé pour effacer les calculs de dégradé des itérations précédentes.
- Ajuster le modèle
- Calculez la perte et enregistrez les numéros du meilleur modèle.
- Calculez les poids et les biais avec la passe arrière.
- Répétez N fois et imprimez le meilleur modèle.
# Mean Squared Error (MSE) is standard for regression
criterion = nn.MSELoss()
# Random Search
values = []
best_loss = 999
for idx in range(1000):
# Randomly sample a learning rate factor between 2 and 4
factor = np.random.uniform(2,5)
lr = 10 ** -factor
# Randomly select a momentum between 0.85 and 0.99
momentum = np.random.uniform(0.90, 0.99)
# 1. Get Data
feature, target = dataset[:]
# 2. Zero Gradients: Clear old gradients before the backward pass
optimizer = optim.SGD(model.parameters(), lr=lr, momentum=momentum)
optimizer.zero_grad()
# 3. Forward Pass: Compute prediction
y_pred = model(feature)
# 4. Compute Loss
loss = criterion(y_pred, target)
# 4.1 Register best Loss
if loss < best_loss:
best_loss = loss
best_lr = lr
best_momentum = momentum
best_idx = idx
# 5. Backward Pass: Compute gradient of the loss w.r.t W and b'
loss.backward()
# 6. Update Parameters: Adjust W and b using the calculated gradients
optimizer.step()
values.append([idx, lr, momentum, loss])
print(f'n: {idx},lr: {lr}, momentum: {momentum}, loss: {loss}')
n: 999,lr: 0.004782946959508322, momentum: 0.9801209929050066, loss: 0.06135804206132889
Une fois que nous aurons obtenu le meilleur taux d’apprentissage et le meilleur élan, nous pourrons passer à autre chose.
# --- 3. Loss Function and Optimizer ---
# Mean Squared Error (MSE) is standard for regression
criterion = nn.MSELoss()
# Stochastic Gradient Descent (SGD) with a small learning rate (lr)
optimizer = optim.SGD(model.parameters(), lr=0.004, momentum=0.98)
Ensuite, nous reformerons ce modèle, en utilisant les mêmes étapes que précédemment, mais cette fois en gardant le même taux d’apprentissage et le même élan.
L’ajustement d’un modèle PyTorch nécessite un script plus long que le script normal fit() méthode de Scikit-Learn. Mais ce n’est pas grave. La structure sera toujours similaire à ces étapes :
- Activez le
model.train()mode - Créez une boucle pour le nombre d’itérations souhaité. Chaque itération est appelée une époque.
- Zéro les dégradés des passes précédentes avec
optimizer.zero_grad(). - Récupérez les lots du chargeur de données.
- Calculez les prédictions avec
model(X) - Calculez la perte en utilisant
criterion(y_pred, target). - Effectuez la passe arrière pour calculer les poids et les biais :
loss.backward() - Mettez à jour les poids et les biais avec
optimizer.step()
Nous formerons ce modèle pour 1000 époques (itérations). Ici, nous ajoutons seulement une étape pour obtenir le meilleur modèle à la fin, nous veillons donc à utiliser le modèle avec la meilleure perte.
# 4. Training
torch.manual_seed(42)
NUM_EPOCHS = 1001
loss_history = []
best_loss = 999
# Put model in training mode
model.train()
for epoch in range(NUM_EPOCHS):
for data in dataloader:
# 1. Get Data
feature, target = data
# 2. Zero Gradients: Clear old gradients before the backward pass
optimizer.zero_grad()
# 3. Forward Pass: Compute prediction
y_pred = model(feature)
# 4. Compute Loss
loss = criterion(y_pred, target)
loss_history.append(loss)
# Get Best Model
if loss < best_loss:
best_loss = loss
best_model_state = model.state_dict() # save best model
# 5. Backward Pass: Compute gradient of the loss w.r.t W and b'
loss.backward()
# 6. Update Parameters: Adjust W and b using the calculated gradients
optimizer.step()
# Load the best model before returning predictions
model.load_state_dict(best_model_state)
# Print status every 50 epochs
if epoch % 200 == 0:
print(epoch, loss.item())
print(f'Best Loss: {best_loss}')
0 0.061786893755197525
Best Loss: 0.06033024191856384
200 0.036817338317632675
Best Loss: 0.03243456035852432
400 0.03307393565773964
Best Loss: 0.03077109158039093
600 0.032522525638341904
Best Loss: 0.030613820999860764
800 0.03488151729106903
Best Loss: 0.029514113441109657
1000 0.0369877889752388
Best Loss: 0.029514113441109657
Bon. Le modèle est formé. Il est maintenant temps d’évaluer.
Évaluation
Vérifions si ce modèle a fait mieux que la régression régulière. Pour cela, je vais mettre le modèle en mode évaluation en utilisant model.eval()PyTorch sait donc qu’il doit modifier le comportement de l’entraînement et passer en mode inférence. Cela désactivera la normalisation des couches et les abandons, par exemple.
# Get features
features, targets = dataset[:]
# Get Predictions
model.eval()
with torch.no_grad():
predictions = model(features)
# Add to dataframe
df2['Predictions'] = np.exp(predictions.detach().numpy())
# RMSE
print(root_mean_squared_error(df2['Rings'], df2['Predictions']))
2.1108551025390625
L’amélioration était modeste, environ 4%.
Examinons quelques prédictions de chaque modèle.

Les deux modèles obtiennent des résultats très similaires. Ils luttent davantage à mesure que le nombre d’anneaux augmente. Cela est dû à la forme conique de la variable cible.
Si nous y réfléchissons un instant :
- À mesure que le nombre d’anneaux augmente, la variance provenant de la variable explicative augmente.
- Un ormeau à 15 anneaux se situera dans une plage de valeurs beaucoup plus large qu’un autre à 4 anneaux.
- Cela perturbe le modèle car il doit tracer une seule ligne au milieu des données qui n’est pas si linéaire.
Avant de partir
Nous avons beaucoup appris dans ce projet :
- Comment explorer les données.
- Comment vérifier si le modèle linéaire serait une bonne option.
- Comment créer un modèle PyTorch pour une régression linéaire multivariée.
Au final, nous avons vu qu’une variable cible qui n’est pas homogène, même après transformations de puissance, peut conduire à un modèle peu performant. Notre modèle est toujours meilleur que la prise en compte de la valeur moyenne pour toutes les prédictions, mais l’erreur reste élevée, restant environ 20 % de la valeur moyenne.
Nous avons essayé d’utiliser le Deep Learning pour améliorer le résultat, mais toute cette puissance n’était pas suffisante pour réduire considérablement l’erreur. J’opterais probablement pour le modèle Scikit-Learn, car il est plus simple et plus explicable.
D’autres options pour essayer d’améliorer les résultats seraient de créer un modèle d’ensemble personnalisé avec une forêt aléatoire + régression linéaire. Mais c’est une tâche que je vous laisse, si vous le souhaitez.
Si vous avez aimé ce contenu, retrouvez-moi sur mon site Internet.
https://gustavorsantos.me
Dépôt GitHub
Le code de cet exercice.
https://github.com/gurezende/Linear-Regression-PyTorch
Références
[1. Abalone Dataset – UCI Repository, CC BY 4.0 license.] https://archive.ics.uci.edu/dataset/1/abalone
[2. Eval mode] https://stackoverflow.com/questions/60018578/what-does-model-eval-do-in-pytorch
https://docs.pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.eval
[3. PyTorch Docs] https://docs.pytorch.org/docs/stable/nn.html
[4. Kaggle Notebook] https://www.kaggle.com/code/samlakhmani/s4e4-deeplearning-with-oof-strategy
[5. GitHub Repo] https://github.com/gurezende/Linear-Regression-PyTorch



