Skip to content

Commit cdfdd5f

Browse files
authored
selection algorithm for Eval set (#659)
* selection algo * update
1 parent 11ebd1d commit cdfdd5f

1 file changed

Lines changed: 143 additions & 0 deletions

File tree

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""Markov-based greedy evaluation subset selection."""
2+
3+
from __future__ import annotations
4+
5+
import math
6+
from collections import Counter
7+
8+
import numpy as np
9+
from scipy import sparse
10+
11+
12+
def _min_max_normalize(values: list[float]) -> list[float]:
13+
if not values:
14+
return []
15+
low, high = min(values), max(values)
16+
if math.isclose(low, high):
17+
return [0.0] * len(values)
18+
return [(v - low) / (high - low) for v in values]
19+
20+
21+
def _build_markov_model(
22+
op_seqs: list[tuple[str, ...]],
23+
) -> tuple[dict[str, int], sparse.csr_matrix, np.ndarray]:
24+
operators = sorted({op for seq in op_seqs for op in seq})
25+
op_to_id = {op: idx for idx, op in enumerate(operators)}
26+
27+
transitions: Counter[tuple[str, str]] = Counter()
28+
for seq in op_seqs:
29+
for src, dst in zip(seq[:-1], seq[1:]):
30+
transitions[(src, dst)] += 1
31+
32+
rows, cols, data = [], [], []
33+
for (src, dst), cnt in transitions.items():
34+
rows.append(op_to_id[src])
35+
cols.append(op_to_id[dst])
36+
data.append(cnt)
37+
38+
size = len(operators)
39+
count_matrix = sparse.coo_matrix((data, (rows, cols)), shape=(size, size)).tocsr()
40+
row_sums = np.asarray(count_matrix.sum(axis=1)).ravel()
41+
return op_to_id, count_matrix, row_sums
42+
43+
44+
def _transition_prob(
45+
op_to_id: dict[str, int],
46+
count_matrix: sparse.csr_matrix,
47+
row_sums: np.ndarray,
48+
src: str,
49+
dst: str,
50+
alpha: float,
51+
) -> float:
52+
"""Laplace-smoothed P(dst | src)."""
53+
vocab_size = count_matrix.shape[1]
54+
src_id, dst_id = op_to_id[src], op_to_id[dst]
55+
count = float(count_matrix[src_id, dst_id])
56+
denom = float(row_sums[src_id]) + alpha * vocab_size
57+
return (count + alpha) / denom if denom else 1.0 / vocab_size
58+
59+
60+
def _compute_metrics(
61+
op_seqs: list[tuple[str, ...]],
62+
op_to_id: dict[str, int],
63+
count_matrix: sparse.csr_matrix,
64+
row_sums: np.ndarray,
65+
alpha: float,
66+
) -> list[dict]:
67+
result = []
68+
for seq in op_seqs:
69+
edges = list(zip(seq[:-1], seq[1:]))
70+
if not edges:
71+
result.append(
72+
{"rarity_score": 0.0, "unique_edges": frozenset(), "edge_count": 0}
73+
)
74+
continue
75+
neg_log_probs = [
76+
-math.log(
77+
_transition_prob(op_to_id, count_matrix, row_sums, src, dst, alpha)
78+
)
79+
for src, dst in edges
80+
]
81+
result.append(
82+
{
83+
"rarity_score": sum(neg_log_probs) / len(neg_log_probs),
84+
"unique_edges": frozenset(edges),
85+
"edge_count": len(edges),
86+
}
87+
)
88+
return result
89+
90+
91+
def _greedy_select(metrics: list[dict], k: int, rarity_weight: float) -> list[int]:
92+
"""Greedily maximise edge coverage, weighted by rarity score."""
93+
target = min(k, len(metrics))
94+
rarity_norm = _min_max_normalize([m["rarity_score"] for m in metrics])
95+
96+
selected: list[int] = []
97+
selected_set: set[int] = set()
98+
covered_edges: set[tuple[str, str]] = set()
99+
100+
while len(selected) < target:
101+
best_idx, best_key = None, None
102+
for idx, metric in enumerate(metrics):
103+
if idx in selected_set:
104+
continue
105+
new_edge_count = len(metric["unique_edges"] - covered_edges)
106+
bonus = rarity_weight * rarity_norm[idx]
107+
gain = float(new_edge_count) * (1.0 + bonus)
108+
key = (gain, new_edge_count, bonus, metric["edge_count"])
109+
if best_key is None or key > best_key:
110+
best_idx, best_key = idx, key
111+
if best_idx is None:
112+
break
113+
selected.append(best_idx)
114+
selected_set.add(best_idx)
115+
covered_edges.update(metrics[best_idx]["unique_edges"])
116+
117+
return selected
118+
119+
120+
def select_evaluation_subset(
121+
op_seqs: list[tuple[str, ...]],
122+
k: int,
123+
*,
124+
smoothing_alpha: float = 1e-3,
125+
rarity_weight: float = 1,
126+
) -> list[tuple[str, ...]]:
127+
"""Select k sequences from op_seqs using Markov-based greedy coverage.
128+
129+
Builds a Markov model over all sequences, scores each by rarity
130+
(mean -log transition prob), then greedily picks sequences that
131+
maximise new edge coverage weighted by rarity.
132+
133+
Returns selected sequences in greedy order (most valuable first).
134+
"""
135+
if k <= 0:
136+
return []
137+
seqs = [tuple(s) for s in op_seqs]
138+
if k >= len(seqs):
139+
return list(seqs)
140+
141+
op_to_id, count_matrix, row_sums = _build_markov_model(seqs)
142+
metrics = _compute_metrics(seqs, op_to_id, count_matrix, row_sums, smoothing_alpha)
143+
return [seqs[i] for i in _greedy_select(metrics, k, rarity_weight)]

0 commit comments

Comments
 (0)