Home / 
Blog / 
Wie GANs helfen Produktionsfehler zu erkennen

Wie GANs helfen Produktionsfehler zu erkennen

Lorenzo Melchior

Am 24. Sep 2019 in Hands-on Machine Learning

Im Machine Learning behindert oft eine unzureichende Menge an Trainingsdaten die Leistung von Klassifikationsalgorithmen. Die Erfahrung zeigt, dass der Mangel an Trainingsdaten eher die Regel als die Ausnahme ist, weshalb die Menschen clevere Methoden zur Datenvermehrung (Data Augmentation) entwickelt haben.

In diesem Blogbeitrag zeige ich, wie Sie mit einem Generative Adversarial Network (GAN) neue Bilder einer Verteilung von Bildern erstellen können. Dies kann als Data Augmentation-Methode bei Problemen wie der Fehlererkennung in der industriellen Produktion eingesetzt werden.

GANs zur Datenvermehrung

Wir können Datenerweiterung nutzen, wie z.B. leichtes Drehen oder Spiegeln der Originaldaten, um neue Trainingsdaten zu generieren. Aber das bringt uns natürlich nicht wirklich neue Bilder.


GANs wiederum geben in der Tat völlig neue Bilder aus. Vielleicht haben Sie schon von GANs als Mittel zur Erstellung überraschend realistischer "Fake"-Bilder und -Videos (was unter dem Begriff "Deepfake" bekannt geworden ist) gehört. Wie aktuelle Forschungsergebnisse (z.B. Antoniou et al. 2017, Wang et al. 2018 und Frid-Adar et al. 2018) zeigen, können sie auch die Leistung von Klassifikatoren für das maschinelle Lernen verbessern, indem sie zusätzliche Trainingsdaten generieren.

Industrielle Anwendungen

Der GAN-Datenvermehrungsansatz ist besonders vielversprechend, wenn nur sehr wenige Trainingsdaten vorhanden sind.


Stellen Sie sich vor, wir wollen ein ML-Modell trainieren, das in einem Zwischenschritt eines industriellen Produktionsablaufes defekte Komponenten identifiziert. Hoffentlich treten Fehler selten auf; aber das bedeutet auch, dass wir wahrscheinlich nur eine kleine Anzahl von Bildern mit exemplarischen Fehlern haben, um das Netzwerk zu trainieren.


Mit Hilfe eines GANs können wir zusätzliche Bilder für jeden Fehlertyp generieren.

Die Daten

Wir verwenden die NEU-Oberflächenfehlerdatenbank, die 300 Bilder von Kratzern auf Metall enthält, die während der Produktion aufgetreten sind.

Eine GAN ist eine unbeaufsichtigte Lernmethode, für die wir keine Label benötigen. Wir haben keine verschiedenen Arten von gelabelten Bildern, die wir unterscheiden wollen, sondern einen Satz ungelabelter Daten, die wir nachzuahmen versuchen.

Das Netzwerk

Ein GAN lässt sich nicht als ein einziges neuronales Netzwerk auffassen. Vielmehr kombiniert es zwei neuronale Netzwerke, die ein Spiel miteinander spielen. Ich werde kurz die Spielregeln erklären.

Wie GANs funktionieren

Erstens gibt es ein Diskriminatornetzwerk, das nur ein einfaches Convolutional Neural Network (CNN) ist. Zweitens haben wir das Generatornetzwerk, das mehr oder weniger ein umgekehrtes CNN ist. Es erhält eine zufällige Eingabe und erzeugt ein Bild als Ausgabe aus dem Up-Sampling der Eingabe mit transponierten Konvolutionen.


Das Spiel findet wie folgt statt: Der Generator erhält eine zufällige Eingabe und erzeugt ein Bild. Der Diskriminator nimmt abwechselnd erzeugte Bilder und Originalbilder auf (ohne zu wissen, welches welches ist) und versucht vorherzusagen, ob es sich bei einem bestimmten Bild um ein Original oder ein erzeugtes Bild handelt, wobei nur die Merkmale des Bildes berücksichtigt werden.


Beide Netzwerke versuchen, mit der Zeit besser zu werden. Der Diskriminator versucht, die realen von den erzeugten Bildern zu unterscheiden, während der Generator darauf abzielt, den Diskriminator dazu zu bringen, seine Bilder für echt zu halten.

Formal betrachtet spielt das Netzwerk das folgende Min-Max-Spiel:

$$\underset{G}{min}\ \underset{D}{max}\ V(D,G) = \mathbb{E}_{x\sim p_{data}(x)}[log D(x)] + \mathbb{E}_{z\sim p_z(z)}[log(1-D(G(z)))]$$

Hier ist z ein Zufallsrauschen, das den Generator G aktiviert, um ein Bild G(z) zu erzeugen. D ist der Diskriminator, der vorhersagt, ob ein Bild echt oder erzeugt ist, d.h. D(x) ist die Wahrscheinlichkeit, dass x ein reales Bild ist. pdata ist die Verteilung der Originaldaten, pz die Verteilung des Rauschens.


Der Diskriminator versucht also, seinen Erfolg zu maximieren, während der Generator versucht, ihn zu minimieren.

Die Konfiguration 

Wir verwenden Python mit PyTorch, etwas NumPy, Pandas und Matplotlib für Visualisierungen. Für das Modell haben wir uns für diese Konfigurationen entschieden:

batch_size = 12
generator_depth = 64
discriminator_depth = 128 loss_function=nn.BCELoss()
number_of_epochs = 128
discriminator_optimizer = optim.Adam(discriminator.parameters(), lr=0.0004, betas(0.5,0.999))
generator_optimizer = optim.Adam(generator.parameters(), lr=0.0001, betas=(0.5,0.999))

Im folgenden Codeblock definieren wir den Diskriminator, der das Bild als Eingabe erhält. Wir definieren eine Reihe von Filtern, die das Modell verwendet, um dieses Eingangsbild zu klassifizieren. Wenn wir es trainieren, passen wir diese Filter so an, dass es lernt, zwischen ursprünglichen und generierten Bildern zu unterscheiden.

class Discriminator(nn.Module):
    '''
    The Discriminator that shall distinguish between dataset images and the ones generated by the generator.
    '''
    def __init__(self, number_of_gpus):
        super(Discriminator, self).__init__()
        self.ngpu = number_of_gpus

        self.layer1 = nn.Sequential(
            spectral_norm(nn.Conv2d(in_channels=3, out_channels=discriminator_depth, 
                                    kernel_size=(4,4), stride=2, padding=1, bias=False)),
            nn.LeakyReLU(0.2, inplace=True)
        )

        self.layer2 = nn.Sequential(
            spectral_norm(nn.Conv2d(in_channels=discriminator_depth, out_channels=discriminator_depth*2, 
                                    kernel_size=(4,4), stride=2, padding=1, bias=False)),
            nn.BatchNorm2d(discriminator_depth*2),
            nn.LeakyReLU(0.2, inplace=True)
        )

        self.layer3 = nn.Sequential(
            spectral_norm(nn.Conv2d(in_channels=discriminator_depth*2, out_channels=discriminator_depth*4, 
                                    kernel_size=(4,4), stride=2, padding=1, bias=False)),
            nn.BatchNorm2d(discriminator_depth*4),
            nn.LeakyReLU(0.2, inplace=True)
        )

        self.layer4 = nn.Sequential(
            spectral_norm(nn.Conv2d(in_channels=discriminator_depth*4, out_channels=discriminator_depth*8, 
                                    kernel_size=(4,4), stride=2, padding=1, bias=False)),
            nn.BatchNorm2d(discriminator_depth*8),
            nn.LeakyReLU(0.2, inplace=True)
        )

        self.layer5 = nn.Sequential(
            spectral_norm(nn.Conv2d(in_channels=discriminator_depth*8, out_channels=discriminator_depth*16, 
                                    kernel_size=(4,4), stride=2, padding=1, bias=False)),
            nn.BatchNorm2d(discriminator_depth*16),
            nn.LeakyReLU(0.2, inplace=True)
        )

        self.output_layer = nn.Sequential(
            nn.Conv2d(in_channels=discriminator_depth*16, out_channels=1, 
                                    kernel_size=(4,4), stride=1, padding=0, bias=False),
            nn.Sigmoid()
        )


    def forward(self, input_image):

        layer1 = self.layer1(input_image)
        layer2 = self.layer2(layer1)
        layer3 = self.layer3(layer2)
        layer4 = self.layer4(layer3)
        layer5 = self.layer5(layer4)
        return self.output_layer(layer5)

Der Generator hat ähnliche Filter wie der Diskriminator, nur umgekehrt. Anstatt die Bilder anzusehen, um Muster zu erkennen, gibt er Bilder zurück, die auf einem Muster basieren, das wir ihm beigebracht haben zu zeichnen. Die Eingabe besteht aus Zufallszahlen, die diese Filter aktivieren ein Bild zu zeichnen.

class Generator(nn.Module):
    '''
    The Generator Network. It is mostly a reversed discriminator with a random input noise which outputs an image.
    '''
    def __init__(self, number_of_gpus):
        super(Generator, self).__init__()
        self.ngpu = number_of_gpus

        self.layer1 = nn.Sequential(
            nn.ConvTranspose2d(in_channels=100, out_channels=generator_depth*16, 
                               kernel_size=(4,4), stride=1, padding=0, bias=False),
            nn.BatchNorm2d(num_features=generator_depth*16),
            nn.ReLU(inplace=True)
        )

        self.layer2 = nn.Sequential(
            nn.ConvTranspose2d(in_channels=generator_depth*16, out_channels=generator_depth*8, 
                               kernel_size=(4,4), stride=2, padding=1, bias=False),
            nn.BatchNorm2d(num_features=generator_depth*8),
            nn.ReLU(inplace=True)
        )

        self.layer3 = nn.Sequential(
            nn.ConvTranspose2d(in_channels=generator_depth*8, out_channels=generator_depth*4, 
                               kernel_size=(4,4), stride=2, padding=1, bias=False),
            nn.BatchNorm2d(num_features=generator_depth*4),
            nn.ReLU(inplace=True)
        )

        self.layer4 = nn.Sequential(
            nn.ConvTranspose2d(in_channels=generator_depth*4, out_channels=generator_depth*2, 
                               kernel_size=(4,4), stride=2, padding=1, bias=False),
            nn.BatchNorm2d(num_features=generator_depth*2),
            nn.ReLU(inplace=True)
        )

        self.layer5 = nn.Sequential(
            nn.ConvTranspose2d(in_channels=generator_depth*2, out_channels=generator_depth, 
                               kernel_size=(4,4), stride=2, padding=1, bias=False),
            nn.BatchNorm2d(num_features=generator_depth),
            nn.ReLU(inplace=True)
        )

        self.output_layer = nn.Sequential(
            nn.ConvTranspose2d(in_channels=generator_depth, out_channels=3, 
                               kernel_size=(4,4), stride=2, padding=1, bias=False),
            nn.Tanh()
        )


    def forward(self, input_noise):

        layer1 = self.layer1(input_noise)
        layer2 = self.layer2(layer1)
        layer3 = self.layer3(layer2)
        layer4 = self.layer4(layer3)
        layer5 = self.layer5(layer4)
        return self.output_layer(layer5)

Training

Wir können das Training in drei Teile unterteilen. 


Training des Diskriminators mit realen Bildern:

discriminator.zero_grad()

prediction = discriminator(batch)

labels_for_dataset_images = torch.ones((batch_size,), device=device).view(-1)

loss_discriminator = loss_function(prediction.view(-1), labels_for_dataset_images)
loss_discriminator.backward()

Training des Diskriminators mit den vom Generator erzeugten Bildern:

random_noise = torch.randn(batch_size,100,1,1, device=device)        
generated_image = generator(random_noise)

labels_for_generated_images = torch.zeros(np.prod(prediction.size()), device=device)

prediction = discriminator(generated_image.detach())

loss_generator = loss_function(prediction.view(-1), labels_for_generated_images)
loss_generator.backward()

discriminator_optimizer.step()

Training des Generators:

generator.zero_grad()

prediction = discriminator(generated_image).view(-1)

loss_generator = loss_function(prediction, labels_for_dataset_images)
loss_generator.backward()

generator_optimizer.step()

Ergebnisse

Regenerieren wir nur die Bilder aus dem Datensatz?

Wenn der Generator overfittet, könnten wir Bilder erhalten, die sehr ähnlich oder sogar fast identisch mit Bildern aus dem Datensatz sind. Das ist natürlich nicht das Ergebnis, das wir wollen. Deshalb testen wir, wie ähnlich unsere generierten Bilder den Bildern im Datensatz sind.


Ich benutze den k-nearest-neighbour approach. Dies ist ein Klassifizierungsalgorithmus, der nach den "nächstgelegenen" Bildern aus dem zu klassifizierenden Bild zu allen Bildern im Datensatz sucht.


In unserem Fall bedeutet die Berechnung des Abstands, jedes Pixel als Dimension zu betrachten und dann den euklidischen Abstand zwischen diesen (128x128)-dimensionalen Bildern zu berechnen.


Werfen wir einen Blick auf die nächstgelegenen oder ähnlichsten Bilder aus dem Originaldatensatz für einige unserer erzeugten Bilder:

def euclidean_distance(a, b):
    '''
    Calculates the euklidean Distance of two torch tensors of the same size.
    '''
    return torch.sqrt(((a - b) ** 2).sum())


def get_k_nearest_samples(image, k):
    '''
    Searches for the k-nearest samples in the dataset of a given image based on the euclidean distance.
    '''
    return np.argsort([euclidean_distance(image[0][0], sample[0][0]) for sample in dataset])[:k]

Die Bilder sind ähnlich wie die Datensatzbilder, aber sie sind nicht allzu ähnlich - also hat der Generator nicht overfittet.  

Fazit

Das Generative Adversarial Network hat in der Tat gelernt, wie man aus der gegebenen Datenverteilung neue Bilder generiert: Sie sind wirklich neu, weil sie nicht nur Kopien der Originalbilder sind, und lassen sich dennoch nicht von den Originalbildern unterscheiden. Also könnten wir diese neu erstellten Bilder nutzen, um ein Fehlererkennungs- oder Fehlerklassifikationsmodell zu trainieren.


Natürlich sollte man in der Praxis immer noch einmal überprüfen, ob sich die von GAN erzeugten Bilder wirklich positiv auf die Modellleistung auswirken. Dies ist möglicherweise nicht immer der Fall.


Allerdings gibt es viele potenzielle Anwendungsfälle für GANs (nicht nur) in der industriellen Produktion. Aufgrund des aktuellen Forschungsinteresses an GANs werden wir bald viele neue Erkenntnisse darüber haben, wann und wie wir sie sich optimal nutzen lassen. 


Schließlich eine kleine Warnung: GANs sind ziemlich empfindlich und kleine Änderungen der Parameter können zu verzerrten Ergebnissen führen. Außerdem ist das Training eine rechenintensive Aufgabe, da man zwei Netzwerke auf einmal trainieren muss. Ohne eine leistungsfähige GPU kommt man nicht weit.

Use Cases zu NLP

Handschriftliche Dokumente können durch Machine-Learning-Algorithmen ausgelesen und vorausgefüllt werden. Durch weitere Eingaben können diese Dokumente noch einmal editiert werden, wovon die Genauigkeit des Algorithmus noch mal profitiert.

Elektronische Patientenakten können mittels Machine Learning Algorithmen ausgewertet werden. Dadurch erhalten Ärzte automatisch Vorschläge für Diagnosen und Therapiemöglichkeiten.

Unternehmen, die mit Tausenden von Kunden und Lieferanten zusammenarbeiten, müssen ihre Dokumente kategorisieren, damit Anfragen rechtzeitig bearbeitet werden können.

Lesen Sie auch

27. Aug 2019

Künstliche Intelligenz (KI) und insbesondere Computer Vision versprechen wertvolle Hilfsmittel zur Diagnose von Krankheiten auf der... weiterlesen

12. Aug 2019

Natural Language Processing (kurz: NLP, manchmal auch Computerlinguistik) ist eines der Gebiete, das eine Revolution erfahren hat, seit Methoden des Machine Learning (ML)... weiterlesen

15. Jul 2019

In den letzten fünf bis zehn Jahren hat kaum ein Thema einen so rasanten Anstieg der Popularität erlebt wie Deep Learning. Seit 2009 hat sich die Anzahl der jährlich veröffentlichten Deep... weiterlesen


Erfahren Sie, was dida für Sie tun kann