diff --git a/06-clustering/01-inertia-sol.ipynb b/06-clustering/01-inertia-sol.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..a8f8855e01c056b827594e8b96fc485535651815
--- /dev/null
+++ b/06-clustering/01-inertia-sol.ipynb
@@ -0,0 +1,183 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Trägheitsmoment\n",
+    "\n",
+    "Gegeben sind folgende Daten:"
+   ],
+   "id": "0001-a3ae08f5a4a259687b176e7ee5d142ca2207e5ace1aada9d73aa8abe333"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "import numpy as np\n",
+    "import matplotlib.pyplot as plt\n",
+    "from sklearn.datasets import make_circles\n",
+    "\n",
+    "\n",
+    "X, y1 = make_circles(1000, noise=0.1, factor=0.55)\n",
+    "y2 = np.asarray(X[:, 0] > 0, dtype=int)"
+   ],
+   "id": "0002-c8ceb98996d092532358b219fb962ede51c887f4bbaf30a4a2a9fa39712"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Hier ein Plot der Daten:"
+   ],
+   "id": "0003-3d78bcf9122a2a1bb3e1681a8c95f29db1a5e142a429a286e2f70f5ab28"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "fig, axs = plt.subplots(1, 2, sharey=True)\n",
+    "axs[0].scatter(X[:, 0], X[:, 1], c=y1)\n",
+    "axs[1].scatter(X[:, 0], X[:, 1], c=y2)\n",
+    "axs[0].set_box_aspect(1)\n",
+    "axs[1].set_box_aspect(1)\n",
+    "axs[0].set_title('y1')\n",
+    "axs[0].set_title('y2')\n",
+    "plt.show()"
+   ],
+   "id": "0004-2738953bffe1a6a1bf6958471b829f9f4cf355ce4d038f06451f55017ae"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Dabei stellen `y1` die tatsächlich gewünschte, aber nicht konvexe\n",
+    "Clusterung und `y2` eine konvexe Clusterung dar. Berechnen Sie das\n",
+    "Gesamtträgheitsmoment. Beachten Sie dabei, dass Sie für beide\n",
+    "Clusterungen nicht die Formel für einen konvergierten $k$-Means\n",
+    "Algorithmus verwenden können. Hier Ihr Code:\n",
+    "\n",
+    "## Lösung\n",
+    "\n",
+    "Wir geben zwei Lösungsvorschläge an. In beiden wird jeweils das\n",
+    "Trägheitsmoment von `y` berechnet, was in einer äußeren `for`-Schleife\n",
+    "im ersten Durchlauf `y1` und im zweiten `y2` ist.\n",
+    "\n",
+    "Im ersten Ansatz durchlaufen wir mit `j` die Cluster und betrachten mit\n",
+    "`X_j` nur die Samples, die zu Cluster `j` gehören. Damit berechnen wir\n",
+    "dessen Repräsentanten `mu_j` und bilden die Differenzen von `X_j` zu\n",
+    "deren Repräsentanten. Wenn man die Differenzen quadriert und zeilenweise\n",
+    "aufsummiert, erhält man die Distanzen. Wenn man die wiederrum\n",
+    "aufsummiert, erhält man das Trägheitsmoment für Cluster `j`. Daher kann\n",
+    "man auch direkt über beide Achsen summieren."
+   ],
+   "id": "0008-2d3d0ac1dc306d99dffc90565a9e0335c9d741efdf70aa228dc53c54c66"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "inertia_loop = []\n",
+    "for y in [y1, y2]:\n",
+    "    total_inertia = 0\n",
+    "    for j in range(max(y) + 1):\n",
+    "        X_j = X[y == j]\n",
+    "        mu_j = X_j.mean(axis=0)\n",
+    "        diff = X_j - mu_j\n",
+    "\n",
+    "        inertia = np.sum(diff ** 2)\n",
+    "        total_inertia += inertia\n",
+    "\n",
+    "    inertia_loop.append(total_inertia)"
+   ],
+   "id": "0009-2a402c7b8cd559ae7dcb8bb3ec30f9029fb26c37ee966558437ace37014"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "[663.5525861044789, 420.48555691708304]"
+     ]
+    }
+   ],
+   "source": [
+    "inertia_loop"
+   ],
+   "id": "0010-312845f479b003bfe4db2fc5e3c7f7b7f42c593739d43473d785c47586a"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Im zweiten Ansatz arbeiten wir ohne (innere) `for`-Schleife und\n",
+    "verwenden anstatt dessen `list`-Comprehensions. Zunächst berechnen wir\n",
+    "die Repräsentanten für alle Cluster `mu` im Prinzip analog zum ersten\n",
+    "Ansatz. Dann berechnen wir die quadrierte Distanzmatrix\n",
+    "`sqr_dist_matrix`, analog zu den Folien (nur halt ohne Wurzel).\n",
+    "Anschließend wählen wir per Indizierung die Abstände der Samples, die zu\n",
+    "Cluster `j` gehören, zu $\\mu_j$ (Spalte `j`) und summieren sie auf. Das\n",
+    "wird in der `list`-Comprehension für jedes Cluster `j` gemacht und diese\n",
+    "Trägheitsmomente werden zum Gesamtträgheitsmoment `total_inertia`\n",
+    "aufsummiert."
+   ],
+   "id": "0011-f01157f3b95bd8a3730718503771b0bfdc3571b74ca13b9939f17eb58ab"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "inertia_comp = []\n",
+    "for y in [y1, y2]:\n",
+    "    k = max(y) + 1\n",
+    "    mu = np.array([X[y == j].mean(axis=0) for j in range(k)])\n",
+    "    diff = X[:, np.newaxis, :] - mu[np.newaxis, :, :]\n",
+    "    sqr_dist_matrix = np.sum(diff ** 2, axis=2)\n",
+    "    total_inertia = sum([np.sum(sqr_dist_matrix[y == j, j]) for j in range(k)])\n",
+    "    inertia_comp.append(total_inertia)"
+   ],
+   "id": "0012-fe2cf201c04462c6164ddc58d877a6d58d84dc101e5c31b244951544183"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "[663.552586104479, 420.48555691708304]"
+     ]
+    }
+   ],
+   "source": [
+    "inertia_comp"
+   ],
+   "id": "0013-54434e150ebc898aa6628247bbd58911180b2dd5b07d06ab7fd7306f86b"
+  }
+ ],
+ "nbformat": 4,
+ "nbformat_minor": 5,
+ "metadata": {}
+}
diff --git a/06-clustering/01-inertia.ipynb b/06-clustering/01-inertia.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..157e1cef310333e9609802921ffc07a50c61894b
--- /dev/null
+++ b/06-clustering/01-inertia.ipynb
@@ -0,0 +1,84 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Trägheitsmoment\n",
+    "\n",
+    "Gegeben sind folgende Daten:"
+   ],
+   "id": "0001-a3ae08f5a4a259687b176e7ee5d142ca2207e5ace1aada9d73aa8abe333"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "import numpy as np\n",
+    "import matplotlib.pyplot as plt\n",
+    "from sklearn.datasets import make_circles\n",
+    "\n",
+    "\n",
+    "X, y1 = make_circles(1000, noise=0.1, factor=0.55)\n",
+    "y2 = np.asarray(X[:, 0] > 0, dtype=int)"
+   ],
+   "id": "0002-c8ceb98996d092532358b219fb962ede51c887f4bbaf30a4a2a9fa39712"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Hier ein Plot der Daten:"
+   ],
+   "id": "0003-3d78bcf9122a2a1bb3e1681a8c95f29db1a5e142a429a286e2f70f5ab28"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "fig, axs = plt.subplots(1, 2, sharey=True)\n",
+    "axs[0].scatter(X[:, 0], X[:, 1], c=y1)\n",
+    "axs[1].scatter(X[:, 0], X[:, 1], c=y2)\n",
+    "axs[0].set_box_aspect(1)\n",
+    "axs[1].set_box_aspect(1)\n",
+    "axs[0].set_title('y1')\n",
+    "axs[0].set_title('y2')\n",
+    "plt.show()"
+   ],
+   "id": "0004-2738953bffe1a6a1bf6958471b829f9f4cf355ce4d038f06451f55017ae"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Dabei stellen `y1` die tatsächlich gewünschte, aber nicht konvexe\n",
+    "Clusterung und `y2` eine konvexe Clusterung dar. Berechnen Sie das\n",
+    "Gesamtträgheitsmoment. Beachten Sie dabei, dass Sie für beide\n",
+    "Clusterungen nicht die Formel für einen konvergierten $k$-Means\n",
+    "Algorithmus verwenden können. Hier Ihr Code:"
+   ],
+   "id": "0005-ed9bf2c1f0a2b4ef60c708c0ac574e49b137ccf2393010d4a84a8e8e968"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [],
+   "id": "0006-44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+  }
+ ],
+ "nbformat": 4,
+ "nbformat_minor": 5,
+ "metadata": {}
+}
diff --git a/06-clustering/02-elbow-sol.ipynb b/06-clustering/02-elbow-sol.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..67b255cf431aac3a2748a1601afbdbeccb62f65b
--- /dev/null
+++ b/06-clustering/02-elbow-sol.ipynb
@@ -0,0 +1,121 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Elbow Curve\n",
+    "\n",
+    "Gegeben sind folgende Daten:"
+   ],
+   "id": "0001-1a3c130f07a751c8ea5b72e8d2a5a34dbd13a7537e082b96e6e49115171"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "import matplotlib.pyplot as plt\n",
+    "import numpy as np\n",
+    "from sklearn.cluster import KMeans\n",
+    "from sklearn.datasets import make_blobs\n",
+    "\n",
+    "X1, y = make_blobs(n_samples=[100, 100, 400, 400], random_state=1)\n",
+    "X2, y = make_blobs(n_samples=1000, random_state=42)\n",
+    "X3, y = make_blobs(n_samples=[1000], random_state=1)\n",
+    "X4, y = make_blobs(n_samples=10000, centers=7, random_state=42)"
+   ],
+   "id": "0002-068063fc1a2e0782cb21fdb20b731cd396412cd0c1e0c33510188cc5fad"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Vervollständigen Sie folgende Funktion, indem Sie\n",
+    "[`KMeans`](https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html)\n",
+    "für die verschiedenen $k$ in `ks` laufen lassen und mit dem Attribut\n",
+    "`inertia_` die Gesamtträgheit in `inertias` hinzufügen. Der Code zum\n",
+    "Plotten ist bereits fertig.\n",
+    "\n",
+    "Probieren Sie es an den vier gegebenen Datensätzen aus und überlegen\n",
+    "Sie, wie man damit die Anzahl der Cluster bestimmen könnte.\n",
+    "\n",
+    "## Lösung\n",
+    "\n",
+    "Wir fügen eine `for`-Schleife über `ks` hinzu und initialisieren ein\n",
+    "`KMeans`-Objekt mit `n_clusters=k`. Dann rufen wir `fit` auf und\n",
+    "anschließend steht im Attribut `inertia_` das Gesamtträgheitsmoment."
+   ],
+   "id": "0006-2951e6f38bd99c99442f6c969884a29efb57da373470865482352a59a3e"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "def elbow_plot(X):\n",
+    "    inertias = []\n",
+    "    ks = np.arange(1, 15)\n",
+    "    for k in ks:\n",
+    "        kmeans = KMeans(n_clusters=k, n_init=5)\n",
+    "        kmeans.fit(X)\n",
+    "        inertias.append(kmeans.inertia_)\n",
+    "\n",
+    "    inertias = np.array(inertias)\n",
+    "    log_reductions = np.log(inertias[:-1] / inertias[1:])\n",
+    "    improvement = log_reductions / inertias[1:]\n",
+    "    improvement /= np.max(improvement)\n",
+    "\n",
+    "    fig, axs = plt.subplots(1, 2, figsize=(9, 4))\n",
+    "    axs[0].scatter(X[:, 0], X[:, 1])\n",
+    "    axs[1].plot(ks[1:], inertias[1:], c='gray')\n",
+    "    axs[1].scatter(ks[1:], inertias[1:], c=log_reductions, cmap='brg', s=improvement * 100)\n",
+    "    axs[0].set_title('data')\n",
+    "    axs[1].set_title('elbow curve')"
+   ],
+   "id": "0007-f24988a2e5d0eb16d4920cd550507417ce9a5b5922b9a41d8261ce46f3b"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Mit den Plots unten lässt sich darauf schließen, dass diejenige\n",
+    "Clusteranzahl geeignet ist, bei der es noch einen starken Abfall des\n",
+    "Gesamtträgheitsmoments gab, aber danach keine große Verbesserung mehr.\n",
+    "Das ist quasi der namensgebende Ellenbogen. In den vorbereiteten Plots\n",
+    "ist dieser Punkt auch farblich abgehoben zu den darauffolgenden Punkten\n",
+    "und der Marker vergrößert. Das ganze funktioniert nur gut für konvexe\n",
+    "Cluster.\n",
+    "\n",
+    "## Tests\n",
+    "\n",
+    "Hier die Plots:"
+   ],
+   "id": "0010-d14915689fec3f521698aed18439260e8d369cb5df36e417228479bfe9c"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "elbow_plot(X1)\n",
+    "elbow_plot(X2)\n",
+    "elbow_plot(X3)\n",
+    "elbow_plot(X4)"
+   ],
+   "id": "0011-4e7cbe8184375d0a6989ad51b06c108c305f5cbefea444528be5512526f"
+  }
+ ],
+ "nbformat": 4,
+ "nbformat_minor": 5,
+ "metadata": {}
+}
diff --git a/06-clustering/02-elbow.ipynb b/06-clustering/02-elbow.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..6e32caeb17befc3073bd947788ccf1ca6783d3a3
--- /dev/null
+++ b/06-clustering/02-elbow.ipynb
@@ -0,0 +1,119 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Elbow Curve\n",
+    "\n",
+    "Gegeben sind folgende Daten:"
+   ],
+   "id": "0001-1a3c130f07a751c8ea5b72e8d2a5a34dbd13a7537e082b96e6e49115171"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "import matplotlib.pyplot as plt\n",
+    "import numpy as np\n",
+    "from sklearn.cluster import KMeans\n",
+    "from sklearn.datasets import make_blobs\n",
+    "\n",
+    "X1, y = make_blobs(n_samples=[100, 100, 400, 400], random_state=1)\n",
+    "X2, y = make_blobs(n_samples=1000, random_state=42)\n",
+    "X3, y = make_blobs(n_samples=[1000], random_state=1)\n",
+    "X4, y = make_blobs(n_samples=10000, centers=7, random_state=42)"
+   ],
+   "id": "0002-068063fc1a2e0782cb21fdb20b731cd396412cd0c1e0c33510188cc5fad"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Vervollständigen Sie folgende Funktion, indem Sie\n",
+    "[`KMeans`](https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html)\n",
+    "für die verschiedenen $k$ in `ks` laufen lassen und mit dem Attribut\n",
+    "`inertia_` die Gesamtträgheit in `inertias` hinzufügen. Der Code zum\n",
+    "Plotten ist bereits fertig."
+   ],
+   "id": "0003-e51ef15b6bc7dd1274d3315e9810bfba0975804d79ae5f2978f4d8c904a"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "def elbow_plot(X):\n",
+    "    inertias = []\n",
+    "    ks = np.arange(1, 15)\n",
+    "\n",
+    "    # TODO: collect inertias for clusterings with varying k\n",
+    "\n",
+    "    inertias = np.array(inertias)\n",
+    "    reductions = inertias[:-1] / inertias[1:]\n",
+    "\n",
+    "    fig, axs = plt.subplots(1, 2, figsize=(10, 6))\n",
+    "    axs[0].scatter(X[:, 0], X[:, 1])\n",
+    "    axs[1].plot(ks[1:], inertias[1:], c='gray')\n",
+    "    axs[1].scatter(ks[1:], inertias[1:], c=np.log(reductions), cmap='brg')"
+   ],
+   "id": "0004-9700539d6d88f68b0c24b391384a0c6d7a6d7d3b58e751ed98bf47a2aff"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Probieren Sie es an den vier gegebenen Datensätzen aus und überlegen\n",
+    "Sie, wie man damit die Anzahl der Cluster bestimmen könnte.\n",
+    "\n",
+    "Hier Ihr Code:"
+   ],
+   "id": "0006-cb8fc7d72755f52d3e72b456da76d7e5b30e6799278749023b02b7df4c0"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [],
+   "id": "0007-44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Tests\n",
+    "\n",
+    "Hier die Plots:"
+   ],
+   "id": "0009-fe1bda9a49d27437b792fc1096fa669f2e5b826a1e60b25f2452a4bbcbf"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "elbow_plot(X1)\n",
+    "elbow_plot(X2)\n",
+    "elbow_plot(X3)\n",
+    "elbow_plot(X4)"
+   ],
+   "id": "0010-4e7cbe8184375d0a6989ad51b06c108c305f5cbefea444528be5512526f"
+  }
+ ],
+ "nbformat": 4,
+ "nbformat_minor": 5,
+ "metadata": {}
+}
diff --git a/06-clustering/03-mosaic-sol.ipynb b/06-clustering/03-mosaic-sol.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..49c7f41998f7507fb353812ff9c59e5defb8dbcd
--- /dev/null
+++ b/06-clustering/03-mosaic-sol.ipynb
@@ -0,0 +1,211 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Mosaic\n",
+    "\n",
+    "In dieser Aufgabe soll ein Bild `target` aus vielen kleinen Bildern\n",
+    "zusammengesetzt werden. Dazu werden im Startcode bereits die\n",
+    "CIFAR100-Bilder geladen und in der Variable `imgs` gespeichert."
+   ],
+   "id": "0001-f51edf4079cb1b470c271c788df582594acbcaaaaa1ec62e3286ad082e3"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "import matplotlib.pyplot as plt\n",
+    "import numpy as np\n",
+    "from sklearn.neighbors import NearestNeighbors\n",
+    "from tensorflow import keras\n",
+    "rng = np.random.default_rng()\n",
+    "\n",
+    "(x_train, y_train), (x_test, y_test) = keras.datasets.cifar100.load_data()\n",
+    "imgs = np.vstack((x_train, x_test))\n",
+    "\n",
+    "target = plt.imread('frochte.webp')"
+   ],
+   "id": "0002-6e8318925ecf9f50d56d73b2ed5ffb3d3ba92762c1fbfa2ad105e6b65fe"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Es sind 60000 32x32 RGB-Bilder, was man an der Shape schön erkennt:"
+   ],
+   "id": "0003-941da6218559043f35279a4c335f50e5b68c0c514e67ffbd4322530f8f5"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "(60000, 32, 32, 3)"
+     ]
+    }
+   ],
+   "source": [
+    "imgs.shape"
+   ],
+   "id": "0004-1229e2d8dc8e1728d3048fefc550fc060b1f1de9193f0f5a46dca3238ec"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Das Zielbild hat hier folgende Dimensionen:"
+   ],
+   "id": "0005-22c3afb3e0a561db8a3de73e91fd6bcc6390eaf182dee0c35bf37ed31ce"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "(68, 50, 3)"
+     ]
+    }
+   ],
+   "source": [
+    "target.shape"
+   ],
+   "id": "0006-2d221d7ec0ae34f32bfa5ed8787519ea66338bd40130f2f54827c73740d"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Bilden Sie von jedem Bild den Mittelwert, aber nur über die Pixel, nicht\n",
+    "über die Farben. Ihr Ziel ist also eine Shape von (60000, 3). Suchen Sie\n",
+    "anschließend zu jedem Pixel von `target` das Bild mit den ähnlichsten\n",
+    "Farben. *Bonus: Suchen Sie z. B. die 10 Bilder mit den ähnlichsten\n",
+    "Farben und wählen Sie ein zufälliges aus.* Setzen Sie anschließend die\n",
+    "gefundenen Bilder als `mosaic` zusammen, sodass ein großes Bild 32-mal\n",
+    "so groß wie das ursprüngliche `target`-Bild erhalten, also hier\n",
+    "$(68 \\cdot 32, 50 \\cdot 32, 3)$.\n",
+    "\n",
+    "## Lösung\n",
+    "\n",
+    "Leider noch ohne Erklärung, sorry!"
+   ],
+   "id": "0009-de978f4ff20b8c801097dd510fc91fbafff5599b0738a4427307471b77a"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "X = imgs.mean(axis=(1, 2))\n",
+    "\n",
+    "# # just the single nearest neighbor (lots of repetition)\n",
+    "ind = NearestNeighbors(n_neighbors=1).fit(X).kneighbors(target.reshape((-1, 3)), return_distance=False)\n",
+    "\n",
+    "# select one out of the ten nearest neighbors\n",
+    "ind = NearestNeighbors(n_neighbors=12).fit(X).kneighbors(target.reshape((-1, 3)), return_distance=False)\n",
+    "ind = ind[np.arange(ind.shape[0]), rng.choice(ind.shape[1], size=ind.shape[0])]\n",
+    "\n",
+    "ind2d = ind.reshape(target.shape[:2])\n",
+    "tiles = imgs[ind2d]\n",
+    "mosaic = np.hstack(np.hstack(tiles))  # this is hard to understand! First dimension vanishes and third is buffed. Inner call uses the vertical tiles and stacks them vertically, second used the horizontal tiles and stacks them horizontally...\n",
+    "# This is more straightforward:\n",
+    "mosaic = np.vstack([np.hstack(tiles[row]) for row in range(tiles.shape[0])])"
+   ],
+   "id": "0010-1af355aab8dd3341d7a01273fc3536afcd2422c84d1688ecc9b968450a4"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Tests\n",
+    "\n",
+    "Die Dimensionen von `mosaic` sind:"
+   ],
+   "id": "0012-c5712a3895bf6d089e3023df66a35ce6cdebc2bb919c5f5b7fd3bae4f27"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "(2176, 1600, 3)"
+     ]
+    }
+   ],
+   "source": [
+    "mosaic.shape"
+   ],
+   "id": "0013-6117ace787b35b15ec90c0b04bef2dee6442176716271b8bdf9775313cc"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "und sollten sein:"
+   ],
+   "id": "0014-63c76eaa9d35584bc36e3f2ca577bfc94f8834910dab8da4a980df5f86c"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "(2176, 1600, 3)"
+     ]
+    }
+   ],
+   "source": [
+    "target.shape[0] * 32, target.shape[1] * 32, 3"
+   ],
+   "id": "0015-0516c0ef5eb482707621327f31354b03bfd194cf6e37a97de4db231e3b4"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Hier lässt sich das Mosaic-Bild plotten:"
+   ],
+   "id": "0016-7fe0b7441cce862b7e35b9f32c45195a35b532bd6aaa1f6d82bcefec760"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "plt.figure(figsize=(16, 10))\n",
+    "plt.imshow(mosaic)"
+   ],
+   "id": "0017-c81f1b00d2217b3ef3a541a3bf0969c691e633f1c550f30d84efc918327"
+  }
+ ],
+ "nbformat": 4,
+ "nbformat_minor": 5,
+ "metadata": {}
+}
diff --git a/06-clustering/03-mosaic.ipynb b/06-clustering/03-mosaic.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..2e2a9615af4453cc57f3aedabdd4a25f1b8d82b4
--- /dev/null
+++ b/06-clustering/03-mosaic.ipynb
@@ -0,0 +1,194 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Mosaic\n",
+    "\n",
+    "In dieser Aufgabe soll ein Bild `target` aus vielen kleinen Bildern\n",
+    "zusammengesetzt werden. Dazu werden im Startcode bereits die\n",
+    "CIFAR100-Bilder geladen und in der Variable `imgs` gespeichert."
+   ],
+   "id": "0001-f51edf4079cb1b470c271c788df582594acbcaaaaa1ec62e3286ad082e3"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "import matplotlib.pyplot as plt\n",
+    "import numpy as np\n",
+    "from sklearn.neighbors import NearestNeighbors\n",
+    "from tensorflow import keras\n",
+    "rng = np.random.default_rng()\n",
+    "\n",
+    "(x_train, y_train), (x_test, y_test) = keras.datasets.cifar100.load_data()\n",
+    "imgs = np.vstack((x_train, x_test))\n",
+    "\n",
+    "target = plt.imread('frochte.webp')"
+   ],
+   "id": "0002-6e8318925ecf9f50d56d73b2ed5ffb3d3ba92762c1fbfa2ad105e6b65fe"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Es sind 60000 32x32 RGB-Bilder, was man an der Shape schön erkennt:"
+   ],
+   "id": "0003-941da6218559043f35279a4c335f50e5b68c0c514e67ffbd4322530f8f5"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "(60000, 32, 32, 3)"
+     ]
+    }
+   ],
+   "source": [
+    "imgs.shape"
+   ],
+   "id": "0004-1229e2d8dc8e1728d3048fefc550fc060b1f1de9193f0f5a46dca3238ec"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Das Zielbild hat hier folgende Dimensionen:"
+   ],
+   "id": "0005-22c3afb3e0a561db8a3de73e91fd6bcc6390eaf182dee0c35bf37ed31ce"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "(68, 50, 3)"
+     ]
+    }
+   ],
+   "source": [
+    "target.shape"
+   ],
+   "id": "0006-2d221d7ec0ae34f32bfa5ed8787519ea66338bd40130f2f54827c73740d"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Bilden Sie von jedem Bild den Mittelwert, aber nur über die Pixel, nicht\n",
+    "über die Farben. Ihr Ziel ist also eine Shape von (60000, 3). Suchen Sie\n",
+    "anschließend zu jedem Pixel von `target` das Bild mit den ähnlichsten\n",
+    "Farben. *Bonus: Suchen Sie z. B. die 10 Bilder mit den ähnlichsten\n",
+    "Farben und wählen Sie ein zufälliges aus.* Setzen Sie anschließend die\n",
+    "gefundenen Bilder als `mosaic` zusammen, sodass ein großes Bild 32-mal\n",
+    "so groß wie das ursprüngliche `target`-Bild erhalten, also hier\n",
+    "$(68 \\cdot 32, 50 \\cdot 32, 3)$.\n",
+    "\n",
+    "Hier Ihr Code:"
+   ],
+   "id": "0008-ed2a543c286667ee3711dcf37d5ebf4923818e7172044500e2e1aef94f4"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [],
+   "id": "0009-44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Tests\n",
+    "\n",
+    "Die Dimensionen von `mosaic` sind:"
+   ],
+   "id": "0011-c5712a3895bf6d089e3023df66a35ce6cdebc2bb919c5f5b7fd3bae4f27"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "(2176, 1600, 3)"
+     ]
+    }
+   ],
+   "source": [
+    "mosaic.shape"
+   ],
+   "id": "0012-6117ace787b35b15ec90c0b04bef2dee6442176716271b8bdf9775313cc"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "und sollten sein:"
+   ],
+   "id": "0013-63c76eaa9d35584bc36e3f2ca577bfc94f8834910dab8da4a980df5f86c"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "(2176, 1600, 3)"
+     ]
+    }
+   ],
+   "source": [
+    "target.shape[0] * 32, target.shape[1] * 32, 3"
+   ],
+   "id": "0014-0516c0ef5eb482707621327f31354b03bfd194cf6e37a97de4db231e3b4"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Hier lässt sich das Mosaic-Bild plotten:"
+   ],
+   "id": "0015-7fe0b7441cce862b7e35b9f32c45195a35b532bd6aaa1f6d82bcefec760"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "plt.figure(figsize=(16, 10))\n",
+    "plt.imshow(mosaic)"
+   ],
+   "id": "0016-c81f1b00d2217b3ef3a541a3bf0969c691e633f1c550f30d84efc918327"
+  }
+ ],
+ "nbformat": 4,
+ "nbformat_minor": 5,
+ "metadata": {}
+}
diff --git a/06-clustering/frochte.webp b/06-clustering/frochte.webp
new file mode 100644
index 0000000000000000000000000000000000000000..46e5861c8c7890150d5ad9d4e67a6a274a8362c0
Binary files /dev/null and b/06-clustering/frochte.webp differ