☰
np.pad和F.pad怎么选?参数格式与维度语义全面对比
2026/10/4 14:41:28 网站建设 项目流程

上个月给组里新人review代码,看到他把NumPy里的pad写法原样搬进PyTorch,直接写F.pad(x, ((1, 1), (1, 1))),运行时报错不说,他自己也没想明白为什么。这种"两边都叫pad、用法却完全不一样"的坑,我从NumPy切到PyTorch那会儿也踩过不止一次。今天就把np.pad和F.pad放在一起彻底捋清楚:参数格式、维度语义、填充模式的区别、以及跨库换算时最容易出错的地方。无论你是在做数据预处理、给特征图加padding,还是写自定义算子,这篇文章都能帮你省下不少排查时间。

1. 两个pad函数:定位不同,别把它们当成同名兄弟

1.1 名字像,工作方式却完全相反

np.pad是NumPy标准库里的通用数组填充接口,你在任何numpy环境里都能用;F.pad是torch.nn.functional下的函数式接口,专门处理Tensor的边界扩展。一个服务于"数组",一个服务于"张量",连"pad这个词在两边心里的理解都不一样。

最简单的差异从调用方式就能看出来。np.pad先问的是"你要给哪些维度加边,每个维度的前后各加多少",所以它的核心参数pad_width是按数组维度的顺序、一个维度一个维度地给的。F.pad则问的是"从最后一个维度往前数,每个维度的左边和右边要加多少",它的pad参数是一个扁平的元组,从张量的最后一维往回调。

这里建议把PyTorch官方文档里那句话反复读三遍:padding sizes start from the last dimension and move backward。我第一次读的时候没太在意,后来在NCHW格式的特征图上写padding,一不留神就把宽高方向填反了,模型精度跟着崩。这种方向上的差异,是后续所有混乱的根源。

1.2 一个最容易先入为主的混淆点

很多人从numpy转过来,以为F.pad只是np.pad的tensor版本,顶多改个函数名,参数稍微变一变。实际上两者的参数结构完全不兼容,直接套用会触发以下几种情况之一:

  • pad参数格式错误,PyTorch直接抛TypeError或ValueError;
  • 参数格式恰好能被解析,但维度语义和实际想要的方向不一致。比如给四维NCHW张量传F.pad(x, (1, 1)),它只处理最后一维W,而你可能以为把H和W都填了一圈;
  • 明明在某个维度上填了边缘,结果shape完全不对,debug半天才发现是数字顺序的问题。

我见过最典型的一个例子,是有人想给特征图在H和W两个方向各加一圈像素,numpy里写np.pad(a, ((0,0),(0,0),(1,1),(1,1))),到了PyTorch里写F.pad(x, (1,1,1,1)),看似合理,实际第一对数字对应W方向,第二对数字对应H方向,恰好和numpy的书写方向反着来。代码层面没有任何报错,但你在心里把H和W的位置搞反了,整个后续下游操作就全歪了。

2. np.pad用法全拆解:逐轴给参数,从第0维开始

2.1 pad_width的三种写法

np.pad的标准签名是np.pad(array, pad_width, mode='constant', **kwargs)。最关键的是pad_width,它决定了每个轴前后各填充多少。写法有三种等价形式:

import numpy as np a = np.arange(6).reshape(2, 3) print(a) # [[0 1 2] # [3 4 5]] # 写法1:标量,所有维度前后各填2 b = np.pad(a, 2, mode='constant') print(b.shape) # (6, 7) # 写法2:每个维度使用同一个(before, after),所有维度都一样 c = np.pad(a, (1, 2), mode='constant') print(c.shape) # (4, 6) # 写法3:最精确的嵌套元组,按维度从第0维到最后一维依次给出 d = np.pad(a, ((1, 1), (2, 3)), mode='constant') print(d.shape) # (4, 8)

第三种写法最常用,因为它可以精确控制每个轴。其中第一个二元组((1, 1))作用于第0维(行方向),前填1行后填1行;第二个二元组((2, 3))作用于第1维(列方向),前填2列后填3列。这里的关键是,numpy的维度顺序和shape顺序完全一致,你心里怎么想轴,代码就怎么写,非常直观。

2.2 mode参数里容易看走眼的几个模式

np.pad支持的模式很多:constant、edge、reflect、symmetric、wrap、linear_ramp、maximum、mean、median。实际开发里最常用的是前五个,其中reflect和symmetric是最容易被混淆的一对。

用一个一维数组[1, 2, 3]分别去pad一个位置,结果如下:

  • constant:前后补常数0,得到[0, 1, 2, 3, 0];
  • edge:把边界值复制到外侧,得到[1, 1, 2, 3, 3];
  • reflect:以边界为轴反射,但不包含边界元素本身,得到[2, 1, 2, 3, 2];
  • symmetric:也是反射,但会把边界元素自己复制一份,得到[1, 1, 2, 3, 3],和edge在单层pad时恰好一样,但多层时区别就出来了,比如pad两个位置,symmetric得到[2, 1, 1, 2, 3, 3, 2, 1];
  • wrap:循环拼接,得到[3, 1, 2, 3, 1]。

其中reflect模式当填充宽度过大时会直接抛异常,比如数组长度只有3,却要reflect填充2个位置,因为反射索引已经越界。symmetric会相对宽容一些。这两种模式的适用边界,后面讲到F.pad时还会再踩一次。

2.3 np.pad的典型应用场景

我在日常项目里用np.pad最多的场景有三个:

  • 图像预处理阶段给边框扩展,类似cv2.copyMakeBorder的效果;
  • 把一批长度不一致的序列数据在数组层面padding到同一长度,方便后续批量喂给模型;
  • 在写CNN相关数据流时,先对numpy图像做空间填充,再转成tensor送进网络。

有一个使用习惯值得强调:np.pad是在CPU上运行的,返回一个新的数组,不修改原数组。如果你在数据加载流水线里高频调用,要注意它每次调用都会产生新的内存拷贝,并不是零开销操作。同时,它也不支持负的填充宽度,想裁剪就老老实实用切片语法,这一点和后面F.pad的负padding特性完全不同。

3. F.pad用法全拆解:扁平元组,从最后一维往前读

3.1 pad参数的维度配对规则

F.pad的标准签名是torch.nn.functional.pad(input, pad, mode='constant', value=None)。这里的pad是一个长度可变的元组,数字必须是偶数,并且每一对数字对应一个维度,配对顺序永远从张量的最后一维开始往前推。

看一个二维张量的例子:

import torch import torch.nn.functional as F x = torch.arange(6).reshape(2, 3).float() print(x.shape) # (2, 3) # (1, 1)对应最后一个维度(列方向),左右各填1 # (2, 0)对应倒数第二个维度(行方向),上方填2,下方不填 y = F.pad(x, (1, 1, 2, 0), mode='constant') print(y.shape) # (4, 5)

这个例子能清楚看到配对规则:元组里第一对数字先分配给最后一维,第二对数字再分配给倒数第二维。对于常见的四维NCHW张量来说,pad=(1, 1)只作用于最后一维W;pad=(1, 1, 2, 2)作用于W和H;pad=(1, 1, 2, 2, 3, 3)作用于W、H、C三个尾部维度。Batch维通常不会有人去pad,所以也基本不会用到超过6个数字的写法。

3.2 四种mode与版本注意点

F.pad支持的mode比np.pad少:constant、reflect、replicate、circular。对应关系大致是这样的:

  • constant对应numpy的constant,默认填0,可以用value指定填充值;
  • reflect对应numpy的reflect,反射但不复制边界元素;
  • replicate对应numpy的edge,复制边界元素;
  • circular对应numpy的wrap,循环拼接。

特别提醒一点:F.pad没有symmetric模式。如果你在numpy里用惯了symmetric,到了PyTorch里想找F.pad的symmetric参数,大概率会一脸茫然。想实现类似效果,通常需要自己手动处理,或者换上replicate来近似。此外,circular模式是相对较晚才加入的,老版本的PyTorch可能不支持,依赖这个模式的项目最好确认一下当前版本的官方文档。

3.3 它是拷贝操作,不是view

F.pad返回的是一个新的张量,和slice、expand这类返回视图的操作不同。它不会修改原张量,也不会以view形式共享底层存储。这一点在PyTorch生态里容易被忽略,因为大家习惯了in-place操作满天飞。

但换个角度看,这恰恰是安全的设计。在模型forward里调用F.pad,原特征图不会被意外篡改,梯度也能正常通过这个复制操作传播回来。constant填充模式下,梯度只会回传到原来有值的区域,填充区域的梯度为0,这是符合直觉的。我一般建议把F.pad当做一个"纯函数"来用:输入一个张量,输出一个新的张量,不要指望它有什么就地优化的魔法。

4. 两个pad的核心差异:一张表加三个深层原因

4.1 核心参数差异对照表

把两者最关键的差异列成一张表,比用文字反复强调更直接:

对比项np.padF.pad
所属库NumPytorch.nn.functional
操作对象ndarrayTensor,支持GPU
参数结构pad_width,每个轴一个二元组pad,扁平元组
维度顺序从第0维到最后一维从最后一维倒着往前
是否支持负值不支持,负值直接报错支持,可做裁剪
mode集合constant/edge/reflect/symmetric/wrap等constant/reflect/replicate/circular
能否填充任意维度所有轴都可以只能填充尾部连续维度
是否返回新对象是是

这个表格基本能回答绝大多数"为什么我这么写不对"的疑问。更多时候问题不是出在函数本身,而是出在跨库翻译时维度方向没对齐。

4.2 为什么PyTorch要把pad顺序反过来

可能有人会问:PyTorch为什么不能学NumPy,按从前往后的维度顺序给padding?这其实和两个库的目标定位有关。

NumPy是通用数组库,它面对的是任意维度的ndarray,每个轴地位平等,所以pad_width按shape顺序逐个轴给,最通用也最直白。PyTorch的Tensor虽然也是N维结构,但神经网络里真正需要做边界填充的,几乎永远是尾部那几个空间维度。卷积、池化、插值,默认都是对最后几维操作,NCHW也好,NHWC也好,空间的W和H都排在后两位。F.pad把"最后的维度"放在参数列表最前面,写起来和连续的空间padding操作高度契合:先写W方向,再写H方向,再写C方向,一层层从近到远。

另外还有一个实际原因:batch维和channel维在绝大多数场景下不应该存在"边界填充"这个概念。把batch从16填到32,或者给channel维前后各补几个通道,这在语义上就很奇怪,通常也不会往那个方向想。F.pad从尾部开始配对,等于在接口层面把这种不合理用法直接挡掉了。

4.3 F.pad不能填充channel维:这一点很多人没意识到

虽然F.pad通过多给几对数字可以覆盖到C维,但更准确的说法是:F.pad只能填充"尾部连续的维度",无法跳过某一维去填充更靠前的维度。也就是说,你没法只填channel维而保持W和H不动,也没法只填batch维。

如果确实需要在channel维上做填充,例如想手动增加通道数并保持空间尺寸不变,最简单的替代方案是torch.cat:

import torch x = torch.randn(2, 3, 32, 32) # 在channel维两侧各补一个零通道 c_before = torch.zeros(2, 1, 32, 32) c_after = torch.zeros(2, 1, 32, 32) x_padded = torch.cat([c_before, x, c_after], dim=1) print(x_padded.shape) # (2, 5, 32, 32)

用torch.cat做channel维填充,虽然代码没有F.pad那么简洁,但语义清楚,也不会限制只能处理尾部维度。不过要注意拼接操作会新开内存,在大特征图上反复拼接,显存开销会比较明显。

5. 实战对照:从图像预处理到NCHW特征图填充

5.1 场景A:图像加边框,numpy与torch的等价写法

假设图像在numpy里是HWC格式,要给H和W两个方向各加10像素黑边,channel维不处理:

import numpy as np img = np.random.rand(224, 224, 3) padded = np.pad(img, ((10, 10), (10, 10), (0, 0)), mode='constant', constant_values=0) print(padded.shape) # (244, 244, 3)

如果图像已经转成PyTorch的NCHW形式,同样做空间方向的填充:

import torch import torch.nn.functional as F x = torch.randn(1, 3, 224, 224) x_padded = F.pad(x, (10, 10, 10, 10), mode='constant', value=0) print(x_padded.shape) # (1, 3, 244, 244)

这两个写法的效果一致,但参数方向是反的。numpy里第一个二元组作用于H,第二个作用于W,第三个作用于C;而在F.pad里,第一对(10, 10)对应W,第二对(10, 10)对应H。初学者最容易在转写时直接照搬数字,结果发现空间方向反了。

这里再补充一个细节:如果图像数据在numpy里还是HWC,没有先转成NCHW,那么确实可以直接用np.pad加边框,之后再去转tensor。但建议先在预处理阶段想清楚最终要喂给模型的是什么布局,尽量把padding统一放到某一个体系里做,避免numpy填完再转tensor、tensor里又要重新处理一遍边框。

5.2 场景B:把np.pad写法翻译成F.pad的三步法

跨库翻译是高频需求,我总结了一个三步法,基本能覆盖90%的翻译场景:

  1. 先把numpy的pad_width写成每个轴一个(before, after),从第0维到最后一维排好;
  2. 只保留尾部需要被padding的连续维度。如果numpy里某些非尾部维度也做了填充,F.pad无法直接覆盖,需要用torch.cat等方案绕过;
  3. 从最后一个维度开始倒着取,每维展开成before、after两个数字,按从后往前的顺序连成一串。

举个NCHW数组的例子:

# numpy里的写法,希望C维前后各1,H维前2后3,W维前4后5 a = np.zeros((2, 3, 224, 224)) a_padded = np.pad(a, ((0, 0), (1, 1), (2, 3), (4, 5)), mode='constant') print(a_padded.shape) # (2, 5, 229, 233) # 对应tensor的正确翻译 t = torch.zeros(2, 3, 224, 224) t_padded = F.pad(t, (4, 5, 2, 3, 1, 1), mode='constant') print(t_padded.shape) # (2, 5, 229, 233)

注意F.pad里的第一个数字对(4, 5)给了W维,第二个数字对(2, 3)给了H维,第三个数字对(1, 1)给了C维。batch维没有出现在pad参数里,因为它位于最前面,F.pad的"尾巴填充"机制根本碰不到它。

很多人在这一步会直接写F.pad(t, (1, 1, 2, 3, 4, 5)),结果W方向变成了前后各1,H方向变成了前2后3,C方向变成了前4后5,整个空间布局错得离谱。所以翻译过程一定不能省,先写草稿再敲代码。

5.3 场景C:把F.pad放进模型forward里用

F.pad真正的价值是能嵌进神经网络前向流程中。比如自定义一个可学习边界的padding层:

import torch import torch.nn as nn import torch.nn.functional as F class LearnableBoundaryPad(nn.Module): def __init__(self, pad_size=1): super().__init__() self.pad_size = pad_size self.weight = nn.Parameter(torch.tensor(1.0)) def forward(self, x): p = self.pad_size x = F.pad(x, (p, p, p, p), mode='constant', value=0) return x * self.weight

这个模块对输入特征图在W和H两个方向各加一圈零边界,然后用一个可学习标量去缩放边界区域。F.pad在GPU上正常工作,梯度可以回传到输入x上,整个操作完全可微。如果用np.pad,数据要从GPU拷回CPU,处理完再拷回GPU,不仅慢,而且根本无法放进计算图里参与训练。

如果这个padding操作在模型里是固定不变的,也可以直接用模块化的nn.ZeroPad2d、nn.ConstantPad2d、nn.ReflectionPad2d等。它们本质上是F.pad的包装,但以Module的形式存在,更适合放进Sequential里和卷积层串联。

6. 边界场景与踩坑记录:负padding、reflect限制、dtype与版本差异

6.1 负padding等于做裁剪

F.pad允许padding值为负数,负值表示裁剪而不是填充。比如F.pad(x, (-1, -1)),执行效果就是在最后一维的左右两侧各去掉一个元素。这个特性在处理尺寸对齐时相当实用,很多需要中心裁剪或者边缘裁剪的地方,可以直接用负padding一步完成,不需要写切片逻辑。

但np.pad完全不允许负值,一旦pad_width里出现负数就直接抛ValueError。如果你习惯在numpy里用切片裁剪,那没问题;但如果是从PyTorch切回numpy写数据处理,顺手把负padding带回numpy里,就会原地报错。这两种思维之间需要有一个切换意识。

6.2 reflect模式碰小特征图就会爆

reflect模式的原理是基于边界做镜像反射,而且不包含边界元素。这意味着它需要有足够的内部元素作为反射来源。当特征图的某个维度很小,比如只有3个像素长度,你却要reflect填充2个位置,反射索引导出界,F.pad会直接报错。np.pad的reflect同样存在这个限制,真需要小尺寸特征图加边框时,换成replicate或者constant模式更稳妥。

实际项目里,最典型的场景是在低分辨率特征图上加一圈padding。比如特征图是4x4,需要外扩一圈,如果用了reflect模式,四个方向同时反射,内部像素不够用,训练过程中突然就抛异常。这种错误往往在训练跑到中途才出现,因为前面几层特征图分辨率还比较大,到了深层变小后问题才暴露。提前摸清特征图的尺寸变化曲线,能省很多事。

6.3 value参数和dtype匹配问题

F.pad的value参数必须与输入张量的dtype匹配。float张量传0没问题,传0.0更规范;int张量传0.0可能在部分版本里直接抛TypeError。我遇到过自定义数据集里整型tensor用value=0.5填充,报错后排查半天才意识到是类型问题。

np.pad的constant_values虽然没有这么严格,但也要求能够和数组类型做加法或赋值运算。习惯做法是让constant_values和数组dtype保持一致,float数组用0.0,int数组用0,避免隐式转换带来的不确定性。

6.4 pad长度上限与"少写一对数字"的空间突变

F.pad的pad元组数字个数必须是偶数,且最多不能超过输入维度数的两倍。四维张量最多给8个数字,超过直接报错;少于8个时,只有尾部对应的维度会受影响,其他维度保持不变。这个机制在生产环境里容易出"静默错误":原本想给W、H、C三个方向都填充,结果pad里少写了一对数字,编译期不报错、运行期也不报错,只是空间shape悄悄变了,下游逻辑全部跑偏。

我在排查别人代码时遇到过几次这种情况,最后的定位手段就是打印每一层前后的shape,逐层核对预期。对于模型内部的F.pad,建议在代码里加一个简单的shape断言,比如:

def pad_to_shape(x, target_w, target_h): w_pad = target_w - x.shape[-1] h_pad = target_h - x.shape[-2] # 左右均分,余数放在左边 y = F.pad(x, (w_pad // 2, w_pad - w_pad // 2, h_pad // 2, h_pad - h_pad // 2)) assert y.shape[-1] == target_w and y.shape[-2] == target_h return y

这样一旦pad参数写错,assert会在第一时间暴露问题,而不是让错误的特征图继续往后传。

6.5 numpy与pytorch跨界时的内存拷贝问题

numpy数组转tensor本身会经历一次内存拷贝,除非用torch.from_numpy共享内存。如果先np.pad再转tensor,等于数组填充一次、拷贝一次,两份开销叠加。数据量小的时候无所谓,但在高分辨率图像或者大规模预处理流水线里,这些额外拷贝会明显拖慢速度。

我的建议是,预处理阶段如果能在numpy里一步完成就都在numpy里做,进入训练循环之后,所有padding统一交给F.pad。不要一会儿np.pad一会儿F.pad混着用,否则代码可读性差,性能也难优化。另外一点,无论是np.pad还是F.pad,都是返回新对象而不是修改原对象,不要在高频循环里假设"反正只是填个边,零拷贝",该考虑内存的时候还是要考虑。

最后再分享一个我自己的使用心法。看到代码里np.pad和F.pad同时出现却找不到bug时,第一件事一定是把输入shape和pad参数写在草稿纸上。np.pad给的是"从batch到channel到高到宽"的每个轴,F.pad给的是"从宽到高到channel到batch"的数字对,两者正好是反的。只要记住这一点,后面所有mode、value、是否拷贝的问题都是小case。希望这篇对你有用,也欢迎交流你在项目里踩过的padding相关的坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询