pird-api / pird /data /raid.py
MohsinEli's picture
PIRD REST API: FastAPI + CORS, encoder-only checkpoint
e58f648 verified
Raw
History Blame Contribute Delete
5.64 kB
"""RAID loader (liamdugan/raid) — multi-domain, multi-generator AI-text data.
RAID is sorted by (domain, model, attack). Schema: text=`generation`, label=`model`
('human' = human, else = AI), `domain`, `attack` ('none' = clean).
IMPORTANT: HF only PARTIALLY converts RAID to parquet (it's huge), and because RAID is
domain-sorted the partial parquet contains only the *early* domains. So we never hardcode which
domains exist — `load_raid_records` returns domain-tagged records for whatever is present, and the
caller splits train/OOD from the domains actually observed. Shards are cached + column-pruned.
"""
from __future__ import annotations
import os
import random
import tempfile
from collections import Counter, defaultdict
import requests
_PARQUET_API = "https://datasets-server.huggingface.co/parquet?dataset=liamdugan/raid"
_CACHE_DIR = os.path.join(tempfile.gettempdir(), "raid_parquet")
_NEEDED = ["generation", "model", "domain", "attack"]
def _shard_files(split: str):
meta = requests.get(_PARQUET_API, timeout=60).json()
files = [f for f in meta.get("parquet_files", [])
if f.get("split") == split and f.get("config") == "raid"]
if not files:
raise RuntimeError(f"No RAID parquet shards for split={split}; api keys={list(meta)}")
files.sort(key=lambda f: f["url"])
print(f"[raid] split='{split}': {len(files)} shard(s), partial-conversion="
f"{requests.get(_PARQUET_API, timeout=60).json().get('partial')}")
return files
def _download_shard(f) -> str:
os.makedirs(_CACHE_DIR, exist_ok=True)
path = os.path.join(_CACHE_DIR, f["split"] + "_" + f["filename"])
if os.path.exists(path) and os.path.getsize(path) > 1000:
return path
print(f"[raid] downloading {f['filename']} ({f.get('size',0)/1e6:.0f} MB)...")
b = requests.get(f["url"], timeout=900).content
with open(path, "wb") as fh:
fh.write(b)
return path
def _read_shard(path):
import pandas as pd
try:
import pyarrow.parquet as pq
names = pq.read_schema(path).names
cols = [c for c in _NEEDED if c in names]
if len(cols) == len(_NEEDED):
return pd.read_parquet(path, columns=cols)
except Exception:
pass
return pd.read_parquet(path)
def load_raid_records(n_per_domain_class=300, split="train", attack="none",
min_words=30, max_words=600, max_shards=10, seed=42, verbose=True):
"""Return a list of {text, label(0=human,1=ai), domain, model} tagged records, balanced up to
`n_per_domain_class` per (domain, label) across whatever domains the partial parquet contains."""
counts = defaultdict(int)
records = []
raw_domains, raw_attacks = set(), set()
for si, f in enumerate(_shard_files(split)[:max_shards]):
df = _read_shard(_download_shard(f))
for col in _NEEDED:
if col not in df.columns:
raise RuntimeError(f"RAID shard missing '{col}'; columns={list(df.columns)}")
dom = df["domain"].astype(str).str.strip().str.lower()
atk = df["attack"].astype(str).str.strip().str.lower()
mdl = df["model"].astype(str).str.strip().str.lower()
raw_domains.update(dom.unique().tolist()); raw_attacks.update(atk.unique().tolist())
if si == 0 and verbose:
print(f"[raid] shard0 shape={df.shape} domains={sorted(set(dom))[:12]} "
f"attacks={sorted(set(atk))[:6]}")
mask = df["generation"].notna()
if attack is not None:
mask &= (atk == str(attack).lower())
wc = df["generation"].str.split().str.len()
mask &= (wc >= min_words) & (wc <= max_words)
idx = df.index[mask].tolist()
random.Random(seed + si).shuffle(idx)
gen = df["generation"]
for i in idx:
d = dom[i]; label = 0 if mdl[i] == "human" else 1
if counts[(d, label)] >= n_per_domain_class:
continue
counts[(d, label)] += 1
records.append({"text": gen[i].strip(), "label": label, "domain": d, "model": mdl[i]})
if verbose:
print(f"[raid] after {f['filename']}: domains={sorted({k[0] for k in counts})}")
if not records:
raise RuntimeError(f"RAID collected 0 records. pre-filter domains={sorted(raw_domains)} "
f"attacks={sorted(raw_attacks)} attack-requested='{attack}'.")
if verbose:
print(f"[raid] records/domain: {dict(Counter(r['domain'] for r in records))}")
print(f"[raid] label balance: {dict(Counter(r['label'] for r in records))}")
return records
def load_raid(n_per_class=1500, split="train", domains=None, holdout_domains=None,
attack="none", min_words=30, max_words=600, max_shards=10, seed=42, verbose=True):
"""(human, ai) convenience wrapper over load_raid_records, with optional domain include/holdout."""
recs = load_raid_records(n_per_domain_class=max(50, n_per_class // 3), split=split, attack=attack,
min_words=min_words, max_words=max_words, max_shards=max_shards,
seed=seed, verbose=verbose)
if domains:
ds = {d.lower() for d in domains}; recs = [r for r in recs if r["domain"] in ds]
if holdout_domains:
hs = {d.lower() for d in holdout_domains}; recs = [r for r in recs if r["domain"] not in hs]
human = [r["text"] for r in recs if r["label"] == 0]
ai = [r["text"] for r in recs if r["label"] == 1]
random.Random(seed + 2).shuffle(human); random.Random(seed + 3).shuffle(ai)
return human[:n_per_class], ai[:n_per_class]