HarshithaaaaReddddyyy commited on
Commit
fa86c2a
·
verified ·
1 Parent(s): 2658239

Delete ngram_model.py

Browse files
Files changed (1) hide show
  1. ngram_model.py +0 -284
ngram_model.py DELETED
@@ -1,284 +0,0 @@
1
- """
2
- Character N-gram Language Identification Model
3
- High-accuracy baseline using TF-IDF character n-grams + Logistic Regression.
4
- Achieves ~98%+ accuracy on clean Indic text.
5
- """
6
- from __future__ import annotations
7
-
8
- import pickle
9
- from pathlib import Path
10
- from typing import Dict, List, Optional, Tuple
11
-
12
- import numpy as np
13
- import pandas as pd
14
- from loguru import logger
15
- from sklearn.calibration import CalibratedClassifierCV
16
- from sklearn.feature_extraction.text import TfidfVectorizer
17
- from sklearn.linear_model import LogisticRegression
18
- from sklearn.metrics import classification_report, f1_score
19
- from sklearn.pipeline import Pipeline
20
- from sklearn.preprocessing import LabelEncoder
21
-
22
- from src.utils.languages import LANGUAGE_REGISTRY, LABEL_TO_CODE, CODE_TO_LABEL
23
-
24
-
25
- class CharNgramLIDModel:
26
- """
27
- Character N-gram Language Identification using TF-IDF + LogReg.
28
-
29
- Key design choices:
30
- - analyzer='char_wb': treats word boundaries specially
31
- - ngram_range=(2,4): captures morpheme-level patterns
32
- - sublinear_tf=True: dampens frequency explosion
33
- - Calibration: isotonic regression for reliable probabilities
34
- - Per-language confidence thresholds for rejection
35
- """
36
-
37
- def __init__(
38
- self,
39
- ngram_range: Tuple[int, int] = (2, 4),
40
- max_features: int = 150_000,
41
- C: float = 5.0,
42
- max_iter: int = 1000,
43
- calibrate: bool = True,
44
- n_jobs: int = -1,
45
- ):
46
- self.ngram_range = ngram_range
47
- self.max_features = max_features
48
- self.C = C
49
- self.max_iter = max_iter
50
- self.calibrate = calibrate
51
- self.n_jobs = n_jobs
52
- self.pipeline: Optional[Pipeline] = None
53
- self.label_encoder = LabelEncoder()
54
- self.classes_: Optional[np.ndarray] = None
55
- self.confidence_thresholds: Dict[str, float] = {}
56
- self.is_fitted = False
57
-
58
- def _build_pipeline(self) -> Pipeline:
59
- vectorizer = TfidfVectorizer(
60
- analyzer="char_wb",
61
- ngram_range=self.ngram_range,
62
- max_features=self.max_features,
63
- sublinear_tf=True,
64
- strip_accents=None, # preserve Indic diacritics!
65
- min_df=2,
66
- max_df=0.95,
67
- dtype=np.float32,
68
- )
69
- base_clf = LogisticRegression(
70
- C=self.C,
71
- max_iter=self.max_iter,
72
- solver="saga",
73
- multi_class="multinomial",
74
- n_jobs=self.n_jobs,
75
- verbose=0,
76
- )
77
- if self.calibrate:
78
- clf = CalibratedClassifierCV(base_clf, method="isotonic", cv=3)
79
- else:
80
- clf = base_clf
81
-
82
- return Pipeline([
83
- ("tfidf", vectorizer),
84
- ("clf", clf),
85
- ])
86
-
87
- def fit(
88
- self,
89
- train_df: pd.DataFrame,
90
- val_df: Optional[pd.DataFrame] = None,
91
- ) -> "CharNgramLIDModel":
92
- logger.info(
93
- f"Training CharNgramLID | n_gram={self.ngram_range} | "
94
- f"features={self.max_features} | C={self.C}"
95
- )
96
- texts = train_df["text"].tolist()
97
- labels = self.label_encoder.fit_transform(train_df["lang"].tolist())
98
- self.classes_ = self.label_encoder.classes_
99
-
100
- self.pipeline = self._build_pipeline()
101
- self.pipeline.fit(texts, labels)
102
- self.is_fitted = True
103
-
104
- if val_df is not None:
105
- self._compute_confidence_thresholds(val_df)
106
- val_preds = self.predict(val_df["text"].tolist())
107
- f1 = f1_score(val_df["lang"].tolist(), val_preds, average="macro")
108
- logger.success(f"Val Macro-F1: {f1:.4f}")
109
-
110
- return self
111
-
112
- def predict(self, texts: List[str]) -> List[str]:
113
- assert self.is_fitted, "Model not fitted!"
114
- label_ids = self.pipeline.predict(texts)
115
- return self.label_encoder.inverse_transform(label_ids).tolist()
116
-
117
- def predict_proba(self, texts: List[str]) -> np.ndarray:
118
- """Returns probability matrix [n_samples × n_classes]."""
119
- assert self.is_fitted
120
- return self.pipeline.predict_proba(texts)
121
-
122
- def predict_with_confidence(
123
- self, texts: List[str]
124
- ) -> List[Dict]:
125
- """
126
- Returns per-sample predictions with confidence and
127
- top-K alternatives.
128
- """
129
- probs = self.predict_proba(texts)
130
- results = []
131
- for i, prob_row in enumerate(probs):
132
- top_k_idx = np.argsort(prob_row)[::-1][:5]
133
- top_k = [
134
- {
135
- "lang": self.classes_[j],
136
- "name": LANGUAGE_REGISTRY.get(self.classes_[j], None),
137
- "probability": float(prob_row[j]),
138
- }
139
- for j in top_k_idx
140
- ]
141
- best = top_k[0]
142
- threshold = self.confidence_thresholds.get(best["lang"], 0.5)
143
- results.append({
144
- "text": texts[i][:100] + "..." if len(texts[i]) > 100 else texts[i],
145
- "predicted_lang": best["lang"],
146
- "confidence": best["probability"],
147
- "is_confident": best["probability"] >= threshold,
148
- "top_5": top_k,
149
- })
150
- return results
151
-
152
- def _compute_confidence_thresholds(
153
- self, val_df: pd.DataFrame, target_precision: float = 0.95
154
- ):
155
- """
156
- Compute per-language confidence thresholds that achieve
157
- target_precision on the validation set.
158
- """
159
- probs = self.predict_proba(val_df["text"].tolist())
160
- true_langs = val_df["lang"].tolist()
161
- pred_langs = self.predict(val_df["text"].tolist())
162
-
163
- for lang in self.classes_:
164
- lang_idx = np.where(self.classes_ == lang)[0]
165
- if len(lang_idx) == 0:
166
- continue
167
- idx = lang_idx[0]
168
- lang_probs = probs[:, idx]
169
- is_correct = np.array([p == lang for p in pred_langs])
170
- is_predicted = np.array([p == lang for p in pred_langs])
171
-
172
- # Find threshold where precision ≥ target
173
- thresholds = np.linspace(0.3, 0.99, 50)
174
- best_t = 0.5
175
- for t in thresholds:
176
- mask = (lang_probs >= t) & is_predicted
177
- if mask.sum() == 0:
178
- break
179
- prec = is_correct[mask].mean()
180
- if prec >= target_precision:
181
- best_t = t
182
- break
183
- self.confidence_thresholds[lang] = best_t
184
-
185
- logger.debug(f"Confidence thresholds: {self.confidence_thresholds}")
186
-
187
- def evaluate(self, test_df: pd.DataFrame) -> Dict:
188
- """Full evaluation with per-language metrics."""
189
- texts = test_df["text"].tolist()
190
- true_labels = test_df["lang"].tolist()
191
- pred_labels = self.predict(texts)
192
- probs = self.predict_proba(texts)
193
-
194
- overall_f1 = f1_score(true_labels, pred_labels, average="macro")
195
- report = classification_report(
196
- true_labels, pred_labels,
197
- target_names=self.classes_,
198
- output_dict=True,
199
- )
200
-
201
- # Confidence calibration: ECE (Expected Calibration Error)
202
- confidences = probs.max(axis=1)
203
- correct = np.array(true_labels) == np.array(pred_labels)
204
- ece = self._compute_ece(confidences, correct)
205
-
206
- logger.info(f"\n{classification_report(true_labels, pred_labels)}")
207
- logger.success(f"Macro F1: {overall_f1:.4f} | ECE: {ece:.4f}")
208
-
209
- return {
210
- "macro_f1": overall_f1,
211
- "ece": ece,
212
- "per_language": report,
213
- "predictions": pred_labels,
214
- "true_labels": true_labels,
215
- "confidences": confidences.tolist(),
216
- }
217
-
218
- @staticmethod
219
- def _compute_ece(confidences: np.ndarray, correct: np.ndarray, n_bins: int = 10) -> float:
220
- bins = np.linspace(0, 1, n_bins + 1)
221
- ece = 0.0
222
- for lo, hi in zip(bins[:-1], bins[1:]):
223
- mask = (confidences >= lo) & (confidences < hi)
224
- if mask.sum() == 0:
225
- continue
226
- avg_conf = confidences[mask].mean()
227
- avg_acc = correct[mask].mean()
228
- ece += mask.mean() * abs(avg_conf - avg_acc)
229
- return float(ece)
230
-
231
- def save(self, path: str):
232
- Path(path).parent.mkdir(parents=True, exist_ok=True)
233
- with open(path, "wb") as f:
234
- pickle.dump({
235
- "pipeline": self.pipeline,
236
- "label_encoder": self.label_encoder,
237
- "classes_": self.classes_,
238
- "confidence_thresholds": self.confidence_thresholds,
239
- "config": {
240
- "ngram_range": self.ngram_range,
241
- "max_features": self.max_features,
242
- "C": self.C,
243
- },
244
- }, f)
245
- logger.success(f"Model saved: {path}")
246
-
247
- @classmethod
248
- def load(cls, path: str) -> "CharNgramLIDModel":
249
- with open(path, "rb") as f:
250
- data = pickle.load(f)
251
- cfg = data.get("config", {})
252
- model = cls(
253
- ngram_range=cfg.get("ngram_range", (2, 4)),
254
- max_features=cfg.get("max_features", 150_000),
255
- C=cfg.get("C", 5.0),
256
- )
257
- model.pipeline = data["pipeline"]
258
- model.label_encoder = data["label_encoder"]
259
- model.classes_ = data["classes_"]
260
- model.confidence_thresholds = data.get("confidence_thresholds", {})
261
- model.is_fitted = True
262
- logger.success(f"Model loaded: {path}")
263
- return model
264
-
265
- def get_top_features_per_language(self, n: int = 20) -> Dict[str, List[str]]:
266
- """Extract most discriminative n-grams per language."""
267
- assert self.is_fitted
268
- tfidf = self.pipeline.named_steps["tfidf"]
269
- clf = self.pipeline.named_steps["clf"]
270
- feature_names = np.array(tfidf.get_feature_names_out())
271
-
272
- # Get the underlying LR if calibrated
273
- if hasattr(clf, "calibrated_classifiers_"):
274
- coef = np.mean(
275
- [c.estimator.coef_ for c in clf.calibrated_classifiers_], axis=0
276
- )
277
- else:
278
- coef = clf.coef_
279
-
280
- result = {}
281
- for i, lang in enumerate(self.classes_):
282
- top_idx = np.argsort(coef[i])[::-1][:n]
283
- result[lang] = feature_names[top_idx].tolist()
284
- return result