debatra-ai / workers /stance_tracker.py
kumarsuresh45's picture
Add workers and all dependencies for AI service
2226762
Raw
History Blame Contribute Delete
3.78 kB
"""
Debatra — Worker 3: Stance Tracker Inference
=============================================
Base model: cross-encoder/nli-deberta-v3-small
Adapter: LoRA r=16 trained on ibm/claim_stance + ibm/argq_30k + climate_fever
Task: 3-class sequence classification
Labels: FAVOR=0 AGAINST=1 NONE=2
Test F1: 0.848
Input: (text: str, topic: str)
Internally formatted as: "[TOPIC] {topic} [ARGUMENT] {text}"
Output: {
"label": 0,
"stance": "FAVOR",
"confidence": 0.91,
"score": 9.0, # 0-10 (high = clear committed stance)
"uncertain": False,
}
"""
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from peft import PeftModel
LABEL2ID = {"FAVOR":0, "AGAINST":1, "NONE":2}
ID2LABEL = {v: k for k, v in LABEL2ID.items()}
BASE_MODEL = "cross-encoder/nli-deberta-v3-small"
MAX_LENGTH = 256
class StanceTrackerWorker:
def __init__(self, model_path: str, confidence_threshold: float = 0.60):
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 = AutoModelForSequenceClassification.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"Stance Tracker 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, topic: str = "") -> dict:
"""
Format input as "[TOPIC] {topic} [ARGUMENT] {text}"
matching the training format from data_prep_v3.py.
"""
if topic:
formatted = f"[TOPIC] {topic} [ARGUMENT] {text}"
else:
formatted = text
enc = self.tokenizer(
formatted,
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)
probs = torch.softmax(outputs.logits[0], dim=-1)
label_id = torch.argmax(probs).item()
confidence = probs[label_id].item()
stance = ID2LABEL[label_id]
uncertain = confidence < self.confidence_threshold
# Score: clear FAVOR or AGAINST = high, NONE = mid, uncertain = low
if stance == "NONE":
score = 4.0
else:
score = round(confidence * 10.0, 2) # max 10 for 100% conf
if uncertain:
score = round(score * 0.6, 2)
return {
"label": label_id,
"stance": stance if not uncertain else "NONE",
"confidence": round(confidence, 3),
"score": min(score, 10.0),
"uncertain": uncertain,
}