Utilisation du lasso pour la sélection de paramètres

L’exercice présenté dans ce post est issu du cours sur la régression dans le parcours “Machine Learning” de l’université de Washington sur Coursera.

Les données à notre disposition pour cet exercice sont celles des ventes de maisons dans le conté de King (état de Washington). Ce fichier contient les informations sur 21613 transactions immobilières et pour chaque vente donne 20 caractéristiques des biens (variables) : date, prix, surface, nombre de chambres, nombre de salles de bain, surface du terrain, localisation, etc.

Le premier travail consiste à utiliser le lasso, technique de réduction des coefficients  afin de sélectionner les variables pertinentes pour établir un modèle de prévision de prix de maisons à partir de leurs caractéristiques.

J’utilise ici le package python “Graphlab” (plus d’informations chez Turi)

Avant de procéder à la création d’un modèle, je crée de nouvelles variables :

  • La racine carrée des surfaces de plancher et de terrain, pour limiter leur impact sur le modèle. En effet, le prix au m² plafonne pour les grandes surfaces.
  • Le nombre de chambres au carré, pour “favoriser” les plus grands nombres de chambres.

import graphlab
import numpy as np
sales = graphlab.SFrame('kc_house_data.gl/kc_house_data.gl/')
from math import log, sqrt
sales['sqft_living_sqrt'] = sales['sqft_living'].apply(sqrt) #Afin de limiter l'impact de la surface sur le prix
sales['sqft_lot_sqrt'] = sales['sqft_lot'].apply(sqrt)
sales['bedrooms_square'] = sales['bedrooms']*sales['bedrooms']

# La variable 'floors' est de type string, on la converti donc en float 
sales['floors'] = sales['floors'].astype(float) 
sales['floors_square'] = sales['floors']*sales['floors']

#Liste des variables utilisées dans le modèle
all_features = ['bedrooms', 'bedrooms_square',
            'bathrooms',
            'sqft_living', 'sqft_living_sqrt',
            'sqft_lot', 'sqft_lot_sqrt',
            'floors', 'floors_square',
            'waterfront', 'view', 'condition', 'grade',
            'sqft_above',
            'sqft_basement',
            'yr_built', 'yr_renovated']

Pour tester l’effet de la pénalité L1 sur les variables du modèle, on peut faire un premier essai avec une valeur élevée (1E10), sur toutes les variables, et afficher les coefficients.

#Création du modèle avec toutes les variables
model_all = graphlab.linear_regression.create(sales, target='price', features=all_features,validation_set=None,l2_penalty=0., l1_penalty=1e10)
print(model_all.coefficients.print_rows(num_rows=model_all.num_coefficients))
 On voit ici qu’avec cette pénalité, seules 5 variables sont non nulles, donc appliquer une pénalité assez forte permet de faire une sélection de variables.
+------------------+-------+---------------+--------+
|       name       | index |     value     | stderr |
+------------------+-------+---------------+--------+
|   (intercept)    |  None |  274873.05595 |  None  |
|     bedrooms     |  None |      0.0      |  None  |
| bedrooms_square  |  None |      0.0      |  None  |
|    bathrooms     |  None | 8468.53108691 |  None  |
|   sqft_living    |  None | 24.4207209824 |  None  |
| sqft_living_sqrt |  None | 350.060553386 |  None  |
|     sqft_lot     |  None |      0.0      |  None  |
|  sqft_lot_sqrt   |  None |      0.0      |  None  |
|      floors      |  None |      0.0      |  None  |
|  floors_square   |  None |      0.0      |  None  |
|    waterfront    |  None |      0.0      |  None  |
|       view       |  None |      0.0      |  None  |
|    condition     |  None |      0.0      |  None  |
|      grade       |  None | 842.068034898 |  None  |
|    sqft_above    |  None | 20.0247224171 |  None  |
|  sqft_basement   |  None |      0.0      |  None  |
|     yr_built     |  None |      0.0      |  None  |
|   yr_renovated   |  None |      0.0      |  None  |
+------------------+-------+---------------+--------+

Pour trouver une meilleure valeur de pénalité je vais séparer les données en sets de train/test/validation puis construire des modèles pour différentes valeurs de la pénalité dont on va mesurer le RSS.

#Création des jeux training, testing, validation
(training_and_validation, testing) = sales.random_split(.9,seed=1) # initial train/test split
(training, validation) = training_and_validation.random_split(0.5, seed=1) # On sépare training en train and validate

L1_RSS = graphlab.SFrame({"L1":[],"RSS":[]})
a = np.logspace(1,7,num=13) #On va tester des valeurs de pénalité L1 entre 10 et 10E7
RSS = 0.0
for l1_penalty in a:
    model = graphlab.linear_regression.create(training, target='price', features=all_features,validation_set=None,l2_penalty=0., l1_penalty=l1_penalty, verbose=False)
    prediction = model.predict(validation)
    RSS = ((validation['price'] - prediction)*(validation['price'] - prediction)).sum()
    print("L1 : %s ; RSS : $%.2f" %(l1_penalty, RSS))
    test = graphlab.SFrame({"L1":[l1_penalty],"RSS":[RSS]})
    L1_RSS = L1_RSS.append(test) #On stocke la valeur de pénalité et le RSS (residual sum of squares) associé

Le RSS le plus faible est obtenu pour une pénalité de 10, et cette fois nous voyons qu’aucun coefficient n’est annulé par le lasso :

+------------------+-------+------------------+--------+
|       name       | index |      value       | stderr |
+------------------+-------+------------------+--------+
|   (intercept)    |  None |  20459.2475219   |  None  |
|     bedrooms     |  None |  8155.38098737   |  None  |
| bedrooms_square  |  None |  1479.73787423   |  None  |
|    bathrooms     |  None |  24576.2383172   |  None  |
|   sqft_living    |  None |  37.4911504798   |  None  |
| sqft_living_sqrt |  None |  1109.39597073   |  None  |
|     sqft_lot     |  None | -0.0168499198461 |  None  |
|  sqft_lot_sqrt   |  None |  149.569423985   |  None  |
|      floors      |  None |  20983.5137368   |  None  |
|  floors_square   |  None |  12278.1023451   |  None  |
|    waterfront    |  None |  581971.306649   |  None  |
|       view       |  None |  92988.9899686   |  None  |
|    condition     |  None |  6924.28719657   |  None  |
|      grade       |  None |  6205.64105779   |  None  |
|    sqft_above    |  None |  41.3497390696   |  None  |
|  sqft_basement   |  None |   118.23242135   |  None  |
|     yr_built     |  None |  10.1881669529   |  None  |
|   yr_renovated   |  None |  58.7115840166   |  None  |
+------------------+-------+------------------+--------+

Pour finir, je veux construire un modèle avec un nombre de variables non nulles fixe, par exemple 7 :

max_nonzeros = 7 #Nombre de variables à sélectionner pour le modèle

l1_penalty_values = np.logspace(8, 10, num=20)#Le range de valeurs de pénalité à scanner

Nonzeros = graphlab.SFrame({"L1":[0.0],"Nb Nonzeros":[0]})
a = np.logspace(8,10,num=20)
for l1_penalty in a:
    model = graphlab.linear_regression.create(training, target='price', features=all_features,
                                              validation_set=None, 
                                              l2_penalty=0., l1_penalty=l1_penalty, verbose=False)
    nb_nz = graphlab.SFrame({"L1":[l1_penalty],"Nb Nonzeros":[model['coefficients']['value'].nnz()]})
    Nonzeros.append(nb_nz)
    print(l1_penalty,model['coefficients']['value'].nnz())

Nous obtenons une fenêtre de pénalité que nous allons raffiner encore un peu :

l1_penalty_min = 2976351441.6313128
l1_penalty_max = 3792690190.7322536
l1_penalty_values = np.linspace(l1_penalty_min,l1_penalty_max,20) #On raffine la valeur de la pénalité
a = np.linspace(l1_penalty_min,l1_penalty_max,20)
for l1_penalty in a:
    model3 = graphlab.linear_regression.create(training, target='price', features=all_features,
                                              validation_set=None, 
                                              l2_penalty=0., l1_penalty=l1_penalty, verbose=False)
    prediction3 = model3.predict(validation)
    RSS = ((validation['price'] - prediction3)*(validation['price'] - prediction3)).sum()
    print(l1_penalty,model3['coefficients']['value'].nnz(),RSS)
    if model3.coefficients['value'].nnz() == max_nonzeros: #Pour identifier automatiquement la bonne réponse
        print("7 non-zeros coefs")

Enfin, je peux créer un modèle de régression avec la valeur de pénalité pour laquelle le RSS est le plus faible (et n’ayant sélectionné que 7 variables non nulles) :

model_7nz = graphlab.linear_regression.create(training, target='price', features=all_features,validation_set=None, l2_penalty=0., l1_penalty=3448968612.1634364, verbose=False)
model_7nz.coefficients.print_rows(num_rows=answer.num_coefficients)

Au final, le modèle a retenu les variables/coefficients ci-dessous.

+------------------+-------+---------------+--------+
|       name       | index |     value     | stderr |
+------------------+-------+---------------+--------+
|   (intercept)    |  None | 222253.192544 |  None  |
|     bedrooms     |  None | 661.722717782 |  None  |
| bedrooms_square  |  None |      0.0      |  None  |
|    bathrooms     |  None | 15873.9572593 |  None  |
|   sqft_living    |  None | 32.4102214513 |  None  |
| sqft_living_sqrt |  None | 690.114773313 |  None  |
|     sqft_lot     |  None |      0.0      |  None  |
|  sqft_lot_sqrt   |  None |      0.0      |  None  |
|      floors      |  None |      0.0      |  None  |
|  floors_square   |  None |      0.0      |  None  |
|    waterfront    |  None |      0.0      |  None  |
|       view       |  None |      0.0      |  None  |
|    condition     |  None |      0.0      |  None  |
|      grade       |  None | 2899.42026975 |  None  |
|    sqft_above    |  None | 30.0115753022 |  None  |
|  sqft_basement   |  None |      0.0      |  None  |
|     yr_built     |  None |      0.0      |  None  |
|   yr_renovated   |  None |      0.0      |  None  |
+------------------+-------+---------------+--------+

En toute logique, des critères tels que nombre de chambres, de salles de bain ou la surface du bien sont les plus pertinents pour évaluer le prix.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.