资讯动态

从TensorBoard到论文级图表:训练曲线数据提取与matplotlib重绘完整指南

发布时间:2026/10/5 11:23:32 来源:尧图企业网站定制
前些天有个实验室的师弟把训练好的模型截图发我问我TensorBoard里那么漂亮的训练曲线能不能直接拷进论文。我看了眼那张还带着网页灰底的截图上头坐标文字模糊得跟马赛克似的当场回了一句你投顶会要是敢这么贴审稿人第一眼就会觉得图没用心。其实这问题挺普遍的刚入门深度学习的人基本都会踩一次TensorBoard作为训练过程的监视工具确实好用但它的定位从来就不是“论文出图工具”。今天我就以自己的实际习惯为主线整理一套从TensorBoard取图、加工到排进论文的完整流程相关的数据读取、绘制参数、格式坑点都会讲到。无论你是刚开始写第一篇论文还是被导师要求重新出图这篇文章应该都能帮你少走不少弯路。1. 训练完毕先别急着截图想清楚论文里到底需要哪种图1.1 TensorBoard里的图在论文里分两类开始处理之前我会先逼自己把TensorBoard里的可视化分成两类。第一类是标量曲线比如train loss、validation loss、accuracy、IoU、学习率这些随step或epoch变化的曲线它们一般是放进对比实验、消融实验、收敛性分析这几类章节第二类是图像型可视化比如语义分割的预测掩码、目标检测的边界框、GAN生成的样本它们一般放在方法概览图、定性结果图或者补充材料里。两类图的处理方式完全不一样。曲线类本质上是“数据重绘”因为你需要的是背后的数值而不是那个网页截图图像类则需要从模型输出里重新渲染高分辨率结果不能指望TensorBoard面板里那个小缩略图能满足印刷需求。搞清楚这个区别后面就不会走错路。1.2 直接截图会带来三个致命问题我见过不少低年级同学直接“WinShiftS”截图交给导师最后无一例外被打回来。仔细分析直接截图至少有三个硬伤分辨率完全不可控。TensorBoard页面的渲染分辨率取决于你屏幕的物理分辨率、浏览器缩放比例和窗口大小。你看着挺清晰一旦放进论文里被缩到单栏宽度曲线边缘就开始发虚如果导师想把某段曲线放大看趋势你会立刻发现放不了因为像素就那么多。字体和排版与论文模板脱节。TensorBoard默认是网页端的英文字体尺寸和坐标轴风格都不是为期刊模板设计的。论文里通常要求图表字号在7到10磅之间线条粗细不超过1.5磅而这些参数在网页截图中是完全不可控的。最后导致论文整体的图表风格不统一这是很多审稿人反感的一点。后期无法二次编辑。截图的坐标轴范围、图例位置、曲线平滑度、颜色对比一旦截下来就全定死了。等你跑完一个更好的baseline想把它和之前的结果画在同一张图里比较你就只能重新打开TensorBoard再截一次之前所有排版工作全部作废。1.3 我的工作流把“取数据”和“出图”彻底解耦我自己的处理思路其实特别简单只有两段。第一段是从TensorBoard底层的事件文件里提取数值得到一份干净的DataFrame第二段是用matplotlib或者你想用其他的绘图库也行重新绘制出期刊级别的图表。这样做的最大好处是log目录一旦生成不管以后要改配色、换坐标范围、增加一个对比方法都只动绘图脚本不需要重新训练模型也不用再打开浏览器去碰TensorBoard。这一步看似多绕了一圈实际在你论文周期内能省下数不清的时间。下面我就把这个流程从头到尾拆开讲。2. 从TensorBoard日志里取数而不是从网页上取图2.1 先认识events文件TensorBoard的一切数据源头先明确一点TensorBoard网页上所有曲线背后都是磁盘上的一堆事件文件。训练时你设置logdir参数框架会在该目录下生成形如events.out.tfevents.1699999999.localhost.12345.0的文件。这个文件内部是一种顺序记录格式每一条记录包含tag名、step数、wall_time时间戳以及对应的数值。TensorBoard前端通过解析这些记录画出曲线。因此如果你不想依赖浏览器就可以直接在Python里解析这些events文件。不管你的模型是PyTorch、TensorFlow还是PaddlePaddle训练的只要最后写日志时用的是TensorBoard协议这个方案都通用。2.2 用EventAccumulator读取标量数据最直接的方式是TensorBoard自带的EventAccumulator不需要额外安装依赖TensorBoard装好了就能用。一个典型的读取代码如下from tensorboard.backend.event_processing import event_accumulator ea event_accumulator.EventAccumulator( runs/exp1, size_guidance{event_accumulator.SCALARS: 0} ) ea.Reload() print(ea.Tags())输出里的Tags()会返回当前运行目录下所有可用的tag集合通常包括scalars、images、histograms等类别。对于标量我们这样取出全部数值scalar_data ea.Scalars(loss/train) for item in scalar_data: print(item.step, item.value, item.wall_time)每条记录是一个命名元组.step是训练步数.value是标量值.wall_time是写入时的时间戳。注意size_guidance参数很重要默认情况下EventAccumulator会限制每个tag最多保留多少条记录设置成{event_accumulator.SCALARS: 0}表示“不限制”否则等日志文件很大时会发现读出来的数据被截断了这个坑我踩过不止一次。2.3 用tbparse一步拿到整理好的DataFrame如果只是单个run、单个tagEventAccumulator完全够用。但如果要横向对比很多实验、很多tag我建议用tbparse这个第三方库它会把多个run读成长表格式的pandas DataFrame处理起来非常顺手。pip install tbparse用法如下import tbparse df tbparse.SummaryReader(runs).scalars print(df.head())输出大概长这样run tag step value 0 exp1 loss/train 0 2.315000 1 exp1 loss/train 1 2.041000 2 exp1 loss/train 2 1.833000之后你就能用pandas的口令随便筛选和聚合了比如按run和tag分组计算均值、标准差或者把多个随机种子的实验合并成一条带置信区间的曲线。每次读数据时我会顺手做两步清洗第一步去掉step重复的行第二步按step排序。这两步看起来多余但训练中断重启过的情况下日志里很可能出现相同step的重复记录不清洗的话画出来的曲线会突然出现折返。df df.drop_duplicates(subset[run, tag, step], keeplast) df df.sort_values([run, tag, step])2.4 TensorBoard网页上的“下载CSV”按钮只能应急可能有人会说TensorBoard面板里不是能下载CSV吗为什么还要写代码去读日志确实标量仪表盘右上角的小菜单可以导出当前tag的CSV但实际用过就会知道它很鸡肋。一是它只能导出你当前选中的tag批量导出多个tag非常麻烦二是它不包含多run对比结构你要一次对比10个实验就得下载10次三是它不会做重复step的清洗后期还得自己重新整理。所以我的结论是网页CSV下载适合个别应急场景但凡涉及任何批量处理或者论文级图表老老实实用代码读日志才是正道。3. 用matplotlib重绘训练曲线论文级出图的关键参数3.1 尺寸、字号、线宽怎么设才像论文图拿到数据之后重绘才是真正的技术活。先说尺寸。论文插图的宽度和期刊模板强相关。以常见单栏格式为例正文内容宽度一般是3.3英寸或8.5厘米左右双栏页面如果要放一个横跨两栏的大图宽度大概是6.8到7英寸。我一般在绘图脚本里先用这两个锚点值基本不会出错单栏小图figsize(3.3, 2.6)双栏大图figsize(6.8, 4.0)字号方面论文正文常用9到10.5磅图表内的文字不建议比正文更小太多。我的默认设置是坐标刻度8磅、坐标轴标签9磅、图例7.5到8磅。线宽设在1.0到1.5磅之间太细的话打印出来看不清太粗又会盖住曲线趋势。一份能直接用的matplotlib全局配置如下import matplotlib.pyplot as plt plt.rcParams.update({ font.size: 8, axes.linewidth: 0.8, xtick.major.width: 0.8, ytick.major.width: 0.8, lines.linewidth: 1.2, legend.frameon: False, axes.grid: False, figure.dpi: 150, savefig.dpi: 600, })这里有几个细节容易踩坑。legend.frameon: False是去掉图例边框跟期刊模板的风格更贴合axes.grid: False是默认不要网格部分期刊喜欢网格但需要自己按需添加而且网格线一定要用浅色虚线别用深色实线。3.2 平滑曲线别直接用TensorBoard默认值要自己算TensorBoard面板里那个“Smoothing”滑条本质上是对原始曲线做指数移动平均EMA。它默认的平滑系数有时候甚至是0.6这个强度会明显压低早期loss的震荡幅度让曲线看起来“过度美化”。因为审稿人可能注意到并质疑我更推荐自己在matplotlib里控制平滑参数并且在图注里明确写清楚。EMA的实现其实很简短import numpy as np def ema(values, alpha0.9): out [] prev values[0] for v in values: prev alpha * prev (1 - alpha) * v out.append(prev) return np.array(out)alpha的取值范围是0到1越大代表平滑越强曲线越柔和但细节损失越多。我个人的习惯是alpha0.8左右跑长训练时用0.85短训练可以降到0.7。实际操作中我会把原始曲线用淡色细线画在底下把EMA之后的重曲线叠在上面这样既有原始波动信息又不显得乱。不过论文里如果只放一条平滑后的线而不放原始噪声线一定要在图注里说明“curves are smoothed with an EMA factor of 0.8”否则有数据美化的嫌疑。3.3 一份可以直接套用的训练曲线绘制模板把读取、平滑、绘图串起来完整的模板大致是import numpy as np import matplotlib.pyplot as plt from matplotlib.ticker import MaxNLocator import pandas as pd from tensorboard.backend.event_processing import event_accumulator def load_tb_scalars(logdir, run, tag): ea event_accumulator.EventAccumulator( f{logdir}/{run}, size_guidance{scalars: 0} ) ea.Reload() rows [(ev.step, ev.value) for ev in ea.Scalars(tag)] df pd.DataFrame(rows, columns[step, value]) df df.drop_duplicates(subsetstep, keeplast) return df.sort_values(step) logdir runs tag loss/valid runs [exp1, exp2] colors [#1f77b4, #ff7f0e] linestyles [-, --] fig, ax plt.subplots(figsize(3.3, 2.6)) for run, color, ls in zip(runs, colors, linestyles): df load_tb_scalars(logdir, run, tag) # 固定只画前100个step避免后期曲线抖动干扰对比 df df[df[step] 100] smooth ema(df[value].values, alpha0.8) ax.plot(df[step], smooth, colorcolor, linestylels, labelrun) ax.set_xlabel(Epoch) ax.set_ylabel(Validation Loss) ax.set_ylim([0.0, 1.2]) ax.xaxis.set_major_locator(MaxNLocator(integerTrue)) ax.legend(locupper right, fontsize7) fig.tight_layout() fig.savefig(loss_valid.pdf, bbox_inchestight) plt.close(fig)这段代码里有几个点值得强调。一是MaxNLocator(integerTrue)它保证x轴刻度不会出现0.5、1.5这种不适合epoch展示的值二是bbox_inchestight用来裁掉多余白边但使用后要检查xlabel有没有被裁半的情况出现的话就给fig.subplots_adjust(bottom...)留一点空间三是plt.close(fig)循环画多张图时如果忘记关闭会吃掉大量内存尤其在本机跑完几十个实验后会更明显。3.4 多条曲线对比时的配色与线型策略论文里最常见的场景是把“我们的方法”和几个baseline放在同一张图里。如果只靠颜色区分那么期刊打印成灰度版时会非常痛苦。我个人的经验是颜色和线型双重编码。例如一组方案可以是方法颜色线型含义Ours#1f77b4实线主推方法Baseline A#ff7f0e虚线经典对比Baseline B#2ca02c点划线消融变体Baseline C#d62728点线最弱参考颜色尽量避开红绿相邻因为红绿色盲读者会分不出来。我觉得tab10色彩面板作为起点没问题但真要投稿前最好换成色盲友好型配色。另外曲线的数量一般不要超过6条。超过6条时图例会占掉很大区域主曲线反而不突出。真遇到方法特别多的情况优先考虑拆成多个子图或者用表格代替曲线。3.5 保存格式PDF、SVG、PNG选哪个曲线类图我几乎永远保存成矢量格式。原因很简单论文正文中排版后可能在Word或LaTeX里被缩放到任意尺寸矢量图无论怎么缩放都是清晰锐利的。在LaTeX投稿场景下我首选保存为PDF因为\includegraphics可以直接编译也方便和正文统一字体。在Word投稿场景下PDF支持度不如EMF不过很多期刊允许高分辨率PNG那就存600dpi的PNG。如果投的会议明确要求图片是TIFF/EPS可以到了投稿前再统一转换不必在绘图阶段纠结。# 矢量图 fig.savefig(fig.pdf, bbox_inchestight) # 高分辨率位图 fig.savefig(fig.png, dpi600, bbox_inchestight)关于PNG的dpi有一条简单的计算公式论文里图的实际宽度英尺× dpi得到两个方向的最小像素数。比如单栏图宽3.3英寸600dpi就需要至少1980像素宽这远高于屏幕截图所能提供的分辨率。4. 图像类可视化也要走“重新推理”路线4.1 TensorBoard里的缩略图只能当校视图用很多同学把语义分割的add_image可视化结果在TensorBoard里看了一遍觉得效果不错就想截图放到论文里。这里必须泼一盆冷水TensorBoard的Image面板显示的图像是有损缩略图它为了适应网页展示往往把分辨率降到几万像素以下有些框架还会在写入事件文件时压缩质量。所以不管TensorBoard里显得多清晰它的定位只能是在训练过程中快速确认模型行为是否正常“校内检查用”的角色。真正要进论文必须回到模型推理环节在测试集上重新跑一次保存未压缩或无损压缩的原始分辨率结果。4.2 还原模型输出时最坑的归一化错误在从模型输出保存图像之前最大的坑是归一化还原。训练时图像一般会被归一化到[0,1]或[-1,1]或者按RGB均值/标准差标准化。如果你直接用模型输出的浮点数组保存出来的图要么漆黑一片要么过曝发白。最稳妥的办法是记住你训练时的预处理参数。以最简单的情况为例如果只用torchvision.transforms.ToTensor()把图像从PIL的[0,255]转到[0,1]那么保存时直接乘255即可img output[0].permute(1, 2, 0).cpu().numpy() # (C, H, W) - (H, W, C) img img * 255 img img.clip(0, 255).astype(uint8)如果训练时还减了均值、除了标准差就要先反标准化回去mean np.array([0.485, 0.456, 0.406]) std np.array([0.229, 0.224, 0.225]) img img * std mean img img * 255 img img.clip(0, 255).astype(uint8)这里最容易翻车的点是有些模型输出范围是[-1,1]忘了加(x1)/2这一步保存出来的图整体偏暗颜色灰蒙蒙的。我的建议是在保存前加一行断言检查数据范围避免肉眼排查半天。比如assert 0 img.min() and img.max() 255。4.3 一种高效批量生成分割/检测结果图的做法对于分割类任务我一般写成类似这样的脚本model.eval() save_dir vis_results os.makedirs(save_dir, exist_okTrue) for i, batch in enumerate(valid_loader): if i 10: break images, masks batch[0], batch[1] with torch.no_grad(): preds model(images) pred_mask preds.argmax(dim1, keepdimTrue) # (B, 1, H, W) for b in range(images.size(0)): fig, axes plt.subplots(1, 3, figsize(9, 3)) axes[0].imshow(denormalize(images[b])) axes[0].set_title(Input) axes[1].imshow(denormalize(images[b])) axes[1].imshow(masks[b][0].squeeze().cpu(), cmapjet, alpha0.5) axes[1].set_title(Ground Truth) axes[2].imshow(denormalize(images[b])) axes[2].imshow(pred_mask[b][0].squeeze().cpu(), cmapjet, alpha0.5) axes[2].set_title(Prediction) for ax in axes: ax.axis(off) plt.tight_layout() plt.savefig(f{save_dir}/sample_{i:03d}_{b}.png, dpi300, bbox_inchestight, pad_inches0.02) plt.close(fig)几个细节要注意。第一保存分割叠加图时alpha0.5左右的半透明掩码是通用做法太透明会导致掩码看不清太不透明会盖住原图底层信息。第二cmapjet虽然显眼但并非所有期刊都认可部分审稿人会觉得太花哨可以换成更温和的colormap比如viridis或自己定义一个分类色板。第三pad_inches参数用于控制白边保存结果需要在Word里直接使用时把白边压到最小最方便。对于检测类任务建议直接用PIL或者OpenCV绘制边界框和类别标签后保存不要再用matplotlib一层层叠加否则在大批量处理时性能很差而且框线坐标在matplotlib坐标变换下容易被搞错。4.4 合成对比大图时的对齐问题如果论文里要放“多方法对比”的大图我习惯先用一个小脚本把所有单张结果按行列拼接好再贴到论文里。这样输出的图片格式可控避免在LaTeX里临时拼图时对不齐。拼接时最影响美观的问题就是子图尺寸不一致。解决办法是统一每张子图的宽高比比如都从同一个resize尺寸的原始图上裁剪或者补边。拼接推荐使用PIL.Image的new()paste()比起matplotlib的subplots再保存更容易控制像素精度from PIL import Image img_list [...] # 每张已经是统一尺寸的 PIL.Image width, height img_list[0].size canvas Image.new(RGB, (width * len(img_list), height), white) for idx, img in enumerate(img_list): canvas.paste(img, (idx * width, 0)) canvas.save(comparison.png)这样拼出来的图边界干净不容易出现matplotlib保存时引入的微小间距。5. 排进论文前的最后一遍自检清单5.1 坐标轴、刻度、图例的统一检查论文定稿前我会把所有插图的风格统一检查一遍。重点看三点第一同一篇文章中所有图表的坐标轴标签字号、图例字号必须一致不能一张图8磅一张图9磅第二同类曲线图的坐标范围尽量一致例如不同方法在训练集上的loss比较图最好都设为相同的x轴和y轴范围否则会因为“选择性裁剪”被质疑第三图例的位置放在不遮挡主曲线的区域通常会放角落但如果那个角落恰好是曲线下降经过的地方就会非常难看宁可换到对侧或顶部外侧。另外还有一个小问题如果图中有多条曲线图例在正文里必须能做一一对应。最好的做法是在图注里直接说明“Ours表示……”而不是让读者自己去猜。5.2 字体嵌入和Type 3字体问题这个问题在LaTeX投稿时特别常见。如果用于绘制PDF的字体没有被正确嵌入PDF渲染器或者出版社后端打开时会找不到字体导致文字显示为乱码或替换成系统默认字体。用下面命令可以快速查看PDF中所有字体信息pdffonts fig.pdf输出中每行对应一种字体最后一列是字体是否被嵌入。如果出现no说明字体没有嵌入需要处理。另一个让出版社抓狂的问题是Type 3字体很多老的期刊投稿系统明确不接受Type 3。解决方式通常是尽量用TrueType/OpenType字体比如英文用Helvetica或Times中文用思源黑体保存PDF后转验必要时可以用Ghostscript重新转换。gs -dNOPAUSE -dBATCH -sDEVICEpdfwrite \ -dEmbedAllFontstrue -dSubsetFontstrue \ -sOutputFilefig_embedded.pdf fig.pdf如果论文里完全没有中文字符我建议曲线标签一律用英文这是绕开中文字体各种坑的最简单粗暴方法。5.3 双栏模板里图片宽度的黄金规则Word或LaTeX模板里图片宽度有很多讲究。单栏期刊正文宽度一般是3.3英寸双栏期刊如果要放跨栏图模板会给出7英寸左右的图宽。我的建议是图片宽度不要随便填更不要在Word里用手动拉拽改尺寸那样会破坏图内比例。在LaTeX里我会写成这样% 单栏小图 \includegraphics[width0.98\linewidth]{figures/loss_valid.pdf} % 双栏跨栏大图 \includegraphics[width1.0\linewidth]{figures/comparison.png}如果模板是双栏但某个图想横跨两栏显示用figure*环境而不是figure环境。换环境后不要忘了重新调整图片宽度因为跨栏后的宽度比单栏大了接近一倍原本为单栏设计的字体和线宽放大后会显得略粗。5.4 投稿系统对图片格式的要求与转换工具不同出版社对图片格式的要求差别很大。以我接触到的场景为例Springer系列不少要求TIFF或EPSIEEE系列接受PDF和PNG比较常见Elsevier部分期刊也接受高分辨率TIFF。遇到需要格式转换时我一般这样处理原始格式目标格式推荐命令/工具PNGTIFFmagick convert fig.png -compress LZW fig.tiffPNGEPSmagick convert fig.png -density 600 fig.epsPDFEPS不推荐pdftops -eps fig.pdf fig.epsPDFSVG用于Wordpdf2svg fig.pdf fig.svg要注意的是位图转EPS时一定要设置足够高的密度否则转换结果会非常糊。按600dpi转一般最稳低于300dpi会有明显的锯齿风险。还有一点虽然TIFF普遍带LZW无损压缩但有些投稿系统要求“no compression”那就按压缩参数去掉-compress LZW即可。我个人的体会是论文出图这件事与其最后被导师或审稿人打回重做不如一开始就把可视化脚本当成和训练脚本同等重要的资产来管理。每一次实验产出一份可重复运行的绘图脚本实验结果一更新图片立刻重新生成这是我在多次投稿后觉得收益最大的一个习惯。最后再分享一个小技巧把matplotlib的全局样式抽成一个公共配置文件比如paper_style.py所有图都调用同一套rcParams你的论文图表风格才能真正做到从头到尾统一。这样处理完TensorBoard里的图片才算真正变成了能在论文里站得住脚的结果。

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

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

免费获取报价 →
↑