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

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

View File

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

View File

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

Binary file not shown.

View File

@ -42,11 +42,15 @@ class ReportGenerator:
table_data = [
{
"Тест": "Тест",
"Скор": "Скор",
"F1-Score": "F1-Score",
"Levenshtein": "Levenshtein",
"BLEU": "BLEU",
"ROUGE-1": "ROUGE-1",
"ROUGE-2": "ROUGE-2",
"ROUGE-L": "ROUGE-L",
"Code Similarity": "Code Similarity",
"Время (с)": "Время (с)",
"Промпт": "Промпт",
"Ожидаемый": "Ожидаемый",
"Ответ модели": "Ответ модели"
"Лог файл": "Лог файл"
}
]
@ -54,26 +58,86 @@ class ReportGenerator:
if 'error' in result:
table_data.append({
"Тест": result['test_case'],
"Скор": "Ошибка",
"F1-Score": "Ошибка",
"Levenshtein": "-",
"BLEU": "-",
"ROUGE-1": "-",
"ROUGE-2": "-",
"ROUGE-L": "-",
"Code Similarity": "-",
"Время (с)": "-",
"Промпт": result['prompt'][:50] + "..." if len(result['prompt']) > 50 else result['prompt'],
"Ожидаемый": result['expected'][:50] + "..." if len(result['expected']) > 50 else result['expected'],
"Ответ модели": result['error']
"Лог файл": "-"
})
else:
# Извлекаем все метрики из результата
f1_score = result.get('f1_score', result.get('score', 0))
levenshtein = result.get('normalized_levenshtein', 0)
bleu = result.get('bleu_score', 0)
rouge_scores = result.get('rouge_scores', {})
code_sim = result.get('code_similarity', 0)
table_data.append({
"Тест": result['test_case'],
"Скор": str(result['score']),
"F1-Score": f"{f1_score:.3f}" if f1_score else "-",
"Levenshtein": f"{levenshtein:.3f}" if levenshtein else "-",
"BLEU": f"{bleu:.3f}" if bleu else "-",
"ROUGE-1": f"{rouge_scores.get('rouge1', 0):.3f}" if rouge_scores else "-",
"ROUGE-2": f"{rouge_scores.get('rouge2', 0):.3f}" if rouge_scores else "-",
"ROUGE-L": f"{rouge_scores.get('rougeL', 0):.3f}" if rouge_scores else "-",
"Code Similarity": f"{code_sim:.3f}" if code_sim else "-",
"Время (с)": f"{result['latency']:.2f}",
"Промпт": result['prompt'][:50] + "..." if len(result['prompt']) > 50 else result['prompt'],
"Ожидаемый": result['expected'][:50] + "..." if len(result['expected']) > 50 else result['expected'],
"Ответ модели": result['model_response'][:50] + "..." if len(result['model_response']) > 50 else result['model_response']
"Лог файл": f"{results['benchmark_name']}_{result['test_case']}.txt"
})
f.write("## Результаты тестов\n\n")
f.write(markdown_table(table_data).get_markdown())
f.write("| Тест | F1-Score | Levenshtein | BLEU | ROUGE-1 | ROUGE-2 | ROUGE-L | Code Similarity | Время (с) | Лог файл |\n")
f.write("|--|--|--|--|--|--|--|--|--|--|\n")
for result in results['results']:
if 'error' in result:
f.write(f"| {result['test_case']} | Ошибка | - | - | - | - | - | - | - | - |\n")
else:
# Извлекаем все метрики из результата
f1_score = result.get('f1_score', result.get('score', 0))
levenshtein = result.get('normalized_levenshtein', 0)
bleu = result.get('bleu_score', 0)
rouge_scores = result.get('rouge_scores', {})
code_sim = result.get('code_similarity', 0)
f1_score_display = f"{f1_score:.3f}" if f1_score else "-"
levenshtein_display = f"{levenshtein:.3f}" if levenshtein else "-"
bleu_display = f"{bleu:.3f}" if bleu else "-"
rouge1_display = f"{rouge_scores.get('rouge1', 0):.3f}" if rouge_scores else "-"
rouge2_display = f"{rouge_scores.get('rouge2', 0):.3f}" if rouge_scores else "-"
rougeL_display = f"{rouge_scores.get('rougeL', 0):.3f}" if rouge_scores else "-"
code_sim_display = f"{code_sim:.3f}" if code_sim else "-"
f.write(f"| {result['test_case']} | "
f"{f1_score_display} | "
f"{levenshtein_display} | "
f"{bleu_display} | "
f"{rouge1_display} | "
f"{rouge2_display} | "
f"{rougeL_display} | "
f"{code_sim_display} | "
f"{result['latency']:.2f} | "
f"{results['benchmark_name']}_{result['test_case']}.txt |\n")
f.write("\n\n")
# Сохранение request-response в лог
if model_name:
logs_dir = os.path.join(output_dir, "logs")
os.makedirs(logs_dir, exist_ok=True)
for result in results['results']:
if 'error' in result:
continue
log_filename = os.path.join(logs_dir, f"{results['benchmark_name']}_{result['test_case']}.txt")
with open(log_filename, 'w', encoding='utf-8') as log_file:
log_file.write(f"Промпт:\n{result['prompt']}\n\n")
log_file.write(f"Ответ модели:\n{result['model_response']}\n\n")
log_file.write(f"Ожидаемый ответ:\n{result['expected']}\n")
# Статистика
successful = [r for r in results['results'] if 'score' in r]
if successful:
@ -87,7 +151,7 @@ class ReportGenerator:
self.logger.info(f"Report saved to {file_path}")
return file_path
def generate_summary_report(self, all_results: List[Dict[str, Any]], output_dir: str = "results", model_name: str = None) -> str:
def generate_summary_report(self, all_results: List[Dict[str, Any]], output_dir: str = "results", model_name: str = None, ollama_url: str = None) -> str:
"""
Генерация сводного отчета по всем бенчмаркам.
@ -95,6 +159,7 @@ class ReportGenerator:
all_results: Список результатов всех бенчмарков
output_dir: Директория для сохранения отчета
model_name: Имя модели (для структурирования результатов)
ollama_url: URL сервера Ollama
Returns:
Путь к сгенерированному файлу
@ -114,6 +179,8 @@ class ReportGenerator:
f.write(f"**Дата:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
if model_name:
f.write(f"**Модель:** {model_name}\n\n")
if ollama_url:
f.write(f"**Ollama URL:** {ollama_url}\n\n")
# Таблица с общими результатами
table_data = [
@ -140,7 +207,16 @@ class ReportGenerator:
})
f.write("## Общие результаты\n\n")
f.write(markdown_table(table_data).get_markdown())
f.write("| Бенчмарк | Тестов | Успешно | Средний скор | Среднее время |\n")
f.write("|--|--|--|--|--|\n")
for result in all_results:
successful = [r for r in result['results'] if 'score' in r]
avg_score = sum(r['score'] for r in successful) / len(successful) if successful else 0
avg_latency = sum(r['latency'] for r in successful) / len(successful) if successful else 0
f.write(f"| {result['benchmark_name']} | {result['total_tests']} | {result['successful_tests']} | {avg_score:.3f} | {avg_latency:.3f} |\n")
f.write("\n\n")
# Подробности по каждому бенчмарку

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