ragbits-evaluate 0.0.30.dev29302392__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ragbits/evaluate/__init__.py +0 -0
- ragbits/evaluate/agent_simulation/__init__.py +87 -0
- ragbits/evaluate/agent_simulation/context.py +140 -0
- ragbits/evaluate/agent_simulation/conversation.py +333 -0
- ragbits/evaluate/agent_simulation/deepeval_evaluator.py +92 -0
- ragbits/evaluate/agent_simulation/logger.py +165 -0
- ragbits/evaluate/agent_simulation/metrics/__init__.py +19 -0
- ragbits/evaluate/agent_simulation/metrics/builtin.py +221 -0
- ragbits/evaluate/agent_simulation/metrics/collectors.py +142 -0
- ragbits/evaluate/agent_simulation/models.py +37 -0
- ragbits/evaluate/agent_simulation/results.py +200 -0
- ragbits/evaluate/agent_simulation/scenarios.py +129 -0
- ragbits/evaluate/agent_simulation/simulation.py +245 -0
- ragbits/evaluate/cli.py +150 -0
- ragbits/evaluate/config.py +11 -0
- ragbits/evaluate/dataloaders/__init__.py +3 -0
- ragbits/evaluate/dataloaders/base.py +95 -0
- ragbits/evaluate/dataloaders/document_search.py +61 -0
- ragbits/evaluate/dataloaders/exceptions.py +25 -0
- ragbits/evaluate/dataloaders/gaia.py +78 -0
- ragbits/evaluate/dataloaders/hotpot_qa.py +95 -0
- ragbits/evaluate/dataloaders/human_eval.py +70 -0
- ragbits/evaluate/dataloaders/question_answer.py +56 -0
- ragbits/evaluate/dataset_generator/__init__.py +0 -0
- ragbits/evaluate/dataset_generator/pipeline.py +141 -0
- ragbits/evaluate/dataset_generator/prompts/__init__.py +0 -0
- ragbits/evaluate/dataset_generator/prompts/corpus_generation.py +21 -0
- ragbits/evaluate/dataset_generator/prompts/qa.py +83 -0
- ragbits/evaluate/dataset_generator/tasks/__init__.py +0 -0
- ragbits/evaluate/dataset_generator/tasks/corpus_generation.py +67 -0
- ragbits/evaluate/dataset_generator/tasks/filter/__init__.py +0 -0
- ragbits/evaluate/dataset_generator/tasks/filter/base.py +43 -0
- ragbits/evaluate/dataset_generator/tasks/filter/dont_know.py +34 -0
- ragbits/evaluate/dataset_generator/tasks/text_generation/__init__.py +0 -0
- ragbits/evaluate/dataset_generator/tasks/text_generation/base.py +66 -0
- ragbits/evaluate/dataset_generator/tasks/text_generation/qa.py +96 -0
- ragbits/evaluate/dataset_generator/utils.py +43 -0
- ragbits/evaluate/evaluator.py +244 -0
- ragbits/evaluate/factories/__init__.py +42 -0
- ragbits/evaluate/metrics/__init__.py +3 -0
- ragbits/evaluate/metrics/base.py +89 -0
- ragbits/evaluate/metrics/document_search.py +90 -0
- ragbits/evaluate/metrics/gaia.py +84 -0
- ragbits/evaluate/metrics/hotpot_qa.py +51 -0
- ragbits/evaluate/metrics/human_eval.py +105 -0
- ragbits/evaluate/metrics/question_answer.py +205 -0
- ragbits/evaluate/optimizer.py +210 -0
- ragbits/evaluate/pipelines/__init__.py +37 -0
- ragbits/evaluate/pipelines/base.py +64 -0
- ragbits/evaluate/pipelines/document_search.py +106 -0
- ragbits/evaluate/pipelines/gaia.py +249 -0
- ragbits/evaluate/pipelines/hotpot_qa.py +342 -0
- ragbits/evaluate/pipelines/human_eval.py +323 -0
- ragbits/evaluate/pipelines/question_answer.py +96 -0
- ragbits/evaluate/py.typed +0 -0
- ragbits/evaluate/utils.py +160 -0
- ragbits_evaluate-0.0.30.dev29302392.dist-info/METADATA +58 -0
- ragbits_evaluate-0.0.30.dev29302392.dist-info/RECORD +59 -0
- ragbits_evaluate-0.0.30.dev29302392.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from statistics import mean
|
|
3
|
+
|
|
4
|
+
from ragbits.evaluate.metrics.base import Metric
|
|
5
|
+
from ragbits.evaluate.pipelines.human_eval import HumanEvalResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HumanEvalPassAtK(Metric[HumanEvalResult]):
|
|
9
|
+
"""
|
|
10
|
+
Computes pass@k over HumanEval tasks.
|
|
11
|
+
Measures the fraction of tasks with at least one passing sample out of k attempts.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, k: int = 1) -> None:
|
|
15
|
+
super().__init__()
|
|
16
|
+
self.k = k
|
|
17
|
+
|
|
18
|
+
async def compute(self, results: list[HumanEvalResult]) -> dict:
|
|
19
|
+
"""Compute pass@k averaged over tasks.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Dictionary with humaneval_pass@k: fraction of tasks with at least one passing sample.
|
|
23
|
+
"""
|
|
24
|
+
values = []
|
|
25
|
+
for r in results:
|
|
26
|
+
n = len(r.passed_mask)
|
|
27
|
+
m = sum(1 for x in r.passed_mask if x)
|
|
28
|
+
k = min(self.k, n)
|
|
29
|
+
if n == 0 or k == 0:
|
|
30
|
+
values.append(0.0)
|
|
31
|
+
continue
|
|
32
|
+
if m == 0:
|
|
33
|
+
values.append(0.0)
|
|
34
|
+
continue
|
|
35
|
+
if m == n:
|
|
36
|
+
values.append(1.0)
|
|
37
|
+
continue
|
|
38
|
+
# 1 - C(n-m, k) / C(n, k)
|
|
39
|
+
denom = math.comb(n, k)
|
|
40
|
+
numer = math.comb(n - m, k) if n - m >= k else 0
|
|
41
|
+
values.append(1.0 - (numer / denom))
|
|
42
|
+
return {f"humaneval_pass@{self.k}": float(mean(values)) if values else 0.0}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class HumanEvalQualityPerf(Metric[HumanEvalResult]):
|
|
46
|
+
"""
|
|
47
|
+
Code quality and execution performance metrics:
|
|
48
|
+
- humaneval_compile_rate: fraction of samples that compiled
|
|
49
|
+
- humaneval_syntax_error_rate: fraction of samples with syntax error (compile failed)
|
|
50
|
+
- humaneval_assert_fail_rate: fraction of samples that ran but failed assertions
|
|
51
|
+
- humaneval_runtime_error_rate: fraction of samples with other runtime errors
|
|
52
|
+
- humaneval_timeout_rate: fraction of samples that timed out
|
|
53
|
+
- humaneval_tasks_solved: fraction of tasks with any passing sample
|
|
54
|
+
- humaneval_avg_exec_time_sec: average exec time over compilable runs
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
async def compute(results: list[HumanEvalResult]) -> dict:
|
|
59
|
+
"""Compute code quality and execution performance metrics.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Dictionary with compile rates, error rates, tasks solved rate, and average execution time.
|
|
63
|
+
"""
|
|
64
|
+
total_samples = sum(len(r.passed_mask) for r in results)
|
|
65
|
+
compiled = 0
|
|
66
|
+
syntax_errors = 0
|
|
67
|
+
assert_fails = 0
|
|
68
|
+
runtime_errors = 0
|
|
69
|
+
timeouts = 0
|
|
70
|
+
any_pass = sum(1 for r in results if any(r.passed_mask))
|
|
71
|
+
durations: list[float] = []
|
|
72
|
+
|
|
73
|
+
for r in results:
|
|
74
|
+
for ok, err, dur in zip(r.compile_ok_mask, r.errors, r.exec_durations_sec, strict=False):
|
|
75
|
+
if ok:
|
|
76
|
+
compiled += 1
|
|
77
|
+
durations.append(dur)
|
|
78
|
+
if err:
|
|
79
|
+
if err.startswith("AssertionError"):
|
|
80
|
+
assert_fails += 1
|
|
81
|
+
elif err.startswith("TimeoutError"):
|
|
82
|
+
timeouts += 1
|
|
83
|
+
else:
|
|
84
|
+
runtime_errors += 1
|
|
85
|
+
else:
|
|
86
|
+
# Compile failed: count as syntax error
|
|
87
|
+
syntax_errors += 1
|
|
88
|
+
|
|
89
|
+
compile_rate = (compiled / total_samples) if total_samples else 0.0
|
|
90
|
+
syntax_error_rate = (syntax_errors / total_samples) if total_samples else 0.0
|
|
91
|
+
assert_fail_rate = (assert_fails / total_samples) if total_samples else 0.0
|
|
92
|
+
runtime_error_rate = (runtime_errors / total_samples) if total_samples else 0.0
|
|
93
|
+
timeout_rate = (timeouts / total_samples) if total_samples else 0.0
|
|
94
|
+
tasks_solved = (any_pass / len(results)) if results else 0.0
|
|
95
|
+
avg_exec_time = float(mean(durations)) if durations else 0.0
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
"humaneval_compile_rate": float(compile_rate),
|
|
99
|
+
"humaneval_syntax_error_rate": float(syntax_error_rate),
|
|
100
|
+
"humaneval_assert_fail_rate": float(assert_fail_rate),
|
|
101
|
+
"humaneval_runtime_error_rate": float(runtime_error_rate),
|
|
102
|
+
"humaneval_timeout_rate": float(timeout_rate),
|
|
103
|
+
"humaneval_tasks_solved": float(tasks_solved),
|
|
104
|
+
"humaneval_avg_exec_time_sec": avg_exec_time,
|
|
105
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from asyncio import AbstractEventLoop
|
|
4
|
+
from itertools import chain
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from continuous_eval.llm_factory import LLMInterface
|
|
8
|
+
from continuous_eval.metrics.base import LLMBasedMetric
|
|
9
|
+
from continuous_eval.metrics.generation.text import (
|
|
10
|
+
LLMBasedAnswerCorrectness,
|
|
11
|
+
LLMBasedAnswerRelevance,
|
|
12
|
+
LLMBasedFaithfulness,
|
|
13
|
+
LLMBasedStyleConsistency,
|
|
14
|
+
)
|
|
15
|
+
from typing_extensions import Self
|
|
16
|
+
|
|
17
|
+
from ragbits.agents.types import QuestionAnswerPromptOutputT
|
|
18
|
+
from ragbits.core.llms.base import LLM
|
|
19
|
+
from ragbits.core.utils.helpers import batched
|
|
20
|
+
from ragbits.evaluate.metrics.base import Metric
|
|
21
|
+
from ragbits.evaluate.pipelines.question_answer import QuestionAnswerResult
|
|
22
|
+
|
|
23
|
+
MetricT = TypeVar("MetricT", bound=LLMBasedMetric)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _MetricLMM(LLMInterface):
|
|
27
|
+
"""
|
|
28
|
+
Implementation of required interface of Relari generative metrics based on LiteLMM.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, llm: LLM, loop: AbstractEventLoop) -> None:
|
|
32
|
+
self._llm = llm
|
|
33
|
+
self._loop = loop
|
|
34
|
+
|
|
35
|
+
def run(self, prompt: dict[str, str], temperature: float = 0, max_tokens: int = 1024) -> str:
|
|
36
|
+
formatted_prompt = [
|
|
37
|
+
{"role": "system", "content": prompt["system_prompt"]},
|
|
38
|
+
{"role": "user", "content": prompt["user_prompt"]},
|
|
39
|
+
]
|
|
40
|
+
options = self._llm.options_cls(
|
|
41
|
+
temperature=temperature,
|
|
42
|
+
max_tokens=max_tokens,
|
|
43
|
+
)
|
|
44
|
+
return asyncio.run_coroutine_threadsafe(
|
|
45
|
+
self._llm.generate(formatted_prompt, options=options),
|
|
46
|
+
self._loop,
|
|
47
|
+
).result()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class QuestionAnswerMetric(Generic[MetricT], Metric[QuestionAnswerResult], ABC):
|
|
51
|
+
"""
|
|
52
|
+
Metric for question answer evaluation based on Relari backend.
|
|
53
|
+
More details can be found [here](https://docs.relari.ai/category/text-generation).
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
metric_cls: type[MetricT]
|
|
57
|
+
|
|
58
|
+
def __init__(self, llm: LLM, batch_size: int = 15, weight: float = 1.0) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Initialize the agent metric.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
llm: Judge LLM instance.
|
|
64
|
+
batch_size: Batch size for metric computation.
|
|
65
|
+
weight: Metric value weight in the final score, used during optimization.
|
|
66
|
+
"""
|
|
67
|
+
super().__init__(weight=weight)
|
|
68
|
+
self.llm = llm
|
|
69
|
+
self.batch_size = batch_size
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_config(cls, config: dict) -> Self:
|
|
73
|
+
"""
|
|
74
|
+
Create an instance of `QuestionAnswerMetric` from a configuration dictionary.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
config: A dictionary containing configuration settings for the metric.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
An instance of the metric class initialized with the provided configuration.
|
|
81
|
+
"""
|
|
82
|
+
config["llm"] = LLM.from_config(config["llm"])
|
|
83
|
+
config["batch_size"] = config.get("batch_size", 15)
|
|
84
|
+
config["weight"] = config.get("weight", 1.0)
|
|
85
|
+
return super().from_config(config)
|
|
86
|
+
|
|
87
|
+
async def compute(self, results: list[QuestionAnswerResult[QuestionAnswerPromptOutputT]]) -> dict:
|
|
88
|
+
"""
|
|
89
|
+
Compute the metric.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
results: The evaluation results.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
The computed metric.
|
|
96
|
+
"""
|
|
97
|
+
metric = self.metric_cls(_MetricLMM(self.llm, loop=asyncio.get_running_loop()))
|
|
98
|
+
metric_results = chain.from_iterable(
|
|
99
|
+
[
|
|
100
|
+
await asyncio.gather(*[asyncio.to_thread(self._call_metric, metric, result) for result in batch])
|
|
101
|
+
for batch in batched(results, self.batch_size)
|
|
102
|
+
]
|
|
103
|
+
)
|
|
104
|
+
return metric.aggregate(list(metric_results))
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
@abstractmethod
|
|
108
|
+
def _call_metric(metric: MetricT, result: QuestionAnswerResult[QuestionAnswerPromptOutputT]) -> dict:
|
|
109
|
+
"""
|
|
110
|
+
Call the metric with the proper arguments.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class QuestionAnswerAnswerCorrectness(QuestionAnswerMetric[LLMBasedAnswerCorrectness]):
|
|
115
|
+
"""
|
|
116
|
+
Metric checking answer correctness based on LLM.
|
|
117
|
+
More details can be found [here](https://docs.relari.ai/metrics/Generation/LLM-Based/llm_correctness).
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
metric_cls: type[LLMBasedAnswerCorrectness] = LLMBasedAnswerCorrectness
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _call_metric(
|
|
124
|
+
metric: LLMBasedAnswerCorrectness,
|
|
125
|
+
result: QuestionAnswerResult[QuestionAnswerPromptOutputT],
|
|
126
|
+
) -> dict:
|
|
127
|
+
return metric(
|
|
128
|
+
question=result.question,
|
|
129
|
+
answer=(
|
|
130
|
+
result.predicted_result.content
|
|
131
|
+
if isinstance(result.predicted_result.content, str)
|
|
132
|
+
else result.predicted_result.content.answer
|
|
133
|
+
),
|
|
134
|
+
ground_truth_answers=result.reference_answer,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class QuestionAnswerAnswerFaithfulness(QuestionAnswerMetric[LLMBasedFaithfulness]):
|
|
139
|
+
"""
|
|
140
|
+
Metric checking answer faithfulness based on LLM.
|
|
141
|
+
More details can be found [here](https://docs.relari.ai/metrics/Generation/LLM-Based/llm_faithfulness).
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
metric_cls: type[LLMBasedFaithfulness] = LLMBasedFaithfulness
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _call_metric(
|
|
148
|
+
metric: LLMBasedFaithfulness,
|
|
149
|
+
result: QuestionAnswerResult[QuestionAnswerPromptOutputT],
|
|
150
|
+
) -> dict:
|
|
151
|
+
return metric(
|
|
152
|
+
question=result.question,
|
|
153
|
+
answer=(
|
|
154
|
+
result.predicted_result.content
|
|
155
|
+
if isinstance(result.predicted_result.content, str)
|
|
156
|
+
else result.predicted_result.content.answer
|
|
157
|
+
),
|
|
158
|
+
retrieved_context=result.reference_context,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class QuestionAnswerAnswerRelevance(QuestionAnswerMetric[LLMBasedAnswerRelevance]):
|
|
163
|
+
"""
|
|
164
|
+
Metric checking answer relevance based on LLM.
|
|
165
|
+
More details can be found [here](https://docs.relari.ai/metrics/Generation/LLM-Based/llm_relevance).
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
metric_cls: type[LLMBasedAnswerRelevance] = LLMBasedAnswerRelevance
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def _call_metric(
|
|
172
|
+
metric: LLMBasedAnswerRelevance,
|
|
173
|
+
result: QuestionAnswerResult[QuestionAnswerPromptOutputT],
|
|
174
|
+
) -> dict:
|
|
175
|
+
return metric(
|
|
176
|
+
question=result.question,
|
|
177
|
+
answer=(
|
|
178
|
+
result.predicted_result.content
|
|
179
|
+
if isinstance(result.predicted_result.content, str)
|
|
180
|
+
else result.predicted_result.content.answer
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class QuestionAnswerAnswerConsistency(QuestionAnswerMetric[LLMBasedStyleConsistency]):
|
|
186
|
+
"""
|
|
187
|
+
Metric checking answer relevance based on LLM.
|
|
188
|
+
More details can be found [here](https://docs.relari.ai/metrics/Generation/LLM-Based/llm_style).
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
metric_cls: type[LLMBasedStyleConsistency] = LLMBasedStyleConsistency
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def _call_metric(
|
|
195
|
+
metric: LLMBasedStyleConsistency,
|
|
196
|
+
result: QuestionAnswerResult[QuestionAnswerPromptOutputT],
|
|
197
|
+
) -> dict:
|
|
198
|
+
return metric(
|
|
199
|
+
answer=(
|
|
200
|
+
result.predicted_result.content
|
|
201
|
+
if isinstance(result.predicted_result.content, str)
|
|
202
|
+
else result.predicted_result.content.answer
|
|
203
|
+
),
|
|
204
|
+
ground_truth_answers=result.reference_answer,
|
|
205
|
+
)
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import warnings
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
|
|
6
|
+
import optuna
|
|
7
|
+
from optuna import Trial
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from ragbits.core.utils.config_handling import WithConstructionConfig, import_by_path
|
|
11
|
+
from ragbits.evaluate.dataloaders.base import DataLoader
|
|
12
|
+
from ragbits.evaluate.evaluator import Evaluator, EvaluatorConfig
|
|
13
|
+
from ragbits.evaluate.metrics.base import MetricSet
|
|
14
|
+
from ragbits.evaluate.pipelines.base import EvaluationPipeline
|
|
15
|
+
from ragbits.evaluate.utils import setup_optuna_neptune_callback
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OptimizerConfig(BaseModel):
|
|
19
|
+
"""
|
|
20
|
+
Schema for the optimizer config.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
evaluator: EvaluatorConfig
|
|
24
|
+
optimizer: dict | None = None
|
|
25
|
+
neptune_callback: bool = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Optimizer(WithConstructionConfig):
|
|
29
|
+
"""
|
|
30
|
+
Optimizer class.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, direction: str = "maximize", n_trials: int = 10, max_retries_for_trial: int = 1) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Initialize the pipeline optimizer.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
direction: Direction of optimization.
|
|
39
|
+
n_trials: The number of trials for each process.
|
|
40
|
+
max_retries_for_trial: The number of retires for single process.
|
|
41
|
+
"""
|
|
42
|
+
self.direction = direction
|
|
43
|
+
self.n_trials = n_trials
|
|
44
|
+
self.max_retries_for_trial = max_retries_for_trial
|
|
45
|
+
# workaround for optuna not allowing different choices for different trials
|
|
46
|
+
# TODO check how optuna handles parallelism. discuss if we want to have parallel studies
|
|
47
|
+
self._choices_cache: dict[str, list] = {}
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def run_from_config(cls, config: dict) -> list[tuple[dict, float, dict[str, float]]]:
|
|
51
|
+
"""
|
|
52
|
+
Run the optimization process configured with a config object.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
config: Optimizer config.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List of tested configs with associated scores and metrics.
|
|
59
|
+
"""
|
|
60
|
+
optimizer_config = OptimizerConfig.model_validate(config)
|
|
61
|
+
evaluator_config = EvaluatorConfig.model_validate(optimizer_config.evaluator)
|
|
62
|
+
|
|
63
|
+
dataloader: DataLoader = DataLoader.subclass_from_config(evaluator_config.evaluation.dataloader)
|
|
64
|
+
metricset: MetricSet = MetricSet.from_config(evaluator_config.evaluation.metrics)
|
|
65
|
+
|
|
66
|
+
pipeline_class = import_by_path(evaluator_config.evaluation.pipeline.type)
|
|
67
|
+
pipeline_config = dict(evaluator_config.evaluation.pipeline.config)
|
|
68
|
+
callbacks = [setup_optuna_neptune_callback()] if optimizer_config.neptune_callback else []
|
|
69
|
+
|
|
70
|
+
optimizer = cls.from_config(optimizer_config.optimizer or {})
|
|
71
|
+
return optimizer.optimize(
|
|
72
|
+
pipeline_class=pipeline_class,
|
|
73
|
+
pipeline_config=pipeline_config,
|
|
74
|
+
metricset=metricset,
|
|
75
|
+
dataloader=dataloader,
|
|
76
|
+
callbacks=callbacks,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def optimize(
|
|
80
|
+
self,
|
|
81
|
+
pipeline_class: type[EvaluationPipeline],
|
|
82
|
+
pipeline_config: dict,
|
|
83
|
+
dataloader: DataLoader,
|
|
84
|
+
metricset: MetricSet,
|
|
85
|
+
callbacks: list[Callable] | None = None,
|
|
86
|
+
) -> list[tuple[dict, float, dict[str, float]]]:
|
|
87
|
+
"""
|
|
88
|
+
Run the optimization process for given parameters.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
pipeline_class: Pipeline to be optimized.
|
|
92
|
+
pipeline_config: Configuration defining the optimization process.
|
|
93
|
+
dataloader: Data loader.
|
|
94
|
+
metricset: Metrics to be optimized.
|
|
95
|
+
callbacks: Experiment callbacks.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
List of tested configs with associated scores and metrics.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def objective(trial: Trial) -> float:
|
|
102
|
+
return self._objective(
|
|
103
|
+
trial=trial,
|
|
104
|
+
pipeline_class=pipeline_class,
|
|
105
|
+
pipeline_config=pipeline_config,
|
|
106
|
+
dataloader=dataloader,
|
|
107
|
+
metricset=metricset,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
study = optuna.create_study(direction=self.direction)
|
|
111
|
+
study.optimize(
|
|
112
|
+
func=objective,
|
|
113
|
+
n_trials=self.n_trials,
|
|
114
|
+
callbacks=callbacks,
|
|
115
|
+
)
|
|
116
|
+
return sorted(
|
|
117
|
+
[
|
|
118
|
+
(
|
|
119
|
+
trial.user_attrs["config"],
|
|
120
|
+
trial.user_attrs["score"],
|
|
121
|
+
trial.user_attrs["metrics"],
|
|
122
|
+
)
|
|
123
|
+
for trial in study.get_trials()
|
|
124
|
+
],
|
|
125
|
+
key=lambda x: -x[1] if self.direction == "maximize" else x[1],
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def _objective(
|
|
129
|
+
self,
|
|
130
|
+
trial: Trial,
|
|
131
|
+
pipeline_class: type[EvaluationPipeline],
|
|
132
|
+
pipeline_config: dict,
|
|
133
|
+
dataloader: DataLoader,
|
|
134
|
+
metricset: MetricSet,
|
|
135
|
+
) -> float:
|
|
136
|
+
"""
|
|
137
|
+
Run a single experiment.
|
|
138
|
+
"""
|
|
139
|
+
evaluator = Evaluator()
|
|
140
|
+
event_loop = asyncio.get_event_loop()
|
|
141
|
+
|
|
142
|
+
score = 1e16 if self.direction == "maximize" else -1e16
|
|
143
|
+
metrics_values = None
|
|
144
|
+
config_for_trial = None
|
|
145
|
+
|
|
146
|
+
for attempt in range(1, self.max_retries_for_trial + 1):
|
|
147
|
+
try:
|
|
148
|
+
config_for_trial = deepcopy(pipeline_config)
|
|
149
|
+
self._set_values_for_optimized_params(cfg=config_for_trial, trial=trial, ancestors=[])
|
|
150
|
+
pipeline = pipeline_class.from_config(config_for_trial)
|
|
151
|
+
|
|
152
|
+
results = event_loop.run_until_complete(
|
|
153
|
+
evaluator.compute(
|
|
154
|
+
pipeline=pipeline,
|
|
155
|
+
dataloader=dataloader,
|
|
156
|
+
metricset=metricset,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
score = sum(results.metrics.values())
|
|
160
|
+
metrics_values = results.metrics
|
|
161
|
+
break
|
|
162
|
+
except Exception as exc:
|
|
163
|
+
message = (
|
|
164
|
+
f"Execution of the trial failed: {exc}. A retry will be initiated"
|
|
165
|
+
if attempt < self.max_retries_for_trial
|
|
166
|
+
else f"Execution of the trial failed: {exc}. Setting the score to {score}"
|
|
167
|
+
)
|
|
168
|
+
warnings.warn(message=message, category=UserWarning)
|
|
169
|
+
|
|
170
|
+
trial.set_user_attr("score", score)
|
|
171
|
+
trial.set_user_attr("metrics", metrics_values)
|
|
172
|
+
trial.set_user_attr("config", config_for_trial)
|
|
173
|
+
|
|
174
|
+
return score
|
|
175
|
+
|
|
176
|
+
def _set_values_for_optimized_params(self, cfg: dict, trial: Trial, ancestors: list[str]) -> None: # noqa: PLR0912
|
|
177
|
+
"""
|
|
178
|
+
Recursive method for sampling parameter values for optuna trial.
|
|
179
|
+
"""
|
|
180
|
+
for key, value in cfg.items():
|
|
181
|
+
if isinstance(value, dict):
|
|
182
|
+
if value.get("optimize"):
|
|
183
|
+
param_id = f"{'.'.join(ancestors)}.{key}" # type: ignore
|
|
184
|
+
choices = value.get("choices")
|
|
185
|
+
values_range = value.get("range")
|
|
186
|
+
if choices and values_range:
|
|
187
|
+
raise ValueError("Choices and range cannot be defined in couple")
|
|
188
|
+
choices_index = self._choices_cache.get(param_id)
|
|
189
|
+
if choices and not choices_index:
|
|
190
|
+
choices_index = list(range(len(choices)))
|
|
191
|
+
self._choices_cache[param_id] = choices_index
|
|
192
|
+
if values_range:
|
|
193
|
+
if isinstance(values_range[0], float) and isinstance(values_range[1], float):
|
|
194
|
+
cfg[key] = trial.suggest_float(name=param_id, low=values_range[0], high=values_range[1])
|
|
195
|
+
elif isinstance(values_range[0], int) and isinstance(values_range[1], int):
|
|
196
|
+
cfg[key] = trial.suggest_int(name=param_id, low=values_range[0], high=values_range[1])
|
|
197
|
+
else:
|
|
198
|
+
if not choices:
|
|
199
|
+
raise ValueError("Either choices or range must be specified")
|
|
200
|
+
choice_idx = trial.suggest_categorical(name=param_id, choices=choices_index) # type: ignore
|
|
201
|
+
choice = choices[choice_idx]
|
|
202
|
+
if isinstance(choice, dict):
|
|
203
|
+
self._set_values_for_optimized_params(choice, trial, ancestors + [key, str(choice_idx)]) # type: ignore
|
|
204
|
+
cfg[key] = choice
|
|
205
|
+
else:
|
|
206
|
+
self._set_values_for_optimized_params(value, trial, ancestors + [key]) # type: ignore
|
|
207
|
+
elif isinstance(value, list):
|
|
208
|
+
for param in value:
|
|
209
|
+
if isinstance(param, dict):
|
|
210
|
+
self._set_values_for_optimized_params(param, trial, ancestors + [key]) # type: ignore
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from ragbits.core.utils.config_handling import WithConstructionConfig
|
|
2
|
+
from ragbits.document_search import DocumentSearch
|
|
3
|
+
from ragbits.evaluate.pipelines.base import EvaluationData, EvaluationPipeline, EvaluationResult
|
|
4
|
+
from ragbits.evaluate.pipelines.document_search import DocumentSearchPipeline
|
|
5
|
+
from ragbits.evaluate.pipelines.gaia import GaiaPipeline
|
|
6
|
+
from ragbits.evaluate.pipelines.hotpot_qa import HotpotQAPipeline
|
|
7
|
+
from ragbits.evaluate.pipelines.human_eval import HumanEvalPipeline
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"DocumentSearchPipeline",
|
|
11
|
+
"EvaluationData",
|
|
12
|
+
"EvaluationPipeline",
|
|
13
|
+
"EvaluationResult",
|
|
14
|
+
"GaiaPipeline",
|
|
15
|
+
"HotpotQAPipeline",
|
|
16
|
+
"HumanEvalPipeline",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
_target_to_evaluation_pipeline: dict[type[WithConstructionConfig], type[EvaluationPipeline]] = {
|
|
20
|
+
DocumentSearch: DocumentSearchPipeline,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_evaluation_pipeline_for_target(evaluation_target: WithConstructionConfig) -> EvaluationPipeline:
|
|
25
|
+
"""
|
|
26
|
+
A function instantiating evaluation pipeline for given WithConstructionConfig object
|
|
27
|
+
Args:
|
|
28
|
+
evaluation_target: WithConstructionConfig object to be evaluated
|
|
29
|
+
Returns:
|
|
30
|
+
instance of evaluation pipeline
|
|
31
|
+
Raises:
|
|
32
|
+
ValueError for classes with no registered evaluation pipeline
|
|
33
|
+
"""
|
|
34
|
+
for supported_type, evaluation_pipeline_type in _target_to_evaluation_pipeline.items():
|
|
35
|
+
if isinstance(evaluation_target, supported_type):
|
|
36
|
+
return evaluation_pipeline_type(evaluation_target=evaluation_target)
|
|
37
|
+
raise ValueError(f"Evaluation pipeline not implemented for {evaluation_target.__class__}")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from types import ModuleType
|
|
5
|
+
from typing import ClassVar, Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from ragbits.core.utils.config_handling import WithConstructionConfig
|
|
10
|
+
from ragbits.evaluate import pipelines
|
|
11
|
+
|
|
12
|
+
EvaluationDataT = TypeVar("EvaluationDataT", bound="EvaluationData")
|
|
13
|
+
EvaluationResultT = TypeVar("EvaluationResultT", bound="EvaluationResult")
|
|
14
|
+
EvaluationTargetT = TypeVar("EvaluationTargetT", bound=WithConstructionConfig)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EvaluationData(BaseModel, ABC):
|
|
18
|
+
"""
|
|
19
|
+
Represents the data for a single evaluation.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class EvaluationResult(ABC):
|
|
25
|
+
"""
|
|
26
|
+
Represents the result of a single evaluation.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class EvaluationPipeline(WithConstructionConfig, Generic[EvaluationTargetT, EvaluationDataT, EvaluationResultT], ABC):
|
|
31
|
+
"""
|
|
32
|
+
Evaluation pipeline.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
default_module: ClassVar[ModuleType | None] = pipelines
|
|
36
|
+
configuration_key: ClassVar[str] = "pipeline"
|
|
37
|
+
|
|
38
|
+
def __init__(self, evaluation_target: EvaluationTargetT) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Initialize the evaluation pipeline.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
evaluation_target: Evaluation target instance.
|
|
44
|
+
"""
|
|
45
|
+
super().__init__()
|
|
46
|
+
self.evaluation_target = evaluation_target
|
|
47
|
+
|
|
48
|
+
async def prepare(self) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Prepare pipeline for evaluation. Optional step.
|
|
51
|
+
"""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
async def __call__(self, data: Iterable[EvaluationDataT]) -> Iterable[EvaluationResultT]:
|
|
56
|
+
"""
|
|
57
|
+
Run the evaluation pipeline.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
data: The evaluation data.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The evaluation result.
|
|
64
|
+
"""
|