123 lines
4.6 KiB
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)) |