Область глубокого обучения стала свидетелем замечательных достижений в генеративных моделях. Одним из таких прорывов является разработка глубоких сверточных генеративно-состязательных сетей (DCGAN).

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

Возвращаясь к GAN

Генеративно-состязательные сети (GAN) работают на основе архитектуры генератора-дискриминатора. В GAN генераторы играют роль повышения частоты дискретизации. Генератор принимает в качестве входных данных изображение с низким разрешением или вектор случайного шума и постепенно увеличивает его дискретизацию. С другой стороны, дискриминаторы понижают разрешение изображения, уменьшая его размеры.

Генератор — повышающая дискретизация

Повышение дискретизации позволяет генератору преобразовывать входные данные с низким разрешением в выходные данные с более высоким разрешением, которые напоминают реальные данные. Часто используются транспонированные свертки, интерполяция ближайших соседей, билинейная интерполяция или перемешивание пикселей. В DCGAN используются транспонированные свертки. Давайте посмотрим на код PyTorch.

В архитектуре модели мы видим использование транспонированных сверток (ConvTranspose2D). Это апсэмплеры. Они могут эффективно увеличивать разрешение при сохранении определенных структурных паттернов. Они расширяют пространственные размеры карт объектов. Они обращают вспять этот процесс обычных CNN, расширяя пространственные измерения входного тензора при изучении соответствующих весов посредством обратного распространения. Вместо того, чтобы скользить фильтрами по входу и понижать его дискретизацию, эти свертки используют дробные шаги больше 1 для повышения дискретизации и увеличения разрешения. Здесь важно отметить, что транспонированные свертки — это не «деконволюции», а «декомпрессоры».

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

Наконец, мы добавляем функцию активации tanh, которая требуется для получения массива изображений в виде заданного диапазона вывода от -1 до 1.

class Generator(nn.Module):
    def __init__(self, latent_dim=100):
        super().__init__()
        self.latent_dim = latent_dim

        def block(in_channels, out_channels, normalize=True, ks=4, s=2, pad=1):
            layers = []
            layers.append(nn.ConvTranspose2d(in_channels, out_channels, kernel_size=ks, stride=s, padding=pad, bias=False))
            if normalize:
                layers.append(nn.BatchNorm2d(out_channels))
            layers.append(nn.ReLU(True))
            return layers

        self.model = nn.Sequential(
            *block(self.latent_dim, G * 8, normalize=False, s=1, pad=0),
            *block(G * 8, G * 4),
            *block(G * 4, G * 2),
            *block(G * 2, G),
            nn.ConvTranspose2d(in_channels=G, out_channels=3, kernel_size=4, stride=2, padding=1, bias=False),
            nn.Tanh()
        )
        
    def forward(self, z):
        return self.model(z)

Дискриминатор — понижающая дискретизация

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

  1. Уменьшение размерности
  2. Путем постепенного понижения дискретизации GAN могут изучать иерархические представления, в которых карты объектов с более низким разрешением отражают глобальный контекст, а карты с более высоким разрешением фокусируются на более мелких деталях.
  3. Позволяет GAN изучать более надежные и обобщенные функции, не сосредотачиваясь на отдельных пикселях.
class Discriminator(nn.Module):
    def __init__(self):
        super().__init__()
        
        def block(in_channels, out_channels, normalize=True):
            layers=[]
            layers.append(nn.Conv2d(in_channels, out_channels, kernel_size=4, stride=2, padding=1, bias=False))
            if normalize:
                layers.append(nn.BatchNorm2d(out_channels))
            layers.append(nn.LeakyReLU(0.2, inplace=True))
            return layers

        self.model = nn.Sequential(
            *block(3, D, normalize=False),
            *block(D, D*2),
            *block(D*2, D*4),
            *block(D*4, D*8),
            nn.Conv2d(D*8, 1, kernel_size=4, stride=1, padding=0, bias=False),
            nn.Sigmoid() 
        )

    def forward(self, img):
        return self.model(img)

Дискриминатор выполняет понижающую дискретизацию, используя слой Conv2D. Это в значительной степени классификатор изображений, пытающийся отличить реальное изображение от сгенерированного. Что отличает Дискриминатор от классификаторов изображений, так это функция активации LeakyRelu.

В отличие от ReLU, LeakyReLU позволяет выскользнуть определенному количеству. Это позволяет нейронам оставаться активными. Если бы использовался ReLU, это привело бы к тому, что выход нейронов был бы равен нулю, что привело бы к «мертвым нейронам».

Обучение

Вот где GAN становятся интересными. GAN запускает мин-макс игры Генератора и Дискриминатора. Сначала мы отправляем шум в Генератор для генерации изображения. Реальные изображения отправляются в Дискриминатор, и для этого случая обнаруживается потеря. Точно так же мы делаем это для поддельных изображений. Наконец, мы находим общую потерю дискриминатора, вычитая среднее значение.

Как это работает!

После выполнения прямого прохода выполняется обратное распространение и вычисляются потери. Оптимизаторы генераторов и дискриминаторов играют роль в уменьшении этих потерь в соответствующих моделях. Теперь начинается игра мин-макс. Генератор делает все возможное, чтобы уменьшить потери при генерации, то есть пытается минимизировать свои потери. Чтобы генерация прошла успешно, генератор пытается максимизировать потери дискриминатора.

D(x) — потери, рассчитанные дискриминатором для реальных изображений. D(G(z)) — потеря, рассчитанная дискриминатором для сгенерированных изображений для скрытого вектора «z». Основная цель генератора — обмануть или обмануть дискриминатор, создавая реалистичные изображения. Минимизация log(1-D(G(z))) поощряет выходы G(Z), которые имеют более высокие вероятности, назначенные дискриминаторами.

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

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

img_list = []
train={
    "G_losses" : [],
    "D_losses" : [],
    "D_xs" : [],
    "D_G_z1s" : [],
    "D_G_z2s" : []
}

val={
    "G_losses" : [],
    "D_losses" : [],
    "D_xs" : [],
    "D_G_z1s" : [],
    "D_G_z2s" : []
}

iters = 0
num_epochs = 30

for epoch in tqdm(range(num_epochs)):
    
    #TRAINING LOOP
    for i, data in enumerate(train_dataloader, 0):
        optimizerD.zero_grad()
        
        real_data = data[0].to(device)
        b_size = real_data.size(0)
        
        #GENERATE FAKE IMAGE
        targets = torch.ones(batch_size, 1, device=device)
        noise = torch.randn(b_size, nz, 1, 1, device=device)
        fake_data = netG(noise)
        
        #DISCRIMINATOR LOSS CALCULATION
        #REAL LOSS
        output = netD(real_data).view(-1)
        real_targets = torch.ones(real_data.size(0), 1, device=device)
        errD_real = criterion(output, real_targets.squeeze(1))
        errD_real.backward()
        D_x = output.mean().item()

        #FAKE LOSS
        fake_targets = torch.zeros(fake_data.size(0), 1, device=device)
        output = netD(fake_data.detach()).view(-1)
        errD_fake = criterion(output, fake_targets.squeeze(1))
        errD_fake.backward()
        D_G_z1 = output.mean().item()

        #DISCRIMINATOR LOSS
        errD = (errD_real + errD_fake)/2

        optimizerD.step()

        #GENERATOR LOSS CALCULATION
        optimizerG.zero_grad()
        output = netD(fake_data).view(-1)
        Gfake_targets = torch.ones_like(output)

        errG = criterion(output, Gfake_targets)
        errG.backward()
        D_G_z2 = output.mean().item()

        optimizerG.step()

        #SAVE THE LOGS
        train["G_losses"].append(errG.item())
        train["D_losses"].append(errD.item())
        train["D_xs"].append(D_x)
        train["D_G_z1s"].append(D_G_z1)
        train["D_G_z2s"].append(D_G_z2)

    with torch.no_grad():
        #VALIDATION LOOP
        for i, data in enumerate(val_dataloader, 0):
        
            real_data = data[0].to(device)
            b_size = real_data.size(0)

            targets = torch.ones(batch_size, 1, device=device)
            noise = torch.randn(b_size, nz, 1, 1, device=device)
            fake_data = netG(noise)

            output = netD(real_data).view(-1)
            real_targets = torch.ones(real_data.size(0), 1, device=device)
            errD_real = criterion(output, real_targets.squeeze(1))
            D_x = output.mean().item()

            fake_targets = torch.zeros(fake_data.size(0), 1, device=device)
            output = netD(fake_data.detach()).view(-1)
            errD_fake = criterion(output, fake_targets.squeeze(1))
            D_G_z1 = output.mean().item()

            errD = (errD_real + errD_fake)/2

            output = netD(fake_data).view(-1)
            Gfake_targets = torch.ones_like(output)

            errG = criterion(output, Gfake_targets)
            D_G_z2 = output.mean().item()

            val["G_losses"].append(errG.item())
            val["D_losses"].append(errD.item())
            val["D_xs"].append(D_x)
            val["D_G_z1s"].append(D_G_z1)
            val["D_G_z2s"].append(D_G_z2)

Выход и заключение

Давайте проверим вывод нашего DCGAN для набора данных Anime Image.

Хммм… результаты выглядят прилично, и это требует решительной импровизации. Архитектуры GAN выглядят простыми, но их сложно взломать. Существует множество других алгоритмов, таких как StyleGAN и преобразователи ViT, которые быстро развиваются, помогая с GAN. DCGAN, без сомнения, является прекрасным и простым для понимания алгоритмом. Но, как мы знаем, это зависит и от сложности данных.

Ссылка на код: https://www.kaggle.com/code/kausthubkannan/anime-faces-dcgan-pytorch/notebook

Давайте посмотрим на другие алгоритмы GAN в следующий раз перед Эпохалипсисом!