zhanghao пре 2 недеља
родитељ
комит
deb1f29931

+ 0 - 60
models/causal-inference/args.py

@@ -1,60 +0,0 @@
-import torch
-import argparse
-
-def get_args():
-    parser = argparse.ArgumentParser(description='RL-Optimized GAT for time series prediction')
-    
-    # 数据参数
-    parser.add_argument('--data_dir', type=str, default='../datasets_xishan', 
-                       help='Directory for data files')
-    parser.add_argument('--num_files', type=int, default=50, 
-                       help='Number of data files (1 to num_files)')
-    parser.add_argument('--test_ratio', type=float, default=0.2, 
-                       help='Ratio of test data')
-    parser.add_argument('--val_ratio', type=float, default=0.1, 
-                       help='Ratio of validation data')
-    
-    # 模型参数
-    parser.add_argument('--num_features', type=int, default=145, 
-                       help='Number of feature variables')
-    parser.add_argument('--num_targets', type=int, default=47, 
-                       help='Number of target variables')
-    parser.add_argument('--hidden_dim', type=int, default=64, 
-                       help='Default hidden dimension of GAT')
-    parser.add_argument('--num_heads', type=int, default=4, 
-                       help='Default number of attention heads')
-    parser.add_argument('--dropout', type=float, default=0.3, 
-                       help='Default dropout rate')
-    
-    # 训练参数
-    parser.add_argument('--batch_size', type=int, default=128, 
-                       help='Batch size')
-    parser.add_argument('--lr', type=float, default=0.001, 
-                       help='Default learning rate')
-    parser.add_argument('--epochs', type=int, default=100, 
-                       help='Number of epochs for final training')
-    parser.add_argument('--weight_decay', type=float, default=1e-4, 
-                       help='Weight decay')
-    parser.add_argument('--device', type=str, default='cuda' if torch.cuda.is_available() else 'cpu',
-                       help='Device to use for training')
-    parser.add_argument('--grad_clip', type=float, default=1.0,
-                       help='Gradient clipping threshold')
-    parser.add_argument('--patience', type=int, default=20,
-                       help='Patience for early stopping')
-    
-    # 强化学习参数
-    parser.add_argument('--rl_timesteps', type=int, default=5000, 
-                       help='Total timesteps for RL training')
-    parser.add_argument('--rl_max_steps', type=int, default=20, 
-                       help='Max steps per RL episode')
-    parser.add_argument('--rl_eval_episodes', type=int, default=10, 
-                       help='Number of episodes for RL evaluation')
-    
-    # 小波去噪参数
-    parser.add_argument('--wavelet', type=str, default='db4',
-                       help='Wavelet type for denoising')
-    parser.add_argument('--wavelet_level', type=int, default=1,
-                       help='Wavelet decomposition level')
-    
-    args = parser.parse_args()
-    return args

+ 0 - 227
models/causal-inference/data_preprocessor.py

@@ -1,227 +0,0 @@
-import os
-import pandas as pd
-import numpy as np
-import pywt
-import logging
-from sklearn.preprocessing import StandardScaler
-from sklearn.model_selection import train_test_split
-import torch
-import joblib
-from torch.utils.data import TensorDataset, DataLoader
-
-class DataPreprocessor:
-    def __init__(self, args, logger=None):
-        self.args = args
-        self.data_dir = args.data_dir
-        self.num_files = args.num_files
-        self.scaler_features = StandardScaler()
-        self.scaler_targets = StandardScaler()
-        self.logger = logger if logger is not None else self._default_logger()
-        self.features = None  # 保存特征数据用于构建邻接矩阵
-        self.scaler_dir = 'scalers'
-        os.makedirs(self.scaler_dir, exist_ok=True)
-        self.features_scaler_path = os.path.join(self.scaler_dir, 'features_scaler.joblib')
-        self.targets_scaler_path = os.path.join(self.scaler_dir, 'targets_scaler.joblib')
-        
-    def _default_logger(self):
-        """默认日志记录器"""
-        logger = logging.getLogger('DataPreprocessor')
-        logger.setLevel(logging.INFO)
-        console_handler = logging.StreamHandler()
-        console_handler.setLevel(logging.INFO)
-        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
-        console_handler.setFormatter(formatter)
-        logger.addHandler(console_handler)
-        return logger
-    
-    def load_data(self):
-        """加载所有数据文件并合并"""
-        all_data = []
-        
-        for i in range(1, self.num_files + 1):
-            file_path = os.path.join(self.data_dir, f'data_process_{i}.csv')
-            try:
-                df = pd.read_csv(file_path, index_col=0)
-                df = df.reset_index()  # 将原索引作为第一列
-                all_data.append(df)
-                self.logger.info(f"Loaded file {i}/{self.num_files}")
-            except Exception as e:
-                self.logger.error(f"Error loading file {i}: {e}")
-        
-        combined_df = pd.concat(all_data, ignore_index=True)
-        return combined_df
-    
-    def decompose_time(self, df):
-        """将时间列分解为年、月、日、时、分、秒"""
-        time_col = df.columns[0]
-        df[time_col] = pd.to_datetime(df[time_col])
-        
-        df['year'] = df[time_col].dt.year
-        df['month'] = df[time_col].dt.month
-        df['day'] = df[time_col].dt.day
-        df['hour'] = df[time_col].dt.hour
-        df['minute'] = df[time_col].dt.minute
-        df['second'] = df[time_col].dt.second
-        
-        df = df.drop(columns=[time_col])
-        
-        # 调整列顺序
-        time_features = ['year', 'month', 'day', 'hour', 'minute', 'second']
-        other_features = [col for col in df.columns if col not in time_features]
-        df = df[time_features + other_features]
-        
-        return df
-    
-    def wavelet_denoising(self, data, wavelet='db4', level=1):
-        """对数据进行小波降噪,避免除以零警告"""
-        denoised_data = np.zeros_like(data)
-        epsilon = 1e-10  # 极小值,避免除以零
-        
-        for i in range(data.shape[1]):
-            # 小波分解
-            coeffs = pywt.wavedec(data[:, i], wavelet, level=level)
-            
-            # 计算阈值时避免系数为零
-            sigma = np.median(np.abs(coeffs[-level] + epsilon)) / 0.6745  # 加epsilon
-            original_length = len(data[:, i])
-            threshold = sigma * np.sqrt(2 * np.log(original_length))
-            
-            # 对系数进行阈值处理(手动实现软阈值,避免库函数警告)
-            processed_coeffs = []
-            for c in coeffs[1:]:
-                # 手动计算软阈值:y = sign(x) * max(|x| - threshold, 0)
-                magnitude = np.abs(c)
-                # 避免除以零:给magnitude加epsilon
-                thresholded = np.where(
-                    magnitude > threshold,
-                    np.sign(c) * (magnitude - threshold),
-                    0
-                )
-                processed_coeffs.append(thresholded)
-            
-            coeffs[1:] = processed_coeffs
-            
-            # 小波重构(保持之前的长度对齐处理)
-            reconstructed = pywt.waverec(coeffs, wavelet)
-            # 补充之前的长度对齐逻辑(如果之前已添加)
-            original_length = data[:, i].shape[0]
-            if len(reconstructed) > original_length:
-                reconstructed = reconstructed[:original_length]
-            elif len(reconstructed) < original_length:
-                reconstructed = np.pad(reconstructed, (0, original_length - len(reconstructed)), mode='edge')
-            
-            denoised_data[:, i] = reconstructed
-        
-        return denoised_data
-    
-    def normalize_data(self, features, targets):
-        """归一化数据并保存scaler"""
-        features_scaled = self.scaler_features.fit_transform(features)
-        targets_scaled = self.scaler_targets.fit_transform(targets)
-        
-        # 保存scaler
-        joblib.dump(self.scaler_features, self.features_scaler_path)
-        joblib.dump(self.scaler_targets, self.targets_scaler_path)
-        self.logger.info(f"已保存特征归一化模型到 {self.features_scaler_path}")
-        self.logger.info(f"已保存目标归一化模型到 {self.targets_scaler_path}")
-        
-        return features_scaled, targets_scaled
-    
-    def inverse_transform_targets(self, targets_scaled):
-        """将归一化的目标变量反变换回原始尺度"""
-        return self.scaler_targets.inverse_transform(targets_scaled)
-    
-    def split_data(self, features, targets):
-        """划分训练集、验证集和测试集"""
-        X_train, X_temp, y_train, y_temp = train_test_split(
-            features, targets, test_size=self.args.test_ratio + self.args.val_ratio, 
-            shuffle=False
-        )
-        
-        test_size = self.args.test_ratio / (self.args.test_ratio + self.args.val_ratio)
-        X_val, X_test, y_val, y_test = train_test_split(
-            X_temp, y_temp, test_size=test_size, shuffle=False
-        )
-        
-        return X_train, X_val, X_test, y_train, y_val, y_test
-    
-    def create_dataloaders(self, X_train, X_val, X_test, y_train, y_val, y_test):
-        """创建DataLoader"""
-        X_train = torch.FloatTensor(X_train)
-        y_train = torch.FloatTensor(y_train)
-        X_val = torch.FloatTensor(X_val)
-        y_val = torch.FloatTensor(y_val)
-        X_test = torch.FloatTensor(X_test)
-        y_test = torch.FloatTensor(y_test)
-        
-        train_dataset = TensorDataset(X_train, y_train)
-        val_dataset = TensorDataset(X_val, y_val)
-        test_dataset = TensorDataset(X_test, y_test)
-        
-        train_loader = DataLoader(
-            train_dataset, batch_size=self.args.batch_size, shuffle=True
-        )
-        val_loader = DataLoader(
-            val_dataset, batch_size=self.args.batch_size, shuffle=False
-        )
-        test_loader = DataLoader(
-            test_dataset, batch_size=self.args.batch_size, shuffle=False
-        )
-        
-        return train_loader, val_loader, test_loader
-    
-    def preprocess(self):
-        """完整的预处理流程"""
-        df = self.load_data()
-        self.logger.info(f"Original data shape: {df.shape}")
-        
-        df = self.decompose_time(df)
-        self.logger.info(f"Data shape after time decomposition: {df.shape}")
-        
-        data = df.values
-        data_denoised = self.wavelet_denoising(data)
-        self.logger.info(f"Data shape after wavelet denoising: {data_denoised.shape}")
-        
-        # 保存特征数据用于构建邻接矩阵
-        self.features = data_denoised[:, :self.args.num_features]
-        targets = data_denoised[:, self.args.num_features:self.args.num_features+self.args.num_targets]
-        self.logger.info(f"Features shape: {self.features.shape}, Targets shape: {targets.shape}")
-        
-        features_scaled, targets_scaled = self.normalize_data(self.features, targets)
-        
-        X_train, X_val, X_test, y_train, y_val, y_test = self.split_data(
-            features_scaled, targets_scaled
-        )
-        self.logger.info(f"Train: {X_train.shape}, Val: {X_val.shape}, Test: {X_test.shape}")
-        
-        train_loader, val_loader, test_loader = self.create_dataloaders(
-            X_train, X_val, X_test, y_train, y_val, y_test
-        )
-        
-        return train_loader, val_loader, test_loader, self
-    
-    def create_adjacency_matrix(self):
-        """创建有向图的邻接矩阵(基于特征相关性)"""
-        num_nodes = self.args.num_features
-        adj = torch.zeros((num_nodes, num_nodes))
-        
-        if self.features is None:
-            self.logger.warning("特征数据未初始化,使用默认邻接矩阵")
-            # 默认自连接
-            for i in range(num_nodes):
-                adj[i, i] = 1
-            return adj
-        
-        # 计算特征之间的相关性
-        corr_matrix = np.corrcoef(self.features.T)
-        corr_threshold = 0.3  # 相关性阈值
-        
-        # 基于相关性构建有向边
-        for i in range(num_nodes):
-            adj[i, i] = 1  # 自连接
-            for j in range(num_nodes):
-                if i != j and abs(corr_matrix[i, j]) > corr_threshold:
-                    adj[i, j] = 1
-        
-        self.logger.info(f"邻接矩阵中边的数量: {int(torch.sum(adj))}")
-        return adj

+ 0 - 262
models/causal-inference/data_trainer.py

@@ -1,262 +0,0 @@
-import torch
-import torch.nn as nn
-import torch.optim as optim
-import numpy as np
-import matplotlib.pyplot as plt
-from tqdm import tqdm
-import os
-from sklearn.metrics import mean_absolute_error, mean_squared_error
-import logging
-
-class DataTrainer:
-    def __init__(self, model, args, preprocessor, optimizer=None, scheduler=None, logger=None):
-        self.model = model
-        self.args = args
-        self.preprocessor = preprocessor
-        self.device = args.device
-        self.logger = logger if logger is not None else self._default_logger()
-        
-        self.criterion = nn.MSELoss()
-        self.optimizer = optimizer if optimizer is not None else optim.Adam(
-            model.parameters(),
-            lr=args.lr,
-            weight_decay=args.weight_decay
-        )
-        self.scheduler = scheduler
-        
-        self.train_losses = []
-        self.val_losses = []
-        self.train_mae = []
-        self.val_mae = []
-        self.best_val_loss = float('inf')
-        self.early_stop_counter = 0
-        self.model_save_path = os.path.join('models', 'best_model.pth')
-        self.final_model_path = os.path.join('models', 'final_model.pth')
-        
-        if not os.path.exists('models'):
-            os.makedirs('models')
-        if not os.path.exists('plots'):
-            os.makedirs('plots')
-        
-    def _default_logger(self):
-        logger = logging.getLogger('DefaultLogger')
-        logger.setLevel(logging.INFO)
-        console_handler = logging.StreamHandler()
-        console_handler.setLevel(logging.INFO)
-        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
-        console_handler.setFormatter(formatter)
-        if not logger.handlers:
-            logger.addHandler(console_handler)
-        return logger
-    
-    @torch.enable_grad()
-    def train_epoch(self, train_loader, adj, max_batches=None):
-        """
-        支持通过 max_batches 限制本 epoch 使用的批次数(用于 RL 快速评估)。
-        """
-        self.model.train()
-        total_loss = 0.0
-        all_outputs = []
-        all_targets = []
-        adj = adj.to(self.device)
-
-        for batch_idx, (data, target) in enumerate(train_loader):
-            if max_batches is not None and batch_idx >= max_batches:
-                break
-            data, target = data.to(self.device), target.to(self.device)
-            data = data.unsqueeze(-1)  # (batch_size, 145, 1)
-            
-            self.optimizer.zero_grad()
-            output = self.model(data, adj)  # (batch_size, 145, 47)
-            output = output.mean(dim=1)     # (batch_size, 47)
-            
-            loss = self.criterion(output, target)
-            loss.backward()
-            if self.args.grad_clip > 0:
-                torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.args.grad_clip)
-            self.optimizer.step()
-            
-            total_loss += loss.item()
-            all_outputs.append(output.detach().cpu().numpy())
-            all_targets.append(target.detach().cpu().numpy())
-        
-        if len(all_outputs) == 0:
-            # 在极小数据/极小 batch 情况下的保护
-            return float('inf'), float('inf')
-        
-        all_outputs = np.vstack(all_outputs)
-        all_targets = np.vstack(all_targets)
-        mae = mean_absolute_error(all_targets, all_outputs)
-        
-        avg_loss = total_loss / (min(len(train_loader), max_batches) if max_batches else len(train_loader))
-        self.train_losses.append(avg_loss)
-        self.train_mae.append(mae)
-        return avg_loss, mae
-    
-    @torch.no_grad()
-    def validate(self, val_loader, adj, max_batches=None):
-        """
-        支持通过 max_batches 限制验证批次数(用于 RL 快速评估)。
-        """
-        self.model.eval()
-        total_loss = 0.0
-        all_outputs = []
-        all_targets = []
-        adj = adj.to(self.device)
-        
-        for batch_idx, (data, target) in enumerate(val_loader):
-            if max_batches is not None and batch_idx >= max_batches:
-                break
-            data, target = data.to(self.device), target.to(self.device)
-            data = data.unsqueeze(-1)  # (batch_size, 145, 1)
-            output = self.model(data, adj)  # (batch_size, 145, 47)
-            output = output.mean(dim=1)     # (batch_size, 47)
-            loss = self.criterion(output, target)
-            total_loss += loss.item()
-            all_outputs.append(output.cpu().numpy())
-            all_targets.append(target.cpu().numpy())
-        
-        if len(all_outputs) == 0:
-            return float('inf'), float('inf')
-        
-        all_outputs = np.vstack(all_outputs)
-        all_targets = np.vstack(all_targets)
-        mae = mean_absolute_error(all_targets, all_outputs)
-        avg_loss = total_loss / (min(len(val_loader), max_batches) if max_batches else len(val_loader))
-        self.val_losses.append(avg_loss)
-        self.val_mae.append(mae)
-        return avg_loss, mae
-    
-    def train(self, train_loader, val_loader, adj, epochs=None):
-        adj = adj.to(self.device)
-        epochs = epochs if epochs is not None else self.args.epochs
-        self.logger.info(f"开始训练,共 {epochs} 个epoch")
-        
-        for epoch in tqdm(range(epochs)):
-            train_loss, train_mae = self.train_epoch(train_loader, adj)
-            val_loss, val_mae = self.validate(val_loader, adj)
-            if self.scheduler is not None:
-                self.scheduler.step(val_loss)
-            
-            # 保存最佳模型
-            if val_loss < self.best_val_loss:
-                self.best_val_loss = val_loss
-                torch.save({
-                    'epoch': epoch,
-                    'model_state_dict': self.model.state_dict(),
-                    'optimizer_state_dict': self.optimizer.state_dict(),
-                    'loss': val_loss,
-                    'args': self.args  # 保存训练参数
-                }, self.model_save_path)
-                self.early_stop_counter = 0
-                self.logger.info(f"已更新最佳模型到 {self.model_save_path}")  # 日志
-            else:
-                self.early_stop_counter += 1
-                if self.early_stop_counter >= self.args.patience:
-                    self.logger.info(f"早停机制触发,在第 {epoch+1} 轮停止训练")
-                    break
-            
-            if (epoch + 1) % 10 == 0:
-                self.logger.info(f'Epoch {epoch+1}/{epochs}, '
-                                 f'Train Loss: {train_loss:.6f}, Train MAE: {train_mae:.6f}, '
-                                 f'Val Loss: {val_loss:.6f}, Val MAE: {val_mae:.6f}')
-        
-        self.plot_losses()
-        self.plot_mae()
-        
-        # 加载最佳模型
-        checkpoint = torch.load(self.model_save_path, map_location=self.device)
-        self.model.load_state_dict(checkpoint['model_state_dict'])
-        self.logger.info(f"加载最佳模型(第 {checkpoint['epoch']+1} 轮,验证损失: {checkpoint['loss']:.6f})")
-        
-        # 保存最终训练完成的模型(加载最佳模型后)
-        torch.save({
-            'model_state_dict': self.model.state_dict(),
-            'optimizer_state_dict': self.optimizer.state_dict(),
-            'best_val_loss': self.best_val_loss,
-            'args': self.args
-        }, self.final_model_path)
-        self.logger.info(f"已保存最终模型到 {self.final_model_path}")
-        
-        return self.model
-    
-    @torch.no_grad()
-    def test(self, test_loader, adj):
-        self.model.eval()
-        total_loss = 0.0
-        all_outputs = []
-        all_targets = []
-        adj = adj.to(self.device)
-        
-        for data, target in test_loader:
-            data, target = data.to(self.device), target.to(self.device)
-            data = data.unsqueeze(-1)
-            output = self.model(data, adj)
-            output = output.mean(dim=1)
-            loss = self.criterion(output, target)
-            total_loss += loss.item()
-            all_outputs.append(output.cpu().numpy())
-            all_targets.append(target.cpu().numpy())
-        
-        all_outputs = np.vstack(all_outputs)
-        all_targets = np.vstack(all_targets)
-        mse = total_loss / len(test_loader)
-        mae = mean_absolute_error(all_targets, all_outputs)
-        rmse = np.sqrt(mean_squared_error(all_targets, all_outputs))
-        
-        all_outputs_original = self.preprocessor.inverse_transform_targets(all_outputs)
-        all_targets_original = self.preprocessor.inverse_transform_targets(all_targets)
-        original_mse = mean_squared_error(all_targets_original, all_outputs_original)
-        original_mae = mean_absolute_error(all_targets_original, all_outputs_original)
-        original_rmse = np.sqrt(original_mse)
-        
-        self.logger.info(f'Test Loss (normalized): MSE={mse:.6f}, MAE={mae:.6f}, RMSE={rmse:.6f}')
-        self.logger.info(f'Test Loss (original scale): MSE={original_mse:.6f}, MAE={original_mae:.6f}, RMSE={original_rmse:.6f}')
-        self.plot_predictions(all_outputs_original, all_targets_original)
-        return {
-            'normalized_mse': mse,
-            'normalized_mae': mae,
-            'normalized_rmse': rmse,
-            'original_mse': original_mse,
-            'original_mae': original_mae,
-            'original_rmse': original_rmse,
-            'predictions': all_outputs_original,
-            'targets': all_targets_original
-        }
-    
-    def plot_losses(self):
-        plt.figure(figsize=(10, 6))
-        plt.plot(self.train_losses, label='Train Loss')
-        plt.plot(self.val_losses, label='Validation Loss')
-        plt.xlabel('Epoch')
-        plt.ylabel('MSE Loss')
-        plt.title('Training and Validation Loss')
-        plt.legend()
-        plt.savefig('plots/loss_curve.png')
-        plt.close()
-    
-    def plot_mae(self):
-        plt.figure(figsize=(10, 6))
-        plt.plot(self.train_mae, label='Train MAE')
-        plt.plot(self.val_mae, label='Validation MAE')
-        plt.xlabel('Epoch')
-        plt.ylabel('MAE')
-        plt.title('Training and Validation MAE')
-        plt.legend()
-        plt.savefig('plots/mae_curve.png')
-        plt.close()
-    
-    def plot_predictions(self, predictions, targets):
-        num_plots = min(3, self.args.num_targets)
-        plt.figure(figsize=(15, 5*num_plots))
-        for i in range(num_plots):
-            plt.subplot(num_plots, 1, i+1)
-            plt.plot(targets[:100, i], label='True Value')
-            plt.plot(predictions[:100, i], label='Predicted Value')
-            plt.xlabel('Time Step')
-            plt.ylabel(f'Target {i+1}')
-            plt.title(f'Prediction vs True Value for Target {i+1}')
-            plt.legend()
-        plt.tight_layout()
-        plt.savefig('plots/prediction_examples.png')
-        plt.close()

BIN
models/causal-inference/features_scaler.joblib


+ 0 - 226
models/causal-inference/gat.py

@@ -1,226 +0,0 @@
-"""
-有向图注意力网络 (Directed Graph Attention Network)
-
-实现基于有向图的注意力机制,用于建模节点间的非对称因果关系。
-与传统GAT不同,本实现分离源节点和目标节点的注意力参数,更适合因果推理任务。
-
-核心特性:
-    - 有向注意力: 源节点和目标节点使用独立的注意力参数
-    - 多头机制: 并行学习多种关系模式
-    - 邻接掩码: 仅在图中存在的边上计算注意力
-
-技术实现:
-    - 框架: PyTorch
-    - 注意力: 加性注意力 (Additive Attention)
-    - 激活函数: LeakyReLU (α=0.2)
-"""
-
-import torch
-import torch.nn as nn
-import torch.nn.functional as F
-
-class GraphAttentionLayer(nn.Module):
-    """
-    有向图注意力层 (Directed Graph Attention Layer)
-
-    实现单层有向图注意力机制,是GAT模型的基本构建块。
-    通过分离源节点和目标节点的注意力参数,支持建模非对称的因果关系。
-
-    核心思想:
-        传统GAT使用对称注意力权重 (A→B 和 B→A 权重相同)
-        有向GAT分离源节点和目标节点参数,学习方向性的因果影响
-
-    注意力计算:
-        e_ij = LeakyReLU(a_src^T·Wh_i + a_dst^T·Wh_j)
-        α_ij = softmax_j(e_ij)
-        h_i' = σ(Σ_j α_ij·Wh_j)
-
-    Args:
-        in_features (int): 输入特征维度
-        out_features (int): 输出特征维度
-        dropout (float): Dropout概率 [0,1]
-        alpha (float): LeakyReLU负斜率,默认0.2
-        concat (bool): 是否使用ELU激活 (True用于中间层,False用于输出层)
-
-    Example:
-        >>> layer = GraphAttentionLayer(1, 64, dropout=0.3, alpha=0.2, concat=True)
-        >>> h = torch.randn(32, 145, 1)  # (batch, nodes, features)
-        >>> adj = torch.ones(145, 145)   # 邻接矩阵
-        >>> output = layer(h, adj)       # (32, 145, 64)
-    """
-    def __init__(self, in_features, out_features, dropout, alpha, concat=True):
-        super(GraphAttentionLayer, self).__init__()
-        self.dropout = dropout
-        self.in_features = in_features
-        self.out_features = out_features
-        self.alpha = alpha
-        self.concat = concat
-        
-        # 特征变换矩阵: 输入特征 → 输出特征空间
-        # Xavier初始化保证前向/反向传播时方差稳定
-        self.W = nn.Parameter(torch.empty(size=(in_features, out_features)))
-        nn.init.xavier_uniform_(self.W.data, gain=1.414)  # gain=1.414 适配LeakyReLU
-
-        # 有向注意力参数 (核心创新)
-        # a_src: 源节点注意力向量 (发出边的权重)
-        # a_dst: 目标节点注意力向量 (接收边的权重)
-        # 分离参数使模型能够学习非对称因果关系
-        self.a_src = nn.Parameter(torch.empty(size=(out_features, 1)))
-        self.a_dst = nn.Parameter(torch.empty(size=(out_features, 1)))
-        nn.init.xavier_uniform_(self.a_src.data, gain=1.414)
-        nn.init.xavier_uniform_(self.a_dst.data, gain=1.414)
-
-        # LeakyReLU: 允许负值有小梯度,防止神经元死亡
-        self.leakyrelu = nn.LeakyReLU(self.alpha)
-        
-    def forward(self, h, adj):
-        """
-        前向传播
-
-        计算流程:
-            1. 线性变换: Wh = h @ W
-            2. 计算源/目标节点注意力分数
-            3. 构建注意力矩阵: e_ij = LeakyReLU(src_i + dst_j)
-            4. 应用邻接掩码 (不存在的边设为-∞)
-            5. Softmax归一化得到注意力权重 α_ij
-            6. 加权聚合邻居特征: h_i' = Σ_j α_ij·Wh_j
-
-        Args:
-            h (Tensor): 输入特征 (batch_size, num_nodes, in_features)
-                例: (32, 145, 1)
-            adj (Tensor): 邻接矩阵 (num_nodes, num_nodes)
-                adj[i,j]=1 表示节点i→j存在有向边
-
-        Returns:
-            Tensor: 输出特征 (batch_size, num_nodes, out_features)
-                经过图注意力聚合后的节点特征
-        """
-        batch_size = h.size(0) 
-        num_nodes = h.size(1)
-
-        # Step 1: 线性变换 (batch, nodes, in_features) → (batch, nodes, out_features)
-        Wh = torch.matmul(h, self.W)
-
-        # Step 2-3: 计算源/目标节点注意力分数
-        a_input_src = torch.matmul(Wh, self.a_src)  # 源节点分数 (信息发送方)
-        a_input_dst = torch.matmul(Wh, self.a_dst)  # 目标节点分数 (信息接收方)
-
-        # Step 4: 构建注意力矩阵 (广播: src[i] + dst[j]^T)
-        # e[i,j] = a_src^T·Wh_i + a_dst^T·Wh_j
-        e = a_input_src + a_input_dst.transpose(1, 2)
-        e = self.leakyrelu(e)
-
-        # Step 5: 应用邻接掩码 (不存在的边设为-9e15,softmax后≈0)
-        zero_vec = -9e15 * torch.ones_like(e)
-        attention = torch.where(adj > 0, e, zero_vec)
-
-        # Step 6: Softmax归一化 (dim=2: 对每个节点的所有邻居归一化)
-        attention = F.softmax(attention, dim=2)
-        attention = F.dropout(attention, self.dropout, training=self.training)
-
-        # Step 7: 加权聚合邻居特征 h_i' = Σ_j α_ij·Wh_j
-        h_prime = torch.matmul(attention, Wh)
-
-        # 中间层使用ELU激活,输出层保持线性
-        if self.concat:
-            return F.elu(h_prime)
-        else:
-            return h_prime
-        
-    def __repr__(self):
-        return self.__class__.__name__ + f'({self.in_features} -> {self.out_features})'
-
-class GAT(nn.Module):
-    """
-    多层图注意力网络 (Multi-layer Graph Attention Network)
-
-    组合多个图注意力层构建完整的GAT模型,采用多头注意力机制从不同视角捕捉节点关系。
-
-    网络结构:
-        输入 → 多头注意力层 (nheads个并行) → 拼接 → 输出注意力层 → 输出
-
-    Args:
-        nfeat (int): 输入特征维度,例: 1
-        nhid (int): 隐藏层维度 (每个注意力头的输出维度),推荐: 32-128
-        noutput (int): 输出维度 (目标变量数量),例: 47
-        dropout (float): Dropout概率 [0,1],例: 0.3
-        alpha (float): LeakyReLU负斜率,例: 0.2
-        nheads (int): 注意力头数量,例: 4
-
-    多头注意力机制:
-        多个独立注意力头并行学习不同关系模式 (直接因果、间接影响、周期性等)
-        最后拼接所有头的输出,形成丰富的特征表示
-
-    维度变化:
-        (batch, 145, 1) → [多头] → (batch, 145, nhid×nheads)
-        → [输出层] → (batch, 145, noutput)
-
-    Example:
-        >>> model = GAT(nfeat=1, nhid=64, noutput=47, dropout=0.3, alpha=0.2, nheads=4)
-        >>> x = torch.randn(32, 145, 1)
-        >>> adj = torch.ones(145, 145)
-        >>> output = model(x, adj)  # (32, 145, 47)
-    """
-    def __init__(self, nfeat, nhid, noutput, dropout, alpha, nheads):
-        super(GAT, self).__init__()
-        self.dropout = dropout
-        
-        # 多头注意力层: 创建 nheads 个独立的图注意力层
-        self.attentions = [
-            GraphAttentionLayer(
-                in_features=nfeat,
-                out_features=nhid,
-                dropout=dropout,
-                alpha=alpha,
-                concat=True  # 中间层使用ELU激活
-            )
-            for _ in range(nheads)
-        ]
-
-        # 注册为子模块,使参数可被自动追踪和优化
-        for i, attention in enumerate(self.attentions):
-            self.add_module(f'attention_{i}', attention)
-
-        # 输出注意力层: 输入维度 = nhid×nheads (拼接后)
-        self.out_att = GraphAttentionLayer(
-            in_features=nhid * nheads,
-            out_features=noutput,
-            dropout=dropout,
-            alpha=alpha,
-            concat=False  # 输出层保持线性
-        )
-        
-    def forward(self, x, adj):
-        """
-        前向传播
-
-        计算流程:
-            1. 输入dropout
-            2. 多头注意力并行计算并拼接
-            3. 中间dropout
-            4. 输出层 + ELU激活
-
-        Args:
-            x (Tensor): 输入特征 (batch_size, num_nodes, nfeat)
-                例: (32, 145, 1)
-            adj (Tensor): 邻接矩阵 (num_nodes, num_nodes)
-                adj[i,j]=1 表示特征i对特征j有因果影响
-
-        Returns:
-            Tensor: 输出特征 (batch_size, num_nodes, noutput)
-                例: (32, 145, 47)
-        """
-        # 输入dropout (防止过拟合)
-        x = F.dropout(x, self.dropout, training=self.training)
-
-        # 多头注意力并行计算 + 拼接
-        # (batch, nodes, nfeat) → nheads × (batch, nodes, nhid) → (batch, nodes, nhid×nheads)
-        x = torch.cat([att(x, adj) for att in self.attentions], dim=2)
-
-        # 中间dropout
-        x = F.dropout(x, self.dropout, training=self.training)
-
-        # 输出层 + ELU激活
-        x = F.elu(self.out_att(x, adj))
-
-        return x

+ 0 - 313
models/causal-inference/main.py

@@ -1,313 +0,0 @@
-"""
-因果推理模型主程序(Causal Inference Main Program)
-
-本程序实现了基于强化学习优化的图注意力网络训练流程,用于工业时间序列预测。
-整个系统分为三个核心阶段:
-    1. 数据预处理阶段: 数据加载、清洗、降噪、归一化、图构建
-    2. RL超参数优化阶段: 使用PPO算法自动搜索最优超参数
-    3. 最终训练评估阶段: 使用最优参数训练模型并在测试集上评估
-
-核心特点:
-    - 自动化超参数优化: 无需手动调参,RL智能体自动寻找最优配置
-    - 有向图注意力: 建模特征间的因果关系,支持非对称影响
-    - 小波降噪预处理: 提升数据质量,增强模型精度
-    - 完善的监控机制: 日志记录、早停、学习率调度、模型保存
-
-技术栈:
-    - PyTorch: 深度学习框架
-    - Stable-Baselines3: 强化学习库(PPO算法)
-    - PyWavelets: 小波变换库
-    - Scikit-learn: 数据预处理
-
-工作流程:
-    main() → 数据预处理 → RL优化超参数 → 训练最终模型 → 测试评估
-
-"""
-
-import torch.optim as optim
-from args import get_args
-from data_preprocessor import DataPreprocessor
-from gat import GAT
-from data_trainer import DataTrainer
-from rl_optimizer import RLOptimizer
-import logging
-import os
-
-def setup_logger(args):
-    """
-    配置日志系统
-    
-    功能:
-        创建并配置日志记录器,同时输出到控制台和文件。
-        日志文件以训练数据文件数量命名,便于区分不同实验。
-    
-    参数:
-        args: 命令行参数对象
-            - args.num_files: 数据文件数量,用于日志文件命名
-    
-    返回:
-        logging.Logger: 配置好的日志记录器
-    
-    日志级别:
-        INFO: 记录关键步骤和指标信息
-        
-    输出位置:
-        - 控制台: 实时查看训练进度
-        - 文件: logs/training_{num_files}.log,便于事后分析
-    
-    日志格式:
-        时间戳 - 记录器名称 - 日志级别 - 消息内容
-        示例: 2025-01-10 10:30:45 - GAT-Training - INFO - 开始训练
-    
-    技术要点:
-        - 自动创建logs目录
-        - 文件和控制台使用相同的格式化器
-        - 避免重复添加处理器
-    """
-    # 创建日志目录(如果不存在)
-    if not os.path.exists('logs'):
-        os.makedirs('logs')
-    
-    # 创建日志记录器
-    logger = logging.getLogger('GAT-Training')
-    logger.setLevel(logging.INFO)
-    
-    # 文件处理器: 将日志写入文件
-    file_handler = logging.FileHandler(f'logs/training_{args.num_files}.log')
-    file_handler.setLevel(logging.INFO) 
-    
-    # 控制台处理器: 将日志输出到终端
-    console_handler = logging.StreamHandler()
-    console_handler.setLevel(logging.INFO)
-    
-    # 格式化器: 定义日志消息的格式
-    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
-    file_handler.setFormatter(formatter)    
-    console_handler.setFormatter(formatter)
-    
-    # 添加处理器到记录器
-    logger.addHandler(file_handler) # 添加文件处理器
-    logger.addHandler(console_handler) # 添加控制台处理器
-    
-    return logger
-
-def main():
-    """
-    主程序入口
-    
-    功能:
-        协调整个训练流程,包括数据预处理、RL优化、模型训练和测试评估。
-        这是整个系统的控制中心,按顺序执行各个阶段的任务。
-    
-    执行流程:
-        第一阶段: 数据预处理
-            1. 加载50个CSV数据文件
-            2. 时间特征分解(年月日时分秒)
-            3. 小波降噪(db4小波,1层分解)
-            4. 数据归一化(StandardScaler)
-            5. 划分训练集/验证集/测试集(70%/10%/20%)
-            6. 构建有向图邻接矩阵(相关性阈值0.3)
-            
-        第二阶段: RL超参数优化
-            1. 创建GATEnv强化学习环境
-            2. 使用PPO算法训练5000时间步
-            3. 搜索最优超参数(lr, hidden_dim, num_heads, dropout)
-            4. 快速评估策略(1-2个batch)加速收敛
-            5. 选择奖励最高的超参数组合
-            
-        第三阶段: 最终模型训练
-            1. 使用最优超参数创建GAT模型
-            2. 配置Adam优化器和学习率调度器
-            3. 训练最多100轮,早停耐心20轮
-            4. 保存最佳模型和最终模型
-            5. 生成训练曲线图
-            
-        第四阶段: 测试评估
-            1. 加载最佳模型
-            2. 在测试集上评估性能
-            3. 计算归一化和原始尺度的MSE/MAE/RMSE
-            4. 生成预测对比图
-    
-    输出文件:
-        日志文件:
-            - logs/training_{num_files}.log
-        
-        归一化器:
-            - scalers/features_scaler.joblib
-            - scalers/targets_scaler.joblib
-        
-        模型文件:
-            - models/best_model.pth (验证损失最低的模型)
-            - models/final_model.pth (训练完成后的最终模型)
-            - gat_ppo_agent (RL优化器模型)
-        
-        可视化图表:
-            - plots/loss_curve.png (训练/验证损失曲线)
-            - plots/mae_curve.png (训练/验证MAE曲线)
-            - plots/prediction_examples.png (预测vs真实值对比)
-    
-    关键技术:
-        1. RL自动调参: 避免手动网格搜索,智能寻优
-        2. 有向图建模: 捕捉特征间的因果关系
-        3. 小波降噪: 提升数据质量
-        4. 早停机制: 防止过拟合
-        5. 学习率调度: 自适应调整学习率
-    
-    性能优化:
-        - GPU加速: 自动检测并使用CUDA
-        - 梯度裁剪: 防止梯度爆炸
-        - Dropout正则化: 防止过拟合
-        - ReduceLROnPlateau: 验证损失停滞时降低学习率
-    
-    使用示例:
-        >>> python main.py
-        # 使用默认参数训练
-        
-        >>> python main.py --num_files 30 --epochs 50
-        # 自定义参数训练
-    """
-    # ========== 阶段0: 初始化配置 ==========
-    # 获取命令行参数(或使用默认值)
-    args = get_args()
-    
-    # 配置日志系统
-    logger = setup_logger(args)
-    logger.info(f"使用设备: {args.device}")
-    logger.info("=" * 80)
-    logger.info("因果推理模型训练系统启动")
-    logger.info("=" * 80)
-    
-    # ========== 阶段1: 数据预处理 ==========
-    logger.info("\n" + "=" * 80)
-    logger.info("阶段1: 数据预处理")
-    logger.info("=" * 80)
-    
-    # 创建数据预处理器
-    preprocessor = DataPreprocessor(args, logger)
-    
-    # 执行完整的预处理流程
-    # 返回: train_loader(训练数据加载器), val_loader(验证数据加载器), 
-    #       test_loader(测试数据加载器), preprocessor(预处理器对象)
-    train_loader, val_loader, test_loader, preprocessor = preprocessor.preprocess()
-    logger.info("数据预处理完成!")
-    
-    # 创建有向图邻接矩阵
-    # 基于特征相关性构建图结构,相关性>0.3的特征对之间建立有向边
-    adj = preprocessor.create_adjacency_matrix()
-    logger.info(f"邻接矩阵形状: {adj.shape}")
-    logger.info(f"边的数量: {int(adj.sum())}")
-    
-    # ========== 阶段2: RL超参数优化 ==========
-    logger.info("\n" + "=" * 80)
-    logger.info("阶段2: 强化学习超参数优化")
-    logger.info("=" * 80)
-    logger.info("使用PPO算法搜索最优超参数...")
-    
-    # 创建RL优化器
-    # 在环境中评估不同的超参数组合,找到使验证损失最小的配置
-    rl_optimizer = RLOptimizer(args, preprocessor, train_loader, val_loader, adj, logger)
-    
-    # 执行优化,返回最优超参数字典
-    # best_hparams包含: lr(学习率), hidden_dim(隐藏层维度), 
-    #                   num_heads(注意力头数), dropout(dropout率)
-    best_hparams = rl_optimizer.optimize()
-    logger.info(f"最优超参数: {best_hparams}")
-    
-    # ========== 阶段3: 使用最优超参数训练最终模型 ==========
-    logger.info("\n" + "=" * 80)
-    logger.info("阶段3: 训练最终模型")
-    logger.info("=" * 80)
-    logger.info("使用RL优化得到的最优超参数...")
-    
-    # 创建GAT模型,使用最优超参数
-    final_model = GAT(
-        nfeat=1,                          # 输入特征维度(每个节点1维)
-        nhid=best_hparams['hidden_dim'],  # 隐藏层维度(RL优化得到)
-        noutput=args.num_targets,         # 输出维度(47个目标变量)
-        dropout=best_hparams['dropout'],  # Dropout率(RL优化得到)
-        nheads=best_hparams['num_heads'], # 注意力头数(RL优化得到)
-        alpha=0.2                         # LeakyReLU斜率(固定值)
-    ).to(args.device)  # 移动到GPU(如果可用)
-    
-    logger.info(f"模型结构: nfeat=1, nhid={best_hparams['hidden_dim']}, "
-                f"noutput={args.num_targets}, dropout={best_hparams['dropout']}, "
-                f"nheads={best_hparams['num_heads']}")
-    
-    # 配置优化器
-    # Adam优化器: 自适应学习率,使用RL优化得到的学习率
-    optimizer = optim.Adam(
-        final_model.parameters(),
-        lr=best_hparams['lr'],           # 学习率(RL优化得到)
-        weight_decay=args.weight_decay   # L2正则化系数
-    )
-    logger.info(f"优化器: Adam(lr={best_hparams['lr']}, weight_decay={args.weight_decay})")
-    
-    # 配置学习率调度器
-    # ReduceLROnPlateau: 当验证损失停滞时,将学习率降低一半
-    scheduler = optim.lr_scheduler.ReduceLROnPlateau(
-        optimizer, 
-        mode='min',      # 监控指标越小越好(损失函数)
-        factor=0.5,      # 降低因子(新lr = 旧lr * 0.5)
-        patience=10,     # 容忍10轮无改善
-        verbose=True     # 打印学习率变化信息
-    )
-    logger.info("学习率调度器: ReduceLROnPlateau(factor=0.5, patience=10)")
-    
-    # 创建训练器
-    # 负责模型训练、验证、测试和可视化
-    trainer = DataTrainer(
-        model=final_model,
-        args=args,
-        preprocessor=preprocessor,
-        optimizer=optimizer,
-        scheduler=scheduler,
-        logger=logger
-    )
-    
-    # 执行训练
-    # 训练最多100轮,使用早停机制(耐心20轮)
-    # 自动保存最佳模型(验证损失最低)和最终模型
-    logger.info("开始训练循环...")
-    trained_model = trainer.train(train_loader, val_loader, adj)
-    logger.info("模型训练完成!")
-    
-    # ========== 阶段4: 在测试集上评估 ==========
-    logger.info("\n" + "=" * 80)
-    logger.info("阶段4: 测试集评估")
-    logger.info("=" * 80)
-    logger.info("在测试集上评估最终模型性能...")
-    
-    # 测试模型性能
-    # 返回归一化和原始尺度的MSE/MAE/RMSE指标
-    test_results = trainer.test(test_loader, adj)
-    
-    # 打印最终结果摘要
-    logger.info("\n" + "=" * 80)
-    logger.info("训练完成总结")
-    logger.info("=" * 80)
-    logger.info(f"最优超参数: {best_hparams}")
-    logger.info(f"测试集性能(归一化):")
-    logger.info(f"  - MSE:  {test_results['normalized_mse']:.6f}")
-    logger.info(f"  - MAE:  {test_results['normalized_mae']:.6f}")
-    logger.info(f"  - RMSE: {test_results['normalized_rmse']:.6f}")
-    logger.info(f"测试集性能(原始尺度):")
-    logger.info(f"  - MSE:  {test_results['original_mse']:.6f}")
-    logger.info(f"  - MAE:  {test_results['original_mae']:.6f}")
-    logger.info(f"  - RMSE: {test_results['original_rmse']:.6f}")
-    logger.info("=" * 80)
-    logger.info("所有任务完成!")
-    logger.info("=" * 80)
-
-if __name__ == "__main__":
-    """
-    程序入口点
-    
-    直接运行此文件时执行main()函数。
-    支持命令行参数自定义配置,详见args.py。
-    
-    运行方式:
-        python main.py                    # 使用默认参数
-        python main.py --epochs 50        # 自定义训练轮数
-        python main.py --num_files 30     # 自定义数据文件数量
-    """
-    main()

BIN
models/causal-inference/policy.optimizer.pth


BIN
models/causal-inference/policy.pth


+ 0 - 188
models/causal-inference/rl_optimizer.py

@@ -1,188 +0,0 @@
-import torch
-import numpy as np
-import gymnasium as gym
-from gymnasium import spaces
-from stable_baselines3 import PPO
-from stable_baselines3.common.callbacks import BaseCallback
-import torch.optim as optim
-from gat import GAT
-from data_trainer import DataTrainer
-
-class GATEnv(gym.Env):
-    metadata = {'render_modes': ['human'], 'render_fps': 4}
-    
-    def __init__(self, preprocessor, train_loader, val_loader, adj, args, logger):
-        super().__init__()
-        self.preprocessor = preprocessor
-        self.train_loader = train_loader
-        self.val_loader = val_loader
-        # 使用指定设备(支持GPU)
-        self.eval_device = torch.device(args.device)
-        self.adj = adj.to(self.eval_device)
-        self.args = args
-        self.logger = logger
-        
-        self.action_space = spaces.Box(
-            low=np.array([1e-5, 32, 2, 0.1], dtype=np.float32),
-            high=np.array([1e-2, 128, 8, 0.5], dtype=np.float32),
-            shape=(4,),
-            dtype=np.float32
-        )
-        self.observation_space = spaces.Box(
-            low=np.array([1e-5, 32, 2, 0.1, 0], dtype=np.float32),
-            high=np.array([1e-2, 128, 8, 0.5, 100], dtype=np.float32),
-            shape=(5,),
-            dtype=np.float32
-        )
-        self.best_val_loss = float('inf')
-        self.current_step = 0
-        self.max_steps = args.rl_max_steps
-        self.render_mode = None
-    
-    def reset(self, seed=None, options=None):
-        super().reset(seed=seed)
-        self.current_step = 0
-        self.best_val_loss = float('inf')
-        self.current_state = np.array([
-            float(self.args.lr),
-            float(self.args.hidden_dim),
-            float(self.args.num_heads),
-            float(self.args.dropout),
-            10.0
-        ], dtype=np.float32)
-        return self.current_state, {}
-    
-    def step(self, action):
-        self.current_step += 1
-        lr = float(action[0])
-        hidden_dim = int(round(float(action[1])))
-        num_heads = int(round(float(action[2])))
-        dropout = float(action[3])
-        
-        hidden_dim = max(32, min(128, hidden_dim))
-        num_heads = max(2, min(8, num_heads))
-        dropout = max(0.1, min(0.5, dropout))
-        
-        # 在指定设备上构建与评估(支持GPU)
-        model = GAT(
-            nfeat=1,
-            nhid=hidden_dim,
-            noutput=self.args.num_targets,
-            dropout=dropout,
-            nheads=num_heads,
-            alpha=0.2
-        ).to(self.eval_device)
-        optimizer = optim.Adam(model.parameters(), lr=lr, weight_decay=self.args.weight_decay)
-        
-        # 使用指定设备
-        rl_args = self.args
-        trainer = DataTrainer(model, rl_args, self.preprocessor, optimizer, logger=self.logger)
-        val_loss = self._short_evaluate(trainer)
-        
-        reward = 1.0 / (1.0 + val_loss)
-        if val_loss < self.best_val_loss:
-            reward += 0.5
-            self.best_val_loss = val_loss
-        
-        self.current_state = np.array([lr, hidden_dim, num_heads, dropout, val_loss], dtype=np.float32)
-        terminated = self.current_step >= self.max_steps
-        truncated = False
-        return self.current_state, float(reward), terminated, truncated, {}
-    
-    def _short_evaluate(self, trainer):
-        """
-        关键加速:只用极少 batch 做快速近似,保证一个 env.step() 在毫秒到秒级完成。
-        """
-        # 训练 1 个 batch、重复 2 次以产生可用梯度信号
-        for _ in range(2):
-            trainer.train_epoch(self.train_loader, self.adj, max_batches=1)
-        # 验证用 2 个 batch,降低方差
-        val_loss, _ = trainer.validate(self.val_loader, self.adj, max_batches=2)
-        return float(val_loss)
-    
-    def render(self):
-        if self.render_mode == 'human':
-            print(f"[RL] Step: {self.current_step}, Best Val Loss: {self.best_val_loss:.6f}")
-    
-    def close(self):
-        pass
-
-class TrainingCallback(BaseCallback):
-    def __init__(self, verbose=0, print_every=100):
-        super().__init__(verbose)
-        self.print_every = print_every
-    
-    def _on_step(self) -> bool:
-        # BaseCallback.logger 不是 logging.Logger;用 print 或 record。
-        if self.n_calls % self.print_every == 0:
-            # 某些版本下 self.locals 里没有 'rewards' 键,做个健壮保护
-            rew = None
-            try:
-                r = self.locals.get('rewards', None)
-                if r is not None:
-                    rew = float(r[0])
-            except Exception:
-                pass
-            print(f"[RL] timesteps={self.num_timesteps} calls={self.n_calls} reward={rew}")
-        return True
-
-class RLOptimizer:
-    def __init__(self, args, preprocessor, train_loader, val_loader, adj, logger):
-        self.args = args
-        self.preprocessor = preprocessor
-        self.train_loader = train_loader
-        self.val_loader = val_loader
-        self.adj = adj
-        self.logger = logger
-    
-    def optimize(self):
-        env = GATEnv(
-            self.preprocessor, self.train_loader, self.val_loader,
-            self.adj, self.args, self.logger
-        )
-        
-        # 关键:将 PPO rollout 和训练配置调小,避免一次 rollout 等太久
-        model = PPO(
-            "MlpPolicy",
-            env,
-            verbose=1,
-            learning_rate=3e-4,
-            n_steps=32,       # 原来 2048 -> 32
-            batch_size=32,    # 原来 64 -> 32
-            n_epochs=1,       # 原来 10 -> 1
-            gamma=0.99,
-            gae_lambda=0.95,
-            clip_range=0.2,
-            ent_coef=0.01,
-            device=self.args.device  # 使用指定设备(支持GPU)
-        )
-        
-        self.logger.info("开始训练强化学习智能体...")
-        callback = TrainingCallback(verbose=1, print_every=100)
-        model.learn(total_timesteps=self.args.rl_timesteps, callback=callback)
-        model.save("gat_ppo_agent")
-        
-        # 评估并选最优动作
-        self.logger.info("寻找最优超参数组合...")
-        best_reward = -1.0
-        best_action = None
-        eval_env = GATEnv(
-            self.preprocessor, self.train_loader, self.val_loader,
-            self.adj, self.args, self.logger
-        )
-        for _ in range(self.args.rl_eval_episodes):
-            obs, _ = eval_env.reset()
-            action, _ = model.predict(obs, deterministic=True)
-            _, reward, _, _, _ = eval_env.step(action)
-            if reward > best_reward:
-                best_reward = reward
-                best_action = action
-        
-        best_hparams = {
-            'lr': float(best_action[0]),
-            'hidden_dim': int(round(float(best_action[1]))),
-            'num_heads': int(round(float(best_action[2]))),
-            'dropout': float(best_action[3])
-        }
-        self.logger.info(f"\n最优超参数: {best_hparams}")
-        return best_hparams

BIN
models/causal-inference/targets_scaler.joblib