Skip to content

Utilities Module

Helper functions and utilities for leech.

Overview

The util module provides various helper functions for model loading, metrics computation, and more.

Model Loading

load_model_from_checkpoint

load_model_from_checkpoint(checkpoint_path: Path, device: str = 'cuda', checkpoint_name: str = 'model_best.pt') -> tuple[nn.Module, dict]

Load a trained model from checkpoint directory.

Parameters:

Name Type Description Default
checkpoint_path Path

Path to checkpoint directory (contains config.json and .pt files)

required
device str

Device to load model on

'cuda'
checkpoint_name str

Name of checkpoint file (default: model_best.pt)

'model_best.pt'

Returns:

Type Description
tuple[Module, dict]

Tuple of (model, config_dict)

Raises:

Type Description
FileNotFoundError

If config.json or checkpoint file not found

ValueError

If config is invalid

Source code in src/leech/model_loading.py
def load_model_from_checkpoint(
    checkpoint_path: Path, device: str = "cuda", checkpoint_name: str = "model_best.pt"
) -> tuple[nn.Module, dict]:
    """
    Load a trained model from checkpoint directory.

    Args:
        checkpoint_path: Path to checkpoint directory (contains config.json and .pt files)
        device: Device to load model on
        checkpoint_name: Name of checkpoint file (default: model_best.pt)

    Returns:
        Tuple of (model, config_dict)

    Raises:
        FileNotFoundError: If config.json or checkpoint file not found
        ValueError: If config is invalid
    """
    checkpoint_path = Path(checkpoint_path)

    # Load config
    config_file = checkpoint_path / "config.json"
    if not config_file.exists():
        raise FileNotFoundError(f"Config file not found: {config_file}")

    with open(config_file) as f:
        config = json.load(f)

    # Create model from config (filters training params and validates constructor args)
    model = _instantiate_model(config)

    # Load checkpoint
    checkpoint_file = checkpoint_path / checkpoint_name
    if not checkpoint_file.exists():
        raise FileNotFoundError(f"Checkpoint file not found: {checkpoint_file}")

    checkpoint = torch.load(checkpoint_file, map_location="cpu", weights_only=False)

    # Strip _orig_mod. prefix added by torch.compile()
    state_dict = checkpoint["model_state_dict"]
    state_dict = {k.removeprefix("_orig_mod."): v for k, v in state_dict.items()}
    state_dict = _migrate_state_dict_keys(state_dict)
    model.load_state_dict(state_dict)
    model = model.to(device)
    model.eval()

    # Pass CL regression head state dict through config for inference setup
    if checkpoint.get("cl_regression_head_state_dict") is not None:
        config["cl_regression"] = True
        config["cl_regression_head_state_dict"] = checkpoint["cl_regression_head_state_dict"]

    return model, config

Metrics Computation

compute_metrics

compute_metrics(y_true: ndarray, y_pred: ndarray, y_prob: ndarray) -> dict

Compute classification metrics.

Parameters:

Name Type Description Default
y_true ndarray

True labels (binary)

required
y_pred ndarray

Predicted labels (binary)

required
y_prob ndarray

Predicted probabilities (0-1)

required

Returns:

Type Description
dict

Dictionary with metrics:

dict
  • accuracy: Overall accuracy
dict
  • precision: Precision score
dict
  • recall: Recall score
dict
  • f1: F1 score
dict
  • auroc: ROC AUC score (area under receiver operating characteristic curve)
dict
  • auprc: Average precision score (area under precision-recall curve)
dict
  • confusion_matrix: 2x2 confusion matrix as list

Raises:

Type Description
ValueError

If input arrays are empty or have mismatched lengths

Source code in src/leech/metrics.py
def compute_metrics(y_true: np.ndarray, y_pred: np.ndarray, y_prob: np.ndarray) -> dict:
    """
    Compute classification metrics.

    Args:
        y_true: True labels (binary)
        y_pred: Predicted labels (binary)
        y_prob: Predicted probabilities (0-1)

    Returns:
        Dictionary with metrics:
        - accuracy: Overall accuracy
        - precision: Precision score
        - recall: Recall score
        - f1: F1 score
        - auroc: ROC AUC score (area under receiver operating characteristic curve)
        - auprc: Average precision score (area under precision-recall curve)
        - confusion_matrix: 2x2 confusion matrix as list

    Raises:
        ValueError: If input arrays are empty or have mismatched lengths
    """
    # Validate inputs
    if len(y_true) == 0 or len(y_pred) == 0 or len(y_prob) == 0:
        raise ValueError("Cannot compute metrics on empty arrays")

    if len(y_true) != len(y_pred) or len(y_true) != len(y_prob):
        raise ValueError(
            f"Array length mismatch: y_true={len(y_true)}, y_pred={len(y_pred)}, y_prob={len(y_prob)}"
        )

    metrics = {
        "accuracy": float(accuracy_score(y_true, y_pred)),
        "precision": float(precision_score(y_true, y_pred, zero_division=0)),
        "recall": float(recall_score(y_true, y_pred, zero_division=0)),
        "f1": float(f1_score(y_true, y_pred, zero_division=0)),
    }

    # AUROC and AUPRC only if we have both classes
    if len(np.unique(y_true)) > 1:
        metrics["auroc"] = float(roc_auc_score(y_true, y_prob))
        metrics["auprc"] = float(average_precision_score(y_true, y_prob))
        # The metrics above this line are all read at one fixed threshold.
        metrics["threshold_sweep"] = sweep_thresholds(y_true, y_prob)
    else:
        metrics["auroc"] = 0.0
        metrics["auprc"] = 0.0

    # Confusion matrix
    cm = confusion_matrix(y_true, y_pred, labels=[0, 1])
    metrics["confusion_matrix"] = cm.tolist()

    return metrics

save_metrics

save_metrics(metrics: dict, output_path: Path) -> None

Save metrics to JSON file.

Parameters:

Name Type Description Default
metrics dict

Dictionary of metrics

required
output_path Path

Output file path

required
Source code in src/leech/metrics.py
def save_metrics(metrics: dict, output_path: Path) -> None:
    """
    Save metrics to JSON file.

    Args:
        metrics: Dictionary of metrics
        output_path: Output file path
    """
    output_path.parent.mkdir(parents=True, exist_ok=True)

    with open(output_path, "w") as f:
        json.dump(metrics, f, indent=2)

    logger.info(f"Metrics saved to {output_path}")

print_metrics

print_metrics(metrics: dict) -> None

Pretty print metrics to console using Rich tables.

Parameters:

Name Type Description Default
metrics dict

Dictionary of metrics

required
Source code in src/leech/metrics.py
def print_metrics(metrics: dict) -> None:
    """
    Pretty print metrics to console using Rich tables.

    Args:
        metrics: Dictionary of metrics
    """
    # Main metrics table
    table = Table(title="Evaluation Metrics", show_header=True, header_style="bold magenta")
    table.add_column("Metric", style="cyan", width=20)
    table.add_column("Value", justify="right", style="green", width=15)

    table.add_row("Accuracy", f"{metrics['accuracy']:.4f}")
    table.add_row("Precision", f"{metrics['precision']:.4f}")
    table.add_row("Recall", f"{metrics['recall']:.4f}")
    table.add_row("F1 Score", f"{metrics['f1']:.4f}")

    # Handle both old (auc) and new (auroc) formats
    if "auroc" in metrics:
        table.add_row("AUROC", f"{metrics['auroc']:.4f}")
        if "auprc" in metrics:
            table.add_row("AUPRC", f"{metrics['auprc']:.4f}")
    elif "auc" in metrics:
        # Backward compatibility with old format
        table.add_row("ROC AUC", f"{metrics['auc']:.4f}")

    console.print(table)

    # Confusion matrix table
    if "confusion_matrix" in metrics:
        cm = metrics["confusion_matrix"]
        cm_table = Table(title="Confusion Matrix", show_header=True, header_style="bold magenta")
        cm_table.add_column("", style="cyan", width=10)
        cm_table.add_column("Predicted Neg", justify="right", style="yellow", width=15)
        cm_table.add_column("Predicted Pos", justify="right", style="yellow", width=15)

        cm_table.add_row("Actual Neg", str(cm[0][0]), str(cm[0][1]))
        cm_table.add_row("Actual Pos", str(cm[1][0]), str(cm[1][1]))

        console.print(cm_table)

Reproducibility

setup_random_seed

setup_random_seed(seed: int | None, output_dir: Path | None = None) -> int

Setup random seed for reproducibility and optionally save to file.

Parameters:

Name Type Description Default
seed int | None

Random seed value, or None to generate one

required
output_dir Path | None

Directory to save seed.txt file, or None to skip saving

None

Returns:

Type Description
int

The seed value used

Source code in src/leech/model_loading.py
def setup_random_seed(seed: int | None, output_dir: Path | None = None) -> int:
    """Setup random seed for reproducibility and optionally save to file.

    Args:
        seed: Random seed value, or None to generate one
        output_dir: Directory to save seed.txt file, or None to skip saving

    Returns:
        The seed value used
    """
    # Generate if needed
    if seed is None:
        seed = generate_random_seed()
        logger.info(f"Generated random seed: {seed}")
    else:
        logger.info(f"Using provided seed: {seed}")

    # Set for all libraries
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    if torch.cuda.is_available():
        torch.cuda.manual_seed(seed)
        torch.cuda.manual_seed_all(seed)

    # Save if requested
    if output_dir is not None:
        output_dir.mkdir(parents=True, exist_ok=True)
        seed_file = output_dir / "seed.txt"
        with open(seed_file, "w") as f:
            f.write(f"{seed}\n")
        logger.info(f"Saved seed to {seed_file}")

    return seed

Example Usage

Python
from leech.model_loading import load_model_from_checkpoint, setup_random_seed
from pathlib import Path

# Set random seed for reproducibility
seed = setup_random_seed(42, output_dir=Path("models/"))

# Load model
model = load_model_from_checkpoint(
    checkpoint_path=Path("models/model_best.pt"),
    device="cuda"
)

# Model is now ready for inference
predictions = model(input_data)