Example #1
0
def _autograd_is_indep_analytic(func, *args, **kwargs):
    """Test analytically whether a function is independent of its arguments
    using Autograd.

    Args:
        func (callable): Function to test for independence
        args (tuple): Arguments for the function with respect to which
            to test for independence
        kwargs (dict): Keyword arguments for the function at which
            (but not with respect to which) to test for independence

    Returns:
        bool: Whether the function seems to not depend on it ``args``
        analytically. That is, an output of ``True`` means that the
        ``args`` do *not* feed into the output.

    In Autograd, we test this by sending a ``Box`` through the function and
    testing whether the output is again a ``Box`` and on the same trace as
    the input ``Box``. This means that we can trace actual *independence*
    of the output from the input, not only whether the passed function is
    constant.
    The code is adapted from
    `autograd.tracer.py::trace
    <https://github.com/HIPS/autograd/blob/master/autograd/tracer.py#L7>`__.
    """
    # pylint: disable=protected-access
    node = VJPNode.new_root()
    with trace_stack.new_trace() as t:
        start_box = new_box(args, t, node)
        end_box = func(*start_box, **kwargs)

    if type(end_box) in [tuple, list]:
        if any(
                isbox(_end) and _end._trace == start_box._trace
                for _end in end_box):
            return False
    elif isinstance(end_box, np.ndarray):
        if end_box.ndim == 0:
            end_box = [end_box.item()]
        if any(
                isbox(_end) and _end._trace == start_box._trace
                for _end in end_box):
            return False
    else:
        if isbox(end_box) and end_box._trace == start_box._trace:
            return False
    return True
Example #2
0
def trace(fun, start_nodes, args):
    with tracer.trace_stack.new_trace() as t:
        start_boxes = [
            tracer.new_box(x, t, n) for x, n in zip(args, start_nodes)
        ]
        end_box = fun(*start_boxes)
        if tracer.isbox(end_box) and end_box._trace == t:
            return end_box._value, end_box._node
        else:
            warnings.warn("Output seems independent of input.")
            return end_box, None
Example #3
0
def test_value_and_grad():
    fun = lambda x: np.sum(np.sin(x)**2)
    dfun = grad(fun)
    dfun_both = value_and_grad(fun)
    x = npr.randn(5)
    assert not isbox(dfun_both(x)[0])
    check_equivalent(fun(x), dfun_both(x)[0])
    check_equivalent(dfun(x), dfun_both(x)[1])

    def fun2(x): return dfun_both(x)[0]
    check_grads(fun2)(x)
def test_value_and_grad():
    fun = lambda x: np.sum(np.sin(x)**2)
    dfun = grad(fun)
    dfun_both = value_and_grad(fun)
    x = npr.randn(5)
    assert not isbox(dfun_both(x)[0])
    check_equivalent(fun(x), dfun_both(x)[0])
    check_equivalent(dfun(x), dfun_both(x)[1])

    def fun2(x):
        return dfun_both(x)[0]

    check_grads(fun2)(x)
Example #5
0
def calc_jacobian(start, end):
    # if the end_box is not a box - autograd can not track back
    if not isbox(end):
        return vspace(start.shape).zeros()

    # the final jacobian matrices
    jac = []

    # the backward pass is done for each objective function once
    for j in range(end.shape[1]):
        b = anp.zeros(end.shape)
        b[:, j] = 1
        n = new_box(b, 0, VJPNode.new_root())
        _jac = backward_pass(n, end._node)
        jac.append(_jac)

    jac = anp.stack(jac, axis=1)

    return jac
Example #6
0
def is_constant(x):
    return not ag_tracer.isbox(x)
Example #7
0
def backtrack(output_boxes, input_node_map):
    """Trace the computation graph from output boxes to ancestors in the input.

    Given the output of a function called on boxed inputs, backtrack finds
    which, if any, of the nodes in the input_node_map each output element
    depends on.

    Parameters
    ----------
        output_boxes: list
            List of outputs from a traced function.
        input_node_map: dict
            Dictionary of TracerNode -> box elements representing the potential root
            nodes of the computation graph.

    Returns
    -------
        output_dependencies: dict
            Dictionary mapping output_idx to a (deduplicated) list of nodes
            in the input that output idx depends on. For instance, if we have

            def f(x, y, z):
                return x + y, y + z

            Then, we get output_dependencies {0: [Node_x, Node_y], 1: [Node_y, Node_z]}.

    """
    if not isinstance(output_boxes, Iterable):
        output_boxes = [output_boxes]

    # For each output, figure out which (if any) of the inputs it depends on
    # by solving a graph search problem on the computation graph.
    output_dependencies = dict(
        (output_idx, set()) for output_idx in range(len(output_boxes)))
    for idx, output_box in enumerate(output_boxes):
        # If the output isn't a box, then it's independent of all the inputs.
        if isbox(output_box):
            # Use breadth-first search to find parents
            # pylint: disable-msg=protected-access
            queue = [output_box._node]
            while queue:
                node = queue.pop(0)

                # Check if ancestor is an input node
                if node in input_node_map:
                    input_box = input_node_map[node]
                    if output_box._trace == input_box._trace:
                        output_dependencies[idx].add(node)

                # If the node corresponds to an irrelevant dependency,
                # e.g. 0 * constant, we skip it.
                if is_dead_node(node):
                    continue

                # Add the parents to the queue
                for parent in node.parents:
                    # This is a hack to handle a common pattern where the
                    # user returns z = np.array([x1, x2]) from a function and
                    # then we inspect one item, e.g. z[0]. By default, both x1 and x2
                    # are parents of z, but in this case, we can detect that
                    # z[0] only depends on x1. In general, however, if after
                    # constructing z, we apply another function to the array,
                    # e.g. f(z) we cannot detect the subset of x that the result depends on.
                    # This is one of the limitations of using boxes based on autograd.
                    # Note this limitation means that we might add extra
                    # edges to the causal graph. It will never mean we miss an
                    # edge, so the returned graphs are still valid.
                    if is_getitem_node(node) and is_array_node(parent):
                        index = node.args[1]
                        queue.append(parent.parents[index])
                    else:
                        queue.append(parent)

    return dict((k, list(v)) for k, v in output_dependencies.items())