NNExperiments/src/main.nim

123 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
import std/strformat
## 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
if params.hasKey("sameMove"):
result = 24 - params["moves"]
else:
result = params["moves"]
if int(params["result"]) == GameStatus.Draw.int:
result += 6
elif int(params["result"]) == GameStatus.Lose.int:
result += 12
echo result
proc compareNetworks(a, b: NeuralNetwork): int =
if a.params.len() == 0:
return -1
elif b.params.len() == 0:
return 1
return cmp(loss(a.params), loss(b.params))
proc crossover(a, b: NeuralNetwork): NeuralNetwork =
result = deepCopy(a)
var i = 0
while i < a.layers.len():
# 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 = 100
const Iterations = 300
const Epochs = 10
const Take = 15
var networks: seq[NeuralNetwork] = @[]
var best: seq[NeuralNetwork] = @[]
for _ in 0..<Population:
networks.add(newNeuralNetwork(@[9, 16, 12, 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), biasRange=(-0.5, 0.5),
learnRate=0.02))
var gameOne: TrisGame
var gameTwo: TrisGame
var one: NeuralNetwork
var two: NeuralNetwork
var pos: tuple[row, col: int]
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)
one.params["sameMove"] = 1.0
two.params["sameMove"] = 1.0
break
gameTwo.place(TileKind.Self, pos.row, pos.col)
gameOne.place(TileKind.Enemy, pos.row, pos.col)
if not one.params.hasKey("sameMove"):
one.params["result"] = gameOne.get().float()
two.params["result"] = gameTwo.get().float()
one.params["moves"] = gameOne.moves.float()
two.params["moves"] = gameTwo.moves.float()
networks.sort(cmp=compareNetworks)
best = networks[0..<Take]
while networks.len() < Population:
one = sample(best)
two = sample(best)
while one == two:
two = sample(best)
networks.add(one.crossover(two))