====== 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 {{ :clase:ia:saa:7_sml_clasificacion:mnist.zip | 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) {{ :clase:ia:saa:7_sml_clasificacion:digito.png?200 |}} 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 [[https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.cross_val_predict.html|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 [[https://scikit-learn.org/stable/modules/generated/sklearn.metrics.confusion_matrix.html|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 2x2 (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 [[https://scikit-learn.org/stable/modules/generated/sklearn.metrics.plot_confusion_matrix.html|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() {{ :clase:ia:saa:7_sml_clasificacion:confusion_matrix.png?400 |}} ==== 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 [[https://scikit-learn.org/stable/modules/generated/sklearn.metrics.precision_recall_curve.html|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) {{ :clase:ia:saa:7_sml_clasificacion:precision_vs_recall_01.png?400 |}} 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") {{ :clase:ia:saa:7_sml_clasificacion:precision_vs_recall_02.png?400 |}} 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 [[https://numpy.org/doc/stable/reference/generated/numpy.argmax.html|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 [[https://scikit-learn.org/stable/modules/generated/sklearn.metrics.roc_curve.html|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) {{ :clase:ia:saa:7_sml_clasificacion:roc_01.png?400 |}} 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: [[https://scikit-learn.org/stable/modules/generated/sklearn.metrics.roc_auc_score.html|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: [[https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html|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) {{ :clase:ia:saa:7_sml_clasificacion:roc_02.png?400 |}} 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