| |
| """ |
| 详细训练教程 |
| Detailed Training Tutorial for Emotion and Physiological State Prediction Model |
| |
| 这个脚本演示了如何训练情绪与生理状态变化预测模型的完整流程: |
| 1. 数据准备和探索 |
| 2. 数据预处理 |
| 3. 模型配置和创建 |
| 4. 训练过程监控 |
| 5. 模型评估和验证 |
| 6. 超参数调优 |
| |
| 运行方式: |
| python training_tutorial.py |
| """ |
|
|
| import sys |
| import os |
| from pathlib import Path |
| import numpy as np |
| import pandas as pd |
| import torch |
| import torch.nn as nn |
| import torch.optim as optim |
| from torch.utils.data import DataLoader, TensorDataset |
| import matplotlib.pyplot as plt |
| import seaborn as sns |
| from typing import Dict, Any, List, Tuple |
| import yaml |
| import json |
|
|
| |
| project_root = Path(__file__).parent.parent |
| sys.path.insert(0, str(project_root)) |
|
|
| from src.data.synthetic_generator import SyntheticDataGenerator |
| from src.models.pad_predictor import PADPredictor |
| from src.data.preprocessor import DataPreprocessor |
| from src.utils.trainer import ModelTrainer |
| from src.utils.logger import setup_logger |
| from src.models.loss_functions import WeightedMSELoss |
| from src.models.metrics import RegressionMetrics |
|
|
| def main(): |
| """主函数""" |
| print("=" * 80) |
| print("情绪与生理状态变化预测模型 - 详细训练教程") |
| print("Emotion and Physiological State Prediction Model - Detailed Training Tutorial") |
| print("=" * 80) |
| |
| |
| setup_logger(level='INFO') |
| |
| |
| output_dir = Path(project_root) / "examples" / "training_outputs" |
| output_dir.mkdir(exist_ok=True) |
| |
| |
| print("\n1. 数据准备和探索") |
| print("-" * 50) |
| train_data, val_data, test_data = prepare_and_explore_data(output_dir) |
| |
| |
| print("\n2. 数据预处理") |
| print("-" * 50) |
| preprocessor = preprocess_data(train_data, val_data, test_data, output_dir) |
| |
| |
| print("\n3. 模型配置和创建") |
| print("-" * 50) |
| model = create_and_configure_model() |
| |
| |
| print("\n4. 训练配置") |
| print("-" * 50) |
| training_config = configure_training() |
| |
| |
| print("\n5. 模型训练") |
| print("-" * 50) |
| history = train_model(model, preprocessor, train_data, val_data, training_config, output_dir) |
| |
| |
| print("\n6. 模型评估") |
| print("-" * 50) |
| evaluate_model(model, preprocessor, test_data, output_dir) |
| |
| |
| print("\n7. 超参数调优示例") |
| print("-" * 50) |
| demonstrate_hyperparameter_tuning(output_dir) |
| |
| print("\n" + "=" * 80) |
| print("详细训练教程完成!") |
| print("Detailed Training Tutorial Completed!") |
| print("=" * 80) |
|
|
| def prepare_and_explore_data(output_dir: Path) -> Tuple[Tuple, Tuple, Tuple]: |
| """数据准备和探索""" |
| print(" - 生成不同模式的训练数据...") |
| |
| |
| generator = SyntheticDataGenerator(seed=42) |
| |
| |
| patterns = ['stress', 'relaxation', 'excitement', 'calm'] |
| pattern_weights = [0.3, 0.3, 0.2, 0.2] |
| |
| |
| generator.num_samples = 2000 |
| train_features, train_labels = generator.generate_dataset_with_patterns( |
| patterns=patterns, |
| pattern_weights=pattern_weights |
| ) |
| |
| |
| generator.num_samples = 500 |
| generator.seed = 123 |
| val_features, val_labels = generator.generate_data(add_noise=True, add_correlations=True) |
| |
| |
| generator.num_samples = 300 |
| generator.seed = 456 |
| test_features, test_labels = generator.generate_data(add_noise=True, add_correlations=True) |
| |
| print(f" - 数据集大小:") |
| print(f" 训练集: {train_features.shape}") |
| print(f" 验证集: {val_features.shape}") |
| print(f" 测试集: {test_features.shape}") |
| |
| |
| print(" - 生成数据探索图表...") |
| visualize_data_exploration( |
| train_features, train_labels, val_features, val_labels, |
| test_features, test_labels, output_dir |
| ) |
| |
| |
| save_data_splits( |
| (train_features, train_labels), |
| (val_features, val_labels), |
| (test_features, test_labels), |
| output_dir |
| ) |
| |
| return (train_features, train_labels), (val_features, val_labels), (test_features, test_labels) |
|
|
| def visualize_data_exploration(train_features, train_labels, val_features, val_labels, |
| test_features, test_labels, output_dir: Path): |
| """可视化数据探索""" |
| |
| feature_columns = [ |
| 'user_pleasure', 'user_arousal', 'user_dominance', |
| 'vitality', 'current_pleasure', 'current_arousal', 'current_dominance' |
| ] |
| label_columns = [ |
| 'delta_pleasure', 'delta_arousal', 'delta_dominance', |
| 'delta_pressure', 'confidence' |
| ] |
| |
| |
| train_df = pd.DataFrame(train_features, columns=feature_columns) |
| train_labels_df = pd.DataFrame(train_labels, columns=label_columns) |
| |
| |
| fig, axes = plt.subplots(2, 4, figsize=(16, 8)) |
| fig.suptitle('特征分布', fontsize=16) |
| |
| for i, col in enumerate(feature_columns): |
| row, col_idx = i // 4, i % 4 |
| axes[row, col_idx].hist(train_df[col], bins=30, alpha=0.7, color='skyblue') |
| axes[row, col_idx].set_title(col) |
| axes[row, col_idx].set_xlabel('值') |
| axes[row, col_idx].set_ylabel('频率') |
| |
| |
| axes[1, 3].set_visible(False) |
| |
| plt.tight_layout() |
| plt.savefig(output_dir / 'feature_distribution.png', dpi=300, bbox_inches='tight') |
| plt.close() |
| |
| |
| fig, axes = plt.subplots(2, 3, figsize=(15, 10)) |
| fig.suptitle('标签分布', fontsize=16) |
| |
| for i, col in enumerate(label_columns): |
| row, col_idx = i // 3, i % 3 |
| axes[row, col_idx].hist(train_labels_df[col], bins=30, alpha=0.7, color='lightcoral') |
| axes[row, col_idx].set_title(col) |
| axes[row, col_idx].set_xlabel('值') |
| axes[row, col_idx].set_ylabel('频率') |
| |
| |
| axes[1, 2].set_visible(False) |
| |
| plt.tight_layout() |
| plt.savefig(output_dir / 'label_distribution.png', dpi=300, bbox_inches='tight') |
| plt.close() |
| |
| |
| full_df = pd.concat([train_df, train_labels_df], axis=1) |
| correlation_matrix = full_df.corr() |
| |
| plt.figure(figsize=(12, 10)) |
| sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', center=0, |
| square=True, fmt='.2f', cbar_kws={'label': '相关系数'}) |
| plt.title('特征和标签相关性热力图') |
| plt.tight_layout() |
| plt.savefig(output_dir / 'correlation_heatmap.png', dpi=300, bbox_inches='tight') |
| plt.close() |
| |
| print(f" - 数据探索图表已保存到: {output_dir}") |
|
|
| def save_data_splits(train_data, val_data, test_data, output_dir: Path): |
| """保存数据分割""" |
| feature_columns = [ |
| 'user_pleasure', 'user_arousal', 'user_dominance', |
| 'vitality', 'current_pleasure', 'current_arousal', 'current_dominance' |
| ] |
| label_columns = [ |
| 'delta_pleasure', 'delta_arousal', 'delta_dominance', |
| 'delta_pressure', 'confidence' |
| ] |
| |
| |
| train_df = pd.DataFrame(train_data[0], columns=feature_columns) |
| train_labels_df = pd.DataFrame(train_data[1], columns=label_columns) |
| train_full = pd.concat([train_df, train_labels_df], axis=1) |
| train_full.to_csv(output_dir / 'train_data.csv', index=False) |
| |
| |
| val_df = pd.DataFrame(val_data[0], columns=feature_columns) |
| val_labels_df = pd.DataFrame(val_data[1], columns=label_columns) |
| val_full = pd.concat([val_df, val_labels_df], axis=1) |
| val_full.to_csv(output_dir / 'val_data.csv', index=False) |
| |
| |
| test_df = pd.DataFrame(test_data[0], columns=feature_columns) |
| test_labels_df = pd.DataFrame(test_data[1], columns=label_columns) |
| test_full = pd.concat([test_df, test_labels_df], axis=1) |
| test_full.to_csv(output_dir / 'test_data.csv', index=False) |
|
|
| def preprocess_data(train_data, val_data, test_data, output_dir: Path) -> DataPreprocessor: |
| """数据预处理""" |
| print(" - 创建数据预处理器...") |
| |
| |
| preprocessor = DataPreprocessor() |
| |
| print(" - 拟合预处理器...") |
| |
| preprocessor.fit(train_data[0], train_data[1]) |
| |
| print(" - 转换数据...") |
| |
| train_processed = preprocessor.transform(train_data[0], train_data[1]) |
| val_processed = preprocessor.transform(val_data[0], val_data[1]) |
| test_processed = preprocessor.transform(test_data[0], test_data[1]) |
| |
| print(" - 创建数据加载器...") |
| |
| def create_dataloader(data, batch_size=32, shuffle=True): |
| features, labels = data |
| dataset = TensorDataset( |
| torch.FloatTensor(features), |
| torch.FloatTensor(labels) |
| ) |
| return DataLoader(dataset, batch_size=batch_size, shuffle=shuffle) |
| |
| train_loader = create_dataloader(train_processed, batch_size=32, shuffle=True) |
| val_loader = create_dataloader(val_processed, batch_size=32, shuffle=False) |
| test_loader = create_dataloader(test_processed, batch_size=32, shuffle=False) |
| |
| |
| preprocessor_path = output_dir / 'preprocessor.pkl' |
| preprocessor.save(str(preprocessor_path)) |
| print(f" - 预处理器已保存到: {preprocessor_path}") |
| |
| |
| print(f" - 预处理统计:") |
| print(f" 训练集样本数: {len(train_loader.dataset)}") |
| print(f" 验证集样本数: {len(val_loader.dataset)}") |
| print(f" 测试集样本数: {len(test_loader.dataset)}") |
| |
| return preprocessor |
|
|
| def create_and_configure_model() -> PADPredictor: |
| """创建和配置模型""" |
| print(" - 加载模型配置...") |
| |
| |
| config_path = Path(project_root) / "configs" / "model_config.yaml" |
| with open(config_path, 'r', encoding='utf-8') as f: |
| model_config = yaml.safe_load(f) |
| |
| print(" - 创建模型...") |
| |
| model = PADPredictor( |
| input_dim=model_config['dimensions']['input_dim'], |
| output_dim=model_config['dimensions']['output_dim'], |
| hidden_dims=[layer['size'] for layer in model_config['architecture']['hidden_layers']], |
| dropout_rate=model_config['architecture']['hidden_layers'][0]['dropout'], |
| weight_init=model_config['initialization']['weight_init'], |
| bias_init=model_config['initialization']['bias_init'] |
| ) |
| |
| |
| model_info = model.get_model_info() |
| print(f" - 模型信息:") |
| print(f" 模型类型: {model_info['model_type']}") |
| print(f" 输入维度: {model_info['input_dim']}") |
| print(f" 输出维度: {model_info['output_dim']}") |
| print(f" 隐藏层: {model_info['hidden_dims']}") |
| print(f" 总参数数: {model_info['total_parameters']}") |
| print(f" 可训练参数数: {model_info['trainable_parameters']}") |
| |
| return model |
|
|
| def configure_training() -> Dict[str, Any]: |
| """配置训练参数""" |
| print(" - 配置训练参数...") |
| |
| |
| config_path = Path(project_root) / "configs" / "training_config.yaml" |
| with open(config_path, 'r', encoding='utf-8') as f: |
| training_config = yaml.safe_load(f) |
| |
| |
| config = { |
| 'epochs': 100, |
| 'learning_rate': 0.001, |
| 'weight_decay': 1e-4, |
| 'batch_size': 32, |
| 'patience': 15, |
| 'min_delta': 1e-6, |
| 'save_best_only': True, |
| 'save_dir': Path(project_root) / "examples" / "training_outputs" / "models" |
| } |
| |
| print(f" - 训练配置:") |
| print(f" 训练轮数: {config['epochs']}") |
| print(f" 学习率: {config['learning_rate']}") |
| print(f" 权重衰减: {config['weight_decay']}") |
| print(f" 批次大小: {config['batch_size']}") |
| print(f" 早停耐心值: {config['patience']}") |
| |
| return config |
|
|
| def train_model(model: PADPredictor, preprocessor: DataPreprocessor, |
| train_data: Tuple, val_data: Tuple, |
| training_config: Dict[str, Any], output_dir: Path) -> Dict[str, List]: |
| """训练模型""" |
| print(" - 创建训练器...") |
| |
| |
| trainer = ModelTrainer(model, preprocessor) |
| |
| print(" - 创建数据加载器...") |
| |
| def create_dataloader(data, batch_size=32, shuffle=True): |
| features, labels = data |
| processed_features, processed_labels = preprocessor.transform(features, labels) |
| dataset = TensorDataset( |
| torch.FloatTensor(processed_features), |
| torch.FloatTensor(processed_labels) |
| ) |
| return DataLoader(dataset, batch_size=batch_size, shuffle=shuffle) |
| |
| train_loader = create_dataloader(train_data, batch_size=training_config['batch_size'], shuffle=True) |
| val_loader = create_dataloader(val_data, batch_size=training_config['batch_size'], shuffle=False) |
| |
| print(" - 开始训练...") |
| |
| history = trainer.train( |
| train_loader=train_loader, |
| val_loader=val_loader, |
| config=training_config |
| ) |
| |
| print(" - 训练完成,保存结果...") |
| |
| history_path = output_dir / 'training_history.json' |
| with open(history_path, 'w', encoding='utf-8') as f: |
| json.dump(history, f, indent=2, ensure_ascii=False) |
| |
| |
| plot_training_curves(history, output_dir) |
| |
| print(f" - 训练历史已保存到: {history_path}") |
| |
| return history |
|
|
| def plot_training_curves(history: Dict[str, List], output_dir: Path): |
| """绘制训练曲线""" |
| fig, axes = plt.subplots(2, 2, figsize=(15, 10)) |
| fig.suptitle('训练过程监控', fontsize=16) |
| |
| |
| axes[0, 0].plot(history['train_loss'], label='训练损失', color='blue') |
| axes[0, 0].plot(history['val_loss'], label='验证损失', color='red') |
| axes[0, 0].set_title('损失曲线') |
| axes[0, 0].set_xlabel('轮数') |
| axes[0, 0].set_ylabel('损失') |
| axes[0, 0].legend() |
| axes[0, 0].grid(True) |
| |
| |
| if 'learning_rate' in history: |
| axes[0, 1].plot(history['learning_rate'], color='green') |
| axes[0, 1].set_title('学习率变化') |
| axes[0, 1].set_xlabel('轮数') |
| axes[0, 1].set_ylabel('学习率') |
| axes[0, 1].grid(True) |
| |
| |
| if 'val_metrics' in history: |
| metrics = history['val_metrics'][0].keys() |
| for i, metric in enumerate(metrics): |
| if i < 2: |
| row, col = 1, i |
| metric_values = [m[metric] for m in history['val_metrics']] |
| axes[row, col].plot(metric_values, label=metric, color=f'C{i+2}') |
| axes[row, col].set_title(f'验证指标: {metric}') |
| axes[row, col].set_xlabel('轮数') |
| axes[row, col].set_ylabel(metric) |
| axes[row, col].legend() |
| axes[row, col].grid(True) |
| |
| plt.tight_layout() |
| plt.savefig(output_dir / 'training_curves.png', dpi=300, bbox_inches='tight') |
| plt.close() |
| |
| print(f" - 训练曲线已保存到: {output_dir / 'training_curves.png'}") |
|
|
| def evaluate_model(model: PADPredictor, preprocessor: DataPreprocessor, |
| test_data: Tuple, output_dir: Path): |
| """评估模型""" |
| print(" - 加载最佳模型...") |
| |
| |
| best_model_path = output_dir / 'models' / 'best_model.pth' |
| if best_model_path.exists(): |
| model = PADPredictor.load_model(str(best_model_path)) |
| |
| print(" - 在测试集上评估...") |
| |
| |
| features, labels = test_data |
| processed_features, processed_labels = preprocessor.transform(features, labels) |
| |
| model.eval() |
| with torch.no_grad(): |
| predictions = model(torch.FloatTensor(processed_features)) |
| |
| |
| metrics_calculator = RegressionMetrics() |
| metrics = metrics_calculator.calculate_all_metrics( |
| torch.FloatTensor(processed_labels), |
| predictions |
| ) |
| |
| print(" - 测试集评估结果:") |
| for metric_name, value in metrics.items(): |
| if isinstance(value, (int, float)): |
| print(f" {metric_name}: {value:.4f}") |
| |
| |
| eval_results = { |
| 'test_metrics': {k: float(v) if isinstance(v, (int, float)) else str(v) |
| for k, v in metrics.items()}, |
| 'model_info': model.get_model_info() |
| } |
| |
| eval_path = output_dir / 'evaluation_results.json' |
| with open(eval_path, 'w', encoding='utf-8') as f: |
| json.dump(eval_results, f, indent=2, ensure_ascii=False) |
| |
| print(f" - 评估结果已保存到: {eval_path}") |
| |
| |
| visualize_predictions(processed_labels, predictions.cpu().numpy(), output_dir) |
|
|
| def visualize_predictions(true_labels: np.ndarray, predictions: np.ndarray, output_dir: Path): |
| """可视化预测结果""" |
| label_names = ['ΔPleasure', 'ΔArousal', 'ΔDominance', 'ΔPressure', 'Confidence'] |
| |
| fig, axes = plt.subplots(2, 3, figsize=(15, 10)) |
| fig.suptitle('预测结果可视化', fontsize=16) |
| |
| for i in range(5): |
| row, col = i // 3, i % 3 |
| |
| |
| axes[row, col].scatter(true_labels[:, i], predictions[:, i], alpha=0.6, s=20) |
| |
| |
| min_val = min(true_labels[:, i].min(), predictions[:, i].min()) |
| max_val = max(true_labels[:, i].max(), predictions[:, i].max()) |
| axes[row, col].plot([min_val, max_val], [min_val, max_val], 'r--', alpha=0.8) |
| |
| axes[row, col].set_xlabel('真实值') |
| axes[row, col].set_ylabel('预测值') |
| axes[row, col].set_title(label_names[i]) |
| axes[row, col].grid(True, alpha=0.3) |
| |
| |
| r2 = 1 - np.sum((true_labels[:, i] - predictions[:, i])**2) / np.sum((true_labels[:, i] - true_labels[:, i].mean())**2) |
| axes[row, col].text(0.05, 0.95, f'R² = {r2:.3f}', transform=axes[row, col].transAxes, |
| bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8)) |
| |
| |
| axes[1, 2].set_visible(False) |
| |
| plt.tight_layout() |
| plt.savefig(output_dir / 'prediction_visualization.png', dpi=300, bbox_inches='tight') |
| plt.close() |
| |
| print(f" - 预测结果可视化已保存到: {output_dir / 'prediction_visualization.png'}") |
|
|
| def demonstrate_hyperparameter_tuning(output_dir: Path): |
| """演示超参数调优""" |
| print(" - 演示不同学习率的训练效果...") |
| |
| |
| generator = SyntheticDataGenerator(num_samples=200, seed=789) |
| features, labels = generator.generate_data() |
| |
| |
| preprocessor = DataPreprocessor() |
| processed_features, processed_labels = preprocessor.transform(features, labels) |
| |
| dataset = TensorDataset( |
| torch.FloatTensor(processed_features), |
| torch.FloatTensor(processed_labels) |
| ) |
| |
| |
| train_size = int(0.8 * len(dataset)) |
| val_size = len(dataset) - train_size |
| train_dataset, val_dataset = torch.utils.data.random_split(dataset, [train_size, val_size]) |
| |
| train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True) |
| val_loader = DataLoader(val_dataset, batch_size=16, shuffle=False) |
| |
| |
| learning_rates = [0.01, 0.001, 0.0001] |
| results = {} |
| |
| for lr in learning_rates: |
| print(f" 测试学习率: {lr}") |
| |
| |
| model = PADPredictor(input_dim=7, output_dim=5, hidden_dims=[64, 32]) |
| |
| |
| optimizer = optim.Adam(model.parameters(), lr=lr, weight_decay=1e-4) |
| |
| |
| trainer = ModelTrainer(model, preprocessor) |
| |
| config = { |
| 'epochs': 20, |
| 'learning_rate': lr, |
| 'weight_decay': 1e-4, |
| 'patience': 5, |
| 'save_best_only': False |
| } |
| |
| history = trainer.train(train_loader, val_loader, config) |
| |
| |
| final_val_loss = history['val_loss'][-1] |
| results[str(lr)] = final_val_loss |
| |
| print(f" 最终验证损失: {final_val_loss:.4f}") |
| |
| |
| tuning_results = { |
| 'learning_rates': learning_rates, |
| 'final_val_losses': results, |
| 'best_lr': min(results.keys(), key=lambda k: results[k]) |
| } |
| |
| tuning_path = output_dir / 'hyperparameter_tuning.json' |
| with open(tuning_path, 'w', encoding='utf-8') as f: |
| json.dump(tuning_results, f, indent=2, ensure_ascii=False) |
| |
| print(f" - 超参数调优结果已保存到: {tuning_path}") |
| print(f" - 最佳学习率: {tuning_results['best_lr']}") |
|
|
| if __name__ == "__main__": |
| main() |