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.