Download src/model_training.py from ruoc188/ML1_project_temperature_forecast: direct link, hf CLI and curl.
- Browser
- Download file 19.8 kB
-
https://huggingface.co/spaces/ruoc188/ML1_project_temperature_forecast/resolve/main/src/model_training.py
- Command line
-
hf download hf://spaces/ruoc188/ML1_project_temperature_forecast/src/model_training.py
-
curl -L -o model_training.py https://huggingface.co/spaces/ruoc188/ML1_project_temperature_forecast/resolve/main/src/model_training.py
19.8 kB
| """ | |
| Model Training for time series | |
| - Uses time-based train/test split | |
| - Trains a set of 5 individual LightGBM models for multi-step forecasting | |
| - Handles hyperparameter tuning for models and pipelines via Optuna | |
| - Compares and deploys the best model set | |
| - Saves artifacts and logs experiments to ClearML | |
| """ | |
| import joblib | |
| import pandas as pd | |
| import clearml | |
| import random | |
| import sys, os | |
| from pathlib import Path | |
| sys.path.append(str(Path(__file__).parent.parent)) | |
| import optuna | |
| import numpy as np | |
| import shutil | |
| import logging | |
| import subprocess | |
| from clearml import Logger, Task | |
| from sklearn.model_selection import TimeSeriesSplit | |
| from datetime import datetime | |
| from zoneinfo import ZoneInfo | |
| from lightgbm import LGBMRegressor | |
| from sklearn.metrics import mean_squared_error | |
| from src.data_preprocessing import prepare_data | |
| from src.pipeline import build_full_pipeline | |
| from src.model_evaluation import evaluate_multi_output, evaluate | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
| # --- Đường dẫn sử dụng pathlib --- | |
| BASE_DIR = Path(__file__).parent.parent | |
| DATA_PATH_10_YEARS = BASE_DIR / "data" / "raw data" / "Hanoi Daily 10 years.csv" | |
| DATA_PATH = BASE_DIR / "data" / "latest_3_year.csv" | |
| CURRENT_MODEL_PATH = BASE_DIR / "models" / "Current_model.pkl" | |
| TEMP_NEW_MODEL_PATH = BASE_DIR / "models" / "Update_model.pkl" | |
| # --- Hàm lấy múi giờ --- | |
| def get_timezone(): | |
| """Lấy múi giờ từ biến môi trường TZ, mặc định là Asia/Ho_Chi_Minh.""" | |
| tz_string = os.getenv("TZ", "Asia/Ho_Chi_Minh") | |
| try: | |
| return ZoneInfo(tz_string) | |
| except Exception: | |
| return ZoneInfo("Asia/Ho_Chi_Minh") | |
| # --- Lớp Wrapper cho Multi-Output Prediction --- | |
| class CustomMultiOutputRegressor: | |
| """ | |
| Lớp bao bọc (wrapper) cho 5 bộ model/pipeline riêng biệt, | |
| cung cấp một giao diện .predict() thống nhất. | |
| """ | |
| def __init__(self, models_per_target, complete_pipelines_per_target): | |
| self.models_per_target = models_per_target | |
| self.complete_pipelines_per_target = complete_pipelines_per_target | |
| self.target_names = list(models_per_target.keys()) | |
| def predict(self, X): | |
| if not isinstance(X, pd.DataFrame): | |
| X = pd.DataFrame(X) | |
| predictions = {} | |
| first_target_name = self.target_names[0] | |
| first_pipeline = self.complete_pipelines_per_target[first_target_name] | |
| X_processed_sample = first_pipeline.transform(X) | |
| final_index = X_processed_sample.iloc[30:].index | |
| for target_name in self.target_names: | |
| model = self.models_per_target[target_name] | |
| pipeline = self.complete_pipelines_per_target[target_name] | |
| X_processed = pipeline.transform(X) | |
| X_final = X_processed.iloc[30:] | |
| pred = model.predict(X_final) | |
| predictions[target_name] = pred | |
| return pd.DataFrame(predictions, index=final_index) | |
| def get_params(self, deep=True): | |
| return { | |
| "models_per_target": self.models_per_target, | |
| "complete_pipelines_per_target": self.complete_pipelines_per_target | |
| } | |
| # --- Các hàm chức năng --- | |
| def preprocess_data(data_path: str): | |
| """ | |
| Sử dụng hàm prepare_data từ data_preprocessing.py để chuẩn bị và chia dữ liệu thô. | |
| """ | |
| logging.info("Bắt đầu chuẩn bị và chia dữ liệu từ đường dẫn...") | |
| _, _, _, X_train, y_train, X_test, y_test = prepare_data(is_print=False, path=data_path) | |
| logging.info(f"Chia dữ liệu hoàn tất. Kích thước X_train: {X_train.shape}, X_test: {X_test.shape}") | |
| save_dir = BASE_DIR / "data" | |
| save_dir.mkdir(parents=True, exist_ok=True) | |
| X_train.to_csv(save_dir / "New_X_train_raw.csv", index=True) | |
| y_train.to_csv(save_dir / "New_y_train_raw.csv", index=True) | |
| return X_train, y_train, X_test, y_test | |
| def build_final_complete_pipeline(params: dict): | |
| """Xây dựng pipeline hoàn chỉnh với bộ tham số tốt nhất từ Optuna.""" | |
| return build_full_pipeline( | |
| is_linear=params.get('is_linear', False), | |
| is_category_conditions=params['conditions_is_category'], | |
| is_category_season=params['season_is_category'], | |
| is_category_wind=params['wind_is_category'], | |
| encoding_method_condition=params['encoding_method_condition'], | |
| n_seasons=params['n_seasons'], | |
| n_quantiles=params['n_quantiles'], | |
| drop_nan=False, | |
| drop_base=True | |
| ) | |
| def extract_lgbm_params(params: dict): | |
| """Trích xuất các tham số của LightGBM từ dict params của Optuna.""" | |
| lgbm_params = { | |
| 'boosting_type': params['boosting_type'], 'objective': 'regression', 'metric': 'rmse', | |
| 'learning_rate': params['learning_rate'], 'n_estimators': params['n_estimators'], | |
| 'max_depth': params['max_depth'], 'num_leaves': params['num_leaves'], | |
| 'min_child_samples': params['min_child_samples'], 'min_split_gain': params['min_split_gain'], | |
| 'colsample_bytree': params['colsample_bytree'], 'reg_alpha': params['reg_alpha'], | |
| 'reg_lambda': params['reg_lambda'], 'random_state': 42, 'n_jobs': -1, 'verbose': -1 | |
| } | |
| if params['boosting_type'] in ['gbdt', 'dart']: lgbm_params['subsample'] = params['subsample'] | |
| if params['boosting_type'] == 'dart': lgbm_params['subsample_freq'] = params['subsample_freq'] | |
| return lgbm_params | |
| def train_model(X_train, y_train, n_trials=50, random_state=42): | |
| """ | |
| Tinh chỉnh và huấn luyện 5 mô hình LightGBM riêng biệt cho 5 ngày dự báo. | |
| """ | |
| tz = get_timezone() | |
| task = Task.current_task() | |
| if not task: | |
| task = Task.init(project_name="Hanoi Temperature Forecast", task_name=f"Training_{datetime.now(tz):%Y%m%d_%H%M}") | |
| logger = task.get_logger() | |
| best_models_per_target, best_pipelines_per_target, best_params_per_target = {}, {}, {} | |
| for idx, target_name in enumerate(y_train.columns): | |
| logging.info(f"===== Bắt đầu Tinh chỉnh cho target: {target_name} ({idx + 1}/{len(y_train.columns)}) =====") | |
| def objective(trial): | |
| pipeline_params = { | |
| 'is_linear': trial.suggest_categorical("is_linear", [False]), | |
| 'encoding_method_condition': trial.suggest_categorical("encoding_method_condition", ["ordinal", "target", "quantile"]), | |
| 'n_seasons': trial.suggest_int("n_seasons", 3, 8), 'n_quantiles': trial.suggest_int("n_quantiles", 2, 6), | |
| 'conditions_is_category': trial.suggest_categorical("conditions_is_category", [True, False]), | |
| 'season_is_category': trial.suggest_categorical("season_is_category", [True, False]), | |
| 'wind_is_category': trial.suggest_categorical("wind_is_category", [True, False]), | |
| } | |
| lgbm_params = { | |
| 'boosting_type': trial.suggest_categorical("boosting_type", ["gbdt", "dart", "goss"]), | |
| 'learning_rate': trial.suggest_float("learning_rate", 1e-3, 0.3, log=True), | |
| 'n_estimators': trial.suggest_int("n_estimators", 100, 1000), | |
| 'max_depth': trial.suggest_int("max_depth", 3, 12), | |
| 'num_leaves': trial.suggest_int("num_leaves", 15, 255), | |
| 'min_child_samples': trial.suggest_int("min_child_samples", 5, 100), | |
| 'min_split_gain': trial.suggest_float("min_split_gain", 0.0, 1.0), | |
| 'colsample_bytree': trial.suggest_float("colsample_bytree", 0.6, 1.0), | |
| 'reg_alpha': trial.suggest_float("reg_alpha", 1e-8, 10.0, log=True), | |
| 'reg_lambda': trial.suggest_float("reg_lambda", 1e-8, 10.0, log=True), | |
| } | |
| if lgbm_params['boosting_type'] in ['gbdt', 'dart']: lgbm_params['subsample'] = trial.suggest_float("subsample", 0.6, 1.0) | |
| if lgbm_params['boosting_type'] == 'dart': lgbm_params['subsample_freq'] = trial.suggest_int("subsample_freq", 1, 10) | |
| trial_params = {**pipeline_params, **lgbm_params} | |
| cv = TimeSeriesSplit(n_splits=5) | |
| rmse_scores = [] | |
| for train_idx, val_idx in cv.split(X_train): | |
| X_tr, X_val = X_train.iloc[train_idx], X_train.iloc[val_idx] | |
| y_tr, y_val = y_train.iloc[train_idx], y_train.iloc[val_idx] | |
| y_tr_target = y_tr[target_name] | |
| pipeline = build_final_complete_pipeline(trial_params) | |
| pipeline.fit(X_tr, y_tr_target) | |
| X_tr_processed, X_val_processed = pipeline.transform(X_tr), pipeline.transform(X_val) | |
| X_tr_final, y_tr_final = X_tr_processed.iloc[30:], y_tr_target.iloc[30:] | |
| X_val_final, y_val_final = X_val_processed.iloc[30:], y_val[target_name].iloc[30:] | |
| model = LGBMRegressor(**extract_lgbm_params(trial_params)) | |
| model.fit(X_tr_final, y_tr_final) | |
| metrics = evaluate_model(model, pipeline, X_val, y_val[target_name]) | |
| rmse_scores.append(metrics['RMSE']) | |
| avg_rmse = np.mean(rmse_scores) | |
| logger.report_scalar(f'Optuna CV RMSE/{target_name}', 'RMSE', iteration=trial.number, value=avg_rmse) | |
| return avg_rmse | |
| study = optuna.create_study(direction='minimize', sampler=optuna.samplers.TPESampler(seed=random_state)) | |
| study.optimize(objective, n_trials=n_trials, show_progress_bar=True) | |
| best_params = study.best_trial.params | |
| best_params_per_target[target_name] = best_params | |
| logging.info(f"Tinh chỉnh cho {target_name} hoàn tất. Best RMSE: {study.best_value:.4f}") | |
| logging.info(f"Huấn luyện lại model cuối cùng cho {target_name}...") | |
| final_pipeline = build_final_complete_pipeline(best_params) | |
| final_pipeline.fit(X_train, y_train[target_name]) | |
| X_train_processed = final_pipeline.transform(X_train) | |
| X_train_final, y_train_final = X_train_processed.iloc[30:], y_train[target_name].iloc[30:] | |
| final_model = LGBMRegressor(**extract_lgbm_params(best_params)) | |
| final_model.fit(X_train_final, y_train_final) | |
| best_models_per_target[target_name] = final_model | |
| best_pipelines_per_target[target_name] = final_pipeline | |
| logging.info("===== Tinh chỉnh và huấn luyện hoàn tất cho tất cả 5 target =====") | |
| return best_models_per_target, best_pipelines_per_target, best_params_per_target, task | |
| def evaluate_model(model, pipeline, X_test, y_test): | |
| # --- TRƯỜNG HỢP 1: ĐÁNH GIÁ MỘT BỘ 5 MODEL/PIPELINE --- | |
| if isinstance(model, dict) and isinstance(pipeline, dict): | |
| logging.info("Bắt đầu đánh giá bộ 5 mô hình trên tập test...") | |
| all_predictions = {} | |
| target_names = model.keys() | |
| for target_name in target_names: | |
| p = pipeline[target_name] | |
| m = model[target_name] | |
| X_test_processed = p.transform(X_test) | |
| X_test_final = X_test_processed.iloc[30:] | |
| y_pred = m.predict(X_test_final) | |
| all_predictions[target_name] = y_pred | |
| y_pred_df = pd.DataFrame(all_predictions, index=X_test_final.index) | |
| y_test_final = y_test.loc[y_pred_df.index] | |
| metrics = evaluate_multi_output(y_test_final, y_pred_df) | |
| logging.info(f"Đánh giá bộ mô hình hoàn tất. RMSE trung bình: {metrics['average']['RMSE']:.4f}") | |
| return metrics | |
| # --- TRƯỜNG HỢP 2: ĐÁNH GIÁ MỘT MODEL/PIPELINE ĐƠN LẺ --- | |
| elif hasattr(model, 'predict') and hasattr(pipeline, 'transform'): | |
| X_test_processed = pipeline.transform(X_test) | |
| X_test_final = X_test_processed.iloc[30:] | |
| y_test_final = y_test.iloc[30:] | |
| y_pred = model.predict(X_test_final) | |
| # Hàm evaluate từ model_evaluation.py sẽ trả về RMSE, MAE, R2... | |
| metrics = evaluate(y_test_final, y_pred) | |
| return metrics | |
| else: | |
| raise TypeError("Đầu vào cho evaluate_model không hợp lệ. Phải là model/pipeline đơn lẻ hoặc dict.") | |
| def save_artifacts( | |
| models: dict, pipelines: dict, best_params: dict, metrics: dict, | |
| final_model_object: CustomMultiOutputRegressor, task: Task, output_path: str | |
| ): | |
| tz = get_timezone() | |
| output_path = Path(output_path) | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| artifacts_to_save = { | |
| "models": models, "pipelines": pipelines, "best_params": best_params, | |
| "metrics": metrics, "final_multi_model": final_model_object, | |
| "timestamp": datetime.now(tz).strftime("%Y-%m-%d %H:%M:%S") | |
| } | |
| joblib.dump(artifacts_to_save, output_path) | |
| logging.info(f"Đã lưu toàn bộ artifacts vào: {output_path}") | |
| if metrics and 'average' in metrics: | |
| avg_metrics = metrics['average'] | |
| for metric_name, value in avg_metrics.items(): | |
| if isinstance(value, (int, float)): | |
| Logger.current_logger().report_scalar("Final Model Metrics", metric_name, value=value, iteration=0) | |
| task.upload_artifact(name="complete_model_package", artifact_object=str(output_path)) | |
| logging.info(f"Đã upload '{output_path}' lên ClearML. Đang chờ hoàn tất...") | |
| task.flush() | |
| logging.info("Upload artifact lên ClearML đã hoàn tất.") | |
| retrain_log_path = output_path.parent.parent / "logs" / "retrain_log.pkl" | |
| try: | |
| history = joblib.load(retrain_log_path) if retrain_log_path.exists() else [] | |
| log_entry = { | |
| "timestamp": artifacts_to_save["timestamp"], "metrics": metrics, | |
| "best_params_summary": {k: v['boosting_type'] for k, v in best_params.items()} | |
| } | |
| history.append(log_entry) | |
| retrain_log_path.parent.mkdir(parents=True, exist_ok=True) | |
| joblib.dump(history, retrain_log_path) | |
| logging.info(f"Đã cập nhật lịch sử huấn luyện tại: {retrain_log_path}") | |
| except Exception as e: | |
| logging.error(f"Không thể lưu lịch sử huấn luyện: {e}") | |
| def compare_models( | |
| new_models: dict, new_pipelines: dict, old_model_path: str, | |
| X_test: pd.DataFrame, y_test: pd.DataFrame, threshold=0.99 | |
| ): | |
| logging.info("Bắt đầu so sánh mô hình mới và mô hình cũ...") | |
| new_metrics = evaluate_model(new_models, new_pipelines, X_test, y_test) | |
| new_rmse = new_metrics["average"]["RMSE"] | |
| logging.info(f"-> RMSE trung bình của bộ mô hình mới: {new_rmse:.4f}") | |
| old_model_path = Path(old_model_path) | |
| if not old_model_path.exists(): | |
| logging.info("Không tìm thấy mô hình cũ. Tự động chấp nhận mô hình mới.") | |
| return True, new_metrics, None | |
| try: | |
| old_artifacts = joblib.load(old_model_path) | |
| old_models, old_pipelines = old_artifacts['models'], old_artifacts['pipelines'] | |
| logging.info("Đánh giá mô hình cũ trên tập test...") | |
| old_metrics = evaluate_model(old_models, old_pipelines, X_test, y_test) | |
| old_rmse = old_metrics["average"]["RMSE"] | |
| logging.info(f"-> RMSE trung bình của bộ mô hình cũ: {old_rmse:.4f}") | |
| if new_rmse < old_rmse * threshold: | |
| improvement = (1 - new_rmse / old_rmse) * 100 | |
| logging.info(f"QUYẾT ĐỊNH: Chấp nhận mô hình mới (Tốt hơn {improvement:.2f}%)") | |
| return True, new_metrics, old_metrics | |
| else: | |
| improvement = (new_rmse / old_rmse - 1) * 100 | |
| logging.warning(f"QUYẾT ĐỊNH: Giữ lại mô hình cũ (Không đủ cải thiện, kém hơn {improvement:.2f}%)") | |
| return False, new_metrics, old_metrics | |
| except Exception as e: | |
| logging.error(f"Lỗi xảy ra trong quá trình so sánh mô hình: {e}") | |
| logging.warning("QUYẾT ĐỊNH: Giữ lại mô hình cũ để đảm bảo an toàn.") | |
| return False, new_metrics, None | |
| def retrain_pipeline(data_path): | |
| logging.info("Bắt đầu quy trình huấn luyện lại...") | |
| X_train, y_train, X_test, y_test = preprocess_data(data_path) | |
| logging.info("Bắt đầu quá trình tuning và huấn luyện bộ mô hình mới...") | |
| new_models, new_pipelines, best_params, task = train_model(X_train, y_train) | |
| logging.info("Huấn luyện bộ mô hình mới hoàn tất.") | |
| if not new_models: | |
| logging.error("Quá trình huấn luyện không tạo ra được mô hình mới. Dừng lại.") | |
| return "training_failed", None, None, None | |
| is_new_model_better, new_metrics, old_metrics = compare_models( | |
| new_models=new_models, new_pipelines=new_pipelines, | |
| old_model_path=str(CURRENT_MODEL_PATH), | |
| X_test=X_test, y_test=y_test | |
| ) | |
| final_new_model = CustomMultiOutputRegressor( | |
| models_per_target=new_models, | |
| complete_pipelines_per_target=new_pipelines | |
| ) | |
| deployment_decision = "kept_old_model" | |
| final_metrics = old_metrics | |
| if is_new_model_better: | |
| deployment_decision = "deployed_new_model" | |
| final_metrics = new_metrics | |
| logging.info(f"Triển khai bộ mô hình mới...") | |
| save_artifacts( | |
| models=new_models, pipelines=new_pipelines, best_params=best_params, | |
| metrics=final_metrics, final_model_object=final_new_model, | |
| task=task, output_path=str(CURRENT_MODEL_PATH) | |
| ) | |
| try: | |
| logging.info("Bắt đầu quá trình chuyển đổi sang định dạng ONNX...") | |
| # subprocess.run(['python', str(BASE_DIR / 'src' / 'convert_to_onnx.py')], check=True) | |
| logging.info("Chuyển đổi ONNX hoàn tất.") | |
| except (subprocess.CalledProcessError, FileNotFoundError) as e: | |
| logging.error(f"Lỗi khi chạy script convert_to_onnx.py: {e}") | |
| else: | |
| logging.info("Giữ lại bộ mô hình cũ. Không có thay đổi nào được triển khai.") | |
| final_metrics = old_metrics if old_metrics else (new_metrics if new_metrics else {}) | |
| task.set_parameters({"Deployment Decision": deployment_decision}) | |
| task.close() | |
| logging.info("Quy trình huấn luyện lại đã được ghi lại trên ClearML.") | |
| return deployment_decision, final_metrics, new_metrics, old_metrics | |
| def main(): | |
| print("="*50 + "\n BẮT ĐẦU QUY TRÌNH HUẤN LUYỆN LẠI MÔ HÌNH \n" + "="*50) | |
| results = retrain_pipeline(data_path=str(DATA_PATH)) | |
| if not results or results[0] == "training_failed": | |
| print("\n[LỖI] Quy trình huấn luyện lại đã thất bại. Vui lòng kiểm tra logs.") | |
| return | |
| deployment_decision, final_metrics, new_metrics, old_metrics = results | |
| print("\n" + "="*50 + "\n KẾT QUẢ HUẤN LUYỆN LẠI \n" + "="*50) | |
| if deployment_decision == "deployed_new_model": | |
| print("\n[QUYẾT ĐỊNH]: Triển khai MÔ HÌNH MỚI thành công!") | |
| if old_metrics: | |
| improvement = (1 - new_metrics["average"]["RMSE"] / old_metrics["average"]["RMSE"]) * 100 | |
| print(f"-> Cải thiện so với mô hình cũ: {improvement:.2f}%") | |
| else: | |
| print("-> Đây là mô hình đầu tiên được triển khai.") | |
| else: | |
| print("\n[QUYẾT ĐỊNH]: Giữ lại MÔ HÌNH CŨ.") | |
| if new_metrics: print("-> Mô hình mới không đạt đủ ngưỡng cải thiện.") | |
| print("\n--- Metrics của mô hình được triển khai ---") | |
| if final_metrics and 'average' in final_metrics: | |
| print(f" RMSE (trung bình): {final_metrics['average'].get('RMSE', 'N/A'):.4f}") | |
| print(f" R2 (trung bình): {final_metrics['average'].get('R2', 'N/A'):.4f}") | |
| print(f" MAE (trung bình): {final_metrics['average'].get('MAE', 'N/A'):.4f}") | |
| else: | |
| print(" Không có metrics để hiển thị.") | |
| print("\n" + "="*50 + "\n QUY TRÌNH ĐÃ HOÀN TẤT \n" + "="*50) | |
| if __name__ == "__main__": | |
| main() |