Création d'art à l'aide de DCGAN sur Keras

Bonjour. Il y a six mois, j'ai commencé à étudier l'apprentissage automatique, j'ai suivi quelques cours et acquis une certaine expérience dans ce domaine. Puis, en voyant toutes sortes de nouvelles sur ce que les réseaux de neurones sont cool et peuvent faire beaucoup, j'ai décidé d'essayer de les étudier. J'ai commencé à lire le livre de Nikolenko sur l'apprentissage en profondeur et au cours de la lecture, j'ai proposé plusieurs idées (qui ne sont pas nouvelles dans le monde, mais qui m'ont beaucoup intéressé), dont l'une est de créer un réseau de neurones qui générerait pour moi de l'art qui semblerait cool non seulement pour moi, le "père de l'enfant dessin", mais aussi pour les autres. Dans cet article, je vais essayer de décrire le chemin que j'ai parcouru afin d'obtenir les premiers résultats qui me satisfont.


Collecte de données


Quand j'ai lu le chapitre sur les réseaux compétitifs, j'ai réalisé que maintenant je peux écrire quelque chose.
L'une des premières tâches a été d'écrire un analyseur de page Web pour collecter l'ensemble de données. Pour cela, le site wikiart était parfait , il a un grand nombre de tableaux et tous sont rassemblés par style. C'était mon premier analyseur, donc je l'ai écrit pendant 4-5 jours, dont les 3 premiers ont pris des coups sur un chemin complètement faux. La bonne façon était d'aller dans l'onglet réseau dans le code source de la page et de suivre la façon dont les images apparaissent lorsque vous cliquez sur le bouton "plus". En fait, pour les mêmes débutants que moi, ce sera bien de montrer le code.


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 

Dans la première cellule jupiter, j'ai importé les bibliothèques nécessaires.


  • glob - Une chose pratique pour obtenir une liste de fichiers dans un répertoire
  • demandes, BeautifulSoup - Moustache pour l'analyse
  • json - une bibliothèque pour obtenir un dictionnaire qui revient lorsque vous cliquez sur le bouton "plus" sur un site
  • redimensionner, enregistrer, lire - pour lire les images et les préparer.

 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) 

Je décris les fonctions pour un fonctionnement pratique.


Le premier - obtient une page sous forme de texte, le second rend ce texte plus pratique pour le travail. Eh bien, la troisième consiste à créer les dossiers nécessaires par style.


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

Dans le tableau des styles, par conception, il devait y avoir plusieurs styles, mais il se trouve que je les ai téléchargés de manière complètement inégale.


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

Créez les dossiers nécessaires. Le deuxième cycle crée des dossiers dans lesquels l'image sera stockée, aplatie en un carré de 256x256.


(Au début, j'ai pensé à ne pas normaliser les tailles des images pour qu'il n'y ait pas de distorsions, mais j'ai réalisé que c'était soit impossible, soit trop difficile pour moi)


 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') 

Ici, les images sont téléchargées et enregistrées dans le dossier souhaité. Ici, les images ne changent pas de taille, les originaux sont enregistrés.


Des choses intéressantes se produisent dans la première boucle imbriquée:


J'ai décidé de demander stupidement constamment à json (json est le dictionnaire que le serveur retourne lorsque vous cliquez sur le bouton "Plus". Le dictionnaire contient toutes les informations sur les images), et de m'arrêter lorsque le serveur renvoie quelque chose de flou et pas comme les valeurs typiques . Dans ce cas, le premier caractère du texte renvoyé aurait dû être une accolade ouvrante, après quoi vient le corps du dictionnaire.


Il a également été remarqué que le serveur peut renvoyer quelque chose comme un album photo. Il s'agit essentiellement d'un tableau de peintures. Au début, je pensais que des peintures uniques revenaient, le nom des artistes pour eux, ou peut-être pour qu'à la fois avec un seul nom de l'artiste, un tableau de peintures soit donné.


  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) 

Ici, les images sont redimensionnées et enregistrées dans le dossier préparé pour elles.


Eh bien, l'ensemble de données est assemblé, vous pouvez passer à la plus intéressante!


Commencer petit



De plus, après avoir lu l' article original, j'ai commencé à créer! Mais quelle a été ma déception quand rien de bon n'est sorti. Dans ces tentatives, j'ai formé le réseau sur le même style d'images, mais même cela n'a pas fonctionné, j'ai donc décidé de commencer à apprendre à générer des nombres à partir du multiplicateur. Je ne m'étendrai pas ici en détail, je ne parlerai que de l'architecture et du point de basculement, grâce auxquels les chiffres ont commencé à être générés.


 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 - un tableau de 100 nombres générés aléatoirement.


     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 

    C'est-à-dire que, par le total, les tailles de sortie des couches convolutives et le nombre de couches sont généralement inférieurs à ceux de l'article d'origine. 28x28 car je génère, pas des intérieurs!



Eh bien, l'astuce en raison de laquelle tout a fonctionné - à l'itération égale de la formation, le discriminateur a regardé les images générées, et à l'itération impaire - aux vraies.


C’est fondamentalement ça. Un DCGAN similaire a appris très rapidement, par exemple, l'image au début de ce sous-sujet a été obtenue à la 19e ère,



Ceux-ci sont déjà confiants, mais néanmoins parfois pas de vrais chiffres, ils se sont avérés dans la 99e ère de l'éducation.


Satisfait du résultat préliminaire, j'ai arrêté d'apprendre et j'ai commencé à réfléchir à la façon de résoudre le problème principal.


Réseau contradictoire créatif


L'étape suivante consistait à lire sur le GAN avec des étiquettes: la classe de l'image actuelle est servie au discriminateur et au générateur. Et après le gan avec des étiquettes, j'ai découvert CAN - le décodage est essentiellement au nom du sous-sujet.


En CAN, le discriminateur essaie de deviner la classe de l'image si l'image provient d'un ensemble réel. Et, par conséquent, dans le cas de la formation sur une image réelle, en tant qu'erreur, le discriminateur, en plus de la valeur par défaut, reçoit une erreur de deviner la classe.


Lors de la formation sur une image générée, le discriminateur n'a qu'à prédire si cette image est réelle ou non.


Le générateur, en outre, juste pour tromper le discriminateur, doit rendre le discriminateur à perte lorsqu'il devine la classe de l'image, c'est-à-dire que le générateur sera intéressé par le fait que les sorties vers les discriminateurs sont aussi éloignées que possible de 1, en toute confiance.


En ce qui concerne CAN, j'ai de nouveau connu des difficultés, une démoralité due au fait que rien ne fonctionne et n'apprend pas. Après plusieurs échecs désagréables, j'ai décidé de tout recommencer et de sauvegarder tous les changements (oui, je ne l'avais pas fait auparavant), les poids et l'architecture (pour interrompre la formation).


Tout d'abord, je voulais créer un réseau qui générerait pour moi une seule image 256x256 (toutes les images suivantes de cette taille) sans étiquette. Le point tournant ici a été que, au contraire, à chaque itération de la formation, le discriminateur doit regarder les images générées et les vraies.



C'est le résultat auquel je me suis arrêté et je suis passé à l'étape suivante. Oui, les couleurs sont différentes de l'image réelle, mais j'étais plus intéressé par la capacité du réseau à mettre en valeur les contours et les objets. Elle s'en est sortie.


Ensuite, nous pourrions passer à la tâche principale - générer de l'art. Présentez immédiatement le code, commentez-le en cours de route.


Tout d'abord, comme toujours, vous devez importer toutes les bibliothèques.


 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 

Création d'un générateur.


La sortie des couches est à nouveau différente de l'article. Quelque part afin d'économiser de la mémoire (Conditions: un ordinateur personnel avec gtx970), et quelque part en raison du succès de la configuration


 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 fonction de création de discriminateur renvoie deux modèles, dont l'un essaie de savoir si l'image est réelle, et l'autre essaie de découvrir la classe de l'image.


 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) 

Fonction pour créer un modèle compétitif. Dans un modèle compétitif, le discriminateur n'est pas formé.


 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]) 

Fonction de téléchargement d'un lot avec de vraies images et étiquettes. data - un tableau d'adresses qui sera défini plus tard. Dans la même fonction, l'image est normalisée.


 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 

Fonction pour une belle sortie du lot d'images. En fait, toutes les photos de cet article ont été collectées par cette fonction.


 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 

Et voici ces mêmes données. Il sous une forme plus ou moins pratique retourne un ensemble d'adresses d'image, que nous, ci-dessus, sommes disposées dans des dossiers


 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 

Pour passer l'ère, un grand nombre aléatoire a été défini, car il était trop paresseux pour calculer le nombre de toutes les images. Dans la même fonction, le chargement des balances est fourni s'il est nécessaire de poursuivre la formation. Toutes les 5 époques, le poids et l'architecture sont préservés.


Il vaut également la peine d'écrire que j'ai essayé d'ajouter du bruit aux images d'entrée, mais lors de la dernière formation, j'ai décidé de ne pas le faire.
Des étiquettes de classe lissées sont utilisées, elles aident beaucoup à l'apprentissage.


 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)) 

Initialisez les variables et exécutez la formation. En raison de la faible "puissance" de mon ordinateur, la formation est possible sur un maximum de 16 images.


 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 général, depuis longtemps, je veux écrire un article sur le habr à propos de cette idée, ce n'est pas le meilleur moment pour cela, parce que ce neurone étudie depuis trois jours et est maintenant à la 113e ère, mais aujourd'hui, j'ai trouvé des images intéressantes, alors j'ai décidé qu'il était temps écrirait déjà un article!



Ce sont les photos qui sont sorties aujourd'hui. Peut-être qu'en les nommant, je peux transmettre au lecteur ma perception personnelle de ces images. Il est tout à fait notable que le réseau n'est pas suffisamment formé (ou peut-être pas du tout formé par de telles méthodes), d'autant plus que les photos ont été prises par scribing, mais aujourd'hui j'ai obtenu un résultat que j'ai aimé.


Les plans futurs incluent le recyclage de cette configuration jusqu'à ce qu'il devienne clair de quoi elle est capable. Il est également prévu de créer un réseau qui agrandirait ces images à des tailles raisonnables. Cela a déjà été inventé et il existe des implémentations.


Je serais extrêmement heureux de recevoir des critiques constructives, de bons conseils et des questions.

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


All Articles