Manuscript Psy01-A@1 · Psy01-A.tex · listing · 94477fa3
Listing: Listing
Independent evaluation streams and paired branch replay · lines 2400–2545
import math
import torch
from torch.utils.data import DataLoader
@torch.no_grad()
def forward_explicit_noise(model, x, cue, sigma, generator):
if sigma < 0:
raise ValueError("sigma must be nonnegative")
h = torch.cat([x, cue], dim=1)
for layer in (model.fc1, model.fc2, model.fc3):
h = torch.relu(layer(h))
if sigma > 0:
noise = torch.randn(
h.shape,
dtype=h.dtype,
device=h.device,
generator=generator,
)
h = h + sigma * noise
return model.head(h)
@torch.no_grad()
def paired_cue_probe(model, x, y, seed=10001, batch_size=1000):
if len(x) == 0:
raise ValueError("Empty test set")
was_training = model.training
model.eval()
generator = torch.Generator(device=x.device)
generator.manual_seed(int(seed))
permutation = torch.tensor(
[2, 3, 0, 1], device=y.device, dtype=torch.long
)
counts = {
"correct_visual": 0,
"wrong_visual": 0,
"correct_voice": 0,
"zero_visual": 0,
"midpoint_visual": 0,
"midpoint_voice": 0,
"random_visual": 0,
}
try:
for start in range(0, len(x), batch_size):
xb = x[start:start + batch_size]
yb = y[start:start + batch_size]
y_voice = permutation[yb]
n = len(xb)
cv = xb.new_tensor([1.0, 0.0]).expand(n, 2)
ca = xb.new_tensor([0.0, 1.0]).expand(n, 2)
cz = xb.new_zeros((n, 2))
cm = xb.new_full((n, 2), 0.5)
# Independent, normalized per-example random cue.
alpha = torch.rand(
(n, 1),
dtype=xb.dtype,
device=xb.device,
generator=generator,
)
cr = torch.cat([alpha, 1.0 - alpha], dim=1)
def predict(cue):
return forward_explicit_noise(
model, xb, cue, 0.0, generator
).argmax(dim=1)
pv = predict(cv)
pa = predict(ca)
pz = predict(cz)
pm = predict(cm)
pr = predict(cr)
counts["correct_visual"] += int((pv == yb).sum())
counts["wrong_visual"] += int((pa == yb).sum())
counts["correct_voice"] += int(
(pa == y_voice).sum()
)
counts["zero_visual"] += int((pz == yb).sum())
counts["midpoint_visual"] += int((pm == yb).sum())
counts["midpoint_voice"] += int(
(pm == y_voice).sum()
)
counts["random_visual"] += int((pr == yb).sum())
finally:
model.train(was_training)
result = {key: value / len(x) for key, value in counts.items()}
result["cue_drop"] = (
result["correct_visual"] - result["wrong_visual"]
)
result["zero_cue_drop"] = (
result["correct_visual"] - result["zero_visual"]
)
result["balanced_midpoint"] = 0.5 * (
result["midpoint_visual"] + result["midpoint_voice"]
)
assert result["balanced_midpoint"] <= 0.5 + 1e-12
return result
@torch.no_grad()
def stress_mc(
model, x, y, sigma, seeds,
batch_size=1000, delta=0.05
):
seeds = list(seeds)
if len(x) == 0 or len(seeds) == 0:
raise ValueError("Nonempty inputs and seeds are required")
if not 0.0 < delta < 1.0:
raise ValueError("delta must lie in (0, 1)")
was_training = model.training
model.eval()
replicate_means = []
try:
for seed in seeds:
generator = torch.Generator(device=x.device)
generator.manual_seed(int(seed))
correct = 0
for start in range(0, len(x), batch_size):
xb = x[start:start + batch_size]
yb = y[start:start + batch_size]
cue = xb.new_tensor([1.0, 0.0]).expand(len(xb), 2)
pred = forward_explicit_noise(
model, xb, cue, sigma, generator
).argmax(dim=1)
correct += int((pred == yb).sum())
replicate_means.append(correct / len(x))
finally:
model.train(was_training)
n_noise_outcomes = len(x) * len(seeds)
radius = math.sqrt(
math.log(2.0 / delta) / (2.0 * n_noise_outcomes)
)
return {
"mean": sum(replicate_means) / len(replicate_means),
"replicate_means": replicate_means,
# Conditional on this fixed held-out input set.
"conditional_mc_hoeffding_radius": radius,
}