← กลับไปยังบทความ
August 15, 2026
อ่าน 5 นาที

Multi-Task Learning for Simultaneous Price, Volume, and Volatility Prediction

Multi-Task Learning for Simultaneous Price, Volume, and Volatility Prediction
#deep-learning
#multi-task
#shared-representation
#prediction
#auxiliary

โดยทั่วไป Multi-task learning (MTL) มักถูกนำเสนอด้วยคำกล่าวว่า: การแชร์ encoder ระหว่างเป้าหมายที่สัมพันธ์กันจะช่วยให้งานหลักดีขึ้น ในการเทรด เป้าหมายที่สัมพันธ์กันเห็นได้ชัด — ผลตอบแทน ปริมาณ และความผันผวนที่เกิดขึ้นจริงล้วนมาจาก order flow เดียวกัน — แต่แทบไม่เคยทดสอบคำกล่าวนี้ คำถามที่น่าสนใจไม่ใช่ว่างานต่าง ๆ สัมพันธ์กันหรือไม่ แต่คือ gradient ที่แชร์กัน เห็นพ้องกัน หรือไม่ และเกิดอะไรขึ้นใน fold ที่พวกมันไม่เห็นพ้อง

บทความนี้ยกสองเรื่องขึ้นมาเป็นแกนกลาง ซึ่งบทความ MTL ส่วนใหญ่มักปฏิบัติต่อมันเป็นเพียงเชิงอรรถ:

  1. การปรับสมดุล loss คือการทดลอง ไม่ใช่รายละเอียด น้ำหนักคงที่ การถ่วงน้ำหนักความไม่แน่นอนแบบ Kendall และ GradNorm คือโมเดลสามแบบที่แตกต่างกัน รันทั้งสามแบบบน fold เดียวกัน และรายงานน้ำหนักที่เรียนรู้พร้อม metric ของงานหลักสำหรับแต่ละแบบ
  2. ตรวจวัด negative transfer ได้ก่อนเห็น metric cosine similarity ระหว่าง gradient ของงานบน encoder ที่แชร์จะบอกคุณระหว่างการฝึกว่า auxiliary tasks กำลังดึง representation ไปในทิศทางที่งานหลักต้องการหรือไม่ ใส่เครื่องหมายให้ cosine แล้วตรวจว่าทิศทางนั้นทำนายผลลัพธ์ใน fold นั้นได้หรือไม่

ทุกอย่างที่เหลือใน pipeline — กระบวนการความผันผวน ลูปการฝึก การควบคุม leakage และ validation protocol — มีอธิบายไว้แล้วที่อื่นในบล็อกนี้ และใส่ลิงก์ไว้แทนการอนุมานใหม่

การตั้งค่า

เป้าหมายตลาดสามแบบที่เกิดจาก representation แฝงร่วมกัน

เมื่อมีฟีเจอร์อินพุต xRd\mathbf{x} \in \mathbb{R}^d (OHLCV, technical indicators, order flow) มีเป้าหมายสามอย่าง:

  • งานที่ 1 (หลัก): ผลตอบแทนช่วงถัดไป y(1)=rt+1y^{(1)} = r_{t+1}
  • งานที่ 2 (เสริม): log volume ของช่วงถัดไป y(2)=logVt+1y^{(2)} = \log V_{t+1}
  • งานที่ 3 (เสริม): realized volatility ของช่วงถัดไป y(3)=σt+1y^{(3)} = \sigma_{t+1}

โมเดลหลายงานสร้างผลลัพธ์ทั้งสามพร้อมกัน y^=fθ(x)\hat{\mathbf{y}} = f_\theta(\mathbf{x}) และความเสี่ยงของหลายงานคือผลรวมถ่วงน้ำหนักของความเสี่ยงรายงาน:

RMTL(θ)=k=1KwkE[(k)(fθ(k)(x),y(k))]\mathcal{R}_{\text{MTL}}(\theta) = \sum_{k=1}^{K} w_k \cdot \mathbb{E}\bigl[\ell^{(k)}(f_\theta^{(k)}(\mathbf{x}), y^{(k)})\bigr]

บทความทั้งหมดเกี่ยวกับ wkw_k และสิ่งที่ gradient ของแต่ละงานทำต่อกัน

ทำไมการฝึกร่วมกันอาจช่วยได้ ในหนึ่งย่อหน้า Auxiliary tasks บังคับให้ representation ที่แชร์กันอธิบายปรากฏการณ์ตลาดมากกว่าหนึ่งอย่าง ซึ่งเป็นทั้งการควบคุม capacity และ inductive bias ในเวลาเดียวกัน และเนื่องจากปริมาณกับความผันผวนถูกสังเกตโดยตรง ขณะที่ "expected return" ไม่ได้ถูกสังเกต หัวข้อเสริมจึงให้สัญญาณ gradient ที่สะอาดกว่าหัวข้อหลัก เหตุผลสำหรับโมเดลเดียวที่ปล่อยเอาต์พุตหลายแบบมีอธิบายอย่างละเอียด — พร้อมเครื่องมือด้าน interpretability — ใน temporal fusion transformers สำหรับการพยากรณ์หลายช่วงเวลา ซึ่งใช้เหตุผล shared-encoder-many-heads เดียวกันกับ quantiles หลายช่วงเวลา

สถาปัตยกรรมโดยย่อ

การแชร์พารามิเตอร์แบบแข็ง: encoder ร่วม gϕg_\phi ป้อนหัวเฉพาะงาน KK หัว hψkh_{\psi_k} ดังนั้น y^(k)=hψk(gϕ(x))\hat{y}^{(k)} = h_{\psi_k}(g_\phi(\mathbf{x})) นี่คือเวอร์ชันที่วัดในบทความนี้ เพราะเป็นเวอร์ชันที่ gradient conflict บน ϕ\phi นิยามได้ชัดเจน

การแชร์พารามิเตอร์แบบนุ่ม ให้แต่ละงานมี encoder ของตัวเองพร้อม coupling penalty λkjϕkϕj2\lambda \sum_{k \neq j} \|\phi_k - \phi_j\|^2 — พารามิเตอร์มากขึ้น ยืดหยุ่นมากขึ้น และไม่มีเวกเตอร์พารามิเตอร์ร่วมเพียงชุดเดียวให้วัด conflict เครือข่าย Cross-stitch อยู่ตรงกลาง โดยผสมฟีเจอร์รายงานผ่านเมทริกซ์ที่เรียนรู้ได้ αkj\alpha_{kj} ในแต่ละระดับ ทั้งสองแบบควรลองหาก hard sharing แสดง conflict และทั้งสองอยู่นอกขอบเขตการวัดด้านล่าง

การทดลองสำคัญ: วิธีปรับสมดุล loss สามแบบ

gradient ของงานที่แข่งขันกันถูกปรับสมดุลรอบแกนกลาง neural ร่วม

loss แบบ naive L=kwkL(k)\mathcal{L} = \sum_k w_k \mathcal{L}^{(k)} ไวต่อสเกล หาก loss ของผลตอบแทนอยู่ราว 0.010.01 และ loss ของปริมาณอยู่ราว 1.01.0 ปริมาณจะครอง gradient และหัวผลตอบแทนจะอดสัญญาณ มีคำตอบสามแบบ:

น้ำหนักคงที่ ตั้ง wk=1w_k = 1 หลัง standardize เป้าหมายทุกตัว นี่คือ baseline ที่ซื่อสัตย์ — หากชนะ วิธีแบบปรับตัวก็เป็นเพียงพิธีกรรม

การถ่วงน้ำหนักความไม่แน่นอน (Kendall et al., 2018) เรียนรู้สเกล noise แบบ homoscedastic σk\sigma_k ต่องาน:

LMTL=k=1K12σk2L(k)+logσk\mathcal{L}_{\text{MTL}} = \sum_{k=1}^{K} \frac{1}{2\sigma_k^2} \mathcal{L}^{(k)} + \log \sigma_k

งานที่ไม่แน่นอนสูงจะถูกลดน้ำหนักโดยอัตโนมัติ ส่วนพจน์ logσk\log \sigma_k ป้องกันคำตอบแบบง่าย σk\sigma_k \to \infty โปรดสังเกตว่า σk\sigma_k นี้เป็นอุปกรณ์ถ่วงน้ำหนัก loss ตอนฝึก ไม่ใช่ predictive interval — สำหรับความไม่แน่นอนที่ใช้กำหนดขนาด position ได้จริง ดู conformal prediction

GradNorm (Chen et al., 2018) ปรับสมดุล ขนาด gradient แทนสเกล loss ในแต่ละขั้น: คำนวณ Gk=ϕwkL(k)2G_k = \|\nabla_\phi w_k \mathcal{L}^{(k)}\|_2 และค่าเฉลี่ย Gˉ\bar{G} คำนวณอัตราการฝึกสัมพัทธ์ r~k=L(k)(t)/L(k)(0)rˉ\tilde{r}_k = \frac{\mathcal{L}^{(k)}(t)/\mathcal{L}^{(k)}(0)}{\bar{r}} แล้วอัปเดต wkwkηwwkkGkGˉr~kαw_k \leftarrow w_k - \eta_w \nabla_{w_k} \sum_k |G_k - \bar{G} \cdot \tilde{r}_k^\alpha| จากนั้นทุกงานจะฝึกด้วยอัตราใกล้เคียงกันโดยไม่ขึ้นกับสเกล loss

โค้ดเฉพาะของ MTL คือหัวต่าง ๆ, forward ที่คืนค่าเป็นลิสต์ และการรวม loss ส่วน stack ของ Linear/BatchNorm/ReLU/Dropout, boilerplate ของ Adam/cosine/clip และลูป epoch เป็นรูปแบบมาตรฐานที่แสดงใน 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 มีพารามิเตอร์ จึงต้องใส่ใน optimizer ร่วมกับโมเดล: optim.Adam(list(model.parameters()) + list(uw.parameters()), ...) การลืมเรื่องนี้เป็นวิธีที่พบบ่อยที่สุดในการ "รัน uncertainty weighting" แต่จริง ๆ แล้วเงียบ ๆ กลับรันน้ำหนักคงที่

สิ่งที่ต้องรายงาน

สำหรับแต่ละวิธี ในแต่ละ fold: น้ำหนักงานสุดท้ายที่เรียนรู้ metric ของงานหลัก และ — เพราะวิธีถ่วงน้ำหนักเป็นตัวเลือกของโมเดล — จำนวนวิธีที่เปรียบเทียบก่อนเลือกหนึ่งวิธี

วิธี wreturnw_{\text{return}} wvolumew_{\text{volume}} wvolw_{\text{vol}} metric งานหลักเทียบกับงานเดี่ยว
คงที่ (wk=1w_k = 1) 1.00 1.00 1.00
ถ่วงน้ำหนักความไม่แน่นอน
GradNorm

สามวิธีคูณด้วยหลาย fold ก็เป็นการค้นหาโมเดลขนาดเล็กแล้ว การปรับปรุงใด ๆ ที่รายงานตรงนี้ต้องผ่านการแก้ multiple-testing ตามที่อธิบายใน deflated Sharpe และ multiple testing ก่อนจึงจะมีความหมาย

Negative Transfer: ใส่เครื่องหมายให้ Gradient

gradient ของงานที่สอดคล้องและขัดแย้งกันที่ representation ร่วม

นี่คือส่วนที่ควรเก็บไว้ Negative transfer เกิดขึ้นเมื่อ auxiliary tasks ทำให้งานหลักแย่ลง และมี diagnostic โดยตรง: มุมระหว่าง gradient ของงานในพื้นที่พารามิเตอร์ร่วม

cos(ϕL(k),ϕL(j))<0    conflicting tasks\cos\bigl(\nabla_\phi \mathcal{L}^{(k)}, \nabla_\phi \mathcal{L}^{(j)}\bigr) < 0 \implies \text{conflicting tasks}

วัดบน encoder ร่วมเท่านั้น — หัวแต่ละงานเป็นของงานนั้นโดยการออกแบบ และจะ "เห็นพ้อง" กันอย่างไร้ความหมายเสมอ

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:]
    }

เรียกใช้สิ่งนี้กับ held-out batch ตาม cadence คงที่ระหว่างการฝึก ไม่ใช่แค่ครั้งเดียวตอนจบ คู่หนึ่งอาจเริ่มสอดคล้องแล้วแยกออกเมื่อ encoder เชี่ยวชาญขึ้น ตัวเลขเดียวตอนจบการฝึกจะซ่อนสิ่งนี้ไว้

สิ่งที่ควรมองหา — และควรเผยแพร่ไม่ว่าจะได้ผลแบบใด:

คู่ cos sim ช่วงต้นการฝึก cos sim ช่วงท้ายการฝึก MTL ช่วยงานหลักหรือไม่?
ผลตอบแทน ↔ ปริมาณ
ผลตอบแทน ↔ ความผันผวน
ปริมาณ ↔ ความผันผวน

หาก gradient ของปริมาณและความผันผวนสอดคล้องกัน แต่ทั้งคู่ขัดแย้งกับ gradient ของผลตอบแทน ข้อสรุปที่ถูกต้องคือ auxiliary tasks สองงานรวมกันเป็นบล็อกที่สอดคล้องกัน ซึ่งงานผลตอบแทนไม่ได้เป็นสมาชิก — วิธีแก้คือจัดกลุ่มงาน ไม่ใช่เพิ่ม capacity เมื่อ conflict เกิดขึ้นจริง วิธีมาตรฐานคือ PCGrad (Yu et al., 2020) ซึ่งฉาย gradient ที่ขัดแย้งลงบนระนาบปกติของ gradient อีกตัว; CAGrad (Liu et al., 2021) ซึ่งค้นหาทิศทางลงที่ไม่ทำร้ายงานใด; หรือการทิ้ง auxiliary task ทั้งหมด

สังเกตสิ่งที่จงใจไม่มี: พล็อต t-SNE ของ representation ร่วมที่ลงสีตามค่าเป้าหมาย มันเป็นของตกแต่ง — ตัวเลข cosine ด้านบนบอกทุกอย่างที่ embedding จะสื่อ และบอกเป็นตัวเลข

โปรโตคอลการตรวจสอบ

หน้าต่าง validation แบบ walk-forward ที่แยกจากกันบนไทม์ไลน์วิจัย

การวัดด้านบนไร้ค่าภายใต้โปรโตคอลที่หย่อนยาน และ MTL ทำให้กับดักทั่วไปแย่ลงเพราะมีเป้าหมายให้ leakage สามเป้าหมายแทนที่จะเป็นหนึ่ง

ข้อมูลจริง ไม่ใช่ simulator เป้าหมายต้องมาจากข้อมูล OHLCV/trade จริง GARCH toy ที่ hardcode จะสร้างความผันผวนซึ่งสัมพันธ์กับผลตอบแทน โดยการก่อสร้าง ซึ่งเป็นสิ่งที่กำลังทดสอบพอดี — การทดลองจะกำลังวัดตัวสร้างของมันเอง หากต้องการกระบวนการความผันผวนที่ fit แล้ว การพยากรณ์ GARCH volatility สำหรับคริปโต จะ fit GARCH(1,1) ด้วย maximum likelihood บน BTC/ETH จริงและตรวจสอบ standardized residuals ส่วน GARCH แบบไม่สมมาตรและ leverage effect อธิบายว่าทำไม simulator การตอบสนองแบบ Gaussian symmetric จึงระบุความผันผวนของคริปโตผิดตั้งแต่ต้น ข้อมูลสังเคราะห์ปกป้องได้ก็ต่อเมื่อให้ ground truth ที่ควบคุมได้ — correlation ของงานที่ผู้เขียนกำหนดและพยายามกู้คืน — ซึ่งเป็นการทดลองคนละแบบกับที่นี่

ฟิต scaler จาก train เท่านั้น ฟิต feature scaler และ target scaler ทั้งสามภายในแต่ละ training fold แล้วนำไปใช้กับ validation การทำ fit_transform ระดับ global ก่อน split จะรั่ว moments ของ test set เข้าไปใน training ความผิดพลาดนี้มีบันทึกไว้ใน taxonomy ของ look-ahead bias

Fold walk-forward แบบ purge และ embargo chronological split เดียว 80/20 แยกไม่ออกว่าการปรับปรุงของ MTL มาจาก fold effect หรือไม่ — นี่คือข้อโต้แย้งทั้งหมดของ walk-forward optimization ซึ่งแสดงว่า 3 split ให้ข้อสรุป 3 แบบ ให้นำ generator purged_walk_forward แบบ expanding-window จาก spread modeling ด้วย machine learning กลับมาใช้: มันทิ้งช่องว่าง horizon rows ทั้งสองข้างของแต่ละ boundary ซึ่งสำคัญเพราะหน้าต่าง realized volatility ที่ซ้อนกันรั่วข้าม boundary แม้ target ผลตอบแทนจะไม่รั่ว

Baseline แบบคลาสสิก เครือข่าย MTL ที่ชนะเครือข่าย single-task สามเครือข่ายยังพิสูจน์อะไรไม่ได้ หากโมเดล gradient boosting หรือ ridge แยกตามเป้าหมายชนะทั้งสี่โมเดล ให้ฟิตโมเดลหนึ่งตัวต่อเป้าหมายด้วย LightGBM หรือ ridge บน fold และฟีเจอร์เดียวกัน แล้วรายงานในตารางเดียวกัน

โมเดล metric งานหลัก หมายเหตุ
Ridge ต่อเป้าหมาย baseline แบบคลาสสิก
LightGBM ต่อเป้าหมาย baseline แบบคลาสสิก
MLP แบบ single-task ต่อเป้าหมาย สามเครือข่ายแยกกัน
MTL วิธี loss ที่ดีที่สุด หนึ่งเครือข่าย สามหัว

เมื่อใด MTL จึงคุ้มค่าที่นี่

เกณฑ์การตัดสินใจที่วัดได้สำหรับความซับซ้อนของโมเดลหลายงาน

เงื่อนไขที่ MTL ควรชนะ เขียนเป็นสมมติฐานให้ตรวจสอบกับ fold ด้านบน ไม่ใช่รายการตรวจสอบ:

  • ป้ายกำกับเสริมสะอาดกว่าป้ายกำกับหลัก ปริมาณถูกสังเกตโดยตรง แต่ "expected return" ไม่ได้ หากหัวผลตอบแทนกำลัง fit noise เป็นส่วนใหญ่ สัญญาณ gradient จากหัวเสริมคือส่วนเดียวของ objective ที่วางปัญหาได้ดี
  • ข้อมูลฝึกมีจำกัดเมื่อเทียบกับ capacity ของ encoder ดังนั้นข้อจำกัดเสริมจึงทำหน้าที่ regularization จริง แทนที่จะแย่งพารามิเตอร์กันเท่านั้น
  • เวลาแฝงของการอนุมานสำคัญ และ forward pass เดียวชนะสามครั้ง

และข้อโต้แย้งฝั่งตรงข้ามก็ตรวจสอบได้เช่นกัน: หากค่า cos_sim(return, ·) ติดลบอย่างต่อเนื่อง encoder ร่วมกำลังถูกดึงออกจากงานหลัก และหัวเสริมเป็นภาษี ไม่ใช่ regularizer

บทสรุป

สตรีมการพยากรณ์ที่ประสานกันคลี่คลายเป็นบทสรุปหลายงาน

ผลตอบแทน ปริมาณ และความผันผวนมาจาก microstructure เดียวกัน ดังนั้น representation ร่วมจึงเป็น prior ที่สมเหตุผล — แต่ prior ไม่ใช่ผลลัพธ์ สิ่งที่การตั้งค่านี้พิสูจน์ได้จริงมีสองอย่าง: วิธีถ่วงน้ำหนัก loss ที่ข้อมูลชอบ (รายงานน้ำหนักที่เรียนรู้ ไม่ใช่แค่เรียกชื่อผู้ชนะ) และ gradient ของงานบน encoder ร่วมสอดคล้องกันหรือไม่ โดยวัดตลอดการฝึก ไม่ใช่อนุมานจากการที่เป้าหมายสัมพันธ์กัน

หาก fold walk-forward แบบ purge แสดงว่าเครือข่าย MTL ไม่สามารถชนะโมเดล gradient boosting แยกตามเป้าหมาย นั่นคือผลลัพธ์และควรเผยแพร่ตามนั้น — แม่แบบคือ ผลลัพธ์เชิงลบอย่างซื่อสัตย์ ผลลัพธ์เชิงลบเกี่ยวกับ negative transfer ก็ยังเป็นผลลัพธ์เกี่ยวกับ negative transfer

ข้อจำกัดความรับผิดชอบ: ข้อมูลที่ให้ไว้ในบทความนี้มีไว้เพื่อการศึกษาและให้ข้อมูลเท่านั้น และไม่ถือเป็นคำแนะนำทางการเงิน การลงทุน หรือการเทรด การเทรดสกุลเงินดิจิทัลมีความเสี่ยงสูงที่จะขาดทุน

ผู้เขียน

Eugen Soloviov
Eugen Soloviov

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.

Newsletter

ก้าวนำหน้าตลาด

สมัครรับจดหมายข่าวของเราเพื่อรับข้อมูลเชิงลึกการเทรดด้วย AI เฉพาะ การวิเคราะห์ตลาด และการอัปเดตแพลตฟอร์ม

เราเคารพความเป็นส่วนตัวของคุณ ยกเลิกการสมัครได้ทุกเมื่อ