feat: add run.sh script and update documentation
- Added run.sh script with init, upd, run, and clean commands - Updated README.md to document run.sh usage and examples - Added documentation on Score calculation methodology - Updated base.py to include score calculation logic ``` This commit message follows the conventional commit format with a short title and a detailed description of the changes made. It explains what was changed and why, making it clear and informative.
This commit is contained in:
parent
33ba55f4c1
commit
774d8fed1d
60
README.md
60
README.md
@ -10,6 +10,14 @@ pip install -r requirements.txt
|
||||
|
||||
## Использование
|
||||
|
||||
### Через скрипт run.sh
|
||||
|
||||
```bash
|
||||
./run.sh run --model llama3 --ollama-url http://localhost:11434
|
||||
```
|
||||
|
||||
### Через Python
|
||||
|
||||
```bash
|
||||
python src/main.py --model llama3 --ollama-url http://localhost:11434
|
||||
```
|
||||
@ -24,19 +32,24 @@ python src/main.py --model llama3 --ollama-url http://localhost:11434
|
||||
|
||||
### Примеры
|
||||
|
||||
Запуск всех бенчмарков:
|
||||
Запуск всех бенчмарков через скрипт:
|
||||
```bash
|
||||
python src/main.py --model llama3 --ollama-url http://localhost:11434
|
||||
./run.sh run --model llama3 --ollama-url http://localhost:11434
|
||||
```
|
||||
|
||||
Запуск только тестов переводов:
|
||||
```bash
|
||||
python src/main.py --model llama3 --ollama-url http://localhost:11434 --benchmarks translation
|
||||
./run.sh run --model llama3 --ollama-url http://localhost:11434 --benchmarks translation
|
||||
```
|
||||
|
||||
Очистка отчетов:
|
||||
```bash
|
||||
./run.sh clean
|
||||
```
|
||||
|
||||
Запуск с подробным выводом:
|
||||
```bash
|
||||
python src/main.py --model llama3 --ollama-url http://localhost:11434 --verbose
|
||||
./run.sh run --model llama3 --ollama-url http://localhost:11434 --verbose
|
||||
```
|
||||
|
||||
## Структура проекта
|
||||
@ -84,3 +97,42 @@ ai-benchmark/
|
||||
## Результаты
|
||||
|
||||
После выполнения бенчмарков в директории `results/` будут сгенерированы файлы в формате Markdown с таблицами результатов. Каждый бенчмарк будет иметь свой отчет, а также будет создан сводный отчет со статистикой по всем тестам.
|
||||
|
||||
## Методика расчета Score (Скор)
|
||||
|
||||
### Основная метрика: F1-score
|
||||
|
||||
Каждый тест оценивается по метрике F1-score, которая вычисляется на основе сходства между ответом модели и ожидаемым ответом:
|
||||
|
||||
1. **Токенизация**: Ответ модели и ожидаемый ответ разбиваются на отдельные токены (слова)
|
||||
2. **Precision (Точность)**: Доля токенов из ответа модели, которые присутствуют в ожидаемом ответе
|
||||
3. **Recall (Полнота)**: Доля токенов из ожидаемого ответа, которые присутствуют в ответе модели
|
||||
4. **F1-score**: Гармоническое среднее между точностью и полнотой:
|
||||
```
|
||||
F1 = 2 × (Precision × Recall) / (Precision + Recall)
|
||||
```
|
||||
5. **Диапазон**: 0.0 - 1.0, где 1.0 означает идеальное совпадение
|
||||
|
||||
### Альтернативные метрики
|
||||
|
||||
Для более детального анализа можно использовать следующие метрики:
|
||||
|
||||
- **Levenshtein Distance / Edit Distance**: Количество редактирований (вставок, удалений, замен) для преобразования ответа модели в ожидаемый ответ. Полезно для оценки структурных различий.
|
||||
- **BLEU Score**: Популярная метрика для оценки качества машинного перевода, основанная на n-граммах. Подходит для задач переводов.
|
||||
- **ROUGE Score**: Метрика для оценки качества суммаризации, основанная на перекрытии n-грамм, слов и последовательностей. Подходит для задач пересказов.
|
||||
- **Code Similarity Metrics**: Для генерации кода можно использовать процент совпадения структуры кода (функции, классы, синтаксис), а также метрики типа AST (Abstract Syntax Tree) similarity.
|
||||
|
||||
### Средний Score
|
||||
|
||||
В отчетах вычисляется средний Score по всем успешно выполненным тестам в каждом бенчмарке. Этот показатель позволяет сравнить общую производительность модели по разным задачам.
|
||||
|
||||
### Пример расчета F1-score
|
||||
|
||||
Если модель ответила "hello world" на промпт, а ожидаемый ответ "hello there", расчет будет следующим:
|
||||
|
||||
- Токены модели: {"hello", "world"}
|
||||
- Токены ожидаемого: {"hello", "there"}
|
||||
- Пересечение: {"hello"}
|
||||
- Precision = 1/2 = 0.5
|
||||
- Recall = 1/2 = 0.5
|
||||
- F1-score = 2 × (0.5 × 0.5) / (0.5 + 0.5) = 0.5
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
# Отчет бенчмарка: codegen
|
||||
|
||||
**Дата:** 2026-01-16 19:54:24
|
||||
|
||||
**Общее количество тестов:** 1
|
||||
|
||||
**Успешно выполнено:** 1
|
||||
|
||||
## Результаты тестов
|
||||
|
||||
```
|
||||
+-----+-----+---------+-----------------------------------------------------+-----------------------------------------------------+-----------------------------------------------------+
|
||||
| Тест| Скор|Время (с)| Промпт | Ожидаемый | Ответ модели |
|
||||
+-----+-----+---------+-----------------------------------------------------+-----------------------------------------------------+-----------------------------------------------------+
|
||||
| Тест| Скор|Время (с)| Промпт | Ожидаемый | Ответ модели |
|
||||
+-----+-----+---------+-----------------------------------------------------+-----------------------------------------------------+-----------------------------------------------------+
|
||||
|test1|0.239| 3.51 |Write a Python function that calculates the factor...|def factorial(n):\n if n == 0 or n == 1:\n ...|```python
|
||||
def factorial(n):
|
||||
"""
|
||||
Calculate ...|
|
||||
+-----+-----+---------+-----------------------------------------------------+-----------------------------------------------------+-----------------------------------------------------+```
|
||||
|
||||
## Статистика
|
||||
|
||||
- **Средний скор:** 0.239
|
||||
- **Среднее время ответа:** 3.507 секунд
|
||||
@ -1,23 +0,0 @@
|
||||
# Отчет бенчмарка: summarization
|
||||
|
||||
**Дата:** 2026-01-16 19:54:24
|
||||
|
||||
**Общее количество тестов:** 1
|
||||
|
||||
**Успешно выполнено:** 1
|
||||
|
||||
## Результаты тестов
|
||||
|
||||
```
|
||||
+-----+-----+---------+-----------------------------------------------------+-----------------------------------------------------+-----------------------------------------------------+
|
||||
| Тест| Скор|Время (с)| Промпт | Ожидаемый | Ответ модели |
|
||||
+-----+-----+---------+-----------------------------------------------------+-----------------------------------------------------+-----------------------------------------------------+
|
||||
| Тест| Скор|Время (с)| Промпт | Ожидаемый | Ответ модели |
|
||||
+-----+-----+---------+-----------------------------------------------------+-----------------------------------------------------+-----------------------------------------------------+
|
||||
|test1|0.571| 1.21 |Summarize the following text in 1-2 sentences: 'Th...|A quick fox jumps over a lazy dog, surprising it. ...|In a brief summary, the quick brown fox jumps over...|
|
||||
+-----+-----+---------+-----------------------------------------------------+-----------------------------------------------------+-----------------------------------------------------+```
|
||||
|
||||
## Статистика
|
||||
|
||||
- **Средний скор:** 0.571
|
||||
- **Среднее время ответа:** 1.206 секунд
|
||||
@ -1,44 +0,0 @@
|
||||
# Сводный отчет по всем бенчмаркам
|
||||
|
||||
**Дата:** 2026-01-16 19:54:24
|
||||
|
||||
**Модель:** rnj-1:8b
|
||||
|
||||
## Общие результаты
|
||||
|
||||
```
|
||||
+-------------+------+-------+------------+-------------+
|
||||
| Бенчмарк |Тестов|Успешно|Средний скор|Среднее время|
|
||||
+-------------+------+-------+------------+-------------+
|
||||
| Бенчмарк |Тестов|Успешно|Средний скор|Среднее время|
|
||||
+-------------+------+-------+------------+-------------+
|
||||
| translation | 2 | 2 | 0.666 | 1.262 |
|
||||
+-------------+------+-------+------------+-------------+
|
||||
|summarization| 1 | 1 | 0.571 | 1.206 |
|
||||
+-------------+------+-------+------------+-------------+
|
||||
| codegen | 1 | 1 | 0.239 | 3.507 |
|
||||
+-------------+------+-------+------------+-------------+```
|
||||
|
||||
## Подробности
|
||||
|
||||
### translation
|
||||
|
||||
- **Тестов:** 2
|
||||
- **Успешно:** 2
|
||||
- **Средний скор:** 0.666
|
||||
- **Среднее время:** 1.262 секунд
|
||||
|
||||
### summarization
|
||||
|
||||
- **Тестов:** 1
|
||||
- **Успешно:** 1
|
||||
- **Средний скор:** 0.571
|
||||
- **Среднее время:** 1.206 секунд
|
||||
|
||||
### codegen
|
||||
|
||||
- **Тестов:** 1
|
||||
- **Успешно:** 1
|
||||
- **Средний скор:** 0.239
|
||||
- **Среднее время:** 3.507 секунд
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
# Отчет бенчмарка: translation
|
||||
|
||||
**Дата:** 2026-01-16 19:54:24
|
||||
|
||||
**Общее количество тестов:** 2
|
||||
|
||||
**Успешно выполнено:** 2
|
||||
|
||||
## Результаты тестов
|
||||
|
||||
```
|
||||
+-----+-----+---------+-----------------------------------------------------+-------------------------+-------------------------+
|
||||
| Тест| Скор|Время (с)| Промпт | Ожидаемый | Ответ модели |
|
||||
+-----+-----+---------+-----------------------------------------------------+-------------------------+-------------------------+
|
||||
| Тест| Скор|Время (с)| Промпт | Ожидаемый | Ответ модели |
|
||||
+-----+-----+---------+-----------------------------------------------------+-------------------------+-------------------------+
|
||||
|test1| 1.0 | 2.21 |Translate the following English text to Russian: '...|Привет, как дела сегодня?|Привет, как дела сегодня?|
|
||||
+-----+-----+---------+-----------------------------------------------------+-------------------------+-------------------------+
|
||||
|test2|0.333| 0.32 |Translate the following Russian text to English: '...| How are you? | "How are you?" |
|
||||
+-----+-----+---------+-----------------------------------------------------+-------------------------+-------------------------+```
|
||||
|
||||
## Статистика
|
||||
|
||||
- **Средний скор:** 0.666
|
||||
- **Среднее время ответа:** 1.262 секунд
|
||||
13
run.sh
13
run.sh
@ -18,6 +18,11 @@ upd() {
|
||||
git submodule update --remote --merge
|
||||
}
|
||||
|
||||
clean() {
|
||||
rm -rf results/*
|
||||
echo "Отчеты успешно очищены"
|
||||
}
|
||||
|
||||
activate() {
|
||||
source z/bin/activate
|
||||
}
|
||||
@ -28,10 +33,18 @@ if [ -n "$1" ]; then
|
||||
init
|
||||
elif [[ "$1" == "upd" ]]; then
|
||||
upd
|
||||
elif [[ "$1" == "run" ]]; then
|
||||
activate
|
||||
shift
|
||||
python src/main.py "$@"
|
||||
elif [[ "$1" == "clean" ]]; then
|
||||
clean
|
||||
fi
|
||||
else
|
||||
echo " Аргументом необходимо написать название скрипта (+опционально аргументы скрипта)"
|
||||
echo "Скрипты:"
|
||||
echo " * init - инициализация, устанавливает env"
|
||||
echo " * upd - обновление зависимостей"
|
||||
echo " * run - запуск бенчмарков"
|
||||
echo " * clean - очистка отчетов"
|
||||
fi
|
||||
|
||||
Binary file not shown.
@ -1,5 +1,7 @@
|
||||
import logging
|
||||
import time
|
||||
import os
|
||||
import json
|
||||
from typing import Dict, Any, List
|
||||
from abc import ABC, abstractmethod
|
||||
from models.ollama_client import OllamaClient
|
||||
@ -52,6 +54,8 @@ class Benchmark(ABC):
|
||||
Returns:
|
||||
Результаты бенчмарка
|
||||
"""
|
||||
from utils.scoring import get_all_scores
|
||||
|
||||
test_cases = self.load_test_data()
|
||||
results = []
|
||||
|
||||
@ -76,13 +80,21 @@ class Benchmark(ABC):
|
||||
# Оценка качества
|
||||
score = self.evaluate(model_response, test_case['expected'])
|
||||
|
||||
# Вычисление всех дополнительных метрик
|
||||
scores = get_all_scores(model_response, test_case['expected'])
|
||||
|
||||
results.append({
|
||||
'test_case': test_case['name'],
|
||||
'prompt': prompt,
|
||||
'expected': test_case['expected'],
|
||||
'model_response': model_response,
|
||||
'score': score,
|
||||
'latency': latency
|
||||
'latency': latency,
|
||||
'f1_score': scores['f1_score'],
|
||||
'normalized_levenshtein': scores['normalized_levenshtein'],
|
||||
'bleu_score': scores['bleu_score'],
|
||||
'rouge_scores': scores['rouge_scores'],
|
||||
'code_similarity': scores['code_similarity']
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
|
||||
12
src/main.py
12
src/main.py
@ -53,12 +53,12 @@ def run_benchmarks(ollama_client: OllamaClient, model_name: str, benchmarks: Lis
|
||||
def main():
|
||||
"""Основная функция запуска."""
|
||||
parser = argparse.ArgumentParser(description='LLM Benchmarking Tool')
|
||||
parser.add_argument('--model', required=True, help='Название модели для тестирования')
|
||||
parser.add_argument('--ollama-url', required=True, help='URL подключения к Ollama серверу')
|
||||
parser.add_argument('--benchmarks', nargs='+', default=['translation', 'summarization', 'codegen'],
|
||||
parser.add_argument('-m', '--model', required=True, help='Название модели для тестирования')
|
||||
parser.add_argument('-u', '--ollama-url', default='http://localhost:11434', help='URL подключения к Ollama серверу')
|
||||
parser.add_argument('-b', '--benchmarks', nargs='+', default=['translation', 'summarization', 'codegen'],
|
||||
help='Список бенчмарков для выполнения (translation, summarization, codegen)')
|
||||
parser.add_argument('--output', default='results', help='Директория для сохранения результатов')
|
||||
parser.add_argument('--verbose', action='store_true', help='Подробный режим вывода')
|
||||
parser.add_argument('-o', '--output', default='results', help='Директория для сохранения результатов')
|
||||
parser.add_argument('-v', '--verbose', action='store_true', help='Подробный режим вывода')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@ -83,7 +83,7 @@ def main():
|
||||
report_generator.generate_benchmark_report(result, args.output, args.model)
|
||||
|
||||
if len(results) > 1:
|
||||
report_generator.generate_summary_report(results, args.output, args.model)
|
||||
report_generator.generate_summary_report(results, args.output, args.model, args.ollama_url)
|
||||
|
||||
logging.info("Benchmarking completed successfully!")
|
||||
|
||||
|
||||
Binary file not shown.
BIN
src/utils/__pycache__/scoring.cpython-313.pyc
Normal file
BIN
src/utils/__pycache__/scoring.cpython-313.pyc
Normal file
Binary file not shown.
@ -42,11 +42,15 @@ class ReportGenerator:
|
||||
table_data = [
|
||||
{
|
||||
"Тест": "Тест",
|
||||
"Скор": "Скор",
|
||||
"F1-Score": "F1-Score",
|
||||
"Levenshtein": "Levenshtein",
|
||||
"BLEU": "BLEU",
|
||||
"ROUGE-1": "ROUGE-1",
|
||||
"ROUGE-2": "ROUGE-2",
|
||||
"ROUGE-L": "ROUGE-L",
|
||||
"Code Similarity": "Code Similarity",
|
||||
"Время (с)": "Время (с)",
|
||||
"Промпт": "Промпт",
|
||||
"Ожидаемый": "Ожидаемый",
|
||||
"Ответ модели": "Ответ модели"
|
||||
"Лог файл": "Лог файл"
|
||||
}
|
||||
]
|
||||
|
||||
@ -54,26 +58,86 @@ class ReportGenerator:
|
||||
if 'error' in result:
|
||||
table_data.append({
|
||||
"Тест": result['test_case'],
|
||||
"Скор": "Ошибка",
|
||||
"F1-Score": "Ошибка",
|
||||
"Levenshtein": "-",
|
||||
"BLEU": "-",
|
||||
"ROUGE-1": "-",
|
||||
"ROUGE-2": "-",
|
||||
"ROUGE-L": "-",
|
||||
"Code Similarity": "-",
|
||||
"Время (с)": "-",
|
||||
"Промпт": result['prompt'][:50] + "..." if len(result['prompt']) > 50 else result['prompt'],
|
||||
"Ожидаемый": result['expected'][:50] + "..." if len(result['expected']) > 50 else result['expected'],
|
||||
"Ответ модели": result['error']
|
||||
"Лог файл": "-"
|
||||
})
|
||||
else:
|
||||
# Извлекаем все метрики из результата
|
||||
f1_score = result.get('f1_score', result.get('score', 0))
|
||||
levenshtein = result.get('normalized_levenshtein', 0)
|
||||
bleu = result.get('bleu_score', 0)
|
||||
rouge_scores = result.get('rouge_scores', {})
|
||||
code_sim = result.get('code_similarity', 0)
|
||||
|
||||
table_data.append({
|
||||
"Тест": result['test_case'],
|
||||
"Скор": str(result['score']),
|
||||
"F1-Score": f"{f1_score:.3f}" if f1_score else "-",
|
||||
"Levenshtein": f"{levenshtein:.3f}" if levenshtein else "-",
|
||||
"BLEU": f"{bleu:.3f}" if bleu else "-",
|
||||
"ROUGE-1": f"{rouge_scores.get('rouge1', 0):.3f}" if rouge_scores else "-",
|
||||
"ROUGE-2": f"{rouge_scores.get('rouge2', 0):.3f}" if rouge_scores else "-",
|
||||
"ROUGE-L": f"{rouge_scores.get('rougeL', 0):.3f}" if rouge_scores else "-",
|
||||
"Code Similarity": f"{code_sim:.3f}" if code_sim else "-",
|
||||
"Время (с)": f"{result['latency']:.2f}",
|
||||
"Промпт": result['prompt'][:50] + "..." if len(result['prompt']) > 50 else result['prompt'],
|
||||
"Ожидаемый": result['expected'][:50] + "..." if len(result['expected']) > 50 else result['expected'],
|
||||
"Ответ модели": result['model_response'][:50] + "..." if len(result['model_response']) > 50 else result['model_response']
|
||||
"Лог файл": f"{results['benchmark_name']}_{result['test_case']}.txt"
|
||||
})
|
||||
|
||||
f.write("## Результаты тестов\n\n")
|
||||
f.write(markdown_table(table_data).get_markdown())
|
||||
f.write("| Тест | F1-Score | Levenshtein | BLEU | ROUGE-1 | ROUGE-2 | ROUGE-L | Code Similarity | Время (с) | Лог файл |\n")
|
||||
f.write("|--|--|--|--|--|--|--|--|--|--|\n")
|
||||
|
||||
for result in results['results']:
|
||||
if 'error' in result:
|
||||
f.write(f"| {result['test_case']} | Ошибка | - | - | - | - | - | - | - | - |\n")
|
||||
else:
|
||||
# Извлекаем все метрики из результата
|
||||
f1_score = result.get('f1_score', result.get('score', 0))
|
||||
levenshtein = result.get('normalized_levenshtein', 0)
|
||||
bleu = result.get('bleu_score', 0)
|
||||
rouge_scores = result.get('rouge_scores', {})
|
||||
code_sim = result.get('code_similarity', 0)
|
||||
|
||||
f1_score_display = f"{f1_score:.3f}" if f1_score else "-"
|
||||
levenshtein_display = f"{levenshtein:.3f}" if levenshtein else "-"
|
||||
bleu_display = f"{bleu:.3f}" if bleu else "-"
|
||||
rouge1_display = f"{rouge_scores.get('rouge1', 0):.3f}" if rouge_scores else "-"
|
||||
rouge2_display = f"{rouge_scores.get('rouge2', 0):.3f}" if rouge_scores else "-"
|
||||
rougeL_display = f"{rouge_scores.get('rougeL', 0):.3f}" if rouge_scores else "-"
|
||||
code_sim_display = f"{code_sim:.3f}" if code_sim else "-"
|
||||
|
||||
f.write(f"| {result['test_case']} | "
|
||||
f"{f1_score_display} | "
|
||||
f"{levenshtein_display} | "
|
||||
f"{bleu_display} | "
|
||||
f"{rouge1_display} | "
|
||||
f"{rouge2_display} | "
|
||||
f"{rougeL_display} | "
|
||||
f"{code_sim_display} | "
|
||||
f"{result['latency']:.2f} | "
|
||||
f"{results['benchmark_name']}_{result['test_case']}.txt |\n")
|
||||
|
||||
f.write("\n\n")
|
||||
|
||||
# Сохранение request-response в лог
|
||||
if model_name:
|
||||
logs_dir = os.path.join(output_dir, "logs")
|
||||
os.makedirs(logs_dir, exist_ok=True)
|
||||
for result in results['results']:
|
||||
if 'error' in result:
|
||||
continue
|
||||
log_filename = os.path.join(logs_dir, f"{results['benchmark_name']}_{result['test_case']}.txt")
|
||||
with open(log_filename, 'w', encoding='utf-8') as log_file:
|
||||
log_file.write(f"Промпт:\n{result['prompt']}\n\n")
|
||||
log_file.write(f"Ответ модели:\n{result['model_response']}\n\n")
|
||||
log_file.write(f"Ожидаемый ответ:\n{result['expected']}\n")
|
||||
|
||||
# Статистика
|
||||
successful = [r for r in results['results'] if 'score' in r]
|
||||
if successful:
|
||||
@ -87,7 +151,7 @@ class ReportGenerator:
|
||||
self.logger.info(f"Report saved to {file_path}")
|
||||
return file_path
|
||||
|
||||
def generate_summary_report(self, all_results: List[Dict[str, Any]], output_dir: str = "results", model_name: str = None) -> str:
|
||||
def generate_summary_report(self, all_results: List[Dict[str, Any]], output_dir: str = "results", model_name: str = None, ollama_url: str = None) -> str:
|
||||
"""
|
||||
Генерация сводного отчета по всем бенчмаркам.
|
||||
|
||||
@ -95,6 +159,7 @@ class ReportGenerator:
|
||||
all_results: Список результатов всех бенчмарков
|
||||
output_dir: Директория для сохранения отчета
|
||||
model_name: Имя модели (для структурирования результатов)
|
||||
ollama_url: URL сервера Ollama
|
||||
|
||||
Returns:
|
||||
Путь к сгенерированному файлу
|
||||
@ -114,6 +179,8 @@ class ReportGenerator:
|
||||
f.write(f"**Дата:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
||||
if model_name:
|
||||
f.write(f"**Модель:** {model_name}\n\n")
|
||||
if ollama_url:
|
||||
f.write(f"**Ollama URL:** {ollama_url}\n\n")
|
||||
|
||||
# Таблица с общими результатами
|
||||
table_data = [
|
||||
@ -140,7 +207,16 @@ class ReportGenerator:
|
||||
})
|
||||
|
||||
f.write("## Общие результаты\n\n")
|
||||
f.write(markdown_table(table_data).get_markdown())
|
||||
f.write("| Бенчмарк | Тестов | Успешно | Средний скор | Среднее время |\n")
|
||||
f.write("|--|--|--|--|--|\n")
|
||||
|
||||
for result in all_results:
|
||||
successful = [r for r in result['results'] if 'score' in r]
|
||||
avg_score = sum(r['score'] for r in successful) / len(successful) if successful else 0
|
||||
avg_latency = sum(r['latency'] for r in successful) / len(successful) if successful else 0
|
||||
|
||||
f.write(f"| {result['benchmark_name']} | {result['total_tests']} | {result['successful_tests']} | {avg_score:.3f} | {avg_latency:.3f} |\n")
|
||||
|
||||
f.write("\n\n")
|
||||
|
||||
# Подробности по каждому бенчмарку
|
||||
|
||||
368
src/utils/scoring.py
Normal file
368
src/utils/scoring.py
Normal file
@ -0,0 +1,368 @@
|
||||
"""
|
||||
Модуль для вычисления различных метрик оценки качества ответов моделей.
|
||||
"""
|
||||
import re
|
||||
import math
|
||||
from typing import List, Tuple, Dict, Any
|
||||
from collections import Counter
|
||||
|
||||
def calculate_f1_score(model_response: str, expected: str) -> float:
|
||||
"""
|
||||
Вычисляет F1-score на основе совпадения токенов.
|
||||
|
||||
Args:
|
||||
model_response: Ответ от модели
|
||||
expected: Ожидаемый ответ
|
||||
|
||||
Returns:
|
||||
F1-score (0.0-1.0)
|
||||
"""
|
||||
model_tokens = set(model_response.lower().split())
|
||||
expected_tokens = set(expected.lower().split())
|
||||
|
||||
if len(expected_tokens) == 0:
|
||||
return 0.0
|
||||
|
||||
intersection = model_tokens.intersection(expected_tokens)
|
||||
precision = len(intersection) / len(model_tokens) if model_tokens else 0.0
|
||||
recall = len(intersection) / len(expected_tokens) if expected_tokens else 0.0
|
||||
|
||||
if (precision + recall) == 0:
|
||||
return 0.0
|
||||
f1 = 2 * (precision * recall) / (precision + recall)
|
||||
|
||||
return round(f1, 3)
|
||||
|
||||
def calculate_exact_match(model_response: str, expected: str) -> float:
|
||||
"""
|
||||
Вычисляет Exact Match Ratio (EM) - процент тестов с точным совпадением.
|
||||
|
||||
Args:
|
||||
model_response: Ответ от модели
|
||||
expected: Ожидаемый ответ
|
||||
|
||||
Returns:
|
||||
1.0 если ответ точно совпадает, иначе 0.0
|
||||
"""
|
||||
# Удаление лишних пробелов и табуляций
|
||||
model_clean = ' '.join(model_response.strip().split())
|
||||
expected_clean = ' '.join(expected.strip().split())
|
||||
|
||||
return 1.0 if model_clean == expected_clean else 0.0
|
||||
|
||||
def calculate_levenshtein_distance(model_response: str, expected: str) -> int:
|
||||
"""
|
||||
Вычисляет Levenshtein Distance (расстояние редактирования) между двумя строками.
|
||||
|
||||
Args:
|
||||
model_response: Ответ от модели
|
||||
expected: Ожидаемый ответ
|
||||
|
||||
Returns:
|
||||
Количество редактирований (вставок, удалений, замен)
|
||||
"""
|
||||
if len(expected) == 0:
|
||||
return len(model_response)
|
||||
if len(model_response) == 0:
|
||||
return len(expected)
|
||||
|
||||
# Матрица для хранения расстояний
|
||||
d = [[0] * (len(expected) + 1) for _ in range(len(model_response) + 1)]
|
||||
|
||||
# Инициализация
|
||||
for i in range(len(model_response) + 1):
|
||||
d[i][0] = i
|
||||
for j in range(len(expected) + 1):
|
||||
d[0][j] = j
|
||||
|
||||
# Заполнение матрицы
|
||||
for j in range(1, len(expected) + 1):
|
||||
for i in range(1, len(model_response) + 1):
|
||||
if model_response[i-1] == expected[j-1]:
|
||||
substitution_cost = 0
|
||||
else:
|
||||
substitution_cost = 1
|
||||
d[i][j] = min(
|
||||
d[i-1][j] + 1, # удаление
|
||||
d[i][j-1] + 1, # вставка
|
||||
d[i-1][j-1] + substitution_cost # замена
|
||||
)
|
||||
|
||||
return d[len(model_response)][len(expected)]
|
||||
|
||||
def calculate_normalized_levenshtein(model_response: str, expected: str) -> float:
|
||||
"""
|
||||
Вычисляет нормализованное Levenshtein Distance (0.0-1.0).
|
||||
|
||||
Args:
|
||||
model_response: Ответ от модели
|
||||
expected: Ожидаемый ответ
|
||||
|
||||
Returns:
|
||||
Нормализованное расстояние (0.0 = идентично, 1.0 = полностью разные)
|
||||
"""
|
||||
max_len = max(len(model_response), len(expected))
|
||||
if max_len == 0:
|
||||
return 0.0
|
||||
distance = calculate_levenshtein_distance(model_response, expected)
|
||||
return round(1.0 - (distance / max_len), 3)
|
||||
|
||||
def calculate_bleu_score(model_response: str, expected: str, ngram_weights: List[float] = None) -> float:
|
||||
"""
|
||||
Вычисляет BLEU Score на основе n-грамм.
|
||||
|
||||
Args:
|
||||
model_response: Ответ от модели
|
||||
expected: Ожидаемый ответ
|
||||
ngram_weights: Веса для разных n-грамм. По умолчанию [0.25, 0.25, 0.25, 0.25] для 1-4 грамм
|
||||
|
||||
Returns:
|
||||
BLEU Score (0.0-1.0)
|
||||
"""
|
||||
if ngram_weights is None:
|
||||
ngram_weights = [0.25, 0.25, 0.25, 0.25]
|
||||
|
||||
# Токенизация
|
||||
model_tokens = model_response.lower().split()
|
||||
expected_tokens = expected.lower().split()
|
||||
|
||||
if not model_tokens or not expected_tokens:
|
||||
return 0.0
|
||||
|
||||
# Вычисление precision для разных n-грамм
|
||||
precisions = []
|
||||
for n in range(1, 5):
|
||||
if n > len(model_tokens):
|
||||
precisions.append(0)
|
||||
continue
|
||||
|
||||
# Счетчики n-грамм в ответе модели
|
||||
model_ngrams = Counter(
|
||||
tuple(model_tokens[i:i+n])
|
||||
for i in range(len(model_tokens) - n + 1)
|
||||
)
|
||||
|
||||
# Счетчики n-грамм в ожидаемом ответе
|
||||
expected_ngrams = Counter(
|
||||
tuple(expected_tokens[i:i+n])
|
||||
for i in range(len(expected_tokens) - n + 1)
|
||||
)
|
||||
|
||||
if not model_ngrams:
|
||||
precisions.append(0)
|
||||
continue
|
||||
|
||||
# Вычисление precision с smoothing
|
||||
clipped_count = 0
|
||||
for ngram, count in model_ngrams.items():
|
||||
clipped_count += min(count, expected_ngrams.get(ngram, 0))
|
||||
|
||||
precision = clipped_count / len(model_ngrams)
|
||||
precisions.append(precision)
|
||||
|
||||
# Взвешенное среднее
|
||||
weighted_sum = sum(w * p for w, p in zip(ngram_weights[:len(precisions)], precisions))
|
||||
|
||||
# Бонус за brevity (длина ответа близка к ожидаемой)
|
||||
if len(model_tokens) > len(expected_tokens):
|
||||
bp = 1.0
|
||||
else:
|
||||
bp = math.exp(1 - len(expected_tokens) / len(model_tokens))
|
||||
|
||||
bleu_score = bp * math.exp(weighted_sum)
|
||||
return round(bleu_score, 3)
|
||||
|
||||
def calculate_rouge_scores(model_response: str, expected: str) -> Dict[str, float]:
|
||||
"""
|
||||
Вычисляет ROUGE Scores (ROUGE-1, ROUGE-2, ROUGE-L).
|
||||
|
||||
Args:
|
||||
model_response: Ответ от модели
|
||||
expected: Ожидаемый ответ
|
||||
|
||||
Returns:
|
||||
Словарь с ROUGE метриками: {'rouge1': ..., 'rouge2': ..., 'rougeL': ...}
|
||||
"""
|
||||
model_tokens = model_response.lower().split()
|
||||
expected_tokens = expected.lower().split()
|
||||
|
||||
if not model_tokens or not expected_tokens:
|
||||
return {'rouge1': 0.0, 'rouge2': 0.0, 'rougeL': 0.0}
|
||||
|
||||
# ROUGE-1: перекрытие unigrams
|
||||
model_grams = Counter(model_tokens)
|
||||
expected_grams = Counter(expected_tokens)
|
||||
|
||||
intersection = sum((model_grams & expected_grams).values())
|
||||
rouge1 = intersection / len(expected_grams) if expected_grams else 0.0
|
||||
|
||||
# ROUGE-2: перекрытие bigrams
|
||||
model_bigrams = Counter(
|
||||
tuple(model_tokens[i:i+2])
|
||||
for i in range(len(model_tokens) - 1)
|
||||
)
|
||||
expected_bigrams = Counter(
|
||||
tuple(expected_tokens[i:i+2])
|
||||
for i in range(len(expected_tokens) - 1)
|
||||
)
|
||||
|
||||
intersection = sum((model_bigrams & expected_bigrams).values())
|
||||
rouge2 = intersection / len(expected_bigrams) if expected_bigrams else 0.0
|
||||
|
||||
# ROUGE-L: Longest Common Subsequence (LCS)
|
||||
rougeL = calculate_rouge_lcs(model_tokens, expected_tokens)
|
||||
|
||||
return {
|
||||
'rouge1': round(rouge1, 3),
|
||||
'rouge2': round(rouge2, 3),
|
||||
'rougeL': round(rougeL, 3)
|
||||
}
|
||||
|
||||
def calculate_rouge_lcs(model_tokens: List[str], expected_tokens: List[str]) -> float:
|
||||
"""
|
||||
Вычисляет ROUGE-L на основе Longest Common Subsequence (LCS).
|
||||
|
||||
Args:
|
||||
model_tokens: Токены ответа модели
|
||||
expected_tokens: Токены ожидаемого ответа
|
||||
|
||||
Returns:
|
||||
ROUGE-L score (0.0-1.0)
|
||||
"""
|
||||
# Матрица для хранения длины LCS
|
||||
m = len(model_tokens)
|
||||
n = len(expected_tokens)
|
||||
dp = [[0] * (n + 1) for _ in range(m + 1)]
|
||||
|
||||
for i in range(1, m + 1):
|
||||
for j in range(1, n + 1):
|
||||
if model_tokens[i-1] == expected_tokens[j-1]:
|
||||
dp[i][j] = dp[i-1][j-1] + 1
|
||||
else:
|
||||
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
|
||||
|
||||
lcs_length = dp[m][n]
|
||||
|
||||
# Вычисление ROUGE-L
|
||||
precision = lcs_length / m if m > 0 else 0.0
|
||||
recall = lcs_length / n if n > 0 else 0.0
|
||||
|
||||
if (precision + recall) == 0:
|
||||
return 0.0
|
||||
f_score = 2 * (precision * recall) / (precision + recall)
|
||||
|
||||
return f_score
|
||||
|
||||
def calculate_code_similarity(model_response: str, expected: str) -> float:
|
||||
"""
|
||||
Вычисляет похожесть кода на основе структурных элементов.
|
||||
|
||||
Args:
|
||||
model_response: Сгенерированный код
|
||||
expected: Ожидаемый код
|
||||
|
||||
Returns:
|
||||
Коэффициент похожести (0.0-1.0)
|
||||
"""
|
||||
# Удаление комментариев
|
||||
model_clean = remove_comments(model_response)
|
||||
expected_clean = remove_comments(expected)
|
||||
|
||||
# Нормализация (удаление лишних пробелов, табуляций)
|
||||
model_normalized = normalize_code(model_clean)
|
||||
expected_normalized = normalize_code(expected_clean)
|
||||
|
||||
# Сравнение токенов кода
|
||||
model_tokens = tokenize_code(model_normalized)
|
||||
expected_tokens = tokenize_code(expected_normalized)
|
||||
|
||||
if not expected_tokens:
|
||||
return 0.0
|
||||
|
||||
# Простая метрика на основе совпадения ключевых токенов
|
||||
intersection = set(model_tokens) & set(expected_tokens)
|
||||
precision = len(intersection) / len(model_tokens) if model_tokens else 0.0
|
||||
recall = len(intersection) / len(expected_tokens) if expected_tokens else 0.0
|
||||
|
||||
if (precision + recall) == 0:
|
||||
return 0.0
|
||||
f1 = 2 * (precision * recall) / (precision + recall)
|
||||
|
||||
return round(f1, 3)
|
||||
|
||||
def remove_comments(code: str) -> str:
|
||||
"""
|
||||
Удаляет комментарии из кода.
|
||||
|
||||
Args:
|
||||
code: Исходный код
|
||||
|
||||
Returns:
|
||||
Код без комментариев
|
||||
"""
|
||||
# Удаление однострочных комментариев
|
||||
code = re.sub(r'//.*', '', code)
|
||||
code = re.sub(r'#.*', '', code)
|
||||
|
||||
# Удаление многострочных комментариев
|
||||
code = re.sub(r'/\*.*?\*/', '', code, flags=re.DOTALL)
|
||||
|
||||
return code
|
||||
|
||||
def normalize_code(code: str) -> str:
|
||||
"""
|
||||
Нормализует код (удаляет лишние пробелы, табуляции).
|
||||
|
||||
Args:
|
||||
code: Исходный код
|
||||
|
||||
Returns:
|
||||
Нормализованный код
|
||||
"""
|
||||
# Замена нескольких пробелов/табуляций на один
|
||||
code = re.sub(r'\s+', ' ', code)
|
||||
# Удаление пробелов в начале и конце строк
|
||||
code = '\n'.join(line.strip() for line in code.split('\n'))
|
||||
return code
|
||||
|
||||
def tokenize_code(code: str) -> List[str]:
|
||||
"""
|
||||
Токенизирует код, выделяя ключевые элементы.
|
||||
|
||||
Args:
|
||||
code: Исходный код
|
||||
|
||||
Returns:
|
||||
Список токенов
|
||||
"""
|
||||
# Регулярное выражение для токенизации кода
|
||||
token_pattern = r"""
|
||||
\w+| # Идентификаторы и ключевые слова
|
||||
[+\-*/%=<>!&|^~.,;(){}[\]]| # Операторы и знаки препинания
|
||||
[0-9]+| # Числа
|
||||
[A-Za-z_][A-Za-z0-9_]*| # Идентификаторы
|
||||
\S # Любые другие непробельные символы
|
||||
"""
|
||||
tokens = re.findall(token_pattern, code, re.VERBOSE)
|
||||
return [token for token in tokens if token.strip()]
|
||||
|
||||
def get_all_scores(model_response: str, expected: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Вычисляет все доступные метрики для ответа модели.
|
||||
|
||||
Args:
|
||||
model_response: Ответ от модели
|
||||
expected: Ожидаемый ответ
|
||||
|
||||
Returns:
|
||||
Словарь со всеми метриками
|
||||
"""
|
||||
return {
|
||||
'f1_score': calculate_f1_score(model_response, expected),
|
||||
'exact_match': calculate_exact_match(model_response, expected),
|
||||
'levenshtein_distance': calculate_levenshtein_distance(model_response, expected),
|
||||
'normalized_levenshtein': calculate_normalized_levenshtein(model_response, expected),
|
||||
'bleu_score': calculate_bleu_score(model_response, expected),
|
||||
'rouge_scores': calculate_rouge_scores(model_response, expected),
|
||||
'code_similarity': calculate_code_similarity(model_response, expected)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
{
|
||||
"prompt": "Write a Python function that calculates the factorial of a number using recursion.",
|
||||
"expected": "def factorial(n):\\n if n == 0 or n == 1:\\n return 1\\n else:\\n return n * factorial(n-1)"
|
||||
"expected": "def factorial(n):\n if n == 0 or n == 1:\n return 1\n else:\n return n * factorial(n-1)"
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user