Source code for ts3.models.supervised.lolcat.lolcat

import itertools

import torch
import torch.nn as nn


class MLP(nn.Module):
    """Multi-layer perceptron with optional batchnorm and dropout.

    Args:
        hidden_layers: list of dims from input to output. Use -1 for lazy first layer.
        batchnorm: insert BatchNorm1d after each linear layer.
        drop_last_nonlin: remove the ReLU/batchnorm/dropout of the last block.
        dropout: dropout probability applied after the ReLU in each block.
    """

    def __init__(
        self,
        hidden_layers: list[int],
        *,
        bias: bool,
        batchnorm: bool,
        dropout: float,
        drop_last_nonlin: bool = True,
    ):
        super().__init__()
        layers = []
        for in_dim, out_dim in itertools.pairwise(hidden_layers):
            if in_dim == -1:
                layers.append(nn.LazyLinear(out_dim, bias=bias))
            else:
                layers.append(nn.Linear(in_dim, out_dim, bias=bias))
            if batchnorm:
                layers.append(nn.BatchNorm1d(out_dim))
            layers.append(nn.ReLU(inplace=True))
            if dropout > 0.0:
                layers.append(nn.Dropout(dropout))

        if drop_last_nonlin:
            layers = layers[: -(1 + int(batchnorm) + int(dropout > 0.0))]

        self.layers = nn.Sequential(*layers)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.layers(x)


[docs] class MultiHeadGlobalAttention(nn.Module): """Multi-Head Global pooling layer.""" def __init__( self, in_channels: int, out_channels: int, heads: int = 1, *, bias: bool, batchnorm: bool, dropout: float, ): super().__init__() self.heads = heads self.out_channels = out_channels self.gate_nn = nn.Sequential( nn.Linear(in_channels, heads * in_channels, bias=False), nn.PReLU(), nn.Linear(heads * in_channels, heads, bias=False), ) self.proj = MLP( [in_channels, out_channels * heads, out_channels * heads], bias=bias, batchnorm=batchnorm, dropout=dropout, )
[docs] def forward(self, x: torch.Tensor, batch: torch.Tensor) -> torch.Tensor: n_units = int(batch.max().item()) + 1 gate = self.gate_nn(x) # (N, heads) score = self._batched_softmax(gate, batch, n_units) # (N, heads) proj = self.proj(x).view(x.size(0), self.heads, self.out_channels) # (N, heads, C) score = score.unsqueeze(-1) # (N, heads, 1) weighted = score * proj # (N, heads, C) out = torch.zeros(n_units, self.heads, self.out_channels, dtype=x.dtype, device=x.device) idx = batch.view(-1, 1, 1).expand_as(weighted) out.scatter_add_(0, idx, weighted) # (B, heads, C) return out.view(n_units, -1) # (B, heads * C)
@staticmethod def _batched_softmax(gate: torch.Tensor, batch: torch.Tensor, n_units: int) -> torch.Tensor: """Softmax over nodes within each batch item (each unit).""" idx = batch.unsqueeze(1).expand_as(gate) src_max = torch.full( (n_units, gate.size(1)), float("-inf"), dtype=gate.dtype, device=gate.device ) src_max.scatter_reduce_(0, idx, gate.detach(), reduce="amax", include_self=True) out = (gate - src_max[batch]).exp() out_sum = torch.zeros(n_units, gate.size(1), dtype=gate.dtype, device=gate.device) out_sum.scatter_add_(0, idx, out) return out / (out_sum[batch] + 1e-16)
[docs] class LOLCAT(nn.Module): """ISI histogram encoder, attention pooling and a brain region classifier :cite:`lolcat`. Reference implementation: `nerdslab/lolcat <https://github.com/nerdslab/lolcat>`_. """ def __init__( self, n_classes: int, encoder_hidden: list[int], n_heads: int, attn_hidden: int, dropout: float, batchnorm: bool, bias: bool, n_bins: int, ): super().__init__() enc_out = encoder_hidden[-1] self.encoder = MLP( [n_bins, *encoder_hidden], bias=bias, batchnorm=batchnorm, dropout=dropout ) self.pool = MultiHeadGlobalAttention( enc_out, attn_hidden, heads=n_heads, bias=bias, batchnorm=batchnorm, dropout=dropout, ) self.classifier = MLP( [attn_hidden * n_heads, n_classes], bias=True, batchnorm=False, dropout=0.0, ) @staticmethod def collate(batch: list[dict]) -> tuple: x = torch.cat([item["isi_hists"] for item in batch], dim=0) batch_idx = torch.cat( [ torch.full((item["isi_hists"].size(0),), i, dtype=torch.long) for i, item in enumerate(batch) ] ) labels = torch.tensor([item["label"] for item in batch], dtype=torch.long) uids = [item["uid"] for item in batch] return x, batch_idx, labels, uids def link_datasets(self, train_ds, val_ds, test_ds) -> None: assert train_ds.label_map == val_ds.label_map == test_ds.label_map, ( "All splits must share the same label_map" )
[docs] def forward(self, x: torch.Tensor, batch: torch.Tensor) -> torch.Tensor: emb = self.encoder(x) global_emb = self.pool(emb, batch) return self.classifier(global_emb)