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.

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!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

AI

PyTorch LSTMCell - Formes de l'entrée, de l'état caché, de l'état de cellule et de la sortie

Dans Pytorch, pour utiliser un LSTMCell (avec nn.LSTMCell), nous devons comprendre comment les tenseurs représentant ...

AI

5 conseils d'ingénierie rapides pour Claude

Beaucoup de personnes ont commencé à utiliser Claude à la place de ChatGPT... Voici comment tirer le meilleur parti d...

AI

Meta AI présente IMAGEBIND Le premier projet d'IA open-source capable de lier des données provenant de six modalités à la fois, sans besoin de supervision explicite.

Les humains peuvent comprendre des idées complexes après avoir été exposés à seulement quelques exemples. La plupart ...

Apprentissage automatique

Utiliser ChatGPT pour un débogage efficace

Améliorez votre expérience de débogage et apprenez plus rapidement grâce à la puissance des grands modèles de langage...

AI

Un gymnase open-source pour la conception assistée par l'apprentissage automatique de l'architecture informatique

Publié par Amir Yazdanbakhsh, chercheur en informatique, et Vijay Janapa Reddi, chercheur invité, Google Research La ...

AI

Les défis de la génération de stops dans Llama 2

Le lancement de Llama 2 par Meta a suscité l'enthousiasme au sein de la communauté, marquant l'aube d'une ère pour de...