In [2]:
import numpy as np

Module is an abstract class which defines fundamental methods necessary for a training a neural network. You do not need to change anything here, just read the comments.


In [3]:
class Module(object):
    """
    Basically, you can think of a module as of a something (black box) 
    which can process `input` data and produce `ouput` data.
    This is like applying a function which is called `forward`: 
        
        output = module.forward(input)
    
    The module should be able to perform a backward pass: to differentiate the `forward` function. 
    More, it should be able to differentiate it if is a part of chain (chain rule).
    The latter implies there is a gradient from previous step of a chain rule. 
    
        gradInput = module.backward(input, gradOutput)
    """
    def __init__ (self):
        self.output = None
        self.gradInput = None
        self.training = True
    
    def forward(self, input):
        """
        Takes an input object, and computes the corresponding output of the module.
        """
        return self.updateOutput(input)

    def backward(self,input, gradOutput):
        """
        Performs a backpropagation step through the module, with respect to the given input.
        
        This includes 
         - computing a gradient w.r.t. `input` (is needed for further backprop),
         - computing a gradient w.r.t. parameters (to update parameters while optimizing).
        """
        self.updateGradInput(input, gradOutput)
        self.accGradParameters(input, gradOutput)
        return self.gradInput
    

    def updateOutput(self, input):
        """
        Computes the output using the current parameter set of the class and input.
        This function returns the result which is stored in the `output` field.
        
        Make sure to both store the data in `output` field and return it. 
        """
        
        # The easiest case:
            
        # self.output = input 
        # return self.output
        
        pass

    def updateGradInput(self, input, gradOutput):
        """
        Computing the gradient of the module with respect to its own input. 
        This is returned in `gradInput`. Also, the `gradInput` state variable is updated accordingly.
        
        The shape of `gradInput` is always the same as the shape of `input`.
        
        Make sure to both store the gradients in `gradInput` field and return it.
        """
        
        # The easiest case:
        
        # self.gradInput = gradOutput 
        # return self.gradInput
        
        pass   
    
    def accGradParameters(self, input, gradOutput):
        """
        Computing the gradient of the module with respect to its own parameters.
        No need to override if module has no parameters (e.g. ReLU).
        """
        pass
    
    def zeroGradParameters(self): 
        """
        Zeroes `gradParams` variable if the module has params.
        """
        pass
        
    def getParameters(self):
        """
        Returns a list with its parameters. 
        If the module does not have parameters return empty list. 
        """
        return []
        
    def getGradParameters(self):
        """
        Returns a list with gradients with respect to its parameters. 
        If the module does not have parameters return empty list. 
        """
        return []
    
    def train(self):
        """
        Sets training mode for the module.
        Training and testing behaviour differs for Dropout, BatchNorm.
        """
        self.training = True
    
    def evaluate(self):
        """
        Sets evaluation mode for the module.
        Training and testing behaviour differs for Dropout, BatchNorm.
        """
        self.training = False
    
    def __repr__(self):
        """
        Pretty printing. Should be overrided in every module if you want 
        to have readable description. 
        """
        return "Module"

Sequential container

Define a forward and backward pass procedures.


In [4]:
class Sequential(Module):
    """
         This class implements a container, which processes `input` data sequentially. 
         
         `input` is processed by each module (layer) in self.modules consecutively.
         The resulting array is called `output`. 
    """
    
    def __init__ (self):
        super(Sequential, self).__init__()
        self.modules = []
        self.inputs = []
   
    def add(self, module):
        """
        Adds a module to the container.
        """
        self.modules.append(module)

    def updateOutput(self, input):
        """
        Basic workflow of FORWARD PASS:
        
            y_0    = module[0].forward(input)
            y_1    = module[1].forward(y_0)
            ...
            output = module[n-1].forward(y_{n-2})   
            
            
        Just write a little loop. 
        """
        # Your code goes here. ################################################
        self.inputs = []
        
        for module in self.modules:
            output = module.forward(input)
            input = output
            self.inputs.append(input)
        self.output = output
        del self.inputs[-1]

        return self.output

    def backward(self, input, gradOutput):
        """
        Workflow of BACKWARD PASS:
            
            g_{n-1} = module[n-1].backward(y_{n-2}, gradOutput)
            g_{n-2} = module[n-2].backward(y_{n-3}, g_{n-1})
            ...
            g_1 = module[1].backward(y_0, g_2)   
            gradInput = module[0].backward(input, g_1)   
             
             
        !!!
                
        To each module you need to provide the input, module saw while forward pass, 
        it is used while computing gradients. 
        Make sure that the input for `i-th` layer the output of `module[i]` (just the same input as in forward pass) 
        and NOT `input` to this Sequential module. 
        
        !!!
        
        """
        # Your code goes here. ################################################
        inputs = [input] + self.inputs.copy();
        
        for module, forward_input in zip(reversed(self.modules), reversed(inputs)):
            gradInput = module.backward(forward_input, gradOutput)
            gradOutput = gradInput
        self.gradInput = gradInput
        
        return self.gradInput
      

    def zeroGradParameters(self): 
        for module in self.modules:
            module.zeroGradParameters()
    
    def getParameters(self):
        """
        Should gather all parameters in a list.
        """
        return [x.getParameters() for x in self.modules]
    
    def getGradParameters(self):
        """
        Should gather all gradients w.r.t parameters in a list.
        """
        return [x.getGradParameters() for x in self.modules]
    
    def __repr__(self):
        string = "".join([str(x) + '\n' for x in self.modules])
        return string
    
    def __getitem__(self,x):
        return self.modules.__getitem__(x)
    
    def train(self):
        """
        Propagates training parameter through all modules
        """
        self.training = True
        for module in self.modules:
            module.train()
    
    def evaluate(self):
        """
        Propagates training parameter through all modules
        """
        self.training = False
        for module in self.modules:
            module.evaluate()

Layers

1. Linear transform layer

Also known as dense layer, fully-connected layer, FC-layer, InnerProductLayer (in caffe), affine transform

  • input: batch_size x n_feats1
  • output: batch_size x n_feats2

In [197]:
class Linear(Module):
    """
    A module which applies a linear transformation 
    A common name is fully-connected layer, InnerProductLayer in caffe. 
    
    The module should work with 2D input of shape (n_samples, n_feature).
    """
    def __init__(self, n_in, n_out):
        super(Linear, self).__init__()
       
        # This is a nice initialization
        stdv = 1./np.sqrt(n_in)
        self.W = np.random.uniform(-stdv, stdv, size = (n_out, n_in))
        self.b = np.random.uniform(-stdv, stdv, size = n_out)
        
        self.gradW = np.zeros_like(self.W)
        self.gradb = np.zeros_like(self.b)
        
    def updateOutput(self, input):
        # Your code goes here. ################################################
        self.output = np.add(np.dot(input, self.W.T), self.b)
        return self.output
    
    def updateGradInput(self, input, gradOutput):
        # Your code goes here. ################################################
        self.gradInput = np.dot(gradOutput, self.W)
        return self.gradInput
    
    def accGradParameters(self, input, gradOutput):
        # Your code goes here. ################################################
        self.gradW = np.dot( gradOutput.T, input)
        self.gradb = np.sum(gradOutput, axis=0)
    
    def zeroGradParameters(self):
        self.gradW.fill(0)
        self.gradb.fill(0)
        
    def getParameters(self):
        return [self.W, self.b]
    
    def getGradParameters(self):
        return [self.gradW, self.gradb]
    
    def __repr__(self):
        s = self.W.shape
        q = 'Linear %d -> %d' %(s[1],s[0])
        return q

2. SoftMax

  • input: batch_size x n_feats
  • output: batch_size x n_feats

$\text{softmax}(x)_i = \frac{\exp x_i} {\sum_j \exp x_j}$

Recall that $\text{softmax}(x) == \text{softmax}(x - \text{const})$. It makes possible to avoid computing exp() from large argument.


In [7]:
class SoftMax(Module):
    def __init__(self):
         super(SoftMax, self).__init__()
    
    def updateOutput(self, input):
        # start with normalization for numerical stability
        self.output = np.subtract(input, input.max(axis=1, keepdims=True))
        # Your code goes here. ################################################
        self.output = np.exp(self.output)/np.sum(np.exp(self.output), axis=1, keepdims=True)
        return self.output
    
    def updateGradInput(self, input, gradOutput):
        # Your code goes here. ################################################
        pred_grad = np.multiply(self.output, gradOutput)
        self.gradInput = pred_grad - np.multiply(self.output, np.sum(pred_grad, axis=1, keepdims=True))
        return self.gradInput
    
    def __repr__(self):
        return "SoftMax"

3. LogSoftMax

  • input: batch_size x n_feats
  • output: batch_size x n_feats

$\text{logsoftmax}(x)_i = \log\text{softmax}(x)_i = x_i - \log {\sum_j \exp x_j}$

The main goal of this layer is to be used in computation of log-likelihood loss.


In [138]:
class LogSoftMax(Module):
    def __init__(self):
         super(LogSoftMax, self).__init__()
    
    def updateOutput(self, input):
        # start with normalization for numerical stability
        self.output = np.subtract(input, input.max(axis=1, keepdims=True))
        self.output = self.output - np.log(np.sum(np.exp(self.output), axis=1, keepdims=True))
        # Your code goes here. ################################################
        return self.output
    
    def updateGradInput(self, input, gradOutput):
        # Your code goes here. ################################################
        input_new_exp = np.exp(np.subtract(input, input.max(axis=1, keepdims=True)))
        self.gradInput = gradOutput
        self.gradInput -= np.multiply(input_new_exp/np.sum(input_new_exp, axis=1, keepdims=True),
                                      np.sum(gradOutput, axis=1, keepdims=True))
        return self.gradInput
    
    def __repr__(self):
        return "LogSoftMax"

4. Batch normalization

One of the most significant recent ideas that impacted NNs a lot is Batch normalization. The idea is simple, yet effective: the features should be whitened ($mean = 0$, $std = 1$) all the way through NN. This improves the convergence for deep models letting it train them for days but not weeks. You are to implement the first part of the layer: features normalization. The second part (ChannelwiseScaling layer) is implemented below.

  • input: batch_size x n_feats
  • output: batch_size x n_feats

The layer should work as follows. While training (self.training == True) it transforms input as $$y = \frac{x - \mu} {\sqrt{\sigma + \epsilon}}$$ where $\mu$ and $\sigma$ - mean and variance of feature values in batch and $\epsilon$ is just a small number for numericall stability. Also during training, layer should maintain exponential moving average values for mean and variance:

    self.moving_mean = self.moving_mean * alpha + batch_mean * (1 - alpha)
    self.moving_variance = self.moving_variance * alpha + batch_variance * (1 - alpha)

During testing (self.training == False) the layer normalizes input using moving_mean and moving_variance.

Note that decomposition of batch normalization on normalization itself and channelwise scaling here is just a common implementation choice. In general "batch normalization" always assumes normalization + scaling.


In [177]:
class BatchNormalization(Module):
    EPS = 1e-3
    def __init__(self, alpha = 0.):
        super(BatchNormalization, self).__init__()
        self.alpha = alpha
        self.moving_mean = 0 
        self.moving_variance = 0
        
    def updateOutput(self, input):
        # Your code goes here. ################################################
        # use self.EPS please
        if (self.training == True):
            self.mean = np.mean(input, axis=0, keepdims=True)
            self.var = np.var(input, axis=0, keepdims=True)
            self.moving_mean = self.moving_mean * self.alpha + self.mean * (1 - self.alpha)
            self.moving_variance = self.moving_variance * self.alpha + self.var * (1 - self.alpha)
            self.output = (input - self.mean)/np.sqrt(self.var + self.EPS)
        else:
            self.output = (input - self.moving_mean) / np.sqrt(self.moving_variance + self.EPS)
            
        return self.output
    
    def updateGradInput(self, input, gradOutput):
        # Your code goes here. ################################################
        self.gradInput = input.shape[0] * gradOutput
        self.gradInput -= np.sum(gradOutput, axis=0, keepdims=True)
        self.gradInput -= self.output * np.sum(gradOutput * self.output, axis=0, keepdims=True)
        self.gradInput = 1/(input.shape[0] * np.sqrt(self.var + self.EPS)) * self.gradInput
        
        return self.gradInput
    
    def __repr__(self):
        return "BatchNormalization"

In [207]:
class ChannelwiseScaling(Module):
    """
       Implements linear transform of input y = \gamma * x + \beta
       where \gamma, \beta - learnable vectors of length x.shape[-1]
    """
    def __init__(self, n_out):
        super(ChannelwiseScaling, self).__init__()

        stdv = 1./np.sqrt(n_out)
        self.gamma = 1 #np.random.uniform(-stdv, stdv, size=n_out) # ACHTUNG!! 1
        self.beta = 0 #0 #np.random.uniform(-stdv, stdv, size=n_out) # 0
        
        self.gradGamma = np.zeros_like(self.gamma)
        self.gradBeta = np.zeros_like(self.beta)

    def updateOutput(self, input):
        self.output = input * self.gamma + self.beta
        return self.output
        
    def updateGradInput(self, input, gradOutput):
        self.gradInput = gradOutput * self.gamma
        return self.gradInput
    
    def accGradParameters(self, input, gradOutput):
        self.gradBeta = np.sum(gradOutput, axis=0)
        self.gradGamma = np.sum(gradOutput*input, axis=0)
    
    def zeroGradParameters(self):
        self.gradGamma.fill(0)
        self.gradBeta.fill(0)
        
    def getParameters(self):
        return [self.gamma, self.beta]
    
    def getGradParameters(self):
        return [self.gradGamma, self.gradBeta]
    
    def __repr__(self):
        return "ChannelwiseScaling"

Practical notes. If BatchNormalization is placed after a linear transformation layer (including dense layer, convolutions, channelwise scaling) that implements function like y = weight * x + bias, than bias adding become useless and could be omitted since its effect will be discarded while batch mean subtraction. If BatchNormalization (followed by ChannelwiseScaling) is placed before a layer that propagates scale (including ReLU, LeakyReLU) followed by any linear transformation layer than parameter gamma in ChannelwiseScaling could be freezed since it could be absorbed into the linear transformation layer.

5. Dropout

Implement dropout. The idea and implementation is really simple: just multimply the input by $Bernoulli(p)$ mask. Here $p$ is probability of an element to be zeroed.

This has proven to be an effective technique for regularization and preventing the co-adaptation of neurons.

While training (self.training == True) it should sample a mask on each iteration (for every batch), zero out elements and multiply elements by $1 / (1 - p)$. The latter is needed for keeping mean values of features close to mean values which will be in test mode. When testing this module should implement identity transform i.e. self.output = input.

  • input: batch_size x n_feats
  • output: batch_size x n_feats

In [215]:
class Dropout(Module):
    def __init__(self, p=0.5):
        super(Dropout, self).__init__()
        self.p = p
        self.mask = None
        
    def updateOutput(self, input):
        # Your code goes here. ################################################
        if (self.training == True):
            if (self.p == 0.0):
                self.mask = np.ones_like(input)
            else:
                self.mask = np.random.binomial(1, 1 - self.p, input.shape)
            self.output = np.multiply(input, self.mask) / (1 - self.p)
        else:
            self.output = input
        return self.output
    
    def updateGradInput(self, input, gradOutput):
        # Your code goes here. ################################################
        self.gradInput = gradOutput * self.mask / (1 - self.p)
        return self.gradInput
        
    def __repr__(self):
        return "Dropout"

Activation functions

Here's the complete example for the Rectified Linear Unit non-linearity (aka ReLU):


In [211]:
class ReLU(Module):
    def __init__(self):
         super(ReLU, self).__init__()
    
    def updateOutput(self, input):
        self.output = np.maximum(input, 0)
        return self.output
    
    def updateGradInput(self, input, gradOutput):
        self.gradInput = np.multiply(gradOutput, input > 0)
        return self.gradInput
    
    def __repr__(self):
        return "ReLU"

6. Leaky ReLU

Implement Leaky Rectified Linear Unit. Expriment with slope.


In [210]:
class LeakyReLU(Module):
    def __init__(self, slope = 0.03):
        super(LeakyReLU, self).__init__()
        self.slope = slope
        
    def updateOutput(self, input):
        # Your code goes here. ################################################
        self.output = np.where(input <= 0, self.slope * input, input)
        return self.output
    
    def updateGradInput(self, input, gradOutput):
        # Your code goes here. ################################################
        input_copy = np.where(input > 0, 1, input)
        input_copy = np.where(input_copy < 0, self.slope, input_copy)
        self.gradInput = np.multiply(gradOutput, input_copy)
        return self.gradInput
    
    def __repr__(self):
        return "LeakyReLU"

7. ELU

Implement Exponential Linear Units activations.


In [214]:
class ELU(Module):
    def __init__(self, alpha = 1.0):
        super(ELU, self).__init__()
        
        self.alpha = alpha
        
    def updateOutput(self, input):
        # Your code goes here. ################################################
        self.output = np.where(input <= 0, self.alpha * (np.exp(input) - 1), input)
        return self.output
    
    def updateGradInput(self, input, gradOutput):
        # Your code goes here. ################################################
        input_copy = np.where(input > 0, 1, input)
        input_copy = np.where(input_copy <= 0, self.alpha * np.exp(input_copy), input_copy)
        self.gradInput = np.multiply(input_copy, gradOutput)
        return self.gradInput
    
    def __repr__(self):
        return "ELU"

8. SoftPlus

Implement SoftPlus activations. Look, how they look a lot like ReLU.


In [68]:
class SoftPlus(Module):
    def __init__(self):
        super(SoftPlus, self).__init__()
    
    def updateOutput(self, input):
        # Your code goes here. ################################################
        self.output = np.log(1 + np.exp(input))
        return  self.output
    
    def updateGradInput(self, input, gradOutput):
        # Your code goes here. ################################################
        self.gradInput = np.exp(input)/(1 + np.exp(input)) * gradOutput
        return self.gradInput
    
    def __repr__(self):
        return "SoftPlus"

Criterions

Criterions are used to score the models answers.


In [1]:
class Criterion(object):
    def __init__ (self):
        self.output = None
        self.gradInput = None
        
    def forward(self, input, target):
        """
            Given an input and a target, compute the loss function 
            associated to the criterion and return the result.
            
            For consistency this function should not be overrided,
            all the code goes in `updateOutput`.
        """
        return self.updateOutput(input, target)

    def backward(self, input, target):
        """
            Given an input and a target, compute the gradients of the loss function
            associated to the criterion and return the result. 

            For consistency this function should not be overrided,
            all the code goes in `updateGradInput`.
        """
        return self.updateGradInput(input, target)
    
    def updateOutput(self, input, target):
        """
        Function to override.
        """
        return self.output

    def updateGradInput(self, input, target):
        """
        Function to override.
        """
        return self.gradInput   

    def __repr__(self):
        """
        Pretty printing. Should be overrided in every module if you want 
        to have readable description. 
        """
        return "Criterion"

The MSECriterion, which is basic L2 norm usually used for regression, is implemented here for you.

  • input: batch_size x n_feats
  • target: batch_size x n_feats
  • output: scalar

In [71]:
class MSECriterion(Criterion):
    def __init__(self):
        super(MSECriterion, self).__init__()
        
    def updateOutput(self, input, target):   
        self.output = np.sum(np.power(input - target,2)) / input.shape[0]
        return self.output 
 
    def updateGradInput(self, input, target):
        self.gradInput  = (input - target) * 2 / input.shape[0]
        return self.gradInput

    def __repr__(self):
        return "MSECriterion"

9. Negative LogLikelihood criterion (numerically unstable)

You task is to implement the ClassNLLCriterion. It should implement multiclass log loss. Nevertheless there is a sum over y (target) in that formula, remember that targets are one-hot encoded. This fact simplifies the computations a lot. Note, that criterions are the only places, where you divide by batch size. Also there is a small hack with adding small number to probabilities to avoid computing log(0).

  • input: batch_size x n_feats - probabilities
  • target: batch_size x n_feats - one-hot representation of ground truth
  • output: scalar

In [143]:
class ClassNLLCriterionUnstable(Criterion):
    EPS = 1e-15
    def __init__(self):
        a = super(ClassNLLCriterionUnstable, self)
        super(ClassNLLCriterionUnstable, self).__init__()
        
    def updateOutput(self, input, target): 
        
        # Use this trick to avoid numerical errors
        input_clamp = np.clip(input, self.EPS, 1 - self.EPS)
        
        # Your code goes here. ################################################
        self.output = - np.sum(np.multiply(target, np.log(input_clamp)))/input.shape[0]
        return self.output

    def updateGradInput(self, input, target):
        
        # Use this trick to avoid numerical errors
        input_clamp = np.clip(input, self.EPS, 1 - self.EPS)
                
        # Your code goes here. ################################################
        self.gradInput = - (target / input_clamp)/input.shape[0]
        return self.gradInput
    
    def __repr__(self):
        return "ClassNLLCriterionUnstable"

10. Negative LogLikelihood criterion (numerically stable)

  • input: batch_size x n_feats - log probabilities
  • target: batch_size x n_feats - one-hot representation of ground truth
  • output: scalar

Task is similar to the previous one, but now the criterion input is the output of log-softmax layer. This decomposition allows us to avoid problems with computation of forward and backward of log().


In [6]:
class ClassNLLCriterion(Criterion):
    def __init__(self):
        a = super(ClassNLLCriterion, self)
        super(ClassNLLCriterion, self).__init__()
        
    def updateOutput(self, input, target): 
        # Your code goes here. ################################################
        self.output = - np.sum(np.multiply(target, input))/input.shape[0]
        return self.output

    def updateGradInput(self, input, target):
        # Your code goes here. ################################################
        self.gradInput = - target/input.shape[0]
        return self.gradInput
    
    def __repr__(self):
        return "ClassNLLCriterion"

11. Contrastive criterion

  • input: batch_size x n_feats - embedding vectors (can be outputs of any layer, but usually the last one before the classification linear layer is used)
  • target: labels - ground truth class indices (not one-hot ecodings)
  • output: scalar

The contrastive loss pulls the examples of the same class closer (in terms of the embedding layer) and pushes the examples of different classes from each other.

This is the formula for the contrastive loss in the terms of embedding pairs: $$L_c(x_i, x_j, label_i, label_j) = \begin{cases}\frac{1}{2N_{pos}} \| x_i - x_j \|^{2}_{2} & label_i = label_j\\ \frac{1}{2N_{neg}} (\max(0, M - \| x_i - x_j \|_{2}))^2 & label_i \neq label_j \end{cases}$$ Where $M$ is a hyperparameter (constant), $N_{pos}$ and $N_{neg}$ - number of 'positive' (examples of the same class) and 'negative' pairs (examples of different classes).

You should generate all the possible pairs in the batch (ensure that your batch size is > 10, so that there are positive pairs), but remember to be efficient and use vectorize opetations, hint: https://medium.com/dataholiks-distillery/l2-distance-matrix-vectorization-trick-26aa3247ac6c. Then compute the Euclidean distances for them, and finally compute the loss value.

When computing the gradients with respect to inputs, you should (as always) use the chain rule: compute the loss gradients w.r.t distances, compute the distance gradients w.r.t input features (each example may take part in different pairs).


In [12]:
class ClassContrastiveCriterion(Criterion):
    def __init__(self, M):
        a = super(ClassContrastiveCriterion, self)
        super(ClassContrastiveCriterion, self).__init__()
        self.M = M #margin for punishing the negative pairs
        
    def updateOutput(self, input, target): 
        # Your code goes here. ################################################
        # Taken from the website given
        def compute_distances_no_loops(X):
            dists = -2 * np.dot(X, X.T) + np.sum(X**2, axis=1) + \
                    np.sum(X**2, axis=1)[:, np.newaxis]
            return dists
        
        # Calculate N_{neg} and N_{pos}
        target_aug = (target[np.newaxis, :].T == target)
        target_aug_neg = target_aug ^ np.eye(target.shape[0]).astype('bool')
        Np = target_aug_neg.sum()
        Nn = (target.shape[0] * (target.shape[0] - 1)) - Np
        # Calculate distances
        dist = compute_distances_no_loops(input)
        loss = ~target_aug * 1/(2*Nn) * (np.maximum(0., self.M - np.sqrt(np.abs(dist)))**2)
        loss = loss + target_aug_neg * dist / (2*Np)
        self.output = loss.sum()
        return self.output

    def updateGradInput(self, input, target):
        # Your code goes here. ################################################   
        def compute_distances_no_loops(X):
            dists = X - X[:, np.newaxis]
            norms = np.linalg.norm(dists, axis=-1)[:,:,np.newaxis]
            norms = np.where(norms == 0, 1.0, norms)
            ndist = dists / norms
            return dists, norms, ndist
        
        dist, norms, ndist = compute_distances_no_loops(input)
        
        target_aug = target[np.newaxis, :]
        target_aug = (target_aug.T == target)
        target_aug_neg = target_aug ^ np.eye(target.shape[0]).astype('bool')
        Np = target_aug_neg.sum()
        Nn = (target.shape[0] * (target.shape[0] - 1)) - Np
        
        loss_grad =  - 2/Nn * ( ~target_aug[:,:,np.newaxis] * (np.maximum(0, self.M - norms) * ndist)).sum(axis=0)
        loss_grad += 2/Np * (target_aug_neg[:,:,np.newaxis] * dist).sum(axis=0)

        self.gradInput = loss_grad
        return self.gradInput
    
    def __repr__(self):
        return "ClassContrastiveCriterion"

Optimizers

SGD optimizer with momentum

  • variables - list of lists of variables (one list per layer)
  • gradients - list of lists of current gradients (same structure as for variables, one array for each var)
  • config - dict with optimization parameters (learning_rate and momentum)
  • state - dict with optimizator state (used to save accumulated gradients)

In [196]:
def sgd_momentum(variables, gradients, config, state):  
    # 'variables' and 'gradients' have complex structure, accumulated_grads will be stored in a simpler one
    state.setdefault('accumulated_grads', {})
    
    var_index = 0 
    for current_layer_vars, current_layer_grads in zip(variables, gradients): 
        for current_var, current_grad in zip(current_layer_vars, current_layer_grads):
            
            old_grad = state['accumulated_grads'].setdefault(var_index, np.zeros_like(current_grad))
            
            np.add(config['momentum'] * old_grad, config['learning_rate'] * current_grad, out=old_grad)
            current_var -= old_grad
            var_index += 1

12. Adam optimizer

  • variables - list of lists of variables (one list per layer)
  • gradients - list of lists of current gradients (same structure as for variables, one array for each var)
  • config - dict with optimization parameters (learning_rate, beta1, beta2, epsilon)
  • state - dict with optimizator state (used to save 1st and 2nd moment for vars)

Formulas for optimizer:

Current step learning rate: $$\text{lr}_t = \text{learning_rate} * \frac{\sqrt{1-\beta_2^t}} {1-\beta_1^t}$$ First moment of var: $$\mu_t = \beta_1 * \mu_{t-1} + (1 - \beta_1)*g$$ Second moment of var: $$v_t = \beta_2 * v_{t-1} + (1 - \beta_2)*g*g$$ New values of var: $$\text{variable} = \text{variable} - \text{lr}_t * \frac{\mu_t}{\sqrt{v_t} + \epsilon}$$


In [141]:
def adam_optimizer(variables, gradients, config, state):  
    # 'variables' and 'gradients' have complex structure, accumulated_grads will be stored in a simpler one
    state.setdefault('m', {})  # first moment vars
    state.setdefault('v', {})  # second moment vars
    state.setdefault('t', 0)   # timestamp
    state['t'] += 1
    for k in ['learning_rate', 'beta1', 'beta2', 'epsilon']:
        assert k in config, config.keys()
    
    var_index = 0 
    lr_t = config['learning_rate'] * np.sqrt(1 - config['beta2']**state['t']) / (1 - config['beta1']**state['t'])
    for current_layer_vars, current_layer_grads in zip(variables, gradients): 
        for current_var, current_grad in zip(current_layer_vars, current_layer_grads):
            var_first_moment = state['m'].setdefault(var_index, np.zeros_like(current_grad))
            var_second_moment = state['v'].setdefault(var_index, np.zeros_like(current_grad))
            
            # <YOUR CODE> #######################################
            # update `current_var_first_moment`, `var_second_moment` and `current_var` values
            np.add(config['beta1'] * var_first_moment, (1 - config['beta1']) * current_grad, out=var_first_moment)
            np.add(config['beta2'] * var_second_moment, (1 - config['beta2']) * np.multiply(current_grad, current_grad), out=var_second_moment)
            current_var -= lr_t * var_first_moment / (np.sqrt(var_second_moment) + config['epsilon'])
            
            # small checks that you've updated the state; use np.add for rewriting np.arrays values
            assert var_first_moment is state['m'].get(var_index)
            assert var_second_moment is state['v'].get(var_index)
            var_index += 1

Flatten layer

Just reshapes inputs and gradients. It's usually used as proxy layer between Conv2d and Linear.


In [35]:
class Flatten(Module):
    def __init__(self):
         super(Flatten, self).__init__()
    
    def updateOutput(self, input):
        self.output = input.reshape(len(input), -1)
        return self.output
    
    def updateGradInput(self, input, gradOutput):
        self.gradInput = gradOutput.reshape(input.shape)
        return self.gradInput
    
    def __repr__(self):
        return "Flatten"

In [ ]: