fastapi_django_main_live / AutoPrompt /optimization_pipeline.py
kenken999's picture
fda
0f43f8a
raw
history blame
13 kB
import pandas as pd
from eval.evaluator import Eval
from dataset.base_dataset import DatasetBase
from utils.llm_chain import MetaChain
from estimator import give_estimator
from pathlib import Path
import pickle
import os
import json
import logging
import wandb
class OptimizationPipeline:
"""
The main pipeline for optimization. The pipeline is composed of 4 main components:
1. dataset - The dataset handle the data including the annotation and the prediction
2. annotator - The annotator is responsible generate the GT
3. predictor - The predictor is responsible to generate the prediction
4. eval - The eval is responsible to calculate the score and the large errors
"""
def __init__(self, config, task_description: str = None, initial_prompt: str = None, output_path: str = ''):
"""
Initialize a new instance of the ClassName class.
:param config: The configuration file (EasyDict)
:param task_description: Describe the task that needed to be solved
:param initial_prompt: Provide an initial prompt to solve the task
:param output_path: The output dir to save dump, by default the dumps are not saved
"""
if config.use_wandb: # In case of using W&B
wandb.login()
self.wandb_run = wandb.init(
project="AutoGPT",
config=config,
)
if output_path == '':
self.output_path = None
else:
if not os.path.isdir(output_path):
os.makedirs(output_path)
self.output_path = Path(output_path)
logging.basicConfig(filename=self.output_path / 'info.log', level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s', force=True)
self.dataset = None
self.config = config
self.meta_chain = MetaChain(config)
self.initialize_dataset()
self.task_description = task_description
self.cur_prompt = initial_prompt
self.predictor = give_estimator(config.predictor)
self.annotator = give_estimator(config.annotator)
self.eval = Eval(config.eval, self.meta_chain.error_analysis, self.dataset.label_schema)
self.batch_id = 0
self.patient = 0
@staticmethod
def log_and_print(message):
print(message)
logging.info(message)
def initialize_dataset(self):
"""
Initialize the dataset: Either empty dataset or loading an existing dataset
"""
logging.info('Initialize dataset')
self.dataset = DatasetBase(self.config.dataset)
if 'initial_dataset' in self.config.dataset.keys():
logging.info(f'Load initial dataset from {self.config.dataset.initial_dataset}')
self.dataset.load_dataset(self.config.dataset.initial_dataset)
def calc_usage(self):
"""
Calculate the usage of the optimization process (either $ in case of openAI or #tokens the other cases)
"""
total_usage = 0
total_usage += self.meta_chain.calc_usage()
total_usage += self.annotator.calc_usage()
total_usage += self.predictor.calc_usage()
return total_usage
def extract_best_prompt(self):
sorted_history = sorted(
self.eval.history[min(self.config.meta_prompts.warmup - 1, len(self.eval.history) - 1):],
key=lambda x: x['score'],
reverse=False)
return {'prompt': sorted_history[-1]['prompt'], 'score': sorted_history[-1]['score']}
def run_step_prompt(self):
"""
Run the meta-prompts and get new prompt suggestion, estimated prompt score and a set of challenging samples
for the new prompts
"""
step_num = len(self.eval.history)
if (step_num < self.config.meta_prompts.warmup) or (step_num % 3) > 0:
last_history = self.eval.history[-self.config.meta_prompts.history_length:]
else:
sorted_history = sorted(self.eval.history[self.config.meta_prompts.warmup - 1:], key=lambda x: x['score'],
reverse=False)
last_history = sorted_history[-self.config.meta_prompts.history_length:]
history_prompt = '\n'.join([self.eval.sample_to_text(sample,
num_errors_per_label=self.config.meta_prompts.num_err_prompt,
is_score=True) for sample in last_history])
prompt_input = {"history": history_prompt, "task_description": self.task_description,
'error_analysis': last_history[-1]['analysis']}
if 'label_schema' in self.config.dataset.keys():
prompt_input["labels"] = json.dumps(self.config.dataset.label_schema)
prompt_suggestion = self.meta_chain.step_prompt_chain.invoke(prompt_input)
self.log_and_print(f'Previous prompt score:\n{self.eval.mean_score}\n#########\n')
self.log_and_print(f'Get new prompt:\n{prompt_suggestion["prompt"]}')
self.batch_id += 1
if len(self.dataset) < self.config.dataset.max_samples:
batch_input = {"num_samples": self.config.meta_prompts.samples_generation_batch,
"task_description": self.task_description,
"prompt": prompt_suggestion['prompt']}
batch_inputs = self.generate_samples_batch(batch_input, self.config.meta_prompts.num_generated_samples,
self.config.meta_prompts.samples_generation_batch)
if sum([len(t['errors']) for t in last_history]) > 0:
history_samples = '\n'.join([self.eval.sample_to_text(sample,
num_errors_per_label=self.config.meta_prompts.num_err_samples,
is_score=False) for sample in last_history])
for batch in batch_inputs:
extra_samples = self.dataset.sample_records()
extra_samples_text = DatasetBase.samples_to_text(extra_samples)
batch['history'] = history_samples
batch['extra_samples'] = extra_samples_text
else:
for batch in batch_inputs:
extra_samples = self.dataset.sample_records()
extra_samples_text = DatasetBase.samples_to_text(extra_samples)
batch['history'] = 'No previous errors information'
batch['extra_samples'] = extra_samples_text
samples_batches = self.meta_chain.step_samples.batch_invoke(batch_inputs,
self.config.meta_prompts.num_workers)
new_samples = [element for sublist in samples_batches for element in sublist['samples']]
new_samples = self.dataset.remove_duplicates(new_samples)
self.dataset.add(new_samples, self.batch_id)
logging.info('Get new samples')
self.cur_prompt = prompt_suggestion['prompt']
def stop_criteria(self):
"""
Check if the stop criteria holds. The conditions for stopping:
1. Usage is above the threshold
2. There was no improvement in the last > patient steps
"""
if 0 < self.config.stop_criteria.max_usage < self.calc_usage():
return True
if len(self.eval.history) <= self.config.meta_prompts.warmup:
self.patient = 0
return False
min_batch_id, max_score = self.eval.get_max_score(self.config.meta_prompts.warmup-1)
if max_score - self.eval.history[-1]['score'] > -self.config.stop_criteria.min_delta:
self.patient += 1
else:
self.patient = 0
if self.patient > self.config.stop_criteria.patience:
return True
return False
@staticmethod
def generate_samples_batch(batch_input, num_samples, batch_size):
"""
Generate samples in batch
"""
batch_num = num_samples // batch_size
all_batches = [batch_input.copy() for _ in range(batch_num)]
reminder = num_samples - batch_num * batch_size
if reminder > 0:
all_batches.append(batch_input.copy())
all_batches[-1]['num_samples'] = reminder
return all_batches
def generate_initial_samples(self):
"""
In case the initial dataset is empty generate the initial samples
"""
batch_input = {"num_samples": self.config.meta_prompts.samples_generation_batch,
"task_description": self.task_description,
"instruction": self.cur_prompt}
batch_inputs = self.generate_samples_batch(batch_input, self.config.meta_prompts.num_initialize_samples,
self.config.meta_prompts.samples_generation_batch)
samples_batches = self.meta_chain.initial_chain.batch_invoke(batch_inputs, self.config.meta_prompts.num_workers)
samples_list = [element for sublist in samples_batches for element in sublist['samples']]
samples_list = self.dataset.remove_duplicates(samples_list)
self.dataset.add(samples_list, 0)
def save_state(self):
"""
Save the process state
"""
if self.output_path is None:
return
logging.info('Save state')
self.dataset.save_dataset(self.output_path / 'dataset.csv')
state = {'history': self.eval.history, 'batch_id': self.batch_id,
'prompt': self.cur_prompt, 'task_description': self.task_description,
'patient': self.patient}
pickle.dump(state, open(self.output_path / 'history.pkl', 'wb'))
def load_state(self, path: str):
"""
Load pretrain state
"""
path = Path(path)
if (path / 'dataset.csv').is_file():
self.dataset.load_dataset(path / 'dataset.csv')
if (path / 'history.pkl').is_file():
state = pickle.load(open(path / 'history.pkl', 'rb'))
self.eval.history = state['history']
self.batch_id = state['batch_id']
self.cur_prompt = state['prompt']
self.task_description = state['task_description']
self.patient = state['patient']
def step(self, current_iter, total_iter):
"""
This is the main optimization process step.
"""
self.log_and_print(f'Starting step {self.batch_id}')
if len(self.dataset.records) == 0:
self.log_and_print('Dataset is empty generating initial samples')
self.generate_initial_samples()
if self.config.use_wandb:
cur_batch = self.dataset.get_leq(self.batch_id)
random_subset = cur_batch.sample(n=min(10, len(cur_batch)))[['text']]
self.wandb_run.log(
{"Prompt": wandb.Html(f"<p>{self.cur_prompt}</p>"), "Samples": wandb.Table(dataframe=random_subset)},
step=self.batch_id)
logging.info('Running annotator')
records = self.annotator.apply(self.dataset, self.batch_id)
self.dataset.update(records)
self.predictor.cur_instruct = self.cur_prompt
logging.info('Running Predictor')
records = self.predictor.apply(self.dataset, self.batch_id, leq=True)
self.dataset.update(records)
self.eval.dataset = self.dataset.get_leq(self.batch_id)
self.eval.eval_score()
logging.info('Calculating Score')
large_errors = self.eval.extract_errors()
self.eval.add_history(self.cur_prompt, self.task_description)
if self.config.use_wandb:
large_errors = large_errors.sample(n=min(6, len(large_errors)))
correct_samples = self.eval.extract_correct()
correct_samples = correct_samples.sample(n=min(6, len(correct_samples)))
vis_data = pd.concat([large_errors, correct_samples])
self.wandb_run.log({"score": self.eval.history[-1]['score'],
"prediction_result": wandb.Table(dataframe=vis_data),
'Total usage': self.calc_usage()}, step=self.batch_id)
if self.stop_criteria():
self.log_and_print('Stop criteria reached')
return True
if current_iter != total_iter-1:
self.run_step_prompt()
self.save_state()
return False
def run_pipeline(self, num_steps: int):
# Run the optimization pipeline for num_steps
num_steps_remaining = num_steps - self.batch_id
for i in range(num_steps_remaining):
stop_criteria = self.step(i, num_steps_remaining)
if stop_criteria:
break
final_result = self.extract_best_prompt()
return final_result