From 774d8fed1de87325296f8bd83a1f22d40297e759 Mon Sep 17 00:00:00 2001 From: second_constantine Date: Fri, 16 Jan 2026 22:30:48 +0300 Subject: [PATCH] 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. --- README.md | 60 ++- results/rnj-1:8b/codegen_20260116_195424.md | 26 -- .../rnj-1:8b/summarization_20260116_195424.md | 23 -- results/rnj-1:8b/summary_20260116_195424.md | 44 --- .../rnj-1:8b/translation_20260116_195424.md | 25 -- run.sh | 13 + .../__pycache__/base.cpython-313.pyc | Bin 3854 -> 4186 bytes src/benchmarks/base.py | 14 +- src/main.py | 12 +- src/utils/__pycache__/report.cpython-313.pyc | Bin 10171 -> 13951 bytes src/utils/__pycache__/scoring.cpython-313.pyc | Bin 0 -> 15288 bytes src/utils/report.py | 106 ++++- src/utils/scoring.py | 368 ++++++++++++++++++ tests/codegen/test1.json | 2 +- 14 files changed, 548 insertions(+), 145 deletions(-) delete mode 100644 results/rnj-1:8b/codegen_20260116_195424.md delete mode 100644 results/rnj-1:8b/summarization_20260116_195424.md delete mode 100644 results/rnj-1:8b/summary_20260116_195424.md delete mode 100644 results/rnj-1:8b/translation_20260116_195424.md create mode 100644 src/utils/__pycache__/scoring.cpython-313.pyc create mode 100644 src/utils/scoring.py 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 9ec44899b0290e1f203126d5cf314191d6e14ecb..71e117adcbaa9fc3c96dd3be7a16d6fdca825012 100644 GIT binary patch delta 1609 zcmaJ>U1%It6h3!mXLjd5$(sM9nQnv0#%7Z=O&e_!8x5&!*dQ6F2&u$1nXD5wv*pgj zpMtF7OCLm9hEVjuR(vqfJ{0;=e9#KD1bo@02w{S>Z{mw2)atAE&h9i3f9B!)&N=to zbIv{I&i(XIpsSA+MFjHQ`efC9L%pK5l~;+j0R|Xo7I2LbMxoANCS(?~gqhh`mcv|@ z$9y)9<2veHVgj#N&j1>^l{cGIm9*y|Y-H$T_NtK9H0a@g*I+!re`M+_U z<=7B7+(Mj&rJory*fFLiC*hR`0P94%P&h<)Bx?49I4>R4YO)bCd6m?^phRnCYz)<8 zV&_RDHCpj6@$FJWq|G<@H~H>~gc&#D95FhAiRP2h>OPx{DHsVu7)FNpJnM)He~-{r zHWPznP>w`WHSceWq}RVTBpNASY zt$d0^SSEwZk@1+?t4xku1)9WV94$(1azPMAt{6~pzTQR-HX%wgh%UjGBi;M1330toJg)m$0BTGTN zbiuPrj*Dq6Sf-}L%1F&EXmLt-Enl=RTFZ+?>mAE+-}Wrq3BXY#b&KT`!NlMbBQVS4fHje2l^f_2X+j3y>8c?9#tX*ZnD}FG-7a?<0T@;YkY! zBafQf{r4mt4fvl)R~z?u=;+^6?+#SQa}V0*F7y7deB{)&{_ORORsGm@=6Gc+SIx|A zr}{U$s;Lv(y#t$*)!rAj`vxnS7pr||_F84V=?{<_lY1l3J+^z^UzR`bz45X#JOKVd>6h3pG$$iB(rBrR|HMP~WrYbeQttdt%F-0NnF=|kABcZYFY7npdi_kg6 z%q&s_7E-k=EQ_pTcW7L~M;i@nrj?UXc0pqsmaJnjv>Gc|oY~Bx_J9#>$cNH%02_Qc zplDlD3mCA}6c)v_Bph`^Wdno?&fCSNUAm09I}zb5PQGL#y?aC{;S?lEs040A%{1CF zpsj(`P-%+@O&Bc!bOg@Q$0ep?ZBx`~oR7;K4+gae4L9)LAyk$}9L$N3$T!IVsWx$}ur|tTr)t&nNG;1drC2XTf1_?}mhjL+ zQ|xrsE|ZDyD&FtQP;ZC!72=}fleDE4EbhDa);!)f zus^B1&RO}a^F}qKOj`Lq{Y`2w$@l1Qd-Qyt81V{ZyLjvkl0D+1H)zOzExaOH6BApu z>DGhJI-q(n*ih4(XVe)w6MHrEQ$~3^)^d3tGY*%n@*E2BGI4rq0S)XH!X6ZLd?&~p ecynNXhV%zWe}?W)ko~5ez*ppk`W>Whfc*xoo5vgg 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 8a7363b27432f02e1e9f69969b8bda403574d7eb..e1b31d92964159a25e29b8c8c394125c8c050781 100644 GIT binary patch literal 13951 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 Jf&e*~^lxs%__6>1 diff --git a/src/utils/__pycache__/scoring.cpython-313.pyc b/src/utils/__pycache__/scoring.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b861ee83216dee169947893b5b35974e38a923dc GIT binary patch literal 15288 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 YG6@<;rW&TiD;-mhy&wFXUfFv59|OW)x&QzG literal 0 HcmV?d00001 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)" }