I am having the Actor network and PPoagent with continuous action space, please review the complete provided code. and address any implementation issues were existing or not in the given structure. and also address all the logical immplementation error, improper implementation, improper functions used, improper computation technique used finally address the 'log_prob  were properly computed or not and check its expression.

class Actor(torch.nn.Module):
    def __init__(self, gnn_model):
        super(Actor, self).__init__()
        self.gnn = gnn_model
        # Bounds are converted to tensors for ease of calculation
        self.bounds_low = torch.tensor([0.18e-6, 0.18e-6, 0.18e-6, 0.18e-6, 0.18e-6, 
                                        0.5e-6, 0.5e-6, 0.5e-6, 0.5e-6, 0.5e-6, 
                                        15e-6, 0.1e-12, 0.8], dtype=torch.float32)
                                        
        self.bounds_high = torch.tensor([0.2e-6, 0.2e-6, 0.2e-6, 0.2e-6, 0.2e-6, 
                                         50e-6, 50e-6, 50e-6, 50e-6, 50e-6, 
                                         30e-6, 10e-12, 1.4], dtype=torch.float32)

    def forward(self, state):
        node_features_tensor, _, edge_index = state
        processed_features = self.gnn(node_features_tensor, edge_index)
        
        # Specific (row, column) indices for action values, converted to 0-based indexing
        action_indices = [
            (10, 19), (16, 19), (5, 19), (3, 19), (0, 19), (10, 18),
            (16, 18), (5, 18), (3, 18), (0, 18), (17, 20), (18, 21), (19, 22)
        ]
        
        # For gathering specific indices, we first convert action_indices to a tensor format that can be used with gather or indexing
        action_indices_tensor = torch.tensor(action_indices, dtype=torch.long).t()
        selected_features = processed_features[action_indices_tensor[0], action_indices_tensor[1]]
        
        print("selected_features", selected_features)
        
        # Perform min-max scaling to map selected features to the range defined by bounds
        #min_values = torch.min(selected_features, dim=0)[0]
        #max_values = torch.max(selected_features, dim=0)[0]
        #scaled_action = (selected_features - min_values) / (max_values - min_values)
        
        # Use sigmoid or tanh followed by scaling to the specific bounds
        # This assumes outputs are approximately normalized or that such a transformation is desirable.
        #scaled_action = torch.sigmoid(selected_features)  # Sigmoid maps to (0, 1)
        
        # Alternatively, for a wider range before scaling, you could use:
        scaled_action = (torch.tanh(selected_features) + 1) / 2  # Maps outputs to (0, 1)
        
        # Scale the min-max scaled features to fit within the specified bounds
        scaled_actions = self.bounds_low + (self.bounds_high - self.bounds_low) * scaled_action

        return scaled_actions

class PPOAgent:
    def __init__(self, gnn_model, state_dim, action_space, lr_actor, lr_critic, gamma, gae_lambda, epsilon, policy_clip, epochs, entropy_coef):
        self.gamma = gamma
        self.gae_lambda = gae_lambda
        self.epsilon = epsilon
        self.policy_clip = policy_clip
        self.epochs = epochs
        self.entropy_coef = entropy_coef  # Dynamic entropy coefficient
        
        self.rewards_history = []
        self.dynamic_factor_base = 0.05  # Base value for the dynamic factor
        
        self.actor = Actor(gnn_model)
        self.critic = Critic()
        self.optimizer_actor = optim.Adam(self.actor.parameters(), lr=lr_actor)
        self.optimizer_critic = optim.Adam(self.critic.parameters(), lr=lr_critic)
        self.action_space = action_space  # Assume continuous
        
        
    def update_rewards_history(self, reward):
        self.rewards_history.append(reward)
        # Limit the history size to keep only the most recent rewards
        if len(self.rewards_history) > 100:
            self.rewards_history = self.rewards_history[-100:]
    
    def calculate_dynamic_factor(self):
        # Adjust exploration based on recent reward history
        if len(self.rewards_history) >= 100:
            recent_rewards = self.rewards_history[-10:]  # Last 10 rewards
            earlier_rewards = self.rewards_history[-100:-10]  # Previous 90 out of 100 rewards
            
            recent_avg_reward = np.mean(recent_rewards)
            earlier_avg_reward = np.mean(earlier_rewards)
            
            # Increase exploration if recent rewards are not significantly better than earlier
            if recent_avg_reward <= earlier_avg_reward * 1.1:  # Example threshold
                return self.dynamic_factor_base * 2  # Example: increasing exploration
            else:
                return self.dynamic_factor_base  # Sticking with base exploration level
        else:
            return self.dynamic_factor_base  # Use base dynamic_factor if not enough data

    def select_action(self, state, performance_metrics):
        dynamic_factor = self.calculate_dynamic_factor()        
        
        action_probs = self.actor(state)        

        # Adjust epsilon based on the spread of bounds
        bounds_range = self.actor.bounds_high - self.actor.bounds_low
        epsilon = (1e-4 + bounds_range * dynamic_factor).clamp(min=0.01)
        
        # To calculate variances, ensure actor yields predictions
        variances = action_probs.var(dim=0, keepdim=True).expand(action_probs.shape[0]) + epsilon        

        cov_mat = torch.diag(variances)  # Form an [13, 13] covariance matrix with variances on the diagonal

        # Define the Multivariate Normal distribution
        dist = torch.distributions.MultivariateNormal(action_probs, cov_mat)
        
        # Sample an action
        raw_action = dist.sample()         
        
        # Scale the raw sampled action to fit within the specified bounds
        #scaled_action = self.actor.bounds_low + (torch.sigmoid(raw_action) * (bounds_range))     
        
        # Adjusted scaling using tanh       
        scaled_action = ((torch.tanh(raw_action) + 1) / 2) * (bounds_range) + self.actor.bounds_low
        
        log_prob = dist.log_prob(raw_action) - torch.log(bounds_range).sum()  # Compute log probability with raw action
        
        return scaled_action.detach().numpy().squeeze(), log_prob.item(), performance_metrics