Spaces:
Sleeping
Sleeping
| """ | |
| Debatra — Worker 1: ADU Parser Inference | |
| ========================================== | |
| Base model: microsoft/deberta-v3-base | |
| Adapter: LoRA r=16 trained on climate_fever + DebateSum | |
| Task: BIO token classification | |
| Labels: O=0 B-claim=1 I-claim=2 B-warrant=3 I-warrant=4 | |
| Test F1: 0.836 | |
| Input: raw text string | |
| Output: { | |
| "tokens": ["word1", "word2", ...], | |
| "labels": [0, 1, 2, ...], # BIO int per token | |
| "segments": [ | |
| {"type": "claim", "text": "..."}, | |
| {"type": "warrant", "text": "..."}, | |
| ], | |
| "structure": "claim → warrant", # human readable | |
| "completeness": 0.82, # 0-1 score | |
| "score": 8.2, # 0-10 for composite | |
| "uncertain": False, | |
| } | |
| """ | |
| import re | |
| import torch | |
| from transformers import AutoTokenizer, AutoModelForTokenClassification | |
| from peft import PeftModel | |
| LABEL2ID = {"O":0, "B-claim":1, "I-claim":2, "B-warrant":3, "I-warrant":4} | |
| ID2LABEL = {v: k for k, v in LABEL2ID.items()} | |
| BASE_MODEL = "microsoft/deberta-v3-base" | |
| MAX_LENGTH = 128 | |
| class ADUParserWorker: | |
| def __init__(self, model_path: str, confidence_threshold: float = 0.70): | |
| self.model_path = model_path | |
| self.confidence_threshold = confidence_threshold | |
| self._loaded = False | |
| self._load() | |
| def _load(self): | |
| try: | |
| try: | |
| self.tokenizer = AutoTokenizer.from_pretrained( | |
| self.model_path, | |
| use_fast=True, | |
| ) | |
| except Exception: | |
| # Some LoRA export folders omit config.json; fall back to base tokenizer. | |
| self.tokenizer = AutoTokenizer.from_pretrained( | |
| BASE_MODEL, | |
| use_fast=True, | |
| ) | |
| base = AutoModelForTokenClassification.from_pretrained( | |
| BASE_MODEL, | |
| num_labels=len(LABEL2ID), | |
| ignore_mismatched_sizes=True, | |
| ) | |
| self.model = PeftModel.from_pretrained(base, self.model_path) | |
| self.model.eval() | |
| if torch.cuda.is_available(): | |
| self.model = self.model.cuda() | |
| self._loaded = True | |
| except Exception as e: | |
| raise RuntimeError(f"ADU Parser failed to load from {self.model_path}: {e}") | |
| def status(self) -> str: | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| return f"loaded ({device})" if self._loaded else "not loaded" | |
| def predict(self, text: str) -> dict: | |
| """Run BIO inference on input text.""" | |
| tokens = text.split() | |
| if not tokens: | |
| return self._empty_result() | |
| enc = self.tokenizer( | |
| tokens, | |
| is_split_into_words=True, | |
| truncation=True, | |
| max_length=MAX_LENGTH, | |
| return_tensors="pt", | |
| ) | |
| device = next(self.model.parameters()).device | |
| enc = {k: v.to(device) for k, v in enc.items()} | |
| with torch.no_grad(): | |
| outputs = self.model(**enc) | |
| logits = outputs.logits[0] | |
| probs = torch.softmax(logits, dim=-1) | |
| preds = torch.argmax(probs, dim=-1) | |
| confs = probs.max(dim=-1).values | |
| # Align predictions back to word level | |
| word_ids = enc["input_ids"][0] if "input_ids" in enc else [] | |
| enc_word_ids = self.tokenizer( | |
| tokens, | |
| is_split_into_words=True, | |
| truncation=True, | |
| max_length=MAX_LENGTH, | |
| ).word_ids() | |
| word_labels = {} | |
| word_confs = {} | |
| for idx, word_id in enumerate(enc_word_ids): | |
| if word_id is None: | |
| continue | |
| if word_id not in word_labels: | |
| word_labels[word_id] = preds[idx].item() | |
| word_confs[word_id] = confs[idx].item() | |
| predicted_labels = [word_labels.get(i, 0) for i in range(len(tokens))] | |
| avg_conf = sum(word_confs.values()) / max(len(word_confs), 1) | |
| # Extract segments | |
| segments = self._extract_segments(tokens, predicted_labels) | |
| structure = self._describe_structure(segments) | |
| completeness= self._compute_completeness(segments) | |
| uncertain = avg_conf < self.confidence_threshold | |
| score = round(completeness * 10, 2) | |
| return { | |
| "tokens": tokens, | |
| "labels": predicted_labels, | |
| "segments": segments, | |
| "structure": structure, | |
| "completeness": completeness, | |
| "score": score, | |
| "confidence": round(avg_conf, 3), | |
| "uncertain": uncertain, | |
| } | |
| def _extract_segments(self, tokens, labels): | |
| segments = [] | |
| cur_type = None | |
| cur_words= [] | |
| for word, label_id in zip(tokens, labels): | |
| label = ID2LABEL[label_id] | |
| if label.startswith("B-"): | |
| if cur_type and cur_words: | |
| segments.append({"type": cur_type, "text": " ".join(cur_words)}) | |
| cur_type = label[2:] # "claim" or "warrant" | |
| cur_words = [word] | |
| elif label.startswith("I-") and cur_type: | |
| cur_words.append(word) | |
| else: # O | |
| if cur_type and cur_words: | |
| segments.append({"type": cur_type, "text": " ".join(cur_words)}) | |
| cur_type = None | |
| cur_words = [] | |
| if cur_type and cur_words: | |
| segments.append({"type": cur_type, "text": " ".join(cur_words)}) | |
| return segments | |
| def _describe_structure(self, segments): | |
| if not segments: | |
| return "no_structure" | |
| types = [s["type"] for s in segments] | |
| return " → ".join(types) | |
| def _compute_completeness(self, segments): | |
| """ | |
| Score: claim present = 0.4, warrant present = 0.4, both = 0.8+ | |
| Multiple warrants = bonus up to 1.0. | |
| """ | |
| has_claim = any(s["type"] == "claim" for s in segments) | |
| n_warrants = sum(1 for s in segments if s["type"] == "warrant") | |
| score = 0.0 | |
| if has_claim: score += 0.4 | |
| if n_warrants: score += min(0.4 + (n_warrants - 1) * 0.1, 0.6) | |
| return round(min(score, 1.0), 3) | |
| def _empty_result(self): | |
| return { | |
| "tokens":[], "labels":[], "segments":[], | |
| "structure":"no_structure", "completeness":0.0, | |
| "score":0.0, "confidence":0.0, "uncertain":True, | |
| } | |