3_optimization-design-ptn/03_prompt-optimization/promptwizard/glue/promptopt/instantiate.py (168 lines of code) (raw):
from os.path import dirname, join
import pickle
import time
from typing import Any
from ..common.base_classes import LLMConfig, SetupConfig
from ..common.constants.log_strings import CommonLogsStr
from ..common.llm.llm_mgr import LLMMgr
from ..common.utils.logging import get_glue_logger, set_logging_config
from ..common.utils.file import read_jsonl, yaml_to_class, yaml_to_dict, read_jsonl_row
from ..paramlogger import ParamLogger
from ..promptopt.constants import PromptOptimizationLiterals
from ..promptopt.techniques.common_logic import DatasetSpecificProcessing
from ..promptopt.utils import get_promptopt_class
class GluePromptOpt:
"""
This class is trigger point for any prompt optimization method. Different prompt optimization techniques are
represented by different classes. This class collates all the user configs present in different yaml files and
other boilerplate code. Any of supported prompt optimization techniques can be triggered by this class.
"""
BEST_PROMPT = None
EXPERT_PROFILE = None
data_processor = None
iolog = ParamLogger()
class EvalLiterals:
IS_CORRECT = "is_correct"
PREDICTED_ANS = "predicted_ans"
LLM_OUTPUT = "llm_output"
def __init__(
self,
prompt_config_path: str,
setup_config_path: str,
dataset_jsonl: str,
data_processor: DatasetSpecificProcessing,
dataset_processor_pkl_path: str = None,
prompt_pool_path: str = None,
):
"""
Collates all the configs present in different yaml files. Initialize logger, de-serialize pickle file that has
class/method for dataset processing (for given dataset).
:param llm_config_path: Path to yaml file that has LLM related configs.
:param prompt_config_path: Path to yaml file that has prompt templates for the given techniques.
:param setup_config_path: Path to yaml file that has user preferences.
:param dataset_jsonl: Path to jsonl file that has dataset present in jsonl format.
:param data_processor: object of DatasetSpecificProcessing class, which has data handling methods which are
specific to that dataset
:param dataset_processor_pkl_path: Path to pickle file that has object of class DatasetSpecificProcessing
serialized.
:param prompt_pool_path: Path to yaml file that has prompts
"""
if dataset_jsonl != None:
if data_processor:
self.data_processor = data_processor
else:
with open(dataset_processor_pkl_path, "rb") as file:
self.data_processor = pickle.load(
file
) # datatype: class DatasetSpecificProcessing
prompt_config_dict = yaml_to_dict(prompt_config_path)
prompt_opt_cls, prompt_opt_hyperparam_cls, promptpool_cls = get_promptopt_class(
prompt_config_dict[PromptOptimizationLiterals.PROMPT_TECHNIQUE_NAME]
)
print(f"==== Prompt optimization class ===={prompt_opt_cls}")
self.setup_config = yaml_to_class(setup_config_path, SetupConfig)
self.prompt_opt_param = yaml_to_class(
prompt_config_path, prompt_opt_hyperparam_cls
)
current_dir = dirname(__file__)
default_yaml_path = join(
current_dir,
"techniques",
prompt_config_dict[PromptOptimizationLiterals.PROMPT_TECHNIQUE_NAME],
"prompt_pool.yaml",
)
self.prompt_pool = yaml_to_class(
prompt_pool_path, promptpool_cls, default_yaml_path
)
if dataset_jsonl != None:
dataset = read_jsonl(dataset_jsonl)
self.prompt_opt_param.answer_format += (
self.prompt_pool.ans_delimiter_instruction
)
base_path = join(
self.setup_config.dir_info.base_dir, self.setup_config.experiment_name
)
set_logging_config(
join(base_path, self.setup_config.dir_info.log_dir_name),
self.setup_config.mode,
)
self.logger = get_glue_logger(__name__)
if dataset_jsonl != None:
if len(dataset) < self.prompt_opt_param.seen_set_size:
self.prompt_opt_param.seen_set_size = len(dataset)
self.logger.info(
f"Dataset has {len(dataset)} samples. However values for seen_set_size is "
f"{self.prompt_opt_param.seen_set_size}. Hence resetting seen_set_size"
f" to {len(dataset)}"
)
if self.prompt_opt_param.few_shot_count > self.prompt_opt_param.seen_set_size:
self.prompt_opt_param.few_shot_count = self.prompt_opt_param.seen_set_size
self.logger.info(
f"Value set for few_shot_count is {self.prompt_opt_param.few_shot_count}. "
f"However values for seen_set_size is {self.prompt_opt_param.seen_set_size}. "
f"Hence resetting few_shot_count to {self.prompt_opt_param.few_shot_count}"
)
if dataset_jsonl != None:
training_dataset = dataset[: self.prompt_opt_param.seen_set_size]
else:
training_dataset = None
self.logger.info(
f"Setup configurations parameters: {self.setup_config} \n{CommonLogsStr.LOG_SEPERATOR}"
)
self.logger.info(
f"Prompt Optimization parameters: {self.prompt_opt_param} \n{CommonLogsStr.LOG_SEPERATOR}"
)
# This iolog is going to be used when doing complete evaluation over test-dataset
self.iolog.reset_eval_glue(join(base_path, "evaluation"))
self.prompt_opt = prompt_opt_cls(
training_dataset,
base_path,
self.setup_config,
self.prompt_pool,
self.data_processor,
self.logger,
)
def get_best_prompt(
self,
use_examples=False,
run_without_train_examples=False,
generate_synthetic_examples=False,
) -> (str, Any):
"""
Call get_best_prompt() method of class PromptOptimizer & return its value.
:return: (best_prompt, expert_profile)
best_prompt-> Best prompt for a given task description
expert_profile-> Description of an expert who is apt to solve the task at hand. LLM would be asked to take
identity of described in expert_profile.
"""
start_time = time.time()
self.BEST_PROMPT, self.EXPERT_PROFILE = self.prompt_opt.get_best_prompt(
self.prompt_opt_param,
use_examples=use_examples,
run_without_train_examples=run_without_train_examples,
generate_synthetic_examples=generate_synthetic_examples,
)
self.logger.info(
f"Time taken to find best prompt: {(time.time() - start_time)} sec"
)
return self.BEST_PROMPT, self.EXPERT_PROFILE
def evaluate(self, test_dataset_jsonl: str) -> float:
"""
Evaluate the performance of self.BEST_PROMPT over test dataset. Return the accuracy.
:param test_dataset_jsonl: Path to jsonl file that has test dataset
:return: Percentage accuracy
"""
start_time = time.time()
self.logger.info(f"Evaluation started {CommonLogsStr.LOG_SEPERATOR}")
if not self.BEST_PROMPT:
self.logger.error(
"BEST_PROMPT attribute is not set. Please set self.BEST_PROMPT attribute of this object, "
"either manually or by calling get_best_prompt() method."
)
return
total_correct = 0
total_count = 0
for json_obj in read_jsonl_row(test_dataset_jsonl):
answer = self.predict_and_access(
json_obj[DatasetSpecificProcessing.QUESTION_LITERAL],
json_obj[DatasetSpecificProcessing.FINAL_ANSWER_LITERAL],
)
total_correct += answer[self.EvalLiterals.IS_CORRECT]
total_count += 1
result = {
"accuracy": f"{total_correct}/{total_count} : {total_correct/total_count}%",
"predicted": answer[self.EvalLiterals.PREDICTED_ANS],
"actual": json_obj[DatasetSpecificProcessing.FINAL_ANSWER_LITERAL],
}
self.iolog.append_dict_to_chained_logs(result)
self.logger.info(result)
self.iolog.dump_chained_log_to_file(
file_name=f"eval_result_{self.setup_config.experiment_name}"
)
self.logger.info(f"Time taken for evaluation: {(time.time() - start_time)} sec")
return total_correct / total_count
@iolog.log_io_params
def predict_and_access(self, question: str, gt_answer: str) -> (bool, str, str):
"""
For the given input question, get answer to it from LLM, using the BEST_PROMPT & EXPERT_PROFILE
computes earlier.
:param question: Question to be asked to LLM, to solve
:param gt_answer: Ground truth, final answer.
:return: (is_correct, predicted_ans, llm_output)
is_correct -> Tells if prediction by LLM was correct.
predicted_ans -> is the actual predicted answer by LLM.
llm_output -> Output text generated by LLM for the given question
:rtype: (bool, str, str)
"""
final_prompt = self.prompt_pool.eval_prompt.format(
instruction=self.BEST_PROMPT, question=question
)
llm_output = self.prompt_opt.chat_completion(
user_prompt=final_prompt, system_prompt=self.EXPERT_PROFILE
)
is_correct, predicted_ans = self.data_processor.access_answer(
llm_output, gt_answer
)
return {
self.EvalLiterals.IS_CORRECT: is_correct,
self.EvalLiterals.PREDICTED_ANS: predicted_ans,
self.EvalLiterals.LLM_OUTPUT: llm_output,
}