Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/base reporter #15

Draft
wants to merge 12 commits into
base: develop
Choose a base branch
from
34 changes: 16 additions & 18 deletions circle_evolution/evolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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.

Expand All @@ -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)
1 change: 1 addition & 0 deletions circle_evolution/fitness.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions circle_evolution/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import circle_evolution.helpers as helpers

from circle_evolution import reporter


def main():
"""Entrypoint of application"""
Expand All @@ -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()
Expand Down
150 changes: 150 additions & 0 deletions circle_evolution/reporter.py
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions circle_evolution/runner.py
Original file line number Diff line number Diff line change
@@ -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)