遥感技术

MATLAB与遥感影像处理:训练 U-Net 时 512×180 的小图跑完训练后一到 semanticseg 测试就 out of memory,是验证集太大还是 activations 的默认 batch 在作祟?

👤 为我痴狂 👁 46 阅读 ❤ 0 点赞 ➦ 0 分享 📅 2026-10-26
首页› 遥感› 遥感技术› 正文
遥感深度学习 · 工程排障 · 内存机理
MATLAB与遥感影像处理:训练 U-Net 时 512×180 的小图跑完训练后一到 semanticseg 测试就 out of memory,是验证集太大还是 activations 的默认 batch 在作祟?

一张 512×180 的遥感小图,训练阶段安然无恙,训练结束调用 semanticseg 做推理却直接 OOM。表面看是"验证集太大",实际更可能是 activations 的默认批处理策略、双精度中间张量与 GPU 显存碎片三者叠加。本文以内存账本为主线,把这条链路拆开算清楚。

关键词:语义分割 · U-Net · 显存核算 · 分块推理 · MATLAB Deep Learning Toolbox

摘要

遥感语义分割的工程现场经常出现一种"反直觉"的 OOM:训练集图像只有 512×180 量级,训练若干 epoch 正常收敛,但训练结束后对测试集调用 semanticseg 立刻报 Out of memory。多数人的第一反应是"验证集/测试集太大",于是删样本、降分辨率,问题却时好时坏。

本文提出一条贯穿全文的分析主线:把 OOM 当作一次"内存账本对不上"的审计问题,而不是"数据太多"的容量问题。围绕这条主线,本文依次核算训练与推理两个阶段的内存构成差异,指出 semanticseg 的默认 MiniBatchSize、imresize 产生的双精度张量、以及 GPU 缓存池碎片化,是三类最常被忽视的"隐性占用"。

在此基础上,本文给出可复现的诊断脚本、分块推理(tiling)的完整实现路径、输入尺寸对齐到 2n 的工程约束,以及验证集流式评估的改造方案,并对遥感大图推理的内存-精度权衡做出前瞻判断。

一、问题的重新定义:OOM 不是容量问题,是账本问题

先把现象精确化。用户描述的场景包含三个事实:(1) 训练图像尺寸约 512×180;(2) 训练过程完整跑完,未报内存错误;(3) 训练结束后调用 semanticseg 对测试数据推理时 OOM。这三个事实放在一起,本身就排除了一种最常见的解释——"数据太大"。因为如果单张 512×180 的图像在推理时都放不下,那么训练阶段前向传播同样要处理这个尺寸,理应更早崩溃。

所以真正的问题不是"图像有多大",而是训练与推理两个阶段,同一张图被"以什么方式、以什么精度、以多大的批"送进网络。本文把 GPU 显存占用拆成四本账:模型参数账、激活值账、工作区账(cuDNN 算法临时缓冲)、框架开销账(CUDA context、缓存池碎片)。OOM 的发生,本质是这四本账在推理阶段的总和超过了物理显存,而训练阶段的总和没有。

核心判断:遥感小图训练通过、推理 OOM,绝大多数情况不是验证集样本数量的问题,而是推理调用路径上的默认参数(批大小、精度、尺寸处理)与训练路径不一致所导致的"账本错位"。

这里需要给出一个准确定义。所谓激活值(activations),指网络前向传播过程中每一层输出的中间张量。在训练时,这些张量需要保留到反向传播用于计算梯度,因此训练的内存峰值通常出现在前向传播结束、反向传播开始的时刻;而在推理时,理论上中间张量可以逐层释放,内存峰值应显著低于训练。本文评述:正因为推理"理论上更省",一旦推理反而 OOM,就说明有某个环节把"本可释放"的中间结果保留了下来,或者把输入放大成了训练时从未出现过的规模。

MATLAB 的 semanticseg 函数在语义分割工作流中承担"对图像或图像数据存储执行语义分割"的职责,其签名允许指定 MiniBatchSize、ExecutionEnvironment、OutputType 等参数。官方文档对 MiniBatchSize 的描述是"用于处理的小批量大小",默认值随输入类型与版本变化。本文评述:恰恰是这个"随版本变化"的默认值,构成了跨机器、跨版本复现困难的主要来源——同一份代码在 A 机器上默认批为 1,在 B 机器上可能被自动调大。

为便于后续核算,先约定符号。设输入张量形状为 H×W×C,批大小为 B,网络第 l 层输出通道数为 Fl,空间尺寸为 hl×wl,数据类型字节数为 s(单精度 4、双精度 8)。则该层激活值占用为:

Mem_act(l) = B × F_l × h_l × w_l × s  (bytes)

全网络激活峰值 ≈ Σ_l Mem_act(l)  (训练,需保留至反传)
全网络激活峰值 ≈ max_l Mem_act(l)(推理,逐层释放的理想情况)

这个公式是全文的"账本骨架"。后续每一节,都是往这个骨架里代入具体的 B、s、hl,看哪一项在推理阶段被悄悄放大了。笔者认为,把 OOM 从"感觉数据太多"转化为"逐项核对公式中的四个因子",是解决此类问题的关键一步,也是本文区别于一般"调小 batch 试试"经验帖的地方。

二、训练阶段的内存账本:为什么 512×180 能跑通

训练能跑通,说明在训练配置下,四本账之和没有超限。要理解推理为何失败,先要看清训练阶段各本账的规模。以典型的 U-Net 编码器-解码器结构为例,512×180 的输入经过 4 次下采样后,最深层特征图空间尺寸约为 32×11,通道数通常为 512 或 1024。此时单层激活值并不大,真正的大头在浅层高分辨率特征图。

设编码器第一层输出 64 通道、空间尺寸 512×180、单精度、批大小 4,则单层激活为 4×64×512×180×4 ≈ 94.4 MB。这还只是一层。U-Net 的跳跃连接会把编码器各层输出保留到解码器对应层,因此浅层激活在整个前向过程中都无法释放。本文评述:跳跃连接是 U-Net 精度优势的来源,同时也是内存账本上"最不灵活"的一项——它把本可释放的浅层特征钉死在显存里,直到解码器用完为止。

账本项 训练阶段典型规模 是否随批大小线性增长 备注
模型参数U-Net 约 2–31 M 参数,单精度约 8–124 MB否固定开销,可忽略量级
激活值(含跳跃连接)数百 MB 至数 GB,随分辨率与批大小上升是训练需保留至反传,峰值最高
梯度与优化器状态与参数量同阶,Adam 约 3 倍参数否仅训练存在
cuDNN 工作区数十 MB 至数百 MB弱相关算法选择影响大
框架与缓存池CUDA context 数百 MB,缓存池按需增长否碎片化风险来源

表 1:训练阶段四本账的典型规模(量级为工程经验估计,具体数值随网络深度、通道数与硬件变化,非实测数据)。

训练阶段之所以"看起来"能承受,还有一个常被忽略的原因:训练循环是分批的,且 MATLAB 的 trainingOptions 默认 MiniBatchSize 通常较小(常见默认 128 对分类任务,但语义分割的像素级任务在自定义循环中往往被设为 1–8)。如果用户用的是自定义训练循环,批大小由自己控制,那么训练内存是"已知可控"的;而推理时若未显式指定批大小,就进入了"默认值不可控"的区间。

另一个训练侧的缓冲机制是 prefetch。MATLAB 的 trainingOptions 提供 'PreprocessingEnvironment' 与数据预取相关设置,用于在后台准备下一批数据以重叠 I/O 与计算。预取发生在 CPU 侧或主机内存,通常不直接吃 GPU 显存,但它会抬高主机内存峰值。本文评述:很多"OOM"报告其实混淆了主机内存与显存,排查第一步就应区分报错来自 MATLAB 的 Java 堆、系统内存还是 GPU 显存,这一点在第五节会给出具体判别方法。

综上,训练阶段的内存账本是"高但可预期"的:批大小由训练配置显式给定,精度由 single 转换保证,尺寸由数据预处理固定。推理阶段一旦打破这三条"可预期性",账本就会错位。这正是下一节要展开的内容。

三、semanticseg 的内存账本:默认批处理与双精度陷阱

3.1 默认 MiniBatchSize:被忽视的乘数因子

回到公式 Mem_act = B × F × h × w × s。批大小 B 是一个纯乘数:B 翻倍,激活值占用翻倍。训练时用户可能把 B 设为 2 或 4,推理时若 semanticseg 的默认批处理把整个 imageDatastore 或 pixelLabelDatastore 一次性送入,B 就可能等于测试集样本数。这是"验证集太大"这一直觉的真正合理内核——不是单张图大,而是批的粒度把样本数变成了乘数。

MATLAB 文档对 semanticseg 的 MiniBatchSize 参数说明为"用于处理的小批量大小",并指出其默认行为与输入类型相关。本文评述:文档给出的是"行为描述"而非"固定数值",这在工程上意味着不能假设默认值等于 1。稳妥做法是永远显式写出 'MiniBatchSize',1,把乘数因子钉死,再逐步上调以测出显存上限。

% 显式钉死批大小,排除默认值干扰
pred = semanticseg(imgTest, net, ...
    'MiniBatchSize', 1, ...
    'ExecutionEnvironment', 'gpu', ...
    'OutputType', 'uint8');

3.2 双精度陷阱:imresize 与数据类型传播

第二类隐性占用来自数据类型。MATLAB 的 imresize 在输入为 uint8 时通常返回 uint8,但一旦输入经过某些运算(如归一化、均值相减)变成 double,后续所有中间结果都会以双精度传播。代入公式:s 从 4 变成 8,激活值占用直接翻倍。若此时批大小又偏大,两个因子相乘,占用可能达到训练时的 4–8 倍。

遥感影像尤其容易踩这个坑。多光谱数据常以 uint16 存储(如 Sentinel-2 的 L1C/L2A 产品,反射率放大 10000 倍存储),读取后若做 double()/255 之类的归一化,就会把整幅图变成双精度。本文评述:正确的做法是先归一化到 [0,1] 再 single() 转换,且转换应发生在进入网络之前、且只转换一次。网络内部不应再出现 double 类型的张量。

重要提示:精度自查清单

  • 用 class(img) 确认送入网络的张量类型是 single 而非 double。
  • 用 whos 查看变量字节数,双精度是单精度的两倍。
  • 检查 imresize 前后类型是否被意外提升。
  • 确认 semanticseg 的 OutputType 为 'uint8',避免输出端再产生大张量。

3.3 GPU 缓存池碎片:看不见的"已释放但不可用"

第三类占用最难排查:显存碎片。训练结束后,GPU 上可能残留大量已分配但未归还给驱动的缓存块。MATLAB 的 GPU 支持底层依赖 CUDA,其内存分配器会缓存已释放的块以加速后续分配。当推理需要一块较大的连续显存时,即使总空闲显存足够,也可能因为找不到足够大的连续块而 OOM。这类错误的特点是:重启 MATLAB 后同样的代码能跑通。

本文评述:碎片化问题在"训练完立刻推理"的工作流中尤为突出,因为训练阶段分配过大量不同尺寸的临时缓冲,释放顺序与尺寸都不规则。工程上的缓解手段包括:推理前调用 reset(gpuDevice) 重置设备、将训练与推理拆成两个独立进程、或使用 gpuDevice([]) 显式释放后重新选择设备。需要说明的是,reset 会清空设备上所有数据,调用前必须确保网络权重已保存或已复制回 CPU。

% 训练结束后、推理开始前的显存整理(谨慎使用)
reset(gpuDevice(1));          % 清空设备缓存,代价是丢失设备上所有变量
net = net.toGPU();            % 重新把网络搬到 GPU(若网络曾驻留 GPU)

% 更温和的替代:查询可用显存,判断是否碎片化
g = gpuDevice;
fprintf('Total: %.2f GB, Available: %.2f GB\n', ...
    g.TotalMemory/1e9, g.AvailableMemory/1e9);

四、验证集到底冤不冤:预取、缓存与评估循环

现在正面回答标题中的问题:是验证集太大,还是 activations 的默认 batch 在作祟?本文的结论是——两者不是对立选项,而是同一枚硬币的两面。验证集"大"只有通过批处理才会转化为显存压力;而默认批处理之所以危险,恰恰是因为它让验证集的规模变成了乘数。真正需要追问的是:验证集在评估流程中是以什么粒度、什么时机被加载的。

4.1 数据存储的惰性加载与评估的急切加载

MATLAB 的 imageDatastore 与 pixelLabelDatastore 采用惰性加载:创建对象时只记录文件路径,不读入像素。这对内存友好。但 semanticseg 在内部会按批读取并送入网络,若批大小未限制,读入的批次就会同时驻留。本文评述:惰性加载只解决了"存储层"的内存,没有解决"计算层"的内存;OOM 发生在计算层,所以调整存储层(删样本)往往治标不治本。

另一个容易被忽视的点是评估指标的累积。语义分割常用混淆矩阵(confusion matrix)计算 IoU、F1 等指标。混淆矩阵的尺寸是 numClasses × numClasses,与图像尺寸无关,本身很小。但如果用户为了"图省事"把所有预测结果 cat 或存入一个 cell 数组再统一评估,那么内存占用会随样本数线性增长。这类占用不在 GPU 上,而在主机内存,报错信息通常是 MATLAB 的 "Out of memory. The likely cause is an infinite recursion within the program" 或 Java heap 相关提示,与 GPU OOM 的报错文本不同。

4.2 训练时的验证与训练后的测试不是一回事

很多用户在 trainingOptions 中设置了 'ValidationData' 与 'ValidationFrequency',训练过程中验证集评估正常,于是认为验证集不大。但训练中的验证评估由 trainNetwork 内部调度,其批大小与训练批大小关联,且评估后立即释放;而训练结束后手动调用 semanticseg 时,批大小、执行环境、是否累积输出都由调用者决定,两者不可类比。

判断准则:如果训练过程中的验证评估正常,而训练后手动 semanticseg OOM,那么"验证集太大"这一解释基本可以排除,问题几乎必然出在手动调用路径的参数差异上。

五、诊断路径:五步定位真实占用源

本节给出一套可复现的五步诊断流程。每一步都对应一个可证伪的假设,避免"改一改试试"的盲目调试。需要强调,以下脚本为诊断逻辑示例,具体函数名与参数请以所用 MATLAB 版本的官方文档为准。

5.1 第一步:区分报错来源

GPU OOM 的报错通常包含 "GPU"、"device"、"CUDA" 或 "out of memory on device" 等字样;主机内存 OOM 则涉及 "Java heap"、"system memory" 或 "array exceeds maximum array size preference"。两者解法完全不同,必须先分清。若报错指向主机内存,应优先检查是否把所有预测结果累积在内存中。

5.2 第二步:单张、单批、单精度最小复现

取测试集中一张图,强制 MiniBatchSize=1、single 输入、OutputType='uint8',看是否仍 OOM。若单张能过,问题就在批或累积;若单张仍 OOM,问题在输入尺寸或网络本身。

% 最小复现:单张、单批、单精度
img = imread(testList{1});
img = im2single(img);                 % 关键:转单精度
if size(img,3) == 1
    img = repmat(img,1,1,3);          % 若网络要求三通道
end
pred = semanticseg(img, net, ...
    'MiniBatchSize', 1, ...
    'ExecutionEnvironment', 'gpu', ...
    'OutputType', 'uint8');
disp('单张推理通过,问题在批处理或累积');

5.3 第三步:批大小二分搜索显存上限

在单张通过的前提下,逐步增大 MiniBatchSize(1、2、4、8……),记录每次的 gpuDevice.AvailableMemory 变化,找到临界值。这个临界值就是本机、本网络、本尺寸下的安全批上限。本文评述:二分搜索比"直接设 1"更有工程价值,因为它给出了显存与批大小的定量关系,便于后续在吞吐与内存间做权衡。

5.4 第四步:检查输入尺寸是否被隐式放大

若网络输入层要求固定尺寸(如 512×512),而原始图为 512×180,MATLAB 可能通过填充或缩放把输入变成 512×512,激活值占用随之上升。用 analyzeNetwork(net) 查看输入层尺寸,并核对预处理是否做了 imresize。这是"小图反而 OOM"最隐蔽的原因之一。

5.5 第五步:碎片化验证

重启 MATLAB,只加载网络与一张图,直接推理。若此时通过,而"训练完立刻推理"失败,则碎片化成立。此时应采用第六节的进程隔离或分块推理方案,而非继续调参。

现象 最可能原因 验证方法 对应解法
单张也 OOM输入被放大或双精度analyzeNetwork + class()尺寸对齐、single 转换
单张通过、整集 OOM默认批过大显式 MiniBatchSize=1固定批大小或分块
重启后通过显存碎片对比训练后与重启后reset(gpuDevice)、进程隔离
报错含 Java heap主机内存累积检查结果是否 cat 累积流式评估、及时 clear

表 2:四类典型现象的判别与对应解法(基于工程经验的归纳,非特定版本实测数据)。

六、工程解法一:分块推理与重叠拼接

当单张图在目标分辨率下无论如何都放不进显存时,分块推理(tiling)是标准解法。其思想是把大图切成若干小块,逐块推理后拼接。遥感领域对此并不陌生:大幅面影像的语义分割长期采用滑窗推理,相关工程实践在开源社区有大量讨论(可参考 segmentation_models.pytorch 的推理示例与 MONAI 的 sliding window inferer 文档)。

6.1 分块的两个关键参数:块尺寸与重叠

块尺寸决定单次推理的激活值规模,重叠(overlap)决定拼接处的一致性。U-Net 的感受野在边界处不完整,若块与块之间无重叠,拼接缝会出现明显的类别跳变。重叠宽度一般取网络有效感受野的一半左右。本文评述:重叠不是越大越好——重叠翻倍,推理次数约翻四倍(二维情况下),时间成本急剧上升。工程上常用 1/4 到 1/2 块尺寸的重叠,并在拼接时对重叠区取平均或投票。

function predFull = tiledSemanticSeg(img, net, tileSize, overlap, numClasses)
% 分块语义分割,重叠区投票融合
% img: H×W×C single;tileSize: [th tw];overlap: 像素数
    [H, W, ~] = size(img);
    th = tileSize(1); tw = tileSize(2);
    strideH = th - overlap; strideW = tw - overlap;
    votes = zeros(H, W, numClasses, 'single');
    counts = zeros(H, W, 1, 'single');

    for r = 1:strideH:(H-th+1)
        for c = 1:strideW:(W-tw+1)
            tile = img(r:r+th-1, c:c+tw-1, :);
            p = semanticseg(tile, net, 'MiniBatchSize', 1, ...
                'ExecutionEnvironment', 'gpu', 'OutputType', 'uint8');
            p = double(p);
            for k = 1:numClasses
                votes(r:r+th-1, c:c+tw-1, k) = ...
                    votes(r:r+th-1, c:c+tw-1, k) + (p == k);
            end
            counts(r:r+th-1, c:c+tw-1) = ...
                counts(r:r+th-1, c:c+tw-1) + 1;
        end
    end
    [~, predFull] = max(votes ./ max(counts, 1), [], 3);
    predFull = uint8(predFull);
end

上述代码为逻辑示意,未处理边界不足一块的补齐问题。实际使用时应先用 padarray 把图像补齐到块尺寸的整数倍,推理后再裁回原尺寸。本文评述:分块推理的代价是边界效应与时间开销,收益是把显存需求从"整图"降到"单块"。对于 512×180 这种本身并不大的图,分块往往不是首选——它更适合真正的大幅面遥感影像(如数千像素见方)。对小图 OOM,应优先排查批与精度,分块是兜底手段。

6.2 分块与批处理的组合边界

分块后每块仍可组成小批以提高吞吐,但批大小需重新标定。设块尺寸 256×256、批大小 4,其激活值规模与整图 512×512、批大小 1 相当。工程上应把"块尺寸×批大小"作为一个整体来调,而不是分别调。这一点在 MathWorks 语义分割官方示例 中未展开,但对内存受限场景很关键。

七、工程解法二:输入尺寸对齐与网络结构约束

U-Net 的编码器-解码器结构要求输入尺寸能被 2 的下采样次数整除,否则跳跃连接处会出现尺寸不匹配。512×180 中,180 不是 2n 的倍数(180 = 4×45,最多被 4 整除),若网络有 5 次下采样(需被 32 整除),就必须填充或缩放。MATLAB 在构建网络时可能自动处理,但处理方式(填充还是缩放)直接影响内存。

本文评述:填充(padding)保持分辨率但增加计算量,缩放(resize)降低分辨率但可能损失小目标。对遥感地物分类,小目标(如车辆、小建筑)对分辨率敏感,应优先填充而非缩放。工程上可把 180 填充到 192(32×6),仅增加 12 行,代价很小;而若缩放到 160,则损失约 11% 的垂直分辨率,可能影响精度。

处理方式 输出尺寸 激活值变化 精度影响
填充至 512×192512×192+6.7%基本无损
缩放至 512×160512×160−11.1%小目标可能损失
填充至 512×512512×512+184%无损但内存激增
不处理(若网络允许)512×180基准需网络支持非 2n 尺寸

表 3:输入尺寸处理方式对激活值与精度的影响(激活值变化按面积比例估算,为模拟计算值)。

需要特别指出,表 3 中"填充至 512×512"这一行解释了大量"小图 OOM"的案例:用户以为输入是 512×180,实际网络输入层要求 512×512,预处理悄悄做了填充,激活值按 512×512 计算,是 512×180 的 2.84 倍。本文评述:这类问题的排查成本极低(一行 analyzeNetwork 即可),但若不查,就会陷入"图明明很小为什么还 OOM"的困惑。

八、工程解法三:验证集流式评估与显存回收

若诊断确认问题在"整集一次性评估",则应改为流式评估:逐样本或逐小批推理,立即计算指标并释放中间结果,不在内存中累积预测图。这既降低主机内存,也降低 GPU 显存峰值。

% 流式评估:逐样本推理,累积混淆矩阵
numClasses = numel(classNames);
cm = zeros(numClasses, numClasses);
for i = 1:numel(testList)
    img = im2single(imread(testList{i}));
    gt  = imread(labelList{i});
    pred = semanticseg(img, net, 'MiniBatchSize', 1, ...
        'ExecutionEnvironment', 'gpu', 'OutputType', 'uint8');
    cm = cm + confusionMatrix(pred, gt, classNames);  % 自定义累积
    clear img pred;                                    % 及时释放
end
iou = cm ./ (sum(cm,1) + sum(cm,2)' - cm);             % 逐类 IoU

本文评述:流式评估的关键不是"少用内存"这个结果,而是"内存占用与样本数解耦"这个性质。一旦解耦,验证集规模就不再是 OOM 的变量,标题中的疑问也就有了明确答案——验证集大小本身不构成显存压力,构成压力的是"一次性送入"的评估方式。工程上还应配合 clear、gpuDevice 查询与必要的 reset,形成完整的内存管理习惯。

关于混淆矩阵的累积,MATLAB 的 Computer Vision Toolbox 提供 confusionMatrix 相关函数,语义分割评估也可参考 evaluateSemanticSegmentation 的用法(见 官方文档)。该函数内部同样按批处理,因此批大小参数仍需显式控制。

九、对比与局限:各方案的适用边界

前述方案各有边界,不能一概而论。本节从内存收益、精度代价、实现成本、适用场景四个维度做对比,并指出各自的局限。

方案 内存收益 精度代价 实现成本 适用场景
固定 MiniBatchSize=1高(线性)无极低首选,几乎万能
single 精度转换高(2×)可忽略极低必做
尺寸对齐(填充)中(视尺寸)基本无损低非 2n 尺寸
分块推理极高边界效应中大幅面影像
流式评估高(主机内存)无低大验证集
进程隔离 / reset中(碎片)无低训练后立即推理

🔒 复制本站文章内容需登录并达到 L3。当前:未登录

分享到

💬
微信
📷
朋友圈
🐧
QQ好友
🌐
QQ空间
👁
微博
📌
钉钉
🔗
复制链接
📑
复制图文

微信扫一扫分享

打开微信「扫一扫」,扫描二维码后在微信中分享给好友或朋友圈。

💬 评论 (0)

评论功能已关闭

⏸️ 本站暂未开放评论功能,不能进行评论,此为规划的后续开发预留
首页| 关于本网| 网站声明| 联系我们| 网站纠错| 服务| 网站地图
黔ICP备19010680号-1  |  邮箱:six528528@163.com
贵公网安备 52010302001819号
Copyright 2019-2026 http://www.databrush.com/ All rights reserved.
QQ
QQ扫一扫
Logo
DBN数据刷