mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-08-08 12:37:44 +00:00
2e1d0624d2
* refactor: move exp_gen selection files into select directory * fix selector value in data_science conf * fix selector template name * fix CI --------- Co-authored-by: Young <afe.young@gmail.com>
173 lines
7.4 KiB
Python
173 lines
7.4 KiB
Python
import json
|
|
import random
|
|
from typing import Dict, Tuple
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
from rdagent.app.data_science.conf import DS_RD_SETTING
|
|
from rdagent.core.proposal import ExperimentFeedback, SOTAexpSelector, Trace
|
|
from rdagent.log import rdagent_logger as logger
|
|
from rdagent.oai.llm_utils import APIBackend, md5_hash
|
|
from rdagent.scenarios.data_science.experiment.experiment import DSExperiment
|
|
from rdagent.scenarios.data_science.proposal.exp_gen.base import DSHypothesis, DSTrace
|
|
from rdagent.utils.agent.tpl import T
|
|
from rdagent.utils.workflow import wait_retry
|
|
|
|
|
|
class GlobalSOTASelector(SOTAexpSelector):
|
|
"""
|
|
return the latest SOTA experiment from the trace to submit
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
):
|
|
print(f"Using global SOTA policy by default")
|
|
|
|
def get_sota_exp_to_submit(self, trace: Trace) -> DSExperiment | None:
|
|
|
|
return trace.sota_experiment(search_type="all")
|
|
|
|
|
|
class AutoSOTAexpSelector(SOTAexpSelector):
|
|
"""
|
|
retrieve a list of SOTA experiments from the trace, then call the LLM to select the best one
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
):
|
|
print(f"Using auto SOTA policy")
|
|
|
|
@wait_retry(retry_n=5)
|
|
def get_sota_exp_to_submit(self, trace: Trace) -> DSExperiment | None:
|
|
# retrieve all SOTA experiments from the trace
|
|
|
|
sota_exp_fb_list = trace.experiment_and_feedback_list_after_init(
|
|
return_type="sota", search_type="all", max_retrieve_num=DS_RD_SETTING.max_sota_retrieved_num
|
|
)
|
|
logger.info(f"Auto SOTA selector: Found {len(sota_exp_fb_list)} SOTA experiments")
|
|
if len(sota_exp_fb_list) == 0:
|
|
logger.info("Auto SOTA selector: No SOTA in trace yet")
|
|
return None
|
|
|
|
elif len(sota_exp_fb_list) == 1:
|
|
sota_idx_in_trace = trace.hist.index(sota_exp_fb_list[0])
|
|
logger.info(
|
|
f"Auto SOTA selector: Only one SOTA in trace, using it, which is the No. {sota_idx_in_trace + 1} in the trace"
|
|
)
|
|
return sota_exp_fb_list[0][0]
|
|
|
|
else:
|
|
logger.info(
|
|
f"Auto SOTA selector: Multiple SOTA in trace, calling LLM to select the best one in {DS_RD_SETTING.max_sota_retrieved_num} SOTA experiments"
|
|
)
|
|
|
|
SOAT_exp_with_desc_and_scores = "Historical SOTA experiments:\n\n"
|
|
|
|
leaves: list[int] = trace.get_leaves()
|
|
|
|
if len(leaves) >= 2:
|
|
|
|
logger.info(
|
|
f"Auto SOTA selector: {len(leaves)} traces found, collecting SOTA experiments from each trace"
|
|
)
|
|
# multiple trace case, collect the latest SOTA experiments from each trace
|
|
new_sota_exp_fb_list: list[tuple[DSExperiment, ExperimentFeedback]] = []
|
|
# calculate the number of SOTA experiments to retrieve from each trace, prevent it from becoming zero
|
|
max_sota_retrieved_num_per_trace = max(DS_RD_SETTING.max_sota_retrieved_num // len(leaves), 2)
|
|
# recall, due to the integer division, the final number of SOTA experiments to retrieve may be different
|
|
for leaf in leaves:
|
|
sota_exp_fb_list_per_trace = trace.experiment_and_feedback_list_after_init(
|
|
return_type="sota",
|
|
search_type="ancestors",
|
|
selection=(leaf,),
|
|
max_retrieve_num=max_sota_retrieved_num_per_trace,
|
|
)
|
|
logger.info(
|
|
f"Auto SOTA selector: Collected {len(sota_exp_fb_list_per_trace)} SOTA experiments from trace with leaf #. {leaf}"
|
|
)
|
|
|
|
new_sota_exp_fb_list.extend(sota_exp_fb_list_per_trace)
|
|
|
|
sota_exp_fb_list = new_sota_exp_fb_list
|
|
|
|
if len(sota_exp_fb_list) == 0:
|
|
logger.info("Auto SOTA selector: No SOTA in trace yet")
|
|
return None
|
|
|
|
elif len(sota_exp_fb_list) == 1:
|
|
logger.info("Auto SOTA selector: Only one SOTA in trace, using it")
|
|
return sota_exp_fb_list[0][0]
|
|
else:
|
|
logger.info(
|
|
f"Auto SOTA selector: {len(sota_exp_fb_list)} SOTA experiments found in all traces, calling LLM to select the best one"
|
|
)
|
|
|
|
for i, (exp, ef) in enumerate(sota_exp_fb_list):
|
|
if exp:
|
|
current_final_score = pd.DataFrame(exp.result).loc["ensemble"].iloc[0]
|
|
desc = T("scenarios.data_science.share:describe.exp").r(
|
|
exp=exp, heading="SOTA of previous exploration of the scenario"
|
|
)
|
|
SOAT_exp_with_desc_and_scores += f"""SOTA experiment No. {i+1}:
|
|
Description: {desc}
|
|
Final score: {current_final_score}\n\n"""
|
|
|
|
system_prompt = T(".prompts:auto_sota_selector.system").r(scenario=trace.scen.get_scenario_all_desc())
|
|
|
|
user_prompt = T(".prompts:auto_sota_selector.user").r(
|
|
historical_sota_exp_with_desc_and_scores=SOAT_exp_with_desc_and_scores,
|
|
)
|
|
|
|
response = APIBackend().build_messages_and_create_chat_completion(
|
|
user_prompt=user_prompt,
|
|
system_prompt=system_prompt,
|
|
json_mode=True,
|
|
json_target_type=Dict[str, str | int],
|
|
)
|
|
|
|
response_dict = json.loads(response)
|
|
|
|
sota_submit_idx = response_dict.get("selected_SOTA_idx", None)
|
|
|
|
if sota_submit_idx and int(sota_submit_idx) - 1 < len(sota_exp_fb_list):
|
|
sota_submit = sota_exp_fb_list[int(sota_submit_idx) - 1]
|
|
sota_idx_in_trace = trace.hist.index(sota_submit)
|
|
logger.info(
|
|
f"Auto SOTA selector: selected SOTA experiment No. {sota_submit_idx} to submit, which is the No. {sota_idx_in_trace + 1} in the trace"
|
|
)
|
|
return sota_submit[0]
|
|
else:
|
|
# no SOTA experiment to submit, using the latest SOTA experiment
|
|
if len(sota_exp_fb_list) > 0:
|
|
logger.info("Auto SOTA selector: No SOTA experiment to submit, using the latest SOTA experiment")
|
|
return sota_exp_fb_list[-1][0]
|
|
else:
|
|
logger.info("Auto SOTA selector: No SOTA experiment in trace yet")
|
|
return None
|
|
|
|
|
|
class BestValidSelector(SOTAexpSelector):
|
|
def get_sota_exp_to_submit(self, trace: Trace) -> DSExperiment | None:
|
|
sota_exp_fb_list = trace.experiment_and_feedback_list_after_init(return_type="all", search_type="all")
|
|
direction_sign = 1 if trace.scen.metric_direction else -1
|
|
|
|
def get_sort_key(exp_fb: tuple[DSExperiment, ExperimentFeedback]) -> tuple[bool, float]:
|
|
score = -np.inf
|
|
result: pd.DataFrame | None = exp_fb[0].result
|
|
if result is not None:
|
|
score = result.loc["ensemble"].iloc[0]
|
|
return (exp_fb[1].decision, direction_sign * score)
|
|
|
|
if len(sota_exp_fb_list) == 0:
|
|
logger.info("Best Valid SOTA selector: No SOTA in trace yet")
|
|
return None
|
|
else:
|
|
sota_exp_fb_list = sorted(sota_exp_fb_list, key=get_sort_key, reverse=True)
|
|
return sota_exp_fb_list[0][0]
|
|
|
|
|
|
# TODO: more advanced sota exp selector (e.g. LLM-based, merge exp with multiple sub-trace)
|