---
# System prepended metadata

title: PyTorch Note

---

# PyTorch Note
## Tutorial
- https://medium.com/deeplearningbrasilia/deep-learning-introduction-to-pytorch-5bd39421c84
- https://medium.com/dair-ai/a-first-shot-at-deep-learning-with-pytorch-4a8252d30c75
### Defining Your Network
- PyTorch has a standard way for you to create your own models. 
- The entire definition should stay inside an object that is a child of the class **nn.Module**.
    - Inside this class, there are only two methods that must be implemented.
    - `__init__`
        - As in other Python class, `__init__` method is used to define the class' attributes and any value that you would want upon instantiation.
        - You should always call the `super()` method to initialize the parent class in the PyTorch context.
        - Beyond that, you can define all the layers that have optimizable parameters.
        - The definition of layers **do not need to be in order** of which they will be used in the network.
    - `forward`
        - This is the method where you tell how the layers are connected.
        - It is worth noticing that there are some other functions applied inside the forward method the `__init__` method but can traditionally be regarded as layers.
            - Take a look of instance at the `F.relu` function. We didn't define that in the `__init__` method because it does not have any trainable parameters.
- The following example shows an implementation of a network containing 3 linear layers combined by 2 ReLU layers.
```python=
import torch
import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        # Defining 3 linear layers but NOT the way they should be connected
        # Receives an array of length 240 and outputs one with length 120
        self.fc1 = nn.Linear(240, 120)
        # Receives an array of length 120 and outputs one with length 60
        self.fc2 = nn.Linear(120, 60)
        # Receives an array of length 60 and outputs one with length 10
        self.fc3 = nn.Linear(60, 10)

    def forward(self, x):
        # Defining the way that the layers of the model should be connected
        # Performs RELU on the output of layer 'self.fc1 = nn.Linear(240, 120)'
        x = F.relu(self.fc1(x))
        # Performs RELU on the output of layer 'self.fc2 = nn.Linear(120, 60)'
        x = F.relu(self.fc2(x))
        # Passes the array through the last linear layer 'self.fc3 = nn.Linear(60, 10)'
        x = self.fc3(x)
        return x


net = Net()
```

- As a rule of thumb, you can put inside the `forward` method all the layers that **do not** have any weights to be updated 
    - On the other hand, you should put all the layers that have weights to be updated inside the `__init__`.

### Lodaing Your Data: Dataset and Data Loaders
- **Dataset** and **Data loaders** are the tools in PyTorch can define how to access your data.
    - For instance, if you have several images in some directory structure, you can personalize the way you access it with the **Dataset** class.
- The code below give a basic example of how to define your own **Dataset** class and then loop over your data accessing the elements.
```python=
import torch
import pandas as pd
from torch.utils.data import Dataset, DataLoader

class ExampleDataset(Dataset):
    """Example Dataset"""

    def __init__(self, csv_file):
        """ 
        csv_file (string): Path to the csv file containing data.
        """
        self.data_frame = pd.read_csv(csv_file)

    def __len__(self):
        return len(self.data_frame)

    def __getitem__(self, idx):
        return self.data_frame[idx]

# instantiates the dataset  
example_dataset = ExampleDataset('my_data_file.csv')

# batch size: number of samples returned per iteration
# shuffle: Flag to shuffle the data before reading so you don't read always in the same order
# num_workers: used to load the data in parallel
example_data_loader = DataLoader(example_dataset, , batch_size=4, shuffle=True, num_workers=4)

# Loops over the data 4 samples at a time
for batch_index, batch in enumerate(example_data_loader):
    print(batch_index, batch)
  
```

- There are 3 required methods we need to impement in the **Dataset** classes:
    - `__init__`
        - You should put your directories information and other things that would allow to access it in the initialization.
        - Inside the `__init__` you do not have to already load your data.
    - `__len__`
        - In this method, you should implement a way to get the entire size of your dataset.
            - For example, you have to implement an approach of counting the total number of files that makes your data if you have a set of images in some directories.
        - Nevertheless, this can be arbitraily complicated depending on how your data is stored.
    - `__getitem__`
        - This is where you implement how to get a single item from your dataset.
            - For instance, if you have plenty of images, here is where you would load your image from disk into memory and serve it as what the method returns.
        - Using **DataLoader** class results in more efficiency of accessing your dataset (it can read a batch of data in parallel).

### Training: Updating the Network Weights
- optimizer
    - It goes over all your weights and update them of you.
    - It automatically accesses all the layers your defined in the `__init__` method of your network class and updates them using a certain learning rate and algorithm depending on the kind of optimizer you choose.
    - It must have a criterion to use of optimization.
        - loss function
- sample code
```python=
import torch.optim as optim
import torch.nn as nn
# instantiate your network that should be defined by you
net = Net()
# create your optimizer
optimizer = optim.SGD(net.parameters(), lr=0.01)
# define your criterion for optimization
criterion = nn.MSELoss()
# dat_set comes from somewhere
for data in data_set:
  # zero the gradient buffers
  optimizer.zero_grad()  
  # Passes the data through your network
  output = net.forward(data)
  # calculates the loss
  loss = criterion(output, target)
  # Propagates the loss back
  loss.backward()
  # Updates all the weights of the network
  optimizer.step()    
```

## Example
### Drawing Architecture: Building Deep Convolutional GAN’s In Pytorch
- https://towardsdatascience.com/drawing-architecure-building-deep-convolutional-gans-in-pytorch-5ed60348d43c
#### Generator
```python=
nn.Sequential(
 nn.ConvTranspose2d( nz, ngf * 8, 4, 1, 0, bias=False),
 nn.BatchNorm2d(ngf * 8),
 nn.ReLU(True),
 nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
 nn.BatchNorm2d(ngf * 4),
 nn.ReLU(True),
 nn.ConvTranspose2d( ngf * 4, ngf * 2, 4, 2, 1, bias=False),
 nn.BatchNorm2d(ngf * 2),
 nn.ReLU(True),
 nn.ConvTranspose2d( ngf * 2, ngf, 4, 2, 1, bias=False),
 nn.BatchNorm2d(ngf),
 nn.ReLU(True),
 nn.ConvTranspose2d( ngf, nc, 4, 2, 1, bias=False),
 nn.Tanh()
 )
```
#### Random Noise
```python=
fixed_noise = torch.randn(64, nz, 1, 1, device=device)
```

#### Discriminator
```python=
nn.Sequential(
 nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
 nn.LeakyReLU(0.2, inplace=True),
 nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
 nn.BatchNorm2d(ndf * 2),
 nn.LeakyReLU(0.2, inplace=True),
 nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
 nn.BatchNorm2d(ndf * 4),
 nn.LeakyReLU(0.2, inplace=True),
 nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
 nn.BatchNorm2d(ndf * 8),
 nn.LeakyReLU(0.2, inplace=True),
 nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
 nn.Sigmoid()
 )
```

## Transfer Learning
- https://heartbeat.fritz.ai/transfer-learning-with-pytorch-cfcb69016c72
### Import
```python=
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
from matplotlib import pyplot as plt
import torch
from torch import nn
import torch.nn.functional as F
from torch import optim
from torch.autograd import Variable
from torchvision import datasets, transforms, models
from PIL import Image
import numpy as np
import os
from torch.utils.data.sampler import SubsetRandomSampler
import pandas as pd
```

### Visualizing
```python=
img_dir='/content/drive/My Drive/app/cell_images'
def imshow(image):
    """Display image"""
    plt.figure(figsize=(6, 6))
    plt.imshow(image)
    plt.axis('off')
    plt.show()
# Example image
x = Image.open(img_dir + '/Parasitized/C33P1thinF_IMG_20150619_114756a_cell_179.png')
np.array(x).shape
imshow(x)
```

### Transform
```python=
# Define your transforms for the training, validation, and testing sets
train_transforms = transforms.Compose([transforms.RandomResizedCrop(size=256, scale=(0.8, 1.0)),
                                      transforms.RandomRotation(degrees=15),
                                      transforms.ColorJitter(),
                                      transforms.RandomHorizontalFlip(),
                                      transforms.CenterCrop(size=224),  # Image net standards
                                      transforms.ToTensor(),
                                      transforms.Normalize([0.485, 0.456, 0.406],
                                                           [0.229, 0.224, 0.225])
                                      ])

test_transforms = transforms.Compose([transforms.Resize(256),
                                      transforms.CenterCrop(224),
                                      transforms.ToTensor(),
                                      transforms.Normalize([0.485, 0.456, 0.406], 
                                                           [0.229, 0.224, 0.225])])

validation_transforms = transforms.Compose([transforms.Resize(256),
                                            transforms.CenterCrop(224),
                                            transforms.ToTensor(),
                                            transforms.Normalize([0.485, 0.456, 0.406], 
                                                                 [0.229, 0.224, 0.225])])
```

### Loading Data
```python=
#Loading in the dataset

train_data = datasets.ImageFolder(img_dir,transform=train_transforms)
# number of subprocesses to use for data loading
num_workers = 0
# percentage of training set to use as validation
valid_size = 0.2

test_size = 0.1

# obtain training indices that will be used for validation
num_train = len(train_data)
indices = list(range(num_train))
np.random.shuffle(indices)
valid_split = int(np.floor((valid_size) * num_train))
test_split = int(np.floor((valid_size+test_size) * num_train))
valid_idx, test_idx, train_idx = indices[:valid_split], indices[valid_split:test_split], indices[test_split:]

print(len(valid_idx), len(test_idx), len(train_idx))

# define samplers for obtaining training and validation batches
train_sampler = SubsetRandomSampler(train_idx)
valid_sampler = SubsetRandomSampler(valid_idx)
test_sampler = SubsetRandomSampler(test_idx)

# prepare data loaders (combine dataset and sampler)
train_loader = torch.utils.data.DataLoader(train_data, batch_size=32,
    sampler=train_sampler, num_workers=num_workers)
valid_loader = torch.utils.data.DataLoader(train_data, batch_size=32, 
    sampler=valid_sampler, num_workers=num_workers)
test_loader = torch.utils.data.DataLoader(train_data, batch_size=32, 
    sampler=test_sampler, num_workers=num_workers)
```

### Steps of Training the mode
1. Loading in the pre-trained mode.
2. Freezing the convolutional layers.
3. Replacing the fully connected layers with a custom classifier.
4. Training the custom classifier for the specific task.

#### Loading the Pre-Trained Model
```python=
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
#pretrained=True will download a pretrained network for us
model = models.densenet121(pretrained=True)
model
```

#### Freezing the convolutional layers & replacing the fully connected layers with a custom classifier
```python=
#Freezing model parameters and defining the fully connected network to be attached to the model, loss function and the optimizer.
#We there after put the model on the GPUs
for param in model.parameters():
  param.require_grad = False
fc = nn.Sequential(
    nn.Linear(1024, 460),
    nn.ReLU(),
    nn.Dropout(0.4),
    
    nn.Linear(460,2),
    nn.LogSoftmax(dim=1)
    
)
model.classifier = fc
criterion = nn.NLLLoss()
#Over here we want to only update the parameters of the classifier so
optimizer = torch.optim.Adam(model.classifier.parameters(), lr=0.003)
model.to(device)
```

#### Training the custom classifier for the specific task
```python=
#Training the model and saving checkpoints of best performances. That is lower validation loss and higher accuracy
epochs = 10
valid_loss_min = np.Inf
import time
for epoch in range(epochs):
    
    start = time.time()
    
    #scheduler.step()
    model.train()
    
    train_loss = 0.0
    valid_loss = 0.0
    
    for inputs, labels in train_loader:
        
       
        # Move input and label tensors to the default device
        inputs, labels = inputs.to(device), labels.to(device)
        
        optimizer.zero_grad()
        
        logps = model(inputs)
        loss = criterion(logps, labels)
        loss.backward()
        optimizer.step()
train_loss += loss.item()
        
       
    model.eval()
    
    with torch.no_grad():
        accuracy = 0
        for inputs, labels in valid_loader:
            
            inputs, labels = inputs.to(device), labels.to(device)
            logps = model.forward(inputs)
            batch_loss = criterion(logps, labels)
valid_loss += batch_loss.item()
# Calculate accuracy
            ps = torch.exp(logps)
            top_p, top_class = ps.topk(1, dim=1)
            equals = top_class == labels.view(*top_class.shape)
            accuracy += torch.mean(equals.type(torch.FloatTensor)).item()
                    
            
            
# calculate average losses
    train_loss = train_loss/len(train_loader)
    valid_loss = valid_loss/len(valid_loader)
    valid_accuracy = accuracy/len(valid_loader) 
      
    # print training/validation statistics 
    print('Epoch: {} \tTraining Loss: {:.6f} \tValidation Loss: {:.6f} \tValidation Accuracy: {:.6f}'.format(
        epoch + 1, train_loss, valid_loss, valid_accuracy))
            
    
    
    if valid_loss <= valid_loss_min:
      
      
      print('Validation loss decreased ({:.6f} --> {:.6f}).  Saving model ...'.format(
      valid_loss_min,
      valid_loss))
      model_save_name = "Malaria.pt"
      path = F"/content/drive/My Drive/{model_save_name}"
torch.save(model.state_dict(), path)
      valid_loss_min = valid_loss        
       
    print(f"Time per epoch: {(time.time() - start):.3f} seconds")
```

### Loading a Saved Model from Disk
```python=
model.load_state_dict(torch.load('Malaria.pt'))
```

### Test
```python=
def test(model, criterion):
# monitor test loss and accuracy
    test_loss = 0.
    correct = 0.
    total = 0.
for batch_idx, (data, target) in enumerate(test_loader):
        # move to GPU
        if torch.cuda.is_available():
            data, target = data.cuda(), target.cuda()
        # forward pass: compute predicted outputs by passing inputs to the model
        output = model(data)
        # calculate the loss
        loss = criterion(output, target)
        # update average test loss 
        test_loss = test_loss + ((1 / (batch_idx + 1)) * (loss.data - test_loss))
        # convert output probabilities to predicted class
        pred = output.data.max(1, keepdim=True)[1]
        # compare predictions to true label
        correct += np.sum(np.squeeze(pred.eq(target.data.view_as(pred))).cpu().numpy())
        total += data.size(0)
            
    print('Test Loss: {:.6f}\n'.format(test_loss))
print('\nTest Accuracy: %2d%% (%2d/%2d)' % (
        100. * correct / total, correct, total))
test(model, criterion)
Test Loss: 0.257728   Test Accuracy: 90% (2483/2756)
```

### Predict and Visualize
```python=
def load_input_image(img_path):    
    image = Image.open(img_path)
    prediction_transform = transforms.Compose([transforms.Resize(size=(224, 224)),
                                     transforms.ToTensor(), 
                                     transforms.Normalize([0.485, 0.456, 0.406], 
                                                          [0.229, 0.224, 0.225])])

    # discard the transparent, alpha channel (that's the :3) and add the batch dimension
    image = prediction_transform(image)[:3,:,:].unsqueeze(0)
    return image
    
def predict_malaria(model, class_names, img_path):
    # load the image and return the predicted breed
    img = load_input_image(img_path)
    model = model.cpu()
    model.eval()
    idx = torch.argmax(model(img))
    return class_names[idx]
    
from glob import glob
from PIL import Image
from termcolor import colored

class_names=['Parasitized','Uninfected']
inf = np.array(glob(img_dir + "/Parasitized/*"))
uninf = np.array(glob(img_dir + "/Uninfected/*"))
for i in range(3):
    img_path=inf[i]
    img = Image.open(img_path)
    if predict_malaria(model, class_names, img_path) == 'Parasitized':
        print(colored('Parasitized', 'green'))
    else:
        print(colored('Uninfected', 'red'))
    plt.imshow(img)
    plt.show()
for i in range(3):
    img_path=uninf[i]
    img = Image.open(img_path)
    if predict_malaria(model, class_names, img_path) == 'Uninfected':
        print(colored('Uninfected', 'green'))
    else:
        print(colored('Parasitized', 'red'))        
    plt.imshow(img)
    plt.show()
```

## Image Augmentation
- https://medium.com/analytics-vidhya/image-augmentation-for-deep-learning-using-pytorch-feature-engineering-for-images-3f4a64122614