Spaces:
Runtime error
Runtime error
| """Train the modern, model-agnostic AI-text detector and validate it the honest | |
| way: LEAVE-ONE-MODEL-OUT (does it catch a generator it never trained on?) plus | |
| an external test on the 13 real Turnitin papers. | |
| Features: plagdetect/aifeatures (surface regularities, torch-free). Model: a | |
| regularised logistic regression (18 features, ~1900 docs => low overfit), Platt | |
| -calibrated. Saved to models/ai_modern.json for aidetect to consume. | |
| Run: python scripts/train_modern_ai.py | |
| """ | |
| import json, os, sys, time | |
| import numpy as np | |
| ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| sys.path.insert(0, ROOT) | |
| from plagdetect.aifeatures import FEATURES, vector # noqa: E402 | |
| from plagdetect.webpipeline import extract_text, split_body_references # noqa: E402 | |
| from sklearn.linear_model import LogisticRegression # noqa: E402 | |
| from sklearn.preprocessing import StandardScaler # noqa: E402 | |
| from sklearn.metrics import roc_auc_score # noqa: E402 | |
| CORPUS = os.path.join(ROOT, "data", "calibration", "mage_sci.jsonl") | |
| SCORED = os.path.join(ROOT, "data", "calibration", "mage_sci_scored.jsonl") | |
| GT = os.path.join(ROOT, "data", "turnitin_groundtruth.json") | |
| DSET = os.path.join(ROOT, "DATASET FOR training of turnitin") | |
| OUT = os.path.join(ROOT, "models", "ai_modern.json") | |
| MIN_AI_PER_MODEL = 25 # only validate against models with enough samples | |
| def score_corpus(): | |
| if os.path.exists(SCORED): | |
| return [json.loads(l) for l in open(SCORED, encoding="utf-8")] | |
| rows = [json.loads(l) for l in open(CORPUS, encoding="utf-8")] | |
| out = [] | |
| for i, r in enumerate(rows): | |
| out.append({"x": vector(r["text"]), "y": r["y"], "g": r["src_model"]}) | |
| if i % 200 == 0: | |
| print(f" scored {i}/{len(rows)}", end="\r") | |
| with open(SCORED, "w", encoding="utf-8") as f: | |
| for r in out: | |
| f.write(json.dumps(r) + "\n") | |
| print(f"\nscored {len(out)} -> {SCORED}") | |
| return out | |
| def fit(Xtr, ytr, C=0.3): | |
| sc = StandardScaler().fit(Xtr) | |
| clf = LogisticRegression(max_iter=5000, C=C, class_weight="balanced") | |
| clf.fit(sc.transform(Xtr), ytr) | |
| return sc, clf | |
| def main(): | |
| data = score_corpus() | |
| X = np.array([d["x"] for d in data], float) | |
| y = np.array([d["y"] for d in data], int) | |
| g = np.array([d["g"] for d in data]) | |
| ai_models = sorted({m for m in g if m != "human"}) | |
| big = [m for m in ai_models if (g == m).sum() >= MIN_AI_PER_MODEL] | |
| hum_idx = np.where(y == 1)[0] | |
| rng = np.random.RandomState(13) | |
| rng.shuffle(hum_idx) | |
| hum_folds = np.array_split(hum_idx, len(big)) | |
| # ---- leave-one-MODEL-out: train without model m, test human vs m -------- | |
| print("LEAVE-ONE-MODEL-OUT (held-out generator never seen in training)") | |
| print("-" * 60) | |
| aucs = [] | |
| for i, m in enumerate(big): | |
| te_h = hum_folds[i] | |
| te_ai = np.where(g == m)[0] | |
| te = np.concatenate([te_h, te_ai]) | |
| tr = np.array([j for j in range(len(y)) | |
| if g[j] != m and j not in set(te_h)]) | |
| sc, clf = fit(X[tr], y[tr]) | |
| p = clf.predict_proba(sc.transform(X[te]))[:, 1] # p(human) | |
| auc = roc_auc_score(y[te], p) | |
| aucs.append(auc) | |
| print(f" hold {m:24s} n_ai={len(te_ai):3d} AUC={auc:.3f}") | |
| print(f"\nMEAN leave-one-model-out AUC = {np.mean(aucs):.3f} " | |
| f"(min {np.min(aucs):.3f})") | |
| # ---- final model on all + Platt calibration ----------------------------- | |
| idx = rng.permutation(len(y)) | |
| n_tr = int(0.8 * len(y)) | |
| tr, cal = idx[:n_tr], idx[n_tr:] | |
| sc, clf = fit(X[tr], y[tr]) | |
| p_cal_raw = clf.predict_proba(sc.transform(X[cal]))[:, 1] | |
| platt = LogisticRegression(max_iter=5000, C=1e6) | |
| platt.fit(p_cal_raw.reshape(-1, 1), y[cal]) | |
| # NOTE: model predicts p(human); we store p_ai = 1 - p(human) | |
| gx = np.linspace(0, 1, 101) | |
| gy = platt.predict_proba(gx.reshape(-1, 1))[:, 1] | |
| meta = {"features": FEATURES, | |
| "mean": sc.mean_.tolist(), "scale": sc.scale_.tolist(), | |
| "coef": clf.coef_[0].tolist(), "intercept": float(clf.intercept_[0]), | |
| "platt_x": gx.tolist(), "platt_y": gy.tolist(), | |
| "predicts": "p_human", | |
| "lomo_auc": round(float(np.mean(aucs)), 4), | |
| "lomo_min": round(float(np.min(aucs)), 4), | |
| "n": len(y), "models": big, | |
| "dataset": "MAGE/sci_gen", "trained": time.strftime("%Y-%m-%d %H:%M")} | |
| json.dump(meta, open(OUT, "w", encoding="utf-8"), indent=1) | |
| print(f"saved {OUT}") | |
| # ---- external test on the 13 real Turnitin papers ----------------------- | |
| print("\nEXTERNAL TEST — 13 real papers (Turnitin AI% as noisy reference)") | |
| print("-" * 60) | |
| gt = json.load(open(GT, encoding="utf-8")) | |
| print(f"{'draft':30s} {'turnitin':>8s} {'p_ai':>6s}") | |
| yy, pp = [], [] | |
| for rec in gt: | |
| draft, ai = rec.get("draft"), (rec.get("ai") or {}).get("ai_pct") | |
| if not draft or ai is None: | |
| continue | |
| _t, text = extract_text(os.path.join(DSET, draft)) | |
| body, _ = split_body_references(text) | |
| x = (np.array(vector(body)) - sc.mean_) / sc.scale_ | |
| p_h = clf.predict_proba(x.reshape(1, -1))[:, 1][0] | |
| p_ai = 1 - p_h | |
| lbl = "AI" if (ai != "*" and ai >= 40) else ("hum" if ai == "*" else "?") | |
| print(f"{draft[:29]:30s} {str(ai):>8s} {p_ai:6.2f} {lbl}") | |
| if lbl in ("AI", "hum"): | |
| yy.append(1 if lbl == "AI" else 0) | |
| pp.append(p_ai) | |
| if len(set(yy)) == 2: | |
| print(f"\nexternal AUC (AI>=40 vs suppressed<20) = " | |
| f"{roc_auc_score(yy, pp):.3f} on {len(yy)} papers") | |
| if __name__ == "__main__": | |
| main() | |