{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "8f591955",
   "metadata": {},
   "source": [
    "# Wykład 7 - automatyczne różniczkowanie (autograd) w `torch`\n",
    "# + Lista 4"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "75649c0c",
   "metadata": {},
   "source": [
    "Pierwsza część: https://math.uni.wroc.pl/~jagiella/files/geografia_ai/wyklad7.ipynb"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "71c9f81b",
   "metadata": {},
   "source": [
    "`torch` - pozwala na niskopoziomowe definiowanie modeli i optymalizaję funkcji bez konieczności ręcznego wyznaczania gradientu (pisania `f_grad` z poprzedniego notatnika)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f2a4c977",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import torch"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "21ef0d1e",
   "metadata": {},
   "source": [
    "## Regresja liniowa"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e1204ba0",
   "metadata": {},
   "source": [
    "Dane do regresji:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b4c85daf",
   "metadata": {},
   "outputs": [],
   "source": [
    "n = 50\n",
    "\n",
    "x = np.linspace(2, 10, n)\n",
    "y = 2 * x + 5 + np.random.randn(n) "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13402c72",
   "metadata": {},
   "source": [
    "Tworzymy *tensory* z `x` i `y`.\n",
    "\n",
    "Tensory zachowują sie podobnie jak tablice z `numpy`, ale wyliczanie z ich pomocą funkcji jednocześnie automatycznie wyznacza wartość gradientu w punkcie, w którym funkcja jest liczona."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f3861540",
   "metadata": {},
   "outputs": [],
   "source": [
    "x_t = torch.tensor(x, dtype=torch.float32)\n",
    "y_t = torch.tensor(y, dtype=torch.float32)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "683e9852",
   "metadata": {},
   "source": [
    "Parametry `a` i `b` to tensory (jednoelementowe tablice, początkowo zerowe) *wymagające gradientu*."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "65fdb2b2",
   "metadata": {},
   "outputs": [],
   "source": [
    "a = torch.zeros(1, requires_grad=True)\n",
    "b = torch.zeros(1, requires_grad=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3fc52227",
   "metadata": {},
   "source": [
    "Obiekt opakowujący gradient descent (właściwie *stochastic* gradient descent):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "52265453",
   "metadata": {},
   "outputs": [],
   "source": [
    "optimizer = torch.optim.SGD([a, b], lr=0.01) # lr = learning rate = lamba"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "afae6029",
   "metadata": {},
   "outputs": [],
   "source": [
    "for _ in range(1000):\n",
    "    y_pred = a * x_t + b\n",
    "\n",
    "    # MSE dla przybliżeń (y_pred) vs. oryginalne wartości\n",
    "    mse = ((y_pred - y_t) ** 2).mean() # tutaj nie pomijamy czynnika 1/n\n",
    "\n",
    "    optimizer.zero_grad() # usuwamy \"stary\" gradient, wyliczony w poprzednim kroku\n",
    "    mse.backward() # wyliczamy gradient\n",
    "    optimizer.step() # pojedynczy krok gradient descent\n",
    "\n",
    "print(\"a:\", a.item())\n",
    "print(\"b:\", b.item())\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "513a63be",
   "metadata": {},
   "source": [
    "## Produkt macierzy"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c4f3c3d4",
   "metadata": {},
   "outputs": [],
   "source": [
    "import imageio\n",
    "image = imageio.v2.imread(\"https://math.uni.wroc.pl/~jagiella/files/geografia_ai/badcat.png\", pilmode=\"L\")\n",
    "image = image / 255 # format \"liczby rzeczywiste 0..1\" zamiast \"liczby całkowite 0..255\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "39313d1b",
   "metadata": {},
   "outputs": [],
   "source": [
    "n, d = image.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d7168d8f",
   "metadata": {},
   "outputs": [],
   "source": [
    "image_t = torch.tensor(image, dtype=torch.float32)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f9f082e4",
   "metadata": {},
   "outputs": [],
   "source": [
    "r = 10"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f75d0cda",
   "metadata": {},
   "source": [
    "`W` i `H` to macierze (układy parametrów) wymagające gradientu:"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c0cbc2f3",
   "metadata": {},
   "source": [
    "### Główny kod"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fc4d641b",
   "metadata": {},
   "outputs": [],
   "source": [
    "W = torch.randn((n,r), requires_grad=True)\n",
    "H = torch.randn((r,d), requires_grad=True)\n",
    "\n",
    "optimizer = torch.optim.SGD([W, H], lr=0.001)\n",
    "\n",
    "for _ in range(1000):\n",
    "    Z = W @ H\n",
    "\n",
    "    loss = ((Z - image_t) ** 2).sum()\n",
    "\n",
    "    optimizer.zero_grad()\n",
    "    loss.backward()\n",
    "    optimizer.step()\n",
    "print(\"Końcowa strata: \", loss.item())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "683c713f",
   "metadata": {},
   "source": [
    "Tutaj liczymy wynik z pominięciem mechanizmu automatycznego różniczkowania:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "527de696",
   "metadata": {},
   "outputs": [],
   "source": [
    "with torch.no_grad():\n",
    "    Z = W @ H\n",
    "    plt.imshow(Z.clip(0, 1), cmap='gray')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "284d44e9",
   "metadata": {},
   "source": [
    "# Lista 4"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "69402b22",
   "metadata": {},
   "source": [
    "### Zadanie 0 (0,25 punktu)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d234e7e5",
   "metadata": {},
   "source": [
    "- Upewnij się, że cały powyższy kod działa. Jeśli trzeba, zainstaluj `torch` (optymistycznie: `pip install torch`). Google Colab domyslnie ma tę bibliotekę.\n",
    "- Poeksperymentuj z parametrami w części [Główny kod](#Główny-kod)\n",
    "  - zbadaj efekt zbyt dużego learning rate (`lr`)\n",
    "  - dobierz liczbę iteracji, aby osiągnąć (prawie) minimalną wartość `loss` dla mniejszych `lr`\n",
    "  - zbadaj efekt zmiany `loss` na \"prawdziwe\" `mse` (zmień `sum()` na `mean()`)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5620ef1e",
   "metadata": {},
   "source": [
    "### Zadanie 1 (1 punkt)\n",
    "- Zmodyfikuj problem znajdowania przybliżenia macierzy iloczynem macierzy mniejszych rozmiarów (modyfikując część  [Główny kod](#Główny-kod)): znajdź dwie macierze `W` i `H` o współczynnikach *nieujemnych* dla których `loss` jest najmniejsza możliwa.\n",
    "  - wskazówka: dla liczby $a$ (lub układu liczb), zarówno $a^2$ jak i $\\exp(a)$ są nieujemne. Funkcja $exp$ w `torch`: `torch.exp(t)`, gdzie `t` to tensor.\n",
    "  - dobierz learning rate i liczbę iteracji (lub dobierz inny warunek stopu) aby uzyskać zbieżność do optymalnych macierzy."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1118eeba",
   "metadata": {},
   "source": [
    "### Zadanie 2 (1 punkt)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "61472705",
   "metadata": {},
   "source": [
    "(*zastosowanie metod gradientu poza ścisłym ML*)\n",
    "Znajdź (dowolne) minimum funkcji\n",
    "\n",
    "$$f(x) = \\frac{x^{4}}{10} + \\frac{x^{3}}{5} - x^{2} - \\frac{4 x}{5} + 1.$$\n",
    "\n",
    "- utwórz tensor (wymagający gradientu) reprezentujący $x$ oraz zaimplementuj $f(x)$ jako funkcję straty.\n",
    "- dobierz learning rate, liczbę iteracji, lub inny warunek stopu aby uzyskać zbieżność do minimum."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6a07ceeb",
   "metadata": {},
   "source": [
    "### Zadanie 3 (1,75 punktu)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7e93db5c",
   "metadata": {},
   "source": [
    "Rozważmy ciąg punktów $x_1, x_2, \\ldots, x_n$ na prostej. Każdy punkt ma przyznaną zero-jedynkową etykietę $y_1, y_2, \\ldots, y_n$. 50 przykładowych punktów (zebranych w ciąg `x`) i ich etykietki (ciąg `y`):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "980428cc",
   "metadata": {},
   "outputs": [],
   "source": [
    "N = 50\n",
    "x = np.linspace(0, 10, N) + np.random.rand(N) * 4\n",
    "y = (np.linspace(0, 10, N) > 5).astype(int)\n",
    "print(x)\n",
    "print(y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bedd909e",
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.plot([np.min(x)-1, np.max(x)+1], [0, 0], linestyle=\":\", color=\"black\", alpha=0.5) # oś rzeczywista\n",
    "plt.scatter(x, np.zeros(N), c=np.array([\"orange\", \"blue\"])[y], alpha=0.5)\n",
    "plt.title(\"Punkty na prostej\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5e6b67c0",
   "metadata": {},
   "source": [
    "W *jednowymiarowej* regresji logistycznej pojawiają się dwa trenowalne parametry $a, b$. Regresja przypisuje punktowi $x$ na prostej następujące prawdopodobieństwo $\\hat y$, dla którego $x$ należy do klasy $1$:\n",
    "\n",
    "$$\\hat y := \\sigma(ax+b),$$\n",
    "\n",
    "gdzie $\\sigma$ to funkcja logistyczna zdefiniowana jako\n",
    "\n",
    "$$\\sigma(z) := \\frac{1}{1 + e^{-z}}.$$"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6649e62b",
   "metadata": {},
   "source": [
    "W idealnej sytuacji, dla punktu $x_i$ wartość $\\sigma(ax_i+b)$ jest:\n",
    "- bliska $1$, jeśli $y_i = 1$,\n",
    "- bliska $0$, jeśli $y_i = 0$."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9e69a3e3",
   "metadata": {},
   "source": [
    "Optymalne parametry $a,b$ to takie, dla których funkcja straty\n",
    "\n",
    "$$L(a, b) = -\\frac{1}{n} \\sum_{i=1}^{n}\\big( y_i \\log(\\sigma(a x_i + b)) + (1 - y_i)\\log(1 -\\sigma(a x_i + b))\\big)$$\n",
    "\n",
    "przyjmuje najmniejszą wartość."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ddd3f605",
   "metadata": {},
   "source": [
    "Dla punktów z `x` i ich etykietek `y` znajdź optymalne parametry $a, b$. W tym celu (wzorując się np. na regresji liniowej):\n",
    "- utwórz tensory `torch` z `x` i `y`.\n",
    "- utwórz tensor(y) reprezentujące parametry $a$ i $b$ wymagające gradientu.\n",
    "- zaimplementuj funkcję straty używając tensorów i funkcji z `torch` (w tym np. `torch.exp`, `torch.log`).\n",
    "- dobierz learning rate, liczbę iteracji, lub inny warunek stopu aby uzyskać zbieżność do optymalnej wartości.\n",
    "\n",
    "Po wytrenowaniu $a$ i $b$ wyznacz wartości $\\sigma(a x_i + b)$ i porównaj je z wzorcowymi $y_i$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9fad3531",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
