资讯动态

PyTorch图像可视化:FashionMNIST网格拼接与subplot/subplots区别

发布时间:2026/10/17 1:09:52 来源:尧图企业网站定制
1. 项目思路从FashionMNIST到一张网格图做深度学习可视化时把一批样本拼到一张图里几乎是天天都会遇到的需求。早期调试图像分类模型经常要把预测错的样本攒出来“批阅”一遍看模型到底错在哪些类别上。那时候用的就是matplotlib的subplot功能一行行循环把图像往画布里铺铺完再保存成一张大图发给同组的人一起“会诊”。后来换到FashionMNIST这种数据量适中的公开数据集做Demo发现很多人卡在两步一是数据从Tensor到可显示图像的类型转换二是subplot和subplots这两个长得几乎一模一样的函数到底有什么差别。这篇内容就以FashionMNIST为例讲清楚用PyTorch加载数据、再用matplotlib把多张图片拼接成一张网格图的全流程。重点放在subplot和subplots差异的底层逻辑、以及实操中会踩到的几个典型坑。适合刚接触PyTorch和matplotlib的读者也适合写过不少可视化脚本但始终没搞明白两者区别的人。看完你会得到一个能直接跑通的脚本并且知道自己每一步在做什么。FashionMNIST是灰度服饰图像数据集每张图28×28像素共10个类别T恤、裤子、套头衫、裙子、大衣、凉鞋、衬衫、运动鞋、包、短靴。类别名称我一般会定义一个列表画图时作为标题直接显示方便核对。下面从整体设计拆起再说清subplot和subplots的根本差异最后给完整可复现的代码。1.1 为什么拿FashionMNIST做演示选FashionMNIST有三个原因。一是它足够小单张28×28的灰度图内存占用极低普通笔记本CPU也能轻松跑完加载和可视化的全流程。二是它的类别视觉差异比较明显T恤和衬衫有相似度但裤子和鞋一眼就能区分拼成网格图后观感清晰容易检验拼接效果。三是它自带官方数据加载接口不用自己写下载和解析逻辑十几行就能把训练集拉进内存。不过要注意的是FashionMNIST和MNIST一样图像数据是以Tensor形式存的取值范围经过归一化后可能在0到1之间也可能包含负值直接拿去做图像显示会出问题。完整流程里必须包含“把Tensor转成numpy数组、把范围规整到0到255或0到1、单通道灰度图要选对color map”这几个环节。很多人在第一步就翻车看到的热力图是全黑的以为数据坏了其实是显示设置不对。1.2 两种拼接思路make_grid和matplotlib图像拼接这件事PyTorch官方其实给了参考方案就是torchvision.utils.make_grid。它能把一批图像打包成一个网格Tensor再用matplotlib显示出来。这个方案代码量最少但有两个短板一是定制能力弱想给某一张子图单独加标题、加边框、改间距都很别扭二是它本质还是Tensor变换放在纯matplotlib画布上时如果想对子图做精细控制仍然要去操作坐标轴绕了一圈又回到subplot。matplotlib路线则是直接在画布上开多个子区域把每一张图用imshow放进对应区域。这条路看似代码多但灵活度最高既能统一控制也能单独修改某一格的内容。更重要的是用这条路线能把subplot和subplots的用法一并讲透。网格数量多时用subplots一把梭生成Axes数组循环往里面填图网格数量少、需要精确控制某几个位置时也可以用subplot逐个创建。这两者的取舍恰恰是项目里最值得先想清楚的事。2. subplot和subplots到底差在哪新手最容易混淆的是matplotlib.pyplot.subplot和matplotlib.pyplot.subplots名字就差一个s功能也都是创建子图为什么文档里总是强调它们不一样这个问题如果只记住“subplots返回多了个Figure对象”就算完了下次换场景还是不知道怎么选。要理解它俩的差别先得知道matplotlib的绘图模型。早期的matplotlib更偏“状态机”风格调用plt.plot、plt.imshow时会自动在最近一次创建的Axes上绘图plt.subplot就是这时期的设计产物。它负责在当前Figure上划出一块子区域并让这个区域成为“当前Axes”返回的是一个Axes对象。每次调用都需要显式传入行数、列数和第几个位置比如plt.subplot(2, 3, 4)含义是2行3列网格里的第4个子区域。它的重点在“切换当前操作对象”而不是同时管理所有子图。subplots是后面面向对象思路的产物它一次性在一张画布上开好所有你喜欢的小格子直接给你一个Figure图和一组Axes对象数组。你不需要手动维护“当前”状态拿着数组逐个访问就行。从代码风格上讲subplots更符合现代matplotlib推荐方式尤其是在“循环填图”这种场景下非常顺手。但它的返回结构有点讲究当行数或列数为1时返回的Axes是一维数组大于1时是二维数组新手经常在这一步被索引方式卡住。2.1 从状态机到面向对象用生活类比来解释subplot像是一个人在一块大画板上按编号一块块地画格子画完一块再画下一块每次都要告诉主人“我现在要画第几块”subplots则像是直接订做一块出厂就分好格的画板格子的编号已经印好了你只需要找到对应的格子往里填内容。前者方便临时切换位置后者方便整体管理和批量操作。做批量图像拼接时99%的情况用subplots因为它天然适合循环。从底层看plt.subplot背后依然通过plt.gca()的概念在起作用每次它会让新创建的Axes变成“当前Axes”之后的绘图命令不指定坐标轴就会画到它上面。而plt.subplots会把生成的Axes全部返回给你绘图时必须显式指定操作对象比如axes[0].imshow(...)。这种显式风格在多子图场景中能有效避免画错格子因为代码里每一步都在说清“我在操作哪个位置”.2.2 返回结构上的核心差异subplot返回单个Axes对象subplots返回一个Figure对象和一个Axes数组。这里的Axes数组维度取决于你设置的行列数1行N列时返回shape为(N,)的一维数组N行1列时返回shape为(N,1)的二维数组多行多列时则返回二维数组。很多报错都出在这个地方比如2行3列时axes.shape是(2,3)如果直接用for ax in axes遍历拿到的每个元素是一整行数组不是单个Axes后面调用ax.imshow就会直接报错。所以我个人习惯是定义完subplots后立刻调用axes.ravel()或axes.flatten()把数组拉平成一维再循环。这样无论行列是多少循环里拿到的都是单个Axes对象。这个习惯在代码里看起来只是多了一步但实际省掉大量排查报错的时间。如果某次画图只需要一个子图直接用plt.subplots(1,1)然后取axes[0]即可也能规避状态机带来的隐患。2.3 参数差异对实际项目的影响subplot的关键参数是nrows、ncols、index它也可以在已有画布上继续追加子图。subplots增加了很多批量控制参数比如sharex、sharey可以控制所有子图是否共享坐标轴figsize可以指定整张画布的尺寸squeeze参数能控制返回的Axes是否压缩维度。还有constrained_layout、gridspec_kw等可以精细调整子图之间的间距。FashionMNIST可视化项目里最实用的参数组合是figsize(宽, 高)加上dpi150或更高保证保存的图片清晰。有个经验如果子图之间关系紧密比如对比不同epoch的损失曲线可以令sharexTrue不仅看起来整齐而且放大查看时所有子图联动方便对齐分析但如果是图像拼接展示每个子图独立显示内容就不应该共享坐标轴否则图像坐标范围会被强制统一虽然对imshow影响不大但对后续添加刻度或者做局部放大参考会造成干扰。3. 实操用subplots完成FashionMNIST图像拼接现在进入正题用完整代码把FashionMNIST的64张图像拼接成一张8×8网格图。按流程分为环境准备、数据加载、图像预处理、网格绘图、保存输出五个环节逐一说明。3.1 环境准备与数据加载先保证基础环境齐全需要安装的是torch、torchvision、matplotlib。版本方面没有特殊要求PyTorch 1.x和2.x都能跑通matplotlib 3.4以上可以完整使用所有参数。测试环境建议用Python 3.9以上太低版本容易在类型转换上出幺蛾子。数据加载直接走torchvision.datasets.FashionMNIST设置downloadTrue即可。第一次运行会从服务器下载数据集文件到本地大概需要几十兆网络流量之后会缓存到指定root路径无需重复下载。加载完成后包一层DataLoader设置batch_size大于你想展示的数量比如取64张图就设置batch_size64这样一次迭代就能拿到足够数量的样本。需要注意的一点是FashionMNIST默认返回PIL图像还是Tensor取决于transform参数。不传transform时返回的是PIL对象传了transform.ToTensor()才会转成Tensor。为了后续能用torchvision的规范化操作通常会在transform里同时写ToTensor和Normalize两个步骤。Normalize的mean和std我常用的是0.5和0.5这样数据会被映射到[-1, 1]区间后续反归一化时用同样的值还原即可。有人喜欢沿用MNIST经典的0.1307和0.3081也能用但要保证反归一化时用的是同一组参数否则颜色会偏。import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms import matplotlib.pyplot as plt transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]) ]) train_dataset datasets.FashionMNIST( root./data, trainTrue, downloadTrue, transformtransform ) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) # 取一批数据 images, labels next(iter(train_loader))3.2 归一化、反归一化及常见坑上面用Normalize把图像像素从原始的0到255归一化到[-1, 1]区间这是PyTorch训练常用的做法。但matplotlib显示图像时期望的数据范围是有讲究的imshow支持浮点数但浮点数范围如果落在[-1, 1]它会被理解为“负数部分用特殊色彩映射”也就是说你直接把归一化后的Tensor丢给imshow图像会变得很奇怪——浅色部分正常深色部分全黑对比度完全是错乱的。坑就在这里。很多新手拿训练好的模型做可视化发现图像显示成黑白颠倒或者暗部全糊第一反应是模型有问题其实是没做反归一化。所谓反归一化就是把标准化后的数据还原到原始像素范围。对应公式是原始像素 标准化像素 × std mean。因为上面用的mean0.5, std0.5所以还原操作就是每个像素乘以0.5再加0.5得到的范围恰好在[0, 1]imshow直接显示正常。如果你换成MNIST经典参数就要按照0.3081和0.1307来还原不能混用。实现上有两种常见写法。一种简单直接img img * 0.5 0.5。另一种通用写法img img * torch.tensor(std).view(3,1,1) torch.tensor(mean).view(3,1,1)。对于单通道图像view成(1,1,1)即可。同时要记得Tensor是CHW格式通道、高、宽而imshow期望的是HWC格式高、宽、通道或者对单通道图而言直接用shape为(高, 宽)的二维数组。所以必须调用squeeze()去掉通道维。这几个转换连在一起很多人就是在这一步被绕晕的。3.3 核心绘图代码完整代码设计成8行8列展示64张图。先通过fig, axes plt.subplots完成画布创建再把axes展平逐个绘制图像并添加类别标签。每一张子图内关闭坐标轴避免网格线干扰图像本身。图像默认灰度显示colormap指定为gray出来的效果最接近黑白照片。标题用类别名显示在图像上方字体大小调小一些防止文字之间互相挤占。为了演示subplot和subplots的差异这里先给出subplots版本后面再给一个subplot版本。实际项目中绝大多数时候你只需要其中一种写法但两种都过一遍能加深理解。import torch import matplotlib.pyplot as plt # 反归一化函数 def denormalize(img_tensor): img_tensor img_tensor * 0.5 0.5 return img_tensor # 类别标签 class_names [T-shirt/top, Trouser, Pullover, Dress, Coat, Sandal, Shirt, Sneaker, Bag, Ankle boot] # 取前64张 batch_images images[:64] batch_labels labels[:64] # 关键创建8x8子图网格 num_rows, num_cols 8, 8 fig, axes plt.subplots(nrowsnum_rows, ncolsnum_cols, figsize(16, 16)) # 展平便于循环 axes axes.flatten() for idx, ax in enumerate(axes): img_tensor batch_images[idx] img denormalize(img_tensor) img img.squeeze(0).numpy() # (28, 28) ax.imshow(img, cmapgray) ax.set_title(class_names[batch_labels[idx]], fontsize8) ax.axis(off) plt.tight_layout() plt.savefig(fashion_mnist_grid.png, dpi150, bbox_inchestight) plt.show()运行这段代码后应该能看到一张8行8列的网格图每张图都是清晰的灰度服饰图像上方标注着类别名。格子之间排列有序没有坐标轴刻度干扰。3.4 nrows、ncols和figsize怎么定很多人在这一步犯选择困难症到底该设几行几列figsize设多大合适其实可遵循两个原则。第一网格总格子数大于等于你要展示的样本数否则循环写越界报错。第二每张子图的显示宽度建议不低于1.5英寸否则图像缩得太小细节看不清。比如16显示64张图figsize(16,16)每张图宽度16/82英寸观感很不错。如果只展示16张图4×4网格配figsize(8,8)每张2英寸也合适。格子数量确定后如果样本数不足比如只有62张图但网格是8×8多出的两个子图会空着。这时可以手动关闭后两个Axesax.axis(off)或者用ax.set_visible(False)。我实际遇到过有强迫症的项目甲方接受不了空白格子后来统一逻辑是要么精确设置网格数使总格子数等于样本数要么空余位置放一个“None”占位标记至少看上去是刻意留白不是代码出bug。3.5 保存图片时的几个细节plt.savefig是保存图片的标准方式但直接保存会出现两个常见问题。一是图片四周白边太大原因是Figure本身包含的空白区被一起保存进去了。解决方法是添加bbox_inchestight让保存时自动裁剪到内容边界。二是保存的图片模糊特别是网格图里每张子图本来就不大再被压缩后文字几乎看不清。解决方法是调高dpi150起步如果要打印或者放进论文里300也不为过。还有一点如果在保存前调用plt.show()某些环境下plt.show会清空画布导致保存的文件变成空白。稳妥做法是先savefig后show顺序固定住不要颠倒。另外Windows系统上有时会弹出一个独立的显示窗口如果内存吃紧可以在保存后调用plt.close(fig)释放资源。网格图画了64张子图内存占用不大但这种习惯在后续扩展到几百张时会帮你省掉不必要的崩溃。4. 常见问题排查实录自己踩过的坑整理成速查表按现象、原因、解决办法列出。这些问题是初学者到进阶过程中出现频率最高的每一项都值得收藏。很多问题看起来五花八门追到根上都是类型转换和状态管理那一套。现象原因解决办法图片全黑或全白归一化后未反归一化或channel维度未压缩先反归一化再squeeze(0)转numpy循环时报Axes对象不可迭代多行多列时axes是二维数组直接遍历拿到的是行数组先axes.flatten()或ravel()再遍历子图之间挤成一团没加tight_layout或约束布局调用plt.tight_layout()或创建fig时开constrained_layoutTrue保存的图有大量白边savefig默认保存整个Figure加bbox_inchestight图片颜色偏蓝或偏绿cmap未设为grayimshow默认用viridis显式指定cmapgray图上有坐标刻度很难看未关闭刻度显示ax.axis(off)标题被截断标题文字太长fontsize太大设置fontsize8配合tight_layout4.1 图片全黑或颜色不对这个真被我搭救过不少人。问题描述通常是“图像加载出来了但是整块都是黑的只有轮廓隐约可见”。排查路径很简单先打印出img数组的min和max如果min小于0或者max大于1说明反归一化没做对。再检查shape如果是(1,28,28)说明squeeze(0)没写。最后检查dtype如果是torch.Tensor直接传给了imshow需要先转numpy因为imshow的输入只认numpy数组、PIL图像或路径字符串不认Tensor。这三步走完90%的显示问题都能解决。另有一种情况是图片显示出来是彩色的而且是奇怪的彩色。那是cmap默认使用了viridis它是一种蓝绿色映射对灰度图显示效果非常不符合审美。显式设置cmapgray马上恢复黑白视觉效果。如果执意要保留灰度图像却还想有点高级感cmapGreys也是不错的选择效果几乎一样只是命名上更贴合语义。4.2 子图之间挤成一团当图片排列紧密时标题和图像边缘会互相重叠特别是类别名比较长的“T-shirt/top”这类显示起来容易压到旁边的图。这时优先调figsize把整体尺寸放大其次调整subplots调参中可用的gridspec_kw如wspace和hspace它控制子图之间的水平和垂直间距。通常hspace0.3就足够让标题和上面的图像分开。有人喜欢用plt.subplots_adjust也能达到目标但那是在subplots之后手动调节的参数行为类似可以理解为对自动布局的微调。比较高级的解法是用constrained_layout。创建Figure时直接传plt.subplots(constrained_layoutTrue)matplotlib会自动根据所有子图内容计算合理的间距不再需要手动调节space。代价是布局计算稍慢一些但64格以内毫无压力。遇到复杂的标题和颜色条时推荐优先用这个参数省心。4.3 axes数组维度引发的报错多行多列的情况下subplots返回的axes是一个二维数组这个设计影响了很多人。第一次写代码往往是for ax in axes去遍历结果ax拿到的是某一行所有的Axes然后再传imshow就报错。即使不报错行为也无法预料。解决方案只有一个统一套路拿到axes后立刻调用flatten()把二维展平成一维循环时逐个处理。有一种反直觉的例外是nrows1或ncols1时subplots返回的是一维数组虽然flatten()不报错但不需要单独处理。如果用了squeezeTrue呢这个参数可以让返回的Axes自动压缩掉长度为1的维度大多数情况下是合理的但建议新手不要用保持默认False让返回结果可预期。工程上忌讳函数的返回值形态随参数变化而变化因为你每次都要去想这次它返回的是一维还是二维精力全花在防意外上了。4.4 其他小问题合集图上的刻度线关闭方法虽然简单但记住axis(off)会同时关闭坐标轴线和刻度而set_visible(False)则把整个子图隐藏包括背景色结果就是那一格完全空白。两者的区别要分清楚。适合清空多余子图的场景可以统一用set_visible(False)想保留位置但隐藏内容则用axis(off)。保存中文标题时容易遇到方块字乱码这是因为matplotlib默认字体不含中文字符。通常的做法是指定支持中文的字体比如SimHei或Microsoft YaHei或者干脆用英文标签避免编码麻烦。FashionMNIST自带的就是英文类别名正好避开了这个问题也侧面说明选它做演示确实省心。5. 再往外走一步网格布局的进阶玩法64张图拼一张图只是最基础的诉求。实际项目中遇到过更复杂的需求比如给每一格加预测概率、给不同类别设置不同背景色、按训练轮次对比生成图像质量等。这些需求本质都是在subplots返回的Axes对象上做定制原理不变只是对单个Axes的操作变多了。比较实用的一种扩展是给每张子图加两行信息第一行是真实类别第二行是模型预测类别预测错误时用红色标注预测正确用绿色。实现思路是set_title里传入多行字符串配合color参数控制颜色。代码结构依然是循环遍历axes只是每个循环里多做一些分支判断。这种“出错样本分布图”在模型迭代时非常直观能快速看出哪两个类别容易被混淆。5.1 gridspec与不规则网格有些场景下所有子图等宽等高并不理想。比如第一行想放大显示几个代表性样本下面几行展示对比结果。此时subplots自带的简单网格满足不了需求要引入GridSpec。GridSpec允许你手动划分画布为不规则区域通过指定某个子图跨多行或多列来形成差异化布局。它的写法和subplots略有不同需要自己创建Figure再fig.add_subplot(GridSpec对象的具体位置)。逻辑稍显复杂但理解后在论文排版类的可视化里非常好用。如果觉得GridSpec代码量大matplotlib 3.3之后提供了subplot_mosaic接口直接用类似ASCII画图的字符串方式来描述布局。比如AA;B C表示A区域横跨上方两格B和C在下面并排。这种方式对多图布局的阅读性更强代码也更短。不过subplot_mosaic返回的是一个字典键是字符串对应的Axes和subplots的数组风格不同新手需要一个适应过程。5.2 多个网格图联动输出还有一种高频扩展需求将多个批次的图像拼成多张网格图然后按顺序保存成序列文件再用图像处理工具合成视频或者做动态对比。实现上只需在外层再套一层循环每一轮迭代取一个batch调用同一套绘图逻辑。需要注意的是每轮绘图前要关闭旧的Figure否则内存占用会随循环次数线性上升。实际测试循环1000次以上时内存增长非常明显关闭Figure后基本能保持平稳。我在这个基础上做过类似“训练数据分布演进”的动画做法很简单每隔若干个epoch采样一批重构图像保存为网格图最后用视频处理工具按时序合成。看完整个动画模型从模糊到清晰的过程一目了然比单张静态图更有说服力。这时的绘图代码和上面给出的几乎一样只是套了一层循环再做好图片文件名的序号填充即可。5.3 个人踩坑后的建议最后分享一点实操经验。画多子图时我习惯在脚本里先打印一张极简验证图只取4张图2×2网格确认数据读取、反归一化、类型转换都没问题再扩展到你需要的64张或更多。这个习惯帮我节省过大量调试时间因为问题在小规模时更容易暴露日志也比64格时清晰。还有一种记忆subplots用的是面向对象思路核心是Axes数组subplot用的是状态机思路核心是切换当前操作对象。用Python写可视化时优先选择面向对象它在复杂场景下可控性显著更好这也是matplotlib官方文档反复强调的方向。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑