Initial work on tris test

This commit is contained in:
Mattia Giambirtone 2022-12-23 00:17:57 +01:00 committed by Nocturn9x
parent 1f875e6f2b
commit 3baacadb1c
7 changed files with 231 additions and 194 deletions

View File

@ -1,13 +1,119 @@
import nn/network
import nn/util/activations
import nn/util/losses
import nn/util/matrix
import nn/util/tris
const InitialSize = 50
import std/tables
import std/math
import std/random
import std/algorithm
## A bunch of activation functions
func step*(input: float): float = (if input < 0.0: 0.0 else: 1.0)
func sigmoid*(input: float): float = 1 / (1 + exp(-input))
func silu*(input: float): float = 1 / (1 + exp(-input))
func relu*(input: float): float = max(0.0, input)
func htan*(input: float): float =
let temp = exp(2 * input)
result = (temp - 1) / (temp + 1)
func ind2sub(n: int, shape: tuple[rows, cols: int]): tuple[row, col: int] =
## Converts an absolute index into an x, y pair
return (n div shape.rows, n mod shape.cols)
proc loss(params: TableRef[string, float]): float =
## Our loss function for tris
result = params["moves"]
if int(params["result"]) == GameStatus.Draw.int:
result += 6.0
elif int(params["result"]) == GameStatus.Lose.int:
result += 12.0
result = sigmoid(result)
proc compareNetworks(a, b: NeuralNetwork): int =
return cmp(loss(a.params), loss(b.params))
proc crossover(a, b: NeuralNetwork): NeuralNetwork =
result = deepCopy(a)
for i, layer in a.layers:
result.layers[i].weights = layer.weights.copy()
result.layers[i].biases = layer.biases.copy()
var i = 0
while i < a.layers.len():
result.layers[i] = new(Layer)
result.layers[i].inputSize = a.layers[i].inputSize
result.layers[i].outputSize = a.layers[i].outputSize
# We inherit 50% of our weights and biases from our first
# parent and the other 50% from the other parent
result.layers[i].weights = where(rand[float](a.layers[i].weights.shape) >= 0.5, a.layers[i].weights, b.layers[i].weights)
result.layers[i].biases = where(rand[float](a.layers[i].biases.shape) >= 0.5, a.layers[i].biases, b.layers[i].biases)
# Now we sprinkle some mutations into the inherited weights
# and biases, just to spice things up. If learnRate = 0.02,
# then 2% of our weights and biases will randomly change
result.layers[i].weights = where(rand[float](result.layers[i].weights.shape) < a.learnRate, rand[float](result.layers[i].weights.shape),
result.layers[i].weights)
result.layers[i].biases = where(rand[float](result.layers[i].biases.shape) < a.learnRate, rand[float](result.layers[i].biases.shape),
result.layers[i].biases)
inc(i)
result.learnRate = a.learnRate
## Our training program
const Population = 2
const Iterations = 100
const Epochs = 10
const Take = 2
var networks: seq[NeuralNetwork] = @[]
for _ in 0..<InitialSize:
for _ in 0..<Population:
networks.add(newNeuralNetwork(@[9, 8, 10, 9], activationFunc=newActivation(sigmoid, func (x, y: float): float = 0.0),
lossFunc=newLoss(mse, func (x, y: float): float = 0.0), weightRange=(-1.0, +1.0), learnRate=0.05))
lossFunc=newLoss(loss, func (x, y: float): float = 0.0), weightRange=(-1.0, +1.0), learnRate=0.02))
var gameOne: TrisGame
var gameTwo: TrisGame
var one: NeuralNetwork
var two: NeuralNetwork
var pos: tuple[row, col: int]
var lost: bool = false
for epoch in 0..<Epochs:
for iteration in 0..<Iterations:
gameOne = newTrisGame()
gameTwo = newTrisGame()
one = sample(networks)
two = sample(networks)
while one == two:
two = sample(networks)
while gameOne.get() == Playing:
pos = ind2sub(one.compute(gameOne.map.flatten().asType(float)).argmax() - 1, gameOne.map.shape)
gameOne.place(TileKind.Self, pos.row, pos.col)
gameTwo.place(TileKind.Enemy, pos.row, pos.col)
pos = ind2sub(two.compute(gameTwo.map.flatten().asType(float)).argmax() - 1, gameTwo.map.shape)
if TileKind(gameOne.map[pos.row, pos.col]) != Empty:
# We consider this a loss
one.params["result"] = float(Lose)
two.params["result"] = float(Lose)
lost = true
break
gameTwo.place(TileKind.Self, pos.row, pos.col)
gameOne.place(TileKind.Enemy, pos.row, pos.col)
if not lost:
one.params["result"] = gameOne.get().float()
two.params["result"] = gameTwo.get().float()
else:
lost = false
one.params["moves"] = gameOne.moves.float()
two.params["moves"] = gameTwo.moves.float()
networks.sort(cmp=compareNetworks)
networks = networks[0..<Take]
one = sample(networks)
two = sample(networks)
while one == two:
two = sample(networks)
networks.add(one.crossover(two))

View File

@ -1,85 +0,0 @@
# Copyright 2022 Mattia Giambirtone & All Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import util/matrix
import util/losses
import util/activations
import std/strformat
import std/random
randomize()
type
Layer* = ref object
## A generic neural network
## layer
inputSize*: int # The number of inputs we process
outputSize*: int # The number of outputs we produce
weights*: Matrix[float] # The weights for each connection (2D)
biases*: Matrix[float] # The biases for each neuron (1D)
activation: Activation # The activation function along with its derivative
loss: Loss # The cost function used in training
gradients: tuple[weights, biases: Matrix[float]] # Gradient coefficients for weights and biases
learnRate: float # The speed at which we perform gradient descent
proc `$`*(self: Layer): string =
## Returns a string representation
## of the layer
result = &"Layer(inputs={self.inputSize}, outputs={self.outputSize})"
proc newLayer*(inputSize: int, outputSize: int, activation: Activation, loss: Loss, learnRate: float, weightRange: tuple[start, stop: float]): Layer =
## Creates a new layer with inputSize input
## parameters and outputSize outgoing outputs.
## Weights are initialized with random values
## in the chosen range
new(result)
result.inputSize = inputSize
result.outputSize = outputSize
var biases = newSeqOfCap[float](outputSize)
var biasGradients = newSeqOfCap[float](outputSize)
for _ in 0..<outputSize:
biases.add(0.0)
biasGradients.add(0.0)
var weights = newSeqOfCap[seq[float]](inputSize * outputSize)
var weightGradients = newSeqOfCap[seq[float]](inputSize * outputSize)
for _ in 0..<outputSize:
weights.add(@[])
weightGradients.add(@[])
for _ in 0..<inputSize:
weights[^1].add(rand(weightRange.start..weightRange.stop))
weightGradients[^1].add(0)
result.biases = newMatrix[float](biases)
result.weights = newMatrix[float](weights)
result.activation = activation
result.loss = loss
result.gradients = (weights: newMatrix[float](weightGradients), biases: newMatrix[float](biasGradients))
result.learnRate = learnRate
proc compute*(self: Layer, data: Matrix[float]): Matrix[float] =
## Computes the output of a given layer with
## the given input data and returns it as a
## one-dimensional array
result = (self.weights.dot(data).sum() + self.biases).apply(self.activation.function, axis= -1)
proc cost*(self: Layer, x, y: Matrix[float]): float =
## Returns the total cost of this layer
for i in 0..x.shape.cols:
result += self.loss.function(x[0, i], y[0, i])
result /= float(x.shape.cols)

View File

@ -12,37 +12,122 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import layer
import util/matrix
import util/activations
import util/losses
export layer, matrix, losses, activations
import std/sequtils
import std/strformat
import std/tables
import std/random
type
randomize()
type
NeuralNetwork* = ref object
## A generic feed-forward
## neural network
layers: seq[Layer]
layers*: seq[Layer]
activation: Activation # The activation function along with its derivative
loss: Loss # The cost function along with its derivative
# This parameter has a different meaning depending on
# whether we're learning using backpropagation with gradient
# descent (in which case it is the amount by which we increase
# our input for the next epoch) or using a genetic approach
# (where it will be the rate of mutation for each layer)
learnRate*: float
# Extra parameters
params*: TableRef[string, float]
# Note: The derivatives of the loss and activation
# function are only meaningful when performing gradient
# descent!
Loss* = ref object
## A loss function
function: proc (params: TableRef[string, float]): float
derivative: proc (x, y: float): float {.noSideEffect.}
Activation* = ref object
## An activation function
function: proc (input: float): float {.noSideEffect.}
derivative: proc (x, y: float): float {.noSideEffect.}
Layer* = ref object
## A generic neural network
## layer
inputSize*: int # The number of inputs we process
outputSize*: int # The number of outputs we produce
weights*: Matrix[float] # The weights for each connection (2D)
biases*: Matrix[float] # The biases for each neuron (1D)
gradients: tuple[weights, biases: Matrix[float]] # Gradient coefficients for weights and biases, if using gradient descent
proc newNeuralNetwork*(layers: seq[int], activationFunc: Activation, lossFunc: Loss, learnRate: float,
weightRange: tuple[start, stop: float]): NeuralNetwork =
proc `$`*(self: Layer): string =
## Returns a string representation
## of the layer
result = &"Layer(inputs={self.inputSize}, outputs={self.outputSize})"
proc `$`*(self: NeuralNetwork): string =
## Returns a string representation
## of the network
result = &"NeuralNetwork(learnRate={self.learnRate}, layers={self.layers})"
proc newLoss*(function: proc (params: TableRef[string, float]): float, derivative: proc (x, y: float): float {.noSideEffect.}): Loss =
## Creates a new Loss object
new(result)
result.function = function
result.derivative = derivative
proc newActivation*(function: proc (input: float): float {.noSideEffect.}, derivative: proc (x, y: float): float {.noSideEffect.}): Activation =
## Creates a new Activation object
new(result)
result.function = function
result.derivative = derivative
proc newLayer*(inputSize: int, outputSize: int, weightRange: tuple[start, stop: float]): Layer =
## Creates a new layer with inputSize input
## parameters and outputSize outgoing outputs.
## Weights are initialized with random values
## in the chosen range
new(result)
result.inputSize = inputSize
result.outputSize = outputSize
var biases = newSeqOfCap[float](outputSize)
var biasGradients = newSeqOfCap[float](outputSize)
for _ in 0..<outputSize:
biases.add(0.0)
biasGradients.add(0.0)
var weights = newSeqOfCap[seq[float]](inputSize * outputSize)
var weightGradients = newSeqOfCap[seq[float]](inputSize * outputSize)
for _ in 0..<outputSize:
weights.add(@[])
weightGradients.add(@[])
for _ in 0..<inputSize:
weights[^1].add(rand(weightRange.start..weightRange.stop))
weightGradients[^1].add(0)
result.biases = newMatrix[float](biases)
result.weights = newMatrix[float](weights)
result.gradients = (weights: newMatrix[float](weightGradients), biases: newMatrix[float](biasGradients))
proc newNeuralNetwork*(layers: seq[int], activationFunc: Activation, lossFunc: Loss,
learnRate: float, weightRange: tuple[start, stop: float]): NeuralNetwork =
## Initializes a new neural network
## with the given layer layout
new(result)
result.layers = newSeqOfCap[Layer](len(layers))
for i in 0..<layers.high():
result.layers.add(newLayer(layers[i], layers[i + 1], activationFunc, lossFunc, learnRate, weightRange))
result.layers.add(newLayer(layers[i], layers[i + 1], weightRange))
result.activation = activationFunc
result.loss = lossFunc
result.learnRate = learnRate
result.params = newTable[string, float]()
proc predict*(self: NeuralNetwork, data: Matrix[float]): Matrix[float] =
## Performs a prediction and returns a 1D array
proc compute*(self: NeuralNetwork, data: Matrix[float]): Matrix[float] =
## Performs a computation and returns a 1D array
## with the output
when not defined(release):
if data.shape.rows > 1:
@ -51,17 +136,5 @@ proc predict*(self: NeuralNetwork, data: Matrix[float]): Matrix[float] =
raise newException(ValueError, &"input is of the wrong shape (expecting (1, {self.layers[0].inputSize}), got ({data.shape.rows}, {data.shape.cols}) instead)")
result = data
for layer in self.layers:
result = layer.compute(result)
result = (layer.weights.dot(result).sum() + layer.biases).apply(self.activation.function, axis= -1)
proc classify*(self: NeuralNetwork, data: Matrix[float]): int =
## Performs a prediction and returns the label
## with the highest likelyhood
result = maxIndex(self.predict(data).raw[])
proc cost*(self: NeuralNetwork, x, y: Matrix[float]): float =
## Returns the total average cost of the network
for layer in self.layers:
result += layer.cost(x, y)
result /= float(self.layers.len())

View File

@ -1,35 +0,0 @@
# Copyright 2022 Mattia Giambirtone & All Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import std/math
func step*(input: float): float = (if input < 0.0: 0.0 else: 1.0)
func sigmoid*(input: float): float = 1 / (1 + exp(-input))
func silu*(input: float): float = 1 / (1 + exp(-input))
func relu*(input: float): float = max(0.0, input)
func htan*(input: float): float =
let temp = exp(2 * input)
result = (temp - 1) / (temp + 1)
type Activation* = ref object
function*: proc (input: float): float {.noSideEffect.}
derivative*: proc (x, y: float): float {.noSideEffect.}
proc newActivation*(function: proc (input: float): float {.noSideEffect.}, derivative: proc (x, y: float): float {.noSideEffect.}): Activation =
## Creates a new activation object
new(result)
result.function = function
result.derivative = derivative

View File

@ -1,29 +0,0 @@
# Copyright 2022 Mattia Giambirtone & All Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import std/math
# Mean squared error
func mse*(x, y: float): float = pow(x - y, 2)
type Loss* = ref object
function*: proc (x, y: float): float
derivative*: proc (x, y: float): float
proc newLoss*(function: proc (x, y: float): float, derivative: proc (x, y: float): float): Loss =
## Creates a new activation object
new(result)
result.function = function
result.derivative = derivative

View File

@ -132,6 +132,16 @@ proc rand*[T: int | float](shape: tuple[rows, cols: int], order: MatrixOrder = R
result.data[].add(rand(0.0..1.0))
proc asType*[T](self: Matrix[T], kind: typedesc): Matrix[kind] =
## Same as np.array.astype(...)
new(result)
new(result.data)
for e in self.data[]:
result.data[].add(kind(e))
result.shape = self.shape
result.order = self.order
# Simple one-line helpers and forward declarations
func len*[T](self: Matrix[T]): int {.inline.} = self.data[].len()
func len*[T](self: MatrixView[T]): int {.inline.} = self.shape.cols
@ -814,9 +824,7 @@ proc dot*[T](self, other: Matrix[T]): Matrix[T] =
if self.shape.rows != other.shape.cols:
raise newException(ValueError, &"incompatible argument shapes for dot product")
result = zeros[T]((self.shape.rows, other.shape.cols))
echo self
var other = other.transpose()
echo other
for i in 0..<result.shape.rows:
for j in 0..<result.shape.cols:
result[i, j] = (self[i] * other[j]).sum()
@ -828,13 +836,7 @@ proc dot*[T](self, other: Matrix[T]): Matrix[T] =
for i in 0..<result.shape.cols:
result[0, i] = (self[i] * other[0]).sum()
elif other.shape.rows > 1:
when not defined(release):
if self.shape.cols != other.shape.cols:
raise newException(ValueError, &"incompatible argument shapes for dot product")
result = zeros[T]((0, self.shape.cols))
var other = other.transpose()
for i in 0..<result.shape.cols:
result[0, i] = (self[0] * other[i]).sum()
return other.transpose().dot(self)
else:
return self * other
@ -877,8 +879,8 @@ proc max*[T](self: Matrix[T]): T =
return m
proc argmax*[T](self: Matrix[T]): T =
## Returns the index largest element
proc argmax*[T](self: Matrix[T]): int =
## Returns the index of largest element
## into the matrix
var m: T = self[0, 0]
var
@ -893,7 +895,7 @@ proc argmax*[T](self: Matrix[T]): T =
if self[0, col] > m:
m = self[0, col]
inc(col)
return m
return self.getIndex(row, col)
proc contains*[T](self: Matrix[T], e: T): bool =
@ -905,6 +907,7 @@ proc contains*[T](self: Matrix[T], e: T): bool =
return true
return false
when isMainModule:
import math

View File

@ -15,12 +15,14 @@ type
Draw
TrisGame* = ref object
map*: Matrix[int]
moves*: int
proc newTrisGame*: TrisGame =
## Creates a new TrisGame object
new(result)
result.map = zeros[int]((3, 3))
result.moves = 0
proc get*(self: TrisGame): GameStatus =
@ -57,7 +59,9 @@ proc `$`*(self: TrisGame): string =
proc place*(self: TrisGame, tile: TileKind, x, y: int) =
## Places a tile onto the playing board
self.map[x, y] = int(tile)
if TileKind(self.map[x, y]) == Empty:
self.map[x, y] = int(tile)
inc(self.moves)
when isMainModule: