File size: 4,010 Bytes
25cfe3f
 
 
 
9521b3b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25cfe3f
9521b3b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25cfe3f
9521b3b
 
 
 
25cfe3f
9521b3b
 
 
 
 
 
 
25cfe3f
9521b3b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25cfe3f
9521b3b
 
 
 
25cfe3f
9521b3b
 
 
25cfe3f
9521b3b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
"""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))