Einfachheit und Komplexität von Grundelementen oder Feststellung einer unnötigen Vorverarbeitung für ein neuronales Netzwerk

Published on February 06, 2019

Einfachheit und Komplexität von Grundelementen oder Feststellung einer unnötigen Vorverarbeitung für ein neuronales Netzwerk

Dies ist der dritte Artikel zur Analyse und Untersuchung von Ellipsen, Dreiecken und anderen geometrischen Formen.
Frühere Artikel haben einige sehr interessante Fragen für die Leser aufgeworfen, insbesondere zur Komplexität oder Einfachheit verschiedener Trainingssequenzen. Die Fragen sind tatsächlich sehr interessant, zum Beispiel, wie viel schwieriger ist ein Dreieck zum Lernen als ein Viereck oder ein anderes Polygon?



Versuchen wir zu vergleichen, und zum Vergleich haben wir eine ausgezeichnete, von Generationen von Studenten nachgewiesene Idee - je kürzer der Spickzettel, desto einfacher die Prüfung.

Auch dieser Artikel ist einfach das Ergebnis von Neugier und müßigem Interesse, nichts davon ist in der Praxis zu finden, und für praktische Aufgaben gibt es ein paar großartige Ideen, aber für das Kopieren gibt es fast nichts. Dies ist eine kleine Studie über die Komplexität der Trainingssequenzen - die Argumentation des Autors und der Code sind dargelegt, Sie können alles selbst überprüfen / hinzufügen / ändern.

Versuchen wir also herauszufinden, welche geometrische Figur für die Segmentierung schwieriger oder einfacher ist, welcher Kurs für KI-Vorlesungen verständlicher ist und sich besser anpasst.

Es gibt viele verschiedene geometrische Figuren, aber wir werden nur Dreiecke, Vierecke und fünfzackige Sterne vergleichen. Wir werden eine einfache Methode anwenden, um eine Zugsequenz zu konstruieren - wir werden das 128x128-Einfarbenbild in vier Teile teilen und zufällig eine Ellipse und zum Beispiel ein Dreieck in diese Viertel platzieren. Wir werden ein Dreieck mit der gleichen Farbe wie die Ellipse erkennen. Dh die aufgabe besteht darin, das netzwerk zu trainieren, um beispielsweise ein viereckiges polygon von einer ellipse zu unterscheiden, die in derselben farbe gefärbt ist. Hier sind Beispiele von Bildern, die wir untersuchen werden:







Wir werden kein Dreieck und kein Viereck in einem Bild erkennen, wir werden sie getrennt in verschiedenen Zügen vor dem Hintergrund eines ellipsenartigen Rauschens erkennen.

Lernen Sie das klassische U-Netz und drei Arten von Trainingssequenzen mit Dreiecken, Vierecken und Sternen.

Also gegeben:

  • drei Trainingssequenzen von Bild- / Maskenpaaren;
  • Netzwerk. Gemeinsames U-Netz, das häufig zur Segmentierung verwendet wird.

Idee zur Verifizierung:

  • Ermitteln Sie, welche der Trainingssequenzen „schwieriger“ zu erlernen sind.
  • Wie einige Vorverarbeitungstechniken das Lernen beeinflussen

Beginnen wir, wählen Sie 10.000 Bilderpaare von Vierecken mit Ellipsen und Masken und betrachten Sie sie sorgfältig. Uns interessiert, wie kurz das Kinderbett wird und von welcher Länge es abhängt.

Laden Sie die Bibliothek und bestimmen Sie die Größe des Bildarrays
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


Verlust- und Genauigkeitsfunktionen bestimmen
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 Pixelmaske vorhersagen - dies ist der „Hintergrund“ oder das „Viereck“ - und die Wahrheit oder Falschheit der Vorhersage bewerten. Dh 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 haben einen Fehler bei der Vorhersage eines „Hintergrunds“ oder eines „Vierecks“ gemacht. Und so schätzen wir für alle Bilder und Pixel die Anzahl aller vier Optionen und berechnen das Ergebnis - dies ist das Ergebnis des Netzwerks. Und je weniger falsche Vorhersagen und je wahrer, desto genauer das Ergebnis und desto besser der Netzwerkbetrieb.

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 ausgewählt werden. Schauen Sie später in die Tiefen des Netzwerks, wenn Sie die Netzwerke vergleichen.

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 Paaren von Bild / Maske. Auf einem Schwarzweißbild 128x128, gefüllt mit zufälligem Rauschen, mit zufällig ausgewählten Werten 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. Platzieren Sie ein Viereck im anderen Viertel und färben Sie es gleichmäßig 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, sehen wir uns Random 10 an. Lassen Sie mich daran erinnern, dass die Bilder einfarbig und in Graustufen 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())



Der erste Schritt. Wir trainieren mit dem Mindeststartsatz


Der erste Schritt unseres Experiments ist einfach: Wir versuchen, das Netzwerk so zu trainieren, dass nur 11 der ersten 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 anfänglichen Sequenz ausgewählt und das Netzwerk darauf trainiert. Jetzt ist es egal, ob das Netzwerk diese bestimmten Bilder lernt oder verallgemeinert, Hauptsache, es kann diese 11 Bilder erkennen, wie wir sie brauchen. Abhängig vom gewählten Datensatz und der Genauigkeit kann das Netzwerktraining sehr lange dauern. Wir haben aber nur wenige Iterationen. Ich wiederhole, dass es uns jetzt egal ist, wie oder was das Netzwerk gelernt oder gelernt hat. Hauptsache, es hat die festgelegte Vorhersagegenauigkeit erreicht.

Beginnen wir nun mit dem Hauptexperiment.


Lassen Sie uns einen Spickzettel erstellen, wir erstellen solche Spickzettel separat für alle drei Trainingssequenzen und vergleichen ihre Länge. Wir werden neue Bild / Masken-Paare aus der erstellten Sequenz nehmen und versuchen, sie mit einem Netzwerk vorherzusagen, das auf die bereits ausgewählte Sequenz trainiert ist. Am Anfang sind es nur 11 Paar Bilder / Masken und das Netzwerk ist vielleicht nicht sehr gut trainiert. Wenn die neue Maske im Bild mit akzeptabler Genauigkeit vorhergesagt wird, dann werfen wir dieses Paar raus, es enthält keine neuen Informationen für das Netzwerk, es kennt die Maske bereits und kann sie aus diesem Bild berechnen. Wenn die Vorhersagegenauigkeit nicht ausreicht, fügen wir dieses maskierte Bild zu unserer Sequenz hinzu und beginnen, das Netzwerk zu trainieren, bis eine akzeptable Genauigkeit für die ausgewählte Sequenz erreicht ist. Dh

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 Standardmetrik von Keras verwendet, und das Unterprogramm "my_iou_metric" wird zur Berechnung der Genauigkeit verwendet.

Und jetzt vergleichen wir die Arbeit desselben Netzwerks mit denselben Parametern in einer anderen Sequenz, in Dreiecken,



und wir erhalten ein völlig 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, d.h. Die Markigkeit von Bildern mit Dreiecken ist zweimal niedriger als bei Vierecken!

Überprüfen Sie dasselbe auf den Sternen und führen Sie das Netzwerk in der dritten Sequenz aus, die



wir erhalten

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

Wie Sie sehen, waren die Sterne die informativsten, nur 476 Bilder im Spickzettel.

Wir haben einen Grund, die Komplexität geometrischer Formen für die Wahrnehmung ihres neuronalen Netzwerks zu beurteilen. Das einfachste ist ein Stern, nur 476 Bilder im Spickzettel, dann ein Quad mit seinen 1007 und das schwierigste ist ein Dreieck - zum Lernen braucht man 1913 Bilder.

Bedenken Sie, dies ist für uns, für Menschen sind dies Bilder, und für das Netzwerk ist dies ein Kurs mit Wiedererkennungsvorträgen und ein Kurs über Dreiecke, der sich als der schwierigste herausgestellt hat.

Nun zum Ernst


Auf den ersten Blick scheinen all diese Ellipsen und Dreiecke zu verwöhnen, Sandkuchen und Lego. Aber hier ist eine spezifische und ernste Frage: Wenn wir eine Art Vorverarbeitung, einen Filter, auf die anfängliche Sequenz anwenden, wie wird sich die Komplexität der Sequenz ändern? Nehmen Sie zum Beispiel dieselben Ellipsen und Vierecke und wenden Sie diese 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 ganz anders zu funktionieren:

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

Eine kleine Erklärung ist hier nötig, wir verwenden keine Augmentation, da Die Form des Polygons und die Form der Ellipse werden anfänglich zufällig ausgewählt. Daher liefert Augmentation keine neuen Informationen und ist in diesem Fall nicht sinnvoll.

Wie sich jedoch aus dem Ergebnis der Arbeit ergibt, hat der einfache Gauß-Filter viele Probleme für das Netzwerk verursacht und viele neue und wahrscheinlich unnötige Informationen generiert.

Nun, für Liebhaber der Einfachheit in ihrer reinen Form nehmen wir die gleichen Ellipsen mit Polygonen, aber ohne jede Chance in der Farbe, das



Ergebnis legt nahe, dass zufällige Farbe überhaupt kein einfaches Additiv ist.

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

Das Netzwerk hat die aus 251 Bildern extrahierten Informationen vollständig verwaltet, fast viermal weniger als aus der Vielzahl der durch Rauschen gefärbten Bilder.

Der Zweck des Artikels ist es, einige Werkzeuge und Beispiele seiner Arbeit an leichtfertigen Beispielen, Lego im Sandkasten, zu zeigen. Wir haben ein Tool zum Vergleichen zweier Trainingssequenzen erhalten, mit dem wir abschätzen können, inwieweit unsere Vorverarbeitung die Trainingssequenz kompliziert oder vereinfacht, wie einfach dieses oder jenes Grundelement in der Trainingssequenz für die Erkennung ist.

Die Möglichkeit, dieses Lego-Beispiel in realen Fällen zu verwenden, liegt auf der Hand, aber echte Schulungen und Netzwerke von Lesern sind Sache der Leser.