Single-cell and perturbations
Zero-inflated negative-binomial likelihood
Free account with email, Google or GitHub. Your code runs in an isolated PyTorch sandbox.
Your task
Combine a structural-zero component with a negative-binomial count model. Return observed-count log probabilities, per-cell negative log likelihood, and the posterior probability that each observed zero is structural.
Function signature
zero_inflated_negative_binomial_log_prob(
counts: torch.Tensor,
mean: torch.Tensor,
inverse_dispersion: torch.Tensor,
zero_inflation_logits: torch.Tensor,
observed_mask: torch.Tensor,
mean_floor: float = 0.0,
) -> dict[str, torch.Tensor]Worked example
Two routes to an observed zero
Zero logits assign probability one half to each mixture component. The NB component has zero probability one half, so total zero probability is 1/2+(1/2)(1/2)=3/4. Its structural-zero responsibility is (1/2)/(3/4)=2/3; positive counts have zero responsibility.
import torch
counts = torch.tensor([[0, 1, 0, 2]], dtype=torch.int64)
mean = torch.tensor([[1.0, 1.0, 1.0, 1.0]], dtype=torch.float64, requires_grad=True)
inverse_dispersion = torch.tensor([1.0, 1.0, 1.0, 1.0], dtype=torch.float64, requires_grad=True)
zero_inflation_logits = torch.tensor([[0.0, 0.0, 0.0, 0.0]], dtype=torch.float64, requires_grad=True)
observed_mask = torch.tensor([[True, True, True, True]], dtype=torch.bool)
mean_floor = 0.0
result = zero_inflated_negative_binomial_log_prob(counts=counts, mean=mean, inverse_dispersion=inverse_dispersion, zero_inflation_logits=zero_inflation_logits, observed_mask=observed_mask, mean_floor=mean_floor)Expected return
{
'log_prob': torch.tensor([[-0.2876820724517809, -2.0794415416798357, -0.2876820724517809, -2.772588722239781]], dtype=torch.float64),
'cell_nll': torch.tensor([5.4273944088231785], dtype=torch.float64),
'structural_zero_posterior': torch.tensor([[0.6666666666666666, 0.0, 0.6666666666666666, 0.0]], dtype=torch.float64),
}Parameters
| Parameter | Shape and meaning |
|---|---|
countstorch.Tensor | [B, G]Observed integer counts; finite payloads at unobserved entries are ignored. |
meantorch.Tensor | [B, G]Raw NB means; differentiable, nonnegative on observed entries and \operatorname{mean}+\mathrm{mean\_floor}>0. |
inverse_dispersiontorch.Tensor | [G] or [B, G]Positive NB inverse dispersion at observed entries; gene-only values broadcast across cells. |
zero_inflation_logitstorch.Tensor | [B, G]Decoder logits for a structural zero; differentiable. |
observed_masktorch.Tensor | [B, G]True means observed, including measured zero. |
mean_floorfloatDefault 0.0 | Nonnegative value added to the supplied NB mean, not a clamp. Its default zero preserves the exact supplied distribution. |
Return
dict[str, torch.Tensor]
Return exactly the named fields applicable to this operation.
Required behavior
- Implement a normalized zero-inflated negative-binomial (ZINB) distribution in log space.
countsisint64[B,G];meanandzero_inflation_logitsare floating [B,G];inverse_dispersionis[G]or [B,G].observed_maskis bool [B,G],Truefor a measured count INCLUDING zero.B,G \ge 1. - All supplied tensors are finite. Observed
countsare nonnegative integers. Observed inverse dispersion\theta>0. Missing entries can contain finite sentinels and must not affect any output or local derivative. The onlykeyismean_floor, a finite nonnegative scalar defaulting to0. - Effective NB
meanm=\operatorname{mean}+\mathrm{mean\_floor}; raw observed means must be nonnegative andmstrictly positive. Thus with default floor the means must be positive; a positive floor permits rawmeanzero. The floor is ADDED to the NBmean, not used to clamp it or added independently to logarithms. - This preserves a normalized distribution and is an explicit teaching adaptation, not exact scvi-tools
\epsilonparity. Lett=\mathrm{zero\_inflation\_logits}and\pi=\operatorname{sigmoid}(t), the probability of the STRUCTURAL-ZERO component (not the nonzero-count probability). With NB inverse dispersion\theta: \log p_{\mathrm{NB}}(x)=\log\Gamma(x+\theta)-\log\Gamma(\theta)-\log\Gamma(x+1)+\theta\log\frac{\theta}{\theta+m}+x\log\frac{m}{\theta+m}. Ifx=0,\log p(0)=\log\left[\pi+(1-\pi)e^{\log p_{\mathrm{NB}}(0)}\right]. Otherwise\mathrm{log\_prob}=\operatorname{log}(1-\pi)+\mathrm{log\_NB}(x). Detect zero byx=0exactly.- For observed zero also return
\texttt{structural\_zero\_posterior}=\frac{\pi}{\pi+(1-\pi)p_{\mathrm{NB}}(0)}; this responsibility is zero at positivecounts.log_proband responsibility are exactly zero at missing entries.\mathrm{cell\_nll}[b]=-\sum_{g}log_prob[b,g], so entirely missing cells return zero. - Work in log space:
\operatorname{log}(\pi)=-\operatorname{softplus}(-t),\operatorname{log}(1-\pi)=-\operatorname{softplus}(t),a=\log m-\log\theta,\log p_{\mathrm{NB}}(0)=-\theta\operatorname{softplus}(a). - Compose stable softplus and two-term logsumexp from elementary operations; do not call sigmoid, softplus, logsumexp, distributions, or a ready-made ZINB implementation. Keep the derivative correct at
a=0andt=0as well as logits near\pm 1 000.torch.lgammais the only exceptional numerical operation. - Reject negative
mean_floor, negative observedcounts, negative raw means, nonpositive effective means, and nonpositive observed\thetawithValueError. Other shape/finiteness/integrality conditions are preconditions.
Why this matters
A zero can arise from either mixture component, so its probability is a sum. A positive count can arise only from the negative-binomial component. The reported zero responsibility is a model attribution, not proof of a biological cause.
Starter code
You write the body of this function in the BioTorch workspace. Your code runs in an isolated PyTorch sandbox; Run checks the public examples and Submit grades the full test suite, including gradients.
import torch
def zero_inflated_negative_binomial_log_prob(
counts: torch.Tensor, # (B, G)
mean: torch.Tensor, # (B, G)
inverse_dispersion: torch.Tensor, # (G,) or (B, G)
zero_inflation_logits: torch.Tensor, # (B, G)
observed_mask: torch.Tensor, # (B, G)
mean_floor: float = 0.0,
) -> dict[str, torch.Tensor]:
"""Return dict[str, torch.Tensor]:
log_prob: (B, G).
cell_nll: (B,).
structural_zero_posterior: (B, G)."""
# Implement the operation. Inputs and test setup are supplied by BioTorch.
raise NotImplementedError("Implement zero_inflated_negative_binomial_log_prob")
Ready to implement it?
3 progressive hints, a solution walkthrough, and grading that checks values, shapes and gradients are waiting in the workspace.
Free account with email, Google or GitHub. Your code runs in an isolated PyTorch sandbox.
Sources and further reading
A primitive exercise, not an end-to-end reproduction of the named models. No affiliation or endorsement is implied.