Das Entwurfsmuster Feature Cross hilft Modellen, Beziehungen zwischen Eingaben schneller zu lernen, indem explizit jede Kombination von Eingabewerten zu einem eigenen Feature gemacht wird.
Sehen Sie sich den Datensatz in Abbildung 2-14 an. Die Aufgabe besteht darin, einen binären Klassifizierer zu erstellen, der die Plus- und Minuszeichen voneinander trennt.
Wenn man nur die x_1- und x_2-Koordinaten verwendet, ist es nicht möglich, eine lineare Grenze zu finden, die die Plus- und Minusklassen trennt.
Um also dieses Problem zu lösen, müssen wir das Modell komplexer machen, vielleicht mit zusätzlichen Schichten. Es gibt aber eine einfachere Lösung.
Abbildung 2-14: Allein mit »x_1« und »x_2« als Eingaben ist dieser Datensatz nicht linear separierbar.
Im maschinellen Lernen greift das Feature Engineering auf Domänenwissen zurück, um neue Features zu erzeugen, die das maschinelle Lernen unterstützen und die Vorhersagekraft unseres Modells erhöhen. Eine häufig verwendete Feature-Engineering-Technik ist das Erstellen eines Feature Cross.
Ein Feature Cross ist ein synthetisches Feature, das aus der Verkettung von zwei oder mehr kategorialen Features entsteht, um die Interaktion zwischen ihnen zu erfassen. Indem zwei Features auf diese Weise verknüpft werden, ist es möglich, Nichtlinearität im Modell zu codieren, was Vorhersagemöglichkeiten eröffnet, die über das hinausgehen, was jedes der Features an sich hätte leisten können. Feature Crosses erlauben einem ML-Modell, Beziehungen zwischen den Features schneller zu lernen. Während komplexere Modelle wie neuronale Netze und Bäume Feature Crosses selbstständig erlernen können, ist es durch die explizite Verwendung von Feature Crosses möglich, lediglich ein lineares Modell trainieren zu müssen. Folglich können Feature Crosses das Modelltraining beschleunigen (und damit preiswerter machen) und die Modellkomplexität verringern (weniger Trainingsdaten werden benötigt).
Um eine Feature-Spalte für den obigen Datensatz zu erstellen, können wir x_1 und x_2 abhängig von ihrem Vorzeichen jeweils auf zwei Buckets aufteilen. Damit konvertieren wir x_1 und x_2 zu kategorialen Features. Mit A kennzeichnen wir den Bucket, für den x_1 >= 0 gilt, und mit B den Bucket, für den x_1 < 0 ist. Zudem soll C den Bucket für x_2 >= 0 und D den Bucket für x_2 < 0 kennzeichnen (siehe Abbildung 2-15).
Abbildung 2-15: Das Feature Cross führt vier neue boolesche Features ein.
Ein Feature Cross dieser in Buckets eingeordneten Features führt vier neue boolesche Features für unser Modell ein:
AC, wenn x_1 >= 0 und x_2 >= 0
BC, wenn x_1 < 0 und x_2 >= 0
AD, wenn x_1 >= 0 und x_2 < 0
BD, wenn x_1 < 0 und x_2 < 0
Jedes dieser vier booleschen Features (AC, BC, AD und BD) erhält beim Training des Modells sein eigenes Gewicht. Das heißt, wir können jeden Quadranten als eigenes Feature behandeln. Da der Originaldatensatz perfekt auf die von uns erstellten Buckets aufgeteilt wurde, ist ein Feature Cross von A und B in der Lage, den Datensatz linear zu trennen.
Doch dies ist lediglich eine Veranschaulichung. Wie sieht es mit realen Daten aus? Nehmen wir einen öffentlichen Datensatz von Taxifahrten in New York City (siehe Tabelle 2-8).6
Tabelle 2-8: Eine Vorschau auf den öffentlichen Datensatz mit Taxifahrten in New York City in BigQuery
Dieser Datensatz enthält Informationen zu Taxifahrten in New York City mit Features wie dem Zeitstempel der Abholung, dem Breiten- und Längengrad der Abholung und des Ziels sowie der Anzahl der Fahrgäste. Das hier verwendete Label fare_amount steht für die Kosten der Taxifahrt. Welche Feature Crosses könnten für diesen Datensatz relevant sein?
Es könnten viele sein. Sehen wir uns pickup_datetime an. Von diesem Feature können wir Informationen über die Stunde und den Wochentag der Fahrt verwenden. Es sind kategoriale Variablen, und zweifellos besitzen beide Vorhersagepotenzial, wenn es um die Ermittlung des Fahrpreises geht. Für diesen Datensatz ist ein Feature Cross von day_of_week und hour_of_day sinnvoll, denn man kann davon ausgehen, dass Taxifahrten am Montag um 17:00 Uhr anders abgerechnet werden als Fahrten am Freitag um 5:00 Uhr (siehe Tabelle 2-9).
Tabelle 2-9: Eine Vorschau auf die Daten, mit denen wir ein Feature Cross erstellen: die Spalten Wochentag und Tageszeit
day_of_week |
hour_of_day |
Sunday |
00 |
Sunday |
01 |
… |
… |
Saturday |
23 |
Ein Feature Cross dieser beiden Features ergibt einen 168-dimensionalen 1-aus-n-codierten Vektor (24 Stunden × 7 Tage = 168). Das Beispiel »Montag um 17 Uhr« belegt dann einen einzigen Index (day_of_week ist Montag verkettet mit hour_of_day ist 17).
Die beiden Features sind für sich genommen wichtig. Wenn wir aber ein Feature Cross von hour_of_day und day_of_week vorsehen, kann ein Modell zur Fahrpreisvorhersage leichter erkennen, dass die Rushhour am Ende der Woche die Fahrzeit und damit den Taxitarif auf ihre eigene Weise beeinflusst.
Das Feature Cross erzeugen wir in BigQuery mithilfe der Funktion ML.FEATURE_CROSS, der wir eine Struktur mit den Features day_of_week und hour_of_day übergeben:
ML.FEATURE_CROSS(STRUCT(day_of_week,hour_of_week)) AS day_X_hour
Die STRUCT-Klausel erzeugt ein geordnetes Paar aus den beiden Features. Wenn unser Software-Framework keine Feature-Cross-Funktion unterstützt, lässt sich der gleiche Effekt mit einer Zeichenfolgenverkettung erreichen:
CONCAT(CAST(day_of_week AS STRING),
CAST(hour_of_week AS STRING)) AS day_X_hour
Ein komplettes Trainingsbeispiel für das Geburtenproblem sehen Sie unten, wobei ein Feature Cross der Spalten is_male und plurality als Feature verwendet wird. Den vollständigen Code finden Sie im Repository zum Buch (https://github.com/GoogleCloudPlatform/ml-design-patterns/blob/master/02_data_representation/feature_cross.ipynb):
CREATE OR REPLACE MODEL babyweight.natality_model_feat_eng
TRANSFORM(weight_pounds,
is_male,
plurality,
gestation_weeks,
mother_age,
CAST(mother_race AS string) AS mother_race,
ML.FEATURE_CROSS(
STRUCT(
is_male,
plurality)
) AS gender_X_plurality)
OPTIONS
(MODEL_TYPE='linear_reg',
INPUT_LABEL_COLS=['weight_pounds'],
DATA_SPLIT_METHOD="NO_SPLIT") AS
SELECT
*
FROM
babyweight.babyweight_data_train
![]() |
Das Muster Transformation (siehe Kapitel 6) wird hier für das Feature Engineering im Geburtenmodell verwendet. Dadurch kann sich das Modell auch »daran erinnern«, das Feature Cross der Eingabedatenfelder bei der Vorhersage durchzuführen. |
Wenn wir über genügend Daten verfügen, erlaubt das Muster Feature Cross es auch, dass die Modelle einfacher werden. Für ein lineares Modell mit dem Muster Feature Cross beträgt der RMSE7 im Geburtendatensatz 1,056. Alternativ dazu liefert das Training eines tiefen neuronalen Netzes in BigQuery ML auf demselben Datensatz ohne Feature Crosses einen RMSE von 1,074. Trotz der Verwendung eines wesentlich einfacheren linearen Modells ist eine leichte Verbesserung der Performance zu verzeichnen. Auch die Zeit für das Training ist drastisch reduziert.
Um ein Feature Cross mit den Features is_male und plurality in TensorFlow zu implementieren, verwenden wir tf.feature_column.crossed_column. Die Methode crossed_column übernimmt zwei Argumente: eine Liste der zu kreuzenden Feature-Schlüssel und die Hash-Bucket-Größe. Da für gekreuzte Features ein Hashwert entsprechend der hash_bucket_size erzeugt wird, sollte der Bucket groß genug sein, um die Wahrscheinlichkeit für Kollisionen komfortabel zu verringern. Die Eingabe is_male kann drei Werte (True, False, Unknown) annehmen, die Eingabe plurality sechs Werte (Single(1), Twins(2), Triplets(3), Quadruplets(4), Quintuplets(5), Multiple(2+)). Es gibt also 18 mögliche (is_male, plurality)-Paare. Wenn wir hash_bucket_size auf 1000 setzen, können wir zu 85 % sicher sein, dass es keine Kollisionen gibt.
Um schließlich eine gekreuzte Spalte in einem DNN-Modell zu verwenden, müssen wir sie entweder in eine indicator_column oder eine embedding_column einhüllen, je nachdem, ob wir sie 1-aus-n-codieren oder in einer niedrigeren Dimension darstellen wollen (siehe den Abschnitt »Entwurfsmuster 2: Einbettungen« auf Seite 58 in diesem Kapitel):
gender_x_plurality = fc.crossed_column(["is_male", "plurality"],
hash_bucket_size=1000)
crossed_feature = fc.embedding_column(gender_x_plurality, dimension=2)
oder
gender_x_plurality = fc.crossed_column(["is_male", "plurality"],
hash_bucket_size=1000)
crossed_feature = fc.indicator_column(gender_x_plurality)
Feature Crosses sind ein wertvolles Instrument des Feature Engineering. Sie bieten mehr Komplexität, mehr Ausdruckskraft und mehr Kapazität für einfache Modelle. Denken Sie noch einmal an das gekreuzte Feature von is_male und plurality im Geburtendatensatz. Dieses Feature-Cross-Muster ermöglicht dem Modell, männliche Zwillinge getrennt von weiblichen Zwillingen und getrennt von männlichen Drillingen und getrennt von weiblichen Einzelkindern usw. zu behandeln. Mit einer indicator_column ist das Modell in der Lage, jede der resultierenden Kreuzungen als unabhängige Variable zu behandeln, wodurch im Wesentlichen 18 zusätzliche binäre kategoriale Features zum Modell hinzugefügt werden (siehe Abbildung 2-16).
Feature Crosses lassen sich gut für große Datenmengen skalieren. Während zusätzliche Schichten in einem tiefen neuronalen Netz genügend Nichtlinearität einbringen könnten, um zu lernen, wie sich (is_male, plurality)-Paare verhalten, erhöht sich dadurch auch die Trainingszeit drastisch. Im Geburtendatensatz haben wir beobachtet, dass ein lineares Modell mit einem Feature Cross, das in BigQuery ML trainiert wird, eine vergleichbare Performance wie ein DNN erzielt, das ohne ein Feature Cross trainiert wurde. Allerdings lässt sich das lineare Modell erheblich schneller trainieren.
Abbildung 2-16: Ein Feature Cross zwischen »is_male« und »plurality« erzeugt zusätzliche 18 binäre Features in unserem ML-Modell.
Tabelle 2-10 vergleicht die Trainingszeit in BigQuery ML und den Bewertungsverlust sowohl für ein lineares Modell mit einem Feature Cross von (is_male, plurality) als auch für ein tiefes neuronales Netz ohne Feature Cross.
Tabelle 2-10: Ein Vergleich von BigQuery-ML-Trainingsmetriken für Modelle mit und ohne Features Crosses
Eine einfache lineare Regression erreicht einen vergleichbaren Fehler beim Bewertungssatz, trainiert aber hundertmal schneller. Die Kombination von Feature Crosses mit massiven Daten ist eine alternative Strategie, um komplexe Beziehungen in Trainingsdaten zu lernen.
Feature Crosses haben wir zwar als Möglichkeit vorgestellt, kategoriale Variablen zu verarbeiten, doch sie lassen sich mit ein wenig Vorverarbeitung auch auf numerische Features anwenden. Feature Crosses bewirken eine Dünnbesetztheit (Sparsity) in Modellen und werden oft zusammen mit Techniken eingesetzt, die dieser Dünnbesetztheit entgegenwirken.
Wir würden niemals ein Feature Cross mit einer kontinuierlichen Eingabe erzeugen. Denn wenn die eine Eingabe m mögliche Werte annimmt und eine andere Eingabe n mögliche Werte, ergibt sich ein Feature Cross mit m * n Elementen. Eine numerische Eingabe ist dicht und nimmt ein Kontinuum von Werten an. Es wäre unmöglich, alle möglichen Werte in einem Feature Cross von kontinuierlichen Eingabedaten aufzuzählen.
Handelt es sich um kontinuierliche Daten, können wir sie stattdessen in Buckets unterteilen, um sie kategorial zu machen, bevor wir ein Feature Cross anwenden. Nehmen wir als Beispiel Breiten- und Längengrade. Dies sind kontinuierliche Eingaben, und es ist intuitiv sinnvoll, ein Feature Cross aus diesen Eingaben zu erstellen, da der Standort durch ein geordnetes Paar aus Breite und Länge bestimmt wird. Anstatt jedoch ein Feature Cross mit den rohen Werten für Breiten- und Längengrad zu erstellen, kategorisieren wir diese kontinuierlichen Werte und kreuzen die daraus entstandenen binned_latitude und binned_longitude:
import tensorflow.feature_column as fc
# Eine Bucket-Feature-Spalte für den Breitengrad erzeugen. latitude_as_numeric = fc.numeric_column("latitude") lat_bucketized = fc.bucketized_column(latitude_as_numeric,
lat_boundaries)
# Eine Bucket-Feature-Spalte für den Längengrad erzeugen. longitude_as_numeric = fc.numeric_column("longitude") lon_bucketized = fc.bucketized_column(longitude_as_numeric,
lon_boundaries)
# Ein Feature Cross aus Breite und Länge erzeugen.
lat_x_lon = fc.crossed_column([lat_bucketized, lon_bucketized],
hash_bucket_size=nbuckets**4)
crossed_feature = fc.indicator_column(lat_x_lon)
Da die Kardinalität der resultierenden Kategorien aus einem Feature Cross multiplikativ mit der Kardinalität der Eingabe-Features zunimmt, führen Feature Crosses zu dünn besetzten Modelleingaben. Selbst mit dem Feature Cross aus day_of_week und hour_of_day würde ein Feature Cross zu einem dünn besetzten Vektor der Dimension 168 werden (siehe Abbildung 2-17).
Es kann sinnvoll sein, ein Feature Cross über eine Einbettungsschicht zu übergeben (siehe den Abschnitt »Entwurfsmuster 2: Einbettungen« auf Seite 58 in diesem Kapitel), um eine Darstellung mit weniger Dimensionen zu erzeugen, wie Abbildung 2-18 zeigt.
Abbildung 2-17: Ein Feature Cross aus »day_of_week« und »hour_of_day« ergibt einen dünn besetzten Vektor der Dimension 168.
Abbildung 2-18: Eine Einbettungsschicht ist eine nützliche Methode, um mit dem dünn besetzten Feature Cross umzugehen.
Wie weiter oben erläutert, können wir mit dem Entwurfsmuster Einbettungen die Nähe von Beziehungen erfassen. Wenn wir also das Feature Cross über eine Einbettungsschicht an das Modell übergeben, kann das Modell verallgemeinern, wie bestimmte Feature Crosses, die aus Paaren von Stunden- und Tageskombinationen stammen, die Ausgabe des Modells beeinflussen. Im obigen Beispiel von Breite und Länge könnten wir eine Einbettungs-Feature-Spalte anstelle der Indikatorspalte verwenden:
crossed_feature = fc.embedding_column(lat_x_lon, dimension=2)
Wenn man zwei kategoriale Features, jeweils mit hoher Kardinalität, kreuzt, entsteht ein Cross Feature mit multiplikativer Kardinalität. Natürlich kann die Anzahl der Kategorien in einem Feature Cross drastisch steigen, je mehr Kategorien ein einzelnes Feature hat. Wenn dies so weit geht, dass einzelne Buckets zu wenige Elemente enthalten, ist das Modell nicht mehr in der Lage zu verallgemeinern. Nehmen Sie das Beispiel der Breiten- und Längengrade. Wenn wir sehr feine Buckets für Breiten- und Längengrade nehmen, würde ein Feature Cross so genau sein, dass sich das Modell jeden Punkt auf der Karte merken könnte. Beruht dieses Merken jedoch nur auf einer Handvoll Beispiele, würde es sich tatsächlich um eine Überanpassung handeln.
Nehmen Sie zur Veranschaulichung das Beispiel mit der Vorhersage des Taxitarifs in New York für die gegebenen Einstiegs- und Ausstiegsorte sowie die Einstiegszeit:8
CREATE OR REPLACE MODEL mlpatterns.taxi_l2reg
TRANSFORM(
fare_amount
, ML.FEATURE_CROSS(STRUCT(CAST(EXTRACT(DAYOFWEEK FROM pickup_datetime)
AS STRING) AS dayofweek,
CAST(EXTRACT(HOUR FROM pickup_datetime)
AS STRING) AS hourofday), 2) AS day_hr
, CONCAT(
ML.BUCKETIZE(pickuplon, GENERATE_ARRAY(-78, -70, 0.01)),
ML.BUCKETIZE(pickuplat, GENERATE_ARRAY(37, 45, 0.01)),
ML.BUCKETIZE(dropofflon, GENERATE_ARRAY(-78, -70, 0.01)),
ML.BUCKETIZE(dropofflat, GENERATE_ARRAY(37, 45, 0.01))
) AS pickup_and_dropoff
)
OPTIONS(input_label_cols=['fare_amount'],
model_type='linear_reg', l2_reg=0.1)
AS
SELECT * FROM mlpatterns.taxi_data
Hier gibt es zwei Feature Crosses: eines für die Zeit (Wochentag und Stunde des Tages) und das andere für den Raum (die Einstiegs- und Ausstiegsorte). Da insbesondere der Ort eine sehr hohe Kardinalität hat, ist es wahrscheinlich, dass in manche Buckets nur sehr wenige Beispiele fallen.
Deshalb ist es ratsam, Feature Crosses mit einer Regularisierung zu kombinieren: mit einer L1-Regularisierung, die Dünnbesetztheit von Features fördert, oder mit einer L2-Regularisierung, die Überanpassung begrenzt. Dadurch ist unser Modell in der Lage, Störgeräusche zu ignorieren, die durch die vielen synthetischen Features generiert werden, und Überanpassung zu bekämpfen. Und tatsächlich verbessert die Regularisierung auf diesem Datensatz den RMSE geringfügig, nämlich um 0,3 %.
Außerdem sollte man bei Features, die in einem Feature Cross kombiniert werden sollen, darauf achten, dass die betreffenden Features nicht stark korreliert sind. Ein Feature Cross kann man sich so vorstellen, dass zwei Features kombiniert werden, um ein geordnetes Paar zu erzeugen. In der Tat bezieht sich der Begriff »Cross« aus Feature Cross auf das kartesische Produkt. Wenn zwei Features stark korreliert sind, bringt das »Produkt« ihres Feature Cross keine neuen Informationen in das Modell ein. Als extremes Beispiel nehmen wir die zwei Features x_1 und x_2 an, wobei x_2 = 5*x_1 gilt. Teilt man die Werte für x_1 und x_2 nach ihrem Vorzeichen in Buckets auf und erzeugt ein Feature Cross, entstehen noch vier neue boolesche Features. Aufgrund der Abhängigkeit von x_1 und x_2 werden jedoch zwei dieser vier Features tatsächlich leer sein, und die beiden anderen sind genau die beiden Buckets, die für x_1 eingerichtet wurden.
Das Entwurfsmuster Multimodale Eingabe befasst sich mit dem Problem der Darstellung von Daten, die verschiedene Typen haben oder die sich auf komplexe Art und Weise ausdrücken lassen, indem alle verfügbaren Datendarstellungen verkettet werden.
In der Regel lässt sich eine Eingabe in ein Modell als Zahl oder als Kategorie darstellen, als Bild oder als formatfreier Text. Viele Standardmodelle sind nur für bestimmte Eingabetypen definiert – zum Beispiel kann ein Standardbildklassifizierungsmodell wie Resnet-50 keine anderen Eingaben als Bilder verarbeiten.
Um zu verstehen, weshalb multimodale Eingaben erforderlich sind, nehmen wir eine Kamera an, die an einer Kreuzung Filmmaterial aufnimmt, um Verkehrsverstöße zu identifizieren. Unser Modell soll sowohl Bilddaten (das Filmmaterial der Kamera) als auch einige Metadaten zum Zeitpunkt der Bildaufnahme (Tageszeit, Wochentag, Wetter usw.) erfassen, wie Abbildung 2-19 zeigt.
Dieses Problem tritt ebenfalls auf, wenn ein strukturiertes Datenmodell trainiert wird, bei dem eine der Eingaben formatfreier Text ist. Im Gegensatz zu numerischen Daten kann man Bilder und Text nicht direkt in ein Bild einspeisen. Daher müssen wir Bild- und Texteingaben in einer für das Modell verständlichen Form darstellen (üblicherweise mithilfe des Entwurfsmusters Einbettungen) und dann diese Eingaben mit anderen tabellarischen9 Features kombinieren. So könnten wir etwa die Bewertung eines Restaurantbesuchers auf der Grundlage seines Bewertungstexts und anderer Attribute vorhersagen, beispielsweise was der Gast bezahlt hat und ob es ein Mittag- oder Abendessen war (siehe Abbildung 2-20).
Abbildung 2-19: Modell, das Bild- und numerische Features kombiniert, um vorherzusagen, ob das Filmmaterial von einer Kreuzung einen Verkehrsverstoß darstellt
Abbildung 2-20: Modell, das Freiformtexteingaben mit tabellarischen Daten kombiniert, um die Bewertung einer Restaurantrezension vorherzusagen
Beginnen wir mit dem obigen Beispiel, das Text aus einer Restaurantkritik mit tabellarischen Metadaten über das Essen, auf das sich die Kritik bezieht, zusammenbringt. Zuerst kombinieren wir die numerischen und kategorialen Features. Für meal_type gibt es drei Optionen, die wir in eine 1-aus-n-Codierung überführen können. Das Abendessen stellen wir dann als [0, 0, 1] dar. Dieses kategoriale Feature, das als Array dargestellt wird, können wir nun mit meal_total kombinieren, indem wir den Preis der Mahlzeit als viertes Element des Arrays hinzufügen: [0, 0, 1, 30.5].
Das Entwurfsmuster Einbettungen ist ein gängiger Ansatz, um Text für ML-Modelle zu codieren. Hätte unser Modell nur Text zu verarbeiten, würden wir ihn mit dem folgenden tf.keras-Code als Einbettungsschicht darstellen:
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Embedding
model = Sequential()
model.add(Embedding(batch_size, 64, input_length=30))
Hier müssen wir die Einbettung abflachen,10 um sie mit meal_type und meal_total zu verketten:
model.add(Flatten())
Wir könnten dann mit einer Reihe von Dense-Schichten dieses sehr große Array11 in kleinere Arrays transformieren, bis wir zu unserer Ausgabe als Array von – sagen wir – drei Zahlen gelangen:
model.add(Dense(3, activation="relu"))
Nun müssen wir diese drei Zahlen, die die Satzeinbettung der Kritik bilden, mit den früheren Eingaben verketten: [0, 0, 1, 30.5, 0.75, -0.82, 0.45].
Hierfür verwenden wir die funktionale API von Keras und wenden die gleichen Schritte an. Schichten, die mit der funktionalen API erstellt werden, sind aufrufbar, sodass wir sie miteinander verketten können, beginnend bei der Input-Schicht.12 Um davon Gebrauch zu machen, definieren wir zunächst unsere Einbettungs- und Tabellenschichten:
embedding_input = Input(shape=(30,))
embedding_layer = Embedding(batch_size, 64)(embedding_input)
embedding_layer = Flatten()(embedding_layer)
embedding_layer = Dense(3, activation='relu')(embedding_layer)
tabular_input = Input(shape=(4,))
tabular_layer = Dense(32, activation='relu')(tabular_input)
Wir haben hier die Input-Teile dieser beiden Schichten als ihre eigenen Variablen definiert. Das hängt damit zusammen, dass wir Eingabeschichten übergeben müssen, wenn wir ein Modell mit der funktionalen API erzeugen. Als Nächstes erstellen wir eine verkettete Schicht, speisen diese in unsere Ausgabeschicht ein und erzeugen schließlich das Modell, indem wir die ursprünglichen, oben definierten Input-Schichten übergeben:
merged_input = keras.layers.concatenate([embedding_layer, tabular_layer])
merged_dense = Dense(16)(merged_input)
output = Dense(1)(merged_dense)
model = Model(inputs=[embedding_input, tabular_input], outputs=output)
merged_dense = Dense(16, activation='relu')(merged_input)
output = Dense(1)(merged_dense)
model = Model(inputs=[embedding_input, tabular_input], outputs=output)
Nun verfügen wir über ein geschlossenes Modell, das die multimodale Eingabe akzeptiert.
Wie eben gezeigt, untersucht das Entwurfsmuster Multimodale Eingabe, wie man verschiedene Eingabeformate im selben Modell darstellen kann. Wir möchten aber nicht nur verschiedene Datentypen mischen, sondern auch dieselben Daten auf verschiedene Weise darstellen können, damit unser Modell es leichter hat, Muster zu identifizieren. Wir könnten zum Beispiel ein Bewertungsfeld, dessen ordinale Skala von 1 bis 5 Sternen reicht, sowohl numerisch als auch kategorial behandeln. Unter multimodalen Eingaben verstehen wir hier Folgendes:
Wir untersuchen zunächst, wie sich tabellarische Daten auf verschiedene Arten darstellen lassen, und kommen dann zu Text- und Bilddaten.
Um zu sehen, wie wir tabellarische Daten auf verschiedene Arten für dasselbe Modell darstellen können, kehren wir zum Beispiel der Restaurantbewertung zurück. Dabei stellen wir uns vor, dass wir stattdessen die Bewertung als Eingabe für unser Modell verwenden, und versuchen, die Nützlichkeit der Bewertung vorherzusagen (wie viele Personen die Kritik für gut befunden haben). Als Eingabe kann die Bewertung sowohl als Ganzzahl im Bereich von 1 bis 5 als auch als kategoriales Feature dargestellt werden. Um die Bewertung kategorial darzustellen, können wir sie in Buckets einteilen. Wie das geschieht, bleibt uns überlassen und hängt von unserem Datensatz und dem Anwendungsfall ab. Der Einfachheit halber sagen wir, dass wir zwei Buckets erstellen wollen: »gut« und »schlecht«. Der Bucket »gut« umfasst die Bewertungen 4 und 5, der Bucket »schlecht« die Bewertungen ab 3 abwärts. Wir können dann mit einem booleschen Wert die Bewertungs-Buckets codieren und sowohl den ganzzahligen als auch den booleschen Teil in einem einzigen Array verketten. (Den vollständigen Code finden Sie auf GitHub unter https://github.com/GoogleCloudPlatform/ml-design-patterns/blob/master/02_data_representation/mixed_representation.ipynb.)
Für einen kleinen Datensatz mit drei Datenpunkten könnte das wie folgt aussehen:
rating_data = [2, 3, 5]
def good_or_bad(rating):
if rating > 3:
return 1
else:
return 0
rating_processed = []
for i in rating_data:
rating_processed.append([i, good_or_bad(i)])
Das resultierende Feature ist ein 2-elementiges Array, das aus der ganzzahligen Bewertung und ihrer booleschen Darstellung besteht:
[[2, 0], [3, 0], [5, 1]]
Hätten wir uns stattdessen entschieden, mehr als zwei Buckets einzurichten, würden wir jede Eingabe 1-aus-n-codieren und dieses 1-aus-n-Array an die Ganzzahldarstellung anfügen.
Die Bewertung auf zwei Arten darzustellen, ist deshalb nützlich, weil die mit 1 bis 5 Sternen angegebene Bewertung nicht unbedingt linear ansteigt. Bewertungen von 4 und 5 sind sehr ähnlich, und Bewertungen von 1 bis 3 weisen höchstwahrscheinlich darauf hin, dass der Kritiker unzufrieden war. Ob Sie etwas, das Ihnen nicht gefällt, einen, zwei oder drei Sterne geben, hängt oft eher mit Ihren Bewertungstendenzen als mit der Kritik an sich zusammen. Dennoch ist es sinnvoll, die detaillierten Informationen bei der Sternebewertung zu behalten, weshalb wir sie auf zwei Arten codieren.
Betrachten Sie außerdem auch Features mit einem größeren Bereich als 1 bis 5, etwa die Entfernung zwischen dem Wohnort eines Kritikers und einem Restaurant. Wenn jemand zwei Stunden fährt, um ein Restaurant zu besuchen, kann seine Bewertung kritischer ausfallen als bei jemandem, der gleich gegenüber wohnt. Da hierdurch auch mit Ausreißerwerten zu rechnen ist, wäre es sinnvoll, sowohl die numerische Entfernungsdarstellung auf beispielsweise 50 Kilometer zu begrenzen als auch eine separate kategoriale Darstellung der Entfernung aufzunehmen. Das kategoriale Feature ließe sich in Buckets wie »im Bundesland«, »im Land« und »im Ausland« einteilen.
Sowohl Textdaten als auch Bilder sind unstrukturiert und erfordern mehr Transformationen als tabellarische Daten. Stellt man sie in verschiedenen Formaten dar, können unsere Modelle möglicherweise mehr Muster extrahieren. Wir bauen nun auf der Diskussion über Textmodelle im vorherigen Abschnitt auf und gehen näher auf verschiedene Ansätze für die Darstellung von Textdaten ein. Dann kommen wir zu Bildern und untersuchen einige Optionen, um Bilddaten in ML-Modellen darzustellen.
Verschiedene Arten von Textdaten.Angesichts der komplexen Natur von Textdaten gibt es viele Methoden, um eine Bedeutung aus ihnen zu extrahieren. Das Entwurfsmuster Einbettungen ermöglicht einem Modell, ähnliche Wörter zu gruppieren, Beziehungen zwischen Wörtern zu identifizieren und syntaktische Textelemente zu verstehen. Während die Darstellung von Text durch Worteinbettungen am ehesten widerspiegelt, wie Menschen Sprache von Natur aus verstehen, gibt es zusätzliche Textdarstellungen, die die Fähigkeit des Modells maximieren können, eine bestimmte Vorhersageaufgabe durchzuführen. Dieser Abschnitt stellt den Ansatz Bag-of-Words für die Darstellung von Text vor und zeigt, wie sich tabellarische Features aus Text herausziehen lassen.
Um die Textdatendarstellung vorzuführen, beziehen wir uns auf einen Datensatz, der den Text von Millionen Fragen und Antworten aus Stack Overflow13 sowie Metadaten zu jedem Beitrag enthält. Zum Beispiel liefert uns die folgende Abfrage eine Teilmenge der Fragen, die mit keras, matplotlib oder pandas markiert sind, zusammen mit der Anzahl der Antworten, die jede Frage erhalten hat:
SELECT
title,
answer_count,
REPLACE(tags, "|", ",") as tags
FROM
`bigquery-public-data.stackoverflow.posts_questions`
WHERE
REGEXP_CONTAINS( tags, r"(?:keras|matplotlib|pandas)")
Die Abfrage liefert die folgende Ausgabe:
Bei der Darstellung von Text mit dem BOW-(Bag-of-Words-)Ansatz stellen wir uns jede Texteingabe in das Modell als Säckchen von Scrabble-Steinen vor, wobei aber jeder Stein ein ganzes Wort und nicht nur einen Buchstaben enthält. BOW bewahrt nicht die Reihenfolge der Wörter im Text, erkennt aber das Vorhandensein oder Nichtvorhandensein bestimmter Wörter in jedem Textteil, den wir an unser Modell senden. Dieser Ansatz ist eine Art Multi-Hot-Codierung, wobei jede Texteingabe in ein Array von Einsen und Nullen konvertiert wird. Jeder Index in diesem BOW-Array entspricht einem Wort aus unserem Vokabular.
Wie Bag-of-Words funktioniert
Bei der BOW-Codierung wählt man im ersten Schritt die Größe des Vokabulars, das die N am häufigsten in unserem Textkorpus vorkommenden Wörter enthält. Theoretisch könnte die Größe des Vokabulars gleich der Anzahl eindeutiger Wörter in unserem gesamten Datensatz sein. Allerdings führt das möglicherweise zu sehr großen Eingabearrays, die hauptsächlich aus Nullen bestehen, wenn viele Wörter nur in einer einzigen Frage vorkommen. Stattdessen wählen wir ein Vokabular, das klein genug ist, um wichtige, wiederkehrende Wörter aufzunehmen, die für unsere Vorhersageaufgabe von Bedeutung sind, aber groß genug, damit unser Vokabular nicht auf Wörter beschränkt wird, die in nahezu jeder Frage enthalten sind (wie »the«, »is«, »and« usw.).
Jede Eingabe in unser Modell ist dann ein Array von der Größe unseres Vokabulars. Diese BOW-Darstellung lässt also Wörter, die in unserem Vokabular nicht enthalten sind, völlig außer Acht. Es gibt weder eine magische Zahl noch einen Prozentsatz für die Wahl der Vokabulargröße – es ist hilfreich, zu experimentieren und zu sehen, welche Größe für ein Modell am besten geeignet ist.
Um die BOW-Codierung zu verstehen, sehen wir uns zuerst ein vereinfachtes Beispiel an. Wir nehmen für dieses Beispiel an, dass wir das Tag einer Stack-Overflow-Frage aus einer Liste von drei möglichen Tags vorhersagen wollen: pandas, keras und matplotlib. Der Einfachheit halber nehmen wir an, dass unser Vokabular nur aus den zehn unten aufgeführten Wörtern besteht:
dataframe
layer
series
graph
column
plot
color
axes
read_csv
activation
Diese Liste ist unser Wortindex, und jede Eingabe, die wir in unser Modell einspeisen, wird ein 10-elementiges Array sein, wobei jeder Index einem der oben aufgeführten Wörter entspricht. So bedeutet eine 1 im ersten Index eines Eingabearrays, dass eine bestimmte Frage das Wort dataframe enthält. Um die BOW-Codierung aus Sicht unseres Modells zu verstehen, stellen Sie sich vor, dass wir eine neue Sprache lernen und die obigen zehn Wörter die einzigen Wörter sind, die wir kennen. Jede »Vorhersage«, die wir treffen, basiert ausschließlich auf dem Vorhandensein oder Nichtvorhandensein dieser zehn Wörter und lässt alle Wörter außerhalb dieser Liste außer Acht.
Wie transformieren wir also die Frage »How to plot dataframe bar graph« in eine BOW-Darstellung? Zuerst notieren wir in diesem Satz die Wörter, die in unserem Vokabular erscheinen: plot, dataframe und graph. Die anderen Wörter in diesem Satz ignorieren wir bei der BOW-Methode. Mit dem obigen Wortindex wird der Satz dann zu:
[ 1 0 0 1 0 1 0 0 0 0 ]
Die Einsen in diesem Array korrespondieren mit den Indizes von dataframe, graph bzw. plot. Als Zusammenfassung zeigt Abbildung 2-21, wie wir unsere Eingabe von rohem Text in ein BOW-codiertes Array basierend auf unserem Vokabular transformiert haben.
Da Keras einige Hilfsmethoden mitbringt, um Text als Bag-of-Words zu codieren, brauchen wir den Code, der die häufigsten Wörter aus unserem Textkorpus identifiziert und rohen Text in Multi-Hot-Arrays codiert, nicht von Grund auf neu zu schreiben.
Abbildung 2-21: Rohe Texteingabe → Identifizieren der Wörter aus unserem Vokabular, die in diesem Text vorkommen → Transformieren in eine Multi-Hot-BOW-Codierung
Sicherlich werden Sie sich nun fragen, welcher der beiden Ansätze zur Darstellung von Text (Einbetten und BOW) für eine bestimmte Aufgabe verwendet werden sollte. Wie bei vielen Aspekten des maschinellen Lernens hängt dies ab vom Datensatz, dem Wesen der Vorhersageaufgabe und der Art des Modells, das verwendet werden soll.
Einbettungen bringen eine zusätzliche Schicht in unser Modell ein und liefern zusätzliche Informationen über die Wortbedeutung, die bei der BOW-Codierung nicht verfügbar sind. Allerdings müssen wir Einbettungen trainieren (sofern wir für unser Problem nicht auf eine vorab trainierte Einbettung zurückgreifen können). Zwar erreicht ein Deep-Learning-Modell möglicherweise eine höhere Genauigkeit, doch wir können auch versuchen, BOW-Codierung mithilfe von scikit-learn oder XGBoost in einem linearen Regressions- oder Entscheidungsbaummodell einzusetzen. Die BOW-Codierung kann in Verbindung mit einem einfacheren Modelltyp hilfreich sein für schnelles Prototyping oder um zu verifizieren, ob die gewählte Vorhersageaufgabe auf unserem Datensatz funktioniert. Im Unterschied zu Einbettungen berücksichtigt BOW weder die Reihenfolge noch die Bedeutung von Wörtern in einem Textdokument. Sollten diese Punkte für eine Vorhersageaufgabe wichtig sein, sind Einbettungen möglicherweise der beste Ansatz.
Es kann auch vorteilhaft sein, ein tiefes Modell zu erstellen, das Bag-of-Words-Texteinbettungsdarstellungen miteinander kombiniert, um mehr Muster in den Daten auszumachen. Hierfür können wir das Konzept der multimodalen Eingabe verwenden, jedoch mit der Ausnahme, dass wir statt der Verkettung von Text- und tabellarischen Features die Einbettung und BOW-Darstellungen verketten können (siehe den Code auf GitHub unter https://github.com/GoogleCloudPlatform/ml-design-patterns/blob/master/02_data_representation/mixed_representation.ipynb). Hier wäre die Form unserer Eingabeschicht die Vokabulargröße der BOW-Darstellung. Zu den Vorteilen einer Darstellung von Text auf mehrere Arten gehören:
Tabellarische Features aus Text extrahieren.Neben der Codierung von rohen Textdaten gibt es oft andere Eigenschaften von Text, die sich als tabellarische Features darstellen lassen. Angenommen, wir erstellten ein Modell, um vorherzusagen, ob eine Stack-Overflow-Frage eine Antwort erhalten wird oder nicht. Verschiedene Faktoren über den Text, die aber mit den genauen Wörtern selbst nichts zu tun haben, können für das Training eines Modells bei dieser Aufgabe relevant sein. Zum Beispiel beeinflusst vielleicht die Länge einer Frage oder das Vorhandensein eines Fragezeichens die Wahrscheinlichkeit einer Antwort. Wenn wir jedoch eine Einbettung erstellen, schneiden wir die Wörter normalerweise auf eine bestimmte Länge ab. Die tatsächliche Länge einer Frage geht in dieser Datendarstellung verloren. Auch Satzzeichen werden oft entfernt. Mit dem Entwurfsmuster Multimodale Eingabe können wir diese verlorenen Informationen wieder in das Modell zurückholen.
In der folgenden Abfrage extrahieren wir einige tabellarische Features aus dem Feld title des Stack-Overflow-Datensatzes, um vorherzusagen, ob eine Frage eine Antwort erhält oder nicht:
SELECT
LENGTH(title) AS title_len,
ARRAY_LENGTH(SPLIT(title, " ")) AS word_count,
ENDS_WITH(title, "?") AS ends_with_q_mark,
IF
(answer_count > 0,
1,
0) AS is_answered,
FROM
`bigquery-public-data.stackoverflow.posts_questions`
Diese Abfrage liefert folgendes Ergebnis:
Zusätzlich zu diesen Features, die direkt aus dem Titel einer Frage herausgezogen wurden, könnten wir auch Metadaten über die Frage als Features darstellen. So könnten wir Features hinzufügen, die die Anzahl der Tags, die die Frage hatte, und den Wochentag, an dem sie gepostet wurde, darstellen. Diese tabellarischen Features könnten wir dann mit unserem codierten Text kombinieren und beide Darstellungen mithilfe der Concatenate-Schicht von Keras in unser Modell einspeisen, um das BOW-codierte Textarray mit den tabellarischen Metadaten, die unseren Text beschreiben, zu kombinieren.
Ähnlich wie bei unserer Analyse von Einbettungen und BOW-Codierung für Text gibt es viele Möglichkeiten, Bilddaten darzustellen, wenn sie für ein ML-Modell vorbereitet werden. Wie roher Text lassen sich Bilder nicht direkt in ein Modell einspeisen, sondern müssen in ein numerisches Format, das das Modell verstehen kann, transformiert werden. Zunächst diskutieren wir einige gebräuchliche Ansätze, um Bilddaten darzustellen: als Pixelwerte, als Gruppen von Kacheln und als Gruppen von gleitenden Sequenzen. Das Entwurfsmuster Multimodale Eingabe bietet eine Möglichkeit, mehr als eine Darstellung eines Bilds einzuspeisen.
Bilder als Pixelwerte.Bilder sind aus technischer Sicht Arrays von Pixelwerten. Zum Beispiel enthält ein Schwarz-Weiß-Bild Pixelwerte, die von 0 bis 255 reichen. Wir könnten also ein 28 × 28-Pixel-Schwarz-Weiß-Bild in einem Modell als 28 × 28-Array mit Ganzzahlwerten von 0 bis 255 darstellen. In diesem Abschnitt beziehen wir uns auf den MNIST-Datensatz, einen beliebten ML-Datensatz mit Bildern handgeschriebener Ziffern. Mithilfe der Sequential-API können wir die aus Pixelwerten bestehenden MNIST-Bilder mit einer Flatten-Schicht darstellen, die das Bild in ein eindimensionales Array aus 784 (= 28 × 28) Elementen abflacht:
layers.Flatten(input_shape=(28, 28))
Bei Farbbildern ist die Umwandlung etwas komplexer. Jedes Pixel in einem RGB-Farbbild besteht aus drei Werten – je einem für Rot, Grün und Blau. Wären die Bilder im obigen Beispiel stattdessen Farbbilder, würden wir zu input_shape des Modells eine dritte Dimension hinzufügen:
layers.Flatten(input_shape=(28, 28, 3))
Die Darstellung von Bildern als Arrays von Pixelwerten funktioniert zwar gut für einfache Bilder wie die Graustufenbilder im MNIST-Datensatz, doch versagt das Verfahren schnell, wenn wir Bilder mit mehr Kanten und Formen einführen. Wenn man in ein Netz alle Pixel eines Bilds auf einmal einspeist, kann sich das Netz nur schwer auf kleinere Bereiche benachbarter Pixel konzentrieren, die wichtige Informationen enthalten.
Bilder als gekachelte Strukturen.Wir brauchen eine Möglichkeit, um komplexere, reale Bilder darzustellen, sodass unser Modell in der Lage ist, bedeutsame Details zu extrahieren und Muster zu verstehen. Wenn wir dem Netz nur kleine Teile eines Bilds auf einmal anbieten, ist es wahrscheinlicher, dass es Dinge wie räumliche Gradienten und Kanten in benachbarten Pixeln erkennen kann.
Eine gängige Modellarchitektur, um dies zu erreichen, ist ein Convolutional Neural Network (CNN).
CNN-Schichten
Abbildung 2-22 zeigt als Beispiel ein 4 × 4-Raster, in dem jedes Quadrat Pixelwerte unseres Bilds repräsentiert. Wir nehmen dann per Max-Pooling den größten Wert für jedes Feld und erzeugen daraus eine kleinere Matrix. Indem wir unser Bild in ein Raster von Kacheln unterteilen, kann das Modell wichtige Erkenntnisse aus jeder Region eines Bilds auf verschiedenen Granularitätsebenen extrahieren.
Abbildung 2-22: Max-Pooling auf einer einzelnen 4 × 4-Scheibe von Bilddaten
Das Beispiel in Abbildung 2-22 verwendet eine Kernelgröße von (2, 2). Mit Kernelgröße ist die Größe jedes Blocks in unserem Bild gemeint. Die Anzahl der Plätze, um die der Filter weiterwandert, bevor der nächste Block gebildet wird, ist die sogenannte Schrittweite (engl. Stride), hier 2. Da unsere Schrittweite gleich der Kernelgröße ist, überlappen sich die erzeugten Blöcke nicht.
Zwar bewahrt diese Kachelmethode mehr Details als die Darstellung von Bildern als Arrays von Pixelwerten, doch gehen nach jedem Pooling-Schritt recht viele Informationen verloren. In Abbildung 2-22 würde der nächste Pooling-Schritt einen Skalarwert von 8 erzeugen, was unsere Matrix von 4 × 4 in nur zwei Schritten auf einen einzigen Wert reduziert. Man kann sich vorstellen, wie dies in einem realen Bild ein Modell dazu verleiten könnte, sich auf Bereiche mit dominanten Pixelwerten zu konzentrieren und dabei wichtige Details zu verlieren, die diese Bereiche umgeben.
Wie können wir auf dieser Idee – Bilder in kleinere Blöcke aufzuteilen – aufbauen, dabei aber wichtige Details in den Bildern erhalten? Hierzu erzeugen wir sich überlappende Blöcke. Wenn wir im Beispiel von Abbildung 2-22 eine Schrittweite von 1 gewählt hätten, wäre die Ausgabe stattdessen eine 3 × 3-Matrix (siehe Abbildung 2-23).
Abbildung 2-23: Überlappende Fenster für Max-Pooling auf einem 4 × 4-Pixel-Raster
Dieses Raster könnten wir dann in ein 2 × 2-Raster umwandeln (siehe Abbildung 2-24).
Abbildung 2-24: Das 3 × 3-Raster mit gleitenden Fenstern und Max-Pooling in ein 2 × 2-Raster überführen
Am Ende haben wir einen Skalarwert von 127. Auch wenn der Endwert der gleiche ist, können Sie verfolgen, wie die Zwischenschritte mehr Details aus der ursprünglichen Matrix bewahrt haben.
Mit den Faltungsschichten von Keras lassen sich Modelle erstellen, die Bilder in kleinere, gleitende Blöcke aufteilen. Nehmen wir an, wir erstellten ein Modell, um 28 × 28-Farbbilder als »Hund« oder »Katze« zu klassifizieren. Da es sich um farbige Bilder handelt und jedes Pixel aus drei Farbkanälen besteht, wird jedes Bild durch ein 28 × 28 × 3-dimensionales Array dargestellt. Die Eingaben für dieses Modell definieren wir mit einer Faltungsschicht und der Sequential-API:
Conv2D(filters=16, kernel_size=3, activation='relu', input_shape=(28,28,3))
In diesem Beispiel teilen wir unsere Eingabebilder in 3 × 3-Blöcke auf, bevor wir sie über eine Max-Pooling-Schicht weiterleiten. Mit einer Modellarchitektur, die Bilder in Blöcke von gleitenden Fenstern aufteilt, ist unser Modell in der Lage, feinere Details in einem Bild zu erkennen, beispielsweise Kanten und Formen.
Verschiedene Bilddarstellungen kombinieren.Wie bei Bag-of-Words und Texteinbettungen kann es nützlich sein, die gleichen Bilddaten auf mehrere Arten darzustellen. Auch hier können wir dies mit der funktionalen API von Keras erreichen.
Unsere Pixelwerte würden wir wie folgt mit der Darstellung gleitender Fenster über die Concatenate-Schicht von Keras kombinieren:
# Bildeingabeschicht definieren (gleiche Gestalt für
# Pixel- und Kacheldarstellung).
image_input = Input(shape=(28,28,3))
# Pixeldarstellung definieren.
pixel_layer = Flatten()(image_input)
# Kacheldarstellung definieren.
tiled_layer = Conv2D(filters=16, kernel_size=3,
activation='relu')(image_input)
tiled_layer = MaxPooling2D()(tiled_layer)
tiled_layer = tf.keras.layers.Flatten()(tiled_layer)
# Zu einer einzigen Schicht verketten.
merged_image_layers = keras.layers.concatenate([pixel_layer, tiled_layer])
Um ein Modell zu definieren, das eine multimodale Eingabedarstellung akzeptiert, können wir dann unsere verkettete Schicht in unsere Ausgabeschicht einspeisen:
merged_dense = Dense(16, activation='relu')(merged_image_layers)
merged_output = Dense(1)(merged_dense)
model = Model(inputs=image_input, outputs=merged_output)
Die Entscheidung darüber, welche Bilddarstellung zu verwenden ist oder ob multimodale Darstellungen verwendet werden sollen, hängt weitgehend von der Art der zu verarbeitenden Bilddaten ab. Allgemein gilt: Je detaillierter die Bilder sind, desto wahrscheinlicher ist es, dass wir sie als Kacheln oder gleitende Fenster von Kacheln darstellen werden. Für den MNIST-Datensatz kann es genügen, Bilder allein als Pixelwerte darzustellen. Dagegen lässt sich die Genauigkeit bei komplexen medizinischen Bildern steigern, wenn man mehrere Darstellungen kombiniert. Weshalb sollte man mehrere Bilddarstellungen kombinieren? Stellt man Bilder als Pixelwerte dar, kann das Modell markante Punkte in einem Bild identifizieren, beispielsweise kontrastreiche Objekte. Dagegen helfen Kacheldarstellungen dem Modell, detailreichere, kontrastärmere Kanten und Formen zu erkennen.
Bilder mit Metadaten verwenden.Weiter oben haben wir bereits gezeigt, dass verschiedene Arten von Metadaten mit Text verbunden sein können und wie sich diese Metadaten als tabellarische Features für unser Modell extrahieren und darstellen lassen. Das gleiche Konzept können wir auch auf Bilder anwenden. Kehren wir dazu zu dem in Abbildung 2-19 gezeigten Beispiel eines Modells zurück, das anhand des Bildmaterials von einer Kreuzung vorhersagt, ob es einen Verkehrsverstoß enthält oder nicht. Unser Modell kann viele Muster aus den Verkehrsbildern an sich extrahieren, doch es können noch andere Daten verfügbar sein, die die Genauigkeit unseres Modells verbessern könnten. Zum Beispiel könnte ein bestimmtes Verhalten (etwa Rechtsabbiegen bei Rot) während der Hauptverkehrszeit nicht erlaubt, zu anderen Tageszeiten aber in Ordnung sein. Oder vielleicht verstoßen Fahrer bei schlechtem Wetter häufiger gegen die Straßenverkehrsordnung. Wenn wir Bilddaten von mehreren Kreuzungen sammeln, kann es für unser Modell auch nützlich sein, den Ort der Bildaufnahme zu kennen.
Wir haben nun drei zusätzliche tabellarische Features herausgearbeitet, die unser Bildmodell verbessern könnten:
Als Nächstes stellen wir Überlegungen zu möglichen Darstellungen für jedes dieser Features an. Die Zeit ließe sich als Ganzzahl für die Stunde des Tages darstellen. Das könnte uns helfen, Muster zu identifizieren, die für Zeiten mit hohem Verkehrsaufkommen typisch sind, beispielsweise die Hauptverkehrszeiten. Im Kontext dieses Modells könnte es nützlicher sein, zu wissen, ob es dunkel war, als das Bild aufgenommen wurde. In diesem Fall könnten wir Zeit zu einem booleschen Feature machen.
Das Wetter kann ebenfalls auf verschiedene Arten dargestellt werden, sowohl durch numerische als auch durch kategoriale Werte. Die Temperatur käme ebenfalls als Feature infrage, doch könnte in diesem Fall die Sicht sinnvoller sein. Für das Wetter bietet sich auch eine kategoriale Variable an, die das Vorhandensein von Schnee oder Regen anzeigt.
Wenn wir Daten von vielen Standorten zusammentragen, werden wir daneben den Standort als Feature codieren. Am sinnvollsten wäre ein kategoriales Feature, oder es könnten auch mehrere Features (Stadt, Bundesland, Staat usw.) sein, je nachdem, von wie vielen Orten wir das Bildmaterial beziehen.
Für dieses Beispiel nehmen wir an, dass wir die folgenden tabellarischen Features verwenden wollen:
Eine Teilmenge dieses Datensatzes könnte für die drei Beispiele wie folgt aussehen:
data = {
'time': [9,10,2],
'visibility': [0.2, 0.5, 0.1],
'inclement_weather': [[0,0,1], [0,0,1], [1,0,0]],
'location': [[0,1,0,0,0], [0,0,0,1,0], [1,0,0,0,0]]
}
Diese tabellarischen Features könnten wir dann in einem einzigen Array für jedes Beispiel kombinieren, sodass die Eingabeform unseres Modells 10 wäre. Das Eingabearray für das erste Beispiel sähe dann so aus:
[9, 0.2, 0, 0, 1, 0, 1, 0, 0, 0]
Diese Eingabe könnten wir in eine vollständige verbundene Dense-Schicht einspeisen, und die Ausgabe unseres Modells wäre ein einzelner Wert zwischen 0 und 1, der anzeigt, ob die Instanz einen Verkehrsverstoß enthält oder nicht. Um dies mit unseren Bilddaten zu kombinieren, verwenden wir einen ähnlichen Ansatz, den wir bei Textmodellen erörtert haben. Zuerst definieren wir eine Faltungsschicht, um unsere Bilddaten zu verarbeiten, dann eine Dense-Schicht, um unsere tabellarischen Daten zu verarbeiten, und schließlich verketten wir beide zu einer einzigen Ausgabe.
Abbildung 2-25 skizziert dieses Verfahren.
Abbildung 2-25: Schichten verketten, um Bilddaten und tabellarische Metadaten-Features zu verarbeiten
Deep-Learning-Modelle sind ihrem Wesen nach schwer zu erklären. Wenn wir ein Modell erstellen, das eine Genauigkeit von 99 % erreicht, wissen wir immer noch nicht genau, wie unser Modell Vorhersagen trifft, und demnach auch nicht, ob es diese Vorhersagen auf korrekte Art und Weise erzeugt. Nehmen wir zum Beispiel an, wir trainierten ein Modell mit Bildern von Petrischalen, die aus einem Labor stammen, und erreichen damit eine hohe Genauigkeit. Diese Bilder enthalten auch Anmerkungen von den Wissenschaftler:innen, die die Aufnahmen angefertigt haben. Uns bleibt aber verborgen, dass das Modell fälschlicherweise die Anmerkungen für seine Vorhersagen heranzieht und nicht den Inhalt der Petrischalen.
Es gibt mehrere Techniken, um Bildmodelle zu erklären. Sie heben die Pixel hervor, die eine Vorhersage des Modells signalisiert haben. Wenn wir jedoch mehrere Datendarstellungen in einem einzigen Modell zusammenbringen, werden diese Features voneinander abhängig. Im Ergebnis kann es schwierig sein, zu erklären, wie die Vorhersagen des Modells entstehen. Mit der Erklärbarkeit beschäftigt sich Kapitel 7.
In diesem Kapitel haben Sie verschiedene Ansätze kennengelernt, um Daten für ein Modell darzustellen. Zunächst wurde gezeigt, wie man numerische Eingaben verarbeitet und wie das Skalieren dieser Eingaben dazu beiträgt, die Trainingszeit zu verkürzen und die Genauigkeit zu verbessern. Dann haben wir erläutert, wie Feature Engineering auf kategorialen Eingaben funktioniert, insbesondere mit 1-aus-n-Codierung und der Verwendung von Arrays mit kategorialen Werten.
Im übrigen Teil des Kapitels haben wir vier Entwurfsmuster für die Datendarstellung diskutiert. Das erste Entwurfsmuster Hashed Feature codiert kategoriale Eingaben als eindeutige Strings. Anhand des Flughafendatensatzes in BigQuery haben wir verschiedene Ansätze für das Hashing untersucht. Das zweite Muster in diesem Kapitel, Einbettungen, ist eine Technik für die Darstellung von Daten mit hoher Kardinalität, wie zum Beispiel Eingaben mit vielen möglichen Kategorien oder Textdaten. Einbettungen repräsentieren Daten im mehrdimensionalen Raum, wobei die Dimension abhängig ist von den Daten und der Vorhersageaufgabe. Als Nächstes haben wir uns Feature Crosses angesehen. Dieses Konzept verknüpft zwei Features, um Beziehungen zu extrahieren, die sich nur schwer erfassen lassen, wenn man die Features allein codiert. Schließlich ging es um Darstellungen von multimodalen Eingaben, indem wir das Problem betrachtet haben, wie Eingaben verschiedener Typen im selben Modell kombiniert werden sollen und wie sich ein einzelnes Feature auf mehrere Möglichkeiten darstellen lässt.
Im Mittelpunkt dieses Kapitels stand die Aufbereitung der Eingabedaten für unsere Modelle. Das nächste Kapitel konzentriert sich auf die Modellausgabe. Dazu beleuchten wir verschiedene Ansätze für die Darstellung unserer Vorhersageaufgabe.