Text Classification
PEFT
Safetensors
English
decision-model
zero-shot-classification
calibration
lora
Instructions to use thinkingdbx/DECISIONx-1.5B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use thinkingdbx/DECISIONx-1.5B with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
Download decisionx/model.py from thinkingdbx/DECISIONx-1.5B: direct link, hf CLI and curl.
- Browser
- Download file 9.6 kB
-
https://huggingface.co/thinkingdbx/DECISIONx-1.5B/resolve/main/decisionx/model.py
- Command line
-
hf download hf://thinkingdbx/DECISIONx-1.5B/decisionx/model.py
-
curl -L -o model.py https://huggingface.co/thinkingdbx/DECISIONx-1.5B/resolve/main/decisionx/model.py
9.6 kB
| """Backbone + one scoring head. No lm_head, no generation. | |
| A decision is one forward pass: render state, question and candidates; take the | |
| backbone's final hidden state at each candidate's last token; a small MLP turns | |
| each into a logit; softmax over the candidates. The backbone's pretraining is | |
| where the world knowledge comes from, LoRA adapts it, and the head is the only | |
| part that is new. | |
| Calibration is a temperature per question type, fitted on held-out data after | |
| training (see evaluate.py) and stored beside the weights. It changes no | |
| answer, only how sure the model says it is. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import math | |
| import os | |
| import torch | |
| from torch import nn | |
| from .render import Renderer | |
| from .schema import Decision | |
| class Head(nn.Module): | |
| def __init__(self, hidden: int): | |
| super().__init__() | |
| mid = max(64, hidden // 4) | |
| self.net = nn.Sequential(nn.LayerNorm(hidden), nn.Linear(hidden, mid), | |
| nn.GELU(), nn.Linear(mid, 1)) | |
| def forward(self, h: torch.Tensor) -> torch.Tensor: | |
| return self.net(h).squeeze(-1) | |
| def lora_targets(model) -> list[str]: | |
| """Attention and MLP projections, by whatever names this family uses.""" | |
| names = {n.rsplit(".", 1)[-1] for n, m in model.named_modules() | |
| if isinstance(m, nn.Linear) or type(m).__name__ == "Conv1D"} | |
| llama = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"] | |
| got = [n for n in llama if n in names] | |
| if got: | |
| return got | |
| return [n for n in ("c_attn", "c_proj", "c_fc") if n in names] # gpt2 | |
| class DecisionModel(nn.Module): | |
| def __init__(self, backbone, tok, config: dict): | |
| super().__init__() | |
| self.backbone = backbone | |
| self.tok = tok | |
| self.config = config | |
| hidden = backbone.config.hidden_size | |
| self.head = Head(hidden) | |
| self.renderer = Renderer(tok, config.get("max_state_tokens", 384), | |
| config.get("max_option_tokens", 32)) | |
| self.temperature = {t: 1.0 for t in ("choice", "score", "null")} | |
| # ---- construction ---------------------------------------------------- # | |
| def create(cls, base: str, lora_rank: int = 16, lora_alpha: int = 32, | |
| dtype: torch.dtype = torch.float32, gradient_checkpointing: bool = False, | |
| **cfg) -> "DecisionModel": | |
| from peft import LoraConfig, get_peft_model | |
| from transformers import AutoModel, AutoTokenizer | |
| tok = AutoTokenizer.from_pretrained(base) | |
| if tok.pad_token_id is None: | |
| tok.pad_token = tok.eos_token | |
| bb = AutoModel.from_pretrained(base, dtype=dtype) | |
| if gradient_checkpointing: | |
| bb.gradient_checkpointing_enable( | |
| gradient_checkpointing_kwargs={"use_reentrant": False}) | |
| bb.enable_input_require_grads() | |
| bb.config.use_cache = False | |
| targets = lora_targets(bb) | |
| bb = get_peft_model(bb, LoraConfig( | |
| r=lora_rank, lora_alpha=lora_alpha, lora_dropout=0.05, | |
| target_modules=targets, bias="none")) | |
| config = {"base": base, "lora_rank": lora_rank, "lora_alpha": lora_alpha, | |
| "lora_targets": targets, **cfg} | |
| return cls(bb, tok, config) | |
| def save(self, path: str) -> None: | |
| os.makedirs(path, exist_ok=True) | |
| self.backbone.save_pretrained(os.path.join(path, "adapter")) | |
| self.tok.save_pretrained(os.path.join(path, "tokenizer")) | |
| # safetensors, not a pickle: loading a .pt can run code, and a model | |
| # meant to be downloaded should not ask anyone to trust that. | |
| from safetensors.torch import save_file | |
| save_file({k: v.detach().cpu().contiguous() for k, v in self.head.state_dict().items()}, | |
| os.path.join(path, "head.safetensors")) | |
| with open(os.path.join(path, "decisionx.json"), "w") as fh: | |
| json.dump({"config": self.config, "temperature": self.temperature}, | |
| fh, indent=2) | |
| def load(cls, path: str, dtype: torch.dtype = torch.float32, | |
| device: str | None = None) -> "DecisionModel": | |
| from peft import PeftModel | |
| from transformers import AutoModel, AutoTokenizer | |
| meta = json.load(open(os.path.join(path, "decisionx.json"))) | |
| cfg = meta["config"] | |
| tok = AutoTokenizer.from_pretrained(os.path.join(path, "tokenizer")) | |
| bb = AutoModel.from_pretrained(cfg["base"], dtype=dtype) | |
| bb = PeftModel.from_pretrained(bb, os.path.join(path, "adapter")) | |
| m = cls(bb, tok, cfg) | |
| st = os.path.join(path, "head.safetensors") | |
| if os.path.exists(st): | |
| from safetensors.torch import load_file | |
| m.head.load_state_dict(load_file(st)) | |
| else: # saved before v2's export | |
| m.head.load_state_dict(torch.load(os.path.join(path, "head.pt"), | |
| map_location="cpu", weights_only=True)) | |
| m.temperature.update(meta.get("temperature") or {}) | |
| m.eval() | |
| if device: | |
| m.to(device) | |
| return m | |
| # ---- the forward pass ------------------------------------------------ # | |
| def forward(self, input_ids, attention_mask, option_pos) -> torch.Tensor: | |
| """(B, K) logits; padded candidate slots (option_pos == -1) are -inf.""" | |
| out = self.backbone(input_ids=input_ids, attention_mask=attention_mask) | |
| h = out.last_hidden_state # (B, L, H) | |
| idx = option_pos.clamp(min=0) | |
| picked = torch.gather(h, 1, idx.unsqueeze(-1).expand(-1, -1, h.size(-1))) | |
| logits = self.head(picked.float()) # (B, K) | |
| return logits.masked_fill(option_pos < 0, float("-inf")) | |
| # ---- the API ----------------------------------------------------------- # | |
| def decide(self, type: str, question: str, state: str = "", | |
| options: list[str] | None = None, scale: dict | None = None, | |
| calibration=None) -> dict: | |
| """One decision. Returns the answer, a probability per candidate, and | |
| a confidence; the shape of the answer depends on the type. Pass the | |
| result of `calibrate()` to apply a per-task calibration.""" | |
| d = Decision(type=type, state=state, question=question, | |
| options=list(options or []), scale=scale).validate() | |
| return self.decide_many([d], calibration)[0] | |
| def raw_logits(self, decisions: list[Decision]) -> list[torch.Tensor]: | |
| """Uncalibrated logits over each decision's candidates.""" | |
| from .data.collate import collate | |
| dev = next(self.parameters()).device | |
| batch = collate([self.renderer.render(d) for d in decisions], | |
| self.tok.pad_token_id) | |
| logits = self(batch["input_ids"].to(dev), batch["attention_mask"].to(dev), | |
| batch["option_pos"].to(dev)).float().cpu() | |
| return [logits[i, :len(d.candidates())] for i, d in enumerate(decisions)] | |
| def decide_many(self, decisions: list[Decision], calibration=None) -> list[dict]: | |
| return [self._answer(d, lg, calibration) | |
| for d, lg in zip(decisions, self.raw_logits(decisions))] | |
| def calibrate(self, examples: list[Decision], strength: float | None = None, | |
| batch_size: int = 16): | |
| """Fit a calibration for one task from labelled examples (20-50 is | |
| plenty). Every example must be the same type; for a choice task they | |
| should share an option set, in any order.""" | |
| from .calibration import fit, fit_auto | |
| if not examples or any(d.target() is None for d in examples): | |
| raise ValueError("calibration needs labelled examples") | |
| types = {d.type for d in examples} | |
| if len(types) != 1: | |
| raise ValueError(f"one task at a time; got types {sorted(types)}") | |
| rows = [] | |
| for i in range(0, len(examples), batch_size): | |
| chunk = examples[i:i + batch_size] | |
| for d, lg in zip(chunk, self.raw_logits(chunk)): | |
| rows.append((lg.tolist(), d.candidates(), d.target())) | |
| t = types.pop() | |
| T0 = self.temperature.get(t, 1.0) | |
| if strength is None: # chosen by cross-validation on these examples | |
| return fit_auto(rows, t, T0, examples[0].question) | |
| return fit(rows, t, T0, strength, examples[0].question) | |
| def _answer(self, d: Decision, logits: torch.Tensor, calibration=None) -> dict: | |
| if calibration is not None: | |
| p = calibration.apply(d, logits) | |
| else: | |
| p = torch.softmax(logits / self.temperature.get(d.type, 1.0), -1) | |
| probs = [round(float(x), 6) for x in p] | |
| best = int(p.argmax()) | |
| if d.type == "null": | |
| return {"type": "null", "probability": probs[0], "answer": probs[0] >= 0.5} | |
| if d.type == "score": | |
| vals = d.scale_values() | |
| mean = sum(v * q for v, q in zip(vals, probs)) | |
| sd = math.sqrt(sum(q * (v - mean) ** 2 for v, q in zip(vals, probs))) | |
| return {"type": "score", "score": round(mean, 4), "mode": vals[best], | |
| "spread": round(sd, 4), "distribution": dict(zip(vals, probs))} | |
| return {"type": "choice", "answer": d.options[best], "index": best, | |
| "confidence": probs[best], "probabilities": dict(zip(d.options, probs))} | |