Реализация простого GAN.

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

На двух рисунках ниже показаны несколько изображений из исходного набора данных рукописных цифр MNIST (рис. 1) и изображений, сгенерированных GAN, который ранее был обучен с использованием изображений в наборе данных (рис. 2).

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

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

  1. Что такое GAN и как выглядит наш набор данных
  2. Реализация архитектуры
  3. Генерация реальных и поддельных образцов
  4. Обучение ГАН
  5. Полученные результаты

1. Что такое GAN и как выглядит наш набор данных

Архитектура ГАН

На рисунке 3 ниже показано, что GAN состоит из двух основных частей: генератора и дискриминатора. Как следует из названия, генератор отвечает за создание новых (поддельных) образцов, в то время как дискриминатор пытается отличить настоящие от поддельных. Основная цель обучения GAN — сделать так, чтобы генератор мог генерировать новые выборки, чтобы эти выборки были неразличимы дискриминатором. Как только это произойдет, это означает, что наш генератор теперь может создавать образцы, качество которых уже не уступает оригиналам.

Набор данных

Как я упоминал ранее, мы будем работать с двумерными данными, так как они намного проще по сравнению с набором данных MNIST, который мы видели ранее. Точки данных, которые должны быть сгенерированы, следуют схеме синусоиды, в которой эти точки будут восприниматься как исходные образцы (рис. 4). Позже мы ожидаем, что наш обученный GAN сможет генерировать такой же (или, по крайней мере, похожий) шаблон на основе этих исходных точек данных. Обратите внимание, что наши исходные образцы могут не всегда выглядеть точно так же, как показано на рисунке 4, поскольку мы выполняем своего рода аугментацию данных, которая позволяет генерировать больше точек данных. Однако эти точки по-прежнему следуют одной и той же синусоидальной волне.

2. Реализация архитектуры

Теперь пришло время кодировать! В этом случае мы собираемся реализовать нейронную сеть с помощью Keras, простого, но мощного фреймворка, который я обычно использую для своих проектов глубокого обучения. Все используемые модули показаны в кодовом блоке 1.

# Codeblock 1
import numpy as np
import matplotlib.pyplot as plt
from keras.layers import Input, Dense
from keras.models import Model
from tqdm import tqdm

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

Дискриминатор

# Codeblock 2
def create_discriminator():
    d_input = Input(shape=(2,))
    d = Dense(30, activation='relu')(d_input)
    d = Dense(20, activation='relu')(d)
    d = Dense(10, activation='relu')(d)
    d_output = Dense(1, activation='sigmoid')(d)

    discriminator = Model(d_input, d_output)

    discriminator.compile(loss='binary_crossentropy', 
                          optimizer='adam', 
                          metrics=['acc'])
    
    return discriminator

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

Во-первых, эта функция работает, инициализируя входной слой (d_input). Помните, что, поскольку наши точки данных лежат в двумерном пространстве (ось x и ось y), нам нужно разместить эти значения координат с помощью массива длины два для каждой выборки. Далее этот слой соединяется с 3 полносвязными слоями с количеством нейронов 30, 20 и 10 соответственно. На самом деле, у меня нет особой причины использовать такое количество нейронов. Вы можете попробовать использовать другие конфигурации, если хотите. Наконец, эти скрытые слои будут подключены к выходному слою одного нейрона. Имейте в виду, что, поскольку цель дискриминатора состоит в том, чтобы различать настоящие и поддельные образцы, мы можем сказать, что эта часть GAN работает так же, как двоичный классификатор. Позже в процессе обучения мы собираемся пометить настоящие образцы 1, а поддельные образцы будут помечены 0. В дополнение к этому реализовано binary_crossentropy, поскольку это функция потерь, которая специализирована для задачи двоичной классификации.

Теперь давайте посмотрим, как выглядит наш дискриминатор, запустив кодовый блок 3 ниже. Результат показан на рисунке 5.

# Codeblock 3
d_test = create_discriminator()
d_test.summary()

Генератор

Поскольку дискриминатор успешно создан, теперь мы перейдем к генератору. Реализация кода этой части GAN показана в кодовом блоке 4.

# Codeblock 4
def create_generator():
    g_input = Input(shape=(10,))
    g = Dense(16, activation='relu')(g_input)
    g = Dense(8, activation='relu')(g)
    g = Dense(4, activation='relu')(g)
    g_output = Dense(2, activation='linear', 
                     kernel_initializer='he_uniform')(g)
    
    generator = Model(g_input, g_output)
    
    return generator

Функция create_generator() изначально работает, инициализируя входной слой, который принимает вектор размера 10, в котором это число соответствует размеру нашего скрытого пространства. Опять же, нет никакой конкретной причины для выбора этого числа. Само скрытое пространство — это, по сути, место, где генерируются случайные числа (т. е. шум). Позже эти шумы будут преобразованы в значимую точку данных обученным генератором.

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

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

Теперь давайте запустим Codeblock 5 ниже, чтобы увидеть детали генератора, который мы только что реализовали.

# Codeblock 5
g_test = create_generator()
g_test.summary()

Объединение генератора и дискриминатора

На данный момент у нас уже есть две части GAN. Я хотел бы объединить их, используя функцию create_gan(), показанную в Codeblock 6.

# Codeblock 6
def create_gan(generator, discriminator):
    discriminator.trainable = False     # (1)

    gan_input = Input(shape=(10,))      # (2)
    gan = generator(gan_input)          # (3)
    gan_output = discriminator(gan)     # (4)
    
    gan = Model(gan_input, gan_output)

    gan.compile(loss='binary_crossentropy', 
                optimizer='adam', 
                metrics=['acc'])
    
    return gan

Вышеупомянутая функция работает, принимая два входа: модели generator и discriminator. Затем мы делаем наш дискриминатор необучаемым (т.е. замораживаем его веса и смещения) (1). По сути, это сделано, потому что на этом этапе мы хотим обучить только генератор. (Как я упоминал ранее, дискриминатор будет обучаться отдельно). Сама инициализация сети довольно проста. Сначала мы определяем входное измерение сети, в котором оно должно совпадать с размером скрытого пространства (2). Затем мы соединяем этот входной слой с генератором (3), и, наконец, выходные данные будут подаваться на дискриминатор (4). Двоичная перекрестная энтропия, которую мы здесь реализовали, используется для проверки того, похож ли уже вывод генератора на исходные образцы.

Мы можем увидеть детали всей GAN, запустив Codeblock 7.

# Codeblock 7
gan_test = create_gan(g_test, d_test)
gan_test.summary()

Согласно выходным данным, показанным на рисунке 7, мы видим, что мы получили всего 1289 параметров, но только 358 из них поддаются обучению. Эти 358 параметров на самом деле соответствуют параметрам, принадлежащим генератору, а те, которые не подлежат обучению, отсчитываются от дискриминатора, который мы заморозили еще в кодовом блоке 6.

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

3. Генерация реальных и поддельных образцов

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

Генерация реальных образцов

Как я упоминал ранее, исходные сэмплы будут повторять форму синусоиды. Для этого мы можем просто использовать функцию np.sin(). Подробности использования этой функции можно увидеть в Codeblock 8.

# Codeblock 8
def create_real_samples(n_data):
    x_real = np.random.normal(0, 3, n_data)      # (1)
    y_real = np.sin(x_real)                      # (2)
    
    x_real = x_real.reshape(x_real.shape[0], 1)
    y_real = y_real.reshape(y_real.shape[0], 1)
    
    features_real = np.hstack((x_real, y_real))
    labels_real = np.array([1] * n_data)        # (3)
    
    return features_real, labels_real

Функция create_real_samples() работает, принимая количество создаваемых точек данных. Следовательно, значение должно быть целым числом. Далее мы будем использовать np.random.normal() для генерации случайных значений для оси X (1). Затем эти значения будут отображены функцией синуса, чтобы получить значение для оси Y (2). Мы также создаем группу 1 в строке, отмеченной (3), которая будет действовать как метка каждой реальной точки данных. Теперь мы можем проверить, работает ли функция должным образом, запустив Codeblock 9 ниже. В этом случае я говорю функции create_real_samples() сгенерировать 100 выборок.

# Codeblock 9
x_line = np.linspace(-7, 7, 1000)
y_line = np.sin(x_line)

f_real_test, l_real_test = create_real_samples(100)
plt.scatter(f_real_test[:,0], f_real_test[:,1], c='b')
plt.show()

Создание поддельных образцов

Следующим шагом, который необходимо сделать, является создание функции для генерации поддельных данных. За это отвечает функция, которую я называю create_fake_samples(), показанная в кодовом блоке 10.

# Codeblock 10
def create_fake_samples(generator, n_data):
    latent_points = np.random.normal(0, 3, (n_data, 10))          # (1)
    features_fake = generator.predict(latent_points, verbose=0)   # (2)
    
    labels_fake = np.array([0] * n_data)                          # (3)
    return features_fake, labels_fake

Эта функция на самом деле похожа на craete_real_samples(), которую мы только что инициализировали в предыдущем подразделе. Эта функция работает, принимая два входных параметра: объект генератор и количество создаваемых поддельных точек данных. После этого мы также будем использовать np.random.normal() для генерации случайных значений в скрытом пространстве. Важно обратить внимание на форму, так как наш генератор принимает ряд точек, лежащих в скрытом пространстве в 10-м измерении. По сути, это причина, по которой нам нужно установить форму (n_data, 10) (1). Затем мы позволяем генератору распространять эти случайные значения вперед, чтобы сгенерировать поддельные выборки (2). Наконец, поскольку эти данные являются поддельными, они будут помечены нулями (3). Теперь мы можем запустить следующий код, чтобы увидеть, как выглядят точки данных, созданные нашим генератором.

# Codeblock 11
f_fake_test, l_fake_test = create_fake_samples(g_test, 100)
plt.scatter(f_fake_test[:,0], f_fake_test[:,1], c='orange')
plt.show()

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

Обертывание двух функций

В этом подразделе я собираюсь создать новую функцию, которая будет реализовывать как create_real_samples(), так и create_fake_samples(). Подробности этой функции можно увидеть в кодовом блоке 12 ниже.

# Codeblock 12
def visualize(generator, n_data):
    feats_real, labels_real = create_real_samples(n_data)
    feats_fake, labels_fake = create_fake_samples(generator, n_data)
    
    plt.scatter(feats_real[:,0], feats_real[:,1], c='blue')
    plt.scatter(feats_fake[:,0], feats_fake[:,1], c='orange')
    plt.show()

Каждый раз, когда вызывается функция visualize(), она будет генерировать реальные и поддельные образцы, а затем напрямую отображать их в двухмерном пространстве. Имейте в виду, что настоящие образцы нарисованы синим цветом, а поддельные отмечены оранжевым. Мы можем проверить правильность работы функции, запустив Codeblock 13. Позже в процессе обучения эта функция visualize() поможет нам качественно изучить результаты обучения.

# Codeblock 13
visualize(g_test, 100)

4. Обучение ГАН

Функция обучения

Обучение GAN будет реализовано в другой функции, чтобы коды выглядели аккуратно. Посмотрите на функцию train() в следующем кодовом блоке.

# Codeblock 14
def train(generator, discriminator, gan, epochs, batch_size):
    for i in tqdm(range(epochs)):
        feats_real, labels_real = create_real_samples(batch_size)    # (1)
        feats_fake, labels_fake = create_fake_samples(generator, batch_size)  # (2)
        
        discriminator.train_on_batch(feats_real, labels_real)        # (3)
        discriminator.train_on_batch(feats_fake, labels_fake)        # (4)
        
        latent_points = np.random.normal(0, 3, (batch_size, 10))     # (5)
        labels_gan = np.array([1] * batch_size)                      # (6)
        
        gan.train_on_batch(latent_points, labels_gan)                # (7)
        
        if i == 0:
            visualize(generator, batch_size)
        elif i%1000 == 0:
            visualize(generator, batch_size)

Самое первое, что делается в коде, — это определение цикла for, количество итераций которого определяется в соответствии со значением epoch. Для каждой эпохи генерируются как настоящие, так и поддельные образцы, а также их метки (комментарий №1 и №2). Затем сгенерированные образцы будут использоваться для обучения дискриминатора двумя партиями. В первом используются только настоящие образцы, а во втором — только поддельные образцы (комментарии №3 и №4).

Все еще внутри того же цикла мы создадим точки в скрытом пространстве на линии, отмеченной (5). На этом этапе мы начнем обучать генератор, используя входные данные, поступающие из скрытого пространства. Так как мы хотим, чтобы все сэмплы, созданные генератором, воспринимались как настоящие, нам нужно пометить их все единицами (комментарий №6). Сам генератор будет обучаться как целая GAN, в которой веса и смещения дискриминатора заморожены (комментарий № 7). Вы можете вернуться к кодовому блоку 6, чтобы увидеть, как замораживаются веса дискриминатора. Наконец, мы увидим производительность нашего генератора для создания новых выборок каждые 1000 эпох с помощью функции visualize(), которую мы создали ранее.

Использование функции обучения

Поскольку функция обучения была создана, теперь мы можем использовать ее для фактического обучения модели GAN (см. Кодовый блок 15). В этом случае я решил установить эпоху 10 000 и размер партии 128 по некоторым причинам. Затем мы инициализируем дискриминатор (1) и генератор (2). Я также соединим их с помощью функции create_gan() (3). Наконец, мы помещаем все объекты, которые мы только что инициализировали, в функцию train(). Мы собираемся обсудить полученные результаты в следующем разделе.

# Codeblock 15
EPOCHS = 10000
BATCH_SIZE = 128

discriminator = create_discriminator()      # (1)
generator = create_generator()              # (2)
gan = create_gan(generator, discriminator)  # (3)

train(generator, discriminator, gan, 
      EPOCHS, BATCH_SIZE)

5. Результаты

Kaggle Notebook занимает около 15 минут, чтобы завершить все процессы, выполняемые в Codeblock 15. На самом деле это не совсем скучно, поскольку я показываю прогресс обучения каждые 1000 эпох. Теперь давайте посмотрим, как выглядят наши поддельные образцы в эпоху 0 на рисунке 11 ниже. На рисунке видно, что наш генератор совершенно не представляет себе, как он должен распределять все точки данных из-за того, что поддельные образцы (оранжевые) вообще не следуют синусоидальному паттерну. Это абсолютно логично, потому что на данный момент модель GAN (особенно генератор) еще не обучена.

После 1000 эпох мы видим, что наш генератор начинает понимать, куда поместить поддельные образцы. На рис. 12 видно, что многие фальшивые точки данных перекрываются оригинальными образцами.

Результат становится еще лучше в эпоху 2000. Посмотрите на рисунок 13 ниже.

Перенесемся вперед: на рис. 14 ниже показаны результаты от эпохи 3000 до эпохи 8000. Вы можете видеть на последнем изображении, что все сгенерированные поддельные точки данных пространственно очень близки к исходным. Это указывает на то, что наш генератор теперь достаточно умен, поскольку он может генерировать образцы, неотличимые от реальных.

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

Надеюсь, вы найдете эту статью полезной! Вы можете увидеть полностью рабочий код на моем GitHub, перейдя по этой ссылке. Спасибо за прочтение!

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

[1] Джейсон Браунли. Как разработать GAN для создания рукописных цифр MNIST. Мастерство машинного обучения. https://machinelearningmastery.com/how-to-develop-a-generative-adversarial-network-for-an-mnist-handwriting-digits-from-scratch-in-keras/ [По состоянию на 4 апреля 2023 г.].

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .

Заинтересованы в масштабировании запуска вашего программного обеспечения? Ознакомьтесь с разделом Схема.