import numpy as np from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split from sklearn.metrics import r2_score, mean_absolute_error, mean_squared_error import logging import os from datetime import datetime import json from src.feature_analysis import FeatureAnalysis from src.database import get_db_connection from .logger import setup_logger import math # PyTorch 为可选依赖 try: import torch import torch.nn as nn from torch.utils.data import DataLoader from src.data_preparation import EquipmentDataset _HAS_TORCH = True except ImportError: torch = None nn = None DataLoader = None EquipmentDataset = None _HAS_TORCH = False logger = setup_logger(__name__) # 条件基类:有 PyTorch 时继承 nn.Module,否则继承 object _PYTORCH_BASE = nn.Module if _HAS_TORCH else object class CostPredictionModel(_PYTORCH_BASE): def __init__(self, input_size, equipment_type): if not _HAS_TORCH: raise ImportError("PyTorch is not installed. Install with: pip install torch") super().__init__() self.equipment_type = equipment_type if equipment_type == '火箭炮': self.net = nn.Sequential( nn.Linear(input_size, 32), nn.ReLU(), nn.BatchNorm1d(32), nn.Linear(32, 16), nn.ReLU(), nn.BatchNorm1d(16), nn.Linear(16, 8), nn.ReLU(), nn.BatchNorm1d(8), nn.Linear(8, 1) ) def init_weights(m): if isinstance(m, nn.Linear): torch.nn.init.orthogonal_(m.weight, gain=0.5) torch.nn.init.constant_(m.bias, 0.0) elif isinstance(m, nn.BatchNorm1d): torch.nn.init.constant_(m.weight, 0.5) torch.nn.init.constant_(m.bias, 0.0) self.net.apply(init_weights) else: self.manufacturer_net = nn.Sequential( nn.Linear(5, 4), nn.ReLU(), nn.BatchNorm1d(4), nn.Dropout(0.2) ) self.equipment_net = nn.Sequential( nn.Linear(input_size - 5, 64), nn.LeakyReLU(0.1), nn.BatchNorm1d(64), nn.Dropout(0.2), nn.Linear(64, 32), nn.LeakyReLU(0.1), nn.BatchNorm1d(32), nn.Dropout(0.2), nn.Linear(32, 16), nn.LeakyReLU(0.1), nn.BatchNorm1d(16), nn.Dropout(0.2) ) self.combined_net = nn.Sequential( nn.Linear(20, 32), nn.LeakyReLU(0.1), nn.BatchNorm1d(32), nn.Dropout(0.2), nn.Linear(32, 16), nn.LeakyReLU(0.1), nn.BatchNorm1d(16), nn.Dropout(0.2), nn.Linear(16, 8), nn.LeakyReLU(0.1), nn.BatchNorm1d(8), nn.Linear(8, 1) ) def forward(self, x): if self.equipment_type == '火箭炮': return self.net(x) else: manufacturer_features = x[:, -5:] equipment_features = x[:, :-5] manu_out = self.manufacturer_net(manufacturer_features) equip_out = self.equipment_net(equipment_features) combined = torch.cat([equip_out, manu_out], dim=1) return self.combined_net(combined) class ModelTrainer: def __init__(self): if not _HAS_TORCH: raise ImportError("PyTorch is not installed. Install with: pip install torch") self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') self.model = None self.feature_scaler = None self.target_scaler = None self.equipment_type = None self.feature_analyzer = FeatureAnalysis() def train_model(self, dataloader, epochs=100, learning_rate=0.001, equipment_type=None): """训练模型""" try: sample_features, _ = next(iter(dataloader)) input_size = sample_features.shape[1] # 设置确定性 torch.manual_seed(42) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False if torch.cuda.is_available(): torch.cuda.manual_seed_all(42) np.random.seed(42) self.model = CostPredictionModel(input_size, equipment_type).to(self.device) if equipment_type == '火箭炮': # 火箭炮使用更保守和稳定的训练设置 criterion = nn.SmoothL1Loss(beta=0.1) # 使用Huber损失,beta值较小 learning_rate = 0.0003 # 更小的学习率 weight_decay = 0.001 # 适中的权重衰减 # 使用AdamW优化器,更小的beta值 optimizer = torch.optim.AdamW( self.model.parameters(), lr=learning_rate, weight_decay=weight_decay, betas=(0.8, 0.9), # 更小的动量值 eps=1e-8 ) # 使用带预热的学习率调度 num_steps = len(dataloader) * epochs warmup_steps = num_steps // 10 # 10%的预热步数 def lr_lambda(current_step): if current_step < warmup_steps: return float(current_step) / float(max(1, warmup_steps)) return 0.5 * (1.0 + math.cos( math.pi * (current_step - warmup_steps) / float(max(1, num_steps - warmup_steps)) )) scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda) else: # 巡飞弹保持原有设置 # 巡飞弹使用更激进的训练设置 criterion = nn.MSELoss() learning_rate = 0.001 # 较大的学习率 weight_decay = 0.001 # 较小的权重衰减 patience = 20 # 较短的耐心值 # 使用Adam优化器 optimizer = torch.optim.Adam( self.model.parameters(), lr=learning_rate, weight_decay=weight_decay, betas=(0.9, 0.999) ) # 使用余弦退火学习率调度 scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max=epochs, eta_min=learning_rate * 0.01 ) # 训练循环 best_loss = float('inf') patience = 30 patience_counter = 0 best_model_state = None moving_avg_loss = None alpha = 0.9 # 移动平均系数 for epoch in range(epochs): self.model.train() total_loss = 0 batch_count = 0 for batch_features, batch_targets in dataloader: batch_features = batch_features.to(self.device) batch_targets = batch_targets.to(self.device) # 前向传播 outputs = self.model(batch_features) loss = criterion(outputs, batch_targets.view(-1, 1)) # 反向传播 optimizer.zero_grad(set_to_none=True) # 更高效的梯度清零 loss.backward() # 梯度裁剪 if equipment_type == '火箭炮': torch.nn.utils.clip_grad_norm_( self.model.parameters(), max_norm=0.1 ) optimizer.step() if equipment_type == '火箭炮': scheduler.step() total_loss += loss.item() batch_count += 1 avg_loss = total_loss / batch_count # 使用移动平均计算损失 if moving_avg_loss is None: moving_avg_loss = avg_loss else: moving_avg_loss = alpha * moving_avg_loss + (1 - alpha) * avg_loss # 早停检查使用移动平均损失 if moving_avg_loss < best_loss: best_loss = moving_avg_loss patience_counter = 0 best_model_state = { 'state_dict': self.model.state_dict(), 'optimizer': optimizer.state_dict(), 'scheduler': scheduler.state_dict() if equipment_type == '火箭炮' else None } else: patience_counter += 1 if (epoch + 1) % 10 == 0: logger.info(f'Epoch [{epoch+1}/{epochs}], Loss: {moving_avg_loss:.4f}, ' f'LR: {optimizer.param_groups[0]["lr"]:.6f}') if patience_counter >= patience: logger.info(f"Early stopping triggered at epoch {epoch+1}") break # 恢复最佳模型 if best_model_state is not None: self.model.load_state_dict(best_model_state['state_dict']) optimizer.load_state_dict(best_model_state['optimizer']) if equipment_type == '火箭炮' and best_model_state['scheduler']: scheduler.load_state_dict(best_model_state['scheduler']) return True except Exception as e: logger.error(f"Error in model training: {str(e)}") raise def save_model(self, equipment_type, metrics=None): """保存模型""" try: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") model_dir = 'models' os.makedirs(model_dir, exist_ok=True) # 转换装备类型为英文 equipment_type_en = 'rocket' if equipment_type == '火箭炮' else 'missile' # 保存模型 model_path = f'{model_dir}/{equipment_type_en}_{timestamp}.pth' torch.save({ 'model_state_dict': self.model.state_dict(), 'input_size': self.model.equipment_net[0].in_features + 5, 'manufacturer_net_state': self.model.manufacturer_net.state_dict(), 'equipment_net_state': self.model.equipment_net.state_dict(), 'combined_net_state': self.model.combined_net.state_dict() }, model_path) # 保存标准化器 scaler_path = f'{model_dir}/{equipment_type_en}_{timestamp}_scaler.pth' torch.save({ 'feature_scaler': self.feature_scaler, 'target_scaler': self.target_scaler }, scaler_path) # 获取评估指标 r2 = metrics['validation']['r2'] if metrics and metrics.get('validation') else metrics['train']['r2'] mae = metrics['validation']['mae'] if metrics and metrics.get('validation') else metrics['train']['mae'] rmse = metrics['validation']['rmse'] if metrics and metrics.get('validation') else metrics['train']['rmse'] # 更新数据库 with get_db_connection() as conn: cursor = conn.cursor() # 将所有同类型模型设置为非激活, 除了 PLS 模型 cursor.execute(""" UPDATE trained_models SET is_active = FALSE WHERE equipment_type = ? AND model_type != ? """, (equipment_type, 'pls')) # 保存新模型记录 cursor.execute(""" INSERT INTO trained_models ( model_name, model_type, equipment_type, model_path, scaler_path, training_date, is_active, created_by, r2_score, mae, rmse ) VALUES (?, ?, ?, ?, ?, datetime('now','localtime'), TRUE, ?, ?, ?, ?) """, ( f"{equipment_type}_{timestamp}", 'pytorch', equipment_type, model_path, scaler_path, 'system', r2, mae, rmse )) conn.commit() logger.info(f"Model saved successfully: {model_path}") return True except Exception as e: logger.error(f"Error saving model: {str(e)}") return False def load_model(self, equipment_type, model_type): """加载模型""" try: # 从数据库获取最新的激活模型 with get_db_connection() as conn: cursor = conn.cursor() cursor.execute(""" SELECT * FROM trained_models WHERE equipment_type = ? AND model_type = ? AND is_active = TRUE ORDER BY training_date DESC LIMIT 1 """, (equipment_type, model_type)) model_record = cursor.fetchone() if not model_record: raise ValueError(f"No trained model found for {equipment_type}") # 加载模型 if model_record['model_type'] == 'pytorch': # 加载PyTorch模型 checkpoint = torch.load(model_record['model_path'], encoding='latin1') input_size = checkpoint['input_size'] # 创建新模型实例 self.model = CostPredictionModel(input_size, equipment_type).to(self.device) self.model.load_state_dict(checkpoint['model_state_dict']) # 加载标准化器 scalers = torch.load(model_record['scaler_path'], encoding='latin1') self.feature_scaler = scalers['feature_scaler'] self.target_scaler = scalers['target_scaler'] else: # 加载sklearn模型 from joblib import load with open(model_record['model_path'], 'rb') as f: self.model = load(f) with open(model_record['scaler_path'], 'rb') as f: scalers = load(f) self.feature_scaler = scalers['feature_scaler'] self.target_scaler = scalers['target_scaler'] logger.info(f"Model loaded successfully from {model_record['model_path']}") return True except Exception as e: logger.error(f"Error loading model: {str(e)}") return False def predict(self, features): """使用模型进行预测""" try: self.model.eval() with torch.no_grad(): features_tensor = torch.FloatTensor(features).to(self.device) predictions = self.model(features_tensor) # 直接返回预测值 return predictions.cpu().numpy() except Exception as e: logger.error(f"Error in prediction: {str(e)}") raise def fit_model(self, X_train, y_train, models, X_val=None, y_val=None, equipment_type=None): """训练模型并返回评估结果""" try: logger.info(f"Starting model training for {equipment_type}") logger.info(f"Selected models: {models}") logger.info(f"Training data shape: {X_train.shape}") all_metrics = {} best_model = None best_score = float('-inf') best_model_type = None # 添加变量记录最佳模型类型 # 训练所有选择的模型 for model_type in models: logger.info(f"Training {model_type} model...") if model_type == 'pls': # PLS模型单独处理,不参与最优模型评选 from sklearn.cross_decomposition import PLSRegression # 使用较少的组件数来避免过拟合 n_components = min(3, X_train.shape[1] // 5) model = PLSRegression( n_components=n_components, scale=True, max_iter=500, tol=1e-6 ) model.fit(X_train, y_train) # 评估PLS模型 y_train_pred = model.predict(X_train).ravel() if X_val is not None: y_val_pred = model.predict(X_val).ravel() # 将预测值转换回原始尺度 y_train_pred_original = self.target_scaler.inverse_transform(y_train_pred.reshape(-1, 1)).ravel() y_train_original = self.target_scaler.inverse_transform(y_train.reshape(-1, 1)).ravel() train_metrics = { 'r2': float(r2_score(y_train_original, y_train_pred_original)), 'mae': float(mean_absolute_error(y_train_original, y_train_pred_original)), 'rmse': float(np.sqrt(mean_squared_error(y_train_original, y_train_pred_original))) } val_metrics = None if X_val is not None: y_val_pred_original = self.target_scaler.inverse_transform(y_val_pred.reshape(-1, 1)).ravel() y_val_original = self.target_scaler.inverse_transform(y_val.reshape(-1, 1)).ravel() val_metrics = { 'r2': float(r2_score(y_val_original, y_val_pred_original)), 'mae': float(mean_absolute_error(y_val_original, y_val_pred_original)), 'rmse': float(np.sqrt(mean_squared_error(y_val_original, y_val_pred_original))) } all_metrics[model_type] = { 'train': train_metrics, 'validation': val_metrics } # 保存PLS模型,但不参与最优模型评选 if equipment_type: self._save_sklearn_model(equipment_type, model_type, model, all_metrics[model_type]) continue # 跳过后续的最优模型评选 elif model_type == 'xgboost': import xgboost as xgb model = xgb.XGBRegressor( n_estimators=50, learning_rate=0.03, max_depth=3, min_child_weight=5, subsample=0.6, colsample_bytree=0.6, reg_alpha=0.5, reg_lambda=2.0, gamma=1, random_state=42 ) # 训练模型 model.fit(X_train, y_train) elif model_type == 'lightgbm': import lightgbm as lgb model = lgb.LGBMRegressor( n_estimators=50, learning_rate=0.03, max_depth=3, num_leaves=8, subsample=0.6, colsample_bytree=0.6, reg_alpha=0.5, reg_lambda=2.0, min_child_samples=10, min_split_gain=1.0, random_state=42 ) # 训练模型 model.fit(X_train, y_train) elif model_type == 'gbm': from sklearn.ensemble import GradientBoostingRegressor model = GradientBoostingRegressor( n_estimators=50, learning_rate=0.03, max_depth=3, min_samples_split=10, min_samples_leaf=5, subsample=0.6, min_impurity_decrease=0.01, random_state=42 ) # 训练模型 model.fit(X_train, y_train) elif model_type == 'rf': from sklearn.ensemble import RandomForestRegressor model = RandomForestRegressor( n_estimators=100, max_depth=4, min_samples_split=5, min_samples_leaf=3, max_features='sqrt', bootstrap=True, random_state=42 ) # 训练模型 model.fit(X_train, y_train) elif model_type == 'pytorch': # 训练PyTorch模型(如未安装则跳过) try: train_dataset = EquipmentDataset(X_train, y_train) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True) training_success = self.train_model(train_loader) if not training_success: continue except ImportError as e: logger.warning(f"PyTorch not available, skipping: {e}") continue # 评估模型性能 if model_type == 'pytorch': with torch.no_grad(): X_train_tensor = torch.FloatTensor(X_train).to(self.device) y_train_pred = self.model(X_train_tensor).cpu().numpy() # 直接获取输出 if X_val is not None: X_val_tensor = torch.FloatTensor(X_val).to(self.device) y_val_pred = self.model(X_val_tensor).cpu().numpy() # 直接获取输出 else: # 使用训练好的模型进行预测 y_train_pred = model.predict(X_train) if X_val is not None: if model_type == 'pls': y_val_pred = model.predict(X_val).ravel() # 记录PLS的一些额外信息 if hasattr(model, 'score'): train_r2 = model.score(X_train, y_train) val_r2 = model.score(X_val, y_val) logger.info(f"PLS built-in R² - Train: {train_r2:.4f}, Val: {val_r2:.4f}") # 记录每个组件解释的方差比例 if hasattr(model, 'explained_variance_ratio_'): logger.info("PLS explained variance ratios: " + ", ".join([f"{v:.4f}" for v in model.explained_variance_ratio_])) else: y_val_pred = model.predict(X_val) # 将测值转换回始尺度 y_train_pred_original = self.target_scaler.inverse_transform(y_train_pred.reshape(-1, 1)).ravel() y_train_original = self.target_scaler.inverse_transform(y_train.reshape(-1, 1)).ravel() train_metrics = { 'r2': float(r2_score(y_train_original, y_train_pred_original)), 'mae': float(mean_absolute_error(y_train_original, y_train_pred_original)), 'rmse': float(np.sqrt(mean_squared_error(y_train_original, y_train_pred_original))) } val_metrics = None if X_val is not None and y_val is not None: y_val_pred_original = self.target_scaler.inverse_transform(y_val_pred.reshape(-1, 1)).ravel() y_val_original = self.target_scaler.inverse_transform(y_val.reshape(-1, 1)).ravel() val_metrics = { 'r2': float(r2_score(y_val_original, y_val_pred_original)), 'mae': float(mean_absolute_error(y_val_original, y_val_pred_original)), 'rmse': float(np.sqrt(mean_squared_error(y_val_original, y_val_pred_original))) } all_metrics[model_type] = { 'train': train_metrics, 'validation': val_metrics } # 更新最佳模型(不包括PLS) current_score = val_metrics['r2'] if val_metrics else train_metrics['r2'] if model_type != 'pls' and current_score > best_score: best_score = current_score best_model = { 'type': model_type, 'r2': current_score, 'mae': val_metrics['mae'] if val_metrics else train_metrics['mae'], 'rmse': val_metrics['rmse'] if val_metrics else train_metrics['rmse'] } best_model_type = model_type # 记录最佳模型类型 # 保存最佳模型实例(但不立即写入数据库) if model_type == 'pytorch': self.best_pytorch_model = self.model.state_dict() # 保存模型状态 self.best_pytorch_metrics = all_metrics[model_type] # 保存指标 else: self.best_model = model self.best_model_metrics = all_metrics[model_type] # 单独保存PLS模型(不参与最佳模型评选) if model_type == 'pls' and equipment_type: self._save_sklearn_model(equipment_type, model_type, model, all_metrics[model_type]) # 在所有模型训练完成后,只保存最佳模型 if best_model_type and equipment_type: if best_model_type == 'pytorch': # 恢复最佳PyTorch模型状态并保存 self.model.load_state_dict(self.best_pytorch_model) self.save_model(equipment_type, self.best_pytorch_metrics) else: # 保存最佳sklearn模型 self._save_sklearn_model(equipment_type, best_model_type, self.best_model, self.best_model_metrics) return { 'metrics': all_metrics, 'feature_importance': None, 'best_model': best_model } except Exception as e: logger.error(f"Error in model fitting: {str(e)}") logger.error("Detailed traceback:", exc_info=True) raise def _calculate_feature_importance(self, X): """计算特征重要性""" try: if self.model is None: return None self.model.eval() feature_importance = np.zeros(X.shape[1]) # 使用特征扰动计算重要性 with torch.no_grad(): X_tensor = torch.FloatTensor(X).to(self.device) baseline_pred = self.model(X_tensor).cpu().numpy() # 直接获取预测值 for i in range(X.shape[1]): # 创建动后的特征 X_perturbed = X.copy() X_perturbed[:, i] = np.random.permutation(X_perturbed[:, i]) # 预测并计算影响 X_perturbed_tensor = torch.FloatTensor(X_perturbed).to(self.device) perturbed_pred = self.model(X_perturbed_tensor).cpu().numpy() # 直接获取预测值 # 特征重要性为预变化的平均绝对值 feature_importance[i] = np.mean(np.abs(baseline_pred - perturbed_pred)) # 归一化特征重要性 feature_importance = feature_importance / np.sum(feature_importance) return feature_importance except Exception as e: logger.error(f"Error calculating feature importance: {str(e)}") return None def _save_sklearn_model(self, equipment_type, model_type, model, metrics=None): """保存sklearn类型的模型""" try: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") model_dir = 'models' os.makedirs(model_dir, exist_ok=True) # 转换装备类型为英文 equipment_type_en = 'rocket' if equipment_type == '火箭炮' else 'missile' # 保存模型 model_path = f'{model_dir}/{equipment_type_en}_{model_type}_{timestamp}.joblib' from joblib import dump dump(model, model_path) # 保存标准化器 scaler_path = f'{model_dir}/{equipment_type_en}_{model_type}_{timestamp}_scaler.joblib' dump({ 'feature_scaler': self.feature_scaler, 'target_scaler': self.target_scaler }, scaler_path) # 获取评估指标 r2 = metrics['validation']['r2'] if metrics and metrics.get('validation') else metrics['train']['r2'] mae = metrics['validation']['mae'] if metrics and metrics.get('validation') else metrics['train']['mae'] rmse = metrics['validation']['rmse'] if metrics and metrics.get('validation') else metrics['train']['rmse'] # 更新数据库 with get_db_connection() as conn: cursor = conn.cursor() # 将同类型的其他模型设置为非激活 if model_type != 'pls': cursor.execute(""" UPDATE trained_models SET is_active = FALSE WHERE equipment_type = ? AND model_type != ? """, (equipment_type, 'pls')) else: cursor.execute(""" UPDATE trained_models SET is_active = FALSE WHERE equipment_type = ? AND model_type = ? """, (equipment_type, 'pls')) # 保存新模型记录 cursor.execute(""" INSERT INTO trained_models ( model_name, model_type, equipment_type, model_path, scaler_path, training_date, is_active, created_by, r2_score, mae, rmse ) VALUES (?, ?, ?, ?, ?, datetime('now','localtime'), TRUE, ?, ?, ?, ?) """, ( f"{equipment_type}_{model_type}_{timestamp}", model_type, equipment_type, model_path, scaler_path, 'system', r2, mae, rmse )) conn.commit() logger.info(f"Model {model_type} saved successfully: {model_path}") return True except Exception as e: logger.error(f"Error saving sklearn model: {str(e)}") return False