AI-Genetic
view release on metacpan or search on metacpan
package AI::Genetic;
use strict;
use Carp;
use vars qw/$VERSION/;
$VERSION = 0.05;
use AI::Genetic::Defaults;
# new AI::Genetic. More modular.
# Not too many checks are done still.
##### Shared private vars
# this hash predefines some strategies
my %_strategy = (
rouletteSinglePoint => \&AI::Genetic::Defaults::rouletteSinglePoint,
rouletteTwoPoint => \&AI::Genetic::Defaults::rouletteTwoPoint,
rouletteUniform => \&AI::Genetic::Defaults::rouletteUniform,
tournamentSinglePoint => \&AI::Genetic::Defaults::tournamentSinglePoint,
tournamentTwoPoint => \&AI::Genetic::Defaults::tournamentTwoPoint,
tournamentUniform => \&AI::Genetic::Defaults::tournamentUniform,
randomSinglePoint => \&AI::Genetic::Defaults::randomSinglePoint,
randomTwoPoint => \&AI::Genetic::Defaults::randomTwoPoint,
randomUniform => \&AI::Genetic::Defaults::randomUniform,
);
# this hash maps the genome types to the
# classes they're defined in.
my %_genome2class = (
bitvector => 'AI::Genetic::IndBitVector',
rangevector => 'AI::Genetic::IndRangeVector',
listvector => 'AI::Genetic::IndListVector',
);
##################
# sub new():
# This is the constructor. It creates a new AI::Genetic
# object. Options are:
# -population: set the population size
# -crossover: set the crossover probability
# -mutation: set the mutation probability
# -fitness: set the fitness function
# -type: set the genome type. See docs.
# -terminate: set termination sub.
sub new {
my ($class, %args) = @_;
my $self = bless {
ADDSEL => {}, # user-defined selections
ADDCRS => {}, # user-defined crossovers
ADDMUT => {}, # user-defined mutations
ADDSTR => {}, # user-defined strategies
} => $class;
$self->{FITFUNC} = $args{-fitness} || sub { 1 };
$self->{CROSSRATE} = $args{-crossover} || 0.95;
$self->{MUTPROB} = $args{-mutation} || 0.05;
$self->{POPSIZE} = $args{-population} || 100;
$self->{TYPE} = $args{-type} || 'bitvector';
$self->{TERM} = $args{-terminate} || sub { 0 };
$self->{PEOPLE} = []; # list of individuals
$self->{GENERATION} = 0; # current gen.
$self->{INIT} = 0; # whether pop is initialized or not.
$self->{SORTED} = 0; # whether the population is sorted by score or not.
$self->{INDIVIDUAL} = ''; # name of individual class to use().
return $self;
}
# sub createStrategy():
# This method creates a new strategy.
# It takes two arguments: name of strategy, and
# anon sub that implements it.
sub createStrategy {
my ($self, $name, $sub) = @_;
if (ref($sub) eq 'CODE') {
$self->{ADDSTR}{$name} = $sub;
} else {
# we don't know what this operation is.
carp <<EOC;
ERROR: Must specify anonymous subroutine for strategy.
Strategy '$name' will be deleted.
EOC
;
delete $self->{ADDSTR}{$name};
return undef;
}
return $name;
}
# sub evolve():
# This method evolves the population using a specific strategy
# for a specific number of generations.
sub evolve {
my ($self, $strategy, $gens) = @_;
unless ($self->{INIT}) {
carp "can't evolve() before init()";
return undef;
}
my $strSub;
if (exists $self->{ADDSTR}{$strategy}) {
# ...
return $fitness;
}
sub terminateFunc {
my $ga = shift;
# terminate if reached some threshold.
return 1 if $ga->getFittest->score > $THRESHOLD;
return 0;
}
=head1 DESCRIPTION
This module implements a Genetic Algorithm (GA) in pure Perl.
Other Perl modules that achieve the same thing (perhaps better,
perhaps worse) do exist. Please check CPAN. I mainly wrote this
module to satisfy my own needs, and to learn something about GAs
along the way.
B<PLEASE NOTE:> As of v0.02, AI::Genetic has been re-written from
scratch to be more modular and expandable. To achieve this, I had
to modify the API, so it is not backward-compatible with v0.01.
As a result, I do not plan on supporting v0.01.
I will not go into the details of GAs here, but here are the
bare basics. Plenty of information can be found on the web.
In a GA, a population of individuals compete for survival. Each
individual is designated by a set of genes that define its
behaviour. Individuals that perform better (as defined by the
fitness function) have a higher chance of mating with other
individuals. When two individuals mate, they swap some of
their genes, resulting in an individual that has properties
from both of its "parents". Every now and then, a mutation
occurs where some gene randomly changes value, resulting in
a different individual. If all is well defined, after a few
generations, the population should converge on a "good-enough"
solution to the problem being tackled.
A GA implementation runs for a discrete number of time steps
called I<generations>. What happens during each generation can
vary greatly depending on the strategy being used (See
L</"STRATEGIES"> for more info).
Typically, a variation of the following happens at
each generation:
=over 4
=item B<1. Selection>
Here the performance of all the individuals is evaluated
based on the fitness function, and each is given a specific
fitness value. The higher the value, the bigger the chance
of an individual passing its genes on in future generations
through mating (crossover).
=item B<2. Crossover>
Here, individuals selected are randomly paired up for
crossover (aka I<sexual reproduction>). This is further
controlled by the crossover rate specified and may result in
a new offspring individual that contains genes common to
both parents. New individuals are injected into the current
population.
=item B<3. Mutation>
In this step, each individual is given the chance to mutate
based on the mutation probability specified. If an individual
is to mutate, each of its genes is given the chance to randomly
switch its value to some other state.
=back
=head1 CLASS METHODS
Here are the public methods.
=over 4
=item I<$ga>-E<gt>B<new>(I<options>)
This is the constructor. It accepts options in the form of
hash-value pairs. These are:
=over 8
=item B<-population>
This defines the size of the population, i.e. how many individuals
to simultaneously exist at each generation. Defaults to 100.
=item B<-crossover>
This defines the crossover rate. Defaults to 0.95.
=item B<-mutation>
This defines the mutation rate. Defaults to 0.05.
=item I<-fitness>
This defines a fitness function. It expects a reference to a subroutine.
More details are given in L</"FITNESS FUNCTION">.
=item I<-type>
This defines the type of the genome. Currently, AI::Genetic
supports only three types:
=over
=item I<bitvector>
Individuals of this type have genes that are bits. Each gene
can be in one of two possible states, on or off.
=item I<listvector>
are cached for speed.
=item I<$ga>-E<gt>B<sortIndividuals>(?[I<ListOfIndividuals>]?)
Given an anonymous list of individuals, this method sorts them according
to fitness, returning an anonymous list of the sorted individuals.
=item I<$ga>-E<gt>B<people>()
Returns an anonymous list of individuals of the current population.
B<IMPORTANT>: the actual array reference used by the AI::Genetic object
is returned, so any changes to it will be reflected in I<$ga>.
=item I<$ga>-E<gt>B<size>(?I<newSize>?)
This method is used to query and set the population size.
=item I<$ga>-E<gt>B<crossProb>(?I<newProb>?)
This method is used to query and set the crossover rate.
=item I<$ga>-E<gt>B<mutProb>(?I<newProb>?)
This method is used to query and set the mutation rate.
=item I<$ga>-E<gt>B<indType>()
This method returns the type of individual: I<bitvector>, I<listvector>,
or I<rangevector>.
=item I<$ga>-E<gt>B<generation>()
This method returns the current generation.
=back
=head1 FITNESS FUNCTION
Very quickly you will realize that properly defining the fitness function
is the most important aspect of a GA. Most of the time that a genetic
algorithm takes to run is spent in running the fitness function for each
separate individual to get its fitness. AI::Genetic tries to minimize this
time by caching the fitness result for each individual. But, B<you should
spend a lot of time optimizing your fitness function to achieve decent run
times.>
The fitness function should expect only one argument, an anonymous list of
genes, corresponding to the individual being analyzed. It is expected
to return a number which defines the fitness score of the said individual.
The higher the score, the more fit the individual, the more the chance it
has to be chosen for crossover.
=head1 STRATEGIES
AI::Genetic comes with 9 predefined strategies. These are:
=over
=item rouletteSinglePoint
This strategy implements roulette-wheel selection and single-point crossover.
=item rouletteTwoPoint
This strategy implements roulette-wheel selection and two-point crossover.
=item rouletteUniform
This strategy implements roulette-wheel selection and uniform crossover.
=item tournamentSinglePoint
This strategy implements tournament selection and single-point crossover.
=item tournamentTwoPoint
This strategy implements tournament selection and two-point crossover.
=item tournamentUniform
This strategy implements tournament selection and uniform crossover.
=item randomSinglePoint
This strategy implements random selection and single-point crossover.
=item randomTwoPoint
This strategy implements random selection and two-point crossover.
=item randomUniform
This strategy implements random selection and uniform crossover.
=back
More detail on these strategies and how to call them in your own
custom strategies can be found in L<AI::Genetic::OpSelection>,
L<AI::Genetic::OpCrossover> and L<AI::Genetic::OpMutation>.
You can use the functions defined in the above modules in your
own custom-made strategy. Consult their manpages for more info.
A custom-made strategy can be defined using the I<strategy()>
method and is called at the beginning of each generation. The only
argument to it is the AI::Genetic object itself. Note that the
population at this point is sorted accoring to each individual's
fitness score. It is expected that the strategy sub will modify
the population stored in the AI::Genetic object. Here's the
pseudo-code of events:
for (1 .. num_generations) {
sort population;
call strategy_sub;
if (termination_sub exists) {
call termination_sub;
last if returned true value;
}
}
=head1 A NOTE ON SPEED/EFFICIENCY
Genetic algorithms are inherently slow.
Perl can be pretty fast, but will never reach the speed of optimized
C code (at least my Perl coding will not). I wrote AI::Genetic mainly
for my own learning experience, but still tried to optimize it as
much as I can while trying to keep it as flexible as possible.
To do that, I resorted to some well-known tricks like passing a
reference of a long list instead of the list itself (for example,
when calling the fitness function, a reference of the gene list
is passed), and caching fitness scores (if you try to evaluate
the fitness of the same individual more than once, then the fitness
function will not be called, and the cached result is returned).
To help speed up your run times, you should pay special attention
to the design of your fitness function since this will be called once
for each unique individual in each generation. If you can shave off a
few clock cycles here and there, then it will be greatly magnified in
the total run time.
=head1 BUGS
I have tested this module quite a bit, and even used it to solve a
work-related problem successfully. But, if you think you found a bug
then please let me know, and I promise to look at it.
Also, if you have any requests, comments or suggestions, then feel
free to email me.
=head1 INSTALLATION
Either the usual:
( run in 1.243 second using v1.01-cache-2.11-cpan-39bf76dae61 )