diff --git a/lectures/03_linear_regressioin_autogradient/0_pytorch_fundamentals_A.ipynb b/lectures/03_linear_regressioin_autogradient/0_pytorch_fundamentals_A.ipynb index a7f44b4..340a7f2 100644 --- a/lectures/03_linear_regressioin_autogradient/0_pytorch_fundamentals_A.ipynb +++ b/lectures/03_linear_regressioin_autogradient/0_pytorch_fundamentals_A.ipynb @@ -13,7 +13,7 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 2, "id": "739c5173", "metadata": {}, "outputs": [ @@ -23,7 +23,7 @@ "'2.6.0+cu126'" ] }, - "execution_count": 48, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -43,7 +43,7 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 3, "id": "0e82be1e", "metadata": {}, "outputs": [ @@ -53,7 +53,7 @@ "tensor(5)" ] }, - "execution_count": 49, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -66,7 +66,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 4, "id": "7c239759", "metadata": {}, "outputs": [ @@ -76,7 +76,7 @@ "0" ] }, - "execution_count": 50, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -87,7 +87,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 5, "id": "d176548d", "metadata": {}, "outputs": [ @@ -97,7 +97,7 @@ "torch.Size([])" ] }, - "execution_count": 51, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -108,7 +108,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 6, "id": "07e03145", "metadata": {}, "outputs": [ @@ -118,7 +118,7 @@ "5" ] }, - "execution_count": 52, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -129,7 +129,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 7, "id": "41fcc46e", "metadata": {}, "outputs": [ @@ -139,7 +139,7 @@ "tensor([1, 2, 3])" ] }, - "execution_count": 53, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -152,7 +152,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 8, "id": "f9894c37", "metadata": {}, "outputs": [ @@ -162,7 +162,7 @@ "1" ] }, - "execution_count": 54, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -173,7 +173,7 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 9, "id": "7dc166eb", "metadata": {}, "outputs": [ @@ -183,7 +183,7 @@ "torch.Size([3])" ] }, - "execution_count": 55, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -195,7 +195,7 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 10, "id": "2581817b", "metadata": {}, "outputs": [ @@ -206,7 +206,7 @@ " [ 9, 10]])" ] }, - "execution_count": 56, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -220,7 +220,7 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": 11, "id": "46961042", "metadata": {}, "outputs": [ @@ -230,7 +230,7 @@ "2" ] }, - "execution_count": 57, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -241,7 +241,7 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": 12, "id": "9669fda8", "metadata": {}, "outputs": [ @@ -251,7 +251,7 @@ "torch.Size([2, 2])" ] }, - "execution_count": 58, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -262,7 +262,7 @@ }, { "cell_type": "code", - "execution_count": 59, + "execution_count": 13, "id": "15297945", "metadata": {}, "outputs": [ @@ -274,7 +274,7 @@ " [2, 4, 5]]])" ] }, - "execution_count": 59, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -289,7 +289,7 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 14, "id": "5bbed071", "metadata": {}, "outputs": [ @@ -299,7 +299,7 @@ "3" ] }, - "execution_count": 60, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -310,7 +310,7 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 15, "id": "483d25c7", "metadata": {}, "outputs": [ @@ -320,7 +320,7 @@ "torch.Size([1, 3, 3])" ] }, - "execution_count": 61, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -331,7 +331,7 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 16, "id": "c4e76ef2", "metadata": {}, "outputs": [ @@ -341,7 +341,7 @@ "torch.Size([1, 3, 3])" ] }, - "execution_count": 62, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -352,7 +352,7 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 17, "id": "b56abf50", "metadata": {}, "outputs": [ @@ -364,7 +364,7 @@ " [6, 9]])" ] }, - "execution_count": 63, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -376,7 +376,7 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 18, "id": "cdd39ae8", "metadata": {}, "outputs": [ @@ -391,7 +391,7 @@ " [9]])" ] }, - "execution_count": 64, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -403,7 +403,7 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 19, "id": "adf1ab41", "metadata": {}, "outputs": [ @@ -415,7 +415,7 @@ " [2., 4., 5.]]])" ] }, - "execution_count": 65, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -430,7 +430,7 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 20, "id": "a368079f", "metadata": {}, "outputs": [ @@ -440,7 +440,7 @@ "torch.float32" ] }, - "execution_count": 66, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -451,20 +451,20 @@ }, { "cell_type": "code", - "execution_count": 67, + "execution_count": 21, "id": "4d00ea95", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "(tensor([[0.6636, 0.4190],\n", - " [0.4294, 0.9632]]),\n", - " tensor([[0.0473, 0.9045],\n", - " [0.2971, 0.3203]]))" + "(tensor([[0.9019, 0.8531],\n", + " [0.9996, 0.5826]]),\n", + " tensor([[0.0682, 0.6102],\n", + " [0.5610, 0.0305]]))" ] }, - "execution_count": 67, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -485,7 +485,7 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": 22, "id": "aeed7a0a", "metadata": {}, "outputs": [ @@ -495,7 +495,7 @@ "tensor([1, 2])" ] }, - "execution_count": 68, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -507,7 +507,7 @@ }, { "cell_type": "code", - "execution_count": 69, + "execution_count": 23, "id": "721ce7eb", "metadata": {}, "outputs": [ @@ -517,7 +517,7 @@ "tensor([1, 2])" ] }, - "execution_count": 69, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } @@ -529,7 +529,7 @@ }, { "cell_type": "code", - "execution_count": 70, + "execution_count": 24, "id": "6423f4d2", "metadata": {}, "outputs": [ @@ -539,7 +539,7 @@ "tensor(6)" ] }, - "execution_count": 70, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } @@ -551,7 +551,7 @@ }, { "cell_type": "code", - "execution_count": 71, + "execution_count": 25, "id": "0125386f", "metadata": {}, "outputs": [ @@ -561,7 +561,7 @@ "tensor([3, 4, 5, 6])" ] }, - "execution_count": 71, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } @@ -573,7 +573,7 @@ }, { "cell_type": "code", - "execution_count": 72, + "execution_count": 26, "id": "97373387", "metadata": {}, "outputs": [ @@ -584,7 +584,7 @@ " [4, 5]])" ] }, - "execution_count": 72, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } @@ -604,7 +604,7 @@ }, { "cell_type": "code", - "execution_count": 73, + "execution_count": 27, "id": "bba6b1b4", "metadata": {}, "outputs": [ @@ -614,7 +614,7 @@ "tensor([4, 5, 6])" ] }, - "execution_count": 73, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -626,7 +626,7 @@ }, { "cell_type": "code", - "execution_count": 74, + "execution_count": 28, "id": "12a96c84", "metadata": {}, "outputs": [ @@ -636,7 +636,7 @@ "tensor([3, 6, 9])" ] }, - "execution_count": 74, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -648,7 +648,7 @@ }, { "cell_type": "code", - "execution_count": 75, + "execution_count": 29, "id": "a0f73c88", "metadata": {}, "outputs": [ @@ -658,7 +658,7 @@ "tensor([[5, 6]])" ] }, - "execution_count": 75, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } @@ -678,12 +678,12 @@ "id": "a3c1d8b5", "metadata": {}, "source": [ - "### sum, mean, " + "### sum, mean, max" ] }, { "cell_type": "code", - "execution_count": 76, + "execution_count": 30, "id": "da5391eb", "metadata": {}, "outputs": [ @@ -693,7 +693,7 @@ "tensor([False, False, True, True])" ] }, - "execution_count": 76, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -706,7 +706,7 @@ }, { "cell_type": "code", - "execution_count": 77, + "execution_count": 31, "id": "78ed8e4b", "metadata": {}, "outputs": [ @@ -716,7 +716,7 @@ "tensor(2)" ] }, - "execution_count": 77, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" } @@ -727,16 +727,135 @@ ] }, { - "cell_type": "markdown", - "id": "02a00747", + "cell_type": "code", + "execution_count": 32, + "id": "4698dc38", "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor(6)" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "### Operation" + "torch.tensor([0, 1, 2, 3]).sum()" ] }, { "cell_type": "code", - "execution_count": 78, + "execution_count": 54, + "id": "cfa1dcae", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor(6)" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "y=torch.tensor([0, 1, 2, 3])\n", + "torch.sum(y)" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "f27ae72f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor(3)" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "y=torch.tensor([0, 1, 2, 3])\n", + "torch.max(y)" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "id": "e10312d5", + "metadata": {}, + "outputs": [], + "source": [ + "test_outputs = torch.tensor([[2.5, 0.8, 1.3], # Sample 1\n", + " [0.4, 3.2, 1.9]]) # Sample 2\n", + "max_values, max_indices = torch.max(test_outputs,1)" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "id": "7f887d49", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([2.5000, 3.2000])" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "max_values" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "600af54b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([0, 1])" + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "max_indices" + ] + }, + { + "cell_type": "markdown", + "id": "02a00747", + "metadata": {}, + "source": [ + "### Operations" + ] + }, + { + "cell_type": "code", + "execution_count": 33, "id": "45267f2f", "metadata": {}, "outputs": [ @@ -749,7 +868,7 @@ " [7, 8]]))" ] }, - "execution_count": 78, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } @@ -764,7 +883,7 @@ }, { "cell_type": "code", - "execution_count": 79, + "execution_count": 34, "id": "193a7828", "metadata": {}, "outputs": [ @@ -775,7 +894,7 @@ " [10, 12]])" ] }, - "execution_count": 79, + "execution_count": 34, "metadata": {}, "output_type": "execute_result" } @@ -786,7 +905,7 @@ }, { "cell_type": "code", - "execution_count": 80, + "execution_count": 35, "id": "1ce81689", "metadata": {}, "outputs": [ @@ -797,7 +916,7 @@ " [21, 32]])" ] }, - "execution_count": 80, + "execution_count": 35, "metadata": {}, "output_type": "execute_result" } @@ -809,7 +928,7 @@ }, { "cell_type": "code", - "execution_count": 81, + "execution_count": 36, "id": "62f8cde3", "metadata": {}, "outputs": [ @@ -819,7 +938,7 @@ "tensor([11, 12, 13])" ] }, - "execution_count": 81, + "execution_count": 36, "metadata": {}, "output_type": "execute_result" } @@ -834,7 +953,7 @@ }, { "cell_type": "code", - "execution_count": 82, + "execution_count": 37, "id": "2098ad78", "metadata": {}, "outputs": [ @@ -845,7 +964,7 @@ " [4, 5, 6]])" ] }, - "execution_count": 82, + "execution_count": 37, "metadata": {}, "output_type": "execute_result" } @@ -858,7 +977,7 @@ }, { "cell_type": "code", - "execution_count": 83, + "execution_count": 38, "id": "883321f8", "metadata": {}, "outputs": [ @@ -870,7 +989,7 @@ " [5, 6]])" ] }, - "execution_count": 83, + "execution_count": 38, "metadata": {}, "output_type": "execute_result" } @@ -879,6 +998,113 @@ "tensor.view(3, 2) # Reshape to 3x2" ] }, + { + "cell_type": "code", + "execution_count": 39, + "id": "9ceace9b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([False, True, True, False, True, False])" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x = torch.tensor([0, 1, 1, 0, 1, 0])\n", + "x == 1" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "96ea0d2f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([ True, False, True, True, False, False])" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "y = torch.tensor([0, 1, 0, 0, 1, 1])\n", + "y == 0" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "c1d9f060", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([False, False, True, False, False, False])" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "(x == 1) & (y==0)" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "796d977f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor(1)" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "((x == 1) & (y==0)).sum()" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "60402427", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor(1)" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "torch.sum(((x == 1) & (y==0)))" + ] + }, { "cell_type": "markdown", "id": "9d716eb9", @@ -893,7 +1119,7 @@ }, { "cell_type": "code", - "execution_count": 84, + "execution_count": 44, "id": "2a3fd4ae", "metadata": {}, "outputs": [ @@ -903,7 +1129,7 @@ "tensor([1, 2, 3])" ] }, - "execution_count": 84, + "execution_count": 44, "metadata": {}, "output_type": "execute_result" } @@ -916,7 +1142,7 @@ }, { "cell_type": "code", - "execution_count": 85, + "execution_count": 45, "id": "df247bd3", "metadata": {}, "outputs": [ @@ -926,7 +1152,7 @@ "array([1, 2, 3])" ] }, - "execution_count": 85, + "execution_count": 45, "metadata": {}, "output_type": "execute_result" } @@ -938,7 +1164,7 @@ }, { "cell_type": "code", - "execution_count": 86, + "execution_count": 46, "id": "9ada07ab", "metadata": {}, "outputs": [ @@ -948,7 +1174,7 @@ "tensor([1, 2, 3])" ] }, - "execution_count": 86, + "execution_count": 46, "metadata": {}, "output_type": "execute_result" } @@ -968,7 +1194,7 @@ }, { "cell_type": "code", - "execution_count": 87, + "execution_count": 47, "id": "30c9ea9f", "metadata": {}, "outputs": [ @@ -978,7 +1204,7 @@ "True" ] }, - "execution_count": 87, + "execution_count": 47, "metadata": {}, "output_type": "execute_result" } @@ -991,7 +1217,7 @@ }, { "cell_type": "code", - "execution_count": 88, + "execution_count": 48, "id": "dd523b3e", "metadata": {}, "outputs": [ @@ -1001,7 +1227,7 @@ "'cuda'" ] }, - "execution_count": 88, + "execution_count": 48, "metadata": {}, "output_type": "execute_result" } @@ -1014,7 +1240,7 @@ }, { "cell_type": "code", - "execution_count": 89, + "execution_count": 49, "id": "11d1a029", "metadata": {}, "outputs": [ @@ -1031,7 +1257,7 @@ "tensor([1, 2, 3], device='cuda:0')" ] }, - "execution_count": 89, + "execution_count": 49, "metadata": {}, "output_type": "execute_result" } @@ -1050,7 +1276,7 @@ }, { "cell_type": "code", - "execution_count": 90, + "execution_count": 50, "id": "db5249d0", "metadata": {}, "outputs": [ @@ -1058,7 +1284,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "C:\\Users\\Weife\\AppData\\Local\\Temp\\ipykernel_111340\\3540074575.py:6: UserWarning: To copy construct from a tensor, it is recommended to use sourceTensor.clone().detach() or sourceTensor.clone().detach().requires_grad_(True), rather than torch.tensor(sourceTensor).\n", + "C:\\Users\\Weife\\AppData\\Local\\Temp\\ipykernel_154616\\3540074575.py:6: UserWarning: To copy construct from a tensor, it is recommended to use sourceTensor.clone().detach() or sourceTensor.clone().detach().requires_grad_(True), rather than torch.tensor(sourceTensor).\n", " y = torch.tensor(x, device=device) # directly create a tensor on GPU\n" ] } diff --git a/lectures/07_binary_classification_n_to_1/0_ML_workflow_breast_cancer.pptx b/lectures/07_binary_classification_n_to_1/0_ML_workflow_breast_cancer.pptx new file mode 100644 index 0000000..7be7ba9 Binary files /dev/null and b/lectures/07_binary_classification_n_to_1/0_ML_workflow_breast_cancer.pptx differ diff --git a/lectures/07_binary_classification_n_to_1/0_predict_breast_cancer_A_steps.ipynb b/lectures/07_binary_classification_n_to_1/0_predict_breast_cancer_A_steps.ipynb new file mode 100644 index 0000000..3f2ad0c --- /dev/null +++ b/lectures/07_binary_classification_n_to_1/0_predict_breast_cancer_A_steps.ipynb @@ -0,0 +1,484 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "31ee256c", + "metadata": {}, + "source": [ + "## Breast cancer prediction" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "53af081c", + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "import torch.nn as nn\n", + "import numpy as np\n", + "from sklearn.datasets import load_breast_cancer\n", + "from sklearn.preprocessing import StandardScaler\n", + "from sklearn.model_selection import train_test_split" + ] + }, + { + "cell_type": "markdown", + "id": "536078f0", + "metadata": {}, + "source": [ + "### Load and preprocess breast cancer dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "06746e3c", + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"Load and preprocess breast cancer dataset.\"\"\"\n", + "# Load dataset\n", + "data = load_breast_cancer()\n", + "X, y = data.data, data.target" + ] + }, + { + "cell_type": "markdown", + "id": "3477485c", + "metadata": {}, + "source": [ + "### Understand inputs" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "76d4d576", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(569, 30)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "fddcc037", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([1.799e+01, 1.038e+01, 1.228e+02, 1.001e+03, 1.184e-01, 2.776e-01,\n", + " 3.001e-01, 1.471e-01, 2.419e-01, 7.871e-02, 1.095e+00, 9.053e-01,\n", + " 8.589e+00, 1.534e+02, 6.399e-03, 4.904e-02, 5.373e-02, 1.587e-02,\n", + " 3.003e-02, 6.193e-03, 2.538e+01, 1.733e+01, 1.846e+02, 2.019e+03,\n", + " 1.622e-01, 6.656e-01, 7.119e-01, 2.654e-01, 4.601e-01, 1.189e-01])" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X[0, :]" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "070dcd69", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(569,)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "y.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c4632c29", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "np.int64(0)" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "y[0]" + ] + }, + { + "cell_type": "markdown", + "id": "b74373cb", + "metadata": {}, + "source": [ + " ### Split dataset into training and testing" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "0675a8c7", + "metadata": {}, + "outputs": [], + "source": [ + "X_train, X_test, y_train, y_test = train_test_split(\n", + " X, y, test_size=0.2, random_state=1234\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "bfe70bd9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(455, 30)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X_train.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "a4df0052", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(114, 30)" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X_test.shape" + ] + }, + { + "cell_type": "markdown", + "id": "d597a997", + "metadata": {}, + "source": [ + "### Scale fetures\n", + "Scaling features, as done in the code with StandardScaler, transforms the input data so that each feature has a mean of 0 and a standard deviation of 1. This is also known as standardization. The purpose of scaling features in this context is to:\n", + "\n", + "- Improve Model Convergence: Many machine learning algorithms, including neural networks optimized with gradient-based methods like SGD, converge faster when features are on a similar scale. Unscaled features with different ranges can cause gradients to vary widely, slowing down or destabilizing training.\n", + "- Ensure Fair Feature Influence: Features with larger numerical ranges could disproportionately influence the model compared to features with smaller ranges. Standardization ensures all features contribute equally to the model's predictions.\n", + "- Enhance Numerical Stability: Large or highly variable feature values can lead to numerical instability in computations, especially in deep learning frameworks like PyTorch. Scaling mitigates this risk." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "3aeb88da", + "metadata": {}, + "outputs": [], + "source": [ + "# Scale features\n", + "scaler = StandardScaler()\n", + "X_train = scaler.fit_transform(X_train)\n", + "X_test = scaler.transform(X_test)\n", + "\n", + "# Convert to PyTorch tensors\n", + "X_train = torch.from_numpy(X_train.astype(np.float32))\n", + "X_test = torch.from_numpy(X_test.astype(np.float32))\n", + "y_train = torch.from_numpy(y_train.astype(np.float32)).view(-1, 1)\n", + "y_test = torch.from_numpy(y_test.astype(np.float32)).view(-1, 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "3b10079f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "torch.Size([455, 30])" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X_train.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "13f4059c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([-0.3618, -0.2652, -0.3172, -0.4671, 1.8038, 1.1817, -0.5169, 0.1065,\n", + " -0.3901, 1.3914, 0.1437, -0.1208, 0.1601, -0.1326, -0.5863, -0.1248,\n", + " -0.5787, 0.1091, -0.2819, -0.1889, -0.2571, -0.2403, -0.2442, -0.3669,\n", + " 0.5449, 0.2481, -0.7109, -0.0797, -0.5280, 0.2506])" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X_train[0,:]" + ] + }, + { + "cell_type": "markdown", + "id": "b0b15d2f", + "metadata": {}, + "source": [ + "### Binary Classifier model" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "e1b50a04", + "metadata": {}, + "outputs": [], + "source": [ + "class BinaryClassifier(nn.Module):\n", + " \"\"\"Simple neural network for binary classification.\"\"\"\n", + " def __init__(self, input_features):\n", + " super(BinaryClassifier, self).__init__()\n", + " self.linear = nn.Linear(input_features, 1)\n", + " \n", + " def forward(self, x):\n", + " return torch.sigmoid(self.linear(x))" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "49694959", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "torch.Size([455, 30])" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X_train.shape" + ] + }, + { + "cell_type": "markdown", + "id": "14873622", + "metadata": {}, + "source": [ + "### show binary classification model \n", + "- the number of input features\n", + "- the number of output features" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "466f6c41", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "BinaryClassifier(\n", + " (linear): Linear(in_features=30, out_features=1, bias=True)\n", + ")" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n_features = X_train.shape[1]\n", + "model = BinaryClassifier(n_features)\n", + "model" + ] + }, + { + "cell_type": "markdown", + "id": "c66978b5", + "metadata": {}, + "source": [ + "### Train the model with given parameters.\n", + "\n", + "- forward pass: prediction\n", + "- loss: error\n", + "- autograd: weight change direction\n", + "- stochastic gradient descent (optimizer): update weights\n", + "- optimizer.zero_grad()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "1d1d7868", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch [10/100], Loss: 0.4627\n", + "Epoch [20/100], Loss: 0.4105\n", + "Epoch [30/100], Loss: 0.3721\n", + "Epoch [40/100], Loss: 0.3424\n", + "Epoch [50/100], Loss: 0.3186\n", + "Epoch [60/100], Loss: 0.2990\n", + "Epoch [70/100], Loss: 0.2825\n", + "Epoch [80/100], Loss: 0.2683\n", + "Epoch [90/100], Loss: 0.2560\n", + "Epoch [100/100], Loss: 0.2452\n" + ] + } + ], + "source": [ + "num_epochs=100\n", + "learning_rate=0.01\n", + "\n", + "\"\"\"Train the model with given parameters.\"\"\"\n", + "criterion = nn.BCELoss()\n", + "optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)\n", + "\n", + "for epoch in range(num_epochs):\n", + " # Forward pass\n", + " y_pred = model(X_train)\n", + " loss = criterion(y_pred, y_train)\n", + " \n", + " # Backward pass and optimization\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + " \n", + " # Log progress\n", + " if (epoch + 1) % 10 == 0:\n", + " print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')\n" + ] + }, + { + "cell_type": "markdown", + "id": "1a59248d", + "metadata": {}, + "source": [ + "### Evaluate model performance on test set" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "eeddd812", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Test Accuracy: 0.8947\n" + ] + } + ], + "source": [ + "with torch.no_grad():\n", + " y_pred = model(X_test)\n", + " y_pred_classes = y_pred.round() # Values 𝑥 ≥ 0.5 are rounded to 1, else 0\n", + " accuracy = y_pred_classes.eq(y_test).sum() / float(y_test.shape[0])\n", + " print(f'\\nTest Accuracy: {accuracy:.4f}')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1dc4fcd3", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/lectures/07_binary_classification_n_to_1/0_predict_breast_cancer_B_modularized.ipynb b/lectures/07_binary_classification_n_to_1/0_predict_breast_cancer_B_modularized.ipynb new file mode 100644 index 0000000..4c0582b --- /dev/null +++ b/lectures/07_binary_classification_n_to_1/0_predict_breast_cancer_B_modularized.ipynb @@ -0,0 +1,146 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "id": "53af081c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training model...\n", + "Epoch [10/100], Loss: 0.6247\n", + "Epoch [20/100], Loss: 0.4940\n", + "Epoch [30/100], Loss: 0.4156\n", + "Epoch [40/100], Loss: 0.3641\n", + "Epoch [50/100], Loss: 0.3277\n", + "Epoch [60/100], Loss: 0.3005\n", + "Epoch [70/100], Loss: 0.2794\n", + "Epoch [80/100], Loss: 0.2624\n", + "Epoch [90/100], Loss: 0.2483\n", + "Epoch [100/100], Loss: 0.2364\n", + "\n", + "Test Accuracy: 0.9211\n" + ] + } + ], + "source": [ + "import torch\n", + "import torch.nn as nn\n", + "import numpy as np\n", + "from sklearn.datasets import load_breast_cancer\n", + "from sklearn.preprocessing import StandardScaler\n", + "from sklearn.model_selection import train_test_split\n", + "\n", + "def prepare_data():\n", + " \"\"\"Load and preprocess breast cancer dataset.\"\"\"\n", + " # Load dataset\n", + " data = load_breast_cancer()\n", + " X, y = data.data, data.target\n", + " \n", + " # Split dataset\n", + " X_train, X_test, y_train, y_test = train_test_split(\n", + " X, y, test_size=0.2, random_state=1234\n", + " )\n", + " \n", + " # Scale features\n", + " scaler = StandardScaler()\n", + " X_train = scaler.fit_transform(X_train)\n", + " X_test = scaler.transform(X_test)\n", + " \n", + " # Convert to PyTorch tensors\n", + " X_train = torch.from_numpy(X_train.astype(np.float32))\n", + " X_test = torch.from_numpy(X_test.astype(np.float32))\n", + " y_train = torch.from_numpy(y_train.astype(np.float32)).view(-1, 1)\n", + " y_test = torch.from_numpy(y_test.astype(np.float32)).view(-1, 1)\n", + " \n", + " return X_train, X_test, y_train, y_test\n", + "\n", + "class BinaryClassifier(nn.Module):\n", + " \"\"\"Simple neural network for binary classification.\"\"\"\n", + " def __init__(self, input_features):\n", + " super(BinaryClassifier, self).__init__()\n", + " self.linear = nn.Linear(input_features, 1)\n", + " \n", + " def forward(self, x):\n", + " return torch.sigmoid(self.linear(x))\n", + "\n", + "def train_model(model, X_train, y_train, num_epochs=100, learning_rate=0.01):\n", + " \"\"\"Train the model with given parameters.\"\"\"\n", + " criterion = nn.BCELoss()\n", + " optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)\n", + " \n", + " for epoch in range(num_epochs):\n", + " # Forward pass\n", + " y_pred = model(X_train)\n", + " loss = criterion(y_pred, y_train)\n", + " \n", + " # Backward pass and optimization\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + " \n", + " # Log progress\n", + " if (epoch + 1) % 10 == 0:\n", + " print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')\n", + "\n", + "def evaluate_model(model, X_test, y_test):\n", + " \"\"\"Evaluate model performance on test set.\"\"\"\n", + " with torch.no_grad():\n", + " y_pred = model(X_test)\n", + " y_pred_classes = y_pred.round()\n", + " accuracy = y_pred_classes.eq(y_test).sum() / float(y_test.shape[0])\n", + " return accuracy.item()\n", + "\n", + "def main():\n", + " # Prepare data\n", + " X_train, X_test, y_train, y_test = prepare_data()\n", + " \n", + " # Initialize model\n", + " n_features = X_train.shape[1]\n", + " model = BinaryClassifier(n_features)\n", + " \n", + " # Train model\n", + " print(\"Training model...\")\n", + " train_model(model, X_train, y_train)\n", + " \n", + " # Evaluate model\n", + " accuracy = evaluate_model(model, X_test, y_test)\n", + " print(f'\\nTest Accuracy: {accuracy:.4f}')\n", + "\n", + "main()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "76d4d576", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/lectures/07_binary_classification_n_to_1/1_breast_cancer_F1.ipynb b/lectures/07_binary_classification_n_to_1/1_breast_cancer_F1.ipynb new file mode 100644 index 0000000..39c162e --- /dev/null +++ b/lectures/07_binary_classification_n_to_1/1_breast_cancer_F1.ipynb @@ -0,0 +1,407 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "87585727", + "metadata": {}, + "source": [ + "## Confusion matrix metrics for binary classification in PyTorch" + ] + }, + { + "cell_type": "markdown", + "id": "4add68ec", + "metadata": {}, + "source": [ + "### 1. Binary Confusion Matrix Calculation" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "8db4add7", + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "\n", + "def get_binary_confusion_matrix(y_true, y_pred):\n", + " TP = ((y_true == 1) & (y_pred == 1)).sum().item()\n", + " TN = ((y_true == 0) & (y_pred == 0)).sum().item()\n", + " FP = ((y_true == 0) & (y_pred == 1)).sum().item()\n", + " FN = ((y_true == 1) & (y_pred == 0)).sum().item()\n", + "\n", + " return TP, TN, FP, FN\n" + ] + }, + { + "cell_type": "markdown", + "id": "98cbb299", + "metadata": {}, + "source": [ + "### 2. Binary Metrics Calculation" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "d346ba5c", + "metadata": {}, + "outputs": [], + "source": [ + "def compute_binary_metrics(TP, TN, FP, FN):\n", + " accuracy = (TP + TN) / (TP + TN + FP + FN + 1e-8)\n", + " precision = TP / (TP + FP + 1e-8)\n", + " recall = TP / (TP + FN + 1e-8)\n", + " f1 = 2 * precision * recall / (precision + recall + 1e-8)\n", + "\n", + " return {\n", + " 'accuracy': accuracy,\n", + " 'precision': precision,\n", + " 'recall': recall,\n", + " 'f1': f1\n", + " }\n" + ] + }, + { + "cell_type": "markdown", + "id": "d1ce8331", + "metadata": {}, + "source": [ + "### 3. Example Usage" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "0c0b8e8e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Confusion Matrix:\n", + "TP=4, TN=4, FP=1, FN=1\n", + "Metrics: {'accuracy': 0.7999999992, 'precision': 0.7999999984, 'recall': 0.7999999984, 'f1': 0.7999999934}\n" + ] + } + ], + "source": [ + "# Example binary predictions (0 or 1)\n", + "y_true = torch.tensor([1, 1, 1, 1, 1, 0, 0, 0, 0, 0])\n", + "y_pred = torch.tensor([1, 1, 1, 1, 0, 0, 0, 0, 0, 1])\n", + "\n", + "TP, TN, FP, FN = get_binary_confusion_matrix(y_true, y_pred)\n", + "metrics = compute_binary_metrics(TP, TN, FP, FN)\n", + "\n", + "print(\"Confusion Matrix:\")\n", + "print(f\"TP={TP}, TN={TN}, FP={FP}, FN={FN}\")\n", + "print(\"Metrics:\", metrics)\n" + ] + }, + { + "cell_type": "markdown", + "id": "9d687a14", + "metadata": {}, + "source": [ + "### 4. Use sklearn.metrics" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "b0aeda3d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[4 1]\n", + " [1 4]]\n" + ] + } + ], + "source": [ + "from sklearn.metrics import confusion_matrix\n", + "\n", + "y_true = [1, 1, 1, 1, 1, 0, 0, 0, 0, 0]\n", + "y_pred = [1, 1, 1, 1, 0, 0, 0, 0, 0, 1]\n", + "\n", + "cm = confusion_matrix(y_true, y_pred)\n", + "print(cm)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "738b8db5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Precision: 0.8\n", + "Recall: 0.8\n" + ] + } + ], + "source": [ + "from sklearn.metrics import precision_score, recall_score\n", + "\n", + "y_true = [1, 1, 1, 1, 1, 0, 0, 0, 0, 0]\n", + "y_pred = [1, 1, 1, 1, 0, 0, 0, 0, 0, 1]\n", + "\n", + "precision = precision_score(y_true, y_pred)\n", + "recall = recall_score(y_true, y_pred)\n", + "\n", + "print(\"Precision:\", precision)\n", + "print(\"Recall:\", recall)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "cd857229", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "F1 Score: 0.8\n" + ] + } + ], + "source": [ + "from sklearn.metrics import f1_score\n", + "\n", + "y_true = [1, 1, 1, 1, 1, 0, 0, 0, 0, 0]\n", + "y_pred = [1, 1, 1, 1, 0, 0, 0, 0, 0, 1]\n", + "\n", + "f1 = f1_score(y_true, y_pred)\n", + "print(\"F1 Score:\", f1)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "ba41a021", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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s8ICroslkxFJWrBggUaMGKHf/e53KigoUNu2bbVmzRpnpaKoqEhDhw7V8ePHFRISoo4dO2rGjBmXvVZcXJwGDx6sXr166ccff9SECROcy3t/LTExUVOmTFHdunXVunVrl88mT56sW265RUlJSTp8+LCqVq2qli1b6rnnnvPq147S0WXwa2aHAHjNPa0a6vT2N8wOA6XsYmXEkzaNF4MpRTbDMGiel6GcnByFhobK3mSgbBX8zQ4HKBX8kcSNLCcnRzWrhyo7O1shISGldo/Q0FDdOnypKtirXPd1ivLzdHjW70s1Vm8w/aFnAADAt1m2TQMAgK9jNQ0AADCVr6ymoU0DAABMRWUEAACL8vOzyc/v+ssbhgfnliWSEQAALIo2DQAAQBmgMgIAgEWxmgYAAJjKV9o0JCMAAFiUr1RGmDMCAABMRWUEAACL8pXKCMkIAAAW5StzRmjTAAAAU1EZAQDAomzysE2j8lEaIRkBAMCiaNMAAACUASojAABYFKtpAACAqWjTAAAAlAGSEQAALOpSm8aTzR2zZ89W06ZNFRISopCQEMXGxurDDz+86jn//Oc/1ahRIwUEBKhJkyZas2aN218nyQgAABZ1qU3jyeaO2rVra9q0afriiy+0Y8cO3Xffferatau+/vrryx6/detW9e7dW/3799euXbvUrVs3devWTXv27HHrviQjAABYVFlXRrp06aIHH3xQDRo0UMOGDTVlyhQFBQXp008/vezxr732mjp27Kinn35aMTExmjx5slq2bKk33njDrfuSjAAAcIPLyclx2fLz8695TlFRkRYvXqy8vDzFxsZe9pht27YpPj7eZSwhIUHbtm1zKz6SEQAArMrTFs1/CyORkZEKDQ11bklJSVe85e7duxUUFCS73a7Bgwdr+fLlaty48WWPPXnypGrWrOkyVrNmTZ08edKtL5OlvQAAWJS3njNy7NgxhYSEOMftdvsVz4mOjlZGRoays7O1dOlS9e3bVxs3brxiQuINJCMAANzgLq2OKQl/f39FRUVJklq1aqXt27frtdde09y5c4sdGxYWplOnTrmMnTp1SmFhYW7FR5sGAACLKuvVNJfjcDiuOMckNjZW69evdxlLS0u74hyTK6EyAgCARZX14+DHjRunTp06qU6dOjp79qwWLVqk9PR0rV27VpLUp08fRUREOOecjBgxQu3a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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "from sklearn.metrics import ConfusionMatrixDisplay\n", + "\n", + "disp = ConfusionMatrixDisplay.from_predictions(\n", + " y_true, y_pred, display_labels=['Positive', 'Negtive'], cmap='Blues'\n", + ")\n", + "plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "74e76178", + "metadata": {}, + "source": [ + "## Breast cancer with F1 scores" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "53af081c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training model...\n", + "Epoch [10/100], Loss: 0.6216\n", + "Epoch [20/100], Loss: 0.5017\n", + "Epoch [30/100], Loss: 0.4293\n", + "Epoch [40/100], Loss: 0.3805\n", + "Epoch [50/100], Loss: 0.3452\n", + "Epoch [60/100], Loss: 0.3181\n", + "Epoch [70/100], Loss: 0.2966\n", + "Epoch [80/100], Loss: 0.2789\n", + "Epoch [90/100], Loss: 0.2641\n", + "Epoch [100/100], Loss: 0.2515\n", + "\n", + "Test Accuracy: 0.9035\n", + "\n", + "Calculating additional metrics...\n", + "\n", + "Confusion Matrix:\n", + "TP: 65, TN: 38, FP: 7, FN: 4\n", + "Precision: 0.9028\n", + "Recall: 0.9420\n", + "F1 Score: 0.9220\n" + ] + } + ], + "source": [ + "import torch\n", + "import torch.nn as nn\n", + "import numpy as np\n", + "from sklearn.datasets import load_breast_cancer\n", + "from sklearn.preprocessing import StandardScaler\n", + "from sklearn.model_selection import train_test_split\n", + "\n", + "def prepare_data():\n", + " \"\"\"Load and preprocess breast cancer dataset.\"\"\"\n", + " # Load dataset\n", + " data = load_breast_cancer()\n", + " X, y = data.data, data.target\n", + " \n", + " # Split dataset\n", + " X_train, X_test, y_train, y_test = train_test_split(\n", + " X, y, test_size=0.2, random_state=1234\n", + " )\n", + " \n", + " # Scale features\n", + " scaler = StandardScaler()\n", + " X_train = scaler.fit_transform(X_train)\n", + " X_test = scaler.transform(X_test)\n", + " \n", + " # Convert to PyTorch tensors\n", + " X_train = torch.from_numpy(X_train.astype(np.float32))\n", + " X_test = torch.from_numpy(X_test.astype(np.float32))\n", + " y_train = torch.from_numpy(y_train.astype(np.float32)).view(-1, 1)\n", + " y_test = torch.from_numpy(y_test.astype(np.float32)).view(-1, 1)\n", + " \n", + " return X_train, X_test, y_train, y_test\n", + "\n", + "class BinaryClassifier(nn.Module):\n", + " \"\"\"Simple neural network for binary classification.\"\"\"\n", + " def __init__(self, input_features):\n", + " super(BinaryClassifier, self).__init__()\n", + " self.linear = nn.Linear(input_features, 1)\n", + " \n", + " def forward(self, x):\n", + " return torch.sigmoid(self.linear(x))\n", + "\n", + "def train_model(model, X_train, y_train, num_epochs=100, learning_rate=0.01):\n", + " \"\"\"Train the model with given parameters.\"\"\"\n", + " criterion = nn.BCELoss()\n", + " optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)\n", + " \n", + " for epoch in range(num_epochs):\n", + " # Forward pass\n", + " y_pred = model(X_train)\n", + " loss = criterion(y_pred, y_train)\n", + " \n", + " # Backward pass and optimization\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + " \n", + " # Log progress\n", + " if (epoch + 1) % 10 == 0:\n", + " print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')\n", + "\n", + "def evaluate_model(model, X_test, y_test):\n", + " \"\"\"Evaluate model performance on test set.\"\"\"\n", + " with torch.no_grad():\n", + " y_pred = model(X_test)\n", + " y_pred_classes = y_pred.round()\n", + " accuracy = y_pred_classes.eq(y_test).sum() / float(y_test.shape[0])\n", + " return accuracy.item()\n", + "\n", + "def calculate_metrics(model, X_test, y_test):\n", + " \"\"\"Calculate confusion matrix and F1 score for test set.\"\"\"\n", + " with torch.no_grad():\n", + " # Get predictions\n", + " y_pred = model(X_test)\n", + " y_pred_classes = y_pred.round().squeeze() # Convert to binary (0 or 1)\n", + " y_true = y_test.squeeze() # True labels\n", + " \n", + " # Calculate confusion matrix\n", + " TP = torch.sum((y_pred_classes == 1) & (y_true == 1)).item()\n", + " TN = torch.sum((y_pred_classes == 0) & (y_true == 0)).item()\n", + " FP = torch.sum((y_pred_classes == 1) & (y_true == 0)).item()\n", + " FN = torch.sum((y_pred_classes == 0) & (y_true == 1)).item()\n", + " \n", + " # Calculate precision, recall, and F1 score\n", + " precision = TP / (TP + FP) if (TP + FP) > 0 else 0\n", + " recall = TP / (TP + FN) if (TP + FN) > 0 else 0\n", + " f1 = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0\n", + " \n", + " # Print results\n", + " print(\"\\nConfusion Matrix:\")\n", + " print(f\"TP: {TP}, TN: {TN}, FP: {FP}, FN: {FN}\")\n", + " print(f\"Precision: {precision:.4f}\")\n", + " print(f\"Recall: {recall:.4f}\")\n", + " print(f\"F1 Score: {f1:.4f}\")\n", + " \n", + " return TP, TN, FP, FN, f1\n", + "\n", + "def main():\n", + " # Prepare data\n", + " X_train, X_test, y_train, y_test = prepare_data()\n", + " \n", + " # Initialize model\n", + " n_features = X_train.shape[1]\n", + " model = BinaryClassifier(n_features)\n", + " \n", + " # Train model\n", + " print(\"Training model...\")\n", + " train_model(model, X_train, y_train)\n", + " \n", + " # Evaluate model\n", + " accuracy = evaluate_model(model, X_test, y_test)\n", + " print(f'\\nTest Accuracy: {accuracy:.4f}')\n", + " \n", + " # Calculate additional metrics\n", + " print(\"\\nCalculating additional metrics...\")\n", + " calculate_metrics(model, X_test, y_test)\n", + "\n", + "if __name__ == \"__main__\":\n", + " main()" + ] + }, + { + "cell_type": "markdown", + "id": "58ae6a68", + "metadata": {}, + "source": [ + "## Homework: replace Breast cancer conusioin matrix and F1 scores with sklean.metrcs" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/lectures/07_binary_classification_n_to_1/1_breast_cancer_F1.pptx b/lectures/07_binary_classification_n_to_1/1_breast_cancer_F1.pptx new file mode 100644 index 0000000..708e9e0 Binary files /dev/null and b/lectures/07_binary_classification_n_to_1/1_breast_cancer_F1.pptx differ diff --git a/lectures/07_binary_classification_n_to_1/2_DataLoader_wine.pptx b/lectures/07_binary_classification_n_to_1/2_DataLoader_wine.pptx new file mode 100644 index 0000000..2f9fef6 Binary files /dev/null and b/lectures/07_binary_classification_n_to_1/2_DataLoader_wine.pptx differ diff --git a/lectures/07_binary_classification_n_to_1/2_dataloader_wine.ipynb b/lectures/07_binary_classification_n_to_1/2_dataloader_wine.ipynb new file mode 100644 index 0000000..5695a88 --- /dev/null +++ b/lectures/07_binary_classification_n_to_1/2_dataloader_wine.ipynb @@ -0,0 +1,209 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 4, + "id": "52950b67", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "First sample - Features: tensor([1.4230e+01, 1.7100e+00, 2.4300e+00, 1.5600e+01, 1.2700e+02, 2.8000e+00,\n", + " 3.0600e+00, 2.8000e-01, 2.2900e+00, 5.6400e+00, 1.0400e+00, 3.9200e+00,\n", + " 1.0650e+03]), Label: tensor([1.])\n" + ] + } + ], + "source": [ + "import torch\n", + "import torchvision\n", + "from torch.utils.data import Dataset, DataLoader\n", + "import numpy as np\n", + "import math\n", + "\n", + "# Custom Dataset class for Wine dataset\n", + "class WineDataset(Dataset):\n", + " def __init__(self, data_path='data/wine.csv'):\n", + " \"\"\"\n", + " Initialize the dataset by loading wine data from a CSV file.\n", + " \n", + " Args:\n", + " data_path (str): Path to the wine CSV file\n", + " \"\"\"\n", + " # Load data from CSV, skipping header row\n", + " xy = np.loadtxt(data_path, delimiter=',', dtype=np.float32, skiprows=1)\n", + " self.n_samples = xy.shape[0]\n", + " \n", + " # Split into features (all columns except first) and labels (first column)\n", + " self.x_data = torch.from_numpy(xy[:, 1:]) # Shape: [n_samples, n_features]\n", + " self.y_data = torch.from_numpy(xy[:, [0]]) # Shape: [n_samples, 1]\n", + "\n", + " def __getitem__(self, index):\n", + " \"\"\"\n", + " Enable indexing to retrieve a specific sample.\n", + " \n", + " Args:\n", + " index (int): Index of the sample to retrieve\n", + " \n", + " Returns:\n", + " tuple: (features, label) for the specified index\n", + " \"\"\"\n", + " return self.x_data[index], self.y_data[index]\n", + "\n", + " def __len__(self):\n", + " \"\"\"\n", + " Return the total number of samples in the dataset.\n", + " \n", + " Returns:\n", + " int: Number of samples\n", + " \"\"\"\n", + " return self.n_samples\n", + "\n", + "# Create dataset instance\n", + "dataset = WineDataset()\n", + "\n", + "# Access and print first sample\n", + "features, labels = dataset[0]\n", + "print(f\"First sample - Features: {features}, Label: {labels}\")\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "5448f749", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample batch - Features: torch.Size([4, 13]), Labels: torch.Size([4, 1])\n" + ] + } + ], + "source": [ + "\"\"\"\n", + "Create a DataLoader for the wine dataset.\n", + "\n", + "Args:\n", + " dataset (Dataset): The dataset to load\n", + " batch_size (int): Number of samples per batch\n", + " shuffle (bool): Whether to shuffle the data\n", + " num_workers (int): Number of subprocesses for data loading\n", + " \n", + "Returns:\n", + " DataLoader: Configured DataLoader instance\n", + "\"\"\"\n", + "train_loader = DataLoader(dataset, batch_size=4, shuffle=True, num_workers=0)\n", + "\n", + "# Examine one batch\n", + "dataiter = iter(train_loader)\n", + "features, labels = next(dataiter)\n", + "print(f\"Sample batch - Features: {features.shape}, Labels: {labels.shape}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0e122c46", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total samples: 178, Iterations per epoch: 45\n", + "Epoch: 1/2, Step 5/45 | Inputs torch.Size([4, 13]) | Labels torch.Size([4, 1])\n", + "Epoch: 1/2, Step 10/45 | Inputs torch.Size([4, 13]) | Labels torch.Size([4, 1])\n", + "Epoch: 1/2, Step 15/45 | Inputs torch.Size([4, 13]) | Labels torch.Size([4, 1])\n", + "Epoch: 1/2, Step 20/45 | Inputs torch.Size([4, 13]) | Labels torch.Size([4, 1])\n", + "Epoch: 1/2, Step 25/45 | Inputs torch.Size([4, 13]) | Labels torch.Size([4, 1])\n", + "Epoch: 1/2, Step 30/45 | Inputs torch.Size([4, 13]) | Labels torch.Size([4, 1])\n", + "Epoch: 1/2, Step 35/45 | Inputs torch.Size([4, 13]) | Labels torch.Size([4, 1])\n", + "Epoch: 1/2, Step 40/45 | Inputs torch.Size([4, 13]) | Labels torch.Size([4, 1])\n", + "Epoch: 1/2, Step 45/45 | Inputs torch.Size([2, 13]) | Labels torch.Size([2, 1])\n", + "Epoch: 2/2, Step 5/45 | Inputs torch.Size([4, 13]) | Labels torch.Size([4, 1])\n", + "Epoch: 2/2, Step 10/45 | Inputs torch.Size([4, 13]) | Labels torch.Size([4, 1])\n", + "Epoch: 2/2, Step 15/45 | Inputs torch.Size([4, 13]) | Labels torch.Size([4, 1])\n", + "Epoch: 2/2, Step 20/45 | Inputs torch.Size([4, 13]) | Labels torch.Size([4, 1])\n", + "Epoch: 2/2, Step 25/45 | Inputs torch.Size([4, 13]) | Labels torch.Size([4, 1])\n", + "Epoch: 2/2, Step 30/45 | Inputs torch.Size([4, 13]) | Labels torch.Size([4, 1])\n", + "Epoch: 2/2, Step 35/45 | Inputs torch.Size([4, 13]) | Labels torch.Size([4, 1])\n", + "Epoch: 2/2, Step 40/45 | Inputs torch.Size([4, 13]) | Labels torch.Size([4, 1])\n", + "Epoch: 2/2, Step 45/45 | Inputs torch.Size([2, 13]) | Labels torch.Size([2, 1])\n" + ] + } + ], + "source": [ + "# Training loop parameters\n", + "num_epochs = 2\n", + "total_samples = len(dataset)\n", + "n_iterations = math.ceil(total_samples / 4)\n", + "print(f\"Total samples: {total_samples}, Iterations per epoch: {n_iterations}\")\n", + "\n", + "# Dummy training loop\n", + "for epoch in range(num_epochs):\n", + " for i, (inputs, labels) in enumerate(train_loader):\n", + " # Training step\n", + " if (i + 1) % 5 == 0:\n", + " print(f'Epoch: {epoch+1}/{num_epochs}, Step {i+1}/{n_iterations} | '\n", + " f'Inputs {inputs.shape} | Labels {labels.shape}')" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "37095d28", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MNIST batch - Inputs: torch.Size([3, 1, 28, 28]), Targets: torch.Size([3])\n" + ] + } + ], + "source": [ + "# Example with MNIST dataset\n", + "train_dataset = torchvision.datasets.MNIST(root='./data',\n", + " train=True,\n", + " transform=torchvision.transforms.ToTensor(),\n", + " download=True)\n", + "\n", + "mnist_loader = DataLoader(dataset=train_dataset,\n", + " batch_size=3,\n", + " shuffle=True)\n", + "\n", + "# Examine MNIST batch\n", + "dataiter = iter(mnist_loader)\n", + "inputs, targets = next(dataiter)\n", + "print(f\"MNIST batch - Inputs: {inputs.shape}, Targets: {targets.shape}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/lectures/07_binary_classification_n_to_1/data/MNIST/raw/t10k-images-idx3-ubyte b/lectures/07_binary_classification_n_to_1/data/MNIST/raw/t10k-images-idx3-ubyte new file mode 100644 index 0000000..1170b2c Binary files /dev/null and b/lectures/07_binary_classification_n_to_1/data/MNIST/raw/t10k-images-idx3-ubyte differ diff --git a/lectures/07_binary_classification_n_to_1/data/MNIST/raw/t10k-images-idx3-ubyte.gz b/lectures/07_binary_classification_n_to_1/data/MNIST/raw/t10k-images-idx3-ubyte.gz new file mode 100644 index 0000000..5ace8ea Binary files /dev/null and b/lectures/07_binary_classification_n_to_1/data/MNIST/raw/t10k-images-idx3-ubyte.gz differ diff --git a/lectures/07_binary_classification_n_to_1/data/MNIST/raw/t10k-labels-idx1-ubyte b/lectures/07_binary_classification_n_to_1/data/MNIST/raw/t10k-labels-idx1-ubyte new file mode 100644 index 0000000..d1c3a97 Binary files /dev/null and b/lectures/07_binary_classification_n_to_1/data/MNIST/raw/t10k-labels-idx1-ubyte differ diff --git a/lectures/07_binary_classification_n_to_1/data/MNIST/raw/t10k-labels-idx1-ubyte.gz b/lectures/07_binary_classification_n_to_1/data/MNIST/raw/t10k-labels-idx1-ubyte.gz new file mode 100644 index 0000000..a7e1415 Binary files /dev/null and b/lectures/07_binary_classification_n_to_1/data/MNIST/raw/t10k-labels-idx1-ubyte.gz differ diff --git a/lectures/07_binary_classification_n_to_1/data/MNIST/raw/train-images-idx3-ubyte b/lectures/07_binary_classification_n_to_1/data/MNIST/raw/train-images-idx3-ubyte new file mode 100644 index 0000000..bbce276 Binary files /dev/null and b/lectures/07_binary_classification_n_to_1/data/MNIST/raw/train-images-idx3-ubyte differ diff --git a/lectures/07_binary_classification_n_to_1/data/MNIST/raw/train-images-idx3-ubyte.gz b/lectures/07_binary_classification_n_to_1/data/MNIST/raw/train-images-idx3-ubyte.gz new file mode 100644 index 0000000..b50e4b6 Binary files /dev/null and b/lectures/07_binary_classification_n_to_1/data/MNIST/raw/train-images-idx3-ubyte.gz differ diff --git a/lectures/07_binary_classification_n_to_1/data/MNIST/raw/train-labels-idx1-ubyte b/lectures/07_binary_classification_n_to_1/data/MNIST/raw/train-labels-idx1-ubyte new file mode 100644 index 0000000..d6b4c5d Binary files /dev/null and b/lectures/07_binary_classification_n_to_1/data/MNIST/raw/train-labels-idx1-ubyte differ diff --git a/lectures/07_binary_classification_n_to_1/data/MNIST/raw/train-labels-idx1-ubyte.gz b/lectures/07_binary_classification_n_to_1/data/MNIST/raw/train-labels-idx1-ubyte.gz new file mode 100644 index 0000000..707a576 Binary files /dev/null and b/lectures/07_binary_classification_n_to_1/data/MNIST/raw/train-labels-idx1-ubyte.gz differ diff --git a/lectures/08_multiclass_classification_n_to_n_2hidden/0_ML_concepts_multiclass.pptx b/lectures/08_multiclass_classification_n_to_n_2hidden/0_ML_concepts_multiclass.pptx index bc7d2ac..95108a1 100644 Binary files a/lectures/08_multiclass_classification_n_to_n_2hidden/0_ML_concepts_multiclass.pptx and b/lectures/08_multiclass_classification_n_to_n_2hidden/0_ML_concepts_multiclass.pptx differ diff --git a/lectures/08_multiclass_classification_n_to_n_2hidden/multiclass_iris.ipynb b/lectures/08_multiclass_classification_n_to_n_2hidden/multiclass_iris.ipynb index e69de29..743e5fb 100644 --- a/lectures/08_multiclass_classification_n_to_n_2hidden/multiclass_iris.ipynb +++ b/lectures/08_multiclass_classification_n_to_n_2hidden/multiclass_iris.ipynb @@ -0,0 +1,207 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "id": "c694345f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch [20/100], Loss: 0.7266\n", + "Epoch [40/100], Loss: 0.6070\n", + "Epoch [60/100], Loss: 0.5695\n", + "Epoch [80/100], Loss: 0.5610\n", + "Epoch [100/100], Loss: 0.5574\n", + "\n", + "Model Architecture:\n", + "MultiClassModel(\n", + " (layer1): Linear(in_features=4, out_features=64, bias=True)\n", + " (relu1): ReLU()\n", + " (layer2): Linear(in_features=64, out_features=32, bias=True)\n", + " (relu2): ReLU()\n", + " (output): Linear(in_features=32, out_features=3, bias=True)\n", + " (softmax): Softmax(dim=1)\n", + ")\n" + ] + } + ], + "source": [ + "import torch\n", + "import torch.nn as nn\n", + "import torch.optim as optim\n", + "from sklearn.datasets import load_iris\n", + "from sklearn.model_selection import train_test_split\n", + "from sklearn.preprocessing import StandardScaler\n", + "import numpy as np\n", + "\n", + "# Define the neural network model\n", + "class MultiClassModel(nn.Module):\n", + " def __init__(self, input_dim, num_classes):\n", + " super(MultiClassModel, self).__init__()\n", + " self.layer1 = nn.Linear(input_dim, 64)\n", + " self.relu1 = nn.ReLU()\n", + " self.layer2 = nn.Linear(64, 32)\n", + " self.relu2 = nn.ReLU()\n", + " self.output = nn.Linear(32, num_classes)\n", + " self.softmax = nn.Softmax(dim=1)\n", + " \n", + " def forward(self, x):\n", + " x = self.relu1(self.layer1(x))\n", + " x = self.relu2(self.layer2(x))\n", + " x = self.softmax(self.output(x))\n", + " return x\n", + "\n", + "# Load and preprocess Iris dataset\n", + "iris = load_iris()\n", + "X = iris.data\n", + "y = iris.target\n", + "\n", + "# Standardize features\n", + "scaler = StandardScaler()\n", + "X = scaler.fit_transform(X)\n", + "\n", + "# Convert to PyTorch tensors\n", + "X = torch.FloatTensor(X)\n", + "# nn.CrossEntropyLoss expects the target tensor to have the torch.int64 (long) data type\n", + "y = torch.tensor(y, dtype=torch.int64)\n", + "\n", + "# Split dataset\n", + "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)\n", + "\n", + "# Model parameters\n", + "input_dim = X.shape[1]\n", + "num_classes = len(np.unique(y))\n", + "\n", + "# Initialize model, loss, and optimizer\n", + "model = MultiClassModel(input_dim, num_classes)\n", + "criterion = nn.CrossEntropyLoss()\n", + "optimizer = optim.Adam(model.parameters(), lr=0.001)\n", + "\n", + "# Training loop\n", + "num_epochs = 100\n", + "batch_size = 32\n", + "n_batches = len(X_train) // batch_size\n", + "\n", + "for epoch in range(num_epochs):\n", + " model.train()\n", + " for i in range(0, len(X_train), batch_size):\n", + " batch_X = X_train[i:i+batch_size]\n", + " batch_y = y_train[i:i+batch_size]\n", + " \n", + " # Forward pass\n", + " outputs = model(batch_X)\n", + " loss = criterion(outputs, batch_y)\n", + " \n", + " # Backward pass and optimization\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + " \n", + " # Print progress every 20 epochs\n", + " if (epoch + 1) % 20 == 0:\n", + " print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')\n", + "\n", + "# Print model architecture\n", + "print('\\nModel Architecture:')\n", + "print(model)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3d325c03", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Test Accuracy: 1.0000\n" + ] + } + ], + "source": [ + "# Evaluate model\n", + "model.eval()\n", + "with torch.no_grad():\n", + " test_outputs = model(X_test)\n", + " _, predicted = torch.max(test_outputs, 1)\n", + " accuracy = (predicted == y_test).float().mean()\n", + " print(f'\\nTest Accuracy: {accuracy:.4f}')" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "c15bb757", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The first prediction [0.000702839985024184, 0.9973917007446289, 0.001905460492707789]\n" + ] + } + ], + "source": [ + "print(\"The first prediction\", test_outputs[0].tolist())" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "91588c01", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([1, 0, 2, 1, 1, 0, 1, 2, 1, 1, 2, 0, 0, 0, 0, 1, 2, 1, 1, 2, 0, 2, 0, 2,\n", + " 2, 2, 2, 2, 0, 0])" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "predicted" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3811df4a", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/lectures/09_multiclass_classification_cnn_n_to_n/01_MNIST.ipynb b/lectures/09_multiclass_classification_cnn_n_to_n/01_MNIST.ipynb new file mode 100644 index 0000000..84dc3e1 --- /dev/null +++ b/lectures/09_multiclass_classification_cnn_n_to_n/01_MNIST.ipynb @@ -0,0 +1,280 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "8ce2d850", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1, Loss: 0.1675, Accuracy: 95.05%\n", + "Epoch 2, Loss: 0.0710, Accuracy: 97.94%\n", + "Epoch 3, Loss: 0.0518, Accuracy: 98.42%\n", + "Epoch 4, Loss: 0.0462, Accuracy: 98.64%\n", + "Epoch 5, Loss: 0.0384, Accuracy: 98.87%\n", + "Epoch 6, Loss: 0.0336, Accuracy: 99.00%\n", + "Epoch 7, Loss: 0.0323, Accuracy: 99.06%\n", + "Epoch 8, Loss: 0.0260, Accuracy: 99.22%\n", + "Epoch 9, Loss: 0.0254, Accuracy: 99.19%\n", + "Epoch 10, Loss: 0.0226, Accuracy: 99.27%\n", + "\n", + "Test Accuracy: 99.34%\n", + "Training complete. Plots saved as 'mnist_training_metrics.png', 'mnist_confusion_matrix.png', and 'mnist_sample_predictions.png'\n" + ] + } + ], + "source": [ + "import torch # PyTorch library for tensor operations and deep learning\n", + "import torch.nn as nn # Neural network modules and layers\n", + "import torch.nn.functional as F # Functional operations like activations\n", + "import torch.optim as optim # Optimization algorithms (e.g., Adam)\n", + "import torchvision # Datasets, models, and transforms for computer vision\n", + "import torchvision.transforms as transforms # Image preprocessing transformations\n", + "import matplotlib.pyplot as plt # Plotting library for visualizations\n", + "import numpy as np # Numerical computations for array operations\n", + "from sklearn.metrics import confusion_matrix # For computing confusion matrix\n", + "import seaborn as sns # Visualization library for heatmap plots\n", + "\n", + "# Set random seed for reproducibility across runs\n", + "torch.manual_seed(42)\n", + "\n", + "# Define data transformations for preprocessing MNIST dataset\n", + "# - ToTensor: Converts PIL images to PyTorch tensors (HWC to CHW format)\n", + "# - Normalize: Normalizes pixel values using MNIST dataset mean (0.1307) and std (0.3081)\n", + "transform = transforms.Compose([\n", + " transforms.ToTensor(),\n", + " transforms.Normalize((0.1307,), (0.3081,))\n", + "])\n", + "\n", + "# Load MNIST training dataset\n", + "# - root: Directory to store the dataset\n", + "# - train: True for training set\n", + "# - download: Download dataset if not already present\n", + "# - transform: Apply the defined transformations\n", + "trainset = torchvision.datasets.MNIST(root='./data', train=True,\n", + " download=True, transform=transform)\n", + "# Create DataLoader for training set\n", + "# - batch_size: 64 images per batch\n", + "# - shuffle: Randomly shuffle data for better training\n", + "# - num_workers: 2 subprocesses for faster data loading\n", + "trainloader = torch.utils.data.DataLoader(trainset, batch_size=64,\n", + " shuffle=True, num_workers=2)\n", + "\n", + "# Load MNIST test dataset\n", + "# - train: False for test set\n", + "# - Same transformations as training set for consistency\n", + "testset = torchvision.datasets.MNIST(root='./data', train=False,\n", + " download=True, transform=transform)\n", + "# Create DataLoader for test set\n", + "# - shuffle: False to maintain order during evaluation\n", + "testloader = torch.utils.data.DataLoader(testset, batch_size=64,\n", + " shuffle=False, num_workers=2)\n", + "\n", + "# Define class labels for MNIST (digits 0-9)\n", + "classes = tuple(str(i) for i in range(10))\n", + "\n", + "# Define CNN architecture\n", + "class Net(nn.Module):\n", + " def __init__(self):\n", + " super(Net, self).__init__()\n", + " # First convolutional layer\n", + " # - Input: 1 channel (grayscale)\n", + " # - Output: 32 channels\n", + " # - Kernel: 3x3, padding=1 to maintain spatial dimensions\n", + " self.conv1 = nn.Conv2d(1, 32, 5, padding=0)\n", + " # Second convolutional layer\n", + " # - Input: 32 channels\n", + " # - Output: 64 channels\n", + " # - Kernel: 3x3, padding=1\n", + " self.conv2 = nn.Conv2d(32, 64, 5, padding=0)\n", + " # Max pooling layer\n", + " # - Kernel: 2x2, stride=2 (reduces spatial dimensions by half)\n", + " self.pool = nn.MaxPool2d(2, 2)\n", + " # Batch normalization for first conv layer (32 channels)\n", + " self.bn1 = nn.BatchNorm2d(32)\n", + " # Batch normalization for second conv layer (64 channels)\n", + " self.bn2 = nn.BatchNorm2d(64)\n", + " # First fully connected layer\n", + " # - Input: 64*4*4 (after two pooling layers on 28x28 input)\n", + " # - Output: 128 units\n", + " self.fc1 = nn.Linear(64 * 4 * 4, 128)\n", + " # Second fully connected layer\n", + " # - Input: 128 units\n", + " # - Output: 10 units (one per class)\n", + " self.fc2 = nn.Linear(128, 10)\n", + " # Dropout layer with 50% probability to prevent overfitting\n", + " self.dropout = nn.Dropout(0.5)\n", + "\n", + " def forward(self, x):\n", + " # Forward pass through the network\n", + " # Conv1 -> BatchNorm -> ReLU -> MaxPool\n", + " x = self.pool(F.relu(self.bn1(self.conv1(x))))\n", + " # Conv2 -> BatchNorm -> ReLU -> MaxPool\n", + " x = self.pool(F.relu(self.bn2(self.conv2(x))))\n", + " # Flatten output for fully connected layers\n", + " # - Input size: 64 channels, 4x4 spatial dimensions\n", + " x = x.view(-1, 64 * 4 * 4)\n", + " # Fully connected layer 1 -> ReLU\n", + " x = F.relu(self.fc1(x))\n", + " # Apply dropout during training\n", + " x = self.dropout(x)\n", + " # Fully connected layer 2 for classification\n", + " x = self.fc2(x)\n", + " return x\n", + "\n", + "# Initialize model and move to appropriate device (GPU if available, else CPU)\n", + "device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n", + "model = Net().to(device)\n", + "\n", + "# Define loss function\n", + "# - CrossEntropyLoss: Combines log softmax and negative log likelihood loss\n", + "criterion = nn.CrossEntropyLoss()\n", + "\n", + "# Define optimizer\n", + "# - Adam optimizer with learning rate 0.001\n", + "optimizer = optim.Adam(model.parameters(), lr=0.001)\n", + "\n", + "# Training loop\n", + "num_epochs = 10 # Number of epochs to train\n", + "train_losses = [] # Store loss for each epoch\n", + "train_accuracies = [] # Store accuracy for each epoch\n", + "\n", + "for epoch in range(num_epochs):\n", + " model.train() # Set model to training mode\n", + " running_loss = 0.0 # Accumulate loss for the epoch\n", + " correct = 0 # Track correct predictions\n", + " total = 0 # Track total samples\n", + " for i, data in enumerate(trainloader, 0):\n", + " # Get inputs and labels, move to device\n", + " inputs, labels = data[0].to(device), data[1].to(device)\n", + " # Zero out gradients to prevent accumulation\n", + " optimizer.zero_grad()\n", + " # Forward pass through the model\n", + " outputs = model(inputs)\n", + " # Compute loss\n", + " loss = criterion(outputs, labels)\n", + " # Backward pass to compute gradients\n", + " loss.backward()\n", + " # Update model parameters\n", + " optimizer.step()\n", + " \n", + " # Update running loss\n", + " running_loss += loss.item()\n", + " # Calculate accuracy\n", + " _, predicted = torch.max(outputs.data, 1)\n", + " total += labels.size(0)\n", + " correct += (predicted == labels).sum().item()\n", + " \n", + " # Calculate and store epoch metrics\n", + " epoch_loss = running_loss / len(trainloader)\n", + " epoch_acc = 100 * correct / total\n", + " train_losses.append(epoch_loss)\n", + " train_accuracies.append(epoch_acc)\n", + " print(f\"Epoch {epoch + 1}, Loss: {epoch_loss:.4f}, Accuracy: {epoch_acc:.2f}%\")\n", + "\n", + "# Evaluate model on test set\n", + "model.eval() # Set model to evaluation mode\n", + "correct = 0\n", + "total = 0\n", + "all_preds = [] # Store predictions for confusion matrix\n", + "all_labels = [] # Store true labels\n", + "with torch.no_grad(): # Disable gradient computation for efficiency\n", + " for data in testloader:\n", + " images, labels = data[0].to(device), data[1].to(device)\n", + " outputs = model(images)\n", + " _, predicted = torch.max(outputs.data, 1)\n", + " total += labels.size(0)\n", + " correct += (predicted == labels).sum().item()\n", + " all_preds.extend(predicted.cpu().numpy())\n", + " all_labels.extend(labels.cpu().numpy())\n", + "\n", + "# Calculate and print test accuracy\n", + "test_accuracy = 100 * correct / total\n", + "print(f\"\\nTest Accuracy: {test_accuracy:.2f}%\")\n", + "\n", + "# Plot training metrics (loss and accuracy)\n", + "plt.figure(figsize=(12, 4))\n", + "\n", + "# Plot training loss\n", + "plt.subplot(1, 2, 1)\n", + "plt.plot(train_losses, label='Training Loss')\n", + "plt.title('Training Loss')\n", + "plt.xlabel('Epoch')\n", + "plt.ylabel('Loss')\n", + "plt.legend()\n", + "\n", + "# Plot training accuracy\n", + "plt.subplot(1, 2, 2)\n", + "plt.plot(train_accuracies, label='Training Accuracy')\n", + "plt.title('Training Accuracy')\n", + "plt.xlabel('Epoch')\n", + "plt.ylabel('Accuracy (%)')\n", + "plt.legend()\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig('mnist_training_metrics.png') # Save the plot\n", + "plt.close()\n", + "\n", + "# Plot confusion matrix to visualize class-wise performance\n", + "cm = confusion_matrix(all_labels, all_preds)\n", + "plt.figure(figsize=(10, 8))\n", + "sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',\n", + " xticklabels=classes, yticklabels=classes)\n", + "plt.title('Confusion Matrix')\n", + "plt.xlabel('Predicted')\n", + "plt.ylabel('True')\n", + "plt.savefig('mnist_confusion_matrix.png') # Save the plot\n", + "plt.close()\n", + "\n", + "# Function to unnormalize and display MNIST images\n", + "def imshow(img):\n", + " img = img * 0.3081 + 0.1307 # Unnormalize (reverse mean/std normalization)\n", + " npimg = img.numpy()\n", + " return npimg[0] # Return single channel for grayscale image\n", + "\n", + "# Display sample test images with predictions\n", + "dataiter = iter(testloader)\n", + "images, labels = next(dataiter)\n", + "images, labels = images[:8].to(device), labels[:8]\n", + "outputs = model(images)\n", + "_, predicted = torch.max(outputs, 1)\n", + "\n", + "plt.figure(figsize=(12, 6))\n", + "for i in range(8):\n", + " plt.subplot(2, 4, i + 1)\n", + " plt.imshow(imshow(images[i].cpu()), cmap='gray') # Display grayscale image\n", + " plt.title(f'Pred: {classes[predicted[i]]}\\nTrue: {classes[labels[i]]}')\n", + " plt.axis('off')\n", + "plt.savefig('mnist_sample_predictions.png') # Save the plot\n", + "plt.close()\n", + "\n", + "# Print completion message\n", + "print(\"Training complete. Plots saved as 'mnist_training_metrics.png', 'mnist_confusion_matrix.png', and 'mnist_sample_predictions.png'\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/lectures/09_multiclass_classification_cnn_n_to_n/02_CIFAR10.ipynb b/lectures/09_multiclass_classification_cnn_n_to_n/02_CIFAR10.ipynb new file mode 100644 index 0000000..91a4b0f --- /dev/null +++ b/lectures/09_multiclass_classification_cnn_n_to_n/02_CIFAR10.ipynb @@ -0,0 +1,273 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "46ca8277", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 170M/170M [00:07<00:00, 24.0MB/s] \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 1, Loss: 1.535, Accuracy: 44.58%\n", + "Epoch 2, Loss: 1.254, Accuracy: 54.91%\n", + "Epoch 3, Loss: 1.144, Accuracy: 59.47%\n", + "Epoch 4, Loss: 1.071, Accuracy: 62.25%\n", + "Epoch 5, Loss: 1.017, Accuracy: 64.26%\n", + "Epoch 6, Loss: 0.975, Accuracy: 65.75%\n", + "Epoch 7, Loss: 0.945, Accuracy: 67.02%\n", + "Epoch 8, Loss: 0.916, Accuracy: 68.16%\n", + "Epoch 9, Loss: 0.892, Accuracy: 68.86%\n", + "Epoch 10, Loss: 0.865, Accuracy: 70.02%\n", + "\n", + "Test Accuracy: 73.62%\n", + "Training complete. Plots saved as 'training_metrics.png', 'confusion_matrix.png', and 'sample_predictions.png'\n" + ] + } + ], + "source": [ + "import torch # PyTorch library for tensor computations and deep learning\n", + "import torch.nn as nn # Neural network modules\n", + "import torch.nn.functional as F # Functional interface for neural network operations\n", + "import torch.optim as optim # Optimization algorithms\n", + "import torchvision # Computer vision datasets and models\n", + "import torchvision.transforms as transforms # Image transformations\n", + "import matplotlib.pyplot as plt # Plotting library\n", + "import numpy as np # Numerical computations\n", + "from sklearn.metrics import confusion_matrix # For confusion matrix\n", + "import seaborn as sns # Visualization library for confusion matrix\n", + "\n", + "# Set random seed for reproducibility across runs\n", + "torch.manual_seed(42)\n", + "\n", + "# Define data transformations for preprocessing\n", + "# - RandomHorizontalFlip: Randomly flip images horizontally for data augmentation\n", + "# - RandomRotation: Randomly rotate images by up to 10 degrees for augmentation\n", + "# - ToTensor: Convert images to PyTorch tensors (HWC to CHW format)\n", + "# - Normalize: Normalize RGB channels with mean=0.5 and std=0.5\n", + "transform = transforms.Compose([\n", + " transforms.RandomHorizontalFlip(),\n", + " transforms.RandomRotation(10),\n", + " transforms.ToTensor(),\n", + " transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))\n", + "])\n", + "\n", + "# Load CIFAR-10 training dataset\n", + "# - root: Directory to store dataset\n", + "# - train: True for training set\n", + "# - download: Download dataset if not present\n", + "# - transform: Apply defined transformations\n", + "trainset = torchvision.datasets.CIFAR10(root='./data', train=True,\n", + " download=True, transform=transform)\n", + "# Create DataLoader for training set\n", + "# - batch_size: Number of images per batch (64)\n", + "# - shuffle: Randomly shuffle data for better training\n", + "# - num_workers: Number of subprocesses for data loading\n", + "trainloader = torch.utils.data.DataLoader(trainset, batch_size=64,\n", + " shuffle=True, num_workers=2)\n", + "\n", + "# Load CIFAR-10 test dataset\n", + "testset = torchvision.datasets.CIFAR10(root='./data', train=False,\n", + " download=True, transform=transform)\n", + "# Create DataLoader for test set\n", + "# - shuffle: False to maintain order for evaluation\n", + "testloader = torch.utils.data.DataLoader(testset, batch_size=64,\n", + " shuffle=False, num_workers=2)\n", + "\n", + "# Define class labels for CIFAR-10\n", + "classes = ('airplane', 'automobile', 'bird', 'cat', 'deer',\n", + " 'dog', 'frog', 'horse', 'ship', 'truck')\n", + "\n", + "# Define CNN architecture\n", + "class Net(nn.Module):\n", + " def __init__(self):\n", + " super(Net, self).__init__()\n", + " # First convolutional layer: 3 input channels (RGB), 32 output channels, 3x3 kernel\n", + " self.conv1 = nn.Conv2d(3, 32, 3, padding=1)\n", + " # Second convolutional layer: 32 input channels, 64 output channels, 3x3 kernel\n", + " self.conv2 = nn.Conv2d(32, 64, 3, padding=1)\n", + " # Max pooling layer: 2x2 kernel, stride 2\n", + " self.pool = nn.MaxPool2d(2, 2)\n", + " # Batch normalization for first conv layer\n", + " self.bn1 = nn.BatchNorm2d(32)\n", + " # Batch normalization for second conv layer\n", + " self.bn2 = nn.BatchNorm2d(64)\n", + " # First fully connected layer: Input size calculated from conv output (64*8*8), 512 units\n", + " self.fc1 = nn.Linear(64 * 8 * 8, 512)\n", + " # Second fully connected layer: 512 units to 10 output classes\n", + " self.fc2 = nn.Linear(512, 10)\n", + " # Dropout layer with 50% probability to prevent overfitting\n", + " self.dropout = nn.Dropout(0.5)\n", + "\n", + " def forward(self, x):\n", + " # Forward pass through the network\n", + " # Conv1 -> BatchNorm -> ReLU -> MaxPool\n", + " x = self.pool(F.relu(self.bn1(self.conv1(x))))\n", + " # Conv2 -> BatchNorm -> ReLU -> MaxPool\n", + " x = self.pool(F.relu(self.bn2(self.conv2(x))))\n", + " # Flatten the output for fully connected layers\n", + " x = x.view(-1, 64 * 8 * 8)\n", + " # Fully connected layer 1 -> ReLU\n", + " x = F.relu(self.fc1(x))\n", + " # Apply dropout\n", + " x = self.dropout(x)\n", + " # Final fully connected layer for classification\n", + " x = self.fc2(x)\n", + " return x\n", + "\n", + "# Initialize model and move to appropriate device (GPU if available, else CPU)\n", + "device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n", + "model = Net().to(device)\n", + "\n", + "# Define loss function (CrossEntropyLoss for multi-class classification)\n", + "criterion = nn.CrossEntropyLoss()\n", + "# Define optimizer (Adam with learning rate 0.001)\n", + "optimizer = optim.Adam(model.parameters(), lr=0.001)\n", + "\n", + "# Training loop\n", + "num_epochs = 10 # Number of training epochs\n", + "train_losses = [] # Store loss per epoch\n", + "train_accuracies = [] # Store accuracy per epoch\n", + "\n", + "for epoch in range(num_epochs):\n", + " model.train() # Set model to training mode\n", + " running_loss = 0.0 # Track total loss for epoch\n", + " correct = 0 # Track correct predictions\n", + " total = 0 # Track total samples\n", + " for i, data in enumerate(trainloader, 0):\n", + " # Get inputs and labels, move to device\n", + " inputs, labels = data[0].to(device), data[1].to(device)\n", + " # Zero the parameter gradients\n", + " optimizer.zero_grad()\n", + " # Forward pass\n", + " outputs = model(inputs)\n", + " # Compute loss\n", + " loss = criterion(outputs, labels)\n", + " # Backward pass and optimize\n", + " loss.backward()\n", + " optimizer.step()\n", + " \n", + " # Update running loss\n", + " running_loss += loss.item()\n", + " # Calculate accuracy\n", + " _, predicted = torch.max(outputs.data, 1)\n", + " total += labels.size(0)\n", + " correct += (predicted == labels).sum().item()\n", + " \n", + " # Calculate and store epoch metrics\n", + " epoch_loss = running_loss / len(trainloader)\n", + " epoch_acc = 100 * correct / total\n", + " train_losses.append(epoch_loss)\n", + " train_accuracies.append(epoch_acc)\n", + " print(f\"Epoch {epoch + 1}, Loss: {epoch_loss:.3f}, Accuracy: {epoch_acc:.2f}%\")\n", + "\n", + "# Evaluate model on test set\n", + "model.eval() # Set model to evaluation mode\n", + "correct = 0\n", + "total = 0\n", + "all_preds = [] # Store predictions for confusion matrix\n", + "all_labels = [] # Store true labels\n", + "with torch.no_grad(): # Disable gradient computation for evaluation\n", + " for data in testloader:\n", + " images, labels = data[0].to(device), data[1].to(device)\n", + " outputs = model(images)\n", + " _, predicted = torch.max(outputs.data, 1)\n", + " total += labels.size(0)\n", + " correct += (predicted == labels).sum().item()\n", + " all_preds.extend(predicted.cpu().numpy())\n", + " all_labels.extend(labels.cpu().numpy())\n", + "\n", + "# Calculate and print test accuracy\n", + "test_accuracy = 100 * correct / total\n", + "print(f\"\\nTest Accuracy: {test_accuracy:.2f}%\")\n", + "\n", + "# Plot training metrics (loss and accuracy)\n", + "plt.figure(figsize=(12, 4))\n", + "\n", + "# Plot training loss\n", + "plt.subplot(1, 2, 1)\n", + "plt.plot(train_losses, label='Training Loss')\n", + "plt.title('Training Loss')\n", + "plt.xlabel('Epoch')\n", + "plt.ylabel('Loss')\n", + "plt.legend()\n", + "\n", + "# Plot training accuracy\n", + "plt.subplot(1, 2, 2)\n", + "plt.plot(train_accuracies, label='Training Accuracy')\n", + "plt.title('Training Accuracy')\n", + "plt.xlabel('Epoch')\n", + "plt.ylabel('Accuracy (%)')\n", + "plt.legend()\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig('training_metrics.png') # Save plot\n", + "plt.close()\n", + "\n", + "# Plot confusion matrix\n", + "cm = confusion_matrix(all_labels, all_preds)\n", + "plt.figure(figsize=(10, 8))\n", + "sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',\n", + " xticklabels=classes, yticklabels=classes)\n", + "plt.title('Confusion Matrix')\n", + "plt.xlabel('Predicted')\n", + "plt.ylabel('True')\n", + "plt.savefig('confusion_matrix.png') # Save plot\n", + "plt.close()\n", + "\n", + "# Function to unnormalize and display images\n", + "def imshow(img):\n", + " img = img / 2 + 0.5 # Unnormalize\n", + " npimg = img.numpy()\n", + " return np.transpose(npimg, (1, 2, 0)) # Convert from CHW to HWC\n", + "\n", + "# Show sample test images with predictions\n", + "dataiter = iter(testloader)\n", + "images, labels = next(dataiter)\n", + "images, labels = images[:8].to(device), labels[:8]\n", + "outputs = model(images)\n", + "_, predicted = torch.max(outputs, 1)\n", + "\n", + "plt.figure(figsize=(12, 6))\n", + "for i in range(8):\n", + " plt.subplot(2, 4, i + 1)\n", + " plt.imshow(imshow(images[i].cpu()))\n", + " plt.title(f'Pred: {classes[predicted[i]]}\\nTrue: {classes[labels[i]]}')\n", + " plt.axis('off')\n", + "plt.savefig('sample_predictions.png') # Save plot\n", + "plt.close()\n", + "\n", + "print(\"Training complete. Plots saved as 'training_metrics.png', 'confusion_matrix.png', and 'sample_predictions.png'\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/lectures/09_multiclass_classification_cnn_n_to_n/0_cnn_concepts.pptx b/lectures/09_multiclass_classification_cnn_n_to_n/0_cnn_concepts.pptx new file mode 100644 index 0000000..e300455 Binary files /dev/null and b/lectures/09_multiclass_classification_cnn_n_to_n/0_cnn_concepts.pptx differ diff --git a/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/t10k-images-idx3-ubyte b/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/t10k-images-idx3-ubyte new file mode 100644 index 0000000..1170b2c Binary files /dev/null and b/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/t10k-images-idx3-ubyte differ diff --git a/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/t10k-images-idx3-ubyte.gz b/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/t10k-images-idx3-ubyte.gz new file mode 100644 index 0000000..5ace8ea Binary files /dev/null and b/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/t10k-images-idx3-ubyte.gz differ diff --git a/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/t10k-labels-idx1-ubyte b/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/t10k-labels-idx1-ubyte new file mode 100644 index 0000000..d1c3a97 Binary files /dev/null and b/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/t10k-labels-idx1-ubyte differ diff --git a/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/t10k-labels-idx1-ubyte.gz b/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/t10k-labels-idx1-ubyte.gz new file mode 100644 index 0000000..a7e1415 Binary files /dev/null and b/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/t10k-labels-idx1-ubyte.gz differ diff --git a/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/train-images-idx3-ubyte b/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/train-images-idx3-ubyte new file mode 100644 index 0000000..bbce276 Binary files /dev/null and b/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/train-images-idx3-ubyte differ diff --git a/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/train-images-idx3-ubyte.gz b/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/train-images-idx3-ubyte.gz new file mode 100644 index 0000000..b50e4b6 Binary files /dev/null and b/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/train-images-idx3-ubyte.gz differ diff --git a/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/train-labels-idx1-ubyte b/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/train-labels-idx1-ubyte new file mode 100644 index 0000000..d6b4c5d Binary files /dev/null and b/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/train-labels-idx1-ubyte differ diff --git a/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/train-labels-idx1-ubyte.gz b/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/train-labels-idx1-ubyte.gz new file mode 100644 index 0000000..707a576 Binary files /dev/null and b/lectures/09_multiclass_classification_cnn_n_to_n/data/MNIST/raw/train-labels-idx1-ubyte.gz differ diff --git a/lectures/09_multiclass_classification_cnn_n_to_n/dropout.svg b/lectures/09_multiclass_classification_cnn_n_to_n/dropout.svg new file mode 100644 index 0000000..6f16232 --- /dev/null +++ b/lectures/09_multiclass_classification_cnn_n_to_n/dropout.svg @@ -0,0 +1,225 @@ + + +Dropout + + +cluster_input + +Input Layer + + +cluster_hidden + +Hidden Layer (with Dropout) + + +cluster_output + +Output Layer + + + +I1 + +I1 + + + +H1 + +H1 + + + +I1->H1 + + + + + +H2 + +H2 + + + +I1->H2 + + + + + +H3 + +H3 + + + +I1->H3 + + + + + +H4 + +H4 + + + +I1->H4 + + + + + +I2 + +I2 + + + +I2->H1 + + + + + +I2->H2 + + + + + +I2->H3 + + + + + +I2->H4 + + + + + +I3 + +I3 + + + +I3->H1 + + + + + +I3->H2 + + + + + +I3->H3 + + + + + +I3->H4 + + + + + +I4 + +I4 + + + +I4->H1 + + + + + +I4->H2 + + + + + +I4->H3 + + + + + +I4->H4 + + + + + +O1 + +O1 + + + +H1->O1 + + + + + +O2 + +O2 + + + +H1->O2 + + + + + +H2->O1 + + + + + +H2->O2 + + + + + +H3->O1 + + + + + +H3->O2 + + + + + +H4->O1 + + + + + +H4->O2 + + + + + \ No newline at end of file