Глубокая оптимизация сверточных нейронных сетей: Анализ методов улучшения модели на примере CIFAR-10

Введение

Сверточные нейронные сети (CNN) стали основой для обработки изображений и компьютерного зрения. Однако их обучение требует тщательной настройки архитектуры и гиперпараметров, что может быть сложной задачей, особенно при работе с большими наборами данных. В этой статье мы подробно рассмотрим несколько методов оптимизации, используемых для повышения производительности CNN на примере набора данных CIFAR-10, и покажем, как различные техники влияют на потери и точность модели. Мы протестируем аугментацию данных, различные архитектурные решения, такие как Batch Normalization и Dropout, и адаптивные подходы к обучению.

Набор данных CIFAR-10 и предобработка

CIFAR-10 — это один из самых популярных наборов данных для обучения моделей компьютерного зрения. Он содержит 60,000 изображений размером 32×32 пикселя, разделенных на 10 классов, что делает его отличным кандидатом для тестирования эффективности различных методов оптимизации.

import torch
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

# Определение преобразований для набора данных CIFAR-10
transform_cifar = transforms.Compose([
    transforms.RandomHorizontalFlip(),
    transforms.RandomCrop(32, padding=4),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

# Загрузка набора данных
cifar10_train = datasets.CIFAR10(root='data', train=True, transform=transform_cifar, download=True)
cifar10_loader_train = DataLoader(cifar10_train, batch_size=32, shuffle=True)

Для эксперимента мы применили несколько различных преобразований, включая аугментацию с предобработкой на основе наборов данных Fashion-MNIST и SVHN.

# Загрузка каждого набора данных с предобработкой
cifar10_loader = DataLoader(cifar10_train, batch_size=32, shuffle=True)
fmnist_loader = DataLoader(fmnist_train, batch_size=32, shuffle=True)
svhn_loader = DataLoader(svhn_train, batch_size=32, shuffle=True)


# Добавляем трансформацию для повторения канала в Fashion-MNIST
transform_fmnist_rgb = transforms.Compose([
    transform_fmnist,  # Текущие трансформации для FMNIST
    Lambda(lambda x: x.repeat(3, 1, 1))  # Повторяем канал 3 раза
])

# Загрузка данных Fashion-MNIST с трансформацией повторения каналов
fmnist_cifar_transform = DataLoader(
    datasets.FashionMNIST(root='data', train=True, transform=transform_fmnist_rgb, download=True),
    batch_size=32, shuffle=True
)

svhn_loader = DataLoader(svhn_train, batch_size=32, shuffle=True)

# Загрузка CIFAR-10 с альтернативной предобработкой
cifar10_fmnist_transform = DataLoader(datasets.CIFAR10(root='data', train=True, transform=transform_fmnist, download=True), batch_size=32, shuffle=True)
cifar10_svhn_transform = DataLoader(datasets.CIFAR10(root='data', train=True, transform=transform_svhn, download=True), batch_size=32, shuffle=True)

# Загрузка Fashion-MNIST с альтернативной предобработкой
#fmnist_cifar_transform = DataLoader(datasets.FashionMNIST(root='data', train=True, transform=transform_cifar, download=True), batch_size=32, shuffle=True)
fmnist_svhn_transform = DataLoader(datasets.FashionMNIST(root='data', train=True, transform=transform_svhn, download=True), batch_size=32, shuffle=True)

# Загрузка SVHN с альтернативной предобработкой
svhn_cifar_transform = DataLoader(datasets.SVHN(root='data', split='train', transform=transform_cifar, download=True), batch_size=32, shuffle=True)
svhn_fmnist_transform = DataLoader(datasets.SVHN(root='data', split='train', transform=transform_fmnist, download=True), batch_size=32, shuffle=True)

Эксперимент 1: Влияние предобработки данных

Различные методы предобработки могут влиять на то, какие особенности изображений извлекает модель. Мы протестировали несколько вариантов, включая стандартную предобработку CIFAR-10, а также трансформации, адаптированные из других наборов данных:

  • Оригинальная предобработка CIFAR-10

  • Предобработка, основанная на Fashion-MNIST

  • Предобработка на основе SVHN

Обучение CIFAR-10 с собственной предобработкой:

Эпоха 1

Потери: 1.6969

Точность: 37.90%

Эпоха 5

Потери: 1.1355

Точность: 59.95%

Обучение CIFAR-10 с предобработкой Fashion-MNIST

Эпоха 1

Потери: 1.4984

Точность: 46.54%

Эпоха 5

Потери: 0.9440

Точность: 66.95%

Обучение CIFAR-10 с предобработкой SVHN

Эпоха 1

Потери: 1.6444

Точность: 39.83%

Эпоха 5

Потери: 1.0892

Точность: 61.49%

Результаты: Предобработка, основанная на Fashion-MNIST, продемонстрировала лучшие результаты, обеспечив потерю в 0.9440 и точность 66.95%. Это позволяет предположить, что некоторые преобразования могут быть лучше адаптированы для задач классификации изображений, несмотря на различия в данных.

Результаты для различных видов предобработки данных

Результаты для различных видов предобработки данных

Эксперимент 2: Оптимизация архитектуры — выбор фильтров и пуллинга

Следующим шагом было изучение того, как количество фильтров и метод пуллинга влияет на результаты. Мы тестировали три варианта с разным количеством фильтров (16, 32 и 64), а также несколько типов пуллинга.

Пример конфигурации фильтров:

import torch.nn as nn

class CNNVariant1(nn.Module):
    def __init__(self):
        super(CNNVariant1, self).__init__()
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
        self.pool = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(16 * 16 * 16, 10)
    
    def forward(self, x):
        x = self.pool(torch.relu(self.conv1(x)))
        x = x.view(-1, 16 * 16 * 16)
        x = self.fc1(x)
        return x

Результаты: Среднее количество фильтров (32 фильтра) и Max Pooling 2×2 оказались оптимальными, обеспечив 57.33% точности и потерю в 1.2018. Использование большого количества фильтров привело к переобучению, а использование Average Pooling показало менее хорошие результаты.

Результаты по различным количествам фильтров

Результаты по различным количествам фильтров

Эксперимент 3: Batch Normalization и Dropout

Batch Normalization и Dropout — это техники, которые помогают улучшить стабильность и обобщающую способность модели. Мы тестировали Batch Normalization после каждого сверточного слоя и только в начале и в конце, а также различные значения Dropout.

Пример использования Batch Normalization и Dropout:

class CNNBatchNorm(nn.Module):
    def __init__(self):
        super(CNNBatchNorm, self).__init__()
        self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
        self.bn1 = nn.BatchNorm2d(32)
        self.dropout = nn.Dropout(0.2)
        self.fc1 = nn.Linear(32 * 16 * 16, 10)

    def forward(self, x):
        x = self.dropout(self.bn1(torch.relu(self.conv1(x))))
        x = x.view(-1, 32 * 16 * 16)
        x = self.fc1(x)
        return x

Результаты: Batch Normalization после каждого слоя обеспечил лучшую стабильность обучения и точность 60.25%. Умеренный Dropout (0.2) оказался наиболее эффективным и достиг 63.79% точности, тогда как сильный Dropout (0.5) привел к ухудшению результатов.

Результаты по количеству уровней Dropout

Результаты по количеству уровней Dropout

Эксперимент 4: Адаптивные стратегии темпа обучения и динамический размер пакета

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

Cyclic Learning Rate:

from torch.optim.lr_scheduler import CyclicLR

optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
scheduler = CyclicLR(optimizer, base_lr=0.001, max_lr=0.01, step_size_up=5)

Результаты: Оптимизатор Adam показал лучшие результаты по сравнению с Cyclic Learning Rate, с потерей 1.2331 и точностью 57.42%. Dynamic Batch Size и Gradient Accumulation продемонстрировали схожие результаты, но Gradient Accumulation позволил несколько улучшить точность при ограниченных вычислительных ресурсах.

Результаты для адаптивных стратегий оптимизации архитектуры

Результаты для адаптивных стратегий оптимизации архитектуры

ROC-AUC для модели с адаптивным темпом (Adam)

ROC-AUC для модели с адаптивным темпом (Adam)

Эксперимент 5: Progressive Layer Growth и Pruning

Мы протестировали Progressive Layer Growth и Random Filter Pruning для регулировки сложности модели.

Пример Progressive Layer Growth:

class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
        self.fc1 = nn.Linear(16 * 16 * 16, 10)

# После нескольких эпох добавляем слои
class ExtendedCNN(SimpleCNN):
    def __init__(self):
        super(ExtendedCNN, self).__init__()
        self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
        self.fc2 = nn.Linear(32 * 8 * 8, 10)

Результаты: Progressive Layer Growth улучшил точность модели до 60.87%, а Random Filter Pruning обеспечил точность в 64.80%, что доказывает его эффективность для предотвращения переобучения.

Визуализация результатов Progressive Layer Growth

Визуализация результатов Progressive Layer Growth

Заключение

Результаты экспериментов подтверждают, что оптимизация архитектуры и гиперпараметров CNN требует комплексного подхода, включающего различные методы.

  • Наилучшие результаты были достигнуты при использовании предобработки на основе Fashion-MNIST, Batch Normalization после каждого слоя, Dropout с вероятностью 0.2 и оптимизатора Adam. Эти методы в совокупности обеспечили улучшение точности, минимизацию потерь и повышение стабильности модели.

  • Адаптивные стратегии темпа обучения и размера пакета также показали хорошие результаты, особенно Gradient Accumulation для ограниченных вычислительных ресурсов.

  • Progressive Layer Growth и Pruning оказались полезными для регулировки сложности модели, помогая избежать переобучения и улучшить обобщающую способность.

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

Спасибо за внимание!

P.S. Пишите в комментариях, какие методы вы используете для улучшения моделей, и делитесь своими результатами!

© Habrahabr.ru