Skip to content

Commit

Permalink
add cvrp code
Browse files Browse the repository at this point in the history
  • Loading branch information
RoyalSkye committed Nov 19, 2022
1 parent aea860d commit 238cbc8
Show file tree
Hide file tree
Showing 25 changed files with 2,542 additions and 265 deletions.
34 changes: 11 additions & 23 deletions EAS/run_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pickle
import sys
import time
import math
import random

import numpy as np
Expand All @@ -28,28 +29,27 @@


def get_config():
# TODO: Check CVRP env.py
parser = argparse.ArgumentParser(description='Efficient Active Search')

parser.add_argument('-problem', default="TSP", type=str, choices=['TSP', 'CVRP'])
parser.add_argument('-method', default="eas-tab", type=str, choices=['eas-emb', 'eas-lay', 'eas-tab'], help="EAS method")
parser.add_argument('-model_path', default="../pretrained/checkpoint-50000.pt", type=str, help="Path of the trained model weights")
parser.add_argument('-instances_path', default="../data/TSP/Size/tsp100_uniform.pkl", type=str, help="Path of the instances")
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=30, type=int, help="Maximum number of instances that should be solved")
parser.add_argument('-num_instances', default=10000, 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('-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=30, type=int) # Set to 1 for single instance search
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('-gpu_id', default=0, type=int)
parser.add_argument('-gpu_id', default=2, type=int)
parser.add_argument('-seed', default=2023, type=int, help="random seed")

# EAS-Emb and EAS-Lay parameters
parser.add_argument('-param_lambda', default=0.012, type=float)
parser.add_argument('-param_lambda', default=0.0058, type=float)
parser.add_argument('-param_lr', default=0.0032, type=float)

# EAS-Tab parameters
Expand Down Expand Up @@ -81,21 +81,9 @@ def read_instance_data(config):
problem_size = instance_data[0].shape[1] - 1

# The vehicle capacity (here called demand_scaler) is hardcoded for these instances as follows
if problem_size == 20:
demand_scaler = 30
elif problem_size == 50:
demand_scaler = 40
elif problem_size == 100:
demand_scaler = 50
elif problem_size == 125:
demand_scaler = 55
elif problem_size == 150:
demand_scaler = 60
elif problem_size == 200:
demand_scaler = 70
else:
raise NotImplementedError
instance_data_scaled = instance_data[0], instance_data[1] / demand_scaler
# demand_scaler = math.ceil(30 + problem_size / 5) if problem_size >= 20 else 20
# instance_data_scaled = instance_data[0], instance_data[1] / demand_scaler # already done in fun(read_instance_pkl)
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
Expand Down
111 changes: 31 additions & 80 deletions EAS/source/cvrp/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ def __init__(self, **model_params):
# shape: (batch, problem+1, EMBEDDING_DIM)

def pre_forward(self, reset_state):
depot_xy = reset_state.depot_xy
# print(reset_state.data) # (batch, problem+1, 3)
depot_xy = reset_state.data[:, [0], 0:2]
# shape: (batch, 1, 2)
node_xy = reset_state.node_xy
node_xy = reset_state.data[:, 1:, 0:2]
# shape: (batch, problem, 2)
node_demand = reset_state.node_demand
node_demand = reset_state.data[:, 1:, 2]
# shape: (batch, problem)
node_xy_demand = torch.cat((node_xy, node_demand[:, :, None]), dim=2)
# shape: (batch, problem, 3)
Expand All @@ -29,52 +30,15 @@ def pre_forward(self, reset_state):
# shape: (batch, problem+1, embedding)
self.decoder.set_kv(self.encoded_nodes)

def forward(self, state):
batch_size = state.BATCH_IDX.size(0)
pomo_size = state.BATCH_IDX.size(1)


if state.selected_count == 0: # First Move, depot
selected = torch.zeros(size=(batch_size, pomo_size), dtype=torch.long)
prob = torch.ones(size=(batch_size, pomo_size))

# # Use Averaged encoded nodes for decoder input_1
# encoded_nodes_mean = self.encoded_nodes.mean(dim=1, keepdim=True)
# # shape: (batch, 1, embedding)
# self.decoder.set_q1(encoded_nodes_mean)

# # Use encoded_depot for decoder input_2
# encoded_first_node = self.encoded_nodes[:, [0], :]
# # shape: (batch, 1, embedding)
# self.decoder.set_q2(encoded_first_node)

elif state.selected_count == 1: # Second Move, POMO
selected = torch.arange(start=1, end=pomo_size+1)[None, :].expand(batch_size, pomo_size)
prob = torch.ones(size=(batch_size, pomo_size))

def forward(self, state, selected=None):
if selected is not None: # First Move - depot or Second Move - POMO
pass
else:
encoded_last_node = _get_encoding(self.encoded_nodes, state.current_node)
# shape: (batch, pomo, embedding)
probs = self.decoder(encoded_last_node, state.load, ninf_mask=state.ninf_mask)
probs = self.decoder(encoded_last_node, state.loaded, ninf_mask=state.ninf_mask)
# shape: (batch, pomo, problem+1)

if self.training or self.model_params['eval_type'] == 'softmax':
while True: # to fix pytorch.multinomial bug on selecting 0 probability elements
with torch.no_grad():
selected = probs.reshape(batch_size * pomo_size, -1).multinomial(1) \
.squeeze(dim=1).reshape(batch_size, pomo_size)
# shape: (batch, pomo)
prob = probs[state.BATCH_IDX, state.POMO_IDX, selected].reshape(batch_size, pomo_size)
# shape: (batch, pomo)
if (prob != 0).all():
break

else:
selected = probs.argmax(dim=2)
# shape: (batch, pomo)
prob = None # value not needed. Can be anything.

return selected, prob
return probs


def _get_encoding(encoded_nodes, node_index_to_pick):
Expand Down Expand Up @@ -141,9 +105,9 @@ def __init__(self, **model_params):
self.Wv = nn.Linear(embedding_dim, head_num * qkv_dim, bias=False)
self.multi_head_combine = nn.Linear(head_num * qkv_dim, embedding_dim)

self.add_n_normalization_1 = AddAndInstanceNormalization(**model_params)
self.add_n_normalization_1 = Add_And_Normalization_Module(**model_params)
self.feed_forward = FeedForward(**model_params)
self.add_n_normalization_2 = AddAndInstanceNormalization(**model_params)
self.add_n_normalization_2 = Add_And_Normalization_Module(**model_params)

def forward(self, input1):
# input1.shape: (batch, problem+1, embedding)
Expand Down Expand Up @@ -319,50 +283,37 @@ def multi_head_attention(q, k, v, rank2_ninf_mask=None, rank3_ninf_mask=None):
return out_concat


class AddAndInstanceNormalization(nn.Module):
class Add_And_Normalization_Module(nn.Module):
def __init__(self, **model_params):
super().__init__()
embedding_dim = model_params['embedding_dim']
self.norm = nn.InstanceNorm1d(embedding_dim, affine=True, track_running_stats=False)
if model_params["norm"] == "batch":
self.norm = nn.BatchNorm1d(embedding_dim, affine=True, track_running_stats=True)
elif model_params["norm"] == "instance":
self.norm = nn.InstanceNorm1d(embedding_dim, affine=True, track_running_stats=False)
else:
self.norm = None

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

added = input1 + input2
# shape: (batch, problem, embedding)

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):
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):
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)
else:
back_trans = added

return back_trans


class AddAndBatchNormalization(nn.Module):
def __init__(self, **model_params):
super().__init__()
embedding_dim = model_params['embedding_dim']
self.norm_by_EMB = nn.BatchNorm1d(embedding_dim, affine=True)
# 'Funny' Batch_Norm, as it will normalized by EMB dim

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

batch_s = input1.size(0)
problem_s = input1.size(1)
embedding_dim = input1.size(2)

added = input1 + input2
normalized = self.norm_by_EMB(added.reshape(batch_s * problem_s, embedding_dim))
back_trans = normalized.reshape(batch_s, problem_s, embedding_dim)

return back_trans

class FeedForward(nn.Module):
def __init__(self, **model_params):
super().__init__()
Expand Down
16 changes: 9 additions & 7 deletions EAS/source/cvrp/read_data.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import numpy as np
import math
import pickle
import numpy as np


def read_instance_pkl(instances_path):
Expand All @@ -9,13 +10,14 @@ def read_instance_pkl(instances_path):
coord = []
demands = []
for instance_data in instances_data:
coord.append([instance_data[0]])
coord[-1].extend(instance_data[1])
coord[-1] = np.array(coord[-1])
demands.append(np.array(instance_data[2]))
coord.append(instance_data[0]) # depot
coord[-1].extend(instance_data[1]) # nodes
coord[-1] = np.array(coord[-1]) # (1 + problem_size, 2)
demands.append(np.array(instance_data[2]) / instance_data[3])

coord = np.stack(coord) # (dataset_size, problem_size+1, 2)
demands = np.stack(demands) # (dataset_size, problem_size)

coord = np.stack(coord)
demands = np.stack(demands)
return coord, demands


Expand Down
4 changes: 2 additions & 2 deletions EAS/source/eas_emb.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def run_eas_emb(model, instance_data, problem_size, config, get_episode_data_fn,
if config.problem == "CVRP":
# First Move is given
first_action = LongTensor(np.zeros((batch_s, group_s))) # start from node_0-depot
# TODO: CVRP model need to do something?
# model(group_state, selected=first_action) # do nothing for CVRP
group_state, reward, done = env.step(first_action)
solutions.append(first_action.unsqueeze(2))
step += 1
Expand All @@ -73,7 +73,7 @@ def run_eas_emb(model, instance_data, problem_size, config, get_episode_data_fn,
second_action = LongTensor(np.arange(group_s) % problem_size)[None, :].expand(batch_s, group_s).clone()
if iter > 0:
second_action[:, -1] = incumbent_solutions_expanded[:, step] # Teacher forcing imitation learning loss
model(group_state, selected=second_action) # for the first step, set_q1 TODO: check for CVRP model
model(group_state, selected=second_action) # for the first step, set_q1 for TSP, do nothing for CVRP
group_state, reward, done = env.step(second_action)
solutions.append(second_action.unsqueeze(2))
step += 1
Expand Down
5 changes: 2 additions & 3 deletions EAS/source/eas_lay.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,17 +273,16 @@ def run_eas_lay(model, instance_data, problem_size, config, get_episode_data_fn,
if config.problem == "CVRP":
# First Move is given
first_action = LongTensor(np.zeros((batch_s, group_s))) # start from node_0-depot
# TODO: CVRP model need to do something?
# model_modified(group_state, selected=first_action) # do nothing for CVRP
group_state, reward, done = env.step(first_action)
solutions.append(first_action.unsqueeze(2))
step += 1

# First/Second Move is given
second_action = LongTensor(np.arange(group_s) % problem_size)[None, :].expand(batch_s, group_s).clone()

if iter > 0:
second_action[:, -1] = incumbent_solutions_expanded[:, step] # Teacher forcing the imitation learning loss
model_modified(group_state, selected=second_action) # for the first step, set_q1 TODO: check for CVRP model
model_modified(group_state, selected=second_action) # for the first step, set_q1 for TSP, do nothing for CVRP
group_state, reward, done = env.step(second_action)
solutions.append(second_action.unsqueeze(2))
step += 1
Expand Down
2 changes: 1 addition & 1 deletion EAS/source/eas_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def run_eas_tab(model, instance_data, problem_size, config, get_episode_data_fn,
last_action = first_action
elif config.problem == "CVRP": # start from node_0-depot
first_action = LongTensor(np.zeros((batch_s, group_s)))
# TODO: CVRP model need to do something?
# model(group_state, selected=first_action) # do nothing for CVRP
group_state, reward, done = env.step(first_action)
last_action = first_action

Expand Down
4 changes: 2 additions & 2 deletions EAS/source/tsp/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ def forward(self, state, selected=None):
else:
encoded_last_node = _get_encoding(self.encoded_nodes, state.current_node)
# shape: (batch, pomo, embedding)
prob = self.decoder(encoded_last_node, ninf_mask=state.ninf_mask)
probs = self.decoder(encoded_last_node, ninf_mask=state.ninf_mask)
# shape: (batch, pomo, problem)
return prob
return probs


def _get_encoding(encoded_nodes, node_index_to_pick):
Expand Down
Loading

0 comments on commit 238cbc8

Please sign in to comment.