Creando arte usando DCGAN en Keras

Buen dia Hace seis meses, comencé a estudiar aprendizaje automático, realicé un par de cursos y obtuve algo de experiencia con esto. Luego, al ver todo tipo de noticias sobre qué redes neuronales son geniales y pueden hacer mucho, decidí tratar de estudiarlas. Comencé a leer el libro de Nikolenko sobre el aprendizaje profundo y durante la lectura tuve varias ideas (que no son nuevas para el mundo, pero fueron de gran interés para mí), una de las cuales es crear una red neuronal que generaría arte para mí que parecería genial no solo para mí, el "padre del niño dibujante", pero también para otras personas. En este artículo trataré de describir el camino que recorrí para obtener los primeros resultados que me satisfagan.


Recolección de datos


Cuando leí el capítulo sobre redes competitivas, me di cuenta de que ahora puedo escribir algo.
Una de las primeras tareas fue escribir un analizador de páginas web para recopilar el conjunto de datos. Para esto, el sitio web de wikiart fue perfecto , tiene una gran cantidad de pinturas y todas están recopiladas por estilo. Este fue mi primer analizador, así que lo escribí durante 4-5 días, los primeros 3 tomaron un camino completamente equivocado. La forma correcta era ir a la pestaña de red en el código fuente de la página y rastrear cómo aparecen las imágenes al hacer clic en el botón "más". En realidad, para los mismos principiantes como yo, será bueno mostrar el código.


from scipy.misc import imresize, imsave from matplotlib.image import imread import requests import json from bs4 import BeautifulSoup from itertools import count import os import glob 

En la primera celda de Júpiter, importé las bibliotecas necesarias.


  • glob: algo útil para obtener una lista de archivos en un directorio
  • solicitudes, BeautifulSoup - Bigote para analizar
  • json: una biblioteca para obtener un diccionario que vuelve cuando hace clic en el botón "más" en un sitio
  • cambiar el tamaño, guardar, leer: para leer imágenes y prepararlas.

 def get_page(style, pagenum): page = requests.get(url1 + style + url2 + str(pagenum) + url3) return page def make_soup(page): soup = BeautifulSoup(page.text, 'html5lib') return soup def make_dir(name, s): path = os.getcwd() + '/' + s + '/' + name os.mkdir(path) 

Describo las funciones para una operación conveniente.


El primero: obtiene una página en forma de texto, el segundo hace que este texto sea más conveniente para el trabajo. Bueno, el tercero es crear las carpetas necesarias por estilo.


 styles = ['kubizm'] url1 = 'https://www.wikiart.org/ru/paintings-by-style/' url2 = '?select=featured&json=2&layout=new&page=' url3 = '&resultType=masonry' 

En la matriz de estilos, por diseño, debería haber varios estilos, pero sucedió que los descargué de manera completamente desigual.


 for style in styles: make_dir(style, 'images') for style in styles: make_dir(style, 'new256_images') 

Crea las carpetas necesarias. El segundo ciclo crea carpetas en las que se almacenará la imagen, aplanada en un cuadrado de 256x256.


(Al principio pensé en no normalizar de alguna manera el tamaño de las imágenes para que no hubiera distorsiones, pero me di cuenta de que esto era imposible o demasiado difícil para mí)


 for style in styles: path = os.getcwd() + '\\images\\' + style + '\\' images = [] names = [] titles = [] for pagenum in count(start=1): page = get_page(style, pagenum) if page.text[0]!='{': break jsons = json.loads(page.text) paintings = jsons['Paintings'] if paintings is None: break for item in paintings: images_temp = [] images_dict = item['images'] if images_dict is None: images_temp.append(item['image']) names.append(item['artistName']) titles.append(item['title']) else: for inner_item in item['images']: images_temp.append(inner_item['image']) names.append(item['artistName']) titles.append(item['title']) images.append(images_temp) for char in ['/','\\','"', '?', ':','*','|','<','>']: titles = [title.replace(char, ' ') for title in titles] for listimg, name, title in zip(images, names, titles): if len(name) > 30: name = name[:25] if len(title) > 50: title = title[:50] if len(listimg) == 1: response = requests.get(listimg[0]) if response.status_code == 200: with open(path + name + ' ' + title + '.png', 'wb') as f: f.write(response.content) else: print('Error from server') else: for i, img in enumerate(listimg): response = requests.get(img) if response.status_code == 200: with open(path + name + ' ' + title + str(i) + '.png', 'wb') as f: f.write(response.content) else: print('Error from server') 

Aquí las imágenes se descargan y guardan en la carpeta deseada. Aquí las imágenes no cambian de tamaño, se guardan los originales.


Suceden cosas interesantes en el primer bucle anidado:


Decidí preguntarle estúpidamente constantemente a json (json es el diccionario que devuelve el servidor cuando hace clic en el botón "Más". El diccionario contiene toda la información sobre las imágenes), y me detengo cuando el servidor devuelve algo borroso y no como valores típicos . En este caso, el primer carácter del texto devuelto debería haber sido una llave de apertura, después de lo cual viene el cuerpo del diccionario.


También se ha notado que el servidor puede devolver algo como un álbum de imágenes. Eso es esencialmente una serie de pinturas. Al principio pensé que volvían pinturas individuales, el nombre de los artistas, o tal vez de una vez con un nombre del artista se da una serie de pinturas.


  for style in styles: directory = os.getcwd() + '\\images\\' + style + '\\' new_dir = os.getcwd() + '\\new256_images\\' + style + '\\' filepaths = [] for dir_, _, files in os.walk(directory): for fileName in files: #relDir = os.path.relpath(dir_, directory) #relFile = os.path.join(relDir, fileName) relFile = fileName #print(directory) #print(relFile) filepaths.append(relFile) #print(filepaths[-1]) print(filepaths[0]) for i, fp in enumerate(filepaths): img = imread(directory + fp, 0) #/ 255.0 img = imresize(img, (256, 256)) imsave(new_dir + str(i) + ".png", img) 

Aquí las imágenes se redimensionan y se guardan en la carpeta preparada para ellas.


Bueno, el conjunto de datos está ensamblado, ¡puedes proceder a lo más interesante!


Comenzando pequeño



Además, después de leer el artículo original, ¡comencé a crear! Pero cuál fue mi decepción cuando no salió nada bueno. En estos intentos, entrené a la red en el mismo estilo de imágenes, pero incluso eso no funcionó, así que decidí comenzar a aprender cómo generar números a partir del multiplicador. No me detendré aquí en detalle, solo hablaré sobre la arquitectura y el punto de inflexión, gracias a lo cual se comenzaron a generar números.


 def build_generator(): model = Sequential() model.add(Dense(128 * 7 * 7, input_dim = latent_dim)) model.add(BatchNormalization()) model.add(LeakyReLU()) model.add(Reshape((7, 7, 128))) model.add(Conv2DTranspose(64, filter_size, strides=(2,2), padding='same')) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU()) model.add(Conv2DTranspose(32, filter_size, strides=(1, 1), padding='same')) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU()) model.add(Conv2DTranspose(img_channels, filter_size, strides=(2,2), padding='same')) model.add(Activation("tanh")) model.summary() return model 

  • latent_dim: una matriz de 100 números generados aleatoriamente.


     def build_discriminator(): model = Sequential() model.add(Conv2D(64, kernel_size=filter_size, strides = (2,2), input_shape=img_shape, padding="same")) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.25)) model.add(Conv2D(128, kernel_size=filter_size, strides = (2,2), padding="same")) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.25)) model.add(Conv2D(128, kernel_size=filter_size, strides = (2,2), padding="same")) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.25)) model.add(Flatten()) model.add(Dense(1)) model.add(Activation('sigmoid')) model.summary() return model 

    Es decir, por el total, los tamaños de salida de las capas convolucionales y el número de capas son generalmente menores que en el artículo original. 28x28 porque genero, no interiores!



Bueno, el verdadero truco debido al cual todo funcionó, en la iteración uniforme del entrenamiento, el discriminador miró las imágenes generadas, y en la iteración impar, las reales.


Eso es básicamente todo. Un DCGAN similar aprendió muy rápidamente, por ejemplo, la imagen al comienzo de este subtema se obtuvo en la era 19,



Estos ya son confiados, pero a veces no son números reales, resultaron en la 99a era de la educación.


Satisfecho con el resultado preliminar, dejé de aprender y comencé a pensar en cómo resolver el problema principal.


Red creativa de confrontación


El siguiente paso fue leer acerca de la GAN con etiquetas: la clase de la imagen actual se sirve al discriminador y al generador. Y después del gan con etiquetas, descubrí CAN - la decodificación es básicamente en nombre del subtema.


En CAN, el discriminador intenta adivinar la clase de la imagen si la imagen es de un conjunto real. Y, en consecuencia, en el caso del entrenamiento en una imagen real, el discriminador, además del predeterminado, recibe un error al adivinar la clase como un error.


Al entrenar en una imagen generada, el discriminador solo necesita predecir si esta imagen es real o no.


El generador, además, solo para engañar al discriminador, necesita hacer que el discriminador se pierda al adivinar la clase de la imagen, es decir, el generador estará interesado en el hecho de que las salidas a los discriminadores están lo más lejos posible de 1, plena confianza.


En cuanto a CAN, nuevamente experimenté dificultades, desmoralización debido al hecho de que nada funciona y no aprende. Después de varias fallas desagradables, decidí comenzar de nuevo y guardar todos los cambios (sí, no lo hice antes), pesos y arquitectura (para interrumpir el entrenamiento).


Primero, quería hacer una red que generara una sola imagen de 256x256 para mí (todas las siguientes imágenes de este tamaño) sin ninguna etiqueta. El punto de inflexión aquí fue que, por el contrario, en cada iteración del entrenamiento, el discriminador debería observar las imágenes generadas y las imágenes reales.



Este es el resultado en el que me detuve y pasé al siguiente paso. Sí, los colores son diferentes de la imagen real, pero estaba más interesado en la capacidad de la red para resaltar contornos y objetos. Ella hizo frente a esto.


Entonces podríamos proceder a la tarea principal: generar arte. Presente de inmediato el código, comentando en el camino.


Primero, como siempre, debe importar todas las bibliotecas.


 import glob from PIL import Image from keras.preprocessing.image import array_to_img, img_to_array, load_img from datetime import date from datetime import datetime import tensorflow as tf import numpy as np import argparse import math import os from matplotlib.image import imread from scipy.misc.pilutil import imresize, imsave import matplotlib.pyplot as plt import cv2 import keras from keras.models import Sequential, Model from keras.layers import Dense, Activation, Reshape, Flatten, Dropout, Input from keras.layers.convolutional import Conv2D, Conv2DTranspose, MaxPooling2D from keras.layers.normalization import BatchNormalization from keras.layers.advanced_activations import LeakyReLU from keras.optimizers import Adam, SGD from keras.datasets import mnist from keras import initializers import numpy as np import random 

Creando un generador.


La salida de las capas es nuevamente diferente del artículo. En algún lugar para ahorrar memoria (Condiciones: una computadora doméstica con gtx970), y en algún lugar debido al éxito con la configuración


 def build_generator(): model = Sequential() model.add(Dense(128 * 16 * 8, input_dim = latent_dim)) model.add(BatchNormalization()) model.add(LeakyReLU()) model.add(Reshape((8, 8, 256))) model.add(Conv2DTranspose(512, filter_size_g, strides=(1,1), padding='same')) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU()) model.add(Conv2DTranspose(512, filter_size_g, strides=(1,1), padding='same')) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU()) model.add(Conv2DTranspose(256, filter_size_g, strides=(1,1), padding='same')) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU()) model.add(Conv2DTranspose(128, filter_size_g, strides=(2,2), padding='same')) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU()) model.add(Conv2DTranspose(64, filter_size_g, strides=(2,2), padding='same')) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU()) model.add(Conv2DTranspose(32, filter_size_g, strides=(2,2), padding='same')) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU()) model.add(Conv2DTranspose(16, filter_size_g, strides=(2,2), padding='same')) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU()) model.add(Conv2DTranspose(8, filter_size_g, strides=(2,2), padding='same')) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU()) model.add(Conv2DTranspose(img_channels, filter_size_g, strides=(1,1), padding='same')) model.add(Activation("tanh")) model.summary() return model 

La función de creación de discriminador devuelve dos modelos, uno de los cuales intenta averiguar si la imagen es real y el otro intenta averiguar la clase de la imagen.


 def build_discriminator(num_classes): model = Sequential() model.add(Conv2D(64, kernel_size=filter_size_d, strides = (2,2), input_shape=img_shape, padding="same")) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.25)) model.add(Conv2D(128, kernel_size=filter_size_d, strides = (2,2), padding="same")) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.25)) model.add(Conv2D(256, kernel_size=filter_size_d, strides = (2,2), padding="same")) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.25)) model.add(Conv2D(512, kernel_size=filter_size_d, strides = (2,2), padding="same")) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.25)) model.add(Conv2D(512, kernel_size=filter_size_d, strides = (2,2), padding="same")) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.25)) model.add(Conv2D(512, kernel_size=filter_size_d, strides = (2,2), padding="same")) model.add(BatchNormalization(momentum=0.8)) model.add(LeakyReLU(alpha=0.2)) model.add(Dropout(0.25)) model.add(Flatten()) model.summary() img = Input(shape=img_shape) features = model(img) validity = Dense(1)(features) valid = Activation('sigmoid')(validity) label1 = Dense(1024)(features) lrelu1 = LeakyReLU(alpha=0.2)(label1) label2 = Dense(512)(label1) lrelu2 = LeakyReLU(alpha=0.2)(label2) label3 = Dense(num_classes)(label2) label = Activation('softmax')(label3) return Model(img, valid), Model(img, label) 

Función para crear un modelo competitivo. En un modelo competitivo, el discriminador no está entrenado.


 def generator_containing_discriminator(g, d, d_label): noise = Input(shape=(latent_dim,)) img = g(noise) d.trainable = False d_label.trainable = False valid, target_label = d(img), d_label(img) return Model(noise, [valid, target_label]) 

Función para descargar un lote con imágenes reales y etiquetas. datos: una matriz de direcciones que se definirán más adelante. En la misma función, la imagen se normaliza.


 def get_images_classes(batch_size, data): X_train = np.zeros((batch_size, img_rows, img_cols, img_channels)) y_labels = np.zeros(batch_size) choice_arr = np.random.randint(0, len(data), batch_size) for i in range(batch_size): rand_number = np.random.randint(0, len(data[choice_arr[i]])) temp_img = cv2.imread(data[choice_arr[i]][rand_number]) X_train[i] = temp_img y_labels[i] = choice_arr[i] X_train = (X_train - 127.5)/127.5 return X_train, y_labels 

Función para una bella salida del lote de imágenes. En realidad, todas las imágenes de este artículo fueron recopiladas por esta función.


 def combine_images(generated_images): num = generated_images.shape[0] width = int(math.sqrt(num)) height = int(math.ceil(float(num)/width)) shape = generated_images.shape[1:3] image = np.zeros((height*shape[0], width*shape[1], img_channels), dtype=generated_images.dtype) for index, img in enumerate(generated_images): i = int(index/width) j = index % width image[i*shape[0]:(i+1)*shape[0], j*shape[1]:(j+1)*shape[1]] = \ img[:, :, :,] return image 

Y aquí está esta misma información. En una forma más o menos conveniente, devuelve un conjunto de direcciones de imagen, que nosotros, arriba, estamos organizados en carpetas


 def get_data(): styles_folder = os.listdir(path=os.getcwd() + "\\new256_images\\") num_styles = len(styles_folder) data = [] for i in range(num_styles): data.append(glob.glob(os.getcwd() + '\\new256_images\\' + styles_folder[i] + '\\*')) return data, num_styles 

Para pasar la era, se estableció un gran número aleatorio, porque era demasiado vago para calcular el número de todas las imágenes. En la misma función se proporciona la carga de escalas si es necesario continuar con el entrenamiento. Cada 5 eras, se conservan el peso y la arquitectura.


También vale la pena escribir que intenté agregar ruido a las imágenes de entrada, pero en el último entrenamiento decidí no hacerlo.
Se utilizan etiquetas de clase suavizadas, que ayudan mucho al aprendizaje.


 def train_another(epochs = 100, BATCH_SIZE = 4, weights = False, month_day = '', epoch = ''): data, num_styles = get_data() generator = build_generator() discriminator, d_label = build_discriminator(num_styles) discriminator.compile(loss=losses[0], optimizer=d_optim) d_label.compile(loss=losses[1], optimizer=d_optim) generator.compile(loss='binary_crossentropy', optimizer=g_optim) if month_day != '': generator.load_weights(os.getcwd() + '/' + month_day + epoch + ' gen_weights.h5') discriminator.load_weights(os.getcwd() + '/' + month_day + epoch + ' dis_weights.h5') d_label.load_weights(os.getcwd() + '/' + month_day + epoch + ' dis_label_weights.h5') dcgan = generator_containing_discriminator(generator, discriminator, d_label) dcgan.compile(loss=losses[0], optimizer=g_optim) discriminator.trainable = True d_label.trainable = True for epoch in range(epochs): for index in range(int(15000/BATCH_SIZE)): noise = np.random.normal(0, 1, (BATCH_SIZE, latent_dim)) real_images, real_labels = get_images_classes(BATCH_SIZE, data) #real_images += np.random.normal(size = img_shape, scale= 0.1) generated_images = generator.predict(noise) X = real_images real_labels = real_labels - 0.1 + np.random.rand(BATCH_SIZE)*0.2 y_classif = keras.utils.to_categorical(np.zeros(BATCH_SIZE) + real_labels, num_styles) y = 0.8 + np.random.rand(BATCH_SIZE)*0.2 d_loss = [] d_loss.append(discriminator.train_on_batch(X, y)) discriminator.trainable = False d_loss.append(d_label.train_on_batch(X, y_classif)) print("epoch %d batch %d d_loss : %f, label_loss: %f" % (epoch, index, d_loss[0], d_loss[1])) X = generated_images y = np.random.rand(BATCH_SIZE) * 0.2 d_loss = discriminator.train_on_batch(X, y) print("epoch %d batch %d d_loss : %f" % (epoch, index, d_loss)) noise = np.random.normal(0, 1, (BATCH_SIZE, latent_dim)) discriminator.trainable = False d_label.trainable = False y_classif = keras.utils.to_categorical(np.zeros(BATCH_SIZE) + 1/num_styles, num_styles) y = np.random.rand(BATCH_SIZE) * 0.3 g_loss = dcgan.train_on_batch(noise, [y, y_classif]) d_label.trainable = True discriminator.trainable = True print("epoch %d batch %d g_loss : %f, label_loss: %f" % (epoch, index, g_loss[0], g_loss[1])) if index % 50 == 0: image = combine_images(generated_images) image = image*127.5+127.5 cv2.imwrite( os.getcwd() + '\\generated\\epoch%d_%d.png' % (epoch, index), image) image = combine_images(real_images) image = image*127.5+127.5 cv2.imwrite( os.getcwd() + '\\generated\\epoch%d_%d_data.png' % (epoch, index), image) if epoch % 5 == 0: date_today = date.today() month, day = date_today.month, date_today.day #      json d_json = discriminator.to_json() #     json_file = open(os.getcwd() + "/%d.%d dis_model.json" % (day, month), "w") json_file.write(d_json) json_file.close() #      json d_l_json = d_label.to_json() #     json_file = open(os.getcwd() + "/%d.%d dis_label_model.json" % (day, month), "w") json_file.write(d_l_json) json_file.close() #      json gen_json = generator.to_json() #     json_file = open(os.getcwd() + "/%d.%d gen_model.json" % (day, month), "w") json_file.write(gen_json) json_file.close() discriminator.save_weights(os.getcwd() + '/%d.%d %d_epoch dis_weights.h5' % (day, month, epoch)) d_label.save_weights(os.getcwd() + '/%d.%d %d_epoch dis_label_weights.h5' % (day, month, epoch)) generator.save_weights(os.getcwd() + '/%d.%d %d_epoch gen_weights.h5' % (day, month, epoch)) 

Inicializar variables y ejecutar entrenamiento. Debido al bajo "poder" de mi computadora, el entrenamiento es posible en un máximo de 16 imágenes.


 img_rows = 256 img_cols = 256 img_channels = 3 img_shape = (img_rows, img_cols, img_channels) latent_dim = 100 filter_size_g = (5,5) filter_size_d = (5,5) d_strides = (2,2) color_mode = 'rgb' losses = ['binary_crossentropy', 'categorical_crossentropy'] g_optim = Adam(0.0002, beta_2 = 0.5) d_optim = Adam(0.0002, beta_2 = 0.5) train_another(1000, 16) 

En general, durante mucho tiempo quiero escribir una publicación en el habr sobre esta idea mía, ahora no es el mejor momento para esto, porque esta neurona ha estado estudiando durante tres días y ahora está en la era 113, pero hoy encontré imágenes interesantes, así que decidí que era el momento Ya escribiría una publicación!



Estas son las fotos que salieron hoy. Quizás al nombrarlos, puedo transmitir al lector mi percepción personal de estas imágenes. Es bastante notable que la red no está lo suficientemente capacitada (o puede que no sea capacitada por tales métodos en absoluto), especialmente teniendo en cuenta que las imágenes fueron tomadas por trazado, pero hoy obtuve un resultado que me gustó.


Los planes futuros incluyen reentrenar esta configuración hasta que quede claro de lo que es capaz. También está previsto crear una red que amplíe estas imágenes a tamaños razonables. Esto ya ha sido inventado y hay implementaciones.


Me encantaría recibir críticas constructivas, buenos consejos y preguntas.

Source: https://habr.com/ru/post/es431614/


All Articles