from copy import deepcopy
import os
import pickle
from dragon.search_algorithm.search_algorithm import SearchAlgorithm
import numpy as np
import pandas as pd
from dragon.utils.tools import logger
[docs]
class Mutant_UCB(SearchAlgorithm):
"""Mutant_UCB
Search algorithm implementing the Mutant-UCB search algorithm inheriting from the `SearchAlgorithm` class.
It implements a `select_next_configuration` and a `process_evaluated_configuration` methods, specific to Mutant-UCB.
Parameters
----------
search_space: `Variable`
`Variable` containing all the design choices from the search space. It should implement a `random` method and a `neighbor` one.
T: int
Number of iterations.
K: int
Size of the population.
N: int
Maximum number of partial training for one configuration.
E: float
Exploratory parameters.
evaluation: function
Performance evaluation function. Takes as argument a set of configuration and the unique index of this configuration. Returns the performance and the model built.
save_dir: str
Path towards saving directory. If not empty, the content will be replaced.
models: list, default=None
List of configurations that should be included into the initial population.
pop_path: str, default=None
Path towards a directory containing an former evaluation that we aim to continue.
verbose: bool, default=False
Verbose boolean.
time_max: int, default=45
Maximum number of time (in minutes) for one evaluation.
Attributes
----------
N: int
Maximum number of partial training for one configuration.
E: float
Exploratory parameters.
sent: dict, default={}
Dictionary containing the configurations that are currently evaluated and thus are temporarly removed from the population.
search_space: `Variable`
`Variable` containing all the design choices from the search space. It should implement a `random` method and a `neighbor` one if necessary.
n_iterations: int
Number of iterations.
population_size: int
Size of the randomly initialized population.
evaluation: function
Performance evaluation function. Takes as argument a set of configuration and the unique index of this configuration. Returns the performance and the model built.
save_dir: str
Path towards saving directory. If not empty, the content will be replaced.
models: list, default=None
List of configurations that should be included into the initial population.
pop_path: str, default=None
Path towards a directory containing an former evaluation that we aim to continue.
verbose: bool, default=False
Verbose boolean.
run: function
Run function to use: MPI (run_mpi) or not (run_no_mpi).
set_mpi: dict
Dictionary containing the MPI parameters.
storage: dict, default={}
Dictionary storing the configurations from the population.
min_loss: float, default=np.min
Current minimum loss found.
time_max: int, default=45
Maximum number of time (in minutes) for one evaluation.
Example
--------
>>> from dragon.search_space.base_variables import ArrayVar
>>> from dragon.search_operators.base_neighborhoods import ArrayInterval
>>> from dragon.search_algorithm.mutant_ucb import Mutant_UCB
>>> search_space = ArrayVar(dag, label="Search Space", neighbor=ArrayInterval())
>>> search_algorithm = Mutant_UCB(search_space, save_dir="save/test_mutant", T=20, N=5, K=5, E=0.01, evaluation=loss_function)
>>> search_algorithm.run()
"""
def __init__(self, search_space, T, K, N, E, evaluation, save_dir, models=None, pop_path=None, verbose=False, **args):
super(Mutant_UCB, self).__init__(search_space=search_space,
n_iterations=T,
init_population_size=K,
evaluation=evaluation,
save_dir=save_dir,
models=models, pop_path=pop_path,
verbose=verbose,
time_max=45)
self.N = N
self.E = E
self.sent = {}
[docs]
def select_next_configurations(self):
"""select_next_configurations()
Defines a selection strategy for Mutant-UCB.
Select the next configuration optimistically: the minimum loss + UCB interval.
With a certain probability remove the configuration from `self.storage` and add it to `self.sent` to perform a new partial training.
If not, increments the number of time the configuration has been picked, creates a new configuration using the `neighbor` attribute, increments the number of models within the population `self.K` and add the configuration to `self.sent`.
Returns
----------
[idx]: list
List containing the idx of the selected configuration.
"""
# Compute ucb loss
iterated = False
while not iterated:
tries = 0
ucb_losses = [self.storage[i]['UCBLoss'] - np.sqrt(self.E/self.storage[i]['N']) for i in self.storage.keys()]
idx = list(self.storage.keys())[np.argmin(ucb_losses)]
try:
# Mutation probability
mutation_p = self.storage[idx]['N_bar'] / self.N
# Random variable
r = np.random.binomial(1, mutation_p, 1)[0]
if r == 0:
# Keep Training, remove the model from the storage
logger.info(f'With p = {mutation_p} = {self.storage[idx]["N_bar"]} / {self.N}, training {idx} instead')
self.sent[idx] = self.storage.pop(idx)
else:
# Mutate the model
logger.info(f'With p = {mutation_p} = {self.storage[idx]["N_bar"]} / {self.N}, mutating {idx} to {self.K}')
self.storage[idx]['N'] +=1
# Load model
with open(f"{self.save_dir}/x_{idx}.pkl", 'rb') as f:
old_x = pickle.load(f)
# mutate the model
x = self.search_space.neighbor(deepcopy(old_x))
idx = self.K
self.K+=1
with open(f"{self.save_dir}/x_{idx}.pkl", 'wb') as f:
pickle.dump(x, f)
del x
self.sent[idx] = {"N": 0, "N_bar": 0, "UCBLoss": 0}
iterated = True
except Exception as e:
tries +=1
if tries < 5:
logger.error(f"While ucb iration, an exception was raised: {e}, attempt {tries}/5.")
else:
self.storage.pop(idx)
if os.path.exists(f"{self.save_dir}/x_{idx}.pkl"):
os.remove(f"{self.save_dir}/x_{idx}.pkl")
logger.error(f"While ucb iration, an exception was raised: {e}, removing {idx} from population. Size storage: {len(self.storage)}.")
return [idx]
[docs]
def process_evaluated_configuration(self, idx, loss):
"""process_evaluated_configuration(idx, loss)
Defines how to process the last evaluated configuration given its loss.
Save the current loss, average loss according to previous evaluations.
Increments the number of time the configuration has been picked and evaluated.
Removes the configuration from `self.sent` and add it to `self.storage`.
Parameters
----------
idx: int
Index of the configuration.
loss: float
Loss of the evaluation.
Returns
--------
delete: False
Boolean indicating if the configuration extra-information should be deleted. In Mutant-UCB, all evaluated configurations are kept.
row_pop: dict
Dictionary containing evaluation information to be saved within a `.csv` file called `computation_file.csv`.
loss: float
Average loss across all evaluations.
"""
if idx in self.sent.keys():
self.sent[idx]['Loss'] = loss
self.sent[idx]['UCBLoss'] = (loss + self.sent[idx]['N_bar']*self.sent[idx]['UCBLoss'])/(self.sent[idx]['N_bar']+1)
self.sent[idx]['N'] +=1
self.sent[idx]['N_bar'] +=1
self.storage[idx] = self.sent.pop(idx)
else:
self.storage[idx] = {"N": 1, "N_bar": 1, "UCBLoss": loss, "Loss": loss}
return False, pd.DataFrame({k: [v] for k, v in self.storage[idx].items()}), loss
[docs]
def process_evaluated_row(self, row):
"""process_evaluated_row(row)
Modifies the `process_evaluated_row` method from `SearchAlgorithm` to add the extra information contained by `computation_file.csv`.
Add the average loss, the number of times the configuration has been picked and evaluated to the `self.storage` dictionary.
Parameters
----------
row: dict
Dictionary containing the information of an evaluated configuration.
"""
loss = row['UCBLoss']
self.storage[row['Idx']] = {"Loss": row['Loss'], "N": row['N'], "N_bar": row['N_bar'], "UCBLoss": loss}
if self.min_loss > loss:
logger.info(f'Best found! {loss} < {self.min_loss}')
self.min_loss = loss