├── .gitignore ├── LICENSE ├── README.md ├── environment.yml ├── eval.py ├── generate_data.py ├── images ├── cvrp_0.png ├── cvrp_1.png ├── cvrp_2.png ├── cvrp_3.png ├── cvrp_4.png ├── cvrp_5.png ├── cvrp_6.png ├── cvrp_7.png ├── cvrp_8.png ├── cvrp_9.png └── tsp.gif ├── nets ├── __init__.py ├── attention_model.py ├── critic_network.py ├── graph_encoder.py └── pointer_network.py ├── options.py ├── plot_vrp.ipynb ├── pretrained ├── cvrp_10 │ ├── args.json │ └── epoch-99.pt ├── cvrp_100 │ ├── args.json │ └── epoch-99.pt ├── cvrp_20 │ ├── args.json │ └── epoch-99.pt ├── cvrp_50 │ ├── args.json │ └── epoch-99.pt ├── op_const_100 │ ├── args.json │ └── epoch-99.pt ├── op_const_20 │ ├── args.json │ └── epoch-99.pt ├── op_const_50 │ ├── args.json │ └── epoch-99.pt ├── op_dist_100 │ ├── args.json │ └── epoch-99.pt ├── op_dist_20 │ ├── args.json │ └── epoch-99.pt ├── op_dist_50 │ ├── args.json │ └── epoch-99.pt ├── op_unif_100 │ ├── args.json │ └── epoch-99.pt ├── op_unif_20 │ ├── args.json │ └── epoch-99.pt ├── op_unif_50 │ ├── args.json │ └── epoch-99.pt ├── pctsp_det_100 │ ├── args.json │ └── epoch-99.pt ├── pctsp_det_20 │ ├── args.json │ └── epoch-99.pt ├── pctsp_det_50 │ ├── args.json │ └── epoch-99.pt ├── pctsp_stoch_100 │ ├── args.json │ └── epoch-99.pt ├── pctsp_stoch_20 │ ├── args.json │ └── epoch-99.pt ├── pctsp_stoch_50 │ ├── args.json │ └── epoch-99.pt ├── sdvrp_10 │ ├── args.json │ └── epoch-99.pt ├── sdvrp_100 │ ├── args.json │ └── epoch-99.pt ├── sdvrp_20 │ ├── args.json │ └── epoch-99.pt ├── sdvrp_50 │ ├── args.json │ └── epoch-99.pt ├── tsp_100 │ ├── args.json │ └── epoch-99.pt ├── tsp_20 │ ├── args.json │ └── epoch-99.pt └── tsp_50 │ ├── args.json │ └── epoch-99.pt ├── problems ├── __init__.py ├── op │ ├── .gitignore │ ├── __init__.py │ ├── install_compass.sh │ ├── op_baseline.py │ ├── op_gurobi.py │ ├── op_ortools.py │ ├── opga │ │ ├── README.md │ │ ├── __init__.py │ │ ├── opevo.py │ │ ├── oph.py │ │ ├── optest.py │ │ └── test instances │ │ │ └── set_64_1_15.txt │ ├── problem_op.py │ ├── state_op.py │ └── tsiligirides.py ├── pctsp │ ├── PCTSP │ │ ├── .gitignore │ │ ├── Instances │ │ │ └── problem_20_100_100_1000.pctsp │ │ └── PCPTSP │ │ │ └── main.cpp │ ├── __init__.py │ ├── pctsp_baseline.py │ ├── pctsp_gurobi.py │ ├── pctsp_ortools.py │ ├── problem_pctsp.py │ ├── salesman │ │ ├── .gitignore │ │ ├── README.md │ │ ├── __init__.py │ │ └── pctsp │ │ │ ├── __init__.py │ │ │ ├── __main__.py │ │ │ ├── algo │ │ │ ├── __init__.py │ │ │ ├── geni.py │ │ │ ├── genius.py │ │ │ └── ilocal_search.py │ │ │ ├── application.py │ │ │ └── model │ │ │ ├── __init__.py │ │ │ ├── pctsp.py │ │ │ ├── solution.py │ │ │ └── tests │ │ │ ├── __init__.py │ │ │ └── test_solution.py │ └── state_pctsp.py ├── tsp │ ├── .gitignore │ ├── __init__.py │ ├── install_concorde.sh │ ├── problem_tsp.py │ ├── state_tsp.py │ ├── tsp_baseline.py │ └── tsp_gurobi.py └── vrp │ ├── .gitignore │ ├── __init__.py │ ├── encode-attend-navigate │ ├── Neural_Reinforce.py │ ├── data_generator.py │ └── utils.py │ ├── problem_vrp.py │ ├── state_cvrp.py │ ├── state_sdvrp.py │ └── vrp_baseline.py ├── reinforce_baselines.py ├── run.py ├── simple_tsp.ipynb ├── train.py └── utils ├── __init__.py ├── beam_search.py ├── boolmask.py ├── data_utils.py ├── functions.py ├── lexsort.py ├── log_utils.py ├── monkey_patch.py └── tensor_functions.py /.gitignore: -------------------------------------------------------------------------------- 1 | data/ 2 | outputs/ 3 | logs/ 4 | results/ 5 | __pycache__/ 6 | .idea 7 | */.ipynb_checkpoints 8 | *.tc.* 9 | *.tc_backward.* 10 | *.log 11 | *.bak -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Wouter Kool 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Note: I am currently not able to actively maintain this repository. Please also checkout more recent implementations, e.g. https://github.com/ai4co/rl4co and https://github.com/cpwan/RLOR. 2 | 3 | # Attention, Learn to Solve Routing Problems! 4 | 5 | Attention based model for learning to solve the Travelling Salesman Problem (TSP) and the Vehicle Routing Problem (VRP), Orienteering Problem (OP) and (Stochastic) Prize Collecting TSP (PCTSP). Training with REINFORCE with greedy rollout baseline. 6 | 7 | ![TSP100](images/tsp.gif) 8 | 9 | ## Paper 10 | For more details, please see our paper [Attention, Learn to Solve Routing Problems!](https://openreview.net/forum?id=ByxBFsRqYm) which has been accepted at [ICLR 2019](https://iclr.cc/Conferences/2019). If this code is useful for your work, please cite our paper: 11 | 12 | ``` 13 | @inproceedings{ 14 | kool2018attention, 15 | title={Attention, Learn to Solve Routing Problems!}, 16 | author={Wouter Kool and Herke van Hoof and Max Welling}, 17 | booktitle={International Conference on Learning Representations}, 18 | year={2019}, 19 | url={https://openreview.net/forum?id=ByxBFsRqYm}, 20 | } 21 | ``` 22 | 23 | ## Dependencies 24 | 25 | * Python>=3.8 26 | * NumPy 27 | * SciPy 28 | * [PyTorch](http://pytorch.org/)>=1.7 29 | * tqdm 30 | * [tensorboard_logger](https://github.com/TeamHG-Memex/tensorboard_logger) 31 | * Matplotlib (optional, only for plotting) 32 | 33 | ## Quick start 34 | 35 | For training TSP instances with 20 nodes and using rollout as REINFORCE baseline: 36 | ```bash 37 | python run.py --graph_size 20 --baseline rollout --run_name 'tsp20_rollout' 38 | ``` 39 | 40 | ## Usage 41 | 42 | ### Generating data 43 | 44 | Training data is generated on the fly. To generate validation and test data (same as used in the paper) for all problems: 45 | ```bash 46 | python generate_data.py --problem all --name validation --seed 4321 47 | python generate_data.py --problem all --name test --seed 1234 48 | ``` 49 | 50 | ### Training 51 | 52 | For training TSP instances with 20 nodes and using rollout as REINFORCE baseline and using the generated validation set: 53 | ```bash 54 | python run.py --graph_size 20 --baseline rollout --run_name 'tsp20_rollout' --val_dataset data/tsp/tsp20_validation_seed4321.pkl 55 | ``` 56 | 57 | #### Multiple GPUs 58 | By default, training will happen *on all available GPUs*. To disable CUDA at all, add the flag `--no_cuda`. 59 | Set the environment variable `CUDA_VISIBLE_DEVICES` to only use specific GPUs: 60 | ```bash 61 | CUDA_VISIBLE_DEVICES=2,3 python run.py 62 | ``` 63 | Note that using multiple GPUs has limited efficiency for small problem sizes (up to 50 nodes). 64 | 65 | #### Warm start 66 | You can initialize a run using a pretrained model by using the `--load_path` option: 67 | ```bash 68 | python run.py --graph_size 100 --load_path pretrained/tsp_100/epoch-99.pt 69 | ``` 70 | 71 | The `--load_path` option can also be used to load an earlier run, in which case also the optimizer state will be loaded: 72 | ```bash 73 | python run.py --graph_size 20 --load_path 'outputs/tsp_20/tsp20_rollout_{datetime}/epoch-0.pt' 74 | ``` 75 | 76 | The `--resume` option can be used instead of the `--load_path` option, which will try to resume the run, e.g. load additionally the baseline state, set the current epoch/step counter and set the random number generator state. 77 | 78 | ### Evaluation 79 | To evaluate a model, you can add the `--eval-only` flag to `run.py`, or use `eval.py`, which will additionally measure timing and save the results: 80 | ```bash 81 | python eval.py data/tsp/tsp20_test_seed1234.pkl --model pretrained/tsp_20 --decode_strategy greedy 82 | ``` 83 | If the epoch is not specified, by default the last one in the folder will be used. 84 | 85 | #### Sampling 86 | To report the best of 1280 sampled solutions, use 87 | ```bash 88 | python eval.py data/tsp/tsp20_test_seed1234.pkl --model pretrained/tsp_20 --decode_strategy sample --width 1280 --eval_batch_size 1 89 | ``` 90 | Beam Search (not in the paper) is also recently added and can be used using `--decode_strategy bs --width {beam_size}`. 91 | 92 | #### To run baselines 93 | Baselines for different problems are within the corresponding folders and can be ran (on multiple datasets at once) as follows 94 | ```bash 95 | python -m problems.tsp.tsp_baseline farthest_insertion data/tsp/tsp20_test_seed1234.pkl data/tsp/tsp50_test_seed1234.pkl data/tsp/tsp100_test_seed1234.pkl 96 | ``` 97 | To run baselines, you need to install [Compass](https://github.com/bcamath-ds/compass) by running the `install_compass.sh` script from within the `problems/op` directory and [Concorde](http://www.math.uwaterloo.ca/tsp/concorde.html) using the `install_concorde.sh` script from within `problems/tsp`. [LKH3](http://akira.ruc.dk/~keld/research/LKH-3/) should be automatically downloaded and installed when required. To use [Gurobi](http://www.gurobi.com), obtain a ([free academic](http://www.gurobi.com/registration/academic-license-reg)) license and follow the [installation instructions](https://www.gurobi.com/documentation/8.1/quickstart_windows/installing_the_anaconda_py.html). 98 | 99 | ### Other options and help 100 | ```bash 101 | python run.py -h 102 | python eval.py -h 103 | ``` 104 | 105 | ### Example CVRP solution 106 | See `plot_vrp.ipynb` for an example of loading a pretrained model and plotting the result for Capacitated VRP with 100 nodes. 107 | 108 | ![CVRP100](images/cvrp_0.png) 109 | 110 | ## Acknowledgements 111 | Thanks to [pemami4911/neural-combinatorial-rl-pytorch](https://github.com/pemami4911/neural-combinatorial-rl-pytorch) for getting me started with the code for the Pointer Network. 112 | 113 | This repository includes adaptions of the following repositories as baselines: 114 | * https://github.com/MichelDeudon/encode-attend-navigate 115 | * https://github.com/mc-ride/orienteering 116 | * https://github.com/jordanamecler/PCTSP 117 | * https://github.com/rafael2reis/salesman 118 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | # run: conda env create --file environment.yml 2 | name: attention_tsp 3 | channels: 4 | - pytorch 5 | dependencies: 6 | - python>=3.8 7 | - anaconda 8 | - tqdm 9 | - pytorch 10 | - torchvision 11 | - cuda91 12 | - pip 13 | - pip: 14 | - tensorboard_logger 15 | -------------------------------------------------------------------------------- /generate_data.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import numpy as np 4 | from utils.data_utils import check_extension, save_dataset 5 | 6 | 7 | def generate_tsp_data(dataset_size, tsp_size): 8 | return np.random.uniform(size=(dataset_size, tsp_size, 2)).tolist() 9 | 10 | 11 | def generate_vrp_data(dataset_size, vrp_size): 12 | CAPACITIES = { 13 | 10: 20., 14 | 20: 30., 15 | 50: 40., 16 | 100: 50. 17 | } 18 | return list(zip( 19 | np.random.uniform(size=(dataset_size, 2)).tolist(), # Depot location 20 | np.random.uniform(size=(dataset_size, vrp_size, 2)).tolist(), # Node locations 21 | np.random.randint(1, 10, size=(dataset_size, vrp_size)).tolist(), # Demand, uniform integer 1 ... 9 22 | np.full(dataset_size, CAPACITIES[vrp_size]).tolist() # Capacity, same for whole dataset 23 | )) 24 | 25 | 26 | def generate_op_data(dataset_size, op_size, prize_type='const'): 27 | depot = np.random.uniform(size=(dataset_size, 2)) 28 | loc = np.random.uniform(size=(dataset_size, op_size, 2)) 29 | 30 | # Methods taken from Fischetti et al. 1998 31 | if prize_type == 'const': 32 | prize = np.ones((dataset_size, op_size)) 33 | elif prize_type == 'unif': 34 | prize = (1 + np.random.randint(0, 100, size=(dataset_size, op_size))) / 100. 35 | else: # Based on distance to depot 36 | assert prize_type == 'dist' 37 | prize_ = np.linalg.norm(depot[:, None, :] - loc, axis=-1) 38 | prize = (1 + (prize_ / prize_.max(axis=-1, keepdims=True) * 99).astype(int)) / 100. 39 | 40 | # Max length is approximately half of optimal TSP tour, such that half (a bit more) of the nodes can be visited 41 | # which is maximally difficult as this has the largest number of possibilities 42 | MAX_LENGTHS = { 43 | 20: 2., 44 | 50: 3., 45 | 100: 4. 46 | } 47 | 48 | return list(zip( 49 | depot.tolist(), 50 | loc.tolist(), 51 | prize.tolist(), 52 | np.full(dataset_size, MAX_LENGTHS[op_size]).tolist() # Capacity, same for whole dataset 53 | )) 54 | 55 | 56 | def generate_pctsp_data(dataset_size, pctsp_size, penalty_factor=3): 57 | depot = np.random.uniform(size=(dataset_size, 2)) 58 | loc = np.random.uniform(size=(dataset_size, pctsp_size, 2)) 59 | 60 | # For the penalty to make sense it should be not too large (in which case all nodes will be visited) nor too small 61 | # so we want the objective term to be approximately equal to the length of the tour, which we estimate with half 62 | # of the nodes by half of the tour length (which is very rough but similar to op) 63 | # This means that the sum of penalties for all nodes will be approximately equal to the tour length (on average) 64 | # The expected total (uniform) penalty of half of the nodes (since approx half will be visited by the constraint) 65 | # is (n / 2) / 2 = n / 4 so divide by this means multiply by 4 / n, 66 | # However instead of 4 we use penalty_factor (3 works well) so we can make them larger or smaller 67 | MAX_LENGTHS = { 68 | 20: 2., 69 | 50: 3., 70 | 100: 4. 71 | } 72 | penalty_max = MAX_LENGTHS[pctsp_size] * (penalty_factor) / float(pctsp_size) 73 | penalty = np.random.uniform(size=(dataset_size, pctsp_size)) * penalty_max 74 | 75 | # Take uniform prizes 76 | # Now expectation is 0.5 so expected total prize is n / 2, we want to force to visit approximately half of the nodes 77 | # so the constraint will be that total prize >= (n / 2) / 2 = n / 4 78 | # equivalently, we divide all prizes by n / 4 and the total prize should be >= 1 79 | deterministic_prize = np.random.uniform(size=(dataset_size, pctsp_size)) * 4 / float(pctsp_size) 80 | 81 | # In the deterministic setting, the stochastic_prize is not used and the deterministic prize is known 82 | # In the stochastic setting, the deterministic prize is the expected prize and is known up front but the 83 | # stochastic prize is only revealed once the node is visited 84 | # Stochastic prize is between (0, 2 * expected_prize) such that E(stochastic prize) = E(deterministic_prize) 85 | stochastic_prize = np.random.uniform(size=(dataset_size, pctsp_size)) * deterministic_prize * 2 86 | 87 | return list(zip( 88 | depot.tolist(), 89 | loc.tolist(), 90 | penalty.tolist(), 91 | deterministic_prize.tolist(), 92 | stochastic_prize.tolist() 93 | )) 94 | 95 | 96 | if __name__ == "__main__": 97 | parser = argparse.ArgumentParser() 98 | parser.add_argument("--filename", help="Filename of the dataset to create (ignores datadir)") 99 | parser.add_argument("--data_dir", default='data', help="Create datasets in data_dir/problem (default 'data')") 100 | parser.add_argument("--name", type=str, required=True, help="Name to identify dataset") 101 | parser.add_argument("--problem", type=str, default='all', 102 | help="Problem, 'tsp', 'vrp', 'pctsp' or 'op_const', 'op_unif' or 'op_dist'" 103 | " or 'all' to generate all") 104 | parser.add_argument('--data_distribution', type=str, default='all', 105 | help="Distributions to generate for problem, default 'all'.") 106 | 107 | parser.add_argument("--dataset_size", type=int, default=10000, help="Size of the dataset") 108 | parser.add_argument('--graph_sizes', type=int, nargs='+', default=[20, 50, 100], 109 | help="Sizes of problem instances (default 20, 50, 100)") 110 | parser.add_argument("-f", action='store_true', help="Set true to overwrite") 111 | parser.add_argument('--seed', type=int, default=1234, help="Random seed") 112 | 113 | opts = parser.parse_args() 114 | 115 | assert opts.filename is None or (len(opts.problems) == 1 and len(opts.graph_sizes) == 1), \ 116 | "Can only specify filename when generating a single dataset" 117 | 118 | distributions_per_problem = { 119 | 'tsp': [None], 120 | 'vrp': [None], 121 | 'pctsp': [None], 122 | 'op': ['const', 'unif', 'dist'] 123 | } 124 | if opts.problem == 'all': 125 | problems = distributions_per_problem 126 | else: 127 | problems = { 128 | opts.problem: 129 | distributions_per_problem[opts.problem] 130 | if opts.data_distribution == 'all' 131 | else [opts.data_distribution] 132 | } 133 | 134 | for problem, distributions in problems.items(): 135 | for distribution in distributions or [None]: 136 | for graph_size in opts.graph_sizes: 137 | 138 | datadir = os.path.join(opts.data_dir, problem) 139 | os.makedirs(datadir, exist_ok=True) 140 | 141 | if opts.filename is None: 142 | filename = os.path.join(datadir, "{}{}{}_{}_seed{}.pkl".format( 143 | problem, 144 | "_{}".format(distribution) if distribution is not None else "", 145 | graph_size, opts.name, opts.seed)) 146 | else: 147 | filename = check_extension(opts.filename) 148 | 149 | assert opts.f or not os.path.isfile(check_extension(filename)), \ 150 | "File already exists! Try running with -f option to overwrite." 151 | 152 | np.random.seed(opts.seed) 153 | if problem == 'tsp': 154 | dataset = generate_tsp_data(opts.dataset_size, graph_size) 155 | elif problem == 'vrp': 156 | dataset = generate_vrp_data( 157 | opts.dataset_size, graph_size) 158 | elif problem == 'pctsp': 159 | dataset = generate_pctsp_data(opts.dataset_size, graph_size) 160 | elif problem == "op": 161 | dataset = generate_op_data(opts.dataset_size, graph_size, prize_type=distribution) 162 | else: 163 | assert False, "Unknown problem: {}".format(problem) 164 | 165 | print(dataset[0]) 166 | 167 | save_dataset(dataset, filename) 168 | -------------------------------------------------------------------------------- /images/cvrp_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/images/cvrp_0.png -------------------------------------------------------------------------------- /images/cvrp_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/images/cvrp_1.png -------------------------------------------------------------------------------- /images/cvrp_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/images/cvrp_2.png -------------------------------------------------------------------------------- /images/cvrp_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/images/cvrp_3.png -------------------------------------------------------------------------------- /images/cvrp_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/images/cvrp_4.png -------------------------------------------------------------------------------- /images/cvrp_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/images/cvrp_5.png -------------------------------------------------------------------------------- /images/cvrp_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/images/cvrp_6.png -------------------------------------------------------------------------------- /images/cvrp_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/images/cvrp_7.png -------------------------------------------------------------------------------- /images/cvrp_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/images/cvrp_8.png -------------------------------------------------------------------------------- /images/cvrp_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/images/cvrp_9.png -------------------------------------------------------------------------------- /images/tsp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/images/tsp.gif -------------------------------------------------------------------------------- /nets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/nets/__init__.py -------------------------------------------------------------------------------- /nets/critic_network.py: -------------------------------------------------------------------------------- 1 | from torch import nn 2 | from nets.graph_encoder import GraphAttentionEncoder 3 | 4 | 5 | class CriticNetwork(nn.Module): 6 | 7 | def __init__( 8 | self, 9 | input_dim, 10 | embedding_dim, 11 | hidden_dim, 12 | n_layers, 13 | encoder_normalization 14 | ): 15 | super(CriticNetwork, self).__init__() 16 | 17 | self.hidden_dim = hidden_dim 18 | 19 | self.encoder = GraphAttentionEncoder( 20 | node_dim=input_dim, 21 | n_heads=8, 22 | embed_dim=embedding_dim, 23 | n_layers=n_layers, 24 | normalization=encoder_normalization 25 | ) 26 | 27 | self.value_head = nn.Sequential( 28 | nn.Linear(embedding_dim, hidden_dim), 29 | nn.ReLU(), 30 | nn.Linear(hidden_dim, 1) 31 | ) 32 | 33 | def forward(self, inputs): 34 | """ 35 | 36 | :param inputs: (batch_size, graph_size, input_dim) 37 | :return: 38 | """ 39 | _, graph_embeddings = self.encoder(inputs) 40 | return self.value_head(graph_embeddings) 41 | -------------------------------------------------------------------------------- /options.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import argparse 4 | import torch 5 | 6 | 7 | def get_options(args=None): 8 | parser = argparse.ArgumentParser( 9 | description="Attention based model for solving the Travelling Salesman Problem with Reinforcement Learning") 10 | 11 | # Data 12 | parser.add_argument('--problem', default='tsp', help="The problem to solve, default 'tsp'") 13 | parser.add_argument('--graph_size', type=int, default=20, help="The size of the problem graph") 14 | parser.add_argument('--batch_size', type=int, default=512, help='Number of instances per batch during training') 15 | parser.add_argument('--epoch_size', type=int, default=1280000, help='Number of instances per epoch during training') 16 | parser.add_argument('--val_size', type=int, default=10000, 17 | help='Number of instances used for reporting validation performance') 18 | parser.add_argument('--val_dataset', type=str, default=None, help='Dataset file to use for validation') 19 | 20 | # Model 21 | parser.add_argument('--model', default='attention', help="Model, 'attention' (default) or 'pointer'") 22 | parser.add_argument('--embedding_dim', type=int, default=128, help='Dimension of input embedding') 23 | parser.add_argument('--hidden_dim', type=int, default=128, help='Dimension of hidden layers in Enc/Dec') 24 | parser.add_argument('--n_encode_layers', type=int, default=3, 25 | help='Number of layers in the encoder/critic network') 26 | parser.add_argument('--tanh_clipping', type=float, default=10., 27 | help='Clip the parameters to within +- this value using tanh. ' 28 | 'Set to 0 to not perform any clipping.') 29 | parser.add_argument('--normalization', default='batch', help="Normalization type, 'batch' (default) or 'instance'") 30 | 31 | # Training 32 | parser.add_argument('--lr_model', type=float, default=1e-4, help="Set the learning rate for the actor network") 33 | parser.add_argument('--lr_critic', type=float, default=1e-4, help="Set the learning rate for the critic network") 34 | parser.add_argument('--lr_decay', type=float, default=1.0, help='Learning rate decay per epoch') 35 | parser.add_argument('--eval_only', action='store_true', help='Set this value to only evaluate model') 36 | parser.add_argument('--n_epochs', type=int, default=100, help='The number of epochs to train') 37 | parser.add_argument('--seed', type=int, default=1234, help='Random seed to use') 38 | parser.add_argument('--max_grad_norm', type=float, default=1.0, 39 | help='Maximum L2 norm for gradient clipping, default 1.0 (0 to disable clipping)') 40 | parser.add_argument('--no_cuda', action='store_true', help='Disable CUDA') 41 | parser.add_argument('--exp_beta', type=float, default=0.8, 42 | help='Exponential moving average baseline decay (default 0.8)') 43 | parser.add_argument('--baseline', default=None, 44 | help="Baseline to use: 'rollout', 'critic' or 'exponential'. Defaults to no baseline.") 45 | parser.add_argument('--bl_alpha', type=float, default=0.05, 46 | help='Significance in the t-test for updating rollout baseline') 47 | parser.add_argument('--bl_warmup_epochs', type=int, default=None, 48 | help='Number of epochs to warmup the baseline, default None means 1 for rollout (exponential ' 49 | 'used for warmup phase), 0 otherwise. Can only be used with rollout baseline.') 50 | parser.add_argument('--eval_batch_size', type=int, default=1024, 51 | help="Batch size to use during (baseline) evaluation") 52 | parser.add_argument('--checkpoint_encoder', action='store_true', 53 | help='Set to decrease memory usage by checkpointing encoder') 54 | parser.add_argument('--shrink_size', type=int, default=None, 55 | help='Shrink the batch size if at least this many instances in the batch are finished' 56 | ' to save memory (default None means no shrinking)') 57 | parser.add_argument('--data_distribution', type=str, default=None, 58 | help='Data distribution to use during training, defaults and options depend on problem.') 59 | 60 | # Misc 61 | parser.add_argument('--log_step', type=int, default=50, help='Log info every log_step steps') 62 | parser.add_argument('--log_dir', default='logs', help='Directory to write TensorBoard information to') 63 | parser.add_argument('--run_name', default='run', help='Name to identify the run') 64 | parser.add_argument('--output_dir', default='outputs', help='Directory to write output models to') 65 | parser.add_argument('--epoch_start', type=int, default=0, 66 | help='Start at epoch # (relevant for learning rate decay)') 67 | parser.add_argument('--checkpoint_epochs', type=int, default=1, 68 | help='Save checkpoint every n epochs (default 1), 0 to save no checkpoints') 69 | parser.add_argument('--load_path', help='Path to load model parameters and optimizer state from') 70 | parser.add_argument('--resume', help='Resume from previous checkpoint file') 71 | parser.add_argument('--no_tensorboard', action='store_true', help='Disable logging TensorBoard files') 72 | parser.add_argument('--no_progress_bar', action='store_true', help='Disable progress bar') 73 | 74 | opts = parser.parse_args(args) 75 | 76 | opts.use_cuda = torch.cuda.is_available() and not opts.no_cuda 77 | opts.run_name = "{}_{}".format(opts.run_name, time.strftime("%Y%m%dT%H%M%S")) 78 | opts.save_dir = os.path.join( 79 | opts.output_dir, 80 | "{}_{}".format(opts.problem, opts.graph_size), 81 | opts.run_name 82 | ) 83 | if opts.bl_warmup_epochs is None: 84 | opts.bl_warmup_epochs = 1 if opts.baseline == 'rollout' else 0 85 | assert (opts.bl_warmup_epochs == 0) or (opts.baseline == 'rollout') 86 | assert opts.epoch_size % opts.batch_size == 0, "Epoch size must be integer multiple of batch size!" 87 | return opts 88 | -------------------------------------------------------------------------------- /pretrained/cvrp_10/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "log_dir": "logs", 3 | "eval_only": false, 4 | "tanh_clipping": 10.0, 5 | "lr_model": 0.0001, 6 | "val_size": 10000, 7 | "epoch_size": 1280000, 8 | "graph_size": 10, 9 | "batch_size": 512, 10 | "val_dataset": "", 11 | "checkpoint_epochs": 50, 12 | "output_dir": "outputs", 13 | "save_dir": "", 14 | "n_epochs": 100, 15 | "resume": null, 16 | "exp_beta": 0.8, 17 | "seed": 1234, 18 | "embedding_dim": 128, 19 | "no_cuda": false, 20 | "eval_batch_size": 2048, 21 | "use_cuda": true, 22 | "lr_critic": 0.0001, 23 | "n_encode_layers": 3, 24 | "baseline": "rollout", 25 | "epoch_start": 0, 26 | "no_progress_bar": true, 27 | "bl_alpha": 0.05, 28 | "run_name": "cvrp_10", 29 | "log_step": 500, 30 | "normalization": "batch", 31 | "max_grad_norm": 1.0, 32 | "model": "attention", 33 | "no_tensorboard": false, 34 | "bl_warmup_epochs": 1, 35 | "lr_decay": 1.0, 36 | "load_path": null, 37 | "hidden_dim": 128, 38 | "problem": "cvrp" 39 | } -------------------------------------------------------------------------------- /pretrained/cvrp_10/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/cvrp_10/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/cvrp_100/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "val_size": 10000, 3 | "log_step": 500, 4 | "exp_beta": 0.8, 5 | "tanh_clipping": 10.0, 6 | "run_name": "cvrp_100", 7 | "no_tensorboard": false, 8 | "resume": null, 9 | "use_cuda": true, 10 | "log_dir": "logs", 11 | "bl_warmup_epochs": 1, 12 | "no_cuda": false, 13 | "eval_only": false, 14 | "seed": 1234, 15 | "max_grad_norm": 1.0, 16 | "graph_size": 100, 17 | "problem": "cvrp", 18 | "lr_model": 0.0001, 19 | "lr_critic": 0.0001, 20 | "normalization": "batch", 21 | "no_progress_bar": true, 22 | "save_dir": "", 23 | "n_epochs": 100, 24 | "bl_alpha": 0.05, 25 | "baseline": "rollout", 26 | "batch_size": 256, 27 | "output_dir": "outputs", 28 | "n_encode_layers": 3, 29 | "val_dataset": "", 30 | "epoch_start": 0, 31 | "model": "attention", 32 | "eval_batch_size": 2048, 33 | "embedding_dim": 128, 34 | "lr_decay": 1.0, 35 | "hidden_dim": 128, 36 | "load_path": null, 37 | "checkpoint_epochs": 50, 38 | "epoch_size": 640000 39 | } -------------------------------------------------------------------------------- /pretrained/cvrp_100/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/cvrp_100/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/cvrp_20/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "no_cuda": false, 3 | "val_size": 10000, 4 | "bl_alpha": 0.05, 5 | "seed": 1234, 6 | "batch_size": 512, 7 | "no_tensorboard": false, 8 | "log_step": 500, 9 | "resume": null, 10 | "lr_decay": 1.0, 11 | "run_name": "cvrp_20", 12 | "load_path": null, 13 | "no_progress_bar": true, 14 | "epoch_start": 0, 15 | "problem": "cvrp", 16 | "n_encode_layers": 3, 17 | "bl_warmup_epochs": 1, 18 | "exp_beta": 0.8, 19 | "baseline": "rollout", 20 | "epoch_size": 1280000, 21 | "max_grad_norm": 1.0, 22 | "val_dataset": "", 23 | "eval_only": false, 24 | "graph_size": 20, 25 | "use_cuda": true, 26 | "n_epochs": 100, 27 | "tanh_clipping": 10.0, 28 | "output_dir": "outputs", 29 | "eval_batch_size": 2048, 30 | "normalization": "batch", 31 | "hidden_dim": 128, 32 | "checkpoint_epochs": 50, 33 | "embedding_dim": 128, 34 | "model": "attention", 35 | "lr_critic": 0.0001, 36 | "log_dir": "logs", 37 | "save_dir": "", 38 | "lr_model": 0.0001 39 | } -------------------------------------------------------------------------------- /pretrained/cvrp_20/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/cvrp_20/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/cvrp_50/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "graph_size": 50, 3 | "lr_decay": 1.0, 4 | "problem": "cvrp", 5 | "no_progress_bar": true, 6 | "run_name": "cvrp_50", 7 | "embedding_dim": 128, 8 | "use_cuda": true, 9 | "save_dir": "", 10 | "bl_alpha": 0.05, 11 | "eval_only": false, 12 | "output_dir": "outputs", 13 | "no_cuda": false, 14 | "max_grad_norm": 1.0, 15 | "checkpoint_epochs": 50, 16 | "n_epochs": 100, 17 | "no_tensorboard": false, 18 | "log_step": 500, 19 | "baseline": "rollout", 20 | "load_path": null, 21 | "epoch_start": 0, 22 | "tanh_clipping": 10.0, 23 | "hidden_dim": 128, 24 | "lr_critic": 0.0001, 25 | "bl_warmup_epochs": 1, 26 | "lr_model": 0.0001, 27 | "val_size": 10000, 28 | "epoch_size": 1280000, 29 | "eval_batch_size": 2048, 30 | "normalization": "batch", 31 | "model": "attention", 32 | "resume": null, 33 | "log_dir": "logs", 34 | "exp_beta": 0.8, 35 | "n_encode_layers": 3, 36 | "seed": 1235, 37 | "batch_size": 512, 38 | "val_dataset": "" 39 | } -------------------------------------------------------------------------------- /pretrained/cvrp_50/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/cvrp_50/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/op_const_100/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "problem": "op_const", 3 | "graph_size": 100, 4 | "batch_size": 512, 5 | "epoch_size": 1280000, 6 | "val_size": 10000, 7 | "val_dataset": "", 8 | "model": "attention", 9 | "embedding_dim": 128, 10 | "hidden_dim": 128, 11 | "n_encode_layers": 3, 12 | "tanh_clipping": 10.0, 13 | "normalization": "batch", 14 | "lr_model": 0.0001, 15 | "lr_critic": 0.0001, 16 | "lr_decay": 1.0, 17 | "eval_only": false, 18 | "n_epochs": 39, 19 | "seed": 1234, 20 | "max_grad_norm": 1.0, 21 | "no_cuda": false, 22 | "exp_beta": 0.8, 23 | "baseline": "rollout", 24 | "bl_alpha": 0.05, 25 | "bl_warmup_epochs": 1, 26 | "eval_batch_size": 2048, 27 | "checkpoint_encoder": true, 28 | "log_step": 50, 29 | "log_dir": "logs", 30 | "run_name": "op_const_100", 31 | "output_dir": "outputs", 32 | "epoch_start": 0, 33 | "checkpoint_epochs": 1, 34 | "load_path": null, 35 | "resume": "outputs/op_const_100/op_const_100_rollout_20180910T180544/epoch-60.pt", 36 | "no_tensorboard": false, 37 | "no_progress_bar": false, 38 | "use_cuda": true, 39 | "save_dir": "" 40 | } -------------------------------------------------------------------------------- /pretrained/op_const_100/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/op_const_100/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/op_const_20/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "problem": "op_const", 3 | "graph_size": 20, 4 | "batch_size": 512, 5 | "epoch_size": 1280000, 6 | "val_size": 10000, 7 | "val_dataset": "", 8 | "model": "attention", 9 | "embedding_dim": 128, 10 | "hidden_dim": 128, 11 | "n_encode_layers": 3, 12 | "tanh_clipping": 10.0, 13 | "normalization": "batch", 14 | "lr_model": 0.0001, 15 | "lr_critic": 0.0001, 16 | "lr_decay": 1.0, 17 | "eval_only": false, 18 | "n_epochs": 100, 19 | "seed": 1234, 20 | "max_grad_norm": 1.0, 21 | "no_cuda": false, 22 | "exp_beta": 0.8, 23 | "baseline": "rollout", 24 | "bl_alpha": 0.05, 25 | "bl_warmup_epochs": 1, 26 | "eval_batch_size": 2048, 27 | "checkpoint_encoder": false, 28 | "log_step": 50, 29 | "log_dir": "logs", 30 | "run_name": "op_const_20", 31 | "output_dir": "outputs", 32 | "epoch_start": 0, 33 | "checkpoint_epochs": 1, 34 | "load_path": null, 35 | "resume": null, 36 | "no_tensorboard": false, 37 | "no_progress_bar": false, 38 | "use_cuda": true, 39 | "save_dir": "" 40 | } -------------------------------------------------------------------------------- /pretrained/op_const_20/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/op_const_20/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/op_const_50/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "problem": "op_const", 3 | "graph_size": 50, 4 | "batch_size": 512, 5 | "epoch_size": 1280000, 6 | "val_size": 10000, 7 | "val_dataset": "", 8 | "model": "attention", 9 | "embedding_dim": 128, 10 | "hidden_dim": 128, 11 | "n_encode_layers": 3, 12 | "tanh_clipping": 10.0, 13 | "normalization": "batch", 14 | "lr_model": 0.0001, 15 | "lr_critic": 0.0001, 16 | "lr_decay": 1.0, 17 | "eval_only": false, 18 | "n_epochs": 100, 19 | "seed": 1234, 20 | "max_grad_norm": 1.0, 21 | "no_cuda": false, 22 | "exp_beta": 0.8, 23 | "baseline": "rollout", 24 | "bl_alpha": 0.05, 25 | "bl_warmup_epochs": 1, 26 | "eval_batch_size": 2048, 27 | "checkpoint_encoder": false, 28 | "log_step": 50, 29 | "log_dir": "logs", 30 | "run_name": "op_const_50", 31 | "output_dir": "outputs", 32 | "epoch_start": 0, 33 | "checkpoint_epochs": 1, 34 | "load_path": null, 35 | "resume": null, 36 | "no_tensorboard": false, 37 | "no_progress_bar": false, 38 | "use_cuda": true, 39 | "save_dir": "" 40 | } -------------------------------------------------------------------------------- /pretrained/op_const_50/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/op_const_50/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/op_dist_100/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "problem": "op_dist", 3 | "graph_size": 100, 4 | "batch_size": 512, 5 | "epoch_size": 1280000, 6 | "val_size": 10000, 7 | "val_dataset": "", 8 | "model": "attention", 9 | "embedding_dim": 128, 10 | "hidden_dim": 128, 11 | "n_encode_layers": 3, 12 | "tanh_clipping": 10.0, 13 | "normalization": "batch", 14 | "lr_model": 0.0001, 15 | "lr_critic": 0.0001, 16 | "lr_decay": 1.0, 17 | "eval_only": false, 18 | "n_epochs": 100, 19 | "seed": 1234, 20 | "max_grad_norm": 1.0, 21 | "no_cuda": false, 22 | "exp_beta": 0.8, 23 | "baseline": "rollout", 24 | "bl_alpha": 0.05, 25 | "bl_warmup_epochs": 1, 26 | "eval_batch_size": 2048, 27 | "checkpoint_encoder": true, 28 | "log_step": 50, 29 | "log_dir": "logs", 30 | "run_name": "op_dist_100", 31 | "output_dir": "outputs", 32 | "epoch_start": 0, 33 | "checkpoint_epochs": 1, 34 | "load_path": null, 35 | "resume": null, 36 | "no_tensorboard": false, 37 | "no_progress_bar": false, 38 | "use_cuda": true, 39 | "save_dir": "" 40 | } -------------------------------------------------------------------------------- /pretrained/op_dist_100/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/op_dist_100/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/op_dist_20/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "problem": "op_dist", 3 | "graph_size": 20, 4 | "batch_size": 512, 5 | "epoch_size": 1280000, 6 | "val_size": 10000, 7 | "val_dataset": "", 8 | "model": "attention", 9 | "embedding_dim": 128, 10 | "hidden_dim": 128, 11 | "n_encode_layers": 3, 12 | "tanh_clipping": 10.0, 13 | "normalization": "batch", 14 | "lr_model": 0.0001, 15 | "lr_critic": 0.0001, 16 | "lr_decay": 1.0, 17 | "eval_only": false, 18 | "n_epochs": 100, 19 | "seed": 1234, 20 | "max_grad_norm": 1.0, 21 | "no_cuda": false, 22 | "exp_beta": 0.8, 23 | "baseline": "rollout", 24 | "bl_alpha": 0.05, 25 | "bl_warmup_epochs": 1, 26 | "eval_batch_size": 2048, 27 | "checkpoint_encoder": false, 28 | "log_step": 50, 29 | "log_dir": "logs", 30 | "run_name": "op_dist_20", 31 | "output_dir": "outputs", 32 | "epoch_start": 0, 33 | "checkpoint_epochs": 1, 34 | "load_path": null, 35 | "resume": null, 36 | "no_tensorboard": false, 37 | "no_progress_bar": false, 38 | "use_cuda": true, 39 | "save_dir": "" 40 | } -------------------------------------------------------------------------------- /pretrained/op_dist_20/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/op_dist_20/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/op_dist_50/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "problem": "op_dist", 3 | "graph_size": 50, 4 | "batch_size": 512, 5 | "epoch_size": 1280000, 6 | "val_size": 10000, 7 | "val_dataset": "", 8 | "model": "attention", 9 | "embedding_dim": 128, 10 | "hidden_dim": 128, 11 | "n_encode_layers": 3, 12 | "tanh_clipping": 10.0, 13 | "normalization": "batch", 14 | "lr_model": 0.0001, 15 | "lr_critic": 0.0001, 16 | "lr_decay": 1.0, 17 | "eval_only": false, 18 | "n_epochs": 100, 19 | "seed": 1234, 20 | "max_grad_norm": 1.0, 21 | "no_cuda": false, 22 | "exp_beta": 0.8, 23 | "baseline": "rollout", 24 | "bl_alpha": 0.05, 25 | "bl_warmup_epochs": 1, 26 | "eval_batch_size": 2048, 27 | "checkpoint_encoder": false, 28 | "log_step": 50, 29 | "log_dir": "logs", 30 | "run_name": "op_dist_50", 31 | "output_dir": "outputs", 32 | "epoch_start": 0, 33 | "checkpoint_epochs": 1, 34 | "load_path": null, 35 | "resume": null, 36 | "no_tensorboard": false, 37 | "no_progress_bar": false, 38 | "use_cuda": true, 39 | "save_dir": "" 40 | } -------------------------------------------------------------------------------- /pretrained/op_dist_50/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/op_dist_50/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/op_unif_100/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "problem": "op_unif", 3 | "graph_size": 100, 4 | "batch_size": 512, 5 | "epoch_size": 1280000, 6 | "val_size": 10000, 7 | "val_dataset": "", 8 | "model": "attention", 9 | "embedding_dim": 128, 10 | "hidden_dim": 128, 11 | "n_encode_layers": 3, 12 | "tanh_clipping": 10.0, 13 | "normalization": "batch", 14 | "lr_model": 0.0001, 15 | "lr_critic": 0.0001, 16 | "lr_decay": 1.0, 17 | "eval_only": false, 18 | "n_epochs": 35, 19 | "seed": 1234, 20 | "max_grad_norm": 1.0, 21 | "no_cuda": false, 22 | "exp_beta": 0.8, 23 | "baseline": "rollout", 24 | "bl_alpha": 0.05, 25 | "bl_warmup_epochs": 1, 26 | "eval_batch_size": 2048, 27 | "checkpoint_encoder": true, 28 | "log_step": 50, 29 | "log_dir": "logs", 30 | "run_name": "op_unif_100", 31 | "output_dir": "outputs", 32 | "epoch_start": 0, 33 | "checkpoint_epochs": 1, 34 | "load_path": null, 35 | "resume": "outputs/op_unif_100/op_unif_100_rollout_20180910T180547/epoch-64.pt", 36 | "no_tensorboard": false, 37 | "no_progress_bar": false, 38 | "use_cuda": true, 39 | "save_dir": "" 40 | } -------------------------------------------------------------------------------- /pretrained/op_unif_100/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/op_unif_100/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/op_unif_20/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "problem": "op_unif", 3 | "graph_size": 20, 4 | "batch_size": 512, 5 | "epoch_size": 1280000, 6 | "val_size": 10000, 7 | "val_dataset": "", 8 | "model": "attention", 9 | "embedding_dim": 128, 10 | "hidden_dim": 128, 11 | "n_encode_layers": 3, 12 | "tanh_clipping": 10.0, 13 | "normalization": "batch", 14 | "lr_model": 0.0001, 15 | "lr_critic": 0.0001, 16 | "lr_decay": 1.0, 17 | "eval_only": false, 18 | "n_epochs": 100, 19 | "seed": 1234, 20 | "max_grad_norm": 1.0, 21 | "no_cuda": false, 22 | "exp_beta": 0.8, 23 | "baseline": "rollout", 24 | "bl_alpha": 0.05, 25 | "bl_warmup_epochs": 1, 26 | "eval_batch_size": 2048, 27 | "checkpoint_encoder": false, 28 | "log_step": 50, 29 | "log_dir": "logs", 30 | "run_name": "op_unif_20", 31 | "output_dir": "outputs", 32 | "epoch_start": 0, 33 | "checkpoint_epochs": 1, 34 | "load_path": null, 35 | "resume": null, 36 | "no_tensorboard": false, 37 | "no_progress_bar": false, 38 | "use_cuda": true, 39 | "save_dir": "" 40 | } -------------------------------------------------------------------------------- /pretrained/op_unif_20/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/op_unif_20/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/op_unif_50/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "problem": "op_unif", 3 | "graph_size": 50, 4 | "batch_size": 512, 5 | "epoch_size": 1280000, 6 | "val_size": 10000, 7 | "val_dataset": "", 8 | "model": "attention", 9 | "embedding_dim": 128, 10 | "hidden_dim": 128, 11 | "n_encode_layers": 3, 12 | "tanh_clipping": 10.0, 13 | "normalization": "batch", 14 | "lr_model": 0.0001, 15 | "lr_critic": 0.0001, 16 | "lr_decay": 1.0, 17 | "eval_only": false, 18 | "n_epochs": 100, 19 | "seed": 1234, 20 | "max_grad_norm": 1.0, 21 | "no_cuda": false, 22 | "exp_beta": 0.8, 23 | "baseline": "rollout", 24 | "bl_alpha": 0.05, 25 | "bl_warmup_epochs": 1, 26 | "eval_batch_size": 2048, 27 | "checkpoint_encoder": false, 28 | "log_step": 50, 29 | "log_dir": "logs", 30 | "run_name": "op_unif_50", 31 | "output_dir": "outputs", 32 | "epoch_start": 0, 33 | "checkpoint_epochs": 1, 34 | "load_path": null, 35 | "resume": null, 36 | "no_tensorboard": false, 37 | "no_progress_bar": false, 38 | "use_cuda": true, 39 | "save_dir": "" 40 | } -------------------------------------------------------------------------------- /pretrained/op_unif_50/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/op_unif_50/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/pctsp_det_100/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "problem": "pctsp_det", 3 | "graph_size": 100, 4 | "batch_size": 512, 5 | "epoch_size": 1280000, 6 | "val_size": 10000, 7 | "val_dataset": "", 8 | "model": "attention", 9 | "embedding_dim": 128, 10 | "hidden_dim": 128, 11 | "n_encode_layers": 3, 12 | "tanh_clipping": 10.0, 13 | "normalization": "batch", 14 | "lr_model": 0.0001, 15 | "lr_critic": 0.0001, 16 | "lr_decay": 1.0, 17 | "eval_only": false, 18 | "n_epochs": 100, 19 | "seed": 1234, 20 | "max_grad_norm": 1.0, 21 | "no_cuda": false, 22 | "exp_beta": 0.8, 23 | "baseline": "rollout", 24 | "bl_alpha": 0.05, 25 | "bl_warmup_epochs": 1, 26 | "eval_batch_size": 2048, 27 | "checkpoint_encoder": false, 28 | "log_step": 50, 29 | "log_dir": "logs", 30 | "run_name": "pctsp_det_100", 31 | "output_dir": "outputs", 32 | "epoch_start": 0, 33 | "checkpoint_epochs": 1, 34 | "load_path": null, 35 | "resume": null, 36 | "no_tensorboard": false, 37 | "no_progress_bar": false, 38 | "use_cuda": true, 39 | "save_dir": "" 40 | } -------------------------------------------------------------------------------- /pretrained/pctsp_det_100/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/pctsp_det_100/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/pctsp_det_20/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "problem": "pctsp_det", 3 | "graph_size": 20, 4 | "batch_size": 512, 5 | "epoch_size": 1280000, 6 | "val_size": 10000, 7 | "val_dataset": "", 8 | "model": "attention", 9 | "embedding_dim": 128, 10 | "hidden_dim": 128, 11 | "n_encode_layers": 3, 12 | "tanh_clipping": 10.0, 13 | "normalization": "batch", 14 | "lr_model": 0.0001, 15 | "lr_critic": 0.0001, 16 | "lr_decay": 1.0, 17 | "eval_only": false, 18 | "n_epochs": 100, 19 | "seed": 1235, 20 | "max_grad_norm": 1.0, 21 | "no_cuda": false, 22 | "exp_beta": 0.8, 23 | "baseline": "rollout", 24 | "bl_alpha": 0.05, 25 | "bl_warmup_epochs": 1, 26 | "eval_batch_size": 2048, 27 | "checkpoint_encoder": false, 28 | "log_step": 50, 29 | "log_dir": "logs", 30 | "run_name": "pctsp_det_20", 31 | "output_dir": "outputs", 32 | "epoch_start": 0, 33 | "checkpoint_epochs": 1, 34 | "load_path": null, 35 | "resume": null, 36 | "no_tensorboard": false, 37 | "no_progress_bar": false, 38 | "use_cuda": true, 39 | "save_dir": "" 40 | } -------------------------------------------------------------------------------- /pretrained/pctsp_det_20/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/pctsp_det_20/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/pctsp_det_50/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "problem": "pctsp_det", 3 | "graph_size": 50, 4 | "batch_size": 512, 5 | "epoch_size": 1280000, 6 | "val_size": 10000, 7 | "val_dataset": "", 8 | "model": "attention", 9 | "embedding_dim": 128, 10 | "hidden_dim": 128, 11 | "n_encode_layers": 3, 12 | "tanh_clipping": 10.0, 13 | "normalization": "batch", 14 | "lr_model": 0.0001, 15 | "lr_critic": 0.0001, 16 | "lr_decay": 1.0, 17 | "eval_only": false, 18 | "n_epochs": 100, 19 | "seed": 1234, 20 | "max_grad_norm": 1.0, 21 | "no_cuda": false, 22 | "exp_beta": 0.8, 23 | "baseline": "rollout", 24 | "bl_alpha": 0.05, 25 | "bl_warmup_epochs": 1, 26 | "eval_batch_size": 2048, 27 | "checkpoint_encoder": false, 28 | "log_step": 50, 29 | "log_dir": "logs", 30 | "run_name": "pctsp_det_50", 31 | "output_dir": "outputs", 32 | "epoch_start": 0, 33 | "checkpoint_epochs": 1, 34 | "load_path": null, 35 | "resume": null, 36 | "no_tensorboard": false, 37 | "no_progress_bar": false, 38 | "use_cuda": true, 39 | "save_dir": "" 40 | } -------------------------------------------------------------------------------- /pretrained/pctsp_det_50/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/pctsp_det_50/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/pctsp_stoch_100/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "problem": "pctsp_stoch", 3 | "graph_size": 100, 4 | "batch_size": 512, 5 | "epoch_size": 1280000, 6 | "val_size": 10000, 7 | "val_dataset": "", 8 | "model": "attention", 9 | "embedding_dim": 128, 10 | "hidden_dim": 128, 11 | "n_encode_layers": 3, 12 | "tanh_clipping": 10.0, 13 | "normalization": "batch", 14 | "lr_model": 0.0001, 15 | "lr_critic": 0.0001, 16 | "lr_decay": 1.0, 17 | "eval_only": false, 18 | "n_epochs": 100, 19 | "seed": 1234, 20 | "max_grad_norm": 1.0, 21 | "no_cuda": false, 22 | "exp_beta": 0.8, 23 | "baseline": "rollout", 24 | "bl_alpha": 0.05, 25 | "bl_warmup_epochs": 1, 26 | "eval_batch_size": 2048, 27 | "checkpoint_encoder": false, 28 | "log_step": 50, 29 | "log_dir": "logs", 30 | "run_name": "pctsp_stoch_100", 31 | "output_dir": "outputs", 32 | "epoch_start": 0, 33 | "checkpoint_epochs": 1, 34 | "load_path": null, 35 | "resume": null, 36 | "no_tensorboard": false, 37 | "no_progress_bar": false, 38 | "use_cuda": true, 39 | "save_dir": "" 40 | } -------------------------------------------------------------------------------- /pretrained/pctsp_stoch_100/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/pctsp_stoch_100/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/pctsp_stoch_20/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "problem": "pctsp_stoch", 3 | "graph_size": 20, 4 | "batch_size": 512, 5 | "epoch_size": 1280000, 6 | "val_size": 10000, 7 | "val_dataset": "", 8 | "model": "attention", 9 | "embedding_dim": 128, 10 | "hidden_dim": 128, 11 | "n_encode_layers": 3, 12 | "tanh_clipping": 10.0, 13 | "normalization": "batch", 14 | "lr_model": 0.0001, 15 | "lr_critic": 0.0001, 16 | "lr_decay": 1.0, 17 | "eval_only": false, 18 | "n_epochs": 100, 19 | "seed": 1234, 20 | "max_grad_norm": 1.0, 21 | "no_cuda": false, 22 | "exp_beta": 0.8, 23 | "baseline": "rollout", 24 | "bl_alpha": 0.05, 25 | "bl_warmup_epochs": 1, 26 | "eval_batch_size": 2048, 27 | "checkpoint_encoder": false, 28 | "log_step": 50, 29 | "log_dir": "logs", 30 | "run_name": "pctsp_stoch_20", 31 | "output_dir": "outputs", 32 | "epoch_start": 0, 33 | "checkpoint_epochs": 1, 34 | "load_path": null, 35 | "resume": null, 36 | "no_tensorboard": false, 37 | "no_progress_bar": false, 38 | "use_cuda": true, 39 | "save_dir": "" 40 | } -------------------------------------------------------------------------------- /pretrained/pctsp_stoch_20/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/pctsp_stoch_20/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/pctsp_stoch_50/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "problem": "pctsp_stoch", 3 | "graph_size": 50, 4 | "batch_size": 512, 5 | "epoch_size": 1280000, 6 | "val_size": 10000, 7 | "val_dataset": "", 8 | "model": "attention", 9 | "embedding_dim": 128, 10 | "hidden_dim": 128, 11 | "n_encode_layers": 3, 12 | "tanh_clipping": 10.0, 13 | "normalization": "batch", 14 | "lr_model": 0.0001, 15 | "lr_critic": 0.0001, 16 | "lr_decay": 1.0, 17 | "eval_only": false, 18 | "n_epochs": 100, 19 | "seed": 1234, 20 | "max_grad_norm": 1.0, 21 | "no_cuda": false, 22 | "exp_beta": 0.8, 23 | "baseline": "rollout", 24 | "bl_alpha": 0.05, 25 | "bl_warmup_epochs": 1, 26 | "eval_batch_size": 2048, 27 | "checkpoint_encoder": false, 28 | "log_step": 50, 29 | "log_dir": "logs", 30 | "run_name": "pctsp_stoch_50", 31 | "output_dir": "outputs", 32 | "epoch_start": 0, 33 | "checkpoint_epochs": 1, 34 | "load_path": null, 35 | "resume": null, 36 | "no_tensorboard": false, 37 | "no_progress_bar": false, 38 | "use_cuda": true, 39 | "save_dir": "" 40 | } -------------------------------------------------------------------------------- /pretrained/pctsp_stoch_50/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/pctsp_stoch_50/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/sdvrp_10/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "no_tensorboard": false, 3 | "exp_beta": 0.8, 4 | "model": "attention", 5 | "max_grad_norm": 1.0, 6 | "no_progress_bar": true, 7 | "resume": null, 8 | "eval_batch_size": 2048, 9 | "epoch_size": 1280000, 10 | "n_encode_layers": 3, 11 | "log_dir": "logs", 12 | "hidden_dim": 128, 13 | "val_dataset": "", 14 | "seed": 1235, 15 | "normalization": "batch", 16 | "log_step": 500, 17 | "no_cuda": false, 18 | "batch_size": 512, 19 | "bl_warmup_epochs": 1, 20 | "problem": "sdvrp", 21 | "val_size": 10000, 22 | "lr_critic": 0.0001, 23 | "epoch_start": 0, 24 | "checkpoint_epochs": 50, 25 | "load_path": null, 26 | "embedding_dim": 128, 27 | "baseline": "rollout", 28 | "graph_size": 10, 29 | "eval_only": false, 30 | "lr_model": 0.0001, 31 | "save_dir": "", 32 | "use_cuda": true, 33 | "lr_decay": 1.0, 34 | "output_dir": "outputs", 35 | "run_name": "sdvrp_10", 36 | "tanh_clipping": 10.0, 37 | "bl_alpha": 0.05, 38 | "n_epochs": 100 39 | } -------------------------------------------------------------------------------- /pretrained/sdvrp_10/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/sdvrp_10/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/sdvrp_100/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "hidden_dim": 128, 3 | "val_dataset": "", 4 | "model": "attention", 5 | "baseline": "rollout", 6 | "checkpoint_epochs": 50, 7 | "bl_warmup_epochs": 1, 8 | "log_dir": "logs", 9 | "run_name": "sdvrp_100", 10 | "n_epochs": 100, 11 | "output_dir": "outputs", 12 | "val_size": 10000, 13 | "bl_alpha": 0.05, 14 | "graph_size": 100, 15 | "normalization": "batch", 16 | "load_path": null, 17 | "batch_size": 256, 18 | "lr_decay": 1.0, 19 | "max_grad_norm": 1.0, 20 | "epoch_start": 0, 21 | "no_tensorboard": false, 22 | "epoch_size": 640000, 23 | "exp_beta": 0.8, 24 | "tanh_clipping": 10.0, 25 | "eval_only": false, 26 | "resume": null, 27 | "n_encode_layers": 3, 28 | "log_step": 500, 29 | "lr_model": 0.0001, 30 | "embedding_dim": 128, 31 | "seed": 1235, 32 | "use_cuda": true, 33 | "no_progress_bar": true, 34 | "lr_critic": 0.0001, 35 | "save_dir": "", 36 | "no_cuda": false, 37 | "problem": "sdvrp", 38 | "eval_batch_size": 2048 39 | } -------------------------------------------------------------------------------- /pretrained/sdvrp_100/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/sdvrp_100/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/sdvrp_20/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseline": "rollout", 3 | "checkpoint_epochs": 50, 4 | "n_epochs": 100, 5 | "no_tensorboard": false, 6 | "eval_batch_size": 2048, 7 | "no_progress_bar": true, 8 | "normalization": "batch", 9 | "output_dir": "outputs", 10 | "epoch_size": 1280000, 11 | "model": "attention", 12 | "tanh_clipping": 10.0, 13 | "log_step": 500, 14 | "n_encode_layers": 3, 15 | "save_dir": "", 16 | "max_grad_norm": 1.0, 17 | "hidden_dim": 128, 18 | "log_dir": "logs", 19 | "lr_model": 0.0001, 20 | "bl_warmup_epochs": 1, 21 | "no_cuda": false, 22 | "graph_size": 20, 23 | "epoch_start": 0, 24 | "problem": "sdvrp", 25 | "eval_only": false, 26 | "exp_beta": 0.8, 27 | "embedding_dim": 128, 28 | "run_name": "sdvrp_20", 29 | "lr_critic": 0.0001, 30 | "load_path": null, 31 | "val_dataset": "", 32 | "use_cuda": true, 33 | "batch_size": 512, 34 | "lr_decay": 1.0, 35 | "bl_alpha": 0.05, 36 | "seed": 1234, 37 | "resume": null, 38 | "val_size": 10000 39 | } -------------------------------------------------------------------------------- /pretrained/sdvrp_20/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/sdvrp_20/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/sdvrp_50/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "lr_critic": 0.0001, 3 | "val_dataset": "", 4 | "n_encode_layers": 3, 5 | "no_tensorboard": false, 6 | "output_dir": "outputs", 7 | "no_progress_bar": true, 8 | "max_grad_norm": 1.0, 9 | "epoch_size": 1280000, 10 | "embedding_dim": 128, 11 | "lr_decay": 1.0, 12 | "tanh_clipping": 10.0, 13 | "baseline": "rollout", 14 | "lr_model": 0.0001, 15 | "run_name": "sdvrp_50", 16 | "load_path": null, 17 | "bl_warmup_epochs": 1, 18 | "save_dir": "", 19 | "hidden_dim": 128, 20 | "problem": "sdvrp", 21 | "log_dir": "logs", 22 | "no_cuda": false, 23 | "bl_alpha": 0.05, 24 | "use_cuda": true, 25 | "normalization": "batch", 26 | "checkpoint_epochs": 50, 27 | "graph_size": 50, 28 | "exp_beta": 0.8, 29 | "epoch_start": 0, 30 | "batch_size": 512, 31 | "seed": 1234, 32 | "eval_only": false, 33 | "n_epochs": 100, 34 | "model": "attention", 35 | "log_step": 500, 36 | "resume": null, 37 | "eval_batch_size": 2048, 38 | "val_size": 10000 39 | } -------------------------------------------------------------------------------- /pretrained/sdvrp_50/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/sdvrp_50/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/tsp_100/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "eval_batch_size": 2048, 3 | "val_dataset": "", 4 | "val_size": 10000, 5 | "load_path": null, 6 | "n_encode_layers": 3, 7 | "lr_critic": 0.0001, 8 | "output_dir": "outputs", 9 | "epoch_size": 1280000, 10 | "tanh_clipping": 10.0, 11 | "hidden_dim": 128, 12 | "log_dir": "logs", 13 | "seed": 1235, 14 | "n_epochs": 100, 15 | "no_tensorboard": false, 16 | "eval_only": false, 17 | "embedding_dim": 128, 18 | "lr_model": 0.0001, 19 | "baseline": "rollout", 20 | "resume": null, 21 | "bl_warmup_epochs": 1, 22 | "no_progress_bar": true, 23 | "run_name": "tsp_100", 24 | "problem": "tsp", 25 | "use_cuda": true, 26 | "max_grad_norm": 1.0, 27 | "epoch_start": 0, 28 | "batch_size": 512, 29 | "log_step": 500, 30 | "save_dir": "", 31 | "no_cuda": false, 32 | "graph_size": 100, 33 | "normalization": "batch", 34 | "model": "attention", 35 | "bl_alpha": 0.05, 36 | "exp_beta": 0.8, 37 | "lr_decay": 1.0, 38 | "checkpoint_epochs": 50 39 | } -------------------------------------------------------------------------------- /pretrained/tsp_100/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/tsp_100/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/tsp_20/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "save_dir": "", 3 | "resume": null, 4 | "baseline": "rollout", 5 | "batch_size": 512, 6 | "log_step": 500, 7 | "exp_beta": 0.8, 8 | "val_size": 10000, 9 | "n_encode_layers": 3, 10 | "load_path": null, 11 | "embedding_dim": 128, 12 | "eval_batch_size": 2048, 13 | "seed": 1235, 14 | "max_grad_norm": 1.0, 15 | "output_dir": "outputs", 16 | "graph_size": 20, 17 | "no_progress_bar": true, 18 | "lr_decay": 1.0, 19 | "checkpoint_epochs": 50, 20 | "run_name": "tsp_20", 21 | "bl_alpha": 0.05, 22 | "epoch_start": 0, 23 | "log_dir": "logs", 24 | "no_tensorboard": false, 25 | "normalization": "batch", 26 | "no_cuda": false, 27 | "model": "attention", 28 | "eval_only": false, 29 | "use_cuda": true, 30 | "problem": "tsp", 31 | "lr_critic": 0.0001, 32 | "hidden_dim": 128, 33 | "val_dataset": "", 34 | "n_epochs": 100, 35 | "epoch_size": 1280000, 36 | "lr_model": 0.0001, 37 | "bl_warmup_epochs": 1, 38 | "tanh_clipping": 10.0 39 | } -------------------------------------------------------------------------------- /pretrained/tsp_20/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/tsp_20/epoch-99.pt -------------------------------------------------------------------------------- /pretrained/tsp_50/args.json: -------------------------------------------------------------------------------- 1 | { 2 | "resume": null, 3 | "use_cuda": true, 4 | "lr_critic": 0.0001, 5 | "output_dir": "outputs", 6 | "log_step": 500, 7 | "epoch_start": 0, 8 | "run_name": "tsp_50", 9 | "hidden_dim": 128, 10 | "log_dir": "logs", 11 | "model": "attention", 12 | "n_encode_layers": 3, 13 | "checkpoint_epochs": 50, 14 | "epoch_size": 1280000, 15 | "baseline": "rollout", 16 | "problem": "tsp", 17 | "graph_size": 50, 18 | "n_epochs": 100, 19 | "lr_decay": 1.0, 20 | "no_cuda": false, 21 | "eval_batch_size": 2048, 22 | "eval_only": false, 23 | "exp_beta": 0.8, 24 | "seed": 1235, 25 | "no_tensorboard": false, 26 | "no_progress_bar": true, 27 | "max_grad_norm": 1.0, 28 | "batch_size": 512, 29 | "val_dataset": "", 30 | "load_path": null, 31 | "bl_alpha": 0.05, 32 | "val_size": 10000, 33 | "save_dir": "", 34 | "normalization": "batch", 35 | "embedding_dim": 128, 36 | "tanh_clipping": 10.0, 37 | "lr_model": 0.0001, 38 | "bl_warmup_epochs": 1 39 | } -------------------------------------------------------------------------------- /pretrained/tsp_50/epoch-99.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/pretrained/tsp_50/epoch-99.pt -------------------------------------------------------------------------------- /problems/__init__.py: -------------------------------------------------------------------------------- 1 | from problems.tsp.problem_tsp import TSP 2 | from problems.vrp.problem_vrp import CVRP, SDVRP 3 | from problems.op.problem_op import OP 4 | from problems.pctsp.problem_pctsp import PCTSPDet, PCTSPStoch -------------------------------------------------------------------------------- /problems/op/.gitignore: -------------------------------------------------------------------------------- 1 | compass/ -------------------------------------------------------------------------------- /problems/op/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/problems/op/__init__.py -------------------------------------------------------------------------------- /problems/op/install_compass.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | git clone https://github.com/bcamath-ds/compass 3 | cd compass 4 | sudo apt-get install libtool m4 5 | sudo apt-get install libgsl0-dev libatlas-base-dev libbfd-dev libiberty-dev 6 | sudo apt-get install libssl-dev 7 | sudo apt-get install autoconf automake 8 | autoheader 9 | libtoolize 10 | aclocal 11 | automake --add-missing 12 | autoconf 13 | ./configure 14 | make 15 | cd .. -------------------------------------------------------------------------------- /problems/op/op_gurobi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2017, Gurobi Optimization, Inc. 4 | 5 | # Solve a traveling salesman problem on a set of 6 | # points using lazy constraints. The base MIP model only includes 7 | # 'degree-2' constraints, requiring each node to have exactly 8 | # two incident edges. Solutions to this model may contain subtours - 9 | # tours that don't visit every city. The lazy constraint callback 10 | # adds new constraints to cut them off. 11 | 12 | from gurobipy import * 13 | 14 | 15 | def solve_euclidian_op(depot, loc, prize, max_length, threads=0, timeout=None, gap=None): 16 | """ 17 | Solves the Euclidan op problem to optimality using the MIP formulation 18 | with lazy subtour elimination constraint generation. 19 | :param points: list of (x, y) coordinate 20 | :return: 21 | """ 22 | 23 | points = [depot] + loc 24 | n = len(points) 25 | 26 | # Callback - use lazy constraints to eliminate sub-tours 27 | 28 | def subtourelim(model, where): 29 | if where == GRB.Callback.MIPSOL: 30 | # make a list of edges selected in the solution 31 | vals = model.cbGetSolution(model._vars) 32 | selected = tuplelist((i, j) for i, j in model._vars.keys() if vals[i, j] > 0.5) 33 | # find the shortest cycle in the selected edge list 34 | tour = subtour(selected) 35 | if tour is not None: 36 | # add subtour elimination constraint for every pair of cities in tour 37 | # model.cbLazy(quicksum(model._vars[i, j] 38 | # for i, j in itertools.combinations(tour, 2)) 39 | # <= len(tour) - 1) 40 | 41 | model.cbLazy(quicksum(model._vars[i, j] 42 | for i, j in itertools.combinations(tour, 2)) 43 | <= quicksum(model._dvars[i] for i in tour) * (len(tour) - 1) / float(len(tour))) 44 | 45 | # Given a tuplelist of edges, find the shortest subtour 46 | 47 | def subtour(edges, exclude_depot=True): 48 | unvisited = list(range(n)) 49 | #cycle = range(n + 1) # initial length has 1 more city 50 | cycle = None 51 | while unvisited: # true if list is non-empty 52 | thiscycle = [] 53 | neighbors = unvisited 54 | while neighbors: 55 | current = neighbors[0] 56 | thiscycle.append(current) 57 | unvisited.remove(current) 58 | neighbors = [j for i, j in edges.select(current, '*') if j in unvisited] 59 | # If we do not yet have a cycle or this is the shorter cycle, keep this cycle 60 | # Unless it contains the depot while we do not want the depot 61 | if ( 62 | (cycle is None or len(cycle) > len(thiscycle)) 63 | and len(thiscycle) > 1 and not (0 in thiscycle and exclude_depot) 64 | ): 65 | cycle = thiscycle 66 | return cycle 67 | 68 | # Dictionary of Euclidean distance between each pair of points 69 | 70 | dist = {(i,j) : 71 | math.sqrt(sum((points[i][k]-points[j][k])**2 for k in range(2))) 72 | for i in range(n) for j in range(i)} 73 | 74 | m = Model() 75 | m.Params.outputFlag = False 76 | 77 | # Create variables 78 | 79 | vars = m.addVars(dist.keys(), vtype=GRB.BINARY, name='e') 80 | for i,j in vars.keys(): 81 | vars[j,i] = vars[i,j] # edge in opposite direction 82 | 83 | # Depot vars can be 2 84 | for i,j in vars.keys(): 85 | if i == 0 or j == 0: 86 | vars[i,j].vtype = GRB.INTEGER 87 | vars[i,j].ub = 2 88 | 89 | prize_dict = { 90 | i + 1: -p # We need to maximize so negate 91 | for i, p in enumerate(prize) 92 | } 93 | delta = m.addVars(range(1, n), obj=prize_dict, vtype=GRB.BINARY, name='delta') 94 | 95 | # Add degree-2 constraint (2 * delta for nodes which are not the depot) 96 | m.addConstrs(vars.sum(i,'*') == (2 if i == 0 else 2 * delta[i]) for i in range(n)) 97 | 98 | # Length of tour constraint 99 | m.addConstr(quicksum(var * dist[i, j] for (i, j), var in vars.items() if j < i) <= max_length) 100 | 101 | # Optimize model 102 | 103 | m._vars = vars 104 | m._dvars = delta 105 | m.Params.lazyConstraints = 1 106 | m.Params.threads = threads 107 | if timeout: 108 | m.Params.timeLimit = timeout 109 | if gap: 110 | m.Params.mipGap = gap * 0.01 # Percentage 111 | m.optimize(subtourelim) 112 | 113 | vals = m.getAttr('x', vars) 114 | selected = tuplelist((i,j) for i,j in vals.keys() if vals[i,j] > 0.5) 115 | 116 | tour = subtour(selected, exclude_depot=False) 117 | assert tour[0] == 0, "Tour should start with depot" 118 | 119 | return m.objVal, tour -------------------------------------------------------------------------------- /problems/op/opga/README.md: -------------------------------------------------------------------------------- 1 | # orienteering 2 | GA for orienteering problem 3 | -------------------------------------------------------------------------------- /problems/op/opga/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/problems/op/opga/__init__.py -------------------------------------------------------------------------------- /problems/op/opga/opevo.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import random 3 | import time 4 | from . import oph 5 | 6 | #fitness will take a set s and a set of weights and return a tuple containing the fitness and the best path 7 | def fitness( chrom, s, start_point, end_point, tmax ): 8 | augs = [] 9 | for i in range( len( s ) ): 10 | augs.append( ( s[ i ][0], 11 | s[ i ][1], 12 | s[ i ][2], 13 | s[ i ][3], 14 | s[ i ][4] + chrom[ i ] ) ) 15 | if debug: 16 | print ('fitness---------------------------------') 17 | print ('augs:') 18 | print (augs) 19 | #best = oph.ellinit_replacement( augs, start_point, end_point, tmax ) 20 | ellset = oph.ell_sub( tmax, start_point, end_point, augs ) 21 | #best = oph.initialize( ellset, start_point, end_point, tmax )[0] 22 | best = oph.init_replacement( ellset, start_point, end_point, tmax )[0] 23 | if debug: 24 | print ('best:') 25 | print (best) 26 | print ('best real reward:') 27 | print ([ x[3] for x in best ]) 28 | print (len( s )) 29 | print ([ s[ x[3] - 2 ] for x in best[ 1:len( best ) - 1 ] ]) 30 | print ([ s[ x[3] - 2 ][2] for x in best[ 1:len( best ) - 1 ] ]) 31 | print (( sum( [ s[ x[3] - 2 ][2] for x in best[ 1:len( best ) - 1 ] ] ), best )) 32 | return ( sum( [ s[ x[3] - 2 ][2] for x in best[ 1:len( best ) - 1 ] ] ), best ) 33 | 34 | def crossover( c1, c2 ): 35 | assert( len( c1 ) == len( c2 ) ) 36 | point = random.randrange( len( c1 ) ) 37 | first = random.randrange( 2 ) 38 | if( first ): 39 | return c1[:point] + c2[point:] 40 | else: 41 | return c2[:point] + c1[point:] 42 | 43 | def mutate( chrom, mchance, msigma ): 44 | return [ x + random.gauss( 0, msigma ) if random.randrange( mchance ) == 0 else 45 | x for x in chrom ] 46 | 47 | def run_alg_f( f, tmax, N ): 48 | random.seed() 49 | cpoints = [] 50 | an_unused_value = f.readline() # ignore first line of file 51 | for i in range( N ): 52 | cpoints.append( tuple( [ float( x ) for x in f.readline().split() ] ) ) 53 | if debug: 54 | print ('N: ', N) 55 | return run_alg(cpoints, tmax) 56 | 57 | def run_alg(points, tmax, return_sol=False, verbose=True): 58 | cpoints = [tuple(p) + (i, 0) for i, p in enumerate(points)] 59 | start_point = cpoints.pop( 0 ) 60 | end_point = cpoints.pop( 0 ) 61 | assert( oph.distance( start_point, end_point ) < tmax ) 62 | popsize = 10 63 | genlimit = 10 64 | kt = 5 65 | isigma = 10 66 | msigma = 7 67 | mchance = 2 68 | elitismn = 2 69 | if( debug ): 70 | print ('data set size:', len( cpoints ) + 2) 71 | print ('tmax: ', tmax) 72 | print ('parameters:') 73 | print ('generations: ', genlimit) 74 | print ('population size: ', popsize) 75 | print ('ktournament size:', kt) 76 | print ('mutation chance: ', mchance) 77 | print (str( elitismn ) + '-elitism') 78 | 79 | start_time = time.clock() 80 | #generate initial random population 81 | pop = [] 82 | for i in range( popsize + elitismn ): 83 | chrom = [] 84 | for j in range( len( cpoints ) ): 85 | chrom.append( random.gauss( 0, isigma ) ) 86 | chrom = ( fitness( chrom, cpoints, start_point, end_point, tmax )[0], chrom ) 87 | while( i - j > 0 and j < elitismn and chrom > pop[ i - 1 - j ] ): 88 | j += 1 89 | pop.insert( i - j, chrom ) 90 | 91 | bestfit = 0 92 | for i in range( genlimit ): 93 | nextgen = [] 94 | for j in range( popsize ): 95 | #select parents in k tournaments 96 | parents = sorted( random.sample( pop, kt ) )[ kt - 2: ] #optimize later 97 | #crossover and mutate 98 | offspring = mutate( crossover( parents[0][1], parents[1][1] ), mchance, msigma ) 99 | offspring = ( fitness( offspring, cpoints, start_point, end_point, tmax )[0], offspring ) 100 | if( offspring[0] > bestfit ): 101 | bestfit = offspring[0] 102 | if verbose: 103 | print (bestfit) 104 | if( elitismn > 0 and offspring > pop[ popsize ] ): 105 | l = 0 106 | while( l < elitismn and offspring > pop[ popsize + l ] ): 107 | l += 1 108 | pop.insert( popsize + l, offspring ) 109 | nextgen.append( pop.pop( popsize ) ) 110 | else: 111 | nextgen.append( offspring ) 112 | pop = nextgen + pop[ popsize: ] 113 | 114 | bestchrom = sorted( pop )[ popsize + elitismn - 1 ] 115 | end_time = time.clock() 116 | 117 | if verbose: 118 | print ('time:') 119 | print (end_time - start_time) 120 | print ('best fitness:') 121 | print (bestchrom[0]) 122 | print ('best path:') 123 | best_path = fitness( bestchrom[1], cpoints, start_point, end_point, tmax )[1] 124 | if verbose: 125 | print ([ x[3] for x in best_path ]) 126 | 127 | print ('their stuff:') 128 | stuff = oph.initialize( oph.ell_sub( tmax, start_point, end_point, cpoints ) 129 | , start_point, end_point, tmax )[0] 130 | if verbose: 131 | print ('fitness:', sum( [ x[2] for x in stuff ] )) 132 | print ('my stuff:') 133 | stuff2 = oph.ellinit_replacement( cpoints, start_point, end_point, tmax ) 134 | if verbose: 135 | print ('fitness:', sum( [ x[2] for x in stuff2 ] )) 136 | print ('checking correctness...') 137 | total_distance = ( oph.distance( start_point, cpoints[ best_path[ 1 ][3] - 2 ] ) + 138 | oph.distance( end_point, cpoints[ best_path[ len( best_path ) - 2 ][3] - 2 ] ) ) 139 | for i in range( 1, len( best_path ) - 3 ): 140 | total_distance += oph.distance( cpoints[ best_path[ i ][3] - 2 ], 141 | cpoints[ best_path[ i + 1 ][3] - 2 ] ) 142 | if verbose: 143 | print ('OK' if total_distance <= tmax else 'not OK') 144 | print ('tmax: ', tmax) 145 | print ('total distance:', total_distance) 146 | if return_sol: 147 | return ( bestchrom[0], best_path, end_time - start_time ) 148 | return ( bestchrom[0], end_time - start_time ) 149 | 150 | if( __name__ == '__main__' ): 151 | debug = True if 'd' in sys.argv else False 152 | run_alg( open( sys.argv[1] ), int( sys.argv[2] ), int( sys.argv[3] ) ) 153 | else: 154 | debug = False 155 | -------------------------------------------------------------------------------- /problems/op/opga/oph.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | def distance( p1, p2 ): 4 | return math.sqrt( ( p1[0] - p2[0] ) ** 2 + ( p1[1] - p2[1] ) ** 2 ) 5 | 6 | #returns a path (list of points) through s with high value 7 | def ellinit_replacement( s1, start_point, end_point, tmax ): 8 | s = list( s1 ) 9 | path = [ start_point, end_point ] 10 | length = distance( start_point, end_point ) 11 | found = True 12 | while( found == True and len( s ) > 0 ): 13 | min_added_length = -1 14 | max_added_reward = 0 15 | for j in range( len( s ) ): 16 | for k in range( len( path ) - 1 ): 17 | added_length = ( distance( path[ k ], s[ j ] ) + 18 | distance( path[ k + 1 ], s[ j ] ) - 19 | distance( path[ k ], path[ k + 1 ] ) ) # optimize later 20 | if( length + added_length < tmax and s[ j ][2] > max_added_reward ): 21 | min_added_length = added_length 22 | max_added_reward = s[ j ][2] 23 | minpoint = j 24 | pathpoint = k + 1 25 | if( min_added_length > 0 ): 26 | #add to path 27 | path.insert( pathpoint, s.pop( minpoint ) ) 28 | length = length + min_added_length 29 | else: 30 | found = False 31 | return path 32 | 33 | #returns a list of L paths with the best path in the first position 34 | #by weight rather than length 35 | def init_replacement( s1, start_point, end_point, tmax ): 36 | s = list( s1 ) 37 | L = len( s ) if len( s ) <= 10 else 10 38 | if( L == 0 ): 39 | #print 'something is probably wrong' 40 | #actually maybe not 41 | return [ [ start_point, end_point ] ] 42 | 43 | #decorate and sort by weight 44 | dsub = sorted( [ ( x[4], x ) for x in s ] )[::-1] #this is different 45 | ls = dsub[ :L ] 46 | rest = dsub[ L: ] 47 | paths = [] 48 | for i in range( L ): 49 | path = [ start_point, ls[ i ][1] , end_point ] 50 | length = distance( path[0], path[1] ) + distance( path[1], path[2] ) 51 | assert( length < tmax ) 52 | arest = ls[ :i ] + ls[ i + 1: ] + rest 53 | arest = [ x[1] for x in arest ] #undecorate 54 | assert( len( arest ) + len( path ) == len( s ) + 2 ) 55 | found = True 56 | while( found == True and len( arest ) > 0 ): 57 | min_added_length = -1 58 | max_weight = 0 59 | for j in range( len( arest ) ): 60 | for k in range( len( path ) - 1 ): 61 | added_length = ( distance( path[ k ], arest[ j ] ) + 62 | distance( path[ k + 1 ], arest[ j ] ) - 63 | distance( path[ k ], path[ k + 1 ] ) ) # optimize later 64 | if( length + added_length < tmax and arest[ j ][4] < max_weight ): 65 | min_added_length = added_length 66 | max_weight = arest[ j ][4] 67 | minpoint = j 68 | pathpoint = k + 1 69 | if( min_added_length > 0 ): 70 | #add to path 71 | path.insert( pathpoint, arest.pop( minpoint ) ) 72 | length = length + min_added_length 73 | else: 74 | found = False 75 | if( length < tmax ): 76 | paths.append( path ) 77 | 78 | assert( len( paths ) > 0 ) 79 | return [ x[1] for x in sorted( [ ( sum( [ y[2] for y in z ] ), z ) for z in paths ] )[::-1] ] 80 | 81 | 82 | #returns the subset of s that is on/in the ellipse defined by foci f1, f2 and the major axis 83 | def ell_sub( axis, f1, f2, s ): 84 | result = [] 85 | for item in s: 86 | if( distance( item, f1 ) + distance( item, f2 ) <= axis ): 87 | result.append( item ) 88 | return result 89 | 90 | #returns a list of L paths with the best path in the first position 91 | def initialize( s, start_point, end_point, tmax ): 92 | L = len( s ) if len( s ) <= 10 else 10 93 | if( L == 0 ): 94 | return [ [ start_point, end_point ] ] 95 | 96 | dsub = sorted( [ ( distance( x, start_point ) + distance( x, end_point ), x ) for x in s ] 97 | )[::-1] #optimize later 98 | ls = dsub[ :L ] 99 | rest = dsub[ L: ] 100 | paths = [] 101 | for i in range( L ): 102 | path = [ start_point, ls[ i ][1] , end_point ] 103 | length = ls[ i ][0] 104 | assert( length == distance( path[0], path[1] ) + distance( path[1], path[2] ) ) 105 | arest = ls[ :i ] + ls[ i + 1: ] + rest 106 | arest = [ x[1] for x in arest ] #undecorate 107 | assert( len( arest ) + len( path ) == len( s ) + 2 ) 108 | found = True 109 | while( found == True and len( arest ) > 0 ): 110 | min_added = -1 111 | for j in range( len( arest ) ): 112 | for k in range( len( path ) - 1 ): 113 | added_length = ( distance( path[ k ], arest[ j ] ) + 114 | distance( path[ k + 1 ], arest[ j ] ) - 115 | distance( path[ k ], path[ k + 1 ] ) ) # optimize later 116 | if( length + added_length < tmax and ( added_length < min_added or min_added < 0 ) ): 117 | min_added = added_length 118 | minpoint = j 119 | pathpoint = k + 1 120 | if( min_added > 0 ): 121 | #add to path 122 | path.insert( pathpoint, arest.pop( minpoint ) ) 123 | length = length + min_added 124 | else: 125 | found = False 126 | paths.append( path ) 127 | 128 | assert( len( [ x[1] for x in sorted( [ ( sum( [ y[2] for y in z ] ), z ) for z in paths ] 129 | )[::-1] ] ) > 0 ) 130 | return [ x[1] for x in sorted( [ ( sum( [ y[2] for y in z ] ), z ) for z in paths ] )[::-1] ] 131 | 132 | -------------------------------------------------------------------------------- /problems/op/opga/optest.py: -------------------------------------------------------------------------------- 1 | import time 2 | import opevo 3 | 4 | files = [ 'test instances/set_64_1_15.txt' ] 5 | tmaxs = [ range( 15, 80 + 1, 5 ) ] 6 | Ns = [ 64 ] 7 | 8 | test_runs = 30 9 | 10 | assert( len( files ) == len( tmaxs ) and len( tmaxs ) == len( Ns ) ) 11 | 12 | for i in range( len( files ) ): 13 | f = open( files[ i ] ) 14 | of = open( files[ i ][ :len( files[ i ] ) - 4 ] + '_results.dat', 'a' ) 15 | 16 | of.write( time.asctime() + '\n' ) 17 | of.write( 't avgfit avgtime bestfit\n' ) 18 | for t in tmaxs[ i ]: 19 | fit_sum = float( 0 ) 20 | time_sum = float( 0 ) 21 | best_fit = 0 22 | for j in range( test_runs ): 23 | print('TEST %i/%i' % ( j + 1, test_runs )) 24 | f.seek( 0 ) 25 | result = opevo.run_alg_f( f, t, Ns[ i ] ) 26 | fit_sum += result[0] 27 | time_sum += result[1] 28 | best_fit = result[0] if result[0] > best_fit else best_fit 29 | #find avg fit, time, best fit then write to file 30 | of.write( ' '.join( [ str( x ) for x in [ t, fit_sum / test_runs, time_sum / test_runs, 31 | best_fit ] ] ) + '\n' ) 32 | f.close() 33 | of.close() 34 | -------------------------------------------------------------------------------- /problems/op/opga/test instances/set_64_1_15.txt: -------------------------------------------------------------------------------- 1 | 15 1 2 | 0.000 -7.000 0 3 | 0.000 7.000 0 4 | -1.000 -6.000 6 5 | 1.000 -6.000 6 6 | -2.000 -5.000 12 7 | 0.000 -5.000 6 8 | 2.000 -5.000 12 9 | -3.000 -4.000 18 10 | -1.000 -4.000 12 11 | 1.000 -4.000 12 12 | 3.000 -4.000 18 13 | -4.000 -3.000 24 14 | -2.000 -3.000 18 15 | 0.000 -3.000 12 16 | 2.000 -3.000 18 17 | 4.000 -3.000 24 18 | -5.000 -2.000 30 19 | -3.000 -2.000 24 20 | -1.000 -2.000 18 21 | 1.000 -2.000 18 22 | 3.000 -2.000 24 23 | 5.000 -2.000 30 24 | -6.000 -1.000 36 25 | -4.000 -1.000 30 26 | -2.000 -1.000 24 27 | 0.000 -1.000 18 28 | 2.000 -1.000 24 29 | 4.000 -1.000 30 30 | 6.000 -1.000 36 31 | -7.000 0.000 42 32 | -5.000 0.000 36 33 | -3.000 0.000 30 34 | -1.000 0.000 24 35 | 1.000 0.000 24 36 | 3.000 0.000 30 37 | 5.000 0.000 36 38 | 7.000 0.000 42 39 | -6.000 1.000 36 40 | -4.000 1.000 30 41 | -2.000 1.000 24 42 | 0.000 1.000 18 43 | 2.000 1.000 24 44 | 4.000 1.000 30 45 | 6.000 1.000 36 46 | -5.000 2.000 30 47 | -3.000 2.000 24 48 | -1.000 2.000 18 49 | 1.000 2.000 18 50 | 3.000 2.000 24 51 | 5.000 2.000 30 52 | -4.000 3.000 24 53 | -2.000 3.000 18 54 | 0.000 3.000 12 55 | 2.000 3.000 18 56 | 4.000 3.000 24 57 | -3.000 4.000 18 58 | -1.000 4.000 12 59 | 1.000 4.000 12 60 | 3.000 4.000 18 61 | -2.000 5.000 12 62 | 0.000 5.000 6 63 | 2.000 5.000 12 64 | -1.000 6.000 6 65 | 1.000 6.000 6 66 | -------------------------------------------------------------------------------- /problems/op/problem_op.py: -------------------------------------------------------------------------------- 1 | from torch.utils.data import Dataset 2 | import torch 3 | import os 4 | import pickle 5 | from problems.op.state_op import StateOP 6 | from utils.beam_search import beam_search 7 | 8 | 9 | class OP(object): 10 | 11 | NAME = 'op' # Orienteering problem 12 | 13 | @staticmethod 14 | def get_costs(dataset, pi): 15 | if pi.size(-1) == 1: # In case all tours directly return to depot, prevent further problems 16 | assert (pi == 0).all(), "If all length 1 tours, they should be zero" 17 | # Return 18 | return torch.zeros(pi.size(0), dtype=torch.float, device=pi.device), None 19 | 20 | # Check that tours are valid, i.e. contain 0 to n -1 21 | sorted_pi = pi.data.sort(1)[0] 22 | # Make sure each node visited once at most (except for depot) 23 | assert ((sorted_pi[:, 1:] == 0) | (sorted_pi[:, 1:] > sorted_pi[:, :-1])).all(), "Duplicates" 24 | 25 | prize_with_depot = torch.cat( 26 | ( 27 | torch.zeros_like(dataset['prize'][:, :1]), 28 | dataset['prize'] 29 | ), 30 | 1 31 | ) 32 | p = prize_with_depot.gather(1, pi) 33 | 34 | # Gather dataset in order of tour 35 | loc_with_depot = torch.cat((dataset['depot'][:, None, :], dataset['loc']), 1) 36 | d = loc_with_depot.gather(1, pi[..., None].expand(*pi.size(), loc_with_depot.size(-1))) 37 | 38 | length = ( 39 | (d[:, 1:] - d[:, :-1]).norm(p=2, dim=-1).sum(1) # Prevent error if len 1 seq 40 | + (d[:, 0] - dataset['depot']).norm(p=2, dim=-1) # Depot to first 41 | + (d[:, -1] - dataset['depot']).norm(p=2, dim=-1) # Last to depot, will be 0 if depot is last 42 | ) 43 | assert (length <= dataset['max_length'] + 1e-5).all(), \ 44 | "Max length exceeded by {}".format((length - dataset['max_length']).max()) 45 | 46 | # We want to maximize total prize but code minimizes so return negative 47 | return -p.sum(-1), None 48 | 49 | @staticmethod 50 | def make_dataset(*args, **kwargs): 51 | return OPDataset(*args, **kwargs) 52 | 53 | @staticmethod 54 | def make_state(*args, **kwargs): 55 | return StateOP.initialize(*args, **kwargs) 56 | 57 | @staticmethod 58 | def beam_search(input, beam_size, expand_size=None, 59 | compress_mask=False, model=None, max_calc_batch_size=4096): 60 | 61 | assert model is not None, "Provide model" 62 | 63 | fixed = model.precompute_fixed(input) 64 | 65 | def propose_expansions(beam): 66 | return model.propose_expansions( 67 | beam, fixed, expand_size, normalize=True, max_calc_batch_size=max_calc_batch_size 68 | ) 69 | 70 | state = OP.make_state( 71 | input, visited_dtype=torch.int64 if compress_mask else torch.uint8 72 | ) 73 | 74 | return beam_search(state, beam_size, propose_expansions) 75 | 76 | 77 | def generate_instance(size, prize_type): 78 | # Details see paper 79 | MAX_LENGTHS = { 80 | 20: 2., 81 | 50: 3., 82 | 100: 4. 83 | } 84 | 85 | loc = torch.FloatTensor(size, 2).uniform_(0, 1) 86 | depot = torch.FloatTensor(2).uniform_(0, 1) 87 | # Methods taken from Fischetti et al. 1998 88 | if prize_type == 'const': 89 | prize = torch.ones(size) 90 | elif prize_type == 'unif': 91 | prize = (1 + torch.randint(0, 100, size=(size, ))) / 100. 92 | else: # Based on distance to depot 93 | assert prize_type == 'dist' 94 | prize_ = (depot[None, :] - loc).norm(p=2, dim=-1) 95 | prize = (1 + (prize_ / prize_.max(dim=-1, keepdim=True)[0] * 99).int()).float() / 100. 96 | 97 | return { 98 | 'loc': loc, 99 | # Uniform 1 - 9, scaled by capacities 100 | 'prize': prize, 101 | 'depot': depot, 102 | 'max_length': torch.tensor(MAX_LENGTHS[size]) 103 | } 104 | 105 | 106 | class OPDataset(Dataset): 107 | 108 | def __init__(self, filename=None, size=50, num_samples=1000000, offset=0, distribution='const'): 109 | super(OPDataset, self).__init__() 110 | assert distribution is not None, "Data distribution must be specified for OP" 111 | # Currently the distribution can only vary in the type of the prize 112 | prize_type = distribution 113 | 114 | self.data_set = [] 115 | if filename is not None: 116 | assert os.path.splitext(filename)[1] == '.pkl' 117 | 118 | with open(filename, 'rb') as f: 119 | data = pickle.load(f) 120 | self.data = [ 121 | { 122 | 'loc': torch.FloatTensor(loc), 123 | 'prize': torch.FloatTensor(prize), 124 | 'depot': torch.FloatTensor(depot), 125 | 'max_length': torch.tensor(max_length) 126 | } 127 | for depot, loc, prize, max_length in (data[offset:offset+num_samples]) 128 | ] 129 | else: 130 | self.data = [ 131 | generate_instance(size, prize_type) 132 | for i in range(num_samples) 133 | ] 134 | 135 | self.size = len(self.data) 136 | 137 | def __len__(self): 138 | return self.size 139 | 140 | def __getitem__(self, idx): 141 | return self.data[idx] 142 | -------------------------------------------------------------------------------- /problems/op/state_op.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import NamedTuple 3 | from utils.boolmask import mask_long2bool, mask_long_scatter 4 | import torch.nn.functional as F 5 | 6 | 7 | class StateOP(NamedTuple): 8 | # Fixed input 9 | coords: torch.Tensor # Depot + loc 10 | prize: torch.Tensor 11 | # Max length is not a single value, but one for each node indicating max length tour should have when arriving 12 | # at this node, so this is max_length - d(depot, node) 13 | max_length: torch.Tensor 14 | 15 | # If this state contains multiple copies (i.e. beam search) for the same instance, then for memory efficiency 16 | # the coords and prizes tensors are not kept multiple times, so we need to use the ids to index the correct rows. 17 | ids: torch.Tensor # Keeps track of original fixed data index of rows 18 | 19 | # State 20 | prev_a: torch.Tensor 21 | visited_: torch.Tensor # Keeps track of nodes that have been visited 22 | lengths: torch.Tensor 23 | cur_coord: torch.Tensor 24 | cur_total_prize: torch.Tensor 25 | i: torch.Tensor # Keeps track of step 26 | 27 | @property 28 | def visited(self): 29 | if self.visited_.dtype == torch.uint8: 30 | return self.visited_ 31 | else: 32 | return mask_long2bool(self.visited_, n=self.coords.size(-2)) 33 | 34 | @property 35 | def dist(self): 36 | return (self.coords[:, :, None, :] - self.coords[:, None, :, :]).norm(p=2, dim=-1) 37 | 38 | def __getitem__(self, key): 39 | assert torch.is_tensor(key) or isinstance(key, slice) # If tensor, idx all tensors by this tensor: 40 | return self._replace( 41 | ids=self.ids[key], 42 | prev_a=self.prev_a[key], 43 | visited_=self.visited_[key], 44 | lengths=self.lengths[key], 45 | cur_coord=self.cur_coord[key], 46 | cur_total_prize=self.cur_total_prize[key], 47 | ) 48 | 49 | # Warning: cannot override len of NamedTuple, len should be number of fields, not batch size 50 | # def __len__(self): 51 | # return len(self.used_capacity) 52 | 53 | @staticmethod 54 | def initialize(input, visited_dtype=torch.uint8): 55 | depot = input['depot'] 56 | loc = input['loc'] 57 | prize = input['prize'] 58 | max_length = input['max_length'] 59 | 60 | batch_size, n_loc, _ = loc.size() 61 | coords = torch.cat((depot[:, None, :], loc), -2) 62 | return StateOP( 63 | coords=coords, 64 | prize=F.pad(prize, (1, 0), mode='constant', value=0), # add 0 for depot 65 | # max_length is max length allowed when arriving at node, so subtract distance to return to depot 66 | # Additionally, substract epsilon margin for numeric stability 67 | max_length=max_length[:, None] - (depot[:, None, :] - coords).norm(p=2, dim=-1) - 1e-6, 68 | ids=torch.arange(batch_size, dtype=torch.int64, device=loc.device)[:, None], # Add steps dimension 69 | prev_a=torch.zeros(batch_size, 1, dtype=torch.long, device=loc.device), 70 | visited_=( # Visited as mask is easier to understand, as long more memory efficient 71 | # Keep visited_ with depot so we can scatter efficiently (if there is an action for depot) 72 | torch.zeros( 73 | batch_size, 1, n_loc + 1, 74 | dtype=torch.uint8, device=loc.device 75 | ) 76 | if visited_dtype == torch.uint8 77 | else torch.zeros(batch_size, 1, (n_loc + 1 + 63) // 64, dtype=torch.int64, device=loc.device) # Ceil 78 | ), 79 | lengths=torch.zeros(batch_size, 1, device=loc.device), 80 | cur_coord=input['depot'][:, None, :], # Add step dimension 81 | cur_total_prize=torch.zeros(batch_size, 1, device=loc.device), 82 | i=torch.zeros(1, dtype=torch.int64, device=loc.device) # Vector with length num_steps 83 | ) 84 | 85 | def get_remaining_length(self): 86 | # max_length[:, 0] is max length arriving at depot so original max_length 87 | return self.max_length[self.ids, 0] - self.lengths 88 | 89 | def get_final_cost(self): 90 | 91 | assert self.all_finished() 92 | # The cost is the negative of the collected prize since we want to maximize collected prize 93 | return -self.cur_total_prize 94 | 95 | def update(self, selected): 96 | 97 | assert self.i.size(0) == 1, "Can only update if state represents single step" 98 | 99 | # Update the state 100 | selected = selected[:, None] # Add dimension for step 101 | prev_a = selected 102 | 103 | # Add the length 104 | cur_coord = self.coords[self.ids, selected] 105 | lengths = self.lengths + (cur_coord - self.cur_coord).norm(p=2, dim=-1) # (batch_dim, 1) 106 | 107 | # Add the collected prize 108 | cur_total_prize = self.cur_total_prize + self.prize[self.ids, selected] 109 | 110 | if self.visited_.dtype == torch.uint8: 111 | # Note: here we do not subtract one as we have to scatter so the first column allows scattering depot 112 | # Add one dimension since we write a single value 113 | visited_ = self.visited_.scatter(-1, prev_a[:, :, None], 1) 114 | else: 115 | # This works, by check_unset=False it is allowed to set the depot visited a second a time 116 | visited_ = mask_long_scatter(self.visited_, prev_a, check_unset=False) 117 | 118 | return self._replace( 119 | prev_a=prev_a, visited_=visited_, 120 | lengths=lengths, cur_coord=cur_coord, cur_total_prize=cur_total_prize, i=self.i + 1 121 | ) 122 | 123 | def all_finished(self): 124 | # All must be returned to depot (and at least 1 step since at start also prev_a == 0) 125 | # This is more efficient than checking the mask 126 | return self.i.item() > 0 and (self.prev_a == 0).all() 127 | # return self.visited[:, :, 0].all() # If we have visited the depot we're done 128 | 129 | def get_current_node(self): 130 | """ 131 | Returns the current node where 0 is depot, 1...n are nodes 132 | :return: (batch_size, num_steps) tensor with current nodes 133 | """ 134 | return self.prev_a 135 | 136 | def get_mask(self): 137 | """ 138 | Gets a (batch_size, n_loc + 1) mask with the feasible actions (0 = depot), depends on already visited and 139 | remaining capacity. 0 = feasible, 1 = infeasible 140 | Forbids to visit depot twice in a row, unless all nodes have been visited 141 | :return: 142 | """ 143 | 144 | exceeds_length = ( 145 | self.lengths[:, :, None] + (self.coords[self.ids, :, :] - self.cur_coord[:, :, None, :]).norm(p=2, dim=-1) 146 | > self.max_length[self.ids, :] 147 | ) 148 | # Note: this always allows going to the depot, but that should always be suboptimal so be ok 149 | # Cannot visit if already visited or if length that would be upon arrival is too large to return to depot 150 | # If the depot has already been visited then we cannot visit anymore 151 | visited_ = self.visited.to(exceeds_length.dtype) 152 | mask = visited_ | visited_[:, :, 0:1] | exceeds_length 153 | # Depot can always be visited 154 | # (so we do not hardcode knowledge that this is strictly suboptimal if other options are available) 155 | mask[:, :, 0] = 0 156 | return mask 157 | 158 | def construct_solutions(self, actions): 159 | return actions 160 | -------------------------------------------------------------------------------- /problems/op/tsiligirides.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from problems.op.state_op import StateOP 3 | 4 | 5 | def op_tsiligirides(batch, sample=False, power=4.0): 6 | state = StateOP.initialize(batch) 7 | 8 | all_a = [] 9 | while not state.all_finished(): 10 | # Compute scores 11 | mask = state.get_mask() 12 | p = ( 13 | (mask[..., 1:] == 0).float() * 14 | state.prize[state.ids, 1:] / 15 | ((state.coords[state.ids, 1:, :] - state.cur_coord[:, :, None, :]).norm(p=2, dim=-1) + 1e-6) 16 | ) ** power 17 | bestp, besta = p.topk(4, dim=-1) 18 | bestmask = mask[..., 1:].gather(-1, besta) 19 | 20 | # If no feasible actions, must go to depot 21 | # mask == 0 means feasible, so if mask == 0 sums to 0 there are no feasible and 22 | # all corresponding ps should be 0, so we need to add a column with a 1 that corresponds 23 | # to selecting the end destination 24 | to_depot = ((bestmask == 0).sum(-1, keepdim=True) == 0).float() 25 | # best_p should be zero if we have to go to depot, but because of numeric stabilities, it isn't 26 | p_ = torch.cat((to_depot, bestp), -1) 27 | pnorm = p_ / p_.sum(-1, keepdim=True) 28 | 29 | if sample: 30 | a = pnorm[:, 0, :].multinomial(1) # Sample action 31 | else: 32 | # greedy 33 | a = pnorm[:, 0, :].max(-1)[1].unsqueeze(-1) # Add 'sampling dimension' 34 | 35 | # a == 0 means depot, otherwise subtract one 36 | final_a = torch.cat((torch.zeros_like(besta[..., 0:1]), besta + 1), -1)[:, 0, :].gather(-1, a) 37 | 38 | selected = final_a[..., 0] # Squeeze unnecessary sampling dimension 39 | state = state.update(selected) 40 | all_a.append(selected) 41 | return torch.stack(all_a, -1) 42 | 43 | -------------------------------------------------------------------------------- /problems/pctsp/PCTSP/.gitignore: -------------------------------------------------------------------------------- 1 | ## Mac .DS_Store 2 | .DS_Store 3 | 4 | ## Ignore Visual Studio temporary files, build results, and 5 | ## files generated by popular Visual Studio add-ons. 6 | ## 7 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 8 | 9 | # User-specific files 10 | *.suo 11 | *.user 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Build results 19 | [Dd]ebug/ 20 | [Dd]ebugPublic/ 21 | [Rr]elease/ 22 | [Rr]eleases/ 23 | x64/ 24 | x86/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_i.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *.log 83 | *.vspscc 84 | *.vssscc 85 | .builds 86 | *.pidb 87 | *.svclog 88 | *.scc 89 | 90 | # Chutzpah Test files 91 | _Chutzpah* 92 | 93 | # Visual C++ cache files 94 | ipch/ 95 | *.aps 96 | *.ncb 97 | *.opendb 98 | *.opensdf 99 | *.sdf 100 | *.cachefile 101 | *.VC.db 102 | *.VC.VC.opendb 103 | 104 | # Visual Studio profiler 105 | *.psess 106 | *.vsp 107 | *.vspx 108 | *.sap 109 | 110 | # Visual Studio Trace Files 111 | *.e2e 112 | 113 | # TFS 2012 Local Workspace 114 | $tf/ 115 | 116 | # Guidance Automation Toolkit 117 | *.gpState 118 | 119 | # ReSharper is a .NET coding add-in 120 | _ReSharper*/ 121 | *.[Rr]e[Ss]harper 122 | *.DotSettings.user 123 | 124 | # JustCode is a .NET coding add-in 125 | .JustCode 126 | 127 | # TeamCity is a build add-in 128 | _TeamCity* 129 | 130 | # DotCover is a Code Coverage Tool 131 | *.dotCover 132 | 133 | # AxoCover is a Code Coverage Tool 134 | .axoCover/* 135 | !.axoCover/settings.json 136 | 137 | # Visual Studio code coverage results 138 | *.coverage 139 | *.coveragexml 140 | 141 | # NCrunch 142 | _NCrunch_* 143 | .*crunch*.local.xml 144 | nCrunchTemp_* 145 | 146 | # MightyMoose 147 | *.mm.* 148 | AutoTest.Net/ 149 | 150 | # Web workbench (sass) 151 | .sass-cache/ 152 | 153 | # Installshield output folder 154 | [Ee]xpress/ 155 | 156 | # DocProject is a documentation generator add-in 157 | DocProject/buildhelp/ 158 | DocProject/Help/*.HxT 159 | DocProject/Help/*.HxC 160 | DocProject/Help/*.hhc 161 | DocProject/Help/*.hhk 162 | DocProject/Help/*.hhp 163 | DocProject/Help/Html2 164 | DocProject/Help/html 165 | 166 | # Click-Once directory 167 | publish/ 168 | 169 | # Publish Web Output 170 | *.[Pp]ublish.xml 171 | *.azurePubxml 172 | # Note: Comment the next line if you want to checkin your web deploy settings, 173 | # but database connection strings (with potential passwords) will be unencrypted 174 | *.pubxml 175 | *.publishproj 176 | 177 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 178 | # checkin your Azure Web App publish settings, but sensitive information contained 179 | # in these scripts will be unencrypted 180 | PublishScripts/ 181 | 182 | # NuGet Packages 183 | *.nupkg 184 | # The packages folder can be ignored because of Package Restore 185 | **/[Pp]ackages/* 186 | # except build/, which is used as an MSBuild target. 187 | !**/[Pp]ackages/build/ 188 | # Uncomment if necessary however generally it will be regenerated when needed 189 | #!**/[Pp]ackages/repositories.config 190 | # NuGet v3's project.json files produces more ignorable files 191 | *.nuget.props 192 | *.nuget.targets 193 | 194 | # Microsoft Azure Build Output 195 | csx/ 196 | *.build.csdef 197 | 198 | # Microsoft Azure Emulator 199 | ecf/ 200 | rcf/ 201 | 202 | # Windows Store app package directories and files 203 | AppPackages/ 204 | BundleArtifacts/ 205 | Package.StoreAssociation.xml 206 | _pkginfo.txt 207 | *.appx 208 | 209 | # Visual Studio cache files 210 | # files ending in .cache can be ignored 211 | *.[Cc]ache 212 | # but keep track of directories ending in .cache 213 | !*.[Cc]ache/ 214 | 215 | # Others 216 | ClientBin/ 217 | ~$* 218 | *~ 219 | *.dbmdl 220 | *.dbproj.schemaview 221 | *.jfm 222 | *.pfx 223 | *.publishsettings 224 | orleans.codegen.cs 225 | 226 | # Including strong name files can present a security risk 227 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 228 | #*.snk 229 | 230 | # Since there are multiple workflows, uncomment next line to ignore bower_components 231 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 232 | #bower_components/ 233 | 234 | # RIA/Silverlight projects 235 | Generated_Code/ 236 | 237 | # Backup & report files from converting an old project file 238 | # to a newer Visual Studio version. Backup files are not needed, 239 | # because we have git ;-) 240 | _UpgradeReport_Files/ 241 | Backup*/ 242 | UpgradeLog*.XML 243 | UpgradeLog*.htm 244 | ServiceFabricBackup/ 245 | *.rptproj.bak 246 | 247 | # SQL Server files 248 | *.mdf 249 | *.ldf 250 | *.ndf 251 | 252 | # Business Intelligence projects 253 | *.rdl.data 254 | *.bim.layout 255 | *.bim_*.settings 256 | *.rptproj.rsuser 257 | 258 | # Microsoft Fakes 259 | FakesAssemblies/ 260 | 261 | # GhostDoc plugin setting file 262 | *.GhostDoc.xml 263 | 264 | # Node.js Tools for Visual Studio 265 | .ntvs_analysis.dat 266 | node_modules/ 267 | 268 | # Visual Studio 6 build log 269 | *.plg 270 | 271 | # Visual Studio 6 workspace options file 272 | *.opt 273 | 274 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 275 | *.vbw 276 | 277 | # Visual Studio LightSwitch build output 278 | **/*.HTMLClient/GeneratedArtifacts 279 | **/*.DesktopClient/GeneratedArtifacts 280 | **/*.DesktopClient/ModelManifest.xml 281 | **/*.Server/GeneratedArtifacts 282 | **/*.Server/ModelManifest.xml 283 | _Pvt_Extensions 284 | 285 | # Paket dependency manager 286 | .paket/paket.exe 287 | paket-files/ 288 | 289 | # FAKE - F# Make 290 | .fake/ 291 | 292 | # JetBrains Rider 293 | .idea/ 294 | *.sln.iml 295 | 296 | # CodeRush 297 | .cr/ 298 | 299 | # Python Tools for Visual Studio (PTVS) 300 | __pycache__/ 301 | *.pyc 302 | 303 | # Cake - Uncomment if you are using it 304 | # tools/** 305 | # !tools/packages.config 306 | 307 | # Tabs Studio 308 | *.tss 309 | 310 | # Telerik's JustMock configuration file 311 | *.jmconfig 312 | 313 | # BizTalk build output 314 | *.btp.cs 315 | *.btm.cs 316 | *.odx.cs 317 | *.xsd.cs 318 | 319 | # OpenCover UI analysis results 320 | OpenCover/ 321 | 322 | # Azure Stream Analytics local run output 323 | ASALocalRun/ 324 | 325 | # MSBuild Binary and Structured Log 326 | *.binlog 327 | 328 | # NVidia Nsight GPU debugger configuration file 329 | *.nvuser 330 | 331 | # MFractors (Xamarin productivity tool) working folder 332 | .mfractor/ 333 | -------------------------------------------------------------------------------- /problems/pctsp/PCTSP/Instances/problem_20_100_100_1000.pctsp: -------------------------------------------------------------------------------- 1 | 2 | 0 10 17 4 13 11 4 31 85 62 53 59 90 19 82 25 52 67 86 2 3 | 4 | 5 | 1000000 57 70 43 55 35 50 40 77 16 21 8 45 10 94 92 57 58 8 9 6 | 7 | 8 | 0 274 163 189 282 865 187 563 639 364 267 730 113 95 994 363 79 272 283 572 9 | 274 0 978 857 422 812 397 80 751 559 286 798 940 254 888 677 726 196 551 648 10 | 163 978 0 102 441 458 720 123 1000 605 850 442 175 675 143 701 99 292 726 151 11 | 189 857 102 0 382 927 43 953 680 839 478 953 412 21 344 184 129 926 234 673 12 | 282 422 441 382 0 546 89 895 109 23 425 80 419 669 242 114 588 88 737 826 13 | 865 812 458 927 546 0 413 146 126 633 765 286 355 469 62 211 951 316 168 320 14 | 187 397 720 43 89 413 0 458 714 815 388 30 440 570 258 578 980 949 73 290 15 | 563 80 123 953 895 146 458 0 709 627 583 561 82 249 956 869 81 941 742 949 16 | 639 751 1000 680 109 126 714 709 0 993 883 584 267 431 413 80 38 680 798 710 17 | 364 559 605 839 23 633 815 627 993 0 978 156 397 146 183 246 245 575 147 698 18 | 267 286 850 478 425 765 388 583 883 978 0 603 610 740 582 546 172 121 307 787 19 | 730 798 442 953 80 286 30 561 584 156 603 0 734 189 324 55 802 862 114 753 20 | 113 940 175 412 419 355 440 82 267 397 610 734 0 598 836 884 467 771 721 87 21 | 95 254 675 21 669 469 570 249 431 146 740 189 598 0 138 2 365 457 537 702 22 | 994 888 143 344 242 62 258 956 413 183 582 324 836 138 0 336 990 679 241 88 23 | 363 677 701 184 114 211 578 869 80 246 546 55 884 2 336 0 189 824 224 876 24 | 79 726 99 129 588 951 980 81 38 245 172 802 467 365 990 189 0 99 264 652 25 | 272 196 292 926 88 316 949 941 680 575 121 862 771 457 679 824 99 0 760 347 26 | 283 551 726 234 737 168 73 742 798 147 307 114 721 537 241 224 264 760 0 845 27 | 572 648 151 673 826 320 290 949 710 698 787 753 87 702 88 876 652 347 845 0 28 | -------------------------------------------------------------------------------- /problems/pctsp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/problems/pctsp/__init__.py -------------------------------------------------------------------------------- /problems/pctsp/pctsp_gurobi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2017, Gurobi Optimization, Inc. 4 | 5 | # Solve a traveling salesman problem on a set of 6 | # points using lazy constraints. The base MIP model only includes 7 | # 'degree-2' constraints, requiring each node to have exactly 8 | # two incident edges. Solutions to this model may contain subtours - 9 | # tours that don't visit every city. The lazy constraint callback 10 | # adds new constraints to cut them off. 11 | 12 | from gurobipy import * 13 | 14 | 15 | def solve_euclidian_pctsp(depot, loc, penalty, prize, min_prize, threads=0, timeout=None, gap=None): 16 | """ 17 | Solves the Euclidan pctsp problem to pctsptimality using the MIP formulation 18 | with lazy subtour elimination constraint generation. 19 | :param points: list of (x, y) coordinate 20 | :return: 21 | """ 22 | 23 | points = [depot] + loc 24 | n = len(points) 25 | 26 | # Callback - use lazy constraints to eliminate sub-tours 27 | 28 | def subtourelim(model, where): 29 | if where == GRB.Callback.MIPSOL: 30 | # make a list of edges selected in the solution 31 | vals = model.cbGetSolution(model._vars) 32 | selected = tuplelist((i, j) for i, j in model._vars.keys() if vals[i, j] > 0.5) 33 | # find the shortest cycle in the selected edge list 34 | tour = subtour(selected) 35 | if tour is not None: 36 | # add subtour elimination constraint for every pair of cities in tour 37 | # model.cbLazy(quicksum(model._vars[i, j] 38 | # for i, j in itertools.combinations(tour, 2)) 39 | # <= len(tour) - 1) 40 | 41 | model.cbLazy(quicksum(model._vars[i, j] 42 | for i, j in itertools.combinations(tour, 2)) 43 | <= quicksum(model._dvars[i] for i in tour) * (len(tour) - 1) / float(len(tour))) 44 | 45 | # Given a tuplelist of edges, find the shortest subtour 46 | 47 | def subtour(edges, exclude_depot=True): 48 | unvisited = list(range(n)) 49 | #cycle = range(n + 1) # initial length has 1 more city 50 | cycle = None 51 | while unvisited: # true if list is non-empty 52 | thiscycle = [] 53 | neighbors = unvisited 54 | while neighbors: 55 | current = neighbors[0] 56 | thiscycle.append(current) 57 | unvisited.remove(current) 58 | neighbors = [j for i, j in edges.select(current, '*') if j in unvisited] 59 | # If we do not yet have a cycle or this is the shorter cycle, keep this cycle 60 | # Unless it contains the depot while we do not want the depot 61 | if ( 62 | (cycle is None or len(cycle) > len(thiscycle)) 63 | and len(thiscycle) > 1 and not (0 in thiscycle and exclude_depot) 64 | ): 65 | cycle = thiscycle 66 | return cycle 67 | 68 | # Dictionary of Euclidean distance between each pair of points 69 | 70 | dist = {(i,j) : 71 | math.sqrt(sum((points[i][k]-points[j][k])**2 for k in range(2))) 72 | for i in range(n) for j in range(i)} 73 | 74 | m = Model() 75 | m.Params.outputFlag = False 76 | 77 | # Create variables 78 | vars = m.addVars(dist.keys(), obj=dist, vtype=GRB.BINARY, name='e') 79 | for i,j in vars.keys(): 80 | vars[j,i] = vars[i,j] # edge in pctspposite direction 81 | 82 | # Depot vars can be 2 83 | for i,j in vars.keys(): 84 | if i == 0 or j == 0: 85 | vars[i,j].vtype = GRB.INTEGER 86 | vars[i,j].ub = 2 87 | 88 | penalty_dict = { 89 | i + 1: -p # We get penalties for the nodes not visited so we 'save the penalties' for the nodes visited 90 | for i, p in enumerate(penalty) 91 | } 92 | delta = m.addVars(range(1, n), obj=penalty_dict, vtype=GRB.BINARY, name='delta') 93 | 94 | # Add degree-2 constraint (2 * delta for nodes which are not the depot) 95 | m.addConstrs(vars.sum(i,'*') == (2 if i == 0 else 2 * delta[i]) for i in range(n)) 96 | 97 | 98 | # Minimum prize constraint 99 | assert min_prize <= sum(prize) 100 | # Subtract 1 from i since prizes are 0 indexed while delta vars start with 1 (0 is depot) 101 | m.addConstr(quicksum(var * prize[i - 1] for i, var in delta.items()) >= min_prize) 102 | 103 | # optimize model 104 | 105 | m._vars = vars 106 | m._dvars = delta 107 | m.Params.lazyConstraints = 1 108 | m.Params.threads = threads 109 | if timeout: 110 | m.Params.timeLimit = timeout 111 | if gap: 112 | m.Params.mipGap = gap * 0.01 # Percentage 113 | # For the correct objective, we need to add the sum of the penalties, which are subtracted when nodes are visited 114 | # this is important for the relative gap termination criterion 115 | m.objcon = sum(penalty) 116 | m.optimize(subtourelim) 117 | 118 | vals = m.getAttr('x', vars) 119 | selected = tuplelist((i,j) for i,j in vals.keys() if vals[i,j] > 0.5) 120 | 121 | tour = subtour(selected, exclude_depot=False) 122 | assert tour[0] == 0, "Tour should start with depot" 123 | 124 | return m.objVal, tour -------------------------------------------------------------------------------- /problems/pctsp/problem_pctsp.py: -------------------------------------------------------------------------------- 1 | from torch.utils.data import Dataset 2 | import torch 3 | import os 4 | import pickle 5 | from problems.pctsp.state_pctsp import StatePCTSP 6 | from utils.beam_search import beam_search 7 | 8 | 9 | class PCTSP(object): 10 | 11 | NAME = 'pctsp' # Prize Collecting TSP, without depot, with penalties 12 | 13 | @staticmethod 14 | def _get_costs(dataset, pi, stochastic=False): 15 | if pi.size(-1) == 1: # In case all tours directly return to depot, prevent further problems 16 | assert (pi == 0).all(), "If all length 1 tours, they should be zero" 17 | # Return 18 | return torch.zeros(pi.size(0), dtype=torch.float, device=pi.device), None 19 | 20 | # Check that tours are valid, i.e. contain 0 to n -1 21 | sorted_pi = pi.data.sort(1)[0] 22 | # Make sure each node visited once at most (except for depot) 23 | assert ((sorted_pi[:, 1:] == 0) | (sorted_pi[:, 1:] > sorted_pi[:, :-1])).all(), "Duplicates" 24 | 25 | prize = dataset['stochastic_prize'] if stochastic else dataset['deterministic_prize'] 26 | prize_with_depot = torch.cat( 27 | ( 28 | torch.zeros_like(prize[:, :1]), 29 | prize 30 | ), 31 | 1 32 | ) 33 | p = prize_with_depot.gather(1, pi) 34 | 35 | # Either prize constraint should be satisfied or all prizes should be visited 36 | assert ( 37 | (p.sum(-1) >= 1 - 1e-5) | 38 | (sorted_pi.size(-1) - (sorted_pi == 0).int().sum(-1) == dataset['loc'].size(-2)) 39 | ).all(), "Total prize does not satisfy min total prize" 40 | penalty_with_depot = torch.cat( 41 | ( 42 | torch.zeros_like(dataset['penalty'][:, :1]), 43 | dataset['penalty'] 44 | ), 45 | 1 46 | ) 47 | pen = penalty_with_depot.gather(1, pi) 48 | 49 | # Gather dataset in order of tour 50 | loc_with_depot = torch.cat((dataset['depot'][:, None, :], dataset['loc']), 1) 51 | d = loc_with_depot.gather(1, pi[..., None].expand(*pi.size(), loc_with_depot.size(-1))) 52 | 53 | length = ( 54 | (d[:, 1:] - d[:, :-1]).norm(p=2, dim=-1).sum(1) # Prevent error if len 1 seq 55 | + (d[:, 0] - dataset['depot']).norm(p=2, dim=-1) # Depot to first 56 | + (d[:, -1] - dataset['depot']).norm(p=2, dim=-1) # Last to depot, will be 0 if depot is last 57 | ) 58 | # We want to maximize total prize but code minimizes so return negative 59 | # Incurred penalty cost is total penalty cost - saved penalty costs of nodes visited 60 | return length + dataset['penalty'].sum(-1) - pen.sum(-1), None 61 | 62 | @staticmethod 63 | def make_dataset(*args, **kwargs): 64 | return PCTSPDataset(*args, **kwargs) 65 | 66 | @staticmethod 67 | def beam_search(input, beam_size, expand_size=None, 68 | compress_mask=False, model=None, max_calc_batch_size=4096): 69 | 70 | assert model is not None, "Provide model" 71 | 72 | fixed = model.precompute_fixed(input) 73 | 74 | def propose_expansions(beam): 75 | return model.propose_expansions( 76 | beam, fixed, expand_size, normalize=True, max_calc_batch_size=max_calc_batch_size 77 | ) 78 | 79 | # With beam search we always consider the deterministic case 80 | state = PCTSPDet.make_state( 81 | input, visited_dtype=torch.int64 if compress_mask else torch.uint8 82 | ) 83 | 84 | return beam_search(state, beam_size, propose_expansions) 85 | 86 | 87 | class PCTSPDet(PCTSP): 88 | 89 | @staticmethod 90 | def get_costs(dataset, pi): 91 | return PCTSP._get_costs(dataset, pi, stochastic=False) 92 | 93 | @staticmethod 94 | def make_state(*args, **kwargs): 95 | return StatePCTSP.initialize(*args, **kwargs, stochastic=False) 96 | 97 | 98 | class PCTSPStoch(PCTSP): 99 | 100 | # Stochastic variant of PCTSP, the real (stochastic) prize is only revealed when node is visited 101 | 102 | @staticmethod 103 | def get_costs(dataset, pi): 104 | return PCTSP._get_costs(dataset, pi, stochastic=True) 105 | 106 | @staticmethod 107 | def make_state(*args, **kwargs): 108 | return StatePCTSP.initialize(*args, **kwargs, stochastic=True) 109 | 110 | 111 | def generate_instance(size, penalty_factor=3): 112 | depot = torch.rand(2) 113 | loc = torch.rand(size, 2) 114 | 115 | # For the penalty to make sense it should be not too large (in which case all nodes will be visited) nor too small 116 | # so we want the objective term to be approximately equal to the length of the tour, which we estimate with half 117 | # of the nodes by half of the tour length (which is very rough but similar to op) 118 | # This means that the sum of penalties for all nodes will be approximately equal to the tour length (on average) 119 | # The expected total (uniform) penalty of half of the nodes (since approx half will be visited by the constraint) 120 | # is (n / 2) / 2 = n / 4 so divide by this means multiply by 4 / n, 121 | # However instead of 4 we use penalty_factor (3 works well) so we can make them larger or smaller 122 | MAX_LENGTHS = { 123 | 20: 2., 124 | 50: 3., 125 | 100: 4. 126 | } 127 | penalty_max = MAX_LENGTHS[size] * (penalty_factor) / float(size) 128 | penalty = torch.rand(size) * penalty_max 129 | 130 | # Take uniform prizes 131 | # Now expectation is 0.5 so expected total prize is n / 2, we want to force to visit approximately half of the nodes 132 | # so the constraint will be that total prize >= (n / 2) / 2 = n / 4 133 | # equivalently, we divide all prizes by n / 4 and the total prize should be >= 1 134 | deterministic_prize = torch.rand(size) * 4 / float(size) 135 | 136 | # In the deterministic setting, the stochastic_prize is not used and the deterministic prize is known 137 | # In the stochastic setting, the deterministic prize is the expected prize and is known up front but the 138 | # stochastic prize is only revealed once the node is visited 139 | # Stochastic prize is between (0, 2 * expected_prize) such that E(stochastic prize) = E(deterministic_prize) 140 | stochastic_prize = torch.rand(size) * deterministic_prize * 2 141 | 142 | return { 143 | 'depot': depot, 144 | 'loc': loc, 145 | 'penalty': penalty, 146 | 'deterministic_prize': deterministic_prize, 147 | 'stochastic_prize': stochastic_prize 148 | } 149 | 150 | 151 | class PCTSPDataset(Dataset): 152 | 153 | def __init__(self, filename=None, size=50, num_samples=1000000, offset=0, distribution=None): 154 | super(PCTSPDataset, self).__init__() 155 | 156 | self.data_set = [] 157 | if filename is not None: 158 | assert os.path.splitext(filename)[1] == '.pkl' 159 | 160 | with open(filename, 'rb') as f: 161 | data = pickle.load(f) 162 | self.data = [ 163 | { 164 | 'depot': torch.FloatTensor(depot), 165 | 'loc': torch.FloatTensor(loc), 166 | 'penalty': torch.FloatTensor(penalty), 167 | 'deterministic_prize': torch.FloatTensor(deterministic_prize), 168 | 'stochastic_prize': torch.tensor(stochastic_prize) 169 | } 170 | for depot, loc, penalty, deterministic_prize, stochastic_prize in (data[offset:offset+num_samples]) 171 | ] 172 | else: 173 | self.data = [ 174 | generate_instance(size) 175 | for i in range(num_samples) 176 | ] 177 | 178 | self.size = len(self.data) 179 | 180 | def __len__(self): 181 | return self.size 182 | 183 | def __getitem__(self, idx): 184 | return self.data[idx] 185 | -------------------------------------------------------------------------------- /problems/pctsp/salesman/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /problems/pctsp/salesman/README.md: -------------------------------------------------------------------------------- 1 | # salesman 2 | Prize Collecting Travelling Salesman Problem 3 | -------------------------------------------------------------------------------- /problems/pctsp/salesman/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/problems/pctsp/salesman/__init__.py -------------------------------------------------------------------------------- /problems/pctsp/salesman/pctsp/__init__.py: -------------------------------------------------------------------------------- 1 | # package qextractor 2 | # 3 | # Copyright (c) 2015 Rafael Reis 4 | # 5 | """ 6 | Package qextractor - Packages for building and evaluating a machine learning 7 | model to tackle the Quotation Extractor Task 8 | 9 | """ 10 | __version__="1.0" 11 | __author__ = "Rafael Reis " -------------------------------------------------------------------------------- /problems/pctsp/salesman/pctsp/__main__.py: -------------------------------------------------------------------------------- 1 | # from qextractor.application import main 2 | # main() -------------------------------------------------------------------------------- /problems/pctsp/salesman/pctsp/algo/__init__.py: -------------------------------------------------------------------------------- 1 | # package algo 2 | # 3 | # Copyright (c) 2018 Rafael Reis 4 | # 5 | """ 6 | Package algo - Algorithms for solving the Prize Collecting Travelling Salesman Problem 7 | 8 | """ 9 | __version__="1.0" 10 | __author__ = "Rafael Reis " 11 | -------------------------------------------------------------------------------- /problems/pctsp/salesman/pctsp/algo/geni.py: -------------------------------------------------------------------------------- 1 | # module geni.py 2 | # 3 | # Copyright (c) 2018 Rafael Reis 4 | # 5 | """ 6 | geni module - Auxiliary functions to the GENI method. 7 | 8 | """ 9 | __version__="1.0" 10 | 11 | import numpy as np 12 | import sys 13 | 14 | def geni(v, s, max_i): 15 | quality_1 = 0 16 | quality_2 = 0 17 | 18 | s_star = Solution() 19 | s_start.quality = sys.maxint 20 | 21 | for i in range(1, max_i): 22 | quality_1 = quality_after_insertion_1(v, i, ) 23 | quality_2 = quality_after_insertion_2() 24 | 25 | if quality_1 < quality_2 and quality_1 < s_star.quality: 26 | s_star = insertion_1(s) 27 | elif quality_2 < quality_1 and quality_2 < s_star.quality: 28 | s_star = insertion_2(s) 29 | 30 | return s_star 31 | -------------------------------------------------------------------------------- /problems/pctsp/salesman/pctsp/algo/genius.py: -------------------------------------------------------------------------------- 1 | # module genius.py 2 | # 3 | # Copyright (c) 2018 Rafael Reis 4 | # 5 | """ 6 | genius module - Implements GENIUS, an algorithm for generation of a solution. 7 | 8 | """ 9 | __version__="1.0" 10 | 11 | from pctsp.model.pctsp import * 12 | from pctsp.model import solution 13 | 14 | import numpy as np 15 | 16 | def genius(pctsp): 17 | s = solution.random(pctsp, size=3) 18 | s = geni(pstsp, s) 19 | s = us(pctsp, s) 20 | 21 | return s 22 | 23 | def geni(pctsp, s): 24 | return 25 | 26 | def us(pctsp, s): 27 | return 28 | -------------------------------------------------------------------------------- /problems/pctsp/salesman/pctsp/algo/ilocal_search.py: -------------------------------------------------------------------------------- 1 | # module ilocal_search.py 2 | # 3 | # Copyright (c) 2018 Rafael Reis 4 | # 5 | """ 6 | ilocal_search module - Implements Iterate Local Search algorithm. 7 | 8 | """ 9 | __version__="1.0" 10 | 11 | import numpy as np 12 | import random 13 | 14 | def ilocal_search(s, n_runs=10): 15 | h = s.copy() 16 | best = s.copy() 17 | times = [1000] * n_runs # random.sample(range(1000, 2000), n_runs) 18 | 19 | while len(times) > 0: 20 | time = times.pop() 21 | t = 0 22 | s_tabu = s.copy() 23 | while t < time: 24 | r = tweak(s_tabu.copy()) 25 | if r.quality < s_tabu.quality: 26 | s_tabu = r 27 | 28 | if s_tabu.is_valid(): 29 | s = s_tabu 30 | t += 1 31 | 32 | if s.quality < best.quality and s.is_valid(): 33 | best = s 34 | 35 | h = newHomeBase(h, s) 36 | s = perturb(h) 37 | 38 | return best 39 | 40 | def tweak(solution): 41 | s = solution 42 | 43 | s_1 = m1(solution.copy()) 44 | s_2 = m2(solution.copy()) 45 | 46 | if (s_1 and s_1.quality < solution.quality 47 | and (not s_2 or s_1.quality < s_2.quality) 48 | ):#and s_1.is_valid()): 49 | s = s_1 50 | elif (s_2 and s_2.quality < solution.quality 51 | and (not s_1 or s_2.quality < s_1.quality) 52 | ):#and s_2.is_valid()): 53 | s = s_2 54 | else: 55 | s_3 = m3(solution.copy()) 56 | if (s_3 and s_3.quality < solution.quality 57 | ):#and s_3.is_valid()): 58 | s = s_3 59 | 60 | return s 61 | 62 | def newHomeBase(h, s): 63 | if s.quality <= h.quality: 64 | return s 65 | else: 66 | return h 67 | 68 | def perturb(solution): 69 | s = solution.copy() 70 | if s.size > 5: 71 | quant = int(s.size/5) 72 | s.remove_cities(quant=quant) 73 | 74 | return s 75 | 76 | def m1(solution): 77 | size = solution.size 78 | length = len(solution.route) 79 | 80 | if size > 1 and size < length: 81 | i = random.randrange(1, size) 82 | j = random.randrange(size, length) 83 | solution.swap(i, j) 84 | 85 | return solution 86 | 87 | def m2(solution): 88 | if solution.size > 1: 89 | i = random.randrange(1, solution.size) 90 | solution.remove_city(index=i) 91 | 92 | return solution 93 | 94 | def m3(solution): 95 | if solution.size < len(solution.route): 96 | solution.add_city() 97 | 98 | return solution 99 | -------------------------------------------------------------------------------- /problems/pctsp/salesman/pctsp/application.py: -------------------------------------------------------------------------------- 1 | # module application.py 2 | # 3 | # Copyright (c) 2015 Rafael Reis 4 | # 5 | """ 6 | application module - Main module that solves the Prize Collecting Travelling Salesman Problem 7 | 8 | """ 9 | 10 | from pctsp.model.pctsp import * 11 | from pctsp.model import solution 12 | from pctsp.algo.genius import genius 13 | from pctsp.algo import ilocal_search as ils 14 | from pkg_resources import resource_filename 15 | import random 16 | 17 | INPUT_INSTANCE_FILE = resource_filename('pctsp', 'data/problem_20_100_100_1000.pctsp') 18 | 19 | def solve_instance(filename, min_prize, runs=10, seed=1234): 20 | random.seed(seed) 21 | pctsp = Pctsp() 22 | pctsp.load(filename, min_prize) 23 | s = solution.random(pctsp, size=int(len(pctsp.prize) * 0.7)) 24 | s = ils.ilocal_search(s, n_runs=runs) 25 | 26 | return (s.route[1:], s.quality) 27 | 28 | def main(): 29 | """Main function, that solves the PCTSP. 30 | 31 | """ 32 | pctsp = Pctsp() 33 | pctsp.load(INPUT_INSTANCE_FILE, 386) 34 | #pctsp.prize = np.array([0, 4, 8, 3]) 35 | #pctsp.penal = np.array([1000, 7, 11, 17]) 36 | #pctsp.cost = np.array([[0, 1, 1, 1], [1, 0, 1, 1], [1, 1, 0, 1], [1, 1, 1, 0]]) 37 | # print(pctsp.type) 38 | 39 | size = int(len(pctsp.prize)*0.7) 40 | 41 | s = solution.random(pctsp, size=size) 42 | print(s.route) 43 | print(s.size) 44 | print(s.quality) 45 | print(s.is_valid()) 46 | 47 | print("\n") 48 | 49 | # s = genius(pctsp) 50 | # print(s.route) 51 | # print(s.quality) 52 | 53 | s = ils.ilocal_search(s) 54 | print(s.route) 55 | print(s.size) 56 | print(s.quality) 57 | print(s.is_valid()) 58 | 59 | if __name__ == '__main__': 60 | main() 61 | -------------------------------------------------------------------------------- /problems/pctsp/salesman/pctsp/model/__init__.py: -------------------------------------------------------------------------------- 1 | # package model 2 | # 3 | # Copyright (c) 2018 Rafael Reis 4 | # 5 | """ 6 | Package model - Models of Prize Collecting Travelling Salesman Problem 7 | 8 | """ 9 | __version__="1.0" 10 | __author__ = "Rafael Reis " 11 | -------------------------------------------------------------------------------- /problems/pctsp/salesman/pctsp/model/pctsp.py: -------------------------------------------------------------------------------- 1 | # module pctsp.py 2 | # 3 | # Copyright (c) 2018 Rafael Reis 4 | # 5 | """ 6 | pctsp module - Implements Pctsp, a class that describes an instance of the problem.. 7 | 8 | """ 9 | __version__="1.0" 10 | 11 | import numpy as np 12 | import re 13 | 14 | class Pctsp(object): 15 | """ 16 | Attributes: 17 | c (:obj:`list` of :obj:`list`): Costs from i to j 18 | p (:obj:`list` of :obj:`int`): Prize for visiting each city i 19 | gama (:obj:`list` of :obj:`int`): Penalty for not visiting each city i 20 | """ 21 | def __init__(self): 22 | self.prize = [] 23 | self.penal = [] 24 | self.cost = [] 25 | self.prize_min = 0 26 | 27 | def load(self, file_name, prize_min): 28 | 29 | f = open(file_name,'r') 30 | for i,line in enumerate(f): 31 | if i is 5: break 32 | if i is 1: self.prize = np.fromstring(line, dtype=int, sep=' ') 33 | if i is 4: self.penal = np.fromstring(line, dtype=int, sep=' ') 34 | 35 | f.close() 36 | 37 | self.cost = np.loadtxt(file_name, dtype=int, skiprows=7) 38 | self.prize_min = prize_min 39 | 40 | assert sum(self.prize) >= prize_min, "Infeasible" 41 | 42 | 43 | -------------------------------------------------------------------------------- /problems/pctsp/salesman/pctsp/model/solution.py: -------------------------------------------------------------------------------- 1 | # module solution.py 2 | # 3 | # Copyright (c) 2018 Rafael Reis 4 | # 5 | """ 6 | solution module - Implements Solution, a class that describes a solution for the problem. 7 | 8 | """ 9 | __version__="1.0" 10 | 11 | import numpy as np 12 | import copy 13 | import sys 14 | from random import shuffle 15 | 16 | def random(pctsp, start_size): 17 | s = Solution(pctsp) 18 | length = len(pctsp.prize) 19 | 20 | # Modification: start from start_size but increase after maximum number of iterations in case no feasible solution 21 | # is found. When the full length is used, there should always be a feasible solution 22 | for size in range(start_size, length + 1): 23 | if size: s.size = size 24 | 25 | i = 0 26 | min_solutions = 30 27 | max_solutions = 1000 28 | 29 | while i < min_solutions or (i < max_solutions and not s.is_valid()): 30 | r = Solution(pctsp) 31 | if size: r.size = size 32 | cities = list(range(1, length, 1)) 33 | shuffle(cities) # Shuffle in place 34 | r.route = [0] + cities # The city 0 is always the first 35 | 36 | if r.quality < s.quality and r.is_valid(): 37 | s = r 38 | 39 | i += 1 40 | if s.is_valid(): 41 | break 42 | assert s.is_valid() 43 | return s 44 | 45 | 46 | class Solution(object): 47 | """ 48 | Attributes: 49 | route (:obj:`list` of :obj:`int`): The list of cities in the visiting order 50 | size (:obj:`int`): The quantity of the first cities to be considered in the route list 51 | quality (:obj:`int`): The quality of the solution 52 | """ 53 | 54 | def __init__(self, pctsp, size=None): 55 | self._route = [] 56 | 57 | if size: 58 | self.size = size 59 | else: 60 | self.size = len(pctsp.prize) # Default size value is the total of cities 61 | 62 | self.quality = sys.maxsize 63 | self.pctsp = pctsp 64 | self.prize = 0 65 | 66 | """ 67 | Computes the quality of the solution. 68 | """ 69 | def compute(self): 70 | self.prize = 0 71 | self.quality = 0 72 | 73 | for i,city in enumerate(self._route): 74 | if i < self.size: 75 | self.prize += self.pctsp.prize[city] 76 | if i > 0: 77 | previousCity = self._route[i - 1] 78 | self.quality += self.pctsp.cost[previousCity][city] 79 | if i + 1 == self.size: 80 | self.quality += self.pctsp.cost[city][0] 81 | else: 82 | self.quality += self.pctsp.penal[city] 83 | 84 | def copy(self): 85 | cp = copy.copy(self) 86 | cp._route = list(self._route) 87 | 88 | return cp 89 | 90 | def swap(self, i, j): 91 | city_i = self._route[i] 92 | city_i_prev = self._route[i-1] 93 | city_i_next = self._route[(i+1) % self.size] 94 | 95 | city_j = self._route[j] 96 | 97 | self.quality = (self.quality 98 | - self.pctsp.cost[city_i_prev][city_i] - self.pctsp.cost[city_i][city_i_next] 99 | + self.pctsp.cost[city_i_prev][city_j] + self.pctsp.cost[city_j][city_i_next] 100 | - self.pctsp.penal[city_j] + self.pctsp.penal[city_i]) 101 | self.prize = self.prize - self.pctsp.prize[city_i] + self.pctsp.prize[city_j] 102 | 103 | self._route[j], self._route[i] = self._route[i], self._route[j] 104 | 105 | def is_valid(self): 106 | return self.prize >= self.pctsp.prize_min 107 | 108 | def add_city(self): 109 | city_l = self._route[self.size - 1] 110 | city_add = self._route[self.size] 111 | 112 | self.quality = (self.quality 113 | - self.pctsp.cost[city_l][0] 114 | - self.pctsp.penal[city_add] 115 | + self.pctsp.cost[city_l][city_add] 116 | + self.pctsp.cost[city_add][0]) 117 | 118 | self.size += 1 119 | self.prize += self.pctsp.prize[city_add] 120 | 121 | def remove_city(self, index): 122 | city_rem = self._route[index] 123 | city_rem_prev = self._route[index-1] 124 | city_rem_next = self._route[(index+1)%self.size] 125 | 126 | self.quality = (self.quality 127 | - self.pctsp.cost[city_rem_prev][city_rem] - self.pctsp.cost[city_rem][city_rem_next] 128 | + self.pctsp.penal[city_rem] 129 | + self.pctsp.cost[city_rem_prev][city_rem_next]) 130 | self.prize -= self.pctsp.prize[city_rem] 131 | 132 | del self._route[index] 133 | self._route.append(city_rem) 134 | 135 | self.size -= 1 136 | 137 | def remove_cities(self, quant): 138 | for i in range(self.size-quant,self.size): 139 | city_rem = self._route[i] 140 | city_rem_prev = self._route[i-1] 141 | 142 | self.quality = (self.quality 143 | - self.pctsp.cost[city_rem_prev][city_rem] 144 | + self.pctsp.penal[city_rem]) 145 | self.prize -= self.pctsp.prize[city_rem] 146 | 147 | city_rem = self._route[self.size-1] 148 | city_l = self._route[self.size-quant-1] 149 | self.quality = (self.quality - self.pctsp.cost[city_rem][0] 150 | + self.pctsp.cost[city_l][0]) 151 | 152 | self.size -= quant 153 | 154 | def print_route(self): 155 | print(self._route) 156 | 157 | @property 158 | def route(self): 159 | return self._route 160 | 161 | @route.setter 162 | def route(self, r): 163 | self._route = r 164 | self.compute() 165 | -------------------------------------------------------------------------------- /problems/pctsp/salesman/pctsp/model/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/problems/pctsp/salesman/pctsp/model/tests/__init__.py -------------------------------------------------------------------------------- /problems/pctsp/salesman/pctsp/model/tests/test_solution.py: -------------------------------------------------------------------------------- 1 | # python -m pctsp.model.tests.test_solution 2 | import unittest 3 | 4 | from pctsp.model import solution 5 | from pctsp.model import pctsp 6 | import numpy as np 7 | 8 | class TestTrain(unittest.TestCase): 9 | def setUp(self): 10 | self.p = pctsp.Pctsp() 11 | self.p.prize = np.array([0, 4, 8, 3]) 12 | self.p.penal = np.array([1000, 7, 11, 17]) 13 | self.p.cost = np.array([[0, 1, 1, 1], [1, 0, 1, 1], [1, 1, 0, 1], [1, 1, 1, 0]]) 14 | 15 | def test_quality(self): 16 | s = solution.Solution(self.p) 17 | s.route = [0, 1, 2, 3] 18 | print("Quality: ", s.quality) 19 | self.assertEqual(s.quality, 4) 20 | 21 | def test_quality_2(self): 22 | s = solution.Solution(self.p, size=2) 23 | s.route = [0, 1, 2, 3] 24 | print("Quality: ", s.quality) 25 | self.assertEqual(s.quality, 30) 26 | 27 | def test_swap(self): 28 | s = solution.Solution(self.p, size=3) 29 | s.route = [0, 1, 2, 3] 30 | 31 | s.swap(1,3) 32 | print("Quality: ", s.quality) 33 | print("route:", s.route) 34 | self.assertEqual(s.quality, 10) 35 | 36 | def test_add_city(self): 37 | s = solution.Solution(self.p, size=3) 38 | s.route = [0, 1, 2, 3] 39 | 40 | s.add_city() 41 | print("Quality: ", s.quality) 42 | self.assertEqual(s.quality, 4) 43 | 44 | def test_remove_city(self): 45 | s = solution.Solution(self.p) 46 | s.route = [0, 1, 2, 3] 47 | 48 | s.remove_city(3) 49 | print("Quality: ", s.quality) 50 | self.assertEqual(s.quality, 20) 51 | 52 | def test_remove_cities(self): 53 | s = solution.Solution(self.p) 54 | s.route = [0, 1, 2, 3] 55 | 56 | s.remove_cities(quant=3) 57 | self.assertEqual(s.quality, 35) 58 | 59 | if __name__ == '__main__': 60 | unittest.main() 61 | -------------------------------------------------------------------------------- /problems/pctsp/state_pctsp.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import NamedTuple 3 | from utils.boolmask import mask_long2bool, mask_long_scatter 4 | import torch.nn.functional as F 5 | 6 | 7 | class StatePCTSP(NamedTuple): 8 | # Fixed input 9 | coords: torch.Tensor # Depot + loc 10 | expected_prize: torch.Tensor 11 | real_prize: torch.Tensor 12 | penalty: torch.Tensor 13 | 14 | # If this state contains multiple copies (i.e. beam search) for the same instance, then for memory efficiency 15 | # the coords and prizes tensors are not kept multiple times, so we need to use the ids to index the correct rows. 16 | ids: torch.Tensor # Keeps track of original fixed data index of rows 17 | 18 | # State 19 | prev_a: torch.Tensor 20 | visited_: torch.Tensor # Keeps track of nodes that have been visited 21 | lengths: torch.Tensor 22 | cur_total_prize: torch.Tensor 23 | cur_total_penalty: torch.Tensor 24 | cur_coord: torch.Tensor 25 | i: torch.Tensor # Keeps track of step 26 | 27 | @property 28 | def visited(self): 29 | if self.visited_.dtype == torch.uint8: 30 | return self.visited_ 31 | else: 32 | return mask_long2bool(self.visited_, n=self.coords.size(-2)) 33 | 34 | @property 35 | def dist(self): 36 | return (self.coords[:, :, None, :] - self.coords[:, None, :, :]).norm(p=2, dim=-1) 37 | 38 | def __getitem__(self, key): 39 | assert torch.is_tensor(key) or isinstance(key, slice) # If tensor, idx all tensors by this tensor: 40 | return self._replace( 41 | ids=self.ids[key], 42 | prev_a=self.prev_a[key], 43 | visited_=self.visited_[key], 44 | lengths=self.lengths[key], 45 | cur_total_prize=self.cur_total_prize[key], 46 | cur_total_penalty=self.cur_total_penalty[key], 47 | cur_coord=self.cur_coord[key], 48 | ) 49 | 50 | # Warning: cannot override len of NamedTuple, len should be number of fields, not batch size 51 | # def __len__(self): 52 | # return len(self.used_capacity) 53 | 54 | @staticmethod 55 | def initialize(input, visited_dtype=torch.uint8, stochastic=False): 56 | depot = input['depot'] 57 | loc = input['loc'] 58 | # For both deterministic and stochastic variant, model sees only deterministic (expected) prize 59 | expected_prize = input['deterministic_prize'] 60 | # This is the prize that is actually obtained at each node 61 | real_prize = input['stochastic_prize' if stochastic else 'deterministic_prize'] 62 | penalty = input['penalty'] 63 | 64 | batch_size, n_loc, _ = loc.size() 65 | coords = torch.cat((depot[:, None, :], loc), -2) 66 | # For prize, prepend 0 (corresponding to depot) so we can gather efficiently 67 | 68 | real_prize_with_depot = torch.cat((torch.zeros_like(real_prize[:, :1]), real_prize), -1) 69 | penalty_with_depot = F.pad(penalty, (1, 0), mode='constant', value=0) 70 | 71 | return StatePCTSP( 72 | coords=coords, 73 | expected_prize=expected_prize, 74 | real_prize=real_prize_with_depot, 75 | penalty=penalty_with_depot, 76 | ids=torch.arange(batch_size, dtype=torch.int64, device=loc.device)[:, None], # Add steps dimension 77 | prev_a=torch.zeros(batch_size, 1, dtype=torch.long, device=loc.device), 78 | visited_=( # Visited as mask is easier to understand, as long more memory efficient 79 | # Keep visited_ with depot so we can scatter efficiently (if there is an action for depot) 80 | torch.zeros( 81 | batch_size, 1, n_loc + 1, 82 | dtype=torch.uint8, device=loc.device 83 | ) 84 | if visited_dtype == torch.uint8 85 | else torch.zeros(batch_size, 1, (n_loc + 63) // 64, dtype=torch.int64, device=loc.device) # Ceil 86 | ), 87 | lengths=torch.zeros(batch_size, 1, device=loc.device), 88 | cur_total_prize=torch.zeros(batch_size, 1, device=loc.device), 89 | cur_total_penalty=penalty.sum(-1)[:, None], # Sum penalties (all when nothing is visited), add step dim 90 | cur_coord=input['depot'][:, None, :], # Add step dimension 91 | i=torch.zeros(1, dtype=torch.int64, device=loc.device) # Vector with length num_steps 92 | ) 93 | 94 | def get_remaining_prize_to_collect(self): 95 | # returns the remaining prize to collect, or 0 if already collected the minimum (1.0) 96 | return torch.clamp(1 - self.cur_total_prize, min=0) 97 | 98 | def get_final_cost(self): 99 | 100 | assert self.all_finished() 101 | # assert self.visited_. 102 | # We are at the depot so no need to add remaining distance 103 | return self.lengths + self.cur_total_penalty 104 | 105 | def update(self, selected): 106 | 107 | assert self.i.size(0) == 1, "Can only update if state represents single step" 108 | 109 | # Update the state 110 | selected = selected[:, None] # Add dimension for step 111 | prev_a = selected 112 | 113 | # Add the length 114 | cur_coord = self.coords[self.ids, selected] 115 | lengths = self.lengths + (cur_coord - self.cur_coord).norm(p=2, dim=-1) # (batch_dim, 1) 116 | # Add current total prize 117 | cur_total_prize = self.cur_total_prize + self.real_prize[self.ids, selected] 118 | cur_total_penalty = self.cur_total_penalty + self.penalty[self.ids, selected] 119 | 120 | if self.visited_.dtype == torch.uint8: 121 | # Note: here we do not subtract one as we have to scatter so the first column allows scattering depot 122 | # Add one dimension since we write a single value 123 | visited_ = self.visited_.scatter(-1, prev_a[:, :, None], 1) 124 | else: 125 | # This works, by check_unset=False it is allowed to set the depot visited a second a time 126 | visited_ = mask_long_scatter(self.visited_, prev_a, check_unset=False) 127 | 128 | return self._replace( 129 | prev_a=prev_a, visited_=visited_, 130 | lengths=lengths, cur_total_prize=cur_total_prize, cur_total_penalty=cur_total_penalty, cur_coord=cur_coord, 131 | i=self.i + 1 132 | ) 133 | 134 | def all_finished(self): 135 | # All must be returned to depot (and at least 1 step since at start also prev_a == 0) 136 | # This is more efficient than checking the mask 137 | return self.i.item() > 0 and (self.prev_a == 0).all() 138 | # return self.visited[:, :, 0].all() # If we have visited the depot we're done 139 | 140 | def get_current_node(self): 141 | """ 142 | Returns the current node where 0 is depot, 1...n are nodes 143 | :return: (batch_size, num_steps) tensor with current nodes 144 | """ 145 | return self.prev_a 146 | 147 | def get_mask(self): 148 | """ 149 | Gets a (batch_size, n_loc + 1) mask with the feasible actions (0 = depot), depends on already visited and 150 | remaining capacity. 0 = feasible, 1 = infeasible 151 | Forbids to visit depot twice in a row, unless all nodes have been visited 152 | :return: 153 | """ 154 | 155 | # Note: this always allows going to the depot, but that should always be suboptimal so be ok 156 | # Cannot visit if already visited or if the depot has already been visited then we cannot visit anymore 157 | visited_ = self.visited 158 | mask = ( 159 | visited_ | visited_[:, :, 0:1] 160 | ) 161 | # Cannot visit depot if not yet collected 1 total prize and there are unvisited nodes 162 | mask[:, :, 0] = (self.cur_total_prize < 1.) & (visited_[:, :, 1:].int().sum(-1) < visited_[:, :, 1:].size(-1)) 163 | 164 | return mask > 0 # Hacky way to return bool or uint8 depending on pytorch version 165 | 166 | def construct_solutions(self, actions): 167 | return actions 168 | -------------------------------------------------------------------------------- /problems/tsp/.gitignore: -------------------------------------------------------------------------------- 1 | concorde/ -------------------------------------------------------------------------------- /problems/tsp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/problems/tsp/__init__.py -------------------------------------------------------------------------------- /problems/tsp/install_concorde.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir concorde 3 | cd concorde 4 | mkdir qsopt 5 | cd qsopt 6 | # Download qsopt 7 | if [[ "$OSTYPE" == "darwin"* ]]; then 8 | curl -O http://www.math.uwaterloo.ca/~bico/qsopt/beta/codes/mac64/qsopt.a 9 | curl -O http://www.math.uwaterloo.ca/~bico/qsopt/beta/codes/mac64/qsopt.h 10 | curl -O http://www.math.uwaterloo.ca/~bico/qsopt/beta/codes/mac64/qsopt 11 | else 12 | wget http://www.math.uwaterloo.ca/~bico/qsopt/beta/codes/centos/qsopt.a 13 | wget http://www.math.uwaterloo.ca/~bico/qsopt/beta/codes/centos/qsopt.h 14 | wget http://www.math.uwaterloo.ca/~bico/qsopt/beta/codes/centos/qsopt 15 | fi 16 | cd .. 17 | wget http://www.math.uwaterloo.ca/tsp/concorde/downloads/codes/src/co031219.tgz 18 | tar xf co031219.tgz 19 | cd concorde 20 | if [[ "$OSTYPE" == "darwin"* ]]; then 21 | ./configure --with-qsopt=$(pwd)/../qsopt --host=powerpc-apple-macos 22 | else 23 | ./configure --with-qsopt=$(realpath ../qsopt) 24 | fi 25 | make 26 | TSP/concorde -s 99 -k 100 27 | cd ../.. -------------------------------------------------------------------------------- /problems/tsp/problem_tsp.py: -------------------------------------------------------------------------------- 1 | from torch.utils.data import Dataset 2 | import torch 3 | import os 4 | import pickle 5 | from problems.tsp.state_tsp import StateTSP 6 | from utils.beam_search import beam_search 7 | 8 | 9 | class TSP(object): 10 | 11 | NAME = 'tsp' 12 | 13 | @staticmethod 14 | def get_costs(dataset, pi): 15 | # Check that tours are valid, i.e. contain 0 to n -1 16 | assert ( 17 | torch.arange(pi.size(1), out=pi.data.new()).view(1, -1).expand_as(pi) == 18 | pi.data.sort(1)[0] 19 | ).all(), "Invalid tour" 20 | 21 | # Gather dataset in order of tour 22 | d = dataset.gather(1, pi.unsqueeze(-1).expand_as(dataset)) 23 | 24 | # Length is distance (L2-norm of difference) from each next location from its prev and of last from first 25 | return (d[:, 1:] - d[:, :-1]).norm(p=2, dim=2).sum(1) + (d[:, 0] - d[:, -1]).norm(p=2, dim=1), None 26 | 27 | @staticmethod 28 | def make_dataset(*args, **kwargs): 29 | return TSPDataset(*args, **kwargs) 30 | 31 | @staticmethod 32 | def make_state(*args, **kwargs): 33 | return StateTSP.initialize(*args, **kwargs) 34 | 35 | @staticmethod 36 | def beam_search(input, beam_size, expand_size=None, 37 | compress_mask=False, model=None, max_calc_batch_size=4096): 38 | 39 | assert model is not None, "Provide model" 40 | 41 | fixed = model.precompute_fixed(input) 42 | 43 | def propose_expansions(beam): 44 | return model.propose_expansions( 45 | beam, fixed, expand_size, normalize=True, max_calc_batch_size=max_calc_batch_size 46 | ) 47 | 48 | state = TSP.make_state( 49 | input, visited_dtype=torch.int64 if compress_mask else torch.uint8 50 | ) 51 | 52 | return beam_search(state, beam_size, propose_expansions) 53 | 54 | 55 | class TSPDataset(Dataset): 56 | 57 | def __init__(self, filename=None, size=50, num_samples=1000000, offset=0, distribution=None): 58 | super(TSPDataset, self).__init__() 59 | 60 | self.data_set = [] 61 | if filename is not None: 62 | assert os.path.splitext(filename)[1] == '.pkl' 63 | 64 | with open(filename, 'rb') as f: 65 | data = pickle.load(f) 66 | self.data = [torch.FloatTensor(row) for row in (data[offset:offset+num_samples])] 67 | else: 68 | # Sample points randomly in [0, 1] square 69 | self.data = [torch.FloatTensor(size, 2).uniform_(0, 1) for i in range(num_samples)] 70 | 71 | self.size = len(self.data) 72 | 73 | def __len__(self): 74 | return self.size 75 | 76 | def __getitem__(self, idx): 77 | return self.data[idx] 78 | -------------------------------------------------------------------------------- /problems/tsp/state_tsp.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import NamedTuple 3 | from utils.boolmask import mask_long2bool, mask_long_scatter 4 | 5 | 6 | class StateTSP(NamedTuple): 7 | # Fixed input 8 | loc: torch.Tensor 9 | dist: torch.Tensor 10 | 11 | # If this state contains multiple copies (i.e. beam search) for the same instance, then for memory efficiency 12 | # the loc and dist tensors are not kept multiple times, so we need to use the ids to index the correct rows. 13 | ids: torch.Tensor # Keeps track of original fixed data index of rows 14 | 15 | # State 16 | first_a: torch.Tensor 17 | prev_a: torch.Tensor 18 | visited_: torch.Tensor # Keeps track of nodes that have been visited 19 | lengths: torch.Tensor 20 | cur_coord: torch.Tensor 21 | i: torch.Tensor # Keeps track of step 22 | 23 | @property 24 | def visited(self): 25 | if self.visited_.dtype == torch.uint8: 26 | return self.visited_ 27 | else: 28 | return mask_long2bool(self.visited_, n=self.loc.size(-2)) 29 | 30 | def __getitem__(self, key): 31 | assert torch.is_tensor(key) or isinstance(key, slice) # If tensor, idx all tensors by this tensor: 32 | return self._replace( 33 | ids=self.ids[key], 34 | first_a=self.first_a[key], 35 | prev_a=self.prev_a[key], 36 | visited_=self.visited_[key], 37 | lengths=self.lengths[key], 38 | cur_coord=self.cur_coord[key] if self.cur_coord is not None else None, 39 | ) 40 | 41 | @staticmethod 42 | def initialize(loc, visited_dtype=torch.uint8): 43 | 44 | batch_size, n_loc, _ = loc.size() 45 | prev_a = torch.zeros(batch_size, 1, dtype=torch.long, device=loc.device) 46 | return StateTSP( 47 | loc=loc, 48 | dist=(loc[:, :, None, :] - loc[:, None, :, :]).norm(p=2, dim=-1), 49 | ids=torch.arange(batch_size, dtype=torch.int64, device=loc.device)[:, None], # Add steps dimension 50 | first_a=prev_a, 51 | prev_a=prev_a, 52 | # Keep visited with depot so we can scatter efficiently (if there is an action for depot) 53 | visited_=( # Visited as mask is easier to understand, as long more memory efficient 54 | torch.zeros( 55 | batch_size, 1, n_loc, 56 | dtype=torch.uint8, device=loc.device 57 | ) 58 | if visited_dtype == torch.uint8 59 | else torch.zeros(batch_size, 1, (n_loc + 63) // 64, dtype=torch.int64, device=loc.device) # Ceil 60 | ), 61 | lengths=torch.zeros(batch_size, 1, device=loc.device), 62 | cur_coord=None, 63 | i=torch.zeros(1, dtype=torch.int64, device=loc.device) # Vector with length num_steps 64 | ) 65 | 66 | def get_final_cost(self): 67 | 68 | assert self.all_finished() 69 | # assert self.visited_. 70 | 71 | return self.lengths + (self.loc[self.ids, self.first_a, :] - self.cur_coord).norm(p=2, dim=-1) 72 | 73 | def update(self, selected): 74 | 75 | # Update the state 76 | prev_a = selected[:, None] # Add dimension for step 77 | 78 | # Add the length 79 | # cur_coord = self.loc.gather( 80 | # 1, 81 | # selected[:, None, None].expand(selected.size(0), 1, self.loc.size(-1)) 82 | # )[:, 0, :] 83 | cur_coord = self.loc[self.ids, prev_a] 84 | lengths = self.lengths 85 | if self.cur_coord is not None: # Don't add length for first action (selection of start node) 86 | lengths = self.lengths + (cur_coord - self.cur_coord).norm(p=2, dim=-1) # (batch_dim, 1) 87 | 88 | # Update should only be called with just 1 parallel step, in which case we can check this way if we should update 89 | first_a = prev_a if self.i.item() == 0 else self.first_a 90 | 91 | if self.visited_.dtype == torch.uint8: 92 | # Add one dimension since we write a single value 93 | visited_ = self.visited_.scatter(-1, prev_a[:, :, None], 1) 94 | else: 95 | visited_ = mask_long_scatter(self.visited_, prev_a) 96 | 97 | return self._replace(first_a=first_a, prev_a=prev_a, visited_=visited_, 98 | lengths=lengths, cur_coord=cur_coord, i=self.i + 1) 99 | 100 | def all_finished(self): 101 | # Exactly n steps 102 | return self.i.item() >= self.loc.size(-2) 103 | 104 | def get_current_node(self): 105 | return self.prev_a 106 | 107 | def get_mask(self): 108 | return self.visited > 0 # Hacky way to return bool or uint8 depending on pytorch version 109 | 110 | def get_nn(self, k=None): 111 | # Insert step dimension 112 | # Nodes already visited get inf so they do not make it 113 | if k is None: 114 | k = self.loc.size(-2) - self.i.item() # Number of remaining 115 | return (self.dist[self.ids, :, :] + self.visited.float()[:, :, None, :] * 1e6).topk(k, dim=-1, largest=False)[1] 116 | 117 | def get_nn_current(self, k=None): 118 | assert False, "Currently not implemented, look into which neighbours to use in step 0?" 119 | # Note: if this is called in step 0, it will have k nearest neighbours to node 0, which may not be desired 120 | # so it is probably better to use k = None in the first iteration 121 | if k is None: 122 | k = self.loc.size(-2) 123 | k = min(k, self.loc.size(-2) - self.i.item()) # Number of remaining 124 | return ( 125 | self.dist[ 126 | self.ids, 127 | self.prev_a 128 | ] + 129 | self.visited.float() * 1e6 130 | ).topk(k, dim=-1, largest=False)[1] 131 | 132 | def construct_solutions(self, actions): 133 | return actions 134 | -------------------------------------------------------------------------------- /problems/tsp/tsp_gurobi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2017, Gurobi Optimization, Inc. 4 | 5 | # Solve a traveling salesman problem on a set of 6 | # points using lazy constraints. The base MIP model only includes 7 | # 'degree-2' constraints, requiring each node to have exactly 8 | # two incident edges. Solutions to this model may contain subtours - 9 | # tours that don't visit every city. The lazy constraint callback 10 | # adds new constraints to cut them off. 11 | 12 | import argparse 13 | import numpy as np 14 | from utils.data_utils import load_dataset, save_dataset 15 | from gurobipy import * 16 | 17 | 18 | def solve_euclidian_tsp(points, threads=0, timeout=None, gap=None): 19 | """ 20 | Solves the Euclidan TSP problem to optimality using the MIP formulation 21 | with lazy subtour elimination constraint generation. 22 | :param points: list of (x, y) coordinate 23 | :return: 24 | """ 25 | 26 | n = len(points) 27 | 28 | # Callback - use lazy constraints to eliminate sub-tours 29 | 30 | def subtourelim(model, where): 31 | if where == GRB.Callback.MIPSOL: 32 | # make a list of edges selected in the solution 33 | vals = model.cbGetSolution(model._vars) 34 | selected = tuplelist((i, j) for i, j in model._vars.keys() if vals[i, j] > 0.5) 35 | # find the shortest cycle in the selected edge list 36 | tour = subtour(selected) 37 | if len(tour) < n: 38 | # add subtour elimination constraint for every pair of cities in tour 39 | model.cbLazy(quicksum(model._vars[i, j] 40 | for i, j in itertools.combinations(tour, 2)) 41 | <= len(tour) - 1) 42 | 43 | # Given a tuplelist of edges, find the shortest subtour 44 | 45 | def subtour(edges): 46 | unvisited = list(range(n)) 47 | cycle = range(n + 1) # initial length has 1 more city 48 | while unvisited: # true if list is non-empty 49 | thiscycle = [] 50 | neighbors = unvisited 51 | while neighbors: 52 | current = neighbors[0] 53 | thiscycle.append(current) 54 | unvisited.remove(current) 55 | neighbors = [j for i, j in edges.select(current, '*') if j in unvisited] 56 | if len(cycle) > len(thiscycle): 57 | cycle = thiscycle 58 | return cycle 59 | 60 | # Dictionary of Euclidean distance between each pair of points 61 | 62 | dist = {(i,j) : 63 | math.sqrt(sum((points[i][k]-points[j][k])**2 for k in range(2))) 64 | for i in range(n) for j in range(i)} 65 | 66 | m = Model() 67 | m.Params.outputFlag = False 68 | 69 | # Create variables 70 | 71 | vars = m.addVars(dist.keys(), obj=dist, vtype=GRB.BINARY, name='e') 72 | for i,j in vars.keys(): 73 | vars[j,i] = vars[i,j] # edge in opposite direction 74 | 75 | # You could use Python looping constructs and m.addVar() to create 76 | # these decision variables instead. The following would be equivalent 77 | # to the preceding m.addVars() call... 78 | # 79 | # vars = tupledict() 80 | # for i,j in dist.keys(): 81 | # vars[i,j] = m.addVar(obj=dist[i,j], vtype=GRB.BINARY, 82 | # name='e[%d,%d]'%(i,j)) 83 | 84 | 85 | # Add degree-2 constraint 86 | 87 | m.addConstrs(vars.sum(i,'*') == 2 for i in range(n)) 88 | 89 | # Using Python looping constructs, the preceding would be... 90 | # 91 | # for i in range(n): 92 | # m.addConstr(sum(vars[i,j] for j in range(n)) == 2) 93 | 94 | 95 | # Optimize model 96 | 97 | m._vars = vars 98 | m.Params.lazyConstraints = 1 99 | m.Params.threads = threads 100 | if timeout: 101 | m.Params.timeLimit = timeout 102 | if gap: 103 | m.Params.mipGap = gap * 0.01 # Percentage 104 | m.optimize(subtourelim) 105 | 106 | vals = m.getAttr('x', vars) 107 | selected = tuplelist((i,j) for i,j in vals.keys() if vals[i,j] > 0.5) 108 | 109 | tour = subtour(selected) 110 | assert len(tour) == n 111 | 112 | return m.objVal, tour 113 | 114 | 115 | def solve_all_gurobi(dataset): 116 | results = [] 117 | for i, instance in enumerate(dataset): 118 | print ("Solving instance {}".format(i)) 119 | result = solve_euclidian_tsp(instance) 120 | results.append(result) 121 | return results 122 | -------------------------------------------------------------------------------- /problems/vrp/.gitignore: -------------------------------------------------------------------------------- 1 | lkh/ -------------------------------------------------------------------------------- /problems/vrp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterkool/attention-learn-to-route/c9abf41ac2f878a55b20dc7e829bc942bb999631/problems/vrp/__init__.py -------------------------------------------------------------------------------- /problems/vrp/encode-attend-navigate/data_generator.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | import math 5 | from sklearn.decomposition import PCA 6 | 7 | 8 | # Compute a sequence's reward 9 | def reward(tsp_sequence): 10 | tour = np.concatenate((tsp_sequence, np.expand_dims(tsp_sequence[0],0))) # sequence to tour (end=start) 11 | inter_city_distances = np.sqrt(np.sum(np.square(tour[:-1,:2]-tour[1:,:2]),axis=1)) # tour length 12 | return np.sum(inter_city_distances) # reward 13 | 14 | # Swap city[i] with city[j] in sequence 15 | def swap2opt(tsp_sequence,i,j): 16 | new_tsp_sequence = np.copy(tsp_sequence) 17 | new_tsp_sequence[i:j+1] = np.flip(tsp_sequence[i:j+1], axis=0) # flip or swap ? 18 | return new_tsp_sequence 19 | 20 | # One step of 2opt = one double loop and return first improved sequence 21 | def step2opt(tsp_sequence): 22 | seq_length = tsp_sequence.shape[0] 23 | distance = reward(tsp_sequence) 24 | for i in range(1,seq_length-1): 25 | for j in range(i+1,seq_length): 26 | new_tsp_sequence = swap2opt(tsp_sequence,i,j) 27 | new_distance = reward(new_tsp_sequence) 28 | if new_distance < distance: 29 | return new_tsp_sequence, new_distance 30 | return tsp_sequence, distance 31 | 32 | 33 | class DataGenerator(object): 34 | 35 | def __init__(self): 36 | pass 37 | 38 | def gen_instance(self, max_length, dimension, seed=0): # Generate random TSP instance 39 | if seed!=0: np.random.seed(seed) 40 | sequence = np.random.rand(max_length, dimension) # (max_length) cities with (dimension) coordinates in [0,1] 41 | pca = PCA(n_components=dimension) # center & rotate coordinates 42 | sequence = pca.fit_transform(sequence) 43 | return sequence 44 | 45 | def train_batch(self, batch_size, max_length, dimension): # Generate random batch for training procedure 46 | input_batch = [] 47 | for _ in range(batch_size): 48 | input_ = self.gen_instance(max_length, dimension) # Generate random TSP instance 49 | input_batch.append(input_) # Store batch 50 | return input_batch 51 | 52 | def test_batch(self, batch_size, max_length, dimension, seed=0, shuffle=False): # Generate random batch for testing procedure 53 | input_batch = [] 54 | input_ = self.gen_instance(max_length, dimension, seed=seed) # Generate random TSP instance 55 | for _ in range(batch_size): 56 | sequence = np.copy(input_) 57 | if shuffle==True: 58 | np.random.shuffle(sequence) # Shuffle sequence 59 | input_batch.append(sequence) # Store batch 60 | return input_batch 61 | 62 | def loop2opt(self, tsp_sequence, max_iter=2000): # Iterate step2opt max_iter times (2-opt local search) 63 | best_reward = reward(tsp_sequence) 64 | new_tsp_sequence = np.copy(tsp_sequence) 65 | for _ in range(max_iter): 66 | new_tsp_sequence, new_reward = step2opt(new_tsp_sequence) 67 | if new_reward < best_reward: 68 | best_reward = new_reward 69 | else: 70 | break 71 | return new_tsp_sequence, best_reward 72 | 73 | def visualize_2D_trip(self, trip): # Plot tour 74 | plt.figure(1) 75 | colors = ['red'] # First city red 76 | for i in range(len(trip)-1): 77 | colors.append('blue') 78 | 79 | plt.scatter(trip[:,0], trip[:,1], color=colors) # Plot cities 80 | tour=np.array(list(range(len(trip))) + [0]) # Plot tour 81 | X = trip[tour, 0] 82 | Y = trip[tour, 1] 83 | plt.plot(X, Y,"--") 84 | 85 | plt.xlim(-0.75,0.75) 86 | plt.ylim(-0.75,0.75) 87 | plt.xlabel('X') 88 | plt.ylabel('Y') 89 | plt.show() 90 | 91 | def visualize_sampling(self, permutations): # Heatmap of permutations (x=cities; y=steps) 92 | max_length = len(permutations[0]) 93 | grid = np.zeros([max_length,max_length]) # initialize heatmap grid to 0 94 | 95 | transposed_permutations = np.transpose(permutations) 96 | for t, cities_t in enumerate(transposed_permutations): # step t, cities chosen at step t 97 | city_indices, counts = np.unique(cities_t,return_counts=True,axis=0) 98 | for u,v in zip(city_indices, counts): 99 | grid[t][u]+=v # update grid with counts from the batch of permutations 100 | 101 | fig = plt.figure(1) # plot heatmap 102 | ax = fig.add_subplot(1,1,1) 103 | ax.set_aspect('equal') 104 | plt.imshow(grid, interpolation='nearest', cmap='gray') 105 | plt.colorbar() 106 | plt.title('Sampled permutations') 107 | plt.ylabel('Time t') 108 | plt.xlabel('City i') 109 | plt.show() -------------------------------------------------------------------------------- /problems/vrp/encode-attend-navigate/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | import tensorflow as tf 4 | import numpy as np 5 | from tqdm import tqdm 6 | 7 | 8 | # Embed input sequence [batch_size, seq_length, from_] -> [batch_size, seq_length, to_] 9 | def embed_seq(input_seq, from_, to_, is_training, BN=True, initializer=tf.contrib.layers.xavier_initializer()): 10 | with tf.variable_scope("embedding"): # embed + BN input set 11 | W_embed = tf.get_variable("weights",[1,from_, to_], initializer=initializer) 12 | embedded_input = tf.nn.conv1d(input_seq, W_embed, 1, "VALID", name="embedded_input") 13 | if BN == True: embedded_input = tf.layers.batch_normalization(embedded_input, axis=2, training=is_training, name='layer_norm', reuse=None) 14 | return embedded_input 15 | 16 | 17 | # Apply multihead attention to a 3d tensor with shape [batch_size, seq_length, n_hidden]. 18 | # Attention size = n_hidden should be a multiple of num_head 19 | # Returns a 3d tensor with shape of [batch_size, seq_length, n_hidden] 20 | def multihead_attention(inputs, num_units=None, num_heads=16, dropout_rate=0.1, is_training=True): 21 | with tf.variable_scope("multihead_attention", reuse=None): 22 | # Linear projections 23 | Q = tf.layers.dense(inputs, num_units, activation=tf.nn.relu) # [batch_size, seq_length, n_hidden] 24 | K = tf.layers.dense(inputs, num_units, activation=tf.nn.relu) # [batch_size, seq_length, n_hidden] 25 | V = tf.layers.dense(inputs, num_units, activation=tf.nn.relu) # [batch_size, seq_length, n_hidden] 26 | # Split and concat 27 | Q_ = tf.concat(tf.split(Q, num_heads, axis=2), axis=0) # [batch_size, seq_length, n_hidden/num_heads] 28 | K_ = tf.concat(tf.split(K, num_heads, axis=2), axis=0) # [batch_size, seq_length, n_hidden/num_heads] 29 | V_ = tf.concat(tf.split(V, num_heads, axis=2), axis=0) # [batch_size, seq_length, n_hidden/num_heads] 30 | # Multiplication 31 | outputs = tf.matmul(Q_, tf.transpose(K_, [0, 2, 1])) # num_heads*[batch_size, seq_length, seq_length] 32 | # Scale 33 | outputs = outputs / (K_.get_shape().as_list()[-1] ** 0.5) 34 | # Activation 35 | outputs = tf.nn.softmax(outputs) # num_heads*[batch_size, seq_length, seq_length] 36 | # Dropouts 37 | outputs = tf.layers.dropout(outputs, rate=dropout_rate, training=tf.convert_to_tensor(is_training)) 38 | # Weighted sum 39 | outputs = tf.matmul(outputs, V_) # num_heads*[batch_size, seq_length, n_hidden/num_heads] 40 | # Restore shape 41 | outputs = tf.concat(tf.split(outputs, num_heads, axis=0), axis=2 ) # [batch_size, seq_length, n_hidden] 42 | # Residual connection 43 | outputs += inputs # [batch_size, seq_length, n_hidden] 44 | # Normalize 45 | outputs = tf.layers.batch_normalization(outputs, axis=2, training=is_training, name='ln', reuse=None) # [batch_size, seq_length, n_hidden] 46 | 47 | return outputs 48 | 49 | 50 | # Apply point-wise feed forward net to a 3d tensor with shape [batch_size, seq_length, n_hidden] 51 | # Returns: a 3d tensor with the same shape and dtype as inputs 52 | def feedforward(inputs, num_units=[2048, 512], is_training=True): 53 | with tf.variable_scope("ffn", reuse=None): 54 | # Inner layer 55 | params = {"inputs": inputs, "filters": num_units[0], "kernel_size": 1, "activation": tf.nn.relu, "use_bias": True} 56 | outputs = tf.layers.conv1d(**params) 57 | # Readout layer 58 | params = {"inputs": outputs, "filters": num_units[1], "kernel_size": 1, "activation": None, "use_bias": True} 59 | outputs = tf.layers.conv1d(**params) 60 | # Residual connection 61 | outputs += inputs 62 | # Normalize 63 | outputs = tf.layers.batch_normalization(outputs, axis=2, training=is_training, name='ln', reuse=None) # [batch_size, seq_length, n_hidden] 64 | return outputs 65 | 66 | 67 | # Encode input sequence [batch_size, seq_length, n_hidden] -> [batch_size, seq_length, n_hidden] 68 | def encode_seq(input_seq, input_dim, num_stacks, num_heads, num_neurons, is_training, dropout_rate=0.): 69 | with tf.variable_scope("stack"): 70 | for i in range(num_stacks): # block i 71 | with tf.variable_scope("block_{}".format(i)): # Multihead Attention + Feed Forward 72 | input_seq = multihead_attention(input_seq, num_units=input_dim, num_heads=num_heads, dropout_rate=dropout_rate, is_training=is_training) 73 | input_seq = feedforward(input_seq, num_units=[num_neurons, input_dim], is_training=is_training) 74 | return input_seq # encoder_output is the ref for actions [Batch size, Sequence Length, Num_neurons] 75 | 76 | 77 | # From a query (decoder output) [Batch size, n_hidden] and a set of reference (encoder_output) [Batch size, seq_length, n_hidden] 78 | # predict a distribution over next decoder input 79 | def pointer(encoded_ref, query, mask, W_ref, W_q, v, C=10., temperature=1.0): 80 | encoded_query = tf.expand_dims(tf.matmul(query, W_q), 1) # [Batch size, 1, n_hidden] 81 | scores = tf.reduce_sum(v * tf.tanh(encoded_ref + encoded_query), [-1]) # [Batch size, seq_length] 82 | scores = C*tf.tanh(scores/temperature) # control entropy 83 | masked_scores = tf.clip_by_value(scores -100000000.*mask, -100000000., 100000000.) # [Batch size, seq_length] 84 | return masked_scores 85 | 86 | 87 | # From a query [Batch size, n_hidden], glimpse at a set of reference vectors (ref) [Batch size, seq_length, n_hidden] 88 | def full_glimpse(ref, from_, to_, initializer=tf.contrib.layers.xavier_initializer()): 89 | with tf.variable_scope("glimpse"): 90 | W_ref_g =tf.get_variable("W_ref_g",[1,from_, to_],initializer=initializer) 91 | W_q_g =tf.get_variable("W_q_g",[from_, to_],initializer=initializer) 92 | v_g =tf.get_variable("v_g",[to_],initializer=initializer) 93 | # Attending mechanism 94 | encoded_ref_g = tf.nn.conv1d(ref, W_ref_g, 1, "VALID", name="encoded_ref_g") # [Batch size, seq_length, n_hidden] 95 | scores_g = tf.reduce_sum(v_g * tf.tanh(encoded_ref_g), [-1], name="scores_g") # [Batch size, seq_length] 96 | attention_g = tf.nn.softmax(scores_g, name="attention_g") 97 | # 1 glimpse = Linear combination of reference vectors (defines new query vector) 98 | glimpse = tf.multiply(ref, tf.expand_dims(attention_g,2)) 99 | glimpse = tf.reduce_sum(glimpse,1) 100 | return glimpse -------------------------------------------------------------------------------- /problems/vrp/state_cvrp.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import NamedTuple 3 | from utils.boolmask import mask_long2bool, mask_long_scatter 4 | 5 | 6 | class StateCVRP(NamedTuple): 7 | # Fixed input 8 | coords: torch.Tensor # Depot + loc 9 | demand: torch.Tensor 10 | 11 | # If this state contains multiple copies (i.e. beam search) for the same instance, then for memory efficiency 12 | # the coords and demands tensors are not kept multiple times, so we need to use the ids to index the correct rows. 13 | ids: torch.Tensor # Keeps track of original fixed data index of rows 14 | 15 | # State 16 | prev_a: torch.Tensor 17 | used_capacity: torch.Tensor 18 | visited_: torch.Tensor # Keeps track of nodes that have been visited 19 | lengths: torch.Tensor 20 | cur_coord: torch.Tensor 21 | i: torch.Tensor # Keeps track of step 22 | 23 | VEHICLE_CAPACITY = 1.0 # Hardcoded 24 | 25 | @property 26 | def visited(self): 27 | if self.visited_.dtype == torch.uint8: 28 | return self.visited_ 29 | else: 30 | return mask_long2bool(self.visited_, n=self.demand.size(-1)) 31 | 32 | @property 33 | def dist(self): 34 | return (self.coords[:, :, None, :] - self.coords[:, None, :, :]).norm(p=2, dim=-1) 35 | 36 | def __getitem__(self, key): 37 | assert torch.is_tensor(key) or isinstance(key, slice) # If tensor, idx all tensors by this tensor: 38 | return self._replace( 39 | ids=self.ids[key], 40 | prev_a=self.prev_a[key], 41 | used_capacity=self.used_capacity[key], 42 | visited_=self.visited_[key], 43 | lengths=self.lengths[key], 44 | cur_coord=self.cur_coord[key], 45 | ) 46 | 47 | # Warning: cannot override len of NamedTuple, len should be number of fields, not batch size 48 | # def __len__(self): 49 | # return len(self.used_capacity) 50 | 51 | @staticmethod 52 | def initialize(input, visited_dtype=torch.uint8): 53 | 54 | depot = input['depot'] 55 | loc = input['loc'] 56 | demand = input['demand'] 57 | 58 | batch_size, n_loc, _ = loc.size() 59 | return StateCVRP( 60 | coords=torch.cat((depot[:, None, :], loc), -2), 61 | demand=demand, 62 | ids=torch.arange(batch_size, dtype=torch.int64, device=loc.device)[:, None], # Add steps dimension 63 | prev_a=torch.zeros(batch_size, 1, dtype=torch.long, device=loc.device), 64 | used_capacity=demand.new_zeros(batch_size, 1), 65 | visited_=( # Visited as mask is easier to understand, as long more memory efficient 66 | # Keep visited_ with depot so we can scatter efficiently 67 | torch.zeros( 68 | batch_size, 1, n_loc + 1, 69 | dtype=torch.uint8, device=loc.device 70 | ) 71 | if visited_dtype == torch.uint8 72 | else torch.zeros(batch_size, 1, (n_loc + 63) // 64, dtype=torch.int64, device=loc.device) # Ceil 73 | ), 74 | lengths=torch.zeros(batch_size, 1, device=loc.device), 75 | cur_coord=input['depot'][:, None, :], # Add step dimension 76 | i=torch.zeros(1, dtype=torch.int64, device=loc.device) # Vector with length num_steps 77 | ) 78 | 79 | def get_final_cost(self): 80 | 81 | assert self.all_finished() 82 | 83 | return self.lengths + (self.coords[self.ids, 0, :] - self.cur_coord).norm(p=2, dim=-1) 84 | 85 | def update(self, selected): 86 | 87 | assert self.i.size(0) == 1, "Can only update if state represents single step" 88 | 89 | # Update the state 90 | selected = selected[:, None] # Add dimension for step 91 | prev_a = selected 92 | n_loc = self.demand.size(-1) # Excludes depot 93 | 94 | # Add the length 95 | cur_coord = self.coords[self.ids, selected] 96 | # cur_coord = self.coords.gather( 97 | # 1, 98 | # selected[:, None].expand(selected.size(0), 1, self.coords.size(-1)) 99 | # )[:, 0, :] 100 | lengths = self.lengths + (cur_coord - self.cur_coord).norm(p=2, dim=-1) # (batch_dim, 1) 101 | 102 | # Not selected_demand is demand of first node (by clamp) so incorrect for nodes that visit depot! 103 | #selected_demand = self.demand.gather(-1, torch.clamp(prev_a - 1, 0, n_loc - 1)) 104 | selected_demand = self.demand[self.ids, torch.clamp(prev_a - 1, 0, n_loc - 1)] 105 | 106 | # Increase capacity if depot is not visited, otherwise set to 0 107 | #used_capacity = torch.where(selected == 0, 0, self.used_capacity + selected_demand) 108 | used_capacity = (self.used_capacity + selected_demand) * (prev_a != 0).float() 109 | 110 | if self.visited_.dtype == torch.uint8: 111 | # Note: here we do not subtract one as we have to scatter so the first column allows scattering depot 112 | # Add one dimension since we write a single value 113 | visited_ = self.visited_.scatter(-1, prev_a[:, :, None], 1) 114 | else: 115 | # This works, will not set anything if prev_a -1 == -1 (depot) 116 | visited_ = mask_long_scatter(self.visited_, prev_a - 1) 117 | 118 | return self._replace( 119 | prev_a=prev_a, used_capacity=used_capacity, visited_=visited_, 120 | lengths=lengths, cur_coord=cur_coord, i=self.i + 1 121 | ) 122 | 123 | def all_finished(self): 124 | return self.i.item() >= self.demand.size(-1) and self.visited.all() 125 | 126 | def get_finished(self): 127 | return self.visited.sum(-1) == self.visited.size(-1) 128 | 129 | def get_current_node(self): 130 | return self.prev_a 131 | 132 | def get_mask(self): 133 | """ 134 | Gets a (batch_size, n_loc + 1) mask with the feasible actions (0 = depot), depends on already visited and 135 | remaining capacity. 0 = feasible, 1 = infeasible 136 | Forbids to visit depot twice in a row, unless all nodes have been visited 137 | :return: 138 | """ 139 | 140 | if self.visited_.dtype == torch.uint8: 141 | visited_loc = self.visited_[:, :, 1:] 142 | else: 143 | visited_loc = mask_long2bool(self.visited_, n=self.demand.size(-1)) 144 | 145 | # For demand steps_dim is inserted by indexing with id, for used_capacity insert node dim for broadcasting 146 | exceeds_cap = (self.demand[self.ids, :] + self.used_capacity[:, :, None] > self.VEHICLE_CAPACITY) 147 | # Nodes that cannot be visited are already visited or too much demand to be served now 148 | mask_loc = visited_loc.to(exceeds_cap.dtype) | exceeds_cap 149 | 150 | # Cannot visit the depot if just visited and still unserved nodes 151 | mask_depot = (self.prev_a == 0) & ((mask_loc == 0).int().sum(-1) > 0) 152 | return torch.cat((mask_depot[:, :, None], mask_loc), -1) 153 | 154 | def construct_solutions(self, actions): 155 | return actions 156 | -------------------------------------------------------------------------------- /problems/vrp/state_sdvrp.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from typing import NamedTuple 3 | 4 | 5 | class StateSDVRP(NamedTuple): 6 | # Fixed input 7 | coords: torch.Tensor 8 | demand: torch.Tensor 9 | 10 | # If this state contains multiple copies (i.e. beam search) for the same instance, then for memory efficiency 11 | # the coords and demands tensors are not kept multiple times, so we need to use the ids to index the correct rows. 12 | ids: torch.Tensor # Keeps track of original fixed data index of rows 13 | 14 | # State 15 | prev_a: torch.Tensor 16 | used_capacity: torch.Tensor 17 | demands_with_depot: torch.Tensor # Keeps track of remaining demands 18 | lengths: torch.Tensor 19 | cur_coord: torch.Tensor 20 | i: torch.Tensor # Keeps track of step 21 | 22 | VEHICLE_CAPACITY = 1.0 # Hardcoded 23 | 24 | def __getitem__(self, key): 25 | assert torch.is_tensor(key) or isinstance(key, slice) # If tensor, idx all tensors by this tensor: 26 | return self._replace( 27 | ids=self.ids[key], 28 | prev_a=self.prev_a[key], 29 | used_capacity=self.used_capacity[key], 30 | demands_with_depot=self.demands_with_depot[key], 31 | lengths=self.lengths[key], 32 | cur_coord=self.cur_coord[key], 33 | ) 34 | 35 | @staticmethod 36 | def initialize(input): 37 | 38 | depot = input['depot'] 39 | loc = input['loc'] 40 | demand = input['demand'] 41 | 42 | batch_size, n_loc, _ = loc.size() 43 | return StateSDVRP( 44 | coords=torch.cat((depot[:, None, :], loc), -2), 45 | demand=demand, 46 | ids=torch.arange(batch_size, dtype=torch.int64, device=loc.device)[:, None], # Add steps dimension 47 | prev_a=torch.zeros(batch_size, 1, dtype=torch.long, device=loc.device), 48 | used_capacity=demand.new_zeros(batch_size, 1), 49 | demands_with_depot=torch.cat(( 50 | demand.new_zeros(batch_size, 1), 51 | demand[:, :] 52 | ), 1)[:, None, :], 53 | lengths=torch.zeros(batch_size, 1, device=loc.device), 54 | cur_coord=input['depot'][:, None, :], # Add step dimension 55 | i=torch.zeros(1, dtype=torch.int64, device=loc.device) # Vector with length num_steps 56 | ) 57 | 58 | def get_final_cost(self): 59 | 60 | assert self.all_finished() 61 | 62 | return self.lengths + (self.coords[self.ids, 0, :] - self.cur_coord).norm(p=2, dim=-1) 63 | 64 | def update(self, selected): 65 | 66 | assert self.i.size(0) == 1, "Can only update if state represents single step" 67 | 68 | # Update the state 69 | selected = selected[:, None] # Add dimension for step 70 | prev_a = selected 71 | 72 | # Add the length 73 | cur_coord = self.coords[self.ids, selected] 74 | lengths = self.lengths + (cur_coord - self.cur_coord).norm(p=2, dim=-1) # (batch_dim, 1) 75 | 76 | # Not selected_demand is demand of first node (by clamp) so incorrect for nodes that visit depot! 77 | selected_demand = self.demands_with_depot.gather(-1, prev_a[:, :, None])[:, :, 0] 78 | delivered_demand = torch.min(selected_demand, self.VEHICLE_CAPACITY - self.used_capacity) 79 | 80 | # Increase capacity if depot is not visited, otherwise set to 0 81 | #used_capacity = torch.where(selected == 0, 0, self.used_capacity + delivered_demand) 82 | used_capacity = (self.used_capacity + delivered_demand) * (prev_a != 0).float() 83 | 84 | # demands_with_depot = demands_with_depot.clone()[:, 0, :] 85 | # Add one dimension since we write a single value 86 | demands_with_depot = self.demands_with_depot.scatter( 87 | -1, 88 | prev_a[:, :, None], 89 | self.demands_with_depot.gather(-1, prev_a[:, :, None]) - delivered_demand[:, :, None] 90 | ) 91 | 92 | return self._replace( 93 | prev_a=prev_a, used_capacity=used_capacity, demands_with_depot=demands_with_depot, 94 | lengths=lengths, cur_coord=cur_coord, i=self.i + 1 95 | ) 96 | 97 | def all_finished(self): 98 | return self.i.item() >= self.demands_with_depot.size(-1) and not (self.demands_with_depot > 0).any() 99 | 100 | def get_current_node(self): 101 | return self.prev_a 102 | 103 | def get_mask(self): 104 | """ 105 | Gets a (batch_size, n_loc + 1) mask with the feasible actions (0 = depot), depends on already visited and 106 | remaining capacity. 0 = feasible, 1 = infeasible 107 | Forbids to visit depot twice in a row, unless all nodes have been visited 108 | :return: 109 | """ 110 | 111 | # Nodes that cannot be visited are already visited or too much demand to be served now 112 | mask_loc = (self.demands_with_depot[:, :, 1:] == 0) | (self.used_capacity[:, :, None] >= self.VEHICLE_CAPACITY) 113 | 114 | # Cannot visit the depot if just visited and still unserved nodes 115 | mask_depot = (self.prev_a == 0) & ((mask_loc == 0).int().sum(-1) > 0) 116 | return torch.cat((mask_depot[:, :, None], mask_loc), -1) 117 | 118 | def construct_solutions(self, actions): 119 | return actions 120 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import json 5 | import pprint as pp 6 | 7 | import torch 8 | import torch.optim as optim 9 | from tensorboard_logger import Logger as TbLogger 10 | 11 | from nets.critic_network import CriticNetwork 12 | from options import get_options 13 | from train import train_epoch, validate, get_inner_model 14 | from reinforce_baselines import NoBaseline, ExponentialBaseline, CriticBaseline, RolloutBaseline, WarmupBaseline 15 | from nets.attention_model import AttentionModel 16 | from nets.pointer_network import PointerNetwork, CriticNetworkLSTM 17 | from utils import torch_load_cpu, load_problem 18 | 19 | 20 | def run(opts): 21 | 22 | # Pretty print the run args 23 | pp.pprint(vars(opts)) 24 | 25 | # Set the random seed 26 | torch.manual_seed(opts.seed) 27 | 28 | # Optionally configure tensorboard 29 | tb_logger = None 30 | if not opts.no_tensorboard: 31 | tb_logger = TbLogger(os.path.join(opts.log_dir, "{}_{}".format(opts.problem, opts.graph_size), opts.run_name)) 32 | 33 | os.makedirs(opts.save_dir) 34 | # Save arguments so exact configuration can always be found 35 | with open(os.path.join(opts.save_dir, "args.json"), 'w') as f: 36 | json.dump(vars(opts), f, indent=True) 37 | 38 | # Set the device 39 | opts.device = torch.device("cuda:0" if opts.use_cuda else "cpu") 40 | 41 | # Figure out what's the problem 42 | problem = load_problem(opts.problem) 43 | 44 | # Load data from load_path 45 | load_data = {} 46 | assert opts.load_path is None or opts.resume is None, "Only one of load path and resume can be given" 47 | load_path = opts.load_path if opts.load_path is not None else opts.resume 48 | if load_path is not None: 49 | print(' [*] Loading data from {}'.format(load_path)) 50 | load_data = torch_load_cpu(load_path) 51 | 52 | # Initialize model 53 | model_class = { 54 | 'attention': AttentionModel, 55 | 'pointer': PointerNetwork 56 | }.get(opts.model, None) 57 | assert model_class is not None, "Unknown model: {}".format(model_class) 58 | model = model_class( 59 | opts.embedding_dim, 60 | opts.hidden_dim, 61 | problem, 62 | n_encode_layers=opts.n_encode_layers, 63 | mask_inner=True, 64 | mask_logits=True, 65 | normalization=opts.normalization, 66 | tanh_clipping=opts.tanh_clipping, 67 | checkpoint_encoder=opts.checkpoint_encoder, 68 | shrink_size=opts.shrink_size 69 | ).to(opts.device) 70 | 71 | if opts.use_cuda and torch.cuda.device_count() > 1: 72 | model = torch.nn.DataParallel(model) 73 | 74 | # Overwrite model parameters by parameters to load 75 | model_ = get_inner_model(model) 76 | model_.load_state_dict({**model_.state_dict(), **load_data.get('model', {})}) 77 | 78 | # Initialize baseline 79 | if opts.baseline == 'exponential': 80 | baseline = ExponentialBaseline(opts.exp_beta) 81 | elif opts.baseline == 'critic' or opts.baseline == 'critic_lstm': 82 | assert problem.NAME == 'tsp', "Critic only supported for TSP" 83 | baseline = CriticBaseline( 84 | ( 85 | CriticNetworkLSTM( 86 | 2, 87 | opts.embedding_dim, 88 | opts.hidden_dim, 89 | opts.n_encode_layers, 90 | opts.tanh_clipping 91 | ) 92 | if opts.baseline == 'critic_lstm' 93 | else 94 | CriticNetwork( 95 | 2, 96 | opts.embedding_dim, 97 | opts.hidden_dim, 98 | opts.n_encode_layers, 99 | opts.normalization 100 | ) 101 | ).to(opts.device) 102 | ) 103 | elif opts.baseline == 'rollout': 104 | baseline = RolloutBaseline(model, problem, opts) 105 | else: 106 | assert opts.baseline is None, "Unknown baseline: {}".format(opts.baseline) 107 | baseline = NoBaseline() 108 | 109 | if opts.bl_warmup_epochs > 0: 110 | baseline = WarmupBaseline(baseline, opts.bl_warmup_epochs, warmup_exp_beta=opts.exp_beta) 111 | 112 | # Load baseline from data, make sure script is called with same type of baseline 113 | if 'baseline' in load_data: 114 | baseline.load_state_dict(load_data['baseline']) 115 | 116 | # Initialize optimizer 117 | optimizer = optim.Adam( 118 | [{'params': model.parameters(), 'lr': opts.lr_model}] 119 | + ( 120 | [{'params': baseline.get_learnable_parameters(), 'lr': opts.lr_critic}] 121 | if len(baseline.get_learnable_parameters()) > 0 122 | else [] 123 | ) 124 | ) 125 | 126 | # Load optimizer state 127 | if 'optimizer' in load_data: 128 | optimizer.load_state_dict(load_data['optimizer']) 129 | for state in optimizer.state.values(): 130 | for k, v in state.items(): 131 | # if isinstance(v, torch.Tensor): 132 | if torch.is_tensor(v): 133 | state[k] = v.to(opts.device) 134 | 135 | # Initialize learning rate scheduler, decay by lr_decay once per epoch! 136 | lr_scheduler = optim.lr_scheduler.LambdaLR(optimizer, lambda epoch: opts.lr_decay ** epoch) 137 | 138 | # Start the actual training loop 139 | val_dataset = problem.make_dataset( 140 | size=opts.graph_size, num_samples=opts.val_size, filename=opts.val_dataset, distribution=opts.data_distribution) 141 | 142 | if opts.resume: 143 | epoch_resume = int(os.path.splitext(os.path.split(opts.resume)[-1])[0].split("-")[1]) 144 | 145 | torch.set_rng_state(load_data['rng_state']) 146 | if opts.use_cuda: 147 | torch.cuda.set_rng_state_all(load_data['cuda_rng_state']) 148 | # Set the random states 149 | # Dumping of state was done before epoch callback, so do that now (model is loaded) 150 | baseline.epoch_callback(model, epoch_resume) 151 | print("Resuming after {}".format(epoch_resume)) 152 | opts.epoch_start = epoch_resume + 1 153 | 154 | if opts.eval_only: 155 | validate(model, val_dataset, opts) 156 | else: 157 | for epoch in range(opts.epoch_start, opts.epoch_start + opts.n_epochs): 158 | train_epoch( 159 | model, 160 | optimizer, 161 | baseline, 162 | lr_scheduler, 163 | epoch, 164 | val_dataset, 165 | problem, 166 | tb_logger, 167 | opts 168 | ) 169 | 170 | 171 | if __name__ == "__main__": 172 | run(get_options()) 173 | -------------------------------------------------------------------------------- /train.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from tqdm import tqdm 4 | import torch 5 | import math 6 | 7 | from torch.utils.data import DataLoader 8 | from torch.nn import DataParallel 9 | 10 | from nets.attention_model import set_decode_type 11 | from utils.log_utils import log_values 12 | from utils import move_to 13 | 14 | 15 | def get_inner_model(model): 16 | return model.module if isinstance(model, DataParallel) else model 17 | 18 | 19 | def validate(model, dataset, opts): 20 | # Validate 21 | print('Validating...') 22 | cost = rollout(model, dataset, opts) 23 | avg_cost = cost.mean() 24 | print('Validation overall avg_cost: {} +- {}'.format( 25 | avg_cost, torch.std(cost) / math.sqrt(len(cost)))) 26 | 27 | return avg_cost 28 | 29 | 30 | def rollout(model, dataset, opts): 31 | # Put in greedy evaluation mode! 32 | set_decode_type(model, "greedy") 33 | model.eval() 34 | 35 | def eval_model_bat(bat): 36 | with torch.no_grad(): 37 | cost, _ = model(move_to(bat, opts.device)) 38 | return cost.data.cpu() 39 | 40 | return torch.cat([ 41 | eval_model_bat(bat) 42 | for bat 43 | in tqdm(DataLoader(dataset, batch_size=opts.eval_batch_size), disable=opts.no_progress_bar) 44 | ], 0) 45 | 46 | 47 | def clip_grad_norms(param_groups, max_norm=math.inf): 48 | """ 49 | Clips the norms for all param groups to max_norm and returns gradient norms before clipping 50 | :param optimizer: 51 | :param max_norm: 52 | :param gradient_norms_log: 53 | :return: grad_norms, clipped_grad_norms: list with (clipped) gradient norms per group 54 | """ 55 | grad_norms = [ 56 | torch.nn.utils.clip_grad_norm_( 57 | group['params'], 58 | max_norm if max_norm > 0 else math.inf, # Inf so no clipping but still call to calc 59 | norm_type=2 60 | ) 61 | for group in param_groups 62 | ] 63 | grad_norms_clipped = [min(g_norm, max_norm) for g_norm in grad_norms] if max_norm > 0 else grad_norms 64 | return grad_norms, grad_norms_clipped 65 | 66 | 67 | def train_epoch(model, optimizer, baseline, lr_scheduler, epoch, val_dataset, problem, tb_logger, opts): 68 | print("Start train epoch {}, lr={} for run {}".format(epoch, optimizer.param_groups[0]['lr'], opts.run_name)) 69 | step = epoch * (opts.epoch_size // opts.batch_size) 70 | start_time = time.time() 71 | 72 | if not opts.no_tensorboard: 73 | tb_logger.log_value('learnrate_pg0', optimizer.param_groups[0]['lr'], step) 74 | 75 | # Generate new training data for each epoch 76 | training_dataset = baseline.wrap_dataset(problem.make_dataset( 77 | size=opts.graph_size, num_samples=opts.epoch_size, distribution=opts.data_distribution)) 78 | training_dataloader = DataLoader(training_dataset, batch_size=opts.batch_size, num_workers=1) 79 | 80 | # Put model in train mode! 81 | model.train() 82 | set_decode_type(model, "sampling") 83 | 84 | for batch_id, batch in enumerate(tqdm(training_dataloader, disable=opts.no_progress_bar)): 85 | 86 | train_batch( 87 | model, 88 | optimizer, 89 | baseline, 90 | epoch, 91 | batch_id, 92 | step, 93 | batch, 94 | tb_logger, 95 | opts 96 | ) 97 | 98 | step += 1 99 | 100 | epoch_duration = time.time() - start_time 101 | print("Finished epoch {}, took {} s".format(epoch, time.strftime('%H:%M:%S', time.gmtime(epoch_duration)))) 102 | 103 | if (opts.checkpoint_epochs != 0 and epoch % opts.checkpoint_epochs == 0) or epoch == opts.n_epochs - 1: 104 | print('Saving model and state...') 105 | torch.save( 106 | { 107 | 'model': get_inner_model(model).state_dict(), 108 | 'optimizer': optimizer.state_dict(), 109 | 'rng_state': torch.get_rng_state(), 110 | 'cuda_rng_state': torch.cuda.get_rng_state_all(), 111 | 'baseline': baseline.state_dict() 112 | }, 113 | os.path.join(opts.save_dir, 'epoch-{}.pt'.format(epoch)) 114 | ) 115 | 116 | avg_reward = validate(model, val_dataset, opts) 117 | 118 | if not opts.no_tensorboard: 119 | tb_logger.log_value('val_avg_reward', avg_reward, step) 120 | 121 | baseline.epoch_callback(model, epoch) 122 | 123 | # lr_scheduler should be called at end of epoch 124 | lr_scheduler.step() 125 | 126 | 127 | def train_batch( 128 | model, 129 | optimizer, 130 | baseline, 131 | epoch, 132 | batch_id, 133 | step, 134 | batch, 135 | tb_logger, 136 | opts 137 | ): 138 | x, bl_val = baseline.unwrap_batch(batch) 139 | x = move_to(x, opts.device) 140 | bl_val = move_to(bl_val, opts.device) if bl_val is not None else None 141 | 142 | # Evaluate model, get costs and log probabilities 143 | cost, log_likelihood = model(x) 144 | 145 | # Evaluate baseline, get baseline loss if any (only for critic) 146 | bl_val, bl_loss = baseline.eval(x, cost) if bl_val is None else (bl_val, 0) 147 | 148 | # Calculate loss 149 | reinforce_loss = ((cost - bl_val) * log_likelihood).mean() 150 | loss = reinforce_loss + bl_loss 151 | 152 | # Perform backward pass and optimization step 153 | optimizer.zero_grad() 154 | loss.backward() 155 | # Clip gradient norms and get (clipped) gradient norms for logging 156 | grad_norms = clip_grad_norms(optimizer.param_groups, opts.max_grad_norm) 157 | optimizer.step() 158 | 159 | # Logging 160 | if step % int(opts.log_step) == 0: 161 | log_values(cost, grad_norms, epoch, batch_id, step, 162 | log_likelihood, reinforce_loss, bl_loss, tb_logger, opts) 163 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .functions import * -------------------------------------------------------------------------------- /utils/boolmask.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn.functional as F 3 | 4 | 5 | def _pad_mask(mask): 6 | # By taking -size % 8, we get 0 if exactly divisible by 8 7 | # and required padding otherwise (i.e. -1 % 8 = 7 pad) 8 | pad = -mask.size(-1) % 8 9 | if pad != 0: 10 | mask = F.pad(mask, [0, pad]) 11 | return mask, mask.size(-1) // 8 12 | 13 | 14 | def _mask_bool2byte(mask): 15 | assert mask.dtype == torch.uint8 16 | # assert (mask <= 1).all() # Precondition, disabled for efficiency 17 | mask, d = _pad_mask(mask) 18 | return (mask.view(*mask.size()[:-1], d, 8) << torch.arange(8, out=mask.new())).sum(-1, dtype=torch.uint8) 19 | 20 | 21 | def _mask_byte2long(mask): 22 | assert mask.dtype == torch.uint8 23 | mask, d = _pad_mask(mask) 24 | # Note this corresponds to a temporary factor 8 25 | # memory overhead by converting to long before summing 26 | # Alternatively, aggregate using for loop 27 | return (mask.view(*mask.size()[:-1], d, 8).long() << (torch.arange(8, dtype=torch.int64, device=mask.device) * 8)).sum(-1) 28 | 29 | 30 | def mask_bool2long(mask): 31 | assert mask.dtype == torch.uint8 32 | return _mask_byte2long(_mask_bool2byte(mask)) 33 | 34 | 35 | def _mask_long2byte(mask, n=None): 36 | if n is None: 37 | n = 8 * mask.size(-1) 38 | return (mask[..., None] >> (torch.arange(8, out=mask.new()) * 8))[..., :n].to(torch.uint8).view(*mask.size()[:-1], -1)[..., :n] 39 | 40 | 41 | def _mask_byte2bool(mask, n=None): 42 | if n is None: 43 | n = 8 * mask.size(-1) 44 | return (mask[..., None] & (mask.new_ones(8) << torch.arange(8, out=mask.new()) * 1)).view(*mask.size()[:-1], -1)[..., :n] > 0 45 | 46 | 47 | def mask_long2bool(mask, n=None): 48 | assert mask.dtype == torch.int64 49 | return _mask_byte2bool(_mask_long2byte(mask), n=n) 50 | 51 | 52 | def mask_long_scatter(mask, values, check_unset=True): 53 | """ 54 | Sets values in mask in dimension -1 with arbitrary batch dimensions 55 | If values contains -1, nothing is set 56 | Note: does not work for setting multiple values at once (like normal scatter) 57 | """ 58 | assert mask.size()[:-1] == values.size() 59 | rng = torch.arange(mask.size(-1), out=mask.new()) 60 | values_ = values[..., None] # Need to broadcast up do mask dim 61 | # This indicates in which value of the mask a bit should be set 62 | where = (values_ >= (rng * 64)) & (values_ < ((rng + 1) * 64)) 63 | # Optional: check that bit is not already set 64 | assert not (check_unset and ((mask & (where.long() << (values_ % 64))) > 0).any()) 65 | # Set bit by shifting a 1 to the correct position 66 | # (% not strictly necessary as bitshift is cyclic) 67 | # since where is 0 if no value needs to be set, the bitshift has no effect 68 | return mask | (where.long() << (values_ % 64)) 69 | -------------------------------------------------------------------------------- /utils/data_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | 4 | 5 | def check_extension(filename): 6 | if os.path.splitext(filename)[1] != ".pkl": 7 | return filename + ".pkl" 8 | return filename 9 | 10 | 11 | def save_dataset(dataset, filename): 12 | 13 | filedir = os.path.split(filename)[0] 14 | 15 | if not os.path.isdir(filedir): 16 | os.makedirs(filedir) 17 | 18 | with open(check_extension(filename), 'wb') as f: 19 | pickle.dump(dataset, f, pickle.HIGHEST_PROTOCOL) 20 | 21 | 22 | def load_dataset(filename): 23 | 24 | with open(check_extension(filename), 'rb') as f: 25 | return pickle.load(f) -------------------------------------------------------------------------------- /utils/functions.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | import torch 4 | import numpy as np 5 | import os 6 | import json 7 | from tqdm import tqdm 8 | from multiprocessing.dummy import Pool as ThreadPool 9 | from multiprocessing import Pool 10 | import torch.nn.functional as F 11 | 12 | 13 | def load_problem(name): 14 | from problems import TSP, CVRP, SDVRP, OP, PCTSPDet, PCTSPStoch 15 | problem = { 16 | 'tsp': TSP, 17 | 'cvrp': CVRP, 18 | 'sdvrp': SDVRP, 19 | 'op': OP, 20 | 'pctsp_det': PCTSPDet, 21 | 'pctsp_stoch': PCTSPStoch, 22 | }.get(name, None) 23 | assert problem is not None, "Currently unsupported problem: {}!".format(name) 24 | return problem 25 | 26 | 27 | def torch_load_cpu(load_path): 28 | return torch.load(load_path, map_location=lambda storage, loc: storage) # Load on CPU 29 | 30 | 31 | def move_to(var, device): 32 | if isinstance(var, dict): 33 | return {k: move_to(v, device) for k, v in var.items()} 34 | return var.to(device) 35 | 36 | 37 | def _load_model_file(load_path, model): 38 | """Loads the model with parameters from the file and returns optimizer state dict if it is in the file""" 39 | 40 | # Load the model parameters from a saved state 41 | load_optimizer_state_dict = None 42 | print(' [*] Loading model from {}'.format(load_path)) 43 | 44 | load_data = torch.load( 45 | os.path.join( 46 | os.getcwd(), 47 | load_path 48 | ), map_location=lambda storage, loc: storage) 49 | 50 | if isinstance(load_data, dict): 51 | load_optimizer_state_dict = load_data.get('optimizer', None) 52 | load_model_state_dict = load_data.get('model', load_data) 53 | else: 54 | load_model_state_dict = load_data.state_dict() 55 | 56 | state_dict = model.state_dict() 57 | 58 | state_dict.update(load_model_state_dict) 59 | 60 | model.load_state_dict(state_dict) 61 | 62 | return model, load_optimizer_state_dict 63 | 64 | 65 | def load_args(filename): 66 | with open(filename, 'r') as f: 67 | args = json.load(f) 68 | 69 | # Backwards compatibility 70 | if 'data_distribution' not in args: 71 | args['data_distribution'] = None 72 | probl, *dist = args['problem'].split("_") 73 | if probl == "op": 74 | args['problem'] = probl 75 | args['data_distribution'] = dist[0] 76 | return args 77 | 78 | 79 | def load_model(path, epoch=None): 80 | from nets.attention_model import AttentionModel 81 | from nets.pointer_network import PointerNetwork 82 | 83 | if os.path.isfile(path): 84 | model_filename = path 85 | path = os.path.dirname(model_filename) 86 | elif os.path.isdir(path): 87 | if epoch is None: 88 | epoch = max( 89 | int(os.path.splitext(filename)[0].split("-")[1]) 90 | for filename in os.listdir(path) 91 | if os.path.splitext(filename)[1] == '.pt' 92 | ) 93 | model_filename = os.path.join(path, 'epoch-{}.pt'.format(epoch)) 94 | else: 95 | assert False, "{} is not a valid directory or file".format(path) 96 | 97 | args = load_args(os.path.join(path, 'args.json')) 98 | 99 | problem = load_problem(args['problem']) 100 | 101 | model_class = { 102 | 'attention': AttentionModel, 103 | 'pointer': PointerNetwork 104 | }.get(args.get('model', 'attention'), None) 105 | assert model_class is not None, "Unknown model: {}".format(model_class) 106 | 107 | model = model_class( 108 | args['embedding_dim'], 109 | args['hidden_dim'], 110 | problem, 111 | n_encode_layers=args['n_encode_layers'], 112 | mask_inner=True, 113 | mask_logits=True, 114 | normalization=args['normalization'], 115 | tanh_clipping=args['tanh_clipping'], 116 | checkpoint_encoder=args.get('checkpoint_encoder', False), 117 | shrink_size=args.get('shrink_size', None) 118 | ) 119 | # Overwrite model parameters by parameters to load 120 | load_data = torch_load_cpu(model_filename) 121 | model.load_state_dict({**model.state_dict(), **load_data.get('model', {})}) 122 | 123 | model, *_ = _load_model_file(model_filename, model) 124 | 125 | model.eval() # Put in eval mode 126 | 127 | return model, args 128 | 129 | 130 | def parse_softmax_temperature(raw_temp): 131 | # Load from file 132 | if os.path.isfile(raw_temp): 133 | return np.loadtxt(raw_temp)[-1, 0] 134 | return float(raw_temp) 135 | 136 | 137 | def run_all_in_pool(func, directory, dataset, opts, use_multiprocessing=True): 138 | # # Test 139 | # res = func((directory, 'test', *dataset[0])) 140 | # return [res] 141 | 142 | num_cpus = os.cpu_count() if opts.cpus is None else opts.cpus 143 | 144 | w = len(str(len(dataset) - 1)) 145 | offset = getattr(opts, 'offset', None) 146 | if offset is None: 147 | offset = 0 148 | ds = dataset[offset:(offset + opts.n if opts.n is not None else len(dataset))] 149 | pool_cls = (Pool if use_multiprocessing and num_cpus > 1 else ThreadPool) 150 | with pool_cls(num_cpus) as pool: 151 | results = list(tqdm(pool.imap( 152 | func, 153 | [ 154 | ( 155 | directory, 156 | str(i + offset).zfill(w), 157 | *problem 158 | ) 159 | for i, problem in enumerate(ds) 160 | ] 161 | ), total=len(ds), mininterval=opts.progress_bar_mininterval)) 162 | 163 | failed = [str(i + offset) for i, res in enumerate(results) if res is None] 164 | assert len(failed) == 0, "Some instances failed: {}".format(" ".join(failed)) 165 | return results, num_cpus 166 | 167 | 168 | def do_batch_rep(v, n): 169 | if isinstance(v, dict): 170 | return {k: do_batch_rep(v_, n) for k, v_ in v.items()} 171 | elif isinstance(v, list): 172 | return [do_batch_rep(v_, n) for v_ in v] 173 | elif isinstance(v, tuple): 174 | return tuple(do_batch_rep(v_, n) for v_ in v) 175 | 176 | return v[None, ...].expand(n, *v.size()).contiguous().view(-1, *v.size()[1:]) 177 | 178 | 179 | def sample_many(inner_func, get_cost_func, input, batch_rep=1, iter_rep=1): 180 | """ 181 | :param input: (batch_size, graph_size, node_dim) input node features 182 | :return: 183 | """ 184 | input = do_batch_rep(input, batch_rep) 185 | 186 | costs = [] 187 | pis = [] 188 | for i in range(iter_rep): 189 | _log_p, pi = inner_func(input) 190 | # pi.view(-1, batch_rep, pi.size(-1)) 191 | cost, mask = get_cost_func(input, pi) 192 | 193 | costs.append(cost.view(batch_rep, -1).t()) 194 | pis.append(pi.view(batch_rep, -1, pi.size(-1)).transpose(0, 1)) 195 | 196 | max_length = max(pi.size(-1) for pi in pis) 197 | # (batch_size * batch_rep, iter_rep, max_length) => (batch_size, batch_rep * iter_rep, max_length) 198 | pis = torch.cat( 199 | [F.pad(pi, (0, max_length - pi.size(-1))) for pi in pis], 200 | 1 201 | ) # .view(embeddings.size(0), batch_rep * iter_rep, max_length) 202 | costs = torch.cat(costs, 1) 203 | 204 | # (batch_size) 205 | mincosts, argmincosts = costs.min(-1) 206 | # (batch_size, minlength) 207 | minpis = pis[torch.arange(pis.size(0), out=argmincosts.new()), argmincosts] 208 | 209 | return minpis, mincosts 210 | -------------------------------------------------------------------------------- /utils/lexsort.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import numpy as np 3 | 4 | 5 | def torch_lexsort(keys, dim=-1): 6 | if keys[0].is_cuda: 7 | return _torch_lexsort_cuda(keys, dim) 8 | else: 9 | # Use numpy lex sort 10 | return torch.from_numpy(np.lexsort([k.numpy() for k in keys], axis=dim)) 11 | 12 | 13 | def _torch_lexsort_cuda(keys, dim=-1): 14 | """ 15 | Function calculates a lexicographical sort order on GPU, similar to np.lexsort 16 | Relies heavily on undocumented behavior of torch.sort, namely that when sorting more than 17 | 2048 entries in the sorting dim, it performs a sort using Thrust and it uses a stable sort 18 | https://github.com/pytorch/pytorch/blob/695fd981924bd805704ecb5ccd67de17c56d7308/aten/src/THC/generic/THCTensorSort.cu#L330 19 | """ 20 | 21 | MIN_NUMEL_STABLE_SORT = 2049 # Minimum number of elements for stable sort 22 | 23 | # Swap axis such that sort dim is last and reshape all other dims to a single (batch) dimension 24 | reordered_keys = tuple(key.transpose(dim, -1).contiguous() for key in keys) 25 | flat_keys = tuple(key.view(-1) for key in keys) 26 | d = keys[0].size(dim) # Sort dimension size 27 | numel = flat_keys[0].numel() 28 | batch_size = numel // d 29 | batch_key = torch.arange(batch_size, dtype=torch.int64, device=keys[0].device)[:, None].repeat(1, d).view(-1) 30 | 31 | flat_keys = flat_keys + (batch_key,) 32 | 33 | # We rely on undocumented behavior that the sort is stable provided that 34 | if numel < MIN_NUMEL_STABLE_SORT: 35 | n_rep = (MIN_NUMEL_STABLE_SORT + numel - 1) // numel # Ceil 36 | rep_key = torch.arange(n_rep, dtype=torch.int64, device=keys[0].device)[:, None].repeat(1, numel).view(-1) 37 | flat_keys = tuple(k.repeat(n_rep) for k in flat_keys) + (rep_key,) 38 | 39 | idx = None # Identity sorting initially 40 | for k in flat_keys: 41 | if idx is None: 42 | _, idx = k.sort(-1) 43 | else: 44 | # Order data according to idx and then apply 45 | # found ordering to current idx (so permutation of permutation) 46 | # such that we can order the next key according to the current sorting order 47 | _, idx_ = k[idx].sort(-1) 48 | idx = idx[idx_] 49 | 50 | # In the end gather only numel and strip of extra sort key 51 | if numel < MIN_NUMEL_STABLE_SORT: 52 | idx = idx[:numel] 53 | 54 | # Get only numel (if we have replicated), swap axis back and shape results 55 | return idx[:numel].view(*reordered_keys[0].size()).transpose(dim, -1) % d 56 | -------------------------------------------------------------------------------- /utils/log_utils.py: -------------------------------------------------------------------------------- 1 | def log_values(cost, grad_norms, epoch, batch_id, step, 2 | log_likelihood, reinforce_loss, bl_loss, tb_logger, opts): 3 | avg_cost = cost.mean().item() 4 | grad_norms, grad_norms_clipped = grad_norms 5 | 6 | # Log values to screen 7 | print('epoch: {}, train_batch_id: {}, avg_cost: {}'.format(epoch, batch_id, avg_cost)) 8 | 9 | print('grad_norm: {}, clipped: {}'.format(grad_norms[0], grad_norms_clipped[0])) 10 | 11 | # Log values to tensorboard 12 | if not opts.no_tensorboard: 13 | tb_logger.log_value('avg_cost', avg_cost, step) 14 | 15 | tb_logger.log_value('actor_loss', reinforce_loss.item(), step) 16 | tb_logger.log_value('nll', -log_likelihood.mean().item(), step) 17 | 18 | tb_logger.log_value('grad_norm', grad_norms[0], step) 19 | tb_logger.log_value('grad_norm_clipped', grad_norms_clipped[0], step) 20 | 21 | if opts.baseline == 'critic': 22 | tb_logger.log_value('critic_loss', bl_loss.item(), step) 23 | tb_logger.log_value('critic_grad_norm', grad_norms[1], step) 24 | tb_logger.log_value('critic_grad_norm_clipped', grad_norms_clipped[1], step) 25 | -------------------------------------------------------------------------------- /utils/monkey_patch.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from itertools import chain 3 | from collections import defaultdict, Iterable 4 | from copy import deepcopy 5 | 6 | 7 | def load_state_dict(self, state_dict): 8 | """Loads the optimizer state. 9 | Arguments: 10 | state_dict (dict): optimizer state. Should be an object returned 11 | from a call to :meth:`state_dict`. 12 | """ 13 | # deepcopy, to be consistent with module API 14 | state_dict = deepcopy(state_dict) 15 | # Validate the state_dict 16 | groups = self.param_groups 17 | saved_groups = state_dict['param_groups'] 18 | 19 | if len(groups) != len(saved_groups): 20 | raise ValueError("loaded state dict has a different number of " 21 | "parameter groups") 22 | param_lens = (len(g['params']) for g in groups) 23 | saved_lens = (len(g['params']) for g in saved_groups) 24 | if any(p_len != s_len for p_len, s_len in zip(param_lens, saved_lens)): 25 | raise ValueError("loaded state dict contains a parameter group " 26 | "that doesn't match the size of optimizer's group") 27 | 28 | # Update the state 29 | id_map = {old_id: p for old_id, p in 30 | zip(chain(*(g['params'] for g in saved_groups)), 31 | chain(*(g['params'] for g in groups)))} 32 | 33 | def cast(param, value): 34 | """Make a deep copy of value, casting all tensors to device of param.""" 35 | if torch.is_tensor(value): 36 | # Floating-point types are a bit special here. They are the only ones 37 | # that are assumed to always match the type of params. 38 | if any(tp in type(param.data).__name__ for tp in {'Half', 'Float', 'Double'}): 39 | value = value.type_as(param.data) 40 | value = value.to(param.device) 41 | return value 42 | elif isinstance(value, dict): 43 | return {k: cast(param, v) for k, v in value.items()} 44 | elif isinstance(value, Iterable): 45 | return type(value)(cast(param, v) for v in value) 46 | else: 47 | return value 48 | 49 | # Copy state assigned to params (and cast tensors to appropriate types). 50 | # State that is not assigned to params is copied as is (needed for 51 | # backward compatibility). 52 | state = defaultdict(dict) 53 | for k, v in state_dict['state'].items(): 54 | if k in id_map: 55 | param = id_map[k] 56 | state[param] = cast(param, v) 57 | else: 58 | state[k] = v 59 | 60 | # Update parameter groups, setting their 'params' value 61 | def update_group(group, new_group): 62 | new_group['params'] = group['params'] 63 | return new_group 64 | 65 | param_groups = [ 66 | update_group(g, ng) for g, ng in zip(groups, saved_groups)] 67 | self.__setstate__({'state': state, 'param_groups': param_groups}) 68 | 69 | 70 | torch.optim.Optimizer.load_state_dict = load_state_dict -------------------------------------------------------------------------------- /utils/tensor_functions.py: -------------------------------------------------------------------------------- 1 | import torch 2 | 3 | 4 | def compute_in_batches(f, calc_batch_size, *args, n=None): 5 | """ 6 | Computes memory heavy function f(*args) in batches 7 | :param n: the total number of elements, optional if it cannot be determined as args[0].size(0) 8 | :param f: The function that is computed, should take only tensors as arguments and return tensor or tuple of tensors 9 | :param calc_batch_size: The batch size to use when computing this function 10 | :param args: Tensor arguments with equally sized first batch dimension 11 | :return: f(*args), this should be one or multiple tensors with equally sized first batch dimension 12 | """ 13 | if n is None: 14 | n = args[0].size(0) 15 | n_batches = (n + calc_batch_size - 1) // calc_batch_size # ceil 16 | if n_batches == 1: 17 | return f(*args) 18 | 19 | # Run all batches 20 | # all_res = [f(*batch_args) for batch_args in zip(*[torch.chunk(arg, n_batches) for arg in args])] 21 | # We do not use torch.chunk such that it also works for other classes that support slicing 22 | all_res = [f(*(arg[i * calc_batch_size:(i + 1) * calc_batch_size] for arg in args)) for i in range(n_batches)] 23 | 24 | # Allow for functions that return None 25 | def safe_cat(chunks, dim=0): 26 | if chunks[0] is None: 27 | assert all(chunk is None for chunk in chunks) 28 | return None 29 | return torch.cat(chunks, dim) 30 | 31 | # Depending on whether the function returned a tuple we need to concatenate each element or only the result 32 | if isinstance(all_res[0], tuple): 33 | return tuple(safe_cat(res_chunks, 0) for res_chunks in zip(*all_res)) 34 | return safe_cat(all_res, 0) 35 | --------------------------------------------------------------------------------