dialectica / scripts /inference.py
Kattine
Polish: concept top-2 calibration, cross-cutting tag, uncertainty flag, overall depth bar
006a1d9
Raw
History Blame Contribute Delete
3.35 kB
"""Inference helpers for Dialectica."""
ORDERED_LABELS = ["Surface", "Mechanistic", "Critical"]
class CognitiveClassifier:
"""DistilBERT question classifier."""
def __init__(self, product_config):
"""Load model and tokenizer."""
import torch
from transformers import (
AutoModelForSequenceClassification,
AutoTokenizer,
)
self.cfg = product_config
self.torch = torch
self.tokenizer = AutoTokenizer.from_pretrained(self.cfg.classifier_dir)
self.model = AutoModelForSequenceClassification.from_pretrained(
self.cfg.classifier_dir
)
self.model.eval()
self.id2label = self.model.config.id2label or {
i: label for i, label in enumerate(ORDERED_LABELS)
}
def classify(self, question):
"""Predict level and confidence."""
inputs = self.tokenizer(
question,
truncation=True,
max_length=self.cfg.max_question_length,
return_tensors="pt",
)
with self.torch.no_grad():
logits = self.model(**inputs).logits
probs = self.torch.softmax(logits, dim=1)[0]
predicted_id = int(self.torch.argmax(probs))
return {
"level": self.id2label[predicted_id],
"confidence": float(probs[predicted_id]),
}
class ConceptMatcher:
"""Embedding-based concept matcher."""
def __init__(self, product_config):
"""Load embedding model."""
from sentence_transformers import SentenceTransformer
self.cfg = product_config
self.model = SentenceTransformer(self.cfg.embed_model)
self.concepts = []
self.concept_embeddings = None
def set_concepts(self, concepts):
"""Cache concept embeddings."""
self.concepts = concepts
if concepts:
self.concept_embeddings = self.model.encode(
concepts, convert_to_tensor=True
)
else:
self.concept_embeddings = None
def match(self, question):
"""Return up to two matching concepts."""
from sentence_transformers import util
if not self.concepts or self.concept_embeddings is None:
return []
query = self.model.encode(question, convert_to_tensor=True)
scores = util.cos_sim(query, self.concept_embeddings)[0]
ranked = sorted(
range(len(self.concepts)),
key=lambda i: float(scores[i]),
reverse=True,
)
threshold = self.cfg.concept_match_threshold
# Keep a second concept if it's close to top or strong enough.
runner_up_margin = 0.05
runner_up_absolute = 0.45
top_index = ranked[0]
top_score = float(scores[top_index])
if top_score < threshold:
return []
matched = [self.concepts[top_index]]
if len(ranked) > 1:
second_index = ranked[1]
second_score = float(scores[second_index])
close_to_top = top_score - second_score <= runner_up_margin
strong_alone = second_score >= runner_up_absolute
if second_score >= threshold and (close_to_top or strong_alone):
matched.append(self.concepts[second_index])
return matched