Skip to content

Commit

Permalink
archived ver for icml
Browse files Browse the repository at this point in the history
  • Loading branch information
RoyalSkye committed Feb 7, 2023
1 parent 56e60f7 commit ca913ec
Show file tree
Hide file tree
Showing 27 changed files with 486 additions and 223 deletions.
121 changes: 77 additions & 44 deletions EAS/run_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from source.eas_tab import run_eas_tab
from source.tsp.model import TSPModel as TSP_model
from source.tsp.read_data import read_instance_pkl as TSP_read_instance_pkl
from source.tsp.read_data import read_instance_tsp
from source.tsp.utilities import augment_and_repeat_episode_data as TSP_augment_and_repeat_episode_data
from source.tsp.utilities import get_episode_data as TSP_get_episode_data

Expand All @@ -34,17 +35,18 @@ def get_config():
parser.add_argument('-problem', default="TSP", type=str, choices=['TSP', 'CVRP'])
parser.add_argument('-method', default="eas-emb", type=str, choices=['eas-emb', 'eas-lay', 'eas-tab'], help="EAS method")
parser.add_argument('-model_path', default="../pretrained/pomo_pretrained/checkpoint-30500.pt", type=str, help="Path of the trained model weights")
parser.add_argument('-instances_path', default="../data/TSP/Size/tsp100_gaussian.pkl", type=str, help="Path of the instances")
parser.add_argument('-sol_path', default="../data/TSP/Size/opt_tsp100_uniform.pkl", type=str, help="Path of the optimal sol")
parser.add_argument('-num_instances', default=10000, type=int, help="Maximum number of instances that should be solved")
parser.add_argument('-instances_path', default="../data/TSP/Size_Distribution/tsp200_rotation.pkl", type=str, help="Path of the instances")
parser.add_argument('-sol_path', default="../data/TSP/Size_Distribution/concorde/tsp200_rotationoffset0n1000-concorde.pkl", type=str, help="Path of the optimal sol")
parser.add_argument('-num_instances', default=1000, type=int, help="Maximum number of instances that should be solved")
parser.add_argument('-instances_offset', default=0, type=int)
parser.add_argument('-round_distances', default=False, action='store_true', help="Round distances to the nearest integer. Required to solve .vrp instances")
parser.add_argument('-loc_scaler', default=1.0, type=float, help="The scaler of coordinates to valid range [0, 1]")
parser.add_argument('-max_iter', default=200, type=int, help="Maximum number of EAS iterations")
parser.add_argument('-max_runtime', default=100000, type=int, help="Maximum runtime of EAS per batch in seconds")
parser.add_argument('-batch_size', default=150, type=int) # Set to 1 for single instance search
parser.add_argument('-p_runs', default=1, type=int) # If batch_size is 1, set this to > 1 to do multiple runs for the instance in parallel
parser.add_argument('-output_path', default="EAS_results", type=str)
parser.add_argument('-norm', default="none", choices=['instance', 'batch', 'none'], type=str)
parser.add_argument('-norm', default="batch_no_track", choices=['instance', 'batch', 'batch_no_track', 'none'], type=str)
parser.add_argument('-gpu_id', default=2, type=int)
parser.add_argument('-seed', default=2023, type=int, help="random seed")

Expand Down Expand Up @@ -86,34 +88,46 @@ def read_instance_data(config):
instance_data_scaled = instance_data[0], instance_data[1]

else:
# Read in .vrp instance(s) that have the VRPLIB format. In this case the distances between customers
# should be rounded.

# Read in .vrp instance(s) that have the VRPLIB format. In this case the distances between customers should be rounded.
assert config.round_distances

if config.instances_path.endswith(".vrp"):
if config.instances_path.endswith(".vrp") or config.instances_path.endswith(".tsp"):
# Read in a single instance
instance_file_paths = [config.instances_path]
elif os.path.isdir(config.instances_path):
# or all instances in the given directory.
instance_file_paths = [os.path.join(config.instances_path, f) for f in
sorted(os.listdir(config.instances_path))]
instance_file_paths = instance_file_paths[
config.instances_offset:config.instances_offset + config.num_instances]

# Read in the first instance only to determine the problem_size
_, locations, _, _ = read_instance_vrp(instance_file_paths[0])
problem_size = locations.shape[1] - 1
else:
print("Not supported for Dir now.")
raise NotImplementedError

# Prepare empty numpy array to store instance data
instance_data_scaled = (np.zeros((len(instance_file_paths), locations.shape[1], 2)),
np.zeros((len(instance_file_paths), locations.shape[1] - 1)))
# elif os.path.isdir(config.instances_path):
# # or all instances in the given directory.
# instance_file_paths = [os.path.join(config.instances_path, f) for f in sorted(os.listdir(config.instances_path))]
# instance_file_paths = instance_file_paths[config.instances_offset:config.instances_offset + config.num_instances]

# Read in all instances
for idx, file in enumerate(instance_file_paths):
# logging.info(f'Instance: {os.path.split(file)[-1]}')
original_locations, locations, demand, capacity = read_instance_vrp(file)
instance_data_scaled[0][idx], instance_data_scaled[1][idx] = locations, demand / capacity
# Read in the first instance only to determine the problem_size
if config.instances_path.endswith(".vrp"):
config.loc_scaler = 1000
_, locations, _, _ = read_instance_vrp(instance_file_paths[0])
problem_size = locations.shape[1] - 1
# Prepare empty numpy array to store instance data
instance_data_scaled = (np.zeros((len(instance_file_paths), locations.shape[1], 2)),
np.zeros((len(instance_file_paths), locations.shape[1] - 1)))
# Read in all instances
for idx, file in enumerate(instance_file_paths):
# logging.info(f'Instance: {os.path.split(file)[-1]}')
original_locations, locations, demand, capacity = read_instance_vrp(file)
instance_data_scaled[0][idx], instance_data_scaled[1][idx] = locations, demand / capacity
elif config.instances_path.endswith(".tsp"):
_, locations, _ = read_instance_tsp(instance_file_paths[0])
problem_size = locations.shape[1]
# Prepare empty numpy array to store instance data
instance_data_scaled = (np.zeros((len(instance_file_paths), locations.shape[1], 2)), None)
# Read in all instances
for idx, file in enumerate(instance_file_paths):
# logging.info(f'Instance: {os.path.split(file)[-1]}')
original_locations, locations, loc_scaler = read_instance_tsp(file)
instance_data_scaled[0][idx] = locations
config.loc_scaler = loc_scaler
config.original_loc = torch.Tensor(original_locations)

return instance_data_scaled, problem_size

Expand Down Expand Up @@ -195,26 +209,45 @@ def search(run_id, config):

if config.problem == "CVRP" and not config.instances_path.endswith(".pkl"):
# For instances with the CVRPLIB format the costs need to be adjusted to match the original coordinates
perf = np.round(perf * 1000).astype('int')
perf = np.round(perf * config.loc_scaler).astype('int')
elif config.problem == "TSP" and not config.instances_path.endswith(".pkl"):
# For instances with the TSPLIB format the costs need to be adjusted to match the original coordinates
perf = np.round(perf * config.loc_scaler).astype('int')
# [!] double-check, we regard best_obj as our final result
# since the current implementation is inaccurate (e.g, it may even outperform the obj of optimal sol)
best_sol, best_obj = best_solutions.tolist()[0], 0
for i in range(problem_size):
if i == problem_size - 1:
best_obj += ((config.original_loc[0, best_sol[i]] - config.original_loc[0, best_sol[0]]) ** 2).sum().sqrt().item()
break
best_obj += torch.round(((config.original_loc[0, best_sol[i]] - config.original_loc[0, best_sol[i+1]]) ** 2).sum().sqrt()).item()
print(">> best_obj {} = [{}]".format(best_obj, np.round(best_obj).astype('int')))

# compute gaps
with open(config.sol_path, 'rb') as f:
opt_sol = pickle.load(f)[config.instances_offset: config.instances_offset+config.num_instances] # [(obj, route), ...]
print(">> Load {} optimal solutions ({}) from {}".format(len(opt_sol), type(opt_sol), config.sol_path))
assert len(opt_sol) == len(perf)
gap_list = [(perf[i] - opt_sol[i][0]) / opt_sol[i][0] * 100 for i in range(len(perf))]

logging.info(f"EAS Method: {config.method}, Seed: {config.seed}")
logging.info(f"Mean costs: {np.mean(perf)}")
logging.info(f"Mean gaps: {sum(gap_list)/len(gap_list)}%")
logging.info(f"Runtime: {runtime}s")
logging.info("MEM: " + str(cutorch.max_memory_reserved(config.gpu_id) / 1024 / 1024) + "MB")
logging.info(f"Num. instances: {len(perf)}")

res = {"EAS_score_list": perf.tolist(), "EAS_gap_list": gap_list}

pickle.dump(res, open(os.path.join(config.output_path, "./Results_{}.pkl".format(config.method)), 'wb'), pickle.HIGHEST_PROTOCOL)
# pickle.dump(res, open("./Results_{}.pkl".format(config.method), 'wb'), pickle.HIGHEST_PROTOCOL)
if config.instances_path.endswith(".pkl"):
with open(config.sol_path, 'rb') as f:
opt_sol = pickle.load(f)[config.instances_offset: config.instances_offset+config.num_instances] # [(obj, route), ...]
print(">> Load {} optimal solutions ({}) from {}".format(len(opt_sol), type(opt_sol), config.sol_path))
assert len(opt_sol) == len(perf)
gap_list = [(perf[i] - opt_sol[i][0]) / opt_sol[i][0] * 100 for i in range(len(perf))]

logging.info(f"EAS Method: {config.method}, Seed: {config.seed}")
logging.info(f"Mean costs: {np.mean(perf)}")
logging.info(f"Mean gaps: {sum(gap_list)/len(gap_list)}%")
logging.info(f"Runtime: {runtime}s")
logging.info("MEM: " + str(cutorch.max_memory_reserved(config.gpu_id) / 1024 / 1024) + "MB")
logging.info(f"Num. instances: {len(perf)}")

res = {"EAS_score_list": perf.tolist(), "EAS_gap_list": gap_list}

pickle.dump(res, open(os.path.join(config.output_path, "./Results_{}.pkl".format(config.method)), 'wb'), pickle.HIGHEST_PROTOCOL)
else:
logging.info(f"EAS Method: {config.method}, Seed: {config.seed}")
logging.info(f"Mean costs: {np.mean(perf)}")
logging.info(f"Runtime: {runtime}s")
logging.info("MEM: " + str(cutorch.max_memory_reserved(config.gpu_id) / 1024 / 1024) + "MB")
logging.info(f"Num. instances: {len(perf)}")
print(">> Solving {}, with sol {}".format(config.instances_path, perf))


def seed_everything(seed=2022):
Expand Down
5 changes: 3 additions & 2 deletions EAS/source/cvrp/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def move_to(self, selected_idx_mat):

class GROUP_ENVIRONMENT:

def __init__(self, data, problem_size, round_distances):
def __init__(self, data, problem_size, round_distances, loc_scaler=1.0):
depot_xy = data[0]
# depot_xy.shape = (batch, 1, 2)
node_xy = data[1]
Expand All @@ -119,6 +119,7 @@ def __init__(self, data, problem_size, round_distances):
self.group_state = None
self.problem_size = problem_size
self.round_distances = round_distances
self.loc_scaler = loc_scaler

all_node_xy = torch.cat((depot_xy, node_xy), dim=1)
# shape = (batch, problem+1, 2)
Expand Down Expand Up @@ -163,7 +164,7 @@ def _get_travel_distance(self):
# size = (batch, group, selected_count)

if self.round_distances:
segment_lengths = torch.round(segment_lengths * 1000) / 1000
segment_lengths = torch.round(segment_lengths * self.loc_scaler) / self.loc_scaler

travel_distances = segment_lengths.sum(2)
# size = (batch, group)
Expand Down
11 changes: 9 additions & 2 deletions EAS/source/cvrp/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,27 +289,34 @@ def __init__(self, **model_params):
embedding_dim = model_params['embedding_dim']
if model_params["norm"] == "batch":
self.norm = nn.BatchNorm1d(embedding_dim, affine=True, track_running_stats=True)
elif model_params["norm"] == "batch_no_track":
self.norm = nn.BatchNorm1d(embedding_dim, affine=True, track_running_stats=False)
elif model_params["norm"] == "instance":
self.norm = nn.InstanceNorm1d(embedding_dim, affine=True, track_running_stats=False)
elif model_params["norm"] == "rezero":
self.norm = torch.nn.Parameter(torch.Tensor([0.]), requires_grad=True)
else:
self.norm = None

def forward(self, input1, input2):
# input.shape: (batch, problem, embedding)
added = input1 + input2
if isinstance(self.norm, nn.InstanceNorm1d):
added = input1 + input2
transposed = added.transpose(1, 2)
# shape: (batch, embedding, problem)
normalized = self.norm(transposed)
# shape: (batch, embedding, problem)
back_trans = normalized.transpose(1, 2)
# shape: (batch, problem, embedding)
elif isinstance(self.norm, nn.BatchNorm1d):
added = input1 + input2
batch_s, problem_s, embedding_dim = input1.size(0), input1.size(1), input1.size(2)
normalized = self.norm(added.reshape(batch_s * problem_s, embedding_dim))
back_trans = normalized.reshape(batch_s, problem_s, embedding_dim)
elif isinstance(self.norm, nn.Parameter):
back_trans = input1 + self.norm * input2
else:
back_trans = added
back_trans = input1 + input2

return back_trans

Expand Down
8 changes: 8 additions & 0 deletions EAS/source/cvrp/read_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,12 @@ def read_instance_vrp(path):
locations = original_locations / 1000 # Scale location coordinates to [0, 1]
demand = demand[1:, 1:].reshape((1, -1))

# original_locations: unnormalized with shape of (1, n+1, 2)
# locations: normalized to [0, 1] with shape of (1, n+1, 2)
# demand: unnormalized with shape of (1, n)
# capacity: with shape of (1)
return original_locations, locations, demand, capacity


if __name__ == "__main__":
read_instance_vrp("../X-n101-k25.vrp")
2 changes: 1 addition & 1 deletion EAS/source/eas_emb.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def run_eas_emb(model, instance_data, problem_size, config, get_episode_data_fn,

with torch.no_grad():
aug_data = augment_and_repeat_episode_data_fn(episode_data, problem_size, p_runs, AUG_S)
env = GROUP_ENVIRONMENT(aug_data, problem_size, config.round_distances)
env = GROUP_ENVIRONMENT(aug_data, problem_size, config.round_distances, loc_scaler=config.loc_scaler)
group_state, reward, done = env.reset(group_size=group_s)
# model.reset(group_state) # Generate the embeddings (i.e., k, v, and single_head_key)
model.pre_forward(group_state)
Expand Down
2 changes: 1 addition & 1 deletion EAS/source/eas_lay.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def run_eas_lay(model, instance_data, problem_size, config, get_episode_data_fn,

with torch.no_grad():
aug_data = augment_and_repeat_episode_data_fn(episode_data, problem_size, p_runs, AUG_S)
env = GROUP_ENVIRONMENT(aug_data, problem_size, config.round_distances)
env = GROUP_ENVIRONMENT(aug_data, problem_size, config.round_distances, loc_scaler=config.loc_scaler)
# Replace the decoder of the loaded model with the modified decoder with added layers
model_modified = replace_decoder(model, batch_s, original_decoder_state_dict, config.problem, config.model_params).cuda()
group_state, reward, done = env.reset(group_size=group_s)
Expand Down
4 changes: 2 additions & 2 deletions EAS/source/eas_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def run_eas_tab(model, instance_data, problem_size, config, get_episode_data_fn,
with torch.no_grad():
# Augment instances and generate the embeddings using the encoder
aug_data = augment_and_repeat_episode_data_fn(episode_data, problem_size, p_runs, AUG_S)
env = GROUP_ENVIRONMENT(aug_data, problem_size, config.round_distances)
env = GROUP_ENVIRONMENT(aug_data, problem_size, config.round_distances, loc_scaler=config.loc_scaler)
group_state, reward, done = env.reset(group_size=group_s)
# model.reset(group_state) # Generate the embeddings
model.pre_forward(group_state)
Expand All @@ -60,7 +60,7 @@ def run_eas_tab(model, instance_data, problem_size, config, get_episode_data_fn,
###############################################
t_start = time.time()
for iter in range(config.max_iter):
env = GROUP_ENVIRONMENT(aug_data, problem_size, config.round_distances) # not necessary?
env = GROUP_ENVIRONMENT(aug_data, problem_size, config.round_distances, loc_scaler=config.loc_scaler) # not necessary?
group_state, reward, done = env.reset(group_size=group_s)

# First Move is given
Expand Down
6 changes: 5 additions & 1 deletion EAS/source/tsp/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def move_to(self, selected_idx_mat):

class GROUP_ENVIRONMENT:

def __init__(self, data, problem_size, round_distances):
def __init__(self, data, problem_size, round_distances, loc_scaler=1.0):
# seq.shape = (batch, TSP_SIZE, 2)

self.data = data
Expand All @@ -84,6 +84,7 @@ def __init__(self, data, problem_size, round_distances):
self.group_state = None
self.problem_size = problem_size
self.round_distances = round_distances
self.loc_scaler = loc_scaler

def reset(self, group_size):
self.group_s = group_size
Expand Down Expand Up @@ -118,6 +119,9 @@ def _get_group_travel_distance(self):
segment_lengths = ((ordered_seq - rolled_seq) ** 2).sum(3).sqrt()
# size = (batch, group, TSP_SIZE)

if self.round_distances:
segment_lengths = torch.round(segment_lengths * self.loc_scaler) / self.loc_scaler

group_travel_distances = segment_lengths.sum(2)
# size = (batch, group)
return group_travel_distances
34 changes: 21 additions & 13 deletions EAS/source/tsp/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,25 +262,33 @@ def __init__(self, **model_params):
embedding_dim = model_params['embedding_dim']
if model_params["norm"] == "batch":
self.norm = nn.BatchNorm1d(embedding_dim, affine=True, track_running_stats=True)
elif model_params["norm"] == "batch_no_track":
self.norm = nn.BatchNorm1d(embedding_dim, affine=True, track_running_stats=False)
elif model_params["norm"] == "instance":
self.norm = nn.InstanceNorm1d(embedding_dim, affine=True, track_running_stats=False)
elif model_params["norm"] == "rezero":
self.norm = torch.nn.Parameter(torch.Tensor([0.]), requires_grad=True)
else:
self.norm = None

def forward(self, input1, input2):
# input.shape: (batch, problem, embedding)

added = input1 + input2
# shape: (batch, problem, embedding)
if self.norm is None:
return added

transposed = added.transpose(1, 2)
# shape: (batch, embedding, problem)
normalized = self.norm(transposed)
# shape: (batch, embedding, problem)
back_trans = normalized.transpose(1, 2)
# shape: (batch, problem, embedding)
if isinstance(self.norm, nn.InstanceNorm1d):
added = input1 + input2
transposed = added.transpose(1, 2)
# shape: (batch, embedding, problem)
normalized = self.norm(transposed)
# shape: (batch, embedding, problem)
back_trans = normalized.transpose(1, 2)
# shape: (batch, problem, embedding)
elif isinstance(self.norm, nn.BatchNorm1d):
added = input1 + input2
batch, problem, embedding = added.size()
normalized = self.norm(added.reshape(batch * problem, embedding))
back_trans = normalized.reshape(batch, problem, embedding)
elif isinstance(self.norm, nn.Parameter):
back_trans = input1 + self.norm * input2
else:
back_trans = input1 + input2

return back_trans

Expand Down
Loading

0 comments on commit ca913ec

Please sign in to comment.