09. April 2013 · Comments Off on Creating Class Schedules using Genetic Algorithms · Categories: Programming · Tags: , ,

Making class schedule belong to class of NP hard problems. The problem can be solved using heuristic search algorithm to find optimal solution, but it works for simple cases. For more complex inputs and requirements finding considerably good solution can take a while or it may be impossible. This is where genetic algorithms come in the game.

ga_class_sch_app

When you make class schedule you must take into consideration many requirements (number of professors, students, classes and classrooms, size of classroom, laboratory equipment in classroom and many other). These requirements can be divided into several groups by their importance. Hard requirements (if you break one of these then the schedule is infeasible):

  • Class can be placed only in spear classroom.
  • No professor or student group can have more then on class at one time.
  • Classroom must have enough seats to accommodate all students.
  • To place class in classroom, classroom must have laboratory equipment (computers in our case) if class requires it.

Some soft requirements (can be broken but schedule is still feasible):

  • Preferred time of class by professors.
  • Preferred classroom by professors.
  • Distribution (in time or space) of classes for student groups or professors.

Hard and soft requirements of course depend on situation. In this example only hard requirements are implemented. Let start by explaining object which makes on class schedule.

Objects of Class Schedule

Professor

Professor class has ID and name of professor. It also contains list of classes that professor teaches.

Student Group

StudentsGroup class has ID and name of student group, as well as number of students (size of group). It also contains list of classes that group attends.

Classroom

Room class has ID and name of classroom, as well as number of seats and if there are computers in it. If classroom has computers, it is expected that there is a computer for each seat. IDs are generated internally and automatically.

Course

Course class has ID and name of course.

Class

CourseClass holds reference to course to which class belongs, reference to professor who teaches and list of student groups that attend class. It also stores how many seats are needed (sum of student groups’ sizes), if class requires computers in classroom and duration of class (in hours).

Chromosome

The first thing we should consider when we deal with genetic algorithm is how to represent our solution in such way that is feasible for genetic operations such as crossover and mutation and we should know how to specify how good our solution is (called fitness).

Representation

How we represent chromosome? Well we need a slot (time-space slot) for each hour, we assume that time is in one hour granules, for every room of every day. Also we assume that classes cannot begin before 9am and should finish before or at 10pm (12 hours total) and working days are Monday to Friday (5 days total). So we need vector with size 12*5*number_of_rooms. Slot should be of std::list type because during execution of our algorithm we allow multiple classes at same slot. There is additional hash map which is used to obtain slot at which class begins (its position in vector) from address of class’s object. Each hour of a class has separate entry in vector, but there is only one entry per class in hash map. For instance if class starts at 1pm and lasts for three hours, it has entries in 1pm, 2pm and 3pm slots.

Class Schedule Chromosome Representation
Figure 1 – Chromosome Representation

As we have vector of slots we can use Chromosome::Representation::GaMultiValueChromosome<T> template class to represent our chromosome, but we must modify it by adding previously explained hash map.

class Schedule : public GaMultiValueChromosome<list><CourseClass*>>
{

    friend class ScheduleCrossover;
    friend class ScheduleMutation;
    friend class ScheduleFitness;
    friend class ScheduleObserver;

private:

    hash_map<CourseClass*, int> _classes;

    hash_map<CourseClass*, int> _backupClasses;

public:

    Schedule(GaChromosomeDomainBlock<list><CourseClass*>>* configBlock);

    Schedule(const Schedule& c, bool setupOnly);

    virtual ~Schedule();

    virtual GaChromosomePtr GACALL MakeCopy(bool setupOnly) const;

    virtual GaChromosomePtr GACALL MakeNewFromPrototype() const;

    virtual void GACALL PreapareForMutation();

    virtual void GACALL AcceptMutation();

    virtual void GACALL RejectMutation();

};

Chromosome has three major parts: code, fitness value and setup. Chromosome’s code contains representation of solution, in our case it is hash map and vector of slots. Fitness value keeps information about how good that solution is. Setup of chromosomes stores references to genetic operations which are used to operate on chromosome (such as fitness operation, crossover operation or mutation operation). It also stores parameters which are used by these genetic operation as well as parameters of chromosomes. Chromosome’s setup is stored in Chromosome Configuration Block (CCB). When we make first chromosome we must give reference to CCB. The CCB is then used for new chromosomes produced by copy constructor or MakeCopy and MakeFromPrototype methods.

_classes attribute stores hash map and _backupClasses used to save copy of it if improving only mutations are enabled. The first constructor takes configuration block and initialize empty chromosome (no chromosome’s code) with provided setup. The second constructor is copy constructor with additional parameter setupOnly. This parameter specifies weather constructor should copy only setup of chromosomes or it should copy chromosome’s code as well (hash map and vector of slots). Destructor must be declared even if it does not do anything to avoid memory leakage. MakeCopy does what it says – it makes copy of the chromosomes and returns smart pointer to it. It copies CCB to new chromosome as well as chromosome’s code and fitness (if specified so). MakeNewFromPrototype method makes new chromosome with same CCB but it initializes new chromosome’s code with random values (size of new code is same as size of prototype code). PreapareForMutation method called before algorithm performs mutation operation on chromosome, it is used to save current chromosomes code. AcceptMutation method is called if algorithm has accepted mutation that had been made, it is used to clear saved code. RejectMutation method is called if algorithm has rejected mutation that had been made, it is used to restore chromosome’s code. When overriding these methods, overridden methods of base class should also be called from child class.

Fitness

Now we need to assign a fitness value to the chromosome. We will use minimal requirements for a class schedule (nothing fancy, for instance we assume that professor can have classes at any time). This is how we do it:

  • Each class can have from 0 to 5 points.
  • If class use empty room, we increment its score.
  • If class requires computers and it is located in room with them, we increment its score.
    But if class does not requires computers, well we increment its score anyway.
  • If class is located in room with enough available seats, guess what,
    we increment its score.
  • If professor is available (has no other classes) at the time,
    we increment class’s score once again.
  • And last criteria that we check is if student groups of the class has no other classes
    at same time, and if they do not we increment score of the class.
  • If class brakes some rule at any slot then the score is not incremented
  • Total score of schedule is sum of points of all classes.
  • Fitness value is calculated as schedule_score/maximum_score,
    and maximum_score is number_of_classes*5.

Fitness values are represented by single-precision floating point number (float). Fitness operation must inherit Chromosome::GaFitnessOperation.

class ScheduleFitness : public GaFitnessOperation
{

public:

    virtual float GACALL operator ()(const GaChromosome* chromosome) const;

    virtual GaParameters* GACALL MakeParameters() const;

    virtual bool GACALL CheckParameters(const GaParameters& parameters) const;
};

operator () calculates fitness value of chromosome and returns that value. MakeParameters and CheckParameters do not do anything they just override pure virtual functions from Common::GaOperation class.

Crossover

Crossover operation combines data in hash maps of two parent chromosomes, and then it creates vector of slots according to content of new hash map. Crossover ‘splits’ hash maps of both parents in parts of random size. Number of parts is defined by number of crossover points (plus one) in chromosome’s parameters. Then it alternately copies parts form parents to new chromosome, and forms new vector of slots.

Class Schedule Crossover Operation
Figure 2 – Crossover operation

GAL has some built-in crossover operations, but now we need specialized crossover, so we make our own. Crossover operation must inherit Chromosome::GaCrossoverOperation class.

class ScheduleCrossover : public GaCrossoverOperation
{

public:

    virtual GaChromosomePtr GACALL operator ()(const GaChromosome* parent1,
        const GaChromosome* parent2) const;

    virtual GaParameters* GACALL MakeParameters() const;

    virtual bool GACALL CheckParameters(const GaParameters& parameters) const;

};

operator () performs crossover operation, produces new chromosomes and returns smart pointer to it. MakeParameters and CheckParameters like in fitness operation do not do anything.

Mutation

Mutation operation is very simple. It just takes class randomly and moves it to another randomly chosen slot. Number of classes which are going to be moved in a single operation is defined by mutation size in chromosome’s parameters.

GAL also has built-in mutation operations, but now we need specialized crossover, so we make our own. Crossover operation must inherit Chromosome::GaMutationOperation class.

class ScheduleMutation : public GaMutationOperation
{

public:

    virtual void GACALL operator ()(GaChromosome* chromosome) const;

    virtual GaParameters* GACALL MakeParameters() const;

    virtual bool GACALL CheckParameters(const GaParameters& parameters) const;

};

operator () performs mutation operation. MakeParameters and CheckParameters like in crossover operation do not do anything.

Observing

GAL observer pattern to notify user about progress of the algorithm and changes of it state. Observer’s class must inherit Observing::GaObserver. All methods in this class are abstract and must be overridden:

  • StatisticUpdate – this method is called each time before algorithm progress
    to next generation
  • NewBestChromosome – this method is called each time when algorithm
    finds new best solution
  • EvolutionStateChanged – this method is called when user stops, starts,
    pauses or resumes execution of algorithm

If we do not have to handle all these events observer can inherit Observing::GaObserverAdapter class, and only override methods that handles desired events.

class ScheduleObserver : public GaObserverAdapter
{

private:

    HANDLE _event;

    void ReleaseEvent();

public:

    ScheduleObserver();

    virtual ~ScheduleObserver();

    void WaitEvent();
	
    virtual void GACALL NewBestChromosome(const GaChromosome& newChromosome,
        const GaAlgorithm& algorithm);

    virtual void GACALL EvolutionStateChanged(GaAlgorithmState newState,
        const GaAlgorithm& algorithm);

};

Putting all the pieces together

On top of all things what we need is genetic algorithm. We choose an incremental genetic algorithm (it replaces only few chromosomes in each generation). But to make an algorithm object we need stop criteria, population on which algorithm is going to operate and parameters for the algorithm. Desired algorithm use Algorithm::GaMultithreadingAlgorithmParams class as parameters because it only needs number of working threads. To specify stop criteria to the algorithm, there should be parameters for it. We use stop criteria based on fitness value (in our case, based on fitness value of best chromosome) Algorithm::StopCriterias::GaFitnessCriteria and its parameters Algorithm::StopCriterias::GaFitnessCriteriaParams.

Population is encapsulated in Population::GaPopulation class. To instantiate a population object we must provide prototype of chromosome, and configuration of population. Configuration is consists genetic operations which are performed over population (selection, coupling, scaling and replacement), comparator of chromosomes’ fitness values which is used to sort chromosome in population and sorted groups and parameters of population. Configuration of population is represented by GaPopulationConfiguration class. Because algorithm should maximize fitness values Chromosome::FitnessComparators::GaMaxFitnessComparator comparator should be used. Population::GaPopulationParameters class represents parameters of population. Incremental algorithm requires population with fixed size. Because random selection and replacement is used population do not have to be sorted. Also we want to track few best and few worst chromosomes. Last thing about population parameters is whether we’ll use scaled fitness values or non-scaled fitness values. Because we won’t use scaling operation population will use non-scaled fitness values. As said, we select chromosomes randomly produce few chromosomes and replace random chromosomes in each generation. As selection operation Population::SelectionOperations::GaSelectRandomBest is used with Population::SelectionOperations::GaSelectRandomBestParams class as parameters. Replacement operation is Population::ReplacementOperations::GaReplaceRandom with Population::ReplacementOperations::GaReplaceElitismParams class as parameters. Default coupling operation is used and scaling operation is not used.

Chromosome prototype is empty chromosome, has no code, with defined Chromosome Configuration Block (CCB) and defined size of code. Because our chromosome inherits Chromosome::Representation::GaMultiValueChromosome we must use Chromosome::Representation::GaChromosomeDomainBlock CCB even if we do not use value set. When constructing CCB we specify chromosomes parameters, which crossover, mutation and fitness operations are going to be used as well as fitness comparator. Chromosome parameters are represented by Chromosome::GaChromosomeParams class. This class also handles parameters for genetic operation which operates on chromosome. Parameters define how frequently crossover and mutation operation are going to be performed, size of mutation and number of crossover points. Also it tells if only the mutations that improve fitness value should be accepted.

And here is the code:

int main()
{
    // parse config file
    Configuration::GetInstance().ParseFile( "GaSchedule.cfg" );

    // initialize GAL internal structures
    GaInitialize();

    // make chromosome parameters
    // crossover probability: 80%
    // crossover points: 2
    // no "improving only mutations"
    // mutation probability: 3%
    // number of moved classes per mutation: 2
    GaChromosomeParams chromosomeParams( 0.8F, 2, false, 0.03F, 2 );

    // instances of genetic operations
    ScheduleCrossover crossoverOperation;
    ScheduleMutation mutationOperation;
    ScheduleFitness fitnessOperation;

    // make CCB with fallowing setup:
    // there are no value set with ScheduleCrossover, 
    // ScheduleMutation, ScheduleFitness genetic operations
    // set fittnes comparator for maximizing fitness value
    // use previously defined chromosome's parameters
    GaChromosomeDomainBlock<list<CourseClass*>> configBlock( NULL, 
        &crossoverOperation, &mutationOperation, &fitnessOperation,
        GaFitnessComparatorCatalogue::Instance().GetEntryData(
        "GaMaxFitnessComparator" ),
        &chromosomeParams );

    // make prototype of chromosome
    Schedule prototype( &configBlock );

    // make population parameters
    // number of chromosomes in population: 100
    // population always has fixed number of chromosomes
    // population is not sorted
    // non-transformed(non-scaled) fitness values are used
    // for sorting and tracking chromosomes
    // population tracks 5 best and 5 worst chromosomes
    GaPopulationParameters populationParams( 100, false, false, false, 5, 5 );
    
    // make population parameters
    // number of chromosomes in population: 100
    // population always has fixed number of chromosomes
    // population is not sorted
    // non-transformed(non-scaled) fitness values are used
    // for sorting and tracking chromosomes
    // population tracks 5 best and 5 worst chromosomes
    GaPopulationParameters populationParams( 100, false, false, false, 5, 5 );

    // make parameters for selection operation
    // selection will choose 16 chromosomes
    // but only 8 best of them will be stored in selection result set
    // there will be no duplicates of chromosomes in result set
    GaSelectRandomBestParams selParam( 8, false, 16 );

    // make parameters for replacement operation
    // replace 8 chromosomes
    // but keep 5 best chromosomes in population
    GaReplaceElitismParams repParam( 8, 5 );

    // make parameters for coupling operation
    // coupling operation will produce 8 new chromosomes
    // from selected parents
    GaCouplingParams coupParam( 8 );

    // make population configuration
    // use defined population parameters
    // use same comparator for sorting as comparator used by chromosomes
    // use selection operation which randomly selects chromosomes
    // use replacement operation which randomly chooses chromosomes
    // from population which are going to be replaced,
    // but keeps best chromosomes
    // use simple coupling
    // disable scaling
    GaPopulationConfiguration populationConfig( populationParams,
        &configBlock.GetFitnessComparator(),
        GaSelectionCatalogue::Instance().GetEntryData( "GaSelectRandomBest" ),
        &selParam,
        GaReplacementCatalogue::Instance().GetEntryData( "GaReplaceRandom" ),
        &repParam,
        GaCouplingCatalogue::Instance().GetEntryData( "GaSimpleCoupling" ),
        &coupParam,
        NULL, NULL );

    // make population
    // with previously defined prototype of chromosomes
    // and population configuration
    GaPopulation population( &prototype, &populationConfig );

    // make parameters for genetic algorithms
    // algorithm will use two workers
    GaMultithreadingAlgorithmParams algorithmParams( 2 );
    // make incremental algorithm with previously defined
    // population and parameters
    GaIncrementalAlgorithm algorithm( &population, algorithmParams );

    // make parameters for stop criteria based on fitness value
    // stop when best chromosome reaches fitness value of 1
    GaFitnessCriteriaParams criteriaParams( 1, GFC_EQUALS_TO,
        GSV_BEST_FITNESS );

    // sets algorithm's stop criteria (base on fitness value)
    // and its parameters
    algorithm.SetStopCriteria(
        GaStopCriteriaCatalogue::Instance().GetEntryData( 
        "GaFitnessCriteria" ),
        &criteriaParams );

    // make observer and subscribe it on algorithms events
    ScheduleObserver observer;
    algorithm.SubscribeObserver( &observer );

    // start execution 
    algorithm.StartSolving( false );

    observer.WaitEvent();

    // free memory used by GAL internal structures
    GaFinalize();

    return 0;
}

Note that GaInitialize must be called before any part of GAL is used, and GaFinalize must be called before program exits or when GAL is not needed anymore.

Configuration

Configuration file

Types of objects:

  1. professor (#prof tag) – describes professor.
  2. course (#course tag) – describes course.
  3. room (#room tag) – describes room.
  4. group (#group tag) – describes student group.
  5. course class (#class tag) – describes class, binds
    professor, course and student groups.

Each object begins with its tag and finishes with #end tag, all tags must be in separate lines. In a body of an object each line contains only one key and value pair (attribute) separated by = character. Each attribute should be specified just one time except for group attribute in #group object which can have multiple group attributes. Tag and key names are case sensitive. List of objects’ attributes:

  1. #prof
    • id (number, required) – ID of the professor.
    • name (string, required) – name of the professor.
  2. #course
    • id (number, required) – ID of the course.
    • name (string, required) – name of the course.
  3. #room
    • name (string, required) – name of the room.
    • size (number, required) – number of seats in the room.
    • lab (boolean, optional) – indicates if the room is lab (has computers).
      If not specified, default value is false.
  4. #group
    • id (number, required) – ID of the student groups.
    • name (string, required) – name of the student groups.
    • size (number, required) – number of students in the group.
  5. #class
    • professor (number, required) – ID of a professor.
      Binds professor to class.
    • course (number, required) – ID of a course.
      Binds course to class.
    • group (number, required) – ID of a student group.
      Binds student group to class.
      Each class can be bound to multiple student groups.
    • duration (number, optional) – duration of class (in hours).
      If not specified, default value is 1.
    • lab (boolean, optional) – is the class requires computers in a room.
      If not specified, default value is false.

Note that professor, student group and course objects must be defined before they are bound to course class object.

Example of configuration file

#prof
    id = 1
    name = John Smith
#end

#course
    id = 1
    name = Introduction to Programming
#end

#room
    name = R1
    lab = true
    size = 24
#end

#group
    id = 1
    name = 1O1
    size = 19
#end

#group
    id = 2
    name = 1O2
    size = 20
#end

#class
    professor = 1
    course = 1
    duration = 2
    group = 1
    group = 2
#end

#class
    professor = 1
    course = 1
    duration = 3
    group = 1
    lab = true
#end

#class
    professor = 1
    course = 1
    duration = 3
    group = 2
    lab = true
#end

Parsing configuration

Parsing of configuration file is done by Configuration class. ParseFile method opens and parse configuration file. It searches for object tags and calls appropriate method for parsing object. ParseFile method also clears previously parsed object.

public:

    void ParseFile(char* fileName);

private:

    Professor* ParseProfessor(ifstream& file);
    StudentsGroup* ParseStudentsGroup(ifstream& file);
    Course* ParseCourse(ifstream& file);
    Room* ParseRoom(ifstream& file);
    CourseClass* ParseCourseClass(ifstream& file);

To parse file:

    Configuration::GetInstance().ParseFile( "GaSchedule.cfg" );

Parsed objects are kept in hash map except course classes, so they can be accessed easily and fast.

private:

    hash_map<int, Professor*> _professors;
    hash_map<int, StudentsGroup*> _studentGroups;
    hash_map<int, Course*> _courses;
    hash_map<int, Room*> _rooms;

    list<CourseClass*> _courseClasses;

Configuration class also contains methods for retrieving parsed information and objects.

public:

    inline Professor* GetProfessorById(int id) //...
    inline int GetNumberOfProfessors() const //...

    inline StudentsGroup* GetStudentsGroupById(int id) //...
    inline int GetNumberOfStudentGroups() const //...

    inline Course* GetCourseById(int id) //...
    inline int GetNumberOfCourses() const //...

    inline Room* GetRoomById(int id) //...
    inline int GetNumberOfRooms() const //...

    inline const list<CourseClass*>& GetCourseClasses() const //...
    inline int GetNumberOfCourseClasses() const //...

Downloads

Source code and Demo application

08. April 2013 · Comments Off on AI for Target Number Game using Genetic Algorithm · Categories: Programming · Tags: , ,

In this game, the player has the task to find a mathematical expression whose value is closest to a randomly chosen value using only basic operations and a predefined set of numbers. The game has other restrictions: all intermediate results and the final result must be positive integer numbers. Additional restrictions are also placed on the selected and target numbers. The player gets four random numbers between 1 and 9, one number from the { 10, 15, 20 } set and one from the { 25, 50, 75, 100 } set. The target number is randomly chosen from the interval [100,999]. The player can use the selected numbers only once. Whoever has the expression with the closest number to the target wins, and if there is a tie, the player who was choosing the numbers wins.

ga_tng_app

Number of Possible Expressions

Before implementing the actual algorithm for solving this problem, it would be interesting to see how many expressions we can make using the selected numbers and basic operations. The number of possible expression trees for a defined number of leafs (numbers used in the expression) is defined by the Catalan number where n should be the number of leafs minus 1.

The number of possible expressions is also governed by the number of performed operations and the number of combinations of the selected numbers. The number of combinations is defined by the binomial coefficient where n is the count of selected numbers and k is the count of numbers that we actually use in the expression (expression tree leafs).

The final formula for calculating the number of expressions that can be made using n numbers, assuming that we only use the basic math operations { +, -, *, / } is where n is the count of selected numbers that we can use. The actual count of expressions that we should search using brute force algorithm can be reduced to since expressions that don’t use all the selected numbers are already represented by the subtrees of the longest possible expressions (the ones that use all the numbers). This formula yields around 600,000 expressions for 6 numbers, but many of these expressions are not valid (they may contain division by zero or division operations that don’t produce an integer result), or they are not different in any significant way from other expressions, so the actual count can be reduced even further.

Genetic Algorithm

In this example, a genetic algorithm is used instead of the brute force algorithm. Genetic Algorithm Library is used to implement the algorithm. Detailed information for implementing custom genetic operations are provided in the referenced article and they won’t be discussed here.

Chromosome

The first that should be considered is how to represent an expression with a chromosome in GA that will be suitable for performing the various genetic operations and which will allow them to preserve and propagate the good characteristics to the offspring.

Representation

This example will use a tree representation of the expression which will allow crossover and mutation operations to be implemented easily. Another thing that should be considered is that the genetic algorithm can produce many erratic expressions, introduce useless operations into an expression, or build many chromosomes with different expression trees for expressions that are essentially the same. Solutions for these problems are discussed in the next two sections.

A node of the expression tree is represented by the TngNode struct.

struct TngNode
{

  TngNodeType _type;
  int _value;

  /* structure of the tree */
  TngNode* _left;
  TngNode* _right;
  TngNode* _parent;

  /* ... */

};

The _type field stores the type of the node (whether the node is a number or operation, and if it is an operation, which operation it is). If the node type is number, the _value field stores the index of the selected number stored in the node; otherwise, this field is unused.

The chromosome is represented by the TngChromosome class. The selected and target numbers are stored in a chromosome configuration block represented by TngConfigBlock.

class TngConfigBlock : public Chromosome::GaChromosomeOperationsBlock
{

private:

  int _numbers[ TNG_NUMBER_COUNT ];
  int _targetNumber;

public:

  TngConfigBlock(Chromosome::GaCrossoverOperation* crossoverOperation,
    Chromosome::GaMutationOperation* mutationOperation,
    Chromosome::GaFitnessOperation* fitnessOperation,
    Chromosome::GaFitnessComparator* fitnessComparator,
    Chromosome::GaChromosomeParams* parameters);

  TngConfigBlock(const int* numbers,
    int targetNumber,
    Chromosome::GaCrossoverOperation* crossoverOperation,
    Chromosome::GaMutationOperation* mutationOperation,
    Chromosome::GaFitnessOperation* fitnessOperation,
    Chromosome::GaFitnessComparator* fitnessComparator,
    Chromosome::GaChromosomeParams* parameters);

  TngConfigBlock(const TngConfigBlock& rhs);
  inline void GACALL SetNumbers(const int* numbers);
  inline int* GetNumbers();
  inline const int* GetNumbers() const;
  inline void GACALL SetTargetNumber(int number);
  inline int GACALL GetTargetNumber() const;

};

class TngChromosome : public Chromosome::GaDynamicOperationChromosome
{

private:

  TngNode* _root;
  TngNode* _backup;

public:

  TngChromosome(TngConfigBlock* configBlock) ;
  TngChromosome(const TngChromosome& c, bool setupOnly);
  virtual ~TngChromosome();
  virtual Chromosome::GaChromosomePtr GACALL MakeCopy(
    bool setupOnly) const;
  virtual Chromosome::GaChromosomePtr GACALL MakeNewFromPrototype() const;
  virtual void GACALL PreapareForMutation();
  virtual void GACALL AcceptMutation();
  virtual void GACALL RejectMutation();
  inline void GACALL SetRoot(TngNode* root);
  virtual int GACALL GetCodeSize(void) const;
  inline TngNode* GACALL GetRoot();
  inline const TngNode* GACALL GetRoot() const;
  virtual bool GACALL operator ==(const Chromosome::GaChromosome& c) const;

};

Calculating the value of an expression is performed by in-order traversal of the expression tree calculating the value of each node.

int GACALL TngAdd(int a, int b) { return a + b; }
int GACALL TngSub(int a, int b) { return a - b; }
int GACALL TngMul(int a, int b) { return a * b; }
int GACALL TngDiv(int a, int b) { return !b || a % b ? a : a / b; }

typedef int (GACALL *TngOp)(int, int);
TngOp TngOps[] = { TngAdd, TngSub, TngMul, TngDiv };

inline int GACALL TngOpExec(TngNodeType nodeType,
  int a,
  int b) { return TngOps[ nodeType - 1 ]( a, b ); }

int GACALL TngCalculateValue(const TngNode* node,
  const int* values)
{
  if( node->_type == TNT_NUMBER )
    return values[ node->_value ];

    return TngOpExec( node->_type,
      TngCalculateValue( node->_left, values ),
      TngCalculateValue( node->_right, values ) );
}

Notice that the division operation has special cases for invalid operands. Why such situations are handled in this way is explained in the next section.

Reduction of Expression Tree

The purpose of the reduction operation is to remove useless and erratic operations from the expression. Cases which will trigger reduction are:

  • division by zero,
  • division that does not produce an integer number, and
  • operations that yield the same result as one of their sub-expressions (i.e., multiplying or dividing by an expression that has a value of 1, and adding or subtraction of expressions that has a value of 0).

This operation replaces the expression with its sub-expression that yields the same result if such a sub-expression exists. As it is explained in the previous section, invalid division operations return a result as if the operation was not performed, effectively eliminating it from the expression.

reduction of + operation reduction of * operation
reduction of invalid / operation reduction to an equivalent sub tree

Reduction is implemented by the TngReduceTree function.

Normalization of Expression Tree

Normalization transforms encoding by changing the order in which successive + and * operations are performed and the order of their operands. Operands or sub-expressions of these operations are sorted by their value in increasing order. That way, we can transform many seemingly different expressions to a single form.

normalization of + and * operations

Similar transformations can be applied to successive – and / operations, but these are not implemented in this example.

While the motivation for a reduction operation is quite clear, the need for normalization operation is not so obvious. Allowing different encodings to represent the same solution effect of implicit parallelism is diminished as one of the strongest features of a genetic algorithm, and its performance is severely reduced. Performing this transformation ensures that encodings obey convention that reduces the number of duplicate encodings, effectively narrowing the search space and improving the algorithm’s performance.

Normalization is implemented by the TngNormalizeTree function.

Fitness assignment

The fitness operation calculates how far off is the value of the expression from the targeted number. The actual fitness function is , where t is the target number and n is the calculated value of the expression. This means that the fitness values are in the range (0,1] and a greater value means the chromosome is closer to the target number. The algorithm can stop when a chromosome with a fitness value of 1 is found.

Crossover

Crossover makes an offspring chromosome by copying an expression tree of one of its parents, but it removes a random sub-tree from the copy and inserts a random sub-tree of the other parent in place of the one removed. Crossover must ensure that the inserted sub-tree does not contain numbers already used in the rest of the offspring tree. To do this, it selects a pair of sub-trees that will not produce an expression tree with more numbers than is allowed; after that, if it detects that the expression uses a number more times than is allowed, the operation simply replaces it with an unused number. After crossover, reduction and normalization are performed.

Crossover is implemented by the TngCrossover class.

Mutation

There are two types of mutation performed on a chromosome. The first one swaps positions of two random sub-trees and the second flips an operation or number randomly. Each type of mutation has equal chances of being performed. After a mutation is performed, chromosomes are reduced and normalized.

Mutation type I
Mutation type II

Mutation is implemented by the TngMutation class.

Algorithm Setup

In each generation, the algorithm selects eight parents using roulette wheel selection and produces eight parents using simple coupling that uses the custom crossover and mutation operations described previously. The algorithm replaces chromosomes which have the worst fitness with the produced offspring. Offspring chromosomes that are already in the population are not inserted.

CpuPlayer wraps the genetic algorithm and provides an interface to the other parts of the application. The constructor of the class initializes parameters and configurations of the genetic algorithms, and the Start method make an instance of the algorithm and starts it.

CpuPlayer::CpuPlayer(CStatic* cpuStatus,
  CListCtrl* populationContent) : _result(0), _cpuStatus(cpuStatus),
  _populationContent(populationContent), _algorithm(NULL),
  _chromosomeParams( 0.3f, 1, false, 0.8f, 1 ),
  _configBlock( &_crossover, &_mutation,
    &_fitnessOperation, GaFitnessComparatorCatalogue::Instance()
    .GetEntryData( "GaMaxFitnessComparator" ),
    &_chromosomeParams ),
  _populationParams( 32, false, true, false, 0, 0 ),
  _populationConfig( _populationParams,
    &_configBlock.GetFitnessComparator(),
    GaSelectionCatalogue::Instance().GetEntryData(
    "GaSelectRouletteWheel" ),
    &Population::SelectionOperations::GaSelectDuplicatesParams( false, 8 ),
    GaReplacementCatalogue::Instance().GetEntryData(
    "GaReplaceWorst" ),
    &Population::GaReplacementParams( 8 ),
    GaCouplingCatalogue::Instance().GetEntryData(
    "GaSimpleCoupling" ),
    &Population::GaCouplingParams( 8 ),
    NULL, NULL),
  _algorithmParams( 2 ) { } 
Algorithm parameters
mutation probability: 30%
mutation size: 1 gene
only accept mutations that improve fitness: yes
crossover probability: 80%
crossover points: 1
population size: 32 chromosomes
sorted population: yes
fitness sorting: maximization
selection type: roulette wheel
selection size: 8
coupling type: simple coupling
number of offsprings to produce: 8
replacement type: replace worst
chromosomes to replace: 8
number of worker threads: 2
void CpuPlayer::Start(const NumberGenerator& numbers)
{
  _populationContent->DeleteAllItems();

  _configBlock.SetNumbers( numbers.GetGenerated() );
  _configBlock.SetTargetNumber(
    numbers[ NumberGenerator::NUMBERS_TO_GENERATE - 1 ] );

  TNG::TngChromosome prototype( &_configBlock );
  Population::GaPopulation population( &prototype, &_populationConfig );
  Algorithm::SimpleAlgorithms::GaIncrementalAlgorithm algorithm(
    &population, _algorithmParams );

  _algorithm = &algorithm; 

  TNG::TngStopCriteriaParams criteriaParams( GAME_TIME - 2 );
  algorithm.SetStopCriteria( &_stopCriteria, &criteriaParams);

  algorithm.SubscribeObserver( this );

  algorithm.StartSolving( false );
  algorithm.WaitForThreads();
}

Custom stop criteria are implemented by the TngStopCriteria class. It causes the genetic algorithm to stop when chromosomes with a fitness value of 1 is found or just before time expires. Parameters for this stop criteria are handled by TngStopCriteriaParams and they store the time when the algorithm was started and the duration of the game.

Supporting Classes

Generating numbers is managed by the NumberGenerator class. Timing is implemented in the Timer class. The InputManager class manages, parses, and calculates the player’s input. The Results struct stores the state of the game after it ends. The game flow is implemented by the Master class. These classes are located in the Game namespace.

Parsing User’s Expression

Parsing and calculating user’s expressions is implemented by the two classes Lexer and Parser located in the Parsing namespace.

Lexer reads tokens from input and feeds the parser. A token is represented by the LexSymbol struct.

struct LexSymbol
{
  LexSymbolType _type;
  int _value;
  int _position;

  /* ... */
};

_type stores a type of token, the _value field stores information that depends on the type of token, and _position stores the position of the token’s first character in the input.

LexSymbolType defines the allowed token types:

enum LexSymbolType
{
  LEX_ST_PARENS_OPEN,
  LEX_ST_PARENS_CLOSE,
  LEX_ST_NUMBER,
  LEX_ST_OPERATOR,
  LEX_ST_END
};

For the LEX_ST_NUMBER token, the value field stores the actual number, and for LEX_ST_OPERATOR, the field stores the type of operator:

enum LexOperatorType
{
  LEX_OT_PLUS,
  LEX_OT_MINUS,
  LEX_OT_TIMES,
  LEX_OT_OVER
};

The Get method of the Lexer class implements tokenization of the input string and returns the next token. If the end of input is reached, the method returns a LEX_ST_END token. When an invalid character is detected, it throws a SyntaxException.

LexSymbol Lexer::Get()
{
  int c = GetChar();
  switch( c )
  {
  case '(':
    return LexSymbol( LEX_ST_PARENS_OPEN, GetPosition() );
  case ')':
    return LexSymbol( LEX_ST_PARENS_CLOSE, GetPosition() );
  case '+':
    return LexSymbol( LEX_ST_OPERATOR, LEX_OT_PLUS, GetPosition() );
  case '-':
    return LexSymbol( LEX_ST_OPERATOR, LEX_OT_MINUS, GetPosition() );
  case '*':
    return LexSymbol( LEX_ST_OPERATOR, LEX_OT_TIMES, GetPosition() );
  case '/':
    return LexSymbol( LEX_ST_OPERATOR, LEX_OT_OVER, GetPosition() );
  case -1:
    return LexSymbol( LEX_ST_END, GetPosition() );

  default:
    if( !IsDigit( c ) ) throw SyntaxException( GetPosition() );

    int pos = GetPosition(), num = ToNum( c );
    while( IsDigit( PeekChar() ) )
      num = num * 10 + ToNum( GetChar() );

    return LexSymbol( LEX_ST_NUMBER, num, pos );
  }
}

The Parse method of the Parser class analyzes tokens and calculates the value of the user’s expression. This method will throw one of the following exceptions: SyntaxException, InputException, or ArithmeticException, or it will return the value of the expression if there were no errors. If a list of allowed numbers is provided as an argument to the method, it will throw an InputException if the illegal number is detected in the user’s input; otherwise, all numbers are allowed. An ArithmeticException is thrown when an illegal arithmetic operation is detected (such as a division by zero), and finally, a SyntaxException is thrown when there is a syntax error in the user’s input.

int Parser::Parse(int count/* = 0*/,
       const int* allowedNumbers/* = NULL*/)
{
  for( int i = count - 1; i >= 0; i-- )
    _allowedNumber[ allowedNumbers[ i ] ]++;

  while( 1 )
  {
    LexSymbol symbol = _lexer.Get();

    switch( symbol._type )
    {

    case LEX_ST_END: return Calculate( symbol._position );
    case LEX_ST_PARENS_OPEN:
      OpenParens( symbol._position );
      break;
    case LEX_ST_PARENS_CLOSE:
      CloseParens( symbol._position );
      break;
    case LEX_ST_NUMBER:
      StoreNumber( symbol._value, symbol._position );
      break;
    case LEX_ST_OPERATOR:
      StoreOperator( symbol._value, symbol._position );
      break;
    }
  }

  throw SyntaxException( -1 );
}

StoreOperator, StoreNumber, OpenParens, and CloseParens store operations that should be performed, and calculates the order (priority) of their execution. The Calculate method performs stored operations according to their priority and calculates the expression’s value.

Downloads

Source code and Demo application

08. April 2013 · Comments Off on Solving Traveling Salesperson Problem using Genetic Algorithm · Categories: Programming · Tags: , ,

We are going implement genetic algorithm that solves Traveling Salesperson Problem using Genetic Algorithm Library.

Traveling Salesperson Problem Application

Screenshot – TSP application

The chromosome is an array of cities [pointers to objects of the TspCity class] in the order in which they are visited. It is implemented by the TspChromosome class. The class inherits GaMultiValueChromosome to implement a custom initialization of the chromosome by overriding the MakeFromPrototype method. This method copies cities into the chromosomes’ code and then it shuffles their positions. This class also overrides the MakeCopy method and defines a copy constructor.

class TspChromosome : public GaMultiValueChromosome<const TspCity*>
{
public:
    TspChromosome(GaChromosomeDomainBlock<const TspCity*>* configBlock) :
        GaMultiValueChromosome(configBlock) { }

    TspChromosome(const TspChromosome& chromosome,
        bool setupOnly) :
        GaMultiValueChromosome<const TspCity*>(chromosome, setupOnly) { }

    virtual GaChromosomePtr GACALL MakeCopy(bool setupOnly) const
        { return new TspChromosome( *this, setupOnly ); }

    virtual GaChromosomePtr GACALL MakeNewFromPrototype() const;

    int GACALL GetCityPosition(const TspCity* city) const;
};

Using a simple single-point or multi-point crossover operation will generate a large amount of invalid solutions which degrades the algorithm’s performance and results. To prevent the generation of invalid solutions, the algorithm uses a custom crossover operation. The operation takes a random city from one parent and copies it to the child chromosome. Then, it searches for the cities which are connected to the chosen city [in both parents] and takes the nearest one [and copies it to the child chromosome] if it is not already taken. It is taken if the operation chooses another connected city. If all the connected cities are taken, the operation randomly chooses a city that has not been taken. Then, the crossover uses that city to extend the path in the same way. The process is repeated to select all the cities. The TspCrossover class implements this crossover operation:

class TspCrossover : public GaCrossoverOperation
{
public:
    virtual GaChromosomePtr GACALL operator ()(
        const GaChromosome* parent1,
        const GaChromosome* parent2) const;

    virtual GaParameters* GACALL MakeParameters() const { return NULL; }

    virtual bool GACALL CheckParameters(
        const GaParameters& parameters) const { return true; }

private:
    inline void SelectNextCity(const TspCity* previousCity,
        const TspCity** currentBestNextCity,
        const TspCity* nextCity) const;
};

The algorithm uses the built-in GaSwapMutation operation. The fitness value is equal to the length of the path. The TspFitnessOperation class implements the fitness operation:

class TspFitness : public GaFitnessOperation
{
public:
    virtual float GACALL operator ()(
        const GaChromosome* chromosome) const;

    virtual GaParameters* GACALL MakeParameters() const { return NULL; }

    virtual bool GACALL CheckParameters(
        const GaParameters& parameters) const { return true; }
};

Parameters of chromosomes:

  1. mutation probability: 3%
  2. mutation size: 2
  3. improving only mutations: no
  4. crossover probability: 80%
  5. number of crossover points: 1 [ignored]

CCB:

  1. TspCrossover
  2. TspSwapMutation
  3. TspFitnessOperation
  4. TspMinFitnessComparator
  5. Value set is not defined

Population parameters:

  1. population size: 100
  2. resizable population: no [an incremental algorithm is used which does not require a resizable population]
  3. population is sorted: yes
  4. scaled fitness is used for sorting: no
  5. tracking of the best chromosomes: 0 [population is already sorted]
  6. tracking of the worst chromosomes: 0 [population is already sorted]

Configuration of the population:

  1. GaSelectRandomBest selection which selects 8 chromosomes
  2. GaSimpleCoupling which produces 8 offspring chromosomes
  3. GaRandomReplaces which replaces 8 chromosomes in each generation, with an elitism size of 10 chromosomes
  4. No scaling operation

The algorithm uses GaFitnessProgressCriteria because the exact termination condition is not known. The criteria will stop the algorithm if it is unable to improve the fitness value for more than 1 in 50000 generations. The genetic algorithm is incremental.

The TSP class is the container for the object of the algorithm. The TspCity class represents and stores information about a city [such as its coordinates and name]. It has the GetDistance method which calculates the distances between the cities. TspCities manages the collection of cities entered by the user.

Downloads

Source code and Demo application