Skip to content

Commit

Permalink
add L2D code
Browse files Browse the repository at this point in the history
  • Loading branch information
RoyalSkye committed May 25, 2023
1 parent 9b9ab62 commit 0b90547
Show file tree
Hide file tree
Showing 41 changed files with 3,366 additions and 0 deletions.
118 changes: 118 additions & 0 deletions L2D/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
### L2D - Learning to Delegate

```shell
./
├── code # source code
├── exps # pretrained model (checkpoint)
├── generations # training/test dataset
```

#### 1. Meta-learning implementation:

Please ref to supervised_maml.py

#### 2. How to run?

Please ref to https://github.com/mit-wu-lab/learning-to-delegate for the full instructions.

**Setups:** We train a regression model since (1) it has a better training efficiency (around 6 hrs) compared to the classification model; (2) it has better flexibility in training multiple sizes and distributions, which is quite suitable for the meta-learning setting. We use the datasets provided by the authors of L2D to construct the training task set. Concretely, the training task set contains six mixed CVRP distributions with $N\in$ {500, 1000} x $D \in$ {3,5,7}, where N is the problem size and D is the cluster center. We use HGS as the subsolver and keep the other settings the same as L2D. During the evaluation, we set the number of runs to 1 for each instance and set the time limit for solving each subproblem to 1s. For a fair comparison, we retrain the regression model (L2D with batch size 512 and 2048) and meta-train a regression model (Ours with batch size 512) on our training task set. We show the average cost over 10 instances in each test dataset (provided by the authors).

#### 3. Running command for size N=1000:

```shell
! MKL_NUM_THREADS=1 python supervised.py generations/mixed_merge/subproblem_selection_hgs/ exps/regression_model --generate --step 40000 --generate_partition test --save_dir generations/mixed_nc3_N1000 --save_suffix _test --generate_depth 600 --generate_index_start 0 --generate_index_end 10 --time_threshold 1 --n_trajectories 1 --n_cpus 5 --device cpu --solver HGS --data_suffix ""
```

#### 4. Full results:

```shell
mixed_nc3_N1000 - l2d(512)
[251.62551106502104, 53.57689983460714, 28.56822744543322, 166.9091070088698, 91.15954761906055, 170.48554992760728, 162.57677161432514, 129.81172138505744, 217.05214095248624, 153.577202052794]

mixed_nc3_N1000 - l2d(2048)
[84.7261170488263, 154.47144959719782, 83.69741048932549, 132.55254007655012, 218.25089110261925, 90.82448688068173, 150.43330142172704, 64.05975205820026, 173.92140200474418, 256.09531748297223]

mixed_nc3_N1000 - ours
[69.42047071444838, 51.0427442793504, 97.65782295711232, 104.50522717224759, 153.83061643063832, 42.17797180672912, 79.05410121573252, 119.54723134578701, 36.725955585189745, 55.51555610691494]



mixed_nc5_N1000 - l2d(512)
[69.74019808224472, 145.62450211310988, 169.72627323642416, 175.68106323374602, 157.3990536080209, 140.5325179742928, 82.34193183291808, 134.56869159571897, 55.80117246607038, 61.89810510360775]

mixed_nc5_N1000 - l2d(2048)
[110.72574893288841, 210.50907757356333, 184.88189880567415, 137.9578037311125, 192.99076090600752, 160.20049773022308, 57.41022826327845, 195.1705199857107, 163.49641411392273, 202.78750286094393]

mixed_nc5_N1000 - ours
[57.99501089117571, 93.34225822425061, 23.893524686932167, 177.03256662708472, 81.19762216863502, 100.40882459729039, 80.0493212122108, 144.23420107087776, 53.01993327542823, 144.12822490612234]



mixed_nc7_N1000 - l2d(512)
[84.62617227541953, 157.64712536068947, 128.03225841148753, 111.38248097396769, 59.74387879033938, 63.97868835936334, 114.86516268704834, 77.3905800445684, 112.78580207952513, 119.14617272509064]

mixed_nc7_N1000 - l2d(2048)
[135.89377747817426, 296.5332504141086, 278.9569668944009, 117.70339047152336, 232.70333555461082, 136.15778021436194, 64.44752992501857, 174.37551994169954, 253.02258379140608, 399.67403791328377]

mixed_nc7_N1000 - ours
[140.32011966077465, 292.97712925488884, 47.40998068710085, 39.58978814873822, 136.05281145567835, 56.59073682903006, 152.6357394237988, 63.939639374008316, 149.9488242006247, 170.02422984660086]



cluster_nc3_1000 - l2d(512)
[30.120461218524323, 150.4278131780203, 117.15000206739259, 42.32548831592055, 205.21742728318816, 250.7096387226733, 67.85091837340616, 127.59407470084068, 241.11326376547004, 261.83017371705233]

cluster_nc3_1000 - l2d(2048)
[171.7667802125586, 47.84423728470563, 77.10981619124246, 47.45477736388848, 129.16827534800174, 39.310031232815525, 12.145358431531388, 134.87052455643556, 109.25089985635734, 43.20389513123844]

cluster_nc3_1000 - ours
[58.656071567009434, 41.92006968030428, 49.618833336973864, 81.95952442043318, 166.16650047935875, 266.65154333726207, 51.09558283707406, 34.58893279238909, 58.64886438014483, 110.2826485745973]



Running Command for size N=2000:
! MKL_NUM_THREADS=1 python supervised.py generations/mixed_merge/subproblem_selection_hgs/ exps/regression_model --generate --step 40000 --generate_partition test --save_dir generations/mixed_nc3_N2000 --save_suffix _test --generate_depth 1200 --generate_index_start 0 --generate_index_end 10 --time_threshold 1 --n_trajectories 1 --n_cpus 5 --device cpu --solver HGS --data_suffix ""

mixed_nc3_N2000 - l2d(512)
[297.17555995646046, 224.1117641739214, 323.6044628226989, 603.002817128388, 86.74928776573856, 31.01868685831254, 249.67777303587462, 235.97461509991484, 222.13176661809973, 603.002817128388]

mixed_nc3_N2000 - l2d(2048)
[587.184820767217, 210.7612750888125, 75.38392770167391, 520.371185956098, 304.69808997879454, 252.4886658906179, 188.74746698222876, 499.0123259666653, 238.5963423825351, 243.42672266328546]

mixed_nc3_N2000 - ours
[166.38890659919699, 14.473578857181273, 56.02676363673076, 102.7478707574481, 269.31974708922655, 65.79044088651204, 121.26241497652829, 58.73771721324477, 54.156602636351344, 103.84752849939254]



mixed_nc5_N2000 - l2d(512)
[336.05504129618356, 305.35381922008713, 545.7554339182415, 31.107871095495945, 15.412486810713151, 377.00637798927505, 228.69223396604536, 181.2950498462646, 139.77915218755012, 295.05411923606533]

mixed_nc5_N2000 - l2d(2048)
[120.18143415266007, 239.50822722280745, 395.9750986602888, 231.17618970855347, 123.9906443227357, 61.886272600207526, 63.21917965150509, 306.3240563321283, 268.02123944893873, 138.08266388635508]

mixed_nc5_N2000 - ours
[169.22787904589654, 15.25169628958133, 183.63330947438064, 44.88655876991325, 203.14387332737172, 164.50153655549016, 52.84231699778137, 321.23316840142826, 49.347673589059355, 190.37120468174933]



mixed_nc7_N2000 - l2d(512)
[58.41117336948586, 162.38480853630307, 20.173872809889595, 284.8120501912994, 252.75855695353138, 407.1377845829606, 243.13406959016777, 118.38687248195713, 283.9189665950691, 187.23938054255234]

mixed_nc7_N2000 - l2d(2048)
[274.059016924158, 341.0806826295227, 257.68884597770165, 440.3634987968597, 347.35328305574257, 298.4314865476653, 292.16630394701144, 172.3322990292549, 402.508525286955, 178.39174662542698]

mixed_nc7_N2000 - ours
[39.02544223359225, 95.01056393060658, 159.83643251882305, 123.78308760692866, 207.52486457330627, 126.03625945016063, 45.65872605374195, 22.730502604456245, 96.75671104917379, 43.97014364418021]



cluster_nc3_2000 - l2d(512)
[563.2109033353247, 104.06906492981103, 244.64043763420003, 206.17920183959623, 89.24705936199747, 128.66908520327183, 147.56376484351892, 180.53860000617067, 137.05012740969215, 145.8099772676529]

cluster_nc3_2000 - l2d(2048)
[385.5637432105201, 277.29559212692345, 92.46191472292881, 426.25341582036725, 180.45819685366106, 240.1705485484613, 389.27261295285996, 275.3181096552707, 140.05090556334272, 245.7650284797365]

cluster_nc3_2000 - ours
[166.2742823513385, 10.246224912987316, 120.88454563307403, 63.16772656055168, 93.23240030367036, 99.73175989491011, 200.17958806832718, 111.87716161302477, 32.97521295307001, 73.30037148228023]
```

20 changes: 20 additions & 0 deletions L2D/code/concat_preprocessed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from util import *

parser = argparse.ArgumentParser()
parser.add_argument('path_or_globs', type=Path, nargs='+')
parser.add_argument('--statistics', action='store_true')
parser.add_argument('output', type=Path)

if __name__ == '__main__':
args = parser.parse_args()
assert args.output.name.endswith('_subproblems_statistics.npz' if args.statistics else '_subproblems.npz')
here = Path('.')
npzs = [np.load(path) for glob in args.path_or_globs for path in glob.parent.glob(glob.name)]

args.output.parent.mkdir(parents=True, exist_ok=True)
if args.statistics:
np.savez(args.output, **{key: np.concatenate([npz[key] for npz in npzs]) for key in npzs[0].files})
else:
n_nodes = [len(npz['xs']) for npz in npzs]
offsets = np.cumsum(n_nodes) - n_nodes
np.savez(args.output, **{key: np.concatenate([(npz[key] + o) if key == 'offsets' else npz[key] for npz, o in zip(npzs, offsets)]) for key in npzs[0].files})
171 changes: 171 additions & 0 deletions L2D/code/generate_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
from util import *

def clustered_xys(args, center_depot=False, max_xy=1):
uniform_frac = 0.5 if args.mixed else 0.0
n_uniform = int((args.n_nodes - args.n_c) * uniform_frac)
n_clustered = args.n_nodes - args.n_c - n_uniform
uniform_locs = np.random.uniform(0, max_xy, size=(n_uniform, 2))

assert args.n_c < args.n_nodes
centers = np.random.uniform(0.2, max_xy - 0.2, size=(args.n_c, 2))

n_clustered_samples = 0
all_clustered_locs = []
while n_clustered_samples < n_clustered:
center_locs = centers[np.random.randint(len(centers), size=2 * (n_clustered - n_clustered_samples))]
cluster_locs = np.random.normal(center_locs, args.std_cluster)
cluster_locs = cluster_locs[(cluster_locs >= 0).all(axis=1) & (cluster_locs < max_xy).all(axis=1)]
all_clustered_locs.append(cluster_locs)
n_clustered_samples += len(cluster_locs)
cluster_locs = np.concatenate(all_clustered_locs)[:n_clustered]
xys = np.vstack((centers, uniform_locs, cluster_locs))
if center_depot:
depot = np.mean(xys, axis=0, keepdims=True)
else:
min_x, min_y = np.clip(xys.min(axis=0) - 0.1, 0, max_xy)
max_x, max_y = np.clip(xys.max(axis=0) + 0.1, 0, max_xy)
depot = np.array([[np.random.uniform(min_x, max_x), np.random.uniform(min_y, max_y)]])
return np.vstack((depot, xys))


def generate_gaussian_mixture_tsp(graph_size, num_modes=0, cdist=0):
'''
Adaptation from AAAI-2022 "Learning to Solve Travelling Salesman Problem with Hardness-Adaptive Curriculum".
'''

def gaussian_mixture(graph_size=100, num_modes=0, cdist=1):
'''
GMM create one instance of TSP-100, using cdist
'''
from sklearn.preprocessing import MinMaxScaler
nums = np.random.multinomial(graph_size, np.ones(num_modes) / num_modes)
xy = []
for num in nums:
center = np.random.uniform(0, cdist, size=(1, 2))
nxy = np.random.multivariate_normal(mean=center.squeeze(), cov=np.eye(2, 2), size=(num,))
xy.extend(nxy)
xy = np.array(xy)
xy = MinMaxScaler().fit_transform(xy)
return xy

if num_modes == 0: # (0, 0) - uniform
return np.random.uniform(0, 1, [graph_size, 2])
else:
return np.array(gaussian_mixture(graph_size=graph_size, num_modes=num_modes, cdist=cdist))


def generate_problem(args, init=None):
if init:
xys, demands, capacity, pkwargs = init
else:
if args.dist == "uniform":
xys = clustered_xys(args) if args.n_c else np.random.uniform(0, 1, size=(1 + args.n_nodes, 2))
demands = np.random.randint(args.min_demand, args.max_demand, size=1 + args.n_nodes)
demands[0] = 0
elif args.dist == "gm":
training_set = [(0, 0), (3, 10), (3, 30), (3, 50), (5, 10), (5, 30), (5, 50), (7, 10), (7, 30), (7, 50)]
if args.partition != "train":
num_modes, cdist = training_set[args.id % len(training_set)]
else:
num_modes, cdist = training_set[args.id // 50]
xys = generate_gaussian_mixture_tsp(1 + args.n_nodes, num_modes, cdist)
demands = np.random.randint(args.min_demand, args.max_demand, size=1 + args.n_nodes)
demands[0] = 0

pkwargs = {}
if args.ptype == 'CVRPTW':
windows = np.random.uniform(0, 1, size=(1 + args.n_nodes, 2))
dist_depot = cdist(xys[1:], [xys[0]]).flatten()
windows[0, 0], windows[0, 1] = depot_start, depot_end = 0, 3 # np.max(dist_depot) * 2
time_centers = np.random.uniform(depot_start + dist_depot, depot_end - dist_depot - args.service_time)
time_half_width = np.random.uniform(args.service_time / 2, args.max_window_width, size=(args.n_nodes))

# start time: 0 - 2; end time: 1 - 3
windows[1:, 0] = np.clip(time_centers - time_half_width, depot_start, depot_end)
windows[1:, 1] = np.clip(time_centers + time_half_width, depot_start, depot_end)
pkwargs['window'] = windows
pkwargs['service_time'] = args.service_time
elif args.ptype == 'VRPMPD':
pkwargs['is_pickup'] = is_pickup = np.zeros_like(demands, dtype=np.bool)
is_pickup[1::args.pickup_every] = True

init_routes = solve_init(xys, demands, args, pkwargs=pkwargs)
return VRFullProblem(xys, demands, args.capacity, init_routes, ptype=args.ptype, pkwargs=pkwargs)

def generate_i(gen_args):
i, seed, args, init = gen_args
np.random.seed(seed)
start_time = time()
print(f'Generating problem {i}...')
args.id = i
p = generate_problem(args, init)

total_time = time() - start_time
print(f'Problem {i} took {total_time:.4f} seconds')
return p.xys, p._demands, p.capacity, p.route_dists, pack_routes(p.routes), p.pkwargs, total_time

if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('save_dir', type=Path)
parser.add_argument('partition', type=str, choices=['train', 'val', 'test'])
parser.add_argument('n_nodes', type=int)
parser.add_argument('--n_c', type=int, default=0, help='Number of city clusters in the problem instance')
parser.add_argument('--mixed', action='store_true')
parser.add_argument('--std_cluster', type=float, default=0.07, help='Standard deviation for normal distribution of city clusters')
parser.add_argument('--ptype', type=str, default='CVRP', choices=['CVRP', 'CVRPTW', 'VRPMPD'])
parser.add_argument('--n_instances', type=int, default=None)
parser.add_argument('--n_clusters', type=int, default=10)
parser.add_argument('--n_lkh_trials', type=int, default=100)
parser.add_argument('--min_demand', type=int, default=1, help='Inclusive')
parser.add_argument('--max_demand', type=int, default=10, help='Exclusive')
parser.add_argument('--capacity', type=int, default=50)
parser.add_argument('--service_time', type=float, default=0.02)
parser.add_argument('--max_window_width', type=float, default=1.5)
parser.add_argument('--pickup_every', type=int, default=2)
parser.add_argument('--n_cpus', type=int, default=40)
parser.add_argument('--n_process', type=int, default=None)
parser.add_argument('--n_threads_per_process', type=int, default=None)
parser.add_argument('--solver', type=str, choices=['LKH', 'HGS'], default='LKH')
parser.add_argument('--naive_init', action='store_true')
parser.add_argument('--full_solver_init', action='store_true')
parser.add_argument('--dist', type=str, choices=['uniform', 'gm'], default='gm') # (0, 0) + {3, 5, 7} * {10, 30, 50}
args = parser.parse_args()

args.save_dir.mkdir(parents=True, exist_ok=True)
args.n_instances = args.n_instances or (2000 if args.partition == 'train' else 40)

partition = args.partition
ref_path = ref_problems = None
save_path = args.save_dir / f'problems_{partition}.npz'
if args.naive_init or args.full_solver_init or args.n_lkh_trials != parser.get_default('n_lkh_trials'):
ref_path = save_path
assert ref_path.exists(), f'If using --naive_init or --full_solver_init or --n_lkh_trials, must have already used the default init to generate {ref_path}'
partition_name = diff_args(args, parser, partition, naive_init='initnaive', full_solver_init='initfull', n_lkh_trials='lkh')
save_path = args.save_dir / f'problems_{partition_name}.npz'
if save_path.exists():
print(f'Already generated {save_path}, quitting', flush=True)
exit()
print(f'Generating to {save_path}', flush=True)
if ref_path:
print(f'Generating {args.n_instances} {args.ptype} initial solutions for previous problems from {ref_path}', flush=True)
ref_problems = np.load(ref_path)
nodes, demands, capacities = ref_problems['nodes'], ref_problems['demands'], ref_problems['capacities']
assert demands.shape == (args.n_instances, args.n_nodes + 1)
pkwarg_keys = dict(CVRP=[], CVRPTW=['window', 'service_time'], VRPMPD=['is_pickup'])[args.ptype]
pkwargs = [{k: ref_problems[k][i] for k in pkwarg_keys} for i in range(args.n_instances)]
else:
print(f'Generating {args.n_instances} {args.ptype} instances from {"uniform distribution" if args.n_c == 0 else f"mixed distribution with {args.n_c} city clusters" if args.mixed else f"clustered distribution with {args.n_c} city_clusters"}, each with {args.n_clusters} radial sections to run LKH subsolver on', flush=True)

results = multiprocess(generate_i, list(zip(
range(0, args.n_instances),
np.random.randint(np.iinfo(np.int32).max, size=args.n_instances),
[args] * args.n_instances,
zip(nodes, demands, capacities, pkwargs) if ref_problems else [None] * args.n_instances,
)), cpus=args.n_process or (args.n_cpus - 1) // args.n_clusters + 1)

xys, demands, capacities, route_dists, init_tours, pkwargs, times = zip(*results)
route_dists = pad_each(route_dists)
init_tours = pad_each(init_tours)
pkwargs = {k: np.array([pk[k] for pk in pkwargs]) for k in pkwargs[0]}
np.savez(save_path, nodes=xys, demands=demands, capacities=capacities, dists=route_dists, routes=init_tours, times=np.array(times), **pkwargs)

Loading

0 comments on commit 0b90547

Please sign in to comment.