This function optimizes w and b by running a gradient descent algorithm

    Arguments:
    w       weights, a numpy array of size (dim, 1)
    b       bias, a scalar
    X       data of shape (dim, number of examples)
    Y       true "label" vector, of shape (1, number of examples)
    num_iterations      number of iterations of the optimization loop
    learning_rate       learning rate of the gradient descent update rule
    print_cost          True to print the loss every 100 steps

    Returns:
    params      dictionary containing the weights w and bias b
    grads       dictionary containing the gradients of the weights and bias with respect to the cost function
    costs       list of all the costs computed during the optimization, this will be used to plot the learning curve.

def LR_train(w, b, X, Y, num_iterations, learning_rate, print_cost=True):
    """
    This function optimizes w and b by running a gradient descent algorithm

    Arguments:
    w       weights, a numpy array of size (dim, 1)
    b       bias, a scalar
    X       data of shape (dim, number of examples)
    Y       true "label" vector, of shape (1, number of examples)
    num_iterations      number of iterations of the optimization loop
    learning_rate       learning rate of the gradient descent update rule
    print_cost          True to print the loss every 100 steps

    Returns:
    params      dictionary containing the weights w and bias b
    grads       dictionary containing the gradients of the weights and bias with respect to the cost function
    costs       list of all the costs computed during the optimization, this will be used to plot the learning curve.
    """

    costs = []

    for i in range(num_iterations):

        # Cost and gradient calculation
        grads, cost = propagate(w, b, X, Y)

        # Retrieve derivatives from grads
        dw = grads["dw"]
        db = grads["db"]

        # update rule
        w = w - learning_rate * dw
        b = b - learning_rate * db

        # Record the costs
        if i % 100 == 0:
            costs.append(cost)

        # Print the cost every 100 training examples
        if print_cost and i % 100 == 0:
            print("Cost after iteration %i: %f" % (i, cost))

    params = {"w": w,
              "b": b}

    grads = {"dw": dw,
             "db": db}

    return params, grads, costs