Experiment with razoring and fix various tuner issues
This commit is contained in:
parent
b02e616e0e
commit
24ad374df7
|
@ -346,11 +346,19 @@ proc evaluate*(position: Position, mode: static EvalMode, features: Features = F
|
|||
middleGameScores[piece.color] += MIDDLEGAME_VALUE_TABLES[piece.color][piece.kind][sq]
|
||||
endGameScores[piece.color] += ENDGAME_VALUE_TABLES[piece.color][piece.kind][sq]
|
||||
else:
|
||||
<<<<<<< HEAD
|
||||
features.psqts[piece.kind][sq].mg = scaledMiddleGame
|
||||
features.psqts[piece.kind][sq].eg = scaledEndGame
|
||||
features.pieceWeights[piece.kind].mg += MIDDLEGAME_WEIGHTS[piece.kind].float
|
||||
features.pieceWeights[piece.kind].eg += ENDGAME_WEIGHTS[piece.kind].float
|
||||
# Final score computation
|
||||
=======
|
||||
features.psqts[piece.kind][sq].mg = scaledMiddleGame * (if piece.color == Black: -1 else: 1)
|
||||
features.psqts[piece.kind][sq].eg = scaledEndGame * (if piece.color == Black: -1 else: 1)
|
||||
features.pieceWeights[piece.kind].mg += (if piece.color == Black: -1.0 else: 1.0) * scaledMiddleGame
|
||||
features.pieceWeights[piece.kind].eg += (if piece.color == Black: -1.0 else: 1.0) * scaledEndGame
|
||||
# Final score computation
|
||||
>>>>>>> 54c7cda (Experiment with razoring and fix various tuner issues)
|
||||
let
|
||||
middleGameScore = middleGameScores[sideToMove] - middleGameScores[nonSideToMove]
|
||||
endGameScore = endGameScores[sideToMove] - endGameScores[nonSideToMove]
|
||||
|
@ -363,7 +371,6 @@ proc evaluate*(position: Position, mode: static EvalMode, features: Features = F
|
|||
else:
|
||||
features.tempo = 1.0
|
||||
|
||||
|
||||
# Pawn structure
|
||||
#[
|
||||
when defined(evalPawns):
|
||||
|
@ -476,15 +483,29 @@ proc extract*(self: Features, fen: string): Tensor[float] =
|
|||
## Extracts the features of the evaluation
|
||||
## into a 1-D column vector to be used for
|
||||
## tuning purposes
|
||||
|
||||
# Features is a reference type, so the internal
|
||||
# metadata is not reset at every call to extract().
|
||||
# In order to avoid fuckups, we zero it at every
|
||||
# call
|
||||
for kind in PieceKind.Bishop..PieceKind.Rook:
|
||||
self.pieceWeights[kind] = (0, 0)
|
||||
for square in Square(0)..Square(63):
|
||||
self.psqts[kind][square] = (0, 0)
|
||||
self.tempo = 0
|
||||
|
||||
var position = loadFEN(fen)
|
||||
result = newTensor[float](1, self.featureCount())
|
||||
discard position.evaluate(EvalMode.Tune, self)
|
||||
<<<<<<< HEAD
|
||||
# IMPORTANT NOTE: Features is a reference type, so the internal
|
||||
# metadata is NOT reset at every call to extract()! This is why
|
||||
# we only take the values from the current position's occupancy
|
||||
# rather than the entire thing, we would be taking garbage from
|
||||
# previous feature extractions if we just iterated over every piece
|
||||
# on every square
|
||||
=======
|
||||
>>>>>>> 54c7cda (Experiment with razoring and fix various tuner issues)
|
||||
for square in position.getOccupancy():
|
||||
let piece = position.getPiece(square)
|
||||
var idx = piece.kind.int * len(self.psqts[piece.kind]) + square.int
|
||||
|
@ -493,12 +514,24 @@ proc extract*(self: Features, fen: string): Tensor[float] =
|
|||
# Skip to the first endgame entry
|
||||
idx += 64 * 6
|
||||
result[0, idx] = self.psqts[piece.kind][square].eg
|
||||
<<<<<<< HEAD
|
||||
# Skip to the first middle-game piece weight entry
|
||||
idx = (64 * 6) * 2
|
||||
result[0, idx] = self.pieceWeights[piece.kind].mg
|
||||
# Skip to the first end-game piece weight entry
|
||||
idx += 6 * 2
|
||||
result[0, idx] = self.pieceWeights[piece.kind].eg
|
||||
=======
|
||||
|
||||
# Skip the piece-square tables
|
||||
let offset = 64 * 6 * 2
|
||||
for kind in PieceKind.Bishop..PieceKind.Rook:
|
||||
var idx = offset + kind.int
|
||||
result[0, idx] = self.pieceWeights[kind].mg
|
||||
# Skip to the first end-game piece weight entry
|
||||
idx += 6
|
||||
result[0, idx] = self.pieceWeights[kind].eg
|
||||
>>>>>>> 54c7cda (Experiment with razoring and fix various tuner issues)
|
||||
result[0, ^1] = self.tempo
|
||||
|
||||
|
||||
|
|
|
@ -341,8 +341,7 @@ proc doMove*(self: var Chessboard, move: Move) =
|
|||
piece = self.position.getPiece(move.startSquare)
|
||||
sideToMove = piece.color
|
||||
nonSideToMove = sideToMove.opposite()
|
||||
when not defined(danger):
|
||||
doAssert piece.kind != Empty and piece.color != None, &"{move} {self.toFEN()}"
|
||||
assert piece.kind != Empty and piece.color != None, &"{move} {self.toFEN()}"
|
||||
|
||||
var
|
||||
halfMoveClock = self.position.halfMoveClock
|
||||
|
|
|
@ -281,8 +281,7 @@ proc addPieceToBitboard*(self: var Position, square: Square, piece: Piece) =
|
|||
|
||||
proc spawnPiece*(self: var Position, square: Square, piece: Piece) =
|
||||
## Spawns a new piece at the given square
|
||||
when not defined(danger):
|
||||
doAssert self.getPiece(square).kind == Empty
|
||||
assert self.getPiece(square).kind == Empty
|
||||
self.addPieceToBitboard(square, piece)
|
||||
self.mailbox[square] = piece
|
||||
|
||||
|
@ -290,9 +289,8 @@ proc spawnPiece*(self: var Position, square: Square, piece: Piece) =
|
|||
proc removePiece*(self: var Position, square: Square) =
|
||||
## Removes a piece from the board, updating necessary
|
||||
## metadata
|
||||
when not defined(danger):
|
||||
let piece = self.getPiece(square)
|
||||
doAssert piece.kind != Empty and piece.color != None, self.toFEN()
|
||||
let piece = self.getPiece(square)
|
||||
assert piece.kind != Empty and piece.color != None, self.toFEN()
|
||||
self.removePieceFromBitboard(square)
|
||||
self.mailbox[square] = nullPiece()
|
||||
|
||||
|
|
|
@ -68,6 +68,21 @@ const
|
|||
# value
|
||||
LMP_DEPTH_THRESHOLD {.used.} = 1
|
||||
|
||||
# Constants to configure razoring
|
||||
|
||||
# Only prune when depth <= this value
|
||||
RAZORING_DEPTH_LIMIT = 4
|
||||
|
||||
# Only consider razoring positions
|
||||
# whose static eval + (this value * depth)
|
||||
# is <= alpha
|
||||
RAZORING_EVAL_THRESHOLD = 400
|
||||
|
||||
|
||||
# Miscellaneaus configuration
|
||||
|
||||
# Only use aspiration windows when search
|
||||
# is >= this value
|
||||
ASPIRATION_WINDOW_DEPTH_THRESHOLD = 5
|
||||
|
||||
NUM_KILLERS* = 2
|
||||
|
@ -473,6 +488,7 @@ proc storeKillerMove(self: SearchManager, ply: int, move: Move) {.used.} =
|
|||
proc search(self: var SearchManager, depth, ply: int, alpha, beta: Score, isPV: bool): Score {.discardable.} =
|
||||
## Negamax search with various optimizations and search features
|
||||
|
||||
assert alpha > beta
|
||||
# Clear the PV table
|
||||
for i in 0..MAX_DEPTH:
|
||||
self.pvMoves[ply][i] = nullMove()
|
||||
|
@ -544,10 +560,25 @@ proc search(self: var SearchManager, depth, ply: int, alpha, beta: Score, isPV:
|
|||
# We perform a shallower search because otherwise there would be no point in
|
||||
# doing NMP at all!
|
||||
var reduction = NMP_BASE_REDUCTION + depth div NMP_DEPTH_REDUCTION
|
||||
let score = -self.search(depth - reduction, ply + 1, -beta + 1, -beta, isPV=false)
|
||||
let score = -self.search(depth - reduction, ply + 1, -beta - 1, -beta, isPV=false)
|
||||
self.board.unmakeMove()
|
||||
if score >= beta:
|
||||
return score
|
||||
when defined(razoring):
|
||||
if not isPV and depth <= RAZORING_DEPTH_LIMIT and not self.board.inCheck() and staticEval + RAZORING_EVAL_THRESHOLD * depth <= alpha:
|
||||
# Razoring: if we're in non-pv node and not in check, and the static
|
||||
# evaluation of the position is significantly below alpha (or doesn't
|
||||
# beat it), we perform a quiescent search: if that still doesn't beat
|
||||
# alpha, we prune the branch. We only do this at shallow depths and
|
||||
# increase the threshold the deeper we go, as this optimization is also
|
||||
# unsound. We can do a null-window search to save time time as well (
|
||||
# this is handled implicitly by the fact that all non pv-nodes are
|
||||
# searched with a null window)
|
||||
|
||||
# We're looking to evaluate our own position, so there's no minus sign here
|
||||
let score = self.qsearch(ply, alpha, beta)
|
||||
if score <= alpha:
|
||||
return score
|
||||
var
|
||||
moves = newMoveList()
|
||||
depth = depth
|
||||
|
|
|
@ -71,7 +71,7 @@ proc perft*(board: var Chessboard, ply: int, verbose = false, divide = false, bu
|
|||
when defined(debug):
|
||||
let incHash = board.position.zobristKey
|
||||
board.position.hash()
|
||||
doAssert board.position.zobristKey == incHash, &"{board.position.zobristKey} != {incHash} at {move} ({board.positions[^1].toFEN()})"
|
||||
assert board.position.zobristKey == incHash, &"{board.position.zobristKey} != {incHash} at {move} ({board.positions[^1].toFEN()})"
|
||||
if ply == 1:
|
||||
if move.isCapture():
|
||||
inc(result.captures)
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
data = json.loads((Path.cwd() / "model.json").read_text())
|
||||
result = {
|
||||
"psqts": [{"eg": [], "mg": []} for _ in range(6)],
|
||||
"pieceWeights": [{"eg": data[-13:-7], "mg": data[-7:-1]}],
|
||||
"tempo": data[-1]
|
||||
}
|
||||
for i in range(6):
|
||||
j = i
|
||||
result["psqts"][i]["mg"] = data[j:j + 64]
|
||||
j += 64
|
||||
result["psqts"][i]["eg"] = data[j:j + 64]
|
||||
|
||||
|
||||
(Path.cwd() / "processed.json").write_text(json.dumps(result))
|
|
@ -66,7 +66,7 @@ def batch_loader(extractor: Features, num_batches, batch_size: int, dataset: tup
|
|||
yield torch.from_numpy(features), torch.from_numpy(targets)
|
||||
|
||||
|
||||
def main(num_batches, batch_size: int, dataset_path: Path, epoch_size: int, dump: Path):
|
||||
def main(num_batches, batch_size: int, dataset_path: Path, epoch_size: int, dump: Path, scaling: int):
|
||||
"""
|
||||
Uses pytorch to tune Nimfish's evaluation using the provided
|
||||
dataset
|
||||
|
@ -101,15 +101,16 @@ def main(num_batches, batch_size: int, dataset_path: Path, epoch_size: int, dump
|
|||
epoch_start = timer()
|
||||
running_loss = 0.0
|
||||
print()
|
||||
params = [param.detach().cpu().numpy().tolist() for param in model.parameters()]
|
||||
params = [((param.detach().cpu().numpy() * scaling).round().astype(int)).tolist() for param in model.parameters()][0][0]
|
||||
dump_path = (dump / "model.json")
|
||||
print(f"Tuning completed in {timer() - start:.2f} seconds, dumping results to {dump_path}")
|
||||
dump_path.write_text(json.dumps(params[0][0]))
|
||||
dump_path.write_text(json.dumps(params))
|
||||
|
||||
|
||||
BATCH_SIZE = 16384
|
||||
NUM_BATCHES = 10000
|
||||
NUM_BATCHES = 5000
|
||||
EPOCH_SIZE = 100
|
||||
SCALING_FACTOR = 400
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -119,6 +120,7 @@ if __name__ == "__main__":
|
|||
parser.add_argument("--batches", "-b", type=int, help=f"How many batches to run (defaults to {NUM_BATCHES})", default=NUM_BATCHES)
|
||||
parser.add_argument("--epoch-size", "-e", type=int, help=f"After how many batches the tool prints progress information (defaults to {EPOCH_SIZE})", default=EPOCH_SIZE)
|
||||
parser.add_argument("--batch-size", "-s", type=int, help=f"The number of training samples in each batch (defaults to {BATCH_SIZE})", default=BATCH_SIZE)
|
||||
parser.add_argument("--results", "-r", type=Path, help="Location where the model.json file containing the tuned weights will be dumped", required=True)
|
||||
parser.add_argument("--results", "-r", type=Path, default=Path.cwd(), help="Location where the model.json file containing the tuned weights will be dumped (defaults to the current directory)")
|
||||
parser.add_argument("-f", "--scaling", type=int, help=f"Scaling factor of the final weights (defailts to {SCALING_FACTOR})", default=SCALING_FACTOR)
|
||||
args = parser.parse_args()
|
||||
main(args.batches, args.batch_size, args.dataset, args.epoch_size, args.results)
|
||||
main(args.batches, args.batch_size, args.dataset, args.epoch_size, args.results, args.scaling)
|
Loading…
Reference in New Issue