鳕鱼NNUE文档
原文地址:
https://github.com/official-stockfish/nnue-pytorch/blob/master/docs/nnue.md
前言
这个文档包含的内容:
- 技术内容
- NNUE及其原理的详细描述
- 线性代数快速复习
- 输入定义和因式分解
- 适用于NNUE网络的组件(层)
- 推理代码和优化
- 量化数学及其实现
- 几乎可用于生产的优化代码
- PyTorch训练器实现(+ 重要的CUDA内核)
- 架构考虑和历史
这个文档不包含的内容:
- 网络训练教程(请参阅wiki)
- 数据集、优化器、超参数
- 实验结果日志
基础
什么是NNUE?
NNUE(ƎUИИ Efficiently Updatable Neural Network)广义上讲是一种神经网络架构,它利用了在连续评估之间网络输入的最小变化。它由Yu Nasu发明,用于将其集成到Motohiro Isozaki在2018年5月开发的Shogi软件YaneuraOu中,后来在2019年6月由Hisayori Noda移植到国际象棋引擎Stockfish中,但也适用于许多其他棋类游戏,甚至可能在其他领域中应用。NNUE遵循以下原则:
- 网络应具有相对较少的非零输入。
- 在连续评估之间,输入应尽可能少地改变。
- 网络应足够简单,以便在整数域中实现低精度推理。
遵循第一个原则意味着,当网络规模扩大时,输入必须变得稀疏。当前最佳架构的输入稀疏度约为0.1%。少量非零输入对需要完整评估的情况下所需的时间设置了一个低上限。这是NNUE网络在可以非常快速评估的情况下仍然可以很大的主要原因。
遵循第二个原则(在遵循第一个原则的前提下)创建了一种高效更新网络(或至少是其昂贵部分)的方法,而不是重新评估整个网络。这利用了单一移动只会略微改变棋盘状态的事实。这比第一个原则重要性低,对于实现来说是完全可选的,但在确实利用它的实现中,仍然可以提供可测量的改进。
遵循第三个原则可以在常见硬件上实现最大性能,并使模型特别适合低延迟CPU推理,这是传统象棋引擎所必需的。
总体而言,NNUE原则也适用于昂贵的深度网络,但它们在快速浅层网络中表现出色,适合低延迟CPU推理,无需批处理和加速器。目标性能是每线程每秒进行数百万次评估。这是一个极端的用例,需要极端的解决方案,最重要的是量化。
量化 101 及其重要性
量化是将神经网络模型的域从浮点数更改为整数的过程。NNUE 网络设计为在低精度整数域中快速评估,并能够充分利用现代 CPU 的 int8/int16 性能。浮点数在实现最大引擎强度方面并不是一个选择,因为它牺牲了过多的速度,却只获得了很少的准确性提升(尽管由于其简单性,一些引擎使用浮点表示)。量化不可避免地会引入误差,网络越深误差累积越多,但对于相对浅层的 NNUE 网络来说,这种误差是可以忽略的。量化将在本文档的后面详细描述。在此之前,本文档将使用浮点数而不是整数,直到我们实际进行代码优化时这才会变得重要。插入这段内容的目的是让读者意识到 NNUE 的最终目标,因为这是塑造 NNUE 模型的最大因素,并决定了什么是可能的,什么是不可能的。
NNUE 中哪些层有用?
NNUE 依赖于可以在低精度环境中使用简单算术实现的简单层。这意味着线性层(全连接,基本上是矩阵乘法)和 ClippedReLU(clamp(0, 1))层特别适合它。池化层(乘法/平均/最大)或更复杂激活函数(如 sigmoid)的近似也适用,但不常用。
通常,这种网络保持浅层(2-4 层),因为大部分知识保存在第一层(利用输入稀疏性保持性能),在第一层之后网络需要急剧减少其宽度(在网络后部更深部分的好处将被前面大层的影响所主导)以维持性能要求。
线性层
线性(全连接)层只是一个简单的矩阵乘法。它可以高效实现,支持稀疏输入,并提供良好的容量。它接收 in_features 值作为输入,并产生 out_features 值作为输出。操作为 y = Ax + b,其中:
x - 大小为 in_features 的输入列向量
A - 大小为 (out_features, in_features) 的权重矩阵
b - 大小为 out_features 的偏置列向量
y - 大小为 out_features 的输出列向量
具有稀疏输入的线性层
乘法 Ax 可以概念上简化为“如果 x[i] 不为零,则取 A 的第 i 列,将其乘以 x[i] 并将其加到结果中”。现在应该很明显,每当输入的一个元素为零时,我们可以跳过处理权重矩阵的整行。这意味着我们只需处理 A 的与输入向量中的非零值相对应的列。即使权重矩阵中可能有数万个列,我们每个位置只关注其中的少数几个!这就是为什么第一层可以如此大的原因。
Clipped ReLU 层
这是基于普通 ReLU 的激活函数,不同之处在于它在上下限都有界限。公式为 y = min(max(x, 0), 1)。
这一层的目的是为网络添加非线性。如果只有线性层,它们都可以被折叠成一个,因为矩阵可以直接相乘。
理想情况下,ClippedReLU 将被 ReLU 替代,但激进的量化要求减少隐藏层输入的动态范围,因此将值限制在 1 对性能很重要。
Sigmoid
这是一个与 [clipped] ReLU 相反的平滑激活函数。公式为 y = 1/(1+e^-kx),其中 k 是确定形状“拉伸”程度的参数。