diff --git a/src/main.nim b/src/main.nim index 0f56cdf..e7b3f41 100644 --- a/src/main.nim +++ b/src/main.nim @@ -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.. 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()) \ No newline at end of file diff --git a/src/nn/util/activations.nim b/src/nn/util/activations.nim deleted file mode 100644 index 85b25aa..0000000 --- a/src/nn/util/activations.nim +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/nn/util/losses.nim b/src/nn/util/losses.nim deleted file mode 100644 index 5244e08..0000000 --- a/src/nn/util/losses.nim +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/nn/util/matrix.nim b/src/nn/util/matrix.nim index 5591d58..91101f6 100644 --- a/src/nn/util/matrix.nim +++ b/src/nn/util/matrix.nim @@ -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.. 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.. 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 diff --git a/src/nn/util/tris.nim b/src/nn/util/tris.nim index a3be29a..dcab9a6 100644 --- a/src/nn/util/tris.nim +++ b/src/nn/util/tris.nim @@ -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: