VLbai-2.6AD / probe_features.py
eyupipler's picture
Upload 21 files
1013007 verified
Raw
History Blame Contribute Delete
3.78 kB
"""
Feature probe
=============
Question: do the vectors produced by extract_features.py actually CONTAIN the
class information?
Why this matters: when the LLM side underperforms there are two very different
causes β€”
(a) the information is not in the vector β†’ encoder / feature problem
(b) it is there but the LLM cannot read it β†’ channel problem
These call for opposite fixes. Fitting a plain LOGISTIC REGRESSION on the vector
separates them: if a linear layer can recover the class, the information is
present and the fault lies in the channel.
Measurement on the released checkpoint (subject-level split, 123 test cases):
fused (512) bal_acc 0.880 ← the head itself scores 0.895
tab (256) bal_acc 0.855
mri (512) bal_acc 0.410 (chance is 0.333)
Conclusion: the information is present and linearly decodable. Early failures of
projector + LoRA training were NOT a data or feature problem β€” they were a
channel problem.
Needs no GPU; runs in seconds.
Run:
python probe_features.py --features features.pt
"""
from __future__ import annotations
import argparse
import numpy as np
import torch
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import balanced_accuracy_score, f1_score, confusion_matrix
from sklearn.preprocessing import StandardScaler
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--features", required=True)
ap.add_argument("--C", type=float, default=1.0,
help="logistic regression regularisation")
args = ap.parse_args()
d = torch.load(args.features, map_location="cpu", weights_only=False)
y = d["label"].numpy()
sp = np.array(d["split"])
cls = list(d["class_names"])
tr, te = sp == "train", sp == "test"
print(f"[data] {len(y)} records | "
f"{ {cls[i]: int((y == i).sum()) for i in range(len(cls))} }")
print(f"[split] train {int(tr.sum())} / test {int(te.sum())}\n")
sets = {
"fused (512)": d["fused_features"],
"mri (512)": d["mri_features"],
"tab (256)": d["tab_features"],
"all (1280)": torch.cat(
[d["fused_features"], d["mri_features"], d["tab_features"]], dim=-1),
}
best_name, best_ba = None, -1.0
for name, X in sets.items():
X = X.numpy()
sc = StandardScaler().fit(X[tr])
clf = LogisticRegression(max_iter=3000, C=args.C,
class_weight="balanced").fit(sc.transform(X[tr]), y[tr])
p = clf.predict(sc.transform(X[te]))
ba = balanced_accuracy_score(y[te], p)
print(f" {name:14s} bal_acc={ba:.3f} macroF1={f1_score(y[te], p, average='macro'):.3f}")
if ba > best_ba:
best_name, best_ba, best_pred = name, ba, p
hp = d["class_probs"].argmax(-1).numpy()
hb = balanced_accuracy_score(y[te], hp[te])
print(f"\n {'head (reference)':14s} bal_acc={hb:.3f} "
f"macroF1={f1_score(y[te], hp[te], average='macro'):.3f}")
print(f"\n Best vector: {best_name}")
print(f" Confusion (rows = true, columns = predicted {cls}):")
print(" " + str(confusion_matrix(y[te], best_pred)).replace("\n", "\n "))
print("\n" + "-" * 62)
if best_ba >= 0.8 * hb:
print(" β†’ The information is present and linearly decodable.")
print(" If the LLM side misbehaves, the fault is in the CHANNEL")
print(" (projector / LoRA), not the features. No need to retrain")
print(" the encoder.")
else:
print(" β†’ The vector does not carry what the head knows. Check feature")
print(" extraction first, and confirm the checkpoint matches the")
print(" input modality.")
print("-" * 62)
if __name__ == "__main__":
main()