Spaces:
Sleeping
Sleeping
File size: 6,614 Bytes
297cdf3 1997a7a 297cdf3 ca2008b 297cdf3 1997a7a ca2008b 297cdf3 ca2008b 297cdf3 ca2008b 297cdf3 ca2008b 297cdf3 ca2008b 297cdf3 ca2008b 78f1267 297cdf3 78f1267 297cdf3 78f1267 297cdf3 78f1267 297cdf3 78f1267 297cdf3 78f1267 297cdf3 78f1267 297cdf3 78f1267 297cdf3 78f1267 297cdf3 78f1267 297cdf3 78f1267 297cdf3 | 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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | from __future__ import annotations
import argparse
import json
import os
import re
from pathlib import Path
from typing import Any
PROJECT_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_MODEL_DIR = PROJECT_ROOT / "src" / "models" / "saved_emotion_model"
DEFAULT_HF_MODEL_ID = ""
def _load_transformer_stack() -> tuple[Any, Any, Any]:
try:
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer
except ImportError as exc:
raise ImportError(
"Module 2 requires torch and transformers. Install them with "
"`python -m pip install -r requirements.txt`, or run the Colab notebook."
) from exc
return torch, AutoModelForSequenceClassification, AutoTokenizer
class EmotionClassifier:
"""Transformer emotion classifier with confidence and simple word-occlusion explanations."""
def __init__(
self,
model_dir: str | Path | None = None,
) -> None:
self.model_dir = Path(model_dir or os.getenv("EMOTION_MODEL_DIR", DEFAULT_MODEL_DIR))
self.model_id = os.getenv("EMOTION_MODEL_ID", DEFAULT_HF_MODEL_ID).strip()
self.active_model_source = str(self.model_dir if self.model_dir.exists() else self.model_id)
self.torch = None
self.tokenizer = None
self.model = None
self.id2label: dict[int, str] = {}
def load_model(self) -> None:
model_source = self._resolve_model_source()
if not model_source:
raise FileNotFoundError(
"Emotion model is not available. Train Module 2 locally, or set "
"EMOTION_MODEL_ID to a Hugging Face model repository."
)
torch, model_cls, tokenizer_cls = _load_transformer_stack()
self.torch = torch
self.tokenizer = tokenizer_cls.from_pretrained(model_source)
self.model = model_cls.from_pretrained(model_source)
self.model.eval()
self.active_model_source = str(model_source)
config_labels = self.model.config.id2label
self.id2label = {int(key): value for key, value in config_labels.items()}
def _resolve_model_source(self) -> str | Path | None:
if self.model_dir.exists():
return self.model_dir
if self.model_id:
return self.model_id
return None
def _score_text(self, text: str) -> dict[str, Any]:
if self.model is None or self.tokenizer is None or self.torch is None:
self.load_model()
inputs = self.tokenizer(
text,
return_tensors="pt",
truncation=True,
max_length=128,
)
inputs.pop("token_type_ids", None)
with self.torch.no_grad():
logits = self.model(**inputs).logits
probabilities = self.torch.softmax(logits, dim=-1)[0]
best_index = int(probabilities.argmax().item())
confidence = float(probabilities[best_index].item())
scores = {self.id2label.get(index, str(index)): float(value.item()) for index, value in enumerate(probabilities)}
return {
"index": best_index,
"emotion": self.id2label.get(best_index, str(best_index)),
"confidence": confidence,
"scores": scores,
}
def predict_with_confidence(self, text: str) -> dict[str, Any]:
clean_text = text.strip()
if not clean_text:
return {
"emotion": "unknown",
"confidence": 0.0,
"is_confident": False,
"message": "Please enter text to classify.",
}
prediction = self._score_text(clean_text)
return {
"emotion": prediction["emotion"],
"confidence": prediction["confidence"],
"is_confident": prediction["confidence"] >= 0.60,
"message": None,
}
def explain(self, text: str, top_k: int = 8) -> dict[str, Any]:
"""Estimate influential words by measuring confidence drop after removing each word."""
clean_text = text.strip()
base_scores = self._score_text(clean_text)
base_prediction = {
"emotion": base_scores["emotion"],
"confidence": base_scores["confidence"],
"is_confident": base_scores["confidence"] >= 0.60,
"message": None,
}
target_emotion = base_prediction["emotion"]
base_confidence = base_prediction["confidence"]
words = list(re.finditer(r"\b[\w']+\b", clean_text))
impacts = []
for match in words:
reduced_text = (clean_text[: match.start()] + clean_text[match.end() :]).strip()
reduced_scores = self._score_text(reduced_text) if reduced_text else {"scores": {target_emotion: 0.0}}
target_confidence_without_word = reduced_scores["scores"].get(target_emotion, 0.0)
confidence_drop = base_confidence - target_confidence_without_word
if confidence_drop > 0.001:
effect = "supports prediction"
elif confidence_drop < -0.001:
effect = "reduces prediction"
else:
effect = "neutral"
impact = round(float(confidence_drop), 4)
if impact == -0.0:
impact = 0.0
impacts.append(
{
"word": match.group(0),
"impact": impact,
"confidence_without_word": round(float(target_confidence_without_word), 4),
"effect": effect,
}
)
impacts = sorted(impacts, key=lambda item: item["impact"], reverse=True)
return {
"prediction": base_prediction,
"top_evidence": impacts[:top_k],
"all_evidence": impacts,
"method": "word occlusion: larger impact means removing the word reduced confidence more",
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run Module 2 emotion inference.")
parser.add_argument("text", nargs="?", default="I feel anxious and overwhelmed today.")
parser.add_argument("--explain", action="store_true")
parser.add_argument("--model-dir", default=DEFAULT_MODEL_DIR, type=Path)
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
classifier = EmotionClassifier(model_dir=args.model_dir)
output = classifier.explain(args.text) if args.explain else classifier.predict_with_confidence(args.text)
print(json.dumps(output, indent=2))
|