Skip to content

Commit 51c7bfa

Browse files
Updated samples and their documentation.
1 parent 7fdb3fd commit 51c7bfa

13 files changed

+612
-5
lines changed

examples/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ directly related to the NEAT library usage.
66

77
## The examples ##
88

9+
* `xor` A "hello world" sample showing basic usage.
10+
911
* `circuits` Uses an external circuit simulator (PySpice) to create electronic circuits that reproduce an arbitrary function of the input voltage.
1012

1113
* `memory-fixed` Reproduce a fixed-length sequence of binary inputs.

examples/circuits/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
## Circuit example ##
2+
3+
The scripts in this directory show how to evolve a network that describes an electronic
4+
circuit that reproduces an arbitrary function of the input voltage.
5+
6+
Note that there is a significant amount of duplication between these scripts, and this is intentional. The goal is to
7+
make it easier to see what the example is doing, without making the user dig through a bunch of code that is not
8+
directly related to the NEAT library usage.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
## Single-pole balancing examples ##
2+
3+
The scripts in this directory show how to evolve a network that balances a pole on top of a movable cart. The network
4+
inputs are the cart position, pole angle, and the derivatives of cart position and pole angle. The network output is
5+
used to apply a constant-magnitude force to the cart in either the left or right direction.
6+
7+
Note that there is a significant amount of duplication between these scripts, and this is intentional. The goal is to
8+
make it easier to see what the example is doing, without making the user dig through a bunch of code that is not
9+
directly related to the NEAT library usage.
10+
11+
## Running the examples ##
12+
13+
* Run `evolve-feedforward.py`. When it completes, it will have created the following output:
14+
- `avg_fitness.svg` Plot of mean/max fitness vs. generation.
15+
- `speciation.svg` Speciation plot vs. generation.
16+
- `winner-feedforward` A pickled version of the most fit genome.
17+
- `winner-feedforward.gv` Graphviz layout of the full winning network.
18+
- `winner-feedforward.gv.svg` Rendered image of the full winning network.
19+
- `winner-feedforward-enabled.gv` Graphviz layout of the winning network, with disabled connections removed.
20+
- `winner-feedforward-enabled.gv.svg` Rendered image of the winning network, with disabled connections removed.
21+
- `winner-feedforward-enabled-pruned.gv` Graphviz layout of the winning network, with disabled and non-functional connections removed.
22+
- `winner-feedforward-enabled-pruned.gv.svg` Rendered image of the winning network, with disabled and non-functional connections removed.
23+
24+
* Run `feedforward-test.py`. It will load the most fit genome from `winner-feedforward` and run it in a new simulation to test its
25+
performance. It will also run a second simulation, and produce a movie `feedforward-movie.mp4` showing the behavior. (See a sample
26+
movie [here](http://gfycat.com/CavernousCheeryIbadanmalimbe).)
27+
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
'''
2+
General settings and implementation of the single-pole cart system dynamics.
3+
'''
4+
5+
from math import cos, pi, sin
6+
import random
7+
8+
9+
class CartPole(object):
10+
gravity = 9.8 # acceleration due to gravity, positive is downward, m/sec^2
11+
mcart = 1.0 # cart mass in kg
12+
mpole = 0.1 # pole mass in kg
13+
lpole = 0.5 # half the pole length in meters
14+
time_step = 0.01 # time step in seconds
15+
16+
def __init__(self, x=None, theta=None, dx=None, dtheta=None,
17+
position_limit=2.4, angle_limit_radians=45 * pi / 180):
18+
self.position_limit = position_limit
19+
self.angle_limit_radians = angle_limit_radians
20+
21+
if x is None:
22+
x = random.uniform(-0.5 * self.position_limit, 0.5 * self.position_limit)
23+
24+
if theta is None:
25+
theta = random.uniform(-0.5 * self.angle_limit_radians, 0.5 * self.angle_limit_radians)
26+
27+
if dx is None:
28+
dx = random.uniform(-1.0, 1.0)
29+
30+
if dtheta is None:
31+
dtheta = random.uniform(-1.0, 1.0)
32+
33+
self.t = 0.0
34+
self.x = x
35+
self.theta = theta
36+
37+
self.dx = dx
38+
self.dtheta = dtheta
39+
40+
self.xacc = 0.0
41+
self.tacc = 0.0
42+
43+
def step(self, force):
44+
'''
45+
Update the system state using leapfrog integration.
46+
x_{i+1} = x_i + v_i * dt + 0.5 * a_i * dt^2
47+
v_{i+1} = v_i + 0.5 * (a_i + a_{i+1}) * dt
48+
'''
49+
# Locals for readability.
50+
g = self.gravity
51+
mp = self.mpole
52+
mc = self.mcart
53+
mt = mp + mc
54+
L = self.lpole
55+
dt = self.time_step
56+
57+
# Remember acceleration from previous step.
58+
tacc0 = self.tacc
59+
xacc0 = self.xacc
60+
61+
# Update position/angle.
62+
self.x += dt * self.dx + 0.5 * xacc0 * dt ** 2
63+
self.theta += dt * self.dtheta + 0.5 * tacc0 * dt ** 2
64+
65+
# Compute new accelerations as given in "Correct equations for the dynamics of the cart-pole system"
66+
# by Razvan V. Florian.
67+
st = sin(self.theta)
68+
ct = cos(self.theta)
69+
tacc1 = (g * st + ct * (-force - mp * L * self.dtheta ** 2 * st) / mt) / (L * (4.0 / 3 - mp * ct ** 2 / mt))
70+
xacc1 = (force + mp * L * (self.dtheta ** 2 * st - tacc1 * ct)) / mt
71+
72+
# Update velocities.
73+
self.dx += 0.5 * (xacc0 + xacc1) * dt
74+
self.dtheta += 0.5 * (tacc0 + tacc1) * dt
75+
76+
# Remember current acceleration for next step.
77+
self.tacc = tacc1
78+
self.xacc = xacc1
79+
self.t += dt
80+
81+
def get_scaled_state(self):
82+
'''Get full state, scaled into (approximately) [0, 1].'''
83+
return [0.5 * (self.x + self.position_limit) / self.position_limit,
84+
(self.dx + 0.75) / 1.5,
85+
0.5 * (self.theta + self.angle_limit_radians) / self.angle_limit_radians,
86+
(self.dtheta + 1.0) / 2.0]
87+
88+
89+
def continuous_actuator_force(action):
90+
return -10.0 + 2.0 * action[0]
91+
92+
93+
def noisy_continuous_actuator_force(action):
94+
a = action[0] + random.gauss(0, 0.2)
95+
return 10.0 if a > 0.5 else -10.0
96+
97+
98+
def discrete_actuator_force(action):
99+
return 10.0 if action[0] > 0.5 else -10.0
100+
101+
102+
def noisy_discrete_actuator_force(action):
103+
a = action[0] + random.gauss(0, 0.2)
104+
return 10.0 if a > 0.5 else -10.0
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env bash
2+
rm *.svg *.gv *.mp4 winner-feedforward
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# NEAT configuration for the bit-sequence memory experiment.
2+
3+
# The `NEAT` section specifies parameters particular to the NEAT algorithm
4+
# or the experiment itself. This is the only required section.
5+
[NEAT]
6+
pop_size = 250
7+
max_fitness_threshold = 60000
8+
reset_on_extinction = 0
9+
10+
[DefaultGenome]
11+
num_inputs = 4
12+
num_hidden = 1
13+
num_outputs = 1
14+
initial_connection = partial 0.5
15+
feed_forward = 0
16+
compatibility_threshold = 3.0
17+
compatibility_disjoint_coefficient = 1.0
18+
compatibility_weight_coefficient = 0.6
19+
conn_add_prob = 0.2
20+
conn_delete_prob = 0.2
21+
node_add_prob = 0.2
22+
node_delete_prob = 0.2
23+
activation_default = sigmoid
24+
activation_options = sigmoid
25+
activation_mutate_rate = 0.0
26+
aggregation_default = sum
27+
aggregation_options = sum
28+
aggregation_mutate_rate = 0.0
29+
bias_init_mean = 0.0
30+
bias_init_stdev = 1.0
31+
bias_replace_rate = 0.1
32+
bias_mutate_rate = 0.7
33+
bias_mutate_power = 0.5
34+
bias_max_value = 30.0
35+
bias_min_value = -30.0
36+
response_init_mean = 1.0
37+
response_init_stdev = 0.0
38+
response_replace_rate = 0.0
39+
response_mutate_rate = 0.0
40+
response_mutate_power = 0.0
41+
response_max_value = 30.0
42+
response_min_value = -30.0
43+
44+
weight_max_value = 30
45+
weight_min_value = -30
46+
weight_init_mean = 0.0
47+
weight_init_stdev = 1.0
48+
weight_mutate_rate = 0.8
49+
weight_replace_rate = 0.1
50+
weight_mutate_power = 0.5
51+
enabled_default = True
52+
enabled_mutate_rate = 0.01
53+
54+
[DefaultStagnation]
55+
species_fitness_func = max
56+
max_stagnation = 20
57+
58+
[DefaultReproduction]
59+
elitism = 2
60+
survival_threshold = 0.2
61+
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""
2+
Single-pole balancing experiment using a feed-forward neural network.
3+
"""
4+
5+
from __future__ import print_function
6+
7+
import os
8+
import pickle
9+
10+
import cart_pole
11+
12+
import neat
13+
import visualize
14+
15+
runs_per_net = 5
16+
num_steps = 60000 # equivalent to 1 minute of simulation time
17+
18+
19+
# Use the NN network phenotype and the discrete actuator force function.
20+
def eval_genomes(genomes, config):
21+
for genome_id, genome in genomes:
22+
net = neat.nn.FeedForwardNetwork.create(genome, config)
23+
24+
fitnesses = []
25+
26+
for runs in range(runs_per_net):
27+
sim = cart_pole.CartPole()
28+
29+
# Run the given simulation for up to num_steps time steps.
30+
fitness = 0.0
31+
for s in range(num_steps):
32+
inputs = sim.get_scaled_state()
33+
action = net.activate(inputs)
34+
35+
# Apply action to the simulated cart-pole
36+
force = cart_pole.discrete_actuator_force(action)
37+
sim.step(force)
38+
39+
# Stop if the network fails to keep the cart within the position or angle limits.
40+
# The per-run fitness is the number of time steps the network can balance the pole
41+
# without exceeding these limits.
42+
if abs(sim.x) >= sim.position_limit or abs(sim.theta) >= sim.angle_limit_radians:
43+
break
44+
45+
fitness += 1.0
46+
47+
fitnesses.append(fitness)
48+
49+
# The genome's fitness is its worst performance across all runs.
50+
genome.fitness = min(fitnesses)
51+
52+
53+
# Load the config file, which is assumed to live in
54+
# the same directory as this script.
55+
local_dir = os.path.dirname(__file__)
56+
config_path = os.path.join(local_dir, 'config-feedforward')
57+
config = neat.Config(neat.DefaultGenome, neat.DefaultReproduction, neat.DefaultStagnation, config_path)
58+
59+
pop = neat.Population(config)
60+
stats = neat.StatisticsReporter()
61+
pop.add_reporter(stats)
62+
pop.add_reporter(neat.StdOutReporter())
63+
winner = pop.run(eval_genomes, 2000)
64+
65+
# Save the winner.
66+
with open('winner-feedforward', 'wb') as f:
67+
pickle.dump(winner, f)
68+
69+
print(winner)
70+
71+
visualize.plot_stats(stats, ylog=True, view=True)
72+
visualize.plot_species(stats, view=True)
73+
74+
node_names = {-1: 'x', -2: 'dx', -3: 'theta', -4: 'dtheta', 0: 'control'}
75+
visualize.draw_net(config, winner, True, node_names=node_names)
76+
77+
visualize.draw_net(config, winner, view=True, node_names=node_names, filename="winner-feedforward.gv")
78+
visualize.draw_net(config, winner, view=True, node_names=node_names, filename="winner-feedforward-enabled.gv", show_disabled=False)
79+
visualize.draw_net(config, winner, view=True, node_names=node_names, filename="winner-feedforward-enabled-pruned.gv", show_disabled=False, prune_unused=True)
80+
81+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from cart_pole import CartPole, run_simulation
2+
3+
runs_per_net = 5
4+
5+
6+
def evaluate_population(genomes, create_func, force_func):
7+
for g in genomes:
8+
net = create_func(g)
9+
10+
fitness = 0
11+
12+
for runs in range(runs_per_net):
13+
sim = CartPole()
14+
fitness += run_simulation(sim, net, force_func)
15+
16+
# The genome's fitness is its average performance across all runs.
17+
g.fitness = fitness / float(runs_per_net)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import gizeh as gz
2+
import moviepy.editor as mpy
3+
from cart_pole import CartPole
4+
5+
6+
def make_movie(net, force_function, duration_seconds, output_filename):
7+
w, h = 300, 100
8+
scale = 300 / 6
9+
10+
cart = gz.rectangle(scale * 0.5, scale * 0.25, xy=(150, 80), stroke_width=1, fill=(0, 1, 0))
11+
pole = gz.rectangle(scale * 0.1, scale * 1.0, xy=(150, 55), stroke_width=1, fill=(1, 1, 0))
12+
13+
sim = CartPole()
14+
15+
def make_frame(t):
16+
inputs = sim.get_scaled_state()
17+
action = net.activate(inputs)
18+
sim.step(force_function(action))
19+
20+
surface = gz.Surface(w, h, bg_color=(1, 1, 1))
21+
22+
# Convert position to display units
23+
visX = scale * sim.x
24+
25+
# Draw cart.
26+
group = gz.Group((cart,)).translate((visX, 0))
27+
group.draw(surface)
28+
29+
# Draw pole.
30+
group = gz.Group((pole,)).translate((visX, 0)).rotate(sim.theta, center=(150 + visX, 80))
31+
group.draw(surface)
32+
33+
return surface.get_npimage()
34+
35+
clip = mpy.VideoClip(make_frame, duration=duration_seconds)
36+
clip.write_videofile(output_filename, codec="mpeg4", fps=50)

0 commit comments

Comments
 (0)