Source code for secmlt.trackers.tensorboard_tracker
"""Tensorboard tracking utilities."""
from __future__ import annotations # noqa: I001
from secmlt.trackers.trackers import (
IMAGE,
MULTI_SCALAR,
SCALAR,
GradientNormTracker,
LossTracker,
Tracker,
)
from torch.utils.tensorboard import SummaryWriter
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import torch
[docs]
class TensorboardTracker(Tracker):
"""Tracker for Tensorboard. Uses other trackers as subscribers."""
def __init__(self, logdir: str, trackers: list[Tracker] | None = None) -> None:
"""
Create tensorboard tracker.
Parameters
----------
logdir : str
Folder to store tensorboard logs.
trackers : list[Tracker] | None, optional
List of trackers subsctibed to the updates, by default None.
"""
super().__init__(name="Tensorboard")
if trackers is None:
trackers = [
LossTracker(),
GradientNormTracker(),
]
self.writer = SummaryWriter(log_dir=logdir)
self.trackers = trackers
self._global_sample_offset = 0
@property
def requires_grad(self) -> bool:
"""True when any sub-tracker needs gradients."""
return any(getattr(t, "requires_grad", False) for t in self.trackers)
@requires_grad.setter
def requires_grad(self, value: bool) -> None:
del value
# computed dynamically; ignore assignments from Tracker.__init__
@property
def loss_fn(self) -> object | None:
"""Return the first sub-tracker loss function, if any."""
for tracker in self.trackers:
lf = getattr(tracker, "loss_fn", None)
if lf is not None:
return lf
return None
[docs]
def init_tracking(self) -> None:
"""Initialize tracking for a new batch."""
for tracker in self.trackers:
tracker.init_tracking()
[docs]
def end_tracking(self) -> None:
"""End tracking for current batch and update global sample offset."""
# Calculate batch size from first tracker
if (
self.trackers
and hasattr(self.trackers[0], "tracked")
and isinstance(self.trackers[0].tracked, list)
and len(self.trackers[0].tracked) > 0
):
# Get batch size from first iteration
batch_size = self.trackers[0].tracked[0].shape[0]
self._global_sample_offset += batch_size
for tracker in self.trackers:
tracker.end_tracking()
[docs]
def track(
self,
iteration: int,
loss: torch.Tensor,
scores: torch.Tensor,
x_adv: torch.tensor,
delta: torch.Tensor,
grad: torch.Tensor,
) -> None:
"""
Update all subscribed trackers.
Parameters
----------
iteration : int
The attack iteration number.
loss : torch.Tensor
The value of the (per-sample) loss of the attack.
scores : torch.Tensor
The output scores from the model.
x_adv : torch.tensor
The adversarial examples at the current iteration.
delta : torch.Tensor
The adversarial perturbations at the current iteration.
grad : torch.Tensor
The gradient of delta at the given iteration.
"""
for tracker in self.trackers:
tracker.track(iteration, loss, scores, x_adv, delta, grad)
tracked_value = tracker.get_last_tracked()
if tracked_value is None:
continue
for i, sample in enumerate(tracked_value):
global_i = self._global_sample_offset + i # Use global sample index
if tracker.tracked_type == SCALAR:
self.writer.add_scalar(
f"Sample #{global_i}/{tracker.name}",
sample,
global_step=iteration,
)
elif tracker.tracked_type == MULTI_SCALAR:
self.writer.add_scalars(
main_tag=f"Sample #{global_i}/{tracker.name}",
tag_scalar_dict={
f"Sample #{global_i}/{tracker.name}{j}": v
for j, v in enumerate(sample)
},
global_step=iteration,
)
elif tracker.tracked_type == IMAGE:
self.writer.add_image(
f"Sample #{global_i}/{tracker.name}",
sample,
global_step=iteration,
)
[docs]
def get_last_tracked(self) -> NotImplementedError:
"""Not implemented for this tracker."""
return NotImplementedError(
"Last tracked value is not available for this tracker.",
)