Updates dieser Seite:

  • 15.04.2022: Anpassung in 3.1
  • 31.03.2022: Kleinere Korrekturen
  • 20.03.2022: Neues Semester

Überblick

In diesem Kapitel steigen wir in das Thema des Maschinellen Lernens ein. Wir sehen uns die vier Typen des Maschinellen Lernens an und betrachten dann ein Regressionsverfahren genauer. Wir implementieren das Verfahren der linearen Regression direkt in Python und lernen anschließend die Anwendung des bereits implementierten linearen Regressors in der Bibliothek Scikit-learn kennen.

Konzepte

Lineare Regression, Gradientenabstieg, Overfitting

Datensätze

Advertising

Importe

Wir importieren Libraries fürs Datenhandling (NumPy und Pandas) und zum Visualisieren (Matplotlib).

In [1]:
import warnings
warnings.filterwarnings('ignore')

import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt

1 Maschinelles Lernen

In diesem Kapitel wollen wir uns einen Überblick über einfache Methoden des Maschinellen Lernens (ML) verschaffen. Beim ML geht es immer darum, ein Modell durch Daten und Lernmechanismen zu optimieren.

1.1 Typen des Lernens

Wir unterscheiden fünf Typen des maschinellen Lernens:

  1. Überwachtes Lernen (Supervised Learning)
  2. Unüberwachtes Lernen (Unsupervised Learning)
  3. Semi-überwachtes Lernen (Semi-supervised Learning)
  4. Selbstüberwachtes Lernen (Self-supervised Learning)
  5. Bestärkendes Lernen (Reinforcement Learning)

Überwachtes Lernen (Supervised Learning)

Beim überwachten Lernen gibt es gelabelte Trainingsdaten, d.h. man hat Paare $(x^k, y^k)$ von Eingabefeatures $x^k$ und korrekter Ausgabe $y^k$. Die Ausgabe $y^k$ ist das jeweilige "Label" (oder die Klasse oder die Kategorie) des $k$-ten Trainingsbeispiels. Man benutzt die Trainingsdaten, um das Modell anzupassen und zu optimieren. Optimierung heißt, dass die Differenz (Fehler) zwischen Ausgabe des Modells und korrekter Ausgabe möglichst gering ist.

Hinweis: Wir verwenden den hochgestellten Index (wie in $w^k$), um ein einzelnes Trainingsbeispiel zu kennzeichnen. Es handelt sich nicht um Potenzierung.

In dieser Vorlesung geht es hauptsächlich um überwachtes Lernen.

Unüberwachtes Lernen (Unsupervised Learning)

Beim unüberwachten Lernen sind die Trainingsdaten ungelabelt, d.h. man hat nur Features $(x^k)$. Das Lernverfahren bildet aufgrund der Daten eigene Kategorien (Cluster) bzw. erlaubt es, die Features auf eine kleinere Menge von Features zu reduzieren, die besonders gut zur Trennung der Trainingsdaten geeignet sind.

Methodisch sind die Bereiche des Supervised Learning und des Unsupervised Learning sehr verschieden. Wir gehen im letzten Kapitel kurz auf Unsupervised Learning mit Neuronalen Netzen ein, um diesen Unterschied zu illustrieren.

Semi-überwachtes Lernen (Semi-supervised Learning)

Beim semi-überwachten Lernen ist sowohl eine eher geringe Menge gelabelter Daten gegeben sowie eine große Menge ungelabelter Daten. Mit Hilfe von Verfahren des unüberwachten Lernens versucht man, die Kategorien der gelabelten Daten auf die ungelabelten Beispiele zu übertragen.

Selbstüberwachtes Lernen (Self-supervised Learning)

Beim selbstüberwachten Lernen möchte auf ungelabelten Daten die Methoden des überwachten Lernens anwenden. Wie funktioniert das? Man nimmt eine Menge an Daten und "versteckt" einen bestimmten Aspekt. Zum Beispiel die Farbinformation bei Farbfotos. Die (vorhandene) Farbinformation wird zum Label erklärt und ein Mechanismus lernt (in diesem Beispiel), für Schwarzweiß-Fotos die Farbinformation vorherzusagen (Zhang et al. 2016).

Im Vergleich zum semi-überwachten Lernen hat man also keine Teilmenge an gelabelten Daten, sondern man nimmt die Labels aus den Daten selbst.

Obwohl die Ausgangslage (ungelabelte Daten) das selbstüberwachte Lernen in die Nähe des Unsupervised Learning rückt, sind die Methoden natürlich exakt die gleichen wie beim Supervised Learning.

Wen die Methode interessiert, kann ich folgenden Überblicksartikel von Lilian Weng empfehlen - auch mit Blick auf aktuelle Forschung: Self-Supervised Representation Learning. Außerdem gibt es ein ca. 1-stündiges Video Self-supervised Learning, ein Tutorial von Licheng Yu, Yen-Chun Chen und Linjie Li auf der CVPR2020-Konferenz.

Bestärkendes Lernen (Reinforcement Learning)

Beim bestärkenden Lernen werden Techniken des überwachten Lernens genutzt, um in einer Umwelt (repräsentiert durch einen Zustand) mit entsprechenden Umweltreizen (Belohnung/Bestrafung, engl. Reward) das Verhalten zu optimieren. Hier spricht man von einem Agenten. Das Modell bekommt als Input den aktuellen Zustand und gibt als Ausgabe die nächste Handlung (Aktion) aus. Die Belohnung/Betrafung wird für das Lernen verwendet und kann nach jeder einzelnen Aktion oder nach einer Reihe von Aktionen erfolgen.

Reinforcement Learning werden wir in dieser Vorlesung leider nicht behandeln können. Wenn Sie etwas dazu lesen wollen, kann ich das Standardwerk von Sutton und Barto (2018) empfehlen. Außerdem gibt es einen guten Online-Kurs: Reinforcement Learning - Goal Oriented Intelligence von DeepLizard.

1.2 Regression vs. Klassfikation

Wir gehen hier auf zwei einfache Verfahren des überwachtes Lernens ein:

  • Regression: Auf Basis von Eingabefeatures wird eine Zahl geschätzt (z.B. Kaufpreis)
  • Klassifikation: Auf Basis von Eingabefeatures wird ein Label vorhergesagt (z.B. Katze/Nicht-Katze)

Hier sehen wir schematisch ein Modell für Regression (oben) und eines für Klassifikation (unten):

Wir beginnen mit Regression.

2 Lineare Regression

Bei der linearen Regression trainieren wir ein Modell, das es erlaubt, anhand von Eingabedaten eine Zahl vorherzusagen.

Die Darstellung ist angelehnt an Material aus [Burkov 2019] und [Ng ML].

2.1 Problemstellung

Wir haben einen Datensatz von $N$ gelabelten Trainingsbeispielen $(x^{k}, y^{k})$ mit $k = 1, \ldots, N$. Der Index $k$ kennzeichnet ein Trainingsbeispiel.

Jedes $x \in \mathbb{R}^D$ ist ein $D$-dimensionaler Feature-Vektor und $y \in \mathbb{R}$ der entsprechende Zielwert. Da das $y$ ein echter, kontinuierlicher Wert ist, ist die Bezeichnung "gelabelte Beispiele" etwas irreführend, da es kein "Label" im Sinne einer Kategorie ist. "Gelabelt" bedeutet hier, dass der Zielwert bekannt ist.

Ein typisches Beispiel ist der Wert von Häusern. Der Vektor $x = (x_1, x_2, \ldots, x_D)$ repräsentiert verschiedene Haus-Eigenschaften wie Wohnfläche in qm, Anzahl der Zimmer und Größe des Grundstücks in qm. Der Wert $y$ repräsentiert den Kaufpreis, zum Beispiel in Tausend Euro (z.B. "321" für 321000 Euro).

Hinweis: Der tiefgestellte Index bezeichnet eine Komponente (einen Koeffizienten) eines Vektors oder einer Matrix (dort entsprechend zwei Indizes für Zeile und Spalte).

2.2 Modell und Parameter

Die Grundannahme ist jetzt, dass es eine ideale Funktion $h^*$ gibt, die für jeden möglichen Eigenschaftsvektor $x$ (Feature-Vektor) eines Hauses den "wahren" Wert des Hauses zurückgibt. Der Buchstabe "h" kommt von "Hypothese" bzw. hypothetische Funktion.

$h^*$ ist also eine Abbildung der Art

$$ h^*: \mathbb{R}^D \rightarrow \mathbb{R} $$

Gesucht ist jetzt ein sogenanntes Modell, das sich dieser idealen Funktion $h^*$ möglichst gut annähert. Wir nennen diese Modell $h$, das ist natürlich ebenfalls eine Funktion $h: \mathbb{R}^D \rightarrow \mathbb{R}$. Das $h$ steht für engl. hypothesis (Hypothese) und soll zeigen, dass die Funktion eine Vermutung über die Wirklichkeit darstellt.

Die Funktion $h$ beinhaltet eine Reihe von Parametern $w = (w_0, w_1, \ldots, w_D) \in\mathbb{R}^{D+1}$. Da $h$ von diesen Parametern abhängt, nennen wir die Funktion $h_w$. Wir lassen das kleine $w$ aber häufig aus Gründen der Lesbarkeit weg.

Hinweis: Das $w$ kommt von engl. weight (Gewicht) und deutet auf die spätere Verwendung bei Neuronalen Netzen hin. In der Literatur wird auch oft der griechische Buchstabe $\theta$ ("Theta") benutzt, um Parameter zu repräsentieren.

Im einfachsten Fall kann man $h_w(x)$ als Linearkombination der Komponenten von $x$ plus einer Konstanten $w_0$ auffassen:

$$ h_w (x) = w_0 + w_1 x_1 + \ldots + w_D x_D $$

Wenn $x$ kein Vektor, sondern ein Skalar ist, wird daraus eine einfache Gerade (lineare Funktion):

$$ h_w (x) = w_0 + w_1 x $$

wobei $w_0$ den y-Achsenabschnitt darstellt und $w_1$ die Steigung.

2.3 Lösung durch Optimierung

Wir suchen die optimalen Werte $w$. Was optimal genau bedeutet, legen wir über eine Zielfunktion fest, engl. objective function. Diese spiegelt entweder Fehler oder Kosten wider und muss dann minimiert werden, dann spricht man von einer Verlustfunktion, engl. loss function. Alternativ kann sie auch Nutzen oder den Gewinn angeben, dann muss sie maximiert werden. Eine solche Funktion nennt man Nutzenfunktion, engl. utility function. Beide Spielarten sind klassische Probleme der mathematischen Optimierung.

Wir messen den Fehler als Differenz zwischen unserem Modell $h_w$ und dem tatsächlichen Wert $y_k$ aus den Trainingsdaten. Genauer gesagt nehmen wir die Summe der quadratischen Fehler, teilen diese durch die Anzahl der Trainingsbeispiele und erhalten so den mittleren quadratischen Fehler, engl. mean squared error oder MSE:

$$ \frac{1}{N} \sum_{k=1}^N \left( y^k - h_w (x^k) \right)^2 $$

Warum wird das Quadrat genommen und nicht der Betrag? Weil die Quadratsfunktion differenzierbar ist, d.h. man kann die Ableitung bilden, um ein Verfahren namens Gradientenabstieg, engl. gradient descent, einzusetzen.

Die Betragsfunktion ist hingegen nicht differenzierbar, da sie nicht-stetig am Nullpunkt ist (auch gut erklärt unter https://de.wikipedia.org/wiki/Differenzierbarkeit).

Wir zeigen hier nochmal die beiden Funktionen um den Nullpunkt herum.

Betragsfunktion

In [2]:
x = np.arange(-10, 10, .1) # stellt eine Reihe von Werten von -10 bis 10 mit Schrittweite 0.1 als Numpy-Array her
y = abs(x) # wendet die Funktion elementweise auf die Werte an und produziert einen neuen Numpy-Array
plt.plot(x,y)
plt.xlabel('x')
plt.ylabel('y')
plt.title('Betragsfunktion')
plt.xticks([-10,-5,0,5,10])
plt.yticks([0,5, 10])
plt.show()

Sie sehen, dass die Betragsfunktion am Nullpunkt nicht differenzierbar ist. Was heißt differenzierbar überhaupt? Damit eine Funktion differenzierbar ist, muss man eine Tangente an jedem Punkt bestimmen können. Wenn Sie eine "Ecke" sehen, ist das nicht möglich.

In [3]:
x = np.arange(-10, 10, .1) # stellt eine Reihe von Werten von -10 bis 10 mit Schrittweite 0.1 als Numpy-Array her
y = x**2 # Schreibweise für Potenz
plt.plot(x,y)
plt.xlabel('x')
plt.ylabel('y')
plt.title('Quadratfunktion')
plt.xticks([-10,-5,0,5,10])
plt.yticks([0,50, 100])
plt.show()

Bei der Quadratfunktion gibt es eine Tangente am Nullpunkt, diese hätte die Steigung 0. Es lässt sich also zu $f(x) = x^2$ die Ableitung bilden, nämlich $f'(x) = 2x$.

Hinweis: Dass eine Funktion nicht differenzierbar ist, ist kein "Knock-out-Kriterium", wenn es um Gradientenabstieg geht. Es erleichtert aber die mathematischen Herleitungen, da man sonst Fallunterscheidungen einführen müsste. Im Bereich Neuronaler Netze, wo auch Gradientenabstieg verwendet wird, wird z.B. aktuell häufig die sogenannte ReLU-Funktion benutzt, die nicht-differenzierbar bei $x=0$ ist.

Als nächstes sehen wir uns ein Verfahren an, mit dem wir Parameter für ein Modell finden, das die Zielfunktion optimiert.

2.4 Gradientenabstieg

Eine Umsetzung von Optimierung kann durch ein Verfahren namens Gradientenabstieg (engl. gradient descent) geleistet werden. Wir illustrieren das Verfahren mit einem konkreten Beispiel in Python.

Daten

Für unser Python-Beispiel schauen wir hier den Datensatz "Ad spending" (Ausgaben für Werbeanzeigen) aus [Burkov 2019] an. Sie finden den Datensatz als Datei "Advertising.csv" in unserem Moodle-Kursraum (Verzeichnis "Daten").

Wir lesen die Daten aus einer CSV-Datei (CSV steht für comma-separated values) in einen Pandas-Dataframe. Pandas ist eine Python-Bibliothek. Ein Pandas-Dataframe ist einfach eine Tabelle. Eine Tabelle ist eine Matrix, die zusätzlich Zeilen- und Spaltenbeschriftungen hat:

In [4]:
df = pd.read_csv('data/Advertising.csv', index_col=0) # dataframe einlesen

Es handelt sich um Daten über Werbeausgaben (in Mill. USD) in den Bereichen TV, Radio, Zeitung einerseits und über die Einnahmen (sales, auch in Mill. USD) andererseits. Die Fragestellung ist: Steigert man die Einnahmen, wenn man mehr in Werbung investiert?

Werfen wir einen Blick in die Tabelle. Die ersten drei Spalten sind Werbeausgaben für verschiedene Medien. Wir nehmen uns das Medium radio vor, das sind also unsere Eingabewerte $x^k$. In der letzten Spalte sehen wir sales, dies sind die Einnahmen und somit unsere Ausgabewerte $y^k$.

In [5]:
df.head(5) # zeigt die ersten 5 Zeilen der Tabelle
Out[5]:
TV radio newspaper sales
1 230.1 37.8 69.2 22.1
2 44.5 39.3 45.1 10.4
3 17.2 45.9 69.3 9.3
4 151.5 41.3 58.5 18.5
5 180.8 10.8 58.4 12.9

Eine einzelne Spalte kann man sich über folgende "Punktnotation" holen. Man bekommt dann eine Datenstruktur vom Typ "Pandas Series", die man aber ähnlich wie eine Python-Liste behandeln kann. Einziger Unterschied: Der Index beginnt bei 1.

In [6]:
df.radio
Out[6]:
1      37.8
2      39.3
3      45.9
4      41.3
5      10.8
       ... 
196     3.7
197     4.9
198     9.3
199    42.0
200     8.6
Name: radio, Length: 200, dtype: float64

Visualisierung der Daten

Wir sehen uns mal einen Scatterplot für Radio vs. Sales an.

Man sieht direkt, dass es eine Korrelation zu geben scheint. Je mehr man in (Radio-)Werbung investiert, umso höher in der Regel auch die Einnahmen.

In [7]:
plt.scatter(df.radio, df.sales)
plt.xlabel('spendings radio')
plt.ylabel('sales')
plt.title('Sales as a function of ad spendings')
plt.show()

Modell

Als nächstes bilden wir ein Modell und versuchen dieses dann anhand der Daten in der Tabelle zu optimieren.

Das Modell für eine lineare Regression ist eine lineare Funktion $h$ der Form:

$$ h(x) = w_0 + w_1 x $$

wobei hier $w_0$ und $w_1$ Skalare sind. Die Funktion repräsentiert also eine Gerade mit Steigung $w_1$ und y-Achsenabschnitt $w_0$.

Unsere Beispieldaten schreiben wir in der Form $(x^k, y^k)$, wobei $x^k$ die Radio-Werbeausgaben darstellt und $y^k$ die Einnahmen. Index $k$ repräsentiert die Zeile in unserer Datentabelle. Wenn $N$ die Gesamtzahl der Daten (=Zeilen) bezeichnet, läuft Index $k$ also von $1$ bis $N$. Man schreibt auch $k \in {1, \ldots, N}$.

Wir definieren unsere Zielfunktion $L$ (L von engl. loss) als mittleren quadratischen Fehler (MSE). Jeder Fehler ist die quadrierte Differenz zwischen dem tatsächlichen Wert $y^k$ und dem errechneten Wert unseres Modells $h(x^k)$.

$$ \begin{align} L :&= \frac{1}{N} \sum_{k=1}^N \left( y^k - h(x^k) \right)^2 \\[2mm] &= \frac{1}{N} \sum_{k=1}^N \left( y^k - (w_0 + w_1 x^k) \right)^2 \end{align} $$

Für das Verfahren des Gradientenabstiegs berechnen wir für jeden der zwei Parameter die partielle Ableitung. Hier kommt die Kettenregel zum Einsatz.

$$ \begin{align} \frac{\partial L}{\partial w_0} & = \frac{1}{N} \sum_{k=1}^N - 2\left( y^k - (w_0 + w_1 x^k) \right) \\[2mm] \frac{\partial L}{\partial w_1} & = \frac{1}{N} \sum_{k=1}^N - 2x^k\left( y^k - (w_0 + w_1 x^k) \right) \end{align} $$

Jetzt setzen wir wieder $h(x^k)$ ein:

$$ \begin{align} \frac{\partial L}{\partial w_0} & = \frac{1}{N} \sum_{k=1}^N - 2\left( y^k - h(x^k) \right) \\[2mm] \frac{\partial L}{\partial w_1} & = \frac{1}{N} \sum_{k=1}^N - 2x^k\left( y^k - h(x^k) \right) \end{align} $$

Beachten Sie, dass aufgrund der Summierung alle Trainingsbeispiele zur Berechnung dieser Ableitungen durchlaufen werden müssen.

Training

Das Training führt jetzt die Optimierung durch. Das Ziel ist, durch schrittweise Anpassung - also Updates der Parameter - die optimalen Werte für $w_0$ und $w_1$ zu finden. Zu Beginn initialisieren wir die Parameter wie folgt:

$$w_0 := 0 \\[2mm] w_1 := 0 $$

Das Training vollzieht sich in Runden, die wir Epochen nennen. Das Update der Parameter $w_0$ und $w_1$ geschieht mit Hilfe der jeweiligen partiellen Ableitung am Ende einer Epoche:

$$ \begin{align} w_0 & := w_0 - \alpha \frac{\partial L}{\partial w_0}\\[2mm] w_1 & := w_1 - \alpha \frac{\partial L}{\partial w_1} \end{align} $$

Beachten Sie, dass in jeder Epoche erst alle Trainingsbeispiele durchlaufen werden müssen, um obige Ableitungen zu errechnen. Daher kann erst am Ende der Epoche das Update der Parameter durchgeführt werden.

Da die Ableitungen in eine Richtung zeigen, die den Wert vergrößern, müssen wir die Ableitungen negieren, daher das Minuszeichen.

Außerdem möchten wir kontrollieren, wie stark wir die Parameter pro Update ändern - manchmal nennt man das die Schrittweite. Das wird über die Lernrate $\alpha$ gesteuert, ein Wert $\in (0, 1)$, der als Faktor die Ableitung modifiziert. Je höher $\alpha$ ist, umso schneller wird gelernt. Ein zu hoher Wert birgt aber die Gefahr, ein Minimum zu überspringen und im schlimmsten Fall um das Minimum herum zu oszillieren.

Für die Wahl der Lernrate gibt es keine allgemeingültige Empfehlung, in der Praxis zeigt sich aber, dass Werte um $0.1$ bis $0.3$ gute Ausgangspunkte sind. Teilweise kann die Lernrate aber auch deutlich niedriger ausfallen; wir verwenden später zum Beispiel $\alpha = 0.001$. Die Lernrate wird im einfachsten Fall empirisch festgestellt.

Hinweis: Empirisch heißt "aus Erfahrung" - man kann auch einfach sagen: Trial and Error.

Die Lernrate ist ein Beispiel für einen Hyperparameter. Ein Hyperparameter ist ein Parameter, der während des Trainings nicht verändert wird, wohingegen unsere "normalen" Parameter $w_0$ und $w_1$ ja während des Trainings fortlaufend angepasst werden.

Trainingsprozess

Hier sehen wir den Trainingsprozess schematisch. Innerhalb einer Epoche werden alle $N$ Trainingsdaten durchlaufen und der jeweilige Summand für die zwei partiellen Ableitungen aufgesammelt. Dies fließt am Ende der Epoche in die Berechnung des Gradienten ein (Summation und Berechnung des Durchschnitts). Der Gradient dient wiederum dazu, die Parameter anzupassen, also ein Update durchzuführen.

2.5 Evaluation

Jetzt haben wir unser Modell mit unseren Trainingsdaten angepasst. Wie bewerten wir, ob unser Modell "gut" ist?

Im Fall der Regression können wir einfach die Zielfunktion $L$ als Maß für den Fehler unseres Modells verwenden. Ein Modell ist also gut, wenn der Fehler möglichst niedrig ist.

Zunächst aber trennen wir unsere Daten sauber (also ohne Überlappung, man nennt das auch disjunkt) in

  1. Trainingsdaten (z.B. 80% der Daten)
  2. Testdaten (z.B. 20% der Daten)

Das Modell darf nur mit Trainingsdaten trainiert werden, so dass das Modell die Testdaten nie "sieht".

Wir bestimmen dann die Güte des Modells, indem wir den Fehler auf den Testdaten berechnen. Das ist unsere Evaluation des Modells.

Wenn die jeweiligen Fehlerwerte auf Trainings- und Testdaten ähnlich sind, dann generalisiert das Verfahren gut für ungesehene Daten. Sind die Werte sehr unterschiedlich (niedriger Fehler bei Trainingsdaten, hoher Fehler bei Testdaten), dann liegt wahrscheinlich ein Overfitting vor. Dazu später mehr.

3 Lineare Regression implementieren

3.1 Umsetzung in Python

Zunächst sehen wir uns an, wie man lineare Regression von Grund auf in Python implementiert. Oft versteht man so am besten, wie die Theorie in die Praxis übertragen wird.

Funktionen

Wir schreiben eine Funktion update, die die Parameter $w_0$ und $w_1$ einmalig updatet, also für eine Epoche. Dazu müssen wir die zwei partiellen Ableitungen wie oben gezeigt berechnen, diese werden in den Variablen dLw1 und dLw0 gespeichert.

Zur Erinnerung:

$$ \begin{align} \frac{\partial L}{\partial w_0} & = \frac{1}{N} \sum_{k=1}^N - 2\left( y^k - (w_0 + w_1 x^k) \right) \\[2mm] \frac{\partial L}{\partial w_1} & = \frac{1}{N} \sum_{k=1}^N - 2x^k\left( y^k - (w_0 + w_1 x^k) \right) \end{align} $$

Sie sehen, dass wir in einer Schleife alle Trainingsbeispiele durchlaufen, um die Summe zu berechnen. Erst beim Update wird das $1/N$ durchgeführt. Dies entspricht einer Epoche.

In [8]:
# Eingabe: x-Werte, y-Werte, Parameter und Lernrate

def update(spendings, sales, w1, w0, alpha):
    
    # Schritt 1: Ableitungen berechnen
    dLdw1 = 0
    dLdw0 = 0
    N = len(spendings)
    for i in range(1, N+1):
        dLdw1 += -2 * spendings[i] * (sales[i] - (w1 * spendings[i] + w0))
        dLdw0 += -2 * (sales[i] - (w1 * spendings[i] + w0))
        
    # Schritt 2: Updates durchführen
    w1 = w1 - (1/float(N)) * dLdw1 * alpha
    w0 = w0 - (1/float(N)) * dLdw0 * alpha
    
    return w1, w0

Jetzt definieren wir die Funktion train, die das Training in mehreren Epochen durchführt.

Wir speichern in jeder Epoche den Fehlerwert (Loss) und die Parameterwerte in einer Liste, der Historie. Außerdem geben wir alle 100 Epochen den Zwischenstand auf der Konsole aus. Das History-Objekt geben wir am Ende des Funktionscodes zurück.

In [9]:
# Eingabe: x-Werte, y-Werte, Parameter, Lernrate und Anzahl der Epochen

def train(spendings, sales, w1, w0, alpha, epochs):
    history = []
    for e in range(epochs+1):
        # Historie speichern
        l = loss(spendings, sales, w1, w0)
        history.append((l, w1, w0))
        if e % 100 == 0:
            print(f"epoch: {e:4}  loss: {l:7.3f} w1={w1:.3f} w0={w0:.3f}")
        w1, w0 = update(spendings, sales, w1, w0, alpha)
    return history

Es fehlt noch die oben verwendete Funktion loss, die den aktuellen Fehler (MSE) berechnet. Auch hier müssen alle Trainingsbeispiele in einer Schleife durchlaufen werden.

Hinweis: In Python bedeutet der Doppelstern "potenzieren".

In [10]:
# Eingabe: x-Werte, y-Werte und Parameter

def loss(spendings, sales, w1, w0):
    N = len(spendings)
    error = 0
    
    # summiere quadratischen Fehler auf
    for i in range(1, N+1):
        error += (sales[i] - (w1 * spendings[i] + w0))**2
    
    # gib Mittelwert zurück
    return error / float(N)

Training

Jetzt können wir trainieren. Wir initialisieren unsere Parameter mit 0 und wählen eine sehr niedrige Lernrate von 0.001. Es sollen 3000 Epochen durchlaufen werden. Wir speichern die zurückgegebene Historie.

In [11]:
history = train(df.radio, df.sales, 0, 0, .001, 3000)
epoch:    0  loss: 223.716 w1=0.000 w0=0.000
epoch:  100  loss:  40.303 w1=0.471 w0=0.536
epoch:  200  loss:  37.885 w1=0.456 w0=1.027
epoch:  300  loss:  35.730 w1=0.442 w0=1.491
epoch:  400  loss:  33.809 w1=0.428 w0=1.929
epoch:  500  loss:  32.098 w1=0.416 w0=2.343
epoch:  600  loss:  30.573 w1=0.404 w0=2.733
epoch:  700  loss:  29.214 w1=0.393 w0=3.101
epoch:  800  loss:  28.003 w1=0.382 w0=3.449
epoch:  900  loss:  26.924 w1=0.372 w0=3.778
epoch: 1000  loss:  25.963 w1=0.362 w0=4.088
epoch: 1100  loss:  25.106 w1=0.353 w0=4.380
epoch: 1200  loss:  24.342 w1=0.345 w0=4.656
epoch: 1300  loss:  23.662 w1=0.337 w0=4.917
epoch: 1400  loss:  23.055 w1=0.329 w0=5.163
epoch: 1500  loss:  22.515 w1=0.322 w0=5.396
epoch: 1600  loss:  22.033 w1=0.316 w0=5.615
epoch: 1700  loss:  21.604 w1=0.309 w0=5.822
epoch: 1800  loss:  21.222 w1=0.303 w0=6.017
epoch: 1900  loss:  20.881 w1=0.298 w0=6.202
epoch: 2000  loss:  20.577 w1=0.292 w0=6.376
epoch: 2100  loss:  20.307 w1=0.287 w0=6.541
epoch: 2200  loss:  20.066 w1=0.283 w0=6.696
epoch: 2300  loss:  19.851 w1=0.278 w0=6.842
epoch: 2400  loss:  19.659 w1=0.274 w0=6.981
epoch: 2500  loss:  19.489 w1=0.270 w0=7.111
epoch: 2600  loss:  19.337 w1=0.266 w0=7.234
epoch: 2700  loss:  19.201 w1=0.263 w0=7.351
epoch: 2800  loss:  19.081 w1=0.259 w0=7.461
epoch: 2900  loss:  18.973 w1=0.256 w0=7.564
epoch: 3000  loss:  18.877 w1=0.253 w0=7.662

Sanity check: Wir schauen uns die ersten 5 Einträge der Historie an. Jeder Eintrag sollte ein 3-Tupel mit Loss, w1 und w0 enthalten.

In [12]:
history[:5]
Out[12]:
[(223.71625000000003, 0, 0),
 (92.32078294903626, 0.7412639000000002, 0.028045000000000007),
 (56.427829727692455, 0.35370226699746, 0.0215443832608),
 (46.607920811576825, 0.555954800831779, 0.03308923541542059),
 (43.90620467079716, 0.4500281747738891, 0.03520059197148873)]

Visualisierung

Wir können die Plotfunktion der Matplotlib-Bibliothek nutzen, um die Fehlerentwicklung über die Epochen zu visualisieren.

Dazu benötigen wir eine Liste der Loss-Werte. Wir wenden List Comprehension an.

In [13]:
losses = [x[0] for x in history]

losses[:5] # Sanity check
Out[13]:
[223.71625000000003,
 92.32078294903626,
 56.427829727692455,
 46.607920811576825,
 43.90620467079716]

Jetzt der Plot. Die Plotfunktion benötigt für die Werte zwei Listen, eine für die x-Werte, eine für die y-Werte.

Für die x-Achse erzeugen wir eine einfache Liste der Form (1, 2, ..., 30).

In [14]:
erange = range(1, len(losses)+1)

plt.plot(erange, losses, label='Loss-Entwiclung')
plt.ylabel("Loss")
plt.xlabel("Epochen")
plt.show()

Finale (optimale) Parameterwerte

Der letzte Eintrag der Historie enthält hier die optimalen Parameterwerte, die wir zwischenspeichern.

In [15]:
last = history[len(history)-1]

w1opt = last[1]
w0opt = last[2]

w1opt, w0opt
Out[15]:
(0.25297222070703446, 7.662103559639288)

Vorhersagen

Jetzt kann man die berechneten Parameter $w_0$ und $w_1$ verwenden, um Vorhersagen zu treffen. Wir möchten wissen, welche Verkaufszahl zu erwarten ist, wenn wir einen bestimmten Betrag für Werbung ausgeben.

Dazu definieren wir die Funktion predict, die einfach die Funktion $h$ auf einem beliebigen $x$ mit den angegebenen Parametern anwendet.

In [16]:
def predict(x, w1, w0):
    return w1*x + w0

Wir fragen uns, was bei einer Werbeinvestition von 35 Mill USD für ein Umsatz zu erwarten ist. Oben hat unser Algorithmus die optimalen Parameter berechnet, die wir jetzt in unsere Vorhersagefunktion einsetzen:

In [17]:
predict(35, w1opt, w0opt)
Out[17]:
16.516131284385494

Schauen Sie oben im Scatterplot bei 35 (auf der x-Achse) nach, ob die Vorsage plausibel ist.

Visualisierung der Regressionsgeraden

Jetzt schauen wir uns nochmal den Scatterplot von oben an und zeichnen die Gerade ein, die sich aus den berechneten Parametern ergibt. Dazu zeichnen wir eine Linie von x=0 bis x=50 und berechnen die y-Werte mit Hilfe unserer Funktion predict.

Das tun wir in folgender Funktion:

In [18]:
def plot_scatter_regress(w1, w0, title):
    plt.scatter(df.radio, df.sales)
    plt.plot([0, 50], [predict(0, w1, w0), predict(50, w1, w0)], 'r-') # Regressionsgerade
    plt.xlabel('spendings radio')
    plt.ylabel('sales')
    plt.title(title)
    plt.show()

Jetzt zeichnen wir Scatterplot und Regressionsgerade:

In [19]:
plot_scatter_regress(w1opt, w0opt, 'Sales as a function of ad spendings')

Visualisierung der Entwicklung

Im Rückblick schauen wir uns an, wie sich die Regressionsgerade in den ersten 10 Epochen des Trainings entwickelt. Man sieht gut, wie die Gerade zu Beginn große Sprünge macht und sich in späteren Epochen nur noch wenig verändert.

In [20]:
e = 0 # Epochs
erange = range(0, 10)

for i in erange:
    w1 = history[i][1]
    w0 = history[i][2]
    plot_scatter_regress(w1, w0, f'Epoch {i} Loss={history[i][0]:.2f}')

Varianten des Gradientenabstiegs

Unsere Variante des Gradientenabstiegs kann sehr langsam sein, insbesondere bei großen Datenmengen. Das liegt daran, dass wir in jeder Epoche erst durch alle Trainingsbeispiele durchlaufen, bevor wir die Parameter updaten. Ein weiterer Faktor ist die Lernrate $\alpha$. Manchmal ist es gut, eine sehr niedrige Lernrate zu haben (in der Nähe des Minimums), manchmal ist eine höhere Lernrate besser, um schneller in die Nähe des Minimums zu kommen.

In den weiteren Kapiteln werden Sie Varianten wie stochastic gradient descent (SGD) und Minibatch kennen lernen. Außerdem werden wir bei den Neuronalen Netzen über Verfahren sprechen, die Lernrate adaptiv zu gestalten (Momentum, Adagrad, Adam ...).

3.2 Umsetzung mit Scikit-learn

Oben haben wir lineare Regression selbst implementiert, aber natürlich gibt es das Verfahren schon in vielen Bibliotheken. Für die Praxis ist es nicht sinnvoll, eigene Implementierungen zu verwenden, da vorhandene Bibliotheken in der Regel von professionellen Entwicklern geschrieben und von vielen Experten auch im Praxiseinsatz getestet wurden, also deutlich zuverlässiger/robuster sind als Eigenentwicklungen.

Ein guter Grund, eigene Implementierungen vorzunehmen, ist allerdings der persönliche Lerneffekt. Oft versteht man ein Verfahren erst dann, wenn man es selbst implementiert hat. Ein weiterer Grund ist natürlich die Forschung, d.h. wenn man bekannte Verfahren modifizieren oder neue Verfahren entwickeln möchte.

Wir möchten jetzt statt der Eigenentwicklung oben eine erprobte Bibliothek nutzen. Wir verwenden die scikit-learn, eine Python-Bibliothek, die in den letzten Jahren zum Quasi-Standard für das Lernen und Entwickeln im Bereich Machine Learning geworden ist.

Daten

Wir nehmen uns aus der Tabelle die Spalte radio und bilden sie zu einem 2-dimensionalen Array um, weil der Mechanismus von Scikit-learn die Eingabe in dieser Form benötigt. Grund: Es könnten mehrere Eingabewerte pro Trainingsbeispiel vorliegen.

In [21]:
x_train = df.radio.to_numpy() # Spalte zu NumPy-Array machen
x_train = x_train.reshape(-1, 1) # zu 2-dimensionalen Array umbilden (N Zeilen, 1 Spalte)
x_train[:5] # Testausgabe der ersten 5 Elemente
Out[21]:
array([[37.8],
       [39.3],
       [45.9],
       [41.3],
       [10.8]])

Die Zielwerte müssen in keine neue Form gebracht werden. Da nehmen wir einfach die Spalte sales. Wir müssen sie nur in einen Array umwandeln.

In [22]:
df.sales.array[:5] # Spalte als Array => Testausgabe der ersten 5 Elemente
Out[22]:
<PandasArray>
[22.1, 10.4, 9.3, 18.5, 12.9]
Length: 5, dtype: float64

Modell

Jetzt erstellen wir ein Modell, indem wir die Klasse LinearRegression instanziieren.

In [23]:
from sklearn.linear_model import LinearRegression

model = LinearRegression() # neues Modell

Training

Das Training wird in der Methode fit geleistet ("to fit" im Sinne von "anpassen").

In [24]:
model.fit(x_train, df.sales.array) # Training mit Eingabearray und Zielwerten
Out[24]:
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None, normalize=False)

Vorhersagen

Mit der Methode predict können wir jetzt Vorhersagen treffen. Oben hatten wir uns gefragt, was ist bei einer Werbeinvestition von 35 Mill USD für ein Umsatz zu erwarten ist.

In [25]:
model.predict([[35]])
Out[25]:
array([16.39899051])

Vergleichen Sie den Wert mit "unserem" Wert oben.

3.3 Overfitting

In den obigen Versuchen haben wir mit einer sehr einfachen linearen Funktion gearbeitet:

$$ h(x) = w_0 + w_1 x $$

Natürlich könnte man noch weitere Koeffizienten und Potenzen von x hinzunehmen, um so eine Kurve zu erzeugen, die sich den Daten besser anschmiegt.

$$ \begin{align} h(x) & = w_0 + w_1 x + w_2 x^2 \\[1mm] h(x) & = w_0 + w_1 x + w_2 x^2 + w_3 x^3 \\[1mm] & \vdots\\[1mm] h(x) & = w_0 + w_1 x + \ldots + w_n x^n \end{align} $$

In der folgenden Abbildung sehen wir drei Modelle für dieselben Trainingsdaten.

Beim ersten Modell könnten man von Underfitting sprechen, d.h. der Fehler, den das Modell bei Vorhersagen - selbst auf den Trainingsdaten - macht, ist relativ groß. Ganz grob kann man sagen, dass das Modell nicht komplex genug ist, was man an einer geringen Anzahl von Parametern erkennt. Zum Beispiel bei einem linearen Modell, das nur zwei Parameter $w_0$ und $w_1$ aufweist. Ein weiterer Grund könnte die geringe Aussagekraft der Eingabe-Features sein, dann müsste man die Auswahl der Features anpassen.

Beim ganz rechts abgebildeten Modell spricht man von Overfitting. Das Modell (vielleicht ein Polynom hoher Ordnung) deckt zwar alle Trainingsdaten sehr genau ab, man könnte aber vermuten, dass das Modell bei neuen (ungesehenen) Daten eher schlecht abschneidet, weil es die natürlichen Schwankungen der Trainingsdaten zu genau abbildet. Man spricht auch davon, dass das Modell nicht ausreichend generalisiert.

Ganz grob kann man sagen: Overfitting wird oft dadurch verursacht, dass das Modell zu viele Parameter hat, die man zu lange trainiert (also über zu viele Epochen). Ob ein Modell "zu viele" Parameter hat, hängt auch mit der Menge der Trainingsdaten zusammen. Je weniger Daten man hat, umso weniger Parameter sollte auch das Modell haben. Wichtige Maßnahmen gegen Overfitting sind also

  1. Parameter reduzieren
  2. Trainingsdauer (Epochen) reduzieren
  3. Regularisierung

Regularisierung werden wir bei den Neuronalen Netzen noch kennenlernen. Es handelt sich dabei um die Einführung eines zusätzlichen Terms in der Fehlerfunktion, um Extremwerte bei den Parametern zu "bestrafen".

Die mittlere Abbildung scheint ein guter Mittelweg zu sein. Die Trainingsdaten werden mit relativ geringem Fehler abgedeckt. Gleichzeitig hat das Modell das Potential, neue Daten gut vorherzusagen. Man sagt, das Modell generalisiert gut.

Wie stellen Sie fest, ob ein Overfitting vorliegt? Wie bereits erwähnt, evaluieren Sie Ihr Modell auf den (ungesehenen) Testdaten. Wenn Ihr Modell auf den Testdaten schlecht abschneidet, obwohl es auf den Trainingsdaten sehr gut abschneidet, dann liegt in der Regel ein Overfitting vor.

4 Literatur

Burkov, Andriy (2019) The Hundred-Page Machine Learning Book (auch online lesbar)

Sutton, Richard S.; Barto, Andrew G. (2018) Reinforcement Learning: An Introduction, 2nd Edition, Bradford Books.

Zhang, Richard; Isola, Phillip; Efros, Alexei A. (2016) Colorful Image Colorization. In: European conference on computer vision | arXiv:1603.08511