API de sciki-learn et modèles customisés#
Links: notebook
, html, PDF
, python
, slides, GitHub
scikit-learn est devenu le module incontournable quand il s’agit de machine learning. Cela tient en partie à son API épurée qui permet à quiconque d’implémenter ses propres modèles tout permettant à scikit-learn de les manipuler comme s’il s’agissait des siens.
from jyquickhelper import add_notebook_menu
add_notebook_menu(last_level=3)
Cette présentation détaille l’API de scikit-learn, aborde la mise en production avec pickle, montre un exemple d’implémentation d’un modèle customisé appliqué à la sélection d’arbres dans une forêt aléatoire.
import matplotlib.pyplot as plt
from jupytalk.pres_helper import show_images
Design et API#
On peut penser que deux implémentations du même algorithme se valent à partir du moment où elles produisent les mêmes résultats. Voici deux chaises, vers laquelle votre instinct vous poussera-t-il ?
show_images("zigzag.jpg", "chaise.jpg", figsize=(14, 4), title2="Le Corbusier");

Quatre ou cinq librairies ont fait le succès de Python#
numpy: calcul matriciel - existait avant Python (matlab, R, …)
pandas: manipulation de données - existait avant Python (R, …)
matplotlib: graphes - existait avant Python - (matlab, R…)
scikit-learn: machine learning - innovation : design
jupyter: notebooks - innovation : mélange interactif code, texte, images
show_images("trends.png", title1="Google Trendss Python / Matlab");

Machine learning résumé#
Modèle de machine learning = résultat d’une optimisation
Cette optimisation dépend de paramètres (dimension, pas du gradient, …)
Optimisation = apprentissage
On s’en sert pour faire de la prédiction.
Ce que les codeurs imaginent#
Des designs souvent très jolis mais à usage unique.
show_images("coop.jpg", "coop2.jpg", title1="Coop Himeblau", title2="Rooftop", figsize=(16,8));

Vues incompatibles#
Les chercheurs aiment l’innonvation, cherchent de nouveaux modèles.
Les datascientist assemblent des modèles existants.
L’estimation d’un modèle arrivent à la toute fin.
On retient facilement ce qui est court et qui se répète.
Vocabulaire scikit-learn#
Predictor : modèle de machine learning qu’on apprend (
fit
) et qui prédit (predict
)Transformer : prétraitement de données qui précède un prédicteur, qu’on apprend (
fit
) et qui transforme les données (transform
)
Utilisation de classes : predictor#
- ::
- class Predictor:
- def __init__(self, **kwargs):
# kwargs sont les paramètres d’apprentissage
- def fit(self, X, y):
# apprentissage return self
- def predict(self, X):
# prédiction
Utilisation de classes : transformer#
- ::
- class Transformer:
- def __init__(self, **kwargs):
# kwargs sont les paramètres d’apprentissage
- def fit(self, X, y):
# apprentissage return self
- def transform(self, X):
# prédiction
pipeline (sandwitch en français)#
Normalisation + ACP + Régression Logistique
Classe |
Step 1 |
Step 2 |
Step 3 |
Step 4 |
---|---|---|---|---|
Normalizer |
|
|
|
|
PCA |
. |
|
|
|
LogisticReg ression |
. |
. |
``fit(X3,y) `` |
|
En langage Python#
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import Normalizer
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
pipe = Pipeline([
('norm', Normalizer()),
('pca', PCA()),
('lr', LogisticRegression())
])
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
data = load_iris()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(X, y)
pipe.fit(X_train, y_train)
Pipeline(memory=None,
steps=[('norm', Normalizer(copy=True, norm='l2')),
('pca',
PCA(copy=True, iterated_power='auto', n_components=None,
random_state=None, svd_solver='auto', tol=0.0,
whiten=False)),
('lr',
LogisticRegression(C=1.0, class_weight=None, dual=False,
fit_intercept=True, intercept_scaling=1,
l1_ratio=None, max_iter=100,
multi_class='auto', n_jobs=None,
penalty='l2', random_state=None,
solver='lbfgs', tol=0.0001, verbose=0,
warm_start=False))],
verbose=False)
prediction = pipe.predict(X_test)
prediction[:5]
array([2, 2, 0, 2, 2])
pipe.score(X_test, y_test)
0.6578947368421053
Raffinement#
show_images("church-of-light-1024x614.jpg", title1="Tadao Ando", figsize=(10, 6));

Un design commun aux régresseurs et classifieurs#
Les régresseurs sont les plus simples, ils modèlisent une fonction
.
Les classifieurs modélisent une fonction
Mais
Les classifieurs sont liés à la notion de distance par rapport à la frontière, distance qu’on relie ensuite à une probabilité mais pas toujours.
show_images('logreg.png');

Besoin d’un classifieur#
- ::
- class Classifier:
- def __init__(self, **kwargs):
# kwargs sont les paramètres d’apprentissage
- def fit(self, X, y):
# apprentissage return self
- def decision_function(self, X):
# distances
- def predict_proba(self, X):
# distances –> proba
- def predict(self, X):
# classes
Besoin d’un régresseur par mimétisme#
- ::
- class Classifier:
- def __init__(self, **kwargs):
# kwargs sont les paramètres d’apprentissage
- def fit(self, X, y):
# apprentissage return self
- def decision_function(self, X):
# une ou plusieurs régressions
- def predict(self, X):
# moyennes
Paramètres et résultats d’apprentissage#
Tout attribut terminé par
_
est un résultat d’apprentissage.A l’opposé, tout ce qui ne se termine pas par
_
est connu avant l’apprentissage
show_images("lasso.png");

Problèmes standards - moule commun#
show_images('sklearn_base.png');

Analyser ou prédire#
Certains modèles ne peuvent pas prédire, simplement analyser. C’est le cas du SpectralClustering.
- ::
- class NoPredictionButAnalysis:
- def __init__(self, **kwargs):
# kwargs sont les paramètres d’apprentissage
- def fit_predict(self, X, y=None):
# apprentissage et prédiction return self
Limites du concept#
Et si on veut réutiliser les sorties d’un prédicteur pour en faire autre chose ?
A suivre… dans la dernière partie.
Le design, c’est le design, le code, c’est de la bidouille.
pickle#
Un modèle c’est :
une classe, un pipeline, une liste de traitements définis avant apprentissage
des coefficients obtenus après apprentissage
Comment conserver le résultat ? –> pickle
Cas des dataframes#
from pandas import DataFrame, read_csv
df = DataFrame(X)
df['label'] = y
df.head()
0 | 1 | 2 | 3 | label | |
---|---|---|---|---|---|
0 | 5.1 | 3.5 | 1.4 | 0.2 | 0 |
1 | 4.9 | 3.0 | 1.4 | 0.2 | 0 |
2 | 4.7 | 3.2 | 1.3 | 0.2 | 0 |
3 | 4.6 | 3.1 | 1.5 | 0.2 | 0 |
4 | 5.0 | 3.6 | 1.4 | 0.2 | 0 |
df.to_csv("data_iris.csv")
%timeit read_csv("data_iris.csv")
3.4 ms ± 217 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
import pickle
with open("data_iris.pickle", "wb") as f:
pickle.dump(df, f)
def load_from_pickle(name):
with open(name, "rb") as f:
return pickle.load(f)
%timeit load_from_pickle("data_iris.pickle")
874 µs ± 35.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
pickle est plus rapide#
read_csv : convertit un fichier texte en dataframe –> format intermédiaire csv
pickle : conserve des données comme elles sont stockées en mémoire –> pas de conversion
from jyquickhelper import RenderJsDot
RenderJsDot('''digraph{ rankdir="LR";
B [label="mémoire"]; C [label="csv"]; C2 [label="csv"];
D [label="disque"]; B -> C [label="to_csv", color="red"];
C -> D ; D -> C2 ;
C2 -> B [label="read_csv", color="red"];
B -> D [label="pickle.dump", color="blue"];
D -> B [label="pickle.load", color="blue"];
}''')
scikit-learn, pickle#
unique moyen de conserver les modèles
with open("pipe.pickle", "wb") as f:
pickle.dump(pipe, f)
with open("pipe.pickle", "rb") as f:
pipe2 = pickle.load(f)
from numpy.testing import assert_almost_equal
assert_almost_equal(pipe.predict(X_test), pipe2.predict(X_test))
Problème avec pickle#
L’état de la mémoire dépend très fortement des librairies installées
Changer de version scikit-learn –> l’état de la mémoire est différente
Analogie : pickle ne conserve que les coefficients en mémoire, ils sont cryptés en quelque sorte.
On ne peut les décrypter qu’avec le même code.
Dissocier les colonnes#
Toutes les colonnes subissent le même traitement.
pipe = Pipeline([
('norm', Normalizer()),
('pca', PCA()),
('lr', LogisticRegression())
])
Mais ce n’est pas forcément ce que l’on veut.
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import MinMaxScaler
pipe2 = Pipeline([
('multi', ColumnTransformer([
('c01', Normalizer(), [0, 1]),
('c23', MinMaxScaler(), [2, 3]),
])),
('pca', PCA()),
('lr', LogisticRegression())
])
pipe2.fit(X_train, y_train);
from mlinsights.plotting import pipeline2dot
RenderJsDot(pipeline2dot(pipe2, X_train))
Concepts appliqués à un nouveau régresseur#
On construit une forêt d’arbres puis on réduit le nombre d’arbres à l’aide d’une régression Lasso.
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
data = load_boston()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(X, y)
Sketch de l’algorithme#
import numpy
from sklearn.ensemble import RandomForestRegressor
from sklearn.linear_model import Lasso
# Apprentissage d'une forêt aléatoire
clr = RandomForestRegressor()
clr.fit(X_train, y_train)
# Récupération de la prédiction de chaque arbre
X_train_2 = numpy.zeros((X_train.shape[0], len(clr.estimators_)))
estimators = numpy.array(clr.estimators_).ravel()
for i, est in enumerate(estimators):
pred = est.predict(X_train)
X_train_2[:, i] = pred
# Apprentissage d'une régression Lasso
lrs = Lasso(max_iter=10000)
lrs.fit(X_train_2, y_train)
lrs.coef_
array([0. , 0. , 0.02869904, 0. , 0. ,
0.01230231, 0. , 0. , 0.06268181, 0. ,
0.04434885, 0.04454832, 0. , 0. , 0. ,
0. , 0.00328847, 0. , 0. , 0. ,
0.01964477, 0.02122032, 0. , 0.03659488, 0. ,
0. , 0.01859637, 0. , 0. , 0. ,
0. , 0.08754916, 0. , 0. , 0. ,
0. , 0.01080401, 0. , 0.03181241, 0. ,
0.01764386, 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0.07638484,
0. , 0. , 0. , 0. , 0. ,
0.00960185, 0.03188872, 0. , 0.04100308, 0. ,
0. , 0. , 0. , 0. , 0.03538388,
0.02744132, 0.03044355, 0.05237069, 0.02037819, 0.01031177,
0. , 0. , 0. , 0.05316585, 0. ,
0. , 0. , 0.04871499, 0. , 0. ,
0.00355434, 0. , 0. , 0. , 0. ,
0.03507367, 0.03236543, 0. , 0. , 0.01899959,
0. , 0.01047442, 0. , 0. , 0. ,
0.01293732, 0. , 0. , 0.01444821, 0.01773635])
Ce que l’on veut#
- ::
- class LassoRandomForestRegressor:
- def fit(self, X, y):
# apprendre une random forest # sélectionner les arbres à garder avec un Lasso # supprimer les arbres associés à un poids nul return self
- def predict(self, X):
# retourner une moyenne pondérée des prédictions return …
Implémentation#
lasso_random_forest_regressor.py
import numpy
from sklearn.base import BaseEstimator, RegressorMixin, clone
from sklearn.ensemble import RandomForestRegressor
from sklearn.linear_model import Lasso
class LassoRandomForestRegressor(BaseEstimator, RegressorMixin):
def __init__(self, rf_estimator=None, lasso_estimator=None):
BaseEstimator.__init__(self)
RegressorMixin.__init__(self)
if rf_estimator is None:
rf_estimator = RandomForestRegressor()
if lasso_estimator is None:
lasso_estimator = Lasso()
self.rf_estimator = rf_estimator
self.lasso_estimator = lasso_estimator
def fit(self, X, y, sample_weight=None):
self.rf_estimator_ = clone(self.rf_estimator)
self.rf_estimator_.fit(X, y, sample_weight)
estims = self.rf_estimator_.estimators_
estimators = numpy.array(estims).ravel()
X2 = numpy.zeros((X.shape[0], len(estimators)))
for i, est in enumerate(estimators):
pred = est.predict(X)
X2[:, i] = pred
self.lasso_estimator_ = clone(self.lasso_estimator)
self.lasso_estimator_.fit(X2, y)
not_null = self.lasso_estimator_.coef_ != 0
self.intercept_ = self.lasso_estimator_.intercept_
self.estimators_ = estimators[not_null]
self.coef_ = self.lasso_estimator_.coef_[not_null]
return self
def predict(self, X):
prediction = None
for i, est in enumerate(self.estimators_):
pred = est.predict(X)
if prediction is None:
prediction = pred * self.coef_[i]
else:
prediction += pred * self.coef_[i]
return prediction + self.intercept_
ls = LassoRandomForestRegressor()
ls.fit(X_train, y_train)
C:xavierdupre__home_github_forkscikit-learnsklearnlinear_modelcoordinate_descent.py:475: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 14.277320258655209, tolerance: 3.196316 positive)
LassoRandomForestRegressor(lasso_estimator=Lasso(alpha=1.0, copy_X=True,
fit_intercept=True,
max_iter=1000, normalize=False,
positive=False,
precompute=False,
random_state=None,
selection='cyclic', tol=0.0001,
warm_start=False),
rf_estimator=RandomForestRegressor(bootstrap=True,
ccp_alpha=0.0,
criterion='mse',
max_depth=None,
max_features='auto',
max_leaf_nodes=None,
max_samples=None,
min_impurity_decrease=0.0,
min_impurity_split=None,
min_samples_leaf=1,
min_samples_split=2,
min_weight_fraction_leaf=0.0,
n_estimators=100,
n_jobs=None,
oob_score=False,
random_state=None,
verbose=0,
warm_start=False))
Résultats#
La forêt aléatoire seule.
clr.score(X_test, y_test)
0.8005425626013631
La forêt aléatoire réduite.
ls.score(X_test, y_test)
0.8325193431184871
Avec une réduction conséquente.
len(ls.estimators_), len(clr.estimators_)
(36, 100)
Critère AIC#
On peut même sélectionner le nombre d’arbres avec un critère AIC et le modèle LassoLarsIC.
from sklearn.linear_model import LassoLarsIC
ls_aic = LassoRandomForestRegressor(lasso_estimator=LassoLarsIC())
ls_aic.fit(X_train, y_train)
LassoRandomForestRegressor(lasso_estimator=LassoLarsIC(copy_X=True,
criterion='aic',
eps=2.220446049250313e-16,
fit_intercept=True,
max_iter=500,
normalize=True,
positive=False,
precompute='auto',
verbose=False),
rf_estimator=RandomForestRegressor(bootstrap=True,
ccp_alpha=0.0,
criterion='mse',
max_depth=None,
max_features='auto',
max_leaf_nodes=None,
max_samples=None,
min_impurity_decrease=0.0,
min_impurity_split=None,
min_samples_leaf=1,
min_samples_split=2,
min_weight_fraction_leaf=0.0,
n_estimators=100,
n_jobs=None,
oob_score=False,
random_state=None,
verbose=0,
warm_start=False))
ls_aic.score(X_test, y_test)
0.8122126507071663
len(ls_aic.estimators_)
19
pickling#
A partir du moment où les conventions de l’API de scikit-learn sont respectées, tout est pris en charge.
from io import BytesIO
by = BytesIO()
pickle.dump(ls, by)
by2 = BytesIO(by.getvalue())
mod2 = pickle.load(by2)
p1 = ls.predict(X_test)
p2 = mod2.predict(X_test)
p1[:5], p2[:5]
(array([26.75262967, 18.61136749, 22.82312896, 18.0698006 , 22.2971346 ]),
array([26.75262967, 18.61136749, 22.82312896, 18.0698006 , 22.2971346 ]))
Conclusion#
L’API est une sorte de légo. Tout marche si on respecte les dimensions de départ.
show_images('lego.png', 'lego-architecture-studio-8804.jpg', figsize=(16,6));

show_images('vue-interieure-cite-de-musique-christian-de.jpg', 'PaulPoiret-7.jpg', figsize=(16,6));

show_images('lycee_chanzy_maquette.jpg', figsize=(16,10));
