2026-07-12 10:00:00 +0800

einops 实用指南:用可读的方式重排、归约和重复张量

einops 是一个用于张量变换的 Python 库,它把 reshapetransposepermuteviewsummeanrepeat 等操作统一成一种接近数学记号的字符串表达式。它的核心价值不是"能做原生框架做不到的事",而是让张量操作更可读、更难写错、更容易维护。

在 PyTorch、TensorFlow、JAX、NumPy 中,张量变换经常会写成:

x = x.view(b, h, w, c).permute(0, 3, 1, 2).contiguous()

这段代码对机器很清楚,对人却不够直观:每个维度代表什么?为什么要这样交换?维度是否匹配?einops 的写法更接近"声明我要什么":

from einops import rearrange

x = rearrange(x, 'b h w c -> b c h w')

左边描述输入维度,右边描述输出维度。b h w c -> b c h w 直接表达"把 NHWC 转成 NCHW"。


1. 安装与基本用法

安装:

pip install einops

常用导入:

from einops import rearrange, reduce, repeat, pack, unpack, einsum

在 PyTorch 中使用:

import torch
from einops import rearrange, reduce, repeat

x = torch.randn(2, 3, 4)
y = rearrange(x, 'b c t -> b t c')

print(y.shape)  # torch.Size([2, 4, 3])

einops 支持多个后端,包括 PyTorch、TensorFlow、JAX、NumPy 等。实际计算仍由对应框架完成,einops 主要负责解析模式并调用合适的底层张量操作。


2. einops 的核心概念

einops 最常用的三个函数是:

函数 作用 典型用途
rearrange 重排、合并、拆分维度 transposepermutereshapeflatten、patch 提取
reduce 对某些维度做归约 summeanmaxmin、池化
repeat 重复或扩展维度 增加 batch、复制 token、广播式扩展

模式字符串

模式字符串的基本形式:

'输入维度表达式 -> 输出维度表达式'

例如:

rearrange(x, 'b c h w -> b h w c')

含义是:

  • 输入张量维度顺序是 batch, channel, height, width
  • 输出张量维度顺序是 batch, height, width, channel

合并维度

括号表示把多个维度组合成一个维度:

rearrange(x, 'b c h w -> b (c h w)')

这会把 c, h, w 展平成一个维度。

拆分维度

也可以把一个维度拆成多个维度,但需要显式提供至少一个维度大小:

rearrange(x, 'b c (h w) -> b c h w', h=2, w=2)

如果 h * w 与原维度大小不匹配,einops 会直接报错。


3. 为什么 einops 比原生 PyTorch/TensorFlow 更适合表达张量操作

3.1 维度语义更明确

原生 PyTorch:

x = x.permute(0, 2, 3, 1)

这段代码只告诉我们"第 0、2、3、1 个维度重排",但没有告诉我们每个维度是什么。

einops

x = rearrange(x, 'b c h w -> b h w c')

这里每个轴都有名字:b 是 batch,c 是 channel,h 是 height,w 是 width。

3.2 复杂变换可以一步表达

例如把图像切成 patch,原生 PyTorch 往往需要 reshape + permute + reshape 多步操作。einops 可以写成:

patches = rearrange(
    images,
    'b c (h ph) (w pw) -> b (h w) (ph pw c)',
    ph=16, pw=16,
)

这行代码直接表达了:

  • 输入是 b c height width
  • 每个 patch 大小是 ph * pw
  • 输出是 b num_patches patch_dim

3.3 更容易发现 shape 错误

如果维度大小不匹配,einops 会给出较清晰的错误。例如:

rearrange(torch.randn(2, 3, 5), 'b c (h w) -> b c h w', h=2, w=3)

最后一维大小是 5,但 h * w = 6,因此会报错。相比手写 view,这种错误更容易定位。

3.4 可跨框架迁移

同样的模式字符串可以用于 PyTorch、TensorFlow、JAX、NumPy。更换后端时,大部分张量变换逻辑不需要重写。


4. rearrange:重排、转置、reshape、flatten

先创建一个简单张量:

import torch
from einops import rearrange

x = torch.arange(2 * 3 * 4).reshape(2, 3, 4)

print(x.shape)  # torch.Size([2, 3, 4])

这里我们把三个维度记作 b c t(b=2, c=3, t=4)。

4.1 转置 / permute

b c t 变成 b t c

y = rearrange(x, 'b c t -> b t c')

print(y.shape)  # torch.Size([2, 4, 3])

等价 PyTorch 写法:y = x.permute(0, 2, 1)

4.2 flatten:展平多个维度

ct 合并:

flat = rearrange(x, 'b c t -> b (c t)')

print(flat.shape)  # torch.Size([2, 12])

等价 PyTorch:flat = x.reshape(2, 12)。但 einops 更清楚地表达了 (c t) 是由哪些维度合并得到的。

4.3 拆分维度

把长度为 4 的维度拆成 2 * 2

z = rearrange(x, 'b c (h w) -> b c h w', h=2, w=2)

print(z.shape)  # torch.Size([2, 3, 2, 2])

4.4 合并 batch 和时间维

深度学习中经常需要把 batch 和 sequence 合并,送入某个只接受二维输入的模块:

x = torch.randn(2, 5, 16)  # b n d

y = rearrange(x, 'b n d -> (b n) d')

print(y.shape)  # torch.Size([10, 16])

恢复原形状:

x_restored = rearrange(y, '(b n) d -> b n d', b=2, n=5)

print(x_restored.shape)  # torch.Size([2, 5, 16])

5. reduce:对维度做 sum、mean、max

reduce 的模式同样是 '输入维度 -> 输出维度'。出现在输入中但没有出现在输出中的维度,会被归约掉。

import torch
from einops import reduce

x = torch.arange(2 * 3 * 4).reshape(2, 3, 4).float()

# 沿时间维求和
sum_t = reduce(x, 'b c t -> b c', 'sum')
print(sum_t)
# tensor([[ 6., 22., 38.],
#         [54., 70., 86.]])

# 沿 c 和 t 维求均值
mean_ct = reduce(x, 'b c t -> b', 'mean')
print(mean_ct)  # tensor([ 5.5000, 17.5000])

# 沿 channel 维取最大值
max_c = reduce(x, 'b c t -> b t', 'max')
print(max_c)
# tensor([[ 8.,  9., 10., 11.],
#         [20., 21., 22., 23.]])

全局平均池化

CNN 中常见的 global average pooling:

features = torch.randn(8, 64, 14, 14)  # b c h w

pooled = reduce(features, 'b c h w -> b c', 'mean')

print(pooled.shape)  # torch.Size([8, 64])

6. repeat:重复与扩展张量

import torch
from einops import repeat

v = torch.tensor([1, 2, 3])

x = repeat(v, 'c -> b c', b=2)

print(x)
# tensor([[1, 2, 3],
#         [1, 2, 3]])

增加新维度

x = torch.tensor([[1, 2, 3], [4, 5, 6]])

y = repeat(x, 'b c -> b c r', r=2)

print(y.shape)  # torch.Size([2, 3, 2])

为每个样本重复出多个候选

这在 beam search、contrastive learning 等场景中很常见:

x = torch.randn(4, 128)  # b d

candidates = repeat(x, 'b d -> b k d', k=10)

print(candidates.shape)  # torch.Size([4, 10, 128])

7. space-to-depth 与 depth-to-space

创建一张 1 x 1 x 4 x 4 的图像:

import torch
from einops import rearrange

image = torch.arange(1 * 1 * 4 * 4).reshape(1, 1, 4, 4)

print(image[0, 0])
# tensor([[ 0,  1,  2,  3],
#         [ 4,  5,  6,  7],
#         [ 8,  9, 10, 11],
#         [12, 13, 14, 15]])

7.1 space-to-depth

把每个 2 x 2 空间块移动到 channel 维:

s2d = rearrange(
    image,
    'b c (h ph) (w pw) -> b (c ph pw) h w',
    ph=2, pw=2,
)

print(s2d.shape)  # torch.Size([1, 4, 2, 2])

7.2 depth-to-space

恢复回原图:

restored = rearrange(
    s2d,
    'b (c ph pw) h w -> b c (h ph) (w pw)',
    ph=2, pw=2,
)

print(torch.equal(restored, image))  # True

8. packunpack:处理可变形状片段

einops.packeinops.unpack 适合把多个张量沿某个"打包维度"拼接起来,并保存恢复所需的形状信息。

典型例子:把一个 CLS token 和一组 patch tokens 拼成 Transformer 的输入。

import torch
from einops import pack, unpack

cls_token = torch.randn(2, 1, 8)      # b 1 d
patch_tokens = torch.randn(2, 4, 8)   # b n d

tokens, packed_shapes = pack([cls_token, patch_tokens], 'b * d')

print(tokens.shape)       # torch.Size([2, 5, 8])
print(packed_shapes)      # [torch.Size([1]), torch.Size([4])]

cls_restored, patches_restored = unpack(tokens, packed_shapes, 'b * d')

print(cls_restored.shape)     # torch.Size([2, 1, 8])
print(patches_restored.shape) # torch.Size([2, 4, 8])

这里的 * 表示"需要被打包的中间维度"。相比手动记录每段长度,pack/unpack 更不容易出错。


9. 多头注意力中的 reshape 模式

Transformer 中的多头注意力经常需要在 b n (h d) <-> b h n d 之间转换。

import torch
from einops import rearrange

batch, seq_len, num_heads, head_dim = 2, 5, 4, 8
embed_dim = num_heads * head_dim

q = torch.randn(batch, seq_len, embed_dim)

q_heads = rearrange(q, 'b n (h d) -> b h n d', h=num_heads)

print(q_heads.shape)  # torch.Size([2, 4, 5, 8])

经过注意力计算后,合并 heads:

out_heads = torch.randn(batch, num_heads, seq_len, head_dim)

out = rearrange(out_heads, 'b h n d -> b n (h d)')

print(out.shape)  # torch.Size([2, 5, 32])

这比下面这种原生写法更清晰:

q_heads = q.view(batch, seq_len, num_heads, head_dim).permute(0, 2, 1, 3)

einops 写法直接写出了语义:embed_dim = h * d


10. 图像 patch 提取

Vision Transformer 中常见操作是把图像切成 patch,再把每个 patch 展平成 token。

images = torch.randn(1, 3, 4, 4)  # b c h w

# patch 大小 2x2
patches = rearrange(
    images,
    'b c (h ph) (w pw) -> b (h w) (ph pw c)',
    ph=2, pw=2,
)

print(patches.shape)  # torch.Size([1, 4, 12])

恢复:

restored = rearrange(
    patches,
    'b (h w) (ph pw c) -> b c (h ph) (w pw)',
    h=2, w=2, ph=2, pw=2, c=3,
)

print(torch.equal(restored, images))  # True

对于标准的 ViT 设置(ImageNet,224x224,patch=16):

images = torch.randn(32, 3, 224, 224)

patches = rearrange(
    images,
    'b c (h ph) (w pw) -> b (h w) (ph pw c)',
    ph=16, pw=16,
)

print(patches.shape)  # torch.Size([32, 196, 768])
# 196 = (224/16)^2, 768 = 16*16*3

11. 与爱因斯坦求和记号的关系

einops 的名字来自 Einstein operations。它的模式表达式借鉴了爱因斯坦求和记号的思想:用符号表示轴,用重复或消失的符号表示组合、变换或归约。

einops 也提供了 einsum

import torch
from einops import einsum

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

y = einsum(x, w, 'b t d, d o -> b t o')

print(y.shape)  # torch.Size([2, 3, 5])

相比 torch.einsum('btd,do->bto', x, w),空格分隔的写法更适合在复杂模型中阅读和维护。

注意:rearrange/reduce/repeat 主要处理轴变换;einsum 主要处理乘法与求和。


12. 深度学习中的常见模式

12.1 CNN:NCHW 与 NHWC 转换

x = torch.randn(8, 3, 224, 224)  # NCHW

x_nhwc = rearrange(x, 'b c h w -> b h w c')

print(x_nhwc.shape)  # torch.Size([8, 224, 224, 3])

12.2 CNN:全局平均池化

features = torch.randn(8, 512, 7, 7)

pooled = reduce(features, 'b c h w -> b c', 'mean')

print(pooled.shape)  # torch.Size([8, 512])

12.3 批处理:合并 batch 和时间维

x = torch.randn(4, 10, 128)

x_flat = rearrange(x, 'b t d -> (b t) d')

print(x_flat.shape)  # torch.Size([40, 128])

# 恢复
x_restore = rearrange(x_flat, '(b t) d -> b t d', b=4, t=10)

print(x_restore.shape)  # torch.Size([4, 10, 128])

12.4 Transformer:合并和拆分 attention heads

x = torch.randn(2, 16, 64)  # b n d

x_heads = rearrange(x, 'b n (h d) -> b h n d', h=8)

print(x_heads.shape)  # torch.Size([2, 8, 16, 8])

# 恢复
x_merge = rearrange(x_heads, 'b h n d -> b n (h d)')

print(x_merge.shape)  # torch.Size([2, 16, 64])

13. 性能考虑

  • 实际计算仍由底层框架完成 — einops 最终调用 PyTorch 的 reshape、permute 等操作
  • 能返回 view 时通常返回 view — 纯 reshape/permute 不复制数据
  • 需要改变内存布局时可能产生 copy — 某些 permute 后再 reshape 需要 contiguous
  • 模式解析有轻微开销 — 对超大张量可忽略;极小张量的高频内循环中,原生操作可能略快

实用建议:

  • 模型结构代码、数据预处理、复杂 shape 变换 → 优先 einops
  • 极端性能敏感的内循环 → benchmark 比较
  • 需要控制内存连续性时 → 显式检查 .is_contiguous() 并在必要时 .contiguous()

14. 最佳实践

使用有语义的轴名

推荐:rearrange(x, 'b c h w -> b h w c') 不推荐:rearrange(x, 'a b c d -> a c d b')

拆分维度时显式传参

rearrange(x, 'b (h w) c -> b h w c', h=14, w=14)

在注释中写出关键 shape

# x: [batch, num_patches, patch_dim]
x = rearrange(x, 'b (h w) (ph pw c) -> b c (h ph) (w pw)',
              h=14, w=14, ph=16, pw=16, c=3)

优先使用 einops layer

einops 还提供适合神经网络模块的层:

from einops.layers.torch import Rearrange, Reduce
import torch.nn as nn

model = nn.Sequential(
    Rearrange('b c h w -> b (c h w)'),
    nn.Linear(3 * 32 * 32, 10),
)

这种写法可以把 shape 变换直接放进 nn.Sequential


15. 小结

einops 用统一的模式字符串表达张量重排、归约、重复和打包操作。它特别适合深度学习代码,因为模型中的张量维度通常有明确语义:batch、channel、height、width、sequence、head、embedding 等。

核心记法可以总结为:

rearrange(x, 'b c h w -> b h w c')       # 重排维度
rearrange(x, 'b c h w -> b (c h w)')     # 合并维度
rearrange(x, 'b (h w) c -> b h w c')     # 拆分维度
reduce(x, 'b c h w -> b c', 'mean')      # 归约维度
repeat(x, 'b d -> b k d', k=10)          # 重复/扩展维度

在工程实践中,einops 的最大价值是让 shape 变换从"位置索引操作"变成"带语义的声明式表达"。对于 CNN、Transformer、ViT、多头注意力、图像 patch、批处理 reshape 等场景,它通常比原生 view/reshape/permute 更容易阅读和维护。

进一步资源