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
¿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.
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).
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.
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:
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:
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
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:
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 |
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 |
La pérdida de entropía cruzada binaria se define como:
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.
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 .
Para implementar una clasificación probabilística multiclase NN necesitamos:
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) :
Codifiquemos una red neuronal para la clasificación de clases múltiples con el marco PyTorch.
Primero, instale
# 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
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é
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
Defina funciones para visualizar e imprimir estadísticas de conjuntos de datos. usos de la función show_dataset
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()
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]
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]
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.
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])
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
Esta sección muestra una implementación de todas las funciones necesarias para entrenar un modelo de clasificación binaria.
La implementación basada en PyTorch de la fórmula 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:
genere una matriz numpy test_input en el rango [-10, 11] con el paso 1
remodelarlo en un tensor con forma [7,3]
procese test_input con la función softmax implementada y la implementación predeterminada de PyTorch torch.nn.functional.softmax
comparar los resultados (deben ser idénticos)
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]
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:
generar matriz test_input con forma [10,5] y valores en el rango [0,1) con
genere una matriz test_target con forma [10,] y valores en el rango [0,4].
codificación one-hot matriz test_target
calcular la pérdida con la función cross_entropy implementada y la implementación de PyTorch
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
usaré
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))
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
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
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%
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()
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
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()
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')
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.