Text Classification
Transformers
Safetensors
English
nli
cross-encoder
qwen3.5
reranker
image-text-to-text
Instructions to use AlexWortega/openjev with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use AlexWortega/openjev with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="AlexWortega/openjev")# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("AlexWortega/openjev", device_map="auto") - Notebooks
- Google Colab
- Kaggle
File size: 10,950 Bytes
1ccf050 | 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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | #!/usr/bin/env python
"""Keenable WebQL null detection (benchmarks/sem_extract_null_detection.py) with a jev NLI cross-encoder.
Binary task on the 1,200 sem_extract_bench pages: does the page state at least one of the requested values?
Gold "no data" = the 291 all-null examples. The request is rebuilt exactly as keenable-webql builds it for Jev:
* field instructions from `_spec_instruction` (SEM_EXTRACT / SEM_EXTRACT_ALL wording, evidence clauses included);
* the `jev_prefilter` question/true/false texts, includes_match=False;
* content truncated to 70,000 chars, then windowed with 2,000-char overlap, max probability over windows.
Jev windows at 35,000 chars; ours defaults to 24,000 (--window-chars) so a window fits the 8k-token encoder
without silent truncation. Same pages, same gold, same question — only the window size differs.
Two framings (--hyp):
claim (default) one plain claim per spec, "The document states <description> with <fields>." — the phrasing the
openjev WebQL runs used; p_has_data = P(entailment), max over specs and windows.
jev Jev's own true/false criteria as the two hypotheses, with the field instructions appended;
p_has_data = P(ent | true) / (P(ent | true) + P(ent | false)). Closer to the Jev wire format, but the text
is an instruction rather than a statement, which a cross-encoder reads badly.
python webql_null.py --models ckpt/qwen3.5-0.8b-nli-v2s-jev --data data/sem_extract_bench.jsonl \
--jev data/sem_extract_null_detection_jev-latest.jsonl --out results/v2s/webql_null.json
"""
import argparse
import json
import os
import time
import numpy as np
from eval import ENT, NLIScorer
TRUNCATE = 70_000
WINDOW, OVERLAP = 24_000, 2_000
QUOTE_SPAN = "the full sentence or complete table line around the supporting phrase"
CONTEXT_QUOTES = ("plus, as separate verbatim quotes, the page-level context: the table's header"
" row or caption when the supporting quote is a table row, and the page title,"
" section heading, or introductory sentence stating what the page or table is"
" about")
EVIDENCE_QUOTES = ("a list of one or more supporting quotes, each copied verbatim,"
" character for character, from the '{column}' text, spanning " + QUOTE_SPAN + ", " + CONTEXT_QUOTES)
EVIDENCE_INSTRUCTION = "'evidence' — " + EVIDENCE_QUOTES
# The Jev question ("Does the document state at least one of the requested values...") is not a statement, so it
# cannot be a hypothesis; its two criteria are, and the field list rides along with each of them.
TRUE = "The document explicitly states at least one requested value for the described entity."
FALSE = ("The document does not contain the requested data: the values are absent, the description does not apply"
" to this document, or only unrelated data is present.")
def field_keys(spec):
return "; ".join(f"'{name}': {description}" for name, description in spec["fields"])
def fields_evidence_clause(spec, subject):
col = spec["column"]
if spec.get("evidence") == "per_field":
return (f"; {subject} also carries one '<field>_evidence' key per non-null field, placed right after the"
f" field it supports and supporting that field's value alone — {EVIDENCE_QUOTES.format(column=col)}")
if spec.get("evidence"):
return f"; {subject} ends with {EVIDENCE_INSTRUCTION.format(column=col)}"
return ""
def spec_instruction(spec, key):
"""keenable_webql.operators.sem_extract._spec_instruction, rebuilt from the dataset's parsed `extract` block."""
col, desc = spec["column"], spec["description"]
if spec["func"] == "SEM_EXTRACT_ALL":
if spec["fields"]:
return (f"- {key}: an array with one object per {desc} (from the '{col}' field), each with keys"
f" {field_keys(spec)}; cover every distinct one the text states, null when it states none"
+ fields_evidence_clause(spec, "each object"))
out = f"- {key}: an array of values — every distinct {desc} the '{col}' text states; null when it states none"
if spec.get("evidence"):
out += f"; each array item is an object with 'value', then {EVIDENCE_INSTRUCTION.format(column=col)}"
return out
if spec["fields"]:
scope, null_when = "", "the text states none of them"
if desc:
scope = f"; object description: {desc}"
null_when += " or the description does not apply"
return (f"- {key}: an object with keys {field_keys(spec)} (from the '{col}' field){scope};"
f" null when {null_when}" + fields_evidence_clause(spec, "the object"))
out = f"- {key}: {desc} (from the '{col}' field)"
if spec.get("evidence"):
out += f"; return an object with 'value', then {EVIDENCE_INSTRUCTION.format(column=col)}"
return out
def is_evidence_key(k):
return k == "evidence" or k.endswith("_evidence")
def value_units(value):
if value is None:
return 0
if isinstance(value, dict):
return sum(value_units(v) for k, v in value.items() if not is_evidence_key(k))
if isinstance(value, list):
return sum(value_units(v) for v in value)
return 1
def windows(text, size=WINDOW):
if len(text) <= size:
return [text]
step = size - OVERLAP
return [text[s:s + size] for s in range(0, max(len(text) - OVERLAP, 1), step)]
def spec_claim(spec):
"""Plain statement form of a spec (openjev's webql_bench.spec_hypothesis)."""
d = spec["description"].strip()
if spec.get("fields"):
fields = "; ".join(f"{n}: {desc}" for n, desc in spec["fields"])
return f"The document states {d} with {fields}."
return f"The document states {d}."
def build(record, window=WINDOW, hyp="claim"):
"""(premise windows, [hypothesis]) — the Jev criteria pair, or one plain claim per spec."""
keys = [k for k in record["gold"] if not is_evidence_key(k)]
specs = record["extract"]
instr = "\n".join(spec_instruction(s, keys[i] if i < len(keys) else f"field{i}") for i, s in enumerate(specs))
hyps = [spec_claim(s) for s in specs] if hyp == "claim" else [
f"{TRUE}\nRequested fields:\n{instr}", f"{FALSE}\nRequested fields:\n{instr}"]
cols = sorted({s["column"] for s in specs})
state = {c: record["input"].get(c) for c in cols}
state = {k: (v[:TRUNCATE] if isinstance(v, str) and len(v) > TRUNCATE else v) for k, v in state.items()}
main = max((k for k, v in state.items() if isinstance(v, str)), key=lambda k: len(state[k]), default=None)
head = "\n".join(f"{k}: {json.dumps(v, ensure_ascii=False)[:2000]}" for k, v in state.items() if k != main)
body = state.get(main) or ""
return [f"{head}\n{main}:\n{w}".strip() for w in windows(body, window)], hyps
def metrics(gold_has_data, p, thr):
pred = np.asarray(p) >= thr
g = np.asarray(gold_has_data, dtype=bool)
tp, tn = int((g & pred).sum()), int((~g & ~pred).sum())
fp, fn = int((~g & pred).sum()), int((g & ~pred).sum())
r = lambda a, b: round(a / b, 4) if b else None # noqa: E731
return {"accuracy": r(tp + tn, len(g)), "null_recall": r(tn, tn + fp), "null_precision": r(tn, tn + fn),
"null_f1": r(2 * tn, 2 * tn + fn + fp), "data_recall": r(tp, tp + fn),
"confusion": {"tp": tp, "tn": tn, "fp": fp, "fn": fn}}
def report(gold, p):
from sklearn.metrics import roc_auc_score
thrs = [round(t, 2) for t in np.arange(0.05, 1.0, 0.05)]
at = {str(t): metrics(gold, p, t) for t in thrs}
best = max(thrs, key=lambda t: at[str(t)]["null_f1"] or 0)
return {"n": len(gold), "roc_auc": round(float(roc_auc_score(gold, p)), 4),
"at_0.3": metrics(gold, p, 0.3), "at_0.5": metrics(gold, p, 0.5),
"best_threshold": best, "at_best": at[str(best)], "sweep": at}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--models", nargs="+", required=True)
ap.add_argument("--data", default="data/sem_extract_bench.jsonl")
ap.add_argument("--jev", default=None, help="jev-latest run jsonl, re-scored here for reference")
ap.add_argument("--out", required=True)
ap.add_argument("--bs", type=int, default=8)
ap.add_argument("--max-len", type=int, default=8192)
ap.add_argument("--limit", type=int, default=0)
ap.add_argument("--window-chars", type=int, default=WINDOW)
ap.add_argument("--hyp", choices=["claim", "jev"], default="claim")
args = ap.parse_args()
records = [json.loads(l) for l in open(args.data) if l.strip()]
if args.limit:
records = records[: args.limit]
gold = [value_units(r["gold"]) > 0 for r in records]
prepared = [build(r, args.window_chars, args.hyp) for r in records]
print(f"{len(records)} pages, {sum(len(w) for w, _ in prepared)} windows, {sum(gold)} with data", flush=True)
res = json.load(open(args.out)) if os.path.exists(args.out) else {}
if args.jev:
by_id = {json.loads(l)["id"]: json.loads(l) for l in open(args.jev) if l.strip()}
rows = [(g, by_id[r["id"]]["p_has_data"]) for r, g in zip(records, gold) if r["id"] in by_id]
res["jev-1.13.0 (reference run)"] = report([g for g, _ in rows], [p for _, p in rows])
for m in args.models:
scorer = NLIScorer(m, bs=args.bs, max_len=args.max_len)
pairs, owner = [], []
for i, (wins, hyps) in enumerate(prepared):
for w in wins:
for h in hyps:
pairs.append((w, h)); owner.append(i)
t0 = time.perf_counter()
pe = scorer.predict(pairs)[:, ENT]
wall = time.perf_counter() - t0
p = np.zeros(len(records))
if args.hyp == "jev": # pairs come in (true, false) order per window
for (i, t), f in zip(list(zip(owner, pe))[::2], pe[1::2]):
p[i] = max(p[i], t / max(t + f, 1e-9))
else:
for i, v in zip(owner, pe):
p[i] = max(p[i], v)
res[m] = report(gold, p)
res[m]["wall_s"] = round(wall, 1)
res[m]["pages_per_s"] = round(len(records) / wall, 2)
res[m]["p_has_data"] = {r["id"]: float(x) for r, x in zip(records, p)}
print(m, json.dumps({k: v for k, v in res[m].items() if k not in ("sweep", "p_has_data")})[:600], flush=True)
os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True)
json.dump(res, open(args.out, "w"), indent=2)
del scorer
import torch
torch.cuda.empty_cache()
print("\n| system | ROC AUC | acc@best | null F1@best | data recall | thr |\n|---|---|---|---|---|---|")
for k, v in sorted(res.items(), key=lambda kv: -kv[1]["roc_auc"]):
b = v["at_best"]
print(f"| {k} | {v['roc_auc']} | {b['accuracy']} | {b['null_f1']} | {b['data_recall']} | {v['best_threshold']} |")
if __name__ == "__main__":
main()
|