Abstract
Texel's tuning method is basically a supervised learning using logistic regression.
Current paper offers the most simple implementation possible with an emphasis on
understanding the basic pipe line and explanations of all the basics steps needed
to establish it.
Generating dataset
There are two major approaches to obtain a dataset for tuning purposes -
external PGN file or one produced by engine's self play. I used both
ways to tune Wukong JS. All we need to do is to extract FEN positions
from the PGN file and associate game result with it so that
white win maps to '1.0', draw to '0.5' and black win to '0.1'
See example dataset below:
8/p5p1/1pnnkp2/4p2p/4P3/P2K1P2/1PN3PP/4B3 b - - 0 37 [0.5]
8/4k3/7R/7p/p6P/1bK5/8/8 b - - 40 60 [0.5]
8/8/5n2/5p2/3N2k1/6P1/6K1/8 w - - 12 120 [0.5]
rn1qk2r/pb2bppp/1pp2n2/3pN3/B2P4/2N1P3/PP3PPP/R1BQK2R b KQkq - 1 9 [0.0]
r1bqr1k1/1p3pp1/3p1n1p/2p5/2Pp4/PQ1P2PP/3NPPB1/R3R1K1 b - - 1 17 [0.5]
1q1r1rk1/pb1pbppp/1p2p3/8/2PnPBn1/P1N2N2/1P1QBPPP/2R2RK1 b - - 11 15 [1.0]
5rk1/r2nbppp/pqp1p3/1p1n4/3PP3/2N2BP1/PP1B1P1P/R1QR2K1 b - - 0 17 [0.5]
8/6k1/3p1np1/b3pP1p/6P1/P3BK1P/8/2N5 b - - 2 43 [1.0]
4r1k1/r4p1p/2p1b1p1/3pP3/1p2nP2/5N1P/1PP3P1/3RRBK1 w - - 0 30 [0.5]
rn1qk2b/pb3p2/2p1pn2/4N1p1/2pPP3/2N3B1/PP3PP1/R2QK3 w Qq - 0 14 [0.0]
8/8/6kP/1pp5/4K1n1/1P6/P2B4/8 b - - 10 50 [1.0]
2Q5/4q1pk/7p/5p2/8/6R1/6KP/4r3 b - - 13 52 [0.0]
8/p2r1pk1/1np1b1p1/2p1p1b1/2P1P2p/qP3B1P/2R1NPP1/2NQ2K1 w - - 2 48 [0.0]
2kr1b2/p1q2p1p/4RQrp/2pp2N1/5p2/3P4/PPPN1P2/5K1R w - - 2 20 [0.5]
2r2rk1/4q1pp/p2n1pn1/3p3N/1p1Pp1PP/4P2B/PPP2QR1/5R1K w - - 1 26 [0.5]
1n1q1r1k/pp4pp/4Q2n/1B1p4/3P2P1/8/1R4KP/6B1 w - - 3 32 [0.0]
7k/6b1/4B2p/r7/1p2KP2/1P5P/P5R1/8 b - - 2 56 [1.0]
I've removed first 5 positions from each game and those containing
checkmates on board - just like in the original method. Now the difference is
that in order to label positions with scalar evaluation value later on
using evaluate() function instead of quiescence() I've extracted only
those positions where static evaluation equals quiescence score so that
tactical noise didn't affect the tuning process. All the positions within
a single game are shuffled. Also the overall data has been shuffled as well
Calculating mean square error
In order to calculate the mean square error all we need is:
- Get evaluation score
- Map evaluation score to probabilistic value using sigmoid
- Subtract it from game result and square the product
- Divide sum of errors for all positions by number of positions
See the python implementation below:
def mean_square_error(self, K, number_of_positions): # open file containing FEN positions with open('positions.txt', encoding='utf-8') as f: # init positions list with fixed number of games fens = f.read().split('\n')[0:number_of_positions] # error accumulator error = 0.0 # loop over FEN strings for fen in fens: try: # extract game result from FEN string result = float(fen.split('[')[-1][:-1]) # extract position position = fen.split('[')[0] # set position on internal chess board (python-chess library) self.board.set_fen(position) # get static evaluation of the position score = self.evaluate() # map evaluation score to probabilistic value sigmoid = 1 / (1 + pow(10, -K * score / 400)) # sum error error += pow(result - sigmoid, 2) except Exception as e: print(e) # get mean square error return error / number_of_positions
Tuning
Now when we're able to get the mean square error for a set of given position
all we need is to loop over the evaluation parameters we want to tune, adjust
them sligthly and recalculate mean square error. If it get's reduced we store
the new parameters otherwise keep existing ones. The procedure is repeated until
mean square error is getting minimized. There are much more efficient and sophisticated
algorithms to adjust the evaluation parameters but I use the simplest possible.
Unfortunately it's the slowest method as well.
See the python implementation below:
def tune(self, K, number_of_positions): # value to adjust parameter by adjust_value = 1 # get the copy of evaluation parameters actually used to evaluate position best_params = self.extract_weights() # calculate initial mean square error best_error = self.mean_square_error(K, number_of_positions) # variable to flag whether the mean square error has been minimized improved = True # run routine until mean square error is getting minimized while improved: improved = False # loop over all evaluation parameters for index in range(len(best_params)): # copy evaluation weights new_params = json.loads(json.dumps(best_params)) # adjust current parameter by +1 new_params[index] += adjust_value # update evaluation parameters actually used to get static evaluation score self.update_weights(new_params) # recalculate mean square error with updated parameter new_error = self.mean_square_error(K, number_of_positions) print('Tuning param %s out of %s, mean square error %s' % (index, len(best_params), new_error)) # parameter adjustments reduced mean square error if new_error < best_error: # remember error best_error = new_error # update best parameters best_params = json.loads(json.dumps(new_params)) improved = 1 print('found better params +') self.store_weights('tuning_weights.txt') # parameter adjustments didn't reduce mean square error else: # adjust current parameter by -2 (-1 to restore initial state + -1 to adjust) new_params[index] -= adjust_value * 2 # update evaluation parameters actually used to get static evaluation score self.update_weights(new_params) # recalculate mean square error with updated parameter new_error = self.mean_square_error(K, number_of_positions) # parameter adjustments reduced mean square error if new_error < best_error: # remember error best_error = new_error # update best parameters best_params = json.loads(json.dumps(new_params)) improved = 1 print('found better params -') self.store_weights('tuning_weights.txt') # store adjusted evaluation parameters when all of them has been traversed self.store_weights('session_weights_' + str(datetime.datetime.today().strftime('%Y-%m-%d-%H-%M')) + '.txt') print('Writing current session weights...') print('Writing final weights...') self.store_weights('final_weights.txt')