Views
No views yet
answerdotai/ModernBERT-base that scores a
single 8,192-token chunk of an academic paper for the presence of a funding
statement. Used as stage 1 of a three-stage funding-extraction cascade to
narrow a long PDF down to the most-likely chunk before running expensive
span-extraction and cleanup.P(this chunk contains a funding statement). Take top-K
chunks above a threshold (we use top-2 above 0.4).cometadata/funding-extraction-modernbert-base-spanhead
— picks the exact start/end token within the top chunk.cometadata/funding-cleaning-qwen3-4b-lora
— strips LaTeX markers and normalizes whitespace in the extracted span.ChunkClassifier module (included in
modeling.py):1import torch.nn as nn
2from transformers import AutoModel
3
4
5class ChunkClassifier(nn.Module):
6 """ModernBERT encoder + mean-pool + binary head."""
7
8 def __init__(self, base="answerdotai/ModernBERT-base"):
9 super().__init__()
10 self.encoder = AutoModel.from_pretrained(base)
11 self.head = nn.Linear(self.encoder.config.hidden_size, 1)
12
13 def forward(self, input_ids, attention_mask):
14 out = self.encoder(input_ids=input_ids, attention_mask=attention_mask)
15 # Mean pool over real (non-padding) tokens
16 mask = attention_mask.unsqueeze(-1).float()
17 pooled = (out.last_hidden_state * mask).sum(1) / mask.sum(1).clamp(min=1)
18 return self.head(pooled).squeeze(-1) # one logit per chunk1import torch
2from huggingface_hub import hf_hub_download
3from transformers import AutoTokenizer
4from modeling import ChunkClassifier # bundled in this repo
5
6REPO = "cometadata/funding-chunk-classifier-modernbert-base"
7device = "cuda"
8
9tokenizer = AutoTokenizer.from_pretrained(REPO)
10model = ChunkClassifier("answerdotai/ModernBERT-base").to(device)
11state_dict = torch.load(
12 hf_hub_download(REPO, "pytorch_model.bin"),
13 map_location=device, weights_only=True,
14)
15model.load_state_dict(state_dict)
16model.eval()
17
18# For a long paper, slide an 8192-token window with stride 4096.
19def chunks_of(text, max_tok=8192, stride=4096):
20 enc = tokenizer(text, add_special_tokens=False, truncation=False)
21 ids = enc["input_ids"]
22 if len(ids) <= max_tok:
23 yield ids, 0, len(ids)
24 return
25 for st in range(0, len(ids), stride):
26 en = min(st + max_tok, len(ids))
27 yield ids[st:en], st, en
28 if en == len(ids):
29 break
30
31probs = []
32for chunk_ids, st, en in chunks_of(paper_text):
33 ids_t = torch.tensor(chunk_ids).unsqueeze(0).to(device)
34 attn = torch.ones_like(ids_t)
35 with torch.no_grad():
36 with torch.amp.autocast("cuda", dtype=torch.bfloat16):
37 logit = model(ids_t, attn).float()
38 probs.append((torch.sigmoid(logit).item(), st, en))
39
40# Top-K chunks above threshold
41top_k = sorted(probs, key=lambda p: -p[0])[:2]
42top_k = [p for p in top_k if p[0] >= 0.4]
43# `top_k` is the list to hand off to the span-head model.cometadata/arxiv-pdf-only-works-funding-statement-extraction-train-test.vlm_markdown with the ModernBERT tokenizer.1 iff the gold funding statement (located via
verbatim substring or rapidfuzz.partial_ratio_alignment ≥ 0.7) overlaps
the chunk's character range by more than half its length, else 0.pos_weight = n_examples / n_positives to
counteract the class imbalance:1loss_fn = nn.BCEWithLogitsLoss(pos_weight=torch.tensor(n_examples / n_positives))
2loss = loss_fn(logits, labels)answerdotai/ModernBERT-base (149M, 8,192-token context)pytorch_model.bin is the epoch-2 (final) state dictcometadata/arxiv-pdf-only-works-funding-statement-extraction-train-test,
treated as a per-document binary task (does the doc have any funding
statement?): we score each candidate chunk and use the max probability as
the document-level prediction. Threshold = 0.5.| Metric | Precision | Recall | F1 | F0.5 |
|---|---|---|---|---|
| Doc-level funding detection | 0.9831 | 0.9537 | 0.9682 | 0.9771 |
no_answer head to suppress empty chunks.cometadata/arxiv-pdf-only-works-funding-statement-extraction-train-test
dataset by Comet.