В моем предыдущем посте сформулирована проблема классификации, она разделена на 3 типа (двоичная, мультиклассовая и мультиметочная) и отвечает на вопрос: «Какие функции активации и потерь вам нужно использовать для решения задачи бинарной классификации?».
В этом посте я отвечу на тот же вопрос, но для задачи многоклассовой классификации, и предоставлю вам
Какие функции активации и потерь необходимо использовать для решения задачи классификации нескольких классов?
Предоставленный код во многом основан на реализации двоичной классификации, поскольку вам нужно добавить очень мало изменений в ваш код и NN, чтобы переключиться с бинарной классификации на многоклассовую. Измененные блоки кода помечены значком (Изменено) для упрощения навигации.
Как будет показано позже, функция активации, используемая для многоклассовой классификации, представляет собой активацию softmax. Softmax широко используется в различных архитектурах нейронных сетей, помимо многоклассовой классификации. Например, softmax лежит в основе многоголового блока внимания, используемого в моделях Transformer (см. «Внимание — это все, что вам нужно ») из-за его способности преобразовывать входные значения в распределение вероятностей (подробнее об этом см. ниже).
Если вы знаете мотивацию применения активации softmax и потери CE для решения задач многоклассовой классификации, вы сможете понять и реализовать гораздо более сложные NN-архитектуры и функции потерь.
Задачу многоклассовой классификации можно представить как набор выборок {(x_1, y_1), (x_2, y_2),...,(x_n, y_n)} , где x_i — m-мерный вектор, содержащий признаки выборки. i и y_i — это класс, к которому принадлежит x_i . Где метка y_i может принимать одно из значений k , где k — количество классов выше 2. Цель — построить модель, которая предсказывает метку y_i для каждой входной выборки x_i .
Примеры задач, которые можно рассматривать как задачи многоклассовой классификации:
В многоклассовой классификации вам даны:
набор образцов {(x_1, y_1), (x_2, y_2),...,(x_n, y_n)}
x_i — m-мерный вектор, содержащий признаки образца i.
y_i — класс, к которому принадлежит x_i , и может принимать одно из значений k , где k>2 — количество классов.
Для построения нейронной сети многоклассовой классификации в качестве вероятностного классификатора нам понадобится:
Последний линейный слой нейронной сети выводит вектор «необработанных выходных значений». В случае классификации выходные значения представляют уверенность модели в том, что входные данные принадлежат одному из k классов. Как обсуждалось ранее, выходной слой должен иметь размер k , а выходные значения должны представлять вероятности p_i для каждого из k классов и SUM(p_i)=1 .
В статье о двоичной классификации используется сигмовидная активация для преобразования выходных значений NN в вероятности. Давайте попробуем применить сигмоид к выходным значениям k в диапазоне [-3, 3] и посмотреть, удовлетворяет ли сигмоид ранее перечисленным требованиям:
выходные значения k должны находиться в диапазоне (0,1), где k — количество классов.
сумма k выходных значений должна быть равна 1
В предыдущей статье показано, что сигмовидная функция отображает входные значения в диапазон (0,1). Посмотрим, удовлетворяет ли активация сигмовидной кишки второму требованию. В приведенной ниже таблице примеров я обработал вектор размера k (k=7) с сигмовидной активацией и суммировал все эти значения — сумма этих 7 значений равна 3,5. Самый простой способ исправить это — разделить все значения k на их сумму.
Вход | -3 | -2 | -1 | 0 | 1 | 2 | 3 | СУММА |
---|---|---|---|---|---|---|---|---|
сигмовидный выход | 0,04743 | 0,11920 | 0,26894 | 0,50000 | 0,73106 | 0,88080 | 0,95257 | 3.5000 |
Другой способ — взять показатель степени входного значения и разделить его на сумму показателей всех входных значений:
Функция softmax преобразует вектор действительных чисел в вектор вероятностей. Каждая вероятность в результате находится в диапазоне (0,1), а сумма вероятностей равна 1.
Вход | -3 | -2 | -1 | 0 | 1 | 2 | 3 | СУММА |
---|---|---|---|---|---|---|---|---|
софтмакс | 0,00157 | 0,00426 | 0,01159 | 0,03150 | 0,08563 | 0,23276 | 0,63270 | 1 |
Есть одна вещь, о которой вам нужно знать при работе с softmax: выходное значение p_i зависит от всех значений во входном массиве, поскольку мы делим его на сумму показателей всех значений. Таблица ниже демонстрирует это: два входных вектора имеют 3 общих значения {1, 3, 4}, но выходные значения softmax различаются, поскольку второй элемент отличается (2 и 4).
Вход 1 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
софтмакс 1 | 0,0321 | 0,0871 | 0,2369 | 0,6439 |
Вход 2 | 1 | 4 | 3 | 4 |
софтмакс 2 | 0,0206 | 0,4136 | 0,1522 | 0,4136 |
Потери двоичной перекрестной энтропии определяются как:
В двоичной классификации есть две выходные вероятности p_i и (1-p_i) и основные значения истинности y_i и (1-y_i).
Задача многоклассовой классификации использует обобщение потерь BCE для N классов: перекрестную энтропийную потерю.
N — количество входных выборок, y_i — основная истина, а p_i — прогнозируемая вероятность класса i .
Для реализации вероятностной многоклассовой классификации NN нам необходимо:
Большая часть кода основана на коде из предыдущей статьи о бинарной классификации.
Измененные детали отмечены значком (Изменено) :
Давайте напишем нейронную сеть для многоклассовой классификации с помощью платформы PyTorch.
Сначала установите
# used for accuracy metric and confusion matrix !pip install torchmetrics
Импортируйте пакеты, которые будут использоваться позже в коде.
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
Установите глобальную переменную с количеством классов (если вы установите ее на 2 и получите NN двоичной классификации, которая использует softmax и потерю перекрестной энтропии)
number_of_classes=4
я буду использовать
n_samples — количество сгенерированных выборок
n_features — устанавливает количество измерений сгенерированных образцов X
n_classes — количество классов в сгенерированном наборе данных. В задаче многоклассовой классификации должно быть более двух классов.
Сгенерированный набор данных будет иметь X с формой [n_samples, n_features] и Y с формой [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
Определите функции для визуализации и распечатки статистики набора данных. Функция 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()
Масштабируйте функции набора данных X до диапазона [0,1] с помощью минимального и максимального масштабатора. Обычно это делается для более быстрого и стабильного обучения.
def scale(x_in): return (x_in - x_in.min(axis=0))/(x_in.max(axis=0)-x_in.min(axis=0))
Давайте распечатаем сгенерированную статистику набора данных и визуализируем ее с помощью функций, описанных выше.
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')
Результаты, которые вы должны получить, приведены ниже.
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]
Масштабирование Min-Max не искажает характеристики набора данных, оно линейно преобразует их в диапазон [0,1]. Фигура «набор данных после мин-максного масштабирования» кажется искаженной по сравнению с предыдущим рисунком, поскольку 20 измерений уменьшаются до 2 с помощью алгоритма PCA, а на алгоритм PCA может влиять мин-максное масштабирование.
Создайте загрузчики данных 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
Тестирование загрузчиков данных 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}')
Выход:
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])
Создайте функции предварительной и постобработки. Как вы, возможно, уже заметили, текущая форма Y — [N], нам нужно, чтобы она была [N,number_of_classes]. Для этого нам нужно выполнить горячее кодирование значений вектора Y.
Горячее кодирование — это процесс преобразования индексов классов в двоичное представление, где каждый класс представлен уникальным двоичным вектором.
Другими словами: создайте нулевой вектор размером [число_классов] и установите элемент в позиции class_id равным 1, где class_ids {0,1,…,number_of_classes-1}:
0 >> [1. 0. 0. 0.]
1 >> [0. 1. 0. 0.]
2 >> [0. 0. 1. 0.]
2 >> [0. 0. 0. 1.]
Тензоры Pytorch можно обрабатывать с помощью torch.nn.functional.one_hot, а реализация numpy очень проста. Выходной вектор будет иметь форму [N,number_of_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
Чтобы преобразовать вектор с горячим кодированием обратно в идентификатор класса, нам нужно найти индекс максимального элемента в векторе с горячим кодированием. Это можно сделать с помощью torch.argmax или np.argmax ниже.
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)
Проверьте определенные функции предварительной и постобработки.
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]}')
Выход:
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
В этом разделе показана реализация всех функций, необходимых для обучения модели двоичной классификации.
Реализация формулы softmax на базе 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
Давайте проверим softmax:
сгенерировать массив test_input numpy в диапазоне [-10, 11] с шагом 1
преобразовать его в тензор с формой [7,3]
обработать test_input с реализованной функцией softmax и реализацией PyTorch по умолчанию torch.nn.functional.softmax
сравнить результаты (они должны быть идентичными)
выведите значения softmax и сумму для всех семи [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()}')
Выход:
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]
Реализация формулы CE на основе 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
Тестовая реализация CE:
сгенерируйте массив test_input с формой [10,5] и значениями в диапазоне [0,1) с помощью
сгенерируйте массив test_target с формой [10,] и значениями в диапазоне [0,4].
горячее кодирование массива test_target
вычислить потери с помощью реализованной функции cross_entropy и реализации PyTorch
сравнить результаты (они должны быть идентичными)
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()}')
Ожидаемый результат:
test_input shape: [10, 5], test_target shape: [10, 5] Loss outputs are the same: True
я буду использовать
Для создания метрики точности многоклассовой классификации необходимы два параметра:
тип задачи «мультикласс»
количество классов 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))
NN, используемая в этом примере, представляет собой глубокую NN с двумя скрытыми слоями. Входные и скрытые слои используют активацию ReLU, а последний слой использует функцию активации, предоставленную в качестве входных данных класса (это будет функция активации сигмовидной формы, которая была реализована ранее).
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
На рисунке выше показана логика обучения для одного пакета. Позже функция train_epoch будет вызываться несколько раз (выбранное количество эпох).
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
Функция оценки выполняет итерацию по предоставленному загрузчику данных PyTorch, вычисляет текущую точность модели и возвращает средние потери и среднюю точность.
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
Функция прогнозирования выполняет итерацию по предоставленному загрузчику данных, собирает постобработанные (горячее декодирование) прогнозы модели и основные значения истинности в массивы [N,1] PyTorch и возвращает оба массива. Позже эта функция будет использоваться для вычисления матрицы путаницы и визуализации прогнозов.
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
Для обучения модели нам просто нужно вызвать функцию train_epoch N раз, где N — количество эпох. Функция оценки вызывается для регистрации текущей точности модели в наборе проверочных данных. Наконец, лучшая модель обновляется на основе точности проверки. Функция model_train возвращает максимальную точность проверки и историю обучения.
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
Давайте соберем все вместе и обучим модель многоклассовой классификации.
######################################### # 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))
Ожидаемый результат должен быть аналогичен приведенному ниже.
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')
Для многоклассовой классификации вам необходимо использовать активацию softmax и потерю перекрестной энтропии. Для перехода от двоичной классификации к многоклассовой классификации необходимо внести несколько изменений в код: предварительная и постобработка данных, функции активации и потери. Более того, вы можете решить проблему двоичной классификации, установив количество классов равным 2 с помощью горячего кодирования, softmax и перекрестной энтропийной потери.