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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ It can be used to:
## Quick Links

- Please see [our latest talk from the Sillicon Valley ACM meetup](https://www.youtube.com/watch?v=Tnafo6JVoJs)
- Docs: [analyze_weights Log-Normal Self-Averaging Diagnostic](./docs/analyze_weights_lognormal_self_averaging.html)
- Docs: [Correlation Trap Workflow (`analyze_traps` + `remove_traps`)](./docs_trap_features.md)

- Join the [Discord Server](https://discord.gg/uVVsEAcfyF)

Expand Down Expand Up @@ -121,6 +123,11 @@ trap_df = watcher.analyze_traps(layers=[3, 5], plot=True, savefig="trap_images")

See the new usage guide: [Correlation Trap Workflow (`analyze_traps` + `remove_traps`)](./docs_trap_features.md)

### `analyze_weights()` output docs

For the additive finite-sample log-normal self-averaging fields emitted by `analyze_weights()`, see:
[analyze_weights Log-Normal Self-Averaging Diagnostic](./docs/analyze_weights_lognormal_self_averaging.html)

## PEFT / LORA models (experimental)
To analyze an PEFT / LORA fine-tuned model, specify the peft option.

Expand Down
53 changes: 53 additions & 0 deletions docs/analyze_weights_lognormal_self_averaging.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WeightWatcher analyze_weights Log-Normal Self-Averaging Diagnostic</title>
</head>
<body>
<h1>analyze_weights: finite-sample log-normal self-averaging diagnostic</h1>

<p>
This diagnostic applies <strong>only</strong> when a side of the weight-element distribution is plausibly log-normal.
Left and right sides are tested separately.
</p>

<h2>Per-side definitions</h2>
<ul>
<li>Right side: <code>w[w &gt; 0]</code></li>
<li>Left side: <code>abs(w[w &lt; 0])</code></li>
</ul>

<p>For a side with <code>N</code> samples and fitted log-normal parameters:</p>
<ul>
<li><code>mu_hat = mean(log x)</code></li>
<li><code>sigma^2_hat = var(log x)</code></li>
<li>finite-sample control parameter: <code>exp(sigma^2_hat) / N</code></li>
<li>warning threshold: <code>sigma^2_hat &gt;= log(N)</code></li>
</ul>

<h2>Regimes</h2>
<ul>
<li><strong>self_averaging</strong></li>
<li><strong>marginal</strong></li>
<li><strong>non_self_averaging</strong></li>
</ul>

<p>
Non-log-normal sides are marked <code>undetermined</code>, with numeric diagnostic fields left as <code>NaN</code>.
</p>

<h2>Output columns</h2>
<p>
The diagnostic adds per-layer columns to <code>analyze_weights()</code> output for right and left sides,
including detected flags, fitted moments, sample counts, finite-sample ratios, margins, regime labels,
and non-self-averaging booleans.
</p>

<h2>Disclaimer</h2>
<p>
This is a practical finite-sample diagnostic for weight-element distributions, not a universal theorem for all layer statistics.
</p>
</body>
</html>
105 changes: 105 additions & 0 deletions tests/test_analyze_weights.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import numpy as np

from weightwatcher.analyze_weights import (
_fit_side_models,
compute_lognormal_self_averaging_stats,
)


def _best(rows):
best = [r for r in rows if r.get("is_best_fit", False)]
assert len(best) == 1
return best[0]["distribution"]


def test_fit_power_law_right_side():
rng = np.random.default_rng(7)
samples = (1.0 + rng.pareto(a=3.0, size=12000)).astype(float)
rows = _fit_side_models(samples, side_label="right", min_points=64)
assert _best(rows) == "power_law"


def test_fit_exponential_right_side():
rng = np.random.default_rng(8)
samples = rng.exponential(scale=2.0, size=12000).astype(float)
rows = _fit_side_models(samples, side_label="right", min_points=64)
assert _best(rows) == "exponential"


def test_fit_lognormal_right_side():
rng = np.random.default_rng(9)
samples = rng.lognormal(mean=0.0, sigma=0.5, size=12000).astype(float)
rows = _fit_side_models(samples, side_label="right", min_points=64)
assert _best(rows) == "lognormal"


def test_fit_laplace_left_side():
rng = np.random.default_rng(10)
samples = rng.laplace(loc=-5.0, scale=0.4, size=12000).astype(float)
rows = _fit_side_models(samples, side_label="left", min_points=64)
assert _best(rows) == "laplace"


def _fixed_lognormal_samples(mu, sigma2, n, seed):
rng = np.random.default_rng(seed)
z = rng.normal(size=n)
z = (z - z.mean()) / z.std(ddof=0)
y = mu + np.sqrt(sigma2) * z
return np.exp(y)


def test_lognormal_right_self_averaging_regime():
n = 200
sigma2 = 1.0
vals = _fixed_lognormal_samples(mu=0.2, sigma2=sigma2, n=n, seed=101)
out = compute_lognormal_self_averaging_stats(vals, "right", min_samples=10, tol=0.05, classified_as_lognormal=True)
assert out["lognormal_right_detected"] is True
assert out["lognormal_right_sa_regime"] == "self_averaging"
assert out["lognormal_right_non_self_averaging"] is False
assert np.isclose(out["lognormal_right_sigma2"], sigma2, atol=1e-10)
assert out["lognormal_right_n"] == n
assert np.isclose(out["lognormal_right_sa_ratio"], np.exp(sigma2) / n)


def test_lognormal_right_marginal_regime():
n = 40
sigma2 = np.log(n)
vals = _fixed_lognormal_samples(mu=-0.1, sigma2=sigma2, n=n, seed=102)
out = compute_lognormal_self_averaging_stats(vals, "right", min_samples=10, tol=0.05, classified_as_lognormal=True)
assert out["lognormal_right_detected"] is True
assert out["lognormal_right_sa_regime"] == "marginal"
assert out["lognormal_right_non_self_averaging"] is False
assert np.isclose(out["lognormal_right_nsa_margin"], 0.0, atol=1e-10)


def test_lognormal_right_non_self_averaging_regime():
n = 40
sigma2 = np.log(n) + 0.5
vals = _fixed_lognormal_samples(mu=0.0, sigma2=sigma2, n=n, seed=103)
out = compute_lognormal_self_averaging_stats(vals, "right", min_samples=10, tol=0.05, classified_as_lognormal=True)
assert out["lognormal_right_detected"] is True
assert out["lognormal_right_sa_regime"] == "non_self_averaging"
assert out["lognormal_right_non_self_averaging"] is True
assert out["lognormal_right_nsa_margin"] > 0.0


def test_lognormal_left_negative_case():
n = 120
sigma2 = 1.3
vals = -_fixed_lognormal_samples(mu=0.3, sigma2=sigma2, n=n, seed=104)
out = compute_lognormal_self_averaging_stats(vals, "left", min_samples=10, tol=0.05, classified_as_lognormal=True)
assert out["lognormal_left_detected"] is True
assert np.isclose(out["lognormal_left_sigma2"], sigma2, atol=1e-10)
assert out["lognormal_left_n"] == n


def test_non_lognormal_control_is_undetermined():
rng = np.random.default_rng(105)
vals = rng.uniform(low=0.01, high=1.0, size=300)
out = compute_lognormal_self_averaging_stats(vals, "right", min_samples=10, tol=0.05, classified_as_lognormal=False)
assert out["lognormal_right_detected"] is False
assert out["lognormal_right_sa_regime"] == "undetermined"
assert np.isnan(out["lognormal_right_non_self_averaging"])
assert np.isnan(out["lognormal_right_sigma2"])
assert np.isnan(out["lognormal_right_n"])
assert np.isnan(out["lognormal_right_sa_ratio"])
Loading