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

Neural ODEs: Does Continuous Time Beat a Delta-Time Feature?

Neural ODEs: Does Continuous Time Beat a Delta-Time Feature?
#deep-learning
#neural-ODE
#continuous-time
#SDE
#dynamics

สมการเชิงอนุพันธ์สามัญแบบนิวรัล (Chen และคณะ, NeurIPS 2018) กำหนดพารามิเตอร์ให้กับ อนุพันธ์ ของสถานะซ่อนด้วยโครงข่ายประสาท แล้วให้ตัวแก้ ODE คำนวณผลลัพธ์ โมเดลจึงมีความลึกแบบต่อเนื่อง และที่สำคัญกว่าสำหรับตลาดคือมี เวลา แบบต่อเนื่อง: สามารถประเมินสถานะซ่อนได้ที่ทุกค่า tt ไม่ใช่เฉพาะจุดกริดที่ข้อมูลของคุณบังเอิญอยู่

คุณสมบัตินี้มีคำอธิบายที่น่าสนใจอย่างชัดเจนสำหรับข้อมูลการเงินซึ่งมาถึงในช่วงเวลาสุ่ม แต่คำอธิบายนั้นซ่อนข้ออ้างที่อ่อนกว่าที่ดูเหมือนจะสื่อ GRU ธรรมดาที่ได้รับฟีเจอร์อินพุตเพิ่ม — log(Δt)\log(\Delta t) นับจากการสังเกตครั้งก่อน — ก็รู้เช่นกันว่าเวลาผ่านไปเท่าใด คำถามจริงไม่ใช่ว่าโมเดลเวลาแบบต่อเนื่องรับข้อมูลไม่สม่ำเสมอได้หรือไม่ แต่คือไดนามิกส์ต่อเนื่องที่เรียนรู้ระหว่างการสังเกตสกัดสิ่งใดที่ฟีเจอร์เดลตาเวลาไม่ได้ให้มาอยู่แล้วหรือไม่ โดยต้องแลกกับต้นทุนอนุมานที่สูงขึ้นสิบถึงร้อยเท่า

บทความนี้วางการทดลองดังกล่าว ให้ชุดเครื่องมือครบถ้วนสำหรับดำเนินการ และรายงานว่าการเปรียบเทียบต้องแสดงอะไรจึงจะรองรับความซับซ้อนนี้ ประเด็นการสร้างแท่งเทียน — ว่าเวลาของการซื้อขายมีสัญญาณในตัวเอง — ได้รับการยืนยันด้วยหลักฐานจากการวัดแท่ง 17 ประเภทใน แท่งเทียนนอกเหนือจากแท่งตามเวลา; ผลที่สำคัญที่นี่มีเพียงด้านการสร้างโมเดล: ตัวแก้ประเมินสถานะซ่อนได้ที่ทุกค่า tt จึงไม่ต้องใช้การอินเตอร์โพเลตหรือการเติมแพดดิ้ง

ข้ออ้างที่กำลังทดสอบ

สมมติฐานสามชั้นสำหรับการสร้างโมเดลตลาดแบบเวลาต่อเนื่อง

สมมติฐานสามชั้น แต่ละข้อแข็งแรงกว่าข้อก่อนหน้าอย่างเคร่งครัด:

  1. H1 — ODE-RNN ที่ฟิตกับสตรีมการซื้อขาย BTC ดิบแบบไม่สม่ำเสมอเอาชนะ GRU ที่ฟิตกับสตรีมเดียวกันโดยไม่มีข้อมูลเวลาเลย ข้อนี้แทบแน่นอนและแทบไร้ประโยชน์: ODE-RNN เพียงมีข้อมูลที่เบสไลน์ไม่มี
  2. H2 — ODE-RNN เอาชนะ GRU เดิมเมื่อ GRU ได้รับ log(Δt)\log(\Delta t) เป็นฟีเจอร์อินพุต นี่คือการทดสอบที่ซื่อตรง เป็นข้ออ้างที่งานวิจัย Neural ODE มักถูกตีความว่ากำลังเสนอ และเป็นข้ออ้างที่แทบไม่เคยสาธิตกับข้อมูลการเงิน
  3. H3 — ความได้เปรียบของ H2 อยู่รอดภายใต้งบเวลาแฝง กล่าวคือยังคงมีอยู่เมื่อใช้งาน ODE-RNN ด้วยตัวแก้แบบก้าวคงที่ที่เร็วพอสำหรับการอนุมานสด แทน dopri5 แบบปรับตัว

โพรโทคอลการทดลอง

สตรีมทิกเดียวกัน เป้าหมายเดียวกัน งบการปรับจูนเดียวกันสำหรับทั้งสองฝั่ง ไม่รีแซมเพิลเป็นแท่ง 1 นาที — การรีแซมเพิลทำลายโครงสร้างที่กำลังทดสอบโดยตรง

การตั้งค่า
ข้อมูล รายการธุรกรรม BTC ช่วงห่างระหว่างการมาถึงแปรผัน ไม่รีแซมเพิล
ฝั่งการทดลอง ODE-RNN; GRU + log(Δt)\log(\Delta t); GRU ที่ไม่มีฟีเจอร์เวลา
เป้าหมาย เหมือนกันทุกฝั่ง; ระบุพร้อมกับการฟิต
เมตริก RMSE, log-likelihood ของชุดกันไว้, เวลานาฬิกาต่อ epoch, เวลาแฝงของการอนุมานต่อสเต็ป
การกวาดตัวแก้ dopri5 (rtol 1e-5) เทียบกับ Euler แบบก้าวคงที่ โดยจับคู่ที่ความแม่นยำการฝึก
การแบ่งชุด เดินหน้าไปตามเวลา ใช้เฉพาะนอกตัวอย่าง
ฝั่ง RMSE Log-lik วินาที/epoch เวลาแฝงการอนุมาน/สเต็ป
GRU (ไม่มีฟีเจอร์เวลา)
GRU + log(dt)
ODE-RNN (dopri5)
ODE-RNN (Euler แบบก้าวคงที่)

ผลลัพธ์เชิงลบของ H2 เผยแพร่ได้อย่างเต็มที่ และอาจเป็นผลลัพธ์ที่มีค่ากว่า — ใช้มาตรฐานเดียวกับใน ผลลัพธ์เชิงลบอย่างซื่อตรง และ Sharpe ที่หักล้างเงินเฟ้อภายใต้การทดสอบหลายครั้ง

การทดลองรอง: CNF เทียบกับ Student-t

หากการเปรียบเทียบ ODE-RNN แพงเกินไป จุดยึดเชิงประจักษ์ที่ถูกกว่าคือด้านการแจกแจง: ฟิต continuous normalizing flow กับผลตอบแทน BTC รายวันจริง แล้วเปรียบเทียบความหนาแน่นที่ฟิตได้กับการฟิต Student-t และหางเชิงประจักษ์ พร้อมรายงานข้อผิดพลาดของควอนไทล์หางที่ระดับ 1% และ 5% เรื่องนี้เชื่อมโดยตรงกับงานด้านการแจกแจงที่วัดแล้วใน การพยากรณ์ความผันผวนด้วย GARCH และ GARCH แบบไม่สมมาตร


ส่วนที่เหลือของบทความนี้คือพื้นฐานและชุดเครื่องมือที่จำเป็นสำหรับรันงานข้างต้น

พื้นฐาน: จาก ResNet สู่ไดนามิกส์ต่อเนื่อง

โครงข่ายรีซิดวลที่กำลังกลายเป็นสนามไดนามิกส์ต่อเนื่อง

โครงข่ายรีซิดวลคำนวณ ht+1=ht+f(ht,θt)h_{t+1} = h_t + f(h_t, \theta_t). เมื่อย่อลดขนาดสเต็ปและเพิ่มจำนวนเลเยอร์ คุณจะเข้าใกล้ลิมิตแบบต่อเนื่อง:

dh(t)dt=f(h(t),t,θ)\frac{dh(t)}{dt} = f(h(t), t, \theta)

แทนที่จะมี TT เลเยอร์แบบไม่ต่อเนื่องที่ใช้พารามิเตอร์แยกกัน โครงข่ายเดียว ff กำหนดอัตราการเปลี่ยนแปลงฉับพลัน ผลลัพธ์ที่เวลา TT แก้ปัญหาค่าเริ่มต้น:

h(T)=h(0)+0Tf(h(t),t,θ)dth(T) = h(0) + \int_0^T f(h(t), t, \theta) \, dt

ตัวแก้แบบกล่องดำ (Euler, Runge-Kutta, Dormand-Prince) คำนวณอินทิกรัลเชิงตัวเลข โดยเลือกขนาดสเต็ปแบบปรับตัวจากค่าประมาณข้อผิดพลาดเฉพาะที่

วิธีแอดจอยน์ต์

การแบ็กโพรพาเกตผ่านทุกสเต็ปของตัวแก้ต้องเก็บสถานะกลางทั้งหมด ใช้หน่วยความจำแปรผันตามจำนวนสเต็ป Chen และคณะจึงแก้ ODE ย้อนกลับด้วยหน่วยความจำ O(1)O(1). กำหนดสถานะแอดจอยน์ต์ a(t)=L/h(t)a(t) = -\partial L / \partial h(t) ซึ่งเป็นไปตาม

da(t)dt=a(t)Tf(h(t),t,θ)h\frac{da(t)}{dt} = -a(t)^T \frac{\partial f(h(t), t, \theta)}{\partial h}

และสะสมเกรเดียนต์ของพารามิเตอร์ระหว่างการเดินย้อนกลับ:

dLdθ=T0a(t)Tf(h(t),t,θ)θdt\frac{dL}{d\theta} = -\int_T^0 a(t)^T \frac{\partial f(h(t), t, \theta)}{\partial \theta} \, dt

การเดินย้อนกลับแก้ระบบเสริมที่คำนวณ h(t)h(t), a(t)a(t) และ dL/dθdL/d\theta พร้อมกัน โดยรันตัวแก้ย้อนจาก TT ไปยัง 00

ข้อแลกเปลี่ยนคือการสร้าง h(t)h(t) ย้อนกลับสะสมข้อผิดพลาดเชิงตัวเลข โดยเฉพาะในไดนามิกส์แข็งหรือโกลาหล การทำ checkpoint เป็นทางสายกลาง — เก็บ h(t)h(t) ไว้ในเวลาตัวกลางไม่กี่จุด แล้วคำนวณใหม่ระหว่างจุดเหล่านั้น กระบวนการราคาคือเซมิ-มาร์ติงเกลต่อเนื่องและค่อนข้างราบรื่น ดังนั้นโดยทั่วไปวิธีแอดจอยน์ต์ยังใช้ได้; แต่ความแข็งรอบเหตุการณ์ไมโครสตรักเจอร์อาจบังคับให้ใช้ตัวแก้แบบปรับตัวหรือแบบไฮบริด

ODE-RNN: สถานะซ่อนต่อเนื่องระหว่างการสังเกต

สถานะซ่อนที่วิวัฒน์อย่างราบรื่นระหว่างการสังเกตไม่สม่ำเสมอ

ODE-RNN คือสถาปัตยกรรมที่การทดลองหลักมุ่งทดสอบ ระหว่างการสังเกต สถานะซ่อนวิวัฒน์ตาม ODE:

h(t)=ODESolve(fθ,h(ti),ti,t)h(t) = \text{ODESolve}(f_\theta, h(t_i), t_i, t)

เมื่อการสังเกต xi+1x_{i+1} มาถึงที่ ti+1t_{i+1} จะเกิดการอัปเดตแบบไม่ต่อเนื่อง:

h(ti+1+)=RNNCell(h(ti+1),xi+1)h(t_{i+1}^+) = \text{RNNCell}(h(t_{i+1}^-), x_{i+1})

ตัวยกกำลังระบุสถานะก่อนและหลังการสังเกตทันที ระหว่างการสังเกตคือไดนามิกส์ต่อเนื่องที่เรียนรู้; ณ จุดสังเกตคือข้อมูลใหม่

กลไกที่การทดลองตัดออก H2 ตรวจสอบมีดังนี้: ช่องว่างยาวหมายความว่าสถานะซ่อนได้วิวัฒน์ไปไกลภายใต้ fθf_\theta — สลายเข้าหาเบสไลน์หรือแยกออก — และรูปทรงของวิวัฒน์นี้ เรียนรู้ แทนที่จะป้อนเป็นสเกลาร์ การแสดงออกที่มากขึ้นนี้คุ้มต้นทุนบนข้อมูลการซื้อขายจริงหรือไม่ คือสิ่งที่ยังไม่ได้วัดอย่างแน่ชัด

การใช้งานที่เหมาะสมนอกเหนือจากทิก: พอร์ตหลายสินทรัพย์ที่แต่ละสินทรัพย์มีตารางการสังเกตของตนเอง ขณะที่สถานะแฝงของสินทรัพย์ที่สัมพันธ์กันยังวิวัฒน์ต่อไปแม้สังเกตเพียงสินทรัพย์เดียว; และสัญญาณขับเคลื่อนด้วยเหตุการณ์ (ข่าว ผลประกอบการ การประกาศเศรษฐกิจมหภาค) ที่มาถึงในเวลาไม่สม่ำเสมอ

Neural SDE: เพิ่มองค์ประกอบสุ่ม

ไดนามิกส์ต่อเนื่องพร้อมการแพร่แบบสุ่มที่ควบคุมได้

Neural ODE เป็นแบบกำหนดแน่นอนและแทนสัญญาณรบกวนในเส้นทางราคาไม่ได้ การรักษาแบบคลาสสิก — geometric Brownian motion, drift ที่เป็นกลางต่อความเสี่ยง และวิธีที่สมมติฐานความผันผวนคงที่ล้มเหลวเมื่อเจอกับ smile — กล่าวถึงไว้ใน การกำหนดราคาตัวเลือก Black-Scholes Neural SDE แทนรูปแบบพารามิเตอร์ด้วยพจน์ diffusion ที่เรียนรู้:

dh(t)=f(h(t),t,θ)dt+g(h(t),t,ϕ)dW(t)dh(t) = f(h(t), t, \theta) \, dt + g(h(t), t, \phi) \, dW(t)

ff คือ drift, gg คือ diffusion, W(t)W(t) คือกระบวนการ Wiener และทั้ง ff กับ gg เป็นโครงข่ายประสาท

สถาปัตยกรรม: โครงข่าย Drift และ Diffusion

  • โครงข่าย Drift fθf_\theta: วิถีที่คาดหมาย — แนวโน้ม การย้อนกลับสู่ค่าเฉลี่ย โมเมนตัม ฝึกเพื่อลดข้อผิดพลาดการพยากรณ์
  • โครงข่าย Diffusion gϕg_\phi: ขนาดสัญญาณรบกวน เรียนรู้เป็นฟังก์ชันของสถานะ สูงในระบอบผันผวน ต่ำในระบอบสงบ

การแยกนี้สะท้อนการเงินเชิงปริมาณแบบคลาสสิก: drift คือไดนามิกส์ภายใต้การวัดที่เป็นกลางต่อความเสี่ยง (หรือการวัด P) ส่วน diffusion คือพื้นผิวความผันผวน

การฝึก Neural SDE

อินทิกรัลสุ่ม gdW\int g\,dW ไม่สามารถหาอนุพันธ์ได้แบบคลาสสิก มีสามแนวทาง:

  1. เกรเดียนต์ตามเส้นทาง: เทคนิค reparameterization — สุ่มเส้นทางบราวเนียน แล้วหาอนุพันธ์ผ่านตัวแก้โดยถือว่าสัญญาณรบกวนเป็นอินพุตคงที่
  2. การจับคู่สกอร์: ประมาณ hlogp(h)\nabla_h \log p(h) และฝึกด้วยเป้าหมาย denoising — เครื่องมือเดียวกับที่ใช้เป็นโมเดลกำเนิดแบบเดี่ยวใน โมเดล diffusion สำหรับการพยากรณ์คริปโต แต่ที่นี่ลดบทบาทเหลือเพียงตัวเลือกการฝึกสำหรับ SDE
  3. การจับคู่การแจกแจงมิติจำกัด: จับคู่ marginal ณ เวลาสังเกต แทนที่จะจับคู่ measure ของเส้นทางเต็ม

วิธีตามเส้นทางเป็นค่าเริ่มต้นในทางปฏิบัติ torchsde รองรับ Euler-Maruyama, Milstein และ stochastic Runge-Kutta พร้อม autodiff

การเรียนรู้พื้นผิวความผันผวน

โมเดลคลาสสิก (Heston, SABR) บังคับรูปแบบพารามิเตอร์ให้กับค่าสัมประสิทธิ์ diffusion ส่วน Neural SDE เรียนรู้ gϕ(S,t)g_\phi(S, t) เป็นฟังก์ชันทั่วไป:

dS(t)=μθ(S(t),t)dt+σϕ(S(t),t)S(t)dW(t)dS(t) = \mu_\theta(S(t), t) \, dt + \sigma_\phi(S(t), t) \, S(t) \, dW(t)

นี่คือตัวประมาณสากลสำหรับกระบวนการ diffusion — กระบวนการ Ito ใด ๆ ก็ประมาณได้แม่นยำตามต้องการเมื่อมีความจุมากพอ

Latent ODE สำหรับข้อมูลหายและข้อมูลไม่พร้อมกัน

สถานะต่อเนื่องแฝงที่สร้างข้อมูลหายและข้อมูลอะซิงโครนัสขึ้นใหม่

Latent ODE (Rubanova, Chen, Duvenaud, 2019) จับคู่ Neural ODE กับ VAE: อนุกรมการเงินคือการสังเกตที่มีสัญญาณรบกวนของกระบวนการพื้นฐานที่ราบรื่น ซึ่งเรียนรู้ในสเปซแฝงมิติต่ำ

  1. โครงข่ายรับรู้: ODE-RNN วิ่งย้อนผ่านการสังเกตเพื่อสร้าง q(z0x1:N)q(z_0 | x_{1:N})
  2. ไดนามิกส์แฝง: z(t)=z(t0)+t0tfθ(z(s),s)dsz(t) = z(t_0) + \int_{t_0}^{t} f_\theta(z(s), s)\, ds
  3. ตัวถอดรหัส: x^(t)=Decoder(z(t))\hat{x}(t) = \text{Decoder}(z(t)) ประเมินได้ ณ เวลาใด ๆ ที่ร้องขอ

ฟังก์ชันสูญเสียคือ ELBO มาตรฐาน:

L=Eq(z0)[i=1Nlogp(xiz(ti))]KL(q(z0x1:N)p(z0))\mathcal{L} = \mathbb{E}_{q(z_0)} \left[ \sum_{i=1}^{N} \log p(x_i | z(t_i)) \right] - \text{KL}(q(z_0 | x_{1:N}) \| p(z_0))

การประมาณสถานะพอร์ต: จากการสังเกตแบบเบาบางและอะซิงโครนัสข้ามสินทรัพย์ อนุมานสถานะแฝงต่อเนื่องที่จับไดนามิกส์ร่วม — โดยพื้นฐานคือเวอร์ชันไม่เชิงเส้นที่เรียนรู้ของตัวกรอง Kalman

การเติมข้อมูลหาย: ช่วงหยุดซื้อขายและช่องว่างสุดสัปดาห์ได้วิถีแฝงที่อินเตอร์โพเลตอย่างราบรื่น; ตัวถอดรหัสสร้างเส้นทางที่เป็นไปได้ผ่านช่องว่าง พร้อมความไม่แน่นอนจาก posterior ของ VAE

การผสานหลายความถี่: ราคาปิดรายวัน VWAP ระหว่างวัน และข้อมูลทิกในโมเดลเดียวโดยไม่มีกริดเวลาร่วม

Continuous Normalizing Flow สำหรับการแจกแจงผลตอบแทน

โฟลว์ต่อเนื่องที่กำลังปรับรูปการแจกแจงผลตอบแทนหางหนา

CNF ใช้ Neural ODE แปลงการแจกแจงฐานอย่างง่ายเป็นเป้าหมายที่ซับซ้อน การแปลงคือ dz(t)dt=f(z(t),t,θ)\frac{dz(t)}{dt} = f(z(t), t, \theta) และ log-density เป็นไปตามการเปลี่ยนตัวแปรฉับพลัน:

logp(z(t))t=tr(fz(t))\frac{\partial \log p(z(t))}{\partial t} = -\text{tr}\left(\frac{\partial f}{\partial z(t)}\right)

อินทิเกรตสถานะและ log-density ไปข้างหน้าจาก z(0)p0z(0) \sim p_0 เพื่อให้ได้ z(T)z(T) และ logp(z(T))\log p(z(T)) รอยเทรซของจาโคเบียนประมาณด้วยตัวประมาณเชิงสุ่มของ Hutchinson เพื่อการขยายขนาด

ผลตอบแทนคริปโตมี kurtosis สูงและเบ้ซ้าย — วัดได้จากข้อมูลจริงใน การพยากรณ์ความผันผวนด้วย GARCH และ GARCH แบบไม่สมมาตรกับผลของเลเวอเรจ — และ CNF เรียนรู้รูปทรงนั้นแทนการสมมติ การทำให้โฟลว์มีเงื่อนไขตามตัวแปรสถานะทำให้รูปทรงเปลี่ยนตาม regime ได้ เพราะความหนาแน่นเป็นแบบ exact ไม่ใช่ค่าประมาณ จึงส่งต่อไปยังเมตริกหางที่คำนวณใน แบ็กเทสต์มอนติคาร์โลและบูตสแตรป และการสร้าง CVaR ใน ไปป์ไลน์พอร์ต HRP/CVaR; โครงสร้างหางร่วมจัดการแยกต่างหากใน โมเดล copula สำหรับความเสี่ยงร่วม

การประมาณความหนาแน่นแบบมีเงื่อนไข

กรอบที่มีประโยชน์คือการเปรียบเทียบโดยตรงระหว่างสามแนวทางที่ตีพิมพ์สำหรับปัญหาเดียวกัน TFT ให้ควอนไทล์ชุดคงที่จากเลเยอร์เอาต์พุตควอนไทล์ Conformal prediction ให้ช่วงที่ปรับเทียบแล้วพร้อมหลักประกันด้าน coverage ส่วน CNF แบบมีเงื่อนไขให้ความหนาแน่นที่ exact และหาอนุพันธ์ได้:

dz(t)dt=f(z(t),t,featurest,θ)\frac{dz(t)}{dt} = f(z(t), t, \text{features}_t, \theta)

ความสามารถในการหาอนุพันธ์คือคุณสมบัติที่แตกต่าง: ความหนาแน่นวางใน objective ปลายทางและแบ็กโพรพาเกตผ่านได้ ซึ่งทั้งควอนไทล์คงที่และช่วง conformal ทำไม่ได้ ความหนาแน่น exact คุ้มต้นทุนเมื่อเทียบกับควอนไทล์ TFT บนเป้าหมายเดียวกันหรือไม่ ยังไม่ได้ทดสอบที่นี่

เปรียบเทียบกับทางเลือกแบบไม่ต่อเนื่อง

โมเดลไม่ต่อเนื่องและต่อเนื่องเปรียบเทียบกันบนการสังเกตร่วม

คุณสมบัติ LSTM/GRU Transformer Neural ODE Neural SDE
การจัดการเวลาไม่สม่ำเสมอ แย่ (ต้องเติมแพดดิ้ง) positional encoding native native
หน่วยความจำ (ฝึก) O(T)O(T) O(T2)O(T^2) adjoint O(1)O(1) adjoint O(1)O(1)
การหาปริมาณความไม่แน่นอน ไม่ (กำหนดแน่นอน) ไม่ (กำหนดแน่นอน) ผ่าน ensemble native
อินเตอร์โพเลตระหว่างการสังเกต ไม่ ไม่ ได้ ได้
ความหนาแน่นเวลาแบบต่อเนื่อง ไม่ ไม่ ผ่าน CNF ผ่าน path measure
ต้นทุนคำนวณ ต่ำ ปานกลาง แปรผัน (ตัวแก้) สูง (ตัวแก้ SDE)

ส่วน LSTM เทียบกับ attention ของตารางนี้อธิบายโดยละเอียดพร้อมเบนช์มาร์กใน บทความ TFT — ดูส่วน TFT เทียบกับ LSTM เทียบกับ Vanilla Transformer และ เมื่อ LSTM ยังชนะ ส่วนที่ใหม่ที่นี่คือสองคอลัมน์ทางขวา และแถวที่ตัดสินคำถาม H2/H3 คือสองแถวสุดท้าย

การใช้งาน Python ด้วย torchdiffeq

ตัวแก้สมการเชิงอนุพันธ์นิวรัลในฐานะวงโคจรคำนวณที่แม่นยำ

torchdiffeq มีตัวแก้ ODE พร้อมแบ็กโพรพาเกชันแบบแอดจอยน์ต์

Neural ODE พื้นฐานสำหรับไดนามิกส์ราคา

import torch
import torch.nn as nn
from torchdiffeq import odeint_adjoint as odeint

class PriceDynamics(nn.Module):
    """Neural network defining dh/dt = f(h, t)."""

    def __init__(self, hidden_dim: int = 64):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(hidden_dim, 128),
            nn.Tanh(),
            nn.Linear(128, 128),
            nn.Tanh(),
            nn.Linear(128, hidden_dim),
        )

    def forward(self, t, h):
        return self.net(h)


class NeuralODEPredictor(nn.Module):
    """
    Encode observed features -> latent state,
    evolve via Neural ODE,
    decode to price prediction.
    """

    def __init__(self, input_dim: int, hidden_dim: int = 64):
        super().__init__()
        self.encoder = nn.Linear(input_dim, hidden_dim)
        self.dynamics = PriceDynamics(hidden_dim)
        self.decoder = nn.Linear(hidden_dim, 1)

    def forward(self, x0, eval_times):
        """
        x0:         (batch, input_dim) features at t=0
        eval_times: (T,) times at which to evaluate the ODE
        Returns:    (T, batch, 1) predictions
        """
        h0 = self.encoder(x0)                          # (batch, hidden_dim)
        h_traj = odeint(self.dynamics, h0, eval_times,
                        method='dopri5', rtol=1e-5, atol=1e-7)
        return self.decoder(h_traj)

ODE-RNN สำหรับข้อมูลทิกไม่สม่ำเสมอ

นี่คือฝั่งการทดลอง เบสไลน์ที่ต้องเอาชนะคือ nn.GRUCell บนสตรีมเดียวกัน โดยต่อ log(t_next - t_prev) เข้ากับ x

class ODERNNCell(nn.Module):
    """Single step: ODE-evolve, then RNN-update."""

    def __init__(self, input_dim: int, hidden_dim: int = 64):
        super().__init__()
        self.dynamics = PriceDynamics(hidden_dim)
        self.gru_cell = nn.GRUCell(input_dim, hidden_dim)

    def forward(self, h, x, t_prev, t_next):
        times = torch.tensor([t_prev, t_next], dtype=torch.float32)
        h_evolved = odeint(self.dynamics, h, times,
                           method='dopri5')[-1]  # state at t_next
        h_updated = self.gru_cell(x, h_evolved)
        return h_updated


class ODERNN(nn.Module):
    """Process irregularly-sampled sequence."""

    def __init__(self, input_dim: int, hidden_dim: int = 64):
        super().__init__()
        self.cell = ODERNNCell(input_dim, hidden_dim)
        self.decoder = nn.Linear(hidden_dim, 1)
        self.hidden_dim = hidden_dim

    def forward(self, observations, times):
        """
        observations: list of (batch, input_dim) tensors
        times:        list of floats, observation timestamps
        """
        batch_size = observations[0].shape[0]
        h = torch.zeros(batch_size, self.hidden_dim)

        outputs = []
        for i in range(len(observations)):
            t_prev = 0.0 if i == 0 else times[i - 1]
            h = self.cell(h, observations[i], t_prev, times[i])
            outputs.append(self.decoder(h))

        return torch.stack(outputs)  # (seq_len, batch, 1)

ลูปการฝึก

def train_neural_ode(model, train_loader, epochs=100, lr=1e-3):
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
        optimizer, T_max=epochs
    )

    for epoch in range(epochs):
        epoch_loss = 0.0
        for batch in train_loader:
            features, times, targets = batch
            optimizer.zero_grad()

            predictions = model(features, times)
            loss = torch.nn.functional.mse_loss(predictions, targets)

            loss.backward()
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
            optimizer.step()

            epoch_loss += loss.item()

        scheduler.step()

        if (epoch + 1) % 10 == 0:
            avg_loss = epoch_loss / len(train_loader)
            print(f"Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.6f}")

Continuous Normalizing Flow สำหรับการแจกแจงผลตอบแทน

from torchdiffeq import odeint

class CNFDynamics(nn.Module):
    """Dynamics for continuous normalizing flow."""

    def __init__(self, dim: int = 1, hidden_dim: int = 64):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(dim + 1, hidden_dim),  # +1 for time
            nn.Tanh(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.Tanh(),
            nn.Linear(hidden_dim, dim),
        )
        self.dim = dim

    def forward(self, t, state):
        z = state[..., :self.dim]
        t_expand = t.expand(z.shape[0], 1)
        zt = torch.cat([z, t_expand], dim=-1)
        dz = self.net(zt)

        e = torch.randn_like(z)
        e_dz = torch.autograd.grad(
            dz, z, e, create_graph=True
        )[0]
        trace_jac = (e_dz * e).sum(dim=-1, keepdim=True)

        return torch.cat([dz, -trace_jac], dim=-1)


class ReturnDistributionCNF(nn.Module):
    """Model return distributions with continuous normalizing flows."""

    def __init__(self, dim: int = 1):
        super().__init__()
        self.dynamics = CNFDynamics(dim)
        self.dim = dim

    def log_prob(self, x):
        """Compute log probability of observed returns."""
        log_p0 = torch.zeros(x.shape[0], 1)
        state0 = torch.cat([x, log_p0], dim=-1)
        state0.requires_grad_(True)

        times = torch.tensor([1.0, 0.0])  # backward
        state_T = odeint(self.dynamics, state0, times,
                         method='dopri5')[-1]

        z_T = state_T[..., :self.dim]
        delta_log_p = state_T[..., self.dim:]

        log_p_base = -0.5 * (z_T ** 2 + torch.log(
            torch.tensor(2 * torch.pi)
        )).sum(dim=-1, keepdim=True)

        return log_p_base + delta_log_p

    def sample(self, n_samples: int):
        """Generate samples from learned distribution."""
        z0 = torch.randn(n_samples, self.dim)
        log_p0 = torch.zeros(n_samples, 1)
        state0 = torch.cat([z0, log_p0], dim=-1)

        times = torch.tensor([0.0, 1.0])  # forward
        state_T = odeint(self.dynamics, state0, times,
                         method='dopri5')[-1]

        return state_T[..., :self.dim]

หมายเหตุเชิงปฏิบัติ

สมดุลระหว่างความแม่นยำเชิงตัวเลขและความเร็วการอนุมาน

สิ่งเหล่านี้คือจุดที่จะทำให้คุณเจอปัญหาระหว่างรันการทดลองข้างต้น

การเลือกตัวแก้และความเร็ว

dopri5 ให้หลักประกันด้านความแม่นยำแต่มีต้นทุนคำนวณแปรผัน นี่คือเหตุผลที่ H3 เป็นสมมติฐานแยกจาก H2: ความได้เปรียบที่มีอยู่เฉพาะภายใต้ตัวแก้แบบปรับตัวอาจไม่รอดงบเวลาแฝงแบบสด ตัวแก้แบบก้าวคงที่ (Euler, RK4) ให้เวลาแฝงคาดเดาได้แลกกับความแม่นยำ ทางประนีประนอมเชิงปฏิบัติคือฝึกด้วย dopri5 แล้วนำไปใช้งานด้วยตัวแก้ก้าวคงที่ที่สอบเทียบให้เอาต์พุตตรงกับตัวแก้แบบปรับตัวบนข้อมูลตัวแทน — และวัดช่องว่างแทนการสมมติว่ามันเล็ก

odeint(f, h0, t, method='dopri5', rtol=1e-6, atol=1e-8)

odeint(f, h0, t, method='euler', options={'step_size': 0.1})

สำหรับปัญหาแข็งใกล้การกระโดดไม่ต่อเนื่อง ใช้ method='implicit_adams' หรือ method='scipy_solver'

เสถียรภาพเชิงตัวเลข

หาก fθf_\theta ให้ค่าขนาดใหญ่ สถานะจะพุ่งออกจากระบบ วิธีบรรเทามีดังนี้:

  • การทำ spectral normalization บนเลเยอร์ของ fθf_\theta เพื่อควบคุมค่าคงที่ Lipschitz
  • การตัดเกรเดียนต์ ระหว่างฝึก (แสดงในลูปการฝึกข้างต้น)
  • การทำ normalization ของเวลา: ปรับสเกล timestamp ให้อยู่ใน [0,1][0, 1]
  • การทำ regularization กับนอร์มไดนามิกส์: เพิ่ม λfθ(h,t)2\lambda \|f_\theta(h, t)\|^2 ลงใน loss

ช่วงอินทิเกรต

สำหรับข้อมูลที่ครอบคลุมหลายเดือน อย่าอินทิเกรตจาก t=0t = 0 ถึง t=10,000t = 10{,}000 นาที:

t_normalized = (timestamps - timestamps[0]) / (timestamps[-1] - timestamps[0])

วิธีนี้ทำให้ตัวแก้อยู่ในช่วงที่เป็นมิตรต่อเชิงตัวเลข และทำให้ไดนามิกส์ที่เรียนรู้ไม่ขึ้นกับสเกล นอกจากนี้ยังสำคัญต่อการเปรียบเทียบ: ODE-RNN ที่ไม่ normalize อาจแพ้เบสไลน์ GRU ด้วยเหตุผลเชิงตัวเลขล้วน ๆ ซึ่งจะเป็นสิ่งประดิษฐ์จากการวัด ไม่ใช่ข้อค้นพบ

การจัดการหลายสเกลเวลา

ตลาดมีไดนามิกส์ในระดับไมโครวินาที วินาที นาที และวันพร้อมกัน และ Neural ODE เดียวอาจรักษาทั้งหมดไว้ได้ยาก ทางเลือกคือซ้อนบล็อก ODE หลายชุดในสเกลเวลาต่างกัน; ขยายมิติซ่อนเพื่อรองรับความจุทั้งเร็วและช้า; หรือรัน Neural ODE แยกตามแถบความถี่แล้วรวมเอาต์พุต (นี่เป็นคำถามเรื่องความจุโมเดล แยกจากคำถามเรื่องความเที่ยงตรงของแบ็กเทสต์ใน การเจาะลึกความละเอียดแบบปรับตัว)

ทิศทางวิจัยที่เปิดอยู่

เส้นทางวิจัยเวลาแบบต่อเนื่องที่แผ่ออกจากโมเดลเดียว

Neural Jump SDE: เพิ่มองค์ประกอบ jump ที่เรียนรู้ได้สำหรับการกระจัดฉับพลัน (แฟลชแครช เซอร์ไพรส์จากผลประกอบการ):

dh=fdt+gdW+Rγ(h,z)N~(dt,dz)dh = f \, dt + g \, dW + \int_{\mathbb{R}} \gamma(h, z) \, \tilde{N}(dt, dz)

โดยที่ N~\tilde{N} คือ random measure แบบปัวซองชดเชย และ γ\gamma คือเคอร์เนล jump ที่เรียนรู้ได้

สมการเชิงอนุพันธ์ควบคุมแบบนิวรัล (Neural CDE): แทนที่กระบวนการ Wiener ด้วยสัญญาณขับทั่วไป ให้สตรีมข้อมูลที่สังเกตได้ขับไดนามิกส์ เหมาะกับ order flow ที่สตรีมธุรกรรมและราคาเสนอซื้อขายขับสถานะตลาดแฝง

การจำลองตลาดที่หาอนุพันธ์ได้: ใช้ Neural SDE เป็นโมเดลกำเนิดภายในตัวจำลองที่หาอนุพันธ์ได้ และฝึกกลยุทธ์แบบ end-to-end ด้วยการแบ็กโพรพาเกตผ่านมันด้วยวิธีแอดจอยน์ต์ กลยุทธ์และโมเดลตลาดวิวัฒน์ร่วมกัน

Neural ODE ที่คำนึงถึงฟิสิกส์: PINN ฝังรีซิดวลของสมการเชิงอนุพันธ์ไว้ใน loss — เทคนิคนี้แนะนำไว้ใน บทความ Navier-Stokes การประยุกต์ด้านการเงินคือการบังคับ no-arbitrage, put-call parity และเงื่อนไขมาร์ติงเกลให้เป็นพจน์ penalty หรือข้อจำกัดแข็งบนไดนามิกส์ที่เรียนรู้

บทสรุป

ไดนามิกส์ต่อเนื่องที่ราบรื่นคลี่คลายสู่บทสรุปจากการวัด

กรอบเวลาแบบต่อเนื่องถูกต้องในเชิงโครงสร้าง: ตลาดเป็นกระบวนการต่อเนื่องที่สังเกตในเวลาไม่ต่อเนื่องและไม่สม่ำเสมอ โมเดลควรเคารพสิ่งนี้ ชุดเครื่องมือมีความพร้อมแล้ว — torchdiffeq, torchsde, PyTorch ธรรมดาสำหรับส่วนที่เหลือ — และต้นทุนที่รู้จักคือความเร็วตัวแก้กับเสถียรภาพเชิงตัวเลขในช่วงอินทิเกรตยาว

สิ่งที่ยังไม่ยืนยันคือส่วนที่ตัดสินว่าเรื่องนี้ควรอยู่ใน production stack หรือไม่ ความถูกต้องเชิงโครงสร้างไม่ใช่หลักฐานของความได้เปรียบด้านการพยากรณ์ และความได้เปรียบเฉพาะที่อ้างว่าไดนามิกส์ต่อเนื่องเหนือกว่าฟีเจอร์ log(Δt)\log(\Delta t) ยังไม่ได้วัดกับข้อมูลการซื้อขายจริงที่นี่ จนกว่าจะเติมตาราง H2 ด้านบน ให้ถือทุกอย่างหลังเส้นพับเป็นสมมติฐานที่ระบุไว้อย่างชัดเจนพร้อม implementation ที่ใช้งานได้ ไม่ใช่ผลลัพธ์


เอกสารอ้างอิง

  • Chen, R.T.Q., Rubanova, Y., Bettencourt, J., Duvenaud, D. (2018). สมการเชิงอนุพันธ์สามัญแบบนิวรัล. NeurIPS 2018. arXiv:1806.07366
  • Rubanova, Y., Chen, R.T.Q., Duvenaud, D. (2019). Latent ODE สำหรับอนุกรมเวลาที่มีการสุ่มตัวอย่างไม่สม่ำเสมอ. NeurIPS 2019. arXiv:1907.03907
  • Jia, J., Benson, A.R. (2019). สมการเชิงอนุพันธ์สุ่มแบบนิวรัลที่มีการกระโดด. NeurIPS 2019.
  • Kidger, P., Morrill, J., Foster, J., Lyons, T. (2020). สมการเชิงอนุพันธ์ควบคุมแบบนิวรัลสำหรับอนุกรมเวลาที่ไม่สม่ำเสมอ. NeurIPS 2020.
  • Hasan, A., Pereira, J.M., Farsiu, S., Carin, L. (2021). โมเดลสมการเชิงอนุพันธ์สุ่มของโครงข่ายประสาทพร้อมการประยุกต์กับการพยากรณ์ข้อมูลการเงิน. arXiv:2111.13164
  • torchdiffeq: github.com/rtqichen/torchdiffeq
  • บทเรียนการเรียนรู้เชิงลึกของ UvA — Neural ODE: uvadlc-notebooks.readthedocs.io
ข้อจำกัดความรับผิดชอบ: ข้อมูลที่ให้ไว้ในบทความนี้มีไว้เพื่อการศึกษาและให้ข้อมูลเท่านั้น และไม่ถือเป็นคำแนะนำทางการเงิน การลงทุน หรือการเทรด การเทรดสกุลเงินดิจิทัลมีความเสี่ยงสูงที่จะขาดทุน

ผู้เขียน

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 เฉพาะ การวิเคราะห์ตลาด และการอัปเดตแพลตฟอร์ม

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