YOLOv8训练日志解析与可视化实战指南
1. YOLOv8训练日志解析基础
在目标检测模型的训练过程中,日志分析是优化模型性能的关键环节。YOLOv8作为当前最先进的实时目标检测算法,其训练过程会产生丰富的日志数据,这些数据蕴含着模型学习过程的重要信息。掌握日志解析技术,能够帮助我们深入理解模型行为,及时发现训练问题,并做出针对性调整。
1.1 日志文件结构与核心指标
YOLOv8训练过程中会自动生成两种格式的日志文件:结构化的CSV文件和详细的文本日志。results.csv文件采用标准的表格格式存储,每一行代表一个epoch的训练数据,包含以下关键列:
- epoch:训练轮次序号
- train/box_loss:边界框回归损失
- train/obj_loss:目标置信度损失
- train/cls_loss:分类损失
- metrics/precision:精确率
- metrics/recall:召回率
- metrics/mAP50:IoU阈值为0.5时的平均精度
- metrics/mAP50-95:IoU阈值从0.5到0.95的平均精度
这些指标构成了评估模型性能的多维度指标体系。box_loss反映边界框定位的准确性,obj_loss衡量目标检测的置信度,cls_loss则体现分类的正确性。三个损失函数的加权和构成了总损失函数,直接指导模型参数的优化方向。
实际项目中,我通常会重点关注mAP50-95的变化趋势。这个指标对边界框的定位精度要求更高,能更全面地反映模型的实际检测能力。当mAP50表现良好但mAP50-95较低时,往往说明模型的定位精度有待提升。
1.2 指标的业务意义与技术内涵
理解每个指标的技术定义和业务含义是进行有效分析的前提:
损失函数指标 :
-
Box Loss:计算预测框与真实框的CIoU损失,反映定位误差。计算公式为:
CIoU = IoU - (ρ²(b_pred,b_gt)/c² + αv)其中ρ表示中心点距离,c是最小外接矩形对角线长度,α是权重系数,v衡量长宽比一致性。
-
Obj Loss:采用二元交叉熵,判断网格单元是否包含目标。这个指标异常波动可能预示正负样本不平衡问题。
-
Cls Loss:多分类交叉熵损失,评估类别预测准确性。在类别不平衡的数据集上需要特别关注。
评估指标 :
-
Precision/Recall:构成PR曲线的两个关键指标。在安防等场景中,高Recall往往更重要;而在内容审核中,高Precision通常是首要目标。
-
mAP:基于PR曲线下面积计算,是目标检测的核心评估标准。mAP50对定位误差容忍度较高,而mAP50-95则要求严格的定位精度。
1.3 解析环境配置与工具选型
为高效分析训练日志,我们需要搭建专业的Python分析环境。以下是经过多个项目验证的工具组合:
# 日志解析核心工具栈
pandas==1.5.3 # 数据处理与分析
numpy==1.23.5 # 数值计算
matplotlib==3.6.2 # 基础可视化
seaborn==0.12.2 # 统计可视化
plotly==5.11.0 # 交互式可视化
scikit-learn==1.2.0 # 数据分析工具
# 可选的高级工具
pyarrow==8.0.0 # 加速大数据处理
tqdm==4.64.1 # 进度显示
jupyterlab==3.5.0 # 交互式分析环境
在硬件配置方面,对于大型训练日志(如超过1000个epoch的记录),建议:
- 使用SSD存储加速数据读取
- 为pandas配置pyarrow后端提升处理效率
- 对超大规模数据考虑使用Dask进行分布式处理
# 推荐安装命令(使用清华镜像源加速)
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
2. 日志数据提取与预处理技术
2.1 CSV文件的高效解析方法
使用pandas读取CSV文件时,合理的参数配置可以显著提升处理效率。以下是优化后的日志解析器实现:
import pandas as pd
from pathlib import Path
import numpy as np
class EnhancedLogParser:
def __init__(self, csv_path, dtype=None, parse_dates=False):
"""
增强型日志解析器
:param csv_path: CSV文件路径
:param dtype: 列数据类型指定(提升加载速度)
:param parse_dates: 是否解析日期列
"""
self.csv_path = Path(csv_path)
self.raw_data = None
self.processed_data = None
self.dtype = dtype or {
'epoch': 'int32',
'train/box_loss': 'float32',
'metrics/mAP50': 'float32'
}
self._load_data()
def _load_data(self):
"""优化数据加载流程"""
try:
# 使用低内存占用模式读取
self.raw_data = pd.read_csv(
self.csv_path,
dtype=self.dtype,
engine='c', # 使用C引擎加速
float_precision='high'
)
# 自动检测并填充缺失值
self._handle_missing_values()
# 转换epoch为索引
self.raw_data.set_index('epoch', inplace=True)
print(f"数据加载完成,共{len(self.raw_data)}个epoch记录")
print(f"内存使用量:{self.raw_data.memory_usage().sum()/1024:.2f} KB")
except Exception as e:
print(f"数据加载失败:{str(e)}")
raise
def _handle_missing_values(self):
"""智能处理缺失值"""
# 检测各列缺失率
missing_ratio = self.raw_data.isnull().mean()
for col in missing_ratio[missing_ratio > 0].index:
if missing_ratio[col] < 0.1: # 少量缺失使用前后填充
self.raw_data[col] = self.raw_data[col].fillna(
method='ffill').fillna(method='bfill')
else: # 大量缺失使用插值
self.raw_data[col] = self.raw_data[col].interpolate()
# 记录处理结果
print(f"缺失值处理完成,各列缺失率:\n{missing_ratio.to_string()}")
2.2 文本日志的智能解析
对于train.log文件,我们需要处理非结构化的文本数据。以下是结合正则表达式和状态机的增强解析器:
import re
from collections import defaultdict
class TextLogParser:
def __init__(self, log_path):
self.log_path = Path(log_path)
self.epoch_data = defaultdict(dict)
self._parse_log()
def _parse_log(self):
# 编译多个正则模式
epoch_pattern = re.compile(
r'Epoch\s+(\d+)/\d+.+?'
r'box_loss=([\d.]+).+?'
r'obj_loss=([\d.]+).+?'
r'cls_loss=([\d.]+).+?'
r'Precision=([\d.]+).+?'
r'Recall=([\d.]+).+?'
r'mAP50=([\d.]+).+?'
r'mAP50-95=([\d.]+)'
)
lr_pattern = re.compile(r'lr/pg\d+\s*:\s*([\d.e-]+)')
time_pattern = re.compile(r'Time:\s*([\d.]+)ms')
with open(self.log_path, 'r', encoding='utf-8') as f:
current_epoch = 0
for line in f:
# 匹配epoch数据
epoch_match = epoch_pattern.search(line)
if epoch_match:
current_epoch = int(epoch_match.group(1))
self.epoch_data[current_epoch].update({
'box_loss': float(epoch_match.group(2)),
'obj_loss': float(epoch_match.group(3)),
'cls_loss': float(epoch_match.group(4)),
'precision': float(epoch_match.group(5)),
'recall': float(epoch_match.group(6)),
'mAP50': float(epoch_match.group(7)),
'mAP50-95': float(epoch_match.group(8))
})
# 匹配学习率
lr_match = lr_pattern.search(line)
if lr_match and current_epoch:
self.epoch_data[current_epoch]['lr'] = float(lr_match.group(1))
# 匹配耗时
time_match = time_pattern.search(line)
if time_match and current_epoch:
self.epoch_data[current_epoch]['time'] = float(time_match.group(1))
# 转换为DataFrame
self.df = pd.DataFrame.from_dict(self.epoch_data, orient='index')
self.df.index.name = 'epoch'
print(f"解析完成,共提取{len(self.df)}个epoch的详细数据")
2.3 数据清洗与质量验证
获得原始数据后,需要进行严格的质量检查:
class DataQualityChecker:
@staticmethod
def validate_training_log(df):
"""验证训练日志数据质量"""
report = []
# 检查指标范围合理性
metrics_ranges = {
'train/box_loss': (0, 10),
'metrics/mAP50': (0, 1)
}
for metric, (min_val, max_val) in metrics_ranges.items():
if metric in df.columns:
out_of_range = ~df[metric].between(min_val, max_val)
if out_of_range.any():
report.append(f"警告:{metric}有{out_of_range.sum()}条记录超出合理范围({min_val}-{max_val})")
# 检查指标单调性
increasing_metrics = ['metrics/mAP50', 'metrics/mAP50-95']
for metric in increasing_metrics:
if metric in df.columns:
decreasing = df[metric].diff() < -0.05 # 允许小幅波动
if decreasing.any():
report.append(f"注意:{metric}在{decreasing.sum()}个epoch出现异常下降")
# 检查数据连续性
missing_epochs = set(range(df.index.min(), df.index.max()+1)) - set(df.index)
if missing_epochs:
report.append(f"严重:缺失{len(missing_epochs)}个epoch的数据,缺失的epoch:{sorted(missing_epochs)[:5]}...")
return report or ["数据质量检查通过,未发现明显问题"]
3. 关键指标可视化技术
3.1 专业级训练曲线绘制
使用matplotlib绘制出版级质量的训练曲线:
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
import seaborn as sns
class TrainingVisualizer:
def __init__(self, data, style='seaborn'):
self.data = data
plt.style.use(style)
sns.set_palette("husl", 8)
self.colors = plt.rcParams['axes.prop_cycle'].by_key()['color']
def plot_combined_metrics(self, metrics, figsize=(14, 8), save_path=None):
"""
绘制组合指标图
:param metrics: 要绘制的指标列表
:param figsize: 图像尺寸
:param save_path: 保存路径
"""
fig, axes = plt.subplots(2, 1, figsize=figsize,
gridspec_kw={'height_ratios': [2, 1]})
# 上部:主要指标
for i, metric in enumerate(metrics):
if metric in self.data.columns:
axes[0].plot(self.data.index, self.data[metric],
label=metric.replace('metrics/', '').replace('train/', ''),
color=self.colors[i], linewidth=2)
axes[0].set_title('训练关键指标趋势', fontsize=14, pad=20)
axes[0].set_ylabel('指标值', fontsize=12)
axes[0].legend(loc='upper left', bbox_to_anchor=(1, 1))
axes[0].grid(True, linestyle='--', alpha=0.6)
axes[0].xaxis.set_major_locator(MaxNLocator(integer=True))
# 下部:损失函数
loss_metrics = [m for m in ['train/box_loss', 'train/obj_loss', 'train/cls_loss']
if m in self.data.columns]
for metric in loss_metrics:
axes[1].plot(self.data.index, self.data[metric],
label=metric.replace('train/', ''),
linewidth=1.5)
axes[1].set_title('损失函数变化', fontsize=14, pad=20)
axes[1].set_xlabel('Epoch', fontsize=12)
axes[1].set_ylabel('Loss', fontsize=12)
axes[1].legend(loc='upper left', bbox_to_anchor=(1, 1))
axes[1].grid(True, linestyle='--', alpha=0.6)
axes[1].xaxis.set_major_locator(MaxNLocator(integer=True))
plt.tight_layout()
if save_path:
plt.savefig(save_path, dpi=300, bbox_inches='tight')
plt.show()
3.2 交互式可视化实现
使用Plotly创建动态可交互的可视化:
import plotly.graph_objects as go
from plotly.subplots import make_subplots
class InteractiveVisualizer:
def __init__(self, data):
self.data = data.reset_index()
def create_dashboard(self, metrics=None, save_path=None):
"""
创建交互式仪表板
:param metrics: 要展示的指标列表
:param save_path: HTML保存路径
"""
metrics = metrics or ['metrics/mAP50', 'metrics/precision']
fig = make_subplots(
rows=2, cols=2,
specs=[[{"type": "xy"}, {"type": "xy"}],
[{"type": "xy", "colspan": 2}, None]],
subplot_titles=("mAP指标趋势", "精确率/召回率", "损失函数变化")
)
# mAP指标
fig.add_trace(
go.Scatter(x=self.data['epoch'], y=self.data['metrics/mAP50'],
name='mAP50', line=dict(color='blue')),
row=1, col=1
)
fig.add_trace(
go.Scatter(x=self.data['epoch'], y=self.data['metrics/mAP50-95'],
name='mAP50-95', line=dict(color='green')),
row=1, col=1
)
# 精确率/召回率
fig.add_trace(
go.Scatter(x=self.data['epoch'], y=self.data['metrics/precision'],
name='Precision', line=dict(color='red')),
row=1, col=2
)
fig.add_trace(
go.Scatter(x=self.data['epoch'], y=self.data['metrics/recall'],
name='Recall', line=dict(color='orange')),
row=1, col=2
)
# 损失函数
loss_metrics = ['train/box_loss', 'train/obj_loss', 'train/cls_loss']
colors = ['#1f77b4', '#ff7f0e', '#2ca02c']
for metric, color in zip(loss_metrics, colors):
fig.add_trace(
go.Scatter(x=self.data['epoch'], y=self.data[metric],
name=metric.replace('train/', ''),
line=dict(color=color)),
row=2, col=1
)
# 更新布局
fig.update_layout(
title='YOLOv8训练指标交互式仪表板',
height=800,
hovermode='x unified',
showlegend=True,
template='plotly_white'
)
# 更新坐标轴标签
fig.update_xaxes(title_text="Epoch", row=2, col=1)
fig.update_yaxes(title_text="mAP值", row=1, col=1)
fig.update_yaxes(title_text="百分比", row=1, col=2)
fig.update_yaxes(title_text="Loss值", row=2, col=1)
if save_path:
fig.write_html(save_path)
fig.show()
3.3 自定义主题与样式优化
创建统一的视觉风格:
class CustomTheme:
@staticmethod
def set_tech_theme():
"""设置科技感主题"""
plt.style.use('default')
params = {
'axes.facecolor': '#f5f5f5',
'figure.facecolor': 'white',
'axes.grid': True,
'grid.color': 'white',
'grid.linewidth': 1.5,
'axes.edgecolor': '#333333',
'axes.linewidth': 1,
'xtick.color': '#333333',
'ytick.color': '#333333',
'text.color': '#333333',
'font.family': ['Arial', 'DejaVu Sans'],
'font.size': 10,
'axes.titlesize': 12,
'axes.labelsize': 10,
'legend.fontsize': 9,
'figure.titlesize': 14,
'lines.linewidth': 2.5
}
plt.rcParams.update(params)
return {
'primary': '#2c7be5',
'secondary': '#d26c4e',
'success': '#5cb85c',
'danger': '#d9534f',
'warning': '#f0ad4e',
'info': '#5bc0de'
}
@staticmethod
def create_colormap(metric_type):
"""
根据指标类型返回合适的颜色映射
:param metric_type: 'loss'/'metric'/'lr'
"""
if metric_type == 'loss':
return ['#e41a1c', '#377eb8', '#4daf4a'] # 红蓝绿
elif metric_type == 'metric':
return ['#984ea3', '#ff7f00', '#ffff33'] # 紫橙黄
else:
return ['#a65628', '#f781bf', '#999999'] # 棕粉灰
4. 深度分析技术
4.1 训练稳定性评估方法
训练稳定性直接影响模型最终性能。以下是量化评估方法:
class StabilityAnalyzer:
def __init__(self, data, window=10):
self.data = data
self.window = window
def calculate_volatility(self, metric):
"""计算指标波动率"""
series = self.data[metric]
# 计算滚动标准差
rolling_std = series.rolling(self.window).std()
# 计算变异系数
cv = rolling_std.mean() / series.mean()
# 计算最大回撤
max_drawdown = (series.max() - series.min()) / series.max()
return {
'rolling_std_mean': rolling_std.mean(),
'coefficient_of_variation': cv,
'max_drawdown': max_drawdown,
'stability_score': 1 / (cv + 1e-6) # 避免除零
}
def detect_anomalies(self, metric, method='iqr', threshold=1.5):
"""检测异常训练阶段"""
series = self.data[metric]
if method == 'iqr':
q1 = series.quantile(0.25)
q3 = series.quantile(0.75)
iqr = q3 - q1
lower = q1 - threshold * iqr
upper = q3 + threshold * iqr
anomalies = ~series.between(lower, upper)
elif method == 'zscore':
zscore = (series - series.mean()) / series.std()
anomalies = abs(zscore) > threshold
else:
raise ValueError("Method must be 'iqr' or 'zscore'")
anomaly_epochs = self.data.index[anomalies].tolist()
# 分析异常阶段特征
analysis = {}
if anomaly_epochs:
anomaly_data = self.data.loc[anomaly_epochs]
analysis = {
'count': len(anomaly_epochs),
'mean_value': anomaly_data[metric].mean(),
'min_epoch': min(anomaly_epochs),
'max_epoch': max(anomaly_epochs),
'related_metrics': self._find_correlated_metrics(metric, anomaly_epochs)
}
return anomaly_epochs, analysis
def _find_correlated_metrics(self, target_metric, anomaly_epochs):
"""寻找相关性高的其他指标"""
corr_results = []
normal_data = self.data.drop(anomaly_epochs, errors='ignore')
anomaly_data = self.data.loc[anomaly_epochs]
for metric in self.data.columns:
if metric != target_metric and self.data[metric].dtype in ['float64', 'float32']:
# 计算正常阶段和异常阶段的差异
normal_mean = normal_data[metric].mean()
anomaly_mean = anomaly_data[metric].mean()
change = (anomaly_mean - normal_mean) / normal_mean
if abs(change) > 0.1: # 变化超过10%认为相关
corr_results.append({
'metric': metric,
'change_percent': change * 100,
'normal_mean': normal_mean,
'anomaly_mean': anomaly_mean
})
# 按变化幅度排序
return sorted(corr_results, key=lambda x: abs(x['change_percent']), reverse=True)
4.2 过拟合诊断与解决方案
过拟合是训练过程中最常见的问题之一。以下是系统化的诊断方法:
class OverfittingDetector:
def __init__(self, train_data, val_data=None):
self.train_data = train_data
self.val_data = val_data
def diagnose(self, primary_metric='metrics/mAP50-95'):
"""全面诊断过拟合情况"""
diagnosis = {
'status': 'normal',
'confidence': 0,
'indicators': {},
'suggestions': []
}
if self.val_data is None:
diagnosis['status'] = 'unknown'
diagnosis['suggestions'].append("缺少验证集数据,无法进行过拟合诊断")
return diagnosis
# 指标1:训练-验证差距
train_final = self.train_data[primary_metric].iloc[-10:].mean()
val_final = self.val_data[primary_metric].iloc[-10:].mean()
gap = train_final - val_final
diagnosis['indicators']['final_gap'] = {
'value': gap,
'threshold': 0.05
}
# 指标2:验证集指标趋势
val_trend = self._calculate_trend(self.val_data[primary_metric])
diagnosis['indicators']['val_trend'] = {
'value': val_trend,
'threshold': -0.001
}
# 指标3:损失函数比值
if 'train/box_loss' in self.train_data.columns:
train_loss = self.train_data['train/box_loss'].iloc[-1]
val_loss = self.val_data['val/box_loss'].iloc[-1]
loss_ratio = val_loss / train_loss
diagnosis['indicators']['loss_ratio'] = {
'value': loss_ratio,
'threshold': 1.2
}
# 综合判断
overfitting_signs = 0
if gap > 0.05:
overfitting_signs += 1
diagnosis['suggestions'].append(
f"训练-验证差距较大({gap:.3f}),建议增加正则化或数据增强")
if val_trend < -0.001:
overfitting_signs += 1
diagnosis['suggestions'].append(
f"验证指标呈下降趋势(斜率={val_trend:.5f}),建议早停或减小学习率")
if 'loss_ratio' in diagnosis['indicators'] and loss_ratio > 1.2:
overfitting_signs += 1
diagnosis['suggestions'].append(
f"验证损失显著高于训练损失(ratio={loss_ratio:.2f}),可能出现过拟合")
# 确定诊断结果
if overfitting_signs >= 2:
diagnosis['status'] = 'overfitting'
diagnosis['confidence'] = overfitting_signs / 3
elif overfitting_signs == 1:
diagnosis['status'] = 'potential_overfitting'
diagnosis['confidence'] = 0.5
return diagnosis
def _calculate_trend(self, series, window=10):
"""计算序列的线性趋势斜率"""
if len(series) < window:
window = len(series)
x = np.arange(window)
y = series.values[-window:]
slope = np.polyfit(x, y, 1)[0]
return slope
4.3 学习率调度分析技术
学习率是影响训练效果的最关键超参数。以下是分析学习率调度效果的完整方案:
class LRAnalyzer:
def __init__(self, data, lr_col='lr'):
self.data = data
self.lr_col = lr_col
def analyze_schedule(self):
"""全面分析学习率调度效果"""
lrs = self.data[self.lr_col]
analysis = {
'initial_lr': lrs.iloc[0],
'final_lr': lrs.iloc[-1],
'total_changes': self._count_lr_changes(lrs),
'schedule_type': self._detect_schedule_type(lrs),
'optimal_range': self._find_optimal_range(lrs)
}
return analysis
def _count_lr_changes(self, lrs):
"""统计学习率变化次数"""
changes = np.diff(lrs) != 0
return np.sum(changes)
def _detect_schedule_type(self, lrs):
"""识别学习率调度策略类型"""
diff = np.diff(lrs)
if np.all(diff <= 0): # 单调递减
if len(np.unique(lrs)) < 5: # 阶梯式下降
return 'step_decay'
else: # 连续下降
return 'continuous_decay'
elif np.any(diff > 0): # 有上升
return 'cyclic'
else: # 恒定
return 'constant'
def _find_optimal_range(self, lrs, metric='metrics/mAP50'):
"""寻找最佳学习率范围"""
if metric not in self.data.columns:
return None
# 计算指标变化率
metric_vals = self.data[metric]
metric_growth = metric_vals.diff().rolling(5).mean()
# 找出指标增长最快的阶段
best_window = metric_growth.idxmax()
window_size = 10
start = max(0, best_window - window_size)
end = min(len(lrs), best_window + window_size)
return {
'start_epoch': int(start),
'end_epoch': int(end),
'min_lr': float(lrs.iloc[start:end].min()),
'max_lr': float(lrs.iloc[start:end].max()),
'avg_metric_growth': float(metric_growth.iloc[start:end].mean())
}
def plot_lr_impact(self, metrics, figsize=(12, 8)):
"""可视化学习率对指标的影响"""
fig, ax1 = plt.subplots(figsize=figsize)
# 绘制学习率曲线
ax1.plot(self.data.index, self.data[self.lr_col],
color='tab:blue', label='Learning Rate')
ax1.set_xlabel('Epoch')
ax1.set_ylabel('Learning Rate', color='tab:blue')
ax1.tick_params(axis='y', labelcolor='tab:blue')
ax1.set_yscale('log')
# 创建第二个y轴
ax2 = ax1.twinx()
# 绘制各指标曲线
colors = ['tab:red', 'tab:green', 'tab:purple']
for i, metric in enumerate(metrics):
if metric in self.data.columns:
ax2.plot(self.data.index, self.data[metric],
color=colors[i], label=metric.replace('metrics/', ''))
ax2.set_ylabel('Metric Value', color='tab:red')
ax2.tick_params(axis='y', labelcolor='tab:red')
# 添加图例和标题
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')
plt.title('学习率对训练指标的影响', fontsize=14)
plt.grid(True, linestyle='--', alpha=0.3)
plt.show()
5. 自动化报告生成系统
5.1 专业报告模板设计
使用Jinja2模板引擎生成HTML格式的自动化报告:
from jinja2 import Template
import base64
from io import BytesIO
class ReportGenerator:
def __init__(self, analysis_results):
self.results = analysis_results
self.template = self._load_template()
def _load_template(self):
"""加载HTML模板"""
return Template('''
<!DOCTYPE html>
<html>
<head>
<title>YOLOv8训练分析报告</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.header { background-color: #2c3e50; color: white; padding: 20px; border-radius: 5px; }
.section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
.metric-card { background: #f8f9fa; padding: 10px; margin: 10px 0; border-left: 4px solid #3498db; }
.plot-container { margin: 20px 0; text-align: center; }
table { width: 100%; border-collapse: collapse; margin: 10px 0; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #3498db; color: white; }
.warning { background-color: #fff3cd; padding: 10px; border-left: 4px solid #ffc107; }
.danger { background-color: #f8d7da; padding: 10px; border-left: 4px solid #dc3545; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>YOLOv8训练分析报告</h1>
<p>生成时间: {{ timestamp }}</p>
</div>
<div class="section">
<h2>训练概览</h2>
<div class="row">
<div class="metric-card">
<h3>训练轮数</h3>
<p>{{ epochs }} epochs</p>
</div>
<div class="metric-card">
<h3>最佳mAP50</h3>
<p>{{ best_map50 | round(3) }}</p>
</div>
</div>
<div class="plot-container">
<img src="data:image/png;base64,{{ loss_plot }}" alt="损失曲线" style="max-width: 100%;">
</div>
</div>
<div class="section">
<h2>稳定性分析</h2>
{% if stability.issues %}
<div class="warning">
<h3>⚠️ 稳定性问题</h3>
<ul>
{% for issue in stability.issues %}
<li>{{ issue }}</li>
{% endfor %}
</ul>
</div>
{% else %}
<p>训练过程稳定,未检测到明显问题</p>
{% endif %}
<table>
<tr>
<th>指标</th>
<th>变异系数</th>
<th>最大波动</th>
</tr>
{% for metric, data in stability.metrics.items() %}
<tr>
<td>{{ metric }}</td>
<td>{{ data.cv | round(4) }}</td>
<td>{{ data.max_fluctuation | round(4) }}</td>
</tr>
{% endfor %}
</table>
</div>
<div class="section">
<h2>过拟合诊断</h2>
{% if overfitting.status != 'normal' %}
<div class="{{ 'danger' if overfitting.status == 'overfitting' else 'warning' }}">
<h3>{% if overfitting.status == 'overfitting' %}❌ 过拟合{% else %}⚠️ 潜在过拟合{% endif %}</h3>
<p>置信度: {{ (overfitting.confidence * 100) | round(1) }}%</p>
<ul>
{% for suggestion in overfitting.suggestions %}
<li>{{ suggestion }}</li>
{% endfor %}
</ul>
</div>
{% else %}
<p>未检测到明显过拟合迹象</p>
{% endif %}
</div>
<div class="section">
<h2>学习率分析</h2>
<p>调度策略: <strong>{{ lr.schedule_type }}</strong></p>
<p>初始学习率: {{ lr.initial_lr | round(6) }}, 最终学习率: {{ lr.final_lr | round(6) }}</p>
{% if lr.optimal_range %}
<div class="metric-card">
<h3>最佳学习率范围</h3>
<p>Epoch {{ lr.optimal_range.start_epoch }}-{{ lr.optimal_range.end_epoch }}:
{{ lr.optimal_range.min_lr | round(6) }} 到 {{ lr.optimal_range.max_lr | round(6) }}</p>
</div>
{% endif %}
<div class="plot-container">
<img src="data:image/png;base64,{{ lr_plot }}" alt="学习率曲线" style="max-width: 100%;">
</div>
</div>
</div>
</body>
</html更多推荐
所有评论(0)