Die Einfachheit und Komplexität von Grundelementen oder wie unnötige Vorverarbeitung für ein neuronales Netzwerk ermittelt werden kann

Dies ist der dritte Artikel über die Analyse und Untersuchung von Ellipsen, Dreiecken und anderen geometrischen Formen.
Die vorangegangenen Artikel haben unter den Lesern einige sehr interessante Fragen aufgeworfen, insbesondere zur Komplexität oder Einfachheit bestimmter Trainingssequenzen. Die Fragen sind tatsächlich sehr interessant, zum Beispiel, wie viel schwieriger ist es, ein Dreieck zu lernen als ein Viereck oder ein anderes Polygon?



Versuchen wir zu vergleichen, und zum Vergleich haben wir eine großartige Idee, die von Generationen von Studenten getestet wurde: Je kürzer der Spickzettel, desto einfacher die Prüfung.

Dieser Artikel ist auch einfach das Ergebnis von Neugier und müßigem Interesse, nichts davon ist in der Praxis anzutreffen und für praktische Aufgaben gibt es ein paar großartige Ideen, aber es gibt fast nichts zum Kopieren und Einfügen. Dies ist eine kleine Studie über die Komplexität von Trainingssequenzen. Die Argumentation und der Code des Autors werden vorgestellt. Sie können alles selbst überprüfen / ergänzen / ändern.

Versuchen wir also herauszufinden, welche geometrische Figur für die Segmentierung komplizierter oder einfacher ist, welcher Vorlesungskurs für KI verständlicher ist und besser aufgenommen wird.

Es gibt viele verschiedene geometrische Formen, aber wir werden nur Dreiecke, Vierecke und fünfzackige Sterne vergleichen. Wir werden eine einfache Methode zum Konstruieren einer Zugsequenz verwenden - wir werden 128x128 monochrome Bilder in vier Teile teilen und zufällig eine Ellipse und zum Beispiel ein Dreieck in diesen Vierteln platzieren. Wir werden ein Dreieck mit der gleichen Farbe wie die Ellipse erkennen. Das heißt, Die Aufgabe besteht darin, das Netzwerk zu trainieren, um beispielsweise ein viereckiges Polygon von einer Ellipse zu unterscheiden, die in derselben Farbe gemalt ist. Hier sind Beispiele von Bildern, die wir studieren werden







Wir werden kein Dreieck und kein Viereck in einem Bild erkennen, wir werden sie getrennt in verschiedenen Zügen vor dem Hintergrund von Interferenzen in Form einer Ellipse erkennen.

Nehmen wir das klassische U-Netz und drei Arten von Trainingssequenzen mit Dreiecken, Vierecken und Sternen für die Forschung.

Also gegeben:

  • drei Trainingssequenzen von Bild / Masken-Paaren;
  • das Netzwerk. Gewöhnliches U-Netz, das häufig zur Segmentierung verwendet wird.

Idee zu testen:

  • Bestimmen Sie, welche der Trainingssequenzen „schwerer“ zu lernen ist.
  • wie sich einige Vorverarbeitungstechniken auf das Lernen auswirken

Beginnen wir mit der Auswahl von 10.000 Bildpaaren von Vierecken mit Ellipsen und Masken und betrachten Sie diese sorgfältig. Wir sind daran interessiert, wie kurz die Krippe wird und wie lang sie ist.

Wir laden Bibliotheken, wir bestimmen die Größe eines Arrays von Bildern
import numpy as np import matplotlib.pyplot as plt %matplotlib inline import math from tqdm import tqdm from skimage.draw import ellipse, polygon from keras import Model from keras.optimizers import Adam from keras.layers import Input,Conv2D,Conv2DTranspose,MaxPooling2D,concatenate from keras.layers import BatchNormalization,Activation,Add,Dropout from keras.losses import binary_crossentropy from keras import backend as K import tensorflow as tf import keras as keras w_size = 128 train_num = 10000 radius_min = 10 radius_max = 20 


Bestimmen Sie die Verlust- und Genauigkeitsfunktionen
 def dice_coef(y_true, y_pred): y_true_f = K.flatten(y_true) y_pred = K.cast(y_pred, 'float32') y_pred_f = K.cast(K.greater(K.flatten(y_pred), 0.5), 'float32') intersection = y_true_f * y_pred_f score = 2. * K.sum(intersection) / (K.sum(y_true_f) + K.sum(y_pred_f)) return score def dice_loss(y_true, y_pred): smooth = 1. y_true_f = K.flatten(y_true) y_pred_f = K.flatten(y_pred) intersection = y_true_f * y_pred_f score = (2. * K.sum(intersection) + smooth) / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth) return 1. - score def bce_dice_loss(y_true, y_pred): return binary_crossentropy(y_true, y_pred) + dice_loss(y_true, y_pred) def get_iou_vector(A, B): # Numpy version batch_size = A.shape[0] metric = 0.0 for batch in range(batch_size): t, p = A[batch], B[batch] true = np.sum(t) pred = np.sum(p) # deal with empty mask first if true == 0: metric += (pred == 0) continue # non empty mask case. Union is never empty # hence it is safe to divide by its number of pixels intersection = np.sum(t * p) union = true + pred - intersection iou = intersection / union # iou metrric is a stepwise approximation of the real iou over 0.5 iou = np.floor(max(0, (iou - 0.45)*20)) / 10 metric += iou # teake the average over all images in batch metric /= batch_size return metric def my_iou_metric(label, pred): # Tensorflow version return tf.py_func(get_iou_vector, [label, pred > 0.5], tf.float64) from keras.utils.generic_utils import get_custom_objects get_custom_objects().update({'bce_dice_loss': bce_dice_loss }) get_custom_objects().update({'dice_loss': dice_loss }) get_custom_objects().update({'dice_coef': dice_coef }) get_custom_objects().update({'my_iou_metric': my_iou_metric }) 


Wir werden die Metrik aus dem ersten Artikel verwenden . Lassen Sie mich die Leser daran erinnern, dass wir die Maske des Pixels vorhersagen werden - dies ist der "Hintergrund" oder das "Viereck" und die Wahrheit oder Falschheit der Vorhersage bewerten. Das heißt, Die folgenden vier Optionen sind möglich: Wir haben richtig vorausgesagt, dass ein Pixel ein Hintergrund ist, richtig vorausgesagt, dass ein Pixel ein Viereck ist, oder einen Fehler bei der Vorhersage eines „Hintergrunds“ oder „Vierecks“ gemacht. Daher schätzen wir für alle Bilder und alle Pixel die Anzahl aller vier Optionen und berechnen das Ergebnis - dies ist das Ergebnis des Netzwerks. Und je weniger fehlerhafte Vorhersagen und wahrer, desto genauer das Ergebnis und desto besser das Netzwerk.

Wir untersuchen das Netzwerk als „Black Box“, wir werden nicht untersuchen, was mit dem Netzwerk im Inneren passiert, wie sich Gewichte ändern und wie Gradienten gewählt werden - wir werden später beim Vergleich der Netzwerke in die Eingeweide des Netzwerks schauen.

einfaches U-Netz
 def build_model(input_layer, start_neurons): # 128 -> 64 conv1 = Conv2D(start_neurons * 1, (3, 3), activation="relu", padding="same")(input_layer) conv1 = Conv2D(start_neurons * 1, (3, 3), activation="relu", padding="same")(conv1) pool1 = MaxPooling2D((2, 2))(conv1) pool1 = Dropout(0.25)(pool1) # 64 -> 32 conv2 = Conv2D(start_neurons * 2, (3, 3), activation="relu", padding="same")(pool1) conv2 = Conv2D(start_neurons * 2, (3, 3), activation="relu", padding="same")(conv2) pool2 = MaxPooling2D((2, 2))(conv2) pool2 = Dropout(0.5)(pool2) # 32 -> 16 conv3 = Conv2D(start_neurons * 4, (3, 3), activation="relu", padding="same")(pool2) conv3 = Conv2D(start_neurons * 4, (3, 3), activation="relu", padding="same")(conv3) pool3 = MaxPooling2D((2, 2))(conv3) pool3 = Dropout(0.5)(pool3) # 16 -> 8 conv4 = Conv2D(start_neurons * 8, (3, 3), activation="relu", padding="same")(pool3) conv4 = Conv2D(start_neurons * 8, (3, 3), activation="relu", padding="same")(conv4) pool4 = MaxPooling2D((2, 2))(conv4) pool4 = Dropout(0.5)(pool4) # Middle convm = Conv2D(start_neurons * 16, (3, 3), activation="relu", padding="same")(pool4) convm = Conv2D(start_neurons * 16, (3, 3), activation="relu", padding="same")(convm) # 8 -> 16 deconv4 = Conv2DTranspose(start_neurons * 8, (3, 3), strides=(2, 2), padding="same")(convm) uconv4 = concatenate([deconv4, conv4]) uconv4 = Dropout(0.5)(uconv4) uconv4 = Conv2D(start_neurons * 8, (3, 3), activation="relu", padding="same")(uconv4) uconv4 = Conv2D(start_neurons * 8, (3, 3), activation="relu", padding="same")(uconv4) # 16 -> 32 deconv3 = Conv2DTranspose(start_neurons * 4, (3, 3), strides=(2, 2), padding="same")(uconv4) uconv3 = concatenate([deconv3, conv3]) uconv3 = Dropout(0.5)(uconv3) uconv3 = Conv2D(start_neurons * 4, (3, 3), activation="relu", padding="same")(uconv3) uconv3 = Conv2D(start_neurons * 4, (3, 3), activation="relu", padding="same")(uconv3) # 32 -> 64 deconv2 = Conv2DTranspose(start_neurons * 2, (3, 3), strides=(2, 2), padding="same")(uconv3) uconv2 = concatenate([deconv2, conv2]) uconv2 = Dropout(0.5)(uconv2) uconv2 = Conv2D(start_neurons * 2, (3, 3), activation="relu", padding="same")(uconv2) uconv2 = Conv2D(start_neurons * 2, (3, 3), activation="relu", padding="same")(uconv2) # 64 -> 128 deconv1 = Conv2DTranspose(start_neurons * 1, (3, 3), strides=(2, 2), padding="same")(uconv2) uconv1 = concatenate([deconv1, conv1]) uconv1 = Dropout(0.5)(uconv1) uconv1 = Conv2D(start_neurons * 1, (3, 3), activation="relu", padding="same")(uconv1) uconv1 = Conv2D(start_neurons * 1, (3, 3), activation="relu", padding="same")(uconv1) uncov1 = Dropout(0.5)(uconv1) output_layer = Conv2D(1, (1,1), padding="same", activation="sigmoid")(uconv1) return output_layer # model input_layer = Input((w_size, w_size, 1)) output_layer = build_model(input_layer, 26) model = Model(input_layer, output_layer) model.compile(loss=bce_dice_loss, optimizer=Adam(lr=1e-4), metrics=[my_iou_metric]) model.summary() 


Die Funktion zum Erzeugen von Bild / Masken-Paaren. Auf einem Schwarzweißbild 128x128 gefüllt mit zufälligem Rauschen mit einem zufällig ausgewählten aus zwei Bereichen oder 0,0 ... 0,75 oder 0,25..1,0. Wählen Sie zufällig ein Viertel im Bild aus und platzieren Sie eine zufällig ausgerichtete Ellipse. Im anderen Viertel platzieren wir ein Viereck und eine Farbe mit zufälligem Rauschen.

 def next_pair(): img_l = (np.random.sample((w_size, w_size, 1))* 0.75).astype('float32') img_h = (np.random.sample((w_size, w_size, 1))* 0.75 + 0.25).astype('float32') img = np.zeros((w_size, w_size, 2), dtype='float') i0_qua = math.trunc(np.random.sample()*4.) i1_qua = math.trunc(np.random.sample()*4.) while i0_qua == i1_qua: i1_qua = math.trunc(np.random.sample()*4.) _qua = np.int(w_size/4) qua = np.array([[_qua,_qua],[_qua,_qua*3],[_qua*3,_qua*3],[_qua*3,_qua]]) p = np.random.sample() - 0.5 r = qua[i0_qua,0] c = qua[i0_qua,1] r_radius = np.random.sample()*(radius_max-radius_min) + radius_min c_radius = np.random.sample()*(radius_max-radius_min) + radius_min rot = np.random.sample()*360 rr, cc = ellipse( r, c, r_radius, c_radius, rotation=np.deg2rad(rot), shape=img_l.shape ) p0 = np.rint(np.random.sample()*(radius_max-radius_min) + radius_min) p1 = qua[i1_qua,0] - (radius_max-radius_min) p2 = qua[i1_qua,1] - (radius_max-radius_min) p3 = np.rint(np.random.sample()*radius_min) p4 = np.rint(np.random.sample()*radius_min) p5 = np.rint(np.random.sample()*radius_min) p6 = np.rint(np.random.sample()*radius_min) p7 = np.rint(np.random.sample()*radius_min) p8 = np.rint(np.random.sample()*radius_min) poly = np.array(( (p1, p2), (p1+p3, p2+p4+p0), (p1+p5+p0, p2+p6+p0), (p1+p7+p0, p2+p8), (p1, p2), )) rr_p, cc_p = polygon(poly[:, 0], poly[:, 1], img_l.shape) if p > 0: img[:,:,:1] = img_l.copy() img[rr, cc,:1] = img_h[rr, cc] img[rr_p, cc_p,:1] = img_h[rr_p, cc_p] else: img[:,:,:1] = img_h.copy() img[rr, cc,:1] = img_l[rr, cc] img[rr_p, cc_p,:1] = img_l[rr_p, cc_p] img[:,:,1] = 0. img[rr_p, cc_p,1] = 1. return img 

Lassen Sie uns eine Trainingssequenz von Paaren erstellen, siehe Zufall 10. Ich möchte Sie daran erinnern, dass die Bilder monochrom und grau sind.

 _txy = [next_pair() for idx in range(train_num)] f_imgs = np.array(_txy)[:,:,:,:1].reshape(-1,w_size ,w_size ,1) f_msks = np.array(_txy)[:,:,:,1:].reshape(-1,w_size ,w_size ,1) del(_txy) #    10   fig, axes = plt.subplots(2, 10, figsize=(20, 5)) for k in range(10): kk = np.random.randint(train_num) axes[0,k].set_axis_off() axes[0,k].imshow(f_imgs[kk]) axes[1,k].set_axis_off() axes[1,k].imshow(f_msks[kk].squeeze()) 



Erster Schritt. Wir trainieren am Mindeststart


Der erste Schritt unseres Experiments ist einfach. Wir versuchen, das Netzwerk so zu trainieren, dass nur 11 erste Bilder vorhergesagt werden.

 batch_size = 10 val_len = 11 precision = 0.85 m0_select = np.zeros((f_imgs.shape[0]), dtype='int') for k in range(val_len): m0_select[k] = 1 t = tqdm() while True: fit = model.fit(f_imgs[m0_select>0], f_msks[m0_select>0], batch_size=batch_size, epochs=1, verbose=0 ) current_accu = fit.history['my_iou_metric'][0] current_loss = fit.history['loss'][0] t.set_description("accuracy {0:6.4f} loss {1:6.4f} ".\ format(current_accu, current_loss)) t.update(1) if current_accu > precision: break t.close() 

accuracy 0.8545 loss 0.0674 lenght 11 : : 793it [00:58, 14.79it/s]

Wir haben die ersten 11 aus der Anfangssequenz ausgewählt und das Netzwerk darauf trainiert. Jetzt spielt es keine Rolle, ob das Netzwerk diese Bilder speziell speichert oder zusammenfasst. Hauptsache, es kann diese 11 Bilder so erkennen, wie wir es brauchen. Abhängig vom ausgewählten Datensatz und der Genauigkeit kann das Netzwerktraining sehr lange dauern. Wir haben aber nur wenige Iterationen. Ich wiederhole, dass es für uns jetzt nicht wichtig ist, wie und was das Netzwerk gelernt oder gelernt hat. Hauptsache, es hat die etablierte Genauigkeit der Vorhersage erreicht.

Starten Sie nun das Hauptexperiment


Wir werden den Spickzettel erstellen, wir werden solche Spickzettel für alle drei Trainingssequenzen separat erstellen und ihre Länge vergleichen. Wir werden neue Bild / Masken-Paare aus der konstruierten Sequenz nehmen und versuchen, sie durch das trainierte Netzwerk auf der bereits ausgewählten Sequenz vorherzusagen. Am Anfang sind es nur 11 Bild- / Maskenpaare und das Netzwerk ist trainiert, vielleicht nicht sehr richtig. Wenn in einem neuen Paar die Maske aus dem Bild mit akzeptabler Genauigkeit vorhergesagt wird, verwerfen wir dieses Paar, es enthält keine neuen Informationen für das Netzwerk, es kennt die Maske bereits und kann sie aus diesem Bild berechnen. Wenn die Genauigkeit der Vorhersage nicht ausreicht, fügen wir dieses Bild mit einer Maske zu unserer Sequenz hinzu und beginnen, das Netzwerk zu trainieren, bis ein akzeptables Genauigkeitsergebnis für die ausgewählte Sequenz erzielt wird. Das heißt, Dieses Bild enthält neue Informationen und wir fügen sie unserer Trainingssequenz hinzu und extrahieren die darin enthaltenen Informationen durch Training.

 batch_size = 50 t_batch_size = 1024 raw_len = val_len t = tqdm(-1) id_train = 0 #id_select = 1 while True: t.set_description("Accuracy {0:6.4f} loss {1:6.4f}\ selected img {2:5d} tested img {3:5d} ". format(current_accu, current_loss, val_len, raw_len)) t.update(1) if id_train == 1: fit = model.fit(f_imgs[m0_select>0], f_msks[m0_select>0], batch_size=batch_size, epochs=1, verbose=0 ) current_accu = fit.history['my_iou_metric'][0] current_loss = fit.history['loss'][0] if current_accu > precision: id_train = 0 else: t_pred = model.predict( f_imgs[raw_len: min(raw_len+t_batch_size,f_imgs.shape[0])], batch_size=batch_size ) for kk in range(t_pred.shape[0]): val_iou = get_iou_vector( f_msks[raw_len+kk].reshape(1,w_size,w_size,1), t_pred[kk].reshape(1,w_size,w_size,1) > 0.5) if val_iou < precision*0.95: new_img_test = 1 m0_select[raw_len+kk] = 1 val_len += 1 break raw_len += (kk+1) id_train = 1 if raw_len >= train_num: break t.close() 

 Accuracy 0.9338 loss 0.0266 selected img 1007 tested img 9985 : : 4291it [49:52, 1.73s/it] 

Hier wird Genauigkeit im Sinne von "Genauigkeit" und nicht als Standard-Keras-Metrik verwendet, und das Unterprogramm "my_iou_metric" wird zur Berechnung der Genauigkeit verwendet.

Vergleichen Sie nun den Betrieb desselben Netzwerks mit denselben Parametern in einer anderen Sequenz in Dreiecken



Und wir bekommen ein ganz anderes Ergebnis

 Accuracy 0.9823 loss 0.0108 selected img 1913 tested img 9995 : : 6343it [2:11:36, 3.03s/it] 

Das Netzwerk wählte 1913 Bilder mit "neuen" Informationen aus, d.h. Der Inhalt von Bildern mit Dreiecken ist halb so hoch wie bei Vierecken!

Lassen Sie uns dasselbe auf den Sternen überprüfen und das Netzwerk in der dritten Sequenz ausführen



wir bekommen

 Accuracy 0.8985 loss 0.0478 selected img 476 tested img 9985 : : 2188it [16:13, 1.16it/s] 

Wie Sie sehen können, erwiesen sich die Sterne als die informativsten, nur 476 Bilder in einem Spickzettel.

Wir hatten Grund, die Komplexität geometrischer Formen für die Wahrnehmung anhand ihres neuronalen Netzwerks zu beurteilen. Am einfachsten ist der Stern mit nur 476 Bildern im Spickzettel, dann das Viereck mit seinen 1007 und das komplexeste als Dreieck - für das Training benötigen Sie 1913 Bilder.

Denken Sie daran, dies ist für uns, für Menschen ist es ein Bild, aber für das Netzwerk ist es ein Vorlesungskurs über Anerkennung und der Kurs über Dreiecke erwies sich als der schwierigste.

Nun zum Ernst


Auf den ersten Blick scheinen sich all diese Ellipsen und Dreiecke verwöhnen zu lassen, Sandkuchen und Lego. Aber hier ist eine spezifische und ernste Frage: Wenn wir eine Art Vorverarbeitung anwenden, filtern Sie auf die ursprüngliche Sequenz, wie wird sich die Komplexität der Sequenz ändern? Zum Beispiel nehmen wir alle gleichen Ellipsen und Vierecke und wenden eine solche Vorverarbeitung auf sie an

 from scipy.ndimage import gaussian_filter _tmp = [gaussian_filter(idx, sigma = 1) for idx in f_imgs] f1_imgs = np.array(_tmp)[:,:,:,:1].reshape(-1,w_size ,w_size ,1) del(_tmp) fig, axes = plt.subplots(2, 5, figsize=(20, 7)) for k in range(5): kk = np.random.randint(train_num) axes[0,k].set_axis_off() axes[0,k].imshow(f1_imgs[kk].squeeze(), cmap="gray") axes[1,k].set_axis_off() axes[1,k].imshow(f_msks[kk].squeeze(), cmap="gray") 



Auf den ersten Blick ist alles gleich, die gleichen Ellipsen, die gleichen Polygone, aber das Netzwerk begann auf ganz andere Weise zu funktionieren:

 Accuracy 1.0575 loss 0.0011 selected img 7963 tested img 9999 : : 17765it [29:02:00, 12.40s/it] 

Hier ist eine kleine Erklärung nötig, wir verwenden keine Augmentation, weil Die Polygonform und die Ellipsenform werden anfänglich zufällig ausgewählt. Daher liefert die Erweiterung keine neuen Informationen und ist in diesem Fall nicht sinnvoll.

Wie aus dem Ergebnis der Arbeit hervorgeht, verursachte ein einfacher gaussian_filter viele Probleme für das Netzwerk und erzeugte viele neue und wahrscheinlich überflüssige Informationen.

Nun, für Liebhaber der Einfachheit in ihrer reinsten Form nehmen wir die gleichen Ellipsen mit Polygonen, aber ohne zufällige Farbgebung



Das Ergebnis legt nahe, dass zufällige Farben überhaupt keine einfache Ergänzung sind.

 Accuracy 0.9004 loss 0.0315 selected img 251 tested img 9832 : : 1000it [06:46, 1.33it/s] 

Das Netzwerk war die Informationen aus 251 Bildern absolut wert, fast viermal weniger als aus vielen mit Rauschen gemalten Bildern.

Der Zweck des Artikels ist es, einige Werkzeuge und Beispiele seiner Arbeit an leichtfertigen Beispielen zu zeigen, das Lego im Sandkasten. Wir haben ein Tool zum Vergleichen von zwei Trainingssequenzen. Wir können bewerten, wie sehr unsere Vorverarbeitung die Trainingssequenz kompliziert oder vereinfacht, wie einfach dieses oder jenes Grundelement in der Trainingssequenz zu erkennen ist.

Die Möglichkeit, dieses Lego-Beispiel in realen Fällen anzuwenden, liegt auf der Hand, aber die realen Schulungen und die Netzwerke der Leser liegen bei den Lesern selbst.

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


All Articles