CostPrediction/src/model_trainer.py
2024-11-09 00:19:12 +08:00

422 lines
18 KiB
Python

import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.impute import SimpleImputer
import xgboost as xgb
import lightgbm as lgb
import logging
import joblib
import os
from src.feature_analysis import FeatureAnalysis
from datetime import datetime
import json
from src.database import get_db_connection
from src.data_preparation import DataPreparation
class ModelTrainer:
def __init__(self):
self.models = {
'xgboost': self._create_xgboost_model(),
'lightgbm': self._create_lightgbm_model(),
'gbdt': self._create_gbdt_model(),
'rf': self._create_rf_model()
}
self.best_model = None
self.imputer = SimpleImputer(strategy='mean')
self.feature_scaler = None
self.target_scaler = None
def fit_model(self, X_train, y_train, model_names, X_val=None, y_val=None, equipment_type=None):
"""
训练模型并返回评估结果
"""
try:
# 记录数据范围
logging.info(f"Training data range - X: min={X_train.min()}, max={X_train.max()}")
logging.info(f"Training data range - y: min={y_train.min()}, max={y_train.max()}")
results = {}
best_score = -float('inf')
best_model_info = None
for model_name in model_names:
if model_name not in self.models:
logging.warning(f"Unknown model: {model_name}")
continue
logging.info(f"Training {model_name}...")
model = self.models[model_name]
# 训练模型
model.fit(X_train, y_train)
# 计算评估指标
metrics = self._calculate_metrics(
model,
X_train, y_train,
X_val, y_val
)
# 更新最佳模型
if metrics['validation']['r2'] > best_score:
best_score = metrics['validation']['r2']
self.best_model = model
best_model_info = {
'type': model_name,
'r2': float(metrics['validation']['r2']),
'mae': float(metrics['validation']['mae']) if metrics['validation']['mae'] is not None else None,
'rmse': float(metrics['validation']['rmse']) if metrics['validation']['rmse'] is not None else None
}
# 转换 numpy 数据类型为 Python 原生类型
results[model_name] = {
'train': {
'r2': float(metrics['train']['r2']),
'mae': float(metrics['train']['mae']),
'rmse': float(metrics['train']['rmse'])
},
'validation': {
'r2': float(metrics['validation']['r2']),
'mae': float(metrics['validation']['mae']) if metrics['validation']['mae'] is not None else None,
'rmse': float(metrics['validation']['rmse']) if metrics['validation']['rmse'] is not None else None
}
}
# 保存最佳模型
if equipment_type and best_model_info:
self._save_best_model(equipment_type, best_model_info, X_train)
# 转换特征重要性为列表
feature_importance = None
if self.best_model and hasattr(self.best_model, 'feature_importances_'):
feature_importance = [float(x) for x in self.best_model.feature_importances_]
return {
'metrics': results,
'best_model': best_model_info,
'feature_importance': feature_importance
}
except Exception as e:
logging.error(f"Error in model training: {str(e)}")
raise
def _calculate_metrics(self, model, X_train, y_train, X_val=None, y_val=None):
"""
计算模型评估指标
"""
from sklearn.metrics import r2_score, mean_absolute_error, mean_squared_error
# 训练集评估
train_pred = model.predict(X_train)
# 如果使用了标准化,需要转换回原始范围
if hasattr(self, 'target_scaler'):
train_pred = self.target_scaler.inverse_transform(train_pred.reshape(-1, 1)).ravel()
y_train_orig = self.target_scaler.inverse_transform(y_train.reshape(-1, 1)).ravel()
else:
y_train_orig = y_train
# 记录预测范围
logging.info(f"Train predictions range: min={train_pred.min()}, max={train_pred.max()}")
logging.info(f"Train actual range: min={y_train_orig.min()}, max={y_train_orig.max()}")
train_metrics = {
'r2': r2_score(y_train_orig, train_pred),
'mae': mean_absolute_error(y_train_orig, train_pred),
'rmse': np.sqrt(mean_squared_error(y_train_orig, train_pred))
}
# 验证集评估
if X_val is not None and y_val is not None:
val_pred = model.predict(X_val)
# 如果使用了标准化,需要转换回原始范围
if hasattr(self, 'target_scaler'):
val_pred = self.target_scaler.inverse_transform(val_pred.reshape(-1, 1)).ravel()
y_val_orig = self.target_scaler.inverse_transform(y_val.reshape(-1, 1)).ravel()
else:
y_val_orig = y_val
# 记录预测范围
logging.info(f"Validation predictions range: min={val_pred.min()}, max={val_pred.max()}")
logging.info(f"Validation actual range: min={y_val_orig.min()}, max={y_val_orig.max()}")
val_metrics = {
'r2': r2_score(y_val_orig, val_pred),
'mae': mean_absolute_error(y_val_orig, val_pred),
'rmse': np.sqrt(mean_squared_error(y_val_orig, val_pred))
}
else:
# 使用交叉验证
cv_scores = cross_val_score(model, X_train, y_train, cv=5)
val_metrics = {
'r2': cv_scores.mean(),
'mae': None,
'rmse': None
}
return {
'train': train_metrics,
'validation': val_metrics
}
def _create_xgboost_model(self):
"""
创建 XGBoost 模型,增强正则化
"""
return xgb.XGBRegressor(
n_estimators=50, # 减少树的数量
learning_rate=0.05, # 减小学习率
max_depth=3, # 减小树的深度
min_child_weight=3, # 增加最小子节点权重
subsample=0.7, # 减小样本采样比例
colsample_bytree=0.7, # 减小特征采样比例
reg_alpha=0.1, # L1 正则化
reg_lambda=1, # L2 正则化
random_state=42
)
def _create_lightgbm_model(self):
"""
创建 LightGBM 模型,增强正则化
"""
return lgb.LGBMRegressor(
n_estimators=50,
learning_rate=0.05,
max_depth=3,
num_leaves=7,
min_data_in_leaf=3,
min_sum_hessian_in_leaf=1e-3,
subsample=0.7,
colsample_bytree=0.7,
reg_alpha=0.1,
reg_lambda=1,
random_state=42,
verbose=-1
)
def _create_gbdt_model(self):
"""
创建 GBDT 模型,增强正则化以减轻过拟合
"""
return GradientBoostingRegressor(
n_estimators=20, # 减少树的数量
learning_rate=0.01, # 减小学习率
max_depth=2, # 减小树的深度
min_samples_split=4, # 增加分裂所需的最小样本数
min_samples_leaf=3, # 增加叶子节点最小样本数
subsample=0.5, # 减小样本采样比例
random_state=42,
validation_fraction=0.2 # 使用部分训练数据作为验证集
)
def _create_rf_model(self):
"""
创建随机森林模型,针对小样本数据调整参数
"""
return RandomForestRegressor(
n_estimators=100, # 增加树的数量
max_depth=4, # 限制树的深度
min_samples_split=2, # 减小分需的最小样本数
min_samples_leaf=1, # 减小叶子节点最小样本数
max_features='sqrt', # 特征采样
bootstrap=True, # 使用 bootstrap 采样
oob_score=True, # 计算袋外分数
random_state=42
)
def _save_best_model(self, equipment_type, best_model_info, X_train):
"""
保存最佳模型
"""
try:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
model_dir = 'models'
os.makedirs(model_dir, exist_ok=True)
# 保存模型文件
model_path = f'{model_dir}/{equipment_type}_{timestamp}'
if isinstance(self.best_model, xgb.XGBRegressor):
self.best_model.save_model(f'{model_path}.json')
model_format = 'json'
else:
joblib.dump(self.best_model, f'{model_path}.joblib')
model_format = 'joblib'
# 验证标准化器
if not isinstance(self.feature_scaler, StandardScaler):
raise ValueError("Invalid feature scaler")
if not isinstance(self.target_scaler, StandardScaler):
raise ValueError("Invalid target scaler")
# 保存标准化器
scaler_path = f'{model_dir}/{equipment_type}_{timestamp}_scaler.joblib'
joblib.dump({
'feature_scaler': self.feature_scaler,
'target_scaler': self.target_scaler
}, scaler_path)
logging.info(f"Saved model to {model_path}.{model_format}")
logging.info(f"Saved scalers to {scaler_path}")
# 更新数据库中的模型记录
with get_db_connection() as conn:
cursor = conn.cursor()
# 将之前的激活模型设置为非激活
cursor.execute("""
UPDATE trained_models
SET is_active = FALSE
WHERE equipment_type = %s
""", (equipment_type,))
# 插入新的模型记录
cursor.execute("""
INSERT INTO trained_models (
model_name, model_type, equipment_type, model_path,
scaler_path, r2_score, mae, rmse, feature_importance,
training_data_size, training_date, is_active, created_by
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), TRUE, 'system')
""", (
f'{best_model_info["type"]}_{timestamp}',
best_model_info["type"],
equipment_type,
f'{model_path}.{model_format}',
scaler_path,
best_model_info["r2"],
best_model_info["mae"],
best_model_info["rmse"],
json.dumps(self.feature_importance) if hasattr(self, 'feature_importance') else None,
len(X_train)
))
conn.commit()
logging.info(f"Best model saved: {model_path}")
return True
except Exception as e:
logging.error(f"Error saving best model: {str(e)}")
return False
def load_model(self, equipment_type):
"""
加载已训练的模型
"""
try:
logging.info(f"Loading model for {equipment_type}")
# 从数据库获最新的激活模型
with get_db_connection() as conn:
cursor = conn.cursor(dictionary=True)
cursor.execute("""
SELECT * FROM trained_models
WHERE equipment_type = %s AND is_active = TRUE
ORDER BY training_date DESC LIMIT 1
""", (equipment_type,))
model_record = cursor.fetchone()
if not model_record:
raise ValueError(f"No active model found for {equipment_type}")
logging.info(f"Found model: {model_record['model_name']}")
logging.info(f"Model path: {model_record['model_path']}")
logging.info(f"Scaler path: {model_record['scaler_path']}")
# 检查文件是否存在
if not os.path.exists(model_record['model_path']):
raise FileNotFoundError(f"Model file not found: {model_record['model_path']}")
if not os.path.exists(model_record['scaler_path']):
raise FileNotFoundError(f"Scaler file not found: {model_record['scaler_path']}")
# 加载模型文件
if model_record['model_type'] == 'xgboost':
self.best_model = xgb.XGBRegressor()
self.best_model.load_model(model_record['model_path'])
else:
self.best_model = joblib.load(model_record['model_path'])
# 加载标准化器
try:
scalers = joblib.load(model_record['scaler_path'])
logging.info(f"Loaded scalers: {scalers.keys()}")
if 'feature_scaler' not in scalers or 'target_scaler' not in scalers:
raise ValueError("Missing scalers in saved file")
self.feature_scaler = scalers['feature_scaler']
self.target_scaler = scalers['target_scaler']
# 验证标准化器
if not hasattr(self.feature_scaler, 'transform') or not hasattr(self.target_scaler, 'transform'):
raise ValueError("Invalid scaler objects")
logging.info("Model and scalers loaded successfully")
logging.info(f"Feature scaler type: {type(self.feature_scaler)}")
logging.info(f"Target scaler type: {type(self.target_scaler)}")
except Exception as e:
logging.error(f"Error loading scalers: {str(e)}")
logging.error(f"Scaler file content: {scalers if 'scalers' in locals() else 'Not loaded'}")
raise ValueError(f"Failed to load scalers: {str(e)}")
return True
except Exception as e:
logging.error(f"Error loading model: {str(e)}")
logging.error("Detailed traceback:", exc_info=True)
return False
def predict(self, features):
"""
使用加载的模型进行预测
"""
try:
if not self.best_model:
raise ValueError("No model loaded")
if not self.feature_scaler:
raise ValueError("Feature scaler not loaded")
if not self.target_scaler:
raise ValueError("Target scaler not loaded")
logging.info("Starting prediction")
logging.info(f"Input features shape: {features.shape}")
logging.info(f"Input features: \n{features}")
# 处理缺失值
features_filled = np.array(features, dtype=float)
features_filled[np.isnan(features_filled)] = 0
features_filled = np.nan_to_num(features_filled, 0)
logging.info(f"Filled features: \n{features_filled}")
# 标准化特征
X = self.feature_scaler.transform(features_filled)
logging.info(f"Transformed features shape: {X.shape}")
logging.info(f"Transformed features: \n{X}")
# 预测
y_pred_scaled = self.best_model.predict(X)
logging.info(f"Scaled prediction shape: {y_pred_scaled.shape}")
logging.info(f"Scaled prediction: {y_pred_scaled}")
# 反标准化
y_pred = self.target_scaler.inverse_transform(y_pred_scaled.reshape(-1, 1))
logging.info(f"Final prediction shape: {y_pred.shape}")
logging.info(f"Final prediction: {y_pred}")
# 记录标准化器的参数
logging.info("Target scaler params:")
logging.info(f"Mean: {self.target_scaler.mean_}")
logging.info(f"Scale: {self.target_scaler.scale_}")
return y_pred.ravel()
except Exception as e:
logging.error(f"Error in prediction: {str(e)}")
raise