Knowledge Distillation: Compressing Trading Models for Low-Latency Deployment
机器学习驱动的交易中的准确性与延迟的紧张关系已经在本博客上发布了答案。 使用机器学习进行扩展建模 建议两阶段分割:快速梯度提升模型执行延迟关键的实时引用,而深度模型异步运行并向其提供辅助信号或调整其参数。两种模型,两种时钟,一种系统。
知识蒸馏是对相同张力的“不同”答案。您不必将慢速模型与快速模型一起运行,而是离线使用一次来训练快速模型 - 学生学习老师对结果的完整概率分布,而不仅仅是硬标签,然后老师完全离开热路径。推理时只有一个模型,无异步耦合,无陈旧窗口。
哪个答案获胜是经验性的,本文还没有回答这个问题。接下来是机械装置,以及决定它的测量的明确说明。这里没有什么是基准结果;在通常有数字的地方,有一个标记说明必须运行什么。
前面的一个框架修正,来自DeepLOB 和订单簿上的深度学习 :高分类精度不会自动转化为利润 - 预测的走势必须清除买卖价差。因此,“保持教师的方向准确性”是优化蒸馏设置的错误之举。
师生框架

Hinton、Vinyals 和 Dean (2015) 的最初表述很简单。您有一个教师模型 (大、慢、准确)和学生模型 (小、快、待训练)。学生同时从两个信号中学习:
- 硬目标:真实标签(例如,价格上涨或下跌)
- 软目标:教师在所有班级的输出概率分布
学生的损失函数结合了两者:
其中和是教师和学生的logits,是softmax函数,是温度参数,控制两个损耗分量之间的平衡。
为什么软目标对交易很重要
三级上涨/固定/下跌中间价格公式、 阈值以及为什么由此产生的不平衡意味着您报告加权 F1 而不是准确性,这些都在 DeepLOB - 假设此处的标签方案中设置。蒸馏特定点是老师在 argmax 之前发出的内容:硬“向上”携带一位,而 0.72/0.21/0.07 也表示移动可能会停滞并且几乎肯定不会逆转。这种跨类的结构是额外的训练信号,这就是为什么软目标学生比单独接受标签训练的学生能够更好地概括。
警告这种信心不是什么。 Softmax 输出不是经过校准的不确定性,将 0.55 与 0.85 视为头寸调整输入是交易保形预测 存在的捷径——它从区间宽度、边缘比率和区间跨过零时的无交易过滤器得出调整大小,而原始 Softmax 无法提供这些。在这里,获得尺寸声明意味着根据教师的校准来衡量学生的校准(可靠性图,ECE),并显示蒸馏保留了它。该结果尚未出现在本文中。
温度和软目标

温度参数控制概率分布的“柔软度”。给定 logits ,具有温度的 softmax 为:
当 (标准 softmax)时,分布呈峰值——主导类获得大部分概率质量。随着 的增加,分布变平,更清楚地揭示 logits 的相对大小。
| 温度 | 效果 | 使用案例 |
|---|---|---|
| 标准softmax,peaky | 正常推理 | |
| 适度软化 | 一般蒸馏 | |
| 严重软化 | 当老师很自信的时候 | |
| 几乎统一 | 很少有用,会消除信号 |
有一个合理的论点认为,交易模型需要适度的温度:金融预测的可信度远不如图像分类,因此老师可能会输出 0.55/0.30/0.15,而不是 0.99/0.005/0.005,从而在信号消失之前留下较少的峰值来软化。这是一个论点,而不是一个发现——这个范围必须来自对真实数据的扫描,通过加权 F1 进行评分,并且可能因制度而异。
KL 散度项中的 因子补偿了较高温度下梯度幅度的减小。如果没有它,随着 的增加,蒸馏损失将变得小到可以忽略不计。
通过网格搜索选择温度
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from sklearn.metrics import f1_score
def distillation_loss(
student_logits: torch.Tensor,
teacher_logits: torch.Tensor,
labels: torch.Tensor,
temperature: float,
alpha: float,
) -> torch.Tensor:
"""Combined hard-target + soft-target distillation loss."""
hard_loss = F.cross_entropy(student_logits, labels)
soft_teacher = F.log_softmax(teacher_logits / temperature, dim=-1)
soft_student = F.log_softmax(student_logits / temperature, dim=-1)
soft_loss = F.kl_div(
soft_student,
soft_teacher,
log_target=True,
reduction="batchmean",
)
return alpha * hard_loss + (1.0 - alpha) * (temperature ** 2) * soft_loss
def search_temperature(
teacher: nn.Module,
student_factory, # callable returning a fresh student
train_loader: DataLoader,
val_loader: DataLoader,
temperatures: list[float] = [1, 2, 3, 5, 8, 12],
alpha: float = 0.3,
epochs: int = 30,
lr: float = 1e-3,
device: str = "cuda",
):
"""Grid search over temperature, scored by weighted F1 (not accuracy:
the up/flat/down label scheme is heavily imbalanced toward flat)."""
best_f1, best_T, best_student = 0.0, 1.0, None
for T in temperatures:
student = student_factory().to(device)
optimizer = torch.optim.AdamW(student.parameters(), lr=lr)
for epoch in range(epochs):
student.train()
for X, y in train_loader:
X, y = X.to(device), y.to(device)
with torch.no_grad():
teacher_logits = teacher(X)
student_logits = student(X)
loss = distillation_loss(
student_logits, teacher_logits, y, T, alpha
)
optimizer.zero_grad()
loss.backward()
optimizer.step()
student.eval()
preds, targets = [], []
with torch.no_grad():
for X, y in val_loader:
preds.append(student(X.to(device)).argmax(dim=-1).cpu())
targets.append(y)
f1 = f1_score(
torch.cat(targets), torch.cat(preds), average="weighted"
)
print(f"T={T:>4.1f} val_weighted_f1={f1:.4f}")
if f1 > best_f1:
best_f1, best_T, best_student = f1, T, student
print(f"\nBest temperature: T={best_T}, val_weighted_f1={best_f1:.4f}")
return best_T, best_student
将集成提炼为单一模型

量化集成混合了归纳偏差:订单簿特征上的梯度增强树、最近报价上的 1D-CNN、多时间帧窗口上的转换器、宏观因素上的线性模型。平均比任何单独的成员都更稳定,并且运行所有四个成员会增加延迟和成本 - 使用机器学习的扩展建模 的两阶段拆分通过将慢速成员降级到异步侧通道来处理这种情况。相反,蒸馏将所有四个在热路径中折叠成一个学生。
集成教师的输出是其成员的 softmax 输出的平均值:
其中 是集合成员的数量。学生根据这种平均分布进行训练。
class EnsembleTeacher(nn.Module):
"""Wraps K models, returns averaged logits for distillation."""
def __init__(self, models: list[nn.Module]):
super().__init__()
self.models = nn.ModuleList(models)
@torch.no_grad()
def forward(self, x: torch.Tensor) -> torch.Tensor:
logits = torch.stack([m(x) for m in self.models], dim=0)
return logits.mean(dim=0) # average logits, not softmax
class TradingStudent(nn.Module):
"""Lightweight MLP for sub-millisecond inference."""
def __init__(self, input_dim: int, hidden: int = 64, n_classes: int = 3):
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, hidden),
nn.ReLU(),
nn.BatchNorm1d(hidden),
nn.Linear(hidden, hidden),
nn.ReLU(),
nn.BatchNorm1d(hidden),
nn.Linear(hidden, n_classes),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.net(x)
参数计数不对称是关键所在:对于 60 个特征、3 类任务,具有 64 个隐藏单元的两层 MLP 大约有 8,000 个参数,而与总计数达到数百万的集成相比。
学生保留什么和失去什么
这是承重经验问题,文章没有回答。直觉是,学生跟踪整体分布并在压力状态下下降,其中整体的多样性正在发挥作用,但保留数字仅意味着根据真实订单数据测量的数据,按状态划分,并报告为加权 F1。在平静的日子里坚持下来并在清算级联中崩溃的学生与优雅地降级的学生是不同的产品,并且总数无法区分它们。
三种缓解措施值得针对该测量进行测试,而不是提前断言:
- 在蒸馏集中包含压力期,以便学生看到预计会出现差距的状态。
- 基于特征的蒸馏 - 匹配中间表示,而不仅仅是最终输出。 3.辅助政权头在学生身上,迫使政权意识功能进入共享主干。
##自我升华:当学生成为老师

自蒸馏是一种模型从自身中提取知识的技术。
Born-Again Networks(BAN)
培养一名具有与老师相同的建筑风格的学生。 “重生”的学生通常比原来的学生表现更好,并且这个过程会不断重复:
每一代都针对前一代的软目标进行训练,增益通常在几代后饱和。对于交易模型来说,这在架构上没有任何成本——没有新功能,没有新数据,只是不同的训练程序——这也意味着测试成本低廉,并且没有理由报告未经测试的情况。
深度自蒸馏
在中间层附加辅助分类器。最深的出口是较浅出口的老师。在推理时,您选择一个出口:浅出口可降低延迟,深出口可实现最高准确度。
这是最适合交易系统的想法,因为退出深度成为运行时延迟旋钮:一个经过训练的网络涵盖一系列预算,而不是在训练时致力于单一架构。当书快速移动时,你会选择浅出口并接受更糟糕的后验;当安静的时候,你就需要为完整的深度付出代价。每次退出的准确度和每次退出的延迟曲线都是可测量的,它们的交叉决定了旋钮是否值得拥有。
class SelfDistillingNet(nn.Module):
"""Network with early-exit classifiers for variable-latency inference."""
def __init__(self, input_dim: int, n_classes: int = 3):
super().__init__()
self.block1 = nn.Sequential(
nn.Linear(input_dim, 128), nn.ReLU(), nn.BatchNorm1d(128)
)
self.block2 = nn.Sequential(
nn.Linear(128, 64), nn.ReLU(), nn.BatchNorm1d(64)
)
self.block3 = nn.Sequential(
nn.Linear(64, 32), nn.ReLU(), nn.BatchNorm1d(32)
)
self.exit1 = nn.Linear(128, n_classes)
self.exit2 = nn.Linear(64, n_classes)
self.exit3 = nn.Linear(32, n_classes) # final exit
def forward(
self, x: torch.Tensor, exit_layer: int = 3
) -> torch.Tensor:
h1 = self.block1(x)
if exit_layer == 1:
return self.exit1(h1)
h2 = self.block2(h1)
if exit_layer == 2:
return self.exit2(h2)
h3 = self.block3(h2)
return self.exit3(h3)
def forward_all_exits(self, x: torch.Tensor):
"""Return logits from all exits (for self-distillation training)."""
h1 = self.block1(x)
h2 = self.block2(h1)
h3 = self.block3(h2)
return self.exit1(h1), self.exit2(h2), self.exit3(h3)
def self_distillation_step(
model: SelfDistillingNet,
x: torch.Tensor,
y: torch.Tensor,
temperature: float = 4.0,
alpha: float = 0.5,
) -> torch.Tensor:
"""One training step with self-distillation from deepest exit."""
logits_1, logits_2, logits_3 = model.forward_all_exits(x)
loss_hard = F.cross_entropy(logits_3, y)
loss_distill_1 = distillation_loss(
logits_1, logits_3.detach(), y, temperature, alpha
)
loss_distill_2 = distillation_loss(
logits_2, logits_3.detach(), y, temperature, alpha
)
return loss_hard + 0.5 * loss_distill_1 + 0.5 * loss_distill_2
推理预算从何而来

仅当推理必须位于硬预算内时,蒸馏才有意义,并且完整的交易阶梯 - NIC 到用户空间、内核旁路、低于 100 µs 的总时间以及强制 FPGA 和共享内存的低于 10 µs 的层级 - 已经在算法交易中的数据和通信]中列出。梯子留下的行是模型推理,也就是蒸馏试图填充的行。
拒绝用模型类延迟表填充其他行。 使用机器学习进行传播建模 已经发布了 GBM 与深度学习的比较,以及比数字更重要的警告:延迟取决于实现,并且相同的 LightGBM 模型在 Python 中每行需要数十微秒,但在编译的预测器中则需要几微秒。这里的任何延迟声明都必须指定框架、核心和批量大小,否则就是噪音。
特别是在 GPU 上:每次启动的固定开销必须在设备发挥作用之前进行摊销,并且单行推理位于屋顶线山脊左侧很远的地方,而它从来都不是。 当 GPU 发挥作用时 通过批量扫描正确测量摊销曲线,包括分立 PCIe 卡如何将山脊进一步向右推 — 请阅读该内容,而不是相信内存中引用的常数。
蒸馏后量化
经过精炼的学生会进一步压缩:INT8 权重(在使用 AVX-512 VNNI 的 CPU 上大约压缩 2 倍)、将乘法转换为加法的二进制/三进制权重,以及修剪以跳过近零计算。
诱人的说法是,蒸馏然后量化比单独量化保留了更高的准确性,因为学生已经学习了紧凑的表示。不要在上面运送。 GPU 精度陷阱 是博客在降低数字精度方面的立场:它默默地返回看似合理的垃圾,而使快速路径可交付的是量化的等价门 - 填充移位,以 bps 为单位的 PnL 增量 - 而不是断言。 INT8 学生是一个不同的模型,直到该门针对 FP32 学生进行测量。
import torch.quantization as quant
def quantize_student(student: nn.Module, calibration_loader: DataLoader):
"""Post-training static quantization for CPU deployment."""
student.cpu()
student.eval()
student.qconfig = quant.get_default_qconfig("x86")
student_prepared = quant.prepare(student)
with torch.no_grad():
for X, _ in calibration_loader:
student_prepared(X)
student_quantized = quant.convert(student_prepared)
return student_quantized
FPGA 部署:从蒸馏到比特流的流水线

FPGA 是延迟阶梯 中的低于 10 µs 层,Tbricks/Broadridge 评论 ] 在生产中与内核旁路 NIC 一起涵盖了它们 — 确定性延迟,无操作系统抖动,与网络堆栈位于同一位置。本博客中没有介绍如何将精炼模型转化为模型。
DeepLOB 的生产笔记 将 ONNX/TensorRT、INT8 量化和 FPGA 部署列为三个选项,并就此结束。这就是第三个扩展:
1. Train ensemble teacher (offline, GPU cluster, hours/days)
|
2. Distill to small MLP student (offline, single GPU, minutes)
|
3. Quantize student to INT8 / fixed-point (offline, CPU)
|
4. Convert to HLS (High-Level Synthesis) or RTL
|
5. Synthesize FPGA bitstream (offline, hours)
|
6. Deploy to FPGA card in production server
|
7. Inference: market data -> FPGA -> trading signal
绑定约束是模型必须适合设备的逻辑元素 — LUT、DSP 切片、块 RAM。作为数量级预算而不是测量:具有 64 个隐藏单元和 INT8 权重的 2 层 MLP 约为每次推理 8,000 个乘法累加和约 16 KB 的权重,只是中等部分的一小部分。这就是精炼得以维持的地方——合奏老师不符合任何预算;学生远未达到极限。
将 PyTorch/ONNX 自动化为可合成硬件的工具包括 AMD/Xilinx Vitis AI、hls4ml(来自 CERN)和 FINN(来自 Xilinx Research)。
示例:hls4ml 转换
import hls4ml
import onnx
dummy_input = torch.randn(1, 60) # 60 input features
torch.onnx.export(student, dummy_input, "student.onnx", opset_version=13)
hls_config = hls4ml.utils.config_from_onnx_model(
onnx.load("student.onnx"),
granularity="name",
default_precision="ap_fixed<16,8>",
default_reuse_factor=1, # full parallelism
)
hls_model = hls4ml.converters.convert_from_onnx_model(
"student.onnx",
hls_config=hls_config,
output_dir="hls_student",
backend="VivadoAccelerator",
board="alveo-u250",
)
hls_model.compile()
hls_model.build(csim=True, synth=True)
hls_model.report()
hls_model.report() 是给定型号、电路板、精度和复用系数的资源和延迟数据的唯一可靠来源 — 这些数据仅随着 default_reuse_factor 的变化而大幅变化。引用“典型”综合表而不运行它是猜测。
实际注意事项

预计算教师 logits
蒸馏需要教师对整个训练集进行预测——这是值得刻意付出的一次性离线成本:运行一次集成,保留逻辑,根据缓存训练学生。然后,温度扫描和架构搜索在教师前向传播中不需要花费任何额外费用,这就是使上述扫描变得实用的原因。
一个专用于蒸馏的监视器
特征管道卫生、滚动归一化(因为 z 分数参数漂移)、输入分布偏移监控和状态触发的再训练都包含在 DeepLOB 的生产部分 中,并在此处保持不变。
蒸馏专用的监控器是实时数据上的师生 KL 散度。老师离线仍然存在;在实时输入样本上运行它并比较分布。 KL 的上升意味着学生的近似值在未经过提炼的情况下会下降——并且它会在准确性之前触发,因为它不会等待标签。再训练阈值必须根据已知良好和已知退化期间观察到的 KL 进行校准;先验的选择是任意的。
何时不蒸馏
- 老师已经很小了(线性模型,浅GBM):蒸馏增加了一个管道阶段,不进行压缩。
- 延迟不是限制(每日重新平衡、结束信号):部署教师。
- 可解释性胜过速度:精炼的网络比它所取代的树整体更难解释。
- 两阶段分割已经起作用:如果扩展建模架构中的异步慢速模型正在交付,蒸馏必须在测量比较中击败它,然后才能证明更换工作系统是合理的。
## 概括

蒸馏是两阶段快/慢分割的连贯替代方案:离线训练你能负担得起的最好的教师,将其软目标结构转移到足够小的学生中以适应热路径,量化,部署在CPU或FPGA上。深度方面的变体更进一步,使延迟成为运行时的选择,而不是训练时的选择。
本文故意不声称其中任何内容都优于博客已经发布的内容。该结论需要对真实订单数据进行三项测量:按状态划分的学生与整体加权 F1 保留曲线、温度扫描以及 GPU 精度陷阱 风格的 INT8 奇偶校验门。在这些技术存在之前,这是对技术的描述,而不是部署该技术的建议。
## 参考
-
Hinton, G.、Vinyals, O. 和 Dean, J. (2015)。 提炼神经网络中的知识。 arXiv:1503.02531
-
Furlanello, T.、Lipton, Z. C.、Tschannen, M.、Itti, L. 和 Anandkumar, A. (2018)。 重生的神经网络。 ICML。 arXiv:1805.04770
-
张丽、宋健、高安、陈静、鲍成、马凯(2019)。 做你自己的老师:通过自蒸馏提高卷积神经网络的性能。 ICCV。 arXiv:1905.08094
-
Romero, A.、Ballas, N.、Kahou, S. E.、Chassang, A.、Gatta, C. 和 Bengio, Y. (2015)。 FitNets:薄深网的提示。 ICLR。 arXiv:1412.6550
-
Gou, J.、Yu, B.、Maybank, S. J. 和 Tai, D. (2021)。 知识蒸馏:调查。国际计算机视觉杂志,129,1789-1819。 arXiv:2006.05525
6.杜阿尔特,J.,等人。 (2018)。 用于粒子物理的 FPGA 中深度神经网络的快速推理 (hls4ml)。仪器仪表杂志,13,P07027。 arXiv:1804.06913
-
Umuroglu, Y. 等人。 (2017)。 FINN:快速、可扩展的二值化神经网络推理框架。 FPGA'17。 arXiv:1612.07119
-
张 Z.、Zohren, S. 和罗伯茨, S. (2019)。 DeepLOB:用于限价订单簿的深度卷积神经网络。 IEEE 信号处理汇刊,67(11), 3001-3012。 arXiv:1808.03668
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.