예제 #1
0
    def __init__(self, experiment_params):
        """
        A class that can interact with the ESP service
        :param experiment_params: the experiment parameters dictionary
        """
        self.experiment_params = experiment_params
        self.extension_packaging = ExtensionPackaging()
        self.experiment_params_as_bytes = self.extension_packaging.to_extension_bytes(
            experiment_params)
        self.experiment_id = experiment_params["LEAF"]["experiment_id"]
        self.version = experiment_params["LEAF"]["version"]

        # gRPC connection
        esp_host = experiment_params["LEAF"]["esp_host"]
        esp_port = experiment_params["LEAF"]["esp_port"]
        grpc_options = experiment_params["LEAF"].get(
            "grpc_options", DEFAULT_GRPC_OPTIONS).items()
        print("ESP service: {}:{}".format(esp_host, esp_port))
        print("gRPC options:")
        for pair in grpc_options:
            print("  {}: {}".format(pair[0], pair[1]))
        channel = grpc.insecure_channel('{}:{}'.format(esp_host, esp_port),
                                        options=grpc_options)
        print("Ready to connect.")
        self.esp_service = PopulationServiceStub(channel)
예제 #2
0
 def __init__(self, experiment_params, evaluator):
     self.experiment_params = experiment_params
     self.extension_packaging = ExtensionPackaging()
     self.save_to_dir = self._generate_persistence_directory()
     # Possible values are all, elites, best, none
     self.candidates_to_persist = self.experiment_params["LEAF"].get("candidates_to_persist", "best").lower()
     if self.candidates_to_persist not in ["all", "elites", "best", "none"]:
         raise ValueError("Unknown value for experiment param [LEAF][candidates_to_persist]: {}".format(
             self.candidates_to_persist))
     self.persist_experiment_params(experiment_params)
     self.evaluator = evaluator
예제 #3
0
class EspEvaluator(ABC):
    """
    An abstract class to evaluate ESP populations.
    """
    def __init__(self):
        self._extension_packaging = ExtensionPackaging()

    @abstractmethod
    def evaluate_candidate(self, candidate):
        """
        Evaluates a single Keras model.
        :param candidate: a Keras model
        :return: a dictionary of metrics
        """
        pass

    @abstractmethod
    def evaluate_population(self, population_response):
        """
        Evaluates the candidates in an ESP PopulationResponse
        and updates the PopulationResponse with the candidates fitness.
        :param population_response: an ESP PopulationResponse
        :return: nothing. A dictionary containing the metrics is assigned to the candidates metrics as a UTF-8
        encoded string (bytes) within the passed response
        """
        pass

    def _encode_metrics(self, metrics):
        if not isinstance(metrics, dict):
            # For backward compatibility: if the evaluation didn't return a dictionary,
            # convert the returned value to a float and put it in a dictionary.
            # metric might be a numpy object, like numpy.int64 or numpy.float32. Convert it to float.
            score = float(metrics)
            # Create a dictionary
            metrics = {"score": score}
        encoded_metrics = self._extension_packaging.to_extension_bytes(metrics)
        return encoded_metrics

    def _decode_metrics(self, encoded_metrics):
        metrics = self._extension_packaging.from_extension_bytes(
            encoded_metrics)
        return metrics
예제 #4
0
 def __init__(self):
     self._extension_packaging = ExtensionPackaging()
예제 #5
0
class EspPersistor:
    """
    A class to persist any kind of information from an experiment.
    """

    def __init__(self, experiment_params, evaluator):
        self.experiment_params = experiment_params
        self.extension_packaging = ExtensionPackaging()
        self.save_to_dir = self._generate_persistence_directory()
        # Possible values are all, elites, best, none
        self.candidates_to_persist = self.experiment_params["LEAF"].get("candidates_to_persist", "best").lower()
        if self.candidates_to_persist not in ["all", "elites", "best", "none"]:
            raise ValueError("Unknown value for experiment param [LEAF][candidates_to_persist]: {}".format(
                self.candidates_to_persist))
        self.persist_experiment_params(experiment_params)
        self.evaluator = evaluator

    def _generate_persistence_directory(self):
        timestamp = time.strftime("%Y%m%d-%H%M%S")
        persistence_dir = self.experiment_params["LEAF"]["persistence_dir"]
        experiment_id = self.experiment_params["LEAF"]["experiment_id"]
        dirname = os.path.join(persistence_dir, experiment_id)
        version = self.experiment_params["LEAF"]["version"]
        version = version + "_" + timestamp
        dirname = os.path.join(dirname, version)
        os.makedirs(dirname, exist_ok=True)
        return dirname

    def get_persistence_directory(self):
        """
        Returns the name of the directory used for persistence.
        :return: a string
        """
        return self.save_to_dir

    def persist_experiment_params(self, experiment_params):
        """
        Persists the passed experiment parameters.
        :param experiment_params: the experiment parameters to persist
        :return: nothing. Saves a file called `experiment_params.json` to the persistence directory
        """
        filename = os.path.join(self.save_to_dir, 'experiment_params.json')
        with open(filename, 'w') as f:
            f.write(json.dumps(experiment_params, indent=4))

    def persist_response(self, response):
        """
        Persists a generation's information.
        :param response: an evaluated ESP PopulationResponse
        :return: nothing. Saves files to the persistence directory
        """
        gen = response.generation_count
        checkpoint_id = response.checkpoint_id
        candidates_info = self.persist_generation(response)
        self.persist_stats(candidates_info, gen, checkpoint_id)
        self.persist_candidates(candidates_info, gen)
        stats_file = os.path.join(self.save_to_dir, 'experiment_stats.csv')
        title = self.experiment_params["LEAF"]["experiment_id"]
        EspPlotter.plot_stats(stats_file, title)

    def persist_stats(self, candidates_info, generation, checkpoint_id):
        """
        Collects statistics for the passed generation of candidates.
        :param candidates_info: the candidates information
        :param generation: the generation these candidates belong to
        :param checkpoint_id: the checkpoint id corresponding to this generation
        :return: nothing. Saves a file called `experiment_stats.csv` to the persistence directory
        """
        filename = os.path.join(self.save_to_dir, 'experiment_stats.csv')
        file_exists = os.path.exists(filename)

        metrics_stats = {}
        for metric_name in candidates_info[0]["metrics"].keys():
            metric_values = [candidate["metrics"][metric_name] for candidate in candidates_info]
            metrics_stats["max_" + metric_name] = max(metric_values)
            metrics_stats["min_" + metric_name] = min(metric_values)
            metrics_stats["mean_" + metric_name] = statistics.mean(metric_values)
            candidates_info.sort(key=lambda k: k["metrics"][metric_name], reverse=False)
            metrics_stats["cid_min_" + metric_name] = candidates_info[0]["id"]
            metrics_stats["cid_max_" + metric_name] = candidates_info[-1]["id"]

        # 'a+' Opens the file for appending; any data written to the file is automatically added to the end.
        # The file is created if it does not exist.
        with open(filename, 'a+') as stats_file:
            writer = csv.writer(stats_file)
            if not file_exists:
                headers = ["generation", "checkpoint_id"]
                headers.extend(metrics_stats.keys())
                writer.writerow(headers)
            generation_stats = [generation, checkpoint_id]
            generation_stats.extend(metrics_stats.values())
            writer.writerow(generation_stats)

    def persist_generation(self, response):
        """
        Persists the details of a generation to a file.
        :param response: an evaluated ESP PopulationResponse
        :return: nothing. Saves a file called `gen.csv` to the persistence directory (e.g. 1.csv for generation 1)
        """
        gen = response.generation_count
        gen_filename = os.path.join(self.save_to_dir, str(gen) + '.csv')
        # 'w' to truncate the file if it already exists
        candidates_info = []
        with open(gen_filename, 'w') as stats_file:
            writer = csv.writer(stats_file)
            write_header = True
            for candidate in response.population:
                # Candidate's details
                cid = candidate.id
                identity = candidate.identity.decode('UTF-8')
                metrics = self.extension_packaging.from_extension_bytes(candidate.metrics)
                c = {"id": cid,
                     "identity": identity,
                     "metrics": metrics,
                     "model": candidate.interpretation}
                candidates_info.append(c)

                # Write the header if needed
                if write_header:
                    # Unpack the metric names list
                    writer.writerow(["cid", "identity", *metrics.keys()])
                    write_header = False

                # Write a row for this candidate
                row_values = [cid, identity]
                row_values.extend(metrics.values())
                writer.writerow(row_values)
        return candidates_info

    def persist_candidates(self, candidates_info, gen):
        """
        Persists the candidates in the response's population according to the experiment params.
        Can be "all", "elites", "best", "none"
        :param candidates_info: a PopulationResponse containing evaluated candidates
        :param gen: the generation these candidates belong to
        :return: nothing. Saves the candidates to a generation folder in the persistence directory
        """
        if self.candidates_to_persist == "none":
            return

        gen_folder = os.path.join(self.save_to_dir, str(gen))
        os.makedirs(gen_folder, exist_ok=True)
        if self.candidates_to_persist == "all":
            for candidate in candidates_info:
                self.persist_candidate(candidate, gen_folder)
        elif self.candidates_to_persist == "best":
            # Save the best candidate, per objective
            objectives = self.experiment_params['evolution'].get("fitness", DEFAULT_FITNESS)
            for objective in objectives:
                # Sort the candidates to figure out the best one for this objective
                metric_name = objective["metric_name"]
                candidates_info.sort(key=lambda k: k["metrics"][metric_name],
                                     reverse=objective["maximize"])
                # The best candidate for this objective is the first one
                self.persist_candidate(candidates_info[0], gen_folder)
        elif self.candidates_to_persist == "elites":
            # Save the elites, according to the first objective only
            nb_elites = self.experiment_params["evolution"]["nb_elites"]
            objectives = self.experiment_params['evolution'].get("fitness", DEFAULT_FITNESS)
            objective = objectives[0]
            metric_name = objective["metric_name"]
            candidates_info.sort(key=lambda k: k["metrics"][metric_name],
                                 reverse=objective["maximize"])
            for candidate in candidates_info[len(candidates_info) - nb_elites:]:
                self.persist_candidate(candidate, gen_folder)
        else:
            print("Skipping candidates persistence: unknown candidates_to_persist attribute: {}".format(
                self.candidates_to_persist))

    def persist_candidate(self, candidate, gen_folder):
        """
        Persists a candidates to a file
        :param candidate: the candidates to persist
        :param gen_folder: the folder to which to persist it
        :return: nothing. Saves the candidate to a cid.h5 file in generation folder in the persistence directory
        (where cid is the candidate id)
        """
        cid = candidate["id"]
        filename = cid + ".h5"
        filename = os.path.join(gen_folder, filename)
        representation = self.experiment_params["LEAF"]["representation"]
        if representation == "KerasNN":
            self.persist_keras_nn_model(candidate["model"], filename)
        elif representation == "NNWeights":
            self.persist_nn_weights_model(candidate["model"], filename)
        else:
            print("Persistor: Unknown representation: {}".format(representation))

    @staticmethod
    def persist_keras_nn_model(model_bytes, filename):
        """
        Converts the passed bytes to a Keras model and saves it to a file
        :param model_bytes: the bytes corresponding to a Keras model
        :param filename: the name of the file to save to
        :return: nothing
        """
        # Convert the received bytes to a Keras model
        import io
        from keras.models import load_model
        model_file = io.BytesIO(model_bytes)
        keras_model = load_model(model_file)
        # Save the model, without the optimizer (not used)
        keras_model.save(filename, include_optimizer=False)

    def persist_nn_weights_model(self, weights_bytes, filename):
        """
        Creates a model from the passed weight bytes and saves it to a file
        :param weights_bytes: the bytes corresponding to a Keras model weights
        :param filename: the name of the file to save to
        :return: nothing
        """
        indy_weights = pickle.loads(weights_bytes)
        model = self.evaluator.get_keras_model(indy_weights)
        # Save the model, without the optimizer (not used)
        model.save(filename, include_optimizer=False)
예제 #6
0
 def setUp(self):
     # Executed before each test
     self.extension_packaging = ExtensionPackaging()
     with open(EXPERIMENT_JSON) as json_data:
         self.experiment_params = json.load(json_data)
예제 #7
0
class TestEspNNWeightsEvaluator(unittest.TestCase):
    def setUp(self):
        # Executed before each test
        self.extension_packaging = ExtensionPackaging()
        with open(EXPERIMENT_JSON) as json_data:
            self.experiment_params = json.load(json_data)

    # Mock where the class is used, i.e. in esp_evaluator
    @patch('esp_sdk.v1_0.esp_evaluator.EspService', autospec=True)
    def test_constructor(self, esp_service_mock):
        evaluator = EspNNWeightsEvaluatorForTests(self.experiment_params)
        self.assertIsNotNone(evaluator)
        # Make sure we called the ESP service mock to get a base model
        self.assertEqual(
            1, esp_service_mock.return_value.request_base_model.call_count)

    @patch('esp_sdk.v1_0.esp_evaluator.EspService', autospec=True)
    def test_evaluate_population(self, esp_service_mock):
        # If nothing is specified in the experiment_params, re-evaluate elites
        self._evaluate_population(esp_service_mock, reevaluate_elites=True)

    @patch('esp_sdk.v1_0.esp_evaluator.EspService', autospec=True)
    def test_reevaluate_elites(self, esp_service_mock):
        # Reevaluate elites
        self.experiment_params["LEAF"]["reevaluate_elites"] = True
        self._evaluate_population(esp_service_mock, reevaluate_elites=True)

    @patch('esp_sdk.v1_0.esp_evaluator.EspService', autospec=True)
    def test_do_not_reevaluate_elites(self, esp_service_mock):
        # Do NOT reevaluate elites
        self.experiment_params["LEAF"]["reevaluate_elites"] = False
        self._evaluate_population(esp_service_mock, reevaluate_elites=False)

    def _evaluate_population(self, esp_service_mock, reevaluate_elites):
        evaluator = EspNNWeightsEvaluatorForTests(self.experiment_params)
        # Make sure we called the ESP service mock to get a base model
        self.assertEqual(
            1, esp_service_mock.return_value.request_base_model.call_count)

        # Create a population
        response = self._create_population_response()
        # And evaluate it
        evaluator.evaluate_population(response)

        # Check c1
        c = response.population[0]
        metrics_json = self.extension_packaging.from_extension_bytes(c.metrics)
        score = metrics_json['score']
        if reevaluate_elites:
            # This candidate is an elite: we want to make sure it has been re-evaluated and it's score
            # is the re-evaluated score, not the elite score.
            self.assertEqual(
                C1_SCORE, score,
                "This elite candidate should have been re-evaluated")
        else:
            # This candidate is an elite, and we make sure we have NOT re-evaluated it
            self.assertEqual(
                E1_SCORE, score,
                "This elite candidate should still have its elite score")

        # Check c2
        c = response.population[1]
        score_json = self.extension_packaging.from_extension_bytes(c.metrics)
        score = score_json['score']
        self.assertEqual(C2_SCORE, score)

    def _create_population_response(self):
        population = []

        # elite
        c1 = Candidate()
        c1.id = "1_1"
        c1.interpretation = pickle.dumps(C1_MODEL)
        # This is an elite: it has already been evaluated anc already contains a score
        c1.metrics = self.extension_packaging.to_extension_bytes(
            {"score": E1_SCORE})
        c1.identity = pickle.dumps("C1 identity")
        population.append(c1)

        # new candidate
        c2 = Candidate()
        c2.id = "2_1"
        c2.interpretation = pickle.dumps(C2_MODEL)
        # c2.metrics = self.extension_packaging.to_extension_bytes(None)
        c2.identity = pickle.dumps("C2 identity")
        population.append(c2)

        response = PopulationResponse(population=population)
        return response

    @patch('esp_sdk.v1_0.esp_evaluator.EspService', autospec=True)
    def test_evaluate_population_multi_metrics(self, esp_service_mock):
        evaluator = EspNNWeightsMultiMetricsEvaluatorForTests(
            self.experiment_params)
        # Make sure we called the ESP service mock to get a base model
        self.assertEqual(
            1, esp_service_mock.return_value.request_base_model.call_count)

        # Create a population
        response = self._create_population_response()
        # And evaluate it
        evaluator.evaluate_population(response)

        # Check c1
        c = response.population[0]
        metrics_json = self.extension_packaging.from_extension_bytes(c.metrics)
        score = metrics_json['score']
        self.assertEqual(C1_SCORE, score)
        time = metrics_json['time']
        self.assertEquals(C1_TIME, time)

        # Check c2
        c = response.population[1]
        score_json = self.extension_packaging.from_extension_bytes(c.metrics)
        score = score_json['score']
        self.assertEqual(C2_SCORE, score)
        time = metrics_json['time']
        self.assertEquals(C1_TIME, time)
예제 #8
0
class EspService(object):
    def __init__(self, experiment_params):
        """
        A class that can interact with the ESP service
        :param experiment_params: the experiment parameters dictionary
        """
        self.experiment_params = experiment_params
        self.extension_packaging = ExtensionPackaging()
        self.experiment_params_as_bytes = self.extension_packaging.to_extension_bytes(
            experiment_params)
        self.experiment_id = experiment_params["LEAF"]["experiment_id"]
        self.version = experiment_params["LEAF"]["version"]

        # gRPC connection
        esp_host = experiment_params["LEAF"]["esp_host"]
        esp_port = experiment_params["LEAF"]["esp_port"]
        grpc_options = experiment_params["LEAF"].get(
            "grpc_options", DEFAULT_GRPC_OPTIONS).items()
        print("ESP service: {}:{}".format(esp_host, esp_port))
        print("gRPC options:")
        for pair in grpc_options:
            print("  {}: {}".format(pair[0], pair[1]))
        channel = grpc.insecure_channel('{}:{}'.format(esp_host, esp_port),
                                        options=grpc_options)
        print("Ready to connect.")
        self.esp_service = PopulationServiceStub(channel)

    def get_next_population(self, prev_response):
        """
        Returns a new generation for a given experiment.
        :param prev_response: the previous generation, *with* evaluation metrics for each Candidate
        :return: a new generation, as a PopulationResponse object
        """
        # Prepare a request for next generation
        request_params = {
            'version': self.version,
            'experiment_id': self.experiment_id
        }
        request = ParseDict(request_params, PopulationRequest())
        request.config = self.experiment_params_as_bytes
        if prev_response:
            request.evaluated_population_response.CopyFrom(prev_response)

        # Ask for next generation
        response = self._next_population_with_retry(request)
        return response

    def get_previous_population(self, experiment_id, checkpoint_id):
        """
        Returns the population corresponding to the passed experiment_id and checkpoint_id
        :param experiment_id: the experiment id
        :param checkpoint_id: the checkpoint id returned by a previous call to get_next_population
        :return: a previous generation, as a PopulationResponse object
        """
        # Prepare a GetPopulation request
        request_params = {
            'version': self.version,
            'experiment_id': experiment_id,
            'checkpoint_id': checkpoint_id
        }
        request = ParseDict(request_params, ExistingPopulationRequest())
        # Ask for a previous generation
        response = self.esp_service.GetPopulation(request)
        return response

    @retry(stop=stop_after_attempt(NB_RETRIES),
           wait=wait_random(1, 3),
           retry=retry_if_exception_type(grpc.RpcError))
    def _next_population_with_retry(self, request):
        print("Sending NextPopulation request")
        response = self.esp_service.NextPopulation(request)
        print("NextPopulation response received.")
        return response

    @retry(stop=stop_after_attempt(NB_RETRIES),
           wait=wait_random(1, 3),
           retry=retry_if_exception_type(grpc.RpcError))
    def _get_population_with_retry(self, request):
        print("Sending GetPopulation request")
        response = self.esp_service.GetPopulation(request)
        print("GetPopulation response received.")
        return response

    def request_base_model(self):
        # Update the request to query for 1 Keras model
        params = copy.deepcopy(self.experiment_params)
        params["evolution"] = {"population_size": 1}
        params["LEAF"]["representation"] = "KerasNN"

        # Prepare a request for next generation
        request_params = {
            'version': self.version,
            'experiment_id': self.experiment_id
        }
        request = ParseDict(request_params, PopulationRequest())
        request.config = self.extension_packaging.to_extension_bytes(params)

        # Ask for the base model
        response = self._next_population_with_retry(request)

        # Convert the received bytes to a Keras model
        model_bytes = response.population[0].interpretation
        model_file = io.BytesIO(model_bytes)
        keras_model = load_model(model_file)

        # return the base model
        return keras_model

    def extract_candidates_info(self, response):
        """
        Prints Candidate details from a population
        :param response: a PopulationResponse from the ESP service
        :return: a dictionary representing a candidate
        """
        candidates_info = []
        for candidate in response.population:
            c = {
                "id":
                candidate.id,
                "identity":
                candidate.identity.decode('UTF-8'),
                "metrics":
                self.extension_packaging.from_extension_bytes(
                    candidate.metrics),
                "model":
                candidate.interpretation
            }
            candidates_info.append(c)
        return candidates_info

    @staticmethod
    def print_population_response(response):
        """
        Prints out the details of a population represented by a PopulationResponse object
        :param response: a PopulationResponse object returned by the ESP API
        """
        print("PopulationResponse:")
        print("  Generation: {}".format(response.generation_count))
        print("  Population size: {}".format(len(response.population)))
        print("  Checkpoint id: {}".format(response.checkpoint_id))
        print("  Evaluation stats: {}".format(
            response.evaluation_stats.decode('UTF-8')))

    def print_candidates(self, response, sort_candidates=True):
        """
        Prints the candidates details
        :param response: an evaluated PopulationResponse
        :param sort_candidates: if True, sort the candidates by score, lowest first, to always see the best
        candidates at the bottom of the logs
        """
        # Interpret the response received from the ESP service
        candidates_info = self.extract_candidates_info(response)
        if sort_candidates:
            # Sort the candidates by score, lowest first to always see the best candidates at the bottom of the log
            candidates_info = sorted(candidates_info,
                                     key=lambda k: k["metrics"]["score"],
                                     reverse=False)

        print("Evaluated candidates:")
        for candidate in candidates_info:
            print("Id: {} Identity: {} Metrics: {}".format(
                candidate["id"], candidate["identity"], candidate["metrics"]))
        print("")