Models API¶
Model registry¶
The registry (pytorch_model_dict, pytorch_criterion_dict, pytorch_opt_dict) maps model / loss / optimizer name strings to their classes.
Author: Wenyu Ouyang Date: 2021-12-31 11:08:29 <LastEditTime: 2025-11-22 16:41:37 LastEditors: Wenyu Ouyang Description: Dicts including models (which are seq-first), losses, and optims FilePath: orchhydro orchhydro\models\model_dict_function.py Copyright (c) 2021-2022 Wenyu Ouyang. All rights reserved.
Loss functions¶
Author: Wenyu Ouyang Date: 2021-12-31 11:08:29 LastEditTime: 2025-12-07 09:51:25 LastEditors: Wenyu Ouyang Description: Loss functions FilePath: orchhydro orchhydro\models\crits.py Copyright (c) 2021-2022 Wenyu Ouyang. All rights reserved.
DynamicTaskPrior (Module)
¶
Dynamic Task Prioritization
This method is proposed in https://openaccess.thecvf.com/content_ECCV_2018/html/Michelle_Guo_Focus_on_the_ECCV_2018_paper.html In contrast to UW and other curriculum learning methods, where easy tasks are prioritized above difficult tasks, It shows the importance of prioritizing difficult tasks first. It automatically prioritize more difficult tasks by adaptively adjusting the mixing weight of each task's loss. Here we choose correlation as KPI. As KPI must be in [0,1], we set (corr+1)/2 as KPI
Source code in torchhydro/models/crits.py
class DynamicTaskPrior(torch.nn.Module):
r"""Dynamic Task Prioritization
This method is proposed in https://openaccess.thecvf.com/content_ECCV_2018/html/Michelle_Guo_Focus_on_the_ECCV_2018_paper.html
In contrast to UW and other curriculum learning methods, where easy tasks are prioritized above difficult tasks,
It shows the importance of prioritizing difficult tasks first.
It automatically prioritize more difficult tasks by adaptively adjusting the mixing weight of each task's loss.
Here we choose correlation as KPI. As KPI must be in [0,1], we set (corr+1)/2 as KPI
"""
def __init__(
self,
loss_funcs: Union[torch.nn.Module, list],
data_gap: list = None,
device: list = None,
limit_part: list = None,
gamma=2,
alpha=0.5,
):
"""
Parameters
----------
loss_funcs
data_gap
device
limit_part
gamma
the example-level focusing parameter
alpha
default is 1, which means we only use the newest KPI value
"""
if data_gap is None:
data_gap = [0, 2]
if device is None:
device = [0]
super(DynamicTaskPrior, self).__init__()
self.loss_funcs = loss_funcs
self.data_gap = data_gap
self.device = get_the_device(device)
self.limit_part = limit_part
self.gamma = gamma
self.alpha = alpha
def forward(self, output, target, kpi_last=None):
"""
Parameters
----------
output
model's prediction
target
observation
kpi_last
the KPI value of last iteration; each element for an output
It use a moving average KPI as the weighting coefficient: KPI_i = alpha * KPI_i + (1-alpha) * KPI_{i-1}
Returns
-------
torch.Tensor
multi-task loss by Dynamic Task Prioritization method
"""
n_out = target.shape[-1]
loss = 0
kpis = torch.zeros(n_out).to(self.device)
for k in range(n_out):
if self.limit_part is not None and k in self.limit_part:
continue
p0 = output[:, :, k]
t0 = target[:, :, k]
mask = t0 == t0
p = p0[mask]
t = t0[mask]
if self.data_gap[k] > 0:
p, t = deal_gap_data(p0, t0, self.data_gap[k], self.device)
if type(self.loss_funcs) is list:
temp = self.loss_funcs[k](p, t)
else:
temp = self.loss_funcs(p, t)
# kpi must be in [0, 1], as corr's range is [-1, 1], just trans corr to (corr+1)/2
kpi = (torch.corrcoef(torch.stack([p, t], 1).T)[0, 1] + 1) / 2
if self.alpha < 1:
assert kpi_last is not None
kpi = kpi * self.alpha + kpi_last[k] * (1 - self.alpha)
# if we exclude kpi from the backward, it trans to a normal multi-task model
# kpi = kpi.detach().clone() * self.alpha + kpi_last[k] * (1 - self.alpha)
kpis[k] = kpi
# focal loss
fl = -((1 - kpi) ** self.gamma) * torch.log(kpi)
loss += torch.sum(fl * temp, -1)
# if kpi has grad_fn, backward will repeat. It won't work
return loss, kpis.detach().clone()
__init__(self, loss_funcs, data_gap=None, device=None, limit_part=None, gamma=2, alpha=0.5)
special
¶
Parameters¶
loss_funcs data_gap device limit_part gamma the example-level focusing parameter alpha default is 1, which means we only use the newest KPI value
Source code in torchhydro/models/crits.py
def __init__(
self,
loss_funcs: Union[torch.nn.Module, list],
data_gap: list = None,
device: list = None,
limit_part: list = None,
gamma=2,
alpha=0.5,
):
"""
Parameters
----------
loss_funcs
data_gap
device
limit_part
gamma
the example-level focusing parameter
alpha
default is 1, which means we only use the newest KPI value
"""
if data_gap is None:
data_gap = [0, 2]
if device is None:
device = [0]
super(DynamicTaskPrior, self).__init__()
self.loss_funcs = loss_funcs
self.data_gap = data_gap
self.device = get_the_device(device)
self.limit_part = limit_part
self.gamma = gamma
self.alpha = alpha
forward(self, output, target, kpi_last=None)
¶
Parameters¶
output model's prediction target observation
kpi_last the KPI value of last iteration; each element for an output It use a moving average KPI as the weighting coefficient: KPI_i = alpha * KPI_i + (1-alpha) * KPI_{i-1}
Returns¶
torch.Tensor multi-task loss by Dynamic Task Prioritization method
Source code in torchhydro/models/crits.py
def forward(self, output, target, kpi_last=None):
"""
Parameters
----------
output
model's prediction
target
observation
kpi_last
the KPI value of last iteration; each element for an output
It use a moving average KPI as the weighting coefficient: KPI_i = alpha * KPI_i + (1-alpha) * KPI_{i-1}
Returns
-------
torch.Tensor
multi-task loss by Dynamic Task Prioritization method
"""
n_out = target.shape[-1]
loss = 0
kpis = torch.zeros(n_out).to(self.device)
for k in range(n_out):
if self.limit_part is not None and k in self.limit_part:
continue
p0 = output[:, :, k]
t0 = target[:, :, k]
mask = t0 == t0
p = p0[mask]
t = t0[mask]
if self.data_gap[k] > 0:
p, t = deal_gap_data(p0, t0, self.data_gap[k], self.device)
if type(self.loss_funcs) is list:
temp = self.loss_funcs[k](p, t)
else:
temp = self.loss_funcs(p, t)
# kpi must be in [0, 1], as corr's range is [-1, 1], just trans corr to (corr+1)/2
kpi = (torch.corrcoef(torch.stack([p, t], 1).T)[0, 1] + 1) / 2
if self.alpha < 1:
assert kpi_last is not None
kpi = kpi * self.alpha + kpi_last[k] * (1 - self.alpha)
# if we exclude kpi from the backward, it trans to a normal multi-task model
# kpi = kpi.detach().clone() * self.alpha + kpi_last[k] * (1 - self.alpha)
kpis[k] = kpi
# focal loss
fl = -((1 - kpi) ** self.gamma) * torch.log(kpi)
loss += torch.sum(fl * temp, -1)
# if kpi has grad_fn, backward will repeat. It won't work
return loss, kpis.detach().clone()
FloodBaseLoss (Module, ABC)
¶
Abstract base class for flood-related loss functions.
All flood-related loss functions should inherit from this class. The labels tensor is expected to have the flood mask as the last column.
Source code in torchhydro/models/crits.py
class FloodBaseLoss(torch.nn.Module, ABC):
"""
Abstract base class for flood-related loss functions.
All flood-related loss functions should inherit from this class.
The labels tensor is expected to have the flood mask as the last column.
"""
def __init__(self):
super(FloodBaseLoss, self).__init__()
@abstractmethod
def compute_flood_loss(
self, predictions: torch.Tensor, targets: torch.Tensor, flood_mask: torch.Tensor
) -> torch.Tensor:
"""
Compute the flood-aware loss.
Parameters
----------
predictions : torch.Tensor
Model predictions [batch_size, seq_len, output_features]
targets : torch.Tensor
Target values [batch_size, seq_len, output_features]
flood_mask : torch.Tensor
Flood mask [batch_size, seq_len] (1 for flood, 0 for normal)
Returns
-------
torch.Tensor
Computed loss value
"""
pass
def forward(
self, predictions: torch.Tensor, targets: torch.Tensor, flood_mask: torch.Tensor
) -> torch.Tensor:
"""
Forward pass that calls the abstract compute_flood_loss method.
Parameters
----------
predictions : torch.Tensor
Model predictions [batch_size, seq_len, output_features]
targets : torch.Tensor
Target values [batch_size, seq_len, output_features]
flood_mask : torch.Tensor
Flood mask [batch_size, seq_len] (1 for flood, 0 for normal)
Returns
-------
torch.Tensor
Computed loss value
"""
return self.compute_flood_loss(predictions, targets, flood_mask)
compute_flood_loss(self, predictions, targets, flood_mask)
¶
Compute the flood-aware loss.
Parameters¶
predictions : torch.Tensor Model predictions [batch_size, seq_len, output_features] targets : torch.Tensor Target values [batch_size, seq_len, output_features] flood_mask : torch.Tensor Flood mask [batch_size, seq_len] (1 for flood, 0 for normal)
Returns¶
torch.Tensor Computed loss value
Source code in torchhydro/models/crits.py
@abstractmethod
def compute_flood_loss(
self, predictions: torch.Tensor, targets: torch.Tensor, flood_mask: torch.Tensor
) -> torch.Tensor:
"""
Compute the flood-aware loss.
Parameters
----------
predictions : torch.Tensor
Model predictions [batch_size, seq_len, output_features]
targets : torch.Tensor
Target values [batch_size, seq_len, output_features]
flood_mask : torch.Tensor
Flood mask [batch_size, seq_len] (1 for flood, 0 for normal)
Returns
-------
torch.Tensor
Computed loss value
"""
pass
forward(self, predictions, targets, flood_mask)
¶
Forward pass that calls the abstract compute_flood_loss method.
Parameters¶
predictions : torch.Tensor Model predictions [batch_size, seq_len, output_features] targets : torch.Tensor Target values [batch_size, seq_len, output_features] flood_mask : torch.Tensor Flood mask [batch_size, seq_len] (1 for flood, 0 for normal)
Returns¶
torch.Tensor Computed loss value
Source code in torchhydro/models/crits.py
def forward(
self, predictions: torch.Tensor, targets: torch.Tensor, flood_mask: torch.Tensor
) -> torch.Tensor:
"""
Forward pass that calls the abstract compute_flood_loss method.
Parameters
----------
predictions : torch.Tensor
Model predictions [batch_size, seq_len, output_features]
targets : torch.Tensor
Target values [batch_size, seq_len, output_features]
flood_mask : torch.Tensor
Flood mask [batch_size, seq_len] (1 for flood, 0 for normal)
Returns
-------
torch.Tensor
Computed loss value
"""
return self.compute_flood_loss(predictions, targets, flood_mask)
FloodLoss (FloodBaseLoss)
¶
Source code in torchhydro/models/crits.py
class FloodLoss(FloodBaseLoss):
def __init__(
self,
loss_func: Union[torch.nn.Module, str] = "MSELoss",
flood_weight: float = 1.0,
non_flood_weight: float = 0,
flood_strategy: str = "weight",
flood_focus_factor: float = 2.0,
use_score_weighting: bool = False,
score_weight_min: float = 0.5,
score_weight_max: float = 2.0,
score_alpha: float = 2.0,
device: list = None,
**kwargs,
):
"""
General flood-aware loss function with configurable base loss and strategy.
Parameters
----------
loss_func : Union[torch.nn.Module, str]
Base loss function to use. Can be a PyTorch loss function or string name.
Supported strings: "MSELoss", "MAELoss", "RMSELoss", "L1Loss"
flood_weight : float
Weight multiplier for flood events when using binary weighting (use_score_weighting=False), default is 2.0
non_flood_weight : float
Weight multiplier for non-flood events (flood_mask <= 0), default is 1.0
flood_strategy : str
Strategy for handling flood events:
- "weight": Apply higher weights to flood events
- "focal": Use focal loss approach based on flood event frequency
flood_focus_factor : float
Factor for focal loss when using "focal" strategy, default is 2.0
use_score_weighting : bool
If True, use data quality scores (from flood_mask values 1-100) as loss weights.
flood_mask: -1/0 = non-flood (weight=non_flood_weight), 1-100 = flood quality score
Default is False (use binary weighting: 0/1).
score_weight_min : float
Minimum weight for lowest quality score (score=1), default is 0.5
score_weight_max : float
Maximum weight for highest quality score (score=100), default is 2.0
score_alpha : float
Power factor controlling high-quality data weight proportion.
weight(score) = base + (score/100)^alpha * (max - base)
- alpha > 1: emphasize high-quality data (steeper curve)
- alpha < 1: smoother weight distribution
- alpha = 1: linear mapping
Default is 2.0 (quadratic, emphasizing high scores).
device : list
Device configuration, default is None (auto-detect)
"""
super(FloodLoss, self).__init__()
self.flood_weight = flood_weight
self.non_flood_weight = non_flood_weight
self.flood_strategy = flood_strategy
self.flood_focus_factor = flood_focus_factor
self.use_score_weighting = use_score_weighting
self.score_weight_min = score_weight_min
self.score_weight_max = score_weight_max
self.score_alpha = score_alpha
self.device = get_the_device(device if device is not None else [0])
# Initialize epoch-level statistics for score weighting
self.reset_statistics()
# Initialize base loss function
self.base_loss_func = self._initialize_base_loss(loss_func, kwargs)
def reset_statistics(self):
"""Reset epoch-level statistics for score weighting."""
self.epoch_stats = {
'total_batches': 0,
'total_flood_events': 0,
'score_sum': 0.0,
'score_min': float('inf'),
'score_max': float('-inf'),
'all_scores': [],
'high_quality_count': 0, # score >= 90
'perfect_score_count': 0, # score == 100
'perfect_score_positions': [], # List of (batch_idx, timestep, batch_num)
'weight_sum': 0.0,
'weight_min': float('inf'),
'weight_max': float('-inf'),
}
def _initialize_base_loss(self, loss_func, kwargs):
"""Initialize base loss function."""
if isinstance(loss_func, str):
loss_dict = {
# NOTE: reduction="none" is important, otherwise the loss will be reduced to a scalar
"MSELoss": torch.nn.MSELoss(reduction="none"),
"MAELoss": torch.nn.L1Loss(reduction="none"),
"L1Loss": torch.nn.L1Loss(reduction="none"),
"RMSELoss": RMSELoss(),
"HybridLoss": HybridLoss(
kwargs.get("mae_weight", 0.5), reduction="none"
),
}
if loss_func in loss_dict:
return loss_dict[loss_func]
else:
raise ValueError(f"Unsupported loss function string: {loss_func}")
else:
return loss_func
def compute_flood_loss(
self, predictions: torch.Tensor, targets: torch.Tensor, flood_mask: torch.Tensor
) -> torch.Tensor:
"""
Compute flood-aware loss using the specified strategy.
Parameters
----------
predictions : torch.Tensor
Model predictions [batch_size, seq_len, output_features]
targets : torch.Tensor
Target values [batch_size, seq_len, output_features]
flood_mask : torch.Tensor
Flood mask [batch_size, seq_len, 1] (1 for flood, 0 for normal)
Returns
-------
torch.Tensor
Computed loss value
"""
# Ensure flood_mask has correct shape
if flood_mask.dim() == 3 and flood_mask.shape[-1] == 1:
flood_mask = flood_mask.squeeze(-1) # Remove last dimension if it's 1
if self.flood_strategy == "weight":
return self._compute_weighted_loss(predictions, targets, flood_mask)
elif self.flood_strategy == "focal":
return self._compute_focal_loss(predictions, targets, flood_mask)
else:
raise ValueError(f"Unsupported flood strategy: {self.flood_strategy}")
def _compute_weighted_loss(
self, predictions: torch.Tensor, targets: torch.Tensor, flood_mask: torch.Tensor
) -> torch.Tensor:
"""Compute loss with higher weights for flood events."""
# Handle warmup_length: predictions may have more timesteps than targets
# If predictions shape is [batch, seq_pred, features] and targets is [batch, seq_target, features]
# where seq_pred > seq_target, we need to slice predictions to match targets
if predictions.shape[1] > targets.shape[1]:
# Take the last seq_target timesteps from predictions
seq_diff = predictions.shape[1] - targets.shape[1]
predictions = predictions[:, seq_diff:, :]
# Compute base loss
if isinstance(self.base_loss_func, RMSELoss):
# Special handling for RMSELoss which doesn't have reduction="none"
base_loss = torch.pow(predictions - targets, 2)
else:
mask = ~torch.isnan(targets)
predictions = predictions[mask]
targets = targets[mask]
flood_mask = flood_mask[
mask.squeeze(-1)
] # Ensure flood_mask matches predictions/targets
base_loss = self.base_loss_func(predictions, targets)
# return base_loss
# Apply flood weights
if self.use_score_weighting:
# Use data quality scores (1-100) as loss weights
# flood_mask: -1/0 = non-flood, 1-100 = flood quality score
weights = torch.full_like(
flood_mask, self.non_flood_weight, dtype=predictions.dtype
)
# For flood events (flood_mask >= 1), compute score-based weights
is_flood = flood_mask >= 1
# Increment batch counter
self.epoch_stats['total_batches'] += 1
if is_flood.any():
scores = flood_mask[is_flood]
num_flood_events = is_flood.sum().item()
# Accumulate statistics
self.epoch_stats['total_flood_events'] += num_flood_events
self.epoch_stats['score_sum'] += scores.sum().item()
self.epoch_stats['score_min'] = min(self.epoch_stats['score_min'], scores.min().item())
self.epoch_stats['score_max'] = max(self.epoch_stats['score_max'], scores.max().item())
# Store all scores for median calculation
self.epoch_stats['all_scores'].extend(scores.cpu().tolist())
# Count high-quality events (score >= 90)
high_quality_mask = scores >= 90
self.epoch_stats['high_quality_count'] += high_quality_mask.sum().item()
# Find perfect score events (score == 100)
perfect_mask = scores == 100
if perfect_mask.any():
num_perfect = perfect_mask.sum().item()
self.epoch_stats['perfect_score_count'] += num_perfect
# Get positions in the original flood_mask tensor
flood_indices = torch.nonzero(is_flood, as_tuple=False)
perfect_indices = flood_indices[perfect_mask]
# Store positions (batch_idx, timestep, current_batch_num)
for idx in perfect_indices:
if idx.dim() == 1 and len(idx) == 2:
batch_idx, timestep = idx[0].item(), idx[1].item()
self.epoch_stats['perfect_score_positions'].append(
(batch_idx, timestep, self.epoch_stats['total_batches'])
)
elif idx.dim() == 1 and len(idx) == 1:
timestep = idx[0].item()
self.epoch_stats['perfect_score_positions'].append(
(0, timestep, self.epoch_stats['total_batches'])
)
# Normalize scores to [0, 1]: (score / 100)
normalized_scores = scores / 100.0
normalized_scores = torch.clamp(normalized_scores, 0.0, 1.0)
# Apply power transformation with alpha to control high-quality emphasis
# weight(score) = base + (score/100)^alpha * (max - base)
score_weights = self.score_weight_min + torch.pow(normalized_scores, self.score_alpha) * (
self.score_weight_max - self.score_weight_min
)
# Accumulate weight statistics
self.epoch_stats['weight_sum'] += score_weights.sum().item()
self.epoch_stats['weight_min'] = min(self.epoch_stats['weight_min'], score_weights.min().item())
self.epoch_stats['weight_max'] = max(self.epoch_stats['weight_max'], score_weights.max().item())
weights[is_flood] = score_weights
else:
# Increment batch counter for binary weighting mode
self.epoch_stats['total_batches'] += 1
# Use original binary weighting (backward compatible)
# flood_mask >= 1: flood event, otherwise: non-flood
weights = torch.full_like(
flood_mask, self.non_flood_weight, dtype=predictions.dtype
)
weights[flood_mask >= 1] = self.flood_weight
# Apply weights to loss
if base_loss.dim() == 3: # [batch, seq, features]
weighted_loss = base_loss * weights.unsqueeze(-1)
else: # [batch, seq]
weighted_loss = base_loss * weights
valid_mask = ~torch.isnan(weighted_loss)
weighted_loss = weighted_loss[valid_mask]
if isinstance(self.base_loss_func, RMSELoss):
return torch.sqrt(weighted_loss.mean())
else:
return torch.mean(weighted_loss)
def _compute_focal_loss(
self, predictions: torch.Tensor, targets: torch.Tensor, flood_mask: torch.Tensor
) -> torch.Tensor:
"""Compute focal loss that emphasizes flood events."""
# Handle warmup_length: predictions may have more timesteps than targets
if predictions.shape[1] > targets.shape[1]:
# Take the last seq_target timesteps from predictions
seq_diff = predictions.shape[1] - targets.shape[1]
predictions = predictions[:, seq_diff:, :]
# Compute base loss
if isinstance(self.base_loss_func, RMSELoss):
base_loss = torch.pow(predictions - targets, 2)
else:
base_loss = self.base_loss_func(predictions, targets)
# Calculate flood ratio for focal weighting
flood_ratio = flood_mask.float().mean(dim=1, keepdim=True) # [batch_size, 1]
# Focal weight: higher weight when flood events are rare
focal_weight = (1 - flood_ratio) ** self.flood_focus_factor
# Separate flood and normal events
flood_loss = base_loss * flood_mask.unsqueeze(-1).float()
normal_loss = base_loss * (1 - flood_mask.unsqueeze(-1).float())
# Apply focal weight to flood events
weighted_flood_loss = flood_loss * focal_weight.unsqueeze(-1)
# Combine losses
total_loss = weighted_flood_loss + normal_loss
if isinstance(self.base_loss_func, RMSELoss):
return torch.sqrt(total_loss.mean())
else:
return total_loss.mean()
__init__(self, loss_func='MSELoss', flood_weight=1.0, non_flood_weight=0, flood_strategy='weight', flood_focus_factor=2.0, use_score_weighting=False, score_weight_min=0.5, score_weight_max=2.0, score_alpha=2.0, device=None, **kwargs)
special
¶
General flood-aware loss function with configurable base loss and strategy.
Parameters¶
loss_func : Union[torch.nn.Module, str] Base loss function to use. Can be a PyTorch loss function or string name. Supported strings: "MSELoss", "MAELoss", "RMSELoss", "L1Loss" flood_weight : float Weight multiplier for flood events when using binary weighting (use_score_weighting=False), default is 2.0 non_flood_weight : float Weight multiplier for non-flood events (flood_mask <= 0), default is 1.0 flood_strategy : str Strategy for handling flood events: - "weight": Apply higher weights to flood events - "focal": Use focal loss approach based on flood event frequency flood_focus_factor : float Factor for focal loss when using "focal" strategy, default is 2.0 use_score_weighting : bool If True, use data quality scores (from flood_mask values 1-100) as loss weights. flood_mask: -1/0 = non-flood (weight=non_flood_weight), 1-100 = flood quality score Default is False (use binary weighting: 0/1). score_weight_min : float Minimum weight for lowest quality score (score=1), default is 0.5 score_weight_max : float Maximum weight for highest quality score (score=100), default is 2.0 score_alpha : float Power factor controlling high-quality data weight proportion. weight(score) = base + (score/100)^alpha * (max - base) - alpha > 1: emphasize high-quality data (steeper curve) - alpha < 1: smoother weight distribution - alpha = 1: linear mapping Default is 2.0 (quadratic, emphasizing high scores). device : list Device configuration, default is None (auto-detect)
Source code in torchhydro/models/crits.py
def __init__(
self,
loss_func: Union[torch.nn.Module, str] = "MSELoss",
flood_weight: float = 1.0,
non_flood_weight: float = 0,
flood_strategy: str = "weight",
flood_focus_factor: float = 2.0,
use_score_weighting: bool = False,
score_weight_min: float = 0.5,
score_weight_max: float = 2.0,
score_alpha: float = 2.0,
device: list = None,
**kwargs,
):
"""
General flood-aware loss function with configurable base loss and strategy.
Parameters
----------
loss_func : Union[torch.nn.Module, str]
Base loss function to use. Can be a PyTorch loss function or string name.
Supported strings: "MSELoss", "MAELoss", "RMSELoss", "L1Loss"
flood_weight : float
Weight multiplier for flood events when using binary weighting (use_score_weighting=False), default is 2.0
non_flood_weight : float
Weight multiplier for non-flood events (flood_mask <= 0), default is 1.0
flood_strategy : str
Strategy for handling flood events:
- "weight": Apply higher weights to flood events
- "focal": Use focal loss approach based on flood event frequency
flood_focus_factor : float
Factor for focal loss when using "focal" strategy, default is 2.0
use_score_weighting : bool
If True, use data quality scores (from flood_mask values 1-100) as loss weights.
flood_mask: -1/0 = non-flood (weight=non_flood_weight), 1-100 = flood quality score
Default is False (use binary weighting: 0/1).
score_weight_min : float
Minimum weight for lowest quality score (score=1), default is 0.5
score_weight_max : float
Maximum weight for highest quality score (score=100), default is 2.0
score_alpha : float
Power factor controlling high-quality data weight proportion.
weight(score) = base + (score/100)^alpha * (max - base)
- alpha > 1: emphasize high-quality data (steeper curve)
- alpha < 1: smoother weight distribution
- alpha = 1: linear mapping
Default is 2.0 (quadratic, emphasizing high scores).
device : list
Device configuration, default is None (auto-detect)
"""
super(FloodLoss, self).__init__()
self.flood_weight = flood_weight
self.non_flood_weight = non_flood_weight
self.flood_strategy = flood_strategy
self.flood_focus_factor = flood_focus_factor
self.use_score_weighting = use_score_weighting
self.score_weight_min = score_weight_min
self.score_weight_max = score_weight_max
self.score_alpha = score_alpha
self.device = get_the_device(device if device is not None else [0])
# Initialize epoch-level statistics for score weighting
self.reset_statistics()
# Initialize base loss function
self.base_loss_func = self._initialize_base_loss(loss_func, kwargs)
compute_flood_loss(self, predictions, targets, flood_mask)
¶
Compute flood-aware loss using the specified strategy.
Parameters¶
predictions : torch.Tensor Model predictions [batch_size, seq_len, output_features] targets : torch.Tensor Target values [batch_size, seq_len, output_features] flood_mask : torch.Tensor Flood mask [batch_size, seq_len, 1] (1 for flood, 0 for normal)
Returns¶
torch.Tensor Computed loss value
Source code in torchhydro/models/crits.py
def compute_flood_loss(
self, predictions: torch.Tensor, targets: torch.Tensor, flood_mask: torch.Tensor
) -> torch.Tensor:
"""
Compute flood-aware loss using the specified strategy.
Parameters
----------
predictions : torch.Tensor
Model predictions [batch_size, seq_len, output_features]
targets : torch.Tensor
Target values [batch_size, seq_len, output_features]
flood_mask : torch.Tensor
Flood mask [batch_size, seq_len, 1] (1 for flood, 0 for normal)
Returns
-------
torch.Tensor
Computed loss value
"""
# Ensure flood_mask has correct shape
if flood_mask.dim() == 3 and flood_mask.shape[-1] == 1:
flood_mask = flood_mask.squeeze(-1) # Remove last dimension if it's 1
if self.flood_strategy == "weight":
return self._compute_weighted_loss(predictions, targets, flood_mask)
elif self.flood_strategy == "focal":
return self._compute_focal_loss(predictions, targets, flood_mask)
else:
raise ValueError(f"Unsupported flood strategy: {self.flood_strategy}")
reset_statistics(self)
¶
Reset epoch-level statistics for score weighting.
Source code in torchhydro/models/crits.py
def reset_statistics(self):
"""Reset epoch-level statistics for score weighting."""
self.epoch_stats = {
'total_batches': 0,
'total_flood_events': 0,
'score_sum': 0.0,
'score_min': float('inf'),
'score_max': float('-inf'),
'all_scores': [],
'high_quality_count': 0, # score >= 90
'perfect_score_count': 0, # score == 100
'perfect_score_positions': [], # List of (batch_idx, timestep, batch_num)
'weight_sum': 0.0,
'weight_min': float('inf'),
'weight_max': float('-inf'),
}
GaussianLoss (Module)
¶
Source code in torchhydro/models/crits.py
class GaussianLoss(torch.nn.Module):
def __init__(self, mu=0, sigma=0):
"""Compute the negative log likelihood of Gaussian Distribution
From https://arxiv.org/abs/1907.00235
"""
super(GaussianLoss, self).__init__()
self.mu = mu
self.sigma = sigma
def forward(self, x: torch.Tensor):
loss = -tdist.Normal(self.mu, self.sigma).log_prob(x)
return torch.sum(loss) / (loss.size(0) * loss.size(1))
__init__(self, mu=0, sigma=0)
special
¶
Compute the negative log likelihood of Gaussian Distribution From https://arxiv.org/abs/1907.00235
Source code in torchhydro/models/crits.py
def __init__(self, mu=0, sigma=0):
"""Compute the negative log likelihood of Gaussian Distribution
From https://arxiv.org/abs/1907.00235
"""
super(GaussianLoss, self).__init__()
self.mu = mu
self.sigma = sigma
forward(self, x)
¶
Define the computation performed at every call.
Should be overridden by all subclasses.
.. note::
Although the recipe for forward pass needs to be defined within
this function, one should call the :class:Module instance afterwards
instead of this since the former takes care of running the
registered hooks while the latter silently ignores them.
Source code in torchhydro/models/crits.py
def forward(self, x: torch.Tensor):
loss = -tdist.Normal(self.mu, self.sigma).log_prob(x)
return torch.sum(loss) / (loss.size(0) * loss.size(1))
GenerativeLoss (Module)
¶
A loss function for generative models (e.g., Diffusion models).
During training: - Model's forward() returns {'loss': loss} - This loss is extracted and returned directly
During validation/inference: - Model's sample() returns predicted tensor - Uses the configured eval_loss_func to compute loss
Parameters¶
eval_loss_func : torch.nn.Module, optional Loss function to use during validation/inference. Default is None, which uses MSE loss. Can be any loss function from pytorch_criterion_dict (e.g., RmseLoss, NSELoss)
Examples¶
In training config:
loss_func="GenerativeLoss", criterion_params={ "eval_loss_func": "RMSESum", # Use RMSE for validation }
Source code in torchhydro/models/crits.py
class GenerativeLoss(torch.nn.Module):
"""
A loss function for generative models (e.g., Diffusion models).
During training:
- Model's forward() returns {'loss': loss}
- This loss is extracted and returned directly
During validation/inference:
- Model's sample() returns predicted tensor
- Uses the configured eval_loss_func to compute loss
Parameters
----------
eval_loss_func : torch.nn.Module, optional
Loss function to use during validation/inference.
Default is None, which uses MSE loss.
Can be any loss function from pytorch_criterion_dict (e.g., RmseLoss, NSELoss)
Examples
--------
In training config:
>>> loss_func="GenerativeLoss",
>>> criterion_params={
>>> "eval_loss_func": "RMSESum", # Use RMSE for validation
>>> }
"""
def __init__(self, eval_loss_func: torch.nn.Module = None) -> None:
super().__init__()
self.eval_loss_func = eval_loss_func
def forward(
self, prediction: torch.Tensor | dict, target: torch.Tensor
) -> torch.Tensor:
"""
Compute loss for generative models.
Parameters
----------
prediction : dict or torch.Tensor
During training: dict with 'loss' key
During inference: predicted tensor from sample()
target : torch.Tensor
Ground truth target values
Returns
-------
torch.Tensor
The computed loss
"""
# Training mode: model returns {'loss': loss}
if isinstance(prediction, dict) and "loss" in prediction:
return prediction["loss"]
# Scalar tensor (already computed loss)
if isinstance(prediction, torch.Tensor) and prediction.dim() == 0:
return prediction
# Inference/validation mode: compute loss between prediction and target
if isinstance(prediction, torch.Tensor) and isinstance(target, torch.Tensor):
# Use configured eval_loss_func or default to MSE
if self.eval_loss_func is not None:
return self.eval_loss_func(prediction, target)
else:
# Default: MSE with NaN handling
mask = ~torch.isnan(target)
if mask.sum() == 0:
return torch.tensor(
0.0, device=prediction.device, requires_grad=True
)
return torch.nn.functional.mse_loss(prediction[mask], target[mask])
raise ValueError(
f"GenerativeLoss got unexpected types: prediction={type(prediction)}, "
f"target={type(target)}"
)
forward(self, prediction, target)
¶
Compute loss for generative models.
Parameters¶
prediction : dict or torch.Tensor During training: dict with 'loss' key During inference: predicted tensor from sample() target : torch.Tensor Ground truth target values
Returns¶
torch.Tensor The computed loss
Source code in torchhydro/models/crits.py
def forward(
self, prediction: torch.Tensor | dict, target: torch.Tensor
) -> torch.Tensor:
"""
Compute loss for generative models.
Parameters
----------
prediction : dict or torch.Tensor
During training: dict with 'loss' key
During inference: predicted tensor from sample()
target : torch.Tensor
Ground truth target values
Returns
-------
torch.Tensor
The computed loss
"""
# Training mode: model returns {'loss': loss}
if isinstance(prediction, dict) and "loss" in prediction:
return prediction["loss"]
# Scalar tensor (already computed loss)
if isinstance(prediction, torch.Tensor) and prediction.dim() == 0:
return prediction
# Inference/validation mode: compute loss between prediction and target
if isinstance(prediction, torch.Tensor) and isinstance(target, torch.Tensor):
# Use configured eval_loss_func or default to MSE
if self.eval_loss_func is not None:
return self.eval_loss_func(prediction, target)
else:
# Default: MSE with NaN handling
mask = ~torch.isnan(target)
if mask.sum() == 0:
return torch.tensor(
0.0, device=prediction.device, requires_grad=True
)
return torch.nn.functional.mse_loss(prediction[mask], target[mask])
raise ValueError(
f"GenerativeLoss got unexpected types: prediction={type(prediction)}, "
f"target={type(target)}"
)
HybridFloodloss (FloodBaseLoss)
¶
Source code in torchhydro/models/crits.py
class HybridFloodloss(FloodBaseLoss):
def __init__(self, mae_weight=0.5):
"""
Hybrid Flood Loss: PES loss + mae_weight × MAE with flood weighting
Combines PES loss (MSE × sigmoid(MSE)) with Mean Absolute Error,
applying flood weighting to the loss.
The difference from FloodLoss is that this class filter flood events first then calculate loss,
because Hybrid does sigmoid on MSE, when the non-flood-weight is 0, which means we do not want to
calculate loss on non-flood events, so we need to filter them out first.
Parameters
----------
mae_weight : float
Weight for the MAE component, default is 0.5
flood_weight : float
Weight multiplier for flood events, default is 2.0
"""
super(HybridFloodloss, self).__init__()
self.mae_weight = mae_weight
def compute_flood_loss(
self, predictions: torch.Tensor, targets: torch.Tensor, flood_mask: torch.Tensor
) -> torch.Tensor:
"""
Compute flood-aware loss using the specified strategy.
Parameters
----------
predictions : torch.Tensor
Model predictions [batch_size, seq_len, output_features]
targets : torch.Tensor
Target values [batch_size, seq_len, output_features]
flood_mask : torch.Tensor
Flood mask [batch_size, seq_len, 1] (1 for flood, 0 for normal)
Returns
-------
torch.Tensor
Computed loss value
"""
boolean_mask = flood_mask.to(torch.bool)
predictions = predictions[boolean_mask]
targets = targets[boolean_mask]
base_loss_func = HybridLoss(self.mae_weight)
return base_loss_func(predictions, targets)
__init__(self, mae_weight=0.5)
special
¶
Hybrid Flood Loss: PES loss + mae_weight × MAE with flood weighting
Combines PES loss (MSE × sigmoid(MSE)) with Mean Absolute Error, applying flood weighting to the loss.
The difference from FloodLoss is that this class filter flood events first then calculate loss, because Hybrid does sigmoid on MSE, when the non-flood-weight is 0, which means we do not want to calculate loss on non-flood events, so we need to filter them out first.
Parameters¶
mae_weight : float Weight for the MAE component, default is 0.5 flood_weight : float Weight multiplier for flood events, default is 2.0
Source code in torchhydro/models/crits.py
def __init__(self, mae_weight=0.5):
"""
Hybrid Flood Loss: PES loss + mae_weight × MAE with flood weighting
Combines PES loss (MSE × sigmoid(MSE)) with Mean Absolute Error,
applying flood weighting to the loss.
The difference from FloodLoss is that this class filter flood events first then calculate loss,
because Hybrid does sigmoid on MSE, when the non-flood-weight is 0, which means we do not want to
calculate loss on non-flood events, so we need to filter them out first.
Parameters
----------
mae_weight : float
Weight for the MAE component, default is 0.5
flood_weight : float
Weight multiplier for flood events, default is 2.0
"""
super(HybridFloodloss, self).__init__()
self.mae_weight = mae_weight
compute_flood_loss(self, predictions, targets, flood_mask)
¶
Compute flood-aware loss using the specified strategy.
Parameters¶
predictions : torch.Tensor Model predictions [batch_size, seq_len, output_features] targets : torch.Tensor Target values [batch_size, seq_len, output_features] flood_mask : torch.Tensor Flood mask [batch_size, seq_len, 1] (1 for flood, 0 for normal)
Returns¶
torch.Tensor Computed loss value
Source code in torchhydro/models/crits.py
def compute_flood_loss(
self, predictions: torch.Tensor, targets: torch.Tensor, flood_mask: torch.Tensor
) -> torch.Tensor:
"""
Compute flood-aware loss using the specified strategy.
Parameters
----------
predictions : torch.Tensor
Model predictions [batch_size, seq_len, output_features]
targets : torch.Tensor
Target values [batch_size, seq_len, output_features]
flood_mask : torch.Tensor
Flood mask [batch_size, seq_len, 1] (1 for flood, 0 for normal)
Returns
-------
torch.Tensor
Computed loss value
"""
boolean_mask = flood_mask.to(torch.bool)
predictions = predictions[boolean_mask]
targets = targets[boolean_mask]
base_loss_func = HybridLoss(self.mae_weight)
return base_loss_func(predictions, targets)
HybridLoss (Module)
¶
Source code in torchhydro/models/crits.py
class HybridLoss(torch.nn.Module):
def __init__(self, mae_weight: float = 0.5, reduction: str = "mean"):
"""
Hybrid Loss: PES loss + mae_weight × MAE
Combines PES loss (MSE × sigmoid(MSE)) with Mean Absolute Error.
Parameters
----------
mae_weight : float
Weight for the MAE component, default is 0.5
reduction : str
Reduction method for the loss, default is "mean". Can be "mean" or "none".
If "none", returns the loss without reduction.
"""
super(HybridLoss, self).__init__()
self.pes_loss = PESLoss()
self.mae = MAELoss(reduction=reduction)
self.mae_weight = mae_weight
self.reduction = reduction
def forward(self, output: torch.Tensor, target: torch.Tensor):
pes = self.pes_loss(output, target)
mae = self.mae(output, target)
if self.reduction == "none":
return pes + self.mae_weight * mae
elif self.reduction == "mean":
loss = pes + self.mae_weight * mae
valid_mask = ~torch.isnan(loss)
return torch.mean(loss[valid_mask])
else:
raise ValueError(
f"Unsupported reduction method: {self.reduction}. Use 'mean' or 'none'."
)
__init__(self, mae_weight=0.5, reduction='mean')
special
¶
Hybrid Loss: PES loss + mae_weight × MAE
Combines PES loss (MSE × sigmoid(MSE)) with Mean Absolute Error.
Parameters¶
mae_weight : float Weight for the MAE component, default is 0.5 reduction : str Reduction method for the loss, default is "mean". Can be "mean" or "none". If "none", returns the loss without reduction.
Source code in torchhydro/models/crits.py
def __init__(self, mae_weight: float = 0.5, reduction: str = "mean"):
"""
Hybrid Loss: PES loss + mae_weight × MAE
Combines PES loss (MSE × sigmoid(MSE)) with Mean Absolute Error.
Parameters
----------
mae_weight : float
Weight for the MAE component, default is 0.5
reduction : str
Reduction method for the loss, default is "mean". Can be "mean" or "none".
If "none", returns the loss without reduction.
"""
super(HybridLoss, self).__init__()
self.pes_loss = PESLoss()
self.mae = MAELoss(reduction=reduction)
self.mae_weight = mae_weight
self.reduction = reduction
forward(self, output, target)
¶
Define the computation performed at every call.
Should be overridden by all subclasses.
.. note::
Although the recipe for forward pass needs to be defined within
this function, one should call the :class:Module instance afterwards
instead of this since the former takes care of running the
registered hooks while the latter silently ignores them.
Source code in torchhydro/models/crits.py
def forward(self, output: torch.Tensor, target: torch.Tensor):
pes = self.pes_loss(output, target)
mae = self.mae(output, target)
if self.reduction == "none":
return pes + self.mae_weight * mae
elif self.reduction == "mean":
loss = pes + self.mae_weight * mae
valid_mask = ~torch.isnan(loss)
return torch.mean(loss[valid_mask])
else:
raise ValueError(
f"Unsupported reduction method: {self.reduction}. Use 'mean' or 'none'."
)
MAELoss (Module)
¶
Source code in torchhydro/models/crits.py
class MAELoss(torch.nn.Module):
def __init__(self, reduction: str = "mean"):
super().__init__()
self.reduction = reduction
def forward(self, output: torch.Tensor, target: torch.Tensor):
# Create a mask to filter out NaN values
mask = ~torch.isnan(target)
# Apply the mask to both target and output
target = target[mask]
output = output[mask]
# Calculate MAE for the non-NaN values
if self.reduction == "mean": # Return mean MAe
return torch.mean(torch.abs(target - output))
elif self.reduction == "none":
return torch.abs(target - output)
else:
raise ValueError(
"Reduction must be 'mean' or 'none', got {}".format(self.reduction)
)
forward(self, output, target)
¶
Define the computation performed at every call.
Should be overridden by all subclasses.
.. note::
Although the recipe for forward pass needs to be defined within
this function, one should call the :class:Module instance afterwards
instead of this since the former takes care of running the
registered hooks while the latter silently ignores them.
Source code in torchhydro/models/crits.py
def forward(self, output: torch.Tensor, target: torch.Tensor):
# Create a mask to filter out NaN values
mask = ~torch.isnan(target)
# Apply the mask to both target and output
target = target[mask]
output = output[mask]
# Calculate MAE for the non-NaN values
if self.reduction == "mean": # Return mean MAe
return torch.mean(torch.abs(target - output))
elif self.reduction == "none":
return torch.abs(target - output)
else:
raise ValueError(
"Reduction must be 'mean' or 'none', got {}".format(self.reduction)
)
MAPELoss (Module)
¶
Returns MAPE using: target -> True y output -> Predtion by model
Source code in torchhydro/models/crits.py
class MAPELoss(torch.nn.Module):
"""
Returns MAPE using:
target -> True y
output -> Predtion by model
"""
def __init__(self, variance_penalty=0.0):
super().__init__()
self.variance_penalty = variance_penalty
def forward(self, output: torch.Tensor, target: torch.Tensor):
if len(output) > 1:
return torch.mean(
torch.abs(torch.sub(target, output) / target)
) + self.variance_penalty * torch.std(torch.sub(target, output))
else:
return torch.mean(torch.abs(torch.sub(target, output) / target))
forward(self, output, target)
¶
Define the computation performed at every call.
Should be overridden by all subclasses.
.. note::
Although the recipe for forward pass needs to be defined within
this function, one should call the :class:Module instance afterwards
instead of this since the former takes care of running the
registered hooks while the latter silently ignores them.
Source code in torchhydro/models/crits.py
def forward(self, output: torch.Tensor, target: torch.Tensor):
if len(output) > 1:
return torch.mean(
torch.abs(torch.sub(target, output) / target)
) + self.variance_penalty * torch.std(torch.sub(target, output))
else:
return torch.mean(torch.abs(torch.sub(target, output) / target))
MASELoss (Module)
¶
Source code in torchhydro/models/crits.py
class MASELoss(torch.nn.Module):
def __init__(self, baseline_method):
"""
This implements the MASE loss function (e.g. MAE_MODEL/MAE_NAIEVE)
"""
super(MASELoss, self).__init__()
self.method_dict = {
"mean": lambda x, y: torch.mean(x, 1).unsqueeze(1).repeat(1, y[1], 1)
}
self.baseline_method = self.method_dict[baseline_method]
def forward(
self, target: torch.Tensor, output: torch.Tensor, train_data: torch.Tensor, m=1
) -> torch.Tensor:
# Ugh why can't all tensors have batch size... Fixes for modern
if len(train_data.shape) < 3:
train_data = train_data.unsqueeze(0)
if m == 1 and len(target.shape) == 1:
output = output.unsqueeze(0)
output = output.unsqueeze(2)
target = target.unsqueeze(0)
target = target.unsqueeze(2)
if len(target.shape) == 2:
output = output.unsqueeze(0)
target = target.unsqueeze(0)
result_baseline = self.baseline_method(train_data, output.shape)
MAE = torch.nn.L1Loss()
mae2 = MAE(output, target)
mase4 = MAE(result_baseline, target)
# Prevent divison by zero/loss exploding
if mase4 < 0.001:
mase4 = 0.001
return mae2 / mase4
__init__(self, baseline_method)
special
¶
This implements the MASE loss function (e.g. MAE_MODEL/MAE_NAIEVE)
Source code in torchhydro/models/crits.py
def __init__(self, baseline_method):
"""
This implements the MASE loss function (e.g. MAE_MODEL/MAE_NAIEVE)
"""
super(MASELoss, self).__init__()
self.method_dict = {
"mean": lambda x, y: torch.mean(x, 1).unsqueeze(1).repeat(1, y[1], 1)
}
self.baseline_method = self.method_dict[baseline_method]
forward(self, target, output, train_data, m=1)
¶
Define the computation performed at every call.
Should be overridden by all subclasses.
.. note::
Although the recipe for forward pass needs to be defined within
this function, one should call the :class:Module instance afterwards
instead of this since the former takes care of running the
registered hooks while the latter silently ignores them.
Source code in torchhydro/models/crits.py
def forward(
self, target: torch.Tensor, output: torch.Tensor, train_data: torch.Tensor, m=1
) -> torch.Tensor:
# Ugh why can't all tensors have batch size... Fixes for modern
if len(train_data.shape) < 3:
train_data = train_data.unsqueeze(0)
if m == 1 and len(target.shape) == 1:
output = output.unsqueeze(0)
output = output.unsqueeze(2)
target = target.unsqueeze(0)
target = target.unsqueeze(2)
if len(target.shape) == 2:
output = output.unsqueeze(0)
target = target.unsqueeze(0)
result_baseline = self.baseline_method(train_data, output.shape)
MAE = torch.nn.L1Loss()
mae2 = MAE(output, target)
mase4 = MAE(result_baseline, target)
# Prevent divison by zero/loss exploding
if mase4 < 0.001:
mase4 = 0.001
return mae2 / mase4
MseMaskedLoss (Module)
¶
Source code in torchhydro/models/crits.py
class MseMaskedLoss(torch.nn.Module):
def __init__(self):
super().__init__()
def forward(
self, predictions: torch.Tensor, targets: torch.Tensor, mask: torch.Tensor
):
"""
Compute NSE loss for masked data.
Parameters
----------
predictions : torch.Tensor
Model predictions [batch_size, seq_len, output_features]
targets : torch.Tensor
Target values [batch_size, seq_len, output_features]
mask : torch.Tensor
Mask [batch_size, seq_len, 1] (1 for valid, 0 for invalid)
Returns
-------
torch.Tensor
Computed NSE loss value
"""
if mask is None:
mask = ~torch.isnan(targets)
valid_predictions = predictions[mask]
valid_targets = targets[mask]
if valid_predictions.numel() == 0:
return torch.tensor(0.0, device=predictions.device, requires_grad=True)
loss = torch.mean((valid_predictions - valid_targets) ** 2)
return loss
forward(self, predictions, targets, mask)
¶
Compute NSE loss for masked data.
Parameters¶
predictions : torch.Tensor Model predictions [batch_size, seq_len, output_features] targets : torch.Tensor Target values [batch_size, seq_len, output_features] mask : torch.Tensor Mask [batch_size, seq_len, 1] (1 for valid, 0 for invalid)
Returns¶
torch.Tensor Computed NSE loss value
Source code in torchhydro/models/crits.py
def forward(
self, predictions: torch.Tensor, targets: torch.Tensor, mask: torch.Tensor
):
"""
Compute NSE loss for masked data.
Parameters
----------
predictions : torch.Tensor
Model predictions [batch_size, seq_len, output_features]
targets : torch.Tensor
Target values [batch_size, seq_len, output_features]
mask : torch.Tensor
Mask [batch_size, seq_len, 1] (1 for valid, 0 for invalid)
Returns
-------
torch.Tensor
Computed NSE loss value
"""
if mask is None:
mask = ~torch.isnan(targets)
valid_predictions = predictions[mask]
valid_targets = targets[mask]
if valid_predictions.numel() == 0:
return torch.tensor(0.0, device=predictions.device, requires_grad=True)
loss = torch.mean((valid_predictions - valid_targets) ** 2)
return loss
MultiOutLoss (Module)
¶
Source code in torchhydro/models/crits.py
class MultiOutLoss(torch.nn.Module):
def __init__(
self,
loss_funcs: Union[torch.nn.Module, list],
data_gap: list = None,
device: list = None,
limit_part: list = None,
item_weight: list = None,
):
"""
Loss function for multiple output
Parameters
----------
loss_funcs
The loss functions for each output
data_gap
It belongs to the feature dim.
If 1, then the corresponding value is uniformly-spaced with NaN values filling the gap;
in addition, the first non-nan value means the aggregated value of the following interval,
for example, in [5, nan, nan, nan], 5 means all four data's sum, although the next 3 values are nan
hence the calculation is a little different;
if 2, the first non-nan value means the average value of the following interval,
for example, in [5, nan, nan, nan], 5 means all four data's mean value;
default is [0, 2]
device
the number of device: -1 -> "cpu" or "cuda:x" (x is 0, 1 or ...)
limit_part
when transfer learning, we may ignore some part;
the default is None, which means no ignorance;
other choices are list, such as [0], [0, 1] or [1,2,..];
0 means the first variable;
tensor is [seq, time, var] or [time, seq, var]
item_weight
use different weight for each item's loss;
for example, the default values [0.5, 0.5] means 0.5 * loss1 + 0.5 * loss2
"""
if data_gap is None:
data_gap = [0, 2]
if device is None:
device = [0]
if item_weight is None:
item_weight = [0.5, 0.5]
super(MultiOutLoss, self).__init__()
self.loss_funcs = loss_funcs
self.data_gap = data_gap
self.device = get_the_device(device)
self.limit_part = limit_part
self.item_weight = item_weight
def forward(self, output: Tensor, target: Tensor):
"""
Calculate the sum of losses for different variables
When there are NaN values in observation, we will perform a "reduce" operation on prediction.
For example, pred = [0,1,2,3,4], obs=[5, nan, nan, 6, nan]; the "reduce" is sum;
then, pred_sum = [0+1+2, 3+4], obs_sum=[5,6], loss = loss_func(pred_sum, obs_sum).
Notice: when "sum", actually final index is not chosen,
because the whole observation may be [5, nan, nan, 6, nan, nan, 7, nan, nan], 6 means sum of three elements.
Just as the rho is 5, the final one is not chosen
Parameters
----------
output
the prediction tensor; 3-dims are time sequence, batch and feature, respectively
target
the observation tensor
Returns
-------
Tensor
Whole loss
"""
n_out = target.shape[-1]
loss = 0
for k in range(n_out):
if self.limit_part is not None and k in self.limit_part:
continue
p0 = output[:, :, k]
t0 = target[:, :, k]
mask = t0 == t0
p = p0[mask]
t = t0[mask]
if self.data_gap[k] > 0:
p, t = deal_gap_data(p0, t0, self.data_gap[k], self.device)
if type(self.loss_funcs) is list:
temp = self.item_weight[k] * self.loss_funcs[k](p, t)
else:
temp = self.item_weight[k] * self.loss_funcs(p, t)
# sum of all k-th loss
if torch.isnan(temp).any():
continue
loss = loss + temp
return loss
__init__(self, loss_funcs, data_gap=None, device=None, limit_part=None, item_weight=None)
special
¶
Loss function for multiple output
Parameters¶
loss_funcs The loss functions for each output data_gap It belongs to the feature dim. If 1, then the corresponding value is uniformly-spaced with NaN values filling the gap; in addition, the first non-nan value means the aggregated value of the following interval, for example, in [5, nan, nan, nan], 5 means all four data's sum, although the next 3 values are nan hence the calculation is a little different; if 2, the first non-nan value means the average value of the following interval, for example, in [5, nan, nan, nan], 5 means all four data's mean value; default is [0, 2] device the number of device: -1 -> "cpu" or "cuda:x" (x is 0, 1 or ...) limit_part when transfer learning, we may ignore some part; the default is None, which means no ignorance; other choices are list, such as [0], [0, 1] or [1,2,..]; 0 means the first variable; tensor is [seq, time, var] or [time, seq, var] item_weight use different weight for each item's loss; for example, the default values [0.5, 0.5] means 0.5 * loss1 + 0.5 * loss2
Source code in torchhydro/models/crits.py
def __init__(
self,
loss_funcs: Union[torch.nn.Module, list],
data_gap: list = None,
device: list = None,
limit_part: list = None,
item_weight: list = None,
):
"""
Loss function for multiple output
Parameters
----------
loss_funcs
The loss functions for each output
data_gap
It belongs to the feature dim.
If 1, then the corresponding value is uniformly-spaced with NaN values filling the gap;
in addition, the first non-nan value means the aggregated value of the following interval,
for example, in [5, nan, nan, nan], 5 means all four data's sum, although the next 3 values are nan
hence the calculation is a little different;
if 2, the first non-nan value means the average value of the following interval,
for example, in [5, nan, nan, nan], 5 means all four data's mean value;
default is [0, 2]
device
the number of device: -1 -> "cpu" or "cuda:x" (x is 0, 1 or ...)
limit_part
when transfer learning, we may ignore some part;
the default is None, which means no ignorance;
other choices are list, such as [0], [0, 1] or [1,2,..];
0 means the first variable;
tensor is [seq, time, var] or [time, seq, var]
item_weight
use different weight for each item's loss;
for example, the default values [0.5, 0.5] means 0.5 * loss1 + 0.5 * loss2
"""
if data_gap is None:
data_gap = [0, 2]
if device is None:
device = [0]
if item_weight is None:
item_weight = [0.5, 0.5]
super(MultiOutLoss, self).__init__()
self.loss_funcs = loss_funcs
self.data_gap = data_gap
self.device = get_the_device(device)
self.limit_part = limit_part
self.item_weight = item_weight
forward(self, output, target)
¶
Calculate the sum of losses for different variables
When there are NaN values in observation, we will perform a "reduce" operation on prediction. For example, pred = [0,1,2,3,4], obs=[5, nan, nan, 6, nan]; the "reduce" is sum; then, pred_sum = [0+1+2, 3+4], obs_sum=[5,6], loss = loss_func(pred_sum, obs_sum). Notice: when "sum", actually final index is not chosen, because the whole observation may be [5, nan, nan, 6, nan, nan, 7, nan, nan], 6 means sum of three elements. Just as the rho is 5, the final one is not chosen
Parameters¶
output the prediction tensor; 3-dims are time sequence, batch and feature, respectively target the observation tensor
Returns¶
Tensor Whole loss
Source code in torchhydro/models/crits.py
def forward(self, output: Tensor, target: Tensor):
"""
Calculate the sum of losses for different variables
When there are NaN values in observation, we will perform a "reduce" operation on prediction.
For example, pred = [0,1,2,3,4], obs=[5, nan, nan, 6, nan]; the "reduce" is sum;
then, pred_sum = [0+1+2, 3+4], obs_sum=[5,6], loss = loss_func(pred_sum, obs_sum).
Notice: when "sum", actually final index is not chosen,
because the whole observation may be [5, nan, nan, 6, nan, nan, 7, nan, nan], 6 means sum of three elements.
Just as the rho is 5, the final one is not chosen
Parameters
----------
output
the prediction tensor; 3-dims are time sequence, batch and feature, respectively
target
the observation tensor
Returns
-------
Tensor
Whole loss
"""
n_out = target.shape[-1]
loss = 0
for k in range(n_out):
if self.limit_part is not None and k in self.limit_part:
continue
p0 = output[:, :, k]
t0 = target[:, :, k]
mask = t0 == t0
p = p0[mask]
t = t0[mask]
if self.data_gap[k] > 0:
p, t = deal_gap_data(p0, t0, self.data_gap[k], self.device)
if type(self.loss_funcs) is list:
temp = self.item_weight[k] * self.loss_funcs[k](p, t)
else:
temp = self.item_weight[k] * self.loss_funcs(p, t)
# sum of all k-th loss
if torch.isnan(temp).any():
continue
loss = loss + temp
return loss
MultiOutWaterBalanceLoss (Module)
¶
Source code in torchhydro/models/crits.py
class MultiOutWaterBalanceLoss(torch.nn.Module):
def __init__(
self,
loss_funcs: Union[torch.nn.Module, list],
data_gap: list = None,
device: list = None,
limit_part: list = None,
item_weight: list = None,
alpha=0.5,
beta=0.0,
wb_loss_func=None,
means=None,
stds=None,
):
"""
Loss function for multiple output considering water balance
loss = alpha * water_balance_loss + (1-alpha) * mtl_loss
This loss function is only for p, q, et now
we use the difference between p_obs_mean-q_obs_mean-et_obs_mean and p_pred_mean-q_pred_mean-et_pred_mean as water balance loss
which is the difference between (q_obs_mean + et_obs_mean) and (q_pred_mean + et_pred_mean)
Parameters
----------
loss_funcs
The loss functions for each output
data_gap
It belongs to the feature dim.
If 1, then the corresponding value is uniformly-spaced with NaN values filling the gap;
in addition, the first non-nan value means the aggregated value of the following interval,
for example, in [5, nan, nan, nan], 5 means all four data's sum, although the next 3 values are nan
hence the calculation is a little different;
if 2, the first non-nan value means the average value of the following interval,
for example, in [5, nan, nan, nan], 5 means all four data's mean value;
default is [0, 2]
device
the number of device: -1 -> "cpu" or "cuda:x" (x is 0, 1 or ...)
limit_part
when transfer learning, we may ignore some part;
the default is None, which means no ignorance;
other choices are list, such as [0], [0, 1] or [1,2,..];
0 means the first variable;
tensor is [seq, time, var] or [time, seq, var]
item_weight
use different weight for each item's loss;
for example, the default values [0.5, 0.5] means 0.5 * loss1 + 0.5 * loss2
alpha
the weight of the water-balance item's loss
beta
the weight of real water-balance item's loss, et_mean/p_mean + q_mean/p_mean = 1 can be a loss.
It is not strictly correct as training batch only have about one year data, but still could be a constraint
wb_loss_func
the loss function for water balance item, by default it is None, which means we use function in loss_funcs
"""
if data_gap is None:
data_gap = [0, 2]
if device is None:
device = [0]
if item_weight is None:
item_weight = [0.5, 0.5]
super(MultiOutWaterBalanceLoss, self).__init__()
self.loss_funcs = loss_funcs
self.data_gap = data_gap
self.device = get_the_device(device)
self.limit_part = limit_part
self.item_weight = item_weight
self.alpha = alpha
self.beta = beta
self.wb_loss_func = wb_loss_func
self.means = means
self.stds = stds
def forward(self, output: Tensor, target: Tensor):
"""
Calculate the sum of losses for different variables and water-balance loss
When there are NaN values in observation, we will perform a "reduce" operation on prediction.
For example, pred = [0,1,2,3,4], obs=[5, nan, nan, 6, nan]; the "reduce" is sum;
then, pred_sum = [0+1+2, 3+4], obs_sum=[5,6], loss = loss_func(pred_sum, obs_sum).
Notice: when "sum", actually final index is not chosen,
because the whole observation may be [5, nan, nan, 6, nan, nan, 7, nan, nan], 6 means sum of three elements.
Just as the rho is 5, the final one is not chosen
Parameters
----------
output
the prediction tensor; 3-dims are time sequence, batch and feature, respectively
target
the observation tensor
Returns
-------
Tensor
Whole loss
"""
n_out = target.shape[-1]
loss = 0
p_means = []
t_means = []
all_means = self.means
all_stds = self.stds
for k in range(n_out):
if self.limit_part is not None and k in self.limit_part:
continue
p0 = output[:, :, k]
t0 = target[:, :, k]
# for water balance loss
if all_means is not None:
# denormalize for q and et
p1 = p0 * all_stds[k] + all_means[k]
t1 = t0 * all_stds[k] + all_means[k]
p2 = (10**p1 - 0.1) ** 2
t2 = (10**t1 - 0.1) ** 2
p_mean = torch.nanmean(p2, dim=0)
t_mean = torch.nanmean(t2, dim=0)
else:
p_mean = torch.nanmean(p0, dim=0)
t_mean = torch.nanmean(t0, dim=0)
p_means.append(p_mean)
t_means.append(t_mean)
# for mtl normal loss
mask = t0 == t0
p = p0[mask]
t = t0[mask]
if self.data_gap[k] > 0:
p, t = deal_gap_data(p0, t0, self.data_gap[k], self.device)
if type(self.loss_funcs) is list:
temp = self.item_weight[k] * self.loss_funcs[k](p, t)
else:
temp = self.item_weight[k] * self.loss_funcs(p, t)
# sum of all k-th loss
loss = loss + temp
# water balance loss
p_mean_q_plus_et = torch.sum(torch.stack(p_means), dim=0)
t_mean_q_plus_et = torch.sum(torch.stack(t_means), dim=0)
wb_ones = torch.ones_like(t_mean_q_plus_et)
if self.wb_loss_func is None:
if type(self.loss_funcs) is list:
# if wb_loss_func is None, we use the first loss function in loss_funcs
wb_loss = self.loss_funcs[0](p_mean_q_plus_et, t_mean_q_plus_et)
wb_1loss = self.loss_funcs[0](p_mean_q_plus_et, wb_ones)
else:
wb_loss = self.loss_funcs(p_mean_q_plus_et, t_mean_q_plus_et)
wb_1loss = self.loss_funcs(p_mean_q_plus_et, wb_ones)
else:
wb_loss = self.wb_loss_func(p_mean_q_plus_et, t_mean_q_plus_et)
wb_1loss = self.wb_loss_func(p_mean_q_plus_et, wb_ones)
return (
self.alpha * wb_loss
+ (1 - self.alpha - self.beta) * loss
+ self.beta * wb_1loss
)
__init__(self, loss_funcs, data_gap=None, device=None, limit_part=None, item_weight=None, alpha=0.5, beta=0.0, wb_loss_func=None, means=None, stds=None)
special
¶
Loss function for multiple output considering water balance
loss = alpha * water_balance_loss + (1-alpha) * mtl_loss
This loss function is only for p, q, et now we use the difference between p_obs_mean-q_obs_mean-et_obs_mean and p_pred_mean-q_pred_mean-et_pred_mean as water balance loss which is the difference between (q_obs_mean + et_obs_mean) and (q_pred_mean + et_pred_mean)
Parameters¶
loss_funcs The loss functions for each output data_gap It belongs to the feature dim. If 1, then the corresponding value is uniformly-spaced with NaN values filling the gap; in addition, the first non-nan value means the aggregated value of the following interval, for example, in [5, nan, nan, nan], 5 means all four data's sum, although the next 3 values are nan hence the calculation is a little different; if 2, the first non-nan value means the average value of the following interval, for example, in [5, nan, nan, nan], 5 means all four data's mean value; default is [0, 2] device the number of device: -1 -> "cpu" or "cuda:x" (x is 0, 1 or ...) limit_part when transfer learning, we may ignore some part; the default is None, which means no ignorance; other choices are list, such as [0], [0, 1] or [1,2,..]; 0 means the first variable; tensor is [seq, time, var] or [time, seq, var] item_weight use different weight for each item's loss; for example, the default values [0.5, 0.5] means 0.5 * loss1 + 0.5 * loss2 alpha the weight of the water-balance item's loss beta the weight of real water-balance item's loss, et_mean/p_mean + q_mean/p_mean = 1 can be a loss. It is not strictly correct as training batch only have about one year data, but still could be a constraint wb_loss_func the loss function for water balance item, by default it is None, which means we use function in loss_funcs
Source code in torchhydro/models/crits.py
def __init__(
self,
loss_funcs: Union[torch.nn.Module, list],
data_gap: list = None,
device: list = None,
limit_part: list = None,
item_weight: list = None,
alpha=0.5,
beta=0.0,
wb_loss_func=None,
means=None,
stds=None,
):
"""
Loss function for multiple output considering water balance
loss = alpha * water_balance_loss + (1-alpha) * mtl_loss
This loss function is only for p, q, et now
we use the difference between p_obs_mean-q_obs_mean-et_obs_mean and p_pred_mean-q_pred_mean-et_pred_mean as water balance loss
which is the difference between (q_obs_mean + et_obs_mean) and (q_pred_mean + et_pred_mean)
Parameters
----------
loss_funcs
The loss functions for each output
data_gap
It belongs to the feature dim.
If 1, then the corresponding value is uniformly-spaced with NaN values filling the gap;
in addition, the first non-nan value means the aggregated value of the following interval,
for example, in [5, nan, nan, nan], 5 means all four data's sum, although the next 3 values are nan
hence the calculation is a little different;
if 2, the first non-nan value means the average value of the following interval,
for example, in [5, nan, nan, nan], 5 means all four data's mean value;
default is [0, 2]
device
the number of device: -1 -> "cpu" or "cuda:x" (x is 0, 1 or ...)
limit_part
when transfer learning, we may ignore some part;
the default is None, which means no ignorance;
other choices are list, such as [0], [0, 1] or [1,2,..];
0 means the first variable;
tensor is [seq, time, var] or [time, seq, var]
item_weight
use different weight for each item's loss;
for example, the default values [0.5, 0.5] means 0.5 * loss1 + 0.5 * loss2
alpha
the weight of the water-balance item's loss
beta
the weight of real water-balance item's loss, et_mean/p_mean + q_mean/p_mean = 1 can be a loss.
It is not strictly correct as training batch only have about one year data, but still could be a constraint
wb_loss_func
the loss function for water balance item, by default it is None, which means we use function in loss_funcs
"""
if data_gap is None:
data_gap = [0, 2]
if device is None:
device = [0]
if item_weight is None:
item_weight = [0.5, 0.5]
super(MultiOutWaterBalanceLoss, self).__init__()
self.loss_funcs = loss_funcs
self.data_gap = data_gap
self.device = get_the_device(device)
self.limit_part = limit_part
self.item_weight = item_weight
self.alpha = alpha
self.beta = beta
self.wb_loss_func = wb_loss_func
self.means = means
self.stds = stds
forward(self, output, target)
¶
Calculate the sum of losses for different variables and water-balance loss
When there are NaN values in observation, we will perform a "reduce" operation on prediction. For example, pred = [0,1,2,3,4], obs=[5, nan, nan, 6, nan]; the "reduce" is sum; then, pred_sum = [0+1+2, 3+4], obs_sum=[5,6], loss = loss_func(pred_sum, obs_sum). Notice: when "sum", actually final index is not chosen, because the whole observation may be [5, nan, nan, 6, nan, nan, 7, nan, nan], 6 means sum of three elements. Just as the rho is 5, the final one is not chosen
Parameters¶
output the prediction tensor; 3-dims are time sequence, batch and feature, respectively target the observation tensor
Returns¶
Tensor Whole loss
Source code in torchhydro/models/crits.py
def forward(self, output: Tensor, target: Tensor):
"""
Calculate the sum of losses for different variables and water-balance loss
When there are NaN values in observation, we will perform a "reduce" operation on prediction.
For example, pred = [0,1,2,3,4], obs=[5, nan, nan, 6, nan]; the "reduce" is sum;
then, pred_sum = [0+1+2, 3+4], obs_sum=[5,6], loss = loss_func(pred_sum, obs_sum).
Notice: when "sum", actually final index is not chosen,
because the whole observation may be [5, nan, nan, 6, nan, nan, 7, nan, nan], 6 means sum of three elements.
Just as the rho is 5, the final one is not chosen
Parameters
----------
output
the prediction tensor; 3-dims are time sequence, batch and feature, respectively
target
the observation tensor
Returns
-------
Tensor
Whole loss
"""
n_out = target.shape[-1]
loss = 0
p_means = []
t_means = []
all_means = self.means
all_stds = self.stds
for k in range(n_out):
if self.limit_part is not None and k in self.limit_part:
continue
p0 = output[:, :, k]
t0 = target[:, :, k]
# for water balance loss
if all_means is not None:
# denormalize for q and et
p1 = p0 * all_stds[k] + all_means[k]
t1 = t0 * all_stds[k] + all_means[k]
p2 = (10**p1 - 0.1) ** 2
t2 = (10**t1 - 0.1) ** 2
p_mean = torch.nanmean(p2, dim=0)
t_mean = torch.nanmean(t2, dim=0)
else:
p_mean = torch.nanmean(p0, dim=0)
t_mean = torch.nanmean(t0, dim=0)
p_means.append(p_mean)
t_means.append(t_mean)
# for mtl normal loss
mask = t0 == t0
p = p0[mask]
t = t0[mask]
if self.data_gap[k] > 0:
p, t = deal_gap_data(p0, t0, self.data_gap[k], self.device)
if type(self.loss_funcs) is list:
temp = self.item_weight[k] * self.loss_funcs[k](p, t)
else:
temp = self.item_weight[k] * self.loss_funcs(p, t)
# sum of all k-th loss
loss = loss + temp
# water balance loss
p_mean_q_plus_et = torch.sum(torch.stack(p_means), dim=0)
t_mean_q_plus_et = torch.sum(torch.stack(t_means), dim=0)
wb_ones = torch.ones_like(t_mean_q_plus_et)
if self.wb_loss_func is None:
if type(self.loss_funcs) is list:
# if wb_loss_func is None, we use the first loss function in loss_funcs
wb_loss = self.loss_funcs[0](p_mean_q_plus_et, t_mean_q_plus_et)
wb_1loss = self.loss_funcs[0](p_mean_q_plus_et, wb_ones)
else:
wb_loss = self.loss_funcs(p_mean_q_plus_et, t_mean_q_plus_et)
wb_1loss = self.loss_funcs(p_mean_q_plus_et, wb_ones)
else:
wb_loss = self.wb_loss_func(p_mean_q_plus_et, t_mean_q_plus_et)
wb_1loss = self.wb_loss_func(p_mean_q_plus_et, wb_ones)
return (
self.alpha * wb_loss
+ (1 - self.alpha - self.beta) * loss
+ self.beta * wb_1loss
)
NSELoss (Module)
¶
Source code in torchhydro/models/crits.py
class NSELoss(torch.nn.Module):
# Same as Fredrick 2019
def __init__(self):
super(NSELoss, self).__init__()
def forward(self, output, target):
Ngage = target.shape[1]
losssum = 0
nsample = 0
for ii in range(Ngage):
t0 = target[:, ii, 0]
mask = t0 == t0
if len(mask[mask]) > 0:
p0 = output[:, ii, 0]
p = p0[mask]
t = t0[mask]
tmean = t.mean()
SST = torch.sum((t - tmean) ** 2)
SSRes = torch.sum((t - p) ** 2)
temp = SSRes / ((torch.sqrt(SST) + 0.1) ** 2)
# original NSE
# temp = SSRes / SST
losssum = losssum + temp
nsample = nsample + 1
return losssum / nsample
forward(self, output, target)
¶
Define the computation performed at every call.
Should be overridden by all subclasses.
.. note::
Although the recipe for forward pass needs to be defined within
this function, one should call the :class:Module instance afterwards
instead of this since the former takes care of running the
registered hooks while the latter silently ignores them.
Source code in torchhydro/models/crits.py
def forward(self, output, target):
Ngage = target.shape[1]
losssum = 0
nsample = 0
for ii in range(Ngage):
t0 = target[:, ii, 0]
mask = t0 == t0
if len(mask[mask]) > 0:
p0 = output[:, ii, 0]
p = p0[mask]
t = t0[mask]
tmean = t.mean()
SST = torch.sum((t - tmean) ** 2)
SSRes = torch.sum((t - p) ** 2)
temp = SSRes / ((torch.sqrt(SST) + 0.1) ** 2)
# original NSE
# temp = SSRes / SST
losssum = losssum + temp
nsample = nsample + 1
return losssum / nsample
NegativeLogLikelihood (Module)
¶
target -> True y output -> predicted distribution
Source code in torchhydro/models/crits.py
class NegativeLogLikelihood(torch.nn.Module):
"""
target -> True y
output -> predicted distribution
"""
def __init__(self):
super().__init__()
def forward(self, output: torch.distributions, target: torch.Tensor):
"""
calculates NegativeLogLikelihood
"""
return -output.log_prob(target).sum()
forward(self, output, target)
¶
calculates NegativeLogLikelihood
Source code in torchhydro/models/crits.py
def forward(self, output: torch.distributions, target: torch.Tensor):
"""
calculates NegativeLogLikelihood
"""
return -output.log_prob(target).sum()
PESLoss (Module)
¶
Source code in torchhydro/models/crits.py
class PESLoss(torch.nn.Module):
def __init__(self):
"""
PES Loss: MSE × sigmoid(MSE)
This loss function applies a sigmoid activation to MSE and then multiplies it with MSE,
creating a non-linear penalty that increases more gradually for larger errors.
"""
super(PESLoss, self).__init__()
self.mse = torch.nn.MSELoss(reduction="none")
def forward(self, output: torch.Tensor, target: torch.Tensor):
mse_value = self.mse(output, target)
sigmoid_mse = torch.sigmoid(mse_value)
return mse_value * sigmoid_mse
__init__(self)
special
¶
PES Loss: MSE × sigmoid(MSE)
This loss function applies a sigmoid activation to MSE and then multiplies it with MSE, creating a non-linear penalty that increases more gradually for larger errors.
Source code in torchhydro/models/crits.py
def __init__(self):
"""
PES Loss: MSE × sigmoid(MSE)
This loss function applies a sigmoid activation to MSE and then multiplies it with MSE,
creating a non-linear penalty that increases more gradually for larger errors.
"""
super(PESLoss, self).__init__()
self.mse = torch.nn.MSELoss(reduction="none")
forward(self, output, target)
¶
Define the computation performed at every call.
Should be overridden by all subclasses.
.. note::
Although the recipe for forward pass needs to be defined within
this function, one should call the :class:Module instance afterwards
instead of this since the former takes care of running the
registered hooks while the latter silently ignores them.
Source code in torchhydro/models/crits.py
def forward(self, output: torch.Tensor, target: torch.Tensor):
mse_value = self.mse(output, target)
sigmoid_mse = torch.sigmoid(mse_value)
return mse_value * sigmoid_mse
PeakShapeLoss (FloodBaseLoss)
¶
Peak Shape Loss for flood events.
This loss focuses on three key hydrological indicators for flood peak prediction: 1. Peak Magnitude: Maximum flow value during flood event 2. Peak Timing: Time of peak occurrence (with asymmetric penalty for late/early) 3. Flood Volume: Total volume of water during flood event
Loss = peak_magnitude_weight * L_magnitude + peak_timing_weight * L_timing + flood_volume_weight * L_volume
Where: - L_magnitude: absolute difference in peak flow values - L_timing: time shift with asymmetric penalty (late > early) - L_volume: difference in cumulative flood volume
Source code in torchhydro/models/crits.py
class PeakShapeLoss(FloodBaseLoss):
"""
Peak Shape Loss for flood events.
This loss focuses on three key hydrological indicators for flood peak prediction:
1. Peak Magnitude: Maximum flow value during flood event
2. Peak Timing: Time of peak occurrence (with asymmetric penalty for late/early)
3. Flood Volume: Total volume of water during flood event
Loss = peak_magnitude_weight * L_magnitude +
peak_timing_weight * L_timing +
flood_volume_weight * L_volume
Where:
- L_magnitude: absolute difference in peak flow values
- L_timing: time shift with asymmetric penalty (late > early)
- L_volume: difference in cumulative flood volume
"""
def __init__(
self,
loss_func: Union[torch.nn.Module, str] = "MSELoss",
peak_magnitude_weight: float = 1.0,
peak_timing_weight: float = 1.0,
flood_volume_weight: float = 1.0,
late_penalty_factor: float = 2.0,
early_penalty_factor: float = 0.5,
flood_weight: float = 1.0,
non_flood_weight: float = 0.5,
device: list = None,
**kwargs,
):
"""
Initialize Peak Shape Loss.
Parameters
----------
loss_func : Union[torch.nn.Module, str]
Base loss function, default is "MSELoss"
peak_magnitude_weight : float
Weight for peak magnitude loss, default is 1.0
peak_timing_weight : float
Weight for peak timing loss, default is 1.0
flood_volume_weight : float
Weight for flood volume loss, default is 1.0
late_penalty_factor : float
Penalty multiplier for late peak predictions, default is 2.0
early_penalty_factor : float
Penalty multiplier for early peak predictions, default is 0.5
flood_weight : float
Weight multiplier for flood events in global loss, default is 1.0
non_flood_weight : float
Weight multiplier for non-flood events, default is 0.5
device : list
Device configuration, default is None (auto-detect)
"""
super(PeakShapeLoss, self).__init__()
self.peak_magnitude_weight = peak_magnitude_weight
self.peak_timing_weight = peak_timing_weight
self.flood_volume_weight = flood_volume_weight
self.late_penalty_factor = late_penalty_factor
self.early_penalty_factor = early_penalty_factor
self.flood_weight = flood_weight
self.non_flood_weight = non_flood_weight
self.device = get_the_device(device if device is not None else [0])
# Initialize base loss function
self.base_loss_func = self._initialize_base_loss(loss_func, kwargs)
print(f"[PeakShapeLoss] Initialized with:")
print(f" Peak magnitude weight: {peak_magnitude_weight}")
print(f" Peak timing weight: {peak_timing_weight} (late penalty: {late_penalty_factor}x, early: {early_penalty_factor}x)")
print(f" Flood volume weight: {flood_volume_weight}")
print(f" Flood weight: {flood_weight} vs non-flood: {non_flood_weight}")
def _initialize_base_loss(self, loss_func, kwargs):
"""Initialize base loss function."""
if isinstance(loss_func, str):
loss_dict = {
"MSELoss": torch.nn.MSELoss(reduction="none"),
"MAELoss": torch.nn.L1Loss(reduction="none"),
"L1Loss": torch.nn.L1Loss(reduction="none"),
"RMSELoss": RMSELoss(),
"HybridLoss": HybridLoss(
kwargs.get("mae_weight", 0.5), reduction="none"
),
}
if loss_func in loss_dict:
return loss_dict[loss_func]
else:
raise ValueError(f"Unsupported loss function string: {loss_func}")
else:
return loss_func
def compute_flood_loss(
self, predictions: torch.Tensor, targets: torch.Tensor, flood_mask: torch.Tensor
) -> torch.Tensor:
"""
Compute peak-based loss with three hydrological indicators.
Parameters
----------
predictions : torch.Tensor
Model predictions [batch_size, seq_len, output_features]
targets : torch.Tensor
Target values [batch_size, seq_len, output_features]
flood_mask : torch.Tensor
Flood mask [batch_size, seq_len] (1 for flood, 0 for normal)
Returns
-------
torch.Tensor
Computed loss value
"""
# Ensure flood_mask has correct shape
if flood_mask.dim() == 3 and flood_mask.shape[-1] == 1:
flood_mask = flood_mask.squeeze(-1)
# Handle warmup_length: predictions may have more timesteps than targets
if predictions.shape[1] > targets.shape[1]:
seq_diff = predictions.shape[1] - targets.shape[1]
predictions = predictions[:, seq_diff:, :]
flood_mask = flood_mask[:, seq_diff:]
batch_size, seq_len, num_features = targets.shape
# Extract streamflow (first feature)
pred_flow = predictions[:, :, 0] # [batch_size, seq_len]
target_flow = targets[:, :, 0] # [batch_size, seq_len]
magnitude_losses = []
timing_losses = []
volume_losses = []
for b in range(batch_size):
# Get flood mask for this sample
flood_mask_b = flood_mask[b] # [seq_len]
# Skip if no flood events
if flood_mask_b.sum() == 0:
continue
# Extract flood period data
pred_flood = pred_flow[b] # [seq_len]
target_flood = target_flow[b] # [seq_len]
# Skip if contains NaN
if torch.isnan(target_flood).any() or torch.isnan(pred_flood).any():
continue
# === 1. Peak Magnitude Loss ===
# Find peak values in the entire sequence
pred_peak_value = torch.max(pred_flood)
target_peak_value = torch.max(target_flood)
magnitude_loss = torch.abs(pred_peak_value - target_peak_value)
magnitude_losses.append(magnitude_loss)
# === 2. Peak Timing Loss (asymmetric penalty) ===
# Find peak positions
pred_peak_idx = torch.argmax(pred_flood)
target_peak_idx = torch.argmax(target_flood)
# Time difference (positive = late, negative = early)
time_diff = pred_peak_idx.float() - target_peak_idx.float()
# Asymmetric penalty
if time_diff > 0:
# Late prediction: higher penalty
timing_loss = self.late_penalty_factor * torch.abs(time_diff)
else:
# Early prediction: lower penalty
timing_loss = self.early_penalty_factor * torch.abs(time_diff)
timing_losses.append(timing_loss)
# === 3. Flood Volume Loss ===
# Compute cumulative volume during flood period
# Only sum over flood mask to focus on flood events
pred_volume = torch.sum(pred_flood * flood_mask_b)
target_volume = torch.sum(target_flood * flood_mask_b)
volume_loss = torch.abs(pred_volume - target_volume)
volume_losses.append(volume_loss)
# Compute mean losses across batch
if len(magnitude_losses) == 0:
# Fallback: if no valid samples, use global MSE
print("[PeakShapeLoss] Warning: No valid flood samples found, using global MSE")
mask = ~torch.isnan(targets)
base_loss = self.base_loss_func(predictions[mask], targets[mask])
return base_loss.mean()
magnitude_loss_mean = torch.stack(magnitude_losses).mean()
timing_loss_mean = torch.stack(timing_losses).mean()
volume_loss_mean = torch.stack(volume_losses).mean()
# Combined loss with equal weights (as requested)
total_loss = (
self.peak_magnitude_weight * magnitude_loss_mean +
self.peak_timing_weight * timing_loss_mean +
self.flood_volume_weight * volume_loss_mean
)
return total_loss
__init__(self, loss_func='MSELoss', peak_magnitude_weight=1.0, peak_timing_weight=1.0, flood_volume_weight=1.0, late_penalty_factor=2.0, early_penalty_factor=0.5, flood_weight=1.0, non_flood_weight=0.5, device=None, **kwargs)
special
¶
Initialize Peak Shape Loss.
Parameters¶
loss_func : Union[torch.nn.Module, str] Base loss function, default is "MSELoss" peak_magnitude_weight : float Weight for peak magnitude loss, default is 1.0 peak_timing_weight : float Weight for peak timing loss, default is 1.0 flood_volume_weight : float Weight for flood volume loss, default is 1.0 late_penalty_factor : float Penalty multiplier for late peak predictions, default is 2.0 early_penalty_factor : float Penalty multiplier for early peak predictions, default is 0.5 flood_weight : float Weight multiplier for flood events in global loss, default is 1.0 non_flood_weight : float Weight multiplier for non-flood events, default is 0.5 device : list Device configuration, default is None (auto-detect)
Source code in torchhydro/models/crits.py
def __init__(
self,
loss_func: Union[torch.nn.Module, str] = "MSELoss",
peak_magnitude_weight: float = 1.0,
peak_timing_weight: float = 1.0,
flood_volume_weight: float = 1.0,
late_penalty_factor: float = 2.0,
early_penalty_factor: float = 0.5,
flood_weight: float = 1.0,
non_flood_weight: float = 0.5,
device: list = None,
**kwargs,
):
"""
Initialize Peak Shape Loss.
Parameters
----------
loss_func : Union[torch.nn.Module, str]
Base loss function, default is "MSELoss"
peak_magnitude_weight : float
Weight for peak magnitude loss, default is 1.0
peak_timing_weight : float
Weight for peak timing loss, default is 1.0
flood_volume_weight : float
Weight for flood volume loss, default is 1.0
late_penalty_factor : float
Penalty multiplier for late peak predictions, default is 2.0
early_penalty_factor : float
Penalty multiplier for early peak predictions, default is 0.5
flood_weight : float
Weight multiplier for flood events in global loss, default is 1.0
non_flood_weight : float
Weight multiplier for non-flood events, default is 0.5
device : list
Device configuration, default is None (auto-detect)
"""
super(PeakShapeLoss, self).__init__()
self.peak_magnitude_weight = peak_magnitude_weight
self.peak_timing_weight = peak_timing_weight
self.flood_volume_weight = flood_volume_weight
self.late_penalty_factor = late_penalty_factor
self.early_penalty_factor = early_penalty_factor
self.flood_weight = flood_weight
self.non_flood_weight = non_flood_weight
self.device = get_the_device(device if device is not None else [0])
# Initialize base loss function
self.base_loss_func = self._initialize_base_loss(loss_func, kwargs)
print(f"[PeakShapeLoss] Initialized with:")
print(f" Peak magnitude weight: {peak_magnitude_weight}")
print(f" Peak timing weight: {peak_timing_weight} (late penalty: {late_penalty_factor}x, early: {early_penalty_factor}x)")
print(f" Flood volume weight: {flood_volume_weight}")
print(f" Flood weight: {flood_weight} vs non-flood: {non_flood_weight}")
compute_flood_loss(self, predictions, targets, flood_mask)
¶
Compute peak-based loss with three hydrological indicators.
Parameters¶
predictions : torch.Tensor Model predictions [batch_size, seq_len, output_features] targets : torch.Tensor Target values [batch_size, seq_len, output_features] flood_mask : torch.Tensor Flood mask [batch_size, seq_len] (1 for flood, 0 for normal)
Returns¶
torch.Tensor Computed loss value
Source code in torchhydro/models/crits.py
def compute_flood_loss(
self, predictions: torch.Tensor, targets: torch.Tensor, flood_mask: torch.Tensor
) -> torch.Tensor:
"""
Compute peak-based loss with three hydrological indicators.
Parameters
----------
predictions : torch.Tensor
Model predictions [batch_size, seq_len, output_features]
targets : torch.Tensor
Target values [batch_size, seq_len, output_features]
flood_mask : torch.Tensor
Flood mask [batch_size, seq_len] (1 for flood, 0 for normal)
Returns
-------
torch.Tensor
Computed loss value
"""
# Ensure flood_mask has correct shape
if flood_mask.dim() == 3 and flood_mask.shape[-1] == 1:
flood_mask = flood_mask.squeeze(-1)
# Handle warmup_length: predictions may have more timesteps than targets
if predictions.shape[1] > targets.shape[1]:
seq_diff = predictions.shape[1] - targets.shape[1]
predictions = predictions[:, seq_diff:, :]
flood_mask = flood_mask[:, seq_diff:]
batch_size, seq_len, num_features = targets.shape
# Extract streamflow (first feature)
pred_flow = predictions[:, :, 0] # [batch_size, seq_len]
target_flow = targets[:, :, 0] # [batch_size, seq_len]
magnitude_losses = []
timing_losses = []
volume_losses = []
for b in range(batch_size):
# Get flood mask for this sample
flood_mask_b = flood_mask[b] # [seq_len]
# Skip if no flood events
if flood_mask_b.sum() == 0:
continue
# Extract flood period data
pred_flood = pred_flow[b] # [seq_len]
target_flood = target_flow[b] # [seq_len]
# Skip if contains NaN
if torch.isnan(target_flood).any() or torch.isnan(pred_flood).any():
continue
# === 1. Peak Magnitude Loss ===
# Find peak values in the entire sequence
pred_peak_value = torch.max(pred_flood)
target_peak_value = torch.max(target_flood)
magnitude_loss = torch.abs(pred_peak_value - target_peak_value)
magnitude_losses.append(magnitude_loss)
# === 2. Peak Timing Loss (asymmetric penalty) ===
# Find peak positions
pred_peak_idx = torch.argmax(pred_flood)
target_peak_idx = torch.argmax(target_flood)
# Time difference (positive = late, negative = early)
time_diff = pred_peak_idx.float() - target_peak_idx.float()
# Asymmetric penalty
if time_diff > 0:
# Late prediction: higher penalty
timing_loss = self.late_penalty_factor * torch.abs(time_diff)
else:
# Early prediction: lower penalty
timing_loss = self.early_penalty_factor * torch.abs(time_diff)
timing_losses.append(timing_loss)
# === 3. Flood Volume Loss ===
# Compute cumulative volume during flood period
# Only sum over flood mask to focus on flood events
pred_volume = torch.sum(pred_flood * flood_mask_b)
target_volume = torch.sum(target_flood * flood_mask_b)
volume_loss = torch.abs(pred_volume - target_volume)
volume_losses.append(volume_loss)
# Compute mean losses across batch
if len(magnitude_losses) == 0:
# Fallback: if no valid samples, use global MSE
print("[PeakShapeLoss] Warning: No valid flood samples found, using global MSE")
mask = ~torch.isnan(targets)
base_loss = self.base_loss_func(predictions[mask], targets[mask])
return base_loss.mean()
magnitude_loss_mean = torch.stack(magnitude_losses).mean()
timing_loss_mean = torch.stack(timing_losses).mean()
volume_loss_mean = torch.stack(volume_losses).mean()
# Combined loss with equal weights (as requested)
total_loss = (
self.peak_magnitude_weight * magnitude_loss_mean +
self.peak_timing_weight * timing_loss_mean +
self.flood_volume_weight * volume_loss_mean
)
return total_loss
PenalizedMSELoss (Module)
¶
Returns MSE using: target -> True y output -> Predtion by model source: https://discuss.pytorch.org/t/rmse-loss-function/16540/3
Source code in torchhydro/models/crits.py
class PenalizedMSELoss(torch.nn.Module):
"""
Returns MSE using:
target -> True y
output -> Predtion by model
source: https://discuss.pytorch.org/t/rmse-loss-function/16540/3
"""
def __init__(self, variance_penalty=0.0):
super().__init__()
self.mse = torch.nn.MSELoss()
self.variance_penalty = variance_penalty
def forward(self, output: torch.Tensor, target: torch.Tensor):
return self.mse(target, output) + self.variance_penalty * torch.std(
torch.sub(target, output)
)
forward(self, output, target)
¶
Define the computation performed at every call.
Should be overridden by all subclasses.
.. note::
Although the recipe for forward pass needs to be defined within
this function, one should call the :class:Module instance afterwards
instead of this since the former takes care of running the
registered hooks while the latter silently ignores them.
Source code in torchhydro/models/crits.py
def forward(self, output: torch.Tensor, target: torch.Tensor):
return self.mse(target, output) + self.variance_penalty * torch.std(
torch.sub(target, output)
)
PyGraphLoss (Module)
¶
Source code in torchhydro/models/crits.py
class PyGraphLoss(torch.nn.Module):
def __init__(
self,
loss_func: Union[torch.nn.Module, str] = "MSELoss",
horizon_range: Union[int, list, tuple] = None,
use_node_mask: bool = True,
):
"""
Specialized loss for PyG batch data.
It assumes the input is flattened [Batch*Nodes, Horizon] and uses node_mask
to filter valid nodes before computing the base loss.
Parameters
----------
loss_func : Union[torch.nn.Module, str]
Base loss function
horizon_range : Union[int, list, tuple], optional
Which forecast horizons to include in loss calculation.
- None: Use all horizons (default)
- int: Use the first N horizons (e.g., 1 means only the first step)
- list/tuple: Use specific indices (e.g., [0, 2, 4]) or range (start, end)
"""
super(PyGraphLoss, self).__init__()
self.horizon_range = horizon_range
# Initialize base loss function
if isinstance(loss_func, str):
loss_dict = {
# NOTE: reduction="none" is important, otherwise the loss will be reduced to a scalar
"MSELoss": torch.nn.MSELoss(reduction="none"),
"MAELoss": torch.nn.L1Loss(reduction="none"),
"L1Loss": torch.nn.L1Loss(reduction="none"),
"RMSE": RMSELoss(),
}
if loss_func in loss_dict:
self.base_loss = loss_dict[loss_func]
else:
raise ValueError(f"Unsupported loss function string: {loss_func}")
else:
self.base_loss = loss_func
def forward(self, output, labels, node_mask=None, **kwargs):
# 0. Select Horizon Range
if self.horizon_range is not None:
if isinstance(self.horizon_range, int):
# Use first N steps
output = output[:, : self.horizon_range]
labels = labels[:, : self.horizon_range]
elif isinstance(self.horizon_range, (list, tuple)):
if len(self.horizon_range) == 2 and isinstance(self.horizon_range[0], int):
# Assume range [start, end)
output = output[:, self.horizon_range[0] : self.horizon_range[1]]
labels = labels[:, self.horizon_range[0] : self.horizon_range[1]]
else:
# Specific indices
output = output[:, self.horizon_range]
labels = labels[:, self.horizon_range]
# 1. Apply Node Mask
if node_mask is not None:
if not isinstance(node_mask, torch.Tensor):
node_mask = torch.tensor(
node_mask, dtype=torch.bool, device=labels.device
)
else:
node_mask = node_mask.to(labels.device).bool()
# Ensure mask is 1D [B*N_total]
if node_mask.dim() > 1:
node_mask = node_mask.flatten()
output = output[node_mask]
labels = labels[node_mask]
# 2. Handle NaN in labels
mask_nan = ~torch.isnan(labels)
output = output[mask_nan]
labels = labels[mask_nan]
return self.base_loss(output, labels)
__init__(self, loss_func='MSELoss', horizon_range=None, use_node_mask=True)
special
¶
Specialized loss for PyG batch data.
It assumes the input is flattened [Batch*Nodes, Horizon] and uses node_mask to filter valid nodes before computing the base loss.
Parameters¶
loss_func : Union[torch.nn.Module, str] Base loss function horizon_range : Union[int, list, tuple], optional Which forecast horizons to include in loss calculation. - None: Use all horizons (default) - int: Use the first N horizons (e.g., 1 means only the first step) - list/tuple: Use specific indices (e.g., [0, 2, 4]) or range (start, end)
Source code in torchhydro/models/crits.py
def __init__(
self,
loss_func: Union[torch.nn.Module, str] = "MSELoss",
horizon_range: Union[int, list, tuple] = None,
use_node_mask: bool = True,
):
"""
Specialized loss for PyG batch data.
It assumes the input is flattened [Batch*Nodes, Horizon] and uses node_mask
to filter valid nodes before computing the base loss.
Parameters
----------
loss_func : Union[torch.nn.Module, str]
Base loss function
horizon_range : Union[int, list, tuple], optional
Which forecast horizons to include in loss calculation.
- None: Use all horizons (default)
- int: Use the first N horizons (e.g., 1 means only the first step)
- list/tuple: Use specific indices (e.g., [0, 2, 4]) or range (start, end)
"""
super(PyGraphLoss, self).__init__()
self.horizon_range = horizon_range
# Initialize base loss function
if isinstance(loss_func, str):
loss_dict = {
# NOTE: reduction="none" is important, otherwise the loss will be reduced to a scalar
"MSELoss": torch.nn.MSELoss(reduction="none"),
"MAELoss": torch.nn.L1Loss(reduction="none"),
"L1Loss": torch.nn.L1Loss(reduction="none"),
"RMSE": RMSELoss(),
}
if loss_func in loss_dict:
self.base_loss = loss_dict[loss_func]
else:
raise ValueError(f"Unsupported loss function string: {loss_func}")
else:
self.base_loss = loss_func
forward(self, output, labels, node_mask=None, **kwargs)
¶
Define the computation performed at every call.
Should be overridden by all subclasses.
.. note::
Although the recipe for forward pass needs to be defined within
this function, one should call the :class:Module instance afterwards
instead of this since the former takes care of running the
registered hooks while the latter silently ignores them.
Source code in torchhydro/models/crits.py
def forward(self, output, labels, node_mask=None, **kwargs):
# 0. Select Horizon Range
if self.horizon_range is not None:
if isinstance(self.horizon_range, int):
# Use first N steps
output = output[:, : self.horizon_range]
labels = labels[:, : self.horizon_range]
elif isinstance(self.horizon_range, (list, tuple)):
if len(self.horizon_range) == 2 and isinstance(self.horizon_range[0], int):
# Assume range [start, end)
output = output[:, self.horizon_range[0] : self.horizon_range[1]]
labels = labels[:, self.horizon_range[0] : self.horizon_range[1]]
else:
# Specific indices
output = output[:, self.horizon_range]
labels = labels[:, self.horizon_range]
# 1. Apply Node Mask
if node_mask is not None:
if not isinstance(node_mask, torch.Tensor):
node_mask = torch.tensor(
node_mask, dtype=torch.bool, device=labels.device
)
else:
node_mask = node_mask.to(labels.device).bool()
# Ensure mask is 1D [B*N_total]
if node_mask.dim() > 1:
node_mask = node_mask.flatten()
output = output[node_mask]
labels = labels[node_mask]
# 2. Handle NaN in labels
mask_nan = ~torch.isnan(labels)
output = output[mask_nan]
labels = labels[mask_nan]
return self.base_loss(output, labels)
QuantileLoss (Module)
¶
From https://medium.com/the-artificial-impostor/quantile-regression-part-2-6fdbc26b2629
Source code in torchhydro/models/crits.py
class QuantileLoss(torch.nn.Module):
"""From https://medium.com/the-artificial-impostor/quantile-regression-part-2-6fdbc26b2629"""
def __init__(self, quantiles):
super().__init__()
self.quantiles = quantiles
def forward(self, preds, target):
assert not target.requires_grad
assert preds.size(0) == target.size(0)
losses = []
for i, q in enumerate(self.quantiles):
mask = ~torch.isnan(target[:, :, i])
errors = target[:, :, i][mask] - preds[:, :, i][mask]
losses.append(torch.max((q - 1) * errors, q * errors))
return torch.mean(torch.cat(losses, dim=0), dim=0)
forward(self, preds, target)
¶
Define the computation performed at every call.
Should be overridden by all subclasses.
.. note::
Although the recipe for forward pass needs to be defined within
this function, one should call the :class:Module instance afterwards
instead of this since the former takes care of running the
registered hooks while the latter silently ignores them.
Source code in torchhydro/models/crits.py
def forward(self, preds, target):
assert not target.requires_grad
assert preds.size(0) == target.size(0)
losses = []
for i, q in enumerate(self.quantiles):
mask = ~torch.isnan(target[:, :, i])
errors = target[:, :, i][mask] - preds[:, :, i][mask]
losses.append(torch.max((q - 1) * errors, q * errors))
return torch.mean(torch.cat(losses, dim=0), dim=0)
RMSELoss (Module)
¶
Source code in torchhydro/models/crits.py
class RMSELoss(torch.nn.Module):
def __init__(self, variance_penalty=0.0):
"""
Calculate RMSE
using:
target -> True y
output -> Prediction by model
source: https://discuss.pytorch.org/t/rmse-loss-function/16540/3
Parameters
----------
variance_penalty
penalty for big variance; default is 0
"""
super().__init__()
self.mse = torch.nn.MSELoss()
self.variance_penalty = variance_penalty
def forward(self, output: torch.Tensor, target: torch.Tensor):
valid = torch.isfinite(target) & torch.isfinite(output)
if not valid.all():
if not valid.any():
return output.new_zeros(())
output = output[valid]
target = target[valid]
if len(output) <= 1 or self.variance_penalty <= 0.0:
return torch.sqrt(self.mse(target, output))
diff = torch.sub(target, output)
std_dev = torch.std(diff)
var_penalty = self.variance_penalty * std_dev
return torch.sqrt(self.mse(target, output)) + var_penalty
__init__(self, variance_penalty=0.0)
special
¶
Calculate RMSE
!!! using target -> True y output -> Prediction by model source: https://discuss.pytorch.org/t/rmse-loss-function/16540/3
Parameters¶
variance_penalty penalty for big variance; default is 0
Source code in torchhydro/models/crits.py
def __init__(self, variance_penalty=0.0):
"""
Calculate RMSE
using:
target -> True y
output -> Prediction by model
source: https://discuss.pytorch.org/t/rmse-loss-function/16540/3
Parameters
----------
variance_penalty
penalty for big variance; default is 0
"""
super().__init__()
self.mse = torch.nn.MSELoss()
self.variance_penalty = variance_penalty
forward(self, output, target)
¶
Define the computation performed at every call.
Should be overridden by all subclasses.
.. note::
Although the recipe for forward pass needs to be defined within
this function, one should call the :class:Module instance afterwards
instead of this since the former takes care of running the
registered hooks while the latter silently ignores them.
Source code in torchhydro/models/crits.py
def forward(self, output: torch.Tensor, target: torch.Tensor):
valid = torch.isfinite(target) & torch.isfinite(output)
if not valid.all():
if not valid.any():
return output.new_zeros(())
output = output[valid]
target = target[valid]
if len(output) <= 1 or self.variance_penalty <= 0.0:
return torch.sqrt(self.mse(target, output))
diff = torch.sub(target, output)
std_dev = torch.std(diff)
var_penalty = self.variance_penalty * std_dev
return torch.sqrt(self.mse(target, output)) + var_penalty
RmseLoss (Module)
¶
Source code in torchhydro/models/crits.py
class RmseLoss(torch.nn.Module):
def __init__(self):
"""
RMSE loss which could ignore NaN values
Now we only support 3-d tensor and 1-d tensor
"""
super(RmseLoss, self).__init__()
def forward(self, output, target):
if target.dim() == 1:
mask = target == target
p = output[mask]
t = target[mask]
return torch.sqrt(((p - t) ** 2).mean())
ny = target.shape[2]
loss = 0
for k in range(ny):
p0 = output[:, :, k]
t0 = target[:, :, k]
mask = t0 == t0
p = p0[mask]
p = torch.where(torch.isnan(p), torch.full_like(p, 0), p)
t = t0[mask]
t = torch.where(torch.isnan(t), torch.full_like(t, 0), t)
temp = torch.sqrt(((p - t) ** 2).mean())
loss = loss + temp
return loss
__init__(self)
special
¶
RMSE loss which could ignore NaN values
Now we only support 3-d tensor and 1-d tensor
Source code in torchhydro/models/crits.py
def __init__(self):
"""
RMSE loss which could ignore NaN values
Now we only support 3-d tensor and 1-d tensor
"""
super(RmseLoss, self).__init__()
forward(self, output, target)
¶
Define the computation performed at every call.
Should be overridden by all subclasses.
.. note::
Although the recipe for forward pass needs to be defined within
this function, one should call the :class:Module instance afterwards
instead of this since the former takes care of running the
registered hooks while the latter silently ignores them.
Source code in torchhydro/models/crits.py
def forward(self, output, target):
if target.dim() == 1:
mask = target == target
p = output[mask]
t = target[mask]
return torch.sqrt(((p - t) ** 2).mean())
ny = target.shape[2]
loss = 0
for k in range(ny):
p0 = output[:, :, k]
t0 = target[:, :, k]
mask = t0 == t0
p = p0[mask]
p = torch.where(torch.isnan(p), torch.full_like(p, 0), p)
t = t0[mask]
t = torch.where(torch.isnan(t), torch.full_like(t, 0), t)
temp = torch.sqrt(((p - t) ** 2).mean())
loss = loss + temp
return loss
SigmaLoss (Module)
¶
Source code in torchhydro/models/crits.py
class SigmaLoss(torch.nn.Module):
def __init__(self, prior="gauss"):
super(SigmaLoss, self).__init__()
self.reduction = "elementwise_mean"
self.prior = None if prior == "" else prior.split("+")
def forward(self, output, target):
ny = target.shape[-1]
lossMean = 0
for k in range(ny):
p0 = output[:, :, k * 2]
s0 = output[:, :, k * 2 + 1]
t0 = target[:, :, k]
mask = t0 == t0
p = p0[mask]
s = s0[mask]
t = t0[mask]
if self.prior[0] == "gauss":
loss = torch.exp(-s).mul((p - t) ** 2) / 2 + s / 2
elif self.prior[0] == "invGamma":
c1 = float(self.prior[1])
c2 = float(self.prior[2])
nt = p.shape[0]
loss = (
torch.exp(-s).mul((p - t) ** 2 + c2 / nt) / 2
+ (1 / 2 + c1 / nt) * s
)
lossMean = lossMean + torch.mean(loss)
return lossMean
forward(self, output, target)
¶
Define the computation performed at every call.
Should be overridden by all subclasses.
.. note::
Although the recipe for forward pass needs to be defined within
this function, one should call the :class:Module instance afterwards
instead of this since the former takes care of running the
registered hooks while the latter silently ignores them.
Source code in torchhydro/models/crits.py
def forward(self, output, target):
ny = target.shape[-1]
lossMean = 0
for k in range(ny):
p0 = output[:, :, k * 2]
s0 = output[:, :, k * 2 + 1]
t0 = target[:, :, k]
mask = t0 == t0
p = p0[mask]
s = s0[mask]
t = t0[mask]
if self.prior[0] == "gauss":
loss = torch.exp(-s).mul((p - t) ** 2) / 2 + s / 2
elif self.prior[0] == "invGamma":
c1 = float(self.prior[1])
c2 = float(self.prior[2])
nt = p.shape[0]
loss = (
torch.exp(-s).mul((p - t) ** 2 + c2 / nt) / 2
+ (1 / 2 + c1 / nt) * s
)
lossMean = lossMean + torch.mean(loss)
return lossMean
UncertaintyWeights (Module)
¶
Uncertainty Weights (UW).
This method is proposed in Multi-Task Learning Using Uncertainty to Weigh Losses for Scene Geometry and Semantics (CVPR 2018) <https://openaccess.thecvf.com/content_cvpr_2018/papers/Kendall_Multi-Task_Learning_Using_CVPR_2018_paper.pdf>_ \
and implemented by us.
Source code in torchhydro/models/crits.py
class UncertaintyWeights(torch.nn.Module):
r"""Uncertainty Weights (UW).
This method is proposed in `Multi-Task Learning Using Uncertainty to Weigh Losses for Scene Geometry and Semantics (CVPR 2018) <https://openaccess.thecvf.com/content_cvpr_2018/papers/Kendall_Multi-Task_Learning_Using_CVPR_2018_paper.pdf>`_ \
and implemented by us.
"""
def __init__(
self,
loss_funcs: Union[torch.nn.Module, list],
data_gap: list = None,
device: list = None,
limit_part: list = None,
):
if data_gap is None:
data_gap = [0, 2]
if device is None:
device = [0]
super(UncertaintyWeights, self).__init__()
self.loss_funcs = loss_funcs
self.data_gap = data_gap
self.device = get_the_device(device)
self.limit_part = limit_part
def forward(self, output, target, log_vars):
"""
Parameters
----------
output
target
log_vars
sigma in uncertainty weighting;
default is None, meaning we manually set weights for different target's loss;
more info could be seen in
https://libmtl.readthedocs.io/en/latest/docs/_autoapi/LibMTL/weighting/index.html#LibMTL.weighting.UW
Returns
-------
torch.Tensor
multi-task loss by uncertainty weighting method
"""
n_out = target.shape[-1]
loss = 0
for k in range(n_out):
precision = torch.exp(-log_vars[k])
if self.limit_part is not None and k in self.limit_part:
continue
p0 = output[:, :, k]
t0 = target[:, :, k]
mask = t0 == t0
p = p0[mask]
t = t0[mask]
if self.data_gap[k] > 0:
p, t = deal_gap_data(p0, t0, self.data_gap[k], self.device)
if type(self.loss_funcs) is list:
temp = self.loss_funcs[k](p, t)
else:
temp = self.loss_funcs(p, t)
loss += torch.sum(precision * temp + log_vars[k], -1)
return loss
forward(self, output, target, log_vars)
¶
Parameters¶
output target log_vars sigma in uncertainty weighting; default is None, meaning we manually set weights for different target's loss; more info could be seen in https://libmtl.readthedocs.io/en/latest/docs/_autoapi/LibMTL/weighting/index.html#LibMTL.weighting.UW
Returns¶
torch.Tensor multi-task loss by uncertainty weighting method
Source code in torchhydro/models/crits.py
def forward(self, output, target, log_vars):
"""
Parameters
----------
output
target
log_vars
sigma in uncertainty weighting;
default is None, meaning we manually set weights for different target's loss;
more info could be seen in
https://libmtl.readthedocs.io/en/latest/docs/_autoapi/LibMTL/weighting/index.html#LibMTL.weighting.UW
Returns
-------
torch.Tensor
multi-task loss by uncertainty weighting method
"""
n_out = target.shape[-1]
loss = 0
for k in range(n_out):
precision = torch.exp(-log_vars[k])
if self.limit_part is not None and k in self.limit_part:
continue
p0 = output[:, :, k]
t0 = target[:, :, k]
mask = t0 == t0
p = p0[mask]
t = t0[mask]
if self.data_gap[k] > 0:
p, t = deal_gap_data(p0, t0, self.data_gap[k], self.device)
if type(self.loss_funcs) is list:
temp = self.loss_funcs[k](p, t)
else:
temp = self.loss_funcs(p, t)
loss += torch.sum(precision * temp + log_vars[k], -1)
return loss
deal_gap_data(output, target, data_gap, device)
¶
How to handle with gap data
When there are NaN values in observation, we will perform a "reduce" operation on prediction. For example, pred = [0,1,2,3,4], obs=[5, nan, nan, 6, nan]; the "reduce" is sum; then, pred_sum = [0+1+2, 3+4], obs_sum=[5,6], loss = loss_func(pred_sum, obs_sum). Notice: when "sum", actually final index is not chosen, because the whole observation may be [5, nan, nan, 6, nan, nan, 7, nan, nan], 6 means sum of three elements. Just as the rho is 5, the final one is not chosen
Parameters¶
output model output for k-th variable target target for k-th variable data_gap data_gap=1: reduce is sum data_gap=2: reduce is mean device where to save the data
Returns¶
tuple[tensor, tensor] output and target after dealing with gap
Source code in torchhydro/models/crits.py
def deal_gap_data(output, target, data_gap, device):
"""
How to handle with gap data
When there are NaN values in observation, we will perform a "reduce" operation on prediction.
For example, pred = [0,1,2,3,4], obs=[5, nan, nan, 6, nan]; the "reduce" is sum;
then, pred_sum = [0+1+2, 3+4], obs_sum=[5,6], loss = loss_func(pred_sum, obs_sum).
Notice: when "sum", actually final index is not chosen,
because the whole observation may be [5, nan, nan, 6, nan, nan, 7, nan, nan], 6 means sum of three elements.
Just as the rho is 5, the final one is not chosen
Parameters
----------
output
model output for k-th variable
target
target for k-th variable
data_gap
data_gap=1: reduce is sum
data_gap=2: reduce is mean
device
where to save the data
Returns
-------
tuple[tensor, tensor]
output and target after dealing with gap
"""
# all members in a batch has different NaN-gap, so we need a loop
seg_p_lst = []
seg_t_lst = []
for j in range(target.shape[1]):
non_nan_idx = torch.nonzero(
~torch.isnan(target[:, j]), as_tuple=False
).squeeze()
if len(non_nan_idx) < 1:
raise ArithmeticError("All NaN elements, please check your data")
# 使用 cumsum 生成 scatter_index
is_not_nan = ~torch.isnan(target[:, j])
cumsum_is_not_nan = torch.cumsum(is_not_nan.to(torch.int), dim=0)
first_non_nan = non_nan_idx[0]
scatter_index = torch.full_like(
target[:, j], fill_value=-1, dtype=torch.long
) # 将所有值初始化为 -1
scatter_index[first_non_nan:] = cumsum_is_not_nan[first_non_nan:] - 1
scatter_index = scatter_index.to(device=device)
# 创建掩码,只保留有效的索引
valid_mask = scatter_index >= 0
if data_gap == 1:
seg = torch.zeros(
len(non_nan_idx), device=device, dtype=output.dtype
).scatter_add_(0, scatter_index[valid_mask], output[valid_mask, j])
# for sum, better exclude final non-nan value as it didn't include all necessary periods
seg_p_lst.append(seg[:-1])
seg_t_lst.append(target[non_nan_idx[:-1], j])
elif data_gap == 2:
counts = torch.zeros(
len(non_nan_idx), device=device, dtype=output.dtype
).scatter_add_(
0,
scatter_index[valid_mask],
torch.ones_like(output[valid_mask, j], dtype=output.dtype),
)
seg = torch.zeros(
len(non_nan_idx), device=device, dtype=output.dtype
).scatter_add_(0, scatter_index[valid_mask], output[valid_mask, j])
seg = seg / counts.clamp(min=1)
# for mean, we can include all periods
seg_p_lst.append(seg)
seg_t_lst.append(target[non_nan_idx, j])
else:
raise NotImplementedError(
"We have not provided this reduce way now!! Please choose 1 or 2!!"
)
p = torch.cat(seg_p_lst)
t = torch.cat(seg_t_lst)
return p, t
l1_regularizer(model, lambda_l1=0.01)
¶
source: https://stackoverflow.com/questions/58172188/how-to-add-l1-regularization-to-pytorch-nn-model
Source code in torchhydro/models/crits.py
def l1_regularizer(model, lambda_l1=0.01):
"""
source: https://stackoverflow.com/questions/58172188/how-to-add-l1-regularization-to-pytorch-nn-model
"""
lossl1 = 0
for model_param_name, model_param_value in model.named_parameters():
if model_param_name.endswith("weight"):
lossl1 += lambda_l1 * model_param_value.abs().sum()
return lossl1
orth_regularizer(model, lambda_orth=0.01)
¶
source: https://stackoverflow.com/questions/58172188/how-to-add-l1-regularization-to-pytorch-nn-model
Source code in torchhydro/models/crits.py
def orth_regularizer(model, lambda_orth=0.01):
"""
source: https://stackoverflow.com/questions/58172188/how-to-add-l1-regularization-to-pytorch-nn-model
"""
lossorth = 0
for model_param_name, model_param_value in model.named_parameters():
if model_param_name.endswith("weight"):
param_flat = model_param_value.view(model_param_value.shape[0], -1)
sym = torch.mm(param_flat, torch.t(param_flat))
sym -= torch.eye(param_flat.shape[0])
lossorth += lambda_orth * sym.sum()
return lossorth