{ "cells": [ { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "# Data Science 1 (k-Means Clustering)\n", "\n", "In den letzten beiden Vorlesungsthemen setzen wir uns mit Data Science auseinander. Data Science beruht auf Maschinellem Lernen, welches sich grob in drei Klassen unterteilen lässt: Unsupervised Learning, Supervised Learning und Reinforcement Learning. Wir beschäftigen uns in diesem Tutorium spezifisch mit Unsupervised Learning, ehe wir uns in der kommenden Woche mit Supervised Learning auseinandersetzen. \n", "\n", "Das Tutorium in dieser Woche besteht aus zwei Jupyter Notebooks. In diesem Notebook behandeln wir zuerst k-Means Clustering, ehe wir uns im zweiten Notebook mit Hierarchischen Clustering beschäftigen. Wir empfehlen Ihnen, die Notebooks Aufgabe für Aufgabe durchzuarbeiten, da viele Aufgaben auf vorherigen Aufgaben aufbauen.\n", "\n", "In den Data Science Tutorien benutzen wir verstärkt externe Bibliotheken für die Implementierung der Inhalte. Wir empfehlen Ihnen, sich mit diesen Bibliotheken (insbeonsdere Numpy, Pandas, scikit-learn und matplotlib) auseinanderzusetzen. Das ist allerdings keine Voraussetzung für die Prüfungsleistungen in ISDA.\n", "\n", "Hinweis: Aufgaben, die durch einen Asterisk (*) markiert sind, sind Bonusaufgaben. Diese Aufgaben können im Tutorium behandelt werden, dies ist jedoch von den Übungsleitern nicht geplant." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "import copy\n", "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import pandas as pd\n", "from sklearn.cluster import KMeans\n", "\n", "# Matplotlib parameters\n", "plt.rcParams[\"figure.dpi\"] = 150\n", "plt.rcParams[\"figure.figsize\"] = [4, 3]\n", "plt.rcParams[\"font.size\"] = 8" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "## Aufgabe 1: k-Means von Hand\n", "\n", "### Datenpunkte\n", "Zuerst generieren wir 8 Datenpunkte, welche wir als `toy_data` abspeichern. " ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "toy_data = pd.DataFrame({\"x\": [2, 2, 4, 4, 6, 6, 8, 8], \"y\": [2, 4, 2, 4, 6, 8, 6, 8]})\n", "toy_data.style.hide(axis=\"index\")" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "### Start-Centroids\n", "Zusätzlich generieren wir 2 Start-Centroids. " ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "centroids = pd.DataFrame({\"x\": [2, 4], \"y\": [1, 1]})\n", "centroids.style.hide(axis=\"index\")" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "### Startzustand visualisiert\n", "\n", "**Aufgabe:** Führen Sie auf dem Datensatz `toy_data` k-Means aus, bis die Centroids konvergiert sind. " ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "ax = toy_data.plot.scatter(x=\"x\", y=\"y\", c=\"black\", figsize=(3, 3))\n", "centroids.plot.scatter(x=\"x\", y=\"y\", c=[\"red\", \"blue\"], ax=ax, marker=\"x\")\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "## Aufgabe 2: k-Means implementieren\n", "Ihre Aufgabe ist nun, k-Means selbst zu implementieren. Hierzu werden Sie Code-Gerüste (siehe unten) ergänzen.\n", "\n", "Gegeben ist eine Funktion, die ein Clustering visualisiert (siehe unten), und eine Funktion, die zufällige initiale Centroids generiert.\n", "Weitere Informationen sind im Code als Kommentare enthalten.\n", "\n", "Sie können Ihren Code zunächst mit dem Beispiel aus der Handsimulation testen (`toy_data`). Weiter unten führen wir einen größeren Datensatz mit 500 Datenpunkten ein." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "def visualize_kmeans(data, centroids, assignments, figsize=(2.5, 2.5)):\n", " \"\"\"\n", " Visualisiert ein 2-dimensionales k-Means Clustering (Spaltennamen \"x\" und \"y\"). Die Funktion zeichnet einen Plot,\n", " der die Datenpunkte und die Centroids enthält. Datenpunkte sind nach Centroid-Zuordnung gefärbt.\n", "\n", " Argumente:\n", " data: eine Matrix mit Dimension N x 2. Jede Zeile enthält einen von N Datenpunkten.\n", " centroids: eine Matrix mit Dimension K x 2. Jede Zeile enthält einen von K Centroids.\n", " assignments: Ein Vektor mit N Zeilen. Jede Zeile enhält die Cluster-Zuordnung für den jeweiligen Datenpunkt (0 bis M-1).\n", " figsize: Ein Tupel mit der Größe des Plots (default: (3, 3)).\n", " \"\"\"\n", " plt.figure(figsize=figsize)\n", "\n", " all_data = data.copy()\n", " all_data[\"assignments\"] = assignments.astype(\"int\")\n", " all_data[\"assignments\"] = all_data[\"assignments\"].astype(\"category\")\n", "\n", " groups = all_data.groupby(\"assignments\", observed=False)\n", " for centroid, group in groups:\n", " plt.plot(\n", " group[\"x\"],\n", " group[\"y\"],\n", " marker=\"o\",\n", " linestyle=\"\",\n", " label=centroid,\n", " zorder=1,\n", " markersize=5,\n", " )\n", " plt.scatter(\n", " centroids[centroid, 0],\n", " centroids[centroid, 1],\n", " marker=\"s\",\n", " edgecolors=\"black\",\n", " s=40,\n", " zorder=2,\n", " )\n", "\n", " plt.legend()\n", " plt.grid(visible=True, which=\"both\", axis=\"both\")\n", " plt.xlabel(\"x\")\n", " plt.ylabel(\"y\")\n", " plt.title(str(centroids.shape[0]) + \" clusters\")\n", " plt.tight_layout()\n", " plt.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "def random_centroids(data, count):\n", " \"\"\"Generiert zufällige Centroids für k-Means aus den Datenpunkten.\"\"\"\n", " mean = np.mean(data, axis=0)\n", " var = np.var(data, axis=0)\n", " centroids = np.zeros((count, 2))\n", " for i in range(count):\n", " centroids[i][0] = mean.iloc[0] + np.random.uniform(-1, 1) * var.iloc[0]\n", " centroids[i][1] = mean.iloc[1] + np.random.uniform(-1, 1) * var.iloc[1]\n", " return centroids" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "### Aufgabe 2.1: Ihre Implementierung\n", "Gegeben sei der vorherige Datensatz und ein zufälliger Startzustand zweier Centroids. Den Startzustand lassen wir im unten stehenden Codeblock generieren und lassen dann erneut den Datensatz als auch die Centroids visualisieren. \n", "\n", "Die k-Means-Implementierung basiert auf zwei zentralen Funktionen: `assign_to_centroids()` und `new_centroids()`. Unten finden Sie Beschreibungen und Gerüste für beide Funktionen. Ihre Aufgabe ist es, diese zu vervollständigen." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "# Generierung zufälliger Centroids\n", "num_centroids = 2\n", "initial_centroids = random_centroids(toy_data, num_centroids)\n", "\n", "# Zufällige initiale Zuordnungen\n", "initial_assignments = np.random.randint(num_centroids, size=toy_data.shape[0])\n", "\n", "visualize_kmeans(toy_data, initial_centroids, initial_assignments)" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "Nun müssen Sie noch die beiden Funktionen `assign_to_centroids()` und `new_centroids()` implementieren, um eine Iteration von k-Means durchführen zu können." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "def assign_to_centroids(data, centroids):\n", " \"\"\"\n", " Berechnet für gegebene Datenpunkte und Centroids neue Centroid-Zuordnungen.\n", "\n", " Argumente:\n", " data: eine Matrix mit Dimension N x 2. Jede Zeile enthält einen von N Datenpunkten.\n", " centroids: eine Matrix mit Dimension M x 2. Jede Zeile enthält einen von M Centroids.\n", "\n", " Rückgabe:\n", " Ein Vektor mit N Zeilen. Jede Zeile enhält die Cluster-Zuordnung für den jeweiligen Datenpunkt (0 bis M-1).\n", " \"\"\"\n", " assignments = np.zeros((data.shape[0],))\n", "\n", " # Hilfestellung (muss nicht verwendet werden):\n", " for i in range(data.shape[0]):\n", " distance = np.zeros((centroids.shape[0], 1))\n", " for j in range(centroids.shape[0]):\n", " # your code here\n", " print(\"Noch nicht implementiert\")\n", "\n", " return assignments\n", "\n", "\n", "# Potentiell hilfreiche Funktionen:\n", "# np.linalg.norm(point1 - point2) -- berechnet die Distanz zwischen zwei Punkten\n", "# np.argmin() -- bestimmt den Index, dessen Wert minimal ist entlang einer Dimension (0 or 1 in unserem Fall)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "def new_centroids(data, old_centroids, assignments):\n", " \"\"\"\n", " Berechnet für gegebene Datenpunkte und Centroid-Zuordnungen neue Centroids.\n", "\n", " Argumente:\n", " data: eine Matrix mit Dimension N x 2. Jede Zeile enthält einen von N Datenpunkten.\n", " old_centroids: eine Matrix mit Dimension M x 2. Jede Zeile enthält einen M Centroids.\n", " assignments: ein Vektor mit N Zeilen. Jede Zeile enthält die Zuordnung für einen Datenpunkt (0 bis M-1).\n", "\n", " Rückgabe:\n", " eine Mx2 Matrix. Jede Zeile enthält die (neuen) Koordinaten eines Centroids.\n", " \"\"\"\n", " centroids = old_centroids\n", " for i in range(old_centroids.shape[0]):\n", " # your code here\n", " print(\"Noch nicht implementiert\")\n", "\n", " return centroids\n", "\n", "\n", "# Potentiell hilfreiche Funktionen:\n", "# np.mean() -- berechnet den Mittelwert entlang der angegebenen Dimension (0 oder 1 in unserem Fall)" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "Mit diesem Startzustand können Sie nun eine Iteration von k-Means durchführen." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "# Eine k-Means-Iteration\n", "assignments = assign_to_centroids(toy_data, initial_centroids)\n", "centroids = new_centroids(toy_data, initial_centroids, assignments)\n", "visualize_kmeans(toy_data, centroids, assignments)" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "### Aufgabe 2.2: k-Means auf größerem Datensatz ausführen\n", "Wir laden nun einen größeren Datensatz. Ihre Aufgabe ist es, mit Ihrer k-Means Implementierung einige k-Means-Iterationen auf diesem Datensatz laufen zu lassen. Visualisieren Sie das Clustering zu Beginn und nach jeder Iteration. Wie viele Cluster halten Sie für sinnvoll?" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "# Datensatz laden\n", "data = pd.DataFrame(\n", " np.loadtxt(\"../resources/10_data_science/cluster.dat\", dtype=\"float64\").T,\n", " columns=[\"x\", \"y\"],\n", ")\n", "print(\n", " f\"Dimension des Datensets: {data.shape[0]} Zeilen, {data.shape[1]} Spalten mit Shape {data.shape}\"\n", ")\n", "data.head()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "# your code here" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "### Aufgabe 2.3: Mehrere Iterationen von k-Means*\n", "Ergänzen Sie unten die Funktion `run_kmeans()`, um das Ausführen mehrerer k-Means-Iterationen zu automatisieren. Die Funktion soll eine gegebene Zahl von k-Means-Iterationen durchführen." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "def run_kmeans(data, centroids, iterations):\n", " \"\"\"\n", " Führt eine gegebene Zahl von k-Means-Iterationen durch.\n", "\n", " Argumente:\n", " data: Datenset (Nx2)\n", " centroids: Initiale Centroids (Kx2)\n", " iterations: Zahl der durchzuführenden Iterationen\n", "\n", " Rückgabe:\n", " Eine Liste von neuen Centroids\n", " \"\"\"\n", "\n", " # your code here\n", " print(\"Noch nicht implementiert\")\n", "\n", " return centroids" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "### Aufgabe 2.4*: k-Means mit Stop-Kriterium\n", "Da die Zahl der benötigten Iterationen in der Regel vor dem Clustering nicht bekannt ist, wird für k-Means oft mit einem Stop-Kriterium gearbeitet. Ihre Aufgabe ist nun, eine Variante der Funktion `run_kmeans()` zu implementieren, die mit solch einem Kriterium selbst entscheidet, wie viele Iterationen sie ausführen will.\n", "\n", "Hinweis: Ein mögliches Stop-Kriterium sind die Veränderungen der Centroids von einer Iteration zur nächsten. Es sind aber auch andere Varianten denkbar." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "def kmeans_stop(data, centroids):\n", " \"\"\"\n", " Führt k-Means aus und terminiert selbstständig, wenn ein Stop-Kriterium erfüllt ist.\n", "\n", " Argumente:\n", " data: Datenset (Nx2)\n", " centroids: Initiale Centroids (Kx2)\n", "\n", " Rückgabe:\n", " Eine Liste von neuen Centroids\n", " \"\"\"\n", "\n", " # your code here\n", " print(\"Noch nicht implementiert\")\n", " num_iterations = -1\n", " assignments = -1\n", "\n", " return (centroids, num_iterations, assignments)" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "### Aufgabe 2.5: Die Grenzen von k-Means\n", "Setzen Sie die initialen Centroids manuell. Ist es möglich, Startpunkte so zu wählen, dass k-Means kein sinnvolles Clustering findet?" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "# Centroids manuell setzen\n", "x1 = 0\n", "y1 = 0\n", "x2 = 0\n", "y2 = 0\n", "\n", "centroids = np.array([[x1, y1], [x2, y2]], dtype=\"float64\").T" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "# k-Means implementierung aus Aufgabe 2.1 zum Visualisieren\n", "num_centroids = 2\n", "initial_centroids = centroids\n", "initial_assignments = np.random.randint(num_centroids, size=toy_data.shape[0])\n", "visualize_kmeans(toy_data, initial_centroids, initial_assignments)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "# Eine k-Means-Iteration durchführen\n", "assignments = assign_to_centroids(toy_data, initial_centroids)\n", "centroids = new_centroids(toy_data, initial_centroids, assignments)\n", "visualize_kmeans(toy_data, centroids, assignments)" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "Ein sinnvolles Clustering ist bei einem k-Means-Algorithmus in der Praxis nicht immer direkt erkennbar. Mit zunehmender Dimensionalität wird unser intuitives Verständnis von \"Nähe\" und \"Distanz\" ungenau. Datenpunkte, die in einer Dimension weit voneinander entfernt scheinen, können in anderen Dimensionen sehr nah beieinander liegen.\n", "\n", "Hinzu kommt, dass der k-Means-Algorithmus bestimmte Annahmen über die Form der Cluster macht: Er bevorzugt kugelförmige, in etwa gleich große und gleich dichte Cluster. In realen Datensätzen sind die Cluster jedoch häufig unregelmäßig geformt, unterschiedlich groß oder überlappend, was dazu führen kann, dass k-Means trotz korrekter Berechnung keine inhaltlich überzeugenden Gruppen bildet. Die Interpretation erfordert oft Domänenwissen." ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "### Aufgabe 2.6: Ellenbogen-Methode\n", "Die Ellenbogen-Methode ist eine weit verbreitete Heuristik, um die Zahl der Cluster (`k`) zu bestimmen.\n", "Der untenstehende Code führt zunächst für verschiedene k jeweils mehrere k-Means-Durchläufe durch und visualisiert für jedes k das errechnete Clustering für einen dieser Durchläufe. \n", "Lassen Sie den Code laufen und untersuchen Sie die Visualisierungen. Wie viele Cluster halten Sie für dieses Datensatz für sinnvoll?\n", "\n", "Wir verwenden hierzu die [k-Means-Implementierung von scikit-learn](https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html). Sie können natürlich auch Ihre Implementierung nutzen. Diese läuft eventuell etwas langsamer. " ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "max_k = 10 # maximale zu testendes k\n", "n = 5 # Zahl der durchgeführten Clusterings pro k\n", "score = np.zeros((max_k, n))\n", "iterations = np.zeros((max_k, n))\n", "threshold = 1e-4\n", "\n", "# Wir probieren k=1..max_k\n", "for i in range(0, max_k):\n", " k = i + 1\n", "\n", " # n Durchläufe für jedes k\n", " for j in range(n):\n", " # k-Means laufen lassen für dieses k, startet mit zufälligen Centroids\n", " kmeans_model = KMeans(n_clusters=k, tol=threshold, init=\"random\", n_init=\"auto\").fit(data)\n", " assignments = kmeans_model.labels_\n", " centroids = kmeans_model.cluster_centers_\n", "\n", " # (für später) Wir notieren für diesen Durchlauf (1) den Abstand der Punkte zu ihren Centroids\n", " score[i][j] = kmeans_model.inertia_\n", " # und (2) die Zahl der Iterationen, die k-Means durchgeführt hat\n", " iterations[i][j] = kmeans_model.n_iter_\n", "\n", " # Wir visualisieren den letzten der Durchläufe für dieses k\n", " visualize_kmeans(data, centroids, assignments)" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "Nun wenden wir die Ellenbogen-Methode an. Für jeden Durchlauf hat der Code eine Art \"Qualität\" des entstandenen Clusterings errechnet. Dazu misst er die Distanz jedes Datenpunkts zum nächsten Centroid. Diese Distanzen quadrieren wir und summieren dann die quadrierten Distanzen aller Datenpunkte. Je kleiner diese Summe, desto \"genauer\" fassen die Centroids den Datensatz zusammen. scikit-learn erledigt dies automatisch und stellt uns diese Summe im Attribut `inertia_` des Modell-Objekts bereit. \n", "\n", "Wie viele Cluster halten Sie jetzt für sinnvoll?\n", "\n", "Warum führen wir mehrere Durchläufe für jedes k durch? Was passiert, wenn wir dies nicht tun?" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "plt.figure(figsize=(3, 3))\n", "plt.plot(\n", " np.arange(1, max_k + 1),\n", " np.mean(score, axis=1)[:,],\n", " \"o-\",\n", ")\n", "plt.grid(visible=True, which=\"both\", axis=\"both\")\n", "plt.xlabel(\"k\")\n", "plt.ylabel(\"Summe der Abstände\")\n", "plt.title(\"Durchschnittliche Summe der quadrierten \\nAbstände zum nächsten Centroid\")\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "Wie ändert sich die Zahl der ausgeführten Iterationen, wenn wir `k` erhöhen? Warum?" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ "plt.figure(figsize=(3, 3))\n", "plt.plot(\n", " np.arange(1, max_k + 1),\n", " np.mean(iterations, axis=1)[:,],\n", " \"*-\",\n", " color=\"red\",\n", ")\n", "plt.grid(visible=True, which=\"both\", axis=\"both\")\n", "plt.xlabel(\"k\")\n", "plt.ylabel(\"Iterationen\")\n", "plt.title(\"Zahl der durchschnittlich \\ndurchgeführten Iterationen\")\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "## Aufgabe 3: Mögliche und Unmögliche k-Means-Ergebnisse\n", "Welche der folgenden Clusterings sind als Ergebnis einer\n", "konvergierten k-Means-Clusteranalyse möglich, welche sind unmöglich?\n", "\n", "![Clusterings](../resources/10_data_science/kmeans_clusterings.png)" ] } ], "metadata": {}, "nbformat": 4, "nbformat_minor": 4 }