Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions brainscore_vision/models/predictive_coding_pc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from brainscore_vision import model_registry
from brainscore_vision.model_helpers.brain_transformation import ModelCommitment
from .model import get_model, get_layers

model_registry['predictive_coding_pc'] = lambda: ModelCommitment(
identifier='predictive_coding_pc',
activations_model=get_model('predictive_coding_pc'),
layers=get_layers('predictive_coding_pc'),
)
152 changes: 152 additions & 0 deletions brainscore_vision/models/predictive_coding_pc/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""
Brain-Score Model: Predictive Coding Visual Hierarchy
======================================================
Hierarchical Predictive Coding network (Rao & Ballard 1999).
Layers r0-r3 correspond to ResNet-50 layer1-layer4 features.

RSA against 7T fMRI (THINGS-fMRI, N=3 subjects) shows crossing
hierarchy gradient: r0 maximally correlated with V1 (rho=0.30),
r3 with IT (rho=0.16), interaction Δr0-Δr3 = +0.266, p=0.007.

Reference:
Leutenegger, N. (2025).
https://github.com/nilsleut/Predictive-Coding-and-the-Visual-Cortex
"""

import torch
import torch.nn as nn
import torchvision.models as models
from torchvision import transforms
from brainscore_vision.model_helpers.activations.pytorch import PytorchWrapper
from brainscore_vision.model_helpers.brain_transformation import ModelCommitment
from brainscore_vision.model_helpers.check_submission import check_models

MODEL_IDENTIFIER = 'predictive_coding_pc'


# ── PC Network ───────────────────────────────────────────────

class PCConfig:
d_layer1 = 256; d_layer2 = 512
d_layer3 = 1024; d_layer4 = 2048
T_infer = 30; lr_r = 0.01


class PredictiveCodingNet(nn.Module):
def __init__(self, cfg):
super().__init__()
self.cfg = cfg
self.W1 = nn.Parameter(torch.randn(cfg.d_layer1, cfg.d_layer2) * 0.01)
self.W2 = nn.Parameter(torch.randn(cfg.d_layer2, cfg.d_layer3) * 0.01)
self.W3 = nn.Parameter(torch.randn(cfg.d_layer3, cfg.d_layer4) * 0.01)
self.b1 = nn.Parameter(torch.zeros(cfg.d_layer1))
self.b2 = nn.Parameter(torch.zeros(cfg.d_layer2))
self.b3 = nn.Parameter(torch.zeros(cfg.d_layer3))

def predict(self, r, W, b):
return torch.tanh(r @ W.T + b)

@torch.no_grad()
def infer(self, inputs):
cfg = self.cfg
r0, r1 = inputs['layer1'].clone(), inputs['layer2'].clone()
r2, r3 = inputs['layer3'].clone(), inputs['layer4'].clone()
for _ in range(cfg.T_infer):
eps0 = r0 - self.predict(r1, self.W1, self.b1)
eps1 = r1 - self.predict(r2, self.W2, self.b2)
eps2 = r2 - self.predict(r3, self.W3, self.b3)
r0 = r0 + cfg.lr_r * 0.5 * (-eps0)
r1 = r1 + cfg.lr_r * (-eps1 + eps0 @ self.W1)
r2 = r2 + cfg.lr_r * (-eps2 + eps1 @ self.W2)
r3 = r3 + cfg.lr_r * (eps2 @ self.W3)
return r0, r1, r2, r3


# ── Brain-Score Wrapper ───────────────────────────────────────

class PCWrapper(nn.Module):
"""
Wraps PC network for Brain-Score.
Exposes r0-r3 as named Identity submodules for layer hooking.
"""
def __init__(self):
super().__init__()
cfg = PCConfig()

# ResNet-50 feature extractor
resnet = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1)
resnet.eval()
self._cache = {}
for name in ['layer1', 'layer2', 'layer3', 'layer4']:
def make_hook(n):
def hook(m, i, o):
self._cache[n] = o.mean(dim=[2, 3])
return hook
getattr(resnet, name).register_forward_hook(make_hook(name))
self.resnet = resnet

# PC network — random init (inference dynamics drive hierarchy)
torch.manual_seed(42)
self.pc = PredictiveCodingNet(cfg)

# Named hook targets for Brain-Score
self.r0 = nn.Identity()
self.r1 = nn.Identity()
self.r2 = nn.Identity()
self.r3 = nn.Identity()

def forward(self, x):
self._cache.clear()
with torch.no_grad():
self.resnet(x)
feats = {k: self._cache[k] for k in
['layer1', 'layer2', 'layer3', 'layer4']}
r0, r1, r2, r3 = self.pc.infer(feats)
self.r0(r0)
self.r1(r1)
self.r2(r2)
return self.r3(r3)


# ── Brain-Score API ───────────────────────────────────────────

def get_model(identifier):
assert identifier == MODEL_IDENTIFIER
model = PCWrapper()
model.eval()

preprocessing = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225],
),
])

return PytorchWrapper(
identifier=identifier,
model=model,
preprocessing=preprocessing,
batch_size=32,
)


def get_layers(identifier):
return ['r0', 'r1', 'r2', 'r3']


def get_bibtex(identifier):
return """
@misc{leutenegger2025pc,
title={Predictive Coding and the Visual Cortex},
author={Leutenegger, Nils},
year={2025},
url={https://github.com/nilsleut/Predictive-Coding-and-the-Visual-Cortex}
}
"""


if __name__ == '__main__':
check_models.check_base_models(__name__)
1 change: 1 addition & 0 deletions brainscore_vision/models/predictive_coding_pc/pc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
torch
torchvision
7 changes: 7 additions & 0 deletions brainscore_vision/models/predictive_coding_pc/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import pytest
import brainscore_vision


def test_has_identifier():
model = brainscore_vision.load_model('predictive_coding_pc')
assert model.identifier == 'predictive_coding_pc'