Download my_rows.py from parsecai/ragzilla: direct link, hf CLI and curl.
- Browser
- Download file 4.61 kB
-
https://huggingface.co/parsecai/ragzilla/resolve/main/my_rows.py
- Command line
-
hf download hf://parsecai/ragzilla/my_rows.py
-
curl -L -o my_rows.py https://huggingface.co/parsecai/ragzilla/resolve/main/my_rows.py
4.61 kB
| """Build an openjev dataset from your own retrieval logs, to fine-tune or re-calibrate Ragzilla. | |
| One JSONL line per question your retriever served, with the passages in the order it returned them | |
| and which of them answered (from clicks, citations, annotators, or a judge you already run): | |
| {"id": "q17", "question": "how long is a cdl permit good for in texas", | |
| "passages": ["...", {"title": "...", "text": "..."}, "..."], "relevant": [0, 3], "group": "optional"} | |
| python my_rows.py my_rows.jsonl --out my_rows # 50 / 10 / 20 / 20 split by group | |
| openjev tod train my_rows --method encoder --model ragzilla --out my_ragzilla \ | |
| --loss focal --focal-gamma 2 --focal-alpha 0.5 --brier 0.1 --learning-rate 1e-5 --max-steps 25 | |
| openjev tod calibrate my_ragzilla my_rows | |
| openjev tod evaluate my_ragzilla my_rows | |
| Rows are prepared the way Ragzilla's training data was: whitespace collapsed, a titled passage rendered | |
| "<title>\\n<text>", passages over 4,000 characters cut at a word boundary, duplicate passages merged, | |
| empty passages dropped, rows with fewer than 2 passages dropped (and counted). Rows with the same `group` | |
| (a user, a document, the same question) share a split; a small workload needs a calibration split large | |
| enough to choose an operating point on, hence 20% calibration and 20% test by default. | |
| Needs only `pip install openjev-sdk==0.5.0`. The spec below is Ragzilla's own (`spec.json` in this | |
| folder has the same fingerprint), so the rows load into the model as its training rows did. | |
| """ | |
| import argparse | |
| import json | |
| import re | |
| from pathlib import Path | |
| from openjev.tod import Episode, ListSpec, Subset, build_dataset | |
| SPEC = ListSpec('ragzilla', [dict(name='relevant', question='Which passages are relevant to answering the question?', | |
| candidates='passages', version='1')]) | |
| MAX_CHARS = 4000 | |
| def clean(text): | |
| text = re.sub(r'\s+', ' ', text).strip() | |
| if len(text) > MAX_CHARS: | |
| text = text[:MAX_CHARS].rsplit(' ', 1)[0] + ' ...' | |
| return text | |
| def passage_text(p): | |
| if isinstance(p, dict): | |
| title = re.sub(r'\s+', ' ', p.get('title') or '').strip() | |
| body = clean(p.get('text') or '') | |
| return f'{title}\n{body}' if title and body else body | |
| return clean(str(p)) | |
| def decision(row, drops): | |
| """One SDK decision from a row, or None (counted in `drops`).""" | |
| texts, keep_idx, seen = [], [], {} | |
| for i, p in enumerate(row['passages']): | |
| t = passage_text(p) | |
| if not t: | |
| drops['empty passages'] += 1 | |
| continue | |
| if t in seen: # duplicate: the first copy stands for both | |
| if i in set(row.get('relevant', [])): | |
| keep_idx.append(seen[t]) | |
| drops['duplicate passages merged'] += 1 | |
| continue | |
| seen[t] = len(texts) | |
| if i in set(row.get('relevant', [])): | |
| keep_idx.append(len(texts)) | |
| texts.append(t) | |
| if len(texts) < 2: | |
| drops['rows with fewer than 2 passages'] += 1 | |
| return None | |
| state = {'question': re.sub(r'\s+', ' ', row['question']).strip(), | |
| 'passages': [{'id': f'p{i}', 'text': t} for i, t in enumerate(texts)]} | |
| target = Subset(sorted({f'p{i}' for i in keep_idx})) | |
| try: | |
| return SPEC.decision(str(row['id']), 'relevant', state, target=target, group_id=str(row.get('group') or row['id'])) | |
| except ValueError as exc: # e.g. more than 255 passages | |
| drops[f'refused by the SDK: {exc}'[:80]] += 1 | |
| return None | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument('rows', type=Path) | |
| ap.add_argument('--out', type=Path, required=True) | |
| ap.add_argument('--fractions', default='0.5,0.1,0.2,0.2', help='train,validation,calibration,test') | |
| ap.add_argument('--seed', default='0') | |
| a = ap.parse_args() | |
| from collections import Counter | |
| drops = Counter() | |
| rows = [json.loads(l) for l in a.rows.open(encoding='utf-8') if l.strip()] | |
| decisions = [d for d in (decision(r, drops) for r in rows) if d is not None] | |
| f = [float(x) for x in a.fractions.split(',')] | |
| fractions = dict(train=f[0], validation=f[1], calibration=f[2], test=f[3]) | |
| ds = build_dataset(SPEC, [Episode(d.id, (d,)) for d in decisions], fractions=fractions, seed=a.seed) | |
| ds.write(a.out) | |
| print(json.dumps(dict(out=str(a.out), rows_read=len(rows), decisions=len(decisions), dropped=dict(drops), | |
| spec_fingerprint=SPEC.fingerprint, splits=ds.report['splits']), indent=2)) | |
| if __name__ == '__main__': | |
| main() | |