library(tensorPredictors) library(Rchess) library(mgcv) # for `gam()` (Generalized Additive Model) source("./gmlm_chess.R") ################################################################################ ### Fitting the GMLM mixture model ### ################################################################################ # Data set file name of chess positions with Stockfish [https://stockfishchess.org] # evaluation scores (downloaded and processed by `./preprocessing.sh` from the # lichess data base [https://database.lichess.org/]) data_set <- "lichess_db_standard_rated_2023-11.fen" # Function to draw samples `X` form the chess position `data_set` conditioned on # `Y` (position scores) to be in the interval `score_min` to `score_max`. data_gen <- function(batch_size, score_min, score_max) { Rchess::fen2int(Rchess::data.gen(data_set, batch_size, score_min, score_max, quiet = TRUE)) } # Invoke specialized GMLM optimization routine for chess data fit.gmlm <- gmlm_chess(data_gen) ################################################################################ ### Reduction Interpretation and Validation ### ################################################################################ # load last save point (includes reduction as `betas`) save_point <- sort(list.files( ".", pattern = "save_point_[0-9]*\\.Rdata", full.names = TRUE ), decreasing = TRUE)[[1]] load(save_point) ### Construct PSQT (Piece SQuare Tables) from reduction `betas` sample_size <- 100000 # Sample a new position data set for fitting a linear model to conbine different # reduction directions into a per piece PSQT matrix fens <- Rchess::data.gen(data_set, sample_size, -20, 20, quiet = TRUE) # extract stockfish (non-static) position evaluation y <- attr(fens, "scores") # Convert position into "One-Hot Encoded" / "Bit Board" tensor X <- Rchess::fen2int(fens) # Compute reduction reducedX <- Reduce(rbind, Map(function(piece) { # "condition" on piece, that is to extract the current mixture component X <- X[, , piece, ] # reduce mixture component mlm(X - as.vector(rowMeans(X, dims = 2)), betas[[piece]], transposed = TRUE) }, 1:12)) # Convert memory layout to contain vectorized observations in rows reducedX <- t(`dim<-`(reducedX, c(48, sample_size))) # set names for coefficient extraction from linear fit colnames(reducedX) <- as.vector(outer( unlist(strsplit("PNBRQKpnbrqk", "")), c(1, "yl", "yu", "y.2"), paste, sep = "." )) # Estimate PSQT linear combination weights from reduced sample (exclude dead # draw positions, that is "score = 0". This are approx 5% of all positions) fit <- lm(y ~ ., data = data.frame(y = y, reducedX), subset = y != 0.0) summary(fit) # Translate reduction with weighting estimate into PSQTs psqt <- Map(function(piece) { # reduction column names corresponding to the current white piece (upper case) piece <- toupper(piece) col_names <- paste(piece, c(1, "yl", "yu", "y.2"), sep = ".") # Whites PSQT psqt_white <- do.call(kronecker, rev(betas[[piece]])) %*% coef(fit)[col_names] dim(psqt_white) <- c(8, 8) # the same for black piece <- tolower(piece) col_names <- paste(piece, c(1, "yl", "yu", "y.2"), sep = ".") psqt_black <- do.call(kronecker, rev(betas[[piece]])) %*% coef(fit)[col_names] dim(psqt_black) <- c(8, 8) # Combine into shared PSQT from whites point of view psqt_white - psqt_black[8:1, ] }, c("P", "N", "B", "R", "Q", "K")) # finish by enforcing the pawn constraint (irrelevant for validation, the # corresponding values in an encoded position is always zero) psqt[["P"]][c(1, 8), ] <- 0 ### Validation by GAM fitted on reduced data formula <- as.formula(paste("y ~ ", paste("s(", colnames(reducedX), ")", collapse = "+"))) fit.gam <- mgcv::gam(formula, data = data.frame(y = y, reducedX), subset = y != 0.0) summary(fit.gam) # compair estimates with mean as baseline and static human crafted evaluation (HCE) rmse.base <- sqrt(mean((mean(y) - y)^2)) y.hce <- Rchess::HCE(fens) rmse.hce <- sqrt(mean((y.hce - y)^2)) y.hat <- predict(fit.gam, newdata = data.frame(reducedX)) rmse.hat <- sqrt(mean((y.hat - y)^2)) # Also extract R^2 (eval by hand or get from models) (r.sq.lm <- summary(fit)$r.squared) (r.sq.gam <- summary(fit.gam)$r.sq) (r.sq.hce <- 1 - (rmse.hce / rmse.base)^2) ################################################################################ ### Generate LaTeX PSQT plot ### ################################################################################ if (FALSE) { sink("psqt.tex") cat("% Authomatically generated by `dataAnalysis/chess.R` \\documentclass{standalone} \\usepackage[LSB, T1]{fontenc} \\usepackage{chessboard} \\usepackage{skak} \\usepackage{tikz} \\usepackage{amsmath} \\usepackage{xcolor} \\setboardfontencoding{LSB} \\setchessboard{linewidth = 0.1em, showmover = false, smallboard} ") cat(paste0("\\definecolor{col", 1:128, "}{HTML}{", mapply(`[`, strsplit(hcl.colors(128, "Blue-Red 3", rev = TRUE), "#"), 2), "}" )) cat(" \\begin{document} \\begin{tikzpicture} \\coordinate (pawn) at (0, 0); \\coordinate (knight) at (5, 0); \\coordinate (bishop) at (10, 0); \\coordinate (rook) at (0, -5.2); \\coordinate (queen) at (5, -5.2); \\coordinate (king) at (10, -5.2); ") local({ zlim <- c(-1, 1) * max(abs(unlist(psqt, use.names = FALSE))) breaks <- seq(zlim[1], zlim[2], len = 129) pieces <- c("pawn", "knight", "bishop", "rook", "queen", "king") for (i in seq_along(psqt)) { cat(paste0("\\node (", pieces[i], ") at (", pieces[i], ") {\\chessboard[", paste0( "color=col", as.integer(cut(psqt[[i]], breaks)), ",colorbackfield=", outer(8:1, letters[1:8], function(r, f) paste0(f, r)), collapse="," ), "]};\n")) } }) cat(" \\node[anchor = north, yshift = -0.4em] at (pawn.north) {Pawn}; \\node[anchor = north, yshift = -0.4em] at (knight.north) {Knight}; \\node[anchor = north, yshift = -0.4em] at (bishop.north) {Bishop}; \\node[anchor = north, yshift = -0.4em] at (rook.north) {Rook}; \\node[anchor = north, yshift = -0.4em] at (queen.north) {Queen}; \\node[anchor = north, yshift = -0.4em] at (king.north) {King}; \\end{tikzpicture} \\end{document} ") sink() }