aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorInigoGutierrez <inigogf.95@gmail.com>2022-06-29 01:36:07 +0200
committerInigoGutierrez <inigogf.95@gmail.com>2022-06-29 01:36:07 +0200
commit15faec71e4c3e972c522d6b0c81fe0b1ec7f7811 (patch)
treea32e8d4ad6c9ec3aa75a77b82c0ca4ec77f4f746
parentd5d3ffad63ff3c274430725dd519191ec0f33029 (diff)
downloadimago-15faec71e4c3e972c522d6b0c81fe0b1ec7f7811.tar.gz
imago-15faec71e4c3e972c522d6b0c81fe0b1ec7f7811.zip
Added error messages to engine IO and pass move to neural networks.
-rw-r--r--imago/engine/core.py3
-rw-r--r--imago/engine/decisionAlgorithmFactory.py2
-rw-r--r--imago/engine/imagoIO.py75
-rw-r--r--imago/engine/keras/convNeuralNetwork.py2
-rw-r--r--imago/engine/keras/initialDenseNeuralNetwork.py28
-rw-r--r--imago/engine/keras/keras.py5
-rw-r--r--imago/engine/keras/neuralNetwork.py63
-rw-r--r--imago/engine/monteCarlo.py2
-rw-r--r--imago/engine/parseHelpers.py16
-rw-r--r--imago/sgfParser/astNode.py8
10 files changed, 143 insertions, 61 deletions
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 <newSize>")
+ 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 <newKomi>")
+ 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 <count>")
+ 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 <color> <vertex>")
+ 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 <color>")
+ 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):