Grenzen des Maschinellen Lernens¶
Das Maschinelle Lernen stößt an bestimmten Stellen an seine Grenzen. Für die meisten Business-Cases wird mithilfe Maschinellen Lernens nicht-lineare Zusammenhänge in strukturierten Datensätzen gesucht. Diese Zusammenhänge liegen in der Praxis aber gar nicht zwangsläufig vor. Dies hält aber manche Praktiker nicht davon ab, die Auswertung der eigenen Daten immer wieder zu verändert, bis auf einmal das Modell zu funktionieren scheint. Dies wird P-Hacking genannt (der Begriff stammt eigentlich aus der Statistik und hatte ursprünglich eine leicht andere Bedeutung). Der Begriff bedeutet, dass das lang erwünschte Ergebnis leider rein zufällig zustande kam. Dies tritt vor allem bei kleinen Datensätzen auf.
Um besonders gute Genauigkeitswerte für Modell des Maschinellen Lernens zu erhalten, können z. B. Scheinkorrelationen (d.h. lineare zufällige Zusammenhänge) oder andere nicht-lineare zufällige Zusammenhänge „ausgenutzt“ werden. Wenn genügend Attribute gleichzeitig betrachtet werden, gibt es mit hoher Wahrscheinlichkeit ein Attribut, welches einen Zusammenhang mit der Zielvariable aufweist. Deswegen sollte immer sehr kritisch betrachtet werden, aus welchen Daten welches Ergebnis angeblich vorhersagbar sein soll.
So ein Verhalten ist ein Problem für alle Bereiche: In der Wissenschaft werden unter Umständen falsche Annahmen und Vorhersagemodelle verwendet. In der Praxis bedeutet es, dass ggf. defekte Vorhersagemodelle in den Betrieb aufgenommen werden. Sobald sich jemand auf die Vorhersagen des falschen Modells verlässt, kann dies zu ernsthaften Schäden an Menschen oder Umwelt führen. Ebenso sind finanzielle Schäden nicht ausgeschlossen.
[1]:
import random
import pandas as pd
import sklearn.tree
/tmp/ipykernel_2188/3939139406.py:2: DeprecationWarning:
Pyarrow will become a required dependency of pandas in the next major release of pandas (pandas 3.0),
(to allow more performant data types, such as the Arrow string type, and better interoperability with other libraries)
but was not found to be installed on your system.
If this would cause problems for you,
please provide us feedback at https://github.com/pandas-dev/pandas/issues/54466
import pandas as pd
Es wird zunächst ein Datensatz mit zufälligen Attribute erzeugt. Dies könnte z. B. die Zahlenrepräsentation von nominalskalierten Attributen sein.
[2]:
# Fixiere Zufallsgenerator für random.choice
random.seed(0)
number_rows = 50
df = pd.DataFrame({
"Outlook": [random.choice(["Sunny", "Overcast", "Rain"]) for _ in range(number_rows)],
"Temperature": [random.choice(["Hot", "Mild", "Cool"]) for _ in range(number_rows)],
"Humidity": [random.choice(["High", "Normal"]) for _ in range(number_rows)],
"Wind": [random.choice(["Weak", "Strong"]) for _ in range(number_rows)],
"Play Tennis?": [random.choice(["Yes", "No"]) for _ in range(number_rows)],
})
df
[2]:
Outlook | Temperature | Humidity | Wind | Play Tennis? | |
---|---|---|---|---|---|
0 | Overcast | Cool | Normal | Weak | No |
1 | Overcast | Cool | Normal | Strong | No |
2 | Sunny | Hot | High | Weak | No |
3 | Overcast | Cool | High | Weak | No |
4 | Rain | Mild | High | Weak | Yes |
5 | Overcast | Mild | High | Strong | No |
6 | Overcast | Hot | High | Weak | No |
7 | Overcast | Cool | High | Strong | No |
8 | Overcast | Mild | Normal | Weak | Yes |
9 | Overcast | Cool | Normal | Weak | Yes |
10 | Rain | Hot | High | Weak | Yes |
11 | Sunny | Hot | High | Strong | No |
12 | Rain | Cool | Normal | Strong | Yes |
13 | Sunny | Hot | Normal | Strong | Yes |
14 | Overcast | Hot | Normal | Weak | Yes |
15 | Sunny | Hot | Normal | Weak | No |
16 | Sunny | Cool | Normal | Strong | No |
17 | Rain | Mild | High | Weak | No |
18 | Overcast | Hot | Normal | Weak | Yes |
19 | Rain | Hot | High | Strong | No |
20 | Rain | Mild | Normal | Weak | No |
21 | Rain | Cool | Normal | Strong | Yes |
22 | Sunny | Mild | High | Strong | Yes |
23 | Overcast | Hot | High | Strong | No |
24 | Sunny | Mild | High | Weak | Yes |
25 | Rain | Cool | Normal | Weak | No |
26 | Sunny | Mild | High | Weak | Yes |
27 | Rain | Cool | High | Weak | No |
28 | Overcast | Hot | Normal | Weak | No |
29 | Overcast | Cool | High | Strong | Yes |
30 | Rain | Mild | Normal | Strong | Yes |
31 | Sunny | Cool | Normal | Weak | No |
32 | Overcast | Hot | High | Strong | No |
33 | Overcast | Cool | High | Weak | No |
34 | Overcast | Cool | High | Weak | No |
35 | Rain | Cool | High | Strong | Yes |
36 | Rain | Mild | High | Strong | No |
37 | Sunny | Mild | High | Strong | Yes |
38 | Rain | Hot | High | Strong | Yes |
39 | Overcast | Cool | High | Strong | Yes |
40 | Overcast | Mild | High | Strong | Yes |
41 | Rain | Mild | High | Weak | No |
42 | Overcast | Cool | Normal | Weak | No |
43 | Sunny | Hot | High | Strong | No |
44 | Rain | Mild | Normal | Weak | Yes |
45 | Sunny | Hot | High | Strong | Yes |
46 | Sunny | Hot | High | Weak | Yes |
47 | Rain | Hot | High | Strong | Yes |
48 | Overcast | Hot | High | Weak | Yes |
49 | Rain | Cool | High | Weak | Yes |
[3]:
df_cat = df.assign(**{
col: df[col].astype('category').cat.codes for col in ["Humidity", "Wind", "Play Tennis?"]
})
df_cat = df_cat.assign(
Outlook=df["Outlook"].astype(
pd.CategoricalDtype(categories=["Rain", "Overcast", "Sunny"], ordered=True)).cat.codes,
Temperature=df["Temperature"].astype(
pd.CategoricalDtype(categories=["Cool", "Mild", "Hot"], ordered=True)).cat.codes
)
df_cat
[3]:
Outlook | Temperature | Humidity | Wind | Play Tennis? | |
---|---|---|---|---|---|
0 | 1 | 0 | 1 | 1 | 0 |
1 | 1 | 0 | 1 | 0 | 0 |
2 | 2 | 2 | 0 | 1 | 0 |
3 | 1 | 0 | 0 | 1 | 0 |
4 | 0 | 1 | 0 | 1 | 1 |
5 | 1 | 1 | 0 | 0 | 0 |
6 | 1 | 2 | 0 | 1 | 0 |
7 | 1 | 0 | 0 | 0 | 0 |
8 | 1 | 1 | 1 | 1 | 1 |
9 | 1 | 0 | 1 | 1 | 1 |
10 | 0 | 2 | 0 | 1 | 1 |
11 | 2 | 2 | 0 | 0 | 0 |
12 | 0 | 0 | 1 | 0 | 1 |
13 | 2 | 2 | 1 | 0 | 1 |
14 | 1 | 2 | 1 | 1 | 1 |
15 | 2 | 2 | 1 | 1 | 0 |
16 | 2 | 0 | 1 | 0 | 0 |
17 | 0 | 1 | 0 | 1 | 0 |
18 | 1 | 2 | 1 | 1 | 1 |
19 | 0 | 2 | 0 | 0 | 0 |
20 | 0 | 1 | 1 | 1 | 0 |
21 | 0 | 0 | 1 | 0 | 1 |
22 | 2 | 1 | 0 | 0 | 1 |
23 | 1 | 2 | 0 | 0 | 0 |
24 | 2 | 1 | 0 | 1 | 1 |
25 | 0 | 0 | 1 | 1 | 0 |
26 | 2 | 1 | 0 | 1 | 1 |
27 | 0 | 0 | 0 | 1 | 0 |
28 | 1 | 2 | 1 | 1 | 0 |
29 | 1 | 0 | 0 | 0 | 1 |
30 | 0 | 1 | 1 | 0 | 1 |
31 | 2 | 0 | 1 | 1 | 0 |
32 | 1 | 2 | 0 | 0 | 0 |
33 | 1 | 0 | 0 | 1 | 0 |
34 | 1 | 0 | 0 | 1 | 0 |
35 | 0 | 0 | 0 | 0 | 1 |
36 | 0 | 1 | 0 | 0 | 0 |
37 | 2 | 1 | 0 | 0 | 1 |
38 | 0 | 2 | 0 | 0 | 1 |
39 | 1 | 0 | 0 | 0 | 1 |
40 | 1 | 1 | 0 | 0 | 1 |
41 | 0 | 1 | 0 | 1 | 0 |
42 | 1 | 0 | 1 | 1 | 0 |
43 | 2 | 2 | 0 | 0 | 0 |
44 | 0 | 1 | 1 | 1 | 1 |
45 | 2 | 2 | 0 | 0 | 1 |
46 | 2 | 2 | 0 | 1 | 1 |
47 | 0 | 2 | 0 | 0 | 1 |
48 | 1 | 2 | 0 | 1 | 1 |
49 | 0 | 0 | 0 | 1 | 1 |
Nun lass uns betrachten, ob der Entscheidungsbaum einen zufälligen Zusammenhang findet, und das Kreuzvalidierungsergebnis dennoch gut aussieht.
[4]:
dt = sklearn.tree.DecisionTreeClassifier(random_state=0)
eingabe = df_cat[list(set(df_cat.columns) - set(["Play Tennis?"]))]
ziel = df["Play Tennis?"]
X_train, X_test, y_train, y_test = sklearn.model_selection.train_test_split(eingabe, ziel, random_state=42)
dt.fit(X_train, y_train)
dt.score(X_test, y_test)
[4]:
0.46153846153846156
Aufgabe 1
Interpretieren Sie den Zahlenwert. Liegt hier ein zufälliger nicht-linearer Zusammenhang vor? Ist dieses Ergebnis plausibel? Warum (nicht)?
Antwort: …
Feature Engineering falsch gemacht¶
Normalerweise hilft Feature Engineering dabei, zielführende Zahlenwerte zu generieren. So ist es z. B. sinnvoll, statt einem Start- und einem Endzeitpunkt lieber gleich die Zeitspanne zu generieren, oder statt einer Länge und einer Breite gleich die Fläche zu berechnen. Es gibt aber auch Fälle, in denen Projektverantwortliche beliebige Attribute addiert, multipliziert etc. haben, ohne dass es dafür eine inhaltliche Rechtfertigung gab. Genau das werden wir jetzt auf die Spitze treiben.
Nun werden wir so lange neue Attribute erstellen, bis wir (mindestens) eines finden, mit dem der Entscheidungsbaum zu einem guten Ergebnis kommt.
[5]:
new_columns = {}
for column_A in list(set(df_cat.columns) - set(["Play Tennis?"])):
for column_B in list(set(df_cat.columns) - set(["Play Tennis?"])):
if column_A == column_B:
continue
addition = df_cat[column_A] + df_cat[column_B]
addition.name = f"{column_A} + {column_B}"
new_columns.update({addition.name: addition})
subtraktion = df_cat[column_A] - df_cat[column_B]
subtraktion.name = f"{column_A} - {column_B}"
new_columns.update({subtraktion.name: subtraktion})
multiplication = df_cat[column_A] * df_cat[column_B]
multiplication.name = f"{column_A} * {column_B}"
new_columns.update({multiplication.name: multiplication})
Diese kreierten Attribute fügen wir nun dem DataFrame hinzu.
[6]:
df_extended = df_cat.assign(**new_columns)
df_extended
[6]:
Outlook | Temperature | Humidity | Wind | Play Tennis? | Outlook + Wind | Outlook - Wind | Outlook * Wind | Outlook + Humidity | Outlook - Humidity | ... | Humidity * Temperature | Temperature + Outlook | Temperature - Outlook | Temperature * Outlook | Temperature + Wind | Temperature - Wind | Temperature * Wind | Temperature + Humidity | Temperature - Humidity | Temperature * Humidity | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 0 | 1 | 1 | 0 | 2 | 0 | 1 | 2 | 0 | ... | 0 | 1 | -1 | 0 | 1 | -1 | 0 | 1 | -1 | 0 |
1 | 1 | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 2 | 0 | ... | 0 | 1 | -1 | 0 | 0 | 0 | 0 | 1 | -1 | 0 |
2 | 2 | 2 | 0 | 1 | 0 | 3 | 1 | 2 | 2 | 2 | ... | 0 | 4 | 0 | 4 | 3 | 1 | 2 | 2 | 2 | 0 |
3 | 1 | 0 | 0 | 1 | 0 | 2 | 0 | 1 | 1 | 1 | ... | 0 | 1 | -1 | 0 | 1 | -1 | 0 | 0 | 0 | 0 |
4 | 0 | 1 | 0 | 1 | 1 | 1 | -1 | 0 | 0 | 0 | ... | 0 | 1 | 1 | 0 | 2 | 0 | 1 | 1 | 1 | 0 |
5 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | ... | 0 | 2 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 0 |
6 | 1 | 2 | 0 | 1 | 0 | 2 | 0 | 1 | 1 | 1 | ... | 0 | 3 | 1 | 2 | 3 | 1 | 2 | 2 | 2 | 0 |
7 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | ... | 0 | 1 | -1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
8 | 1 | 1 | 1 | 1 | 1 | 2 | 0 | 1 | 2 | 0 | ... | 1 | 2 | 0 | 1 | 2 | 0 | 1 | 2 | 0 | 1 |
9 | 1 | 0 | 1 | 1 | 1 | 2 | 0 | 1 | 2 | 0 | ... | 0 | 1 | -1 | 0 | 1 | -1 | 0 | 1 | -1 | 0 |
10 | 0 | 2 | 0 | 1 | 1 | 1 | -1 | 0 | 0 | 0 | ... | 0 | 2 | 2 | 0 | 3 | 1 | 2 | 2 | 2 | 0 |
11 | 2 | 2 | 0 | 0 | 0 | 2 | 2 | 0 | 2 | 2 | ... | 0 | 4 | 0 | 4 | 2 | 2 | 0 | 2 | 2 | 0 |
12 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 1 | -1 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | -1 | 0 |
13 | 2 | 2 | 1 | 0 | 1 | 2 | 2 | 0 | 3 | 1 | ... | 2 | 4 | 0 | 4 | 2 | 2 | 0 | 3 | 1 | 2 |
14 | 1 | 2 | 1 | 1 | 1 | 2 | 0 | 1 | 2 | 0 | ... | 2 | 3 | 1 | 2 | 3 | 1 | 2 | 3 | 1 | 2 |
15 | 2 | 2 | 1 | 1 | 0 | 3 | 1 | 2 | 3 | 1 | ... | 2 | 4 | 0 | 4 | 3 | 1 | 2 | 3 | 1 | 2 |
16 | 2 | 0 | 1 | 0 | 0 | 2 | 2 | 0 | 3 | 1 | ... | 0 | 2 | -2 | 0 | 0 | 0 | 0 | 1 | -1 | 0 |
17 | 0 | 1 | 0 | 1 | 0 | 1 | -1 | 0 | 0 | 0 | ... | 0 | 1 | 1 | 0 | 2 | 0 | 1 | 1 | 1 | 0 |
18 | 1 | 2 | 1 | 1 | 1 | 2 | 0 | 1 | 2 | 0 | ... | 2 | 3 | 1 | 2 | 3 | 1 | 2 | 3 | 1 | 2 |
19 | 0 | 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 2 | 2 | 0 | 2 | 2 | 0 | 2 | 2 | 0 |
20 | 0 | 1 | 1 | 1 | 0 | 1 | -1 | 0 | 1 | -1 | ... | 1 | 1 | 1 | 0 | 2 | 0 | 1 | 2 | 0 | 1 |
21 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 1 | -1 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | -1 | 0 |
22 | 2 | 1 | 0 | 0 | 1 | 2 | 2 | 0 | 2 | 2 | ... | 0 | 3 | -1 | 2 | 1 | 1 | 0 | 1 | 1 | 0 |
23 | 1 | 2 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | ... | 0 | 3 | 1 | 2 | 2 | 2 | 0 | 2 | 2 | 0 |
24 | 2 | 1 | 0 | 1 | 1 | 3 | 1 | 2 | 2 | 2 | ... | 0 | 3 | -1 | 2 | 2 | 0 | 1 | 1 | 1 | 0 |
25 | 0 | 0 | 1 | 1 | 0 | 1 | -1 | 0 | 1 | -1 | ... | 0 | 0 | 0 | 0 | 1 | -1 | 0 | 1 | -1 | 0 |
26 | 2 | 1 | 0 | 1 | 1 | 3 | 1 | 2 | 2 | 2 | ... | 0 | 3 | -1 | 2 | 2 | 0 | 1 | 1 | 1 | 0 |
27 | 0 | 0 | 0 | 1 | 0 | 1 | -1 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 1 | -1 | 0 | 0 | 0 | 0 |
28 | 1 | 2 | 1 | 1 | 0 | 2 | 0 | 1 | 2 | 0 | ... | 2 | 3 | 1 | 2 | 3 | 1 | 2 | 3 | 1 | 2 |
29 | 1 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | ... | 0 | 1 | -1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
30 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 0 | 1 | -1 | ... | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 2 | 0 | 1 |
31 | 2 | 0 | 1 | 1 | 0 | 3 | 1 | 2 | 3 | 1 | ... | 0 | 2 | -2 | 0 | 1 | -1 | 0 | 1 | -1 | 0 |
32 | 1 | 2 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | ... | 0 | 3 | 1 | 2 | 2 | 2 | 0 | 2 | 2 | 0 |
33 | 1 | 0 | 0 | 1 | 0 | 2 | 0 | 1 | 1 | 1 | ... | 0 | 1 | -1 | 0 | 1 | -1 | 0 | 0 | 0 | 0 |
34 | 1 | 0 | 0 | 1 | 0 | 2 | 0 | 1 | 1 | 1 | ... | 0 | 1 | -1 | 0 | 1 | -1 | 0 | 0 | 0 | 0 |
35 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
36 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 0 |
37 | 2 | 1 | 0 | 0 | 1 | 2 | 2 | 0 | 2 | 2 | ... | 0 | 3 | -1 | 2 | 1 | 1 | 0 | 1 | 1 | 0 |
38 | 0 | 2 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 2 | 2 | 0 | 2 | 2 | 0 | 2 | 2 | 0 |
39 | 1 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | ... | 0 | 1 | -1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
40 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | ... | 0 | 2 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 0 |
41 | 0 | 1 | 0 | 1 | 0 | 1 | -1 | 0 | 0 | 0 | ... | 0 | 1 | 1 | 0 | 2 | 0 | 1 | 1 | 1 | 0 |
42 | 1 | 0 | 1 | 1 | 0 | 2 | 0 | 1 | 2 | 0 | ... | 0 | 1 | -1 | 0 | 1 | -1 | 0 | 1 | -1 | 0 |
43 | 2 | 2 | 0 | 0 | 0 | 2 | 2 | 0 | 2 | 2 | ... | 0 | 4 | 0 | 4 | 2 | 2 | 0 | 2 | 2 | 0 |
44 | 0 | 1 | 1 | 1 | 1 | 1 | -1 | 0 | 1 | -1 | ... | 1 | 1 | 1 | 0 | 2 | 0 | 1 | 2 | 0 | 1 |
45 | 2 | 2 | 0 | 0 | 1 | 2 | 2 | 0 | 2 | 2 | ... | 0 | 4 | 0 | 4 | 2 | 2 | 0 | 2 | 2 | 0 |
46 | 2 | 2 | 0 | 1 | 1 | 3 | 1 | 2 | 2 | 2 | ... | 0 | 4 | 0 | 4 | 3 | 1 | 2 | 2 | 2 | 0 |
47 | 0 | 2 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 2 | 2 | 0 | 2 | 2 | 0 | 2 | 2 | 0 |
48 | 1 | 2 | 0 | 1 | 1 | 2 | 0 | 1 | 1 | 1 | ... | 0 | 3 | 1 | 2 | 3 | 1 | 2 | 2 | 2 | 0 |
49 | 0 | 0 | 0 | 1 | 1 | 1 | -1 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 1 | -1 | 0 | 0 | 0 | 0 |
50 rows × 41 columns
Nun lass uns sehen, ob der Entscheidungsbaum mit diesen generierten Features besser arbeiten kann:
[7]:
ziel = df_extended["Play Tennis?"]
eingabe = df_extended[list(sorted(list(set(df_extended.columns) - set(["Play Tennis?"]))))]
X_train, X_test, y_train, y_test = sklearn.model_selection.train_test_split(eingabe, ziel, random_state=74)
dt = sklearn.tree.DecisionTreeClassifier(random_state=5)
dt.fit(X_train, y_train)
s = dt.score(X_test, y_test)
s
[7]:
0.9230769230769231
Aufgabe 2
Interpretieren Sie den Zahlenwert. Liegt hier ein zufälliger nicht-linearer Zusammenhang vor? Ist dieses Ergebnis plausibel? Warum (nicht)?
Antwort: …
Aufgabe 3
Stellen Sie sich vor, Sie haben mit einem Dienstleister einen Vertrag abgeschlossen. Dort heißt es, es soll mindestens eine Genauigkeit von 80% erreicht werden (wie oben der Fall). Wie können Sie sich davor schützen, dass der Dienstleister Ihnen ein nicht praxistaugliches Produkt verkauft? Gehen Sie davon aus, dass Sie den Source Code nicht einsehen können.
Antwort: …
Dieses Werk von Marvin Kastner ist lizenziert unter einer Creative Commons Namensnennung 4.0 International Lizenz.