Spaces:
Running
Running
Kattine
Polish: concept top-2 calibration, cross-cutting tag, uncertainty flag, overall depth bar
006a1d9 | """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 | |