tungman's picture
Upload 13 files
a6fae72 verified
Raw
History Blame Contribute Delete
10.1 kB
import torch
import torch.nn as nn
__all__ = [
"DynamicBetaSmoothL1Loss",
"DynamicBetaSmoothL1LossMultiStep",
"DynamicMultiStepHLoss",
"DynamicMultiStepQLoss",
]
import torch
import torch.nn as nn
from scipy.ndimage import gaussian_filter1d
class DynamicMultiStepHLoss(nn.Module):
def __init__(self, init_beta=1.0, alpha=0.01,
horizon_decay=0.9, reduction='mean',
min_beta=0.1, update_freq=50,
smooth_sigma=0.0):
"""
Dynamic Multi-step Huber Loss with horizon decay + optional Gaussian smoothing
Args:
init_beta: initial huber beta
alpha: update rate for beta
horizon_decay: decay factor for weighting steps
reduction: 'mean', 'sum', 'none'
min_beta: lower bound for beta
update_freq: frequency (steps) to update beta
smooth_sigma: if > 0, apply gaussian_filter1d to errors along horizon
"""
super().__init__()
self.register_buffer('beta', torch.tensor(float(init_beta)))
self.alpha = alpha
self.horizon_decay = horizon_decay
self.reduction = reduction
self.min_beta = min_beta
self.update_freq = update_freq
self.register_buffer('step_count', torch.tensor(0))
self.smooth_sigma = smooth_sigma
def _align_shapes(self, pred, true):
"""
Align shapes for pred and true.
1. Truncate horizon to min(pred, true)
2. Match batch size by slicing/repeating target if needed
3. Handle extra dimensions
"""
# Squeeze singleton dimensions
if pred.dim() == 3 and pred.shape[2] == 1:
pred = pred.squeeze(-1)
if true.dim() == 3 and true.shape[2] == 1:
true = true.squeeze(-1)
if pred.dim() == 3 and pred.shape[1] == 1:
pred = pred.squeeze(1)
if true.dim() == 3 and true.shape[1] == 1:
true = true.squeeze(1)
# Make sure 2D (B,H)
if pred.dim() != 2:
pred = pred.view(pred.shape[0], -1)
if true.dim() != 2:
true = true.view(true.shape[0], -1)
Bp, Hp = pred.shape
Bt, Ht = true.shape
# Truncate horizon to match pred
H = min(Hp, Ht)
pred = pred[:, :H]
true = true[:, :H]
# Align batch size
if Bt < Bp:
# repeat target along batch
reps = (Bp + Bt - 1) // Bt # ceil
true = true.repeat(reps, 1)[:Bp, :]
elif Bt > Bp:
# slice target batch
true = true[:Bp, :]
return pred, true
def _apply_smoothing(self, diff):
"""Apply gaussian_filter1d along horizon axis (dim=1)"""
if self.smooth_sigma and self.smooth_sigma > 0:
diff_np = diff.detach().cpu().numpy()
smoothed = gaussian_filter1d(diff_np, sigma=self.smooth_sigma, axis=1)
return torch.tensor(smoothed, dtype=diff.dtype, device=diff.device)
return diff
def forward(self, pred, true):
self.step_count += 1
pred, true = self._align_shapes(pred, true)
# Absolute error
diff = torch.abs(pred - true)
# Apply smoothing on error if enabled
diff = self._apply_smoothing(diff)
B, H = diff.shape
# Horizon weights
with torch.no_grad():
step_idx = torch.arange(H, device=diff.device, dtype=torch.float32)
weights = torch.pow(self.horizon_decay, step_idx)
weights = weights / weights.sum()
weights = weights.unsqueeze(0) # (1,H)
# Huber loss
beta = max(self.beta.item(), self.min_beta)
loss = torch.where(
diff < beta,
0.5 * (diff ** 2) / beta,
diff - 0.5 * beta
)
# Apply horizon weights
weighted_loss = loss * weights
# Beta update
if self.step_count % self.update_freq == 0:
with torch.no_grad():
median_error = torch.median(diff).item()
new_beta = (1 - self.alpha) * self.beta.item() + self.alpha * median_error
self.beta.fill_(max(new_beta, self.min_beta))
if self.reduction == 'mean':
return weighted_loss.mean()
elif self.reduction == 'sum':
return weighted_loss.sum()
elif self.reduction == 'none':
return weighted_loss
else:
return weighted_loss.mean()
def get_beta(self):
return self.beta.item()
class DynamicBetaSmoothL1Loss(nn.Module):
def __init__(self, init_beta=0.5, alpha=0.1, reduction='mean'):
super(DynamicBetaSmoothL1Loss, self).__init__()
self.beta = init_beta
self.alpha = alpha
self.reduction = reduction
def forward(self, pred, target):
# ensure tensor type
pred = torch.as_tensor(pred, device=target.device, dtype=target.dtype)
target = torch.as_tensor(target, device=target.device, dtype=target.dtype)
diff = torch.abs(pred - target)
# ✅ median ต้อง detach ไม่งั้นจะลาก graph
current_beta = torch.median(diff.detach())
self.beta = (1 - self.alpha) * self.beta + self.alpha * current_beta.item()
loss = torch.where(
diff < self.beta,
0.5 * (diff ** 2) / self.beta,
diff - 0.5 * self.beta
)
if self.reduction == 'mean':
return loss.mean()
elif self.reduction == 'sum':
return loss.sum()
return loss
class DynamicBetaSmoothL1LossMultiStep(nn.Module):
def __init__(self, init_beta=0.5, alpha=0.1, reduction='mean', eps=1e-6):
super().__init__()
self.register_buffer('beta', torch.tensor(init_beta))
self.alpha = alpha
self.reduction = reduction
self.eps = eps
def forward(self, pred, target):
# pred, target: (B, horizon, 1)
diff = torch.abs(pred - target)
# Dynamic beta per batch
batch_beta = diff.mean(dim=(0,1)).detach() + self.eps
self.beta = self.alpha * self.beta + (1 - self.alpha) * batch_beta
beta = self.beta
# Smooth L1 per element
loss = torch.where(diff < beta, 0.5 * diff**2 / beta, diff - 0.5*beta)
if self.reduction=='mean':
return loss.mean()
elif self.reduction=='sum':
return loss.sum()
else:
return loss
class DynamicMultiStepQLoss(nn.Module):
def __init__(self, quantiles=(0.5,), weight=1.0, step_decay=0.9, reduction='mean'):
"""
Multi-step Quantile Loss (Pinball loss) พร้อม step weighting
รองรับ target/pred หลาย step
Args:
quantiles: list/tuple ของ quantiles (เช่น [0.1, 0.5, 0.9])
weight: global weight factor
step_decay: decay สำหรับ step หลัง ๆ
reduction: 'mean', 'sum', หรือ None
"""
super().__init__()
self.quantiles = torch.tensor(quantiles, dtype=torch.float32)
self.weight = float(weight)
self.step_decay = float(step_decay)
self.reduction = reduction
@staticmethod
def _to_bh(x):
"""บังคับ tensor ให้อยู่รูป (B,H)"""
if x.dim() == 1: # (H,) -> (1,H)
x = x.unsqueeze(0)
if x.dim() == 2: # (B,H) OK
return x
if x.dim() == 3:
# (B,H,1)
if x.size(-1) == 1:
return x.squeeze(-1)
# (B,1,H)
if x.size(1) == 1:
return x.squeeze(1)
# (B,H,2) เช่น high/low -> เลือกช่องแรก
if x.size(-1) == 2:
return x[..., 0]
if x.dim() >= 3:
B = x.size(0)
return x.reshape(B, -1)
return x
def forward(self, pred, true):
"""
pred: (B,H,Q) หรือ (B,H) ถ้า Q=1
true: (B,H)
"""
pred = torch.as_tensor(pred)
true = torch.as_tensor(true)
# normalize shape
true = self._to_bh(true) # (B,H)
if pred.dim() == 2:
# กรณีไม่มี quantile dim -> เพิ่มเข้ามา
pred = pred.unsqueeze(-1) # (B,H,1)
if pred.dim() == 3:
Bp, Hp, Qp = pred.shape
Bt, Ht = true.shape
if Bp != Bt:
raise ValueError(f"[DynamicMultiStepQLoss] batch mismatch: pred {pred.shape}, true {true.shape}")
H = min(Hp, Ht)
pred = pred[:, :H, :]
true = true[:, :H]
else:
raise ValueError(f"[DynamicMultiStepQLoss] pred shape must be (B,H) or (B,H,Q), got {pred.shape}")
# ขยาย true ให้เท่ากับ pred
true = true.unsqueeze(-1).expand_as(pred) # (B,H,Q)
# step weights
H = pred.size(1)
step_weights = torch.pow(
torch.tensor(self.step_decay, dtype=pred.dtype, device=pred.device),
torch.arange(H, dtype=pred.dtype, device=pred.device)
).view(1, H, 1) # (1,H,1)
# quantiles tensor
quantiles = self.quantiles.to(pred.device).view(1, 1, -1) # (1,1,Q)
# Pinball loss
diff = true - pred # (B,H,Q)
loss = torch.max(quantiles * diff, (quantiles - 1) * diff)
# apply weights
loss = loss * step_weights * self.weight
if self.reduction == 'mean':
return loss.mean()
elif self.reduction == 'sum':
return loss.sum()
else:
return loss