Actionable-Information Optimizer / actionable_optimizer_toy.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import json
  2import random
  3from pathlib import Path
  4
  5import numpy as np
  6from sklearn.metrics import mutual_info_score
  7
  8
  9def set_seed(seed: int) -> None:
 10    random.seed(seed)
 11    np.random.seed(seed)
 12
 13
 14def entropy(x: np.ndarray) -> float:
 15    _, counts = np.unique(x, return_counts=True)
 16    p = counts.astype(float) / len(x)
 17    return float(-(p * np.log(p + 1e-15)).sum())
 18
 19
 20def mi(x: np.ndarray, y: np.ndarray) -> float:
 21    return float(mutual_info_score(x, y))
 22
 23
 24def quantize(x: np.ndarray, delta: float) -> np.ndarray:
 25    return np.floor(x / delta + 0.5).astype(np.int64)
 26
 27
 28def sample(seed: int, n: int = 120_000, noise: float = 0.35):
 29    set_seed(seed)
 30    theta = np.random.normal(size=n)
 31    g = theta + np.random.normal(scale=noise, size=n)
 32    # A fixed-magnitude update is useful on the quadratic iff it moves toward 0.
 33    useful_action = (theta * g > 0).astype(np.int64)
 34    return theta, g, useful_action
 35
 36
 37def channel_sweep(seed=7):
 38    theta, g, utility = sample(seed)
 39    deltas = [2.0, 1.0, 0.5, 0.25, 0.125, 0.0625, 0.03125]
 40    out = []
 41    for delta in deltas:
 42        z = quantize(g, delta)
 43        acquired = entropy(z)
 44        useful = mi(z, utility)
 45        unavailable = acquired - useful
 46        # Controller: sign observation, with zero-bin rejection.
 47        eta = 0.5
 48        update = -eta * np.sign(z)
 49        theta_new = theta + update
 50        progress = float(np.mean(0.5 * theta**2 - 0.5 * theta_new**2))
 51        action_correct = float(np.mean((z != 0) & (np.sign(z) == np.sign(theta))))
 52        out.append({"delta": delta, "I_acq": acquired, "I_use": useful,
 53                    "I_un": unavailable, "progress": progress,
 54                    "action_correct": action_correct,
 55                    "zero_fraction": float(np.mean(z == 0))})
 56    return out
 57
 58
 59def learning_sweep(seed=7):
 60    # Compare raw high-resolution SGD with the actionable finite-resolution
 61    # controller on L(theta)=theta^2/2, averaged over fixed initial points.
 62    set_seed(seed)
 63    initials = np.random.normal(size=2000)
 64    rows = []
 65    for delta in [None, 2.0, 1.0, 0.5, 0.25, 0.125]:
 66        theta = initials.copy()
 67        for _ in range(25):
 68            g = theta + np.random.normal(scale=0.35, size=len(theta))
 69            if delta is None:
 70                z = g
 71            else:
 72                z = quantize(g, delta) * delta
 73            theta -= 0.35 * np.sign(z) * np.minimum(np.abs(z), 2.0)
 74        rows.append({"method": "raw" if delta is None else f"quantized_{delta}",
 75                     "final_loss": float(np.mean(0.5 * theta**2))})
 76    return rows
 77
 78
 79def predictions(rows):
 80    # Prediction 1: acquired information grows approximately linearly in
 81    # -log(delta), with slope equal to the effective one-dimensional channel.
 82    x = np.array([-np.log(r["delta"]) for r in rows[-5:]])
 83    y = np.array([r["I_acq"] for r in rows[-5:]])
 84    slope, intercept = np.polyfit(x, y, 1)
 85    r2 = float(np.corrcoef(x, y)[0, 1] ** 2)
 86    # Prediction 2: useful information saturates while acquired information
 87    # keeps rising; compare last two increments.
 88    du_acq = rows[-1]["I_acq"] - rows[-2]["I_acq"]
 89    du_use = rows[-1]["I_use"] - rows[-2]["I_use"]
 90    # Prediction 3: progress saturates at fine resolution.
 91    dp = rows[-1]["progress"] - rows[-2]["progress"]
 92    return {"acq_log_slope": float(slope), "acq_log_r2": r2,
 93            "fine_delta_I_acq": float(du_acq),
 94            "fine_delta_I_use": float(du_use),
 95            "fine_delta_progress": float(dp),
 96            "predicted_acq_slope": 1.0,
 97            "predicted_useful_trend": "plateau relative to I_acq",
 98            "predicted_progress_trend": "plateau relative to I_acq"}
 99
100
101def main():
102    channel = channel_sweep()
103    result = {"channel": channel, "learning": learning_sweep(),
104              "predictions": predictions(channel)}
105    Path("toy_results.json").write_text(json.dumps(result, indent=2))
106    print(json.dumps(result, indent=2))
107
108
109if __name__ == "__main__":
110    main()