Skip to content

Commit 7feb39a

Browse files
committed
fix: much more spaghetti against the wall.
1 parent 3231e33 commit 7feb39a

9 files changed

Lines changed: 549 additions & 66 deletions

File tree

examples/advanced/DroppedNeuralNet/Data/_08_Reporting/Catalog.Reporting.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ public partial class Catalog
88
/// <summary>
99
/// Diagnostic measurements from the Validation flow.
1010
/// Each row is a (Category, Metric, Value, Notes) tuple covering pairing signal
11-
/// quality, fixed-ordering baseline error, and per-candidate forward-pass errors.
11+
/// quality, fixed-ordering baseline error, per-candidate forward-pass errors, and
12+
/// per-block residual blame attribution.
1213
/// Persisted as JSON so reports survive process exit and can be inspected offline.
1314
/// </summary>
1415
public IItem<IEnumerable<DiagnosticEntry>> Diagnostics =>
@@ -19,4 +20,18 @@ public partial class Catalog
1920
filePath: $"{_basePath}/_08_Reporting/Datasets/diagnostics.json"
2021
)
2122
);
23+
24+
/// <summary>
25+
/// Forward-pass evaluation of every candidate permutation produced by the Solver flow.
26+
/// Always populated — no candidate is gated by tolerance. Downstream SelectSolution
27+
/// picks the best entry to write to Solution.
28+
/// </summary>
29+
public IItem<IEnumerable<CandidateEvaluation>> CandidateEvaluations =>
30+
CreateItem(
31+
() =>
32+
ItemFactory.Enumerable.Json<CandidateEvaluation>(
33+
label: "CandidateEvaluations",
34+
filePath: $"{_basePath}/_08_Reporting/Datasets/candidate_evaluations.json"
35+
)
36+
);
2237
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Flowthru.Core.Abstractions;
2+
3+
namespace DroppedNeuralNet.Data._08_Reporting.Schemas;
4+
5+
/// <summary>
6+
/// Forward-pass evaluation result for a single candidate permutation produced by the
7+
/// Solver flow. All candidates are always emitted — no tolerance gating — so the full
8+
/// error distribution is preserved as a catalog entry for downstream analysis.
9+
/// </summary>
10+
[FlowthruSchema]
11+
public partial record CandidateEvaluation
12+
{
13+
public int CandidateIndex { get; init; }
14+
15+
/// <summary>Maximum absolute error across all historical samples.</summary>
16+
public float MaxErr { get; init; }
17+
18+
/// <summary>Mean absolute error across all historical samples.</summary>
19+
public float MeanErr { get; init; }
20+
21+
/// <summary>1 if MaxErr is below the tolerance threshold used during evaluation, 0 otherwise.</summary>
22+
public int PassesTolerance { get; init; }
23+
24+
/// <summary>JSON-encoded int[97] permutation (same encoding as CandidatePermutation.Permutation).</summary>
25+
public string Permutation { get; init; } = "[]";
26+
}

examples/advanced/DroppedNeuralNet/Flows/Exploration/ExplorationFlow.cs

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,19 @@ namespace DroppedNeuralNet.Flows.Exploration;
1818
/// Sieves PieceMetadata to enumerate every structurally valid (inp, out) Block candidate.
1919
/// Pure dimension arithmetic; no blobs.
2020
///
21-
/// Step 2 (Python) — compute_pairing_scores:
22-
/// For every legal (inp, out) candidate, computes ||W_out @ W_inp||_F.
23-
/// Lower scores indicate residual coupling — the signal left by training.
21+
/// Step 2 (Python) — compute_svd_activation_scores:
22+
/// Scores each legal (inp, out) candidate by SVD subspace alignment. The top-R left
23+
/// singular vectors of W_inp (principal hidden directions written to) are compared against
24+
/// the top-R right singular vectors of W_out (principal hidden directions read from).
25+
/// High overlap → trained pair; orthogonal subspaces → mismatch.
26+
/// No historical data required — purely geometric.
27+
///
28+
/// Supersedes compute_activation_scores (Pearson correlation on data pass, v2) and
29+
/// compute_pairing_scores (Frobenius of W_out @ W_inp, v1). Both are retained but
30+
/// commented out for reference.
2431
///
2532
/// Step 3 (Python) — run_hungarian:
26-
/// Applies the Hungarian algorithm to the 48×48 score matrix.
33+
/// Applies the Hungarian algorithm to the 48×48 score matrix (with Sinkhorn normalization).
2734
/// Produces the globally optimal assignment of inp ↔ out pieces in O(n³).
2835
///
2936
/// Step 4 (Python) — rank_orderings:
@@ -44,6 +51,7 @@ public static Flow Create(Catalog catalog, IPythonExecutor executor)
4451
output: catalog.LegalPairings
4552
);
4653

54+
/* SUPERSEDED — weight-space Frobenius signal collapses under normalization.
4755
pipeline.AddPythonStep<
4856
IEnumerable<PieceMetadata>,
4957
IEnumerable<PieceBlob>,
@@ -58,6 +66,46 @@ public static Flow Create(Catalog catalog, IPythonExecutor executor)
5866
output: catalog.PairingScores,
5967
executor: executor
6068
);
69+
*/
70+
71+
pipeline.AddPythonStep<
72+
IEnumerable<PieceMetadata>,
73+
IEnumerable<PieceBlob>,
74+
IEnumerable<BlockCandidate>,
75+
IEnumerable<PairingScore>
76+
>(
77+
label: "ComputeSvdActivationScores",
78+
description: "Score each (inp, out) candidate by SVD subspace alignment between inp write-directions and out read-directions. Pure weight geometry, no data pass required (Python).",
79+
module: "Flows.Exploration.Steps.compute_svd_activation_scores",
80+
function: "compute_svd_activation_scores",
81+
input: (catalog.PieceMetadata, catalog.Pieces, catalog.LegalPairings),
82+
output: catalog.PairingScores,
83+
executor: executor
84+
);
85+
86+
/* SUPERSEDED — Pearson correlation between mean activation and column attention norms.
87+
Required a full data pass; SVD subspace alignment captures the same signal geometrically.
88+
pipeline.AddPythonStep<
89+
IEnumerable<PieceMetadata>,
90+
IEnumerable<PieceBlob>,
91+
IEnumerable<BlockCandidate>,
92+
IEnumerable<MeasurementSchema>,
93+
IEnumerable<PairingScore>
94+
>(
95+
label: "ComputeActivationScores",
96+
description: "Run historical data through each inp piece; score each out piece by residual response magnitude. Lower = trained pair (Python).",
97+
module: "Flows.Exploration.Steps.compute_activation_scores",
98+
function: "compute_activation_scores",
99+
input: (
100+
catalog.PieceMetadata,
101+
catalog.Pieces,
102+
catalog.LegalPairings,
103+
catalog.HistoricalData
104+
),
105+
output: catalog.PairingScores,
106+
executor: executor
107+
);
108+
*/
61109

62110
pipeline.AddPythonStep<IEnumerable<PairingScore>, IEnumerable<BlockAssignment>>(
63111
label: "RunHungarian",
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
"""Score every legal (inp, out) Block pairing using data-driven activation response.
2+
3+
Attempts
4+
--------
5+
v1 — Mean residual magnitude (SUPERSEDED, see commented block below)
6+
score = mean_i ||W_out · relu(W_inp · x_i + b_inp) + b_out||_2
7+
Result: std=0.33, gap-to-2nd=0.02 — the bias term dominates ||R||_2 regardless
8+
of H, giving poor within-row discrimination (only 6/48 assignments at row min).
9+
10+
v2 — Pearson correlation between mean activation pattern and column attention (CURRENT)
11+
For a trained (inp, out) pair:
12+
- inp uses a specific subset of the 96 hidden neurons on real data
13+
→ measured by mean_act[inp][k] = E[relu(W_inp[k,:] · x + b_inp[k])]
14+
- out has been trained to attend to exactly those neurons
15+
→ measured by col_norm[out][k] = ||W_out[:, k]||_2
16+
17+
Pearson correlation(mean_act[inp], col_norm[out]) should be strongly positive
18+
for trained pairs and near-zero for random ones.
19+
20+
score = 1 - pearson_r (lower = better, consistent with Hungarian minimization)
21+
22+
Bias terms are irrelevant: mean_act uses only the inp bias inside ReLU (shifts the
23+
active set, which is part of the signal), and col_norm ignores b_out entirely.
24+
"""
25+
import io
26+
import logging
27+
import numpy as np
28+
import pandas as pd
29+
import torch
30+
from flowthru import step
31+
32+
logger = logging.getLogger(__name__)
33+
34+
35+
@step(
36+
inputs=["PieceMetadata", "PieceBlob", "BlockCandidate", "MeasurementSchema"],
37+
outputs=["PairingScore"],
38+
)
39+
def compute_activation_scores(
40+
piece_metadata: pd.DataFrame,
41+
pieces: pd.DataFrame,
42+
legal_pairings: pd.DataFrame,
43+
historical_data: pd.DataFrame,
44+
) -> pd.DataFrame:
45+
"""Score each legal (inp, out) pairing by activation-pattern / attention alignment.
46+
47+
Uses Pearson correlation between:
48+
- mean_act[inp]: which of the 96 hidden neurons inp activates most strongly on data
49+
- col_norm[out]: which of the 96 hidden neurons out pays most attention to
50+
51+
A trained pair has high positive correlation; mismatched pairs are near zero.
52+
score = 1 - pearson_r (lower = better for Hungarian minimization).
53+
54+
Args:
55+
piece_metadata: Structural metadata for all pieces (LayerType, dims).
56+
pieces: Raw byte blobs indexed by PieceIndex.
57+
legal_pairings: Dimension-valid (InpPieceIndex, OutPieceIndex) candidates.
58+
historical_data: Sensor measurements used to probe inp activation patterns.
59+
60+
Returns:
61+
DataFrame with [InpPieceIndex, OutPieceIndex, CoherenceScore].
62+
Lower CoherenceScore indicates a more likely trained pair.
63+
"""
64+
blob_by_index: dict[int, bytes] = {
65+
int(r["PieceIndex"]): r["Data"] for _, r in pieces.iterrows()
66+
}
67+
68+
legal_set: set[tuple[int, int]] = set(
69+
zip(
70+
legal_pairings["InpPieceIndex"].astype(int),
71+
legal_pairings["OutPieceIndex"].astype(int),
72+
)
73+
)
74+
75+
inp_indices = (
76+
piece_metadata[piece_metadata["LayerType"] == "BlockInp"]["PieceIndex"]
77+
.astype(int)
78+
.tolist()
79+
)
80+
out_indices = (
81+
piece_metadata[piece_metadata["LayerType"] == "BlockOut"]["PieceIndex"]
82+
.astype(int)
83+
.tolist()
84+
)
85+
86+
logger.info(
87+
f"[compute_activation_scores] {len(inp_indices)} inp × {len(out_indices)} out = "
88+
f"{len(legal_set)} legal pairs"
89+
)
90+
91+
feature_cols = [f"measurement_{i}" for i in range(48)]
92+
X_t = torch.tensor(
93+
historical_data[feature_cols].values, dtype=torch.float32
94+
).T # (48, N)
95+
96+
def load_state(piece_idx: int) -> dict[str, torch.Tensor]:
97+
return torch.load(
98+
io.BytesIO(blob_by_index[piece_idx]),
99+
weights_only=True,
100+
map_location=torch.device("cpu"),
101+
)
102+
103+
# ------------------------------------------------------------------
104+
# mean_act[inp]: (96,) — average activation of each hidden neuron over the dataset
105+
# ------------------------------------------------------------------
106+
mean_act: dict[int, np.ndarray] = {}
107+
with torch.no_grad():
108+
for inp_idx in inp_indices:
109+
sd = load_state(inp_idx)
110+
H = torch.relu(sd["weight"] @ X_t + sd["bias"].unsqueeze(1)) # (96, N)
111+
mean_act[inp_idx] = H.mean(dim=1).numpy() # (96,)
112+
113+
# ------------------------------------------------------------------
114+
# col_norm[out]: (96,) — L2 norm of each column of W_out
115+
# column k corresponds to how much out "attends to" hidden neuron k
116+
# ------------------------------------------------------------------
117+
col_norm: dict[int, np.ndarray] = {}
118+
for out_idx in out_indices:
119+
sd = load_state(out_idx)
120+
col_norm[out_idx] = np.linalg.norm(sd["weight"].numpy(), axis=0) # (96,)
121+
122+
# ------------------------------------------------------------------
123+
# Pearson correlation → score = 1 - r (lower = better)
124+
# ------------------------------------------------------------------
125+
def pearson_r(a: np.ndarray, b: np.ndarray) -> float:
126+
a_c = a - a.mean()
127+
b_c = b - b.mean()
128+
denom = np.linalg.norm(a_c) * np.linalg.norm(b_c)
129+
return float(np.dot(a_c, b_c) / denom) if denom > 1e-8 else 0.0
130+
131+
rows = []
132+
for inp_idx in inp_indices:
133+
for out_idx in out_indices:
134+
if (inp_idx, out_idx) not in legal_set:
135+
continue
136+
r = pearson_r(mean_act[inp_idx], col_norm[out_idx])
137+
rows.append({
138+
"InpPieceIndex": inp_idx,
139+
"OutPieceIndex": out_idx,
140+
"CoherenceScore": 1.0 - r, # lower = better alignment
141+
})
142+
143+
logger.info(f"[compute_activation_scores] Computed {len(rows)} scores")
144+
return pd.DataFrame(rows, columns=["InpPieceIndex", "OutPieceIndex", "CoherenceScore"])
145+
146+
147+
# =============================================================================
148+
# v1 — SUPERSEDED: mean residual magnitude
149+
# Bias dominates ||W_out @ H + b_out||_2 regardless of H; poor within-row gaps.
150+
# =============================================================================
151+
# def compute_activation_scores_v1(...):
152+
# ...
153+
# for inp_idx in inp_indices:
154+
# H = inp_activations[inp_idx]
155+
# for out_idx in out_indices:
156+
# W_out, b_out = out_params[out_idx]
157+
# R = W_out @ H + b_out.unsqueeze(1)
158+
# score = float(torch.norm(R, dim=0).mean())
159+
# rows.append({"InpPieceIndex": inp_idx, "OutPieceIndex": out_idx,
160+
# "CoherenceScore": score})
161+

0 commit comments

Comments
 (0)