emhaihsan commited on
Commit
99f6668
·
0 Parent(s):

Initial commit: LLM Evaluation Dashboard

Browse files
.gitignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .Python
6
+ *.so
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ .env
12
+ .venv
13
+ venv/
14
+ ENV/
README.md ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LLM Evaluation Metrics Dashboard
2
+
3
+ [![Streamlit App](https://static.streamlit.io/badges/streamlit_badge_black_white.svg)](https://huggingface.co/spaces/your-username/llm-evaluation-dashboard)
4
+
5
+ A comprehensive **educational dashboard** demonstrating 5 key NLP evaluation metrics with interactive visualizations and mathematical explanations.
6
+
7
+ ![Dashboard Preview](https://via.placeholder.com/800x400.png?text=LLM+Evaluation+Dashboard+Preview)
8
+
9
+ ## 🎯 Features
10
+
11
+ ### 5 Complete Metric Implementations
12
+
13
+ | Metric | Focus | Use Case |
14
+ |--------|-------|----------|
15
+ | **BLEU** | N-gram Precision | Machine Translation |
16
+ | **ROGUE** | N-gram Recall (5 variants) | Text Summarization |
17
+ | **Perplexity** | Model Confidence | Language Modeling |
18
+ | **MRR** | Ranking Quality | Question Answering |
19
+ | **BERT Score** | Semantic Similarity | Paraphrase Detection |
20
+
21
+ ### Interactive Visualizations
22
+
23
+ - 📊 **Radar charts** for multi-metric comparison
24
+ - 📈 **Bar charts** for precision/recall breakdown
25
+ - 🔥 **Heatmaps** for token-level similarity
26
+ - 🎚️ **Gauge charts** for score visualization
27
+ - 📝 **Step-by-step** calculation breakdowns
28
+
29
+ ### Educational Focus
30
+
31
+ - 🧮 **Mathematical formulas** with LaTeX rendering
32
+ - 🔍 **N-gram matching** visualizations
33
+ - 🎓 **Preset examples** for quick learning
34
+ - 💡 **Quality interpretations** for each score
35
+
36
+ ## 🚀 Quick Start
37
+
38
+ ### Local Installation
39
+
40
+ ```bash
41
+ # Clone the repository
42
+ git clone https://github.com/your-username/llm-evaluation-dashboard.git
43
+ cd llm-evaluation-dashboard
44
+
45
+ # Install dependencies
46
+ pip install -r requirements.txt
47
+
48
+ # Download NLTK data
49
+ python -c "import nltk; nltk.download('punkt')"
50
+
51
+ # Run the app
52
+ streamlit run app.py
53
+ ```
54
+
55
+ ### Online Demo
56
+
57
+ Visit the live demo: [HuggingFace Spaces](https://huggingface.co/spaces/your-username/llm-evaluation-dashboard)
58
+
59
+ ## 📁 Project Structure
60
+
61
+ ```
62
+ llm-evaluation-dashboard/
63
+ ├── app.py # Main entry point
64
+ ├── pages/
65
+ │ ├── 01_overview.py # All metrics comparison
66
+ │ ├── 02_bleu_score.py # BLEU with n-gram viz
67
+ │ ├── 03_rogue_score.py # 5 ROGUE variants
68
+ │ ├── 04_perplexity.py # Token-level perplexity
69
+ │ ├── 05_mrr.py # Ranking evaluation
70
+ │ └── 06_bert_score.py # Semantic similarity
71
+ ├── modules/
72
+ │ ├── metrics/ # Metric implementations
73
+ │ │ ├── bleu.py
74
+ │ │ ├── rogue.py
75
+ │ │ ├── perplexity.py
76
+ │ │ ├── mrr.py
77
+ │ │ └── bert_score.py
78
+ │ ├── visualizations/ # Plotly charts
79
+ │ │ ├── charts.py
80
+ │ │ └── explanations.py
81
+ │ └── utils/ # Helpers
82
+ │ ├── text_processing.py
83
+ │ └── examples.py
84
+ ├── data/
85
+ │ └── sample_evaluations.json # Test cases
86
+ ├── requirements.txt
87
+ └── README.md
88
+ ```
89
+
90
+ ## 🎓 Learning Resources
91
+
92
+ ### BLEU Score
93
+ - Bilingual Evaluation Understudy
94
+ - N-gram precision (1-4 grams)
95
+ - Brevity penalty for short outputs
96
+ - Best for: Machine translation
97
+
98
+ ### ROGUE Score (5 Variants)
99
+ - **ROGUE-N**: Unigram/bigram overlap
100
+ - **ROGUE-L**: Longest Common Subsequence
101
+ - **ROGUE-S**: Skip-bigram matching
102
+ - **ROGUE-SU**: Skip + Unigram combined
103
+ - **ROGUE-W**: Weighted consecutive matches
104
+ - Best for: Text summarization
105
+
106
+ ### Perplexity
107
+ - Model confidence metric
108
+ - Lower = more confident
109
+ - No reference needed
110
+ - Best for: Language model evaluation
111
+
112
+ ### MRR (Mean Reciprocal Rank)
113
+ - Ranking quality metric
114
+ - 1/rank_of_correct_answer
115
+ - Batch evaluation support
116
+ - Best for: Question answering / IR
117
+
118
+ ### BERT Score
119
+ - Contextual embedding similarity
120
+ - Semantic vs lexical matching
121
+ - Token-level alignment
122
+ - Best for: Paraphrase detection
123
+
124
+ ## 💻 Usage Examples
125
+
126
+ ### Compare Two Translations
127
+
128
+ 1. Go to **Overview** page
129
+ 2. Paste reference translation
130
+ 3. Paste candidate translation
131
+ 4. Click "Compare All Metrics"
132
+ 5. See radar chart showing BLEU, ROGUE, and BERT Score
133
+
134
+ ### Evaluate Summarization Quality
135
+
136
+ 1. Go to **ROGUE Score** page
137
+ 2. Select "ROGUE-L" variant
138
+ 3. Enter original article as reference
139
+ 4. Enter summary as candidate
140
+ 5. Check LCS (Longest Common Subsequence) visualization
141
+
142
+ ### Test Question Answering
143
+
144
+ 1. Go to **MRR** page
145
+ 2. Enter question and correct answer
146
+ 3. Enter ranked model outputs
147
+ 4. See reciprocal rank calculation
148
+
149
+ ## 🛠️ Tech Stack
150
+
151
+ - **Framework**: Streamlit
152
+ - **Visualizations**: Plotly, Matplotlib
153
+ - **NLP**: NLTK
154
+ - **Metrics**: rouge-score, bert-score, sentence-transformers
155
+ - **Math**: NumPy, Pandas
156
+
157
+ ## 📝 Implementation Notes
158
+
159
+ ### Educational Simplifications
160
+
161
+ - **Perplexity**: Uses probability simulation (real calculation requires model logprobs)
162
+ - **BERT Score**: Uses simplified embeddings (install sentence-transformers for full BERT)
163
+ - **Tokenization**: Simple word-based (no subword tokenization)
164
+
165
+ ### For Production Use
166
+
167
+ Consider these libraries for production:
168
+ ```python
169
+ # Official implementations
170
+ from nltk.translate.bleu_score import sentence_bleu
171
+ from rouge_score import rouge_scorer
172
+ from bert_score import score as bert_score
173
+ from evaluate import load as load_metric
174
+ ```
175
+
176
+ ## 🤝 Contributing
177
+
178
+ Contributions welcome! Areas for improvement:
179
+ - Add more evaluation metrics (METEOR, chrF, etc.)
180
+ - Implement real LLM API integration for perplexity
181
+ - Add multilingual support
182
+ - Create video tutorials
183
+
184
+ ## 📄 License
185
+
186
+ MIT License - see [LICENSE](LICENSE) for details.
187
+
188
+ ## 🙏 Acknowledgments
189
+
190
+ - Inspired by academic papers on NLG evaluation
191
+ - Built with Streamlit community best practices
192
+ - Icons from [Twemoji](https://twemoji.twitter.com/)
193
+
194
+ ## 📧 Contact
195
+
196
+ For questions or feedback:
197
+ - Open an issue on GitHub
198
+ - Email: your-email@example.com
199
+
200
+ ---
201
+
202
+ **Star ⭐ this repo if you found it helpful!**
app.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LLM Evaluation Metrics Dashboard
2
+
3
+ A comprehensive educational dashboard demonstrating 5 key NLP evaluation metrics:
4
+ - BLEU Score (n-gram precision with brevity penalty)
5
+ - ROGUE Score (recall-oriented with 5 variants)
6
+ - Perplexity (model confidence measurement)
7
+ - Mean Reciprocal Rank (ranking evaluation)
8
+ - BERT Score (semantic similarity via embeddings)
9
+ """
10
+
11
+ import streamlit as st
12
+
13
+ st.set_page_config(
14
+ page_title="LLM Evaluation Dashboard",
15
+ page_icon="📊",
16
+ layout="wide",
17
+ initial_sidebar_state="expanded"
18
+ )
19
+
20
+ st.title("📊 LLM Evaluation Metrics Dashboard")
21
+
22
+ st.markdown("""
23
+ ## Welcome!
24
+
25
+ This dashboard provides **interactive visualizations** of 5 essential NLP evaluation metrics
26
+ covering both **lexical overlap** (BLEU, ROGUE) and **semantic similarity** (BERT Score) approaches,
27
+ along with **model confidence** (Perplexity) and **ranking quality** (MRR) metrics.
28
+
29
+ ### 📚 What You'll Learn
30
+
31
+ | Metric | Focus | Use Case |
32
+ |--------|-------|----------|
33
+ | **BLEU Score** | N-gram precision | Machine translation evaluation |
34
+ | **ROGUE Score** | N-gram recall | Text summarization evaluation |
35
+ | **Perplexity** | Token probability | Language model confidence |
36
+ | **MRR** | Ranking position | Question-answering / retrieval |
37
+ | **BERT Score** | Semantic similarity | Paraphrase detection |
38
+
39
+ ### 🎯 Getting Started
40
+
41
+ Use the **sidebar navigation** to explore each metric:
42
+ - **Overview**: Compare all metrics side-by-side
43
+ - **Individual Pages**: Deep dive into each metric with step-by-step calculations
44
+
45
+ ### 💡 Key Concepts
46
+
47
+ **Ground Truth vs Prediction**: Most metrics compare a *reference* (ground truth) text
48
+ against a *candidate* (model prediction) to measure quality.
49
+
50
+ **Precision vs Recall**:
51
+ - **Precision** (BLEU): Of the words generated, how many are correct?
52
+ - **Recall** (ROGUE): Of the correct words, how many were generated?
53
+
54
+ ---
55
+
56
+ *Built with ❤️ using Streamlit, NLTK, and Transformers*
57
+ """)
58
+
59
+ st.sidebar.success("Select a page above to explore metrics!")
data/sample_evaluations.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "examples": [
3
+ {
4
+ "name": "Exact Match",
5
+ "ground_truth": "The cat sat on the mat",
6
+ "prediction": "The cat sat on the mat",
7
+ "description": "Perfect match - all metrics should be 1.0"
8
+ },
9
+ {
10
+ "name": "Paraphrase",
11
+ "ground_truth": "The cat sat on the mat",
12
+ "prediction": "The cat was sitting on the mat",
13
+ "description": "Same meaning, different words - BERT > BLEU"
14
+ },
15
+ {
16
+ "name": "Partial Match",
17
+ "ground_truth": "The cat sat on the mat and looked outside",
18
+ "prediction": "The cat sat on the mat",
19
+ "description": "Incomplete - brevity penalty applies"
20
+ },
21
+ {
22
+ "name": "Wrong Answer",
23
+ "ground_truth": "Paris is the capital of France",
24
+ "prediction": "Berlin is the capital of France",
25
+ "description": "Completely wrong but similar structure"
26
+ },
27
+ {
28
+ "name": "Extra Content",
29
+ "ground_truth": "The cat sat on the mat",
30
+ "prediction": "The cat sat on the mat and then jumped off quickly",
31
+ "description": "Extra words added - recall suffers"
32
+ },
33
+ {
34
+ "name": "Word Reorder",
35
+ "ground_truth": "The cat sat on the mat",
36
+ "prediction": "On the mat sat the cat",
37
+ "description": "Same words, different order - ROGUE-L handles this"
38
+ }
39
+ ]
40
+ }
modules/__init__.py ADDED
File without changes
modules/metrics/__init__.py ADDED
File without changes
modules/metrics/bert_score.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """BERT Score metric implementation using sentence embeddings."""
2
+
3
+ from typing import List, Dict, Tuple
4
+ import numpy as np
5
+ from ..utils.text_processing import tokenize
6
+
7
+ # Try to import sentence-transformers, fall back to simple implementation
8
+ try:
9
+ from sentence_transformers import SentenceTransformer
10
+ SENTENCE_TRANSFORMERS_AVAILABLE = True
11
+ except ImportError:
12
+ SENTENCE_TRANSFORMERS_AVAILABLE = False
13
+
14
+
15
+ def cosine_similarity(vec1: np.ndarray, vec2: np.ndarray) -> float:
16
+ """Calculate cosine similarity between two vectors.
17
+
18
+ Args:
19
+ vec1: First vector
20
+ vec2: Second vector
21
+
22
+ Returns:
23
+ Cosine similarity (-1 to 1)
24
+ """
25
+ dot_product = np.dot(vec1, vec2)
26
+ norm1 = np.linalg.norm(vec1)
27
+ norm2 = np.linalg.norm(vec2)
28
+
29
+ if norm1 == 0 or norm2 == 0:
30
+ return 0.0
31
+
32
+ return dot_product / (norm1 * norm2)
33
+
34
+
35
+ def simple_embedding(text: str, embedding_dim: int = 384) -> np.ndarray:
36
+ """Create a simple deterministic embedding for demonstration.
37
+
38
+ In production, use sentence-transformers or bert-score library.
39
+ This is a simulation that creates consistent embeddings based on text content.
40
+
41
+ Args:
42
+ text: Input text
43
+ embedding_dim: Dimension of embedding vector
44
+
45
+ Returns:
46
+ Embedding vector
47
+ """
48
+ # Simple hash-based embedding for demonstration
49
+ tokens = tokenize(text)
50
+
51
+ # Initialize embedding
52
+ embedding = np.zeros(embedding_dim)
53
+
54
+ # Create embedding based on token hashes
55
+ for i, token in enumerate(tokens):
56
+ # Use hash to create deterministic but distributed values
57
+ token_hash = hash(token) % (2**32)
58
+ np.random.seed(token_hash)
59
+
60
+ # Add weighted contribution
61
+ token_embedding = np.random.randn(embedding_dim)
62
+ # Weight by position (earlier tokens slightly more important)
63
+ weight = 1.0 / (1 + i * 0.1)
64
+ embedding += token_embedding * weight
65
+
66
+ # Normalize
67
+ if len(tokens) > 0:
68
+ embedding /= len(tokens)
69
+
70
+ # Add some sentence-level statistics
71
+ np.random.seed(hash(text) % (2**32))
72
+ embedding += np.random.randn(embedding_dim) * 0.1
73
+
74
+ return embedding
75
+
76
+
77
+ def calculate_bert_score_simple(reference: str,
78
+ candidate: str,
79
+ model_name: str = "demo") -> Dict:
80
+ """Calculate BERT-like score using simple embeddings.
81
+
82
+ Args:
83
+ reference: Ground truth text
84
+ candidate: Model prediction text
85
+ model_name: Model identifier (for demo purposes)
86
+
87
+ Returns:
88
+ Dictionary with precision, recall, F1
89
+ """
90
+ # Get embeddings
91
+ ref_embedding = simple_embedding(reference)
92
+ cand_embedding = simple_embedding(candidate)
93
+
94
+ # Calculate cosine similarity
95
+ similarity = cosine_similarity(ref_embedding, cand_embedding)
96
+
97
+ # Normalize to 0-1 range (cosine similarity is -1 to 1)
98
+ similarity = (similarity + 1) / 2
99
+
100
+ # For sentence-level similarity, precision = recall = F1
101
+ # BERT Score at token level would have different P/R
102
+ return {
103
+ "precision": similarity,
104
+ "recall": similarity,
105
+ "f1": similarity,
106
+ "similarity": similarity,
107
+ "cosine_similarity": similarity * 2 - 1, # Back to -1 to 1
108
+ "model": model_name,
109
+ "embedding_dim": len(ref_embedding),
110
+ "reference_tokens": len(tokenize(reference)),
111
+ "candidate_tokens": len(tokenize(candidate))
112
+ }
113
+
114
+
115
+ def calculate_token_level_similarity(reference: str,
116
+ candidate: str) -> Dict:
117
+ """Simulate token-level BERT Score with alignment visualization.
118
+
119
+ Args:
120
+ reference: Ground truth text
121
+ candidate: Model prediction text
122
+
123
+ Returns:
124
+ Dictionary with token-level alignment
125
+ """
126
+ ref_tokens = tokenize(reference)
127
+ cand_tokens = tokenize(candidate)
128
+
129
+ # Create token embeddings
130
+ ref_embeddings = [simple_embedding(token, 128) for token in ref_tokens]
131
+ cand_embeddings = [simple_embedding(token, 128) for token in cand_tokens]
132
+
133
+ # Calculate similarity matrix
134
+ similarity_matrix = np.zeros((len(ref_tokens), len(cand_tokens)))
135
+
136
+ for i, ref_emb in enumerate(ref_embeddings):
137
+ for j, cand_emb in enumerate(cand_embeddings):
138
+ sim = cosine_similarity(ref_emb, cand_emb)
139
+ # Normalize to 0-1
140
+ similarity_matrix[i, j] = (sim + 1) / 2
141
+
142
+ # Greedy matching for visualization
143
+ matches = []
144
+ used_cand = set()
145
+
146
+ for i in range(len(ref_tokens)):
147
+ best_match = -1
148
+ best_score = -1
149
+
150
+ for j in range(len(cand_tokens)):
151
+ if j not in used_cand and similarity_matrix[i, j] > best_score:
152
+ best_score = similarity_matrix[i, j]
153
+ best_match = j
154
+
155
+ if best_match >= 0 and best_score > 0.3: # Threshold
156
+ matches.append({
157
+ "ref_token": ref_tokens[i],
158
+ "ref_pos": i,
159
+ "cand_token": cand_tokens[best_match],
160
+ "cand_pos": best_match,
161
+ "similarity": best_score
162
+ })
163
+ used_cand.add(best_match)
164
+
165
+ # Calculate metrics from matches
166
+ if matches:
167
+ avg_similarity = sum(m["similarity"] for m in matches) / len(matches)
168
+ precision = len(matches) / len(cand_tokens) if cand_tokens else 0
169
+ recall = len(matches) / len(ref_tokens) if ref_tokens else 0
170
+ f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
171
+ else:
172
+ avg_similarity = 0
173
+ precision = recall = f1 = 0
174
+
175
+ return {
176
+ "precision": precision,
177
+ "recall": recall,
178
+ "f1": f1,
179
+ "avg_similarity": avg_similarity,
180
+ "similarity_matrix": similarity_matrix.tolist(),
181
+ "matches": matches,
182
+ "ref_tokens": ref_tokens,
183
+ "cand_tokens": cand_tokens,
184
+ "num_matches": len(matches)
185
+ }
186
+
187
+
188
+ def calculate_bert_score(reference: str,
189
+ candidate: str,
190
+ use_real_model: bool = False) -> Dict:
191
+ """Calculate BERT Score with fallback to simple implementation.
192
+
193
+ Args:
194
+ reference: Ground truth text
195
+ candidate: Model prediction text
196
+ use_real_model: Whether to try loading real sentence-transformers model
197
+
198
+ Returns:
199
+ Dictionary with BERT Score results
200
+ """
201
+ if use_real_model and SENTENCE_TRANSFORMERS_AVAILABLE:
202
+ try:
203
+ # Try to use real model
204
+ model = SentenceTransformer('all-MiniLM-L6-v2')
205
+ ref_emb = model.encode(reference)
206
+ cand_emb = model.encode(candidate)
207
+
208
+ similarity = cosine_similarity(ref_emb, cand_emb)
209
+ similarity = (similarity + 1) / 2
210
+
211
+ return {
212
+ "precision": similarity,
213
+ "recall": similarity,
214
+ "f1": similarity,
215
+ "similarity": similarity,
216
+ "model": "sentence-transformers/all-MiniLM-L6-v2",
217
+ "is_real_model": True
218
+ }
219
+ except Exception as e:
220
+ # Fall back to simple implementation
221
+ pass
222
+
223
+ # Use simple implementation
224
+ result = calculate_bert_score_simple(reference, candidate)
225
+ result["is_real_model"] = False
226
+ result["note"] = "Using simulation. Install sentence-transformers for real embeddings."
227
+
228
+ return result
229
+
230
+
231
+ def interpret_bert_score(f1: float) -> str:
232
+ """Interpret BERT Score F1 quality.
233
+
234
+ Args:
235
+ f1: BERT Score F1 (0-1)
236
+
237
+ Returns:
238
+ Quality description
239
+ """
240
+ if f1 >= 0.9:
241
+ return "Excellent (Semantically identical)"
242
+ elif f1 >= 0.7:
243
+ return "Good (Very similar meaning)"
244
+ elif f1 >= 0.5:
245
+ return "Fair (Related meaning)"
246
+ elif f1 >= 0.3:
247
+ return "Poor (Some semantic overlap)"
248
+ else:
249
+ return "Very Poor (Little semantic relation)"
250
+
251
+
252
+ def get_bert_score_grade(f1: float) -> str:
253
+ """Get letter grade for BERT Score.
254
+
255
+ Args:
256
+ f1: BERT Score F1
257
+
258
+ Returns:
259
+ Letter grade (A-F)
260
+ """
261
+ if f1 >= 0.85:
262
+ return "A"
263
+ elif f1 >= 0.75:
264
+ return "B"
265
+ elif f1 >= 0.60:
266
+ return "C"
267
+ elif f1 >= 0.40:
268
+ return "D"
269
+ else:
270
+ return "F"
modules/metrics/bleu.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """BLEU Score implementation with visualization support."""
2
+
3
+ import math
4
+ from typing import List, Tuple, Dict
5
+ from collections import Counter
6
+ from ..utils.text_processing import tokenize, count_ngrams
7
+
8
+
9
+ def calculate_bleu(reference: str, candidate: str, max_n: int = 4,
10
+ weights: List[float] = None) -> Dict:
11
+ """Calculate BLEU score with detailed breakdown.
12
+
13
+ BLEU = BP * exp(Σ w_n * log(p_n))
14
+
15
+ Args:
16
+ reference: Ground truth text
17
+ candidate: Model prediction text
18
+ max_n: Maximum n-gram size (default 4 for BLEU-4)
19
+ weights: Weights for each n-gram precision (default uniform)
20
+
21
+ Returns:
22
+ Dictionary with BLEU score and component breakdown
23
+ """
24
+ if weights is None:
25
+ weights = [1.0 / max_n] * max_n
26
+
27
+ # Tokenize
28
+ ref_tokens = tokenize(reference)
29
+ cand_tokens = tokenize(candidate)
30
+
31
+ # Calculate n-gram precisions
32
+ precisions = []
33
+ clipped_counts = []
34
+ total_counts = []
35
+
36
+ for n in range(1, max_n + 1):
37
+ # Count n-grams
38
+ ref_counts = count_ngrams(ref_tokens, n)
39
+ cand_counts = count_ngrams(cand_tokens, n)
40
+
41
+ # Calculate clipped counts
42
+ clipped_count = 0
43
+ total_count = 0
44
+
45
+ for ngram, count in cand_counts.items():
46
+ total_count += count
47
+ clipped_count += min(count, ref_counts.get(ngram, 0))
48
+
49
+ # Calculate precision
50
+ if total_count == 0:
51
+ precision = 0.0
52
+ else:
53
+ precision = clipped_count / total_count
54
+
55
+ precisions.append(precision)
56
+ clipped_counts.append(clipped_count)
57
+ total_counts.append(total_count)
58
+
59
+ # Calculate brevity penalty
60
+ ref_len = len(ref_tokens)
61
+ cand_len = len(cand_tokens)
62
+
63
+ if cand_len > ref_len:
64
+ bp = 1.0
65
+ elif cand_len == 0:
66
+ bp = 0.0
67
+ else:
68
+ bp = math.exp(1 - ref_len / cand_len)
69
+
70
+ # Calculate weighted log precision
71
+ log_precisions = [math.log(p) if p > 0 else -999 for p in precisions]
72
+ weighted_log_sum = sum(w * lp for w, lp in zip(weights, log_precisions))
73
+
74
+ # Final BLEU score
75
+ if all(p == 0 for p in precisions):
76
+ bleu = 0.0
77
+ else:
78
+ bleu = bp * math.exp(weighted_log_sum)
79
+
80
+ return {
81
+ "bleu": bleu,
82
+ "brevity_penalty": bp,
83
+ "precisions": precisions,
84
+ "clipped_counts": clipped_counts,
85
+ "total_counts": total_counts,
86
+ "ref_length": ref_len,
87
+ "cand_length": cand_len,
88
+ "weights": weights,
89
+ "geo_mean_precision": math.exp(weighted_log_sum) if any(p > 0 for p in precisions) else 0
90
+ }
91
+
92
+
93
+ def calculate_modified_precision(reference: str, candidate: str, n: int) -> Tuple[float, int, int]:
94
+ """Calculate modified n-gram precision for BLEU.
95
+
96
+ Args:
97
+ reference: Ground truth text
98
+ candidate: Model prediction text
99
+ n: N-gram size
100
+
101
+ Returns:
102
+ (precision, clipped_count, total_count)
103
+ """
104
+ ref_tokens = tokenize(reference)
105
+ cand_tokens = tokenize(candidate)
106
+
107
+ ref_counts = count_ngrams(ref_tokens, n)
108
+ cand_counts = count_ngrams(cand_tokens, n)
109
+
110
+ clipped_count = 0
111
+ total_count = 0
112
+
113
+ for ngram, count in cand_counts.items():
114
+ total_count += count
115
+ clipped_count += min(count, ref_counts.get(ngram, 0))
116
+
117
+ if total_count == 0:
118
+ return 0.0, 0, 0
119
+
120
+ return clipped_count / total_count, clipped_count, total_count
121
+
122
+
123
+ def get_matching_ngrams_detailed(reference: str, candidate: str, n: int) -> Dict:
124
+ """Get detailed matching information for visualization.
125
+
126
+ Args:
127
+ reference: Ground truth text
128
+ candidate: Model prediction text
129
+ n: N-gram size
130
+
131
+ Returns:
132
+ Dictionary with matching details
133
+ """
134
+ ref_tokens = tokenize(reference)
135
+ cand_tokens = tokenize(candidate)
136
+
137
+ ref_counts = count_ngrams(ref_tokens, n)
138
+ cand_counts = count_ngrams(cand_tokens, n)
139
+
140
+ matches = []
141
+ over_matches = []
142
+
143
+ for ngram, cand_count in cand_counts.items():
144
+ ref_count = ref_counts.get(ngram, 0)
145
+ if ref_count > 0:
146
+ matches.append({
147
+ "ngram": ngram,
148
+ "candidate_count": cand_count,
149
+ "reference_count": ref_count,
150
+ "clipped_count": min(cand_count, ref_count)
151
+ })
152
+
153
+ if cand_count > ref_count:
154
+ over_matches.append({
155
+ "ngram": ngram,
156
+ "candidate_count": cand_count,
157
+ "reference_count": ref_count,
158
+ "excess": cand_count - ref_count
159
+ })
160
+
161
+ # Non-matching n-grams
162
+ non_matches = []
163
+ for ngram, cand_count in cand_counts.items():
164
+ if ngram not in ref_counts:
165
+ non_matches.append({
166
+ "ngram": ngram,
167
+ "count": cand_count
168
+ })
169
+
170
+ return {
171
+ "matches": matches,
172
+ "over_matches": over_matches,
173
+ "non_matches": non_matches,
174
+ "ref_ngrams": list(ref_counts.keys()),
175
+ "cand_ngrams": list(cand_counts.keys()),
176
+ "total_ref_ngrams": len(ref_tokens) - n + 1 if len(ref_tokens) >= n else 0,
177
+ "total_cand_ngrams": len(cand_tokens) - n + 1 if len(cand_tokens) >= n else 0
178
+ }
179
+
180
+
181
+ def interpret_bleu_score(bleu: float) -> str:
182
+ """Interpret BLEU score quality.
183
+
184
+ Args:
185
+ bleu: BLEU score (0-1)
186
+
187
+ Returns:
188
+ Quality description
189
+ """
190
+ if bleu >= 0.8:
191
+ return "Excellent (Human-level quality)"
192
+ elif bleu >= 0.6:
193
+ return "Good (Understandable, minor errors)"
194
+ elif bleu >= 0.4:
195
+ return "Fair (Understandable but awkward)"
196
+ elif bleu >= 0.2:
197
+ return "Poor (Many errors, hard to understand)"
198
+ else:
199
+ return "Very Poor (Unintelligible)"
modules/metrics/mrr.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Mean Reciprocal Rank (MRR) metric implementation."""
2
+
3
+ from typing import List, Dict, Tuple, Union
4
+ import statistics
5
+
6
+
7
+ def calculate_reciprocal_rank(ranked_list: List[str],
8
+ correct_answer: str,
9
+ case_sensitive: bool = False) -> Dict:
10
+ """Calculate Reciprocal Rank for a single query.
11
+
12
+ RR = 1 / rank_of_correct_answer
13
+ If not found, RR = 0
14
+
15
+ Args:
16
+ ranked_list: List of ranked answers (best first)
17
+ correct_answer: The correct answer to find
18
+ case_sensitive: Whether matching is case-sensitive
19
+
20
+ Returns:
21
+ Dictionary with RR and rank information
22
+ """
23
+ if not case_sensitive:
24
+ ranked_list_lower = [item.lower() for item in ranked_list]
25
+ correct_answer_lower = correct_answer.lower()
26
+ search_list = ranked_list_lower
27
+ search_answer = correct_answer_lower
28
+ else:
29
+ search_list = ranked_list
30
+ search_answer = correct_answer
31
+
32
+ # Find rank of correct answer
33
+ rank = None
34
+ for i, item in enumerate(search_list, 1):
35
+ if item == search_answer:
36
+ rank = i
37
+ break
38
+
39
+ if rank is None:
40
+ return {
41
+ "reciprocal_rank": 0.0,
42
+ "rank": None,
43
+ "found": False,
44
+ "correct_answer": correct_answer,
45
+ "total_candidates": len(ranked_list)
46
+ }
47
+
48
+ return {
49
+ "reciprocal_rank": 1.0 / rank,
50
+ "rank": rank,
51
+ "found": True,
52
+ "correct_answer": correct_answer,
53
+ "total_candidates": len(ranked_list),
54
+ "top_k": rank <= 3 # Considered good if in top 3
55
+ }
56
+
57
+
58
+ def calculate_mrr(query_results: List[Dict]) -> Dict:
59
+ """Calculate Mean Reciprocal Rank across multiple queries.
60
+
61
+ MRR = (1/|Q|) * Σ (1/rank_i)
62
+
63
+ Args:
64
+ query_results: List of dictionaries containing:
65
+ - "question": The question text
66
+ - "ranked_answers": List of ranked answers
67
+ - "correct_answer": The correct answer
68
+
69
+ Returns:
70
+ Dictionary with MRR and breakdown
71
+ """
72
+ rr_scores = []
73
+ query_details = []
74
+
75
+ for result in query_results:
76
+ question = result.get("question", "")
77
+ ranked_answers = result.get("ranked_answers", [])
78
+ correct_answer = result.get("correct_answer", "")
79
+
80
+ rr_result = calculate_reciprocal_rank(ranked_answers, correct_answer)
81
+
82
+ rr_scores.append(rr_result["reciprocal_rank"])
83
+ query_details.append({
84
+ "question": question,
85
+ "correct_answer": correct_answer,
86
+ "rank": rr_result["rank"],
87
+ "reciprocal_rank": rr_result["reciprocal_rank"],
88
+ "found": rr_result["found"],
89
+ "ranked_answers": ranked_answers[:5] # Store top 5 for display
90
+ })
91
+
92
+ # Calculate MRR
93
+ mrr = statistics.mean(rr_scores) if rr_scores else 0.0
94
+
95
+ # Additional statistics
96
+ found_count = sum(1 for q in query_details if q["found"])
97
+ top1_count = sum(1 for q in query_details if q["rank"] == 1)
98
+ top3_count = sum(1 for q in query_details if q["rank"] and q["rank"] <= 3)
99
+
100
+ return {
101
+ "mrr": mrr,
102
+ "num_queries": len(query_results),
103
+ "reciprocal_ranks": rr_scores,
104
+ "query_details": query_details,
105
+ "found_rate": found_count / len(query_results) if query_results else 0,
106
+ "top1_accuracy": top1_count / len(query_results) if query_results else 0,
107
+ "top3_accuracy": top3_count / len(query_results) if query_results else 0,
108
+ "hits_at_1": top1_count,
109
+ "hits_at_3": top3_count
110
+ }
111
+
112
+
113
+ def calculate_mrr_simple(ranked_lists: List[List[str]],
114
+ correct_answers: List[str]) -> Dict:
115
+ """Simplified MRR calculation from parallel lists.
116
+
117
+ Args:
118
+ ranked_lists: List of ranked answer lists
119
+ correct_answers: List of correct answers (parallel to ranked_lists)
120
+
121
+ Returns:
122
+ Dictionary with MRR results
123
+ """
124
+ query_results = [
125
+ {
126
+ "question": f"Query {i+1}",
127
+ "ranked_answers": ranked,
128
+ "correct_answer": correct
129
+ }
130
+ for i, (ranked, correct) in enumerate(zip(ranked_lists, correct_answers))
131
+ ]
132
+
133
+ return calculate_mrr(query_results)
134
+
135
+
136
+ def interpret_mrr(mrr: float) -> str:
137
+ """Interpret MRR score quality.
138
+
139
+ MRR ranges from 0 to 1 (perfect ranking).
140
+
141
+ Args:
142
+ mrr: Mean Reciprocal Rank (0-1)
143
+
144
+ Returns:
145
+ Quality description
146
+ """
147
+ if mrr >= 0.8:
148
+ return "Excellent (Usually ranks correct answer at #1)"
149
+ elif mrr >= 0.6:
150
+ return "Good (Usually in top 2)"
151
+ elif mrr >= 0.4:
152
+ return "Fair (Usually in top 3)"
153
+ elif mrr >= 0.2:
154
+ return "Poor (Correct answer often buried)"
155
+ else:
156
+ return "Very Poor (Rarely finds correct answer)"
157
+
158
+
159
+ def get_mrr_grade(mrr: float) -> str:
160
+ """Get letter grade for MRR.
161
+
162
+ Args:
163
+ mrr: Mean Reciprocal Rank
164
+
165
+ Returns:
166
+ Letter grade (A-F)
167
+ """
168
+ if mrr >= 0.9:
169
+ return "A+"
170
+ elif mrr >= 0.8:
171
+ return "A"
172
+ elif mrr >= 0.7:
173
+ return "B+"
174
+ elif mrr >= 0.6:
175
+ return "B"
176
+ elif mrr >= 0.5:
177
+ return "C+"
178
+ elif mrr >= 0.4:
179
+ return "C"
180
+ elif mrr >= 0.3:
181
+ return "D"
182
+ else:
183
+ return "F"
184
+
185
+
186
+ def visualize_ranking(ranked_list: List[str],
187
+ correct_answer: str,
188
+ max_display: int = 5) -> List[Dict]:
189
+ """Create visualization data for a ranking.
190
+
191
+ Args:
192
+ ranked_list: List of ranked answers
193
+ correct_answer: The correct answer
194
+ max_display: Maximum items to display
195
+
196
+ Returns:
197
+ List of dictionaries with rank info for display
198
+ """
199
+ result = []
200
+ found_rank = None
201
+
202
+ for i, answer in enumerate(ranked_list[:max_display], 1):
203
+ is_correct = answer.lower() == correct_answer.lower()
204
+ if is_correct:
205
+ found_rank = i
206
+
207
+ result.append({
208
+ "rank": i,
209
+ "answer": answer,
210
+ "is_correct": is_correct,
211
+ "reciprocal_score": 1.0 / i if is_correct else 0.0
212
+ })
213
+
214
+ return result
modules/metrics/perplexity.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Perplexity metric implementation."""
2
+
3
+ import math
4
+ from typing import List, Dict, Tuple
5
+ from ..utils.text_processing import tokenize
6
+
7
+
8
+ def calculate_perplexity_from_probs(token_probs: List[float]) -> Dict:
9
+ """Calculate perplexity from token probabilities.
10
+
11
+ Perplexity = exp(-1/N * Σ log(P(token_i)))
12
+
13
+ Args:
14
+ token_probs: List of probabilities for each token (0 to 1)
15
+
16
+ Returns:
17
+ Dictionary with perplexity and components
18
+ """
19
+ if not token_probs or any(p <= 0 for p in token_probs):
20
+ return {
21
+ "perplexity": float('inf'),
22
+ "avg_log_prob": float('-inf'),
23
+ "num_tokens": len(token_probs),
24
+ "token_details": []
25
+ }
26
+
27
+ # Calculate log probabilities
28
+ log_probs = [math.log(p) for p in token_probs]
29
+ avg_log_prob = sum(log_probs) / len(log_probs)
30
+
31
+ # Calculate perplexity
32
+ perplexity = math.exp(-avg_log_prob)
33
+
34
+ # Token-level details
35
+ token_details = [
36
+ {
37
+ "token_index": i,
38
+ "probability": p,
39
+ "log_probability": lp,
40
+ "perplexity_contrib": math.exp(-lp)
41
+ }
42
+ for i, (p, lp) in enumerate(zip(token_probs, log_probs))
43
+ ]
44
+
45
+ return {
46
+ "perplexity": perplexity,
47
+ "avg_log_prob": avg_log_prob,
48
+ "num_tokens": len(token_probs),
49
+ "token_details": token_details,
50
+ "cross_entropy": -avg_log_prob
51
+ }
52
+
53
+
54
+ def simulate_token_probabilities(text: str,
55
+ base_confidence: float = 0.7,
56
+ variation: float = 0.2) -> List[float]:
57
+ """Simulate token probabilities for demonstration.
58
+
59
+ In real scenarios, these come from the language model's softmax output.
60
+ This is a simulation for educational purposes.
61
+
62
+ Args:
63
+ text: Input text
64
+ base_confidence: Average probability (0-1)
65
+ variation: How much probabilities vary
66
+
67
+ Returns:
68
+ List of simulated token probabilities
69
+ """
70
+ tokens = tokenize(text)
71
+ import random
72
+
73
+ # Set seed for reproducibility
74
+ random.seed(sum(ord(c) for c in text))
75
+
76
+ probs = []
77
+ for i, token in enumerate(tokens):
78
+ # Vary probability based on token characteristics
79
+ # Common words have higher probability
80
+ is_common = len(token) <= 4 and token.isalpha()
81
+
82
+ if is_common:
83
+ base = base_confidence + 0.1
84
+ else:
85
+ base = base_confidence - 0.1
86
+
87
+ # Add some randomness
88
+ prob = base + random.uniform(-variation, variation)
89
+ prob = max(0.01, min(0.99, prob)) # Clamp between 0.01 and 0.99
90
+
91
+ probs.append(prob)
92
+
93
+ return probs
94
+
95
+
96
+ def calculate_perplexity_approximation(text: str,
97
+ model_confidence: str = "medium") -> Dict:
98
+ """Calculate approximate perplexity for demonstration.
99
+
100
+ Args:
101
+ text: Input text
102
+ model_confidence: "high", "medium", or "low"
103
+
104
+ Returns:
105
+ Dictionary with perplexity and token breakdown
106
+ """
107
+ tokens = tokenize(text)
108
+
109
+ # Map confidence to base probability
110
+ confidence_map = {
111
+ "high": 0.85,
112
+ "medium": 0.65,
113
+ "low": 0.40
114
+ }
115
+
116
+ base_confidence = confidence_map.get(model_confidence, 0.65)
117
+
118
+ # Generate simulated probabilities
119
+ token_probs = simulate_token_probabilities(text, base_confidence, 0.15)
120
+
121
+ # Calculate perplexity
122
+ result = calculate_perplexity_from_probs(token_probs)
123
+
124
+ # Add token text for display
125
+ for i, token in enumerate(tokens):
126
+ if i < len(result["token_details"]):
127
+ result["token_details"][i]["token"] = token
128
+
129
+ return result
130
+
131
+
132
+ def interpret_perplexity(perplexity: float) -> str:
133
+ """Interpret perplexity score.
134
+
135
+ Lower is better. Perplexity = number of choices the model had at each step.
136
+
137
+ Args:
138
+ perplexity: Perplexity score (>= 1)
139
+
140
+ Returns:
141
+ Interpretation string
142
+ """
143
+ if perplexity < 10:
144
+ return "Excellent (Model is very confident, almost deterministic)"
145
+ elif perplexity < 50:
146
+ return "Good (Model is confident with minor uncertainty)"
147
+ elif perplexity < 100:
148
+ return "Fair (Model has moderate uncertainty)"
149
+ elif perplexity < 500:
150
+ return "Poor (Model is often confused)"
151
+ else:
152
+ return "Very Poor (Model is very confused, near random)"
153
+
154
+
155
+ def get_perplexity_grade(perplexity: float) -> str:
156
+ """Get letter grade for perplexity.
157
+
158
+ Args:
159
+ perplexity: Perplexity score
160
+
161
+ Returns:
162
+ Letter grade (A-F)
163
+ """
164
+ if perplexity < 20:
165
+ return "A"
166
+ elif perplexity < 50:
167
+ return "B"
168
+ elif perplexity < 100:
169
+ return "C"
170
+ elif perplexity < 200:
171
+ return "D"
172
+ else:
173
+ return "F"
174
+
175
+
176
+ def calculate_perplexity_with_external_model(text: str,
177
+ api_provider: str = "openai") -> Dict:
178
+ """Placeholder for real API-based perplexity calculation.
179
+
180
+ Note: Most LLM APIs don't expose raw token probabilities.
181
+ This would require using a local model or special API endpoints.
182
+
183
+ Args:
184
+ text: Input text
185
+ api_provider: API provider name
186
+
187
+ Returns:
188
+ Dictionary with perplexity (placeholder)
189
+ """
190
+ # Placeholder - real implementation would call API or local model
191
+ return {
192
+ "perplexity": None,
193
+ "note": f"Real perplexity calculation requires local model or {api_provider} logprobs API",
194
+ "simulation_available": True
195
+ }
modules/metrics/rogue.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ROGUE Score implementation with 5 variants."""
2
+
3
+ from typing import List, Tuple, Dict
4
+ from collections import Counter
5
+ import math
6
+ from ..utils.text_processing import tokenize, get_ngrams, get_skip_bigrams
7
+
8
+
9
+ def longest_common_subsequence(X: List[str], Y: List[str]) -> List[str]:
10
+ """Find the Longest Common Subsequence (LCS) between two sequences.
11
+
12
+ Args:
13
+ X: First sequence
14
+ Y: Second sequence
15
+
16
+ Returns:
17
+ The LCS as a list of elements
18
+ """
19
+ m, n = len(X), len(Y)
20
+
21
+ # Build LCS table
22
+ L = [[0] * (n + 1) for _ in range(m + 1)]
23
+
24
+ for i in range(1, m + 1):
25
+ for j in range(1, n + 1):
26
+ if X[i-1] == Y[j-1]:
27
+ L[i][j] = L[i-1][j-1] + 1
28
+ else:
29
+ L[i][j] = max(L[i-1][j], L[i][j-1])
30
+
31
+ # Backtrack to find LCS
32
+ lcs = []
33
+ i, j = m, n
34
+ while i > 0 and j > 0:
35
+ if X[i-1] == Y[j-1]:
36
+ lcs.append(X[i-1])
37
+ i -= 1
38
+ j -= 1
39
+ elif L[i-1][j] > L[i][j-1]:
40
+ i -= 1
41
+ else:
42
+ j -= 1
43
+
44
+ return lcs[::-1]
45
+
46
+
47
+ def weighted_lcs(X: List[str], Y: List[str], weight_func=None) -> Tuple[int, float]:
48
+ """Calculate Weighted Longest Common Subsequence (ROGUE-W).
49
+
50
+ Args:
51
+ X: First sequence
52
+ Y: Second sequence
53
+ weight_func: Function to calculate weight based on consecutive length
54
+
55
+ Returns:
56
+ (lcs_length, weighted_lcs_score)
57
+ """
58
+ if weight_func is None:
59
+ weight_func = lambda x: x ** 1.2 # Default: f(k) = k^1.2
60
+
61
+ lcs = longest_common_subsequence(X, Y)
62
+
63
+ if not lcs:
64
+ return 0, 0.0
65
+
66
+ # Find consecutive sequences in LCS
67
+ consecutive_lengths = []
68
+ current_len = 1
69
+
70
+ for i in range(1, len(lcs)):
71
+ # Check if consecutive in both sequences
72
+ try:
73
+ idx1_x = X.index(lcs[i-1])
74
+ idx2_x = X.index(lcs[i])
75
+ idx1_y = Y.index(lcs[i-1])
76
+ idx2_y = Y.index(lcs[i])
77
+
78
+ if idx2_x == idx1_x + 1 and idx2_y == idx1_y + 1:
79
+ current_len += 1
80
+ else:
81
+ consecutive_lengths.append(current_len)
82
+ current_len = 1
83
+ except ValueError:
84
+ consecutive_lengths.append(current_len)
85
+ current_len = 1
86
+
87
+ consecutive_lengths.append(current_len)
88
+
89
+ # Calculate weighted LCS
90
+ wlcs = sum(weight_func(length) for length in consecutive_lengths)
91
+
92
+ return len(lcs), wlcs
93
+
94
+
95
+ def calculate_rogue_n(reference: str, candidate: str, n: int = 1) -> Dict:
96
+ """Calculate ROGUE-N (n-gram based).
97
+
98
+ Args:
99
+ reference: Ground truth text
100
+ candidate: Model prediction text
101
+ n: N-gram size (1 for unigram, 2 for bigram)
102
+
103
+ Returns:
104
+ Dictionary with precision, recall, F1
105
+ """
106
+ ref_tokens = tokenize(reference)
107
+ cand_tokens = tokenize(candidate)
108
+
109
+ # Get n-grams
110
+ ref_ngrams = get_ngrams(ref_tokens, n)
111
+ cand_ngrams = get_ngrams(cand_tokens, n)
112
+
113
+ if not ref_ngrams or not cand_ngrams:
114
+ return {"precision": 0.0, "recall": 0.0, "f1": 0.0, "matches": 0}
115
+
116
+ ref_counts = Counter(ref_ngrams)
117
+ cand_counts = Counter(cand_ngrams)
118
+
119
+ # Count matches
120
+ matches = 0
121
+ for ngram in cand_counts:
122
+ matches += min(cand_counts[ngram], ref_counts.get(ngram, 0))
123
+
124
+ # Calculate metrics
125
+ recall = matches / len(ref_ngrams) if ref_ngrams else 0.0
126
+ precision = matches / len(cand_ngrams) if cand_ngrams else 0.0
127
+ f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0
128
+
129
+ return {
130
+ "precision": precision,
131
+ "recall": recall,
132
+ "f1": f1,
133
+ "matches": matches,
134
+ "ref_ngrams": len(ref_ngrams),
135
+ "cand_ngrams": len(cand_ngrams)
136
+ }
137
+
138
+
139
+ def calculate_rogue_l(reference: str, candidate: str) -> Dict:
140
+ """Calculate ROGUE-L (Longest Common Subsequence based).
141
+
142
+ Args:
143
+ reference: Ground truth text
144
+ candidate: Model prediction text
145
+
146
+ Returns:
147
+ Dictionary with precision, recall, F1
148
+ """
149
+ ref_tokens = tokenize(reference)
150
+ cand_tokens = tokenize(candidate)
151
+
152
+ lcs = longest_common_subsequence(ref_tokens, cand_tokens)
153
+ lcs_len = len(lcs)
154
+
155
+ if lcs_len == 0:
156
+ return {"precision": 0.0, "recall": 0.0, "f1": 0.0, "lcs": []}
157
+
158
+ recall = lcs_len / len(ref_tokens) if ref_tokens else 0.0
159
+ precision = lcs_len / len(cand_tokens) if cand_tokens else 0.0
160
+ f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0
161
+
162
+ return {
163
+ "precision": precision,
164
+ "recall": recall,
165
+ "f1": f1,
166
+ "lcs": lcs,
167
+ "lcs_length": lcs_len,
168
+ "ref_length": len(ref_tokens),
169
+ "cand_length": len(cand_tokens)
170
+ }
171
+
172
+
173
+ def calculate_rogue_s(reference: str, candidate: str, max_skip: int = 2) -> Dict:
174
+ """Calculate ROGUE-S (Skip-bigram based).
175
+
176
+ Args:
177
+ reference: Ground truth text
178
+ candidate: Model prediction text
179
+ max_skip: Maximum number of words to skip
180
+
181
+ Returns:
182
+ Dictionary with precision, recall, F1
183
+ """
184
+ ref_tokens = tokenize(reference)
185
+ cand_tokens = tokenize(candidate)
186
+
187
+ # Get skip-bigrams
188
+ ref_skip_bigrams = get_skip_bigrams(ref_tokens, max_skip)
189
+ cand_skip_bigrams = get_skip_bigrams(cand_tokens, max_skip)
190
+
191
+ if not ref_skip_bigrams or not cand_skip_bigrams:
192
+ return {"precision": 0.0, "recall": 0.0, "f1": 0.0, "matches": 0}
193
+
194
+ ref_counts = Counter(ref_skip_bigrams)
195
+ cand_counts = Counter(cand_skip_bigrams)
196
+
197
+ # Count matches
198
+ matches = 0
199
+ for bigram in cand_counts:
200
+ matches += min(cand_counts[bigram], ref_counts.get(bigram, 0))
201
+
202
+ # Calculate metrics
203
+ recall = matches / len(ref_skip_bigrams) if ref_skip_bigrams else 0.0
204
+ precision = matches / len(cand_skip_bigrams) if cand_skip_bigrams else 0.0
205
+ f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0
206
+
207
+ return {
208
+ "precision": precision,
209
+ "recall": recall,
210
+ "f1": f1,
211
+ "matches": matches,
212
+ "ref_skip_bigrams": len(ref_skip_bigrams),
213
+ "cand_skip_bigrams": len(cand_skip_bigrams)
214
+ }
215
+
216
+
217
+ def calculate_rogue_su(reference: str, candidate: str, max_skip: int = 2) -> Dict:
218
+ """Calculate ROGUE-SU (Skip-bigram + Unigram combined).
219
+
220
+ This combines unigrams with skip-bigrams to avoid the weakness of ROGUE-S
221
+ (which gives 0 when word order is completely reversed).
222
+
223
+ Args:
224
+ reference: Ground truth text
225
+ candidate: Model prediction text
226
+ max_skip: Maximum number of words to skip
227
+
228
+ Returns:
229
+ Dictionary with precision, recall, F1
230
+ """
231
+ # Get ROGUE-1 (unigram) and ROGUE-S
232
+ rogue1 = calculate_rogue_n(reference, candidate, 1)
233
+ rogue_s = calculate_rogue_s(reference, candidate, max_skip)
234
+
235
+ # Combine: add unigram matches to skip-bigram matches
236
+ ref_tokens = tokenize(reference)
237
+ cand_tokens = tokenize(candidate)
238
+
239
+ ref_unigrams = get_ngrams(ref_tokens, 1)
240
+ cand_unigrams = get_ngrams(cand_tokens, 1)
241
+ ref_skip_bigrams = get_skip_bigrams(ref_tokens, max_skip)
242
+ cand_skip_bigrams = get_skip_bigrams(cand_tokens, max_skip)
243
+
244
+ # Total counts
245
+ total_ref = len(ref_unigrams) + len(ref_skip_bigrams)
246
+ total_cand = len(cand_unigrams) + len(cand_skip_bigrams)
247
+
248
+ # Combined matches
249
+ combined_matches = rogue1["matches"] + rogue_s["matches"]
250
+
251
+ # Calculate combined metrics
252
+ recall = combined_matches / total_ref if total_ref > 0 else 0.0
253
+ precision = combined_matches / total_cand if total_cand > 0 else 0.0
254
+ f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0
255
+
256
+ return {
257
+ "precision": precision,
258
+ "recall": recall,
259
+ "f1": f1,
260
+ "rogue1": rogue1,
261
+ "rogue_s": rogue_s,
262
+ "combined_matches": combined_matches
263
+ }
264
+
265
+
266
+ def calculate_rogue_w(reference: str, candidate: str, weight_exponent: float = 1.2) -> Dict:
267
+ """Calculate ROGUE-W (Weighted Longest Common Subsequence).
268
+
269
+ Rewards consecutive matches more heavily than scattered matches.
270
+
271
+ Args:
272
+ reference: Ground truth text
273
+ candidate: Model prediction text
274
+ weight_exponent: Exponent for weight function (default 1.2)
275
+
276
+ Returns:
277
+ Dictionary with precision, recall, F1
278
+ """
279
+ ref_tokens = tokenize(reference)
280
+ cand_tokens = tokenize(candidate)
281
+
282
+ weight_func = lambda x: x ** weight_exponent
283
+ lcs_len, wlcs = weighted_lcs(ref_tokens, cand_tokens, weight_func)
284
+
285
+ if wlcs == 0:
286
+ return {"precision": 0.0, "recall": 0.0, "f1": 0.0, "wlcs": 0}
287
+
288
+ # Calculate f^-1 for normalization
289
+ # For f(k) = k^p, f^-1(x) = x^(1/p)
290
+ f_inv = lambda x: x ** (1.0 / weight_exponent)
291
+
292
+ # ROGUE-W recall and precision
293
+ recall_denom = f_inv(sum(weight_func(len(seq)) for seq in [[t] for t in ref_tokens]))
294
+ precision_denom = f_inv(sum(weight_func(len(seq)) for seq in [[t] for t in cand_tokens]))
295
+
296
+ recall = f_inv(wlcs) / len(ref_tokens) if ref_tokens else 0.0
297
+ precision = f_inv(wlcs) / len(cand_tokens) if cand_tokens else 0.0
298
+
299
+ # Alternative calculation as used in paper
300
+ recall_alt = wlcs / sum(weight_func(len(seq)) for seq in [[t] for t in ref_tokens]) if ref_tokens else 0.0
301
+
302
+ f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0
303
+
304
+ return {
305
+ "precision": precision,
306
+ "recall": recall,
307
+ "f1": f1,
308
+ "recall_alt": recall_alt,
309
+ "wlcs": wlcs,
310
+ "lcs_length": lcs_len,
311
+ "ref_length": len(ref_tokens),
312
+ "cand_length": len(cand_tokens)
313
+ }
314
+
315
+
316
+ def calculate_all_rogue(reference: str, candidate: str) -> Dict:
317
+ """Calculate all ROGUE variants.
318
+
319
+ Args:
320
+ reference: Ground truth text
321
+ candidate: Model prediction text
322
+
323
+ Returns:
324
+ Dictionary with all ROGUE scores
325
+ """
326
+ return {
327
+ "rogue1": calculate_rogue_n(reference, candidate, 1),
328
+ "rogue2": calculate_rogue_n(reference, candidate, 2),
329
+ "rogueL": calculate_rogue_l(reference, candidate),
330
+ "rogueS": calculate_rogue_s(reference, candidate),
331
+ "rogueSU": calculate_rogue_su(reference, candidate),
332
+ "rogueW": calculate_rogue_w(reference, candidate)
333
+ }
334
+
335
+
336
+ def interpret_rogue_score(f1: float) -> str:
337
+ """Interpret ROGUE F1 score quality.
338
+
339
+ Args:
340
+ f1: ROGUE F1 score (0-1)
341
+
342
+ Returns:
343
+ Quality description
344
+ """
345
+ if f1 >= 0.5:
346
+ return "Good (Captures most key points)"
347
+ elif f1 >= 0.3:
348
+ return "Fair (Captures some key points)"
349
+ elif f1 >= 0.1:
350
+ return "Poor (Misses most key points)"
351
+ else:
352
+ return "Very Poor (Little to no overlap)"
modules/utils/__init__.py ADDED
File without changes
modules/utils/examples.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Preset examples for metric demonstrations."""
2
+
3
+ from typing import List, Dict, Tuple
4
+
5
+
6
+ class EvaluationExample:
7
+ """Represents a ground truth / prediction pair example."""
8
+
9
+ def __init__(self, name: str, ground_truth: str, prediction: str,
10
+ description: str = "", tags: List[str] = None):
11
+ self.name = name
12
+ self.ground_truth = ground_truth
13
+ self.prediction = prediction
14
+ self.description = description
15
+ self.tags = tags or []
16
+
17
+
18
+ # Text comparison examples
19
+ TEXT_EXAMPLES = [
20
+ EvaluationExample(
21
+ name="Exact Match",
22
+ ground_truth="The cat sat on the mat",
23
+ prediction="The cat sat on the mat",
24
+ description="Perfect match - all metrics should be 1.0",
25
+ tags=["exact", "perfect"]
26
+ ),
27
+ EvaluationExample(
28
+ name="Paraphrase (Synonyms)",
29
+ ground_truth="The cat sat on the mat",
30
+ prediction="The cat was sitting on the mat",
31
+ description="Same meaning, different words - good BERT Score, lower BLEU/ROGUE",
32
+ tags=["paraphrase", "semantic"]
33
+ ),
34
+ EvaluationExample(
35
+ name="Partial Match",
36
+ ground_truth="The cat sat on the mat and looked outside",
37
+ prediction="The cat sat on the mat",
38
+ description="Incomplete prediction - brevity penalty applies",
39
+ tags=["partial", "incomplete"]
40
+ ),
41
+ EvaluationExample(
42
+ name="Wrong Answer",
43
+ ground_truth="Paris is the capital of France",
44
+ prediction="Berlin is the capital of France",
45
+ description="Completely wrong but similar structure",
46
+ tags=["wrong", "incorrect"]
47
+ ),
48
+ EvaluationExample(
49
+ name="Extra Content",
50
+ ground_truth="The cat sat on the mat",
51
+ prediction="The cat sat on the mat and then jumped off quickly",
52
+ description="Extra words added - recall will suffer",
53
+ tags=["extra", "verbose"]
54
+ ),
55
+ EvaluationExample(
56
+ name="Word Order Changed",
57
+ ground_truth="The cat sat on the mat",
58
+ prediction="On the mat sat the cat",
59
+ description="Same words, different order - ROGUE-L should handle this",
60
+ tags=["reorder", "scrambled"]
61
+ ),
62
+ EvaluationExample(
63
+ name="Long Text - Translation",
64
+ ground_truth="Machine translation has revolutionized how we communicate across languages, enabling instant understanding of foreign texts.",
65
+ prediction="Machine translation has changed how we communicate between languages, allowing instant comprehension of foreign texts.",
66
+ description="Realistic translation scenario",
67
+ tags=["translation", "long"]
68
+ ),
69
+ EvaluationExample(
70
+ name="Summarization - Key Points",
71
+ ground_truth="The research paper demonstrates that neural networks can effectively predict protein structures with high accuracy, potentially revolutionizing drug discovery and biological research.",
72
+ prediction="Neural networks can predict protein structures accurately, which could transform drug discovery.",
73
+ description="Abstractive summarization - ROGUE should capture key points",
74
+ tags=["summarization", "abstractive"]
75
+ ),
76
+ ]
77
+
78
+ # MRR-specific examples (question-answering format)
79
+ MRR_EXAMPLES = [
80
+ {
81
+ "name": "Correct at Position 1",
82
+ "question": "What is the capital of France?",
83
+ "ranked_answers": ["Paris", "London", "Berlin", "Madrid"],
84
+ "correct_answer": "Paris",
85
+ "description": "Perfect ranking - MRR = 1.0"
86
+ },
87
+ {
88
+ "name": "Correct at Position 2",
89
+ "question": "What is the capital of France?",
90
+ "ranked_answers": ["London", "Paris", "Berlin", "Madrid"],
91
+ "correct_answer": "Paris",
92
+ "description": "Good ranking - MRR = 0.5"
93
+ },
94
+ {
95
+ "name": "Correct at Position 3",
96
+ "question": "What is the capital of France?",
97
+ "ranked_answers": ["London", "Berlin", "Paris", "Madrid"],
98
+ "correct_answer": "Paris",
99
+ "description": "Average ranking - MRR = 0.33"
100
+ },
101
+ {
102
+ "name": "Not in Top 3",
103
+ "question": "What is the capital of France?",
104
+ "ranked_answers": ["London", "Berlin", "Madrid", "Paris"],
105
+ "correct_answer": "Paris",
106
+ "description": "Poor ranking - MRR = 0.25"
107
+ },
108
+ {
109
+ "name": "Multiple Questions Batch",
110
+ "questions": [
111
+ {"q": "Capital of France?", "ranked": ["Paris", "London", "Berlin"], "correct": "Paris"},
112
+ {"q": "Capital of Japan?", "ranked": ["Beijing", "Tokyo", "Seoul"], "correct": "Tokyo"},
113
+ {"q": "Capital of UK?", "ranked": ["London", "Paris", "Berlin"], "correct": "London"},
114
+ ],
115
+ "description": "Batch evaluation - calculates mean across multiple questions"
116
+ }
117
+ ]
118
+
119
+
120
+ def get_text_example_names() -> List[str]:
121
+ """Get list of available text example names."""
122
+ return [ex.name for ex in TEXT_EXAMPLES]
123
+
124
+
125
+ def get_text_example(name: str) -> EvaluationExample:
126
+ """Get a specific text example by name.
127
+
128
+ Args:
129
+ name: Example name
130
+
131
+ Returns:
132
+ EvaluationExample object
133
+ """
134
+ for ex in TEXT_EXAMPLES:
135
+ if ex.name == name:
136
+ return ex
137
+ return TEXT_EXAMPLES[0]
138
+
139
+
140
+ def get_mrr_example_names() -> List[str]:
141
+ """Get list of available MRR example names."""
142
+ return [ex["name"] for ex in MRR_EXAMPLES]
143
+
144
+
145
+ def get_mrr_example(name: str) -> Dict:
146
+ """Get a specific MRR example by name."""
147
+ for ex in MRR_EXAMPLES:
148
+ if ex["name"] == name:
149
+ return ex
150
+ return MRR_EXAMPLES[0]
modules/utils/text_processing.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Text processing utilities for tokenization and n-gram generation."""
2
+
3
+ import re
4
+ from typing import List, Tuple, Dict
5
+ import nltk
6
+ from collections import Counter
7
+
8
+ # Download required NLTK data
9
+ try:
10
+ nltk.data.find('tokenizers/punkt')
11
+ except LookupError:
12
+ nltk.download('punkt', quiet=True)
13
+
14
+
15
+ def tokenize(text: str, lowercase: bool = True) -> List[str]:
16
+ """Tokenize text into words.
17
+
18
+ Args:
19
+ text: Input text string
20
+ lowercase: Whether to convert to lowercase
21
+
22
+ Returns:
23
+ List of tokens
24
+ """
25
+ if lowercase:
26
+ text = text.lower()
27
+ # Simple word tokenization
28
+ tokens = re.findall(r'\b\w+\b', text)
29
+ return tokens
30
+
31
+
32
+ def get_ngrams(tokens: List[str], n: int) -> List[Tuple[str, ...]]:
33
+ """Generate n-grams from token list.
34
+
35
+ Args:
36
+ tokens: List of tokens
37
+ n: N-gram size
38
+
39
+ Returns:
40
+ List of n-gram tuples
41
+ """
42
+ if n > len(tokens):
43
+ return []
44
+ return [tuple(tokens[i:i+n]) for i in range(len(tokens) - n + 1)]
45
+
46
+
47
+ def get_all_ngrams(tokens: List[str], max_n: int = 4) -> Dict[int, List[Tuple[str, ...]]]:
48
+ """Generate all n-grams up to max_n.
49
+
50
+ Args:
51
+ tokens: List of tokens
52
+ max_n: Maximum n-gram size
53
+
54
+ Returns:
55
+ Dictionary mapping n to list of n-grams
56
+ """
57
+ return {n: get_ngrams(tokens, n) for n in range(1, max_n + 1)}
58
+
59
+
60
+ def count_ngrams(tokens: List[str], n: int) -> Counter:
61
+ """Count n-gram occurrences.
62
+
63
+ Args:
64
+ tokens: List of tokens
65
+ n: N-gram size
66
+
67
+ Returns:
68
+ Counter of n-gram frequencies
69
+ """
70
+ ngrams = get_ngrams(tokens, n)
71
+ return Counter(ngrams)
72
+
73
+
74
+ def find_matching_ngrams(ref_tokens: List[str], cand_tokens: List[str], n: int) -> List[Tuple[Tuple[str, ...], int, int]]:
75
+ """Find matching n-grams between reference and candidate.
76
+
77
+ Args:
78
+ ref_tokens: Reference tokens
79
+ cand_tokens: Candidate tokens
80
+ n: N-gram size
81
+
82
+ Returns:
83
+ List of (ngram, ref_pos, cand_pos) tuples
84
+ """
85
+ ref_ngrams = get_ngrams(ref_tokens, n)
86
+ cand_ngrams = get_ngrams(cand_tokens, n)
87
+
88
+ matches = []
89
+ for i, ref_ngram in enumerate(ref_ngrams):
90
+ for j, cand_ngram in enumerate(cand_ngrams):
91
+ if ref_ngram == cand_ngram:
92
+ matches.append((ref_ngram, i, j))
93
+
94
+ return matches
95
+
96
+
97
+ def get_skip_bigrams(tokens: List[str], max_skip: int = 2) -> List[Tuple[str, str]]:
98
+ """Generate skip-bigrams (pairs with gaps).
99
+
100
+ Args:
101
+ tokens: List of tokens
102
+ max_skip: Maximum number of tokens to skip
103
+
104
+ Returns:
105
+ List of skip-bigram tuples
106
+ """
107
+ skip_bigrams = []
108
+ for i in range(len(tokens)):
109
+ for j in range(i + 1, min(i + max_skip + 2, len(tokens))):
110
+ skip_bigrams.append((tokens[i], tokens[j]))
111
+ return skip_bigrams
112
+
113
+
114
+ def highlight_matching_ngrams(text: str, matches: List[Tuple[Tuple[str, ...], int, int]],
115
+ color: str = "#90EE90") -> str:
116
+ """Highlight matching n-grams in text with HTML.
117
+
118
+ Args:
119
+ text: Original text
120
+ matches: List of (ngram, start_pos, end_pos) - end_pos is length in tokens
121
+ color: Highlight color
122
+
123
+ Returns:
124
+ HTML string with highlights
125
+ """
126
+ tokens = tokenize(text, lowercase=False)
127
+ if not matches:
128
+ return text
129
+
130
+ # Sort matches by position
131
+ matches = sorted(matches, key=lambda x: x[1])
132
+
133
+ # Build highlighted text
134
+ result = []
135
+ last_end = 0
136
+
137
+ for ngram, start, _ in matches:
138
+ # Add text before match
139
+ if start > last_end:
140
+ result.append(" ".join(tokens[last_end:start]))
141
+
142
+ # Add highlighted match
143
+ ngram_text = " ".join(ngram)
144
+ result.append(f'<span style="background-color: {color}; padding: 2px; border-radius: 3px;">{ngram_text}</span>')
145
+ last_end = start + len(ngram)
146
+
147
+ # Add remaining text
148
+ if last_end < len(tokens):
149
+ result.append(" ".join(tokens[last_end:]))
150
+
151
+ return " ".join(result)
modules/visualizations/__init__.py ADDED
File without changes
modules/visualizations/charts.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Chart visualization utilities for metrics."""
2
+
3
+ import plotly.graph_objects as go
4
+ import plotly.express as px
5
+ from plotly.subplots import make_subplots
6
+ import numpy as np
7
+ from typing import List, Dict, Any
8
+
9
+
10
+ def create_radar_chart(metric_scores: Dict[str, float],
11
+ title: str = "Metric Comparison") -> go.Figure:
12
+ """Create a radar chart comparing multiple metrics.
13
+
14
+ Args:
15
+ metric_scores: Dictionary of metric name -> score (0-1)
16
+ title: Chart title
17
+
18
+ Returns:
19
+ Plotly figure object
20
+ """
21
+ categories = list(metric_scores.keys())
22
+ values = list(metric_scores.values())
23
+
24
+ fig = go.Figure(data=go.Scatterpolar(
25
+ r=values + [values[0]], # Close the polygon
26
+ theta=categories + [categories[0]],
27
+ fill='toself',
28
+ fillcolor='rgba(100, 149, 237, 0.3)',
29
+ line=dict(color='rgb(100, 149, 237)', width=2),
30
+ name='Scores'
31
+ ))
32
+
33
+ fig.update_layout(
34
+ polar=dict(
35
+ radialaxis=dict(
36
+ visible=True,
37
+ range=[0, 1],
38
+ tickformat='.2f'
39
+ )
40
+ ),
41
+ showlegend=False,
42
+ title=title,
43
+ height=500
44
+ )
45
+
46
+ return fig
47
+
48
+
49
+ def create_bar_chart(data: Dict[str, float],
50
+ title: str = "Scores",
51
+ color_scale: str = "Blues") -> go.Figure:
52
+ """Create a bar chart for metric scores.
53
+
54
+ Args:
55
+ data: Dictionary of label -> value
56
+ title: Chart title
57
+ color_scale: Plotly color scale name
58
+
59
+ Returns:
60
+ Plotly figure object
61
+ """
62
+ labels = list(data.keys())
63
+ values = list(data.values())
64
+
65
+ colors = px.colors.sequential.Blues[2:2+len(labels)]
66
+
67
+ fig = go.Figure(data=[go.Bar(
68
+ x=labels,
69
+ y=values,
70
+ marker_color=colors,
71
+ text=[f'{v:.3f}' for v in values],
72
+ textposition='auto'
73
+ )])
74
+
75
+ fig.update_layout(
76
+ title=title,
77
+ yaxis=dict(range=[0, 1], title="Score"),
78
+ xaxis_title="Metric",
79
+ height=400,
80
+ showlegend=False
81
+ )
82
+
83
+ return fig
84
+
85
+
86
+ def create_ngram_precision_chart(precisions: List[float],
87
+ clipped_counts: List[int],
88
+ total_counts: List[int]) -> go.Figure:
89
+ """Create chart showing n-gram precision breakdown.
90
+
91
+ Args:
92
+ precisions: List of precision values for n=1,2,3,4
93
+ clipped_counts: List of clipped n-gram counts
94
+ total_counts: List of total n-gram counts
95
+
96
+ Returns:
97
+ Plotly figure object
98
+ """
99
+ n_values = [f"{i+1}-gram" for i in range(len(precisions))]
100
+
101
+ fig = make_subplots(
102
+ rows=1, cols=2,
103
+ subplot_titles=("N-gram Precision", "Match Counts"),
104
+ specs=[[{"type": "bar"}, {"type": "bar"}]]
105
+ )
106
+
107
+ # Precision bars
108
+ fig.add_trace(
109
+ go.Bar(x=n_values, y=precisions,
110
+ text=[f'{p:.3f}' for p in precisions],
111
+ textposition='auto',
112
+ marker_color='lightblue',
113
+ name="Precision"),
114
+ row=1, col=1
115
+ )
116
+
117
+ # Match counts
118
+ fig.add_trace(
119
+ go.Bar(x=n_values, y=clipped_counts,
120
+ text=clipped_counts,
121
+ textposition='auto',
122
+ marker_color='lightgreen',
123
+ name="Clipped"),
124
+ row=1, col=2
125
+ )
126
+
127
+ fig.add_trace(
128
+ go.Bar(x=n_values, y=[t - c for t, c in zip(total_counts, clipped_counts)],
129
+ text=[t - c for t, c in zip(total_counts, clipped_counts)],
130
+ textposition='auto',
131
+ marker_color='lightcoral',
132
+ name="Non-matching"),
133
+ row=1, col=2
134
+ )
135
+
136
+ fig.update_layout(
137
+ barmode='stack',
138
+ height=400,
139
+ showlegend=True,
140
+ title_text="BLEU N-gram Analysis"
141
+ )
142
+
143
+ fig.update_yaxes(title_text="Precision", range=[0, 1], row=1, col=1)
144
+ fig.update_yaxes(title_text="Count", row=1, col=2)
145
+
146
+ return fig
147
+
148
+
149
+ def create_heatmap(similarity_matrix: List[List[float]],
150
+ x_labels: List[str],
151
+ y_labels: List[str],
152
+ title: str = "Similarity Matrix") -> go.Figure:
153
+ """Create a heatmap for similarity visualization.
154
+
155
+ Args:
156
+ similarity_matrix: 2D array of similarity values
157
+ x_labels: Labels for x-axis (candidate tokens)
158
+ y_labels: Labels for y-axis (reference tokens)
159
+ title: Chart title
160
+
161
+ Returns:
162
+ Plotly figure object
163
+ """
164
+ fig = go.Figure(data=go.Heatmap(
165
+ z=similarity_matrix,
166
+ x=x_labels,
167
+ y=y_labels,
168
+ colorscale='Blues',
169
+ text=[[f'{v:.2f}' for v in row] for row in similarity_matrix],
170
+ texttemplate='%{text}',
171
+ textfont={"size": 10},
172
+ hoverongaps=False
173
+ ))
174
+
175
+ fig.update_layout(
176
+ title=title,
177
+ xaxis_title="Candidate Tokens",
178
+ yaxis_title="Reference Tokens",
179
+ height=500,
180
+ width=600
181
+ )
182
+
183
+ return fig
184
+
185
+
186
+ def create_ranking_visualization(ranked_items: List[Dict],
187
+ correct_answer: str) -> go.Figure:
188
+ """Create visualization for MRR ranking.
189
+
190
+ Args:
191
+ ranked_items: List of dicts with 'answer', 'rank', 'is_correct'
192
+ correct_answer: The correct answer string
193
+
194
+ Returns:
195
+ Plotly figure object
196
+ """
197
+ ranks = [item['rank'] for item in ranked_items]
198
+ answers = [item['answer'] for item in ranked_items]
199
+ is_correct = [item['is_correct'] for item in ranked_items]
200
+
201
+ colors = ['green' if c else 'gray' for c in is_correct]
202
+
203
+ fig = go.Figure(data=[go.Bar(
204
+ x=[f"#{r}" for r in ranks],
205
+ y=[1.0 / r for r in ranks],
206
+ text=answers,
207
+ textposition='auto',
208
+ marker_color=colors
209
+ )])
210
+
211
+ # Add correct answer highlight
212
+ for i, correct in enumerate(is_correct):
213
+ if correct:
214
+ fig.add_annotation(
215
+ x=i,
216
+ y=1.0 / ranks[i] + 0.1,
217
+ text="✓ CORRECT",
218
+ showarrow=False,
219
+ font=dict(color="green", size=12)
220
+ )
221
+
222
+ fig.update_layout(
223
+ title="Answer Ranking (Reciprocal Rank Score)",
224
+ yaxis=dict(title="Reciprocal Rank", range=[0, 1.2]),
225
+ xaxis=dict(title="Rank Position"),
226
+ height=400,
227
+ showlegend=False
228
+ )
229
+
230
+ return fig
231
+
232
+
233
+ def create_token_probability_chart(token_details: List[Dict]) -> go.Figure:
234
+ """Create chart showing per-token perplexity contribution.
235
+
236
+ Args:
237
+ token_details: List of dicts with 'token', 'probability', 'perplexity_contrib'
238
+
239
+ Returns:
240
+ Plotly figure object
241
+ """
242
+ tokens = [detail.get('token', f'token_{i}') for i, detail in enumerate(token_details)]
243
+ probs = [detail['probability'] for detail in token_details]
244
+ perps = [detail['perplexity_contrib'] for detail in token_details]
245
+
246
+ fig = make_subplots(
247
+ rows=2, cols=1,
248
+ subplot_titles=("Token Probabilities", "Perplexity Contribution"),
249
+ vertical_spacing=0.15
250
+ )
251
+
252
+ # Probability bars
253
+ colors = ['lightgreen' if p > 0.7 else 'orange' if p > 0.4 else 'lightcoral' for p in probs]
254
+ fig.add_trace(
255
+ go.Bar(x=tokens, y=probs, marker_color=colors, name="Probability"),
256
+ row=1, col=1
257
+ )
258
+
259
+ # Perplexity bars
260
+ fig.add_trace(
261
+ go.Bar(x=tokens, y=perps, marker_color='lightblue', name="Perplexity"),
262
+ row=2, col=1
263
+ )
264
+
265
+ fig.update_layout(height=600, showlegend=False)
266
+ fig.update_yaxes(title_text="Probability", range=[0, 1], row=1, col=1)
267
+ fig.update_yaxes(title_text="Perplexity", row=2, col=1)
268
+
269
+ return fig
270
+
271
+
272
+ def create_gauge_chart(value: float,
273
+ title: str,
274
+ min_val: float = 0,
275
+ max_val: float = 1) -> go.Figure:
276
+ """Create a gauge chart for single metric display.
277
+
278
+ Args:
279
+ value: Value to display
280
+ title: Chart title
281
+ min_val: Minimum value
282
+ max_val: Maximum value
283
+
284
+ Returns:
285
+ Plotly figure object
286
+ """
287
+ # Determine color based on value
288
+ if value >= 0.7:
289
+ color = "green"
290
+ elif value >= 0.4:
291
+ color = "orange"
292
+ else:
293
+ color = "red"
294
+
295
+ fig = go.Figure(go.Indicator(
296
+ mode="gauge+number",
297
+ value=value,
298
+ title={'text': title},
299
+ gauge={
300
+ 'axis': {'range': [min_val, max_val]},
301
+ 'bar': {'color': color},
302
+ 'steps': [
303
+ {'range': [min_val, max_val*0.33], 'color': 'lightcoral'},
304
+ {'range': [max_val*0.33, max_val*0.67], 'color': 'lightyellow'},
305
+ {'range': [max_val*0.67, max_val], 'color': 'lightgreen'}
306
+ ],
307
+ 'threshold': {
308
+ 'line': {'color': "black", 'width': 4},
309
+ 'thickness': 0.75,
310
+ 'value': value
311
+ }
312
+ }
313
+ ))
314
+
315
+ fig.update_layout(height=300)
316
+
317
+ return fig
modules/visualizations/explanations.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Mathematical formula and explanation utilities."""
2
+
3
+ import streamlit as st
4
+
5
+
6
+ def show_bleu_formula():
7
+ """Display BLEU score formula with explanation."""
8
+ st.latex(r"""
9
+ \\text{BLEU} = \\text{BP} \\times \\exp\\left(\\sum_{n=1}^{N} w_n \\log p_n\\right)
10
+ """)
11
+
12
+ st.markdown("""
13
+ **Where:**
14
+ - **BP** = Brevity Penalty (penalizes short candidates)
15
+ - **p_n** = Modified precision for n-grams
16
+ - **w_n** = Weight for n-gram precision (usually uniform)
17
+ - **N** = Maximum n-gram size (typically 4)
18
+ """)
19
+
20
+ with st.expander("Brevity Penalty Formula"):
21
+ st.latex(r"""
22
+ \\text{BP} = \\begin{cases}
23
+ 1 & \\text{if } c > r \\\\
24
+ e^{(1-r/c)} & \\text{if } c \\leq r
25
+ \\end{cases}
26
+ """)
27
+ st.markdown("- **c** = candidate length, **r** = reference length")
28
+
29
+ with st.expander("Modified N-gram Precision"):
30
+ st.latex(r"""
31
+ p_n = \\frac{\\sum_{\\text{n-gram} \\in \\text{Cand}} \\text{Count}_{\\text{clip}}(\\text{n-gram})}{\\sum_{\\text{n-gram} \\in \\text{Cand}} \\text{Count}(\\text{n-gram})}
32
+ """)
33
+ st.markdown("Count_clip = min(candidate count, reference count)")
34
+
35
+
36
+ def show_rogue_formula():
37
+ """Display ROGUE score formula with explanation."""
38
+ st.subheader("ROGUE-N (N-gram Based)")
39
+ st.latex(r"""
40
+ \\text{ROGUE-N} = \\frac{\\sum_{S \\in \\{\\text{References}\\}} \\sum_{\\text{gram}_n \\in S} \\text{Count}_{\\text{match}}(\\text{gram}_n)}{\\sum_{S \\in \\{\\text{References}\\}} \\sum_{\\text{gram}_n \\in S} \\text{Count}(\\text{gram}_n)}
41
+ """)
42
+ st.markdown("**Focus: Recall** — how many reference n-grams were captured")
43
+
44
+ st.subheader("ROGUE-L (Longest Common Subsequence)")
45
+ st.latex(r"""
46
+ \\text{R}_{\\text{lcs}} = \\frac{LCS(X, Y)}{|X|}, \\quad \\text{P}_{\\text{lcs}} = \\frac{LCS(X, Y)}{|Y|}
47
+ """)
48
+ st.latex(r"""
49
+ \\text{F}_{\\text{lcs}} = \\frac{(1 + \\beta^2) \\text{R}_{\\text{lcs}} \\text{P}_{\\text{lcs}}}{\\text{R}_{\\text{lcs}} + \\beta^2 \\text{P}_{\\text{lcs}}}
50
+ """)
51
+ st.markdown("**LCS** finds longest sequence appearing in both (not necessarily consecutive)")
52
+
53
+
54
+ def show_perplexity_formula():
55
+ """Display Perplexity formula with explanation."""
56
+ st.latex(r"""
57
+ \\text{Perplexity} = \\exp\\left(-\\frac{1}{N} \\sum_{i=1}^{N} \\log P(w_i | w_1 \\ldots w_{i-1})\\right)
58
+ """)
59
+
60
+ st.markdown("""
61
+ **Interpretation:**
62
+ - Perplexity = $2^{H}$ where $H$ is the cross-entropy
63
+ - Can be thought of as "weighted average branching factor"
64
+ - Lower is better (model is less "confused")
65
+ """)
66
+
67
+ with st.expander("Example Interpretation"):
68
+ st.markdown("""
69
+ | Perplexity | Meaning |
70
+ |------------|---------|
71
+ | 1 | Perfect prediction |
72
+ | 10 | Model has ~10 choices at each step |
73
+ | 100 | Model is very confused |
74
+ | 1000 | Near random guessing |
75
+ """)
76
+
77
+
78
+ def show_mrr_formula():
79
+ """Display MRR formula with explanation."""
80
+ st.latex(r"""
81
+ \\text{MRR} = \\frac{1}{|Q|} \\sum_{i=1}^{|Q|} \\frac{1}{\\text{rank}_i}
82
+ """)
83
+
84
+ st.markdown("""
85
+ **Where:**
86
+ - **|Q|** = Number of queries
87
+ - **rank_i** = Position of correct answer for query i
88
+ - If correct answer not in list: $\frac{1}{\text{rank}} = 0$
89
+ """)
90
+
91
+ with st.expander("Reciprocal Rank Examples"):
92
+ st.markdown("""
93
+ | Position | Reciprocal Rank |
94
+ |------------|-----------------|
95
+ | 1st | 1.0 |
96
+ | 2nd | 0.5 |
97
+ | 3rd | 0.33 |
98
+ | 4th | 0.25 |
99
+ | Not found | 0 |
100
+ """)
101
+
102
+
103
+ def show_bert_score_formula():
104
+ """Display BERT Score formula with explanation."""
105
+ st.markdown("""
106
+ BERT Score uses contextual embeddings from pre-trained BERT to compute similarity:
107
+ """)
108
+
109
+ st.latex(r"""
110
+ \\text{Similarity}(x_i, y_j) = \\frac{\\mathbf{x}_i^T \\mathbf{y}_j}{||\\mathbf{x}_i|| ||\\mathbf{y}_j||}
111
+ """)
112
+
113
+ st.subheader("Greedy Matching for Precision/Recall")
114
+ st.latex(r"""
115
+ \\text{P}_{\\text{BERT}} = \\frac{1}{|x|} \\sum_{x_i \\in x} \\max_{y_j \\in y} \\mathbf{x}_i^T \\mathbf{y}_j
116
+ """)
117
+ st.latex(r"""
118
+ \\text{R}_{\\text{BERT}} = \\frac{1}{|y|} \\sum_{y_j \\in y} \\max_{x_i \\in x} \\mathbf{x}_i^T \\mathbf{y}_j
119
+ """)
120
+
121
+ st.markdown("""
122
+ **Key Idea:** Each token in one text is matched to the most similar token in the other text.
123
+ - **Precision:** Average of best matches from candidate to reference
124
+ - **Recall:** Average of best matches from reference to candidate
125
+ """)
126
+
127
+
128
+ def show_metric_comparison_table():
129
+ """Display comparison table of all metrics."""
130
+ st.markdown("""
131
+ | Metric | Type | Needs Reference | Best For | Range |
132
+ |--------|------|-----------------|----------|-------|
133
+ | **BLEU** | Lexical (Precision) | Yes | Machine Translation | 0-1 |
134
+ | **ROGUE** | Lexical (Recall) | Yes | Summarization | 0-1 |
135
+ | **Perplexity** | Model Confidence | No | Language Modeling | 1-∞ |
136
+ | **MRR** | Ranking | Yes (answer) | QA/Retrieval | 0-1 |
137
+ | **BERT Score** | Semantic | Yes | Paraphrase Detection | 0-1 |
138
+ """)
139
+
140
+
141
+ def create_formula_expander(metric_name: str):
142
+ """Create an expander with formula for a given metric.
143
+
144
+ Args:
145
+ metric_name: Name of the metric ('bleu', 'rogue', etc.)
146
+ """
147
+ with st.expander(f"📐 {metric_name.upper()} Formula & Explanation"):
148
+ if metric_name.lower() == 'bleu':
149
+ show_bleu_formula()
150
+ elif metric_name.lower() == 'rogue':
151
+ show_rogue_formula()
152
+ elif metric_name.lower() == 'perplexity':
153
+ show_perplexity_formula()
154
+ elif metric_name.lower() == 'mrr':
155
+ show_mrr_formula()
156
+ elif metric_name.lower() == 'bert_score':
157
+ show_bert_score_formula()
158
+ else:
159
+ st.write("Formula not available for this metric.")
pages/01_overview.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Overview page - Compare all metrics."""
2
+
3
+ import streamlit as st
4
+ import sys
5
+ sys.path.append('/Users/mymac/Documents/applyjob/supportProjects/llm-evaluation-dashboard')
6
+
7
+ from modules.metrics.bleu import calculate_bleu, interpret_bleu_score
8
+ from modules.metrics.rogue import calculate_all_rogue, interpret_rogue_score
9
+ from modules.metrics.perplexity import calculate_perplexity_approximation, interpret_perplexity
10
+ from modules.metrics.bert_score import calculate_bert_score, interpret_bert_score
11
+ from modules.visualizations.charts import create_radar_chart, create_bar_chart
12
+ from modules.visualizations.explanations import show_metric_comparison_table
13
+ from modules.utils.examples import get_text_example_names, get_text_example
14
+
15
+ st.set_page_config(page_title="Overview", page_icon="📊", layout="wide")
16
+
17
+ st.title("📊 Metrics Overview & Comparison")
18
+
19
+ st.markdown("""
20
+ Compare all 5 evaluation metrics side-by-side on the same text pair.
21
+ See how different metrics capture different aspects of quality.
22
+ """)
23
+
24
+ # Input section
25
+ col1, col2 = st.columns(2)
26
+
27
+ with col1:
28
+ st.subheader("📄 Ground Truth (Reference)")
29
+ example_name = st.selectbox("Choose example:", ["Custom"] + get_text_example_names())
30
+
31
+ if example_name != "Custom":
32
+ example = get_text_example(example_name)
33
+ ground_truth = st.text_area("Reference:", value=example.ground_truth, height=100)
34
+ st.caption(example.description)
35
+ else:
36
+ ground_truth = st.text_area("Reference:",
37
+ value="The cat sat on the mat and looked outside at the birds.",
38
+ height=100)
39
+
40
+ with col2:
41
+ st.subheader("🤖 Model Prediction (Candidate)")
42
+ if example_name != "Custom":
43
+ prediction = st.text_area("Candidate:", value=example.prediction, height=100)
44
+ else:
45
+ prediction = st.text_area("Candidate:",
46
+ value="The cat was sitting on the mat and gazing at the birds outside.",
47
+ height=100)
48
+
49
+ # Calculate all metrics
50
+ if st.button("🧮 Compare All Metrics", type="primary"):
51
+
52
+ # Calculate each metric
53
+ bleu_result = calculate_bleu(ground_truth, prediction)
54
+ rogue_result = calculate_all_rogue(ground_truth, prediction)
55
+ perplexity_result = calculate_perplexity_approximation(prediction, "medium")
56
+ bert_result = calculate_bert_score(ground_truth, prediction)
57
+
58
+ st.divider()
59
+
60
+ # Summary cards
61
+ st.subheader("📈 Metric Scores Summary")
62
+
63
+ cols = st.columns(5)
64
+
65
+ with cols[0]:
66
+ st.metric("BLEU", f"{bleu_result['bleu']:.3f}")
67
+ st.caption(interpret_bleu_score(bleu_result['bleu']))
68
+
69
+ with cols[1]:
70
+ st.metric("ROGUE-L", f"{rogue_result['rogueL']['f1']:.3f}")
71
+ st.caption(interpret_rogue_score(rogue_result['rogueL']['f1']))
72
+
73
+ with cols[2]:
74
+ ppl = perplexity_result['perplexity']
75
+ st.metric("Perplexity", f"{ppl:.1f}")
76
+ st.caption(interpret_perplexity(ppl))
77
+
78
+ with cols[3]:
79
+ # MRR placeholder for comparison (would need QA setup)
80
+ st.metric("MRR", "N/A")
81
+ st.caption("Needs QA format")
82
+
83
+ with cols[4]:
84
+ st.metric("BERT Score", f"{bert_result['f1']:.3f}")
85
+ st.caption(interpret_bert_score(bert_result['f1']))
86
+
87
+ # Radar chart
88
+ st.subheader("🎯 Metric Comparison Radar")
89
+
90
+ # Normalize all to 0-1 scale
91
+ radar_data = {
92
+ "BLEU": bleu_result['bleu'],
93
+ "ROGUE-L": rogue_result['rogueL']['f1'],
94
+ "BERT Score": bert_result['f1'],
95
+ # Normalize perplexity (inverse, lower is better, cap at 100)
96
+ "Perplexity\n(inv)": max(0, 1 - perplexity_result['perplexity'] / 100)
97
+ }
98
+
99
+ fig = create_radar_chart(radar_data, "All Metrics Comparison")
100
+ st.plotly_chart(fig, use_container_width=True)
101
+
102
+ # Bar chart comparison
103
+ st.subheader("📊 Side-by-Side Comparison")
104
+
105
+ bar_data = {
106
+ "BLEU": bleu_result['bleu'],
107
+ "ROGUE-1": rogue_result['rogue1']['f1'],
108
+ "ROGUE-L": rogue_result['rogueL']['f1'],
109
+ "ROGUE-2": rogue_result['rogue2']['f1'],
110
+ "BERT Score": bert_result['f1']
111
+ }
112
+
113
+ fig_bar = create_bar_chart(bar_data, "Lexical & Semantic Metrics")
114
+ st.plotly_chart(fig_bar, use_container_width=True)
115
+
116
+ # Detailed breakdown
117
+ st.subheader("📋 Detailed Metric Analysis")
118
+
119
+ tab1, tab2, tab3, tab4 = st.tabs(["BLEU Details", "ROGUE Details", "Perplexity", "BERT Score"])
120
+
121
+ with tab1:
122
+ st.markdown(f"**BLEU Score:** {bleu_result['bleu']:.4f}")
123
+ st.markdown(f"- Brevity Penalty: {bleu_result['brevity_penalty']:.4f}")
124
+ st.markdown(f"- Geo-Mean Precision: {bleu_result['geo_mean_precision']:.4f}")
125
+ st.markdown("**N-gram Precisions:**")
126
+ for i, p in enumerate(bleu_result['precisions'], 1):
127
+ st.markdown(f"- {i}-gram: {p:.4f}")
128
+
129
+ with tab2:
130
+ st.markdown("**ROGUE Scores:**")
131
+ for name, result in rogue_result.items():
132
+ st.markdown(f"- {name}: F1={result['f1']:.4f}, P={result['precision']:.4f}, R={result['recall']:.4f}")
133
+
134
+ with tab3:
135
+ ppl = perplexity_result['perplexity']
136
+ st.markdown(f"**Perplexity:** {ppl:.2f}")
137
+ st.markdown(f"- Cross-Entropy: {perplexity_result['cross_entropy']:.4f}")
138
+ st.markdown(f"- Tokens: {perplexity_result['num_tokens']}")
139
+ st.info(interpret_perplexity(ppl))
140
+
141
+ with tab4:
142
+ st.markdown(f"**BERT Score F1:** {bert_result['f1']:.4f}")
143
+ st.markdown(f"- Cosine Similarity: {bert_result['cosine_similarity']:.4f}")
144
+ st.info(interpret_bert_score(bert_result['f1']))
145
+
146
+ # Export section
147
+ st.subheader("💾 Export Results")
148
+
149
+ import json
150
+ export_data = {
151
+ "ground_truth": ground_truth,
152
+ "prediction": prediction,
153
+ "metrics": {
154
+ "bleu": bleu_result['bleu'],
155
+ "rogue1": rogue_result['rogue1']['f1'],
156
+ "rogueL": rogue_result['rogueL']['f1'],
157
+ "perplexity": perplexity_result['perplexity'],
158
+ "bert_score": bert_result['f1']
159
+ }
160
+ }
161
+
162
+ col_exp1, col_exp2 = st.columns(2)
163
+
164
+ with col_exp1:
165
+ st.download_button(
166
+ "📥 Download JSON",
167
+ json.dumps(export_data, indent=2),
168
+ file_name="evaluation_results.json",
169
+ mime="application/json"
170
+ )
171
+
172
+ with col_exp2:
173
+ # CSV export
174
+ import csv
175
+ import io
176
+ csv_buffer = io.StringIO()
177
+ writer = csv.writer(csv_buffer)
178
+ writer.writerow(["Metric", "Score"])
179
+ for metric, score in export_data["metrics"].items():
180
+ writer.writerow([metric, f"{score:.6f}"])
181
+
182
+ st.download_button(
183
+ "📥 Download CSV",
184
+ csv_buffer.getvalue(),
185
+ file_name="evaluation_results.csv",
186
+ mime="text/csv"
187
+ )
188
+
189
+ # Metric comparison table
190
+ st.divider()
191
+ show_metric_comparison_table()
192
+
193
+ # Guide
194
+ with st.expander("💡 How to Choose the Right Metric"):
195
+ st.markdown("""
196
+ **Quick Decision Guide:**
197
+
198
+ | Task | Recommended Metric | Why |
199
+ |------|---------------------|-----|
200
+ | Machine Translation | **BLEU** | Industry standard, checks word-for-word accuracy |
201
+ | Summarization | **ROGUE** | Focuses on recall (covering key points) |
202
+ | Language Model | **Perplexity** | No reference needed, measures confidence |
203
+ | Question Answering | **MRR** | Evaluates ranking quality |
204
+ | Paraphrase Detection | **BERT Score** | Captures semantic similarity |
205
+ | General Generation | **All 3 (BLEU+ROGUE+BERT)** | Covers lexical + semantic |
206
+
207
+ **Pro Tip:** Use multiple metrics! A high BLEU but low BERT Score suggests
208
+ the model is copying words without understanding meaning.
209
+ """)
pages/02_bleu_score.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """BLEU Score evaluation page."""
2
+
3
+ import streamlit as st
4
+ import sys
5
+ sys.path.append('/Users/mymac/Documents/applyjob/supportProjects/llm-evaluation-dashboard')
6
+
7
+ from modules.metrics.bleu import calculate_bleu, get_matching_ngrams_detailed, interpret_bleu_score
8
+ from modules.visualizations.charts import create_ngram_precision_chart, create_gauge_chart
9
+ from modules.visualizations.explanations import create_formula_expander
10
+ from modules.utils.examples import get_text_example_names, get_text_example
11
+ from modules.utils.text_processing import tokenize, get_ngrams
12
+
13
+ st.set_page_config(page_title="BLEU Score", page_icon="🔵", layout="wide")
14
+
15
+ st.title("🔵 BLEU Score Evaluation")
16
+
17
+ st.markdown("""
18
+ **Bilingual Evaluation Understudy** — Measures n-gram precision with brevity penalty.
19
+ Best for: **Machine Translation** evaluation.
20
+ """)
21
+
22
+ # Input section
23
+ col1, col2 = st.columns(2)
24
+
25
+ with col1:
26
+ st.subheader("📄 Ground Truth (Reference)")
27
+ example_name = st.selectbox("Choose example:", ["Custom"] + get_text_example_names())
28
+
29
+ if example_name != "Custom":
30
+ example = get_text_example(example_name)
31
+ ground_truth = st.text_area("Reference text:", value=example.ground_truth, height=100)
32
+ st.caption(f"Example: {example.description}")
33
+ else:
34
+ ground_truth = st.text_area("Reference text:",
35
+ value="The cat sat on the mat and looked outside",
36
+ height=100)
37
+
38
+ with col2:
39
+ st.subheader("🤖 Model Prediction (Candidate)")
40
+ if example_name != "Custom":
41
+ prediction = st.text_area("Candidate text:", value=example.prediction, height=100)
42
+ else:
43
+ prediction = st.text_area("Candidate text:",
44
+ value="The cat sat on the mat",
45
+ height=100)
46
+
47
+ # Calculate button
48
+ if st.button("🧮 Calculate BLEU Score", type="primary"):
49
+ result = calculate_bleu(ground_truth, prediction)
50
+
51
+ # Display main score
52
+ st.divider()
53
+
54
+ score_col, interp_col = st.columns([1, 2])
55
+
56
+ with score_col:
57
+ st.plotly_chart(create_gauge_chart(result['bleu'], "BLEU Score"), use_container_width=True)
58
+
59
+ with interp_col:
60
+ st.subheader("📊 Score Interpretation")
61
+ st.markdown(f"**Score: {result['bleu']:.4f}**")
62
+ st.markdown(f"**Quality: {interpret_bleu_score(result['bleu'])}**")
63
+
64
+ # Components
65
+ st.markdown(f"- **Brevity Penalty:** {result['brevity_penalty']:.4f}")
66
+ st.markdown(f"- **Geo-Mean Precision:** {result['geo_mean_precision']:.4f}")
67
+ st.markdown(f"- **Reference Length:** {result['ref_length']} tokens")
68
+ st.markdown(f"- **Candidate Length:** {result['cand_length']} tokens")
69
+
70
+ # N-gram precision breakdown
71
+ st.subheader("📈 N-gram Precision Breakdown")
72
+
73
+ chart_col, detail_col = st.columns([2, 1])
74
+
75
+ with chart_col:
76
+ fig = create_ngram_precision_chart(
77
+ result['precisions'],
78
+ result['clipped_counts'],
79
+ result['total_counts']
80
+ )
81
+ st.plotly_chart(fig, use_container_width=True)
82
+
83
+ with detail_col:
84
+ st.markdown("**Precision Details:**")
85
+ for i, (p, clipped, total) in enumerate(zip(
86
+ result['precisions'],
87
+ result['clipped_counts'],
88
+ result['total_counts']
89
+ ), 1):
90
+ st.markdown(f"**{i}-gram:** {p:.4f}")
91
+ st.caption(f"Matches: {clipped}/{total}")
92
+
93
+ # Detailed matching
94
+ st.subheader("🔍 Detailed N-gram Matching")
95
+
96
+ tab1, tab2, tab3, tab4 = st.tabs(["1-gram", "2-gram", "3-gram", "4-gram"])
97
+
98
+ tabs = [tab1, tab2, tab3, tab4]
99
+ for n, tab in enumerate(tabs, 1):
100
+ with tab:
101
+ details = get_matching_ngrams_detailed(ground_truth, prediction, n)
102
+
103
+ col_a, col_b = st.columns(2)
104
+
105
+ with col_a:
106
+ st.markdown(f"**{n}-grams in Reference:**")
107
+ for gram in details['ref_ngrams'][:10]:
108
+ st.markdown(f"- {' '.join(gram)}")
109
+ if len(details['ref_ngrams']) > 10:
110
+ st.caption(f"... and {len(details['ref_ngrams']) - 10} more")
111
+
112
+ with col_b:
113
+ st.markdown(f"**Matched {n}-grams:**")
114
+ for match in details['matches'][:10]:
115
+ st.markdown(f"- {' '.join(match['ngram'])} " +
116
+ f"(ref: {match['reference_count']}, cand: {match['candidate_count']})")
117
+
118
+ if details['over_matches']:
119
+ st.markdown("**Over-generated:**")
120
+ for over in details['over_matches'][:5]:
121
+ st.markdown(f"- {' '.join(over['ngram'])} " +
122
+ f"(excess: {over['excess']})")
123
+
124
+ # Formula section
125
+ create_formula_expander('bleu')
126
+
127
+ # Additional info
128
+ st.divider()
129
+ with st.expander("💡 Understanding BLEU Score"):
130
+ st.markdown("""
131
+ **Why BLEU works for translation:**
132
+ 1. **N-gram precision** ensures word choice accuracy
133
+ 2. **Brevity penalty** prevents overly short outputs
134
+ 3. **Multiple n-gram sizes** capture fluency (1-gram = content, 4-gram = fluency)
135
+
136
+ **Limitations:**
137
+ - Doesn't capture semantic meaning (synonyms score low)
138
+ - Sensitive to word order changes
139
+ - Requires reference text
140
+ """)
pages/03_rogue_score.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ROGUE Score evaluation page."""
2
+
3
+ import streamlit as st
4
+ import sys
5
+ sys.path.append('/Users/mymac/Documents/applyjob/supportProjects/llm-evaluation-dashboard')
6
+
7
+ from modules.metrics.rogue import calculate_all_rogue, calculate_rogue_l, interpret_rogue_score
8
+ from modules.visualizations.charts import create_bar_chart, create_gauge_chart
9
+ from modules.visualizations.explanations import create_formula_expander
10
+ from modules.utils.examples import get_text_example_names, get_text_example
11
+
12
+ st.set_page_config(page_title="ROGUE Score", page_icon="🔴", layout="wide")
13
+
14
+ st.title("🔴 ROGUE Score Evaluation")
15
+
16
+ st.markdown("""
17
+ **Recall-Oriented Understudy for Gisting Evaluation** — Focuses on recall, not precision.
18
+ Best for: **Text Summarization** evaluation.
19
+ """)
20
+
21
+ # Variant selector
22
+ variant = st.selectbox(
23
+ "Select ROGUE Variant:",
24
+ ["All Variants", "ROGUE-1 (Unigram)", "ROGUE-2 (Bigram)", "ROGUE-L (LCS)",
25
+ "ROGUE-S (Skip-bigram)", "ROGUE-SU (Skip + Unigram)", "ROGUE-W (Weighted LCS)"]
26
+ )
27
+
28
+ # Input section
29
+ col1, col2 = st.columns(2)
30
+
31
+ with col1:
32
+ st.subheader("📄 Ground Truth (Reference)")
33
+ example_name = st.selectbox("Choose example:", ["Custom"] + get_text_example_names(), key="rogue_example")
34
+
35
+ if example_name != "Custom":
36
+ example = get_text_example(example_name)
37
+ ground_truth = st.text_area("Reference text:", value=example.ground_truth, height=150, key="rogue_ref")
38
+ st.caption(f"Example: {example.description}")
39
+ else:
40
+ ground_truth = st.text_area("Reference text:",
41
+ value="The cat sat on the mat and looked outside at the birds.",
42
+ height=150, key="rogue_ref")
43
+
44
+ with col2:
45
+ st.subheader("🤖 Model Prediction (Candidate)")
46
+ if example_name != "Custom":
47
+ prediction = st.text_area("Candidate text:", value=example.prediction, height=150, key="rogue_cand")
48
+ else:
49
+ prediction = st.text_area("Candidate text:",
50
+ value="The cat sat on the mat and looked outside.",
51
+ height=150, key="rogue_cand")
52
+
53
+ # Calculate button
54
+ if st.button("🧮 Calculate ROGUE Score", type="primary"):
55
+ results = calculate_all_rogue(ground_truth, prediction)
56
+
57
+ st.divider()
58
+
59
+ # Display based on selected variant
60
+ if variant == "All Variants":
61
+ st.subheader("📊 All ROGUE Variants Comparison")
62
+
63
+ # Create comparison data
64
+ comparison_data = {
65
+ "ROGUE-1": results['rogue1']['f1'],
66
+ "ROGUE-2": results['rogue2']['f1'],
67
+ "ROGUE-L": results['rogueL']['f1'],
68
+ "ROGUE-S": results['rogueS']['f1'],
69
+ "ROGUE-SU": results['rogueSU']['f1'],
70
+ "ROGUE-W": results['rogueW']['f1']
71
+ }
72
+
73
+ col_chart, col_table = st.columns([2, 1])
74
+
75
+ with col_chart:
76
+ fig = create_bar_chart(comparison_data, "ROGUE F1 Scores Comparison")
77
+ st.plotly_chart(fig, use_container_width=True)
78
+
79
+ with col_table:
80
+ st.markdown("**F1 Scores:**")
81
+ for name, score in comparison_data.items():
82
+ st.markdown(f"**{name}:** {score:.4f}")
83
+
84
+ st.markdown("---")
85
+ st.caption("ROGUE-N: N-gram recall")
86
+ st.caption("ROGUE-L: LCS-based (word order)")
87
+ st.caption("ROGUE-S: Skip-bigrams")
88
+ st.caption("ROGUE-SU: S + Unigrams")
89
+ st.caption("ROGUE-W: Weighted consecutive matches")
90
+
91
+ else:
92
+ # Display single variant
93
+ variant_map = {
94
+ "ROGUE-1 (Unigram)": ('rogue1', results['rogue1']),
95
+ "ROGUE-2 (Bigram)": ('rogue2', results['rogue2']),
96
+ "ROGUE-L (LCS)": ('rogueL', results['rogueL']),
97
+ "ROGUE-S (Skip-bigram)": ('rogueS', results['rogueS']),
98
+ "ROGUE-SU (Skip + Unigram)": ('rogueSU', results['rogueSU']),
99
+ "ROGUE-W (Weighted LCS)": ('rogueW', results['rogueW'])
100
+ }
101
+
102
+ key, result = variant_map[variant]
103
+
104
+ col_gauge, col_details = st.columns([1, 2])
105
+
106
+ with col_gauge:
107
+ st.plotly_chart(create_gauge_chart(result['f1'], f"{variant} F1"), use_container_width=True)
108
+
109
+ with col_details:
110
+ st.subheader(f"📊 {variant} Details")
111
+ st.markdown(f"**F1 Score:** {result['f1']:.4f}")
112
+ st.markdown(f"**Quality:** {interpret_rogue_score(result['f1'])}")
113
+ st.markdown(f"- **Precision:** {result['precision']:.4f}")
114
+ st.markdown(f"- **Recall:** {result['recall']:.4f}")
115
+
116
+ # Variant-specific details
117
+ if key == 'rogueL' and 'lcs' in result:
118
+ st.markdown(f"- **LCS Length:** {result['lcs_length']} tokens")
119
+ st.markdown(f"- **LCS:** {' '.join(result['lcs'][:10])}")
120
+ elif key in ['rogue1', 'rogue2', 'rogueS'] and 'matches' in result:
121
+ st.markdown(f"- **Matches:** {result['matches']} n-grams")
122
+
123
+ # ROGUE-L LCS Visualization (special case)
124
+ if variant in ["All Variants", "ROGUE-L (LCS)"]:
125
+ st.subheader("🔍 ROGUE-L: Longest Common Subsequence Visualization")
126
+
127
+ rogue_l = results['rogueL']
128
+
129
+ if 'lcs' in rogue_l and rogue_l['lcs']:
130
+ st.markdown("**LCS (Longest Common Subsequence):**")
131
+ st.markdown(f"### {' → '.join(rogue_l['lcs'])}")
132
+
133
+ st.caption(f"""
134
+ This sequence appears in both texts in the same order (but not necessarily consecutively).
135
+ Length: {rogue_l['lcs_length']} tokens out of {rogue_l['ref_length']} reference tokens.
136
+ Recall = {rogue_l['lcs_length']}/{rogue_l['ref_length']} = {rogue_l['recall']:.4f}
137
+ """)
138
+
139
+ # Visual alignment
140
+ st.markdown("**Visual Alignment:**")
141
+
142
+ ref_tokens = ground_truth.split()
143
+ cand_tokens = prediction.split()
144
+ lcs_tokens = rogue_l['lcs']
145
+
146
+ # Mark matching tokens
147
+ ref_highlighted = []
148
+ cand_highlighted = []
149
+
150
+ lcs_idx = 0
151
+ for token in ref_tokens:
152
+ if lcs_idx < len(lcs_tokens) and token.lower() == lcs_tokens[lcs_idx].lower():
153
+ ref_highlighted.append(f"**{token}**")
154
+ lcs_idx += 1
155
+ else:
156
+ ref_highlighted.append(token)
157
+
158
+ lcs_idx = 0
159
+ for token in cand_tokens:
160
+ if lcs_idx < len(lcs_tokens) and token.lower() == lcs_tokens[lcs_idx].lower():
161
+ cand_highlighted.append(f"**{token}**")
162
+ lcs_idx += 1
163
+ else:
164
+ cand_highlighted.append(token)
165
+
166
+ col_ref, col_cand = st.columns(2)
167
+ with col_ref:
168
+ st.markdown("**Reference:**")
169
+ st.markdown(" ".join(ref_highlighted))
170
+ with col_cand:
171
+ st.markdown("**Candidate:**")
172
+ st.markdown(" ".join(cand_highlighted))
173
+
174
+ st.caption("**Bold** = part of LCS")
175
+
176
+ # Formula section
177
+ create_formula_expander('rogue')
178
+
179
+ # Additional info
180
+ st.divider()
181
+ with st.expander("💡 Understanding ROGUE vs BLEU"):
182
+ st.markdown("""
183
+ | Aspect | BLEU | ROGUE |
184
+ |--------|------|-------|
185
+ | **Focus** | Precision | Recall |
186
+ | **Penalizes** | Over-generation | Under-generation |
187
+ | **Best for** | Translation | Summarization |
188
+ | **N-grams** | Multiple (1-4) | Usually 1-2 |
189
+ | **Word Order** | Strict | Flexible (ROGUE-L) |
190
+
191
+ **When to use ROGUE:**
192
+ - Summarization: You want to ensure all key points are covered (recall)
193
+ - Allow some flexibility in phrasing
194
+ - Don't want to penalize synonyms heavily
195
+ """)
pages/04_perplexity.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Perplexity evaluation page."""
2
+
3
+ import streamlit as st
4
+ import sys
5
+ sys.path.append('/Users/mymac/Documents/applyjob/supportProjects/llm-evaluation-dashboard')
6
+
7
+ from modules.metrics.perplexity import calculate_perplexity_approximation, interpret_perplexity, get_perplexity_grade
8
+ from modules.visualizations.charts import create_token_probability_chart, create_gauge_chart
9
+ from modules.visualizations.explanations import create_formula_expander
10
+
11
+ st.set_page_config(page_title="Perplexity", page_icon="🤔", layout="wide")
12
+
13
+ st.title("🤔 Perplexity Evaluation")
14
+
15
+ st.markdown("""
16
+ **Model Confidence Metric** — Measures how "confused" the model is when generating text.
17
+ Lower is better. No ground truth needed!
18
+
19
+ Best for: **Language Model Quality** assessment.
20
+ """)
21
+
22
+ # Input section
23
+ st.subheader("📝 Input Text for Perplexity Calculation")
24
+
25
+ # Model confidence selector
26
+ confidence_level = st.select_slider(
27
+ "Simulate Model Confidence Level:",
28
+ options=["Low (Confused)", "Medium", "High (Confident)"],
29
+ value="Medium"
30
+ )
31
+
32
+ confidence_map = {
33
+ "Low (Confused)": "low",
34
+ "Medium": "medium",
35
+ "High (Confident)": "high"
36
+ }
37
+
38
+ # Text input
39
+ text_input = st.text_area(
40
+ "Enter text to analyze:",
41
+ value="The cat sat on the mat and looked outside at the birds flying in the sky.",
42
+ height=100
43
+ )
44
+
45
+ # Example presets
46
+ col1, col2, col3 = st.columns(3)
47
+ with col1:
48
+ if st.button("📖 Fluent Text"):
49
+ text_input = "The quick brown fox jumps over the lazy dog in the garden."
50
+ confidence_level = "High (Confident)"
51
+ st.rerun()
52
+
53
+ with col2:
54
+ if st.button("🔀 Mixed Quality"):
55
+ text_input = "The cat sat on mat and looked birds flying sky."
56
+ confidence_level = "Medium"
57
+ st.rerun()
58
+
59
+ with col3:
60
+ if st.button("🌀 Nonsensical"):
61
+ text_input = "Zxq sat mat looked birds quantum flying sky purple."
62
+ confidence_level = "Low (Confused)"
63
+ st.rerun()
64
+
65
+ # Calculate button
66
+ if st.button("🧮 Calculate Perplexity", type="primary"):
67
+ result = calculate_perplexity_approximation(text_input, confidence_map[confidence_level])
68
+
69
+ st.divider()
70
+
71
+ # Main display
72
+ col_gauge, col_info = st.columns([1, 2])
73
+
74
+ with col_gauge:
75
+ # Normalize for gauge (perplexity can be > 1)
76
+ ppl = result['perplexity']
77
+ gauge_value = min(ppl / 100, 1.0) # Cap at 100 for display
78
+ st.plotly_chart(create_gauge_chart(gauge_value, f"Perplexity: {ppl:.2f}", 0, 1), use_container_width=True)
79
+
80
+ with col_info:
81
+ st.subheader("📊 Perplexity Analysis")
82
+
83
+ # Grade and interpretation
84
+ grade = get_perplexity_grade(ppl)
85
+ interpretation = interpret_perplexity(ppl)
86
+
87
+ col_a, col_b = st.columns(2)
88
+ with col_a:
89
+ st.metric("Perplexity", f"{ppl:.2f}")
90
+ st.metric("Grade", grade)
91
+ with col_b:
92
+ st.metric("Cross-Entropy", f"{result['cross_entropy']:.3f}")
93
+ st.metric("Avg Log-Prob", f"{result['avg_log_prob']:.3f}")
94
+
95
+ st.info(f"**Interpretation:** {interpretation}")
96
+
97
+ # Token-level breakdown
98
+ st.subheader("📈 Token-Level Perplexity Breakdown")
99
+
100
+ if result['token_details']:
101
+ fig = create_token_probability_chart(result['token_details'])
102
+ st.plotly_chart(fig, use_container_width=True)
103
+
104
+ # Token table
105
+ with st.expander("📋 Token Details Table"):
106
+ token_data = []
107
+ for detail in result['token_details']:
108
+ token_data.append({
109
+ 'Token': detail.get('token', 'N/A'),
110
+ 'Probability': f"{detail['probability']:.4f}",
111
+ 'Log-Prob': f"{detail['log_probability']:.4f}",
112
+ 'Perplexity': f"{detail['perplexity_contrib']:.2f}"
113
+ })
114
+
115
+ st.table(token_data)
116
+
117
+ # Educational section
118
+ st.subheader("🎓 Understanding the Calculation")
119
+
120
+ col_calc, col_interpret = st.columns(2)
121
+
122
+ with col_calc:
123
+ st.markdown("**Step-by-Step:**")
124
+ st.markdown(f"""
125
+ 1. **Tokenize:** {result['num_tokens']} tokens
126
+ 2. **Get Probabilities:** P(token_i | previous tokens)
127
+ 3. **Log Transform:** log(P) for numerical stability
128
+ 4. **Average:** Mean of log probabilities
129
+ 5. **Exponentiate:** exp(-mean) = **{ppl:.2f}**
130
+ """)
131
+
132
+ with col_interpret:
133
+ st.markdown("**What this means:**")
134
+ st.markdown(f"""
135
+ - At each step, the model had ~**{ppl:.0f}** choices
136
+ - Lower = more confident predictions
137
+ - Ideal: 1 (perfect prediction)
138
+ - Typical good LM: 10-50
139
+ - Random guessing: ~vocab_size (10k-100k)
140
+ """)
141
+
142
+ # Formula section
143
+ create_formula_expander('perplexity')
144
+
145
+ # Additional info
146
+ st.divider()
147
+ with st.expander("💡 Perplexity vs Other Metrics"):
148
+ st.markdown("""
149
+ | Feature | Perplexity | BLEU/ROGUE |
150
+ |---------|------------|------------|
151
+ | **Needs Reference** | ❌ No | ✅ Yes |
152
+ | **Measures** | Model confidence | Text similarity |
153
+ | **Best for** | LM evaluation | Generation quality |
154
+ | **Direction** | Lower is better | Higher is better |
155
+ | **Range** | [1, ∞) | [0, 1] |
156
+
157
+ **Key Insight:**
158
+ Perplexity measures how well the model **predicts** the text, not how similar it is to a reference.
159
+ A fluent, grammatical text can have low perplexity even if it says something completely different!
160
+ """)
pages/05_mrr.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Mean Reciprocal Rank (MRR) evaluation page."""
2
+
3
+ import streamlit as st
4
+ import sys
5
+ sys.path.append('/Users/mymac/Documents/applyjob/supportProjects/llm-evaluation-dashboard')
6
+
7
+ from modules.metrics.mrr import calculate_mrr, calculate_reciprocal_rank, interpret_mrr, get_mrr_grade
8
+ from modules.visualizations.charts import create_ranking_visualization, create_gauge_chart
9
+ from modules.visualizations.explanations import create_formula_expander
10
+ from modules.utils.examples import get_mrr_example_names, get_mrr_example
11
+
12
+ st.set_page_config(page_title="MRR", page_icon="📊", layout="wide")
13
+
14
+ st.title("📊 Mean Reciprocal Rank (MRR)")
15
+
16
+ st.markdown("""
17
+ **Ranking Quality Metric** — Measures how well a model ranks the correct answer.
18
+ Best for: **Question Answering** and **Information Retrieval** systems.
19
+ """)
20
+
21
+ # Mode selector
22
+ mode = st.radio("Evaluation Mode:", ["Single Query", "Multiple Queries (Batch)"])
23
+
24
+ if mode == "Single Query":
25
+ # Single query input
26
+ st.subheader("📝 Single Query Evaluation")
27
+
28
+ # Example selector
29
+ example_name = st.selectbox("Choose example:", ["Custom"] + get_mrr_example_names())
30
+
31
+ if example_name != "Custom":
32
+ example = get_mrr_example(example_name)
33
+ question = st.text_input("Question:", value=example.get("question", ""))
34
+ correct_answer = st.text_input("Correct Answer:", value=example.get("correct_answer", ""))
35
+ default_ranked = "\n".join(example.get("ranked_answers", []))
36
+ st.caption(f"Example: {example.get('description', '')}")
37
+ else:
38
+ question = st.text_input("Question:", value="What is the capital of France?")
39
+ correct_answer = st.text_input("Correct Answer:", value="Paris")
40
+ default_ranked = "London\nParis\nBerlin\nMadrid"
41
+
42
+ # Ranked answers input
43
+ st.markdown("**Ranked Answers (one per line, best first):**")
44
+ ranked_input = st.text_area("", value=default_ranked, height=150)
45
+ ranked_answers = [line.strip() for line in ranked_input.split("\n") if line.strip()]
46
+
47
+ if st.button("🧮 Calculate RR", type="primary"):
48
+ result = calculate_reciprocal_rank(ranked_answers, correct_answer)
49
+
50
+ st.divider()
51
+
52
+ col_gauge, col_details = st.columns([1, 2])
53
+
54
+ with col_gauge:
55
+ rr = result['reciprocal_rank']
56
+ st.plotly_chart(create_gauge_chart(rr, "Reciprocal Rank"), use_container_width=True)
57
+
58
+ with col_details:
59
+ st.subheader("📊 Query Results")
60
+
61
+ if result['found']:
62
+ st.success(f"✅ Correct answer found at position **#{result['rank']}**")
63
+ st.markdown(f"**Reciprocal Rank:** {rr:.4f}")
64
+ grade = get_mrr_grade(rr)
65
+ st.markdown(f"**Grade:** {grade}")
66
+ st.markdown(f"**Quality:** {interpret_mrr(rr)}")
67
+ else:
68
+ st.error(f"❌ Correct answer '{correct_answer}' not found in top {len(ranked_answers)}")
69
+ st.markdown("**Reciprocal Rank:** 0.0")
70
+
71
+ # Ranking visualization
72
+ st.subheader("🔍 Answer Ranking Visualization")
73
+
74
+ ranked_items = [
75
+ {"rank": i+1, "answer": ans, "is_correct": ans.lower() == correct_answer.lower()}
76
+ for i, ans in enumerate(ranked_answers)
77
+ ]
78
+
79
+ if ranked_items:
80
+ fig = create_ranking_visualization(ranked_items, correct_answer)
81
+ st.plotly_chart(fig, use_container_width=True)
82
+
83
+ # Show the list
84
+ st.markdown("**Ranked List:**")
85
+ for i, answer in enumerate(ranked_answers, 1):
86
+ is_correct = answer.lower() == correct_answer.lower()
87
+ icon = "✅" if is_correct else f"{i}."
88
+ score = f"(RR: {1.0/i:.4f})" if is_correct else ""
89
+ st.markdown(f"{icon} **{answer}** {score}")
90
+
91
+ else:
92
+ # Batch mode
93
+ st.subheader("📝 Batch Query Evaluation")
94
+
95
+ st.markdown("Enter multiple queries as JSON or use preset:")
96
+
97
+ preset = st.selectbox("Preset batch:", ["Custom", "Mixed Quality", "All Perfect", "All Poor"])
98
+
99
+ if preset == "Mixed Quality":
100
+ default_json = '''[
101
+ {"question": "Capital of France?", "ranked_answers": ["Paris", "London", "Berlin"], "correct_answer": "Paris"},
102
+ {"question": "Capital of Japan?", "ranked_answers": ["Beijing", "Tokyo", "Seoul"], "correct_answer": "Tokyo"},
103
+ {"question": "Capital of UK?", "ranked_answers": ["London", "Paris", "Berlin"], "correct_answer": "London"},
104
+ {"question": "Capital of Italy?", "ranked_answers": ["Madrid", "Rome", "Paris"], "correct_answer": "Rome"}
105
+ ]'''
106
+ elif preset == "All Perfect":
107
+ default_json = '''[
108
+ {"question": "Q1", "ranked_answers": ["A", "B", "C"], "correct_answer": "A"},
109
+ {"question": "Q2", "ranked_answers": ["X", "Y", "Z"], "correct_answer": "X"},
110
+ {"question": "Q3", "ranked_answers": ["Yes", "No", "Maybe"], "correct_answer": "Yes"}
111
+ ]'''
112
+ elif preset == "All Poor":
113
+ default_json = '''[
114
+ {"question": "Q1", "ranked_answers": ["A", "B", "C"], "correct_answer": "D"},
115
+ {"question": "Q2", "ranked_answers": ["X", "Y", "Z"], "correct_answer": "W"}
116
+ ]'''
117
+ else:
118
+ default_json = '[\n {"question": "Q1", "ranked_answers": ["A", "B"], "correct_answer": "A"}\n]'
119
+
120
+ json_input = st.text_area("Queries (JSON array):", value=default_json, height=200)
121
+
122
+ if st.button("🧮 Calculate MRR", type="primary"):
123
+ try:
124
+ import json
125
+ queries = json.loads(json_input)
126
+ result = calculate_mrr(queries)
127
+
128
+ st.divider()
129
+
130
+ # Summary metrics
131
+ col1, col2, col3, col4 = st.columns(4)
132
+ with col1:
133
+ st.metric("MRR", f"{result['mrr']:.4f}")
134
+ with col2:
135
+ st.metric("Top-1 Accuracy", f"{result['top1_accuracy']:.1%}")
136
+ with col3:
137
+ st.metric("Top-3 Accuracy", f"{result['top3_accuracy']:.1%}")
138
+ with col4:
139
+ st.metric("Found Rate", f"{result['found_rate']:.1%}")
140
+
141
+ # Interpretation
142
+ st.info(f"**Overall Quality:** {interpret_mrr(result['mrr'])}")
143
+
144
+ # Query details
145
+ st.subheader("📋 Query Details")
146
+
147
+ for detail in result['query_details']:
148
+ with st.expander(f"Q: {detail['question'][:50]}... (RR: {detail['reciprocal_rank']:.4f})"):
149
+ st.markdown(f"**Question:** {detail['question']}")
150
+ st.markdown(f"**Correct Answer:** {detail['correct_answer']}")
151
+
152
+ if detail['found']:
153
+ st.markdown(f"**Rank:** #{detail['rank']}")
154
+ st.markdown(f"**Reciprocal Rank:** {detail['reciprocal_rank']:.4f}")
155
+ else:
156
+ st.markdown("**Status:** Not found in ranked list")
157
+
158
+ st.markdown("**Top Rankings:**")
159
+ for i, ans in enumerate(detail['ranked_answers'][:5], 1):
160
+ is_correct = ans.lower() == detail['correct_answer'].lower()
161
+ icon = "✅" if is_correct else f"{i}."
162
+ st.markdown(f"{icon} {ans}")
163
+
164
+ # Bar chart of RR scores
165
+ st.subheader("📊 Reciprocal Rank Distribution")
166
+
167
+ import plotly.graph_objects as go
168
+ fig = go.Figure(data=[go.Bar(
169
+ x=[f"Q{i+1}" for i in range(len(result['reciprocal_ranks']))],
170
+ y=result['reciprocal_ranks'],
171
+ text=[f'{rr:.3f}' for rr in result['reciprocal_ranks']],
172
+ textposition='auto',
173
+ marker_color=['green' if rr > 0 else 'red' for rr in result['reciprocal_ranks']]
174
+ )])
175
+ fig.update_layout(
176
+ title="Reciprocal Rank per Query",
177
+ yaxis=dict(title="Reciprocal Rank", range=[0, 1.1]),
178
+ xaxis_title="Query"
179
+ )
180
+ st.plotly_chart(fig, use_container_width=True)
181
+
182
+ except Exception as e:
183
+ st.error(f"Error parsing JSON: {e}")
184
+
185
+ # Formula section
186
+ create_formula_expander('mrr')
187
+
188
+ # Additional info
189
+ st.divider()
190
+ with st.expander("💡 When to Use MRR"):
191
+ st.markdown("""
192
+ **MRR is ideal for:**
193
+ - **Question Answering**: Does the system rank the correct answer highly?
194
+ - **Search Engines**: Are relevant results in the top positions?
195
+ - **Recommendation Systems**: Is the relevant item recommended early?
196
+
197
+ **vs Accuracy:**
198
+ - Accuracy: "Is the first answer correct?" (harsh)
199
+ - MRR: "How high is the correct answer ranked?" (nuanced)
200
+
201
+ **Example:**
202
+ - Top-1 Accuracy of 60% means 60% of queries have correct answer at #1
203
+ - MRR of 0.6 means on average, correct answer is at position ~1.67
204
+ """)
pages/06_bert_score.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """BERT Score evaluation page."""
2
+
3
+ import streamlit as st
4
+ import sys
5
+ sys.path.append('/Users/mymac/Documents/applyjob/supportProjects/llm-evaluation-dashboard')
6
+
7
+ from modules.metrics.bert_score import calculate_bert_score, calculate_token_level_similarity, interpret_bert_score, get_bert_score_grade
8
+ from modules.visualizations.charts import create_heatmap, create_gauge_chart
9
+ from modules.visualizations.explanations import create_formula_expander
10
+ from modules.utils.examples import get_text_example_names, get_text_example
11
+
12
+ st.set_page_config(page_title="BERT Score", page_icon="🤖", layout="wide")
13
+
14
+ st.title("🤖 BERT Score Evaluation")
15
+
16
+ st.markdown("""
17
+ **Semantic Similarity via Embeddings** — Uses contextual embeddings to measure meaning similarity.
18
+ Best for: **Paraphrase Detection** and **Semantic Quality** assessment.
19
+ """)
20
+
21
+ # Input section
22
+ col1, col2 = st.columns(2)
23
+
24
+ with col1:
25
+ st.subheader("📄 Ground Truth (Reference)")
26
+ example_name = st.selectbox("Choose example:", ["Custom"] + get_text_example_names(), key="bert_example")
27
+
28
+ if example_name != "Custom":
29
+ example = get_text_example(example_name)
30
+ ground_truth = st.text_area("Reference text:", value=example.ground_truth, height=150, key="bert_ref")
31
+ st.caption(f"Example: {example.description}")
32
+ else:
33
+ ground_truth = st.text_area("Reference text:",
34
+ value="The cat sat on the mat and looked outside.",
35
+ height=150, key="bert_ref")
36
+
37
+ with col2:
38
+ st.subheader("🤖 Model Prediction (Candidate)")
39
+ if example_name != "Custom":
40
+ prediction = st.text_area("Candidate text:", value=example.prediction, height=150, key="bert_cand")
41
+ else:
42
+ prediction = st.text_area("Candidate text:",
43
+ value="The cat was sitting on the mat and gazing outdoors.",
44
+ height=150, key="bert_cand")
45
+
46
+ # Calculate button
47
+ if st.button("🧮 Calculate BERT Score", type="primary"):
48
+ result = calculate_bert_score(ground_truth, prediction)
49
+
50
+ st.divider()
51
+
52
+ # Main score display
53
+ col_gauge, col_info = st.columns([1, 2])
54
+
55
+ with col_gauge:
56
+ st.plotly_chart(create_gauge_chart(result['f1'], "BERT Score F1"), use_container_width=True)
57
+
58
+ with col_info:
59
+ st.subheader("📊 Semantic Similarity Analysis")
60
+
61
+ f1 = result['f1']
62
+ grade = get_bert_score_grade(f1)
63
+ interpretation = interpret_bert_score(f1)
64
+
65
+ st.markdown(f"**F1 Score:** {f1:.4f}")
66
+ st.markdown(f"**Grade:** {grade}")
67
+ st.markdown(f"**Quality:** {interpretation}")
68
+ st.markdown(f"**Cosine Similarity:** {result['cosine_similarity']:.4f}")
69
+
70
+ if not result.get('is_real_model', False):
71
+ st.caption(result.get('note', ''))
72
+
73
+ # Token-level analysis
74
+ st.subheader("🔍 Token-Level Semantic Alignment")
75
+
76
+ token_result = calculate_token_level_similarity(ground_truth, prediction)
77
+
78
+ if token_result['ref_tokens'] and token_result['cand_tokens']:
79
+ # Heatmap
80
+ st.markdown("**Similarity Matrix (Token-to-Token):**")
81
+
82
+ # Limit size for readability
83
+ max_display = min(15, len(token_result['ref_tokens']), len(token_result['cand_tokens']))
84
+
85
+ matrix_subset = [row[:max_display] for row in token_result['similarity_matrix'][:max_display]]
86
+ ref_labels = token_result['ref_tokens'][:max_display]
87
+ cand_labels = token_result['cand_tokens'][:max_display]
88
+
89
+ fig = create_heatmap(matrix_subset, cand_labels, ref_labels, "Token Similarity Matrix")
90
+ st.plotly_chart(fig, use_container_width=True)
91
+
92
+ # Best matches
93
+ st.markdown("**Best Token Matches:**")
94
+
95
+ matches = token_result['matches']
96
+ if matches:
97
+ for match in matches[:10]:
98
+ sim_pct = match['similarity'] * 100
99
+ st.markdown(f"- **{match['ref_token']}** ↔ **{match['cand_token']}**: {sim_pct:.1f}% similar")
100
+ else:
101
+ st.warning("No significant token matches found (threshold: 30%)")
102
+
103
+ # Precision/Recall breakdown
104
+ st.subheader("📈 Precision vs Recall Breakdown")
105
+
106
+ col_p, col_r = st.columns(2)
107
+ with col_p:
108
+ st.metric("Precision", f"{token_result['precision']:.4f}")
109
+ st.caption("Avg best match from candidate → reference")
110
+ with col_r:
111
+ st.metric("Recall", f"{token_result['recall']:.4f}")
112
+ st.caption("Avg best match from reference → candidate")
113
+
114
+ # Comparison with lexical metrics
115
+ st.subheader("🔄 Why BERT Score is Different")
116
+
117
+ col_lex, col_sem = st.columns(2)
118
+
119
+ with col_lex:
120
+ st.markdown("**Lexical Metrics (BLEU/ROGUE):**")
121
+ st.markdown("""
122
+ - "sat" ≠ "was sitting" ❌
123
+ - "looked" ≠ "gazing" ❌
124
+ - "outside" ≠ "outdoors" ❌
125
+ - **Score: Low** (different words)
126
+ """)
127
+
128
+ with col_sem:
129
+ st.markdown("**BERT Score (Semantic):**")
130
+ st.markdown("""
131
+ - "sat" ≈ "was sitting" ✅
132
+ - "looked" ≈ "gazing" ✅
133
+ - "outside" ≈ "outdoors" ✅
134
+ - **Score: High** (same meaning)
135
+ """)
136
+
137
+ # Formula section
138
+ create_formula_expander('bert_score')
139
+
140
+ # Additional info
141
+ st.divider()
142
+ with st.expander("💡 BERT Score Advantages"):
143
+ st.markdown("""
144
+ **Why use BERT Score over BLEU/ROGUE?**
145
+
146
+ | Scenario | BLEU/ROGUE | BERT Score |
147
+ |----------|------------|------------|
148
+ | "happy" vs "joyful" | Low | **High** ✅ |
149
+ | "car" vs "automobile" | Low | **High** ✅ |
150
+ | "run fast" vs "sprint" | Low | **High** ✅ |
151
+ | Paraphrases | Low | **High** ✅ |
152
+ | Word-for-word match | High | High |
153
+
154
+ **How it works:**
155
+ 1. Convert each token to contextual embedding (768-dim vector)
156
+ 2. Calculate cosine similarity between all token pairs
157
+ 3. Greedy matching: each token matches to most similar counterpart
158
+ 4. Average similarities = Precision/Recall
159
+
160
+ **Note:** This demo uses simplified embeddings. Real BERT Score uses pre-trained BERT.
161
+ """)
plan.md ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LLM Evaluation Metrics Dashboard
2
+
3
+ A comprehensive Streamlit-based educational dashboard demonstrating 5 key LLM evaluation metrics (BLEU, ROGUE, Perplexity, MRR, BERT Score) with interactive visualizations and mathematical explanations.
4
+
5
+ ## Project Structure
6
+
7
+ ```
8
+ llm-evaluation-dashboard/
9
+ ├── app.py # Main entry point with page routing
10
+ ├── pages/
11
+ │ ├── 01_overview.py # Comparison dashboard (all metrics)
12
+ │ ├── 02_bleu_score.py # BLEU detailed page
13
+ │ ├── 03_rogue_score.py # ROGUE detailed page
14
+ │ ├── 04_perplexity.py # Perplexity detailed page
15
+ │ ├── 05_mrr.py # MRR detailed page
16
+ │ └── 06_bert_score.py # BERT Score detailed page
17
+ ├── modules/
18
+ │ ├── metrics/
19
+ │ │ ├── __init__.py
20
+ │ │ ├── bleu.py # BLEU implementation with n-gram precision
21
+ │ │ ├── rogue.py # ROGUE variants (N, L, S, SU, W)
22
+ │ │ ├── perplexity.py # Perplexity calculation
23
+ │ │ ├── mrr.py # Mean Reciprocal Rank
24
+ │ │ └── bert_score.py # BERT-based similarity
25
+ │ ├── visualizations/
26
+ │ │ ├── __init__.py
27
+ │ │ ├── charts.py # Plotly charts for metrics
28
+ │ │ └── explanations.py # Mathematical formula displays
29
+ │ └── utils/
30
+ │ ├── __init__.py
31
+ │ ├── text_processing.py # Tokenization helpers
32
+ │ └── examples.py # Sample ground truth/prediction pairs
33
+ ├── data/
34
+ │ └── sample_evaluations.json # Pre-loaded test cases
35
+ ├── requirements.txt
36
+ └── README.md
37
+ ```
38
+
39
+ ## Key Features
40
+
41
+ ### 1. Overview Page (Comparison Dashboard)
42
+ - Side-by-side input for Ground Truth vs Predicted text
43
+ - Radar chart showing all 5 metrics simultaneously
44
+ - Metric cards with scores and grade interpretation
45
+ - Export results to JSON/CSV
46
+
47
+ ### 2. Individual Metric Pages (Educational Focus)
48
+ Each page includes:
49
+ - **Interactive Calculator**: Input custom text, see real-time score
50
+ - **Step-by-Step Breakdown**: Visual walkthrough of calculation
51
+ - **Mathematical Formula**: LaTeX-rendered equations
52
+ - **Visual Explanations**: Color-coded n-gram matching, alignment matrices
53
+ - **Preset Examples**: 3-4 curated examples demonstrating edge cases
54
+
55
+ ## Metric Implementations
56
+
57
+ ### BLEU Score
58
+ - **Features**: N-gram precision (1-4), brevity penalty visualization
59
+ - **UI**: Highlight matching n-grams, show precision@N breakdown
60
+ - **Formula**: `BP × exp(Σ w_n × log(p_n))`
61
+
62
+ ### ROGUE Score (5 Variants)
63
+ - **ROGUE-N**: Unigram/bigram overlap (precision/recall/F1)
64
+ - **ROGUE-L**: Longest Common Subsequence visualization
65
+ - **ROGUE-S**: Skip-bigram matching
66
+ - **ROGUE-SU**: Unigram + Skip-bigram combined
67
+ - **ROGUE-W**: Weighted LCS with consecutive bonus
68
+ - **UI**: Alignment view showing matched sequences
69
+
70
+ ### Perplexity
71
+ - **Features**: Next-token probability visualization
72
+ - **Note**: Simulated with API-based approximation (no model weights required)
73
+ - **UI**: Token-by-token probability breakdown
74
+
75
+ ### MRR (Mean Reciprocal Rank)
76
+ - **Features**: Multi-query ranking evaluation
77
+ - **UI**: Ranked list visualization with position highlighting
78
+ - **Use case**: QA/retrieval system evaluation
79
+
80
+ ### BERT Score
81
+ - **Features**: Embedding-based semantic similarity
82
+ - **Implementation**: Using `bert-score` library or sentence-transformers
83
+ - **UI**: Token alignment heatmap with cosine similarity matrix
84
+
85
+ ## Tech Stack
86
+
87
+ | Component | Library |
88
+ |-----------|---------|
89
+ | UI Framework | Streamlit |
90
+ | Math Rendering | `st.latex()` + custom CSS |
91
+ | Visualizations | Plotly (interactive), Matplotlib (static) |
92
+ | NLP/Metrics | NLTK, rouge-score, bert-score, evaluate |
93
+ | Embeddings | sentence-transformers (all-MiniLM-L6-v2) |
94
+ | Utils | Pandas, NumPy |
95
+
96
+ ## Implementation Phases
97
+
98
+ ### Phase 1: Core Structure (Day 1)
99
+ - Project scaffolding
100
+ - Page routing system
101
+ - Shared sidebar navigation
102
+ - Common utilities (tokenization, text processing)
103
+
104
+ ### Phase 2: BLEU + ROGUE (Day 2)
105
+ - BLEU with n-gram visualization
106
+ - ROGUE-N and ROGUE-L implementations
107
+ - Step-by-step calculation displays
108
+ - Educational content with examples
109
+
110
+ ### Phase 3: Perplexity + MRR (Day 3)
111
+ - Perplexity calculator (approximation)
112
+ - MRR ranking visualization
113
+ - Interactive test case builder
114
+
115
+ ### Phase 4: BERT Score + Polish (Day 4)
116
+ - BERT embedding similarity
117
+ - Token alignment visualization
118
+ - Overview comparison page
119
+ - Export functionality
120
+
121
+ ### Phase 5: Documentation (Day 5)
122
+ - README with screenshots
123
+ - Docstrings for all functions
124
+ - Deployment to HuggingFace Spaces
125
+ - CV integration
126
+
127
+ ## CV Integration
128
+
129
+ Add to Technical Projects section:
130
+
131
+ ```
132
+ \projectHeading
133
+ {LLM Evaluation Metrics Dashboard}{\href{https://huggingface.co/spaces/...}{Live Demo}}{Python, Streamlit, NLTK, Plotly}
134
+ \begin{itemize}[leftmargin=0.15in, label={--}, nosep]
135
+ \small{\resumeItem{Built interactive educational dashboard demonstrating 5 key NLP evaluation metrics: BLEU, ROGUE, Perplexity, MRR, BERT Score.}}
136
+ \small{\resumeItem{Implemented step-by-step visualizations showing n-gram matching, longest common subsequences, and embedding similarity alignment.}}
137
+ \small{\resumeItem{Created comparison framework for evaluating text generation quality across lexical and semantic dimensions.}}
138
+ \end{itemize}
139
+ ```
140
+
141
+ ## Key Learning Outcomes (for interviews)
142
+
143
+ 1. **BLEU**: Understanding precision vs recall trade-offs, brevity penalty importance
144
+ 2. **ROGUE**: Why recall matters for summarization, LCS vs n-gram approaches
145
+ 3. **Perplexity**: Model confidence interpretation, relationship to cross-entropy
146
+ 4. **MRR**: Ranking evaluation for retrieval systems
147
+ 5. **BERT Score**: Semantic similarity beyond lexical overlap
148
+
149
+ ## Deployment
150
+
151
+ - Primary: HuggingFace Spaces (free, quick setup)
152
+ - Fallback: Streamlit Community Cloud
153
+ - README badges: Live Demo | MIT License
154
+
155
+ ## Notes
156
+
157
+ - All metrics use established libraries (don't reinvent, wrap and visualize)
158
+ - Focus on educational value: "show the math, not just the score"
159
+ - Mobile-responsive design (Streamlit handles most of this)
160
+ - No API keys required for core functionality
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit>=1.28.0
2
+ nltk>=3.8.1
3
+ rouge-score>=0.1.2
4
+ bert-score>=0.3.13
5
+ sentence-transformers>=2.2.2
6
+ plotly>=5.18.0
7
+ numpy>=1.24.0
8
+ pandas>=2.0.0
9
+ transformers>=4.35.0
10
+ torch>=2.1.0