From 8519597eb00f838ca7beca5324ab27435adb4d36 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Sun, 19 Jun 2022 19:46:22 +0200 Subject: Fixed keras algorithm playing on occupied vertices. --- imago/engine/core.py | 10 +++--- imago/engine/decisionAlgorithm.py | 4 +++ imago/engine/decisionAlgorithmFactory.py | 8 +++-- imago/engine/imagoIO.py | 4 +-- imago/engine/keras.py | 16 --------- imago/engine/keras/keras.py | 27 ++++++++++++++ imago/engine/keras/neuralNetwork.py | 60 ++++++++++++++++++++++++++----- imago/engine/monteCarlo.py | 6 ++-- imago/gameLogic/gameMove.py | 2 +- imagocli.py | 18 +++++++++- models/testModel.h5 | Bin 0 -> 43120 bytes 11 files changed, 116 insertions(+), 39 deletions(-) delete mode 100644 imago/engine/keras.py create mode 100644 imago/engine/keras/keras.py create mode 100644 models/testModel.h5 diff --git a/imago/engine/core.py b/imago/engine/core.py index 09ee51d..810b851 100644 --- a/imago/engine/core.py +++ b/imago/engine/core.py @@ -5,7 +5,7 @@ from imago.gameLogic.gameState import GameState DEF_SIZE = 7 DEF_KOMI = 5.5 -DEF_ALGORITHM = DecisionAlgorithms.MONTECARLO +DEF_ALGORITHM = DecisionAlgorithms.KERAS class GameEngine: """Plays the game of Go.""" @@ -23,7 +23,7 @@ class GameEngine: It is wise to call clear_board after this command. """ self.gameState = GameState(newSize) - self.da = self.da.__init__(self.gameState.lastMove) + self.da = self.daFactory.create(self.daId, self.gameState.lastMove) def clearBoard(self): """The board is cleared, the number of captured stones reset to zero and the move @@ -55,10 +55,8 @@ class GameEngine: def genmove(self, color): """Returns a list representing coordinates of the board in the form (row, col).""" - 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) + coords = self.da.pickMove() + self.play(color, [coords[0], coords[1]]) return coords def undo(self): diff --git a/imago/engine/decisionAlgorithm.py b/imago/engine/decisionAlgorithm.py index e3f4b15..5727d25 100644 --- a/imago/engine/decisionAlgorithm.py +++ b/imago/engine/decisionAlgorithm.py @@ -12,3 +12,7 @@ class DecisionAlgorithm: def pickMove(self): """Returns a move to play.""" raise NotImplementedError("Method pickMove not implemented on type %s" % type(self)) + + def clearBoard(self): + """Empties move history.""" + raise NotImplementedError("Method clearBoard not implemented on type %s" % type(self)) diff --git a/imago/engine/decisionAlgorithmFactory.py b/imago/engine/decisionAlgorithmFactory.py index c40c687..bd86864 100644 --- a/imago/engine/decisionAlgorithmFactory.py +++ b/imago/engine/decisionAlgorithmFactory.py @@ -2,6 +2,7 @@ from enum import Enum, auto as enumAuto from imago.engine.monteCarlo import MCTS +from imago.engine.keras.keras import Keras class DecisionAlgorithms(Enum): RANDOM = enumAuto() @@ -19,5 +20,8 @@ class DecisionAlgorithmFactory: if algorithm == DecisionAlgorithms.MONTECARLO: return MCTS(move) -# if algorithm == DecisionAlgorithms.KERAS: -# -- + if algorithm == DecisionAlgorithms.KERAS: + return Keras(move) + + else: + return MCTS(move) diff --git a/imago/engine/imagoIO.py b/imago/engine/imagoIO.py index 371c447..6ada674 100644 --- a/imago/engine/imagoIO.py +++ b/imago/engine/imagoIO.py @@ -30,7 +30,7 @@ def getCoordsText(row, col): class ImagoIO: """Recieves and handles commands.""" - def __init__(self): + def __init__(self, decisionAlgorithmId = None): self.commands_set = { protocol_version, name, @@ -47,7 +47,7 @@ class ImagoIO: self.genmove, self.undo } - self.gameEngine = GameEngine() + self.gameEngine = GameEngine(decisionAlgorithmId) def start(self): """Starts reading commands interactively.""" diff --git a/imago/engine/keras.py b/imago/engine/keras.py deleted file mode 100644 index 5445d92..0000000 --- a/imago/engine/keras.py +++ /dev/null @@ -1,16 +0,0 @@ -"""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/keras.py b/imago/engine/keras/keras.py new file mode 100644 index 0000000..a6aa913 --- /dev/null +++ b/imago/engine/keras/keras.py @@ -0,0 +1,27 @@ +"""Keras neural network module.""" + +from imago.gameLogic.gameMove import GameMove +from imago.gameLogic.gameBoard import GameBoard +from imago.engine.decisionAlgorithm import DecisionAlgorithm +from imago.engine.keras.neuralNetwork import NeuralNetwork + +MODEL_FILE = "models/testModel.h5" + +class Keras(DecisionAlgorithm): + + def __init__(self, move): + self.currentMove = move + self.boardSize = move.board.getBoardHeight() + self.nn = NeuralNetwork(MODEL_FILE, self.boardSize) + + def forceNextMove(self, coords): + """Selects given move as next move.""" + self.currentMove = self.currentMove.addMoveByCoords(coords) + + def pickMove(self): + """Returns a move to play.""" + return self.nn.pickMove(self.currentMove) + + def clearBoard(self): + """Empties move history.""" + self.currentMove = GameMove(GameBoard(self.boardSize, self.boardSize)) diff --git a/imago/engine/keras/neuralNetwork.py b/imago/engine/keras/neuralNetwork.py index 26cb19e..41b8b23 100644 --- a/imago/engine/keras/neuralNetwork.py +++ b/imago/engine/keras/neuralNetwork.py @@ -1,15 +1,14 @@ """Keras neural network.""" +import sys import os.path + import numpy -from copy import deepcopy +from matplotlib import pyplot -import tensorflow as tf -from tensorflow import keras from tensorflow.keras.models import Sequential, load_model -from tensorflow.keras.layers import Dense, Flatten +from tensorflow.keras.layers import Dense from tensorflow.keras.optimizers import Adam -from tensorflow.keras.metrics import categorical_crossentropy from imago.data.enums import Player @@ -20,6 +19,7 @@ OPPONENT_ID = -1 class NeuralNetwork: def __init__(self, modelPath="", boardSize=DEF_BOARD_SIZE): + self.boardSize = boardSize self.path = defaultModelFile if modelPath != "": self.path = modelPath @@ -59,8 +59,8 @@ class NeuralNetwork: x=trainMoves, y=targets, validation_split=0.1, - batch_size=10, - epochs=30, + batch_size=1, + epochs=20, shuffle=False, verbose=2 ) @@ -94,7 +94,7 @@ class NeuralNetwork: OPPONENT_ID marks the stones of the other player.""" boardRows = len(board) boardCols = len(board[0]) - contextBoard = numpy.zeros( (boardRows, boardCols), dtype = int) + contextBoard = numpy.zeros((boardRows, boardCols), dtype = int) for row in range(boardRows): for col in range(boardCols): if board[row][col] != Player.EMPTY: @@ -114,3 +114,47 @@ class NeuralNetwork: target[move.getRow()][move.getCol()] = 1 targets.append(target.tolist()) return targets + + def pickMove(self, gameMove): + prediction = self._predict(gameMove)[0] + #self.showHeatmap(prediction) + playableVertices = gameMove.getPlayableVertices() + highest = sys.float_info.min + hRow = -1 + hCol = -1 + for row in range(self.boardSize): + for col in range(self.boardSize): + #print("[%d, %d] %s" % (row, col, (row, col) in playableVertices)) + if prediction[row][col] > highest and (row, col) in playableVertices: + hRow = row + hCol = col + highest = prediction[row][col] + return [hRow, hCol] + + def _predict(self, gameMove): + board = gameMove.board.board + player = gameMove.getPlayer() + sampleBoard = self._boardToPlayerContext(board, player) + sampleBoard = numpy.array([sampleBoard]) + return self.model.predict( + x = sampleBoard, + batch_size = 1, + verbose = 2) + + def showHeatmap(self, data): + fig, ax = pyplot.subplots() + im = ax.imshow(data) + + # Show all ticks and label them with the respective list entries + ax.set_xticks(numpy.arange(9)) + ax.set_yticks(numpy.arange(9)) + + # Loop over data dimensions and create text annotations. + for i in range(9): + for j in range(9): + text = ax.text(j, i, data[i, j], + ha="center", va="center", color="w") + + ax.set_title("Harvest of local farmers (in tons/year)") + fig.tight_layout() + pyplot.show() diff --git a/imago/engine/monteCarlo.py b/imago/engine/monteCarlo.py index 3f28d8d..2113fdd 100644 --- a/imago/engine/monteCarlo.py +++ b/imago/engine/monteCarlo.py @@ -29,8 +29,8 @@ class MCTS(DecisionAlgorithm): # completely random for _ in range(5): self.root.selection().expansion().simulation(10, 20) - self.root = self.selectBestNextNode() - return self.root.move + selectedNode = self.selectBestNextNode() + return selectedNode.move.coords def selectBestNextNode(self): """Returns best ucb node available for the current player.""" @@ -169,7 +169,7 @@ class MCTSNode: selectedMove = random.choice(list(sensibleMoves)) currentMove = currentMove.addMoveByCoords(selectedMove) score = currentMove.board.score() - print("Current move: %s" % (currentMove.toString())) + print("Current move: %s" % (str(currentMove))) print("Current move game length: ", currentMove.getGameLength()) print("Score of the board: %d, %d (%d)" % (score[0], diff --git a/imago/gameLogic/gameMove.py b/imago/gameLogic/gameMove.py index cdd2fd6..c1c7a05 100644 --- a/imago/gameLogic/gameMove.py +++ b/imago/gameLogic/gameMove.py @@ -132,7 +132,7 @@ class GameMove: return previousMoves return self.nextMoves[0].getMainLineOfPlay(previousMoves) - def toString(self): + def __str__(self): """Returns the coordinates of the move as a string.""" if self.isPass: return "Pass" diff --git a/imagocli.py b/imagocli.py index e8e4580..150587e 100755 --- a/imagocli.py +++ b/imagocli.py @@ -2,8 +2,24 @@ """Run the Imago engine.""" +import sys + from imago.engine.imagoIO import ImagoIO +from imago.engine.decisionAlgorithmFactory import DecisionAlgorithms if __name__ == "__main__": - io = ImagoIO() + + decisionAlgorithm = None + if len(sys.argv) >= 3: + if sys.argv[1] == "-e": + if sys.argv[2] == "montecarlo": + decisionAlgorithm = DecisionAlgorithms.MONTECARLO + if sys.argv[2] == "keras": + decisionAlgorithm = DecisionAlgorithms.KERAS + + if decisionAlgorithm is None: + io = ImagoIO() + else: + io = ImagoIO(decisionAlgorithm) + io.start() diff --git a/models/testModel.h5 b/models/testModel.h5 new file mode 100644 index 0000000..6bc5fe2 Binary files /dev/null and b/models/testModel.h5 differ -- cgit v1.2.1