import torch
import torch.nn as nn
import torch.nn.functional as F
from torch import Tensor
print('PyTorch version:', torch.__version__)PyTorch version: 2.13.0+cpu
(N, C, H, W) Inputs Grouped?num_groups=1: All Channels Belong to One Groupnum_groups=C: Each Channel Forms Its Own Grouptrain() and eval()num_groups Be Chosen?jshn9515
2026-06-27
2026-06-27
In the previous section, we introduced Instance Normalization. For image features with shape (N, C, H, W), InstanceNorm fixes the sample and channel and computes the mean and variance only over the spatial dimensions H and W. It does not depend on other samples in the batch, so even when the batch size is small it does not suffer from the statistical instability of BatchNorm.
However, InstanceNorm introduces a new problem: every channel is normalized completely independently, so different channels no longer share statistical information. This property is often useful for style transfer and image generation, but for visual tasks such as object detection and semantic segmentation, we may want to preserve some connections between channels while still avoiding dependence on the batch.
Group Normalization (GroupNorm) (Wu and He 2018) is a compromise between these two goals. It divides the channels into several groups and, within each sample, computes the mean and variance jointly over the channels and spatial positions in the same group.
Therefore, the key to understanding GroupNorm is still the statistical dimensions:
num_groups=1 and num_groups=C correspond to, respectively?We begin with a small tensor, gradually build the statistical perspective of GroupNorm, and implement a version corresponding to nn.GroupNorm from scratch.
PyTorch version: 2.13.0+cpu
For an input (N, C, H, W), BatchNorm fixes the channel C and computes statistics over the batch and spatial dimensions N, H, W. It usually works well when the batch is large enough, but when memory constraints allow only a small number of samples per device, the batch statistics become unstable. This is especially common in object detection and semantic segmentation. High-resolution images consume a large amount of memory, so the batch size on a single GPU is often small, sometimes only 1 or 2. InstanceNorm avoids this problem because it never computes statistics across samples. But it handles every channel separately, which may remove too much channel-level information.
The idea of GroupNorm is:
Do not compute statistics across samples, but also do not separate every channel completely. Instead, put several channels into a group and share statistics within the group.
Suppose the input has 8 channels. We can divide them into 4 groups, with 2 channels in each group:
Group 1: channel 0, channel 1
Group 2: channel 2, channel 3
Group 3: channel 4, channel 5
Group 4: channel 6, channel 7
Each sample independently performs this grouping and normalization, so other samples in the batch do not affect the output of the current sample.
(N, C, H, W) Inputs Grouped?Let the input be:
\[ X\in\mathbb{R}^{N\times C\times H\times W} \]
GroupNorm uses G groups and requires:
\[ C \bmod G = 0 \]
In other words, the number of channels must be divisible by the number of groups. The number of channels in each group is:
\[ C_g=\frac{C}{G} \]
In implementation, we can first reshape the input from (N, C, H, W) to (N, G, C/G, H, W). The second dimension then corresponds to the group, and the third dimension corresponds to the channels within the group.
Original shape: torch.Size([2, 8, 2, 2])
Grouped shape: torch.Size([2, 4, 2, 2, 2])
For each sample and group, GroupNorm computes statistics over the channels within the group and the spatial dimensions, namely over:
as the reduction dimensions.
Therefore, the mean and variance have shape (N, G, 1, 1, 1).
Mean shape: torch.Size([2, 4, 1, 1, 1])
Variance shape: torch.Size([2, 4, 1, 1, 1])
The rule of GroupNorm can be summarized in one sentence:
For an
(N, C, H, W)input, GroupNorm fixes the sample and group, and normalizes over the channels within the group and the spatial dimensions.
For the \(n\)-th sample and the \(g\)-th group, let the set of elements in the group be \(S_{n,g}\), with cardinality:
\[ |S_{n,g}|=\frac{C}{G}HW \]
The within-group mean is:
\[ \mu_{n,g} = \frac{1}{|S_{n,g}|} \sum_{i\in S_{n,g}}x_i \]
The within-group variance is:
\[ \sigma_{n,g}^2 = \frac{1}{|S_{n,g}|} \sum_{i\in S_{n,g}}(x_i-\mu_{n,g})^2 \]
Then standardize every element within the group:
\[ \hat{x}_i = \frac{x_i-\mu_{n,g}}{\sqrt{\sigma_{n,g}^2+\epsilon}} \]
Finally, like other normalization layers, GroupNorm uses learnable parameters to restore expressive power:
\[ y_{n,c,h,w} = \gamma_c\hat{x}_{n,c,h,w}+\beta_c \]
Note that although the statistics are computed per group, the learnable parameters are still one set per channel:
\[ \gamma,\beta\in\mathbb{R}^{C} \]
Weight shape: torch.Size([8])
Bias shape: torch.Size([8])
Thus, channels within the same group share a mean and variance, but they can still learn different scaling and shifting through different weight and bias values.
Below, we first omit the learnable affine transformation and manually perform grouping, standardization, and reshaping.
num_groups = 4
x = torch.randn(2, 8, 4, 4)
n, c, h, w = x.size()
dim = (2, 3, 4)
x_grouped = x.reshape(n, num_groups, c // num_groups, h, w)
mean = x_grouped.mean(dim, keepdim=True)
var = x_grouped.var(dim, correction=0, keepdim=True)
x_hat_grouped = (x_grouped - mean) / (var + 1e-5).sqrt()
print('Output means by group:')
print(x_hat_grouped.mean(dim))
print('\nOutput variances by group:')
print(x_hat_grouped.var(dim, correction=0))Output means by group:
tensor([[ 3.7253e-09, 7.4506e-09, 6.2864e-09, 3.7253e-09],
[-4.4703e-08, -1.1176e-08, -1.4901e-08, -2.9802e-08]])
Output variances by group:
tensor([[1.0000, 1.0000, 1.0000, 1.0000],
[1.0000, 1.0000, 1.0000, 1.0000]])
Each sample and group obtains a mean close to 0 and a variance close to 1. To avoid division by 0, we add an \(\epsilon\) to the variance.
GroupNorm does not compute statistics along the batch dimension, so changing the other samples in a batch does not change the output of the current sample.
sample = torch.randn(1, 8, 4, 4)
other_sample1 = torch.randn(1, 8, 4, 4)
other_sample2 = torch.randn(1, 8, 4, 4) * 100.0 + 500.0
batch1 = torch.concat([sample, other_sample1], dim=0)
batch2 = torch.concat([sample, other_sample2], dim=0)
group_norm = nn.GroupNorm(4, 8, affine=False)
output1 = group_norm(batch1)[0]
output2 = group_norm(batch2)[0]
max_diff = (output1 - output2).abs().max()
print('Maximum difference for the fixed sample:', max_diff.item())Maximum difference for the fixed sample: 0.0
Because the first sample is exactly the same, its GroupNorm output is also exactly the same. This differs from BatchNorm. BatchNorm computes statistics over all samples in the same channel within the batch, so when the other samples change, the mean and variance used by the current sample also change.
This property of GroupNorm means that:
num_groups=1: All Channels Belong to One GroupWhen \(G=1\), all channels are placed in one group. For an (N, C, H, W) input, statistics are computed jointly over C, H, and W. In terms of statistical dimensions, this is similar to nn.LayerNorm((C, H, W)). Both compute the mean and variance over the entire (C, H, W) for each sample.
Maximum difference: 0.0
When the affine transformations are disabled, the two standardization results are the same, but the default affine parameter shapes are different:
GroupNorm(1, C) is (C,);LayerNorm((C, H, W)) is (C, H, W).Therefore, the more precise statement is:
When
num_groups=1, GroupNorm’s statistical procedure is equivalent to applying LayerNorm to(C, H, W), but the default affine parameterization is different.
num_groups=C: Each Channel Forms Its Own GroupWhen \(G=C\), each group contains only one channel. GroupNorm then fixes the sample and channel and computes statistics over the spatial dimensions H and W. This is exactly the statistical procedure of InstanceNorm.
Maximum difference: 2.384185791015625e-07
The outputs are the same because both methods independently compute spatial statistics for each sample and channel.
Therefore, GroupNorm can be viewed as a bridge between LayerNorm and InstanceNorm:
This is also the origin of the name GroupNorm: the number of groups determines the granularity at which channels share statistical information.
Below, we implement a function-form GroupNorm. This implementation supports arbitrary spatial dimensions, not only two-dimensional images.
def group_norm(
x: Tensor,
num_groups: int,
weight: Tensor | None = None,
bias: Tensor | None = None,
eps: float = 1e-5,
) -> Tensor:
"""Apply group normalization to an input tensor."""
if x.ndim < 2:
raise AssertionError(
f'Expected input tensor to have at least 2 dimensions, but got {x.ndim}.'
)
if num_groups <= 0:
raise AssertionError(
f'Expected `num_groups` to be greater than 0, but got {num_groups}.'
)
num_channels = x.size(1)
channels_per_group = num_channels // num_groups
if num_channels % num_groups != 0:
raise AssertionError(
f'Expected the number of channels ({num_channels}) to be divisible '
f'by `num_groups` ({num_groups}).'
)
# (N, C, H, W) -> (N, G, C // G, H, W)
grouped_shape = (x.size(0), num_groups, channels_per_group, *x.shape[2:])
grouped_x = x.reshape(grouped_shape)
# Reduce over the channels in each group and all spatial dimensions.
# (N, G, C // G, H, W) -> reduce_dims = (2, 3, 4)
reduce_dims = tuple(range(2, grouped_x.ndim))
group_mean = grouped_x.mean(dim=reduce_dims, keepdim=True)
group_var = grouped_x.var(dim=reduce_dims, correction=0, keepdim=True)
grouped_y = (grouped_x - group_mean) * (group_var + eps).rsqrt()
y = grouped_y.reshape_as(x)
# (C,) -> (1, C, 1, 1)
broadcast_shape = (1, num_channels) + (1,) * (x.ndim - 2)
if weight is not None:
y = y * weight.reshape(broadcast_shape)
if bias is not None:
y = y + bias.reshape(broadcast_shape)
return yNow compare it with F.group_norm:
Maximum difference: 4.76837158203125e-07
The difference between the two should come only from the order of floating-point computations.
The three core steps of the implementation are:
(N, C, ...) to (N, G, C/G, ...);Next, wrap the function in a module.
class GroupNorm(nn.Module):
"""Apply group normalization over channel groups."""
weight: Tensor | None
bias: Tensor | None
def __init__(
self,
num_groups: int,
num_channels: int,
eps: float = 1e-5,
affine: bool = True,
):
"""Initialize a group normalization module."""
super().__init__()
if num_channels % num_groups != 0:
raise AssertionError('`num_channels` must be divisible by `num_groups`.')
self.num_groups = num_groups
self.num_channels = num_channels
self.eps = eps
self.affine = affine
if affine:
self.weight = nn.Parameter(torch.empty(num_channels))
self.bias = nn.Parameter(torch.empty(num_channels))
else:
self.register_parameter('weight', None)
self.register_parameter('bias', None)
self.reset_parameters()
def reset_parameters(self) -> None:
if self.weight is not None:
nn.init.ones_(self.weight)
if self.bias is not None:
nn.init.zeros_(self.bias)
def forward(self, x: Tensor) -> Tensor:
if x.size(1) != self.num_channels:
raise AssertionError(
f'Expected {self.num_channels} channels, but got {x.size(1)} channels.'
)
return group_norm(
x,
self.num_groups,
weight=self.weight,
bias=self.bias,
eps=self.eps,
)
def extra_repr(self) -> str:
return (
f'{self.num_groups}, {self.num_channels}, eps={self.eps}, '
f'affine={self.affine}'
)Copy the parameters into the PyTorch module and compare the outputs:
x = torch.randn(2, 8, 4, 4)
group_norm1 = GroupNorm(4, 8)
group_norm2 = nn.GroupNorm(4, 8)
with torch.no_grad():
group_norm2.weight.copy_(group_norm1.weight)
group_norm2.bias.copy_(group_norm1.bias)
actual = group_norm1(x)
expected = group_norm2(x)
max_diff = (actual - expected).abs().max()
print('Maximum difference:', max_diff.item())Maximum difference: 2.384185791015625e-07
The result should differ only by the error caused by the order of floating-point computations.
Like LayerNorm, the same nn.GroupNorm can process (N, C, *) as long as the second dimension is the channel dimension.
1D output shape: torch.Size([2, 4, 10])
2D output shape: torch.Size([2, 4, 8, 8])
3D output shape: torch.Size([2, 4, 4, 8, 8])
For these inputs, GroupNorm always:
num_groups groups;Therefore, there is no classification into GroupNorm1d, GroupNorm2d, and GroupNorm3d.
train() and eval()GroupNorm does not maintain running_mean or running_var. During both training and inference, it uses the group statistics of the current sample. Therefore, train() and eval() do not change how GroupNorm computes its output.
x = torch.randn(2, 8, 4, 4)
group_norm = nn.GroupNorm(4, 8)
group_norm.train()
train_output = group_norm(x)
group_norm.eval()
eval_output = group_norm(x)
max_diff = (train_output - eval_output).abs().max()
print('Maximum difference:', max_diff.item())
print('State dict keys:', dict(group_norm.state_dict()))Maximum difference: 0.0
State dict keys: {'weight': tensor([1., 1., 1., 1., 1., 1., 1., 1.]), 'bias': tensor([0., 0., 0., 0., 0., 0., 0., 0.])}
The state_dict contains only the learnable weight and bias, and does not contain BatchNorm’s:
running_mean;running_var;num_batches_tracked.This also means that, unlike BatchNorm, GroupNorm does not require running statistics to have been correctly estimated before inference.
num_groups Be Chosen?The most important hyperparameter of GroupNorm is num_groups. It must satisfy:
\[ C \bmod G = 0 \]
The fewer groups there are, the more channels each group contains and the more channels share statistics; the more groups there are, the fewer channels each group contains and the closer the normalization behavior is to InstanceNorm.
The original GroupNorm work commonly used 32 groups, but this is not a universally optimal rule for every network. When the number of channels is small, choose a number of groups that divides the number of channels and does not make each group too small.
For example:
Each of these gives 8 channels per group.
Therefore, when choosing num_groups, it is more important to consider how many channels each group contains and whether the channel counts of different layers are divisible by the number of groups.
In CNNs, GroupNorm is usually placed after the convolutional layer and before the activation function:
Because GroupNorm includes a learnable bias, the convolutional layer immediately before it can usually be configured as:
The reason is similar to the Conv-BN structure: the constant bias produced by the convolution is subtracted during standardization, and then GroupNorm’s bias performs the shift.
However, GroupNorm usually cannot be fused into a convolution as simply as BatchNorm during inference. BatchNorm uses fixed running statistics in eval() mode, so the overall operation is a fixed affine transformation. GroupNorm still needs to dynamically compute the mean and variance from the current input during inference, so it cannot be absorbed into the convolution weights in advance.
The most typical use cases for GroupNorm are small-batch vision tasks, such as:
Its main advantages include:
However, GroupNorm is not the universally optimal default for every task.
When the batch is sufficiently large and the training and inference data distributions are similar, BatchNorm is still a highly effective choice for CNNs. Moreover, BatchNorm’s cross-sample statistics introduce some random perturbation, which may provide an additional regularization effect. GroupNorm also requires manually choosing num_groups. The number of channels may differ across layers, so we must ensure that the channel count of every layer is divisible by the number of groups. Finally, GroupNorm still needs to compute the mean and variance of the current input during inference, so it cannot perform a simple Conv-BN Fusion like BatchNorm.
This section introduced Group Normalization.
For an input (N, C, H, W), GroupNorm first divides the channels into G groups and reshapes the input as:
\[ \left(N, G, \frac{C}{G}, H, W \right) \]
It then fixes the sample and group and computes the mean and variance over the channels within the group and the spatial dimensions.
Its most important characteristics are:
num_groups=1, its statistical procedure is close to LayerNorm;num_groups=C, its statistical procedure is equivalent to InstanceNorm;At this point, we have introduced BatchNorm, LayerNorm, InstanceNorm, and GroupNorm separately. They all first compute the mean and variance, and then perform centering and scale normalization. What truly differs is which elements are placed into the same group to share statistics.
However, not every normalization method uses both the mean and variance. RMSNorm has also become a very common choice in modern Transformers and large language models. It no longer subtracts the mean, but controls the overall scale of hidden vectors using only the root mean square of the features.
In the next section, we introduce Root Mean Square Normalization (Zhang and Sennrich 2019) and compare its relationship with and difference from LayerNorm. After that, we will place BatchNorm, LayerNorm, InstanceNorm, GroupNorm, and RMSNorm into a unified framework and compare them again from the perspective of “which statistics are computed over which dimensions.”