Current section
Files
Jump to
Current section
Files
lib/optimization.ex
defmodule Numerix.Optimization do
@moduledoc """
Optimization algorithms to select the best element from a set of possible solutions.
"""
@default_opts [population_size: 50, mutation_prob: 0.2, elite_fraction: 0.2, iterations: 100]
@doc """
Genetic algorithm to find the solution with the lowest cost where `domain` is
a set of all possible values (i.e. ranges) in the solution and `cost_fun` determines
how optimal each solution is.
Example
iex> domain = [0..9] |> Stream.cycle |> Enum.take(10)
iex> cost_fun = fn(x) -> Enum.sum(x) end
iex> Numerix.Optimize.genetic(domain, cost_fun)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
## Options
* `:population_size` - the size of population to draw the solutions from
* `:mutation_prob` - the minimum probability that decides if mutation should occur
* `:elite_fraction` - the percentage of population that will form the elite group in each generation
* `:iterations` - the maximum number of generations to evolve the solutions
"""
@spec genetic([Range.t()], ([integer] -> number), Keyword.t()) :: [integer]
@lint [{Credo.Check.Refactor.Nesting, false}]
def genetic(domain, cost_fun, opts \\ []) do
merged_opts = Keyword.merge(@default_opts, opts)
top_elite = round(merged_opts[:elite_fraction] * merged_opts[:population_size])
population = init_population(domain, merged_opts[:population_size])
evolve = fn
[best | _rest], 0, _fun ->
best
pop, iteration, fun ->
Stream.repeatedly(fn ->
if :rand.uniform() < merged_opts[:mutation_prob] do
pop |> mutate(domain, top_elite)
else
pop |> crossover(top_elite)
end
end)
|> Stream.take(merged_opts[:population_size])
|> Stream.concat(pop |> Enum.take(top_elite))
|> Stream.map(&{cost_fun.(&1), &1})
|> Enum.sort()
|> Enum.map(fn {_cost, solution} -> solution end)
|> fun.(iteration - 1, fun)
end
evolve.(population, merged_opts[:iterations], evolve)
end
defp init_population(domain, size) do
Stream.repeatedly(fn -> Enum.map(domain, &Enum.random/1) end)
|> Enum.take(size)
end
defp crossover(population, top_elite) do
vector1 = population |> Enum.at(Enum.random(0..top_elite))
vector2 = population |> Enum.at(Enum.random(0..top_elite))
idx = random_index(vector1)
vector1
|> Enum.take(idx)
|> Enum.concat(vector2 |> Enum.drop(idx))
end
defp mutate(population, domain, top_elite) do
elite_idx = Enum.random(0..top_elite)
target_vector = population |> Enum.at(elite_idx)
target_idx = random_index(target_vector)
target_vector
|> Stream.with_index()
|> Stream.map(&do_mutate(&1, target_idx, domain))
|> Enum.to_list()
end
defp do_mutate({x, i}, target_idx, domain) when i == target_idx do
{min, max} = domain |> Enum.at(i) |> Enum.min_max()
cond do
:rand.uniform() < 0.5 and x > min ->
x - 1
x < max ->
x + 1
true ->
x
end
end
defp do_mutate({x, _i}, _target_idx, _domain), do: x
defp random_index(vector) do
max = Enum.count(vector) - 1
Enum.random(0..max)
end
end