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).
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:
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 OneVsRestClassifier (o al contrario, usando 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 ]])
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 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