paint-brush
Classificação multiclasse: Compreendendo as funções de ativação e perda em redes neuraispor@owlgrey
1,977 leituras
1,977 leituras

Classificação multiclasse: Compreendendo as funções de ativação e perda em redes neurais

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

Muito longo; Para ler

Para construir uma rede neural de classificação multiclasse, você precisa usar a função de ativação softmax em sua camada final junto com a perda de entropia cruzada. O tamanho final da camada deve ser k, onde k é o número de classes. Os IDs de classe devem ser pré-processados com codificação one-hot. Tal rede neural produzirá probabilidades p_i de que a entrada pertença a uma classe i. Para encontrar o ID da classe prevista, você precisa encontrar o índice de probabilidade máxima.
featured image - Classificação multiclasse: Compreendendo as funções de ativação e perda em redes neurais
Dmitrii Matveichev  HackerNoon profile picture


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 exemplo de implementação de pytorch no Google colab .


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.


1 Por que é importante entender a função de ativação e a perda usadas para classificação multiclasse?

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


Atenção de produto escalado em escala (mais comumente no módulo de atenção Multi-Head)



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.


2 Formulação de problema de classificação multiclasse

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:

  • diagnóstico médico - diagnosticar um paciente com uma das várias doenças com base nos dados fornecidos (histórico médico, resultados de testes, sintomas)
  • categorização de produtos - classificação automática de produtos para plataformas de comércio eletrônico
  • previsão do tempo - classificando o tempo futuro como ensolarado, nublado, chuvoso, etc.
  • categorizando filmes, músicas e artigos em diferentes gêneros
  • classificar avaliações de clientes on-line em categorias como feedback de produto, feedback de serviço, reclamações, etc.


3 Funções de ativação e perda para 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:

  • uma camada de saída totalmente conectada com tamanho k
  • os valores de saída devem estar no intervalo [0,1]
  • a soma dos valores de saída deve ser igual a 1. Na classificação multiclasse, cada entrada x pode pertencer a apenas uma classe (classes mutuamente exclusivas), portanto, a soma das probabilidades de todas as classes deve ser 1: SUM(p_0,…,p_k )=1 .
  • uma função de perda que tem o valor mais baixo quando a previsão e a verdade fundamental são iguais


3.1 A função de ativação softmax

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


    Definição de função sigmóide


    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:


Definição da função Softmax


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

O gráfico do expoente no intervalo [-10, 10]


Softmax de um vetor de tamanho 21 com valores [-10, 10]


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


3.2 Perda de entropia cruzada

A perda de entropia cruzada binária é definida como:

Perda binária de entropia cruzada


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.


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 .


4 Exemplo de classificação NN multiclasse com PyTorch

Para implementar uma classificação probabilística multiclasse NN, precisamos:

  • a verdade básica e as previsões devem ter dimensões [N,k] onde N é o número de amostras de entrada, k é o número de classes - o ID da classe precisa ser codificado em um vetor com tamanho k
  • o tamanho final da camada linear deve ser k
  • as saídas da camada final devem ser processadas com ativação softmax para obter probabilidades de saída
  • A perda de CE deve ser aplicada às probabilidades de classe previstas e aos valores reais
  • encontre o ID da classe de saída do vetor de saída com tamanho k



O processo de treinamento de uma classificação multiclasse NN


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) :

  • pré-processamento e pós-processamento de dados
  • função de ativação
  • função de perda
  • métrica de desempenho
  • matriz de confusão


Vamos codificar uma rede neural para classificação multiclasse com a estrutura PyTorch.

Primeiro, instale tochamétrica - este pacote será usado posteriormente para calcular a precisão da classificação e a matriz de confusão.


 # 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


4.1 Criar conjunto de dados

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 sklearn.datasets.make_classification para gerar um conjunto de dados de classificação binária:

  • 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


4.2 Visualização do conjunto de dados

Defina funções para visualizar e imprimir estatísticas do conjunto de dados. função show_dataset usa PCA para reduzir a dimensionalidade de X de qualquer número até 2 para simplificar a visualização dos dados de entrada X no 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 conjunto de dados

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] 

O conjunto de dados antes do escalonamento min-max


 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] 

O conjunto de dados após escalonamento min-max


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. sklearn.datasets.make_classification gera o conjunto de dados como duas matrizes numpy. Para criar dataloaders PyTorch, precisamos transformar o conjunto de dados numpy em torch.tensor com 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


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])


4.4 Pré-processamento e pós-processamento do conjunto de dados (alterado)

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


4.5 Criando e treinando um modelo de classificação multiclasse

Esta seção mostra uma implementação de todas as funções necessárias para treinar um modelo de classificação binária.


4.5.1 Ativação Softmax (Alterado)

A implementação da fórmula softmax baseada em PyTorch

Definição de ativação 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


Vamos testar o softmax:

  1. gerar matriz numpy test_input no intervalo [-10, 11] com a etapa 1

  2. remodele-o em um tensor com forma [7,3]

  3. processe test_input com a função softmax implementada e implementação padrão do PyTorch torch.nn.funcional.softmax

  4. compare os resultados (eles devem ser idênticos)

  5. 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]


4.5.2 Função de perda: entropia cruzada (alterada)

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:


  1. gerar array test_input com formato [10,5] e valores no intervalo [0,1) com tocha.rand

  2. gerar array test_target com formato [10,] e valores no intervalo [0,4].

  3. array test_target de codificação one-hot

  4. perda de cálculo com a função cross_entropy implementada e implementação PyTorch torch.nn.funcional.binary_cross_entropy

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


4.5.3 Métrica de precisão (alterada)

usarei tochamétrica implementação para calcular a precisão com base em previsões de modelos e fatos reais.


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))


4.5.4 Modelo NN

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


4.5.5 Treinamento, avaliação e previsão

O processo de treinamento de uma classificação multiclasse NN


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


4.5.6 Obtenha o conjunto de dados, crie o modelo e treine-o (alterado)

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%


4.5.7 Traçar histórico de treinamento

 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() 

Histórico de perda e precisão de treinamento e validação


4.6 Avalie o modelo


4.6.1 Calcular a precisão do treinamento e da validação

 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 confusão (alterado)

 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 confusão no conjunto de dados de validação


4.6.3 Previsões de plotagem e verdades básicas

 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') 


Verdade fundamental do conjunto de dados de validação

Previsões de modelo no conjunto de dados de validação


Conclusão

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.