Список работ

Порождающие состязательные нейронные сети на основе многослойного перцептрона

Содержание

Введение

Рассматриваемая порождающая состязательная нейронная сеть обучается на 30'000 эпохах для генерации рукописных цифр. Обучение выполняется на обучающем множестве (x_train) набора данных MNIST. Загрузка x_train выполняется из ранее сформированного бинарного файла [1].
В процессе обучения через каждые 3'000 эпох сохраняется png-файл размера 640*480 пикселей с 25-ю изображениями. Это обеспечивает следующая процедура:

def save_sample_images(latent_dim, generator, epoch):
    r, c = 5, 5 # Выводим и сохраняем 25 изображений
    # latent_dim - размер шума, подаваемого на вход генератора
    noise = np.random.normal(0, 1, (r * c, latent_dim))
    gen_imgs = generator.predict(noise)
    # Возвращаемся к диапазону [0, 1]
    gen_imgs = 0.5 * gen_imgs + 0.5
    fig, axs = plt.subplots(r, c)
    cnt = 0
    for i in range(r):
        for j in range(c):
            axs[i,j].imshow(gen_imgs[cnt, :, :, 0], cmap = 'gray')
            axs[i,j].axis('off')
            cnt += 1
    fig.savefig('images/%d.png' % epoch) # Сохраняем в папку images
    plt.close()

Обученный генератор (generator) после завершения последней эпохи (epochs) сохраняется файл:

file_gen = 'generator_model_%03d.h5' % epochs
generator.save(file_gen)

Истории обучения дискриминатора и генератора запоминаются в списках d_loss, d_acc и g_loss, хранящих соответственно потери и точность дискриминатора и потери генератора. Данные добавляются в эти списки с интервалом в 100 эпох. Вывод историй обучения обеспечивает следующий код (путь pathToData задается ранее):

# Вывод историй обучения в файлы
fn_d_loss, fn_d_acc, fn_g_loss = 'd_loss.txt', 'd_acc.txt', 'g_loss.txt'
print('Истории сохранены в файлы:\n' + fn_d_loss + '\n' + fn_d_acc + '\n' + fn_g_loss)
print('Путь:', pathToData)
with open(pathToData + fn_d_loss, 'w') as output:
    for val in d_loss: output.write(str(val) + '\n')
with open(pathToData + fn_d_acc, 'w') as output:
    for val in d_acc: output.write(str(val) + '\n')
with open(pathToData + fn_g_loss, 'w') as output:
    for val in g_loss: output.write(str(val) + '\n')

Потери, зафиксированные в историях обучения, отображаются в виде графиков (рис. 1.)

Графики потерь

Рис. 1. Графики потерь генератора и дискриминатора

Чтение файлов и вывод графиков потерь обеспечивает следующий код:

import numpy as np
import matplotlib.pyplot as plt
pathToHistory = 'G:\\AM\\GAN\\images\\'
fn_g_loss = pathToHistory + 'g_loss.txt'
fn_d_loss = pathToHistory + 'd_loss.txt'
#
def readFile(fn): # Читает заданный файл, занося данные в список
    # В файле fn потери или точность после каждой эпохи
    with open(fn, 'r') as f: loss_acc = f.readlines()
    # Удаляем последний элемент, если он пустой
    if loss_acc[len(loss_acc) - 1] == '': del loss_acc[-1]
    loss_acc = np.array(loss_acc, dtype = 'float32')
    return loss_acc
#
g_loss = readFile(fn_g_loss)
d_loss = readFile(fn_d_loss)
plt.figure(figsize = (7, 3.7))
plt.title('Потери генератора и дискриминатора')
lb, lb2 = 'Потери генератора', 'Потери дискриминатора'
yMin = 0
yMax = 1.05 * max(max(g_loss), max(d_loss))
plt.plot(g_loss, color = 'r', label = lb, linestyle = '--')
plt.plot(d_loss, color = 'g', label = lb2)
plt.ylabel('Потери')
plt.xlabel('Эпоха / 100')
plt.ylim([0.95 * yMin, yMax])
plt.legend()
plt.show()

Сохраненная модель генератора может быть впоследствии загружена и использована для генерации изображений рукописных цифр:

import numpy as np
from keras.models import load_model
from matplotlib import pyplot as plt
latent_dim = 100
pathToData = 'G:/AM/GAN/images/'
generator = load_model(pathToData + 'generator_model_30001.h5')
r, c = 3, 10 # Выводим 30 изображений
noise = np.random.normal(0, 1, (r * c, latent_dim))
gen_imgs = generator.predict(noise)
# Возвращаемся к диапазону [0, 1]
gen_imgs = 0.5 * gen_imgs + 0.5
plt.figure(figsize = (7, 3))
for cnt in range(r * c):
    plt.subplot(r, c, cnt + 1)
    plt.imshow(gen_imgs[cnt, :, :, 0], cmap = 'gray')
    plt.axis('off')
plt.show()

Возможный результат показан на рис. 2.

Генерируемые изображения

Рис. 2. Изображения, генерируемые обученным генератором

Описание порождающих состязательных нейронных сетей

Порождающие состязательные нейронные сети (Generative Adversarial Networks) были предложены в 2014 г. аспирантом Иэн Гудфеллоу (руководитель Йошуа Бенжи) [2].
Порождающая модель содержит две нейронные сети – генератор и дискриминатор:

На вход генератора подается шум – массив noise формы (batch_size, latent_dim) (в приводимой ниже программе batch_size = 32, latent_dim = 100). Шум формируется на основе нормального распределения:

import numpy as np ... noise = np.random.normal(0, 1, (batch_size, latent_dim))

На выходе генератора тензор (массив) формы (batch_size, 28, 28, 1), то есть batch_size изображений размера 28*28 пикселей, выполненных в оттенках серого цвета.
Цель дискриминатора – научиться надежно отличать порожденные генератором (поддельные) примеры от настоящих, то есть дискриминатор решает задачу бинарной классификации: по заданному примеру решить, это настоящий пример или пример, порожденный генератором.
Цель генератора – научиться порождать примеры с распределением pgen, таким, что дискриминатор не смог бы отличить pgen от распределения данных pdata, которые наряду с поддельными используются для обучения дискриминатора.
Таким образом, генератор пытается научиться порождать правильные примеры, а дискриминатор – отличать порожденные примеры от настоящих. По мере обучения генератор и дискриминатор, состязаясь, постепенно улучшают друг друга.
Обучение дискриминатора ведется на примерах двух видов – поддельных, порожденных генератором, и настоящих, взятых из предварительно сформированного набора данных, например, MNIST. (Используя MNIST, порождающая модель учится генерировать рукописные цифры.)
Возможная схема обучения порождающей состязательной нейронной сети показана на рис. 3.

GANs

Рис. 3. Схема обучения порождающей состязательной нейронной сети

Дискриминатор обучается, принимая как поддельные, так и настоящие, реальные, примеры. Все примеры снабжаются меткой: поддельные примеры получают метку 0, а настоящие – 1. Обучение дискриминатора можно вести раздельно, то есть сначала на настоящих примерах, а затем на поддельных (или наоборот). Так же можно перемешать поддельные и настоящие примеры и затем выполнить один этап обучения. Обучение дискриминатора выполняется на пакете размера batch_size = 32 с использованием метода train_on_batch.
Для обучения генератора создается обобщенная модель, в которой прежде определяется генератор, а вслед дискриминатор. В этой модели веса дискриминатора делаются необучаемыми. При обучении используются примеры, порожденные генератором, то есть только поддельные. Все они при обучении генератора снабжаются меткой 1. Так же как и при обучении дискриминатора, используется метод train_on_batch. Ошибка, полученная на выходе обобщенной модели (то есть на выходе дискриминатора), используется для пересчета весов генератора (веса дискриминатора заморожены).
Дискриминатор и обобщенная модель (генератор) обучаются попеременно. В процессе обучения методом градиентного спуска минимизируются функции потерь, заданные для дискриминатора и обобщенной модели.
При обучении дискриминатора данные, поступающие на его вход от генератора, снабжаются меткой 0. А при обучении генератора (обобщенной модели) – меткой 1. Это чередование значений меток обеспечивает состязание между дискриминатором и генератором: дискриминатор пытается научиться отличать поддельные примеры от настоящих, а генератор – порождать такие примеры, которые дискриминатор не сможет отличать от настоящих.
Поскольку дискриминатор выполняет бинарную классификацию, то логично в обеих моделях использовать в качестве функции потерь бинарную перекрестную энтропию: keras.losses.binary_crossentropy. Впрочем, функция потерь может быть иной.
В приводимой далее программе порождающей состязательной нейронной сети и генератор, и дискриминатор конструируются из полносвязных слоев (Dense). Обучение ведется на протяжении 30'001 эпохи. В процессе обучения каждые 100 эпох пополняется истории обучения – списки d_loss, d_acc и g_loss, которые после завершения обучения записываются в текстовые файлы. Каждые 3000 эпох запускается генератор и в папку images сохраняются сгенерированные им 25 изображений. Все изображения размещаются на одном рисунке.
Описания слоев генератора, дискриминатора и структуры обобщенной модели приведены в табл. 1-3.

Таблица 1. Описание слоев генератора

Идентификатор слоя (тип слоя)Форма выхода слояЧисло параметров
input_1 (InputLayer)(None, 100)0
dense_1 (Dense)(None, 256)25'856
leaky_re_lu_1 (LeakyReLU)(None, 256)0
batch_normalization_1 (BatchNormalization)(None, 256)1'024
dense_2 (Dense)(None, 512)131'584
leaky_re_lu_2 (LeakyReLU)(None, 512)0
batch_normalization_2 (BatchNormalization)(None, 512)2'048
dense_3 (Dense)(None, 1024)525'312
leaky_re_lu_3 (LeakyReLU)(None, 1024)0
batch_normalization_3 (BatchNormalization)(None, 1024)4'096
dense_4 (Dense)(None, 784)803'600
reshape_1 (Reshape)(None, 28, 28, 1)0
Всего параметров: 1'493'520
Обучаемых параметров: 1'489'936
Необучаемых параметров: 3'584

На выходе слоя Reshape, завершающего модель генератора, имеем сгенерированные этой моделью изображения размера 28*28 пикселей.

Таблица 2. Описание слоев дискриминатора

Идентификатор слоя (тип слоя)Форма выхода слояЧисло параметров
input_1 (InputLayer)(None, 28, 28, 1)0
flatten_1 (Flatten)(None, 784)0
dense_5 (Dense)(None, 512)40'1920
leaky_re_lu_4 (LeakyReLU)(None, 512)0
dense_6 (Dense)(None, 512)262'656
leaky_re_lu_5 (LeakyReLU)(None, 512)0
dense_7 (Dense)(None, 1)513
Всего параметров: 665'089
Обучаемых параметров: 665'089
Необучаемых параметров: 0

Таблица 3. Описание структуры обобщенной модели

Идентификатор слоя (тип слоя)Форма выхода слояЧисло параметров
input_3 (InputLayer) (None, 100)0
model_1 (Model) (None, 28, 28, 1)1'493'520
model_2 (Model) (None, 1)665'089
Всего параметров: 2'158'609
Обучаемых параметров: 1'489'936
Необучаемых параметров: 668'673

Обучение порождающей состязательной нейронной сети

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

from keras.layers import Input, Dense, Reshape, Flatten
from keras.layers import BatchNormalization
from keras.layers.advanced_activations import LeakyReLU
from keras.models import Model
from keras.optimizers import Adam
import matplotlib.pyplot as plt
import numpy as np
import sys
#
# Создает модель генератора
def build_generator(img_shape, latent_dim):
    inp = Input(shape = (latent_dim,))
    x = Dense(256)(inp)
    x = LeakyReLU(alpha = 0.2)(x)
    x = BatchNormalization(momentum = 0.8)(x)
    x = Dense(512)(x)
    x = LeakyReLU(alpha = 0.2)(x)
    x = BatchNormalization(momentum = 0.8)(x)
    x = Dense(1024)(x)
    x = LeakyReLU(alpha = 0.2)(x)
    x = BatchNormalization(momentum = 0.8)(x)
    x = Dense(np.prod(img_shape), activation = 'tanh')(x)
    img = Reshape(img_shape)(x)
    generator = Model(inp, img)
    print('Модель генератора')
    generator.summary()
    return generator
#
# Создает модель дискриминатора
def build_discriminator(img_shape, loss, optimizer):
    img = Input(shape = img_shape)
    x = Flatten()(img)
    x = Dense(512)(x)
    x = LeakyReLU(alpha = 0.2)(x)
    x = Dense(512)(x)
    x = LeakyReLU(alpha = 0.2)(x)
    output = Dense(1, activation = 'sigmoid')(x)
    discriminator = Model(img, output)
    print('Модель дискриминатора')
    discriminator.summary()
    discriminator.compile(loss = loss, optimizer = optimizer, metrics = ['accuracy'])
    return discriminator
#
def loadTrainFromBinData(pathToData, img_rows, img_cols, num_classes, show_img):
    print('Загрузка данных из двоичных файлов...')
    with open(pathToData + 'imagesTrain.bin', 'rb') as read_binary:
        x_train = np.fromfile(read_binary, dtype = np.uint8)
    x_train_shape = int(x_train.shape[0] / (img_rows * img_cols)) # 60000
    x_train = x_train.reshape(x_train_shape, img_rows, img_cols, 1)
    if show_img:
        with open(pathToData + 'labelsTrain.bin', 'rb') as read_binary:
            y_train = np.fromfile(read_binary, dtype = np.uint8)
        print('Показываем примеры обучающих данных')
        # Выводим 9 изображений обучающего набора
        names = []
        for i in range(10): names.append(chr(48 + i)) # ['0', '1', '2', ..., '9']
        for i in range(9):
            plt.subplot(3, 3, i + 1)
            img = x_train[i]
            img_label = y_train[i]
            img = img[:, :, 0]
            plt.imshow(img, cmap = plt.get_cmap('gray'))
            plt.title(names[img_label])
            plt.axis('off')
        plt.subplots_adjust(hspace = 0.5) # wspace
        plt.show()
        sys.exit()
    # Преобразование целочисленных данных в float32 и приведение к диапазону [0.0, 1.0]
    x_train = np.asarray(x_train, dtype = 'float32') / 255
    return x_train
#
def train(discriminator, generator, combined, epochs, batch_size,
          sample_interval, latent_dim, pathToHistory, x_train = None):
    if x_train is None:
        # Загрузка MNIST
        (x_train, _), (_, _) = mnist.load_data()
        # Приводим к диапазону [-1, 1]; activation = 'sigmoid' & activation = 'tanh'
        x_train = x_train / 127.5 - 1.
        # Добавляем измерение
        x_train = np.expand_dims(x_train, axis = 3)
    else:
        # Приводим к диапазону [-1, 1]; activation = 'sigmoid' & activation = 'tanh'
        x_train = 2.0 * x_train - 1.0
    # Метки истинных и ложных изображений
    valid = np.ones((batch_size, 1))
    fake = np.zeros((batch_size, 1))
    d_loss = []
    d_acc = []
    g_loss = []
    for epoch in range(epochs):
        # Обучаем дискриминатор
        # Выбираем batch_size случайных изображений из обучающего множества
        # Формируем массив из batch_size целых чисел (индексов) из диапазона [0, x_train.shape[0]]
        idx = np.random.randint(0, x_train.shape[0], batch_size)
        # idx: [27011 19867 1049 10487 30340 12711 24354 3040 ...]
        # Формируем массив imgs из batch_size изображений
        imgs = x_train[idx]
        # Шум (массив), подаваемый на вход генератора
        noise = np.random.normal(0, 1, (batch_size, latent_dim)) # numpy.ndarray: shape = (batch_size, latent_dim)
        # Генерируем batch_size изображений
        gen_imgs = generator.predict(noise) # numpy.ndarray: shape = (batch_size, 28, 28, 1)
        # Обучаем дискриминатор, подавая ему сначала настоящие, а затем поддельные изображения
        d_hist_real = discriminator.train_on_batch(imgs, valid) # Результат: list: [0.3801252, 0.96875]
        d_hist_fake = discriminator.train_on_batch(gen_imgs, fake) # Результат: list: [0.75470906, 0.1875]
        # Усредняем результаты и получаем средние потери и точность
        d_hist = 0.5 * np.add(d_hist_real, d_hist_fake) # numpy.ndarray: [0.56741714 0.578125]
        #
        # Обучение обобщенной модели. Реально обучается только генератор
        noise = np.random.normal(0, 1, (batch_size, latent_dim)) # numpy.ndarray: shape = (batch_size, latent_dim)
        # Обучение генератора. Метки изображений valid (единицы),
        # то есть изображения, порожденные генератором при его обучении, считаются истинными
        g_ls = combined.train_on_batch(noise, valid) # numpy.float32: 0.6742059
        if epoch % 100 == 0:
            d_loss.append(d_hist[0])
            d_acc.append(d_hist[1])
            g_loss.append(g_ls)
        # Потери и точность дискриминатора и потери генератора
        if epoch % (sample_interval / 10) == 0:
            print("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" % (epoch, d_hist[0], 100 * d_hist[1], g_ls))
        # Генерируем и сохраняем рисунок с 25-ю изображениями
        if epoch % sample_interval == 0:
            save_sample_images(latent_dim, generator, epoch)
    # Сохраняем обученный генератор в файл
    file_gen = pathToHistory + 'generator_model_%03d.h5' % epochs
    generator.save(file_gen)
    print('Модель генератора сохранена в файл', file_gen)
    #
    # Вывод историй обучения в файлы
    fn_d_loss, fn_d_acc, fn_g_loss = 'd_loss.txt', 'd_acc.txt', 'g_loss.txt'
    print('Истории сохранены в файлы:\n' + fn_d_loss + '\n' + fn_d_acc + '\n' + fn_g_loss)
    print('Путь:', pathToHistory)
    with open(pathToHistory + fn_d_loss, 'w') as output:
        for val in d_loss: output.write(str(val) + '\n')
    with open(pathToHistory + fn_d_acc, 'w') as output:
        for val in d_acc: output.write(str(val) + '\n')
    with open(pathToHistory + fn_g_loss, 'w') as output:
        for val in g_loss: output.write(str(val) + '\n')
    # Вывод графиков историй обучения
    yMax = max(g_loss)
    cnt = len(g_loss)
    rng = np.arange(cnt)
    fig, ax = plt.subplots(figsize = (7, 4))
    ax.scatter(rng, d_loss, marker = 'o', c = 'blue', edgecolor = 'black')
    ax.scatter(rng, g_loss, marker = 'x', c = 'red')
    ax.set_title('Потери генератора (x) и дискриминатора (o)')
    ax.set_ylabel('Потери')
    ax.set_xlabel('Эпоха / 100')
    ax.set_xlim([-0.5, cnt])
    ax.set_ylim([0, 1.1 * yMax])
    fig.show()
#
def save_sample_images(latent_dim, generator, epoch):
    r, c = 5, 5 # Выводим и сохраняем 25 изображений
    # latent_dim - размер шума, подаваемого на вход генератора
    noise = np.random.normal(0, 1, (r * c, latent_dim))
    gen_imgs = generator.predict(noise)
    # Возвращаемся к диапазону [0, 1]
    gen_imgs = 0.5 * gen_imgs + 0.5
    fig, axs = plt.subplots(r, c)
    cnt = 0
    for i in range(r):
        for j in range(c):
            axs[i, j].imshow(gen_imgs[cnt, :, :, 0], cmap = 'gray')
            axs[i, j].axis('off')
            cnt += 1
    # Сохраняем в папку images
    fig.savefig(pathToHistory + '%d.png' % epoch)
    plt.close()
#
pathToData = 'G:/AM/Лекции/mnist_data/'
pathToHistory = 'G:/AM/GAN/images/'
img_rows = 28
img_cols = 28
channels = 1
num_classes = 10
img_shape = (img_rows, img_cols, channels)
optimizer = Adam(0.0002, 0.5)
loss = 'binary_crossentropy'
loss_g = 'binary_crossentropy' # 'mse', 'poisson', 'binary_crossentropy'
# latent_dim - размер шума, подаваемого на вход генератора
# Шум - это вектор, формируемый на базе нормального распределения
# Число формируемых векторов равно batch_size
# Шум можно рассматривать как изображение размера 10*10
latent_dim = 100
epochs = 30001 # Число эпох обучения (30001)
batch_size = 32 # Размер пакета обучения (число генерируемых изображений)
sample_interval = 3000 # Интервал между сохранением сгенерированных изображений в файл
show_img = False
#
# Построение генератора
generator = build_generator(img_shape, latent_dim)
# Построение и компиляция дискриминатора
discriminator = build_discriminator(img_shape, loss, optimizer)
#
# Обобщенная модель
# Генератор принимает шум и возвращает (генерирует) изображения
# (их количество равно размеру пакета обучения batch_size)
inp = Input(shape = (latent_dim,))
img = generator(inp)
# В объединенной модели обучаем только генератор
discriminator.trainable = False
# Дискриминатор принимает сгенерированное изображение
# и классифицирует его либо как истинное, либо как поддельное, возвращая validity
# output = 1, если дискриминатор посчитает, что изображение истинное, или 0 - в противном случае
output = discriminator(img) # <class 'tensorflow.python.framework.ops.Tensor'>: shape = (?, 1)
# Объединенная модель - стек генератора и дискриминатора
combined = Model(inp, output)
# Поскольку метрика не задана, то после каждой эпохи вычисляются только потери
combined.compile(loss = loss_g, optimizer = optimizer)
print('Обобщеная модель')
combined.summary()
#
x_train = loadTrainFromBinData(pathToData, img_rows, img_cols, num_classes, show_img)
#
print('Обучение. Интервал между выводом изображений', sample_interval)
train(discriminator, generator, combined, epochs,
      batch_size, sample_interval, latent_dim, pathToHistory, x_train = x_train)

Заключение

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

Источники

  1. Классификация изображений нейронной сетью. Загрузка MNIST и EMNIST из бинарных файлов. [Электронный ресурс] URL: http://100byte.ru/python/imgClasses/imgClasses.html#p2_3 (дата обращения: 01.11.2019).
  2. Generative Adversarial Networks / I. J. Goodfellow et al. // ArXiv e-prints, 2014.
  3. Keras Adversarial Models. [Электронный ресурс] URL: https://pythonhosted.org/keras-adversarial/ (дата обращения: 01.11.2019).

Список работ

Рейтинг@Mail.ru