Spaces:
Sleeping
Sleeping
Nouns proposal calibration tool
Browse files- README.md +36 -7
- app.py +206 -0
- requirements.txt +5 -0
README.md
CHANGED
|
@@ -1,15 +1,44 @@
|
|
| 1 |
---
|
| 2 |
title: Nouns Proposal Check
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
| 12 |
-
short_description: Check the quality of your Nouns Proposal
|
| 13 |
---
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Nouns Proposal Check
|
| 3 |
+
emoji: π€
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 4.44.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# Nouns Proposal Check
|
| 14 |
+
|
| 15 |
+
A calibration tool for Nouns DAO proposers. It does **not** predict whether your
|
| 16 |
+
proposal will pass β the underlying model isn't accurate enough for that, and
|
| 17 |
+
saying so is the point.
|
| 18 |
+
|
| 19 |
+
It shows you three things:
|
| 20 |
+
|
| 21 |
+
1. **The current base rate.** After the BreakEven bloc began voting down spend
|
| 22 |
+
(~proposal #786), the pass rate fell from ~56% to ~28%. Most proposers don't
|
| 23 |
+
know this number. It's the single most useful fact here.
|
| 24 |
+
2. **Where your proposal ranks** against past proposals in the current regime.
|
| 25 |
+
3. **The most similar past proposals** and what happened to them β go read them.
|
| 26 |
+
|
| 27 |
+
## The finding behind it
|
| 28 |
+
|
| 29 |
+
A DistilBERT model fine-tuned on 982 Nouns proposals reaches AUC 0.65 β real but
|
| 30 |
+
weak ranking signal. That signal *survives* the BreakEven regime change: the model
|
| 31 |
+
still ranks proposals about as well as before. What broke is calibration. Pass
|
| 32 |
+
probability collapsed, and a model trained on the old regime is systematically
|
| 33 |
+
overconfident about the new one.
|
| 34 |
+
|
| 35 |
+
**BreakEven didn't change what makes a proposal good. It changed how good a
|
| 36 |
+
proposal has to be.**
|
| 37 |
+
|
| 38 |
+
## Verify it
|
| 39 |
+
|
| 40 |
+
- Model: https://huggingface.co/gramajo/nouns-proposal-predictor
|
| 41 |
+
- Dataset: https://huggingface.co/datasets/gramajo/nouns-proposals
|
| 42 |
+
- Exact split IDs + raw metrics: `splits.json` / `results.json` in the model repo
|
| 43 |
+
|
| 44 |
+
Trust but verify. Open a discussion if you find a flaw.
|
app.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Nouns Proposal Check β a calibration tool, not an oracle.
|
| 3 |
+
|
| 4 |
+
Deliberately does NOT output a pass/fail verdict. The underlying model has an
|
| 5 |
+
AUC of ~0.65: real ranking signal, nowhere near enough to tell an individual
|
| 6 |
+
person their proposal will fail. So instead it reports:
|
| 7 |
+
|
| 8 |
+
1. the current base rate (what you're actually up against)
|
| 9 |
+
2. where the proposal ranks against past proposals (percentile β the thing
|
| 10 |
+
AUC actually supports)
|
| 11 |
+
3. the most similar past proposals and what happened to them (verifiable,
|
| 12 |
+
actionable, and requires no faith in the model's calibration)
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import json
|
| 16 |
+
import gradio as gr
|
| 17 |
+
import numpy as np
|
| 18 |
+
import torch
|
| 19 |
+
import torch.nn.functional as F
|
| 20 |
+
from datasets import load_dataset
|
| 21 |
+
from transformers import AutoTokenizer, AutoModelForSequenceClassification
|
| 22 |
+
|
| 23 |
+
MODEL_ID = "gramajo/nouns-proposal-predictor"
|
| 24 |
+
DATASET_ID = "gramajo/nouns-proposals"
|
| 25 |
+
BREAK_ID = 786 # BreakEven bloc regime change
|
| 26 |
+
MAX_LENGTH = 512
|
| 27 |
+
AUC = 0.65 # measured, run A (stratified random split)
|
| 28 |
+
ACC, BASELINE = 0.589, 0.502
|
| 29 |
+
|
| 30 |
+
print("Loading model...")
|
| 31 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
|
| 32 |
+
model = AutoModelForSequenceClassification.from_pretrained(MODEL_ID)
|
| 33 |
+
model.eval()
|
| 34 |
+
|
| 35 |
+
print("Loading corpus...")
|
| 36 |
+
ds = load_dataset(DATASET_ID)
|
| 37 |
+
CORPUS = sorted(
|
| 38 |
+
[dict(r) for r in list(ds["train"]) + list(ds["test"])],
|
| 39 |
+
key=lambda r: int(r["id"]),
|
| 40 |
+
)
|
| 41 |
+
for r in CORPUS:
|
| 42 |
+
r["passed"] = int(r["passed"])
|
| 43 |
+
|
| 44 |
+
POST = [r for r in CORPUS if int(r["id"]) >= BREAK_ID]
|
| 45 |
+
CURRENT_BASE_RATE = sum(r["passed"] for r in POST) / len(POST)
|
| 46 |
+
HISTORIC_BASE_RATE = sum(r["passed"] for r in CORPUS if int(r["id"]) < BREAK_ID) / max(
|
| 47 |
+
1, len([r for r in CORPUS if int(r["id"]) < BREAK_ID])
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _encode(texts, batch=16):
|
| 52 |
+
"""Return (pass_prob, CLS embedding) for each text."""
|
| 53 |
+
probs, embs = [], []
|
| 54 |
+
for i in range(0, len(texts), batch):
|
| 55 |
+
chunk = texts[i : i + batch]
|
| 56 |
+
enc = tokenizer(
|
| 57 |
+
chunk, padding=True, truncation=True,
|
| 58 |
+
max_length=MAX_LENGTH, return_tensors="pt",
|
| 59 |
+
)
|
| 60 |
+
with torch.no_grad():
|
| 61 |
+
out = model(**enc, output_hidden_states=True)
|
| 62 |
+
probs.extend(F.softmax(out.logits, dim=-1)[:, 1].tolist())
|
| 63 |
+
# last hidden layer, [CLS] token
|
| 64 |
+
embs.append(out.hidden_states[-1][:, 0, :].cpu().numpy())
|
| 65 |
+
return np.array(probs), np.vstack(embs)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
print("Scoring corpus (one-time, ~1-2 min on CPU)...")
|
| 69 |
+
_texts = [(r["title"] + " " + (r.get("description") or ""))[:2000] for r in CORPUS]
|
| 70 |
+
CORPUS_PROBS, CORPUS_EMBS = _encode(_texts)
|
| 71 |
+
_norms = np.linalg.norm(CORPUS_EMBS, axis=1, keepdims=True)
|
| 72 |
+
CORPUS_EMBS_N = CORPUS_EMBS / np.clip(_norms, 1e-9, None)
|
| 73 |
+
POST_PROBS = np.array([CORPUS_PROBS[i] for i, r in enumerate(CORPUS) if int(r["id"]) >= BREAK_ID])
|
| 74 |
+
print("Ready.")
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def analyze(title, description):
|
| 78 |
+
if not title.strip() and not description.strip():
|
| 79 |
+
return "Enter a proposal title and description to see how it compares."
|
| 80 |
+
|
| 81 |
+
text = (title + " " + description)[:2000]
|
| 82 |
+
prob, emb = _encode([text])
|
| 83 |
+
prob = float(prob[0])
|
| 84 |
+
emb_n = emb[0] / max(np.linalg.norm(emb[0]), 1e-9)
|
| 85 |
+
|
| 86 |
+
# Percentile against the CURRENT regime -- ranking is what AUC supports.
|
| 87 |
+
pct = float((POST_PROBS < prob).mean() * 100)
|
| 88 |
+
|
| 89 |
+
# Nearest neighbours by cosine similarity.
|
| 90 |
+
sims = CORPUS_EMBS_N @ emb_n
|
| 91 |
+
top = np.argsort(-sims)[:5]
|
| 92 |
+
|
| 93 |
+
md = []
|
| 94 |
+
md.append("## What you're up against\n")
|
| 95 |
+
md.append(
|
| 96 |
+
f"In the current regime (proposals #{BREAK_ID}+, after the BreakEven bloc "
|
| 97 |
+
f"began voting down spend), **{CURRENT_BASE_RATE:.0%} of proposals pass.** "
|
| 98 |
+
f"Before that, it was {HISTORIC_BASE_RATE:.0%}.\n"
|
| 99 |
+
)
|
| 100 |
+
md.append(
|
| 101 |
+
"The bar moved. What makes a proposal *good* didn't change much β how good "
|
| 102 |
+
"it has to be did.\n"
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
md.append("\n## Where yours ranks\n")
|
| 106 |
+
md.append(
|
| 107 |
+
f"Your proposal scores higher than **{pct:.0f}%** of proposals submitted in "
|
| 108 |
+
f"the current regime.\n"
|
| 109 |
+
)
|
| 110 |
+
if pct >= 75:
|
| 111 |
+
md.append(
|
| 112 |
+
"\nThat's in the upper quartile of what the model has seen. It is **not** "
|
| 113 |
+
"a prediction that you'll pass β most proposals in this regime fail "
|
| 114 |
+
"regardless of where they rank.\n"
|
| 115 |
+
)
|
| 116 |
+
elif pct >= 40:
|
| 117 |
+
md.append(
|
| 118 |
+
"\nMiddle of the pack. Worth looking hard at the similar proposals below, "
|
| 119 |
+
"especially the ones that failed.\n"
|
| 120 |
+
)
|
| 121 |
+
else:
|
| 122 |
+
md.append(
|
| 123 |
+
"\nLower end of the distribution. That doesn't mean it *will* fail β but "
|
| 124 |
+
"it's worth understanding why similar proposals didn't land.\n"
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
md.append("\n## Most similar past proposals\n")
|
| 128 |
+
md.append("Read these. They're more informative than any score this tool prints.\n\n")
|
| 129 |
+
md.append("| outcome | similarity | proposal |\n|---|---|---|\n")
|
| 130 |
+
for i in top:
|
| 131 |
+
r = CORPUS[i]
|
| 132 |
+
outcome = "β
passed" if r["passed"] else "β failed"
|
| 133 |
+
era = "post-BreakEven" if int(r["id"]) >= BREAK_ID else "pre-BreakEven"
|
| 134 |
+
t = r["title"][:70]
|
| 135 |
+
md.append(f"| {outcome} | {sims[i]:.2f} | **#{r['id']}** {t} <br/><sub>{era}</sub> |\n")
|
| 136 |
+
|
| 137 |
+
md.append(
|
| 138 |
+
f"\n---\n\n*Model AUC {AUC:.2f} β it ranks proposals better than chance, but it "
|
| 139 |
+
f"is **not** accurate enough to tell you whether your proposal will pass. "
|
| 140 |
+
f"Treat the ranking as a weak signal and the similar-proposal list as the "
|
| 141 |
+
f"actual output.*"
|
| 142 |
+
)
|
| 143 |
+
return "".join(md)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
LIMITATIONS = f"""
|
| 147 |
+
### How this works, and where it fails
|
| 148 |
+
|
| 149 |
+
This is a fine-tuned DistilBERT that read {len(CORPUS)} past Nouns DAO proposals
|
| 150 |
+
(title + description only) and learned to rank them.
|
| 151 |
+
|
| 152 |
+
**Measured performance**, on a stratified random split:
|
| 153 |
+
|
| 154 |
+
| metric | value | meaning |
|
| 155 |
+
|---|---|---|
|
| 156 |
+
| AUC | {AUC:.2f} | Given a passing and a failing proposal, it ranks them correctly ~65% of the time. Real signal, but weak. |
|
| 157 |
+
| accuracy | {ACC:.1%} | Against a majority-class baseline of {BASELINE:.1%}. It beats "always guess," but not by much. |
|
| 158 |
+
|
| 159 |
+
**Why there is no pass/fail verdict here.** At AUC 0.65, a confident verdict would
|
| 160 |
+
be wrong roughly a third of the time. Telling someone who spent three weeks on a
|
| 161 |
+
proposal that it "will fail" β and being wrong that often β would discourage good
|
| 162 |
+
proposals and teach people to write toward the model instead of toward the DAO.
|
| 163 |
+
|
| 164 |
+
**What the model cannot see:** who is proposing, whether they've shipped before,
|
| 165 |
+
how much ETH they're asking for, what the treasury looked like, what happened in
|
| 166 |
+
Discord beforehand, who showed up to vote. Those almost certainly matter more than
|
| 167 |
+
the prose. This model reads only the text.
|
| 168 |
+
|
| 169 |
+
**Goodhart warning.** If you optimize your proposal to score well here, you are
|
| 170 |
+
optimizing for *resemblance to proposals that passed*, not for quality. That is a
|
| 171 |
+
good way to produce a monoculture. Use the similar-proposals list to learn; don't
|
| 172 |
+
tune your wording against the percentile.
|
| 173 |
+
|
| 174 |
+
**Everything is open. Verify it:**
|
| 175 |
+
- Model: [{MODEL_ID}](https://huggingface.co/{MODEL_ID})
|
| 176 |
+
- Dataset: [{DATASET_ID}](https://huggingface.co/datasets/{DATASET_ID})
|
| 177 |
+
- Exact split IDs and raw metrics are in the model repo (`splits.json`, `results.json`)
|
| 178 |
+
|
| 179 |
+
Found a flaw? Open a discussion on the model repo. That's the point.
|
| 180 |
+
"""
|
| 181 |
+
|
| 182 |
+
with gr.Blocks(title="Nouns Proposal Check") as demo:
|
| 183 |
+
gr.Markdown(
|
| 184 |
+
"# Nouns Proposal Check\n"
|
| 185 |
+
"**Not a verdict machine.** This won't tell you whether your proposal will pass β "
|
| 186 |
+
"the model isn't good enough for that, and I'd rather say so than pretend otherwise.\n\n"
|
| 187 |
+
"What it *will* do: show you the current pass rate, where your proposal ranks "
|
| 188 |
+
"against past ones, and the most similar proposals that came before β so you can "
|
| 189 |
+
"go read what worked and what didn't."
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
with gr.Row():
|
| 193 |
+
with gr.Column(scale=1):
|
| 194 |
+
title = gr.Textbox(label="Proposal title", placeholder="Nouns Γ ...", lines=1)
|
| 195 |
+
desc = gr.Textbox(label="Proposal description", lines=14,
|
| 196 |
+
placeholder="Paste the full proposal body here...")
|
| 197 |
+
btn = gr.Button("Compare against past proposals", variant="primary")
|
| 198 |
+
with gr.Column(scale=1):
|
| 199 |
+
out = gr.Markdown("Enter a proposal to see how it compares.")
|
| 200 |
+
|
| 201 |
+
btn.click(analyze, inputs=[title, desc], outputs=out)
|
| 202 |
+
|
| 203 |
+
with gr.Accordion("Limitations, metrics, and how to verify this", open=False):
|
| 204 |
+
gr.Markdown(LIMITATIONS)
|
| 205 |
+
|
| 206 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.44.0
|
| 2 |
+
transformers>=4.44.0
|
| 3 |
+
torch>=2.2.0
|
| 4 |
+
datasets>=2.20.0
|
| 5 |
+
numpy>=1.26.0
|