Search code examples
python-2.7exceptionargskeyword-argumentgradient-descent

Removing infinity values of a function using exception handling, *args, and **kwargs


I'm currently working through the book Data Science from Scratch by Joel Grus, and I've run across a function that I don't really understand:

def safe(f):
    def safe_f(*args, **kwargs):
        try:
            return f(*args, **kwargs)
        except:
            return float('inf')
    return safe_f

The safe function is called within a Gradient Descent algorithm to remove infinity values.

def minimize_batch(target_fn, gradient_fn, theta_0, tolerance=0.000001):
    step_sizes = [100, 10, 1, 0.1, 0.001, 0.0001, 0.00001]

    theta = theta_0
    target_fn = safe(target_fn)
    value = target_fn(theta)

    while True:
        gradient = gradient_fn(theta)
        next_thetas = [step(theta, gradient, -step_size) for step_size in     step_sizes]
        next_theta = min(next_thetas, key=target_fn)
        next_value = target_fn(next_theta)

        if abs(value - next_value) < tolerance:
            return theta
        else:
            theta, value = next_theta, next_value

I get the gist of what safe is doing, but I don't understand how it's doing it. For example, how does safe evaluate target_fn if there are no inputs for target_fn? What is safe doing such that it knows how to remove infinity values?

Gradient Descent aside, would this safe function work on a crazy functions that was undefined at uncountably many places?


Solution

  • It might help you to understand if we replace the variable names step by step:

    target_fn = safe(target_fn)
    

    means that f in safe is target_fn:

    def safe(target_fn):
        def safe_f(*args, **kwargs):
            try:
                return target_fn(*args, **kwargs)
            except:
                return float('inf')
        return safe_f
    

    and:

    target_fn = safe_f
    

    i.e. we replace the function originally bound to target_fn with the function safe_f we just created in the decorator function safe, retaining access to the original as f, via a closure.

    So the arguments are passed through *args, **kwargs (see What does ** (double star) and * (star) do for parameters?):

    next_value = target_fn(next_theta)
    

    gets resolved to:

    def safe_f(next_theta):
        try:
            return target_fn(next_theta)
        except:
            return float('inf')
    

    i.e. either return the result from calling the original target_fn with the argument next_theta or, if any errors occur when doing so, float('inf').

    would this safe function work on a crazy functions that was undefined at uncountably many places?

    As it uses *args, **kwargs you can use it to wrap any function, and it will quietly suppress any error raised by the function and return float('inf') instead - this won't always be desirable, though! There are many other uses for decorator functions, which are often applied using @decorator syntax (e.g. for class and static methods and properties on classes); see e.g. What are some common uses for Python decorators?