Zero-Shot Classification
Transformers
Safetensors
English
qwen3_5_text
text-generation
scientific-papers
classification
field-of-science
taxonomy
research-classification
science-of-science
Instructions to use maximerivest/paper-classifier-0.8B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use maximerivest/paper-classifier-0.8B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("zero-shot-classification", model="maximerivest/paper-classifier-0.8B")# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("maximerivest/paper-classifier-0.8B") model = AutoModelForCausalLM.from_pretrained("maximerivest/paper-classifier-0.8B", device_map="auto") - Notebooks
- Google Colab
- Kaggle
File size: 8,936 Bytes
0323f96 | 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 | """paper-classifier: give a scientific paper and ANY list of options (a classification at any level, your own labels,
or a single topic), get a probability for every option. One forward pass, no generated text.
from paper_classifier import PaperClassifier, paper_text
clf = PaperClassifier("maximerivest/paper-classifier-2B") # or a local folder
paper = paper_text("Habitat fragmentation reduces bird diversity", "We surveyed birds in 40 forest fragments...")
clf.classify(paper, ["Ecology", "Ornithology", "Particle physics"], scheme="My fields") # {option: probability}
clf.is_on_topic(paper, "habitat fragmentation") # P(yes)
The prompt below is exactly the one the model was trained on. Requires transformers >= 5.3 and torch.
Apache-2.0.
"""
from __future__ import annotations
import json
import random
from pathlib import Path
import torch
SYSTEM = ("You classify scientific papers. You get a paper (its title, and its abstract when available), then a "
"question. For a list of options, answer with the code of the option that best describes the research "
"content of the paper, following the classification's own meaning of each option. For a single topic, answer "
"yes if the topic is a central subject of the paper, else no. On topic: the paper studies the topic itself as "
"a main subject: the phenomenon, its causes, consequences, mechanisms, measurement, methods, or how it is "
"managed, in any system or organism. Not on topic: the topic is absent or only mentioned in passing; or the "
"words mean something else in this paper; or the paper studies a neighbouring subject without studying the "
"topic itself.")
NONE = "None of these (outside the options listed)"
PAPER_TOKENS = 700 # the paper part was cut to at most 700 tokens in training
def paper_text(title: str, abstract: str | None = None, venue: str | None = None, year: int | str | None = None) -> str:
"""The paper as the model saw it in training: Title / Venue / Year / Abstract, or "(no abstract)"."""
s = f"Title: {title.strip()}"
if venue:
s += f"\nVenue: {venue.strip()}"
if year:
s += f"\nYear: {year}"
abstract = " ".join((abstract or "").split())
return s + (f"\nAbstract: {abstract[:1500]}" if abstract else "\n(no abstract)")
def _option(o) -> dict:
return {"name": o} if isinstance(o, str) else dict(o)
def option_line(code: str, o: dict, describe: bool = True) -> str:
s = f"{code}. {o['name']}"
extra = [x for x in ((o.get("about") if describe else None), (f"in {o['parent']}" if o.get("parent") else None)) if x]
return s + (" — " + "; ".join(extra) if extra else "")
def pick_prompt(paper: str, scheme: str, question: str, options: list[dict], codes: list[str], describe: bool = True) -> str:
lines = "\n".join(option_line(c, o, describe) for c, o in zip(codes, options))
return (f"{paper}\n\n## Classification: {scheme}\n{question}\nOptions:\n{lines}\n"
"Answer with the option code only.")
def yesno_prompt(paper: str, topic: str, definition: str | None = None) -> str:
d = f" (what it covers: {definition})" if definition else ""
return f"{paper}\n\n## Topic: {topic}{d}\nIs this topic a central subject of the paper? Answer yes or no."
class PaperClassifier:
def __init__(self, model: str = "maximerivest/paper-classifier-2B", device: str | None = None,
dtype=torch.bfloat16):
from huggingface_hub import hf_hub_download
from transformers import AutoModelForCausalLM, AutoTokenizer
self.tok = AutoTokenizer.from_pretrained(model)
self.tok.padding_side = "left"
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
self.model = AutoModelForCausalLM.from_pretrained(model, dtype=dtype).to(self.device).eval()
local = Path(model) / "classifier.json"
cfg = json.loads((local if local.exists() else Path(hf_hub_download(model, "classifier.json"))).read_text())
self.codes, self.code_ids = cfg["codes"], cfg["code_ids"]
self.yes, self.no = cfg["yes"], cfg["no"]
# ------------------------------------------------------------------ low level
def _ids(self, user: str) -> list[int]:
paper, sep, rest = user.partition("\n\n## ")
ids = self.tok(paper, add_special_tokens=False)["input_ids"]
if len(ids) > PAPER_TOKENS: # cut the paper, never the question
user = self.tok.decode(ids[:PAPER_TOKENS]) + sep + rest
text = self.tok.apply_chat_template([{"role": "system", "content": SYSTEM}, {"role": "user", "content": user}],
tokenize=False, add_generation_prompt=True, enable_thinking=False)
return self.tok(text, add_special_tokens=False)["input_ids"]
@torch.no_grad()
def scores(self, users: list[str], answer_ids: list[list[int]], batch_size: int = 8) -> list[list[float]]:
"""For each prompt, the softmax over ITS answer tokens of the next-token logits (no generation)."""
out = []
for i in range(0, len(users), batch_size):
enc = self.tok.pad({"input_ids": [self._ids(u) for u in users[i:i + batch_size]]}, return_tensors="pt")
logits = self.model(**enc.to(self.device), logits_to_keep=1).logits[:, -1, :].float()
for row, ans in zip(logits, answer_ids[i:i + batch_size]):
out.append(torch.softmax(row[ans], -1).tolist())
return out
# ------------------------------------------------------------------ tasks
def classify(self, paper: str, options: list, scheme: str = "Classification of research",
question: str = "Which option best describes the research content of this paper?",
describe: bool = True, orders: int = 1, none_option: bool = False, seed: int = 0) -> dict:
"""{option name: probability}. options: names, or dicts {"name", "about" (optional), "parent" (optional)}.
orders > 1: also ask with the options shuffled and average (steadier picks, `orders` x the cost)."""
opts = [_option(o) for o in options] + ([{"name": NONE}] if none_option else [])
if len(opts) > len(self.codes):
raise ValueError(f"at most {len(self.codes)} options in one question: ask level by level (classify_tree)")
rng = random.Random(seed)
perms = [list(range(len(opts)))] + [rng.sample(range(len(opts)), len(opts)) for _ in range(orders - 1)]
codes = self.codes[:len(opts)]
users = [pick_prompt(paper, scheme, question, [opts[j] for j in p], codes, describe) for p in perms]
res = self.scores(users, [self.code_ids[:len(opts)]] * len(users))
total = [0.0] * len(opts)
for p, r in zip(perms, res):
for pos, j in enumerate(p):
total[j] += r[pos] / len(perms)
return dict(sorted(((o["name"], q) for o, q in zip(opts, total)), key=lambda x: -x[1]))
def is_on_topic(self, paper: str, topic: str, definition: str | None = None) -> float:
"""P(the topic is a central subject of the paper)."""
return self.scores([yesno_prompt(paper, topic, definition)], [[self.yes, self.no]])[0][0]
def classify_tree(self, paper: str, tree: dict[str, list], scheme: str = "Classification of research",
levels: tuple[str, str] = ("field", "subfield")) -> dict:
"""Two levels: P(child) = P(parent) x P(child | parent). tree = {parent: [children]}.
For lists that fit in one question (<= 562 options), one flat classify() is usually better."""
top = self.classify(paper, list(tree), scheme, f"Which {levels[0]} best describes the research content of this paper?")
out = {}
for parent, children in tree.items():
sub = self.classify(paper, children, f"{scheme} — within: {parent}",
f"Which {levels[1]} best describes the research content of this paper?")
for c, q in sub.items():
out[c] = top[parent] * q
return dict(sorted(out.items(), key=lambda x: -x[1]))
if __name__ == "__main__":
import sys
clf = PaperClassifier(sys.argv[1] if len(sys.argv) > 1 else "maximerivest/paper-classifier-2B")
p = paper_text("Habitat fragmentation reduces bird diversity in tropical forests",
"We surveyed bird communities in 40 forest fragments of different sizes and isolation in the Atlantic "
"Forest of Brazil. Species richness declined with fragment area and increased with connectivity.")
print(clf.classify(p, ["Ecology", "Ornithology", "Particle physics", "Economics"], scheme="Fields of science"))
print(clf.is_on_topic(p, "habitat fragmentation"), clf.is_on_topic(p, "dark matter"))
|