kronos / webui /output /training_engine.py
sgfw's picture
Upload 68 files
459d1c8 verified
Raw History Blame Contribute Delete
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:
"""拟合度报表生成器"""
@staticmethod
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
@staticmethod
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] 模块加载成功")