Al contrario que en regresión, en los problemas de clasificación nuestro objetivo es predecir un valor finito (la clase o clases a la que pertenecen nuestros datos). Para los siguientes ejemplos, vamos a utilizar el conjunto de datos mnist. Este dataset contiene 60.000 imágenes de dígitos escritos a mano.
import pandas as pd import numpy as np from sklearn.linear_model import SGDClassifier from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.model_selection import cross_val_score, cross_val_predict import matplotlib.pyplot as plt # Configuración warnings # ============================================================================== import warnings warnings.filterwarnings('ignore')
df = pd.read_csv('./mnist.csv', header = None) # Renombramos la columna 0 a target df.rename(columns={0: 'target'}, inplace=True) df.shape
(60000, 785)
Hay 60.000 imágenes con 784 características cada una (28 x 28 píxeles cada imagen).
X = df.drop(columns = 'target') y = df['target']
Vamos a mostrar una imagen del dataframe:
n = 0 digit_n = X.loc[n, :] digit_n_target = y.loc[n] digit = np.array(digit_n) digit_image = digit.reshape(28, 28) plt.imshow(digit_image, cmap = "binary") plt.axis("off") plt.show() print("Target:", digit_n_target)
Target: 5
Separamos, como siempre, train y test:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=91)
Primero, vamos a simplificar el problema para hacer un clasificador binario (el target tendrá sólo 2 clases posibles) que distinga un sólo dígito; por ejemplo, el 5.
y_train_5 = (y_train == 5) y_test_5 = (y_test == 5)
Creamos nuestro modelo (En este caso clasificador SGD) y comprobamos su exactitud (tasa de aciertos) usando validación cruzada:
model = SGDClassifier(random_state = 42) cross_val_score(model, X_train, y_train_5, cv=3, scoring="accuracy")
array([0.96192857, 0.958 , 0.93578571])
Más del 93% en todas las iteraciones de la validación cruzada. En principio, no parece mal modelo.
Vamos a hacer un clasificador propio que prediga siempre “no es 5”:
from sklearn.base import BaseEstimator class myModel(BaseEstimator): def fit(self, X, y = None): return self def predict(self, X): return np.zeros((len(X), 1), dtype=bool)
model2 = myModel() cross_val_score(model2, X_train, y_train_5, cv=3, scoring="accuracy")
array([0.90957143, 0.90785714, 0.90857143])
90% de exactitud. Lógico, ya que sólo el 10% de los dígitos más o menos son cincos. Tampoco se va tanto de nuestro primer modelo.
La matriz de confusión nos da una idea más acertada del funcionamiento de nuestro modelo. Indica el número de veces que las instancias de una clase A se clasifica como otra clase B.
Vamos a hacer las predicciones sobre el conjunto de entrenamiento (recuerda que el conjunto de test es mejor no tocarlo hasta el final) usando el método cross_val_predict de sklearn.
model.fit(X_train, y_train_5) y_predict_train = cross_val_predict(model, X_train, y_train_5, cv = 3)
Para sacar la matriz de confusión, usaremos confusion_matrix:
from sklearn.metrics import confusion_matrix confusion_matrix(y_train_5, y_predict_train)
array([[36715, 1449], [ 571, 3265]])
Este método nos devuelve una matriz de 2×2 (en realidad, nos devuelve una matriz de NxN, siendo N el número de clases del target). Cada fila representa una clase real, mientras que cada columna representa una clase predicha. En nuestro caso, la primera fila representa las imágenes “no 5” (clase negativa). 36.715 imágenes se han clasificado correctamente (verdaderos negativos o VN), mientras que 1.449 se han clasificado erróneamente como positivos (falsos positivos o FP). La segunda fila representa la clase real “es 5” (clase positiva). 571 imágenes se han clasificado erróneamente como “no 5” (falsos negativos o FN) y 3.265 se han clasificado correctamente como “es 5” (verdaderos positivos o VP).
Predicción | |||
---|---|---|---|
0 | 1 | ||
Realidad | 0 | VN | FP |
1 | FN | VP |
Podemos usar plot_confusion_matrix para ver mejor los datos:
from sklearn.metrics import plot_confusion_matrix plot_confusion_matrix(model, X_train, y_train_5, cmap="summer") plt.show()
La matriz de confusión nos da mucha información, pero a menudo preferimos tener una métrica más concisa. Una posible métrica es la precisión (precision): exactitud de las predicciones positivas. Su formula es:
$precisión = \frac{VP}{VP + FP}$
La precisión suele utilizarse junto a otra métrica llamada sensibilidad (recall): tasa de verdaderos positivos:
$sensibilidad = \frac{VP}{VP + FN}$
sklearn ofrece varias funciones para calcular las métricas, entre ellas la precisión y sensibilidad:
from sklearn.metrics import precision_score, recall_score precision = precision_score(y_train_5, y_predict_train) recall = recall_score(y_train_5, y_predict_train) print("Precisión:", precision) print("Sensibilidad", recall)
Precisión: 0.692617734408146 Sensibilidad 0.8511470281543274
Ahora nuestro modelo ya no parece tan bueno. Cuando detecta que es un 5, sólo está en lo cierto el 69,3% de las veces. Además, sólo detecta el 85,1% de los 5.
Esta métrica combina precisión y sensibilidad. Es útil para comparar de manera sencilla dos clasificadores. El valor $F_{1}$ es la media armónica de la precisión y la sensibilidad. Mientras que la media regular trata a todos los valores igual, la media armónica da mucho más peso a los valores bajos. Como resultado, el clasificador sólo obtendrá un buen valor $F_{1}$ si la sensibilidad y la precisión son altas.
$F_{1} = \frac{2}{\frac{1}{precisión} + \frac{1}{sensibilidad}} = 2 * \frac{precisión * sensibilidad}{precisión + sensibilidad} = \frac{VP}{VP + \frac{FN + FP}{2}}$
from sklearn.metrics import f1_score f1_score(y_train_5, y_predict_train)
0.7637426900584795
Aunque lo ideal sería tener una alta precisión y sensibilidad, cuando aumentamos la precisión se reduce la sensibilidad y viceversa. Para comprobarlo, vamos a usar decision_function como método para que cross_val_predict devuelva no la clase predicha, si no las puntuaciones en las que se basa para seleccionar la clase.
y_scores = cross_val_predict(model, X_train, y_train_5, cv = 3, method = "decision_function")
Ahora podemos usar la función precision_recall_curve para calcular la precisión y la sensibilidad para todos los umbrales posibles.
from sklearn.metrics import precision_recall_curve precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)
Mostramos un gráfico con los datos:
def plot_precision_recall_vs_threshold(precisions, recalls, thresholds): figure=plt.figure(figsize=(10, 5)) axes = figure.add_subplot() axes.plot(thresholds, precisions[:-1], "b--", label = "Precisión") axes.plot(thresholds, recalls[:-1], "g-", label = "Sensibilidad") axes.legend(fontsize=15,facecolor='#CDCDCD',labelcolor="#000000")
plot_precision_recall_vs_threshold(precisions, recalls, thresholds)
También podemos mostrar directamente la precisión frente a la sensibilidad:
figure=plt.figure(figsize=(10, 5)) axes = figure.add_subplot() axes.plot(precisions[:-1], recalls[:-1]) axes.legend(fontsize=15,facecolor='#CDCDCD',labelcolor="#000000") axes.set_xlabel('Sensibilidad', fontsize=15,labelpad=20,color="#003B80") axes.set_ylabel('Precisión', fontsize=15,labelpad=20,color="#003B80") axes.set_title("Precisión vs Sensibilidad", fontsize=20,pad=30,color="#003B80")
Vemos que la precisión empieza a bajar bruscamente a partir del 80% de la sensibilidad, con lo que podemos elegir una combinación precisión/sensibilidad justo antes de ese descenso.
Sklearn no te permite establecer el umbral directamente, pero podemos usar las puntuaciones calculadas anteriormente (y_scores) para usar cualquier umbral que queramos.
thresholds_90_precision = thresholds[np.argmax(precisions >= 0.90)] print(thresholds_90_precision)
8023.079089272452
Para hacer las predicciones utilizamos y_scores en lugar del método predict():
y_predict_train_90 = (y_scores >= thresholds_90_precision) precision_90 = precision_score(y_train_5, y_predict_train_90) recall_precision_90 = recall_score(y_train_5, y_predict_train_90) print("Precisión:", precision_90) print("Sensibilidad", recall_precision_90)
Precisión: 0.9002293577981652 Sensibilidad 0.6139207507820647
Hemos aumentado la precisión de nuestro clasificador a costa de la sensibilidad.
Otra métrica común utilizada en los clasificadores binarios es la curva ROC (receiver operating characteristic). Es parecida a la curva precisión/sensibilidad, pero, en vez de trazar la precisión frente a la sensibilidad, traza la tasa de verdaderos positivos (TPR, otra forma de llamar a la sensibilidad) frente a la tasa de falsos positivos (FPR, proporción de instancias negativas que se clasifican de manera incorrecta como positivas).
Para trazar la curva ROC, primero tenemos que usar la función roc_curve() para calcular la TPR y la FPR para varios valores de umbral:
from sklearn.metrics import roc_curve fpr, tpr, thresholds = roc_curve(y_train_5, y_scores)
def plot_roc_curve(fpr, tpr, label = None): figure=plt.figure(figsize=(10, 5)) axes = figure.add_subplot() axes.plot(fpr, tpr, label = label) #trazar diagonal discontinua axes.plot([0,1], [0,1], "k--") axes.set_ylabel('True Positive Rate (TPR or Recall)', fontsize=15,labelpad=20,color="#003B80") axes.set_xlabel('False Positive Rate (FPR)', fontsize=15,labelpad=20,color="#003B80") axes.set_xlim([0, 1]) axes.set_ylim([0, 1]) axes.set_title("ROC Curve", fontsize=20,pad=30,color="#003B80")
plot_roc_curve(fpr, tpr)
Como vemos, cuanto mayor es la sensibilidad (TPR), más falsos positivos hay (FPR). La línea de puntos representa la curva ROC de un clasificador puramente aleatorio. El objetivo es mantener nuestra línea lo más lejana posible (hacia la esquina superior izquierda).
Una buena forma de comparar clasificadores es medir el área debajo de la curva. Un clasificador perfecto tendrá un área debajo de la curva igual a 1. SKlearn nos da una función para calcular ese área: roc_auc_score().
from sklearn.metrics import roc_auc_score roc_auc_score(y_train_5, y_scores)
0.9660616032404575
Vamos a usar ahora otro clasificador: RandomForestClassifier. En este caso, el clasificador no tiene el método decision_function(), sino que tiene predict_proba().
model_2 = RandomForestClassifier(random_state=42)
y_probas_forest = cross_val_predict(model_2, X_train, y_train_5, cv=2, method="predict_proba")
Como hemos dicho antes, este método devuelve probabilidades de pertenencia de la instancia a cada clase:
y_probas_forest
array([[1. , 0. ], [1. , 0. ], [1. , 0. ], ..., [0.92, 0.08], [0.92, 0.08], [1. , 0. ]])
La función roc_curve() espera etiquetas y puntuaciones, pero, en vez de puntuaciones, podemos darle probabilidades de clase. Vamos a utilizar la probabilidad de la clase positiva (“Es un 5”) como puntuación:
y_scores_forest = y_probas_forest[:, 1] fpr_forest, tpr_forest, thresholds_forest = roc_curve(y_train_5, y_scores_forest)
Mostramos su curva ROC y el área debajo de la curva:
plot_roc_curve(fpr_forest, tpr_forest)
roc_auc_score(y_train_5, y_scores_forest)
0.9977233839270482
Como vemos, la curva ROC del RandomForestClassifier y el área debajo de la curva son bastantes mejores que la del SGDClassifier. Si medimos su precisión y sensibilidad vemos que también es bastante mejor:
y_predict_train_forest = cross_val_predict(model_2, X_train, y_train_5, cv = 3) precision_forest = precision_score(y_train_5, y_predict_train_forest) recall_forest = recall_score(y_train_5, y_predict_train_forest) print("Precisión:", precision_forest) print("Sensibilidad", recall_forest)
Precisión: 0.990036231884058 Sensibilidad 0.8547966631908238