"""FlorenceForge可视化工具模块 提供训练过程和结果的可视化功能 """ import json import numpy as np from pathlib import Path from typing import Dict, List, Optional, Union, Tuple, Any from .optional_dependencies import missing_dependency_message def _get_pandas(): try: import pandas as pd return pd except ImportError as e: raise ImportError( missing_dependency_message("可视化功能", "pandas") ) from e def _get_pil_image(): try: from PIL import Image return Image except ImportError as e: raise ImportError( missing_dependency_message("可视化功能", "Pillow") ) from e def _get_matplotlib(): try: import matplotlib.pyplot as plt from matplotlib.patches import Rectangle except ImportError as e: raise ImportError( missing_dependency_message("可视化功能", "matplotlib") ) from e # 按需设置 matplotlib 中文字体,避免模块导入时产生副作用 plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'DejaVu Sans'] plt.rcParams['axes.unicode_minus'] = False return plt, Rectangle def _get_confusion_matrix(): try: from sklearn.metrics import confusion_matrix return confusion_matrix except ImportError as e: raise ImportError( missing_dependency_message("混淆矩阵绘制", "scikit-learn") ) from e def _get_seaborn(): try: import seaborn as sns return sns except ImportError as e: raise ImportError( missing_dependency_message("该可视化功能", "seaborn") ) from e def _get_plotly(): try: from plotly.subplots import make_subplots import plotly.graph_objects as go return make_subplots, go except ImportError as e: raise ImportError( missing_dependency_message("交互式仪表板", "plotly") ) from e def plot_training_curves( metrics_data: Dict[str, List[float]], save_path: Optional[Union[str, Path]] = None, title: str = "训练曲线", figsize: Tuple[int, int] = (12, 8) ) -> None: """绘制训练曲线 Args: metrics_data: 指标数据字典,键为指标名称,值为数值列表 save_path: 保存路径 title: 图表标题 figsize: 图像尺寸 """ plt, _ = _get_matplotlib() fig, axes = plt.subplots(2, 2, figsize=figsize) fig.suptitle(title, fontsize=16) # 损失曲线 ax1 = axes[0, 0] if 'train_loss' in metrics_data: ax1.plot(metrics_data['train_loss'], label='训练损失', color='blue') if 'val_loss' in metrics_data: ax1.plot(metrics_data['val_loss'], label='验证损失', color='red') ax1.set_title('损失曲线') ax1.set_xlabel('Epoch') ax1.set_ylabel('Loss') ax1.legend() ax1.grid(True, alpha=0.3) # 学习率曲线 ax2 = axes[0, 1] if 'learning_rate' in metrics_data: ax2.plot(metrics_data['learning_rate'], label='学习率', color='green') ax2.set_title('学习率曲线') ax2.set_xlabel('Step') ax2.set_ylabel('Learning Rate') ax2.legend() ax2.grid(True, alpha=0.3) # 准确率曲线 ax3 = axes[1, 0] accuracy_metrics = [k for k in metrics_data.keys() if 'accuracy' in k.lower()] for metric in accuracy_metrics: ax3.plot(metrics_data[metric], label=metric) if accuracy_metrics: ax3.set_title('准确率曲线') ax3.set_xlabel('Epoch') ax3.set_ylabel('Accuracy') ax3.legend() ax3.grid(True, alpha=0.3) # 其他指标 ax4 = axes[1, 1] other_metrics = [k for k in metrics_data.keys() if k not in ['train_loss', 'val_loss', 'learning_rate'] and 'accuracy' not in k.lower()] for metric in other_metrics[:5]: # 最多显示5个指标 ax4.plot(metrics_data[metric], label=metric) if other_metrics: ax4.set_title('其他指标') ax4.set_xlabel('Epoch') ax4.set_ylabel('Value') ax4.legend() ax4.grid(True, alpha=0.3) plt.tight_layout() if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.show() def plot_task_distribution( task_counts: Dict[str, int], save_path: Optional[Union[str, Path]] = None, title: str = "任务分布", figsize: Tuple[int, int] = (10, 6) ) -> None: """绘制任务分布图 Args: task_counts: 任务计数字典 save_path: 保存路径 title: 图表标题 figsize: 图像尺寸 """ plt, _ = _get_matplotlib() fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize) fig.suptitle(title, fontsize=16) tasks = list(task_counts.keys()) counts = list(task_counts.values()) # 柱状图 bars = ax1.bar(tasks, counts, color=plt.cm.Set3(np.linspace(0, 1, len(tasks)))) ax1.set_title('任务样本数量') ax1.set_xlabel('任务类型') ax1.set_ylabel('样本数量') ax1.tick_params(axis='x', rotation=45) # 添加数值标签 for bar, count in zip(bars, counts): height = bar.get_height() ax1.text(bar.get_x() + bar.get_width()/2., height + max(counts)*0.01, f'{count}', ha='center', va='bottom') # 饼图 ax2.pie(counts, labels=tasks, autopct='%1.1f%%', startangle=90, colors=plt.cm.Set3(np.linspace(0, 1, len(tasks)))) ax2.set_title('任务比例分布') plt.tight_layout() if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.show() def visualize_detection_results( image: Union[str, Path, Any], detections: List[Dict[str, Any]], save_path: Optional[Union[str, Path]] = None, title: str = "检测结果", figsize: Tuple[int, int] = (12, 8), show_confidence: bool = True ) -> None: """可视化目标检测结果 Args: image: 输入图像 detections: 检测结果列表 save_path: 保存路径 title: 图表标题 figsize: 图像尺寸 show_confidence: 是否显示置信度 """ Image = _get_pil_image() plt, Rectangle = _get_matplotlib() # 加载图像 if isinstance(image, (str, Path)): image = Image.open(image) elif not isinstance(image, Image.Image): raise ValueError("图像必须是路径或PIL.Image对象") fig, ax = plt.subplots(1, 1, figsize=figsize) ax.imshow(image) ax.set_title(title) ax.axis('off') # 颜色映射 colors = plt.cm.Set1(np.linspace(0, 1, len(detections))) img_width, img_height = image.size for i, detection in enumerate(detections): bbox = detection.get('bbox', []) label = detection.get('label', 'unknown') confidence = detection.get('confidence', 0.0) if len(bbox) == 4: x1, y1, x2, y2 = bbox # 如果坐标是归一化的,转换为绝对坐标 if all(0 <= coord <= 1 for coord in bbox): x1, y1, x2, y2 = x1*img_width, y1*img_height, x2*img_width, y2*img_height # 绘制边界框 rect = Rectangle((x1, y1), x2-x1, y2-y1, linewidth=2, edgecolor=colors[i], facecolor='none') ax.add_patch(rect) # 添加标签 text = label if show_confidence and confidence > 0: text += f' ({confidence:.2f})' ax.text(x1, y1-5, text, fontsize=10, color=colors[i], bbox=dict(boxstyle='round,pad=0.3', facecolor='white', alpha=0.8)) plt.tight_layout() if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.show() def plot_confusion_matrix( y_true: List[str], y_pred: List[str], labels: Optional[List[str]] = None, save_path: Optional[Union[str, Path]] = None, title: str = "混淆矩阵", figsize: Tuple[int, int] = (8, 6) ) -> None: """绘制混淆矩阵 Args: y_true: 真实标签 y_pred: 预测标签 labels: 标签列表 save_path: 保存路径 title: 图表标题 figsize: 图像尺寸 """ plt, _ = _get_matplotlib() sns = _get_seaborn() confusion_matrix = _get_confusion_matrix() if labels is None: labels = sorted(list(set(y_true + y_pred))) cm = confusion_matrix(y_true, y_pred, labels=labels) plt.figure(figsize=figsize) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=labels, yticklabels=labels) plt.title(title) plt.xlabel('预测标签') plt.ylabel('真实标签') if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.show() def plot_metric_comparison( metrics_dict: Dict[str, Dict[str, float]], save_path: Optional[Union[str, Path]] = None, title: str = "指标比较", figsize: Tuple[int, int] = (12, 6) ) -> None: """绘制指标比较图 Args: metrics_dict: 指标字典,格式为 {model_name: {metric_name: value}} save_path: 保存路径 title: 图表标题 figsize: 图像尺寸 """ pd = _get_pandas() plt, _ = _get_matplotlib() # 转换为DataFrame df = pd.DataFrame(metrics_dict).T fig, axes = plt.subplots(1, 2, figsize=figsize) fig.suptitle(title, fontsize=16) # 柱状图 df.plot(kind='bar', ax=axes[0], rot=45) axes[0].set_title('指标对比') axes[0].set_xlabel('模型') axes[0].set_ylabel('指标值') axes[0].legend(bbox_to_anchor=(1.05, 1), loc='upper left') axes[0].grid(True, alpha=0.3) # 雷达图 metrics = list(df.columns) models = list(df.index) angles = np.linspace(0, 2*np.pi, len(metrics), endpoint=False).tolist() angles += angles[:1] # 闭合图形 ax = axes[1] ax.set_theta_offset(np.pi / 2) ax.set_theta_direction(-1) ax.set_thetagrids(np.degrees(angles[:-1]), metrics) for i, model in enumerate(models): values = df.loc[model].tolist() values += values[:1] # 闭合图形 ax.plot(angles, values, 'o-', linewidth=2, label=model) ax.fill(angles, values, alpha=0.25) ax.set_ylim(0, 1) ax.set_title('雷达图对比') ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left') plt.tight_layout() if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.show() def create_evaluation_dashboard( results: Dict[str, Any], save_path: Optional[Union[str, Path]] = None, title: str = "评估仪表板" ) -> None: """创建评估仪表板 Args: results: 评估结果字典 save_path: 保存路径 title: 仪表板标题 """ make_subplots, go = _get_plotly() fig = make_subplots( rows=2, cols=2, subplot_titles=('任务性能', '指标分布', '错误分析', '样本难度'), specs=[[{'type': 'bar'}, {'type': 'box'}], [{'type': 'pie'}, {'type': 'scatter'}]] ) # 任务性能柱状图 if 'task_performance' in results: task_perf = results['task_performance'] fig.add_trace( go.Bar( x=list(task_perf.keys()), y=list(task_perf.values()), name='任务性能' ), row=1, col=1 ) # 指标分布箱线图 if 'metric_distributions' in results: metric_dist = results['metric_distributions'] for metric, values in metric_dist.items(): fig.add_trace( go.Box( y=values, name=metric ), row=1, col=2 ) # 错误类型饼图 if 'error_analysis' in results: error_analysis = results['error_analysis'] fig.add_trace( go.Pie( labels=list(error_analysis.keys()), values=list(error_analysis.values()), name='错误分析' ), row=2, col=1 ) # 样本难度散点图 if 'sample_difficulty' in results: difficulty = results['sample_difficulty'] fig.add_trace( go.Scatter( x=difficulty.get('complexity', []), y=difficulty.get('performance', []), mode='markers', name='样本难度', text=difficulty.get('labels', []), hovertemplate='复杂度: %{x}
性能: %{y}
%{text}' ), row=2, col=2 ) fig.update_layout( title_text=title, showlegend=True, height=800 ) if save_path: fig.write_html(save_path) fig.show() def plot_attention_heatmap( attention_weights: np.ndarray, tokens: List[str], save_path: Optional[Union[str, Path]] = None, title: str = "注意力热力图", figsize: Tuple[int, int] = (10, 8) ) -> None: """绘制注意力权重热力图 Args: attention_weights: 注意力权重矩阵 tokens: 词汇列表 save_path: 保存路径 title: 图表标题 figsize: 图像尺寸 """ plt, _ = _get_matplotlib() sns = _get_seaborn() plt.figure(figsize=figsize) sns.heatmap( attention_weights, xticklabels=tokens, yticklabels=tokens, cmap='Blues', annot=False, cbar=True ) plt.title(title) plt.xlabel('Key Tokens') plt.ylabel('Query Tokens') plt.xticks(rotation=45) plt.yticks(rotation=0) if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.show() def plot_loss_landscape( loss_surface: np.ndarray, save_path: Optional[Union[str, Path]] = None, title: str = "损失地形图", figsize: Tuple[int, int] = (10, 8) ) -> None: """绘制损失地形图 Args: loss_surface: 损失表面数据 save_path: 保存路径 title: 图表标题 figsize: 图像尺寸 """ plt, _ = _get_matplotlib() fig = plt.figure(figsize=figsize) ax = fig.add_subplot(111, projection='3d') x = np.arange(loss_surface.shape[0]) y = np.arange(loss_surface.shape[1]) X, Y = np.meshgrid(x, y) surf = ax.plot_surface(X, Y, loss_surface.T, cmap='viridis', alpha=0.8) ax.set_title(title) ax.set_xlabel('参数维度1') ax.set_ylabel('参数维度2') ax.set_zlabel('损失值') fig.colorbar(surf) if save_path: plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.show() def create_training_report( training_history: Dict[str, Any], model_info: Dict[str, Any], save_dir: Union[str, Path], report_name: str = "training_report" ) -> None: """创建训练报告 Args: training_history: 训练历史数据 model_info: 模型信息 save_dir: 保存目录 report_name: 报告名称 """ pd = _get_pandas() save_dir = Path(save_dir) save_dir.mkdir(parents=True, exist_ok=True) # 绘制训练曲线 if 'metrics' in training_history: plot_training_curves( training_history['metrics'], save_path=save_dir / f"{report_name}_curves.png", title="训练过程曲线" ) # 绘制任务分布 if 'task_distribution' in training_history: plot_task_distribution( training_history['task_distribution'], save_path=save_dir / f"{report_name}_task_dist.png", title="训练任务分布" ) # 创建HTML报告 html_content = f""" 训练报告 - {report_name}

训练报告

{report_name}

模型信息

模型名称: {model_info.get('name', 'Unknown')}
参数数量: {model_info.get('parameters', 'Unknown')}
训练时间: {model_info.get('training_time', 'Unknown')}

训练曲线

训练曲线

任务分布

任务分布

最终指标

""" # 添加最终指标 if 'final_metrics' in training_history: for metric, value in training_history['final_metrics'].items(): html_content += f'
{metric}: {value:.4f}
' html_content += """
""" # 保存HTML报告 with open(save_dir / f"{report_name}.html", 'w', encoding='utf-8') as f: f.write(html_content) # 保存JSON数据 report_data = { 'training_history': training_history, 'model_info': model_info, 'generated_at': pd.Timestamp.now().isoformat() } with open(save_dir / f"{report_name}_data.json", 'w', encoding='utf-8') as f: json.dump(report_data, f, ensure_ascii=False, indent=2, default=str) class VisualizationManager: """可视化管理器 统一管理各种可视化功能 """ def __init__(self, output_dir: Union[str, Path]): """初始化可视化管理器 Args: output_dir: 输出目录 """ self.output_dir = Path(output_dir) self.output_dir.mkdir(parents=True, exist_ok=True) # 设置样式 plt, _ = _get_matplotlib() sns = _get_seaborn() plt.style.use('seaborn-v0_8') sns.set_palette("husl") def plot_and_save( self, plot_func, filename: str, *args, **kwargs ) -> Path: """绘制并保存图表 Args: plot_func: 绘图函数 filename: 文件名 *args: 位置参数 **kwargs: 关键字参数 Returns: 保存路径 """ save_path = self.output_dir / filename kwargs['save_path'] = save_path plot_func(*args, **kwargs) return save_path def create_summary_dashboard( self, data: Dict[str, Any], title: str = "总结仪表板" ) -> Path: """创建总结仪表板 Args: data: 数据字典 title: 仪表板标题 Returns: 保存路径 """ save_path = self.output_dir / "dashboard.html" create_evaluation_dashboard( data, save_path=save_path, title=title ) return save_path def generate_report( self, training_history: Dict[str, Any], model_info: Dict[str, Any], report_name: str = "report" ) -> Path: """生成完整报告 Args: training_history: 训练历史 model_info: 模型信息 report_name: 报告名称 Returns: 报告目录路径 """ create_training_report( training_history, model_info, self.output_dir, report_name ) return self.output_dir