diff options
-rw-r--r-- | doc/diagrams/DecisionAlgorithm.pumlc | 8 | ||||
-rw-r--r-- | doc/diagrams/Keras.pumlc | 9 | ||||
-rw-r--r-- | doc/diagrams/NeuralNetwork.pumlc | 8 | ||||
-rw-r--r-- | doc/diagrams/analysisClasses.puml | 20 | ||||
-rw-r--r-- | doc/diagrams/keras.puml | 13 | ||||
-rw-r--r-- | doc/diagrams/skinparams.puml | 3 | ||||
-rw-r--r-- | imago/engine/core.py | 17 | ||||
-rw-r--r-- | imago/engine/decisionAlgorithm.py | 14 | ||||
-rw-r--r-- | imago/engine/decisionAlgorithmFactory.py | 23 | ||||
-rw-r--r-- | imago/engine/keras.py | 16 | ||||
-rw-r--r-- | imago/engine/keras/neuralNetwork.py | 52 | ||||
-rw-r--r-- | imago/engine/monteCarlo.py | 11 | ||||
-rw-r--r-- | imago/scripts/__init__.py | 0 | ||||
-rw-r--r-- | imago/scripts/monteCarloSimulation.py | 25 | ||||
-rwxr-xr-x | imago/scripts/monteCarloSimulation.sh | 24 | ||||
-rw-r--r-- | results.txt | 41 | ||||
-rwxr-xr-x | simulationMatchesPyplot.py | 27 |
17 files changed, 290 insertions, 21 deletions
diff --git a/doc/diagrams/DecisionAlgorithm.pumlc b/doc/diagrams/DecisionAlgorithm.pumlc new file mode 100644 index 0000000..aada4f0 --- /dev/null +++ b/doc/diagrams/DecisionAlgorithm.pumlc @@ -0,0 +1,8 @@ +@startuml + +interface DecisionAlgorithm { + {abstract} forceNextMove(self, coords) + {abstract} pickMove(self) +} + +@enduml diff --git a/doc/diagrams/Keras.pumlc b/doc/diagrams/Keras.pumlc new file mode 100644 index 0000000..daca149 --- /dev/null +++ b/doc/diagrams/Keras.pumlc @@ -0,0 +1,9 @@ +@startuml + +class Keras { + forceNextMove(self, coords) + pickMove(self) + loadNetwork(self) +} + +@enduml diff --git a/doc/diagrams/NeuralNetwork.pumlc b/doc/diagrams/NeuralNetwork.pumlc new file mode 100644 index 0000000..30f783e --- /dev/null +++ b/doc/diagrams/NeuralNetwork.pumlc @@ -0,0 +1,8 @@ +@startuml + +class NeuralNetwork { + load(self, path) + save(self, path) +} + +@enduml diff --git a/doc/diagrams/analysisClasses.puml b/doc/diagrams/analysisClasses.puml index 4a286f7..d051cf9 100644 --- a/doc/diagrams/analysisClasses.puml +++ b/doc/diagrams/analysisClasses.puml @@ -7,24 +7,24 @@ package GameModule { class GameState class GameBoard class GameMove -} -GameIO -> GameState -GameState -> GameMove -GameMove -> GameBoard + GameIO -> GameState + GameState -> GameMove + GameMove -> GameBoard +} package EngineModule { class EngineIO class EngineLogic - interface DecisionAlgorithm + !include DecisionAlgorithm.pumlc class MonteCarloTreeSearch class OtherDecisionAlgorithm -} -EngineIO --> EngineLogic -EngineLogic -> DecisionAlgorithm -DecisionAlgorithm <|.. MonteCarloTreeSearch -DecisionAlgorithm <|.. OtherDecisionAlgorithm + EngineIO --> EngineLogic + EngineLogic -> DecisionAlgorithm + DecisionAlgorithm <|.. MonteCarloTreeSearch + DecisionAlgorithm <|.. OtherDecisionAlgorithm +} MonteCarloTreeSearch --> GameMove OtherDecisionAlgorithm --> GameMove diff --git a/doc/diagrams/keras.puml b/doc/diagrams/keras.puml new file mode 100644 index 0000000..98776d1 --- /dev/null +++ b/doc/diagrams/keras.puml @@ -0,0 +1,13 @@ +@startuml + +!include skinparams.puml + +!include DecisionAlgorithm.pumlc +!include Keras.pumlc +!include NeuralNetwork.pumlc + +DecisionAlgorithm <|.. Keras + +Keras -> NeuralNetwork + +@enduml diff --git a/doc/diagrams/skinparams.puml b/doc/diagrams/skinparams.puml index cde3da7..2a9e58e 100644 --- a/doc/diagrams/skinparams.puml +++ b/doc/diagrams/skinparams.puml @@ -1,5 +1,8 @@ @startuml +'Old style +skin rose + skinparam { monochrome true shadowing false diff --git a/imago/engine/core.py b/imago/engine/core.py index 44da3b8..09ee51d 100644 --- a/imago/engine/core.py +++ b/imago/engine/core.py @@ -1,18 +1,21 @@ """Imago GTP engine""" -from imago.engine.monteCarlo import MCTS +from imago.engine.decisionAlgorithmFactory import DecisionAlgorithms, DecisionAlgorithmFactory from imago.gameLogic.gameState import GameState DEF_SIZE = 7 DEF_KOMI = 5.5 +DEF_ALGORITHM = DecisionAlgorithms.MONTECARLO class GameEngine: """Plays the game of Go.""" - def __init__(self): + def __init__(self, decisionAlgorithmId = DEF_ALGORITHM): self.komi = DEF_KOMI self.gameState = GameState(DEF_SIZE) - self.mcts = MCTS(self.gameState.lastMove) + self.daId = decisionAlgorithmId + self.daFactory = DecisionAlgorithmFactory() + self.da = self.daFactory.create(self.daId, self.gameState.lastMove) def setBoardsize(self, newSize): """Changes the size of the board. @@ -20,14 +23,14 @@ class GameEngine: It is wise to call clear_board after this command. """ self.gameState = GameState(newSize) - self.mcts = MCTS(self.gameState.lastMove) + self.da = self.da.__init__(self.gameState.lastMove) def clearBoard(self): """The board is cleared, the number of captured stones reset to zero and the move history reset to empty. """ self.gameState.clearBoard() - self.mcts.clearBoard() + self.da.clearBoard() def setKomi(self, komi): """Sets a new value of komi.""" @@ -48,11 +51,11 @@ class GameEngine: row = vertex[0] col = vertex[1] self.gameState.playMoveForPlayer(row, col, color) - self.mcts.forceNextMove(vertex) + self.da.forceNextMove(vertex) def genmove(self, color): """Returns a list representing coordinates of the board in the form (row, col).""" - coords = self.mcts.pickMove().coords + coords = self.da.pickMove().coords #TODO: The move should NOT be played in its generation. This method is just for #suggesting a move. #self.gameState.playMoveForPlayer(coords[0], coords[1], color) diff --git a/imago/engine/decisionAlgorithm.py b/imago/engine/decisionAlgorithm.py new file mode 100644 index 0000000..67fc3ef --- /dev/null +++ b/imago/engine/decisionAlgorithm.py @@ -0,0 +1,14 @@ +"""Decision algorithm interface.""" + +class DecisionAlgorithm: + + def __init__(self, move): + pass + + def forceNextMove(self, coords): + """Selects given move as next move.""" + pass + + def pickMove(self): + """Returns a move to play.""" + pass diff --git a/imago/engine/decisionAlgorithmFactory.py b/imago/engine/decisionAlgorithmFactory.py new file mode 100644 index 0000000..c40c687 --- /dev/null +++ b/imago/engine/decisionAlgorithmFactory.py @@ -0,0 +1,23 @@ +"""Decision algorithm factory.""" + +from enum import Enum, auto as enumAuto +from imago.engine.monteCarlo import MCTS + +class DecisionAlgorithms(Enum): + RANDOM = enumAuto() + MONTECARLO = enumAuto() + KERAS = enumAuto() + +class DecisionAlgorithmFactory: + + def create(self, algorithm, move): + """Creates an instance of the requested algorithm.""" + +# if algorithm == DecisionAlgorithms.RANDOM: +# -- + + if algorithm == DecisionAlgorithms.MONTECARLO: + return MCTS(move) + +# if algorithm == DecisionAlgorithms.KERAS: +# -- diff --git a/imago/engine/keras.py b/imago/engine/keras.py new file mode 100644 index 0000000..5445d92 --- /dev/null +++ b/imago/engine/keras.py @@ -0,0 +1,16 @@ +"""Keras neural network module.""" + +from imago.engine.decisionAlgorithm import DecisionAlgorithm + +class Keras(DecisionAlgorithm): + + def __init__(self, move): + pass + + def forceNextMove(self, coords): + """Selects given move as next move.""" + pass + + def pickMove(self): + """Returns a move to play.""" + pass diff --git a/imago/engine/keras/neuralNetwork.py b/imago/engine/keras/neuralNetwork.py new file mode 100644 index 0000000..7a712f2 --- /dev/null +++ b/imago/engine/keras/neuralNetwork.py @@ -0,0 +1,52 @@ +"""Keras neural network.""" + +import os.path + +import tensorflow as tf +from tensorflow import keras +from tensorflow.keras.models import Sequential +from tensorflow.keras.layers import Activation, Dense +from tensorflow.keras.optimizers import Adam +from tensorflow.keras.metrics import categorical_crossentropy + +modelFile = 'models/medical_trial_model.h5' + +class NeuralNetwork: + def __init__(self, boardSize): + self.path = modelFile + + def _initModel(self, modelPath, boardSize=9): + # Save full model + model = Sequential([ + Dense(units=16, activation='relu', input_shape=(boardSize,boardSize)), + Dense(units=32, activation='relu'), + Dense(units=2, activation='softmax') + ]) + + model.summary() + + model.compile( + optimizer=Adam(learning_rate=0.0001), + loss='sparse_categorical_crossentropy', + metrics=['accuracy'] + ) + + model.fit( + x=scaled_train_samples, + y=train_labels, + validation_split=0.1, + batch_size=10, + epochs=30, + shuffle=True, + verbose=2 + ) + + model.save(modelPath) + + def _loadModel(self, modelPath): + # Load model + if os.path.isfile(modelPath): + from tensorflow.keras.models import load_model + model = load_model(modelPath) + else: + raise Exception("Keras neural network file not found at %s" % modelPath) diff --git a/imago/engine/monteCarlo.py b/imago/engine/monteCarlo.py index 5c916b0..3f28d8d 100644 --- a/imago/engine/monteCarlo.py +++ b/imago/engine/monteCarlo.py @@ -4,8 +4,9 @@ import sys import random from imago.data.enums import Player +from imago.engine.decisionAlgorithm import DecisionAlgorithm -class MCTS: +class MCTS(DecisionAlgorithm): """Monte Carlo tree.""" def __init__(self, move): @@ -15,7 +16,7 @@ class MCTS: """Selects given move as next move.""" for node in self.root.children: if (node.move.getRow() == coords[0] - and node.move.getCol() == coords[1]): + and node.move.getCol() == coords[1]): self.root = node return self.root = self.root.expansionForCoords(coords) @@ -108,13 +109,15 @@ class MCTSNode: # Check if node has unexplored children and better UCB than previously explored if len(self.unexploredVertices) > 0: - if self.ucbForSpecificPlayer(player) > bestNode.ucbForSpecificPlayer(player): + if self.ucbForSpecificPlayer( + player) > bestNode.ucbForSpecificPlayer(player): bestNode = self # Recursively search children for better UCB for child in self.children: bestChildNode = child.selectionRec(bestNode) - if bestChildNode.ucbForSpecificPlayer(player) > bestNode.ucbForSpecificPlayer(player): + if bestChildNode.ucbForSpecificPlayer( + player) > bestNode.ucbForSpecificPlayer(player): bestNode = bestChildNode return bestNode diff --git a/imago/scripts/__init__.py b/imago/scripts/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/imago/scripts/__init__.py diff --git a/imago/scripts/monteCarloSimulation.py b/imago/scripts/monteCarloSimulation.py new file mode 100644 index 0000000..395a87e --- /dev/null +++ b/imago/scripts/monteCarloSimulation.py @@ -0,0 +1,25 @@ +#!/usr/bin/python + +"""Runs MonteCarlo simulations.""" + +import sys + +from imago.gameLogic.gameBoard import GameBoard +from imago.gameLogic.gameMove import GameMove +from imago.engine.monteCarlo import MCTSNode + +if __name__ == "__main__": + + if len(sys.argv) != 4: + print("[ERROR] Usage: monteCarloSimulation.py <size> <scoreDiffHeur> <nMatches>") + sys.exit(1) + + size = int(sys.argv[1]) + scoreDiffHeur = int(sys.argv[2]) + nMatches = int(sys.argv[3]) + + board = GameBoard(size, size) + move = GameMove(board) + node = MCTSNode(move, None) + node.simulation(nMatches, scoreDiffHeur) + print("Score: %d" % node.score) diff --git a/imago/scripts/monteCarloSimulation.sh b/imago/scripts/monteCarloSimulation.sh new file mode 100755 index 0000000..8aa9e68 --- /dev/null +++ b/imago/scripts/monteCarloSimulation.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# Stores statistical data about MonteCarlo simulations + +outputDir="out" + +sampleSize=50 +nMatches=10 + +mkdir -p "$outputDir" + +for size in 9 13 19; do + for scoreDiffHeur in 10 30 60; do + filename="size${size}_scoreDiffHeur${scoreDiffHeur}.dat" + logFile="size${size}_scoreDiffHeur${scoreDiffHeur}_fullOutput.log" + rm "$logFile" + for i in $(seq "$sampleSize"); do + python ./monteCarloSimulation.py "$size" "$scoreDiffHeur" "$nMatches" | + tee -a "${outputDir}/${logFile}" | + grep -E '^Score:' | + cut -d' ' -f2 + done >"${outputDir}/${filename}" + done +done diff --git a/results.txt b/results.txt new file mode 100644 index 0000000..aa1a8a3 --- /dev/null +++ b/results.txt @@ -0,0 +1,41 @@ +Visits: 10 +Score: 2 +Visits: 10 +Score: -4 +Visits: 10 +Score: 2 +Visits: 10 +Score: -2 +Visits: 10 +Score: -4 +Visits: 10 +Score: 0 +Visits: 10 +Score: 4 +Visits: 10 +Score: -2 +Visits: 10 +Score: 0 +Visits: 10 +Score: -4 +---------- +Visits: 50 +Score: -4 +Visits: 50 +Score: 0 +Visits: 50 +Score: -6 +Visits: 50 +Score: 0 +Visits: 50 +Score: -12 +Visits: 50 +Score: -6 +Visits: 50 +Score: 10 +Visits: 50 +Score: 0 +Visits: 50 +Score: 8 +Visits: 50 +Score: -2 diff --git a/simulationMatchesPyplot.py b/simulationMatchesPyplot.py new file mode 100755 index 0000000..809e1a1 --- /dev/null +++ b/simulationMatchesPyplot.py @@ -0,0 +1,27 @@ +#!/usr/bin/python + +"""Uses pyplot to create graphs of the results of MonteCarlo simulations""" + +import matplotlib.pyplot as plt +import numpy as np + +data = np.loadtxt("out/size9_scoreDiffHeur10.dat") + +dic = {} +for value in data: + if value in dic.keys(): + dic[value] += 1 + else: + dic[value] = 1 + +names = list(dic.keys()) +values = list(dic.values()) + +fig, axs = plt.subplots(1, 3, figsize=(9, 3), sharey=True) +axs[0].bar(names, values) +axs[1].scatter(names, values) +axs[2].plot(names, values) +axs[2].axis([-10, 10, 0, max(values)+2]) +fig.suptitle('Categorical Plotting') + +plt.show() |