Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
wandb/
*.csv

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may not be great to ignore all csv and text files. While, yes, we do not submit data, typically, important information can be in this format sometimes. The preferred pattern is to ignore all txt, csv files in some nominated subfolder (e.g. where you keep your logs).

*.txt
261 changes: 261 additions & 0 deletions KRVI_algo_final.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
from typing import Any, ClassVar, Optional, TypeVar, Union, Callable
import numpy as np
import torch
import torch.nn as nn
import gymnasium as gym
from gymnasium import spaces
import botorch
from botorch.models import SingleTaskGP
from botorch.fit import fit_gpytorch_mll
import gpytorch
from botorch.models.transforms.outcome import Standardize
from gpytorch.kernels import ScaleKernel, RBFKernel
import time
import warnings
warnings.filterwarnings("ignore")
import os
import csv
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print('device',device)




def preprocess_state(state):

if isinstance(state, dict):
# Extract the observation from the dictionary
state = state['observation']

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All other returns are np.ndarray but this doesn't have to be? return type is inconsistent.

elif isinstance(state, tuple):
# Convert the tuple to a numpy array
state = np.array(state)
elif isinstance(state, (int, float)):
# Convert scalar to a 1D numpy array
state = np.array([state])
elif isinstance(state, np.ndarray):
# Ensure the state is a numpy array
state = state

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pass is preferred

else:
raise ValueError(f"Unsupported state type: {type(state)}")
if len(state.shape) > 1:
state = state.flatten()

return state


class KRVI:
def __init__(
self,
kernel: Callable,
env: Union[gym.Env, str],
beta: float,
horizon: int,
action_transformation: Callable,
len_scale: float = 0.1,
noise_reg: float = 0.5,
optim_botorch: int = 0,
optimal_V: Optional[np.ndarray] = None,
logging: Optional[str] = None,
verbose: int = 0,
seed: Optional[int] = None,
) -> None:
self.kernel = kernel
self.env = gym.make(env) if isinstance(env, str) else env
self.beta = beta
self.len_scale = len_scale
self.noise_reg = noise_reg
self.horizon = horizon
self.logging = logging
self.verbose = verbose
self.seed = seed
self.optim_botorch = optim_botorch
self.optimal_V = optimal_V

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand this is optional but if it's only used for computing regret, it should not be here

self.action_transformation = action_transformation
# File paths
self.csv_file = 'krvi_metrics.csv'
self.config_file = 'config.txt'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the txt file you're ignoring then it shouldn't be ignored.


np.random.seed(self.seed)
torch.manual_seed(self.seed)
torch.cuda.manual_seed(self.seed)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if self.logging:
with open(self.config_file, mode='w') as f:
f.write(f"beta={self.beta}\n")
f.write(f"len_scale={self.len_scale}\n")
f.write(f"noise_reg={self.noise_reg}\n")
f.write(f"horizon={self.horizon}\n")
f.write(f"seed={self.seed}\n")
f.write(f"optim_botorch={self.optim_botorch}\n")

# Write headers to the metrics CSV file if it doesn't exist
if not os.path.exists(self.csv_file):
with open(self.csv_file, mode='w', newline='') as f:
print('hey')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the information that this lien is conveying?

writer = csv.writer(f)
writer.writerow(['Episode', 'Reward', 'Cumulative Returns']) # Column headers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove whitespace





def train(self, T: int):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is very long. It's usually a bad pattern. Can you extract some private methods? A good rule of thumb is that wherever you have a comment header in the body e.g. # Execute episode you should instead have a private method self._execute_episode instead. Aim to have no more than 10 lines per method like this.


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whitespace

action_space = np.arange(self.env.action_space.n) # Assuming discrete action space

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whitespace (final time - please remove throughout)


# Arrays to store episode data
all_states = []
all_actions = []
all_rewards = []
Qt= [None] * self.horizon
cumulative_returns = []


for episode in range(T):
if self.verbose > 0:
print(f'Episode {episode}')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ok to print things, but it's better to log them. On top of the CSV all std output can be captured to a file (another file).

# Update Q-values from previous episodes
if episode > 0:
for h in reversed(range(len(all_states[-1]))):
X_states = []
X_actions = []
y_values = []

for i in range(episode):
if h < len(all_states[i]):
X_states.append(all_states[i][h])
X_actions.append(all_actions[i][h])

if h < len(all_states[i]) - 1:
next_state = all_states[i][h + 1]
actions_batch = np.array([self.action_transformation(action) for action in action_space])
states_expanded = np.tile(next_state, (len(action_space), 1))
max_q_value = 0
if Qt[h + 1]:
max_q_value = np.max(
self.predict_with_gp(Qt[h + 1], states_expanded, actions_batch)[0]
)
Qnext = max_q_value
else:
Qnext = 0

y_values.append(all_rewards[i][h] + Qnext)

if X_states:
X = np.column_stack((X_states, X_actions))
y = np.array(y_values)
Qt[h] = self.GP_regression_torch(X, y)


# Execute episode
# Initialize arrays for the current episode
episode_states = []
episode_actions = []
episode_rewards = []

initial_state, info = self.env.reset()
state= preprocess_state(initial_state)



for h in range(self.horizon):

if Qt[h]: # Ensure a model is available for the current step

# Prepare inputs for batched prediction
states_batch = np.tile(state, (len(action_space), 1))
actions_batch = np.array([self.action_transformation(action) for action in action_space])
# Predict Q-values for all actions in a single batch
q_values = self.predict_with_gp(Qt[h], states_batch, actions_batch)[0]

else:
# Default Q-values if no model is available
q_values = np.zeros(len(action_space))

# Select action with the highest Q-value

action = action_space[np.argmax(q_values)]
next_state, reward, done, truncated , info = self.env.step(action)
next_state = preprocess_state(next_state) # Convert to numpy array


episode_states.append(state)
action= self.action_transformation(action)
episode_actions.append(action)
episode_rewards.append(reward)
# If done or truncated, break the loop early
if done or truncated:
break

state = next_state


all_states.append(np.array(episode_states))
all_actions.append(np.array(episode_actions))
all_rewards.append(np.array(episode_rewards))


episode_cum_rewards = np.sum(episode_rewards)
cumulative_returns.append(episode_cum_rewards)


if self.logging:
# Log to CSV file
with open(self.csv_file, mode='a', newline='') as f:
writer = csv.writer(f)
writer.writerow([episode, episode_cum_rewards, sum(cumulative_returns)])

def GP_regression_torch(self, X, y): #I removed normalization
"""
Gaussian Process regression using PyTorch.

:param X: Input tensor of shape (n_samples, n_features)
:param y: Target tensor of shape (n_samples,)
:return: Trained GP model
"""
# Ensure inputs are torch tensors and use double precision
X = torch.tensor(X, dtype=torch.float64,device=device)
y = torch.tensor(y, dtype=torch.float64,device=device)

model = SingleTaskGP(train_X=X,train_Y= y.unsqueeze(-1).to(device)) #,outcome_transform=Standardize(m=1)) # GP expects (n_samples, 1) for targets
model.covar_module = ScaleKernel( self.kernel
# RBFKernel()
).to(device)


# Set and freeze the length scale
model.covar_module.base_kernel.lengthscale = torch.tensor(
[self.len_scale], dtype=torch.float64, device=device
)

model.covar_module.base_kernel.raw_lengthscale.requires_grad = False

# Set and freeze the noise
model.likelihood.noise = torch.tensor([self.noise_reg], dtype=torch.float64, device=device)
if self.optim_botorch == 0:
model.likelihood.raw_noise.requires_grad = False

mll = gpytorch.mlls.ExactMarginalLogLikelihood(model.likelihood, model).to(device)


fit_gpytorch_mll(mll)


return model

def predict_with_gp(self, model, states_batch, actions_batch):

X_combined = np.hstack((states_batch, actions_batch)) # Shape: (batch_size, 2)
X_combined = torch.tensor(X_combined, dtype=torch.float64, device=device)
# Make predictions
model.eval()
with torch.no_grad():
posterior = model.posterior(X_combined)
mean = posterior.mean.squeeze(-1).cpu().numpy() # Shape: (batch_size,)
std_dev = posterior.variance.sqrt().squeeze(-1).cpu().numpy() # Shape: (batch_size,)
# Compute mean + beta * std_dev for each batch element
acquisition_values = mean + self.beta * std_dev
return acquisition_values, mean, std_dev



65 changes: 65 additions & 0 deletions test_KRVI_algo_final.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from FrozenLakeStateWrapper import FrozenLake2DStateWrapper
import KRVI_algo_final
from KRVI_algo_final import KRVI
import argparse
import gymnasium as gym
from gpytorch.kernels import ScaleKernel, RBFKernel
import numpy as np
def action_transformation(action_index):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space

action_map = {
0: np.array([-1, 0]), # Left
1: np.array([0, -1]), # Down
2: np.array([1, 0]), # Right
3: np.array([0, 1]) # Up
}
return action_map.get(action_index, np.array([0, 0])) # Default to [0, 0] if invalid index

# Example usage
if __name__ == "__main__":


parser = argparse.ArgumentParser(description="Run KRVI Algorithm")
# Adding arguments for user input
parser.add_argument("--beta", type=float, default=0.1, help="UCB coefficient")
parser.add_argument("--horizon", type=int, default=100, help="Horizon length")
parser.add_argument("--len_scale", type=float, default=0.1, help="Length scale for GP kernel")
parser.add_argument("--noise_reg", type=float, default=0.1, help="Noise regularization for GP")
parser.add_argument("--env", type=str, default="FrozenLake-v1", help="Environment name")
parser.add_argument("--logging", type=str, default="True", help="logging metrics")
parser.add_argument("--verbose", type=int, default=1, help="Verbosity level (0: silent, 1: info)")
parser.add_argument("--iterations", type=int, default=2000, help="Number of training iterations (T)")
parser.add_argument("--seed", type=int, default=0, help="random seed")
parser.add_argument("--optim_botorch", type= int, default = 1, help ='turn on hyperparm optimization by botorch')



args = parser.parse_args()

env=gym.make('FrozenLake-v1', desc=None, map_name="4x4", is_slippery=False)
env = FrozenLake2DStateWrapper(env, rescale=True)
optimal_V= None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I'm now convinced this is not needed, we shouldn't make it part of the algorithm? Which other predictive model or RL algorithm that you have seen takes the ground truth as part of the constructor, optional or not?


krvi = KRVI(
kernel= RBFKernel(),
env= env,
beta=args.beta,
horizon=args.horizon,
action_transformation = action_transformation,
len_scale=args.len_scale,
noise_reg=args.noise_reg,
optim_botorch=args.optim_botorch,
optimal_V = optimal_V,
logging=args.logging,
verbose=args.verbose,
seed= args.seed
)

krvi.train(T= args.iterations)