From 1a336a094eb409ab1bea73dbba1c5c9a03f9d13a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Monta=C3=B1ana=20G=C3=B3mez?= Date: Fri, 20 Dec 2024 17:36:43 +0100 Subject: [PATCH] Refactor gridsearch and begin gridexperiment --- src/CMakeLists.txt | 2 +- src/commands/b_grid.cpp | 1 + src/grid/GridConfig.h | 74 ++++++ src/grid/GridExperiment.cpp | 243 +++++++++++++++++++ src/grid/GridExperiment.h | 35 +++ src/grid/GridFunctions.cpp | 458 ++++++++++++++++++++++++++++++++++++ src/grid/GridSearch.cpp | 227 +----------------- src/grid/GridSearch.h | 32 +-- 8 files changed, 815 insertions(+), 257 deletions(-) create mode 100644 src/grid/GridConfig.h create mode 100644 src/grid/GridExperiment.cpp create mode 100644 src/grid/GridExperiment.h create mode 100644 src/grid/GridFunctions.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ea0a3bc..f0f9835 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -29,7 +29,7 @@ add_executable( target_link_libraries(b_best Boost::boost "${PyClassifiers}" "${BayesNet}" fimdlp ${Python3_LIBRARIES} "${TORCH_LIBRARIES}" ${LIBTORCH_PYTHON} Boost::python Boost::numpy "${XLSXWRITER_LIB}") # b_grid -set(grid_sources GridSearch.cpp GridData.cpp) +set(grid_sources GridSearch.cpp GridData.cpp GridExperiment.cpp GridFunctions.cpp) list(TRANSFORM grid_sources PREPEND grid/) add_executable(b_grid commands/b_grid.cpp ${grid_sources} common/Datasets.cpp common/Dataset.cpp common/Discretization.cpp diff --git a/src/commands/b_grid.cpp b/src/commands/b_grid.cpp index 86b5669..168d00a 100644 --- a/src/commands/b_grid.cpp +++ b/src/commands/b_grid.cpp @@ -11,6 +11,7 @@ #include "common/Colors.h" #include "common/DotEnv.h" #include "grid/GridSearch.h" +#include "grid/GridExperiment.h" #include "config_platform.h" using json = nlohmann::ordered_json; diff --git a/src/grid/GridConfig.h b/src/grid/GridConfig.h new file mode 100644 index 0000000..ccf0b8d --- /dev/null +++ b/src/grid/GridConfig.h @@ -0,0 +1,74 @@ +#ifndef GRIDCONFIG_H +#define GRIDCONFIG_H +#include +#include +#include +#include +#include "common/Datasets.h" +#include "common/Timer.h" +#include "main/HyperParameters.h" +#include "GridData.h" +#include "bayesnet/network/Network.h" + + +namespace platform { + using json = nlohmann::ordered_json; + struct ConfigGrid { + std::string model; + std::string score; + std::string continue_from; + std::string platform; + std::string smooth_strategy; + bool quiet; + bool only; // used with continue_from to only compute that dataset + bool discretize; + bool stratified; + int nested; + int n_folds; + json excluded; + std::vector seeds; + }; + struct ConfigMPI { + int rank; + int n_procs; + int manager; + }; + typedef struct { + uint idx_dataset; + uint idx_combination; + int n_fold; + double score; + double time; + } Task_Result; + const int TAG_QUERY = 1; + const int TAG_RESULT = 2; + const int TAG_TASK = 3; + const int TAG_END = 4; + /* ************************************************************************************************************* + // + // MPI Common Functions + // + ************************************************************************************************************* */ + std::string get_color_rank(int rank); + /* ************************************************************************************************************* + // + // MPI Experiment Functions + // + ************************************************************************************************************* */ + json mpi_experiment_producer(std::vector& names, json& tasks, struct ConfigMPI& config_mpi, MPI_Datatype& MPI_Result); + void mpi_experiment_consumer(Datasets& datasets, json& tasks, struct ConfigGrid& config, struct ConfigMPI& config_mpi, MPI_Datatype& MPI_Result); + void join_results_folds(json& results, json& all_results, std::string& model); + json store_experiment_result(std::vector& names, Task_Result& result, json& results); + void mpi_experiment_consumer_go(struct ConfigGrid& config, struct ConfigMPI& config_mpi, json& tass, int n_task, Datasets& datasets, Task_Result* result); + /* ************************************************************************************************************* + // + // MPI Search Functions + // + ************************************************************************************************************* */ + json mpi_search_producer(std::vector& names, json& tasks, struct ConfigMPI& config_mpi, MPI_Datatype& MPI_Result); + void mpi_search_consumer(Datasets& datasets, json& tasks, struct ConfigGrid& config, struct ConfigMPI& config_mpi, MPI_Datatype& MPI_Result); + void select_best_results_folds(json& results, json& all_results, std::string& model); + json store_search_result(std::vector& names, Task_Result& result, json& results); + void mpi_experiment_consumer_go(struct ConfigGrid& config, struct ConfigMPI& config_mpi, json& tasks, int n_task, Datasets& datasets, Task_Result* result); +} /* namespace platform */ +#endif \ No newline at end of file diff --git a/src/grid/GridExperiment.cpp b/src/grid/GridExperiment.cpp new file mode 100644 index 0000000..ccb4016 --- /dev/null +++ b/src/grid/GridExperiment.cpp @@ -0,0 +1,243 @@ +#include +#include +#include +#include +#include "main/Models.h" +#include "common/Paths.h" +#include "common/Colors.h" +#include "common/Utils.h" +#include "GridExperiment.h" + +namespace platform { + + GridExperiment::GridExperiment(struct ConfigGrid& config) : config(config) + { + if (config.smooth_strategy == "ORIGINAL") + smooth_type = bayesnet::Smoothing_t::ORIGINAL; + else if (config.smooth_strategy == "LAPLACE") + smooth_type = bayesnet::Smoothing_t::LAPLACE; + else if (config.smooth_strategy == "CESTNIK") + smooth_type = bayesnet::Smoothing_t::CESTNIK; + else { + std::cerr << "GridSearch: Unknown smoothing strategy: " << config.smooth_strategy << std::endl; + exit(1); + } + } + json GridExperiment::loadResults() + { + std::ifstream file(Paths::grid_output(config.model)); + if (file.is_open()) { + return json::parse(file); + } + return json(); + } + std::vector GridExperiment::filterDatasets(Datasets& datasets) const + { + // Load datasets + auto datasets_names = datasets.getNames(); + if (config.continue_from != NO_CONTINUE()) { + // Continue previous execution: + if (std::find(datasets_names.begin(), datasets_names.end(), config.continue_from) == datasets_names.end()) { + throw std::invalid_argument("Dataset " + config.continue_from + " not found"); + } + // Remove datasets already processed + std::vector::iterator it = datasets_names.begin(); + while (it != datasets_names.end()) { + if (*it != config.continue_from) { + it = datasets_names.erase(it); + } else { + if (config.only) + ++it; + else + break; + } + } + } + // Exclude datasets + for (const auto& name : config.excluded) { + auto dataset = name.get(); + auto it = std::find(datasets_names.begin(), datasets_names.end(), dataset); + if (it == datasets_names.end()) { + throw std::invalid_argument("Dataset " + dataset + " already excluded or doesn't exist!"); + } + datasets_names.erase(it); + } + return datasets_names; + } + json GridExperiment::build_tasks_mpi(int rank) + { + auto tasks = json::array(); + auto grid = GridData(Paths::grid_input(config.model)); + auto datasets = Datasets(false, Paths::datasets()); + auto all_datasets = datasets.getNames(); + auto datasets_names = filterDatasets(datasets); + for (int idx_dataset = 0; idx_dataset < datasets_names.size(); ++idx_dataset) { + auto dataset = datasets_names[idx_dataset]; + for (const auto& seed : config.seeds) { + auto combinations = grid.getGrid(dataset); + for (int n_fold = 0; n_fold < config.n_folds; n_fold++) { + json task = { + { "dataset", dataset }, + { "idx_dataset", idx_dataset}, + { "seed", seed }, + { "fold", n_fold}, + }; + tasks.push_back(task); + } + } + } + // Shuffle the array so heavy datasets are eas ier spread across the workers + std::mt19937 g{ 271 }; // Use fixed seed to obtain the same shuffle + std::shuffle(tasks.begin(), tasks.end(), g); + std::cout << "* Number of tasks: " << tasks.size() << std::endl; + std::cout << separator << std::flush; + for (int i = 0; i < tasks.size(); ++i) { + if ((i + 1) % 10 == 0) + std::cout << separator; + else + std::cout << (i + 1) % 10; + } + std::cout << separator << std::endl << separator << std::flush; + return tasks; + } + void GridExperiment::go(struct ConfigMPI& config_mpi) + { + /* + * Each task is a json object with the following structure: + * { + * "dataset": "dataset_name", + * "idx_dataset": idx_dataset, // used to identify the dataset in the results + * // this index is relative to the list of used datasets in the actual run not to the whole datasets list + * "seed": # of seed to use, + * "fold": # of fold to process + * } + * + * This way a task consists in process all combinations of hyperparameters for a dataset, seed and fold + * + * The overall process consists in these steps: + * 0. Create the MPI result type & tasks + * 0.1 Create the MPI result type + * 0.2 Manager creates the tasks + * 1. Manager will broadcast the tasks to all the processes + * 1.1 Broadcast the number of tasks + * 1.2 Broadcast the length of the following string + * 1.2 Broadcast the tasks as a char* string + * 2a. Producer delivers the tasks to the consumers + * 2a.1 Producer will loop to send all the tasks to the consumers and receive the results + * 2a.2 Producer will send the end message to all the consumers + * 2b. Consumers process the tasks and send the results to the producer + * 2b.1 Consumers announce to the producer that they are ready to receive a task + * 2b.2 Consumers receive the task from the producer and process it + * 2b.3 Consumers send the result to the producer + * 3. Manager select the bests scores for each dataset + * 3.1 Loop thru all the results obtained from each outer fold (task) and select the best + * 3.2 Save the results + */ + // + // 0.1 Create the MPI result type + // + Task_Result result; + int tasks_size; + MPI_Datatype MPI_Result; + MPI_Datatype type[5] = { MPI_UNSIGNED, MPI_UNSIGNED, MPI_INT, MPI_DOUBLE, MPI_DOUBLE }; + int blocklen[5] = { 1, 1, 1, 1, 1 }; + MPI_Aint disp[5]; + disp[0] = offsetof(Task_Result, idx_dataset); + disp[1] = offsetof(Task_Result, idx_combination); + disp[2] = offsetof(Task_Result, n_fold); + disp[3] = offsetof(Task_Result, score); + disp[4] = offsetof(Task_Result, time); + MPI_Type_create_struct(5, blocklen, disp, type, &MPI_Result); + MPI_Type_commit(&MPI_Result); + // + // 0.2 Manager creates the tasks + // + char* msg; + json tasks; + if (config_mpi.rank == config_mpi.manager) { + timer.start(); + tasks = build_tasks_mpi(config_mpi.rank); + auto tasks_str = tasks.dump(); + tasks_size = tasks_str.size(); + msg = new char[tasks_size + 1]; + strcpy(msg, tasks_str.c_str()); + } + // + // 1. Manager will broadcast the tasks to all the processes + // + MPI_Bcast(&tasks_size, 1, MPI_INT, config_mpi.manager, MPI_COMM_WORLD); + if (config_mpi.rank != config_mpi.manager) { + msg = new char[tasks_size + 1]; + } + MPI_Bcast(msg, tasks_size + 1, MPI_CHAR, config_mpi.manager, MPI_COMM_WORLD); + tasks = json::parse(msg); + delete[] msg; + auto env = platform::DotEnv(); + auto datasets = Datasets(config.discretize, Paths::datasets(), env.get("discretize_algo")); + + if (config_mpi.rank == config_mpi.manager) { + // + // 2a. Producer delivers the tasks to the consumers + // + auto datasets_names = filterDatasets(datasets); + json all_results = mpi_search_producer(datasets_names, tasks, config_mpi, MPI_Result); + std::cout << separator << std::endl; + // + // 3. Manager select the bests sccores for each dataset + // + auto results = initializeResults(); + select_best_results_folds(results, all_results, config.model); + // + // 3.2 Save the results + // + save(results); + } else { + // + // 2b. Consumers process the tasks and send the results to the producer + // + mpi_search_consumer(datasets, tasks, config, config_mpi, MPI_Result); + } + } + json GridExperiment::initializeResults() + { + // Load previous results if continue is set + json results; + if (config.continue_from != NO_CONTINUE()) { + if (!config.quiet) + std::cout << "* Loading previous results" << std::endl; + try { + std::ifstream file(Paths::grid_output(config.model)); + if (file.is_open()) { + results = json::parse(file); + results = results["results"]; + } + } + catch (const std::exception& e) { + std::cerr << "* There were no previous results" << std::endl; + std::cerr << "* Initizalizing new results" << std::endl; + results = json(); + } + } + return results; + } + void GridExperiment::save(json& results) + { + std::ofstream file(Paths::grid_output(config.model)); + json output = { + { "model", config.model }, + { "score", config.score }, + { "discretize", config.discretize }, + { "stratified", config.stratified }, + { "n_folds", config.n_folds }, + { "seeds", config.seeds }, + { "date", get_date() + " " + get_time()}, + { "nested", config.nested}, + { "platform", config.platform }, + { "duration", timer.getDurationString(true)}, + { "results", results } + + }; + file << output.dump(4); + } + +} /* namespace platform */ \ No newline at end of file diff --git a/src/grid/GridExperiment.h b/src/grid/GridExperiment.h new file mode 100644 index 0000000..6fb109b --- /dev/null +++ b/src/grid/GridExperiment.h @@ -0,0 +1,35 @@ +#ifndef GRIDEXPERIMENT_H +#define GRIDEXPERIMENT_H +#include +#include +#include +#include +#include "common/Datasets.h" +#include "common/Timer.h" +#include "main/HyperParameters.h" +#include "GridData.h" +#include "GridConfig.h" +#include "bayesnet/network/Network.h" + + +namespace platform { + using json = nlohmann::ordered_json; + class GridExperiment { + public: + explicit GridExperiment(struct ConfigGrid& config); + void go(struct ConfigMPI& config_mpi); + ~GridExperiment() = default; + json loadResults(); + static inline std::string NO_CONTINUE() { return "NO_CONTINUE"; } + private: + void save(json& results); + json initializeResults(); + std::vector filterDatasets(Datasets& datasets) const; + struct ConfigGrid config; + json build_tasks_mpi(int rank); + Timer timer; // used to measure the time of the whole process + const std::string separator = "|"; + bayesnet::Smoothing_t smooth_type{ bayesnet::Smoothing_t::NONE }; + }; +} /* namespace platform */ +#endif \ No newline at end of file diff --git a/src/grid/GridFunctions.cpp b/src/grid/GridFunctions.cpp new file mode 100644 index 0000000..ad0584a --- /dev/null +++ b/src/grid/GridFunctions.cpp @@ -0,0 +1,458 @@ + +#include +#include +#include +#include "main/Models.h" +#include "common/Paths.h" +#include "common/Colors.h" +#include "common/Utils.h" +namespace platform { + using json = nlohmann::ordered_json; + std::string get_color_rank(int rank) + { + auto colors = { Colors::WHITE(), Colors::RED(), Colors::GREEN(), Colors::BLUE(), Colors::MAGENTA(), Colors::CYAN(), Colors::YELLOW(), Colors::BLACK() }; + std::string id = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + auto idx = rank % id.size(); + return *(colors.begin() + rank % colors.size()) + id[idx]; + } + /* ************************************************************************************************************* + // + // MPI Experiment Functions + // + ************************************************************************************************************* */ + json mpi_experiment_producer(std::vector& names, json& tasks, struct ConfigMPI& config_mpi, MPI_Datatype& MPI_Result) + { + Task_Result result; + json results; + int num_tasks = tasks.size(); + + // + // 2a.1 Producer will loop to send all the tasks to the consumers and receive the results + // + for (int i = 0; i < num_tasks; ++i) { + MPI_Status status; + MPI_Recv(&result, 1, MPI_Result, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status); + if (status.MPI_TAG == TAG_RESULT) { + //Store result + store_search_result(names, result, results); + } + MPI_Send(&i, 1, MPI_INT, status.MPI_SOURCE, TAG_TASK, MPI_COMM_WORLD); + } + // + // 2a.2 Producer will send the end message to all the consumers + // + for (int i = 0; i < config_mpi.n_procs - 1; ++i) { + MPI_Status status; + MPI_Recv(&result, 1, MPI_Result, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status); + if (status.MPI_TAG == TAG_RESULT) { + //Store result + store_search_result(names, result, results); + } + MPI_Send(&i, 1, MPI_INT, status.MPI_SOURCE, TAG_END, MPI_COMM_WORLD); + } + return results; + } + void mpi_experiment_consumer(Datasets& datasets, json& tasks, struct ConfigGrid& config, struct ConfigMPI& config_mpi, MPI_Datatype& MPI_Result) + { + Task_Result result; + // + // 2b.1 Consumers announce to the producer that they are ready to receive a task + // + MPI_Send(&result, 1, MPI_Result, config_mpi.manager, TAG_QUERY, MPI_COMM_WORLD); + int task; + while (true) { + MPI_Status status; + // + // 2b.2 Consumers receive the task from the producer and process it + // + MPI_Recv(&task, 1, MPI_INT, config_mpi.manager, MPI_ANY_TAG, MPI_COMM_WORLD, &status); + if (status.MPI_TAG == TAG_END) { + break; + } + mpi_search_consumer_go(config, config_mpi, tasks, task, datasets, &result); + // + // 2b.3 Consumers send the result to the producer + // + MPI_Send(&result, 1, MPI_Result, config_mpi.manager, TAG_RESULT, MPI_COMM_WORLD); + } + } + void join_results_folds(json& results, json& all_results, std::string& model) + { + Timer timer; + auto grid = GridData(Paths::grid_input(model)); + // + // Select the best result of the computed outer folds + // + for (const auto& result : all_results.items()) { + // each result has the results of all the outer folds as each one were a different task + double best_score = 0.0; + json best; + for (const auto& result_fold : result.value()) { + double score = result_fold["score"].get(); + if (score > best_score) { + best_score = score; + best = result_fold; + } + } + auto dataset = result.key(); + auto combinations = grid.getGrid(dataset); + json json_best = { + { "score", best_score }, + { "hyperparameters", combinations[best["combination"].get()] }, + { "date", get_date() + " " + get_time() }, + { "grid", grid.getInputGrid(dataset) }, + { "duration", timer.translate2String(best["time"].get()) } + }; + results[dataset] = json_best; + } + } + json store_experiment_result(std::vector& names, Task_Result& result, json& results) + { + json json_result = { + { "score", result.score }, + { "combination", result.idx_combination }, + { "fold", result.n_fold }, + { "time", result.time }, + { "dataset", result.idx_dataset } + }; + auto name = names[result.idx_dataset]; + if (!results.contains(name)) { + results[name] = json::array(); + } + results[name].push_back(json_result); + return results; + } + void mpi_experiment_consumer_go(struct ConfigGrid& config, struct ConfigMPI& config_mpi, json& tasks, int n_task, Datasets& datasets, Task_Result* result) + { + // + // initialize + // + Timer timer; + timer.start(); + json task = tasks[n_task]; + auto model = config.model; + auto grid = GridData(Paths::grid_input(model)); + auto dataset_name = task["dataset"].get(); + auto idx_dataset = task["idx_dataset"].get(); + auto seed = task["seed"].get(); + auto n_fold = task["fold"].get(); + bool stratified = config.stratified; + bayesnet::Smoothing_t smooth; + if (config.smooth_strategy == "ORIGINAL") + smooth = bayesnet::Smoothing_t::ORIGINAL; + else if (config.smooth_strategy == "LAPLACE") + smooth = bayesnet::Smoothing_t::LAPLACE; + else if (config.smooth_strategy == "CESTNIK") + smooth = bayesnet::Smoothing_t::CESTNIK; + // + // Generate the hyperparameters combinations + // + auto& dataset = datasets.getDataset(dataset_name); + auto combinations = grid.getGrid(dataset_name); + dataset.load(); + auto [X, y] = dataset.getTensors(); + auto features = dataset.getFeatures(); + auto className = dataset.getClassName(); + // + // Start working on task + // + folding::Fold* fold; + if (stratified) + fold = new folding::StratifiedKFold(config.n_folds, y, seed); + else + fold = new folding::KFold(config.n_folds, y.size(0), seed); + auto [train, test] = fold->getFold(n_fold); + auto [X_train, X_test, y_train, y_test] = dataset.getTrainTestTensors(train, test); + auto states = dataset.getStates(); // Get the states of the features Once they are discretized + float best_fold_score = 0.0; + int best_idx_combination = -1; + json best_fold_hyper; + for (int idx_combination = 0; idx_combination < combinations.size(); ++idx_combination) { + auto hyperparam_line = combinations[idx_combination]; + auto hyperparameters = platform::HyperParameters(datasets.getNames(), hyperparam_line); + folding::Fold* nested_fold; + if (config.stratified) + nested_fold = new folding::StratifiedKFold(config.nested, y_train, seed); + else + nested_fold = new folding::KFold(config.nested, y_train.size(0), seed); + double score = 0.0; + for (int n_nested_fold = 0; n_nested_fold < config.nested; n_nested_fold++) { + // + // Nested level fold + // + auto [train_nested, test_nested] = nested_fold->getFold(n_nested_fold); + auto train_nested_t = torch::tensor(train_nested); + auto test_nested_t = torch::tensor(test_nested); + auto X_nested_train = X_train.index({ "...", train_nested_t }); + auto y_nested_train = y_train.index({ train_nested_t }); + auto X_nested_test = X_train.index({ "...", test_nested_t }); + auto y_nested_test = y_train.index({ test_nested_t }); + // + // Build Classifier with selected hyperparameters + // + auto clf = Models::instance()->create(config.model); + auto valid = clf->getValidHyperparameters(); + hyperparameters.check(valid, dataset_name); + clf->setHyperparameters(hyperparameters.get(dataset_name)); + // + // Train model + // + clf->fit(X_nested_train, y_nested_train, features, className, states, smooth); + // + // Test model + // + score += clf->score(X_nested_test, y_nested_test); + } + delete nested_fold; + score /= config.nested; + if (score > best_fold_score) { + best_fold_score = score; + best_idx_combination = idx_combination; + best_fold_hyper = hyperparam_line; + } + } + delete fold; + // + // Build Classifier with the best hyperparameters to obtain the best score + // + auto hyperparameters = platform::HyperParameters(datasets.getNames(), best_fold_hyper); + auto clf = Models::instance()->create(config.model); + auto valid = clf->getValidHyperparameters(); + hyperparameters.check(valid, dataset_name); + clf->setHyperparameters(best_fold_hyper); + clf->fit(X_train, y_train, features, className, states, smooth); + best_fold_score = clf->score(X_test, y_test); + // + // Return the result + // + result->idx_dataset = task["idx_dataset"].get(); + result->idx_combination = best_idx_combination; + result->score = best_fold_score; + result->n_fold = n_fold; + result->time = timer.getDuration(); + // + // Update progress bar + // + std::cout << get_color_rank(config_mpi.rank) << std::flush; + } + /* ************************************************************************************************************* + // + // MPI Search Functions + // + ************************************************************************************************************* */ + json mpi_search_producer(std::vector& names, json& tasks, struct ConfigMPI& config_mpi, MPI_Datatype& MPI_Result) + { + Task_Result result; + json results; + int num_tasks = tasks.size(); + + // + // 2a.1 Producer will loop to send all the tasks to the consumers and receive the results + // + for (int i = 0; i < num_tasks; ++i) { + MPI_Status status; + MPI_Recv(&result, 1, MPI_Result, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status); + if (status.MPI_TAG == TAG_RESULT) { + //Store result + store_search_result(names, result, results); + } + MPI_Send(&i, 1, MPI_INT, status.MPI_SOURCE, TAG_TASK, MPI_COMM_WORLD); + } + // + // 2a.2 Producer will send the end message to all the consumers + // + for (int i = 0; i < config_mpi.n_procs - 1; ++i) { + MPI_Status status; + MPI_Recv(&result, 1, MPI_Result, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status); + if (status.MPI_TAG == TAG_RESULT) { + //Store result + store_search_result(names, result, results); + } + MPI_Send(&i, 1, MPI_INT, status.MPI_SOURCE, TAG_END, MPI_COMM_WORLD); + } + return results; + } + void mpi_search_consumer(Datasets& datasets, json& tasks, struct ConfigGrid& config, struct ConfigMPI& config_mpi, MPI_Datatype& MPI_Result) + { + Task_Result result; + // + // 2b.1 Consumers announce to the producer that they are ready to receive a task + // + MPI_Send(&result, 1, MPI_Result, config_mpi.manager, TAG_QUERY, MPI_COMM_WORLD); + int task; + while (true) { + MPI_Status status; + // + // 2b.2 Consumers receive the task from the producer and process it + // + MPI_Recv(&task, 1, MPI_INT, config_mpi.manager, MPI_ANY_TAG, MPI_COMM_WORLD, &status); + if (status.MPI_TAG == TAG_END) { + break; + } + mpi_experiment_consumer_go(config, config_mpi, tasks, task, datasets, &result); + // + // 2b.3 Consumers send the result to the producer + // + MPI_Send(&result, 1, MPI_Result, config_mpi.manager, TAG_RESULT, MPI_COMM_WORLD); + } + } + void select_best_results_folds(json& results, json& all_results, std::string& model) + { + Timer timer; + auto grid = GridData(Paths::grid_input(model)); + // + // Select the best result of the computed outer folds + // + for (const auto& result : all_results.items()) { + // each result has the results of all the outer folds as each one were a different task + double best_score = 0.0; + json best; + for (const auto& result_fold : result.value()) { + double score = result_fold["score"].get(); + if (score > best_score) { + best_score = score; + best = result_fold; + } + } + auto dataset = result.key(); + auto combinations = grid.getGrid(dataset); + json json_best = { + { "score", best_score }, + { "hyperparameters", combinations[best["combination"].get()] }, + { "date", get_date() + " " + get_time() }, + { "grid", grid.getInputGrid(dataset) }, + { "duration", timer.translate2String(best["time"].get()) } + }; + results[dataset] = json_best; + } + } + json store_search_result(std::vector& names, Task_Result& result, json& results) + { + json json_result = { + { "score", result.score }, + { "combination", result.idx_combination }, + { "fold", result.n_fold }, + { "time", result.time }, + { "dataset", result.idx_dataset } + }; + auto name = names[result.idx_dataset]; + if (!results.contains(name)) { + results[name] = json::array(); + } + results[name].push_back(json_result); + return results; + } + void mpi_experiment_consumer_go(struct ConfigGrid& config, struct ConfigMPI& config_mpi, json& tasks, int n_task, Datasets& datasets, Task_Result* result) + { + // + // initialize + // + Timer timer; + timer.start(); + json task = tasks[n_task]; + auto model = config.model; + auto grid = GridData(Paths::grid_input(model)); + auto dataset_name = task["dataset"].get(); + auto idx_dataset = task["idx_dataset"].get(); + auto seed = task["seed"].get(); + auto n_fold = task["fold"].get(); + bool stratified = config.stratified; + bayesnet::Smoothing_t smooth; + if (config.smooth_strategy == "ORIGINAL") + smooth = bayesnet::Smoothing_t::ORIGINAL; + else if (config.smooth_strategy == "LAPLACE") + smooth = bayesnet::Smoothing_t::LAPLACE; + else if (config.smooth_strategy == "CESTNIK") + smooth = bayesnet::Smoothing_t::CESTNIK; + // + // Generate the hyperparameters combinations + // + auto& dataset = datasets.getDataset(dataset_name); + auto combinations = grid.getGrid(dataset_name); + dataset.load(); + auto [X, y] = dataset.getTensors(); + auto features = dataset.getFeatures(); + auto className = dataset.getClassName(); + // + // Start working on task + // + folding::Fold* fold; + if (stratified) + fold = new folding::StratifiedKFold(config.n_folds, y, seed); + else + fold = new folding::KFold(config.n_folds, y.size(0), seed); + auto [train, test] = fold->getFold(n_fold); + auto [X_train, X_test, y_train, y_test] = dataset.getTrainTestTensors(train, test); + auto states = dataset.getStates(); // Get the states of the features Once they are discretized + float best_fold_score = 0.0; + int best_idx_combination = -1; + json best_fold_hyper; + for (int idx_combination = 0; idx_combination < combinations.size(); ++idx_combination) { + auto hyperparam_line = combinations[idx_combination]; + auto hyperparameters = platform::HyperParameters(datasets.getNames(), hyperparam_line); + folding::Fold* nested_fold; + if (config.stratified) + nested_fold = new folding::StratifiedKFold(config.nested, y_train, seed); + else + nested_fold = new folding::KFold(config.nested, y_train.size(0), seed); + double score = 0.0; + for (int n_nested_fold = 0; n_nested_fold < config.nested; n_nested_fold++) { + // + // Nested level fold + // + auto [train_nested, test_nested] = nested_fold->getFold(n_nested_fold); + auto train_nested_t = torch::tensor(train_nested); + auto test_nested_t = torch::tensor(test_nested); + auto X_nested_train = X_train.index({ "...", train_nested_t }); + auto y_nested_train = y_train.index({ train_nested_t }); + auto X_nested_test = X_train.index({ "...", test_nested_t }); + auto y_nested_test = y_train.index({ test_nested_t }); + // + // Build Classifier with selected hyperparameters + // + auto clf = Models::instance()->create(config.model); + auto valid = clf->getValidHyperparameters(); + hyperparameters.check(valid, dataset_name); + clf->setHyperparameters(hyperparameters.get(dataset_name)); + // + // Train model + // + clf->fit(X_nested_train, y_nested_train, features, className, states, smooth); + // + // Test model + // + score += clf->score(X_nested_test, y_nested_test); + } + delete nested_fold; + score /= config.nested; + if (score > best_fold_score) { + best_fold_score = score; + best_idx_combination = idx_combination; + best_fold_hyper = hyperparam_line; + } + } + delete fold; + // + // Build Classifier with the best hyperparameters to obtain the best score + // + auto hyperparameters = platform::HyperParameters(datasets.getNames(), best_fold_hyper); + auto clf = Models::instance()->create(config.model); + auto valid = clf->getValidHyperparameters(); + hyperparameters.check(valid, dataset_name); + clf->setHyperparameters(best_fold_hyper); + clf->fit(X_train, y_train, features, className, states, smooth); + best_fold_score = clf->score(X_test, y_test); + // + // Return the result + // + result->idx_dataset = task["idx_dataset"].get(); + result->idx_combination = best_idx_combination; + result->score = best_fold_score; + result->n_fold = n_fold; + result->time = timer.getDuration(); + // + // Update progress bar + // + std::cout << get_color_rank(config_mpi.rank) << std::flush; + } +} \ No newline at end of file diff --git a/src/grid/GridSearch.cpp b/src/grid/GridSearch.cpp index d3a8972..886a11d 100644 --- a/src/grid/GridSearch.cpp +++ b/src/grid/GridSearch.cpp @@ -9,14 +9,6 @@ #include "GridSearch.h" namespace platform { - - std::string get_color_rank(int rank) - { - auto colors = { Colors::WHITE(), Colors::RED(), Colors::GREEN(), Colors::BLUE(), Colors::MAGENTA(), Colors::CYAN(), Colors::YELLOW(), Colors::BLACK() }; - std::string id = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - auto idx = rank % id.size(); - return *(colors.begin() + rank % colors.size()) + id[idx]; - } GridSearch::GridSearch(struct ConfigGrid& config) : config(config) { if (config.smooth_strategy == "ORIGINAL") @@ -107,221 +99,6 @@ namespace platform { std::cout << separator << std::endl << separator << std::flush; return tasks; } - void process_task_mpi_consumer(struct ConfigGrid& config, struct ConfigMPI& config_mpi, json& tasks, int n_task, Datasets& datasets, Task_Result* result) - { - // - // initialize - // - Timer timer; - timer.start(); - json task = tasks[n_task]; - auto model = config.model; - auto grid = GridData(Paths::grid_input(model)); - auto dataset_name = task["dataset"].get(); - auto idx_dataset = task["idx_dataset"].get(); - auto seed = task["seed"].get(); - auto n_fold = task["fold"].get(); - bool stratified = config.stratified; - bayesnet::Smoothing_t smooth; - if (config.smooth_strategy == "ORIGINAL") - smooth = bayesnet::Smoothing_t::ORIGINAL; - else if (config.smooth_strategy == "LAPLACE") - smooth = bayesnet::Smoothing_t::LAPLACE; - else if (config.smooth_strategy == "CESTNIK") - smooth = bayesnet::Smoothing_t::CESTNIK; - // - // Generate the hyperparameters combinations - // - auto& dataset = datasets.getDataset(dataset_name); - auto combinations = grid.getGrid(dataset_name); - dataset.load(); - auto [X, y] = dataset.getTensors(); - auto features = dataset.getFeatures(); - auto className = dataset.getClassName(); - // - // Start working on task - // - folding::Fold* fold; - if (stratified) - fold = new folding::StratifiedKFold(config.n_folds, y, seed); - else - fold = new folding::KFold(config.n_folds, y.size(0), seed); - auto [train, test] = fold->getFold(n_fold); - auto [X_train, X_test, y_train, y_test] = dataset.getTrainTestTensors(train, test); - auto states = dataset.getStates(); // Get the states of the features Once they are discretized - float best_fold_score = 0.0; - int best_idx_combination = -1; - json best_fold_hyper; - for (int idx_combination = 0; idx_combination < combinations.size(); ++idx_combination) { - auto hyperparam_line = combinations[idx_combination]; - auto hyperparameters = platform::HyperParameters(datasets.getNames(), hyperparam_line); - folding::Fold* nested_fold; - if (config.stratified) - nested_fold = new folding::StratifiedKFold(config.nested, y_train, seed); - else - nested_fold = new folding::KFold(config.nested, y_train.size(0), seed); - double score = 0.0; - for (int n_nested_fold = 0; n_nested_fold < config.nested; n_nested_fold++) { - // - // Nested level fold - // - auto [train_nested, test_nested] = nested_fold->getFold(n_nested_fold); - auto train_nested_t = torch::tensor(train_nested); - auto test_nested_t = torch::tensor(test_nested); - auto X_nested_train = X_train.index({ "...", train_nested_t }); - auto y_nested_train = y_train.index({ train_nested_t }); - auto X_nested_test = X_train.index({ "...", test_nested_t }); - auto y_nested_test = y_train.index({ test_nested_t }); - // - // Build Classifier with selected hyperparameters - // - auto clf = Models::instance()->create(config.model); - auto valid = clf->getValidHyperparameters(); - hyperparameters.check(valid, dataset_name); - clf->setHyperparameters(hyperparameters.get(dataset_name)); - // - // Train model - // - clf->fit(X_nested_train, y_nested_train, features, className, states, smooth); - // - // Test model - // - score += clf->score(X_nested_test, y_nested_test); - } - delete nested_fold; - score /= config.nested; - if (score > best_fold_score) { - best_fold_score = score; - best_idx_combination = idx_combination; - best_fold_hyper = hyperparam_line; - } - } - delete fold; - // - // Build Classifier with the best hyperparameters to obtain the best score - // - auto hyperparameters = platform::HyperParameters(datasets.getNames(), best_fold_hyper); - auto clf = Models::instance()->create(config.model); - auto valid = clf->getValidHyperparameters(); - hyperparameters.check(valid, dataset_name); - clf->setHyperparameters(best_fold_hyper); - clf->fit(X_train, y_train, features, className, states, smooth); - best_fold_score = clf->score(X_test, y_test); - // - // Return the result - // - result->idx_dataset = task["idx_dataset"].get(); - result->idx_combination = best_idx_combination; - result->score = best_fold_score; - result->n_fold = n_fold; - result->time = timer.getDuration(); - // - // Update progress bar - // - std::cout << get_color_rank(config_mpi.rank) << std::flush; - } - json store_result(std::vector& names, Task_Result& result, json& results) - { - json json_result = { - { "score", result.score }, - { "combination", result.idx_combination }, - { "fold", result.n_fold }, - { "time", result.time }, - { "dataset", result.idx_dataset } - }; - auto name = names[result.idx_dataset]; - if (!results.contains(name)) { - results[name] = json::array(); - } - results[name].push_back(json_result); - return results; - } - json producer(std::vector& names, json& tasks, struct ConfigMPI& config_mpi, MPI_Datatype& MPI_Result) - { - Task_Result result; - json results; - int num_tasks = tasks.size(); - - // - // 2a.1 Producer will loop to send all the tasks to the consumers and receive the results - // - for (int i = 0; i < num_tasks; ++i) { - MPI_Status status; - MPI_Recv(&result, 1, MPI_Result, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status); - if (status.MPI_TAG == TAG_RESULT) { - //Store result - store_result(names, result, results); - } - MPI_Send(&i, 1, MPI_INT, status.MPI_SOURCE, TAG_TASK, MPI_COMM_WORLD); - } - // - // 2a.2 Producer will send the end message to all the consumers - // - for (int i = 0; i < config_mpi.n_procs - 1; ++i) { - MPI_Status status; - MPI_Recv(&result, 1, MPI_Result, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status); - if (status.MPI_TAG == TAG_RESULT) { - //Store result - store_result(names, result, results); - } - MPI_Send(&i, 1, MPI_INT, status.MPI_SOURCE, TAG_END, MPI_COMM_WORLD); - } - return results; - } - void select_best_results_folds(json& results, json& all_results, std::string& model) - { - Timer timer; - auto grid = GridData(Paths::grid_input(model)); - // - // Select the best result of the computed outer folds - // - for (const auto& result : all_results.items()) { - // each result has the results of all the outer folds as each one were a different task - double best_score = 0.0; - json best; - for (const auto& result_fold : result.value()) { - double score = result_fold["score"].get(); - if (score > best_score) { - best_score = score; - best = result_fold; - } - } - auto dataset = result.key(); - auto combinations = grid.getGrid(dataset); - json json_best = { - { "score", best_score }, - { "hyperparameters", combinations[best["combination"].get()] }, - { "date", get_date() + " " + get_time() }, - { "grid", grid.getInputGrid(dataset) }, - { "duration", timer.translate2String(best["time"].get()) } - }; - results[dataset] = json_best; - } - } - void consumer(Datasets& datasets, json& tasks, struct ConfigGrid& config, struct ConfigMPI& config_mpi, MPI_Datatype& MPI_Result) - { - Task_Result result; - // - // 2b.1 Consumers announce to the producer that they are ready to receive a task - // - MPI_Send(&result, 1, MPI_Result, config_mpi.manager, TAG_QUERY, MPI_COMM_WORLD); - int task; - while (true) { - MPI_Status status; - // - // 2b.2 Consumers receive the task from the producer and process it - // - MPI_Recv(&task, 1, MPI_INT, config_mpi.manager, MPI_ANY_TAG, MPI_COMM_WORLD, &status); - if (status.MPI_TAG == TAG_END) { - break; - } - process_task_mpi_consumer(config, config_mpi, tasks, task, datasets, &result); - // - // 2b.3 Consumers send the result to the producer - // - MPI_Send(&result, 1, MPI_Result, config_mpi.manager, TAG_RESULT, MPI_COMM_WORLD); - } - } void GridSearch::go(struct ConfigMPI& config_mpi) { /* @@ -402,7 +179,7 @@ namespace platform { // 2a. Producer delivers the tasks to the consumers // auto datasets_names = filterDatasets(datasets); - json all_results = producer(datasets_names, tasks, config_mpi, MPI_Result); + json all_results = mpi_search_producer(datasets_names, tasks, config_mpi, MPI_Result); std::cout << separator << std::endl; // // 3. Manager select the bests sccores for each dataset @@ -417,7 +194,7 @@ namespace platform { // // 2b. Consumers process the tasks and send the results to the producer // - consumer(datasets, tasks, config, config_mpi, MPI_Result); + mpi_search_consumer(datasets, tasks, config, config_mpi, MPI_Result); } } json GridSearch::initializeResults() diff --git a/src/grid/GridSearch.h b/src/grid/GridSearch.h index 3aedd44..8504f69 100644 --- a/src/grid/GridSearch.h +++ b/src/grid/GridSearch.h @@ -8,42 +8,12 @@ #include "common/Timer.h" #include "main/HyperParameters.h" #include "GridData.h" +#include "GridConfig.h" #include "bayesnet/network/Network.h" namespace platform { using json = nlohmann::ordered_json; - struct ConfigGrid { - std::string model; - std::string score; - std::string continue_from; - std::string platform; - std::string smooth_strategy; - bool quiet; - bool only; // used with continue_from to only compute that dataset - bool discretize; - bool stratified; - int nested; - int n_folds; - json excluded; - std::vector seeds; - }; - struct ConfigMPI { - int rank; - int n_procs; - int manager; - }; - typedef struct { - uint idx_dataset; - uint idx_combination; - int n_fold; - double score; - double time; - } Task_Result; - const int TAG_QUERY = 1; - const int TAG_RESULT = 2; - const int TAG_TASK = 3; - const int TAG_END = 4; class GridSearch { public: explicit GridSearch(struct ConfigGrid& config);