
PyTorch — среда глубокого обучения, которая была принята такими технологическими гигантами, как Tesla, OpenAI и Microsoft для ключевых исследовательских и производственных рабочих нагрузок.
PyTorch-Ignite — это библиотека высокого уровня, помогающая гибко и прозрачно обучать и оценивать нейронные сети в PyTorch. Основная проблема с реализацией глубокого обучения заключается в том, что коды могут быстро расти, становиться повторяющимися и слишком длинными. Рассматривать данную библиотеку буду, решая задачу оценки вероятности отнесения изображения к определенному классу на примере датасета CIFAR10. Чуть позже расскажу о нем подробнее. А сейчас начнем подготовку с установки и импорта необходимых библиотек.
Установка и импорт необходимых библиотек
Советую работать в сервисе GoogleColab
!pip install pytorch-ignite
import numpy as np import matplotlib.pyplot as plt import torch from torch import nn, optim import torch.nn.functional as F from torch.utils.data import DataLoader from torchvision import datasets, transforms import torchvision from torch.utils.data import sampler from torch.utils.data.sampler import SubsetRandomSampler from ignite.engine import Events, create_supervised_trainer, create_supervised_evaluator from ignite.metrics import Accuracy, Loss, RunningAverage, ConfusionMatrix from ignite.handlers import ModelCheckpoint, EarlyStopping
Теперь подробнее рассмотрим датасет CIFAR10. Он содержит 60 000 изображений в 10 классах. Все изображения размером 32х32. С помощью следующего блока кода разобью данные на тестовые, тренировочные и валидационные.
transform = transforms.Compose( [transforms.ToTensor(),transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) trainset = datasets.CIFAR10('./data', download=True, train=True, transform=transform) train_loader = DataLoader(trainset, batch_size=4, shuffle=True) validationset = datasets.CIFAR10('./data', download=True, train=False, transform=transform) classes = ('plane', 'car', 'bird', 'cat','deer', 'dog', 'frog', 'horse', 'ship', 'truck') validation_split = .2 shuffle_dataset = True random_seed= 42 dataset_size = len(validationset) indices = list(range(dataset_size)) split = int(np.floor(validation_split * dataset_size)) if shuffle_dataset : np.random.seed(random_seed) np.random.shuffle(indices) val_indices, test_indices = indices[split:], indices[:split] val_sampler = SubsetRandomSampler(val_indices) test_sampler = SubsetRandomSampler(test_indices) val_loader = torch.utils.data.DataLoader(validationset, batch_size=4, sampler=val_sampler) test_loader = torch.utils.data.DataLoader(validationset, batch_size=4, sampler=test_sampler) print(f'Кол-во валидационных данных {len(val_loader)}') print(f'Кол-во тестовых данных {len(test_loader)}') print(f'Кол-во обучающих данных {len(train_loader)}')
Предлагаю посмотреть, как выглядят наши изображения. Код ниже выводит изображение из датасета и подписывает к какому классу оно принадлежит.
def imshow(img): img = img / 2 + 0.5 npimg = img.numpy() print(f'Размер изображения {npimg.shape}') plt.imshow(np.transpose(npimg, (1, 2, 0))) plt.show() train_loader = DataLoader(trainset, batch_size=1, shuffle=True) dataiter = iter(train_loader) images, labels = dataiter.next() imshow(torchvision.utils.make_grid(images)) # print(' '.join('%5s' % classes[labels[j]] for j in range(1))) print(f'Класс изображения {" ".join("%5s" % classes[labels[j]] for j in range(1))}') train_loader = DataLoader(trainset, batch_size=64, shuffle=True)
Теперь разберу архитектуру модели. На изображении ниже схематично показано, как работают сверточные нейронные сети. У нас есть 2 основных слоя: Conv_1 и Conv_2. Между ними идет увеличение найденных уникальных признаков Max-polling. После прохождения по всем слоям происходит подключение функции активации Relu. Перед тем как сеть определит, к какому классу принадлежит изображение, подключается dropout, чтобы избежать переобучения модели. И после этого на выходе получается предполагаемый класс изображения.

Далее разберу основные компоненты сети.
class Mod(nn.Module): def __init__(self): super(Mod, self).__init__() self.conv1 = nn.Conv2d(3, 16, 5) self.pool = nn.MaxPool2d(2, 2) self.bn1 = nn.BatchNorm2d(16) self.conv2 = nn.Conv2d(16, 32, 5) self.bn2 = nn.BatchNorm2d(32) self.fc1 = nn.Linear(32 * 5 * 5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 10) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.bn1(x) x = self.pool(F.relu(self.conv2(x))) x = self.bn2(x) x = torch.flatten(x, 1) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) # return x return F.log_softmax(x,dim=1)
Метод Conv2d создает набор сверточных фильтров, их задача состоит в обработке изображений небольшим фильтром, который переходит по изображению небольшими шагами. Это основа сети.
BatchNorm2d выполняет функцию нормализации.
Maxpooling сохраняет ��аиболее активные пиксели из предыдущего слоя, укрупняет масштаб полученных признаков. Другими словами, ключевые области, которые определяют изображение к определённому классу, делает более крупными.
Слои Linear и Dropout, нужны для того чтобы избежать переобучения.
В качестве функции активации используем Relu. Relu все отрицательные числа делает равным 0, а положительные остаются без изменения. Блок кода ниже создает экземпляр созданной ранее сверточной сети, а также задает функцию потерь и скорость обучения. Теперь можно провести небольшое исследование с переменной lr(Learning rate), чтобы посмотреть, как меняется результат в зависимости от скорости обучения. На графике оранжевая линия означает lr=0.001, а синяя lr=0.1. Как видим, данная переменная играет важную роль в настройке модели.

model_classification = Mod() criterion = nn.CrossEntropyLoss() def opt(rate): optimizer = optim.SGD(model_classification.parameters(), lr=rate, momentum=0.9) return optimizer optimizer = opt(0.001)
Воспользуемся метриками из ignite.metrics, которые хотим рассчитать для модели: Accuracy, ConfusionMatrix и Loss. Далее передам их механизмам оценки, которые будут вычислять эти показатели для каждой итерации.
epochs = 12 trainer = create_supervised_trainer(model_classification, optimizer, criterion) metrics = {'accuracy':Accuracy(),'nll':Loss(criterion),'cm':ConfusionMatrix(num_classes=len(classes))} train_evaluator = create_supervised_evaluator(model_classification, metrics=metrics) val_evaluator = create_supervised_evaluator(model_classification, metrics=metrics) training_history = {'accuracy':[],'loss':[]} validation_history = {'accuracy':[],'loss':[]} last_epoch = []
Отследить потери для каждого шага можно, запустив данный блок.
RunningAverage(output_transform=lambda x: x).attach(trainer, 'loss')
Теперь настрою обработчик потерь EarlyStopping.
def score_function(engine): val_loss = engine.state.metrics['nll'] return val_loss handler = EarlyStopping(patience=20, score_function=score_function, trainer=trainer) val_evaluator.add_event_handler(Events.COMPLETED, handler)
Данный обработчик может приостановить процесс обучения. В случае, если потери проверочного множества не уменьшатся, процесс обучения остановится досрочно.
Далее просмотрим 2 функции, которые печатают результаты работы с набором данных. Одна функция делает это с обучающими данными, а другая с данными для проверки (валидационными).
Первая функция использует декоратор “trainer.on()”. Это означает, что декорируемая функция будет прикреплена к тренировочной функции и будет вызываться в конце каждого прогона обучения.
@trainer.on(Events.EPOCH_COMPLETED) def log_training_results(trainer): train_evaluator.run(train_loader) metrics_trian = train_evaluator.state.metrics accuracy = metrics_trian['accuracy']*100 loss = metrics_trian['nll'] last_epoch.append(0) training_history['accuracy'].append(accuracy) training_history['loss'].append(loss) print(f"Training Results - Epoch: {trainer.state.epoch} TRAIN_accuracy: {round(accuracy,2)} TRAIN_loss: {round(loss,2)}".format(trainer.state.epoch, accuracy, loss))
Вторая функция предполагает использование метода add_event_handler. При этом достигается тот же результат, что и выше.
def log_validation_results(trainer): val_evaluator.run(val_loader) metrics_val = val_evaluator.state.metrics accuracy = metrics_val['accuracy']*100 loss = metrics_val['nll'] validation_history['accuracy'].append(accuracy) validation_history['loss'].append(loss) print(f"VAL_Training Results - Epoch: {trainer.state.epoch} VAL_accuracy: {round(accuracy,2)} VAL_loss: {round(loss,2)}".format(trainer.state.epoch, accuracy, loss)) trainer.add_event_handler(Events.EPOCH_COMPLETED, log_validation_results)
Теперь проверю модель. Это важный шаг, так как процессы обучения могут занимать много времени и, если по какой-то причине во время обучения что-то пойдет не так, контрольная точка модели может помочь перезапустить обучение с точки отказа.
Буду использовать обработчик Ignite ModelCheckpoint для проверки моделей в конце каждого прогона обучения.
checkpointer = ModelCheckpoint('./saved_models', 'CIFAR10', n_saved=2, create_dir=True, save_as_state_dict=True, require_empty=False) trainer.add_event_handler(Events.EPOCH_COMPLETED, checkpointer, {'CIFAR10': model_classification})
Далее запущу обучение нашей модели на 24 epochs. Другими словами, модель пройдет обучение 24 раза. И после каждого раза будет виден результат обучения.
trainer.run(train_loader, max_epochs=24)

Результат обучения модели
Если при тесте данной модели вы увидите accuracy 100, это может означать два варианта:
Модель начала переобучение. Она, как студент, который просто заучивает правильные ответы и даже не понимает, что учит.
Ваши тестовые данные такие же, как тренировочные. Важно следить за качеством данных и не забывать их менять.
Визуализирую данные об обучении модели на обучающих и валидационных данных.
Вывод графика истории на обучающих данных.
plt.plot(training_history['accuracy'],label="Training Accuracy") plt.plot(validation_history['accuracy'],label="Validation Accuracy") plt.xlabel('Epochs') plt.ylabel('Accuracy') plt.legend(frameon=False) plt.show()
Вывод графика истории на валидационных данных.
plt.plot(training_history['loss'],label="Training Loss") plt.plot(validation_history['loss'],label="Validation Loss") plt.xlabel('Epochs') plt.ylabel('Loss') plt.legend(frameon=False) plt.show()
Код ниже будет использоваться для вывода данных из модели и визуализации результатов.
classes = ['plane', 'car', 'bird', 'cat','deer', 'dog', 'frog', 'horse', 'ship', 'truck'] def predict_class(batch_size): test_loader = torch.utils.data.DataLoader(validationset, batch_size=batch_size,sampler=test_sampler) dataiter = iter(test_loader) images, labels = dataiter.next() imshow(torchvision.utils.make_grid(images)) outputs = model_classification(images) _, predicted = torch.max(outputs, 1) print(labels) print('DATA: '+' '.join(f'{classes[labels[j]]}' for j in range(batch_size))) print('PRED: '+' '.join(f'{classes[predicted[j]]}'for j in range(batch_size))) label_dataset = [classes[predicted[j]] for j in range(batch_size)] label_predict = [classes[labels[j]] for j in range(batch_size)] kol = 0 for i in range(batch_size): if label_predict[i] == label_dataset[i]: kol+=1 print(f'Правильно классифицировано: {(kol/batch_size)*100}%') predict_class(40)
Результат:

В заключении я сравню реализацию нескольких задач с данной библиотекой и без нее.
Первая задача вывод значений accuracy и loss.
PyTorch-Ignite
train_evaluator.run(train_loader) metrics_trian = train_evaluator.state.metrics accuracy = metrics_trian['accuracy']*100 loss = metrics_trian['nll'] print(f"Training Results - Epoch: {trainer.state.epoch} TRAIN_accuracy: {round(accuracy,2)} TRAIN_loss: {round(loss,2)}".format(trainer.state.epoch, accuracy, loss))
PyTorch
train_epoch_loss=0 train_epoch_accuracy=0 def metrics_accuracy(pred, train): pred = torch.log_softmax(pred, dim = 1) _, pred = torch.max(pred, dim = 1) res = (pred == train).sum().float() accuracy =res/train.shape[0] accuracy = torch.round(accuracy * 100) return accuracy for train_img, train_label in train_loader: optimizer.zero_grad() label_train_pred = model_classification(train_img).squeeze() train_loss = criterion(label_train_pred, train_label) train_accuracy = metrics_accuracy(label_train_pred, train_label) train_loss.backward() optimizer.step() train_epoch_loss += train_loss.item() train_epoch_accuracy += train_accuracy.item()
Вторая задача преждевременная остановка обучения.
PyTorch-Ignite
handler = EarlyStopping(patience=10, score_function=score_function, trainer=trainer) val_evaluator.add_event_handler(Events.COMPLETED, handler)
PyTorch
val_loss_list.append(val_epoch_loss/len(val_loader)) if len(val_loss_list)>1: if val_loss_list[-2] < val_loss_list[-1]: trigger_times += 1 if trigger_times > patience: print('Early stopping!') break else: trigger_times = 0
Даже по двум задачам видно, на сколько данная библиотека сокращает объем кода.
Я рассмотрел основные функции данной библиотеки, а также сравнил выполнение некоторых задач без использования PyTorch-Ignite и с использованием.