diff --git a/circle_evolution/evolution.py b/circle_evolution/evolution.py index 7aa7456..a994bf1 100644 --- a/circle_evolution/evolution.py +++ b/circle_evolution/evolution.py @@ -4,12 +4,14 @@ import numpy as np -from circle_evolution.species import Specie - import circle_evolution.fitness as fitness +from circle_evolution import runner + +from circle_evolution.species import Specie + -class Evolution: +class Evolution(runner.Runner): """Logic for a Species Evolution. Use the Evolution class when you want to train a Specie to look like @@ -35,6 +37,7 @@ def __init__(self, size, target, genes=100): self.target = target # Target Image self.generation = 1 self.genes = genes + self.best_fit = self.new_fit = 0 self.specie = Specie(size=self.size, genes=genes) @@ -70,14 +73,6 @@ def mutate(self, specie): return new_specie - def print_progress(self, fit): - """Prints progress of Evolution. - - Args: - fit (float): fitness value of specie. - """ - print("GEN {}, FIT {:.8f}".format(self.generation, fit)) - def evolve(self, fitness=fitness.MSEFitness, max_generation=100000): """Genetic Algorithm for evolution. @@ -87,19 +82,22 @@ def evolve(self, fitness=fitness.MSEFitness, max_generation=100000): fitness (fitness.Fitness): fitness class to score species preformance. max_generation (int): amount of generations to train for. """ - fitness = fitness(self.target) + self.notify(self, runner.START) + fitness_ = fitness(self.target) self.specie.render() - fit = fitness.score(self.specie.phenotype) + self.best_fit = fitness_.score(self.specie.phenotype) - for i in range(max_generation): + for i in range(0, max_generation): self.generation = i + 1 mutated = self.mutate(self.specie) mutated.render() - newfit = fitness.score(mutated.phenotype) + self.new_fit = fitness_.score(mutated.phenotype) + self.notify(self) - if newfit > fit: - fit = newfit + if self.new_fit > self.best_fit: + self.best_fit = self.new_fit self.specie = mutated - self.print_progress(newfit) + + self.notify(self, runner.END) diff --git a/circle_evolution/fitness.py b/circle_evolution/fitness.py index 7d63b17..68517e8 100644 --- a/circle_evolution/fitness.py +++ b/circle_evolution/fitness.py @@ -48,6 +48,7 @@ class MSEFitness(Fitness): See: https://en.wikipedia.org/wiki/Mean_squared_error. """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._max_error = (np.square((1 - (self.target >= 127)) * 255 - self.target)).mean(axis=None) diff --git a/circle_evolution/main.py b/circle_evolution/main.py index 2afaa41..a06a005 100644 --- a/circle_evolution/main.py +++ b/circle_evolution/main.py @@ -8,6 +8,8 @@ import circle_evolution.helpers as helpers +from circle_evolution import reporter + def main(): """Entrypoint of application""" @@ -21,8 +23,13 @@ def main(): args = parser.parse_args() target = helpers.load_target_image(args.image, size=size_options[args.size]) + reporter_logger = reporter.LoggerMetricReporter() + reporter_csv = reporter.CSVMetricReporter() evolution = Evolution(size_options[args.size], target, genes=args.genes) + evolution.attach(reporter_logger) + evolution.attach(reporter_csv) + evolution.evolve(max_generation=args.max_generations) evolution.specie.render() diff --git a/circle_evolution/reporter.py b/circle_evolution/reporter.py new file mode 100644 index 0000000..abfec81 --- /dev/null +++ b/circle_evolution/reporter.py @@ -0,0 +1,150 @@ +"""Reporters for capturing events and notifying the user about them. + +You can make your own Reporter class or use the already implemented one's. +Notice the abstract class before implementing your Reporter to see which +functions should be implemented. +""" +from abc import ABC, abstractmethod + +import csv + +from datetime import datetime + +import logging +import logging.config + +import tempfile + + +class Reporter(ABC): + """Base Reporter class. + + The Reporter is responsible for capturing particular events and sending + them for visualization. Please, use this class if you want to implement + your own Reporter. + """ + + def __init__(self): + """Initialization calls setup to configure Reporter""" + self.setup() + + def setup(self): + """Function for configuring the reporter. + + Some reporters may need configuring some internal parameters or even + creating objects to warmup. This function deals with this + """ + + @abstractmethod + def update(self, report): + """Receives report from subject. + + This is the main function for reporting events. The Reporter receives a + report and have to deal with what to do with that. To accomodate with + context, it also receives an status. + """ + + @abstractmethod + def on_start(self, report): + """Receives report for when the Subject start processing. + + If a reporter needs to report an object being initialized or starts + processing, it can use this. Please note that ALL reporters need + to implement this, if it is not used you can just `return` or `pass` + """ + + @abstractmethod + def on_stop(self, report): + """Receives report for when the Subject finishes processing. + + If a reporter needs to report an object that finished + processing, it can use this. Please note that ALL reporters need + to implement this, if it is not used you can just `return` or `pass` + """ + + +class LoggerMetricReporter(Reporter): + """Reporter for logging. + + This Reporter is responsible for setting up a Logger object and logging all + events that happened during circle-evolution cycle. + """ + + def setup(self): + """Sets up Logger""" + config_initial = { + "version": 1, + "disable_existing_loggers": True, + "formatters": { + "simple": {"format": "Circle-Evolution %(message)s"}, + "complete": {"format": "%(asctime)s %(name)s %(message)s", "datefmt": "%H:%M:%S"}, + }, + "handlers": { + "console": {"level": "INFO", "class": "logging.StreamHandler", "formatter": "simple"}, + "file": { + "level": "DEBUG", + "class": "logging.FileHandler", + "filename": f"{tempfile.gettempdir()}/circle_evolution.log", + "mode": "w", + "formatter": "complete", + }, + }, + "loggers": {"circle-evolution": {"handlers": ["console", "file"], "level": "DEBUG"}}, + "root": {"handlers": ["console", "file"], "level": "DEBUG"}, + } + logging.config.dictConfig(config_initial) + self.logger = logging.getLogger(__name__) # Creating new logger + + def update(self, report): + """Logs events using logger""" + self.logger.debug("Received event...") + + improvement = report.new_fit - report.best_fit + message = f"\tGeneration {report.generation} - Fitness {report.new_fit:.5f}" + + if improvement > 0: + improvement = improvement / report.best_fit * 100 + message += f" - Improvement {improvement:.5f}%%" + self.logger.info(message) + else: + message += " - No Improvement" + self.logger.debug(message) + + def on_start(self, report): + """Just logs the maximum generations""" + self.logger.info("Starting evolution...") + + def on_stop(self, report): + """Just logs the final fitness""" + self.logger.info("Evolution ended! Enjoy your Circle-Evolved Image!\t" f"Final fitness: {report.best_fit:.5f}") + + +class CSVMetricReporter(Reporter): + """CSV Report for Data Analysis. + + In case one wants to extract evolution metrics for a CSV file. + """ + + def setup(self): + """Sets up Logger""" + now = datetime.now() + self.filename = f"circle-evolution-{now.strftime('%d-%m-%Y_%H-%M-%S')}.csv" + self._write_to_csv(["generation", "fitness"]) # header + + def _write_to_csv(self, content): + """Safely writes content to CSV file.""" + with open(self.filename, "a") as fd: + writer = csv.writer(fd) + writer.writerow(content) + + def update(self, report): + """Logs events using logger""" + self._write_to_csv([report.generation, report.new_fit]) + + def on_start(self, report): + # Nothing to do here + pass + + def on_stop(self, report): + # Nothing to do here + pass diff --git a/circle_evolution/runner.py b/circle_evolution/runner.py new file mode 100644 index 0000000..4b2c3ab --- /dev/null +++ b/circle_evolution/runner.py @@ -0,0 +1,40 @@ +"""Base Class for Reported Objects. + +If you want to receive reports about any objects in Circle-Evolution you just +need to extend your class with the base Runner. It provides an interface for +attaching reporters and notifying all reporters of a particular event. +""" +# Constants for Runners +START = 0 +PROCESSING = 1 +END = 2 + + +class Runner: + """Base Runner class. + + The Runner class is responsible for managing reporters and sending events + to them. If you need to receive updates by a particular reporter you just + need to use this base class. + + Attributes: + _reporters: list of reporters that are going to receive reports. + """ + + _reporters = [] + + def attach(self, reporter): + """Attaches reporter for notifications""" + self._reporters.append(reporter) + + def notify(self, report, status=PROCESSING): + """Send report to all attached reporters""" + if status == START: + for reporter in self._reporters: + reporter.on_start(report) + elif status == END: + for reporter in self._reporters: + reporter.on_stop(report) + elif status == PROCESSING: + for reporter in self._reporters: + reporter.update(report)