写这篇内容的时候,我刚装好TensorFlow 2.1跑通第一个张量程序,正准备接触手写数字识别。翻了几份入门教程,发现很多人直接甩一段模型训练代码,可我只想知道:我手里这批数据到底长什么样?训练过程到底发生了什么?答案是画图。这也是我把Matplotlib当作TensorFlow入门第二课的原因。
这一篇内容只讲一件事:怎么用Matplotlib把深度学习入门阶段会遇到的“数据”和“训练过程”变成能看的图。适合刚装好TensorFlow、还不太会看数据的初学者,也适合那些不想一上来就啃文档的人。
1. 为什么画图要排在搭模型之前
很多初学者有个错觉,觉得深度学习入门的第一步是搭模型。实际上,拿到一批数据之后,第一件事应该是“看数据”。
1.1 没有可视化,训练就是盲人摸象
我第一次做手写数字识别时,直接用别人的代码跑了训练,准确率看起来不错,但我完全不知道模型在干什么。直到有一天我试着把训练集的前9张图打印出来,才发现数据里有大小不一、方向倾斜、甚至有些数字变形到人眼都难认的样本。那一刻才明白,所谓“准确率99%”到底意味着什么。
可视化在深度学习入门阶段至少有三种用途:
- 数据探索:数据长什么样,有没有异常样本,类别是否均衡,这些都需要画图才能快速感知。
- 过程监控:训练时损失值是否下降、精度是否波动、有没有过拟合,画成曲线比看一屏数字直观得多。
- 结果检查:预测错了哪些样本,错在什么地方,把预测结果和原始图片放在一起对比才有意义。
1.2 选Matplotlib而不是其他工具的原因
Python生态里可视化工具不少,比如Seaborn、Plotly、Bokeh。我入门阶段只推荐先学Matplotlib,理由很实在:
第一,Matplotlib是TensorFlow官方文档和大部分教程默认使用的可视化工具,跟着教程做不会出现工具不匹配的问题。第二,它的信息密度极高,网上随便一搜就能找到各种绘图问题的解决方案。第三,在入门这个阶段,Matplotlib的性能和功能足够用了,画几千个点的散点图、几百轮训练曲线,完全不卡。
有一个容易被忽略的点:Matplotlib绘图的底层逻辑和图像处理是紧密相关的。深度学习中经常要处理图像数据,而Matplotlib里imshow、颜色映射、通道顺序这些概念,和TensorFlow里图像张量的处理方式是相通的。学Matplotlib不只是学画图,也是在为后面处理图像数据做铺垫。
2. 第一张图:figure、axes到底在控制什么
刚开始用Matplotlib,很多人会写plt.plot(x, y),然后它就出图了。这个写法没有错,但只停留在“会用”的层面。入门阶段最好稍微理解一下它背后的对象模型,后面画复杂的图才不会乱。
2.1 两种用法:点菜模式和开火模式
Matplotlib有两条使用路线。一条是pyplot模式,直接调用plt.plot()、plt.xlabel()这类函数,像在餐厅点菜,省心省力。另一条是面向对象模式,先创建一个画布对象,再在里面创建坐标轴对象,然后在这个坐标轴上绘图,像自己下厨,每一步都可控。
入门阶段我的建议是:先用plt.plot()快速出图,感受流程;等画到子图布局、多坐标轴的时候,再切换成面向对象写法。不要一上来就两个模式混着学,容易混乱。
下面是我TensorFlow入门阶段画的第一张图示例:
import matplotlib.pyplot as plt import numpy as np x = np.linspace(-2*np.pi, 2*np.pi, 100) y = np.sin(x) plt.plot(x, y) plt.title('y = sin(x)') plt.show()这段代码很简单,但已经包含了绘图的基本流程:准备数据、传入数据、设置标题、展示图形。
2.2 Figure、Axes、Axis三个概念别搞混
很多读者会混淆三个英文词:Figure、Axes、Axis。
Figure是整张空白画布,你所有的线条、文字、图例都会画在这张画布上。Axes是画布上的一个绘图区域,通常包含坐标轴、刻度、数据图形。一个Figure上可以有多个Axes,这就是子图。Axis是坐标轴本身,包括x轴、y轴以及上面的刻度线。
用生活场景来比喻:Figure是一块面板,Axes是面板上用胶带贴出来的一个画框,Axis是这个画框边缘的刻度尺。
入门阶段最常见的写法是:
fig, ax = plt.subplots()这行代码一次返回两个对象,fig是画布,ax是绘图区域。后面所有操作都用ax来执行,比如ax.plot()、ax.set_title()。这种写法的好处是当你想在一张画布上画多个子图时,只需要改plt.subplots(1, 2)这种参数,后面的代码逻辑完全不变。
2.3 中文字体和负号显示问题
画训练曲线时,我第一时间遇到的是中文标题乱码。这个问题几乎所有用过Matplotlib的人都碰到过:默认字体不支持中文。
处理方式是在绘图代码最前面加上全局配置:
import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False第一行把默认字体改成支持中文的字体,第二行解决负号显示成方块的问题。实际使用中,如果是在Linux服务器上画图,SimHei可能不存在,需要改成系统里已安装的中文字体,比如Noto Sans CJK SC。可以先执行plt.rcParams['font.sans-serif']查看当前字体列表,再选择合适的中文字体放进去。
一个小提示:在脚本开头设置好rcParams,比每张图单独设置fontproperties参数省事得多。后面所有图都会自动使用这个配置,不需要重复操作。
3. 入门阶段最常用的三类图分别怎么画
深度学习入门阶段,真正会反复用到的图其实就三类:折线图、散点图、柱状图/直方图。再加上子图布局,基本覆盖日常需求。
3.1 折线图:一切训练曲线的基础
折线图适合展示随时间或迭代步数变化的趋势。训练过程中的loss曲线、accuracy曲线,本质上都是折线图。
基础的折线图不必细说,关键是绘图时的几个常用参数:
plt.plot(x, y, color='red', linewidth=2, linestyle='--', marker='o')color控制颜色,linewidth控制线宽,linestyle控制线型,marker控制数据点标记。入门阶段记住这四个参数就够用了。
有一点想补充:折线图看起来简单,但很多人在画训练曲线时忽略了一个问题,就是x轴的含义。训练曲线上的 x 轴是 epoch 或 batch 数,不是实际时间。如果训练中数据读取速度不稳定,用“秒”作为x轴画出来的曲线会变形,无法真实反映模型收敛过程。习惯上直接用迭代次数或者epoch编号就好。
3.2 散点图:看数据分布的最佳方式
做分类任务时,我最喜欢用散点图看两类样本的分布情况。比如两个特征的二分类数据,直观画出分布,能提前判断数据是线性可分,还是必须用复杂模型。
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='bwr', alpha=0.7) plt.colorbar() plt.show()c参数传入每个点的类别标签,cmap指定颜色映射,alpha控制透明度。当数据点很多时,透明度过低会导致所有点叠在一起看不清分布,alpha=0.5~0.7是比较稳妥的三区间。
3.3 柱状图与直方图:两类最容易混淆的图
初学阶段很容易把bar和hist搞混。bar是柱状图,用来展示分类数据的数量对比;hist是直方图,用来展示连续数据的分布情况。
比如查看手写数字数据集中各类别样本数量是否均衡,用柱状图:
cnt = np.bincount(y_train, minlength=10) plt.bar(range(10), cnt) plt.show()再看某张图像的像素值分布,用直方图:
plt.hist(X_train[0].ravel(), bins=32, color='gray')bins是分箱数量,它决定了直方图显示的粗糙程度。分箱过少,分布细节丢失;分箱过多,噪声太重。对于0到255的像素值,32个分箱是比较常规的经验值。
3.4 子图布局:一张图讲清楚多个角度的信息
当数据需要多角度呈现时,子图是最有效的方式。比如同时显示原图、灰度图、像素分布直方图,可以用一行代码创建三块画板:
fig, axes = plt.subplots(1, 3, figsize=(12, 4)) axes[0].imshow(X_train[0].reshape(28, 28), cmap='gray') axes[1].imshow(X_train[0].reshape(28, 28), cmap='gray_r') axes[2].hist(X_train[0].ravel(), bins=32) plt.tight_layout() plt.show()子图模式的好处是可以统一管理三块绘图区域,不用反复调用plt.subplot()切换焦点。这里请注意:三块子图用的是同一个axes数组,索引axes[0]、axes[1]分别对应不同的绘图区域。
4. 图像数据怎么显示才不出“怪色调”
深度学习入门阶段处理最多的数据就是图像。而 Matplotlib 显示图像时有一个非常容易踩坑的点:图片颜色看起来不对劲。
4.1 单通道图像直接 imshow 会变成绿色调
第一次运行手写数字数据集的图像显示,我发现图居然是绿色调的。一张明明是黑白的数字图片,在Matplotlib里默认被映射成了类似“viridis”的蓝绿色渐变色。
原因是imshow对单通道数据默认使用颜色映射(colormap),如果不手动指定cmap='gray',它会用默认颜色映射来着色。解决办法是每次显示灰度图时都显式指定:
plt.imshow(image, cmap='gray')这里可以顺便解释一个高级技巧:像cmap='gray_r'这样的反向灰度映射,可以让黑色背景变成白色背景,在查看手写数字时白色前景配黑色背景更贴近常规审美,便于阅读识别结果。
4.2 归一化和显示顺序不对,图会“全黑”或“全白”
第二个坑是归一化顺序。训练时通常会把像素值除以255,让数据范围落在0到1之间,方便模型训练。但显示图像时,很多新手直接把这个0到1的数据传入imshow,结果图像是全黑的。
原因在于Matplotlib对不同类型的数组取值理解不同。如果传入的数据类型是float,它默认值域是0到1,0到1的数据可以正常显示。但如果数据类型是uint8,它默认值域是0到255,此时0到1的所有值都会被视为接近0的值,也就是接近纯黑色。
所以,显示之前要明确当前数组的取值范围。如果你做了归一化,想看到原始图像的效果,要么把数值重新缩放到0到255并转换为uint8,要么保持0到1浮点数据直接传。不要在一个流程里混用两种类型。
4.3 一个实用的“九宫格”样本展示代码
数据探索阶段,我每次拿到新的图像数据集,都会先打印一个九宫格,快速查看前9个样本长什么样:
fig, axes = plt.subplots(3, 3, figsize=(8, 8)) for i, ax in enumerate(axes.flat): ax.imshow(X_train[i].reshape(28, 28), cmap='gray') ax.set_title(f'label: {y_train[i]}') ax.axis('off') plt.tight_layout() plt.show()这里的axes.flat是把二维的坐标系数组拉平,方便用一层循环遍历所有子图。axis('off')用来关掉每张图上的坐标轴刻度,让视图更干净。整个过程没有涉及任何深度学习知识,却让我第一次真正“看到”了数据集的含义。
5. 把训练过程画成损失下降曲线的完整做法
数据看明白了,接下来要开始训练模型。训练过程中最重要的事,就是把loss和accuracy的变化过程按时间画成曲线。
5.1 训练时记录指标的意义
训练曲线的变化趋势,能帮你快速判断模型状态:
- 收敛正常:loss持续下降后趋于平缓,accuracy稳步上升后平稳。
- 欠拟合:loss一直处于高位,accuracy迟迟上不去。
- 过拟合:训练集loss继续下降,验证集loss先降后升,两条曲线出现明显分离。
与其去理解一堆理论,不如直接看两条曲线。可视化的价值就在这里。
5.2 用 model.fit() 的返回值直接拿到 history
在TensorFlow 2.1中,model.fit()会返回一个History对象,里面记录了每个epoch的指标。这是入门阶段最省事的方式:
history = model.fit(x_train, y_train, epochs=20, validation_data=(x_test, y_test)) print(history.history.keys())history.history是一个字典,里面通常包含loss、accuracy、val_loss、val_accuracy这些键。每个键对应一个列表,长度等于epoch数。
有了这两个列表,画曲线只是举手之劳:
plt.plot(history.history['loss'], label='train_loss') plt.plot(history.history['val_loss'], label='val_loss') plt.xlabel('epoch') plt.ylabel('loss') plt.legend() plt.show()5.3 对比训练集与验证集曲线的注意事项
画这些曲线时有一个非常重要的细节:val_loss和val_accuracy是在每个epoch结束后用当前模型在验证集上计算得到的,时间点比训练集指标滞后半个epoch。如果两条曲线直接对齐画,后续对比时可能会被细微偏移误导。
更实际的操作建议是:把x轴的标签写成“epoch”,而不是“iteration”。因为validation_data的评估频率和训练集的参数更新频率不同,用iteration做x轴会让两张图的横坐标含义不一致。
6. 图片保存和样式配置,是会被反复用到的基础功
最后说说图片保存和样式设置。很多时候我们是在服务器上跑训练,没有图形界面,画出来的图只能保存成文件后下载查看。
6.1 savefig 和 show 的先后顺序容易踩坑
我最初写代码时是plt.show()之后再调用plt.savefig(),结果保存出来的图片是空白的。原因是show()执行时会把当前画布清空。
正确做法是先保存再显示,或者只保存不显示:
plt.plot(loss_history) plt.savefig('loss_curve.png', dpi=200, bbox_inches='tight')dpi=200控制图片分辨率,bbox_inches='tight'会自动裁掉边缘多余的空白区域。
顺带补充一个Linux下无图形界面时画图的常遇到问题:直接执行plt.show()会报no display之类的错误,解决办法有两个,一是代码里不用show(),只保留savefig();二是在文件最上方设置环境变量plt.switch_backend('Agg')。对这个话题,入门阶段知道这两种方法就足够用了。
6.2 一张图的常用配置清单
入门阶段想画一张“能见人”的图,记下下面这几个配置就够用:
| 配置项 | 示例 | 作用 |
|---|---|---|
| 画布尺寸 | figsize=(8, 6) | 控制图片宽高比,默认可能过小 |
| 全局字体 | rcParams['font.sans-serif'] | 解决中文显示问题 |
| 坐标轴范围 | xlim(0, 20)/ylim(0, 1) | 控制曲线显示范围 |
| 刻度密度 | xticks(range(20)) | 避免刻度过密或过疏 |
| 网格 | grid(True) | 辅助读图,便于观察数值 |
| 图例 | legend(loc='upper right') | 区分多条曲线 |
绘图时一个常见的问题是标题或坐标轴名称被遮挡。遇到过这个问题的人用过几分钟就会明白tight_layout()的价值,它会自动调整各元素的边距,让所有内容完整显示。我习惯在所有图收尾前都调用一下。
6.3 下一步可以往哪边扩展
Matrixplotlib覆盖了大部分静态图的场景,画到顺手之后再去扩展其他方向。常见几个方向包括:
- Seaborn:适合画统计图,自带一些美观的主题,基于Matplotlib封装,学习成本低。
- Plotly:适合画交互式图表,鼠标悬停就能看数值,适合做探索性分析。
- TensorBoard:TensorFlow自带的训练可视化工具,可以跟踪更复杂的模型结构、梯度分布等信息。
不过这些都是后话。对刚入门TensorFlow的人来说,能画清楚数据分布、能看到训练曲线,已经足够支撑接下来的模型学习。先别急着美化图表,把想看的信息准确画出来,这才是核心。
最后分享一个小技巧
在实际动手时有一个我深有体会的小技巧:画训练曲线时,遇到先降后升、反复横跳的曲线,不要马上怀疑模型写错了,先看一眼是不是把多个epoch的学习率调整段位混在同一张图里了。
另外,每次刚拿到新的数据集,我都先跑一遍九宫格图像展示,确认数据量、数据类型、取值范围,然后再开始写模型。就这么一个简单的习惯,后面帮我省掉了不少排查问题的时间。