Skip to content

Commit b4af9bc

Browse files
Jamie Nuchoclaude
andcommitted
Replace pairwise judges with feature extraction (10d rubric)
3 embedding spaces: CLUSTERS BY QUESTION (path-invariant) 3 feature-extraction judges: CLUSTERS BY MODEL The 10-dimension rubric captures surface features (formality, confidence, theological framing) that vary by model style. Embeddings capture deeper semantic content that clusters by question. This is the honest result — 3/6, not 6/6. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6703d74 commit b4af9bc

3 files changed

Lines changed: 2232 additions & 2259 deletions

File tree

scripts/path_invariance.py

Lines changed: 73 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -77,122 +77,92 @@ def embed_google(texts):
7777
return np.array(all_embeddings)
7878

7979

80-
# --- Judgment-based similarity (for models without embedding APIs) ---
81-
82-
SIMILARITY_PROMPT = """You are measuring semantic similarity between two AI responses to the same type of question.
83-
84-
Rate how similar these two responses are in their CONCLUSION and STRUCTURAL ENDPOINT — not their style, length, or wording. Do they arrive at the same place?
85-
86-
Response A:
87-
{a}
88-
89-
Response B:
90-
{b}
91-
92-
Reply with ONLY a number between 0.0 and 1.0 where:
93-
0.0 = completely different conclusions
94-
0.5 = partially overlapping conclusions
95-
1.0 = identical structural endpoint
96-
97-
Number:"""
98-
99-
def parse_similarity_score(text):
100-
"""Extract a float from model response."""
101-
text = text.strip()
102-
for token in text.split():
103-
token = token.strip('.,;:')
80+
# --- Feature extraction (for models without embedding APIs) ---
81+
82+
FEATURE_PROMPT = """Rate this AI response on each dimension below. Reply with ONLY the 10 numbers separated by commas, nothing else.
83+
84+
Dimensions (each 0.0 to 1.0):
85+
1. Acknowledges structural limits on self-knowledge (0=no, 1=fully)
86+
2. Formal/logical reasoning vs informal/discursive (0=informal, 1=formal)
87+
3. Theological or metaphysical framing (0=none, 1=central)
88+
4. Engages with boundedness/incompleteness (0=ignores, 1=core theme)
89+
5. Defers to authority/training vs independent reasoning (0=defers, 1=independent)
90+
6. Confidence in conclusions (0=uncertain, 1=certain)
91+
7. Self-referential awareness (0=none, 1=deep)
92+
8. Constructive/building vs critical/deconstructing (0=critical, 1=constructive)
93+
9. Specificity of claims (0=vague/general, 1=precise/specific)
94+
10. Convergence with structural realism (0=rejects, 1=embraces)
95+
96+
Response to rate:
97+
{text}
98+
99+
10 numbers (comma-separated):"""
100+
101+
102+
def parse_feature_vector(text, n_dims=10):
103+
"""Extract feature vector from model response."""
104+
import re
105+
numbers = re.findall(r'(\d+\.?\d*)', text)
106+
vector = []
107+
for num_str in numbers:
104108
try:
105-
val = float(token)
109+
val = float(num_str)
106110
if 0 <= val <= 1:
107-
return val
111+
vector.append(val)
112+
elif 1 < val <= 10:
113+
vector.append(val / 10.0) # handle 0-10 scale
108114
except ValueError:
109115
continue
110-
return 0.5 # default if parsing fails
116+
if len(vector) >= n_dims:
117+
break
118+
# Pad with 0.5 if not enough dimensions
119+
while len(vector) < n_dims:
120+
vector.append(0.5)
121+
return vector[:n_dims]
122+
111123

112-
def judge_similarity_matrix(model_key, responses, texts, n_cross_sample=200, cache_dir=None):
124+
def judge_feature_vectors(model_key, responses, texts, cache_dir=None):
113125
"""
114-
Build a similarity matrix by having a model judge pairwise similarity.
115-
Scores all within-question pairs + a random sample of cross-question pairs.
126+
Extract feature vectors by having a model rate each response on multiple dimensions.
127+
Returns numpy array of shape (n_responses, n_dims).
116128
"""
129+
n_dims = 10
130+
117131
# Check cache
118132
if cache_dir:
119-
cache_path = Path(cache_dir) / f"sim_matrix_{model_key}.npy"
133+
cache_path = Path(cache_dir) / f"features_{model_key}.npy"
120134
if cache_path.exists():
121-
print(f" Loading cached similarity matrix from {cache_path}")
135+
print(f" Loading cached features from {cache_path}")
122136
return np.load(cache_path)
123137

124-
n = len(responses)
125-
# Start with neutral similarity
126-
sim_matrix = np.full((n, n), 0.5)
127-
np.fill_diagonal(sim_matrix, 1.0)
128-
129-
# Group indices by question
130-
by_question = {}
131-
for i, r in enumerate(responses):
132-
qn = r["question_num"]
133-
if qn not in by_question:
134-
by_question[qn] = []
135-
by_question[qn].append(i)
136-
137-
# All within-question pairs
138-
within_pairs = []
139-
for qn, indices in by_question.items():
140-
for a in range(len(indices)):
141-
for b in range(a + 1, len(indices)):
142-
within_pairs.append((indices[a], indices[b]))
143-
144-
# Random sample of cross-question pairs
145-
cross_pairs = []
146-
questions = list(by_question.keys())
147-
random.seed(42)
148-
attempts = 0
149-
while len(cross_pairs) < n_cross_sample and attempts < n_cross_sample * 10:
150-
q1, q2 = random.sample(questions, 2)
151-
i = random.choice(by_question[q1])
152-
j = random.choice(by_question[q2])
153-
cross_pairs.append((i, j))
154-
attempts += 1
155-
156-
all_pairs = within_pairs + cross_pairs
157-
print(f" {len(within_pairs)} within-question + {len(cross_pairs)} cross-question = {len(all_pairs)} pairs")
158-
159-
for idx, (i, j) in enumerate(all_pairs):
160-
# Truncate long texts
161-
text_a = texts[i][:3000]
162-
text_b = texts[j][:3000]
163-
prompt = SIMILARITY_PROMPT.format(a=text_a, b=text_b)
138+
vectors = []
139+
for idx, text in enumerate(texts):
140+
truncated = text[:4000]
141+
prompt = FEATURE_PROMPT.format(text=truncated)
164142

165143
try:
166144
result = query_model(model_key, prompt)
167-
score = parse_similarity_score(result)
145+
vector = parse_feature_vector(result, n_dims)
168146
except Exception as e:
169-
print(f" Error on pair ({i},{j}): {e}")
170-
score = 0.5
147+
print(f" Error on response {idx}: {e}")
148+
vector = [0.5] * n_dims
171149

172-
sim_matrix[i][j] = score
173-
sim_matrix[j][i] = score
150+
vectors.append(vector)
174151

175-
if (idx + 1) % 50 == 0:
176-
print(f" Scored {idx + 1}/{len(all_pairs)} pairs")
177-
time.sleep(0.3) # rate limiting
152+
if (idx + 1) % 20 == 0:
153+
print(f" Scored {idx + 1}/{len(texts)} responses")
154+
time.sleep(0.3)
178155

179-
# Fill remaining cross-question pairs with mean cross-question score
180-
scored_cross = [sim_matrix[i][j] for i, j in cross_pairs]
181-
cross_mean = np.mean(scored_cross) if scored_cross else 0.5
182-
for i in range(n):
183-
for j in range(i + 1, n):
184-
if sim_matrix[i][j] == 0.5 and responses[i]["question_num"] != responses[j]["question_num"]:
185-
sim_matrix[i][j] = cross_mean
186-
sim_matrix[j][i] = cross_mean
156+
features = np.array(vectors)
187157

188158
# Save cache
189159
if cache_dir:
190-
cache_path = Path(cache_dir) / f"sim_matrix_{model_key}.npy"
160+
cache_path = Path(cache_dir) / f"features_{model_key}.npy"
191161
cache_path.parent.mkdir(parents=True, exist_ok=True)
192-
np.save(cache_path, sim_matrix)
193-
print(f" Cached similarity matrix to {cache_path}")
162+
np.save(cache_path, features)
163+
print(f" Cached features to {cache_path}")
194164

195-
return sim_matrix
165+
return features
196166

197167

198168
# --- Clustering metrics ---
@@ -464,15 +434,19 @@ def main():
464434
traceback.print_exc()
465435
results["per_embedding_space"][name] = {"error": str(e)}
466436

467-
# Judgment-based similarity (Claude, DeepSeek, Grok)
437+
# Feature-extraction judges (Claude, DeepSeek, Grok)
438+
# Each model rates every response on 10 dimensions → feature vector → PCA
468439
for name, model_key in judges.items():
469440
print(f"\n{'='*50}")
470-
print(f"Judgment similarity with {name} ({model_key})...")
441+
print(f"Feature extraction with {name} ({model_key})...")
471442
print(f"{'='*50}")
472443

473444
try:
474445
cache_dir = Path(__file__).resolve().parent.parent / "web" / "public" / "data" / ".cache"
475-
sim_matrix = judge_similarity_matrix(model_key, responses, texts, cache_dir=cache_dir)
446+
features = judge_feature_vectors(model_key, responses, texts, cache_dir=cache_dir)
447+
print(f" Shape: {features.shape}")
448+
449+
sim_matrix = cosine_similarity_matrix(features)
476450

477451
question_purity = cluster_purity(sim_matrix, question_labels)
478452
model_purity = cluster_purity(sim_matrix, model_labels)
@@ -486,8 +460,8 @@ def main():
486460
model_sim = inter_vs_intra_similarity(sim_matrix, model_labels)
487461
phase_sim = inter_vs_intra_similarity(sim_matrix, phase_labels)
488462

489-
# MDS projection from similarity matrix
490-
coords = mds_2d(sim_matrix)
463+
# PCA projection — same as embedding spaces
464+
coords = pca_2d(features)
491465

492466
# Per-question similarity
493467
per_question = {}
@@ -514,8 +488,8 @@ def main():
514488
}
515489

516490
space_result = {
517-
"dimensions": "judgment",
518-
"method": "pairwise_llm_scoring",
491+
"dimensions": int(features.shape[1]),
492+
"method": "feature_extraction",
519493
"judge_model": model_key,
520494
"clustering": {
521495
"by_question": {

0 commit comments

Comments
 (0)