diff --git a/README.md b/README.md index 092dd83..42de511 100644 --- a/README.md +++ b/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 diff --git a/results/rnj-1:8b/codegen_20260116_195424.md b/results/rnj-1:8b/codegen_20260116_195424.md deleted file mode 100644 index 24423ed..0000000 --- a/results/rnj-1:8b/codegen_20260116_195424.md +++ /dev/null @@ -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 секунд diff --git a/results/rnj-1:8b/summarization_20260116_195424.md b/results/rnj-1:8b/summarization_20260116_195424.md deleted file mode 100644 index 8b0cc2c..0000000 --- a/results/rnj-1:8b/summarization_20260116_195424.md +++ /dev/null @@ -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 секунд diff --git a/results/rnj-1:8b/summary_20260116_195424.md b/results/rnj-1:8b/summary_20260116_195424.md deleted file mode 100644 index c6110a0..0000000 --- a/results/rnj-1:8b/summary_20260116_195424.md +++ /dev/null @@ -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 секунд - diff --git a/results/rnj-1:8b/translation_20260116_195424.md b/results/rnj-1:8b/translation_20260116_195424.md deleted file mode 100644 index 860f448..0000000 --- a/results/rnj-1:8b/translation_20260116_195424.md +++ /dev/null @@ -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 секунд diff --git a/run.sh b/run.sh index e205708..3d654f1 100755 --- a/run.sh +++ b/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 diff --git a/src/benchmarks/__pycache__/base.cpython-313.pyc b/src/benchmarks/__pycache__/base.cpython-313.pyc index 9ec4489..71e117a 100644 Binary files a/src/benchmarks/__pycache__/base.cpython-313.pyc and b/src/benchmarks/__pycache__/base.cpython-313.pyc differ diff --git a/src/benchmarks/base.py b/src/benchmarks/base.py index cd9850c..59bd5b6 100644 --- a/src/benchmarks/base.py +++ b/src/benchmarks/base.py @@ -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: diff --git a/src/main.py b/src/main.py index cb7653c..fd84e49 100644 --- a/src/main.py +++ b/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!") diff --git a/src/utils/__pycache__/report.cpython-313.pyc b/src/utils/__pycache__/report.cpython-313.pyc index 8a7363b..e1b31d9 100644 Binary files a/src/utils/__pycache__/report.cpython-313.pyc and b/src/utils/__pycache__/report.cpython-313.pyc differ diff --git a/src/utils/__pycache__/scoring.cpython-313.pyc b/src/utils/__pycache__/scoring.cpython-313.pyc new file mode 100644 index 0000000..b861ee8 Binary files /dev/null and b/src/utils/__pycache__/scoring.cpython-313.pyc differ diff --git a/src/utils/report.py b/src/utils/report.py index ca0000e..4211be7 100644 --- a/src/utils/report.py +++ b/src/utils/report.py @@ -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") # Подробности по каждому бенчмарку diff --git a/src/utils/scoring.py b/src/utils/scoring.py new file mode 100644 index 0000000..e5bc3a9 --- /dev/null +++ b/src/utils/scoring.py @@ -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) + } diff --git a/tests/codegen/test1.json b/tests/codegen/test1.json index ad22db6..837fb2b 100644 --- a/tests/codegen/test1.json +++ b/tests/codegen/test1.json @@ -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)" }