CSBootcampHSG

Lektion 10

Machine Learning I — Klassifikation und KNN

Supervised Machine Learning, Train/Test-Split, der scikit-learn-Workflow `fit`/`predict`, K-Nearest-Neighbors einmal von Hand und einmal mit `sklearn`, sowie die Metriken Accuracy, Precision, Recall und F1.

Dauer:
75 Min. geschätzt
Voraussetzungen:
Pandas (Lektion 08) · Aggregation und Visualisierung (Lektion 09)

Theorie & Konzepte

Phase 1 / 4

Bisher hast du Daten geladen, gefiltert, aggregiert und geplottet. Diese Lektion ist der Sprung in Machine Learning: aus den Daten ein Modell lernen, das Vorhersagen für neue Beispiele macht.

Wir bleiben bei einer der einfachsten Familien — Klassifikation — und arbeiten den ganzen sklearn-Workflow einmal durch:

  1. Daten anschauen und in Train/Test splitten.
  2. Einen Klassifikator wählen, mit fit trainieren.
  3. Mit predict für die Test-Daten Vorhersagen machen.
  4. Mit Metriken bewerten, wie gut das Modell ist.

Im zweiten Teil bauen wir K-Nearest Neighbors einmal selbst — damit du verstehst, was im KNeighborsClassifier von sklearn passiert.

Lektion 11 setzt darauf auf: dort wird aus funktioniert ein funktioniert sauber (Validation-Set, Skalierung, Modellvergleich).

Was *Supervised* an Supervised Machine Learning bedeutet

Supervised Machine Learning heißt: du gibst dem Modell Eingabe-Beispiele plus die richtige Antwort. Das Modell lernt aus diesen Paaren, Eingabe → Antwort, einen Zusammenhang. Bei IRIS:

  • Eingabe (Features X): vier Zahlen pro Blume — sepal_length, sepal_width, petal_length, petal_width.
  • Ausgabe (Label y): die Spezies — setosa, versicolor oder virginica.
  • Trainingsbeispiele: viele Zeilen, jede mit ihren vier Features und ihrer bekannten Spezies.

Nach dem Training kannst du dem Modell die vier Features einer neuen Blume geben (deren Spezies es nie gesehen hat), und es sagt dir die wahrscheinlichste Spezies.

Zwei Hauptaufgaben:

Aufgabey ist…Beispiel
Klassifikationeine KategorieSpezies, Spam/Ham, Buy/Hold/Sell
Regressioneine ZahlHauspreis, Temperatur morgen

Lektion 10 ist Klassifikation. Regression kommt in Lektion 11 zur Sprache.

Abgrenzung zu Unsupervised Learning: unsupervised heißt ohne Labels. Du gibst dem Modell nur X und es findet selbst Strukturen (Clustering, PCA).

Und das dritte Paradigma — Reinforcement Learning: weder Labels noch reine Strukturanalyse, sondern ein Agent, der mit einer Umgebung interagiert und über Belohnungen (Reward) lernt. Statt „hier ist die richtige Antwort" gibt es nur „diese Aktion war gut, jene war schlecht" — und das Modell muss selbst herausfinden, welche Strategie langfristig den höchsten Gesamt-Reward liefert. Klassische Beispiele: Schach- und Go-Engines (AlphaGo), Robotersteuerung, automatisches Trading. Anders als bei Supervised Learning gibt es kein fixes Trainingsset — die Daten entstehen durch das Handeln des Agenten selbst. RL kommt im Bootcamp nicht vor; aber den Begriff solltest du kennen, wenn er im Quiz als Distraktor auftaucht.

Wir bleiben in dieser Lektion bei supervised.

Train/Test-Split — warum man immer trennt

Erste Goldene Regel der ML: Du darfst dein Modell niemals auf denselben Daten testen, auf denen es trainiert wurde. Sonst bewertet du auswendig-Lernen, nicht Generalisierung.

Deshalb teilst du den Datensatz in zwei disjunkte Teile:

  • Trainingsset (typisch 70–80 %): zeigt dem Modell die Wahrheit, daraus lernt es.
  • Testset (typisch 20–30 %): nach dem Training einmal angeschaut, um die Performance zu messen. Das Modell hat diese Beispiele nie gesehen.
from sklearn.model_selection import train_test_split

X = IRIS[['sepal_length', 'sepal_width', 'petal_length', 'petal_width']]
y = IRIS['species']

X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,        # 20% für den Test
    random_state=42,      # reproduzierbar
    stratify=y,           # gleiche Klassenverteilung in beiden Teilen
)

Drei Parameter, die du immer setzt:

  • test_size — Anteil oder absolute Zahl der Test-Zeilen.
  • random_state — Seed für den Random Generator. Pflicht für Reproduzierbarkeit. Ohne kriegst du jedes Mal andere Splits, jedes Mal andere Metriken — das ist nicht messbar.
  • stratify=y — sorgt dafür, dass beide Teile dieselbe Klassenverteilung haben. Bei IRIS (3 Klassen × 50 Zeilen) sehr wichtig — sonst landet eventuell nur eine Klasse im Testset.

Train/Test-Split visuell

90 Beispiele · Train (durchgezogen) · Test (getupft)

Trainingsset · 72 Zeilen

  • setosa24(33%)
  • versicolor24(33%)
  • virginica24(33%)

Testset · 18 Zeilen

  • setosa6(33%)
  • versicolor6(33%)
  • virginica6(33%)
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.20,
    random_state=42,
    stratify=y,
)
Stell `test_size` und `random_state` ein. Du siehst, wie viele Zeilen jeweils im Train- und Test-Set landen — und mit `stratify` an/aus, ob die Klassenverteilung erhalten bleibt.

Der sklearn-Workflow — `fit` und `predict`

Jeder Klassifikator in sklearn hat dieselben drei Methoden:

from sklearn.tree import DecisionTreeClassifier

model = DecisionTreeClassifier(random_state=42)   # 1. Modell-Objekt
model.fit(X_train, y_train)                       # 2. Trainieren
y_pred = model.predict(X_test)                    # 3. Vorhersagen
  • fit(X, y) — schaut sich Trainings-Features und -Labels an und passt die internen Parameter an. Verändert das Modell-Objekt in-place.
  • predict(X) — wendet das gelernte Modell auf neue Features an, gibt einen 1D-Array von Labels zurück.
  • predict_proba(X) (optional) — gibt Wahrscheinlichkeiten pro Klasse zurück, statt nur die wahrscheinlichste.

Der Trick: alle Klassifikatoren in sklearn folgen exakt diesem Interface. Du kannst DecisionTreeClassifier durch KNeighborsClassifier, LogisticRegression, RandomForestClassifier, SVC ersetzen, ohne den Rest des Codes zu ändern. Das ist ein Hauptgrund, warum sklearn so populär ist.

Klassifikator-Auswahl: sklearn-Map vs. Pragmatik

Welchen Klassifikator soll ich nehmen?

Pragmatischer Einstieg

Drei Klassifikatoren reichen für 95 % der Anfänger-Fälle:

  1. DecisionTreeClassifier — schnell, interpretierbar, kein Skalieren nötig. Gut für erste Versuche.
  2. KNeighborsClassifier(n_neighbors=k) — keine Trainingsphase im klassischen Sinn. Braucht skalierte Features.
  3. RandomForestClassifier — der Allrounder. Meist gut ohne viel Tuning.

Fang mit DecisionTree, vergleich mit RandomForest. Das gibt dir eine Baseline.

Falle: Modell-Roulette

Wer 8 verschiedene Klassifikatoren wahllos ausprobiert und denjenigen mit der besten Test-Accuracy nimmt, leckt Information aus dem Testset ins Modellauswahl-Verfahren. Das Modell sieht im Bericht besser aus, als es ist.

Saubere Variante (Lektion 11): mit Validation-Set vergleichen, Gewinner einmal auf Test bewerten.

K-Nearest Neighbors — die Intuition

KNN ist der am einfachsten zu erklärende Klassifikator. Idee in einem Satz:

„Schau dir die k ähnlichsten Trainings-Beispiele an. Welche Klasse haben die meisten? Das ist deine Vorhersage.“

Kein Lernen im klassischen Sinne — KNN speichert einfach alle Trainingsdaten und schaut bei jeder Vorhersage neu nach. Das macht KNN zu einer lazy learning-Methode: viel Arbeit zum predict-Zeitpunkt, fast keine zum fit-Zeitpunkt.

Der Algorithmus in vier Schritten

Für eine neue Beobachtung q:

  1. Distanz zu jedem Trainingspunkt berechnen.
  2. Die k Punkte mit der kleinsten Distanz nehmen.
  3. Unter diesen k Nachbarn die Klassen zählen.
  4. Die häufigste Klasse zurückgeben (Mehrheitsvotum).

Was KNN braucht (für Quiz 10 wichtig!)

KNN ist vollständig definiert durch drei Komponenten:

  • k — wie viele Nachbarn schaue ich an? (1, 3, 5, 7, …)
  • Eine Distanzfunktion — meistens Euklidisch: .
  • Die Trainingsdaten — ohne sie kann KNN nichts vorhersagen.

Was KNN nicht braucht: keine Lernrate, keine Verlustfunktion, keine Anzahl Epochen, keine Schichten. KNN gehört nicht zu den Modellen, die trainiert werden — es erinnert sich einfach.

KNN am 2D-Scatterplot ausprobieren

Klick irgendwo in den Plot, um die Query zu setzen.

Query-Punkt

(3.00, 4.00)

Mehrheitsvotum unter den k = 3 Nachbarn

  • Blau0×
  • Rot1×
  • Grün2×Vorhersage
KNeighborsClassifier(n_neighbors=3).predict( [[3.0, 4.0]])
→ 'green'
Klick irgendwo in den Plot, um eine neue Beobachtung zu setzen. Schieb den k-Slider — die k nächsten Nachbarn werden hervorgehoben und die Vorhersage zeigt sich rechts. Das ist auch genau das, was Quiz 10 Q4 abprüft.

KNN von Hand: vier Zeilen Numpy

Implementierung in Python — gerade so viel Code, dass du das sklearn-KNeighborsClassifier als „genau das, plus Optimierungen“ lesen kannst:

import numpy as np
from collections import Counter

def knn_predict(query, X_train, y_train, k=5):
    # 1. Distanzen zu allen Trainingspunkten
    distances = np.linalg.norm(X_train - query, axis=1)
    # 2. Indizes der k kleinsten
    nearest_idx = np.argsort(distances)[:k]
    # 3. Klassen dieser k Punkte
    nearest_labels = y_train[nearest_idx]
    # 4. Mehrheitsvotum
    return Counter(nearest_labels).most_common(1)[0][0]

Wichtige Details:

  • np.linalg.norm(X - q, axis=1) rechnet die Euklidische Distanz zeilenweise — schnell, weil vektorisiert. Ein Python-for-Loop wäre 100× langsamer.
  • np.argsort(distances)[:k] gibt die Positionen der k kleinsten Distanzen zurück (sortiert aufsteigend).
  • Counter(...).most_common(1) ist die idiomatische Art, das häufigste Element zu finden — viel knapper als max() mit key=.

Das ist alles, was im sklearn-KNeighborsClassifier konzeptionell passiert. Sklearn hat dazu noch KD-Trees und Ball-Trees für schnellere Nachbarsuchen, aber das Verhalten ist gleich.

KNN mit sklearn — derselbe Workflow

Mit sklearn bleibt der Code symmetrisch zu jedem anderen Klassifikator:

from sklearn.neighbors import KNeighborsClassifier

model = KNeighborsClassifier(n_neighbors=5)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

Wichtigste Hyperparameter:

  • n_neighbors=k — wie oben.
  • weights='uniform' (Default) oder 'distance' — bei 'distance' wiegen näherliegende Nachbarn mehr.
  • metric='minkowski' mit p=2 (= Euklidisch) ist der Default. p=1 wäre Manhattan-Distanz.

Achtung Skalierung: KNN basiert auf Distanzen — wenn eine Spalte in 1000ern und eine in 0.01ern misst, dominiert die große Spalte komplett. Standardisieren (StandardScaler oder RobustScaler) ist Pflicht. Bei IRIS sind alle Features im selben Bereich, deshalb fällt es nicht auf — bei echten Daten passiert hier oft der erste Bug.

Lektion 11 macht das Skalieren explizit.

Bewertung — Accuracy, Precision, Recall, F1

Accuracy (Genauigkeit) ist die intuitivste Metrik:

Problem: Bei unbalancierten Klassen ist sie irreführend. Wenn 95 % der Mails Ham sind, kriegst du 95 % Accuracy, indem du alles als Ham klassifizierst — und erkennst keinen einzigen Spam.

Drei Metriken, die du pro Klasse berechnest:

  • Precision = von allen, die das Modell als Klasse X vorhersagt, wie viele waren wirklich X?
  • Recall = von allen, die wirklich X sind, wie viele hat das Modell als X erkannt?
  • F1 = harmonisches Mittel aus Precision und Recall. Eine einzige Zahl, die beide ausbalanciert.

Für mehrere Klassen aggregierst du auf eine Gesamt-Zahl mit:

  • average='macro' — Durchschnitt der Pro-Klassen-Werte. Behandelt jede Klasse gleich, unabhängig von ihrer Häufigkeit. Standard im HSG-Assignment.
  • average='micro' — global über alle Vorhersagen aggregiert. Bei Single-Label-Klassifikation fallen Precision, Recall und F1 hier auf denselben Wert wie die Accuracy zusammen — also keine zusätzliche Information.
  • average='weighted' — Pro-Klasse, gewichtet mit der Häufigkeit.

Positiv vs. negativ — warum derselbe Klassifikator zwei Wertepaare hat

Ein häufiges Stolperproblem: Precision, Recall und F1 hängen davon ab, welche Klasse du als „positiv" betrachtest. Dieselbe Confusion Matrix liefert zwei komplett verschiedene Wertesätze, je nach Perspektive.

Konkretes Beispiel

Ein Spam-Filter wertet 1000 Mails aus:

pred spampred ham
true spam4010
true ham20930

Aus Sicht der Klasse spam (positiv)

  • TP = 40 (Spam korrekt erkannt), FN = 10 (Spam übersehen), FP = 20 (Ham als Spam markiert), TN = 930
  • Precision(spam) = 40 / (40 + 20) = 0.667„Wenn der Filter Spam sagt, in welchen Fällen stimmt's?"
  • Recall(spam) = 40 / (40 + 10) = 0.800„Wieviel echtes Spam erwischt er?"

Aus Sicht der Klasse ham (positiv)

  • TP = 930, FN = 20, FP = 10, TN = 40
  • Precision(ham) = 930 / (930 + 10) = 0.989
  • Recall(ham) = 930 / (930 + 20) = 0.979

Gleicher Klassifikator, gleicher Datensatz — aber die Spam-Erkennung sieht solide-mittelmäßig aus, die Ham-Erkennung sieht herausragend aus. Der Grund ist die Klassenungleichheit: 950 Hams gegen 50 Spams. In so einem Setting verzerrt jede Spam-Verwechslung die Spam-Metriken stark, beeinflusst aber kaum die Ham-Metriken.

Welche Klasse ist eigentlich „positiv"?

Konvention: das, was dir wichtig ist und/oder selten ist.

  • Spam-Filter: spam ist positiv (du willst sie erkennen).
  • Krebsdiagnose: krebs-Diagnose ist positiv (False Negatives wären fatal).
  • Bei IRIS gibt's keine natürliche „positive" Klasse — alle drei Spezies sind gleichberechtigt. Deshalb berichtet man dort gerne macro avg (Durchschnitt über alle Klassen) statt einer einzelnen.

TP/FN/FP/TN sind perspektiv-abhängig

Dieselbe Zelle in der Matrix bekommt unterschiedliche Labels, je nachdem, welche Klasse du gerade als positiv ansiehst:

Zelle (true → pred)aus Sicht spamaus Sicht ham
spam → spamTPTN
spam → hamFNFP
ham → spamFPFN
ham → hamTNTP

Accuracy ist die einzige Metrik, die nicht von der Perspektive abhängt — sie zählt nur die Diagonale (alles korrekt klassifizierte) und teilt durch die Gesamtanzahl. Deshalb steht sie in classification_report auch nicht in einer Pro-Klassen-Zeile, sondern einmal global.

Im Rechner unten kannst du das ausprobieren: schalte zwischen für positiv und für negativ um — Precision, Recall und F1 ändern sich, Accuracy bleibt konstant.

Metriken-Rechner — Confusion Matrix anfassen

Zellen +/- klicken, Metriken updaten live.
100 Beispiele · 85 korrekt

Confusion Matrix

pred positiv
pred negativ
true positiv
40
10
true negativ
5
45

Diagonale (grün) = korrekt klassifiziert. Off-Diagonale = Fehler.

Accuracy

0.850

= 85 / 100

Pro Klasse

KlassePrecisionRecallF1support
positiv0.8890.8000.84250
negativ0.8180.9000.85750
macro avg0.8540.8500.850100

Formeln mit den aktuellen Zahlen

Bezugsklasse: positiv — TP/FN/FP/TN sind aus Sicht von „positiv definiert.
TP = 40,   FN = 10,   FP = 5,   TN = 45
accuracy = (TP + TN) / total = (40 + 45) / 100 = 0.850
precision(positiv) = TP / (TP + FP) = 40 / (40 + 5) = 0.889
recall(positiv) = TP / (TP + FN) = 40 / (40 + 10) = 0.800
F1(positiv) = 2 · P · R / (P + R) = 2 · 0.889 · 0.800 / (0.889 + 0.800) = 0.842

💡 Klick oben auf die andere Klasse — du siehst, wie sich TP/FN/FP/TN und damit Precision, Recall und F1 ändern. Accuracy bleibt gleich (sie ist global, nicht pro Klasse).

Erkunden-Modus: Zellen +/- klicken, Metriken updaten live. Im Übungsmodus rollt eine zufällige Matrix mit einer konkreten Frage (Accuracy / Precision / Recall / F1) — genau das Format der Klausur. Tipp: in „Erkunden“ den Klassen-Tab umschalten, um zu sehen, wie sich die Metriken aus „positiv“- vs. „negativ“-Sicht unterscheiden.

Der `classification_report` als Standard-Output

sklearn liefert dir alles auf einmal:

from sklearn.metrics import classification_report

print(classification_report(y_test, y_pred))

Ausgabe:

              precision    recall  f1-score   support

      setosa       1.00      1.00      1.00        10
  versicolor       0.91      1.00      0.95        10
   virginica       1.00      0.90      0.95        10

    accuracy                           0.97        30
   macro avg       0.97      0.97      0.97        30
weighted avg       0.97      0.97      0.97        30

Lies das so:

  • Pro-Klassen-Zeilen zeigen, ob das Modell bei einer bestimmten Spezies häufiger Fehler macht. Hier: versicolor hat nur 91 % Precision — heißt, manchmal werden andere Spezies fälschlich als versicolor erkannt.
  • support ist die Anzahl Test-Beispiele dieser Klasse.
  • accuracy ist eine globale Zahl — die einzige, die nicht pro Klasse berechnet wird.
  • macro avg mittelt über die drei Pro-Klassen-Zeilen, jede Klasse zählt gleich.
  • weighted avg mittelt mit Gewichtung nach support.

k-fold Cross-Validation — was kommt in Quiz 10

k-fold Cross-Validation ist eine Methode, die Modell-Bewertung robuster zu machen, ohne dafür mehr Daten zu brauchen.

Die Idee in fünf Schritten (für k=5)

  1. Trainings-Daten in 5 gleich große, disjunkte Teile (Folds) zerlegen.
  2. Für jeden Fold:
    • Trainiere auf den anderen 4 Folds.
    • Bewerte auf diesem 1 Fold.
  3. Du hast 5 Performance-Werte.
  4. Mittelwert der 5 Werte ist deine geschätzte Modell-Performance.
  5. Standardabweichung sagt dir, wie stabil das Modell ist.

Was es löst

  • Mehr Daten zum Trainieren: jeder Datenpunkt wird einmal validiert und mehrfach trainiert. (Kein Datenpunkt geht „verloren“.)
  • Robustere Schätzung: ein einzelner Train/Val-Split kann durch Zufall gut oder schlecht ausgehen. 5 Splits → 5 Schätzungen, weniger Varianz.
  • Stabilitäts-Indikator: wenn die 5 Werte stark schwanken, ist das Modell empfindlich gegen die Datenwahl.

Was es nicht tut

  • k-fold ist nicht zwingend stratifiziert. Bei unbalancierten Klassen brauchst du StratifiedKFold (oder cross_val_score(..., cv=StratifiedKFold(...))).
  • k-fold ersetzt das Testset nicht. Du tunst auf den Folds, der echte Test wartet daneben.
  • k-fold trainiert nicht k Modelle, die du danach behältst. Es gibt dir eine Schätzung der Performance — fürs finale Modell trainierst du noch einmal auf allen Trainingsdaten.

Quiz 10 hat eine Frage zu k-fold-CV: typische Distraktoren sind „splittet die Test-Daten in k Teile“ (falsch — Train) oder „liefert ein einzelnes finales Modell“ (auch falsch).

Assignment-Walkthrough

Phase 2 / 4

Hier gehen wir typspezifisch durch die 6 Aufgabenformen von Assignment 10: was gefragt ist, wie man es angeht, und wo man sich typischerweise verrennt. Die konkreten Namen und Daten siehst du im HSG-Notebook — die Muster hier passen auf jede Variante.

Assignment-Aufgabe 1

Daten visualisieren — Eindruck verschaffen

Was ist gefragt

Aufgabentyp: Bevor du modellierst, schau dir die Daten an. Du sollst pro IRIS-Spezies die vier Features visualisieren (Boxplot, Pairplot oder Scatter-Matrix). Es gibt keine Test-Zelle — die Aufgabe ist „zeig, dass du die Daten gesehen hast".

Ziel ist zu erkennen, welche Features die drei Klassen am besten trennen. Bei IRIS sind das petal_length und petal_width — die setosa ist mit ihnen schon linear separierbar.

Strategie

Drei sinnvolle Plot-Varianten:

  1. sns.pairplot(IRIS, hue='species') — die einfachste, aussagekräftigste Visualisierung. Eine Zeile reicht, du bekommst alle paarweisen Scatter und Diagonalen.
  2. 3 Boxplots nebeneinander (eine pro Spezies, vier Features) — wie in Assignment 9 gemacht.
  3. Korrelations-Heatmap mit sns.heatmap(IRIS.drop(columns='species').corr(), annot=True).

Lösungsskelett

import seaborn as sns
import matplotlib.pyplot as plt

# Variante 1: Pairplot — kompakt
sns.pairplot(IRIS, hue='species')
plt.show()

Typische Stolperstellen

  • hue vergessen — ohne hue='species' siehst du nur graue Punkte, kein Klassen-Split.
  • IRIS.drop(columns='species') für corr() — Korrelation funktioniert nur auf numerischen Spalten; das String-Label species wirft sonst eine Warnung oder ein leeres Feld.

Assignment-Aufgabe 2

Train/Test-Split mit `train_test_split`

Was ist gefragt

Aufgabentyp: Du sollst X (vier Feature-Spalten) und y (Spezies) in Trainings- und Test-Set aufteilen. Reproduzierbar, mit Stratifizierung. Die Test-Zelle prüft typischerweise die Größen von X_train, X_test, y_train, y_test.

Strategie

Eine Zeile, vier Variablen zurück:

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,        # oder was die Aufgabe sagt
    random_state=42,      # MUSS gesetzt sein für Reproduzierbarkeit
    stratify=y,           # für gleiche Klassenverteilung in beiden Teilen
)

Wichtig: Die Reihenfolge der Rückgabewerte ist X_train, X_test, y_train, y_testnicht X_train, y_train, X_test, y_test. Wer das vertauscht, trainiert auf den Labels und testet auf den Features.

Lösungsskelett

from sklearn.model_selection import train_test_split

X = IRIS[['sepal_length', 'sepal_width', 'petal_length', 'petal_width']]
y = IRIS['species']

X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,
    random_state=42,
    stratify=y,
)

Typische Stolperstellen

  • random_state weglassen — ohne wird der Split jedes Mal anders, deine Test-Zelle kriegt zufällige Größen je nach Lauf.
  • Reihenfolge der Rückgabewerte vertauschen — der häufigste Fehler. Merke: erst alle X, dann alle y. Test-Zelle scheitert mit „cannot reshape" oder ähnlich.
  • stratify=y weglassen — bei IRIS (50 pro Klasse) meist kein Problem; bei realen Daten manchmal kritisch.

Assignment-Aufgabe 3

Klassifikator wählen, fitten, predicten

Was ist gefragt

Aufgabentyp: Wähle einen Klassifikator aus dem sklearn-Map, trainiere ihn auf X_train, y_train, sage X_test voraus. Speichere die Vorhersagen in y_pred.

Die Aufgabe ist offen — jeder Klassifikator ist OK, solange du am Ende y_pred hast. Für IRIS gibt es viele, die fast 100 % erreichen.

Strategie

Drei Klassifikatoren, alle drei Zeilen Code:

from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression

model = DecisionTreeClassifier(random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

DecisionTree ist der pragmatische Erstversuch:

  • Kein Skalieren nötig.
  • Liefert auf IRIS quasi sofort 95–100 % Accuracy.
  • random_state setzen für Reproduzierbarkeit.

Lösungsskelett

from sklearn.tree import DecisionTreeClassifier

model = DecisionTreeClassifier(random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

Typische Stolperstellen

  • fit(X_test, y_test) statt X_train, y_train — Datenleck. Modell sieht das Testset und „weiß" alles.
  • fit(...) als Wert speichernmodel.fit(...) verändert model in-place und gibt model zurück. m = model.fit(...) ist überflüssig, aber funktioniert. model.fit(...) allein reicht.
  • random_state weglassen — Decision Trees haben Zufallselemente bei Tiebreaks; ohne seed kriegst du minimal andere Vorhersagen pro Lauf.

Assignment-Aufgabe 4

Metriken — `accuracy_score`, `precision_recall_fscore_support`, `classification_report`

Was ist gefragt

Aufgabentyp: Berechne und drucke die vier Metriken aus y_test und y_pred. Für Precision/Recall/F-Score wird average='macro' verlangt — Aufgabe ist explizit.

Strategie

Drei Funktionen aus sklearn.metrics:

from sklearn.metrics import (
    accuracy_score,
    precision_recall_fscore_support,
    classification_report,
)

print('Accuracy:', accuracy_score(y_test, y_pred))

p, r, f, _ = precision_recall_fscore_support(
    y_test, y_pred, average='macro'
)
print(f'Precision (macro): {p:.3f}')
print(f'Recall    (macro): {r:.3f}')
print(f'F1        (macro): {f:.3f}')

print(classification_report(y_test, y_pred))

Das vierte Element von precision_recall_fscore_support ist support — bei average='macro' ist das None, deshalb in _ werfen.

Lösungsskelett

from sklearn.metrics import (
    accuracy_score,
    precision_recall_fscore_support,
    classification_report,
)

print(f'Accuracy: {accuracy_score(y_test, y_pred):.3f}')

p, r, f, _ = precision_recall_fscore_support(y_test, y_pred, average='macro')
print(f'Precision (macro): {p:.3f}')
print(f'Recall    (macro): {r:.3f}')
print(f'F1-Score  (macro): {f:.3f}')

print(classification_report(y_test, y_pred))

Typische Stolperstellen

  • average= vergessen — gibt dir Arrays statt Skalaren. Print-Ausgabe sieht falsch aus, manchmal scheitert die Test-Zelle.
  • accuracy_score mit average= aufrufen — Funktion akzeptiert keinen average-Parameter (Accuracy ist global). Wer das versucht, kriegt TypeError.
  • zero_division-Warnung — bei Klassen ohne Vorhersagen warnt sklearn. Setze zero_division=0 zur Beruhigung.

Assignment-Aufgabe 5

K-Nearest-Neighbors von Hand implementieren

Was ist gefragt

Aufgabentyp: Schreib k_nearest_neighbors(node, nodes, k=5) selbst. Eingabe: ein Query-Punkt node (1D), eine Liste/Array von Trainingspunkten nodes (2D). Ausgabe: die k nächsten Trainingspunkte (als Indizes oder als Punkte selbst — die Test-Zelle gibt vor, was sie erwartet).

Danach baust du auf der Funktion eine predict-Routine: für jede Test-Zeile die k Nachbarn finden, deren Klassen zählen, Mehrheitsvotum.

Strategie

Vier Zeilen NumPy reichen:

import numpy as np

def k_nearest_neighbors(node, nodes, k=5):
    distances = np.linalg.norm(nodes - node, axis=1)   # 1D-Array, ein Wert pro Trainingspunkt
    return np.argsort(distances)[:k]                   # Indizes der k kleinsten

Danach für jede Test-Zeile:

from collections import Counter

y_pred = []
for q in X_test.values:
    idx = k_nearest_neighbors(q, X_train.values, k=5)
    nearest_labels = y_train.values[idx]
    y_pred.append(Counter(nearest_labels).most_common(1)[0][0])

Lösungsskelett

import numpy as np
from collections import Counter

def k_nearest_neighbors(node, nodes, k=5):
    distances = np.linalg.norm(nodes - node, axis=1)
    return np.argsort(distances)[:k]

# Anwenden auf alle Test-Zeilen
y_pred_manual = []
for q in X_test.values:
    idx = k_nearest_neighbors(q, X_train.values, k=5)
    nearest_labels = y_train.values[idx]
    y_pred_manual.append(Counter(nearest_labels).most_common(1)[0][0])

Typische Stolperstellen

  • X_train als DataFrame statt Arraynodes - node mit DataFrames zickt manchmal. Mit .values oder .to_numpy() zu NumPy konvertieren.
  • Python-for-Loop für Distanzen — funktioniert, aber 100× langsamer. np.linalg.norm(..., axis=1) ist die idiomatische Lösung.
  • max(set(labels), key=labels.count) funktioniert auch fürs Mehrheitsvotum, ist aber O(n²)Counter.most_common(1) ist O(n).

Assignment-Aufgabe 6

Sklearn `KNeighborsClassifier` mit derselben Pipeline

Was ist gefragt

Aufgabentyp: Jetzt mit sklearn statt von Hand — drei Zeilen, gleicher Output wie Aufgabe 2.1. Anschließend wieder den classification_report drucken. Die Test-Zelle erwartet, dass deine Vorhersagen mit dem manuellen Resultat aus 2.1 (modulo Tiebreaks) übereinstimmen.

Strategie

from sklearn.neighbors import KNeighborsClassifier

model = KNeighborsClassifier(n_neighbors=5)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

print(classification_report(y_test, y_pred))

Vergleich mit Hand-Implementierung (Aufgabe 2.1): sollte exakt gleich sein, solange du dieselbe Distanzfunktion (euklidisch) und dasselbe k benutzt. Bei genau gleichen Distanzen kann sklearn anders tiebreaken — minimal unterschiedliche Werte sind also OK.

Lösungsskelett

from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import classification_report

model = KNeighborsClassifier(n_neighbors=5)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

print(classification_report(y_test, y_pred))

Typische Stolperstellen

  • n_neighbors als positionales ArgumentKNeighborsClassifier(5) funktioniert, aber explizit ist klarer.
  • Ergebnisse leicht unterschiedlich von 2.1 — bei Tiebreaks (zwei Nachbarn mit identischer Distanz) wählen sklearn und dein Counter eventuell unterschiedlich. Akzeptabler Unterschied.
  • Skalierung vergessen — bei IRIS unkritisch (alle Features in cm), bei realen Daten Pflicht. Siehe Lektion 11.

Aufgaben

Phase 3 / 4
Aufgabe

Euklidische Distanz mit NumPy

2 Punkte

Implementiere die Funktion euclidean(p, q), die die Euklidische Distanz zwischen zwei Punkten p und q zurückgibt. Beide sind 1D-Arrays gleicher Länge.

Formel: d(p, q) = sqrt(sum((p_i - q_i)^2))

Vektorisiert mit NumPy — keine Python-for-Schleife. Das ist genau die Operation, die in der KNN-Vorhersage später für jeden Trainingspunkt einmal läuft, also sollte sie schnell sein.

Hinweise

    Aufgabe

    K-Nearest Neighbors von Hand

    3 Punkte

    Implementiere knn_predict(query, X_train, y_train, k), die für einen einzelnen Punkt query (1D-Array) die vorhergesagte Klasse zurückgibt:

    1. Berechne die Distanz von query zu allen Zeilen von X_train (vektorisiert).
    2. Finde die Indizes der k kleinsten Distanzen.
    3. Schau dir die Labels dieser k Nachbarn in y_train an.
    4. Gib das häufigste Label zurück (Mehrheitsvotum).

    X_train ist 2D (n × d), y_train ist 1D (n) — beide NumPy-Arrays.

    Hinweise

      Aufgabe

      sklearn-Pipeline: Split, Fit, Predict

      3 Punkte

      Schreibe eine Funktion fit_and_predict(X, y, test_size, random_state, k), die:

      1. Mit train_test_split einen Train/Test-Split macht (mit stratify=y und den übergebenen test_size und random_state).
      2. Einen KNeighborsClassifier(n_neighbors=k) auf X_train und y_train trainiert.
      3. Vorhersagen für X_test macht.
      4. Ein Tupel (y_test, y_pred) zurückgibt — beide als 1D-NumPy-Arrays oder Listen, gleiche Länge.

      Damit hast du den kompletten sklearn-Workflow in einer Funktion.

      Hinweise

        Aufgabe

        Macro-Metriken berechnen

        2 Punkte

        Schreibe macro_metrics(y_true, y_pred), die ein Dict zurückgibt mit den vier Schlüsseln 'accuracy', 'precision', 'recall', 'f1' — alle mit average='macro' (außer Accuracy, die hat keinen average-Parameter, sie ist global).

        Werte als float zwischen 0 und 1.

        Benutze accuracy_score und precision_recall_fscore_support aus sklearn.metrics. Der average='macro' ist genau das, was Assignment 10 (Aufgabe 1.4) verlangt — „use the macro avg".

        Hinweise

          Übungsquiz

          Phase 4 / 4

          Teste dein Verständnis in 15 Minuten mit 4 Frage(n), direkt an den HSG-Quizstolperfallen ausgerichtet.

          Quiz starten