I noticed this in the B1 edition as well!
Re:
$ elixir one_max.exs Current Best: 32
But wait, what’s going on here? Why is the algorithm stopping on a best fitness below 42? No matter how many times you run it, the algorithm will almost always certainly stop improving below 42. The problem is premature convergence.
At this point, the chapter’s example code looks like:
Code to Date
population = for _ <- 1..10, do: for _ <- 1..42, do: Enum.random(0..1)
evaluate = fn population ->
Enum.sort_by(population, &Enum.sum(&1), &>=/2)
end
selection = fn population ->
population
|> Enum.chunk_every(2)
|> Enum.map(&List.to_tuple(&1))
end
crossover = fn population ->
Enum.reduce(population, [], fn {p1, p2}, acc ->
cx_point = :rand.uniform(42)
{{h1, t1}, {h2, t2}} = {Enum.split(p1, cx_point), Enum.split(p2, cx_point)}
[h1 ++ t2 | [h2 ++ t1 | acc]]
end)
end
algorithm = fn population, algorithm ->
best = Enum.max_by(population, &Enum.sum(&1))
IO.write("\rCurrent Best: " <> Integer.to_string(Enum.sum(best)))
if Enum.sum(best) == 42 do
best
else
population
|> evaluate.()
|> selection.()
|> crossover.()
|> algorithm.(algorithm)
end
end
solution = algorithm.(population, algorithm)
IO.write("\n Answer is \n")
IO.inspect solution
I parameterized it thusly:
Tunable version
-population = for _ <- 1..10, do: for _ <- 1..42, do: Enum.random(0..1)
+problem_size = 42
+population_size = 100
+
+population = for _ <- 1..population_size, do: for _ <- 1..problem_size, do: Enum.random(0..1)
evaluate = fn population ->
Enum.sort_by(population, &Enum.sum(&1), &>=/2)
end
selection = fn population ->
population
|> Enum.chunk_every(2)
|> Enum.map(&List.to_tuple(&1))
end
crossover = fn population ->
Enum.reduce(population, [], fn {p1, p2}, acc ->
- cx_point = :rand.uniform(42)
+ cx_point = :rand.uniform(problem_size)
{{h1, t1}, {h2, t2}} = {Enum.split(p1, cx_point), Enum.split(p2, cx_point)}
[h1 ++ t2 | [h2 ++ t1 | acc]]
end)
end
algorithm = fn population, algorithm ->
best = Enum.max_by(population, &Enum.sum(&1))
IO.write("\rCurrent Best: " <> Integer.to_string(Enum.sum(best)))
- if Enum.sum(best) == 42 do
+ if Enum.sum(best) == problem_size do
best
else
population
|> evaluate.()
|> selection.()
|> crossover.()
|> algorithm.(algorithm)
end
end
solution = algorithm.(population, algorithm)
IO.write("\n Answer is \n")
IO.inspect solution
In my experimentation, with problem_size = 42
, not only did population_size = 100
always converge on the best answer, but even as low as population_size = 8
consistently converged. I started seeing the need for mutation around population_size = 6
, which normally gets stuck around 35
.
Alternatively, increasing the size of the problem to problem_size = 420
usually converged correctly, but with enough time to watch things work. problem_size = 4200
consistently gets stuck around 2400
, as the narrative of the chapter wants it to.