Kapitel 4. Aufbau einer Reverse Image Search Engine: Das Verständnis von Einbettungen
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Bob hat gerade ein neues Haus gekauft und möchte es mit schicken, modernen Möbeln einrichten. Er blättert endlos in Möbelkatalogen und besucht Möbelausstellungsräume, aber er hat noch nichts gefunden, was ihm gefällt. Eines Tages entdeckt er das Sofa seiner Träume - ein einzigartiges, L-förmiges, weißes, modernes Sofa in einem Empfangsraum eines Büros. Die gute Nachricht ist, dass er weiß, was er will. Die schlechte Nachricht ist, dass er nicht weiß, wo er es kaufen kann. Die Marke und die Modellnummer stehen nicht auf dem Sofa. Den Büroleiter zu fragen, hilft auch nicht weiter. Also macht er ein paar Fotos aus verschiedenen Blickwinkeln, um sich in den örtlichen Möbelgeschäften umzuhören, aber Pech gehabt: Keiner kennt diese bestimmte Marke. Und die Suche im Internet mit Stichwörtern wie "weißes L-förmiges" oder "modernes Sofa" liefert ihm Tausende von Ergebnissen, aber nicht das, wonach er sucht.
Alice hört Bobs Frustration und fragt: "Warum versuchst du nicht die umgekehrte Bildersuche?" Bob lädt seine Bilder bei der umgekehrten Bildersuche von Google und Bing hoch und entdeckt schnell ein ähnlich aussehendes Bild auf einer Online-Shopping-Website. Mit diesem perfekteren Bild von der Website macht er ein paar weitere umgekehrte Bildersuchen und findet andere Websites, die das gleiche Sofa zu günstigeren Preisen anbieten. Nach ein paar Minuten im Internet hat Bob offiziell sein Traumsofa bestellt!
Die umgekehrte Bildersuche (oder, wie es technisch heißt, instance retrieval) ermöglicht es Entwicklern und Forschern, Szenarien zu entwickeln, die über die einfache Stichwortsuche hinausgehen. Von der Entdeckung visuell ähnlicher Objekte auf Pinterest über die Empfehlung ähnlicher Songs auf Spotify bis hin zur kamerabasierten Produktsuche auf Amazon wird eine ähnliche Technologie unter der Haube verwendet. Websites wie TinEye warnen Fotografen vor Urheberrechtsverletzungen, wenn ihre Fotos ohne Zustimmung im Internet veröffentlicht werden. Auch die Gesichtserkennung in verschiedenen Sicherheitssystemen nutzt ein ähnliches Konzept, um die Identität einer Person festzustellen.
Das Beste daran ist, dass du mit dem richtigen Wissen in wenigen Stunden ein funktionierendes Replikat von vielen dieser Produkte bauen kannst. Also lass uns gleich loslegen!
Das machen wir in diesem Kapitel:
-
Merkmalsextraktion und Ähnlichkeitssuche in den Datensätzen Caltech101 und Caltech256
-
Lernen, wie man große Datensätze (bis zu Milliarden von Bildern) skalieren kann
-
Das System genauer machen und optimieren
-
Analyse von Fallstudien, um zu sehen, wie diese Konzepte in Mainstream-Produkten verwendet werden
Bildähnlichkeit
Die erste und wichtigste Frage lautet: Sind sich zwei Bilder ähnlich oder nicht?
Für dieses Problem gibt es mehrere Ansätze. Ein Ansatz ist der Vergleich von Flächenabschnitten zwischen zwei Bildern. Auf diese Weise lassen sich zwar exakte oder fast exakte Bilder finden (die möglicherweise beschnitten wurden), aber selbst eine leichte Drehung würde zu einer Unähnlichkeit führen. Durch die Speicherung der Hashes der Patches können Duplikate eines Bildes gefunden werden. Ein Anwendungsfall für diesen Ansatz wäre die Erkennung von Plagiaten in Fotos.
Ein anderer naiver Ansatz besteht darin, das Histogramm der RGB-Werte zu berechnen und ihre Ähnlichkeiten zu vergleichen. Das kann helfen, ähnliche Bilder zu finden, die in der gleichen Umgebung aufgenommen wurden, ohne dass sich der Inhalt stark verändert hat. In Abbildung 4-1 wird diese Technik z. B. in einer Software zur Bilddeduplizierung verwendet, die darauf abzielt, Fotostrecken auf deiner Festplatte zu finden, damit du die besten auswählen und den Rest löschen kannst. Natürlich steigt die Wahrscheinlichkeit von Fehlalarmen, je größer dein Datensatz ist. Ein weiterer Nachteil dieses Ansatzes ist, dass kleine Änderungen der Farbe, des Farbtons oder des Weißabgleichs die Erkennung erschweren würden.
Ein robusterer traditioneller, auf Computer Vision basierender Ansatz besteht darin, visuelle Merkmale in der Nähe von Kanten mit Algorithmen wie Scale-Invariant Feature Transform (SIFT), Speeded Up Robust Features (SURF) und Oriented FAST and Rotated BRIEF (ORB) zu finden und dann die Anzahl ähnlicher Merkmale zu vergleichen, die die beiden Fotos gemeinsam haben. So kannst du von einem allgemeinen Verständnis auf Bildebene zu einem relativ robusten Verständnis auf Objektebene gelangen. Das ist zwar toll für Bilder mit starren Objekten, die wenig Variationen aufweisen, wie z. B. die bedruckten Seiten einer Müslischachtel, die fast immer gleich aussehen, aber es ist weniger hilfreich für den Vergleich von verformbaren Objekten wie Menschen und Tieren, die unterschiedliche Posen einnehmen können. Als Beispiel kannst du dir die Merkmale ansehen, die bei der kamerabasierten Produktsuche in der Amazon-App angezeigt werden. Die App zeigt diese Merkmale in Form von blauen Punkten an(Abbildung 4-2). Wenn sie eine ausreichende Anzahl von Merkmalen erkennt, sendet sie diese an die Amazon-Server, um Produktinformationen abzurufen .
Ein weiterer Ansatz besteht darin, die Kategorie (z. B. Sofa) eines Bildes mithilfe von Deep Learning zu ermitteln und dann andere Bilder innerhalb derselben Kategorie zu finden. Das entspricht dem Extrahieren von Metadaten aus einem Bild, so dass es anschließend indiziert und in einer typischen textbasierten Suche verwendet werden kann. Dies lässt sich leicht skalieren, indem die Metadaten in Open-Source-Suchmaschinen wie ElasticSearch verwendet werden. Viele E-Commerce-Websites zeigen Empfehlungen auf der Grundlage von Tags an, die aus einem Bild extrahiert wurden, während intern eine abfragebasierte Suche durchgeführt wird. Wie zu erwarten, gehen durch die Extraktion der Tags bestimmte Informationen wie Farbe, Pose, Beziehungen zwischen Objekten in der Szene und so weiter verloren. Ein großer Nachteil dieses Ansatzes ist außerdem, dass enorme Mengen an markierten Daten benötigt werden, um den Klassifikator für die Extraktion dieser Markierungen auf neue Bilder zu trainieren. Und jedes Mal, wenn eine neue Kategorie hinzugefügt werden soll, muss das Modell neu trainiert werden.
Da unser Ziel die Suche in Millionen von Bildern ist, brauchen wir idealerweise eine Möglichkeit, die Informationen, die in den Millionen von Pixeln eines Bildes enthalten sind, in einer kleineren Darstellung (von sagen wir ein paar tausend Dimensionen) zusammenzufassen und dafür zu sorgen, dass diese zusammengefasste Darstellung bei ähnlichen Objekten nahe beieinander und bei unähnlichen Gegenständen weiter entfernt liegt.
Zum Glück gibt es tiefe neuronale Netze, die hier Abhilfe schaffen. Wie wir in Kapitel 2 und Kapitel 3 gesehen haben, wandeln CNNs ein Bild in tausenddimensionale Merkmalsvektoren um, die dann als Eingabe für einen Klassifikator dienen, der die wichtigsten Identitäten ausgibt, zu denen das Bild gehören könnte (z. B. Hund oder Katze). Die Merkmalsvektoren (auch Einbettungen oder Engpassmerkmale genannt) sind im Wesentlichen eine Sammlung von einigen tausend Fließkommawerten. Das Durchlaufen der Faltungsschichten und der Pooling-Schichten in einem CNN ist im Grunde ein Akt der Reduktion, um die im Bild enthaltenen Informationen auf die wichtigsten und auffälligsten Bestandteile zu filtern, die wiederum die Bottleneck-Features bilden. Durch das Training des CNN werden diese Werte so geformt, dass Elemente, die zur gleichen Klasse gehören, einen kleinen euklidischen Abstand haben (oder einfach die Quadratwurzel aus der Summe der Quadrate der Differenz zwischen den entsprechenden Werten) und Elemente aus verschiedenen Klassen durch größere Abstände getrennt sind. Dies ist eine wichtige Eigenschaft, die bei der Lösung vieler Probleme hilft, bei denen ein Klassifikator nicht eingesetzt werden kann, vor allem bei unüberwachten Problemen, weil es an geeigneten markierten Daten fehlt.
Ein idealer Weg, um ähnliche Bilder zu finden, wäre die Anwendung von Transfer Learning. Zum Beispiel, indem man die Bilder durch ein vortrainiertes neuronales Faltungsnetzwerk wie ResNet-50 laufen lässt, die Merkmale extrahiert und dann eine Metrik wie den euklidischen Abstand zur Berechnung der Fehlerrate verwendet.
Merkmalsextraktion
Ein Bild von sagt mehr als tausend Worte.
In diesem Abschnitt lernen wir spielerisch die Konzepte der Merkmalsextraktion kennen, zunächst mit dem Caltech 101-Datensatz (131 MB, ca. 9.000 Bilder) und später mit Caltech 256 (1,2 GB, ca. 30.000 Bilder). Caltech 101 besteht, wie der Name schon sagt, aus etwa 9.000 Bildern in 101 Kategorien, mit etwa 40 bis 800 Bildern pro Kategorie. Wichtig ist, dass es eine 102. Kategorie namens "BACKGROUND_Google" gibt, die aus zufälligen Bildern besteht, die nicht in den ersten 101 Kategorien enthalten sind, und die gelöscht werden muss, bevor wir mit dem Experimentieren beginnen. Erinnere dich daran, dass der gesamte Code, den wir schreiben, auch im GitHub-Repository verfügbar ist.
Bitte beachte (ab dem 01. September 2020), dass der Caltech 101-Datensatz umgezogen ist und nun über Google Drive mit gdown
heruntergeladen werden muss:
$ gdown https://drive.google.com/uc?id=137RyRjvTBkBiIfeYBNZBtViDHQ6_Ewsp --output 101_ObjectCategories.tar.gz $ tar -xvf 101_ObjectCategories.tar.gz $ mv 101_ObjectCategories caltech101 $ rm -rf caltech101/BACKGROUND_Google
Importiere nun alle notwendigen Module:
import
numpy
as
np
from
numpy.linalg
import
norm
import
pickle
from
tqdm
import
tqdm
,
tqdm_notebook
import
os
import
time
from
tf.keras.preprocessing
import
image
from
tf.keras.applications.resnet50
import
ResNet50
,
preprocess_input
Lade das ResNet-50-Modell ohne die oberen Klassifizierungsschichten, damit wir nur die Engpassmerkmale erhalten . Definiere dann eine Funktion, die einen Bildpfad nimmt, das Bild lädt, die Größe auf die von ResNet-50 unterstützten Dimensionen anpasst, die Merkmale extrahiert und sie dann normalisiert:
model
=
ResNet50
(
weights
=
'imagenet'
,
include_top
=
False
,
input_shape
=
(
224
,
224
,
3
))
def
extract_features
(
img_path
,
model
):
input_shape
=
(
224
,
224
,
3
)
img
=
image
.
load_img
(
img_path
,
target_size
=
(
input_shape
[
0
],
input_shape
[
1
]))
img_array
=
image
.
img_to_array
(
img
)
expanded_img_array
=
np
.
expand_dims
(
img_array
,
axis
=
0
)
preprocessed_img
=
preprocess_input
(
expanded_img_array
)
features
=
model
.
predict
(
preprocessed_img
)
flattened_features
=
features
.
flatten
()
normalized_features
=
flattened_features
/
norm
(
flattened_features
)
return
normalized_features
Die Funktion, die im vorherigen Beispiel definiert wurde, ist die Funktion key
, die wir in Keras für fast jeden Bedarf an Merkmalsextraktion verwenden.
Das war's! Schauen wir uns die Feature-Länge an, die das Modell erzeugt:
features
=
extract_features
(
'../../sample_images/cat.jpg'
,
model
)
(
len
(
features
))
> 2048
Das ResNet-50-Modell generierte 2.048 Merkmale aus dem bereitgestellten Bild. Jedes Merkmal ist ein Fließkommawert zwischen 0 und 1.
Wenn dein Modell auf einem Datensatz trainiert oder feinabgestimmt wurde, der nicht mit ImageNet vergleichbar ist, definiere den Schritt "preprocess_input(img)" entsprechend um. Die in der Funktion verwendeten Mittelwerte sind spezifisch für den ImageNet-Datensatz. Jedes Modell in Keras hat seine eigene Vorverarbeitungsfunktion, also stelle sicher, dass du die richtige verwendest.
Jetzt ist es an der Zeit, die Merkmale für den gesamten Datensatz zu extrahieren. Zuerst erhalten wir alle Dateinamen mit dieser praktischen Funktion, die rekursiv nach allen Bilddateien (definiert durch ihre Erweiterungen) in einem Verzeichnis sucht:
extensions
=
[
'.jpg'
,
'.JPG'
,
'.jpeg'
,
'.JPEG'
,
'.png'
,
'.PNG'
]
def
get_file_list
(
root_dir
):
file_list
=
[]
counter
=
1
for
root
,
directories
,
filenames
in
os
.
walk
(
root_dir
):
for
filename
in
filenames
:
if
any
(
ext
in
filename
for
ext
in
extensions
):
file_list
.
append
(
os
.
path
.
join
(
root
,
filename
))
counter
+=
1
return
file_list
Dann geben wir den Pfad zu unserem Datensatz an und rufen die Funktion auf:
# path to the datasets
root_dir
=
'
../../datasets/caltech101
'
filenames
=
sorted
(
get_file_list
(
root_dir
)
)
Wir definieren nun eine Variable, die alle Merkmale speichert, alle Dateinamen im Datensatz durchgeht, ihre Merkmale extrahiert und sie an die zuvor definierte Variable anhängt:
feature_list
=
[]
for
i
in
tqdm_notebook
(
range
(
len
(
filenames
))):
feature_list
.
append
(
extract_features
(
filenames
[
i
],
model
))
Auf einer CPU sollte das weniger als eine Stunde dauern. Auf einem Grafikprozessor nur ein paar Minuten.
Um ein besseres Gefühl für die Zeit zu bekommen, kannst du das praktische Tool tqdm
verwenden, das eine Fortschrittsanzeige(Abbildung 4-3) mit der Geschwindigkeit pro Iteration sowie der verstrichenen und der erwarteten Endzeit anzeigt. In Python kannst du eine Iterable mit tqdm;
einpacken, zum Beispiel mit tqdm(range(10))
. Die Jupyter Notebook-Variante ist tqdm_notebook
.
Schließlich schreibst du diese Merkmale in eine Pickle-Datei, damit wir sie in Zukunft verwenden können, ohne sie neu berechnen zu müssen:
pickle.dump(feature_list, open('data/features-caltech101-resnet.pickle', 'wb')) pickle.dump(filenames, open('data/filenames-caltech101.pickle','wb'))
Das war's, Leute! Wir sind fertig mit dem Teil der Merkmalsextraktion.
Ähnlichkeitssuche
Wenn wir ein Foto gegeben haben, ist es unser Ziel, ein anderes Foto in unserem Datensatz zu finden, das dem aktuellen Foto ähnlich ist. Wir beginnen mit dem Laden der vorberechneten Merkmale:
filenames = pickle.load(open('data/filenames-caltech101.pickle', 'rb')) feature_list = pickle.load(open('data/features-caltech101-resnet.pickle', 'rb'))
Wir verwenden die Python-Bibliothek für maschinelles Lernen scikit-learn
, um die nächsten Nachbarn der Abfragemerkmale zu finden, d. h. die Merkmale, die ein Abfragebild darstellen. Wir trainieren ein Modell der nächsten Nachbarn, indem wir den Brute-Force-Algorithmus verwenden, um die fünf nächsten Nachbarn auf der Grundlage des euklidischen Abstands zu finden (um scikit-learn
auf deinem System zu installieren, verwende pip3 install sklearn)
:
from
sklearn.neighbors
import
NearestNeighbors
neighbors
=
NearestNeighbors
(
n_neighbors
=
5
,
algorithm
=
'brute'
,
metric
=
'euclidean'
)
.
fit
(
feature_list
)
distances
,
indices
=
neighbors
.
kneighbors
([
feature_list
[
0
]])
Jetzt hast du sowohl die Indizes als auch die Entfernungen der fünf nächsten Nachbarn des allerersten Abfragemerkmals (das das erste Bild darstellt). Beachte die schnelle Ausführung des ersten Schritts - des Trainingsschritts. Im Gegensatz zum Training der meisten maschinellen Lernmodelle, das bei großen Datensätzen mehrere Minuten bis Stunden dauern kann, erfolgt die Erstellung des Modells der nächsten Nachbarn sofort, da während des Trainings nicht viel verarbeitet werden muss. Dies wird auch lazy learning genannt, weil die gesamte Verarbeitung auf die Klassifizierungs- oder Inferenzzeit verschoben wird.
Jetzt, wo wir die Indizes kennen, wollen wir uns das eigentliche Bild hinter diesem Merkmal ansehen. Zuerst wählen wir ein Bild aus, das wir abfragen wollen, z. B. mit dem Index = 0:
import
matplotlib.pyplot
as
plt
import
matplotlib.image
as
mpimg
%
matplotlib
inline
# Show the plots as a cell within the Jupyter Notebooks
plt
.
imshow
(
mpimg
.
imread
(
filenames
[
0
]))
Abbildung 4-4 zeigt das Ergebnis.
Untersuchen wir nun die nächsten Nachbarn, indem wir das erste Ergebnis einzeichnen.
plt
.
imshow
(
mpimg
.
imread
(
filenames
[
indices
[
0
]]))
Abbildung 4-5 zeigt das Ergebnis.
Moment, ist das nicht ein Duplikat? Der nächstgelegene Index ist das Bild selbst, denn das wird abgefragt:
for
i
in
range
(
5
):
(
distances
[
0
][
i
])
0.0 0.8285478 0.849847 0.8529018
Dies wird auch durch die Tatsache bestätigt, dass der Abstand des ersten Ergebnisses gleich Null ist. Zeichnen wir nun den echten ersten nächsten Nachbarn ein:
plt.imshow(mpimg.imread(filenames[indices[1]]))
Sieh dir das Ergebnis diesmal in Abbildung 4-6 an.
Das sieht definitiv nach einem ähnlichen Bild aus. Es hat ein ähnliches Konzept, die gleiche Bildkategorie (Gesichter), das gleiche Geschlecht und einen ähnlichen Hintergrund mit Säulen und Vegetation. Tatsächlich ist es dieselbe Person!
Da wir diese Funktion wahrscheinlich regelmäßig nutzen werden, haben wir bereits eine Hilfsfunktion plot_images()
erstellt, die mehrere Abfrage-Bilder mit ihren nächsten Nachbarn visualisiert. Rufen wir nun diese Funktion auf, um die nächsten Nachbarn von sechs zufälligen Bildern zu visualisieren. Beachte auch, dass jedes Mal, wenn du den folgenden Code ausführst, die angezeigten Bilder anders aussehen(Abbildung 4-7), weil die angezeigten Bilder durch eine zufällige Ganzzahl indiziert sind.
for
i
in
range
(
6
)
:
random_image_index
=
random
.
randint
(
0
,
num_images
)
distances
,
indices
=
neighbors
.
kneighbors
(
[
featureList
[
random_image_index
]
]
)
# don't take the first closest image as it will be the same image
similar_image_paths
=
[
filenames
[
random_image_index
]
]
+
[
filenames
[
indices
[
0
]
[
i
]
]
for
i
in
range
(
1
,
4
)
]
plot_images
(
similar_image_paths
,
distances
[
0
]
)
Visualisierung von Bildclustern mit t-SNE
Auf kannst du den gesamten Datensatz visualisieren!
Dazu müssen wir die Dimensionen der Merkmalsvektoren reduzieren, denn es ist nicht möglich, einen 2.048-dimensionalen Vektor (die Merkmalslänge) in zwei Dimensionen darzustellen (das Papier). Der t-distributed stochastic neighbor embedding (t-SNE)-Algorithmus reduziert den hochdimensionalen Merkmalsvektor auf 2D und liefert so eine Vogelperspektive des Datensatzes, die bei der Erkennung von Clustern und nahegelegenen Bildern hilfreich ist. t-SNE lässt sich nur schwer auf große Datensätze skalieren, daher ist es eine gute Idee, die Dimensionalität mithilfe der Hauptkomponentenanalyse (PCA) zu reduzieren und dann t-SNE aufzurufen:
# Perform PCA over the features
num_feature_dimensions
=
100
# Set the number of features
pca
=
PCA
(
n_components
=
num_feature_dimensions
)
pca
.
fit
(
featureList
)
feature_list_compressed
=
pca
.
transform
(
featureList
)
# For speed and clarity, we'll analyze about first half of the dataset.
selected_features
=
feature_list_compressed
[
:
4000
]
selected_class_ids
=
class_ids
[
:
4000
]
selected_filenames
=
filenames
[
:
4000
]
tsne_results
=
TSNE
(
n_components
=
2
,
verbose
=
1
,
metric
=
'
euclidean
'
)
.
fit_transform
(
selected_features
)
# Plot a scatter plot from the generated t-SNE results
colormap
=
plt
.
cm
.
get_cmap
(
'
coolwarm
'
)
scatter_plot
=
plt
.
scatter
(
tsne_results
[
:
,
0
]
,
tsne_results
[
:
,
1
]
,
c
=
selected_class_ids
,
cmap
=
colormap
)
plt
.
colorbar
(
scatter_plot
)
plt
.
show
(
)
Auf die PCA gehen wir in den folgenden Abschnitten näher ein. Um auf größere Dimensionen zu skalieren, kannst du Uniform Manifold Approximation and Projection (UMAP) verwenden.
Abbildung 4-8 zeigt Cluster ähnlicher Klassen und wie sie dicht beieinander liegen.
Jede Farbe in Abbildung 4-8 steht für eine andere Klasse. Um es noch deutlicher zu machen, können wir eine weitere Hilfsfunktion, plot_images_in_2d()
, verwenden, um die Bilder in diesen Clustern darzustellen, wie in Abbildung 4-9 gezeigt.
Toll! Es gibt eine klar abgegrenzte Gruppe von menschlichen Gesichtern, Blumen, Oldtimern, Schiffen, Fahrrädern und eine etwas verstreute Gruppe von Land- und Meerestieren. Da viele Bilder übereinander liegen, ist die Abbildung 4-9 etwas unübersichtlich. Versuchen wir also, das t-SNE mit der Hilfsfunktion tsne_to_grid_plotter_manual()
als klare Kacheln darzustellen, deren Ergebnisse du in Abbildung 4-10 siehst.
tsne_to_grid_plotter_manual
(
tsne_results
[:,
0
],
tsne_results
[:,
1
],
selected_filenames
)
Das ist definitiv viel klarer. Wir können sehen, dass ähnliche Bilder in den Clustern von menschlichen Gesichtern, Stühlen, Fahrrädern, Flugzeugen, Schiffen, Laptops, Tieren, Uhren, Blumen, umgekippten Minaretten, Oldtimern, Ankerschildern und Kameras zu finden sind, die alle nahe beieinander stehen. Gleich und gleich gesellt sich gern!
2D-Cluster von sind toll, aber sie in 3D zu visualisieren, wäre fantastisch. Noch besser wäre es, wenn sie gedreht, vergrößert und verkleinert und mit der Maus manipuliert werden könnten - ganz ohne Programmierung. Und Bonuspunkte, wenn die Daten interaktiv durchsucht werden könnten, um ihre Nachbarn zu finden. Der TensorFlow Embedding Projektor bietet all dies und mehr in einem browserbasierten GUI-Tool. Die vorgeladenen Embeddings aus Bild- und Textdatensätzen sind hilfreich, um ein besseres Gefühl für die Leistungsfähigkeit von Embeddings zu bekommen. Und wie Abbildung 4-11 zeigt, ist es beruhigend zu sehen, wie Deep Learning herausfindet, dass John Lennon, Led Zeppelin und Eric Clapton in der englischen Sprache in einem ähnlichen Kontext wie die Beatles verwendet werden .
Die Geschwindigkeit der Ähnlichkeitssuche verbessern
Es gibt mehrere Möglichkeiten, die Geschwindigkeit der Ähnlichkeitssuche zu verbessern. Für die Ähnlichkeitssuche gibt es zwei Strategien: Entweder wir reduzieren die Länge der Merkmale oder wir verwenden einen besseren Algorithmus für die Suche unter den Merkmalen. Lass uns jede dieser Strategien einzeln untersuchen.
Länge der Feature-Vektoren
Idealerweise würden wir erwarten, dass die Suche umso schneller ist, je kleiner die Datenmenge ist, in der gesucht werden muss. Wir erinnern uns, dass das ResNet-50-Modell 2.048 Merkmale enthält. Da jedes Merkmal eine 32-Bit-Gleitkommazahl ist, wird jedes Bild durch einen 8 KB großen Merkmalsvektor dargestellt. Bei einer Million Bilder entspricht das fast 8 GB. Stell dir vor, wie langsam es sein würde, 8 GB an Merkmalen zu durchsuchen. Um uns ein besseres Bild von unserem Szenario zu machen, zeigt Tabelle 4-1 die Merkmalslängen, die wir von verschiedenen Modellen erhalten.
Modell | Engpass in Spielfilmlänge | Top-1% Genauigkeit im ImageNet |
---|---|---|
VGG16 | 512 | 71.5% |
VGG19 | 512 | 72.7% |
MobileNet | 1024 | 66.5% |
InceptionV3 | 2048 | 78.8% |
ResNet-50 | 2048 | 75.9% |
Xception | 2048 | 79.0% |
Unter der Haube liefern viele Modelle, die auf tf.keras.applications
verfügbar sind, mehrere tausend Merkmale. InceptionV3 zum Beispiel liefert Merkmale in Form von 1 x 5 x 5 x 2048, was 2.048 Feature-Maps mit 5 x 5 Faltungen entspricht, was insgesamt 51.200 Merkmale ergibt. Daher ist es wichtig, diesen großen Vektor mit Hilfe einer Durchschnitts- oder Max-Pooling-Schicht zu reduzieren. Die Pooling-Schicht fasst jede Faltung (z. B. die 5 x 5-Schicht) zu einem einzigen Wert zusammen. Dies kann während der Modellinstanziierung wie folgt definiert werden:
model
=
InceptionV3
(
weights
=
'imagenet'
,
include_top
=
False
,
input_shape
=
(
224
,
224
,
3
),
pooling
=
'max'
)
Bei Modellen, die eine große Anzahl von Merkmalen liefern, wirst du in der Regel feststellen, dass alle Codebeispiele diese Pooling-Option nutzen. Tabelle 4-2 zeigt die Auswirkung des Max-Poolings auf die Anzahl der Merkmale in verschiedenen Modellen vor und nach dem Pooling.
Modell | # Merkmale vor der Zusammenlegung | # Merkmale nach der Zusammenlegung |
---|---|---|
ResNet-50 | [1,1,1,2048] = 2048 | 2048 |
InceptionV3 | [1,5,5,2048] = 51200 | 2048 |
MobileNet | [1,7,7,1024] = 50176 | 1024 |
Wie wir sehen können, erzeugen fast alle Modelle eine große Anzahl von Merkmalen. Stell dir vor, wie viel schneller die Suche wäre, wenn wir sie auf nur 100 Merkmale reduzieren könnten (eine satte Reduzierung um das 10- bis 20-fache!), ohne die Qualität der Ergebnisse zu beeinträchtigen. Abgesehen von der Größe ist dies eine noch größere Verbesserung für Big-Data-Szenarien, bei denen die Daten auf einmal in den Arbeitsspeicher geladen werden können, anstatt Teile davon in regelmäßigen Abständen zu laden, was einen noch größeren Geschwindigkeitszuwachs bedeutet. PCA wird uns dabei helfen, dies zu erreichen.
Reduzierung der Feature-Länge mit PCA
PCA ist ein statistisches Verfahren, das die Frage stellt, ob die Merkmale, die die Daten repräsentieren, gleich wichtig sind. Sind einige der Merkmale so redundant, dass wir auch nach dem Entfernen dieser Merkmale ähnliche Klassifizierungsergebnisse erhalten? Die PCA gilt als eine der besten Techniken zur Dimensionalitätsreduktion. Dabei werden keine redundanten Merkmale entfernt, sondern es wird eine neue Gruppe von Merkmalen erzeugt, die eine lineare Kombination der Eingangsmerkmale darstellt. Diese linearen Merkmale sind orthogonal zueinander, weshalb alle redundanten Merkmale wegfallen. Diese Merkmale werden als Hauptkomponenten bezeichnet.
Die Durchführung der PCA ist ziemlich einfach. Verwende die Bibliothek scikit-learn
und führe Folgendes aus :
import
sklearn.decomposition.PCA
as
PCA
num_feature_dimensions
=
100
pca
=
PCA
(
n_components
=
num_feature_dimensions
)
pca
.
fit
(
feature_list
)
feature_list_compressed
=
pca
.
transform
(
feature_list
)
Die PCA kann uns auch die relative Bedeutung jedes Merkmals anzeigen. Die allererste Dimension hat die größte Varianz und die Varianz nimmt im weiteren Verlauf immer weiter ab:
# Explain the importance of first 20 features
(
pca
.
explained_variance_ratio_
[
0
:
20
])
[ 0.07320023 0.05273142 0.04310822 0.03494248 0.02166119 0.0205037 0.01974325 0.01739547 0.01611573 0.01548918 0.01450421 0.01311005 0.01200541 0.0113084 0.01103872 0.00990405 0.00973481 0.00929487 0.00915592 0.0089256 ]
Hmm, warum haben wir 100 Dimensionen aus den ursprünglichen 2.048 ausgewählt? Warum nicht 200? Die PCA stellt unseren ursprünglichen Merkmalsvektor dar, aber in reduzierten Dimensionen. Jede neue Dimension hat einen abnehmenden Nutzen für die Darstellung des ursprünglichen Vektors (d. h., die neue Dimension erklärt die Daten möglicherweise nicht sehr gut) und nimmt wertvollen Platz ein. Wir können abwägen, wie gut die ursprünglichen Daten erklärt werden und wie stark wir sie reduzieren wollen. Veranschaulichen wir uns die Bedeutung der ersten 200 Dimensionen.
pca
=
PCA
(
200
)
pca
.
fit
(
feature_list
)
matplotlib
.
style
.
use
(
'seaborn'
)
plt
.
plot
(
range
(
1
,
201
),
pca
.
explained_variance_ratio_
,
'o--'
,
markersize
=
4
)
plt
.
title
(
'Variance for each PCA dimension'
)
plt
.
xlabel
(
'PCA Dimensions'
)
plt
.
ylabel
(
'Variance'
)
plt
.
grid
(
True
)
plt
.
show
()
Abbildung 4-12 zeigt die Ergebnisse.
Die individuelle Varianz sagt uns, wie wichtig die neu hinzugefügten Merkmale sind. Nach den ersten 100 Dimensionen fügen die zusätzlichen Dimensionen zum Beispiel nicht mehr viel Varianz hinzu (fast gleich 0) und können vernachlässigt werden. Ohne die Genauigkeit zu überprüfen, kann man also davon ausgehen, dass die PCA mit 100 Dimensionen ein robustes Modell ist. Eine andere Möglichkeit, dies zu betrachten, besteht darin, zu visualisieren, wie viel von den ursprünglichen Daten durch die begrenzte Anzahl von Merkmalen erklärt wird, indem man die kumulative Varianz ermittelt (siehe Abbildung 4-13).
plt
.
plot
(
range
(
1
,
201
),
pca
.
explained_variance_ratio_
.
cumsum
(),
'o--'
,
markersize
=
4
)
plt
.
title
(
'Cumulative Variance with each PCA dimension'
)
plt
.
xlabel
(
'PCA Dimensions'
)
plt
.
ylabel
(
'Variance'
)
plt
.
grid
(
True
)
plt
.
show
()
Wie zu erwarten, erhöht sich die Varianz durch das Hinzufügen von 100 Dimensionen (von 100 auf 200) nur um 0,1 und beginnt dann allmählich zu sinken. Zum Vergleich: Die Verwendung aller 2.048 Merkmale würde zu einer kumulativen Abweichung von 1 führen.
Die Anzahl der Dimensionen in der PCA ist ein wichtiger Parameter, den wir auf das jeweilige Problem abstimmen können. Eine Möglichkeit, einen guten Schwellenwert direkt zu begründen, besteht darin, ein gutes Gleichgewicht zwischen der Anzahl der Merkmale und ihrer Auswirkung auf die Genauigkeit und Geschwindigkeit zu finden:
pca_dimensions
=
[
1
,
2
,
3
,
4
,
5
,
10
,
20
,
50
,
75
,
100
,
150
,
200
]
pca_accuracy
=
[
]
pca_time
=
[
]
for
dimensions
in
pca_dimensions
:
# Perform PCA
pca
=
PCA
(
n_components
=
dimensions
)
pca
.
fit
(
feature_list
)
feature_list_compressed
=
pca
.
transform
(
feature_list
[
:
]
)
# Calculate accuracy over the compressed features
accuracy
,
time_taken
=
accuracy_calculator
(
feature_list_compressed
[
:
]
)
pca_time
.
append
(
time_taken
)
pca_accuracy
.
append
(
accuracy
)
(
"
For PCA Dimensions =
"
,
dimensions
,
"
,
\t
Accuracy =
"
,
accuracy
,
"
%
"
,
"
,
\t
Time =
"
,
pca_time
[
-
1
]
)
Wir veranschaulichen diese Ergebnisse anhand des Diagramms in Abbildung 4-14 und sehen, dass ab einer bestimmten Anzahl von Dimensionen eine Erhöhung der Dimensionen nicht zu einer höheren Genauigkeit führt:
plt
.
plot
(
pca_time
,
pca_accuracy
,
'o--'
,
markersize
=
4
)
for
label
,
x
,
y
in
zip
(
pca_dimensions
,
pca_time
,
pca_accuracy
):
plt
.
annotate
(
label
,
xy
=
(
x
,
y
),
ha
=
'right'
,
va
=
'bottom'
)
plt
.
title
(
'Test Time vs Accuracy for each PCA dimension'
)
plt
.
xlabel
(
'Test Time'
)
plt
.
ylabel
(
'Accuracy'
)
plt
.
grid
(
True
)
plt
.
show
()
Wie in der Grafik zu sehen ist, verbessert sich die Genauigkeit kaum, wenn die Merkmalslänge über 100 Dimensionen hinausgeht. Mit fast 20 Mal weniger Dimensionen (100) als das Original (2.048) bietet dies eine drastisch höhere Geschwindigkeit und einen geringeren Zeitaufwand für fast jeden Suchalgorithmus, während eine ähnliche (und manchmal sogar etwas bessere) Genauigkeit erreicht wird. Daher wäre 100 eine ideale Merkmalslänge für diesen Datensatz. Das bedeutet auch, dass die ersten 100 Dimensionen die meisten Informationen über den Datensatz enthalten.
Diese reduzierte Darstellung bietet eine Reihe von Vorteilen, z. B. die effiziente Nutzung von Rechenressourcen, die Beseitigung von Rauschen, eine bessere Generalisierung aufgrund der geringeren Anzahl von Dimensionen und eine bessere Leistung für maschinelle Lernalgorithmen, die auf diesen Daten lernen. Indem wir die Abstandsberechnung auf die wichtigsten Merkmale reduzieren, können wir auch die Ergebnisgenauigkeit leicht verbessern. Das liegt daran, dass bisher alle 2.048 Merkmale gleichermaßen zur Abstandsberechnung beigetragen haben, während jetzt nur noch die wichtigsten 100 Merkmale zum Zuge kommen. Aber noch wichtiger ist, dass wir dadurch den Fluch der Dimensionalität vermeiden. Es ist bekannt, dass das Verhältnis des euklidischen Abstands zwischen den beiden nächstgelegenen Punkten und den beiden am weitesten entfernten Punkten mit zunehmender Anzahl der Dimensionen gegen 1 tendiert. In einem sehr hochdimensionalen Raum scheinen die meisten Punkte eines realen Datensatzes ähnlich weit voneinander entfernt zu sein, und die euklidische Abstandsmetrik schlägt bei der Unterscheidung zwischen ähnlichen und unähnlichen Elementen fehl. Die PCA hilft, die Vernunft wiederherzustellen.
Du kannst auch mit verschiedenen Entfernungen experimentieren, z. B. mit der Minkowski-Distanz, der Manhattan-Distanz, der Jaccard-Distanz und der gewichteten euklidischen Distanz (wobei das Gewicht der Beitrag jedes Merkmals ist, wie unter pca.explained_variance_ratio_
erklärt).
Wenden wir uns nun der Nutzung dieser reduzierten Funktionen zu, um unsere Suche noch schneller zu machen.
Skalierende Ähnlichkeitssuche mit approximativen nächsten Nachbarn
Welche wollen wir? Nächste Nachbarn. Was ist unsere Basislinie? Brute-force-Suche. Obwohl sie bequem in zwei Zeilen implementiert werden kann, geht sie über jedes Element und skaliert daher linear mit der Datengröße (Anzahl der Elemente und der Dimensionen). Wenn wir unseren Merkmalsvektor mit PCA von 2.048 auf 100 verkleinern, verringert sich nicht nur die Datengröße um das 20-fache, sondern auch die Geschwindigkeit steigt um das 20-fache, wenn wir Brute-Force einsetzen. PCA zahlt sich aus!
Nehmen wir an, dass die Ähnlichkeitssuche in einer kleinen Sammlung von 10.000 Bildern, die jetzt mit 100 Vektoren der Merkmalslänge dargestellt werden, etwa 1 ms dauert. Auch wenn das für 10.000 Objekte schnell aussieht, wird die Suche in einem echten Produktionssystem mit größeren Daten, vielleicht 10 Millionen Objekten, mehr als eine Sekunde dauern. Unser System ist möglicherweise nicht in der Lage, mehr als eine Anfrage pro Sekunde und CPU-Kern zu bearbeiten. Wenn du 100 Anfragen pro Sekunde von Nutzern erhältst, bräuchtest du selbst bei mehreren CPU-Kernen der Maschine (und dem Laden des Suchindex per Thread) mehrere Maschinen, um den Datenverkehr bedienen zu können. Mit anderen Worten: Ein ineffizienter Algorithmus bedeutet Geld, viel Geld, das für Hardware ausgegeben wird.
Brute-Force ist unsere Basis für jeden Vergleich. Wie bei den meisten algorithmischen Ansätzen ist Brute-Force der langsamste Ansatz. Jetzt, wo wir unsere Basis haben, werden wir die Näherungsalgorithmen untersuchen. Anstatt wie bei der Brute-Force-Methode das richtige Ergebnis zu garantieren, erzielen die Näherungsalgorithmen in der Regel das richtige Ergebnis, weil sie... nun ja, Näherungen sind. Die meisten Algorithmen lassen sich in irgendeiner Form einstellen, um ein Gleichgewicht zwischen Korrektheit und Geschwindigkeit herzustellen. Es ist möglich, die Qualität der Ergebnisse zu bewerten, indem man sie mit den Ergebnissen der Brute-Force-Basislösung vergleicht.
Näherungsweise nächstgelegener Benchmark
Es gibt mehrere ANN-Bibliotheken (Approximate Nearest Neighbor), darunter so bekannte wie Annoy von Spotify, FLANN, Faiss von Facebook, NGT von Yahoo und NMSLIB. Sie alle zu vergleichen, wäre eine mühsame Aufgabe (vorausgesetzt, du schaffst es, einige von ihnen zu installieren). Zum Glück haben die guten Leute von ann-benchmarks.com (Martin Aumueller, Erik Bernhardsson und Alec Faitfull) die Arbeit für uns übernommen und reproduzierbare Benchmarks für 19 Bibliotheken mit großen öffentlichen Datensätzen erstellt. Wir werden die Vergleiche mit einem Datensatz von Feature Embeddings, die Wörter (statt Bilder) darstellen, namens GloVe durchführen. Dieser 350 MB große Datensatz besteht aus 400.000 Feature-Vektoren, die Wörter in 100 Dimensionen darstellen. Abbildung 4-15 zeigt die Rohleistung, wenn sie auf Korrektheit getrimmt ist. Die Leistung wird anhand der Fähigkeit der Bibliothek gemessen, Anfragen pro Sekunde zu beantworten. Ein Maß für die Korrektheit ist der Anteil der am nächsten liegenden Top-n-Einträge im Verhältnis zu den tatsächlichen am nächsten liegenden Top-n-Einträgen. Diese Grundwahrheit wird mit der Brute-Force-Suche gemessen.
Die besten Ergebnisse in diesem Datensatz liefern fast mehrere tausend Abfragen pro Sekunde bei einer akzeptablen Trefferquote von 0,8. Zum Vergleich: Unsere Brute-Force-Suche liefert weniger als eine Anfrage pro Sekunde. Einige dieser Bibliotheken (wie z. B. NGT) können bis zu 15.000 Ergebnisse pro Sekunde liefern (allerdings bei einer niedrigen Trefferquote, was sie für die Nutzung unpraktisch macht).
Welche Bibliothek soll ich benutzen?
Es versteht sich von selbst, dass die Bibliothek, die du verwendest, stark von deinem Szenario abhängt. Jede Bibliothek bietet einen Kompromiss zwischen Suchgeschwindigkeit, Genauigkeit, Indexgröße, Speicherverbrauch, Hardware-Nutzung (CPU/GPU) und einfacher Einrichtung. Tabelle 4-3 gibt einen Überblick über die verschiedenen Szenarien und Empfehlungen, welche Bibliothek für jedes Szenario am besten geeignet ist.
Szenario | Empfehlung |
---|---|
Ich möchte schnell in Python experimentieren, ohne zu viele Einstellungen vorzunehmen, aber ich lege auch Wert auf eine hohe Geschwindigkeit. | Annoy oder NMSLIB verwenden |
Ich habe einen großen Datensatz (bis zu 10 Millionen Einträge oder mehrere tausend Dimensionen) und lege größten Wert auf Geschwindigkeit. | NGT verwenden |
Ich habe einen lächerlich großen Datensatz (über 100 Millionen Einträge) und habe auch einen Cluster von GPUs. | Faiss verwenden |
Ich möchte eine Basiswahrheit mit 100%iger Korrektheit festlegen. Dann wechsle ich sofort zu einer schnelleren Bibliothek, beeindrucke meinen Chef mit der um Größenordnungen höheren Geschwindigkeit und bekomme einen Bonus. | Brute-Force-Ansatz verwenden |
Wir bieten auf der GitHub-Website des Buches (siehe http://PracticalDeepLearning.ai) viel detailliertere Code-Beispiele für verschiedene Bibliotheken an, aber für unsere Zwecke hier stellen wir unsere Lieblingsbibliothek Annoy im Detail vor und vergleichen sie mit der Brute-Force-Suche auf einem synthetischen Datensatz. Außerdem gehen wir kurz auf Faiss und NGT ein.
Erstellen eines synthetischen Datensatzes
Um einen direkten Vergleich zwischen den verschiedenen Bibliotheken anzustellen, erstellen wir zunächst einen Datensatz mit einer Million Einträgen, der aus zufälligen Fließkommawerten mit Mittelwert 0 und Varianz 1 besteht. Außerdem wählen wir einen zufälligen Merkmalsvektor als Abfrage, um die nächsten Nachbarn zu finden:
num_items
=
1000000
num_dimensions
=
100
dataset
=
np
.
random
.
randn
(
num_items
,
num_dimensions
)
dataset
/=
np
.
linalg
.
norm
(
dataset
,
axis
=
1
)
.
reshape
(
-
1
,
1
)
random_index
=
random
.
randint
(
0
,
num_items
)
query
=
dataset
[
random_index
]
Brachiale Gewalt
Zuerst berechnen wir die Zeit für die Suche mit dem Brute-Force-Algorithmus. Er geht die gesamten Daten der Reihe nach durch und berechnet den Abstand zwischen der Abfrage und dem aktuellen Element nacheinander. Wir verwenden den Befehl timeit
, um die Zeit zu berechnen. Zuerst erstellen wir den Suchindex, um die fünf nächsten Nachbarn zu finden, und suchen dann mit einer Abfrage:
neighbors
=
NearestNeighbors
(
n_neighbors
=
5
,
algorithm
=
'brute'
,
metric
=
'euclidean'
)
.
fit
(
dataset
)
%
timeit
distances
,
indices
=
neighbors
.
kneighbors
([
query
])
> 177 ms ± 136 μs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Der Befehl timeit
ist ein praktisches Werkzeug. Um die Zeit einer einzelnen Operation zu messen, stellst du ihr diesen Befehl voran. Im Gegensatz zum Befehl time, der eine Anweisung nur einmal ausführt, führt timeit
die nachfolgende Zeile mehrmals aus, um genauere aggregierte Statistiken zusammen mit der Standardabweichung zu erhalten. Standardmäßig wird die Speicherbereinigung ausgeschaltet, damit die unabhängigen Zeitangaben besser vergleichbar sind. Allerdings kann es sein, dass die Zeitangaben nicht den tatsächlichen Produktionslasten entsprechen, wenn die Speicherbereinigung eingeschaltet ist.
Ärgern Sie
Annoy (Approximate Nearest Neighbors Oh Yeah) ist eine C++-Bibliothek mit Python-Bindungen für die Suche nach den nächsten Nachbarn. Sie ist ein Synonym für Geschwindigkeit und wurde von Spotify veröffentlicht und wird in der Produktion für die Musikempfehlungen verwendet. Anders als ihr Name vermuten lässt, ist sie tatsächlich unterhaltsam und einfach zu benutzen.
Um Annoy zu nutzen, installieren wir es mit pip
:
$ pip install annoy
Die Anwendung ist ziemlich einfach. Zuerst erstellen wir einen Suchindex mit zwei Hyperparametern: die Anzahl der Dimensionen des Datensatzes und die Anzahl der Bäume:
from
annoy
import
AnnoyIndex
annoy_index
=
AnnoyIndex
(
num_dimensions
)
# Length of item vector that will be
indexed
for
i
in
range
(
num_items
)
:
annoy_index
.
add_item
(
i
,
dataset
[
i
]
)
annoy_index
.
build
(
40
)
# 40 trees
Jetzt wollen wir herausfinden, wie lange es dauert, die fünf nächsten Nachbarn eines Bildes zu suchen:
%
timeit
indexes
=
t
.
get_nns_by_vector
(
query
,
5
,
include_distances
=
True
)
> 34.9 μs ± 165 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
Das ist rasend schnell! Um das zu verdeutlichen: Selbst für unseren Datensatz mit einer Million Einträgen können fast 28.000 Anfragen auf einem einzigen CPU-Kern bearbeitet werden. Wenn man bedenkt, dass die meisten CPUs mehrere Kerne haben, sollte es in der Lage sein, mehr als 100.000 Anfragen auf einem einzigen System zu bearbeiten. Das Beste daran ist, dass du denselben Index im Speicher für mehrere Prozesse freigeben kannst. So kann der größte Index der Größe deines gesamten Arbeitsspeichers entsprechen, wodurch es möglich ist, mehrere Anfragen auf einem einzigen System zu bedienen.
Zu den weiteren Vorteilen gehört, dass es einen Index von bescheidener Größe erzeugt. Außerdem entkoppelt es das Erstellen von Indizes vom Laden, so dass du einen Index auf einem Rechner erstellen, ihn weitergeben und dann auf deinem Serving-Rechner in den Speicher laden und ausliefern kannst.
Du fragst dich, wie viele Bäume du verwenden sollst? Mehr Bäume ergeben eine höhere Genauigkeit, aber auch größere Indizes. Normalerweise werden nicht mehr als 50 Bäume benötigt, um die höchste Genauigkeit zu erreichen.
NGT
Die Bibliothek Neighborhood Graph and Tree (NGT) von Yahoo Japan führt derzeit die meisten Benchmarks an und eignet sich am besten für große Datensätze (in Millionen von Elementen) mit großen Dimensionen (in mehreren Tausend). Die Bibliothek gibt es zwar schon seit 2016, aber ihr wirklicher Einstieg in die Benchmark-Szene erfolgte 2018 mit der Implementierung des ONNG-Algorithmus (kurz für Optimization of indexing based on k-Nearest Neighbor Graph for proximity). Da NGT auf einem Server von mehreren Threads ausgeführt werden kann, kann er den Index mit Hilfe von Memory-Mapped-Dateien im gemeinsamen Speicher ablegen und so den Speicherverbrauch reduzieren und die Ladezeit erhöhen.
Faiss
Faiss ist die effiziente Ähnlichkeitssuchbibliothek von Facebook. Sie kann Milliarden von Vektoren auf einem einzigen Server im Arbeitsspeicher speichern, indem sie eine komprimierte Darstellung der Vektoren (kompakte Quantisierungscodes) anstelle der Originalwerte speichert. Sie ist besonders für dichte Vektoren geeignet. Es eignet sich besonders gut für Rechner mit Grafikprozessoren, da der Index im GPU-Speicher (VRAM) gespeichert wird. Das funktioniert sowohl auf Single-GPU- als auch auf Multi-GPU-Systemen. Es bietet die Möglichkeit, die Leistung auf der Grundlage von Suchzeit, Genauigkeit, Speichernutzung und Indexierungszeit zu konfigurieren. Es ist eine der schnellsten bekannten Implementierungen der ANN-Suche auf der GPU. Hey, wenn es für Facebook gut genug ist, ist es auch für die meisten von uns gut genug (solange wir genug Daten haben).
Obwohl es den Rahmen dieses Buches sprengen würde, den gesamten Prozess zu zeigen, empfehlen wir, Faiss mit Anaconda zu installieren oder die Docker-Container zu verwenden, um schnell loszulegen.
Verbesserung der Genauigkeit durch Feinabstimmung
Viele der vortrainierten Modelle wurden auf dem ImageNet-Datensatz trainiert. Daher bieten sie in den meisten Fällen eine hervorragende Ausgangsbasis für Ähnlichkeitsberechnungen. Wenn du diese Modelle jedoch an dein spezielles Problem anpasst, werden sie noch besser ähnliche Bilder finden.
In diesem Teil des Kapitels identifizieren wir die Kategorien, die am schlechtesten abschneiden, visualisieren sie mit t-SNE, nehmen eine Feinabstimmung vor und sehen dann, wie sich ihr t-SNE-Diagramm verändert.
Was ist eine gute Kennzahl, um zu überprüfen, ob du tatsächlich ähnliche Bilder bekommst?
- Schmerzhafte Option 1
-
Gehe den gesamten Datensatz ein Bild nach dem anderen durch und bewerte manuell, ob die zurückgegebenen Bilder tatsächlich ähnlich aussehen.
- Glücklichere Option 2
-
Berechne einfach die Genauigkeit. Das heißt, wenn ein Bild der Kategorie X angehört, gehören die ähnlichen Bilder dann auch zur gleichen Kategorie? Wir nennen das die Ähnlichkeitsgenauigkeit.
Welches sind also die Kategorien, die am schlechtesten abschneiden? Und warum sind sie die schlechtesten? Um diese Frage zu beantworten, haben wir eine Hilfsfunktion worst_classes
vordefiniert. Sie findet für jedes Bild im Datensatz die nächsten Nachbarn mit dem Brute-Force-Algorithmus und gibt dann die sechs Klassen mit der geringsten Genauigkeit zurück. Um die Auswirkungen der Feinabstimmung zu sehen, führen wir unsere Analyse an einem schwierigeren Datensatz durch: Caltech-256. Wenn du diese Funktion aufrufst, werden die ungenauesten Klassen aufgedeckt:
names_of_worst_classes_before_finetuning
,
accuracy_per_class_before_finetuning
=
worst_classes
(
feature_list
[:])
Accuracy is 56.54 Top 6 incorrect classifications 059.drinking-straw Accuracy: 11.76% 135.mailbox Accuracy: 16.03% 108.hot-dog Accuracy: 16.72% 163.playing-card Accuracy: 17.29% 195.soda-can Accuracy: 19.68% 125.knife Accuracy: 20.53%
Um zu sehen, warum sie in bestimmten Klassen so schlecht abschneiden, haben wir ein t-SNE-Diagramm erstellt, um die Einbettungen im 2D-Raum zu visualisieren (siehe Abbildung 4-16). Um eine Überfüllung des Diagramms zu vermeiden, verwenden wir nur 50 Elemente aus jeder der 6 Klassen.
Um die Sichtbarkeit des Diagramms zu verbessern, können wir verschiedene Markierungen und Farben für jede Klasse festlegen. Matplotlib bietet eine große Auswahl an Markierungen und Farben.
markers
=
[
"^"
,
"."
,
"s"
,
"o"
,
"x"
,
"P"
]
colors
=
[
'red'
,
'blue'
,
'fuchsia'
,
'green'
,
'purple'
,
'orange'
]
Aah, diese Merkmalsvektoren liegen überall herum und übereinander. Die Verwendung dieser Merkmalsvektoren in anderen Anwendungen, wie z. B. der Klassifizierung, wäre keine gute Idee, denn es wäre schwierig, eine saubere Trennlinie zwischen ihnen zu finden. Kein Wunder, dass sie in diesem Klassifizierungstest auf Basis der nächsten Nachbarn so schlecht abgeschnitten haben.
Was denkst du, wird das Ergebnis sein, wenn wir diese Schritte mit dem feinabgestimmten Modell wiederholen? Wir vermuten etwas Interessantes. Schauen wir uns Abbildung 4-17 an, um das zu sehen.
Das ist so viel sauberer. Mit und einem kleinen Feintuning, wie in Kapitel 3 gezeigt, beginnen sich die Einbettungen zu gruppieren. Vergleiche die verrauschten/verstreuten Einbettungen der vortrainierten Modelle mit denen des fein abgestimmten Modells. Ein Klassifizierungssystem mit maschinellem Lernen wäre in der Lage, eine Trennlinie zwischen diesen Klassen zu finden, was zu einer besseren Klassifizierungsgenauigkeit und zu mehr ähnlichen Bildern führen würde, wenn kein Klassifizierungssystem verwendet würde. Und denk daran, dass dies die Klassen mit den höchsten Fehlklassifizierungen waren; stell dir vor, wie schön die Klassen mit der ursprünglich höheren Genauigkeit nach der Feinabstimmung sein würden.
Zuvor erreichten die vortrainierten Einbettungen eine Genauigkeit von 56 %. Die neuen Einbettungen liefern nach der Feinabstimmung satte 87 % Genauigkeit! Ein bisschen Magie kann viel bewirken.
Die einzige Einschränkung bei der Feinabstimmung ist das Erfordernis von beschrifteten Daten, die nicht immer vorhanden sind. Je nach Anwendungsfall kann es also sein, dass du eine gewisse Menge an Daten kennzeichnen musst.
Allerdings gibt es einen kleinen unkonventionellen Trainingstrick, den wir im nächsten Abschnitt besprechen.
Feinabstimmung ohne vollständig verbundene Schichten
Wie wir bereits wissen, besteht ein neuronales Netz aus drei Teilen:
-
Faltungsschichten, die am Ende die Merkmalsvektoren erzeugen
-
Vollständig verbundene Schichten
-
Die letzte Klassifizierungsschicht
Beim Feintuning geht es, wie der Name schon sagt, darum, ein neuronales Netz leicht zu optimieren, um es an einen neuen Datensatz anzupassen. In der Regel werden dabei die voll verknüpften Schichten (oberste Schichten) entfernt, durch neue ersetzt und das neu zusammengesetzte neuronale Netz dann mit diesem Datensatz trainiert. Diese Art des Trainings bewirkt zwei Dinge:
-
Die Gewichte in allen neu hinzugefügten voll verknüpften Schichten werden erheblich beeinflusst.
-
Die Gewichte in den Faltungsschichten werden nur leicht verändert.
Die voll verknüpften Schichten übernehmen einen Großteil der schweren Arbeit, um eine maximale Klassifizierungsgenauigkeit zu erreichen. Daher wird sich der Großteil des Netzwerks, das die Merkmalsvektoren erzeugt, nur unwesentlich verändern. Daher werden sich die Merkmalsvektoren trotz Feinabstimmung kaum verändern.
Unser Ziel ist es, dass ähnlich aussehende Objekte engere Merkmalsvektoren haben, was mit der oben beschriebenen Feinabstimmung nicht erreicht werden kann. Wenn wir das gesamte aufgabenspezifische Lernen in die Faltungsschichten verlagern, können wir viel bessere Ergebnisse erzielen. Wie erreichen wir das? Indem wir alle voll verknüpften Schichten entfernen und eine Klassifizierungsschicht direkt nach den Faltungsschichten (die die Merkmalsvektoren erzeugen) einfügen. Dieses Modell ist für die Ähnlichkeitssuche und nicht für die Klassifizierung optimiert.
Um den Prozess der Feinabstimmung eines für Klassifizierungsaufgaben optimierten Modells mit dem der Ähnlichkeitssuche zu vergleichen, erinnern wir uns daran, wie wir unser Modell in Kapitel 3 für die Klassifizierung feinabgestimmt haben:
from
tf.keras.applications.resnet50
import
ResNet50
model
=
ResNet50
(
weights
=
'imagenet'
,
include_top
=
False
,
input_shape
=
(
224
,
224
,
3
))
input
=
Input
(
shape
=
(
224
,
224
,
3
))
x
=
model
(
input
)
x
=
GlobalAveragePooling2D
()(
x
)
x
=
Dense
(
64
,
activation
=
'relu'
)(
x
)
x
=
Dropout
(
0.5
)(
x
)
x
=
Dense
(
NUM_CLASSES
,
activation
=
'softmax'
)(
x
)
model_classification_optimized
=
Model
(
inputs
=
input
,
outputs
=
x
)
Und hier sehen wir, wie wir unser Modell für die Ähnlichkeitssuche feinabstimmen. Beachte die fehlende versteckte dichte Schicht in der Mitte:
from
tf
.
keras
.
applications
.
resnet50
import
ResNet50
model
=
ResNet50
(
weights
=
'
imagenet
'
,
include_top
=
False
,
input_shape
=
(
224
,
224
,
3
)
)
input
=
Input
(
shape
=
(
224
,
224
,
3
)
)
x
=
model
(
input
)
x
=
GlobalAveragePooling2D
(
)
(
x
)
# No dense or dropout layers
x
=
Dense
(
NUM_CLASSES
,
activation
=
'
softmax
'
)
(
x
)
model_similarity_optimized
=
Model
(
inputs
=
input
,
outputs
=
x
)
Um nach der Feinabstimmung die model_similarity_optimized
für die Extraktion von Merkmalen zu verwenden, anstatt Wahrscheinlichkeiten für Klassen anzugeben, musst du nur die letzte Ebene pop
(d.h. entfernen):
model_similarity_optimized
.
layers
.
pop
()
model
=
Model
(
model_similarity_optimized
.
input
,
model_similarity_optimized
.
layers
[
-
1
]
.
output
)
Das Wichtigste an dieser Stelle ist, dass wir bei Verwendung des regulären Feinabstimmungsprozesses eine geringere Ähnlichkeitsgenauigkeit als model_similarity_optimized
erhalten würden. Natürlich würden wir model_classification_optimized
für Klassifizierungsszenarien und model_similarity_optimized
für die Extraktion von Einbettungen für die Ähnlichkeitssuche verwenden wollen.
Mit all diesem Wissen kannst du jetzt ein schnelles und genaues Ähnlichkeitssystem für jedes Szenario entwickeln, an dem du arbeitest. Es ist an der Zeit zu sehen, wie die Giganten der KI-Branche ihre Produkte entwickeln.
Siamesische Netzwerke für die One-Shot-Gesichtsüberprüfung
Ein Gesichtserkennungssystem versucht in der Regel, anhand von zwei Bildern von Gesichtern festzustellen, ob die beiden Bilder von derselben Person stammen. Dabei handelt es sich um einen hochpräzisen binären Klassifikator, der mit unterschiedlichen Beleuchtungen, Kleidungsstücken, Frisuren, Hintergründen und Gesichtsausdrücken zurechtkommen muss. Erschwerend kommt hinzu, dass es zwar Bilder von vielen Personen gibt, z. B. in einer Mitarbeiterdatenbank, aber nur eine Handvoll Bilder von ein und derselben Person vorhanden sind. Auch die Identifizierung von Unterschriften in Banken und die Identifizierung von Produkten auf Amazon haben mit der gleichen Herausforderung zu kämpfen: Es gibt nur wenige Bilder pro Artikel.
Wie würdest du einen solchen Klassifikator trainieren? Die Auswahl von Embeddings aus einem Modell wie ResNet, das auf ImageNet trainiert wurde, könnte diese feinen Gesichtsmerkmale nicht erkennen. Ein Ansatz besteht darin, jede Person als eigene Klasse zu betrachten und dann wie ein normales Netzwerk zu trainieren. Dabei ergeben sich zwei wichtige Probleme:
-
Wenn wir eine Million Menschen hätten, wäre eine Ausbildung für eine Million Kategorien nicht machbar.
-
Das Training mit wenigen Bildern pro Klasse führt zu Übertraining.
Ein weiterer Gedanke: Anstatt verschiedene Kategorien zu lehren, könnten wir einem Netzwerk beibringen, direkt zu vergleichen und zu entscheiden, ob ein Bildpaar ähnlich oder unähnlich ist, indem wir während des Trainings Hinweise auf ihre Ähnlichkeit geben. Und das ist der Kerngedanke von Siamesischen Netzen. Du nimmst ein Modell, gibst zwei Bilder ein, extrahierst zwei Einbettungen und berechnest dann den Abstand zwischen den beiden Einbettungen. Wenn der Abstand unter einem Schwellenwert liegt, werden sie als ähnlich angesehen, andernfalls nicht. Indem du ein Bildpaar mit dem zugehörigen Label "ähnlich" oder "unähnlich" einspeist und das Netzwerk von einem Ende zum anderen trainierst, beginnen die Einbettungen, die feinkörnige Darstellung der Eingaben zu erfassen. Dieser in Abbildung 4-18 dargestellte Ansatz der direkten Optimierung für die Distanzmetrik wird metrisches Lernen genannt.
Wir könnten diese Idee erweitern und sogar drei Bilder einspeisen. Wähle ein Ankerbild, ein weiteres positives Beispiel (aus derselben Kategorie) und ein weiteres negatives Beispiel (aus einer anderen Kategorie). Nun trainieren wir dieses Netzwerk so, dass der Abstand zwischen ähnlichen Objekten minimiert und der Abstand zwischen unähnlichen Objekten maximiert wird. Die Verlustfunktion, die uns dabei hilft, nennt man Triplett-Verlustfunktion. Im vorherigen Fall mit einem Bildpaar wird die Verlustfunktion als kontrastive Verlustfunktion bezeichnet. Die Triplett-Verlustfunktion liefert in der Regel bessere Ergebnisse.
Nachdem das Netzwerk trainiert hat, brauchen wir nur noch ein Referenzbild eines Gesichts, um zum Testzeitpunkt zu entscheiden, ob es sich um dieselbe Person handelt. Diese Methode öffnet die Türen für One-Shot-Learning. Weitere gängige Anwendungen sind die Erkennung von Unterschriften und Logos. Eine bemerkenswert kreative Anwendung von Saket Maheshwary und Hemant Misra ist die Verwendung eines siamesischen Netzwerks für den Abgleich von Lebensläufen mit Bewerbern, indem die semantische Ähnlichkeit zwischen beiden berechnet wird.
Fallstudien
Schauen wir uns unter ein paar interessante Beispiele an, die zeigen, wie das, was wir bisher gelernt haben, in der Branche angewendet wird.
Flickr
Flickr ist eine der größten Foto-Sharing-Websites, die besonders bei professionellen Fotografen beliebt ist. Um Fotografen bei der Suche nach Inspiration zu helfen und Inhalte zu präsentieren, die für die Nutzer interessant sein könnten, hat Flickr eine Ähnlichkeitssuche entwickelt, die auf der gleichen semantischen Bedeutung basiert. Wie in Abbildung 4-19 dargestellt, führt die Suche nach einem Wüstenmuster zu mehreren ähnlich gemusterten Ergebnissen. Unter der Haube verwendet Flickr einen ANN-Algorithmus namens Locally Optimized Product Quantization (LOPQ), der sowohl in Python als auch in Spark-Implementierungen als Open Source verfügbar ist.
Pinterest ist eine Anwendung, die vor allem wegen ihrer visuellen Suchfunktionen genutzt wird, genauer gesagt wegen der Funktionen Similar Pins und Related Pins. Andere Unternehmen wie Baidu und Alibaba haben ähnliche visuelle Suchsysteme eingeführt. Auch Zappos, Google Shopping und like.com nutzen Computer Vision für Empfehlungen.
Auf Pinterest ist "Damenmode" eines der beliebtesten Themen für Pins und die Funktion "Similar Looks"(Abbildung 4-20) hilft den Leuten, ähnliche Produkte zu entdecken. Außerdem berichtet Pinterest, dass seine Related Pins-Funktion die Repin-Rate erhöht hat. Nicht jeder Pin auf Pinterest verfügt über zugehörige Metadaten, was die Empfehlung aufgrund des fehlenden Kontexts zu einem schwierigen Problem für den Kaltstart macht. Die Entwickler von Pinterest haben dieses Problem gelöst, indem sie die visuellen Funktionen für die Generierung der verwandten Pins genutzt haben. Darüber hinaus implementiert Pinterest einen inkrementellen Fingerabdruckdienst, der neue digitale Signaturen erzeugt, wenn entweder ein neues Bild hochgeladen wird oder wenn sich die Merkmale weiterentwickeln (aufgrund von Verbesserungen oder Änderungen an den zugrunde liegenden Modellen durch die Ingenieure).
Berühmte Doppelgänger
Auf der Website suchen Anwendungen wie Celebslike.me, die 2015 viral gingen, nach dem nächsten Nachbarn unter den Berühmtheiten, wie in Abbildung 4-21 gezeigt. Einen ähnlichen viralen Ansatz verfolgte 2018 die Google Arts & Culture App, die das nächstgelegene existierende Porträt zu deinem Gesicht zeigt. Twins or not ist eine weitere Anwendung mit einem ähnlichen Ziel.
Spotify
Spotify verwendet Nearest Neighbors, um Musik zu empfehlen und automatische Wiedergabelisten und Radiosender zu erstellen, die auf den aktuell gespielten Songs basieren. Normalerweise sind kollaborative Filtertechniken, die für die Empfehlung von Inhalten wie Filmen auf Netflix eingesetzt werden, inhaltsunabhängig, d. h. die Empfehlung erfolgt, weil große Gruppen von Nutzern mit ähnlichem Geschmack ähnliche Filme ansehen oder ähnliche Songs hören. Dies stellt ein Problem für neue und noch nicht beliebte Inhalte dar, da die Nutzer/innen immer wieder Empfehlungen für bereits beliebte Inhalte erhalten. Dies wird auch als das bereits erwähnte Kaltstartproblem bezeichnet. Die Lösung besteht darin, das latente Verständnis der Inhalte zu nutzen. Ähnlich wie bei Bildern können wir mit Hilfe von MFCC-Merkmalen (Mel Frequency Cepstral Coefficients) Merkmalsvektoren aus Musik erstellen, die wiederum ein 2D-Spektrogramm erzeugen, das als Bild betrachtet und zur Erzeugung von Merkmalen verwendet werden kann. Die Lieder werden in dreisekündige Fragmente unterteilt und ihre Spektrogramme werden zur Erstellung von Merkmalen verwendet. Diese Merkmale werden dann gemittelt, um den kompletten Song darzustellen. Abbildung 4-22 zeigt Künstler, deren Songs in bestimmten Bereichen projiziert werden. Wir können Hip-Hop (oben links), Rock (oben rechts), Pop (unten links) und elektronische Musik (unten rechts) erkennen. Wie bereits erwähnt, verwendet Spotify Annoy im Hintergrund.
Zusammenfassung
Jetzt sind wir am Ende einer erfolgreichen Expedition angelangt, bei der wir das Auffinden ähnlicher Bilder mit Hilfe von Einbettungen untersucht haben. Wir sind noch einen Schritt weiter gegangen und haben untersucht, wie sich die Suche mit Hilfe von ANN-Algorithmen und -Bibliotheken wie Annoy, NGT und Faiss von ein paar Tausend auf ein paar Milliarden Dokumente erweitern lässt. Wir haben auch gelernt, dass die Feinabstimmung des Modells auf deinen Datensatz die Genauigkeit und Repräsentativität von Einbettungen in einer überwachten Umgebung verbessern kann. Außerdem haben wir uns angeschaut, wie Siamesische Netze eingesetzt werden können, die die Leistung von Einbettungen für One-Shot-Lernen nutzen, z. B. für Gesichtsverifikationssysteme. Schließlich haben wir untersucht, wie Nearest-Neighbor-Ansätze in verschiedenen Anwendungsfällen in der Industrie eingesetzt werden. Nächste Nachbarn sind ein einfaches, aber leistungsstarkes Werkzeug, das du in deinem Werkzeugkasten haben solltest.
Get Praktisches Deep Learning für Cloud, Mobile und Edge now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.