====== 11 - SML Clasificación: Tipos de clasificadores ======
En el punto anterior, vimos un clasificador básico que distinguía entre dos etiquetas. En este punto, vamos a ver otros tipos posibles de clasificadores (multiclase y multietiqueta).
===== Clasificadores multiclase =====
Vamos a seguir con el conjunto de datos //mnist//, pero en este caso, nuestro clasificador será capaz de clasificar dígitos del 0 al 9. Para eso, cargamos los datos y separamos //train// y //test// como siempre:
import pandas as pd
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.linear_model import SGDClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
# Configuración warnings
# ==============================================================================
import warnings
warnings.filterwarnings('ignore')
df = pd.read_csv('./mnist.csv', header = None)
df.rename(columns={0: 'target'}, inplace=True)
X = df.drop(columns = 'target')
y = df['target']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=91)
Existen clasificadores que son capaces de clasificar varias clases de forma nativa, por ejemplo //SGD// y //RandomForest//. Vamos a ver primero //SGD//. Como siempre, definimos el modelo y lo entrenamos:
sgd_clf = SGDClassifier(random_state = 42)
sgd_clf.fit(X_train, y_train)
Guardamos el primer número para hacer la predicción (acuérdate que el primer número corresponde a un 5):
n = 0
digit_n = X.loc[n, :]
sgd_clf.predict([digit_n])
array([5])
En este caso, nuestro clasificador ha acertado la clase (número 5). Puedes modificar la //n// para comprobar si es capaz de clasificar otros dígitos. Si miramos lo que devuelve la función //decision_function()//, vemos que nos devuelve 10 puntuaciones (una por cada clase de la etiqueta):
sgd_clf.decision_function([digit_n])
array([[-29971.9150807 , -33957.19380802, -17610.9497977 ,
-1713.07889876, -47927.43234059, 2399.67452617,
-44143.47187723, -18694.63277894, -10228.82799588,
-14859.26628549]])
Vamos a hacer lo mismo con //RandomForest//:
rf_clf = RandomForestClassifier(random_state=42)
rf_clf.fit(X_train, y_train)
rf_clf.predict([digit_n])
array([5])
En este caso, vemos que la función //predict_proba()// devuelve 10 probabilidades en lugar de puntuaciones (de nuevo, una por cada clase de nuestra etiqueta):
rf_clf.predict_proba([digit_n])
array([[0.01, 0. , 0. , 0.12, 0. , 0.82, 0. , 0.01, 0.01, 0.03]])
¿Qué pasa con los clasificadores que no son capaces de clasificar entre más de dos clases de forma nativa como SVM o regresión logística? Para solucionarlo, tenemos dos posibles estrategias:
* Entrenar 10 clasificadores binarios (uno para cada dígito) y seleccionar la clase cuyo clasificador genere la puntuación (o probabilidad) más alta. Esta estrategia se conoce como **OvR (one-versus-the-rest)**
* Entrenar un clasificador binario para cada par de dígitos (0-1, 0-2, 1-2...). Esta estrategia es la llamada **OvO (one-versus-one)**. El número de clasificadores necesarios es: $\frac{N * (N-1)}{2}$
La principal ventaja de //OvO// es que cada clasificador solo necesita entrenarse en la parte del conjunto de entrenamiento para las dos clases que debe distinguir.
Aunque, en general, para la mayoría de clasificadores binarios se prefiere //OvR//, algunos algoritmos (SVM) escalan mal con el tamaño del conjunto de entrenamiento. En ese caso, es mejor usar //OvO//.
//Sklearn// ejecuta de manera automática OvR u OvO dependiendo del algoritmo usado.
Vamos a usar SVM para clasificar nuestros dígitos:
svm_clf = SVC()
svm_clf.fit(X_train, y_train)
svm_clf.predict([digit_n])
array([5])
svm_clf.decision_function([digit_n])
array([[ 2.72999913, 1.7197734 , 7.24519062, 8.30666798, -0.30958744,
9.31225376, 0.71135381, 3.77595107, 6.24183919, 4.8684599 ]])
Como hemos dicho, //Sklearn// usa por defecto //OvO// cuando utilizamos //SVM// como clasificador multiclase. Podemos cambiar la estrategia utilizando la función [[https://scikit-learn.org/stable/modules/generated/sklearn.multiclass.OneVsRestClassifier.html|OneVsRestClassifier]] (o al contrario, usando [[https://scikit-learn.org/stable/modules/generated/sklearn.multiclass.OneVsOneClassifier.html|OneVsOneClassifier]]). A estas funciones, les pasamos el clasificador que queremos usar utilizando esa estrategia:
from sklearn.multiclass import OneVsRestClassifier
svm_ovr_clf = OneVsRestClassifier(SVC())
svm_ovr_clf.fit(X_train, y_train)
svm_ovr_clf.predict([digit_n])
array([5])
svm_ovr_clf.decision_function([digit_n])
array([[-2.11492902, -2.65730473, -1.48726137, -0.89943177, -3.62018156,
1.11959236, -2.9115367 , -1.85285278, -2.47992001, -3.0623506 ]])
===== Clasificadores multietiqueta =====
En este caso, queremos que nuestros clasificadores sean capaces de clasificar datos en función de varias etiquetas. Por ejemplo, vamos a crear un clasificador que nos diga si un dígito es un número grande (mayor que 7) y si es impar.
Lo primero será crear nuestro target multietiqueta:
y_train_large = (y_train >= 7)
y_train_odd = (y_train % 2 == 1)
# np.c_ concatena dos arrays en formato columnas
y_multilabel = np.c_[y_train_large, y_train_odd]
Si miramos el target de entrenamiento, veremos que ahora tenemos dos columnas en lugar de una:
y_multilabel
array([[False, False],
[ True, False],
[ True, True],
...,
[False, False],
[False, True],
[False, True]])
Vamos a usar un clasificador [[https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KNeighborsClassifier.html|KNN]].
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train, y_multilabel)
Si hacemos una predicción sobre un número cualquiera, vemos que ahora nos da dos resultados (si es grande y si es impar):
knn_clf.predict([digit_n])
array([[False, True]])
Para evaluar este tipo de clasificadores podemos usar varias técnicas. Por ejemplo, calcular $F_{1}$ para cada etiqueta individual (o cualquier otra métrica) y calcular la puntuación media.
y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_multilabel, cv = 3)
f1_score(y_multilabel, y_train_knn_pred, average = "macro")
0.9739045810268242
En el caso de un clasificador multietiqueta, //f1_score()// calcula la métrica para cada etiqueta y la puntuación final en función del parámetro **average** (si queremos que todas las métricas tengan el mismo peso ponemos el valor **macro**).
Si quisiéramos, por ejemplo, dar más peso a las etiquetas con más instancias positivas, podemos poner el valor **weighted** en el parámetro //average//:
f1_score(y_multilabel, y_train_knn_pred, average = "weighted")
0.9756131054304887