Why gradient check gives high difference (almost 1)?

Viewed 251

I'm trying to implement a Neural Net in python without the use of libraries like Keras or Tensorflow. I still have to test the net, right now I just tried to train it on Iris dataset and check afterwards the correctness of the backpropagation algorithm.

To do so, I wrote the gradient checking procedure, calculating the analytical gradients and comparing them with the gradients from backpropagation.

The point is that, even if the backpropagation algorithm seems correct to me, the difference between the gradients is always high (around 0.8, instead of the classic 1e-7).

Layer class

class Dense(Layer):
    def __init__(self, input_shape, name=None, activation='relu', regularization='l2'):
        self.name = name
        self.is_output = False
        self.weights = np.random.uniform(low=0.01, high=0.10, size=input_shape)
        self.biases = np.ones((1,input_shape[1]))
        
        if activation == 'sigmoid':
            self.activation = Activation_Sigmoid()
        else: #activation == 'relu':
            self.activation = Activation_ReLU()
        self.cost = Categorical_CrossEntropyLoss()

    def set_as_output(self, is_output=True):
        self.is_output = is_output

    def forward(self, inputs, debug=False, epsilon=None):
        self.net_input = inputs
        if debug:
            augmented_parameters = np.zeros(epsilon.shape)
            weights_column_vector = np.reshape(self.weights,(-1,1))
            biases_column_vector = np.reshape(self.biases,(-1,1))
            concatenated_parameters = np.concatenate((weights_column_vector, biases_column_vector))
            for i in range(concatenated_parameters.shape[0]):
                augmented_parameters[i] = concatenated_parameters[i]
            # make the augmented parameter long as theta in order to sum them
            # this because epsilon is a standard basis vector
            augmented_parameters += epsilon
            # rebuild the weights matrix and biases vector to apply forward propagation
            weights_end = self.weights.shape[0] * self.weights.shape[1]
            biases_end = self.biases.shape[0] * self.biases.shape[1] + weights_end
            weights = np.reshape(augmented_parameters[0:weights_end],self.weights.shape)
            biases = np.reshape(augmented_parameters[weights_end:biases_end], self.biases.shape)
            output = np.dot(inputs, weights) + biases
            activated_output = self.activation.forward(output)
            return activated_output
        self.output = np.dot(inputs, self.weights) + self.biases
        self.activated_output = self.activation.forward(self.output)
        return self.activated_output

    def backward(self, X, y, output, step, l2=0.5): #backpropagation
        m = X.shape[0] # number of examples
        if self.is_output:
            error = self.cost.backward(output, y) #(a_k - y_hat_k)
            delta_k = self.activation.backward(self.output)* error
            # net input for neuron k is a_j^(l-1)
            grad = np.dot(self.net_input.T, delta_k)
            #update weights with l2 regularization
            self.grad_w = grad + (l2 / m)*self.weights
            self.grad_b = np.sum(delta_k * 1,axis=0)
            self.weights -= step * self.grad_w 
            self.biases -= step * self.grad_b
            return np.dot(delta_k ,self.weights.T)
        else:
            delta_j = self.activation.backward(self.output) * output
            grad = np.dot(self.net_input.T, delta_j)
            self.grad_w = grad + (l2 / m) * self.weights
            self.grad_b = np.sum(delta_j * 1, axis=0)
            self.weights -= step * self.grad_w
            self.biases -= step * self.grad_b
            return np.dot(delta_j, self.weights.T)
    
    def get_parameters(self):
        return self.weights, self.biases

    def get_gradients(self):
        return self.grad_w, self.grad_b

Neural Net class

class NeuralNet():
    def __init__(self):
        self.layers = []
        self.layers_output = []
        self.cost = None
        self.regularization = L2_Regularization()

    def add(self,layer):
        self.layers.append(layer)

    def forward(self, inputs, debug=False, epsilon=None):
        input = np.copy(inputs)
        for layer in self.layers:
            output = layer.forward(input, debug=debug, epsilon=epsilon)
            input = output
        return input

    def backward(self, X, y, output, step):
        prev_delta = None
        out = output
        for layer in self.layers[::-1]:
            prev_delta = layer.backward(X, y, out, step)
            out = prev_delta

    def fit(self, X, y, batch_size=1, epochs=10, step=0.05, shuffle=True):
        self.layers[-1].set_as_output()
        self.error = []
        i = 0.005 * epochs
        for epoch in range(epochs):
            if shuffle:
                X = np.random.permutation(X)
            batches = int(np.ceil(X.shape[0]/batch_size))
            batches_error = []
            for t in range(batches):
                batch_X = X[t*batch_size:np.min([X.shape[0],(t+1)*batch_size]),:]
                batch_y = y[t*batch_size:np.min([y.shape[0],(t+1)*batch_size]),:]
                output = self.forward(batch_X)

                cost = self.cost.forward(output,batch_y)
                cost += self.regularization.forward(X, self.layers)
                batches_error.append(cost)
                
                self.backward(batch_X, batch_y, output, step)
            self.error.append(np.mean(batches_error))
            if epoch % i == 0:
                    print('epoch:', epoch, 'error:', np.mean(self.error))
        return self

    def parameters_to_theta(self):        
        theta = []
        for layer in self.layers:
            w, b = layer.get_parameters()
            #flatten parameter w
            new_vector = np.reshape(w, (-1,1))
            theta.append(new_vector)

            #flatten parameter b
            new_vector = np.reshape(b, (-1,1))
            theta.append(new_vector)

        return np.vstack(theta)

    def gradients_to_theta(self):
        theta = []
        for layer in self.layers:
            grad_w, grad_b = layer.get_gradients()
            new_vector = np.reshape(grad_w, (-1,1))
            theta.append(new_vector)

            new_vector = np.reshape(grad_b, (-1,1))
            theta.append(new_vector)

        return np.vstack(theta)

    def gradient_check(self, X, y, epsilon=1e-7):
        theta = self.parameters_to_theta()
        dtheta = self.gradients_to_theta()

        num_parameters = theta.shape[0]
        J_plus = np.zeros((num_parameters, 1))
        J_minus = np.zeros((num_parameters, 1))
        dtheta_approx = np.zeros((num_parameters, 1))

        for i in range(num_parameters):
            theta_plus = np.zeros((num_parameters,1))
            theta_plus[i] = epsilon
            J_plus[i] = self.cost.forward(self.forward(X, debug=True, epsilon=theta_plus),y)
            
            theta_minus = np.zeros((num_parameters,1))
            theta_minus[i] = - epsilon
            J_minus[i] = self.cost.forward(self.forward(X, debug=True, epsilon=theta_minus),y)
            
            dtheta_approx[i] = (J_plus[i] - J_minus[i])/ (2 * epsilon)
            
        numerator = np.linalg.norm(dtheta - dtheta_approx)
        denominator = np.linalg.norm(dtheta_approx) + np.linalg.norm(dtheta)
        difference = numerator / denominator
        return difference

I'm using ReLU and Sigmoid as activation functions, and Categorical Cross Entropy for the cost

import numpy as np
from scipy.special import expit as sigmoid

class Activation_ReLU:
    def forward(self, inputs):
        return np.maximum(0, inputs)
    def backward(self, inputs):
        return np.greater(inputs,0).astype(int)

class Activation_Sigmoid:
    def forward(self, inputs):
        return sigmoid(inputs)
    def backward(self, inputs):
        return sigmoid(inputs) * (1 - sigmoid(inputs))

class Categorical_CrossEntropyLoss():
    def forward(self, y_pred, y_real):
        predictions = np.copy(y_pred)
        predictions = np.clip(predictions, 1e-12, 1 - 1e-12) # avoid zero values for log
        n = y_real.shape[0]
        return - (1 / n) * np.sum(y_real * np.log(y_pred))
    def backward(self, y_pred, y_real):
        return y_real - y_pred

These are the main classes that define the net. The model that I create to train on Iris dataset is a NN with 1 hidden layer.

# random seed is 1

X, y = load_iris(return_X_y=True)

X = (X - np.mean(X)) / np.std(X) # standardize data to improve network convergence

y = y.reshape((-1,1))
encoder = OneHotEncoder(sparse=False)
y = encoder.fit_transform(y)

X_train, X_test, y_train, y_test = train_test_split(X,y,train_size=0.8)

model = NeuralNet()
model.add(Dense((4,10),name='input_layer',activation='relu'))
model.add(Dense((10,10),name='hidden_layer',activation='relu'))
model.add(Dense((10,3),name='output_layer',activation='sigmoid'))

model.fit(X_train,y_train, batch_size=5, epochs=200, step=1e-3)
difference = model.gradient_check(X_train, y_train)

And then, the result of print(difference) is

0.7992920544491866

So there is something wrong with my implementation. What things I have to check to determine the causes of this high difference between gradients?

0 Answers
Related