{"cells":[{"metadata":{},"cell_type":"markdown","source":"# MNIST using PyTorch for Beginners (Detailed Descriptions)"},{"metadata":{},"cell_type":"markdown","source":"## PyTorch\n\nPyTorch is a famous python framework for deep learning. It's gaining interest in the deep learning community because of its syntactical similarities with python and operational similarities with numpy - which is the fundamental package for scientific and mathematical computing in Python."},{"metadata":{},"cell_type":"markdown","source":"## Deep Learning\nDeep Leaarning is a subset of Machine Learning (Shallow Learning) that, generally, requires huge amount of data in order to puke out good results. However, this statement isn't enitrely true as we will soon see, in this notebook itself, that having huge datasets isn't always a necessary criteria to apply deep learning solutions to specific problems.\n\n### What is a Deep Learnig solution anyway?\nTo keep things simple, just remember the following equation\n\n`Deep Learning = Neural Networks with more than 3 layers`\n\n### Neural Networks\nNeural Network is, usually, a supervised method of learning. By supervised I mean, we know the target variable and its values for particular records and hence we can feed in this data to machine learning and deep learning algorithms and specify what it needs to learn. In contrast, the unsupervised learning methods require algorithms to just 'figure out' what are the relations between the records in the data that is fed into them.\n\nFollowing 'layers' consists of a Neural Network\n1. Input Layer\n2. Hidden Layers\n3. Output Layer\n\nEvery network has 1 input and 1 Output layer but can have any number of hidden layers, so in order to implement deep learning, we need at least 2 hidden layers in out network.\n\nDetails about a Neural Network, their different types and architecture is an active research topic and are already well defined and explained in numerous blogs and videos around the internet. Following are few links that I found relatively easy to understand:\n1. 3b1b's neural net video: https://www.youtube.com/watch?v=aircAruvnKk&t=9s\n2. Medium neural net blog: https://towardsdatascience.com/first-neural-network-for-beginners-explained-with-code-4cfd37e06eaf\n3. Neural Network's Chart: https://towardsdatascience.com/the-mostly-complete-chart-of-neural-networks-explained-3fb6f2367464\n\nP.S. I really enjoyed the DeepLizard.com's course on PyTorch. They have really started from the very basic and have ended on a fairly complex note and the learning was so intuitive, probably because they have covered every minute detail that goes on in PyTorch as well as Neural Networks and even Python. Just leaving their course's link here for anyone to get started with... something!\n\nLink: https://deeplizard.com/learn/video/v5cngxo4mIg\n"},{"metadata":{},"cell_type":"markdown","source":"## MNIST Dataset\n\nWe are using the most famous Dataset for Computer Vision provided by the Turing Award holder and a very prominent personality in deep learning community: Dr Yann LeCun.\n\nThe dataset consists of 28x28 pixel, greyscale images. The rest of the details are given clearly in the description of this dataset."},{"metadata":{},"cell_type":"markdown","source":"## Implementation\nWe will be implementing a specific type of Neural Network called <b>'Convolutional Neural Network (CNN)'</b> and we'll follow the following steps:\n\n1. Import the libraries\n2. Load the dataset\n3. Splitting features and target variables\n4. Split the data into train and test sets\n5. Rescaling Values\n6. Converting to torch tensors\n7. Dataset and DataLoader\n8. Define Neural Network Architecture\n    Input Layer\n    Convolution Layer\n    Pooling Layer\n    Activation Function\n    Regularization\n    Linear Layer\n9. Loss Function\n10. Optimiser\n11. Train the Neural Network\n12. Predict the labels on actual test data\n"},{"metadata":{},"cell_type":"markdown","source":"### 1. Import the libraries"},{"metadata":{"trusted":true},"cell_type":"code","source":"#PyTorch Specific libraries\nimport torch\nfrom torch import nn, optim\nimport torch.nn.functional as F\nfrom torch.autograd import Variable\n\n#Data manipulation and visualisation specific libraries\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# For splitting the data into Train and Test set\nfrom sklearn.model_selection import train_test_split\n\n# This piece of code is required to make use of the GPU instead of CPU for faster processing\ndevice = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\nprint(device)\n\n#If it prints \"cuda:0\" that means it has access to GPU. If it prints out \"cpu\", then it's still running on CPU.","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### 2. Load the dataset\n\nAfter loading the dataset with the help of panda's `read_csv()` function, we check if the data is imported correctly or not. This can be done in many ways, the simplest way is to check the number of rows and column. And we can do so with the help of `shape` attribute. It outputs in the format - (rows, columns)."},{"metadata":{"trusted":true},"cell_type":"code","source":"train = pd.read_csv('/kaggle/input/digit-recognizer/train.csv')\ntest = pd.read_csv('/kaggle/input/digit-recognizer/test.csv')\n\n#Let's check if they have been loaded properly\nprint('train.shape:\\n', train.shape)\nprint('test.shape:\\n', test.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### 3. Splitting features and target variables\n\nThe label is the last column and rest of the columns are all the features\nHere, X -> Features; y -> Labels\n\nWhy to name \"X\" and \"y\"? They are the standard names used by everyone. You could use anything instead of X and y but since it's used by almost everyone, so it makes your code more readable to others since they would instantly know what they are looking at and would then be able to debug your code fast, in case you need help."},{"metadata":{"trusted":true},"cell_type":"code","source":"X = train.iloc[:,:-1]\ny = train.iloc[:,-1:] #Could have done like this \ny = train.label.values # but needed to convert it to np.ndarray for torch tensor conversion","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Now, spliting with `train.iloc[:,-1:]` would have resulted the same as `train.labels.values` have.\nThe only difference between the two is, the former would have resulted in a Series and the latter is preserved as ndarray - which is what we want since we would be convertiing this into a PyTorch Tensor in a while."},{"metadata":{"trusted":true},"cell_type":"code","source":"print('X.shape: ', X.shape, 'X.type: ', type(X) )\nprint('y.shape: ', y.shape, 'y.type: ', type(y) )","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### 4. Split the data into train and test sets\n\nWe need a test set, apart from the original test.csv that is provided, for the purpose of our training and validation. The test.csv doesn't contain the target variable and hence the model can't be trainied on that. The target (label column) is only present in train.csv. Hence, we will split the data in the training set (training.csv) into two parts - training set (for training the model) and test set (for validation). To do so, we will use train_test_split() method from `sklearn.model_selection`. This method takes in the features (X) and label column (y) separately in the first 2 parameters. The 3rd parameter is test_size, which we can set in order to specify with what percentage we want to split our data. We are going to use \"0.1\" as the argument value. This will split our data into 90% training and 10% test sets. The 4th parameter is the `random_state`. This is an optional parameter which you will find as a part of many ML algorithms. It is required to produce same results and eliminate any 'randomness' that the algorithms can take since a lot of the internal values in ML algorithms have to assume some random values as part of their execution.\n\nThe function outputs 4 values, essentially splitting the X and y in to 2 parts each (test and training sets)"},{"metadata":{"trusted":true},"cell_type":"code","source":"X_train, X_test, y_train, y_test = train_test_split(X, y , test_size = 0.1, random_state = 1)\n\nprint('X_train.shape: ', X_train.shape)\nprint('y_train.shape: ', y_train.shape)\nprint('X_test.shape: ', X_test.shape)\nprint('y_test.shape: ', y_test.shape)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### 5. Rescaling values\n\nThe pixel values that we have got in the dataset is in the range of (0,255). For neural network (or any machine learning algorithm for that matter) to be efficient, we rescale the values to (0,1). That means, all the values will be between 0 and 1."},{"metadata":{"trusted":true},"cell_type":"code","source":"#Rescaling values\nX_train = X_train.values/255\nX_test = X_test.values/255","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### 6. Converting to torch tensors\n\nPyTorch works with tensors. Tensors can be visualised as multidimensional arrays. Just like numpy arrays, or Tensorflow's tensors. PyTorch has its own form of arrays.\n\ntorch.from_numpy() function converts a numpy array or a panda's dataframe to torch tensor (remember why we needed 'y' to be a numpy array and not series?)"},{"metadata":{"trusted":true},"cell_type":"code","source":"#Converting to Tensors\nX_train = torch.from_numpy(X_train)\nX_test = torch.from_numpy(X_test)\n\ny_train = torch.from_numpy(y_train).type(torch.LongTensor)\ny_test = torch.from_numpy(y_test).type(torch.LongTensor)\n\nprint('X_train.dtype:', X_train.dtype)\nprint('X_test.dtype:', X_test.dtype)\nprint('y_train.dtype:', y_train.dtype)\nprint('y_test.dtype:', y_test.dtype)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### 7. Dataset and DataLoader\n\nWe use Data Loaders because sometimes the datasets can have upto millions of records which may eventually lead to enormous size of those datasets and hence they are generally very difficult to load in memoty all at once. Due to the limited available memory space, we use a technique called batching. Batching is simply accessing records from such huge datasets in `batches` and not all at once.\n\nFor example, Let's suppose you have 1000 records in a dataset and you want to apply batching on this dataset. So, if you set the batch size to 200, your model will first be trained on the first set of 200 records in 1 iteration of the feedforward network. Then, it will train on the next set of records and so on. So, it will essentially take 5 iterations for the whole dataset to be passed through the feed forward network.\n\nJust to clear things:\n\n**batch**: Small set of the larger dataset\n\n**iteration**: 1 `batch` going from the input layer to the output layer is said to complete one iteration\n\n**epochs**: When every record of the dataset is passed through the network, then the network is said to have finised 1 `epoch`\n\nNow, the MNIST dataset isn't that large that we have to use dataloader, but since it's a general practice to load the data in batches, so, I am going to implement using dataloader.\n\nHead to PyTorch's official documentation on Dataset class, DataLoader and Transforms to know more about the same: https://pytorch.org/tutorials/beginner/data_loading_tutorial.html\n\nNow we will make our dataset using TensorDataset class and would set the batch size to be `100`.\n\nThen with the help of DataLoader class, that will provide an iterable over our dataset, we will make our `train_loader` and `test_loader`. The shuffle paramtere is set to true in order to specify that the records in a particular batch needs to be randomly selected from the whole dataset and not select in their original sequence as they appear."},{"metadata":{"trusted":true},"cell_type":"code","source":"train = torch.utils.data.TensorDataset(X_train, y_train)\ntest = torch.utils.data.TensorDataset(X_test, y_test)\n\nbatch = 100\n\n# Set our data loaders\ntrain_loader = torch.utils.data.DataLoader(train, batch_size = batch, shuffle = True)\ntest_loader = torch.utils.data.DataLoader(test, batch_size = batch, shuffle = True)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### 8. Define neural network architecture\n\nWe will be defining the folllowing in the Net() class:\n\n* Input Layer\n* Convolution Layer\n* Pooling Layer\n* Activation Function\n* Regularization\n* Linear Layer\n\nHere's a briefing of everything that's going on in the following cell.\n\n**Net() Class:** `Net()` is the main class which inherits `nn.Module` class. Net() class has two methods, namely, `__init__()` and `forward()`.\n\n**init() method:** The `__init__()` method, which is one of the 'special methods' that are provided by python, stands for initialisation and as the name tells itself, it is used to initialise everything that we need to. It inherits all the properties of the `nn.Module`'s own `__init__` function when we write `super().__init__()`. All the convolution layers, pooling layers, dropout layers, Linear (Dense/fully connected) layers are defined in this method.\n\n**forward() method:** It's the `forward()` method where we set the course of our network and use all the layers that we define in the `__init__` method.\n\nWhen we execute the code, the init method will be called and everything would be initialised. When we'll make the object of the same class, the forward method will be executed.\n\nThe architecture:\n\n`Layer 1: Convolution Layer 1 > Activation Function (RelU) > Pooling Layer 1 > Dropout Layer 1\nLayer 2: Convolution Layer 2 > Activation Function (RelU) > Pooling Layer 2 > Dropout Layer 2\nFlatten the output from the 2 layer\nLayer 3: Linear Layer 1 > Activation Function (RelU) > Dropout Layer 3\nLayer 4: Linear Layer 2 > Activation Function (RelU) > Dropout Layer 4\nLayer 5: Output Layer > Activation function (softmax)`"},{"metadata":{"trusted":true},"cell_type":"code","source":"class Net(nn.Module):\n    \n    def __init__(self):\n        super().__init__()\n        \n        self.conv1 = nn.Conv2d(1, 128, 5)\n        self.pool1 = nn.MaxPool2d(2, 2)\n        self.drop1 = nn.Dropout(p=0.3)\n        \n        self.conv2 = nn.Conv2d(128, 224, 5)\n        self.pool2 = nn.MaxPool2d(2, 2)\n        self.drop2 = nn.Dropout(p=0.4)\n        \n        self.fc3 = nn.Linear(224*4*4, 64)\n        self.drop3 = nn.Dropout(p=0.4)\n        \n        self.fc4 = nn.Linear(64, 32)\n        self.drop4 = nn.Dropout(p=0.4)\n        \n        self.fc5 = nn.Linear(32, 10)\n        self.softmax = nn.Softmax(dim=1)\n   \n    \n    def forward(self, x):\n        x = self.drop1(self.pool1(F.relu(self.conv1(x))))\n        x = self.drop2(self.pool2(F.relu(self.conv2(x))))\n        \n        x = x.view(-1,224*4*4)\n        \n        x = self.drop3(F.relu(self.fc3(x)))\n        x = self.drop4(F.relu(self.fc4(x)))\n        \n        x = self.softmax(self.fc5(x))\n        \n        return x\n\nprint(Net()) ","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Q. Why these many layers? Why not less or more?**\n\nA. Choosing the number of layers is up to the network designer (you). It's with hit and trial that you develop an intiution of how many layers (or neurons) a particular problem may require to get to the desired solution. You can play with the number of layers and neurons (numbers like 128, 224 etc) and see how the network is responsding to your changes.\n\n**Q. What does Conv2d(1, 128, 5) actually mean?**\n\nA. Conv2d() is a function that helps create a convolution layer. The three paramteres mentioned (in order) are: input_channels, output_channels and kernel_size.\n\n**input_channels:** input_channels refers to the number of channels the input has. If our dataset contained colored images, this argument would have been equal to 3. But our dataset contains only the black and white pictures (more generally called as the grayscale images), so a grayscale images have only 1 channel. Hence the argument is equal to 1.\n\n**output_channels:** output_channels is arbitrary number and can be set to any number you want. It basically sets the number of neurons that its input_channels would be connected to. It also sets the input_channels of the next convolution layer. So, `output_channel (previous layer) = input_channel (next layer)`\n\n**kernel_size:** kernel_size is a square matrix of a particular width and height. When we set the third parameter to 5, it basically means we are setting the square kernel of size 5 x 5. A kernel can be thought of as a window (matrix) that slides over our image to extract certain features out of it in order to learn. That's why kernels are also called as feature maps.\n\nThere's anther parameter that we haven't explicitly defined but by default is set to 1, and that is '**stride**'. A stride means by how much the kernel should slide in order to output the next feature map. `stride = 1` essentially means that it will slide by 1 pixel to right and 1 pixel to down.\n\n**Q. What is pooling?**\n\nA. Pooling is another type of layer in a neural network that down samples the feature maps created by the convolution layer. It basically summarises a portion of the image in a much lesser space. It is done to reduce variance and computations. But why Max-pooling? It's because max-pooling helps in extracting low-level features like edges, points, etc. There are other type of pooling methods such as average pooling and others.\n\n**Q. What is the importance of view() function in the forward() method?**\n\nA. The view() function takes in a tensor (for ex a 4D tensor) and outputs a lower dimensional tensor (for example a 1D tensor). This is required because the next set of layers are the linear layers. And linear layers only accept a 1D tensor (a \"flattened\" array) as an input. So, the view function is used to flatten the tensor so that it can then be fed to the linear layers.\n\n**Q. What is a Dropout Layer?**\n\nA. To help understand what dropout really is, we need to understand what regularisation in general is\n\n### Regularization\nRegularization helps to solve over fitting problem in machine learning. It is nothing but adding a penalty term to the objective function and control the model complexity using that penalty term.\n\nToo complex?\n\nFor now, consider it as something that tells our model to not cross a certain boundry (threshold). Because if it does, it will be overfitted. Overfiting is a problem that machine learning algortihms face when the model is 'too exact' to be used for actual real world data. That simply means that our model, when overfitted, wouldn't be accurate in preidction task when a new datapoint is fed into the model (for example a datapoint from the test dataset). So, in order to avoid overfitting, we use regularization. This is just an intuition. We don't need to get into the math of anything for now.\n\nDropout is one of the regularization methods. Dropout helps avoid overfitting by simply 'switching off' some of the neurons in a particular iteration. Sometimes, the outup of a particular neuron from a particular layer can shoot off to a high value or to a very low value. So, the dropout method helps the network to consider some of the other neurons that it might have been ignoring because of their lower valued outputs. So, by simply turning off some neurons, it makes sure that it takes into account every neuron and hence train in a more vigorous manner and hence avoid overfitting.\n\n**Q. What is RelU and softmax?**\n\nA. RelU is short for Rectified Linear Unit. RelU and Softmax are both activation functions. What are activation functions, you ask? Well, they can be thought of as something that restricts the values of the neurons after computations to explode to extremes (very high negative or very high positive) values. Activation functions makes sure the values are bounded within a range. For example, the relu function makes sure the values are between (0,max(val)) range, where 'val' is the actual value of the neuron.\n\n**Q. But why only relu and Softmax are chosen?**\n\nA. You can choose any activation function you want. However, recent researches proves that relu performs well in almost all the cases as comapred to other activation functions. tanh is another very commonly used activation function. We don't have the luxury of choosing just any activation function when it comes to the output layer. We want to choose an activation function according to our desired output. We have chosen softmax because softmax gives out a class score for every class which is basically a probablity distribution that in turn tells us how confident is the neural network about a certain class to be the actual output."},{"metadata":{},"cell_type":"markdown","source":"***Note*:**\nThe two modules imported from the torch library: nn and functional can be used interchangeable. Having said that, all the functions that are present in the nn module are there in the functional module as well. The only difference is the names of these funtions.\n\nFor example, Max pooling function from the functional module can be called as:\n\n`F.max_pool2d(x, kernel_size, stride)`\n\nand from the nn module:\n\n`nn.MaxPool2d(kernel_size, stride)`\n\nWhy are there two implementations of the same things? It's to suit the coding style of different people. Some prefer a stateful approach while others prefer a more functional approach."},{"metadata":{},"cell_type":"markdown","source":"### 9. Loss function\n\nA neural network 'learns' by adjusting various parameters through out the network. Weight is one of the paramter that the neural network learns to adjust. Weight is simply the strength of the two adjoining neurons that it is connected to and it is a numeric value. The new weight values are updated duriing backpropagation process. Now, the thing to note here is, how are these new values calculated?\n\nThe answer is: with the help of loss functions.\n\nLoss functions are simply a method to calculate the prediction error of the network. Simply put, it tells the network which weight to be adjusted and how much it needs to be adjusted. Essentially, it calculates if a particular weight value needs to be decreased (subtracting from thhe original value) or increased (adding to the original value) and by how much.\n\nThere are many loss functions that can be used. The two most common ones are Cross Entropy Loss and NLL Loss for classification problems (just a reminder that we are dealing with a classification problem right now). The choice of which loss function to use is an experimental problem. You can follow <b><a href='https://machinelearningmastery.com/how-to-choose-loss-functions-when-training-deep-learning-neural-networks/'>this</a></b> guide to start using the loss functions and by practice, build your own intiution.\n\nFor now, I'll be using the Cross Entropy Loss\n\nWe are now making an object of our Net() class and setting the loss function. We are using the Cross Entropy Loss. You can use any other loss function as well."},{"metadata":{"trusted":true},"cell_type":"code","source":"#Making an object of the Net class\nmodel = Net().to(device)\n\n#Loss function\ncriterion = nn.CrossEntropyLoss ()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### 10. Optimizer\nAn optimizer ties the loss function and model parameters together by updating the model in response to the output of the loss criterion. We will be using Adam as our optimizer with a learning rate of 0.0015. Why 0.0015? Again, it's an experimental value. You need to hit and trial with all of these parameters that are changable. After a while dealiing with and constructing neural networks, you'll develop an intiution of which values could possibly work for a particular problem."},{"metadata":{"trusted":true},"cell_type":"code","source":"# Optimizer\noptimizer = optim.Adam(model.parameters(), lr = 0.0015)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### 11. Train the Neural Network\n\nNow it's finally time to train our network. We will be setting some variablbes first and then start training our network."},{"metadata":{"trusted":true},"cell_type":"code","source":"# Initialising variables\nepochs = 30\nsteps = 0\nprint_every = 100\ntrainLoss = [] \ntestLoss = []","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"for e in range(epochs):\n    running_loss = 0\n    for images, labels in train_loader:\n        steps += 1   # Forward pass\n        \n        images = (images.view(-1,1,28,28)).type(torch.DoubleTensor)\n        optimizer.zero_grad()\n        log_ps = model(images.type(torch.FloatTensor).to(device))\n        labels = labels.to(device)\n        loss = criterion(log_ps, labels)\n        loss.backward()   # Backward pass\n        optimizer.step()\n        \n        running_loss += loss.item()\n        if steps % print_every == 0:\n            test_loss = 0\n            accuracy = 0\n\n            with torch.no_grad():\n                model.eval()\n                for images, labels in test_loader:\n                    images = (images.view(-1,1,28,28)).type(torch.DoubleTensor)\n                    log_ps = model(images.type(torch.FloatTensor).to(device))\n                    labels = labels.to(device)\n                    test_loss += criterion(log_ps, labels)\n                    ps = torch.exp(log_ps)\n                    \n                    top_p, top_class = ps.topk(1, dim = 1)\n                    equals = top_class == labels.view(*top_class.shape)\n                    accuracy += torch.mean(equals.type(torch.FloatTensor))\n\n            model.train()\n\n            trainLoss.append(running_loss/len(train_loader))\n            testLoss.append(test_loss/len(test_loader))\n\n            print(\"Epoch: {}/{}.. \".format(e + 1, epochs),\n                  \"Test Accuracy: {:.3f}\".format(accuracy/len(test_loader)))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"We are using a `for` loop to go through a total of 30 epochs.\n\nThe `images` tensor is a 2D tensor, we first convert it into a 4D tensor since our neural network (the Net() class) requires it to be a 4D tensor. Hence we use the `view()` function.\n\n`optimizer.zero_grad()` sets all the gradient's values to zero. It's an essential step while training the network.\n\n`log_ps = model(images.type(torch.FloatTensor))` simply gives a set of images to the network for training. Here the set is of 100, as we set it earlier while defining the `DataLoader`\n\n`loss = criterion(log_ps, labels)` compare the predicted and the actual labels and calculates the loss.\n\n`loss.backward()` is the step where the network starts backpropagating in order to adjust the weights and other parameters of the network and start the training process all over again.\n\n*Please note that the loss is calculated by the loss function but the weights are **updated** only during backpropagation.\n*\n\n`running_loss += loss.item()` is used to calculate `trainLoss` which eventually would be helpful in plotting the graph of training and validation loss\n\nSince we don't want every iteration to get printed on the screen, we only want every 100th iteration to get prinited with the accuracy score, hence we have used an `if` statment for the same.\n\n`with torch.no_grad():` the `with` keyword is an elegant way of handling exceptions in python. The wrapper `torch.no_grad` temporarily sets all the `requires_grad` flag to false. Eventually, it will reduce the memory usage and speed up computations.\n\n`model.eval()` it indicates the model that nothing new is to be learnt and the model is used for testing.\n\n`torch.exp(log_ps)` returns a new tensor with the exponential of the elements of the input tensor\n\n`ps.topk(1, dim = 1)` returns a sparse copy of the tensor\n\n`labels.view(*top_class.shape)` sees how many of the classes were correct?\n\n`torch.mean(equals.type(torch.FloatTensor))` calculate the mean (gets the accuracy for this batch) and add it to the running accuracy for this epoch\n\n`model.train()` tells your model that you are training the model. So effectively, layers like dropout etc, which behave different on the train and test procedures, know what is going on and hence can behave accordingly."},{"metadata":{},"cell_type":"markdown","source":"Let's now visualise the training and validation loss"},{"metadata":{"trusted":true},"cell_type":"code","source":"%matplotlib inline\n\nplt.plot(trainLoss, label = 'Training Loss')\nplt.plot(testLoss, label = 'Validation Loss')\nplt.legend(frameon = False)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### 12. Predict the labels on actual test data\n\nNow, we apply all the steps for the actual test data from the test.csv and finally make a 'submission.csv' with all out predictions."},{"metadata":{"trusted":true},"cell_type":"code","source":"test = pd.read_csv('/kaggle/input/digit-recognizer/test.csv')\n\nfinalTest = test.values/255\n\nfinalTest = torch.from_numpy(finalTest)\n\ntemp = np.zeros(finalTest.shape)\ntemp = torch.from_numpy(temp)\n\ndata = torch.utils.data.TensorDataset(finalTest, temp)\n\nsubmissionLoader = torch.utils.data.DataLoader(data, batch_size = batch, shuffle = False)\n\nsubmission = [['ImageId', 'Label']]\n\nwith torch.no_grad():\n    model.eval()\n    image_id = 1\n    for images, _ in submissionLoader:\n        images = (images.view(-1,1,28,28)).type(torch.DoubleTensor)\n        log_ps = model(images.type(torch.FloatTensor).to(device))\n        ps = torch.exp(log_ps)\n        top_p, top_class = ps.topk(1, dim = 1)\n        \n        for prediction in top_class:\n            submission.append([image_id, prediction.item()])\n            image_id += 1\n            \n\n\npytorchSubmission = pd.DataFrame(submission)\npytorchSubmission.columns = pytorchSubmission.iloc[0]\npytorchSubmission = pytorchSubmission.drop(0, axis = 0)\n\npytorchSubmission.to_csv(\"submission.csv\", index = False)\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"*I have tried to provide every possible minute detail that I found was necessary for any beginner, who is just getting started with either neural networks or PyTorch, to know and understand.*\n\n*I'll keep on updating this notebook as and when I feel the need to. You are welcome to point out any mistakes, share any suggestoins, imporvements or anything you feel like with me in the comments. Please consider liking this notebook if you feel so and share it with anyone you might think would benefit from this or can improve this further.*"}],"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat":4,"nbformat_minor":4}