Skip to content

Neural Network Visualiser

interpretation.explainer.nn.nn_vis.NNVis

Bases: NNExplainer

Visualize learned features of neural networks via gradient ascent. This is ideal for visualization of models that use visual features, e.g. image classification

Source code in interpretation/explainer/nn/nn_vis/explainer.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
class NNVis(NNExplainer):
    """Visualize learned features of neural networks via gradient ascent. This is ideal for visualization of models that use visual features, e.g. image classification""" 
    ## only for torch and tf
    def __init__(self, input_model):
        super().__init__(input_model)
        if isinstance(input_model, torch.nn.Module):
            self.istorch = True
        elif isinstance(input_model, tf.keras.Model):
            self.istorch = False
        else:
            raise TypeError(f"`NNVis` only support Torch or TensorFlow models.")


    def visualize(
        self,
        layer_identifier: int | str,
        input_shape: tuple[int, ...],
        channel_idx: int = 0,
        lr: float = 1e-3,
        max_iter: int = 200,
        weight_decay: float = 1e-3,
        max_jitter: int = 2,
        blur_sigma: float = 0.5,
        blur_every: int = 4,
        clipping: tuple[float, float] | None = None
    ) -> npt.NDArray:
        """Generate the feature visualisation, based on optimisation of maximum activation with respect to a given layer by gradient ascent.
        We use L2, Gaussian blurring and jittering regularisation strategies to prevent noisy outputs.

        Parameters
        ----------
        layer_identifier : int | str
            The layer we maximize the input with respect to.
        input_shape : tuple[int, ...]
            The shape of the input we are optimizing.
        channel_idx : int
            The channel of the image we are maximizing on, by default 0.
        lr : float, optional
            Learning rate of the gradient ascent, by default 1e-3
        max_iter : int, optional
            Maximum number of iterations of the gradient ascent by default 200
        weight_decay : float, optional
            L2 regularization strength of the gradient ascent, by default 1e-3
        max_jitter : int, optional
            Maximum number of pixels jitter, by default 2
        blur_sigma : float, optional
            Sigma of the Gaussian blurring, by default 0.5
        blur_every : int, optional
            Number of iterations between each Gaussian blurring, by default 4
        clipping : tuple[float, float] | None, optional
            Clipping of the image values to prevent overflow, by default (0.0, 1.0)

        Returns
        -------
        npt.NDArray
            The input generated that maximizes the activation at the specified layer.

        Raises
        ------
        ValueError
            Input shape is not 1-d, 2-d or 3-d.
        """
        if self.istorch:
            objective_fn = lambda x : x[:, channel_idx, :, :].mean()
        else:
            objective_fn = lambda x : tf.reduce_mean(x[:, :, :, channel_idx])

        input_shape = np.asarray(input_shape)

        if self.istorch and input_shape.ndim == 3:
            objective_fn = lambda x : x[channel_idx, :, :].mean()
        elif not self.istorch and input_shape.ndim == 3:
            objective_fn = lambda x : tf.reduce_mean(x[:, :, channel_idx])
        elif self.istorch and (input_shape.ndim == 2 or input_shape.ndim == 1):
            objective_fn = lambda x : x.mean()
        elif not self.istorch and (input_shape.ndim == 2 or input_shape.ndim == 1):
            objective_fn = lambda x : tf.reduce_mean(x)
        else:
            raise ValueError("Input shape can only be 1-d, 2-d or 3-d.")

        X = np.random.normal(0.5, 0.1, size=input_shape)[None, :]

        for step in range(1, max_iter + 1):
            if blur_sigma > 0 and step % blur_every == 0:
                if input_shape.ndim < 3:
                    X = gaussian_filter(X, sigma=blur_sigma)
                else:
                    if self.istorch:
                        for j in range(X.shape[1]):
                            X[0, j] = gaussian_filter(X[0, j], sigma=blur_sigma)
                    else:
                        for j in range(X.shape[-1]):
                            X[0, :, :, j] = gaussian_filter(X[0, :, :, j], sigma=blur_sigma)

            ox, oy = np.random.randint(-max_jitter, max_jitter + 1, size=2)

            if input_shape.ndim == 1:
                X_jitter = np.roll(X, ox)
            elif input_shape.ndim == 2:
                X_jitter = np.roll(np.roll(X, ox, axis=0), oy, axis=1)
            else:
                if self.istorch:
                    X_jitter = np.roll(np.roll(X, ox, axis=-2), oy, axis=-1)
                else:
                    X_jitter = np.roll(np.roll(X, ox, axis=1), oy, axis=2)

            grad = self.model.compute_gradients(
                X_jitter,
                objective=layer_identifier,
                objective_fn=objective_fn
            )

            if input_shape.ndim == 1:
                grad = np.roll(grad, -ox)
            elif input_shape.ndim == 2:
                grad = np.roll(np.roll(grad, -ox, axis=0), -oy, axis=1)
            else:
                if self.istorch:
                    grad = np.roll(np.roll(grad, -ox, axis=-2), -oy, axis=-1)
                else:
                    grad = np.roll(np.roll(grad, -ox, axis=1), -oy, axis=2)

            grad_std = np.std(grad) + 1e-8
            grad = grad / grad_std

            X += lr * grad - weight_decay * (X ** 2)

            if clipping is not None:
                X = np.clip(X, clipping[0], clipping[1])

        return X

    def plot_visualization(
        self,
        layer_identifier: int | str,
        input_shape: tuple[int, ...],
        channel_idx: int = 0,
        lr: float = 1e-3,
        max_iter: int = 200,
        weight_decay: float = 1e-3,
        max_jitter: int = 2,
        blur_sigma: float = 0.5,
        blur_every: int = 4,
        clipping: tuple[float, float] | None = None,
        ax = None
    ) -> npt.NDArray:
        """Plots the feature visualization on a matplotlib axis."""
        kwargs = locals()
        kwargs.pop("self")
        ax = kwargs.pop("ax")

        img = self.visualize(**kwargs)

        if ax is None:
            fig, ax = plt.subplots(figsize=(8,8))
        else:
            fig = ax.figure

        img_min, img_max = img.min(), img.max()
        img = (img - img_min) / (img_max - img_min + 1e-8)
        img_disp = np.transpose(img.squeeze(), (1, 2, 0))

        ax.imshow(img_disp)
        ax.axis("off")

        return fig, ax

plot_visualization(layer_identifier, input_shape, channel_idx=0, lr=0.001, max_iter=200, weight_decay=0.001, max_jitter=2, blur_sigma=0.5, blur_every=4, clipping=None, ax=None)

Plots the feature visualization on a matplotlib axis.

Source code in interpretation/explainer/nn/nn_vis/explainer.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def plot_visualization(
    self,
    layer_identifier: int | str,
    input_shape: tuple[int, ...],
    channel_idx: int = 0,
    lr: float = 1e-3,
    max_iter: int = 200,
    weight_decay: float = 1e-3,
    max_jitter: int = 2,
    blur_sigma: float = 0.5,
    blur_every: int = 4,
    clipping: tuple[float, float] | None = None,
    ax = None
) -> npt.NDArray:
    """Plots the feature visualization on a matplotlib axis."""
    kwargs = locals()
    kwargs.pop("self")
    ax = kwargs.pop("ax")

    img = self.visualize(**kwargs)

    if ax is None:
        fig, ax = plt.subplots(figsize=(8,8))
    else:
        fig = ax.figure

    img_min, img_max = img.min(), img.max()
    img = (img - img_min) / (img_max - img_min + 1e-8)
    img_disp = np.transpose(img.squeeze(), (1, 2, 0))

    ax.imshow(img_disp)
    ax.axis("off")

    return fig, ax

visualize(layer_identifier, input_shape, channel_idx=0, lr=0.001, max_iter=200, weight_decay=0.001, max_jitter=2, blur_sigma=0.5, blur_every=4, clipping=None)

Generate the feature visualisation, based on optimisation of maximum activation with respect to a given layer by gradient ascent. We use L2, Gaussian blurring and jittering regularisation strategies to prevent noisy outputs.

Parameters:

Name Type Description Default
layer_identifier int | str

The layer we maximize the input with respect to.

required
input_shape tuple[int, ...]

The shape of the input we are optimizing.

required
channel_idx int

The channel of the image we are maximizing on, by default 0.

0
lr float

Learning rate of the gradient ascent, by default 1e-3

0.001
max_iter int

Maximum number of iterations of the gradient ascent by default 200

200
weight_decay float

L2 regularization strength of the gradient ascent, by default 1e-3

0.001
max_jitter int

Maximum number of pixels jitter, by default 2

2
blur_sigma float

Sigma of the Gaussian blurring, by default 0.5

0.5
blur_every int

Number of iterations between each Gaussian blurring, by default 4

4
clipping tuple[float, float] | None

Clipping of the image values to prevent overflow, by default (0.0, 1.0)

None

Returns:

Type Description
NDArray

The input generated that maximizes the activation at the specified layer.

Raises:

Type Description
ValueError

Input shape is not 1-d, 2-d or 3-d.

Source code in interpretation/explainer/nn/nn_vis/explainer.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def visualize(
    self,
    layer_identifier: int | str,
    input_shape: tuple[int, ...],
    channel_idx: int = 0,
    lr: float = 1e-3,
    max_iter: int = 200,
    weight_decay: float = 1e-3,
    max_jitter: int = 2,
    blur_sigma: float = 0.5,
    blur_every: int = 4,
    clipping: tuple[float, float] | None = None
) -> npt.NDArray:
    """Generate the feature visualisation, based on optimisation of maximum activation with respect to a given layer by gradient ascent.
    We use L2, Gaussian blurring and jittering regularisation strategies to prevent noisy outputs.

    Parameters
    ----------
    layer_identifier : int | str
        The layer we maximize the input with respect to.
    input_shape : tuple[int, ...]
        The shape of the input we are optimizing.
    channel_idx : int
        The channel of the image we are maximizing on, by default 0.
    lr : float, optional
        Learning rate of the gradient ascent, by default 1e-3
    max_iter : int, optional
        Maximum number of iterations of the gradient ascent by default 200
    weight_decay : float, optional
        L2 regularization strength of the gradient ascent, by default 1e-3
    max_jitter : int, optional
        Maximum number of pixels jitter, by default 2
    blur_sigma : float, optional
        Sigma of the Gaussian blurring, by default 0.5
    blur_every : int, optional
        Number of iterations between each Gaussian blurring, by default 4
    clipping : tuple[float, float] | None, optional
        Clipping of the image values to prevent overflow, by default (0.0, 1.0)

    Returns
    -------
    npt.NDArray
        The input generated that maximizes the activation at the specified layer.

    Raises
    ------
    ValueError
        Input shape is not 1-d, 2-d or 3-d.
    """
    if self.istorch:
        objective_fn = lambda x : x[:, channel_idx, :, :].mean()
    else:
        objective_fn = lambda x : tf.reduce_mean(x[:, :, :, channel_idx])

    input_shape = np.asarray(input_shape)

    if self.istorch and input_shape.ndim == 3:
        objective_fn = lambda x : x[channel_idx, :, :].mean()
    elif not self.istorch and input_shape.ndim == 3:
        objective_fn = lambda x : tf.reduce_mean(x[:, :, channel_idx])
    elif self.istorch and (input_shape.ndim == 2 or input_shape.ndim == 1):
        objective_fn = lambda x : x.mean()
    elif not self.istorch and (input_shape.ndim == 2 or input_shape.ndim == 1):
        objective_fn = lambda x : tf.reduce_mean(x)
    else:
        raise ValueError("Input shape can only be 1-d, 2-d or 3-d.")

    X = np.random.normal(0.5, 0.1, size=input_shape)[None, :]

    for step in range(1, max_iter + 1):
        if blur_sigma > 0 and step % blur_every == 0:
            if input_shape.ndim < 3:
                X = gaussian_filter(X, sigma=blur_sigma)
            else:
                if self.istorch:
                    for j in range(X.shape[1]):
                        X[0, j] = gaussian_filter(X[0, j], sigma=blur_sigma)
                else:
                    for j in range(X.shape[-1]):
                        X[0, :, :, j] = gaussian_filter(X[0, :, :, j], sigma=blur_sigma)

        ox, oy = np.random.randint(-max_jitter, max_jitter + 1, size=2)

        if input_shape.ndim == 1:
            X_jitter = np.roll(X, ox)
        elif input_shape.ndim == 2:
            X_jitter = np.roll(np.roll(X, ox, axis=0), oy, axis=1)
        else:
            if self.istorch:
                X_jitter = np.roll(np.roll(X, ox, axis=-2), oy, axis=-1)
            else:
                X_jitter = np.roll(np.roll(X, ox, axis=1), oy, axis=2)

        grad = self.model.compute_gradients(
            X_jitter,
            objective=layer_identifier,
            objective_fn=objective_fn
        )

        if input_shape.ndim == 1:
            grad = np.roll(grad, -ox)
        elif input_shape.ndim == 2:
            grad = np.roll(np.roll(grad, -ox, axis=0), -oy, axis=1)
        else:
            if self.istorch:
                grad = np.roll(np.roll(grad, -ox, axis=-2), -oy, axis=-1)
            else:
                grad = np.roll(np.roll(grad, -ox, axis=1), -oy, axis=2)

        grad_std = np.std(grad) + 1e-8
        grad = grad / grad_std

        X += lr * grad - weight_decay * (X ** 2)

        if clipping is not None:
            X = np.clip(X, clipping[0], clipping[1])

    return X