File size: 11,011 Bytes
ed46d32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7f9268b
ed46d32
 
e687bc2
ed46d32
 
 
 
 
 
 
 
 
 
e687bc2
ed46d32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e687bc2
ed46d32
 
 
 
e687bc2
 
ed46d32
e687bc2
ed46d32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33075da
 
ed46d32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bf31e24
ed46d32
a964ffc
 
 
 
 
bf31e24
ed46d32
 
 
 
 
 
a6e6126
ed46d32
 
 
7f9268b
ed46d32
 
e687bc2
 
 
ed46d32
e687bc2
ed46d32
 
 
 
 
a964ffc
ed46d32
 
 
e687bc2
 
 
 
 
 
 
 
ed46d32
e687bc2
 
ed46d32
e687bc2
33075da
 
 
 
e687bc2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33075da
ed46d32
 
 
 
 
d98749a
ed46d32
 
 
 
 
 
 
 
 
 
 
 
 
 
d98749a
 
ed46d32
 
33075da
ed46d32
 
 
 
 
a6e6126
ed46d32
 
33075da
ed46d32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33075da
ed46d32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33075da
ed46d32
1
2
3
4
5
6
7
8
9
10
11
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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
import ast
import os

import gradio as gr
import numpy as np
import spaces
import torch
import torch.nn as nn
import torchvision.utils as vutils
from PIL import Image
from torchvision import transforms as T
from torchvision.datasets import Imagenette
from torchvision.models import densenet121, resnet50, vgg11_bn

from lib.dataset import (
    ConditionalTransform,
    FromMyNormalizeToImageNet,
    imagenette_label_to_imagenet,
)
from lib.helpers import maxpool2d_param_extractor, replace_module_with_custom_
from lib.modules import SurrogateSoftMaxPool2d, TwoWayReLU
from lib.pga import PGA

if torch.cuda.is_available():
    device_ = "cuda"
elif torch.backends.mps.is_available():
    device_ = "mps"
else:
    device_ = "cpu"
DEVICE = torch.device(device_)


####
## Data
####


INIT_CLASS = 497

# Predefined class names (shortened for demo)
PREDEFINED_CLASSES = {
    0: "tench",
    217: "English springer",
    482: "cassette player",
    491: "chain saw",
    497: "church",
    566: "French horn",
    569: "garbage truck",
    571: "gas pump",
    574: "golf ball",
    701: "parachute",
    9: "ostrich",
}


def load_imagenet_labels(path="imagenet1000_clsidx_to_labels.txt"):
    with open(path, "r") as f:
        labels = ast.literal_eval(f.read())
    return labels


IMAGENET_LABELS = load_imagenet_labels()


def get_class_name(target_class):
    return IMAGENET_LABELS.get(int(target_class), "Unknown")


def get_dataset(download=False):
    return Imagenette(
        root="./data",
        split="val",  # or "train"
        size="160px",  # can also be "320" or "full"
        download=download,
        transform=None,
        target_transform=imagenette_label_to_imagenet,
    )


# Predefined images from Imagenette val
try:
    DATASET = get_dataset(download=True)
except RuntimeError as e:
    # wierdly, Imagenette raises error if already downloaded (at least in some torchvision versions)
    print(e)
    DATASET = get_dataset(download=False)


def sample_val_img():
    idx = np.random.randint(0, len(DATASET))
    img, _ = DATASET[idx]

    return img


# Load predefined images from examples folder
EXAMPLES_DIR = "examples"
predefined_files = sorted(
    [
        os.path.join(EXAMPLES_DIR, f)
        for f in os.listdir(EXAMPLES_DIR)
        if f.lower().endswith((".png", ".jpg", ".jpeg"))
    ]
)
# PREDEFINED_IMAGES = predefined_files
# [os.path.join(EXAMPLES_DIR, fname)] for fname in predefined_files
PREDEFINED_IMAGES = [
    np.array(Image.open(fname).convert("RGB")) for fname in predefined_files
]


def tensor_to_gradio_image(tensor):
    # tensor: [B, C, H, W] lub [C, H, W]
    if tensor.ndim == 3:
        tensor = tensor.unsqueeze(0)
    grid = vutils.make_grid(tensor, nrow=1, normalize=True, scale_each=True)
    # grid: [C, H, W]
    img = grid.permute(1, 2, 0).cpu().numpy()  # [H, W, C]
    img = (img * 255).clip(0, 255).astype(np.uint8)
    return img


####
## Model
####


# Model mapping
MODEL_MAP = {
    "ResNet50": resnet50,
    "VGG11_BN": vgg11_bn,
    "DenseNet121": densenet121,
}
INIT_MODEL_NAME = "ResNet50"
INIT_MODEL_TEMP = 0.3

current_model = None
current_model_params = None


def get_model(model_name, temp=0.3):
    global current_model, current_model_params

    params = (model_name, temp)
    if current_model is not None and current_model_params == params:
        return current_model

    backbone = MODEL_MAP[model_name](pretrained=True)
    model = nn.Sequential(FromMyNormalizeToImageNet(), backbone)
    model.eval()

    replace_module_with_custom_(
        model, lambda: TwoWayReLU(temperature=temp), original_cls=nn.ReLU
    )
    replace_module_with_custom_(
        model,
        lambda **params: SurrogateSoftMaxPool2d(**params, temperature=temp),
        original_cls=nn.MaxPool2d,
        param_extractor=maxpool2d_param_extractor,
    )

    model = model.to(DEVICE)

    current_model = model
    current_model_params = params
    return model


@spaces.GPU
def run_pullback(
    input_image,
    model_name,
    target_class,
    steps,
    alpha,
    eps,
    temp,
):
    image_transform = ConditionalTransform()
    img_tensor = T.ToPILImage()(input_image)
    img_tensor = image_transform(img_tensor).unsqueeze(0).to(DEVICE)

    model = get_model(model_name, temp=temp)

    # Prepare target
    target = torch.tensor([target_class]).to(DEVICE)

    predicted_top5 = model(img_tensor).topk(5, dim=1)[1].flatten().tolist()  # [0])

    # Compute gradients/perturbation
    atk = PGA(
        model,
        alpha=alpha,
        steps=steps,
        eps=eps,
    )
    atk.set_mode_targeted_by_label()
    perturbed_img, grad = atk(img_tensor, target)

    # Visualize

    diff_img = perturbed_img - img_tensor

    img_tensor = tensor_to_gradio_image(img_tensor)
    diff_img = tensor_to_gradio_image(diff_img)
    perturbed_img = tensor_to_gradio_image(perturbed_img)

    return (perturbed_img, diff_img), (perturbed_img, img_tensor), predicted_top5


with gr.Blocks() as demo:
    gr.Markdown(
        """
        # Semantic Pullbacks: Making AI Transparent

        Our method shows what the model really looks at when making a prediction.
        It amplifies the most important features for the chosen label,
        and these features turn out to align surprisingly well with human perception.
        This makes AI decisions easier to understand and more transparent.
        Future work will enable neuron-specific adjustment of the "temp" hyperparameter, which is expected to significantly enhance explanation quality.
        For details, check out our [paper](https://arxiv.org/abs/2507.22832) and its corresponding code [repository](https://github.com/314-Foundation/SemanticPullbacks).
        """
    )
    with gr.Row():
        with gr.Column():
            gr.Markdown(
                """
                Choose an input image - either sample from [Imagenette](https://github.com/fastai/imagenette) dataset, select a predefined example or upload your own. Square images are resized to 224x224 pixels, others are first resized to 256x256 and then center-cropped to 224x224 pixels.
                """
            )
            input_image = gr.Image(
                type="numpy", label="Input Image", value=PREDEFINED_IMAGES[4]
            )
            sample_from_val = gr.Button("Sample from Imagenette val")
            examples = gr.Examples(
                examples=predefined_files,
                # examples=PREDEFINED_IMAGES,
                inputs=[input_image],
                label=f"Example images from Imagenette val (corresponding to Example classes)",
            )

        with gr.Column():
            gr.Markdown(
                """
                Select a target class and amplify it's features -  compute Projected Gradient Ascent along the Excitaton Pullback. Very low temperature approximates (noisy) vanilla gradients, while very high temperature linearizes the model.
                """
            )
            with gr.Row():
                # with gr.Column():
                target_class = gr.Dropdown(
                    label="Target Class (ImageNet)",
                    info="idx - class name",
                    choices=[
                        (f"{idx} - {name}", int(idx))
                        for idx, name in IMAGENET_LABELS.items()
                    ],
                    value=INIT_CLASS,
                    allow_custom_value=False,
                    # render=False,
                )
                # with gr.Column():
                model_name = gr.Dropdown(
                    list(MODEL_MAP.keys()),
                    value=INIT_MODEL_NAME,
                    label="Model",
                    info="ImageNet-pretrained ReLU model",
                )
            with gr.Row():
                examples = gr.Examples(
                    # examples=[
                    #     [os.path.join(EXAMPLES_DIR, fname)] for fname in predefined_files
                    # ],
                    # examples=predefined_files,
                    examples=list(PREDEFINED_CLASSES.keys()),
                    example_labels=[
                        f"{cls} - {PREDEFINED_CLASSES[cls]}"
                        for cls in PREDEFINED_CLASSES.keys()
                    ],
                    inputs=[target_class],
                    label=f'Example classes (corresponding to Example images + "ostrich")',
                    examples_per_page=11,
                )
            with gr.Row():
                steps = gr.Number(
                    value=10,
                    label="Steps",
                    info="N steps for Projected Gradient Ascent",
                    maximum=1000,
                    minimum=1,
                    precision=0,
                )
                alpha = gr.Number(
                    value=20,
                    label="Alpha",
                    info="Step size (in L2 norm)",
                    minimum=1.0,
                    step=1.0,
                )
                eps = gr.Number(
                    value=100,
                    label="Eps",
                    info="Maximum perturbation (in L2 norm)",
                    minimum=10,
                    step=10,
                )
                temp = gr.Number(
                    value=INIT_MODEL_TEMP,
                    label="Temp",
                    info="Temperature for soft gating (sigmoid)",
                    minimum=0.01,
                    step=0.01,
                )
            run_button = gr.ClearButton(components=None, value="Amplify features!")
            with gr.Row():
                predicted_class_name_output = gr.Textbox(
                    label="Input image predicted top5 labels",
                    interactive=False,
                )
    with gr.Row():
        # with gr.Column():
        diff_img = gr.ImageSlider(
            # diff_img = gr.Image(
            label="Perturbed / Difference",
            # max_height=800,
            max_height=500,
            # show_fullscreen_button=False,
            interactive=False,
            slider_position=50,
            # show_fullscreen_button=False,
        )
        perturbed_img = gr.ImageSlider(
            # perturbed_img = gr.Image(
            label="Perturbed / Input",
            # max_height=800,
            max_height=500,
            # show_fullscreen_button=False,
            interactive=False,
            slider_position=50,
        )

    run_button.add(perturbed_img)
    run_button.add(diff_img)
    sample_from_val.click(fn=sample_val_img, outputs=input_image)

    run_button.click(
        fn=run_pullback,
        inputs=[
            input_image,
            model_name,
            target_class,
            steps,
            alpha,
            eps,
            temp,
        ],
        outputs=[diff_img, perturbed_img, predicted_class_name_output],
    )

if __name__ == "__main__":
    get_model(INIT_MODEL_NAME, INIT_MODEL_TEMP)  # preload default model
    demo.launch()