#!/usr/bin/perl -w package neat; # NEAT - Neuro-Evolution of Augmenting Topologies use strict; use warnings; use Data::Dumper; my $DEBUG = 0; BEGIN { use Exporter (); our ( $VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS ); $VERSION = 1.0; @ISA = qw( Exporter ); @EXPORT = qw( Copy SaveData LoadData ); } # interface methods sub new { my $self = {}; $self->{CLASSNAME} = shift; bless $self; $self->Init( @_ ); return $self; } sub DESTROY { my $self = shift; # nothing important to be done } sub Init { my $self = shift; my $file = shift; my $fast = shift; die "MUST specify an initial experiment configuration file!" unless defined $file; # load the experiment file my $data = LoadData( $file ); foreach my $key ( keys %$data ) { $self->{$key} = $data->{$key}; } if ( ! defined $fast ) { # re-init topology activation functions foreach my $t ( values %{$self->{'topologies'}} ) { delete $t->{'code'}; delete $t->{'func'}; } $self->InitTopologies(); } $DEBUG = $self->{'settings'}->{'debug'}; } sub InitTopologies { my $self = shift; my $net; foreach $net ( values %{$self->{'population'}} ) { my $t = NetworkTopology( $net ); $self->AddTopology( $t, $net ); } } sub EvaluationFunction { my ( $self, $func ) = @_; $self->{'settings'}->{'eval-func'} = $func; } sub EvaluateAll { my $self = shift; my @networks = values %{$self->{'population'}}; my $func = $self->{'settings'}->{'eval-func'}; foreach my $net ( @networks ) { $net->{'fitness'} = &$func( $net ); } } sub Save { my ( $self, $file ) = @_; if ( ! defined $file ) { $file = $self->{'settings'}->{'experiment'} . "." . $self->{'save'}->{'generation'} . ".pl"; } SaveData( $self, $file ); } sub Champions { my $self = shift; my @champions; my ($top,$net,$fitness); # initialize the toplogy records foreach $top ( values %{$self->{'topologies'}} ) { $top->{'members'} = []; $top->{'min'} = 1e+99; $top->{'max'} = -1e+99; $top->{'total'} = 0; $top->{'count'} = 0; $top->{'last'} = $top->{'avg'}; $top->{'avg'} = 0; $top->{'offspring'} = 0; } my $min = 1e+99; my $max = -1e+99; my $total = 0; my $count = 0; # update topologies with total fitness values foreach $net ( values %{$self->{'population'}} ) { my $f = $net->{'fitness'}; # update population min/max/total/count $min = $f if $f < $min; $max = $f if $f > $max; $total += $f; $count++; # update topology min/max/total/count $top = $self->{'topologies'}->{ $net->{'topology'} }; $top->{'min'} = $f if $f < $top->{'min'}; $top->{'max'} = $f if $f > $top->{'max'}; $top->{'total'} += $f; $top->{'count'}++; push @{$top->{'members'}}, $net->{'id'}; # if this is better than the historical max, save it if ( $f > $self->{'save'}->{'max-fitness'} ) { $self->{'save'}->{'max-fitness'} = $f; $self->{'save'}->{'max-fitness-generation'} = $self->{'save'}->{'generation'}; $self->{'save'}->{'champion'} = Copy( $net ); } } # update global statistics $self->{'save'}->{'gen-min'} = $min; $self->{'save'}->{'gen-max'} = $max; $self->{'save'}->{'gen-avg'} = $total / $count; # calculate average fitness for each topology foreach $top ( values %{$self->{'topologies'}} ) { if ( $top->{'count'} > 0 ) { $top->{'avg'} = $top->{'total'} / $top->{'count'}; } else { $top->{'avg'} = 0; } } # calculate an adjusted average after throwing out all below-average values foreach $top ( values %{$self->{'topologies'}} ) { $top->{'adj-total'} = $top->{'total'}; $top->{'adj-count'} = $top->{'count'}; } foreach $net ( values %{$self->{'population'}} ) { my $f = $net->{'fitness'}; my $top = $self->{'topologies'}->{ $net->{'topology'} }; if ( $f < $top->{'avg'} ) { $top->{'adj-total'} -= $f; $top->{'adj-count'}--; } } foreach $top ( values %{$self->{'topologies'}} ) { if ( $top->{'adj-count'} > 0 ) { $top->{'adj-avg'} = $top->{'adj-total'} / $top->{'adj-count'}; } else { $top->{'adj-avg'} = 0; } } # sort members by fitness # adjust penalty # save best to champions list foreach $top ( values %{$self->{'topologies'}} ) { # sort members by fitness, descending @{$top->{'members'}} = sort { $self->{'population'}->{$b}->{'fitness'} <=> $self->{'population'}->{$a}->{'fitness'}; } @{$top->{'members'}}; # update 'best' fitness if ( defined( $top->{'members'}->[0] ) ) { # lookup the fitness of the topology leader $fitness = $self->{'population'}->{ $top->{'members'}->[0] }->{'fitness'}; # add the fitness to the history record (limit 100) $top->{'history'} = [] if ( ! exists $top->{'history'} ); unshift @{$top->{'history'}}, $fitness; pop @{$top->{'history'}} if @{$top->{'history'}} > 100; =huh # calculate a weighted average my $total = 0; my $weight = 100; my $count = 0; foreach my $f ( @{$top->{'history'}} ) { $total += $f * $weight; $count += $weight; $weight--; } my $avg = $total / $count; =cut # require at least X generations before we start looking # at the penalty, to give it a chance to establish itself if ( @{$top->{'history'}} < 5 ) { $top->{'penalty'} = 0; $top->{'best'} = 0; } else { # adjust penalty based on improvement if ( $fitness > $top->{'best'} ) { $top->{'penalty'} *= 0.9; # 90% of what it was $top->{'best'} = $fitness; } else { $top->{'penalty'}++; } } # save this member as a champion push @champions, $top->{'members'}->[0]; } # save the second-place member, too, if there is one push @champions, $top->{'members'}->[1] if ( defined( $top->{'members'}->[1] ) ); } @champions = sort @champions; $self->{'champions'} = \@champions; return @champions; } sub Epoch { my $self = shift; my @champs = Champions( $self ); my $t; # topology # save the experiment data, if requested $self->Save() if ( $self->{'settings'}->{'save-epochs'} ); # calculate minimum/maximum weights and active topology search count my %active; my $min = $self->{'settings'}->{'max-weight'}; my $max = - $min; foreach my $net ( values %{$self->{'population'}} ) { $active{$net->{'topology'}}++; next if $net->{'fitness'} < $self->{'topologies'}->{ $net->{'topology'} }->{'avg'}; foreach my $node ( values %{$net->{'nodes'}} ) { foreach my $c ( values %$node ) { $min = $c if $c < $min; $max = $c if $c > $max; } } } $self->{'save'}->{'min-weight'} = $min; $self->{'save'}->{'max-weight'} = $max; $self->{'save'}->{'generation'}++; $self->AllocateOffspring(); my $remaining; $self->{'offspring'} = {}; # keep the new offspring here # create the new generation. foreach $t ( values %{$self->{'topologies'}} ) { $remaining = int( $self->{'settings'}->{'population'} - scalar( keys %{$self->{'offspring'}} ) ); $remaining = 0 if $remaining < 0; # how many offspring are allowed # special case for offspring less than 1 but non-zero # or where the population limit is exceeded my $limit = $self->{'settings'}->{'limit'}; my $o = abs($t->{'offspring'}); $o = 3 if ( $o > 0 && $o < 1 ); $o = int ( $o ); $o = $limit if ( $o > $limit ); $o = $remaining if ( $o > $remaining ); $o = 3 if ( $o < 1 && $t->{'offspring'} > 0 ); # first place champion next unless defined $t->{'members'}->[0]; my $one = $self->{'population'}->{ $t->{'members'}->[0] }; $t->{'champion'} = Copy( $one ); # pass the best one on to the next generation next unless $o--; $self->Elite( $one ); # focus the first few offspring on potentially profitable # changes so that if it is our last chance, we have a # CHANCE of making an improvement during this generation next unless $o--; $self->WeightAdjust( $one ); # only change ONE weight next unless $o--; $self->WeightsAdjust( $one ); # change all weights next unless $o--; $self->WeightsRandom( $one ); # choose all new weights # make a list of all members within the topology my @members; foreach my $m ( @{$t->{'members'}} ) { my $n = $self->{'population'}->{$m}; # next if $n->{'fitness'} < # $self->{'topologies'}->{ $n->{'topology'} }->{'avg'}; push @members, $m; } # focus the next group of offspring around 2nd place if ( @members > 1 ) { my $two = $self->{'population'}->{ $members[1] }; next unless $o--; $self->Elite( $two ); next unless $o--; $self->WeightAdjust( $two ); next unless $o--; $self->WeightsAdjust( $two ); next unless $o--; $self->CrossoverAverage( $one, $two ); # only change ONE weight next unless $o--; $self->CrossoverRandom( $one, $two ); # only change ONE weight next unless $o--; $self->CrossoverRandom( $one, $two ); # only change ONE weight } # use this function for a weighted selection of # who to act on sub Choose { my $self = shift; return $self->{'population'}->{ $_[int( rand() * @_ )] }; } # for the rest of the offspring, do something at random while ( $o-- >= 1 ) { # 50% chance of just modifying an existing if ( rand() < 0.5 ) { my $net = &Choose( $self, @members ); my $r = rand(); if ( $r < 0.50 ) { $self->WeightsAdjust( $net ); } # elsif ( $r < 0.80 ) { $self->WeightAdjust( $net ); } else { $self->WeightsRandom( $net ); } } else { # crossover two my $one = &Choose( $self, @members ); my $two = &Choose( $self, @members ); if ( $one == $two ) { # crossover of an individual with itself is useless $o++; next; } if ( rand() < 0.5 ) { $self->CrossoverRandom( $one, $two ); } else { $self->CrossoverAverage( $one, $two ); } } } } # if we have room left in the population # create a topological mutation $remaining = ( $self->{'settings'}->{'population'} - scalar( keys %{$self->{'offspring'}} ) ); my $active = scalar keys %active; if ( $remaining && $active < $self->{'settings'}->{'topology-limit'} ) { my $net; if ( rand() < 0.75 ) { # mutate the current champion network $net = $self->{'save'}->{'champion'}; } else { # mutate some poor unsuspecting topology champion my ( $which ) = RandomOrder( keys %{$self->{'topologies'}} ); $net = $self->{'topologies'}->{$which}->{'champion'}; } my $r = rand(); if ( $r < 0.05 ) { $self->ConnectionRemove( $net ); } elsif ( $r < 0.25 ) { $self->ConnectionSplit( $net ); } else { $self->ConnectionAdd( $net ); } } # replace the current population with the offspring $self->{'population'} = $self->{'offspring'}; delete $self->{'offspring'}; } sub Activate { my ( $self, $net, $values ) = @_; if ( ! exists( $net->{'topology'} ) ) { $self->AddTopology( NetworkTopology( $net ), $net ); } my $func = $self->{'topologies'}->{ $net->{'topology'} }->{'func'}; &$func( $net, $values ); } sub Stats { my $self = shift; my %seen; my $active = 0; my $best = 0; foreach my $net ( values %{$self->{'population'}} ) { $best = $net->{'fitness'} if ( $net->{'fitness'} > $best ); next if exists $seen{$net->{'topology'}}; $seen{$net->{'topology'}} = 1; $active++; } return sprintf( "%s: g%04d p%04d t%03d/%04d f%10.10f m%10.10f n%10.10f", $self->{'settings'}->{'experiment'}, $self->{'save'}->{'generation'}, scalar( keys %{$self->{'population'}} ), $active, scalar( keys %{$self->{'topologies'}} ), $self->{'save'}->{'max-fitness'}, $self->{'save'}->{'max-weight'}, $self->{'save'}->{'min-weight'} ); } sub ChampionNetwork { my $self = shift; return $self->{'save'}->{'champion'}; } # private methods sub UniqueID { my $self = shift; return ++($self->{'save'}->{'unique-id'}); } sub AddTopology { my ( $self, $topology, $net ) = @_; # lookup id my ( $id, $n ); if ( exists $self->{'topology-lookup'}->{$topology} ) { $id = $self->{'topology-lookup'}->{$topology}; } else { $id = $self->UniqueID(); $self->{'topology-lookup'}->{$topology} = $id; } # init topology record $net->{'topology'} = $id; if ( ! exists $self->{'topologies'}->{$id} ) { $self->{'topologies'}->{$id} = { 'members' => [], 'count' => 0, 'total' => 0, 'last' => 0, 'avg' => 0, 'best' => 0, 'penalty' => 0, 'offspring' => 0, }; } else { # new blood, reduce any existing penalty my $t = $self->{'topologies'}->{$id}; $t->{'penalty'} /= 2; # if was dead, resurrect the elite member if ( $t->{'offspring'} == 0 ) { $t->{'offspring'} = 2; # the one that was just 'created' and # the old topology champion/elite my $new = Copy( $t->{'champion'} ); $new->{'elite'} = 1; $self->{'offspring'}->{ $new->{'id'} } = $new; } } # lookup and save the activation order my @order; ActivationOrder( $net, undef, \@order, {} ); $self->{'topologies'}->{$id}->{'order'} = \@order; # identify any non-contributing nodes my %useless = $self->UselessNodes( $net ); $self->{'topologies'}->{$id}->{'ignore'} = \%useless; # remove non-contributing nodes from the activation order $self->{'topologies'}->{$id}->{'order'} = []; foreach $n ( @order ) { push @{$self->{'topologies'}->{$id}->{'order'}}, $n if ( ! exists( $self->{'topologies'}->{$id}->{'ignore'}->{$n} ) ); } # if using perl activation, add activation function if ( $self->{'settings'}->{'activation'} eq 'perl' && ! exists $self->{'topologies'}->{$id}->{'code'} ) { my $code = $self->ActivationFunction_Perl( $net ); $self->{'topologies'}->{$id}->{'code'} = $code; eval "\$self->{'topologies'}->{'$id'}->{'func'} = $code;"; } } sub UselessNodes { my ( $self, $net ) = @_; my ( %input, %output, %useless ); my @order; my ( $n, $c ); @order = @{$self->{'topologies'}->{ $net->{'topology'} }->{'order'}}; my @list; # start by finding all output nodes; foreach $n ( @order ) { next unless $self->{'nodes'}->{$n}->{'type'} eq 'output'; push @list, $n; $output{$n} = 1; } # now find all nodes connected to an output node while ( @list ) { $n = shift @list; my @connections = keys %{$net->{'nodes'}->{$n}}; while ( @connections ) { $c = shift @connections; next if exists $output{$c}; $output{$c} = 1; push @list, $c; } } # now work the other way...find all input nodes @list = (); foreach $n ( @order ) { next unless $self->{'nodes'}->{$n}->{'type'} eq 'input'; push @list, $n; $input{$n} = 1; } # now find all nodes connected to an input node my $count = 1; while ( $count ) { $count = 0; foreach $n ( @order ) { next if exists $input{$n}; foreach $c ( keys %{$net->{'nodes'}->{$n}} ) { next unless exists $input{$c}; $input{$n} = 1; $count++; last; } } } # all nodes that do not appear in both lists are useless foreach $n ( @order ) { if ( ! ( exists $input{$n} && exists $output{$n} ) ) { $useless{$n} = 1; } } return %useless; } sub AllocateOffspring { my $self = shift; my $top; my ( $total, $temp, $count ); my $penalty; # NOTE: Assumes that $self->Champions() called already # calculate a total population fitness adjusted for penalties $total = 0; foreach $top ( values %{$self->{'topologies'}} ) { # penalty is 0..100 # avg fitness is reduced 1% for each penalty point $temp = $top->{'adj-avg'}; $penalty = $temp * ( $top->{'penalty'} / $self->{'settings'}->{'penalty'} )**10; $penalty = $temp if $penalty > $temp; $temp -= $penalty; $top->{'adjusted'} = $temp; $total += $temp; } # each is allocated offspring based on contribution to total $count = $self->{'settings'}->{'population'}; if ( $total < 1 ) { my $offspring = $count / scalar keys %{$self->{'topologies'}}; foreach $top ( values %{$self->{'topologies'}} ) { $top->{'offspring'} = $offspring; } } else { foreach $top ( values %{$self->{'topologies'}} ) { # if ( $top->{'total'} < 1 ) { # # keep it alive long enough to optimize # $top->{'offspring'} = 3; # } else { $top->{'offspring'} = $count * ( $top->{'adjusted'} / $total ); # } } } } sub AvailableConnections { my ( $self, $net ) = @_; my $node; my $connection; my @nodes; my @list; # make a list of all possible NEW connections @nodes = keys %{$net->{'nodes'}}; foreach $node ( @nodes ) { # omit input nodes, they can't have any dependencies next if $self->{'nodes'}->{$node}->{'type'} eq 'input'; foreach $connection ( @nodes ) { next if exists( $net->{'nodes'}->{$node}->{$connection} ); push @list, "${node}:${connection}"; } } return RandomOrder( @list ); } sub ActivationFunction_Perl { my ( $self, $net ) = @_; my @nodes; @nodes = @{$self->{'topologies'}->{ $net->{'topology'} }->{'order'}}; my $code = 'sub { my ( $n, $v ) = @_; '; # . 'foreach my $node ( keys %{$n->{\'nodes\'}} ) ' # . '{ $v->{$node} = 0 unless exists $v->{$node}; } '; foreach my $node ( @nodes ) { my @params; foreach my $dep ( sort keys %{$net->{'nodes'}->{$node}} ) { # make sure the node we depend on has dependancies, # or is an input node if ( $self->{'nodes'}->{$dep}->{'type'} eq 'input' || scalar( keys %{$net->{'nodes'}->{$dep}} ) ) { push @params, "(\$v->{'$dep'}*\$n->{'nodes'}->{'$node'}->{'$dep'})"; } } if ( @params ) { $code .= "\$v->{'$node'} = " . $self->{'nodes'}->{$node}->{'function'} . '( ' . join( '+', @params ) . ", $self->{'nodes'}->{$node}->{'gain'} ); "; } } $code .= '}'; return $code; } sub ActivationFunction_C_PartialEval { # generates a C function that activates a specific network # with specific weights and some inputs, pre-calculating as much # of the calculations as humanly possible # # as of right now, assumes there is only one output my ( $self, $net, $values ) = @_; my $code = ''; my ( @nodes, @conn ); my ( $n, $c ); my $value = Copy( $values ); # don't want to modify the caller's data my $accum = {}; # get the activation order @nodes = @{$self->{'topologies'}->{ $net->{'topology'} }->{'order'}}; # initialize the node accumulators foreach $n ( @nodes ) { $accum->{$n} = 0; } # make a copy of the network so we can prune as we pre-calculate $net = Copy( $net ); # pre-calculate the values of any nodes we can my $changes = 1; while ( $changes ) { $changes = 0; foreach $n ( @nodes ) { next if exists $value->{$n}; next if $self->{'nodes'}->{$n}->{'type'} eq 'input'; @conn = keys %{$net->{'nodes'}->{$n}}; foreach $c ( @conn ) { next unless exists $value->{$c}; $accum->{$n} += $value->{$c} * $net->{'nodes'}->{$n}->{$c}; delete $net->{'nodes'}->{$n}->{$c}; } # if the node is now complete, calculate its value if ( ! scalar( keys %{$net->{'nodes'}->{$n}} ) ) { $value->{$n} = eval( $self->{'nodes'}->{$n}->{'function'} . '( ' . $accum->{$n} . ', ' . $self->{'nodes'}->{$n}->{'gain'} . ');' ); $changes++; } } } # make a list of inputs whose value is not passed in (variable) # don't need this right now, because the experiment defines the function prototype =disabled my @inputs; foreach $n ( @nodes ) { next unless ( $self->{'nodes'}->{$n}->{'type'} eq 'input' ); next if ( exists $values->{$n} ); push @inputs, $n; } # if nothing ever changes, just die, but chastise the user first. if ( ! @inputs ) { printf( "you attempted to form code that calculates a network " . "that has no variable inputs. What's the point?\n" ); printf( join ",", @inputs ); die; } =cut ##### Start of generated code ##### $code .= "{\n"; # declare the variables to hold the values of nodes my @vars; foreach $n ( @nodes ) { next if ( $value->{$n} ); next if ( $self->{'nodes'}->{$n}->{'type'} eq 'input' ); push @vars, $n; } $code .= 'double n_' . join( ",n_", @vars ) . ";\n"; # perform the summation and sigmoid for every node foreach $n ( @vars ) { my @values; foreach $c ( keys %{$net->{'nodes'}->{$n}} ) { my $temp = "( "; # this should NEVER happen, but just in case... if ( exists $value->{$c} ) { $temp .= sprintf( "%55.55f", $value->{$c} ); } else { $temp .= "n_$c"; } $temp .= sprintf( " * %55.55f )\n", $net->{'nodes'}->{$n}->{$c} ); push @values, $temp; } if ( @values ) { my $func = $self->{'nodes'}->{$n}->{'function'}; if ( $func eq 'sigmoid' ) { $code .= 'n_' . $n . ' = 1/(1+exp( -1 * ' . $self->{'nodes'}->{$n}->{'gain'} . " * (\n" . sprintf( "%55.55f\n+", $accum->{$n} ); $code .= join "+", @values; $code .= ")));\n"; } elsif ( $func eq 'sum' ) { $code .= 'n_' . $n . ' = ' . sprintf( "%55.55f\n+", $accum->{$n} ) . join( '+', @values ) . ";\n"; } else { die "What kind of activation function is $func?"; } } } # find and 'return' the output node value # start looking at the back, because that one SHOULD be it! my $found = 0; foreach $n ( reverse @vars ) { next unless $self->{'nodes'}->{$n}->{'type'} eq 'output'; $found = 1; $code .= "return n_$n;\n}\n"; } # if ( ! $found ) { # printf( "attempted to generate code for a network with no obvious output." . # " That was dumb... " ); # die; # } # $net->{'code'} = $code; return $code; } # offspring producing methods sub Elite { my ( $self, $net ) = @_; # network is copied, unchanged my $new = Copy( $net ); $new->{'elite'} = 1; $self->{'offspring'}->{ $new->{'id'} } = $new; } sub WeightAdjust { my ( $self, $net ) = @_; my $new = Copy( $net ); $new->{'id'} = $self->UniqueID(); $new->{'elite'} = 0; # adjust ONE weight by up to 10 percent in either direction my @existing = ExistingConnections( $new ); my $key = $existing[0]; return if ( ! defined( $key ) ); my ( $n, $c ) = split( ":", $key ); $new->{'nodes'}->{$n}->{$c} = $self->NewWeight( $new->{'nodes'}->{$n}->{$c} ); $self->{'offspring'}->{ $new->{'id'} } = $new; } sub WeightsAdjust { my ( $self, $net ) = @_; my $new = Copy( $net ); $new->{'id'} = $self->UniqueID(); $new->{'elite'} = 0; # adjust each weight by up to 10 percent in either direction my ( $n, $c ); foreach $n ( keys %{$new->{'nodes'}} ) { foreach $c ( keys %{$new->{'nodes'}->{$n}} ) { $new->{'nodes'}->{$n}->{$c} = $self->NewWeight( $new->{'nodes'}->{$n}->{$c} ); } } $self->{'offspring'}->{ $new->{'id'} } = $new; } sub WeightsRandom { my ( $self, $net ) = @_; my $new = Copy( $net ); $new->{'id'} = $self->UniqueID(); $new->{'elite'} = 0; # give every connection a new weight, chosen at random my ( $n, $c ); foreach $n ( keys %{$new->{'nodes'}} ) { foreach $c ( keys %{$new->{'nodes'}->{$n}} ) { $new->{'nodes'}->{$n}->{$c} = $self->NewWeight(); } } $self->{'offspring'}->{ $new->{'id'} } = $new; } sub CrossoverAverage { my ( $self, $one, $two ) = @_; my $new = Copy( $one ); $new->{'id'} = $self->UniqueID(); $new->{'elite'} = 0; # adjust weights to be the average of the two parents my ( $n, $c ); foreach $n ( keys %{$new->{'nodes'}} ) { foreach $c ( keys %{$new->{'nodes'}->{$n}} ) { if ( exists $two->{'nodes'}->{$n}->{$c} ) { $new->{'nodes'}->{$n}->{$c} = ( $one->{'nodes'}->{$n}->{$c} + $two->{'nodes'}->{$n}->{$c} ) / 2; } } } $self->{'offspring'}->{ $new->{'id'} } = $new; } sub CrossoverRandom { my ( $self, $one, $two ) = @_; my $new = Copy( $one ); $new->{'id'} = $self->UniqueID(); $new->{'elite'} = 0; # adjust each weight to be the same as one of the parents, # selected at random my ( $n, $c ); foreach $n ( keys %{$new->{'nodes'}} ) { foreach $c ( keys %{$new->{'nodes'}->{$n}} ) { if ( exists $two->{'nodes'}->{$n}->{$c} && rand() < 0.5 ) { $new->{'nodes'}->{$n}->{$c} = $two->{'nodes'}->{$n}->{$c}; } } } $self->{'offspring'}->{ $new->{'id'} } = $new; } sub ConnectionAdd { my ( $self, $net ) = @_; my $id = $self->UniqueID(); my $topology; my $new; my @available = $self->AvailableConnections( $net ); # randomized print STDERR "c "; do { $new = Copy( $net ); $new->{'id'} = $id; $new->{'elite'} = 0; my ( $n, $c ); my @nodes = keys %{$new->{'nodes'}}; # if recurrency not allowed, find first non-recurrent if ( ! $self->{'settings'}->{'allow-recurrent'} ) { my $found = 0; while ( @available && ! $found ) { ( $n, $c ) = split( ":", $available[0] ); if ( IsRecurrent( $new, $c, $n, 1 ) ) { shift @available; } else { $found = 1; } } } # if there is NOT one to be added, then split, instead. if ( ! defined( $available[0] ) ) { return $self->ConnectionSplit( $net ); } ( $n, $c ) = split ":", $available[0]; $new->{'nodes'}->{$n}->{$c} = ( rand() - 0.5 ) * 2; $topology = NetworkTopology( $new ); shift @available; } while ( exists $self->{'topology-lookup'}->{$topology} ); $self->AddTopology( $topology, $new ); $self->{'offspring'}->{ $new->{'id'} } = $new; print STDERR "+ "; } sub ConnectionRemove { my ( $self, $net ) = @_; my $id = $self->UniqueID(); my $new; my $topology; my ( $n, $c ); my @existing = ExistingConnections( $net ); # randomized print STDERR "x "; do { return if ( ! defined( $existing[0] ) ); $new = Copy( $net ); $new->{'id'} = $id; $new->{'elite'} = 0; ( $n, $c ) = split ":", $existing[0]; delete $new->{'nodes'}->{$n}->{$c}; $topology = NetworkTopology( $new ); shift @existing; } while ( exists $self->{'topology-lookup'}->{$topology} ); $self->AddTopology( $topology, $new ); $self->{'offspring'}->{ $new->{'id'} } = $new; print STDERR "- "; } sub ConnectionSplit { my ( $self, $net ) = @_; my $id = $self->UniqueID(); my $new; my $topology; my $key; my ( $n, $c ); my @existing = ExistingConnections( $net ); print STDERR "n "; do { $key = $existing[0]; shift @existing; return unless defined $key; $new = Copy( $net ); $new->{'id'} = $id; $new->{'elite'} = 0; if ( exists( $self->{'node-lookup'}->{$key} ) ) { # re-use old node ID $id = $self->{'node-lookup'}->{$key}; } else { # make a new node ID $id = $self->UniqueID(); $self->{'node-lookup'}->{$key} = $id; $self->{'nodes'}->{$id} = { 'id' => $id, 'type' => 'hidden', 'function' => 'sigmoid', 'gain' => $self->{'settings'}->{'gain'}, 'formed' => $key }; } # identify endpoints of connection to split ( $n, $c ) = split ":", $key; # make a connection between $n and the new node $new->{'nodes'}->{$n}->{$id} = $new->{'nodes'}->{$n}->{$c}; # between the new and the old connection end point $new->{'nodes'}->{$id} = { $c => 1 }; # delete the old connection delete $new->{'nodes'}->{$n}->{$c}; # if there isn't a connection between the old node and the BIAS node # make one if ( ! exists $new->{'nodes'}->{$n}->{'bias'} ) { $new->{'nodes'}->{$n}->{'bias'} = 0; } # if there isn't a connection between the new node and the bias, # create one if ( ! exists $new->{'nodes'}->{$id}->{'bias'} ) { $new->{'nodes'}->{$id}->{'bias'} = 0; } $topology = NetworkTopology( $new ); } while ( exists $self->{'topology-lookup'}->{$topology} ); $self->AddTopology( $topology, $new ); $self->{'offspring'}->{ $new->{'id'} } = $new; print STDERR "+ "; } # support functions sub NewWeight { my ( $self, $old ) = @_; my $new; if ( defined ( $old ) ) { # adjust up or down by up to 10 percent $new = $old + ( $old * ( ( rand() - 0.5 ) * 0.2 ) ); } else { my $min = $self->{'save'}->{'min-weight'}; my $range = $self->{'save'}->{'max-weight'} - $min; $new = ( rand() * $range ) + $min; } my $limit = $self->{'settings'}->{'max-weight'}; $new = $limit if ( $new > $limit ); $limit *= -1; $new = $limit if ( $new < $limit ); $self->{'save'}->{'max-weight'} = $new if ( $new > $self->{'save'}->{'max-weight'} ); $self->{'save'}->{'min-weight'} = $new if ( $new < $self->{'save'}->{'min-weight'} ); return $new; } sub LoadData { my ( $file ) = @_; my $def; my $data; local $/; # load entire file at once open FILE, "<$file" or die "failed to open file for input:\n $file\n Error: $!\n"; $def = ; close FILE; eval $def; # SHOULD set $data if saved via SaveData return $data; } sub SaveData { my ( $data, $file ) = @_; open SAVE, ">$file" or die "failed to open file for output:\n $file\n Error: $!\n"; $Data::Dumper::Indent = 1; print SAVE Data::Dumper->Dump( [$data], ["data"] ); close SAVE; } sub Copy { my $data = shift; my $copy; eval Data::Dumper->Dump( [$data], ["copy"] ); return $copy; } sub RandomOrder { my @list = @_; my ( $i, $j, $temp ); for ( $i = 0 ; $i <= $#list ; $i++ ) { $j = int( rand() * @list ); $temp = $list[$i]; $list[$i] = $list[$j]; $list[$j] = $temp; } return @list; } sub NetworkTopology { my $net = shift; my $node; my $cx; my @nodes; my @connections; # make a list of nodes @nodes = sort keys %{$net->{'nodes'}}; # make a list of connections foreach $node ( @nodes ) { foreach $cx ( keys %{$net->{'nodes'}->{$node}} ) { push @connections, "${node}:${cx}"; } } # sort the connection list @connections = sort @connections; # return the topology string return "N=" . (join ",", @nodes) . ";" . "C=" . ( join ",", @connections); } sub ActivationOrder { my ( $net, $node, $order, $seen ) = @_; if ( defined( $node ) ) { foreach my $next ( sort keys %{$net->{'nodes'}->{$node}} ) { if ( ! exists $seen->{$next} ) { $seen->{$next} = 1; ActivationOrder( $net, $next, $order, $seen ); } } push @{$order}, $node; } else { foreach $node ( sort keys %{$net->{'nodes'}} ) { next if $seen->{$node}; $seen->{$node} = 1; ActivationOrder( $net, $node, $order, $seen ); } } } sub ExistingConnections { my $net = shift; my $node; my $connection; my @list; # make a list of all existing connections foreach $node ( keys %{$net->{'nodes'}} ) { foreach $connection ( keys %{$net->{'nodes'}->{$node}} ) { push @list, "${node}:${connection}"; } } return RandomOrder( @list ); } sub IsRecurrent { # %seen should only include ancestors, not siblings, in the tree my ( $net, $start, %seen ) = @_; return 1 if ( $seen{$start} ); # handles directly recurrent $seen{$start} = 1; my @list = keys %{$net->{'nodes'}->{$start}}; my $n; foreach $n ( @list ) { return 1 if ( exists( $seen{$n} ) ); return 1 if ( IsRecurrent( $net, $n, %seen ) ); } return 0; } sub DebugOut { printf( @_ ) if ( $DEBUG ); } # activation functions sub sigmoid { my ( $x, $a ) = @_; return 1 / ( 1 + exp( - $a * $x ) ); } sub sum { my ( $x, $a ) = @_; return $x * $a; } 1;