-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
41 changed files
with
3,366 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|
Oops, something went wrong.