diff --git a/README.md b/README.md new file mode 100644 index 0000000..373029b --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +This is the coding part of the lecture Deep Representation Learning in PyTorch. + +python 3.10 + +#### Autoencoder +In this demo we will implement a simple autoencoder. The autoencoder will be trained on the MNIST dataset. The autoencoder will be implemented in the file autoencoder.py. The file autoencoder.py contains a class Autoencoder on the MNIST dataset. +We compare the performance of the fully connected autoencoder with a convolutional autoencoder. +Jupyter notebooks: +* autoencoder_mnist.ipynb +* autoencoder_cifar10.ipynb + +#### Contractive Learning (SimCLR) +In this demo we implemented the SimClR [1] algorithm and trained it on the cifar10 dataset. + +Download the pretrained encoder [here](https://cloud.cps.unileoben.ac.at/index.php/s/feHYqRHwDy7mMDm) and put it in the folder `runs`. + +The code was adapted from this [repo](https://github.com/sthalles/SimCLR/tree/master) + +Jupyter notebooks: +* simclr.ipynb + + +[1] Ting Chen, Simon Kornblith, Mohammad Norouzi, and Geoffrey Hinton. 2020. A simple framework for contrastive learning of visual representations. In Proceedings of the 37th International Conference on Machine Learning (ICML'20), Vol. 119. JMLR.org, Article 149, 1597–1607. \ No newline at end of file diff --git a/autoencoder_cifar10.ipynb b/autoencoder_cifar10.ipynb new file mode 100644 index 0000000..5f61ef2 --- /dev/null +++ b/autoencoder_cifar10.ipynb @@ -0,0 +1,1622 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + "| Credentials | |\n", + "|----|----------------------------------|\n", + "|Host | Montanuniversitaet Leoben |\n", + "|Web | https://cps.unileoben.ac.at |\n", + "|Mail | cps@unileoben.ac.at |\n", + "|Author | Fotios Lygerakis |\n", + "|Corresponding Authors | fotios.lygerakis@unileoben.ac.at |\n", + "|Last edited | 28.09.2023 |" + ], + "metadata": { + "collapsed": false + }, + "id": "25b3e26209aea07e" + }, + { + "cell_type": "markdown", + "id": "1490260facaff836", + "metadata": { + "collapsed": false + }, + "source": [ + "In the first part of this tutorial we will build a fully connected MLP Autoencoder on the CIFAR10 dataset. Then we will perform linear probing on the encoder features to see how well they perform on a linear classification task." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "fc18830bb6f8d534", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:41:16.496631884Z", + "start_time": "2023-10-03T12:41:14.795432603Z" + } + }, + "outputs": [], + "source": [ + "import torch\n", + "import torch.nn as nn\n", + "import torch.optim as optim\n", + "import matplotlib.pyplot as plt\n", + "from torchvision import datasets, transforms\n", + "from sklearn.neighbors import KNeighborsClassifier\n", + "from sklearn.metrics import adjusted_rand_score\n", + "from sklearn.linear_model import LogisticRegression\n", + "from sklearn.cluster import KMeans\n", + "from tqdm import tqdm" + ] + }, + { + "cell_type": "markdown", + "id": "1bad4bd03deb5b7e", + "metadata": { + "collapsed": false + }, + "source": [ + "Set random seed" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "27dd48e60ae7dd9e", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:41:16.504621133Z", + "start_time": "2023-10-03T12:41:16.497289235Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": "" + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Set random seed\n", + "torch.manual_seed(0)" + ] + }, + { + "cell_type": "markdown", + "id": "cc7f167a33227e94", + "metadata": { + "collapsed": false + }, + "source": [ + "Load the CIFAR10 dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "34248e8bc2678fd3", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:41:18.687951160Z", + "start_time": "2023-10-03T12:41:17.491688103Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Files already downloaded and verified\n", + "Files already downloaded and verified\n" + ] + } + ], + "source": [ + "# Define the transformations\n", + "transform = transforms.Compose([transforms.ToTensor(),\n", + " transforms.Normalize((0.5,), (0.5,))])\n", + "# Download and load the training data\n", + "trainset = datasets.CIFAR10('data', download=True, train=True, transform=transform)\n", + "# Download and load the test data\n", + "testset = datasets.CIFAR10('data', download=True, train=False, transform=transform)\n" + ] + }, + { + "cell_type": "markdown", + "id": "928dfac955d0d778", + "metadata": { + "collapsed": false + }, + "source": [ + "Print some examples from the dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "87c6eae807f51118", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:41:19.826292939Z", + "start_time": "2023-10-03T12:41:19.492130184Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": "
", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA8cAAAGJCAYAAACnwkFvAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAADLAUlEQVR4nOz9eZhdVZ3vj3/2medzaq5KZahKZZ4IJkxhSEAwMimoDNo2RBBoWy8OF1q5rQI/7XZEUbQFtUUUtZVBLo3K1IIiIDOBEDJXhqqk5jp15mGfvb5/+Etdi/cHOIypynm/nqeflnf2sPaa9tpVWa9YxhgjhBBCCCGEEEJIDeM60AUghBBCCCGEEEIONPw4JoQQQgghhBBS8/DjmBBCCCGEEEJIzcOPY0IIIYQQQgghNQ8/jgkhhBBCCCGE1Dz8OCaEEEIIIYQQUvPw45gQQgghhBBCSM3Dj2NCCCGEEEIIITUPP44JIYQQQgghhNQ8/Dj+/7Nz506xLEu++c1vvmnXfPDBB8WyLHnwwQdf9zV+/vOfy4IFC8Tr9UoikXjTykbI3zNZ+/+BZs2aNbJkyZIDXQxyAOCY0OGYIFNpbKxZs0bWrFnzpl6TTH6mUh+tBV5Le1x11VViWdaErKOjQ9atW/cWlQ6Z0h/HP/3pT8WyLHnyyScPdFHeEjZt2iTr1q2Trq4u+dGPfiQ//OEPD3SRyCTiYO//v/zlL+Xaa6890MUgUwiOCUJ0DvaxQaY+7KOvjVwuJ1dddRU/1t8CpvTH8cHOgw8+KI7jyHe+8x1Zt26dnH322Qe6SIS8bfBDgJCJcEwQQggR+dvH8dVXX33QfRx//vOfl3w+f0DLwI/jSczAwICIyKv+dWpjzAHvSIQcSAqFgjiOc6CLQcikgWOCkANHLpc70EUgZEri8XgkEAgc0DIc9B/HpVJJvvjFL8qKFSskHo9LOByWY489Vh544IGXPefb3/62zJo1S4LBoKxevVo2bNgAx2zatEk+8IEPSH19vQQCAVm5cqXceeedr1qeXC4nmzZtkqGhoVc8rqOjQ6688koREWlqahLLsuSqq64a/7PTTjtN7rnnHlm5cqUEg0G54YYbRERkx44dctZZZ0l9fb2EQiE58sgj5Xe/+x1cf9euXfKe97xHwuGwNDc3y6c//Wm55557uJ/iIGOq9v81a9bI7373O9m1a5dYliWWZUlHR4eI/L99P//1X/8ln//856W9vV1CoZCkUil1r4rI//vrWjt37pyQ/+EPf5DVq1dLNBqVWCwmhx12mPzyl798xbLde++9EgqF5IMf/KDYtv2qz0wmFxwTf4NjgryUqTo29vPDH/5Qurq6JBgMyuGHHy4PPfSQelyxWJQrr7xS5syZI36/X2bMmCH/8i//IsViEY69+eabZcWKFRIMBqW+vl7OPfdc2bNnz4Rj9u/Df+qpp+S4446TUCgk/+f//J+qykxeG1O5jz700ENy1llnycyZM8f73ac//Wn45dbL7ZNft27d+Jy/c+dOaWpqEhGRq6++evydsP87QUTkj3/8oxx77LESDoclkUjIe9/7XnnxxRcnXHP/+2HLli3y4Q9/WOLxuDQ1NckXvvAFMcbInj175L3vfa/EYjFpbW2Va665Bso1MDAgF154obS0tEggEJBDDjlEbrrpppeth1drj5d7Z72UZDIpn/rUp2TGjBni9/tlzpw58rWvfe1N+aGw5w1fYZKTSqXkxz/+sXzwgx+Uiy66SNLptPznf/6nrF27Vh5//HFZvnz5hON/9rOfSTqdlo9//ONSKBTkO9/5jpxwwgny/PPPS0tLi4iIvPDCC3L00UdLe3u7fO5zn5NwOCy/+c1v5IwzzpDbbrtNzjzzzJctz+OPPy7HH3+8XHnllRM68Uu59tpr5Wc/+5n89re/lR/84AcSiURk2bJl43++efNm+eAHPyiXXHKJXHTRRTJ//nzp7++XVatWSS6Xk0svvVQaGhrkpptukve85z1y6623jpcrm83KCSecIPv27ZNPfvKT0traKr/85S9fcXIhU5Op2v//9V//VcbGxqSnp0e+/e1vi4hIJBKZcMyXvvQl8fl8ctlll0mxWBSfz/ea6uanP/2pXHDBBbJ48WK54oorJJFIyDPPPCN33323fOhDH1LPueuuu+QDH/iAnHPOOfKTn/xE3G73a7onOfBwTLw8HBO1zVQdGyIi//mf/ymXXHKJrFq1Sj71qU/Jjh075D3veY/U19fLjBkzxo9zHEfe8573yF/+8he5+OKLZeHChfL888/Lt7/9bdmyZYvccccd48f+27/9m3zhC1+Qs88+Wz760Y/K4OCgXHfddXLcccfJM888M+Fv9Q0PD8vJJ58s5557rnz4wx8ef37y5jKV++gtt9wiuVxO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ZXzz+v6fQnq9XjnxxBPf/AK/AWbNmiWbN2+GfNOmTeN/vv//b9y4UYwxE+pk27Ztb09BydvGVO7/1dgKX0pdXZ2I/M1i+PeylJf+9LKrq0tERDZs2PCqL75AICB33XWXnHDCCfLud79b/vSnP8nixYtfc9nI5IBj4m9wTJCXMlXHxv61zdatW8f/erSISLlclu7ubjnkkEPGs66uLlm/fr28853vfMXx1NXVJcYY6ezsfMMLbfLmMVX76PPPPy9btmyRm266Sc4777zx/L777oNj6+rqJvzV5f28dM5+uf67fzy83PdAY2PjhN8avxFmzZolzz33nDiOM+G3xy/97tjPq7VHtXR1dUkmk3nL2uyg3yS0/ycff/+Tjscee0weffRR9fg77rhjwv6Cxx9/XB577DE5+eSTRUSkublZ1qxZIzfccIPs27cPzh8cHHzF8rzWf5rgtXDKKafI448/PuHZstms/PCHP5SOjg5ZtGiRiIisXbtWent7J2jqC4WC/OhHP3rTy0QOLFO5/4fD4df8V432L/D/fr9JNpuFf1bgXe96l0SjUfnKV74ihUJhwp9pPxWNx+Nyzz33SHNzs5x00kmv6yfUZHLAMcExQXSm6thYuXKlNDU1yfXXXy+lUmk8/+lPfyrJZHLCsWeffbb09vaq6518Pi/ZbFZERN73vveJ2+2Wq6++Gvq/MWbCXy0lbx9TtY9q5TbGyHe+8x04tqurSzZt2jTh3uvXr5eHH354wnGhUEhEBPp4W1ubLF++XG666aYJf7Zhwwa599575ZRTTnnFsr4WTjnlFOnr65Nf//rX45lt23LddddJJBKR1atXTzj+1dqjWs4++2x59NFH5Z577oE/SyaTb/ifFDwofnP8k5/8RO6++27IP/nJT8ppp50mt99+u5x55ply6qmnSnd3t1x//fWyaNEiyWQycM6cOXPkmGOOkY997GNSLBbl2muvlYaGhgm/uv/+978vxxxzjCxdulQuuugimT17tvT398ujjz4qPT09sn79+pct62vRvr9WPve5z8mvfvUrOfnkk+XSSy+V+vp6uemmm6S7u1tuu+228Z/qXHLJJfK9731PPvjBD8onP/lJaWtrk1/84hfjEpbX89sJcuA4WPv/ihUr5Ne//rV85jOfkcMOO0wikYicfvrpr3jOu971Lpk5c6ZceOGFcvnll4vb7Zaf/OQn0tTUJLt37x4/LhaLybe//W356Ec/Kocddph86EMfkrq6Olm/fr3kcjn13+hrbGyU++67T4455hg58cQT5S9/+cvbso+UvHY4Jv4fHBPk7zkYx4bX65Uvf/nLcskll8gJJ5wg55xzjnR3d8uNN94Ie47/8R//UX7zm9/IP/3TP8kDDzwgRx99tFQqFdm0aZP85je/kXvuuUdWrlwpXV1d8uUvf1muuOIK2blzp5xxxhkSjUalu7tbfvvb38rFF18sl112WRU1Tl4rB2MfXbBggXR1dclll10mvb29EovF5LbbboO9xyIiF1xwgXzrW9+StWvXyoUXXigDAwNy/fXXy+LFiyWVSo0fFwwGZdGiRfLrX/9a5s2bJ/X19bJkyRJZsmSJfOMb35CTTz5ZjjrqKLnwwgvH/ymneDz+pn57XHzxxXLDDTfIunXr5KmnnpKOjg659dZb5eGHH5Zrr71WotHohOOraY9quPzyy+XOO++U0047TdatWycrVqyQbDYrzz//vNx6662yc+dO2C/9mnhDrusDzH7t+8v93549e4zjOObf//3fzaxZs4zf7zeHHnqoueuuu0CJvl8z/o1vfMNcc801ZsaMGcbv95tjjz3WrF+/Hu69fft2c95555nW1lbj9XpNe3u7Oe2008ytt946fsxb+U85nXrqqeo527dvNx/4wAdMIpEwgUDAHH744eauu+6C43bs2GFOPfVUEwwGTVNTk/nf//t/m9tuu82IiPnrX//6qmUjB56Dvf9nMhnzoQ99yCQSCSMi4+Xdf41bbrlFPe+pp54yRxxxhPH5fGbmzJnmW9/6FvyzNfu58847zapVq0wwGDSxWMwcfvjh5le/+tX4n//9P1uzn23btpm2tjazcOFCGJvkwMIxwTFBdA72sWGMMf/xH/9hOjs7jd/vNytXrjR//vOf1X8Wp1Qqma997Wtm8eLFxu/3m7q6OrNixQpz9dVXm7GxsQnH3nbbbeaYY44x4XDYhMNhs2DBAvPxj3/cbN68efwYbUyQ187B3kc3btxoTjzxRBOJRExjY6O56KKLzPr1642ImBtvvHHCsTfffLOZPXu28fl8Zvny5eaee+6BZzTGmEceecSsWLHC+Hw+KMf9999vjj766PG5/PTTTzcbN26ccP7LfWecf/75JhwOwzNofb2/v9985CMfMY2Njcbn85mlS5fC87yW9qjmn3Iy5m//VO0VV1xh5syZY3w+n2lsbDSrVq0y3/zmN02pVIKyvxYsYybBzmoyKbj22mvl05/+tPT09PCn/4QQQgghhJCagh/HNUo+n59gVywUCnLooYdKpVKRLVu2HMCSEUIIIYQQQsjbz0Gx55i8dt73vvfJzJkzZfny5TI2NiY333yzbNq0SX7xi18c6KIRQgghhBBCyNsOP45rlLVr18qPf/xj+cUvfiGVSkUWLVok//Vf/yXnnHPOgS4aIYQQQgghhLzt8K9VE0IIIYQQQgipeQ76f+eYEEIIIYQQQgh5NfhxTAghhBBCCCGk5uHHMSGEEEIIIYSQmqdqIdfRx62GLJkcgczvciBr8OG25pmNIcia6sOQNSaikPncXsg8/iBk4sbHGxlNQlaysXx1iThkrkoZsmKxCFmhUIAsEAxAVpEKZLl8BrJ4IgaZGDy3VCxB5hasK7fbDVk0EoEsHMb28HrxOfLKfY2l/NzFhe2hldk2FmQf/9L1eL0DzI/uvB+ynk1PQTbY/SJklQrWRcvMBZDN7FoIWV3rTMgCQbzelhcegWzXtucgK6exz7mV8sXqcEx4AjiODz/6OMjmzMNnK4zh/PHChmcgcxzsI6UyjrGNLzwPWSo5BFmxhGO2XMIxMTKcgyydzUNmV/B6zc0NkNXV4xirmDReD6cZKeRxjrrj9nvwwEmI4+A7oeZQzB6WhfNcPot9bngE+3B9fR1klRKOiWAIx6fb58fiKfO1I1g+HCWTC5dr8v28f3obtsHf/zOK+9H6g8eFNa49o+3gekCU6yXHUpAFXD7Iwsq7Ol3Euc8Vwr4U9CvXU9YSiXgCspFRfCeUsji/aqKcckmZOLEKxO3BOvV5sU7jYVzrTGvGcdfT1w9ZtoTtEYvhuXYZnySbHYNsxnRcA3q92EYeD2a/+e/1kB1obvndo5Bp74mgH/uXL4Dt4rjxONtgm3qUGcytDB2v9spS9EzGg/coW8pxyuVcFSU1uF7X+kjFpY135SYKmmZKVU8p13McpSzKgdrzavfQ2rxSUZ5Nu56S2eqz4T0ueM/iV73+5HuTEEIIIYQQQgghbzP8OCaEEEIIIYQQUvPw45gQQgghhBBCSM3Dj2NCCCGEEEIIITVP1UKuFza+ANnY8DBkdbgvXqwGDBsrKNqygs2QZR0UNGSUjezGQglEroAin1xekfFUcMP2kBs3mQc8eF/bxnPdiszCr4gFcoUsXk+RD1kFlPsong4pK3KwoAflBRlFgjVSsSELhVCiYblQGGApgjRRpCG5Agoz7DJmbo/SiSYhKUUe0pCoh8w0tWDmQcFG28zZkFUcrB+Xg9IeJ4ftVxjF8WnyKO1pb8RxN3PGHMhmzJkF2bT26ZA1N+Pzer2KMCOBopoZ01vxOBv7a6GAcpjkKIrFhoawjTw+HBNi4YCqU+atgCJpGUspYsIAzgGOwTbyKn09NTYKWamo6SemBpNRkjRZKeZQxjPSswOyPS/icWMpfJ8cfcI7IYspckjt5+SWIlphS752vIoAs6JY9xxlHWL5cF1TtHEe0SRTmpArEcU5N6bIskpp7EtOHufhkBfFYvEQZmGlz0X8uG4YVNZnjsEsEMB5s7mpEbKRUZxLNTlq+zR8B7oV5Y8mWvQq19uxey9kfi+2R10d1n0Uq14a4ijD1MZnNqecPAlxFNmTR+kPJUU0lx1DiaU3rMgDlb4piuxVEw/ailSroqxfC2O4DvEpfbMiOLYzinzXZeG5kTC2vVGu5ygiK03yV60sS6kCVcil1Z/m99LkW9p9NSGX9hyO8iROldKvauC7jhBCCCGEEEJIzcOPY0IIIYQQQgghNQ8/jgkhhBBCCCGE1Dz8OCaEEEIIIYQQUvNULeQKepQd9OiKkFmKxKazBTeUNzejuCioCaCUjdj5IkqFCmWUNhjlXF9Q2aRvK4IvB68Xr0eZhV3Gc32KCEDZYy5uH9ZVsYTPVrbxOULKuZ4w3jegHGdbKG1wGdy0bisb7RVPmUQUoUcmi/coKxISl3K9dAplM5MSRSZWKmKWy6HIpGNeO2RanZXK2B/qG3E8ebz4c665c+dBturIlZC1t6BUKx5vgqzswU4cUuQTirdOLEUik8+ikKKo1GkoiOOuLoECla7ZiyB78cXNSmHwHsUiSs7isTrIvMqcN5bC9jWCmSazGB3FNs/nlLls6vq4VOlGraHVgUsxnvTt6YbsuUf/DFk5j/3VG8H+mlfm0lg9vntVqYqFc8pkb0ltvXCg8XkU2ZlSt3WNKHvKau1cQfmWrcyvltLnprXivNmqSKZ2bN0OWaMH3ztt7ShQdJXx2VxKu2hiuMY4ilqNW5F+JbAsoTC+J9wurJemVhR3BXwog9LWIbbBd0c8kYBsurKmdCurbY8Xj/O78Z3qlPDdG4sqoqby65MPvd2klHd/WXn3Dw2iVLSndwAydwDXoJEozod+F9at4uiSkibMK2NfyqXxOYKKfFRc2C7pEorFSiUszOzOuZDN6UI5ajCA40mTUamCKqUOjBI6mqVLi5S5542sA7R53aWVTxGVvV74m2NCCCGEEEIIITUPP44JIYQQQgghhNQ8/DgmhBBCCCGEEFLz8OOYEEIIIYQQQkjNU7WQK2DhZvRoFMUQ89txE3xDEI/zOigayoygxKbi4Pd7PotlUfbZSywRgcyjCKqSY7gx3qPUTH0UhQ/plCJRKiiSnQJu8Nc2vGtyq3IpD5mrggX0+vHZKhW8r0exahUVkZRPsQ+5HKz7YmYEMqloogk8zFbkAGMZFBJNRuwCtotlozjD70OhyNjQEGQNrSjGmrl4DmTNM6ZB5tVMUYpUomzjuNu0D6UXuR2DeK4Lx+fm59dDdthCFGMdd/hhkGmChpQiQdm9ay9kPi/KJ3y+GGSNTSg+271nK54bwLGdyeM4TqWw3TxeRTYTw+vlFblOBYeT2DaOCb9fad8pwmSUJL3dGEUUUlYkcHv37IIsFsL5I5RAcdHAKL7Hhvf1QtYyYyYW0IWTs6ZPsTSDInlF4jFsq4Aio2ppQVlW/xDOzUFFgpgcSULW2oRSRb/yEg4GcW6ZPhNFW2F1bYITmE8xtfp9+Lw5ZT6c0Y51YLw4dnzKWqdUwnVDoyavVORIxSLO9VFtDlfGbHoM1z/FIq4DGhqxHwTDuI7zWHiup4R1WshiWWxlHTcZeeSvj0KWUSRdLkFRWr6IM1OhguPE68PMrXxPVJQprWCwX9uKjCqi9OughW0aUMZdRVlPZbPYfk8+9wxkA0O4Jprd2QlZYyPK54Ih7NdGkYVWFIuwo4h7LaVO32yDqFG+EzThsramVAVkVcDfHBNCCCGEEEIIqXn4cUwIIYQQQgghpObhxzEhhBBCCCGEkJqHH8eEEEIIIYQQQmqeqoVcdX48NKhIEeIRlIc0xXBTfcXBzd6YiLg9isXJhd/0RUcRTylWLY+yobxSRLGSceM9BgaSeG4ZS53OoSghV8HN95EgCoREETm4FZmLS5EDuP1Y9/ksPlvIq0gqlI3shQKWOV9GUYGjqFuSGRQ/JRXZQCaH1yuUp8bPbIo5lHhEFNFKrB7FKO84ZDlkM2bPhSxtY/1s3rEHspTS5zLJJGTDSZRU7OsbhSwWxzKLC4Un//1ft0LmPedsyFYfdQwe58X+0NqKsjExKMFKKvKhp595DjKPF+eocBTHna0I5EqZJGTKtCBNTQ2QVZTxPjyCkjOXoBxDm7cSiQTemExKNCmINl8PjuBY3LlzN2RF5bhoAAU9uUwKsk3rUebS2tEFWaIVxXWaVEXzrFC49spo84MmiSkV8J3Z2oaCqpAiD/S7cZ3U1oTnlsv4zhoe6ocsqkjEPF6c/JwSPofXg/3B5cKOk89hf1UcpeIK4LMVFUlpURFy+ZU1aiaF745wBOtUExINj+C70u9FUZk2JDRhWDqjSajw5NIYlqVUwvdnJIJlmYwkM8qa2+BzW8ra0uPD74mQIsFyuzDTZHEF5cvDVn5vmFLWe/ksZn4L+2vUYD90K19fXmUNX1DW0tv3oGhx174+yBIxXOvPmI7i16ZGnKMSdShX9ijiRrfyTaW9AzWUZZc4Sv/XrmeU+zqqkOv1ycGmxlcIIYQQQgghhBDyFsKPY0IIIYQQQgghNQ8/jgkhhBBCCCGE1Dz8OCaEEEIIIYQQUvNULeRqTqBoKOrFzdkBRZ7gcuOG6GAQN56XbdwYr2/ORtlNycZ7VBRhgWMwM4o8x3hw4366hJvvKxV83lwFN4rbNmbpDJalt4j38Lrw3FgG66Xch8KffBJFTTObUPzU3DwDMis6BllxFOVImQyWeSyFEoGhMZQwdO9BKUdFkYtMRvx+FEOU3SgyyQcjkHWnsC6e/cvjkI0Mo7Cjdy8KVLxu7A9avyna2Nc18VpbE04NA327IIspwpN0Ett0S3c33qOtETKvF+/bNqMVsmlKtrsPRWWbn8esuQ1lYzt3Y7+WsiJ8UAQ0FQ/OWwEf1ovfg/0lX8BzY4pEw+PB65HJiiYPwXbu7emBrHs3Znu27YCsMYpzyvRGlPHs241j9vknn4Bs5ZoEZCGlH2rCJPLKuBShpibfqhRR2GRrc3gB3+kexRSYUuSLliIfMop4qnfvPsjiUXy3hZR1UqqI6wZNqOML4FxftnFNVFZEVpYiZXW09aMbM78idFKGrOTyeF+fH8VdPkX6GArgQPH7sa7GRpOQJZNYf9EAjkVLWSepY3YSktdEbsq7X5twTEVZwwtmltL2ihdRSmUci2WlKLEQzrmpFI7FlCaLUwR8Ph/2h5hPEe268biMjX3T4yii4iHsS8kkrinDiki5rQ3lqF2dsyGLaGsd5dnKZWVsY7WIEezXTpXSL80Dpkm/qoG/OSaEEEIIIYQQUvPw45gQQgghhBBCSM3Dj2NCCCGEEEIIITUPP44JIYQQQgghhNQ8VQu5pjWj7CPmsyGLhHAjtqVIsDQDgqVsui7mccO7S9mk3xBFEUE4jBKx1BiKd+KxGGTpApZ5V48ioyri5nGfssm8PYRV7fHixv2dw0nICgbv4VXMAokYCjNWLT4MstQ+RcqRw+vFG1FcUczhc2Qy+DMWvxfPndGK5WtuboGsX5F5TUZCISz7QBLHxLY9KIXa+MIGyFyKkKJSxH6YT6MAza2IW/JFFGONpjFLZ1DQ0N3zImSRILbfgjnzIRNF+vXwQw9CNquzE7J58+dB1tCAY9uvyFziMRRDuGwUUmSL2F/zORRc5JNpyCoVHLOBIPb1TArPjSlzlF8RGJYUkWAuh/Pg1EGZEKs2O70BA5Qi4jBqqJTPwvtaVf8sGc91HJwXNPlQOodzX08fipX6laxSwfloejOWedMTKP5rbm2DbN5hh0OmLRlcRqkrTYKiVJ9yqroOqBpr8v2831L6nM+H9agJZmxFPlTM4xxUF8L1mdeFletx4VxVKClrmACunUpFRYQ6hu8iXxTlPpp8yFKErhVFNBQMKPJWZY6MxhKQBZTnsCxc/2jvwHJJETop8i3tHqLIh4rKO6ZSUtZOHhQ/xRrqlVvgnJLKTo33RK6I85y7jHVhKfOwVt/adKPNLY4yMWlZJov9IRBUJGtaHy7jcYUijlnbUiRTSln8LkVQq05zeK7Hg+dq90jn8HnHtuIacGgYv4E0Wdz09umQ1dXVQebz49hW35829nXFcSy2UjEVRYZZDZPvTUIIIYQQQgghhLzN8OOYEEIIIYQQQkjNw49jQgghhBBCCCE1Dz+OCSGEEEIIIYTUPFULueoVyYKnlITMr0iFQv4QZMU8CgvKirQkkcBN3Jq4olTB7/xyGTf9hyIoO9g7iKKE7TtR5DOQxvLlMJKOIG6CP+O4QyGb3oZlufWp7ZA9urUPMttBOYbHpWy0Tw5Alkvj80ajKOqQiiZDwON8ilQoZOFxdgUra+a0aViWEZQZTUYS9Y2QbduzBbK9O7shC3uxDZLZUcgyY/2QWQ6aCJJpFCok89j/PX5sl8YWFPkEFXlUe8chkM1Q2r57/aOQuS3sr+UKihIGh1A0tHTpQsjmzJ2NZWlrgixyJI675zbthqxYQMlH0Yv17AjK+xyD/bqvrxcynx9lLvE6rHsRlNzkFTHh1EHTpVR7ZpVCLtXIgqH27jCC7afKt1RJl5ZpYDqzowOyUBT711gWZS6aeGrDbpwrgh7sc54CjsUXHvkTZA3t2DfrpuO4s2xNronPq7Wlo7yzlKhqlCY64Lhc2FZGechgCNdYBUXa4wujfKuSxfeJWLgWa21phcweVipckSqGfdiXisp7J96K8qhqhYKNLTiHFzNYFreyvvBqsixl7VnI4/rC78O6d/lwfTam1HO5jO8xt7LWKSiSV3Hw/RlUhFMeRWhWKGO9DAwO4j0mISVFumdVlPetstapKKI5Fb8yB7lxLDoubCuP8mVULuE87PNgW0WC2Fa5Eq7FbOW9U1SGYlGZX/0uRfArinxLeY9p31m2YB/W5q2+Efye2FvENdu2XbjGamrC9fK0aTMgi0RQ/BrwKxI2RVRWNoqQS1lnVgN/c0wIIYQQQgghpObhxzEhhBBCCCGEkJqHH8eEEEIIIYQQQmoefhwTQgghhBBCCKl5qhZyNSvyofwIbjJ3KRKITA5FBPmSsgnewg3WOUV2oH3R5xU5QaIO5SalCm5u37FnL2TDKbyv8eBGe7eywT8WwHObPSnIAkr9zY21QbavHu/Rr4i2ijmsg2c2oyDKZaPkoBzBupK4IgtSRADxOEovog7Wc6GE/cCUsF46mlA4MhnZvv1xyDZt3wbZ3r0oWasoIpNoHAUgC+Z1QrZk4RLI9g2iLGLXIIqdmlqxTWd14T2iDc2Q9Y/i9cwQysZ27dwF2WASpQ0LF0EkJ81D+VY2g8/mKI4FU1JEQ39FOdjc+csha2lPQPbXx/8MWV8/9tdyWZGvKGNxRBHNBSMoHHQUWUkmh3U/dXj9P4O1qpQzaaItUeYgx2DHKSvyIZ8iwLHUwmjiKe0wfLfV1eE79Zjj1kD2/LObIOvesROyio3Pts29D7JAZzueu3kr3vdPD0N2xOkoTAqGcN5SXI6qLEtT69hVCtw0GVrVC5q3kZ4BlHtq/TVcwHEfTeC7sFDCdo64UVjT3oZiLH8I68yNHkipC2H/TyjCsGirItBSZGNb+nCNlUjgmqOoSCkLivXUqzxvOaXMw0UUaDnKWHR7MctkcL62FTeetqZsSuCaqD6G7bE1hWuDhnp8JyhFllgY28NJoMxoMmIr7ziNiiKPKijt4lEMWtoc5HXhXO9ox3kx9GqziyIM015abh8K5MrKa9FRsrJyD7uCz+FSJI1GWetr8q2KW5lztTWWcpilyXfLeN+xvTi2d+7bCVnAh2M7FMLxFFDEdX7lve31KrJhWaZkE+FvjgkhhBBCCCGE1Dz8OCaEEEIIIYQQUvPw45gQQgghhBBCSM3Dj2NCCCGEEEIIITVP1f6KukYUL9RFUAjgcuHm52QKN2KXs7ip3lXBHeCO4MZu48ViRyKKoEGwfC9u3wxZpoiym0DAD1nQh/cNhnGjeJ0bJQJPbeuHzC7h9YrxVsia6vHZLEGZRdlGwVeuhAaJbA531ZcUqZClSM40g4rXpUhpXGiQ8CrSBFsRZhhFcDEZ+euf74PM0zIfsjmLlkIWLGG/XrhoLmTz502HrFLAujUupZ1lCMvnxb7kdicgK9vY/7PpEcjiiljPVtpvVz+eG4j04vViKCOZ3dUBmVF+rpdP5iDb9NgzeG4e637J2ndDtnTZbLzHkyjk2r4NpWShMEqK4oqASbNepFJYV8UCPtuUwVRpYlLPxb5kFGGTKnYy2De3bkPxVD6P8/+ChSiG8/tx3Lk0y5SCY/BcR3n9rjr6WMh2d+M4+dEPfgSZncf5etdgEjJ/GOeAuYr0cfNDT0LWNB3HxIKjD4csJ4pESbHN+JT6G8mhwKpYwveEJiDrbEG54IGmqEhxRkZwjIdy+P4uKu9gTQwUiCriLqUeM4rcShs8bhuPK6ZxHdcUxXlu89YdkEUCuE6KBHF9Vizie6yurQEyq6JIgJT6Cygr3HQB+43fj2XpUyRi4uBxkXgCskIe52u7jELSYADnhWgYpUIjisCzUMTnjSrtMRnR+rWlzAWOIlXUZHa20m/yRWwDryLGcisiK78HjzMWjmNLm9cVgZZRDKLKo0muguOupHwDuZT1dUmpP6/y7jUuRcjrUgTESvlcbsUMZyliZuXXrtqq3lHeCaU89vWxrGIHU6RkUsRztX4l8o9KNhH+5pgQQgghhBBCSM3Dj2NCCCGEEEIIITUPP44JIYQQQgghhNQ8/DgmhBBCCCGEEFLzVC3kEkW0ZXkx0/AH8LiQoDjAo3yru5Sd3WVlg7o/GIdsqA+lX7khlEp0KcIrxXUgAUW+NX9OO2Qu5WTbjXWQUkRlHjdKNKI+lG001M2BrGvuTMi6dz8O2abNKHjxeRUxlsHN7baNXcblQYGEJj7QRAWOYgOxFEHCZKR/9yBk7zjkVMj8fpTZ1Steg7ZpKFkbSWIf3rMNZS4lBwVaLgslBm4PtkHFYNuL0s4VRXphKni9aAKfdziN0iOX0q8dzQKhqRzwthIJYP11TMMxEXDj9VyCfX3pEpT7JBIJyO5UREh9+7CN2punQVZRZBZeLx6XSuG8MFXQ2tRSmlQTrRhFUKJOD4p0Y0/vbsj++/d3QabV7aqhAciOX30CZH4/jjvteZXuKrYydiLRKGSnvfc0yLZt3gLZfb+/F7KUIlp8sWcfZHUWioYCBazov96N9/A04Lvc1ZKALJvEevYqopp9qR7IxtJ4bqGAY6fzlIshO9C01GOb2gWcb6IR7EvGxrnF7cF2CQbxHaxNpTllrirZipBIMVktnI/CyL4+FI0Wi3jjxqZmyGxFqOOIslaM4HuilMOx41YEX25FNJQdwff2mCIvi8fxfZJRZKYVB0VbfmVtXFYkZ+2z8P2krYlGx/rwOGU9lajHep6M5JSx69EsTo7yiaI8dz6D/dDnx7aqb0HBaUhxPbmU945bG2MubPvk6LBSPhR5dnSivDVdxr4+Mop90+/Hb5GyJjlThJ/qGkvx9GnHaa5cn2AduBQpsV3Gfl1RhFzaC94o0mRndA9kw73b8Xrm9X1PTI2vEEIIIYQQQggh5C2EH8eEEEIIIYQQQmoefhwTQgghhBBCCKl5+HFMCCGEEEIIIaTmqVrIlS/gpmurjIIebWd3Nosbyktl/C63XShUyORwI3sqh5Ki9hn4KMbGc2c14qbwrnaUJ+QKeFz7vOWQ+QyKBUbHsK6CiUbIZBitTDNaUcaTzOJm9NkLUI4Rq8NN+rG6RVi+Qay/UU2WogiTXAalIWVFqqI4E6SiyGFcWM2qmGcyEoo0QOZVip5MotzHX5+ALGdjpSneCgnWoeDF7ygVWcB2McqIL5RzkAWCinjNUgQqLjwu0oB92GdQUOUO1mH5fDgmHAvLZ1WUvunGsnjDKNEIRjCzizgmhntR8tEQRtnYe09ZC9mT63dCllFkOIUiymGKeZxXE1Gsq6mDYjxRpBujishkbBT7jeXGvt43iGPs0SdRRvjUC+shS40kISsqcpPFS5dA1tyE87pb6YepNPbhZBLv2zEdhTHTpqNkZ91FH4ZsjyIj+euz+LzFLI6xrXtQ0hVqw+OGN2yALHc7RNJ19DsgG80ogkzl/V60kpCVyigNdJyp8Z6IBLAeF81BEVMwhO9vbU7r241tZdtYP+FIC2TJDL5Q3BbOh5YihUqPYfsNDuD8pQwdEUW0lVH6g2Nw7ZTL4fonk8I5MhZCgVZJsDDGUmRLigwqpsjxgiF8Do8H2zcaRcmr26W825SFUvduFAlaXmwjnxuvl85pa/LJR0WRk2nezTo/fhPEFTFuLqQsbJT1ii+D9RNQhHTNzTjnFoLYpiVbWesHsHzuED5HKIb9NRFug6y1UZv7lLWism7OKcftG8R1TTmbhMyrjEWPrcwfDtZzuYxj2+PGenEE61RbU0oer5fa2w1ZcRSfLZNRhLNVwN8cE0IIIYQQQgipefhxTAghhBBCCCGk5uHHMSGEEEIIIYSQmocfx4QQQgghhBBCap6qhVwVS5H7VHBTvSZT0jaoR6KY7R3EzfLde1D44FGsR77+XsgKfXju3BYUKrxzDcqttveiCCbajjKexoZWyAaUDe+JhCIQcrAsPkXaMDCIz+YJJCEbTKKoo3dfBjKvF+s+EceN+/k81rPx4M9TLMWq5SiSLpeFx1mKCKMyNTwrMm1WJ2Ta8xQKKJ3pT+HQ8ynStrKtyFK82G/yGWznssGyeDwoVLPdmGmyiOaGJGRmBMdsSRGvWQ6WJRhESYXS/cUxeL1KRelfXjzZuPG+mSzKHSxFXOFX2jKljO1gqB6y445aBtnm7bsg27CxD8uXQgGNz4viismJJg/RhFwYjaWGIHvokb9AtmtvD2RDqSRko0o7uxRBW6CIc/PAsFaWhyDr6JgBmd+P46m3RxMXocgkn0tClklj5lXe3AsPmw3ZM1ufg6yUxgl2TxLnqJAPyzw9gWO2+8mnIXP7cey4puE4GbNRVKZMASIG261YfH2ilbebiCIZDIewz3l9+IzxBNZZUBk7o8Mos3th42bIbGUe9vsikNW34X339uK4Gx7EcVKwca5KjWH/0qR8RhF5JpO4FtOkX6UihqEQ1n19QxyLopSlaCtrXkUCly8o62BBcZGtSKi0PlxR5sug0l80PIq4a1JiY1vFQyhAq1NEWz17UViWV+bcovJ9Yu3bCVlnA4rrmme0Q/bi3r2QGUWEGsoqQs0wjonndqMsMdKG7/6oH9d7OzZvhKwSQWlnYi6uQyLT5kCW3fUiZO4Mjtm4wXVmVnk/5dK4TvJ5cZ5JFXB8BhMoQ2sIYT1nBIVh2rpCW5NXA39zTAghhBBCCCGk5uHHMSGEEEIIIYSQmocfx4QQQgghhBBCah5+HBNCCCGEEEIIqXmqFnIlEriZ2vbghvdMBkUEpoyCgTFlE/euXbiJO6OIhoIB/KbftwM3j7cEUE7Q3j4LssQ0FJl404oZIoAb46cfcjge1ocCraCNcpOKIm3IZjFrC6EIrFTB8llhbKPp4WmQRRMoEUsPoxhooB9lG2UL67RQwjKLC8UVYT9KCUp5RRjmw3qejBgLZQJlRUaVS6MYyK/IqNIpFI+UCijsyKXwel5FRBANo6SiqQ5FK7F6lH00KeKdigdFJnk/Pu/ILOxzxQrK4qSMMp6KIupwFOlFxaX0f0XIlahHSYVTUe6rtFs8jnXgs7BfJ5W5zJSxXy9fiOMuEcU2uuuueyEbVOSCk5EXXkTJiMeD41mTUY0mk5AlM2OQ7d6H82u8uQGyeqX9GhpxLh3cjn3zxQ3PQ3bf/ffhfWN4D7cH+2GxhP2mVMR58+57MPMqP8KeNh2lJaFGrOflhy6E7OmHNkGWExxPm4cV+VwF54q6Mop0tv31KciSTTj/jyjj2FvC42xtXs3hOJaPYXSgmd6Kwh9NulSXwLnKrbxjvI04h7c2oczx/j8+CJnj4PXqoji/9u3FfthSj+2irQuT/SgkGurH9UWiHqWPYUWYF6/D9040jHUQjeNx4QiOCTuP5duxbSdkbkVemVMEWiVlLisVsX3dihzSUsZdMID3rVjKHFpGIVFZmVMmIy4by94Wwb7UNzoAWTmG/dUTxTnIpYwduzQKWceKxZCNKu1SqkORrdvCTyhXDMfJqLJmSxewHzqKkLGoCN/icbzHnrQiyxpEUd+sRAKyafNR3JXciH0p07MTstF+FI2msnjfio39fyyPbRmswzaPzsD3tp3Db75CHsenS7O8VgF/c0wIIYQQQgghpObhxzEhhBBCCCGEkJqHH8eEEEIIIYQQQmoefhwTQgghhBBCCKl5qhZypZO4wdpT0sRAyve2sh/a48Ywp8hX6qIoAElEcDN6fgQ3Zze3o6SlfdkayDb0oFBhyzbMVrWhBCKZxONaug6BzCUoDykVUbKTMCgCSA1g3QdLitCgXilfBeUO3mUo/sgnUUrz8O/vhKxnD26Wd6sCLdxon0cnjZSVn8+4FNHEpESRR3kczBR3gsyIY/0smJ2ALBJQhD/KGMumkpAVcjiegmGs2/lzsd/MmDUdMpcXZXYZRaI0o60N79GN/SamCF7q61DS4vGgpMVR+pJR5plAGCUatiK4UPxx4nVhPRcEhQ8NjSgSySiyoGwSpTTtTSiaOOP0d0F2x+/uxwJOQh55/BHI8qksZOEAzuunnfZeyGyD89dTz6NQKh5V5jQHhSLTmlGOVFYEQsksljm7Fe9b78c+Eo5jf4jUYTsHwjjXxxPYieMxHBOxGN4jGMG+vuaEIyAbG0pC9vzzOyCrlHGO2pVUhGFenP89+1DmmB7BcWcrQjNXEOuqZ/deyFJKv5qMGOWd7lfemZqwqaz0Q78b28UoRsaKo7xbXXhf9TckDr4nZs1CcWmjMn9N34diIL8f7xuL4xzgVp5tYAAFfKuOQBFq6zQUQdoG+2tqGNddo0MoahpOYt173PiiaGpMQOYoLyinooiVIiiSGh3DtaxxYb2U8vhsmlhyMlIfx+du1OpiBKWA9YoY16/0f03i1zJnPmSz22ZA9sJunA8TflyH2GVl/d+agMylrBGyHmV8RvEeo4O4buhoxvVZzqdILivYh0dGsf+72mZCNmPRkZD17MF3YCGPax2vNkdVcEy4lXmmmMS14qDgt6atrLFcyhxaQTdeVfA3x4QQQgghhBBCah5+HBNCCCGEEEIIqXn4cUwIIYQQQgghpObhxzEhhBBCCCGEkJqnaiGXsr9aKnncJG0UEZNLcGN8xULxyIjiYfKkcBO3KeLG87YEyh0OO/4EyKbPx03mt9/4E8haw7iB3l1CcUvvju147uxFkAUa5kAWNlh/uRHcjB50UDZTUjbBD6UxSzR1QtbQ2gFZPoPSFxdGUvGhBMJSZBFlRVRg2bgz3jKY2XbV3fKAsvqoFZDNXoQytr29KBRpn4YSrHlzuyBrbWqGzG2wvtPpJGTFMvYHra0iYRw7EUV65/ahPMerCMjyWRQ+vGMJyrw65nVAVlYEDUb5GZ7t4JxilEnK7cW+VC4oshRF3uFShBlWQJkIleOKilTO40aRSKWUhKxJkXccc+xheN9JyI6dKDIZG0DZzdzOuZAFg9gP9+7F+XBn927IImHsm2r/T+Ecnk8qEhtlnMztwvHZ1RSHLKpI5QYGFNlkPfabthlYB+kUPocPHU8ScPCdGlPKd9K78b04rAgt+3uw7oeKeOOwIhBqViRiHgvHXXsUpZnhllbIerq7ISvl8P05Gdm9Zw9k2pybTqPIKuFHIV1JcM6tKNLCcBQFR8U89vXmZlxf+F04Trpmt+NxiqTI5cWx6FOEXMGgIgdTxp1R1pnFFNZVOY5lbmjD/u+y8bhZM1Bw5A9gv05lk5D5fPiO8ViY2co7we3BMVspovTRHcB3grHxuEgYx9NkpKMV1z/vOxnnpV07OiBLF7DtiwUcE3YR+3rHNBRPGUWeZhpxDhpT1rTZHJZleiOu2WxFypfJ4lraBHC8RwyOT7eD6+aWOI677ACuxTK9+D4pa/N6C46J9iXHQuaUk5AN7MXvolxGma+V54iFcUx4BMesUT4Tyjm8nvZNWg38zTEhhBBCCCGEkJqHH8eEEEIIIYQQQmoefhwTQgghhBBCCKl5+HFMCCGEEEIIIaTmqdp8pLg0pKIIBiwXfm8rvhoxeTzXpUhG6htCkLWGcaP9O1bOh2zhKpRvjQ7gBnq/nYRs9vQZkDkWFrC1uQkyu4DlyyVxM3/JxuPKeWySiqCMYXtvD2TPb3gSslVH4n0bWlHakEqjfMWLVS+NHSgScZQ2r5QU0ZYiUhsbTEJWTCs3noSsWLYAssWHopArvwRFPuE4CmuU7i/GUgR3itipPowCCaOMO+2nYY6Dd7YVQZUo471YVMQtc1B6EfRhv8lnUVJkXMqUpMhNjDIhOQazilJ/jiLgKOXxOSoOltnl0YSDWKvpYZRe7OpGMc/RxxwKWa6M4oqQJgKbhGTHsE1zBaxbfwiFb2NpPHfXnp2Q1Sljp6LITawCCmv29W3DrBelJZYLzz37/e+DzMmMQPbHvzwI2a7ncL5uiKN8pW8rtnO7IpEZK/dDJl6cw+sbWiBbOn8JZKUzcYz9549/Blk+jfXcm8R3qiiCqEJJkdIMDUE2TWlfnyJvamxO4H0nIbkc9iVHkcSUFGFlfRPWo6PICAsFnJtnzMQ1zAvPb4bMq8xpba0oFWpqUsRAyprIi00lPj/2r5AyB7g182se3235FMqyRgax/xsX9tegMpdqZYlF8T2RyuF4NxWs+2AA5UiWMiY0cWlMERNWlDaKhfB6XnQZTUpibmyXo96B89zhi1ECl1bGU1lZ7JRtbD87pwgZlfdEZwnvmyvi+Mxk8XpeRQI6qvTXQCe2X16RsZlEI2S9ffsg26KIKhfX4TjeNYB9WBSZYyWAQr/IrHdAdlxXB2Qje1DItemppyAb6NsEWdhCgacUsxAVKlhmS1nLel7noOBvjgkhhBBCCCGE1Dz8OCaEEEIIIYQQUvPw45gQQgghhBBCSM3Dj2NCCCGEEEIIITVP1UIuR5FF5Iu4+dkXRnmURxERuBXhyZw2FD4Egvj93jELN+4fcszxkLXNXwbZs4/eCNnMGfWQtS5eCpmvCcVKnlAcslwBBSX5FEp2+veioGe0H8UtlTLKfYJRFEg0NqIJY8/eZyBraUPZgJ3DMps8tpGVxc3yFYNSAk2YFPRj+XytmKX8U0M+FAyjOCMSQMlOOKQMMw9KAhRPlFiakEuTTBkci05ZyRRplSbRsxU9mEtpFmPhuZEEjie7gterKBIIcfAmRnDucWmFqWBW8WD/MqJUtI1iFMvB+/qVMnsrWAfhAh5n+nGcDO5AsdL0+dMhG3Ip0qNJSEkRtOUUmca2bhRj/faO2yD7y5/+BJllsJ37U1g/AztRUOJVrHdlpZ19rTivP/znhyArplAotXErSo8yfShRSg7gfRONOK8PKuemFAlWXR1KgEoVLMuDDz4NWTCGksa6RpS5DJWHIcsVsXw9irjLKPN6Nol9w62IleoaE3icu+rlywFFEygW8zjf+JV1UrGE9egP4HzjUub6SgnHYno0CVkug7KgzplzIAsq7RcJobQnrvTDso3SqkoF68DtxmdrbMR7DAzgs+0bRNHQUxueg2yOIowcGMQ62LsPRX224JooEcPyeZX3p9+PY9tW1gHFAra58lqUUD2ul1OZqfGeyIzgOrKnewNk09s7IWtvQ8mgR+mHjiLyTCkCwGQSy9JQj/NhVpEI55RxnM3gnJbO4PtkftdsPDeriKcUWWhTENeZ3iKWb8URqyAbyeFxO/tQhllyYX+t5LFvSh1Kiactw3ZrWnYSZPYorn9GXnwMsh3PPwHZ0HZ8t7l8WH8uj6a6fXX4m2NCCCGEEEIIITUPP44JIYQQQgghhNQ8/DgmhBBCCCGEEFLz8OOYEEIIIYQQQkjNU7XRwqvIL0bTKIqqFNAcEAyhoMHtQilOc0MIsj17k5B1nfluyKYvxUwEhQXlNG7Yjkdxs3zTvOWQZT0oGnrhGdwoXszjPVKpJGRDvSiMcSuSikAA6769E6Vay+ahRMN2ozTK605g5sNN+p6CItfZ1QuZJmuzlR+7ZNwonwg1YPlapqEMYTISjWN/MIp8JVfENjVFFHsUleM0uUOpjMcVFRmDbaOIoFzG48rK9XI5HNu5LErlbAfvEa3H8RSNJyBLRBshC/hQSlNxsHxioQTIJZhFFXHd8ABer5BHkYnj4PxhCZbPqWBbxqIozJg1E0Ui+Ry2r3HwOeJRHCeTkbjS9mVlLkgpEqCNzz4LWd+OHZC5lNdWSBGv+V1KW5Ww7V2C76wZ01CKVh/F/jCawzlydscCyHZVUPoyOoxymKgf79GfxXtkczjnjo6g3MRS5tyChWVJ5lCQ5vLhe9txY50aH94jpwiJKsp8FFbuEYljHWiiJsdgHUxGWptaIfN7lXehH+s2GMK+aStrBK9ic4wFcB6Z045zUEJZn01rTkAW8WOZY2GcXwsuvJ7PwWdLjWH5AmE81xvCsd03iPP1nhF8Z23e1ofnDqBUKDWG1yuX8X23aOE0yCIBLF8lh+8EUWSORhFkBnzK9ZQ1lqWsye0Kvt8nI4kgvs/Sw9hW+5T1RWMrjom4UhfhaAJvHEdxl9vCOotiN5R4BM81yjvGVtZTL27cBFlTE4qsQiGUxeWUNeAhHbj+X73yHZDlbexfORx2MncG9q/+YXzv7O1D6V1fN4qFd1fwvgVFmhZM4Hs2sQS/5Q6dfxRk07tRtrf+4d9BNtjXDVk18DfHhBBCCCGEEEJqHn4cE0IIIYQQQgipefhxTAghhBBCCCGk5uHHMSGEEEIIIYSQmqdqIVcxjxKDkB9PtwIoHfC6cAe4qWAWjOC57zn3vZCtOvmdkMUaUTTRv+NFyNxKWZLpMcgGd26GbG8aN60/eMdvIYsEUahQKKLwobUF5TWxaASy7h4Ud5WU56if1gHZvKUrIJMKyoJGkj2Q5Qr4s5PRPN7XMtgPCnkUKWQU+YTJYL9amIBoUnLHnX+ArOJ9CLLRURTlZMZQxqM46lRJV38/Xq+iCFnqm5ohq2tE2ZlfkVlkR5KQbdmK42ksjdKSmbM7IHN7cUzEoliWzk4UUkyfgUKbztkopKj3o6gjqshSnHgMMlHERWVljnJ7cEy4lfu2dCiysRiOu7IiFVKcR1Jfr5R5EhJRhFweRSZWGkbJyNAWFHvMjOD1LEWCklbeT3lljrSCKBAKWNj2g33DkD312HrIWqIoGRkeTUKWVAR3GZwiJT+I7yJRhGEepZMEvYoERRGQDSaxfBWXIojyoJXGcmH/dynvfFGEXGJQfJNVZGOpFGZ1DQnlFlgvkxGj1G0giPJRrzK3eP2YFdIoeyqXNYkfjp3lh6IESOs3Xi/2L49HkyUq7ezCsej34TsmEsG52afMpcbBc71KP9y4CaVH2ZwiqKrg3KO9Z32KXNPlwjncWFhmx4XzeiqP/Tqdw7rSxnaphO1rF/HckiL6nIy0Ke8Jq4RtNdI/ANn651Ae+MwGXK+3tM+A7NjVx0HW3oRlKYzifO1W5kNR3kUeD/bXmdNQMhhU1iZ+H/brmA/nConifcsVvEc6j3War2B/fXHrTshGi4OQvWM2zh+ZZnze7n0oV3txF47PZ3dgW6b9CcgaY1gHi1twDXjY6ndB9vQj90JWDfzNMSGEEEIIIYSQmocfx4QQQgghhBBCah5+HBNCCCGEEEIIqXn4cUwIIYQQQgghpOapWsjlGBQWiIPSActGQYOtiDgsCyUQAT9KZ5avQKGUX5H7bHz2GchG926HrKhIDNKjKF/Zs20jZBmDG/K9FbxexIMCjlgApTRNdQnI9vXvg8wuY/3l0ij42tON4i6RFyDJZFCiFPBge9h+FDoN29hGQUVyE4piXQU9KLNI51J4XwflE5OR+x54BLLE9PmQmQq21dMP/xGyjhkokGhsQGlVzx6ljyhjMVSfgKzkwvHZ34MipHcefhRky5cthiynjCeXVxE07N4F2ZatOD6fex7HcV0CJXXv/8CZkB29eB5kPoM//5vehvVcUoRclksRrShSubJg3bs8mPkTOE6CiljGceNcizPe5MRRhCJGEYD43Ip8qIx1NjOO/d9WBEfpPI4Jdwz7jduP81KuLwlZMYlCltQwzptDjiItLOK5nSsOgWzfIL53kooILxLB5yjkcE4pe7F/FYo4l+bLOAe4lL4e8OH1jIXvoooi33IrUhqXjWPHUYRO/QOjkNnYNcTjmxpCrpLy/k5nsY+4oiidySfx/Vi28XqhIL6X3YosKDmchKyoCLnGMiiP0oQ/RulfXg+2i1cZs7mKIo9S2rmUx+M0GWzfPpwDCgb7cNGN9edTZGPuoFLmHBbQVqR3fh9eb6yAddo3PAKZEUVwZ7BOLQvLElTqZTLy3DOPQ2aGcY0Qb0AB1FMvoBj0xS07ITvmhBMhu/kXP4fs9HceA1ldQPk+CaJ80eNVxmwBx3ZTA66lHT9+E4xWKVSzlPdnWfldp6W8E7btQvnut6/5FmSDA9g3jzzqWMhOO+sfIWtuxXYL29j/p9nYr19I4juhosg1+5U15byZKGbuWoDr1mrgb44JIYQQQgghhNQ8/DgmhBBCCCGEEFLz8OOYEEIIIYQQQkjNw49jQgghhBBCCCE1z2vYvY+bpB0bRQTaBvWKYtMoCW6wbonXQ3bPnXdBVt+CkqlmTbKTG4PM60UpVCQch8yjCCTCigistbkRsnwaN7IH3Xjf4cFByMolrKtoACUyJUWqtfWZJyDbt2kLZEVlY7x48XkrWh1MR4mAhLEfuPwoagoooq06wWdbuHg23mMSctYHz4PM3zwXsly6D7Itz62HrK0V+7BLETYFA9hfSw626bwlWJa6NhRD5BpRtHLaySiz0CRrWUXI5SieHNvg/FGw8dwBRQKxq3svliWEApq+HhQc7XxhK2SuAt53R98AZIe/ayVkszqmQVauYL92BVDIIl5FYKjJ5xTRis/C+puMJJM4LxVzOD+ESzi3NLVi3Q7vwnbZ1r0TsoEytmmDIrNzKXNp1sE+VyljJ7ZzKEspFBVBjyKbHNg3hPfNZCEzZTw3HMB3aimP493yo3zFLmCZfWGcw01FGZ9FbDfHheUrKesAvxf7vy+gvHtDKBsLhlF8U1bqRZsbJyNDimRtWgv2TU3SZTvYr+uVfp1OYV+ybcyKijzKwaqVTdu6IXMpc5Am1pupzJGuCLZ9IYtjp6KUzy5hX/cr9x0dxfXell6U9nQ2tUHWEFXWgG58x2SzKPMatZN4rg/XimllzI7mlfenIpG0lKW618J3R1aZoyYjA6PY1zd5ca53D+C8uWsvitdWn3g8ZP/n8/8K2XXf+w/Ifvffd0K2oB3HmNenrIej2EcqFezX9cq3TVM9yqM8isjQp8jdXBYel1HWISUP9qUfXH8jZC9seh4ybQ6//f/+BrLp85dCtnQuylGDyvspZrDM0/CVILbyHFlF9GlK2P9ntc/EC1bB1Hi7EEIIIYQQQgghbyH8OCaEEEIIIYQQUvPw45gQQgghhBBCSM3Dj2NCCCGEEEIIITVP1UIuR7Hs+Dy4QT3gUcQxLmXjtBulIE4JZQdDQ7j5PjOIWbC8BK8nWL76Otxon5jWBJldwY3dvXtRrGREE4VgtZZs3HjutlDaoMlXbKVK3VqoiGAqpSSWT2nLVC4DWcmPAonoNKyXbBDvkXZQrFHI4s9iGmIo32psxjaajPh9+DxbNm2ALDWm9BuDbVVWZCQZRdpjWdh+AT/2pXIO5Uhjg3jf/t17IPvDPX+AbDStXC+DEpRoDCUV8ToUUoRjKGnp6UH5VnNjO2SBGIrFHvodlnlkK4rPKkWcZ7b19WNZsinI5i5E0UQ8hmM2XoeCl2AIhRTxMLabN4DzViiEdTUpyePziOKIsS2UfWTxsWWfheFeZe7LlJT5cCgJkduL4ynn4LlGmSPzyhxujCJPU0QmvYp80VYkWJbgfQdGUBgmyhxgFCGLN4gCspgieNGkmdoc5VbEKEHBNncpwiSvUi+WH/u1UdrDUq6nSWkmI7v34pymyX3sIr5vZ8xshUyTLqUyiszLVtpPkWzmFKHai9u2Q6ZJSvfuwWdrrMe5Ph5PQLZ1K8oStfXUe05dBZnf4DumPoEit2AK5/rhZBIyR5k/vIqkNJXBdWu2qMwpikTM5VOkZGWtr2O/dpQxMZrGd29jDMf7ZGR6J8pCK4Lri3JZkbGF0djUNgPXCEZZD8+YNh2y++64FbJ0H0pKQ0FsP78yv4oyh/s9OPdpMsJQENcS2vsk4MP7GkV4OJjHOt2wEYXGJ52EAtZDlh8C2Q9/9BPIHv0zrrtmtyYg84VwPA314dr42S0oEfZG8HlbY3iPSh7fY0FlnV4N/M0xIYQQQgghhJCahx/HhBBCCCGEEEJqHn4cE0IIIYQQQgipefhxTAghhBBCCCGk5qnaaOGycLN3wK9sCheUgoSVTebhKEqwcuUCZA1R3IzuUe5RGkOhjuPCc3NeFBu0tHTiuYocaf4y3Mz/yAP/g2UxKGjwKgKVvCLRiEVRNOHzoPDEbeHG80wB66973yhkyVGsv6KFZW6ajz87aU9gm5cM1vPoED6br6AIyNpRvpXP4bNNRtLDKBP4nzt+B9mePhReuRTRxPr1KNjQxDu2IgYSC/v1vf+NfdPnRSnUoe94B2QlH8pNUkVs0x27ByAbHn4Rr1fA8vXu64aseyeeu/LQFZB98hP/G7LHH30EMntsGLKxIgpt8ooIZvsT2G5/fhJlgGEPSl804Y5bkQ/FFCHX9A6cj977/nMhw1o58HgUyWBZETtl8tgGIykUoA0rbWV78bVlbKzvQh7nQ6uI83rZYN90KfKhcBznZrdbaWePUj7lx9Cq8Eq7niLocSmSS5dyD0d5Dpcb51e3ItKsOIqkS7uvUmaXUhjLUgqoZI5y37Iy5Xm0eXASYivtPJTEuT6uCPs00ZbWvzT5aDaP52p9xDiKeDOI1xsYwTXCs8/tgiwcRPlcsYBzpAj2OZ8iI3xxK96jJdQIWTSCc09rKx43vAvf25YH+3W/ItGbPgOvV1HkfUVFhpbLKMIp5dyK0h6xOMqbig7eI6uJCSchtuAYryjP4/OjAC2Mrkt1nPQPYPsNjeB6uKcP1wjGxv6qfe+Uy8ocicWTgPLOCisSVbciOQ4GcF4IKN9Ujhv70q4B/C4Sg8edceaZkK1ahSK8PXt6ILv9/94J2TPrZ0FWKeC7d7Qf58HSMN7DU8F3b9ZGifD2UVyzhf34fVIN/M0xIYQQQgghhJCahx/HhBBCCCGEEEJqHn4cE0IIIYQQQgipefhxTAghhBBCCCGk5qlayOXz4Hd0TpGluAO4gd5xo4gmV1ZEE17cyu734cZzrxfv4QvhLv14DI/rG8QN6rl2FG01z5gDWe/AEGSLDzsasszgXsh2bHkBsmwmCZnHjTKGeBw3lFuKzGJfL953907c8O5SJAexVqznpnrcBG8p0i9rBK9XN4pdq725HrLpiRmQbduIwozj0RdwwGlraYNsXifKlIzSVh63kinyLZcbx53RxBXKuBNFvjVtWjtka9auhSwawv4QD9RBtnHDesg2b90GWet0rJeCYilyK6KJDVs24X23bIEs1LkIst5e7HP1dfgcXh+OsVAEBRwjfSiHGerZCtngEM4zhQq2myZk2ZvEsbPqnXjcZCSTRklGKoUin2wG57lsVplblB/fxhI4L/mD+I7RsBQjUdCDbe/14djRZFleH7aVRxEm2Q6Od03IpelctMPcmlnJwgMrFRTGaEI/bU4pK8dVlLlME0R5NGma8iABTTajnatIuvyK4G4yUteAEqd4HOdr7blHUihxCipzZLmE9VNSZEEeH/YbnyKsKVVQSDQwgmXJ23i9+mgCsuldWAdlxbKWSiUh29mD0kdfE8qMXAavFwkpa6dmnP/jQZxT0kkUBO7ciRLJrvkoHyop0qNSBec3xUsluSzWc52yFgsG8NmKeZQeTUYGk7iWLitCXo8yzxkbK+2Z5zZAtvQQVFY+89zzeF/ld4QljyKeLeP8v28fPkehiM/hU+ZIL15OtLe814d93avMFRVFLJkp4Hu2vrEFssYGFOOmFUFma1srZCOjKD675x4U0xYyuA4YHsb1QlZ56XuU97tbGWN1LSh6blbW6dXA3xwTQgghhBBCCKl5+HFMCCGEEEIIIaTm4ccxIYQQQgghhJCahx/HhBBCCCGEEEJqnqqFXC1N+B1dHh6GLF/BTeFZ3IctxqXIIpRN67EYihx8Xtygns/i5vGgsmldSpg9+cgjkM2ej0Kdnh4URblcuCk85MfyuRUpWTCIUg5NVJPPY2bbKF6IKJvWV71jHmSBKMrLbDcKOCqKNC2/B2UDrjRKVZpDUcgOnbcEj0ugHOCpfTsgm4yMDI5AduQRqyBbtXo1ZH4/2hg8inzLpQgpHEW84Ba8niZpyZewTYd7UDIyUsD+MDKEz7tdkW/tHcBxEmlGEZj4sd9YioCvZKP4794//QWyjq6lkM2sx/sGXDgHhLw4dooFFKNsH0PxRzSKspSKIofpG0X5RGNjB2S5MrbvH//0OGQfveg8yA40Q8o7QeuHhQLOX8USZr4AzqU+RUSTy+Ec6VIkki6XYkFRMqPIPuwKtql2j2AI+5ImAtNMWxVF3KVhKfI+S9W5INkczgGauMujyMaM8r7Tnk0rny4gU8qsHBYIoCBnqgi50kp9Ow7Or+2tzZD5FPlWrojjJBxS5JkebFPLjZXr9WGfsxTRVi6vSNGCOIdHGiOQlV04dmwPZoE6fF7Hg3NAOoN1Ond2B96jD+fwvVmcK5IZfLfNmzsXsj27Ub6oiessZWmdHlP6gfL7KU2GGVHmlGxWkdoq667JSMVS+pwb5/WMMnbyGXyP9g3ie+fb370Osl3bUKiZUd5P23pRMqVJC7V5s6zM4VZJkRcrba/N4ZYy7oyl9TkFZS4NhrEsw8p7269ISlNj+J1VLGJZdu7swfIpIrWyUj7jx/6vvTl8Xixf2I9zTy6L5asG/uaYEEIIIYQQQkjNw49jQgghhBBCCCE1Dz+OCSGEEEIIIYTUPPw4JoQQQgghhBBS81Qt5Jo5Azc/xy2UMWzbgxvo+wdxO3WpgoKBSASLk80lIas4KFnQNrePDA5Bls7g5uxCeQyvZ/C+0Ug9ZP19uJG9J4vSKkcRvLQ0oWzMUkQdo0mURfjDWH+JOMoYfG6UzRRLygZ1D7ZvtojPUcqgHCPsYN3PmdEG2bTWBsj29KD4bHgQ+9BkJKxIMoZTWGfPPPcUZM3NdZC1NGN/KJeV/jCaxMIU8L4eTfrSOQ2yGXXYb3q37IMsm0GRQ0srtnOoEZ/NE0BhTC6PZW5rmwlZ316UOwwNJSGbNg3Nf5YiAcoUsV7Eg21ZdjQBDQof/Ip8qDSMQg9x4dhpmd6J5yqyKtVlNAkpl7HsYnB+8CiSnYDiV/IHUcSkmUcs5U3mVuY+xakiFVW+hW3vVsRdHh9mLi8+r095Xk1QpQledJEVonRXVehXl0hAps0zmiCtYmFZqpVvaeIi28b7FirK+FSULFpdTUZCYRTMVBShZlFpA48X+5dXEdFofV373YcyBYnHW50Erqi8TywP3jcUx/Kl07hmCypje1CRXHo8+H6qC+KzhRIoGo0EcC3R2ozHDfaP4vVC+BwtzbiGSadQUqQ4nkRx2UlcGYvRGNZLagzLN6isb40L30+TkYYGrEdRpKL5DL7TCxF8RpeF/SE5ksT7NqH0Ll7fBJmtvCgcg2PWLuOaqKLMc+UydghHsVFpc1pREfA52jtBEbW6lJdlUumvDz/yMGTHH388ZC9sfBEyxcEsJaX+NGmsJqQrKxesaGu2Et5jz649eF//65PU8TfHhBBCCCGEEEJqHn4cE0IIIYQQQgipefhxTAghhBBCCCGk5uHHMSGEEEIIIYSQmqdqIVesDk0OeUWcVNesiCEUIcVQP25kLygCEI8PRT7KYeIoG97LFbzHWB7FBuEgmmAKOZQF5Qso2Skp960omTFYL5kUygZiMayrWAwFEvk81v3QMD5bJBKGzFIkLZaNm9t9HhRD+NHBJj5FStMxpwOyfA7v8ec/vwDZc5sH8CaTEL8iMikWkpA9/PD9kJky9q9YCOu7XFYEcvk8ZB7l51wdnSi3WnLkIsi6ZqKkK7kHJVh9oygA8SljZ05DK2SDgxnIli1YAtnipfMh+9XPb4LMI4pASxHhlUqYGVuxpQSwnt1+vEfn7NmQDezZjNdT5E1BRaK3aOE8yAo5rKsZbSgSmYxoohWX4LujUtGETYqIQxFAFQrY/y03ikcsRdLiOHiPkiIAcTua4AjRpV/Ku0h5NksziykovitxFOGJrfRrR6lntyJRshWJTEkTyziYuZQ6qFbSpdWfu0r5ltaWk5GgInZyWZjlS7he8Sv9MOjHcy3BdvEpMi9RxkksjmO2kEpCVvIo6zM/tkFemXPdbiyz4jKSkrJG2JfH90799Ha83j6UewaV+SMQxXppiuP8OjS8C++bwPWoZjnL2PhwC9rwPeso68JcDuVDuSxmDZpYT3GtTkYqgv1GG88epa/H/bhG9njwU6a+DkVbos2RylyqzWl2CdfcTkWRFlY5V2lOLVuR5aazuB4oFrF/aTLMivK82rn/fdddkG3YuBGyJ55EuaxlKe935d1mawJKpR+Itg6oYL1o2l5LWXcFjCZ4fHX4m2NCCCGEEEIIITUPP44JIYQQQgghhNQ8/DgmhBBCCCGEEFLz8OOYEEIIIYQQQkjNU7WQyxPAQwMx3CxfH8HvbU8eN4B7g7jpOjWqFKeC1wsGWvAwRY5UKaKgyhfCe3g9+BxuN276LxpF5qJsgjdGkcMom++NIq6oYKSWT3wo90mO4vPmS7gZPa5IJTyKpMvlwXvkFPFH/1AastGMJhYYg+y+Bzbh9bSd9pOQnCJFE6Ue333K6ZA5JZSxuRWbhqPIgowmsVH6SEAR4fUlUWaUTm6BbCSPZbECaGPb9Mx2yIYfQXHd7M4FkB0+Zy5kpTwOgKDS140y7nLKuS43jndHERzlNRmIIoGYNR2FXIXMMGSLYyjCe+zJpyHbuwtlXvks9g2Tw7E9GYnFcG5xKkqFGxwnRWWuSilyMo8iGnIrmSZGESXyKmPWVvqDo4lWFPmWKCIwS3kniCKC0XAUkYk6Lyg/63aUd1Yxj2OnXMa6dxQxlrgU0RYepQtolCNDypziU4RhLkXwpUl4JiM+N7ZLKIRzs9Zf3UqHdStSrUoF28+2lbWJUpZ0Gt/f+VSqqrIElHVhSXmPlZX3SS6J60JNAhptSECmrX/KOXy3uX2KaFSRPBkvPkc0pghJlb6ZqEeZl0nhO8FyYf0V0jjX53NKPSv9RZPeqZanSYhlYT16vcq8qfR1Ud4nXi9KobSJySh15lfWU5oF0adMN5bg/GUr/b+iyQOVtnLFsCwNTSjM00StRpnrK8p7wnGwf2Uy2A/39fVB1tHRCVlakcXlFGms1iCqpEt5DqPUnyZNc2nfMco7qxr4m2NCCCGEEEIIITUPP44JIYQQQgghhNQ8/DgmhBBCCCGEEFLz8OOYEEIIIYQQQkjNU7XRIpNRNry7IxBFwijF8QZx03XYjxvZ43HcdJ1J4cbuTAo3imcUiUG5gFnUh5vbA8pmfruIsgiPB3+W4FN+vOD140ZxS5G0hCJY/S6lRWxFtuELYpljCZQ2jIygbCOtbHiP1WO95Gysg607UTTx4nN7IGupRzFPy3SUFIkLy9IYj+Jxk5BwBMUecUUCEW2aB1lR6V8B5WdVPkuRhwQVUUgIj3MKKDNKpxXRSgjbqrkrAVlXaAiyLd0o5BJNthFGgUrvvt2QNTTWQdbYVA9ZMYcCiUIRhW/ZDM5HxRyOiXIR5WqeAI6nlmlNkO3c2w9Z/+5tWL4Mlm/bhmcga2jAe5g6rIPJiKX0YUuxEZbK2P8LRZzry4p4TRNxaEJBo8hISjaKTIo2vicsReJhabIPRdyiSUEcG+tAU+do6hBF5aKKZTTpi7Ewc3uUu7iV97t6XyXTpCoVRSKmPbDyLnIp70rtOLusyNAmIWFFHuVRWlr7TUVAEZZlMjivu5Ux4fPjfYNhfAerxymFyY8lIWtpnglZQRF3JcL4zvI2Ke82pbOXBecKbU0UjOCzeUP4bNogKyvjqbEJ1yE+Bxdobg+OHb8fn9cYfBeFQriGDmplVto3rwhB86oIafJhDD6PUUyZltJYmodMEwCqki5FqKaJzbR5XTvXrcz1XmWi04SHqjBSm1+V67kt5ZtFEYhqrjFNQBmMJiCbPktZUyplyZeUby9NLqu0kaUIArX3iXauNudpdVpQ1trVwN8cE0IIIYQQQgipefhxTAghhBBCCCGk5uHHMSGEEEIIIYSQmocfx4QQQgghhBBCap6qhVw9uzArJlEWEW3CjdiBIG5Gj6OHQOrrsTiZLEoHkknMRodx8/gouqPE7eAmbkcViiib5R3MtJ8uaDIXtwefLV9RNqNj9YnXwfqzcyOQVRRBQ0WRRSQzeJyyp15GFBla91as1OSwcr0sXrA13grZolntkCm3nZTk0lswdBRBg4Wdvb8f5UxbN+6ELOBBsYcvnoCssRlFVtMa45Bp4qKGOMrYFJeRFPKjkLU0o8xr+jSUR+3tQ4ne5s0bIesszcb7FlCokE4nIcvl8B6pMRSQFXMotKmUsNO5/Sh42bABZVklRfjQ3NwC2fRDluJxTXhcYxOOk4BSlsmIJs4oFlGqpYm2SiUU1pSUc0ua7EMx+WgyF03iEVCERC5FvlJRZF7VykMslyKC0aRMyvj0aVYVhUIB689WyqxJZPzKPbRn0+QmuRyOHU1yo8mltPnILuE9NElXIKCIiyYhXqUeXYo8x+fGNUK1fUTrcz5NNGrjWsJR1hcB5R7xKL7HlKWOBHw4VznKAiMUQeFhWRnvhTzKFzWJXsiH9edVZGhZReYYiOK7Ml/CNsor5fMarGe3Mt5dbnyXK0tAyeWxLZNJXO9pY9vnw3XwZKSkyHK1OUPxNalCKVXYpKy5LWVeN4oa0VEyTarrUsRYXkWWa9w4xvzaw6lgvWhzs9YfyiXsr9q7Ujs3V8LjVOGVMqdobSlu5TmU6xltLlP6tUdpX41wCOeZauBvjgkhhBBCCCGE1Dz8OCaEEEIIIYQQUvPw45gQQgghhBBCSM3Dj2NCCCGEEEIIITVP1UKuircRsrLvMMiKjiLTsIcgC8Rxc3aiCYUddS7cKF6fU4QFIyg7SA7h5vt8Fh+5YisSA6Ns+rfxvoU8SlC0zeNuRQSQLuD18hm8ntfgpvqoC0VIjgslT+UyPq8/jJv5A16s+4QP27JLEpAtW44CjvnLlkPWMWcOZIcfhTKvnr0oTJqMOIpAyKX8vMlTxraPebHtn3z0Qcj6+nHsWF6UjBxxxErIjjkKs7Ex7CPPPf0YZFlF7rN5127IduzcCVk+h21qjCLoiaHcKpVKQ5YexTrIplAOpiggxKNIIOJRFDRM6+yErL5xGmTN01CWNe1QFG3Vx3BMaGIlTRAllpIp89FkpFxGOYcm39IEIKJIRlTphiq3QrS61WRGRrEKlZXyaWXRBCWWInNxu1HS4tKeQxGZaPKVaqUl2vNWK+7yKkInT5V1qtWLdg+/ItUK+XF8au2rSl8mIUEf1qNWP0YRfmr9JhZT3v2aBE6pn2QS503jYLvEg7ieiijCK6MITvNFZUw4ivRIEetFw1G8B54qij9UsorIzVvGMZHP43G2C6VyQ4rMMTOEWaIO18bDWaznQFCZewzW6agiOE0p79Sg0kahEGaTEW09oI3yiiJeEwszvyJV1N5FlQpmXmV8auPJI8o4VvqwrfRXVdyovCdcyrtIG8eWMud6/cr6wov9X7ueNh9pdVBW5FsuZf5wtPlfydxKP3CqFF9qmYb2fqrqvNd1FiGEEEIIIYQQchDBj2NCCCGEEEIIITUPP44JIYQQQgghhNQ8/DgmhBBCCCGEEFLzWKbaXc2EEEIIIYQQQshBCn9zTAghhBBCCCGk5uHHMSGEEEIIIYSQmocfx4QQQgghhBBCah5+HBNCCCGEEEIIqXn4cUwIIYQQQgghpObhxzEhhBBCCCGEkJqHH8eEEEIIIYQQQmoefhwTQgghhBBCCKl5JtXH8c6dO8WyLPnmN7/5pl3zwQcfFMuy5MEHH3zTrlkrvJb2uOqqq8SyrAlZR0eHrFu37i0qHXmrmYzjcc2aNbJkyZJXPW5/2X/605++rvvsx7Isueqqq97QNcjUYir3e0LeTibjWKkGy7LkE5/4xKse99Of/lQsy5KdO3e+ZWUhBzdTdYzUOm/443j/5PHkk0++GeU56MnlcnLVVVexU5O3BI5HUouw3xNSHRwrhLwyHCPV88gjj8hVV10lyWTyQBflTWVS/ea4FsjlcnL11VcfdB/Hn//85yWfzx/oYhAiIiKzZs2SfD4v//iP/3igi0IIIWQK8o//+I+Sz+dl1qxZB7oohExKHnnkEbn66qv5cUyIhsfjkUAgcKCLQYiI/O2vzQUCAXG73a94XDabfZtKRMjkxrZtKZVKB7oYhEwa3G63BAIB2DJGCDm4eVs+jkulknzxi1+UFStWSDwel3A4LMcee6w88MADL3vOt7/9bZk1a5YEg0FZvXq1bNiwAY7ZtGmTfOADH5D6+noJBAKycuVKufPOO1+1PLlcTjZt2iRDQ0OveuxDDz0kZ511lsycOVP8fr/MmDFDPv3pT8NvSdesWSNr1qyB89etWycdHR0i8re9B01NTSIicvXVV4tlWbCn8Y9//KMce+yxEg6HJZFIyHvf+1558cUXJ1xz//7eLVu2yIc//GGJx+PS1NQkX/jCF8QYI3v27JH3vve9EovFpLW1Va655hoo18DAgFx44YXS0tIigUBADjnkELnpppteth5erT20PccayWRSPvWpT8mMGTPE7/fLnDlz5Gtf+5o4jvOq55I3h6k8Hvfz1FNPyapVqyQYDEpnZ6dcf/31E/5c23O8bt06iUQisn37djnllFMkGo3KP/zDP4iISLFYlE9/+tPS1NQk0WhU3vOe90hPT0/V5SGTn4Oh32/cuFGOP/54CYVC0t7eLl//+tfhmGrm9r/fB3fttddKV1eX+P1+2bhxo4iIXHfddbJ48WIJhUJSV1cnK1eulF/+8pcTrtHb2ysXXHCBtLS0iN/vl8WLF8tPfvKTqp+FTF6m8ljZunWrvP/975fW1lYJBAIyffp0Offcc2VsbAyOveOOO2TJkiXj/ffuu++e8OfanuOOjg457bTT5N5775Xly5dLIBCQRYsWye233/6qZSMHD1N5jIiIPPbYY3LKKadIXV2dhMNhWbZsmXznO98Z//PnnntO1q1bJ7Nnz5ZAICCtra1ywQUXyPDw8PgxV111lVx++eUiItLZ2Tn+TXMw7NF/Wz6OU6mU/PjHP5Y1a9bI1772NbnqqqtkcHBQ1q5dK88++ywc/7Of/Uy++93vysc//nG54oorZMOGDXLCCSdIf3//+DEvvPCCHHnkkfLiiy/K5z73ObnmmmskHA7LGWecIb/97W9fsTyPP/64LFy4UL73ve+9atlvueUWyeVy8rGPfUyuu+46Wbt2rVx33XVy3nnnveZ6aGpqkh/84AciInLmmWfKz3/+c/n5z38u73vf+0RE5P7775e1a9fKwMCAXHXVVfKZz3xGHnnkETn66KPVznbOOeeI4zjy1a9+VY444gj58pe/LNdee62cdNJJ0t7eLl/72tdkzpw5ctlll8mf//zn8fPy+bysWbNGfv7zn8s//MM/yDe+8Q2Jx+Oybt26CYNjP9W0RzXkcjlZvXq13HzzzXLeeefJd7/7XTn66KPliiuukM985jOv6Vrk9TOVx6OIyOjoqJxyyimyYsUK+frXvy7Tp0+Xj33sY1UtzG3blrVr10pzc7N885vflPe///0iIvLRj35Urr32WnnXu94lX/3qV8Xr9cqpp55aVXnI1OBg6Pfvfve75ZBDDpFrrrlGFixYIJ/97GflD3/4w/gxr3Vuv/HGG+W6666Tiy++WK655hqpr6+XH/3oR3LppZfKokWL5Nprr5Wrr75ali9fLo899tj4ef39/XLkkUfK/fffL5/4xCfkO9/5jsyZM0cuvPBCufbaa6t6HjJ5mapjpVQqydq1a+Wvf/2r/K//9b/k+9//vlx88cWyY8cO+Guff/nLX+Sf//mf5dxzz5Wvf/3rUigU5P3vf/+Exf/LsXXrVjnnnHPk5JNPlq985Svi8XjkrLPOkvvuu+9VzyUHB1N1jIiI3HfffXLcccfJxo0b5ZOf/KRcc801cvzxx8tdd9014ZgdO3bIRz7yEbnuuuvk3HPPlf/6r/+SU045RYwxIiLyvve9Tz74wQ+KyN8+/Pd/0+z/JeCUxrxBbrzxRiMi5oknnnjZY2zbNsVicUI2OjpqWlpazAUXXDCedXd3GxExwWDQ9PT0jOePPfaYERHz6U9/ejx75zvfaZYuXWoKhcJ45jiOWbVqlZk7d+549sADDxgRMQ888ABkV1555as+Xy6Xg+wrX/mKsSzL7Nq1azxbvXq1Wb16NRx7/vnnm1mzZo3/9+Dg4Mvee/ny5aa5udkMDw+PZ+vXrzcul8ucd95549mVV15pRMRcfPHF45lt22b69OnGsizz1a9+dTwfHR01wWDQnH/++ePZtddea0TE3HzzzeNZqVQyRx11lIlEIiaVShljXlt77C/T3zNr1qwJ9/3Sl75kwuGw2bJly4TjPve5zxm32212794NdUJeGwf7eFy9erUREXPNNdeMZ8VicXzslEqlCWW/8cYbx487//zzjYiYz33ucxOu+eyzzxoRMf/8z/88If/Qhz5UdbnIgaVW+v3Pfvaz8axYLJrW1lbz/ve/fzx7rXN7LBYzAwMDE+713ve+1yxevPgVy3PhhReatrY2MzQ0NCE/99xzTTweV9+bZHJwMI+VZ555xoiIueWWW17xOBExPp/PbNu2bTxbv369ERFz3XXXjWf766q7u3s8mzVrlhERc9ttt41nY2Njpq2tzRx66KGveF8yNTiYx4ht26azs9PMmjXLjI6OTvgzx3HG/7c2h//qV78yImL+/Oc/j2ff+MY3YIwcDLwtvzl2u93i8/lERMRxHBkZGRHbtmXlypXy9NNPw/FnnHGGtLe3j//34YcfLkcccYT8/ve/FxGRkZER+eMf/yhnn322pNNpGRoakqGhIRkeHpa1a9fK1q1bpbe392XLs2bNGjHGVPVPtASDwfH/nc1mZWhoSFatWiXGGHnmmWeqrYJXZd++ffLss8/KunXrpL6+fjxftmyZnHTSSePP/vd89KMfHf/fbrdbVq5cKcYYufDCC8fzRCIh8+fPlx07doxnv//976W1tXX8Jz4iIl6vVy699FLJZDLypz/9acJ9Xq09quWWW26RY489Vurq6sbbbGhoSE488USpVCoTfrtN3jqm8ngU+dv+9ksuuWT8v30+n1xyySUyMDAgTz311Kue/7GPfWzCf+9/jksvvXRC/qlPfaqq8pCpwVTv95FIRD784Q+P/7fP55PDDz/8Dc3t73//++Gn/IlEQnp6euSJJ55Qy2GMkdtuu01OP/10McZMmMvXrl0rY2Njan2SqcNUHSvxeFxERO655x7J5XKveOyJJ54oXV1d4/+9bNkyicViE8bTyzFt2jQ588wzx/87FovJeeedJ88884z09fW96vlk6jNVx8gzzzwj3d3d8qlPfUoSicSEP/v7rZF//+1TKBRkaGhIjjzySBGRmpjf3zYh10033STLli2TQCAgDQ0N0tTUJL/73e/UfSBz586FbN68eeN/tXjbtm1ijJEvfOEL0tTUNOH/rrzyShH5276rN4Pdu3ePf7BGIhFpamqS1atXi4ioZX+97Nq1S0RE5s+fD3+2cOFCGRoaAnnQzJkzJ/x3PB6XQCAgjY2NkI+Ojk6419y5c8Xlmtj8CxcunFCW/bxae1TL1q1b5e6774Y2O/HEE0XkzWsz8upM1fEo8reFSTgchvKIyKv2SY/HI9OnT5+Q7dq1S1wu14SFkog+FsnUZir3++nTp4PXoa6u7g3N7Z2dnXCfz372sxKJROTwww+XuXPnysc//nF5+OGHx/98cHBQksmk/PCHP4Tn/shHPiIinMsPBqbiWOns7JTPfOYz8uMf/1gaGxtl7dq18v3vf18t80vXTyI4nl6OOXPmwFis9h1EDh6m4hjZvn27iIgsWbLkFY8bGRmRT37yk9LS0iLBYFCamprG3xdv5rfPZMXzdtzk5ptvlnXr1skZZ5whl19+uTQ3N4vb7ZavfOUr4w31Wtgvb7rssstk7dq16jFz5sx5Q2UWEalUKnLSSSfJyMiIfPazn5UFCxZIOByW3t5eWbdu3QSJlGVZ438P/6XXeKvQTLwvZ+fVyvZ24ziOnHTSSfIv//Iv6p/vf7mQt5apOh7fDPx+P3w4kNpgqvf7t2Ju//vfDuxn4cKFsnnzZrnrrrvk7rvvlttuu03+4z/+Q774xS/K1VdfPf7cH/7wh+X8889Xr7ts2bLXXSZy4JnKY+Waa66RdevWyf/9v/9X7r33Xrn00kvlK1/5ivz1r3+d8IPRybxWIpOfqTxGquHss8+WRx55RC6//HJZvny5RCIRcRxH3v3ud9eEQPdt+Ti+9dZbZfbs2XL77bdP+Gnb/p+GvJStW7dCtmXLlnHr8+zZs0Xkb39dbP9vHd8Knn/+edmyZYvcdNNNEwRcmnShrq5O/es4L/1J/csZnff/O3qbN2+GP9u0aZM0NjbCb8teL7NmzZLnnntOHMeZ8KGwadOmCWXZz6u1R7V0dXVJJpN5S9uMvDpTdTzuZ+/evZLNZieMhy1btoiIvOY+KfK3/u44jmzfvn3Cb4u1sUimLlO931fDa53bX45wOCznnHOOnHPOOVIqleR973uf/Nu//ZtcccUV40b3SqUyaZ6bvLlM9bGydOlSWbp0qXz+858fl5pef/318uUvf/lNuf7+3/L9fd28kXcQmXpM1TGy/2/Ibdiw4WXvMzo6Kv/zP/8jV199tXzxi18cz7VnOFj/mbO3bc+xyMSfyD322GPy6KOPqsffcccdE/5u/eOPPy6PPfaYnHzyySIi0tzcLGvWrJEbbrhB9u3bB+cPDg6+YnmqVZ5r5TbGqNbPrq4u2bRp04R7r1+/fsJfRxMRCYVCIiJgTmxra5Ply5fLTTfdNOHPNmzYIPfee6+ccsopr1jW18Ipp5wifX198utf/3o8s21brrvuOolEIuN/bXw/r9Ye1XL22WfLo48+Kvfccw/8WTKZFNu2X+OTkNfDVB2P+7FtW2644Ybx/y6VSnLDDTdIU1OTrFixoqpr/D37n+O73/3uhJzW3YOLqd7vq+G1zu0aL7X1+nw+WbRokRhjpFwui9vtlve///1y2223qf8Uyas9N5n8TNWxkkqlYB2xdOlScblcUiwWX/Hc18LevXsn2INTqZT87Gc/k+XLl0tra+ubdh8yeZmqY+Qd73iHdHZ2yrXXXgvfIfufRXs2EX1NtP+XFC+91lTnTfvN8U9+8hP4N+JERD75yU/KaaedJrfffruceeaZcuqpp0p3d7dcf/31smjRIslkMnDOnDlz5JhjjpGPfexjUiwW5dprr5WGhoYJfx33+9//vhxzzDGydOlSueiii2T27NnS398vjz76qPT09Mj69etftqyPP/64HH/88XLllVe+4ub1BQsWSFdXl1x22WXS29srsVhMbrvtNnVPygUXXCDf+ta3ZO3atXLhhRfKwMCAXH/99bJ48WJJpVLjxwWDQVm0aJH8+te/lnnz5kl9fb0sWbJElixZIt/4xjfk5JNPlqOOOkouvPBCyefzct1110k8Hq9a2lINF198sdxwww2ybt06eeqpp6Sjo0NuvfVWefjhh+Xaa6+VaDQ64fhq2qMaLr/8crnzzjvltNNOk3Xr1smKFSskm83K888/L7feeqvs3LkT9kuT18fBOB73M23aNPna174mO3fulHnz5smvf/1refbZZ+WHP/yheL3e6iro71i+fLl88IMflP/4j/+QsbExWbVqlfzP//yPbNu27TVfixxYDuZ+Xw2vdW7XeNe73iWtra1y9NFHS0tLi7z44ovyve99T0499dTx87/61a/KAw88IEcccYRcdNFFsmjRIhkZGZGnn35a7r//fhkZGXlTnoe8dRyMY+WPf/yjfOITn5CzzjpL5s2bJ7Zty89//vPxH+i8WcybN08uvPBCeeKJJ6SlpUV+8pOfSH9/v9x4441v2j3IgedgHCMul0t+8IMfyOmnny7Lly+Xj3zkI9LW1iabNm2SF154Qe655x6JxWJy3HHHyde//nUpl8vS3t4u9957r3R3d8P19v9C4l//9V/l3HPPFa/XK6effvqb9jddDxhvVHe9X3n+cv+3Z88e4ziO+fd//3cza9Ys4/f7zaGHHmruuusu+GeO9ivPv/GNb5hrrrnGzJgxw/j9fnPsscea9evXw723b99uzjvvPNPa2mq8Xq9pb283p512mrn11lvHj3mj/4TGxo0bzYknnmgikYhpbGw0F1100bjy/+//mRhjjLn55pvN7Nmzjc/nM8uXLzf33HMPPKMxxjzyyCNmxYoVxufzQTnuv/9+c/TRR5tgMGhisZg5/fTTzcaNGyecv/+fTRocHJyQn3/++SYcDsMzrF69Gv5pjv7+fvORj3zENDY2Gp/PZ5YuXQrP81rao5p/yskYY9LptLniiivMnDlzjM/nM42NjWbVqlXmm9/85vg/w0NePwf7eNzfl5988klz1FFHmUAgYGbNmmW+973vTTju5f4pJ218GGNMPp83l156qWloaDDhcNicfvrpZs+ePfynnKYItdLvX4r2fnmtc/tLueGGG8xxxx1nGhoajN/vN11dXebyyy83Y2NjcJ+Pf/zjZsaMGcbr9ZrW1lbzzne+0/zwhz981echB46Deazs2LHDXHDBBaarq8sEAgFTX19vjj/+eHP//fdPOE5EzMc//nE4/6Vrlpf7p5xOPfVUc88995hly5YZv99vFixY8Kr/fBSZOhzMY2Q/f/nLX8xJJ51kotGoCYfDZtmyZRP+GbOenh5z5plnmkQiYeLxuDnrrLPM3r171Xt86UtfMu3t7cblch00/6yTZQztA4QQQgghhLwSHR0d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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Get the first 10 samples from CIFAR10\n", + "dataiter = iter(trainset)\n", + "images, labels = [], []\n", + "for i in range(10):\n", + " image, label = next(dataiter)\n", + " images.append(image)\n", + " labels.append(label)\n", + "\n", + "# CIFAR10 label names\n", + "cifar10_classes = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']\n", + "\n", + "# Plot the CIFAR10 samples\n", + "fig, axes = plt.subplots(2, 5, figsize=(10, 4))\n", + "for ax, img, lbl in zip(axes.ravel(), images, labels):\n", + " ax.imshow(img.permute(1, 2, 0).numpy() * 0.5 + 0.5) # denormalize\n", + " ax.set_title(f'Label: {cifar10_classes[lbl]}')\n", + " ax.axis('off')\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "e4e25962ef8e5b0d", + "metadata": { + "collapsed": false + }, + "source": [ + "Define the MLP and Convolutional Autoencoder" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "26f2513d92b78e1e", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:41:21.511063666Z", + "start_time": "2023-10-03T12:41:21.485979560Z" + } + }, + "outputs": [], + "source": [ + "\n", + "class Autoencoder(nn.Module):\n", + " def __init__(self, input_size, hidden_size, type='mlp'):\n", + " super(Autoencoder, self).__init__()\n", + " self.type = type\n", + " if self.type == 'mlp':\n", + " self.encoder = nn.Sequential(\n", + " nn.Linear(input_size, hidden_size),\n", + " nn.ReLU(True))\n", + " self.decoder = nn.Sequential(\n", + " nn.Linear(hidden_size, input_size),\n", + " nn.ReLU(True),\n", + " nn.Sigmoid()\n", + " )\n", + " elif self.type == 'cnn':\n", + " # Encoder module for CIFAR10\n", + " self.encoder = nn.Sequential(\n", + " nn.Conv2d(input_size, 16, 3, stride=2, padding=1), # 16x16x16\n", + " nn.ReLU(True),\n", + " nn.Conv2d(16, 32, 3, stride=2, padding=1), # 8x8x32\n", + " nn.ReLU(True),\n", + " nn.Conv2d(32, 64, 3, stride=2, padding=1), # 4x4x64\n", + " nn.ReLU(True)\n", + " )\n", + " # Decoder module for CIFAR10\n", + " self.decoder = nn.Sequential(\n", + " nn.ConvTranspose2d(64, 32, 3, stride=2, padding=1, output_padding=1), # 8x8x32\n", + " nn.ReLU(True),\n", + " nn.ConvTranspose2d(32, 16, 3, stride=2, padding=1, output_padding=1), # 16x16x16\n", + " nn.ReLU(True),\n", + " nn.ConvTranspose2d(16, input_size, 3, stride=2, padding=1, output_padding=1), # 32x32x3\n", + " nn.Sigmoid()\n", + " )\n", + " else:\n", + " raise ValueError(f\"Unknown Autoencoder type: {type}\")\n", + " \n", + " def forward(self, x):\n", + " x = self.encoder(x)\n", + " x = self.decoder(x)\n", + " return x\n" + ] + }, + { + "cell_type": "markdown", + "id": "91a01313b4d95274", + "metadata": { + "collapsed": false + }, + "source": [ + "Check if GPU support is available" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "67006b35b75d8dff", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:41:22.884901160Z", + "start_time": "2023-10-03T12:41:22.860000300Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "cuda\n" + ] + } + ], + "source": [ + "# device\n", + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "print(device)" + ] + }, + { + "cell_type": "markdown", + "id": "8eebf70cb27640d5", + "metadata": { + "collapsed": false + }, + "source": [ + "Define the training function" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "5f96f7be13984747", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:41:23.954782535Z", + "start_time": "2023-10-03T12:41:23.943574306Z" + } + }, + "outputs": [], + "source": [ + "# Define the training function\n", + "def train(model, train_loader, optimizer, criterion, epoch, verbose=True):\n", + " model.train()\n", + " train_loss = 0\n", + " for i, (data, _) in enumerate(train_loader):\n", + " # check the type of autoencoder and modify the input data accordingly\n", + " if model.type == 'mlp':\n", + " data = data.view(data.size(0), -1)\n", + " data = data.to(device)\n", + " optimizer.zero_grad()\n", + " output = model(data)\n", + " loss = criterion(output, data)\n", + " loss.backward()\n", + " train_loss += loss.item()\n", + " optimizer.step()\n", + " train_loss /= len(train_loader.dataset)\n", + " if verbose:\n", + " print(f'{model.type}====> Epoch: {epoch} Average loss: {train_loss:.4f}') \n", + " return train_loss" + ] + }, + { + "cell_type": "markdown", + "id": "5f6386edcab6b1e4", + "metadata": { + "collapsed": false + }, + "source": [ + "The evaluation functions for the linear classification" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "b2c4483492fdd427", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:41:24.972220830Z", + "start_time": "2023-10-03T12:41:24.960696742Z" + } + }, + "outputs": [], + "source": [ + "# Extract encoded representations for a given loader\n", + "def extract_features(loader, model):\n", + " features = []\n", + " labels = []\n", + " model.eval()\n", + " with torch.no_grad():\n", + " for data in loader:\n", + " img, label = data\n", + " if model.type == 'mlp':\n", + " img = img.view(img.size(0), -1)\n", + " img = img.to(device)\n", + " feature = model.encoder(img)\n", + " if model.type == 'cnn':\n", + " feature = feature.view(feature.size(0), -1) # Flatten the CNN encoded features\n", + " features.append(feature)\n", + " labels.append(label)\n", + " return torch.cat(features), torch.cat(labels)\n", + "\n", + "# Define the loss test function\n", + "def test_loss(model, test_loader, criterion, verbose=True):\n", + " model.eval()\n", + " eval_loss = 0\n", + " with torch.no_grad():\n", + " for i, (data, _) in enumerate(test_loader):\n", + " # check the type of autoencoder and modify the input data accordingly\n", + " if model.type == 'mlp':\n", + " data = data.view(data.size(0), -1)\n", + " data = data.to(device)\n", + " output = model(data)\n", + " eval_loss += criterion(output, data).item()\n", + " eval_loss /= len(test_loader.dataset)\n", + " if verbose:\n", + " print('====> Test set loss: {:.4f}'.format(eval_loss))\n", + " return eval_loss\n", + "\n", + "# Define the linear classification test function\n", + "def test_linear(encoded_train, train_labels, encoded_test, test_labels):\n", + " train_features_np = encoded_train.cpu().numpy()\n", + " train_labels_np = train_labels.cpu().numpy()\n", + " test_features_np = encoded_test.cpu().numpy()\n", + " test_labels_np = test_labels.cpu().numpy()\n", + " \n", + " # Apply logistic regression on train features and labels\n", + " logistic_regression = LogisticRegression(random_state=0, max_iter=100).fit(train_features_np, train_labels_np)\n", + " print(f\"Train accuracy: {logistic_regression.score(train_features_np, train_labels_np)}\")\n", + " # Apply logistic regression on test features and labels\n", + " test_accuracy = logistic_regression.score(test_features_np, test_labels_np)\n", + " print(f\"Test accuracy: {test_accuracy}\")\n", + " return test_accuracy\n", + "\n", + "\n", + "def test_clustering(encoded_features, true_labels):\n", + " encoded_features_np = encoded_features.cpu().numpy()\n", + " true_labels_np = true_labels.cpu().numpy()\n", + " \n", + " # Apply k-means clustering\n", + " kmeans = KMeans(n_clusters=10, n_init=10, random_state=0).fit(encoded_features_np)\n", + " cluster_labels = kmeans.labels_\n", + " \n", + " # Evaluate clustering results using Adjusted Rand Index\n", + " ari_score = adjusted_rand_score(true_labels_np, cluster_labels)\n", + " print(f\"Clustering ARI score: {ari_score}\")\n", + " return ari_score\n", + "\n", + "def knn_classifier(encoded_train, train_labels, encoded_test, test_labels, k=5):\n", + " encoded_train_np = encoded_train.cpu().numpy()\n", + " encoded_test_np = encoded_test.cpu().numpy()\n", + " train_labels_np = train_labels.cpu().numpy()\n", + " test_labels_np = test_labels.cpu().numpy()\n", + " \n", + " # Apply k-nearest neighbors classification\n", + " knn = KNeighborsClassifier(n_neighbors=k).fit(encoded_train_np, train_labels_np)\n", + " accuracy_score = knn.score(encoded_test_np, test_labels_np)\n", + " print(f\"KNN accuracy: {accuracy_score}\")\n", + " return accuracy_score\n", + "\n", + "def test(model, train_loader, test_loader, criterion):\n", + " # Extract features once for all tests\n", + " encoded_train, train_labels = extract_features(train_loader, model)\n", + " encoded_test, test_labels = extract_features(test_loader, model)\n", + " print(f\"{model.type} Autoencoder\")\n", + " results = {\n", + " 'reconstruction_loss': test_loss(model, test_loader, criterion),\n", + " 'linear_classification_accuracy': test_linear(encoded_train, train_labels, encoded_test, test_labels),\n", + " 'knn_classification_accuracy': knn_classifier(encoded_train, train_labels, encoded_test, test_labels),\n", + " 'clustering_ari_score': test_clustering(encoded_test, test_labels)\n", + " }\n", + " \n", + " # Save results to a log file\n", + " with open(\"evaluation_results.log\", \"w\") as log_file:\n", + " for key, value in results.items():\n", + " log_file.write(f\"{key}: {value}\")\n", + " \n", + " return results\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "bcb22bc5af9fb014", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:41:26.789225198Z", + "start_time": "2023-10-03T12:41:26.359259721Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MLP AE parameters: 789632\n", + "CNN AE parameters: 47107\n" + ] + } + ], + "source": [ + "# Define the training parameters for the fully connected MLP Autoencoder\n", + "batch_size = 32\n", + "epochs = 5\n", + "input_size = trainset.data.shape[1] * trainset.data.shape[2] * trainset.data.shape[3]\n", + "hidden_size = 128\n", + "train_frequency = epochs\n", + "test_frequency = epochs\n", + "\n", + "# Create the fully connected MLP Autoencoder\n", + "ae = Autoencoder(input_size, hidden_size, type='mlp').to(device)\n", + "input_size = trainset.data.shape[3]\n", + "cnn_ae = Autoencoder(input_size, hidden_size, type='cnn').to(device)\n", + "# print the models' number of parameters\n", + "print(f\"MLP AE parameters: {sum(p.numel() for p in ae.parameters())}\")\n", + "print(f\"CNN AE parameters: {sum(p.numel() for p in cnn_ae.parameters())}\")\n", + "\n", + "# Define the loss function and optimizer\n", + "criterion = nn.MSELoss()\n", + "optimizer = optim.Adam(ae.parameters(), lr=1e-3)\n", + "optimizer_cnn = optim.Adam(cnn_ae.parameters(), lr=1e-3)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f1626ce45bb25883", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:41:27.399274149Z", + "start_time": "2023-10-03T12:41:27.378668195Z" + } + }, + "outputs": [], + "source": [ + "# Create the train and test dataloaders\n", + "train_loader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True)\n", + "test_loader = torch.utils.data.DataLoader(testset, batch_size=batch_size, shuffle=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "7472159fdc5f2532", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:47:02.996829577Z", + "start_time": "2023-10-03T12:41:27.969843962Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " 80%|████████ | 4/5 [01:21<00:22, 22.85s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mlp====> Epoch: 5 Average loss: 0.0174\n", + "cnn====> Epoch: 5 Average loss: 0.0045\n", + "mlp Autoencoder\n", + "====> Test set loss: 0.0172\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/fotis/PycharmProjects/representation_learning_tutorial/venv/lib/python3.10/site-packages/sklearn/linear_model/_logistic.py:460: ConvergenceWarning: lbfgs failed to converge (status=1):\n", + "STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.\n", + "\n", + "Increase the number of iterations (max_iter) or scale the data as shown in:\n", + " https://scikit-learn.org/stable/modules/preprocessing.html\n", + "Please also refer to the documentation for alternative solver options:\n", + " https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression\n", + " n_iter_i = _check_optimize_result(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Train accuracy: 0.39372\n", + "Test accuracy: 0.3933\n", + "KNN accuracy: 0.3391\n", + "Clustering ARI score: 0.03965075119494781\n", + "cnn Autoencoder\n", + "====> Test set loss: 0.0044\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/fotis/PycharmProjects/representation_learning_tutorial/venv/lib/python3.10/site-packages/sklearn/linear_model/_logistic.py:460: ConvergenceWarning: lbfgs failed to converge (status=1):\n", + "STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.\n", + "\n", + "Increase the number of iterations (max_iter) or scale the data as shown in:\n", + " https://scikit-learn.org/stable/modules/preprocessing.html\n", + "Please also refer to the documentation for alternative solver options:\n", + " https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression\n", + " n_iter_i = _check_optimize_result(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Train accuracy: 0.41372\n", + "Test accuracy: 0.3981\n", + "KNN accuracy: 0.3545\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 5/5 [05:34<00:00, 67.00s/it] " + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Clustering ARI score: 0.051552759482472406\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "test_mlp = []\n", + "test_cnn = []\n", + "# Train the model\n", + "for epoch in tqdm(range(1, epochs + 1)):\n", + " verbose = True if epoch % train_frequency == 0 else False\n", + " train(ae, train_loader, optimizer, criterion, epoch, verbose)\n", + " train(cnn_ae, train_loader, optimizer_cnn, criterion, epoch, verbose)\n", + "\n", + " # test every n epochs\n", + " if epoch % test_frequency == 0:\n", + " restults_dic = test(ae, train_loader, test_loader, criterion)\n", + " test_mlp.append([restults_dic['reconstruction_loss'], restults_dic['linear_classification_accuracy'], restults_dic['knn_classification_accuracy'], restults_dic['clustering_ari_score']])\n", + " restults_dic = test(cnn_ae, train_loader, test_loader, criterion)\n", + " test_cnn.append([restults_dic['reconstruction_loss'], restults_dic['linear_classification_accuracy'], restults_dic['knn_classification_accuracy'], restults_dic['clustering_ari_score']])" + ] + }, + { + "cell_type": "markdown", + "id": "10639256e342a159", + "metadata": { + "collapsed": false + }, + "source": [ + "Compare the evaluation results of the MLP and CNN Autoencoders" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "50bb4c3c58af09ee", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T13:06:01.418631529Z", + "start_time": "2023-10-03T13:06:01.405317147Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Model Reconstruction Loss Linear Accuracy KNN Accuracy Clustering ARI \n", + "MLP AE 0.0172 0.3933 0.3391 0.0397 \n", + "CNN AE 0.0044 0.3981 0.3545 0.0516 \n" + ] + } + ], + "source": [ + "print(f\"{'Model':<10} {'Reconstruction Loss':<20} {'Linear Accuracy':<20} {'KNN Accuracy':<20} {'Clustering ARI':<20}\")\n", + "print(f\"{'MLP AE':<10} {test_mlp[-1][0]:<20.4f} {test_mlp[-1][1]:<20.4f} {test_mlp[-1][2]:<20.4f} {test_mlp[-1][3]:<20.4f}\")\n", + "print(f\"{'CNN AE':<10} {test_cnn[-1][0]:<20.4f} {test_cnn[-1][1]:<20.4f} {test_cnn[-1][2]:<20.4f} {test_cnn[-1][3]:<20.4f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "b9201d1403781706", + "metadata": { + "collapsed": false + }, + "source": [ + "Develop a linear classifier with fully connected layers" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "1612800950703181", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:47:03.004530043Z", + "start_time": "2023-10-03T12:47:02.996038048Z" + } + }, + "outputs": [], + "source": [ + "# Define the fully connected classifier for MNIST\n", + "class DenseClassifier(nn.Module):\n", + " def __init__(self, input_size=784, hidden_size=500, num_classes=10):\n", + " super(DenseClassifier, self).__init__()\n", + " self.type = 'mlp'\n", + " self.encoder = nn.Sequential(\n", + " nn.Linear(input_size, hidden_size),\n", + " nn.ReLU(True))\n", + " self.fc1 = nn.Linear(hidden_size, num_classes)\n", + "\n", + " def forward(self, x):\n", + " x = x.view(x.size(0), -1) # Flatten the input tensor\n", + " x = self.encoder(x)\n", + " x = self.fc1(x)\n", + " return x\n" + ] + }, + { + "cell_type": "markdown", + "id": "35db4190e9c7f716", + "metadata": { + "collapsed": false + }, + "source": [ + "Develop a non-linear classifier with convolutional layers" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "cb2dfcf75113fd0b", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:47:03.041289873Z", + "start_time": "2023-10-03T12:47:02.999128924Z" + } + }, + "outputs": [], + "source": [ + "# cnn classifier\n", + "class CNNClassifier(nn.Module):\n", + " def __init__(self, input_size, hidden_size=128, num_classes=10):\n", + " super(CNNClassifier, self).__init__()\n", + " self.type = 'cnn'\n", + " # Encoder (Feature extractor)\n", + " self.encoder = nn.Sequential(\n", + " nn.Conv2d(input_size, 16, 3, stride=2, padding=1), # 16x16x16\n", + " nn.ReLU(True),\n", + " nn.Conv2d(16, 32, 3, stride=2, padding=1), # 8x8x32\n", + " nn.ReLU(True),\n", + " nn.Conv2d(32, 64, 3, stride=2, padding=1), # 4x4x64\n", + " nn.ReLU(True)\n", + " )\n", + " \n", + " # Classifier\n", + " self.classifier = nn.Sequential(\n", + " nn.Flatten(), \n", + " nn.Linear(4*4*64, hidden_size),\n", + " nn.ReLU(True),\n", + " nn.Linear(hidden_size, num_classes)\n", + " )\n", + "\n", + " def forward(self, x):\n", + " x = self.encoder(x)\n", + " x = self.classifier(x)\n", + " return x\n", + " return x" + ] + }, + { + "cell_type": "markdown", + "id": "7c3cf2371479da0c", + "metadata": { + "collapsed": false + }, + "source": [ + "Train and test functions for the non-linear classifier" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "ac980d25bd8a3dd3", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:47:03.062646362Z", + "start_time": "2023-10-03T12:47:03.020366991Z" + } + }, + "outputs": [], + "source": [ + "# Train for the classifier\n", + "def train_classifier(model, train_loader, optimizer, criterion, epoch, verbose=True):\n", + " model.train()\n", + " train_loss = 0\n", + " correct = 0 \n", + " for i, (data, target) in enumerate(train_loader):\n", + " if model.type == 'cnn':\n", + " data = data.to(device)\n", + " else:\n", + " data = data.view(data.size(0), -1)\n", + " data = data.to(device)\n", + " target = target.to(device)\n", + " optimizer.zero_grad()\n", + " output = model(data)\n", + " loss = criterion(output, target)\n", + " loss.backward()\n", + " train_loss += loss.item()\n", + " optimizer.step()\n", + " # Calculate correct predictions for training accuracy\n", + " pred = output.argmax(dim=1, keepdim=True)\n", + " correct += pred.eq(target.view_as(pred)).sum().item()\n", + "\n", + " train_loss /= len(train_loader.dataset)\n", + " train_accuracy = 100. * correct / len(train_loader.dataset)\n", + " if verbose:\n", + " print(f'{model.type}====> Epoch: {epoch} Average loss: {train_loss:.4f}')\n", + " print(f'{model.type}====> Epoch: {epoch} Training accuracy: {train_accuracy:.2f}%')\n", + " return train_loss\n", + "\n", + "\n", + "def test_classifier(model, test_loader, criterion):\n", + " model.eval()\n", + " eval_loss = 0\n", + " correct = 0\n", + " with torch.no_grad():\n", + " for i, (data, target) in enumerate(test_loader):\n", + " if model.type == 'cnn':\n", + " data = data.to(device)\n", + " else:\n", + " data = data.view(data.size(0), -1)\n", + " data = data.to(device)\n", + " target = target.to(device)\n", + " output = model(data)\n", + " eval_loss += criterion(output, target).item()\n", + " pred = output.argmax(dim=1, keepdim=True)\n", + " correct += pred.eq(target.view_as(pred)).sum().item()\n", + " eval_loss /= len(test_loader.dataset)\n", + " print('====> Test set loss: {:.4f}'.format(eval_loss))\n", + " accuracy = correct / len(test_loader.dataset)\n", + " print('====> Test set accuracy: {:.4f}'.format(accuracy))\n", + " return accuracy\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "dff05e622dcfd774", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:47:03.092600803Z", + "start_time": "2023-10-03T12:47:03.049595871Z" + } + }, + "outputs": [], + "source": [ + "# Define the training parameters for the fully connected classifier\n", + "batch_size = 32\n", + "epochs = 5\n", + "learning_rate = 1e-3\n", + "hidden_size = 128\n", + "num_classes = 10\n", + "train_frequency = epochs\n", + "test_frequency = epochs\n", + "# Create the fully connected classifier\n", + "input_size = trainset.data.shape[1] * trainset.data.shape[2] * trainset.data.shape[3]\n", + "classifier = DenseClassifier(input_size, hidden_size, num_classes).to(device)\n", + "input_size = trainset.data.shape[3]\n", + "cnn_classifier = CNNClassifier(input_size, hidden_size, num_classes).to(device)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "3104345cdee0eb00", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:47:03.095971390Z", + "start_time": "2023-10-03T12:47:03.080480468Z" + } + }, + "outputs": [], + "source": [ + "# Define the loss function and optimizer\n", + "criterion = nn.CrossEntropyLoss()\n", + "optimizer = optim.Adam(classifier.parameters(), lr=learning_rate)\n", + "optimizer_cnn = optim.Adam(cnn_classifier.parameters(), lr=learning_rate)\n" + ] + }, + { + "cell_type": "markdown", + "id": "e1fed39be2f04745", + "metadata": { + "collapsed": false + }, + "source": [ + "Train the non-linear classifiers" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "abc0c6ce338d40d9", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:49:41.536281525Z", + "start_time": "2023-10-03T12:47:03.094970008Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " 80%|████████ | 4/5 [02:03<00:30, 30.65s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mlp====> Epoch: 5 Average loss: 0.0401\n", + "mlp====> Epoch: 5 Training accuracy: 55.44%\n", + "cnn====> Epoch: 5 Average loss: 0.0265\n", + "cnn====> Epoch: 5 Training accuracy: 69.87%\n", + "====> Test set loss: 0.0446\n", + "====> Test set accuracy: 0.5129\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 5/5 [02:38<00:00, 31.69s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====> Test set loss: 0.0319\n", + "====> Test set accuracy: 0.6478\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "# Train the classifier\n", + "for epoch in tqdm(range(1, epochs + 1)):\n", + " verbose = True if epoch % train_frequency == 0 else False\n", + " train_classifier(classifier, train_loader, optimizer, criterion, epoch, verbose)\n", + " train_classifier(cnn_classifier, train_loader, optimizer_cnn, criterion, epoch, verbose)\n", + "\n", + " # test every n epochs\n", + " if epoch % test_frequency == 0:\n", + " test_acc = test_classifier(classifier, test_loader, criterion)\n", + " test_acc_cnn = test_classifier(cnn_classifier, test_loader, criterion)\n" + ] + }, + { + "cell_type": "markdown", + "id": "a06038f113d8434f", + "metadata": { + "collapsed": false + }, + "source": [ + "Load the encoder weights into the classifier" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "6a91d8894b70ef7c", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:49:41.571816419Z", + "start_time": "2023-10-03T12:49:41.535695938Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": "" + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# initialize the classifier with the encoder weights\n", + "classifier.encoder.load_state_dict(ae.encoder.state_dict())\n", + "cnn_classifier.encoder.load_state_dict(cnn_ae.encoder.state_dict())" + ] + }, + { + "cell_type": "markdown", + "id": "aafa4a9ba7208647", + "metadata": { + "collapsed": false + }, + "source": [ + "Transfer learning" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "a60dd68f988a8249", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:52:09.051066459Z", + "start_time": "2023-10-03T12:49:41.543118970Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " 80%|████████ | 4/5 [02:06<00:31, 31.63s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mlp====> Epoch: 5 Average loss: 0.0539\n", + "mlp====> Epoch: 5 Training accuracy: 39.10%\n", + "cnn====> Epoch: 5 Average loss: 0.0333\n", + "cnn====> Epoch: 5 Training accuracy: 61.80%\n", + "====> Test set loss: 0.0536\n", + "====> Test set accuracy: 0.3961\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 5/5 [02:27<00:00, 29.50s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====> Test set loss: 0.0341\n", + "====> Test set accuracy: 0.6133\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "# fine-tune the classifier\n", + "learning_rate = 1e-5\n", + "epoch = 20\n", + "optimizer_pretrained = optim.Adam(classifier.parameters(), lr=learning_rate)\n", + "optimizer_pretrained_cnn = optim.Adam(cnn_classifier.parameters(), lr=learning_rate)\n", + "for epoch in tqdm(range(1, epochs + 1)):\n", + " verbose = True if epoch % train_frequency == 0 else False\n", + " train_classifier(classifier, train_loader, optimizer_pretrained, criterion, epoch, verbose)\n", + " train_classifier(cnn_classifier, train_loader, optimizer_cnn, criterion, epoch, verbose)\n", + "\n", + " # test every n epochs\n", + " if epoch % test_frequency == 0:\n", + " test_acc_pretrained = test_classifier(classifier, test_loader, criterion)\n", + " test_acc_pretrained_cnn = test_classifier(cnn_classifier, test_loader, criterion)\n" + ] + }, + { + "cell_type": "markdown", + "id": "31577275b833707a", + "metadata": { + "collapsed": false + }, + "source": [ + "Compare the results of the linear probing with the results of the linear classifier" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "40d0e7f3f13404c9", + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:52:09.054596162Z", + "start_time": "2023-10-03T12:52:09.050719737Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Model Linear Accuracy Non-linear accuracy Pretrained accuracy \n", + "MLP AE 0.3933 0.5129 0.3961 \n", + "CNN AE 0.3981 0.6478 0.6133 \n" + ] + } + ], + "source": [ + "# print a table of the accuracies. compare the results with the results of the linear probing\n", + "print(f\"{'Model':<10} {'Linear Accuracy':<20} {'Non-linear accuracy':<20} {'Pretrained accuracy':<20}\")\n", + "print(f\"{'MLP AE':<10} {test_mlp[-1][1]:<20.4f} {test_acc:<20.4f} {test_acc_pretrained:<20.4f}\")\n", + "print(f\"{'CNN AE':<10} {test_cnn[-1][1]:<20.4f} {test_acc_cnn:<20.4f} {test_acc_pretrained_cnn:<20.4f}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "outputs": [], + "source": [ + "import torchvision.models as models\n", + "\n", + "class ResNet18Autoencoder(nn.Module):\n", + " def __init__(self):\n", + " super(ResNet18Autoencoder, self).__init__()\n", + " self.type = 'cnn'\n", + " # Encoder: Use pre-trained ResNet18 (without its final fc layer)\n", + " self.resnet18 = models.resnet18(pretrained=False)\n", + " self.encoder = nn.Sequential(*list(self.resnet18.children())[:-1], nn.Flatten())\n", + " \n", + " # Decoder: Create an up-sampling network\n", + " self.decoder = nn.Sequential(\n", + " nn.ConvTranspose2d(512, 256, kernel_size=4, stride=2, padding=1), # 8x8\n", + " nn.ReLU(),\n", + " nn.ConvTranspose2d(256, 128, kernel_size=4, stride=2, padding=1), # 16x16\n", + " nn.ReLU(),\n", + " nn.ConvTranspose2d(128, 64, kernel_size=4, stride=2, padding=1), # 32x32\n", + " nn.ReLU(),\n", + " nn.ConvTranspose2d(64, 32, kernel_size=4, stride=2, padding=1), # 64x64\n", + " nn.ReLU(),\n", + " nn.ConvTranspose2d(32, 3, kernel_size=4, stride=2, padding=1), # 128x128\n", + " nn.Sigmoid() # to ensure pixel values are between 0 and 1\n", + " )\n", + " \n", + " def forward(self, x):\n", + " x = self.encoder(x)\n", + " # unflatten the output of the encoder to be fed into the decoder\n", + " x = x.view(x.size(0), 512, 1, 1)\n", + " x = self.decoder(x)\n", + " return x\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:52:09.068053637Z", + "start_time": "2023-10-03T12:52:09.054382586Z" + } + }, + "id": "e4037285d9d70694" + }, + { + "cell_type": "code", + "execution_count": 23, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ResNet18 AE parameters: 14476811\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/fotis/PycharmProjects/representation_learning_tutorial/venv/lib/python3.10/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.\n", + " warnings.warn(\n", + "/home/fotis/PycharmProjects/representation_learning_tutorial/venv/lib/python3.10/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=None`.\n", + " warnings.warn(msg)\n" + ] + } + ], + "source": [ + "# Define the training parameters for the ResNet18 Autoencoder\n", + "batch_size = 128\n", + "epochs = 10\n", + "learning_rate = 1e-3\n", + "train_frequency = epochs\n", + "test_frequency = epochs\n", + "\n", + "# Create the ResNet18 Autoencoder\n", + "resnet18_ae = ResNet18Autoencoder().to(device)\n", + "# print the model's number of parameters\n", + "print(f\"ResNet18 AE parameters: {sum(p.numel() for p in resnet18_ae.parameters())}\")\n", + "\n", + "# Define the loss function and optimizer\n", + "criterion = nn.MSELoss()\n", + "optimizer = optim.Adam(resnet18_ae.parameters(), lr=learning_rate)\n", + "scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=2, verbose=True)\n", + "# Create the train and test dataloaders\n", + "train_loader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True)\n", + "test_loader = torch.utils.data.DataLoader(testset, batch_size=batch_size, shuffle=True)\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:52:09.214178804Z", + "start_time": "2023-10-03T12:52:09.061558426Z" + } + }, + "id": "8963904ee52b9818" + }, + { + "cell_type": "code", + "execution_count": 24, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 10 - Train Loss: 0.0013 - Test Loss: 0.0013: 90%|█████████ | 9/10 [01:49<00:11, 11.05s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "cnn====> Epoch: 10 Average loss: 0.0013\n", + "cnn Autoencoder\n", + "====> Test set loss: 0.0013\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/fotis/PycharmProjects/representation_learning_tutorial/venv/lib/python3.10/site-packages/sklearn/linear_model/_logistic.py:460: ConvergenceWarning: lbfgs failed to converge (status=1):\n", + "STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.\n", + "\n", + "Increase the number of iterations (max_iter) or scale the data as shown in:\n", + " https://scikit-learn.org/stable/modules/preprocessing.html\n", + "Please also refer to the documentation for alternative solver options:\n", + " https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression\n", + " n_iter_i = _check_optimize_result(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Train accuracy: 0.36802\n", + "Test accuracy: 0.3665\n", + "KNN accuracy: 0.3889\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 10 - Train Loss: 0.0013 - Test Loss: 0.0013: 100%|██████████| 10/10 [02:15<00:00, 13.52s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Clustering ARI score: 0.02700985553219709\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "# Train the model\n", + "test_resnet18 = []\n", + "loss = 0\n", + "pbar = tqdm(range(1, epochs + 1))\n", + "for epoch in pbar:\n", + " verbose = True if epoch % train_frequency == 0 else False\n", + " train_loss = train(resnet18_ae, train_loader, optimizer, criterion, epoch, verbose)\n", + " \n", + " # Update tqdm description with the training loss\n", + " pbar.set_description(f\"Epoch {epoch} - Train Loss: {train_loss:.4f} - Test Loss: {loss:.4f}\")\n", + " \n", + " if epoch % 2 == 0:\n", + " loss = test_loss(resnet18_ae, test_loader, criterion, verbose=False)\n", + " scheduler.step(loss)\n", + "\n", + " # test every n epochs\n", + " if epoch % test_frequency == 0:\n", + " results_resnet_dic = test(resnet18_ae, train_loader, test_loader, criterion)\n", + " test_resnet18.append([results_resnet_dic['reconstruction_loss'], results_resnet_dic['linear_classification_accuracy'], results_resnet_dic['knn_classification_accuracy'], results_resnet_dic['clustering_ari_score']])\n", + " \n", + "\n", + " " + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:54:24.395386156Z", + "start_time": "2023-10-03T12:52:09.200297772Z" + } + }, + "id": "7af2592ad350ea8c" + }, + { + "cell_type": "markdown", + "source": [ + "Compare the evaluation results of the ResNet18 Autoencoder with the MLP and CNN Autoencoders " + ], + "metadata": { + "collapsed": false + }, + "id": "18cc2efab5a2e4b0" + }, + { + "cell_type": "code", + "execution_count": 25, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Model Reconstruction Loss Linear Accuracy KNN Accuracy Clustering ARI \n", + "MLP AE 0.0172 0.3933 0.3391 0.0397 \n", + "CNN AE 0.0044 0.3981 0.3545 0.0516 \n", + "ResNet18 AE 0.0013 0.3665 0.3889 0.0270 \n" + ] + } + ], + "source": [ + "print(f\"{'Model':<15} {'Reconstruction Loss':<20} {'Linear Accuracy':<20} {'KNN Accuracy':<20} {'Clustering ARI':<20}\")\n", + "print(f\"{'MLP AE':<15} {test_mlp[-1][0]:<20.4f} {test_mlp[-1][1]:<20.4f} {test_mlp[-1][2]:<20.4f} {test_mlp[-1][3]:<20.4f}\")\n", + "print(f\"{'CNN AE':<15} {test_cnn[-1][0]:<20.4f} {test_cnn[-1][1]:<20.4f} {test_cnn[-1][2]:<20.4f} {test_cnn[-1][3]:<20.4f}\")\n", + "print(f\"{'ResNet18 AE':<15} {test_resnet18[-1][0]:<20.4f} {test_resnet18[-1][1]:<20.4f} {test_resnet18[-1][2]:<20.4f} {test_resnet18[-1][3]:<20.4f}\")" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:54:24.401416457Z", + "start_time": "2023-10-03T12:54:24.394398143Z" + } + }, + "id": "a5dcaa09ceaf1f3f" + }, + { + "cell_type": "code", + "execution_count": 26, + "outputs": [], + "source": [ + "class ResNet18Classifier(nn.Module):\n", + " def __init__(self, num_classes=10, pretrained=False):\n", + " super(ResNet18Classifier, self).__init__()\n", + " self.type = 'cnn'\n", + " # Load the ResNet18 model\n", + " self.resnet18 = models.resnet18(pretrained=pretrained)\n", + " \n", + " # Adjust the first convolutional layer for CIFAR-10 image size\n", + " self.resnet18.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False)\n", + " \n", + " # Adjust the final fully connected layer for CIFAR-10 number of classes\n", + " num_ftrs = self.resnet18.fc.in_features\n", + " self.resnet18.fc = nn.Linear(num_ftrs, num_classes)\n", + " \n", + " # Freeze the encoder weights except the final fc layer\n", + " for param in self.resnet18.parameters():\n", + " param.requires_grad = False\n", + " for param in self.resnet18.fc.parameters():\n", + " param.requires_grad = True\n", + " \n", + " def forward(self, x):\n", + " return self.resnet18(x)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:54:24.409917792Z", + "start_time": "2023-10-03T12:54:24.398893967Z" + } + }, + "id": "f4e2e62d832761c0" + }, + { + "cell_type": "code", + "execution_count": 27, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/fotis/PycharmProjects/representation_learning_tutorial/venv/lib/python3.10/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.\n", + " warnings.warn(\n", + "/home/fotis/PycharmProjects/representation_learning_tutorial/venv/lib/python3.10/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=None`.\n", + " warnings.warn(msg)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ResNet18 classifier parameters: 11173962\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/fotis/PycharmProjects/representation_learning_tutorial/venv/lib/python3.10/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=ResNet18_Weights.IMAGENET1K_V1`. You can also use `weights=ResNet18_Weights.DEFAULT` to get the most up-to-date weights.\n", + " warnings.warn(msg)\n" + ] + } + ], + "source": [ + "# Define the training parameters for the ResNet18 classifier\n", + "batch_size = 128\n", + "epochs = 50\n", + "learning_rate = 1e-4\n", + "train_frequency = epochs\n", + "test_frequency = epochs\n", + "\n", + "# Create the ResNet18 classifier\n", + "resnet18_classifier = ResNet18Classifier(num_classes=10, pretrained=False).to(device)\n", + "resnet18_classifier_pretrained = ResNet18Classifier(num_classes=10, pretrained=True).to(device)\n", + "# print the model's number of parameters\n", + "print(f\"ResNet18 classifier parameters: {sum(p.numel() for p in resnet18_classifier.parameters())}\")\n", + "\n", + "# Define the loss function and optimizer\n", + "criterion = nn.CrossEntropyLoss()\n", + "optimizer = optim.Adam(resnet18_classifier.parameters(), lr=learning_rate)\n", + "optimizer_pretrained = optim.Adam(resnet18_classifier_pretrained.parameters(), lr=learning_rate)\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:54:24.817741667Z", + "start_time": "2023-10-03T12:54:24.402570604Z" + } + }, + "id": "1f4d0fbf693153da" + }, + { + "cell_type": "code", + "execution_count": 28, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 49 - Train Loss: 0.0138 - Train Loss Pretrained: 0.0131: 98%|█████████▊| 49/50 [11:20<00:13, 13.33s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "cnn====> Epoch: 50 Average loss: 0.0138\n", + "cnn====> Epoch: 50 Training accuracy: 37.49%\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 50 - Train Loss: 0.0138 - Train Loss Pretrained: 0.0131: 98%|█████████▊| 49/50 [11:33<00:13, 13.33s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "cnn====> Epoch: 50 Average loss: 0.0131\n", + "cnn====> Epoch: 50 Training accuracy: 41.32%\n", + "====> Test set loss: 0.0142\n", + "====> Test set accuracy: 0.3560\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Epoch 50 - Train Loss: 0.0138 - Train Loss Pretrained: 0.0131: 100%|██████████| 50/50 [11:36<00:00, 13.93s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====> Test set loss: 0.0136\n", + "====> Test set accuracy: 0.3913\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "# Train the model\n", + "test_resnet18_classifier = []\n", + "test_resnet18_classifier_pretrained = []\n", + "pbar = tqdm(range(1, epochs + 1))\n", + "\n", + "for epoch in pbar:\n", + " verbose = True if epoch % train_frequency == 0 else False\n", + " train_loss = train_classifier(resnet18_classifier, train_loader, optimizer, criterion, epoch, verbose)\n", + " train_loss_pretrained = train_classifier(resnet18_classifier_pretrained, train_loader, optimizer_pretrained, criterion, epoch, verbose)\n", + " \n", + " # Update tqdm description with the training loss\n", + " pbar.set_description(f\"Epoch {epoch} - Train Loss: {train_loss:.4f} - Train Loss Pretrained: {train_loss_pretrained:.4f}\")\n", + " \n", + " # test every n epochs\n", + " if epoch % test_frequency == 0:\n", + " results_resnet_acc = test_classifier(resnet18_classifier, test_loader, criterion)\n", + " test_resnet18_classifier.append(results_resnet_acc)\n", + " results_resnet_acc_pretrained = test_classifier(resnet18_classifier_pretrained, test_loader, criterion)\n", + " test_resnet18_classifier_pretrained.append(results_resnet_acc_pretrained)\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T13:06:01.403787907Z", + "start_time": "2023-10-03T12:54:24.817533512Z" + } + }, + "id": "8452e383237a283" + }, + { + "cell_type": "markdown", + "source": [ + "Compare the accuracy results of the ResNet18 classifier with the MLP and CNN classifiers" + ], + "metadata": { + "collapsed": false + }, + "id": "9b3b561b83de5ccc" + }, + { + "cell_type": "code", + "execution_count": 29, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Model Accuracy \n", + "MLP AE 0.3933 \n", + "CNN AE 0.3981 \n", + "ResNet18 AE 0.3665 \n", + "ResNet18 Classifier 0.3560 \n", + "ResNet18 Classifier Pretrained 0.3913 \n" + ] + } + ], + "source": [ + "import numpy as np\n", + "\n", + "print(f\"{'Model':<15} {'Accuracy':<20}\")\n", + "print(f\"{'MLP AE':<15} {test_mlp[-1][1]:<20.4f}\")\n", + "print(f\"{'CNN AE':<15} {test_cnn[-1][1]:<20.4f}\")\n", + "print(f\"{'ResNet18 AE':<15} {test_resnet18[-1][1]:<20.4f}\")\n", + "# take the average of the test accuracies\n", + "print(f\"{'ResNet18 Classifier':<15} {np.mean(test_resnet18_classifier):<20.4f}\")\n", + "print(f\"{'ResNet18 Classifier Pretrained':<15} {np.mean(test_resnet18_classifier_pretrained):<20.4f}\")\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T13:06:01.408109408Z", + "start_time": "2023-10-03T13:06:01.403113475Z" + } + }, + "id": "8d1f038afa9bcff2" + }, + { + "cell_type": "markdown", + "source": [], + "metadata": { + "collapsed": false + }, + "id": "9ff6e7674c4a3e71" + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [], + "metadata": { + "collapsed": false + }, + "id": "36fdbf815e16107b" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/autoencoder_mnist.ipynb b/autoencoder_mnist.ipynb new file mode 100644 index 0000000..ed96f88 --- /dev/null +++ b/autoencoder_mnist.ipynb @@ -0,0 +1,1135 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + "| Credentials | |\n", + "|----|----------------------------------|\n", + "|Host | Montanuniversitaet Leoben |\n", + "|Web | https://cps.unileoben.ac.at |\n", + "|Mail | cps@unileoben.ac.at |\n", + "|Author | Fotios Lygerakis |\n", + "|Corresponding Authors | fotios.lygerakis@unileoben.ac.at |\n", + "|Last edited | 28.09.2023 |" + ], + "metadata": { + "collapsed": false + }, + "id": "ae041e151c5c2222" + }, + { + "cell_type": "markdown", + "source": [ + "In the first part of this tutorial we will build a fully connected MLP Autoencoder on the MNIST dataset. Then we will perform linear probing on the encoder features to see how well they perform on a linear classification task." + ], + "metadata": { + "collapsed": false + }, + "id": "1490260facaff836" + }, + { + "cell_type": "code", + "execution_count": 1, + "outputs": [], + "source": [ + "import torch\n", + "import torch.nn as nn\n", + "import torch.optim as optim\n", + "import matplotlib.pyplot as plt\n", + "from torchvision import datasets, transforms\n", + "from sklearn.neighbors import KNeighborsClassifier\n", + "from sklearn.metrics import adjusted_rand_score\n", + "from sklearn.linear_model import LogisticRegression\n", + "from sklearn.cluster import KMeans\n", + "from tqdm import tqdm" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:40:27.590742473Z", + "start_time": "2023-10-03T12:40:25.356175335Z" + } + }, + "id": "fc18830bb6f8d534" + }, + { + "cell_type": "markdown", + "source": [ + "Set random seed" + ], + "metadata": { + "collapsed": false + }, + "id": "1bad4bd03deb5b7e" + }, + { + "cell_type": "code", + "execution_count": 2, + "outputs": [ + { + "data": { + "text/plain": "" + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Set random seed\n", + "torch.manual_seed(0)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:40:27.592356147Z", + "start_time": "2023-10-03T12:40:27.568457489Z" + } + }, + "id": "27dd48e60ae7dd9e" + }, + { + "cell_type": "markdown", + "source": [ + "Load the MNIST dataset" + ], + "metadata": { + "collapsed": false + }, + "id": "cc7f167a33227e94" + }, + { + "cell_type": "code", + "execution_count": 3, + "outputs": [], + "source": [ + "# Define the transformations\n", + "transform = transforms.Compose([transforms.ToTensor(),\n", + " transforms.Normalize((0.5,), (0.5,))])\n", + "# Download and load the training data\n", + "trainset = datasets.MNIST('data', download=True, train=True, transform=transform)\n", + "# Download and load the test data\n", + "testset = datasets.MNIST('data', download=True, train=False, transform=transform)\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:40:27.639417871Z", + "start_time": "2023-10-03T12:40:27.577605311Z" + } + }, + "id": "34248e8bc2678fd3" + }, + { + "cell_type": "markdown", + "source": [ + "Print some examples from the dataset" + ], + "metadata": { + "collapsed": false + }, + "id": "928dfac955d0d778" + }, + { + "cell_type": "code", + "execution_count": 4, + "outputs": [ + { + "data": { + "text/plain": "
", + "image/png": "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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Get the first 10 samples\n", + "dataiter = iter(trainset)\n", + "images, labels = [], []\n", + "\n", + "for i in range(10):\n", + " image, label = next(dataiter)\n", + " images.append(image)\n", + " labels.append(label)\n", + "\n", + "# Plot the samples\n", + "fig, axes = plt.subplots(2, 5)\n", + "\n", + "for ax, img, lbl in zip(axes.ravel(), images, labels):\n", + " ax.imshow(img.squeeze().numpy(), cmap='gray')\n", + " ax.set_title(f'Label: {lbl}')\n", + " ax.axis('off')\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:40:28.606350630Z", + "start_time": "2023-10-03T12:40:28.277928820Z" + } + }, + "id": "87c6eae807f51118" + }, + { + "cell_type": "markdown", + "source": [ + "Define the MLP and Convolutional Autoencoder" + ], + "metadata": { + "collapsed": false + }, + "id": "e4e25962ef8e5b0d" + }, + { + "cell_type": "code", + "execution_count": 5, + "outputs": [], + "source": [ + "class Autoencoder(nn.Module):\n", + " def __init__(self, input_size, hidden_size, type='mlp'):\n", + " super(Autoencoder, self).__init__()\n", + " # type of autoencoder\n", + " self.type = type\n", + " if self.type == 'mlp':\n", + " self.encoder = nn.Sequential(\n", + " nn.Linear(input_size, hidden_size),\n", + " nn.ReLU(True))\n", + " self.decoder = nn.Sequential(\n", + " nn.Linear(hidden_size, input_size),\n", + " nn.ReLU(True),\n", + " nn.Sigmoid()\n", + " )\n", + " elif self.type == 'cnn':\n", + " # Encoder module\n", + " self.encoder = nn.Sequential(\n", + " nn.Conv2d(in_channels=input_size, out_channels=hidden_size//2, kernel_size=3, stride=2, padding=1),\n", + " nn.ReLU(),\n", + " nn.Conv2d(in_channels=hidden_size//2, out_channels=hidden_size, kernel_size=3, stride=2, padding=1),\n", + " nn.ReLU()\n", + " )\n", + " # Decoder module\n", + " self.decoder = nn.Sequential(\n", + " nn.ConvTranspose2d(in_channels=hidden_size, out_channels=hidden_size//2, kernel_size=3, stride=2, padding=1, output_padding=1),\n", + " nn.ReLU(),\n", + " nn.ConvTranspose2d(in_channels=hidden_size//2, out_channels=1, kernel_size=3, stride=2, padding=1, output_padding=1),\n", + " nn.Sigmoid() # Sigmoid to ensure the output is between 0 and 1\n", + " )\n", + "\n", + " def forward(self, x):\n", + " x = self.encoder(x)\n", + " x = self.decoder(x)\n", + " return x" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:40:29.561602021Z", + "start_time": "2023-10-03T12:40:29.559204154Z" + } + }, + "id": "26f2513d92b78e1e" + }, + { + "cell_type": "markdown", + "source": [ + "Check if GPU support is available" + ], + "metadata": { + "collapsed": false + }, + "id": "91a01313b4d95274" + }, + { + "cell_type": "code", + "execution_count": 6, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "cuda\n" + ] + } + ], + "source": [ + "# device\n", + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "print(device)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:40:30.834696308Z", + "start_time": "2023-10-03T12:40:30.827970016Z" + } + }, + "id": "67006b35b75d8dff" + }, + { + "cell_type": "markdown", + "source": [ + "Define the training function" + ], + "metadata": { + "collapsed": false + }, + "id": "8eebf70cb27640d5" + }, + { + "cell_type": "code", + "execution_count": 7, + "outputs": [], + "source": [ + "# Define the training function\n", + "def train(model, train_loader, optimizer, criterion, epoch, verbose=True):\n", + " model.train()\n", + " train_loss = 0\n", + " for i, (data, _) in enumerate(train_loader):\n", + " # check the type of autoencoder and modify the input data accordingly\n", + " if model.type == 'mlp':\n", + " data = data.view(data.size(0), -1)\n", + " data = data.to(device)\n", + " optimizer.zero_grad()\n", + " output = model(data)\n", + " loss = criterion(output, data)\n", + " loss.backward()\n", + " train_loss += loss.item()\n", + " optimizer.step()\n", + " train_loss /= len(train_loader.dataset)\n", + " if verbose:\n", + " print(f'{model.type}====> Epoch: {epoch} Average loss: {train_loss:.4f}') \n", + " return train_loss" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:40:31.675127463Z", + "start_time": "2023-10-03T12:40:31.655370005Z" + } + }, + "id": "5f96f7be13984747" + }, + { + "cell_type": "markdown", + "source": [ + "The evaluation functions for the linear classification and clustering tasks" + ], + "metadata": { + "collapsed": false + }, + "id": "5f6386edcab6b1e4" + }, + { + "cell_type": "code", + "execution_count": 8, + "outputs": [], + "source": [ + "# Extract encoded representations for a given loader\n", + "def extract_features(loader, model):\n", + " features = []\n", + " labels = []\n", + " model.eval()\n", + " with torch.no_grad():\n", + " for data in loader:\n", + " img, label = data\n", + " if model.type == 'mlp':\n", + " img = img.view(img.size(0), -1)\n", + " img = img.to(device)\n", + " feature = model.encoder(img)\n", + " if model.type == 'cnn':\n", + " feature = feature.view(feature.size(0), -1) # Flatten the CNN encoded features\n", + " features.append(feature)\n", + " labels.append(label)\n", + " return torch.cat(features), torch.cat(labels)\n", + "\n", + "# Define the loss test function\n", + "def test_loss(model, test_loader, criterion):\n", + " model.eval()\n", + " eval_loss = 0\n", + " with torch.no_grad():\n", + " for i, (data, _) in enumerate(test_loader):\n", + " # check the type of autoencoder and modify the input data accordingly\n", + " if model.type == 'mlp':\n", + " data = data.view(data.size(0), -1)\n", + " data = data.to(device)\n", + " output = model(data)\n", + " eval_loss += criterion(output, data).item()\n", + " eval_loss /= len(test_loader.dataset)\n", + " print('====> Test set loss: {:.4f}'.format(eval_loss))\n", + " return eval_loss\n", + "\n", + "# Define the linear classification test function\n", + "def test_linear(encoded_train, train_labels, encoded_test, test_labels):\n", + " train_features_np = encoded_train.cpu().numpy()\n", + " train_labels_np = train_labels.cpu().numpy()\n", + " test_features_np = encoded_test.cpu().numpy()\n", + " test_labels_np = test_labels.cpu().numpy()\n", + " \n", + " # Apply logistic regression on train features and labels\n", + " logistic_regression = LogisticRegression(random_state=0, max_iter=100).fit(train_features_np, train_labels_np)\n", + " print(f\"Train accuracy: {logistic_regression.score(train_features_np, train_labels_np)}\")\n", + " # Apply logistic regression on test features and labels\n", + " test_accuracy = logistic_regression.score(test_features_np, test_labels_np)\n", + " print(f\"Test accuracy: {test_accuracy}\")\n", + " return test_accuracy\n", + "\n", + "\n", + "def test_clustering(encoded_features, true_labels):\n", + " encoded_features_np = encoded_features.cpu().numpy()\n", + " true_labels_np = true_labels.cpu().numpy()\n", + " \n", + " # Apply k-means clustering\n", + " kmeans = KMeans(n_clusters=10, n_init=10, random_state=0).fit(encoded_features_np)\n", + " cluster_labels = kmeans.labels_\n", + " \n", + " # Evaluate clustering results using Adjusted Rand Index\n", + " ari_score = adjusted_rand_score(true_labels_np, cluster_labels)\n", + " print(f\"Clustering ARI score: {ari_score}\")\n", + " return ari_score\n", + "\n", + "def knn_classifier(encoded_train, train_labels, encoded_test, test_labels, k=5):\n", + " encoded_train_np = encoded_train.cpu().numpy()\n", + " encoded_test_np = encoded_test.cpu().numpy()\n", + " train_labels_np = train_labels.cpu().numpy()\n", + " test_labels_np = test_labels.cpu().numpy()\n", + " \n", + " # Apply k-nearest neighbors classification\n", + " knn = KNeighborsClassifier(n_neighbors=k).fit(encoded_train_np, train_labels_np)\n", + " accuracy_score = knn.score(encoded_test_np, test_labels_np)\n", + " print(f\"KNN accuracy: {accuracy_score}\")\n", + " return accuracy_score\n", + "\n", + "def test(model, train_loader, test_loader, criterion):\n", + " # Extract features once for all tests\n", + " encoded_train, train_labels = extract_features(train_loader, model)\n", + " encoded_test, test_labels = extract_features(test_loader, model)\n", + " print(f\"{model.type} Autoencoder\")\n", + " results = {\n", + " 'reconstruction_loss': test_loss(model, test_loader, criterion),\n", + " 'linear_classification_accuracy': test_linear(encoded_train, train_labels, encoded_test, test_labels),\n", + " 'knn_classification_accuracy': knn_classifier(encoded_train, train_labels, encoded_test, test_labels),\n", + " 'clustering_ari_score': test_clustering(encoded_test, test_labels)\n", + " }\n", + " \n", + " # Save results to a log file\n", + " with open(\"evaluation_results.log\", \"w\") as log_file:\n", + " for key, value in results.items():\n", + " log_file.write(f\"{key}: {value}\")\n", + " \n", + " return results\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:40:32.662180572Z", + "start_time": "2023-10-03T12:40:32.657785583Z" + } + }, + "id": "b2c4483492fdd427" + }, + { + "cell_type": "code", + "execution_count": 9, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MLP AE parameters: 201616\n", + "CNN AE parameters: 148865\n" + ] + } + ], + "source": [ + "# Define the training parameters for the fully connected MLP Autoencoder\n", + "batch_size = 32\n", + "epochs = 5\n", + "hidden_size = 128\n", + "train_frequency = epochs\n", + "test_frequency = epochs\n", + "\n", + "# Create the fully connected MLP Autoencoder\n", + "input_size = trainset.data.shape[1] * trainset.data.shape[2]\n", + "ae = Autoencoder(input_size, hidden_size, type='mlp').to(device)\n", + "input_size=1\n", + "cnn_ae = Autoencoder(input_size, hidden_size, type='cnn').to(device)\n", + "# print the models' number of parameters\n", + "print(f\"MLP AE parameters: {sum(p.numel() for p in ae.parameters())}\")\n", + "print(f\"CNN AE parameters: {sum(p.numel() for p in cnn_ae.parameters())}\")\n", + "\n", + "# Define the loss function and optimizer\n", + "criterion = nn.MSELoss()\n", + "optimizer = optim.Adam(ae.parameters(), lr=1e-3)\n", + "optimizer_cnn = optim.Adam(cnn_ae.parameters(), lr=1e-3)\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:40:33.593858793Z", + "start_time": "2023-10-03T12:40:33.153759806Z" + } + }, + "id": "bcb22bc5af9fb014" + }, + { + "cell_type": "code", + "execution_count": 10, + "outputs": [], + "source": [ + "# Create the train and test dataloaders\n", + "train_loader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True)\n", + "test_loader = torch.utils.data.DataLoader(testset, batch_size=batch_size, shuffle=True)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:40:34.157176359Z", + "start_time": "2023-10-03T12:40:34.153720678Z" + } + }, + "id": "f1626ce45bb25883" + }, + { + "cell_type": "code", + "execution_count": 11, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " 80%|████████ | 4/5 [01:02<00:15, 15.79s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mlp====> Epoch: 5 Average loss: 0.0598\n", + "cnn====> Epoch: 5 Average loss: 0.0260\n", + "mlp Autoencoder\n", + "====> Test set loss: 0.0598\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/fotis/PycharmProjects/representation_learning_tutorial/venv/lib/python3.10/site-packages/sklearn/linear_model/_logistic.py:460: ConvergenceWarning: lbfgs failed to converge (status=1):\n", + "STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.\n", + "\n", + "Increase the number of iterations (max_iter) or scale the data as shown in:\n", + " https://scikit-learn.org/stable/modules/preprocessing.html\n", + "Please also refer to the documentation for alternative solver options:\n", + " https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression\n", + " n_iter_i = _check_optimize_result(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Train accuracy: 0.25626666666666664\n", + "Test accuracy: 0.2649\n", + "KNN accuracy: 0.2295\n", + "Clustering ARI score: 0.0614873771495409\n", + "cnn Autoencoder\n", + "====> Test set loss: 0.0260\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/fotis/PycharmProjects/representation_learning_tutorial/venv/lib/python3.10/site-packages/sklearn/linear_model/_logistic.py:460: ConvergenceWarning: lbfgs failed to converge (status=1):\n", + "STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.\n", + "\n", + "Increase the number of iterations (max_iter) or scale the data as shown in:\n", + " https://scikit-learn.org/stable/modules/preprocessing.html\n", + "Please also refer to the documentation for alternative solver options:\n", + " https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression\n", + " n_iter_i = _check_optimize_result(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Train accuracy: 0.9316166666666666\n", + "Test accuracy: 0.9278\n", + "KNN accuracy: 0.9639\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 5/5 [06:56<00:00, 83.22s/it] " + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Clustering ARI score: 0.3909294873624941\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "test_mlp = []\n", + "test_cnn = []\n", + "# Train the model\n", + "for epoch in tqdm(range(1, epochs + 1)):\n", + " verbose = True if epoch % train_frequency == 0 else False\n", + " train(ae, train_loader, optimizer, criterion, epoch, verbose)\n", + " train(cnn_ae, train_loader, optimizer_cnn, criterion, epoch, verbose)\n", + "\n", + " # test every n epochs\n", + " if epoch % test_frequency == 0:\n", + " restults_dic = test(ae, train_loader, test_loader, criterion)\n", + " test_mlp.append([restults_dic['reconstruction_loss'], restults_dic['linear_classification_accuracy'], restults_dic['knn_classification_accuracy'], restults_dic['clustering_ari_score']])\n", + " restults_dic = test(cnn_ae, train_loader, test_loader, criterion)\n", + " test_cnn.append([restults_dic['reconstruction_loss'], restults_dic['linear_classification_accuracy'], restults_dic['knn_classification_accuracy'], restults_dic['clustering_ari_score']])" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:47:30.814501313Z", + "start_time": "2023-10-03T12:40:34.720164326Z" + } + }, + "id": "7472159fdc5f2532" + }, + { + "cell_type": "markdown", + "source": [ + "Compare the evaluation results of the MLP and CNN Autoencoders" + ], + "metadata": { + "collapsed": false + }, + "id": "10639256e342a159" + }, + { + "cell_type": "code", + "execution_count": 12, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Model Reconstruction Loss Linear Accuracy KNN Accuracy Clustering ARI \n", + "MLP AE 0.0598 0.2649 0.2295 0.0615 \n", + "CNN AE 0.0260 0.9278 0.9639 0.3909 \n" + ] + } + ], + "source": [ + "print(f\"{'Model':<10} {'Reconstruction Loss':<20} {'Linear Accuracy':<20} {'KNN Accuracy':<20} {'Clustering ARI':<20}\")\n", + "print(f\"{'MLP AE':<10} {test_mlp[-1][0]:<20.4f} {test_mlp[-1][1]:<20.4f} {test_mlp[-1][2]:<20.4f} {test_mlp[-1][3]:<20.4f}\")\n", + "print(f\"{'CNN AE':<10} {test_cnn[-1][0]:<20.4f} {test_cnn[-1][1]:<20.4f} {test_cnn[-1][2]:<20.4f} {test_cnn[-1][3]:<20.4f}\")" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:47:30.828062767Z", + "start_time": "2023-10-03T12:47:30.812448850Z" + } + }, + "id": "50bb4c3c58af09ee" + }, + { + "cell_type": "markdown", + "source": [ + "Develop a linear classifier with fully connected layers" + ], + "metadata": { + "collapsed": false + }, + "id": "b9201d1403781706" + }, + { + "cell_type": "code", + "execution_count": 13, + "outputs": [], + "source": [ + "# Define the fully connected classifier for MNIST\n", + "class DenseClassifier(nn.Module):\n", + " def __init__(self, input_size=784, hidden_size=500, num_classes=10):\n", + " super(DenseClassifier, self).__init__()\n", + " self.type = 'mlp'\n", + " self.encoder = nn.Sequential(\n", + " nn.Linear(input_size, hidden_size),\n", + " nn.ReLU(True))\n", + " self.fc1 = nn.Linear(hidden_size, num_classes)\n", + "\n", + " def forward(self, x):\n", + " x = x.view(x.size(0), -1) # Flatten the input tensor\n", + " x = self.encoder(x)\n", + " x = self.fc1(x)\n", + " return x\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:47:30.833296890Z", + "start_time": "2023-10-03T12:47:30.819270525Z" + } + }, + "id": "1612800950703181" + }, + { + "cell_type": "markdown", + "source": [ + "Develop a non-linear classifier with convolutional layers" + ], + "metadata": { + "collapsed": false + }, + "id": "35db4190e9c7f716" + }, + { + "cell_type": "code", + "execution_count": 14, + "outputs": [], + "source": [ + "# cnn classifier\n", + "class CNNClassifier(nn.Module):\n", + " def __init__(self, input_size=3, hidden_size=128, num_classes=10):\n", + " super(CNNClassifier, self).__init__()\n", + " self.type = 'cnn'\n", + " # Encoder (Feature extractor)\n", + " self.encoder = nn.Sequential(\n", + " nn.Conv2d(in_channels=input_size, out_channels=hidden_size//2, kernel_size=3, stride=2, padding=1),\n", + " nn.ReLU(),\n", + " nn.Conv2d(in_channels=hidden_size//2, out_channels=hidden_size, kernel_size=3, stride=2, padding=1),\n", + " nn.ReLU()\n", + " )\n", + " \n", + " # Classifier\n", + " # Here, for the sake of example, I'm assuming the spatial size of the encoder output \n", + " # is 7x7 for an input size of 28x28. You might want to adjust this if the spatial dimensions change.\n", + " self.classifier = nn.Sequential(\n", + " nn.Flatten(),\n", + " nn.Linear(hidden_size*7*7, hidden_size),\n", + " nn.ReLU(),\n", + " nn.Linear(hidden_size, num_classes),\n", + " nn.LogSoftmax(dim=1) # LogSoftmax is typically used with NLLLoss\n", + " )\n", + "\n", + " def forward(self, x):\n", + " x = self.encoder(x)\n", + " x = self.classifier(x)\n", + " return x\n", + " return x" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:47:30.916320521Z", + "start_time": "2023-10-03T12:47:30.828281294Z" + } + }, + "id": "cb2dfcf75113fd0b" + }, + { + "cell_type": "markdown", + "source": [ + "Train and test functions for the non-linear classifier" + ], + "metadata": { + "collapsed": false + }, + "id": "7c3cf2371479da0c" + }, + { + "cell_type": "code", + "execution_count": 15, + "outputs": [], + "source": [ + "# Train for the classifier\n", + "def train_classifier(model, train_loader, optimizer, criterion, epoch, verbose=True):\n", + " model.train()\n", + " train_loss = 0\n", + " correct = 0 \n", + " for i, (data, target) in enumerate(train_loader):\n", + " if model.type == 'cnn':\n", + " data = data.to(device)\n", + " else:\n", + " data = data.view(data.size(0), -1)\n", + " data = data.to(device)\n", + " target = target.to(device)\n", + " optimizer.zero_grad()\n", + " output = model(data)\n", + " loss = criterion(output, target)\n", + " loss.backward()\n", + " train_loss += loss.item()\n", + " optimizer.step()\n", + " # Calculate correct predictions for training accuracy\n", + " pred = output.argmax(dim=1, keepdim=True)\n", + " correct += pred.eq(target.view_as(pred)).sum().item()\n", + "\n", + " train_loss /= len(train_loader.dataset)\n", + " train_accuracy = 100. * correct / len(train_loader.dataset)\n", + " if verbose:\n", + " print(f'{model.type}====> Epoch: {epoch} Average loss: {train_loss:.4f}')\n", + " print(f'{model.type}====> Epoch: {epoch} Training accuracy: {train_accuracy:.2f}%')\n", + " return train_loss\n", + "\n", + "\n", + "def test_classifier(model, test_loader, criterion):\n", + " model.eval()\n", + " eval_loss = 0\n", + " correct = 0\n", + " with torch.no_grad():\n", + " for i, (data, target) in enumerate(test_loader):\n", + " if model.type == 'cnn':\n", + " data = data.to(device)\n", + " else:\n", + " data = data.view(data.size(0), -1)\n", + " data = data.to(device)\n", + " target = target.to(device)\n", + " output = model(data)\n", + " eval_loss += criterion(output, target).item()\n", + " pred = output.argmax(dim=1, keepdim=True)\n", + " correct += pred.eq(target.view_as(pred)).sum().item()\n", + " eval_loss /= len(test_loader.dataset)\n", + " print('====> Test set loss: {:.4f}'.format(eval_loss))\n", + " accuracy = correct / len(test_loader.dataset)\n", + " print('====> Test set accuracy: {:.4f}'.format(accuracy))\n", + " return accuracy\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:47:30.924063759Z", + "start_time": "2023-10-03T12:47:30.875486950Z" + } + }, + "id": "ac980d25bd8a3dd3" + }, + { + "cell_type": "code", + "execution_count": 16, + "outputs": [], + "source": [ + "# Define the training parameters for the fully connected classifier\n", + "batch_size = 32\n", + "epochs = 5\n", + "learning_rate = 1e-3\n", + "hidden_size = 128\n", + "num_classes = 10\n", + "train_frequency = epochs\n", + "test_frequency = epochs\n", + "# Create the fully connected classifier\n", + "input_size = trainset.data.shape[1] * trainset.data.shape[2]\n", + "classifier = DenseClassifier(input_size, hidden_size, num_classes).to(device)\n", + "input_size = 1\n", + "cnn_classifier = CNNClassifier(input_size, hidden_size, num_classes).to(device)" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:47:30.924544974Z", + "start_time": "2023-10-03T12:47:30.875705556Z" + } + }, + "id": "dff05e622dcfd774" + }, + { + "cell_type": "code", + "execution_count": 17, + "outputs": [], + "source": [ + "# Define the loss function and optimizer\n", + "criterion = nn.CrossEntropyLoss()\n", + "optimizer = optim.Adam(classifier.parameters(), lr=learning_rate)\n", + "optimizer_cnn = optim.Adam(cnn_classifier.parameters(), lr=learning_rate)\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:47:30.924773429Z", + "start_time": "2023-10-03T12:47:30.875787620Z" + } + }, + "id": "3104345cdee0eb00" + }, + { + "cell_type": "markdown", + "source": [ + "Train the non-linear classifiers" + ], + "metadata": { + "collapsed": false + }, + "id": "e1fed39be2f04745" + }, + { + "cell_type": "code", + "execution_count": 18, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " 80%|████████ | 4/5 [02:10<00:32, 32.47s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mlp====> Epoch: 5 Average loss: 0.0030\n", + "mlp====> Epoch: 5 Training accuracy: 97.08%\n", + "cnn====> Epoch: 5 Average loss: 0.0005\n", + "cnn====> Epoch: 5 Training accuracy: 99.49%\n", + "====> Test set loss: 0.0035\n", + "====> Test set accuracy: 0.9686\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 5/5 [02:48<00:00, 33.62s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====> Test set loss: 0.0013\n", + "====> Test set accuracy: 0.9880\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "# Train the classifier\n", + "for epoch in tqdm(range(1, epochs + 1)):\n", + " verbose = True if epoch % train_frequency == 0 else False\n", + " train_classifier(classifier, train_loader, optimizer, criterion, epoch, verbose)\n", + " train_classifier(cnn_classifier, train_loader, optimizer_cnn, criterion, epoch, verbose)\n", + "\n", + " # test every n epochs\n", + " if epoch % test_frequency == 0:\n", + " test_acc = test_classifier(classifier, test_loader, criterion)\n", + " test_acc_cnn = test_classifier(cnn_classifier, test_loader, criterion)\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:50:19.005163249Z", + "start_time": "2023-10-03T12:47:30.875867176Z" + } + }, + "id": "abc0c6ce338d40d9" + }, + { + "cell_type": "markdown", + "source": [ + "Load the encoder weights into the classifier" + ], + "metadata": { + "collapsed": false + }, + "id": "a06038f113d8434f" + }, + { + "cell_type": "code", + "execution_count": 19, + "outputs": [ + { + "data": { + "text/plain": "" + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# initialize the classifier with the encoder weights\n", + "classifier.encoder.load_state_dict(ae.encoder.state_dict())\n", + "cnn_classifier.encoder.load_state_dict(cnn_ae.encoder.state_dict())" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:50:19.005816667Z", + "start_time": "2023-10-03T12:50:18.994175691Z" + } + }, + "id": "6a91d8894b70ef7c" + }, + { + "cell_type": "markdown", + "source": [ + "Transfer learning" + ], + "metadata": { + "collapsed": false + }, + "id": "aafa4a9ba7208647" + }, + { + "cell_type": "code", + "execution_count": 20, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " 80%|████████ | 4/5 [01:56<00:27, 27.00s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mlp====> Epoch: 5 Average loss: 0.0547\n", + "mlp====> Epoch: 5 Training accuracy: 38.00%\n", + "cnn====> Epoch: 5 Average loss: 0.0004\n", + "cnn====> Epoch: 5 Training accuracy: 99.57%\n", + "====> Test set loss: 0.0526\n", + "====> Test set accuracy: 0.4150\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 5/5 [02:25<00:00, 29.01s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====> Test set loss: 0.0017\n", + "====> Test set accuracy: 0.9868\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "# fine-tune the classifier\n", + "learning_rate = 1e-5\n", + "epochs = 5\n", + "train_frequency = epochs\n", + "test_frequency = epochs\n", + "optimizer_pretrained = optim.Adam(classifier.parameters(), lr=learning_rate)\n", + "optimizer_pretrained_cnn = optim.Adam(cnn_classifier.parameters(), lr=learning_rate)\n", + "for epoch in tqdm(range(1, epochs + 1)):\n", + " verbose = True if epoch % train_frequency == 0 else False\n", + " train_loss = train_classifier(classifier, train_loader, optimizer_pretrained, criterion, epoch, verbose)\n", + " train_loss_cnn = train_classifier(cnn_classifier, train_loader, optimizer_cnn, criterion, epoch, verbose)\n", + "\n", + " # test every n epochs\n", + " if epoch % test_frequency == 0:\n", + " test_acc_pretrained = test_classifier(classifier, test_loader, criterion)\n", + " test_acc_pretrained_cnn = test_classifier(cnn_classifier, test_loader, criterion)\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:52:44.085979647Z", + "start_time": "2023-10-03T12:50:19.003728990Z" + } + }, + "id": "a60dd68f988a8249" + }, + { + "cell_type": "markdown", + "source": [ + "Compare the results of the linear probing with the results of the linear classifier" + ], + "metadata": { + "collapsed": false + }, + "id": "31577275b833707a" + }, + { + "cell_type": "code", + "execution_count": 21, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Model Linear Accuracy Non-linear accuracy Pretrained accuracy \n", + "MLP AE 0.2649 0.9686 0.4150 \n", + "CNN AE 0.9278 0.9880 0.9868 \n" + ] + } + ], + "source": [ + "# print a table of the accuracies. compare the results with the results of the linear probing\n", + "print(f\"{'Model':<10} {'Linear Accuracy':<20} {'Non-linear accuracy':<20} {'Pretrained accuracy':<20}\")\n", + "print(f\"{'MLP AE':<10} {test_mlp[-1][1]:<20.4f} {test_acc:<20.4f} {test_acc_pretrained:<20.4f}\")\n", + "print(f\"{'CNN AE':<10} {test_cnn[-1][1]:<20.4f} {test_acc_cnn:<20.4f} {test_acc_pretrained_cnn:<20.4f}\")" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-10-03T12:52:44.091206610Z", + "start_time": "2023-10-03T12:52:44.084572508Z" + } + }, + "id": "40d0e7f3f13404c9" + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [], + "metadata": { + "collapsed": false + }, + "id": "f38a1ab6951a694e" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/conv_animation.ipynb b/conv_animation.ipynb new file mode 100644 index 0000000..abde85c --- /dev/null +++ b/conv_animation.ipynb @@ -0,0 +1,195 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + "| Credentials | |\n", + "|----|----------------------------------|\n", + "|Host | Montanuniversitaet Leoben |\n", + "|Web | https://cps.unileoben.ac.at |\n", + "|Mail | cps@unileoben.ac.at |\n", + "|Author | Fotios Lygerakis |\n", + "|Corresponding Authors | fotios.lygerakis@unileoben.ac.at |\n", + "|Last edited | 28.09.2023 |" + ], + "metadata": { + "collapsed": false + }, + "id": "e94a99fbf273dd6e" + }, + { + "cell_type": "markdown", + "source": [ + "This notebook contains code for visualizing the convolution operation." + ], + "metadata": { + "collapsed": false + }, + "id": "ffa147d97adb2a8" + }, + { + "cell_type": "code", + "execution_count": 19, + "outputs": [ + { + "data": { + "text/plain": "
", + "image/png": "iVBORw0KGgoAAAANSUhEUgAABdEAAAH/CAYAAAC4vJRfAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAACc6UlEQVR4nOzde5yMdf/H8ffsadZhD9YuFoucrfMhEkIUQpG6ReVQud2i+xZS24F02g6IStFRflEKUSnnUw45poiccmbXae2ymD3M9ftj7dixO1h25rI7r+fjMQ8z3/leM+/ru+uanc9853tZDMMwBAAAAAAAAAAAsvExOwAAAAAAAAAAADcriugAAAAAAAAAALhAER0AAAAAAAAAABcoogMAAAAAAAAA4AJFdAAAAAAAAAAAXKCIDgAAAAAAAACACxTRAQAAAAAAAABwgSI6AAAAAAAAAAAuUEQHAAAAAAAAAMAFiuhAPjN58mRZLBbt27fvmvtu2LDB/cFuQhUqVFCfPn3MjnHDLBaLXn75ZbNjAAC8SKtWrdSqVSuzYwAAYLply5bJYrFo2bJlefq4ffr0UYUKFfL0MQG4D0X0AupmK56eO3dOL7/88jW/6GS+SM2YMcO9wQqIDz/8UJMnT87zx3355ZdlsVh04sQJp/aDBw+qUqVKCgsL06ZNm/L8ec2QnJysV199VXXq1FHhwoUVEhKiFi1aaMqUKTIMw+x4AIB8yNXfY4mJiWrcuLECAwM1b948k9IBAOBee/bsUf/+/VWxYkUFBgYqODhYzZo10/jx43X+/Hmz43nEkSNH9PLLL2vz5s1mR8nm5MmTeuaZZ1StWjUFBgYqLCxM7dq1008//XRDjztt2jSNGzcub0Jexc08vih4/MwOAO9w7tw5jRo1SpKY1XSDHn30UT300EOyWq2Otg8//FDh4eEemXV9+PBhtW7dWqdOndKiRYvUoEEDtz+nu8XHx6tNmzbavn27HnroIQ0aNEgXLlzQzJkz1bt3b/3888+aOnWqfH19zY4KAMjnkpKSdPfdd+vPP//U999/r/bt25sdCQCAPDd37lw9+OCDslqt6tWrl2rVqqWUlBStXLlSzzzzjP766y99/PHHZsd0uyNHjmjUqFGqUKGC6tWr53TfJ598IrvdbkquHTt2qE2bNjp+/Lj69u2rRo0a6fTp05o6dao6d+6sYcOG6Z133rmux542bZq2bt2qwYMH523oHFxpfIG8RhEdyGd8fX1NK+YeOXJErVu31smTJ7Vw4UI1bNjwhh8zOTlZRYoUyYN01693797avn27vv/+e917772O9v/+97965plnNHr0aNWvX1/PPvusxzLZ7XalpKQoMDDQY88JAHCvM2fOqF27dtq8ebNmzZqlDh063NDjXbhwQQEBAfLx4culAICbx969e/XQQw+pfPnyWrJkiSIjIx33DRw4ULt379bcuXNNTHhz8Pf3N+V5U1NT9cADDyghIUErVqxQkyZNHPc9/fTTevjhhzV69Gg1atRI3bt3NyUjcDPiL24v0qdPHxUtWlSHDx9Wly5dVLRoUUVERGjYsGFKT0939Nu3b58sFotGjx6td999V+XLl1ehQoXUsmVLbd261ekxXa2XmXVtr3379ikiIkKSNGrUKFksluta4zlzaZGdO3fqkUceUUhIiCIiIvTSSy/JMAwdPHhQ9913n4KDg1WqVCmNGTPGafuUlBSNGDFCDRs2VEhIiIoUKaIWLVpo6dKl2Z7r5MmTevTRRxUcHKzQ0FD17t1bf/zxhywWS7ZlU/7++2898MADCgsLU2BgoBo1aqQffvjhqvvToEED3X///U5ttWvXlsVi0Z9//ulomz59uiwWi7Zv3y4p+5roFSpU0F9//aXly5c7xvbyn4nNZtOQIUMUERGhIkWKqGvXrjp+/PhVM2Z19OhRtW7dWseOHdOCBQvUqFGjXI9DZvbly5frySefVIkSJVS2bFlJGb9LtWrV0rZt29S6dWsVLlxYZcqU0dtvv50ti81m08iRI1W5cmVZrVZFRUVp+PDhstlsudonSfrtt980f/589enTx6mAnik2NlZVqlTRW2+9pfPnzys1NVVhYWHq27dvtr5JSUkKDAzUsGHDcp3VYrFo0KBBmjp1qmrWrCmr1eryK/779+/Xk08+qWrVqqlQoUIqXry4HnzwQad18v/55x9ZLBa9++672bZfvXq1LBaLvv7662sdJgDADTp79qzat2+vTZs2aebMmerYsaPjvsOHD+uxxx5TyZIlZbVaVbNmTX3++edO22cudffNN9/oxRdfVJkyZVS4cGElJSVd8994UsaHtOPGjVPNmjUVGBiokiVLqn///kpISPDIOAAACr63335bZ8+e1WeffeZUQM9UuXJl/e9//3PcTktL06uvvqpKlSrJarWqQoUKev7557O9Z6pQoYI6deqklStXOpZFq1ixoqZMmeLos2HDBlksFn355ZfZnnf+/PmyWCxOy5X8/vvv6tChg4KDg1W0aFG1adNGv/3221X30dX5t7LWSJYtW6Zbb71VktS3b1/H+/XMmkJOa6InJydr6NChioqKktVqVbVq1TR69OhsS4xmvn+cPXu2atWq5fj74VqWiZs5c6a2bt2q5557zqmALmVM2ps0aZJCQ0Odajauzs12+XrxrVq10ty5c7V//37H/mbuY2bf6dOn6/nnn1epUqVUpEgR3XvvvTp48KDT4+bF+AJ5jZnoXiY9PV3t2rVTkyZNNHr0aC1atEhjxoxRpUqVNGDAAKe+U6ZM0ZkzZzRw4EBduHBB48eP15133qktW7aoZMmS1/ycERER+uijjzRgwAB17drVUTiuU6fOde1D9+7dVaNGDb355puaO3euXnvtNYWFhWnSpEm688479dZbb2nq1KkaNmyYbr31Vt1xxx2SMgqcn376qXr06KF+/frpzJkz+uyzz9SuXTutW7fO8dUfu92uzp07a926dRowYICqV6+uOXPmqHfv3tmy/PXXX2rWrJnKlCmj5557TkWKFNG3336rLl26aObMmeratavL/WjRooVTEfPUqVP666+/5OPjo19//dUxPr/++qsiIiJUo0aNHB9n3Lhxeuqpp1S0aFG98MILkpTt5/PUU0+pWLFiGjlypPbt26dx48Zp0KBBmj59+jWNeXx8vB544AHFxcVpwYIFjheq6x2HJ598UhERERoxYoSSk5Md7QkJCWrfvr3uv/9+/etf/9KMGTP07LPPqnbt2o7Zena7Xffee69Wrlypf//736pRo4a2bNmid999Vzt37tTs2bOvaZ8y/fjjj5KkXr165Xi/n5+fevbsqVGjRmnVqlVq27atunbtqlmzZmnSpEkKCAhw9J09e7ZsNpseeuih68q6ZMkSffvttxo0aJDCw8NdnmRm/fr1Wr16tR566CGVLVtW+/bt00cffaRWrVpp27ZtKly4sCpWrKhmzZpp6tSpevrpp522nzp1qoKCgnTfffflaqwAANcnOTlZHTp00Pr16zVjxgx16tTJcV98fLxuu+02x5vhiIgI/fLLL3r88ceVlJSU7avQr776qgICAjRs2DDZbDbH69C1/o3Xv39/TZ48WX379tV///tf7d27Vx988IF+//13rVq1yrRZcQCAguPHH39UxYoVdfvtt19T/yeeeEJffvmlHnjgAQ0dOlRr165VbGys49vCWe3evVsPPPCAHn/8cfXu3Vuff/65+vTpo4YNG6pmzZpq1KiRKlasqG+//Tbbe/jp06erWLFiateunaSM97EtWrRQcHCwhg8fLn9/f02aNEmtWrXS8uXLsxWYc6tGjRp65ZVXNGLECP373/9WixYtJMnluBiGoXvvvVdLly7V448/rnr16mn+/Pl65plndPjw4WwTpFauXKlZs2bpySefVFBQkN577z1169ZNBw4cUPHixV3mutp74JCQEN1333368ssvtXv3blWuXPma9/mFF15QYmKiDh065MhbtGhRpz6vv/66LBaLnn32WR07dkzjxo1T27ZttXnzZhUqVOianyu34wvcMAMF0hdffGFIMtavX+9o6927tyHJeOWVV5z61q9f32jYsKHj9t69ew1JRqFChYxDhw452teuXWtIMp5++mlHW8uWLY2WLVtme/7evXsb5cuXd9w+fvy4IckYOXLkNeVfunSpIcn47rvvHG0jR440JBn//ve/HW1paWlG2bJlDYvFYrz55puO9oSEBKNQoUJG7969nfrabDan50lISDBKlixpPPbYY462mTNnGpKMcePGOdrS09ONO++805BkfPHFF472Nm3aGLVr1zYuXLjgaLPb7cbtt99uVKlS5Yr7+N133xmSjG3bthmGYRg//PCDYbVajXvvvdfo3r27o1+dOnWMrl27Om5n/mz37t3raKtZs2aOP4fMvm3btjXsdruj/emnnzZ8fX2N06dPXzFj5piXL1/eCA4ONtasWZNjv2sdh8w8zZs3N9LS0pweo2XLloYkY8qUKY42m81mlCpVyujWrZuj7f/+7/8MHx8f49dff3XafuLEiYYkY9WqVY628uXLO/0O5KRLly6GJCMhIcFln1mzZhmSjPfee88wDMOYP3++Icn48ccfnfrdc889RsWKFa8rqyTDx8fH+Ouvv7I9/+X/d86dO5etz5o1a7KN36RJkwxJxvbt2x1tKSkpRnh4+FXHBQBw4zJf98qXL2/4+/sbs2fPztbn8ccfNyIjI40TJ044tT/00ENGSEiI45if+bdRxYoVs70OXOvfeL/++qshyZg6dapTv3nz5mVrd/U3HgAAV5KYmGhIMu67775r6r9582ZDkvHEE084tQ8bNsyQZCxZssTRVr58eUOSsWLFCkfbsWPHDKvVagwdOtTRFhMTY/j7+xunTp1ytNlsNiM0NNTpvX+XLl2MgIAAY8+ePY62I0eOGEFBQcYdd9zhaMt8DV66dKlTlpzeU13++rl+/fpsdYRMl9dNZs+ebUgyXnvtNad+DzzwgGGxWIzdu3c72iQZAQEBTm1//PGHIcl4//33sz1XVvXq1TNCQkKu2Gfs2LGGJOOHH34wDCPnOoRh5Dw2HTt2dNqvy/uWKVPGSEpKcrR/++23hiRj/Pjxjra8GF8gr7Gcixf6z3/+43S7RYsW+ueff7L169Kli8qUKeO43bhxYzVp0kQ///yz2zNeyRNPPOG47uvrq0aNGskwDD3++OOO9tDQUFWrVs1pv3x9fR2ztex2u06dOqW0tDQ1atRImzZtcvSbN2+e/P391a9fP0ebj4+PBg4c6JTj1KlTWrJkif71r3/pzJkzOnHihE6cOKGTJ0+qXbt22rVrlw4fPuxyPzI/JV2xYoWkjBnnt956q+666y79+uuvkqTTp09r69atjr7X69///rcsFovTc6enp2v//v3XtH18fLyKFi2a41fxrmcc+vXrl+O67kWLFtUjjzziuB0QEKDGjRs7/Ry/++471ahRQ9WrV3c814kTJ3TnnXdKUo7L81zJmTNnJElBQUEu+2Tel5SUJEm68847FR4e7jSTPyEhQQsXLnRaMy63WVu2bKno6OirZs766XxqaqpOnjypypUrKzQ01Ol3+V//+pcCAwM1depUR9v8+fN14sQJp3EGALhXfHy8AgMDFRUV5dRuGIZmzpypzp07yzAMp9eKdu3aKTEx0em4LmWcx8PVLK2r/Y333XffKSQkRHfddZfTczVs2FBFixbN9WsoAACXy3zPdKX3V1ll1heGDBni1D506FBJyrZ2enR0tNP744iIiGzv/bt3767U1FTNmjXL0bZgwQKdPn3a8X4tPT1dCxYsUJcuXVSxYkVHv8jISPXs2VMrV6507Iun/Pzzz/L19dV///tfp/ahQ4fKMAz98ssvTu1t27ZVpUqVHLfr1Kmj4ODgHOs7WZ05c+aqP5/L3wPnpV69ejk9/wMPPKDIyEjTa03A1VBE9zKBgYGO9ckzFStWLMd1MKtUqZKtrWrVqtnWwPK0cuXKOd0OCQlRYGCgwsPDs7Vfvl9ffvml6tSpo8DAQBUvXlwRERGaO3euEhMTHX3279+vyMhIFS5c2Gnby7/CtHv3bhmGoZdeekkRERFOl5EjR0qSjh075nI/SpYsqSpVqjgK5r/++qtatGihO+64Q0eOHNE///yjVatWyW6333AR/fIxK1asmCRd8/qnX331lU6dOqW77ror2z5dzzjccsstOT5P2bJlnYr9mVmz5ty1a5f++uuvbM9VtWrVHJ/rajJfvDOL6Tm5vNDu5+enbt26ac6cOY51+mbNmqXU1FSnInpus7oal8udP39eI0aMcKyTFx4eroiICJ0+fdrpdzk0NFSdO3fWtGnTHG1Tp05VmTJlHIV8AID7ZS7/1b59e+3YscPRfvz4cZ0+fVoff/xxtteKzHNvXOtrxbX8jbdr1y4lJiaqRIkS2Z7v7NmzuX4NBQDgcsHBwZKu/P4qq/3798vHxyfb++1SpUopNDQ028Svy9/bStlf7+rWravq1as7TXqaPn26wsPDHe+Djh8/rnPnzqlatWrZHq9GjRqy2+3Z1ul2t/3796t06dLZCtyZS7tez1jkJCgo6Ko/n2uZbHa9Lq81WSwWVa5c2fRaE3A1rInuZXKa/XsjLBZLthNcSMp2Equ8lNM+uNqvrNm++uor9enTR126dNEzzzyjEiVKyNfXV7GxsdqzZ0+uc9jtdknSsGHDHGuqXe5qa4c1b95cixcv1vnz57Vx40aNGDFCtWrVUmhoqH799Vdt375dRYsWVf369XOdL6trGZ8radmypb799lvdf//9ateunZYtW6aQkBBJ1zcOrmbQXUtOu92u2rVra+zYsTn2vXyW39XUqFFDs2fP1p9//ulYP/9ymSd6zTpL/KGHHtKkSZP0yy+/qEuXLvr2229VvXp11a1b97qzXuv6b0899ZS++OILDR48WE2bNlVISIgsFoseeughx88jU69evfTdd99p9erVql27tn744Qc9+eST8vHhM1QA8JTo6Gj9/PPPatOmje666y6tWrVKUVFRjmP2I488kuO5V6Ts55DJ7WtoVna7XSVKlHD6hlJWlxfhAQDIreDgYJUuXVpbt27N1XaXT6Zy5Vrf23bv3l2vv/66Tpw4oaCgIP3www/q0aOH/PzypgzmKm96enqe111cud73+TVq1NDmzZt14MCBHAvxUvb3wFfaX3e4GcYXuBxFdLi0a9eubG07d+50OtlhsWLFcvyq0OWfkF7rC6I7zZgxQxUrVtSsWbOc8mTOls5Uvnx5LV26VOfOnXOajb57926nfplf+fL391fbtm2vK1OLFi30xRdf6JtvvlF6erpuv/12+fj4qHnz5o4i+u23337VFwlPjG/nzp31+eefq3fv3urUqZMWLFigQoUK5ck45EalSpX0xx9/qE2bNnmy3506dVJsbKymTJmSYxE9PT1d06ZNU7FixdSsWTNH+x133KHIyEhNnz5dzZs315IlSxwndnVX1kwzZsxQ7969NWbMGEfbhQsXdPr06Wx927dvr4iICE2dOlVNmjTRuXPn9Oijj+ZZFgDAtWncuLFmz56tjh07OpZui4iIUFBQkNLT0z32Grpo0SI1a9YsVyfuAgAgNzp16qSPP/5Ya9asUdOmTa/Yt3z58rLb7dq1a5djxrWUsRTa6dOnVb58+evK0L17d40aNUozZ85UyZIllZSUpIceeshxf0REhAoXLuz0DbFMf//9t3x8fK44QatYsWI5vv/av3+/0/IwuXkfWL58eS1atCjbcit///234/680KlTJ3399deaMmWKXnzxxWz3JyUlac6cOapevbpjQlzmt9kv3+ecloi92j5fXmsyDEO7d+92mjjgjvEFbhRTEeHS7NmzndayXrdundauXasOHTo42ipVqqS///5bx48fd7T98ccfWrVqldNjZRajczoIekpmITrrp7Jr167VmjVrnPq1a9dOqamp+uSTTxxtdrtdEyZMcOpXokQJtWrVSpMmTdLRo0ezPV/WMXElc5mWt956S3Xq1HHM7m7RooUWL16sDRs2XNNSLkWKFPHI2D766KMaN26cVq5cqW7duik1NTVPxiE3/vWvf+nw4cNOP59M58+fV3Jycq4e7/bbb1fbtm31xRdf6Keffsp2/wsvvKCdO3dq+PDhTgUHHx8fPfDAA/rxxx/1f//3f0pLS3NaysUdWTP5+vpmm13w/vvv5zgLwM/PTz169NC3336ryZMnq3bt2tlmNQIAPKNNmzb6+uuvtXv3brVv317Jycnq1q2bZs6cmeOMPXe8hqanp+vVV1/Ndl9aWpqpf6cBAAqO4cOHq0iRInriiScUHx+f7f49e/Zo/PjxkqR77rlHkjRu3DinPpnf5u3YseN1ZahRo4Zq166t6dOna/r06YqMjHSaNOXr66u7775bc+bMcVpGJD4+XtOmTVPz5s0dS9PkpFKlSvrtt9+UkpLiaPvpp5+yLQFTpEgRSddWC7nnnnuUnp6uDz74wKn93XfflcVicarF3IgHHnhA0dHRevPNN7Vhwwan++x2uwYMGKCEhASnCYeZa69nntNNyphw9vHHH2d7/CJFijgtM3q5KVOmOC0nM2PGDB09ejRbrSmvxxe4UcxEh0uVK1dW8+bNNWDAANlsNo0bN07FixfX8OHDHX0ee+wxjR07Vu3atdPjjz+uY8eOaeLEiapZs6bTCSgKFSqk6OhoTZ8+XVWrVlVYWJhq1aqlWrVqeWx/OnXqpFmzZqlr167q2LGj9u7dq4kTJyo6Olpnz5519OvSpYsaN26soUOHavfu3apevbp++OEHnTp1SpLzJ50TJkxQ8+bNVbt2bfXr108VK1ZUfHy81qxZo0OHDumPP/64YqbKlSurVKlS2rFjh5566ilH+x133KFnn31Wkq6piN6wYUN99NFHeu2111S5cmWVKFHCbWte//e//9WpU6c0atQo9erVS1OnTr3hcciNRx99VN9++63+85//aOnSpWrWrJnS09P1999/69tvv9X8+fPVqFGjXD3mlClT1KZNG913333q2bOnWrRoIZvNplmzZmnZsmXq3r27nnnmmWzbde/eXe+//75Gjhyp2rVrO82ccFdWKeN3+f/+7/8UEhKi6OhorVmzRosWLVLx4sVz7N+rVy+99957Wrp0qd56661cPx8AIO907dpVn3zyiR577DHde++9+vLLL7V06VI1adJE/fr1U3R0tE6dOqVNmzZp0aJFjr8/8kLLli3Vv39/xcbGavPmzbr77rvl7++vXbt26bvvvtP48eP1wAMP5NnzAQC8U6VKlTRt2jR1795dNWrUUK9evVSrVi2lpKRo9erV+u6779SnTx9JGeuX9+7dWx9//LFOnz6tli1bat26dfryyy/VpUsXtW7d+rpzdO/eXSNGjFBgYKAef/zxbEtavvbaa1q4cKGaN2+uJ598Un5+fpo0aZJsNpvefvvtKz72E088oRkzZqh9+/b617/+pT179uirr75yOtFn5liEhoZq4sSJCgoKUpEiRdSkSZMcz3HSuXNntW7dWi+88IL27dununXrasGCBZozZ44GDx6c7bGvV0BAgGbMmKE2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" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.patches as patches\n", + "from matplotlib.animation import FuncAnimation, PillowWriter\n", + "\n", + "# Sample image (5x5)\n", + "image = np.array([\n", + " [1, 2, 3, 4, 5],\n", + " [5, 4, 3, 2, 1],\n", + " [1, 2, 3, 4, 5],\n", + " [5, 4, 3, 2, 1],\n", + " [1, 2, 3, 4, 5]\n", + "])\n", + "\n", + "# Kernel (3x3)\n", + "kernel = np.array([\n", + " [1, 0, -1],\n", + " [1, 0, -1],\n", + " [1, 0, -1]\n", + "])\n", + "# # kernel (2x2)\n", + "# kernel = np.array([\n", + "# [1, 0],\n", + "# [1, 0]\n", + "# ])\n", + "\n", + "# Stride and Padding\n", + "stride = 1\n", + "padding = 1\n", + "\n", + "# Create padded image\n", + "padded_image = np.pad(image, ((padding, padding), (padding, padding)))\n", + "\n", + "# Calculate output dimensions\n", + "output_dim = ((image.shape[0] - kernel.shape[0] + 2*padding) // stride) + 1\n", + "\n", + "# Initialize the convolution output\n", + "conv_output = np.zeros((output_dim, output_dim))\n", + "\n", + "# Calculate all possible top-left positions for the kernel\n", + "positions = [(y, x) for y in range(0, padded_image.shape[0] - kernel.shape[0] + 1, stride)\n", + " for x in range(0, padded_image.shape[1] - kernel.shape[1] + 1, stride)]\n", + "\n", + "# Set up the plotting with three subplots for the image, kernel, and convolution output\n", + "fig, (ax1, ax_kernel, ax2) = plt.subplots(1, 3, figsize=(15, 5))\n", + "ax1.imshow(padded_image, cmap='viridis', aspect='equal') # Use equal aspect for square pixels\n", + "ax_kernel.imshow(kernel, cmap='viridis', aspect='equal') # Display the kernel\n", + "ax2.imshow(conv_output, cmap='viridis', aspect='equal', vmin=-15, vmax=15) # Use equal aspect and set vmin, vmax for consistent color scaling\n", + "\n", + "# Add subplot titles\n", + "ax1.set_title(\"Input Image with Kernel Overlay\")\n", + "ax_kernel.set_title(\"Kernel\")\n", + "ax2.set_title(\"Convolution Output\")\n", + "\n", + "# Display numbers on the matrix for the padded image\n", + "for i in range(padded_image.shape[0]):\n", + " for j in range(padded_image.shape[1]):\n", + " ax1.text(j, i, str(padded_image[i, j]), ha='center', va='center', color='red')\n", + "\n", + "# Display numbers on the kernel\n", + "for i in range(kernel.shape[0]):\n", + " for j in range(kernel.shape[1]):\n", + " ax_kernel.text(j, i, str(kernel[i, j]), ha='center', va='center', color='red')\n", + "\n", + "# Kernel rectangle overlay on ax1\n", + "rect = patches.Rectangle((-0.5, -0.5), kernel.shape[1], kernel.shape[0], \n", + " linewidth=3, edgecolor='blue', facecolor='none')\n", + "ax1.add_patch(rect)\n", + "\n", + "# Animation function\n", + "def animate(i):\n", + " y, x = positions[i]\n", + " rect.set_xy((x-0.5, y-0.5))\n", + "\n", + " # Compute the convolution for the current position\n", + " region = padded_image[y:y+kernel.shape[0], x:x+kernel.shape[1]]\n", + " conv_value = np.sum(region * kernel)\n", + " \n", + " # Correctly compute the position in the output matrix\n", + " out_y = y // stride\n", + " out_x = x // stride\n", + " conv_output[out_y, out_x] = conv_value\n", + "\n", + " # Update the convolution output display\n", + " ax2.imshow(conv_output, cmap='viridis', aspect='equal', vmin=-15, vmax=15) # Adjusted colormap and vmin, vmax for better visualization\n", + "\n", + " # Display numbers on the matrix for the convolution output\n", + " for i in range(conv_output.shape[0]):\n", + " for j in range(conv_output.shape[1]):\n", + " ax2.text(j, i, f\"{conv_output[i, j]:.1f}\", ha='center', va='center', color='red')\n", + "\n", + " return rect,\n", + "\n", + "\n", + "# Create animation with increased interval for slower movement\n", + "ani = FuncAnimation(fig, animate, frames=len(positions), interval=2000, blit=True, repeat_delay=10000) # interval set to 5000 for slower movement\n", + "\n", + "# Title and layout\n", + "# title_text = f\"Kernel Size: {kernel.shape[0]}x{kernel.shape[1]}, Stride: {stride}, Padding: {padding}\"\n", + "# fig.suptitle(title_text, fontsize=16)\n", + "\n", + "# Adjust subplot spacing and layout\n", + "plt.subplots_adjust(wspace=0.5)\n", + "plt.tight_layout()\n", + "\n", + "# Save and show\n", + "writer = PillowWriter(fps=2)\n", + "ani.save(f\"convolution_animation_with_output_k{kernel.shape[0]}_s{stride}_p{padding}.gif\", writer=writer)\n", + "plt.show()\n" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2023-09-26T11:48:00.679662904Z", + "start_time": "2023-09-26T11:47:47.667621921Z" + } + }, + "id": "7e43a9eac318f3c3" + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [], + "metadata": { + "collapsed": false + }, + "id": "bce4c6c7bb659beb" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/evaluation_results.log b/evaluation_results.log new file mode 100644 index 0000000..7c62658 --- /dev/null +++ b/evaluation_results.log @@ -0,0 +1 @@ +reconstruction_loss: 0.001255071537196636linear_classification_accuracy: 0.3665knn_classification_accuracy: 0.3889clustering_ari_score: 0.02700985553219709 \ No newline at end of file