Marwan-Tamer commited on
Commit
78f1267
·
1 Parent(s): 1997a7a

Improve emotion explanations and intent UI

Browse files
src/models/emotion_classifier.py CHANGED
@@ -56,25 +56,17 @@ class EmotionClassifier:
56
  config_labels = self.model.config.id2label
57
  self.id2label = {int(key): value for key, value in config_labels.items()}
58
 
59
- def predict_with_confidence(self, text: str) -> dict[str, Any]:
60
- clean_text = text.strip()
61
- if not clean_text:
62
- return {
63
- "emotion": "unknown",
64
- "confidence": 0.0,
65
- "is_confident": False,
66
- "message": "Please enter text to classify.",
67
- }
68
-
69
  if self.model is None or self.tokenizer is None or self.torch is None:
70
  self.load_model()
71
 
72
  inputs = self.tokenizer(
73
- clean_text,
74
  return_tensors="pt",
75
  truncation=True,
76
  max_length=128,
77
  )
 
78
 
79
  with self.torch.no_grad():
80
  logits = self.model(**inputs).logits
@@ -82,39 +74,72 @@ class EmotionClassifier:
82
 
83
  best_index = int(probabilities.argmax().item())
84
  confidence = float(probabilities[best_index].item())
 
85
 
86
  return {
 
87
  "emotion": self.id2label.get(best_index, str(best_index)),
88
  "confidence": confidence,
89
- "is_confident": confidence >= 0.60,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  "message": None,
91
  }
92
 
93
  def explain(self, text: str, top_k: int = 8) -> dict[str, Any]:
94
  """Estimate influential words by measuring confidence drop after removing each word."""
95
- base_prediction = self.predict_with_confidence(text)
 
 
 
 
 
 
 
96
  target_emotion = base_prediction["emotion"]
97
  base_confidence = base_prediction["confidence"]
98
- words = re.findall(r"\b[\w']+\b", text)
99
 
100
  impacts = []
101
- for index, word in enumerate(words):
102
- reduced_words = words[:index] + words[index + 1 :]
103
- if not reduced_words:
104
- continue
105
-
106
- reduced_text = " ".join(reduced_words)
107
- reduced_prediction = self.predict_with_confidence(reduced_text)
108
- confidence_drop = base_confidence - (
109
- reduced_prediction["confidence"]
110
- if reduced_prediction["emotion"] == target_emotion
111
- else 0.0
112
- )
 
 
 
 
113
 
114
  impacts.append(
115
  {
116
- "word": word,
117
- "impact": round(float(confidence_drop), 4),
 
 
118
  }
119
  )
120
 
@@ -122,6 +147,7 @@ class EmotionClassifier:
122
  return {
123
  "prediction": base_prediction,
124
  "top_evidence": impacts[:top_k],
 
125
  "method": "word occlusion: larger impact means removing the word reduced confidence more",
126
  }
127
 
 
56
  config_labels = self.model.config.id2label
57
  self.id2label = {int(key): value for key, value in config_labels.items()}
58
 
59
+ def _score_text(self, text: str) -> dict[str, Any]:
 
 
 
 
 
 
 
 
 
60
  if self.model is None or self.tokenizer is None or self.torch is None:
61
  self.load_model()
62
 
63
  inputs = self.tokenizer(
64
+ text,
65
  return_tensors="pt",
66
  truncation=True,
67
  max_length=128,
68
  )
69
+ inputs.pop("token_type_ids", None)
70
 
71
  with self.torch.no_grad():
72
  logits = self.model(**inputs).logits
 
74
 
75
  best_index = int(probabilities.argmax().item())
76
  confidence = float(probabilities[best_index].item())
77
+ scores = {self.id2label.get(index, str(index)): float(value.item()) for index, value in enumerate(probabilities)}
78
 
79
  return {
80
+ "index": best_index,
81
  "emotion": self.id2label.get(best_index, str(best_index)),
82
  "confidence": confidence,
83
+ "scores": scores,
84
+ }
85
+
86
+ def predict_with_confidence(self, text: str) -> dict[str, Any]:
87
+ clean_text = text.strip()
88
+ if not clean_text:
89
+ return {
90
+ "emotion": "unknown",
91
+ "confidence": 0.0,
92
+ "is_confident": False,
93
+ "message": "Please enter text to classify.",
94
+ }
95
+
96
+ prediction = self._score_text(clean_text)
97
+
98
+ return {
99
+ "emotion": prediction["emotion"],
100
+ "confidence": prediction["confidence"],
101
+ "is_confident": prediction["confidence"] >= 0.60,
102
  "message": None,
103
  }
104
 
105
  def explain(self, text: str, top_k: int = 8) -> dict[str, Any]:
106
  """Estimate influential words by measuring confidence drop after removing each word."""
107
+ clean_text = text.strip()
108
+ base_scores = self._score_text(clean_text)
109
+ base_prediction = {
110
+ "emotion": base_scores["emotion"],
111
+ "confidence": base_scores["confidence"],
112
+ "is_confident": base_scores["confidence"] >= 0.60,
113
+ "message": None,
114
+ }
115
  target_emotion = base_prediction["emotion"]
116
  base_confidence = base_prediction["confidence"]
117
+ words = list(re.finditer(r"\b[\w']+\b", clean_text))
118
 
119
  impacts = []
120
+ for match in words:
121
+ reduced_text = (clean_text[: match.start()] + clean_text[match.end() :]).strip()
122
+ reduced_scores = self._score_text(reduced_text) if reduced_text else {"scores": {target_emotion: 0.0}}
123
+ target_confidence_without_word = reduced_scores["scores"].get(target_emotion, 0.0)
124
+ confidence_drop = base_confidence - target_confidence_without_word
125
+
126
+ if confidence_drop > 0.001:
127
+ effect = "supports prediction"
128
+ elif confidence_drop < -0.001:
129
+ effect = "reduces prediction"
130
+ else:
131
+ effect = "neutral"
132
+
133
+ impact = round(float(confidence_drop), 4)
134
+ if impact == -0.0:
135
+ impact = 0.0
136
 
137
  impacts.append(
138
  {
139
+ "word": match.group(0),
140
+ "impact": impact,
141
+ "confidence_without_word": round(float(target_confidence_without_word), 4),
142
+ "effect": effect,
143
  }
144
  )
145
 
 
147
  return {
148
  "prediction": base_prediction,
149
  "top_evidence": impacts[:top_k],
150
+ "all_evidence": impacts,
151
  "method": "word occlusion: larger impact means removing the word reduced confidence more",
152
  }
153
 
src/models/emotion_detector_ui.py CHANGED
@@ -46,12 +46,10 @@ CSS = """
46
  def model_status() -> str:
47
  model_dir = Path(os.getenv("EMOTION_MODEL_DIR", DEFAULT_MODEL_DIR))
48
  if model_dir.exists():
49
- return f"<div class='status-box'>Using local trained model: <code>{model_dir}</code></div>"
50
  return (
51
- "<div class='missing-box'>Local emotion model is not available at "
52
- f"<code>{model_dir}</code>. Copy the trained "
53
- "<code>saved_emotion_model</code> folder there, or set "
54
- "<code>EMOTION_MODEL_DIR</code> to its current location.</div>"
55
  )
56
 
57
 
@@ -68,7 +66,7 @@ def predict_emotion(text: str) -> tuple[str, list[list[str | float]], str]:
68
  return _empty_result("Please enter a message to analyze.")
69
 
70
  try:
71
- result = classifier.explain(text or "", top_k=6)
72
  emotion = result["prediction"]["emotion"]
73
  confidence = result["prediction"]["confidence"]
74
  card = (
@@ -78,15 +76,24 @@ def predict_emotion(text: str) -> tuple[str, list[list[str | float]], str]:
78
  f"<div>Confidence: <b>{confidence:.1%}</b></div>"
79
  "</div>"
80
  )
81
- evidence = [[item["word"], item["impact"]] for item in result["top_evidence"]]
82
- status = f"<div class='status-box'>Model source: <code>{classifier.active_model_source}</code></div>"
 
 
 
 
 
 
 
 
83
  return card, evidence, status
84
  except FileNotFoundError:
85
  return _empty_result("Local emotion model is not available yet.")
86
  except ImportError as exc:
87
  return _empty_result(f"Missing dependency: {exc}")
88
  except Exception as exc:
89
- return _empty_result(f"Emotion analysis is unavailable right now: {exc}")
 
90
 
91
 
92
  with gr.Blocks(title="Emotion Classifier") as interface:
@@ -110,8 +117,8 @@ with gr.Blocks(title="Emotion Classifier") as interface:
110
  with gr.Column(scale=4):
111
  result_output = gr.HTML(label="Prediction")
112
  evidence_output = gr.Dataframe(
113
- headers=["Word", "Impact"],
114
- datatype=["str", "number"],
115
  label="Word Evidence",
116
  interactive=False,
117
  )
 
46
  def model_status() -> str:
47
  model_dir = Path(os.getenv("EMOTION_MODEL_DIR", DEFAULT_MODEL_DIR))
48
  if model_dir.exists():
49
+ return "<div class='status-box'>Local trained emotion model is ready.</div>"
50
  return (
51
+ "<div class='missing-box'>Local emotion model is not available yet. "
52
+ "Add the trained <code>saved_emotion_model</code> folder before testing.</div>"
 
 
53
  )
54
 
55
 
 
66
  return _empty_result("Please enter a message to analyze.")
67
 
68
  try:
69
+ result = classifier.explain(text or "", top_k=8)
70
  emotion = result["prediction"]["emotion"]
71
  confidence = result["prediction"]["confidence"]
72
  card = (
 
76
  f"<div>Confidence: <b>{confidence:.1%}</b></div>"
77
  "</div>"
78
  )
79
+ evidence = [
80
+ [
81
+ item["word"],
82
+ item["impact"],
83
+ item["confidence_without_word"],
84
+ item["effect"],
85
+ ]
86
+ for item in result["all_evidence"]
87
+ ]
88
+ status = "<div class='status-box'>Prediction generated by the local trained DistilBERT model.</div>"
89
  return card, evidence, status
90
  except FileNotFoundError:
91
  return _empty_result("Local emotion model is not available yet.")
92
  except ImportError as exc:
93
  return _empty_result(f"Missing dependency: {exc}")
94
  except Exception as exc:
95
+ print(f"Emotion UI error: {type(exc).__name__}: {exc}")
96
+ return _empty_result("Emotion analysis is unavailable right now. Please check the terminal logs.")
97
 
98
 
99
  with gr.Blocks(title="Emotion Classifier") as interface:
 
117
  with gr.Column(scale=4):
118
  result_output = gr.HTML(label="Prediction")
119
  evidence_output = gr.Dataframe(
120
+ headers=["Word", "Impact", "Confidence Without Word", "Effect"],
121
+ datatype=["str", "number", "number", "str"],
122
  label="Word Evidence",
123
  interactive=False,
124
  )
src/models/intent_detector_ui.py CHANGED
@@ -1,3 +1,8 @@
 
 
 
 
 
1
  import gradio as gr
2
 
3
  from intent_classifier import IntentClassifier
@@ -5,24 +10,160 @@ from intent_classifier import IntentClassifier
5
 
6
  classifier = IntentClassifier()
7
 
 
 
 
 
 
 
 
8
 
9
- def predict_intent(text: str) -> dict:
10
- return classifier.classify(text or "")
 
 
 
 
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- interface = gr.Interface(
14
- fn=predict_intent,
15
- inputs=gr.Textbox(
16
- lines=4,
17
- placeholder="Type a user message...",
18
- label="User Message",
19
- ),
20
- outputs=gr.JSON(label="Intent Result"),
21
- title="Module 3: Intent Classification",
22
- description="Few-shot Groq intent classifier for chatbot routing.",
23
- flagging_mode="never",
24
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
 
27
  if __name__ == "__main__":
28
- interface.launch()
 
 
1
+ from __future__ import annotations
2
+
3
+ import html
4
+ import os
5
+
6
  import gradio as gr
7
 
8
  from intent_classifier import IntentClassifier
 
10
 
11
  classifier = IntentClassifier()
12
 
13
+ INTENT_LABELS = {
14
+ "greeting": "Greeting",
15
+ "goodbye": "Goodbye",
16
+ "gratitude": "Gratitude",
17
+ "asking_mental_health_question": "Mental Health Question",
18
+ "out_of_scope": "Out of Scope",
19
+ }
20
 
21
+ THEME = gr.themes.Base(
22
+ primary_hue="amber",
23
+ secondary_hue="cyan",
24
+ neutral_hue="gray",
25
+ radius_size="sm",
26
+ )
27
 
28
+ CSS = """
29
+ .intent-shell {
30
+ max-width: 1040px;
31
+ margin: 0 auto;
32
+ }
33
+ .intent-header {
34
+ border-bottom: 3px solid #111827;
35
+ padding: 18px 0 14px;
36
+ margin-bottom: 18px;
37
+ }
38
+ .intent-kicker {
39
+ color: #0891b2;
40
+ font-size: 13px;
41
+ font-weight: 700;
42
+ letter-spacing: 0;
43
+ text-transform: uppercase;
44
+ }
45
+ .intent-panel {
46
+ background: #ffffff;
47
+ border: 2px solid #111827;
48
+ box-shadow: 6px 6px 0 #facc15;
49
+ padding: 16px;
50
+ }
51
+ .intent-result {
52
+ background: #f9fafb;
53
+ border: 2px solid #111827;
54
+ padding: 16px;
55
+ }
56
+ .intent-label {
57
+ font-size: 28px;
58
+ font-weight: 800;
59
+ color: #111827;
60
+ }
61
+ .confidence-track {
62
+ height: 12px;
63
+ background: #e5e7eb;
64
+ border: 1px solid #111827;
65
+ margin-top: 12px;
66
+ }
67
+ .confidence-fill {
68
+ height: 100%;
69
+ background: #06b6d4;
70
+ }
71
+ .intent-note {
72
+ color: #374151;
73
+ margin-top: 10px;
74
+ }
75
+ .intent-error {
76
+ background: #fff1f2;
77
+ border: 2px solid #be123c;
78
+ padding: 14px;
79
+ }
80
+ """
81
 
82
+
83
+ def _result_card(intent: str, confidence: float, reason: str) -> str:
84
+ label = INTENT_LABELS.get(intent, intent.replace("_", " ").title())
85
+ safe_label = html.escape(label)
86
+ safe_reason = html.escape(reason)
87
+ width = max(0, min(confidence, 1)) * 100
88
+
89
+ return (
90
+ "<div class='intent-result'>"
91
+ "<div>Detected intent</div>"
92
+ f"<div class='intent-label'>{safe_label}</div>"
93
+ f"<div class='confidence-track'><div class='confidence-fill' style='width: {width:.1f}%'></div></div>"
94
+ f"<div class='intent-note'>Confidence: <b>{confidence:.1%}</b></div>"
95
+ f"<div class='intent-note'>{safe_reason}</div>"
96
+ "</div>"
97
+ )
98
+
99
+
100
+ def _empty_result(message: str) -> tuple[str, list[list[str]]]:
101
+ return f"<div class='intent-error'>{html.escape(message)}</div>", []
102
+
103
+
104
+ def predict_intent(text: str) -> tuple[str, list[list[str]]]:
105
+ clean_text = (text or "").strip()
106
+ if not clean_text:
107
+ return _empty_result("Please enter a user message to classify.")
108
+
109
+ try:
110
+ result = classifier.classify(clean_text)
111
+ except RuntimeError:
112
+ return _empty_result("Groq API key is not configured. Set GROQ_API_KEY before running Module 3.")
113
+ except ImportError:
114
+ return _empty_result("Groq SDK is not installed. Install project requirements and try again.")
115
+ except Exception as exc:
116
+ print(f"Intent UI error: {type(exc).__name__}: {exc}")
117
+ return _empty_result("Intent classification is unavailable right now. Please check the terminal logs.")
118
+
119
+ intent = result["intent"]
120
+ confidence = float(result["confidence"])
121
+ reason = result["reason"]
122
+
123
+ details = [
124
+ ["Routing Key", intent],
125
+ ["Display Label", INTENT_LABELS.get(intent, intent)],
126
+ ["Confidence", f"{confidence:.1%}"],
127
+ ["Reason", reason],
128
+ ]
129
+ return _result_card(intent, confidence, reason), details
130
+
131
+
132
+ with gr.Blocks(title="Intent Classifier") as interface:
133
+ with gr.Column(elem_classes=["intent-shell"]):
134
+ gr.HTML(
135
+ """
136
+ <div class="intent-header">
137
+ <div class="intent-kicker">Module 3</div>
138
+ <h1>Intent Routing</h1>
139
+ </div>
140
+ """
141
+ )
142
+
143
+ with gr.Row():
144
+ with gr.Column(scale=5, elem_classes=["intent-panel"]):
145
+ text_input = gr.Textbox(
146
+ lines=7,
147
+ label="User message",
148
+ placeholder="Example: Hi, I feel anxious and cannot sleep.",
149
+ )
150
+ classify_button = gr.Button("Classify intent", variant="primary")
151
+ with gr.Column(scale=4):
152
+ result_output = gr.HTML()
153
+ details_output = gr.Dataframe(
154
+ headers=["Field", "Value"],
155
+ datatype=["str", "str"],
156
+ label="Routing Details",
157
+ interactive=False,
158
+ )
159
+
160
+ classify_button.click(
161
+ fn=predict_intent,
162
+ inputs=text_input,
163
+ outputs=[result_output, details_output],
164
+ )
165
 
166
 
167
  if __name__ == "__main__":
168
+ port = int(os.getenv("GRADIO_SERVER_PORT", "7861"))
169
+ interface.launch(theme=THEME, css=CSS, server_port=port)