Random Forest

Cet article explique les forêts aléatoires (Random Forest) en commentant le cours INTRODUCTION TO RANDOM FORESTS de Jeremy Howard (probablement le meilleur formateur au Machine Learning et au Deep Learning). Le principe de la formation retenue par Jeremy est toujours de partir du code, de l’expliquer, d’arriver ensuite à la théorie. On apprend en codant. Ceci suppose de savoir coder. Si vous ne le savez pas, alors cet article n’est pas pour vous. Le langage utilisé est Python. De préférence, la version 3.x (3.6) actuellement. Le code de l’exemple est ici : https://github.com/fastai/fastai/blob/master/courses/ml1/lesson1-rf.ipynb Anaconda est supposé installé. C’est un must have en machine learning. Vous avez le choix de travailler en local ou d’utiliser les services du Cloud spécialisé en ML/DL. Crestle et Paperspace sont recommandés. AWS est aussi une solution. L’exemple présenté ici est testé en local, sur Mac. Le code proposé par Jeremy ne marche pas sur Mac tel quel. Si vous rencontrez l’erreur : ‘No module names bcolz‘ alors la solution est ici et si vous rencontrez l’erreur « ResolvePackageNotFound: cuda90« , la solution est ici (I commented out cuda90 (=> #cuda90) in environment.yml file and conda env update works successfully) Il est fortement recommandé de lire le Wiki et les Notes. Le code exemple est commenté ci-dessous. Pour bien comprendre, il faut à la fois regarder le code, le cours de Jeremy et cet article. L’idéal est de regarder la vidéo du cours en même temps que cet article et d’avoir le code de l’exemple sous les yeux dans l’environnement Jupyter. Jupyter utilise IPython (commandes en lignes interactives).
IPython is an interactive command-line terminal for Python.

autoreload

autoreload permet de recharger automatiquement tout fichier qui aurait été modifié après le début du lancement de Jupyter. autoreload reloads modules automatically before entering the execution of code typed at the IPython prompt. %autoreload 2
Reload all modules (except those excluded by %aimport) every time before executing the Python code typed.
%load_ext autoreload
%autoreload 2
%matplotlib inline

Imports

Le langage Python recommande un style de programmation PEP 8 . Ce style dit ceci au niveau des imports :
Wildcard imports (from <module> import *) should be avoided, as they make it unclear which names are present in the namespace, confusing both readers and many automated tools
Ce n’est pas ce que fait le code proposé mais l’auteur a ses raisons (on  lui fait confiance !)
from fastai.imports import *
from fastai.imports import *
from fastai.structured import *
from pandas_summary import DataFrameSummary
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier
from IPython.display import display
from sklearn import metrics

Path

La commande !ls permet d’exécuter la commande ls (liste des fichiers du répertoire) En IPython, le  ! indique que la commande suivante sera exécutée par le shell.
When working interactively with the standard Python interpreter, one of the frustrations is the need to switch between multiple windows to access Python tools and system command-line tools. IPython bridges this gap, and gives you a syntax for executing shell commands directly from within the IPython terminal. The magic happens with the exclamation point: anything appearing after ! on a line will be executed not by the Python kernel, but by the system command-line.
Par ailleurs : Communication in the other direction–passing Python variables into the shell–is possible using the {varname} syntax:
PATH = "data/bulldozers/"
!ls {PATH}

Données

Les données sont récupérées sur Kaggle que connait bien l’auteur puisqu’il en a été le président. Il s’agit de prévoir le prix de bulldozers (SalePrice) vendus aux enchères. Oui c’est bizarre mais intéressant. On utilise pandas pour lire les données. Le paramètre parse_dates permet de spécifier qu’un champ est une date.
  • parse_dates : boolean or list of ints or names or list of lists or dict, default False
  • boolean. If True -> try parsing the index. list of ints or names. e.g. If [1, 2, 3] -> try parsing columns 1, 2, 3 each as a separate date column.
f'{PATH}Train.csv’ est du Python 3.x (et non une commande Jupyter). On obtient le contenu de la variable en la mettant entre crochets.
This PEP proposed to add a new string formatting mechanism: Literal String Interpolation. In this PEP, such strings will be referred to as « f-strings », taken from the leading character used to denote such strings, and standing for « formatted strings ». This PEP does not propose to remove or deprecate any of the existing string formatting mechanisms. F-strings provide a way to embed expressions inside string literals, using a minimal syntax. It should be noted that an f-string is really an expression evaluated at run time, not a constant value. In Python source code, an f-string is a literal string, prefixed with ‘f’, which contains expressions inside braces.
df_raw = pd.read_csv(f'{PATH}Train.csv', low_memory=False,
                     parse_dates=["saledate"])
Ne soyez pas surpris si la lecture des données dans le dataframe  prend quelques secondes. Il y a plus de 40 000 lignes. low_memory=false n’est peut-être plus utile, d’après ici.
pandas will guess the data type chunk by chunk if low_memory is True, so each item in a single column might have different types. However, the fix is pretty easy: stating dtype explicitly.
Le choix est de ne pas faire d’EDA (Exploratory Data Analysis) afin d’éviter tout risque d’overfitting.

Display

Pour rappel, .T transpose les données et .tail commence par la fin. display est une commande IPython.
def display_all(df):
    with pd.option_context("display.max_rows", 1000, "display.max_columns", 1000):
        display(df)
display_all(df_raw.tail().T)
display.max_rows : This sets the maximum number of rows pandas should output when printing out various output. For example, this value determines whether the repr() for a dataframe prints out fully or just a summary repr. ‘None’ value means unlimited. display.max_columns : max_rows and max_columns are used in __repr__() methods to decide if to_string() or info() is used to render an object to a string. In case Python/IPython is running in a terminal this is set to 0 by default and pandas will correctly auto-detect the width of the terminal and switch to a smaller format in case all columns would not fit vertically. The IPython notebook, IPython qtconsole, or IDLE do not run in a terminal and hence it is not possible to do correct auto-detection, in which case the default is set to 20. ‘None’ value means unlimited.
Le résultat : [table id=1 /] La variable dépendante est SalePrice.

Describe

Pandas permet de présenter les statistiques descriptives qui résument les formes de la distribution. Le résultat : [table id=2 /]

Métrique

La métrique est imposée.
The evaluation metric for this competition is the RMSLE (root mean squared log error) between the actual and predicted auction prices.
Tout ça paraît logique :
The reason we use log is because generally, you care not so much about missing by $10 but missing by 10%. So if it was $1000,000 item and you are $100,000 off or if it was a $10,000 item and you are $1,000 off — we would consider those equivalent scale issues.
L’objectif est d’obtenir un modèle qui minimise les écarts de log. On s’intéresse au log du prix et non au prix lui même tout simplement parce qu’un écart en % a plus de sens dans notre cas qu’un écart en valeur absolue. Le ratio est plus significatif que l’écart de prix.
df_raw.SalePrice = np.log(df_raw.SalePrice)
Pour cela, on utilise NumPy. log est ici le logarithme népérien.

RandomForestRegressor

Avant d’expliquer Random Forest, le cours démonte deux idées reçues : En résumé, utiliser beaucoup de colonnes n’est pas un problème et les données ne sont pas si aléatoires qu’on l’imagine. RandomForestRegressor est en fait sklearn.ensemble.RandomForestRegressor. n_jobs=-1pour utiliser tous les processeurs. Si vous ne pouvez pas attendre pour en savoir plus sur RandomForestRegressor, RdV ici pour la fonction et lisez le commentaire pour les forêts aléatoires :

Random forest is a universal machine learning technique.

  • It can predict something that can be of any kind — it could be a category (classification), a continuous variable (regression).
  • It can predict with columns of any kind — pixels, zip codes, revenues, etc (i.e. both structured and unstructured data).
  • It does not generally overfit too badly, and it is very easy to stop it from overfitting.
  • You do not need a separate validation set in general. It can tell you how well it generalizes even if you only have one dataset.
  • It has few, if any, statistical assumptions. It does not assume that your data is normally distributed, the relationship is linear, or you have specified interactions.
  • It requires very few pieces of feature engineering. For many different types of situation, you do not have to take the log of the data or multiply interactions together.
m = RandomForestRegressor(n_jobs=-1)
  • RandomForestRegressor — regressor is a method for predicting continuous variables (i.e. regression)
  • RandomForestClassifier — classifier is a method for predicting categorical variables (i.e. classification)

fit

Everything in scikit-learn has the same form.

Create an instance of an object for the machine learning model Call fit by passing in the independent variables (the things you are going to use to predict) and dependent variable (the thing you want to predict). axis=1 means remove columns.
Le code proposé présente à la suite une ligne qui génère une erreur :
m.fit(df_raw.drop('SalePrice', axis=1), df_raw.SalePrice)
fit est une méthode qui crée une forêt à partir de données. fit(X, y[, sample_weight])Build a forest of trees from the training set (X, y). drop (pandas – DataFrame) supprime une ligne ou une colonne dans un tableau de données. En l’occurence, c’est la colonne SalePrice. Le tableau de données des variables indépendantes est tout le tableau moins la colonne SalePrice et la variable dépendante est la colonne SalePrice. Ce code génère une erreur car toutes les données ne sont pas numériques.

Dates

Comme on ne peut pas utiliser la date de la vente telle quelle, alors on crée d’autres champs, comme par exemple : saleDayofweek, saleDayofyear.
add_datepart(df_raw, 'saledate')
df_raw.saleYear.head()

train_cats

Il ne suffit pas de remplacer les dates par d’autre champs. Il faut le faire pour toutes les strings. Fastai propose pour cela train_cats.
Pandas has a concept of a category data type, but by default it would not turn anything into a category for you. Fast.ai provides a function called train_cats which creates categorical variables for everything that is a String. Behind the scenes, it creates a column that is an integer and it is going to store a mapping from the integers to the strings. train_cats is called “train” because it is training data specific. It is important that validation and test sets will use the same category mappings (in other words, if you used 1 for “high” for a training dataset, then 1 should also be for “high” in validation and test datasets).
train_cats(df_raw)
df_raw.UsageBand.cat.categories

df_raw.UsageBand.cat.categories

On fait ici le remplacement de catégories.
df_raw.UsageBand.cat.categories
df_raw.UsageBand.cat.set_categories(['High', 'Medium', 'Low'], ordered=True, inplace=True)
df_raw.UsageBand = df_raw.UsageBand.cat.codes
df_raw.UsageBand.cat — Similar tofld.dt.year , .cat gives you access to things assuming something is a category. The order does not matter too much, but since we are going to be creating a decision tree that split things at a single point (i.e. High vs. Low and Medium , High and Low vs. Medium ) which is a little bit weird. To order them in a sensible manner, you can do the following: inplace will ask Pandas to change the existing dataframe rather than returning a new one. There is a kind of categorical variable called “ordinal”. An ordinal categorical variable has some kind of order (e.g. “Low” < “Medium” < “High”). Random forests are not terribly sensitive for that fact, but it is worth noting.
Ce code n’est pas commenté ici car il n’apporte rien à la compréhension de Random Forests.

Valeurs manquantes

En exécutant la commande suivante, on constate qu’il existe de nombreuses valeurs manquantes.
display_all(df_raw.isnull().sum().sort_index()/len(df_raw))

Sauvegarde

Pour sauvegarder et relire les données :
os.makedirs('tmp', exist_ok=True)
df_raw.to_feather('tmp/bulldozers-raw')
df_raw = pd.read_feather('tmp/bulldozers-raw')
Le format utilisé est feather format, disponible ici.
Feather provides binary columnar serialization for data frames. It is designed to make reading and writing data frames efficient, and to make sharing data across data analysis languages easy.

Fin de la mise à jour des données

df, y, nas = proc_df(df_raw, 'SalePrice')
On obtient cette fois des données qui peuvent être exploitées.
We will replace categories with their numeric codes, handle missing continuous values, and split the dependent variable into a separate variable.

Random ForestRegressor

Cette fois on peut recommencer RandomForest – avec des données nettoyées.
m = RandomForestRegressor(n_jobs=-1)
m.fit(df, y)
m.score(df,y)

score

La méthode score de sklearn.ensemble.RandomForestRegressor retourne le coefficient de détermination R^2 de la prédiction.
En statistique, le coefficient de détermination, noté R2 ou r2, est une mesure de la qualité de la prédiction d’une régression linéaire.
score(X, y[, sample_weight]) La valeur calculée est 0.9829

Création d’un jeu de données de validation

A partir d’ici, le cours de Jeremy est le #2.
def split_vals(a,n): return a[:n].copy(), a[n:].copy()
n_valid = 12000  # same as Kaggle's test set size
n_trn = len(df)-n_valid
raw_train, raw_valid = split_vals(df_raw, n_trn)
X_train, X_valid = split_vals(df, n_trn)
y_train, y_valid = split_vals(y, n_trn)
X_train.shape, y_train.shape, X_valid.shape
Les données sont désormais réparties de la façon suivante :
((389125, 66), (389125,), (12000, 66))

RMSE

def rmse(x,y): return math.sqrt(((x-y)**2).mean())
def print_score(m):
    res = [rmse(m.predict(X_train), y_train), rmse(m.predict(X_valid), y_valid),
                m.score(X_train, y_train), m.score(X_valid, y_valid)]
    if hasattr(m, 'oob_score_'): res.append(m.oob_score_)
    print(res)
RandomForestRegressor a parmi ses méthodes, predict et score.
  • predict(X) : Predict regression target for X
  • score(X, y[, sample_weight]) : Returns the coefficient of determination R^2 of the prediction

Le modèle est-il bon ?

m = RandomForestRegressor(n_jobs=-1)
%time m.fit(X_train, y_train)
print_score(m)
Nous obtenons les résultats :
CPU times: user 1min 36s, sys: 1.1 s, total: 1min 37s
Wall time: 38 s
[0.09041100745630692, 0.24655831325562264, 0.9829164767377156, 0.8914356404718409]

Un seul arbre

Une forêt est composée d’arbres. Voyons ce que ça donne avec un seul arbre. n_estimators = 1 veut dire un seul arbre.
m = RandomForestRegressor(n_estimators=1, max_depth=3, bootstrap=False, n_jobs=-1)
m.fit(X_train, y_train)
print_score(m)
Le résultat obtenu n’est pas terrible.
0.537126968334353, 0.5674541015387411, 0.3970396664995463, 0.42494490873716684]
mais on peut l’afficher. On peut faire mieux en approfondissant l’arbre :
m = RandomForestRegressor(n_estimators=1, bootstrap=False, n_jobs=-1)
m.fit(X_train, y_train)
print_score(m)
mais on overfits.
[3.86639454513118e-06, 0.49783340765277984, 0.9999999999674949, 0.5573952727621732]

Bagging

L’idée est de travailler sur un échantillon des données, d’effectuer un TreeClassier profond qui fera de l’overfit, et d’itérer de nombreuses fois sur différents échantillons. Par défaut, le nombre d’arbres est 10.
m = RandomForestRegressor(n_jobs=-1)
m.fit(X_train, y_train)
print_score(m)
le résultat :
[0.11412465433392749, 0.3578551760274063, 0.971679615586951, 0.7713018874011159]

Prédiction sur chaque arbre

preds = np.stack([t.predict(X_valid) for t in m.estimators_])
preds[:,0], np.mean(preds[:,0]), y_valid[0]
Le résultat :
(array([9.21034, 8.9872 , 9.13238, 9.04782, 9.3501 , 9.04782, 9.04782, 9.13238, 9.13238, 9.13238]),
 9.12206191564506,9.104979856318357)
On peut constater que plus il y a d’arbres, meilleur est r2
preds.shape
(10, 12000)
plt.plot([metrics.r2_score(y_valid, np.mean(preds[:i+1], axis=0)) for i in range(10)]);

Augmentation du nombre d’arbres

Augmenter le nombre d’arbres dans la forêt est efficace…jusqu’à un certain point : Avec 20 arbres :
m = RandomForestRegressor(n_estimators=20, n_jobs=-1)
m.fit(X_train, y_train)
print_score(m)
[0.10301167382085773, 0.3511542522116204, 0.9769265210111048, 0.7797865517170914]
Avec 40 arbres :
m = RandomForestRegressor(n_estimators=40, n_jobs=-1)
m.fit(X_train, y_train)
print_score(m)
[0.09759366332685247, 0.348192247308046, 0.9792898409496985, 0.7834859079368047]
Avec 80 arbres
m = RandomForestRegressor(n_estimators=80, n_jobs=-1)
m.fit(X_train, y_train)
print_score(m)
[0.0945547996014559, 0.34078301827539453, 0.9805595035690792, 0.792602334222947]
Ce 1er cours s’arrête ici – ce qui correspond à 1:12 de la vidéo de la lesson 2. Les autres points, plus techniques seront abordés dans le prochain article.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.