paint-brush
Clasificación de clases múltiples: comprensión de las funciones de activación y pérdida en redes neuronalespor@owlgrey
1,969 lecturas
1,969 lecturas

Clasificación de clases múltiples: comprensión de las funciones de activación y pérdida en redes neuronales

por Dmitrii Matveichev 25m2024/01/24
Read on Terminal Reader

Demasiado Largo; Para Leer

Para construir una red neuronal de clasificación de clases múltiples, debe utilizar la función de activación softmax en su capa final junto con la pérdida de entropía cruzada. El tamaño de capa final debe ser k, donde k es el número de clases. Los ID de clase deben procesarse previamente con codificación one-hot. Dicha red neuronal generará probabilidades p_i de que la entrada pertenezca a una clase i. Para encontrar el ID de clase predicho, necesita encontrar el índice de probabilidad máxima.
featured image - Clasificación de clases múltiples: comprensión de las funciones de activación y pérdida en redes neuronales
Dmitrii Matveichev  HackerNoon profile picture


Mi publicación anterior formula el problema de clasificación y lo divide en 3 tipos (binario, multiclase y multietiqueta) y responde a la pregunta "¿Qué funciones de activación y pérdida necesitas usar para resolver una tarea de clasificación binaria?".


En esta publicación, responderé la misma pregunta pero para la tarea de clasificación de clases múltiples y le brindaré una ejemplo de implementación de pytorch en Google colab .


¿Qué funciones de activación y pérdida necesita utilizar para resolver una tarea de clasificación de clases múltiples?


El código proporcionado se basa en gran medida en la implementación de la clasificación binaria, ya que necesita agregar muy pocas modificaciones a su código y NN para cambiar de clasificación binaria a multiclase. Los bloques de código modificados están marcados con (Cambiado) para facilitar la navegación.


1 ¿Por qué es importante comprender la función de activación y la pérdida utilizadas para la clasificación de clases múltiples?

Como se mostrará más adelante, la función de activación utilizada para la clasificación de clases múltiples es la activación softmax. Softmax se usa ampliamente en diferentes arquitecturas NN fuera de la clasificación de clases múltiples. Por ejemplo, softmax está en el núcleo del bloque de atención de múltiples cabezales utilizado en los modelos Transformer (consulte La atención es todo lo que necesita ) debido a su capacidad para convertir valores de entrada en una distribución de probabilidad (ver más sobre esto más adelante).


Atención de producto escalado (más comúnmente en el módulo de atención de múltiples cabezales)



Si conoce la motivación detrás de la aplicación de la activación softmax y la pérdida CE para resolver problemas de clasificación de clases múltiples, podrá comprender e implementar arquitecturas NN y funciones de pérdida mucho más complicadas.


2 Formulación de problemas de clasificación multiclase

El problema de clasificación de clases múltiples se puede representar como un conjunto de muestras {(x_1, y_1), (x_2, y_2),...,(x_n, y_n)} , donde x_i es un vector m-dimensional que contiene características de la muestra. i e y_i es la clase a la que pertenece x_i . Donde la etiqueta y_i puede asumir uno de los k valores, donde k es el número de clases superiores a 2. El objetivo es construir un modelo que prediga la etiqueta y_i para cada muestra de entrada x_i .

Ejemplos de tareas que pueden tratarse como problemas de clasificación de clases múltiples:

  • Diagnóstico médico: diagnosticar a un paciente con una de varias enfermedades basándose en los datos proporcionados (historial médico, resultados de pruebas, síntomas).
  • Categorización de productos: clasificación automática de productos para plataformas de comercio electrónico.
  • Predicción del tiempo: clasificar el tiempo futuro como soleado, nublado, lluvioso, etc.
  • categorizar películas, música y artículos en diferentes géneros
  • clasificar las opiniones de los clientes en línea en categorías como comentarios sobre productos, comentarios sobre servicios, quejas, etc.


3 funciones de activación y pérdida para clasificación multiclase


En la clasificación de clases múltiples se le proporciona:

  • un conjunto de muestras {(x_1, y_1), (x_2, y_2),...,(x_n, y_n)}

  • x_i es un vector m-dimensional que contiene características de la muestra i

  • y_i es la clase a la que pertenece x_i y puede asumir uno de los k valores, donde k>2 es el número de clases.


Para construir una red neuronal de clasificación multiclase como clasificador probabilístico necesitamos:

  • una capa de salida completamente conectada con un tamaño de k
  • los valores de salida deben estar en el rango [0,1]
  • la suma de los valores de salida debe ser igual a 1. En la clasificación de clases múltiples, cada entrada x puede pertenecer solo a una clase (clases mutuamente excluyentes), por lo tanto, la suma de probabilidades de todas las clases debe ser 1: SUM(p_0,…,p_k )=1 .
  • una función de pérdida que tiene el valor más bajo cuando la predicción y la verdad fundamental son las mismas


3.1 La función de activación de softmax

La capa lineal final de una red neuronal genera un vector de "valores de salida sin procesar". En el caso de la clasificación, los valores de salida representan la confianza del modelo de que la entrada pertenece a una de las k clases. Como se analizó anteriormente, la capa de salida debe tener un tamaño k y los valores de salida deben representar probabilidades p_i para cada una de las k clases y SUM(p_i)=1 .


El artículo sobre clasificación binaria utiliza la activación sigmoidea para transformar los valores de salida de NN en probabilidades. Intentemos aplicar sigmoide en k valores de salida en el rango [-3, 3] y veamos si sigmoide satisface los requisitos enumerados anteriormente:


  • Los valores de salida k deben estar en el rango (0,1), donde k es el número de clases.

  • la suma de k valores de salida debe ser igual a 1


    Definición de la función sigmoidea


    El artículo anterior muestra que la función sigmoidea asigna valores de entrada a un rango (0,1). Veamos si la activación sigmoidea satisface el segundo requisito. En la siguiente tabla de ejemplo, procesé un vector con tamaño k (k=7) con activación sigmoidea y resumí todos estos valores: la suma de estos 7 valores es igual a 3,5. Una forma sencilla de solucionar este problema sería dividir todos los valores de k por su suma.


Aporte

-3

-2

-1

0

1

2

3

SUMA

salida sigmoidea

0.04743

0.11920

0.26894

0.50000

0.73106

0.88080

0.95257

3.5000


Otra forma sería tomar el exponente del valor de entrada y dividirlo por la suma de los exponentes de todos los valores de entrada:


Definición de la función Softmax


La función softmax transforma un vector de números reales en un vector de probabilidades. Cada probabilidad en el resultado está en el rango (0,1) y la suma de las probabilidades es 1.

Aporte

-3

-2

-1

0

1

2

3

SUMA

softmax

0.00157

0.00426

0.01159

0.03150

0.08563

0.23276

0.63270

1

La gráfica del exponente en el rango [-10, 10]


Softmax de un vector con tamaño 21 con valores [-10, 10]


Hay una cosa que debes tener en cuenta cuando trabajas con softmax: el valor de salida p_i depende de todos los valores en la matriz de entrada ya que lo dividimos por la suma de los exponentes de todos los valores. La siguiente tabla demuestra esto: dos vectores de entrada tienen 3 valores comunes {1, 3, 4}, pero los valores softmax de salida difieren porque el segundo elemento es diferente (2 y 4).

Entrada 1

1

2

3

4

softmax 1

0.0321

0.0871

0.2369

0.6439

Entrada 2

1

4

3

4

softmax 2

0.0206

0.4136

0,1522

0.4136


3.2 Pérdida de entropía cruzada

La pérdida de entropía cruzada binaria se define como:

Pérdida de entropía cruzada binaria


En la clasificación binaria, hay dos probabilidades de salida p_i y (1-p_i) y valores de verdad fundamentales y_i y (1-y_i).


El problema de clasificación de clases múltiples utiliza la generalización de la pérdida BCE para N clases: pérdida de entropía cruzada.


Pérdida de entropía cruzada


N es el número de muestras de entrada, y_i es la verdad fundamental y p_i es la probabilidad predicha de la clase i .


4 Ejemplo de clasificación NN de clases múltiples con PyTorch

Para implementar una clasificación probabilística multiclase NN necesitamos:

  • La verdad fundamental y las predicciones deben tener dimensiones [N,k] donde N es el número de muestras de entrada, k es el número de clases; la identificación de clase debe codificarse en un vector con tamaño k
  • el tamaño de la capa lineal final debe ser k
  • Las salidas de la capa final deben procesarse con activación softmax para obtener probabilidades de salida.
  • La pérdida de CE debe aplicarse a las probabilidades de clase previstas y a los valores de verdad fundamentales.
  • encuentre la identificación de la clase de salida del vector de salida con tamaño k



El proceso de entrenamiento de una clasificación de clases múltiples NN.


La mayoría de las partes del código se basan en el código del artículo anterior sobre clasificación binaria.


Las piezas cambiadas están marcadas con (Cambiado) :

  • preprocesamiento y posprocesamiento de datos
  • función de activación
  • función de pérdida
  • métrica de rendimiento
  • matriz de confusión


Codifiquemos una red neuronal para la clasificación de clases múltiples con el marco PyTorch.

Primero, instale antorchamétrica - Este paquete se utilizará más adelante para calcular la precisión de la clasificación y la matriz de confusión.


 # used for accuracy metric and confusion matrix !pip install torchmetrics


Importar paquetes que se utilizarán más adelante en el código.

 from sklearn.datasets import make_classification import numpy as np import torch import torchmetrics import matplotlib.pyplot as plt import seaborn as sn import pandas as pd from sklearn.decomposition import PCA


4.1 Crear conjunto de datos

Establezca la variable global con el número de clases (si la configura en 2 y obtiene la clasificación binaria NN que usa softmax y pérdida de entropía cruzada)


 number_of_classes=4


usaré sklearn.datasets.make_classification para generar un conjunto de datos de clasificación binaria:

  • n_samples : es el número de muestras generadas.

  • n_features : establece el número de dimensiones de las muestras generadas X

  • n_classes : el número de clases en el conjunto de datos generado. En el problema de clasificación de clases múltiples, debería haber más de 2 clases.


El conjunto de datos generado tendrá X con forma [n_samples, n_features] e Y con forma [n_samples, ] .

 def get_dataset(n_samples=10000, n_features=20, n_classes=2): # https://scikit-learn.org/stable/modules/generated/sklearn.datasets.make_classification.html#sklearn.datasets.make_classification data_X, data_y = make_classification(n_samples=n_samples, n_features=n_features, n_classes=n_classes, n_informative=n_classes, n_redundant=0, n_clusters_per_class=2, random_state=42, class_sep=4) return data_X, data_y


4.2 Visualización del conjunto de datos

Defina funciones para visualizar e imprimir estadísticas de conjuntos de datos. usos de la función show_dataset PCA para reducir la dimensionalidad de X desde cualquier número hasta 2 para simplificar la visualización de los datos de entrada X en el gráfico 2D.


 def print_dataset(X, y): print(f'X shape: {X.shape}, min: {X.min()}, max: {X.max()}') print(f'y shape: {y.shape}') print(y[:10]) def show_dataset(X, y, title=''): if X.shape[1] > 2: X_pca = PCA(n_components=2).fit_transform(X) else: X_pca = X fig = plt.figure(figsize=(4, 4)) plt.scatter(x=X_pca[:, 0], y=X_pca[:, 1], c=y, alpha=0.5) # generate colors for all classes colors = plt.cm.rainbow(np.linspace(0, 1, number_of_classes)) # iterate over classes and visualize them with the dedicated color for class_id in range(number_of_classes): class_mask = np.argwhere(y == class_id) X_class = X_pca[class_mask[:, 0]] plt.scatter(x=X_class[:, 0], y=X_class[:, 1], c=np.full((X_class[:, 0].shape[0], 4), colors[class_id]), label=class_id, alpha=0.5) plt.title(title) plt.legend(loc="best", title="Classes") plt.xticks() plt.yticks() plt.show()



4.3 Escalador de conjuntos de datos

Escale las características del conjunto de datos X al rango [0,1] con el escalador mínimo máximo. Esto generalmente se hace para un entrenamiento más rápido y estable.


 def scale(x_in): return (x_in - x_in.min(axis=0))/(x_in.max(axis=0)-x_in.min(axis=0))


Imprimamos las estadísticas del conjunto de datos generado y visualicémoslas con las funciones anteriores.

 X, y = get_dataset(n_classes=number_of_classes) print('before scaling') print_dataset(X, y) show_dataset(X, y, 'before') X_scaled = scale(X) print('after scaling') print_dataset(X_scaled, y) show_dataset(X_scaled, y, 'after')


Los resultados que debería obtener se encuentran a continuación.

 before scaling X shape: (10000, 20), min: -9.549551632357336, max: 9.727761741276673 y shape: (10000,) [0 2 1 2 0 2 0 1 1 2] 

El conjunto de datos antes del escalado mínimo-máximo


 after scaling X shape: (10000, 20), min: 0.0, max: 1.0 y shape: (10000,) [0 2 1 2 0 2 0 1 1 2] 

El conjunto de datos después del escalado mínimo-máximo


La escala mínima-máxima no distorsiona las características del conjunto de datos, las transforma linealmente en el rango [0,1]. La figura del "conjunto de datos después del escalamiento mínimo-máximo" parece estar distorsionada en comparación con la figura anterior porque el algoritmo PCA reduce 20 dimensiones a 2 y el algoritmo PCA puede verse afectado por el escalamiento mínimo-máximo.


Cree cargadores de datos de PyTorch. sklearn.datasets.make_classification genera el conjunto de datos como dos matrices numerosas. Para crear cargadores de datos de PyTorch, necesitamos transformar el conjunto de datos numeroso en torch.tensor con torch.utils.data.TensorDataset.


 def get_data_loaders(dataset, batch_size=32, shuffle=True): data_X, data_y = dataset # https://pytorch.org/docs/stable/data.html#torch.utils.data.TensorDataset torch_dataset = torch.utils.data.TensorDataset(torch.tensor(data_X, dtype=torch.float32), torch.tensor(data_y, dtype=torch.float32)) # https://pytorch.org/docs/stable/data.html#torch.utils.data.random_split train_dataset, val_dataset = torch.utils.data.random_split(torch_dataset, [int(len(torch_dataset)*0.8), int(len(torch_dataset)*0.2)], torch.Generator().manual_seed(42)) # https://pytorch.org/docs/stable/data.html#torch.utils.data.DataLoader loader_train = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=shuffle) loader_val = torch.utils.data.DataLoader(val_dataset, batch_size=batch_size, shuffle=shuffle) return loader_train, loader_val


Pruebe los cargadores de datos de PyTorch

 dataloader_train, dataloader_val = get_data_loaders(get_dataset(n_classes=number_of_classes), batch_size=32) train_batch_0 = next(iter(dataloader_train)) print(f'Batches in the train dataloader: {len(dataloader_train)}, X: {train_batch_0[0].shape}, Y: {train_batch_0[1].shape}') val_batch_0 = next(iter(dataloader_val)) print(f'Batches in the validation dataloader: {len(dataloader_val)}, X: {val_batch_0[0].shape}, Y: {val_batch_0[1].shape}')


La salida:

 Batches in the train dataloader: 250, X: torch.Size([32, 20]), Y: torch.Size([32]) Batches in the validation dataloader: 63, X: torch.Size([32, 20]), Y: torch.Size([32])


4.4 Preprocesamiento y posprocesamiento del conjunto de datos (modificado)

Cree funciones de pre y posprocesamiento. Como habrás notado antes, la forma Y actual es [N], necesitamos que sea [N,número_de_clases]. Para hacer eso, necesitamos codificar en caliente los valores en el vector Y.


La codificación one-hot es un proceso de conversión de índices de clases en una representación binaria donde cada clase está representada por un vector binario único.


En otras palabras: cree un vector cero con el tamaño [número_de_clases] y establezca el elemento en la posición class_id en 1, donde class_ids {0,1,…,número_de_clases-1}:

0 >> [1. 0. 0. 0.]

1 >> [0. 1. 0. 0.]

2 >> [0. 0. 1. 0.]

2 >> [0. 0. 0. 1.]


Los tensores de Pytorch se pueden procesar con torch.nn.function.one_hot y la implementación numpy es muy sencilla. El vector de salida tendrá la forma [N,número_de_clases].

 def preprocessing(y, n_classes): ''' one-hot encoding for input numpy array or pytorch Tensor input: y - [N,] numpy array or pytorch Tensor output: [N, n_classes] the same type as input ''' assert type(y)==np.ndarray or torch.is_tensor(y), f'input should be numpy array or torch tensor. Received input is: {type(categorical)}' assert len(y.shape)==1, f'input shape should be [N,]. Received input shape is: {y.shape}' if torch.is_tensor(y): return torch.nn.functional.one_hot(y, num_classes=n_classes) else: categorical = np.zeros([y.shape[0], n_classes]) categorical[np.arange(y.shape[0]), y]=1 return categorical


Para convertir el vector codificado en caliente nuevamente a la identificación de clase, necesitamos encontrar el índice del elemento máximo en el vector codificado en caliente. Se puede hacer con torch.argmax o np.argmax a continuación.

 def postprocessing(categorical): ''' one-hot to classes decoding with .argmax() input: categorical - [N,classes] numpy array or pytorch Tensor output: [N,] the same type as input ''' assert type(categorical)==np.ndarray or torch.is_tensor(categorical), f'input should be numpy array or torch tensor. Received input is: {type(categorical)}' assert len(categorical.shape)==2, f'input shape should be [N,classes]. Received input shape is: {categorical.shape}' if torch.is_tensor(categorical): return torch.argmax(categorical,dim=1) else: return np.argmax(categorical, axis=1)


Pruebe las funciones de pre y posprocesamiento definidas.

 y = get_dataset(n_classes=number_of_classes)[1] y_logits = preprocessing(y, n_classes=number_of_classes) y_class = postprocessing(y_logits) print(f'y shape: {y.shape}, y preprocessed shape: {y_logits.shape}, y postprocessed shape: {y_class.shape}') print('Preprocessing does one-hot encoding of class ids.') print('Postprocessing does one-hot decoding of class one-hot encoded class ids.') for i in range(10): print(f'{y[i]} >> {y_logits[i]} >> {y_class[i]}')


La salida:

 y shape: (10000,), y preprocessed shape: (10000, 4), y postprocessed shape: (10000,) Preprocessing does one-hot encoding of class ids. Postprocessing does one-hot decoding of one-hot encoded class ids. id>>one-hot encoding>>id 0 >> [1. 0. 0. 0.] >> 0 2 >> [0. 0. 1. 0.] >> 2 1 >> [0. 1. 0. 0.] >> 1 2 >> [0. 0. 1. 0.] >> 2 0 >> [1. 0. 0. 0.] >> 0 2 >> [0. 0. 1. 0.] >> 2 0 >> [1. 0. 0. 0.] >> 0 1 >> [0. 1. 0. 0.] >> 1 1 >> [0. 1. 0. 0.] >> 1 2 >> [0. 0. 1. 0.] >> 2


4.5 Crear y entrenar un modelo de clasificación de clases múltiples

Esta sección muestra una implementación de todas las funciones necesarias para entrenar un modelo de clasificación binaria.


4.5.1 Activación de Softmax (modificado)

La implementación basada en PyTorch de la fórmula softmax

Definición de activación de Softmax


 def softmax(x): assert len(x.shape)==2, f'input shape should be [N,classes]. Received input shape is: {x.shape}' # Subtract the maximum value for numerical stability # you can find explanation here: https://www.deeplearningbook.org/contents/numerical.html x = x - torch.max(x, dim=1, keepdim=True)[0] # Exponentiate the values exp_x = torch.exp(x) # Sum along the specified dimension sum_exp_x = torch.sum(exp_x, dim=1, keepdim=True) # Compute the softmax return exp_x / sum_exp_x


Probemos softmax:

  1. genere una matriz numpy test_input en el rango [-10, 11] con el paso 1

  2. remodelarlo en un tensor con forma [7,3]

  3. procese test_input con la función softmax implementada y la implementación predeterminada de PyTorch torch.nn.functional.softmax

  4. comparar los resultados (deben ser idénticos)

  5. generar valores softmax y suma para los siete tensores [1,3]


 test_input = torch.arange(-10, 11, 1, dtype=torch.float32) test_input = test_input.reshape(-1,3) softmax_output = softmax(test_input) print(f'Input data shape: {test_input.shape}') print(f'input data range: [{test_input.min():.3f}, {test_input.max():.3f}]') print(f'softmax output data range: [{softmax_output.min():.3f}, {softmax_output.max():.3f}]') print(f'softmax output data sum along axis 1: [{softmax_output.sum(axis=1).numpy()}]') softmax_output_pytorch = torch.nn.functional.softmax(test_input, dim=1) print(f'softmax output is the same with pytorch implementation: {(softmax_output_pytorch==softmax_output).all().numpy()}') print('Softmax activation changes values in the chosen axis (1) so that they always sum up to 1:') for i in range(softmax_output.shape[0]): print(f'\t{i}. Sum before softmax: {test_input[i].sum().numpy()} | Sum after softmax: {softmax_output[i].sum().numpy()}') print(f'\t values before softmax: {test_input[i].numpy()}, softmax output values: {softmax_output[i].numpy()}')


La salida:

 Input data shape: torch.Size([7, 3]) input data range: [-10.000, 10.000] softmax output data range: [0.090, 0.665] softmax output data sum along axis 1: [[1. 1. 1. 1. 1. 1. 1.]] softmax output is the same with pytorch implementation: True Softmax activation changes values in the chosen axis (1) so that they always sum up to 1: 0. Sum before softmax: -27.0 | Sum after softmax: 1.0 values before softmax: [-10. -9. -8.], softmax output values: [0.09003057 0.24472848 0.66524094] 1. Sum before softmax: -18.0 | Sum after softmax: 1.0 values before softmax: [-7. -6. -5.], softmax output values: [0.09003057 0.24472848 0.66524094] 2. Sum before softmax: -9.0 | Sum after softmax: 1.0 values before softmax: [-4. -3. -2.], softmax output values: [0.09003057 0.24472848 0.66524094] 3. Sum before softmax: 0.0 | Sum after softmax: 1.0 values before softmax: [-1. 0. 1.], softmax output values: [0.09003057 0.24472848 0.66524094] 4. Sum before softmax: 9.0 | Sum after softmax: 1.0 values before softmax: [2. 3. 4.], softmax output values: [0.09003057 0.24472848 0.66524094] 5. Sum before softmax: 18.0 | Sum after softmax: 1.0 values before softmax: [5. 6. 7.], softmax output values: [0.09003057 0.24472848 0.66524094] 6. Sum before softmax: 27.0 | Sum after softmax: 1.0 values before softmax: [ 8. 9. 10.], softmax output values: [0.09003057 0.24472848 0.66524094]


4.5.2 Función de pérdida: entropía cruzada (modificada)

La implementación basada en PyTorch de la fórmula CE

 def cross_entropy_loss(softmax_logits, labels): # Calculate the cross-entropy loss loss = -torch.sum(labels * torch.log(softmax_logits)) / softmax_logits.size(0) return loss


Prueba de implementación CE:


  1. generar matriz test_input con forma [10,5] y valores en el rango [0,1) con antorcha.rand

  2. genere una matriz test_target con forma [10,] y valores en el rango [0,4].

  3. codificación one-hot matriz test_target

  4. calcular la pérdida con la función cross_entropy implementada y la implementación de PyTorch antorcha.nn.funcional.binary_cross_entropy

  5. comparar los resultados (deben ser idénticos)


 test_input = torch.rand(10, 5, requires_grad=False) test_target = torch.randint(0, 5, (10,), requires_grad=False) test_target = preprocessing(test_target, n_classes=5).float() print(f'test_input shape: {list(test_input.shape)}, test_target shape: {list(test_target.shape)}') # get loss with the cross_entropy_loss implementation loss = cross_entropy_loss(softmax(test_input), test_target) # get loss with the torch.nn.functional.cross_entropy implementation # !!!torch.nn.functional.cross_entropy applies softmax on input logits # !!!pass it test_input without softmax activation loss_pytorch = torch.nn.functional.cross_entropy(test_input, test_target) print(f'Loss outputs are the same: {(loss==loss_pytorch).numpy()}')


El resultado esperado:

 test_input shape: [10, 5], test_target shape: [10, 5] Loss outputs are the same: True


4.5.3 Métrica de precisión (modificada)

usaré antorchamétrica implementación para calcular la precisión basada en predicciones de modelos y verdad sobre el terreno.


Para crear una métrica de precisión de clasificación de varias clases, se requieren dos parámetros:

  • tipo de tarea "multiclase"

  • número de clases num_clases


 # https://torchmetrics.readthedocs.io/en/stable/classification/accuracy.html#module-interface accuracy_metric=torchmetrics.classification.Accuracy(task="multiclass", num_classes=number_of_classes) def compute_accuracy(y_pred, y): assert len(y_pred.shape)==2 and y_pred.shape[1] == number_of_classes, 'y_pred shape should be [N, C]' assert len(y.shape)==2 and y.shape[1] == number_of_classes, 'y shape should be [N, C]' return accuracy_metric(postprocessing(y_pred), postprocessing(y))


4.5.4 Modelo NN

El NN utilizado en este ejemplo es un NN profundo con 2 capas ocultas. Las capas de entrada y ocultas usan la activación ReLU y la capa final usa la función de activación proporcionada como entrada de clase (será la función de activación sigmoidea que se implementó antes).


 class ClassifierNN(torch.nn.Module): def __init__(self, loss_function, activation_function, input_dims=2, output_dims=1): super().__init__() self.linear1 = torch.nn.Linear(input_dims, input_dims * 4) self.linear2 = torch.nn.Linear(input_dims * 4, input_dims * 8) self.linear3 = torch.nn.Linear(input_dims * 8, input_dims * 4) self.output = torch.nn.Linear(input_dims * 4, output_dims) self.loss_function = loss_function self.activation_function = activation_function def forward(self, x): x = torch.nn.functional.relu(self.linear1(x)) x = torch.nn.functional.relu(self.linear2(x)) x = torch.nn.functional.relu(self.linear3(x)) x = self.activation_function(self.output(x)) return x


4.5.5 Entrenamiento, evaluación y predicción

El proceso de entrenamiento de una clasificación de clases múltiples NN.


La figura anterior muestra la lógica de entrenamiento para un solo lote. Posteriormente, la función train_epoch se llamará varias veces (número elegido de épocas).


 def train_epoch(model, optimizer, dataloader_train): # set the model to the training mode # https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.train model.train() losses = [] accuracies = [] for step, (X_batch, y_batch) in enumerate(dataloader_train): ### forward propagation # get model output and use loss function y_pred = model(X_batch) # get class probabilities with shape [N,1] # apply loss function on predicted probabilities and ground truth loss = model.loss_function(y_pred, y_batch) ### backward propagation # set gradients to zero before backpropagation # https://pytorch.org/docs/stable/generated/torch.optim.Optimizer.zero_grad.html optimizer.zero_grad() # compute gradients # https://pytorch.org/docs/stable/generated/torch.Tensor.backward.html loss.backward() # update weights # https://pytorch.org/docs/stable/optim.html#taking-an-optimization-step optimizer.step() # update model weights # calculate batch accuracy acc = compute_accuracy(y_pred, y_batch) # append batch loss and accuracy to corresponding lists for later use accuracies.append(acc) losses.append(float(loss.detach().numpy())) # compute average epoch accuracy train_acc = np.array(accuracies).mean() # compute average epoch loss loss_epoch = np.array(losses).mean() return train_acc, loss_epoch


La función de evaluación itera sobre el cargador de datos PyTorch proporcionado, calcula la precisión del modelo actual y devuelve la pérdida promedio y la precisión promedio.


 def evaluate(model, dataloader_in): # set the model to the evaluation mode # https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.eval model.eval() val_acc_epoch = 0 losses = [] accuracies = [] # disable gradient calculation for evaluation # https://pytorch.org/docs/stable/generated/torch.no_grad.html with torch.no_grad(): for step, (X_batch, y_batch) in enumerate(dataloader_in): # get predictions y_pred = model(X_batch) # calculate loss loss = model.loss_function(y_pred, y_batch) # calculate batch accuracy acc = compute_accuracy(y_pred, y_batch) accuracies.append(acc) losses.append(float(loss.detach().numpy())) # compute average accuracy val_acc = np.array(accuracies).mean() # compute average loss loss_epoch = np.array(losses).mean() return val_acc, loss_epoch 


La función de predicción itera sobre el cargador de datos proporcionado, recopila predicciones de modelos posprocesados (decodificados en caliente) y valores de verdad del terreno en [N,1] matrices PyTorch y devuelve ambas matrices. Posteriormente, esta función se utilizará para calcular la matriz de confusión y visualizar predicciones.


 def predict(model, dataloader): # set the model to the evaluation mode # https://pytorch.org/docs/stable/generated/torch.nn.Module.html#torch.nn.Module.eval model.eval() xs, ys = next(iter(dataloader)) y_pred = torch.empty([0, ys.shape[1]]) x = torch.empty([0, xs.shape[1]]) y = torch.empty([0, ys.shape[1]]) # disable gradient calculation for evaluation # https://pytorch.org/docs/stable/generated/torch.no_grad.html with torch.no_grad(): for step, (X_batch, y_batch) in enumerate(dataloader): # get predictions y_batch_pred = model(X_batch) y_pred = torch.cat([y_pred, y_batch_pred]) y = torch.cat([y, y_batch]) x = torch.cat([x, X_batch]) # print(y_pred.shape, y.shape) y_pred = postprocessing(y_pred) y = postprocessing(y) return y_pred, y, x


Para entrenar el modelo solo necesitamos llamar a la función train_epoch N veces, donde N es el número de épocas. Se llama a la función de evaluación para registrar la precisión del modelo actual en el conjunto de datos de validación. Finalmente, el mejor modelo se actualiza en función de la precisión de la validación. La función model_train devuelve la mejor precisión de validación y el historial de entrenamiento.


 def model_train(model, optimizer, dataloader_train, dataloader_val, n_epochs=50): best_acc = 0 best_weights = None history = {'loss': {'train': [], 'validation': []}, 'accuracy': {'train': [], 'validation': []}} for epoch in range(n_epochs): # train on dataloader_train acc_train, loss_train = train_epoch(model, optimizer, dataloader_train) # evaluate on dataloader_val acc_val, loss_val = evaluate(model, dataloader_val) print(f'Epoch: {epoch} | Accuracy: {acc_train:.3f} / {acc_val:.3f} | ' + f'loss: {loss_train:.5f} / {loss_val:.5f}') # save epoch losses and accuracies in history dictionary history['loss']['train'].append(loss_train) history['loss']['validation'].append(loss_val) history['accuracy']['train'].append(acc_train) history['accuracy']['validation'].append(acc_val) # Save the best validation accuracy model if acc_val >= best_acc: print(f'\tBest weights updated. Old accuracy: {best_acc:.4f}. New accuracy: {acc_val:.4f}') best_acc = acc_val torch.save(model.state_dict(), 'best_weights.pt') # restore model and return best accuracy model.load_state_dict(torch.load('best_weights.pt')) return best_acc, history


4.5.6 Obtener el conjunto de datos, crear el modelo y entrenarlo (modificado)

Juntemos todo y entrenemos el modelo de clasificación de clases múltiples.

 ######################################### # Get the dataset X, y = get_dataset(n_classes=number_of_classes) print(f'Generated dataset shape. X:{X.shape}, y:{y.shape}') # change y numpy array shape from [N,] to [N, C] for multi-class classification y = preprocessing(y, n_classes=number_of_classes) print(f'Dataset shape prepared for multi-class classification with softmax activation and CE loss.') print(f'X:{X.shape}, y:{y.shape}') # Get train and validation datal loaders dataloader_train, dataloader_val = get_data_loaders(dataset=(scale(X), y), batch_size=32) # get a batch from dataloader and output intput and output shape X_0, y_0 = next(iter(dataloader_train)) print(f'Model input data shape: {X_0.shape}, output (ground truth) data shape: {y_0.shape}') ######################################### # Create ClassifierNN for multi-class classification problem # input dims: [N, features] # output dims: [N, C] where C is number of classes # activation - softmax to output [,C] probabilities so that their sum(p_1,p_2,...,p_c)=1 # loss - cross-entropy model = ClassifierNN(loss_function=cross_entropy_loss, activation_function=softmax, input_dims=X.shape[1], output_dims=y.shape[1]) ######################################### # create optimizer and train the model on the dataset optimizer = torch.optim.Adam(model.parameters(), lr=0.001) print(f'Model size: {sum([x.reshape(-1).shape[0] for x in model.parameters()])} parameters') print('#'*10) print('Start training') acc, history = model_train(model, optimizer, dataloader_train, dataloader_val, n_epochs=20) print('Finished training') print('#'*10) print("Model accuracy: %.2f%%" % (acc*100))


El resultado esperado debería ser similar al que se proporciona a continuación.

 Generated dataset shape. X:(10000, 20), y:(10000,) Dataset shape prepared for multi-class classification with softmax activation and CE loss. X:(10000, 20), y:(10000, 4) Model input data shape: torch.Size([32, 20]), output (ground truth) data shape: torch.Size([32, 4]) Model size: 27844 parameters ########## Start training Epoch: 0 | Accuracy: 0.682 / 0.943 | loss: 0.78574 / 0.37459 Best weights updated. Old accuracy: 0.0000. New accuracy: 0.9435 Epoch: 1 | Accuracy: 0.960 / 0.967 | loss: 0.20272 / 0.17840 Best weights updated. Old accuracy: 0.9435. New accuracy: 0.9668 Epoch: 2 | Accuracy: 0.978 / 0.962 | loss: 0.12004 / 0.17931 Epoch: 3 | Accuracy: 0.984 / 0.979 | loss: 0.10028 / 0.13246 Best weights updated. Old accuracy: 0.9668. New accuracy: 0.9787 Epoch: 4 | Accuracy: 0.985 / 0.981 | loss: 0.08838 / 0.12720 Best weights updated. Old accuracy: 0.9787. New accuracy: 0.9807 Epoch: 5 | Accuracy: 0.986 / 0.981 | loss: 0.08096 / 0.12174 Best weights updated. Old accuracy: 0.9807. New accuracy: 0.9812 Epoch: 6 | Accuracy: 0.986 / 0.981 | loss: 0.07944 / 0.12036 Epoch: 7 | Accuracy: 0.988 / 0.982 | loss: 0.07605 / 0.11773 Best weights updated. Old accuracy: 0.9812. New accuracy: 0.9821 Epoch: 8 | Accuracy: 0.989 / 0.982 | loss: 0.07168 / 0.11514 Best weights updated. Old accuracy: 0.9821. New accuracy: 0.9821 Epoch: 9 | Accuracy: 0.989 / 0.983 | loss: 0.06890 / 0.11409 Best weights updated. Old accuracy: 0.9821. New accuracy: 0.9831 Epoch: 10 | Accuracy: 0.989 / 0.984 | loss: 0.06750 / 0.11128 Best weights updated. Old accuracy: 0.9831. New accuracy: 0.9841 Epoch: 11 | Accuracy: 0.990 / 0.982 | loss: 0.06505 / 0.11265 Epoch: 12 | Accuracy: 0.990 / 0.983 | loss: 0.06507 / 0.11272 Epoch: 13 | Accuracy: 0.991 / 0.985 | loss: 0.06209 / 0.11240 Best weights updated. Old accuracy: 0.9841. New accuracy: 0.9851 Epoch: 14 | Accuracy: 0.990 / 0.984 | loss: 0.06273 / 0.11157 Epoch: 15 | Accuracy: 0.991 / 0.984 | loss: 0.05998 / 0.11029 Epoch: 16 | Accuracy: 0.990 / 0.985 | loss: 0.06056 / 0.11164 Epoch: 17 | Accuracy: 0.991 / 0.984 | loss: 0.05981 / 0.11096 Epoch: 18 | Accuracy: 0.991 / 0.985 | loss: 0.05642 / 0.10975 Best weights updated. Old accuracy: 0.9851. New accuracy: 0.9851 Epoch: 19 | Accuracy: 0.990 / 0.986 | loss: 0.05929 / 0.10821 Best weights updated. Old accuracy: 0.9851. New accuracy: 0.9856 Finished training ########## Model accuracy: 98.56%


4.5.7 Trazar el historial de entrenamiento

 def plot_history(history): fig = plt.figure(figsize=(8, 4), facecolor=(0.0, 1.0, 0.0)) ax = fig.add_subplot(1, 2, 1) ax.plot(np.arange(0, len(history['loss']['train'])), history['loss']['train'], color='red', label='train') ax.plot(np.arange(0, len(history['loss']['validation'])), history['loss']['validation'], color='blue', label='validation') ax.set_title('Loss history') ax.set_facecolor((0.0, 1.0, 0.0)) ax.legend() ax = fig.add_subplot(1, 2, 2) ax.plot(np.arange(0, len(history['accuracy']['train'])), history['accuracy']['train'], color='red', label='train') ax.plot(np.arange(0, len(history['accuracy']['validation'])), history['accuracy']['validation'], color='blue', label='validation') ax.set_title('Accuracy history') ax.legend() fig.tight_layout() ax.set_facecolor((0.0, 1.0, 0.0)) fig.show() 

Historial de precisión y pérdida de entrenamiento y validación


4.6 Evaluar el modelo


4.6.1 Calcular la precisión del tren y la validación

 acc_train, _ = evaluate(model, dataloader_train) acc_validation, _ = evaluate(model, dataloader_val) print(f'Accuracy - Train: {acc_train:.4f} | Validation: {acc_validation:.4f}')
 Accuracy - Train: 0.9901 | Validation: 0.9851


4.6.2 Imprimir matriz de confusión (modificado)

 val_preds, val_y, _ = predict(model, dataloader_val) print(val_preds.shape, val_y.shape) multiclass_confusion_matrix = torchmetrics.classification.ConfusionMatrix('multiclass', num_classes=number_of_classes) cm = multiclass_confusion_matrix(val_preds, val_y) print(cm) df_cm = pd.DataFrame(cm) plt.figure(figsize = (6,5), facecolor=(0.0,1.0,0.0)) sn.heatmap(df_cm, annot=True, fmt='d') plt.show() 

Matriz de confusión en el conjunto de datos de validación


4.6.3 Predicciones gráficas y verdad fundamental

 val_preds, val_y, val_x = predict(model, dataloader_val) val_preds, val_y, val_x = val_preds.numpy(), val_y.numpy(), val_x.numpy() show_dataset(val_x, val_y,'Ground Truth') show_dataset(val_x, val_preds, 'Predictions') 


Verdad fundamental del conjunto de datos de validación

Predicciones del modelo en el conjunto de datos de validación.


Conclusión

Para la clasificación de clases múltiples, debe utilizar la activación softmax y la pérdida de entropía cruzada. Se requieren algunas modificaciones de código para pasar de la clasificación binaria a la clasificación de clases múltiples: funciones de preprocesamiento y posprocesamiento de datos, activación y pérdida. Además, puede resolver el problema de clasificación binaria estableciendo el número de clases en 2 con codificación one-hot, softmax y pérdida de entropía cruzada.