Experiment with razoring and fix various tuner issues

This commit is contained in:
Mattia Giambirtone 2024-05-19 01:36:56 +02:00
parent b02e616e0e
commit 24ad374df7
7 changed files with 97 additions and 16 deletions

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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)

18
Chess/process_weights.py Normal file
View File

@ -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))

View File

@ -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)