{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Creating a Custom `Selector`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In an evolutionary algorithm, a selector selects from a set of individuals into a strict subset. The following diagram illustrates where selection occurs in a typical evolutionary algorithm:\n", "\n", "![what](media/highlight-selector.svg). " ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "from evokit.core import Population\n", "from evokit.evolvables.binstring import BinaryString" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To use a selector, first create a `Population`. This example uses the binary string representation." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['[1, 1, 1, 1, 1]', '[1, 1, 1, 1, 0]', '[1, 1, 1, 0, 0]', '[1, 1, 0, 0, 0]', '[1, 0, 0, 0, 0]', '[0, 0, 0, 0, 0]']\n" ] } ], "source": [ "pop : Population[BinaryString] = Population[BinaryString]()\n", "\n", "pop.append(BinaryString(int('11111', 2), 5))\n", "pop.append(BinaryString(int('11110', 2), 5))\n", "pop.append(BinaryString(int('11100', 2), 5))\n", "pop.append(BinaryString(int('11000', 2), 5))\n", "pop.append(BinaryString(int('10000', 2), 5))\n", "pop.append(BinaryString(int('00000', 2), 5))\n", "\n", "print(pop)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To make things easier, manually assign a `.fitness` to each item in the population. This is normally done by a pre-defined `Evaluator`." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "pop[0].fitness = 5\n", "pop[1].fitness = 4\n", "pop[2].fitness = 3\n", "pop[3].fitness = 2\n", "pop[4].fitness = 1\n", "pop[5].fitness = 0" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Verify that all individuals are correctly evaluated:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Fitness of [1, 1, 1, 1, 1] is 5\n", "Fitness of [1, 1, 1, 1, 0] is 4\n", "Fitness of [1, 1, 1, 0, 0] is 3\n", "Fitness of [1, 1, 0, 0, 0] is 2\n", "Fitness of [1, 0, 0, 0, 0] is 1\n", "Fitness of [0, 0, 0, 0, 0] is 0\n" ] } ], "source": [ "for individual in pop:\n", " print(f\"Fitness of {individual} is {individual.fitness}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Selector\n", "\n", "The abstract class `Selector` implements three methods:\n", "\n", "- `.select` defines the strategy for a single selection operation. It has no default implementation.\n", "\n", "- `.select_to_many` applies the `.select` to a collection of individuals. It also handles the removal of selected individuals from the original population. The default implementation repeatedly applies .select_to_many, until `.budget` items are selected.\n", "\n", "- `.select_to_population` applies the `.select_to_many` to a population. \n", "\n", "In general, a custom selector must override `.select`. A selector that requires information from the entire population (e.g. a fitness sharing selector) may override `.select_to_many`. Do not override `.select_to_population` - define a new method that uses `.select_to_many` to select from something else.\n", "\n", "```Python\n", "class Selector(ABC, Generic[D]):\n", " def __init__(self: Self, budget: int): ...\n", "\n", "\n", " def select_to_population(self,\n", " population: Population[D]) -> Population[D]: ...\n", "\n", " def select_to_many(self, population: Population[D]) -> Tuple[D, ...]:\n", " ...\n", "\n", " @abstractmethod\n", " def select(self,\n", " population: Population[D]) -> Tuple[D, ...]: ...\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Define a custom selector. Because the implementation does not provide its own way to decide how many items to select, it must have the `.budget` attribute. There are many ways to do it; this example uses the super constructor.\n", "\n", "The `.select` method returns a tuple of items from a population. Because the `ExampleSimpleSelector` only selects the best individual, `.select` should return a 1-tuple." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "from typing import override, Self, Tuple\n", "from evokit.core import Selector\n", "\n", "class ExampleSimpleSelector(Selector[BinaryString]):\n", " \"\"\"Simple selector that select the highest-fitness individual.\n", "\n", " Example for overriding `select`.\n", " \"\"\"\n", " @override\n", " def __init__(self: Self):\n", " super().__init__(3)\n", "\n", " def select(self,\n", " population: Population[BinaryString]) -> Tuple[BinaryString]:\n", " \n", " population.sort(lambda x: x.fitness)\n", " selected_solution = population[0]\n", " return (selected_solution,)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Apply the selector to the population. Because the selected individual is not removed from the original population, the selector always chooses `[1,1,1,1,1]` - the individual with the highest fitness." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Selected items are ['[1, 1, 1, 1, 1]']\n", "Selected items are ['[1, 1, 1, 1, 1]']\n", "Selected items are ['[1, 1, 1, 1, 1]']\n", "After selection, the old population is ['[1, 1, 1, 1, 1]', '[1, 1, 1, 1, 0]', '[1, 1, 1, 0, 0]', '[1, 1, 0, 0, 0]', '[1, 0, 0, 0, 0]', '[0, 0, 0, 0, 0]']\n" ] } ], "source": [ "old_pop = pop\n", "selector = ExampleSimpleSelector()\n", "\n", "for _ in range(3):\n", " selected_items = selector.select(old_pop)\n", " print(f\"Selected items are {[str(x) for x in selected_items]}\")\n", "\n", "print(f\"After selection, the old population is {old_pop}\")\n", " \n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Observe the effect of select_to_many. With `.budget=3`, `.select_to_many` returns a 3-tuple. Note that items in the returned tuple are removed from the original population.\n", "\n", "Calling `.select_to_population` produces the same result, except that it returns a population with 3 items." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Selected items are ['[1, 1, 1, 1, 1]', '[1, 1, 1, 1, 0]', '[1, 1, 1, 0, 0]']\n", "After selection, the old population is ['[1, 1, 0, 0, 0]', '[1, 0, 0, 0, 0]', '[0, 0, 0, 0, 0]']\n" ] } ], "source": [ "old_pop = pop\n", "selector = ExampleSimpleSelector()\n", "\n", "selected_items = selector.select_to_many(old_pop)\n", "\n", "print(f\"Selected items are {[str(x) for x in selected_items]}\") \n", "\n", "print(f\"After selection, the old population is {old_pop}\")\n", " \n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.0" } }, "nbformat": 4, "nbformat_minor": 2 }