Inspired by Richard Dawkins' weasel program, this is a genetic algorithm,
written in Ruby, to evolve the same "METHINKS IT IS LIKE A WEASEL" string. It's
really fun to experiment with changing the probability constants (mutation,
reproduction) and the algorithm in Evolver#advance, which advances the
population of strings to the next generation. Have fun!
The default algorithm works as follows:
- Initialize a population of random strings.
- Mutate each string with probability 'mutation_probability'.
- Breed each string with another random string from the population with
probability 'breed_probability', keeping the newborn population separate.
- Kill random newborns until the number of newborns is less than 1/4 of the
size of the main population.
- Select a random half of the main population and replace its weakest (furthest
from the target) members with the remaining newborns.
- Go back to Step 2.
In about 5000-10000 generations, the strongest member of the population
stabilizes to the target string.
The same algorithm can be applied to other problems, like finding the maximum of
a mathematical function, by just modifying Weasel.rb.
Sample Output
The left column is the population's strongest member. The right column is the
population's weakest member.
YAPXMLHQFORHPPNVWF DISTEBZEK -- vs. -- YZLHUURLMPKITAP ZYVTTSIZZCQV (Gen: 0)
YAPXMLHQFORHPPNVWF DISTEBZEK -- vs. -- CXZWQCVCUAZFDOUSZNRCXDQPNENR (Gen: 1)
YAEBXJFACES FVUTYWTLATTIACDY -- vs. -- CXZWQCVCUAZFDOUSZNRCXDQPNENR (Gen: 2)
YAEBXJFACES FVUTYWTLATTIACDY -- vs. -- J ICBDLWUGASSNRS YENZDLVBJ V (Gen: 3)
YAEBXJFACES FVUTYWTLATTIACDY -- vs. -- NJQPSUPJUBEUQGNEBPRCMHIVBJ V (Gen: 4)
YAEBXJFACES FVUTYWTLATTIACDY -- vs. -- NJQPSUPJUBEUQGNEBPRCMHIVBJ V (Gen: 5)
YAEBXJFACES FVUTYWTLATTIACDY -- vs. -- RQPSUPJUBEUQWSFJTJ OWHRTKIM (Gen: 6)
MDLDIGLMBUJPTV LWEC W FMNINL -- vs. -- ZXZNNKDUGWIVLKARQLMDKCIZ FGW (Gen: 7)
MDLDIGLMBUJPTV LWEC W FMNINL -- vs. -- ZXZNNKDUGWIVLKARQLMDKCIZ FGW (Gen: 8)
...
MDPXKNIS MVHXZ LHCC W ODACEL -- vs. -- MDLDMFQS MVHCZ LHCCGW IQACDY (Gen: 100)
MDPXKNIS MVHXZ LHCC W ODACEL -- vs. -- MDJDIFVS M DAS HHXC W TSACEL (Gen: 101)
MDPXKNIS MVHXZ LHCC W ODACEL -- vs. -- MDJDMNQS MVGXZ LHCC W FQACDL (Gen: 102)
MADIMNQS MVDXZ LHCE W ODACEL -- vs. -- MDJDMNQS MVGXZ LHEC W FMACIL (Gen: 103)
MDPXKNQS MVDAS HHCE W ODACEL -- vs. -- MDJDMNQS MVGXZ LHEC W FMACIL (Gen: 104)
MDPXKNQS MVDAS HHCE W ODACEL -- vs. -- MAPXYNCM MVHXZ LHTC D DQACIL (Gen: 105)
...
METHINIS ITHIS LHKE A VMAQEL -- vs. -- METHINIS IVHWS LHKEXA VMAQEL (Gen: 400)
METHINIS ITHIS LHKE A VMAQEL -- vs. -- METHINIS RVHIS LHKEEA VMAQEL (Gen: 401)
METHINIS ITHIS LHKE A VMAQEL -- vs. -- METHINIS RVHIS LHKEEA VMAQEL (Gen: 402)
METHINIS ITHIS LHKE A VMACEL -- vs. -- METHINIS IVHWS LHKEXA VMAQEL (Gen: 403)
METHINIS ITHIS LHKE A VMAQEL -- vs. -- METHFNIS RVHIS LHKE A VMAQEL (Gen: 404)
METHINIS ITHIS LHKE A VMAQEL -- vs. -- METHFNIS RRHIS LHKE A VMAQEL (Gen: 405)
...
METHINIS IV IS LHKE A WMAQEL -- vs. -- MVTHINIS IV IS QHKE AMVMAQEL (Gen: 800)
METHINIS IV IS LHKE A WMAQEL -- vs. -- METUINIS IV IS LHKE AMVMEQEL (Gen: 801)
METHINIS IV IS LHKE A WMAQEL -- vs. -- METUINIS IV IS LHKE AMVMEQEL (Gen: 802)
METHINIS IV IS LHKE A WMAQEL -- vs. -- METUINIS IV IS LHKE AMVMEQEL (Gen: 803)
METHINIS IV IS LHKE A WMAQEL -- vs. -- METHINIS IVQISKLHKE A VMAQEL (Gen: 804)
METHINIS IV IS LHKE A WMAQEL -- vs. -- METCINIS IV RS LHKK A WMAQEL (Gen: 805)
...
METHINKS IT IS LIKE A WEAWEL -- vs. -- ROTHINKS IT IS LIKE A WEAHEL (Gen: 5106)
METHINKS IT IS LIKE A WEAWEL -- vs. -- MOTHINKS ITSIS VIKE A WEAWEL (Gen: 5107)
METHINKS IT IS LIKE A WEAWEL -- vs. -- MOTHINKS ITSIS VIKE A WEAWEL (Gen: 5108)
METHINKS IT IS LIKE A WEAWEL -- vs. -- METHINKSOIT IS LIKE A WEAWEL (Gen: 5109)
METHINKS IT IS LIKE A WEASEL -- vs. -- METHINKSOIT IS LIKE A WEAWEL (Gen: 5110)
Main.rb
require './Evolver.rb'
require './Weasel.rb'
evolver = Evolver.new( Weasel )
evolver.setup
i = 0
while 1
print "#{evolver.best} -- vs. -- #{evolver.worst} (Gen: #{i}) \r"
evolver.advance
i += 1
end
Evolver.rb
# WARNING: I have never even read `On the Origin of Species` so do not mistake
# this horrible code to be anything like what happens in nature.
class Evolver
attr_reader :population
attr_accessor :population_size, :mutation_probability, :breed_probability
def initialize( klass )
@entity = klass
@population = []
@population_size = 100
@mutation_probability = 0.07
@breed_probability = 0.3
end
def setup
@population_size.times do |i|
@population << @entity.random
end
end
def advance
@population.each do |subject|
if rand() <= @mutation_probability
subject.mutate
end
end
newborns = []
@population.each do |subject|
if rand() <= @breed_probability
newborns << subject.breed( @population[rand(population.length)] )
end
end
# Limit the number of newborns to at most 1/4 of the population size.
if newborns.length * 4 > @population.length
newborns.shuffle!
newborns = newborns.first( @population.length / 4 )
end
# Select a random half of the population to be candidates for dying.
killed = @population.shuffle.first( @population.length / 2 )
# Sort them by goodness value in increasing order.
killed.sort! do |a,b|
if a.goodness < b.goodness
-1
elsif b.goodness > a.goodness
1
else
0
end
end
# Replace the weakest death candidates with the newborns.
newborns.each_with_index do |subj,k|
idx = @population.index( killed[k] )
@population[idx] = subj
end
end
def best
@population.inject do |a,b|
( a.goodness > b.goodness ) ? a : b
end
end
def worst
@population.inject do |a,b|
( a.goodness > b.goodness ) ? b : a
end
end
end
Weasel.rb
class Weasel
attr_reader :str
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ "
TARGET = "METHINKS IT IS LIKE A WEASEL"
def self.random
str = ""
TARGET.length.times do |i|
str << ALPHABET[rand(ALPHABET.length)]
end
return Weasel.new( str )
end
def initialize( str )
@str = str
end
def mutate
@str[rand(@str.length)] = ALPHABET[rand(ALPHABET.length)]
end
def breed( organism )
split = rand(@str.length)
front = @str[0...split]
back = organism.str[split...organism.str.length]
return Weasel.new( front + back )
end
def goodness
hamming = 0
0.upto( @str.length - 1 ) do |i|
hamming += (@str[i].ord ^ TARGET[i].ord).to_s(2).count("1")
end
return -hamming
end
def to_s
@str
end
end