Skip to content

Commit

Permalink
Add files via upload
Browse files Browse the repository at this point in the history
  • Loading branch information
rushhan authored Sep 23, 2020
1 parent 8467cb4 commit 0fd99c6
Show file tree
Hide file tree
Showing 16 changed files with 1,763 additions and 0 deletions.
216 changes: 216 additions & 0 deletions eval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import math
import torch
import os
import argparse
import numpy as np
import itertools
from tqdm import tqdm
from utils import load_model, move_to
from utils.data_utils import save_dataset
from torch.utils.data import DataLoader
import time
from datetime import timedelta
from utils.functions import parse_softmax_temperature
mp = torch.multiprocessing.get_context('spawn')


def get_best(sequences, cost, ids=None, batch_size=None):
"""
Ids contains [0, 0, 0, 1, 1, 2, ..., n, n, n] if 3 solutions found for 0th instance, 2 for 1st, etc
:param sequences:
:param lengths:
:param ids:
:return: list with n sequences and list with n lengths of solutions
"""
if ids is None:
idx = cost.argmin()
return sequences[idx:idx+1, ...], cost[idx:idx+1, ...]

splits = np.hstack([0, np.where(ids[:-1] != ids[1:])[0] + 1])
mincosts = np.minimum.reduceat(cost, splits)

group_lengths = np.diff(np.hstack([splits, len(ids)]))
all_argmin = np.flatnonzero(np.repeat(mincosts, group_lengths) == cost)
result = np.full(len(group_lengths) if batch_size is None else batch_size, -1, dtype=int)

result[ids[all_argmin[::-1]]] = all_argmin[::-1]

return [sequences[i] if i >= 0 else None for i in result], [cost[i] if i >= 0 else math.inf for i in result]


def eval_dataset_mp(args):
(dataset_path, width, softmax_temp, opts, i, num_processes) = args

model, _ = load_model(opts.model)
val_size = opts.val_size // num_processes
dataset = model.problem.make_dataset(filename=dataset_path, num_samples=val_size, offset=opts.offset + val_size * i)
device = torch.device("cuda:{}".format(i))

return _eval_dataset(model, dataset, width, softmax_temp, opts, device)


def eval_dataset(dataset_path, width, softmax_temp, opts):
# Even with multiprocessing, we load the model here since it contains the name where to write results
model, _ = load_model(opts.model)
use_cuda = torch.cuda.is_available() and not opts.no_cuda
if opts.multiprocessing:
assert use_cuda, "Can only do multiprocessing with cuda"
num_processes = torch.cuda.device_count()
assert opts.val_size % num_processes == 0

with mp.Pool(num_processes) as pool:
results = list(itertools.chain.from_iterable(pool.map(
eval_dataset_mp,
[(dataset_path, width, softmax_temp, opts, i, num_processes) for i in range(num_processes)]
)))

else:
device = torch.device("cuda:0" if use_cuda else "cpu")
dataset = model.problem.make_dataset(filename=dataset_path, num_samples=opts.val_size, offset=opts.offset)
results = _eval_dataset(model, dataset, width, softmax_temp, opts, device)

# This is parallelism, even if we use multiprocessing (we report as if we did not use multiprocessing, e.g. 1 GPU)
parallelism = opts.eval_batch_size

costs, tours, durations = zip(*results) # Not really costs since they should be negative

print("Average cost: {} +- {}".format(np.mean(costs), 2 * np.std(costs) / np.sqrt(len(costs))))
print("Average serial duration: {} +- {}".format(
np.mean(durations), 2 * np.std(durations) / np.sqrt(len(durations))))
print("Average parallel duration: {}".format(np.mean(durations) / parallelism))
print("Calculated total duration: {}".format(timedelta(seconds=int(np.sum(durations) / parallelism))))

dataset_basename, ext = os.path.splitext(os.path.split(dataset_path)[-1])
model_name = "_".join(os.path.normpath(os.path.splitext(opts.model)[0]).split(os.sep)[-2:])
if opts.o is None:
results_dir = os.path.join(opts.results_dir, model.problem.NAME, dataset_basename)
os.makedirs(results_dir, exist_ok=True)

out_file = os.path.join(results_dir, "{}-{}-{}{}-t{}-{}-{}{}".format(
dataset_basename, model_name,
opts.decode_strategy,
width if opts.decode_strategy != 'greedy' else '',
softmax_temp, opts.offset, opts.offset + len(costs), ext
))
else:
out_file = opts.o

assert opts.f or not os.path.isfile(
out_file), "File already exists! Try running with -f option to overwrite."

save_dataset((results, parallelism), out_file)

return costs, tours, durations


def _eval_dataset(model, dataset, width, softmax_temp, opts, device):

model.to(device)
model.eval()

model.set_decode_type(
"greedy" if opts.decode_strategy in ('bs', 'greedy') else "sampling",
temp=softmax_temp)

dataloader = DataLoader(dataset, batch_size=opts.eval_batch_size)

results = []
for batch in tqdm(dataloader, disable=opts.no_progress_bar):
batch = move_to(batch, device)

start = time.time()
with torch.no_grad():
if opts.decode_strategy in ('sample', 'greedy'):
if opts.decode_strategy == 'greedy':
assert width == 0, "Do not set width when using greedy"
assert opts.eval_batch_size <= opts.max_calc_batch_size, \
"eval_batch_size should be smaller than calc batch size"
batch_rep = 1
iter_rep = 1
elif width * opts.eval_batch_size > opts.max_calc_batch_size:
assert opts.eval_batch_size == 1
assert width % opts.max_calc_batch_size == 0
batch_rep = opts.max_calc_batch_size
iter_rep = width // opts.max_calc_batch_size
else:
batch_rep = width
iter_rep = 1
assert batch_rep > 0
# This returns (batch_size, iter_rep shape)
sequences, costs = model.sample_many(batch, batch_rep=batch_rep, iter_rep=iter_rep)
batch_size = len(costs)
ids = torch.arange(batch_size, dtype=torch.int64, device=costs.device)
else:
assert opts.decode_strategy == 'bs'

cum_log_p, sequences, costs, ids, batch_size = model.beam_search(
batch, beam_size=width,
compress_mask=opts.compress_mask,
max_calc_batch_size=opts.max_calc_batch_size
)

if sequences is None:
sequences = [None] * batch_size
costs = [math.inf] * batch_size
else:
sequences, costs = get_best(
sequences.cpu().numpy(), costs.cpu().numpy(),
ids.cpu().numpy() if ids is not None else None,
batch_size
)
duration = time.time() - start
for seq, cost in zip(sequences, costs):
if model.problem.NAME == "tsp":
seq = seq.tolist() # No need to trim as all are same length
elif model.problem.NAME in ("cvrp", "sdvrp"):
seq = np.trim_zeros(seq).tolist() + [0] # Add depot
elif model.problem.NAME in ("op", "pctsp"):
seq = np.trim_zeros(seq) # We have the convention to exclude the depot
else:
assert False, "Unkown problem: {}".format(model.problem.NAME)
# Note VRP only
results.append((cost, seq, duration))

return results


if __name__ == "__main__":

parser = argparse.ArgumentParser()
parser.add_argument("datasets", nargs='+', help="Filename of the dataset(s) to evaluate")
parser.add_argument("-f", action='store_true', help="Set true to overwrite")
parser.add_argument("-o", default=None, help="Name of the results file to write")
parser.add_argument('--val_size', type=int, default=10000,
help='Number of instances used for reporting validation performance')
parser.add_argument('--offset', type=int, default=0,
help='Offset where to start in dataset (default 0)')
parser.add_argument('--eval_batch_size', type=int, default=1024,
help="Batch size to use during (baseline) evaluation")
# parser.add_argument('--decode_type', type=str, default='greedy',
# help='Decode type, greedy or sampling')
parser.add_argument('--width', type=int, nargs='+',
help='Sizes of beam to use for beam search (or number of samples for sampling), '
'0 to disable (default), -1 for infinite')
parser.add_argument('--decode_strategy', type=str,
help='Beam search (bs), Sampling (sample) or Greedy (greedy)')
parser.add_argument('--softmax_temperature', type=parse_softmax_temperature, default=1,
help="Softmax temperature (sampling or bs)")
parser.add_argument('--model', type=str)
parser.add_argument('--no_cuda', action='store_true', help='Disable CUDA')
parser.add_argument('--no_progress_bar', action='store_true', help='Disable progress bar')
parser.add_argument('--compress_mask', action='store_true', help='Compress mask into long')
parser.add_argument('--max_calc_batch_size', type=int, default=10000, help='Size for subbatches')
parser.add_argument('--results_dir', default='results', help="Name of results directory")
parser.add_argument('--multiprocessing', action='store_true',
help='Use multiprocessing to parallelize over multiple GPUs')

opts = parser.parse_args()

assert opts.o is None or (len(opts.datasets) == 1 and len(opts.width) <= 1), \
"Cannot specify result filename with more than one dataset or more than one width"

widths = opts.width if opts.width is not None else [0]

for width in widths:
for dataset_path in opts.datasets:
eval_dataset(dataset_path, width, opts.softmax_temperature, opts)
167 changes: 167 additions & 0 deletions generate_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import argparse
import os
import numpy as np
from utils.data_utils import check_extension, save_dataset


def generate_tsp_data(dataset_size, tsp_size):
return np.random.uniform(size=(dataset_size, tsp_size, 2)).tolist()


def generate_vrp_data(dataset_size, vrp_size):
CAPACITIES = {
10: 20.,
20: 30.,
50: 40.,
100: 50.
}
return list(zip(
np.random.uniform(size=(dataset_size, 2)).tolist(), # Depot location
np.random.uniform(size=(dataset_size, vrp_size, 2)).tolist(), # Node locations
np.random.randint(1, 10, size=(dataset_size, vrp_size)).tolist(), # Demand, uniform integer 1 ... 9
np.full(dataset_size, CAPACITIES[vrp_size]).tolist() # Capacity, same for whole dataset
))


def generate_op_data(dataset_size, op_size, prize_type='const'):
depot = np.random.uniform(size=(dataset_size, 2))
loc = np.random.uniform(size=(dataset_size, op_size, 2))

# Methods taken from Fischetti et al. 1998
if prize_type == 'const':
prize = np.ones((dataset_size, op_size))
elif prize_type == 'unif':
prize = (1 + np.random.randint(0, 100, size=(dataset_size, op_size))) / 100.
else: # Based on distance to depot
assert prize_type == 'dist'
prize_ = np.linalg.norm(depot[:, None, :] - loc, axis=-1)
prize = (1 + (prize_ / prize_.max(axis=-1, keepdims=True) * 99).astype(int)) / 100.

# Max length is approximately half of optimal TSP tour, such that half (a bit more) of the nodes can be visited
# which is maximally difficult as this has the largest number of possibilities
MAX_LENGTHS = {
20: 2.,
50: 3.,
100: 4.
}

return list(zip(
depot.tolist(),
loc.tolist(),
prize.tolist(),
np.full(dataset_size, MAX_LENGTHS[op_size]).tolist() # Capacity, same for whole dataset
))


def generate_pctsp_data(dataset_size, pctsp_size, penalty_factor=3):
depot = np.random.uniform(size=(dataset_size, 2))
loc = np.random.uniform(size=(dataset_size, pctsp_size, 2))

# For the penalty to make sense it should be not too large (in which case all nodes will be visited) nor too small
# so we want the objective term to be approximately equal to the length of the tour, which we estimate with half
# of the nodes by half of the tour length (which is very rough but similar to op)
# This means that the sum of penalties for all nodes will be approximately equal to the tour length (on average)
# The expected total (uniform) penalty of half of the nodes (since approx half will be visited by the constraint)
# is (n / 2) / 2 = n / 4 so divide by this means multiply by 4 / n,
# However instead of 4 we use penalty_factor (3 works well) so we can make them larger or smaller
MAX_LENGTHS = {
20: 2.,
50: 3.,
100: 4.
}
penalty_max = MAX_LENGTHS[pctsp_size] * (penalty_factor) / float(pctsp_size)
penalty = np.random.uniform(size=(dataset_size, pctsp_size)) * penalty_max

# Take uniform prizes
# Now expectation is 0.5 so expected total prize is n / 2, we want to force to visit approximately half of the nodes
# so the constraint will be that total prize >= (n / 2) / 2 = n / 4
# equivalently, we divide all prizes by n / 4 and the total prize should be >= 1
deterministic_prize = np.random.uniform(size=(dataset_size, pctsp_size)) * 4 / float(pctsp_size)

# In the deterministic setting, the stochastic_prize is not used and the deterministic prize is known
# In the stochastic setting, the deterministic prize is the expected prize and is known up front but the
# stochastic prize is only revealed once the node is visited
# Stochastic prize is between (0, 2 * expected_prize) such that E(stochastic prize) = E(deterministic_prize)
stochastic_prize = np.random.uniform(size=(dataset_size, pctsp_size)) * deterministic_prize * 2

return list(zip(
depot.tolist(),
loc.tolist(),
penalty.tolist(),
deterministic_prize.tolist(),
stochastic_prize.tolist()
))


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--filename", help="Filename of the dataset to create (ignores datadir)")
parser.add_argument("--data_dir", default='data', help="Create datasets in data_dir/problem (default 'data')")
parser.add_argument("--name", type=str, required=True, help="Name to identify dataset")
parser.add_argument("--problem", type=str, default='all',
help="Problem, 'tsp', 'vrp', 'pctsp' or 'op_const', 'op_unif' or 'op_dist'"
" or 'all' to generate all")
parser.add_argument('--data_distribution', type=str, default='all',
help="Distributions to generate for problem, default 'all'.")

parser.add_argument("--dataset_size", type=int, default=10000, help="Size of the dataset")
parser.add_argument('--graph_sizes', type=int, nargs='+', default=[20, 50, 100],
help="Sizes of problem instances (default 20, 50, 100)")
parser.add_argument("-f", action='store_true', help="Set true to overwrite")
parser.add_argument('--seed', type=int, default=1234, help="Random seed")

opts = parser.parse_args()

assert opts.filename is None or (len(opts.problems) == 1 and len(opts.graph_sizes) == 1), \
"Can only specify filename when generating a single dataset"

distributions_per_problem = {
'tsp': [None],
'vrp': [None],
'pctsp': [None],
'op': ['const', 'unif', 'dist']
}
if opts.problem == 'all':
problems = distributions_per_problem
else:
problems = {
opts.problem:
distributions_per_problem[opts.problem]
if opts.data_distribution == 'all'
else [opts.data_distribution]
}

for problem, distributions in problems.items():
for distribution in distributions or [None]:
for graph_size in opts.graph_sizes:

datadir = os.path.join(opts.data_dir, problem)
os.makedirs(datadir, exist_ok=True)

if opts.filename is None:
filename = os.path.join(datadir, "{}{}{}_{}_seed{}.pkl".format(
problem,
"_{}".format(distribution) if distribution is not None else "",
graph_size, opts.name, opts.seed))
else:
filename = check_extension(opts.filename)

assert opts.f or not os.path.isfile(check_extension(filename)), \
"File already exists! Try running with -f option to overwrite."

np.random.seed(opts.seed)
if problem == 'tsp':
dataset = generate_tsp_data(opts.dataset_size, graph_size)
elif problem == 'vrp':
dataset = generate_vrp_data(
opts.dataset_size, graph_size)
elif problem == 'pctsp':
dataset = generate_pctsp_data(opts.dataset_size, graph_size)
elif problem == "op":
dataset = generate_op_data(opts.dataset_size, graph_size, prize_type=distribution)
else:
assert False, "Unknown problem: {}".format(problem)

print(dataset[0])

save_dataset(dataset, filename)
1 change: 1 addition & 0 deletions knapsack.ipynb

Large diffs are not rendered by default.

Loading

0 comments on commit 0fd99c6

Please sign in to comment.