1. 为什么学TensorFlow先要啃Matplotlib
一年前我开始啃TensorFlow 2.1的时候,第一反应是赶紧把网络搭建、模型训练的代码跑通,结果发现卡住我的不是模型本身的代码,而是数据看不明白、训练过程没法追踪、结果不知道怎么展示。后来才意识到,Matplotlib数据可视化这一步,表面上是画图工具,实际上是整个深度学习工作流里连接"数据理解"和"结果判断"的桥梁。
很多朋友对TensorFlow 2.1的入门学习计划里,把Matplotlib当成一个可有可无的辅助模块,甚至觉得"等用到再查一下就行"。这个想法我特别理解,但实际做下来你会发现,没有可视化能力,你连自己模型到底有没有收敛都判断不了,更别说调试超参数、分析误差来源这些进阶操作了。也就是从那会儿起,我把这一篇笔记单独立项,整理出在TensorFlow项目中真正高频使用的画图场景,而不是把Matplotlib的API手册从头抄到尾。
这篇笔记适合三类人看:刚接触TensorFlow 2.1、想把数据理解和训练监控做扎实的初学者;已经能跑通简单模型、但每次都用print看损失值的同学;以及需要把模型效果整理成图表做汇报的开发者。内容围绕"训练前看数据、训练中看过程、训练后看结果"这条主线展开,结合我在实际项目中反复用到的场景和踩过的坑,把Matplotlib里那些和深度学习强相关的功能一次说透。
2. 先把核心概念理清楚:画布、坐标系和pyplot状态机
2.1 Figure和Axes到底是个啥
我刚开始用Matplotlib的时候,代码都是照着别人的写,plt.figure、plt.subplot、plt.plot一顿调用,图能出来就算完事。直到有一次需要把多个子图精细调整位置,我才发现如果不理解Figure和Axes这两个核心概念,很多灵活操作根本做不了。
打个比方,Figure就是一张完整的画板,它决定了整张图的尺寸、分辨率、背景色;Axes是画板上具体的绘图区域,也就是真正画数据曲线的坐标系区域。一张Figure上可以放一个Axes,也可以放多个Axes,每个Axes都拥有自己的坐标轴、刻度和标题。平时我们写plt.plot(x, y),其实是Matplotlib在"当前Axes"上画图,而这个当前Axes是自动创建出来的,并不需要我们显式管理。
在TensorFlow项目中理解这个关系特别有用。比如训练完一个模型,你想在一张图里同时展示loss曲线、accuracy曲线、学习率变化,如果理解了Figure和Axes,你可以用一张Figure承载三个Axes,分别控制每个子图的样式,还能让它们共享X轴,看起来整齐又有层次。反之,如果你只会plt.plot一条道走到黑,遇到稍微复杂点的布局需求就只能上网搜代码片段,效率很低。
2.2 两种写法的选择
Matplotlib官网上一直强调两种接口:基于pyplot的状态机方式和面向对象方式。很多初学者会纠结到底学哪种,我的建议是:排查问题和快速验证用pyplot方式,正式做图表和封装复用时用面向对象方式。
pyplot方式就是plt.plot()、plt.xlabel()这种写法,它把当前Figure和当前Axes藏在内部状态里,你只管一句一句调用,适合在交互式环境里快速画图验证想法。面向对象方式则是显式创建Figure和Axes,然后调用ax.plot()、ax.set_xlabel()这类方法。区别在于,前者像你直接跟画师说要画什么,后者像你拿着画板和画笔亲手控制每一个细节。
在TensorFlow训练脚本里,我通常用面向对象方式,因为训练过程往往要循环记录loss和accuracy,最后统一绘图。提前创建好Figure和Axes对象,在循环里往里填充数据,训练结束后一次性调整样式并保存,代码结构清晰,也不会因为多次调用plt.figure而产生多余的空图。上一页翻到笔记(1)里讲TensorFlow基础时用的都是pyplot方式,这篇开始我推荐尽早切换到面向对象方式,后面写复杂可视化会顺手很多。
3. 深度学习里最常用的几张图
3.1 折线图:Loss曲线和Accuracy曲线
折线图是训练过程监控里出现频率最高的图。训练一个模型,每个epoch结束会得到一组训练集和验证集的loss值,把这组值画成折线,就能直观看到模型有没有收敛、有没有过拟合、学习率是否合适。
核心代码其实很简单:
import matplotlib.pyplot as plt epochs = range(1, len(train_loss) + 1) fig, ax = plt.subplots(figsize=(8, 5)) ax.plot(epochs, train_loss, 'b-', label='train_loss') ax.plot(epochs, val_loss, 'r--', label='val_loss') ax.set_xlabel('Epoch') ax.set_ylabel('Loss') ax.set_title('Training and Validation Loss') ax.legend() ax.grid(True, linestyle='--', alpha=0.6) plt.show()这里有个关键点:折线图的label参数一定要写清楚,然后调用ax.legend()显示图例,不然图出来了别人根本不知道哪条线代表什么。我在实际项目里见过不少同学画完图才发现忘了加图例,训练集验证集混在一起根本没法判断过拟合情况,只能重新画一遍。
另外,loss曲线的纵轴范围也值得注意。默认情况下Matplotlib会自动适配数据范围,但如果loss从2.x一路降到0.1,自动缩放会让早期下降看起来特别陡,后期变化几乎看不见。遇到这种情况,我一般先按默认范围看整体趋势,再针对最后几十个epoch单独画一张局部放大图,两个视角配合使用,比单纯调ax.set_ylim效果更好。
3.2 散点图:数据分布一眼看穿
散点图在深度学习前期分析数据时特别好用。举个例子,我做过一个二分类项目,原始特征有几十个维度,我先把数据用散点图按两个特征画出来,颜色区分类别,立刻就发现了有些类别之间存在严重的特征重叠。这个信息如果只看统计量,要花很多时间才能发现。
散点图的调用方式:
fig, ax = plt.subplots(figsize=(7, 6)) ax.scatter(data[:, 0], data[:, 1], c=labels, cmap='viridis', alpha=0.7, s=20) ax.set_xlabel('Feature 1') ax.set_ylabel('Feature 2') ax.set_title('Data Distribution by Class') plt.show()参数上,c对应每个点的颜色值序列,cmap指定颜色映射,alpha控制透明度。训练样本几千甚至上万的时候,点会重叠得很厉害,alpha=0.5到0.7基本是必须的,否则图上一团黑,什么信息都看不出来。s控制点的大小,默认值偏小,在jupyter里缩放后经常看不清,我习惯设到20到30之间。
散点图真正发挥价值的地方是在预处理之前和之后各画一张对比图。比如做归一化之前,特征取值范围可能是0到1000;做完标准化之后,分布集中在-3到3之间。把这两张散点图并排放,能非常直观地确认预处理是否生效,也能帮助判断是否存在异常离群点。
3.3 直方图:数值分布的特征观察
直方图用来观察单维数据的分布形态。在TensorFlow项目中,我主要用在两个场景:一是查看输入特征或标签的分布是否均衡,二是查看模型输出概率的分布情况。
最简单的一段代码:
fig, ax = plt.subplots(figsize=(7, 5)) ax.hist(predictions, bins=50, edgecolor='white', alpha=0.8) ax.set_xlabel('Predicted Probability') ax.set_ylabel('Count') ax.set_title('Distribution of Model Predictions') plt.show()bins参数决定分箱数量,这个值的设置直接决定直方图的信息量。bins太少会掩盖分布细节,太多又会显得杂乱。我一般先设50左右看整体形态,再根据数据量调整。数据量在几千以内,30到50个bins比较合适;数据量到了几万,可以适当增加到100,曲线会更平滑。
还有个常用的技巧是用density=True把频数转换为密度,这样即使不同数据集样本量不一致,也能在同一个坐标系里对比分布形态。这一点在对比训练集和测试集的特征分布时特别有用,两张图叠在一起看,能快速发现训练集和测试集是否存在明显的分布偏移。
4. 子图布局:多张图放一起对比
4.1 subplots基本用法
做深度学习实验,几乎不可能只画一张图就能说明问题。训练集loss、验证集loss、准确率、学习率,这些都是需要放在一起对比观察的。Matplotlib的plt.subplots提供了非常灵活的子图布局能力。
最基础也最常用的是等分网格布局:
fig, axes = plt.subplots(2, 2, figsize=(12, 8))这句话创建了一个2行2列的子图网格,返回的axes是一个二维数组,通过axes[0, 0]、axes[0, 1]等方式访问每一个Axes。顺手说一下,figsize是Figure的宽高,单位是英寸,这个值在最终保存图片时直接影响清晰度和排版效果,不建议忽略。
子图之间的间距问题我栽过跟头。坐标轴标签写长一点之后,子图的标题和相邻子图的坐标轴标签经常挤在一起,尤其保存出来再看特别难受。解决办法是在布局之后加一行plt.tight_layout(),它会自动调整子图间距,让标签和标题不重叠。有时候tight_layout效果还不够理想,可以改用plt.subplots_adjust(hspace=0.4, wspace=0.3)手动调整间距,其中hspace是行间距比例,wspace是列间距比例,多试几次就能找到合适的值。
4.2 训练过程对比图实战
我在训练一个图像分类模型时,会把训练过程整理成一张2x2的对比图,四个子图分别展示训练集loss、验证集loss、训练集accuracy、验证集accuracy。这样训练结束时扫一眼就能判断模型状态,不用翻日志。
实际代码如下:
fig, axes = plt.subplots(2, 2, figsize=(12, 8)) axes[0, 0].plot(epochs, train_loss, 'b-') axes[0, 0].set_title('Train Loss') axes[0, 1].plot(epochs, val_loss, 'r-') axes[0, 1].set_title('Val Loss') axes[1, 0].plot(epochs, train_acc, 'b-') axes[1, 0].set_title('Train Accuracy') axes[1, 1].plot(epochs, val_acc, 'r-') axes[1, 1].set_title('Val Accuracy') for ax in axes.flat: ax.set_xlabel('Epoch') ax.grid(True, linestyle='--', alpha=0.5) plt.tight_layout() plt.savefig('training_curves.png', dpi=150) plt.show()这里有个细节值得单独说:axes.flat会把二维数组展平成一维,然后用一个循环统一处理所有子图,省去重复写四遍的麻烦。我后面写代码时凡是遇到多个子图需要统一设置的,都会用这种方式,代码干净很多。
另一个体会是,验证集loss和验证集accuracy宁可多花点时间把坐标范围调整一致,方便横向对比。如果四个子图的Y轴范围各不相同的默认状态,直观对比效果会打折扣。可以手动设置axes[0, 0].set_ylim(0, 2)这样的语句,让同类型指标保持同尺度。
5. 配合TensorFlow实战:图像数据与结果可视化
5.1 显示MNIST样本
做图像分类任务的时候,最常见的可视化需求是把数据集里的样本显示出来,看看图片长什么样、标签对应得对不对。TensorFlow自带的MNIST、CIFAR这些数据集,下载后都是numpy数组,直接用Matplotlib的imshow就能显示。
显示单张图片:
import matplotlib.pyplot as plt plt.imshow(sample_image, cmap='gray') plt.axis('off') plt.show()注意灰度图像要加cmap='gray',不然默认的colormap会把灰度图渲染成奇怪的彩色,看起来非常误导。axis('off')是为了去掉坐标轴,展示图像时坐标轴通常没有意义。
显示一批样本更常见的做法是用子图网格排列。比如一次显示12张图,排成3行4列:
fig, axes = plt.subplots(3, 4, figsize=(10, 8)) for i, ax in enumerate(axes.flat): ax.imshow(samples[i], cmap='gray') ax.set_title(f'Label: {labels[i]}') ax.axis('off') plt.tight_layout() plt.show()在for循环里有个重要操作:axes.flat把子图数组展平,然后enumerate拿到索引,依次访问对应的样本。整个过程看着简单,但我第一次写的时候忘了在循环里用CMap参数,结果每张图都是默认的紫色调,直到后面做归一化对比时才发现问题。
在显示图像样本这件事上,我还有个实际建议:显示前先确认数据范围。如果图像数据是0到255的整数,直接显示没问题;如果是归一化到0到1的浮点数,也直接显示没问题;但如果经过了标准化处理变成负数范围内的值,imshow会按照数据最小最大值自动映射,显示效果可能偏暗或者偏亮。这种情况可以先做一个np.clip操作把像素值裁剪到合理范围再显示,避免误判样本质量。
5.2 混淆矩阵可视化
模型训练完,光看accuracy远远不够。分类模型每一类分得怎么样,哪些类别容易混淆,这些问题只能通过混淆矩阵回答。Matplotlib配合sklearn.metrics里的confusion_matrix函数,画混淆矩阵非常顺手。
from sklearn.metrics import confusion_matrix import numpy as np cm = confusion_matrix(y_true, y_pred) fig, ax = plt.subplots(figsize=(8, 6)) im = ax.imshow(cm, cmap='Blues') ax.set_xticks(range(num_classes)) ax.set_yticks(range(num_classes)) ax.set_xticklabels(class_names, rotation=45, ha='right') ax.set_yticklabels(class_names) ax.set_xlabel('Predicted Label') ax.set_ylabel('True Label') for i in range(num_classes): for j in range(num_classes): ax.text(j, i, cm[i, j], ha='center', va='center') fig.colorbar(im, ax=ax) plt.tight_layout() plt.show()imshow配合cmap='Blues'可以把矩阵渲染成热力图,颜色越深代表数量越多。我在实际项目里,会把对角线上的数字用加粗显示,或者在每个格子里的文字颜色根据背景深浅自动调整,这样即使是用灰度打印也不至于看不清。
fig.colorbar(im, ax=ax)这行是添加颜色条,用来指示颜色对应的数值范围。颜色条在混淆矩阵里不是必须的,但加上之后读者能快速感知数值规模,尤其是在各类别样本数量差异很大的时候,效果很明显。
这里说个经验性的建议:混淆矩阵图里,文字和格子大小的比例很重要。类别少(比如10类以内),直接在每个格子中央写数字完全没问题;类别多到50类以上,格子太小文字挤在一起,不如只看颜色深浅。这种时候我会把ax.text的fontsize调到8以下,或者干脆去掉数字只保留色谱。
6. 样式调整与图像导出
6.1 让图更专业的几个常用设置
很多同学画图时用的是Matplotlib默认样式,图能看但总觉得"不够专业"。我在反复做实验报告之后,总结出一套让图表立刻提升一个档次的基础设置,核心是:统一字体大小、统一线条风格、合理使用网格。
字体方面,最常见的操作是设置全局字体大小:
plt.rcParams['font.size'] = 12 plt.rcParams['axes.labelsize'] = 13 plt.rcParams['axes.titlesize'] = 14 plt.rcParams['legend.fontsize'] = 10这些配置一旦设置,在当前会话里所有图都会生效,不需要每张图单独调整。我在训练脚本里通常把这些配置写在文件开头的公共配置区,这样后面所有绘图函数都能用上统一的排版风格。
线条风格方面,我习惯区分训练集和验证集:训练集用实线,验证集用虚线或者带标记的实线,颜色上也避免使用过于接近的颜色。Matplotlib默认的颜色循环里,蓝色和橙色对比度还不错,但如果一张图上有四条线,我会显式指定color值,比如'#1f77b4'、'#ff7f0e'、'#2ca02c'、'#d62728',这一组颜色在白色背景和打印输出下都有比较好的辨识度。
网格线这个小设置,看着不起眼,作用非常大。一条没有网格的曲线图,眼睛很难判断某个点对应的数值;加上半透明虚线网格之后,读数效率立刻提升。我的做法是ax.grid(True, linestyle='--', alpha=0.5),既不会抢主数据的视觉权重,又能提供足够的参考线。
导出图片时,dpi参数非常关键。默认的dpi=100在屏幕上够用,但放到论文、报告或微信公众号里,放大后会出现明显的锯齿感。我保存实验图统一用dpi=150,如果是要印刷或投稿级别,直接用到300。文件大小会增加,但清晰度提升明显,这点代价值得。
6.2 高清保存与尺寸控制
保存图片时有个容易被忽略的细节:bbox_inches='tight'参数。不加这个参数时,保存出来的图经常四周留有大量空白,标题和坐标轴标签还可能被裁掉一部分,尤其是坐标轴标签比较长的时候。加上bbox_inches='tight'之后,Matplotlib会自动裁剪空白区域,让图片内容紧贴边界。
fig.savefig('result.png', dpi=150, bbox_inches='tight')另外,savefig支持的格式很多,我平时用PNG格式用于插入文档,用PDF格式用于需要无限放大的场景。特别注意一点:PDF格式的矢量特性在折线图、散点图这类图表上优势明显,但对包含大量图像数据的imshow热力图来说,PDF文件可能很大,这时候PNG反而更合适。
尺寸控制上,figsize的宽高比应当根据内容来定。一条纯时间序列曲线,适合用宽扁的figsize=(10, 5),横向视野更大,曲线起伏更舒展;一个混淆矩阵或图像样本网格,用接近正方形的figsize=(8, 8)更协调。规则是:Y轴的数值变化是主要内容时,高度比例适当加大;X轴上有大量类别或时间点时,宽度比例适当加大。
7. 常见问题与排查技巧实录
7.1 中文乱码和负号显示问题
Matplotlib默认字体不支持中文,这是一个几乎所有中文用户都会碰到的坑。第一次在图上写ax.set_title('训练损失'),保存出来的图标题位置直接变成方框,我第一次遇到时以为是代码写错了,排查了半天才发现是字体问题。
解决中文显示的关键步骤是手动指定中文字体:
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'PingFang SC'] plt.rcParams['axes.unicode_minus'] = False第一行指定了可用的中文字体列表,Matplotlib会按顺序查找系统里存在的字体;第二行解决了坐标轴上负号显示异常的问题。如果不设置axes.unicode_minus为False,图中出现的负号会变成方块,这个问题和中文乱码一样常见,而且单独排查起来特别费劲。
不同操作系统可用的字体不一样。Windows环境下SimHei存在概率高,macOS下PingFang SC比较稳,Linux服务器上则要看系统装了什么字体。我在一台Linux服务器上跑训练脚本时,中文字体完全没有,后来用fc-list :lang=zh命令查看了系统已有的字体,装了一个开源中文字体之后才解决。如果你的运行环境装不了字体,还有一个替代方案:图里坚持用英文标签,输出结果后到文档里再处理中文说明,训练过程内部用英文标注,不影响功能。
7.2 图形不实时刷新
在交互式环境(比如Jupyter Notebook)里画图,经常会遇到"代码跑完了图却没出现"的情况。这个问题的根源在于Matplotlib的渲染模式。Notebook环境默认是inline模式,图形直接嵌入到输出单元格里;但如果你在代码里调用了plt.show(),某些版本的Notebook会出现图形显示异常或者不显示。
我的处理方式很简单:Notebook环境下,尽量不写plt.show(),靠单元格输出的自动展示即可。非Notebook环境下(比如直接运行.py脚本),plt.show()是必须的,否则图会在程序结束时才弹出,甚至某些情况下直接不显示。如果需要在脚本运行的同时把图保存下来,记得在plt.show()之前调用savefig,因为show()之后Figure会被清理,保存操作可能拿到一张空白图。
还有个小技巧:在Notebook里想看训练过程中间阶段的动态变化,可以在plt.figure()之后加一行from IPython.display import clear_output,配合clear_output(wait=True)实现动态刷新效果,每训练几个epoch更新一次图形,能实时监控训练过程。这个技巧在训练时间较长的模型时非常实用,能帮你在第一时间发现loss发散这类问题。
7.3 数据量大时卡顿的处理
深度学习项目里,训练样本动辄几万条,如果直接把所有点全部画到散点图或折线图上,Matplotlib的渲染会明显卡顿,图出来之后交互操作也特别迟钝。我第一次画几万个点的训练过程曲线时,等了十几秒才出图,后期想拖动查看根本卡得没法用。
处理大数据的思路是降采样或者聚合。比如折线图,如果数据点有上万条,可以先按固定间隔取样,比如每10个点取一个点,曲线形状基本不变但渲染速度快很多。散点图同理,可以用np.random.choice随机抽取子集,比如从5万个点里抽5000个点,视觉效果几乎不受影响。
用随机抽样时要注意类别平衡。如果不同类别的样本数量差距很大,随机抽样可能让少数类样本在图中几乎消失。这时可以每个类别分别抽样,保证各类别在图中都有足够的代表点。我的习惯做法是按类别分组,每组抽取的目标数量一致,然后再合并画图。
最后一个偏门但很有效的招数:如果图实在太大,可以在保存成PNG时先降低dpi出预览图,排版确认无误后再用高dpi出正式图。这样既保证了预览阶段的操作流畅,又不牺牲最终出图质量。
7.4 其他高频小问题速查表
下面这组问题是初学者在TensorFlow场景里最常遇到的,我简单整理成表格,方便照着排查:
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| 图片标题或标签显示方块 | 缺少中文字体配置 | 设置plt.rcParams['font.sans-serif'] |
| 坐标轴负号显示为方块 | 缺省字体渲染负号异常 | 设置plt.rcParams['axes.unicode_minus'] = False |
没有调用plt.show()但图未显示 | 脚本模式而非交互式环境 | 在代码中显式调用plt.show() |
savefig保存的图是空的 | 在plt.show()之后调用保存 | 调整顺序,先保存再展示 |
| 多个子图标题和标签重叠 | 子图间距不足 | 使用plt.tight_layout()或手动调整间距 |
| 灰度图显示成彩色 | 缺少cmap='gray'参数 | 在imshow中指定灰度colormap |
| 大数据量绘图卡顿 | 渲染点数量过多 | 抽样或降采样后再绘图 |
我把这一页记在笔记最前面,做实验时遇到异常直接对照排查,省下不少时间。
实操总结
这篇笔记从TensorFlow 2.1入门学习的角度,把Matplotlib数据可视化梳理成一套够用够全的实操框架。我个人在实际操作中的体会是:画图这件事,最重要的不是记住所有API参数,而是养成"先想清楚要表达什么、再选择图表类型"的习惯。训练前的数据分布分析、训练中的loss曲线监控、训练后的混淆矩阵评估,每个环节用对应类型的图,一套流程走下来,你对模型状态的掌握程度会比单纯看日志数字高一个级别。
最后再分享一个小技巧:把常用的绘图函数封装成一个公共模块,比如plot_training_history()、plot_confusion_matrix(),之后每个新实验只需要传入数据就能直接出图。我做完这个封装之后,后续好几个项目都复用了同一套代码,图表风格统一,出图速度也快很多。这一步看起来工作量不大,但对长期做实验的效率提升非常明显。