项目文件夹

文件
wehub-resource-sync 593b94c120
pytest / Unit Tests (push) Has been cancelled
pytest / Integration (integration_tests_a) (push) Has been cancelled
pytest / Integration (integration_tests_b) (push) Has been cancelled
pytest / Integration (integration_tests_c) (push) Has been cancelled
pytest / Integration (integration_tests_d) (push) Has been cancelled
pytest / Integration (integration_tests_e) (push) Has been cancelled
pytest / Integration (integration_tests_f) (push) Has been cancelled
pytest / Integration (integration_tests_g) (push) Has been cancelled
pytest / Integration (integration_tests_h) (push) Has been cancelled
pytest / Integration (integration_tests_i) (push) Has been cancelled
pytest / Integration (integration_tests_j) (push) Has been cancelled
pytest / Distributed (distributed_a) (push) Has been cancelled
pytest / Distributed (distributed_b) (push) Has been cancelled
pytest / Distributed (distributed_c) (push) Has been cancelled
pytest / Distributed (distributed_d) (push) Has been cancelled
pytest / Distributed (distributed_e) (push) Has been cancelled
pytest / Distributed (distributed_f) (push) Has been cancelled
pytest / Minimal Install (push) Has been cancelled
pytest / Event File (push) Has been cancelled
pytest (slow) / py-slow (push) Has been cancelled
Publish JSON Schema / publish-schema (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:49:20 +08:00

351 行
14 KiB
Python

import contextlib
import logging
from abc import ABCMeta, abstractmethod
from collections import OrderedDict
from typing import Any
import numpy as np
import torch
import torchmetrics
from ludwig.combiners.combiners import Combiner
from ludwig.constants import COMBINED, LOSS, NAME
from ludwig.encoders.base import Encoder
from ludwig.features.base_feature import InputFeature, NonPropertyModuleWrapper, OutputFeature
from ludwig.features.feature_registries import get_input_type_registry, get_output_type_registry
from ludwig.features.feature_utils import LudwigFeatureDict
from ludwig.features.passthrough_feature import create_passthrough_input_feature
from ludwig.modules.metric_modules import LudwigMetric
from ludwig.modules.training_hooks import TrainingHook
from ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig, FeatureCollection
from ludwig.utils.algorithms_utils import topological_sort_feature_dependencies
from ludwig.utils.metric_utils import get_scalar_from_ludwig_metric
from ludwig.utils.misc_utils import get_from_registry
from ludwig.utils.torch_utils import LudwigModule, reg_loss
logger = logging.getLogger(__name__)
class BaseModel(LudwigModule, metaclass=ABCMeta):
"""Base model for use in LudwigModule.
Implementations of this class should implement the following methods:
- type()
- forward()
"""
@staticmethod
@abstractmethod
def type() -> str:
"""Returns the model type."""
def __init__(self, random_seed: int | None = None):
self._random_seed = random_seed
# Ensures model weight initialization is deterministic even though set_random_seed()
# is called later in the trainer. Required for test_api::test_api_training_determinism.
if random_seed is not None:
torch.random.manual_seed(random_seed)
super().__init__()
self.input_features = self.create_feature_dict()
self.output_features = self.create_feature_dict()
# ================ Combined loss metric ================
self._eval_loss_metric = NonPropertyModuleWrapper(torchmetrics.MeanMetric())
self._eval_additional_losses_metrics = NonPropertyModuleWrapper(torchmetrics.MeanMetric())
# ================ Training Hook Handles ================
self._forward_hook_handles: list[TrainingHook] = []
def create_feature_dict(self) -> LudwigFeatureDict:
"""Creates and returns a LudwigFeatureDict."""
return LudwigFeatureDict()
def to_device(self, device):
return self.to(device)
def metrics_to_device(self, device: str):
self._eval_loss_metric.module = self._eval_loss_metric.module.to(device)
self._eval_additional_losses_metrics.module = self._eval_additional_losses_metrics.module.to(device)
for feature in self.output_features.values():
feature._eval_loss_metric.module = feature._eval_loss_metric.module.to(device)
@classmethod
def build_inputs(cls, input_feature_configs: FeatureCollection[BaseInputFeatureConfig]) -> dict[str, InputFeature]:
"""Builds and returns input features in topological order."""
input_features = OrderedDict()
input_features_def = topological_sort_feature_dependencies(input_feature_configs.to_list())
for input_feature_def in input_features_def:
input_features[input_feature_def[NAME]] = cls.build_single_input(
getattr(input_feature_configs, input_feature_def[NAME]), input_features
)
return input_features
@staticmethod
def build_single_input(
feature_config: BaseInputFeatureConfig, other_input_features: dict[str, InputFeature] | None
) -> InputFeature:
"""Builds a single input feature from the input feature definition."""
logger.debug(f"Input {feature_config.type} feature {feature_config.name}")
encoder_obj = None
if feature_config.tied is not None:
tied_input_feature_name = feature_config.tied
if tied_input_feature_name in other_input_features:
encoder_obj = other_input_features[tied_input_feature_name].encoder_obj
return create_input_feature(feature_config, encoder_obj)
@classmethod
def build_outputs(
cls, output_feature_configs: FeatureCollection[BaseOutputFeatureConfig], combiner: Combiner
) -> dict[str, OutputFeature]:
"""Builds and returns output features in topological order."""
output_features_def = topological_sort_feature_dependencies(output_feature_configs.to_list())
output_features = {}
for output_feature_def in output_features_def:
# TODO(Justin): Check that the semantics of input_size align with what the combiner's output shape returns
# for seq2seq.
getattr(output_feature_configs, output_feature_def[NAME]).input_size = combiner.output_shape[-1]
output_features[output_feature_def[NAME]] = cls.build_single_output(
getattr(output_feature_configs, output_feature_def[NAME]), output_features
)
return output_features
@staticmethod
def build_single_output(
feature_config: BaseOutputFeatureConfig, output_features: dict[str, OutputFeature] | None
) -> OutputFeature:
"""Builds a single output feature from the output feature definition."""
logger.debug(f"Output {feature_config.type} feature {feature_config.name}")
output_feature_class = get_from_registry(feature_config.type, get_output_type_registry())
output_feature_obj = output_feature_class(feature_config, output_features=output_features)
return output_feature_obj
def get_model_inputs(self):
"""Returns a dict of feature name -> sample model input."""
device = next(self.parameters()).device
inputs = {
input_feature_name: input_feature.create_sample_input().to(device)
for input_feature_name, input_feature in self.input_features.items()
}
return inputs
def get_model_size(self) -> int:
"""Returns total number of parameters in model."""
model_tensors = self.collect_weights()
total_size = 0
for tnsr in model_tensors:
total_size += tnsr[1].detach().cpu().numpy().size
return total_size
@property
def input_shape(self) -> torch.Size:
"""Returns the shape of a single model input (excluding batch dimension).
Subclasses should override this to return a meaningful shape.
The default is a (1,) scalar — sufficient for models that don't rely
on input_shape for decoder sizing.
"""
return torch.Size([1])
@abstractmethod
def forward(
self,
inputs: (
dict[str, torch.Tensor] | dict[str, np.ndarray] | tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]
),
mask=None,
) -> dict[str, torch.Tensor]:
"""Forward pass of the model.
Args:
inputs: Inputs to the model. Can be a dictionary of input names to
input tensors or a tuple of (inputs, targets) where inputs is
a dictionary of input names to input tensors and targets is a
dictionary of target names to target tensors.
mask: A mask for the inputs.
Returns:
A dictionary of output {feature name}::{tensor_name} -> output tensor.
"""
def predictions(self, inputs):
"""Returns the model's predictions for the given inputs."""
outputs = self(inputs)
return self.outputs_to_predictions(outputs)
def outputs_to_predictions(self, outputs: dict[str, torch.Tensor]) -> dict[str, dict[str, torch.Tensor]]:
"""Returns the model's predictions given the raw model outputs."""
predictions = {}
for of_name in self.output_features:
predictions[of_name] = self.output_features.get(of_name).predictions(outputs, of_name)
return predictions
def evaluation_step(self, inputs, targets):
"""Predict the inputs and update evaluation metrics."""
predictions = self.predictions(inputs)
self.update_metrics(targets, predictions)
return predictions
def predict_step(self, inputs):
"""Predict the inputs."""
return self.predictions(inputs)
def train_loss(
self,
targets,
predictions,
regularization_type: str | None = None,
regularization_lambda: float | None = None,
) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
"""Computes the training loss for the model.
Args:
targets: A dictionary of target names to target tensors.
predictions: A dictionary of output names to output tensors.
regularization_type: One of 'l1', 'l2', 'l1_l2', or None.
regularization_lambda: The regularization lambda.
Returns:
A tuple of the loss tensor and a dictionary of loss for every
output feature.
"""
of_train_losses = {}
of_weights = {}
for of_name, of_obj in self.output_features.items():
of_train_loss = of_obj.train_loss(targets[of_name], predictions, of_name)
of_train_losses[of_name] = of_train_loss
of_weights[of_name] = of_obj.loss.weight
# Use loss balancer if available, otherwise static weighted sum
if hasattr(self, "loss_balancer") and self.loss_balancer is not None:
train_loss = self.loss_balancer(of_train_losses, of_weights)
else:
train_loss = sum(of_weights[k] * of_train_losses[k] for k in of_train_losses)
additional_losses = self.losses()
if additional_losses:
train_loss += torch.sum(torch.stack(additional_losses)) # other losses
# Add regularization loss
if regularization_type is not None and regularization_lambda != 0:
train_loss += reg_loss(self, regularization_type, l1=regularization_lambda, l2=regularization_lambda)
return train_loss, of_train_losses
def eval_loss(self, targets, predictions):
"""Computes all evaluation losses for the model given targets and predictions.
Args:
targets: A dictionary of target names to target tensors.
predictions: A dictionary of output names to output tensors.
Returns:
A tuple of loss values for eval losses and additional losses.
"""
eval_loss = 0
for of_name, of_obj in self.output_features.items():
of_eval_loss = of_obj.eval_loss(targets[of_name], predictions[of_name])
eval_loss += of_obj.loss.weight * of_eval_loss
additional_loss = 0
additional_losses = self.losses()
if additional_losses:
additional_loss = torch.sum(torch.stack(additional_losses)) # other losses
return eval_loss, additional_loss
def update_metrics(self, targets, predictions):
"""Updates the model's metrics given targets and predictions."""
for of_name, of_obj in self.output_features.items():
of_obj.update_metrics(targets[of_name], predictions[of_name])
eval_loss, additional_losses = self.eval_loss(targets, predictions)
self.eval_loss_metric.update(eval_loss)
self.eval_additional_losses_metrics.update(additional_losses)
@property
def eval_loss_metric(self) -> LudwigMetric:
return self._eval_loss_metric.module
@eval_loss_metric.setter
def eval_loss_metric(self, value: LudwigMetric) -> None:
self._eval_loss_metric.module = value
@property
def eval_additional_losses_metrics(self) -> LudwigMetric:
return self._eval_additional_losses_metrics.module
def get_metrics(self) -> dict[str, dict[str, float]]:
"""Returns a dictionary of metrics for each output feature of the model."""
all_of_metrics = {}
for of_name, of_obj in self.output_features.items():
all_of_metrics[of_name] = of_obj.get_metrics()
all_of_metrics[COMBINED] = {
LOSS: get_scalar_from_ludwig_metric(self.eval_loss_metric)
+ get_scalar_from_ludwig_metric(self.eval_additional_losses_metrics)
}
return all_of_metrics
def reset_metrics(self):
"""Resets the model's metrics."""
for of_obj in self.output_features.values():
of_obj.reset_metrics()
self.eval_loss_metric.reset()
def collect_weights(self, tensor_names=None, **kwargs):
"""Returns named parameters filtered against `tensor_names` if not None."""
if not tensor_names:
return self.named_parameters()
# Check for bad tensor names.
weight_names = {name for name, _ in self.named_parameters()}
for name in tensor_names:
if name not in weight_names:
raise ValueError(f'Requested tensor name filter "{name}" not present in the model graph')
# Apply filter.
tensor_set = set(tensor_names)
return [named_param for named_param in self.named_parameters() if named_param[0] in tensor_set]
def unskip(self):
"""Converts all skipped features into their fully encoded versions."""
@abstractmethod
def save(self, save_path: str):
"""Saves the model to the given path."""
@abstractmethod
def load(self, save_path: str):
"""Loads the model from the given path."""
@abstractmethod
def get_args(self):
"""Returns init arguments for constructing this model."""
@contextlib.contextmanager
def use_generation_config(self, generation_config: dict[str, Any]):
if generation_config is not None:
raise NotImplementedError(f"{self.__class__.__name__} does not support generation_config. ")
yield
def _activate_forward_hooks(self):
"""Activates/registers forward hooks for the model."""
def _deactivate_forward_hooks(self) -> None:
"""Deactivates/de-registers forward hooks for the model (if needed)."""
for handle in self._forward_hook_handles:
handle.deactivate_hook()
def create_input_feature(feature_config: BaseInputFeatureConfig, encoder_obj: Encoder | None) -> InputFeature:
input_feature_cls = get_from_registry(feature_config.type, get_input_type_registry())
input_feature = input_feature_cls(feature_config, encoder_obj=encoder_obj)
if not feature_config.encoder.skip:
return input_feature
return create_passthrough_input_feature(input_feature, feature_config)