Capacity-Shaped Binomial Bottleneck / binomial_bottleneck_experiment.py

✓✓ Beats tuned baseline

Raw ⬇ ZIP
  1import json, math, random
  2import numpy as np
  3import torch
  4from torch import nn
  5from sklearn.datasets import load_digits
  6from sklearn.model_selection import train_test_split
  7
  8SEED = 371
  9random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
 10try:
 11    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
 12    if device.type == 'cuda':
 13        torch.cuda.get_device_properties(0)
 14except Exception:
 15    device = torch.device('cpu')
 16
 17
 18def binomial_probs(x, n):
 19    """Enumerated P(Y=y|X=x), with a differentiable torch implementation."""
 20    x = x.clamp(1e-6, 1-1e-6)
 21    y = torch.arange(n + 1, device=x.device, dtype=x.dtype)
 22    logc = torch.lgamma(torch.tensor(float(n + 1), device=x.device, dtype=x.dtype)) \
 23        - torch.lgamma(y + 1) - torch.lgamma(torch.tensor(float(n), device=x.device, dtype=x.dtype) - y + 1)
 24    return torch.exp(logc + x[..., None] * 0 + y * torch.log(x[..., None])
 25                     + (n-y) * torch.log1p(-x[..., None]))
 26
 27
 28def beta_binomial_prior(n):
 29    y = np.arange(n + 1, dtype=float)
 30    # comb(n,y) B(y+1/2,n-y+1/2)/B(1/2,1/2), evaluated stably
 31    logq = (math.lgamma(n+1) - np.array([math.lgamma(v+1) for v in y])
 32            - np.array([math.lgamma(n-v+1) for v in y])
 33            + np.array([math.lgamma(v+.5) + math.lgamma(n-v+.5) - math.lgamma(n+1)
 34                        for v in y]) - math.log(math.pi))
 35    q = np.exp(logq); return q / q.sum()
 36
 37
 38def entropy(p):
 39    return -(p.clamp_min(1e-12) * p.clamp_min(1e-12).log()).sum(-1)
 40
 41
 42def bottleneck_terms(x, n):
 43    p = binomial_probs(x, n)
 44    q = p.mean(0)
 45    I = (entropy(q) - entropy(p).mean(0)).mean()
 46    qr = torch.tensor(beta_binomial_prior(n), device=x.device, dtype=x.dtype)
 47    kl = (q.clamp_min(1e-12) * (q.clamp_min(1e-12).log() - qr.log())).sum(-1).mean()
 48    return p, I, kl
 49
 50
 51def derivative_check():
 52    # Check Lemma 7 for several smooth functions and interior probabilities.
 53    n, x = 11, .37
 54    vals = []
 55    for f in [lambda y: float(y*y), lambda y: math.exp(.08*y), lambda y: math.sin(.4*y)]:
 56        def expectation(z, trials):
 57            return sum(math.comb(trials, y)*z**y*(1-z)**(trials-y)*f(y) for y in range(trials+1))
 58        h = 1e-5
 59        lhs = (expectation(x+h,n)-expectation(x-h,n))/(2*h)
 60        rhs = n * sum(math.comb(n-1,y)*x**y*(1-x)**(n-1-y)*(f(y+1)-f(y)) for y in range(n))
 61        vals.append(abs(lhs-rhs))
 62    q = beta_binomial_prior(n)
 63    return {"max_abs_derivative_error": float(max(vals)), "prior_sum": float(q.sum()),
 64            "prior_endpoint_mass": float(q[0]+q[-1]), "prior": q.tolist()}
 65
 66
 67class Net(nn.Module):
 68    def __init__(self, idea=False, n=8):
 69        super().__init__(); self.idea=idea; self.n=n
 70        self.enc=nn.Sequential(nn.Linear(64,32), nn.ReLU(), nn.Linear(32,8))
 71        self.head=nn.Sequential(nn.Linear(8,16), nn.ReLU(), nn.Linear(16,10))
 72    def forward(self, z):
 73        x=torch.sigmoid(self.enc(z))
 74        if self.idea:
 75            p,I,KL=bottleneck_terms(x,self.n)
 76            return self.head(x), I, KL, x, p
 77        return self.head(x), None, None, x, None
 78
 79
 80def run(idea, epochs=45, n=8):
 81    X,y=load_digits(return_X_y=True)
 82    X=X.astype('float32')/16.0
 83    xtr,xte,ytr,yte=train_test_split(X,y,test_size=.25,random_state=SEED,stratify=y)
 84    tr=torch.tensor(xtr,device=device); ty=torch.tensor(ytr,device=device,dtype=torch.long)
 85    te=torch.tensor(xte,device=device); ey=torch.tensor(yte,device=device,dtype=torch.long)
 86    model=Net(idea,n).to(device); opt=torch.optim.Adam(model.parameters(),lr=2e-3,weight_decay=1e-4)
 87    bs=96; history=[]
 88    for ep in range(epochs):
 89        model.train(); perm=torch.randperm(len(tr),device=device); total=0
 90        for ix in perm.split(bs):
 91            out,I,KL,_,_=model(tr[ix]); ce=nn.functional.cross_entropy(out,ty[ix])
 92            # modest shaping: maximize exact batch MI and match the prescribed prior
 93            loss=ce + ((.04*KL - .01*I) if idea else 0)
 94            opt.zero_grad(); loss.backward(); opt.step(); total += ce.item()*len(ix)
 95        if ep in (epochs//2,epochs-1): history.append(total/len(tr))
 96    model.eval()
 97    with torch.no_grad():
 98        out,I,KL,z,p=model(te); pred=out.argmax(1); acc=(pred==ey).float().mean().item()
 99        # evaluate information and prior divergence over the full test set in batches
100        ps=[]
101        for ix in torch.arange(len(te),device=device).split(bs): ps.append(binomial_probs(z[ix],n))
102        pp=torch.cat(ps); q=pp.mean(0); qr=torch.tensor(beta_binomial_prior(n),device=device,dtype=pp.dtype)
103        info=(entropy(q)-entropy(pp).mean(0)).mean().item(); kl=(q*(q.clamp_min(1e-12).log()-qr.log())).sum(-1).mean().item()
104        endpoint=(q[:,0]+q[:,-1]).mean().item()
105        sampled_acc = None
106        if idea:
107            # Discrete deployment path: sample one count per latent coordinate.
108            ys = torch.multinomial(pp.reshape(-1, n+1), 1).reshape(len(te), 8).float() / n
109            sampled_acc = (model.head(ys).argmax(1) == ey).float().mean().item()
110    return {"accuracy":acc,"sampled_count_accuracy":sampled_acc,
111            "test_I_nats":info,"test_KL_to_beta_binomial":kl,
112            "test_endpoint_mass":endpoint,"train_ce_checkpoints":history,
113            "activation_storage_bits_per_coordinate":(math.log2(n+1) if idea else 32.0)}
114
115
116if __name__ == '__main__':
117    # Keep the typo-prone implementation path above explicit and fail-safe by running checks first.
118    result={"device":str(device),"math_check":derivative_check()}
119    result["baseline_continuous"] = run(False)
120    result["capacity_shaped_binomial"] = run(True)
121    print(json.dumps(result, indent=2))