Herramientas de usuario

Herramientas del sitio


clase:ia:saa:2eval:sml_clasificacion_1

11 - SML Clasificación 1: Métricas

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)

Métricas

Accuracy (exactitud)

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 exactitud (accuracy) no suele ser una buena medida de rendimiento, sobre todo si el target está desbalanceado

Matriz de confusión

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()

Precisión (precision) y sensibilidad (recall)

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.

Valor $F_{1}$

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

el valor $F_{1}$ prefiere los clasificadores que tienen una precisión y sensibilidad similares. Hay ocasiones en que ésto no es lo que nos interesa. Por ejemplo, si creamos un modelo para detectar cáncer, nos interesa que tenga una alta sensibilidad (que detecte casi todos los casos) aunque se equivoque más a menudo (baja precisión). Sin embargo, si entrenamos un clasificador para detectar vídeos seguros para niños, es probable que prefiramos un clasificador que rechace muchos vídeos buenos (baja sensibilidad), pero que mantenga sólo los que son seguros (alta precisión).

Compensación precisión/sensibilidad

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.

Por lo general, los clasificadores de sklearn tienen el método decision_function() o predict_proba() (o ambos). Con el primer método, los clasificadores calculan una puntuación y según un umbral (por defecto SGDClassifier utiliza un umbral igual a 0) hace predicciones según esas puntuaciones. El método predict_proba() devuelve una matriz que contiene una fila por instancia y una columna por clase y cada una contiene la probabilidad de que la instancia dada pertenezca a cada 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

np_argmax() nos da el primer índice del valor máximo, que en este caso quiere decir el primer valor True.

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.

Curva ROC

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

La curva ROC es muy similar a la curva precisión/sensibilidad, con lo que tenemos que elegir cuál utilizar. Como regla de oro, deberías elegir la curva precisión/sensibilidad siempre que la clase positiva sea escasa o cuando te interesan más los falsos positivos que los falsos negativos
clase/ia/saa/2eval/sml_clasificacion_1.txt · Última modificación: 2023/02/20 08:32 por cesguiro