Source code for pretrain.models.nuclr.loss
from __future__ import annotations
import numpy as np
import torch
import torch.nn.functional as F
from torch import Tensor, nn
from core.utils.distributed import all_reduce
NEG_INF = float("-inf")
N_VIEWS = 2
[docs]
class NuCLRLoss(nn.Module):
def __init__(
self,
dim_in: int,
tau: float,
dcl: bool,
projector: bool = True,
full_denom: bool = False,
):
super().__init__()
self.tau: float = tau
self.dcl = dcl
self.full_denom = full_denom
self.tau_inv = 1.0 / self.tau
if projector:
self.projector = nn.Sequential(
nn.LayerNorm(dim_in),
nn.Linear(dim_in, dim_in),
nn.ReLU(),
nn.Linear(dim_in, dim_in),
)
else:
self.projector = nn.Identity()
[docs]
def forward(
self,
x: Tensor,
seqlen: Tensor,
unit_ids: np.ndarray,
probe_ids: np.ndarray,
prefix: str = "",
) -> tuple[Tensor, dict]:
dev = x.device
x = F.normalize(self.projector(x), dim=-1)
# Convert uids/pids into unique indices
# it is faster to create masks on gpu using these
# rather than using the raw string arrays
uidx = torch.tensor(np.unique(unit_ids, return_inverse=True)[1], device=dev)
pidx = torch.tensor(np.unique(probe_ids, return_inverse=True)[1], device=dev)
# seqlen here = num neurons in all views for each sample
seqlen = seqlen.view(-1, N_VIEWS).sum(1)
bs = len(seqlen)
loss_numers, loss_denoms = x.new_zeros(bs), x.new_zeros(bs)
num_matches = x.new_zeros(bs, dtype=torch.long)
ptr: int = 0
for b in range(bs):
n = seqlen[b] # num neurons in all views
_x = x[ptr : ptr + n] # all views concatenated
_pidx = pidx[ptr : ptr + n]
_uidx = uidx[ptr : ptr + n]
sim = _x @ _x.T * self.tau_inv
# -- NUMERATOR --
match = _uidx[:, None] == _uidx[None, :]
match.fill_diagonal_(False)
num_matches[b] = match.sum()
if num_matches[b] == 0:
continue
loss_numers[b] = -sim[match].mean()
assert loss_numers[b].isfinite().all()
# -- DENOMENATOR --
if self.dcl:
# No positive-pairs in the denominator
sim[match] = NEG_INF
# No repetitions in the denominator
sim.fill_diagonal_(NEG_INF)
# No negatives across probes
sim[_pidx[:, None] != _pidx[None, :]] = NEG_INF
sim_lse = sim.logsumexp(1)
loss_denoms[b] = sim_lse[sim_lse.isfinite()].mean()
if not (loss_numers[b].isfinite().all() and loss_denoms[b].isfinite().all()):
loss_numers[b] = 0
loss_denoms[b] = 0
num_matches[b] = 0
ptr += n
# Compute loss weight as num_pos_pairs_in_view / total_pos_pairs_in_all_views
total_matches = all_reduce(num_matches.sum())
loss_weights = num_matches / total_matches
# Compute weighted-averaged loss
loss_numer = all_reduce((loss_numers * loss_weights).sum())
loss_denom = all_reduce((loss_denoms * loss_weights).sum())
loss = loss_numer + loss_denom
log_dict = {
f"{prefix}loss_numer": loss_numer.item(),
f"{prefix}loss_denom": loss_denom.item(),
f"{prefix}loss": loss.item(),
}
return loss, log_dict