Генеративно-состязательные сети, или GAN, произвели фурор в сообществе ИИ благодаря своей сверхъестественной способности генерировать поразительно реалистичные изображения. Они использовались для создания всего: от потрясающе реалистичных человеческих лиц до произведений искусства, которые выглядят так, будто их нарисовал мастер-художник. Сегодня я предоставлю исчерпывающее руководство о том, что такое GAN и как они работают, дополненное фрагментами кода Python на реальном примере для ясности.

Что такое ГАН?

Изобретенные Яном Гудфеллоу и его командой в 2014 году (бумага), GAN представляют собой тип системы машинного обучения, состоящей из двух основных компонентов: генератора и дискриминатора. Эти две сети работают друг против друга (отсюда и термин состязательный), и именно эта уникальная динамика делает GAN такими эффективными. По мере того, как сети генератора и дискриминатора конкурируют друг с другом, они обе лучше справляются со своими задачами. Генератор учится создавать более реалистичные изображения, а дискриминатор учится точнее определять поддельные изображения.

Генератор создает новые экземпляры данных, а Дискриминатор оценивает их на подлинность; то есть, принадлежит ли каждый экземпляр данных, которые он просматривает, фактическому набору обучающих данных или нет.

Понимание концепции

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

Что касается GAN, мы хотим, чтобы сеть Генератора производила данные, неотличимые от реальных данных, в то время как Дискриминатор стремится точно классифицировать данные как настоящие или поддельные. GAN использовались для создания самых разных реалистичных изображений, включая лица, животных и объекты.

Пример из реальной жизни: создание лиц с помощью DCGAN

Чтобы воплотить эти идеи в жизнь, давайте рассмотрим реальный пример: глубокая сверточная генеративно-состязательная сеть (DCGAN), обученная генерировать новых знаменитостей после показа фотографий множества реальных знаменитостей. Этот конкретный GAN принимает случайный шум в качестве входных данных и генерирует реалистичные человеческие лица в качестве выходных данных. Для целей этого примера я использую набор данных CelebA от Kaggle.

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

Вот схема того, как определить DCGAN:

  1. Генератор: начинается со слоя, который принимает скрытый вектор (случайный шум), за которым следуют несколько сверточных слоев с повышающей дискретизацией и нормализацией. Выходной слой использует функцию активации tanh, которая масштабирует вывод в диапазоне от -1 до 1, что соответствует диапазону наших нормализованных обучающих изображений.
# Generator Code

class Generator(nn.Module):
    def __init__(self, ngpu):
        super(Generator, self).__init__()
        self.ngpu = ngpu
        self.main = nn.Sequential(
            # input is Z, going into a convolution
            nn.ConvTranspose2d( nz, ngf * 8, 4, 1, 0, bias=False),
            nn.BatchNorm2d(ngf * 8),
            nn.ReLU(True),
            # state size. (ngf*8) x 4 x 4
            nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 4),
            nn.ReLU(True),
            # state size. (ngf*4) x 8 x 8
            nn.ConvTranspose2d( ngf * 4, ngf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 2),
            nn.ReLU(True),
            # state size. (ngf*2) x 16 x 16
            nn.ConvTranspose2d( ngf * 2, ngf, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf),
            nn.ReLU(True),
            # state size. (ngf) x 32 x 32
            nn.ConvTranspose2d( ngf, nc, 4, 2, 1, bias=False),
            nn.Tanh()
            # state size. (nc) x 64 x 64
        )

    def forward(self, input):
        return self.main(input)

2. Дискриминатор:Начинается со сверточного слоя, за которым следуют несколько сверточных слоев с субдискретизацией и нормализацией. Выходной слой использует функцию активации sigmoid, которая масштабирует вывод в диапазоне от 0 до 1, обеспечивая вероятность того, что входное изображение является реальным.

class Discriminator(nn.Module):
    def __init__(self, ngpu):
        super(Discriminator, self).__init__()
        self.ngpu = ngpu
        self.main = nn.Sequential(
            # input is (nc) x 64 x 64
            nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf) x 32 x 32
            nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 2),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*2) x 16 x 16
            nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 4),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*4) x 8 x 8
            nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 8),
            nn.LeakyReLU(0.2, inplace=True),
            # state size. (ndf*8) x 4 x 4
            nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
            nn.Sigmoid()
        )

    def forward(self, input):
        return self.main(input)

Обучающие изображения:

Механика обучения для GAN

С нашими настройками Дискриминатора (D) и Генератора (G) механика их обучения становится в центре внимания за счет использования определенных функций потерь и оптимизаторов. Примечательным упоминанием здесь является потеря двоичной перекрестной энтропии (часто называемая BCELoss). Для тех, кто склонен искать документацию, PyTorch предлагает обширный обзор.

Для энтузиастов математики функция BCELoss может быть визуализирована как:

l(x, y) = L = {l ₁,….,lₙ}^T

Где:

lₙ = -[yₙ * log xₙ + (1 — yₙ) * log(1-xₙ)]

Прелесть этой функции заключается в ее способности охватывать оба логарифмических компонента целевой функции, а именно log(D(x)) и log(1−D(G(z))). Компонент, который мы выбираем для вычисления из уравнения BCE, определяется входными данными y, которые по сути являются нашими метками Ground Truth (GT).

Для ясности в нашей предстоящей механике обучения:

  • Настоящий ярлык определяется как 1
  • Поддельный ярлык определяется как 0

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

Чтобы управлять процессами обучения как D, так и G, мы используем два разных оптимизатора Adam. Если вы ссылаетесь на исследовательскую работу DCGAN, вы заметите, что эти оптимизаторы настроены на скорость обучения 0,0002 и значение β1, равное 0,5.

Совет по отслеживанию работы генератора — работать с последовательным пакетом скрытых векторов, обычно взятых из распределения Гаусса (часто называемого fixed_noise). Во время наших обучающих итераций периодическое введение этого fixed_noise в G предлагает интригующее зрелище: вы будете свидетелями того, как изображения постепенно обретают форму из того, что первоначально выглядело как простой шум.

Демистификация обучения GAN

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

Черпая вдохновение из статьи Гудфеллоу, мы собираемся принять Алгоритм 1 и смешать его с идеями известных ганхаков. Два основных указателя оттуда включают:

  • Создание отдельных мини-пакетов для реальных и поддельных изображений.
  • Настройка цели G для увеличения с помощью logD(G(z)).

Этот путь обучения GAN разделен на два основных сегмента:

1. Воспитание дискриминатора

Основная цель дискриминатора? Поднимите его мастерство в классификации входных данных как реальных или синтетических. Как объясняет Гудфеллоу, цель состоит в том, чтобы «подняться по стохастическому градиенту дискриминатора». По сути, мы стремимся к наилучшему результату log(D(x))+log(1−D(G(z))).

Стратегия? Двунаправленный подход:

  1. Реальные образцы. Извлеките партию из обучающего набора, дайте ей пройти через D, подсчитайте потери с помощью log(D(x)), и отобразите градиенты в обратном направлении.
  2. Поддельные образцы: с нашим текущим генератором в качестве мастера создайте поддельную партию, позвольте D проанализировать ее, определите потери с помощью log(1− D(G(z))) и объединить эти градиенты в другом обратном пути.

С градиентами, собранными как из реального, так и из искусственного мира, настало время для оптимизатора Дискриминатора.

2. Лепка генератора

Погружаясь в основополагающий документ, мы видим цель генератора: уменьшить log(1−D(G(z >))) плодить еще более обманчивые фейки. Но, как заметил Гудфеллоу, начинающие ученики часто спотыкаются из-за тусклых градиентов. Обходной путь? Накачайте лог(D(G(z))).

В коде это превращается в:

  • Детище генератора с первого шага встречает дискриминатор.
  • Мы вычисляем потери G, интригующе используя настоящие ярлыки в качестве исходных данных.
  • Градиенты для G отображаются с обратной траекторией.
  • И вуаля!! ДНК G (параметры) преображается с помощью оптимизатора.

Парадоксальным на первый взгляд маневром является использование реальных меток для функции потерь. Тем не менее, это мастерский ход. Он манит аспектом log(x) BCELoss, точно согласуясь с нашими намерениями.

# Training Loop

# Lists to keep track of progress
img_list = []
G_losses = []
D_losses = []
iters = 0

print("Starting Training Loop...")
# For each epoch
for epoch in range(num_epochs):
    # For each batch in the dataloader
    for i, data in enumerate(dataloader, 0):
        
        ############################
        # (1) Update D network: maximize log(D(x)) + log(1 - D(G(z)))
        ###########################
        ## Train with all-real batch
        netD.zero_grad()
        # Format batch
        real_cpu = data[0].to(device)
        b_size = real_cpu.size(0)
        label = torch.full((b_size,), real_label, dtype=torch.float, device=device)
        # Forward pass real batch through D
        output = netD(real_cpu).view(-1)
        # Calculate loss on all-real batch
        errD_real = criterion(output, label)
        # Calculate gradients for D in backward pass
        errD_real.backward()
        D_x = output.mean().item()

        ## Train with all-fake batch
        # Generate batch of latent vectors
        noise = torch.randn(b_size, nz, 1, 1, device=device)
        # Generate fake image batch with G
        fake = netG(noise)
        label.fill_(fake_label)
        # Classify all fake batch with D
        output = netD(fake.detach()).view(-1)
        # Calculate D's loss on the all-fake batch
        errD_fake = criterion(output, label)
        # Calculate the gradients for this batch, accumulated (summed) with previous gradients
        errD_fake.backward()
        D_G_z1 = output.mean().item()
        # Compute error of D as sum over the fake and the real batches
        errD = errD_real + errD_fake
        # Update D
        optimizerD.step()

        ############################
        # (2) Update G network: maximize log(D(G(z)))
        ###########################
        netG.zero_grad()
        label.fill_(real_label)  # fake labels are real for generator cost
        # Since we just updated D, perform another forward pass of all-fake batch through D
        output = netD(fake).view(-1)
        # Calculate G's loss based on this output
        errG = criterion(output, label)
        # Calculate gradients for G
        errG.backward()
        D_G_z2 = output.mean().item()
        # Update G
        optimizerG.step()
        
        # Output training stats
        if i % 50 == 0:
            print('[%d/%d][%d/%d]\tLoss_D: %.4f\tLoss_G: %.4f\tD(x): %.4f\tD(G(z)): %.4f / %.4f'
                  % (epoch, num_epochs, i, len(dataloader),
                     errD.item(), errG.item(), D_x, D_G_z1, D_G_z2))
        
        # Save Losses for plotting later
        G_losses.append(errG.item())
        D_losses.append(errD.item())
        
        # Check how the generator is doing by saving G's output on fixed_noise
        if (iters % 500 == 0) or ((epoch == num_epochs-1) and (i == len(dataloader)-1)):
            with torch.no_grad():
                fake = netG(fixed_noise).detach().cpu()
            img_list.append(vutils.make_grid(fake, padding=2, normalize=True))
            
        iters += 1

Откровения после тренировки:

По завершении тренировочного балета GAN появляется ряд статистических данных, предлагающих идеи и показатели:

  • Loss_D: совокупность путешествий дискриминатора по реальным и поддельным областям, инкапсулированная в log(D(x) )+log(1−D(G(z))).
  • Потеря_G: история генератора, рассказанная log(D(G(z) )).
  • D(x): средний вердикт дискриминатора для реального мира. Интригующая прогрессия от почти 1, теоретически извивается до 0,5 по мере уточнения G.
  • D(G(z)): пара средних значений до и после обновления D для искусственного мира. Начиная с забвения (0), и теоретически танцуя к 0,5, поскольку G находит свой ритм.

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

Погружение в результаты обучения GAN

Холст установлен, кисти нарисованы, и теперь пришло время сделать шаг назад и полюбоваться нашим шедевром GAN. Во-первых, давайте посмотрим, как изменились потери D и G во время обучения:

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

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

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