{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Einfaches Neuronales Netz\n",
    "Ein Neuronales Netz stellt die Grundlage für viele moderne KI-Anwendungen dar. In dieser Übung wollen wir ein sehr einfaches Neuronales Netz selbst programmieren."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Kurze Einführung\n",
    "Ein Neuronales Netz in seiner einfachsten Form bekommt einen Eingabevektor übergeben, z.B. ein Bild, das zeilenweise abgespeichert wurde. Das Netz selbst besteht aus mehreren so genannten Schichten. Pro Schicht gibt es mehrere Neuronen. Jede Schicht bekommt einen Vektor übergeben, der von allen Neuronen verarbeitet wird. Jedes Neuron generiert dabei eine Ausgabe. Die Ausgaben aller Neuronen einer Schicht werden wieder zu einem Vektor zusammengefasst und generieren dadurch wieder einen Ausgabevektor, der der nächsten Schicht übergeben wird. Jedes Neuron generiert eine Ausgabe $z$ folgendermaßen, wobei $\\mathbf{x}$ für den Eingabevektor der jeweiligen Schicht steht, $w_i$ für die sogenannten Gewichte und $b$ für den sogenannten Bias:\n",
    "\n",
    "$z = \\sum_{i=0}^{n}w_i \\cdot x_i + b; \\mathbf{x} \\in \\mathbb{R}^n$\n",
    "\n",
    "Dieser linear berechnete Wert wird normalerweise noch mit einer nicht-linearen Funktion verändert. Wir nehmen hierfür in unserem Beispiel immer die sogenannten ReLU-Funktion an:\n",
    "\n",
    "$a = ReLU(z) = max(z, 0)$\n",
    "\n",
    "Da jedes Neuron eigene Gewichte $w$ besitzt, der Eingabevektor genau so groß ist, wie die vorherige Schicht Neuronen besitzt, und jedes Neuron der aktuellen Schicht den kompletten Eingabevektor verarbeitet, haben wir pro Schicht eine Matrix $W$ an Gewichten der Größe (Anzahl an Neuronen der aktuellen Schicht) $\\times$ (Anzahl der Neuronen der vorherigen Schicht) und einen Vektor $\\mathbf{b}$ der Größe (Anzahl an Neuronen der aktuellen Schicht). Es ergibt sich pro Schicht $k$ also folgende Formel:\n",
    "\n",
    "$ \\mathbf{z}_k = W \\mathbf{a}_{k-1} + \\mathbf{b}_k$\n",
    "\n",
    "$\\mathbf{a}_k = ReLU(\\mathbf{z}_k)$\n",
    "\n",
    "Als initiale Eingabe würde man nun einen Datenvektor, z.B. ein zeilenweise vektorisiertes Bild, übergeben, was $\\mathbf{a}_0$ entsprechen würde. Um nun eine auf dem Bild gezeigte Kategorie bzw. Klasse, z.B. Katze, Hund, etc., vorherzusagen, vergibt man für jede Klasse eine Zahl. Die letzte Schicht im Netz beinhaltet genauso viele Neuronen, wie es Klassen gibt. Das Neuron der letzten Schicht, das den höchsten Wert generiert, zeigt durch sein Position an, welche Klasse vorhergesagt wird. Generiert z.B. das 4. Neuron der Ausgabeschicht den höchsten Wert, so wird die Klasse 4 vorhergesagt.\n",
    "\n",
    "Die größte Schwierigkeit ist es allerdings, die Gewichte und Biases zu ermitteln, die ein Neuronales Netz überhaupt möglich machen. Wir gehen hier allerdings der Einfachheit halber davon aus, dass diese Wert bereits vorgegeben sind. In der Realität würde man diese Werte anhand von Beispieldaten generieren, was den Begriff \"Maschinelles Lernen\" prägt."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Datensatz\n",
    "Wir werden mit dem sogenannten MNIST-Datensatz herumspielen. Dieser beinhaltet Grauwertbilder der Größe 28 $\\times$ 28. Jedes Bild entspricht einer handgeschriebenen Ziffer. Die Aufgabe ist es, die Ziffer automatisch zu erkennen.\n",
    "\n",
    "Wir schreiben uns zunächst eine Funktion zum Einlesen eines Teils des Datensatzes, der in der Textdatei `mnist.csv` vorliegt. Die Datei enthält in der ersten Zeile die folgenden Informationen mit Kommas getrennt:\n",
    "* Anzahl an Bildern\n",
    "* Breite eines Bilds\n",
    "* Höhe eines Bilds\n",
    "\n",
    "Danach folgen die Bilder. Jede Zeile entspricht einer Zeile eines Bilds mit Komma-getrennten Werten. Die Funktion soll die Bilder in Form einer Liste zurückgeben. Jedes Listen-Element entspricht einem Bild, wobei jedes Bild wiederum eine Liste von Listen ist. Ein Bild ist also eine Liste von Bildzeilen.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def load_mnist(filename):\n",
    "    # Fuegen Sie hier bitte Ihren Code ein.\n",
    "    pass"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nun laden wir die Daten und lassen uns einige Samples anzeigen."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "data = load_mnist('mnist.csv')\n",
    "\n",
    "plt.figure()\n",
    "plt.axis('off')\n",
    "\n",
    "imgs_to_show_per_row = 10\n",
    "imgs_to_show_per_col = 10\n",
    "\n",
    "for idx, img in enumerate(data[:imgs_to_show_per_row*imgs_to_show_per_col], 1):\n",
    "    ax = plt.subplot(imgs_to_show_per_row, imgs_to_show_per_col, idx)\n",
    "    ax.axis('off')\n",
    "    ax.imshow(img, cmap='gray')\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Neuronales Netz\n",
    "Nun wollen wir ein Neuronales Netz programmieren, das vorher gespeicherte Gewichte und Biases lädt, und diese auf die Samples des MNIST-Datensatzes anwendet. Dafür müssen wir die Gewichte und Biases aus den entsprechenden Textdateien laden und dieses mittels der oben angegebenen Formeln auf unsere Eingabebilder anwenden.\n",
    "\n",
    "Die Biases liegen als Vektoren zeilenweise in `biases.csv`. Die Werte sind wieder mit Kommas getrennt. Die Gewichte liegen etwas aufwändiger in `weights.csv`. Die Zeilen jeder Matrix liegen wieder zeilenweise in der Textdatei vor. Sobald eine leere Zeile auftaucht, markiert dies das Ende einer Matrix. Die Werte sind pro Zeile wieder mit Kommas getrennt."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Implementieren Sie zunächst die beiden Hilfsklassen `Matrix` und `Vektor`. Darin implementieren wir eine Matrix-Vektor-Multiplikation, die wieder einen Vektor liefert, bzw. eine Vektor-Addition. In beiden Fällen kann das Ergebnis als Liste zurückgegeben werden. Mit den Test-Code können Sie Ihre Implementierung testen."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Matrix:\n",
    "    def __init__(self, elems):\n",
    "        self.__elems = elems\n",
    "\n",
    "    def __mul__(self, vec):\n",
    "        # Fügen Sie hier Ihren Code ein.\n",
    "        pass\n",
    "    \n",
    "\n",
    "class Vector(list):\n",
    "    def __init__(self, elems):\n",
    "        for elem in elems:\n",
    "            self.append(elem)\n",
    "\n",
    "    def __add__(self, vec):\n",
    "        # Fügen Sie hier Ihren Code ein.\n",
    "        pass\n",
    "\n",
    "mat = Matrix([[1, 2], [3, 4]])\n",
    "vec1 = Vector([3, 2])\n",
    "vec2 = Vector([1, 2])\n",
    "\n",
    "print('Matrix:')\n",
    "print(mat)\n",
    "print('Vektor1:')\n",
    "print(vec1)\n",
    "print('Vektor2:')\n",
    "print(vec2)\n",
    "\n",
    "print('Matrix * Vektor1:')\n",
    "print(mat * vec1)\n",
    "print('Vektor1 + Vektor2:')\n",
    "print(vec1 + vec2)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Implementieren Sie nun das Laden der Gewichte und Biases und anschließend die restlichen Funktionen.\n",
    "\n",
    "**Hinweis:** `argmax(...)` wird benötigt, da das Neuronale Netz zunächst nur die Ausgaben aller Ausgabeneuronen als Vektor bzw. Liste liefert. Um die jeweilige Klasse zu bestimmen, die vorhergesagt wird, muss die Position des Ausgabeneurons bestimmt werden, dessen Wert am höchsten ist."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class NeuralNetwork:\n",
    "    def __init__(self, filepath_biases, filepath_weights):\n",
    "        self.__filepath_biases = filepath_biases\n",
    "        self.__filepath_weights = filepath_weights\n",
    "        self.__weights = None\n",
    "        self.__biases = None\n",
    "\n",
    "    @staticmethod\n",
    "    def __load_biases(filepath):\n",
    "        pass\n",
    "    \n",
    "    @staticmethod\n",
    "    def __load_weights(filepath):\n",
    "        pass\n",
    "    \n",
    "    @staticmethod\n",
    "    def __relu(x):\n",
    "        pass\n",
    "    \n",
    "    @staticmethod\n",
    "    def __flatten(matrix):\n",
    "        pass\n",
    "    \n",
    "    @staticmethod\n",
    "    def __argmax(vec):\n",
    "        pass\n",
    "    \n",
    "    def predict(self, sample):\n",
    "        pass"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nun testen wir unsere Implementierung und klassifizieren einige Ziffern."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "net = NeuralNetwork(filepath_biases='biases.csv',\n",
    "                    filepath_weights='weights.csv')\n",
    "    \n",
    "for sample in data[:20]:\n",
    "    plt.figure()\n",
    "    plt.axis('off')\n",
    "    plt.imshow(sample, cmap='gray')\n",
    "    plt.show()\n",
    "\n",
    "    print(f'Ich glaube, das Bild oben beinhaltet eine {net.predict(sample)}')"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.3"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
