Source code for customhys.experiment

"""
This module contains the main experiments performed using the current framework.

Created on Mon Sep 30 13:42:15 2019

@author: Jorge Mario Cruz-Duarte (jcrvz.github.io), e-mail: j.m.cruzduarte@ieee.org
"""

import multiprocessing
from os import path

from . import benchmark_func as bf
from . import hyperheuristic as hyp
from . import operators as op
from . import tools as tl

# %% PREDEFINED CONFIGURATIONS
# Use configuration files instead of predefined dictionaries

# Configuration dictionary for experiments
ex_configs = [
    {
        "experiment_name": "demo_test",
        "experiment_type": "default",
        "heuristic_collection_file": "default.txt",
        "weights_dataset_file": "operators_weights.json",
    },  # 0 - Default
    {
        "experiment_name": "brute_force",
        "experiment_type": "brute_force",
        "heuristic_collection_file": "default.txt",
    },  # 1 - Brute force
    {
        "experiment_name": "basic_metaheuristics",
        "experiment_type": "basic_metaheuristics",
        "heuristic_collection_file": "basicmetaheuristics.txt",
    },  # 2 - Basic metaheuristics
    {
        "experiment_name": "short_test1",
        "experiment_type": "default",
        "heuristic_collection_file": "default.txt",
        "weights_dataset_file": "operators_weights.json",
    },  # 3 - Short collection
    {
        "experiment_name": "short_test2",
        "experiment_type": "default",
        "heuristic_collection_file": "default.txt",
    },  # 4 - Short collection +
    {
        "experiment_name": "long_test",
        "experiment_type": "default",
        "heuristic_collection_file": "test-set-21.txt",
        "auto_collection_num_vals": 21,
    },  # 5 - Long collection
]

# Configuration dictionary for hyper-heuristics
hh_configs = [
    {"cardinality": 3, "num_replicas": 30},  # 0 - Default
    {"cardinality": 1, "num_replicas": 30},  # 1 - Brute force
    {"cardinality": 1, "num_replicas": 30},  # 2 - Basic metaheuristic
    {"cardinality": 3, "num_replicas": 50},  # 3 - Short collection
    {"cardinality": 5, "num_replicas": 50},  # 4 - Short collection +
    {"cardinality": 3, "num_replicas": 50},  # 5 - Long collection
]

# Configuration dictionary for problems
pr_configs = [
    {"dimensions": [2, 5], "functions": ["<choose_randomly>"]},  # 0 - Default
    {"dimensions": [2, 5, *range(10, 50 + 1, 10)], "functions": bf.__all__},  # 1 - Brute force
    {"dimensions": [2, 5, *range(10, 50 + 1, 10)], "functions": bf.__all__},  # 2 - Basic metaheuristic
    {"dimensions": [2, 5, *range(10, 50 + 1, 10)], "functions": bf.__all__},  # 3 - Short collection
    {"dimensions": [2, 5, *range(10, 50 + 1, 10)], "functions": bf.__all__},  # 4 - Short collection +
    {"dimensions": [2, 5, *range(10, 50 + 1, 10)], "functions": bf.__all__},  # 5 - Long collection
]


# %% EXPERIMENT CLASS

# _ex_config_demo = {'experiment_name': 'demo_test', 'experiment_type': 'default',
#                    'heuristic_collection_file': 'default.txt', 'weights_dataset_file': 'operators_weights.json'}
# _hh_config_demo = {'cardinality': 3, 'num_replicas': 30}
# _pr_config_demo = {'dimensions': [2, 5], 'functions': [bf.__all__[hyp.np.random.randint(0, len(bf.__all__))]]}


[docs] class Experiment: """ Create an experiment using certain configurations. """
[docs] def __init__(self, config_file=None, exp_config=None, hh_config=None, prob_config=None): """ Initialise the experiment object. :param str config_file: Name of the configuration JSON file with the configuration dictionaries: exp_config, hh_config, and prob_config. If only the filename is provided, it is assumed that such a file is in the directory './exconf/'. Otherwise, the full path must be entered. The default value is None. :param dict exp_config: Configuration dictionary related to the experiment. Keys and default values are listed as follows: 'experiment_name': 'test', # Name of the experiment 'experiment_type': 'default', # Type: 'default', 'brute_force', 'basic_metaheuristics' 'heuristic_collection_file': 'default.txt', # Heuristic space located in /collections/ 'weights_dataset_file': None, # Weights or probability distribution of heuristic space 'use_parallel': True, # Run the experiment using a pool of processors 'parallel_pool_size': None, # Number of processors available, None = Default 'auto_collection_num_vals': 5 # Number of values for creating an automatic collection **NOTE 1:** 'experiment_type': 'default' or another name mean hyper-heuristic. **NOTE 2:** If the collection does not exist, and it is not a reserved one ('default.txt', 'automatic.txt', 'basicmetaheuristics.txt', 'test_collection'), then an automatic heuristic space is generated with ``op.build_operators`` with 'auto_collection_num_vals' as ``num_vals`` and 'heuristic_collection_file' as ``filename``. **NOTE 3:** # 'weights_dataset_file' must be determined in a pre-processing step. For the 'default' heuristic space, it is provided 'operators_weights.json'. :param dict hh_config: Configuration dictionary related to the hyper-heuristic procedure. Keys and default values are listed as follows: 'cardinality': 3, # Maximum cardinality used for building metaheuristics 'num_agents': 30, # Population size employed by the metaheuristic 'num_iterations': 100, # Maximum number of iterations used by the metaheuristic 'num_replicas': 50, # Number of replicas for each metaheuristic implemented 'num_steps': 100, # * Number of steps that the hyper-heuristic performs 'max_temperature': 200, # * Initial temperature for HH-Simulated Annealing 'stagnation_percentage': 0.3, # * Percentage of stagnation used by the hyper-heuristic 'cooling_rate': 0.05 # * Cooling rate for HH-Simulated Annealing **NOTE 4:** Keys with * correspond to those that are only used when ``exp_config['experiment_type']`` is neither 'brute_force' nor 'basic_metaheuristic'. :param dict prob_config: Configuration dictionary related to the problems to solve. Keys and default values are listed as follows: 'dimensions': [2, 5, 10, 20, 30, 40, 50], # List of dimensions for the problem domains 'functions': bf.__all__, # List of function names of the optimisation problems 'is_constrained': True # True if the problem domain is hard constrained :return: None. """ self.exp_config, self.hh_config, self.prob_config = read_config_file( config_file, exp_config, hh_config, prob_config ) # Check if the heuristic collection exists if not path.isfile("./collections/" + self.exp_config["heuristic_collection_file"]): # If the name is a reserved one. These files cannot be not created automatically if exp_config["heuristic_collection_file"] in [ "default.txt", "automatic.txt", "basicmetaheuristics.txt", "test_collection", "short_collection.txt", ]: raise ExperimentError("This collection name is reserved and cannot be created automatically!") else: op.build_operators( op.obtain_operators(num_vals=exp_config["auto_collection_num_vals"]), file_name=exp_config["heuristic_collection_file"].split(".")[0], ) self.exp_config["weights_dataset_file"] = None # Check if the weights dataset not exist or required if self.exp_config["weights_dataset_file"] and ( self.exp_config["experiment_type"] not in ["brute_force", "basic_metaheuristics"] ): if path.isfile("collections/" + self.exp_config["weights_dataset_file"]): self.weights_data = tl.read_json("collections/" + self.exp_config["weights_dataset_file"]) else: raise ExperimentError("A valid weights_dataset_file must be provided in exp_config") else: self.weights_data = None
[docs] def run(self): """ Run the experiment according to the configuration variables. :return: None """ # TODO: Create a task log for prevent interruptions # Create a list of problems from functions and dimensions combinations all_problems = create_task_list(self.prob_config["functions"], self.prob_config["dimensions"]) # Check if the experiment will be in parallel if self.exp_config["use_parallel"]: pool = multiprocessing.Pool(self.exp_config["parallel_pool_size"]) pool.map(self._simple_run, all_problems) else: for prob_dim in all_problems: self._simple_run(prob_dim)
def _simple_run(self, prob_dim): """ Perform a single run, i.e., for a problem and dimension combination :param tuple prob_dim: Problem name and dimensionality ``(function_string, num_dimensions)`` :return: None. """ # Read the function name and the number of dimensions function_string, num_dimensions = prob_dim # Message to print and to store in folders label = "{}-{}D-{}".format(function_string, num_dimensions, self.exp_config["experiment_name"]) # Get and format the problem problem = eval(f"bf.{function_string}({num_dimensions})") problem_to_solve = problem.get_formatted_problem(self.prob_config["is_constrained"]) # Read the particular weights array (if so) weights = ( self.weights_data[str(num_dimensions)][problem.get_features(fmt="string", wrd="1")] if self.weights_data else None ) # Call the hyper-heuristic object hh = hyp.Hyperheuristic( heuristic_space=self.exp_config["heuristic_collection_file"], problem=problem_to_solve, parameters=self.hh_config, file_label=label, weights_array=weights, ) # Run the HH according to the specified type save_steps = self.exp_config["save_steps"] if self.exp_config["experiment_type"] in ["brute_force", "bf"]: hh.brute_force(save_steps) elif self.exp_config["experiment_type"] in ["basic_metaheuristics", "bmh"]: hh.basic_metaheuristics(save_steps) elif self.exp_config["experiment_type"] in ["online_learning", "dynamic"]: hh.solve("dynamic", save_steps) elif self.exp_config["experiment_type"] in ["neural_network"]: hh.solve("neural_network", save_steps) else: # 'static_run' hh.solve("static", save_steps) if self.exp_config["verbose"]: print(label + " done!")
[docs] class ExperimentError(Exception): """ Simple ExperimentError to manage exceptions. """ pass
[docs] def read_config_file(config_file=None, exp_config=None, hh_config=None, prob_config=None): """ Return the experimental (`exp_config`), hyper-heuristic (`hh_config`), problem (`prob_config`) configuration variables from `config_file`, if it is supplied. Otherwise, use the `exp_config`, `hh_config`, and `prob_config` inputs. If there is no input, then assume the default values for these three configuration variables. Further information about these variables can be found in the Experiment class's `__doc__`. """ # If there is a configuration file if config_file: # Adjustments directory, filename = path.split(config_file) if directory == "": directory = "./exconf/" # Default directory basename, extension = path.splitext(filename) if extension not in [".json", ""]: raise ExperimentError("Configuration file must be JSON") # Existence verification and load full_path = path.join(directory, basename + ".json") if path.isfile(full_path): all_configs = tl.read_json(full_path) # Load data from json file exp_config = all_configs["ex_config"] hh_config = all_configs["hh_config"] prob_config = all_configs["prob_config"] else: raise ExperimentError(f"File {full_path} does not exist!") else: if exp_config is None: exp_config = {} if hh_config is None: hh_config = {} if prob_config is None: prob_config = {} # Load the default experiment configuration and compare it with exp_cfg exp_config = tl.check_fields( { "experiment_name": "test", "experiment_type": "default", # 'default' -> hh, 'brute_force', 'basic_metaheuristics' "heuristic_collection_file": "default.txt", "weights_dataset_file": None, # 'operators_weights.json', "use_parallel": True, "parallel_pool_size": None, # Default "auto_collection_num_vals": 5, "save_steps": True, "verbose": True, }, exp_config, ) # Load the default hyper-heuristic configuration and compare it with hh_cfg hh_config = tl.check_fields( { "cardinality": 3, "num_agents": 30, "num_iterations": 100, "num_replicas": 50, "num_steps": 200, "max_temperature": 1, "min_temperature": 1e-6, "stagnation_percentage": 0.37, "temperature_scheme": "fast", "acceptance_scheme": "exponential", "cooling_rate": 1e-3, "cardinality_min": 1, "repeat_operators": True, "as_mh": True, "verbose": False, "verbose_statistics": False, "trial_overflow": True, "learnt_dataset": None, "allow_weight_matrix": True, "learning_portion": 0.37, "solver": "static", "initial_scheme": "random", "tabu_idx": None, "model_params": None, "limit_time": None, }, hh_config, ) # Load the default problem configuration and compare it with prob_config prob_config = tl.check_fields( { "dimensions": [2, 5, *range(10, 50 + 1, 10)], "functions": bf.__all__, "is_constrained": True, "features": ["Differentiable", "Separable", "Unimodal"], }, prob_config, ) # Check if there is a special case of function name: <choose_randomly> prob_config["functions"] = [ bf.__all__[hyp.np.random.randint(0, len(bf.__all__))] if fun in ["<choose_randomly>", "<random>"] else fun for fun in prob_config["functions"] ] return exp_config, hh_config, prob_config
[docs] def create_task_list(function_list, dimension_list): """ Return a list of combinations (in tuple form) for problems from functions and dimensions. :param list function_list: List of functions from the `benchmark_func` module. :param list dimension_list: List of dimensions considered for each one of these functions. :return: list of tuples """ return [(x, y) for x in function_list for y in dimension_list]
# %% Auto-run if __name__ == "__main__": # Import module for calling this code from command-line import argparse import os from tools import preprocess_files DATA_FOLDER = "./data_files/raw" OUTPUT_FOLDER = "./data_files/exp_output" # Only one argument is allowed: the code parser = argparse.ArgumentParser(description="Run certain experiment, default experiment is './exconf/demo.json'") parser.add_argument("-b", "--batch", action="store_true", help="carry out a batch of experiments") parser.add_argument( "exp_config", metavar="config_filename", type=str, nargs="?", default="demo", help="""Name of the configuration file in './exconf/' or its full path. Only JSON files. If --batch flag is given, it is assumed that the entered file contains a list of all the paths of experiment files (JSON) to carry out. It would be read as plain text.""", ) args = parser.parse_args() if not os.path.exists(OUTPUT_FOLDER): os.makedirs(OUTPUT_FOLDER) exp_filenames: list[str] | str if args.batch: with open(args.exp_config, encoding="utf-8") as configs: exp_filenames = configs.read() exp_filenames = [filename.strip() for filename in exp_filenames.splitlines()] else: exp_filenames = [args.exp_config] for exp_filename in exp_filenames: tail_message = ( f" - ({exp_filenames.index(exp_filename)+1}/{len(exp_filenames)})" + "\n" + ("=" * 50) if args.batch else "" ) print(f"\nRunning {exp_filename.split('.')[0]}" + tail_message) # Create the experiment to runs exp = Experiment(config_file=exp_filename) # print("* Experiment configuration: \n", "-" * 30 + "\n", json.dumps(exp.prob_config, indent=2, default=str)) # print("* Hyper-heuristic configuration: \n", "-" * 30 + "\n",json.dumps(exp.hh_config, indent=2, default=str)) # print("* Problem configuration: \n", "-" * 30 + "\n", json.dumps(exp.prob_config, indent=2, default=str)) # Run the experiment et voilร  exp.run() # After run, it preprocesses all the raw data print(f"\nPreprocessing {exp_filename.split('.')[0]}" + tail_message) preprocess_files( "data_files/raw/", kind=exp.hh_config["solver"], experiment=exp_filename, output_name=OUTPUT_FOLDER + "/" + exp.exp_config["experiment_name"], ) # Rename the raw folder to raw-$exp_name$ print("\nChanging folder name of raw results...") os.rename(DATA_FOLDER, DATA_FOLDER + "-" + exp.exp_config["experiment_name"]) print("\nExperiment(s) finished!")