Guten Tag. Vor sechs Monaten begann ich maschinelles Lernen zu studieren, absolvierte einige Kurse und sammelte einige Erfahrungen damit. Als ich dann alle möglichen Neuigkeiten darüber sah, welche neuronalen Netze cool sind und viel bewirken können, beschloss ich, sie zu studieren. Ich fing an, das Buch von Nikolenko über tiefes Lernen zu lesen, und kam im Verlauf des Lesens auf mehrere Ideen (die für die Welt nicht neu sind, aber für mich von großem Interesse waren), von denen eine darin besteht, ein neuronales Netzwerk zu schaffen, das Kunst für mich erzeugt, die nicht cool erscheint nur für mich, den "Vater des Zeichenkindes", aber auch für andere Menschen. In diesem Artikel werde ich versuchen, den Weg zu beschreiben, den ich gegangen bin, um die ersten Ergebnisse zu erzielen, die mich zufrieden stellen.
Datenerfassung
Als ich das Kapitel über wettbewerbsfähige Netzwerke las, wurde mir klar, dass ich jetzt etwas schreiben kann.
Eine der ersten Aufgaben bestand darin, einen Webseiten-Parser zu schreiben, um den Datensatz zu erfassen. Dafür war die Wikiart- Website perfekt , sie hat eine große Anzahl von Gemälden und alle sind nach Stil gesammelt. Dies war mein erster Parser, also schrieb ich ihn für 4-5 Tage, von denen die ersten 3 auf einem völlig falschen Weg stießen. Der richtige Weg war, auf die Registerkarte Netzwerk im Quellcode der Seite zu gehen und zu verfolgen, wie die Bilder angezeigt werden, wenn Sie auf die Schaltfläche "Mehr" klicken. Für die gleichen Anfänger wie mich ist es eigentlich gut, den Code zu zeigen.
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
In der ersten Jupiter-Zelle habe ich die erforderlichen Bibliotheken importiert.
- glob - Eine praktische Sache, um eine Liste von Dateien in einem Verzeichnis zu erhalten
- Anfragen, BeautifulSoup - Schnurrbart zum Parsen
- json - eine Bibliothek zum Abrufen eines Wörterbuchs, das zurückgegeben wird, wenn Sie auf einer Site auf die Schaltfläche "Mehr" klicken
- Größe ändern, speichern, imreaden - zum Lesen und Vorbereiten von Bildern.
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)
Ich beschreibe die Funktionen für eine bequeme Bedienung.
Die erste - erhält eine Seite in Form von Text, die zweite macht diesen Text bequemer für die Arbeit. Nun, der dritte besteht darin, die erforderlichen Ordner nach Stil zu erstellen.
styles = ['kubizm'] url1 = 'https://www.wikiart.org/ru/paintings-by-style/' url2 = '?select=featured&json=2&layout=new&page=' url3 = '&resultType=masonry'
Im Styles-Array hätte es eigentlich mehrere Stile geben sollen, aber es kam vor, dass ich sie völlig ungleichmäßig heruntergeladen habe.
for style in styles: make_dir(style, 'images') for style in styles: make_dir(style, 'new256_images')
Erstellen Sie die erforderlichen Ordner. Im zweiten Zyklus werden Ordner erstellt, in denen das Bild gespeichert und auf ein Quadrat von 256 x 256 reduziert wird.
(Zuerst dachte ich darüber nach, die Größe der Bilder irgendwie nicht zu normalisieren, damit es nicht zu Verzerrungen kommt, aber mir wurde klar, dass dies entweder unmöglich oder zu schwierig für mich war.)
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')
Hier werden die Bilder heruntergeladen und im gewünschten Ordner gespeichert. Hier ändern die Bilder ihre Größe nicht, die Originale werden gespeichert.
Interessante Dinge passieren in der ersten verschachtelten Schleife:
Ich habe mich entschlossen, dumm ständig json's zu fragen (json ist das Wörterbuch, das der Server zurückgibt, wenn Sie auf die Schaltfläche "Mehr" klicken. Das Wörterbuch enthält alle Informationen zu den Bildern) und anzuhalten, wenn der Server etwas Unscharfes zurückgibt, das nicht den typischen Werten entspricht . In diesem Fall sollte das erste Zeichen des zurückgegebenen Textes eine öffnende geschweifte Klammer sein, nach der der Hauptteil des Wörterbuchs folgt.
Es wurde auch festgestellt, dass der Server so etwas wie ein Fotoalbum zurückgeben kann. Das ist im Wesentlichen eine Reihe von Gemälden. Zuerst dachte ich, dass einzelne Bilder zurückkehren würden, der Name der Künstler zu ihnen, oder vielleicht so, dass sofort mit einem Namen des Künstlers eine Reihe von Gemälden gegeben wird.
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:
Hier werden die Bilder in der Größe geändert und in dem dafür vorbereiteten Ordner gespeichert.
Nun, der Datensatz ist zusammengestellt, Sie können mit dem interessantesten fortfahren!
Klein anfangen
Nachdem ich den Originalartikel gelesen hatte , fing ich an zu kreieren! Aber was war meine Enttäuschung, als nichts Gutes herauskam? Bei diesen Versuchen habe ich das Netzwerk auf den gleichen Bildstil trainiert, aber selbst das hat nicht geklappt. Deshalb habe ich beschlossen, zu lernen, wie man aus dem Multiplikator Zahlen generiert. Ich werde hier nicht im Detail darauf eingehen, ich werde nur über die Architektur und den Wendepunkt sprechen, dank dessen Zahlen zu generieren begannen.
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 - ein Array von 100 zufällig generierten Zahlen.
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
Das heißt, insgesamt sind die Ausgabegrößen der Faltungsschichten und die Anzahl der Schichten im Allgemeinen geringer als im Originalartikel. 28x28 weil ich generiere, keine Innenräume!
Nun, der Trick, aufgrund dessen alles geklappt hat - bei der geraden Iteration des Trainings betrachtete der Diskriminator die erzeugten Bilder und bei der ungeraden Iteration - die realen.
Das ist es im Grunde. Ein ähnliches DCGAN lernte sehr schnell, zum Beispiel wurde das Bild zu Beginn dieses Unterthemas im 19. Jahrhundert erhalten,
Diese sind bereits zuversichtlich, aber manchmal keine reellen Zahlen, wie sich in der 99. Ära der Bildung herausstellte.
Zufrieden mit dem vorläufigen Ergebnis hörte ich auf zu lernen und begann darüber nachzudenken, wie ich das Hauptproblem lösen könnte.
Kreatives gegnerisches Netzwerk
Der nächste Schritt bestand darin, über das GAN mit Beschriftungen zu lesen: Die Klasse des aktuellen Bildes wird dem Diskriminator und Generator bereitgestellt. Und nach dem Gan mit Labels habe ich etwas über CAN herausgefunden - die Dekodierung erfolgt im Grunde genommen im Namen des Unterthemas.
In CAN versucht der Diskriminator, die Klasse des Bildes zu erraten, wenn das Bild aus einer realen Menge stammt. Und dementsprechend erhält der Diskriminator im Fall des Trainings in einem realen Bild zusätzlich zum Standard einen Fehler, wenn er die Klasse als Fehler errät.
Beim Training in einem erzeugten Bild muss der Diskriminator nur vorhersagen, ob dieses Bild real ist oder nicht.
Der Generator muss außerdem, nur um den Diskriminator auszutricksen, den Diskriminator beim Erraten der Bildklasse ratlos machen, dh der Generator wird daran interessiert sein, dass die Ausgaben an die Diskriminatoren so weit wie möglich von 1 vollem Vertrauen entfernt sind.
Als ich mich an CAN wandte, hatte ich erneut Schwierigkeiten, Demoral aufgrund der Tatsache, dass nichts funktioniert und nicht lernt. Nach mehreren unangenehmen Fehlern habe ich mich entschlossen, von vorne zu beginnen und alle Änderungen (Ja, das habe ich vorher noch nicht getan), Gewichte und Architektur (um das Training zu unterbrechen) zu speichern.
Zuerst wollte ich ein Netzwerk erstellen, das für mich ein einzelnes 256x256-Bild (alle folgenden Bilder dieser Größe) ohne Beschriftung generiert. Der Wendepunkt hier war, dass im Gegenteil bei jeder Iteration des Trainings dem Diskriminator ein Blick auf die erzeugten und die realen Bilder gegeben werden sollte.

Dies ist das Ergebnis, bei dem ich angehalten habe und mit dem nächsten Schritt fortgefahren bin. Ja, die Farben unterscheiden sich vom tatsächlichen Bild, aber ich war mehr an der Fähigkeit des Netzwerks interessiert, Konturen und Objekte hervorzuheben. Sie kam damit klar.
Dann könnten wir zur Hauptaufgabe übergehen - Kunst erzeugen. Präsentieren Sie den Code sofort und kommentieren Sie ihn auf dem Weg.
Zunächst müssen Sie wie immer alle Bibliotheken importieren.
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
Generator erstellen.
Die Ausgabe der Ebenen unterscheidet sich wiederum vom Artikel. Irgendwo, um Speicherplatz zu sparen (Bedingungen: ein Heimcomputer mit gtx970), und irgendwo wegen des Erfolgs bei der Konfiguration
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
Die Diskriminatorerstellungsfunktion gibt zwei Modelle zurück, von denen eines versucht herauszufinden, ob das Bild echt ist, und das andere versucht, die Klasse des Bildes herauszufinden.
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)
Funktion zur Schaffung eines Wettbewerbsmodells. In einem Wettbewerbsmodell wird der Diskriminator nicht trainiert.
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])
Funktion zum Herunterladen eines Stapels mit echten Bildern und Etiketten. Daten - Ein Array von Adressen, die später definiert werden. In der gleichen Funktion wird das Bild normalisiert.
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
Funktion für schöne Ausgabe des Bildstapels. Tatsächlich wurden alle Bilder aus diesem Artikel von dieser Funktion gesammelt.
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
Und hier sind die gleichen Daten. Es gibt in mehr oder weniger praktischer Form eine Reihe von Bildadressen zurück, die wir oben in Ordnern angeordnet haben
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
Um die Ära zu überstehen, wurde eine zufällige große Zahl festgelegt, da sie zu faul war, um die Anzahl aller Bilder zu berechnen. In der gleichen Funktion wird das Laden von Waagen bereitgestellt, wenn das Training fortgesetzt werden muss. Alle 5 Epochen bleiben Gewicht und Architektur erhalten.
Es lohnt sich auch zu schreiben, dass ich versucht habe, den Eingabebildern Rauschen hinzuzufügen, aber im letzten Training habe ich beschlossen, dies nicht zu tun.
Es werden geglättete Klassenbezeichnungen verwendet, die das Lernen sehr erleichtern.
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)
Initialisieren Sie Variablen und führen Sie das Training durch. Aufgrund der geringen "Leistung" meines Computers ist ein Training mit maximal 16 Bildern möglich.
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)
Im Allgemeinen möchte ich lange Zeit einen Beitrag über diese Idee von mir schreiben, jetzt ist nicht die beste Zeit dafür, da dieses Neuron drei Tage lang studiert hat und sich jetzt in der 113. Ära befindet, aber heute habe ich interessante Bilder gefunden, also habe ich beschlossen, dass es Zeit ist würde schon einen Beitrag schreiben!

Dies sind die Bilder, die heute herausgekommen sind. Vielleicht kann ich durch ihre Benennung dem Leser meine persönliche Wahrnehmung dieser Bilder vermitteln. Es ist ziemlich auffällig, dass das Netzwerk nicht ausreichend trainiert ist (oder möglicherweise überhaupt nicht mit solchen Methoden trainiert wird), insbesondere wenn man bedenkt, dass die Bilder durch Anschreiben aufgenommen wurden, aber heute habe ich ein Ergebnis erhalten, das mir gefallen hat.
Zukünftige Pläne sehen eine Umschulung dieser Konfiguration vor, bis klar wird, wozu sie in der Lage ist. Es ist auch geplant, ein Netzwerk zu schaffen, das diese Bilder auf vernünftige Größen vergrößert. Dies wurde bereits erfunden und es gibt Implementierungen.
Ich würde mich sehr über konstruktive Kritik, gute Ratschläge und Fragen freuen.