Добавить информацию о классе в сеть keras

Я пытаюсь выяснить, как я буду использовать информацию метки моего набора данных с помощью Generative Adversarial Networks. Я пытаюсь использовать следующую реализацию условных GAN, которые можно найти здесь. Мой набор данных содержит два разных домена изображения (реальные объекты и эскизы) с общей информацией о классе (стул, дерево, оранжевый и т.д.). Я выбрал эту реализацию, которая рассматривает только два разных домена как разные "классы" для соответствия (образцы образцов X соответствуют реальным изображениям, тогда как целевые образцы y соответствуют изображениям эскиза).

Есть ли способ изменить мой код и принять во внимание информацию о классе (стул, дерево и т.д.) Во всей моей архитектуре? Я хочу, чтобы на самом деле мой дискриминатор предсказывал, относятся ли мои сгенерированные образы от генератора к определенному классу, а не только то, являются ли они реальными или нет. Как и в случае с текущей архитектурой, система во всех случаях пытается создавать похожие эскизы.

Обновление: дискриминатор возвращает тензор размера 1x7x7 после чего оба y_true и y_pred проходят через слой сглаживания перед вычислением потери:

def discriminator_loss(y_true, y_pred):
     BATCH_SIZE=100
     return K.mean(K.binary_crossentropy(K.flatten(y_pred), K.concatenate([K.ones_like(K.flatten(y_pred[:BATCH_SIZE,:,:,:])),K.zeros_like(K.flatten(y_pred[:BATCH_SIZE,:,:,:])) ]) ), axis=-1)

и функция потерь дискриминатора над генератором:

def discriminator_on_generator_loss(y_true,y_pred):
     BATCH_SIZE=100
     return K.mean(K.binary_crossentropy(K.flatten(y_pred), K.ones_like(K.flatten(y_pred))), axis=-1)

Furthremore, моя модификация модели дискриминатора для уровня 1 вывода:

model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
#model.add(Activation('sigmoid'))

Теперь дискриминатор выводит 1 слой. Как я могу изменить вышеупомянутые функции потерь соответственно? Должен ли я иметь 7 вместо 1, для n_classes = 6 + один класс для прогнозирования реальных и поддельных пар?

Ответ 1

Предлагаемое решение

Повторное использование кода из репозитория, который вы поделили, вот некоторые предлагаемые модификации для обучения классификатора вдоль вашего генератора и дискриминатора (их архитектуры и другие потери остаются нетронутыми):

from keras import backend as K
from keras.models import Sequential
from keras.layers.core import Dense, Dropout, Activation, Flatten
from keras.layers.convolutional import Convolution2D, MaxPooling2D

def lenet_classifier_model(nb_classes):
    # Snipped by Fabien Tanc - https://www.kaggle.com/ftence/keras-cnn-inspired-by-lenet-5
    # Replace with your favorite classifier...
    model = Sequential()
    model.add(Convolution2D(12, 5, 5, activation='relu', input_shape=in_shape, init='he_normal'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Convolution2D(25, 5, 5, activation='relu', init='he_normal'))
    model.add(MaxPooling2D(pool_size=(2, 2)))
    model.add(Flatten())
    model.add(Dense(180, activation='relu', init='he_normal'))
    model.add(Dropout(0.5))
    model.add(Dense(100, activation='relu', init='he_normal'))
    model.add(Dropout(0.5))
    model.add(Dense(nb_classes, activation='softmax', init='he_normal'))

def generator_containing_discriminator_and_classifier(generator, discriminator, classifier):
    inputs = Input((IN_CH, img_cols, img_rows))
    x_generator = generator(inputs)

    merged = merge([inputs, x_generator], mode='concat', concat_axis=1)
    discriminator.trainable = False
    x_discriminator = discriminator(merged)

    classifier.trainable = False
    x_classifier = classifier(x_generator)

    model = Model(input=inputs, output=[x_generator, x_discriminator, x_classifier])

    return model


def train(BATCH_SIZE):
    (X_train, Y_train, LABEL_train) = get_data('train')  # replace with your data here
    X_train = (X_train.astype(np.float32) - 127.5) / 127.5
    Y_train = (Y_train.astype(np.float32) - 127.5) / 127.5
    discriminator = discriminator_model()
    generator = generator_model()
    classifier = lenet_classifier_model(6)
    generator.summary()
    discriminator_and_classifier_on_generator = generator_containing_discriminator_and_classifier(
        generator, discriminator, classifier)
    d_optim = Adagrad(lr=0.005)
    g_optim = Adagrad(lr=0.005)
    generator.compile(loss='mse', optimizer="rmsprop")
    discriminator_and_classifier_on_generator.compile(
        loss=[generator_l1_loss, discriminator_on_generator_loss, "categorical_crossentropy"],
        optimizer="rmsprop")
    discriminator.trainable = True
    discriminator.compile(loss=discriminator_loss, optimizer="rmsprop")
    classifier.trainable = True
    classifier.compile(loss="categorical_crossentropy", optimizer="rmsprop")

    for epoch in range(100):
        print("Epoch is", epoch)
        print("Number of batches", int(X_train.shape[0] / BATCH_SIZE))
        for index in range(int(X_train.shape[0] / BATCH_SIZE)):
            image_batch = Y_train[index * BATCH_SIZE:(index + 1) * BATCH_SIZE]
            label_batch = LABEL_train[index * BATCH_SIZE:(index + 1) * BATCH_SIZE]  # replace with your data here

            generated_images = generator.predict(X_train[index * BATCH_SIZE:(index + 1) * BATCH_SIZE])
            if index % 20 == 0:
                image = combine_images(generated_images)
                image = image * 127.5 + 127.5
                image = np.swapaxes(image, 0, 2)
                cv2.imwrite(str(epoch) + "_" + str(index) + ".png", image)
                # Image.fromarray(image.astype(np.uint8)).save(str(epoch)+"_"+str(index)+".png")

            # Training D:
            real_pairs = np.concatenate((X_train[index * BATCH_SIZE:(index + 1) * BATCH_SIZE, :, :, :], image_batch),
                                        axis=1)
            fake_pairs = np.concatenate(
                (X_train[index * BATCH_SIZE:(index + 1) * BATCH_SIZE, :, :, :], generated_images), axis=1)
            X = np.concatenate((real_pairs, fake_pairs))
            y = np.zeros((20, 1, 64, 64))  # [1] * BATCH_SIZE + [0] * BATCH_SIZE
            d_loss = discriminator.train_on_batch(X, y)
            print("batch %d d_loss : %f" % (index, d_loss))
            discriminator.trainable = False

            # Training C:
            c_loss = classifier.train_on_batch(image_batch, label_batch)
            print("batch %d c_loss : %f" % (index, c_loss))
            classifier.trainable = False

            # Train G:
            g_loss = discriminator_and_classifier_on_generator.train_on_batch(
                X_train[index * BATCH_SIZE:(index + 1) * BATCH_SIZE, :, :, :], 
                [image_batch, np.ones((10, 1, 64, 64)), label_batch])
            discriminator.trainable = True
            classifier.trainable = True
            print("batch %d g_loss : %f" % (index, g_loss[1]))
            if index % 20 == 0:
                generator.save_weights('generator', True)
                discriminator.save_weights('discriminator', True)

Теоретические детали

Я считаю, что есть некоторые недоразумения относительно того, как работают условные ГАН и какова роль дискриминаторов в таких схемах.

Роль Дискриминатора

В игре min-max, которая является тренировкой GAN [4], дискриминатор D играет против генератора G (сети, на которой вы действительно заботитесь), так что под D контролем G становится лучше выводить реалистичные результаты.

Для этого D обучается отличать реальные образцы от образцов из G; в то время как G обучается дурачить D, генерируя реалистичные результаты/результаты, следующие за целевым распределением.

Примечание: в случае условных GAN, то есть GAN, отображающих входную выборку из одного домена A (например, реального изображения) в другой домен B (например, эскиз), D обычно питается парами образцов, сложенными вместе и должен различать "реальные "пары (входной образец из A + соответствующего целевого образца из B) и" поддельные "пары (входной образец из A + соответствующего выхода из G) [1, 2]

Обучение условного генератора против D (в отличие от простого обучения G только с потерей L1/L2, например, DAE) улучшает возможности выборки G, заставляя его выводить четкие, реалистичные результаты, а не пытаться усреднить распределение.

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

Примечание: вывод D часто не является простым скаляром/булевым. Обычно существует дискриминатор (например, PatchGAN [1, 2]), возвращающий матрицу вероятностей, оценивая, насколько реалистичные исправления сделаны из его ввода.


Условные GAN

Традиционные ГАН обучаются неконтролируемым образом для создания реалистичных данных (например, изображений) из случайного шума в качестве входных данных. [4]

Как упоминалось ранее, условные GAN имеют дополнительные условия ввода. Вдоль/вместо вектора шума они берут для ввода образца из области A и возвращают соответствующий образец из области B A может быть совершенно другой модальности, например B = sketch image тогда как A = discrete label; B = volumetric data то время как A = RGB image и т.д. [3]

Такие GAN также могут быть обусловлены многократными входами, например A = real image + discrete label тогда как B = sketch image. Известная работа по внедрению таких методов - InfoGAN [5]. В нем описывается, как согласовать GAN на нескольких непрерывных или дискретных входах (например, A = digit class + writing type, B = handwritten digit image), используя более продвинутый дискриминатор, который для второй задачи должен заставить G максимизировать взаимную информацию между ее и его соответствующие выходы.


Максимизация взаимной информации для cGANs

У дискриминатора InfoGAN есть 2 главы/подсерии для выполнения двух задач [5]:

  • Одна голова D1 выполняет традиционную реальную/сгенерированную дискриминацию - G должен минимизировать этот результат, т.е. Он должен обмануть D1 чтобы он не мог отличить данные, генерируемые реальной формой;
  • Другая головка D2 (также называемая Q сетью) пытается регрессировать входную информацию A - G должна максимизировать этот результат, то есть она должна выводить данные, которые "показывают" запрошенную семантическую информацию (см. максимизация взаимной информации между G условными входами и его результаты).

Вы можете найти здесь реализацию Keras: https://github.com/eriklindernoren/Keras-GAN/tree/master/infogan.

В нескольких работах используются аналогичные схемы для улучшения контроля над тем, что генерирует GAN, используя предоставленные метки и максимизируя взаимную информацию между этими входами и выходами G [6, 7]. Основная идея всегда одна и та же:

  • Поезд G для генерации элементов домена B, учитывая некоторые входы домена (ов) A;
  • Поезд D для распознавания "реальных"/"поддельных" результатов - G должен свести к минимуму это;
  • Поезд Q (например, классификатор, может делиться слоями с D), чтобы оценить исходные входы A из B выборок - G должен максимизировать это).

Завершение

В вашем случае, кажется, у вас есть следующие данные обучения:

  • реальные изображения Ia
  • соответствующие эскизные изображения Ib
  • соответствующие метки классов c

И вы хотите обучить генератор G так, чтобы при заданном изображении Ia и его метке класса c он выводил изображение эскиза Ib'.

В общем, у вас много информации, и вы можете контролировать свое обучение как на условных изображениях, так и на условных ярлыках... Вдохновленный вышеупомянутыми методами [1, 2, 5, 6, 7], здесь возможный способ использования всей этой информации для обучения вашего условного G:

Сеть G :
  • Входы: Ia + c
  • Выход: Ib'
  • Архитектура: до вас (например, U-Net, ResNet,...)
  • Потери: потери L1/L2 между Ib' & Ib, -D убыток, потеря Q
Сеть D :
  • Входы: Ia + Ib (действительная пара), Ia + Ib' (поддельная пара)
  • Результат: скалярный/матричный скачок
  • Архитектура: до вас (например, PatchGAN)
  • Потеря: кросс-энтропия по оценке "подлости"
Сеть Q :
  • Входы: Ib (реальный образец, для обучения Q), Ib' (поддельный образец, при обратном распространении через G)
  • Результат: c' (оценочный класс)
  • Архитектура: до вас (например, LeNet, ResNet, VGG,...)
  • Потеря: кросс-энтропия между c и c'
Этап обучения:
  1. Поезд D на партию реальных пар Ia + Ib затем на партию поддельных пар Ia + Ib';
  2. Поезд Q на партии реальных образцов Ib;
  3. Исправить D и Q веса;
  4. Поезд G, передавая свои сгенерированные выходы Ib' в D и Q для распространения через них.

Примечание. Это действительно грубое описание архитектуры. Я бы рекомендовал пройти литературу ([1, 5, 6, 7] как хороший старт), чтобы получить более подробную информацию и, возможно, более сложное решение.


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

  1. Изола, Филлип и др. "Преобразование изображения в изображение с условными состязательными сетями". Препринт arXiv (2017). http://openaccess.thecvf.com/content_cvpr_2017/papers/Isola_Image-To-Image_Translation_With_CVPR_2017_paper.pdf
  2. Zhu, Jun-Yan, et al. "Непарный перевод изображения в изображение с использованием согласованных по последовательному сценарию состязательных сетей". arXiv preprint arXiv: 1703.10593 (2017). http://openaccess.thecvf.com/content_ICCV_2017/papers/Zhu_Unpaired_Image-To-Image_Translation_ICCV_2017_paper.pdf
  3. Мирза, Мехди и Симон Осиндеро. "Условные генеративные состязательные сети". arXiv preprint arXiv: 1411.1784 (2014). https://arxiv.org/pdf/1411.1784
  4. Goodfellow, Ian, et al. "Генеративные состязательные сети". Достижения в системах обработки нейронной информации. 2014. http://papers.nips.cc/paper/5423-generative-adversarial-nets.pdf
  5. Chen, Xi, et al. "Infogan: Интерпретируемое представление обучения посредством информации, максимизирующей генеративные состязательные сети". Достижения в нейронных системах обработки информации. 2016. http://papers.nips.cc/paper/6399-infogan-interpretable-representation-learning-by-information-maximizing-generative-adversarial-nets.pdf
  6. Ли, Минхёк и Джунхи Сёк. "Управляемая генерирующая сеть Adversarial". arXiv preprint arXiv: 1708.00598 (2017). https://arxiv.org/pdf/1708.00598.pdf
  7. Одена, Август, Кристофер Ола и Джонатон Шленс. "Синтез условного изображения с помощью вспомогательных классификаторов". arXiv preprint arXiv: 1610.09585 (2016). http://proceedings.mlr.press/v70/odena17a/odena17a.pdf

Ответ 2

Вы должны изменить свою модель дискриминатора, либо иметь два выхода, либо получить выход "n_classes + 1".

Предупреждение: я не вижу в определении вашего дискриминатора вывода "true/false", я вижу, что он выводит изображение...

Где-то он должен содержать GlobalMaxPooling2D или GlobalAveragePooling2D.
В конце и один или несколько слоев Dense для классификации.

Если указано true/false, последний Dense должен иметь 1 единицу.
В противном случае n_classes + 1 единица.

Итак, конец вашего дискриминатора должен быть чем-то вроде

...GlobalMaxPooling2D()...
...Dense(someHidden,...)...
...Dense(n_classes+1,...)...

Теперь дискриминатор будет выводить n_classes плюс знак "true/fake" (вы не сможете использовать "категорический" там) или даже "поддельный класс" (тогда вы ноль других классов и используете категориальный)

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

Вариант 1 - использование знака "истина/подделка". (Не используйте "категориальную_коссентропию")

#true sketches into discriminator:
fakeClass = np.zeros((total_samples,))
sketchClass = originalClasses

targetClassTrue = np.concatenate([fakeClass,sketchClass], axis=-1)

#fake sketches into discriminator:
fakeClass = np.ones((total_fake_sketches))
sketchClass = originalClasses

targetClassFake = np.concatenate([fakeClass,sketchClass], axis=-1)

Вариант 2 - Использование "поддельного класса" (может использовать "категориальная_процессорность"):

#true sketches into discriminator:
fakeClass = np.zeros((total_samples,))
sketchClass = originalClasses

targetClassTrue = np.concatenate([fakeClass,sketchClass], axis=-1)

#fake sketches into discriminator:
fakeClass = np.ones((total_fake_sketches))
sketchClass = np.zeros((total_fake_sketches, n_classes))

targetClassFake = np.concatenate([fakeClass,sketchClass], axis=-1)

Теперь объедините все в один целевой массив (соответствующий эскизам ввода)

Обновленный метод обучения

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

  • discriminator.compile(loss='binary_crossentropy', optimizer=....)
  • discriminator.compile(loss='categorical_crossentropy', optimizer=...)

Код:

for epoch in range(100):
    print("Epoch is", epoch)
    print("Number of batches", int(X_train.shape[0]/BATCH_SIZE))

    for index in range(int(X_train.shape[0]/BATCH_SIZE)):

        #names:
            #images -> initial images, not changed    
            #sketches -> generated + true sketches    
            #classes -> your classification for the images    
            #isGenerated -> the output of your discriminator telling whether the passed sketches are fake

        batchSlice = slice(index*BATCH_SIZE,(index+1)*BATCH_SIZE)
        trueImages = X_train[batchSlice]

        trueSketches = Y_train[batchSlice] 
        trueClasses = originalClasses[batchSlice]
        trueIsGenerated = np.zeros((len(trueImages),)) #discriminator telling whether the sketch is fake or true (generated images = 1)
        trueEndTargets = np.concatenate([trueIsGenerated,trueClasses],axis=1)

        fakeSketches = generator.predict(trueImages)
        fakeClasses = originalClasses[batchSlize]             #if option 1 -> telling class + isGenerated - use "binary_crossentropy"
        fakeClasses = np.zeros((len(fakeSketches),n_classes)) #if option 2 -> telling if generated is an individual class - use "categorical_crossentropy"    
        fakeIsGenerated = np.ones((len(fakeSketches),))
        fakeEndTargets = np.concatenate([fakeIsGenerated, fakeClasses], axis=1)

        allSketches = np.concatenate([trueSketches,fakeSketches],axis=0)            
        allEndTargets = np.concatenate([trueEndTargets,fakeEndTargets],axis=0)

        d_loss = discriminator.train_on_batch(allSketches, allEndTargets)

        pred_temp = discriminator.predict(allSketches)
        #print(np.shape(pred_temp))
        print("batch %d d_loss : %f" % (index, d_loss))

        ##WARNING## In previous keras versions, "trainable" only takes effect if you compile the models. 
            #you should have the "discriminator" and the "discriminator_on_generator" with these set at the creation of the models and never change it again   

        discriminator.trainable = False
        g_loss = discriminator_on_generator.train_on_batch(trueImages, trueEndTargets)
        discriminator.trainable = True


        print("batch %d g_loss : %f" % (index, g_loss[1]))
        if index % 20 == 0:
            generator.save_weights('generator', True)
            discriminator.save_weights('discriminator', True)

Правильная компиляция моделей

Когда вы создаете "дискриминатор" и "дискриминатор_он_генератора":

discriminator.trainable = True
for l in discriminator.layers:
    l.trainable = True


discriminator.compile(.....)

for l in discriminator_on_generator.layer[firstDiscriminatorLayer:]:
    l.trainable = False

discriminator_on_generator.compile(....)