在Keras上使用DCGAN创作艺术

大家好 六个月前,我开始学习机器学习,完成了几门课程,并获得了一些经验。 然后,看到有关神经网络很酷并且可以做很多事情的各种新闻,我决定尝试研究它们。 我开始阅读Nikolenko的有关深度学习的书,在阅读过程中,我提出了几个想法(这不是世界上的新事物,但对我很感兴趣),其中之一是创建一个神经网络,该网络可以为我产生看起来很酷的艺术。不仅对我来说,“画儿之父”,对其他人也是如此。 在本文中,我将尝试描述为了获得令我满意的最初结果而走的道路。


资料收集


当我阅读有关竞争网络的章节时,我意识到现在我可以写点东西了。
首要任务之一是编写一个网页解析器以收集数据集。 为此, wikiart网站非常完美 ,它拥有大量绘画作品,并且全部都是按风格收集的。 这是我的第一个解析器,所以我写了4-5天,其中前3天采用了完全错误的方式进行戳戳。 正确的方法是转到页面源代码中的“网络”选项卡,并跟踪单击“更多”按钮时图片的显示方式。 实际上,对于像我一样的初学者,最好显示代码。


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 

在第一个木星单元中,我导入了必要的库。


  • glob-获取目录中文件列表的便捷工具
  • 请求,BeautifulSoup-解析的胡子
  • json-用于获取字典的库,当您单击站点上的“更多”按钮时,该字典将返回
  • 调整大小,保存和阅读-用于读取图像并进行准备。

 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) 

我描述了方便操作的功能。


第一个-以文本形式获取页面,第二个使此文本更便于工作。 好吧,第三个是通过样式创建必要的文件夹。


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

在styles数组中,根据设计,应该有几种样式,但是碰巧我完全不均匀地下载了它们。


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

创建必要的文件夹。 第二个循环创建将存储图像的文件夹,并将其展平为256x256正方形。


(起初,我考虑过以某种方式不对图片的大小进行归一化,以免出现失真,但是我意识到这对我来说是不可能或太困难了)


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

在此下载图像并将其保存到所需的文件夹。 在这里,图片不改变尺寸,保存了原稿。


有趣的事情发生在第一个嵌套循环中:


我决定愚蠢地不断询问json(当您单击“更多”按钮时json是服务器返回的字典。字典包含有关图片的所有信息),并在服务器返回不明显且不典型的值时停止。 在这种情况下,返回文本的第一个字符应该是一个大括号,然后是字典正文。


还已经注意到,服务器可以返回类似相册的内容。 那本质上是一系列绘画。 起初,我以为是单幅画回来了,就是艺术家的名字,或者也许是这样,立刻用艺术家的名字给了一系列画。


  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) 

在此,将调整图像大小并保存在为图像准备的文件夹中。


好了,数据集已经组装好了,您可以进行最有趣的操作!


从小开始



此外,阅读原始文章后,我开始创作! 但是,当一切都不好的时候,我感到失望的是。 在这些尝试中,我用相同样式的图片训练了网络,但即使这样也没有用,所以我决定开始学习如何从乘法器生成数字。 我不会在这里详细介绍,我只会谈论架构和临界点,这要归功于这些数字的产生。


 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-100个随机生成的数字组成的数组。


     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 

    也就是说,总的来说,卷积层的输出大小和层数通常小于原始文章中的值。 28x28,因为我生成了,而不是内部!



好吧,这是一个绝妙的技巧,所有事情都可以解决-在训练的均匀迭代中,鉴别器会查看生成的图片,而在奇数迭代中会查看真实图片。


基本上就是这样。 类似的DCGAN很快就学会了,例如,该子主题开头的图片是在19世纪获得的,



他们已经很有信心,但是有时却不是真实的数字,他们出现在第99个教育时代。


对初步结果感到满意后,我停止学习,开始思考如何解决主要问题。


创意对抗网络


下一步是阅读带有标签的GAN:将当前图片的类别提供给鉴别器和生成器。 在带有标签的gan之后,我发现了有关CAN的信息 -解码基本上是在子主题的名称下进行的。


在CAN中,如果图片来自真实集合,则鉴别器尝试猜测图片的类别。 并且,因此,在实拍训练的情况下,作为鉴别错误,除了默认值之外,鉴别器还因猜测该类而产生错误。


当在生成的图片中训练时,鉴别器仅需要预测该图片是否真实。


此外,生成器只是为了欺骗鉴别器,在猜测图片类别时需要使鉴别器不知所措,也就是说,生成器将对以下事实感兴趣:鉴别器的输出尽可能远离1,完全置信。


转向CAN,由于没有任何作用并且没有学习,我再次遇到了困难和士气低落。 在经历了几次令人不愉快的失败之后,我决定重新开始并保存所有更改(是的,我之前没有做过),权重和架构(以中断培训)。


首先,我想建立一个网络,该网络可以为我生成一个没有任何标签的256x256图片(以下所有该尺寸的图片)。 相反,这里的转折点是,在每次训练迭代中,应让鉴别器查看生成的图片和真实图片。



这是我停下来并继续进行下一步的结果。 是的,颜色与真实图片有所不同,但是我对网络突出轮廓和对象的功能更感兴趣。 她解决了这个问题。


然后,我们可以进行主要任务-生成艺术。 立即提供代码,并在此过程中对其进行注释。


首先,与往常一样,您需要导入所有库。


 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 

创建一个生成器。


图层的输出再次与文章不同。 某个地方以节省内存(条件:带有gtx970的家用计算机),某个地方是由于配置成功


 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 

鉴别器创建函数返回两个模型,其中一个试图找出图片是否真实,而另一个试图找出图片的类别。


 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) 

建立竞争模型的功能。 在竞争模型中,不对鉴别器进行训练。


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

用于下载带有真实图片和标签的批处理的功能。 数据-稍后将定义的地址数组。 在相同的功能下,图像被标准化。


 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 

用于精美输出图像批处理的功能。 实际上,本文中的所有图片都是通过此功能收集的。


 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 

这是相同的数据。 它以一种或多或少方便的形式返回一组图像地址,上面我们将其安排在文件夹中


 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 

随着时代的过去,设置了一个随机的大数字,因为它太懒了,无法计算所有图片的数量。 在相同的功能中,如果有必要继续训练,则提供秤的负载。 每5个时代,重量和建筑都会保留下来。


同样值得一提的是,我试图为输入图像添加噪点,但是在上一轮培训中,我决定不这样做。
使用了平滑的类标签,它们非常有助于学习。


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

初始化变量并进行训练。 由于计算机的“低功耗”,最多可以训练16张图像。


 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) 

总的来说,很长一段时间以来,我都想在habr上写一篇有关我的想法的文章,现在不是最佳时机,因为这个神经元已经研究了三天,现在处于第113时代,但是今天我发现了有趣的图片,所以我决定是时候了会已经写了一个帖子!



这些是今天出来的照片。 也许通过命名它们,我可以向读者传达我对这些图片的个人看法。 值得注意的是,网络没有经过足够的训练(或者可能根本没有通过这种方法进行训练),特别是考虑到图片是通过划刻来拍摄的,但是今天我得到了喜欢的结果。


未来的计划包括重新培训此配置,直到明确其功能为止。 还计划创建一个将这些图片放大到合理尺寸的网络。 这已经被发明出来,并且有一些实现。


对于建设性的批评,好的建议和问题,我将感到非常高兴。

Source: https://habr.com/ru/post/zh-CN431614/


All Articles