From f57799a2b45cf7ad76ca9cff32cb4d1d86c2f2c4 Mon Sep 17 00:00:00 2001 From: InigoGutierrez Date: Sat, 25 Jun 2022 01:47:38 +0200 Subject: Working convolutional neural network. --- imago/engine/keras/convNeuralNetwork.py | 35 +++++---- imago/engine/keras/keras.py | 13 ++-- imago/engine/keras/neuralNetwork.py | 133 ++++++++++++++++++++++---------- 3 files changed, 117 insertions(+), 64 deletions(-) diff --git a/imago/engine/keras/convNeuralNetwork.py b/imago/engine/keras/convNeuralNetwork.py index 2534db6..9d97586 100644 --- a/imago/engine/keras/convNeuralNetwork.py +++ b/imago/engine/keras/convNeuralNetwork.py @@ -1,43 +1,46 @@ """Convolutional neural network.""" from tensorflow.keras.models import Sequential -from tensorflow.keras.layers import Conv2D, Conv2DTranspose, Reshape, Dense +from tensorflow.keras.layers import Conv2D, MaxPool2D, Flatten, Dense from tensorflow.keras.optimizers import Adam from imago.engine.keras.neuralNetwork import NeuralNetwork -defaultModelFile = 'models/imagoConvKerasModel.h5' - class ConvNeuralNetwork(NeuralNetwork): + NETWORK_ID = "convNeuralNetwork" + DEFAULT_MODEL_FILE = 'models/imagoConvKerasModel.h5' + def _initModel(self, boardSize=NeuralNetwork.DEF_BOARD_SIZE): model = Sequential([ Conv2D( filters=32, - kernel_size=(3), + kernel_size=(3,3), activation='relu', padding='same', - input_shape=(boardSize,boardSize,1) + input_shape=(boardSize,boardSize,2) + ), + MaxPool2D( + pool_size=(2,2), + strides=2, + padding='valid' ), Conv2D( filters=64, - kernel_size=(3), + kernel_size=(3,3), activation='relu', padding='same' ), - #Conv2DTranspose( - # filters=64, - # kernel_size=(3), - # activation='relu', - # padding='same' - #), + MaxPool2D( + pool_size=(2,2), + strides=2, + padding='valid' + ), + Flatten(), Dense( - units=1, + units=81, activation='softmax' ), - Reshape( - (boardSize,boardSize) - ), ]) model.summary() diff --git a/imago/engine/keras/keras.py b/imago/engine/keras/keras.py index 80b6647..4f39818 100644 --- a/imago/engine/keras/keras.py +++ b/imago/engine/keras/keras.py @@ -3,18 +3,19 @@ from imago.gameLogic.gameMove import GameMove from imago.gameLogic.gameBoard import GameBoard from imago.engine.decisionAlgorithm import DecisionAlgorithm -from imago.engine.keras.denseNeuralNetwork import DenseNeuralNetwork -from imago.engine.keras.convNeuralNetwork import ConvNeuralNetwork +from imago.engine.keras.neuralNetwork import NeuralNetwork -MODEL_FILE = "" # Use network's default model file +MODEL_FILE = "/home/taamas/IIS/TFG/imago/models/imagoConvKerasModel.h5" class Keras(DecisionAlgorithm): def __init__(self, move): self.currentMove = move self.boardSize = move.board.getBoardHeight() - #self.nn = NeuralNetwork(MODEL_FILE, self.boardSize) - self.nn = ConvNeuralNetwork(MODEL_FILE, self.boardSize) + self.nn = NeuralNetwork( + MODEL_FILE, + self.boardSize + ) def forceNextMove(self, coords): """Selects given move as next move.""" @@ -22,7 +23,7 @@ class Keras(DecisionAlgorithm): def pickMove(self): """Returns a move to play.""" - return self.nn.pickMove(self.currentMove) + return self.nn.pickMove(self.currentMove, self.currentMove.getNextPlayer()) def clearBoard(self): """Empties move history.""" diff --git a/imago/engine/keras/neuralNetwork.py b/imago/engine/keras/neuralNetwork.py index a52a3af..d0eb4ae 100644 --- a/imago/engine/keras/neuralNetwork.py +++ b/imago/engine/keras/neuralNetwork.py @@ -1,32 +1,34 @@ """Keras neural network.""" import sys +import os import os.path import numpy from matplotlib import pyplot from tensorflow.keras.models import load_model +from tensorflow.keras.utils import plot_model from imago.data.enums import Player -defaultModelFile = 'models/imagoKerasModel.h5' - class NeuralNetwork: DEF_BOARD_SIZE = 9 - PLAYER_ID = 1 - OPPONENT_ID = -1 + + NETWORK_ID = "neuralNetwork" + DEFAULT_MODEL_FILE = "models/imagoKerasModel.h5" def __init__(self, modelPath="", boardSize=DEF_BOARD_SIZE): self.boardSize = boardSize - self.path = defaultModelFile + self.path = self.DEFAULT_MODEL_FILE if modelPath != "": self.path = modelPath try: - self.model = self._loadModel(modelPath) + self.model = self._loadModel(self.path) except FileNotFoundError: self.model = self._initModel(boardSize) + self.saveModelPlot() def _initModel(self, boardSize=DEF_BOARD_SIZE): raise NotImplementedError("Tried to directly use NeuralNetwork class. Use one of the subclasses instead.") @@ -60,6 +62,7 @@ class NeuralNetwork: % modelPath) def saveModel(self, modelPath=""): + """Saves the neural network model at the given path.""" if modelPath != "": self.model.save(modelPath) else: @@ -68,80 +71,126 @@ class NeuralNetwork: def _movesToTrainMoves(self, moves): trainMoves = [] for move in moves: - trainMove = self._moveToTrainMove(move) + if len(move.nextMoves) == 0: + continue + player = move.nextMoves[0].getPlayer() + board = move.board.board + trainMove = self._boardToPlayerContext(board, player) trainMoves.append(trainMove) return trainMoves - def _moveToTrainMove(self, move): - player = move.getPlayer() - return self._boardToPlayerContext(move.board.board, player) - def _boardToPlayerContext(self, board, player): - """Converts to a board where PLAYER_ID marks the stones who made the move and - OPPONENT_ID marks the stones of the other 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.""" boardRows = len(board) boardCols = len(board[0]) - contextBoard = numpy.zeros((boardRows, boardCols), dtype = int) + contextBoard = numpy.zeros((boardRows, boardCols, 2), dtype = float) for row in range(boardRows): for col in range(boardCols): if board[row][col] != Player.EMPTY: if board[row][col] == player: - contextBoard[row][col] = PLAYER_ID + contextBoard[row][col][0] = 1 else: - contextBoard[row][col] = OPPONENT_ID - return contextBoard.tolist() + contextBoard[row][col][1] = 1 + return contextBoard def _movesToTargets(self, moves): + """Converts the moves to 2D matrices with values zero except for a one on the + played vertex.""" targets = [] for move in moves: - target = numpy.zeros( - (move.board.getBoardHeight(), - move.board.getBoardWidth()), - dtype = float) - target[move.getRow()][move.getCol()] = 1 + 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 targets.append(target.tolist()) return targets - def pickMove(self, gameMove): - prediction = self._predict(gameMove)[0] - #self.showHeatmap(prediction) + def pickMove(self, gameMove, player): + """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)) + for row in range(self.boardSize): + for col in range(self.boardSize): + prediction[row][col] = predictionVector[row * self.boardSize + col] + self.saveHeatmap(prediction) + + # Search the highest valued vertex which is also playable playableVertices = gameMove.getPlayableVertices() highest = -sys.float_info.max 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): + def _predict(self, gameMove, player): board = gameMove.board.board - player = gameMove.getPlayer() - sampleBoard = self._boardToPlayerContext(board, player) - sampleBoard = numpy.array([sampleBoard]) + sampleBoards = self._boardToPlayerContext(board, player) + sampleBoards = numpy.array([sampleBoards]) return self.model.predict( - x = sampleBoard, + x = sampleBoards, batch_size = 1, verbose = 2) - def showHeatmap(self, data): + def saveHeatmap(self, data): + rows = len(data) + cols = len(data[0]) + fig, ax = pyplot.subplots() - im = ax.imshow(data) + im = ax.imshow(data, cmap="YlGn") # Show all ticks and label them with the respective list entries - ax.set_xticks(numpy.arange(9)) - ax.set_yticks(numpy.arange(9)) + 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)) # 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)") + textColorThreshold = 0.35 + 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]), + ha="center", va="center", color=textColor) + + ax.set_title("Heat map of move likelihood") fig.tight_layout() - pyplot.show() + pyplot.savefig("heatmaps/heatmap_%s_%s_%d.png" % + ( + self.NETWORK_ID, + self.path.replace('/','-'), + len([file for file in os.listdir("heatmaps")]) + ) + ) + + def _getLetterLabels(self, count): + labels = [] + letter = 'A' + for _ in range(count): + labels.append(letter) + letter = chr(ord(letter) + 1) + # Skip I + if letter == 'I': + letter = 'J' + return labels + + def saveModelPlot(self): + plot_model( + self.model, + to_file="model.png", + show_shapes=True, + show_dtype=True, + show_layer_names=True, + rankdir="TB", + expand_nested=True, + dpi=96, + layer_range=None, + show_layer_activations=True, + ) -- cgit v1.2.1