Генеративные состязательные нейронные сети (GAN — генеративки) на Keras

Изображения людей синтезированные генеративной сетью (GAN) в 2017 году.

Generative Adversarial Networks (Генеративные Состязательные Сети) придуманы сравнительно недавно в 2014 г. и были представлены Ian’a Goodfellow’a и рядом других исследователей университета Монреаля, включая Yoshua Bengio.

Идея генеративно-состязательных сетей гениально проста. Есть две нейронных сети работающих в паре:

  • Творец (generator) — формирует новые объекты заданного класса при подаче на вход шума. Например, фото лиц людей.
  • Критик (discriminator) — оценивает качество работ творца и дает ему обратную связь: хорошо или никуда не годится.

Мне больше нравится именно термин творец и критик, хотя я не видел, чтобы эти сети в статьях так называли. Слово дискриминатор для меня носит какой-то негативный оттенок. 🙂

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

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

Творец-ученик должен быть в гармонии с критиком-учителем. Если ученик превосходит своего учителя, то ему нужно искать нового учителя, иначе рост мастерства может замедлится. В жизни так и происходит. Например, когда изучаешь язык сначала прогресс идет достаточно хорошо, а затем наступает фаза насыщения. Общаться можешь уже хорошо, а учить идиомы уже слишком трудозатратно, поскольку не так много людей смогут оценить по-достоинству. 🙂 В жизни, ученик является и критиком самому себе — самокритик. В генеративно-состязательных нейронных сетях (GAN-ах) эти роли разведены.

Как работает GAN

Одна нейронная сеть работает в роли творца (генератора) и создает новые экземпляры данных (генерирует новый датаcет). Вторая — критик (дискриминатор) оценивает их качество/подлинность и решает, относится ли полученный экземпляр данных к тренировочному датасету (реальному) или создан творцом.

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

Между тем, творец-генератор постоянно создает новые изображения, которые отдает критику-дискриминатору. Он надеется, что в какой-то момент времени его мастерство настолько вырастет, что критик перестанет угадывать и начнет ошибаться, принимая его работы за подлинные. Идет постоянное состязание между творцом и искусствоведом. 🙂

Шаги, которые проходит GAN:

  • Генератор получает шум навходе и пропуская его через веса сети формирует изображение.
  • Это сгенерированное изображение подается в дискриминатор вместе с потоком изображений, взятых из набора данных.
  • Дискриминатор принимает как реальные, так и поддельные изображения и возвращает вероятности, числа от 0 до 1, причем 1 говорит о том, что изображение точно из тренировочного датасета, 0 — сформированное генератором.

Таким образом, у GAN есть двойной цикл обратной связи:

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

Код GAN на Keras

Пример кода взят отсюда. Я несколько модифицировал его для удобства понимания и гибкости. Google Colab для изучения GAN — здесь. Там достаточно длинный класс, который полностью разбирать в тексте не имеет особого смысла.

Код снабжен большим количеством комментариев взятых по аналогии из курса «Нейронные сети на Python» читаемого Дмитрием Романовым в «Университете искуственного интеллекта«. Действительно значимых для понимания кода комментариев немного, я на них акцентирую внимания отдельно.

Метод генератора в классе GAN-а прост:

    def build_generator(self):                                                  #Метод для создания генератора

        noise_shape = (self.latent_dim,)

        model = Sequential()                                                    # Инициализируем модель generator
        model.add(Dense(256, input_shape=noise_shape, name = "Generator_In"))   # Добавляем Dense-слой на 256 нейронов (размерность входных данных = latent_dim)
        model.add(LeakyReLU(alpha=0.2))                                         # Добавляем слой активационной функции с параметром 0.2
        model.add(BatchNormalization(momentum=0.8))                             # Добавляем слой BatchNormalization  (momentum - параметр расчета скользящего среднего и дисперсии)
        model.add(Dense(512))                                                   # Добавляем Dense-слой на 512 нейронов  
        model.add(LeakyReLU(alpha=0.2))                                         # Добавляем слой активационной функции с параметром 0.2
        model.add(BatchNormalization(momentum=0.8))                             # Добавляем слой BatchNormalization  (momentum - параметр расчета скользящего среднего и дисперсии)
        model.add(Dense(1024))                                                  # Добавляем Dense-слой на 1024 нейронов
        model.add(LeakyReLU(alpha=0.2))                                         # Добавляем слой активационной функции с параметром 0.2    
        model.add(BatchNormalization(momentum=0.8))                             # Добавляем слой BatchNormalization  (momentum - параметр расчета скользящего среднего и дисперсии)    
        model.add(Dense(np.prod(self.img_shape), activation='tanh'))
        model.add(Reshape(self.img_shape, name = "Generator_Out"))
        model.name = "Generator"  
        model.summary()

        noise = Input(shape=noise_shape)                                        # Создаем слой Input (Записываем входные данные рамерностью latent_dim в noise)
        img = model(noise)                                                      # Записываем в переменную img значение, возвращаемое generator'ом  с входным параметром noise

        return Model(inputs = noise, outputs = img)                             # Возвращаем модель generator-а (входные данные: noise, выходные данные: img)

На вход модели подается вектор шума, проходит через несколько полносвязных слоев и далее Reshape(self.img_shape) преобразует в изображение, которое будет подаваться на вход критика-дискриминатора.

Критик-дискриминатор — это нейронная сеть построенная для классификации. Ей нужно отличать оригинальное изображение из тестового датасета от сформированного творцом-генератором:

    def build_discriminator(self, img_shape):                                   # Функция создания дискриминатора

        #img_shape = (self.img_rows, self.img_cols, self.channels)     

        model = Sequential()                                                    # Инициализируем модель discriminator    
        model.add(Flatten(input_shape=img_shape, name = "Discriminator_In"))    # Создаем слой Flatten (размерность входных данных = (img_rows, img_cols, channels), размерность выходных данных = img_rows * img_cols * channels )
        model.add(Dense(512))                                                   # Добавляем Dense-слой на 512 нейронов
        model.add(LeakyReLU(alpha=0.2))                                         # Добавляем слой активационной функции с параметром 0.2
        model.add(Dense(256))                                                   # Добавляем Dense-слой на 256 нейронов
        model.add(LeakyReLU(alpha=0.2))                                         # Добавляем слой активационной функции с параметром 0.2
        model.add(Dense(1, activation='sigmoid', name = "Discriminator_Out"))   # Добавляем Dense-слой c 1 нейроном с активационной функцией sigmoid, поскольку нам нужно категорировать входноые изображения на два класса 1 - из тестовой выборки и 0 - сформирован генератором. 
        model.name = "Discriminator" 
        model.summary()

        img = Input(shape=img_shape)                                            # Создаем слой Input (записываем входные данные размерностью (img_rows, img_cols, channels) в img)
        validity = model(img)                                                   # Записываем в переменную validity значение, возвращаемое discriminator'ом с входным параметром img

        return Model(inputs = img, outputs = validity)                          # Создаем модель discriminator (входные данные: img, выходные данные: validity)

В аргументе передается размер матрицы изображения. Он вынесен, поскольку при создании объекта класса в качестве аргумента можно передать другой дискриминатор (функцию) и ему для создания первого слоя потребуется shape изображения. Это учебный код, для демонстрации как работает GAN.

Поскольку сеть построена на полносвязных слоях, принимающих вектор, сначала двумерное избражение преобразуется в вектор Flatten(input_shape=img_shape). Затем проходит через несколько полносвязных слоев и на выходе слой с одним нейроном и активационной функцией sigmoid, который выдает степень уверенности дискриминатора: 1 — изображение из исходного (тренировочного) dataset-а, 0 — изображение созданное генератором.

Пример сверточного дискриминатора. Он принимает на вход размер изображения. Эта функция создается снаружи класса и может быть передана при создании объекта класса как callback функция.

def build_conv_discriminator(img_shape): # Функция создания сверточного дискрминатора
  model = Sequential()                                                          # Инициализируем модель currDisc

  model.add(Conv2D(4, (3,3), padding="same", input_shape=img_shape, name = "Discriminator_In"))            # Создаем слой  Conv2D (размерность входных данных (img_shape), ядро свертки = 4, окно свертки = (3,3))
  model.add(LeakyReLU(alpha=0.2))                                               # Добавляем слой активационной функции с параметром 0.2
  model.add(Flatten())                                                          # Добавляем слой Flatten ()
  model.add(Dense(512))                                                         # Добавляем Dense-слой на 512 нейронов
  model.add(LeakyReLU(alpha=0.2))                                               # Добавляем слой активационной функции с параметром 0.2
  model.add(Dense(1, activation='sigmoid', name = "Discriminator_Out"))         # Добавляем Dense-слой c 1 нейроном с активационной функцией sigmoid, поскольку нам нужно категорировать входноые изображения на два класса 1 - из тестовой выборки и 0 - сформирован генератором. 

  img = Input(shape=img_shape)                                                  # Создаем слой Input (записываем входные данные размерностью (img_rows, img_cols, channels) в img)
  validity = model(img)                                                         # Записываем в переменную validity значение, возвращаемое currDisc'ом с входным параметром img

  discriminator_conv = Model(inputs = img, outputs = validity)                  # Создаем модель discriminator_conv (входные данные: img, выходные данные: validity)

  return discriminator_conv                                                     # Функция возвращает discriminator_conv

Архитектура сети GAN создается в методе класса build_GAN:

    def build_GAN(self):
        optimizer = Adam(0.0002, 0.5)

        # Build and compile the discriminator
        self.discriminator.name = "Discriminator"
        self.discriminator.compile(loss='binary_crossentropy',
            optimizer=optimizer,
            metrics=['accuracy'])

        # Build and compile the generator
        self.generator.name = "Generator"
        self.generator.compile(loss='binary_crossentropy', optimizer=optimizer)

        # The generator takes noise as input and generated imgs
        z = Input(shape=(self.latent_dim,), name = "GAN_In")                    # Создаем слой Input размерностью latent_dim. На входе подается шум, а на выходе - сгенерированные изображения.
        img = self.generator(z)

        # For the combined model we will only train the generator
        self.discriminator.trainable = False                                    # Замораживаем обучение дискриматора, чтобы в объединенной модели тренировался только генератор 

        # The valid takes generated images as input and determines validity
        valid = self.discriminator(img)                                         #Создаем модель дискриминатора-критика. На вход подаются сгенерированные изображения, а на выходе вероятность распознавания исходных изображений  

        # The combined model  (stacked generator and discriminator) takes
        # noise as input => generates images => determines validity
        self.combined = Model(z, valid, name = "GAN")                           # В объединенной модели на входе шум, а на выходе вероятность насколько генератор-творец обманул дискриминатора-критика    
        self.combined.compile(loss='binary_crossentropy', optimizer=optimizer)
        self.combined.summary()

Я разметил входные и выходные слои именами, чтобы на model.summary() было видно какая модель с какими входами и выходами выводится.

Model: "Generator"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
Generator_In (Dense)         (None, 256)               25856     
_________________________________________________________________
leaky_re_lu_51 (LeakyReLU)   (None, 256)               0         
_________________________________________________________________
batch_normalization_31 (Batc (None, 256)               1024      
_________________________________________________________________
dense_43 (Dense)             (None, 512)               131584    
_________________________________________________________________
leaky_re_lu_52 (LeakyReLU)   (None, 512)               0         
_________________________________________________________________
batch_normalization_32 (Batc (None, 512)               2048      
_________________________________________________________________
dense_44 (Dense)             (None, 1024)              525312    
_________________________________________________________________
leaky_re_lu_53 (LeakyReLU)   (None, 1024)              0         
_________________________________________________________________
batch_normalization_33 (Batc (None, 1024)              4096      
_________________________________________________________________
dense_45 (Dense)             (None, 784)               803600    
_________________________________________________________________
Generator_Out (Reshape)      (None, 28, 28, 1)         0         
=================================================================
Total params: 1,493,520
Trainable params: 1,489,936
Non-trainable params: 3,584
_________________________________________________________________

Use new discriminator.

Model: "Discriminator"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
Discriminator_In (Conv2D)    (None, 28, 28, 4)         40        
_________________________________________________________________
leaky_re_lu_54 (LeakyReLU)   (None, 28, 28, 4)         0         
_________________________________________________________________
flatten_9 (Flatten)          (None, 3136)              0         
_________________________________________________________________
dense_46 (Dense)             (None, 512)               1606144   
_________________________________________________________________
leaky_re_lu_55 (LeakyReLU)   (None, 512)               0         
_________________________________________________________________
Discriminator_Out (Dense)    (None, 1)                 513       
=================================================================
Total params: 1,606,697
Trainable params: 1,606,697
Non-trainable params: 0
_________________________________________________________________
Model: "GAN"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
GAN_In (InputLayer)          (None, 100)               0         
_________________________________________________________________
Generator (Model)            (None, 28, 28, 1)         1493520   
_________________________________________________________________
Discriminator (Model)        (None, 1)                 1606697   
=================================================================
Total params: 3,100,217
Trainable params: 1,489,936
Non-trainable params: 1,610,281

Принцип работы GAN в коде Keras

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

Я в коде метода в явном виде задал свойство self.discriminator.trainable = True, хотя, в данном случае — это необязательно, поскольку в build_GAN() уже указано, что self.discriminator.trainable = False, т.е. при тренировке объединенной модели веса дискриминатора будут заморожены и тренироваться будет ТОЛЬКО генератор.

    def train(self, epochs, batch_size=128, save_interval=1000, save_images = False):

        # Load the dataset
        (X_train, _), (_, _) = mnist.load_data()

        # Rescale -1 to 1
        X_train = (X_train.astype(np.float32) - 127.5) / 127.5                  # Масштабируем значение в диапазон от -1 до 1, поскоьлку активационная функция tanh, у которого значения лежат от -1 до +1
        X_train = np.expand_dims(X_train, axis = 3)                             # Добавляем третью размерность для X_train ((28,28) => (28,28,1))

        valid = np.ones((batch_size, 1))                                        # Создаем массив единиц длинной batch_size
        fake = np.zeros((batch_size, 1))                                        # Создаем массив нулей длинной batch_size

        for epoch in range(epochs):

            # ---Train Discriminator---
            idx = np.random.randint(0, X_train.shape[0], batch_size)            # Выбираем случайным образом batch_size картинок из исходной обучающей выбрки для тренировки дискриминатора 
            imgs = X_train[idx]                                                 # В переменную imgs записываем значение из X_train с индексами из idx

            noise = np.random.normal(0, 1, (batch_size, self.latent_dim))       # Формируем массив размерностью (batch_size, self.latent_dim) из нормально распределенных значений

            gen_imgs = self.generator.predict(noise)                            # Формируем массив изображений с помощью входной переменной generator

            self.discriminator.trainable = True
            d_loss_real = self.discriminator.train_on_batch(imgs, valid)        # Ошибка дискриминатора, обученного на реальных картинках. Передаем в функцию train_on_batch реальные изображения (imgs) и массив единиц (valid).
            d_loss_fake = self.discriminator.train_on_batch(gen_imgs, fake)     # Ошибка дискриминатора, обученного на сгенерированных картинках. Передаем в функцию train_on_batch сгенерированные изображения (gen_imgs) и массив нулей (fake)
            d_loss = np.add(d_loss_real, d_loss_fake) / 2                       # Получаем массив средних ошибок дискриминатора. Поэлементно складываем массивы d_loss_real и d_loss_fake и делим каждое значение пополам

            # --- Train Generator ---
            noise = np.random.normal(0, 1, (batch_size, self.latent_dim))       # Формируем массив рамерностью (batch_size, self.latent_dim) из нормально распределенных значений
            self.discriminator.trainable = False
            g_loss = self.combined.train_on_batch(x = noise, y = valid)         # Получаем ошибку генератора. Передаем в функцию train_on_batch шум (noise) и массив единиц (valid)

            print ("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" % (epoch, d_loss[0], 100*d_loss[1], g_loss)) # Plot the progress

            if (epoch % save_interval == 0) or (epoch == epochs - 1):           # Выводим/сохраняем изображения каждые save_interval эпох и в конце цикла
              self.save_imgs(epoch, save_images)

Основное отличие в коде от привычной тренировки запуском model.fit() — использование метода model.train_on_batch(). Он обучает модель только на одном батче данных. Соответственно, чтобы прогнать обучение по всем эпохам, нужно в цикле вызывать model.train_on_batch().

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

Дискриминатор получает случайные картинки от генератора и промаркированные 0-ми и реальные картинки из обучающей выборки промаркированные 1-ми и подстраивает свои веса так, чтобы отличать одно от другого. В момент, когда вызывается train_on_batch() автоматом проставляется self.discriminator.trainable = True, иначе было бы не логично. Именно поэтому необязательно в явном виде указывать trainable = True.

После того как дискриминатор обучился на актуальных достижениях творца в части самостоятельного воссоздания аналога исходных изображений его веса замораживаются self.discriminator.trainable = False и начинается тренировка общей модели генератор + дискриминатор, при этом на выходе при вызове train_on_batch() в качестве y задается вектор заполненный 1-ми.

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

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

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

Честно говоря, просто обалденно изящно придумано! Не сразу дошло.

Нюансы

Как упоминал Дмитрий Романов наличие сверточного дискриминатора или генератора можно легко отличить по результирующим фото. При использовании сверточных сетей на фото нет редких белых точек, т.е. картинка выглядит ровно черной. В случае с Dense слоями на картинках периодически есть редкие остатки белых точек.

Результаты работы GAN на MIST после 30000 эпох на batch_size — 128 и Dense генераторе и дискриминаторе
Результаты работы GAN на MIST после 30000 эпох на batch_size — 128 и дискриминаторе со сверточными (Convolution) слоями

Качество обучения зависит от размеров batch_size. На больших размерах время на эпоху увеличивается, но визуально обучение идет быстрее, т.е. на меньшем количестве эпох получается достойный результат. Сверточная сеть обучается медленнее. Но это все субъективные утверждения, время не засекал.

Параметры точности распознавания при сбалансированных генераторе и дискриминаторе должны находится в диапазоне 60 — 80%. Если вероятность работы дискриминатора близко к 100%, значит, он подавляет генератор, не давая ему обучаться. Нужно либо ослаблять дискриминатор, упрощая сеть, либо усилить генератор, добавив элементы сети.

Если же вероятность около 50%, значит генератор настолько хорош, что дискриминатору сложно распознавать фейки и в этом случае генератор тоже не будет совершенствоваться, у него и так все хорошо, стимула нет что-то менять. 🙂

Полезные ссылки

Spread the love
Запись опубликована в рубрике IT рецепты. Добавьте в закладки постоянную ссылку.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *