genetic

genetic algorithm framework
git clone [email protected]:dracuxan/genetic.git
Log | Files | Refs | README

bad_apple.exs (3525B)


      1 defmodule BadApple.FrameCtx do
      2   use Agent
      3 
      4   def start_link(_opts \\ []) do
      5     Agent.start_link(fn -> [] end, name: __MODULE__)
      6   end
      7 
      8   def set_target(bits), do: Agent.update(__MODULE__, fn _ -> bits end)
      9   def get_target(), do: Agent.get(__MODULE__, & &1)
     10 end
     11 
     12 defmodule BadApple do
     13   alias Types.Chromosome
     14   @behaviour Problem
     15 
     16   @width 48
     17   @height 36
     18   @size @width * @height
     19 
     20   @impl true
     21   def genotype do
     22     genes =
     23       Stream.repeatedly(fn -> Enum.random([0, 1]) end)
     24       |> Enum.take(@size)
     25 
     26     %Chromosome{genes: genes, size: @size}
     27   end
     28 
     29   @impl true
     30   def fitness_function(%Chromosome{genes: genes}) do
     31     target = BadApple.FrameCtx.get_target()
     32 
     33     matches =
     34       Enum.zip(genes, target)
     35       |> Enum.count(fn {a, b} -> a == b end)
     36 
     37     matches / @size
     38   end
     39 
     40   @impl true
     41   def terminate?(population, generation) do
     42     best = hd(population)
     43     best.fitness >= 0.985 or generation >= 120
     44   end
     45 end
     46 
     47 defmodule BadApple.FrameIO do
     48   def load_bits(path) do
     49     {:ok, image} = Vix.Vips.Image.new_from_file(path)
     50     width = Vix.Vips.Image.width(image)
     51     height = Vix.Vips.Image.height(image)
     52     {:ok, bin} = Vix.Vips.Image.write_to_binary(image)
     53 
     54     expected = width * height
     55 
     56     if byte_size(bin) != expected do
     57       raise "Expected #{expected} bytes, got #{byte_size(bin)} for #{path}"
     58     end
     59 
     60     for <<px <- bin>> do
     61       if px >= 128, do: 1, else: 0
     62     end
     63   end
     64 end
     65 
     66 defmodule BadApple.Output do
     67   @width 48
     68   @height 36
     69 
     70   def save_bits_as_png(bits, out_path) do
     71     bytes =
     72       bits
     73       |> Enum.map(fn b -> if b == 1, do: 255, else: 0 end)
     74       |> :erlang.list_to_binary()
     75 
     76     {:ok, image} =
     77       Vix.Vips.Image.new_from_binary(bytes, @width, @height, 1, :VIPS_FORMAT_UCHAR)
     78 
     79     :ok = File.mkdir_p!(Path.dirname(out_path))
     80     :ok = Vix.Vips.Image.write_to_file(image, out_path)
     81   end
     82 end
     83 
     84 {:ok, _} = BadApple.FrameCtx.start_link()
     85 start_frame = 501
     86 count = 200
     87 
     88 frame_paths =
     89   "priv/frames_target/*.png"
     90   |> Path.wildcard()
     91   |> Enum.sort()
     92   |> Enum.drop(start_frame - 1)
     93   |> Enum.take(count)
     94 
     95 IO.puts("Processing #{length(frame_paths)} frames...")
     96 
     97 last_best = nil
     98 
     99 for {frame_path, idx} <- Enum.with_index(frame_paths, 1), reduce: last_best do
    100   prev_best ->
    101     target_bits = BadApple.FrameIO.load_bits(frame_path)
    102     BadApple.FrameCtx.set_target(target_bits)
    103 
    104     init_type =
    105       if prev_best do
    106         fn genotype, opts ->
    107           population_size = Keyword.get(opts, :population_size, 60)
    108           seed = prev_best.genes
    109 
    110           seeded =
    111             Enum.map(1..population_size, fn _ ->
    112               genes =
    113                 Enum.map(seed, fn bit ->
    114                   if :rand.uniform() < 0.10, do: 1 - bit, else: bit
    115                 end)
    116 
    117               %Types.Chromosome{genes: genes, size: length(genes)}
    118             end)
    119 
    120           [genotype.() | tl(seeded)]
    121         end
    122       else
    123         &Toolbox.Initialize.random/2
    124       end
    125 
    126     {best, generation} =
    127       Genetic.run(BadApple,
    128         population_size: 100,
    129         initialization_type: init_type,
    130         selection_type: &Toolbox.Selection.elite/2,
    131         mutation_type: &Toolbox.Mutation.flip/2,
    132         mutation_rate: 0.010,
    133         crossover_type: &Toolbox.Crossover.uniform/3
    134       )
    135 
    136     out_path = "priv/frames_out/#{String.pad_leading(Integer.to_string(idx), 5, "0")}.png"
    137     BadApple.Output.save_bits_as_png(best.genes, out_path)
    138 
    139     IO.puts(
    140       "\nFrame #{idx} done | gen=#{generation} | fitness=#{Float.round(best.fitness, 4)} | #{Path.basename(frame_path)}"
    141     )
    142 
    143     best
    144 end