Kapitel 4. Vorhersagen mit Entscheidungsbäumenund Entscheidungswäldern treffen
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Klassifizierung und Regression sind die ältesten und am besten untersuchten Arten der prädiktiven Analytik. Die meisten Algorithmen, die du in Analysepaketen und -bibliotheken finden wirst, sind Klassifizierungs- oder Regressionstechniken, wie z. B. Support-Vektor-Maschinen, logistische Regression, neuronale Netze und Deep Learning. Die Gemeinsamkeit zwischen Regression und Klassifizierung besteht darin, dass es in beiden Fällen darum geht, einen (oder mehrere) Werte anhand eines (oder mehrerer) anderer Werte vorherzusagen. Dazu benötigen beide eine Reihe von Eingaben und Ausgaben, aus denen sie lernen können. Sie müssen sowohl mit Fragen als auch mit bekannten Antworten gefüttert werden. Aus diesem Grund werden sie als Arten des überwachten Lernens bezeichnet.
PySpark MLlib bietet Implementierungen einer Anzahl von Klassifizierungs- und Regressionsalgorithmen. Dazu gehören Entscheidungsbäume, Naïve Bayes, logistische Regression und lineare Regression. Das Spannende an diesen Algorithmen ist, dass sie dabei helfen können, die Zukunft vorherzusagen - oder zumindest die Dinge vorherzusagen, die wir noch nicht sicher wissen, wie z. B. die Wahrscheinlichkeit, dass du aufgrund deines Online-Verhaltens ein Auto kaufst, ob eine E-Mail aufgrund der darin enthaltenen Wörter Spam ist oder auf welchen Äckern aufgrund der Lage und der Bodenchemie wahrscheinlich die meisten Pflanzen wachsen werden.
In diesem Kapitel konzentrieren wir uns auf einen beliebten und flexiblen Algorithmus für sowohl für Klassifizierung als auch für Regression (Entscheidungsbäume) und die Erweiterung des Algorithmus (zufällige Entscheidungswälder). Zunächst werden wir die Grundlagen von Entscheidungsbäumen und -wäldern verstehen und die PySpark-Implementierung von Entscheidungsbäumen vorstellen. Die PySpark-Implementierung von Entscheidungsbäumen unterstützt binäre und Mehrklassen-Klassifikation sowie Regression. Die Implementierung teilt die Daten nach Zeilen auf und ermöglicht so ein verteiltes Training mit Millionen oder sogar Milliarden von Instanzen. Im Anschluss daran bereiten wir unseren Datensatz vor und erstellen unseren ersten Entscheidungsbaum. Dann tunen wir unser Entscheidungsbaummodell. Zum Schluss trainieren wir ein Random-Forest-Modell auf unserem verarbeiteten Datensatz und machen Vorhersagen.
Obwohl die PySpark-Implementierung von Entscheidungsbäumen einfach zu bedienen ist, ist es hilfreich, die Grundlagen der Entscheidungsbaum- und Random-Forest-Algorithmen zu verstehen. Das werden wir im nächsten Abschnitt behandeln.
Entscheidungsbäume und Wälder
Entscheidungsbäume sind eine Familie von Algorithmen die sowohl kategoriale als auch numerische Merkmale verarbeiten können. Die Erstellung eines einzelnen Baums kann durch paralleles Rechnen erfolgen, und es können viele Bäume gleichzeitig erstellt werden. Sie sind robust gegenüber Ausreißern in den Daten, was bedeutet, dass einige extreme und möglicherweise fehlerhafte Datenpunkte die Vorhersagen nicht beeinflussen. Sie können Daten unterschiedlicher Art und auf verschiedenen Skalen verarbeiten, ohne dass eine Vorverarbeitung oder Normalisierung erforderlich ist.
Entscheidungsbaumbasierte Algorithmen haben den Vorteil, dass sie vergleichsweise intuitiv zu verstehen und zu verstehen sind. In der Tat verwenden wir wahrscheinlich alle die gleichen Überlegungen, die in Entscheidungsbäumen enthalten sind, implizit im Alltag. Ich setze mich zum Beispiel morgens hin, um meinen Kaffee mit Milch zu trinken. Bevor ich mich für die Milch entscheide und sie in mein Gebräu gebe, möchte ich vorhersagen: Ist die Milch verdorben? Ich weiß es nicht genau. Ich könnte nachsehen, ob das Haltbarkeitsdatum abgelaufen ist. Wenn nicht, sage ich voraus: Nein, sie ist nicht verdorben. Wenn das Datum abgelaufen ist, aber es drei oder weniger Tage her ist, gehe ich das Risiko ein und sage nein, sie ist nicht verdorben. Ansonsten rieche ich an der Milch. Wenn sie komisch riecht, sage ich ja, sonst nein.
Diese Reihe von Ja/Nein-Entscheidungen, die zu einer Vorhersage führen, sind das, was Entscheidungsbäume verkörpern. Jede Entscheidung führt zu einem von zwei Ergebnissen, nämlich entweder zu einer Vorhersage oder zu einer anderen Entscheidung, wie in Abbildung 4-1 dargestellt. In diesem Sinne ist es naheliegend, sich den Prozess als Entscheidungsbaum vorzustellen, bei dem jeder interne Knoten im Baum eine Entscheidung und jeder Blattknoten eine endgültige Antwort darstellt.
Das ist ein vereinfachter Entscheidungsbaum und wurde nicht mit der nötigen Sorgfalt erstellt. Betrachten wir ein anderes Beispiel, um es zu verdeutlichen. Ein Roboter hat einen Job in einem Geschäft für exotische Haustiere angenommen. Er möchte vor der Eröffnung des Ladens herausfinden, welche Tiere im Laden ein gutes Haustier für ein Kind wären. Der Besitzer listet neun Haustiere auf, die sich eignen und die sich nicht eignen, bevor er sich auf den Weg macht. Der Roboter stellt die Informationen in Tabelle 4-1 zusammen, nachdem er die Tiere untersucht hat.
Name | Gewicht (kg) | # Beine | Farbe | Gutes Haustier? |
---|---|---|---|---|
Fido |
20.5 |
4 |
Braun |
Ja |
Mr. Slither |
3.1 |
0 |
Grün |
Nein |
Nemo |
0.2 |
0 |
Tan |
Ja |
Dumbo |
1390.8 |
4 |
Gray |
Nein |
Kitty |
12.1 |
4 |
Gray |
Ja |
Jim |
150.9 |
2 |
Tan |
Nein |
Millie |
0.1 |
100 |
Braun |
Nein |
McPigeon |
1.0 |
2 |
Gray |
Nein |
Spot |
10.0 |
4 |
Braun |
Ja |
Der Roboter kann eine Entscheidung für die neun aufgelisteten Haustiere treffen. Es gibt noch viele weitere Haustiere im Laden. Er braucht noch eine Methode, um zu entscheiden, welche der anderen Tiere als Haustiere für Kinder geeignet sind. Wir können davon ausgehen, dass die Merkmale aller Tiere verfügbar sind. Mithilfe der vom Ladenbesitzer bereitgestellten Entscheidungsdaten und eines Entscheidungsbaums können wir dem Roboter helfen zu lernen, wie ein gutes Haustier für ein Kind aussieht.
Obwohl ein Name angegeben wird, wird er nicht als Merkmal in unser Entscheidungsbaummodell aufgenommen. Es gibt wenig Grund zu der Annahme, dass der Name allein eine Vorhersagekraft hat; "Felix" könnte auch eine Katze oder eine giftige Tarantel heißen, soweit der Roboter weiß. Es gibt also zwei numerische Merkmale (Gewicht, Anzahl der Beine) und ein kategorisches Merkmal (Farbe), die ein kategorisches Ziel vorhersagen (ist/ist kein gutes Haustier für ein Kind).
Die Funktionsweise eines Entscheidungsbaums besteht darin, dass eine oder mehrere Entscheidungen nacheinander auf der Grundlage von vorgegebenen Merkmalen getroffen werden. Zu Beginn könnte der Roboter versuchen, einen einfachen Entscheidungsbaum an die Trainingsdaten anzupassen, der aus einer einzigen Entscheidung auf der Grundlage der Gewichtung besteht, wie in Abbildung 4-2 dargestellt.
Die Logik des Entscheidungsbaums ist einfach zu lesen und nachvollziehbar: 500 kg schwere Tiere klingen sicherlich ungeeignet als Haustiere. Diese Regel sagt in fünf von neun Fällen den richtigen Wert voraus. Ein kurzer Blick zeigt, dass wir die Regel verbessern könnten, indem wir die Gewichtsschwelle auf 100 kg herabsetzen. Dadurch werden sechs von neun Beispielen richtig. Die schweren Tiere werden nun richtig vorhergesagt; die leichteren Tiere sind nur teilweise richtig.
Daher kann eine zweite Entscheidung erstellt werden, um die Vorhersage für Beispiele mit einem Gewicht von weniger als 100 kg weiter zu verfeinern. Es wäre gut, ein Merkmal zu wählen, das einige der falschen Ja-Vorhersagen in Nein ändert. Es gibt zum Beispiel ein kleines grünes Tier, das verdächtig nach einer Schlange klingt und von unserem aktuellen Modell als geeigneter Haustierkandidat eingestuft wird. Der Roboter könnte die richtige Vorhersage treffen, indem er eine Entscheidung aufgrund der Farbe hinzufügt, wie in Abbildung 4-3 gezeigt.
Jetzt sind sieben von neun Beispielen richtig. Natürlich könnten weitere Entscheidungsregeln hinzugefügt werden, bis alle neun Beispiele richtig vorhergesagt wurden. Die Logik des resultierenden Entscheidungsbaums würde wahrscheinlich unplausibel klingen, wenn man sie in die Alltagssprache übersetzt: "Wenn das Tier weniger als 100 kg wiegt, seine Farbe braun statt grün ist und es weniger als 10 Beine hat, dann ja, dann ist es ein geeignetes Haustier." Ein solcher Entscheidungsbaum passt zwar perfekt zu den gegebenen Beispielen, aber er würde fehlschlagen, wenn er vorhersagen würde, dass ein kleiner, brauner, vierbeiniger Vielfraß kein geeignetes Haustier ist. Um dieses Phänomen, das als Overfitting bezeichnet wird, zu vermeiden, ist ein gewisses Gleichgewicht erforderlich.
Entscheidungsbäume verallgemeinern sich zu einem leistungsfähigeren Algorithmus, den sogenannten Random Forests. Random Forests kombinieren viele Entscheidungsbäume, um das Risiko einer Überanpassung zu verringern, und trainieren die Entscheidungsbäume separat. Der Algorithmus fügt dem Trainingsprozess Zufälligkeiten hinzu, sodass jeder Entscheidungsbaum ein wenig anders ist. Durch das Kombinieren der Vorhersagen wird die Varianz der Vorhersagen verringert, das resultierende Modell verallgemeinerbar und die Leistung bei Testdaten verbessert.
Das ist eine ausreichende Einführung in Entscheidungsbäume und Random Forests, damit wir sie mit PySpark verwenden können. Im nächsten Abschnitt werden wir den Datensatz vorstellen, mit dem wir arbeiten werden, und ihn für die Verwendung in PySpark vorbereiten.
Aufbereitung der Daten
Der in diesem Kapitel verwendete Datensatz ist der bekannte Covtype-Datensatz, der online als komprimierte CSV-Datei, covtype.data.gz, und als zugehörige Infodatei, covtype.info, verfügbar ist.
Der Datensatz erfasst die Arten der bewaldeten Grundstücke in Colorado, USA. Es ist nur ein Zufall, dass es sich bei dem Datensatz um echte Wälder handelt! Jeder Datensatz enthält mehrere Merkmale, die das jeweilige Grundstück beschreiben, wie z. B. die Höhe, die Neigung, die Entfernung zum Wasser, den Schatten und die Bodenart, sowie die bekannte Waldart, die das Grundstück bedeckt. Die Art der Waldbedeckung soll aus den übrigen Merkmalen vorhergesagt werden, von denen es insgesamt 54 gibt.
Dieser Datensatz wurde in der Forschung verwendet und hat sogar einem Kaggle-Wettbewerb teilgenommen. Er ist ein interessanter Datensatz, der in diesem Kapitel untersucht werden soll, weil er sowohl kategoriale als auch numerische Merkmale enthält. Der Datensatz enthält 581.012 Beispiele, was nicht gerade als Big Data bezeichnet werden kann, aber groß genug ist, um als Beispiel handhabbar zu sein und dennoch einige Probleme der Skalierung aufzuzeigen.
Glücklicherweise liegen die Daten bereits in einem einfachen CSV-Format vor und müssen nicht weiter aufbereitet werden, damit sie mit PySpark MLlib verwendet werden können. Die Datei covtype.data sollte extrahiert und in deine lokale oder eine Cloud Speicherung (z. B. AWS S3) kopiert werden.
Starte pyspark-shell
. Es kann hilfreich sein, der Shell genügend Speicherplatz zur Verfügung zu stellen, da das Erstellen von Entscheidungswäldern sehr ressourcenintensiv sein kann. Wenn du den Speicherplatz hast, gib --driver-memory 8g
oder ähnliches an.
CSV-Dateien enthalten im Wesentlichen tabellarische Daten, die in Zeilen mit Spalten organisiert sind. Manchmal werden diese Spalten in einer Kopfzeile benannt, aber das ist hier nicht der Fall. Die Spaltennamen werden in der Begleitdatei covtype.info angegeben. Eigentlich hat jede Spalte einer CSV-Datei auch einen Typ - eine Zahl oder eine Zeichenkette -, aber in einer CSV-Datei wird das nicht angegeben.
Es liegt nahe, diese Daten als Datenrahmen zu analysieren, denn das ist PySparks Abstraktion für tabellarische Daten mit einem definierten Spaltenschema, einschließlich Spaltennamen und -typen. PySpark hat eine integrierte Unterstützung für das Lesen von CSV-Daten. Lesen wir unseren Datenrahmen als DataFrame mit dem eingebauten CSV-Reader:
data_without_header
=
spark
.
read
.
option
(
"inferSchema"
,
True
)
\.
option
(
"header"
,
False
)
.
csv
(
"data/covtype.data"
)
data_without_header
.
printSchema
()
...
root
|--
_c0
:
integer
(
nullable
=
true
)
|--
_c1
:
integer
(
nullable
=
true
)
|--
_c2
:
integer
(
nullable
=
true
)
|--
_c3
:
integer
(
nullable
=
true
)
|--
_c4
:
integer
(
nullable
=
true
)
|--
_c5
:
integer
(
nullable
=
true
)
...
Dieser Code liest die Eingabe als CSV und versucht nicht, die erste Zeile als Kopfzeile mit Spaltennamen zu analysieren. Er verlangt auch, dass der Typ jeder Spalte durch die Untersuchung der Daten ermittelt wird. Er folgert korrekt, dass alle Spalten Zahlen sind, genauer gesagt, ganze Zahlen. Leider kann er die Spalten nur _c0
und so weiter nennen.
Wir können die Spaltennamen in der Datei covtype.info nachlesen.
$ cat data/covtype.info ...[
...]
7
. Attribute information: Given is the attribute name, attribute type, the measurement unit and a brief description. The forest covertype
is the classification problem. The order of this listing corresponds to the order of numerals along the rows of the database. Name Data Type Elevation quantitative Aspect quantitative Slope quantitative Horizontal_Distance_To_Hydrology quantitative Vertical_Distance_To_Hydrology quantitative Horizontal_Distance_To_Roadways quantitative Hillshade_9am quantitative Hillshade_Noon quantitative Hillshade_3pm quantitative Horizontal_Distance_To_Fire_Points quantitative Wilderness_Area(
4
binary columns)
qualitative Soil_Type(
40
binary columns)
qualitative Cover_Type(
7
types)
integer Measurement Description meters Elevationin
meters azimuth Aspectin
degrees azimuth degrees Slopein
degrees meters Horz Dist to nearest surface water features meters Vert Dist to nearest surface water features meters Horz Dist to nearest roadway0
to255
index Hillshade index at 9am, summer solstice0
to255
index Hillshade index at noon, summer soltice0
to255
index Hillshade index at 3pm, summer solstice meters Horz Dist to nearest wildfire ignition point0
(
absence)
or1
(
presence)
Wilderness area designation0
(
absence)
or1
(
presence)
Soil Type designation1
to7
Forest Cover Type designation ...
Wenn du dir die Spalteninformationen ansiehst, wird klar, dass einige Merkmale tatsächlich numerisch sind. Elevation
ist eine Höhenangabe in Metern; Slope
wird in Grad gemessen. Wilderness_Area
ist jedoch etwas anderes, denn es wird gesagt, dass es sich über vier Spalten erstreckt, von denen jede eine 0 oder 1 ist. In Wirklichkeit ist Wilderness_Area
ein kategorischer Wert, kein numerischer.
Diese vier Spalten sind eigentlich eine One-Hot- oder 1-of-N-Codierung. Bei dieser Form der Kodierung eines kategorialen Merkmals werden aus einem kategorialen Merkmal, das N verschiedene Werte annimmt, N numerische Merkmale, von denen jedes den Wert 0 oder 1 annimmt. Genau einer der N Werte hat den Wert 1, die anderen sind 0. Ein kategoriales Merkmal für das Wetter, das cloudy
, rainy
oder clear
sein kann, wird beispielsweise in drei numerische Merkmale umgewandelt, wobei cloudy
durch 1,0,0
, rainy
durch 0,1,0
usw. dargestellt wird. Diese drei numerischen Merkmale könnte man sich als is_cloudy
, is_rainy
und is_clear
vorstellen. Genauso sind 40 Spalten ein kategorisches Merkmal Soil_Type
.
Dies ist nicht die einzige Möglichkeit, ein kategorisches Merkmal als Zahl zu kodieren. Eine andere Möglichkeit ist, jedem möglichen Wert des kategorialen Merkmals einen eigenen Zahlenwert zuzuweisen. So kann zum Beispiel aus cloudy
1,0 werden, aus rainy
2,0 und so weiter. Das Ziel selbst, Cover_Type
, ist ein kategorischer Wert, der als Wert 1 bis 7 kodiert ist.
Sei vorsichtig, wenn du ein kategoriales Merkmal als einzelnes numerisches Merkmal kodierst. Die ursprünglichen kategorischen Werte haben keine Ordnung, aber wenn sie als Zahl kodiert werden, scheint es so. Wenn du das kodierte Merkmal als Zahl behandelst, führt das zu bedeutungslosen Ergebnissen, weil der Algorithmus so tut, als ob rainy
irgendwie größer als cloudy
ist. Das ist in Ordnung, solange der numerische Wert der Kodierung nicht als Zahl verwendet wird.
Wir haben beide Arten der Codierung von kategorischen Merkmalen gesehen. Es wäre vielleicht einfacher und unkomplizierter gewesen, solche Merkmale nicht zu kodieren (und das gleich auf zwei Arten) und stattdessen einfach ihre Werte direkt anzugeben, wie z. B. "Rawah Wilderness Area". Das mag ein historisches Artefakt sein; der Datensatz wurde 1998 veröffentlicht. Aus Leistungsgründen oder um dem Format zu entsprechen, das von den damaligen Bibliotheken erwartet wurde, die eher für Regressionsprobleme entwickelt wurden, enthalten Datensätze oft auf diese Weise kodierte Daten.
Bevor du fortfährst, ist es auf jeden Fall sinnvoll, diesem Datenrahmen Spaltennamen hinzuzufügen, um die Arbeit mit ihm zu erleichtern:
from
pyspark
.
sql
.
types
import
DoubleType
from
pyspark
.
sql
.
functions
import
col
colnames
=
[
"
Elevation
"
,
"
Aspect
"
,
"
Slope
"
,
\
"
Horizontal_Distance_To_Hydrology
"
,
\
"
Vertical_Distance_To_Hydrology
"
,
"
Horizontal_Distance_To_Roadways
"
,
\
"
Hillshade_9am
"
,
"
Hillshade_Noon
"
,
"
Hillshade_3pm
"
,
\
"
Horizontal_Distance_To_Fire_Points
"
]
+
\
[
f
"
Wilderness_Area_
{
i
}
"
for
i
in
range
(
4
)
]
+
\
[
f
"
Soil_Type_
{
i
}
"
for
i
in
range
(
40
)
]
+
\
[
"
Cover_Type
"
]
data
=
data_without_header
.
toDF
(
*
colnames
)
.
\
withColumn
(
"
Cover_Type
"
,
col
(
"
Cover_Type
"
)
.
cast
(
DoubleType
(
)
)
)
data
.
head
(
)
.
.
.
Row
(
Elevation
=
2596
,
Aspect
=
51
,
Slope
=
3
,
Horizontal_Distance_To_Hydrology
=
258
,
.
.
.
)
Die Spalten, die sich auf die Wildnis und den Boden beziehen, heißen Wilderness_Area_0
, Soil_Type_0
usw., und mit ein wenig Python können diese 44 Namen generiert werden, ohne dass du sie alle abtippen musst. Schließlich wird die Zielspalte Cover_Type
im Voraus in einen double
Wert umgewandelt, da sie in allen PySpark MLlib APIs als double
und nicht als int
verwendet werden muss. Das wird später noch deutlich werden.
Du kannst data.show
aufrufen, um einige Zeilen des Datensatzes zu sehen, aber die Anzeige ist so breit, dass es schwierig sein wird, alles zu lesen. data.head
zeigt den Datensatz als rohes Row
Objekt an, was in diesem Fall besser lesbar ist.
Jetzt, wo wir mit unserem Datensatz vertraut sind und ihn verarbeitet haben, können wir ein Entscheidungsbaummodell trainieren.
Unser erster Entscheidungsbaum
In Kapitel 3 haben wir sofort ein Empfehlungsmodell aus allen verfügbaren Daten erstellt. Auf diese Weise entstand ein Empfehlungsmodell, das von jedem, der sich ein wenig mit Musik auskennt, auf seine Sinnhaftigkeit hin überprüft werden konnte: Wenn wir uns die Hörgewohnheiten und Empfehlungen eines Nutzers anschauten, bekamen wir ein Gefühl dafür, dass es gute Ergebnisse lieferte. Hier ist das nicht möglich. Wir wüssten nicht, wie wir eine 54-Merkmale-Beschreibung eines neuen Grundstücks in Colorado verfassen oder welche Art von Waldbewuchs wir auf einem solchen Grundstück erwarten könnten.
Stattdessen müssen wir direkt zu übergehen und einige Daten für die Bewertung des resultierenden Modells zurückhalten. Bisher wurde die AUC-Kennzahl verwendet, um die Übereinstimmung zwischen den zurückgehaltenen Hördaten und den Vorhersagen der Empfehlungen zu bewerten. AUC kann als die Wahrscheinlichkeit angesehen werden, dass eine zufällig ausgewählte gute Empfehlung besser abschneidet als eine zufällig ausgewählte schlechte Empfehlung. Das Prinzip ist hier dasselbe, auch wenn der Bewertungsmaßstab ein anderer ist: die Genauigkeit. Der Großteil - 90 % - der Daten wird wieder für das Training verwendet. Später werden wir sehen, dass eine Teilmenge dieses Trainingssets für die Kreuzvalidierung (das CV-Set) zurückgehalten wird. Die anderen 10 %, die hier zurückgehalten werden, sind eigentlich eine dritte Teilmenge, ein richtiges Testset.
(
train_data
,
test_data
)
=
data
.
randomSplit
([
0.9
,
0.1
])
train_data
.
cache
()
test_data
.
cache
()
Die Daten müssen etwas mehr vorbereitet werden, damit sie mit einem Klassifikator in MLlib verwendet werden können. Der eingegebene Datenrahmen enthält viele Spalten, die jeweils ein Merkmal enthalten, das zur Vorhersage der Zielspalte verwendet werden kann. In MLlib müssen alle Eingaben in einer Spalte gesammelt werden, deren Wert ein Vektor ist. Die Klasse VectorAssembler
von PySpark ist eine Abstraktion für Vektoren im Sinne der linearen Algebra und enthält nur Zahlen. Für die meisten Zwecke funktionieren sie wie ein einfaches Array von double
Werten (Gleitkommazahlen). Natürlich sind einige der Eingabemerkmale konzeptionell kategorisch, auch wenn sie in der Eingabe alle mit Zahlen dargestellt werden.
Zum Glück kann die Klasse VectorAssembler
diese Arbeit übernehmen:
from
pyspark
.
ml
.
feature
import
VectorAssembler
input_cols
=
colnames
[
:
-
1
]
vector_assembler
=
VectorAssembler
(
inputCols
=
input_cols
,
outputCol
=
"
featureVector
"
)
assembled_train_data
=
vector_assembler
.
transform
(
train_data
)
assembled_train_data
.
select
(
"
featureVector
"
)
.
show
(
truncate
=
False
)
.
.
.
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
.
.
.
|
featureVector
.
.
.
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
.
.
.
|
(
54
,
[
0
,
1
,
2
,
5
,
6
,
7
,
8
,
9
,
13
,
18
]
,
[
1874.0
,
18.0
,
14.0
,
90.0
,
208.0
,
209.0
,
.
.
.
|
(
54
,
[
0
,
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
,
13
,
18
]
,
[
1879.0
,
28.0
,
19.0
,
30.0
,
12.0
,
95.0
,
.
.
.
.
.
.
Die wichtigsten Parameter von VectorAssembler
sind die Spalten, die zu einem Feature-Vektor kombiniert werden sollen, und der Name der neuen Spalte, die den Feature-Vektor enthält. In diesem Fall werden alle Spalten - außer dem Ziel natürlich - als Eingabe-Features verwendet. Der resultierende Datenrahmen hat eine neue Spalte featureVector
, wie abgebildet.
Die Ausgabe sieht nicht genau wie eine Zahlenfolge aus, aber das liegt daran, dass es sich um eine Rohdarstellung des Vektors handelt, die als SparseVector
Instanz dargestellt wird, um Speicherplatz zu sparen. Da die meisten der 54 Werte 0 sind, werden nur Werte, die nicht Null sind, und ihre Indizes gespeichert. Dieses Detail spielt bei der Klassifizierung keine Rolle.
VectorAssembler
ist ein Beispiel für Transformer
innerhalb der aktuellen MLlib Pipelines API. Es wandelt den eingegebenen Datenrahmen in einen anderen Datenrahmen um, der auf einer bestimmten Logik basiert, und kann mit anderen Umwandlungen zu einer Pipeline zusammengesetzt werden. Später in diesem Kapitel werden diese Transformationen zu einer eigentlichen Pipeline
verbunden. Hier wird die Transformation einfach direkt aufgerufen, was ausreicht, um ein erstes Entscheidungsbaum-Klassifikatormodell zu erstellen:
from
pyspark.ml.classification
import
DecisionTreeClassifier
classifier
=
DecisionTreeClassifier
(
seed
=
1234
,
labelCol
=
"Cover_Type"
,
featuresCol
=
"featureVector"
,
predictionCol
=
"prediction"
)
model
=
classifier
.
fit
(
assembled_train_data
)
(
model
.
toDebugString
)
...
DecisionTreeClassificationModel
:
uid
=
DecisionTreeClassifier_da03f8ab5e28
,
...
If
(
feature
0
<=
3036.5
)
If
(
feature
0
<=
2546.5
)
If
(
feature
10
<=
0.5
)
If
(
feature
0
<=
2412.5
)
If
(
feature
3
<=
15.0
)
Predict
:
4.0
Else
(
feature
3
>
15.0
)
Predict
:
3.0
Else
(
feature
0
>
2412.5
)
...
Auch hier besteht die wesentliche Konfiguration für den Klassifikator aus Spaltennamen: die Spalte mit den Eingangsmerkmalsvektoren und die Spalte mit dem Zielwert, der vorhergesagt werden soll. Da das Modell später verwendet wird, um neue Werte des Ziels vorherzusagen, erhält es den Namen einer Spalte, in der die Vorhersagen gespeichert werden.
Das Drucken einer Darstellung des Modells zeigt einen Teil seiner Baumstruktur. Es besteht aus einer Reihe von verschachtelten Entscheidungen über Merkmale, bei denen die Merkmalswerte mit Schwellenwerten verglichen werden. (Aus historischen Gründen werden die Merkmale hier leider nur mit Nummern und nicht mit Namen bezeichnet).
Entscheidungsbäume sind in der Lage, die Wichtigkeit der Eingabemerkmale als Teil ihres Aufbauprozesses zu bewerten. Das heißt, sie können abschätzen, wie viel jedes Eingangsmerkmal zu einer korrekten Vorhersage beiträgt. Diese Information ist für das Modell leicht zugänglich:
import
pandas
as
pd
pd
.
DataFrame
(
model
.
featureImportances
.
toArray
(),
index
=
input_cols
,
columns
=
[
'importance'
])
.
\sort_values
(
by
=
"importance"
,
ascending
=
False
)
...
importance
Elevation
0.826854
Hillshade_Noon
0.029087
Soil_Type_1
0.028647
Soil_Type_3
0.026447
Wilderness_Area_0
0.024917
Horizontal_Distance_To_Hydrology
0.024862
Soil_Type_31
0.018573
Wilderness_Area_2
0.012458
Horizontal_Distance_To_Roadways
0.003608
Hillshade_9am
0.002840
...
Dabei werden die Wichtigkeitswerte (höher ist besser) mit den Spaltennamen verknüpft und in der Reihenfolge vom wichtigsten zum unwichtigsten Merkmal ausgedruckt. Die Höhe scheint das wichtigste Merkmal zu sein; den meisten Merkmalen wird bei der Vorhersage des Bedeckungstyps praktisch keine Bedeutung beigemessen!
Die daraus resultierende DecisionTreeClassificationModel
ist selbst ein Transformator, weil sie einen Datenrahmen, der Merkmalsvektoren enthält, in einen Datenrahmen umwandeln kann, der auch Vorhersagen enthält.
Es könnte zum Beispiel interessant sein, zu sehen, was das Modell auf den Trainingsdaten vorhersagt und seine Vorhersage mit dem bekannten korrekten Deckungstyp zu vergleichen:
predictions
=
model
.
transform
(
assembled_train_data
)
predictions
.
select
(
"Cover_Type"
,
"prediction"
,
"probability"
)
.
\show
(
10
,
truncate
=
False
)
...
+----------+----------+------------------------------------------------
...
|
Cover_Type
|
prediction
|
probability
...
+----------+----------+------------------------------------------------
...
|
6.0
|
4.0
|
[
0.0
,
0.0
,
0.028372324539571926
,
0.2936784469885515
,
...
|
6.0
|
3.0
|
[
0.0
,
0.0
,
0.024558587479935796
,
0.6454654895666132
,
...
|
6.0
|
3.0
|
[
0.0
,
0.0
,
0.024558587479935796
,
0.6454654895666132
,
...
|
6.0
|
3.0
|
[
0.0
,
0.0
,
0.024558587479935796
,
0.6454654895666132
,
...
...
Interessanterweise enthält die Ausgabe auch eine probability
Spalte, in der das Modell schätzt, wie wahrscheinlich es ist, dass jedes mögliche Ergebnis richtig ist. Daraus geht hervor, dass es in diesen Fällen ziemlich sicher ist, dass die Antwort in einigen Fällen 3 ist und ziemlich sicher, dass die Antwort nicht 1 ist.
Aufmerksamen Lesern wird auffallen, dass die Wahrscheinlichkeitsvektoren von acht Werte haben, obwohl es nur sieben mögliche Ergebnisse gibt. Die Werte des Vektors auf den Indizes 1 bis 7 enthalten die Wahrscheinlichkeiten der Ergebnisse 1 bis 7. Es gibt jedoch auch einen Wert auf dem Index 0, der immer als Wahrscheinlichkeit 0,0 angezeigt wird. Das kann ignoriert werden, da 0 nicht einmal ein gültiges Ergebnis ist, wie hier steht. Das ist eine Eigenart der Darstellung dieser Informationen als Vektor, die du beachten solltest.
Anhand des obigen Schnipsels sieht es so aus, als könnte das Modell noch etwas Arbeit vertragen. Wie bei der ALS-Implementierung in Kapitel 3 gibt es auch bei der Implementierung von DecisionTreeClassifier
mehrere Hyperparameter, für die ein Wert gewählt werden muss, und die wir hier alle auf den Standardwerten belassen haben. Hier kann die Testmenge verwendet werden, um eine unverzerrte Bewertung der erwarteten Genauigkeit eines mit diesen Standard-Hyperparametern erstellten Modells vorzunehmen.
Wir werden nun MulticlassClassificationEvaluator
verwenden, um Genauigkeit und andere Metriken zu berechnen, die die Qualität der Vorhersagen des Modells bewerten. Dies ist ein Beispiel für einen Evaluator in der MLlib, der die Qualität eines ausgegebenen Datenrahmens auf irgendeine Weise bewertet:
from
pyspark.ml.evaluation
import
MulticlassClassificationEvaluator
evaluator
=
MulticlassClassificationEvaluator
(
labelCol
=
"Cover_Type"
,
predictionCol
=
"prediction"
)
evaluator
.
setMetricName
(
"accuracy"
)
.
evaluate
(
predictions
)
evaluator
.
setMetricName
(
"f1"
)
.
evaluate
(
predictions
)
...
0.6989423087953562
0.6821216079701136
Nachdem er die Spalte mit dem "Label" (Ziel oder bekanntermaßen richtiger Ausgabewert) und den Namen der Spalte mit der Vorhersage erhalten hat, stellt er fest, dass die beiden in etwa 70 % der Fälle übereinstimmen. Das ist die Genauigkeit dieses Klassifikators. Er kann auch andere Maßzahlen berechnen, wie z. B. den F1-Score. Für unsere Zwecke hier wird die Genauigkeit verwendet, um Klassifizierer zu bewerten.
Diese einzelne Zahl gibt einen guten Überblick über die Qualität der Ergebnisse des Klassifikators. Manchmal kann es aber auch nützlich sein, sich die Konfusionsmatrix anzusehen. Das ist eine Tabelle mit einer Zeile und einer Spalte für jeden möglichen Wert des Ziels. Da es sieben Werte für die Zielkategorie gibt, handelt es sich um eine 7×7-Matrix, bei der jede Zeile einem tatsächlich richtigen Wert und jede Spalte einem vorhergesagten Wert entspricht. Der Eintrag in Zeile i und Spalte j gibt an, wie oft ein Beispiel mit der richtigen Kategorie i als Kategorie j vorhergesagt wurde. Die richtigen Vorhersagen sind also die Werte entlang der Diagonale und die Vorhersagen sind alles andere.
Es ist möglich, eine Konfusionsmatrix direkt mit der DataFrame API zu berechnen, indem du ihre allgemeineren Operatoren verwendest.
confusion_matrix
=
predictions
.
groupBy
(
"
Cover_Type
"
)
.
\
pivot
(
"
prediction
"
,
range
(
1
,
8
)
)
.
count
(
)
.
\
na
.
fill
(
0.0
)
.
\
orderBy
(
"
Cover_Type
"
)
confusion_matrix
.
show
(
)
.
.
.
+
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
+
-
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
+
-
-
-
+
-
-
-
+
-
-
-
-
-
+
|
Cover_Type
|
1
|
2
|
3
|
4
|
5
|
6
|
7
|
+
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
+
-
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
+
-
-
-
+
-
-
-
+
-
-
-
-
-
+
|
1.0
|
133792
|
51547
|
109
|
0
|
0
|
0
|
5223
|
|
2.0
|
57026
|
192260
|
4888
|
57
|
0
|
0
|
750
|
|
3.0
|
0
|
3368
|
28238
|
590
|
0
|
0
|
0
|
|
4.0
|
0
|
0
|
1493
|
956
|
0
|
0
|
0
|
|
5.0
|
0
|
8282
|
283
|
0
|
0
|
0
|
0
|
|
6.0
|
0
|
3371
|
11872
|
406
|
0
|
0
|
0
|
|
7.0
|
8122
|
74
|
0
|
0
|
0
|
0
|
10319
|
+
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
+
-
-
-
-
-
-
+
-
-
-
-
-
+
-
-
-
+
-
-
-
+
-
-
-
+
-
-
-
-
-
+
Tabellenkalkulationsbenutzer haben vielleicht erkannt, dass das Problem dem der Berechnung einer Pivot-Tabelle ähnelt. Eine Pivot-Tabelle gruppiert Werte nach zwei Dimensionen, deren Werte zu Zeilen und Spalten der Ausgabe werden, und berechnet eine Aggregation innerhalb dieser Gruppierungen, wie hier eine Zählung. Diese Funktion ist auch als PIVOT-Funktion in verschiedenen Datenbanken verfügbar und wird von Spark SQL unterstützt. Die Berechnung auf diese Weise ist eleganter und leistungsfähiger.
Auch wenn 70 % Genauigkeit anständig klingt, ist nicht sofort klar, ob sie hervorragend oder schlecht ist. Wie gut würde sich ein einfacher Ansatz eignen, um eine Grundlage zu schaffen? So wie eine kaputte Uhr zweimal am Tag richtig geht, würde auch eine zufällige Schätzung der Klassifizierung für jedes Beispiel gelegentlich die richtige Antwort ergeben.
Wir könnten einen solchen zufälligen "Klassifikator" konstruieren, indem wir eine Klasse im Verhältnis zu ihrer Häufigkeit in der Trainingsmenge zufällig auswählen. Wenn z. B. 30 % des Trainingssatzes den Deckungstyp 1 enthalten, würde der Zufallsklassifikator in 30 % der Fälle auf "1" tippen. Jede Klassifizierung würde im Verhältnis zu ihrer Häufigkeit in der Testmenge richtig sein. Wenn 40 % der Testmenge die Deckungsart 1 enthielte, würde der Zufallsklassifikator in 40 % der Fälle auf "1" tippen. Die Deckungsart 1 würde dann in 30 % x 40 % = 12 % der Fälle richtig erraten werden und 12 % zur Gesamtgenauigkeit beitragen. Daher können wir die Genauigkeit bewerten, indem wir diese Produkte der Wahrscheinlichkeiten addieren:
from
pyspark
.
sql
import
DataFrame
def
class_probabilities
(
data
)
:
total
=
data
.
count
(
)
return
data
.
groupBy
(
"
Cover_Type
"
)
.
count
(
)
.
\
orderBy
(
"
Cover_Type
"
)
.
\
select
(
col
(
"
count
"
)
.
cast
(
DoubleType
(
)
)
)
.
\
withColumn
(
"
count_proportion
"
,
col
(
"
count
"
)
/
total
)
.
\
select
(
"
count_proportion
"
)
.
collect
(
)
train_prior_probabilities
=
class_probabilities
(
train_data
)
test_prior_probabilities
=
class_probabilities
(
test_data
)
train_prior_probabilities
.
.
.
[
Row
(
count_proportion
=
0.36455357859838705
)
,
Row
(
count_proportion
=
0.4875111371136425
)
,
Row
(
count_proportion
=
0.06155716924206445
)
,
Row
(
count_proportion
=
0.00468236760696409
)
,
Row
(
count_proportion
=
0.016375858943914835
)
,
Row
(
count_proportion
=
0.029920118693908142
)
,
Row
(
count_proportion
=
0.03539976980111887
)
]
.
.
.
train_prior_probabilities
=
[
p
[
0
]
for
p
in
train_prior_probabilities
]
test_prior_probabilities
=
[
p
[
0
]
for
p
in
test_prior_probabilities
]
sum
(
[
train_p
*
cv_p
for
train_p
,
cv_p
in
zip
(
train_prior_probabilities
,
test_prior_probabilities
)
]
)
.
.
.
0.37735294664034547
Zählung nach Kategorie
Auftragszahlen nach Kategorie
Summenprodukte der Paare in Trainings- und Testsets
Das zufällige Raten erreicht dann eine Genauigkeit von 37%, was 70% doch als gutes Ergebnis erscheinen lässt. Das letztgenannte Ergebnis wurde jedoch mit Standard-Hyperparametern erzielt. Wir können es noch besser machen, wenn wir untersuchen, was die Hyperparameter tatsächlich für die Baumbildung bedeuten. Genau das werden wir im nächsten Abschnitt tun.
Entscheidungsbaum Hyperparameter
In Kapitel 3 zeigte der ALS-Algorithmus mehrere Hyperparameter auf, deren Werte wir auswählen mussten, indem wir Modelle mit verschiedenen Kombinationen von Werten erstellten und dann die Qualität jedes Ergebnisses anhand einer Metrik bewerteten. Der Prozess ist hier derselbe, nur dass die Metrik jetzt die Mehrklassengenauigkeit ist und nicht mehr der AUC. Auch die Hyperparameter, die bestimmen, wie die Entscheidungen des Baums getroffen werden, sind ganz anders: maximale Tiefe, maximale Bins, Unreinheitsmaß und minimaler Informationsgewinn.
Die maximale Tiefe begrenzt einfach die Anzahl der Ebenen im Entscheidungsbaum. Das ist die maximale Anzahl an verketteten Entscheidungen, die der Klassifikator treffen kann, um ein Beispiel zu klassifizieren. Es ist sinnvoll, diese Zahl zu begrenzen, um eine Überanpassung der Trainingsdaten zu vermeiden, wie im Beispiel der Tierhandlung gezeigt.
Der Entscheidungsbaum-Algorithmus ist dafür verantwortlich, auf jeder Ebene mögliche Entscheidungsregeln zu finden, wie die Entscheidungen weight >= 100
oder weight >= 500
im Beispiel der Tierhandlung. Die Entscheidungen haben immer die gleiche Form: Für numerische Merkmale haben die Entscheidungen die Form feature >= value
und für kategorische Merkmale die Form feature in (value1, value2, …)
. Der Satz von Entscheidungsregeln, der ausprobiert werden soll, ist also eigentlich ein Satz von Werten, die in die Entscheidungsregel eingesetzt werden. Diese werden in der PySpark MLlib-Implementierung als Bins bezeichnet. Eine größere Anzahl von Bins erfordert mehr Verarbeitungszeit, kann aber dazu führen, dass eine optimalere Entscheidungsregel gefunden wird.
Was macht eine Entscheidungsregel gut? Intuitiv würde eine gute Regel die Beispiele sinnvoll nach dem Zielkategoriewert unterscheiden. Eine Regel, die den Covtype-Datensatz in Beispiele mit nur den Kategorien 1-3 auf der einen Seite und 4-7 auf der anderen Seite unterteilt, wäre zum Beispiel hervorragend, weil sie einige Kategorien klar von anderen trennt. Eine Regel, die in etwa die gleiche Mischung aller Kategorien ergibt, wie sie im gesamten Datensatz zu finden sind, scheint nicht hilfreich zu sein. Wenn du einem der beiden Zweige einer solchen Entscheidung folgst, führt das zu etwa der gleichen Verteilung möglicher Zielwerte und bringt dich daher nicht wirklich weiter in Richtung einer sicheren Klassifizierung.
Anders ausgedrückt: Gute Regeln unterteilen die Zielwerte der Trainingsdaten in relativ homogene oder "reine" Teilmengen. Um die beste Regel auszuwählen, muss die Unreinheit der beiden Teilmengen, die sie erzeugt, minimiert werden. Es gibt zwei gebräuchliche Maßstäbe für die Unreinheit: Gini-Unreinheit und Entropie.
DieGini-Unschärfe steht in direktem Zusammenhang mit der Genauigkeit des Zufallsklassifikators. Innerhalb einer Teilmenge ist sie die Wahrscheinlichkeit, dass eine zufällig gewählte Klassifizierung eines zufällig gewählten Beispiels (jeweils gemäß der Verteilung der Klassen in der Teilmenge)falsch ist. Um diesen Wert zu berechnen, multiplizieren wir zunächst jede Klasse mit ihrem jeweiligen Anteil an allen Klassen. Dann subtrahieren wir die Summe aller Werte von 1. Wenn eine Teilmenge N Klassen hat und pi der Anteil der Beispiele der Klasse i ist, dann ergibt sich ihre Gini-Unreinheit aus der Gini-Unreinheitsgleichung:
Wenn die Teilmenge nur eine Klasse enthält, ist dieser Wert 0, weil sie völlig "rein" ist. Wenn die Teilmenge N Klassen enthält, ist dieser Wert größer als 0 und am größten, wenn die Klassen gleich oft vorkommen - also maximal unrein.
DieEntropie ist ein weiteres Maß für die Unschärfe, das aus der Informationstheorie stammt. Sie ist schwieriger zu erklären, aber sie gibt an, wie viel Unsicherheit die Sammlung der Zielwerte in der Teilmenge in Bezug auf die Vorhersagen für die Daten, die in diese Teilmenge fallen, bedeutet. Eine Teilmenge, die nur eine Klasse enthält, bedeutet, dass das Ergebnis für die Teilmenge völlig sicher ist und 0 Entropie hat - keine Unsicherheit. Eine Teilmenge, die von jeder möglichen Klasse eine enthält, bedeutet dagegen, dass die Vorhersagen für diese Teilmenge sehr unsicher sind, weil Daten mit allen möglichen Zielwerten beobachtet wurden. Dies hat eine hohe Entropie. Daher ist eine niedrige Entropie, ebenso wie eine niedrige Gini-Unsicherheit, eine gute Sache. Die Entropie wird durch die Entropie-Gleichung definiert:
Interessanterweise hat die Unsicherheit Einheiten. Da der Logarithmus der natürliche Logarithmus (zur Basis e) ist, sind die Einheiten nats, das Gegenstück zu den bekannteren Bits zur Basis e (die wir erhalten, wenn wir stattdessen log zur Basis 2 verwenden). Es geht also um die Messung von Informationen. Deshalb spricht man auch häufig vom Informationsgewinn einer Entscheidungsregel, wenn man die Entropie mit Entscheidungsbäumen verwendet.
Das eine oder das andere Maß kann eine bessere Metrik für die Auswahl von Entscheidungsregeln in einem bestimmten Datensatz sein. Sie sind sich in gewisser Weise ähnlich. Bei beiden handelt es sich um einen gewichteten Durchschnitt: eine Summe über die mit pi gewichteten Werte. Die Standardeinstellung in der PySpark-Implementierung ist die Gini-Unreinheit.
Der Mindestinformationsgewinn ist ein Hyperparameter , der einen Mindestinformationsgewinn bzw. eine Verringerung der Unschärfe für die Entscheidungsregeln vorschreibt. Regeln, die die Unreinheit der Teilmengen nicht ausreichend verbessern, werden abgelehnt. Ähnlich wie eine geringere maximale Tiefe kann dies dem Modell helfen, sich nicht zu sehr anzupassen, da Entscheidungen, die kaum zur Aufteilung des Trainingsinputs beitragen, die zukünftigen Daten möglicherweise überhaupt nicht aufteilen.
Da wir nun die relevanten Hyperparameter eines Entscheidungsbaum-Algorithmus kennen, werden wir im nächsten Abschnitt unser Modell optimieren, um seine Leistung zu verbessern.
Entscheidungsbäume abstimmen
Aus den Daten ist nicht ersichtlich, welches Verunreinigungsmaß zu einer besseren Genauigkeit führt oder welche maximale Tiefe oder Anzahl von Bins ausreicht, ohne übertrieben zu sein. Glücklicherweise ist es, wie in Kapitel 3, einfach, PySpark eine Reihe von Kombinationen dieser Werte ausprobieren zu lassen und die Ergebnisse zu melden.
Zunächst muss eine Pipeline eingerichtet werden, die die beiden Schritte aus den vorherigen Abschnitten umfasst - die Erstellung eines Merkmalsvektors und die Verwendung dieses Vektors zur Erstellung eines Entscheidungsbaummodells. Durch das Erstellen von VectorAssembler
und DecisionTreeClassifier
und das miteinander Verknüpfen dieser beiden Transformer
s entsteht ein einziges Pipeline
Objekt, das diese beiden Vorgänge als eine Operation darstellt:
from
pyspark.ml
import
Pipeline
assembler
=
VectorAssembler
(
inputCols
=
input_cols
,
outputCol
=
"featureVector"
)
classifier
=
DecisionTreeClassifier
(
seed
=
1234
,
labelCol
=
"Cover_Type"
,
featuresCol
=
"featureVector"
,
predictionCol
=
"prediction"
)
pipeline
=
Pipeline
(
stages
=
[
assembler
,
classifier
])
Natürlich können die Pipelines viel länger und komplexer sein. Einfacher geht es nicht mehr. Jetzt können wir auch die Kombinationen von Hyperparametern festlegen, die mit der integrierten Unterstützung der PySpark ML API ParamGridBuilder
getestet werden sollen. Es ist auch an der Zeit, die Bewertungsmetrik festzulegen, mit der die "besten" Hyperparameter ausgewählt werden sollen, und das ist wiederum MulticlassClassificationEvaluator
:
from
pyspark.ml.tuning
import
ParamGridBuilder
paramGrid
=
ParamGridBuilder
()
.
\addGrid
(
classifier
.
impurity
,
[
"gini"
,
"entropy"
])
.
\addGrid
(
classifier
.
maxDepth
,
[
1
,
20
])
.
\addGrid
(
classifier
.
maxBins
,
[
40
,
300
])
.
\addGrid
(
classifier
.
minInfoGain
,
[
0.0
,
0.05
])
.
\build
()
multiclassEval
=
MulticlassClassificationEvaluator
()
.
\setLabelCol
(
"Cover_Type"
)
.
\setPredictionCol
(
"prediction"
)
.
\setMetricName
(
"accuracy"
)
Das bedeutet, dass ein Modell für zwei Werte von vier Hyperparametern erstellt und ausgewertet wird. Das sind 16 Modelle. Sie werden nach der Multiklassengenauigkeit bewertet. Schließlich führt TrainValidationSplit
diese Komponenten zusammen - die Pipeline, die die Modelle erstellt, die Metriken für die Modellbewertung und die Hyperparameter, die ausprobiert werden sollen - und kann die Bewertung mit den Trainingsdaten durchführen. Es ist erwähnenswert, dass CrossValidator
hier auch verwendet werden könnte, um eine vollständige k-fache Kreuzvalidierung durchzuführen, aber das ist k-mal teurer und bringt bei großen Datenmengen nicht so viel Mehrwert. Daher wird hier TrainValidationSplit
verwendet:
from
pyspark.ml.tuning
import
TrainValidationSplit
validator
=
TrainValidationSplit
(
seed
=
1234
,
estimator
=
pipeline
,
evaluator
=
multiclassEval
,
estimatorParamMaps
=
paramGrid
,
trainRatio
=
0.9
)
validator_model
=
validator
.
fit
(
train_data
)
Dies kann je nach deiner Hardware mehrere Minuten oder mehr dauern, da viele Modelle erstellt und ausgewertet werden müssen. Beachte, dass der Parameter train ratio auf 0,9 gesetzt ist. Das bedeutet, dass die Trainingsdaten von TrainValidationSplit
in 90%/10% Teilmengen unterteilt werden. Die ersteren werden zum Trainieren jedes Modells verwendet. Die verbleibenden 10 % der Eingabedaten werden als Kreuzvalidierungsmenge für die Bewertung des Modells herangezogen. Wenn es bereits einige Daten zur Bewertung vorhält, warum halten wir dann 10% der ursprünglichen Daten als Testsatz zurück?
Wenn der Zweck des CV-Sets darin bestand, die Parameter zu bewerten, die zum Trainingsset passen, dann besteht der Zweck des Test-Sets darin, die Hyperparameter zu bewerten, die zum CV-Set "passen". Das heißt, die Testmenge gewährleistet eine unverzerrte Schätzung der Genauigkeit des endgültigen Modells und seiner Hyperparameter.
Nehmen wir an, dass das beste Modell, das auf diese Weise ausgewählt wurde, eine Genauigkeit von 90 % für den Lebenslauf aufweist. Es scheint vernünftig zu sein, zu erwarten, dass es auch bei zukünftigen Daten eine Genauigkeit von 90% aufweist. Allerdings gibt es ein Element des Zufalls bei der Erstellung dieser Modelle. Durch Zufall könnten dieses Modell und die Auswertung ungewöhnlich gut ausgefallen sein. Das beste Modell und das beste Auswertungsergebnis könnten von etwas Glück profitiert haben, so dass die Schätzung der Genauigkeit wahrscheinlich etwas optimistisch ist. Anders ausgedrückt: Auch Hyperparameter können überanpassen.
Um wirklich beurteilen zu können, wie gut dieses beste Modell bei zukünftigen Beispielen abschneiden wird, müssen wir es an Beispielen bewerten, die nicht zum Training verwendet wurden. Aber wir müssen auch Beispiele aus dem Lebenslauf vermeiden, die für die Bewertung des Modells verwendet wurden. Deshalb wurde eine dritte Teilmenge, die Testmenge, zurückgehalten.
Das Ergebnis des Validators enthält das beste Modell, das er gefunden hat. Dies ist wiederum eine Darstellung der besten gefundenen Gesamtpipeline, da wir eine Instanz einer Pipeline zur Ausführung bereitgestellt haben. Um die von DecisionTreeClassifier
ausgewählten Parameter abzufragen, muss DecisionTreeClassificationModel
manuell aus der resultierenden PipelineModel
extrahiert werden, die die letzte Stufe der Pipeline darstellt:
from
pprint
import
pprint
best_model
=
validator_model
.
bestModel
pprint
(
best_model
.
stages
[
1
]
.
extractParamMap
())
...
{
Param
(
...
name
=
'predictionCol'
,
doc
=
'prediction column name.'
):
'prediction'
,
Param
(
...
name
=
'probabilityCol'
,
doc
=
'...'
):
'probability'
,
[
...
]
Param
(
...
name
=
'impurity'
,
doc
=
'...'
):
'entropy'
,
Param
(
...
name
=
'maxDepth'
,
doc
=
'...'
):
20
,
Param
(
...
name
=
'minInfoGain'
,
doc
=
'...'
):
0.0
,
[
...
]
Param
(
...
name
=
'featuresCol'
,
doc
=
'features column name.'
):
'featureVector'
,
Param
(
...
name
=
'maxBins'
,
doc
=
'...'
):
40
,
[
...
]
Param
(
...
name
=
'labelCol'
,
doc
=
'label column name.'
):
'Cover_Type'
}
...
}
Diese Ausgabe enthält viele Informationen über das angepasste Modell, aber sie sagt uns auch, dass die Entropie offenbar am besten als Verunreinigungsmaß funktioniert hat und dass eine maximale Tiefe von 20 überraschenderweise besser war als 1. Es mag überraschen, dass das beste Modell mit nur 40 Bins angepasst wurde, aber das ist wahrscheinlich ein Zeichen dafür, dass 40 eher "ausreichend" als "besser" als 300 war. Schließlich war kein minimaler Informationsgewinn besser als ein kleines Minimum, was darauf hindeuten könnte, dass das Modell eher zu einer Unteranpassung als zu einer Überanpassung neigt.
Du fragst dich vielleicht, ob es möglich ist, die Genauigkeit zu sehen, die jedes der Modelle für jede Kombination von Hyperparametern erreicht hat. Die Hyperparameter und die Bewertungen werden unter getEstimatorParamMaps
bzw. validationMetrics
angezeigt. Sie können kombiniert werden, um alle Parameterkombinationen nach metrischen Werten sortiert anzuzeigen:
validator_model
=
validator
.
fit
(
train_data
)
metrics
=
validator_model
.
validationMetrics
params
=
validator_model
.
getEstimatorParamMaps
()
metrics_and_params
=
list
(
zip
(
metrics
,
params
))
metrics_and_params
.
sort
(
key
=
lambda
x
:
x
[
0
],
reverse
=
True
)
metrics_and_params
...
[(
0.9130409881445563
,
{
Param
(
...
name
=
'minInfoGain'
...
):
0.0
,
Param
(
...
name
=
'maxDepth'
...
):
20
,
Param
(
...
name
=
'maxBins'
...
):
40
,
Param
(
...
name
=
'impurity'
...
):
'entropy'
}),
(
0.9112655352131498
,
{
Param
(
...
name
=
'minInfoGain'
,
...
):
0.0
,
Param
(
...
name
=
'maxDepth'
...
):
20
,
Param
(
...
name
=
'maxBins'
...
):
300
,
Param
(
...
name
=
'impurity'
...
:
'entropy'
}),
...
Welche Genauigkeit hat dieses Modell im CV-Set erreicht? Und schließlich, welche Genauigkeit erreicht das Modell in der Testmenge?
metrics
.
sort
(
reverse
=
True
)
(
metrics
[
0
]
)
.
.
.
0.9130409881445563
.
.
.
multiclassEval
.
evaluate
(
best_model
.
transform
(
test_data
)
)
.
.
.
0.9138921373048084
Die Ergebnisse liegen beide bei 91%. Es kommt vor, dass die Schätzung aus dem CV-Set zu Beginn ziemlich gut war. Es ist eigentlich nicht üblich, dass die Testmenge ein ganz anderes Ergebnis liefert.
Dies ist ein interessanter Punkt, um das Problem der Überanpassung zu überdenken. Wie bereits erwähnt, ist es möglich, einen Entscheidungsbaum so tief und ausgeklügelt zu erstellen, dass er sehr gut oder perfekt zu den gegebenen Trainingsbeispielen passt, aber bei der Verallgemeinerung auf andere Beispiele fehlschlägt, weil er die Eigenheiten und das Rauschen der Trainingsdaten zu gut berücksichtigt hat. Dieses Problem tritt bei den meisten Algorithmen für maschinelles Lernen auf, nicht nur bei Entscheidungsbäumen.
Wenn ein Entscheidungsbaum überangepasst ist, weist er eine hohe Genauigkeit auf, wenn er auf denselben Trainingsdaten läuft, auf die er das Modell angepasst hat, aber eine geringe Genauigkeit bei anderen Beispielen. In diesem Fall lag die Genauigkeit des endgültigen Modells bei etwa 91 % für andere, neue Beispiele. Die Genauigkeit kann genauso gut mit denselben Daten bewertet werden, mit denen das Modell trainiert wurde: trainData
. Dies ergibt eine Genauigkeit von etwa 95%. Der Unterschied ist nicht groß, deutet aber darauf hin, dass der Entscheidungsbaum die Trainingsdaten bis zu einem gewissen Grad übererfüllt hat. Eine geringere maximale Tiefe wäre vielleicht die bessere Wahl.
Bisher haben wir alle Eingabemerkmale, auch die kategorischen, implizit so behandelt, als wären sie numerisch. Können wir die Leistung unseres Modells weiter verbessern, indem wir kategoriale Merkmale als genau das behandeln? Das werden wir als Nächstes untersuchen.
Kategorische Merkmale - neu betrachtet
Die kategorischen Merkmale in unserem Datensatz werden als mehrere binäre 0/1-Werte kodiert. Diese einzelnen Merkmale als numerisch zu behandeln, hat sich als richtig erwiesen, denn jede Entscheidungsregel für die "numerischen" Merkmale wählt Schwellenwerte zwischen 0 und 1, die alle gleichwertig sind, da alle Werte 0 oder 1 sind.
Natürlich zwingt diese Kodierung den Entscheidungsbaum-Algorithmus dazu, die Werte der zugrunde liegenden kategorialen Merkmale einzeln zu betrachten. Da Merkmale wie die Bodenart in viele Merkmale unterteilt sind und Entscheidungsbäume die Merkmale einzeln behandeln, ist es schwieriger, Informationen über verwandte Bodenarten in Beziehung zu setzen.
So gehören zum Beispiel neun verschiedene Bodentypen zur Leighton-Familie, und sie können auf eine Weise miteinander verbunden sein, die der Entscheidungsbaum ausnutzen kann. Wenn die Bodenart als ein einziges kategorisches Merkmal mit 40 Bodenwerten kodiert würde, könnte der Baum Regeln wie "wenn die Bodenart zu den neun Typen der Leighton-Familie gehört" direkt ausdrücken. Bei 40 Merkmalen müsste der Baum jedoch eine Abfolge von neun Entscheidungen über die Bodenart lernen, um das Gleiche zu tun.
Wenn jedoch 40 numerische Merkmale ein kategoriales Merkmal mit 40 Werten repräsentieren, erhöht sich der Speicherbedarf und die Geschwindigkeit steigt.
Wie wäre es, wenn du die Kodierung mit einem Punkt rückgängig machen würdest? Das würde zum Beispiel die vier Spalten, die den Wildnistyp kodieren, durch eine Spalte ersetzen, die den Wildnistyp als eine Zahl zwischen 0 und 3 kodiert, wie Cover_Type
:
def
unencode_one_hot
(
data
)
:
wilderness_cols
=
[
'
Wilderness_Area_
'
+
str
(
i
)
for
i
in
range
(
4
)
]
wilderness_assembler
=
VectorAssembler
(
)
.
\
setInputCols
(
wilderness_cols
)
.
\
setOutputCol
(
"
wilderness
"
)
unhot_udf
=
udf
(
lambda
v
:
v
.
toArray
(
)
.
tolist
(
)
.
index
(
1
)
)
with_wilderness
=
wilderness_assembler
.
transform
(
data
)
.
\
drop
(
*
wilderness_cols
)
.
\
withColumn
(
"
wilderness
"
,
unhot_udf
(
col
(
"
wilderness
"
)
)
)
soil_cols
=
[
'
Soil_Type_
'
+
str
(
i
)
for
i
in
range
(
40
)
]
soil_assembler
=
VectorAssembler
(
)
.
\
setInputCols
(
soil_cols
)
.
\
setOutputCol
(
"
soil
"
)
with_soil
=
soil_assembler
.
\
transform
(
with_wilderness
)
.
\
drop
(
*
soil_cols
)
.
\
withColumn
(
"
soil
"
,
unhot_udf
(
col
(
"
soil
"
)
)
)
return
with_soil
Hier wird VectorAssembler
eingesetzt, um die Spalten 4 und 40 für die Wildnis und die Bodenart zu zwei Spalten Vector
zusammenzufassen. Die Werte in diesen Vector
s sind alle 0, mit Ausnahme einer Stelle, die eine 1 hat. Es gibt keine einfache Datenrahmenfunktion dafür, also müssen wir unsere eigene UDF definieren, mit der wir die Spalten bearbeiten können. Dadurch werden die beiden neuen Spalten zu Zahlen, die genau den Typ haben, den wir brauchen.
Jetzt können wir unseren Datensatz umwandeln, indem wir die One-Hot-Codierung mit unserer oben definierten Funktion entfernen:
unenc_train_data
=
unencode_one_hot
(
train_data
)
unenc_train_data
.
printSchema
()
...
root
|--
Elevation
:
integer
(
nullable
=
true
)
|--
Aspect
:
integer
(
nullable
=
true
)
|--
Slope
:
integer
(
nullable
=
true
)
|--
Horizontal_Distance_To_Hydrology
:
integer
(
nullable
=
true
)
|--
Vertical_Distance_To_Hydrology
:
integer
(
nullable
=
true
)
|--
Horizontal_Distance_To_Roadways
:
integer
(
nullable
=
true
)
|--
Hillshade_9am
:
integer
(
nullable
=
true
)
|--
Hillshade_Noon
:
integer
(
nullable
=
true
)
|--
Hillshade_3pm
:
integer
(
nullable
=
true
)
|--
Horizontal_Distance_To_Fire_Points
:
integer
(
nullable
=
true
)
|--
Cover_Type
:
double
(
nullable
=
true
)
|--
wilderness
:
string
(
nullable
=
true
)
|--
soil
:
string
(
nullable
=
true
)
...
unenc_train_data
.
groupBy
(
'wilderness'
)
.
count
()
.
show
()
...
+----------+------+
|
wilderness
|
count
|
+----------+------+
|
3
|
33271
|
|
0
|
234532
|
|
1
|
26917
|
|
2
|
228144
|
+----------+------+
Von hier aus kann fast das gleiche Verfahren wie oben verwendet werden, um die Hyperparameter eines auf diesen Daten aufgebauten Entscheidungsbaummodells abzustimmen und das beste Modell auszuwählen und zu bewerten. Es gibt jedoch einen wichtigen Unterschied. Die beiden neuen numerischen Spalten haben nichts an sich, was darauf hindeutet, dass sie eigentlich kategoriale Werte kodieren. Sie als Zahlen zu behandeln, ist nicht korrekt, da ihre Reihenfolge bedeutungslos ist. Das Modell wird trotzdem erstellt, aber da einige Informationen in diesen Merkmalen nicht verfügbar sind, kann die Genauigkeit darunter leiden.
MLlib kann intern zusätzliche Metadaten zu jeder Spalte speichern. Die Details dieser Daten sind in der Regel vor dem Aufrufer verborgen, aber sie enthalten Informationen wie z. B., ob die Spalte einen kategorischen Wert kodiert und wie viele unterschiedliche Werte sie annimmt. Um diese Metadaten hinzuzufügen, müssen die Daten durch VectorIndexer
geleitet werden. Ihre Aufgabe ist es, die Eingaben in richtig beschriftete kategoriale Merkmalsspalten umzuwandeln. Obwohl wir bereits einen Großteil der Arbeit erledigt haben, um die kategorischen Merkmale in 0-indizierte Werte umzuwandeln, kümmert sich VectorIndexer
um die Metadaten.
Wir müssen diese Stufe in die Pipeline
aufnehmen:
from
pyspark
.
ml
.
feature
import
VectorIndexer
cols
=
unenc_train_data
.
columns
inputCols
=
[
c
for
c
in
cols
if
c
!=
'
Cover_Type
'
]
assembler
=
VectorAssembler
(
)
.
setInputCols
(
inputCols
)
.
setOutputCol
(
"
featureVector
"
)
indexer
=
VectorIndexer
(
)
.
\
setMaxCategories
(
40
)
.
\
setInputCol
(
"
featureVector
"
)
.
setOutputCol
(
"
indexedVector
"
)
classifier
=
DecisionTreeClassifier
(
)
.
setLabelCol
(
"
Cover_Type
"
)
.
\
setFeaturesCol
(
"
indexedVector
"
)
.
\
setPredictionCol
(
"
prediction
"
)
pipeline
=
Pipeline
(
)
.
setStages
(
[
assembler
,
indexer
,
classifier
]
)
Der Ansatz geht davon aus, dass die Trainingsmenge alle möglichen Werte jedes der kategorialen Merkmale mindestens einmal enthält. Das heißt, er funktioniert nur dann richtig, wenn alle 4 Bodenwerte und alle 40 Wildniswerte in der Trainingsmenge vorkommen, sodass alle möglichen Werte eine Zuordnung erhalten. In diesem Fall trifft das zu, aber bei kleinen Trainingsdatensätzen, in denen einige Bezeichnungen sehr selten vorkommen, ist das möglicherweise nicht der Fall. In diesen Fällen könnte es notwendig sein, eine VectorIndexerModel
mit der vollständigen Wertezuordnung manuell zu erstellen und hinzuzufügen.
Abgesehen davon ist der Prozess derselbe wie zuvor. Du solltest feststellen, dass er ein ähnliches bestes Modell gewählt hat, aber dass die Genauigkeit auf der Testmenge etwa 93 % beträgt. Dadurch, dass die kategorischen Merkmale in den vorherigen Abschnitten als tatsächliche kategorische Merkmale behandelt wurden, verbesserte der Klassifikator seine Genauigkeit um fast 2 %.
Wir haben einen Entscheidungsbaum trainiert und abgestimmt. Jetzt werden wir zu Random Forests übergehen, einem leistungsfähigeren Algorithmus. Wie wir im nächsten Abschnitt sehen werden, ist die Implementierung mit PySpark an dieser Stelle überraschend einfach.
Zufallsforsten
Wenn du den Codebeispielen auf gefolgt bist, hast du vielleicht bemerkt, dass deine Ergebnisse leicht von denen in den Codelisten des Buches abweichen. Das liegt daran, dass bei der Erstellung von Entscheidungsbäumen ein Zufallselement vorhanden ist, das ins Spiel kommt, wenn du entscheidest, welche Daten du verwendest und welche Entscheidungsregeln du untersuchen willst.
Der Algorithmus berücksichtigt nicht jede mögliche Entscheidungsregel auf jeder Ebene. Das würde unglaublich viel Zeit in Anspruch nehmen. Für ein kategorisches Merkmal mit N Werten gibt es 2N-2mögliche Entscheidungsregeln (jede Teilmenge außer der leeren Menge und der gesamten Menge). Selbst bei einer mäßig großen Zahl von N gäbe es Milliarden von möglichen Entscheidungsregeln.
Stattdessen verwenden Entscheidungsbäume mehrere Heuristiken, um zu bestimmen, welche wenigen Regeln tatsächlich berücksichtigt werden sollen. Bei der Auswahl der Regeln kommt auch ein gewisser Zufall zum Tragen: Jedes Mal werden nur ein paar zufällig ausgewählte Merkmale betrachtet und nur Werte aus einer zufälligen Teilmenge der Trainingsdaten. Das bedeutet, dass der Entscheidungsbaum-Algorithmus nicht jedes Mal denselben Baum erstellt. Das ist eine gute Sache.
Das ist aus demselben Grund gut, aus dem die "Weisheit der Masse" in der Regel besser ist als individuelle Vorhersagen. Zur Veranschaulichung ein kleines Quiz: Wie viele schwarze Taxis gibt es in London?
Gucke nicht auf die Antwort, sondern rate zuerst.
Ich habe 10.000 geschätzt, was weit von der richtigen Antwort von etwa 19.000 entfernt ist. Da ich mich verschätzt habe, ist die Wahrscheinlichkeit größer, dass du dich verschätzt hast, und deshalb ist der Durchschnitt unserer Antworten tendenziell genauer. Da ist sie wieder, die Regression zum Mittelwert. Bei einer informellen Umfrage unter 13 Personen im Büro lag der Durchschnitt tatsächlich näher: 11.170.
Der Schlüssel zu diesem Effekt ist, dass die Vermutungen unabhängig voneinander waren und sich nicht gegenseitig beeinflusst haben. (Du hast doch nicht etwa geguckt, oder?) Die Übung wäre nutzlos, wenn wir uns alle auf die gleiche Methode geeinigt hätten, um eine Vermutung abzugeben, denn dann hätten wir alle die gleiche Antwort bekommen - die gleiche, möglicherweise falsche Antwort. Es wäre sogar anders und schlimmer gewesen, wenn ich dich nur beeinflusst hätte, indem ich meine Vermutung im Voraus mitgeteilt hätte.
Es wäre toll, nicht nur einen Baum, sondern viele Bäume zu haben, die alle vernünftige, aber unterschiedliche und unabhängige Schätzungen des richtigen Zielwerts abgeben. Ihre gemeinsame durchschnittliche Vorhersage sollte näher an der wahren Antwort liegen als die eines einzelnen Baums. Diese Unabhängigkeit wird durch die Zufälligkeit des Bauprozesses erreicht. Das ist der Schlüssel zu Random Forests.
Der Zufall wird durch den Aufbau vieler Bäume erzeugt, von denen jeder eine andere zufällige Teilmenge der Daten und sogar der Merkmale sieht. Das macht den Wald als Ganzes weniger anfällig für Overfitting. Wenn ein bestimmtes Merkmal verrauschte Daten enthält oder nur in der Trainingsmenge eine trügerische Vorhersagekraft hat, werden die meisten Bäume dieses Problemmerkmal die meiste Zeit nicht berücksichtigen. Die meisten Bäume passen sich dem Rauschen nicht an und neigen dazu, die Bäume zu überstimmen, die sich dem Rauschen im Wald angepasst haben.
Die Vorhersage eines Random Forest ist einfach ein gewichteter Durchschnitt der Vorhersagen der Bäume. Bei einem kategorialen Ziel kann dies ein Mehrheitsvotum oder der wahrscheinlichste Wert sein, der auf dem Durchschnitt der von den Bäumen ermittelten Wahrscheinlichkeiten basiert. Random Forests unterstützen wie Entscheidungsbäume auch die Regression, und die Vorhersage des Forests ist in diesem Fall der Durchschnitt der von jedem Baum vorhergesagten Zahlen.
Zufallswälder sind zwar eine leistungsfähigere und komplexere Klassifizierungstechnik, aber die gute Nachricht ist, dass es praktisch keinen Unterschied macht, sie in der in diesem Kapitel entwickelten Pipeline zu verwenden. Füge einfach eine RandomForestClassifier
anstelle von DecisionTreeClassifier
ein und fahre fort wie zuvor. Es gibt wirklich keinen weiteren Code oder eine API, die du verstehen musst, um sie zu nutzen:
from
pyspark.ml.classification
import
RandomForestClassifier
classifier
=
RandomForestClassifier
(
seed
=
1234
,
labelCol
=
"Cover_Type"
,
featuresCol
=
"indexedVector"
,
predictionCol
=
"prediction"
)
Beachte, dass dieser Klassifikator einen weiteren Hyperparameter hat: die Anzahl der zu bildenden Bäume. Wie bei dem Hyperparameter max bins sollten höhere Werte bis zu einem gewissen Grad bessere Ergebnisse liefern. Der Preis dafür ist jedoch, dass der Aufbau vieler Bäume um ein Vielfaches länger dauert als der Aufbau eines Baumes.
Die Genauigkeit des besten Random-Forest-Modells, das aus einem ähnlichen Tuning-Prozess hervorgegangen ist, liegt auf Anhieb bei 95 % - das sind bereits etwa 2 % mehr. Anders ausgedrückt: Die Fehlerquote des besten Entscheidungsbaums, der zuvor erstellt wurde, ist um 28 % gesunken, von 7 % auf 5 %. Mit weiteren Optimierungen kannst du noch mehr erreichen.
An diesem Punkt haben wir übrigens ein zuverlässigeres Bild von der Bedeutung der Merkmale:
forest_model
=
best_model
.
stages
[
1
]
feature_importance_list
=
list
(
zip
(
input_cols
,
forest_model
.
featureImportances
.
toArray
()))
feature_importance_list
.
sort
(
key
=
lambda
x
:
x
[
1
],
reverse
=
True
)
pprint
(
feature_importance_list
)
...
(
0.28877055118903183
,
Elevation
)
(
0.17288279582959612
,
soil
)
(
0.12105056811661499
,
Horizontal_Distance_To_Roadways
)
(
0.1121550648692802
,
Horizontal_Distance_To_Fire_Points
)
(
0.08805270405239551
,
wilderness
)
(
0.04467393191338021
,
Vertical_Distance_To_Hydrology
)
(
0.04293099150373547
,
Horizontal_Distance_To_Hydrology
)
(
0.03149644050848614
,
Hillshade_Noon
)
(
0.028408483578137605
,
Hillshade_9am
)
(
0.027185325937200706
,
Aspect
)
(
0.027075578474331806
,
Hillshade_3pm
)
(
0.015317564027809389
,
Slope
)
Random Forests sind im Kontext von Big Data attraktiv, weil Bäume unabhängig voneinander aufgebaut werden sollen. und Big-Data-Technologien wie Spark und MapReduce benötigen von Natur aus datenparallele Probleme, bei denen Teile der Gesamtlösung unabhängig voneinander auf Teilen der Daten berechnet werden können. Die Tatsache, dass Bäume nur auf einer Teilmenge von Merkmalen oder Eingabedaten trainieren können und sollten, macht die Parallelisierung der Baumerstellung trivial.
Vorhersagen treffen
Die Erstellung eines Klassifizierers ist zwar ein interessanter und differenzierter Prozess, aber nicht das Endziel. Das Ziel ist es, Vorhersagen zu treffen. Das ist das Ziel, und es ist vergleichsweise einfach.
Das daraus resultierende "beste Modell" ist eigentlich eine ganze Pipeline von Operationen. Sie umfasst, wie die Eingaben für das Modell umgewandelt werden, und beinhaltet das Modell selbst, das Vorhersagen machen kann. Es kann mit einem Datenrahmen mit neuen Eingaben arbeiten. Der einzige Unterschied zu dem data
Datenrahmen, mit dem wir begonnen haben, ist, dass die Spalte Cover_Type
fehlt. Wenn wir Vorhersagen machen - vor allem über die Zukunft, sagt Herr Bohr - ist die Ausgabe natürlich nicht bekannt.
Um das zu beweisen, kannst du versuchen, die Cover_Type
aus den eingegebenen Testdaten zu entfernen und eine Vorhersage zu erhalten:
unenc_test_data
=
unencode_one_hot
(
test_data
)
bestModel
.
transform
(
unenc_test_data
.
drop
(
"Cover_Type"
))
.
\select
(
"prediction"
)
.
show
()
...
+----------+
|
prediction
|
+----------+
|
6.0
|
+----------+
Das Ergebnis sollte 6,0 sein, was der Klasse 7 (das ursprüngliche Merkmal war 1-indiziert) im ursprünglichen Covtype-Datensatz entspricht. Der vorhergesagte Bedeckungstyp für das in diesem Beispiel beschriebene Land ist Krummholz.
Wie geht es weiter?
In diesem Kapitel wurden zwei verwandte und wichtige Arten des maschinellen Lernens vorgestellt, nämlich Klassifizierung und Regression, sowie einige grundlegende Konzepte zum Aufbau und zur Abstimmung von Modellen: Merkmale, Vektoren, Training und Kreuzvalidierung. Anhand des Covtype-Datensatzes wurde demonstriert, wie man mit Entscheidungsbäumen und -wäldern, die in PySpark implementiert wurden, eine Art der Waldbedeckung anhand von Faktoren wie Standort und Bodentyp vorhersagen kann.
Wie bei den Empfehlungsprogrammen in Kapitel 3 könnte es sinnvoll sein, die Auswirkungen von Hyperparametern auf die Genauigkeit weiter zu untersuchen. Bei den meisten Entscheidungsbaum-Hyperparametern wird Zeit gegen Genauigkeit eingetauscht: Mehr Bins und Bäume führen in der Regel zu einer besseren Genauigkeit, aber es gibt einen Punkt, an dem der Ertrag abnimmt.
Der Klassifikator hat sich hier als sehr genau erwiesen. Es ist ungewöhnlich, eine Genauigkeit von mehr als 95 % zu erreichen. In der Regel kannst du die Genauigkeit weiter verbessern, indem du weitere Merkmale einbeziehst oder bestehende Merkmale in eine aussagekräftigere Form umwandelst. Dies ist ein üblicher, wiederholter Schritt bei der iterativen Verbesserung eines Klassifizierungsmodells. Für diesen Datensatz könnte man zum Beispiel aus den beiden Merkmalen, die den horizontalen und vertikalen Abstand zur Wasseroberfläche kodieren, ein drittes Merkmal erstellen: das Merkmal für den geradlinigen Abstand zur Wasseroberfläche. Dieses könnte sich als nützlicher erweisen als die beiden ursprünglichen Merkmale. Wenn es möglich wäre, mehr Daten zu sammeln, könnten wir auch versuchen, neue Informationen wie die Bodenfeuchtigkeit hinzuzufügen, um die Klassifizierung zu verbessern.
Natürlich sind nicht alle Vorhersageprobleme in der realen Welt genau wie der Covtype-Datensatz. Manche Probleme erfordern zum Beispiel die Vorhersage eines kontinuierlichen numerischen Wertes und nicht eines kategorialen Wertes. Für diese Art von Regressionsproblemen gilt ein Großteil der Analyse und des Codes; in diesem Fall ist die Klasse RandomForestRegressor
von Nutzen.
Außerdem sind Entscheidungsbäume und Forests nicht die einzigen Klassifizierungs- oder Regressionsalgorithmen und auch nicht die einzigen, die in PySpark implementiert sind. Jeder Algorithmus arbeitet ganz anders als Entscheidungsbäume und Forests. Viele Elemente sind jedoch gleich: Sie werden in einen Pipeline
eingebunden und arbeiten mit Spalten in einem Datenrahmen und haben Hyperparameter, die du mithilfe von Trainings-, Kreuzvalidierungs- und Testuntermengen der Eingabedaten auswählen musst. Die gleichen allgemeinen Prinzipien können mit diesen anderen Algorithmen auch für die Modellierung von Klassifizierungs- und Regressionsproblemen eingesetzt werden.
Dies waren Beispiele für überwachtes Lernen. Was passiert, wenn einige oder alle Zielwerte unbekannt sind? Im folgenden Kapitel wird untersucht, was in dieser Situation getan werden kann.
Get Erweiterte Analytik mit PySpark 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.