• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bethgelab / foolbox / 8137716344

22 Jan 2024 10:53PM UTC coverage: 98.47%. Remained the same
8137716344

push

github

web-flow
Bump pillow from 10.1.0 to 10.2.0 in /tests (#718)

Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.1.0 to 10.2.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/10.1.0...10.2.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

3475 of 3529 relevant lines covered (98.47%)

7.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.71
/foolbox/attacks/fast_minimum_norm.py
1
import math
10✔
2
from abc import abstractmethod, ABC
10✔
3
from typing import Union, Optional, Any, Tuple
10✔
4

5
import eagerpy as ep
10✔
6
from eagerpy.astensor import T
10✔
7

8
from .base import MinimizationAttack, raise_if_kwargs, get_criterion, get_is_adversarial
10✔
9
from .gradient_descent_base import (
10✔
10
    uniform_l1_n_balls,
11
    normalize_lp_norms,
12
    uniform_l2_n_balls,
13
)
14
from .. import Model, Misclassification, TargetedMisclassification
10✔
15
from ..devutils import atleast_kd, flatten
10✔
16
from ..distances import l1, linf, l2, l0, LpDistance
10✔
17

18
ps = {l0: 0, l1: 1, l2: 2, linf: ep.inf, LpDistance: ep.nan}
10✔
19
duals = {l0: ep.nan, l1: ep.inf, l2: 2, linf: 1, LpDistance: ep.nan}
10✔
20

21

22
def best_other_classes(logits: ep.Tensor, exclude: ep.Tensor) -> ep.Tensor:
10✔
23
    other_logits = logits - ep.onehot_like(logits, exclude, value=ep.inf)
6✔
24
    return other_logits.argmax(axis=-1)
6✔
25

26

27
def project_onto_l1_ball(x: ep.Tensor, eps: ep.Tensor) -> ep.Tensor:
10✔
28
    """Computes Euclidean projection onto the L1 ball for a batch. [#Duchi08]_
29

30
    Adapted from the pytorch version by Tony Duan:
31
    https://gist.github.com/tonyduan/1329998205d88c566588e57e3e2c0c55
32

33
    Args:
34
        x: Batch of arbitrary-size tensors to project, possibly on GPU
35
        eps: radius of l-1 ball to project onto
36

37
    References:
38
      ..[#Duchi08] Efficient Projections onto the l1-Ball for Learning in High Dimensions
39
         John Duchi, Shai Shalev-Shwartz, Yoram Singer, and Tushar Chandra.
40
         International Conference on Machine Learning (ICML 2008)
41
    """
42
    original_shape = x.shape
6✔
43
    x = flatten(x)
6✔
44
    mask = (ep.norms.l1(x, axis=1) <= eps).astype(x.dtype).expand_dims(1)
6✔
45
    mu = ep.flip(ep.sort(ep.abs(x)), axis=-1).astype(x.dtype)
6✔
46
    cumsum = ep.cumsum(mu, axis=-1)
6✔
47
    arange = ep.arange(x, 1, x.shape[1] + 1).astype(x.dtype)
6✔
48
    rho = (
6✔
49
        ep.max(
50
            ((mu * arange > (cumsum - eps.expand_dims(1)))).astype(x.dtype) * arange,
51
            axis=-1,
52
        )
53
        - 1
54
    )
55
    # samples already under norm will have to select
56
    rho = ep.maximum(rho, 0)
6✔
57
    theta = (
6✔
58
        cumsum[ep.arange(x, x.shape[0]), rho.astype(ep.arange(x, 1).dtype)] - eps
59
    ) / (rho + 1.0)
60
    proj = (ep.abs(x) - theta.expand_dims(1)).clip(min_=0, max_=ep.inf)
6✔
61
    x = mask * x + (1 - mask) * proj * ep.sign(x)
6✔
62
    return x.reshape(original_shape)
6✔
63

64

65
class FMNAttackLp(MinimizationAttack, ABC):
10✔
66
    """The Fast Minimum Norm adversarial attack, in Lp norm. [#Pintor21]_
67

68
    Args:
69
        steps: Number of iterations.
70
        max_stepsize: Initial stepsize for the gradient update.
71
        min_stepsize: Final stepsize for the gradient update. The
72
            stepsize will be reduced with a cosine annealing policy.
73
        gamma: Initial stepsize for the epsilon update. It will
74
            be updated with a cosine annealing reduction up to 0.001.
75
        init_attack: Optional initial attack. If an initial attack
76
            is specified (or initial points are provided in the run), the
77
            attack will first try to search for the boundary between the
78
            initial point and the points in a class that satisfies the
79
            adversarial criterion.
80
        binary_search_steps: Number of steps to use for the search
81
            from the adversarial points. If no initial attack or adversarial
82
            starting point is provided, this parameter will be ignored.
83

84
    References:
85
        .. [#Pintor21] Maura Pintor, Fabio Roli, Wieland Brendel,
86
            Battista Biggio, "Fast Minimum-norm Adversarial
87
            Attacks through Adaptive Norm Constraints."
88
            arXiv preprint arXiv:2102.12827 (2021).
89
            https://arxiv.org/abs/2102.12827
90
    """
91

92
    def __init__(
10✔
93
        self,
94
        *,
95
        steps: int = 100,
96
        max_stepsize: float = 1.0,
97
        min_stepsize: Optional[float] = None,
98
        gamma: float = 0.05,
99
        init_attack: Optional[MinimizationAttack] = None,
100
        binary_search_steps: int = 10,
101
    ):
102
        self.steps = steps
10✔
103
        self.max_stepsize = max_stepsize
10✔
104
        self.init_attack = init_attack
10✔
105
        if min_stepsize is not None:
10✔
106
            self.min_stepsize = min_stepsize
10✔
107
        else:
108
            self.min_stepsize = max_stepsize / 100
10✔
109
        self.binary_search_steps = binary_search_steps
10✔
110
        self.gamma = gamma
10✔
111

112
        self.p = ps[self.distance]
10✔
113
        self.dual = duals[self.distance]
10✔
114

115
    def run(
10✔
116
        self,
117
        model: Model,
118
        inputs: T,
119
        criterion: Union[Misclassification, TargetedMisclassification, T],
120
        *,
121
        starting_points: Optional[ep.Tensor] = None,
122
        early_stop: Optional[float] = None,
123
        **kwargs: Any,
124
    ) -> T:
125
        raise_if_kwargs(kwargs)
6✔
126
        criterion_ = get_criterion(criterion)
6✔
127

128
        if isinstance(criterion_, Misclassification):
6✔
129
            targeted = False
6✔
130
            classes = criterion_.labels
6✔
131
        elif isinstance(criterion_, TargetedMisclassification):
6✔
132
            targeted = True
6✔
133
            classes = criterion_.target_classes
6✔
134
        else:
135
            raise ValueError("unsupported criterion")
×
136

137
        def loss_fn(
6✔
138
            inputs: ep.Tensor, labels: ep.Tensor
139
        ) -> Tuple[ep.Tensor, Tuple[ep.Tensor, ep.Tensor]]:
140

141
            logits = model(inputs)
6✔
142

143
            if targeted:
6✔
144
                c_minimize = best_other_classes(logits, labels)
6✔
145
                c_maximize = labels  # target_classes
6✔
146
            else:
147
                c_minimize = labels  # labels
6✔
148
                c_maximize = best_other_classes(logits, labels)
6✔
149

150
            loss = logits[rows, c_minimize] - logits[rows, c_maximize]
6✔
151

152
            return -loss.sum(), (logits, loss)
6✔
153

154
        x, restore_type = ep.astensor_(inputs)
6✔
155
        del inputs, criterion, kwargs
6✔
156
        N = len(x)
6✔
157
        initialized = False
6✔
158
        # start from initialization points/attack
159
        if starting_points is not None:
6✔
160
            x1 = starting_points
6✔
161
            initialized = True
6✔
162
        else:
163
            if self.init_attack is not None:
×
164
                x1 = self.init_attack.run(model, x, criterion_)
×
165
                initialized = True
×
166

167
        # if initial points or initialization attacks are provided,
168
        #   search for the boundary
169
        if initialized is True:
6✔
170
            is_adv = get_is_adversarial(criterion_, model)
6✔
171
            assert is_adv(x1).all()
6✔
172
            lower_bound = ep.zeros(x, shape=(N,))
6✔
173
            upper_bound = ep.ones(x, shape=(N,))
6✔
174
            for _ in range(self.binary_search_steps):
6✔
175
                epsilons = (lower_bound + upper_bound) / 2
6✔
176
                mid_points = self.mid_points(x, x1, epsilons, model.bounds)
6✔
177
                is_advs = is_adv(mid_points)
6✔
178
                lower_bound = ep.where(is_advs, lower_bound, epsilons)
6✔
179
                upper_bound = ep.where(is_advs, epsilons, upper_bound)
6✔
180
            starting_points = self.mid_points(x, x1, upper_bound, model.bounds)
6✔
181
            delta = starting_points - x
6✔
182
        else:
183
            # start from x0
184
            delta = ep.zeros_like(x)
×
185

186
        if classes.shape != (N,):
6✔
187
            name = "target_classes" if targeted else "labels"
×
188
            raise ValueError(
×
189
                f"expected {name} to have shape ({N},), got {classes.shape}"
190
            )
191

192
        min_, max_ = model.bounds
6✔
193
        rows = range(N)
6✔
194
        grad_and_logits = ep.value_and_grad_fn(x, loss_fn, has_aux=True)
6✔
195

196
        if self.p != 0:
6✔
197
            epsilon = ep.inf * ep.ones(x, len(x))
6✔
198
        else:
199
            epsilon = ep.maximum(
6✔
200
                ep.ones(x, len(x)), ep.norms.l0(flatten(delta), axis=-1)
201
            )
202
        if self.p != 0:
6✔
203
            worst_norm = ep.norms.lp(
6✔
204
                flatten(ep.maximum(x - min_, max_ - x)), p=self.p, axis=-1
205
            )
206
        else:
207
            worst_norm = flatten(ep.ones_like(x)).bool().sum(axis=1).float32()
6✔
208

209
        best_lp = worst_norm
6✔
210
        best_delta = delta
6✔
211
        adv_found = ep.zeros(x, len(x)).bool()
6✔
212

213
        for i in range(self.steps):
6✔
214
            # perform cosine annealing of learning rates
215
            stepsize = (
6✔
216
                self.min_stepsize
217
                + (self.max_stepsize - self.min_stepsize)
218
                * (1 + math.cos(math.pi * i / self.steps))
219
                / 2
220
            )
221
            gamma = (
6✔
222
                0.001
223
                + (self.gamma - 0.001) * (1 + math.cos(math.pi * (i / self.steps))) / 2
224
            )
225

226
            x_adv = x + delta
6✔
227

228
            loss, (logits, loss_batch), gradients = grad_and_logits(x_adv, classes)
6✔
229
            is_adversarial = criterion_(x_adv, logits)
6✔
230
            lp = ep.norms.lp(flatten(delta), p=self.p, axis=-1)
6✔
231
            is_smaller = lp <= best_lp
6✔
232
            is_both = ep.logical_and(is_adversarial, is_smaller)
6✔
233
            adv_found = ep.logical_or(adv_found, is_adversarial)
6✔
234
            best_lp = ep.where(is_both, lp, best_lp)
6✔
235
            best_delta = ep.where(atleast_kd(is_both, x.ndim), delta, best_delta)
6✔
236

237
            # update epsilon
238
            if self.p != 0:
6✔
239
                distance_to_boundary = abs(loss_batch) / ep.norms.lp(
6✔
240
                    flatten(gradients), p=self.dual, axis=-1
241
                )
242
                epsilon = ep.where(
6✔
243
                    is_adversarial,
244
                    ep.minimum(
245
                        epsilon * (1 - gamma),
246
                        ep.norms.lp(flatten(best_delta), p=self.p, axis=-1),
247
                    ),
248
                    ep.where(
249
                        adv_found,
250
                        epsilon * (1 + gamma),
251
                        ep.norms.lp(flatten(delta), p=self.p, axis=-1)
252
                        + distance_to_boundary,
253
                    ),
254
                )
255
            else:
256
                epsilon = ep.where(
6✔
257
                    is_adversarial,
258
                    ep.minimum(
259
                        ep.minimum(
260
                            epsilon - 1,
261
                            (epsilon * (1 - gamma))
262
                            .astype(ep.arange(x, 1).dtype)
263
                            .astype(epsilon.dtype),
264
                        ),
265
                        ep.norms.lp(flatten(best_delta), p=self.p, axis=-1),
266
                    ),
267
                    ep.maximum(
268
                        epsilon + 1,
269
                        (epsilon * (1 + gamma))
270
                        .astype(ep.arange(x, 1).dtype)
271
                        .astype(epsilon.dtype),
272
                    ),
273
                )
274
                epsilon = ep.maximum(1, epsilon).astype(epsilon.dtype)
6✔
275

276
            # clip epsilon
277
            epsilon = ep.minimum(epsilon, worst_norm)
6✔
278

279
            # computes normalized gradient update
280
            grad_ = self.normalize(gradients) * stepsize
6✔
281

282
            # do step
283
            delta = delta + grad_
6✔
284

285
            # project according to the given norm
286
            delta = self.project(x=x + delta, x0=x, epsilon=epsilon) - x
6✔
287

288
            # clip to valid bounds
289
            delta = ep.clip(x + delta, *model.bounds) - x
6✔
290

291
        x_adv = x + best_delta
6✔
292
        return restore_type(x_adv)
6✔
293

294
    def normalize(self, gradients: ep.Tensor) -> ep.Tensor:
10✔
295
        return normalize_lp_norms(gradients, p=2)
6✔
296

297
    @abstractmethod
298
    def project(self, x: ep.Tensor, x0: ep.Tensor, epsilon: ep.Tensor) -> ep.Tensor:
299
        ...
300

301
    @abstractmethod
302
    def mid_points(
303
        self,
304
        x0: ep.Tensor,
305
        x1: ep.Tensor,
306
        epsilons: ep.Tensor,
307
        bounds: Tuple[float, float],
308
    ) -> ep.Tensor:
309
        raise NotImplementedError
310

311

312
class L1FMNAttack(FMNAttackLp):
10✔
313
    """The L1 Fast Minimum Norm adversarial attack, in Lp norm. [#Pintor21L1]_
314

315
    Args:
316
        steps: Number of iterations.
317
        max_stepsize: Initial stepsize for the gradient update.
318
        min_stepsize: Final stepsize for the gradient update. The
319
            stepsize will be reduced with a cosine annealing policy.
320
        gamma: Initial stepsize for the epsilon update. It will
321
            be updated with a cosine annealing reduction up to 0.001.
322
        init_attack: Optional initial attack. If an initial attack
323
            is specified (or initial points are provided in the run), the
324
            attack will first try to search for the boundary between the
325
            initial point and the points in a class that satisfies the
326
            adversarial criterion.
327
        binary_search_steps: Number of steps to use for the search
328
            from the adversarial points. If no initial attack or adversarial
329
            starting point is provided, this parameter will be ignored.
330

331
    References:
332
        .. [#Pintor21L1] Maura Pintor, Fabio Roli, Wieland Brendel,
333
            Battista Biggio, "Fast Minimum-norm Adversarial
334
            Attacks through Adaptive Norm Constraints."
335
            arXiv preprint arXiv:2102.12827 (2021).
336
    """
337

338
    distance = l1
10✔
339
    p = 1
10✔
340
    dual = ep.inf
10✔
341

342
    def get_random_start(self, x0: ep.Tensor, epsilon: float) -> ep.Tensor:
10✔
343
        batch_size, n = flatten(x0).shape
×
344
        r = uniform_l1_n_balls(x0, batch_size, n).reshape(x0.shape)
×
345
        return x0 + epsilon * r
×
346

347
    def project(self, x: ep.Tensor, x0: ep.Tensor, epsilon: ep.Tensor) -> ep.Tensor:
10✔
348
        return x0 + project_onto_l1_ball(x - x0, epsilon)
6✔
349

350
    def mid_points(
10✔
351
        self,
352
        x0: ep.Tensor,
353
        x1: ep.Tensor,
354
        epsilons: ep.Tensor,
355
        bounds: Tuple[float, float],
356
    ) -> ep.Tensor:
357
        # returns a point between x0 and x1 where
358
        # epsilon = 0 returns x0 and epsilon = 1
359
        # returns x1
360

361
        # get epsilons in right shape for broadcasting
362
        epsilons = epsilons.reshape(epsilons.shape + (1,) * (x0.ndim - 1))
6✔
363

364
        threshold = (bounds[1] - bounds[0]) * (1 - epsilons)
6✔
365
        mask = (x1 - x0).abs() > threshold
6✔
366
        new_x = ep.where(
6✔
367
            mask, x0 + (x1 - x0).sign() * ((x1 - x0).abs() - threshold), x0
368
        )
369
        return new_x
6✔
370

371

372
class L2FMNAttack(FMNAttackLp):
10✔
373
    """The L2 Fast Minimum Norm adversarial attack, in Lp norm. [#Pintor21L2]_
374

375
    Args:
376
        steps: Number of iterations.
377
        max_stepsize: Initial stepsize for the gradient update.
378
        min_stepsize: Final stepsize for the gradient update. The
379
            stepsize will be reduced with a cosine annealing policy.
380
        gamma: Initial stepsize for the epsilon update. It will
381
            be updated with a cosine annealing reduction up to 0.001.
382
        init_attack: Optional initial attack. If an initial attack
383
            is specified (or initial points are provided in the run), the
384
            attack will first try to search for the boundary between the
385
            initial point and the points in a class that satisfies the
386
            adversarial criterion.
387
        binary_search_steps: Number of steps to use for the search
388
            from the adversarial points. If no initial attack or adversarial
389
            starting point is provided, this parameter will be ignored.
390

391
    References:
392
        .. [#Pintor21L2] Maura Pintor, Fabio Roli, Wieland Brendel,
393
            Battista Biggio, "Fast Minimum-norm Adversarial
394
            Attacks through Adaptive Norm Constraints."
395
            arXiv preprint arXiv:2102.12827 (2021).
396
            https://arxiv.org/abs/2102.12827
397
    """
398

399
    distance = l2
10✔
400
    p = 2
10✔
401
    dual = 2
10✔
402

403
    def get_random_start(self, x0: ep.Tensor, epsilon: float) -> ep.Tensor:
10✔
404
        batch_size, n = flatten(x0).shape
×
405
        r = uniform_l2_n_balls(x0, batch_size, n).reshape(x0.shape)
×
406
        return x0 + epsilon * r
×
407

408
    def project(self, x: ep.Tensor, x0: ep.Tensor, epsilon: ep.Tensor) -> ep.Tensor:
10✔
409
        norms = flatten(x).norms.l2(axis=-1)
6✔
410
        norms = ep.maximum(norms, 1e-12)
6✔
411
        factor = ep.minimum(1, norms / norms)
6✔
412
        factor = atleast_kd(factor, x.ndim)
6✔
413
        return x0 + (x - x0) * factor
6✔
414

415
    def mid_points(
10✔
416
        self,
417
        x0: ep.Tensor,
418
        x1: ep.Tensor,
419
        epsilons: ep.Tensor,
420
        bounds: Tuple[float, float],
421
    ) -> ep.Tensor:
422
        # returns a point between x0 and x1 where
423
        # epsilon = 0 returns x0 and epsilon = 1
424
        # returns x1
425

426
        # get epsilons in right shape for broadcasting
427
        epsilons = epsilons.reshape(epsilons.shape + (1,) * (x0.ndim - 1))
6✔
428
        return epsilons * x1 + (1 - epsilons) * x0
6✔
429

430

431
class LInfFMNAttack(FMNAttackLp):
10✔
432
    """The L-infinity Fast Minimum Norm adversarial attack, in Lp norm. [#Pintor21Linf]_
433

434
    Args:
435
        steps: Number of iterations.
436
        max_stepsize: Initial stepsize for the gradient update.
437
        min_stepsize: Final stepsize for the gradient update. The
438
            stepsize will be reduced with a cosine annealing policy.
439
        gamma: Initial stepsize for the epsilon update. It will
440
            be updated with a cosine annealing reduction up to 0.001.
441
        init_attack: Optional initial attack. If an initial attack
442
            is specified (or initial points are provided in the run), the
443
            attack will first try to search for the boundary between the
444
            initial point and the points in a class that satisfies the
445
            adversarial criterion.
446
        binary_search_steps: Number of steps to use for the search
447
            from the adversarial points. If no initial attack or adversarial
448
            starting point is provided, this parameter will be ignored.
449

450
    References:
451
        .. [#Pintor21Linf] Maura Pintor, Fabio Roli, Wieland Brendel,
452
            Battista Biggio, "Fast Minimum-norm Adversarial
453
            Attacks through Adaptive Norm Constraints."
454
            arXiv preprint arXiv:2102.12827 (2021).
455
            https://arxiv.org/abs/2102.12827
456
    """
457

458
    distance = linf
10✔
459
    p = ep.inf
10✔
460
    dual = 1
10✔
461

462
    def get_random_start(self, x0: ep.Tensor, epsilon: float) -> ep.Tensor:
10✔
463
        return x0 + ep.uniform(x0, x0.shape, -epsilon, epsilon)
×
464

465
    def project(self, x: ep.Tensor, x0: ep.Tensor, epsilon: ep.Tensor) -> ep.Tensor:
10✔
466
        clipped = ep.maximum(flatten(x - x0).T, -epsilon)
6✔
467
        clipped = ep.minimum(clipped, epsilon).T
6✔
468
        return x0 + clipped.reshape(x0.shape)
6✔
469

470
    def mid_points(
10✔
471
        self,
472
        x0: ep.Tensor,
473
        x1: ep.Tensor,
474
        epsilons: ep.Tensor,
475
        bounds: Tuple[float, float],
476
    ) -> ep.Tensor:
477
        # returns a point between x0 and x1 where
478
        # epsilon = 0 returns x0 and epsilon = 1
479
        delta = x1 - x0
6✔
480
        min_, max_ = bounds
6✔
481
        s = max_ - min_
6✔
482
        # get epsilons in right shape for broadcasting
483
        epsilons = epsilons.reshape(epsilons.shape + (1,) * (x0.ndim - 1))
6✔
484

485
        clipped_delta = ep.where(delta < -epsilons * s, -epsilons * s, delta)
6✔
486
        clipped_delta = ep.where(
6✔
487
            clipped_delta > epsilons * s, epsilons * s, clipped_delta
488
        )
489
        return x0 + clipped_delta
6✔
490

491

492
class L0FMNAttack(FMNAttackLp):
10✔
493
    """The L0 Fast Minimum Norm adversarial attack, in Lp norm. [#Pintor21L0]_
494

495
    Args:
496
        steps: Number of iterations.
497
        max_stepsize: Initial stepsize for the gradient update.
498
        min_stepsize: Final stepsize for the gradient update. The
499
            stepsize will be reduced with a cosine annealing policy.
500
        gamma: Initial stepsize for the epsilon update. It will
501
            be updated with a cosine annealing reduction up to 0.001.
502
        init_attack: Optional initial attack. If an initial attack
503
            is specified (or initial points are provided in the run), the
504
            attack will first try to search for the boundary between the
505
            initial point and the points in a class that satisfies the
506
            adversarial criterion.
507
        binary_search_steps: Number of steps to use for the search
508
            from the adversarial points. If no initial attack or adversarial
509
            starting point is provided, this parameter will be ignored.
510

511
    References:
512
        .. [#Pintor21L0] Maura Pintor, Fabio Roli, Wieland Brendel,
513
            Battista Biggio, "Fast Minimum-norm Adversarial
514
            Attacks through Adaptive Norm Constraints."
515
            arXiv preprint arXiv:2102.12827 (2021).
516
            https://arxiv.org/abs/2102.12827
517
    """
518

519
    distance = l0
10✔
520
    p = 0
10✔
521

522
    def project(self, x: ep.Tensor, x0: ep.Tensor, epsilon: ep.Tensor) -> ep.Tensor:
10✔
523
        flatten_delta = flatten(x - x0)
6✔
524
        n, d = flatten_delta.shape
6✔
525
        abs_delta = abs(flatten_delta)
6✔
526
        epsilon = epsilon.astype(ep.arange(x, 1).dtype)
6✔
527
        rows = range(n)
6✔
528
        idx_sorted = ep.flip(ep.argsort(abs_delta, axis=1), -1)[rows, epsilon]
6✔
529
        thresholds = (ep.ones_like(flatten_delta).T * abs_delta[rows, idx_sorted]).T
6✔
530
        clipped = ep.where(abs_delta >= thresholds, flatten_delta, 0)
6✔
531
        return x0 + clipped.reshape(x0.shape).astype(x0.dtype)
6✔
532

533
    def mid_points(
10✔
534
        self,
535
        x0: ep.Tensor,
536
        x1: ep.Tensor,
537
        epsilons: ep.Tensor,
538
        bounds: Tuple[float, float],
539
    ) -> ep.Tensor:
540
        # returns a point between x0 and x1 where
541
        # epsilon = 0 returns x0 and epsilon = 1
542
        # returns x1
543
        # epsilons here will be the percentage of features to keep
544
        n_features = flatten(ep.ones_like(x0)).bool().sum(axis=1).float32()
6✔
545
        new_x = self.project(x1, x0, n_features * epsilons)
6✔
546
        return new_x
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc