NNExperiments/src/main.nim

119 lines
4.6 KiB
Nim

import nn/network
import nn/util/matrix
import nn/util/tris
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..<Population:
networks.add(newNeuralNetwork(@[9, 8, 10, 9], activationFunc=newActivation(sigmoid, func (x, y: float): float = 0.0),
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))