paint-brush
Классификация нескольких классов: понимание функций активации и потерь в нейронных сетяхк@owlgrey
2,871 чтения
2,871 чтения

Классификация нескольких классов: понимание функций активации и потерь в нейронных сетях

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

Слишком долго; Читать

Чтобы построить нейронную сеть с многоклассовой классификацией, вам необходимо использовать функцию активации softmax на ее последнем слое вместе с потерей перекрестной энтропии. Окончательный размер слоя должен составлять k, где k — количество классов. Идентификаторы классов должны быть предварительно обработаны с помощью горячего кодирования. Такая нейронная сеть будет выводить вероятности p_i того, что входные данные принадлежат классу i. Чтобы найти прогнозируемый идентификатор класса, вам нужно найти индекс максимальной вероятности.
featured image - Классификация нескольких классов: понимание функций активации и потерь в нейронных сетях
Dmitrii Matveichev  HackerNoon profile picture


В моем предыдущем посте сформулирована проблема классификации, она разделена на 3 типа (двоичная, мультиклассовая и мультиметочная) и отвечает на вопрос: «Какие функции активации и потерь вам нужно использовать для решения задачи бинарной классификации?».


В этом посте я отвечу на тот же вопрос, но для задачи многоклассовой классификации, и предоставлю вам пример реализации pytorch в Google Colab .


Какие функции активации и потерь необходимо использовать для решения задачи классификации нескольких классов?


Предоставленный код во многом основан на реализации двоичной классификации, поскольку вам нужно добавить очень мало изменений в ваш код и NN, чтобы переключиться с бинарной классификации на многоклассовую. Измененные блоки кода помечены значком (Изменено) для упрощения навигации.


1 Почему важно понимать функцию активации и потери, используемые для многоклассовой классификации?

Как будет показано позже, функция активации, используемая для многоклассовой классификации, представляет собой активацию softmax. Softmax широко используется в различных архитектурах нейронных сетей, помимо многоклассовой классификации. Например, softmax лежит в основе многоголового блока внимания, используемого в моделях Transformer (см. «Внимание — это все, что вам нужно ») из-за его способности преобразовывать входные значения в распределение вероятностей (подробнее об этом см. ниже).


Масштабированное скалярное произведение внимания (чаще всего в модуле внимания Multi-Head)



Если вы знаете мотивацию применения активации softmax и потери CE для решения задач многоклассовой классификации, вы сможете понять и реализовать гораздо более сложные NN-архитектуры и функции потерь.


2 Формулировка задачи многоклассовой классификации

Задачу многоклассовой классификации можно представить как набор выборок {(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 .

Примеры задач, которые можно рассматривать как задачи многоклассовой классификации:

  • медицинская диагностика – постановка диагноза пациенту одного из нескольких заболеваний на основании предоставленных данных (истории болезни, результатов анализов, симптомов)
  • категоризация продуктов - автоматическая классификация продуктов для платформ электронной коммерции
  • прогноз погоды - классификация будущей погоды как солнечная, облачная, дождливая и т. д.
  • категоризация фильмов, музыки и статей по разным жанрам
  • классификация онлайн-отзывов клиентов по таким категориям, как отзывы о продуктах, отзывы об услугах, жалобы и т. д.


3 функции активации и потери для многоклассовой классификации


В многоклассовой классификации вам даны:

  • набор образцов {(x_1, y_1), (x_2, y_2),...,(x_n, y_n)}

  • x_i — m-мерный вектор, содержащий признаки образца i.

  • y_i — класс, к которому принадлежит x_i , и может принимать одно из значений k , где k>2 — количество классов.


Для построения нейронной сети многоклассовой классификации в качестве вероятностного классификатора нам понадобится:

  • выходной полносвязный слой размером k
  • выходные значения должны находиться в диапазоне [0,1]
  • сумма выходных значений должна быть равна 1. В многоклассовой классификации каждый вход x может принадлежать только одному классу (взаимоисключающим классам), следовательно, сумма вероятностей всех классов должна быть равна 1: SUM(p_0,…,p_k )=1 .
  • функция потерь, которая имеет наименьшее значение, когда предсказание и основная истина совпадают


3.1 Функция активации softmax

Последний линейный слой нейронной сети выводит вектор «необработанных выходных значений». В случае классификации выходные значения представляют уверенность модели в том, что входные данные принадлежат одному из 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


Функция 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

График показателя степени в диапазоне [-10, 10]


Softmax вектора размером 21 со значениями [-10, 10]


Есть одна вещь, о которой вам нужно знать при работе с 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


3.2 Перекрестная энтропийная потеря

Потери двоичной перекрестной энтропии определяются как:

Двоичная кросс-энтропийная потеря


В двоичной классификации есть две выходные вероятности p_i и (1-p_i) и основные значения истинности y_i и (1-y_i).


Задача многоклассовой классификации использует обобщение потерь BCE для N классов: перекрестную энтропийную потерю.


Перекрестная энтропийная потеря


N — количество входных выборок, y_i — основная истина, а p_i — прогнозируемая вероятность класса i .


4 Пример многоклассовой классификации NN с PyTorch

Для реализации вероятностной многоклассовой классификации NN нам необходимо:

  • основная истина и прогнозы должны иметь размерности [N,k] , где N — количество входных выборок, k — количество классов — идентификатор класса должен быть закодирован в вектор размера k.
  • конечный размер линейного слоя должен составлять k
  • выходные данные последнего слоя должны обрабатываться с помощью активации softmax для получения выходных вероятностей.
  • Потери CE следует применять к прогнозируемым вероятностям классов и основным значениям истинности.
  • найдите идентификатор выходного класса из выходного вектора размера k



Процесс обучения многоклассовой классификации 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


4.1 Создать набор данных

Установите глобальную переменную с количеством классов (если вы установите ее на 2 и получите NN двоичной классификации, которая использует softmax и потерю перекрестной энтропии)


 number_of_classes=4


я буду использовать sklearn.datasets.make_classification для создания набора данных двоичной классификации:

  • 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


4.2 Визуализация набора данных

Определите функции для визуализации и распечатки статистики набора данных. Функция show_dataset использует СПС уменьшить размерность X с любого числа до 2 для простоты визуализации входных данных X на 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 Масштабирование набора данных

Масштабируйте функции набора данных 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] 

Набор данных до масштабирования 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] 

Набор данных после мин-максного масштабирования


Масштабирование Min-Max не искажает характеристики набора данных, оно линейно преобразует их в диапазон [0,1]. Фигура «набор данных после мин-максного масштабирования» кажется искаженной по сравнению с предыдущим рисунком, поскольку 20 измерений уменьшаются до 2 с помощью алгоритма PCA, а на алгоритм PCA может влиять мин-максное масштабирование.


Создайте загрузчики данных PyTorch. sklearn.datasets.make_classification генерирует набор данных в виде двух массивов numpy. Чтобы создать загрузчики данных PyTorch, нам нужно преобразовать набор данных numpy в torch.tensor с помощью 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


Тестирование загрузчиков данных 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])


4.4 Предварительная и постобработка набора данных (изменено)

Создайте функции предварительной и постобработки. Как вы, возможно, уже заметили, текущая форма 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


4.5 Создание и обучение модели многоклассовой классификации

В этом разделе показана реализация всех функций, необходимых для обучения модели двоичной классификации.


4.5.1 Активация Softmax (Изменено)

Реализация формулы softmax на базе PyTorch.

Определение активации 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


Давайте проверим softmax:

  1. сгенерировать массив test_input numpy в диапазоне [-10, 11] с шагом 1

  2. преобразовать его в тензор с формой [7,3]

  3. обработать test_input с реализованной функцией softmax и реализацией PyTorch по умолчанию torch.nn.functional.softmax

  4. сравнить результаты (они должны быть идентичными)

  5. выведите значения 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]


4.5.2 Функция потерь: перекрестная энтропия (изменено)

Реализация формулы 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:


  1. сгенерируйте массив test_input с формой [10,5] и значениями в диапазоне [0,1) с помощью факел.рэнд

  2. сгенерируйте массив test_target с формой [10,] и значениями в диапазоне [0,4].

  3. горячее кодирование массива test_target

  4. вычислить потери с помощью реализованной функции cross_entropy и реализации PyTorch torch.nn.functional.binary_cross_entropy

  5. сравнить результаты (они должны быть идентичными)


 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


4.5.3 Показатель точности (изменён)

я буду использовать факельные измерения реализация для вычисления точности на основе прогнозов модели и достоверных данных.


Для создания метрики точности многоклассовой классификации необходимы два параметра:

  • тип задачи «мультикласс»

  • количество классов 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 Модель NN

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


4.5.5 Обучение, оценка и прогнозирование

Процесс обучения многоклассовой классификации NN


На рисунке выше показана логика обучения для одного пакета. Позже функция 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


4.5.6 Получите набор данных, создайте модель и обучите ее (Изменено)

Давайте соберем все вместе и обучим модель многоклассовой классификации.

 ######################################### # 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%


4.5.7 Построение истории обучения

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

История потерь и точности при обучении и проверке


4.6 Оценка модели


4.6.1 Расчет точности обучения и проверки

 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 Распечатать матрицу путаницы (Изменено)

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

Матрица путаницы в наборе проверочных данных


4.6.3 Предсказания сюжета и основная истина

 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 и перекрестной энтропийной потери.