-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathNotebook_03.py
More file actions
360 lines (308 loc) · 13 KB
/
Copy pathNotebook_03.py
File metadata and controls
360 lines (308 loc) · 13 KB
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
# %% [raw]
# ---
# title: "GNN Testing: Robustness to Edge Removal"
# author: "Allier, Lappalainen, Saalfeld"
# categories:
# - FlyVis
# - GNN
# - Ablation
# execute:
# echo: false
# image: "log/fly/flyvis_noise_free/results/rollout_noise_free_DAVIS_selected_on_noise_free_mask_50.png"
# description: "Test whether the GNN has learned the circuit's computational rules by randomly ablating 50% of synaptic connections and comparing the model's predictions to the simulator under the same reduced connectivity."
# ---
# %% [markdown]
# ## Ablation
#
#
# A well-trained dynamical model should capture the circuit's
# computational rules, not merely memorize its specific activities.To test this, we randomly ablated 50% of the synaptic connections and
# regenerated the ground truth under the ablated connectivity. The same edge mask is then applied to the GNN's learned weights
# $\widehat{W}_{ij}$, so both the simulator and the model operate on identical reduced circuits. If the GNN has learned the correct
# message-passing functions $f_\theta$ and $g_\phi$, it should generalize to the reduced connectivity without retraining.
# We tested robustness at three noise levels: $\sigma = 0$ noise-free), $\sigma = 0.05$ (low noise), and $\sigma = 0.5$
# (high noise). The test is performed using the best models in ([Notebook 01](Notebook_01.html)).
# %%
#| output: false
import glob
import os
import warnings
from IPython.display import Image, Markdown, display
from flyvis_gnn.config import NeuralGraphConfig
from flyvis_gnn.generators.graph_data_generator import data_generate
from flyvis_gnn.models.graph_trainer import data_test
from flyvis_gnn.utils import set_device, add_pre_folder, graphs_data_path, log_path
warnings.filterwarnings("ignore", message="pkg_resources is deprecated as an API")
warnings.filterwarnings("ignore", category=FutureWarning)
def display_image(path, width=700):
"""Display a full-resolution image; width controls inline size (px)."""
display(Image(filename=path, width=width))
def parse_results_log(path):
"""Parse a results log file into a dict of metric_name -> value string."""
metrics = {}
if not os.path.isfile(path):
return metrics
with open(path) as f:
for line in f:
for key in ['RMSE', 'Pearson r']:
if line.startswith(f'{key}:'):
metrics[key] = line.split(':', 1)[1].strip()
return metrics
# %%
#| output: false
# Base configs (trained models)
base_datasets = [
('flyvis_noise_free', 'Noise-free'),
('flyvis_noise_005', 'Noise 0.05'),
('flyvis_noise_05', 'Noise 0.5'),
]
# Ablated configs (50% edge mask)
mask_datasets = [
('flyvis_noise_free_mask_50', 'Noise-free (50% ablation)'),
('flyvis_noise_005_mask_50', 'Noise 0.05 (50% ablation)'),
('flyvis_noise_05_mask_50', 'Noise 0.5 (50% ablation)'),
]
config_root = "./config"
base_configs = {}
mask_configs = {}
for config_name, label in base_datasets:
config_file, pre_folder = add_pre_folder(config_name)
config = NeuralGraphConfig.from_yaml(f"{config_root}/{config_file}.yaml")
config.dataset = pre_folder + config.dataset
config.config_file = pre_folder + config_name
base_configs[config_name] = config
for config_name, label in mask_datasets:
config_file, pre_folder = add_pre_folder(config_name)
config = NeuralGraphConfig.from_yaml(f"{config_root}/{config_file}.yaml")
config.dataset = pre_folder + config.dataset
config.config_file = pre_folder + config_name
mask_configs[config_name] = config
device = set_device(base_configs[base_datasets[0][0]].training.device)
# %% [markdown]
# ## Generate Ablated Data
#
# For each noise condition, the simulator regenerates voltage
# traces with 50% of edges randomly zeroed.
# The resulting ablation mask (a boolean tensor
# indicating which edges survive) is saved alongside the data so
# that the exact same mask can be applied to the GNN's learned
# weights at test time. This ensures a fair comparison: both the
# ground-truth simulator and the learned model operate on precisely
# the same reduced circuit.
# %%
#| echo: true
#| output: false
print()
print("=" * 80)
print("GENERATE - data with 50% edge ablation")
print("=" * 80)
for config_name, label in mask_datasets:
config = mask_configs[config_name]
graphs_dir = graphs_data_path(config.dataset)
mask_path = os.path.join(graphs_dir, "ablation_mask.pt")
has_test_data = (os.path.isfile(os.path.join(graphs_dir, "x_list_test.pt"))
or os.path.isfile(os.path.join(graphs_dir, "x_list_test.npy"))
or os.path.isdir(os.path.join(graphs_dir, "x_list_test")))
if os.path.exists(mask_path) and has_test_data:
print(f"\n--- {label} ---")
print(f" ablated data already exists at {graphs_dir}/")
print(" skipping generation...")
else:
print(f"\n--- {label} ---")
print(f" generating with ablation_ratio={config.simulation.ablation_ratio}")
data_generate(config, device=device, visualize=False, style='color')
# %% [markdown]
# ## Test: GNN on Ablated Data
#
# Each model, trained on the full non-ablated connectivity, is now
# evaluated on the ablated test data. Before evaluation, the saved
# ablation mask is loaded and applied to the model's learned weight
# vector $\widehat{\mathbf{W}}$, zeroing out the same edges that were
# removed in the simulator. No retraining or fine-tuning is
# performed; the model must rely on the message-passing functions it
# learned from the original circuit.
# %%
#| echo: true
#| output: false
print()
print("=" * 80)
print("TEST - GNN models on ablated data")
print("=" * 80)
# Check that trained models exist for all base configs
pairs = list(zip(base_datasets, mask_datasets))
missing_models = []
for base_name, base_label in base_datasets:
log_dir = log_path(base_configs[base_name].config_file)
model_files = glob.glob(f"{log_dir}/models/best_model_with_*.pt")
if not model_files:
missing_models.append(base_label)
if missing_models:
msg = ", ".join(missing_models)
raise RuntimeError(
f"No trained models found for: {msg}. "
f"Please run Notebook_01 first to train the GNN models."
)
for (base_name, base_label), (mask_name, mask_label) in pairs:
print(f"\n--- {base_label} model on ablated data ---")
data_test(
base_configs[base_name],
best_model='best',
device=device,
test_config=mask_configs[mask_name],
)
# %% [markdown]
# ## Ablation Rollout Traces
#
# The rollout plots below compare ground-truth voltages (green) and
# GNN predictions (black) under 50% edge ablation. The red trace
# corresponds to one of the R1–R6 outer photoreceptors, which receive
# the visual stimulus directly from the compound eye and also
# integrate excitatory feedback from lamina interneurons (L2, L4, and
# amacrine cells). The **all-types** plot shows one representative neuron per cell type
# (65 traces), providing a global view of how the ablated GNN
# captures the circuit dynamics. The **selected** plot zooms into a
# subset for more detailed inspection.
# %%
#| lightbox: true
def show_ablation_rollout(base_name, mask_name):
log_dir = log_path(base_configs[base_name].config_file)
results_dir = os.path.join(log_dir, "results")
if not os.path.isdir(results_dir):
return
mask_suffix = mask_name.replace('flyvis_', '')
rollout_all = [f for f in os.listdir(results_dir)
if f.startswith("rollout_") and "_all" in f
and mask_suffix in f and f.endswith(".png")]
rollout_sel = [f for f in os.listdir(results_dir)
if f.startswith("rollout_") and "selected" in f
and mask_suffix in f and f.endswith(".png")]
if rollout_all:
display_image(os.path.join(results_dir, sorted(rollout_all)[0]), width=900)
if rollout_sel:
display_image(os.path.join(results_dir, sorted(rollout_sel)[0]), width=900)
# %% [markdown]
# ### Noise-free ($\sigma = 0$)
# %%
#| lightbox: true
show_ablation_rollout('flyvis_noise_free', 'flyvis_noise_free_mask_50')
# %% [markdown]
# ### Low noise ($\sigma = 0.05$)
# %%
#| lightbox: true
show_ablation_rollout('flyvis_noise_005', 'flyvis_noise_005_mask_50')
# %% [markdown]
# ### High noise ($\sigma = 0.5$)
# %%
#| lightbox: true
show_ablation_rollout('flyvis_noise_05', 'flyvis_noise_05_mask_50')
# %% [markdown]
# ## Ablation Metrics
#
# The tables below report RMSE and Pearson $r$ (mean $\pm$ std over
# neurons) for the ablated evaluation, for both one-step prediction
# and autoregressive rollout.
# %%
header = "| Metric | " + " | ".join(bl for _, bl in base_datasets) + " |"
sep = "|:--|" + "|".join(":--:" for _ in base_datasets) + "|"
# One-step prediction
rows_1s = [header, sep]
for key in ['RMSE', 'Pearson r']:
cells = []
for (base_name, _), (mask_name, _) in pairs:
log_dir = log_path(base_configs[base_name].config_file)
mask_suffix = mask_name.replace('flyvis_', '')
m = parse_results_log(os.path.join(log_dir, f"results_test_on_{mask_suffix}.log"))
cells.append(m.get(key, '\u2014'))
rows_1s.append(f"| {key} | " + " | ".join(cells) + " |")
display(Markdown("### One-Step Prediction (ablated data)\n\n" + "\n".join(rows_1s)))
# Rollout
rows_ro = [header, sep]
for key in ['RMSE', 'Pearson r']:
cells = []
for (base_name, _), (mask_name, _) in pairs:
log_dir = log_path(base_configs[base_name].config_file)
mask_suffix = mask_name.replace('flyvis_', '')
m = parse_results_log(os.path.join(log_dir, f"results_rollout_on_{mask_suffix}.log"))
cells.append(m.get(key, '\u2014'))
rows_ro.append(f"| {key} | " + " | ".join(cells) + " |")
display(Markdown("### Autoregressive Rollout (ablated data)\n\n" + "\n".join(rows_ro)))
# %% [markdown]
# ## Noise-Free Ablation Evaluation
#
# As in the non-ablated case (Notebook 02), we cross-test the noisy
# models on clean data to verify that the denoising property is
# preserved under ablation. The models trained on noisy data
# ($\sigma{=}0.05$ and $\sigma{=}0.5$) are evaluated on the
# **noise-free ablated** test data. If the GNN has learned the
# deterministic dynamics, it should
# still track the clean ground truth even after losing half its
# synaptic connections.
# %%
#| echo: true
#| output: false
noise_free_mask_config = mask_configs['flyvis_noise_free_mask_50']
noisy_base = [ds for ds in base_datasets if ds[0] != 'flyvis_noise_free']
for base_name, base_label in noisy_base:
print()
print(f"--- {base_label} model on noise-free ablated data ---")
data_test(
base_configs[base_name],
best_model='best',
device=device,
test_config=noise_free_mask_config,
)
# %% [markdown]
# ### Rollout: Noisy Models on Noise-Free Ablated Data
# %%
#| lightbox: true
def show_nf_ablation_rollout(base_name):
log_dir = log_path(base_configs[base_name].config_file)
results_dir = os.path.join(log_dir, "results")
if not os.path.isdir(results_dir):
return
rollout_all = [f for f in os.listdir(results_dir)
if f.startswith("rollout_") and "_all" in f
and "on_noise_free_mask_50" in f and f.endswith(".png")]
rollout_sel = [f for f in os.listdir(results_dir)
if f.startswith("rollout_") and "selected" in f
and "on_noise_free_mask_50" in f and f.endswith(".png")]
if rollout_all:
display_image(os.path.join(results_dir, sorted(rollout_all)[0]), width=900)
if rollout_sel:
display_image(os.path.join(results_dir, sorted(rollout_sel)[0]), width=900)
# %% [markdown]
# ### Low noise ($\sigma = 0.05$) on noise-free ablated data
# %%
#| lightbox: true
show_nf_ablation_rollout('flyvis_noise_005')
# %% [markdown]
# ### High noise ($\sigma = 0.5$) on noise-free ablated data
# %%
#| lightbox: true
show_nf_ablation_rollout('flyvis_noise_05')
# %% [markdown]
# ### Noise-Free Ablation Metrics
# %%
nf_header = "| Metric | " + " | ".join(bl for _, bl in noisy_base) + " |"
nf_sep = "|:--|" + "|".join(":--:" for _ in noisy_base) + "|"
nf_rows = [nf_header, nf_sep]
for key in ['RMSE', 'Pearson r']:
cells = []
for base_name, _ in noisy_base:
log_dir = log_path(base_configs[base_name].config_file)
m = parse_results_log(os.path.join(log_dir, "results_rollout_on_noise_free_mask_50.log"))
cells.append(m.get(key, '\u2014'))
nf_rows.append(f"| {key} | " + " | ".join(cells) + " |")
display(Markdown("\n".join(nf_rows)))
# %% [markdown]
# ### Denoising Under Ablation
#
# The results confirm that the implicit denoising property observed
# in Notebook 02 is preserved under ablation. Models trained on noisy
# data still recover the deterministic dynamics when evaluated on
# noise-free ablated data, tracking the clean ground truth closely.
# This is a strong indication that the GNN has learned the true
# message-passing computation. The functions $f_\theta$ and $g_\phi$
# generalize across both noise conditions and connectivity
# perturbations, rather than being overfitted to the specific training
# dataset.