Skip to content

Commit b177ad8

Browse files
Add microsaccades: interface and default model helper (#492)
* integrate brain-score/model-tools#75 to brain-score/vision * add exception for when temp file write fails * fix error with tf temp file management * add bandaid to a DataAssembly index problem * move microsaccades to the model side * update comments * remove indexing bug * fix bug with activations.shape * Apply suggestions from code review Co-authored-by: Martin Schrimpf <mschrimpf@users.noreply.github.com> * Delete brainscore_vision/data/scialom2024/__init__.py * address review changes * remove needless import * fix bug with temporary file handling test * assume number_of_trials=1 and require_variance=False when getting stored activations * fix bug with access to ActivationsExtractorHelper.set_visual_degrees * move extractor calls to ModelCommitment generic * add check for whether activations_model exists * fix bug with TestVisualDegrees * add link to BrainModel issue * remove shifts from stimulus set packaging * change link signatures * change microsaccade call signature * microsaccades are now computed on both a pixel and degree basis * Apply suggestions from code review Co-authored-by: Martin Schrimpf <mschrimpf@users.noreply.github.com> * fix outdated comments, type hints, etc. * change function call to reduce repetition * add kwargs to microsaccade helpers * refactor microsaccade usage to their own class to improve readability * refactor microsaccade coords into MicrosaccadeHelper * refactor microsaccade building * change the way MultiIndex is set * fix tf/pytorch/keras bug with image shape calculation * cv2 reshaping in translate * add test for exact microsaccades * fix microsaccade indexing * rename test to be in line with current naming * add require_variance to _from_paths_stored * reduce unnecessarily long test times by reducing the number of trials tests run for, while keeping the test the same --------- Co-authored-by: Martin Schrimpf <mschrimpf@users.noreply.github.com>
1 parent 916c051 commit b177ad8

File tree

6 files changed

+515
-50
lines changed

6 files changed

+515
-50
lines changed

brainscore_vision/model_helpers/activations/core.py

Lines changed: 368 additions & 44 deletions
Large diffs are not rendered by default.

brainscore_vision/model_helpers/brain_transformation/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ def __init__(self, identifier,
2424
visual_degrees=8):
2525
self.layers = layers
2626
self.activations_model = activations_model
27+
# We set the visual degrees of the ActivationsExtractorHelper here to avoid changing its signature.
28+
# The ideal solution would be to not expose the _extractor of the activations_model here, but to change
29+
# the signature of the ActivationsExtractorHelper. See https://github.com/brain-score/vision/issues/554
30+
self.activations_model._extractor.set_visual_degrees(visual_degrees) # for microsaccades
2731
self._visual_degrees = visual_degrees
2832
# region-layer mapping
2933
if region_layer_map is None:

brainscore_vision/model_helpers/brain_transformation/neural.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,30 @@ def __init__(self, identifier, activations_model, region_layer_map, visual_degre
2323
def identifier(self):
2424
return self._identifier
2525

26-
def look_at(self, stimuli, number_of_trials=1):
26+
def look_at(self, stimuli, number_of_trials=1, require_variance: bool = False):
27+
"""
28+
:param number_of_trials: An integer that determines how many repetitions of the same image the model performs.
29+
:param require_variance: Whether to require models to return different activations for the same stimuli or not.
30+
For detailed information, see
31+
:meth:`~brainscore_vision.model_helpers.activations.ActivationsExtractorHelper.__call__`,
32+
"""
2733
layer_regions = {}
2834
for region in self.recorded_regions:
2935
layers = self.region_layer_map[region]
3036
layers = make_list(layers)
3137
for layer in layers:
3238
assert layer not in layer_regions, f"layer {layer} has already been assigned for {layer_regions[layer]}"
3339
layer_regions[layer] = region
34-
activations = self.run_activations(
35-
stimuli, layers=list(layer_regions.keys()), number_of_trials=number_of_trials)
40+
activations = self.run_activations(stimuli,
41+
layers=list(layer_regions.keys()),
42+
number_of_trials=number_of_trials,
43+
require_variance=require_variance)
3644
activations['region'] = 'neuroid', [layer_regions[layer] for layer in activations['layer'].values]
3745
return activations
3846

39-
def run_activations(self, stimuli, layers, number_of_trials=1):
40-
activations = self.activations_model(stimuli, layers=layers)
47+
def run_activations(self, stimuli, layers, number_of_trials=1, require_variance=None):
48+
activations = self.activations_model(stimuli, layers=layers, number_of_trials=number_of_trials,
49+
require_variance=require_variance)
4150
return activations
4251

4352
def start_task(self, task):

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies = [
1818
"importlib-metadata<5", # workaround to https://github.com/brain-score/brainio/issues/28
1919
"scikit-learn", # for metric_helpers/transformations.py cross-validation
2020
"scipy", # for benchmark_helpers/properties_common.py
21+
"opencv-python", # for microsaccades
2122
"h5py",
2223
"tqdm",
2324
"gitpython",

tests/test_model_helpers/activations/test___init__.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,20 @@ def tfslim_vgg16():
163163
pytest.param(tfslim_vgg16, ['vgg_16/pool5'], marks=pytest.mark.memory_intense),
164164
]
165165

166+
# exact microsaccades for pytorch_alexnet, grayscale.png, for 1 and 10 number_of_trials
167+
exact_microsaccades = {"x_degrees": {1: np.array([0.]),
168+
10: np.array([0., -0.00639121, -0.02114204, -0.02616418, -0.02128906,
169+
-0.00941355, 0.00596172, 0.02166913, 0.03523793, 0.04498976])},
170+
"y_degrees": {1: np.array([0.]),
171+
10: np.array([0., 0.0144621, 0.00728107, -0.00808922, -0.02338324, -0.0340791,
172+
-0.03826824, -0.03578336, -0.02753704, -0.01503068])},
173+
"x_pixels": {1: np.array([0.]),
174+
10: np.array([0., -0.17895397, -0.59197722, -0.73259714, -0.59609364, -0.26357934,
175+
0.16692818, 0.60673569, 0.98666196, 1.25971335])},
176+
"y_pixels": {1: np.array([0.]),
177+
10: np.array([0., 0.40493885, 0.20386999, -0.22649819, -0.65473077, -0.95421482,
178+
-1.07151061, -1.00193403, -0.77103707, -0.42085896])}}
179+
166180

167181
@pytest.mark.parametrize("image_name", ['rgb.jpg', 'grayscale.png', 'grayscale2.jpg', 'grayscale_alpha.png',
168182
'palletized.png'])
@@ -189,6 +203,68 @@ def test_from_image_path(model_ctr, layers, image_name, pca_components, logits):
189203
return activations
190204

191205

206+
@pytest.mark.parametrize("image_name", ['rgb.jpg', 'grayscale.png', 'grayscale2.jpg', 'grayscale_alpha.png',
207+
'palletized.png'])
208+
@pytest.mark.parametrize(["model_ctr", "layers"], models_layers)
209+
@pytest.mark.parametrize("number_of_trials", [1, 3, 10])
210+
def test_require_variance_has_shift_coords(model_ctr, layers, image_name, number_of_trials):
211+
stimulus_paths = [os.path.join(os.path.dirname(__file__), image_name)]
212+
activations_extractor = model_ctr()
213+
# when using microsaccades, the ModelCommitment sets its visual angle. Since this test skips the ModelCommitment,
214+
# we set it here manually.
215+
activations_extractor._extractor.set_visual_degrees(8.)
216+
217+
activations = activations_extractor(stimuli=stimulus_paths, layers=layers, number_of_trials=number_of_trials,
218+
require_variance=True)
219+
220+
assert activations is not None
221+
assert len(activations['microsaccade_shift_x_pixels']) == number_of_trials * len(stimulus_paths)
222+
assert len(activations['microsaccade_shift_y_pixels']) == number_of_trials * len(stimulus_paths)
223+
assert len(activations['microsaccade_shift_x_degrees']) == number_of_trials * len(stimulus_paths)
224+
assert len(activations['microsaccade_shift_y_degrees']) == number_of_trials * len(stimulus_paths)
225+
226+
227+
@pytest.mark.parametrize("image_name", ['rgb.jpg', 'grayscale.png', 'grayscale2.jpg', 'grayscale_alpha.png',
228+
'palletized.png'])
229+
@pytest.mark.parametrize(["model_ctr", "layers"], models_layers)
230+
@pytest.mark.parametrize("require_variance", [False, True])
231+
@pytest.mark.parametrize("number_of_trials", [1, 3, 10])
232+
def test_require_variance_presentation_length(model_ctr, layers, image_name, require_variance, number_of_trials):
233+
stimulus_paths = [os.path.join(os.path.dirname(__file__), image_name)]
234+
activations_extractor = model_ctr()
235+
# when using microsaccades, the ModelCommitment sets its visual angle. Since this test skips the ModelCommitment,
236+
# we set it here manually.
237+
activations_extractor._extractor.set_visual_degrees(8.)
238+
239+
activations = activations_extractor(stimuli=stimulus_paths, layers=layers,
240+
number_of_trials=number_of_trials, require_variance=require_variance)
241+
242+
assert activations is not None
243+
if require_variance:
244+
assert len(activations['presentation']) == number_of_trials
245+
else:
246+
assert len(activations['presentation']) == 1
247+
248+
249+
@pytest.mark.parametrize("image_name", ['rgb.jpg', 'grayscale.png', 'grayscale2.jpg', 'grayscale_alpha.png',
250+
'palletized.png'])
251+
@pytest.mark.parametrize(["model_ctr", "layers"], models_layers)
252+
def test_temporary_file_handling(model_ctr, layers, image_name):
253+
import tempfile
254+
stimulus_paths = [os.path.join(os.path.dirname(__file__), image_name)]
255+
activations_extractor = model_ctr()
256+
# when using microsaccades, the ModelCommitment sets its visual angle. Since this test skips the ModelCommitment,
257+
# we set it here manually.
258+
activations_extractor._extractor.set_visual_degrees(8.)
259+
260+
activations = activations_extractor(stimuli=stimulus_paths, layers=layers, number_of_trials=2,
261+
require_variance=True)
262+
temp_files = [f for f in os.listdir(tempfile.gettempdir()) if f.startswith('temp') and f.endswith('.png')]
263+
264+
assert activations is not None
265+
assert len(temp_files) == 0
266+
267+
192268
def _build_stimulus_set(image_names):
193269
stimulus_set = StimulusSet([{'stimulus_id': image_name, 'some_meta': image_name[::-1]}
194270
for image_name in image_names])
@@ -223,9 +299,51 @@ def test_exact_activations(pca_components):
223299
image_name='rgb.jpg', pca_components=pca_components, logits=False)
224300
path_to_expected = Path(__file__).parent / f'alexnet-rgb-{pca_components}.nc'
225301
expected = xr.load_dataarray(path_to_expected)
302+
303+
# Originally, the `stimulus_path` Index was used to index into xarrays in Brain-Score, but this was changed
304+
# as a part of PR #492 to a MultiIndex to allow metadata to be attached to multiple repetitions of the same
305+
# `stimulus_path`. Old .nc files need to be updated to use the `presentation` index instead of `stimulus_path`,
306+
# and instead of changing the extant activations, this test was simply modified to simulate that.
307+
expected = expected.rename({'stimulus_path': 'presentation'})
308+
226309
assert (activations == expected).all()
227310

228311

312+
@pytest.mark.memory_intense
313+
@pytest.mark.parametrize("number_of_trials", [1, 10])
314+
def test_exact_microsaccades(number_of_trials):
315+
image_name = 'grayscale.png'
316+
stimulus_paths = [os.path.join(os.path.dirname(__file__), image_name)]
317+
activations_extractor = pytorch_alexnet()
318+
# when using microsaccades, the ModelCommitment sets its visual angle. Since this test skips the ModelCommitment,
319+
# we set it here manually.
320+
activations_extractor._extractor.set_visual_degrees(8.)
321+
# the exact microsaccades were computed at this extent
322+
assert activations_extractor._extractor._microsaccade_helper.microsaccade_extent_degrees == 0.05
323+
324+
activations = activations_extractor(stimuli=stimulus_paths, layers=['features.12'],
325+
number_of_trials=number_of_trials, require_variance=True)
326+
327+
assert activations is not None
328+
# test with np.isclose instead of == since while the arrays are visually equal, == often fails due to float errors
329+
assert np.isclose(activations['microsaccade_shift_x_degrees'].values,
330+
exact_microsaccades['x_degrees'][number_of_trials],
331+
rtol=1e-05,
332+
atol=1e-08).all()
333+
assert np.isclose(activations['microsaccade_shift_y_degrees'].values,
334+
exact_microsaccades['y_degrees'][number_of_trials],
335+
rtol=1e-05,
336+
atol=1e-08).all()
337+
assert np.isclose(activations['microsaccade_shift_x_pixels'].values,
338+
exact_microsaccades['x_pixels'][number_of_trials],
339+
rtol=1e-05,
340+
atol=1e-08).all()
341+
assert np.isclose(activations['microsaccade_shift_y_pixels'].values,
342+
exact_microsaccades['y_pixels'][number_of_trials],
343+
rtol=1e-05,
344+
atol=1e-08).all()
345+
346+
229347
@pytest.mark.memory_intense
230348
@pytest.mark.parametrize(["model_ctr", "internal_layers"], [
231349
(pytorch_alexnet, ['features.12', 'classifier.5']),
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1+
from unittest.mock import Mock
2+
13
from brainscore_vision.model_helpers.brain_transformation import ModelCommitment
24
from brainscore_vision.model_helpers.utils import fullname
35

46

57
class TestVisualDegrees:
68
def test_standard_commitment(self):
7-
brain_model = ModelCommitment(identifier=fullname(self), activations_model=None,
9+
# create mock ActivationsExtractorHelper with a mock set_visual_degrees to avoid failing set_visual_degrees()
10+
mock_extractor = Mock()
11+
mock_extractor.set_visual_degrees = Mock()
12+
mock_activations_model = Mock()
13+
mock_activations_model._extractor = mock_extractor
14+
15+
# Initialize ModelCommitment with the mock activations_model
16+
brain_model = ModelCommitment(identifier=fullname(self), activations_model=mock_activations_model,
817
layers=['dummy'])
918
assert brain_model.visual_degrees() == 8

0 commit comments

Comments
 (0)