{
  "cells": [
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "%matplotlib inline"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "\n# Example for evolutionary regression with local search via evolution strategies\n\nExample demonstrating the use of Cartesian genetic programming for a\nregression task that involves numeric constants. Local search via\nevolution strategies is used to determine numeric leaf values of the\ngraph.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "# The docopt str is added explicitly to ensure compatibility with\n# sphinx-gallery.\ndocopt_str = \"\"\"\n  Usage:\n    example_local_search_evolution_strategies.py [--max-generations=<N>]\n\n  Options:\n    -h --help\n    --max-generations=<N>  Maximum number of generations [default: 500]\n\n\"\"\"\n\nimport functools\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport scipy.constants\nfrom docopt import docopt\n\nimport cgp\n\nargs = docopt(docopt_str)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "We first define the target function. Note that this function contains\nnumeric values which are initially not available as constants to the search.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "def f_target(x):\n    return np.e * x[:, 0] ** 2 + 1.0 + np.pi"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Then we define the objective function for the evolution. It consists\nof an inner objective which accepts a NumPy-compatible function as\nits first argument and returns the mean-squared error between the\nexpression represented by a given individual and the target function\nevaluated on a set of random points. This inner objective is used by\nthe local search to determine appropriate values for Parameter node\nand the actual objective function to update the fitness of the\nindividual.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "def inner_objective(ind, seed):\n    \"\"\"Return a loss for the numpy-compatible function f. Used for\n    calculating the fitness of each individual and for the local\n    search of numeric leaf values.\n\n    \"\"\"\n\n    f = ind.to_numpy()\n    rng = np.random.RandomState(seed)\n    batch_size = 500\n    x = rng.uniform(-5, 5, size=(batch_size, 1))\n    y = f(x)\n    return -np.mean((f_target(x) - y[:, 0]) ** 2)\n\n\ndef objective(individual, seed):\n    \"\"\"Objective function of the regression task.\"\"\"\n\n    if not individual.fitness_is_None():\n        return individual\n\n    individual.fitness = inner_objective(individual, seed)\n\n    return individual"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Next, we define the parameters for the population, the genome of\nindividuals, the evolutionary algorithm, and the local search.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "population_params = {\"n_parents\": 1, \"seed\": 818821}\n\ngenome_params = {\n    \"n_inputs\": 1,\n    \"n_outputs\": 1,\n    \"n_columns\": 36,\n    \"n_rows\": 1,\n    \"levels_back\": None,\n    \"primitives\": (cgp.Add, cgp.Sub, cgp.Mul, cgp.Parameter),\n}\n\nea_params = {\n    \"n_offsprings\": 4,\n    \"mutation_rate\": 0.05,\n    \"tournament_size\": 1,\n    \"n_processes\": 1,\n    \"k_local_search\": 2,\n}\n\nevolve_params = {\"max_generations\": int(args[\"--max-generations\"]), \"termination_fitness\": 0.0}\n\n# restrict the number of steps in the local search; since parameter\n# values are propagated from parents to offsprings, parameter values\n# may be iteratively improved across generations despite the small\n# number of steps per generation\nlocal_search_params = {\"max_steps\": 5}"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "We then create a Population instance and instantiate the local search\nand evolutionary algorithm.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "pop = cgp.Population(**population_params, genome_params=genome_params)\n\n# define the function for local search; an instance of the\n# EvolutionStrategies can be called with an individual as an argument\n# for which the local search is performed\nlocal_search = cgp.local_search.EvolutionStrategies(\n    objective=functools.partial(inner_objective, seed=population_params[\"seed\"] + 1),\n    seed=population_params[\"seed\"] + 2,\n    **local_search_params,\n)\n\nea = cgp.ea.MuPlusLambda(**ea_params, local_search=local_search)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "We define a recording callback closure for bookkeeping of the progression of the evolution.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "history = {}\nhistory[\"champion\"] = []\nhistory[\"fitness_parents\"] = []\n\n\ndef recording_callback(pop):\n    history[\"champion\"].append(pop.champion)\n    history[\"fitness_parents\"].append(pop.fitness_parents())\n\n\nobj = functools.partial(objective, seed=population_params[\"seed\"] + 1)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Finally, we call the `evolve` method to perform the evolutionary search.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "cgp.evolve(pop, obj, ea, **evolve_params, print_progress=True, callback=recording_callback)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "After finishing the evolution, we plot the result and log the final\nevolved expression.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "width = 9.0\nfig = plt.figure(figsize=(width, width / scipy.constants.golden))\n\nax_fitness = fig.add_subplot(121)\nax_fitness.set_xlabel(\"Generation\")\nax_fitness.set_ylabel(\"Fitness\")\nax_fitness.set_yscale(\"symlog\")\n\nax_function = fig.add_subplot(122)\nax_function.set_ylabel(r\"$f(x)$\")\nax_function.set_xlabel(r\"$x$\")\n\n\nprint(f\"Final expression {pop.champion.to_sympy()[0]} with fitness {pop.champion.fitness}\")\n\nhistory_fitness = np.array(history[\"fitness_parents\"])\nax_fitness.plot(np.max(history_fitness, axis=1), label=\"Champion\")\nax_fitness.plot(np.mean(history_fitness, axis=1), label=\"Population mean\")\n\nx = np.linspace(-5.0, 5, 100).reshape(-1, 1)\nf = pop.champion.to_func()\ny = [f(xi) for xi in x]\nax_function.plot(x, f_target(x), lw=2, label=\"Target\")\nax_function.plot(x, y, lw=1, label=\"Target\", marker=\"x\")\n\nplt.savefig(\"example_local_search_evolution_strategies.pdf\", dpi=300)"
      ]
    }
  ],
  "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.8.6"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}