Criando arte usando o DCGAN em Keras

Bom dia Seis meses atrás, comecei a estudar aprendizado de máquina, fiz alguns cursos e ganhei alguma experiência com isso. Então, vendo todos os tipos de notícias sobre o que as redes neurais são legais e que podem fazer muito, decidi tentar estudá-las. Comecei a ler o livro de Nikolenko sobre aprendizado profundo e, ao longo da leitura, tive várias idéias (que não são novas para o mundo, mas que eram de grande interesse para mim), uma das quais é criar uma rede neural que geraria arte para mim que pareceria legal não somente para mim, o "pai da criança que desenha", mas também para outras pessoas. Neste artigo, tentarei descrever o caminho que percorri para obter os primeiros resultados que me satisfazem.


Coleta de dados


Quando li o capítulo sobre redes competitivas, percebi que agora posso escrever algo.
Uma das primeiras tarefas foi escrever um analisador de página da web para coletar o conjunto de dados. Para isso, o site da wikiart foi perfeito , possui um grande número de pinturas e todas são coletadas por estilo. Este foi o meu primeiro analisador, então escrevi por 4-5 dias, os três primeiros dos quais foram puxados por um caminho completamente errado. A maneira correta era ir para a guia de rede no código fonte da página e rastrear como as imagens aparecem quando você clica no botão "mais". Na verdade, para os mesmos iniciantes como eu, será bom mostrar o 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 

Na primeira célula de Júpiter, importei as bibliotecas necessárias.


  • glob - Uma coisa útil para obter uma lista de arquivos em um diretório
  • pedidos, BeautifulSoup - Bigode para análise
  • json - uma biblioteca para obter um dicionário que retorna quando você clica no botão "mais" em um site
  • redimensionar, salvar, ler - para ler e prepará-las.

 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) 

Descrevo as funções para uma operação conveniente.


O primeiro - obtém uma página na forma de texto, o segundo torna esse texto mais conveniente para o trabalho. Bem, o terceiro é criar as pastas necessárias por estilo.


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

Na matriz de estilos, por design, deveria haver vários estilos, mas aconteceu que eu os baixei completamente desigualmente.


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

Crie as pastas necessárias. O segundo ciclo cria pastas nas quais a imagem será armazenada, achatada em um quadrado de 256x256.


(No começo, pensei em não normalizar os tamanhos das fotos para que não houvesse distorções, mas percebi que isso era impossível ou muito difícil para mim)


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

Aqui, as imagens são baixadas e salvas na pasta desejada. Aqui as fotos não mudam de tamanho, os originais são salvos.


Coisas interessantes acontecem no primeiro loop aninhado:


Eu decidi perguntar estupidamente constantemente ao json's (json é o dicionário que o servidor retorna quando você clica no botão "Mais". O dicionário contém todas as informações sobre as imagens) e para quando o servidor retorna algo distorcido e que não se parece com valores típicos . Nesse caso, o primeiro caractere do texto retornado deveria ter sido um colchete de abertura, depois do qual vem o corpo do dicionário.


Também foi observado que o servidor pode retornar algo como um álbum de fotos. Isso é essencialmente uma variedade de pinturas. No começo, pensei que as pinturas individuais estavam voltando, o nome dos artistas para eles, ou talvez para que, ao mesmo tempo, com um nome do artista, fosse dada uma variedade 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) 

Aqui, as imagens são redimensionadas e salvas na pasta preparada para elas.


Bem, o conjunto de dados está montado, você pode prosseguir para o mais interessante!


Começando pequeno



Além disso, depois de ler o artigo original, comecei a criar! Mas qual foi a minha decepção quando nada de bom saiu. Nessas tentativas, treinei a rede com o mesmo estilo de imagens, mas mesmo que não funcionasse, decidi começar a aprender a gerar números a partir do multiplicador. Não vou me deter aqui em detalhes, falarei apenas sobre a arquitetura e o ponto de inflexão, graças ao qual os números começaram a ser gerados.


 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 - uma matriz de 100 números gerados 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 

    Ou seja, pelo total, os tamanhos de saída das camadas convolucionais e o número de camadas são geralmente menores do que no artigo original. 28x28 porque eu gero, não interiores!



Bem, o truque devido ao qual tudo deu certo - na iteração uniforme do treinamento, o discriminador olhou para as imagens geradas e na iteração ímpar - nas reais.


É basicamente isso. Um DCGAN semelhante aprendeu muito rapidamente, por exemplo, a imagem no início deste subtópico foi obtida na década 19,



Eles já estão confiantes, mas, às vezes, não são números reais, eles apareceram na 99a era da educação.


Satisfeito com o resultado preliminar, parei de aprender e comecei a pensar em como resolver o problema principal.


Rede adversária criativa


O próximo passo foi ler sobre o GAN com etiquetas: a classe da imagem atual é veiculada ao discriminador e gerador. E depois do gan com etiquetas, descobri o CAN - a decodificação está basicamente no nome do subtópico.


No CAN, o discriminador tenta adivinhar a classe da imagem se a imagem for de um cenário real. E, consequentemente, no caso de treinar uma imagem real, como um erro, o discriminador, além do padrão, recebe um erro ao adivinhar a classe.


Ao treinar em uma imagem gerada, o discriminador precisa apenas prever se essa imagem é real ou não.


O gerador, além disso, apenas para enganar o discriminador, precisa prejudicá-lo ao adivinhar a classe da imagem, ou seja, o gerador estará interessado no fato de que as saídas para os discriminadores estão o mais longe possível de 1, com total confiança.


Voltando ao CAN, tive novamente dificuldades, desmoralidade pelo fato de nada funcionar e não aprender. Após várias falhas desagradáveis, decidi começar tudo de novo e salvar todas as alterações (sim, não fiz isso antes), pesos e arquitetura (para interromper o treinamento).


Primeiro, eu queria criar uma rede que gerasse uma única imagem de 256x256 para mim (todas as seguintes imagens deste tamanho) sem rótulos. O ponto de virada aqui foi que, ao contrário, em cada iteração do treinamento, o discriminador deve dar uma olhada nas imagens geradas e nas reais.



Este é o resultado em que parei e segui para o próximo passo. Sim, as cores são diferentes da imagem real, mas eu estava mais interessado na capacidade da rede de destacar contornos e objetos. Ela lidou com isso.


Então poderíamos prosseguir para a tarefa principal - gerar arte. Apresente imediatamente o código, comentando-o ao longo do caminho.


Primeiro, como sempre, você precisa importar todas as 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 

Criando um gerador.


A saída das camadas é novamente diferente do artigo. Em algum lugar para economizar memória (Condições: um computador doméstico com gtx970) e em algum lugar por causa do sucesso com a configuração


 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 

A função de criação de discriminador retorna dois modelos, um dos quais tenta descobrir se a imagem é real e o outro tenta descobrir a classe da imagem.


 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) 

Função para criar um modelo competitivo. Em um modelo competitivo, o discriminador não é treinado.


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

Função para baixar um lote com imagens e etiquetas reais. data - uma matriz de endereços que serão definidos posteriormente. Na mesma função, a imagem é normalizada.


 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 

Função para saída bonita do lote de imagens. Na verdade, todas as imagens deste artigo foram coletadas por esta função.


 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 

E aqui estão esses mesmos dados. De uma forma mais ou menos conveniente, retorna um conjunto de endereços de imagem, os quais, acima, são organizados em pastas


 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 passar a era, um número grande e aleatório foi definido, porque era muito preguiçoso para calcular o número de todas as fotos. Na mesma função, o carregamento de balanças é fornecido se for necessário continuar o treinamento. A cada 5 épocas, o peso e a arquitetura são preservados.


Também vale a pena escrever que tentei adicionar ruído às imagens de entrada, mas no último treinamento, decidi não fazer isso.
Etiquetas de classe suavizadas são usadas, elas ajudam muito no aprendizado.


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

Inicialize variáveis ​​e execute o treinamento. Devido ao baixo "poder" do meu computador, é possível treinar no máximo 16 imagens.


 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) 

Em geral, há muito tempo eu quero escrever um post sobre o habr sobre essa ideia minha, agora não é o melhor momento para isso, porque esse neurônio estuda há três dias e está agora na era 113, mas hoje encontrei fotos interessantes, então decidi que estava na hora já escreveria um post!



Estas são as fotos que saíram hoje. Talvez nomeando-os, eu possa transmitir ao leitor minha percepção pessoal dessas imagens. É bastante perceptível que a rede não é treinada o suficiente (ou pode não ser treinada por tais métodos), especialmente considerando que as fotos foram tiradas pelo desenho, mas hoje obtive um resultado que gostei.


Os planos futuros incluem a reciclagem dessa configuração até que fique claro do que é capaz. Também está planejado criar uma rede que amplie essas imagens para tamanhos sãos. Isso já foi inventado e existem implementações.


Eu ficaria extremamente feliz por críticas construtivas, bons conselhos e perguntas.

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


All Articles