From 15faec71e4c3e972c522d6b0c81fe0b1ec7f7811 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Wed, 29 Jun 2022 01:36:07 +0200 Subject: Added error messages to engine IO and pass move to neural networks. --- imago/engine/core.py | 3 +- imago/engine/decisionAlgorithmFactory.py | 2 +- imago/engine/imagoIO.py | 75 +++++++++++++++---------- imago/engine/keras/convNeuralNetwork.py | 2 +- imago/engine/keras/initialDenseNeuralNetwork.py | 28 +++++++++ imago/engine/keras/keras.py | 5 +- imago/engine/keras/neuralNetwork.py | 63 ++++++++++++++------- imago/engine/monteCarlo.py | 2 + imago/engine/parseHelpers.py | 16 ++++-- imago/sgfParser/astNode.py | 8 ++- 10 files changed, 143 insertions(+), 61 deletions(-) create mode 100644 imago/engine/keras/initialDenseNeuralNetwork.py diff --git a/imago/engine/core.py b/imago/engine/core.py index 810b851..4028bb3 100644 --- a/imago/engine/core.py +++ b/imago/engine/core.py @@ -47,6 +47,7 @@ class GameEngine: """Plays in the vertex passed as argument""" if vertex == "pass": self.gameState.passForPlayer(color) + self.da.forceNextMove(vertex) return row = vertex[0] col = vertex[1] @@ -56,7 +57,7 @@ class GameEngine: def genmove(self, color): """Returns a list representing coordinates of the board in the form (row, col).""" coords = self.da.pickMove() - self.play(color, [coords[0], coords[1]]) + self.play(color, coords) return coords def undo(self): diff --git a/imago/engine/decisionAlgorithmFactory.py b/imago/engine/decisionAlgorithmFactory.py index bd86864..094a816 100644 --- a/imago/engine/decisionAlgorithmFactory.py +++ b/imago/engine/decisionAlgorithmFactory.py @@ -5,7 +5,7 @@ from imago.engine.monteCarlo import MCTS from imago.engine.keras.keras import Keras class DecisionAlgorithms(Enum): - RANDOM = enumAuto() + #RANDOM = enumAuto() MONTECARLO = enumAuto() KERAS = enumAuto() diff --git a/imago/engine/imagoIO.py b/imago/engine/imagoIO.py index 6ada674..9b3e367 100644 --- a/imago/engine/imagoIO.py +++ b/imago/engine/imagoIO.py @@ -9,6 +9,10 @@ def _response(text=""): print("= %s" % text) print() +def _responseError(text=""): + print("? %s" % text) + print() + def protocol_version(_): """Version of the GTP Protocol""" _response("2") @@ -51,30 +55,36 @@ class ImagoIO: def start(self): """Starts reading commands interactively.""" - while True: - input_tokens = input().split() - - if input_tokens[0] == "quit": - sys.exit(0) - - command = None - for comm in self.commands_set: - if comm.__name__ == input_tokens[0]: - command = comm - - if command is not None: - arguments = input_tokens[1:] - #print("[DEBUG]:Selected command: %s; args: %s" % (command, arguments)) - command(arguments) - else: - print("unknown command") + try: + while True: + input_tokens = input().split() + + if len(input_tokens) == 0: + continue + + if input_tokens[0] == "quit": + sys.exit(0) + + command = None + for comm in self.commands_set: + if comm.__name__ == input_tokens[0]: + command = comm + + if command is not None: + arguments = input_tokens[1:] + #print("[DEBUG]:Selected command: %s; args: %s" % (command, arguments)) + command(arguments) + else: + _responseError("unknown command") + except Exception as err: + _responseError("An uncontrolled error ocurred. The error was: %s" % err) def known_command(self, args): """True if command is known, false otherwise""" if len(args) != 1: - print ("Wrong number of args.") - print ("Usage: known_command COMMAND_NAME") - sys.exit(0) + _responseError("Wrong number of arguments.") + _responseError("Usage: known_command COMMAND_NAME") + return out = "false" for c in self.commands_set: if c.__name__ == args[0]: @@ -94,8 +104,9 @@ class ImagoIO: It is wise to call clear_board after this command. """ if len(args) != 1: - print("Error - Wrong n of args") - sys.exit(1) + _responseError("Wrong number of arguments") + _responseError("Usag. boardsize ") + return size = int(args[0]) self.gameEngine.setBoardsize(size) _response() @@ -110,8 +121,9 @@ class ImagoIO: def komi(self, args): """Sets a new value of komi.""" if len(args) != 1: - print("Error - Wrong n of args") - sys.exit(1) + _responseError("Wrong number of arguments") + _responseError("Usage: komi ") + return komi = float(args[0]) self.gameEngine.setKomi(komi) _response() @@ -121,8 +133,9 @@ class ImagoIO: These vertices follow the GTP specification. """ if len(args) != 1: - print("Error - Wrong n of args") - sys.exit(1) + _responseError("Wrong number of arguments") + _responseError("Usage: fixed_handicap ") + return stones = float(args[0]) vertices = self.gameEngine.setFixedHandicap(stones) out = getCoordsText(vertices[0][0], vertices[0][1]) @@ -141,8 +154,9 @@ class ImagoIO: def play(self, args): """A stone of the requested color is played at the requested vertex.""" if len(args) != 2: - print("Error - Wrong n of args") - sys.exit(1) + _responseError("Wrong number of arguments.") + _responseError("Usage: play ") + return move = parseHelpers.parseMove(args, self.gameEngine.gameState.size) self.gameEngine.play(move.color, move.vertex) _response() @@ -150,8 +164,9 @@ class ImagoIO: def genmove(self, args): """A stone of the requested color is played where the engine chooses.""" if len(args) != 1: - print("Error - Wrong n of args") - sys.exit(1) + _responseError("Wrong number of arguments.") + _responseError("Usage: genmove ") + return color = parseHelpers.parseColor(args[0]) output = parseHelpers.vertexToString(self.gameEngine.genmove(color), self.gameEngine.gameState.size) diff --git a/imago/engine/keras/convNeuralNetwork.py b/imago/engine/keras/convNeuralNetwork.py index 9d97586..638e2fe 100644 --- a/imago/engine/keras/convNeuralNetwork.py +++ b/imago/engine/keras/convNeuralNetwork.py @@ -38,7 +38,7 @@ class ConvNeuralNetwork(NeuralNetwork): ), Flatten(), Dense( - units=81, + units=82, activation='softmax' ), ]) diff --git a/imago/engine/keras/initialDenseNeuralNetwork.py b/imago/engine/keras/initialDenseNeuralNetwork.py new file mode 100644 index 0000000..dfe8379 --- /dev/null +++ b/imago/engine/keras/initialDenseNeuralNetwork.py @@ -0,0 +1,28 @@ +"""Dense neural network.""" + +from tensorflow.keras.models import Sequential +from tensorflow.keras.layers import Dense +from tensorflow.keras.optimizers import Adam + +from imago.engine.keras.neuralNetwork import NeuralNetwork + +defaultModelFile = 'models/imagoDenseKerasModel.h5' + +class DenseNeuralNetwork(NeuralNetwork): + + def _initModel(self, boardSize=NeuralNetwork.DEF_BOARD_SIZE): + model = Sequential([ + Dense(units=16, activation='relu', input_shape=(boardSize,boardSize)), + Dense(units=32, activation='relu'), + Dense(units=boardSize, activation='softmax') + ]) + + model.summary() + + model.compile( + optimizer=Adam(learning_rate=0.0001), + loss='categorical_crossentropy', + metrics=['accuracy'] + ) + + return model diff --git a/imago/engine/keras/keras.py b/imago/engine/keras/keras.py index 0668cd1..00b06d7 100644 --- a/imago/engine/keras/keras.py +++ b/imago/engine/keras/keras.py @@ -18,7 +18,10 @@ class Keras(DecisionAlgorithm): def forceNextMove(self, coords): """Selects given move as next move.""" - self.currentMove = self.currentMove.addMoveByCoords(coords) + if coords == "pass": + self.currentMove = self.currentMove.addPass() + else: + self.currentMove = self.currentMove.addMoveByCoords(coords) def pickMove(self): """Returns a move to play.""" diff --git a/imago/engine/keras/neuralNetwork.py b/imago/engine/keras/neuralNetwork.py index d0eb4ae..7eddb9d 100644 --- a/imago/engine/keras/neuralNetwork.py +++ b/imago/engine/keras/neuralNetwork.py @@ -81,7 +81,7 @@ class NeuralNetwork: def _boardToPlayerContext(self, board, player): """Converts the board to a 3D matrix with two representations of the board, one - marking the player's stones and the oter marking the opponent's stones.""" + marking the player's stones and the other marking the opponent's stones.""" boardRows = len(board) boardCols = len(board[0]) contextBoard = numpy.zeros((boardRows, boardCols, 2), dtype = float) @@ -95,14 +95,19 @@ class NeuralNetwork: return contextBoard def _movesToTargets(self, moves): - """Converts the moves to 2D matrices with values zero except for a one on the - played vertex.""" + """Converts the moves to 2D matrices with values zero except for a one indicating + the played move.""" targets = [] + targetsSize = self.boardSize * self.boardSize + 1 # Each vertex + 1 for pass for move in moves: if len(move.nextMoves) == 0: continue - target = numpy.zeros(self.boardSize * self.boardSize, dtype = float) - target[move.nextMoves[0].getRow() * self.boardSize + move.nextMoves[0].getCol()] = 1 + target = numpy.zeros(targetsSize, dtype = float) + nextMove = move.nextMoves[0] + if nextMove.isPass: + target[-1] = 1 + else: + target[nextMove.getRow() * self.boardSize + nextMove.getCol()] = 1 targets.append(target.tolist()) return targets @@ -110,11 +115,12 @@ class NeuralNetwork: """Uses the model's predict function to pick the highest valued vertex to play.""" predictionVector = self._predict(gameMove, player)[0] - prediction = numpy.zeros((self.boardSize, self.boardSize)) + predictionBoard = numpy.zeros((self.boardSize, self.boardSize)) for row in range(self.boardSize): for col in range(self.boardSize): - prediction[row][col] = predictionVector[row * self.boardSize + col] - self.saveHeatmap(prediction) + predictionBoard[row][col] = predictionVector[row * self.boardSize + col] + predictionPass = predictionVector[-1] + self.saveHeatmap(predictionBoard, predictionPass) # Search the highest valued vertex which is also playable playableVertices = gameMove.getPlayableVertices() @@ -123,11 +129,13 @@ class NeuralNetwork: hCol = -1 for row in range(self.boardSize): for col in range(self.boardSize): - if prediction[row][col] > highest and (row, col) in playableVertices: + if predictionBoard[row][col] > highest and (row, col) in playableVertices: hRow = row hCol = col - highest = prediction[row][col] + highest = predictionBoard[row][col] + if highest < predictionPass: + return "pass" return [hRow, hCol] def _predict(self, gameMove, player): @@ -139,29 +147,42 @@ class NeuralNetwork: batch_size = 1, verbose = 2) - def saveHeatmap(self, data): + def saveHeatmap(self, data, passChance): rows = len(data) cols = len(data[0]) - fig, ax = pyplot.subplots() - im = ax.imshow(data, cmap="YlGn") + fig, (axBoard, axPass) = pyplot.subplots(1, 2, gridspec_kw={'width_ratios': [9, 1]}) + imBoard = axBoard.imshow(data, cmap="YlGn") + axPass.imshow([[passChance]], cmap="YlGn", norm=imBoard.norm) - # Show all ticks and label them with the respective list entries - ax.set_xticks(numpy.arange(cols)) - ax.set_xticklabels(self._getLetterLabels(cols)) - ax.set_yticks(numpy.arange(rows)) - ax.set_yticklabels(numpy.arange(rows, 0, -1)) + # Tick and label the board + axBoard.set_xticks(numpy.arange(cols)) + axBoard.set_xticklabels(self._getLetterLabels(cols)) + axBoard.set_yticks(numpy.arange(rows)) + axBoard.set_yticklabels(numpy.arange(rows, 0, -1)) + + # Label the pass chance + axPass.set_xticks([0]) + axPass.set_yticks([]) + axPass.set_xticklabels(["Pass"]) # Loop over data dimensions and create text annotations. - textColorThreshold = 0.35 + textColorThreshold = data.max() / 2 for row in range(rows): for col in range(cols): textColor = ("k" if data[row, col] < textColorThreshold else "w") - ax.text(col, row, "%.2f"%(data[row, col]), + axBoard.text(col, row, "%.2f"%(data[row, col]), ha="center", va="center", color=textColor) - ax.set_title("Heat map of move likelihood") + textColor = ("k" if passChance < textColorThreshold else "w") + axPass.text(0, 0, "%.2f"%(passChance), + ha="center", va="center", color=textColor) + + pyplot.suptitle("Heat map of move likelihood") + #axBoard.set_title("Heat map of move likelihood") fig.tight_layout() + + #pyplot.show() pyplot.savefig("heatmaps/heatmap_%s_%s_%d.png" % ( self.NETWORK_ID, diff --git a/imago/engine/monteCarlo.py b/imago/engine/monteCarlo.py index 81bee1d..0587cfc 100644 --- a/imago/engine/monteCarlo.py +++ b/imago/engine/monteCarlo.py @@ -14,6 +14,8 @@ class MCTS(DecisionAlgorithm): def forceNextMove(self, coords): """Selects given move as next move.""" + if coords == "pass": + raise NotImplementedError("Pass not implemented for MCTS algorithm.") for node in self.root.children: if (node.move.getRow() == coords[0] and node.move.getCol() == coords[1]): diff --git a/imago/engine/parseHelpers.py b/imago/engine/parseHelpers.py index 59e0496..fd5ea63 100644 --- a/imago/engine/parseHelpers.py +++ b/imago/engine/parseHelpers.py @@ -51,7 +51,9 @@ def parseVertex(text, boardSize): if not re.match("^[A-HJ-Z][1-9][0-9]*$", text): if text == "PASS": return "pass" - return ParseCodes.ERROR + raise RuntimeError( + "Unable to transform string %s to vertex. Wrong format." + % text) vertexCol = ord(text[0]) # Column 'I' does not exist @@ -63,7 +65,9 @@ def parseVertex(text, boardSize): if (vertexCol < 0 or vertexRow < 0 or vertexCol >= boardSize or vertexRow >= boardSize): - return ParseCodes.ERROR + raise RuntimeError( + "Unable to transform string %s to vertex. Maps to [%d, %d], which is out of board bounds (size %d)" + % (text, vertexCol, vertexRow, boardSize)) return [vertexRow, vertexCol] @@ -76,9 +80,13 @@ def vertexToString(vertex, boardSize): if vertex == "pass": return "pass" if len(vertex) != 2: - return ParseCodes.ERROR + raise RuntimeError( + "Unable to transform vertex %s to string. Too many elements." + % str(vertex)) if vertex[0] >= boardSize or vertex[1] >= boardSize or vertex[0] < 0 or vertex[1] < 0: - return ParseCodes.ERROR + raise RuntimeError( + "Unable to transform vertex %s to string. Vertex out of board bounds (size %d)" + % (str(vertex), boardSize)) vertexRow = boardSize - vertex[0] vertexCol = ord('A') + vertex[1] diff --git a/imago/sgfParser/astNode.py b/imago/sgfParser/astNode.py index ff0c517..41629ce 100644 --- a/imago/sgfParser/astNode.py +++ b/imago/sgfParser/astNode.py @@ -5,7 +5,7 @@ from imago.data.enums import Player class ASTNode: """Abstract Syntax Tree Node of SGF parser""" - def __init__(self, children=None, leaf=None, props=None): + def __init__(self, children=None, props=None): if children: self.children = children else: @@ -14,7 +14,6 @@ class ASTNode: self.props = props else: self.props = [] - self.leaf = leaf def addToSequence(self, move): """Appends a move to the last of the sequence started by this move""" @@ -104,6 +103,11 @@ class ASTNode: gameMove = previousMove.addPassForPlayer(player) for child in self.children: child.toGameMoveTree(previousMove=gameMove) + # Add a couple passes at the end of the match since they are not usually recorded + if len(self.children) == 0 and not gameMove.isPass: + nextMove = gameMove.addPass() + nextMove.addPass() + return gameMove def hasProperty(self, propertyName): -- cgit v1.2.1