Residual update: rezero_residual
Role
Residual connections combine a layer’s update with its input, \(\bm h'=\bm h+\Delta\bm h\). They make deep networks trainable and let a layer learn a correction rather than a replacement. In a pipeline the old and new states are separate slots, so the residual is an explicit stage.
Built-in members of this kind: node_update_residual and
edge_update_residual of the MANDALA blocks, the scaled residuals of the
res* E3MLP variants, irrep_residual (with a projection when layouts
differ) and add.
The new layer
ReZero multiplies the update by a learned scalar initialized at zero. The equivariant version learns one coefficient per irrep copy:
At initialization every ReZero block is the identity, so a network of any depth starts as a shallow one and the signal propagates undistorted; each copy then learns how much of the update to admit. Per-copy coefficients let the network open scalar channels early and higher-degree channels later.
Symmetry. \(\alpha_c\) multiplies whole copies (checklist 3c) and both inputs must have identical layouts, which the factory enforces.
Implementation
class ReZeroResidual(nn.Module):
r"""``h' = h + alpha_c * Delta h`` with ``alpha_c`` initialized at zero.
At initialization the block is the identity map, so arbitrarily deep stacks
start out well conditioned; each copy learns how much update to admit.
"""
def __init__(self, irreps, initial=0.0):
super().__init__()
self.irreps = o3.Irreps(irreps)
self.alpha = nn.Parameter(torch.full((self.irreps.num_irreps,), float(initial)))
def forward(self, old, update):
return old + expand_per_copy(self.alpha, self.irreps) * update
@register_transform("rezero_residual")
def _rezero_residual(options, inputs, mapper):
old, update = inputs
if old.domain != update.domain or global_irreps(old) != global_irreps(update):
raise ValueError("rezero_residual requires identical feature layouts")
require_same_support(old, update)
return ReZeroResidual(global_irreps(old), **options), [old]
Usage
- id: residual_2
transform: rezero_residual
inputs: [node_1, update_2]
outputs: [node_2]
options: {initial: 0.0}
Because slots are write-once, node_1 remains available after this stage;
long skip connections across several layers are written the same way.
Variations
Make \(\alpha\) a function of invariants of \(\bm h\) (a highway gate) so the mixing adapts to the local environment.
Initialize at a small positive value (LayerScale) instead of zero when the update is already well scaled.