Multi-Task Learning for Simultaneous Price, Volume, and Volatility Prediction
Многозадачное обучение (MTL) обычно рекламируют так: общий кодировщик для коррелирующих целей улучшает основную задачу. В трейдинге коррелирующие цели очевидны — доходность, объем и реализованная волатильность возникают из одного и того же потока ордеров, — но это утверждение почти никогда не проверяют. Важен вопрос не о том, связаны ли задачи, а о том, согласуются ли общие градиенты и что происходит на фолдах, где они не согласуются.
В этой статье в центре внимания находятся две вещи, которые в большинстве материалов о MTL считают второстепенными:
- Балансирование потерь — это эксперимент, а не деталь. Фиксированные веса, взвешивание неопределенности Kendall и GradNorm — три разные модели. Запустите все три на одних и тех же фолдах и для каждой сообщите выученные веса вместе с метрикой основной задачи.
- Отрицательный перенос можно измерить до того, как станет видна метрика. Косинусное сходство градиентов задач общего кодировщика показывает во время обучения, тянут ли вспомогательные задачи представление в сторону, нужную основной задаче. Зафиксируйте знак косинусов, а затем проверьте, предсказал ли он результат на этом фолде.
Все остальное в конвейере — процесс волатильности, цикл обучения, контроль утечек и протокол валидации — уже разобрано в других статьях этого блога и здесь приведено ссылками, без повторного вывода.
Настройка

Пусть входные признаки (OHLCV, технические индикаторы, поток ордеров) задают три цели:
- Задача 1 (основная): доходность следующего периода
- Задача 2 (вспомогательная): логарифм объема следующего периода
- Задача 3 (вспомогательная): реализованная волатильность следующего периода
Многозадачная модель одновременно выдает все три значения, , а многозадачный риск представляет собой взвешенную сумму рисков отдельных задач:
Вся статья посвящена и тому, что градиенты отдельных задач делают друг с другом.
Почему совместное обучение может помочь, в одном абзаце. Вспомогательные задачи заставляют общее представление объяснять больше одного рыночного явления, одновременно выполняя роль ограничения емкости и индуктивного смещения. Кроме того, объем и волатильность наблюдаются напрямую, а "ожидаемая доходность" — нет, поэтому вспомогательные головы дают более чистый градиентный сигнал, чем основная голова. Аргументы в пользу одной модели, выдающей много результатов, подробно разобраны вместе с механизмами интерпретируемости в статье о временных трансформерах слияния для многогоризонтного прогнозирования, где для многогоризонтных квантилей используется тот же подход с общим кодировщиком и несколькими головами.
Архитектура вкратце
Жесткое разделение параметров: общий кодировщик подает данные на голов , предназначенных для отдельных задач, поэтому . Именно этот вариант измеряется здесь, поскольку только в нем конфликт градиентов по определен однозначно.
Мягкое разделение параметров дает каждой задаче собственный кодировщик со штрафом связи : параметров больше, гибкости больше, но нет единого общего вектора параметров, на котором можно измерять конфликт. Сети Cross-stitch занимают промежуточное положение и смешивают признаки отдельных задач через обучаемую матрицу на каждом уровне. Оба подхода стоит попробовать, если жесткое разделение выявит конфликт, но измерение ниже выходит за их рамки.
Главный эксперимент: три схемы балансирования потерь

Наивная функция потерь чувствительна к масштабу. Если потери доходности находятся около , а потери объема — около , объем забирает себе градиент, и голова доходности остается без сигнала. Возможны три ответа:
Фиксированные веса. Установить после стандартизации каждой цели. Это честный базовый вариант: если он победит, адаптивные схемы окажутся лишь формальностью.
Взвешивание неопределенности (Kendall et al., 2018). Для каждой задачи обучать масштаб гомоскедастического шума :
Задачи с высокой неопределенностью автоматически получают меньший вес; член не дает получить тривиальное решение . Обратите внимание: здесь служит для взвешивания потерь во время обучения, а не задает прогнозный интервал. Об оценке неопределенности, с помощью которой можно подобрать размер позиции, см. конформное прогнозирование.
GradNorm (Chen et al., 2018). Балансировать величины градиентов, а не масштабы потерь. На каждом шаге вычислить и среднее , вычислить относительную скорость обучения , а затем обновить . Тогда все задачи обучаются с сопоставимыми скоростями независимо от масштаба потерь.
Специфичный для MTL код — это головы, возвращающий список forward и агрегация потерь. Стек Linear/BatchNorm/ReLU/Dropout, шаблон Adam/cosine/clip и цикл по эпохам — стандартный паттерн из статьи DeepLOB, поэтому здесь они опущены.
import torch
import torch.nn as nn
class MultiTaskTradingModel(nn.Module):
"""Hard parameter sharing: one encoder, K heads."""
def __init__(self, encoder: nn.Module, repr_dim: int, n_tasks: int = 3):
super().__init__()
self.shared_encoder = encoder # any MLP/CNN/GRU trunk
self.task_heads = nn.ModuleList(
nn.Linear(repr_dim, 1) for _ in range(n_tasks)
)
def forward(self, x):
h = self.shared_encoder(x)
return [head(h).squeeze(-1) for head in self.task_heads]
def shared_repr(self, x):
return self.shared_encoder(x)
class UncertaintyWeightedLoss(nn.Module):
"""Kendall et al. (2018) homoscedastic weighting."""
def __init__(self, n_tasks: int = 3):
super().__init__()
self.log_vars = nn.Parameter(torch.zeros(n_tasks)) # log(sigma^2)
def forward(self, losses: list) -> torch.Tensor:
return sum(
torch.exp(-self.log_vars[i]) * loss + self.log_vars[i]
for i, loss in enumerate(losses)
)
def get_weights(self) -> list:
with torch.no_grad():
return [torch.exp(-lv).item() for lv in self.log_vars]
У UncertaintyWeightedLoss есть параметры, поэтому его нужно передать в оптимизатор вместе с моделью: optim.Adam(list(model.parameters()) + list(uw.parameters()), ...). Если этого не сделать, вы получите самый распространенный случай, когда заявлено "взвешивание неопределенности", а на деле незаметно используются фиксированные веса.
Что нужно отчитывать
Для каждой схемы на каждом фолде нужно указать итоговые выученные веса задач, метрику основной задачи и — поскольку схема взвешивания является выбором модели — сколько схем сравнивалось до выбора одной из них.
| Схема | Метрика основной задачи относительно однозадачной модели | |||
|---|---|---|---|---|
| Фиксированные () | 1.00 | 1.00 | 1.00 | — |
| Взвешивание неопределенности | — | — | — | — |
| GradNorm | — | — | — | — |
Три схемы на нескольких фолдах — это уже небольшой поиск модели. Любое представленное здесь улучшение должно пережить поправку на множественное тестирование, описанную в статье о дефлированном коэффициенте Шарпа и множественном тестировании, прежде чем оно будет что-либо значить.
Отрицательный перенос: зафиксируйте градиенты

Это самая полезная часть. Отрицательный перенос возникает, когда вспомогательные задачи ухудшают основную задачу; у него есть прямой диагностический признак — угол между градиентами задач в общем пространстве параметров.
Измеряйте его только на общем кодировщике: головы по определению относятся к разным задачам и всегда тривиально "согласуются".
import torch.nn.functional as F
def shared_grad(model, x, y, task_idx, criterion=nn.MSELoss()):
"""Gradient of task `task_idx` w.r.t. the shared encoder, flattened."""
model.zero_grad(set_to_none=True)
loss = criterion(model(x)[task_idx], y)
loss.backward()
return torch.cat([
p.grad.detach().flatten()
for p in model.shared_encoder.parameters()
if p.grad is not None
])
def task_conflict(model, x, y_by_task, task_names):
"""Pairwise cosine similarity between per-task shared-encoder gradients."""
grads = {
name: shared_grad(model, x, y_by_task[name], i)
for i, name in enumerate(task_names)
}
return {
(a, b): F.cosine_similarity(
grads[a].unsqueeze(0), grads[b].unsqueeze(0)
).item()
for i, a in enumerate(task_names)
for b in task_names[i + 1:]
}
Вызывайте эту функцию на отложенном батче через фиксированные интервалы во время обучения, а не один раз в конце. Пара может начать обучение с согласованными градиентами и разойтись по мере специализации кодировщика; одно итоговое число этого не покажет.
Именно это наблюдение нужно искать и публиковать в любом случае:
| Пара | cos sim, начало обучения | cos sim, конец обучения | MTL помогла основной задаче? |
|---|---|---|---|
| доходность ↔ объем | — | — | — |
| доходность ↔ волатильность | — | — | — |
| объем ↔ волатильность | — | — | — |
Если градиенты объема и волатильности согласуются друг с другом, но оба конфликтуют с градиентом доходности, правильный вывод таков: две вспомогательные задачи образуют связный блок, к которому задача доходности не относится. Исправление здесь — группировка задач, а не увеличение емкости. Если конфликт реален, стандартные способы — PCGrad (Yu et al., 2020), который проецирует каждый конфликтующий градиент на нормальную плоскость другого; CAGrad (Liu et al., 2021), который ищет направление спуска, не ухудшающее ни одну задачу; или полное удаление вспомогательной задачи.
Обратите внимание, чего здесь намеренно нет: графика t-SNE общего представления, раскрашенного по значению цели. Это декоративная визуализация: приведенные выше косинусные числа выражают все, на что могло бы намекать вложение, причем выражают это числами.
Протокол валидации

Описанное выше измерение бесполезно при небрежном протоколе, а MTL усугубляет обычные ловушки: вместо одной цели можно допустить утечку сразу трех.
Реальные данные, а не симулятор. Цели должны быть построены по настоящим данным OHLCV/сделок. Жестко заданная игрушечная модель GARCH генерирует волатильность, коррелирующую с доходностью по построению, а именно это и проверяется. В таком эксперименте измерялся бы собственный генератор. Если нужен подогнанный процесс волатильности, прогнозирование волатильности криптовалюты с помощью GARCH подгоняет GARCH(1,1) методом максимального правдоподобия на реальных BTC/ETH и проверяет стандартизированные остатки, а статья об асимметричном GARCH и эффекте плеча объясняет, почему гауссовский симулятор с симметричной реакцией изначально неверно описывает волатильность криптовалют. Синтетические данные оправданы только тогда, когда дают контролируемую истинную зависимость — известную, заданную автором корреляцию задач, которую вы пытаетесь восстановить. Это другой эксперимент, не тот, что описан здесь.
Масштабаторы обучаются только на train. Подгоняйте масштабатор признаков и все три масштабатора целей внутри каждого обучающего фолда и применяйте их к валидации. Глобальный fit_transform до разделения данных переносит моменты тестовой выборки в обучение. Именно этот сбой разобран в таксономии смещения заглядывания вперед.
Очищенные walk-forward-фолды с эмбарго. Одно хронологическое разделение 80/20 не позволяет отличить улучшение MTL от эффекта конкретного фолда. В этом состоит основной аргумент статьи об оптимизации walk-forward, где три разбиения дают три вывода. Повторно используйте генератор расширяющегося окна purged_walk_forward из статьи о моделировании спреда с помощью машинного обучения: он оставляет зазор длиной horizon по обе стороны каждой границы. Это важно, поскольку перекрывающиеся окна реализованной волатильности протекают через границу даже тогда, когда цель доходности этого не делает.
Классический базовый уровень. Сеть MTL, превосходящая три однозадачные сети, ничего не доказала, если градиентный бустинг или ridge-модель для отдельных целей превосходит все четыре. Обучите по одной модели для каждой цели с LightGBM или ridge на тех же фолдах и признаках и добавьте результаты в ту же таблицу.
| Модель | Метрика основной задачи | Примечания |
|---|---|---|
| Ridge, по одной на цель | — | Классический базовый уровень |
| LightGBM, по одной на цель | — | Классический базовый уровень |
| Однозадачный MLP, по одной на цель | — | Три отдельные сети |
| MTL, лучшая схема потерь | — | Одна сеть, три головы |
Когда MTL здесь действительно оправдана

Условия, при которых MTL должна победить; это гипотезы для проверки на фолдах выше, а не готовый чек-лист:
- Вспомогательные метки чище основной метки. Объем наблюдается напрямую, а "ожидаемая доходность" — нет. Если голова доходности в основном подгоняет шум, градиентный сигнал вспомогательных голов — единственная корректно поставленная часть целевой функции.
- Обучающих данных мало относительно емкости кодировщика, поэтому вспомогательное ограничение действительно выполняет работу по регуляризации, а не просто конкурирует за параметры.
- Важна задержка вывода, и один прямой проход лучше трех.
Есть и столь же проверяемый аргумент против: если измеренные значения cos_sim(return, ·) постоянно отрицательны, общий кодировщик тянут в сторону от основной задачи, а вспомогательные головы становятся налогом, а не регуляризатором.
Заключение

Доходность, объем и волатильность возникают из одной микроструктуры, поэтому общее представление — разумный априор. Но априор не является результатом. Эта постановка действительно может установить две вещи: какую схему балансирования потерь предпочитают данные (с указанием выученных весов, а не только имени победителя) и согласуются ли градиенты задач общего кодировщика, измеренные на протяжении обучения, а не предполагаемые лишь потому, что цели коррелируют.
Если очищенные walk-forward-фолды покажут, что сеть MTL не превосходит градиентный бустинг для отдельных целей, это и есть результат, который нужно так и опубликовать; шаблон дает статья честный отрицательный результат. Отрицательный результат об отрицательном переносе все равно остается результатом об отрицательном переносе.
Авторы
Инженер торговых систем
Разработка торговых ботов с 2017 года: межбиржевой арбитраж (подключал до 30 бирж), парный арбитраж на коинтеграции между спотом и фьючерсами, скальпинг, фронтраннинг, торговля по новостям, сентиментный анализ, трендовые алгоритмы, а также алгоритмы управления и балансировки портфелей. Делает выставление ордеров до 1 мс, warehouse для big data, бэктестинг-движки, AI-агентов и интерфейсы для ботов (в т.ч. open-source profitmaker.cc). Стек: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, архитектура.