feat(official-bots): implement king-relative (HalfKP) encoding in NNUE (NCS-109) #80
@@ -53,6 +53,11 @@ class NNUEDataset(Dataset):
|
|||||||
eval_val = self.evals[idx]
|
eval_val = self.evals[idx]
|
||||||
features = fen_to_features(fen)
|
features = fen_to_features(fen)
|
||||||
|
|
||||||
|
# Board is flipped for Black-to-move in fen_to_features; negate eval
|
||||||
|
# so the label still means "good for the side shown as White after flip"
|
||||||
|
if ' b ' in fen:
|
||||||
|
eval_val = -eval_val
|
||||||
|
|
||||||
# Use evaluation as-is if normalized, otherwise apply sigmoid scaling
|
# Use evaluation as-is if normalized, otherwise apply sigmoid scaling
|
||||||
if self.is_normalized:
|
if self.is_normalized:
|
||||||
target = torch.tensor(eval_val, dtype=torch.float32)
|
target = torch.tensor(eval_val, dtype=torch.float32)
|
||||||
@@ -75,10 +80,18 @@ _PIECE_TO_IDX = {
|
|||||||
|
|
||||||
|
|
||||||
def fen_to_features(fen):
|
def fen_to_features(fen):
|
||||||
"""Convert FEN to 98304-dim king-relative (HalfKP) feature vector."""
|
"""Convert FEN to 98304-dim king-relative (HalfKP) feature vector.
|
||||||
|
|
||||||
|
For Black-to-move positions the board is mirrored (ranks flipped, colours
|
||||||
|
swapped) so the network always sees the position from the side-to-move's
|
||||||
|
perspective. The caller is responsible for negating the eval label to match.
|
||||||
|
"""
|
||||||
features = torch.zeros(INPUT_SIZE, dtype=torch.float32)
|
features = torch.zeros(INPUT_SIZE, dtype=torch.float32)
|
||||||
try:
|
try:
|
||||||
board = chess.Board(fen)
|
board = chess.Board(fen)
|
||||||
|
# Perspective flip: present all positions as if White is to move
|
||||||
|
if board.turn == chess.BLACK:
|
||||||
|
board = board.mirror()
|
||||||
wk = board.king(chess.WHITE)
|
wk = board.king(chess.WHITE)
|
||||||
bk = board.king(chess.BLACK)
|
bk = board.king(chess.BLACK)
|
||||||
if wk is None or bk is None:
|
if wk is None or bk is None:
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ class NNUE(model: NbaiModel):
|
|||||||
|
|
||||||
private def squareNum(sq: Square): Int = sq.rank.ordinal * 8 + sq.file.ordinal
|
private def squareNum(sq: Square): Int = sq.rank.ordinal * 8 + sq.file.ordinal
|
||||||
|
|
||||||
|
// Mirror square vertically (rank 0 ↔ rank 7) for the perspective flip
|
||||||
|
private def flipSqNum(sqNum: Int): Int = (7 - sqNum / 8) * 8 + sqNum % 8
|
||||||
|
|
||||||
private def pieceIdx(piece: Piece): Int =
|
private def pieceIdx(piece: Piece): Int =
|
||||||
if piece.color == Color.White then 6 + piece.pieceType.ordinal else piece.pieceType.ordinal
|
if piece.color == Color.White then 6 + piece.pieceType.ordinal else piece.pieceType.ordinal
|
||||||
|
|
||||||
@@ -225,11 +228,23 @@ class NNUE(model: NbaiModel):
|
|||||||
private val legacyL1 = new Array[Float](accSize)
|
private val legacyL1 = new Array[Float](accSize)
|
||||||
|
|
||||||
def evaluate(context: GameContext): Int =
|
def evaluate(context: GameContext): Int =
|
||||||
val wkSq = wkSqOf(context.board)
|
// Match training: for Black-to-move positions, mirror the board (ranks flipped,
|
||||||
val bkSq = bkSqOf(context.board)
|
// colours swapped) so the model always sees from the side-to-move's perspective.
|
||||||
|
// The scoreFromOutput negation then converts back to White's absolute perspective.
|
||||||
|
val (wkSq, bkSq, pieces, turn) =
|
||||||
|
if context.turn == Color.Black then
|
||||||
|
val wk = flipSqNum(bkSqOf(context.board)) // flipped Black king → new "White" king
|
||||||
|
val bk = flipSqNum(wkSqOf(context.board)) // flipped White king → new "Black" king
|
||||||
|
val flipped = context.board.pieces.map { case (sq, p) =>
|
||||||
|
(sq, Piece(p.color.opposite, p.pieceType))
|
||||||
|
}
|
||||||
|
(wk, bk, flipped, Color.Black) // pass Black so scoreFromOutput negates the result
|
||||||
|
else (wkSqOf(context.board), bkSqOf(context.board), context.board.pieces, context.turn)
|
||||||
System.arraycopy(model.weights(0).bias, 0, legacyL1, 0, accSize)
|
System.arraycopy(model.weights(0).bias, 0, legacyL1, 0, accSize)
|
||||||
for (sq, piece) <- context.board.pieces do addPiece(legacyL1, piece, squareNum(sq), wkSq, bkSq)
|
for (sq, piece) <- pieces do
|
||||||
runL2toOutput(legacyL1, context.turn)
|
val sqNum = if turn == Color.Black then flipSqNum(squareNum(sq)) else squareNum(sq)
|
||||||
|
addPiece(legacyL1, piece, sqNum, wkSq, bkSq)
|
||||||
|
runL2toOutput(legacyL1, turn)
|
||||||
|
|
||||||
def benchmark(): Unit =
|
def benchmark(): Unit =
|
||||||
val context = GameContext.initial
|
val context = GameContext.initial
|
||||||
|
|||||||
Reference in New Issue
Block a user