Régression linéaire à partir de zéro avec NumPy
Régression linéaire avec NumPy
Motivation
La régression linéaire est l’un des outils les plus fondamentaux en apprentissage automatique. Il est utilisé pour trouver une ligne droite qui correspond bien à nos données. Même s’il ne fonctionne qu’avec des modèles simples de lignes droites, comprendre les mathématiques qui le sous-tendent nous aide à comprendre la descente de gradient et les méthodes de minimisation des pertes. Ce sont des éléments importants pour les modèles plus complexes utilisés dans toutes les tâches d’apprentissage automatique et d’apprentissage profond.
- Les 5 meilleurs outils d’IA pour maximiser la productivité
- Utilisation de la ROC pour les dessins techniques complexes
- Une revue complète de la Blockchain dans l’IA
Dans cet article, nous allons nous retrousser les manches et construire une régression linéaire à partir de zéro en utilisant NumPy. Au lieu d’utiliser des implémentations abstraites telles que celles fournies par Scikit-Learn, nous partirons de l’essentiel.
Ensemble de données
Nous générons un ensemble de données factices en utilisant les méthodes de Scikit-Learn. Nous n’utilisons qu’une seule variable pour l’instant, mais l’implémentation sera générale et pourra être entraînée sur n’importe quel nombre de caractéristiques.
La méthode make_regression fournie par Scikit-Learn génère des ensembles de données de régression linéaire aléatoires, avec un bruit gaussien ajouté pour ajouter un peu d’aléatoire.
X, y = datasets.make_regression(
n_samples=500, n_features=1, noise=15, random_state=4)
Nous générons 500 valeurs aléatoires, chacune avec une seule caractéristique. Par conséquent, X a une forme (500, 1) et chacune des 500 valeurs indépendantes de X a une valeur y correspondante. Donc, y a également une forme (500, ).
Visualisé, l’ensemble de données ressemble à ceci:
Nous cherchons à trouver une ligne qui correspond le mieux à ces données, minimisant la différence moyenne entre les valeurs y prédites et les valeurs y originales.
Intuition
L’équation générale pour une ligne linéaire est:
y = m*X + b
X est numérique, à valeur unique. Ici, m et b représentent le gradient et l’ordonnée à l’origine (ou le biais). Ce sont des inconnues, et des valeurs variables de celles-ci peuvent générer différentes lignes. En apprentissage automatique, X dépend des données, tout comme les valeurs y. Nous avons seulement le contrôle sur m et b, qui agissent comme nos paramètres de modèle. Nous cherchons à trouver des valeurs optimales pour ces deux paramètres, qui génèrent une ligne qui minimise la différence entre les valeurs y prédites et réelles.
Cela s’étend au scénario où X est multidimensionnel. Dans ce cas, le nombre de valeurs m sera égal au nombre de dimensions dans nos données. Par exemple, si nos données ont trois caractéristiques différentes, nous aurons trois valeurs m différentes, appelées poids.
L’équation deviendra alors:
y = w1*X1 + w2*X2 + w3*X3 + b
Cela peut ensuite s’étendre à n’importe quel nombre de caractéristiques.
Mais comment connaître les valeurs optimales de notre biais et de nos poids? Eh bien, nous ne le savons pas. Mais nous pouvons le découvrir de manière itérative en utilisant la descente de gradient. Nous commençons par des valeurs aléatoires et les modifions légèrement pour plusieurs étapes jusqu’à ce que nous nous rapprochions des valeurs optimales.
Tout d’abord, initialisons la régression linéaire, et nous passerons en revue le processus d’optimisation en détail plus tard.
Initialiser la classe de régression linéaire
import numpy as np
class LinearRegression:
def __init__(self, lr: int = 0.01, n_iters: int = 1000) -> None:
self.lr = lr
self.n_iters = n_iters
self.weights = None
self.bias = None
Nous utilisons un taux d’apprentissage et un nombre d’itérations comme hyperparamètres, qui seront expliqués plus tard. Les poids et les biais sont définis sur None car le nombre de paramètres de poids dépend des caractéristiques d’entrée dans les données. Nous n’avons pas encore accès aux données, nous les initialisons donc à None pour l’instant.
La méthode Fit
Dans la méthode Fit, nous disposons de données et de leurs valeurs associées. Nous pouvons maintenant utiliser celles-ci pour initialiser nos poids, puis entraîner le modèle pour trouver des poids optimaux.
def ajuster(self, X, y):
num_samples, num_features = X.shape # X shape [N, f]
self.weights = np.random.rand(num_features) # W shape [f, 1]
self.bias = 0
La variable indépendante X sera un tableau NumPy de taille (num_samples, num_features). Dans notre cas, la taille de X est (500, 1). Chaque ligne de nos données aura une valeur cible associée, donc y sera également de taille (500,) ou (num_samples).
Nous extrayons cela et initialisons de manière aléatoire les poids en fonction du nombre de caractéristiques d’entrée. Maintenant, nos poids sont également un tableau NumPy de taille (num_features, ). Le biais est une seule valeur initialisée à zéro.
Prédiction des valeurs de Y
Nous utilisons l’équation de la ligne discutée ci-dessus pour calculer les valeurs de y prédites. Cependant, au lieu d’une approche itérative pour additionner toutes les valeurs, nous pouvons suivre une approche vectorisée pour une computation plus rapide. Étant donné que les poids et les valeurs X sont des tableaux NumPy, nous pouvons utiliser la multiplication matricielle pour obtenir des prédictions.
X a une taille (num_samples, num_features) et les poids ont une taille (num_features, ). Nous voulons que les prédictions aient une taille (num_samples, ) correspondant aux valeurs y d’origine. Par conséquent, nous pouvons multiplier X par les poids, ou (num_samples, num_features) x (num_features, ) pour obtenir des prédictions de taille (num_samples, ).
La valeur du biais est ajoutée à la fin de chaque prédiction. Cela peut être simplement implémenté en une seule ligne.
# La forme de y_pred devrait être N, 1
y_pred = np.dot(X, self.weights) + self.bias
Cependant, ces prédictions sont-elles correctes ? Évidemment non. Nous utilisons des valeurs initialisées de manière aléatoire pour les poids et le biais, donc les prédictions seront également aléatoires.
Comment obtenir les valeurs optimales ? Descente de gradient.
Fonction de perte et descente de gradient
Maintenant que nous avons à la fois les valeurs prédites et les valeurs cibles y, nous pouvons trouver la différence entre les deux valeurs. L’erreur quadratique moyenne (MSE) est utilisée pour comparer des nombres réels. L’équation est la suivante :
Nous nous intéressons uniquement à la différence absolue entre nos valeurs. Une prédiction supérieure à la valeur d’origine est aussi mauvaise qu’une prédiction inférieure. Nous élevons donc au carré la différence entre notre valeur cible et nos prédictions, pour convertir les différences négatives en positives. De plus, cela pénalise une plus grande différence entre les cibles et les prédictions, car des différences plus élevées au carré contribueront davantage à la perte finale.
Pour que nos prédictions soient aussi proches que possible des cibles d’origine, nous essayons maintenant de minimiser cette fonction. La fonction de perte sera minimale lorsque le gradient est nul. Comme nous ne pouvons optimiser que nos valeurs de poids et de biais, nous prenons les dérivées partielles de la fonction MSE par rapport aux valeurs de poids et de biais.
Nous optimisons ensuite nos poids en fonction des valeurs de gradient, en utilisant la descente de gradient.
Nous prenons le gradient par rapport à chaque valeur de poids, puis les déplaçons vers l’opposé du gradient. Cela pousse la perte vers un minimum. Comme le montre l’image, le gradient est positif, nous diminuons donc le poids. Cela pousse J(W) ou la perte vers la valeur minimale. Par conséquent, les équations d’optimisation sont les suivantes :
Le taux d’apprentissage (ou alpha) contrôle les étapes incrémentielles montrées dans l’image. Nous ne faisons qu’un petit changement de valeur, pour un mouvement stable vers le minimum.
Implémentation
Si nous simplifions l’équation dérivée en utilisant une manipulation algébrique de base, cela devient très simple à implémenter.
Pour la dérivation, nous implémentons cela en utilisant deux lignes de code :
# X -> [ N, f ]
# y_pred -> [ N ]
# dw -> [ f ]
dw = (1 / num_samples) * np.dot(X.T, y_pred - y)
db = (1 / num_samples) * np.sum(y_pred - y)
dw est à nouveau de forme (num_features, ) Donc nous avons une valeur de dérivée séparée pour chaque poids. Nous les optimisons séparément. db a une seule valeur.
Pour optimiser les valeurs maintenant, nous déplaçons les valeurs dans la direction opposée du gradient en utilisant une soustraction basique.
self.weights = self.weights - self.lr * dw
self.bias = self.bias - self.lr * db
Encore une fois, il s’agit d’une seule étape. Nous ne faisons qu’un petit changement aux valeurs initialisées aléatoirement. Nous répétons maintenant les mêmes étapes à plusieurs reprises, afin de converger vers un minimum.
La boucle complète est la suivante :
for i in range(self.n_iters):
# y_pred shape should be N, 1
y_pred = np.dot(X, self.weights) + self.bias
# X -> [N,f]
# y_pred -> [N]
# dw -> [f]
dw = (1 / num_samples) * np.dot(X.T, y_pred - y)
db = (1 / num_samples) * np.sum(y_pred - y)
self.weights = self.weights - self.lr * dw
self.bias = self.bias - self.lr * db
Prédiction
Nous prédisons de la même manière que pendant l’entraînement. Cependant, maintenant nous avons l’ensemble optimal de poids et de biais. Les valeurs prédites devraient maintenant être proches des valeurs originales.
def predict(self, X):
return np.dot(X, self.weights) + self.bias
Résultats
Avec des poids et un biais initialisés de manière aléatoire, nos prédictions étaient les suivantes :
Image par l’auteur Les poids et le biais ont été initialisés très proches de 0, nous obtenons donc une ligne horizontale. Après avoir entraîné le modèle pendant 1000 itérations, nous obtenons ceci :
Image par l’auteur
La ligne prédite passe en plein centre de nos données et semble être la meilleure ligne d’ajustement possible.
Conclusion
Vous avez maintenant implémenté la régression linéaire à partir de zéro. Le code complet est également disponible sur GitHub.
import numpy as np
class LinearRegression:
def __init__(self, lr: int = 0.01, n_iters: int = 1000) -> None:
self.lr = lr
self.n_iters = n_iters
self.weights = None
self.bias = None
def fit(self, X, y):
num_samples, num_features = X.shape # X shape [N, f]
self.weights = np.random.rand(num_features) # W shape [f, 1]
self.bias = 0
for i in range(self.n_iters):
# y_pred shape should be N, 1
y_pred = np.dot(X, self.weights) + self.bias
# X -> [N,f]
# y_pred -> [N]
# dw -> [f]
dw = (1 / num_samples) * np.dot(X.T, y_pred - y)
db = (1 / num_samples) * np.sum(y_pred - y)
self.weights = self.weights - self.lr * dw
self.bias = self.bias - self.lr * db
return self
def predict(self, X):
return np.dot(X, self.weights) + self.bias
Muhammad Arham est un ingénieur en apprentissage profond travaillant en vision par ordinateur et en traitement du langage naturel. Il a travaillé sur le déploiement et l’optimisation de plusieurs applications d’IA générative qui ont atteint les premières places mondiales chez Vyro.AI. Il s’intéresse à la construction et à l’optimisation de modèles d’apprentissage automatique pour des systèmes intelligents et croit en l’amélioration continue.
We will continue to update IPGirl; if you have any questions or suggestions, please contact us!
Was this article helpful?
93 out of 132 found this helpful
Related articles
- PyTorch LSTMCell – Formes de l’entrée, de l’état caché, de l’état de cellule et de la sortie
- Une nouvelle recherche en IA de Tel Aviv et de l’Université de Copenhague présente une approche plug-and-play pour ajuster rapidement les modèles de diffusion texte-image en utilisant un signal discriminatif.
- Déploiement des modèles PyTorch avec le serveur d’inférence Nvidia Triton
- Créez des mèmes avec le plugin ChatGPT Meme Creator (pour développer votre entreprise)
- Tim Davis, Co-fondateur et Président de Modular – Série d’interviews
- Dévoiler l’avenir de l’IA avec GPT-4 et l’IA Explicative (XAI)
- La Pratique de la Gestion des Risques de l’IA