Evolutionsprogrammering för simulerat flockbeteende

Evolutionsprogrammering för simulerat
flockbeteende
Som ett verktyg inom animering.
PETTER LUNDAHL
Kandidatexamensarbete för CSC
Handledare: Vahid Mosavat
Examinator: Karl Meinke och Örjan Ekeberg
April 2014
iii
Abstract
Humans have always shown an interest in the problem solving capabilities of
nature. Two fields in computer science that strongly represents that interest is
flocking simulation and evolutionary computing. In this article the possibility
to combine these two fields to enhance the flocking simulation is explored.
The evolutionary algorithms did not invoke any new behaviors, but was very
useful in calibrating the flocking behavior, which makes them a useful tool in
animation.
iv
Sammanfattning
Människan har alltid varit intresserad av naturen, och hur naturen har löst
de problem som uppstått under vägen. Två fält där människan tydligt har blivit
inspirerad av naturen är vid flocksimuleringar och evolutionär programmering.
I den här artikeln undersöks hur man kan använda evolutionära algoritmer för
att förbättra och förenkla flocksimuleringar. De evolutionära algoritmerna gav
inte upphov till några nya beteenden för flocksimuleringarna men de förenklade
avsevärt kalibreringen för att få önskvärda flockbeteenden.
Innehåll
Innehåll
v
1 Inledning
1.1 Frågeställning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Avgränsningar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
1
1
2 Bakgrund
2.1 Boids . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2 Evolutionsprogrammering . . . . . . . . . . . . . . . . . . . . . . . .
3
3
4
3 Metod
3.1 Flocksimulering . . . . . . . . . . . . . . .
3.1.1 Grannar . . . . . . . . . . . . . . .
3.1.2 Centrering . . . . . . . . . . . . . .
3.1.3 Kollisionsundvikning . . . . . . . .
3.1.4 Hastighetsmatchning . . . . . . . .
3.1.5 Regler specifika för animering . . .
3.2 Evolutionsalgoritmer . . . . . . . . . . . .
3.2.1 Fitnessfunktion . . . . . . . . . . .
3.2.2 Variationsoperation och Urval . . .
3.3 De variabler som flocken evolverades kring
.
.
.
.
.
.
.
.
.
.
7
7
8
9
10
11
11
12
12
13
13
4 Resultat och diskussion
4.1 Resultat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2 Diskussion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15
15
18
Litteraturförteckning
19
5 Appendix
.1
Källkod för flocksimulering . . . . . . . . . . . . . . . . . . . . . . .
.2
Källkod för evolution . . . . . . . . . . . . . . . . . . . . . . . . . . .
21
v
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
21
30
Kapitel 1
Inledning
1.1
Frågeställning
Är det möjligt att förbättra Reynolds klassiska flocksimulering med hjälp av evolutionär programmering?
Där en förbättring betyder att flocken upplevs som mer naturlig. Det skulle
exempelvis kunna vara att individerna i flocken börjar interagera med varandra på
ett nytt sätt, men också att flocken som helhet upplevs som mer naturlig.
1.2
Avgränsningar
Den här rapporten berör bara Reynolds Boids[1] och enkla evolutionsalgoritmer.
Den förändring som förväntas uppstå är bara tänkt att beröra animation, inte
forskning på flockbeteende. För att förenkla applikationen sker flocksimuleringen
i 2D istället för 3D.
Eftersom jag arbetar själv på den här rapporten och eftersom vi har ont om tid
så har jag valt att inte använda mig av användarstudier, utan har istället utgått
från mina egna upplevelser av andvändbarhet och flockens utseende.
1
Kapitel 2
Bakgrund
Det är idag vanligt att man tar inspiration ifrån naturen när man försöker simulera
eller beräkna vissa problem med hjälp av datorer.[2] Två tydliga sådana inspirationer är simulerat flockbeteende och genetiska algoritmer. Simulerat flockbeteende
används ofta när stora filmscener ska animeras för att animatören ska slippa animera varje individ för sig själv. Dels för att det är extremt tidskrävande, men också
för att det lätt uppstår fel, t.ex. individer som rör sig igenom varandra eller inte reagerar dynamiskt på sin omgivning. Ett tidigt exempel är från Lejonkungen[3] 1994
där en stor hjord med gnuer inte animerades utan simulerades. Ytterligare ett exempel på storskaligheten man kan få till med hjälp av simulerat flockbeteende, som
hade varit nästintill omöjligt utan, är de stora krigsscenerna ur Sagan om ringentrilogin där individernas beteende under stora slagen simulerades.[3] Ytterligare ett
sätt att simulera naturen på är att simulera evolutionen med hjälp av genetiska
algoritmer, man vill då använda sig av evolutionens metodik med reproduktion,
mutation och survival of the fittest för att få fram så optimala parametrar som
möjligt för sitt program.[2] Att kombinera dessa två sätt att efterlikna naturen för
att få flocksimuleringen att upplevas mer naturlig fokuset för det här arbetet.
2.1
Boids
Boids är ett program som Craig Reynolds beskrev i artikeln Flocks, Herds, and
Schools: A Distributed Behavioral Model för SIGGRAPH 1987[1], där han beskriver
ett sätt att med hjälp av några enkla regler för ett partikelsystem simulera flockbeteende. Jag kommer fortsättningsvis i texten att kalla de simulerade aktörer/partiklarna som ingår i flocksimuleringar för boids. Programmet Reynolds utvecklade
försöker i så stor utsträckning som möjligt efterlikna naturen där varje flockmedlem,
som i naturen till exempel är fågel eller fisk, har en egen intelligens och tar beslut
ut efter vad den ser och upplever. Reynolds observerade att beteendet hos de individer som ingår i en flock kan beskrivas med tre enkla regler[4], undvika kollisioner,
matcha grannars hastighet och försöka centrera flocken. För att räkna ut hastighet
och riktning för varje boid så adderas helt enkelt de bidragande faktorerna från
3
4
KAPITEL 2. BAKGRUND
varje regel till dess nuvarande hastighets- och riktningsvektor. Reglerna är rätt
självklara, kollisioner kan enkelt undvikas genom att varje boid tar hänsyn till var
dess grannar befinner sig och undviker att röra sig mot dem, hastighetsmatchning
kan göras genom att en boid tar genomsnittet av dess grannars hastighetsvektorer
och försöker matcha sin hastighetsvektor mot den och centrering av flocken görs
genom att alla boids rör sig mot mitten av flocken.
En ytterligare observation som Reynolds gjorde var att det i naturen inte verkar
finnas någon övre gräns på hur stora och komplexa flockar kan bli.[1] Till exempel
har sillstim på över miljoner individer observerats, och de beter sig fortfarande på
samma sätt som mindre stim. Det tyder på att individerna inte har någon uppfattning om flocken utan bara om sina närmsta grannar i flocken, annars skulle
beräkningskapacitet som krävdes av varje individ öka i och med att flocken växte.[1]
För att efterlikna det här beteendet gav Reynolds varje boid ett synfält och lät den
bara påverkas av de grannar som låg inom synfältet. Att varje boid bara påverkas
av dess närmsta grannar ger också upphov till ett väldigt naturligt beteende när till
exempel statiska hinder introduceras, det gör det möjligt för flocken att delas upp i
flera mindre flockar som inte påverkar varandra förrän de kommer tillräckligt nära
för att se varandra och då smälta samman. Ett beteende som också observerats i
naturen.
2.2
Evolutionsprogrammering
Evolutionsprogrammering är en gren av datalogi som försöker utnyttja Darwin evolutionsteori för att lösa diverse tekniska och datalogiska problem. Detta görs med
en enkel modell av evolution som baseras på naturligt urval. De sex viktigaste
delarna för en evolutionär algoritm är enligt boken Introduktion to Evolutionary
Computing[2] av Eiben och Smith, representation, fitnessfunktion, population, föräldraurvals mekanik, variationsoperatorer och urvalsprocess.
Representation står för hur man väljer att beskriva de problemlösningsinstanser
som man sedan ska utföra det naturliga urvalet på. Inom evolutionsprogrammering
finns det två vanliga datastrukturer att representera dessa objekt på, antingen som
listor eller som träd. Valet av datastruktur leder till olika algoritmer för framförallt
parning, mutation, etc.
Fitnessfunktionen är den algoritm som beräknar hur välanpassade en viss individ, dvs lösningsinstans, är till att lösa problemet man arbetar med. Detta görs
genom att man ställer upp diverse regler och tester för att få ett fitnessvärde för
den givna individen.[2]
Populationens roll är att hålla reda på alla individer som just nu finns tillgängliga för vidare evolution. Det är inte individerna som ändras vid evolutionen utan
vilka individer som ingår i den nuvarande populationen. Det är från populationen
man väljer vilka individer som ska användas vid till exempel parning. Ofta är populationen väldigt enkel att implementera, en lista av de individer som ingår är ofta
fullt tillräckligt.[2]
2.2. EVOLUTIONSPROGRAMMERING
5
Urvalsmekaniken som används för att välja ut de individer som får bygga upp
nästa generation är i nästan alla fall probabilistisk när det kommer till evolutionsprogrammering. Vilket betyder att de individer som har ett högt fitnessvärde är de
individer som med störst sannolikhet kommer att användas som fäder för att skapa
nästa generation, men alla individer har en nollskild sannolikhet att bli fäder. Anledningen till att man låter alla individer ha en chans att bli fäder och inte bara de som
just nu är bäst lämpade är för att man då riskerar att fastna i lokala maximum.[2]
Variationsoperatorer är de algoritmer som används för att givet någon/några
fäder skapa en ny individ. Dessa algoritmer delas in i två grupper, mutation och
rekombination. Mutation tar en endast en fader och skapar en ny individ genom att
slumpmässigt ändra på några eller alla av dess värden. Rekombination är det som i
djur-och växtvärden kallas fortplantning, då väljer man ut två (eller flera) individer
och skapar en ny individ genom att kombinera ihop deras olika värden.[2]
Urvalsprocessen är den process som väljer ut vilka individer som ska överleva till
nästa generation, den är ofta väldigt lik urvalsmekaniken, man rankar individerna
efter deras fitnessvärde, men är oftast deterministisk och inte stokastisk. Man kan
till exempel välja ut den övre halvan av de individer som finns tillgängliga efter
variationssteget.[2]
Reglerna för att initialisera och avsluta en evolutionär algoritm hålls ofta enkla[2],
den första populationen kan till exempel skapas genom att man helt enkelt slumpar
fram alla individer. Och den evolutionära algoritmen kan avslutat efter ett visst
antal generationer, eller då ett visst fitnessvärde har uppnåtts.
Kapitel 3
Metod
3.1
Flocksimulering
För att göra min flocksimulering så enkel och ren som möjligt la jag mig väldigt
nära Reynolds implementering men i 2D istället för i 3D. Populationen bestod av
en lista av boids (så som de är beskrivna i avsnitt 2.1). Varje boid är ett objekt
som har en implementation av varje av de tre reglerna som Reynolds ställde upp,
en position i rummet, en hastighetsvektor och ett synfält.
7
8
3.1.1
KAPITEL 3. METOD
Grannar
Grannar till en boid definieras som de individer som ligger inom boidens synfält.
Synfältet approximeras av en sfär eller i 2 dimensioner en cirkel, så allt som ligger
innanför sfären ligger i boidens synfält. För att få ett mer realistiskt och bättre
resultat framförallt när det gäller statiska objekt, till exempel ifall en flock delas
upp av en vägg, kan man implementera mer avancerade algoritmer för beräkning
av synfältet. Man skulle till exempel kunna använda sig av en enkel ray tracingalgoritm, men för syftet med den här rapporten anses det fullt tillräckligt med
en naiv algoritm för synfältet. Jag valde att implementera synfältet som en enkel
cirkel centrerad kring boiden. I figur 3.1 ser vi hur den förenklade modellen för
boidens synfält fungerar. Alla boids som är innom den aktuella individens synfält,
dvs innanför cirkeln, är grannar till den aktuella individen. I det här fallet är det
de mellangrå boidsen som är grannar till den mörkgrå.
Figur 3.1. Grannar
3.1. FLOCKSIMULERING
3.1.2
9
Centrering
För att centrera flocken så använder sig varje individ sig av ett upplevt flockcentrum.
Metoden för att beräkna det upplevda flockcentra beräknas som medelvärdet av
alla grannars position, vilket ger en punkt i rymden. För att beräkna riktningen
individen skall röra sig åt för att centrera flocken används riktningen för vektorn som
bildas mellan individens nuvarande position och dess upplevda centrum. Längden på
kraftvektorn som används för att beräkna hur boiden skall förflytta sig är en fraktion
av längden till det upplevda flockcentra. I grundutförandet valde jag att den skulle
vara en tiondel av vektorns längd. Längden på kraftvektorn måste vara mindre än
längden till centrum för att individen inte ska överkompensera i sin förflyttning.
Att kraftvektorn är relaterad till avståndet och riktningen från det upplevda centra
leder till att individer som ligger på randen av flocken dras mot mitten av flocken
medan individer som redan befinner sig inne i flocken inte påverkas lika kraftigt
eller alls av centreringen. I figur 3.2 beskriver de grå pilarna varje grannes inverkan
på det upplevda flockcentrat och den svarta pilen är vektorn mellan den aktuella
individen och dess upplevda flockcentrum. Den aktuella individen kommer alltså
att påverkas av en kraftvektor som är en tiondel av den svarta pilen.
Figur 3.2. Centrering
10
3.1.3
KAPITEL 3. METOD
Kollisionsundvikning
På samma sätt som för centrering tar jag för varje individ fram dess grannar, för
varje granne beräknas sedan en kraftvektor med riktning direkt motsatt riktningen från den aktuella individen till den aktuella grannen. Storleken på kraftvektorn
växer när avståndet krymper mellan individen och grannen, det vill säga de närmsta grannarna har störst inverkan på individen. Medelvärdet av alla kraftvektorer
som bildas mellan individen och grannarna är det som sedan påverkar hur individen kommer att förflytta sig. För att ytterligare undvika kollisioner så varierar
kraftvektorn omvänt proportionellt med avståndet, vilket betyder att kraftvektorn
växer snabbare ju mindre avståndet är. I figur 3.3 beskriver de grå pilarna varje
grannes inverkan på kraftvektorn, och den svarta pilen är den slutgiltiga kraftvektorn som påverkar den aktuella individen.
Figur 3.3. Kollisionsundvikning
3.1. FLOCKSIMULERING
3.1.4
11
Hastighetsmatchning
På samma sätt som för centrering skapas en upplevd flockhastighetsvektor för varje
individ genom att jag går igenom alla grannars hastighetsvektorer och med hjälp
av medelvärdet skapar en upplevd flockhastighet. Skillnaden mellan individens egen
hastighet och flockens hastighet blir kraftvektorn som returneras för att styra individens hastighet. Hastighetsmatchning leder både till flocksammanhållning och till
mindre kollisioner inom flocken, eftersom ifall alla individer har samma hastighet
och riktning så splittras inte flocken och det att sannolikheten för kollisioner minskar. I figur 3.4 ser vi varje individs hastighet som ljusgrå pilar, den mörkgrå linjen
är den upplevda flockhastigheten och den svarta pilen är kraftvektorn som påverkar
den aktuella individen.
Figur 3.4. Hastighetsmatchning
3.1.5
Regler specifika för animering
Eftersom man vid animering ofta vill styra flockens rörelse kan man lägga till ytterligare regler som inte förändrar flockdynamiken men som styr flocken i sin helhet.
Det kan till exempel göras genom att man lägger till en global kraftvektor som
påverkar alla individer lika mycket, man kan se det som en ström eller stark vind,
så flocken dras åt ett håll. Ifall man vill att den simulerade flocken skall hålla sig
inom något område kan man lägga till en regel som ger varje individ som börjar
närma sig kanten till det området eller ligger utanför området en kraft mot mitten
12
KAPITEL 3. METOD
av området. Det kommer leda till att när tillräckligt många individer ur flocken har
hamnat utanför gränsen kommer det finnas en nettokraft på hela flocken som styr
flocken tillbaka in mot området igen. Den typen av svaga randvillkor leder till ett
mjukt och realistiskt beteende hos flocken. Man skulle också kunna tänka sig att
kombinera dessa två och ha en kraftvektor som varierar i rummet, på så sätt kan
man skapa en bana flocken följer, men utan att man behöver styra varje individs
beteende. I min implementation har jag lagt till regeln som håller boidsen innom
ett visst område, som i mitt fall är lika stort som skärmen de visas på.
3.2
Evolutionsalgoritmer
För att evolvera gruppen av flockinstanser så körs en Boidsimulering för varje individ i populationen, efter det beräknas fitnessvärdet för varje individ och de som väljs
ut i urvalsprocessen förökar sig för att bygga upp nästa generation. Fitnessfunktionen och urvalet kommer gås i genom i detalj senare i det här kapitlet. Oservera att
en individ i det här fallet är en hel flocksimulering, det som menas med en individ
är alltså en hel flock, inte en individuell boid. I texten skiljs dessa åt genom att
benämnas som individ respektive boid. För att simuleringen skall vara rättvis men
samtidigt ge upphov till olika instanser av flockbeteende så ges varje Boid en slumpmässig position innan simuleringen körs, sedan körs simuleringen i tusen steg och
resultatet bedöms av fitnessfunktionen. Populationen består av 100 individer, som
vid simuleringens start initieras med slumpmässiga värden. För att dessa värden
skall vara relevanta är de normerade kring de värden den klassiska boidsimuleringen har. Det vill säga i det fallet där styrkan av de olika reglerna skall bestämmas så
får de ett slumpmässigt värde som ligger i området 1.00 ± 1. Evolutionen körs i tio
generationer, och den individ med bästa fitnessvärde efter den tionde generationen
returneras som lösning till instansen. Anledningen till att tio generationer valdes
var för att populationens evolution avstannade oftast innan tio generationer hade
körts.
3.2.1
Fitnessfunktion
Fitnessfunktionen som används är fokuserad på tre olika områden, för det första
så ger krockar upphov till ett lägre fitnessvärde, för det andra belönas instanser
för varje individ som syns på skärmen och för det tredje belönas instanser som
har lagom stora flockar där vad som är lagom är upp till animatören. En krock
definieras som att ifall en boid är närmre en något epsilon till en annan boid så har
de krockat, här kan man som animatör både styra hur mycket en krock skall straffas
och värdet på epsilon. Skärmen definieras som beskrivet i avsnittet Regler specifika
för animering, här har animatören möjlighet att styra hur stor skärmen skall vara
och hur mycket en instans skall belönas för varje boid som syns på skärmen. För att
animatören skall få möjlighet att styra hur stora flockar som skall premieras så ges
det möjlighet att styra hur många grannar varje boid skall ha för att ha en positiv
inverkan på fitnessvärdet. Det ges möjlighet att styra både en undre och en övre
3.3. DE VARIABLER SOM FLOCKEN EVOLVERADES KRING
13
gräns för hur många grannar en boid skall ha, och även hur stort avståndet skall
vara för att de skall räknas som grannar. Självklart går det även att styra hur stor
inverkan detta skall ha på fitnessvärdet.
Det totala fitnessvärdet för en flockinstans är de ingående boidsens sammanlagda
fitnessvärde.
3.2.2
Variationsoperation och Urval
För att ta fram nya individer till urvalet används tre olika metoder, den första
är kombination av individer från nuvarande population, den andra är mutationer
hos individer i den nuvarande populationen och den tredje är introducering av helt
nya individer. För att kombinera individer, det som i naturen kallas fortplantning,
tar man två individer slumpmässigt från den nuvarande populationen och kombinerar dessa genom att för varje egenskap de har slumpmässigt välja en av de två
värdena som finns för varje egenskap. För att mutera nuvarande individer så görs
samma sak som för att kombinera individer, men istället för att välja mellan två
individers egenskaper så är urvalet antingen den nuvarande individens värde eller
ett slumpvärde. Introducering av nya individer görs genom att ge dem slumpvärden
för alla egenskaper. Vilka individer som väljs ut för att muteras eller paras ihop är
slumpmässigt, sannolikheten att bli vald till reproduktion är normalfördelat kring
den individ med högst fitnessvärde.
När alla nya individer har tagits fram så kombineras de med den nuvarande
populationens bästa 10%, för att populationen inte skall växa med varje generation
så är det bara de med högst fitnessvärde som sparas, och antalet individer i populationen hålls konstant. Det som avses med individer i populationen är olika instanser
av flocksimuleringar, alltså inte antalet boids i en flocksimulering. Antalet boids i
varje flocksimulering varierar när boids dör på grund av krockar.
3.3
De variabler som flocken evolverades kring
I figur 3.5 kan ni se de värden som förändrades under evolutionen, de värden ni
ser till höger är de värden som användes för flocksimuleringen utan evolution.
viewRange är den variabel som styr hur stor cirkeln som beskriver synfältet är.
personalSpace är också radien för en cirkel, i det här fallet är det bara de grannar
som är innanför den cirkeln som påverkar kollisionsundvikningsbeteendet. change
är en variabel för att styra hur snabbt en boid ändrar sin hastighetsvektor och ownWill är en variable för att styra hur benägen en boid är att matcha sin hastighet
med dess grannars hastighet. gather, avoid och match hör till reglerna centrering,
kollisionsundvikning och hastighetsmatchning, där värdet styr hur stor påverkan de
olika reglerna har på boiden. exit är en variabel för regeln att boidsen skall stanna
innom ett visst område.
14
KAPITEL 3. METOD
viewRange
personalSpace
change
ownWill
gather
avoid
match
exit
100
50
100
10
1
1
1
1
Figur 3.5. Variabler för evolution
Kapitel 4
Resultat och diskussion
4.1
Resultat
Efter tio generationers evolution nåddes i nästan varje försök ett stabilt optimum,
vilket betyder att sannolikheten att fler evolutionssteg skulle ge ett bättre resultat
är väldigt låg. Det gick inte att se något nytt beteende hos flocken, det var inte heller
något av de tidigare beteendena som försvann. Trots att inga nya beteenden introducerades så rörde sig flocken mer naturligt och man kunde se en klar förbättring i
kvalitén på animationen. I figur 4.1 kan vi se hur flocken såg ut med standardvärdena som kan ses i figur 4.2. Vi kan se att flocken ser väldigt organiserad ut, alla
avstånd mellan boids är nästan identiska och riktningarna innom delflockarna är
också väldigt uniforma. I figur 4.3 som är en bild av hur flocken såg ut efter tio generationers evolution uppvisar flocken en större variation i både avstånd och riktning
innom delflockarna. Så trots att det inte finns några nya beteenden i flocken efter
evolutionen så anser jag att flocken ser naturligare ut efter evolutionen. De värden
som anvädes för figur 4.3 kan ses i 4.4, man kan se att de har ett lägre värde för
personalSpace än standard, vilket låter dem komma närmre varandra. Båda värdena för avoid och gather är nere på en fjärdedel av standard och match är större
än standard. Vilket leder till att de är mindre benägna att sammlas i flockar, och
mindre benägna att undvika andra boids, men att de är mycket benägna att matcha
sin hastighet med grannarnas hastighet. De har också ett nästan dubbelt så stort
värde på viewRange, vilket gör att de myckte tidigare kommer se andra boids och
dras mot dem.
15
16
KAPITEL 4. RESULTAT OCH DISKUSSION
Figur 4.1. Flockbeteende innan evolution
public double viewRange
public double personalSpace
public double ownWill
public double gather
public double avoid
public double match
public double exit
public double change
100
50
10
1
1
1
1
100
Figur 4.2. Variabler innan evolution
4.1. RESULTAT
17
Figur 4.3. Flockbeteende efter evolution
public double viewRange
public double personalSpace
public double ownWill
public double gather
public double avoid
public double match
public double exit
public double change
182.34784168267228
29.661291668942212
17.511015855979846
0.26091639900431907
0.2530519661978641
1.4020290081905358
0.7335319605669874
196.43149293116718
Figur 4.4. Variabler efter evolution
18
4.2
KAPITEL 4. RESULTAT OCH DISKUSSION
Diskussion
Jag tycker att man kan se en förbättring i utseendet från figur 4.1 till figur 4.3,
som jag beskrev det i resultatdelen ser figur 4.1 mer ordnat och mindre naturlig
ut, medan figur 4.3 ser mer naturlig ut. När man tittar på siffrorna i figur 4.2 och
4.4 så är det ganska svårt att gissa sig till hur de båda olika flockarna skulle bete
sig. Man skulle till exempel kunna tro att flocken i figur 4.3 inte skulle samlas så
tydligt eftersom värdet på variabeln gather, som styr centreringen, är mycket lägre
än för figur 4.1. Så jag anser att det är svårt att bara genom att välja värden till
flocksimuleringen få ett önskat beteende.
Anledningen till att inga nya beteenden introducerades är antagligen eftersom
det inte sker någon förändring i algoritmen för flockbeteende, det är endast skalära
värden som ändras. För att få ett ändrat flockbeteende tror jag att man förutom
att evolvera fram hur stor påverkan de olika reglerna för flockbeteende skall ha
i körningen, så behöver man också evolvera med avseende på hur reglerna skall
implementeras. Ett exempel på det skulle kunna vara att man evolverar vilken
typ av avstånd som skall användas i de olika reglerna, ger till exempel kvadratisk
avståndsbedömning ett bättre resultat än linjär, osv. Man kan också tänka sig att
istället för att som nu bara evolvera hur stor radien för synfältet skall vara så kan
man tänka sig att man låter synfältets form variera.
Den slutsats jag anser att man kan komma till är att den här typen av enkla
evolutionsalgoritmer inte kan ge något förändrat eller förbättrat resultat, men att
de fortfarande kan användas som ett kraftfullt verktyg för att styra utseendet på
en simulerad flock. Till exempel inom animation, där man vill använda sig av flocksimuleringar, kan det vara enklare att istället för att försöka styra simuleringen
genom att gå in och manuellt ändra de olika styrkorna för reglerna för flocksimulering så kan man istället beskriva det önskvärda beteendet och evolutionera fram
styrkorna. Anledningen till att jag anser att det är enklare att beskriva det önskvärda beteendet istället för att försöka ställa in det manuellt är att trots att reglerna
är väldigt enkla så samverkar de på ett relativt komplicerat sätt. Det är inte direkt uppenbart vad man ska göra för att få en tätare flock, för det första försöket
att stärka regeln för flocksammanhållning kan leda till icke önskvärda beteenden,
till exempel krockar eller underliga beteenden då flocken förväntas dela upp sig på
grund av statiska hinder. Så genom att istället använda sig av evolutionsalgoritmer
blir det enklare att formulera vilken typ av beteende man vill förstärka, men utan
att förändra andra beteenden som anses positiva.
Litteraturförteckning
[1] Craig W Reynolds. Flocks, herds and schools: A distributed behavioral model.
In ACM SIGGRAPH Computer Graphics, volume 21, pages 25–34. ACM, 1987.
[2] J.E. Smith A.E. Eiben. Introduction to Evolutionary Computing. Springer, 2003.
[3] Marcus Beausang Martin Nygren. Flockbeteende och animering. Technical
report, Kungliga Tekniska Högskolan, 2004.
[4] Johanna Neander. Animerat flockbeteende. Technical report, Linköpings universitet, 2009.
19
Kapitel 5
Appendix
.1
Källkod för flocksimulering
//MainClass.java
package flockSim;
import javax.swing.*;
public class MainClass{
public static void main(String[] args)
{
JFrame f = new JFrame("Title");
f.setResizable(false);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Display d = new Display();
f.add(d);
f.setSize(Settings.WIDTH,Settings.HEIGHT);
f.setVisible(true);
Boid[] myBoids = new Boid[100];
d.myBoids = myBoids;
Stats s = new Stats();
s.print();
for(int i = 0; i < myBoids.length; i++)
{
myBoids[i] = new Boid(
(int)(Math.random()*Settings.WIDTH),
(int)(Math.random()*Settings.HEIGHT),
Math.random()*Math.PI*2,
s);
}
while(true)
{
21
22
KAPITEL 5. APPENDIX
for(Boid b : d.myBoids)
{
b.move(myBoids);
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
//Boid.java
package flockSim;
import java.awt.Color;
public class Boid {
public int X;
public int Y;
public double rotation;
public double[] speed = {0,0};
public Color color;
public Stats stats;
public Boid(int x, int y, double rot, Stats s)
{
X = x;
Y = y;
rotation = rot;
stats = s;
double tmp = Math.random()*3;
double radianer = rotation;
speed[0] = Math.cos(radianer) * tmp;
speed[1] = Math.sin(radianer) * tmp;
color = new
Color((float)Math.random(),(float)Math.random(),(float)Math.random());
}
private void scalarMul(double[] in , double val)
{
in[0] *= val;
in[1] *= val;
return;
}
.1.
KÄLLKOD FÖR FLOCKSIMULERING
public void move(Boid[]
{
double[] vector1
double[] vector2
double[] vector3
double[] vector4
myBoids)
=
=
=
=
{0,0};
{0,0};
{0,0};
{0,0};
vector1 = ruleGather (myBoids);
vector2 = ruleAvoid (myBoids);
vector3 = ruleMatch (myBoids);
vector4 = dontExit();
scalarMul(vector1, stats.gather);
scalarMul(vector2, stats.avoid);
scalarMul(vector3, stats.match);
scalarMul(vector4, stats.exit);
double[] change = new double[2];
change[0] = vector1[0] + vector2[0] + vector3[0] +
vector4[0];
change[1] = vector1[1] + vector2[1] + vector3[1] +
vector4[1];
if(delta(change)>stats.change)
{
double diff = delta(change)/stats.change;
change[0] = change[0]/diff;
change[1] = change[1]/diff;
}
speed[0] += change[0];
speed[1] += change[1];
if(delta(speed)>5)
{
double diff = delta(speed)/5;
speed[0] = speed[0]/diff;
speed[1] = speed[1]/diff;
}
X = X + (int)(speed[0]);
Y = Y + (int)(speed[1]);
rotation = Math.atan2(speed[1], speed[0]);
}
public double[] ruleGather(Boid[] myBoids)
{
double[] result = {0,0};
double[] center = {0,0};
int neighbours = 0;
23
24
KAPITEL 5. APPENDIX
for(Boid b : myBoids)
{
double[] diff = {X-b.X , Y-b.Y};
if(delta(diff)<stats.viewRange)
{
center[0] += b.X;
center[1] += b.Y;
neighbours++;
}
}
center[0] = center[0]/neighbours;
center[1] = center[1]/neighbours;
result[0] = (center[0] - this.X)/100;
result[1] = (center[1] - this.Y)/100;
return result;
}
public double[] ruleAvoid(Boid[] myBoids)
{
double[] result = {0,0};
int neighbours = 0;
for(int i = 0; i < myBoids.length; i++)
{
if(myBoids[i] != this)
{
double[] tmp = new double[2];
tmp[0] = myBoids[i].X - this.X;
tmp[1] = myBoids[i].Y - this.Y;
if(delta(tmp)<stats.viewRange)
if(delta(tmp)<stats.personalSpace)
{
if(tmp[0] != 0)
result[0] = result[0] stats.personalSpace/tmp[0];
if(tmp[1] != 0)
result[1] = result[1] stats.personalSpace/tmp[1];
neighbours++;
}
}
}
if(neighbours>0)
{
result[0] = result[0]/neighbours;
result[1] = result[1]/neighbours;
.1.
KÄLLKOD FÖR FLOCKSIMULERING
}
return result;
}
public double[] ruleMatch(Boid[] myBoids)
{
double[] result = {0,0};
int neighbours = 0;
for(Boid b : myBoids)
{
double[] diff = {X-b.X , Y-b.Y};
if(this != b && delta(diff)<stats.viewRange)
{
result[0] += b.speed[0];
result[1] += b.speed[1];
neighbours++;
}
}
if(neighbours == 0)
return result;
result[0] = result[0]/neighbours;
result[1] = result[1]/neighbours;
result[0] = (result[0] - this.speed[0])/stats.ownWill;
result[1] = (result[1] - this.speed[1])/stats.ownWill;
return result;
}
public double[] dontExit()
{
double[] result = {0,0};
if(X < 0)
result[0] = -X;
else if(X > Settings.WIDTH+10)
result[0] = Settings.WIDTH-X;
if(Y < 0)
result[1] = -Y;
else if(Y > Settings.HEIGHT+10)
result[1] = Settings.HEIGHT-Y;
//System.out.println("ruleEXIT: x" + result[0] + " y"
+result[1]);
return result;
}
private double delta(double[] vect)
25
26
KAPITEL 5. APPENDIX
{
return Math.sqrt(vect[0]*vect[0] + vect[1]*vect[1]);
}
}
//Stats.java
package flockSim;
public class Stats {
public
public
public
public
public
public
public
public
double
double
double
double
double
double
double
double
viewRange = 100;
personalSpace = 50;
ownWill = 10;
gather = 1;
avoid = 1;
match = 1;
exit = 1;
change = 100;
/*public double viewRange = 182.34784168267228;
public double personalSpace = 29.661291668942212;
public double ownWill = 17.511015855979846;
public double gather = 0.26091639900431907;
public double avoid = 0.2530519661978641;
public double match = 1.4020290081905358;
public double exit = 0.7335319605669874;
public double change = 196.43149293116718;
*/
public void random()
{
viewRange = 2*Math.random()*viewRange;
personalSpace = 2*Math.random()*personalSpace;
ownWill = 2*Math.random()*ownWill;
gather = 2*Math.random()*gather;
avoid = 2*Math.random()*avoid;
match = 2*Math.random()*match;
exit = 2*Math.random()*exit;
change = 2*Math.random()*change;
}
public void print()
{
System.out.println("viewRange:" + viewRange);
System.out.println("personalSpace:" + personalSpace);
System.out.println("ownWill:" + ownWill);
.1.
KÄLLKOD FÖR FLOCKSIMULERING
System.out.println("gather:" + gather);
System.out.println("avoid:" + avoid);
System.out.println("match:" + match);
System.out.println("exit:" + exit);
System.out.println("change:" + change);
}
}
//Display.java
package flockSim;
import java.awt.*;
import javax.swing.*;
public class Display extends JPanel {
Color c = Color.BLACK;
Boid[] myBoids;
public Display()
{
this.setBackground(c);
}
public void paintComponent(Graphics g)
{
super.paintComponent(g);
paintBoids(g);
}
public void paintBoids(Graphics g)
{
//super.paintComponent(g);
//repaint();
g.setColor(Color.CYAN);
for(Boid b : myBoids)
{
g.setColor(Color.WHITE);
Shape p = new Shape(b);
g.drawPolygon(p);
//g.drawOval(b.X-5, b.Y-5, 10, 10);
}
repaint();
}
27
28
KAPITEL 5. APPENDIX
/*
* Generated automaticly
*/
private static final long serialVersionUID = 759338174544585169L;
}
//Shape.java
package flockSim;
import java.awt.Polygon;
public class Shape extends Polygon{
double length = 15;
double middleDist = Math.sqrt(length*length (length/2)*(length/2)) / 2;
public Shape(int x, int y, double rotation)
{
double radianer = rotation;
double cosAngle = Math.cos(radianer);
double sinAngle = Math.sin(radianer);
//Right point
int ptX = x + (int)(middleDist*cosAngle);
int ptY = y + (int)(middleDist*sinAngle);
this.addPoint(ptX, ptY);
//Bottom point
ptX = x + (int)(-middleDist*cosAngle - (length/2)*sinAngle
ptY = y + (int)(-middleDist*sinAngle + (length/2)*cosAngle
this.addPoint(ptX, ptY);
//Middle point
ptX = x - (int)((middleDist/2)*cosAngle);
ptY = y - (int)((middleDist/2)*sinAngle);
this.addPoint(ptX, ptY);
//Top point
ptX = x + (int)(-middleDist*cosAngle + (length/2)*sinAngle
ptY = y + (int)(-middleDist*sinAngle - (length/2)*cosAngle
this.addPoint(ptX, ptY);
}
public Shape(Boid b)
{
this(b.X,b.Y,b.rotation);
}
private static final long serialVersionUID = 5429989775653640585L;
}
);
);
);
);
.1.
KÄLLKOD FÖR FLOCKSIMULERING
//Settings.java
package flockSim;
public class Settings {
public static int WIDTH = 1200;
public static int HEIGHT = 800;
}
29
30
KAPITEL 5. APPENDIX
.2
Källkod för evolution
//MainClass.java
//For evolution computing
package flockSim;
import java.util.Arrays;
public class MainClass{
public static void main(String[] args)
{
Boid[] myBoids = new Boid[100];
Stats s = new Stats();
for(int i = 0; i < myBoids.length; i++)
{
myBoids[i] = new Boid(
(int)(Math.random()*Settings.WIDTH),
(int)(Math.random()*Settings.HEIGHT),
Math.random()*Math.PI*2,
s);
}
Stats[] population = new Stats[100];
for(int i = 0; i < population.length; i++)
{
population[i] = new Stats();
population[i].random();
}
for(Stats stat : population)
fitness(stat, myBoids);
Arrays.sort(population);
population[0].print();
for(int i = 0; i < 10; i++)
{
System.out.println("Generation:" + i);
System.out.println(population[0].fitness);
Stats[] offspring = mate(population);
for(Stats stat : offspring)
fitness(stat, myBoids);
Arrays.sort(offspring);
Stats[] all = new Stats[110];
for(int j = 0; j < 100; j++)
all[j] = offspring[j];
for(int j = 0; j < 10; j++)
all[j+100] = population[j];
.2.
KÄLLKOD FÖR EVOLUTION
31
Arrays.sort(all);
population = Arrays.copyOfRange(all, 0, 100);
}
for(Stats stat : population)
System.out.println(stat.fitness);
population[0].print();
}
public static int roll()
{
int res = 0;
res =
(int)((Math.random()+Math.random()+Math.random()+Math.random()+Math.random()+
if(res > 100)
return res-101;
return 100-res;
}
public static Stats[] mate(Stats[] parents)
{
Stats[] offspring = new Stats[100];
//Mating
for(int i = 0; i < 5; i++)
{
double[] mom = parents[0].toArray();
double[] dad = parents[1].toArray();
for(int j = 0; j < mom.length; j++)
{
if(Math.random()>0.5)
mom[j] = dad[j];
}
Stats s = new Stats(mom);
offspring[i] = s;
}
for(int i = 5; i < 50; i++)
{
double[] mom = parents[roll()].toArray();
double[] dad = parents[roll()].toArray();
for(int j = 0; j < mom.length; j++)
{
if(Math.random()>0.5)
mom[j] = dad[j];
}
Stats s = new Stats(mom);
offspring[i] = s;
}
32
KAPITEL 5. APPENDIX
//Mutations
for(int i = 0; i < 5; i++)
{
double[] mom = parents[i/2].toArray();
for(int j = 0; j < mom.length; j++)
{
if(Math.random()>0.5)
mom[j] = mom[j]*2*Math.random();
if(Math.random()>0.7)
mom[j] += 100*Math.random();
}
Stats s = new Stats(mom);
offspring[50+i] = s;
}
for(int i = 5; i < 25; i++)
{
double[] mom = parents[roll()].toArray();
for(int j = 0; j < mom.length; j++)
{
if(Math.random()>0.5)
mom[j] = mom[j]*2*Math.random();
}
Stats s = new Stats(mom);
offspring[50+i] = s;
}
//Totally random
for(int i = 0; i < 25; i++)
{
Stats s = new Stats();
s.random();
offspring[75+i] = s;
}
return offspring;
}
public static void fitness(Stats s, Boid[] myBoids)
{
double score = 0;
for(Boid b : myBoids)
{
b.alive = true;
b.stats = s;
b.X = (int)(Math.random()*Settings.WIDTH);
b.Y = (int)(Math.random()*Settings.HEIGHT);
.2.
KÄLLKOD FÖR EVOLUTION
b.rotation
double tmp
b.speed[0]
b.speed[1]
33
=
=
=
=
Math.random()*Math.PI*2;
Math.random()*3;
Math.cos(b.rotation) * tmp;
Math.sin(b.rotation) * tmp;
}
for(int i = 0; i < 100; i++)
{
for(Boid b : myBoids)
{
if(b.alive)
{
b.move(myBoids);
if(b.death(myBoids))
{
b.X = -10000;
b.Y = -10000;
score -= 100;
}
}
}
}
for(Boid b : myBoids)
{
int group = 0;
if(b.alive)
{
score += 0.01*b.delta(b.speed);
if(b.X<Settings.WIDTH && b.X>0)
score += 0.25;
if(b.Y<Settings.HEIGHT && b.Y>0)
score += 0.25;
for(Boid b2 : myBoids)
{
if(b2.alive)
{
if(b.delta(b, b2)<100)
group++;
}
}
if(group>20)
score -= group*0.01;
else
score += group*0.1;
}
}
s.fitness = score;
}
34
KAPITEL 5. APPENDIX
}
//Boid.java
package flockSim;
import java.awt.Color;
public class Boid {
public boolean alive = true;
public int X;
public int Y;
public double rotation;
public double[] speed = {0,0};
public Color color;
public Stats stats;
public Boid(int x, int y, double rot, Stats s)
{
X = x;
Y = y;
rotation = rot;
stats = s;
double tmp = Math.random()*3;
double radianer = rotation;//(rotation/180)*Math.PI;
speed[0] = Math.cos(radianer) * tmp;
speed[1] = Math.sin(radianer) * tmp;
color = new
Color((float)Math.random(),(float)Math.random(),(float)Math.random());
}
private void scalarMul(double[] in , double val)
{
in[0] *= val;
in[1] *= val;
return;
}
public void move(Boid[]
{
double[] vector1
double[] vector2
double[] vector3
double[] vector4
vector1
vector2
vector3
vector4
=
=
=
=
myBoids)
=
=
=
=
{0,0};//new double[2];
{0,0};//new double[2];
{0,0};//new double[2];
{0,0};
ruleGather (myBoids);
ruleAvoid (myBoids);
ruleMatch (myBoids);
dontExit();
.2.
KÄLLKOD FÖR EVOLUTION
scalarMul(vector1,
scalarMul(vector2,
scalarMul(vector3,
scalarMul(vector4,
35
stats.gather);
stats.avoid);
stats.match);
stats.exit);
double[] change = new double[2];
change[0] = vector1[0] + vector2[0] + vector3[0] +
vector4[0];
change[1] = vector1[1] + vector2[1] + vector3[1] +
vector4[1];
if(delta(change)>stats.change)
{
double diff = delta(change)/stats.change;
change[0] = change[0]/diff;
change[1] = change[1]/diff;
}
speed[0] += change[0];
speed[1] += change[1];
if(delta(speed)>5)
{
double diff = delta(speed)/5;
speed[0] = speed[0]/diff;
speed[1] = speed[1]/diff;
}
X = X + (int)(speed[0]);
Y = Y + (int)(speed[1]);
rotation = Math.atan2(speed[1], speed[0]);
}
public double[] ruleGather(Boid[] myBoids)
{
double[] result = {0,0};
double[] center = {0,0};
int neighbours = 0;
for(Boid b : myBoids)
{
double[] diff = {X-b.X , Y-b.Y};
if(delta(diff)<stats.viewRange)
{
center[0] += b.X;
center[1] += b.Y;
neighbours++;
}
}
center[0] = center[0]/neighbours;
36
KAPITEL 5. APPENDIX
center[1] = center[1]/neighbours;
result[0] = (center[0] - this.X)/100;
result[1] = (center[1] - this.Y)/100;
return result;
}
public double[] ruleAvoid(Boid[] myBoids)
{
double[] result = {0,0};
int neighbours = 0;
for(int i = 0; i < myBoids.length; i++)
{
if(myBoids[i] != this)
{
double[] tmp = new double[2];
tmp[0] = myBoids[i].X - this.X;
tmp[1] = myBoids[i].Y - this.Y;
if(delta(tmp)<stats.viewRange)
if(delta(tmp)<stats.personalSpace)
{
if(tmp[0] != 0)
result[0] = result[0] stats.personalSpace/tmp[0];
if(tmp[1] != 0)
result[1] = result[1] stats.personalSpace/tmp[1];
neighbours++;
}
}
}
if(neighbours>0)
{
result[0] = result[0]/neighbours;
result[1] = result[1]/neighbours;
}
return result;
}
public double[] ruleMatch(Boid[] myBoids)
{
double[] result = {0,0};
int neighbours = 0;
for(Boid b : myBoids)
{
double[] diff = {X-b.X , Y-b.Y};
.2.
KÄLLKOD FÖR EVOLUTION
if(this != b && delta(diff)<stats.viewRange)
{
result[0] += b.speed[0];
result[1] += b.speed[1];
neighbours++;
}
}
if(neighbours == 0)
return result;
result[0] = result[0]/neighbours;
result[1] = result[1]/neighbours;
result[0] = (result[0] - this.speed[0])/stats.ownWill;
result[1] = (result[1] - this.speed[1])/stats.ownWill;
return result;
}
public double[] dontExit()
{
double[] result = {0,0};
if(X < 0)
result[0] = -X;
else if(X > Settings.WIDTH+10)
result[0] = Settings.WIDTH-X;
if(Y < 0)
result[1] = -Y;
else if(Y > Settings.HEIGHT+10)
result[1] = Settings.HEIGHT-Y;
return result;
}
public double delta(double[] vect)
{
return Math.sqrt(vect[0]*vect[0] + vect[1]*vect[1]);
}
public double delta(Boid x,Boid y)
{
double[] vect = new double[2];
vect[0] = x.X;
vect[1] = x.Y;
vect[0] -= y.X;
vect[1] -= y.Y;
return Math.sqrt(vect[0]*vect[0] + vect[1]*vect[1]);
}
37
38
KAPITEL 5. APPENDIX
public boolean death(Boid[] myBoids)
{
if(X<-100)
alive = false;
if(X>Settings.WIDTH+100)
alive = false;
if(Y<-100)
alive = false;
if(Y>Settings.HEIGHT+100)
alive = false;
if(!alive)
return true;
for(Boid b : myBoids)
{
if(b != this)
{
if(delta(this,b)<5)
{
this.alive = false;
return true;
}
}
}
return false;
}
}
//Stats.java
package flockSim;
public class Stats implements Comparable<Stats> {
public double fitness = 0;
public double viewRange = 100;
public double personalSpace = 50;
public double ownWill = 10;
public double gather = 1;
public double avoid = 1;
public double match = 1;
public double exit = 1;
public double change = 100;
public Stats()
{
}
.2.
KÄLLKOD FÖR EVOLUTION
39
public Stats(double[] tmp)
{
viewRange = tmp[0];
personalSpace = tmp[1] ;
ownWill = tmp[2] ;
gather = tmp[3] ;
avoid = tmp[4] ;
match = tmp[5] ;
exit = tmp[6] ;
change = tmp[7] ;
}
public double[] toArray()
{
double[] tmp = new double[8];
tmp[0] = viewRange;
tmp[1] = personalSpace;
tmp[2] = ownWill;
tmp[3] = gather;
tmp[4] = avoid;
tmp[5] = match;
tmp[6] = exit;
tmp[7] = change;
return tmp;
}
public void random()
{
viewRange = 2*Math.random()*viewRange;
personalSpace = 2*Math.random()*personalSpace;
ownWill = 2*Math.random()*ownWill;
gather = 2*Math.random()*gather;
avoid = 2*Math.random()*avoid;
match = 2*Math.random()*match;
exit = 2*Math.random()*exit;
change = 2*Math.random()*change;
}
public void print()
{
System.out.println("public double viewRange = " + viewRange
+";");
System.out.println("public double personalSpace = " +
personalSpace +";");
System.out.println("public double ownWill = " + ownWill
+";");
System.out.println("public double gather = " + gather +";");
System.out.println("public double avoid = " + avoid +";");
System.out.println("public double match = " + match +";");
System.out.println("public double exit = " + exit +";");
40
KAPITEL 5. APPENDIX
System.out.println("public double change = " + change +";");
}
@Override
public int compareTo(Stats o) {
// TODO Auto-generated method stub
return (int)((int)o.fitness - (int)this.fitness);
}
}
//Settings.java
package flockSim;
public class Settings {
public static int WIDTH = 1200;
public static int HEIGHT = 800;
}