LINKÖPINGS UNIVERSITET Första versionen Fördjupningsuppgift i kursen 729G11 2009-10-09 Genetisk programmering i Othello Kerstin Johansson kerjo104@student.liu.se
Innehållsförteckning 1. Inledning... 1 1.1 Bakgrund... 1 1.1.1 Regler för Othello... 1 1.1.2 Maskininlärning inom Othello-spel... 1 2. Implementation... 2 2.1 Genetisk programmering... 2 2.2 Heuristik... 2 2.3 Träd... 3 2.4 Fitness... 3 2.5 Elitselektion & Urval... 4 2.6 Mutationer... 4 2.7 Crossover... 4 3. Resultat... 5 4. Diskussion... 7 5. Källor... 7
1. Inledning 1.1 Bakgrund 1.1.1 Regler för Othello Othello, även kallat Reversi, är ett brädspel för två spelare som spelas på en spelplan med 8x8 rutor. Från början ligger fyra brickor utlagda i mitten av spelplanen, två svarta och två vita. Brickorna är tvåfärgade så att undersidan av brickorna har motsatt färg och spelet går ut på att få vända på motspelarens brickor så att det vid spelets slut finns flest brickor av ens egen färg. Svart börjar spelet med att lägga ut en bricka av sin egen färg. Brickor måste läggas så att det på en rak linje från den nya brickan finns en eller flera brickor av motsatt färg, följt av en bricka av sin egen färg. Man får då vända på motståndarens brickor i linjen. Linjerna kan gå horisontellt, vertikalt eller diagonalt. Spelarna turas om att lägga ut brickor av sin egen färg och om en spelare inte kan lägga går turen över till motståndaren. När ingen kan lägga eller då spelplanen är full är spelet slut. 1.1.2 Maskininlärning inom Othello-spel I den här rapporten kommer jag att beskriva mitt försök till att få en dator till att lära sig spela Othello med hjälp av genetisk programmering. Som inspiration har jag använt instruktioner och studentrapporter från ett 3 veckors labbprojekt vid Columbia University kallat GPOthello (Eskin & Siegel, 1999). I detta labbprojekt var större delen av koden redan given i uppgiften och studenterna fick experimentera med att modifiera koden i syfte att förbättra programmet. Jag har inte överhuvudtaget studerat koden till detta program, men jag har fått ledtrådar till hur programmet fungerade genom uppgiftsinstruktionerna och studenternas rapporter. Den initiala koden till GPOthello använde sig av 9 parametrar (terminals) för att utvärdera hur bra ett drag var. Dessa var: 1) Black antalet svarta brickor på spelplanen 2) Black_corners antalet svarta brickor i hörnen 3) Black_near_corners antalet svarta brickor nära ett hörn 4) Black_edges antalet svarta brickor vid kanten av spelplanen 5) White antalet vita brickor på spelplanen 6) White_corners antalet vita brickor i hörnen 7) White_near_corners antalet vita brickor nära ett hörn 8) White_edges antalet vita brickor vid kanten av spelplanen 9) 10 en konstant. Fitness-värdet beräknades genom att spela 5 matcher mot en slumpmässig spelare och räkna ihop antalet av motståndarens pjäser vid spelets slut. De bästa spelarna skulle då ha låga värden. Den enda siffra jag har hittat på hur väl det ursprungliga programmet fungerade var att det i den sista generationen hade utvecklats spelare som kunde slå de slumpmässiga spelarna i 47 fall av 50 (Eskin & Siegel, 1999). Det står dock inte hur stor population som använts eller antalet generationer, vilket gör siffran ganska värdelös för jämförelse med andra program. 1
2. Implementation 2.1 Genetisk programmering Genetisk programmering är en specialisering av genetiska algoritmer, med skillnaden att lösningen är representerad som ett program. Tekniken är inspirerad av den biologiska evolutionen och använder sig av liknande terminologi. För att finna lösningen till ett problem använder sig genetisk programmering av fyra steg: 1. Skapa en population av slumpmässigt sammansatta datorprogram. 2. Kör alla program och beräkna ett fitnessvärde för hur väl de löser problemet. 3. Skapa en ny population med hjälp av: a) Kopiering av de bästa programmen b) Crossover: Parning av existerande program 4. Det bästa programmet som framkommit i någon generation är resultatet av den genetiska programmeringen. Förutom kopiering och parning kan man även använda sig av mutationer, dvs slumpmässiga förändringar hos existerande individer, i syfte att utöka den genetiska mångfalden i populationen. (Koza, 1992) 2.2 Heuristik De program som skapas med hjälp av den genetiska programmeringen kommer att vara sammansatta av fördefinierade funktioner och parametrar. Dessa sätts slumpmässigt samman när nya individer skapas. De funktioner som jag har valt att använda är de fyra räknesätten (+, -, *, /). Jag har dock gjort en egen funktion för division som fungerar på samma sätt som vanlig division förutom att den returnerar 1 vid division med noll. Parametrarna jag har valt är följande: own_bricks: Antalet egna brickor på spelplanen. Detta mått säger egentligen inte så mycket om hur bra positionen är eftersom man kan vända många brickor i ett drag. opp_bricks: Antalet av motståndarens brickor på spelplanen. ståndarens brickor. empty: Antalet tomma rutor på spelplanen. Kan ge en hint om hur långt in i spelet man har kommit 10 : konstanten 10 Tänkt att kunna användas för att t.ex. öka genomslagskraften hos en variabel opp_allowed_moves: Antalet möjliga drag för motståndaren. Om variabeln är lika med noll, går turen tillbaka. Own_in_pos_X: A Här finns 10 olika typer av positioner representerade. De är valda efter symmetri i spelplanen och är markerade i Figur 2.1. 2
Figur 2.1 Symmetriska positioner 2.3 Träd De genetiska programmen är representerade som träd med noder. Funktionen består av roten som har två barn. Jag har begränsat antalet barn för varje nod till antingen noll (för en parameter) eller två (för en funktion). Jag har också satt en djupbegränsning på trädet för att undvika att programmet blir alltför stort. Eftersom värdet av trädet beräknas med en rekursiv loop varje gång man gör ett drag, finns det en tids- och minne däremot en nackdel att begränsa trädet, eftersom man kan missa lösningar som ligger utanför begränsningarna. 2.4 F itness Fitness-värdet beräknas genom att låta varje individ spela Othello ett antal gånger mot en annan motståndare (exempelvis en slumpspelare som slumpar fram ett drag ur alla tillåtna handlingar). Hälften av matcherna spelar individen som Vit, den andra hälften som Svart. Från matcherna sparas dels antalet gånger som individen vunnit och dels hur stor differensen var mellan antalet av de egna och motståndarens brickor. Antalet vunna matcher är den viktigaste aspekten, men för att ytterligare kunna skilja individerna åt har jag valt att även ha med differensen i fitness-värdet. Fitnessen beräknas som: F = Vinster*100 + Differens. (konstanten 100 finns bara med för att öka genomslagskraften av antalet vinster). Det finns dock ett problem med min definition av fitness som jag inte insåg från början. Om en match avslutas innan alla rutor är fyllda (t.ex. för att alla brickor av en färg redan har vänts) så kan detta ge en lägre differens än om alla rutor är fyllda. Så trots att det egentligen 3
borde ses som positivt att man har kunnat avgöra matchen i förtid är det inte något som belönas i beräkningen av fitness. Snarare tvärtom. Därför hade det varit bättre att istället för differens använda samma mått som GPOthello använde, dvs antalet av motståndarens brickor. Ett annat problem är att om man spelar mot en slumpspelare är det inte säkert att en högre fitness automatiskt innebär att man är en bättre spelare. Man kan bara ha haft tur, ifall ens motståndaren slumpade fram dåliga drag. Detta gör att man måste spela väldigt många matcher för att få ett tillförlitligt fitness-värde. Ett alternativ hade varit att låta individerna i populationen spela mot en annan datorspelare, helst en bättre spelare. Tyvärr har jag inte hunnit med att fixa någon sådan träningsspelare. 2.5 Elitselektion & Urval De n bästa individerna i generationen enligt fitness-värdet tillhör elitselektionen. Dessa kopieras direkt till nästa generation. Övriga platser i den nya populationen fylls genom att individer ur den gamla populationen väljs ut för mutation eller crossover. Först sorteras alla individer i den gamla populationen efter fitness så att de bästa individerna hamnar först i listan över individer i populationen. När sedan en individ ska väljas ut slumpas ett tal inom längden av listan fram som slutindex och individen väljs slumpmässigt bland de individer som ligger före slutindex i listan. På så sätt ökar sannolikheten för att bli vald ju högre fitness man har. Ett ännu bättre sätt att välja ut individer hade varit att välja dem med en sannolikhet som är direkt proportionell emot fitness-värdet, men detta har jag inte hunnit med att implementera. 2.6 Mutationer En individ som väljs ut kommer med en viss sannolikhet att antingen muteras eller paras med en annan individ. Mutation sker genom att värdet på en slumpmässigt utvald nod i trädet byts ut mot ett nytt värde. Det nya värdet väljs slumpmässigt bland de tillgängliga funktionerna eller parametrarna. 2.7 Crossover De individer som inte muteras kommer istället att paras. När två individer paras väljs en slumpmässig nod ur varje träd och dessa noder byter förälder i träden. För att undvika att maxdjupet i trädet överträds finns också en funktion som kontrollerar om det är tillåtet att para två noder. Ifall maxdjupet överträds vid parning kommer programmet istället att välja ett av barnen till den nod som har det största trädet. 4
3. Resultat När programmet kördes använde jag en extremt liten population på endast 25 individer. Detta beror på att jag valde att använda en relativt tidskrävande fitness-beräkning. Varje individ spelade 20 matcher per generation (10 som svart och 10 som vit) mot en slumpspelare. Eftersom det i första hand är antalet matcher som avgör hur lång tid den genetiska programmeringen tar, gjorde detta att jag istället var tvungen att minska ner på antalet individer. Jag lät populationen föröka sig och mutera i 50 generationer, med en mutation_rate på 0,05 (5 % mutationer och 95 % crossover) och en elitselektion på 5 individer. Funktionsträdens maxdjup sattes till 5. Jag körde programmet 3 gånger, dvs. med 3 olika slumpmässigt skapade startpopulationer och fick liknande utveckling av fitness vid varje körning. Diagram 3.1 nedan visar medelvärden från dessa 3 körningar. Diagram 3.1 Den blå linjen visar medelvärdet av det högsta fitness som någon individ fått i varje generation. Den gröna linjen visar medelvärdet av det lägsta fitness någon individ fått. Den röda linjen visar medelvärdet för alla individer i generationen. I diagrammet ser vi att utvecklingen går fortast i början och att kurvan sedan planar ut något. I startgenerationerna finns en väldigt stor variation mellan individernas sammansättning, medan det i senare generationer blir mer likriktat. I slutpopulationerna var de flesta individer av liknande uppbyggnad och det fanns mycket liten mångfald. Det förekom också identiska individer eftersom programmet saknar en funktion för att ta bort likadana kopior. De bästa individerna från varje körning, dvs den individ i någon generation som fick högst fitness, var följande: 5
T räd: (+ (* own_in_pos_a own_bricks') (+ (+ own_in_pos_h (/ own_in_pos_a own_in_pos_a)) (+ (* (/ own_in_pos_a own_in_pos_a) own_in_pos_a)) (* own_in_pos_h own_in_pos_a))) Högsta Fitness: 2577, i generation: 47 T räd: (*(* own_in_pos_a own_in_pos_c) (* (* own_in_pos_a (- own_in_pos_a opp_allowed_moves)) own_in_pos_c')) Högsta fitness: 2674, i generation 19 T räd: (+ own_in_pos_d (- (+ ( + (/ empty empty) own_in_pos_a) own_in_pos_a) own_in_pos_e)) Högsta fitness: 2569, i generation 38 Träden är här omskrivna till LISP-liknande funktioner. Egentligen består varje träd av noder som är representerade som objekt. Jag lät den sista av dessa 3 individer spela 10 000 matcher mot slumpspelaren och den vann då 9 057 av dessa, dvs ca 91 % av matcherna. Det kan jämföras med GPOthello-spelaren som vann 47 av 50 matcher mot deras slumpmässiga spelare (94 % av matcherna). Jag vet dock inte hur många generationer de körde eller hur stor population de använde, därför är det svårt att göra några exakta jämförelser. Jag vet inte heller om 47 matcher av 50 innebär att de bara lät den spela 50 matcher, vilket i så fall knappast kan ses som en statistiskt tillförlitlig siffra. I vilket fall som helst kan programmet knappast ses som en bra Othello-spelare om det förlorar eller spelar oavgort i 9 % av matcherna mot slumpen (som ju är en väldigt dålig Othello-spelare). 6
4. Diskussion Eftersom jag har bara har kört den genetiska programmeringen 3 gånger och inte testat att variera exempelvis populationsstorleken så är det svårt att utvärdera hur bra programmet fungerar. Från de körningar jag har gjort kan man dock se att populationerna förbättrar sig och att de program som den genetiska programmeringen resulterar i spelar betydligt bättre än slumpen. Jag har fått många idéer till förbättringar av programmet som jag skulle ha velat implementera om jag hade haft mer tid. Det skulle t.ex. ha varit intressant att utvidga urvalsfunktionen genom att göra det möjlighet att låta individerna i en population spela Othello mot varandra i en turnering, där de bästa går vidare och de sämsta slås ut. Andra förändringar skulle t.ex. vara en funktion som ser till att inga identiska kopior får förekomma i populationen, samt en bättre metod för att beräkna fitness-värde. För tillfället sparas inte heller individerna som skapas, så när man stänger ner Python är det genetiska programmet borta. Detta är också något som borde åtgärdas. 5. K ällor Eskin & Siegel (1999), Department of Computer Science, Columbia University. K oza, John R. (1992) Genetic Programming: On the Programming of Computers by Means of Natural Selection The MIT Press 7