Мотивация

Было много данных, лежащих вокруг для задач мультиклассовой классификации. Поэтому, чтобы узнать больше о различных способах использования сред глубокого обучения с использованием Pytorch, я рассмотрел наборы данных о диких животных от Kaggle. Этот набор данных содержит около 1725 изображений шести видов животных.

Введение

Набор данных относительно небольшой, поэтому для реализации таких моделей, как VGG16, ImageNet и ResNet, нам необходимо обрабатывать данные в соответствии с ними. Здесь основная цель — внедрить VGG16 и ResNet в набор данных и сравнить их точность. Код доступен на https://github.com/biraaj/kaggle_wild_animal_image_classification вместе с инструкциями по его выполнению. Также есть видео с описанием, где вносить изменения и как запустить в следующем видео на ютубе https://youtu.be/7cAKDHIjKkw. Ссылка на блокнот Kaggle: https://www.kaggle.com/code/biraaj027/wild-animal-image-classification/edit.

Объяснение и процедуры

Предварительная обработка данных:

Данные, представленные в репозитории, состоят из изображений, размер которых изменен на 224, 300 и 512. Для этого эксперимента я рассмотрел размер изображения 512 и выполнил для него определенные преобразования.

#Applying Image Tranformations
transformer = transforms.Compose(
    [
        transforms.Resize((244, 244)),
        transforms.ToTensor(),
        transforms.Normalize(
            mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]
        ),
    ]
)
dataset = ImageFolder(images_path, transform=transformer)

Здесь я использую метод преобразования из PyTorch, чтобы преобразовать изображение, изменив его размер до 244 * 244 и выполнив нормализацию со ссылкой на архитектуру VGG16. Эти преобразования взяты из моего предыдущего блога о классификации изображений [1]. Библиотека ImageFolder из PyTorch используется для преобразования изображений в набор данных вместе с их целевыми метками, которые определяются их папкой [2] [1].

Теперь после преобразования я получил изображения, как показано ниже.

Всего существует 6 классов ['гепард', 'лиса', 'гиена', 'лев', 'тигр', 'волк']. Код для приведенной выше визуализации также взят из моего предыдущего кода [1].

Теперь, когда набор данных готов, я разделил набор данных на три отдельные переменные: обучение, проверку и тестирование с соотношением 6:2:2. Таким образом, каждый набор состоит из 1033, 345 и 345 изображений соответственно. Для этого я использовал метод random_split из библиотеки PyTorch [3][1].

# Random split (train:6 valid:2 test:2)
train_set_size = int(len(dataset) * 0.6)
valid_set_size = int(len(dataset)*0.2)+1
test_set_size = valid_set_size
print(train_set_size,valid_set_size,test_set_size)

train_set, valid_set, test_set = data.random_split(dataset, [train_set_size, valid_set_size, test_set_size])

После наблюдения за различными классами в наборе данных было обнаружено несоответствие количества изображений в каждом классе, поэтому для объективного прогноза я использовал WeightedRandomSampler от Pytorch [4].

#Balancing imbalanced classes
train_sampler = WeightedRandomSampler(train_samples_weight.type('torch.DoubleTensor'), len(train_samples_weight))
valid_sampler = WeightedRandomSampler(valid_samples_weight.type('torch.DoubleTensor'), len(valid_samples_weight))

Перед подачей данных во взвешенный случайный сэмплер мы находим вес для каждого класса в обучении, а также данные проверки.

class_weight = 1/number_of_samples_in_class.

Таким образом, используя приведенную выше формулу, мы вычисляем вес каждого класса, а затем присваиваем вес каждому изображению в соответствии с их классом [1].

# Finding weight of each class
train_weight = 1/ train_class_sample_count

# Assigning weight to each index
train_samples_weight = np.array([train_weight[t] for t in train_list])
train_samples_weight = torch.from_numpy(train_samples_weight)

Теперь данные обучения и проверки готовы к загрузке в сеть, но перед этим данные должны присутствовать в памяти, поэтому мы используем метод Dataloader [5] [1] из PyTorch для загрузки данных с размером пакета 4.

# Loading data into RAM
train_dataloader = data.DataLoader(train_set, batch_size=4, sampler=train_sampler)
valid_dataloader = data.DataLoader(valid_set, batch_size=4, sampler=valid_sampler)
test_dataloader = data.DataLoader(test_set, batch_size=4)

Теперь, когда работа с данными завершена, я перешел к созданию моделей для использования этих данных и прогнозирования классов в соответствии с изображениями.

Создание модели глубокого обучения

VGG16

Первой моделью, которую я задумал создать, была VGG16. Я сослался на блог Ноумана [6], мой более ранний код [1] и исследовательскую статью Карена Симоняна и Эндрю Зиссермана [7]. Существует несколько вариантов VGG16, но я реализовал VGG16.

VGG16 имеет 16 утяжеляющих слоев. Параметр слоев свертки обозначается как «Conv (размер рецептивного поля) — (количество каналов)». В приведенном ниже коде [6] [1] показана реализация в соответствии с конфигурацией VGG16 в приведенных выше таблицах.

# VGG16
class VGG16(nn.Module):
    def __init__(self, classes=10):
        super(VGG16, self).__init__()
        self.layer1 = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU())
        self.layer2 = nn.Sequential(
            nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(), 
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer3 = nn.Sequential(
            nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU())
        self.layer4 = nn.Sequential(
            nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer5 = nn.Sequential(
            nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU())
        self.layer6 = nn.Sequential(
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU())
        self.layer7 = nn.Sequential(
            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer8 = nn.Sequential(
            nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU())
        self.layer9 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU())
        self.layer10 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer11 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU())
        self.layer12 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU())
        self.layer13 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.fc = nn.Sequential(
            nn.Dropout(0.5),
            nn.Linear(7*7*512, 4096),
            nn.ReLU())
        self.fc1 = nn.Sequential(
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU())
        self.fc2= nn.Sequential(
            nn.Linear(4096, classes))
        
    def forward(self, x):
        res = self.layer1(x)
        res = self.layer2(res)
        res = self.layer3(res)
        res = self.layer4(res)
        res = self.layer5(res)
        res = self.layer6(res)
        res = self.layer7(res)
        res = self.layer8(res)
        res = self.layer9(res)
        res = self.layer10(res)
        res = self.layer11(res)
        res = self.layer12(res)
        res = self.layer13(res)
        res = res.reshape(res.size(0), -1)
        res = self.fc(res)
        res = self.fc1(res)
        res = self.fc2(res)
        return res

Здесь можно увидеть несколько методов, поэтому я объясню некоторые из них.

CONV2D: применяется двумерная свертка для входных сигналов, состоящих из нескольких входных плоскостей [8].

BATCHNORM2D: применяет пакетную нормализацию к 4D-входу. Формула приведена ниже.

Здесь γ и β — обучаемые параметры входного размера C. По умолчанию для параметров установлено значение 1. [10]

RELU: это нелинейная функция активации, график которой указан ниже. ReLU(x)=(x)+=max(0,x). [9]

MaxPool2D: эта функция применяет максимальный пул, чтобы уменьшить количество параметров и помочь в переоснащении [11].

ПОСЛЕДОВАТЕЛЬНЫЙ: связывает вывод со входом. Здесь он связывает выходные данные каждого уровня в качестве входных данных для следующего уровня с применением таких методов, как нормализация партии, максимальное объединение и т. д.

Итак, теперь, глядя на приведенное выше описание, вы можете связать архитектуру, определенную в модели, с кодом.

Реснет

Я также использовал другую модель под названием ResNet. ResNet Решает проблему исчезающего градиента, возникающую в глубоких нейронных сетях, таких как VGG. Архитектура приведена ниже.

Создается ResBlk, который реализует пропуск соединения, как указано на диаграмме выше. здесь даунсэмплер используется для пропуска соединения. Метод пересылки в ResBlk применяется к вводу, а не к выводу.

class ResBlk(nn.Module):
    def __init__(self, input_chan, output_chan, stride = 1, downsample = None):
        super(ResBlk, self).__init__()
        self.convolution_layer1 = nn.Sequential(
                        nn.Conv2d(input_chan, output_chan, kernel_size = 3, stride = stride, padding = 1),
                        nn.BatchNorm2d(output_chan),
                        nn.ReLU())
        self.convolution_layer2 = nn.Sequential(
                        nn.Conv2d(output_chan, output_chan, kernel_size = 3, stride = 1, padding = 1),
                        nn.BatchNorm2d(output_chan))
        self.output_chan = output_chan
        self.relu_activation = nn.ReLU()
        self._downsampler = downsample
        
    def forward(self, z):
        weight_residual = z
        _output = self.convolution_layer1(z)
        _output = self.convolution_layer2(_output)
        if self._downsampler:
            weight_residual = self._downsampler(z)
        _output += weight_residual
        _output = self.relu_activation(out)
        return _output

Теперь, изучая архитектуру на приведенной выше диаграмме, я написал код с использованием Pytorch, ссылаясь на блог Nouman [13].

class ResNet34(nn.Module):
    def __init__(self, block, layers, classes):
        super(ResNet18, self).__init__()
        self.input_planes = 64
        self.convolution_layer1 = nn.Sequential(
                        nn.Conv2d(3, 64, kernel_size = 7, stride = 2, padding = 3),
                        nn.BatchNorm2d(64),
                        nn.ReLU())
        self.maxpooling = nn.MaxPool2d(kernel_size = 3, stride = 2, padding = 1)
        self.block_layer0 = self.create_layer(block, 64, layers[0], stride = 1)
        self.block_layer1 = self.create_layer(block, 128, layers[1], stride = 2)
        self.block_layer2 = self.create_layer(block, 256, layers[2], stride = 2)
        self.block_layer3 = self.create_layer(block, 512, layers[3], stride = 2)
        self.averagepool = nn.AvgPool2d(7, stride=1)
        self.fullyConnected = nn.Linear(2048, classes)
        
    def create_layer(self, block, planes, blocks, stride=1):
        _down_sample = None
        if stride != 1 or self.input_planes != planes:
            _down_sample = nn.Sequential(
                nn.Conv2d(self.input_planes, planes, kernel_size=1, stride=stride),
                nn.BatchNorm2d(planes),
            )
            
        block_layers = []
        block_layers.append(block(self.input_planes, planes, stride, _down_sample))
        self.input_planes = planes
        for i in range(1, blocks):
            layers.append(block(self.input_planes, planes))

        return nn.Sequential(*layers)
    
    
    def forward(self, z):
        z = self.convolution_layer1(z)
        z = self.maxpooling(z)
        z = self.block_layer0(z)
        z = self.block_layer1(z)
        z = self.block_layer2(z)
        z = self.block_layer3(z)

        z = self.averagepool(z)
        z = z.view(z.size(0), -1)
        z = self.fullyConnected(x)

        return x

Здесь функция create_layer создает слои в каждом отдельном блоке в соответствии с предоставленными параметрами.

Обучение

Поскольку я создал модели, теперь мне нужно их обучить. Поэтому, чтобы обучить его, я создал общий метод, который упоминается ниже.

def train(model, epochs, optimizer):
    total_step = len(train_dataloader)
    training_loss_values = []
    validation_loss_values = []

    for epoch in tqdm(range(epochs)):
        running_loss = 0.0
        for i, (images, labels) in tqdm(enumerate(train_dataloader)):  
            # Loading tensor into configured device
            images = images.to(device)
            labels = labels.to(device)

            # forward pass
            outputs = model(images)
            loss = criterion(outputs, labels)
            running_loss = running_loss + loss.item() 

            # backward pass
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            del images, labels, outputs
            torch.cuda.empty_cache()
            gc.collect()
            
        training_loss_values.append(running_loss/train_set_size)

        print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}' 
                       .format(epoch+1, epochs, i+1, total_step, loss.item()))

        # Validation
        with torch.no_grad():
            correct = 0
            total = 0
            for images, labels in tqdm(valid_dataloader):
                images = images.to(device)
                labels = labels.to(device)
                outputs = model(images)
                loss = criterion(outputs, labels)
                running_loss = running_loss + loss.item()

                _, predicted = torch.max(outputs.data, 1)
                total += labels.size(0)
                correct += (predicted == labels).sum().item()
                del images, labels, outputs
            validation_loss_values.append(running_loss/train_set_size)

        print('model accuracy on {} validation images = {} %'.format(345, 100 * correct / total))
    return model, training_loss_values, validation_loss_values

Здесь вводом является модель, т.е. VGG16/ResNet, эпохи (количество итераций), оптимизатор. Первым шагом здесь является загрузка изображений на устройство, которое может быть GPU или CPU. Затем на прямом проходе мы передаем изображения модели, чтобы начать обучение. Используемый здесь критериальный метод рассчитывает потери. Затем, после расчета потерь, мы выполняем обратный проход для обновления весов модели. Получив новые веса, мы проводим проверку с использованием того же критерия и получаем потерю проверки, что снова помогает изменить веса для следующей итерации.

Тестирование

После обновления нейронов нашей модели до желаемых весов мы затем проверяем их на другом наборе данных и получаем окончательную точность.

def test_accuracy(model):
    with torch.no_grad():
        correct = 0
        total = 0
        for images, labels in test_dataloader:
            images = images.to(device)
            labels = labels.to(device)
            outputs = model(images)
            tensors, predicted = torch.max(outputs.data, 1)
            total = total + labels.size(0)
            correct = correct + (predicted == labels).sum().item()
            del images, labels, outputs
        
        print('model accuracy on the {} test images: {} %'.format(345, 100 * correct / total)) 

Настройка гиперпараметров

# Hyper parameter Tuning

classes = 6
epochs = 10
batch_size = 50
learning_rate = 0.002
historical_result = []

model1 = VGG16(classes).to(device)

# Loss function and optimizer
criterion  = nn.CrossEntropyLoss()
optimizer1 = torch.optim.SGD(model1.parameters(), lr=learning_rate, weight_decay = 0.005, momentum = 0.2)  

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

KNN (k-ближайшие соседи):

Это контролируемый алгоритм машинного обучения, который решает задачи классификации. Этот алгоритм предполагает, что подобные объекты существуют в непосредственной близости. Алгоритм работает на вычислении расстояния между 2 точками. Здесь я использовал реализацию SkLearn kNN [14][15].

Обучение

Здесь обучение занимает O(n) раз, поэтому KNN работает довольно быстро. Гиперпараметры настроены на заданное количество соседей, которое следует учитывать при расчете расстояния. Существует несколько алгоритмов KNN, поэтому сохранение «авто», скорее всего, устанавливает лучший алгоритм в соответствии с данными.

Тестирование

Тестирование выполняется с использованием метода прогнозирования в модели KNN, который сопоставляет изображение во время выполнения, чтобы классифицировать его в соответствии с его соседями.

Полученные результаты

ВГГ16

Тест1

В этом тесте гиперпараметры указаны на диаграмме ниже. Полученная здесь точность теста составляет 57,57%. Как видно из графика потерь при обучении и валидации, переобучения нет.

Тест2

Здесь точность тестирования составляет 60,289%, что увеличилось на 3% из-за снижения скорости обучения и размера партии вместе с импульсом.

Реснет

Тест1

Здесь мы получаем точность тестирования 27%, что намного меньше, чем у нашей модели VGG16. Итак, есть некоторые проблемы с настройкой гиперпараметров. Там график потерь также показывает незначительные изменения в потерях при обучении и проверке.

Тест2

Глядя на плохие результаты первого теста, я изменил скорость обучения и размер партии, что дало точность тестирования 50%. Это все еще меньше, чем наша лучшая точность VGG16. График потерь показал немного многообещающий результат по мере увеличения точности.

Тест3

В этом финальном тесте я увеличил количество эпох и нашел лучший результат. Окончательная точность тестирования составила 80%. Итак, я пришел к выводу, что ResNet работает лучше, чем VGG16.

Кнн

При реализации KNN я обнаружил, что наилучшая точность составляет 32%, что намного меньше, чем у других моделей CNN. Я изменил соседа на 4 и нашел лучшую точность.

Заключение

Наилучшую точность дала модель ReSnet, которая составляет около 80%. Таким образом, в ходе этого эксперимента можно сказать, что CNN работает довольно хорошо по сравнению с другими алгоритмами для изображений.

Мои вклады

  1. Правильное преобразование набора данных изображения, чтобы в конце получить точный результат.
  2. Внедрение модели VGG16 с нуля со ссылкой на исследовательскую работу и блог Nouman.
  3. Внедрение модели ResNet19 из исследовательской работы и изменение входных каналов в соответствии с этими пользовательскими данными изображения.
  4. Правильная настройка гиперпараметров, чтобы избежать переобучения. Я применил оптимальные скорости обучения, чтобы получить наилучшие результаты.
  5. Построение необходимых графиков для отслеживания потерь и переобучения, если таковые имеются.
  6. Понимание библиотек PyTorch и опробование новых методов, таких как кросс-энтропийная потеря.
  7. Реализовал KNN с нуля, используя библиотеку Sklearn и обработал данные, чтобы они соответствовали модели.

Проблемы

  1. Было два изображения, которые были повреждены, из-за чего я не смог преобразовать изображения, выдающие ошибки. Поэтому я их идентифицировал и удалил.
  2. Разделение тестов поезда и проверки не происходило из-за несоответствия количества изображений в данных проверки и тестирования, поэтому я проверил их снова и добавил одно дополнительное изображение в набор проверки, чтобы сделать его равным тестовым данным.
  3. Модель ResNet выдавала ошибку при умножении матриц из-за несоответствия ряда входных параметров, поэтому я изменил исходные входные каналы, чтобы исправить их.
  4. Я также пытался реализовать SVM для данных изображения, но это заняло много времени и работало бесконечно, поэтому я пропустил эту реализацию.
  5. Поскольку было много измененных данных, метод ImageFolder не работал. Поэтому я создал отдельную папку данных в Kaggle с необходимой структурой папок, а затем применил преобразования.

Будущая работа

  1. Найдите способ реализовать SVM в классификации изображений через GPU.
  2. Реализуйте ImageNet с заданными данными и проверьте его точность.
  3. Выполняйте дополнительную аугментацию изображений с помощью сети GAN и тестируйте их с различными моделями CNN.

Рекомендации

[1] https://github.com/biraaj/Caltech101-Airplanes-Motorbikes-Schooners/blob/main/caltech101.ipynb

[2] https://pytorch.org/vision/stable/generated/torchvision.datasets.ImageFolder.html

[3] https://pytorch.org/docs/stable/data.html#torch.utils.data.random_split

[4] https://pytorch.org/docs/stable/data.html#torch.utils.data.WeightedRandomSampler

[5] https://pytorch.org/docs/stable/data.html#torch.utils.data.DataLoader

[6] https://blog.paperspace.com/vgg-from-scratch-pytorch/

[7] https://arxiv.org/pdf/1409.1556.pdf

[8] https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html

[9] https://pytorch.org/docs/stable/generated/torch.nn.ReLU.html

[10] https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html

[11] https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html

[12] https://arxiv.org/pdf/1512.03385.pdf

[13] https://blog.paperspace.com/writing-resnet-from-scratch-in-pytorch/

[14] https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KNeighborsClassifier.html

[15] https://medium.com/analytics-vidhya/knn-implementation-from-scratch-96-6-accuracy-python-machine-learning-31ba66958644