sábado, 16 de enero de 2021

Clasificaciones Multiclase y Multietiqueta en Machine Learning

Anteriormente hemos visto la clasificación binaria donde clasificábamos entre dos tipos, es decir un objeto dado es un objeto de nuestra clasificación, o no lo es. Y tomábamos como ejemplo el 7, un número dado es un 7 o no lo es. Pues bien en la clasificación multiclase, dado un numero tenemos que clasificarlo dentro de las clases posibles, es decir, o es un 0, o un 1, o un 2, etc.

Hay algoritmos estrictamente binarios, como la Regresión Logística, o los clasificadores SVM (Support Vector Machine), otros algoritmos como los SGD, los Random Forest y los naive Bayes son multiclasificadores. Aunque también se pueden utilizar clasificadores binarios para multiclasificación, por ejemplo podemos entrenar 10 modelos SVM, uno para clasificar los ceros, otro para los unos, otro para los doses, etc, y dada una instancia clasificarla con la que mayor puntuación obtenga en los 10 clasificadores, esto se llama estrategia OvR (One versus de rest).

Otra estrategia podría ser entrenar los modelos para distinguir entre 0 y 1, otro modelo entre 0 y 2, otro entre 1 y 2, etc esta estrategia se llama OvO (One versus one). Para esta estrategia, si tenemos N clases necesitamos N x (N-1)/2 modelos, en el caso de 0 a 9 tenemos ¡45 modelos a entrenar! Aunque tiene la ventaja de que cada modelo sólo necesita un subset para entrenarse.

SVM tiene un desempeño pobre con datasets muy grandes, en este caso es mejor utilizar OvO pues es más rápido entrenando cada trozo de subset.

Si no hemos hecho el post anterior, para que funcione el código siguiente, es necesario ejecutar antes este código:

# Python ≥3.5 is required

import sys

assert sys.version_info >= (3, 5)

 

# Scikit-Learn ≥0.20 is required

import sklearn

assert sklearn.__version__ >= "0.20"

 

# Common imports

import numpy as np

import os

 

# to make this notebook's output stable across runs

np.random.seed(42)

 

# To plot pretty figures

%matplotlib inline

import matplotlib as mpl

import matplotlib.pyplot as plt

mpl.rc('axes', labelsize=14)

mpl.rc('xtick', labelsize=12)

mpl.rc('ytick', labelsize=12)

 

#importar tensorflow y el script input_data

import tensorflow as tf

import input_data

 

#importamos el dataset mnist

from sklearn.datasets import fetch_openml

mnist = fetch_openml('mnist_784', version=1)

mnist.keys()

 

#importamos matplotlib para generar graficos

%matplotlib inline

import matplotlib as mpl

import matplotlib.pyplot as plt

some_digit = X[0]

 

#definimos X e y

X, y = mnist["data"], mnist["target"]

 

from sklearn.svm import SVC

svm_clf = SVC(gamma="auto", random_state=42)

svm_clf.fit(X_train[:1000], y_train[:1000]) # y_train

svm_clf.predict([some_digit])


En Scikit-Learn podemos lanzar un algoritmo OvO utilizando OneVsOneClassifier y OvR con OneVrRestClassifier basta con crear una instancia y pasar un clasificador como constructor. Por ejemplo el siguiente código crea un clasificador multiclase basado en SVC usando OvR.

from sklearn.multiclass import OneVsRestClassifier

ovr_clf = OneVsRestClassifier(SVC(gamma="auto", random_state=42))

ovr_clf.fit(X_train[:1000], y_train[:1000])

ovr_clf.predict([some_digit])


Devuelve 0 pues en el post anterior definimos un cero del dataset con some_digit = X[234]

len(ovr_clf.estimators_)

Devuelve 

10

Entrenar un SGDClassifier es aún más fácil

from sklearn.linear_model import SGDClassifier

sgd_clf = SGDClassifier(max_iter=1000, tol=1e-3, random_state=42)

sgd_clf.fit(X_train, y_train)

sgd_clf.predict([some_digit])

Esta vez Sckit-Learn no ha corrido OvR ni OvO porque SGD soporta clasificación multiclase el método decision_function() devuelve un valor para cada clase, vamos a ver las puntuaciones obtenidas

sgd_clf.decision_function([some_digit])

Devuelve

array([[  3816.04556036, -32544.8070744 ,  -8613.62680163,

         -4792.16972022, -10146.9498229 ,  -4250.27483221,

        -21170.5186733 , -22260.53591175,  -2863.12785007,

        -16907.17784199]])

Podemos ver que el clasificador está bastante acertado con su predicción, pues todas las puntuaciones son negativas, mientras que la del 0 es positiva. 

Ahora queremos evaluar este clasificador, para ellos utilizamos validación cruzada como siempre. Utilizaremos la función cross_val_score() para utilizar la exactitud del clasificador  SGDClassifier

from sklearn.model_selection import cross_val_score

cross_val_score(sgd_clf,X_train,y_train,cv=3,scoring="accuracy")

Devuelve

array([0.87365, 0.85835, 0.8689 ])

Esto es un 85% como mínimo en todos los testeos, si hubiésemos utilizado un clasificador aleatorio obtendríamos un 10%, no está mal, pero podemos mejorarlo simplemente escalando las entradas 

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()

X_train_scaled = scaler.fit_transform(X_train.astype(np.float64))

cross_val_score(sgd_clf, X_train_scaled, y_train, cv=3, scoring="accuracy")

Análisis de error

Primero echaremos un vistazo a la matriz de confusión, necesitamos hacer predicciones utilizando la función cross_val_predict() y luego llamando a la función matriz de confusión confusión_matrix() Las filas muestran los números etiquetados (reales) y las columnas los números predichos,

y_train_pred = cross_val_predict(sgd_clf, X_train_scaled, y_train, cv=3)

conf_mx = confusion_matrix(y_train, y_train_pred)

conf_mx

Devuelve

array([[5577,    0,   22,    5,    8,   43,   36,    6,  225,    1],

       [   0, 6400,   37,   24,    4,   44,    4,    7,  212,   10],

       [  27,   27, 5220,   92,   73,   27,   67,   36,  378,   11],

       [  22,   17,  117, 5227,    2,  203,   27,   40,  403,   73],

       [  12,   14,   41,    9, 5182,   12,   34,   27,  347,  164],

       [  27,   15,   30,  168,   53, 4444,   75,   14,  535,   60],

       [  30,   15,   42,    3,   44,   97, 5552,    3,  131,    1],

       [  21,   10,   51,   30,   49,   12,    3, 5684,  195,  210],

       [  17,   63,   48,   86,    3,  126,   25,   10, 5429,   44],

       [  25,   18,   30,   64,  118,   36,    1,  179,  371, 5107]])

Hay un montón de números, si queremos una representación gráfica podemos hacer 

plt.matshow(conf_mx, cmap=plt.cm.gray)

plt.show()


Matriz de error



Esta representación de la matriz de confusión es bastante mejor, pues vemos que las imágenes pertenecientes a la diagonal se han clasificado correctamente, es decir la mayoría de ceros se ha clasificado como cero, los unos como uno, etc. Podemos ver que el 5 es un poco más oscuro que el resto, lo que significa que hay unas pocas imágenes de 5 que se han clasificado erróneamente.

Vamos a eliminar la diagonal para centrarnos en los errores

row_sums = conf_mx.sum(axis=1, keepdims=True)

norm_conf_mx = conf_mx / row_sums

np.fill_diagonal(norm_conf_mx, 0)

plt.matshow(norm_conf_mx, cmap=plt.cm.gray)

save_fig("confusion_matrix_errors_plot", tight_layout=False)

plt.show()

 



Podemos ver claramente los diferentes tipos de errores que ha cometido el clasificador. Recuerda que las filas representan los números reales, mientras que las columnas representas los números predichos. La columna del 8 es bastante clara, lo que indica que muchos números han sido clasificados de forma errónea como ocho, sin embargo la fila 8 es bastante oscura, lo que indica que la mayoría de los ochos se han clasificado correctamente como 8.

Esta matriz de confusión no tiene porqué ser simétrica, también podemos ver que se clasifican muchos 5 como 3 y muchos 3 como 5.

Echando un vistazo a esta matriz podemos ver que nuestros esfuerzos se deben centrar en reducir los falsos ochos y tratar de solucionar los intercambios entre 3 y 5. Centrémonos en este último punto. Por ejemplo vamos a mostrar los 3 y 5 que se han clasificado mal,

cl_a, cl_b = 3, 5

X_aa = X_train[(y_train == cl_a) & (y_train_pred == cl_a)]

X_ab = X_train[(y_train == cl_a) & (y_train_pred == cl_b)]

X_ba = X_train[(y_train == cl_b) & (y_train_pred == cl_a)]

X_bb = X_train[(y_train == cl_b) & (y_train_pred == cl_b)]

 

plt.figure(figsize=(8,8))

plt.subplot(221); plot_digits(X_aa[:25], images_per_row=5)

plt.subplot(222); plot_digits(X_ab[:25], images_per_row=5)

plt.subplot(223); plot_digits(X_ba[:25], images_per_row=5)

plt.subplot(224); plot_digits(X_bb[:25], images_per_row=5)

save_fig("analisis de erorres de 3 y 5")

plt.show()



Los dos bloques de la izquierda muestran dígitos clasificados como 3 y los dos bloques de la derecha los clasificados como 5. Algunos dígitos se han clasificado erróneamente (arriba-derecha y abajo izquierda)

Clasificación Multietiqueta (Multilabel)


Hasta ahora hemos asignado cada instancia a una sola clase, pero puede ser que deseemos clasificar en múltiples clases cada instancia. Por ejemplo si deseamos clasificar diferentes personas en diferentes fotos el clasificador deberá ser entrenado para reconocer digamos a Pepe, María y Luis y de este modo si en una foto sale María y Pepe el clasificador devolverá [1,1,0] significa que en dicha foto salen Pepe y María pero no Luis. Tal sistema de clasificación se llama clasificación multietiqueta.

El siguiente código genera un array multietiqueta que contiene dos etiquetas por cada dígito. La primera etiqueta indica si el número es grande o no (7,8 o 9) y el segundo indica si es o no impar.

from sklearn.neighbors import KNeighborsClassifier

y_train_large = (y_train >= 7)

y_train_odd = (y_train % 2 == 1)

y_multilabel = np.c_[y_train_large, y_train_odd]

knn_clf = KNeighborsClassifier()

knn_clf.fit(X_train, y_multilabel)

Las siguientes líneas crean una instancia KNeighborsClassifier  que devuelve dos etiquetas.

knn_clf.predict([some_digit])

Este es el resultado

array([[False, False]])

Una aproximación es medir la puntuación F1 para cada etiqueta individual, el código a continuación calcula la puntuación media F1

ojo la ejecución de esta línea puede tardar horas

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.976410265560605

Clasificación Multioutput

Es una generalización de la clasificación multilabel, para ilustrarlo vamos a construir un sistema que quita el ruido de las imágenes

noise = np.random.randint(0, 100, (len(X_train), 784))

X_train_mod = X_train + noise

noise = np.random.randint(0, 100, (len(X_test), 784))

X_test_mod = X_test + noise

y_train_mod = X_train

y_test_mod = X_test

some_index = 0

plt.subplot(121); plot_digit(X_test_mod[some_index])

plt.subplot(122); plot_digit(y_test_mod[some_index])

save_fig("ejemplo_de_ruido")

plt.show()

A la izquierda se muestra la imagen con ruido y a la derecha sin ruido

 

Clasificación Multioutput (Machine Learning)

Finalmente entrenaremos el clasificador para limpiar aún más esta imagen.

knn_clf.fit(X_train_mod, y_train_mod)

clean_digit = knn_clf.predict([X_test_mod[some_index]])

plot_digit(clean_digit)

save_fig("digito_limpio")

Vemos que finalmente queda bastante cercana al objetivo.

Clasificación Multioutput ML



No hay comentarios:

Publicar un comentario