├── .gitignore ├── LICENSE.txt ├── README.md ├── docs ├── conf.py └── index.rst ├── peas.ai ├── peas.png └── peas ├── __init__.py ├── examples ├── pole_balancing.py └── xor.py ├── experiments ├── hyperneat_fracture.py ├── hyperneat_line_following.py ├── hyperneat_noise.py └── hyperneat_visual_discrimination.py ├── methods ├── __init__.py ├── evolution.py ├── hyperneat.py ├── neat.py ├── neatpythonwrapper.py ├── reaction.py └── wavelets.py ├── networks ├── __init__.py └── rnn.py ├── tasks ├── __init__.py ├── checkers.py ├── linefollowing │ ├── __init__.py │ ├── eight.ai │ ├── eight.png │ ├── eight_inverted.png │ ├── eight_striped.png │ ├── eight_striped2.png │ └── linefollowing.py ├── polebalance.py ├── shapediscrimination.py ├── targetweights.py ├── walking.py └── xor.py └── test ├── __init__.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | .DS_Store 3 | docs/_build/* 4 | test*.py 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Thomas van den Berg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://github.com/noio/peas/raw/master/peas.png) 2 | 3 | Python Evolutionary Algorithms 4 | ============================== 5 | 6 | This is a library of evolutionary algorithms with a focus on _neuroevolution_, implemented in pure python, depending on numpy. It contains a faithful implementation of [Ken Stanley][1]'s **NEAT** (Neuroevolution of Augmenting Topologies) and HyperNEAT. The focus of this library is **easy experimentation**, it is pure python so it's easy to set up, and it has a simple and flexible API. 7 | 8 | This code was written to do the experiments for: 9 | 10 | - the paper [Critical Factors in the Performance of HyperNEAT](https://staff.fnwi.uva.nl/s.a.whiteson/pubs/vandenberggecco13.pdf) 11 | - and the more extensive thesis [An Empirical Analysis of HyperNEAT](https://staff.fnwi.uva.nl/s.a.whiteson/tvdb-thesis.pdf) 12 | 13 | [1]: http://www.cs.ucf.edu/~kstanley/neat.html 14 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # peas documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Jul 10 13:48:30 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | sys.path.insert(0, os.path.abspath('..')) 21 | # sys.path.insert(0, os.path.abspath('../domination')) 22 | import peas 23 | 24 | 25 | # -- General configuration ----------------------------------------------------- 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be extensions 31 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 32 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.pngmath', 'sphinx.ext.mathjax', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | autoclass_content = 'both' 38 | 39 | # The encoding of source files. 40 | #source_encoding = 'utf-8-sig' 41 | 42 | master_doc = 'index' 43 | 44 | # General information about the project. 45 | project = u'peas' 46 | copyright = u'2012, Thomas van den Berg' 47 | 48 | # The version info for the project you're documenting, acts as replacement for 49 | # |version| and |release|, also used in various other places throughout the 50 | # built documents. 51 | # 52 | # The short X.Y version. 53 | version = '0.1' 54 | # The full version, including alpha/beta/rc tags. 55 | release = '0.1' 56 | 57 | # The language for content autogenerated by Sphinx. Refer to documentation 58 | # for a list of supported languages. 59 | #language = None 60 | 61 | # There are two options for replacing |today|: either, you set today to some 62 | # non-false value, then it is used: 63 | #today = '' 64 | # Else, today_fmt is used as the format for a strftime call. 65 | #today_fmt = '%B %d, %Y' 66 | 67 | # List of patterns, relative to source directory, that match files and 68 | # directories to ignore when looking for source files. 69 | exclude_patterns = ['_build'] 70 | 71 | # The reST default role (used for this markup: `text`) to use for all documents. 72 | #default_role = None 73 | 74 | # If true, '()' will be appended to :func: etc. cross-reference text. 75 | #add_function_parentheses = True 76 | 77 | # If true, the current module name will be prepended to all description 78 | # unit titles (such as .. function::). 79 | #add_module_names = True 80 | 81 | # If true, sectionauthor and moduleauthor directives will be shown in the 82 | # output. They are ignored by default. 83 | #show_authors = False 84 | 85 | # The name of the Pygments (syntax highlighting) style to use. 86 | pygments_style = 'sphinx' 87 | 88 | # A list of ignored prefixes for module index sorting. 89 | #modindex_common_prefix = [] 90 | 91 | 92 | # -- Options for HTML output --------------------------------------------------- 93 | 94 | # The theme to use for HTML and HTML Help pages. See the documentation for 95 | # a list of builtin themes. 96 | html_theme = 'default' 97 | 98 | # Theme options are theme-specific and customize the look and feel of a theme 99 | # further. For a list of options available for each theme, see the 100 | # documentation. 101 | #html_theme_options = {} 102 | 103 | # Add any paths that contain custom themes here, relative to this directory. 104 | #html_theme_path = [] 105 | 106 | # The name for this set of Sphinx documents. If None, it defaults to 107 | # " v documentation". 108 | #html_title = None 109 | 110 | # A shorter title for the navigation bar. Default is the same as html_title. 111 | #html_short_title = None 112 | 113 | # The name of an image file (relative to this directory) to place at the top 114 | # of the sidebar. 115 | #html_logo = None 116 | 117 | # The name of an image file (within the static path) to use as favicon of the 118 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 119 | # pixels large. 120 | #html_favicon = None 121 | 122 | # Add any paths that contain custom static files (such as style sheets) here, 123 | # relative to this directory. They are copied after the builtin static files, 124 | # so a file named "default.css" will overwrite the builtin "default.css". 125 | # html_static_path = ['_static'] 126 | 127 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 128 | # using the given strftime format. 129 | #html_last_updated_fmt = '%b %d, %Y' 130 | 131 | # If true, SmartyPants will be used to convert quotes and dashes to 132 | # typographically correct entities. 133 | #html_use_smartypants = True 134 | 135 | # Custom sidebar templates, maps document names to template names. 136 | #html_sidebars = {} 137 | 138 | # Additional templates that should be rendered to pages, maps page names to 139 | # template names. 140 | #html_additional_pages = {} 141 | 142 | # If false, no module index is generated. 143 | #html_domain_indices = True 144 | 145 | # If false, no index is generated. 146 | #html_use_index = True 147 | 148 | # If true, the index is split into individual pages for each letter. 149 | #html_split_index = False 150 | 151 | # If true, links to the reST sources are added to the pages. 152 | #html_show_sourcelink = True 153 | 154 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 155 | #html_show_sphinx = True 156 | 157 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 158 | #html_show_copyright = True 159 | 160 | # If true, an OpenSearch description file will be output, and all pages will 161 | # contain a tag referring to it. The value of this option must be the 162 | # base URL from which the finished HTML is served. 163 | #html_use_opensearch = '' 164 | 165 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 166 | #html_file_suffix = None 167 | 168 | # Output file base name for HTML help builder. 169 | htmlhelp_basename = 'peasdoc' 170 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. peas documentation master file, created by 2 | sphinx-quickstart on Tue Jul 10 13:48:30 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to peas's documentation! 7 | ================================ 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | .. automodule:: peas.evolution.neat 15 | :members: 16 | 17 | .. automodule:: peas.evolution.hyperneat 18 | :members: 19 | 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | 28 | -------------------------------------------------------------------------------- /peas.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noio/peas/4c2503388644cc5ca58c501e6c773d53a883538b/peas.ai -------------------------------------------------------------------------------- /peas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noio/peas/4c2503388644cc5ca58c501e6c773d53a883538b/peas.png -------------------------------------------------------------------------------- /peas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noio/peas/4c2503388644cc5ca58c501e6c773d53a883538b/peas/__init__.py -------------------------------------------------------------------------------- /peas/examples/pole_balancing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ### IMPORTS ### 4 | import sys, os 5 | from functools import partial 6 | 7 | sys.path.append(os.path.join(os.path.split(__file__)[0],'..','..')) 8 | from peas.methods.neat import NEATPopulation, NEATGenotype 9 | # from peas.methods.neatpythonwrapper import NEATPythonPopulation 10 | from peas.tasks.polebalance import PoleBalanceTask 11 | 12 | # Create a factory for genotypes (i.e. a function that returns a new 13 | # instance each time it is called) 14 | genotype = lambda: NEATGenotype(inputs=6, 15 | weight_range=(-50., 50.), 16 | types=['tanh']) 17 | 18 | # Create a population 19 | pop = NEATPopulation(genotype, popsize=150) 20 | 21 | # Create a task 22 | dpnv = PoleBalanceTask(velocities=True, 23 | max_steps=100000, 24 | penalize_oscillation=True) 25 | 26 | # Run the evolution, tell it to use the task as an evaluator 27 | pop.epoch(generations=100, evaluator=dpnv, solution=dpnv) 28 | -------------------------------------------------------------------------------- /peas/examples/xor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ### IMPORTS ### 4 | import sys, os 5 | from functools import partial 6 | from collections import defaultdict 7 | 8 | sys.path.append(os.path.join(os.path.split(__file__)[0],'..','..')) 9 | from peas.methods.neat import NEATPopulation, NEATGenotype 10 | # from peas.methods.neatpythonwrapper import NEATPythonPopulation 11 | from peas.tasks.xor import XORTask 12 | 13 | # Create a factory for genotypes (i.e. a function that returns a new 14 | # instance each time it is called) 15 | genotype = lambda: NEATGenotype(inputs=2, 16 | weight_range=(-3., 3.), 17 | types=['sigmoid2']) 18 | 19 | # Create a population 20 | pop = NEATPopulation(genotype, popsize=150) 21 | 22 | # Create a task 23 | task = XORTask() 24 | 25 | nodecounts = defaultdict(int) 26 | 27 | for i in xrange(100): 28 | # Run the evolution, tell it to use the task as an evaluator 29 | pop.epoch(generations=100, evaluator=task, solution=task) 30 | nodecounts[len(pop.champions[-1].node_genes)] += 1 31 | 32 | print sorted(nodecounts.items()) 33 | -------------------------------------------------------------------------------- /peas/experiments/hyperneat_fracture.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ### IMPORTS ### 4 | import sys, os 5 | from functools import partial 6 | from itertools import product 7 | 8 | # Libs 9 | import numpy as np 10 | 11 | # Local 12 | 13 | sys.path.append(os.path.join(os.path.split(__file__)[0],'..','..')) 14 | from peas.methods.neat import NEATPopulation, NEATGenotype 15 | from peas.methods.hyperneat import HyperNEATDeveloper, Substrate 16 | from peas.methods.reaction import ReactionDeveloper 17 | from peas.methods.evolution import SimplePopulation 18 | from peas.methods.wavelets import WaveletGenotype, WaveletDeveloper 19 | from peas.networks.rnn import NeuralNetwork, gauss 20 | from peas.tasks.targetweights import TargetWeightsTask 21 | 22 | 23 | def evaluate(individual, task, developer): 24 | stats = task.evaluate(developer.convert(individual)) 25 | if isinstance(individual, NEATGenotype): 26 | stats['nodes'] = len(individual.node_genes) 27 | elif isinstance(individual, WaveletGenotype): 28 | stats['nodes'] = sum(len(w) for w in individual.wavelets) 29 | return stats 30 | 31 | def area(coords, axis, offset): 32 | return coords[0] * axis[0] + coords[1] * axis[1] > offset 33 | 34 | def split(coords, axis, flip, distance): 35 | return coords[axis] * flip >= distance 36 | 37 | def slope(coords, offset, axis): 38 | return coords[0] * axis[0] + coords[1] * axis[1] 39 | 40 | def random_direction_vector(): 41 | theta = np.random.random() * np.pi*2 42 | return np.array([np.cos(theta), np.sin(theta)]) 43 | 44 | def run(method, splits, generations=500, popsize=500): 45 | complexity = 'half' 46 | splits = int(splits) 47 | 48 | funcs = [] 49 | 50 | if complexity in ['half', 'flat', 'slope']: 51 | funcs.append((True, np.random.random() * 6. - 3)) 52 | 53 | for num in range(splits): 54 | axis = random_direction_vector() 55 | offset = np.random.random() - 0.2 56 | where = partial(area, axis=axis, offset=offset) 57 | 58 | if complexity == 'half': 59 | 60 | xs = 0 if num % 2 == 0 else 1 61 | mp = 1 if (num//2) % 2 == 0 else -1 62 | if num < 2: 63 | d = 0 64 | elif num < 2 + 4: 65 | d = 0.5 66 | elif num < 2 + 4 + 4: 67 | d = 0.25 68 | elif num < 2 + 4 + 4 + 4: 69 | d = 0.75 70 | 71 | where = partial(split, axis=xs, flip=mp, distance=d) 72 | what = lambda c, v: v + np.random.random() * 6. - 3 73 | 74 | funcs.append((where, what)) 75 | 76 | task = TargetWeightsTask(substrate_shape=(8,), funcs=funcs, fitnessmeasure='sqerr', uniquefy=True) 77 | 78 | substrate = Substrate() 79 | substrate.add_nodes((8,), 'l') 80 | substrate.add_connections('l', 'l') 81 | 82 | if method == 'hyperneat': 83 | geno = lambda: NEATGenotype(feedforward=True, inputs=2, weight_range=(-3.0, 3.0), 84 | prob_add_conn=0.3, prob_add_node=0.03, 85 | types=['sin', 'ident', 'gauss', 'sigmoid', 'abs']) 86 | 87 | pop = NEATPopulation(geno, popsize=popsize, target_species=8) 88 | developer = HyperNEATDeveloper(substrate=substrate, add_deltas=False, sandwich=False) 89 | 90 | 91 | elif method == '0hnmax': 92 | geno = lambda: NEATGenotype(feedforward=True, inputs=2, weight_range=(-3.0, 3.0), 93 | max_nodes=3, 94 | types=['sin', 'ident', 'gauss', 'sigmoid', 'abs']) 95 | 96 | pop = NEATPopulation(geno, popsize=popsize, target_species=8) 97 | developer = HyperNEATDeveloper(substrate=substrate, add_deltas=False, sandwich=False) 98 | 99 | 100 | elif method == 'wavelet': 101 | geno = lambda: WaveletGenotype(inputs=2) 102 | pop = SimplePopulation(geno, popsize=popsize) 103 | developer = WaveletDeveloper(substrate=substrate, add_deltas=False, sandwich=False) 104 | 105 | 106 | results = pop.epoch(generations=generations, 107 | evaluator=partial(evaluate, task=task, developer=developer), 108 | ) 109 | 110 | return results 111 | 112 | if __name__ == '__main__': 113 | for method in ['hyperneat', 'wavelet', '0hnmax']: 114 | for splits in range(15): 115 | run(method, splits) 116 | -------------------------------------------------------------------------------- /peas/experiments/hyperneat_line_following.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ### IMPORTS ### 4 | import sys, os 5 | from functools import partial 6 | from itertools import product 7 | 8 | # Libs 9 | import numpy as np 10 | np.seterr(invalid='raise') 11 | 12 | # Local 13 | sys.path.append(os.path.join(os.path.split(__file__)[0],'..','..')) 14 | from peas.methods.neat import NEATPopulation, NEATGenotype 15 | from peas.methods.evolution import SimplePopulation 16 | from peas.methods.wavelets import WaveletGenotype, WaveletDeveloper 17 | from peas.methods.hyperneat import HyperNEATDeveloper, Substrate 18 | from peas.tasks.linefollowing import LineFollowingTask 19 | 20 | 21 | def evaluate(individual, task, developer): 22 | phenotype = developer.convert(individual) 23 | stats = task.evaluate(phenotype) 24 | if isinstance(individual, NEATGenotype): 25 | stats['nodes'] = len(individual.node_genes) 26 | elif isinstance(individual, WaveletGenotype): 27 | stats['nodes'] = sum(len(w) for w in individual.wavelets) 28 | print '~', 29 | sys.stdout.flush() 30 | return stats 31 | 32 | def solve(individual, task, developer): 33 | phenotype = developer.convert(individual) 34 | return task.solve(phenotype) 35 | 36 | 37 | ### SETUPS ### 38 | def run(method, setup, generations=100, popsize=100): 39 | """ Use hyperneat for a walking gait task 40 | """ 41 | # Create task and genotype->phenotype converter 42 | 43 | 44 | if setup == 'easy': 45 | task_kwds = dict(field='eight', 46 | observation='eight', 47 | max_steps=3000, 48 | friction_scale=0.3, 49 | damping=0.3, 50 | motor_torque=10, 51 | check_coverage=False, 52 | flush_each_step=False, 53 | initial_pos=(282, 300, np.pi*0.35)) 54 | 55 | elif setup == 'hard': 56 | task_kwds = dict(field='eight', 57 | observation='eight_striped', 58 | max_steps=3000, 59 | friction_scale=0.3, 60 | damping=0.3, 61 | motor_torque=10, 62 | check_coverage=False, 63 | flush_each_step=True, 64 | force_global=True, 65 | initial_pos=(282, 300, np.pi*0.35)) 66 | 67 | elif setup == 'force': 68 | task_kwds = dict(field='eight', 69 | observation='eight', 70 | max_steps=3000, 71 | friction_scale=0.1, 72 | damping=0.9, 73 | motor_torque=3, 74 | check_coverage=True, 75 | flush_each_step=True, 76 | force_global=True, 77 | initial_pos=(17, 256, np.pi*0.5)) 78 | 79 | elif setup == 'prop': 80 | task_kwds = dict(field='eight', 81 | observation='eight_striped', 82 | max_steps=3000, 83 | friction_scale=0.3, 84 | damping=0.3, 85 | motor_torque=10, 86 | check_coverage=False, 87 | flush_each_step=False, 88 | initial_pos=(282, 300, np.pi*0.35)) 89 | 90 | elif setup == 'cover': 91 | task_kwds = dict(field='eight', 92 | observation='eight_striped', 93 | max_steps=3000, 94 | friction_scale=0.1, 95 | damping=0.9, 96 | motor_torque=3, 97 | check_coverage=True, 98 | flush_each_step=False, 99 | initial_pos=(17, 256, np.pi*0.5)) 100 | 101 | task = LineFollowingTask(**task_kwds) 102 | 103 | # The line following experiment has quite a specific topology for its network: 104 | substrate = Substrate() 105 | substrate.add_nodes([(0,0)], 'bias') 106 | substrate.add_nodes([(r, theta) for r in np.linspace(0,1,3) 107 | for theta in np.linspace(-1, 1, 5)], 'input') 108 | substrate.add_nodes([(r, theta) for r in np.linspace(0,1,3) 109 | for theta in np.linspace(-1, 1, 3)], 'layer') 110 | substrate.add_connections('input', 'layer',-1) 111 | substrate.add_connections('bias', 'layer', -2) 112 | substrate.add_connections('layer', 'layer',-3) 113 | 114 | if method == 'wvl': 115 | geno = lambda: WaveletGenotype(inputs=4, layers=3) 116 | pop = SimplePopulation(geno, popsize=popsize) 117 | developer = WaveletDeveloper(substrate=substrate, 118 | add_deltas=False, 119 | sandwich=False, 120 | node_type='tanh') 121 | 122 | else: 123 | geno_kwds = dict(feedforward=True, 124 | inputs=4, 125 | outputs=3, 126 | weight_range=(-3.0, 3.0), 127 | prob_add_conn=0.1, 128 | prob_add_node=0.03, 129 | bias_as_node=False, 130 | types=['sin', 'bound', 'linear', 'gauss', 'sigmoid', 'abs']) 131 | 132 | if method == 'nhn': 133 | pass 134 | elif method == '0hnmax': 135 | geno_kwds['max_nodes'] = 7 136 | elif method == '1hnmax': 137 | geno_kwds['max_nodes'] = 8 138 | 139 | geno = lambda: NEATGenotype(**geno_kwds) 140 | 141 | pop = NEATPopulation(geno, popsize=popsize, target_species=8) 142 | 143 | developer = HyperNEATDeveloper(substrate=substrate, 144 | add_deltas=False, 145 | sandwich=False, 146 | node_type='tanh') 147 | 148 | 149 | results = pop.epoch(generations=generations, 150 | evaluator=partial(evaluate, task=task, developer=developer), 151 | solution=partial(solve, task=task, developer=developer), 152 | ) 153 | 154 | return results 155 | 156 | if __name__ == '__main__': 157 | # Method is one of METHOD = ['wvl', 'nhn', '0hnmax', '1hnmax'] 158 | resnhn = run('nhn', 'hard') 159 | -------------------------------------------------------------------------------- /peas/experiments/hyperneat_noise.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ### IMPORTS ### 4 | import sys, os 5 | from functools import partial 6 | from itertools import product 7 | 8 | # Libs 9 | import numpy as np 10 | 11 | # Local 12 | sys.path.append(os.path.join(os.path.split(__file__)[0],'..','..')) 13 | from peas.methods.neat import NEATPopulation, NEATGenotype 14 | from peas.methods.hyperneat import HyperNEATDeveloper, Substrate 15 | from peas.methods.reaction import ReactionDeveloper 16 | from peas.methods.evolution import SimplePopulation 17 | from peas.methods.wavelets import WaveletGenotype, WaveletDeveloper 18 | from peas.networks.rnn import NeuralNetwork, gauss 19 | from peas.tasks.targetweights import TargetWeightsTask 20 | 21 | 22 | def evaluate(individual, task, developer): 23 | stats = task.evaluate(developer.convert(individual)) 24 | if isinstance(individual, NEATGenotype): 25 | stats['nodes'] = len(individual.node_genes) 26 | elif isinstance(individual, WaveletGenotype): 27 | stats['nodes'] = sum(len(w) for w in individual.wavelets) 28 | return stats 29 | 30 | def run(method, level, generations=500, popsize=500, visualize_individual=None): 31 | 32 | shape = (3,3) 33 | task = TargetWeightsTask(substrate_shape=shape, noise=level, fitnessmeasure='sqerr') 34 | 35 | substrate = Substrate() 36 | substrate.add_nodes(shape, 'l') 37 | substrate.add_connections('l', 'l') 38 | 39 | if method == 'hyperneat': 40 | geno = lambda: NEATGenotype(feedforward=True, inputs=len(shape)*2, weight_range=(-3.0, 3.0), 41 | prob_add_conn=0.3, prob_add_node=0.03, 42 | types=['sin', 'linear', 'gauss', 'sigmoid', 'abs']) 43 | 44 | pop = NEATPopulation(geno, popsize=popsize, target_species=8) 45 | developer = HyperNEATDeveloper(substrate=substrate, add_deltas=False, sandwich=False) 46 | 47 | elif method == '0hn': 48 | 49 | t = [(i, 4) for i in range(4)] 50 | geno = lambda: NEATGenotype(feedforward=True, inputs=len(shape)*2, weight_range=(-3.0, 3.0), 51 | prob_add_conn=0.0, prob_add_node=0.00, topology=t, 52 | types=['sin', 'linear', 'gauss', 'sigmoid', 'abs']) 53 | 54 | pop = NEATPopulation(geno, popsize=popsize, target_species=8) 55 | developer = HyperNEATDeveloper(substrate=substrate, add_deltas=False, sandwich=False) 56 | 57 | 58 | elif method == 'wavelet': 59 | geno = lambda: WaveletGenotype(inputs=len(shape)*2) 60 | pop = SimplePopulation(geno, popsize=popsize) 61 | developer = WaveletDeveloper(substrate=substrate, add_deltas=False, sandwich=False) 62 | 63 | 64 | results = pop.epoch(generations=generations, 65 | evaluator=partial(evaluate, task=task, developer=developer) 66 | ) 67 | return results 68 | 69 | if __name__ == '__main__': 70 | for method in ['0hn', 'wavelet', 'hyperneat']: 71 | for level in np.linspace(0, 1, 11): 72 | run(method, level) 73 | -------------------------------------------------------------------------------- /peas/experiments/hyperneat_visual_discrimination.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ### IMPORTS ### 4 | import sys, os 5 | from functools import partial 6 | from itertools import product 7 | 8 | sys.path.append(os.path.join(os.path.split(__file__)[0],'..','..')) 9 | from peas.methods.neat import NEATPopulation, NEATGenotype 10 | from peas.methods.hyperneat import HyperNEATDeveloper, Substrate 11 | from peas.methods.reaction import ReactionDeveloper 12 | from peas.methods.evolution import SimplePopulation 13 | from peas.methods.wavelets import WaveletGenotype, WaveletDeveloper 14 | from peas.tasks.shapediscrimination import ShapeDiscriminationTask 15 | from peas.networks.rnn import NeuralNetwork 16 | 17 | # Libs 18 | import numpy as np 19 | 20 | ### SETUPS ### 21 | 22 | def evaluate(individual, task, developer): 23 | stats = task.evaluate(developer.convert(individual)) 24 | if isinstance(individual, NEATGenotype): 25 | stats['nodes'] = len(individual.node_genes) 26 | elif isinstance(individual, WaveletGenotype): 27 | stats['nodes'] = sum(len(w) for w in individual.wavelets) 28 | return stats 29 | 30 | def solve(individual, task, developer): 31 | return task.solve(developer.convert(individual)) 32 | 33 | def run(method, setup, generations=250, popsize=100): 34 | # Create task and genotype->phenotype converter 35 | size = 11 36 | task_kwds = dict(size=size) 37 | 38 | if setup == 'big-little': 39 | task_kwds['targetshape'] = ShapeDiscriminationTask.makeshape('box', size//3) 40 | task_kwds['distractorshapes'] = [ShapeDiscriminationTask.makeshape('box', 1)] 41 | elif setup == 'triup-down': 42 | task_kwds['targetshape'] = np.triu(np.ones((size//3, size//3))) 43 | task_kwds['distractorshapes'] = [np.tril(np.ones((size//3, size//3)))] 44 | 45 | task = ShapeDiscriminationTask(**task_kwds) 46 | 47 | substrate = Substrate() 48 | substrate.add_nodes((size, size), 'l') 49 | substrate.add_connections('l', 'l') 50 | 51 | if method == 'wavelet': 52 | num_inputs = 6 if deltas else 4 53 | geno = lambda: WaveletGenotype(inputs=num_inputs) 54 | pop = SimplePopulation(geno, popsize=popsize) 55 | developer = WaveletDeveloper(substrate=substrate, add_deltas=True, sandwich=True) 56 | 57 | else: 58 | geno_kwds = dict(feedforward=True, 59 | inputs=6, 60 | weight_range=(-3.0, 3.0), 61 | prob_add_conn=0.1, 62 | prob_add_node=0.03, 63 | bias_as_node=False, 64 | types=['sin', 'bound', 'linear', 'gauss', 'sigmoid', 'abs']) 65 | 66 | if method == 'nhn': 67 | pass 68 | elif method == '0hnmax': 69 | geno_kwds['max_nodes'] = 7 70 | elif method == '1hnmax': 71 | geno_kwds['max_nodes'] = 8 72 | 73 | geno = lambda: NEATGenotype(**geno_kwds) 74 | pop = NEATPopulation(geno, popsize=popsize, target_species=8) 75 | 76 | developer = HyperNEATDeveloper(substrate=substrate, 77 | sandwich=True, 78 | add_deltas=True, 79 | node_type='tanh') 80 | 81 | # Run and save results 82 | results = pop.epoch(generations=generations, 83 | evaluator=partial(evaluate, task=task, developer=developer), 84 | solution=partial(solve, task=task, developer=developer), 85 | ) 86 | 87 | return results 88 | 89 | if __name__ == '__main__': 90 | # Method is one of ['wvl', 'nhn', '0hnmax', '1hnmax'] 91 | # setup is one of ['big-little', 'triup-down'] 92 | run('nhn', 'big-little') 93 | 94 | 95 | -------------------------------------------------------------------------------- /peas/methods/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noio/peas/4c2503388644cc5ca58c501e6c773d53a883538b/peas/methods/__init__.py -------------------------------------------------------------------------------- /peas/methods/evolution.py: -------------------------------------------------------------------------------- 1 | """ This module implements the different genotypes and evolutionary 2 | methods used in NEAT and HyperNEAT. It is meant to be a reference 3 | implementation, so any inneficiencies are copied as they are 4 | described. 5 | """ 6 | 7 | ### IMPORTS ### 8 | import sys 9 | import random 10 | import multiprocessing 11 | from copy import deepcopy 12 | from itertools import product 13 | from collections import defaultdict 14 | 15 | # Libs 16 | import numpy as np 17 | np.seterr(over='warn', divide='raise') 18 | 19 | # Package 20 | 21 | # Shortcuts 22 | rand = random.random 23 | inf = float('inf') 24 | 25 | ### FUNCTIONS ### 26 | 27 | def evaluate_individual((individual, evaluator)): 28 | if callable(evaluator): 29 | individual.stats = evaluator(individual) 30 | elif hasattr(evaluator, 'evaluate'): 31 | individual.stats = evaluator.evaluate(individual) 32 | else: 33 | raise Exception("Evaluator must be a callable or object" \ 34 | "with a callable attribute 'evaluate'.") 35 | return individual 36 | 37 | 38 | ### CLASSES ### 39 | 40 | class SimplePopulation(object): 41 | 42 | def __init__(self, geno_factory, 43 | popsize = 100, 44 | elitism = True, 45 | stop_when_solved=False, 46 | tournament_selection_k=3, 47 | verbose=True, 48 | max_cores=1): 49 | # Instance properties 50 | self.geno_factory = geno_factory 51 | self.popsize = popsize 52 | self.elitism = elitism 53 | self.stop_when_solved = stop_when_solved 54 | self.tournament_selection_k = tournament_selection_k 55 | self.verbose = verbose 56 | self.max_cores = max_cores 57 | 58 | cpus = multiprocessing.cpu_count() 59 | use_cores = min(self.max_cores, cpus-1) 60 | if use_cores > 1: 61 | self.pool = multiprocessing.Pool(processes=use_cores, maxtasksperchild=5) 62 | else: 63 | self.pool = None 64 | 65 | def _reset(self): 66 | """ Resets the state of this population. 67 | """ 68 | self.population = [] # List of species 69 | 70 | # Keep track of some history 71 | self.champions = [] 72 | self.generation = 0 73 | self.solved_at = None 74 | self.stats = defaultdict(list) 75 | 76 | def epoch(self, evaluator, generations, solution=None, reset=True, callback=None): 77 | """ Runs an evolutionary epoch 78 | 79 | :param evaluator: Either a function or an object with a function 80 | named 'evaluate' that returns a given individual's 81 | fitness. 82 | :param callback: Function that is called at the end of each generation. 83 | """ 84 | if reset: 85 | self._reset() 86 | 87 | for _ in xrange(generations): 88 | self._evolve(evaluator, solution) 89 | 90 | self.generation += 1 91 | 92 | if self.verbose: 93 | self._status_report() 94 | 95 | if callback is not None: 96 | callback(self) 97 | 98 | if self.solved_at is not None and self.stop_when_solved: 99 | break 100 | 101 | return {'stats': self.stats, 'champions': self.champions} 102 | 103 | 104 | def _evolve(self, evaluator, solution=None): 105 | """ Runs a single step of evolution. 106 | """ 107 | 108 | pop = self._birth() 109 | pop = self._evaluate_all(pop, evaluator) 110 | self._find_best(pop, solution) 111 | pop = self._reproduce(pop) 112 | self._gather_stats(pop) 113 | 114 | self.population = pop 115 | 116 | def _birth(self): 117 | """ Creates a population if there is none, returns 118 | current population otherwise. 119 | """ 120 | while len(self.population) < self.popsize: 121 | individual = self.geno_factory() 122 | self.population.append(individual) 123 | 124 | return self.population 125 | 126 | def _evaluate_all(self, pop, evaluator): 127 | """ Evaluates all of the individuals in given pop, 128 | and assigns their "stats" property. 129 | """ 130 | to_eval = [(individual, evaluator) for individual in pop] 131 | if self.pool is not None: 132 | print "Running in %d processes." % self.pool._processes 133 | pop = self.pool.map(evaluate_individual, to_eval) 134 | else: 135 | print "Running in single process." 136 | pop = map(evaluate_individual, to_eval) 137 | 138 | return pop 139 | 140 | def _find_best(self, pop, solution=None): 141 | """ Finds the best individual, and adds it to the champions, also 142 | checks if this best individual 'solves' the problem. 143 | """ 144 | ## CHAMPION 145 | self.champions.append(max(pop, key=lambda ind: ind.stats['fitness'])) 146 | 147 | ## SOLUTION CRITERION 148 | if solution is not None: 149 | if isinstance(solution, (int, float)): 150 | solved = (self.champions[-1].stats['fitness'] >= solution) 151 | elif callable(solution): 152 | solved = solution(self.champions[-1]) 153 | elif hasattr(solution, 'solve'): 154 | solved = solution.solve(self.champions[-1]) 155 | else: 156 | raise Exception("Solution checker must be a threshold fitness value,"\ 157 | "a callable, or an object with a method 'solve'.") 158 | 159 | if solved and self.solved_at is None: 160 | self.solved_at = self.generation 161 | 162 | def _reproduce(self, pop): 163 | """ Reproduces (and mutates) the best individuals to create a new population. 164 | """ 165 | newpop = [] 166 | 167 | if self.elitism: 168 | newpop.append(self.champions[-1]) 169 | 170 | while len(newpop) < self.popsize: 171 | # Perform tournament selection 172 | k = min(self.tournament_selection_k, len(pop)) 173 | winner = max(random.sample(pop, k), key=lambda ind:ind.stats['fitness']) 174 | winner = deepcopy(winner).mutate() 175 | newpop.append(winner) 176 | 177 | return newpop 178 | 179 | def _gather_stats(self, pop): 180 | """ Collects avg and max of individuals' stats (incl. fitness). 181 | """ 182 | for key in pop[0].stats: 183 | self.stats[key+'_avg'].append(np.mean([ind.stats[key] for ind in pop])) 184 | self.stats[key+'_max'].append(np.max([ind.stats[key] for ind in pop])) 185 | self.stats[key+'_min'].append(np.min([ind.stats[key] for ind in pop])) 186 | self.stats['solved'].append( self.solved_at is not None ) 187 | 188 | def _status_report(self): 189 | """ Prints a status report """ 190 | print "\n== Generation %d ==" % self.generation 191 | print "Best (%.2f): %s %s" % (self.champions[-1].stats['fitness'], self.champions[-1], self.champions[-1].stats) 192 | print "Solved: %s" % (self.solved_at) 193 | -------------------------------------------------------------------------------- /peas/methods/hyperneat.py: -------------------------------------------------------------------------------- 1 | """ Implements HyperNEAT's conversion 2 | from genotype to phenotype. 3 | """ 4 | 5 | ### IMPORTS ### 6 | from itertools import product, izip 7 | 8 | # Libs 9 | import numpy as np 10 | 11 | # Local 12 | from ..networks.rnn import NeuralNetwork 13 | 14 | # Shortcuts 15 | inf = float('inf') 16 | 17 | 18 | class Substrate(object): 19 | """ Represents a substrate, that is a configuration 20 | of nodes without connection weights. Connectivity 21 | is defined, and connection weights are later 22 | determined by HyperNEAT or another method. 23 | """ 24 | def __init__(self, nodes_or_shape=None): 25 | """ Constructor, pass either a shape (as in numpy.array.shape) 26 | or a list of node positions. Dimensionality is determined from 27 | the length of the shape, or from the length of the node position 28 | vectors. 29 | """ 30 | self.nodes = None 31 | self.is_input = [] 32 | self.num_nodes = 0 33 | self.layers = {} 34 | self.connections = [] 35 | self.connection_ids = [] 36 | self.linkexpression_ids = [] 37 | # If a shape is passed, create a mesh grid of nodes. 38 | if nodes_or_shape is not None: 39 | self.add_nodes(nodes_or_shape, 'a') 40 | self.add_connections('a', 'a') 41 | 42 | 43 | def add_nodes(self, nodes_or_shape, layer_id='a', is_input=False): 44 | """ Add the given nodes (list) or shape (tuple) 45 | and assign the given id/name. 46 | """ 47 | if type(nodes_or_shape) == list: 48 | newnodes = np.array(nodes_or_shape) 49 | 50 | elif type(nodes_or_shape) == tuple: 51 | # Create coordinate grids 52 | newnodes = np.mgrid[[slice(-1, 1, s*1j) for s in nodes_or_shape]] 53 | # Move coordinates to last dimension 54 | newnodes = newnodes.transpose(range(1,len(nodes_or_shape)+1) + [0]) 55 | # Reshape to a N x nD list. 56 | newnodes = newnodes.reshape(-1, len(nodes_or_shape)) 57 | self.dimensions = len(nodes_or_shape) 58 | 59 | elif type(nodes_or_shape) == np.ndarray: 60 | pass # all is good 61 | 62 | else: 63 | raise Exception("nodes_or_shape must be a list of nodes or a shape tuple.") 64 | 65 | if self.nodes is None: 66 | self.dimensions = newnodes.shape[1] 67 | self.nodes = np.zeros((0, self.dimensions)) 68 | 69 | # keep a dictionary with the set of node IDs for each layer_id 70 | ids = self.layers.get(layer_id, set()) 71 | ids |= set(range(len(self.nodes), len(self.nodes) + len(newnodes))) 72 | self.layers[layer_id] = ids 73 | 74 | # append the new nodes 75 | self.nodes = np.vstack((self.nodes, newnodes)) 76 | self.num_nodes += len(newnodes) 77 | 78 | def add_connections(self, from_layer='a', to_layer='a', connection_id=-1, max_length=inf, link_expression_id=None): 79 | """ Connect all nodes in the from_layer to all nodes in the to_layer. 80 | A maximum connection length can be given to limit the number of connections, 81 | manhattan distance is used. 82 | HyperNEAT uses the connection_id to determine which CPPN output node 83 | to use for the weight. 84 | """ 85 | conns = product( self.layers[from_layer], self.layers[to_layer] ) 86 | conns = filter(lambda (fr, to): np.all(np.abs(self.nodes[fr] - self.nodes[to]) <= max_length), conns) 87 | self.connections.extend(conns) 88 | self.connection_ids.extend([connection_id] * len(conns)) 89 | self.linkexpression_ids.extend([link_expression_id] * len(conns)) 90 | 91 | def get_connection_list(self, add_deltas): 92 | """ Builds the connection list only once. 93 | Storing this is a bit of time/memory tradeoff. 94 | """ 95 | if not hasattr(self, '_connection_list'): 96 | 97 | self._connection_list = [] 98 | for ((i, j), conn_id, expr_id) in izip(self.connections, self.connection_ids, self.linkexpression_ids): 99 | fr = self.nodes[i] 100 | to = self.nodes[j] 101 | if add_deltas: 102 | conn = np.hstack((fr, to, to-fr)) 103 | else: 104 | conn = np.hstack((fr, to)) 105 | self._connection_list.append(((i, j), conn, conn_id, expr_id)) 106 | 107 | return self._connection_list 108 | 109 | class HyperNEATDeveloper(object): 110 | 111 | """ HyperNEAT developer object.""" 112 | 113 | def __init__(self, substrate, 114 | sandwich=False, 115 | feedforward=False, 116 | add_deltas=False, 117 | weight_range=3.0, 118 | min_weight=0.3, 119 | activation_steps=10, 120 | node_type='tanh'): 121 | """ Constructor 122 | 123 | :param substrate: A substrate object 124 | :param weight_range: (min, max) of substrate weights 125 | :param min_weight: The minimum CPPN output value that will lead to an expressed connection. 126 | :param sandwich: Whether to turn the output net into a sandwich network. 127 | :param feedforward: Whether to turn the output net into a feedforward network. 128 | :param node_type: What node type to assign to the output nodes. 129 | """ 130 | self.substrate = substrate 131 | self.sandwich = sandwich 132 | self.feedforward = feedforward 133 | self.add_deltas = add_deltas 134 | self.weight_range = weight_range 135 | self.min_weight = min_weight 136 | self.activation_steps = activation_steps 137 | self.node_type = node_type 138 | 139 | 140 | def convert(self, network): 141 | """ Performs conversion. 142 | 143 | :param network: Any object that is convertible to a :class:`~peas.networks.NeuralNetwork`. 144 | """ 145 | 146 | # Cast input to a neuralnetwork if it isn't 147 | if not isinstance(network, NeuralNetwork): 148 | network = NeuralNetwork(network) 149 | 150 | # Since Stanley mentions to "fully activate" the CPPN, 151 | # I assume this means it's a feedforward net, since otherwise 152 | # there is no clear definition of "full activation". 153 | # In an FF network, activating each node once leads to a stable condition. 154 | 155 | # Check if the network has enough inputs. 156 | required_inputs = 2 * self.substrate.dimensions + 1 157 | if self.add_deltas: 158 | required_inputs += self.substrate.dimensions 159 | if network.cm.shape[0] <= required_inputs: 160 | raise Exception("Network does not have enough inputs. Has %d, needs %d" % 161 | (network.cm.shape[0], cm_dims+1)) 162 | 163 | # Initialize connectivity matrix 164 | cm = np.zeros((self.substrate.num_nodes, self.substrate.num_nodes)) 165 | 166 | for (i,j), coords, conn_id, expr_id in self.substrate.get_connection_list(self.add_deltas): 167 | expression = True 168 | if expr_id is not None: 169 | network.flush() 170 | expression = network.feed(coords, self.activation_steps)[expr_id] > 0 171 | if expression: 172 | network.flush() 173 | weight = network.feed(coords, self.activation_steps)[conn_id] 174 | cm[j, i] = weight 175 | 176 | # Rescale the CM 177 | cm[np.abs(cm) < self.min_weight] = 0 178 | cm -= (np.sign(cm) * self.min_weight) 179 | cm *= self.weight_range / (self.weight_range - self.min_weight) 180 | 181 | # Clip highest weights 182 | cm = np.clip(cm, -self.weight_range, self.weight_range) 183 | net = NeuralNetwork().from_matrix(cm, node_types=[self.node_type]) 184 | 185 | if self.sandwich: 186 | net.make_sandwich() 187 | 188 | if self.feedforward: 189 | net.make_feedforward() 190 | 191 | if not np.all(np.isfinite(net.cm)): 192 | raise Exception("Network contains NaN/inf weights.") 193 | 194 | return net 195 | 196 | -------------------------------------------------------------------------------- /peas/methods/neat.py: -------------------------------------------------------------------------------- 1 | """ This module implements the different genotypes and evolutionary 2 | methods used in NEAT and HyperNEAT. It is meant to be a reference 3 | implementation, so any inneficiencies are copied as they are 4 | described. 5 | """ 6 | 7 | ### IMPORTS ### 8 | import sys 9 | import random 10 | from copy import deepcopy 11 | from itertools import product 12 | from collections import defaultdict 13 | 14 | # Libs 15 | import numpy as np 16 | np.seterr(over='warn', divide='raise') 17 | 18 | # Package 19 | from .evolution import SimplePopulation 20 | from ..networks.rnn import NeuralNetwork 21 | 22 | # Shortcuts 23 | rand = random.random 24 | inf = float('inf') 25 | 26 | 27 | ### CLASSES ### 28 | 29 | class NEATGenotype(object): 30 | """ Implements the NEAT genotype, consisting of 31 | node genes and connection genes. 32 | """ 33 | def __init__(self, 34 | inputs=2, 35 | outputs=1, 36 | types=['tanh'], 37 | topology=None, 38 | feedforward=True, 39 | max_depth=None, 40 | max_nodes=inf, 41 | response_default=4.924273, 42 | initial_weight_stdev=2.0, 43 | bias_as_node=False, 44 | prob_add_node=0.03, 45 | prob_add_conn=0.3, 46 | prob_mutate_weight=0.8, 47 | prob_reset_weight=0.1, 48 | prob_reenable_conn=0.01, 49 | prob_disable_conn=0.01, 50 | prob_reenable_parent=0.25, 51 | prob_mutate_bias=0.2, 52 | prob_mutate_response=0.0, 53 | prob_mutate_type=0.2, 54 | stdev_mutate_weight=1.5, 55 | stdev_mutate_bias=0.5, 56 | stdev_mutate_response=0.5, 57 | weight_range=(-50., 50.), 58 | distance_excess=1.0, 59 | distance_disjoint=1.0, 60 | distance_weight=0.4): 61 | """ Refer to the NEAT paper for an explanation of these parameters. 62 | """ 63 | 64 | # Settings 65 | self.inputs = inputs 66 | self.outputs = outputs 67 | 68 | # Restrictions 69 | self.types = types 70 | self.feedforward = feedforward 71 | self.max_depth = max_depth 72 | self.max_nodes = max_nodes 73 | 74 | # Settings 75 | self.response_default = response_default 76 | self.initial_weight_stdev = initial_weight_stdev 77 | self.bias_as_node = bias_as_node 78 | 79 | # Mutation probabilities 80 | self.prob_add_conn = prob_add_conn 81 | self.prob_add_node = prob_add_node 82 | self.prob_mutate_weight = prob_mutate_weight 83 | self.prob_reset_weight = prob_reset_weight 84 | self.prob_reenable_conn = prob_reenable_conn 85 | self.prob_disable_conn = prob_disable_conn 86 | self.prob_reenable_parent = prob_reenable_parent 87 | self.prob_mutate_bias = prob_mutate_bias 88 | self.prob_mutate_response = prob_mutate_response 89 | self.prob_mutate_type = prob_mutate_type 90 | 91 | # Mutation effects 92 | self.stdev_mutate_weight = stdev_mutate_weight 93 | self.stdev_mutate_bias = stdev_mutate_bias 94 | self.stdev_mutate_response = stdev_mutate_response 95 | self.weight_range = weight_range 96 | 97 | # Species distance measure 98 | self.distance_excess = distance_excess 99 | self.distance_disjoint = distance_disjoint 100 | self.distance_weight = distance_weight 101 | 102 | self.node_genes = [] #: Tuples of (fforder, type, bias, response, layer) 103 | self.conn_genes = {} #: Tuples of (innov, from, to, weight, enabled) 104 | 105 | 106 | if self.bias_as_node: 107 | self.inputs += 1 108 | 109 | max_layer = sys.maxint if (self.max_depth is None) else (self.max_depth - 1) 110 | 111 | if topology is None: 112 | # The default method of creating a genotype 113 | # is to create a so called "fully connected" 114 | # genotype, i.e., connect all input nodes 115 | # to the output node. 116 | 117 | # Create input nodes 118 | for i in xrange(self.inputs): 119 | # We set the 'response' to 4.924273. Stanley doesn't mention having the response 120 | # be subject to evolution, so this is #weird, but we'll do it because neat-python does. 121 | self.node_genes.append( [i * 1024.0, types[0], 0.0, self.response_default, 0] ) 122 | 123 | # Create output nodes 124 | for i in xrange(self.outputs): 125 | self.node_genes.append( [(self.inputs + i) * 1024.0, random.choice(self.types), 126 | 0.0, self.response_default, max_layer] ) 127 | 128 | # Create connections from each input to each output 129 | innov = 0 130 | for i in xrange(self.inputs): 131 | for j in xrange(self.inputs, self.inputs + self.outputs): 132 | self.conn_genes[(i, j)] = [innov, i, j, np.random.normal(0.0, self.initial_weight_stdev), True] 133 | innov += 1 134 | else: 135 | # If an initial topology is given, use that: 136 | fr, to = zip(*topology) 137 | maxnode = max(max(fr), max(to)) 138 | 139 | if maxnode + 1 < inputs + outputs: 140 | raise Exception("Topology (%d) contains fewer than inputs (%d) + outputs (%d) nodes." % 141 | (maxnode, inputs, outputs)) 142 | 143 | for i in xrange(maxnode+1): 144 | # Assign layer 0 to input nodes, otherwise just an incrementing number, 145 | # i.e. each node is on its own layer. 146 | layer = 0 if i < inputs else i + 1 147 | self.node_genes.append( [i * 1024.0, random.choice(self.types), 0.0, self.response_default, layer] ) 148 | innov = 0 149 | for fr, to in topology: 150 | self.conn_genes[(fr, to)] = [innov, fr, to, np.random.normal(0.0, self.initial_weight_stdev), True] 151 | innov += 1 152 | 153 | def mutate(self, innovations={}, global_innov=0): 154 | """ Perform a mutation operation on this genotype. 155 | If a dict with innovations is passed in, any 156 | add connection operations will be added to it, 157 | and checked to ensure identical innovation numbers. 158 | """ 159 | maxinnov = max(global_innov, max(cg[0] for cg in self.conn_genes.values())) 160 | 161 | if len(self.node_genes) < self.max_nodes and rand() < self.prob_add_node: 162 | possible_to_split = self.conn_genes.keys() 163 | # If there is a max depth, we can only split connections that skip a layer. 164 | # E.g. we can split a connection from layer 0 to layer 2, because the resulting 165 | # node would be in layer 1. We cannot split a connection from layer 1 to layer 2, 166 | # because that would create an intra-layer connection. 167 | if self.max_depth is not None: 168 | possible_to_split = [(fr, to) for (fr, to) in possible_to_split if 169 | self.node_genes[fr][4] + 1 < self.node_genes[to][4]] 170 | if possible_to_split: 171 | to_split = self.conn_genes[random.choice(possible_to_split)] 172 | to_split[4] = False # Disable the old connection 173 | fr, to, w = to_split[1:4] 174 | avg_fforder = (self.node_genes[fr][0] + self.node_genes[to][0]) * 0.5 175 | # We assign a random function type to the node, which is #weird 176 | # because I thought that in NEAT these kind of mutations 177 | # initially don't affect the functionality of the network. 178 | new_type = random.choice(self.types) 179 | # We assign a 'layer' to the new node that is one lower than the target of the connection 180 | layer = self.node_genes[fr][4] + 1 181 | node_gene = [avg_fforder, new_type, 0.0, self.response_default, layer] 182 | new_id = len(self.node_genes) 183 | self.node_genes.append(node_gene) 184 | 185 | if (fr, new_id) in innovations: 186 | innov = innovations[(fr, new_id)] 187 | else: 188 | maxinnov += 1 189 | innov = innovations[(fr, new_id)] = maxinnov 190 | self.conn_genes[(fr, new_id)] = [innov, fr, new_id, 1.0, True] 191 | 192 | if (new_id, to) in innovations: 193 | innov = innovations[(new_id, to)] 194 | else: 195 | maxinnov += 1 196 | innov = innovations[(new_id, to)] = maxinnov 197 | self.conn_genes[(new_id, to)] = [innov, new_id, to, w, True] 198 | 199 | # This is #weird, why use "elif"? but this is what 200 | # neat-python does, so I'm copying. 201 | elif rand() < self.prob_add_conn: 202 | potential_conns = product(xrange(len(self.node_genes)), xrange(self.inputs, len(self.node_genes))) 203 | potential_conns = (c for c in potential_conns if c not in self.conn_genes) 204 | # Filter further connections if we're looking only for FF networks 205 | if self.feedforward: 206 | potential_conns = ((f, t) for (f, t) in potential_conns if 207 | self.node_genes[f][0] < self.node_genes[t][0]) # Check FFOrder 208 | # Don't create intra-layer connections if there is a max_depth 209 | if self.max_depth is not None: 210 | potential_conns = ((f, t) for (f, t) in potential_conns if 211 | self.node_genes[f][4] < self.node_genes[t][4]) # Check Layer 212 | potential_conns = list(potential_conns) 213 | # If any potential connections are left 214 | if potential_conns: 215 | (fr, to) = random.choice(potential_conns) 216 | # Check if this innovation was already made, otherwise assign max + 1 217 | if (fr, to) in innovations: 218 | innov = innovations[(fr, to)] 219 | else: 220 | maxinnov += 1 221 | innov = innovations[(fr, to)] = maxinnov 222 | conn_gene = [innov, fr, to, np.random.normal(0, self.stdev_mutate_weight), True] 223 | self.conn_genes[(fr, to)] = conn_gene 224 | 225 | else: 226 | for cg in self.conn_genes.values(): 227 | if rand() < self.prob_mutate_weight: 228 | cg[3] += np.random.normal(0, self.stdev_mutate_weight) 229 | cg[3] = np.clip(cg[3], self.weight_range[0], self.weight_range[1]) 230 | 231 | if rand() < self.prob_reset_weight: 232 | cg[3] = np.random.normal(0, self.stdev_mutate_weight) 233 | 234 | if rand() < self.prob_reenable_conn: 235 | cg[4] = True 236 | 237 | if rand() < self.prob_disable_conn: 238 | cg[4] = False 239 | 240 | # Mutate non-input nodes 241 | for node_gene in self.node_genes[self.inputs:]: 242 | if rand() < self.prob_mutate_bias: 243 | node_gene[2] += np.random.normal(0, self.stdev_mutate_bias) 244 | node_gene[2] = np.clip(node_gene[2], self.weight_range[0], self.weight_range[1]) 245 | 246 | if rand() < self.prob_mutate_type: 247 | node_gene[1] = random.choice(self.types) 248 | 249 | if rand() < self.prob_mutate_response: 250 | node_gene[3] += np.random.normal(0, self.stdev_mutate_response) 251 | 252 | for (fr, to) in self.conn_genes: 253 | if self.node_genes[to][4] == 0: 254 | raise Exception("Connection TO input node not allowed.") 255 | return self # For chaining 256 | 257 | def mate(self, other): 258 | """ Performs crossover between this genotype and another, 259 | and returns the child 260 | """ 261 | child = deepcopy(self) 262 | child.node_genes = [] 263 | child.conn_genes = {} 264 | 265 | # Select node genes from parents 266 | maxnodes = max(len(self.node_genes), len(other.node_genes)) 267 | minnodes = min(len(self.node_genes), len(other.node_genes)) 268 | for i in range(maxnodes): 269 | ng = None 270 | if i < minnodes: 271 | ng = random.choice((self.node_genes[i], other.node_genes[i])) 272 | else: 273 | try: 274 | ng = self.node_genes[i] 275 | except IndexError: 276 | ng = other.node_genes[i] 277 | child.node_genes.append(deepcopy(ng)) 278 | 279 | # index the connections by innov numbers 280 | self_conns = dict( ((c[0], c) for c in self.conn_genes.values()) ) 281 | other_conns = dict( ((c[0], c) for c in other.conn_genes.values()) ) 282 | maxinnov = max( self_conns.keys() + other_conns.keys() ) 283 | 284 | for i in range(maxinnov+1): 285 | cg = None 286 | if i in self_conns and i in other_conns: 287 | cg = random.choice((self_conns[i], other_conns[i])) 288 | enabled = self_conns[i][4] and other_conns[i][4] 289 | else: 290 | if i in self_conns: 291 | cg = self_conns[i] 292 | enabled = cg[4] 293 | elif i in other_conns: 294 | cg = other_conns[i] 295 | enabled = cg[4] 296 | if cg is not None: 297 | child.conn_genes[(cg[1], cg[2])] = deepcopy(cg) 298 | child.conn_genes[(cg[1], cg[2])][4] = enabled or rand() < self.prob_reenable_parent 299 | 300 | # Filter out connections that would become recursive in the new individual. 301 | def is_feedforward(((fr, to), cg)): 302 | return child.node_genes[fr][0] < child.node_genes[to][0] 303 | 304 | if self.feedforward: 305 | child.conn_genes = dict(filter(is_feedforward, child.conn_genes.items())) 306 | 307 | return child 308 | 309 | def distance(self, other): 310 | """ NEAT's compatibility distance 311 | """ 312 | # index the connections by innov numbers 313 | self_conns = dict( ((c[0], c) for c in self.conn_genes.itervalues()) ) 314 | other_conns = dict( ((c[0], c) for c in other.conn_genes.itervalues()) ) 315 | # Select connection genes from parents 316 | allinnovs = self_conns.keys() + other_conns.keys() 317 | mininnov = min(allinnovs) 318 | 319 | e = 0 320 | d = 0 321 | w = 0.0 322 | m = 0 323 | 324 | for i in allinnovs: 325 | if i in self_conns and i in other_conns: 326 | w += np.abs(self_conns[i][3] - other_conns[i][3]) 327 | m += 1 328 | elif i in self_conns or i in other_conns: 329 | if i < mininnov: 330 | d += 1 # Disjoint 331 | else: 332 | e += 1 # Excess 333 | else: 334 | raise Exception("NEITHERHWATHWATHAWTHA") 335 | 336 | w = (w / m) if m > 0 else w 337 | 338 | return (self.distance_excess * e + 339 | self.distance_disjoint * d + 340 | self.distance_weight * w) 341 | 342 | def get_network_data(self): 343 | """ Returns a tuple of (connection_matrix, node_types) 344 | that is reordered by the "feed-forward order" of the network, 345 | Such that if feedforward was set to true, the matrix will be 346 | lower-triangular. 347 | The node bias is inserted as "node 0", the leftmost column 348 | of the matrix. 349 | """ 350 | 351 | # Assemble connectivity matrix 352 | cm = np.zeros((len(self.node_genes), len(self.node_genes))) 353 | cm.fill(np.nan) 354 | for (_, fr, to, weight, enabled) in self.conn_genes.itervalues(): 355 | if enabled: 356 | cm[to, fr] = weight 357 | 358 | # Reorder the nodes/connections 359 | ff, node_types, bias, response, layer = zip(*self.node_genes) 360 | order = [i for _,i in sorted(zip(ff, xrange(len(ff))))] 361 | cm = cm[:,order][order,:] 362 | node_types = np.array(node_types)[order] 363 | bias = np.array(bias)[order] 364 | response = np.array(response)[order] 365 | layers = np.array(layer)[order] 366 | 367 | # Then, we multiply all the incoming connection weights by the response 368 | cm *= np.atleast_2d(response).T 369 | # Finally, add the bias as incoming weights from node-0 370 | if not self.bias_as_node: 371 | cm = np.hstack( (np.atleast_2d(bias).T, cm) ) 372 | cm = np.insert(cm, 0, 0.0, axis=0) 373 | # TODO: this is a bit ugly, we duplicate the first node type for 374 | # bias node. It shouldn't matter though since the bias is used as an input. 375 | node_types = [node_types[0]] + list(node_types) 376 | 377 | if self.feedforward and np.triu(np.nan_to_num(cm)).any(): 378 | import pprint 379 | pprint.pprint(self.node_genes) 380 | pprint.pprint(self.conn_genes) 381 | print ff 382 | print order 383 | print np.sign(cm) 384 | raise Exception("Network is not feedforward.") 385 | 386 | return cm, node_types 387 | 388 | def __str__(self): 389 | return '%s with %d nodes and %d connections.' % (self.__class__.__name__, 390 | len(self.node_genes), len(self.conn_genes)) 391 | 392 | def visualize(self, filename): 393 | return NeuralNetwork(self).visualize(filename, inputs=self.inputs, outputs=self.outputs) 394 | 395 | class NEATSpecies(object): 396 | 397 | def __init__(self, initial_member): 398 | self.members = [initial_member] 399 | self.representative = initial_member 400 | self.offspring = 0 401 | self.age = 0 402 | self.avg_fitness = 0. 403 | self.max_fitness = 0. 404 | self.max_fitness_prev = 0. 405 | self.no_improvement_age = 0 406 | self.has_best = False 407 | 408 | class NEATPopulation(SimplePopulation): 409 | """ A population object for NEAT, it contains the selection 410 | and reproduction methods. 411 | """ 412 | def __init__(self, geno_factory, 413 | compatibility_threshold=3.0, 414 | compatibility_threshold_delta=0.4, 415 | target_species=12, 416 | min_elitism_size=5, 417 | young_age=10, 418 | young_multiplier=1.2, 419 | stagnation_age=15, 420 | old_age=30, 421 | old_multiplier=0.2, 422 | reset_innovations=False, 423 | survival=0.2, 424 | **kwargs): 425 | """ Initializes the object with settings, 426 | does not create a population yet. 427 | 428 | :param geno_factory: A callable (function or object) that returns 429 | a new instance of a genotype. 430 | 431 | """ 432 | super(NEATPopulation, self).__init__(geno_factory, **kwargs) 433 | 434 | self.compatibility_threshold = compatibility_threshold 435 | self.compatibility_threshold_delta = compatibility_threshold_delta 436 | self.target_species = target_species 437 | self.reset_innovations = reset_innovations 438 | self.survival = survival 439 | self.young_age = young_age 440 | self.young_multiplier = young_multiplier 441 | self.old_age = old_age 442 | self.old_multiplier = old_multiplier 443 | self.stagnation_age = stagnation_age 444 | self.min_elitism_size = min_elitism_size 445 | 446 | 447 | def _reset(self): 448 | """ Resets the state of this population. 449 | """ 450 | # Keep track of some history 451 | self.champions = [] 452 | self.generation = 0 453 | self.solved_at = None 454 | self.stats = defaultdict(list) 455 | 456 | # Neat specific: 457 | self.species = [] # List of species 458 | self.global_innov = 0 459 | self.innovations = {} # Keep track of global innovations 460 | self.current_compatibility_threshold = self.compatibility_threshold 461 | 462 | @property 463 | def population(self): 464 | for specie in self.species: 465 | for member in specie.members: 466 | yield member 467 | 468 | def _evolve(self, evaluator, solution=None): 469 | """ A single evolutionary step . 470 | """ 471 | # Unpack species 472 | pop = list(self.population) 473 | 474 | ## INITIAL BIRTH 475 | while len(pop) < self.popsize: 476 | individual = self.geno_factory() 477 | pop.append(individual) 478 | 479 | ## EVALUATE 480 | pop = self._evaluate_all(pop, evaluator) 481 | 482 | ## SPECIATE 483 | # Select random representatives 484 | for specie in self.species: 485 | specie.representative = random.choice(specie.members) 486 | specie.members = [] 487 | specie.age += 1 488 | # Add all individuals to a species 489 | for individual in pop: 490 | found = False 491 | for specie in self.species: 492 | if individual.distance(specie.representative) <= self.current_compatibility_threshold: 493 | specie.members.append(individual) 494 | found = True 495 | break 496 | # Create a new species 497 | if not found: 498 | s = NEATSpecies(individual) 499 | self.species.append(s) 500 | 501 | # Remove empty species 502 | self.species = filter(lambda s: len(s.members) > 0, self.species) 503 | 504 | # Ajust compatibility_threshold 505 | if len(self.species) < self.target_species: 506 | self.current_compatibility_threshold -= self.compatibility_threshold_delta 507 | elif len(self.species) > self.target_species: 508 | self.current_compatibility_threshold += self.compatibility_threshold_delta 509 | 510 | ## FIND CHAMPION / CHECK FOR SOLUTION 511 | self._find_best(pop, solution) 512 | 513 | ## REPRODUCE 514 | 515 | for specie in self.species: 516 | specie.max_fitness_prev = specie.max_fitness 517 | specie.avg_fitness = np.mean( [ind.stats['fitness'] for ind in specie.members] ) 518 | specie.max_fitness = np.max( [ind.stats['fitness'] for ind in specie.members] ) 519 | if specie.max_fitness <= specie.max_fitness_prev: 520 | specie.no_improvement_age += 1 521 | else: 522 | specie.no_improvement_age = 0 523 | specie.has_best = self.champions[-1] in specie.members 524 | 525 | # Remove stagnated species 526 | # This is implemented as in neat-python, which resets the 527 | # no_improvement_age when the average increases 528 | self.species = filter(lambda s: s.no_improvement_age < self.stagnation_age or s.has_best, self.species) 529 | 530 | # Average fitness of each species 531 | avg_fitness = np.array([specie.avg_fitness for specie in self.species]) 532 | 533 | # Adjust based on age 534 | age = np.array([specie.age for specie in self.species]) 535 | for specie in self.species: 536 | if specie.age < self.young_age: 537 | specie.avg_fitness *= self.young_multiplier 538 | if specie.age > self.old_age: 539 | specie.avg_fitness *= self.old_multiplier 540 | 541 | # Compute offspring amount 542 | total_average = sum(specie.avg_fitness for specie in self.species) 543 | for specie in self.species: 544 | specie.offspring = int(round(self.popsize * specie.avg_fitness / total_average)) 545 | 546 | # Remove species without offspring 547 | self.species = filter(lambda s: s.offspring > 0, self.species) 548 | 549 | # Produce offspring 550 | # Stanley says he resets the innovations each generation, but 551 | # neat-python keeps a global list. 552 | # This switch controls which behavior to simulate. 553 | if self.reset_innovations: 554 | self.innovations = dict() 555 | for specie in self.species: 556 | # First we keep only the best individuals of each species 557 | specie.members.sort(key=lambda ind: ind.stats['fitness'], reverse=True) 558 | keep = max(1, int(round(len(specie.members) * self.survival))) 559 | pool = specie.members[:keep] 560 | # Keep one if elitism is set and the size of the species is more than 561 | # that indicated in the settings 562 | if self.elitism and len(specie.members) > self.min_elitism_size: 563 | specie.members = specie.members[:1] 564 | else: 565 | specie.members = [] 566 | # Produce offspring: 567 | while len(specie.members) < specie.offspring: 568 | # Perform tournament selection 569 | k = min(len(pool), self.tournament_selection_k) 570 | p1 = max(random.sample(pool, k), key=lambda ind: ind.stats['fitness']) 571 | p2 = max(random.sample(pool, k), key=lambda ind: ind.stats['fitness']) 572 | # Mate and mutate 573 | child = p1.mate(p2) 574 | child.mutate(innovations=self.innovations, global_innov=self.global_innov) 575 | specie.members.append(child) 576 | 577 | if self.innovations: 578 | self.global_innov = max(self.innovations.itervalues()) 579 | 580 | self._gather_stats(pop) 581 | 582 | def _status_report(self): 583 | """ Print a status report """ 584 | """ Prints a status report """ 585 | print "\n== Generation %d ==" % self.generation 586 | print "Best (%.2f): %s %s" % (self.champions[-1].stats['fitness'], self.champions[-1], self.champions[-1].stats) 587 | print "Solved: %s" % (self.solved_at) 588 | print "Species: %s" % ([len(s.members) for s in self.species]) 589 | print "Age: %s" % ([s.age for s in self.species]) 590 | print "No improvement: %s" % ([s.no_improvement_age for s in self.species]) 591 | 592 | -------------------------------------------------------------------------------- /peas/methods/neatpythonwrapper.py: -------------------------------------------------------------------------------- 1 | """ This module contains a wrapper for the NEAT implementation 2 | called "neat-python", found at http://code.google.com/p/neat-python/ 3 | 4 | It wraps the package's global config in object oriented 5 | variables. 6 | """ 7 | 8 | ### IMPORTS ### 9 | from __future__ import absolute_import 10 | import sys 11 | import os 12 | import pickle 13 | 14 | import numpy as np 15 | 16 | try: 17 | from neat import config, chromosome, genome, population 18 | except ImportError: 19 | print "This module requires neat-python to be installed,\n"\ 20 | "you can get it at http://code.google.com/p/neat-python/." 21 | raise 22 | 23 | class NEATPythonPopulation(object): 24 | """ A wrapper class for python-neat's Population 25 | """ 26 | def __init__(self, 27 | popsize = 200, 28 | 29 | input_nodes = 2, 30 | output_nodes = 1, 31 | fully_connected = 1, 32 | w_range = (-30, 30), 33 | feedforward = False, 34 | nn_activation = 'exp', 35 | hidden_nodes = 0, 36 | weight_stdev = 2.0, 37 | 38 | prob_addconn = 0.05, 39 | prob_addnode = 0.03, 40 | prob_mutatebias = 0.2, 41 | bias_mutation_power = 0.5, 42 | prob_mutate_weight = 0.9, 43 | weight_mutation_power = 1.5, 44 | prob_togglelink = 0.01, 45 | elitism = 1, 46 | 47 | compatibility_threshold = 3.0, 48 | compatibility_change = 0.0, 49 | excess_coeficient = 1.0, 50 | disjoint_coeficient = 1.0, 51 | weight_coeficient = 0.4, 52 | 53 | species_size = 10, 54 | survival_threshold = 0.2, 55 | old_threshold = 30, 56 | youth_threshold = 10, 57 | old_penalty = 0.2, 58 | youth_boost = 1.2, 59 | max_stagnation = 15, 60 | 61 | stop_when_solved=False, 62 | verbose=True): 63 | 64 | # Set config 65 | self.config = dict( 66 | pop_size = popsize, 67 | input_nodes = input_nodes, 68 | output_nodes = output_nodes, 69 | fully_connected = fully_connected, 70 | min_weight = w_range[0], max_weight = w_range[1], 71 | feedforward = feedforward, 72 | nn_activation = nn_activation, 73 | hidden_nodes = hidden_nodes, 74 | weight_stdev = weight_stdev, 75 | 76 | prob_addconn = prob_addconn, 77 | prob_addnode = prob_addnode, 78 | prob_mutatebias = prob_mutatebias, 79 | bias_mutation_power = bias_mutation_power, 80 | prob_mutate_weight = prob_mutate_weight, 81 | weight_mutation_power = weight_mutation_power, 82 | prob_togglelink = prob_togglelink, 83 | elitism = elitism, 84 | 85 | compatibility_threshold = compatibility_threshold, 86 | compatibility_change = compatibility_change, 87 | excess_coeficient = excess_coeficient, 88 | disjoint_coeficient = disjoint_coeficient, 89 | weight_coeficient = weight_coeficient, 90 | species_size = species_size, 91 | survival_threshold = survival_threshold, 92 | old_threshold = old_threshold, 93 | youth_threshold = youth_threshold, 94 | old_penalty = old_penalty, 95 | youth_boost = youth_boost, 96 | max_stagnation = max_stagnation 97 | ) 98 | 99 | self.stop_when_solved = stop_when_solved 100 | self.verbose = verbose 101 | 102 | def epoch(self, evaluator, generations, solution=None): 103 | # Set config 104 | chromosome.node_gene_type = genome.NodeGene 105 | for k, v in self.config.iteritems(): 106 | setattr(config.Config, k, v) 107 | 108 | # neat-python has a max fitness threshold, we can set it if 109 | # we want to stop the simulation there, otherwise set it to some 110 | # really large number 111 | if isinstance(solution, (int, float)) and self.stop_when_solved: 112 | config.Config.max_fitness_threshold = solution 113 | else: 114 | config.Config.max_fitness_threshold = sys.float_info.max 115 | 116 | self.pop = population.Population() 117 | 118 | def evaluate_all(population): 119 | """ Adapter for python-neat, which expects a function that 120 | evaluates all individuals and assigns a .fitness property 121 | """ 122 | for individual in population: 123 | individual.fitness = evaluator(individual) 124 | return [individual.fitness for individual in population] 125 | 126 | population.Population.evaluate = evaluate_all 127 | self.pop.epoch(generations, report=self.verbose, save_best=True, checkpoint_interval=None) 128 | # Find the timestep when the problem was solved 129 | i = 0 130 | self.champions = [] 131 | while os.path.exists('best_chromo_%d' % (i)): 132 | # Load the champions and delete them 133 | f = open('best_chromo_%d' % (i), 'rb') 134 | self.champions.append(pickle.load(f)) 135 | f.close() 136 | os.remove('best_chromo_%d' % (i)) 137 | # Check if champion solves problem 138 | solved = False 139 | if solution is not None: 140 | if isinstance(solution, (int, float)): 141 | solved = (self.champions[-1].neat_fitness >= solution) 142 | elif callable(solution): 143 | solved = solution(self.champions[-1]) 144 | elif hasattr(solution, 'solve'): 145 | solved = solution.solve(self.champions[-1]) 146 | else: 147 | raise Exception("Solution checker must be a threshold fitness value,"\ 148 | "a callable, or an object with a method 'solve'.") 149 | self.champions[-1].solved = solved 150 | i += 1 151 | 152 | self.stats = {} 153 | self.stats['fitness_max'] = np.array([individual.fitness for individual in self.champions]) 154 | self.stats['fitness_avg'] = self.pop.stats[1] 155 | self.stats['solved'] = np.array([individual.solved for individual in self.champions]) 156 | 157 | return {'stats': self.stats, 'champions': self.champions} 158 | -------------------------------------------------------------------------------- /peas/methods/reaction.py: -------------------------------------------------------------------------------- 1 | """ Implements different development modules to convert 2 | from genotype to phenotype (in)directly. 3 | """ 4 | 5 | ### IMPORTS ### 6 | 7 | # Libs 8 | import numpy as np 9 | import scipy.ndimage.filters 10 | 11 | # Local 12 | from ..methods.neat import NEATGenotype 13 | from ..networks import NeuralNetwork 14 | 15 | np.seterr(divide='raise') 16 | 17 | 18 | ### CLASSES ### 19 | 20 | class ReactionDiffusionGenotype(object): 21 | 22 | def __init__(self, num_chemicals=3): 23 | pass 24 | 25 | class ReactionDeveloper(object): 26 | """ Developer that converts a genotype into 27 | a network using a HyperNEAT-like indirect 28 | encoding. 29 | """ 30 | def __init__(self, substrate_shape=(10,), cm_range=(-30., 30.), reaction_steps=5, sandwich=False, 31 | diffusion=0.0, recursion=0.0): 32 | # Instance vars 33 | self.substrate_shape = substrate_shape 34 | self.cm_range = cm_range 35 | self.reaction_steps = reaction_steps 36 | self.diffusion = diffusion 37 | self.sandwich = sandwich 38 | self.recursion = recursion 39 | 40 | def convert(self, network): 41 | """ Generates an n-dimensional connectivity matrix. """ 42 | if not isinstance(network, NeuralNetwork): 43 | network = NeuralNetwork(network) 44 | 45 | os = np.atleast_1d(self.substrate_shape) 46 | # Unpack the genotype 47 | w, f = network.cm.copy(), network.node_types[:] 48 | 49 | # Create substrate 50 | if len(os) == 1: 51 | cm = np.mgrid[-1:1:os[0]*1j,-1:1:os[0]*1j].transpose((1,2,0)) 52 | elif len(os) == 2: 53 | cm = np.mgrid[-1:1:os[0]*1j,-1:1:os[1]*1j,-1:1:os[0]*1j,-1:1:os[1]*1j].transpose(1,2,3,4,0) 54 | else: 55 | raise NotImplementedError("3+D substrates not supported yet.") 56 | # Insert a bias 57 | cm = np.insert(cm, 0, 1.0, -1) 58 | # Check if the genotype has enough weights 59 | if w.shape[0] < cm.shape[-1]: 60 | raise Exception("Genotype weight matrix is too small (%s)" % (w.shape,) ) 61 | # Append zeros 62 | n_elems = len(f) 63 | nvals = np.zeros(cm.shape[:-1] + (n_elems - cm.shape[-1],)) 64 | cm = np.concatenate((cm, nvals), -1) 65 | shape = cm.shape 66 | 67 | # Fix the input elements 68 | frozen = len(os) * 2 + 1 69 | w[:frozen] = 0.0 70 | w[np.diag_indices(frozen, 2)] = 1.0 71 | f[:frozen] = [lambda x: x] * frozen 72 | w[np.diag_indices(n_elems)] = (1 - self.recursion) * w[np.diag_indices(n_elems)] + self.recursion 73 | 74 | # Compute the reaction 75 | self._steps = [] 76 | laplacian = np.empty_like(cm[..., frozen:]) 77 | kernel = self.diffusion * np.array([1.,2.,1.]) 78 | for _ in range(self.reaction_steps): 79 | cm = np.dot(w, cm.reshape((-1, n_elems)).T) 80 | cm = np.clip(cm, self.cm_range[0], self.cm_range[1]) 81 | for el in xrange(cm.shape[0]): 82 | cm[el,:] = f[el](cm[el,:]) 83 | cm = cm.T.reshape(shape) 84 | # apply diffusion 85 | laplacian[:] = 0.0 86 | for ax in xrange(cm.ndim - 1): 87 | laplacian += scipy.ndimage.filters.convolve1d(cm[..., frozen:], kernel, axis=ax, mode='constant') 88 | cm[..., frozen:] += laplacian 89 | self._steps.append(cm[...,-1]) 90 | 91 | # Return the values of the last element (indicating connectivity strength) 92 | output = cm[..., -1] 93 | # Build a network object 94 | net = NeuralNetwork().from_matrix(output) 95 | if self.sandwich: 96 | net.make_sandwich() 97 | return net 98 | 99 | def visualize(self, genotype, filename): 100 | self.convert(genotype) 101 | visualization.image_grid(map(visualization.conmat_to_im, self._steps)).save(filename) 102 | 103 | -------------------------------------------------------------------------------- /peas/methods/wavelets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ Implements a 'wavelets' genotype, that encodes 4 | locations and scales of wavelets for building a connectivity 5 | matrix. 6 | """ 7 | 8 | ### IMPORTS ### 9 | 10 | import random 11 | 12 | # Libs 13 | import numpy as np 14 | import scipy.misc 15 | 16 | # Local 17 | from ..networks.rnn import NeuralNetwork 18 | 19 | # Shortcuts 20 | rand = np.random.random 21 | two_pi = np.pi * 2 22 | exp = np.exp 23 | sin = np.sin 24 | 25 | ### FUNCTIONS ### 26 | 27 | def gabor(x, y, l=1.0, psi=np.pi/2, sigma=0.5, gamma=1.0): 28 | return np.exp(-(x**2 + (gamma*y)**2)/(2*sigma**2)) * np.cos(2*np.pi*x/l + psi) 29 | 30 | def gabor_opt(x, y, sigma=0.5): 31 | return exp(-(x ** 2 + y ** 2) / (2 * sigma ** 2)) * -sin(two_pi * x) 32 | 33 | 34 | def transform_meshgrid(x, y, mat): 35 | """ Transforms the given meshgrid (x, y) 36 | using the given affine matrix 37 | """ 38 | coords = np.vstack([x.flat, y.flat, np.ones(x.size)]) 39 | coords = np.dot(mat, coords) 40 | x = np.reshape(coords[0,:], x.shape) 41 | y = np.reshape(coords[1,:], y.shape) 42 | return (x, y) 43 | 44 | 45 | ### CLASSES ### 46 | 47 | 48 | class WaveletGenotype(object): 49 | 50 | def __init__(self, inputs, layers=1, 51 | prob_add=0.1, 52 | prob_modify=0.3, 53 | stdev_mutate=0.1, 54 | add_initial_uniform=False, 55 | initial=1): 56 | # Instance vars 57 | self.inputs = inputs 58 | self.prob_add = prob_add 59 | self.prob_modify = prob_modify 60 | self.stdev_mutate = stdev_mutate 61 | self.wavelets = [[]] * layers # Each defined by an affine matrix. 62 | 63 | for _ in xrange(initial): 64 | for l in xrange(layers): 65 | self.add_wavelet(l) 66 | if add_initial_uniform: 67 | self.add_wavelet(l, uniform=True) 68 | 69 | def add_wavelet(self, layer=None, uniform=False): 70 | if layer is None: 71 | layer = np.random.randint(0, len(self.wavelets)) 72 | t = rand((2, 1)) * 2 - 1 73 | mat = rand((2, self.inputs)) 74 | norms = np.sqrt(np.sum(mat ** 2, axis=1))[:, np.newaxis] 75 | mat /= norms 76 | mat = np.hstack((mat, t)) 77 | sigma = np.random.normal(0.5, 0.3) 78 | weight = np.random.normal(0, 0.3) 79 | # This option adds a large 'uniform' blob wavelet to allow evolution 80 | # to set a zero weight level. 81 | if uniform: 82 | mat = np.eye(2, self.inputs) * 0.1 83 | mat = np.hstack((mat, np.array([[0], [0]]))) 84 | weight = 0.0 85 | wavelet = [weight, sigma, mat] 86 | self.wavelets[layer].append(wavelet) 87 | 88 | def mutate(self): 89 | """ Mutate this individual """ 90 | if rand() < self.prob_add: 91 | self.add_wavelet() 92 | else: 93 | for layer in self.wavelets: 94 | for wavelet in layer: 95 | if rand() < self.prob_modify: 96 | wavelet[0] += np.random.normal(self.stdev_mutate) 97 | wavelet[1] += np.random.normal(self.stdev_mutate) 98 | wavelet[2] += np.random.normal(0, self.stdev_mutate, wavelet[2].shape) 99 | 100 | return self # for chaining 101 | 102 | def __str__(self): 103 | return "%s with %d wavelets" % (self.__class__.__name__, len(self.wavelets)) 104 | 105 | 106 | class WaveletDeveloper(object): 107 | """ Very simple class to develop the wavelet genotype to a 108 | neural network. 109 | """ 110 | def __init__(self, substrate, 111 | add_deltas=False, 112 | min_weight=0.3, 113 | weight_range=3.0, 114 | node_type='tanh', 115 | sandwich=False, 116 | feedforward=False): 117 | # Fields 118 | self.substrate = substrate 119 | self.add_deltas = add_deltas 120 | self.min_weight = min_weight 121 | self.weight_range = weight_range 122 | self.node_type = node_type 123 | self.sandwich = sandwich 124 | self.feedforward = feedforward 125 | 126 | 127 | def convert(self, individual): 128 | cm = np.zeros((self.substrate.num_nodes, self.substrate.num_nodes)) 129 | 130 | for (i,j), coords, conn_id, expr_id in self.substrate.get_connection_list(self.add_deltas): 131 | # Add a bias (translation) 132 | coords = np.hstack((coords, [1])) 133 | w = sum(weight * gabor_opt(*(np.dot(mat, coords)), sigma=sigma) 134 | for (weight, sigma, mat) in individual.wavelets[conn_id]) 135 | cm[j,i] = w 136 | 137 | # Rescale weights 138 | cm[np.abs(cm) < self.min_weight] = 0 139 | cm -= (np.sign(cm) * self.min_weight) 140 | cm *= self.weight_range / (self.weight_range - self.min_weight) 141 | 142 | 143 | # Clip highest weights 144 | cm = np.clip(cm, -self.weight_range, self.weight_range) 145 | net = NeuralNetwork().from_matrix(cm, node_types=[self.node_type]) 146 | 147 | if self.sandwich: 148 | net.make_sandwich() 149 | 150 | if self.feedforward: 151 | net.make_feedforward() 152 | 153 | return net 154 | 155 | 156 | if __name__ == '__main__': 157 | x, y = np.meshgrid(np.linspace(0,6,4), np.linspace(0,6,4)) 158 | print x, y 159 | -------------------------------------------------------------------------------- /peas/networks/__init__.py: -------------------------------------------------------------------------------- 1 | from rnn import NeuralNetwork -------------------------------------------------------------------------------- /peas/networks/rnn.py: -------------------------------------------------------------------------------- 1 | """ Package with some classes to simulate neural nets. 2 | """ 3 | 4 | ### IMPORTS ### 5 | 6 | import sys 7 | import numpy as np 8 | np.seterr(over='ignore', divide='raise') 9 | 10 | # Libraries 11 | 12 | # Local 13 | 14 | 15 | # Shortcuts 16 | 17 | inf = float('inf') 18 | sqrt_two_pi = np.sqrt(np.pi * 2) 19 | 20 | ### FUNCTIONS ### 21 | 22 | # Node functions 23 | def ident(x): 24 | return x 25 | 26 | def bound(x, clip=(-1.0, 1.0)): 27 | return np.clip(x, *clip) 28 | 29 | def gauss(x): 30 | """ Returns the pdf of a gaussian. 31 | """ 32 | return np.exp(-x ** 2 / 2.0) / sqrt_two_pi 33 | 34 | def sigmoid(x): 35 | """ Sigmoid function. 36 | """ 37 | return 1 / (1 + np.exp(-x)) 38 | 39 | def sigmoid2(x): 40 | """ Sigmoid function. 41 | """ 42 | return 1 / (1 + np.exp(-4.9*x)) 43 | 44 | def abs(x): 45 | return np.abs(x) 46 | 47 | def sin(x): 48 | return np.sin(x) 49 | 50 | def tanh(x): 51 | return np.tanh(x) 52 | 53 | def summed(fn): 54 | return lambda x: fn(sum(x)) 55 | 56 | ### CONSTANTS ### 57 | 58 | SIMPLE_NODE_FUNCS = { 59 | 'sin': np.sin, 60 | 'abs': np.abs, 61 | 'ident': ident, 62 | 'linear': ident, 63 | 'bound': bound, 64 | 'gauss': gauss, 65 | 'sigmoid': sigmoid, 66 | 'sigmoid2': sigmoid2, 67 | 'exp': sigmoid, 68 | 'tanh': tanh, 69 | None : ident 70 | } 71 | 72 | def rbfgauss(x): 73 | return np.exp(-(x ** 2).sum() / 2.0) / sqrt_two_pi 74 | 75 | def rbfwavelet(x): 76 | return np.exp(-(x ** 2).sum() / ( 2* 0.5**2 )) * np.sin(2 * np.pi * x[0]) 77 | 78 | COMPLEX_NODE_FUNCS = { 79 | 'rbfgauss': rbfgauss, 80 | 'rbfwavelet': rbfwavelet 81 | } 82 | 83 | 84 | 85 | ### CLASSES ### 86 | 87 | class NeuralNetwork(object): 88 | """ A neural network. Can have recursive connections. 89 | """ 90 | 91 | def from_matrix(self, matrix, node_types=['sigmoid']): 92 | """ Constructs a network from a weight matrix. 93 | """ 94 | # Initialize net 95 | self.original_shape = matrix.shape[:matrix.ndim//2] 96 | # If the connectivity matrix is given as a hypercube, squash it down to 2D 97 | n_nodes = np.prod(self.original_shape) 98 | self.cm = matrix.reshape((n_nodes,n_nodes)) 99 | self.node_types = node_types 100 | if len(self.node_types) == 1: 101 | self.node_types *= n_nodes 102 | self.act = np.zeros(self.cm.shape[0]) 103 | self.optimize() 104 | return self 105 | 106 | def from_neatchromosome(self, chromosome): 107 | """ Construct a network from a Chromosome instance, from 108 | the neat-python package. This is a connection-list 109 | representation. 110 | """ 111 | # TODO Deprecate the neat-python compatibility 112 | # Typecheck 113 | import neat.chromosome 114 | 115 | if not isinstance(chromosome, neat.chromosome.Chromosome): 116 | raise Exception("Input should be a NEAT chromosome, is %r." % (chromosome)) 117 | # Sort nodes: BIAS, INPUT, HIDDEN, OUTPUT, with HIDDEN sorted by feed-forward. 118 | nodes = dict((n.id, n) for n in chromosome.node_genes) 119 | node_order = ['bias'] 120 | node_order += [n.id for n in filter(lambda n: n.type == 'INPUT', nodes.values())] 121 | if isinstance(chromosome, neat.chromosome.FFChromosome): 122 | node_order += chromosome.node_order 123 | else: 124 | node_order += [n.id for n in filter(lambda n: n.type == 'HIDDEN', nodes.values())] 125 | node_order += [n.id for n in filter(lambda n: n.type == 'OUTPUT', nodes.values())] 126 | # Construct object 127 | self.cm = np.zeros((len(node_order), len(node_order))) 128 | # Add bias connections 129 | for id, node in nodes.items(): 130 | self.cm[node_order.index(id), 0] = node.bias 131 | self.cm[node_order.index(id), 1:] = node.response 132 | # Add the connections 133 | for conn in chromosome.conn_genes: 134 | if conn.enabled: 135 | to = node_order.index(conn.outnodeid) 136 | fr = node_order.index(conn.innodeid) 137 | # dir(conn.weight) 138 | self.cm[to, fr] *= conn.weight 139 | # Verify actual feed forward 140 | if isinstance(chromosome, neat.chromosome.FFChromosome): 141 | if np.triu(self.cm).any(): 142 | raise Exception("NEAT Chromosome does not describe feedforward network.") 143 | node_order.remove('bias') 144 | self.node_types = [nodes[i].activation_type for i in node_order] 145 | self.node_types = ['ident'] + self.node_types 146 | self.act = np.zeros(self.cm.shape[0]) 147 | self.optimize() 148 | return self 149 | 150 | def optimize(self): 151 | # If all nodes are simple nodes 152 | if all(fn in SIMPLE_NODE_FUNCS for fn in self.node_types): 153 | # Simply always sum the node inputs, this is faster 154 | self.sum_all_node_inputs = True 155 | self.cm = np.nan_to_num(self.cm) 156 | # If all nodes are identical types 157 | if all(fn == self.node_types[0] for fn in self.node_types): 158 | self.all_nodes_same_function = True 159 | self.node_types = [SIMPLE_NODE_FUNCS[fn] for fn in self.node_types] 160 | else: 161 | nt = [] 162 | for fn in self.node_types: 163 | if fn in SIMPLE_NODE_FUNCS: 164 | # Substitute the function(x) for function(sum(x)) 165 | nt.append(summed(SIMPLE_NODE_FUNCS[fn])) 166 | else: 167 | nt.append(COMPLEX_NODE_FUNCS[fn]) 168 | self.node_types = nt 169 | 170 | 171 | def __init__(self, source=None): 172 | # Set instance vars 173 | self.feedforward = False 174 | self.sandwich = False 175 | self.cm = None 176 | self.node_types = None 177 | self.original_shape = None 178 | self.sum_all_node_inputs = False 179 | self.all_nodes_same_function = False 180 | 181 | if source is not None: 182 | try: 183 | self.from_matrix(*source.get_network_data()) 184 | if hasattr(source, 'feedforward') and source.feedforward: 185 | self.make_feedforward() 186 | except AttributeError: 187 | raise Exception("Cannot convert from %s to %s" % (source.__class__, self.__class__)) 188 | 189 | def make_sandwich(self): 190 | """ Turns the network into a sandwich network, 191 | a network with no hidden nodes and 2 layers. 192 | """ 193 | self.sandwich = True 194 | self.cm = np.hstack((self.cm, np.zeros(self.cm.shape))) 195 | self.cm = np.vstack((np.zeros(self.cm.shape), self.cm)) 196 | self.act = np.zeros(self.cm.shape[0]) 197 | return self 198 | 199 | def num_nodes(self): 200 | return self.cm.shape[0] 201 | 202 | def make_feedforward(self): 203 | """ Zeros out all recursive connections. 204 | """ 205 | if np.triu(np.nan_to_num(self.cm)).any(): 206 | raise Exception("Connection Matrix does not describe feedforward network. \n %s" % np.sign(self.cm)) 207 | self.feedforward = True 208 | self.cm[np.triu_indices(self.cm.shape[0])] = 0 209 | 210 | def flush(self): 211 | """ Reset activation values. """ 212 | self.act = np.zeros(self.cm.shape[0]) 213 | 214 | def feed(self, input_activation, add_bias=True, propagate=1): 215 | """ Feed an input to the network, returns the entire 216 | activation state, you need to extract the output nodes 217 | manually. 218 | 219 | :param add_bias: Add a bias input automatically, before other inputs. 220 | """ 221 | if propagate != 1 and (self.feedforward or self.sandwich): 222 | raise Exception("Feedforward and sandwich network have a fixed number of propagation steps.") 223 | act = self.act 224 | node_types = self.node_types 225 | cm = self.cm 226 | input_shape = input_activation.shape 227 | 228 | if add_bias: 229 | input_activation = np.hstack((1.0, input_activation)) 230 | 231 | if input_activation.size >= act.size: 232 | raise Exception("More input values (%s) than nodes (%s)." % (input_activation.shape, act.shape)) 233 | 234 | input_size = min(act.size - 1, input_activation.size) 235 | node_count = act.size 236 | 237 | # Feed forward nets reset the activation, and activate as many 238 | # times as there are nodes 239 | if self.feedforward: 240 | act = np.zeros(cm.shape[0]) 241 | propagate = len(node_types) 242 | # Sandwich networks only need to activate a single time 243 | if self.sandwich: 244 | propagate = 1 245 | for _ in xrange(propagate): 246 | act[:input_size] = input_activation.flat[:input_size] 247 | 248 | if self.sum_all_node_inputs: 249 | nodeinputs = np.dot(self.cm, act) 250 | else: 251 | nodeinputs = self.cm * act 252 | nodeinputs = [ni[-np.isnan(ni)] for ni in nodeinputs] 253 | 254 | if self.all_nodes_same_function: 255 | act = node_types[0](nodeinputs) 256 | else: 257 | for i in xrange(len(node_types)): 258 | act[i] = node_types[i](nodeinputs[i]) 259 | 260 | self.act = act 261 | 262 | # Reshape the output to 2D if it was 2D 263 | if self.sandwich: 264 | return act[act.size//2:].reshape(input_shape) 265 | else: 266 | return act.reshape(self.original_shape) 267 | 268 | def cm_string(self): 269 | print "Connectivity matrix: %s" % (self.cm.shape,) 270 | cp = self.cm.copy() 271 | s = np.empty(cp.shape, dtype='a1') 272 | s[cp == 0] = ' ' 273 | s[cp > 0] = '+' 274 | s[cp < 0] = '-' 275 | return '\n'.join([''.join(l) + '|' for l in s]) 276 | 277 | 278 | def visualize(self, filename, inputs=3, outputs=1): 279 | """ Visualize the network, stores in file. """ 280 | if self.cm.shape[0] > 50: 281 | return 282 | import pygraphviz as pgv 283 | # Some settings 284 | node_dist = 1 285 | cm = self.cm.copy() 286 | # Sandwich network have half input nodes. 287 | if self.sandwich: 288 | inputs = cm.shape[0] // 2 289 | outputs = inputs 290 | # Clear connections to input nodes, these arent used anyway 291 | 292 | G = pgv.AGraph(directed=True) 293 | mw = abs(cm).max() 294 | for i in range(cm.shape[0]): 295 | G.add_node(i) 296 | t = self.node_types[i].__name__ 297 | G.get_node(i).attr['label'] = '%d:%s' % (i, t[:3]) 298 | for j in range(cm.shape[1]): 299 | w = cm[i,j] 300 | if abs(w) > 0.01: 301 | G.add_edge(j, i, penwidth=abs(w)/mw*4, color='blue' if w > 0 else 'red') 302 | for n in range(inputs): 303 | pos = (node_dist*n, 0) 304 | G.get_node(n).attr['pos'] = '%s,%s!' % pos 305 | G.get_node(n).attr['shape'] = 'doublecircle' 306 | G.get_node(n).attr['fillcolor'] = 'steelblue' 307 | G.get_node(n).attr['style'] = 'filled' 308 | for i,n in enumerate(range(cm.shape[0] - outputs,cm.shape[0])): 309 | pos = (node_dist*i, -node_dist * 5) 310 | G.get_node(n).attr['pos'] = '%s,%s!' % pos 311 | G.get_node(n).attr['shape'] = 'doublecircle' 312 | G.get_node(n).attr['fillcolor'] = 'tan' 313 | G.get_node(n).attr['style'] = 'filled' 314 | 315 | G.node_attr['shape'] = 'circle' 316 | if self.sandwich: 317 | # neato supports fixed node positions, so it's better for 318 | # sandwich networks 319 | prog = 'neato' 320 | else: 321 | prog = 'dot' 322 | G.draw(filename, prog=prog) 323 | 324 | def __str__(self): 325 | return 'Neuralnet with %d nodes.' % (self.act.shape[0]) 326 | 327 | 328 | if __name__ == '__main__': 329 | # import doctest 330 | # doctest.testmod(optionflags=doctest.ELLIPSIS) 331 | a = NeuralNetwork().from_matrix(np.array([[0,0,0],[0,0,0],[1,1,0]])) 332 | print a.cm_string() 333 | print a.feed(np.array([1,1]), add_bias=False) 334 | 335 | -------------------------------------------------------------------------------- /peas/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noio/peas/4c2503388644cc5ca58c501e6c773d53a883538b/peas/tasks/__init__.py -------------------------------------------------------------------------------- /peas/tasks/checkers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Module implements a checkers game with alphabeta search and input for a heuristic 4 | """ 5 | 6 | ### IMPORTS ### 7 | import random 8 | import copy 9 | import time 10 | import sys 11 | 12 | from collections import defaultdict 13 | 14 | import numpy as np 15 | 16 | ### SHORTCUTS ### 17 | inf = float('inf') 18 | 19 | 20 | ### CONSTANTS ### 21 | 22 | EMPTY = 0 23 | WHITE = 1 24 | BLACK = 2 25 | MAN = 4 26 | KING = 8 27 | FREE = 16 28 | 29 | NUMBERING = np.array([[ 4, 0, 3, 0, 2, 0, 1, 0], 30 | [ 0, 8, 0, 7, 0, 6, 0, 5], 31 | [12, 0, 11, 0, 10, 0, 9, 0], 32 | [ 0, 16, 0, 15, 0, 14, 0, 13], 33 | [20, 0, 19, 0, 18, 0, 17, 0], 34 | [ 0, 24, 0, 23, 0, 22, 0, 21], 35 | [28, 0, 27, 0, 26, 0, 25, 0], 36 | [ 0, 32, 0, 31, 0, 30, 0, 29]]) 37 | 38 | CENTER = [(2,2), (2,4), (3,3), (3,5), (4,2), (4,4), (5,3), (5,5)] 39 | EDGE = [(0,0), (0,2), (0,4), (0,6), (1,7), (2,0), (3,7), (4,0), (5,7), (6,0), (7,1), (7,3), (7,5), (7,7)] 40 | SAFEEDGE = [(0,6), (1,7), (6,0), (7,1)] 41 | 42 | INVNUM = dict([(n, tuple(a[0] for a in np.nonzero(NUMBERING == n))) for n in range(1, NUMBERING.max() + 1)]) 43 | 44 | ### EXCEPTIONS 45 | 46 | class IllegalMoveError(Exception): 47 | pass 48 | 49 | ### FUNCTIONS ### 50 | def alphabeta(node, heuristic, player_max=True, depth=4, alpha=-inf, beta=inf, killer_moves=defaultdict(set), num_evals=[0]): 51 | """ Performs alphabeta search. 52 | From wikipedia pseudocode. 53 | """ 54 | if depth == 0 or node.game_over(): 55 | num_evals[0] += 1 56 | return heuristic(node) 57 | pmx = not player_max 58 | 59 | killers = killer_moves[node.turn+1] 60 | moves = node.all_moves() 61 | m = [] 62 | for move in moves: 63 | if move in killers: 64 | m.insert(0, move) 65 | else: 66 | m.append(move) 67 | moves = m 68 | 69 | if player_max: 70 | for move in moves: 71 | newnode = node.copy_and_play(move) 72 | alpha = max(alpha, alphabeta(newnode, heuristic, pmx, depth-1, alpha, beta, killer_moves, num_evals)) 73 | if beta <= alpha: 74 | if len(killers) > 4: 75 | killers.pop() 76 | killers.add(move) 77 | break # Beta cutoff 78 | return alpha 79 | else: 80 | for move in moves: 81 | newnode = node.copy_and_play(move) 82 | beta = min(beta, alphabeta(newnode, heuristic, pmx, depth-1, alpha, beta, killer_moves, num_evals)) 83 | if beta <= alpha: 84 | if len(killers) > 4: 85 | killers.pop() 86 | killers.add(move) 87 | break # Alpha cutoff 88 | return beta 89 | 90 | def gamefitness(game): 91 | """ Returns the fitness of 92 | the black player. (according to {gauci2008case}) """ 93 | counts = np.bincount(game.board.flat) 94 | return (100 + 2 * counts[BLACK|MAN] + 3 * counts[BLACK|KING] + 95 | 2 * (12 - counts[WHITE|MAN] + 3 * (12 - counts[WHITE|KING]))) 96 | 97 | ### CLASSES ### 98 | 99 | class CheckersTask(object): 100 | """ Represents a checkers game played by an evolved phenotype against 101 | a fixed opponent. 102 | """ 103 | def __init__(self, search_depth=4, opponent_search_depth=4, opponent_handicap=0.0, minefield=False, fly_kings=True, win_to_solve=3): 104 | self.search_depth = search_depth 105 | self.opponent_search_depth = opponent_search_depth 106 | self.opponent_handicap = opponent_handicap 107 | self.win_to_solve = win_to_solve 108 | self.minefield = minefield 109 | self.fly_kings = fly_kings 110 | 111 | def evaluate(self, network): 112 | # Setup 113 | game = Checkers(minefield=self.minefield, fly_kings=self.fly_kings) 114 | player = HeuristicOpponent(NetworkHeuristic(network), search_depth=self.search_depth) 115 | opponent = HeuristicOpponent(SimpleHeuristic(), search_depth=self.opponent_search_depth, handicap=self.opponent_handicap) 116 | # Play the game 117 | fitness = [] 118 | current, next = player, opponent 119 | i = 0 120 | print "Running checkers game..." 121 | while not game.game_over(): 122 | i += 1 123 | move = current.pickmove(game) 124 | game.play(move) 125 | current, next = next, current 126 | fitness.append(gamefitness(game)) 127 | sys.stdout.write('.') 128 | sys.stdout.flush() 129 | 130 | print 131 | print game 132 | # Fitness over last 100 episodes 133 | fitness.extend([gamefitness(game)] * (100 - len(fitness))) 134 | fitness = fitness[-100:] 135 | print fitness 136 | score = sum(fitness) 137 | won = game.winner() >= 1.0 138 | if won: 139 | score += 30000 140 | print "\nGame finished in %d turns. Winner: %s. Score: %s" % (i,game.winner(), score) 141 | return {'fitness':score, 'won': won} 142 | 143 | def play_against(self, network): 144 | # Setup 145 | game = Checkers(minefield=self.minefield) 146 | player = HeuristicOpponent(NetworkHeuristic(network), search_depth=self.search_depth) 147 | 148 | # Play the game 149 | fitness = [gamefitness(game)] * 100 150 | 151 | i = 0 152 | print "Running checkers game..." 153 | while not game.game_over(): 154 | i += 1 155 | move = player.pickmove(game) 156 | print move 157 | game.play(move) 158 | 159 | print game 160 | print NUMBERING 161 | print "enter move" 162 | moved = False 163 | while not moved: 164 | try: 165 | user_input = raw_input() 166 | if 'q' in user_input: 167 | sys.exit() 168 | if ' ' in user_input: 169 | move = tuple(int(i) for i in user_input.split(' ')) 170 | else: 171 | move = tuple(int(i) for i in user_input.split('-')) 172 | game.play(move) 173 | moved = True 174 | except IllegalMoveError: 175 | print "Illegal move" 176 | 177 | 178 | print game 179 | score = sum(fitness) 180 | won = game.winner() >= 1.0 181 | if won: 182 | score += 30000 183 | print "\nGame finished in %d turns. Winner: %s. Score: %s" % (i,game.winner(), score) 184 | return {'fitness':score, 'won': won} 185 | 186 | def solve(self, network): 187 | for _ in range(self.win_to_solve): 188 | if not self.evaluate(network)['won']: 189 | return False 190 | return True 191 | 192 | def visualize(self, network): 193 | pass 194 | 195 | 196 | class HeuristicOpponent(object): 197 | """ Opponent that utilizes a heuristic combined with alphabeta search 198 | to decide on a move. 199 | """ 200 | def __init__(self, heuristic, search_depth=4, handicap=0.0): 201 | self.search_depth = search_depth 202 | self.heuristic = heuristic 203 | self.handicap = handicap 204 | self.killer_moves = defaultdict(set) 205 | 206 | def pickmove(self, board, verbose=False): 207 | player_max = board.to_move == BLACK 208 | bestmove = None 209 | secondbest = None 210 | bestval = -inf if player_max else inf 211 | moves = list(board.all_moves()) 212 | # If there is only one possible move, don't search, just move. 213 | if len(moves) == 1: 214 | if verbose: print "0 evals." 215 | return moves[0] 216 | for move in moves: 217 | evals = [0] 218 | val = alphabeta(board.copy_and_play(move), self.heuristic.evaluate, 219 | depth=self.search_depth, player_max=player_max, killer_moves=self.killer_moves, num_evals=evals) 220 | if player_max and val > bestval or not player_max and val < bestval: 221 | bestval = val 222 | secondbest = bestmove 223 | bestmove = move 224 | # Pick second best move 225 | if secondbest is not None and self.handicap > 0 and random.random() < self.handicap: 226 | return secondbest 227 | if verbose: print "%d evals." % evals[0] 228 | return bestmove 229 | 230 | class SimpleHeuristic(object): 231 | """ Simple piece/position counting heuristic, adapted from simplech 232 | """ 233 | def evaluate(self, game): 234 | if game.game_over(): 235 | return -5000 if game.to_move == BLACK else 5000 236 | board = game.board 237 | counts = np.bincount(board.flat) 238 | 239 | turn = 2; # color to move gets +turn 240 | brv = 3; # multiplier for back rank 241 | kcv = 5; # multiplier for kings in center 242 | mcv = 1; # multiplier for men in center 243 | mev = 1; # multiplier for men on edge 244 | kev = 5; # multiplier for kings on edge 245 | cramp = 5; # multiplier for cramp 246 | opening = -2; # multipliers for tempo 247 | midgame = -1; 248 | endgame = 2; 249 | intactdoublecorner = 3; 250 | 251 | nwm = counts[WHITE|MAN] 252 | nwk = counts[WHITE|KING] 253 | nbm = counts[BLACK|MAN] 254 | nbk = counts[BLACK|KING] 255 | 256 | vb = (100 * nbm + 130 * nbk) 257 | vw = (100 * nwm + 130 * nwk) 258 | 259 | val = 0 260 | 261 | if (vb + vw) > 0: 262 | val = (vb - vw) + (250 * (vb-vw))/(vb+vw); #favor exchanges if in material plus 263 | 264 | 265 | nm = nwm + nbm 266 | nk = nwk + nbk 267 | 268 | val += turn if game.to_move == BLACK else -turn 269 | 270 | if board[4][0] == (BLACK|MAN) and board[5][1] == (WHITE|MAN): 271 | val += cramp 272 | if board[3][7] == (WHITE|MAN) and board[2][6] == (BLACK|MAN): 273 | val -= cramp 274 | 275 | # Back rank guard 276 | code = 0 277 | if (board[0][0] & MAN): code += 1 278 | if (board[0][2] & MAN): code += 2 279 | if (board[0][4] & MAN): code += 4 280 | if (board[0][6] & MAN): code += 8 281 | if code == 1: 282 | backrankb = -1 283 | elif code in (0, 3, 9): 284 | backrankb = 0 285 | elif code in (2, 4, 5, 7, 8): 286 | backrankb = 1 287 | elif code in (6, 12, 13): 288 | backrankb = 2 289 | elif code == 11: 290 | backrankb = 4 291 | elif code == 10: 292 | backrankb = 7 293 | elif code == 15: 294 | backrankb = 8 295 | elif code == 14: 296 | backrankb = 9 297 | 298 | code = 0 299 | if (board[7][1] & MAN): code += 8 300 | if (board[7][3] & MAN): code += 4 301 | if (board[7][5] & MAN): code += 2 302 | if (board[7][7] & MAN): code += 1 303 | 304 | if code == 1: 305 | backrankw = -1 306 | elif code in (0, 3, 9): 307 | backrankw = 0 308 | elif code in (2, 4, 5, 7, 8): 309 | backrankw = 1 310 | elif code in (6, 12, 13): 311 | backrankw = 2 312 | elif code == 11: 313 | backrankw = 4 314 | elif code == 10: 315 | backrankw = 7 316 | elif code == 15: 317 | backrankw = 8 318 | elif code == 14: 319 | backrankw = 9 320 | 321 | val += brv * (backrankb - backrankw) 322 | 323 | if board[0][6] == BLACK|MAN and (board[1][5] == BLACK|MAN or board[1][7] == BLACK|MAN): 324 | val += intactdoublecorner 325 | if board[7][1] == WHITE|MAN and (board[6][0] == WHITE|MAN or board[6][2] == WHITE|MAN): 326 | val -= intactdoublecorner 327 | 328 | bm = bk = wm = wk = 0 329 | for pos in CENTER: 330 | if board[pos] == BLACK|MAN: bm += 1 331 | elif board[pos] == BLACK|KING: bk += 1 332 | elif board[pos] == WHITE|MAN: wm += 1 333 | elif board[pos] == WHITE|KING: wk += 1 334 | 335 | val += (bm - wm) * mcv 336 | val += (bk - wk) * kcv 337 | 338 | bm = bk = wm = wk = 0 339 | for pos in EDGE: 340 | if board[pos] == BLACK|MAN: bm += 1 341 | elif board[pos] == BLACK|KING: bk += 1 342 | elif board[pos] == WHITE|MAN: wm += 1 343 | elif board[pos] == WHITE|KING: wk += 1 344 | 345 | val += (bm - wm) * mev 346 | val += (bk - wk) * kev 347 | 348 | tempo = 0 349 | for i in xrange(8): 350 | for j in xrange(8): 351 | if board[i,j] == BLACK|MAN: 352 | tempo += i 353 | elif board[i,j] == WHITE|MAN: 354 | tempo -= 7-i 355 | if nm >= 16: 356 | val += opening * tempo 357 | if 12 <= nm <= 15: 358 | val += midgame * tempo 359 | if nm < 9: 360 | val += endgame * tempo 361 | 362 | for pos in SAFEEDGE: 363 | if nbk + nbm > nwk + nwm and nwk < 3: 364 | if board[pos] == (WHITE|KING): 365 | val -= 15 366 | 367 | if nwk + nwm > nbk + nbm and nbk < 3: 368 | if board[pos] == (BLACK|KING): 369 | val += 15 370 | 371 | # I have no idea what this last bit does.. :( 372 | stonesinsystem = 0 373 | if nwm + nwk - nbk - nbm == 0: 374 | if game.to_move == BLACK: 375 | for row in xrange(0, 8, 2): 376 | for c in xrange(0, 8, 2): 377 | if board[row,c] != FREE: 378 | stonesinsystem += 1 379 | if stonesinsystem % 2: 380 | if nm + nk <= 12: val += 1 381 | if nm + nk <= 10: val += 1 382 | if nm + nk <= 8: val += 2 383 | if nm + nk <= 6: val += 2 384 | else: 385 | if nm + nk <= 12: val -= 1 386 | if nm + nk <= 10: val -= 1 387 | if nm + nk <= 8: val -= 2 388 | if nm + nk <= 6: val -= 2 389 | else: 390 | for row in xrange(1, 8, 2): 391 | for c in xrange(1, 8, 2): 392 | if board[row,c] != FREE: 393 | stonesinsystem += 1 394 | if stonesinsystem % 2: 395 | if nm + nk <= 12: val += 1 396 | if nm + nk <= 10: val += 1 397 | if nm + nk <= 8: val += 2 398 | if nm + nk <= 6: val += 2 399 | else: 400 | if nm + nk <= 12: val -= 1 401 | if nm + nk <= 10: val -= 1 402 | if nm + nk <= 8: val -= 2 403 | if nm + nk <= 6: val -= 2 404 | 405 | return val 406 | 407 | class PieceCounter(object): 408 | def evaluate(self, game): 409 | counts = np.bincount(game.board.flat) 410 | 411 | nwm = counts[WHITE|MAN] 412 | nwk = counts[WHITE|KING] 413 | nbm = counts[BLACK|MAN] 414 | nbk = counts[BLACK|KING] 415 | 416 | vb = (100 * nbm + 130 * nbk) 417 | vw = (100 * nwm + 130 * nwk) 418 | 419 | return vb - vw 420 | 421 | class NetworkHeuristic(object): 422 | """ Heuristic based on feeding the board state to a neural network 423 | """ 424 | def __init__(self, network): 425 | self.network = network 426 | 427 | def evaluate(self, game): 428 | if game.game_over(): 429 | return -5000 if game.to_move == BLACK else 5000 430 | 431 | net_inputs = ((game.board == BLACK | MAN) * 0.5 + 432 | (game.board == WHITE | MAN) * -0.5 + 433 | (game.board == BLACK | KING) * 0.75 + 434 | (game.board == WHITE | KING) * -0.75) 435 | # Feed twice to propagate through 3 layer network: 436 | value = self.network.feed(net_inputs, add_bias=False, propagate=2) 437 | # print value 438 | return value[-1] 439 | 440 | class RandomOpponent(object): 441 | """ An opponent that plays random moves """ 442 | def pickmove(self, game): 443 | return random.choice(list(game.all_moves())) 444 | 445 | class Checkers(object): 446 | """ Represents the checkers game(state) 447 | """ 448 | 449 | def __init__(self, non_capture_draw=30, fly_kings=True, minefield=False): 450 | """ Initialize the game board. """ 451 | self.non_capture_draw = non_capture_draw 452 | 453 | self.board = NUMBERING.copy() #: The board state 454 | self.to_move = BLACK #: Whose move it is 455 | self.turn = 0 456 | self.history = [] 457 | self.caphistory = [] 458 | self.minefield = minefield 459 | self.fly_kings = fly_kings 460 | 461 | tiles = self.board > 0 462 | self.board[tiles] = EMPTY 463 | self.board[:3,:] = BLACK | MAN 464 | # self.board[3, :] = WHITE | MAN 465 | self.board[5:,:] = WHITE | MAN 466 | self.board[np.logical_not(tiles)] = FREE 467 | 468 | self._moves = None 469 | 470 | def all_moves(self): 471 | if self._moves is None: 472 | self._moves = list(self.generate_moves()) 473 | return self._moves 474 | 475 | def generate_moves(self): 476 | """ Return a list of possible moves. """ 477 | captures_possible = False 478 | pieces = [] 479 | # Check for possible captures first: 480 | for n, (y,x) in INVNUM.iteritems(): 481 | piece = self.board[y, x] 482 | if piece & self.to_move: 483 | pieces.append((n, (y, x), piece)) 484 | for m in self.captures((y, x), piece, self.board): 485 | if len(m) > 1: 486 | captures_possible = True 487 | yield m 488 | # Otherwise check for normal moves: 489 | if not captures_possible: 490 | for (n, (y, x), piece) in pieces: 491 | # MAN moves 492 | if piece & MAN: 493 | nextrow = y + 1 if self.to_move == BLACK else y - 1 494 | if 0 <= nextrow < 8: 495 | if x - 1 >= 0 and self.board[nextrow, x - 1] == EMPTY: 496 | yield (n, NUMBERING[nextrow, x - 1]) 497 | if x + 1 < 8 and self.board[nextrow, x + 1] == EMPTY: 498 | yield (n, NUMBERING[nextrow, x + 1]) 499 | # KING moves 500 | else: 501 | for dx in [-1, 1]: 502 | for dy in [-1, 1]: 503 | dist = 1 504 | while True: 505 | tx, ty = x + (dist + 1) * dx, y + (dist + 1) * dy # Target square 506 | if not ((0 <= tx < 8 and 0 <= ty < 8) and self.board[ty, tx] == EMPTY): 507 | break 508 | else: 509 | yield (n, NUMBERING[ty, tx]) 510 | if not self.fly_kings: 511 | break 512 | dist += 1 513 | 514 | 515 | def captures(self, (py, px), piece, board, captured=[], start=None): 516 | """ Return a list of possible capture moves for given piece in a 517 | checkers game. 518 | 519 | :param (py, px): location of piece on the board 520 | :param piece: piece type (BLACK/WHITE|MAN/KING) 521 | :param board: the 2D board matrix 522 | :param captured: list of already-captured pieces (can't jump twice) 523 | :param start: from where this capture chain started. 524 | """ 525 | if start is None: 526 | start = (py, px) 527 | opponent = BLACK if piece & WHITE else WHITE 528 | forward = [-1, 1] if piece & KING else [1] if piece & BLACK else [-1] 529 | # Look for capture moves 530 | for dx in [-1, 1]: 531 | for dy in forward: 532 | jx, jy = px, py 533 | while True: 534 | jx += dx # Jumped square 535 | jy += dy 536 | # Check if piece at jx, jy: 537 | if not (0 <= jx < 8 and 0 <= jy < 8): 538 | break 539 | if board[jy, jx] != EMPTY: 540 | tx = jx + dx # Target square 541 | ty = jy + dy 542 | # Check if it can be captured: 543 | if ((0 <= tx < 8 and 0 <= ty < 8) and 544 | ((ty, tx) == start or board[ty, tx] == EMPTY) and 545 | (jy, jx) not in captured and 546 | (board[jy, jx] & opponent) 547 | ): 548 | # Normal pieces cannot continue capturing after reaching last row 549 | if not piece & KING and (piece & WHITE and ty == 0 or piece & BLACK and ty == 7): 550 | yield (NUMBERING[py, px], NUMBERING[ty, tx]) 551 | else: 552 | for sequence in self.captures((ty, tx), piece, board, captured + [(jy, jx)], start): 553 | yield (NUMBERING[py, px],) + sequence 554 | break 555 | else: 556 | if piece & MAN or not self.fly_kings: 557 | break 558 | yield (NUMBERING[py, px],) 559 | 560 | 561 | def play(self, move): 562 | """ Play the given move on the board. """ 563 | if move not in self.all_moves(): 564 | raise IllegalMoveError("Illegal move") 565 | self.history.append(move) 566 | positions = [INVNUM[p] for p in move] 567 | (ly, lx) = positions[0] 568 | # Check for captures 569 | capture = False 570 | stone_dies = False 571 | for (py, px) in positions[1:]: 572 | ydir = 1 if py > ly else -1 573 | xdir = 1 if px > lx else -1 574 | for y, x in zip(xrange(ly + ydir, py, ydir),xrange(lx + xdir, px, xdir)): 575 | if self.board[y,x] != EMPTY: 576 | self.board[y,x] = EMPTY 577 | if self.minefield and 2 <= x < 6 and 2 <= y < 6: 578 | stone_dies = True 579 | capture = True 580 | (ly, lx) = (py, px) 581 | self.caphistory.append(capture) 582 | # Move the piece 583 | (ly, lx) = positions[0] 584 | (py, px) = positions[-1] 585 | piece = self.board[ly, lx] 586 | self.board[ly, lx] = EMPTY 587 | # Check if the piece needs to be crowned 588 | if piece & BLACK and py == 7 or piece & WHITE and py == 0: 589 | piece = piece ^ MAN | KING 590 | self.board[py, px] = piece 591 | 592 | # Kill the piece if a capture was performed on the minefield. 593 | if stone_dies: 594 | self.board[py, px] = EMPTY 595 | 596 | self.to_move = WHITE if self.to_move == BLACK else BLACK 597 | self.turn += 1 598 | # Cached moveset is invalidated. 599 | self._moves = None 600 | return self 601 | 602 | def copy_and_play(self, move): 603 | return self.copy().play(move) 604 | 605 | def check_draw(self, verbose=False): 606 | # If there were no captures in the last [30] moves, draw. 607 | i = 0 608 | for i in xrange(len(self.caphistory)): 609 | if self.caphistory[-(i+1)]: 610 | break 611 | if verbose: 612 | print "Last capture: %d turns ago." % (i) 613 | return (i > self.non_capture_draw) 614 | 615 | def game_over(self): 616 | """ Whether the game is over. """ 617 | if self.check_draw(): 618 | return True 619 | for move in self.all_moves(): 620 | # If the iterator returns any moves at all, the game is not over. 621 | return False 622 | # Otherwise it is. 623 | return True 624 | 625 | def winner(self): 626 | """ Returns board score. """ 627 | if self.check_draw() or not self.game_over(): 628 | return 0.0 629 | else: 630 | return 1.0 if self.to_move == WHITE else -1.0 631 | 632 | def copy(self): 633 | new = copy.copy(self) # Copy all. 634 | new.board = self.board.copy() # Copy the board explicitly 635 | new._moves = copy.copy(self._moves) # Shallow copy is enough. 636 | new.history = self.history[:] 637 | new.caphistory = self.caphistory[:] 638 | return new 639 | 640 | def __str__(self): 641 | s = np.array([l for l in "- wb WB "]) 642 | s = s[self.board] 643 | if self.to_move == BLACK: 644 | s[0,7] = 'v' 645 | else: 646 | s[7,0] = '^' 647 | s = '\n'.join(' '.join(l) for l in s) 648 | return s 649 | 650 | ### PROCEDURE ### 651 | 652 | if __name__ == '__main__': 653 | scores = [] 654 | n = 3 655 | tic = time.time() 656 | for i in range(n): 657 | game = Checkers(fly_kings=False) 658 | player = None 659 | # opponent = HeuristicOpponent(PieceCounter()) 660 | opponent = HeuristicOpponent(SimpleHeuristic()) 661 | # Play the game 662 | current, next = player, opponent 663 | i = 0 664 | while not game.game_over(): 665 | i += 1 666 | print game 667 | print NUMBERING 668 | print "enter move" 669 | moved = False 670 | while not moved: 671 | try: 672 | user_input = raw_input() 673 | if 'q' in user_input: 674 | sys.exit() 675 | if ' ' in user_input: 676 | move = tuple(int(i) for i in user_input.split(' ')) 677 | else: 678 | move = tuple(int(i) for i in user_input.split('-')) 679 | game.play(move) 680 | moved = True 681 | except IllegalMoveError: 682 | print "Illegal move" 683 | 684 | move = opponent.pickmove(game) 685 | print move 686 | game.play(move) 687 | 688 | scores.append(gamefitness(game)) 689 | print (time.time() - tic) / n 690 | print 'Score', scores -------------------------------------------------------------------------------- /peas/tasks/linefollowing/__init__.py: -------------------------------------------------------------------------------- 1 | from linefollowing import LineFollowingTask 2 | 3 | __all__ = ['LineFollowingTask'] -------------------------------------------------------------------------------- /peas/tasks/linefollowing/eight.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noio/peas/4c2503388644cc5ca58c501e6c773d53a883538b/peas/tasks/linefollowing/eight.ai -------------------------------------------------------------------------------- /peas/tasks/linefollowing/eight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noio/peas/4c2503388644cc5ca58c501e6c773d53a883538b/peas/tasks/linefollowing/eight.png -------------------------------------------------------------------------------- /peas/tasks/linefollowing/eight_inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noio/peas/4c2503388644cc5ca58c501e6c773d53a883538b/peas/tasks/linefollowing/eight_inverted.png -------------------------------------------------------------------------------- /peas/tasks/linefollowing/eight_striped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noio/peas/4c2503388644cc5ca58c501e6c773d53a883538b/peas/tasks/linefollowing/eight_striped.png -------------------------------------------------------------------------------- /peas/tasks/linefollowing/eight_striped2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noio/peas/4c2503388644cc5ca58c501e6c773d53a883538b/peas/tasks/linefollowing/eight_striped2.png -------------------------------------------------------------------------------- /peas/tasks/linefollowing/linefollowing.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | 3 | """ Top-down line following task 4 | """ 5 | 6 | ### IMPORTS ### 7 | import random 8 | import os 9 | 10 | # Libraries 11 | import pymunk 12 | import numpy as np 13 | from scipy.misc import imread 14 | 15 | # Local 16 | from ...networks.rnn import NeuralNetwork 17 | 18 | # Shortcuts 19 | pi = np.pi 20 | 21 | ### CONSTANTS ### 22 | DATA_DIR = os.path.abspath(os.path.split(__file__)[0]) 23 | 24 | 25 | ### FUNCTIONS ### 26 | 27 | def path_length(path): 28 | l = 0 29 | for i in xrange(1, len(path)): 30 | (x0, y0) = path[i-1] 31 | (x1, y1) = path[i] 32 | l += np.sqrt((x1-x0) ** 2 + (y1-y0) ** 2) 33 | return l 34 | 35 | ### CLASSES ### 36 | 37 | class Robot(object): 38 | """ Robot that performs this task. """ 39 | 40 | def __init__(self, space, field_friction, field_observation, initial_pos, 41 | size=8, 42 | motor_torque=6, 43 | friction_scale=0.2, 44 | angular_damping=0.9, 45 | force_global=False): 46 | self.field_friction = field_friction 47 | self.field_observation = field_observation 48 | self.size = size 49 | self.friction_scale = friction_scale 50 | self.motor_torque = motor_torque 51 | self.angular_damping = angular_damping 52 | self.force_global = force_global 53 | 54 | mass = size ** 2 * 0.2 55 | self.body = body = pymunk.Body(mass, pymunk.moment_for_box(mass, size, size)) 56 | body.position = pymunk.Vec2d(initial_pos[0], initial_pos[1]) 57 | body.angle = initial_pos[2] 58 | self.shape = shape = pymunk.Poly.create_box(body, (size, size)) 59 | shape.group = 1 60 | shape.collision_type = 1 61 | space.add(body, shape) 62 | 63 | self.sensors = [(r, theta) for r in np.linspace(1,4,3) * size * 0.75 64 | for theta in np.linspace(-0.5 * np.pi, 0.5 * np.pi, 5)] 65 | 66 | self.l = self.r = 0 67 | 68 | 69 | def sensor_locations(self): 70 | for (r, theta) in self.sensors: 71 | (x, y) = np.cos(theta) * r, np.sin(theta) * r 72 | yield self.body.local_to_world((x, y)) 73 | 74 | def wheel_locations(self, rel=True): 75 | lwheel = self.body.local_to_world((0, -self.size / 2)) 76 | rwheel = self.body.local_to_world((0, self.size / 2)) 77 | if rel: 78 | lwheel -= self.body.position 79 | rwheel -= self.body.position 80 | return (lwheel, rwheel) 81 | 82 | def sensor_response(self): 83 | for point in self.sensor_locations(): 84 | yield self.field_at(point, observation=True) 85 | 86 | def field_at(self, (x,y), border=1.0, observation=False): 87 | if (0 <= x < self.field_friction.shape[1] and 88 | 0 <= y < self.field_friction.shape[0]): 89 | if observation: 90 | return self.field_observation[int(y), int(x)] 91 | else: 92 | return self.field_friction[int(y), int(x)] 93 | return border 94 | 95 | def apply_friction(self): 96 | f = self.field_at(self.body.position) 97 | f = 1 - self.friction_scale * f 98 | self.body.velocity.x = self.body.velocity.x * f 99 | self.body.velocity.y = self.body.velocity.y * f 100 | self.body.angular_velocity *= self.angular_damping 101 | # Zero out sideways motion. (i.e. 100% perpendicular friction) 102 | self.body.velocity = self.body.velocity.projection(self.body.rotation_vector) 103 | 104 | def drive(self, l, r): 105 | self.l, self.r = np.clip([l, r], -1, 1) 106 | self.l *= self.motor_torque 107 | self.r *= self.motor_torque 108 | if not self.force_global: 109 | lw, rw = self.wheel_locations() 110 | else: 111 | lw, rw = (0, -self.size / 2), (0, self.size / 2) 112 | self.l = float(self.l) 113 | self.r = float(self.r) 114 | self.body.apply_impulse( self.l * self.body.rotation_vector, lw) 115 | self.body.apply_impulse( self.r * self.body.rotation_vector, rw) 116 | 117 | def draw(self, screen): 118 | import pygame 119 | pygame.draw.lines(screen, (255,0,0), True, [(int(p.x), int(p.y)) for p in self.shape.get_points()]) 120 | 121 | lw, rw = self.wheel_locations(rel=False) 122 | pygame.draw.line(screen, (255, 0, 255), lw, lw + self.body.rotation_vector * 50. * self.l / self.motor_torque, 2) 123 | pygame.draw.line(screen, (255, 0, 255), rw, rw + self.body.rotation_vector * 50. * self.r / self.motor_torque, 2) 124 | 125 | for ((x, y), response) in zip(self.sensor_locations(), self.sensor_response()): 126 | r = 255 * response 127 | pygame.draw.circle(screen, (255 - r, r, 0), (int(x), int(y)), 2) 128 | 129 | 130 | class LineFollowingTask(object): 131 | """ Line following task. 132 | """ 133 | 134 | def __init__(self, field='eight', observation='eight_striped', 135 | max_steps=1000, 136 | friction_scale=0.2, 137 | motor_torque=6, 138 | damping=0.2, 139 | initial_pos=None, 140 | flush_each_step=False, 141 | force_global=False, 142 | path_resolution=100, 143 | check_coverage=False, 144 | coverage_memory=20): 145 | # Settings 146 | self.max_steps = max_steps 147 | self.flush_each_step = flush_each_step 148 | self.force_global = force_global 149 | self.fieldpath = os.path.join(DATA_DIR,field) + '.png' 150 | self.observationpath = os.path.join(DATA_DIR,observation) + '.png' 151 | print "Using %s" % (self.fieldpath,) 152 | field_friction = imread(self.fieldpath) 153 | field_observation = imread(self.observationpath) 154 | self.path_resolution = path_resolution 155 | self.check_coverage = check_coverage 156 | self.coverage_memory = coverage_memory 157 | 158 | self.field_friction = field_friction[:,:,0].astype(np.float)/255 159 | self.field_observation = field_observation[:,:,0].astype(np.float)/255 160 | 161 | self.friction_scale = friction_scale 162 | self.motor_torque = motor_torque 163 | self.damping = damping 164 | self.initial_pos = initial_pos 165 | if self.initial_pos is None: 166 | self.initial_pos = self.field_friction.shape[0] / 2, self.field_friction.shape[1]/2 , 0 167 | 168 | 169 | def evaluate(self, network, draw=False, drawname='Simulation'): 170 | """ Evaluate the efficiency of the given network. Returns the 171 | distance that the bot ran 172 | """ 173 | if not isinstance(network, NeuralNetwork): 174 | network = NeuralNetwork(network) 175 | 176 | h,w = self.field_friction.shape 177 | 178 | if draw: 179 | import pygame 180 | pygame.init() 181 | screen = pygame.display.set_mode((w, h)) 182 | pygame.display.set_caption(drawname) 183 | clock = pygame.time.Clock() 184 | running = True 185 | font = pygame.font.Font(pygame.font.get_default_font(), 12) 186 | field_image = pygame.image.load(self.observationpath) 187 | 188 | # Initialize pymunk 189 | self.space = space = pymunk.Space() 190 | space.gravity = (0,0) 191 | space.damping = self.damping 192 | 193 | # Create objects 194 | robot = Robot(space, self.field_friction, self.field_observation, self.initial_pos, 195 | friction_scale=self.friction_scale, motor_torque=self.motor_torque, 196 | force_global=self.force_global) 197 | 198 | path = [(robot.body.position.int_tuple)] 199 | cells = [] 200 | 201 | if network.cm.shape[0] != (3*5 + 3*3 + 1): 202 | raise Exception("Network shape must be a 2 layer controller: 3x5 input + 3x3 hidden + 1 bias. Has %d." % network.cm.shape[0]) 203 | 204 | for step in xrange(self.max_steps): 205 | 206 | net_input = np.array(list(robot.sensor_response())) 207 | # The nodes used for output are somewhere in the middle of the network 208 | # so we extract them using -4 and -6 209 | action = network.feed(net_input)[[-4,-6]] 210 | if self.flush_each_step: 211 | network.flush() 212 | 213 | robot.drive(*action) 214 | robot.apply_friction() 215 | space.step(1/50.0) 216 | 217 | new_cell_covered = False 218 | current_cell = int(robot.body.position.x // 32), int(robot.body.position.y // 32) 219 | if current_cell not in cells: 220 | cells.append(current_cell) 221 | new_cell_covered = True 222 | if len(cells) > self.coverage_memory: 223 | cells.pop(0) 224 | elif cells[-1] == current_cell: 225 | new_cell_covered = True 226 | 227 | if step % self.path_resolution == 0 and (not self.check_coverage or new_cell_covered): 228 | path.append((robot.body.position.int_tuple)) 229 | 230 | 231 | if draw: 232 | screen.fill((255, 255, 255)) 233 | screen.blit(field_image, (0,0)) 234 | txt = font.render('%d - %.0f' % (step, path_length(path)), False, (0,0,0) ) 235 | screen.blit(txt, (0,0)) 236 | # Draw path 237 | if len(path) > 1: 238 | pygame.draw.lines(screen, (0,0,255), False, path, 3) 239 | for cell in cells: 240 | pygame.draw.rect(screen, (200,200,0), (cell[0]*32, cell[1]*32, 32, 32), 2) 241 | robot.draw(screen) 242 | 243 | if pygame.event.get(pygame.QUIT): 244 | break 245 | 246 | for k in pygame.event.get(pygame.KEYDOWN): 247 | if k.key == pygame.K_SPACE: 248 | break 249 | 250 | pygame.display.flip() 251 | # clock.tick(50) 252 | 253 | if draw: 254 | pygame.quit() 255 | 256 | self.last_path = path 257 | dist = path_length(path) 258 | speed = dist / self.max_steps 259 | return {'fitness':1 + dist**2, 'dist':dist, 'speed':speed} 260 | 261 | def solve(self, network): 262 | stats = self.evaluate(network) 263 | return stats['speed'] > 0.20 264 | 265 | def visualize(self, network, filename=None): 266 | """ Visualize a solution strategy by the given individual. """ 267 | import matplotlib.pyplot as plt 268 | self.evaluate(network, draw=False, drawname=filename) 269 | print "Saving as " + os.path.join(os.getcwd(), filename) 270 | plt.figure() 271 | plt.imshow(self.field_observation * 0.2, cmap='Greys', vmin=0, vmax=1) 272 | for i in range(len(self.last_path)-1): 273 | plt.plot(*zip(*self.last_path[i:i+2]), lw=4, alpha=0.3, color=(0.3,0,0.8)) 274 | plt.ylim(0,512) 275 | plt.xlim(0,512) 276 | plt.axis('off') 277 | plt.savefig(filename, bbox_inches='tight', pad_inches=0) 278 | plt.close() 279 | 280 | 281 | 282 | 283 | if __name__ == '__main__': 284 | a = LineFollowingTask() 285 | -------------------------------------------------------------------------------- /peas/tasks/polebalance.py: -------------------------------------------------------------------------------- 1 | """ Implementation of the standard (multiple) pole 2 | balancing task. 3 | """ 4 | 5 | ### IMPORTS ### 6 | import random 7 | 8 | # Libraries 9 | import numpy as np 10 | 11 | # Local 12 | from ..networks.rnn import NeuralNetwork 13 | 14 | class PoleBalanceTask(object): 15 | """ Double pole balancing task. 16 | """ 17 | 18 | def __init__(self, gravity=-9.81, cart_mass=1.0, 19 | pole_mass=[0.1, 0.01], 20 | pole_length=[0.5, 0.05], 21 | track_limit=2.4, 22 | failure_angle=0.628, 23 | timestep=0.01, 24 | force_magnitude=10.0, 25 | start_random=False, 26 | penalize_oscillation=True, 27 | velocities=False, 28 | max_steps=1000): 29 | """ Constructor for PoleBalanceTask 30 | 31 | :param gravity: Gravity constant. 32 | :param cartmass: Mass of the cart. 33 | :param pole_mass: List of the mass of each pole. 34 | :param pole_length: List of the length of each pole, respectively. 35 | :param track_limit: Length of the track that the cart is on. 36 | :param failure_angle: Pole angle in radians at which the task is failed. 37 | :param timestep: Length of each time step. 38 | :param force_magnitude: The force that is exerted when an action of 1.0 is performed. 39 | :param start_random: Set the pole to a random position on each trial. 40 | :param penalize_oscillation: Uses the alternative fitness function described in (Stanley, 2002). 41 | :param velocities: Known velocities make the task markovian. 42 | :param max_steps: Maximum length of the simulation. 43 | """ 44 | 45 | self.g = gravity 46 | self.mc = cart_mass 47 | self.mp = np.array(pole_mass) 48 | self.l = np.array(pole_length) 49 | self.h = track_limit 50 | self.r = failure_angle 51 | self.f = force_magnitude 52 | self.t = timestep 53 | self.velocities = velocities 54 | self.start_random = start_random 55 | self.penalize_oscillation = penalize_oscillation 56 | self.max_steps = max_steps 57 | 58 | def _step(self, action, state): 59 | """ Performs a single simulation step. 60 | The state is a tuple of (x, dx, (p1, p2), (dp1, dp2)), 61 | """ 62 | x, dx, theta, dtheta = state 63 | 64 | f = (min(1.0, max(-1.0, action)) - 0.5) * self.f * 2.0; 65 | 66 | # Alternate equations 67 | fi = self.mp * self.l * dtheta**2 * np.sin(theta) + (3.0/4) * self.mp * np.cos(theta) * self.g * np.sin(theta) 68 | mi = self.mp * (1 - (3.0/4) * np.cos(theta)**2) 69 | ddx = f + np.sum(fi) / (self.mc + np.sum(mi)) 70 | ddtheta = (- 3.0 / (4 * self.l)) * (ddx * np.cos(theta) + self.g * np.sin(theta)) 71 | 72 | # Equations from "THE POLE BALANCING PROBLEM" 73 | # _ni = (-f - self.mp * self.l * dtheta**2 * np.sin(theta)) 74 | # m = self.mc + np.sum(self.mp) 75 | # _n = self.g * np.sin(theta) + np.cos(theta) * (_ni / m) 76 | # _d = self.l * (4./3. - (self.mp * np.cos(theta)**2) / m) 77 | # ddtheta = (_n / _d) 78 | # ddx = (f + np.sum(self.mp * self.l * np.floor(dtheta**2 * np.sin(theta) - ddtheta * np.cos(theta)))) / m 79 | 80 | x += self.t * dx 81 | dx += self.t * ddx 82 | theta += self.t * dtheta 83 | dtheta += self.t * ddtheta 84 | 85 | return (x, dx, theta, dtheta) 86 | 87 | def _loop(self, network, max_steps, initial=None, verbose=False): 88 | if initial is None: 89 | x, dx = 0.0, 0.0 90 | if self.start_random: 91 | theta = np.random.normal(0, 0.01, self.l.size) 92 | else: 93 | # Long pole starts at a fixed 1 degree angle. 94 | theta = np.array([0.017, 0.0]) 95 | dtheta = np.zeros(self.l.size) 96 | else: 97 | (x, dx, theta, dtheta) = initial 98 | steps = 0 99 | states = [] 100 | actions = [] 101 | while (steps < max_steps and 102 | np.abs(x) < self.h and 103 | (np.abs(theta) < self.r).all()): 104 | steps += 1 105 | if self.velocities: 106 | # Divide velocities by 2.0 because that is what neat-python does 107 | net_input = np.hstack((x/self.h, dx/2.0, theta/self.r, dtheta/2.0)) 108 | else: 109 | net_input = np.hstack((x/self.h, theta/self.r)) 110 | action = (network.feed( net_input )[-1] + 1) * 0.5 111 | (x, dx, theta, dtheta) = self._step(action, (x, dx, theta, dtheta)) 112 | actions.append(action) 113 | states.append((x, dx, theta.copy(), dtheta.copy())) 114 | if verbose: 115 | print states[-1] 116 | 117 | return steps, states, actions 118 | 119 | def evaluate(self, network, verbose=False): 120 | """ Perform a single run of this task """ 121 | # Convert to a network if it is not. 122 | 123 | if not isinstance(network, NeuralNetwork): 124 | network = NeuralNetwork(network) 125 | 126 | steps, states, _ = self._loop(network, max_steps=self.max_steps, verbose=verbose) 127 | 128 | if network.node_types[-1].__name__ != 'tanh': 129 | raise Exception("Network output must have range [-1, 1]") 130 | 131 | if self.penalize_oscillation: 132 | """ This bit implements the fitness function as described in 133 | Stanley - Evolving Neural Networks through Augmenting Topologies 134 | """ 135 | f1 = steps/float(self.max_steps) 136 | if steps < 100: 137 | f2 = 0 138 | else: 139 | wiggle = sum(abs(x) + abs(dx) + abs(t[0]) + abs(dt[0]) for 140 | (x, dx, t, dt) in states[-100:]) 141 | wiggle = max(wiggle, 0.01) # Cap the wiggle bonus 142 | f2 = 0.75 / wiggle 143 | score = 0.1 * f1 + 0.9 * f2 144 | else: 145 | """ This is just number of steps without falling. 146 | """ 147 | score = steps/float(self.max_steps) 148 | 149 | return {'fitness': score, 'steps': steps} 150 | 151 | def solve(self, network): 152 | """ This function should measure whether the network passes some 153 | threshold of "solving" the task. Returns False/0 if the 154 | network 'fails' the task. 155 | """ 156 | # Convert to a network if it is not. 157 | if not isinstance(network, NeuralNetwork): 158 | network = NeuralNetwork(network) 159 | 160 | steps, _, _ = self._loop(network, max_steps=100000) 161 | if steps < 100000: 162 | print "Failed 100k test with %d" % steps 163 | return 0 164 | successes = 0 165 | points = np.array([-0.9, -0.5, 0.0, 0.5, 0.9]) 166 | # The 1.35 and 0.15 were taken from the neat-python implementation. 167 | for x in points * self.h: 168 | for theta in points * self.r: 169 | for dx in points * 1.35: 170 | for dtheta0 in points * 0.15: 171 | state = (x, dx, np.array([theta, 0.0]), np.array([dtheta0, 0.0])) 172 | steps, states, _ = self._loop(network, initial=state, max_steps=1000) 173 | if steps >= 1000: 174 | successes += 1 175 | # return random.random() < 0.5 176 | return int(successes > 100) 177 | 178 | def visualize(self, network, f): 179 | """ Visualize a solution strategy by the given individual 180 | """ 181 | import matplotlib 182 | matplotlib.use('Agg',warn=False) 183 | import matplotlib.pyplot as plt 184 | # Convert to a network if it is not. 185 | if not isinstance(network, NeuralNetwork): 186 | network = NeuralNetwork(network) 187 | 188 | fig = plt.figure() 189 | steps, states, actions = self._loop(network, max_steps=1000) 190 | # TEMP STUFF 191 | actions = np.array(actions) 192 | print actions.size, np.histogram(actions)[0] 193 | ## 194 | x, dx, theta, dtheta = zip(*states) 195 | theta = np.vstack(theta).T 196 | dtheta = np.vstack(dtheta).T 197 | # The top plot (cart position) 198 | top = fig.add_subplot(211) 199 | top.fill_between(range(len(x)), -self.h, self.h, facecolor='green', alpha=0.3) 200 | top.plot(x, label=r'$x$') 201 | top.plot(dx, label=r'$\delta x$') 202 | top.legend(loc='lower left', ncol=4, fancybox=True, bbox_to_anchor=(0, 0, 1, 1)) 203 | # The bottom plot (pole angles) 204 | bottom = fig.add_subplot(212) 205 | bottom.fill_between(range(theta.shape[1]), -self.r, self.r, facecolor='green', alpha=0.3) 206 | for i, (t, dt) in enumerate(zip(theta, dtheta)): 207 | bottom.plot(t, label=r'$\theta_%d$'%i) 208 | bottom.plot(dt, ls='--', label=r'$\delta \theta_%d$'%i) 209 | bottom.legend(loc='lower left', ncol=4, fancybox=True, bbox_to_anchor=(0, 0, 1, 1)) 210 | fig.savefig(f) 211 | 212 | def __str__(self): 213 | vel = ('with' if self.velocities else 'without') + ' velocities' 214 | str = ('random' if self.start_random else 'fixed') + ' starts' 215 | r = '[%s] %s, %s' % (self.__class__.__name__, vel, str) 216 | return r 217 | 218 | 219 | if __name__ == '__main__': 220 | t = PoleBalanceTask() 221 | 222 | x, dx = 0.0, 0.0 223 | theta = np.array([0.017, 0]) 224 | dtheta = np.array([0.0, 0]) 225 | 226 | while (np.abs(theta) < t.r).all(): 227 | (x, dx, theta, dtheta) = t._step(0.5, (x, dx, theta, dtheta)) 228 | print theta 229 | 230 | 231 | -------------------------------------------------------------------------------- /peas/tasks/shapediscrimination.py: -------------------------------------------------------------------------------- 1 | """ Implementation of the Shape Discrimination task described in 2 | "Coleman - Evolving Neural Networks for Visual Processing" 3 | """ 4 | 5 | import random 6 | 7 | import numpy as np 8 | 9 | ### HELPER FUNCTION 10 | 11 | def line(im, x0, y0, x1, y1): 12 | """ Bresenham """ 13 | steep = abs(y1 - y0) > abs(x1 - x0) 14 | if steep: 15 | x0, y0 = y0, x0 16 | x1, y1 = y1, x1 17 | 18 | if x0 > x1: 19 | x0, x1 = x1, x0 20 | y0, y1 = y1, y0 21 | 22 | if y0 < y1: 23 | ystep = 1 24 | else: 25 | ystep = -1 26 | 27 | deltax = x1 - x0 28 | deltay = abs(y1 - y0) 29 | error = 0 30 | y = y0 31 | 32 | for x in range(x0, x1 + 1): # We add 1 to x1 so that the range includes x1 33 | if steep: 34 | im[y, x] = 1 35 | else: 36 | im[x, y] = 1 37 | 38 | error = error + deltay 39 | if (error << 1) >= deltax: 40 | y = y + ystep 41 | error = error - deltax 42 | 43 | 44 | class ShapeDiscriminationTask(object): 45 | 46 | def __init__(self, targetshape=None, 47 | distractorshapes=None, size=15, trials=75, fitnessmeasure='dist'): 48 | """ Constructor 49 | If target shape and distractor shape isn't passed, 50 | the setup from the visual field experiment (big-box-little-box) 51 | is used. Otherwise, use makeshape() to initalize the target and 52 | distractor shapes. 53 | """ 54 | self.target = targetshape 55 | self.distractors = distractorshapes 56 | self.size = size 57 | self.trials = trials 58 | self.fitnessmeasure = fitnessmeasure 59 | 60 | if self.target is None: 61 | self.target = self.makeshape('box', size//3) 62 | if self.distractors is None: 63 | self.distractors = [self.makeshape('box', 1)] 64 | 65 | print ":::: Target Shape ::::" 66 | print self.target 67 | print ":::: Distractor Shapes ::::" 68 | for d in self.distractors: 69 | print d 70 | 71 | @classmethod 72 | def makeshape(cls, shape, size=5): 73 | """ Create an image of the given shape. 74 | """ 75 | im = np.zeros((size, size)) 76 | xx, yy = np.mgrid[-1:1:size*1j, -1:1:size*1j] 77 | 78 | # Box used for big-box-little-box. 79 | if shape == 'box': 80 | im[:] = 1 81 | 82 | # Outlined square 83 | elif shape == 'square': 84 | im[:,0] = 1; 85 | im[0,:] = 1; 86 | im[:,-1] = 1; 87 | im[-1,:] = 1; 88 | 89 | # (roughly) a circle. 90 | elif shape == 'circle': 91 | d = np.sqrt(xx * xx + yy * yy) 92 | im[ np.logical_and(0.65 <= d, d <= 1.01) ] = 1 93 | 94 | # An single-pixel lined X 95 | elif shape == 'x': 96 | line(im, 0, 0, size-1, size-1) 97 | line(im, 0, size-1, size-1, 0) 98 | 99 | else: 100 | raise Exception("Shape Unknown.") 101 | 102 | return im 103 | 104 | def evaluate(self, network): 105 | if not network.sandwich: 106 | raise Exception("Object Discrimination task should be performed by a sandwich net.") 107 | 108 | dist = 0.0 109 | correct = 0.0 110 | wsose = 0.0 111 | pattern = np.zeros((self.size, self.size)) 112 | for _ in xrange(self.trials): 113 | pattern *= 0.0 114 | targetsize = self.target.shape[0] 115 | distractor = random.choice(self.distractors) 116 | distsize = distractor.shape[0] 117 | x, y = np.random.randint(self.size - targetsize, size=2) 118 | 119 | pattern[x:x+targetsize, y:y+targetsize] = self.target 120 | cx, cy = x + targetsize // 2, y + targetsize // 2 121 | 122 | for i in xrange(100): 123 | x, y = np.random.randint(self.size - distsize, size=2) 124 | if not np.any(pattern[x:x+distsize, y:y+distsize]): 125 | pattern[x:x+distsize, y:y+distsize] = distractor 126 | break 127 | if i == 99: 128 | raise Exception("No position found") 129 | 130 | network.flush() 131 | output = network.feed(pattern, add_bias=False) 132 | mx = output.argmax() 133 | (x_, y_) = mx // self.size, mx % self.size 134 | dist += np.sqrt(((x_ - cx) ** 2) + ((y_ - cy) ** 2)) 135 | if dist == 0: 136 | correct += 1 137 | wsose += 0.5 * (1 - output.flat[mx]) + 0.5 * output.mean() 138 | 139 | correct /= self.trials 140 | dist /= self.trials 141 | wsose /= self.trials 142 | 143 | 144 | if self.fitnessmeasure == 'dist': 145 | fitness = 1. / (1. + dist) 146 | elif self.fitnessmeasure == 'wsose': 147 | fitness = 0.5 * correct + 0.5 * (1 - wsose) 148 | return {'fitness':fitness, 'correct':correct, 'dist':dist, 'wsose':wsose} 149 | 150 | def solve(self, network): 151 | return self.evaluate(network)['dist'] < 0.5 152 | 153 | -------------------------------------------------------------------------------- /peas/tasks/targetweights.py: -------------------------------------------------------------------------------- 1 | """ Direct target task. Checks if the network resembles 2 | a target connection matrix. 3 | """ 4 | 5 | ### IMPORTS ### 6 | import math 7 | import os 8 | 9 | # Libraries 10 | import numpy as np 11 | from numpy.linalg import lstsq 12 | import scipy.misc 13 | 14 | # Local 15 | from ..networks.rnn import NeuralNetwork 16 | 17 | ### CLASSES ### 18 | 19 | class TargetWeightsTask(object): 20 | 21 | def __init__(self, default_weight=0, substrate_shape=(3,3), noise=0, max_weight=3.0, 22 | funcs=[], fitnessmeasure='absdiff', uniquefy=False, equalize=False, 23 | ): 24 | # Instance vars 25 | self.substrate_shape = substrate_shape 26 | self.max_weight = max_weight 27 | self.fitnessmeasure = fitnessmeasure 28 | if not (0 <= noise <= 1): 29 | raise Exception("Noise value has to be between 0 and 1.") 30 | # Build the connectivity matrix coords system 31 | cm_shape = list(substrate_shape) + list(substrate_shape) 32 | coords = np.mgrid[[slice(-1, 1, s*1j) for s in cm_shape]] 33 | self.locs = coords.transpose(range(1,len(cm_shape)+1) + [0]).reshape(-1, len(cm_shape)) 34 | self.locs = np.hstack((self.locs, np.ones((self.locs.shape[0], 1)))) 35 | # print self.locs 36 | cm = np.ones(cm_shape) * default_weight 37 | # Add weights 38 | for (where, what) in funcs: 39 | mask = where(coords) if callable(where) else (np.ones(cm.shape, dtype=bool)) 40 | vals = what(coords, cm) if callable(what) else (what * np.ones(cm.shape)) 41 | cm[mask] = vals[mask] 42 | 43 | # Add noise 44 | mask = np.random.random(cm.shape) < noise 45 | random_weights = np.random.random(cm.shape) * max_weight * 2 - max_weight 46 | cm[mask] = random_weights[mask] 47 | self.target = cm.reshape(np.product(substrate_shape), np.product(substrate_shape)) 48 | 49 | if uniquefy: 50 | vals,idxs = np.unique(self.target, return_inverse=True) 51 | rnd = np.random.random(vals.size) * 6. - 3. 52 | self.target = rnd[idxs] 53 | self.target = self.target.reshape(np.product(substrate_shape), np.product(substrate_shape)) 54 | 55 | 56 | # Clip 57 | self.target = np.clip(self.target, -max_weight, max_weight) 58 | 59 | if equalize and self.target.min() < self.target.max(): 60 | self.target = (self.target - self.target.min()) / (self.target.max() - self.target.min()) 61 | self.target = (self.target - 0.5) * 2 * max_weight 62 | 63 | def evaluate(self, network): 64 | if not isinstance(network, NeuralNetwork): 65 | network = NeuralNetwork(network) 66 | 67 | if network.cm.shape != self.target.shape: 68 | raise Exception("Network shape (%s) does not match target shape (%s)." % 69 | (network.cm.shape, self.target.shape)) 70 | 71 | diff = np.abs(network.cm - self.target) 72 | err = ((network.cm - self.target) ** 2).sum() 73 | x, res, _, _ = lstsq(self.locs, self.target.flat) 74 | # print self.target.flatten() 75 | # print res 76 | # print x 77 | res = res[0] 78 | diff_lsq = np.abs(np.dot(self.locs, x) - self.target.flat) 79 | diff_nonlin = diff_lsq.mean() - diff.mean() 80 | 81 | correct = (diff < (self.max_weight / 10.0)).mean() 82 | nonlinear = res - err 83 | 84 | if self.fitnessmeasure == 'absdiff': 85 | fitness = 2 ** ((2 * self.max_weight) - diff).mean() 86 | elif self.fitnessmeasure == 'sqerr': 87 | fitness = 1 / (1 + err) 88 | 89 | return {'fitness': fitness, 90 | 'error':err, 91 | 'diff':diff.mean(), 92 | 'diff_lsq':diff_lsq.mean(), 93 | 'correct':correct, 94 | 'err_nonlin':nonlinear, 95 | 'diff_nonlin': diff_nonlin, 96 | 'residue':res} 97 | 98 | def solve(self, network): 99 | return self.evaluate(network)['correct'] > 0.8 100 | 101 | def visualize(self, network, filename): 102 | import matplotlib 103 | matplotlib.use('Agg',warn=False) 104 | import matplotlib.pyplot as plt 105 | cm = network.cm 106 | target = self.target 107 | error = (cm - target) ** 2 108 | directory = os.path.dirname(filename) 109 | if not os.path.exists(directory): 110 | os.makedirs(directory) 111 | plt.imsave(filename, np.hstack((network.cm, self.target, error)), cmap=plt.cm.RdBu) 112 | 113 | 114 | if __name__ == '__main__': 115 | task = TargetWeightsTask() 116 | print task.target 117 | -------------------------------------------------------------------------------- /peas/tasks/walking.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | 3 | """ Walking gait task. 4 | """ 5 | 6 | ### IMPORTS ### 7 | import random 8 | 9 | # Libraries 10 | import pymunk 11 | import numpy as np 12 | 13 | # Local 14 | from ..networks.rnn import NeuralNetwork 15 | 16 | # Shortcuts 17 | pi = np.pi 18 | 19 | ### FUNCTIONS ### 20 | 21 | def angle_fix(theta): 22 | """ Fixes an angle to a value between -pi and pi. 23 | 24 | >>> angle_fix(-2*pi) 25 | 0.0 26 | """ 27 | return ((theta + pi) % (2*pi)) - pi 28 | 29 | ### CLASSES ### 30 | 31 | class Joint(object): 32 | """ Joint object, contains pivot, motor and limit""" 33 | 34 | def __init__(self, a, b, position, range=(-pi, pi), max_rate=2.0): 35 | self.pivot = pymunk.PivotJoint(a, b, position) 36 | self.motor = pymunk.SimpleMotor(a, b, 0) 37 | self.motor.max_force = 1e7 38 | self.limit = pymunk.RotaryLimitJoint(a, b, range[0], range[1]) 39 | self.limit.max_force = 1e1 40 | self.limit.max_bias = 0.5 41 | self.max_rate = max_rate 42 | 43 | def angle(self): 44 | return angle_fix(self.pivot.a.angle - self.pivot.b.angle) 45 | 46 | def set_target(self, target): 47 | """ Target is between 0 and 1, representing min and max angle. """ 48 | cur = self.angle() 49 | tgt = angle_fix(target * (self.limit.max - self.limit.min) + self.limit.min) 50 | if tgt > cur + 0.1: 51 | self.motor.rate = self.max_rate 52 | elif tgt < cur - 0.1: 53 | self.motor.rate = -self.max_rate 54 | else: 55 | self.motor.rate = 0 56 | 57 | class Leg(object): 58 | """ Leg object, contains joints and shapes """ 59 | 60 | def __init__(self, parent, position, walking_task): 61 | self.walking_task = walking_task 62 | (w, l) = walking_task.leg_length / 5.0, walking_task.leg_length 63 | mass = w * l * 0.2 64 | # Upper leg 65 | upperleg = pymunk.Body(mass, pymunk.moment_for_box(mass, w, l)) 66 | upperleg.position = pymunk.Vec2d(parent.position) + pymunk.Vec2d(position) + pymunk.Vec2d(0, l/2.0 - w/2.0) 67 | shape = pymunk.Poly.create_box(upperleg, (w,l)) 68 | shape.group = 1 69 | shape.friction = 2.0 70 | # Joints 71 | pos = pymunk.Vec2d(parent.position) + pymunk.Vec2d(position) 72 | hip = Joint(parent, upperleg, pos, (-0.1*pi, 0.9*pi), self.walking_task.max_rate) 73 | walking_task.space.add(hip.pivot, hip.motor, hip.limit, upperleg, shape) 74 | 75 | # Lower leg 76 | lowerleg = pymunk.Body(mass, pymunk.moment_for_box(mass, w, l * 1.2)) 77 | lowerleg.position = pymunk.Vec2d(upperleg.position) + pymunk.Vec2d(0, l - w/2.0) 78 | shape = pymunk.Poly.create_box(lowerleg, (w, l * 1.2)) 79 | shape.group = 1 80 | shape.friction = 2.0 81 | # Joints 82 | pos = pymunk.Vec2d(upperleg.position) + pymunk.Vec2d(0, l/2.0 - w/2.0) 83 | knee = Joint(upperleg, lowerleg, pos, (-0.9*pi, 0.1*pi), self.walking_task.max_rate) 84 | walking_task.space.add(knee.pivot, knee.motor, knee.limit, lowerleg, shape) 85 | 86 | self.upperleg = upperleg 87 | self.lowerleg = lowerleg 88 | self.hip = hip 89 | self.knee = knee 90 | 91 | 92 | class WalkingTask(object): 93 | """ Walking gait task. 94 | """ 95 | 96 | def __init__(self, max_steps=1000, 97 | track_length=1000, 98 | max_rate=2.0, 99 | torso_height=40, 100 | torso_density=0.2, 101 | leg_spacing=30, 102 | leg_length=30, 103 | num_legs=4): 104 | # Settings 105 | self.max_steps = max_steps 106 | self.track_length = track_length 107 | self.max_rate = max_rate 108 | self.torso_height = torso_height 109 | self.torso_density = torso_density 110 | self.leg_spacing = leg_spacing 111 | self.leg_length = leg_length 112 | self.num_legs = num_legs 113 | 114 | def evaluate(self, network, draw=False): 115 | """ Evaluate the efficiency of the given network. Returns the 116 | distance that the walker ran in the given time (max_steps). 117 | """ 118 | if not isinstance(network, NeuralNetwork): 119 | network = NeuralNetwork(network) 120 | 121 | if draw: 122 | import pygame 123 | pygame.init() 124 | screen = pygame.display.set_mode((self.track_length, 200)) 125 | pygame.display.set_caption("Simulation") 126 | clock = pygame.time.Clock() 127 | running = True 128 | font = pygame.font.Font(pygame.font.get_default_font(), 8) 129 | 130 | # Initialize pymunk 131 | self.space = space = pymunk.Space() 132 | space.gravity = (0.0, 900.0) 133 | space.damping = 0.7 134 | self.touching_floor = False 135 | # Create objects 136 | # Floor 137 | 138 | floor = pymunk.Body() 139 | floor.position = pymunk.Vec2d(self.track_length/2.0 , 210) 140 | sfloor = pymunk.Poly.create_box(floor, (self.track_length, 40)) 141 | sfloor.friction = 1.0 142 | sfloor.collision_type = 1 143 | space.add_static(sfloor) 144 | 145 | # Torso 146 | torsolength = 20 + (self.num_legs // 2 - 1) * self.leg_spacing 147 | mass = torsolength * self.torso_height * self.torso_density 148 | torso = pymunk.Body(mass, pymunk.moment_for_box(mass, torsolength, self.torso_height)) 149 | torso.position = pymunk.Vec2d(200, 200 - self.leg_length * 2 - self.torso_height) 150 | storso = pymunk.Poly.create_box(torso, (torsolength, self.torso_height)) 151 | storso.group = 1 152 | storso.collision_type = 1 153 | storso.friction = 2.0 154 | # space.add_static(storso) 155 | space.add(torso, storso) 156 | 157 | # Legs 158 | legs = [] 159 | for i in range(self.num_legs // 2): 160 | x = 10 - torsolength / 2.0 + i * self.leg_spacing 161 | y = self.torso_height / 2.0 - 10 162 | legs.append( Leg(torso, (x,y), self) ) 163 | legs.append( Leg(torso, (x,y), self) ) 164 | 165 | # Collision callback 166 | def oncollide(space, arb): 167 | self.touching_floor = True 168 | space.add_collision_handler(1, 1, post_solve=oncollide) 169 | 170 | for step in xrange(self.max_steps): 171 | 172 | # Query network 173 | input_width = max(len(legs), 4) 174 | net_input = np.zeros((3, input_width)) 175 | torso_y = torso.position.y 176 | torso_a = torso.angle 177 | sine = np.sin(step / 10.0) 178 | hip_angles = [leg.hip.angle() for leg in legs] 179 | knee_angles = [leg.knee.angle() for leg in legs] 180 | other = [torso_y, torso_a, sine, 1.0] 181 | # Build a 2d input grid, 182 | # as in Clune 2009 Evolving Quadruped Gaits, p4 183 | net_input[0, :len(legs)] = hip_angles 184 | net_input[1, :len(legs)] = knee_angles 185 | net_input[2, :4] = other 186 | act = network.feed(net_input, add_bias=False) 187 | 188 | output = np.clip(act[-self.num_legs*2:] * self.max_rate, -1.0, 1.0) / 2.0 + 0.5 189 | 190 | for i, leg in enumerate(legs): 191 | leg.hip.set_target( output[i * 2] ) 192 | leg.knee.set_target( output[i * 2 + 1] ) 193 | 194 | # Advance simulation 195 | space.step(1/50.0) 196 | # Check for success/failure 197 | if torso.position.x < 0: 198 | break 199 | if torso.position.x > self.track_length - 50: 200 | break 201 | if self.touching_floor: 202 | break 203 | 204 | # Draw 205 | if draw: 206 | print act 207 | # Clear 208 | screen.fill((255, 255, 255)) 209 | # Do all drawing 210 | txt = font.render('%d' % step, False, (0,0,0) ) 211 | screen.blit(txt, (0,0)) 212 | # Draw objects 213 | for o in space.shapes + space.static_shapes: 214 | if isinstance(o, pymunk.Circle): 215 | pygame.draw.circle(screen, (0,0,0), (int(o.body.position.x), int(o.body.position.y)), int(o.radius)) 216 | else: 217 | pygame.draw.lines(screen, (0,0,0), True, [(int(p.x), int(p.y)) for p in o.get_points()]) 218 | # Flip buffers 219 | pygame.display.flip() 220 | clock.tick(50) 221 | 222 | if draw: 223 | pygame.quit() 224 | 225 | distance = torso.position.x 226 | # print "Travelled %.2f in %d steps." % (distance, step) 227 | return {'fitness':distance} 228 | 229 | def solve(self, network): 230 | return False 231 | 232 | def visualize(self, network, filename=None): 233 | """ Visualize a solution strategy by the given individual. """ 234 | self.evaluate(network, draw=True) 235 | 236 | 237 | -------------------------------------------------------------------------------- /peas/tasks/xor.py: -------------------------------------------------------------------------------- 1 | """ Input/output relation task. Every input and output 2 | is explicitly defined. XOR is an example of this task. 3 | """ 4 | 5 | ### IMPORTS ### 6 | import random 7 | 8 | # Libraries 9 | import numpy as np 10 | 11 | # Local 12 | from ..networks.rnn import NeuralNetwork 13 | 14 | 15 | class XORTask(object): 16 | 17 | # Default XOR input/output pairs 18 | INPUTS = [(0,0), (0,1), (1,0), (1,1)] 19 | OUTPUTS = [(-1,), (1,), (1,), (-1,)] 20 | EPSILON = 1e-100 21 | 22 | def __init__(self, do_all=True): 23 | self.do_all = do_all 24 | self.INPUTS = np.array(self.INPUTS, dtype=float) 25 | self.OUTPUTS = np.array(self.OUTPUTS, dtype=float) 26 | 27 | def evaluate(self, network, verbose=False): 28 | if not isinstance(network, NeuralNetwork): 29 | network = NeuralNetwork(network) 30 | 31 | network.make_feedforward() 32 | 33 | if not network.node_types[-1](-1000) < -0.95: 34 | raise Exception("Network should be able to output value of -1, e.g. using a tanh node.") 35 | 36 | pairs = zip(self.INPUTS, self.OUTPUTS) 37 | random.shuffle(pairs) 38 | if not self.do_all: 39 | pairs = [random.choice(pairs)] 40 | rmse = 0.0 41 | for (i, target) in pairs: 42 | # Feed with bias 43 | output = network.feed(i) 44 | # Grab the output 45 | output = output[-len(target):] 46 | err = (target - output) 47 | err[abs(err) < self.EPSILON] = 0; 48 | err = (err ** 2).mean() 49 | # Add error 50 | if verbose: 51 | print "%r -> %r (%.2f)" % (i, output, err) 52 | rmse += err 53 | 54 | score = 1/(1+np.sqrt(rmse / len(pairs))) 55 | return {'fitness': score} 56 | 57 | def solve(self, network): 58 | return int(self.evaluate(network) > 0.9) 59 | 60 | -------------------------------------------------------------------------------- /peas/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noio/peas/4c2503388644cc5ca58c501e6c773d53a883538b/peas/test/__init__.py -------------------------------------------------------------------------------- /peas/test/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ Testing module for PEAS package 3 | 4 | Running this script will test if the SOME of the modules are 5 | working properly. 6 | """ 7 | 8 | ### IMPORTS 9 | 10 | # Python Imports 11 | import os 12 | import sys 13 | import unittest 14 | 15 | # Libraries 16 | import numpy as np 17 | 18 | # Package 19 | sys.path.append(os.path.join(os.path.split(__file__)[0],'..','..')) 20 | from peas.networks import rnn 21 | from peas.methods import neat 22 | from peas.tasks import xor 23 | 24 | ### CONSTANTS 25 | 26 | ### CLASSES 27 | 28 | class TestPEAS(unittest.TestCase): 29 | 30 | def test_rnn(self): 31 | net = rnn.NeuralNetwork().from_matrix(np.array([[0,0,0],[0,0,0],[1,1,0]])) 32 | output = net.feed(np.array([1,1]), add_bias=False) 33 | self.assertEqual(output[-1], rnn.sigmoid(1 + 1)) 34 | 35 | def test_neat(self): 36 | task = xor.XORTask() 37 | genotype = lambda: neat.NEATGenotype(inputs=2, types=['tanh']) 38 | pop = neat.NEATPopulation(genotype) 39 | pop.epoch(task, 3) 40 | 41 | def test_rbfneat(self): 42 | def evaluate(network): 43 | cm, nt = network.get_network_data() 44 | if nt[-1] == 'rbfgauss': 45 | net = rnn.NeuralNetwork(network) 46 | e1 = net.feed(np.array([0, 0]), add_bias=False)[-1] 47 | mid = net.feed(np.array([2, -2]), add_bias=False)[-1] 48 | e2 = net.feed(np.array([4, -4]), add_bias=False)[-1] 49 | return {'fitness': mid - (e1 + e2)} 50 | else: 51 | return {'fitness': 0} 52 | 53 | 54 | genotype = lambda: neat.NEATGenotype(inputs=2, types=['tanh', 'rbfgauss'], max_nodes=5) 55 | pop = neat.NEATPopulation(genotype) 56 | pop.epoch(evaluate, 20) 57 | 58 | 59 | def run_tests(): 60 | suite = unittest.TestLoader().loadTestsFromTestCase(TestPEAS) 61 | unittest.TextTestRunner(verbosity=2).run(suite) 62 | 63 | if __name__ == "__main__": 64 | run_tests() 65 | --------------------------------------------------------------------------------