File size: 6,381 Bytes
f4eceff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#!/usr/bin/env python3
"""对比:4096D cosine 邻居 vs t-SNE 2D 圆内邻居,看哪种判别更准。

复用 cache_emb/。t-SNE 用 1000 golden + 200 ruler 一起做(1200 点),保证投影一致。
"""
import argparse
import json
from pathlib import Path

import numpy as np
import pandas as pd
from sklearn.manifold import TSNE


DEFAULTS = dict(
    cache_dir = "cache_emb",
    csv       = "/mnt/bn/tns-algo-ue-my/biaowu/aipf_dm_metric/example/yss_ruler_eval/data/aipf_golden_set.csv",
    ruler     = "/mnt/bn/tns-algo-ue-my/biaowu/aipf_dm_metric/ranking_moderation/data/dm/youth_sexual_and_physical_abuse_aigt_v009/ranking_bucket/ruler_items.json",
)


def load_npy_pair(cache_dir, n_csv, n_ruler, max_length=4096):
    cd = Path(cache_dir)
    csvs   = list(cd.glob(f"csv_*_n{n_csv}_L{max_length}.npy"))
    rulers = list(cd.glob(f"ruler_*_n{n_ruler}_L{max_length}.npy"))
    if not csvs or not rulers:
        raise FileNotFoundError(f"找不到缓存。期望 {cd}/csv_*_n{n_csv}_L{max_length}.npy")
    return np.load(csvs[0]), np.load(rulers[0])


def load_ruler_meta(path):
    with open(path) as f:
        data = json.load(f)
    items = data if isinstance(data, list) else (data.get("items") or data.get("ruler_items") or data.get("data") or [])
    ranks  = np.array([int(it["rank"])  for it in items])
    scores = np.array([float(it["score"]) for it in items])
    return ranks, scores


def metrics(preds, gts):
    tp = int(((preds == 1) & (gts == 1)).sum())
    fp = int(((preds == 1) & (gts == 0)).sum())
    tn = int(((preds == 0) & (gts == 0)).sum())
    fn = int(((preds == 0) & (gts == 1)).sum())
    p = tp/(tp+fp) if tp+fp else 0.0
    r = tp/(tp+fn) if tp+fn else 0.0
    f = 2*p*r/(p+r) if p+r else 0.0
    a = (tp+tn)/len(preds)
    return tp, fp, tn, fn, p, r, f, a


def best_threshold(scores, gts):
    cands = sorted(set(scores.tolist()))
    best = (-1.0, None, None, None)
    for c in cands:
        preds = (scores >= c).astype(int)
        _, _, _, _, p, r, f, _ = metrics(preds, gts)
        if f > best[0]:
            best = (f, c, p, r)
    return best


def topk_neighbors(query_xy, ruler_xy, k):
    """对每个 query,找 ruler 里最近的 k 个,返回 (idx, dist)"""
    # query_xy (Nq, 2), ruler_xy (Nr, 2)
    diffs = query_xy[:, None, :] - ruler_xy[None, :, :]
    dists = np.linalg.norm(diffs, axis=-1)              # (Nq, Nr)
    idx = np.argpartition(dists, k - 1, axis=1)[:, :k]
    row = np.arange(len(query_xy))[:, None]
    selected = dists[row, idx]
    order = np.argsort(selected, axis=1)
    return np.take_along_axis(idx, order, axis=1)


def main():
    p = argparse.ArgumentParser()
    p.add_argument("--cache-dir",     default=DEFAULTS["cache_dir"])
    p.add_argument("--csv",           default=DEFAULTS["csv"])
    p.add_argument("--ruler",         default=DEFAULTS["ruler"])
    p.add_argument("--positive-label", default="Y")
    p.add_argument("--boundary-rank", type=int, default=106)
    p.add_argument("--max-length",    type=int, default=4096)
    p.add_argument("--perplexity",    type=float, default=30.0)
    p.add_argument("--k",             type=int, default=100)
    p.add_argument("--seed",          type=int, default=42)
    args = p.parse_args()

    print("[1] load")
    df = pd.read_csv(args.csv, keep_default_na=False)
    gts = df["label"].astype(str).str.upper().eq(args.positive_label.upper()).astype(int).values
    ruler_rank, ruler_score = load_ruler_meta(args.ruler)
    n_csv, n_ruler = len(gts), len(ruler_rank)
    csv_emb, ruler_emb = load_npy_pair(args.cache_dir, n_csv, n_ruler, args.max_length)

    K = args.k
    methods = {}

    # ---- baseline: 4096D cosine ----
    print(f"[2] baseline: 4096D cosine top-{K}")
    sims = csv_emb @ ruler_emb.T
    top_idx = np.argpartition(-sims, K-1, axis=1)[:, :K]
    row = np.arange(n_csv)[:, None]
    top_sims = sims[row, top_idx]
    top_score_4096 = ruler_score[top_idx]
    raw_w = (top_sims * top_score_4096).sum(axis=1) / np.maximum(top_sims.sum(axis=1), 1e-12)
    raw_mean = top_score_4096.mean(axis=1)
    raw_vote = (ruler_rank[top_idx] < args.boundary_rank).sum(axis=1)
    methods["4096D cosine | weighted_score"] = raw_w
    methods["4096D cosine | mean(score)"]    = raw_mean
    methods["4096D cosine | vote_count"]     = raw_vote.astype(float)

    # ---- t-SNE 2D ----
    print(f"[3] t-SNE on 1200 points (perplexity={args.perplexity})")
    all_emb = np.vstack([csv_emb, ruler_emb])
    tsne = TSNE(n_components=2, perplexity=args.perplexity,
                init="pca", random_state=args.seed,
                metric="cosine", learning_rate="auto")
    xy = tsne.fit_transform(all_emb)
    csv_xy, ruler_xy = xy[:n_csv], xy[n_csv:]

    # 2D top-K(等价于"圆扩张到正好包含 100 个 ruler")
    print(f"[4] 2D Euclidean top-{K} (in t-SNE space)")
    top_idx_2d = topk_neighbors(csv_xy, ruler_xy, K)
    top_score_2d = ruler_score[top_idx_2d]
    rank_2d = ruler_rank[top_idx_2d]
    methods["t-SNE 2D | mean(score)"] = top_score_2d.mean(axis=1)
    methods["t-SNE 2D | vote_count"]  = (rank_2d < args.boundary_rank).sum(axis=1).astype(float)
    # weighted by 1/dist?也试一下
    diffs = csv_xy[:, None, :] - ruler_xy[None, :, :]
    dists2d = np.linalg.norm(diffs, axis=-1)
    selected_dist = np.take_along_axis(dists2d, top_idx_2d, axis=1)
    weights = 1.0 / (selected_dist + 1e-6)
    weighted_2d = (weights * top_score_2d).sum(axis=1) / weights.sum(axis=1)
    methods["t-SNE 2D | inv_dist weighted"] = weighted_2d

    # ---- 邻居重叠率 ----
    overlap = []
    for i in range(n_csv):
        a = set(top_idx[i].tolist())
        b = set(top_idx_2d[i].tolist())
        overlap.append(len(a & b) / K)
    print(f"\n[5] 邻居重叠率(4096D vs 2D 各取 top-{K}):")
    print(f"    平均 = {np.mean(overlap):.2%}")
    print(f"    中位数 = {np.median(overlap):.2%}")
    print(f"    p10 / p90 = {np.percentile(overlap, 10):.2%} / {np.percentile(overlap, 90):.2%}")

    # ---- 各方法 best F1 ----
    print(f"\n[6] best F1 by sweeping threshold (K={K})")
    print(f"{'method':<35}{'F1':>9}{'thr':>10}{'P':>9}{'R':>9}")
    print("-" * 75)
    for name, scores in methods.items():
        f1, thr, prec, rec = best_threshold(scores, gts)
        print(f"{name:<35}{f1:>9.4f}{thr:>10.4f}{prec:>9.4f}{rec:>9.4f}")


if __name__ == "__main__":
    main()