Add a component#

Every component is a registered class. Write it, import it from its package’s __init__.py, and name it in a config.

A model#

A top-level model receives the packed batch dict and returns a dict. In training it must return a scalar loss; in evaluation it returns the outputs its evaluator reads, such as seg_logits for SemSegEvaluator.

# pimm/models/my_segmentor.py
import torch.nn as nn

from pimm.models.builder import MODELS, build_model
from pimm.models.losses import build_criteria
from pimm.models.utils.structure import Point


@MODELS.register_module("MySegmentor")
class MySegmentor(nn.Module):
    def __init__(self, backbone, backbone_out_channels, num_classes, criteria):
        super().__init__()
        self.backbone = build_model(backbone)
        self.head = nn.Linear(backbone_out_channels, num_classes)
        self.criteria = build_criteria(criteria)

    def forward(self, input_dict):
        point = self.backbone(Point(input_dict))
        logits = self.head(point.feat)
        output = {}
        if "segment" in input_dict:
            output["loss"] = self.criteria(logits, input_dict["segment"])
        if not self.training:
            output["seg_logits"] = logits
        return output

This assumes the backbone returns features for every input point. If it pools points, follow how DefaultSegmentorV2 maps features back. Register the module:

# pimm/models/__init__.py
from .my_segmentor import MySegmentor

A loss#

build_criteria builds each entry of a criteria list from the LOSSES registry and sums their outputs. A new loss receives the arguments the model passes to self.criteria(...):

from pimm.models.losses.builder import LOSSES


@LOSSES.register_module()
class MyLoss(nn.Module):
    def __init__(self, loss_weight=1.0, ignore_index=-1):
        super().__init__()
        self.loss_weight = loss_weight
        self.ignore_index = ignore_index

    def forward(self, pred, target):
        ...
        return self.loss_weight * loss

Use it as criteria=[dict(type="MyLoss", loss_weight=0.5)], alongside existing losses if you like.

A transform#

import numpy as np

from pimm.datasets.transform import TRANSFORMS


@TRANSFORMS.register_module()
class RandomEnergyDropout:
    def __init__(self, ratio=0.1, p=0.5):
        self.ratio = float(ratio)
        self.p = float(p)

    def __call__(self, data_dict):
        if "energy" in data_dict and np.random.rand() < self.p:
            mask = np.random.rand(len(data_dict["energy"])) < self.ratio
            data_dict["energy"][mask] = 0.0
        return data_dict

Import it from pimm/datasets/transform/__init__.py and use dict(type="RandomEnergyDropout", ratio=0.1). Transforms run on NumPy arrays before ToTensor. A transform that changes the number or order of points must apply the same change to every key in index_valid_keys.

A hook#

from pimm.engines.hooks.builder import HOOKS
from pimm.engines.hooks.default import HookBase
from pimm.utils.comm import is_main_process


@HOOKS.register_module()
class MyScalarHook(HookBase):
    def __init__(self, every=100):
        self.every = int(every)

    def after_step(self):
        step = int(self.trainer.global_step)
        if step % self.every == 0 and is_main_process() and self.trainer.writer is not None:
            self.trainer.writer.add_scalar("my/value", 1.0, step)

Import it from pimm/engines/hooks/__init__.py and add it to hooks. Hooks run in list order. Guard file and logging side effects with is_main_process(), but let every process enter collective operations, or the other processes wait forever. Checkpoints don’t save hook attributes; derive counters from self.trainer.global_step.

A dataset#

See Bring your own data.