KerasでDCGANを使用してアートを作成する

良い一日。 半年前、私は機械学習の勉強を始め、いくつかのコースを経験し、これについてある程度の経験を得ました。 それから、どのニューラルネットワークがクールで多くのことができるかについてのあらゆる種類のニュースを見て、それらを研究しようとすることにしました。 ディープラーニングに関するニコレンコの本を読み始めましたが、読む過程でいくつかのアイデアを思いつきました(これは新しいものではありませんが、私にとって非常に興味深いものです)。 「絵を描く子の父」だけでなく、他の人にも。 この記事では、私が満足する最初の結果を得るために私が歩いた道を説明しようとします。







データ収集



敵対的ネットワークの章を読んだとき、私は今、何かを書くことができることに気付きました。

最初のタスクの1つは、データセットを収集するWebページパーサーを作成することでした。 ウィキアートのウェブサイトはこれぴったりで 、多数の絵画があり、すべてスタイル別に組み立てられています。 これが私の最初のパーサーだったので、私は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
      
      





最初のジュピターセルでは、必要なライブラリをインポートしました。









 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)
      
      





便利な操作のための機能を説明します。







最初の-ページをテキスト形式で取得し、2番目のページでこのテキストを作業に便利にします。 3番目は、スタイルごとに必要なフォルダーを作成することです。







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





スタイル配列には、設計上、いくつかのスタイルが存在するはずでしたが、たまたまそれらを完全に不均等にダウンロードしました。







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





必要なフォルダーを作成します。 2番目のサイクルでは、画像が保存されるフォルダーを作成し、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は、「詳細」ボタンをクリックするとサーバーが返す辞書です。辞書には写真に関するすべての情報が含まれます)。 。 この場合、返されるテキストの最初の文字は開き中かっこである必要があり、その後に辞書の本文が続きます。







サーバーが絵画のアルバムのようなものを返すことができることにも気づきました。 それは本質的に絵画の配列です。 最初は、単一の絵画が戻ってきて、アーティストの名前が彼らに返ってきたと思いました。そして、たぶん、アーティストの名前が1つだけで、一連の絵画が与えられたのでしょう。







  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
      
      







まあ、すべてがうまくいったための非常にトリックです-トレーニングの偶数回の繰り返しでは、識別器は生成された写真を見て、奇数回の繰り返しでは-実際の写真を見ました。







基本的には以上です。 同様のDCGANは非常に迅速に学習しました。たとえば、このサブトピックの最初の写真は19世紀に取得されたもので、













これらはすでに自信を持っていますが、それでも実数ではない場合がありますが、教育の第99期になりました。







予備的な結果に満足し、私は学習をやめ、主な問題を解決する方法について考え始めました。







創造的な敵対ネットワーク



次のステップは、ラベル付きの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
      
      





弁別子作成関数は2つのモデルを返します。1つは画像が本物かどうかを調べ、もう1つは画像のクラスを見つけようとしています。







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





実際の写真とラベルを含むバッチをダウンロードする機能。 data-後で定義されるアドレスの配列。 同じ関数で、画像は正規化されます。







 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に投稿したいと思っていますが、今はこれがベストではありません。このニューロンは3日間勉強しており、現在は113時代ですが、今日は面白い写真を見つけたので、それが時間だと決めましたすでに投稿を書くでしょう!









これらは今日出てきた写真です。 おそらくそれらに名前を付けることによって、私はこれらの写真に対する私の個人的な認識を読者に伝えることができます。 特にスクライビングによって写真が撮影されたことを考えると、ネットワークが十分に訓練されていない(またはそのような方法でまったく訓練されていない)ことは非常に注目に値します。







今後、この構成の能力が明らかになるまで、この構成を再トレーニングする予定です。 これらの画像を正常なサイズに拡大するネットワークを作成することも計画されています。 これはすでに発明されており、実装があります。







建設的な批判、良いアドバイス、質問に非常に満足しています。








All Articles