Meu post anterior formula o problema de classificação e o divide em 3 tipos (binário, multiclasse e multirótulo) e responde à pergunta “Quais funções de ativação e perda você precisa usar para resolver uma tarefa de classificação binária?”.
Nesta postagem, responderei à mesma pergunta, mas para a tarefa de classificação multiclasse e fornecerei a você um
Quais funções de ativação e perda você precisa usar para resolver uma tarefa de classificação multiclasse?
O código fornecido é amplamente baseado na implementação da classificação binária, pois você precisa adicionar muito poucas modificações ao seu código e NN para mudar da classificação binária para multiclasse. Os blocos de código modificados são marcados com (Alterado) para facilitar a navegação.
Como será mostrado posteriormente, a função de ativação utilizada para classificação multiclasse é a ativação softmax. Softmax é amplamente utilizado em diferentes arquiteturas NN fora da classificação multiclasse. Por exemplo, softmax está no centro do bloco de atenção multicabeças usado nos modelos Transformer (veja Atenção é tudo que você precisa ) devido à sua capacidade de converter valores de entrada em uma distribuição de probabilidade (veja mais sobre isso mais tarde).
Se você conhece a motivação por trás da aplicação da ativação softmax e perda CE para resolver problemas de classificação multiclasse, você será capaz de entender e implementar arquiteturas NN e funções de perda muito mais complicadas.
O problema de classificação multiclasse pode ser representado como um conjunto de amostras {(x_1, y_1), (x_2, y_2),...,(x_n, y_n)} , onde x_i é um vetor m-dimensional que contém características da amostra i e y_i são a classe à qual x_i pertence. Onde o rótulo y_i pode assumir um dos k valores, onde k é o número de classes superior a 2. O objetivo é construir um modelo que preveja o rótulo y_i para cada amostra de entrada x_i .
Exemplos de tarefas que podem ser tratadas como problemas de classificação multiclasse:
Na classificação multiclasse você recebe:
um conjunto de amostras {(x_1, y_1), (x_2, y_2),...,(x_n, y_n)}
x_i é um vetor m-dimensional que contém características da amostra i
y_i é a classe à qual x_i pertence e pode assumir um dos k valores, onde k>2 é o número de classes.
Para construir uma rede neural de classificação multiclasse como classificador probabilístico, precisamos:
A camada linear final de uma rede neural produz um vetor de "valores de saída brutos". No caso de classificação, os valores de saída representam a confiança do modelo de que a entrada pertence a uma das k classes. Conforme discutido antes, a camada de saída precisa ter tamanho k e os valores de saída devem representar probabilidades p_i para cada uma das k classes e SUM(p_i)=1 .
O artigo sobre classificação binária usa ativação sigmóide para transformar valores de saída NN em probabilidades. Vamos tentar aplicar o sigmóide em k valores de saída no intervalo [-3, 3] e ver se o sigmóide atende aos requisitos listados anteriormente:
k valores de saída devem estar no intervalo (0,1), onde k é o número de classes
a soma dos k valores de saída deve ser igual a 1
O artigo anterior mostra que a função sigmóide mapeia os valores de entrada em um intervalo (0,1). Vamos ver se a ativação sigmóide satisfaz o segundo requisito. Na tabela de exemplo abaixo processei um vetor de tamanho k (k=7) com ativação sigmóide e somei todos esses valores - a soma desses 7 valores é igual a 3,5. Uma maneira simples de corrigir isso seria dividir todos os valores de k pela sua soma.
Entrada | -3 | -2 | -1 | 0 | 1 | 2 | 3 | SOMA |
---|---|---|---|---|---|---|---|---|
saída sigmóide | 0,04743 | 0,11920 | 0,26894 | 0,50000 | 0,73106 | 0,88080 | 0,95257 | 3,5000 |
Outra forma seria pegar o expoente do valor de entrada e dividi-lo pela soma dos expoentes de todos os valores de entrada:
A função softmax transforma um vetor de números reais em um vetor de probabilidades. Cada probabilidade no resultado está no intervalo (0,1) e a soma das probabilidades é 1.
Entrada | -3 | -2 | -1 | 0 | 1 | 2 | 3 | SOMA |
---|---|---|---|---|---|---|---|---|
softmax | 0,00157 | 0,00426 | 0,01159 | 0,03150 | 0,08563 | 0,23276 | 0,63270 | 1 |
Há uma coisa que você precisa estar ciente ao trabalhar com softmax: o valor de saída p_i depende de todos os valores na matriz de entrada, pois o dividimos pela soma dos expoentes de todos os valores. A tabela abaixo demonstra isso: dois vetores de entrada têm 3 valores comuns {1, 3, 4}, mas os valores softmax de saída diferem porque o segundo elemento é diferente (2 e 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 |
A perda de entropia cruzada binária é definida como:
Na classificação binária, existem duas probabilidades de saída p_i e (1-p_i) e valores reais y_i e (1-y_i).
O problema de classificação multiclasse usa a generalização da perda BCE para N classes: perda de entropia cruzada.
N é o número de amostras de entrada, y_i é a verdade básica e p_i é a probabilidade prevista da classe i .
Para implementar uma classificação probabilística multiclasse NN, precisamos:
A maioria das partes do código é baseada no código do artigo anterior sobre classificação binária.
As peças alteradas são marcadas com (Alterado) :
Vamos codificar uma rede neural para classificação multiclasse com a estrutura PyTorch.
Primeiro, instale
# used for accuracy metric and confusion matrix !pip install torchmetrics
Importe pacotes que serão usados posteriormente no 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
Defina a variável global com o número de classes (se você definir como 2 e obter NN de classificação binária que usa softmax e perda de entropia cruzada)
number_of_classes=4
usarei
n_samples - é o número de amostras geradas
n_features - define o número de dimensões das amostras geradas X
n_classes – o número de classes no conjunto de dados gerado. No problema de classificação multiclasse, deve haver mais de 2 classes
O conjunto de dados gerado terá X com formato [n_samples, n_features] e Y com formato [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 funções para visualizar e imprimir estatísticas do conjunto de dados. função show_dataset usa
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()
Dimensione os recursos do conjunto de dados X para o intervalo [0,1] com escalonador min max. Isso geralmente é feito para um treinamento mais rápido e estável.
def scale(x_in): return (x_in - x_in.min(axis=0))/(x_in.max(axis=0)-x_in.min(axis=0))
Vamos imprimir as estatísticas do conjunto de dados gerado e visualizá-las com as funções acima.
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')
Os resultados que você deve obter estão abaixo.
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]
A escala min-max não distorce os recursos do conjunto de dados, ela os transforma linearmente no intervalo [0,1]. A figura “conjunto de dados após escala min-máx” parece estar distorcida em comparação com a figura anterior porque 20 dimensões são reduzidas a 2 pelo algoritmo PCA e o algoritmo PCA pode ser afetado pela escala min-máx.
Crie carregadores de dados 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
Teste carregadores de dados 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}')
A saída:
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])
Crie funções de pré e pós-processamento. Como você deve ter notado antes, a forma Y atual é [N], precisamos que seja [N, número_de_classes]. Para fazer isso, precisamos codificar one-hot os valores no vetor Y.
A codificação one-hot é um processo de conversão de índices de classe em uma representação binária onde cada classe é representada por um vetor binário exclusivo.
Em outras palavras: crie um vetor zero com tamanho [número_de_classes] e defina o elemento na posição class_id como 1, onde class_ids {0,1,…,número_de_classes-1}:
0 >> [1. 0. 0. 0.]
1 >> [0. 1. 0. 0.]
2 >> [0. 0. 1. 0.]
2 >> [0. 0. 0. 1.]
Tensores Pytorch podem ser processados com torch.nn.funcional.one_hot e a implementação numpy é muito direta. O vetor de saída terá formato [N, número_de_classes].
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 converter o vetor codificado one-hot de volta para o ID da classe, precisamos encontrar o índice do elemento max no vetor codificado one-hot. Isso pode ser feito com torch.argmax ou np.argmax abaixo.
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)
Teste as funções de pré e pós-processamento 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]}')
A saída:
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 seção mostra uma implementação de todas as funções necessárias para treinar um modelo de classificação binária.
A implementação da fórmula softmax baseada em PyTorch
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
Vamos testar o softmax:
gerar matriz numpy test_input no intervalo [-10, 11] com a etapa 1
remodele-o em um tensor com forma [7,3]
processe test_input com a função softmax implementada e implementação padrão do PyTorch torch.nn.funcional.softmax
compare os resultados (eles devem ser idênticos)
gerar valores softmax e soma para todos os sete 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()}')
A saída:
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]
A implementação da fórmula CE baseada em PyTorch
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
Implementação de CE de teste:
gerar array test_input com formato [10,5] e valores no intervalo [0,1) com
gerar array test_target com formato [10,] e valores no intervalo [0,4].
array test_target de codificação one-hot
perda de cálculo com a função cross_entropy implementada e implementação PyTorch
compare os resultados (eles devem 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()}')
A saída esperada:
test_input shape: [10, 5], test_target shape: [10, 5] Loss outputs are the same: True
usarei
Para criar uma métrica de precisão de classificação multiclasse, dois parâmetros são necessários:
tipo de tarefa "multiclasse"
número de aulas num_classes
# 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))
O NN usado neste exemplo é um NN profundo com 2 camadas ocultas. As camadas de entrada e ocultas usam a ativação ReLU e a camada final usa a função de ativação fornecida como entrada da classe (será a função de ativação sigmóide que foi implementada 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
A figura acima mostra a lógica de treinamento para um único lote. Posteriormente, a função train_epoch será chamada várias vezes (número de épocas escolhido).
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
A função de avaliação itera sobre o dataloader PyTorch fornecido, calcula a precisão do modelo atual e retorna a perda média e a precisão média.
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
A função de previsão itera sobre o dataloader fornecido, coleta previsões de modelo pós-processadas (decodificadas one-hot) e valores reais em [N,1] matrizes PyTorch e retorna ambas as matrizes. Posteriormente, esta função será usada para calcular a matriz de confusão e visualizar as previsões.
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 treinar o modelo precisamos apenas chamar a função train_epoch N vezes, onde N é o número de épocas. A função de avaliação é chamada para registrar a precisão do modelo atual no conjunto de dados de validação. Finalmente, o melhor modelo é atualizado com base na precisão da validação. A função model_train retorna a melhor precisão de validação e o histórico de treinamento.
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
Vamos juntar tudo e treinar o modelo de classificação multiclasse.
######################################### # 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))
O resultado esperado deve ser semelhante ao fornecido abaixo.
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 classificação multiclasse, você precisa usar ativação softmax e perda de entropia cruzada. Existem algumas modificações de código necessárias para mudar da classificação binária para a classificação multiclasse: pré-processamento e pós-processamento de dados, funções de ativação e perda. Além disso, você pode resolver o problema de classificação binária definindo o número de classes como 2 com codificação one-hot, softmax e perda de entropia cruzada.