Benutzer-Werkzeuge

Webseiten-Werkzeuge


Seitenleiste

ss19:eigenfaces

Eigengesichter

Analysieren und Generieren von menschlichen Gesichtern mit Hilfe eines Autoencoder-Netzwerkes und einer Hauptkomponentenanalyse.

Code: https://git.tu-berlin.de/labo/eigenfaces

Gruppe: Lars Bonczek

Idee

Dieses Projekt ist inspiriert von Videos verschiedener YouTube-Kanäle, so etwa CodeParade (https://youtu.be/4VAkrUNLKSo) und carykh (https://youtu.be/NTlXEJjfsQU). Bei der Umsetzung habe ich mich aber nicht an diesen Vorbildern orientiert.

Das Ziel meines Projektes war es in erster Linie, eine sogenannte Hauptkomponentenanalyse zu verwenden, um die wichtigsten Merkmale eines menschlichen Gesichtes zu isolieren. Mir war aber schon zu Beginn bewusst, dass eine Hauptkomponentenanalyse alleine für diesen Zweck nur bedingt geeignet ist. Also plante ich, zusätzlich ein Convolutional Neural Network zu entwerfen und trainieren, welches Merkmale aus Bildern menschlicher Gesichter extrahieren kann und aus diesen Merkmalen auch wieder entsprechende Bilder generieren kann. Diese Art von Netzwerk nennt man Autoencoder.

Hauptkomponentenanalyse

Abbildung 1 Die Hauptkomponentenanalyse (englisch Pricipal Component Analysis, kurz PCA) ist ein Verfahren aus der Linearen Algebra, bei dem für einen Vektorraum eine neue Basis ermittelt wird, sodass die neuen Basis-Vektoren die Achsen der größten Ausdehnung (Hauptkomponenten) einer Punktwolke in dem Vektorraum beschreiben (siehe Abbildung 1).

Diese Basis-Vektoren lassen sich bestimmen, indem man die Eigenvektoren der Kovarianzmatrix der Punktwolke berechnet. Dies geht wie folgt:

  1. Berechne den Mittelpunkt $\vec{m}$ der Punktwolke, wobei $n$ die Anzahl der Vektoren beschreibt und $\vec{v}_1, \vec{v}_2, \dots, \vec{v}_n \in\mathbb{R}^k$ die Vektoren in der Punktwolke: $$\vec{m} = \frac{1}{n} \sum^n_{i=1} \vec{v}_i$$
  2. Berechne für jeden Vektor $\vec{v}_i$ die Differenz zum Mittelpunkt $\vec{\delta}_i = \vec{v}_i-\vec{m}$.
  3. Berechne die Kovarianzmatrix $C\in\mathbb{R}^{k,k}$ mit $$C=\frac{1}{n}\sum^n_{i=1}\vec{\delta}_i\vec{\delta}_i^T=\Delta\Delta^T,$$ wobei $\Delta = \left\{ \vec{\delta}_1,\vec{\delta}_2,\dots,\vec{\delta}_n \right\} \in\mathbb{R}^{k,n}$.
  4. Berechne die Eigenvektoren $\vec{u}_i$ und Eigenwerte $\lambda_i$ der Kovarianzmatrix $C$, also die Lösungen der Gleichung $$C\vec{u}_i=\lambda_i\vec{u}_i.$$ Die Eigenvektoren sind die gesuchten Hauptkomponenten. Es gilt: Je höher der zugehörige Eigenwert, desto wichtiger die Hauptkomponente.

Führt man eine Hauptkomponentenanalyse für Bilder von menschlichen Gesichtern durch, so nennt man die ermittelten Hauptkomponenten auch „Eigengesichter“, da sie menschlichen Gesichtern ähneln. Den durch die Hauptkomponenten aufgespannten Vektorraum nennt man „feature space“.

Alternative Version

Wenn die Hauptkomponentenanalyse auf Bilder der Größe $h\times w$ mit drei Farbkanälen angewendet wird, gilt für die Anzahl der Dimensionen $k$ eines jeden Vektors $k=3hw$. Für normale Bildgrößen ist die Durchführung des 3. Schrittes des o. g. Algorithmus somit sehr aufwändig, da die Berechnung $C=\Delta\Delta^T$ mit $\Delta\in\mathbb{R}^{k,n}$ die riesige Matrix $C\in\mathbb{R}^{k,k}$ ergibt. Schon für Bilder der Größe $200\times 200$ hat die Kovarianzmatrix $C$ ganze 14,4 Milliarden Einträge.

In solchen Fällen, in denen $k\gg n$, empfiehlt sich also die folgende Alternative für den Algorithmus oben:

  1. s. o.
  2. s. o.
  3. Berechne die Hilfsmatrix $L\in\mathbb{R}^{n,n}$ mit $L=\Delta^T\Delta$.
  4. Berechne die Eigenvektoren $\vec{u}'_i$ und Eigenwerte $\lambda_i$ der Hilfsmatrix $L$ gemäß $$L\vec{u}'_i=\lambda_i\vec{u}'_i.$$ Die Eigenwerte der Hilfsmatrix entsprechen auch den gesuchten Eigenwerten der Kovarianzmatrix $C$. Die Eigenvektoren von $C$ berechnen sich durch $$U=\Delta U' \Lambda^{-0,5},$$ wobei $U'=\left\{\vec{u}'_i\right\}$ die Eigenvektoren und $\Lambda=diag\{\lambda_i\}$ die Eigenwerte von $L$ enthält. $U=\left\{\vec{u}_i\right\}$ enthält nun die Eigenvektoren von $C$ und somit die gesuchten Hauptkomponenten.

Autoencoder

Abbildung 2 Ein Autoencoder ist ein neuronales Netzwerk, das ohne menschliche Unterstützung eine effiziente Kodierung von sich ähnlichen Daten lernen kann. Alles was man dazu benötigt sind viele Beispiele von der Art Daten, die später kodiert werden soll.

Ein Autoencoder besteht aus zwei Teilen, dem Encoder und dem Decoder. Diese überschneiden sich in der Mitte des Netzwerkes in der Schicht, die nachher die kodierten Daten enthält (siehe Abbildung 2). Diese Schicht hat die Funktion eines „Flaschenhalses“, sie ist signifikant kleiner als die Eingabe- und Ausgabeschicht des Autoencoders. Zum Training erhält das Netzwerk nun Datenvektoren. Der „Flaschenhals“ in der Mitte zwingt den Encoder-Teil nun dazu, diese Vektoren zu abstrahieren und durch kleinere Code-Vektoren darzustellen. Der Decoder-Teil hat die Aufgabe, diese Code-Vektoren zu dekodieren und dabei so nah wie möglich an die originalen Datenvektoren zu kommen. Trainiert man dieses Netzwerk mit ausreichend vielen Beispielen, so wird es mit der Zeit immer besser darin, die Originale wiederherzustellen. Möchte man den Autoencoder nun nur zum Kodieren oder nur zum Dekodieren verwenden, kann man einfach nur den Encoder- bzw. Decoder-Teil alleine verwenden.

Autoencoder eignen sich sehr gut dafür, Merkmale von Gesichtern zu extrahieren, da ein Bild eines Gesichts sich sehr viel platzsparender beschreiben lässt, wenn man nicht die RGB-Werte aller Pixel speichert, sondern nur die Merkmale, die ein bestimmtes Gesicht von anderen Gesichtern unterscheidet. Wenn man also die Struktur des Netzwerkes und die Größe des „Flaschenhalses“ geschickt wählt, sollte der Autoencoder automatisch lernen, in den Code-Vektoren die Merkmale der Gesichter abzubilden.

Convolutional Neural Networks

In diesem Projekt sollen Bilder kodiert werden, deshalb empfiehlt sich ein sogenanntes Convolutional Neural Network (kurz CNN). Diese Art von Netzwerk ist gut zur Analyse von Bildern geeignet, da in den ersten Schichten des Netzwerkes zunächst kleine Ausschnitte des vorliegenden Bildes unabhängig voneinander verarbeitet werden. Erst in späteren Schichten werden diese Ausschnitte dann zusammengeführt, sodass das Netzwerk mit jeder Convolution-Schicht immer größere und komplexere Strukturen des Bildes erkennen kann, ohne von Beginn an das Bild in seiner Gesamtheit betrachten zu müssen. Dies spart viele Ressourcen und erzielt trotzdem im Allgemeinen sehr gute Ergebnisse.

Abbildung 3

Ein CNN besteht im Allgemeinen aus drei Arten von Schichten: Convolutions, Subsampling- bzw. Pooling-Schichten und herkömmlichen Fully-connected-Schichten (siehe Abbildung 3).

Convolutions behalten die Breite und Höhe der dreidimensionalen Neuronen-Matrix, die sie erhalten, bei, und verändern nur die Tiefe. Sie verfügen über einen Filter-Kernel, der eine beliebige Anzahl an Filtern auf jeden Punkt der erhaltenen Matrix inklusive seiner lokalen Umgebung anwendet. Das Ergebnis für jeden Filter wird an dem selben Punkt in die ausgegebene Matrix geschrieben, sodass die Tiefe der ausgegebenen Matrix der Anzahl an Filtern entspricht, die Breite und Höhe aber unverändert bleibt.

Pooling-Schichten reduzieren die Breite und Höhe der erhaltenen Matrix. Sie tun dies, indem sie benachbarte Punkte der Matrix zusammenfassen. Beim Max-Pooling wird dazu für jedes 2×2 Quadrat nur der größte Wert in die ausgegebene Matrix geschrieben. So halbieren sich Höhe und Breite der Matrix, die Tiefe bleibt aber unverändert. Dies hat den Zweck, überflüssige Daten zu verwerfen, sodass spätere Convolutions größere Strukturen erkennen können.

Ein CNN besteht aus einer Abfolge von je einer oder mehreren Convolutions, gefolgt von je einer Pooling-Schicht. Ist die Größe der Neuronen-Matrix ausreichend reduziert, folgen noch eine oder mehrere Fully-connected-Schichten, um die Daten abschließend auszuwerten und beispielsweise zu kategorisieren. Nach jeder Schicht folgt meist die ReLU-Aktivierungsfunktion, die definiert ist als $f(x)=\text{max}(0,x)$.

Planung/Verlauf

Protokoll in Stichpunkten: eigenfaces_protokoll.pdf

Mein erster Schritt war es, eine einfache Hauptkomponentenanalyse von Porträtfotos durchzuführen. Dafür suchte ich mir im Internet einen öffentlich zugänglichen Datensatz von Fotos, die von einer Universität zu Forschungszwecken veröffentlicht wurden. Dann implementierte ich die Hauptkomponentenanalyse in Python. Dabei unterliefen mir einige Fehler, sodass die Ergebnisse zunächst nicht meinen Erwartungen entsprachen.

Die ersten fünf Bilder des Utrecht ECVP Datensatzes:

Abbildung 4

Die ersten fünf berechneten Eigengesichter (mit Fehler in der Berechnung):

Abbildung 5

Die ersten fünf berechneten Eigengesichter (immer noch fehlerhaft):

Abbildung 6

Die ersten fünf Eigengesichter (korrekt berechnet):

Abbildung 7

Die ersten fünf Bilder des Datensatzes nach Abbildung in den „feature space“ mit 95% der Kovarianz:

Abbildung 8

Das folgende Bild stammt auch aus dem Datensatz, war aber nicht Teil der Hauptkomponentenanalyse. Man sieht daran, dass die Abstraktion nicht besonders gut funktioniert hat. Links ist das Original zu sehen, rechts die Rekonstruktion:

Abbildung 9

Nachdem die Hauptkomponentenanalyse einigermaßen funktionierte, entwickelte ich eine kleine Webanwendung, die es dem Benutzer ermöglicht, den Bildvektor im „feature space“ mit Hilfe von Schiebereglern zu manipulieren. Diese Webanwendung lief vollständig im Browser des Benutzers, beim Laden der Seite wurden alle Eigenvektoren vom Server heruntergeladen. Für das Laden der Dateien verwendete ich das Skript loadnpy.js. Für die nötigen Berechnungen verwendete ich die Bibliothek numjs.

So sah diese Anwendung aus (der verwendete Datensatz war hier ein anderer):

Abbildung 10

Das Ergebnis war für mich nicht zufriedenstellend. Da hier jedes dargestellte Gesicht nur eine Linearkombination der ermittelten Hauptkomponenten ist, können die Schieberegler nur lineare Veränderungen an dem Bild vornehmen. Eine Translation, Rotation oder Skalierung des Bildes sind aber keine linearen Veränderungen des Bildvektors. Ich wollte jedoch zum Beispiel die Möglichkeit haben, mit einem Regler die Größe des Gesichts oder die Position des Mundes verändern zu können. Also machte ich mich daran, einen Autoencoder zu Entwerfen und Trainieren. Dieser würde theoretisch beliebig komplexe Merkmale isolieren können.

Nun stand ich vor dem Problem, dass ich keinen ausreichend großen Datensatz von Bildern für das Training finden konnte. Es gibt zwar riesige Datensätze von Bildern, auf denen Gesichter zu sehen sind, die Position und Neigung des Gesichtes variieren dann aber zu stark, um ein Netzwerk zu trainieren. Also schrieb ich ein Skript, welches Gesichter in Bildern sucht und die Bilder dann so rotiert und zuschneidet, dass alle Bilder gleich groß sind und die Augen sich immer an derselben Stelle befinden. Dafür habe ich die Face Recognition Bibliothek verwendet. Alle Bilder wurden auf 224×224 Pixel Größe zugeschitten, da das Netzwerk, welches ich mit den ausgerichteten Bildern trainieren wollte, nur Bilder dieser Größe akzeptiert. Die Bilder wurden außerdem so skaliert, dass der Abstand der Augen immer bei 56 Pixeln liegt.

Ein beispielhaftes Bild, vor und nach der Ausrichtung:

Abbildung 11

Bilder, die zu niedrig aufgelöst sind, oder wo sich das Gesicht zu nah am Rand des Bildes befindet, werden von dem Skript automatisch verworfen. Es gibt noch weitere Kriterien, nach denen Bilder aussortiert werden. So werden die Abstände der beiden Augen zur Nase verglichen. Diese sollten nicht zu verschieden sein, da die Person sonst vermutlich nach links oder rechts schaut. Außerdem werden Bilder verworfen, die unscharf oder monochrom sind. Dies wird im Abschnitt aligner.py näher beschrieben.

Mithilfe dieses Skripts wandelte ich nun eine Datenbank mit über hundert Gigabyte an Bildern von berühmten Persönlichkeiten aus der Google Bildersuche in einen brauchbaren Datensatz mit gut 60.000 ausgerichteten Bildern um. Also galt es nun, mit diesen Bildern ein Netzwerk zu trainieren. Zunächst versuchte ich, ein eigenes Autoencoder-Netzwerk von Grund auf zu entwerfen und trainieren. Dies scheiterte aber an meiner fehlenden Erfahrung mit Neuronalen Netzwerken und schlicht an fehlender Rechenleistung. Deswegen entschied ich mich dafür, ein bewährtes CNN zu verwenden, das auf die Klassifizierung von Bildern trainiert wurde, und dieses für meine Zwecke anzupassen und weiter zu trainieren. Meine Wahl viel nach einiger Recherche auf das VGG16-Netzwerk. Dabei handelt es sich um ein CNN mit 13 Convolutions und 3 Fully-connected-Schichten (siehe Abbildung 11), welches bei der ImageNet Large Scale Visual Recognition Challenge 2014 eingereicht wurde und eine Genauigkeit von 92,7% bei der top-5 Klassifizierung der Bilder aus der ImageNet-Datenbank erreichte (Simonyan & Zisserman, 2014).

Abbildung 12

Um aus diesem Klassifizierungs-Netzwerk einen Autoencoder zu machen, musste ich das Netzwerk einfach an der Ausgabeschicht spiegeln. Die Ausgabeschicht wurde so der „Flaschenhals“ des Autoencoders. Das originale VGG16-Netzwerk hat allerdings eine Softmax-Aktivierungsfunktion als Ausgabeschicht. Die Softmax-Funktion sorgt dafür, dass die Aktivitätslevel aller Neuronen der Ausgabeschicht zwischen null und eins liegen und dass deren Summe eins ist. Dies ist für Klassifizierungsaufgaben essentiell, da so das Aktivitätslevel jedes einzelnen Ausgabe-Neurons als die Wahrscheinlichkeit interpretiert werden kann, mit der das betrachtete Bild zu der entsprechenden Klasse gehört. Die Summe aller Wahrscheinlichkeiten muss natürlich eins, also 100%, sein. In meinem Fall gab es aber keinen Grund dafür, deshalb verwendete ich stattdessen eine ReLU-Aktivierungsfunktion. Nur als Ausgabeschicht meines Autoencoders verwendete ich nicht ReLU sondern die Sigmoid-Aktivierungsfunktion, die nur Werte zwischen null und eins ausgibt. So konnte ich die Ausgabe mit 255 multiplizieren und sofort als gültiges Bild verwenden. Dies ist die Struktur des Netzwerkes, welches ich letztendlich trainierte:

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 224, 224, 3)       0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 224, 224, 64)      1792      
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 224, 224, 64)      36928     
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 112, 112, 64)      0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 112, 112, 128)     73856     
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 112, 112, 128)     147584    
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 56, 56, 128)       0         
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 56, 56, 256)       295168    
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 56, 56, 256)       590080    
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 56, 56, 256)       590080    
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 28, 28, 256)       0         
_________________________________________________________________
conv2d_8 (Conv2D)            (None, 28, 28, 512)       1180160   
_________________________________________________________________
conv2d_9 (Conv2D)            (None, 28, 28, 512)       2359808   
_________________________________________________________________
conv2d_10 (Conv2D)           (None, 28, 28, 512)       2359808   
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 14, 14, 512)       0         
_________________________________________________________________
conv2d_11 (Conv2D)           (None, 14, 14, 512)       2359808   
_________________________________________________________________
conv2d_12 (Conv2D)           (None, 14, 14, 512)       2359808   
_________________________________________________________________
conv2d_13 (Conv2D)           (None, 14, 14, 512)       2359808   
_________________________________________________________________
max_pooling2d_5 (MaxPooling2 (None, 7, 7, 512)         0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 25088)             0         
_________________________________________________________________
dense_1 (Dense)              (None, 4096)              102764544 
_________________________________________________________________
dense_2 (Dense)              (None, 4096)              16781312  
_________________________________________________________________
dense_3 (Dense)              (None, 1000)              4097000   
_________________________________________________________________
dense_4 (Dense)              (None, 4096)              4100096   
_________________________________________________________________
dense_5 (Dense)              (None, 4096)              16781312  
_________________________________________________________________
dense_6 (Dense)              (None, 25088)             102785536 
_________________________________________________________________
reshape_1 (Reshape)          (None, 7, 7, 512)         0         
_________________________________________________________________
up_sampling2d_1 (UpSampling2 (None, 14, 14, 512)       0         
_________________________________________________________________
conv2d_14 (Conv2D)           (None, 14, 14, 512)       2359808   
_________________________________________________________________
conv2d_15 (Conv2D)           (None, 14, 14, 512)       2359808   
_________________________________________________________________
conv2d_16 (Conv2D)           (None, 14, 14, 512)       2359808   
_________________________________________________________________
up_sampling2d_2 (UpSampling2 (None, 28, 28, 512)       0         
_________________________________________________________________
conv2d_17 (Conv2D)           (None, 28, 28, 512)       2359808   
_________________________________________________________________
conv2d_18 (Conv2D)           (None, 28, 28, 512)       2359808   
_________________________________________________________________
conv2d_19 (Conv2D)           (None, 28, 28, 256)       1179904   
_________________________________________________________________
up_sampling2d_3 (UpSampling2 (None, 56, 56, 256)       0         
_________________________________________________________________
conv2d_20 (Conv2D)           (None, 56, 56, 256)       590080    
_________________________________________________________________
conv2d_21 (Conv2D)           (None, 56, 56, 256)       590080    
_________________________________________________________________
conv2d_22 (Conv2D)           (None, 56, 56, 128)       295040    
_________________________________________________________________
up_sampling2d_4 (UpSampling2 (None, 112, 112, 128)     0         
_________________________________________________________________
conv2d_23 (Conv2D)           (None, 112, 112, 128)     147584    
_________________________________________________________________
conv2d_24 (Conv2D)           (None, 112, 112, 64)      73792     
_________________________________________________________________
up_sampling2d_5 (UpSampling2 (None, 224, 224, 64)      0         
_________________________________________________________________
conv2d_25 (Conv2D)           (None, 224, 224, 64)      36928     
_________________________________________________________________
conv2d_26 (Conv2D)           (None, 224, 224, 3)       1731      
=================================================================
Total params: 276,738,667
Trainable params: 262,023,979
Non-trainable params: 14,714,688
_________________________________________________________________

Dieses Netzwerk kann mit dem folgenden Befehl erzeugt werden:

python autoencoder.py --network-pattern="conv(3,3,64)>conv(3,3,64)>maxpool(2,2)>conv(3,3,128)>conv(3,3,128)>maxpool(2,2)>conv(3,3,256)>conv(3,3,256)>conv(3,3,256)>maxpool(2,2)>conv(3,3,512)>conv(3,3,512)>conv(3,3,512)>maxpool(2,2)>conv(3,3,512)>conv(3,3,512)>conv(3,3,512)>maxpool(2,2)>flatten()>dense(4096)>dense(4096)>dense(1000)" --shape=224,224,3 --out=<out_dir>

Dabei ist <out_dir> das Ausgabeverzeichnis, in dem die Netzwerk-Datei gespeichert wird. Weitere Information zu diesem Befehl gibt es im Abschnitt autoencoder.py.

Um die Gewichte des VGG16-Netzwerkes zu laden, benutzte ich das Skript pretrain_vgg16.py mit den Parametern pretrained_layers = 18 und locked_layers = 15.

Dieses Netzwerk trainierte ich zunächst mit einem kleinen Teil der Trainingsdaten und, nachdem ich vielversprechende Ergebnisse erhielt, mit allen 63.943 Bildern. Davon verwendete ich 20%, also 12788 Bilder, zur Validierung. Auf den folgenden Bildern ist links ein Originalbild zu sehen und rechts die Rekonstruktion des Autoencoders nach 50 Epochen Training:

Abbildung 13 Abbildung 14 Abbildung 15 Abbildung 16

Wie im letzten Beispiel zu erkennen ist, gab es unter den ausgerichteten Bildern auch einige Bilder, die eigentlich keine menschlichen Gesichter enthalten, aber trotzdem so erkannt wurden. Dies kann zu interessanten Ergebnissen führen.

Für das Trainieren verwendete ich Google Colab. Eigentlich ist dieser Dienst nicht dafür gedacht, große Netzwerke lange zu trainieren, deswegen hatte ich auch einige Probleme damit. Man kann seinen Code zum Beispiel nicht länger als 12 Stunden am Stück laufen lassen, und selbst das klappt nur manchmal. Deswegen war ich schnell dazu gezwungen mit Checkpoints zu arbeiten und den Vorgang regelmäßig neuzustarten. Die Ergebnisse sicherte ich direkt in meinem Google Drive. Dummerweise kam ich erst sehr spät auf die Idee, auch den Verlauf des Trainings in einer Log-Datei auf Google Drive zu sichern. So ging der Verlauf der ersten Anläufe leider verloren. Dieses Diagramm zeigt den Verlauf des Trainings eines späteren Anlaufes:

Abbildung 17

Die ursprünglich entwickelte Webanwendung konnte ich an diesem Punkt nicht mehr verwenden. Bisher hatte die Anwendung ja alle Bilder auf der Seite des Benutzers im Browser berechnet. Dies war nun nicht mehr möglich, da der Browser die Kodierung und Dekodierung der Bilder mithilfe des Autoencoders hätte vornehmen müssen. Also entschied ich mich dazu, die Anwendung so umzugestalten, dass der Browser nur die Werte der Schieberegler an den Server sendet und dieser dann das Bild dazu generiert. Dazu verwendete ich das Framework Flask und für die Darstellung im Browser noch Bootstrap und jQuery. Die neue Version der Webanwendung sah dann so aus:

Abbildung 18

Eine neue Funktion dieser Version ist die Möglichkeit, ein eigenes Bild hochzuladen. Der Server sucht dann nach einem Gesicht in dem Bild, richtet dieses aus und versucht es möglichst gut zu rekonstruieren. Außerdem gibt es nun die Möglichkeit, zufällige Gesichter zu generieren. Dabei wird die Box-Muller-Methode eingesetzt, sodass die zufällig erzeugten Gesichter normalverteilt sind und dieselbe Standardabweichung aufweisen wie die Trainingsdaten. Die Färbung der Schieberegler zeigt jeweils die entsprechende Standardabweichung. In dem obigen Beispiel entspricht der grüne, der gelbe und der rote Bereich jeweils drei Standardabweichungen.

An diesem Punkt musste ich mein Projekt aus zeitlichen Gründen zu einem Ende bringen. Im Folgenden werden noch die verschiedenen Komponenten des finalen Standes beschrieben.

Ergebnis

Das Ergebnis dieses Projektes ist eine Reihe von Python-Scripts, die verwendet werden können, um die folgenden Operationen durchzuführen:

  • Ausschneiden von Gesichtern aus beliebigen Bildern (aligner.py)
  • Gruppieren von Bildern nach Personen (grouper.py)
  • Erstellen und Trainieren eines Autoencoders und Kodieren und Dekodieren von Bildern mit diesem Autoencoder (autoencoder.py)
  • Durchführung einer Hauptkomponentenanalyse über eine Liste von Bildern oder Numpy-Arrays (pca.py)

Außerdem gibt es eine Webanwendung, die für eine beliebige Gesichts-Kodierung das entsprechende dekodierte Gesicht anzeigen kann. Die Parameter des kodierten Gesichts können mit Schiebereglern angepasst werden, sodass man in Echtzeit den Einfluss der Parameter auf das dekodierte Gesicht sieht.

Der aktuelle Code des Projektes ist auf GitLab zu finden und kann dort auch als ZIP-Datei heruntergeladen werden.

Die zuletzt von mir trainierte Version des Autoencoders (trainiert mit gut 64.000 Bildern) inklusive der Ergebnisse der Hauptkomponentenanalyse kann hier heruntergeladen werden. Mit diesem Paket kann die Webanwendung ohne weitere Vorbereitungen (außer der Anpassung der Konfigurationsdatei) verwendet werden. Ein neuer Trainingsdatensatz mit gut 140.000 Bildern kann hier heruntergeladen werden.

aligner.py

Das Skript aligner.py findet in einer Auswahl von unbekannten Bildern menschliche Gesichter und schneidet diese aus. Dabei werden die Bilder so skaliert und rotiert, dass Höhe und Breite der resultierenden Bilder den festgelegten Werten entsprechen, und sodass sich die Augen der Personen in einem festgelegten Abstand in der Mitte der Bilder befinden. Außerdem werden Bilder aussortiert, die monochrom, unscharf oder zu niedrig aufgelöst sind. Der ganze Vorgang kann auf mehrere, parallel laufende Prozesse aufgeteilt werden.

Berechnung der Schärfe

Die Schärfe eines Bildes wird durch die Formel $$LAP\_VAR(I)=\sum_m^M\sum_n^N\left(\left|L(m,n)\right|-\bar{L}\right)^2$$ berechnet, wobei $\bar{L}$ definiert ist als $$\bar{L}=\frac{1}{NM}\sum_m^M\sum_n^N\left|L(m,n)\right|$$ und $L(m,n)$ die Convolution des Bildes mit der Laplace'schen Maske $$L=\frac{1}{6} \left(\begin{array}{rrr} 0 & -1 & 0 \\ -1 & 4 & -1 \\ 0 & -1 & 0 \\ \end{array}\right)$$ ist (Pech-Pacheco, Cristóbal, Chamorro-Martínez & Fernández-Valdivia, 2000). Dies ist wie folgt implementiert:

def get_sharpness(image):
    return np.var(np.abs(ndimage.laplace(np.mean(image, axis=2))))

Berechnung der Farbigkeit

Um zu ermitteln, ob ein Bild monochrom ist, wird das Bild zunächst in einen neuen Farbraum übertragen. \begin{align*} rg \;&=\; R-G\\ yb \;&=\; \frac{1}{2}(R+G)-B \end{align*} Nun kann die Farbigkeit des Bildes nach der Formel \begin{align*} \hat{M}^{(3)} \;&=\; \sigma_{rgyb}+0.3*\mu_{rgyb},\\ \sigma_{rgyb} \;&:=\; \sqrt{\sigma_{rg}^2+\sigma_{yb}^2},\\ \mu_{rgyb} \;&:=\; \sqrt{\mu_{rg}^2+\mu_{yb}^2}, \end{align*} berechnet werden, wobei $\sigma_{rg}$ und $\mu_{rg}$ die Standardabweichung und der Mittelwert von $rg$ sind und $\sigma_{yb}$ und $\mu_{yb}$ die Standardabweichung und der Mittelwert von $yb$ (Hassler & Süsstrunk, 2003). Dies ist wie folgt implementiert:

def get_color_level(image):
    r = image[:,:,0].flatten().astype('int32')
    g = image[:,:,1].flatten().astype('int32')
    b = image[:,:,2].flatten().astype('int32')

    rg = np.absolute(r - g)
    yb = np.absolute(0.5 * (r + g) - b)

    rg_mean = np.mean(rg)
    rg_std = np.std(rg)
    yb_mean = np.mean(yb)
    yb_std = np.std(yb)

    std = np.sqrt((rg_std ** 2) + (yb_std ** 2))
    mean = np.sqrt((rg_mean ** 2) + (yb_mean ** 2))

    return std + (0.3 * mean)

Verwendung

python aligner.py [options] <target_height> <target_width> <target_eye_width> <image_file_pattern> <out_dir>

Durchsucht alle Bild-Dateien, die durch das Muster image_file_pattern beschrieben werden. Das Muster darf die Wildcards * und ? enthalten, in einer Linux-Shell müssen diese aber escaped werden (z.B. files/\*.png). Alle gefundenen Gesichter werden in dem Verzeichnis out_dir gespeichert.

Verfügbare Optionen:

  • --processes=<nr>
    • Legt die Anzahl der Prozesse fest, die parallel arbeiten sollen. Standard: $4$
  • --skip=<n>
    • Überspringt die ersten $n$ Dateien. Kann verwendet werden, um einen abgebrochenen Vorgang fortzusetzen.
  • --max-nose-ratio=<value>
    • Legt das maximale Verhältnis zwischen den Abständen der beiden Augen zur Nase einer Person fest. Standard: $1.7$
  • --min-color=<value>
  • --min-sharpness=<value>

grouper.py

Das Skript grouper.py gruppiert Bilder, die Gesichter enthalten, nach Ähnlichkeit. Im Optimalfall werden so alle Bilder von derselben Person einer Gruppe zugeordnet. Damit nicht alle Bilder zuerst analysiert und dann untereinander verglichen werden müssen (wie etwa beim Single-/Full-Linkage oder K-means Clustering) ist der Gruppierungsalgorithmus sehr primitiv:

  1. Lade das nächste Bild und berechne das Gesichts-Encoding $\vec{v}$ (den Gesichts-„Fingerabdruck“).
  2. Suche unter den bereits existierenden Gruppen $\mathcal{G}$ die Gruppe $G\in\mathcal{G}$, deren durchschnittliches Gesichts-Encoding $$\vec{v}_G=\frac{1}{\left|G\right|}\sum_{\vec{g}\in G}\vec{g}$$ den geringsten Abstand zu $\vec{v}$ hat: $$G=\underset{A\in\mathcal{G}}{arg\,min} \left|\vec{v}_A - \vec{v}\right|$$
  3. Falls der kleinste Abstand kleiner oder gleich der Toleranz $t$ ist, also $\left|\vec{v}_G - \vec{v}\right| \leq t$, ordne das Gesicht $\vec{v}$ der Gruppe $G$ zu. Ansonsten ordne $\vec{v}$ einer neuen, eigenen Gruppe zu.
  4. Gehe zurück zu Schritt 1, bis alle Bilder gruppiert wurden.

Dieser Algorithmus ist zwar sehr effizient, da jedes Bild nur einmal betrachtet werden muss und sofort einer Gruppe zugewiesen werden kann, allerdings hängt das Ergebnis dieses Algorithmus stark von der Reihenfolge ab, in der die Bilder betrachtet werden. Da dieses Skript aber letztendlich sowieso keine wichtige Rolle gespielt hat, habe ich es so belassen.

Verwendung

python grouper.py [options] <image_file_pattern> <out_dir>

Gruppiert alle Bild-Dateien, die durch das Muster image_file_pattern beschrieben werden. Das Muster darf die Wildcards * und ? enthalten, in einer Linux-Shell müssen diese aber escaped werden (z.B. files/\*.png). Die gruppierten Gesichter werden in dem Verzeichnis out_dir gespeichert.

Verfügbare Optionen:

  • --tolerance=<value>
    • Legt die Toleranz beim Vergleich von Gesichtern fest. Weniger Toleranz bedeutet, dass zwei Gesichter sich ähnlicher sein müssen, um derselben Person zugeordnet zu werden. Standard: $0.6$
  • --move
    • Verschiebe die Dateien, statt sie zu kopieren.
  • --group-in-dirs
    • Erstelle einen eigenen Unterordner für jede Person.
  • --save-face-encodings
    • Speichere das durchschnittliche Encoding jeder Person am Ende des Vorgangs.

autoencoder.py

Das Skript autoencoder.py kann zum Erstellen und Trainieren eines Autoencoders und zum Kodieren und Dekodieren von Bildern mit diesem Autoencoder verwendet werden.

Verwendung

python autoencoder.py [-s] [-t] [-d] [-e] [options] [--out=<out_dir>]

Mit den Optionen -s, -t, -e und -d können die Operationen gewählt werden, die durchgeführt werden sollen.

  • -s gibt eine Zusammenfassung des geladenen Autoencoders aus.
  • -t trainiert den geladenen Autoencoder.
  • -e kodiert die geladenen Bilder mit dem geladenen Autoencoder.
  • -d dekodiert die geladenen Bilder mit dem geladenen Autoencoder.

Die Option --out=<out_dir> gibt das Ausgabeverzeichnis an. An diesem Ort werden alle Dateien abgelegt, die während der Durchführung der gewünschten Operation entstehen.

Optionen zum Laden eines Autoencoders:

  • --network-file=<network-file>
    • Lädt den Autoencoder aus der angegebenen .h5-Datei.
  • --recompile
    • Gibt an, dass das Netzwerk mit den Standardeinstellungen neu kompiliert werden soll. Dadurch geht der Zustand des Optimizers verloren, dies sollte also nicht verwendet werden, falls ein abgebrochener Trainingsvorgang fortgesetzt werden soll.
  • --network-pattern=<network_layout>
    • Erstellt einen neuen Autoencoder mit der angegebenen Struktur und speichert ihn ggf. in dem durch --out angegebenen Verzeichnis. Die Struktur-Syntax wird hier näher beschrieben.
  • --shape=<height>,<width>,<depth>
    • Gibt das Format der Bilder an, die der neu erstellte Autoencoder akzeptieren soll. Alternativ können auch mit --images die Trainingsdaten angegeben werden (s.u.), das Programm erkennt das Format dann automatisch.

Optionen zum Kompilieren eines Autoencoders:

  • --optimizer=<optimizer>
    • Legt den neuen Optimizer fest. Verfügbare Werte: sgd, adam und rmsprop
  • --lr=<learn_rate>
    • Legt die learn_rate des neuen Optimizers fest.
  • --momentum=<momentum>
    • Legt das momentum des neuen Optimizers fest.
  • --decay=<decay>
    • Legt den decay des neuen Optimizers fest.
  • --nesterov
    • Aktiviert die nesterov-Option für den neuen Optimizer.
  • --loss=<loss_function>
    • Legt die loss-Funktion fest. Standard: mean_squared_error

Optionen zum Laden von (kodierten) Bildern:

  • --images=<img_file_pattern>
    • Lädt alle Bild-Dateien zum Trainieren oder Kodieren, die durch das Muster img_file_pattern beschrieben werden. Das Muster darf die Wildcards * und ? enthalten, in einer Linux-Shell müssen diese aber escaped werden (z.B. files/\*.png).
  • --encoded=<npy_file_pattern>
    • Lädt alle .npy-Dateien zum Dekodieren, die durch das Muster npy_file_pattern beschrieben werden. Das Muster darf die Wildcards * und ? enthalten, in einer Linux-Shell müssen diese aber escaped werden (z.B. files/\*.npy).

Optionen zum Training:

  • --batch-size=<batch_size>
    • Gibt die batch_size für das Training an, d.h. wie viele Bilder gleichzeitig verarbeitet werden. Eine höhere batch_size beschleunigt das Training, benötigt aber mehr Arbeitsspeicher.
  • --epochs=<epochs>
    • Gibt an, wie viele Epochen trainiert werden soll. Das Training kann aber auch zwischendurch unterbrochen werden, falls --checkpoint aktiviert ist.
  • --checkpoint
    • Legt fest, dass das Netzwerk nach jeder Epoche, in der es sich verbessert hat (also val_loss gesunken ist), gespeichert werden soll.
  • --validation-images=<file_pattern>
    • Verwendet alle Bild-Dateien zur Validierung während des Trainings, die durch das Muster img_file_pattern beschrieben werden. Das Muster darf die Wildcards * und ? enthalten, in einer Linux-Shell müssen diese aber escaped werden (z.B. files/\*.png). Falls diese Option nicht verwendet wird, wird stattdessen ein bestimmter Anteil der Trainingsdaten zur Validierung verwendet. Dieser Anteil kann mit der Option --validation-split angepasst werden.
  • --validation-split=<validation_split>
    • Gibt an, welcher Anteil der Trainingsdaten zur Validierung verwendet werden soll, falls --validation-images nicht verwendet wird. Achtung: Diese Option sollte nicht verwendet werden, falls es möglich sein soll, das Training fortzusetzen, da die Daten zur Validierung so jedes Mal neu zufällig aus den Trainingsdaten gewählt werden. Standard: $0.2$
  • --workers=<nr>
    • Die Anzahl der Prozesse, die zum Training eingesetzt werden sollen. Standard: $6$
  • --show-train-history
    • Zeige nach dem abgeschlossenen Training einen Graphen an, der den Verlauf des Trainings- und Validierungsverlustes anzeigt.
  • --save-train-history
    • Speichert den Trainingsverlauf in der Datei history.tsv im Ausgabeverzeichnis.
  • --initial-epoch=<nr>
    • Gibt an, in welcher Epoche sich das Netzwerk zu Beginn des Trainings befindet. Dies kann verwendet werden, um ein abgebrochenes Training an einem Checkpoint fortzusetzen. Achtung: Null-basiert. Standard: $0$

Netzwerk-Struktur

Die Netzwerk-Struktur-Syntax ermöglicht es, den Aufbau eines Netzwerkes mithilfe eines simpel aufgebauten Codes zu beschrieben. Dieser besteht aus der Beschreibung der einzelnen Schichten in Folge, getrennt jeweils durch das Zeichen >. Die Beschreibung einer Schicht ist immer zusammengesetzt aus der Art der Schicht gefolgt von einer Liste von Parametern. So beschreibt zum Beispiel conv(3,3,32) eine Convolution-Schicht mit einer Kernel-Größe von 3×3 und 32 Filtern. Die folgenden Schicht-Typen sind verfügbar:

  • conv(x,y,z) erzeugt eine Convolution-Schicht mit einer Kernel-Größe von $x\times y$ und $z$ Filtern.
  • maxpool(x,y) erzeugt eine Max-Pooling-Schicht mit einer Pool-Größe von $x\times y$.
  • dense(n) erzeugt eine Fully-connected-Schicht mit $n$ Neuronen.
  • flatten() erzeugt eine Schicht, die eine mehrdimensionale Neuronen-Matrix in eine Dimension plättet.

Es muss allgemein nur die erste Hälfte des Autoencoders beschrieben werden. Die zweite Hälfte erzeugt das Programm dann automatisch, indem es die angegeben Schichten spiegelt. Dabei werden Max-Pooling-Schichten in Upsampling-Schichten und Flatten-Schichten in Reshape-Schichten umgewandelt.

Ein beispielhafter Autoencoder könnte mit diesem Code erzeugt werden:

python autoencoder.py --network-pattern="conv(3,3,32)>maxpool(2,2)>flatten()>dense(1000)" --shape=100,100,3 --out=<out_dir>

Dieser Code erzeugt das folgende Netzwerk:

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
input_1 (InputLayer)         (None, 100, 100, 3)       0
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 100, 100, 32)      896
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 50, 50, 32)        0
_________________________________________________________________
flatten_1 (Flatten)          (None, 80000)             0
_________________________________________________________________
dense_1 (Dense)              (None, 1000)              80001000
_________________________________________________________________
dense_2 (Dense)              (None, 80000)             80080000
_________________________________________________________________
reshape_1 (Reshape)          (None, 50, 50, 32)        0
_________________________________________________________________
up_sampling2d_1 (UpSampling2 (None, 100, 100, 32)      0
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 100, 100, 3)       867
=================================================================
Total params: 160,082,763
Trainable params: 160,082,763
Non-trainable params: 0
_________________________________________________________________

pretrain_vgg16.py

Das Skript pretrain_vgg16 dient dazu, die Gewichte des VGG16-Netzwerkes in ein anderes Netzwerk zu kopieren.

Verwendung

python pretrain_vgg16.py <model_path> <target_path> <pretrained_layers> <locked_layers>

Lädt das Netzwerk aus der Datei model_path, übernimmt die Gewichte des VGG16 und speichert das Ergebnis in target_path. pretrained_layers gibt an, für wie viele Schichten des Netzwerks die Gewichte übernommen werden sollen, und locked_layers gibt an, wie viele der Schichten gesperrt werden sollen, sodass sie sich beim Training nicht mehr verändern.

pca.py

Das Skript pca.py führt eine Hauptkomponentenanalyse über eine Liste von Bildern oder Numpy-Arrays durch.

Verwendung

python pca.py [options] <img_file_pattern> <out_dir>

Durchsucht alle Bild- oder .npy-Dateien, die durch das Muster image_file_pattern beschrieben werden. Das Muster darf die Wildcards * und ? enthalten, in einer Linux-Shell müssen diese aber escaped werden (z.B. files/\*.png). Die Ergebnisse der Hauptkomponentenanalyse werden in dem Verzeichnis out_dir in den folgenden Dateien gespeichert:

  • meanvec.npy enthält den berechneten Durchschnittsvektor.
  • eigvecs.npy enthält die berechneten Eigenvektoren/Hauptkomponenten.
  • transformed.npy enthält die übergebenen Vektoren in den durch die Hauptkomponenten aufgespannten Vektorraum abgebildet.

Verfügbare Optionen:

  • --covar-dropoff=<covar-dropoff>
    • Legt fest, wie viel Prozent der Kovarianz behalten werden soll. $1.0$ behält alle Eigenvektoren. $0.5$ gibt gerade so viele Eigenvektoren zurück, dass mindestens $50\%$ der Kovarianz wiederhergestellt werden kann. Standard: $0.95$
  • --calculate-l-matrix
    • Legt fest, dass an Stelle der Kovarianzmatrix $C$ die Hilfsmatrix $L$ zur Berechnung der Hauptkomponenten verwendet werden soll (siehe alternative Version). Dies sollte nur verwendet werden, wenn die Anzahl der Dimensionen eines (Bild-)Vektors die Anzahl der Vektoren übersteigt.
  • --use-einsum
    • Legt fest, dass an Stelle der Funktion np.dot die Funktion np.einsum zur Berechnung der Kovarianz- oder Hilfsmatrix verwendet werden soll. np.einsum benötigt weniger Arbeitsspeicher, braucht aber viel länger.

Webanwendung

Zusätzlich zu den oben genannten Scripts gibt es noch eine interaktive Webanwendung, die für eine beliebige Gesichts-Kodierung das entsprechende dekomprimierte Gesicht anzeigen kann. Die Parameter des kodierten Gesichts können mit Schiebereglern angepasst werden, sodass man in Echtzeit den Einfluss der Parameter auf das dekodierte Gesicht sieht. Diese Anwendung ist mit Hilfe des Web-Frameworks Flask umgesetzt.

Verwendung

Um die Webanwendung mit dem Flask-Webserver auf dem eigenen Computer zu starten, muss zuerst die Konfigurations-Datei src/flaskapp/config.py angepasst werden. Die Kommentare in der Datei beschreiben die einzelnen Einstellungen.

Nun kann der Server gestartet werden, indem man folgenden Befehl im Hauptverzeichnis des Projektes ausführt:

flask run

Die Webanwendung ist dann unter der URL http://localhost:5000 zu finden.

Fazit

Insgesamt bin ich mit dem Ergebnis dieses Projektes sehr zufrieden. Ich habe beim Trainieren des Autoencoders schneller gute Ergebnisse erzielt als erwartet. Das, was mich wirklich aufgehalten hat, war, dass mir keine vernünftige Hardware zur Verfügung stand. Google Colab ist für solche Aufgaben eigentlich nicht gedacht, und das merkte man auch. Ich werde sehr wahrscheinlich weiter an dem Projekt arbeiten, falls meine sonstigen Verpflichtungen es zulassen. Dann werde ich mir aber eine bessere Möglichkeit suchen als Google Colab.

Mich hat es sehr begeistert im Laufe dieses Projektes die Funktionsweise eines Autoencoders und von Neuronalen Netzwerken im Allgemeinen besser kennen zu lernen. Besonders spannend fand ich es, zu sehen, welche Korrelationen der Autoencoder in Verbindung mit der Hauptkomponentenanalyse in den Trainingsdaten finden konnte. So hingen manchmal die Haarfarbe einer Person und die Neigung des Gesichtes zusammen. Dies lag höchstwahrscheinlich einfach an den Trainingsdaten, die zufällig diese Korrelation aufwiesen. Manche der Merkmale, die ich ursprünglich trennen wollte, hingen aber offensichtlich zu sehr miteinander zusammen, um getrennt zu werden. So waren lange Haare meist in Verbindung mit einem schmaleren Gesicht und mehr Make-Up zu sehen. Blonde Haare meist im Zusammenhang mit heller Haut. Das Programm kann nichts dafür, es hat keine Vorurteile. Alle Vorurteile, die das Programm zu haben scheint, lassen sich durch die Zusammensetzung der Trainingsdaten begründen. Diese Daten bilden (in diesem Projekt) im Optimalfall einen Querschnitt durch die Promi-Welt. Es wurden also nur Zusammenhänge gelernt, die wirklich existieren. Die Gefahr dabei ist, Randgruppen außen vor zu lassen. Dieses Problem gibt es potentiell immer, wenn Machine Learning eingesetzt wird, um Entscheidungen über Menschen zu treffen. Im Endeffekt ist dies oft ein Zeichen für „underfitting“, also eine zu starke Abstraktion der Realität. Vermeiden lässt sich dies nicht wirklich, aber es ist wichtig, sich diese Tatsache vor Augen zu führen. Mir hat dieses Projekt dabei geholfen.

Quellen

Pech-Pacheco, J. L., Cristóbal, G., Chamorro-Martínez, J., & Fernández-Valdivia, J. (2000). Diatom autofocusing in brightfield microscopy: a comparative study (http://optica.csic.es/papers/icpr2k.pdf)

Hassler, D., & Süsstrunk, S. (2003). Measuring colourfulness in natural images (https://infoscience.epfl.ch/record/33994/)

Simonyan, K., & Zisserman, A. (2014). Very Deep Convolutional Networks for Large-Scale Image Recognition (https://arxiv.org/abs/1409.1556)

Abbildungen

Abbildung 1: Von Nicoguaro - Eigenes Werk, CC BY 4.0, https://commons.wikimedia.org/w/index.php?curid=46871195

Abbildung 2: Von Michela Massi - Eigenes Werk, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=80177333

Abbildung 3: Von Aphex34 - Eigenes Werk, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=45679374

Abbildung 12: https://neurohive.io/en/popular-networks/vgg16/

ss19/eigenfaces.txt · Zuletzt geändert: 2021/01/05 23:43 von Labo