Skip to content

Commit

Permalink
base WDL model on material count and normalize evals dynamically
Browse files Browse the repository at this point in the history
  • Loading branch information
robertnurnberg committed Mar 17, 2024
1 parent fb07281 commit 19b4501
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 58 deletions.
4 changes: 2 additions & 2 deletions src/evaluate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ std::string Eval::trace(Position& pos, const Eval::NNUE::Networks& networks) {

Value v = networks.big.evaluate(pos, false);
v = pos.side_to_move() == WHITE ? v : -v;
ss << "NNUE evaluation " << 0.01 * UCI::to_cp(v) << " (white side)\n";
ss << "NNUE evaluation " << 0.01 * UCI::to_cp(v, pos) << " (white side)\n";

v = evaluate(networks, pos, VALUE_ZERO);
v = pos.side_to_move() == WHITE ? v : -v;
ss << "Final evaluation " << 0.01 * UCI::to_cp(v) << " (white side)";
ss << "Final evaluation " << 0.01 * UCI::to_cp(v, pos) << " (white side)";
ss << " [with scaled NNUE, ...]";
ss << "\n";

Expand Down
18 changes: 9 additions & 9 deletions src/nnue/nnue_misc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ void hint_common_parent_position(const Position& pos, const Networks& networks)

// Converts a Value into (centi)pawns and writes it in a buffer.
// The buffer must have capacity for at least 5 chars.
static void format_cp_compact(Value v, char* buffer) {
static void format_cp_compact(Value v, char* buffer, const Position& pos) {

buffer[0] = (v < 0 ? '-' : v > 0 ? '+' : ' ');

int cp = std::abs(UCI::to_cp(v));
int cp = std::abs(UCI::to_cp(v, pos));
if (cp >= 10000)
{
buffer[1] = '0' + cp / 10000;
Expand Down Expand Up @@ -90,9 +90,9 @@ static void format_cp_compact(Value v, char* buffer) {


// Converts a Value into pawns, always keeping two decimals
static void format_cp_aligned_dot(Value v, std::stringstream& stream) {
static void format_cp_aligned_dot(Value v, std::stringstream& stream, const Position& pos) {

const double pawns = std::abs(0.01 * UCI::to_cp(v));
const double pawns = std::abs(0.01 * UCI::to_cp(v, pos));

stream << (v < 0 ? '-'
: v > 0 ? '+'
Expand All @@ -113,7 +113,7 @@ std::string trace(Position& pos, const Eval::NNUE::Networks& networks) {
board[row][8 * 8 + 1] = '\0';

// A lambda to output one box of the board
auto writeSquare = [&board](File file, Rank rank, Piece pc, Value value) {
auto writeSquare = [&board, &pos](File file, Rank rank, Piece pc, Value value) {
const int x = int(file) * 8;
const int y = (7 - int(rank)) * 3;
for (int i = 1; i < 8; ++i)
Expand All @@ -124,7 +124,7 @@ std::string trace(Position& pos, const Eval::NNUE::Networks& networks) {
if (pc != NO_PIECE)
board[y + 1][x + 4] = PieceToChar[pc];
if (value != VALUE_NONE)
format_cp_compact(value, &board[y + 2][x + 2]);
format_cp_compact(value, &board[y + 2][x + 2], pos);
};

// We estimate the value of each piece by doing a differential evaluation from
Expand Down Expand Up @@ -179,13 +179,13 @@ std::string trace(Position& pos, const Eval::NNUE::Networks& networks) {
{
ss << "| " << bucket << " ";
ss << " | ";
format_cp_aligned_dot(t.psqt[bucket], ss);
format_cp_aligned_dot(t.psqt[bucket], ss, pos);
ss << " "
<< " | ";
format_cp_aligned_dot(t.positional[bucket], ss);
format_cp_aligned_dot(t.positional[bucket], ss, pos);
ss << " "
<< " | ";
format_cp_aligned_dot(t.psqt[bucket] + t.positional[bucket], ss);
format_cp_aligned_dot(t.psqt[bucket] + t.positional[bucket], ss, pos);
ss << " "
<< " |";
if (bucket == t.correctBucket)
Expand Down
7 changes: 4 additions & 3 deletions src/search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ void Search::Worker::start_searching() {
{
rootMoves.emplace_back(Move::none());
sync_cout << "info depth 0 score "
<< UCI::value(rootPos.checkers() ? -VALUE_MATE : VALUE_DRAW) << sync_endl;
<< UCI::to_score(rootPos.checkers() ? -VALUE_MATE : VALUE_DRAW, rootPos)
<< sync_endl;
}
else
{
Expand Down Expand Up @@ -1898,10 +1899,10 @@ std::string SearchManager::pv(const Search::Worker& worker,

ss << "info"
<< " depth " << d << " seldepth " << rootMoves[i].selDepth << " multipv " << i + 1
<< " score " << UCI::value(v);
<< " score " << UCI::to_score(v, pos);

if (worker.options["UCI_ShowWDL"])
ss << UCI::wdl(v, pos.game_ply());
ss << UCI::wdl(v, pos);

if (i == pvIdx && !tb && updated) // tablebase- and previous-scores are exact
ss << (rootMoves[i].scoreLowerbound
Expand Down
99 changes: 58 additions & 41 deletions src/uci.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include <memory>
#include <optional>
#include <sstream>
#include <utility>
#include <vector>

#include "benchmark.h"
Expand All @@ -44,9 +45,8 @@

namespace Stockfish {

constexpr auto StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
constexpr int NormalizeToPawnValue = 356;
constexpr int MaxHashMB = Is64Bit ? 33554432 : 2048;
constexpr auto StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
constexpr int MaxHashMB = Is64Bit ? 33554432 : 2048;


namespace NN = Eval::NNUE;
Expand Down Expand Up @@ -338,15 +338,43 @@ void UCI::position(Position& pos, std::istringstream& is, StateListPtr& states)
}
}

int UCI::to_cp(Value v) { return 100 * v / NormalizeToPawnValue; }
namespace {
std::pair<double, double> win_rate_params(const Position& pos) {

int material = pos.count<PAWN>() + 3 * pos.count<KNIGHT>() + 3 * pos.count<BISHOP>()
+ 5 * pos.count<ROOK>() + 9 * pos.count<QUEEN>();

// The fitted model only uses data for material counts in [10, 78], and is anchored at count 62.
double m = std::clamp(material, 10, 78) / 62.0;

// Return a = p_a(material) and b = p_b(material), see github.com/official-stockfish/WDL_model
constexpr double as[] = {-185.71965483, 504.85014385, -438.58295743, 474.04604627};
constexpr double bs[] = {89.23542728, -137.02141296, 73.28669021, 47.53376190};

double a = (((as[0] * m + as[1]) * m + as[2]) * m) + as[3];
double b = (((bs[0] * m + bs[1]) * m + bs[2]) * m) + bs[3];

std::string UCI::value(Value v) {
return {a, b};
}

// The win rate model is 1 / (1 + exp((a - eval) / b)), where a = p_a(material) and b = p_b(material).
// It fits the LTC fishtest statistics rather accurately.
int win_rate_model(Value v, const Position& pos) {

auto [a, b] = win_rate_params(pos);

// Return the win rate in per mille units, rounded to the nearest integer.
return int(0.5 + 1000 / (1 + std::exp((a - double(v)) / b)));
}
}

std::string UCI::to_score(Value v, const Position& pos) {
assert(-VALUE_INFINITE < v && v < VALUE_INFINITE);

std::stringstream ss;

if (std::abs(v) < VALUE_TB_WIN_IN_MAX_PLY)
ss << "cp " << to_cp(v);
ss << "cp " << to_cp(v, pos);
else if (std::abs(v) <= VALUE_TB)
{
const int ply = VALUE_TB - std::abs(v); // recompute ss->ply
Expand All @@ -358,6 +386,30 @@ std::string UCI::value(Value v) {
return ss.str();
}

// Turns a Value to an integer centipawn number,
// without treatment of mate and similar special scores.
int UCI::to_cp(Value v, const Position& pos) {

// In general, the score can be defined via the the WDL as
// (log(1/L - 1) - log(1/W - 1)) / ((log(1/L - 1) + log(1/W - 1))
// Based on our win_rate_model, this simply yields v / a.

auto [a, b] = win_rate_params(pos);

return std::round(100 * int(v) / a);
}

std::string UCI::wdl(Value v, const Position& pos) {
std::stringstream ss;

int wdl_w = win_rate_model(v, pos);
int wdl_l = win_rate_model(-v, pos);
int wdl_d = 1000 - wdl_w - wdl_l;
ss << " wdl " << wdl_w << " " << wdl_d << " " << wdl_l;

return ss.str();
}

std::string UCI::square(Square s) {
return std::string{char('a' + file_of(s)), char('1' + rank_of(s))};
}
Expand All @@ -383,41 +435,6 @@ std::string UCI::move(Move m, bool chess960) {
return move;
}

namespace {
// The win rate model returns the probability of winning (in per mille units) given an
// eval and a game ply. It fits the LTC fishtest statistics rather accurately.
int win_rate_model(Value v, int ply) {

// The fitted model only uses data for moves in [8, 120], and is anchored at move 32.
double m = std::clamp(ply / 2 + 1, 8, 120) / 32.0;

// The coefficients of a third-order polynomial fit is based on the fishtest data
// for two parameters that need to transform eval to the argument of a logistic
// function.
constexpr double as[] = {-1.06249702, 7.42016937, 0.89425629, 348.60356174};
constexpr double bs[] = {-5.33122190, 39.57831533, -90.84473771, 123.40620748};

// Enforce that NormalizeToPawnValue corresponds to a 50% win rate at move 32.
static_assert(NormalizeToPawnValue == int(0.5 + as[0] + as[1] + as[2] + as[3]));

double a = (((as[0] * m + as[1]) * m + as[2]) * m) + as[3];
double b = (((bs[0] * m + bs[1]) * m + bs[2]) * m) + bs[3];

// Return the win rate in per mille units, rounded to the nearest integer.
return int(0.5 + 1000 / (1 + std::exp((a - double(v)) / b)));
}
}

std::string UCI::wdl(Value v, int ply) {
std::stringstream ss;

int wdl_w = win_rate_model(v, ply);
int wdl_l = win_rate_model(-v, ply);
int wdl_d = 1000 - wdl_w - wdl_l;
ss << " wdl " << wdl_w << " " << wdl_d << " " << wdl_l;

return ss.str();
}

Move UCI::to_move(const Position& pos, std::string& str) {
if (str.length() == 5)
Expand Down
6 changes: 3 additions & 3 deletions src/uci.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ class UCI {

void loop();

static int to_cp(Value v);
static std::string value(Value v);
static int to_cp(Value v, const Position& pos);
static std::string to_score(Value v, const Position& pos);
static std::string square(Square s);
static std::string move(Move m, bool chess960);
static std::string wdl(Value v, int ply);
static std::string wdl(Value v, const Position& pos);
static Move to_move(const Position& pos, std::string& str);

static Search::LimitsType parse_limits(const Position& pos, std::istream& is);
Expand Down

0 comments on commit 19b4501

Please sign in to comment.