.. _2020-01-31classificationrst: ============== Classification ============== .. only:: html **Links:** :download:`notebook <2020-01-31_classification.ipynb>`, :downloadlink:`html <2020-01-31_classification2html.html>`, :download:`PDF <2020-01-31_classification.pdf>`, :download:`python <2020-01-31_classification.py>`, :downloadlink:`slides <2020-01-31_classification.slides.html>`, :githublink:`GitHub|_doc/notebooks/encours/2020-01-31_classification.ipynb|*` Notebook autour d’un cas de classification binaire. .. code:: ipython3 from jyquickhelper import add_notebook_menu add_notebook_menu() .. contents:: :local: .. code:: ipython3 %matplotlib inline Les données ----------- .. code:: ipython3 from sklearn.datasets import load_breast_cancer data = load_breast_cancer() .. code:: ipython3 X, y = data.data, data.target .. code:: ipython3 from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y) .. code:: ipython3 X.shape .. parsed-literal:: (569, 30) ACP --- On projette les données dans un plan, avec une couleur pour chaque classe pour voir si le problème paraît simple ou non. Pour projeter les données, le plus simple est une ACP. On conserver trois axes. .. code:: ipython3 from sklearn.decomposition import PCA acp = PCA(3) acp.fit(X_train, y_train) .. parsed-literal:: PCA(copy=True, iterated_power='auto', n_components=3, random_state=None, svd_solver='auto', tol=0.0, whiten=False) .. code:: ipython3 xacp = acp.transform(X_train) xacp.shape .. parsed-literal:: (426, 3) .. code:: ipython3 import matplotlib.pyplot as plt fig, ax = plt.subplots(1, 2, figsize=(8, 4)) ax[0].plot(xacp[y_train == 0, 0], xacp[y_train == 0, 1], '.', label="cl0") ax[0].plot(xacp[y_train == 1, 0], xacp[y_train == 1, 1], '.', label="cl1") ax[1].plot(xacp[y_train == 0, 1], xacp[y_train == 0, 2], '.', label="cl0") ax[1].plot(xacp[y_train == 1, 1], xacp[y_train == 1, 2], '.', label="cl1") ax[0].legend();ax[1].legend(); ax[0].set_title("axes 1, 2");ax[1].set_title("axes 2, 3"); .. image:: 2020-01-31_classification_11_0.png Le problème n’a pas l’air trop compliqué. Régression logistique --------------------- .. code:: ipython3 from sklearn.linear_model import LogisticRegression .. code:: ipython3 logreg = LogisticRegression(solver='lbfgs') logreg.fit(xacp, y_train) .. parsed-literal:: 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) Les coefficients. Si :math:`(X_1, X_2, X_3)` sont les coordonnées des variables projetées sur les trois axes, le modèle définit une droite :math:`f(x_1, x_2, x_3) = a_1 x_1 + a_2 x_2 + a_3 x_3 + b`. .. code:: ipython3 logreg.coef_, logreg.intercept_ .. parsed-literal:: (array([[-0.01244239, 0.02897613, -0.0194635 ]]), array([-1.48191575])) La courbe :math:`f(x_1, x_2, x_3) = 0` définit la frontière entre les deux classes, celle où la probabilité d’appartenir à l’une ou l’autre classe est égale à :math:`\frac{1}{2}`. On ajoute cette frontière sur chacune des deux projections. :math:`f(x_1, x_2, x_3) = a_1 x_1 + a_2 x_2 + a_3 x_3 + b = 0`. Sur la première projection, :math:`x_3 = 0`, donc la droite a pour équation : :math:`a_1 x_1 + a_2 x_2 + a_3 x_3 + b = 0 \Rightarrow x_2 = - \frac{a_1 x_1 + b}{a_2}`. C’est ce qu’on représente ici. .. code:: ipython3 import numpy X1 = numpy.array(list(range(-1000, 1000, 10))) X2 = (-logreg.intercept_[0]-logreg.coef_[0, 0]*X1) / logreg.coef_[0, 1] X22 = numpy.array(list(range(-100, 100, 10))) X3 = (-logreg.intercept_[0]-logreg.coef_[0, 1]*X22) / logreg.coef_[0, 2] import matplotlib.pyplot as plt fig, ax = plt.subplots(1, 2, figsize=(8, 4)) ax[0].plot(xacp[y_train == 0, 0], xacp[y_train == 0, 1], '.', label="cl0") ax[0].plot(xacp[y_train == 1, 0], xacp[y_train == 1, 1], '.', label="cl1") ax[0].plot(X1, X2, '.', label="logreg") ax[1].plot(xacp[y_train == 0, 1], xacp[y_train == 0, 2], '.', label="cl0") ax[1].plot(xacp[y_train == 1, 1], xacp[y_train == 1, 2], '.', label="cl1") ax[1].plot(X22, X3, '.', label="logreg") ax[0].legend();ax[1].legend(); ax[0].set_title("axes 1, 2");ax[1].set_title("axes 2, 3"); .. image:: 2020-01-31_classification_19_0.png On calcule la matrice de confusion. Il faut projeter les variables initiales sur les axes de l’ACP puis prédire. .. code:: ipython3 xacp_test = acp.transform(X_test) pred = logreg.predict(xacp_test) .. code:: ipython3 from sklearn.metrics import confusion_matrix confusion_matrix(y_test, pred) .. parsed-literal:: array([[42, 4], [ 4, 93]], dtype=int64) Courbe ROC ---------- La courbe ROC est une courbe souvent utilisée pour mesurer la pertinence d’un modèle. La matrice de confusion n’utilise pas la probabilité retournée par le modèle, cette dernière exprime si le point :math:`X_i` est plus ou moins loin de la frontière de prédiction. En principe, plus on est loin, moins le modèle prédictif se trompe. C’est ce que la courbe ROC reflète, plus elle est élevé, plus c’est le cas (pour en savoir plus : `courbe ROC `__). .. code:: ipython3 from sklearn.metrics import roc_curve y_pred_grd = logreg.predict_proba(xacp_test)[:, 1] fpr_grd, tpr_grd, _ = roc_curve(y_test, y_pred_grd) .. code:: ipython3 plt.figure(1, figsize=(3, 3)) plt.plot([0, 1], [0, 1], 'k--') plt.plot(fpr_grd, tpr_grd); .. image:: 2020-01-31_classification_25_0.png La ligne en pointillée serait la courbe ROC obtenue pour une prédiction aléatoire. On en est loin. Frontière polynômiale --------------------- On souhaite une frontière polynômiale. On ajoute pour cela un prétraitement qui calcule :math:`X_1^2, X_2^2, X_3^2, X_1 X_2, ...`. .. code:: ipython3 from sklearn.preprocessing import PolynomialFeatures poly = PolynomialFeatures(degree=2) poly.fit(xacp) .. parsed-literal:: PolynomialFeatures(degree=2, include_bias=True, interaction_only=False, order='C') .. code:: ipython3 xacp_poly = poly.transform(xacp) xacp.shape, xacp_poly.shape .. parsed-literal:: ((426, 3), (426, 10)) .. code:: ipython3 logreg2 = LogisticRegression(solver='lbfgs', max_iter=1000) logreg2.fit(xacp_poly, y_train) .. parsed-literal:: LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True, intercept_scaling=1, l1_ratio=None, max_iter=1000, multi_class='auto', n_jobs=None, penalty='l2', random_state=None, solver='lbfgs', tol=0.0001, verbose=0, warm_start=False) Les nouveaux coefficients du modèle. .. code:: ipython3 list(zip(poly.get_feature_names(), logreg2.coef_.ravel())) .. parsed-literal:: [('1', 0.00013474687830331624), ('x0', -0.009796886245028377), ('x1', 0.03422545572030919), ('x2', -0.005487261266497426), ('x0^2', 6.912136603531012e-07), ('x0 x1', -4.353833813532155e-05), ('x0 x2', -0.00019255718749305347), ('x1^2', -0.0004269054807770011), ('x1 x2', -0.0018760934646931198), ('x2^2', -0.0042992267040219976)] La frontière est plus difficile à tracer et il n’est pas évident de trouver son équation à moins de résoudre un polynôme du second degré. On calcule plutôt les prédictions pour de nombreux points disposés sur une grille en leur donnant une couleur différente selon que la classe prédite est 0 ou 1. .. code:: ipython3 Xp1 = numpy.array(list(range(-1000, 1000, 10))) Xp2 = numpy.array(list(range(-100, 100, 2))) cote0 = [] cote1 = [] for x1 in Xp1: for x2 in Xp2: x = numpy.array([[x1, x2, 0]]) xp = poly.transform(x) yp = logreg2.predict(xp) if yp == 1: cote1.append((x1, x2)) else: cote0.append((x1, x2)) cote0 = numpy.array(cote0) cote1 = numpy.array(cote1) .. code:: ipython3 fig, ax = plt.subplots(1, 2, figsize=(8, 4)) ax[0].plot(cote0[:, 0], cote0[:, 1], '.', label='yp0') ax[0].plot(cote1[:, 0], cote1[:, 1], '.', label='yp1') ax[0].plot(xacp[y_train == 0, 0], xacp[y_train == 0, 1], '.', label="cl0") ax[0].plot(xacp[y_train == 1, 0], xacp[y_train == 1, 1], '.', label="cl1") ax[0].plot(X1, X2, '.', label="logreg") ax[1].plot(xacp[y_train == 0, 1], xacp[y_train == 0, 2], '.', label="cl0") ax[1].plot(xacp[y_train == 1, 1], xacp[y_train == 1, 2], '.', label="cl1") ax[1].plot(X22, X3, '.', label="logreg") ax[0].legend();ax[1].legend(); ax[0].set_xlim([-1000, 1000]);ax[0].set_ylim([-150, 150]); ax[0].set_title("axes 1, 2");ax[1].set_title("axes 2, 3"); .. image:: 2020-01-31_classification_35_0.png Ca paraît un peu mieux. .. code:: ipython3 confusion_matrix(y_test, logreg2.predict( poly.transform( acp.transform(X_test)))) .. parsed-literal:: array([[41, 5], [ 4, 93]], dtype=int64) Pipeline -------- Ce n’est pas évident de se souvenir de toutes les étapes. Le mieux est sans doute de tout insérer dans un pipeline. .. code:: ipython3 from sklearn.pipeline import Pipeline pipe = Pipeline([ ('acp', PCA(3)), ('poly', PolynomialFeatures(2)), ('lr', LogisticRegression(solver='lbfgs', max_iter=1000)) ]) pipe.fit(X_train, y_train) pred = pipe.predict(X_test) confusion_matrix(y_test, pred) .. parsed-literal:: array([[41, 5], [ 4, 93]], dtype=int64) C’est plus lisible.