资讯动态

用Matplotlib Pyplot可视化MNIST手写数字:从数据加载到批量绘图

发布时间:2026/10/19 13:19:30 来源:尧图企业网站定制
做机器学习的头一个月几乎所有人都会撞上MNIST。60000张训练图片、10000张测试图片每张都是28×28的灰度像素这个数据集差不多是入门阶段的“标配”。但真正让我意识到问题的是第一次跑完训练代码控制台里打印出一堆标签值我盯着数字“7”发了半天呆——模型到底在“看”什么直到某一天我用pyplot把其中一张图片真正渲染出来才瞬间理解了那句“先看数据”在说什么。这篇文章只围绕一件非常具体的小事用matplotlib的pyplot模块把MNIST数据集里的手写数字画出来。不牵扯模型结构不做训练目标很直接——把数据加载进来把图片显示到屏幕上把标签对上去把布局调舒服。适合刚入门机器学习、想把数据看清楚的同学也适合那些模型已经跑通但从来没认真看过原始输入的读者。文章里的代码都很短但每一行都值得琢磨因为可视化这套思路后面画损失曲线、特征分布、混淆矩阵时统统还能复用。1. 项目概述与可视化设计思路1.1 MNIST到底是什么为什么都拿它练手MNIST全称是Modified National Institute of Standards and Technology手写数字数据库可以理解为“美国某标准化机构手写样本的改良版”。它由0到9共十个类别的手写数字图片组成每张图片尺寸统一为28像素高、28像素宽并且是单通道灰度图——也就是说每张图片本质上就是一个28行28列的整数矩阵矩阵里的每个值代表该位置的像素亮度0表示纯黑背景255表示纯白笔画中间数值就是深浅不一的灰色过渡。这个数据集被分成两份训练集60000张测试集10000张。训练集用来让模型学习“数字长什么样”测试集用来检验模型学到的规律能不能泛化到没见过的新样本上。对于入门者来说MNIST几乎是完美的练手对象数据量不会大到内存爆炸图片分辨率低到随便一台电脑都能处理类别均衡且标注质量高拿来学图像分类、特征提取、数据可视化都非常顺手。更关键的是这个数据集的格式极其规整不需要你做任何清洗、裁剪、对齐工作下载下来就能直接用——这正好给了初学者一个纯粹的环境把注意力全部放在“怎么处理数据”“怎么理解模型”上。不过话说回来MNIST也有它的“时代局限”。28×28的分辨率放到今天来看确实太低笔画细节丢失很多所以它更适合作为教学工具而不是衡量现代算法能力的标尺。但这恰恰不影响我们拿它做可视化练习图片分辨率越低单张图片包含的信息量就越少画出来反而更容易看清数据结构本身。1.2 为什么第一步要先做可视化很多新手把数据加载进来之后习惯直接print一下数组就开写模型。我强烈不建议这样。以MNIST为例你看到的会是这样一份输出[[ 0 0 0 0 0 0 0 0 0 0] [ 0 0 0 0 0 0 0 0 0 0] [ 0 0 0 0 0 0 0 0 0 0] ... [ 0 0 0 0 0 0 0 0 0 0]]几百个数字挤在一起你根本看不出它到底是个“3”还是个“8”。数字和图像之间差了一个“感知翻译层”。人脑对图像的识别能力远强于对纯数字的解析能力同样的信息换一种呈现方式理解成本完全不一样。可视化在机器学习项目里的价值主要体现在三个层面。第一是数据质量检查画出来能直接发现标签错位、图像损坏、像素值异常这些问题。第二是建立直觉比如通过绘制多张图片你会对“笔画粗细”“倾斜角度”“像素值分布”这类抽象概念产生具体感受后面调参时就有方向。第三是结果验证模型训练完把预测结果和真实图片并排画出来一眼就能看出模型在哪些样本上犯了错这比盯着loss曲线猜要高效得多。这个项目只做可视化不做模型本质上就是在给整个机器学习流程打地基。地基稳了后面盖楼才踏实。1.3 为什么选pyplot而不是其他工具Python里显示图片的手段其实不少PIL可以直接打开图片OpenCV也能imshow甚至纯numpy配合终端字符画都能“伪可视化”。但我做这个项目时毫不犹豫选了matplotlib.pyplot原因有三。第一生态无缝衔接。MNIST数据集加载出来就是numpy数组而pyplot的核心数据结构就是numpy数组两者之间没有任何格式转换成本。画完图片之后还能继续用同一套API画直方图、散点图、训练曲线一站式解决。第二使用成本极低。pyplot的imshow函数专门用于显示二维数组数据一行代码就能把28×28矩阵变成图片。对于只想快速查看数据样本的人这个“低门槛”非常重要不需要研究图像格式、通道顺序这类OpenCV里绕不开的细节。第三与Jupyter配合完美。在Jupyter Notebook里matplotlib支持内嵌渲染图片直接出现在单元格下方不用额外弹窗。这个体验对做数据分析、机器学习实验的人来说是刚需——你要的是“看一眼数据”而不是“打开一个窗口再关闭一个窗口”。当然pyplot也有它的局限比如它的imshow本质上是把数组映射成画布上的色块不是真正的图像解码所以不适合做复杂图像处理。但在“可视化MNIST手写数字”这个任务里它就是一个无可替代的工具。1.4 整体方案梳理在做任何项目之前我都会先在脑子里过一遍完整流程这个项目也不例外。整体可以拆成四步第一步准备环境安装numpy、matplotlib以及一个能加载MNIST的数据源库第二步加载数据集并把数据切成“图片部分”和“标签部分”两块第三步用imshow绘制单张图片掌握最简单的可视化操作第四步扩展到批量绘制把一个网格里同时展示多张图片并加上标签信息。这套流程从“点”到“面”从单张到批量符合人认识事物的基本规律。而且每一步都建立在前一步的基础上中间任何一步出了问题排查范围都很小。下面我按这个顺序逐步展开。2. 环境准备与数据集加载2.1 依赖安装与推荐环境这个项目的代码量不大对硬件没有任何要求但环境准备工作还是值得认真对待。我在实际做的时候通常会在虚拟环境里安装齐这几样东西numpyMNIST数据集的底层载体所有图片数据都是numpy数组matplotlib提供pyplot模块负责所有绘图工作数据源库任选一个能下载MNIST的库比如keras、torchvision或scikit-learn。安装命令很简单用pip一行搞定pip install numpy matplotlib scikit-learn如果你后面计划用某个深度学习框架做训练也可以顺带着把框架装好比如pip install tensorflow pip install torch torchvision这里有个值得注意的点matplotlib版本差异可能会影响部分API行为。比如老版本里imshow的interpolation参数默认值是bilinear而新版本改成了antialiased。这种细节平时注意不到但当你照着网上老教程写代码发现显示效果不一样时多半是版本差异造成的。我自己习惯固定使用较新的稳定版本并且在项目根目录放一份requirements.txt方便复现环境。2.2 三种常见的MNIST加载方式对比加载MNIST数据集的姿势很多我把最常用的三种整理成了一张表方便你按自己的技术栈选择加载方式依赖库返回格式推荐场景keras.datasets.mnist.load_data()keras / tensorflow(x_train, y_train), (x_test, y_test)均为numpy数组最省事开箱即用torchvision.datasets.MNISTtorchvision torchDataset对象需配合DataLoader或手动取样本已经在用PyTorch做训练时sklearn.datasets.fetch_openml(mnist_784)scikit-learn图片摊平成784维向量标签为DataFrame列想顺便做特征工程时先说最常见的keras方式。这个接口封装得最彻底直接返回训练集和测试集两个元组并且图片已经处理成(数量, 28, 28)的三维数组不需要任何额外转换from keras.datasets import mnist (x_train, y_train), (x_test, y_test) mnist.load_data()如果你当前环境没有装keras也可以考虑torchvision。它的MNIST类功能更丰富支持在线下载、自动转换为Tensor还支持对数据集做transform增强。但注意它返回的是Dataset对象不是直接的numpy数组。如果你只是为了可视化还得手动从中取出图片和标签稍微绕了一点。from torchvision import datasets, transforms mnist_dataset datasets.MNIST( root./data, trainTrue, downloadTrue, transformtransforms.ToTensor() ) sample_img, sample_label mnist_dataset[0]还有第三种方式sklearn.datasets.fetch_openml。这个接口把每张图片都拉平成784维向量并且标签以字符串DataFrame形式存储可视化之前需要做一些reshape操作。它适合做传统机器学习算法实验但在“画图”这个任务里反而多了转换步骤from sklearn.datasets import fetch_openml X, y fetch_openml(mnist_784, version1, return_X_yTrue, as_frameFalse) X X.reshape(-1, 28, 28) y y.astype(int)三种方式没有绝对优劣取决于你后续要不要跑模型。这个项目本身只关心可视化所以我建议选keras方式代码最简洁省下来的精力全部放到画图上面。2.3 加载后一定要检查的四个关键信息数据加载成功不代表万事大吉。我在拿到数据集后第一件事永远是检查四个信息形状、数据类型、取值范围、标签分布。这四样确认无误后面的操作才不会翻车。print(x_train.shape) # 期望输出: (60000, 28, 28) print(x_train.dtype) # 期望输出: uint8 print(x_train.min(), x_train.max()) # 期望输出: 0 255 print(y_train.shape) # 期望输出: (60000,)x_train.shape告诉你训练集有60000张图片每张是28行28列的二维矩阵dtype是uint8说明每个像素值占8位范围是0到255y_train则是每个样本对应的真实标签表示这张图片里手写的是哪个数字。这些信息看着简单但它们直接影响后面画图的效果。比如说如果你发现数据加载后是float类型值范围是0到1那说明有人帮你做过归一化如果你发现是uint8那画图时imshow会按0到255的原始范围去解释颜色。两种情况下图像的“观感”会不一样但不影响可视化操作本身。养成检查数据信息的习惯后面排查问题时能省去很多无谓的猜测。3. 绘制单张手写数字3.1 imshow到底做了什么可视化的核心是matplotlib的imshow函数。很多人第一次用的时候只知道“它能显示图片”但不清楚它背后做了什么。简单来说imshow接收一个二维数组然后把这个数组里的每一个数值映射成画布上对应位置的一个色块最后拼接成一幅完整的图像。MNIST的单张图片正好就是一个28×28的二维数组可以直接喂给imshowimport matplotlib.pyplot as plt # 取训练集第一张图片和它的标签 sample_image x_train[0] sample_label y_train[0] plt.imshow(sample_image) plt.show()如果不加任何额外参数你会看到一张颜色奇特的图片——背景是蓝绿色的数字笔画是黄绿色的。这不是数据有问题而是imshow默认使用viridis色彩映射简单说就是“用颜色深浅来表达数值大小”。0对应紫色255对应黄色整张图自然就显得花里胡哨。手写数字是灰度图像所以正确的姿势是指定cmapgrayplt.imshow(sample_image, cmapgray) plt.show()这样0对应黑色255对应白色画出来就是一张白底黑字的经典手写数字图。实际项目中我也会偶尔用cmapgray_r它是gray的反转版本会把背景变成黑色、笔画变成白色。很多论文里的MNIST配图就是这种黑底风格视觉冲击力更强。两种显示方式反映的数据完全相同纯粹是审美选择。3.2 隐藏刻度、调整坐标轴默认情况下imshow会在图片周围画上一圈坐标轴刻度从0到27排列。对于28×28的小图来说这些刻度既占空间又没有实际意义反而干扰视觉。大部分展示场景下我会把坐标轴整个关掉plt.imshow(x_train[0], cmapgray) plt.axis(off) plt.show()axis(off)会同时隐藏坐标轴、刻度和边框图片直接铺满整个绘图区域。如果你希望保留边框但去掉刻度也可以单独操作plt.xticks([]) plt.yticks([])另一个值得关注的是图像的长宽比例。28×28本身是正方形但matplotlib默认的画布可能不是正方形导致图片被拉伸。稳妥的做法是设置aspect参数plt.imshow(x_train[0], cmapgray, aspectequal)aspectequal强制横纵方向一个单位等长保证数字不会被拉宽或压扁。不过直白地讲MNIST图片尺寸小视觉上的拉伸感并不明显这个参数在绘制更大地图、遥感影像时才是must-have。3.3 单张图片绘制的完整示例把前面几步组合起来单张图片的绘制代码一共就六行import matplotlib.pyplot as plt from keras.datasets import mnist (x_train, y_train), (_, _) mnist.load_data() plt.imshow(x_train[0], cmapgray) plt.title(fLabel: {y_train[0]}) plt.axis(off) plt.show()运行后你会看到一张标有“Label: 5”的手写数字图片。如果训练集顺序没有被随机打乱第一张往往是5。这里用了一个f-string把标签动态拼进标题里后面换其他样本时不需要修改代码。有一个细节值得记住如果你是在Jupyter Notebook里运行推荐在代码开头加上一句%matplotlib inline这行魔法命令会把matplotlib的渲染模式设为“内嵌显示”图片直接显示在单元格正下方不需要弹窗。如果不加图片也可能会显示但有时会因为后端设置问题出现“不显示”或“弹窗一闪而过”的情况。加上这行省心很多。4. 批量可视化手写数字4.1 用subplots搭建网格布局单张图片画出来后下一步自然是批量查看。手里有60000张图不可能一张张单独调用imshow这时候就要用plt.subplots创建一组子图在一个画布里整齐地展示多张图片。subplots的核心用法是fig, axes plt.subplots(2, 5, figsize(12, 5))这行代码创建了一个2行5列的子图网格参数figsize控制整个画布的大小。返回值有两个fig是整张画布对象axes则是一个2×5的numpy数组里面装着10个独立的坐标轴对象每个坐标轴就对应一个子图。接下来的思路是遍历axes数组把每张图片填充到对应的子图里for i, ax in enumerate(axes.flat): ax.imshow(x_train[i], cmapgray) ax.set_title(fLabel: {y_train[i]}) ax.axis(off)axes.flat把二维数组“拍平”成一维序列这样就能用单个循环依次访问10个子图。i会依次取0到9对应训练集前10张图片。imshow负责画图set_title设置该子图的标题axis(off)隐藏坐标轴。循环结束再调用plt.tight_layout()自动调整子图间距避免标题重叠最后plt.show()统一显示。4.2 一次展示10个数字的完整实现把上面两步合在一起批量可视化的完整代码非常简单import matplotlib.pyplot as plt from keras.datasets import mnist (x_train, y_train), (_, _) mnist.load_data() fig, axes plt.subplots(2, 5, figsize(12, 5)) for i, ax in enumerate(axes.flat): ax.imshow(x_train[i], cmapgray) ax.set_title(fLabel: {y_train[i]}, fontsize14) ax.axis(off) plt.tight_layout() plt.show()运行后你会看到10张手写数字整齐排成2行5列每张图下方标着对应的真实标签。如果数据经过洗牌你会看到不同标签的图片交错排列如果没洗牌最开始的图片可能是少数几个类别重复出现。这个现象本身就说明了一个问题训练前做数据洗牌很重要否则模型可能学到“顺序偏见”。批量可视化还有几个进阶玩法。比如随机选择样本而不是固定取前10张import numpy as np indices np.random.randint(0, len(x_train), size10) for i, ax in zip(indices, axes.flat): ax.imshow(x_train[i], cmapgray) ax.set_title(fLabel: {y_train[i]}) ax.axis(off)用np.random.randint随机抽10个索引每次运行时看到的样本都不同。这个方法非常适合快速扫一眼整个数据集的风格避免只盯着前几张图形成刻板印象。4.3 把图片保存到本地在Notebook里看图是一回事把图存成文件分享给同事、写进文档是另一回事。matplotlib的savefig函数提供了非常简单的保存方案plt.savefig(mnist_samples.png, dpi150, bbox_inchestight)savefig要放在show之前调用否则在Jupyter里图片已经被渲染清空保存出来的可能是空白图。dpi参数控制输出清晰度数值越大图片越精细bbox_inchestight会把图片的空白边缘裁剪掉让最终文件内容更紧凑。如果你要保存的是一张特别轻微的图大概率是分辨率不够或者色彩映射没设置对。检查一下dpi和cmap两个参数问题基本都能解决。保存成PNG格式会比较合适支持无损压缩且文件体积小SVG格式适合后续在矢量软件里继续编辑PDF格式则适合直接放进论文或报告。5. 挖掘标签分布信息5.1 统计每个类别的样本数量可视化不仅限于看图片本身标签分布同样值得画出来。MNIST是0到9的十分类问题每个类别的样本数量是否均衡会在很大程度上影响模型训练。假如某个类别只有几百张图而另一个类别有几万张模型大概率会偏向样本多的类别。统计数量最简洁的方式是使用numpy的bincount函数import numpy as np train_counts np.bincount(y_train) print(train_counts)输出结果大致是[5923 6742 5958 6131 5842 5421 5918 6265 5851 5949]这里第一个数字5923代表标签为0的图片有5923张第二个6742代表标签为1的有6742张以此类推。可以看到每个类别都在6000张上下分布相当均衡这就是MNIST好用的原因之一——不需要额外处理类别不平衡问题。不过bincount要求输入是整数数组。如果你用的是sklearn的fetch_openml标签被存成字符串需要先转换train_counts np.bincount(y_train.astype(int))5.2 用柱状图呈现分布统计数字本身有点抽象转化成柱状图非常直观plt.figure(figsize(10, 5)) plt.bar(range(10), train_counts, colorsteelblue) plt.xticks(range(10)) plt.xlabel(Digit) plt.ylabel(Count) plt.title(MNIST Training Set Label Distribution) plt.show()这段代码用plt.bar绘制柱状图x轴是0到9的类别标签y轴是样本数。如果你还想在柱子上方标注具体数值可以加一层文本循环for i, cnt in enumerate(train_counts): plt.text(i, cnt 20, str(cnt), hacenter, colorblack)hacenter让文本水平居中y坐标在柱子高度基础上再加20避免文字和柱子顶端重叠。整体效果非常清晰一眼就能看出数据集不偏不倚。涉及类别分布的可视化柱状图是用得最多的一种饼图也可以作为补充视角plt.pie(train_counts, labels[str(i) for i in range(10)], autopct%1.1f%%) plt.show()饼图适合展示“占比”这种相对概念对MNIST这种分布均衡的数据集会显得每个扇区大小差不多。我个人还是更推荐柱状图因为柱状图能精确读取到每个类别的绝对数量信息损耗更小。6. 常见问题与排查心得6.1 图片显示成彩色没有层次感最常见的坑就是忘记设置cmapgray。imshow默认走viridis色彩映射把0到255的灰度值映射成紫绿黄等高饱和度颜色画出来的图片像热力图根本看不出“手写”的感觉。解决办法就一行plt.imshow(image, cmapgray)另外有个容易混淆的细节如果你看到的是纯黑白图片但数字笔画边缘出现明显锯齿或色块那是因为matplotlib没有对图像做插值平滑。低分辨率的28×28图片放大显示时每个像素都会变成一个大色块这是正常现象。可以给imshow加interpolation参数让边缘过渡更平滑plt.imshow(image, cmapgray, interpolationbilinear)不过对于MNIST这种分辨率极低的数据集插值并不会让信息变多只是让图片视觉效果更柔和。要不要加全看个人审美。6.2 标题里的中文全部变成方框如果你在set_title里写了中文比如“标签5”运行后标题可能显示成一排方块。这是matplotlib中文字体缺失造成的不是代码逻辑问题。解决办法是在绘图前设置中文字体plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False第一行把中文字体设为黑体第二行解决坐标轴负号显示异常的问题。要注意的是font.sans-serif的值取决于操作系统里安装了哪些字体Linux服务器上可能没有SimHei需要先安装中文字体。早期的经验是默认字体下中文会变成警告信息“Glyph missing from current font”别慌配置好字体后重启内核再跑一次。6.3 下载数据时卡住或报错首次运行mnist.load_data()会自动从远程下载数据到本地缓存目录。在国内网络环境下这个过程偶尔会卡住或者超时这很正常跟代码没有关系。推荐的做法是提前准备好数据集文件手动放到缓存目录里。keras的缓存路径默认位于用户目录下的.keras/datasets把下载好的mnist.npz文件放进去再运行load_data()时会直接读取本地文件不再走网络。或者也可以考虑换一个网络环境稍好的时段重试数据包本身不大正常情况一二分钟就能下完。如果你用的是torchvision可以通过指定downloadTrue自动下载下载失败时检查一下root参数对应的目录确认文件是否完整。下载了一半中途失败会留下残缺文件再次下载时还是要先清理掉再重试。6.4 图片显示成一片黑色或者一片白色如果imshow显示出来的图片是全黑或全白先检查数据本身。打印一下x_train[0]的最小值和最大值print(x_train[0].min(), x_train[0].max())如果最小值等于最大值比如全为0那说明数据在加载时出了问题或者图像矩阵本身就没有有效数据。如果数据范围正常但还是显示成一片色那大概率是数据被归一化成了float类型且值非常小imshow按默认范围解释时把整张图都映射成了一个颜色。这种情况下可以用vmin和vmax显式指定映射范围plt.imshow(image, cmapgray, vmin0, vmax255)vmin和vmax告诉imshow“图像里最黑的值”和“最亮的值”分别对应多少强制把数值范围固定在0到255之间就能恢复正常的灰度层次。6.5 Jupyter里图片一直不显示在Jupyter里最诡异的问题就是代码不报错、运行也正常但图片就是不显示。这种情况十有八九是缺少%matplotlib inline魔法命令导致图片被送去了GUI后端而不是Notebook内嵌区。遇到时先检查Notebook设置确认是否已经启用matplotlib的inline模式然后确保代码末尾有plt.show()。个别情况下重启内核也能解决渲染异常的问题。另外还要注意在循环里反复调用plt.figure()创建新画布但不调用plt.close()会导致内存持续增长图片越跑越慢。批量绘制时最好复用同一个figure和axes或者在一轮循环结束时调用plt.close(fig)好把内存释放掉。写在最后的实操心得从这个项目里我最深的体会是可视化是机器学习调试的第一道门槛。很多新手拿到数据先急着跑模型等到训练结果不对劲再回头找数据问题那时排查成本已经很高了。我自己的习惯是任何数据集到手第一步永远是画图看样本把单张图、批量图、标签分布都过一遍确认数据没有异常再谈建模。这个习惯帮我避免过数据错位的低级错误也让我对“模型到底学了什么”始终保持着直观的感知。如果你想把这一套操作熟练起来我建议不要只复制代码跑通就完事而是亲手改几个参数感受差异把cmap换掉看颜色变化把figsize改大改小看布局变化把interpolation换成nearest看像素块效果把保存格式换成pdf看矢量图长什么样。参数理解透了后面画损失曲线、特征图、混淆矩阵时你就拥有一套完全可迁移的可视化功底。

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

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

免费获取报价 →
↑