
在深度学习里摸爬滚打一段时间后我发现很多人对张量形状的处理都到了“看着代码愣半天”的程度。view、permute、squeeze、unsqueeze这一整套操作在调试的时候经常让我有种“这个维数到底对应哪个维度”的混乱感。尤其是写多头注意力或者图像的 patch 划分时一个形状写错后面全盘报错报错信息还晦涩难懂。直到我用上 einops这个痛点才算真正解决。它是一个让你用“可读的字符串语法”来操作张量的库核心就是rearrange、reduce、repeat这三个函数。这篇文章我就把 einops 的基本使用思路和实际踩坑经验一次性讲透尤其适合正在使用 PyTorch、TensorFlow 这些框架处理多维数组的开发者。1. 为什么需要 einops传统张量操作的三宗罪先聊聊传统方式的痛点。以 PyTorch 为例我们经常会做这些操作# 假设输入 x 形状是 (batch, channel, height, width) x torch.randn(32, 3, 224, 224) # 想把 H 和 W 合并 x1 x.view(32, 3, 224 * 224)问题来了。view只是按照“内存顺序”去展平如果你的 tensor 不是连续的比如前面遇到过transpose直接view就会报错还必须先contiguous()。这只是浅层问题更痛的是下面几类场景维度顺序调整x.permute(0, 2, 1)这种写法谁能一眼看出你是在交换哪两个维度数字索引稍不留神就写反了。squeeze / unsqueeze 的误区squeeze默认会把所有长度为 1 的维度都去掉这根本不是你想要的结果你往往只是想压缩某一个特定的维度。可分步完成但不可维护做 transformer 的多头注意力时要把(batch, seq_len, hidden_dim)拆成(batch, num_heads, seq_len, head_dim)用原生 API 要写好几行view、transpose、contiguous。代码跑起来没问题但过一个月再回来看基本要靠注释才知道自己在干嘛。einops 改变了这一切它借鉴了爱因斯坦求和约定的思想让你直接用“描述性字符串”来表达张量的重新排列、合并和拆分。比如上面那个view(32, 3, 224*224)用 einops 可以写成from einops import rearrange x1 rearrange(x, b c h w - b c (h w))你看输入维度b c h w输出维度b c (h w)括号表示合并一目了然根本不会猜错。而且它不需要你在脑子里反复推演维度顺序报错信息也比原生框架的“shape mismatch”要直观得多。无论你是做图像处理、NLP 还是多模态只要你在跟数组、张量打交道einops 都能让代码可读性上升一个台阶同时降低因形状操作出错的概率。下面的内容我会从安装开始逐步拆解它的核心功能、实际应用和避坑方法。2. einops 核心 API 拆解rearrange、reduce、repeat2.1 安装与导入安装非常简单直接 pip 就行pip install einops没有其他依赖装完就能用。在 Python 里导入最常用的是这三个函数from einops import rearrange, reduce, repeat如果你使用 PyTorcheinops 对它的支持最友好TensorFlow、JAX 甚至普通 NumPy 数组也都能用底层就是通用张量操作。2.2 rearrange重排、合并、拆分维度rearrange有两个参数第一个是待处理的张量第二个是描述维度的字符串。字符串左侧是输入轴的命名右侧是输出轴的命名中间用-连接。rearrange(tensor, 输入轴名称 - 输出轴名称)轴名称你随意起只要方便记忆比如b代表 batchc代表 channelh是 heightw是 width。关键是右侧怎么写括号内合并b c h w - b c (h w)这样就 H 和 W 展成一个维度。括号外拆分b (h w) - b h w把一个维度拆成两个但拆成多少需要你手动在调用时指定x torch.randn(32, 64, 224 * 224) y rearrange(x, b c (h w) - b c h w, h14, w16)einops 知道h * w 224*224再加上你提供的h、w就能推导出另一个。注意这里拆分的总元素个数必须匹配。维度交互b c h w - b (c h) w。c 和 h 合并成一个维度同时保留 w。省略号...当输入维度比较多但你只关心某些维度时可以用...占位符代表任意数量的维度。比如rearrange(x, ... h w - ... (h w))前面不管有多少维度都原样保留最终只要把最后的 h 和 w 合并。顺带一提rearrange内部自动处理contiguous()的逻辑。你从没见过它会因为内存不连续而报错它够聪明会优先尝试view不行就自动转permute加contiguous再从底层走。这也是我特别愿意用它替代原生 API 的原因之一。2.3 reduce在指定维度上做归约操作reduce的作用是“压缩”某个维度你可以用字符串指定在哪些轴上做操作比传统的sum(dim...)好理解得多。调用格式是reduce(tensor, b c h w - b c, reductionmean)这里的reduction可以是mean、sum、max、min等常见归约操作。比如对一张(batch, channel, height, width)的图像做全局平均池化传统写法要x.mean(dim(2, 3))而现在直接pooled reduce(x, b c h w - b c, reductionmean)这个reduce特别适合做图像金字塔、注意力池化、或者对序列做长度压缩。值得注意的是右侧的维度只能是左侧维度的子集或通过括号合并后的结果你可以把多个维度归约成单个维度也可以直接跨多个轴归约只要右侧没有的轴einops 就会按指定操作“收掉”它们。2.4 repeat沿维度重复数据repeat是反向扩展它会将某个维度复制多份。格式是repeat(tensor, b c - b c t, t4)这段代码把(batch, channel)的 tensor 在最后新增一个维度并且重复 4 次变成(b, c, 4)。它最常见的用途包括将单特征图广播到多个头、生成多个候选框或者在注意力机制的 KV 复制中。有人可能会疑惑这不就跟tensor.unsqueeze(-1).repeat(1,1,4)差不多吗对功能是差不多但repeat的直观性碾压原生 API。特别是当你要在多个维度上同时做重复时用一句话写清楚比写一串repeat参数要安全得多。再看一个组合操作# (batch, hidden) - (batch, num_heads, hidden) repeat(x, b h - b n h, n8)这种写法在生成位置编码、扩充 batch 内的样本时非常方便。3. 深度学习实战这些场景你每天都在踩坑掌握了三个核心函数我们直接进入最常用的深度学习场景。看看 einops 如何让那些“看一眼就头大”的代码段变得优雅。3.1 图像 Patch 划分Vision Transformer 风格做 ViT 时需要把(batch, channel, height, width)的图像切分成(num_patches, patch_size, patch_size)的小块。传统写法要torch.nn.functional.unfold或者多层reshape和permute。用 einops 一行搞定import torch from einops import rearrange x torch.randn(2, 3, 32, 32) # (batch2, channels3, h32, w32) # 切成 4x4 大小的 patch输出形状为 (num_patches64, patch_size4*4*348) patches rearrange(x, b c (h p1) (w p2) - (b h w) (p1 p2 c), p14, p24) print(patches.shape) # torch.Size([64, 48])你看(h p1)就把原来的 H 拆成块数 和 每个块大小 两个维度p1通过参数指定。这种写法不仅省去了复杂的切片逻辑而且可读性极强别人一看就知道你是在做 patch 划分。3.2 多头注意力机制里的维度重排这是我在 transformer 里使用频率最高的一个场景。假设输入q的形状是(batch, seq_len, hidden_dim)需要把它拆分成多头。标准写法通常是# 传统写法 q q.view(batch, seq_len, num_heads, head_dim).transpose(1, 2)如果你前后逻辑不顺很容易记错到底是view在前还是transpose在前。用 einops 就明确多了q rearrange(q, b s (h d) - b h s d, h8)这句话直接告诉读者原始维度是b s (h d)把h和d分开然后交换到第二维。最后再拿去做注意力矩阵乘法完全不担心维度错位。同样的场景在多头注意力的末尾需要把(b, h, s, d)重新合并回(b, s, h*d)再逆变换即可q rearrange(q, b h s d - b s (h d))我个人的体会是用 einops 写注意力时代码的思路会跟着字符串走整个人都松弛下来不再在纸上画维度矩阵了。3.3 特征图池化与空间维度压缩做一个全局池化或者多尺度特征融合我经常用reduce直接操作。比如一个分类网络最后要输出(batch, channel)的 logitsfrom einops import reduce x torch.randn(2, 128, 7, 7) global_pooled reduce(x, b c h w - b c, reductionmean) print(global_pooled.shape) # torch.Size([2, 128])如果换成自适应平均池化你得写nn.AdaptiveAvgPool2d((1, 1))再 squeeze而 einops 的表达几乎已经接近自然语言直接“在除 b 和 c 外的所有维度上求平均”。此外通过括号合并还能实现“把 h 和 w 分块取平均”类似空间金字塔的效果非常灵活。3.4 位置编码的批次扩展常见的可学习位置编码形状是(seq_len, hidden_dim)。你要广播到一个 batch 的时候传统做法是unsqueeze(0).expand(batch, -1, -1)。用repeat是这样pos_emb torch.randn(50, 128) pos_emb_expanded repeat(pos_emb, s d - b s d, b4)如果你还需要把 hidden_dim 扩展到多个 head可以连着写pos_emb_expanded repeat(pos_emb, s d - b h s d, b4, h8)这种连续维度的扩展在传统写法下你要嵌套好几层 repeat而这里的字符串一眼到底且完全不存在索引错乱的问题。4. 常见问题与排查技巧实录4.1 遇到 “Shape mismatch” 报错别慌einops 的报错信息比原生框架友好得多它会直接提示你在哪个轴出现了不匹配。最常见的报错是einops.error: Shape mismatch in ...。比如x torch.randn(4, 6) rearrange(x, a b - c, c24) # 4*624这样没问题 rearrange(x, a b - c, c25) # 这里报错因为 4*6 ! 25报错会明确告诉你是a b的乘积24与目标c25不匹配。遇到这种情况先检查你自己是不是在括号里多写了一个轴或者参数传错了。有一类很隐蔽的错误是把b c h w这种顺序在字符串里写错比如把输入忘写了w导致它跑去读取不存在的轴。排查的方法很简单先打印x.shape然后对照字符串的轴名字一一确认。4.2 括号的使用是维度推导的核心einops 中括号的语义其实就是“括号里的轴合并成一个轴”。当你看到(h p1)意思是把原来的一个维度h拆成h和p1两个维度但h * p1必须等于原始维度长度。很多人一开始不习惯总觉得h明明是高度为什么还能拆打个比方假设原来高度是 32你想切成 4 个 patch每个高度 8那字符串里就是(h p1)调用时传p18einops 就能自动算出h4。只有当你同时传了h和p1且它们乘积不等于原始大小时才会报错。还有一个心得不要在左右两侧使用同名的维度却处于不同位置。比如rearrange(x, a b c - b a c)这种轴交换是允许的einops 会理解成交换顺序但如果你写了rearrange(x, a b c - a b c a)这种重复轴它一定会报错因为同一个轴不能凭空消失或凭空增加。4.3 性能与内存的那点事儿很多人担心 einops 的封装会影响性能。实测下来在 GPU 上跑大模型任务时它的性能开销和手写view、permute基本一致因为它核心就是一个轻量级的维度解析器真正执行时还是走底层的张量操作。唯一额外的开销体现在首次调用时的字符串解析这部分会做缓存后续同字符串的调用几乎没有额外成本。不过有几个细节要提醒rearrange有些情况会产生内存拷贝当顺序无法用 view 解决时会自动走 permute contiguous。这和原生permute后需要contiguous()的开销本质相同绕不开。如果你是做极致的性能优化且手写 view 能明确知道不拷贝那还是保选手写。注意别在非常高频的循环里每次传入一个动态拼接的字符串。这样缓存会失效字符串解析开销就会累积性能就会受影响。如果要在循环里用把字符串常量提前定义好。4.4 与传统 API 混用时的注意事项einops 可以和view、permute混用但返回的全新 tensor 都是创建过的。如果你在nn.Module里想让 einops 操作参与反向传播完全没问题它是纯函数式操作梯度能够正常回传。另外在 PyTorch 中如果输入 tensor 是某种带步长的非连续视图比如先切片过einops 依旧能正确操作它不会像view那样给你扔一个RuntimeError: view size is not compatible with input tensors size and stride。我习惯把 einops 视为“对任意混乱布局的最终兜底方案”不需要时刻关心那个 tensor 是不是contiguous。5. 工程化集成与最佳实践5.1 在 torch.nn.Module 中优雅使用你能在模型的forward方法里直接使用 einops 函数但为了让代码更整洁官方还提供了einops.layers.torch这样的整合层。比如Rearrange、Reduce这两个nn.Module可以直接放在nn.Sequential里import torch.nn as nn from einops.layers.torch import Rearrange model nn.Sequential( nn.Conv2d(3, 64, 3, padding1), nn.ReLU(), # 将 (b, c, h, w) 展成 (b, c*h*w)然后接全连接 Rearrange(b c h w - b (c h w)), nn.Linear(64 * 32 * 32, 10) )这样模型的每一层输入输出形状会直接体现在模型定义的字符串里对调试和阅读非常有帮助。如果你做的是 NLP 或表格数据Reduce也可以作为池化层直接嵌入。5.2 与类型注解结合提升可维护性在团队协作或大型项目中我给张量操作加上类型注解后代码的稳定性明显提高。比如from einops import rearrange def process_features(x: torch.Tensor) - torch.Tensor: # 明确标注形状 # x: (batch, dim, seq) # return: (batch, seq, dim) return rearrange(x, b d s - b s d)虽然类型注解没法约束张量的形状但字符串本身就是一种非常“活跃”的形状注释别人读代码的时候不再需要靠上下文猜测输入输出形状。5.3 多框架兼容与迁移技巧einops 不仅是 PyTorch 专用我在 TensorFlow 里也是这么用的import tensorflow as tf from einops import rearrange x tf.random.normal([2, 3, 4]) y rearrange(x, b h w - b (h w))这个特性在我做模型迁移的时候特别实用可以把一段 PyTorch 代码原封不动地贴在 TensorFlow 代码里只要张量格式一致字符串不用改。如果你在做 JAX 或 NumPy 数组实验它同样通用。所以从某种角度讲einops 甚至可以作为框架之间的“张量操作翻译器”。5.4 什么情况下不要用 einops说实话einops 并非银弹。如果只是简单地x.view(-1, 5)这种琐碎操作或者你在写一个对性能要求极其极致的内核算子频繁调用动态字符串会影响效率那直接用原生 API 就好。此外阅读 einops 代码需要付出一点点学习成本如果团队里其他成员不熟悉这个库强行秀操作反而会增加维护负担。我的建议是把它用在复杂的高维张量重排、跨框架代码共享、需要严格可读性的核心模块里其余地方按你习惯来。最后分享一个我在调试时的小习惯。我会在正式写模型前先用 einsum 或 einops 写一个“形状推导测试脚本”把每个中间张量的输出形状打印出来。一旦字符串写错报错信息会精确到轴名称这比在一堆数字里找哪个维度不匹配要省心得多。如果你现在还在为别人代码里的permute(0,2,3,1)感到头疼不妨试着用 einops 重构一下我相信你也会很快上瘾。