利用Matplotlib绘制YOLOv5/v7/v8训练结果中的关键指标对比曲线

📅 发布时间:2026/7/13 23:06:02 👁️ 浏览次数:
利用Matplotlib绘制YOLOv5/v7/v8训练结果中的关键指标对比曲线
1. 为什么我们需要可视化训练结果大家好我是老张一个在AI和计算机视觉领域摸爬滚打了十来年的“老码农”。今天想和大家聊聊一个看似简单但实际项目中极其重要的话题如何用Matplotlib把YOLO训练结果画得明明白白。相信很多朋友在用YOLOv5、v7或者v8训练自己的模型时都见过那个自动生成的results.csv或results.txt文件。训练一结束命令行窗口里唰唰唰地滚动着各种数字mAP0.5从0.1慢慢爬到0.8train/box_loss从2.5逐渐降到0.05……这些数字就是模型学习的“心电图”。但问题是光看这些冰冷的数字你真的能直观地判断模型训练得好不好吗哪个模型收敛得更快哪个模型在验证集上表现更稳定当你在YOLOv5、YOLOv7和YOLOv8之间纠结选哪个时又该如何科学地对比我见过不少新手朋友训练完模型看一眼最终的mAP值觉得“还行”就直接拿去用了。结果在实际部署时发现效果远不如预期排查半天才发现模型其实在训练中期就过拟合了后面的训练都是在“瞎练”。这就是不重视训练过程可视化的后果。把训练指标画成曲线图其价值远超你的想象它能帮你诊断模型是否健康比如是否过拟合、欠拟合、帮你比较不同模型或不同超参下的性能、帮你决定何时该提前停止训练以节省宝贵的时间和算力。而Matplotlib作为Python数据可视化的“瑞士军刀”正是完成这项任务最得心应手的工具。它足够灵活可以绘制任何你想要的对比图也足够简单几行代码就能出图。接下来的内容我会手把手带你不仅学会画图更要看懂图背后的故事让你从“只会跑训练”进阶到“真正懂训练”。2. 第一步读懂YOLO的“成绩单”——result文件在动笔写代码之前我们得先搞清楚我们要画的是什么。YOLO不同版本输出的“成绩单”格式略有不同但核心信息是一致的。这就好比不同学校出的成绩单科目排列顺序可能不一样但语文、数学、英语这些主科肯定都在。2.1 YOLOv7的“txt成绩单”YOLOv7比较“传统”训练日志直接输出到一个results.txt文本文件里。你可以用任何文本编辑器打开它。每一行代表一个epoch训练轮次的结果数据之间用空格分隔。看起来可能有点乱但结构很清晰。我截取一行典型的数据给大家拆解一下0/299 14.7G 0.07522 0.009375 0.02266 0.1073 58 640 0.0002958 0.1458 0.0002676 4.469e-05 0.1005 0.01098 0.02545这么多数字别晕我们按顺序来下标从0开始计数0:epoch- 当前训练轮次。1:gpu_mem- GPU内存占用单位是G。2:train/box_loss- 训练集上的边界框回归损失。这个值越低说明模型预测的框位置越准。3:train/obj_loss- 训练集上的目标置信度损失。这个值衡量模型判断一个格子内是否有目标的准确度。4:train/cls_loss- 训练集上的分类损失。这个值衡量模型识别目标类别的准确度。5:train/total_loss- 训练集上的总损失通常是前面三个损失的和。这是我们最关注的训练曲线之一。6:targets- 当前批次图片中标注的目标总数。7:img_size- 输入图片的尺寸。8:precision- 精确率查准率。简单说模型预测出的目标里有多少是真正的目标。这个值高说明模型“瞎猜”得少。9:recall- 召回率查全率。简单说所有真实的目标里模型找出了多少。这个值高说明模型“漏检”得少。10:mAP0.5- 平均精度mean Average Precision在IoU交并比阈值为0.5时的值。这是评估目标检测模型最核心的指标值越高模型整体性能越好。11:mAP0.5:0.95- mAP在IoU阈值从0.5到0.95步长0.05区间内的平均值。这是一个更严格的指标要求预测框的位置必须非常精确。12:val/box_loss- 验证集上的边界框回归损失。13:val/obj_loss- 验证集上的目标置信度损失。14:val/cls_loss- 验证集上的分类损失。重点来了在写代码读取时我们就是通过这些固定的位置下标来获取我们想要的数据的。比如你想画mAP0.5的曲线就需要提取每一行下标为10的那个数字。2.2 YOLOv5/v8的“csv成绩单”YOLOv5和YOLOv8毕竟出自同一家则采用了更结构化的results.csv文件。用Excel或者文本编辑器打开你会看到清晰的分列数据第一行是列名。典型的列名顺序如下同样下标从0开始epoch,train/box_loss,train/obj_loss,train/cls_loss,metrics/precision,metrics/recall,metrics/mAP_0.5,metrics/mAP_0.5:0.95,val/box_loss,val/obj_loss,val/cls_loss,x/lr0,x/lr1,x/lr2对比v7的txt你会发现v5/v8的csv把训练损失和验证损失分得更开并且把精度指标P, R, mAP用metrics/前缀统一放在了中间位置。一个非常重要的共同点是mAP0.5在csv文件中位于第6列下标6mAP0.5:0.95位于第7列下标7。这个规律对于我们后面写通用代码非常关键。为了方便大家记忆和对照我整理了一个表格指标含义YOLOv7 (results.txt) 下标YOLOv5/v8 (results.csv) 下标/列名训练轮次 Epoch00 (epoch)训练边界框损失21 (train/box_loss)训练总损失5(需自行将1,2,3列相加)精确率 Precision84 (metrics/precision)召回率 Recall95 (metrics/recall)核心指标 mAP0.5106 (metrics/mAP_0.5)严格指标 mAP0.5:0.95117 (metrics/mAP_0.5:0.95)验证边界框损失128 (val/box_loss)3. 手把手编码绘制多模型对比曲线理论说再多不如代码跑一遍。下面我就以一个真实的场景为例我手头有训练好的YOLOv5m、YOLOv7和YOLOv8s三个模型的result文件现在想把它们的mAP和Loss曲线画在一起看看谁的表现更好。3.1 环境准备与数据组织首先确保你的Python环境里安装了必要的库。打开你的终端或命令提示符执行pip install matplotlib pandas numpy然后把你的result文件整理好。我建议建立一个清晰的文件夹结构比如project/ ├── plot_curves.py # 我们的绘图脚本 ├── results/ │ ├── yolov5m/ │ │ └── results.csv │ ├── yolov7/ │ │ └── results.txt │ └── yolov8s/ │ └── results.csv └── outputs/ # 用于保存生成的图片3.2 核心绘图代码详解接下来我们创建plot_curves.py文件。我会把代码分成块并加上详细注释。第一步导入库和定义文件路径import matplotlib.pyplot as plt import pandas as pd import numpy as np # 设置全局字体和图像大小让图更美观 plt.rcParams[font.sans-serif] [SimHei] # 用来正常显示中文标签 plt.rcParams[axes.unicode_minus] False # 用来正常显示负号 plt.figure(figsize(12, 8)) # 设置一个较大的画布 if __name__ __main__: # 关键步骤定义你的模型名称和对应的result文件路径 # 格式模型显示名: 文件绝对或相对路径 result_dict { YOLOv5m: r./results/yolov5m/results.csv, YOLOv7: r./results/yolov7/results.txt, YOLOv8s: r./results/yolov8s/results.csv, # 你可以继续添加更多模型比如 # YOLOv7-tiny: r./results/yolov7-tiny/results.txt, }这里用字典来管理模型非常方便键如YOLOv5m会作为图例里的标签值就是文件路径。注意v7是.txtv5和v8是.csv。第二步绘制mAP0.5对比曲线这是我最常看的图直接反映了模型在宽松阈值下的检测能力。# 1. 绘制 mAP0.5 对比图 print(正在绘制 mAP0.5 对比曲线...) plt.subplot(2, 2, 1) # 创建一个2行2列的子图这是第1个位置 for model_name, res_path in result_dict.items(): ext res_path.split(.)[-1] # 获取文件扩展名判断是csv还是txt if ext csv: # 读取csv文件usecols[6]表示只读取第7列mAP0.5 data pd.read_csv(res_path, usecols[6]).values.ravel() else: # 处理txt文件 with open(res_path, r) as f: lines f.readlines() data [] for line in lines: # 分割每一行的数据取下标为10的值mAP0.5 # strip()去掉首尾空格split()按空格分割成列表 values line.strip().split() if len(values) 10: # 防止文件末尾有空行 data.append(float(values[10])) data np.array(data) # 生成x轴就是epoch的序列 x range(len(data)) # 绘制曲线label用于图例linewidth设置线条粗细 plt.plot(x, data, labelmodel_name, linewidth1.5) # 装饰图表 plt.xlabel(训练轮次 (Epochs), fontsize12) plt.ylabel(mAP0.5, fontsize12) plt.title(不同YOLO模型 mAP0.5 对比, fontsize14, fontweightbold) plt.legend(loclower right) # 显示图例放在右下角 plt.grid(True, linestyle--, alpha0.6) # 添加网格线更易读几个踩坑点提醒下标别搞错csv文件里mAP0.5是第6列下标6txt文件里是第10个值下标10。这是最容易出错的地方。数据长度确保所有模型训练的epoch数最好一致这样对比才公平。如果不一样曲线长度不同但Matplotlib也能画。异常值处理有时训练早期mAP可能是0或非常低的值这是正常的。如果出现NaN非数字需要在读取时处理比如用pd.read_csv(...).fillna(0)。第三步绘制mAP0.5:0.95对比曲线这张图看的是模型的“严苛”性能。# 2. 绘制 mAP0.5:0.95 对比图 print(正在绘制 mAP0.5:0.95 对比曲线...) plt.subplot(2, 2, 2) for model_name, res_path in result_dict.items(): ext res_path.split(.)[-1] if ext csv: data pd.read_csv(res_path, usecols[7]).values.ravel() # 下标改为7 else: with open(res_path, r) as f: lines f.readlines() data [] for line in lines: values line.strip().split() if len(values) 11: # 下标改为11 data.append(float(values[11])) data np.array(data) x range(len(data)) plt.plot(x, data, labelmodel_name, linewidth1.5) plt.xlabel(训练轮次 (Epochs), fontsize12) plt.ylabel(mAP0.5:0.95, fontsize12) plt.title(不同YOLO模型 mAP0.5:0.95 对比, fontsize14, fontweightbold) plt.legend(loclower right) plt.grid(True, linestyle--, alpha0.6)第四步绘制训练总损失对比曲线损失曲线能告诉我们模型学习得是否平稳、是否过拟合。# 3. 绘制训练总损失对比图 print(正在绘制训练总损失对比曲线...) plt.subplot(2, 2, 3) for model_name, res_path in result_dict.items(): ext res_path.split(.)[-1] if ext csv: # 对于csv需要把三个训练损失相加得到总损失 box_loss pd.read_csv(res_path, usecols[1]).values.ravel() obj_loss pd.read_csv(res_path, usecols[2]).values.ravel() cls_loss pd.read_csv(res_path, usecols[3]).values.ravel() # 相加并保留5位小数保持和v7输出格式一致 data np.round(box_loss obj_loss cls_loss, 5) else: # txt文件第5个值下标5直接就是train/total_loss with open(res_path, r) as f: lines f.readlines() data [] for line in lines: values line.strip().split() if len(values) 5: data.append(float(values[5])) data np.array(data) x range(len(data)) plt.plot(x, data, labelmodel_name, linewidth1.5) plt.xlabel(训练轮次 (Epochs), fontsize12) plt.ylabel(训练总损失 (Total Loss), fontsize12) plt.title(不同YOLO模型训练损失对比, fontsize14, fontweightbold) plt.legend(locupper right) # 损失曲线通常希望下降图例放右上 plt.grid(True, linestyle--, alpha0.6)第五步绘制验证集边界框损失对比曲线单独看验证集损失是发现过拟合的“金标准”。# 4. 绘制验证集边界框损失对比图 (val/box_loss) print(正在绘制验证集边界框损失对比曲线...) plt.subplot(2, 2, 4) for model_name, res_path in result_dict.items(): ext res_path.split(.)[-1] if ext csv: # csv文件中val/box_loss在第8列下标8 data pd.read_csv(res_path, usecols[8]).values.ravel() else: # txt文件中val/box_loss在第12列下标12 with open(res_path, r) as f: lines f.readlines() data [] for line in lines: values line.strip().split() if len(values) 12: data.append(float(values[12])) data np.array(data) x range(len(data)) plt.plot(x, data, labelmodel_name, linewidth1.5) plt.xlabel(训练轮次 (Epochs), fontsize12) plt.ylabel(验证集边界框损失 (val/box_loss), fontsize12) plt.title(不同YOLO模型验证损失对比, fontsize14, fontweightbold) plt.legend(locupper right) plt.grid(True, linestyle--, alpha0.6)第六步调整布局并保存# 调整子图之间的间距使布局更紧凑美观 plt.tight_layout() # 保存高清图片dpi越高图片越清晰适合放入论文或报告 output_path ./outputs/model_comparison.png plt.savefig(output_path, dpi600, bbox_inchestight) print(f对比图已保存至{output_path}) # 显示图片 plt.show()把上面所有代码块按顺序组合起来就是一个完整的脚本。运行它你就能得到一张包含4个子图的综合对比图一眼就能看出各个模型在不同指标上的表现。4. 进阶技巧与实战经验分享基础的对比图会画了但要想真正成为“读图高手”还得掌握一些进阶技巧。这些都是我在实际项目中踩过坑后总结出来的。4.1 如何解读曲线图并指导调优拿到对比图后怎么看我教你几招看mAP曲线的“天花板”和“爬升速度”最终收敛的mAP值高说明模型潜力大。曲线前期爬升越快说明模型学习能力越强或者你的学习率设置得比较合适。如果某条曲线一直震荡上不去可能是学习率太大了。看损失曲线的“收敛性”和“间隙”理想的训练损失曲线应该是平滑下降并最终趋于平稳。如果曲线后期还在剧烈震荡说明训练不稳定可以尝试减小学习率。最关键的是对比训练损失和验证损失如果训练损失一直下降但验证损失在中后期开始反弹上升这就是典型的过拟合信号。意味着模型只记住了训练集的特例而没学会通用规律。这时候就该考虑提前终止训练、增加数据增强、或者给模型加一些正则化如Dropout了。看曲线的“平滑度”如果曲线像锯齿一样波动很大除了学习率问题还可能是因为批次大小Batch Size设得太小。增大Batch Size通常可以使梯度下降更稳定曲线更平滑。4.2 绘制PR曲线和F1曲线除了mAP和Loss精确率Precision和召回率Recall也是一对非常重要的矛盾指标。有时候我们需要根据具体场景权衡二者。比如在安防领域我们宁可误报低精确率也不能漏报高召回率。而PR曲线和F1分数能很好地反映这种权衡。YOLO在验证时不会自动生成PR曲线数据但我们可以通过一个“迂回”的方法获取。通常我们需要在验证时运行val.py添加参数让它生成包含每个类别在不同置信度阈值下的精确率和召回率数据。Ultralytics的YOLOv5/v8和YOLOv7官方代码可能略有不同但核心思路是调用其验证函数并捕获输出。由于这部分代码涉及对原验证脚本的修改且不同版本差异较大我建议一个更通用的思路关注result文件中已有的metrics/precision和metrics/recall列v5/v8或第8、9个值v7。我们可以直接把每个epoch的P和R画出来观察它们随训练的变化趋势。虽然这不是标准的PR曲线但同样极具参考价值。# 示例绘制Precision和Recall随Epoch的变化 plt.figure(figsize(14, 5)) # 子图1: Precision plt.subplot(1, 2, 1) for model_name, res_path in result_dict.items(): ext res_path.split(.)[-1] if ext csv: data pd.read_csv(res_path, usecols[4]).values.ravel() # precision列 else: data [] with open(res_path, r) as f: for line in f: vals line.strip().split() if len(vals) 8: data.append(float(vals[8])) data np.array(data) plt.plot(range(len(data)), data, labelmodel_name, linewidth1.5) plt.xlabel(Epochs) plt.ylabel(Precision) plt.title(Precision over Epochs) plt.legend() plt.grid(True) # 子图2: Recall plt.subplot(1, 2, 2) for model_name, res_path in result_dict.items(): ext res_path.split(.)[-1] if ext csv: data pd.read_csv(res_path, usecols[5]).values.ravel() # recall列 else: data [] with open(res_path, r) as f: for line in f: vals line.strip().split() if len(vals) 9: data.append(float(vals[9])) data np.array(data) plt.plot(range(len(data)), data, labelmodel_name, linewidth1.5) plt.xlabel(Epochs) plt.ylabel(Recall) plt.title(Recall over Epochs) plt.legend() plt.grid(True) plt.tight_layout() plt.savefig(./outputs/precision_recall_trend.png, dpi300) plt.show()通过观察P和R的趋势你可以判断模型是更偏向“精准”还是“全召回”。如果某个模型Precision很高但Recall很低说明它很保守只检测非常有把握的目标反之则说明它很激进容易误检。4.3 定制化你的图表让结果更专业默认的图表可能不够美观或者你想突出某些信息。Matplotlib提供了丰富的定制选项设置颜色和线型在plt.plot()里加上colorred和linestyle--参数可以指定颜色和虚线。当对比模型很多时用不同线型实线、虚线、点划线比只用颜色区分更友好。添加关键点标记比如标出每个模型达到最高mAP的epoch点。max_map_epoch np.argmax(data) # 找到最大值的索引 max_map_value data[max_map_epoch] plt.plot(max_map_epoch, max_map_value, ro) # 画一个红点 plt.annotate(fMax: {max_map_value:.3f}, xy(max_map_epoch, max_map_value), xytext(10,10), textcoordsoffset points, fontsize9) # 添加文本标注调整坐标轴范围使用plt.xlim([0, 200])和plt.ylim([0, 1])可以固定x轴和y轴的范围使得多张图对比时尺度一致。保存为矢量图在plt.savefig()时除了设置高dpi还可以保存为.pdf或.svg格式。这种矢量图放大无数倍都不会失真非常适合插入学术论文或技术报告中。4.4 自动化与批处理脚本如果你经常需要训练和比较多个模型每次都手动改路径太麻烦。可以写一个更智能的脚本import os import glob def find_result_files(root_dir): 自动搜索指定目录下所有results.csv和results.txt文件 result_dict {} # 使用glob递归查找 csv_files glob.glob(os.path.join(root_dir, **, results.csv), recursiveTrue) txt_files glob.glob(os.path.join(root_dir, **, results.txt), recursiveTrue) all_files csv_files txt_files for file_path in all_files: # 用包含的文件夹名作为模型名更直观 model_name os.path.basename(os.path.dirname(file_path)) result_dict[model_name] file_path return result_dict # 在主函数中使用 if __name__ __main__: # 只需指定你的所有实验结果存放的根目录 root_directory ./all_experiments/ result_dict find_result_files(root_directory) if not result_dict: print(未找到任何result文件请检查路径。) else: print(f找到 {len(result_dict)} 个模型的训练结果:) for name, path in result_dict.items(): print(f - {name}: {path}) # 接下来调用之前的绘图函数...这个脚本会自动遍历all_experiments/文件夹下的所有子文件夹找到result文件并用文件夹名命名模型非常适合管理大量实验。5. 常见问题与避坑指南在我多年的使用中总结了一些新手最容易遇到的问题“下标越界”错误这是最常见的问题。根本原因就是没搞清楚v5/v8的csv和v7的txt里数据列的位置下标不同。务必回头仔细核对本文第二部分的那张表格或者亲自打开你的result文件数一数你的目标数据在第几列从0开始数。曲线为空白或只有几个点首先检查你的result文件是否真的包含了足够多epoch的数据。有时候训练提前中断文件可能不完整。其次检查读取数据的代码逻辑特别是处理txt文件时strip().split()是否能正确分割每一行。可以在读取后打印一下data的长度和前几个值看看。图表太杂乱看不清当对比的模型超过5个时线条和颜色会很难区分。解决办法分开展示不要把所有指标所有模型都挤在一张图里。可以一个模型画一张综合图包含mAP、loss等或者一个指标画一张图只对比该指标。使用子图就像我们上面的例子用plt.subplot把不同指标分到不同子图里。简化对比每次只重点对比2-3个你最关心的模型。YOLOv8的result文件列名变了是的不同小版本间可能有细微调整。最稳妥的方法是用pandas读取csv后先打印一下列名print(pd.read_csv(‘results.csv’).columns)。然后根据实际的列名来选择比如用usecols[‘metrics/mAP_0.5’]而不是usecols[6]。想画其他指标怎么办原理完全一样只要你知道这个指标在文件中的位置下标。比如你想画train/box_loss在csv里它是第1列下标1在txt里是第2个值下标2。修改代码中usecols或split()后的下标数字即可。最后再分享一个我的个人习惯每次训练完一个模型我都会立刻运行这个绘图脚本把曲线图保存下来并和模型权重文件放在同一个文件夹里。这样以后回顾项目时一眼就能知道每个模型的“训练生平”做模型选型或者问题回溯时效率会高得多。可视化不是训练的终点而是理解模型、优化模型的新起点。希望这篇文章和这些代码能成为你AI开发工具箱里一件趁手的利器。