AppliedMachineAndDeepLearni.../autoencoder_cifar10.ipynb
2023-10-09 10:02:01 +00:00

1623 lines
121 KiB
Plaintext

{
"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": "<torch._C.Generator at 0x7f098e1490d0>"
},
"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": "<Figure size 1000x400 with 10 Axes>",
"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/v9985CMfMY2Njcbn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3LkT9kuT18fBOB73M23aNPna174mO3fulHnz5smvf/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": "<All keys matched successfully>"
},
"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
}