Skip to content

Attack functions

These are the functions ASRAttacker calls. Import them from asr_attacks.attacks if you want to call them directly.

fgsm

fgsm(backend: ASRBackend, audio: Tensor | ndarray, *, epsilon: float = 0.2, targeted: bool = False, target: str | list[str] | None = None, label: str | list[str] | None = None, norm: float | str = 'inf') -> np.ndarray

Fast Gradient Sign / Fast Gradient Method, one step.

x_adv = clip_[-1, 1](x + epsilon * s * normalize_p(grad_x CTC(f(x), y))) with s = +1 untargeted (y = label or the clean greedy decode) and s = -1 targeted (y = target). normalize_p is sign(g) for norm="inf", g / ||g||_2 for norm=2 and g / ||g||_1 for norm=1 (the ART convention).

  • L-inf is FGSM (Goodfellow et al., 2015, https://arxiv.org/abs/1412.6572).
  • L2 / L1 are the Fast Gradient Method, and targeted FGSM follows Kurakin et al., 2017 (https://arxiv.org/abs/1611.01236).

label is the ground-truth transcript for untargeted attacks (the Goodfellow et al. setup). By default the model's clean prediction is used, as in Kurakin et al. 2017 and ART. Passing label with targeted=True raises ValueError.

Source code in src/asr_attacks/attacks/fgsm.py
def fgsm(
    backend: ASRBackend,
    audio: torch.Tensor | np.ndarray,
    *,
    epsilon: float = 0.2,
    targeted: bool = False,
    target: str | list[str] | None = None,
    label: str | list[str] | None = None,
    norm: float | str = "inf",
) -> np.ndarray:
    """Fast Gradient Sign / Fast Gradient Method, one step.

    ``x_adv = clip_[-1, 1](x + epsilon * s * normalize_p(grad_x CTC(f(x), y)))``
    with ``s = +1`` untargeted (``y`` = ``label`` or the clean greedy decode) and
    ``s = -1`` targeted (``y`` = ``target``). ``normalize_p`` is ``sign(g)`` for
    ``norm="inf"``, ``g / ||g||_2`` for ``norm=2`` and ``g / ||g||_1`` for
    ``norm=1`` (the ART convention).

    - L-inf is FGSM (Goodfellow et al., 2015, https://arxiv.org/abs/1412.6572).
    - L2 / L1 are the Fast Gradient Method, and targeted FGSM follows Kurakin
      et al., 2017 (https://arxiv.org/abs/1611.01236).

    ``label`` is the ground-truth transcript for untargeted attacks (the
    Goodfellow et al. setup). By default the model's clean prediction is used,
    as in Kurakin et al. 2017 and ART. Passing ``label`` with ``targeted=True``
    raises ``ValueError``.
    """
    _parse_norm(norm)
    original = prepare_audio(audio, backend.device).detach()
    adversarial = original.clone().requires_grad_(True)
    target_text = resolve_target(backend, original, target, targeted, label)
    target_ids = backend.encode(target_text)
    logits = backend.logits(adversarial)
    loss = ctc_loss(logits, target_ids, backend.blank_id)
    loss.backward()
    if adversarial.grad is None:
        raise RuntimeError(
            "FGSM did not receive input gradients; check that the model is differentiable"
        )
    sign = -1.0 if targeted else 1.0
    step = epsilon * sign * normalized_step(adversarial.grad, norm)
    perturbed = torch.clamp(adversarial.detach() + step, -1.0, 1.0)
    return to_numpy(perturbed)

bim

bim(backend: ASRBackend, audio: Tensor | ndarray, *, epsilon: float = 0.2, alpha: float | None = None, num_iter: int | None = None, targeted: bool = False, target: str | list[str] | None = None, label: str | list[str] | None = None, nested: bool = True, early_stop: bool = False, verbose: bool = True, norm: float | str = 'inf') -> np.ndarray

Basic Iterative Method (Kurakin et al., 2017).

Paper: https://arxiv.org/abs/1607.02533

x_{n+1} = Clip_{X,eps}(x_n + alpha * s * normalize_p(grad CTC(f(x_n), y))) with s = +1 untargeted and s = -1 targeted. Clip_{X,eps} is the L-norm ball projection around the clean waveform followed by clamping to [-1, 1]. normalize_p is sign(g) for norm="inf", g / ||g||_2 for norm=2, and g / ||g||_1 for norm=1.

Defaults: alpha = epsilon / 10; num_iter follows the paper rule ceil(min(eps/alpha + 4, 1.25 * eps/alpha)). The paper is L-inf only; norm=2 / norm=1 are package extensions ("L2 / L1 BIM"). Targeted mode with a user-chosen transcript covers the paper's least-likely-class idea (ASR has no natural least-likely transcript). Early stopping is a package extension (not in the paper or ART BIM).

Source code in src/asr_attacks/attacks/iterative.py
def bim(
    backend: ASRBackend,
    audio: torch.Tensor | np.ndarray,
    *,
    epsilon: float = 0.2,
    alpha: float | None = None,
    num_iter: int | None = None,
    targeted: bool = False,
    target: str | list[str] | None = None,
    label: str | list[str] | None = None,
    nested: bool = True,
    early_stop: bool = False,
    verbose: bool = True,
    norm: float | str = "inf",
) -> np.ndarray:
    """Basic Iterative Method (Kurakin et al., 2017).

    Paper: https://arxiv.org/abs/1607.02533

    ``x_{n+1} = Clip_{X,eps}(x_n + alpha * s * normalize_p(grad CTC(f(x_n), y)))``
    with ``s = +1`` untargeted and ``s = -1`` targeted. ``Clip_{X,eps}`` is the
    L-``norm`` ball projection around the clean waveform followed by clamping to
    ``[-1, 1]``. ``normalize_p`` is ``sign(g)`` for ``norm="inf"``,
    ``g / ||g||_2`` for ``norm=2``, and ``g / ||g||_1`` for ``norm=1``.

    Defaults: ``alpha = epsilon / 10``; ``num_iter`` follows the paper rule
    ``ceil(min(eps/alpha + 4, 1.25 * eps/alpha))``. The paper is L-inf only;
    ``norm=2`` / ``norm=1`` are package extensions ("L2 / L1 BIM"). Targeted
    mode with a user-chosen transcript covers the paper's least-likely-class
    idea (ASR has no natural least-likely transcript). Early stopping is a
    package extension (not in the paper or ART BIM).
    """
    if alpha is None:
        alpha = epsilon / 10.0
    if num_iter is None:
        num_iter = bim_num_iter(epsilon, alpha)
    return _iterative(
        backend,
        audio,
        epsilon=epsilon,
        alpha=alpha,
        num_iter=num_iter,
        norm=norm,
        targeted=targeted,
        target=target,
        label=label,
        nested=nested,
        early_stop=early_stop,
        random_start=False,
        verbose=verbose,
        desc=None,
    )

pgd

pgd(backend: ASRBackend, audio: Tensor | ndarray, *, epsilon: float = 0.3, alpha: float | None = None, num_iter: int = 40, targeted: bool = False, target: str | list[str] | None = None, label: str | list[str] | None = None, nested: bool = True, early_stop: bool = False, random_start: bool = True, verbose: bool = True, norm: float | str = 'inf', restarts: int = 1) -> np.ndarray

Projected Gradient Descent (Madry et al., 2018).

Paper: https://arxiv.org/abs/1706.06083

x_{n+1} = Proj_{X,eps}(x_n + alpha * s * normalize_p(grad CTC(f(x_n), y))) after an optional random start inside the L-norm ball. s = +1 untargeted, s = -1 targeted. Default alpha = 2.5 * epsilon / num_iter.

L-inf is the main Madry attack; L2 (normalized gradient steps) is cited to Sec. 5 of the paper. L1 follows the ART convention and is a package extension. Early stopping is also a package extension. PGD with the CW margin loss is not provided (no direct CTC equivalent).

restarts runs the loop from a fresh random start each time. The kept result is any successful restart (attack_succeeded vs the clean-audio reference), else the restart with the strongest final CTC loss (highest untargeted, lowest targeted). restarts > 1 requires random_start. With early_stop=True, the first successful restart is returned immediately.

Source code in src/asr_attacks/attacks/iterative.py
def pgd(
    backend: ASRBackend,
    audio: torch.Tensor | np.ndarray,
    *,
    epsilon: float = 0.3,
    alpha: float | None = None,
    num_iter: int = 40,
    targeted: bool = False,
    target: str | list[str] | None = None,
    label: str | list[str] | None = None,
    nested: bool = True,
    early_stop: bool = False,
    random_start: bool = True,
    verbose: bool = True,
    norm: float | str = "inf",
    restarts: int = 1,
) -> np.ndarray:
    """Projected Gradient Descent (Madry et al., 2018).

    Paper: https://arxiv.org/abs/1706.06083

    ``x_{n+1} = Proj_{X,eps}(x_n + alpha * s * normalize_p(grad CTC(f(x_n), y)))``
    after an optional random start inside the L-``norm`` ball. ``s = +1``
    untargeted, ``s = -1`` targeted. Default ``alpha = 2.5 * epsilon / num_iter``.

    L-inf is the main Madry attack; L2 (normalized gradient steps) is cited to
    Sec. 5 of the paper. L1 follows the ART convention and is a package
    extension. Early stopping is also a package extension. PGD with the CW
    margin loss is not provided (no direct CTC equivalent).

    ``restarts`` runs the loop from a fresh random start each time. The kept
    result is any successful restart (``attack_succeeded`` vs the clean-audio
    reference), else the restart with the strongest final CTC loss (highest
    untargeted, lowest targeted). ``restarts > 1`` requires ``random_start``.
    With ``early_stop=True``, the first successful restart is returned immediately.
    """
    if restarts < 1:
        raise ValueError(f"restarts must be >= 1; got {restarts}")
    if restarts > 1 and not random_start:
        raise ValueError("restarts > 1 requires random_start=True")
    if alpha is None:
        alpha = 2.5 * epsilon / num_iter

    original = prepare_audio(audio, backend.device).detach()
    target_text = resolve_target(backend, original, target, targeted, label)
    target_ids = backend.encode(target_text)

    best: np.ndarray | None = None
    best_success: bool | None = None
    best_loss: float | None = None

    for _ in range(restarts):
        candidate = _iterative(
            backend,
            audio,
            epsilon=epsilon,
            alpha=alpha,
            num_iter=num_iter,
            norm=norm,
            targeted=targeted,
            nested=nested,
            early_stop=early_stop,
            random_start=random_start,
            verbose=verbose,
            desc=None,
            target_text=target_text,
        )
        hypothesis = backend.decode(prepare_audio(candidate, backend.device))
        success = attack_succeeded(hypothesis, target_text, targeted=targeted)
        if early_stop and success:
            return candidate
        loss = _ctc_value(backend, candidate, target_ids)
        if _prefer_restart(
            success=success,
            loss=loss,
            best_success=best_success,
            best_loss=best_loss,
            targeted=targeted,
        ):
            best = candidate
            best_success = success
            best_loss = loss

    assert best is not None
    return best

cw

cw(backend: ASRBackend, audio: Tensor | ndarray, *, epsilon: float = _CW_EPS, c: float = 1.0, learning_rate: float = _CW_LR, num_iter: int = 5000, decrease_factor_eps: float = 0.8, num_iter_decrease_eps: int = 10, check_every: int | None = None, optimizer: str | None = 'adam', nested: bool = True, early_stop: bool = False, search_eps: bool = True, targeted: bool = False, target: str | list[str] | None = None, label: str | list[str] | None = None, db_bound: float | None = None, loss: str = 'ctc', kappa: float = 0.0, verbose: bool = True, desc: str | None = None, return_tensor: bool = False) -> np.ndarray | torch.Tensor

Audio Carlini & Wagner attack (arXiv:1801.01944).

Sec. III-B minimizes ||delta||_2^2 + c * CTC(x+delta, t) inside an L-inf box (epsilon, or db_bound), with Adam and paper-style bound shrinking. Untargeted mode flips the CTC sign and is a package extension.

Sec. III-C (loss="margin", targeted only) refines a CTC solution with a per-frame hinge on logits under a fixed greedy alignment.

Sec. III-F allows target="": a silence-token hinge replaces CTC, and success is an empty displayed transcript.

Source code in src/asr_attacks/attacks/cw.py
def cw(
    backend: ASRBackend,
    audio: torch.Tensor | np.ndarray,
    *,
    epsilon: float = _CW_EPS,
    c: float = 1.0,
    learning_rate: float = _CW_LR,
    num_iter: int = 5000,
    decrease_factor_eps: float = 0.8,
    num_iter_decrease_eps: int = 10,
    check_every: int | None = None,
    optimizer: str | None = "adam",
    nested: bool = True,
    early_stop: bool = False,
    search_eps: bool = True,
    targeted: bool = False,
    target: str | list[str] | None = None,
    label: str | list[str] | None = None,
    db_bound: float | None = None,
    loss: str = "ctc",
    kappa: float = 0.0,
    verbose: bool = True,
    desc: str | None = None,
    return_tensor: bool = False,
) -> np.ndarray | torch.Tensor:
    """Audio Carlini & Wagner attack ([arXiv:1801.01944](https://arxiv.org/abs/1801.01944)).

    Sec. III-B minimizes ``||delta||_2^2 + c * CTC(x+delta, t)`` inside an L-inf
    box (``epsilon``, or ``db_bound``), with Adam and paper-style bound shrinking.
    Untargeted mode flips the CTC sign and is a package extension.

    Sec. III-C (``loss="margin"``, targeted only) refines a CTC solution with a
    per-frame hinge on logits under a fixed greedy alignment.

    Sec. III-F allows ``target=""``: a silence-token hinge replaces CTC, and
    success is an empty displayed transcript.
    """
    if early_stop and search_eps:
        raise ValueError("early_stop and search_eps cannot both be True")
    loss_key = str(loss).lower()
    if loss_key not in {"ctc", "margin"}:
        raise ValueError("loss must be 'ctc' or 'margin'")
    if loss_key == "margin" and not targeted:
        raise ValueError("loss='margin' is targeted only")

    original = prepare_audio(audio, backend.device).detach()
    if db_bound is not None:
        peak = float(original.abs().max())
        if peak <= 0.0:
            raise ValueError("db_bound requires a non-zero peak amplitude")
        epsilon = peak * (10.0 ** (float(db_bound) / 20.0))
    if epsilon <= 0:
        raise ValueError("epsilon must be greater than 0")

    target_text = resolve_target(backend, original, target, targeted, label)
    silence = targeted and display_text(target_text) == ""
    interval = _resolve_check_every(check_every, num_iter_decrease_eps)

    stage_one, eps_after = _bounded_search(
        backend,
        original,
        target_text,
        targeted=targeted,
        epsilon=epsilon,
        learning_rate=learning_rate,
        num_iter=num_iter,
        decrease_factor_eps=decrease_factor_eps,
        check_every=interval,
        optimizer=optimizer,
        search_eps=search_eps,
        early_stop=early_stop,
        nested=nested,
        verbose=verbose,
        desc=desc,
        use_l2=True,
        c=c,
        sign_grad=False,
        silence=silence,
    )

    if loss_key == "ctc":
        if return_tensor:
            return stage_one
        return to_numpy(stage_one)

    if not attack_succeeded(backend.decode(stage_one), target_text, targeted=True):
        warnings.warn(
            "CW margin stage skipped: CTC stage did not reach the target transcript.",
            UserWarning,
            stacklevel=2,
        )
        if return_tensor:
            return stage_one
        return to_numpy(stage_one)

    refined = _cw_margin_stage(
        backend,
        original,
        stage_one,
        epsilon=eps_after if search_eps else epsilon,
        c=c,
        learning_rate=learning_rate,
        num_iter=num_iter,
        decrease_factor_eps=decrease_factor_eps,
        check_every=interval,
        optimizer=optimizer,
        search_eps=search_eps,
        early_stop=early_stop,
        nested=nested,
        verbose=verbose,
        kappa=kappa,
    )
    if return_tensor:
        return refined
    return to_numpy(refined)

imperceptible

imperceptible(backend: ASRBackend, audio: Tensor | ndarray, target: str | list[str] | None = None, *, epsilon: float = _EPS_QIN, c: float | None = None, learning_rate1: float = _LR1_QIN, learning_rate2: float = _LR2_QIN, num_iter1: int = 1000, num_iter2: int = 4000, decrease_factor_eps: float = 0.8, num_iter_decrease_eps: int = 10, check_every: int | None = None, optimizer1: str | None = 'sgd', optimizer2: str | None = 'adam', nested: bool = True, early_stop_cw: bool = False, search_eps_cw: bool = True, alpha: float = 0.05, sample_rate: int = 16000, verbose: bool = True, targeted: bool = True, label: str | list[str] | None = None, mode: str = 'imperceptible', rooms=None, robust_delta: float = _ROBUST_DELTA, num_iter_r1: int = 2000, num_iter_r2: int = 4000, learning_rate_r1: float = _LR_R1, learning_rate_r2: float = _LR_R2, num_iter_ir1: int = 4000, num_iter_ir2: int = 6000, learning_rate_ir1: float = _LR2_QIN, learning_rate_ir2: float = _LR_IR2) -> np.ndarray

Qin et al. imperceptible / robust ASR attack (arXiv:1903.10346).

mode="imperceptible" (default) follows Sec. 4 offline: stage 1 is a CTC-only L-inf bound search with signed gradient steps (Algorithm 1); stage 2 minimizes CTC + alpha * l_theta with Adam, waveform clamped to [-1, 1]. The paper's Lingvo model uses cross-entropy; CTC backends use CTC for l_net.

mode="robust" / "imperceptible_robust" need rooms= (see :mod:asr_attacks.rooms). IR2 uses a package choice of alpha *= 0.8 every 50 failing iterations (the paper gives no decrease rule).

Untargeted mode (targeted=False) is a package extension: stage 1 maximizes CTC against the reference; stage 2 uses a hinge on the stage-1 CTC value.

c is deprecated and ignored (with a warning) when not None.

Source code in src/asr_attacks/attacks/imperceptible.py
def imperceptible(
    backend: ASRBackend,
    audio: torch.Tensor | np.ndarray,
    target: str | list[str] | None = None,
    *,
    epsilon: float = _EPS_QIN,
    c: float | None = None,
    learning_rate1: float = _LR1_QIN,
    learning_rate2: float = _LR2_QIN,
    num_iter1: int = 1000,
    num_iter2: int = 4000,
    decrease_factor_eps: float = 0.8,
    num_iter_decrease_eps: int = 10,
    check_every: int | None = None,
    optimizer1: str | None = "sgd",
    optimizer2: str | None = "adam",
    nested: bool = True,
    early_stop_cw: bool = False,
    search_eps_cw: bool = True,
    alpha: float = 0.05,
    sample_rate: int = 16000,
    verbose: bool = True,
    targeted: bool = True,
    label: str | list[str] | None = None,
    mode: str = "imperceptible",
    rooms=None,
    robust_delta: float = _ROBUST_DELTA,
    num_iter_r1: int = 2000,
    num_iter_r2: int = 4000,
    learning_rate_r1: float = _LR_R1,
    learning_rate_r2: float = _LR_R2,
    num_iter_ir1: int = 4000,
    num_iter_ir2: int = 6000,
    learning_rate_ir1: float = _LR2_QIN,
    learning_rate_ir2: float = _LR_IR2,
) -> np.ndarray:
    """Qin et al. imperceptible / robust ASR attack ([arXiv:1903.10346](https://arxiv.org/abs/1903.10346)).

    ``mode="imperceptible"`` (default) follows Sec. 4 offline: stage 1 is a
    CTC-only L-inf bound search with signed gradient steps (Algorithm 1);
    stage 2 minimizes ``CTC + alpha * l_theta`` with Adam, waveform clamped to
    ``[-1, 1]``. The paper's Lingvo model uses cross-entropy; CTC backends use
    CTC for ``l_net``.

    ``mode="robust"`` / ``"imperceptible_robust"`` need ``rooms=`` (see
    :mod:`asr_attacks.rooms`). IR2 uses a package choice of ``alpha *= 0.8``
    every 50 failing iterations (the paper gives no decrease rule).

    Untargeted mode (``targeted=False``) is a package extension: stage 1
    maximizes CTC against the reference; stage 2 uses a hinge on the stage-1
    CTC value.

    ``c`` is deprecated and ignored (with a warning) when not ``None``.
    """
    mode_key = str(mode).lower()
    if mode_key not in {"imperceptible", "robust", "imperceptible_robust"}:
        raise ValueError("mode must be 'imperceptible', 'robust', or 'imperceptible_robust'")
    if mode_key in {"robust", "imperceptible_robust"} and rooms is None:
        raise ValueError(f"mode={mode_key!r} requires rooms=")
    if c is not None:
        warnings.warn(
            "imperceptible(c=...) is deprecated and ignored; stage 2 no longer uses c.",
            DeprecationWarning,
            stacklevel=2,
        )

    original = prepare_audio(audio, backend.device).detach()
    target_text = resolve_target(backend, original, target, targeted, label)
    interval = _resolve_check_every(check_every, num_iter_decrease_eps)
    silence = targeted and display_text(target_text) == ""
    opt1 = "sgd" if optimizer1 is None else optimizer1
    opt2 = "adam" if optimizer2 is None else optimizer2

    if mode_key in {"robust", "imperceptible_robust"}:
        robust_wave, bound = _run_robust(
            backend,
            original,
            target_text,
            targeted=targeted,
            rooms=rooms,
            epsilon=epsilon,
            robust_delta=robust_delta,
            decrease_factor_eps=decrease_factor_eps,
            check_every=interval,
            nested=nested,
            verbose=verbose,
            num_iter_r1=num_iter_r1,
            num_iter_r2=num_iter_r2,
            learning_rate_r1=learning_rate_r1,
            learning_rate_r2=learning_rate_r2,
            optimizer=opt1,
        )
        if mode_key == "robust":
            return to_numpy(robust_wave)
        return _run_imperceptible_robust(
            backend,
            original,
            robust_wave,
            target_text,
            targeted=targeted,
            rooms=rooms,
            bound=bound,
            sample_rate=sample_rate,
            nested=nested,
            verbose=verbose,
            check_every=interval,
            num_iter_ir1=num_iter_ir1,
            num_iter_ir2=num_iter_ir2,
            learning_rate_ir1=learning_rate_ir1,
            learning_rate_ir2=learning_rate_ir2,
        )

    # Offline imperceptible (Sec. 4).
    if early_stop_cw and search_eps_cw:
        raise ValueError("early_stop_cw and search_eps_cw cannot both be True")

    stage_one, _ = _bounded_search(
        backend,
        original,
        target_text,
        targeted=targeted,
        epsilon=epsilon,
        learning_rate=learning_rate1,
        num_iter=num_iter1,
        decrease_factor_eps=decrease_factor_eps,
        check_every=interval,
        optimizer=opt1,
        search_eps=search_eps_cw,
        early_stop=early_stop_cw,
        nested=nested,
        verbose=verbose,
        desc="*****Attack Stage 1*****",
        use_l2=False,
        c=1.0,
        sign_grad=str(opt1).lower() == "sgd",
        silence=silence,
    )

    ctc_ref_stage1 = None
    if not targeted and not silence:
        with torch.no_grad():
            ctc_ref_stage1 = ctc_loss(
                backend.logits(stage_one),
                backend.encode(target_text),
                backend.blank_id,
            ).detach()

    return _imperceptible_stage2(
        backend,
        original,
        stage_one,
        target_text,
        targeted=targeted,
        learning_rate2=learning_rate2,
        num_iter2=num_iter2,
        optimizer2=opt2,
        alpha=alpha,
        sample_rate=sample_rate,
        nested=nested,
        verbose=verbose,
        ctc_ref_stage1=ctc_ref_stage1,
        silence=silence,
    )