├── .gitignore ├── Constants.py ├── README.md ├── data_fetcher.py ├── input_data.py ├── main.py ├── standard_neat.py └── tensorflow_utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/pycharm,python 3 | 4 | ### PyCharm ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff: 9 | .idea/workspace.xml 10 | .idea/tasks.xml 11 | .idea/dictionaries 12 | .idea/vcs.xml 13 | .idea/jsLibraryMappings.xml 14 | 15 | # Sensitive or high-churn files: 16 | .idea/dataSources.ids 17 | .idea/dataSources.xml 18 | .idea/dataSources.local.xml 19 | .idea/sqlDataSources.xml 20 | .idea/dynamic.xml 21 | .idea/uiDesigner.xml 22 | 23 | # Gradle: 24 | .idea/gradle.xml 25 | .idea/libraries 26 | 27 | # Mongo Explorer plugin: 28 | .idea/mongoSettings.xml 29 | 30 | ## File-based project format: 31 | *.iws 32 | 33 | ## Plugin-specific files: 34 | 35 | # IntelliJ 36 | /out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Crashlytics plugin (for Android Studio and IntelliJ) 45 | com_crashlytics_export_strings.xml 46 | crashlytics.properties 47 | crashlytics-build.properties 48 | fabric.properties 49 | 50 | ### PyCharm Patch ### 51 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 52 | 53 | # *.iml 54 | # modules.xml 55 | # .idea/misc.xml 56 | # *.ipr 57 | 58 | 59 | ### Python ### 60 | # Byte-compiled / optimized / DLL files 61 | __pycache__/ 62 | *.py[cod] 63 | *$py.class 64 | 65 | # C extensions 66 | *.so 67 | 68 | # Distribution / packaging 69 | .Python 70 | env/ 71 | build/ 72 | develop-eggs/ 73 | dist/ 74 | downloads/ 75 | eggs/ 76 | .eggs/ 77 | lib/ 78 | lib64/ 79 | parts/ 80 | sdist/ 81 | var/ 82 | *.egg-info/ 83 | .installed.cfg 84 | *.egg 85 | 86 | # PyInstaller 87 | # Usually these files are written by a python script from a template 88 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 89 | *.manifest 90 | *.spec 91 | 92 | # Installer logs 93 | pip-log.txt 94 | pip-delete-this-directory.txt 95 | 96 | # Unit test / coverage reports 97 | htmlcov/ 98 | .tox/ 99 | .coverage 100 | .coverage.* 101 | .cache 102 | nosetests.xml 103 | coverage.xml 104 | *,cover 105 | .hypothesis/ 106 | 107 | # Translations 108 | *.mo 109 | *.pot 110 | 111 | # Django stuff: 112 | *.log 113 | local_settings.py 114 | 115 | # Flask stuff: 116 | instance/ 117 | .webassets-cache 118 | 119 | # Scrapy stuff: 120 | .scrapy 121 | 122 | # Sphinx documentation 123 | docs/_build/ 124 | 125 | # PyBuilder 126 | target/ 127 | 128 | # IPython Notebook 129 | .ipynb_checkpoints 130 | 131 | # pyenv 132 | .python-version 133 | 134 | # celery beat schedule file 135 | celerybeat-schedule 136 | 137 | # dotenv 138 | .env 139 | 140 | # virtualenv 141 | venv/ 142 | ENV/ 143 | 144 | # Spyder project settings 145 | .spyderproject 146 | 147 | # Rope project settings 148 | .ropeproject 149 | -------------------------------------------------------------------------------- /Constants.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | INPUT0 = 0 4 | INPUT1 = 1 5 | OUTPUT0 = 10000#sys.maxint - 1 6 | OUTPUT1 = 10001#sys.maxint -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tensorflow Neat 2 | -------------------------------------------------------------------------------- /data_fetcher.py: -------------------------------------------------------------------------------- 1 | """generates training data""" 2 | 3 | 4 | from tensorflow.examples.tutorials.mnist import input_data 5 | from sklearn.datasets.samples_generator import make_gaussian_quantiles 6 | import numpy as np 7 | 8 | def get_mnist(): 9 | """retrieves tensorflow version of mnist""" 10 | mnist = input_data.read_data_sets("MNIST_data/", one_hot=True) 11 | return mnist 12 | 13 | 14 | def get_gaussian_quantiles(n_samples=1000): 15 | x, y = make_gaussian_quantiles(n_samples=n_samples, n_features=2, n_classes=2) 16 | y = np.asarray([[0., 1.] if y_ == 0 else [1., 0,] for y_ in y]) 17 | 18 | x = x.astype(np.float32) 19 | y = y.astype(np.float32) 20 | return x,y 21 | 22 | def generate_xor(n_samples=1000): 23 | # num_samples,num_features 24 | X1 = np.random.randint(0,2,[n_samples,2]) 25 | y = np.logical_xor(X1[:,0],X1[:,1]).astype(np.float32) 26 | y = np.asarray([[0,1] if y_ == 0 else [1,0] for y_ in y]) 27 | return X1, y 28 | -------------------------------------------------------------------------------- /input_data.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 The TensorFlow Authors. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # ============================================================================== 15 | 16 | """Functions for downloading and reading MNIST data.""" 17 | from __future__ import absolute_import 18 | from __future__ import division 19 | from __future__ import print_function 20 | 21 | import gzip 22 | import os 23 | import tempfile 24 | 25 | import numpy 26 | from six.moves import urllib 27 | from six.moves import xrange # pylint: disable=redefined-builtin 28 | import tensorflow as tf 29 | from tensorflow.contrib.learn.python.learn.datasets.mnist import read_data_sets 30 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from data_fetcher import get_gaussian_quantiles, generate_xor 2 | from standard_neat import start_neuroevolution 3 | 4 | 5 | 6 | # X, y = generate_xor(n_samples=1000) 7 | # X_test, y_test = generate_xor(n_samples=10) 8 | 9 | 10 | x, y = generate_xor(n_samples=100) 11 | x_test, y_test = generate_xor(n_samples=100) 12 | 13 | 14 | print x.shape 15 | print y.shape 16 | start_neuroevolution(x, y, x_test, y_test) 17 | 18 | # build_and_test(connections, genotype, x, y, x_test, y_test) 19 | -------------------------------------------------------------------------------- /standard_neat.py: -------------------------------------------------------------------------------- 1 | from Constants import OUTPUT0, OUTPUT1, INPUT1, INPUT0 2 | from tensorflow_utils import build_and_test 3 | import numpy as np 4 | 5 | 6 | def add_connection(connections, genotype): 7 | enabled_innovations = [k for k in genotype.keys() if genotype[k]] 8 | 9 | enabled_connections = [connections[cns] for cns in enabled_innovations] 10 | 11 | # get reachable nodes 12 | froms = set([fr[1] for fr in enabled_connections ]) 13 | tos = set([to[2] for to in enabled_connections]) 14 | 15 | nodes = sorted(list(froms.union(tos))) 16 | 17 | # select random two: 18 | r1 = np.random.randint(0,len(nodes)) 19 | r2 = np.random.randint(0,len(nodes) - 1) 20 | if r2 >= r1: 21 | r2 += 1 22 | 23 | r1 = nodes[r1] 24 | r2 = nodes[r2] 25 | from_node = r2 if r2 < r1 else r1 26 | to_node = r2 if r2 > r1 else r1 27 | 28 | assert(from_node < to_node) 29 | 30 | # prevent connections from input to input nodes and output to output nodes. 31 | # todo change this 32 | if from_node == INPUT0 and to_node == INPUT1 or from_node == OUTPUT0 and to_node == OUTPUT1: 33 | return add_connection( connections, genotype) 34 | 35 | # check if connection already there 36 | if not any(from_node == c[1] and to_node == c[2] for c in connections): 37 | connections.append((len(connections), from_node, to_node)) 38 | 39 | genotype[len(connections) - 1 ] = True 40 | 41 | assert(len(genotype.keys()) <= len(connections)) 42 | return connections, genotype 43 | 44 | 45 | def add_node(connections, genotype, debug=False): 46 | # select random connection that is enabled 47 | enabled_innovations = [k for k in genotype.keys() if genotype[k]] 48 | 49 | # get random connection: 50 | r = np.random.randint(0,len(enabled_innovations)) 51 | connections_innovation_index = enabled_innovations[r] 52 | connection_to_split = connections[connections_innovation_index] 53 | 54 | from_node = connection_to_split[1] 55 | to_node = connection_to_split[2] 56 | 57 | new_node = (to_node - from_node) / 2 + from_node 58 | 59 | if debug: 60 | print "from:", from_node 61 | print "to:", to_node 62 | print "new:", new_node 63 | # todo: what to do if node id already exist? -> just leave it be. 64 | 65 | # add two new connection items: from_node -> new_node; new_node -> to_node 66 | # check if already existing beforehand. 67 | # todo: there should be a smarter way to do this than just give up. 68 | if not from_node < new_node: 69 | return connections, genotype 70 | if not new_node < to_node: 71 | return connections, genotype 72 | assert(from_node < new_node) 73 | assert(new_node < to_node) 74 | # check from to 75 | if not any(from_node == c[1] and new_node == c[2] for c in connections): 76 | id = len(connections) 77 | connections.append((id, from_node, new_node)) 78 | genotype[id] = True 79 | else: 80 | ind = [c[0] for c in connections if c[1] == from_node and c[2] == new_node] 81 | genotype[ind[0]] = True 82 | 83 | if not any(new_node == c[1] and to_node == c[2] for c in connections): 84 | id = len(connections) 85 | connections.append((id, new_node, to_node)) 86 | genotype[id] = True 87 | else: 88 | ind = [c[0] for c in connections if new_node == c[1] and to_node == c[2]] 89 | genotype[ind[0]] = True 90 | 91 | # add new node 92 | # disable old connection where we now inserted a new node 93 | genotype[connections_innovation_index] = False 94 | 95 | assert (len(genotype.keys()) <= len(connections)) 96 | 97 | return connections, genotype 98 | 99 | 100 | 101 | 102 | def crossover(connections, genotype0, performance0 , genotype1, performance1): 103 | # 1. matching genes are inherited at random (everything is made up and the weights don't matter here) 104 | # 2. disjoint and excess from the more fit parent 105 | # 3. preset chance to disable gene if its disabled in either parent 106 | 107 | # new genes should be always in the end 108 | k_0 = sorted(genotype0.keys()) 109 | k_1 = sorted(genotype1.keys()) 110 | 111 | # inherit disjoint from more fit parent 112 | offspring_genotype = {} 113 | if performance0 > performance1 and len(k_0) > len(k_1): 114 | # 0 is better and has more genes 115 | for l in connections: 116 | innovation_num = l[0] 117 | if innovation_num in k_0: 118 | offspring_genotype[innovation_num] = genotype0[innovation_num] 119 | elif innovation_num in k_1: 120 | offspring_genotype[innovation_num] = genotype1[innovation_num] 121 | 122 | elif performance1 > performance0 and len(k_1) > len(k_0): 123 | for l in connections: 124 | innovation_num = l[0] 125 | if innovation_num in k_1: 126 | offspring_genotype[innovation_num] = genotype1[innovation_num] 127 | elif innovation_num in k_0: 128 | offspring_genotype[innovation_num] = genotype0[innovation_num] 129 | 130 | elif len(k_1) < len(k_0): 131 | for k in k_1: 132 | offspring_genotype[k] = genotype1[k] 133 | 134 | elif len(k_0) <= len(k_1): 135 | for k in k_0: 136 | offspring_genotype[k] = genotype0[k] 137 | 138 | return offspring_genotype 139 | 140 | 141 | def eval_fitness(connections, genotype, x, y, x_test, y_test, run_id="1"): 142 | perf_train = build_and_test(connections, genotype, x, y, x_test, y_test, run_id=run_id) 143 | return perf_train 144 | 145 | def start_neuroevolution(x, y, x_test, y_test): 146 | """starts neuroevolution on binary dataset""" 147 | 148 | connections = [(0, INPUT0, OUTPUT0), (1, INPUT1, OUTPUT0), (2, INPUT0, OUTPUT1), (3, INPUT1, OUTPUT1)] 149 | genotypes = [{0: True, 1: True, 2: True, 3: True} for d in xrange(5)] 150 | 151 | for its in xrange(0,5): 152 | print "iteration", its 153 | 154 | fitnesses = [] 155 | # test networks 156 | for i in xrange(0,len(genotypes)): 157 | fitnesses.append(eval_fitness(connections, genotypes[i], x, y, x_test, y_test, run_id=str(its) + "/" + str(i))) 158 | 159 | # get indices of sorted list 160 | fitnesses_sorted_indices = [i[0] for i in reversed(sorted(enumerate(fitnesses), key=lambda x: x[1]))] 161 | 162 | print "connections:\n" 163 | print connections 164 | for ra in xrange(0,len(fitnesses_sorted_indices)): 165 | print fitnesses[fitnesses_sorted_indices[ra]], genotypes[fitnesses_sorted_indices[ra]] 166 | 167 | # run evolutions 168 | # todo: fiddle with parameters, include size of network in fitness? 169 | new_gen = [] 170 | # copy five best survivors already 171 | m = 5 172 | if m > len(fitnesses): 173 | m = len(fitnesses) 174 | 175 | for i in xrange(0,m): 176 | print "adding:", fitnesses[fitnesses_sorted_indices[i]], genotypes[fitnesses_sorted_indices[i]] 177 | new_gen.append(genotypes[fitnesses_sorted_indices[i]]) 178 | 179 | for i in xrange(0,len(fitnesses_sorted_indices)): 180 | fi = fitnesses_sorted_indices[i] 181 | r = np.random.uniform() 182 | # select the best for mutation and breeding, kill of worst. 183 | if r <= 0.2: 184 | # mutate 185 | connections, gen = add_connection(connections, genotypes[i]) 186 | new_gen.append(gen) 187 | r = np.random.uniform() 188 | if r <= 0.5: 189 | connections, gen = add_node(connections, genotypes[i]) 190 | new_gen.append(gen) 191 | 192 | r = np.random.uniform() 193 | if r <= 0.1: 194 | # select random for breeding 195 | r = np.random.randint(0,len(fitnesses)) 196 | r2 = np.random.randint(0,len(fitnesses) - 1) 197 | if r2 >= r: 198 | r2 +=1 199 | gen = crossover(connections, genotypes[r], fitnesses[r], genotypes[r2], fitnesses[r2]) 200 | new_gen.append(gen) 201 | new_gen.append(genotypes[fi]) 202 | # stop if we have 5 candidates 203 | if len(new_gen) > 10: 204 | break 205 | genotypes = new_gen 206 | -------------------------------------------------------------------------------- /tensorflow_utils.py: -------------------------------------------------------------------------------- 1 | """builds graph from genes""" 2 | import tensorflow as tf 3 | import numpy as np 4 | import math 5 | from Constants import OUTPUT0, OUTPUT1, INPUT0, INPUT1 6 | 7 | def add_node(inputs,name="stdname"): 8 | with tf.name_scope(name): 9 | init_vals = tf.truncated_normal([len(inputs), 1], stddev=1. / math.sqrt(2)) 10 | w = tf.Variable(init_vals) 11 | b = tf.Variable(tf.zeros([1])) 12 | 13 | if len(inputs) > 1: 14 | in_tensor = tf.transpose(tf.squeeze(tf.stack(inputs))) 15 | output = tf.nn.relu(tf.matmul(in_tensor, w) + b, name=name) 16 | return output 17 | 18 | else: 19 | in_tensor = tf.squeeze(tf.stack(inputs)) 20 | output = tf.transpose(tf.nn.relu(tf.multiply(in_tensor, w) + b, name=name)) 21 | return output 22 | 23 | 24 | def build_and_test(connections, genotype, x, y, x_test, y_test, run_id="1"): 25 | 26 | with tf.name_scope("input"): 27 | x0 = tf.placeholder(shape=[None,1], dtype=tf.float32, name="x0") 28 | x1 = tf.placeholder(shape=[None,1], dtype=tf.float32, name="x1") 29 | 30 | with tf.name_scope("ground_truth"): 31 | y_ = tf.placeholder(shape=[None, 2], dtype=tf.float32, name="y_") 32 | 33 | # connections contains only (innovation_num, from, to) 34 | # genotype contains only {innovation_num: True/False) 35 | 36 | # need a list containing [node, inputs] 37 | # for all the same to's from connections collect from's (where genotype exists and says enabled) 38 | # connection can only exist from lower number to higher in the network 39 | 40 | # filter out disabled and non existent genes in this phenotype 41 | genotype_keys = sorted(genotype.keys()) 42 | 43 | # filter connections 44 | exisiting_connections = [] 45 | for i in xrange(0,len(genotype_keys)): 46 | if genotype[genotype_keys[i]]: 47 | exisiting_connections.append(connections[genotype_keys[i]]) 48 | 49 | # collect nodes and connections: sort by to field from connections 50 | connections_sorted = sorted(exisiting_connections, key=lambda connections: connections[2]) 51 | 52 | # merge the same nodes 53 | connections_merged = [[connections_sorted[0][2],[connections_sorted[0][1]]]] 54 | for i in xrange(1,len(connections_sorted)): 55 | # same as last node 56 | if connections_sorted[i][2] == connections_merged[-1][0]: 57 | connections_merged[-1][1].append(connections_sorted[i][1]) 58 | else: 59 | connections_merged.append([connections_sorted[i][2],[connections_sorted[i][1]]]) 60 | 61 | tf_nodes_dict = {INPUT0: x0, INPUT1: x1} 62 | for cn in connections_merged: 63 | node_cons = cn[1] 64 | node_id = cn[0] 65 | tf_input_nodes = [tf_nodes_dict[node_key] for node_key in node_cons] 66 | node_name = str(node_id) + "_" 67 | for na in node_cons: 68 | node_name += "_" + str(na) 69 | tf_nodes_dict[cn[0]] = add_node(tf_input_nodes, name=node_name) 70 | 71 | num_nodes = tf.constant(len(tf_nodes_dict.keys())) 72 | tf.summary.scalar("num_nodes", num_nodes) 73 | 74 | with tf.name_scope("softmax_output"): 75 | # requery output nodes to add to optimization 76 | output_0 = tf_nodes_dict[OUTPUT0] 77 | output_1 = tf_nodes_dict[OUTPUT1] 78 | 79 | output_final_pre = tf.transpose(tf.squeeze(tf.stack([output_0,output_1]))) 80 | 81 | W_o1 = tf.Variable(tf.truncated_normal([2, 2], stddev=1. / math.sqrt(2))) 82 | b_o1 = tf.Variable(tf.zeros([2])) 83 | output_final = tf.nn.softmax(tf.matmul(output_final_pre,W_o1) + b_o1, name="output_softmax") 84 | 85 | with tf.name_scope("loss"): 86 | cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits = output_final, labels = y_) 87 | loss = tf.reduce_mean(cross_entropy) 88 | tf.summary.scalar("loss",loss) 89 | 90 | with tf.name_scope("optimizer"): 91 | opt = tf.train.GradientDescentOptimizer(0.5).minimize(loss) 92 | 93 | with tf.name_scope("accuracy"): 94 | correct_prediction = tf.equal(tf.argmax(output_final, 1), tf.argmax(y_, 1)) 95 | accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) 96 | # accuracy = tf.reduce_mean(tf.abs(output_final - y_)) 97 | tf.summary.scalar("accuracy", accuracy) 98 | 99 | init = tf.global_variables_initializer() 100 | sess = tf.Session() 101 | 102 | tf.summary.merge_all() 103 | merged = tf.summary.merge_all() 104 | train_writer = tf.summary.FileWriter('./train/' + run_id, 105 | sess.graph) 106 | 107 | sess.run(init) 108 | 109 | for i in xrange(1000): 110 | _, loss_val, summary = sess.run([opt, loss, merged], feed_dict={x0: np.expand_dims(x[:, 0], 1),x1: np.expand_dims(x[:, 1], 1), y_: y}) 111 | if i % 100 == 0: 112 | train_writer.add_summary(summary, i) 113 | 114 | acc_test = sess.run([accuracy], feed_dict={x0: np.expand_dims(x_test[:, 0], 1), x1: np.expand_dims(x_test[:, 1], 1), y_: y_test}) 115 | sess.close() 116 | tf.reset_default_graph() 117 | return acc_test[0] 118 | --------------------------------------------------------------------------------