//  [python]
def pgd_attack(loss_fn,
               input_image,
               label,
               epsilon,
               num_steps,
               optimizer=UnrolledAdam(),
               project_perturbation=_project_perturbation,
               early_stop_loss_threshold=None,
               is_debug=False):
    """Projected gradient descent for generating adversarial images.

    Args:
        :param loss_fn: A callable which takes `input_image` and `label` as
                        arguments, and returns a batch of loss values. Same
                        interface as UnrolledOptimizer.
        :param input_image: Tensor, a batch of images
        :param label: Tensor, a batch of labels
        :param epsilon: float, the L-infinity norm of the maximum allowable
                                        perturbation
        :param num_steps: int, the number of steps of gradient descent
        :param optimizer: An `UnrolledOptimizer` object
        :param project_perturbation: A function, which will be used to enforce
                                     some constraint. It should have the same
                                     signature as `_project_perturbation`.
        :param early_stop_loss_threshold: A float or None. If specified, the
                                          attack will end if the loss is below
                                          `early_stop_loss_threshold`.
        :param is_debug: A bool. If True, print debug info for attack progress.

    Returns:
        adversarial version of `input_image`, with L-infinity difference less
            than epsilon, which tries to minimize loss_fn.

    Note that this function is not intended as an Attack by itself. Rather, it
    is designed as a helper function which you can use to write your own attack
    methods. The method uses a tf.while_loop to optimize a loss function in
    a single sess.run() call.
    """
    if is_debug:
        with tf.device("/cpu:0"):
            input_image = tf.Print(
                input_image, [],
                "Starting PGD attack with epsilon: %s" % epsilon)

    init_perturbation = tf.random_uniform(
        tf.shape(input_image), minval=-epsilon, maxval=epsilon, dtype=tf_dtype)
    init_perturbation = project_perturbation(init_perturbation, epsilon,
                                             input_image)
    init_optim_state = optimizer.init_state([init_perturbation])
    nest = tf.contrib.framework.nest

    def loop_body(i, perturbation, flat_optim_state):
        """Update perturbation to input image."""
        optim_state = nest.pack_sequence_as(
            structure=init_optim_state, flat_sequence=flat_optim_state)

        def wrapped_loss_fn(x):
            return loss_fn(input_image + x, label)

        new_perturbation_list, new_optim_state = optimizer.minimize(
            wrapped_loss_fn, [perturbation], optim_state)
        loss = reduce_mean(wrapped_loss_fn(perturbation), axis=0)
        if is_debug:
            with tf.device("/cpu:0"):
                loss = tf.Print(loss, [loss], "Total batch loss")
        projected_perturbation = project_perturbation(new_perturbation_list[0],
                                                      epsilon, input_image)
        with tf.control_dependencies([loss]):
            i = tf.identity(i)
            if early_stop_loss_threshold:
                i = tf.cond(
                    tf.less(loss, early_stop_loss_threshold),
                    lambda: float(num_steps), lambda: i)
        return i + 1, projected_perturbation, nest.flatten(new_optim_state)

    def cond(i, *_):
        return tf.less(i, num_steps)

    flat_init_optim_state = nest.flatten(init_optim_state)
    _, final_perturbation, _ = tf.while_loop(
        cond,
        loop_body,
        loop_vars=[tf.constant(0.), init_perturbation, flat_init_optim_state],
        parallel_iterations=1,
        back_prop=False)
    if project_perturbation == _project_perturbation:
        perturbation_max = epsilon * 1.1
        check_diff = tf.assert_less_equal(
            final_perturbation, perturbation_max,
            message="final_perturbation must change no pixel by more than "
                    "%s" % perturbation_max)
    else:
        check_diff = tf.no_op()

    with tf.control_dependencies([check_diff]):
        adversarial_image = input_image + final_perturbation
    return tf.stop_gradient(adversarial_image)