7.5 InstanceNorm:对样本的每个通道进行归一化

Author

jshn9515

Published

2026-06-27

Modified

2026-06-27

上一节我们介绍了 Layer Normalization。LayerNorm 不依赖 batch 中的其他样本,而是在每个样本自己的特征内部计算均值和方差。对于 Transformer 输入 (batch_size, sequence_length, hidden_size),它通常会对每个 token 的 hidden_size 维特征单独归一化。

这一节我们继续讨论另一种不依赖 batch 的归一化方法:Instance Normalization (InstanceNorm) (Ulyanov et al. 2017)

InstanceNorm 最常见于图像任务。对于输入形状为 (N, C, H, W) 的图像特征,它不会像 BatchNorm 那样把同一通道在整个 batch 中的元素放在一起统计,也不会像常见的 LayerNorm 那样把一个样本中的所有通道和空间位置放在一起。它会为每个样本、每个通道分别计算均值和方差,只在空间维度上进行归一化。

因此,InstanceNorm 最重要的问题仍然不是公式,而是统计维度:

这一节会从一个小型图像张量开始,逐步建立 InstanceNorm 的统计视角,并从零实现一个简化版的 InstanceNorm。

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.12.1+cpu

7.5.1 InstanceNorm 对哪些维度归一化

考虑一批二维图像特征:

\[ X\in\mathbb{R}^{N\times C\times H\times W} \]

其中,\(N\) 表示批量大小,\(C\) 表示通道数,\(H\)\(W\) 表示空间尺寸。

InstanceNorm 会固定样本索引 \(n\) 和通道索引 \(c\),只使用该通道内部的 \(H\times W\) 个空间位置计算均值和方差。对于第 \(n\) 个样本的第 \(c\) 个通道,均值为:

\[ \mu_{n,c} = \frac{1}{HW} \sum_{h=1}^{H} \sum_{w=1}^{W} x_{n,c,h,w} \]

方差为:

\[ \sigma_{n,c}^{2} = \frac{1}{HW} \sum_{h=1}^{H} \sum_{w=1}^{W} \left(x_{n,c,h,w}-\mu_{n,c}\right)^2 \]

因此,一个输入中共有 \(N\times C\) 组均值和方差。如果在计算时保留被归一化的维度,则 \(\mu\)\(\sigma^2\) 的形状均为 (N, C, 1, 1)

标准化后的元素为:

\[ \hat{x}_{n,c,h,w} = \frac{x_{n,c,h,w}-\mu_{n,c}} {\sqrt{\sigma_{n,c}^{2}+\epsilon}} \]

在 PyTorch 中,对应的统计过程可以写成:

mean = x.mean(dim=(2, 3), keepdim=True)
var = x.var(dim=(2, 3), correction=0, keepdim=True)

这里的 dim=(2, 3) 表示只沿 \(H\)\(W\) 计算统计量,而 \(N\)\(C\) 两个维度被保留下来。也就是说,不同样本之间不会共享统计量,同一样本的不同通道之间也不会共享统计量。

7.5.2 BatchNorm、LayerNorm 和 InstanceNorm 的统计方向

BatchNorm、LayerNorm 和 InstanceNorm 使用的标准化公式其实是一样的:

\[ \hat{x} = \frac{x-\mu}{\sqrt{\sigma^2+\epsilon}} \]

它们真正的区别不在公式,而在于计算 \(\mu\)\(\sigma^2\) 时,哪些元素被划分到同一组中。

对于同一个输入:

\[ X\in\mathbb{R}^{N\times C\times H\times W} \]

BatchNorm 固定通道 \(C\),沿 \(N\)\(H\)\(W\) 计算统计量,因此均值和方差的形状为:

\[ (1,C,1,1) \]

LayerNorm 在 normalized_shape=(C, H, W) 时,固定样本 \(N\),沿 \(C\)\(H\)\(W\) 计算统计量,因此均值和方差的形状为:

\[ (N,1,1,1) \]

InstanceNorm 则同时固定样本 \(N\) 和通道 \(C\),只沿 \(H\)\(W\) 计算统计量,因此均值和方差的形状为:

\[ (N,C,1,1) \]

对应到 PyTorch 的维度操作,可以写成:

# BatchNorm2d: Fixed C, reduce over N, H, W
bn_mean = x.mean(dim=(0, 2, 3), keepdim=True)
bn_var = x.var(dim=(0, 2, 3), correction=0, keepdim=True)

# LayerNorm((C, H, W)): Fixed N, reduce over C, H, W
ln_mean = x.mean(dim=(1, 2, 3), keepdim=True)
ln_var = x.var(dim=(1, 2, 3), correction=0, keepdim=True)

# InstanceNorm2d: Fixed N, C, reduce over H, W
in_mean = x.mean(dim=(2, 3), keepdim=True)
in_var = x.var(dim=(2, 3), correction=0, keepdim=True)

因此,对于 \((N,C,H,W)\) 输入,可以把三者概括为:

  • BatchNorm:每个通道一组统计量;
  • LayerNorm:每个样本一组统计量;
  • InstanceNorm:每个样本的每个通道一组统计量。

我们可以直接比较三者的输出统计量:

x = torch.randn(4, 3, 5, 5)

batch_norm = nn.BatchNorm2d(3, affine=False, track_running_stats=False)
layer_norm = nn.LayerNorm((3, 5, 5), elementwise_affine=False)
instance_norm = nn.InstanceNorm2d(3, affine=False, track_running_stats=False)

bn_output = batch_norm(x)
ln_output = layer_norm(x)
in_output = instance_norm(x)

print('BatchNorm means over N, H, W:')
print(bn_output.mean(dim=(0, 2, 3)))

print('\nLayerNorm means over C, H, W:')
print(ln_output.mean(dim=(1, 2, 3)))

print('\nInstanceNorm means over H, W:')
print(in_output.mean(dim=(2, 3)))
BatchNorm means over N, H, W:
tensor([-4.7684e-09,  0.0000e+00,  9.5367e-09])

LayerNorm means over C, H, W:
tensor([-4.4505e-08,  2.7816e-09,  1.5895e-09,  1.9073e-08])

InstanceNorm means over H, W:
tensor([[ 9.5367e-09,  2.6226e-08, -2.8610e-08],
        [ 2.3842e-09,  4.1723e-09,  4.7684e-09],
        [-7.1526e-09,  7.1526e-09, -9.5367e-09],
        [-1.9073e-08,  1.4305e-08, -7.1526e-09]])

这里需要注意:

  • BatchNorm 的输出保证每个通道在整个 batch 和空间位置上的均值接近 0;
  • LayerNorm 的输出保证每个样本在所有通道和空间位置上的均值接近 0;
  • InstanceNorm 的输出保证每个样本的每个通道在空间位置上的均值接近 0。

所以,BatchNorm 的输出会受到 batch 中其他样本的影响,而 LayerNorm 和 InstanceNorm 的输出只依赖当前样本自己的统计量,不会受到 batch 中其他样本的影响。

7.5.3 为什么 InstanceNorm 常用于风格迁移

InstanceNorm 最经典的应用之一是图像风格迁移。

在卷积网络中,某个通道的空间均值和方差常常与图像的整体外观统计有关,例如亮度、对比度、颜色强度和纹理响应。对于同一幅内容图像,不同风格可能会让这些通道统计量发生明显变化。InstanceNorm 会对每张图像的每个通道分别去除均值并缩放方差:

\[ \hat{x}_{n,c,h,w} = \frac{x_{n,c,h,w}-\mu_{n,c}} {\sqrt{\sigma_{n,c}^2+\epsilon}} \]

因此,它会弱化单张图像中与通道均值和方差相关的全局外观信息,让后续网络更容易重新注入目标风格。

我们可以用一个非常简单的仿射变换观察这种性质。如果对某个通道做:

\[ x' = ax + b, \quad a > 0 \]

那么标准化前后通常满足近似不变性:

\[ \operatorname{IN}(ax+b) \approx \operatorname{IN}(x) \]

x = torch.randn(1, 3, 8, 8)
x_transformed = x * 7.0 + 20.0

instance_norm = nn.InstanceNorm2d(3, affine=False)

y1 = instance_norm(x)
y2 = instance_norm(x_transformed)

max_diff = (y1 - y2).abs().max()
print('Maximum difference:', max_diff.item())
Maximum difference: 1.8596649169921875e-05

这里并不是说所有图像风格都可以简单地表示为每个通道的仿射变换。更准确地说,InstanceNorm 移除了部分与实例级通道统计量相关的信息,因此特别适合需要控制或重新建模图像风格的网络。而对于分类任务,图像自身的对比度、颜色和强度统计可能包含有用的类别信息。过度去除这些信息,可能反而降低模型表现。

7.5.4 InstanceNorm 也会进行可学习的仿射变换

和 BatchNorm、LayerNorm 一样,InstanceNorm 也可以在标准化之后进行可学习的仿射变换:

\[ y_{n,c,h,w} = \gamma_c\hat{x}_{n,c,h,w}+\beta_c \]

这里的 \(\gamma\)\(\beta\) 按通道定义,形状为 \((C,)\)。它们会通过 broadcasting 作用到每个样本和空间位置。

不过,PyTorch 中 InstanceNorm2d 默认使用:

nn.InstanceNorm2d(num_features, affine=False)

也就是说,默认不创建可学习的 weightbias

instance_norm_no_affine = nn.InstanceNorm2d(3)
instance_norm_affine = nn.InstanceNorm2d(3, affine=True)

print('Default weight:', instance_norm_no_affine.weight)
print('Default bias:', instance_norm_no_affine.bias)
print('Affine weight shape:', instance_norm_affine.weight.shape)
print('Affine bias shape:', instance_norm_affine.bias.shape)
Default weight: None
Default bias: None
Affine weight shape: torch.Size([3])
Affine bias shape: torch.Size([3])

因此,如果我们想让模型学习每个通道的缩放和平移,我们要手动设置设置 affine=True。需要注意,InstanceNorm 的仿射参数是按通道定义的,而不是像 LayerNorm((C, H, W)) 那样为每个通道和空间位置都学习独立参数。

PyTorch 中 InstanceNorm2d 的另一个重要默认值是:

track_running_stats=False

这意味着默认情况下,它不会维护 running_meanrunning_var,训练和推理时都会使用当前输入自己的统计量。

instance_norm = nn.InstanceNorm2d(3)

print('track_running_stats:', instance_norm.track_running_stats)
print('running_mean:', instance_norm.running_mean)
print('running_var:', instance_norm.running_var)
track_running_stats: False
running_mean: None
running_var: None

因此,在默认设置下,train()eval() 不会改变 InstanceNorm 使用哪组统计量:

x = torch.randn(2, 3, 4, 4)
instance_norm = nn.InstanceNorm2d(3)

instance_norm.train()
y_train = instance_norm(x)

instance_norm.eval()
y_eval = instance_norm(x)

max_diff = (y_train - y_eval).abs().max()
print('Maximum train / eval difference:', max_diff.item())
Maximum train / eval difference: 0.0

这和 BatchNorm 不同。BatchNorm 通常在训练时使用当前 batch statistics,在推理时使用 running statistics;默认的 InstanceNorm 在两个阶段都使用当前样本自己的统计量。

不过,PyTorch 允许显式设置:

nn.InstanceNorm2d(
    num_features,
    track_running_stats=True,
)

这时模块会维护运行时统计量,并在 eval() 模式下使用它们。

instance_norm = nn.InstanceNorm2d(3, track_running_stats=True)

print('running_mean shape:', instance_norm.running_mean.shape)
print('running_var shape:', instance_norm.running_var.shape)

instance_norm.train()
for _ in range(5):
    x = torch.randn(4, 3, 8, 8)
    y = instance_norm(torch.randn(4, 3, 8, 8))

instance_norm.eval()
x = torch.randn(2, 3, 8, 8)
y = instance_norm(x)

print('Updated running mean:', instance_norm.running_mean)
print('Updated running variance:', instance_norm.running_var)
running_mean shape: torch.Size([3])
running_var shape: torch.Size([3])
Updated running mean: tensor([-0.0053, -0.0089, -0.0069])
Updated running variance: tensor([0.9879, 0.9809, 1.0125])

但从 InstanceNorm 最常见的使用方式来看,它的核心特点正是按当前实例计算统计量,因此默认不维护 running statistics 更符合它的原始设计目标。

7.5.5 InstanceNorm 的 PyTorch 实现

下面我们实现一个简化版 InstanceNorm。

首先实现一个带 affine 但不维护 running statistics 的函数版本。

def instance_norm(
    x: Tensor,
    weight: Tensor | None = None,
    bias: Tensor | None = None,
    momentum: float = 0.1,
    eps: float = 1e-5,
) -> Tensor:
    """Apply instance normalization to an input tensor."""
    # (N, C, H, W) -> reduce_dims = (2, 3)
    reduce_dims = tuple(range(2, x.ndim))

    # Per-instance statistics have shape (N, C).
    input_stats_shape = (x.size(0), x.size(1)) + (1,) * (x.ndim - 2)

    # Running statistics and affine parameters have shape (C,).
    broadcast_shape = (1, x.size(1)) + (1,) * (x.ndim - 2)

    # Hit this branch when:
    # 1) In training mode, regardless of whether running stats are provided, or
    # 2) In evaluation mode when running stats are not provided.
    sample_count = x[0, 0].numel()
    if sample_count <= 1:
        raise ValueError(
            'Expected more than 1 spatial value when using input statistics, '
            f'but got input shape {tuple(x.shape)}.'
        )

    # Each sample and channel has its own mean and variance.
    instance_mean = x.mean(dim=reduce_dims).reshape(input_stats_shape)
    instance_var = x.var(dim=reduce_dims, correction=0).reshape(input_stats_shape)

    y = (x - instance_mean) * (instance_var + eps).rsqrt()

    if weight is not None:
        y = y * weight.reshape(broadcast_shape)

    if bias is not None:
        y = y + bias.reshape(broadcast_shape)

    return y

将它和 F.instance_norm 对照:

x = torch.randn(4, 3, 5, 5)
weight = torch.randn(3)
bias = torch.randn(3)

actual_output = instance_norm(x)
expected_output = F.instance_norm(x, weight=weight, bias=bias)

max_diff = (actual_output - expected_output).abs().max()
print('Maximum difference:', max_diff.item())
Maximum difference: 2.941128730773926

两者结果应该只存在很小的浮点误差。

把上面的逻辑封装为模块:

class InstanceNorm(nn.Module):
    """Base class for instance normalization modules."""

    weight: Tensor | None
    bias: Tensor | None

    def __init__(
        self,
        num_features: int,
        eps: float = 1e-5,
        momentum: float = 0.1,
        affine: bool = False,
        bias: bool = True,
    ):
        super().__init__()
        self.num_features = num_features
        self.eps = eps
        self.momentum = momentum
        self.affine = affine

        if affine:
            self.weight = nn.Parameter(torch.empty(num_features))
            if bias:
                self.bias = nn.Parameter(torch.empty(num_features))
            else:
                self.register_parameter('bias', None)
        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_features:
            raise AssertionError(
                f'Expected {self.num_features} channels, but got {x.size(1)} channels.'
            )

        return instance_norm(
            x,
            weight=self.weight,
            bias=self.bias,
            momentum=self.momentum,
            eps=self.eps,
        )

    def extra_repr(self) -> str:
        return (
            f'{self.num_features}, eps={self.eps}, momentum={self.momentum}, '
            f'affine={self.affine}'
        )
x = torch.randn(2, 4, 6, 6)

instance_norm1 = InstanceNorm(4, affine=True)
instance_norm2 = nn.InstanceNorm2d(4, affine=True)

with torch.no_grad():
    instance_norm2.weight.copy_(instance_norm1.weight)
    instance_norm2.bias.copy_(instance_norm1.bias)

actual = instance_norm1(x)
expected = instance_norm2(x)

max_err = (actual - expected).abs().max()
print('Maximum difference:', max_err.item())
Maximum difference: 4.76837158203125e-07

这个实现省略了 running statistics、输入为空时的边界情况、不同设备与数据类型检查等工程细节,但保留了 InstanceNorm 的核心计算过程。

7.5.6 InstanceNorm1d、InstanceNorm2d 和 InstanceNorm3d

PyTorch 提供了三个常用 InstanceNorm 模块:

  • nn.InstanceNorm1d
  • nn.InstanceNorm2d
  • nn.InstanceNorm3d

它们的区别主要在于期望的输入形状,而不是归一化思想不同。

表 1:PyTorch InstanceNorm 模块与常见输入形状
模块 常见输入形状 每个样本、每个通道的统计维度
InstanceNorm1d(C) \((N, C, L)\) \(L\)
InstanceNorm2d(C) \((N, C, H, W)\) \(H, W\)
InstanceNorm3d(C) \((N, C, D, H, W)\) \(D, H, W\)

无论输入有几个空间或序列维度,InstanceNorm 都保留批次维度 \(N\) 和通道维度 \(C\),并分别对每个样本、每个通道内部的其他维度计算统计量。因此,InstanceNorm 不会像 BatchNorm 那样在不同样本之间共享均值和方差。对于输入中的每个样本和每个通道,它都会独立计算一组均值和方差。

x_1d = torch.randn(4, 8, 16)
x_2d = torch.randn(4, 8, 16, 16)
x_3d = torch.randn(4, 8, 4, 16, 16)

in_1d = nn.InstanceNorm1d(8)
in_2d = nn.InstanceNorm2d(8)
in_3d = nn.InstanceNorm3d(8)

print('InstanceNorm1d output:', in_1d(x_1d).shape)
print('InstanceNorm2d output:', in_2d(x_2d).shape)
print('InstanceNorm3d output:', in_3d(x_3d).shape)
InstanceNorm1d output: torch.Size([4, 8, 16])
InstanceNorm2d output: torch.Size([4, 8, 16, 16])
InstanceNorm3d output: torch.Size([4, 8, 4, 16, 16])

需要注意,1d、2d 和 3d 描述的是数据中额外的序列或空间结构,而 num_features 始终对应通道维度。与 BatchNorm 不同,InstanceNorm 的统计量只在单个样本的单个通道内部计算,不包含批次维度 \(N\) 和通道维度 \(C\)

7.5.7 InstanceNorm 的局限

InstanceNorm 的优势是完全不依赖 batch,并且可以有效移除单个样本内部每个通道的均值和方差。但这种性质也带来一些局限。

首先,InstanceNorm 可能会移除有用的实例级信息。图像的绝对亮度、对比度、颜色强度等统计量有时本身就和特定任务相关。InstanceNorm 会主动减弱这些信息,因此它并不一定适合图像分类、目标检测等所有视觉任务。

其次,InstanceNorm 在空间尺寸过小时统计量不稳定。InstanceNorm 依赖每个通道中的空间元素计算方差,当空间尺寸很小时,可用于统计的元素数量也会变少。特别是训练模式下,如果每个通道只有一个空间元素,例如输入形状为 (N, C, 1, 1),就无法根据空间维度估计有意义的方差。PyTorch 会对这种情况进行检查。

最后,InstanceNorm 不解决所有训练稳定性问题。InstanceNorm 只调整激活的统计量,它不能替代合理的参数初始化、学习率设置、残差连接、优化器选择和梯度处理。

7.5.8 本章小结

这一节介绍了 Instance Normalization。它与前面两种归一化方法使用相同的标准化公式,但统计维度不同。

对于输入:

\[ X\in\mathbb{R}^{N\times C\times H\times W} \]

InstanceNorm 会固定样本和通道,沿空间维度计算:

\[ \begin{aligned} \mu_{n,c} = \frac{1}{HW} \sum_{h,w}x_{n,c,h,w} \\ \sigma_{n,c}^2 = \frac{1}{HW} \sum_{h,w}(x_{n,c,h,w}-\mu_{n,c})^2 \end{aligned} \]

它的几个核心特点是:

  • 每个样本独立,不依赖 batch 中的其他样本;
  • 每个通道独立,不会把不同通道混在一起统计;
  • 默认不使用 affine 参数;
  • 默认不维护 running statistics;
  • 默认训练和推理都使用当前输入的实例统计量;
  • 常用于风格迁移和部分图像生成任务;
  • 可能移除对分类等任务有用的实例级外观信息。

到目前为止,我们已经看到三种不同的统计方式:

  • BatchNorm:每个通道一组统计量;
  • LayerNorm:每个样本一组统计量;
  • InstanceNorm:每个样本的每个通道一组统计量。

下一节将介绍 Group Normalization (Wu and He 2018)。它会把通道划分成若干组,在每个样本的每个通道组内部计算统计量。GroupNorm 可以看成在 LayerNorm 和 InstanceNorm 之间建立了一种更灵活的折中。

References

Ulyanov, Dmitry, Andrea Vedaldi, and Victor Lempitsky. 2017. Instance Normalization: The Missing Ingredient for Fast Stylization. https://arxiv.org/abs/1607.08022.
Wu, Yuxin, and Kaiming He. 2018. Group Normalization. https://arxiv.org/abs/1803.08494.

Reuse