局面評估與 NNUE
評估回答「這個局面看起來怎樣」,搜尋回答「雙方繼續走下去會怎樣」。 NNUE 給搜尋提供快速判斷,不直接輸出最佳走法,也不替代將殺、重複和棋規判定。
本文核對 official Pikafish 提交 1c66b9b。下面講這份程式碼實際如何使用網路;訓練資料來源、規模與訓練過程不能僅憑推理程式碼推斷。
從棋盤到分數:各部分負責什麼
一次普通的靜態評估大致經過以下步驟:
- 把棋子位置和部分棋子間的關係編碼成網路特徵。
- 複用並更新累加器,再計算後面的網路層,得到原始 NNUE 分數。
evaluate.cpp根據子力、搜尋傾向和限招計數等調整它。- 搜尋按需要加入評估修正,並比較後續變化,形成搜尋結果。
這幾種分數不能混用。介面中的分數還經過輸出換算,詳見引擎執行與分數輸出。搜尋如何使用評估,見搜尋演算法。
evaluate.cpp:不只是呼叫一次網路
當前 Eval::evaluate() 先呼叫 NNUE,再執行縮放處理。它要求局面未被將軍;受將節點由搜尋的相應流程處理,不能把普通靜態分當成應將結果。
simple_eval() 是一份粗略的子力差,用來與 NNUE 的判斷比較,不是網路載入失敗時的備用評估器。實際處理主要有四件事:
- 比較子力差與 NNUE 分數是否同向:例如少子卻被網路看好的局面,就與單純多子佔優不同。
- 結合搜尋傳入的
optimism調整結果;它來自根候選評分,不是隨機加分,也不是使用者的 Elo 等級。 - 根據盤面總子力縮放分數,並隨自然限招計數增加,將分數向零收縮。
- 把普通評估限制在將殺分數範圍之外,避免把「優勢很大」誤寫成「已經算出殺棋」。
因此,原始 NNUE 分數與引擎實際用於搜尋的靜態評估可以不同。限招縮放也只是調整估分;是否真正觸發和棋或其他規則,仍由規則程式碼判斷。
原始碼:子力比較與縮放、optimism 的來源。
NNUE 看哪些資訊?
這份實現同時使用兩類輸入:
| 特徵 | 可以怎樣理解 |
|---|---|
HalfKAv2_hm | 記錄「什麼棋子在哪個格點」,並結合將帥位置、左右鏡射和己方車馬炮配置選擇特徵分組。 |
FullThreats | 在當前盤面上提取經過篩選的棋子間攻擊/保護關係,補充「棋子之間怎樣互相影響」。 |
前者不是固定的棋子位置分表:位置被編碼為網路輸入,再由權重共同計算。後者雖然叫 FullThreats,也不等於已經識別所有戰術威脅。程式碼從當前攻擊範圍與佔用格點生成關係,過濾部分組合和重複關係;它沒有替雙方走完一串變化,也不等於棋規裡的完整「捉子」判定。棄子能否成立、進攻是否將殺,仍需搜尋。
紅黑雙方各保留自己的觀察視角,送往後層時按「當前行棋方在前、對方在後」排列。網路最終給出一個從當前行棋方看的局面分數。
原始碼:位置特徵與分組、關係特徵的篩選與提取、雙視角轉換。
當前網路結構:共用前段,按子力選擇後層
每份 Network 包含一套特徵轉換器和 16 組後層參數。常規求值根據雙方車的數量、馬炮合計數量等配置,選擇其中一組計算;不是每個局面都執行 16 個完整網路再投票。
特徵轉換後的後層輸入為 1024 維,兩級中間仿射層各輸出 32 個值,最後形成單一評分。不過,只寫成普通的「1024→32→32→1」會漏掉結構:程式碼還把截斷與平方啟用的結果拼接,並保留較早層通向輸出的連線。
這些中間數值是網路計算的通道,不是「第幾步變化」或人工指定的「車分、將帥安全分」。本版本普通求值返回一個分數,不能套用其他版本的雙網路、或兩個獨立評估輸出的說明。
原始碼:網路層結構與傳播、16 組後層的選擇、常規求值入口。
增量更新:只補發生變化的部分
相鄰局面通常只差一步。如果每一步都把全部輸入重新累加,成本會很高。NNUE 的累加器儲存前段計算結果:走子時減去舊位置的貢獻、加上新位置的貢獻;吃子再移除被吃子的貢獻,變化的攻擊關係也同步更新。
增量複用的是前段結果,不是跳過整個網路。 後層仍要計算。搜尋棧記錄變化,真正需要評估時才尋找可複用的累加器並補算;撤銷走法也會退回對應狀態。這種做法與置換表儲存搜尋結果是兩回事。
也不是任何走法都能直接沿用上一份累加器。例如己方將帥移動、吃子使特徵子力分組改變、或鏡射選擇發生變化時,相應視角會要求重新整理。重新整理仍可藉助按將帥位置等組織的快取,比較棋子差異來更新位置特徵,再重新加入當前的關係特徵,而非每次都從零做全部工作。
原始碼:累加器棧與按需更新、特徵差分更新、重新整理觸發條件、重新整理快取。
量化與 SIMD:讓每次評估足夠便宜
當前推理主要使用整數計算:特徵權重用 8 位整數,累加器用 16 位整數,後層乘加結果使用 32 位整數,並配合截斷、移位和縮放。這就是這裡所說的量化:用規定的整數尺度表示網路數值。
程式碼還為不同 CPU 提供 SIMD 路徑,用一條指令同時處理多個數,例如 x86 的 SSE/AVX 系列與 ARM 的 NEON。部分層會利用輸入中大量零值減少工作。它們加速的是同一套評估計算,不是讓某個版本額外「懂一種棋理」;實際速度取決於 CPU、編譯路徑與整個搜尋負擔。
原始碼:整數種類與尺度、特徵轉換、後層乘加實現、稀疏輸入層。
網路檔案:名字一樣,不代表相容
EvalFile 指定網路檔案;當前預設名是 pikafish.nnue。載入器會嘗試相應路徑,讀取格式版本、網路結構標識和各層參數,並檢查讀取是否完整。驗證發現要求的網路沒有成功載入時,會提示相容性與路徑問題並終止,不會靜默改用簡單子力評分繼續下棋。
所以,更換網路要與引擎架構匹配;僅把檔案改成同一個名字,不能把舊架構變成新架構。這裡的結構標識用於格式相容性檢查,也不等於對任意檔案作了密碼學意義上的真偽鑑定。
原始碼:預設檔名、查詢與載入、格式和參數檢查、載入驗證與錯誤處理。
看懂 eval 除錯輸出
引擎的 eval 命令用於檢視當前局面的評估細節,不會替你完成一次正常選招搜尋。當前輸出既有以行棋方為視角的 NNUE 內部單位,也有轉換為紅方視角的顯示分;沿用的 white side 標籤在象棋實現裡對應紅方。
這份除錯輸出的最終靜態分使用零 optimism,也沒有還原某個搜尋節點的評估修正歷史。因此,它不必等於 go 搜尋後的分數。被將軍時,除錯入口會明確表示沒有普通的最終靜態評估。
