☰
PyTorch空间变换核心:affine_grid与grid_sample原理与实战
2026/10/15 10:09:27 网站建设 项目流程

1. 为什么这两个函数总被一起提起——从图像变形的本质说起

在 PyTorch 的视觉处理流水线里,affine_grid和grid_sample是一对几乎从不单独露面的“孪生操作”。你很难在真实项目中只用其中一个:要么一起出现,要么干脆绕开整个空间变换范式,改用更粗暴的 OpenCV 函数硬拉伸。但凡你做过图像配准、风格迁移中的几何对齐、医学影像的刚体配准、或者哪怕只是训练一个带 STN(Spatial Transformer Network)模块的模型,就一定会撞上这对组合。它们不是工具箱里可选的螺丝刀,而是整套空间坐标重映射机制的“输入端”和“执行端”——一个生成变形蓝图,一个按图施工。

我第一次真正搞懂它们,是在调试一个 MRI 图像配准模型时。当时模型输出的位移场看起来很合理,但 warp 后的结果却严重错位,边缘撕裂、像素堆叠,像被揉皱又强行展平的纸。排查了三天,最后发现不是网络结构问题,而是affine_grid输出的网格张量形状没对齐grid_sample的期待——前者输出(N, 2, H, W),后者却默认要求(N, H, W, 2)。这个看似微小的通道顺序错位,直接导致所有坐标轴被倒置,x 当成了 y,y 当成了 x。这种错误不会报错,只会静默地生成完全错误的图像。它提醒我:这两个函数之间没有自动适配层,它们之间的契约是靠开发者用代码亲手缝合的,而缝合点,恰恰就是理解它们协作逻辑的钥匙。

关键词里虽然空着,但实际场景中,它们必然绑定在空间变换(spatial transformation)、可微分图像重采样(differentiable image resampling)、坐标系映射(coordinate mapping)这三个核心概念上。它们共同解决的是同一个根本问题:如何让神经网络不仅能学习“看到什么”,还能学会“怎么去看”——即主动调整观察视角、旋转角度、缩放比例,甚至做透视校正。这正是传统 CNN 所欠缺的几何鲁棒性。而 PyTorch 把这个能力封装成两个函数,不是为了增加复杂度,而是为了把“定义变换”和“执行变换”解耦,让前者可学习、后者可微分、整体可端到端训练。

所以,这篇文章不打算罗列 API 文档式的参数说明。我要带你从零重建一次它们的协作流程:从一个最简单的 2D 仿射变换数学表达开始,手推它如何被编码进affine_grid的输出张量,再看grid_sample如何用这个张量,在 GPU 上以亚像素精度完成数百万次插值计算。你会明白,为什么grid_sample的 mode 参数选bilinear而不是nearest会直接影响梯度回传的稳定性;为什么padding_mode设为zeros在训练初期可能导致边界伪影爆炸;以及,当你要处理 3D 体积数据时,那个神秘的(N, 3, D, H, W)网格张量,其内部坐标排列顺序到底遵循怎样的直觉逻辑。

这不是一次 API 学习,而是一次对 PyTorch 底层空间计算范式的实地勘察。

2. Affine grid:不是生成“网格”,而是生成“每个像素该去哪”的坐标字典

很多人被名字误导,以为affine_grid真的在内存里画出了一张物理的“网格线”。其实完全相反:它根本不产生任何可视化的网格,它产出的是一本坐标映射字典——一本厚达N × H × W页的册子,每一页上写着H × W个坐标点,每个坐标点是一个二维向量(x, y),明确告诉grid_sample:“这张图上第 i 行第 j 列这个位置,应该从原图的哪个 (x, y) 坐标取颜色”。

我们来拆解它的输入和输出。假设你有一个 batch size 为 2 的图像张量,尺寸是2 × 3 × 64 × 64(2 张 RGB 图,64×64 像素)。你想对它们做统一的 30 度顺时针旋转。那么你需要先构造一个 2×3 的仿射变换矩阵。注意,这里说的“2×3”不是笔误——PyTorch 的仿射矩阵是[a00, a01, b0; a10, a11, b1],共 6 个参数,排成 2 行 3 列。它对应的是标准的 2D 仿射变换公式:

x' = a00 * x + a01 * y + b0 y' = a10 * x + a11 * y + b1

其中(x, y)是目标图(即你希望得到的变形后图像)上的归一化坐标,范围是[-1, 1];(x', y')是原图上对应的采样坐标。这个归一化范围是关键:-1对应图像左/上边缘,+1对应右/下边缘,中心是(0, 0)。它与 OpenCV 或 PIL 中使用的像素坐标系([0, W-1],[0, H-1])有本质区别。affine_grid的全部工作,就是对目标图上每一个(x, y)点,代入上面的公式,算出它该从原图哪里取值,并把所有结果打包成一个张量。

现在,调用torch.nn.functional.affine_grid(theta, size=(2, 3, 64, 64))。theta就是那个2×2×3的变换矩阵(batch 维度在前),size指定了输出网格的形状。函数返回一个2 × 64 × 64 × 2的张量。注意这个形状:最后一个维度是 2,代表(x, y)坐标;前三个维度2 × 64 × 64正好对应 batch 和目标图的每个像素位置。你可以把它想象成一个巨大的 Excel 表格:2 个 sheet(batch),每个 sheet 有 64 行、64 列,每个单元格里填着两个数字——这就是“该像素该去哪”的答案。

提示:affine_grid的输出张量 dtype 必须是torch.float32,且设备必须与后续grid_sample的输入图像一致(同在 CPU 或同在 CUDA 上)。我曾在一个混合设备的 pipeline 里,因为affine_grid在 CPU 上运行而图像在 GPU 上,导致grid_sample报出一个极其隐晦的Expected all tensors to be on the same device错误,花了整整一个下午才定位到这个源头。

那么,这个(x, y)坐标到底是怎么算出来的?我们手动验证一下。假设theta是一个单位矩阵(即不做任何变换):

theta = torch.tensor([[[1., 0., 0.], [0., 1., 0.]]], dtype=torch.float32) # shape: 1x2x3

对一张1 × 3 × 4 × 4的小图调用affine_grid,输出grid的形状是1 × 4 × 4 × 2。我们取grid[0, 0, 0, :],也就是目标图左上角第一个像素对应的坐标。根据归一化规则,x = -1,y = -1(因为 4×4 网格,索引 (0,0) 映射到归一化坐标的左上角)。代入公式:

x' = 1*(-1) + 0*(-1) + 0 = -1 y' = 0*(-1) + 1*(-1) + 0 = -1

所以grid[0, 0, 0, :]应该是[-1., -1.]。同理,grid[0, 3, 3, :](右下角)应该是[1., 1.]。这个手动推演能让你彻底摆脱对黑盒的恐惧——affine_grid就是这样一个确定性的、可手工验证的坐标计算器。

2.1 仿射矩阵的构造:从人类直觉到机器语言的翻译

你不可能每次都手写theta。实际项目中,theta通常由网络预测,或由torch.eye(2, 3).unsqueeze(0)初始化后微调。但为了调试和理解,我们必须掌握如何从常见几何操作(旋转、缩放、平移)反推出theta。

  • 旋转(Rotation):绕图像中心逆时针旋转 θ 角度。标准旋转矩阵是:

    [cosθ, -sinθ] [sinθ, cosθ]

    但这是绕原点旋转。而图像中心在(0, 0)归一化坐标系中,所以我们需要先平移中心到原点,再旋转,再平移回去。最终的theta是:

    a00 = cosθ, a01 = -sinθ, b0 = 0 a10 = sinθ, a11 = cosθ, b1 = 0

    因为归一化坐标系已经以中心为原点,所以纯旋转不需要额外的平移项b0,b1。

  • 缩放(Scale):沿 x、y 方向分别缩放sx,sy倍。theta为:

    a00 = sx, a01 = 0, b0 = 0 a10 = 0, a11 = sy, b1 = 0
  • 平移(Translation):在归一化坐标系中,平移tx,ty。注意,tx的单位是“图像宽度的比例”。例如,tx = 0.1表示向右平移图像宽度的 10%。theta为:

    a00 = 1, a01 = 0, b0 = tx a10 = 0, a11 = 1, b1 = ty

最关键的实战技巧来了:当你需要组合多个变换时,必须按“先做后乘”的顺序拼接矩阵。比如,你想先旋转再平移,那么theta_combined = theta_translation @ theta_rotation。这是因为grid_sample最终执行的是output[i,j] = input[x', y'],而x', y'是由theta一次性计算出来的。如果你把顺序弄反,就会得到一个“先平移再旋转”的效果,这往往不是你想要的。

我曾经在一个 UI 元素对齐项目中,需要将用户手绘的草图(小图)无缝贴到一张大背景图上。我用了affine_grid来计算草图的缩放和平移。但一开始,我把scale和translate矩阵相乘的顺序搞错了,结果草图不是居中放大,而是被放大后又往右下方飞出去了。后来我画了一个 2×2 的小坐标系草图,手动追踪(0,0)这个点经过两次变换后的最终位置,才恍然大悟:矩阵乘法的顺序,就是人类执行动作的顺序。

2.2 3D 场景下的 affine_grid:从平面到立方体的坐标跃迁

当任务升级到 3D 医学影像(如 CT、MRI 体数据)时,affine_grid的接口会悄然变化。输入theta变成了N × 3 × 4的矩阵,输出grid的形状也变成了N × D × H × W × 3。这里的D是深度(切片数),3代表(x, y, z)三个坐标轴。

3D 仿射变换的数学形式是:

x' = a00*x + a01*y + a02*z + b0 y' = a10*x + a11*y + a12*z + b1 z' = a20*x + a21*y + a22*z + b2

所以theta是一个3×4的矩阵,前三列是线性变换部分,第四列是平移向量。

但这里有个极易踩的坑:PyTorch 的 3Daffine_grid对theta的期望顺序,与大多数 3D 图形学库(如 ITK、SimpleITK)不同。在 ITK 中,theta通常是4×4的齐次矩阵,而 PyTorch 只要其左上3×3和最后一列3×1,并将其拼成3×4。更重要的是,PyTorch 的归一化坐标系中,z轴的方向是:-1对应第一个切片(最“前”),+1对应最后一个切片(最“后”)。这与某些 DICOM 数据的存储顺序(z=0是最“后”切片)可能相反。如果你直接把 ITK 计算出的变换矩阵喂给affine_grid,很可能得到一个前后颠倒的 warped volume。

我的解决方案是:永远用一个已知的、简单的 3D 变换(比如只在 z 方向平移 0.5)作为测试用例。生成一个1 × 32 × 32 × 32的全 1 张量作为“原图”,然后用affine_grid+grid_sample对它做变换。如果输出的grid在z维度上显示grid[0, :, :, :, 2]的值从-1线性增加到+1,那就说明坐标系方向是符合预期的;否则,就需要在theta的b2项上加一个负号来翻转z轴。

3. Grid sample:不是“采样”,而是“亚像素级的、可微分的、批量的插值引擎”

如果说affine_grid是一个精密的坐标规划师,那么grid_sample就是一位不知疲倦的、毫秒级响应的执行官。它接收规划师给出的“每个像素该去哪”的指令(即grid张量),以及原始图像(input),然后在 GPU 上并行地、为每一个目标像素,从input中取出对应位置的颜色值。

但这里的关键在于,“对应位置”几乎从来都不是一个整数像素坐标。grid里的(x', y')是浮点数,比如(12.37, 45.89)。grid_sample的核心任务,就是在input这张离散的像素网格上,对这个浮点坐标进行插值(interpolation),估算出该点的颜色。这个过程必须是可微分的,因为整个链路要参与反向传播——网络需要知道,如果我把theta往某个方向微调一点点,output的像素值会如何变化,从而计算出精确的梯度。

grid_sample的签名是torch.nn.functional.grid_sample(input, grid, mode='bilinear', padding_mode='zeros', align_corners=True)。这四个参数,每一个都藏着影响最终效果和训练稳定性的关键细节。

3.1 Mode 参数:bilinear 是默认,但 nearest 和 bicubic 各有其命

  • mode='bilinear'(双线性插值):这是最常用、最平衡的选择。它取浮点坐标(x', y')周围最近的 4 个整数像素(即(floor(x'), floor(y')),(ceil(x'), floor(y')),(floor(x'), ceil(y')),(ceil(x'), ceil(y'))),然后根据(x', y')到这四个点的距离,进行加权平均。权重是线性的,所以叫“双线性”。它的优点是结果平滑、抗锯齿,梯度计算稳定。缺点是会引入轻微的模糊,对于需要保持边缘锐利的任务(如文本识别)可能不是最佳。

  • mode='nearest'(最近邻插值):它简单粗暴,直接取离(x', y')最近的那个整数像素的值。优点是零模糊、计算极快、保边完美。但致命缺点是:它不可微分。在(x', y')恰好位于两个像素中间时,函数值会发生突变,导数不存在。这意味着,如果你在 STN 的theta预测分支里使用了nearest,那么反向传播到theta的梯度将是零或不稳定的,网络根本无法学习如何调整变换。因此,nearest只适用于推理阶段,或者你明确知道自己不需要梯度回传的场景(比如做数据增强预处理)。

  • mode='bicubic'(双三次插值):它比双线性更高级,会参考周围 16 个像素(4×4 邻域),用三次多项式拟合。结果更锐利、细节保留更好,常用于高质量图像缩放。但在深度学习中,它很少被使用,原因有二:一是计算开销比 bilinear 大约 2-3 倍;二是其插值核(如 Mitchell-Netravali)的高阶导数可能导致梯度噪声增大,在训练不稳定时反而成为干扰源。我只在一次超分辨率项目的后处理环节用过它,用来提升最终输出图的观感,而不是在训练主干网络中。

注意:mode参数只控制“如何从input中取值”,它完全不影响grid的生成。grid依然是由affine_grid决定的,grid_sample只是忠实地执行。

3.2 Padding mode 参数:边界之外的世界,由你定义

当grid中的某个(x', y')坐标落在input图像的有效范围之外时(比如x' = 1.5,而input的归一化 x 范围是[-1, 1]),grid_sample就必须决定:这个“越界”的像素,该填什么值?padding_mode就是这个决策开关。

  • padding_mode='zeros'(零填充):这是默认值。所有越界坐标都返回 0。对于 RGB 图像,这表现为黑色;对于单通道灰度图,就是纯黑。它简单直接,但有一个隐患:在训练初期,theta预测不准,大量像素被 warp 到图像外,output中会出现大片黑色区域。这些黑色区域的梯度是零(因为 0 是常数),会导致网络在这些区域“学不到东西”,收敛变慢。

  • padding_mode='border'(边界填充):越界坐标会被“拉回”到图像的最近边界上。比如x' = 1.5,会被强制设为x' = 1.0,取最右边一列的像素值。这能有效避免大片黑色,让output看起来更“完整”,训练更稳定。但它会人为地在边界处制造出一条“硬边”,可能影响模型对真实边界的感知。

  • padding_mode='reflection'(反射填充):这是最“自然”的选择。它把图像边界当作一面镜子,越界坐标会被镜像反射回来。例如,x' = 1.2会被映射到x' = 0.8(因为1.0是边界,1.2超出0.2,所以反射到1.0 - 0.2 = 0.8)。这能产生无缝的、无硬边的填充效果,特别适合纹理合成、图像修复等任务。但它的计算稍复杂,且在align_corners=False时行为会略有不同。

我通常的做法是:在训练 STN 模块时,首选padding_mode='border',因为它能提供最稳定的梯度信号;在最终部署或生成高质量结果时,切换到padding_mode='reflection',以获得更自然的视觉效果。

3.3 Align corners 参数:一个布尔值引发的坐标系战争

这是grid_sample中最微妙、也最容易被忽视的参数,但它能彻底改变grid的语义。它的作用是:决定归一化坐标系的[-1, 1]区间,是映射到像素的“中心”还是“边缘”。

  • align_corners=True(默认):-1映射到input的第一个像素的中心,+1映射到input的最后一个像素的中心。这是最符合直觉的设定,也是绝大多数教程和文档所采用的。例如,一个4×4的input,其像素中心坐标在归一化系下是(-1, -1), (-1, -0.33), (-1, 0.33), (-1, 1), ...,等等。

  • align_corners=False:-1映射到input的第一个像素的左上角,+1映射到input的最后一个像素的右下角。这意味着,input的整个像素“方块”被严格地、无间隙地铺满在[-1, 1] × [-1, 1]的正方形内。此时,一个4×4的input,其第一个像素覆盖的归一化区域是[-1, -0.5) × [-1, -0.5),最后一个像素覆盖的是[0.5, 1] × [0.5, 1]。

这个区别在grid的数值上体现得淋漓尽致。还是用1 × 3 × 4 × 4的例子,affine_grid的输出在两种模式下是不同的。当align_corners=True时,grid[0, 0, 0, :]是(-1, -1);而当align_corners=False时,它会是(-1, -1)吗?不,它会是(-1.25, -1.25)!因为align_corners=False下,input的“有效范围”实际上被扩展了,以容纳整个像素方块。

这个参数必须与affine_grid的调用严格匹配。affine_grid函数本身也有一个align_corners参数,其默认值也是True。如果你在grid_sample中设为False,却忘了在affine_grid中也设为False,那么grid的坐标和input的像素布局就完全对不上了,结果会是一团乱码。我见过太多人栽在这个坑里,调试时打印出grid的 min/max 值,发现它超出了[-1, 1]的范围,第一反应是theta写错了,殊不知只是align_corners没对齐。

4. 完整实战:从零构建一个可训练的空间变换模块(STN)

现在,让我们把前面所有的知识点,组装成一个真正可用的、端到端可训练的模块。我们将实现一个简化版的 Spatial Transformer Network(STN),它能学习对输入图像进行任意的 2D 仿射变换,以提升模型对几何形变的鲁棒性。

4.1 模块设计:一个三层的“变换预测器”

STN 的核心是一个小型的、可学习的子网络,我们称之为LocalizationNet。它接收原始图像,输出一个2×3的仿射变换矩阵theta。为了保证theta的表达能力,我们让它输出 6 个自由参数,然后 reshape 成2×3。

import torch import torch.nn as nn import torch.nn.functional as F class LocalizationNet(nn.Module): def __init__(self, in_channels=3): super().__init__() # 一个非常轻量的 CNN,只用两层卷积和一层全连接 self.conv1 = nn.Conv2d(in_channels, 8, kernel_size=7) self.conv2 = nn.Conv2d(8, 10, kernel_size=5) self.fc1 = nn.Linear(10 * 50 * 50, 32) # 假设输入是 64x64,经过 conv 后是 50x50 self.fc2 = nn.Linear(32, 6) # 输出 6 个参数,用于构建 2x3 theta def forward(self, x): x = F.relu(self.conv1(x)) x = F.max_pool2d(x, 2) x = F.relu(self.conv2(x)) x = F.max_pool2d(x, 2) x = x.view(x.size(0), -1) # flatten x = F.relu(self.fc1(x)) x = self.fc2(x) # 将 6 个参数 reshape 成 batch x 2 x 3 theta = x.view(-1, 2, 3) return theta

这个LocalizationNet的设计原则是:足够简单,避免过拟合;足够灵活,能表达基本的几何变换。它不追求高精度,只负责给出一个合理的初始变换方向。

4.2 前向传播:affine_grid 与 grid_sample 的精准握手

STN 的前向传播,就是LocalizationNet、affine_grid和grid_sample三者的流水线作业。

class STN(nn.Module): def __init__(self, in_channels=3): super().__init__() self.localization = LocalizationNet(in_channels) def forward(self, x): # Step 1: 用 localization net 预测 theta theta = self.localization(x) # shape: (N, 2, 3) # Step 2: 用 theta 和目标图尺寸,生成采样网格 # 注意:grid_sample 的输入图尺寸,就是我们要 warp 成的目标尺寸 # 这里我们保持和输入相同,所以 size = x.size() grid = F.affine_grid(theta, x.size(), align_corners=True) # Step 3: 用 grid 对 x 进行重采样 # 这里我们显式指定所有关键参数,确保行为可预测 x_transformed = F.grid_sample( x, grid, mode='bilinear', padding_mode='border', align_corners=True ) return x_transformed

这段代码看似简单,但每一行都蕴含着我们前面讨论的所有细节。F.affine_grid(theta, x.size())生成了(N, H, W, 2)的网格;F.grid_sample(x, grid)则用这个网格,对x进行双线性插值。padding_mode='border'确保了即使theta预测有偏差,输出也不会出现大片黑色,梯度依然能有效回传。

4.3 训练技巧:如何让 STN 真正学会“看”

仅仅把 STN 插入主干网络是不够的。为了让LocalizationNet真正学到有用的变换,你需要精心设计训练策略。

  • 损失函数的设计:STN 本身不直接产生分类损失。它的价值体现在下游任务上。所以,你的总损失应该是loss_total = loss_classification + lambda * loss_stn。其中loss_stn可以是一个正则项,比如theta与单位矩阵的 L2 距离:torch.mean((theta - torch.eye(2, 3).unsqueeze(0).to(theta.device)) ** 2)。lambda是一个超参数,通常设为0.001。这个正则项鼓励网络只在必要时才进行变换,避免过度扭曲。

  • 学习率的分离:LocalizationNet的参数通常需要比主干网络更小的学习率。因为它的任务是精细的几何调整,而不是特征提取。你可以这样设置优化器:

    optimizer = torch.optim.Adam([ {'params': model.backbone.parameters(), 'lr': 1e-4}, {'params': model.stn.localization.parameters(), 'lr': 1e-5} # 更小的学习率 ])
  • 可视化调试:在训练过程中,定期保存grid的可视化图。你可以取grid[0, :, :, 0](x 坐标图)和grid[0, :, :, 1](y 坐标图),用matplotlib画出来。一个健康的grid,其 x 图应该是一个从左到右、从-1到+1的渐变色块;y 图是从上到下、从-1到+1的渐变。如果出现大片纯色或剧烈跳变,说明theta的预测已经崩溃。

我曾经在一个细粒度图像分类项目中,将 STN 加入 ResNet-18。最初,模型的 top-1 准确率只提升了 0.3%,远低于预期。通过可视化grid,我发现LocalizationNet几乎没有学习到任何有意义的变换,theta一直非常接近单位矩阵。问题出在loss_stn的lambda太大,正则项压制了所有变换。将lambda从0.01降到0.0001后,grid开始出现清晰的、与物体姿态匹配的扭曲模式,最终准确率提升了 2.1%。

5. 高级应用与避坑指南:那些文档里不会写的实战经验

掌握了基础用法后,真正的挑战才刚刚开始。在复杂的生产环境中,affine_grid和grid_sample会以各种意想不到的方式给你“惊喜”。

5.1 性能瓶颈分析:GPU 显存与计算的双重压力

grid_sample是一个计算密集型操作。它的显存占用主要来自grid张量本身。一个16 × 3 × 256 × 256的输入,其grid的形状是16 × 256 × 256 × 2,dtype 为float32,仅此一项就占用了16 * 256 * 256 * 2 * 4 / (1024**2) ≈ 200 MB的显存。这还不包括input和output。当 batch size 或图像尺寸进一步增大时,显存很容易成为瓶颈。

我的优化策略是:

  • 梯度检查点(Gradient Checkpointing):如果LocalizationNet比较深,可以对其使用torch.utils.checkpoint.checkpoint,牺牲一点计算时间,换取显存的大幅节省。
  • 降低grid精度:在某些对精度要求不高的场景(如实时视频流处理),可以将grid的 dtype 从float32降为float16。grid_sample支持half输入,能立竿见影地减半显存占用。但要注意,float16的数值范围较小,如果theta导致grid坐标超出[-65504, 65504],就会溢出为inf,导致整个output变成 NaN。

5.2 多尺度 warp:如何在特征图上做空间变换

grid_sample不仅能处理原始图像,还能处理任意的特征图(feature map)。这是 STN 能嵌入到 CNN 任意层的关键。但这里有一个陷阱:特征图的H和W通常比原始图像小得多,而affine_grid的size参数,必须与你希望grid_sample输出的特征图尺寸严格一致。

假设你有一个 backbone 的中间层输出feat,其形状是16 × 64 × 32 × 32(16 张图,64 个通道,32×32 特征图)。你想对这个feat做 warp。那么,你不能用affine_grid(theta, feat.size()),因为feat.size()返回的是torch.Size([16, 64, 32, 32]),而affine_grid期望的size是(N, C, H, W),其中C是通道数,但它只关心N, H, W,C会被忽略。所以正确的写法是:

# 错误!feat.size() 包含了通道维度 64,但 affine_grid 只需要 (N, H, W) 的信息 # grid = F.affine_grid(theta, feat.size()) # 正确!显式构造 size,只包含 N, H, W grid = F.affine_grid(theta, (feat.size(0), feat.size(1), feat.size(2), feat.size(3))) # 或者更简洁地 grid = F.affine_grid(theta, feat.shape)

5.3 与 torchvision.transforms 的协同:不要混用

新手常犯的一个错误,是试图把affine_grid/grid_sample和torchvision.transforms.RandomAffine混用。RandomAffine是一个数据增强工具,它在 CPU 上、在DataLoader的 worker 进程中,对每张图片做随机变换,然后送入 GPU。而affine_grid/grid_sample是一个可微分的、在 GPU 上运行的、模型的一部分。

它们的哲学完全不同:前者是静态的、预处理的、不可学习的;后者是动态的、可学习的、端到端的。把它们混在一起,不仅没有意义,还会因为设备不一致(CPU vs GPU)和数据类型不一致(PIL.Image vs torch.Tensor)导致各种难以调试的错误。

我的建议是:在数据加载阶段,只做最基础的、必要的预处理(如ToTensor,Normalize);所有需要学习的、与模型逻辑强相关的空间变换,一律放在模型内部,用affine_grid和grid_sample实现。

5.4 最后的忠告:理解,而非记忆

写完这篇长文,我最

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

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

立即咨询