Kapitel 4. Dateneingabe, Vorverarbeitung und deskriptive Statistik

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Wahrscheinlich kennst du die Redewendung "Garbage in, garbage out". Er drückt gut aus, dass fehlerhafte, falsche oder unsinnige Dateneingaben immer zu fehlerhaften Ergebnissen führen. Im Zusammenhang mit maschinellem Lernen unterstreicht er auch die Tatsache, dass die Aufmerksamkeit, die wir dem Einlesen, der Vorverarbeitung und dem statistischen Verständnis unserer Daten (dem Erforschen und Aufbereiten) widmen, einen Einfluss auf den Erfolg des gesamten Prozesses hat. Eine fehlerhafte Dateneingabe hat direkte Auswirkungen auf die Qualität der Daten, ebenso wie eine fehlerhafte Vorverarbeitung. Um ein Gefühl für die vorliegenden Daten und ihre Korrektheit zu bekommen, setzen wir deskriptive Statistiken ein; das ist ein wichtiger Teil des Prozesses, denn so können wir überprüfen, ob die verwendeten Daten von guter Qualität sind. Data Scientists, Machine Learning Engineers und Data Engineers verbringen oft viel Zeit damit, diese wichtigen Schritte zu erforschen und zu verbessern, und ich werde dich in diesem Kapitel durch sie führen.

Bevor wir beginnen, wollen wir den Ablauf verstehen. Gehen wir davon aus, dass sich unsere Daten zu Beginn auf der Festplatte, in einer Datenbank oder in einem Cloud Data Lake befinden. Hier sind die Schritte, die wir befolgen, um ein Verständnis für unsere Daten zu bekommen:

  1. Ingestion. Wir beginnen damit, die Daten in ihrer aktuellen Form in eine DataFrame-Instanz zu verschieben. Dies wird auch als Deserialisierung der Daten bezeichnet. Genauer gesagt, legen wir in Spark in diesem Schritt einen Plan fest, wie wir die Daten deserialisieren und in einen DataFrame umwandeln. Dieser Schritt liefert uns oft ein grundlegendes Schema, das aus den vorhandenen Daten abgeleitet wird.

  2. Vorverarbeitung. Unter werden die Daten so aufbereitet, dass sie in unser gewünschtes Schema passen. Wenn wir die Daten als Strings laden, sie aber als Floats benötigen, müssen wir den Datentyp umwandeln und die Werte so anpassen, dass sie in das gewünschte Schema passen. Das kann ein komplexer und fehleranfälliger Prozess sein, vor allem, wenn wir Daten aus mehreren Quellen mit einer Größe von mehreren Terabyte synchronisieren, und erfordert eine vorausschauende Planung.

  3. Qualifizieren. Dieser Schritt besteht darin, deskriptive Statistiken zu verwenden, um die Daten zu verstehen und mit ihnen zu arbeiten.

Die Schritte 2 und 3 können sich überschneiden, da wir je nach den in Schritt 3 berechneten Statistiken weitere Vorverarbeitungen an den Daten vornehmen können.

Nachdem du nun eine allgemeine Vorstellung von den einzelnen Schritten hast, wollen wir sie ein bisschen genauer betrachten.

Dateningestion mit Spark

Apache Spark ist generisch genug, um seine API zu erweitern und spezielle Konnektoren für jede Art von Speicher zu entwickeln, um Daten mit Hilfe des Konnektor-Mechanismus aufzunehmen (und zu persistieren/senken/speichern). Von Haus aus unterstützt verschiedene Dateiformate wie Parquet, CSV, Binärdateien, JSON, ORC, Bilddateien und mehr.

Spark ermöglicht es uns auch, mit Batch- und Streaming-Daten zu arbeiten. Die Batch-API von Spark dient der Verarbeitung von Offline-Daten, die in einem Dateispeicher oder einer Datenbank gespeichert sind. Bei Batch-Daten ist die Größe des Datensatzes fest und ändert sich nicht, und wir erhalten keine neuen Daten zur Verarbeitung. Für die Verarbeitung von Streaming-Daten hat Spark eine ältere API namens DStream oder einfach Streaming und eine neuere, verbesserte API namens Structured Streaming. Structured Streaming ist eine API für die verteilte kontinuierliche Verarbeitung von strukturierten Datenströmen. Damit kannst du mehrere Datensätze gleichzeitig verarbeiten, indem du den Eingabestrom in Mikrobatches unterteilst. Wenn deine Daten nicht strukturiert sind oder das Format variiert, musst du stattdessen die alte DStream-API verwenden oder eine Lösung entwickeln, um Schemaänderungen ohne Ausfälle zu automatisieren.

In diesem Kapitel konzentrieren wir uns auf die Stapelverarbeitung mit kalten Offline-Daten. Die Erstellung von Modellen für maschinelles Lernen mit kalten Daten ist der gängigste Ansatz für verschiedene Anwendungsfälle wie Videoproduktion, Finanzmodellierung, Arzneimittelforschung, Genomforschung, Empfehlungsmaschinen und mehr. Die Arbeit mit Streaming-Daten wird in Kapitel 10 behandelt, in dem es um die Bereitstellung von Modellen mit beiden Arten von Datenquellen geht.

Die Angabe von Batch Reading mit einem bestimmten Datenformat erfolgt zum Beispiel mit der Funktion format:

df = spark.read.format("image")

Die Klasse, die dies ermöglicht, ist die DataFrameReader class. Du kannst sie über ihre options API konfigurieren, um festzulegen, wie die Daten geladen und das Schema abgeleitet werden soll, wenn das Dateiformat es nicht bereits bereitstellt, oder um die Metadaten zu extrahieren, wenn es dies tut.

Verschiedene Dateiformate können ein Schema haben oder nicht, je nachdem, ob die Daten strukturiert, halbstrukturiert oder unstrukturiert sind, und natürlich abhängig von der Implementierung des Formats selbst. Das JSON-Format gilt zum Beispiel als halbstrukturiert und enthält von Haus aus keine Metadaten über die Zeilen, Spalten und Merkmale. Bei JSON wird das Schema also abgeleitet.

Auf hingegen haben strukturierte Datenformate wie Avro und Parquet einen Metadatenbereich, der das Datenschema beschreibt. So kann das Schema extrahiert werden.

Arbeiten mit Bildern

Eine Bilddatei kann Daten in einem unkomprimierten, komprimierten oder Vektorformat speichern. JPEG ist zum Beispiel ein komprimiertes Format und TIFF ein unkomprimiertes Format.

Wir speichern die digitalen Daten von in diesen Formaten, um sie leicht für einen Computerbildschirm oder einen Drucker umwandeln zu können. Das ist das Ergebnis der Rasterung. Die Hauptaufgabe der Rasterung ist die Umwandlung der Bilddaten in ein Raster aus Pixeln, wobei jedes Pixel eine Anzahl von Bits hat, die seine Farbe und Transparenz bestimmen. Bei der Rasterung einer Bilddatei für ein bestimmtes Gerät wird die Anzahl der Bits pro Pixel (die Farbtiefe) berücksichtigt, für die das Gerät ausgelegt ist. Wenn wir mit Bildern arbeiten, müssen wir auf das Dateiformat achten und wissen, ob es komprimiert oder unkomprimiert ist (mehr dazu in "Bildkomprimierung und Parquet").

In diesem Kapitel verwenden wir einen Kaggle-Bilddatensatz namens Caltech 256, der Bilddateien im JPEG-Kompressionsformat enthält. Unser erster Schritt besteht darin, sie in eine Spark DataFrame-Instanz zu laden. Dabei können wir zwischen zwei Formaten wählen: Bild oder Binär.

Hinweis

Wenn dein Programm die Operation DataFrame load verarbeitet, lädt die Spark-Engine die Bilder nicht sofort in den Speicher. Wie in Kapitel 2 beschrieben, verwendet Spark die "Lazy Evaluation", d. h., anstatt die Bilder tatsächlich zu laden, wird ein Plan erstellt, wie sie geladen werden sollen, falls und wenn dies notwendig wird. Der Plan enthält Informationen über die eigentlichen Daten, wie Tabellenfelder/-spalten, Format, Dateiadressen usw.

Bildformat

Spark MLlib hat eine eigene Bilddatenquelle, mit der wir Bilder aus einem Verzeichnis in einen Datenrahmen laden können, der OpenCV-Typen zum Lesen und Verarbeiten der Bilddaten verwendet. In diesem Abschnitt erfährst du mehr darüber.

OpenCV ist ein C/C++-basiertes Tool für Computer Vision Workloads. Mit der MLlib-Funktionalität kannst du komprimierte Bilder (.jpeg, .png, etc.) in das OpenCV-Datenformat konvertieren. Beim Laden in einen Spark Datenrahmen wird jedes Bild in einer eigenen Zeile gespeichert.

Im Folgenden sind die unterstützten unkomprimierten OpenCV-Typen aufgeführt:

  • CV_8U

  • CV_8UC1

  • CV_8UC3

  • CV_8UC4

Dabei steht 8 für die Bittiefe, U für vorzeichenlos, und Cx die Anzahl der Kanäle angibt.

Warnung

Seit Spark 3.1.1 beträgt die Grenze für die Bildgröße 1 GB. Da Spark Open Source ist, kannst du die Aktualisierungen der Bildgrößenunterstützung im Quellcode verfolgen: Die Zeile assert(imageSize < 1e9, "image is too large") in der Definition der Funktion decode des Objekts ImageSchema.scala verrät uns, dass die Grenze bei 1 GB liegt.

Um relativ kleine Dateien zu untersuchen, ist das von Spark bereitgestellte Bildformat eine fantastische Möglichkeit, um mit Bildern zu arbeiten und die gerenderte Ausgabe zu sehen. Für den allgemeinen Arbeitsablauf, bei dem es nicht notwendig ist, die Bilder selbst während des Prozesses zu betrachten, empfehle ich dir jedoch das Binärformat, da es effizienter ist und du größere Bilddateien schneller verarbeiten kannst. Außerdem ist das Binärformat bei größeren Bilddateien (≥1 GB) die einzige Möglichkeit, sie zu verarbeiten. Während Spark standardmäßig Daten partitioniert, werden Bilder nicht partitioniert. Bei räumlichen Objekten wie Bildern sprechen wir stattdessen von Kacheln oder der Zerlegung des Bildes in eine Reihe von Segmenten (Kacheln) mit der gewünschten Form. Das Kacheln von Bildern kann Teil der Vorverarbeitung der Daten selbst sein. Um die Sprache anzugleichen, werde ich in Zukunft von Partitionen sprechen, auch wenn es um Bilder oder räumliche Objekte geht.

Binäres Format

Spark unterstützt seit der Version 3.0 eine Datenquelle für Binärdateien.1 Dadurch können Binärdateien gelesen und in einen einzigen Datensatz in einer Tabelle umgewandelt werden. Der Datensatz enthält den Rohinhalt als BinaryType und einige Metadaten-Spalten. Die Verwendung des Binärformats zum Lesen der Daten erzeugt einen Datenrahmen mit den folgenden Spalten:

  • path: StringType

  • modificationTime: TimestampType

  • length: LongType

  • content: BinaryType

Wir verwenden dieses Format, um die Caltech 256 Daten zu laden, wie im folgenden Codeschnipsel gezeigt :

from pyspark.sql.types import BinaryType
spark.sql("set spark.sql.files.ignoreCorruptFiles=true")

df = spark.read.format("binaryFile")
               .option("pathGlobFilter", "*.jpg")
               .option("recursiveFileLookup", "true")
               .load(file_path)

Wie in Kapitel 2 beschrieben, befinden sich die Daten in unserem Datensatz in einer verschachtelten Ordnerhierarchie. recursiveFileLookup ermöglicht es uns, die verschachtelten Ordner zu lesen, während die Option pathGlob​Fil⁠ter es uns ermöglicht, die Dateien zu filtern und nur diejenigen mit der Erweiterung .jpg zu lesen.

Auch hier ist zu beachten, dass dieser Code die Daten nicht tatsächlich in die Executors zur Berechnung lädt. Wie bereits erwähnt , wird die Ausführung aufgrund des Lazy-Evaluation-Mechanismus von Spark erst dann gestartet, wenn eine Aktion ausgelöst wird - der Treiber sammelt die verschiedenen Transformationsanfragen und Abfragen in einem DAG, optimiert sie und wird erst dann aktiv, wenn eine spezifische Anfrage für eine Aktion vorliegt. So können wir Rechenkosten sparen, Abfragen optimieren und die allgemeine Handhabbarkeit unseres Codes verbessern.

Arbeiten mit tabellarischen Daten

Da Spark sofort einsatzbereite Konnektoren für verschiedene Dateiformate bietet, ist die Arbeit mit Tabellendaten ziemlich einfach. Im GitHub-Repository des Buches findest du zum Beispiel unter Datensätze den DatensatzCO2-Emissionen nach Fahrzeugen, dessen Daten im CSV-Format vorliegen. Mit der Connector-Funktion .for⁠mat("csv"), oder direkt mit .csv(file_path)können wir diesen Datensatz ganz einfach in einen Datenrahmen laden.

Achte aber auf das Schema - selbst mit der Option Infer​Schema neigt Spark dazu, Spalten in CSV-Dateien als Strings zu definieren, auch wenn sie ganze Zahlen, boolesche Werte usw. enthalten. Daher besteht unsere Hauptaufgabe zu Beginn darin, die Datentypen der Spalten zu überprüfen und zu korrigieren. Wenn eine Spalte in der CSV-Eingabedatei zum Beispiel JSON-Strings enthält, musst du einen eigenen Code schreiben, um dieses JSON zu verarbeiten.

Beachte, dass jeder Spark-Datenquellen-Connector einzigartige Eigenschaften hat, die dir eine Reihe von Optionen für den Umgang mit beschädigten Daten bieten. So kannst du zum Beispiel das Verhalten beim Beschneiden von Spalten steuern, indem du spark.sql.csv​.parser​.column​Prun⁠ing​.enabled auf False setzt, wenn du Spalten mit beschädigtem Format oder beschädigten Daten nicht beschneiden möchtest, oder True für das gegenteilige Verhalten verwendest. Du kannst auch den Parameter mode nutzen, um das Pruning spezifischer zu gestalten, z. B. mit PERMISSIVE, um das Feld auf null zu setzen, DROPMALFORMED, um den ganzen Datensatz zu ignorieren, oder FAILFAST, um eine Ausnahme zu machen, wenn ein beschädigter Datensatz verarbeitet wird. Das folgende Codeschnipsel ist ein Beispiel dafür:

df = spark.read.option("mode","FAILFAST")
               .option("delimiter","\t")
               .csv(file_path)

Nachdem du die Daten geladen und in einen Datenrahmen deserialisiert hast, ist es an der Zeit, sie vorzuverarbeiten. Bevor du weitermachst, empfehle ich dir, deine Daten in einem Format zu speichern, das ein typisiertes Schema mit klar definierten Spaltennamen und -typen hat, wie z. B. Parquet oder Avro.

Daten vorverarbeiten

Vorverarbeitung ist die Kunst, die Daten in den gewünschten Zustand zu bringen, sei es ein stark typisiertes Schema oder ein bestimmter Datentyp, den der Algorithmus benötigt.

Vorverarbeitung versus Verarbeitung

Die Unterscheidung zwischen Vorverarbeitung und Verarbeitung kann schwierig sein, wenn du gerade erst mit maschinellem Lernen anfängst. Die Vorverarbeitung bezieht sich auf alle Arbeiten, die wir vor der eigentlichen Validierung des Datensatzes durchführen. Diese Arbeit wird erledigt, bevor wir versuchen, mit Hilfe von deskriptiven Statistiken ein Gefühl für die Daten zu bekommen oder ein Feature Engineering durchzuführen, was beides unter die Verarbeitung fällt. Diese Verfahren sind miteinander verzahnt (siehe Abbildung 4-1), und wir werden sie wahrscheinlich immer wieder wiederholen, bis wir die Daten in den gewünschten Zustand gebracht haben. Spark stellt uns alle Werkzeuge zur Verfügung, die wir für diese Aufgaben benötigen, entweder über die MLlib-Bibliothek oder die SQL-APIs.

Abbildung 4-1. Verzahnte Abläufe beim maschinellen Lernen

Warum die Daten vorverarbeiten?

Die Vorverarbeitung der Daten, d. h. die Anpassung an das gewünschte Schema, ist ein entscheidender Schritt, der abgeschlossen sein muss, bevor wir überhaupt mit der Untersuchung der Daten beginnen können, geschweige denn mit der Entwicklung neuer Funktionen. Das ist deshalb so wichtig, weil Algorithmen für maschinelles Lernen oft spezielle Anforderungen an die Eingabe haben, wie z. B. bestimmte Datenstrukturen und/oder Datentypen. In einigen akademischen Forschungsarbeiten wird dieser Prozess als " Data Marshaling" bezeichnet.

Um dir eine Vorstellung davon zu geben, welche Art von Vorverarbeitung du an deinen Daten vornehmen musst, bevor du sie an einen MLlib-Algorithmus weitergibst, werfen wir einen kurzen Blick auf die allgemeinen Anforderungen der verschiedenen Algorithmen:

Klassifizierungs- und/oder Regressionsalgorithmen
Für die Klassifizierung und die Regression möchtest du deine Daten in eine Spalte des Typs vector (dense oder sparse) mit double oder float Werten umwandeln. Diese Spalte wird oft features genannt, aber du kannst den Namen der Eingabespalte später flexibel festlegen.
Empfehlungsalgorithmen
Für die Empfehlung brauchst du eine userCol Spalte mit integer Werten, die die Benutzer-IDs darstellen, eine itemCol Spalte mit integer Werten, die die Artikel-IDs darstellen, und eine ratingCol Spalte mit double oder float Werten, die die Bewertungen der Artikel durch die Benutzer darstellen.
Unüberwachtes Lernen
Wenn du mit Daten arbeitest, die einen unüberwachten Lernansatz erfordern, brauchst du oft eine Spalte vom Typ vector, um deine Merkmale zu repräsentieren.

Datenstrukturen

Abhängig von ihrer Struktur müssen Daten oft verarbeitet werden, bevor sie vollständig genutzt werden können. Daten können in drei Kategorien eingeteilt werden:

Strukturiert
Strukturierte Daten haben einen hohen Organisationsgrad. Sie werden in einem vordefinierten schematischen Format gespeichert, z. B. in einer Datei mit kommagetrennten Werten (.csv) oder in einer Tabelle in einer Datenbank. Manchmal werden sie auch als tabellarische Daten bezeichnet.
Halbstrukturiert
Halbstrukturierte Daten sind in gewissem Maße organisiert, aber die Struktur ist weniger starr und das Schema ist nicht festgelegt. Es kann Tags geben, die Elemente trennen und Hierarchien festlegen, wie in einer JSON-Datei. Solche Daten müssen möglicherweise vorverarbeitet werden, bevor wir sie für maschinelle Lernalgorithmen verwenden können.
Unstrukturiert
Unstrukturierte Daten haben keine definierte Organisation und kein bestimmtes Format - man denke an .jpeg-Bilder, .mp4-Videodateien, Sounddateien usw. Diese Daten müssen oft stark vorverarbeitet werden, bevor wir sie für die Erstellung von Modellen verwenden können. Die meisten Daten, die wir heute erzeugen, sind unstrukturierte Daten.

MLlib Datentypen

MLlib hat ihre eigenen Datentypen, die sie als Eingabe für maschinelle Lernalgorithmen benötigt. Um mit der Spark MLlib zu arbeiten, musst du deine Spalten in einen dieser Typen umwandeln - deshalb sind die Vorverarbeitung und die Umwandlung von Daten Prozesse, die du ineinandergreifend durchführen wirst. Unter der Haube verwendet Spark die privaten Objekte VectorUDT und MatrixUDF, die mehrere Typen von lokalen Vektoren (dichte, spärliche, beschriftete Punkte) und Matrizen (sowohl lokal als auch verteilt) abstrahieren. Diese Objekte ermöglichen eine einfache Interaktion mit den Funktionen von spark.sql.Dataset. Auf einer hohen Ebene sind die beiden Objekttypen wie folgt:

Vektor
Ein Vektor Objekt repräsentiert einen numerischen Vektor. Du kannst es dir wie ein Array vorstellen, genau wie in Python, nur dass hier der Index vom Typ integer und der Wert vom Typ double ist.
Matrix
Ein Matrix Objekt stellt eine numerische Matrix dar. Sie kann lokal auf einem Rechner oder über mehrere Rechner verteilt sein. Bei der lokalen Version sind die Indizes vom Typ integer und die Werte vom Typ double. Bei der verteilten Version sind die Indizes vom Typ long und die Werte vom Typ double. Alle Matrixtypen werden durch Vektoren dargestellt.
Hinweis

Um unsere Arbeit mit Python-Tools zu vereinfachen, erkennt MLlib NumPy-Arrays und Python-Listen als dichte Vektoren und SciPy's csc_matrix mit einer einzigen Spalte als spärliche Vektoren. So können wir leichter von einem Tool zum anderen wechseln. Behalte dies im Hinterkopf, wenn du mit mehreren Tools arbeitest.

Es ist gut zu verstehen, wie sparse und dense Vektoren in der MLlib dargestellt werden, denn du wirst sie in der Dokumentation und in deinen Experimenten finden. Spark entscheidet hinter den Kulissen, welche Art von Vektor für eine bestimmte Aufgabe erstellt wird.

Hinweis

Die dritte Art von Vektor ist ein beschrifteter Punkt; er repräsentiert Merkmale und Beschriftungen eines Datenpunkts und kann entweder dicht oder spärlich sein. In MLlib werden beschriftete Punkte in überwachten Lernalgorithmen verwendet.

Beginnen wir mit einem dichten Vektor. Hier ist ein Beispiel:

Row(features=DenseVector([1.2, 543.5, 0.0, 0.0, 0.0, 1.0, 0.0]))]

Eine DenseVector besteht aus einem Array von Werten fester Größe. Sie gilt in Bezug auf den Speicherverbrauch als weniger effizient als SparseVector, weil sie explizit Speicherplatz für die angegebene Vektorgröße schafft, einschließlich leerer/vorgegebener Werte. In unserem Beispiel gibt es sieben Werte in der DenseVector, aber vier davon sind leer (0.0 ist der Standardwert, also gelten diese Werte als leer).

Eine SparseVector ist eine Optimierung einer DenseVector, die leere/standardmäßige Werte hat. Schauen wir uns an, wie unser Beispiel DenseVector übersetzt aussehen würde in eine SparseVector:

Row(features=SparseVector(7,{0:1.2, 1: 543.5, 5:1.0}))

Die erste Zahl steht für die Größe des Vektors (7), und die Karte ({...}) steht für die Indizes und ihre Werte. In diesem Vektor gibt es nur drei Werte, die gespeichert werden müssen: Der Wert bei Index 0 ist 1.2, der Wert bei Index 2 ist 543.5 und der Wert bei Index 5 ist 1. Die Werte an den anderen Indizes müssen nicht gespeichert werden, da sie alle Standardwerte sind.

Werfen wir einen Blick auf ein größeres Vektorbeispiel:

[Row(features=SparseVector(50, {48: 9.9, 49: 6.7}))]

In diesem Fall ist die Vektorgröße 50 und wir haben nur zwei Werte zu speichern: 9.9 für Index 48 und 6.7 für Index 49.

Eine SparseVector kann auch so aussehen:

(50,[48,49],[9.9,6.7])

wobei die erste Zahl (hier 50) für die Größe steht, das erste Array ([48,49]) für die Indizes, deren Werte in SparseVector gespeichert sind, und das zweite Array ([9.9,6.7]) für die Werte an diesen Indizes. In diesem Beispiel ist der Wert bei Index 48 also 9.9 und der Wert bei Index 49 ist 6.7. Die restlichen Vektorindizes haben alle den Wert 0.0.

Warum brauchen wir beide Arten von Vektoren? Beim maschinellen Lernen funktionieren einige Algorithmen, wie z. B. der Naive Bayes-Klassifikator, besser bei dichten Vektormerkmalen und können daher bei spärlichen Vektormerkmalen schlecht abschneiden.

Was kannst du tun, wenn dein Algorithmus für maschinelles Lernen mit den vorhandenen Merkmalen nicht gut funktioniert? Zunächst einmal hilft dir der im Folgenden beschriebene Prozess, ein Gefühl für deine Daten zu bekommen und sie an dein Ziel des maschinellen Lernens anzupassen. Falls nötig, kannst du versuchen, mehr Daten zu sammeln. Du kannst auch Algorithmen wählen, die bei spärlichen Vektoren besser funktionieren. Schließlich ist das ein Teil des Prozesses, um Modelle für maschinelles Lernen zu erstellen!

Tipp

Setze ein Lesezeichen und nutze die MLlib-Dokumentation. Es gibt ein Community-Projekt, das sich der Verbesserung der Spark-Dokumentation widmet, die jeden Tag weiterentwickelt und besser wird.

Vorverarbeitung mit MLlib-Transformatoren

Transformers sind Teil der Apache Spark MLlib-Bibliothek namens pyspark.ml​.fea⁠ture. Zusätzlich zu den Transformatoren bietet auch Extraktoren und Selektoren. Viele von ihnen basieren auf Algorithmen des maschinellen Lernens und statistischen oder mathematischen Berechnungen. Für die Vorverarbeitung werden wir die Transformers-API nutzen, aber auch die anderen APIs können in verschiedenen Situationen hilfreich sein.

Transformers in Spark sind Algorithmen oder Funktionen, die einen Datenrahmen (DataFrame) als Eingabe nehmen und einen neuen Datenrahmen mit den gewünschten Spalten ausgeben. Mit anderen Worten: Sie übersetzen eine gegebene Eingabe in eine entsprechende Ausgabe. Transformers ermöglichen es uns, bestehende Spalten zu skalieren, umzuwandeln oder zu verändern. Wir können sie grob in die folgenden Kategorien einteilen: Textdaten-Transformatoren, kategoriale Feature-Transformatoren, kontinuierliche numerische Transformatoren und andere.

Die Tabellen in den folgenden Abschnitten zeigen dir, wann du die einzelnen Transformatoren verwenden solltest. Du solltest dir darüber im Klaren sein, dass aufgrund der statistischen Natur der Transformatoren einige APIs länger dauern können als andere.

Arbeiten mit Textdaten

Textdaten bestehen in der Regel aus Dokumenten, die Wörter, Sätze oder irgendeine Form von frei fließendem Text darstellen. Es handelt sich dabei um unstrukturierte Daten, die oft verrauscht sind. Verrauschte Daten beim maschinellen Lernen sind irrelevante oder bedeutungslose Daten, die die Leistung des Modells erheblich beeinträchtigen können. Beispiele dafür sind Stoppwörter wie a, the, is und are. In MLlib findest du eine spezielle Funktion zum Extrahieren von Stoppwörtern und noch viel mehr! MLlib bietet eine Vielzahl von Funktionen für die Bearbeitung von Textdaten.

Mit dem Text wollen wir die Daten aufnehmen und in ein Format umwandeln, das leicht in einen Algorithmus für maschinelles Lernen eingespeist werden kann. Die meisten Algorithmen in MLlib erwarten strukturierte Daten als Eingabe, in einem Tabellenformat mit Zeilen, Spalten usw. Aus Gründen der Speichereffizienz werden die Strings in der Regel gehasht, da String-Werte mehr Platz benötigen als Integer-, Gleitkomma- oder boolesche Werte. Bevor du deinem Projekt für maschinelles Lernen Textdaten hinzufügst, musst du sie zunächst mit einem der Textdaten-Transformatoren bereinigen. Um mehr über die gängigen APIs und ihre Verwendung zu erfahren, wirf einen Blick auf Tabelle 4-1.

Tabelle 4-1. Textdaten-Transformatoren
API Verwendung
Tokenizer Bildet eine Textspalte in eine Liste von Wörtern um, indem es Leerzeichen aufspaltet. Sie basiert auf dem regulären Ausdruck (regex) \\s, der auf ein einzelnes Leerzeichen passt. Unter der Haube verwendet die Tokenizer API die Funktion java.lang.String.split.
Regex​Tokenizer Teilt den Text anhand der eingegebenen Regex auf (die Standardeinstellung ist \\s+, die auf ein oder mehrere Leerzeichen passt. Sie wird in der Regel verwendet, um auf Leerzeichen und/oder Kommas und andere unterstützte Begrenzungszeichen aufzuteilen. RegexTokenizer ist rechenintensiver als Tokenizer, da sie die Regex-Funktion scala.util.matching verwendet. Die übergebene Regex sollte der Java-Syntax für reguläre Ausdrücke entsprechen.
HashingTF Nimmt ein Array von Zeichenketten und erzeugt daraus Hashes. In vielen Freitextszenarien musst du zuerst die Funktion Tokenizer ausführen. Dies ist einer der am häufigsten verwendeten Transformatoren.
NGram Extrahiert eine Folge von n Token mit einer Ganzzahl n. Die Eingabespalte kann nur ein Array von Strings sein. Um eine Textzeichenkette in ein Array von Zeichenketten umzuwandeln, verwende zuerst die Funktion Tokenizer.
StopWordsRemover Nimmt eine Textsequenz und lässt die Standardstoppwörter weg. Du kannst Sprachen und Groß-/Kleinschreibung angeben und deine eigene Liste von Stoppwörtern erstellen.

Bevor wir fortfahren, erstellen wir einen synthetischen Datensatz, der in den folgenden Beispielen verwendet wird:

sentence_data_frame = spark.createDataFrame([
    (0, "Hi I think pyspark is cool ","happy"),
    (1, "All I want is a pyspark cluster","indifferent"),
    (2, "I finally understand how ML works","fulfilled"),
    (3, "Yet another sentence about pyspark and ML","indifferent"),
    (4, "Why didn’t I know about mllib before","sad"),
    (5, "Yes, I can","happy")
], ["id", "sentence", "sentiment"])

Unser Datensatz hat drei Spalten: id, vom Typ int, sowie sentence und sentiment, vom Typ string.

Die Umwandlung umfasst die folgenden Schritte:

  1. Freier Text → Liste von Wörtern

  2. Liste der Wörter → Liste der sinnvollen Wörter

  3. Wähle sinnvolle Werte

Fertig? Fertig? Umwandeln! Unser erster Schritt besteht darin, den freien Text in eine Liste von Wörtern umzuwandeln. Dazu können wir entweder die Tokenizer oder die RegexTokenizer API verwenden, wie hier gezeigt:

from pyspark.ml.feature import Tokenizer

tokenizer = Tokenizer(inputCol="sentence", outputCol="words")
tokenized = tokenizer.transform(sentence_data_frame)

Damit wird Tokenizer angewiesen, die Spalte sentence als Eingabe zu nehmen und einen neuen Datenrahmen zu erstellen, indem eine Ausgabespalte mit dem Namen words hinzugefügt wird. Beachte, dass wir die Funktion trans⁠form verwendet haben - Transformatoren haben immer diese Funktion. Abbildung 4-2 zeigt unseren neuen Datenrahmen mit der zusätzlichen Spalte words.

Abbildung 4-2. Neuer Datenrahmen mit der Spalte words

Im nächsten Schritt entfernen wir Stoppwörter, also Wörter, die für unser maschinelles Lernen wahrscheinlich nicht viel wert sind. Hierfür verwenden wir StopWordsRemover:

from pyspark.ml.feature import StopWordsRemover

remover = StopWordsRemover(inputCol="words", outputCol="meaningful_words")
meaningful_data_frame = remover.transform(tokenized)
# I use the show function here for educational purposes only; with a large 
# dataset, you should avoid it.
meaningful_data_frame.select("words","meaningful_words").show(5,truncate=False)

Beispiel 4-1 zeigt den Datenrahmen mit der neuen Spalte meaningful_words.

Beispiel 4-1. Neuer Datenrahmen mit der Spalte meaningful_words
+-------------------------------------------------+-------------------------------------+
|words                                            |meaningful_words                     |
+-------------------------------------------------+-------------------------------------+
|[hi, i, think, pyspark, is, cool]                |[hi, think, pyspark, cool]           |
|[all, i, want, is, a, pyspark, cluster]          |[want, pyspark, cluster]             |
|[i, finally, understand, how, ml, works]         |[finally, understand, ml, works]     |
|[yet, another, sentence, about, pyspark, and, ml]|[yet, another, sentence, pyspark, ml]|
|[why, didn't, i, know, about, ml lib, before]    |[know, mllib]                        |
|[yes,, i, can]                                   |[yes,]                               |
+-------------------------------------------------+-------------------------------------+

Von nominalen kategorialen Merkmalen zu Indizes

Eine der Strategien, mit denen wir den Prozess des maschinellen Lernens beschleunigen können, besteht darin, diskrete kategoriale Werte, die im Format string dargestellt werden, mithilfe von Indizes in eine numerische Form zu bringen. Die Werte können diskret oder kontinuierlich sein, je nachdem, welche maschinellen Lernmodelle wir verwenden wollen. Tabelle 4-2 listet die gängigsten APIs auf und beschreibt ihre Verwendung .

Tabelle 4-2. Kategorische Merkmalstransformatoren
API Verwendung
String​Indexer Codiert String-Spalten in Indizes, wobei der erste Wert (beginnend bei Index 0) der häufigste Wert in der Spalte ist usw. Wird für ein schnelleres Training mit überwachten Daten verwendet, bei denen die Spalten die Kategorien/Labels sind.
IndexTo​String Das Gegenteil von StringIndexer: Stellt eine Spalte mit Label-Indizes auf eine Spalte mit den ursprünglichen Labels als Strings zurück. Wird oft verwendet, um nach dem Trainingsprozess die Label-Kategorien wiederzufinden.
OneHot​Encoder Bildet eine Spalte mit kategorialen Merkmalen, die als Label-Indizes dargestellt werden, in eine Spalte mit binären Vektoren ab, wobei höchstens ein 1-Wert pro Zeile die Kategorie angibt. So können Algorithmen für maschinelles Lernen, die kontinuierliche Merkmale erwarten, wie z. B. die logistische Regression, kategoriale Merkmale verwenden, indem sie sie in kontinuierliche Merkmale umwandeln.
Vector​Indexer Ähnlich wie StringIndexer; nimmt eine Vektorspalte als Eingabe und wandelt sie in Kategorie-Indizes um.

Der von uns erstellte Datenrahmen enthält eine Spalte, die das Sentiment des Textes darstellt. Unsere Stimmungskategorien sind happy, fulfilled, sad und indifferent. Wir wandeln sie mit StringIndexer in Indizes um:

from pyspark.ml.feature import StringIndexer
indexer = StringIndexer(inputCol="sentiment", outputCol="categoryIndex")
indexed = indexer.fit(meaningful_data_frame).transform(meaningful_data_frame)
indexed.show(5)

In diesem Codeschnipsel erstellen wir eine neue StringIndexer Instanz, die die Spalte sentiment als Eingabe nimmt und einen neuen Datenrahmen mit einer Spalte categoryIndex vom Typ double erstellt. Zuerst rufen wir die Funktion fit auf und geben ihr den Namen unseres Datenrahmens. Dieser Schritt ist notwendig, um den Indexer vor der Transformation zu trainieren: Er erstellt eine Zuordnung zwischen Indizes und Kategorien, indem er die Spalte sentiment durchsucht. Diese Funktion wird von einem anderen Vorverarbeitungstool, dem Schätzer, übernommen, das wir in Kapitel 6 genauer betrachten werden. Nach der Anpassung des Schätzers rufen wir transform auf, um die neuen Indizes zu berechnen. Beispiel 4-2 zeigt den Datenrahmen mit der neuen Spalte cate⁠gory​Index.

Beispiel 4-2. Datenrahmen mit der Spalte categoryIndex
+---+--------------------+-----------+--------------------+--------------------+-------------+ 
| id|            sentence|  sentiment|               words|    meaningful_words|categoryIndex|
+---+--------------------+-----------+--------------------+--------------------+-------------+
|  O|Hi I think pyspar...|      happy|[hi, i, think, py...|[i, think, pyspa... |          0.0|
|  1|All I want is a p...|indifferent|[all, i, want, is...|[want, pyspark, c...|          1.0|
|  2|I finally underst...|  fulfilled|[i, finally, unde...|[finally, underst...|          2.0|
|  3|Yet another sente...|indifferent|[yet, another, se...|[yet, another, se...|          1.0|
|  4|Why didn't I know...|        sad|[why, didn't, i, ...|       [know, mllib]|          3.0|
|  5|          Yes, I can|      happy|      [yes,, i, can]|              [yes,]|          0.0|
+---+--------------------+-----------+--------------------+--------------------+-------------+

Strukturierung kontinuierlicher numerischer Daten

In einigen Fällen haben wir kontinuierliche numerische Daten, die wir strukturieren wollen. Dazu geben wir einen Schwellenwert oder mehrere Schwellenwerte an, um eine Aktion durchzuführen oder eine Entscheidung über eine Klassifizierung zu treffen.

Hinweis

Kontinuierliche numerische Werte werden oft in einem Vektor dargestellt, wobei die gängigen Datentypen integer, float und double sind.

Wenn wir zum Beispiel Werte für bestimmte Stimmungen haben, wie in Beispiel 4-3 gezeigt, können wir eine Maßnahme ergreifen, wenn ein bestimmter Wert in einen bestimmten Bereich fällt. Stell dir ein System zur Kundenzufriedenheit vor: Wir möchten, dass unser maschinelles Lernmodell eine Aktion empfiehlt, die auf den Stimmungswerten der Kunden basiert. Nehmen wir an, unser größter Kunde hat einen sad Wert von 0.75 und unser Schwellenwert für einen Anruf bei diesem Kunden, um zu besprechen, wie wir seine Erfahrung verbessern können, ist ein sad Wert über 0.7. In diesem Fall würden wir den Kunden anrufen wollen. Die Schwellenwerte selbst können manuell oder mithilfe von Algorithmen für maschinelles Lernen oder einfachen Statistiken festgelegt werden. Gehen wir davon aus, dass wir einen Datenrahmen mit einer bestimmten Punktzahl für jedes Sentiment haben. Dieser Wert ist eine kontinuierliche Zahl im Bereich [0,1], die die Relevanz der Stimmungs-Kategorie angibt. Das Geschäftsziel, das wir erreichen wollen, bestimmt die zu verwendenden Schwellenwerte und die Struktur, die wir den Daten für zukünftige Empfehlungen geben.

Beispiel 4-3. Datenrahmen mit Stimmungswerten für jede Kategorie
+-----------+-----+-----------+---------+----+
|sentence_id|happy|indifferent|fulfilled| sad|
+-----------+-----+-----------+---------+----+
|          0| 0.01|       0.43|      0.3| 0.5|
|          1|0.097|       0.21|      0.2| 0.9|
|          2|  0.4|      0.329|     0.97| 0.4|
|          3|  0.7|        0.4|      0.3|0.87|
|          4| 0.34|        0.4|      0.3|0.78|
|          5|  0.1|        0.3|     0.31|0.29|
+-----------+-----+-----------+---------+----+

Berücksichtige die Art der Daten, mit denen du arbeitest, und gruppiere sie, wenn nötig. Dafür kannst du die Spark SQL API nutzen, wie hier gezeigt:

cast_data_frame = sentiment_data_frame.selectExpr("cast(happy as double)")

Im Folgenden findest du einige gängige Strategien für den Umgang mit kontinuierlichen numerischen Daten:

Festes Bucketing/Binning
Diese wird manuell durchgeführt, indem die Daten entweder mit einem bestimmten Schwellenwert binarisiert oder eine Reihe von Bereichen festgelegt werden. Dieser Prozess ähnelt dem, den wir bereits im Zusammenhang mit der Strukturierung kontinuierlicher Daten besprochen haben.
Adaptives Bucketing/Binning
Die Gesamtdaten von können verzerrt sein, d.h. einige Werte kommen häufig vor, während andere selten sind. Das kann es schwierig machen, manuell einen Bereich für jeden Bucket festzulegen. Adaptive Bucketing ist eine fortschrittlichere Technik, bei der der Transformer die Verteilung der Daten berechnet und die Bucket-Größen so festlegt, dass jeder Bucket ungefähr die gleiche Anzahl von Werten enthält.

In Tabelle 4-3 sind die am häufigsten verwendeten kontinuierlichen numerischen Transformatoren aufgeführt, die in MLlib verfügbar sind. Erinnere dich daran, dass du den für dein Projekt passenden Transformator auswählst. .

Tabelle 4-3. Gängige Stetigzahlenwandler
API Verwendung
Binarizer Verwandelt ein numerisches Merkmal in ein binäres Merkmal, wenn ein Schwellenwert angegeben wird. Zum Beispiel wird aus 5,1 mit dem Schwellenwert 0,7 eine 1 und aus 0,6 eine 0.
Bucketizer Nimmt eine Spalte mit fortlaufenden numerischen Werten und wandelt sie in eine Spalte mit Bereichen um, wobei jeder Bereich einen Teil des Wertebereichs darstellt, z. B. 0 bis 1, 1 bis 2 und so weiter.
MaxAbsScaler Nimmt einen Vektor von float Werten und teilt jeden Wert durch den maximalen absoluten Wert in den Eingangsspalten.
MinMaxScaler Skaliert die Daten auf die gewünschten min und max Werte, wobei der Standardbereich [0,1] ist.
Normalizer Konvertiert einen Vektor von double Werten in normalisierte Werte, die nicht-negative reelle Zahlen zwischen 0 und 1 sind. Die Standard p-Norm ist 2, was die euklidische Norm zur Berechnung eines Abstands implementiert und den float Bereich auf [0,1] reduziert.
Quantile​Dis⁠cre⁠tizer Nimmt eine Spalte mit kontinuierlichen numerischen Werten und wandelt sie in eine Spalte mit kategorialen Werten um, wobei die eingegebene maximale Anzahl von Bins optional die ungefähren Quantilwerte bestimmt.
RobustScaler Ähnlich wie StandardScaler; nimmt einen Vektor von float Werten und erzeugt einen Vektor von skalierten Merkmalen, die den Eingabequantilbereich angeben.
StandardScaler Ein Schätzer, der einen Vektor von float Werten nimmt und versucht, die Daten anhand der Standardabweichung und des Mittelwerts zu zentrieren.

Zusätzliche Transformatoren

MLlib bietet viele zusätzliche Transformatoren, die Statistiken verwenden oder andere Spark-Funktionen abstrahieren. Tabelle 4-4 listet einige davon auf und beschreibt ihre Verwendung . Beachte, dass regelmäßig weitere hinzukommen. Codebeispiele findest du im Verzeichnis examples/src/main/python/ml/ des Apache Spark GitHub Repository.

Tabelle 4-4. Zusätzliche Transformatoren
API Verwendung
DCT Führt eine diskrete Kosinustransformation durch, bei der ein Vektor von Datenpunkten im Zeitbereich in den Frequenzbereich übertragen wird. Wird bei der Signalverarbeitung und Datenkomprimierung eingesetzt (z. B. bei Bildern, Audio, Radio und digitalem Fernsehen).
Elementwise​Product Nimmt eine Spalte mit Datenvektoren und einen Transformationsvektor der gleichen Größe und gibt eine Multiplikation aus, die assoziativ, distributiv und kommutativ ist (basierend auf dem Hadamard-Produkt). Dies wird verwendet, um die vorhandenen Vektoren zu skalieren.
Imputer Nimmt eine Spalte vom Typ numerisch und ergänzt fehlende Werte im Datensatz mit dem Mittelwert oder Median der Spalte. Nützlich bei der Verwendung von Schätzern, die mit fehlenden Werten nicht umgehen können.
Interaction Nimmt eine bestimmte vektorielle oder double-wertige Spalte und gibt eine Vektorspalte aus, die die Produkte aller möglichen Wertekombinationen enthält.
PCA Implementiert die Hauptkomponentenanalyse und verwandelt einen Vektor mit potenziell korrelierten Werten in nicht korrelierte Werte, indem die wichtigsten Komponenten der Daten (die Hauptkomponenten) ausgegeben werden. Dies ist nützlich für Vorhersagemodelle und die Reduzierung der Dimensionalität, kann aber zu Lasten der Interpretierbarkeit gehen.
Polynomial​Expansion Nimmt einen Vektor von Merkmalen und expandiert ihn in einen Polynomraum mit n Graden. Ein Wert von 1 bedeutet keine Erweiterung.
SQL​Trans⁠for⁠mer Nimmt eine SQL-Anweisung (jede SELECT Klausel, die Spark unterstützt) und wandelt die Eingabe entsprechend der Anweisung um.
Vector​Assem⁠bler Nimmt eine Liste von Vektorspalten und fügt sie zu einer Spalte im Datensatz zusammen. Dies ist nützlich für verschiedene Schätzer, die nur eine Spalte benötigen.

Vorverarbeitung von Bilddaten

Bilddaten werden häufig für Anwendungen des maschinellen Lernens verwendet und müssen ebenfalls vorverarbeitet werden, um im Workflow des maschinellen Lernens voranzukommen. Aber Bilder unterscheiden sich von den Daten, die wir bisher gesehen haben, und sie erfordern eine andere Vorgehensweise. Je nach Datenlage sind mehr oder weniger Schritte erforderlich, aber der häufigste Weg besteht aus diesen drei Schritten:

  1. Etiketten extrahieren

  2. Etiketten in Indizes umwandeln

  3. Bildgröße extrahieren

Lass uns diese Schritte anhand unseres Beispieldatensatzes durchgehen, um zu sehen, was sie beinhalten.

Etiketten extrahieren

Unser Bilderdatensatz hat eine verschachtelte Struktur, bei der der Verzeichnisname die Klassifizierung des Bildes angibt. Der Pfad eines jeden Bildes im Dateisystem enthält also seine Bezeichnung. Diese müssen wir extrahieren, um sie später verwenden zu können. Die meisten Rohbilddatensätze folgen diesem Muster, und das ist ein wesentlicher Teil der Vorverarbeitung, die wir mit unseren Bildern durchführen werden. Nachdem wir die Bilder als BinaryType geladen haben, erhalten wir eine Tabelle mit einer Spalte namens path vom Typ String. Diese enthält unsere Beschriftungen. Jetzt ist es an der Zeit, die Daten mit Hilfe der Stringmanipulation zu extrahieren. Schauen wir uns einen Beispielpfad an: .../256_Object​Cat⁠egories/198.spider/198_0089.jpg.

Die Bezeichnung ist in diesem Fall eigentlich ein Index und ein Name: 198.spider. Das ist der Teil, den wir aus dem String extrahieren müssen. Glücklicherweise stellen uns die PySpark SQL-Funktionen die regexp_extract API zur Verfügung, mit der wir Strings ganz einfach nach unseren Bedürfnissen manipulieren können.

Definieren wir eine Funktion, die eine path_col nimmt und diese API verwendet, um das Label mit der Regex "256_ObjectCategories/([^/]+)" zu extrahieren:

from pyspark.sql.functions import col, regexp_extract

def extract_label(path_col):
    """Extract label from file path using built-in SQL function"""
    return regexp_extract(path_col,"256_ObjectCategories/([^/]+)",1)

Wir können nun einen neuen Datenrahmen mit den Beschriftungen erstellen, indem wir diese Funktion aus einer Spark-SQL-Abfrage heraus aufrufen:

images_with_label = df_result.select( 
    col("path"),
    extract_label(col("path")).alias("label"),
    col("content"))

Unser images_with_label Datenrahmen besteht aus drei Spalten: zwei String-Spalten namens path und label und einer binären Spalte namens content.

Jetzt, wo wir unsere Labels haben, ist es an der Zeit, sie in Indizes umzuwandeln.

Umwandlung von Etiketten in Indizes

Wie bereits auf erwähnt, ist unsere label Spalte eine String-Spalte. Dies kann eine Herausforderung für maschinelle Lernmodelle darstellen, da Strings in der Regel viel Speicherplatz benötigen. Idealerweise sollte jede Zeichenkette in unserer Tabelle in eine effizientere Darstellung umgewandelt werden, bevor sie in einen Algorithmus für maschinelles Lernen aufgenommen wird, es sei denn, es ist wirklich notwendig, dies nicht zu tun. Da unsere Beschriftungen das folgende Format haben {index.name}haben, haben wir drei Möglichkeiten:

  1. Extrahiere den Index aus dem String selbst, indem du die Stringmanipulation nutzt.

  2. Erstelle einen neuen Index mit Sparks StringIndexer, wie in "Von nominalen kategorialen Merkmalen zu Indizes" beschrieben .

  3. Verwende Python, um einen Index zu definieren (im Caltech 256-Datensatz gibt es nur 257 Indizes, im Bereich [1,257]).

In unserem Fall ist die erste Option der sauberste Weg, dies zu tun. Mit diesem Ansatz können wir eine Zuordnung zwischen den Indizes in den Originaldateien und den Indizes im Datensatz vermeiden.

Bildgröße extrahieren

Der letzte Schritt von besteht darin, die Bildgröße zu extrahieren. Wir tun dies als Teil unserer Vorverarbeitung, weil wir sicher wissen, dass unser Datensatz Bilder in verschiedenen Größen enthält, aber es ist oft ein nützlicher Vorgang, um ein Gefühl für die Daten zu bekommen und uns mit Informationen zu versorgen, die uns bei der Entscheidung für einen Algorithmus helfen. Einige Algorithmen für maschinelles Lernen erfordern eine einheitliche Bildgröße, und wenn wir im Voraus wissen, mit welchen Bildgrößen wir arbeiten, können wir bessere Optimierungsentscheidungen treffen.

Da Spark diese Funktion noch nicht von Haus aus bietet, werden wir Pillow (auch bekannt als PIL) verwenden, eine freundliche Python-Bibliothek für die Arbeit mit Bildern. Um die Breite und Höhe aller unserer Bilder effizient zu extrahieren, werden wir eine benutzerdefinierte Funktion (UDF) von Pandas definieren, die auf unseren Spark-Executors verteilt ausgeführt werden kann. UDFs von Pandas, die mit pandas_udf als Dekorator definiert werden, werden mit Apache Arrow optimiert und sind bei gruppierten Operationen schneller (z. B. wenn sie nach einer groupBy angewendet werden).

Die Gruppierung ermöglicht es Pandas, vektorisierte Operationen durchzuführen. Für diese Art von Anwendungsfällen ist eine Pandas-UDF auf Spark effizienter. Für einfache Operationen wie a*b reicht eine Spark UDF aus und ist schneller, weil sie weniger Overhead hat.

Unsere UDF nimmt eine Reihe von Zeilen und bearbeitet sie parallel, was viel schneller ist als der traditionelle Ansatz, bei dem eine Zeile nach der anderen bearbeitet wird:

from pyspark.sql.functions import col, pandas_udf
from PIL import Image
import pandas as pd
@pandas_udf("width: int, height: int")
def extract_size_udf(content_series):
    sizes = content_series.apply(extract_size)
return pd.DataFrame(list(sizes))

Jetzt, wo wir die Funktion haben, können wir sie an die Funktion select von Spark übergeben, um die Bildgrößeninformationen zu extrahieren:

images_df = images_with_label.select( 
    col("path"),
    col("label"),
    extract_size_udf(col("content")).alias("size"),
    col("content"))

Die Daten zur Bildgröße werden in einen neuen Datenrahmen mit einer size Spalte vom Typ struct extrahiert, die die width und height enthält:

size:struct
    width:integer
    height:integer
Warnung

Beachte , dass bei der Verwendung der Funktion extract_size_udf alle Bilder von der JVM (Spark verwendet Scala unter der Haube) zur Python-Laufzeit mit Arrow übertragen werden, die Größen berechnet werden und die Größen dann zurück in die JVM übertragen werden. Um bei der Arbeit mit großen Datensätzen effizienter zu sein, vor allem wenn du keine Gruppierung verwendest, kann es sich lohnen, die Extraktion der Größen stattdessen auf der JVM/Scala-Ebene zu implementieren. Behalte solche Überlegungen im Hinterkopf, wenn du die Datenvorverarbeitung für die verschiedenen Phasen des maschinellen Lernens implementierst .

Speichere die Daten und vermeide das Problem der kleinen Dateien

du alle Vorverarbeitungen abgeschlossen hast, kann es sinnvoll sein, die Daten in einer kalten oder heißen Speicherung zu speichern, bevor du zum nächsten Schritt übergehst. Dies wird manchmal auch als Checkpoint bezeichnet, also als ein Zeitpunkt, an dem wir eine Version der Daten speichern, mit der wir zufrieden sind. Ein Grund für die Speicherung der Daten ist die schnelle Wiederherstellung: Wenn unser Spark-Cluster komplett zusammenbricht, müssen wir nicht alles von Grund auf neu berechnen, sondern können uns auf den letzten Checkpoint verlassen. Der zweite Grund ist die Vereinfachung der Zusammenarbeit. Wenn deine vorverarbeiteten Daten in der Speicherung verbleiben und deinen Kollegen zur Verfügung stehen, können sie sie nutzen, um ihre eigenen Abläufe zu entwickeln. Das ist besonders nützlich, wenn du mit großen Datensätzen arbeitest und Aufgaben erledigst, die umfangreiche Rechenressourcen und Zeit erfordern. Spark bietet uns die Möglichkeit, Daten in zahlreichen Formaten aufzunehmen und zu speichern. Wenn du dich dafür entscheidest, die Daten für die Zusammenarbeit zu speichern, ist es wichtig, alle Schritte zu dokumentieren: die Vorverarbeitung, die du durchgeführt hast, den Code, mit dem du sie implementiert hast, die aktuellen Anwendungsfälle, alle durchgeführten Anpassungen und alle externen Ressourcen, die du erstellt hast, wie z. B. Stoppwortlisten.

Vermeiden kleiner Dateien

Eine kleine Datei ist jede Datei, die deutlich kleiner als die Blockgröße der Speicherung ist. Ja, es gibt eine Mindestblockgröße, sogar bei Objektspeichern wie Amazon S3, Azure Blob usw.! Dateien, die deutlich kleiner sind als die Blockgröße, können dazu führen, dass Platz auf der Festplatte verschwendet wird, da die Speicherung den gesamten Block verwendet, um die Datei zu speichern, egal wie klein sie ist. Das ist ein Overhead, den wir vermeiden sollten. Darüber hinaus ist die Speicherung so optimiert, dass sie schnelles Lesen und Schreiben nach Blockgröße unterstützt. Aber keine Sorge - die Spark API hilft uns! Wir können ganz einfach vermeiden, wertvollen Speicherplatz zu verschwenden und einen hohen Preis für die Speicherung kleiner Dateien zu zahlen, indem wir die Funktionen repartition oder coalesce von Spark verwenden.

Da wir in unserem Fall mit Offline-Daten arbeiten, ohne dass die Berechnung innerhalb einer Millisekunde abgeschlossen sein muss, haben wir mehr Flexibilität bei der Auswahl der Methode. repartition erstellt völlig neue Partitionen und verteilt die Daten im Netzwerk, um sie gleichmäßig auf die angegebene Anzahl von Partitionen zu verteilen (die höher oder niedriger als die bestehende Anzahl sein kann). Das ist zwar zunächst mit hohen Kosten verbunden, aber die Spark-Funktionen werden schneller ausgeführt, weil die Daten optimal verteilt werden. Tatsächlich kann die Ausführung einer repartition Operation in jeder Phase des maschinellen Lernprozesses nützlich sein, wenn wir feststellen, dass die Berechnung relativ langsam ist und wir sie beschleunigen wollen. Die Funktion coalesce hingegen erkennt zunächst die vorhandenen Partitionen und mischt dann nur die notwendigen Daten. Sie kann nur verwendet werden, um die Anzahl der Partitionen zu reduzieren, nicht aber, um neue Partitionen hinzuzufügen. Sie ist bekannt dafür, dass sie schneller läuft als repartition, da sie die Menge der über das Netzwerk gemischten Daten minimiert. In manchen Fällen wird coalesce gar nicht geshuffelt, sondern es werden nur lokale Partitionen gebildet, was die Reduktionsfunktion sehr effizient macht.

Da wir die Kontrolle über die genaue Anzahl der Partitionen haben wollen und keine extrem schnelle Ausführung benötigen, ist es in unserem Fall in Ordnung, die langsamere Funktion repartition zu verwenden, wie hier gezeigt:

output_df = output_df.repartition(NUM_EXECUTERS)

Bedenke, dass du dich für die Funktion coalesce entscheiden solltest, wenn Zeit, Effizienz und eine möglichst geringe Netzwerkbelastung von entscheidender Bedeutung sind.

Bildkompression und Parkett

Nehmen wir an, wir wollen unseren Bilddatensatz im Parquet-Format speichern (falls du damit nicht vertraut bist: Parquet ist ein Open-Source-Dateiformat, das für eine effiziente Speicherung und Abfrage von Daten entwickelt wurde). Beim Speichern in diesem Format verwendet Spark standardmäßig einen Kompressionscodec namens Snappy. Da Bilder jedoch bereits komprimiert sind (z. B. mit JPEG, PNG usw.), wäre es nicht sinnvoll, sie erneut zu komprimieren. Wie können wir das vermeiden?

Wir speichern den bereits konfigurierten Komprimierungscodec in einer String-Instanz, konfigurieren Spark so, dass es mit dem unkomprimierten Codec in Parquet schreibt, speichern die Daten im Parquet-Format und weisen die Codec-Instanz wieder der Spark-Konfiguration für zukünftige Arbeiten zu. Das folgende Codeschnipsel demonstriert dies:

# Image data is already compressed, so we turn off Parquet compression
compression = spark.conf.get("spark.sql.parquet.compression.codec")
spark.conf.set("spark.sql.parquet.compression.codec", "uncompressed")

# Save the data stored in binary format as Parquet
output_df.write.mode("overwrite").parquet(save_path)
spark.conf.set("spark.sql.parquet.compression.codec", compression)

Deskriptive Statistik: Ein Gefühl für die Daten bekommen

Maschinelles Lernen ist keine Zauberei - du musst deine Daten verstehen, um effizient und effektiv mit ihnen arbeiten zu können. Ein solides Verständnis der Daten, bevor du mit dem Training deiner Algorithmen beginnst, wird dir später viel Zeit und Mühe ersparen. Zum Glück gibt es in MLlib eine spezielle Bibliothek namens pyspark.ml.stat, die alle Funktionen enthält, die du brauchst, um grundlegende Statistiken aus den Daten zu extrahieren.

Keine Sorge, wenn sich das einschüchternd anhört - du musst nicht alles über Statistik wissen, um MLlib nutzen zu können, aber ein gewisses Maß an Vertrautheit wird dir auf deinem Weg zum maschinellen Lernen definitiv helfen. Wenn wir die Daten mithilfe von Statistiken verstehen, können wir besser entscheiden, welchen Algorithmus für maschinelles Lernen wir verwenden, Verzerrungen erkennen und die Qualität der Daten einschätzen - wie bereits erwähnt: Wer Müll reingibt, bekommt Müll raus. Wenn wir Daten von schlechter Qualität in einen Algorithmus für maschinelles Lernen einspeisen, führt das zu einem leistungsschwachen Modell. Deshalb ist dieser Teil ein Muss!

Solange wir jedoch bewusste Annahmen darüber treffen, wie die Daten aussehen, was wir akzeptieren können und was nicht, können wir viel bessere Experimente durchführen und haben eine bessere Vorstellung davon, was wir entfernen, was wir eingeben und was wir nachsichtig behandeln können. Bedenke, dass diese Annahmen und alle Datenbereinigungsvorgänge, die wir durchführen, in der Produktion große Auswirkungen haben können, vor allem, wenn sie aggressiv sind (z. B. wenn wir alle Nullen in einer großen Anzahl von Zeilen entfernen oder zu viele Standardwerte eingeben, was die Entropie komplett zerstört). Achte darauf, dass die Annahmen, die in der Sondierungsphase über den Dateninput, die Qualitätsmessungen und die Definition von "schlechten" oder minderwertigen Daten gemacht wurden, nicht übereinstimmen.

Tipp

Für tiefergehende statistische Analysen eines bestimmten Datensatzes verwenden viele Datenwissenschaftler die Pandas-Bibliothek. Wie in Kapitel 2 erwähnt, ist pandas eine Python-Analysebibliothek für die Arbeit mit relativ kleinen Daten, die in den Speicher (RAM) eines Rechners passen. Ihr Gegenstück im Apache Spark-Ökosystem ist Koalas, das sich zu einer Pandas-API auf Spark entwickelt hat. Die Funktionen von Koalas und Pandas auf Spark sind nicht zu 100 % identisch, aber diese API erweitert die Möglichkeiten von Spark und bringt sie noch weiter.

In diesem Abschnitt schalten wir einen Gang zurück und konzentrieren uns darauf, mit den Spark MLlib-Funktionen zur Berechnung von Statistiken ein Gefühl für die Daten zu bekommen.

Berechnen von Statistiken

Willkommen beim Machine Learning Zoo Project!

Um über die Statistikfunktionen von MLlib zu lernen, verwenden wir den Datensatz zur Klassifizierung von Zootieren aus dem Kaggle-Repository. Dieser Datensatz wurde 1990 erstellt und besteht aus 101 Beispielen von Zootieren, die durch 16 boolesche Attribute beschrieben werden, die verschiedene Eigenschaften erfassen. Die Tiere können in sieben Typen eingeteilt werden: Säugetiere, Vögel, Reptilien, Fische, Amphibien, Käfer und wirbellose Tiere.

Um ein Gefühl für die Daten zu bekommen und deine Reise zum maschinellen Lernen besser planen zu können, musst du zunächst die Statistik der Merkmale berechnen. Wenn du weißt, wie die Daten verteilt sind, erhältst du wertvolle Erkenntnisse darüber, welche Algorithmen du auswählen, wie du das Modell auswerten und wie viel Aufwand du insgesamt in das Projekt investieren musst.

Deskriptive Statistik mit Spark Summarizer

Eine deskriptive Statistik ist eine zusammenfassende Statistik, die Merkmale aus einer Sammlung von Informationen quantitativ beschreibt oder zusammenfasst. MLlib stellt uns ein spezielles Summarizer Objekt zur Verfügung, um statistische Metriken aus einer bestimmten Spalte zu berechnen. Diese Funktion ist Teil des MLlib LinearRegression Algorithmus zur Erstellung der Line⁠ar​Regression​Summary. Bei der Erstellung der Summarizer müssen wir die gewünschten Metriken angeben. In Tabelle 4-5 sind die Funktionen aufgeführt, die in der Spark-API verfügbar sind.

Tabelle 4-5. Optionen für Summarizer-Metriken
Metrisch Beschreibung
mean Berechnet den Durchschnittswert einer bestimmten numerischen Spalte
sum Berechnet die Summe der numerischen Spalte
variance Berechnet die Varianz der Spalte (wie weit die Zahlen in der Spalte im Durchschnitt von ihrem Mittelwert abweichen)
std Berechnet die Standardabweichung der Spalte (die Quadratwurzel aus dem Varianzwert), um Ausreißern in der Spalte mehr Gewicht zu verleihen
count Berechnet die Anzahl der Elemente/Zeilen im Datensatz
numNonZeros Ermittelt die Anzahl der Werte ungleich Null in der Spalte
max Findet den maximalen Wert in der Spalte
min Findet den Mindestwert in der Spalte
normL1 Berechnet die L1-Norm (Ähnlichkeit zwischen den numerischen Werten) der Spalte
normL2 Berechnet die euklidische Norm der Spalte
Hinweis

Die L1- und L2-Normen (auch bekannt als euklidische Normen) sind Werkzeuge zur Berechnung des Abstands zwischen numerischen Punkten in einem n-dimensionalen Raum. Sie werden häufig als Maßstäbe zur Messung der Ähnlichkeit zwischen Datenpunkten in Bereichen wie Geometrie, Data Mining und Deep Learning verwendet.

Dieser Codeschnipsel zeigt, wie du eine Summarizer Instanz mit Metriken erstellst:

from pyspark.ml.stat import Summarizer
summarizer = Summarizer.metrics("mean","sum","variance","std")

Wie die anderen MLlib-Funktionen erwartet auch die Funktion Summarizer.metrics einen Vektor mit numerischen Merkmalen als Eingabe. Du kannst die Funktion Vector​Assem⁠bler von MLlib verwenden, um den Vektor zusammenzustellen.

Obwohl es im Datensatz zur Klassifizierung von Zootieren viele Merkmale gibt, werden wir nur die folgenden Spalten untersuchen:

  • feathers

  • milk

  • fins

  • domestic

Wie in "Data Ingestion mit Spark" beschrieben , laden wir die Daten in einen Datenrahmen namens zoo_data_for_statistics.

Im nächsten Codebeispiel kannst du sehen, wie der Vektor erstellt wird. Beachte, dass wir den Namen der Ausgabespalte auf features gesetzt haben, wie es der Summarizer erwartet:

from pyspark.ml.feature import VectorAssembler
# set the output col to features as expected as input for the summarizer
vecAssembler = VectorAssembler(outputCol="features")
# assemble only part of the columns for the example
vecAssembler.setInputCols(["feathers","milk","fins","domestic"])
vector_df = vecAssembler.transform(zoo_data_for_statistics)

Unser Vektor nutzt die Dataset-Funktion von Apache Spark. Ein Dataset in Spark ist eine stark typisierte Sammlung von Objekten, die den Datenrahmen kapselt. Du kannst den Datenrahmen bei Bedarf immer noch von einem Dataset aus aufrufen, aber die Dataset-API ermöglicht es dir, auf eine bestimmte Spalte zuzugreifen, ohne die spezielle Spaltenfunktionalität nutzen zu müssen:

Vector_df.features

Jetzt wir eine eigene Vektorspalte und eine Zusammenfassung haben, können wir einige Statistiken extrahieren. Wir können die Funktion summarizer.summary aufrufen, um alle Metriken darzustellen oder um eine bestimmte Metrik zu berechnen, wie im folgenden Beispiel gezeigt:

# compute statistics for multiple metrics
statistics_df = vector_df.select(summarizer.summary(vector_df.features))
# statistics_df will plot all the metrics
statistics_df.show(truncate=False)

# compute statistics for single metric (here, std) without the rest
vector_df.select(Summarizer.std(vector_df.features)).show(truncate=False)

Beispiel 4-4 zeigt die Ausgabe des Aufrufs von std für den Vektor der Merkmale.

Beispiel 4-4. std der Spalte features
+-------------------------------------------------------------------------------+
|std(features)                                                                  |
+-------------------------------------------------------------------------------+
|[0.4004947435409863,0.4935223970962651,0.37601348195757744,0.33655211592363116]|
+-------------------------------------------------------------------------------+

Die Standardabweichung (STD) ist ein Indikator für die Schwankung in einer Reihe von Werten. Eine niedrige STD zeigt an, dass die Werte tendenziell nahe am Mittelwert (auch Erwartungswert genannt) der Menge liegen, während eine hohe STD anzeigt, dass die Werte über einen größeren Bereich verteilt sind.

Hinweis

Summarizer.std ist eine globale Funktion, die du verwenden kannst, ohne eine Summarizer Instanz zu erstellen.

Da die Merkmale feathers, milk, fins und domestic von Natur aus vom Typ Boolesch -milk kann 1 für wahr oder 0 für falsch sein, und das Gleiche gilt für fins usw. - sind, bringt uns die Berechnung der STD nicht viel - das Ergebnis ist immer eine Dezimalzahl zwischen 0 und 1. Das verfehlt den Wert der STD, um zu berechnen, wie "verteilt" die Daten sind. Versuchen wir es stattdessen mit der Funktion sum. Diese Funktion sagt uns, wie viele Tiere im Datensatz Federn, Milch oder Flossen haben oder Haustiere sind:

# compute statistics for single metric "sum" without the rest
vector_df.select(Summarizer.sum(vector_df.features)).show(truncate=False)

Schau dir die Ausgabe von sum an, die in Beispiel 4-5 gezeigt wird.

+---------------------+
|sum(features)        |
+---------------------+
|[20.0,41.0,17.0,13.0]|
+---------------------+

So erfahren wir, dass es 20 Tiere mit Federn (der erste Wert des Vektors), 41 Tiere, die Milch geben (der zweite Wert des Vektors), 17 Tiere mit Flossen (der dritte Wert) und 13 Haustiere (der letzte Wert) gibt. Die Funktion sum gibt uns mehr Aufschluss über die Daten selbst als die Funktion std, da die Daten boolescher Natur sind. Je komplizierter und vielfältiger der Datensatz ist, desto hilfreicher ist die Betrachtung der verschiedenen Metriken für.

Daten Schieflage

Schiefe in der Statistik ist ein Maß für die Asymmetrie einer Wahrscheinlichkeitsverteilung. Stell dir eine Glockenkurve vor, bei der die Datenpunkte nicht symmetrisch auf der linken und rechten Seite des Mittelwerts der Kurve verteilt sind. Unter der Annahme, dass der Datensatz einer Normalverteilungskurve folgt, bedeutet Schiefe, dass er auf der einen Seite einen kurzen und auf der anderen Seite einen langen Schwanz hat. Je höher der Wert der Schiefe ist, desto ungleichmäßiger sind die Daten verteilt und desto mehr Datenpunkte fallen auf eine Seite der Glockenkurve.

Um die Schiefe, also die Asymmetrie der Werte um den Mittelwert, zu messen, müssen wir den Mittelwert extrahieren und die Standardabweichung berechnen. Eine statistische Gleichung, die dies ermöglicht, wurde bereits in Spark implementiert. Im nächsten Codeschnipsel siehst du, wie du sie nutzen kannst:

from pyspark.sql.functions import skewness 
df_with_skew = df.select(skewness("{column_name}"))

Dieser Code liefert einen neuen Datenrahmen mit einer speziellen Spalte, die die Schiefe der angeforderten Spalte misst. Spark implementiert auch andere statistische Funktionen, wie z. B. Kurtosis, die den Schwanz der Daten misst. Beide Funktionen sind wichtig, wenn du ein Modell erstellst, das auf der Verteilung von Zufallsvariablen und der Annahme basiert, dass die Daten einer Normalverteilung folgen. Sie können dir helfen, Verzerrungen, Änderungen der Datentopologie und sogar Datendrift zu erkennen. Auf die Datendrift gehen wir in Kapitel 10 näher ein, wenn wir uns mit der Überwachung von Machine Learning-Modellen in der Produktion befassen).

Korrelation

Eine Korrelation zwischen zwei Merkmalen bedeutet, dass, wenn Merkmal A zunimmt oder abnimmt, Merkmal B dasselbe tut (eine positive Korrelation) oder genau das Gegenteil (eine negative Korrelation). Bei der Bestimmung der Korrelation geht es also darum, die lineare Beziehung zwischen den beiden Variablen/Merkmalen zu messen. Da das Ziel eines Algorithmus für maschinelles Lernen darin besteht, aus den Daten zu lernen, ist es unwahrscheinlich, dass perfekt korrelierte Merkmale Erkenntnisse zur Verbesserung der Modellgenauigkeit liefern. Deshalb kann das Herausfiltern dieser Merkmale die Leistung unseres Algorithmus erheblich verbessern und gleichzeitig die Qualität der Ergebnisse erhalten. Die Methode test der KlasseChiSquareTest in MLlib ist ein statistischer Test, der uns hilft, kategoriale Daten und Labels zu bewerten, indem er eine Pearson-Korrelation für alle Paare durchführt und eine Matrix mit Korrelationswerten ausgibt.

Warnung

Beachte, dass Korrelation nicht unbedingt Kausalität bedeutet. Wenn sich die Werte zweier Variablen in einer korrelierten Weise verändern, gibt es keine Garantie dafür, dass die Veränderung der einen Variable die Veränderung der anderen verursacht. Es erfordert mehr Aufwand, eine kausale Beziehung zu beweisen.

In diesem Abschnitt lernst du etwas über Pearson und Spearman-Korrelationen in Spark MLlib.

Pearson-Korrelation

Wenn die Korrelation untersucht, suchen wir nach positiven oder negativen Zusammenhängen. Die Pearson-Korrelation misst die Stärke des linearen Zusammenhangs zwischen zwei Variablen. Sie ergibt einen Koeffizienten r, der angibt, wie weit die Datenpunkte von einer beschreibenden Linie entfernt sind. Der Bereich von r ist [–1,1], wobei:

  • r=1 ist eine perfekte positive Korrelation. Beide Variablen wirken auf die gleiche Weise.

  • r=–1 ist eine perfekte negative/umgekehrte Korrelation, was bedeutet, dass wenn eine Variable steigt, die andere sinkt.

  • r=0 bedeutet keine Korrelation.

Abbildung 4-3 zeigt einige Beispiele in einem Diagramm.

Abbildung 4-3. Beispiele für die Pearson-Korrelation in einem Diagramm

Pearson ist der Standardkorrelationstest mit dem MLlib Correlation Objekt. Schauen wir uns einen Beispielcode und seine Ergebnisse an:

from pyspark.ml.stat import Correlation

# compute r1 0 Pearson correlation
r1 = Correlation.corr(vector_df, "features").head()
print("Pearson correlation matrix:\n" + str(r1[0])+ "\n")

Die Ausgabe ist eine Zeile, in der der erste Wert ein DenseMatrix ist, wie in Beispiel 4-6 gezeigt.

Beispiel 4-6. Pearson-Korrelationsmatrix
Pearson correlation matrix:
DenseMatrix([[ 1.        , -0.41076061, -0.22354106,  0.03158624],
             [-0.41076061,  1.        , -0.15632771,  0.16392762],
             [-0.22354106, -0.15632771,  1.        , -0.09388671],
             [ 0.03158624,  0.16392762, -0.09388671,  1.        ]])

Jede Linie stellt die Korrelation eines Merkmals mit allen anderen Merkmalen dar, und zwar paarweise: r1[0][0,1] stellt zum Beispiel die Korrelation von feathers mit milk dar, was ein negativer Wert ist (-0.41076061), der eine negative Korrelation zwischen Tieren, die Milch produzieren, und Tieren mit Federn anzeigt.

Tabelle 4-6 zeigt, wie die Korrelationstabelle aussieht, um dies zu verdeutlichen.

Tabelle 4-6. Pearson-Korrelationstabelle
feathers milk fins domestic
feathers 1 -.41076061 -0.22354106 0.03158624
milk -0.41076061 1 -0.15632771 0.16392762
fins -0.22354106 -0.15632771 1 -0.09388671
domestic 0.03158624 0.16392762 -0.09388671 1

Anhand dieser Tabelle lassen sich negative und positive Korrelationen leicht erkennen: fins und milk haben zum Beispiel eine negative Korrelation, während domestic und milk eine positive Korrelation haben.

Spearman-Korrelation

Die Spearman Korrelation, auch bekannt als Spearman-Rangkorrelation, misst die Stärke und Richtung der monotonen Beziehung zwischen zwei Variablen. Im Gegensatz zur Pearson-Korrelation, die eine lineare Beziehung misst, handelt es sich hier um eine krummlinige Beziehung, d.h. der Zusammenhang zwischen den beiden Variablen ändert sich, wenn sich die Werte ändern (steigen oder fallen). Die Spearman-Korrelation sollte verwendet werden, wenn es sich um diskrete Daten handelt und die Beziehungen zwischen den Datenpunkten nicht notwendigerweise linear sind, wie in Abbildung 4-4 dargestellt, sowie wenn eine Rangfolge von Interesse ist. Um zu entscheiden, welcher Ansatz besser zu deinen Daten passt, musst du die Art der Daten selbst verstehen: Wenn sie auf einer Ordinalskala liegen, verwende2 verwende Spearman, und wenn es sich um eine Intervallskala handelt,3 Pearson verwenden. Um mehr darüber zu erfahren, empfehle ich dir das Buch Practical Statistics for Data Scientists von Peter Bruce, Andrew Bruce und Peter Gedeck (O'Reilly).

Abbildung 4-4. Ein Beispiel, das zeigt, dass Spearman-Korrelationsdiagramme keine lineare Kurve ergeben (Bildquelle: Wikipedia, CC BY-SA 3.0)

Beachte, dass du Spearman angeben musst, um es zu verwenden (die Standardeinstellung ist Pearson):

from pyspark.ml.stat import Correlation

# compute r2 0 Spearman correlation
r2 = Correlation.corr(vector_df, "features", "spearman").head()
print("Spearman correlation matrix:\n" + str(r2[0]))

Wie zuvor ist die Ausgabe eine Zeile, in der der erste Wert ein DenseMatrix ist, und sie folgt den gleichen Regeln und der gleichen Reihenfolge wie zuvor beschrieben (siehe Beispiel 4-7).

Beispiel 4-7. Spearman-Korrelationsmatrix
Spearman correlation matrix:
DenseMatrix([[ 1.        , -0.41076061, -0.22354106,  0.03158624],
			 [-0.41076061,  1.        , -0.15632771,  0.16392762],
			 [-0.22354106, -0.15632771,  1.        , -0.09388671],
			 [ 0.03158624,  0.16392762, -0.09388671,  1.        ]])
	
Tipp

Spark verfügt über eine Automatisierung für Merkmalsselektoren, die auf Korrelations- und Hypothesentests wie Chi-Quadrat, ANOVA F-Test und F-Wert basieren (die UnivariateFeatureSelector; siehe Tabelle 5-2 in Kapitel 5). Um den Prozess zu beschleunigen, ist es am besten, vorhandene, implementierte Tests zu verwenden, anstatt jede Hypothese selbst zu berechnen. Wenn du nach der Merkmalsauswahl einen unzureichenden Satz von Merkmalen feststellst, solltest du Hypothesentests wie ChiSquareTest verwenden, um zu prüfen, ob du deine Daten anreichern oder einen größeren Satz von Daten finden musst. Ich habe dir im GitHub-Repository des Buches ein Codebeispiel zur Verfügung gestellt, das dir zeigt, wie das geht. Statistische Hypothesentests haben eine Nullhypothese (H0) und eine Alternativhypothese (H1), wobei :

  • H0: Die Stichprobendaten entsprechen der angenommenen Verteilung.
  • H1: Die Stichprobendaten folgen nicht der hypothetischen Verteilung.

Das Ergebnis des Tests ist der p-Wert, der angibt, wie hoch die Wahrscheinlichkeit ist, dass H0 wahr ist.

Zusammenfassung

In diesem Kapitel haben wir drei wichtige Schritte im Arbeitsablauf des maschinellen Lernens besprochen: das Einlesen von Daten, die Vorverarbeitung von Texten und Bildern und das Erfassen von beschreibenden Statistiken. Datenwissenschaftler/innen und Ingenieur/innen für maschinelles Lernen verbringen in der Regel einen großen Teil ihrer Zeit mit diesen Aufgaben, und wenn wir sie mit Bedacht ausführen, können wir erfolgreicher sein und ein besseres maschinelles Lernmodell entwickeln, das das Geschäftsziel auf eine viel tiefgreifendere Art und Weise erreichen kann. Als Faustregel gilt, dass es am besten ist, mit deinen Kollegen zusammenzuarbeiten, um diese Schritte zu validieren und zu entwickeln, damit die daraus resultierenden Erkenntnisse und Daten in mehreren Experimenten wiederverwendet werden können. Die in diesem Kapitel vorgestellten Werkzeuge werden uns während des gesamten Prozesses des maschinellen Lernens immer wieder begleiten. Im nächsten Kapitel werden wir uns mit dem Feature Engineering beschäftigen und auf den Ergebnissen dieses Kapitels aufbauen.

1 Das Schema der binären Datenquelle kann sich mit neuen Versionen von Apache Spark oder bei der Verwendung von Spark in verwalteten Umgebungen wie Databricks ändern.

2 Bei einer Ordinalskala sind alle Variablen in einer bestimmten Reihenfolge angeordnet, die über die bloße Nennung der Variablen hinausgeht.

3 Eine Intervallskala kennzeichnet und ordnet ihre Variablen und gibt einen definierten, gleichmäßigen Abstand zwischen ihnen an.

Get Skalierung von Machine Learning mit Spark 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.