ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

np.pad和F.pad怎么选?参数格式与维度语义全面对比

np.pad和F.pad怎么选?参数格式与维度语义全面对比 上个月给组里新人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, modeconstant, **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, modeconstant) print(b.shape) # (6, 7) # 写法2每个维度使用同一个(before, after)所有维度都一样 c np.pad(a, (1, 2), modeconstant) print(c.shape) # (4, 6) # 写法3最精确的嵌套元组按维度从第0维到最后一维依次给出 d np.pad(a, ((1, 1), (2, 3)), modeconstant) 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, modeconstant, valueNone)。这里的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), modeconstant) print(y.shape) # (4, 5)这个例子能清楚看到配对规则元组里第一对数字先分配给最后一维第二对数字再分配给倒数第二维。对于常见的四维NCHW张量来说pad(1, 1)只作用于最后一维Wpad(1, 1, 2, 2)作用于W和Hpad(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 它是拷贝操作不是viewF.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.catimport 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], dim1) 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)), modeconstant, constant_values0) 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), modeconstant, value0) 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%的翻译场景先把numpy的pad_width写成每个轴一个(before, after)从第0维到最后一维排好只保留尾部需要被padding的连续维度。如果numpy里某些非尾部维度也做了填充F.pad无法直接覆盖需要用torch.cat等方案绕过从最后一个维度开始倒着取每维展开成before、after两个数字按从后往前的顺序连成一串。举个NCHW数组的例子# numpy里的写法希望C维前后各1H维前2后3W维前4后5 a np.zeros((2, 3, 224, 224)) a_padded np.pad(a, ((0, 0), (1, 1), (2, 3), (4, 5)), modeconstant) 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), modeconstant) 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方向变成了前后各1H方向变成了前2后3C方向变成了前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_size1): 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), modeconstant, value0) 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用value0.5填充报错后排查半天才意识到是类型问题。np.pad的constant_values虽然没有这么严格但也要求能够和数组类型做加法或赋值运算。习惯做法是让constant_values和数组dtype保持一致float数组用0.0int数组用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相关的坑。
返回列表