Spaces:
Runtime error
Runtime error
Commit ·
99f6668
0
Parent(s):
Initial commit: LLM Evaluation Dashboard
Browse files- .gitignore +14 -0
- README.md +202 -0
- app.py +59 -0
- data/sample_evaluations.json +40 -0
- modules/__init__.py +0 -0
- modules/metrics/__init__.py +0 -0
- modules/metrics/bert_score.py +270 -0
- modules/metrics/bleu.py +199 -0
- modules/metrics/mrr.py +214 -0
- modules/metrics/perplexity.py +195 -0
- modules/metrics/rogue.py +352 -0
- modules/utils/__init__.py +0 -0
- modules/utils/examples.py +150 -0
- modules/utils/text_processing.py +151 -0
- modules/visualizations/__init__.py +0 -0
- modules/visualizations/charts.py +317 -0
- modules/visualizations/explanations.py +159 -0
- pages/01_overview.py +209 -0
- pages/02_bleu_score.py +140 -0
- pages/03_rogue_score.py +195 -0
- pages/04_perplexity.py +160 -0
- pages/05_mrr.py +204 -0
- pages/06_bert_score.py +161 -0
- plan.md +160 -0
- requirements.txt +10 -0
.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 |
+
[](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 |
+

|
| 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
|