การตัดแต่งโมเดลสำหรับการอนุมานการเทรดที่มีเวลาแฝงต่ำ
บทความ DeepLOB จบส่วนการนำไปใช้งานจริงด้วยสามแนวทาง — ONNX ร่วมกับ TensorRT, การทำ quantization เป็น INT8 และ FPGA — แต่ไม่ได้อธิบายแนวทางใดเลย บทความนี้จึงเติมเต็มปัญหาพื้นฐานที่อยู่เบื้องหลังทั้งสามแนวทาง: โมเดลใหญ่เกินความจำเป็น การตัดแต่งเครือข่ายประสาทเพื่อลบพารามิเตอร์ที่ซ้ำซ้อน และข้ออ้างที่น่าสนใจในวรรณกรรมว่า นอกจากจะช่วยประหยัดหน่วยความจำแล้ว โครงข่ายย่อยที่มีน้ำหนักเพียง 10-20% อาจให้ความแม่นยำเทียบเท่าโมเดลแบบหนาแน่น
บล็อกนี้อธิบายไปแล้วว่าเวลาแฝงสำคัญอย่างไร — ZigBolt ครอบคลุมเส้นทางส่งข้อความ และ ภาษี IPC อธิบายพร้อมคณิตศาสตร์จุดคุ้มทุน — ส่วน การสร้างแบบจำลองสเปรด ครอบคลุมการแลกเปลี่ยนระหว่างตัวเลือกที่เร็วกว่าแต่แย่ลงเล็กน้อยกับตัวเลือกที่ช้ากว่าแต่ดีกว่า พร้อมตารางเปรียบเทียบ gradient boosting กับ deep learning ที่มีแถวเวลาแฝงของการอนุมาน สิ่งที่ยังไม่ครอบคลุมคือ เราจะทำให้โมเดลหนึ่งเล็กลงได้อย่างไร ในลูปการเสนอราคา ขั้นตอนการอนุมานโมเดลอยู่ภายใต้การควบคุมของเราโดยตรง ส่วนช่วงการขนส่งพร้อมตัวเลข p50/p95/p99 ที่ทำซ้ำได้อธิบายไว้ใน การสื่อสารข้อมูลสำหรับการซื้อขายอัลกอริทึม
บทความนี้คืออะไร: คณิตศาสตร์และโค้ดที่ใช้งานได้สำหรับการตัดแต่งตามขนาด การตัดแต่งแบบมีโครงสร้าง Iterative Magnitude Pruning การตัดแต่งตามการเคลื่อนที่ การกลั่นความรู้ และความเบาบางแบบกึ่งมีโครงสร้าง 2:4 ของ NVIDIA ที่นำมาใช้กับ MLP สำหรับการเทรด
บทความนี้ไม่ใช่: ผลลัพธ์ที่วัดแล้ว บทความเชิงประจักษ์ทุกบทความในบล็อกมีบรรทัดระบุที่มา หรือมี repo ประกอบ แต่บทความนี้ยังไม่มีทั้งสองอย่าง เส้นโค้งความเบาบางเทียบกับความแม่นยำและเวลาแฝงด้านล่างเป็นการทดลองที่ต้องรัน ไม่ใช่ตารางสำหรับอ้างตัวเลข ให้ถือว่าทุกอย่างในที่นี้เป็นวิธีการ ส่วนตัวเลขยังรอการวัด
การตัดแต่งโมเดลให้อะไรแก่คุณ

ข้อจำกัดคือขนาด ลองพิจารณาโมเดลความถี่ระดับกลาง — MLP 4 ชั้นที่มีหน่วยซ่อน 2048 หน่วยบนฟีเจอร์ของสมุดคำสั่งซื้อขาย:
สำหรับ , , , จะมีพารามิเตอร์ประมาณ 12.6 ล้านตัว หรือราว 48 MB ใน float32 โดยทั่วไป L2 มีขนาด 1-4 MB ดังนั้นน้ำหนักจึงใส่ไม่พอดีและต้องสตรีมจากหน่วยความจำที่ไกลกว่าในทุก forward pass หากตัดออก 95% จะเหลือพารามิเตอร์ที่ใช้งานจริงราว 630K และ 2.4 MB ซึ่งพอดีกับแคช
การแปลงนี้เป็นเวลานาฬิกาจริงหรือไม่นั้นขึ้นกับว่า kernel ถูกจำกัดด้วยหน่วยความจำหรือไม่ ซึ่งเป็นคำถามด้าน arithmetic intensity ไม่ใช่ขนาด บันไดความเร็วของเครื่องมือทดสอบย้อนหลัง ใช้ roofline model ผ่านตัวอย่างที่วัดจริง แทนการอ้าง penalty factor; กรอบเดียวกันนี้ใช้ที่นี่ และควรมีวินัยแบบเดียวกัน: วัดไบต์ที่เคลื่อนย้ายก่อนอ้างการเร่งความเร็ว
พื้นฐานของการตัดแต่งโมเดล

การตัดแต่งแบบไร้โครงสร้าง
วิธีที่ง่ายที่สุดคือกำหนดน้ำหนักแต่ละตัวเป็นศูนย์ตามขนาดของมัน เมื่อมีเมทริกซ์น้ำหนัก ให้สร้าง binary mask ดังนี้:
โดย คือ threshold ที่เลือกเพื่อให้ได้ระดับ sparsity ที่ต้องการ:
เมทริกซ์ที่ตัดแต่งคือ โดย คือผลคูณแบบ Hadamard แนวคิดคือ น้ำหนักที่ใกล้ศูนย์มีส่วนต่อเอาต์พุตของเลเยอร์เพียงเล็กน้อย
ปัญหาที่ต้องพูดตรง ๆ เพราะตัวเลขความเบาบางอ่านผิดได้ง่าย: ความเบาบางแบบไม่มีโครงสร้างไม่ได้แปลว่าจะเร็วขึ้นบนฮาร์ดแวร์มาตรฐาน เมทริกซ์ที่มีศูนย์ 90% ยังคงออกคำสั่งคูณและสะสมจำนวนเท่าเดิม เว้นแต่จะเปลี่ยนไปใช้ sparse kernels หรือฮาร์ดแวร์ที่รองรับความเบาบาง เมื่อโค้ดพิมพ์ Sparsity: 90.0% นั่นหมายถึงจำนวนศูนย์ ไม่ใช่การเร็วขึ้น 10 เท่า และบน dense CPU GEMM ก็ไม่ได้เร็วขึ้น 1.01 เท่าเช่นกัน วิธีที่ช่วยประหยัดเวลาคือการตัดแต่งแบบมีโครงสร้าง (ทำให้เมทริกซ์เล็กลง) และความเบาบางแบบกึ่งมีโครงสร้าง 2:4 (ซึ่งมีฮาร์ดแวร์รองรับ)
การตัดแต่งแบบมีโครงสร้าง
การตัดแต่งแบบมีโครงสร้างจะลบ neuron, channel หรือ attention head ทั้งชุด สำหรับเลเยอร์เชิงเส้น ที่มี การลบ neuron จะทำให้แถวที่ ของ และสมาชิกที่ ของ เป็นศูนย์:
neuron ที่มี -norm เล็กที่สุดจะถูกลบก่อน วิธีนี้สร้างเมทริกซ์ที่เล็กลงจริง — แต่ต้องสร้างเลเยอร์ใหม่ให้มีขนาดเล็กลงจริง ๆ การทำแถวเป็นศูนย์แล้วคง shape เดิมไม่ลดจำนวน FLOP; ขั้นตอน rebuild ในส่วนการนำไปใช้จะแปลง mask เป็นเมทริกซ์
สำหรับเลเยอร์คอนโวลูชัน วิธีเทียบเท่าคือการตัดแต่งฟิลเตอร์ เมื่อมี ความสำคัญของฟิลเตอร์เอาต์พุต คือ:
การลบฟิลเตอร์ จะลบเอาต์พุตแชนเนลทั้งชุด และลด FLOP ตามสัดส่วน
สมมติฐาน Lottery Ticket

ในปี 2019 Frankle และ Carbin เสนอสมมติฐาน Lottery Ticket (LTH): ภายใน dense network ที่เริ่มต้นด้วยค่าสุ่มมีโครงข่ายย่อยแบบเบาบาง หรือ "winning ticket" ซึ่งเมื่อฝึกจากค่าเริ่มต้นเดิมจะมีความแม่นยำเทียบเท่า full network ด้วยจำนวนรอบการทำซ้ำใกล้เคียงกัน
อย่างเป็นทางการ ให้พิจารณา ที่เริ่มต้นด้วย หลังฝึกจนลู่เข้า เราได้ และสร้าง pruning mask LTH ระบุว่ามี ที่ทำให้:
โดย การทดลองเดิมใช้ MNIST และ CIFAR-10 ซึ่ง winning tickets เหลือพารามิเตอร์ 10-20% แต่ไม่ควรสรุปว่าจะใช้กับสมุดคำสั่งซื้อขายได้ — ฟีเจอร์ LOB ไม่คงที่ และป้ายกำกับมีลักษณะใกล้เคียง noise ซึ่งแตกต่างจากการจำแนกรูปภาพในประเด็นสำคัญ
Iterative Magnitude Pruning (IMP)
ticket ถูกค้นหาด้วย IMP:
- เริ่มต้น network ด้วย
- ฝึกจนลู่เข้าเพื่อให้ได้
- ตัดแต่งน้ำหนัก ที่มีขนาดต่ำสุดและสร้าง mask
- รีเซ็ตน้ำหนักที่เหลือกลับเป็นค่าจาก (rewinding)
- ทำซ้ำจากขั้นที่ 2 ด้วย network ที่มี mask
แต่ละรอบตัดแต่งสัดส่วน (โดยทั่วไป 20%) ดังนั้นหลัง รอบจะเหลือพารามิเตอร์ หลัง 10 รอบที่ จะเหลือประมาณ 10.7%
สามสมมติฐานเกี่ยวกับโมเดลการเทรด ซึ่งยังไม่มีข้อใดถูกทดสอบ
เป็นเรื่องน่าดึงดูดที่จะอ้างว่า LTH ควรทำงานได้ดีกับข้อมูลตลาดเป็นพิเศษ มีเหตุผลในลักษณะนี้อยู่สามข้อ แต่ทั้งสามข้อเป็นเพียงสมมติฐาน และการกล่าวให้เป็นข้อเท็จจริงคือความผิดพลาดแบบที่บล็อกนี้ตั้งใจหลีกเลี่ยง
- สัญญาณทางการเงินมีความเบาบาง ภาพ snapshot ส่วนใหญ่ของสมุดคำสั่งซื้อขายเป็น noise ดังนั้นโครงข่ายย่อยแบบเบาบางอาจสอดคล้องกับสัญญาณแบบเบาบางโดยธรรมชาติ ทดสอบได้โดยเปรียบเทียบ IMP กับ random mask ที่มีความเบาบางเท่ากัน หากความเบาบางเป็นสิ่งที่ทำให้เกิดผลจริง random mask ก็ควรตามหลังไม่มาก
- Winning tickets ใช้ได้ข้าม regime นี่เป็นข้ออ้างเชิงประจักษ์เกี่ยวกับตลาดที่ยังไม่มีแหล่งอ้างอิงรองรับ และเป็นข้อที่น่าสนใจที่สุดในสามข้อ สามารถทดสอบโดยตรงกับป้ายกำกับ regime จาก การตรวจจับ regime ด้วย HMM: ค้นหา ticket ใน regime A แล้วฝึกใหม่ใน regime B จากนั้นเปรียบเทียบกับ ticket ที่ค้นหาโดยตรงใน B
- ความเบาบางทำหน้าที่เป็น regularization ความจุที่ใช้งานจริงลดลงอาจลดการปรับเข้ากับ noise ของ microstructure ซึ่งควรปรากฏเป็นช่องว่างนอกกลุ่มตัวอย่างของโมเดลที่ตัดแต่งแล้ว เล็กกว่า ของโมเดลหนาแน่น ไม่ใช่เพียงใกล้เคียงกัน
การนำไปใช้: ตัดแต่ง Trading MLP

โมเดลพื้นฐาน
import torch
import torch.nn as nn
import torch.nn.utils.prune as prune
from copy import deepcopy
class TradingMLP(nn.Module):
"""MLP for mid-price direction prediction from order book features."""
def __init__(self, input_dim=100, hidden_dim=2048,
num_layers=4, output_dim=3):
super().__init__()
layers = []
dims = [input_dim] + [hidden_dim] * (num_layers - 1) + [output_dim]
for i in range(len(dims) - 1):
layers.append(nn.Linear(dims[i], dims[i + 1]))
if i < len(dims) - 2:
layers.append(nn.BatchNorm1d(dims[i + 1]))
layers.append(nn.ReLU())
layers.append(nn.Dropout(0.1))
self.network = nn.Sequential(*layers)
def forward(self, x):
return self.network(x)
def count_parameters(self):
return sum(p.numel() for p in self.parameters())
model = TradingMLP(input_dim=100, hidden_dim=2048,
num_layers=4, output_dim=3)
print(f"Total parameters: {model.count_parameters():,}")
การตัดแต่ง magnitude แบบไร้โครงสร้าง
def apply_unstructured_pruning(model, sparsity=0.9):
"""Apply global unstructured L1 pruning to all Linear layers."""
parameters_to_prune = []
for name, module in model.named_modules():
if isinstance(module, nn.Linear):
parameters_to_prune.append((module, 'weight'))
prune.global_unstructured(
parameters_to_prune,
pruning_method=prune.L1Unstructured,
amount=sparsity,
)
return model
def compute_sparsity(model):
"""Fraction of zero weights. Note: a *count*, not a speedup."""
total, zeros = 0, 0
for name, param in model.named_parameters():
if 'weight' in name:
total += param.numel()
zeros += (param == 0).sum().item()
return zeros / total if total > 0 else 0
pruned_model = apply_unstructured_pruning(deepcopy(model), sparsity=0.9)
print(f"Sparsity: {compute_sparsity(pruned_model):.1%}")
การตัดแต่งแบบมีโครงสร้าง พร้อม rebuild ที่ทำให้เกิดผลจริง
การทำ mask ให้แถวเป็นเพียงครึ่งหนึ่งของงาน ครึ่งที่ทำให้เร็วขึ้นคือการสร้างแต่ละเลเยอร์ใหม่ให้มีรูปร่างเล็กลง — ซึ่งหมายถึงต้องส่งต่อการลบไปข้างหน้า: เมื่อลบแถว ของเลเยอร์ ก็ต้องลบคอลัมน์ ของเลเยอร์ และแชนเนล ของ BatchNorm1d ที่อยู่ระหว่างนั้นด้วย
def apply_structured_pruning(model, fraction=0.75):
"""Mask entire neurons by L2-norm of their weight rows."""
for name, module in model.named_modules():
if isinstance(module, nn.Linear) and module.out_features > 10:
prune.ln_structured(
module, name='weight', amount=fraction, n=2, dim=0
)
return model
def rebuild_pruned_mlp(model):
"""
Physically shrink a structurally pruned TradingMLP.
Walks the Sequential once. For each Linear: drop the input columns
the previous layer no longer emits, then drop its own dead output
rows. BatchNorm1d channels follow the preceding Linear's survivors.
"""
new_layers = []
keep_in = None # surviving output indices of the previous Linear
for layer in model.network:
if isinstance(layer, nn.Linear):
if prune.is_pruned(layer):
prune.remove(layer, 'weight')
W, b = layer.weight.data, layer.bias.data
keep_out = (W.norm(dim=1) > 0).nonzero(as_tuple=True)[0]
W = W[keep_out]
if keep_in is not None:
W = W[:, keep_in]
new = nn.Linear(W.shape[1], W.shape[0])
new.weight.data = W.clone()
new.bias.data = b[keep_out].clone()
new_layers.append(new)
keep_in = keep_out
elif isinstance(layer, nn.BatchNorm1d):
new = nn.BatchNorm1d(len(keep_in))
new.weight.data = layer.weight.data[keep_in].clone()
new.bias.data = layer.bias.data[keep_in].clone()
new.running_mean = layer.running_mean[keep_in].clone()
new.running_var = layer.running_var[keep_in].clone()
new.num_batches_tracked = layer.num_batches_tracked.clone()
new_layers.append(new)
else: # ReLU, Dropout -- shape-agnostic, reuse as is
new_layers.append(layer)
rebuilt = deepcopy(model)
rebuilt.network = nn.Sequential(*new_layers)
return rebuilt
มีสองสิ่งที่ต้องตรวจสอบก่อนเชื่อถือผลลัพธ์นี้ ตามแนวคิดเดียวกับด่านตรวจสอบความเท่าเทียมที่บทความอื่นในบล็อกใช้:
- รูปร่าง
rebuiltควรแสดงมิติของหน่วยซ่อนเป็น — 512 สำหรับfraction=0.75, — และจำนวนพารามิเตอร์ควรลดลงแบบกำลังสอง เพราะทั้งสองมิติของเมทริกซ์ภายในมีขนาดเล็กลง - เอาต์พุต ในโหมด
eval()ค่าrebuilt(x)ต้องตรงกับเอาต์พุตของโมเดลที่มี mask แต่ยังไม่ได้rebuiltภายในค่าคลาดเคลื่อนของเลขทศนิยม เมื่อใช้ batch เดียวกัน หากไม่ตรง แสดงว่าการส่งต่อคอลัมน์ผิดพลาด และตัวเลข downstream ทุกตัวกำลังวัดโมเดลคนละตัวกับที่คุณคิด
การทดสอบการคงอยู่ของแถวตั้งสมมติฐานว่าแถวที่ถูก mask เป็นศูนย์พอดี และแถวที่ยังใช้งานไม่เป็นศูนย์ ซึ่งเป็นจริงสำหรับเอาต์พุตของ ln_structured; แต่จะไม่จริงหากขั้นตอนอื่นสร้าง neuron ที่ยังใช้งานแต่เป็นศูนย์ทั้งแถว ดังนั้นให้ตรวจสอบจำนวนตัวที่เหลือเทียบกับสัดส่วนที่ร้องขอ แทนการเชื่อผลการทดสอบ norm โดยไม่ตรวจสอบ
Iterative Magnitude Pruning (ค้นหา Lottery Ticket)
def lottery_ticket_search(model_cls, model_kwargs, train_fn, eval_fn,
rounds=10, prune_rate=0.2, device='cpu'):
"""
Iterative Magnitude Pruning to find a winning ticket.
Parameters
----------
model_cls : class -- model constructor
model_kwargs : dict -- constructor arguments
train_fn : callable -- train_fn(model) trains the model in-place
eval_fn : callable -- eval_fn(model) returns out-of-sample accuracy
rounds : int -- number of pruning rounds
prune_rate : float -- fraction of surviving weights pruned per round
"""
model_init = model_cls(**model_kwargs).to(device)
theta_0 = deepcopy(model_init.state_dict())
mask = {}
for name, param in model_init.named_parameters():
if 'weight' in name:
mask[name] = torch.ones_like(param, dtype=torch.bool)
results = []
for round_idx in range(rounds):
model = model_cls(**model_kwargs).to(device)
state = deepcopy(theta_0)
for name in mask:
state[name] = state[name] * mask[name].float()
model.load_state_dict(state)
train_fn(model)
acc = eval_fn(model)
surviving = sum(m.sum().item() for m in mask.values())
total = sum(m.numel() for m in mask.values())
sparsity = 1.0 - surviving / total
results.append({
'round': round_idx,
'accuracy': acc,
'sparsity': sparsity,
'surviving_params': int(surviving)
})
print(f"Round {round_idx}: acc={acc:.4f}, "
f"sparsity={sparsity:.1%}")
all_weights = []
for name, param in model.named_parameters():
if name in mask:
alive = param.data.abs()[mask[name]]
all_weights.append(alive.flatten())
all_weights = torch.cat(all_weights)
k = int(len(all_weights) * prune_rate)
if k == 0:
break
threshold = all_weights.kthvalue(k).values.item()
for name, param in model.named_parameters():
if name in mask:
mask[name] = mask[name] & (
param.data.abs() >= threshold
)
return results, mask
results คือข้อมูลดิบสำหรับเส้นโค้งความเบาบางเทียบกับความแม่นยำที่บทความนี้ยังติดค้างผู้อ่านไว้ eval_fn ต้องวัดนอกกลุ่มตัวอย่างจริงบน split ที่ purge แล้ว — การรัน IMP ที่ให้คะแนนในกลุ่มตัวอย่างจะรายงานเส้นโค้งสวยงามแต่ไม่มีความหมาย
การวัดผล

วัดเวลาแฝงด้วยธรรมเนียมของ harness เดียวกับบทความอื่นในบล็อก — ไม่รวมช่วง warmup ใช้ค่าที่ดีที่สุดจาก N ครั้ง และรายงาน p50/p95/p99 แทนค่าเฉลี่ย — โดยมีขั้นตอนและโค้ดอยู่ใน Polars เทียบกับ pandas มีสามประเด็นเฉพาะสำหรับการตัดแต่ง:
- วัด benchmark ของโมเดลที่ สร้างใหม่ ไม่ใช่โมเดลที่มีเพียง mask โมเดลที่มี mask เมื่อใช้ batch size 1 ยังคงวัดรูปร่างแบบหนาแน่น
- รายงาน batch size ด้วย Batch 1 (ลูปการเสนอราคา) และ batch 256 (การกวาดการทดลองเพื่อการวิจัย) อยู่คนละฝั่งของเส้นแบ่งระหว่าง memory-bound กับ compute-bound และการตัดแต่งให้ประโยชน์กับสองกรณีไม่เท่ากัน
- รายงานความแม่นยำบน split เดียวกัน ที่ horizon เดียวกัน พร้อมระบุคำจำกัดความของ label ตารางเวลาแฝงที่ไม่มีคอลัมน์ความแม่นยำประกอบก็เท่ากับเสนอให้ลบโมเดลทิ้งทั้งหมด
เทคนิคขั้นสูง

การตัดแต่งร่วมกับการกลั่นความรู้
แทนที่จะตัดแต่งและ fine-tune แยกกัน ให้ใช้โมเดลหนาแน่นดั้งเดิมเป็น teacher ส่วน student ที่ถูกตัดแต่งจะลดฟังก์ชันสูญเสียแบบผสมระหว่าง task loss กับ KL divergence จากการแจกแจงเอาต์พุตของ teacher:
โดย และ คือ logits ของ teacher และ student, คือ temperature และ ใช้ถ่วงดุลวัตถุประสงค์ทั้งสอง ปัจจัย จะปรับสเกล gradient ของการกลั่นความรู้ ซึ่งหากไม่มีปัจจัยนี้จะหดลงเป็น
def distillation_loss(student_logits, teacher_logits, labels,
temperature=3.0, alpha=0.5):
"""Combined task + distillation loss."""
task_loss = nn.CrossEntropyLoss()(student_logits, labels)
soft_student = nn.functional.log_softmax(
student_logits / temperature, dim=-1
)
soft_teacher = nn.functional.softmax(
teacher_logits / temperature, dim=-1
)
kd_loss = nn.functional.kl_div(
soft_student, soft_teacher, reduction='batchmean'
)
return (1 - alpha) * task_loss + alpha * (temperature ** 2) * kd_loss
Movement Pruning
แทนที่จะตัดแต่งตามขนาดสัมบูรณ์ movement pruning (Sanh et al., 2020) จะตัดแต่งน้ำหนักที่กำลังเคลื่อนเข้าใกล้ศูนย์ระหว่างการฝึก คะแนนความสำคัญสะสมผลคูณระหว่าง gradient กับน้ำหนัก:
น้ำหนักที่มีคะแนนเป็นลบจะถูกตัดแต่ง เหตุผลที่เลือกวิธีนี้แทนการตัดแต่งตามขนาดเกี่ยวข้องกับ fine-tuning โดยเฉพาะ: เมื่อปรับโมเดลที่ฝึกไว้ล่วงหน้า การแจกแจงของขนาดถูกกำหนดโดยงานก่อนฝึก ดังนั้นขนาดจึงเป็นสัญญาณความสำคัญที่ล้าสมัย ส่วนทิศทางการเคลื่อนที่เป็นสัญญาณที่ใหม่กว่า สำหรับโมเดลการเทรดที่ฝึกใหม่บนหน้าต่างแบบเลื่อน สถานการณ์นี้พบได้บ่อยกว่าการฝึกจากศูนย์
ความเบาบางแบบมีโครงสร้าง 2:4 ของ NVIDIA
GPU NVIDIA รุ่น Ampere และรุ่นใหม่กว่ารองรับความเบาบางแบบมีโครงสร้าง 2:4 ในฮาร์ดแวร์: จากน้ำหนักที่ต่อเนื่องกันทุก 4 ตัว ต้องมีศูนย์พอดี 2 ตัว
นี่คือรูปแบบความเบาบางแบบละเอียดรูปแบบเดียวที่ฮาร์ดแวร์ให้ประโยชน์จริง จึงสำคัญกว่าตัวเลขศูนย์ 90% จากการตัดแต่งแบบไม่มีโครงสร้าง ข้อจำกัดนี้เป็นแบบเฉพาะที่ ไม่ใช่ระดับ global — ไม่สนใจว่า สองตัวใด ในทุกกลุ่มสี่ตัวจะเหลืออยู่ — จึงเป็นข้อจำกัดที่อ่อนกว่าการกำหนด global mask มาก แม้ว่าระดับความเบาบางที่มีให้เลือกจะมีเพียง 50% ก็ตาม
from torch.ao.pruning import WeightNormSparsifier
sparsifier = WeightNormSparsifier(
sparsity_level=0.5,
sparse_block_shape=(1, 4),
zeros_per_block=2,
)
sparsifier.prepare(
model, config=[{"tensor_fqn": "network.0.weight"}]
)
sparsifier.step()
sparsifier.squash_mask()
การจะได้ความเร็วเพิ่มจริงต้องให้เส้นทางการอนุมานใช้ sparse tensor cores — ผ่านการ export เป็น ONNX แล้ว build ด้วย TensorRT หรือใช้ torch.sparse.to_sparse_semi_structured การ export โมเดลที่มี mask 2:4 แล้วรันผ่าน dense runtime จะทำให้เสียความแม่นยำโดยไม่ได้ประโยชน์ด้านความเร็วเลย
การนำไปใช้งานจริงในระบบผลิต

การตรวจสอบ
โมเดลที่ถูกตัดแต่งคือโมเดลใหม่ ไม่ใช่โมเดลเดิมที่ถูกบีบอัด และต้องผ่านด่านยอมรับเดียวกับ candidate อื่นทั้งหมด: ฝึกใหม่แบบหน้าต่างเลื่อนและตรวจสอบซ้ำนอกกลุ่มตัวอย่างตาม การเพิ่มประสิทธิภาพแบบ walk-forward พร้อมแก้ผลจากการคัดเลือกตาม deflated Sharpe ratio การแก้นี้ไม่ใช่สิ่งที่เลือกทำหรือไม่ทำก็ได้ — IMP สร้างโมเดล candidate เป็น ลำดับ ดังนั้นระดับความเบาบางที่ดูดีที่สุดจากสิบรอบถูกเลือกผ่านการค้นหา และค่า Sharpe ต้อง deflate ตามจำนวนการทดลองที่มีผลจริง กฎตายตัวอย่าง "ปฏิเสธถ้า Sharpe ลดลงเกิน 5%" ไม่ผ่านคณิตศาสตร์นี้ จึงไม่มีการใช้กฎดังกล่าวในบทความนี้
การซ้อน quantization
การตัดแต่งสามารถใช้ร่วมกับ quantization ได้ โมเดลที่มีความเบาบาง 90% และทำ quantization เป็น INT8 มีอัตราการบีบอัดดังนี้:
โมเดลขนาด 48 MB จะเหลือ 1.2 MB นี่เป็นเพียงข้ออ้างด้านพื้นที่จัดเก็บเท่านั้น ไม่ได้บอกอย่างอื่น การที่โมเดลขนาด 1.2 MB จะให้การตัดสินใจแบบเดียวกันหรือไม่เป็นคำถามอีกข้อที่ต้องมีคำตอบแยกต่างหาก และ กับดักความแม่นยำของ GPU คือเหตุผลที่ต้องถามคำถามนี้: บล็อกนี้แสดงให้เห็นแล้วว่า fp32 เพียงอย่างเดียวทำให้เกิด relative error เท่ากับ 211 ในการคำนวณ backtest ที่ดูสมเหตุสมผลทุกประการ INT8 เป็นการลดความละเอียดที่รุนแรงกว่านั้นมาก ให้ deploy โมเดลที่ทำ quantization และตัดแต่งแล้วก็ต่อเมื่อผ่านด่าน parity ที่วัดเป็นตัวเลขเทียบกับโมเดลหนาแน่น fp32 — ต้องดูอัตราการตัดสินใจตรงกันและส่วนต่าง PnL ในช่วงที่กันไว้นอกการฝึก ไม่ใช่คำรับรองลอย ๆ
การติดตามผล
โมเดลที่ถูกตัดแต่งอาจไวต่อการเปลี่ยนแปลงของการแจกแจงมากขึ้น สิ่งที่ควรติดตามมีดังนี้:
- ความเบาบางของ activation หาก neuron ที่เหลือปล่อยค่าเป็นศูนย์เกือบทั้งหมด โมเดลที่ใช้งานจริงจะเล็กกว่าที่ตั้งใจและน่าจะกำลังเสื่อมลง
- Gradient norms ระหว่างการฝึกใหม่ gradient ที่พุ่งระเบิดบ่งชี้ว่าโครงข่ายย่อยที่เหลือถูกบังคับให้ชดเชยส่วนที่ถูกลบอย่างรุนแรงเกินไป
- เอนโทรปีของการทำนาย โมเดลที่ถูกตัดแต่งแล้วมีความมั่นใจเกินจริงกับข้อมูล microstructure ที่มี noise มักกำลังปรับเข้ากับ regime ของชุดฝึก
บทสรุป

วิธีการเหล่านี้เป็นที่ยอมรับกันดี และจนกว่าจะรันการกวาดการทดลอง บทความนี้อ้างเพียงเท่านั้น การตัดแต่งแบบไม่มีโครงสร้างให้ตัวเลขความเบาบางแต่ไม่ให้ความเร็วเพิ่ม การตัดแต่งแบบมีโครงสร้างให้ความเร็วเพิ่มก็ต่อเมื่อ — และเฉพาะเมื่อ — สร้างเลเยอร์ใหม่ให้เล็กลง แทนที่จะเพียงใส่ mask สมมติฐาน Lottery Ticket เสนอว่าโมเดลขนาดกะทัดรัดมีอยู่แล้วภายในโมเดลที่มีพารามิเตอร์มากเกินไป แม้จะแสดงให้เห็นบน benchmark รูปภาพ ไม่ใช่ข้อมูลสมุดคำสั่งซื้อขาย และเหตุผลสามข้อที่กล่าวว่าโมเดลนี้ "ควร" ใช้ได้กับข้อมูลตลาดเป็นเพียงสมมติฐานที่ผูกกับการทดลอง ไม่ใช่ข้อค้นพบ
หลักปฏิบัติจากวรรณกรรมคือฝึกโมเดลให้ใหญ่แล้วค่อยตัดแต่ง แทนการออกแบบให้เล็กตั้งแต่ต้น: โมเดลใหญ่สำรวจ loss landscape ได้มีประสิทธิภาพกว่า และการตัดแต่งจะรักษาเส้นทางที่มีความสำคัญไว้ สำหรับโมเดลการเทรด หลักการนี้จะใช้ได้หรือไม่ ที่ระดับความเบาบางเท่าไร และต้องแลกด้วยความแม่นยำเท่าไร จะรู้ได้จากการกวาด IMP เพียงครั้งเดียว — และควรกลับมาอ่านบทความนี้อีกครั้งหลังการกวาดนั้น โดยมีตัวเลขจริงอยู่ในบทความ
ผู้เขียน
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.