@@ 1,11 1,56 @@
# Intro
-This is report for a hw01 for EOA course on CTU TEE. The goal
+This is report for a hw01 for EOA course on CTU FEE. The goal
has been to try to solve traveling salesperson problem by
-means of evolutionary algorithms. The report covers the implemented
+means of evolutionary algorithms.
+
+The traveling salesperson problem tries to find the shortest possible
+route when traveling through multiple cities, visiting every city
+exactly once, returning to the origin city.
+
+The report covers the implemented
algorithms and the results on 10 TSP instances from TSPLIB. All of
those chosen instances are using euclidian metric and are 2D.
+{latex-placement="ht"}
+
+# Prepared algorithms
+Three generic algorithms have been used for solving the TSP.
+Then various representations and operators have been tried on them.
+
+## Evolution algorithm
+The algorithm keeps a population of N each epoch and generates offsprings
+out of the current population. Specifically it uses the following steps:
+
+1. Obtain initial population (ie. random or specialized heuristic).
+2. Select parents to make offsprings from (selection).
+3. Pair the selected parents (pairing).
+4. Somehow join the parent pairs to make an offspring (crossover).
+5. Replace the current population with a new one, combining the current population and offsprings. (replacement)
+6. Repeat steps 2 to 5 until terminating condition is reached (number of iterations)
+
+During its run the algorithm saves the best candidates encountered. It saves them even during
+one epoch for better granularity. This also ensures that if best candidate does not make it
+into the next population, it is still captured by the algorithm and can be returned as
+best candidate at the end. The implementation is in `eoa_lib/src/evolution.rs`.
+
+## Local search
+The local search implementation is implementation of the first improving strategy,
+where the current best solution is being perturbated, until a better solution is found.
+And then, the process starts over from this better solution.
+
+The local search implementation supports evolutionary strategies,
+ie. changing the perturbation paramters during the run, but none
+were used for TSP.
+
+The implementation is available in `eoa_lib/src/local_search/mod.rs`.
+
+## Random search
+Random search initializes new random elements in the search space each
+iteration and saves new elements if they are better than best found.
+
+The implementation is available in `eoa_lib/src/random_search.rs`
+
# Representations
Two representation have been chosen for the TSP problem,
@@ 19,8 64,8 @@ Couple of perturbations and crossovers have been implemented:
- Perturbations
- Move single random city to random position
- - Swap two cities
- - Reverse subsequence of cities
+ - Swap two randomly selected cities
+ - Reverse subsequence of cities (random starting point and random length)
- Crossovers
- Cycle crossover
- Partially mapped crossover
@@ 33,13 78,14 @@ descriptions of the algorithms follow in next sections.
### Crossovers
All used crossovers take two parents and it's possible to create two
offsprings out of the two parents by switching the parent positions.
-(which one is first parent, second parent)
+(switching first parent and second parent)
#### Cycle crossover
-The cycle crossover creates a cycle, takes parts of the first parent
-from the cycle and fills the rest from the second parent.
+The cycle crossover creates a cycle of indices, takes parts of the first parent
+from the cycle and moves them to offspring and fills the rest from the second parent.
The cycle is created as follows:
+
1. Start with index 0, call it current index
2. Save current index
3. Look at current index in first parent, let's call it current element
@@ 47,6 93,7 @@ The cycle is created as follows:
5. Repeat 2 - 4 until reaching index 0 again
Then the offspring is created as follows:
+
1. Clone the second parent
2. Iterate the saved cycle indices, take element at index from first parent and copy it to offspring at the same index
@@ 58,6 105,7 @@ The way to ensure that, while still ensuring the result is a valid permutation,
always swap the elements.
Offspring is created as follows:
+
1. Clone second parent
2. Then, for every index i between the cross points:
3. Take the element on index i from first parent
@@ 67,7 115,31 @@ Offspring is created as follows:
#### Edge recombination crossover
Edge recombination is the most complicated from the three crossover operators.
-First, an adjacency list is created for both parents.
+First, an adjacency list is created for both parents. Let's take an example
+of a permutation: 012345.
+The element 0 has 5 and 1 as adjacement nodes. 1 has 0 and 2 and so on.
+The two adjacency lists are then joined together, while omitting repetitions.
+That means that a single element will have from 2 to 4 elements in its adjacency list.
+
+To make an offspring:
+
+1. First element is taken out of the first parent, this is the current element (city).
+2. The current element is appended to offspring
+3. The current element is removed from adjacency lists of all other elements.
+4. Then, all the cities in adjacency list for current element are taken and one with minimum neighbors in its adjacency list is taken. If there are mutliple such cities, random one is chosen.
+5. The found city becomes current element.
+6. Steps 2 to 5 are repeated until all elements are taken.
+
+In the code, the algorithm has been implemented slightly differently.
+Instead of removing any elements, the elements are just marked as removed.
+Also, instead of constructing a list of adjacency nodes progressively,
+a vector of 4 elements is made beforehand. Each element may be unset (an Option).
+Then, the adjacencies from first parent are put to positions 0, 1 and adjacencies from
+second parent to positions 2 and 3. If there would be a repetition, it's left unset.
+This then allows for static allocation only. This is thanks to a library called nalgebra
+is used and it's possible to tell it the dimensions of adjacency matrix
+beforehand. However for the runs of the algorithms, `Dyn` dimension has been used
+out of convenience, so dynamic allocation is performed.
## Binary string
Classical perturbations and crossovers have been
@@ 77,7 149,7 @@ implemented for the binary string representation, specifically:
- Flip each bit with probability p
- Flip single random bit
- Flip of N bits (N is chosen beforehand, not randomly)
- - Flip of whole string
+ - Flip of whole binary string
- Crossover
- N point crossover
@@ 103,16 175,6 @@ Also, one-point and two-point crossovers
have been implemented separately for more effective implementation
than the generic N-point.
-# Prepared algorithms
-
-## Evolution algorithm
-
-## Local search
-
-## Random search
-Random search initializes new random elements in the search space each
-iteration and saves new elements if they are better than best found.
-
# Heuristics
Instead of starting with random solutions, two heuristics have been tried to
make the initial populations for the evolutionary algorithms.
@@ 136,28 198,123 @@ To initialize the whole population:
2. Generate chromosome starting from each city, but always choosing the second neighbor.
3. The rest of the population is initialized from randomly selected cities with randomly selected probability of choosing second neighbor.
-## Minimum spanning tree
- jjj
-
-# Results
-To compare all the algoritms on various instances, always at least 10 runs of the algorithm
-have been made on the given instance. All the P_s (TODO) graphs were then constructed from
-averaging between the runs. The fitness charts sometimes show less instances to not be too
-overwhelming.
+## Minimal spanning tree
+For using a minimal spanning tree the algorithm of Christofides and Serdyukov has been
+chosen as an inspiration. But instead of trying to find an Eulerian tour on the minimal
+spanning tree, a slightly different randomized approach has been taken. Specifically,
+random node is chosen as the starting point and then edges are chosen out of the minimal
+spanning tree to go through. If there are more edges to travel through, random one is chosen.
+If there are no edges left, nearest neighbor is chosen as next node.
-## Comparing perturbations on LS
+To initialize the whole population, the same algorithm is ran multiple times. Since
+it is random, its results should be different, at least slightly.
+# Results
+To compare all the algoritms on various instances, always 10 runs of the algorithm
+have been made on the given instance. All the graphs of probabilities of reaching
+target for given function evaluation were then constructed from
+averaging between the runs and between the instances. The fitness charts sometimes show less runs to not be too
+overwhelming. All evolution algorithms ran on 5000 iterations (epochs) and the local search
+and random search were adjusted to run on the same number of fitness function evaluations.
+The population size for the evaluation algorithm 500 has been chosen.
+The number of parents (and thus offpsrings) is 250.
+
+The instances chosen from TSPLIB, all 2D Euclidean:
+
+- eil51
+- eil76
+- eil101
+- kroA100
+- ch150
+- kroA150
+- kroA200
+- a280
+- u574
+- u724
## Comparing algorithms
To compare the algorithms,
first it has been ensured the algorithms were tweaked to produce the best results (best that the author
has been capable of). Then, they were ran on 10 instances of TSP and averaged in the following chart:
+TODO Here should be a graph with fitness on eil76
+- EA
+- LS
+- RS
+
+TODO Here should be a graph with probability of success (all instances averaged)
+- EA
+- LS
+- RS
+
+## Comparing perturbations on LS
+Four perturbations have been evaluated:
+- Moving single city
+- Swapping of two cities
+- Reversing subsequence
+- Combination of the above (specifically single random perturbation has been chosen to run)
+
+TODO Here should be a graph with fitness on eil76
+- LS (move)
+- LS (swap)
+- LS (reverse)
+- LS (mix)
+
+TODO Here should be a graph with probability of success(all instances averaged)
+- LS (move)
+- LS (swap)
+- LS (reverse)
+- LS (mix)
+
+from the experiment it seems the reversal of a subsequence is the best singular perturbation,
+but the combination is even better. That could be explained by the fact that in cases the subsequence
+reversal gets stuck somewhere, the other perturbations are able to recover.
+
## Comparing crossovers
During evaluation of the various crossovers, it has become apparent that with the currently
-chosen
+chosen parameters of the algorithms, the dominant parts that are responsible for the good
+convergence are: selection, perturbation and replacement, but not the crossover itself.
+In other words, just replacing the crossovers produced similar results. For this reason,
+the mutations probabilities have been lowered significantly for this comparison. This
+then allows for comparison between the crossovers themselves.
+
+From the runs, the edge recombination has produced the best results, with partially
+mapped crossover being the second and cycle crossover being the worst.
+
+TODO Here should be a graph with probability of success(all instances averaged)
+- EA (pmx)
+- EA (erx)
+- EA (cx)
+
+## Comparing representations
+
+TODO Here should be a graph with probability of success(all instances averaged)
+- EA (binary)
+- EA (normal)
+- LS (binary)
+- LS (normal)
+
+On one hand the binary string representation is simpler, because it doesn't require special treatment
+for crossovers. Any crossover leads to valid result. On the other hand since it's not so specific to the
+given problem, it seems to lead to worse results than the node permutation. It seems that mainly the reverse
+subsequence perturbation is making it that much better, as can be seen from comparison of various LS perturbations.
## Comparing heuristics
+Both of the heuristics are capable of making initial solutions much better
+than random ones. Here is a fitness graph with only the two heuristics.
+
+TODO Here should be a graph with probability of success(all instances averaged)
+- EA (normal)
+- EA (nn)
+- EA (mst)
+- LS (normal)
+- LS (nn)
+- EA (mst)
+
+From the results it can be seen the nearest neighbor heuristic performs better.
+But this could be caused by the fact that minimal spanning tree has been used
+along with a randomized approach. If more deterministic approach of finding
+the best Eulerian path has been chosen, it's possible it would perform better.
# Things done above minimal requirements
@@ 183,12 340,12 @@ the solutions are put to `tsp_hw01/solutions`
# Usage of LLM
-While I was working on this homework, I have used LLM for certain tasks,
-specifically I have used it for the tasks that I do not like to do much myself:
+While I was working on this homework, I have used LLM for writing certain parts of the code,
+specifically I have used it very extensively for the tasks that I do not like to do much myself:
- Loading data,
- Plotting graphs,
-- Refactoring at a lot of places (ie. at first I chose to make perturbation copy the chromosome,
- but I realized this is very ineffective for EAs afterwards and change it to change in-place instead),
+- Refactoring of a triviality at a lot of places at once (ie. at first I chose to make perturbation copy the chromosome,
+ but I realized this is very ineffective for EAs afterwards and changed it to change in-place instead),
- Writing some of the tests
As I am not proficient with Rust, sometimes I asked LLM to help with the syntax as well,
@@ 205,3 362,5 @@ I use Claude from within claude-code, a CLI tool that is capable
of reading files, executing commands and giving the model live feedback,
like outputs of ran commands. This means the LLM is able to iterate by
itself, without my intervention, for example when fixing errors.
+On the other hand it can use a lot of tokens quickly unless I make sure
+to run compact often.