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

Neural ODEs, Neural SDEs and continuous normalizing flows for irregularly-sampled market data - and the one ablation that decides whether continuous dynamics are worth their solver cost.

สมการเชิงอนุพันธ์สามัญแบบนิวรัล (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 แบบก้าวคงที่ โดยจับคู่ที่ความแม่นยำการฝึก
การแบ่งชุดเดินหน้าไปตามเวลา ใช้เฉพาะนอกตัวอย่าง
ฝั่งRMSELog-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)T∂f(h(t),t,θ)∂h\frac{da(t)}{dt} = -a(t)^T \frac{\partial f(h(t), t, \theta)}{\partial h}

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

dLdθ=−∫T0a(t)T∂f(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

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

  1. เกรเดียนต์ตามเส้นทาง: เทคนิค reparameterization — สุ่มเส้นทางบราวเนียน แล้วหาอนุพันธ์ผ่านตัวแก้โดยถือว่าสัญญาณรบกวนเป็นอินพุตคงที่
  2. การจับคู่สกอร์: ประมาณ ∇hlog⁡p(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(z0∣x1: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=1Nlog⁡p(xi∣z(ti))]−KL(q(z0∣x1: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 เป็นไปตามการเปลี่ยนตัวแปรฉับพลัน:

∂log⁡p(z(t))∂t=−tr(∂f∂z(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) และ log⁡p(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/GRUTransformerNeural ODENeural SDE
การจัดการเวลาไม่สม่ำเสมอแย่ (ต้องเติมแพดดิ้ง)positional encodingnativenative
หน่วยความจำ (ฝึก)O(T)O(T)O(T2)O(T^2)adjoint O(1)O(1)adjoint O(1)O(1)
การหาปริมาณความไม่แน่นอนไม่ (กำหนดแน่นอน)ไม่ (กำหนดแน่นอน)ผ่าน ensemblenative
อินเตอร์โพเลตระหว่างการสังเกตไม่ไม่ได้ได้
ความหนาแน่นเวลาแบบต่อเนื่องไม่ไม่ผ่าน 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):
        # t is scalar (current time), h is (batch, hidden_dim)
        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)
        # h_traj: (T, batch, hidden_dim)
        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):
        # Evolve hidden state from t_prev to 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
        # Discrete update with observation
        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()
            # Gradient clipping — important for ODE stability
            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):
        # state = [z, log_p] concatenated along last dim
        z = state[..., :self.dim]
        t_expand = t.expand(z.shape[0], 1)
        zt = torch.cat([z, t_expand], dim=-1)
        dz = self.net(zt)

        # Compute trace of Jacobian via Hutchinson estimator
        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."""
        # Start from data, integrate backward to base distribution
        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 prob under base distribution (standard normal)
        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 แล้วนำไปใช้งานด้วยตัวแก้ก้าวคงที่ที่สอบเทียบให้เอาต์พุตตรงกับตัวแก้แบบปรับตัวบนข้อมูลตัวแทน — และวัดช่องว่างแทนการสมมติว่ามันเล็ก

# High accuracy for training and risk calculations
odeint(f, h0, t, method='dopri5', rtol=1e-6, atol=1e-8)

# Fast inference for real-time signals
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 นาที:

# Normalize timestamps to [0, 1]
t_normalized = (timestamps - timestamps[0]) / (timestamps[-1] - timestamps[0])

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

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

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

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

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

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

dh=f dt+g dW+∫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