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:
second_constantine 2026-01-16 22:30:48 +03:00
parent 33ba55f4c1
commit 774d8fed1d
14 changed files with 548 additions and 145 deletions

View File

@ -10,6 +10,14 @@ pip install -r requirements.txt
## Использование ## Использование
### Через скрипт run.sh
```bash
./run.sh run --model llama3 --ollama-url http://localhost:11434
```
### Через Python
```bash ```bash
python src/main.py --model llama3 --ollama-url http://localhost:11434 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 ```bash
python src/main.py --model llama3 --ollama-url http://localhost:11434 ./run.sh run --model llama3 --ollama-url http://localhost:11434
``` ```
Запуск только тестов переводов: Запуск только тестов переводов:
```bash ```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 ```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 с таблицами результатов. Каждый бенчмарк будет иметь свой отчет, а также будет создан сводный отчет со статистикой по всем тестам. После выполнения бенчмарков в директории `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

View File

@ -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 секунд

View File

@ -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 секунд

View File

@ -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 секунд

View File

@ -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
View File

@ -18,6 +18,11 @@ upd() {
git submodule update --remote --merge git submodule update --remote --merge
} }
clean() {
rm -rf results/*
echo "Отчеты успешно очищены"
}
activate() { activate() {
source z/bin/activate source z/bin/activate
} }
@ -28,10 +33,18 @@ if [ -n "$1" ]; then
init init
elif [[ "$1" == "upd" ]]; then elif [[ "$1" == "upd" ]]; then
upd upd
elif [[ "$1" == "run" ]]; then
activate
shift
python src/main.py "$@"
elif [[ "$1" == "clean" ]]; then
clean
fi fi
else else
echo " Аргументом необходимо написать название скрипта (+опционально аргументы скрипта)" echo " Аргументом необходимо написать название скрипта (+опционально аргументы скрипта)"
echo "Скрипты:" echo "Скрипты:"
echo " * init - инициализация, устанавливает env" echo " * init - инициализация, устанавливает env"
echo " * upd - обновление зависимостей" echo " * upd - обновление зависимостей"
echo " * run - запуск бенчмарков"
echo " * clean - очистка отчетов"
fi fi

View File

@ -1,5 +1,7 @@
import logging import logging
import time import time
import os
import json
from typing import Dict, Any, List from typing import Dict, Any, List
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from models.ollama_client import OllamaClient from models.ollama_client import OllamaClient
@ -52,6 +54,8 @@ class Benchmark(ABC):
Returns: Returns:
Результаты бенчмарка Результаты бенчмарка
""" """
from utils.scoring import get_all_scores
test_cases = self.load_test_data() test_cases = self.load_test_data()
results = [] results = []
@ -76,13 +80,21 @@ class Benchmark(ABC):
# Оценка качества # Оценка качества
score = self.evaluate(model_response, test_case['expected']) score = self.evaluate(model_response, test_case['expected'])
# Вычисление всех дополнительных метрик
scores = get_all_scores(model_response, test_case['expected'])
results.append({ results.append({
'test_case': test_case['name'], 'test_case': test_case['name'],
'prompt': prompt, 'prompt': prompt,
'expected': test_case['expected'], 'expected': test_case['expected'],
'model_response': model_response, 'model_response': model_response,
'score': score, '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: except Exception as e:

View File

@ -53,12 +53,12 @@ def run_benchmarks(ollama_client: OllamaClient, model_name: str, benchmarks: Lis
def main(): def main():
"""Основная функция запуска.""" """Основная функция запуска."""
parser = argparse.ArgumentParser(description='LLM Benchmarking Tool') parser = argparse.ArgumentParser(description='LLM Benchmarking Tool')
parser.add_argument('--model', required=True, help='Название модели для тестирования') parser.add_argument('-m', '--model', required=True, help='Название модели для тестирования')
parser.add_argument('--ollama-url', required=True, help='URL подключения к Ollama серверу') parser.add_argument('-u', '--ollama-url', default='http://localhost:11434', help='URL подключения к Ollama серверу')
parser.add_argument('--benchmarks', nargs='+', default=['translation', 'summarization', 'codegen'], parser.add_argument('-b', '--benchmarks', nargs='+', default=['translation', 'summarization', 'codegen'],
help='Список бенчмарков для выполнения (translation, summarization, codegen)') help='Список бенчмарков для выполнения (translation, summarization, codegen)')
parser.add_argument('--output', default='results', help='Директория для сохранения результатов') parser.add_argument('-o', '--output', default='results', help='Директория для сохранения результатов')
parser.add_argument('--verbose', action='store_true', help='Подробный режим вывода') parser.add_argument('-v', '--verbose', action='store_true', help='Подробный режим вывода')
args = parser.parse_args() args = parser.parse_args()
@ -83,7 +83,7 @@ def main():
report_generator.generate_benchmark_report(result, args.output, args.model) report_generator.generate_benchmark_report(result, args.output, args.model)
if len(results) > 1: 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!") logging.info("Benchmarking completed successfully!")

Binary file not shown.

View File

@ -42,11 +42,15 @@ class ReportGenerator:
table_data = [ 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: if 'error' in result:
table_data.append({ table_data.append({
"Тест": result['test_case'], "Тест": 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: 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({ table_data.append({
"Тест": result['test_case'], "Тест": 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}", "Время (с)": f"{result['latency']:.2f}",
"Промпт": result['prompt'][:50] + "..." if len(result['prompt']) > 50 else result['prompt'], "Лог файл": f"{results['benchmark_name']}_{result['test_case']}.txt"
"Ожидаемый": 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.write("## Результаты тестов\n\n") 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") 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] successful = [r for r in results['results'] if 'score' in r]
if successful: if successful:
@ -87,7 +151,7 @@ class ReportGenerator:
self.logger.info(f"Report saved to {file_path}") self.logger.info(f"Report saved to {file_path}")
return 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: Список результатов всех бенчмарков all_results: Список результатов всех бенчмарков
output_dir: Директория для сохранения отчета output_dir: Директория для сохранения отчета
model_name: Имя модели (для структурирования результатов) model_name: Имя модели (для структурирования результатов)
ollama_url: URL сервера Ollama
Returns: Returns:
Путь к сгенерированному файлу Путь к сгенерированному файлу
@ -114,6 +179,8 @@ class ReportGenerator:
f.write(f"**Дата:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n") f.write(f"**Дата:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
if model_name: if model_name:
f.write(f"**Модель:** {model_name}\n\n") f.write(f"**Модель:** {model_name}\n\n")
if ollama_url:
f.write(f"**Ollama URL:** {ollama_url}\n\n")
# Таблица с общими результатами # Таблица с общими результатами
table_data = [ table_data = [
@ -140,7 +207,16 @@ class ReportGenerator:
}) })
f.write("## Общие результаты\n\n") 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") f.write("\n\n")
# Подробности по каждому бенчмарку # Подробности по каждому бенчмарку

368
src/utils/scoring.py Normal file
View 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)
}

View File

@ -1,4 +1,4 @@
{ {
"prompt": "Write a Python function that calculates the factorial of a number using recursion.", "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)"
} }