Download webui/output/training_engine.py from sgfw/kronos: direct link, hf CLI and curl.
- Browser
- Download file 18.8 kB
-
https://huggingface.co/spaces/sgfw/kronos/resolve/main/webui/output/training_engine.py
- Command line
-
hf download hf://spaces/sgfw/kronos/webui/output/training_engine.py
-
curl -L -o training_engine.py https://huggingface.co/spaces/sgfw/kronos/resolve/main/webui/output/training_engine.py
18.8 kB
| # -*- coding: utf-8 -*- | |
| """ | |
| Kronos-Enhanced FT-Transformer - 训练引擎 | |
| ========================================== | |
| 纯NumPy实现的全流程训练引擎 | |
| 支持: 训练/验证/测试 | 早停 | 学习率调度 | 月度拟合度校验 | 自动迭代优化 | |
| """ | |
| import numpy as np | |
| import json | |
| import time | |
| import os | |
| import sys | |
| from datetime import datetime, timedelta | |
| class Trainer: | |
| """ | |
| 训练引擎 | |
| ======== | |
| 功能: | |
| 1. 数值梯度下降训练FT-Transformer | |
| 2. 四维奖励机制驱动的损失函数 | |
| 3. 早停机制(patience) | |
| 4. 余弦退火学习率调度 | |
| 5. 月度拟合度校验 | |
| 6. 自动迭代优化闭环 | |
| """ | |
| def __init__(self, model, loss_fn, config, checkpoint_dir='./checkpoints', report_dir='./reports'): | |
| self.model = model | |
| self.loss_fn = loss_fn | |
| self.config = config | |
| self.checkpoint_dir = checkpoint_dir | |
| self.report_dir = report_dir | |
| self.batch_size = config.get('batch_size', 8) | |
| self.lr = config.get('learning_rate', 1e-4) | |
| self.max_epochs = config.get('max_epochs', 50) | |
| self.patience = config.get('early_stopping_patience', 10) | |
| self.grad_clip = config.get('gradient_clip_norm', 1.0) | |
| self.warmup_epochs = config.get('warmup_epochs', 3) | |
| self.scheduler_type = config.get('scheduler_type', 'cosine') | |
| self.weight_decay = config.get('weight_decay', 1e-5) | |
| self.best_val_loss = float('inf') | |
| self.best_epoch = 0 | |
| self.patience_counter = 0 | |
| self.history = {'train_loss': [], 'val_loss': [], 'reward_score': [], | |
| 'lr': [], 'epoch_time': []} | |
| self.grad_eps = 1e-5 # 数值梯度步长 | |
| def _cosine_lr(self, epoch): | |
| """余弦退火学习率""" | |
| if epoch < self.warmup_epochs: | |
| return self.lr * (epoch + 1) / self.warmup_epochs | |
| progress = (epoch - self.warmup_epochs) / max(1, self.max_epochs - self.warmup_epochs) | |
| return self.lr * 0.5 * (1 + np.cos(np.pi * progress)) | |
| def _compute_sga_gradient(self, params_dict, x_cont, x_cat, y, features=None): | |
| """ | |
| 随机梯度逼近 (Simultaneous Perturbation Stochastic Approximation) | |
| 仅需2次前向传播即可估计所有参数的梯度方向,适合CPU环境 | |
| """ | |
| eps = self.grad_eps | |
| delta = {} | |
| # 为每个梯度目标生成随机扰动向量 | |
| grad_targets = { | |
| 'head_W': self.model.head_W, | |
| 'head_b': self.model.head_b, | |
| 'head_norm_gamma': self.model.head_norm_gamma, | |
| 'head_norm_beta': self.model.head_norm_beta, | |
| } | |
| if self.model.blocks: | |
| last_block = self.model.blocks[-1] | |
| grad_targets['last_ffn_W2'] = last_block.ffn.W2 | |
| grad_targets['last_ffn_b2'] = last_block.ffn.b2 | |
| for name, param in grad_targets.items(): | |
| delta[name] = np.random.choice([-1.0, 1.0], size=param.shape) | |
| # 前向:所有参数 +eps*delta | |
| base_pred = self.model.forward(x_cont, x_cat, training=False)[0] | |
| base_loss = self.loss_fn.compute(base_pred, y, features) | |
| # 加扰动 | |
| saved = {} | |
| for name, param in grad_targets.items(): | |
| saved[name] = param.copy() | |
| perturbed = param + eps * delta[name] | |
| if name == 'head_W': | |
| self.model.head_W = perturbed | |
| elif name == 'head_b': | |
| self.model.head_b = perturbed | |
| elif name == 'head_norm_gamma': | |
| self.model.head_norm_gamma = perturbed | |
| elif name == 'head_norm_beta': | |
| self.model.head_norm_beta = perturbed | |
| elif name == 'last_ffn_W2': | |
| last_block.ffn.W2 = perturbed | |
| elif name == 'last_ffn_b2': | |
| last_block.ffn.b2 = perturbed | |
| pert_pred = self.model.forward(x_cont, x_cat, training=False)[0] | |
| pert_loss = self.loss_fn.compute(pert_pred, y, features) | |
| # 恢复参数 | |
| for name, param in grad_targets.items(): | |
| orig = saved[name] | |
| if name == 'head_W': | |
| self.model.head_W = orig | |
| elif name == 'head_b': | |
| self.model.head_b = orig | |
| elif name == 'head_norm_gamma': | |
| self.model.head_norm_gamma = orig | |
| elif name == 'head_norm_beta': | |
| self.model.head_norm_beta = orig | |
| elif name == 'last_ffn_W2': | |
| last_block.ffn.W2 = orig | |
| elif name == 'last_ffn_b2': | |
| last_block.ffn.b2 = orig | |
| # 计算SGA梯度估计 | |
| loss_diff = pert_loss - base_loss | |
| grads = {} | |
| for name, param in grad_targets.items(): | |
| grads[name] = loss_diff * delta[name] / eps | |
| return grads, base_loss | |
| def _apply_gradients(self, grads, lr): | |
| """应用梯度更新(带裁剪)""" | |
| for name, grad in grads.items(): | |
| # 梯度裁剪 | |
| grad_norm = np.linalg.norm(grad) | |
| if grad_norm > self.grad_clip: | |
| grad = grad * self.grad_clip / grad_norm | |
| # SGD更新 | |
| if name == 'head_W': | |
| self.model.head_W -= lr * grad | |
| elif name == 'head_b': | |
| self.model.head_b -= lr * grad | |
| elif name == 'head_norm_gamma': | |
| self.model.head_norm_gamma -= lr * grad | |
| elif name == 'head_norm_beta': | |
| self.model.head_norm_beta -= lr * grad | |
| elif name == 'last_ffn_W2' and self.model.blocks: | |
| self.model.blocks[-1].ffn.W2 -= lr * grad | |
| elif name == 'last_ffn_b2' and self.model.blocks: | |
| self.model.blocks[-1].ffn.b2 -= lr * grad | |
| def train(self, X_cont_train, X_cat_train, y_train, | |
| X_cont_val=None, X_cat_val=None, y_val=None, verbose=True): | |
| """ | |
| 执行训练 | |
| """ | |
| print("\n" + "=" * 70) | |
| print(" FT-Transformer 训练启动") | |
| print("=" * 70) | |
| print(" 训练集: {} 样本 | 验证集: {} 样本".format( | |
| len(X_cont_train), len(X_cont_val) if X_cont_val is not None else 0)) | |
| print(" 批次大小: {} | 学习率: {} | 最大轮数: {} | 早停patience: {}".format( | |
| self.batch_size, self.lr, self.max_epochs, self.patience)) | |
| print(" 梯度方式: SGA随机梯度逼近(高效CPU优化)| 梯度步长: {}".format(self.grad_eps)) | |
| print("=" * 70) | |
| train_start = datetime.now() | |
| self.best_val_loss = float('inf') | |
| for epoch in range(1, self.max_epochs + 1): | |
| epoch_start = time.time() | |
| current_lr = self._cosine_lr(epoch) | |
| # === 训练阶段 === | |
| train_losses = [] | |
| reward_scores = [] | |
| indices = np.random.permutation(len(X_cont_train)) | |
| n_batches = 0 | |
| for start in range(0, len(indices), self.batch_size): | |
| batch_idx = indices[start:start + self.batch_size] | |
| x_c = X_cont_train[batch_idx] | |
| x_cat = X_cat_train[batch_idx] | |
| y_batch = y_train[batch_idx] | |
| # 计算梯度(每3个batch计算一次以加速) | |
| if n_batches % 2 == 0: | |
| grads, batch_loss = self._compute_sga_gradient( | |
| {}, x_c, x_cat, y_batch, x_c) | |
| self._apply_gradients(grads, current_lr) | |
| preds, _ = self.model.forward(x_c, x_cat, training=True) | |
| r_score = self.loss_fn.reward.compute(preds, y_batch, x_c)[0] | |
| train_losses.append(batch_loss) | |
| reward_scores.append(r_score) | |
| n_batches += 1 | |
| avg_train_loss = np.mean(train_losses) | |
| avg_reward = np.mean(reward_scores) | |
| # === 验证阶段 === | |
| val_loss = float('inf') | |
| if X_cont_val is not None and len(X_cont_val) > 0: | |
| val_preds, _ = self.model.predict(X_cont_val, X_cat_val) | |
| val_loss = self.loss_fn.compute(val_preds, y_val, X_cont_val) | |
| epoch_time = time.time() - epoch_start | |
| # 记录历史 | |
| self.history['train_loss'].append(float(avg_train_loss)) | |
| self.history['val_loss'].append(float(val_loss)) | |
| self.history['reward_score'].append(float(avg_reward)) | |
| self.history['lr'].append(float(current_lr)) | |
| self.history['epoch_time'].append(epoch_time) | |
| # 早停检查 | |
| if val_loss < self.best_val_loss: | |
| self.best_val_loss = val_loss | |
| self.best_epoch = epoch | |
| self.patience_counter = 0 | |
| # 保存最佳模型 | |
| self._save_checkpoint(epoch, val_loss) | |
| else: | |
| self.patience_counter += 1 | |
| # 打印日志 | |
| if verbose and (epoch % 5 == 0 or epoch == 1): | |
| print(" Epoch {:3d}/{:3d} | LR: {:.6f} | Train Loss: {:.6f} | " | |
| "Val Loss: {:.6f} | Reward: {:.4f} | Time: {:.1f}s | Best: Ep{}".format( | |
| epoch, self.max_epochs, current_lr, avg_train_loss, | |
| val_loss, avg_reward, epoch_time, self.best_epoch)) | |
| if self.patience_counter >= self.patience: | |
| print("\n [早停] 连续 {} 轮无改善,训练终止于 Epoch {}".format( | |
| self.patience, epoch)) | |
| break | |
| train_elapsed = (datetime.now() - train_start).total_seconds() | |
| print("\n" + "-" * 70) | |
| print(" 训练完成 | 总耗时: {:.1f}s | 最佳Epoch: {} | 最佳Val Loss: {:.6f}".format( | |
| train_elapsed, self.best_epoch, self.best_val_loss)) | |
| print("-" * 70) | |
| # 加载最佳模型 | |
| self._load_best_checkpoint() | |
| return self.history | |
| def _save_checkpoint(self, epoch, val_loss): | |
| path = os.path.join(self.checkpoint_dir, 'best_model.pkl') | |
| self.model.save(path) | |
| meta = {'epoch': epoch, 'val_loss': val_loss, 'best_val_loss': self.best_val_loss, | |
| 'history': self.history, 'timestamp': datetime.now().isoformat()} | |
| meta_path = os.path.join(self.checkpoint_dir, 'best_model_meta.json') | |
| with open(meta_path, 'w', encoding='utf-8') as f: | |
| json.dump(meta, f, indent=2, ensure_ascii=False, default=str) | |
| def _load_best_checkpoint(self): | |
| path = os.path.join(self.checkpoint_dir, 'best_model.pkl') | |
| if os.path.exists(path): | |
| self.model.load(path) | |
| print(" [INFO] 已加载最佳模型: Epoch {}".format(self.best_epoch)) | |
| class MonthlyValidator: | |
| """ | |
| 月度拟合度校验器 | |
| ================ | |
| 每自然月计算预测与实际走势的拟合度 | |
| 拟合度 >= 80%: 预测成功,保留参数 | |
| 拟合度 < 80%: 触发自动修正优化 | |
| """ | |
| def __init__(self, config): | |
| self.success_threshold = config['fit_threshold']['success_threshold'] | |
| self.warning_threshold = config['fit_threshold']['warning_threshold'] | |
| self.auto_retrain = config['auto_retrain_on_fail'] | |
| self.max_retrain = config['max_retrain_attempts'] | |
| self.adjustments = config['retrain_adjustments'] | |
| def validate(self, predictions, actuals, pred_days=60): | |
| """ | |
| 月度拟合度校验 | |
| 参数: | |
| predictions: (N, 3) 模型预测 [趋势, 变化率, 波动率] | |
| actuals: (N, 3) 真实值 | |
| 返回: | |
| result: dict, 校验结果 | |
| """ | |
| N = len(predictions) | |
| if N == 0: | |
| return {'status': 'no_data', 'fit_score': 0, 'details': {}} | |
| # 1. 趋势方向准确率 | |
| trend_correct = np.mean(np.sign(predictions[:, 0]) == np.sign(actuals[:, 0])) | |
| # 2. 价格变化率拟合度 (R²) | |
| ss_res = np.sum((predictions[:, 1] - actuals[:, 1]) ** 2) | |
| ss_tot = np.sum((actuals[:, 1] - np.mean(actuals[:, 1])) ** 2) | |
| r2_score = 1 - ss_res / (ss_tot + 1e-10) | |
| # 3. 波动率拟合度 | |
| vol_correct = np.mean(np.abs(predictions[:, 2] - actuals[:, 2]) < 0.1) | |
| # 4. 综合拟合度(加权) | |
| fit_score = (0.4 * trend_correct + 0.3 * max(0, r2_score) + 0.3 * vol_correct) | |
| # 5. 判定 | |
| if fit_score >= self.success_threshold: | |
| status = 'success' | |
| action = 'retain' | |
| elif fit_score >= self.warning_threshold: | |
| status = 'warning' | |
| action = 'monitor' | |
| else: | |
| status = 'failure' | |
| action = 'retrain' if self.auto_retrain else 'manual_review' | |
| result = { | |
| 'status': status, | |
| 'fit_score': round(float(fit_score), 4), | |
| 'threshold': self.success_threshold, | |
| 'action': action, | |
| 'samples': N, | |
| 'pred_days': pred_days, | |
| 'details': { | |
| 'trend_accuracy': round(float(trend_correct), 4), | |
| 'price_r2': round(float(r2_score), 4), | |
| 'volatility_accuracy': round(float(vol_correct), 4), | |
| 'avg_trend_error': round(float(np.mean(np.abs(predictions[:, 0] - actuals[:, 0]))), 4), | |
| 'avg_price_error': round(float(np.mean(np.abs(predictions[:, 1] - actuals[:, 1]))), 4), | |
| }, | |
| 'reward_detail': None, | |
| } | |
| return result | |
| def generate_retrain_plan(self, validation_result, current_lr, current_epoch): | |
| """生成重训计划""" | |
| plan = { | |
| 'trigger': validation_result['status'], | |
| 'current_fit_score': validation_result['fit_score'], | |
| 'target_fit_score': self.success_threshold, | |
| 'adjustments': {}, | |
| 'priority_order': [], | |
| } | |
| # 根据具体薄弱环节排序优化优先级 | |
| details = validation_result['details'] | |
| weak_dims = [] | |
| if details['trend_accuracy'] < 0.7: | |
| weak_dims.append(('trend', details['trend_accuracy'], '增加波浪理论权重')) | |
| if details['price_r2'] < 0.5: | |
| weak_dims.append(('price', details['price_r2'], '调整量价因子权重')) | |
| if details['volatility_accuracy'] < 0.6: | |
| weak_dims.append(('volatility', details['volatility_accuracy'], '调整风控参数')) | |
| weak_dims.sort(key=lambda x: x[1]) | |
| plan['adjustments'] = { | |
| 'learning_rate': current_lr * self.adjustments['learning_rate_decay'], | |
| 'reward_weight_shift': self.adjustments['reward_weight_shift'], | |
| 'max_epochs': 30, | |
| 'weak_dimensions': weak_dims, | |
| } | |
| plan['priority_order'] = [d[0] for d in weak_dims] | |
| return plan | |
| class FitReporter: | |
| """拟合度报表生成器""" | |
| def generate_report(training_history, validation_results, model_info, | |
| output_dir='./reports', format='json'): | |
| """生成完整的训练+验证报告""" | |
| os.makedirs(output_dir, exist_ok=True) | |
| report = { | |
| 'generated_at': datetime.now().isoformat(), | |
| 'model_info': model_info, | |
| 'training': { | |
| 'total_epochs': len(training_history.get('train_loss', [])), | |
| 'final_train_loss': training_history['train_loss'][-1] if training_history.get('train_loss') else None, | |
| 'best_val_loss': min(training_history['val_loss']) if training_history.get('val_loss') else None, | |
| 'final_reward_score': training_history['reward_score'][-1] if training_history.get('reward_score') else None, | |
| 'loss_curve': training_history.get('train_loss', []), | |
| 'val_loss_curve': training_history.get('val_loss', []), | |
| 'reward_curve': training_history.get('reward_score', []), | |
| 'lr_curve': training_history.get('lr', []), | |
| }, | |
| 'validation': validation_results, | |
| 'summary': FitReporter._generate_summary(training_history, validation_results), | |
| } | |
| if format == 'json': | |
| path = os.path.join(output_dir, 'training_report.json') | |
| with open(path, 'w', encoding='utf-8') as f: | |
| json.dump(report, f, indent=2, ensure_ascii=False, default=str) | |
| print("[INFO] 报告已保存: {}".format(path)) | |
| return path | |
| return report | |
| def _generate_summary(history, val_results): | |
| """生成摘要""" | |
| summary_lines = [] | |
| summary_lines.append("=" * 60) | |
| summary_lines.append(" Kronos-Enhanced FT-Transformer 训练报告") | |
| summary_lines.append("=" * 60) | |
| if history.get('train_loss'): | |
| summary_lines.append("\n[训练概况]") | |
| summary_lines.append(" 总轮数: {}".format(len(history['train_loss']))) | |
| summary_lines.append(" 最终训练损失: {:.6f}".format(history['train_loss'][-1])) | |
| if history.get('val_loss'): | |
| summary_lines.append(" 最佳验证损失: {:.6f}".format(min(history['val_loss']))) | |
| if history.get('reward_score'): | |
| summary_lines.append(" 最终奖励分数: {:.4f}".format(history['reward_score'][-1])) | |
| if val_results: | |
| summary_lines.append("\n[月度校验]") | |
| for i, vr in enumerate(val_results): | |
| status_cn = {'success': '达标', 'warning': '警告', 'failure': '不达标'}.get(vr['status'], vr['status']) | |
| summary_lines.append(" 校验#{}: 拟合度={:.4f} (阈值={}) - {}".format( | |
| i + 1, vr['fit_score'], vr['threshold'], status_cn)) | |
| if vr.get('details'): | |
| d = vr['details'] | |
| summary_lines.append(" 趋势准确率: {:.4f} | 价格R²: {:.4f} | 波动率准确率: {:.4f}".format( | |
| d.get('trend_accuracy', 0), d.get('price_r2', 0), d.get('volatility_accuracy', 0))) | |
| summary_lines.append("\n" + "=" * 60) | |
| return '\n'.join(summary_lines) | |
| if __name__ == '__main__': | |
| print("Trainer模块加载测试") | |
| print(" Trainer: 数值梯度训练引擎") | |
| print(" MonthlyValidator: 月度拟合度校验") | |
| print(" FitReporter: 结构化报表生成") | |
| print("[OK] 模块加载成功") | |