dialectica / scripts /classical_models.py
Kattine
Phase 4b: DistilBERT in-domain 0.952, OOD 0.916, degradation halved vs LogReg
25cfe3f
Raw
History Blame Contribute Delete
4.01 kB
"""Baseline and classical models for cognitive-level classification.
Three models in increasing complexity: majority class, keyword heuristic,
and TF-IDF + logistic regression.
"""
from collections import Counter
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import Pipeline
class MajorityClassModel:
"""Predicts the single most frequent class seen during fit."""
def __init__(self):
self.majority_label = None
def fit(self, questions, labels):
"""Record the most common label."""
self.majority_label = Counter(labels).most_common(1)[0][0]
return self
def predict(self, questions):
"""Return the majority label for every input."""
return [self.majority_label] * len(questions)
class KeywordHeuristicModel:
"""Rule-based classifier using keyword cues."""
CRITICAL_CUES = [
"limitation", "trade-off", "tradeoff", "justified", "better than",
"the best", "critique", "evaluate", "design a", "propose", "devise",
"under what condition", "to what extent", "is it worth", "flaw",
"weakness", "redesign", "invent", "how sound", "drawback", "should we",
]
MECHANISTIC_CUES = [
"how would you", "how do", "how does", "why does", "why is",
"what causes", "calculate", "implement", "apply", "given ",
"relationship between", "distinguish", "compare", "what process",
]
SURFACE_CUES = [
"what is", "define", "list", "name the", "state the", "identify",
"when did", "where does", "who ", "what are the", "what does",
]
def fit(self, questions, labels):
"""No training needed; present for interface symmetry."""
return self
def _classify_one(self, question):
"""Apply the cue rules to a single question."""
text = question.lower()
if any(cue in text for cue in self.CRITICAL_CUES):
return "Critical"
if any(cue in text for cue in self.MECHANISTIC_CUES):
return "Mechanistic"
if any(cue in text for cue in self.SURFACE_CUES):
return "Surface"
# fallback
if "why" in text or "how" in text:
return "Mechanistic"
return "Surface"
def predict(self, questions):
"""Classify each question by rules."""
return [self._classify_one(q) for q in questions]
class TfidfLogRegModel:
"""TF-IDF features into logistic regression, tuned by cross-validation."""
def __init__(self, training_config):
"""Store training config."""
self.cfg = training_config
self.search = None
def fit(self, questions, labels):
"""Train with grid search over C and penalty."""
pipeline = Pipeline([
("tfidf", TfidfVectorizer(
ngram_range=(1, self.cfg.tfidf_ngram_max),
max_features=self.cfg.tfidf_max_features,
sublinear_tf=True,
)),
("clf", LogisticRegression(
solver="saga", # supports l1 and l2
max_iter=5000,
)),
])
param_grid = {
"clf__C": self.cfg.logreg_C_grid,
"clf__penalty": self.cfg.logreg_penalty_grid,
}
self.search = GridSearchCV(
pipeline,
param_grid,
scoring="f1_macro",
cv=self.cfg.cv_folds,
n_jobs=-1,
)
self.search.fit(questions, labels)
return self
@property
def best_params(self):
"""Best hyperparameters from grid search."""
return self.search.best_params_
@property
def best_cv_score(self):
"""Best CV macro-F1 from grid search."""
return self.search.best_score_
def predict(self, questions):
"""Predict using best estimator."""
return list(self.search.predict(questions))