Knowledge Distillation: Compressing Trading Models for Low-Latency Deployment
Ответ на проблему точности и задержки в торговле, основанной на машинном обучении, уже опубликован в этом блоге. Моделирование распространения с помощью машинного обучения рекомендует двухэтапное разделение: модель с быстрым повышением градиента выполняет критическую по задержке котировку в реальном времени, тогда как глубокая модель работает асинхронно и подает на нее вторичный сигнал или корректирует ее параметры. Две модели, два часа, одна система.
Дистилляция знаний — это другой ответ на ту же самую напряженность. Вместо того, чтобы запускать медленную модель вместе с быстрой, вы используете ее один раз в автономном режиме для обучения быстрой — ученик изучает полное распределение вероятностей учителя по результатам, а не только жесткие метки, а затем учитель полностью уходит с горячего пути. Одна модель во время вывода, без асинхронной связи, без окна устаревания.
Какой ответ победит, зависит эмпирически, и эта статья еще не дает на него ответа. Далее следует механизм плюс подробное описание измерений, которые будут определять этот вопрос. Ничто здесь не является эталонным результатом; там, где обычно идет число, есть маркер, указывающий, что нужно запустить.
Одно исправление кадра заранее, из DeepLOB и глубокое обучение в книге заказов: высокая точность классификации не приводит автоматически к прибыли — прогнозируемое движение должно устранить разницу между ценами спроса и предложения. Поэтому "сохранять точность направления учителя" — это неправильный подход к оптимизации установки для дистилляции.
Система "учитель-ученик"

Исходная формулировка Хинтона, Виньялса и Дина (2015) проста. У вас есть модель учителя (большая, медленная, точная) и модель ученика (маленькая, быстрая, обучаемая). Ученик учится одновременно по двум сигналам:
- Жесткие цели: истинные метки (например, цена выросла или упала)
- Мягкие цели: распределение вероятностей результатов работы учителя по всем классам
Функция потерь ученика сочетает оба сигнала: где и — логиты учителя и ученика, — функция softmax, — температурный параметр, а регулирует баланс между двумя компонентами потерь.
Почему мягкие цели важны для торговли
Трехклассовая схема средней цены "вверх/без изменений/вниз", пороговая разметка и объяснение того, почему возникающий дисбаланс требует отчитываться о взвешенном F1, а не о точности, уже приведены в DeepLOB — будем считать, что здесь используется та же схема меток. Специфика дистилляции в том, что учитель выдает результат до argmax: жесткая метка "вверх" несет один бит, тогда как 0,72/0,21/0,07 также говорит, что движение может остановиться и почти наверняка не развернется. Такая структура взаимосвязей между классами дает дополнительный обучающий сигнал, поэтому ученик с мягкими целями может обобщать лучше, чем тот же ученик, обученный только на метках.
Важно понимать, чем такая уверенность не является. Выход Softmax не дает калиброванной оценки неопределенности, поэтому использовать значения 0,55 и 0,85 как вход для определения размера позиции — неверный подход. Именно для отказа от такого упрощения существует конформный прогноз для торговли: размер позиции определяется шириной интервала, соотношением сторон и фильтром запрета торговли, когда интервал пересекает ноль, а необработанный softmax ничего этого не дает. Чтобы обосновать использование уверенности для определения размера позиции, нужно сравнить калибровку ученика с калибровкой учителя (диаграмма надежности, ECE) и показать, что дистилляция ее сохраняет. В этой статье такого результата пока нет.
Температура и мягкие цели

Температурный параметр регулирует "мягкость" распределения вероятностей. Для логитов softmax с температурой имеет вид: Когда (стандартный softmax), распределение имеет пик — доминирующий класс получает большую часть вероятностной массы. При увеличении распределение выравнивается и яснее показывает относительные величины логитов.
| Температура | Эффект | Вариант использования |
|---|---|---|
| Стандартный софтмакс, пиковый | Нормальный вывод | |
| Умеренное смягчение | Общая дистилляция | |
| Сильное смягчение | Когда учитель очень уверен в себе | |
| Почти единообразный | Редко полезно, размывает сигнал |
Существует правдоподобный аргумент в пользу того, что торговые модели требуют умеренной температуры: финансовые прогнозы гораздо менее надежны, чем классификация изображений, поэтому учитель может вывести 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)
Смысл именно в асимметрии числа параметров: двухслойный MLP с 64 скрытыми блоками имеет около 8000 параметров для задачи с 60 признаками и тремя классами, тогда как общее число параметров ансамбля исчисляется миллионами.
Что ученик сохраняет и что теряет
Это существенный эмпирический вопрос, и статья на него не отвечает. Интуитивно ученик должен отслеживать распределение ансамбля внутри обучающего распределения, но терять точность в стрессовых режимах, где разнообразие ансамбля играет ключевую роль. Однако показатель удержания имеет смысл только при измерении на реальных данных книги ордеров, разделенных по режимам, с отчетом о взвешенном F1. Ученик, который держится в спокойные дни и терпит крах во время каскада ликвидаций, — совсем другой продукт по сравнению с учеником, который деградирует плавно, а агрегированное число не позволяет отличить их друг от друга.
Три способа смягчить последствия стоит тестировать на основе этих измерений, а не утверждать заранее:
- Включать стрессовые периоды в набор для дистилляции, чтобы ученик видел режимы, в которых ожидается разрыв.
- Дистилляция на основе признаков — сопоставлять промежуточные представления, а не только конечные результаты.
- Вспомогательную голову режима в ученике, принуждая общую часть сети формировать признаки с учетом режима.
Самодистилляция: когда ученик становится учителем

Самодистилляция — это метод, при котором модель извлекает знания из себя.
Возрожденные сети (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
Откуда берется бюджет вывода

Дистилляция имеет смысл только тогда, когда вывод должен укладываться в жесткий бюджет. Полная лестница "тик — сделка" — от сетевого адаптера до пользовательского пространства, с обходом ядра, общим временем менее 100 мкс и уровнем менее 10 мкс, требующим FPGA и общей памяти, — уже изложена в статье Данные и связь в алгоритмическом трейдинге. Оставшийся в этой лестнице участок — вывод модели; именно его и пытается сократить дистилляция.
Не стоит заполнять оставшиеся участки таблицей задержек для разных классов моделей. В статье Моделирование распространения с помощью машинного обучения уже опубликовано сравнение GBM и глубокого обучения, а также более важное предостережение: задержка зависит от реализации. Одна и та же модель LightGBM может занимать десятки микросекунд на одну строку при запуске из Python и несколько микросекунд из скомпилированного предиктора. Любое заявление о задержке должно указывать реализацию, ядро и размер пакета, иначе это будет просто шум.
Что касается GPU: фиксированные накладные расходы каждого запуска нужно амортизировать, прежде чем устройство вообще начнет помогать, а вывод для одной строки находится далеко слева от вершины roofline. В статье Когда графический процессор окупится кривая амортизации правильно измеряется перебором размеров пакета, включая влияние дискретной карты PCIe, сдвигающей вершину вправо. На эти измерения стоит опираться, а не на константу, взятую из памяти.
Квантование после дистилляции
Дистиллированную модель можно сжать еще сильнее: использовать веса INT8 (примерно двукратное ускорение на ЦП с AVX-512 VNNI), двоичные или троичные веса, превращающие умножения в сложения, и прореживание, позволяющее пропускать почти нулевые вычисления.
Заманчиво утверждать, что дистилляция с последующим квантованием сохраняет больше точности, чем одно квантование, поскольку ученик уже выучил компактное представление. Не полагайтесь на это без проверки. Точная ловушка графического процессора — это постоянное напоминание блога о сниженной числовой точности: она может незаметно вернуть правдоподобный на вид мусор. Быстрый путь делает возможным не утверждение, а количественный порог эквивалентности — доля сместившихся сделок и изменение 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 мкс в лестнице задержек, а в обзоре Tbricks/Broadridge они рассматриваются в рабочей среде вместе с сетевыми адаптерами с обходом ядра: детерминированная задержка, отсутствие дрожания ОС и размещение рядом с сетевым стеком. Но в блоге пока не разобрано, как очищенная модель попадает на FPGA.
Производственные заметки 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 и блочную память. Это оценка порядка величины, а не измерение: двухслойный MLP с 64 скрытыми блоками и весами INT8 требует около 8000 операций умножения с накоплением на один вывод и примерно 16 КБ под веса, то есть занимает небольшую долю ресурсов платы среднего класса. Именно здесь дистилляция оправдывает себя: ансамбль учителей не вписывается в бюджет, а ученик остается далеко от предела.
К инструментам, которые автоматизируют 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. Цитирование "типичной" таблицы синтеза без фактического запуска — это предположение.
Практические соображения

Предварительное вычисление логитов учителя
Для дистилляции нужны прогнозы учителя по всему обучающему набору. Это разовая офлайн-затрата, которую стоит принять осознанно: запустите ансамбль один раз, сохраните логиты и обучайте учеников на этом кэше. Измерение температуры и поиск архитектуры не требуют дополнительных проходов учителя, поэтому описанные выше проверки вполне практичны.
Монитор, специфичный для дистилляции
Гигиена конвейера признаков, скользующая нормализация из-за дрейфа параметров z-оценки, мониторинг сдвига входного распределения и переобучение по режиму — все это описано в разделе производства DeepLOB и без изменений применимо здесь.
Специфичный для дистилляции монитор — это расхождение KL между учителем и учеником на реальных данных. Учитель по-прежнему существует офлайн: запустите его на выборке реальных входов и сравните распределения. Рост KL означает, что аппроксимация ученика ухудшается в режимах, на которых он не был очищен. Такой сигнал срабатывает раньше, чем падение точности, потому что ему не нужны метки. Порог переобучения следует калибровать по наблюдаемому KL в заведомо хороших и заведомо ухудшенных периодах, а не выбирать априори.
Когда не следует выполнять дистилляцию
- Учитель уже мал (линейная модель, неглубокий ГБМ): дистилляция добавляет этап конвейера без сжатия.
- Задержка не является ограничением (ежедневная ребалансировка, сигналы конца дня): используйте учителя.
- Интерпретируемость опережает скорость: очищенную сеть труднее объяснить, чем ансамбль деревьев, который она заменила.
- Двухэтапное разделение уже работает: если асинхронная медленная модель в архитектуре моделирования распространения работает, дистилляция должна превзойти ее при измерении сравнения, прежде чем она оправдает замену работающей системы.
Резюме

Дистилляция — это прямая альтернатива двухэтапному разделению "быстрый/медленный": обучите лучшего учителя, которого можете позволить себе в автономном режиме, перенесите его "мягкие цели" в ученика, достаточно маленького для "горячего пути", выполните квантование и разверните модель на ЦП или FPGA. Вариант с разной глубиной идет дальше и превращает задержку в выбор во время выполнения, а не во время обучения.
В этой статье намеренно не утверждается, что дистилляция превосходит уже опубликованный в блоге подход. Для такого вывода нужны три измерения на реальных данных книги ордеров: взвешенная кривая удержания F1 для ученика и ансамбля с разбивкой по режимам, перебор температур и контрольный порог эквивалентности INT8 в стиле точной ловушки графического процессора. Пока этих измерений нет, это описание метода, а не рекомендация к применению.
Ссылки
-
Хинтон Г., Виньялс О. и Дин Дж. (2015). Извлечение знаний в нейронной сети. arXiv:1503.02531
-
Фурланелло Т., Липтон З.К., Чаненнен М., Итти Л. и Анандкумар А. (2018). Возрожденные нейронные сети. ИКМЛ. arXiv:1805.04770
-
Чжан Л., Сун Дж., Гао А., Чен Дж., Бао К. и Ма К. (2019). Будьте сами себе учителем: улучшите производительность сверточных нейронных сетей с помощью самодистилляции. ICCV. arXiv:1905.08094
-
Ромеро А., Баллас Н., Каху С.Э., Чассан А., Гатта К. и Бенджио Ю. (2015). FitNets: Советы по использованию тонких и глубоких сетей. ICLR. arXiv:1412.6550
-
Гоу Дж., Ю Б., Мэйбанк С.Дж. и Тао Д. (2021). Дистилляция знаний: опрос. Международный журнал компьютерного зрения, 129, 1789–1819 гг. arXiv:2006.05525
-
Дуарте Дж. и др. (2018). Быстрый вывод глубоких нейронных сетей в FPGA для физики элементарных частиц (hls4ml). Журнал приборостроения, 13, P07027. arXiv:1804.06913
-
Умуроглу Ю. и др. (2017). FINN: платформа для быстрого и масштабируемого бинаризованного вывода нейронных сетей. ПЛИС '17. arXiv:1612.07119
-
Чжан З., Зорен С. и Робертс С. (2019). DeepLOB: глубокие сверточные нейронные сети для книг лимитных ордеров. Транзакции IEEE по обработке сигналов, 67 (11), 3001-3012. arXiv:1808.03668
Авторы
Инженер торговых систем
Разработка торговых ботов с 2017 года: межбиржевой арбитраж (подключал до 30 бирж), парный арбитраж на коинтеграции между спотом и фьючерсами, скальпинг, фронтраннинг, торговля по новостям, сентиментный анализ, трендовые алгоритмы, а также алгоритмы управления и балансировки портфелей. Делает выставление ордеров до 1 мс, warehouse для big data, бэктестинг-движки, AI-агентов и интерфейсы для ботов (в т.ч. open-source profitmaker.cc). Стек: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, архитектура.