Repository: bethgelab/foolbox (language: python)

Issue:
HopSkipJump Attack returns 'nan'-Values and enters Infinite Loop
I am not sure if the HopSkipJump attack is maintained, as it is not documented. However, it is part of the official API so I thought I report the problem anyways.

We realized that sometimes the attack returns 'nan'-tensors and sometimes enters an infinite loop.

The nan return values appear when the model is naturally misclassifying one of the given samples in the batch. I tried to track down the problem and found this line to be problematic:
https://github.com/bethgelab/foolbox/blob/43858a0bfc2b83023ae87954f738194708f8d923/foolbox/attacks/hop_skip_jump.py#L254

It seems that in that specific case both, divisor and denominator, are zero, causing the nan-values. This can happen when delta becomes zero (which it will after the first step when the distance is zero).
https://github.com/bethgelab/foolbox/blob/43858a0bfc2b83023ae87954f738194708f8d923/foolbox/attacks/hop_skip_jump.py#L364

We were able to fix this and get sensible results by adding a small constant to delta in line 254.

About the infinite loop here:
https://github.com/bethgelab/foolbox/blob/43858a0bfc2b83023ae87954f738194708f8d923/foolbox/attacks/hop_skip_jump.py#L166-L174

Interestingly, torch models might return real values when inputting nan-tensors, the first convolutional layer of our model returned -inf values, which in turn become zero when passed into a ReLU function and the model will return logits. The attack will not enter an infinite loop as long none of the samples in the batch are actually the class, the model predicts for full nan-tensors.



Discussion / hints:

Fix patch:
diff --git a/docs/modules/attacks.rst b/docs/modules/attacks.rst
index 3e7f2381..8eeb482c 100644
--- a/docs/modules/attacks.rst
+++ b/docs/modules/attacks.rst
@@ -32,6 +32,8 @@
    BinarySearchContrastReductionAttack
    LinearSearchContrastReductionAttack
 
+   HopSkipJumpAttack
+
    L2CarliniWagnerAttack
    NewtonFoolAttack
    EADAttack
diff --git a/foolbox/attacks/__init__.py b/foolbox/attacks/__init__.py
index 53dd5a20..b3f58aef 100644
--- a/foolbox/attacks/__init__.py
+++ b/foolbox/attacks/__init__.py
@@ -56,7 +56,7 @@
 from .binarization import BinarizationRefinementAttack  # noqa: F401
 from .dataset_attack import DatasetAttack  # noqa: F401
 from .boundary_attack import BoundaryAttack  # noqa: F401
-from .hop_skip_jump import HopSkipJump  # noqa: F401
+from .hop_skip_jump import HopSkipJumpAttack  # noqa: F401
 from .brendel_bethge import (  # noqa: F401
     L0BrendelBethgeAttack,
     L1BrendelBethgeAttack,
diff --git a/foolbox/attacks/hop_skip_jump.py b/foolbox/attacks/hop_skip_jump.py
index d5ec2fa5..ad96801b 100644
--- a/foolbox/attacks/hop_skip_jump.py
+++ b/foolbox/attacks/hop_skip_jump.py
@@ -24,7 +24,7 @@
 from ..distances import l2, linf
 
 
-class HopSkipJump(MinimizationAttack):
+class HopSkipJumpAttack(MinimizationAttack):
     """A powerful adversarial attack that requires neither gradients
     nor probabilities [#Chen19].
 
@@ -251,7 +251,7 @@ def approximate_gradients(
         perturbed = ep.expand_dims(x_advs, 0) + scaled_rv
         perturbed = ep.clip(perturbed, 0, 1)
 
-        rv = (perturbed - x_advs) / atleast_kd(ep.expand_dims(delta, 0), rv.ndim)
+        rv = (perturbed - x_advs) / atleast_kd(ep.expand_dims(delta + 1e-8, 0), rv.ndim)
 
         multipliers_list: List[ep.Tensor] = []
         for step in range(steps):


Test patch:
diff --git a/tests/test_hsj_attack.py b/tests/test_hsj_attack.py
index 826238c1..2560d606 100644
--- a/tests/test_hsj_attack.py
+++ b/tests/test_hsj_attack.py
@@ -16,7 +16,7 @@ def get_attack_id(x: Tuple[BrendelBethgeAttack, Union[int, float]]) -> str:
 
 attacks: List[Tuple[fa.Attack, Union[int, float]]] = [
     (
-        fa.HopSkipJump(
+        fa.HopSkipJumpAttack(
             steps=1,
             constraint="linf",
             initial_gradient_eval_steps=100,
@@ -25,7 +25,7 @@ def get_attack_id(x: Tuple[BrendelBethgeAttack, Union[int, float]]) -> str:
         ep.inf,
     ),
     (
-        fa.HopSkipJump(
+        fa.HopSkipJumpAttack(
             steps=1,
             constraint="l2",
             initial_gradient_eval_steps=100,
@@ -34,7 +34,7 @@ def get_attack_id(x: Tuple[BrendelBethgeAttack, Union[int, float]]) -> str:
         2,
     ),
     (
-        fa.HopSkipJump(
+        fa.HopSkipJumpAttack(
             steps=1,
             constraint="l2",
             initial_gradient_eval_steps=100,
@@ -51,7 +51,7 @@ def get_attack_id(x: Tuple[BrendelBethgeAttack, Union[int, float]]) -> str:
 def test_hsj_untargeted_attack(
     request: Any,
     fmodel_and_data_ext_for_attacks: ModeAndDataAndDescription,
-    attack_and_p: Tuple[fa.HopSkipJump, Union[int, float]],
+    attack_and_p: Tuple[fa.HopSkipJumpAttack, Union[int, float]],
 ) -> None:
     if request.config.option.skipslow:
         pytest.skip()
@@ -87,7 +87,7 @@ def test_hsj_untargeted_attack(
 def test_hsj_targeted_attack(
     request: Any,
     fmodel_and_data_ext_for_attacks: ModeAndDataAndDescription,
-    attack_and_p: Tuple[fa.HopSkipJump, Union[int, float]],
+    attack_and_p: Tuple[fa.HopSkipJumpAttack, Union[int, float]],
 ) -> None:
     if request.config.option.skipslow:
         pytest.skip()