{
  "cells": [
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "%matplotlib inline"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "\n# Example for differential evolutionary regression\n\nExample demonstrating the use of Cartesian genetic programming for\na regression task that involves numeric constants. Local\ngradient-based search is used to determine numeric leaf values of the\ngraph.\n\nReferences:\n\n- Topchy, A., & Punch, W. F. (2001). Faster genetic programming based\n  on local gradient search of numeric leaf values. In Proceedings of\n  the genetic and evolutionary computation conference (GECCO-2001)\n  (Vol. 155162). Morgan Kaufmann San Francisco, CA, USA.\n\n- Izzo, D., Biscani, F., & Mereta, A. (2017). Differentiable genetic\n  programming. In European Conference on Genetic Programming\n  (pp. 35-51). Springer, Cham.\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_differential_evo_regression.py [--max-generations=<N>]\n\n  Options:\n    -h --help\n    --max-generations=<N>  Maximum number of generations [default: 2000]\n\n\"\"\"\n\nimport functools\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport scipy.constants\nimport torch\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 x[:, 0] ** 2 + 1.0 + np.pi"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Then we define the differentiable(!) objective function for the evolution.  It\nconsists of an inner objective which accepts a torch tensor as input\nvariable and uses mean-squared error between the expression\nrepresented by a given individual and the target function evaluated\non a set of random points. This inner objective is then used by\nactual objective function to update the fitness of the individual.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "def inner_objective(f, seed):\n    \"\"\"Return a differentiable loss of the differentiable graph f. Used\n    for calculating the fitness of each individual and for the local\n    search of numeric leaf values.\n\n    \"\"\"\n\n    torch.manual_seed(seed)\n    batch_size = 500\n    x = torch.DoubleTensor(batch_size, 1).uniform_(-5, 5)\n    y = f(x)\n    return torch.nn.MSELoss()(f_target(x), y[:, 0])\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    f = individual.to_torch()\n    loss = inner_objective(f, seed)\n    individual.fitness = -loss.item()\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\": 20,\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    \"tournament_size\": 1,\n    \"mutation_rate\": 0.05,\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# use an uneven number of gradient steps so they can not easily\n# average out for clipped values\nlocal_search_params = {\"lr\": 1e-3, \"gradient_steps\": 9}"
      ]
    },
    {
      "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; parameters such as the\n# learning rate and number of gradient steps are fixed via the use\n# of `partial`; the `local_search` function should only receive a\n# population of individuals as input\nlocal_search = functools.partial(\n    cgp.local_search.gradient_based,\n    objective=functools.partial(inner_objective, seed=population_params[\"seed\"]),\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\"])"
      ]
    },
    {
      "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_differential_evo_regression.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
}