Certaines techniques d’optimisations sont spécifiques aux différents algorithmes:
D’autres vont pouvoir être applicable dans la majorité des algorithmes, si ce n’est tous:
Pour rappeler l’intérêt du dataset de validation:
Pour pouvoir évaluer les performances du modèle, on partitionne l’ensemble de données de départ en deux: train/test set. Si entre les deux, on veut adjuster le modèle (par exemple modifier les hyperparamètres pour améliorer les performances du modèle), alors on utilise un troisième groupe: le validation set. On a donc
un train set,
utilisé pour entraîner le modèle,
un validation set,
utilisé pour optimiser le modèle, c’est à dire évaluer le modèle et faire des changements en conséquence pour améliorer ses performances,
un test set,
utilisé pour vérifier les performances du modèle sur des données qu’il n’a jamais vu auparavant.
Sauf qu’en partitionnant l’ensemble de données en trois sous-ensembles, on réduit considérablement le nombre de données qui peuvent être utilisées pour entraîner le modèle. Si l’ensemble de données de départ était déjà relativement petit, le train set sera probablement trop petit pour pouvoir construire un bon modèle.
Autre risque: si le validation set est petit, il risque de ne pas être fiable — si on utilise 2-3 données pour comparer les performances de différents modèle, que l’un ait de meilleures prédictions que l’autre sera surtout une question de chance.
Pour parer ce problème, on peut utiliser la cross-validation (validation croisée en français).
La cross-validation K-fold consiste à exécuter l’algorithme et évaluer ses performances sur différents sous-ensembles de données comme suit:
En additionnant toutes ces itérations, 100% des données (hormi le test set) sont utilisées pour l’entraînement, et le score de performance est plus fiable, puisque c’est une moyenne calculée sur différents sous-ensembles.
Si l’ensemble de données a un ordre chronologique, alors on ne peut pas utiliser la cross-validation K-fold, mais on peut utiliser une technique de forward chaining: on entraîne le modèle sur des données passées puis on teste ses performances sur des données futures:
La méthode jackknife (ou leave-one-out, LOO) est un cas extrême du k-fold où K=n: on utilise n sous-ensembles, en supprimant une seule valeur à la fois — on calcule l’ajustement en utilisant n-1 données et on vérifie la fiabilité des résultat avec la valeur qu’on a mise de côté.
Le up-sampling (sur-échantillonage en français) consiste à dupliquer aléatoirement des observations d’une classe minoritaire pour renforcer son signal.
avec remplacement:
La probabilité de choisir l’élément x après avoir tiré l’élément y est 1/n: chaque tirage est indépendant, l’un n’influence pas l’autre. Mathématiquement, ça veut dire que la covariance entre les deux est 0.
P(John, John) = (1/7) * (1/7) = .02
P(John, Jack) = (1/7) * (1/7) = .02
P(John, Qui) = (1/7) * (1/7) = .02
P(Jack, Qui) = (1/7) * (1/7) = .02
P(Jack Tina) = (1/7) * (1/7) = .02
sans remplacement:
La probabilité de choisir l’élément x après avoir tiré l’élément y est 1/(n-1): tirer un élément rend plus probable le fait de tirer un autre élément. La covariance n’est pas 0.
P(John, Jack) = (1/7) * (1/6) = .024
P(John, Qui) = (1/7) * (1/6) = .024
P(Jack, Qui) = (1/7) * (1/6) = .024
P(Jack Tina) = (1/7) * (1/6) = .024
La façon la plus courante de procéder est de:
from sklearn.utils import resample
# Separate majority and minority classes
df_majority = df[df.balance==0]
df_minority = df[df.balance==1]
# Upsampling minority class
df_minority_up = resample(
df_minority,
replace=True,
n_samples=len(df_majority),
random_state=0
)
# Combine majority class with upsampled minority class
df_up = pd.concat([df_majority, df_minority_up])
# Display new class counts
df_up.balance.value_counts()
La technique SMOTE (Synthetic Minority Oversampling Technique) consiste à créer de nouvelles données minoritaires entre les données minoritaires existantes.
Pour chacunes des données voisines sélectionnées (b):
!pip install imbalanced-learn
from imblearn.over_sampling import SMOTE
smote = SMOTE()
X, y = smote.fit_resample(X, y)
Down-sampling (sous-échantillonage en français) consiste à supprimer aléatoirement des observations d’une classe majoritaire pour éviter que son signal domine.
La façon la plus courante de procéder est de:
from sklearn.utils import resample
# Separate majority and minority classes
df_majority = df[df.balance==0]
df_minority = df[df.balance==1]
# Downsampling majority class
df_majority_down = resample(
df_majority,
replace=False,
n_samples=len(df_minority),
random_state=0
)
# Combine minority class with downsampled majority class
df_down = pd.concat([df_majority_down, df_minority])
# Display new class counts
df_down.balance.value_counts()
Le NearMiss consiste à supprimer les données majoritaires les plus éloignées de la classe minoritaire — pour garder un maximum de points proches de la limite de décision.
Calculer les distances entre les observations de la classe majoritaire et les observations de la classe minoritaire (en utilisant K-Nearest Neighbors).
Garder les observations de la classe majoritaire qui ont la distance la plus courte avec la classe minoritaire. Supprimer les plus éloignées.
Pour aller plus loin:
imbalanced-learn API
Techniques to handle Class Imbalance in data
Pandas resample() tricks for time-series data
Différents algorithmes vont avoir différents hyperparamètres, des paramètres qui ne sont pas appris par l’algorithme mais définis par le développeur — par exemple: la fonction d’hypothèse, la fonction coût, la fonction d’optimisation, le nombre K à utiliser dans K-means, etc.
Quand on utilise un framework tel que Keras ou scikit-learn, les hyperparamètres ont une valeur par défaut. On commence généralement avec les valeurs par défaut et, après évaluation, on vient modifier les hyperparamètres. Le but étant de trouver la combinaison d’hyperparamètes avec laquelle le modèle est le plus performant. Pour ce faire, on peut utiliser:
L’approche manuelle
Modifier différents hyperparamètres manuellement, avec une logique de trial and error.
Grid Search
Dresser une liste des différentes valeurs d’hyperparamètres envisagées. Toutes les combinaisons possibles sont testées et la meilleure est sélectionnée.
from sklearn.model_selection import GridSearchCV
param_grid = {
"n_estimators": [100, 200, 300, 400],
"max_depth": [1, 3, 5, 7, 9],
"criterion": ["gini", "entropy"],
}
model = GridSearchCV(
estimator=rf_classifier,
param_grid=param_grid,
cv=5,
verbose=2,
n_jobs=1
)
model.fit(X_scaled,y)
print(grid.best_estimator_)
print(grid.best_score_)
print(grid.best_params_)
GridSearchCV.ipynb
Random Search
Teste des combinaisons aléatoires de valeurs, la meilleure est sélectionnée.
from sklearn.model_selection import RandomizedSearchCV
param_grid = {
"n_estimators": [100, 200, 300, 400],
"max_depth": [1, 3, 5, 7, 9],
"criterion": ["gini", "entropy"],
}
model = RandomizedSearchCV(
estimator=rf_classifier,
param_distributions=param_grid,
n_iter=5,
scv=5,
verbose=2,
n_jobs=1,
random_state=42
)
model.fit(X_scaled,y)
RandomizedSearchCV.ipynb
Optimisation Bayesienne
Avec Grid Search et Random Search, chaque estimation d’hyperparamètre est indépendante. L’approche bayesienne construit une distribution de probabilité avec les performances passées pour estimer la performance du modèle avec certains choix d’hyperparamètres.
!pip install scikit-optimize
from skopt.searchcv import BayesSearchCV
params = {
"n_estimators": [100, 200, 300, 400],
"max_depth": (1, 9),
"criterion": ["gini", "entropy"],
}
search = BayesSearchCV(
estimator=rf_classifier,
search_spaces=params,
n_jobs=1,
cv=5,
n_iter=30,
scoring="accuracy",
verbose=4,
random_state=42
)
search.fit(X_scaled,y)
print(search.best_score_)
print(search.best_params_)
Pour aller plus loin:
Hyperparameter Optimization Techniques
Bayesian Optimization
Un modèle aura toujours ses forces et ses faiblesses — il reconnaîtra certaines tendances, mais passera à côté des autres. Pour rendre les prédictions plus fiables, on peut construire différents modèles et les laisser “voter” la prédiction finale. Ça permet d’exploiter les points forts de différents modèles et les résultats seront généralement meilleurs que les résultats d’un seul modèle. Plusieurs approches existent pour parvenir à ce but.
(aka boostrap aggregating, parallel ensemble learning)
On divise l’ensemble de données en différents sous-ensembles, et on s’en sert pour entraîner parallélement différents modèles du même type. On utilise une méthode statistique (typiquement la moyenne) pour combiner les différentes prédictions en une seule.
Ex: Bagged Decision Trees, Extra Trees
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import BaggingClassifier
model_log = LogisticRegression(solver='liblinear')
bagging_log = BaggingClassifier(
base_estimator=model_log,
n_estimators=12
)
bagging_log.fit(X_train, y_train)
print('Accuracy Score on train:', bagging_log.score(X_train, y_train))
print('Accuracy Score on test:', bagging_log.score(X_test, y_test))
from sklearn.ensemble import ExtraTreesClassifier
model_et = ExtraTreesClassifier(n_estimators=8)
model_et.fit(X_train, y_train)
y_pred = model_et.predict(X_test)
print('Accuracy Score:', accuracy_score(y_test, y_pred))
Random Forest est une technique d’ensemble spécifiques aux Decision Tree. Là où une technique de bagging sélectionne des observations de manière aléatoire pour construire des modèles, Random Forest sélectionne des caractéristiques au hasard. Différents arbres vont donc choisir différentes caractéristiques pour partitionner les données. La prédiction finale est calculée en calculant la réponse moyenne des arbres.
Random Forest peut être utilisé comme une finalité (effectuer une prédiction à partir de différents modèles) mais aussi comme outil pour identifier les caractéristiques les plus pertinentes.
# Random Forest
model = RandomForestClassifier(
n_estimators=1000,
max_features=2,
oob_score=True
)
model.fit(X=X, y=y)
# Feature importance
df_rank = pd.DataFrame(
data=[],
columns=['Feature', 'Importance'])
for feature, imp in zip(X.columns, model.feature_importances_):
df_rank.loc[len(df_rank)] = [feature, imp]
df_rank.sort_values(by=['Importance'], ascending=False)
(aka sequential ensemble learning)
On entraîne un modèle, détermine quelles observations ont été mal prédites, puis on entraîne un nouveau modèle sur ces observations, en espérant qu’il leur accorde plus d’attention et que ses prédictions soient correctes. Chaque modèle apprend à partir des erreurs des précedents.
Ex: AdaBoost, Gradient Boosting Machine (GBM), Extreme Gradient Boosting (XGBoost)
from sklearn.ensemble import AdaBoostClassifier
boosting_log = AdaBoostClassifier(
base_estimator=model_log,
n_estimators=10
)
boosting_log.fit(X_train, y_train)
print('Accuracy Score on train:', boosting_log.score(X_train, y_train))
print('Accuracy Score on test:', boosting_log.score(X_test, y_test))
from sklearn.ensemble import GradientBoostingClassifier
gboosting_log = GradientBoostingClassifier()
gboosting_log.fit(X_train, y_train)
y_pred = gboosting_log.predict(X_test)
print('Accuracy Score:', accuracy_score(y_test, y_pred))
AdaBoost, Gradient Boost, XGBoost
Ce n’est pas une technique d’ensemble — mais on en entend généralement parler dans ce contexte. On entraîne différents types de modèles en parallèle et on choisit celui qui a les meilleures performances.
(aka voting)
On entraîne différents types de modèles en parallèle et on combine leur résultat via une méthode statistique (comme la moyenne ou le mode).
from sklearn.ensemble import VotingClassifier
model1 = LogisticRegression(random_state=1)
model2 = DecisionTreeClassifier(random_state=1)
stacking = VotingClassifier(estimators=[
('lr', model1),
('dt', model2)
], voting='hard')
stacking.fit(X_train,y_train)
stacking.score(X_test,y_test)