From c3075025a5d20ef48da62fc85d05621f8f6b15ca Mon Sep 17 00:00:00 2001 From: S22 <864453277@qq.com> Date: Mon, 12 Sep 2022 11:55:17 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Refactor=20BasicTS.=20(#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🎉 Refactor BasicTS. * fix confict --- .github/workflows/pylint.yml | 27 ++ .gitignore | 2 - .pylintrc | 435 ++++++++++++++++++ README.md | 183 ++++---- TODO.md | 13 + basicts/__init__.py | 5 + basicts/archs/AGCRN_arch/AGCN.py | 27 -- basicts/archs/AGCRN_arch/AGCRN_arch.py | 106 ----- basicts/archs/AGCRN_arch/__init__.py | 1 - .../D2STGNN_arch/DiffusionBlock/__init__.py | 1 - .../D2STGNN_arch/DiffusionBlock/dif_block.py | 31 -- .../D2STGNN_arch/DiffusionBlock/dif_model.py | 87 ---- .../DynamicGraphConv/Utils/__init__.py | 3 - .../D2STGNN_arch/InherentBlock/__init__.py | 1 - .../D2STGNN_arch/InherentBlock/inh_block.py | 65 --- .../D2STGNN_arch/InherentBlock/inh_model.py | 33 -- basicts/archs/D2STGNN_arch/__init__.py | 1 - basicts/archs/DCRNN_arch/__init__.py | 1 - basicts/archs/DGCRN_arch/__init__.py | 1 - basicts/archs/GMAN_arch/GMAN_arch.py | 380 --------------- basicts/archs/GMAN_arch/__init__.py | 1 - basicts/archs/GTS_arch/__init__.py | 1 - basicts/archs/GraphWaveNet_arch/__init__.py | 1 - basicts/archs/HI_arch/HI_arch.py | 45 -- basicts/archs/HI_arch/__init__.py | 1 - basicts/archs/LSTM_arch/LSTM_arch.py | 44 -- basicts/archs/LSTM_arch/__init__.py | 1 - basicts/archs/MTGNN_arch/MTGNN_arch.py | 140 ------ basicts/archs/MTGNN_arch/__init__.py | 1 - basicts/archs/STGCN_arch/__init__.py | 0 basicts/archs/STID_arch/MLP.py | 24 - basicts/archs/STID_arch/STID_arch.py | 97 ---- basicts/archs/STID_arch/__init__.py | 1 - basicts/archs/STNorm_arch/__init__.py | 1 - basicts/archs/Stat_arch/Stat_arch.py | 153 ------ basicts/archs/Stat_arch/__init__.py | 1 - basicts/archs/StemGNN_arch/__init__.py | 1 - basicts/archs/__init__.py | 24 +- basicts/archs/arch_zoo/agcrn_arch/__init__.py | 3 + basicts/archs/arch_zoo/agcrn_arch/agcn.py | 35 ++ .../archs/arch_zoo/agcrn_arch/agcrn_arch.py | 108 +++++ .../agcrn_arch/agcrn_cell.py} | 16 +- .../archs/arch_zoo/d2stgnn_arch/__init__.py | 3 + .../d2stgnn_arch/d2stgnn_arch.py} | 136 +++--- .../d2stgnn_arch/decouple}/estimation_gate.py | 0 .../d2stgnn_arch/decouple}/residual_decomp.py | 0 .../d2stgnn_arch/difusion_block/__init__.py | 1 + .../d2stgnn_arch/difusion_block/dif_block.py | 34 ++ .../d2stgnn_arch/difusion_block/dif_model.py | 103 +++++ .../d2stgnn_arch/difusion_block}/forecast.py | 13 +- .../dynamic_graph_conv/dy_graph_conv.py} | 30 +- .../dynamic_graph_conv/utils/__init__.py | 3 + .../dynamic_graph_conv/utils}/distance.py | 0 .../dynamic_graph_conv/utils}/mask.py | 0 .../dynamic_graph_conv/utils}/normalizer.py | 0 .../d2stgnn_arch/inherent_block/__init__.py | 1 + .../d2stgnn_arch/inherent_block}/forecast.py | 19 +- .../d2stgnn_arch/inherent_block/inh_block.py | 72 +++ .../d2stgnn_arch/inherent_block/inh_model.py | 37 ++ basicts/archs/arch_zoo/dcrnn_arch/__init__.py | 3 + .../dcrnn_arch/dcrnn_arch.py} | 127 +++-- .../dcrnn_arch/dcrnn_cell.py} | 83 ++-- basicts/archs/arch_zoo/dgcrn_arch/__init__.py | 3 + .../dgcrn_arch/dgcrn_arch.py} | 112 +++-- .../dgcrn_arch/dgcrn_layer.py} | 6 +- basicts/archs/arch_zoo/gts_arch/__init__.py | 3 + .../gts_arch/gts_arch.py} | 52 ++- .../gts_arch/gts_cell.py} | 74 +-- basicts/archs/arch_zoo/gwnet_arch/__init__.py | 3 + .../gwnet_arch/gwnet_arch.py} | 106 +++-- .../archs/arch_zoo/linear_arch/__init__.py | 5 + basicts/archs/arch_zoo/linear_arch/dlinear.py | 98 ++++ basicts/archs/arch_zoo/linear_arch/linear.py | 29 ++ basicts/archs/arch_zoo/linear_arch/nlinear.py | 32 ++ basicts/archs/arch_zoo/mtgnn_arch/__init__.py | 3 + .../archs/arch_zoo/mtgnn_arch/mtgnn_arch.py | 165 +++++++ .../mtgnn_arch/mtgnn_layers.py} | 139 +++--- .../archs/arch_zoo/stemgnn_arch/__init__.py | 3 + .../stemgnn_arch/stemgnn_arch.py} | 115 ++--- basicts/archs/arch_zoo/stgcn_arch/__init__.py | 3 + .../stgcn_arch/stgcn_arch.py} | 27 +- .../stgcn_arch/stgcn_layers.py} | 95 ++-- basicts/archs/arch_zoo/stid_arch/__init__.py | 3 + basicts/archs/arch_zoo/stid_arch/mlp.py | 29 ++ basicts/archs/arch_zoo/stid_arch/stid_arch.py | 115 +++++ .../archs/arch_zoo/stnorm_arch/__init__.py | 3 + .../stnorm_arch/stnorm_arch.py} | 66 +-- basicts/data/__init__.py | 10 + basicts/data/base_dataset.py | 51 -- basicts/data/dataset.py | 73 +++ basicts/{archs => data}/registry.py | 2 +- basicts/data/transform.py | 56 +++ basicts/data/transforms.py | 25 - basicts/launcher.py | 19 + basicts/losses/__init__.py | 4 + basicts/losses/losses.py | 23 +- basicts/metrics/__init__.py | 5 + basicts/metrics/mae.py | 9 +- basicts/metrics/mape.py | 10 +- basicts/metrics/rmse.py | 7 +- basicts/options/AGCRN/AGCRN_Electricity336.py | 111 ----- basicts/options/AGCRN/AGCRN_METR-LA.py | 111 ----- basicts/options/AGCRN/AGCRN_PEMS-BAY.py | 111 ----- basicts/options/AGCRN/AGCRN_PEMS03.py | 111 ----- basicts/options/AGCRN/AGCRN_PEMS04.py | 111 ----- basicts/options/AGCRN/AGCRN_PEMS07.py | 111 ----- basicts/options/AGCRN/AGCRN_PEMS08.py | 111 ----- basicts/options/D2STGNN/D2STGNN_METR-LA.py | 125 ----- basicts/options/D2STGNN/D2STGNN_PEMS-BAY.py | 125 ----- basicts/options/D2STGNN/D2STGNN_PEMS03.py | 125 ----- basicts/options/D2STGNN/D2STGNN_PEMS04.py | 125 ----- basicts/options/D2STGNN/D2STGNN_PEMS07.py | 125 ----- basicts/options/D2STGNN/D2STGNN_PEMS08.py | 125 ----- basicts/options/DCRNN/DCRNN_METR-LA.py | 124 ----- basicts/options/DCRNN/DCRNN_PEMS-BAY.py | 124 ----- basicts/options/DCRNN/DCRNN_PEMS03.py | 124 ----- basicts/options/DCRNN/DCRNN_PEMS04.py | 124 ----- basicts/options/DCRNN/DCRNN_PEMS07.py | 124 ----- basicts/options/DCRNN/DCRNN_PEMS08.py | 124 ----- basicts/options/DGCRN/DGCRN_METR-LA.py | 126 ----- basicts/options/DGCRN/DGCRN_PEMS-BAY.py | 126 ----- basicts/options/DGCRN/DGCRN_PEMS03.py | 126 ----- basicts/options/DGCRN/DGCRN_PEMS04.py | 126 ----- basicts/options/DGCRN/DGCRN_PEMS07.py | 126 ----- basicts/options/DGCRN/DGCRN_PEMS08.py | 126 ----- basicts/options/GMAN/GMAN_METR-LA.py | 111 ----- basicts/options/GMAN/GMAN_PEMS-BAY.py | 111 ----- basicts/options/GMAN/GMAN_PEMS03.py | 111 ----- basicts/options/GMAN/GMAN_PEMS04.py | 111 ----- basicts/options/GMAN/GMAN_PEMS07.py | 111 ----- basicts/options/GMAN/GMAN_PEMS08.py | 111 ----- basicts/options/GTS/GTS_METR-LA.py | 130 ------ basicts/options/GTS/GTS_PEMS-BAY.py | 130 ------ basicts/options/GTS/GTS_PEMS03.py | 130 ------ basicts/options/GTS/GTS_PEMS04.py | 130 ------ basicts/options/GTS/GTS_PEMS07.py | 130 ------ basicts/options/GTS/GTS_PEMS08.py | 130 ------ .../GraphWaveNet/GraphWaveNet_METR-LA.py | 121 ----- .../GraphWaveNet/GraphWaveNet_PEMS-BAY.py | 121 ----- .../GraphWaveNet/GraphWaveNet_PEMS03.py | 121 ----- .../GraphWaveNet/GraphWaveNet_PEMS04.py | 121 ----- .../GraphWaveNet/GraphWaveNet_PEMS07.py | 121 ----- .../GraphWaveNet/GraphWaveNet_PEMS08.py | 121 ----- basicts/options/HI/HI_Electricity336.py | 106 ----- basicts/options/HI/HI_METR-LA.py | 105 ----- basicts/options/HI/HI_PEMS-BAY.py | 105 ----- basicts/options/HI/HI_PEMS03.py | 105 ----- basicts/options/HI/HI_PEMS04.py | 105 ----- basicts/options/HI/HI_PEMS07.py | 106 ----- basicts/options/HI/HI_PEMS08.py | 106 ----- basicts/options/LSTM/LSTM_Electricity336.py | 107 ----- basicts/options/LSTM/LSTM_METR-LA.py | 107 ----- basicts/options/LSTM/LSTM_PEMS-BAY.py | 107 ----- basicts/options/LSTM/LSTM_PEMS03.py | 107 ----- basicts/options/LSTM/LSTM_PEMS04.py | 107 ----- basicts/options/LSTM/LSTM_PEMS07.py | 107 ----- basicts/options/LSTM/LSTM_PEMS08.py | 107 ----- basicts/options/MTGNN/MTGNN_Electricity336.py | 139 ------ basicts/options/STGCN/STGCN_METR-LA.py | 117 ----- basicts/options/STGCN/STGCN_PEMS-BAY.py | 118 ----- basicts/options/STGCN/STGCN_PEMS03.py | 117 ----- basicts/options/STGCN/STGCN_PEMS04.py | 117 ----- basicts/options/STGCN/STGCN_PEMS07.py | 117 ----- basicts/options/STGCN/STGCN_PEMS08.py | 117 ----- basicts/options/STID/STID_Electricity336.py | 112 ----- basicts/options/STID/STID_METR-LA.py | 112 ----- basicts/options/STID/STID_PEMS-BAY.py | 112 ----- basicts/options/STID/STID_PEMS03.py | 112 ----- basicts/options/STID/STID_PEMS04.py | 112 ----- basicts/options/STID/STID_PEMS07.py | 112 ----- basicts/options/STID/STID_PEMS08.py | 112 ----- .../options/STNorm/STNorm_Electricity336.py | 112 ----- basicts/options/STNorm/STNorm_METR-LA.py | 112 ----- basicts/options/STNorm/STNorm_PEMS-BAY.py | 112 ----- basicts/options/STNorm/STNorm_PEMS03.py | 112 ----- basicts/options/STNorm/STNorm_PEMS04.py | 112 ----- basicts/options/STNorm/STNorm_PEMS07.py | 112 ----- basicts/options/STNorm/STNorm_PEMS08.py | 112 ----- basicts/options/Stat/Stat_Electricity336.py | 107 ----- basicts/options/Stat/Stat_METR-LA.py | 107 ----- basicts/options/Stat/Stat_PEMS-BAY.py | 107 ----- basicts/options/Stat/Stat_PEMS03.py | 107 ----- basicts/options/Stat/Stat_PEMS04.py | 107 ----- basicts/options/Stat/Stat_PEMS07.py | 107 ----- basicts/options/Stat/Stat_PEMS08.py | 107 ----- .../options/StemGNN/StemGNN_Electricity336.py | 112 ----- basicts/options/StemGNN/StemGNN_METR-LA.py | 111 ----- basicts/options/StemGNN/StemGNN_PEMS-BAY.py | 111 ----- basicts/options/StemGNN/StemGNN_PEMS03.py | 111 ----- basicts/options/StemGNN/StemGNN_PEMS04.py | 111 ----- basicts/options/StemGNN/StemGNN_PEMS07.py | 111 ----- basicts/options/StemGNN/StemGNN_PEMS08.py | 112 ----- basicts/run.py | 135 ------ basicts/runners/AGCRN_runner.py | 60 --- basicts/runners/D2STGNN_runner.py | 60 --- basicts/runners/DCRNN_runner.py | 79 ---- basicts/runners/GMAN_runner.py | 61 --- basicts/runners/GTS_runner.py | 190 -------- basicts/runners/GraphWaveNet_runner.py | 59 --- basicts/runners/HI_runner.py | 68 --- basicts/runners/LSTM_runner.py | 60 --- basicts/runners/STGCN_runner.py | 60 --- basicts/runners/STID_runner.py | 60 --- basicts/runners/STNorm_runner.py | 60 --- basicts/runners/Stat_runner.py | 60 --- basicts/runners/StemGNN_runner.py | 61 --- basicts/runners/__init__.py | 21 + basicts/runners/base_runner.py | 51 +- ...short_mts_runner.py => base_tsf_runner.py} | 169 +++---- basicts/runners/runner_zoo/agcrn_runner.py | 1 + basicts/runners/runner_zoo/d2stgnn_runner.py | 1 + basicts/runners/runner_zoo/dcrnn_runner.py | 84 ++++ basicts/runners/runner_zoo/dgcrn_runner.py | 1 + .../gts_runner.py} | 43 +- basicts/runners/runner_zoo/gwnet_runner.py | 1 + basicts/runners/runner_zoo/linear_runner.py | 3 + .../mtgnn_runner.py} | 48 +- .../runners/runner_zoo/simple_tsf_runner.py | 77 ++++ basicts/runners/runner_zoo/stemgnn_runner.py | 1 + basicts/runners/runner_zoo/stgcn_runner.py | 1 + basicts/runners/runner_zoo/stid_runner.py | 1 + basicts/runners/runner_zoo/stnorm_runner.py | 1 + basicts/utils/__init__.py | 4 + basicts/utils/adjacent_matrix_norm.py | 76 +-- basicts/utils/distance.py | 0 basicts/utils/misc.py | 21 +- basicts/utils/options.py | 0 basicts/utils/serialization.py | 73 +-- docs/DataFormat_CN.md | 51 -- docs/DataPreprocess_CN.md | 169 ------- examples/AGCRN/AGCRN_METR-LA.py | 102 ++++ examples/AGCRN/AGCRN_PEMS-BAY.py | 102 ++++ examples/AGCRN/AGCRN_PEMS03.py | 102 ++++ examples/AGCRN/AGCRN_PEMS04.py | 102 ++++ examples/AGCRN/AGCRN_PEMS07.py | 102 ++++ examples/AGCRN/AGCRN_PEMS08.py | 102 ++++ examples/D2STGNN/D2STGNN_METR-LA.py | 125 +++++ examples/D2STGNN/D2STGNN_PEMS-BAY.py | 126 +++++ examples/D2STGNN/D2STGNN_PEMS03.py | 126 +++++ examples/D2STGNN/D2STGNN_PEMS04.py | 126 +++++ examples/D2STGNN/D2STGNN_PEMS07.py | 126 +++++ examples/D2STGNN/D2STGNN_PEMS08.py | 126 +++++ examples/DCRNN/DCRNN_METR-LA.py | 126 +++++ examples/DCRNN/DCRNN_PEMS-BAY.py | 126 +++++ examples/DCRNN/DCRNN_PEMS03.py | 126 +++++ examples/DCRNN/DCRNN_PEMS04.py | 126 +++++ examples/DCRNN/DCRNN_PEMS07.py | 126 +++++ examples/DCRNN/DCRNN_PEMS08.py | 126 +++++ examples/DGCRN/DGCRN_METR-LA.py | 126 +++++ examples/DGCRN/DGCRN_PEMS-BAY.py | 126 +++++ examples/DGCRN/DGCRN_PEMS03.py | 126 +++++ examples/DGCRN/DGCRN_PEMS04.py | 126 +++++ examples/DGCRN/DGCRN_PEMS07.py | 126 +++++ examples/DGCRN/DGCRN_PEMS08.py | 126 +++++ examples/DLinear/DLinear_METR-LA.py | 104 +++++ examples/DLinear/DLinear_PEMS-BAY.py | 104 +++++ examples/DLinear/DLinear_PEMS03.py | 104 +++++ examples/DLinear/DLinear_PEMS04.py | 104 +++++ examples/DLinear/DLinear_PEMS07.py | 104 +++++ examples/DLinear/DLinear_PEMS08.py | 104 +++++ examples/GTS/GTS_METR-LA.py | 132 ++++++ examples/GTS/GTS_PEMS-BAY.py | 132 ++++++ examples/GTS/GTS_PEMS03.py | 132 ++++++ examples/GTS/GTS_PEMS04.py | 132 ++++++ examples/GTS/GTS_PEMS07.py | 132 ++++++ examples/GTS/GTS_PEMS08.py | 134 ++++++ examples/GTS/loss.py | 16 + examples/GWNet/GWNet_METR-LA.py | 122 +++++ examples/GWNet/GWNet_PEMS-BAY.py | 122 +++++ examples/GWNet/GWNet_PEMS03.py | 122 +++++ examples/GWNet/GWNet_PEMS04.py | 122 +++++ examples/GWNet/GWNet_PEMS07.py | 122 +++++ examples/GWNet/GWNet_PEMS08.py | 122 +++++ examples/Linear/Linear_METR-LA.py | 102 ++++ examples/Linear/Linear_PEMS-BAY.py | 102 ++++ examples/Linear/Linear_PEMS03.py | 102 ++++ examples/Linear/Linear_PEMS04.py | 102 ++++ examples/Linear/Linear_PEMS07.py | 102 ++++ examples/Linear/Linear_PEMS08.py | 102 ++++ examples/MLP/MLP_METR-LA.py | 110 +++++ examples/MLP/MLP_arch.py | 25 + examples/MLP/MLP_runner.py | 80 ++++ .../MTGNN/MTGNN_METR-LA.py | 114 ++--- .../MTGNN/MTGNN_PEMS-BAY.py | 115 +++-- .../MTGNN/MTGNN_PEMS03.py | 115 +++-- .../MTGNN/MTGNN_PEMS04.py | 115 +++-- .../MTGNN/MTGNN_PEMS07.py | 115 +++-- .../MTGNN/MTGNN_PEMS08.py | 115 +++-- examples/NLinear/NLinear_METR-LA.py | 102 ++++ examples/NLinear/NLinear_PEMS-BAY.py | 102 ++++ examples/NLinear/NLinear_PEMS03.py | 102 ++++ examples/NLinear/NLinear_PEMS04.py | 102 ++++ examples/NLinear/NLinear_PEMS07.py | 102 ++++ examples/NLinear/NLinear_PEMS08.py | 102 ++++ examples/STGCN/STGCN_METR-LA.py | 117 +++++ examples/STGCN/STGCN_PEMS-BAY.py | 117 +++++ examples/STGCN/STGCN_PEMS03.py | 117 +++++ examples/STGCN/STGCN_PEMS04.py | 117 +++++ examples/STGCN/STGCN_PEMS07.py | 117 +++++ examples/STGCN/STGCN_PEMS08.py | 117 +++++ examples/STID/STID_METR-LA.py | 112 +++++ examples/STID/STID_PEMS-BAY.py | 112 +++++ examples/STID/STID_PEMS03.py | 112 +++++ examples/STID/STID_PEMS04.py | 112 +++++ examples/STID/STID_PEMS07.py | 113 +++++ examples/STID/STID_PEMS08.py | 112 +++++ examples/STNorm/STNorm_METR-LA.py | 112 +++++ examples/STNorm/STNorm_PEMS-BAY.py | 112 +++++ examples/STNorm/STNorm_PEMS03.py | 112 +++++ examples/STNorm/STNorm_PEMS04.py | 112 +++++ examples/STNorm/STNorm_PEMS07.py | 112 +++++ examples/STNorm/STNorm_PEMS08.py | 112 +++++ examples/StemGNN/StemGNN_METR-LA.py | 107 +++++ examples/StemGNN/StemGNN_PEMS-BAY.py | 107 +++++ examples/StemGNN/StemGNN_PEMS03.py | 107 +++++ examples/StemGNN/StemGNN_PEMS04.py | 107 +++++ examples/StemGNN/StemGNN_PEMS07.py | 107 +++++ examples/StemGNN/StemGNN_PEMS08.py | 107 +++++ examples/run.py | 23 + results/result.png | Bin 495049 -> 0 bytes results/results.png | Bin 0 -> 486284 bytes .../Electricity336/generate_training_data.py | 158 ------- .../METR-LA/generate_training_data.py | 231 +++++----- .../PEMS-BAY/generate_training_data.py | 231 +++++----- .../PEMS03/generate_adj_mx.py | 190 +++----- .../PEMS03/generate_training_data.py | 256 +++++------ .../PEMS04/generate_adj_mx.py | 190 +++----- .../PEMS04/generate_training_data.py | 252 +++++----- .../PEMS07/generate_adj_mx.py | 190 +++----- .../PEMS07/generate_training_data.py | 252 +++++----- .../PEMS08/generate_adj_mx.py | 192 +++----- .../PEMS08/generate_training_data.py | 254 +++++----- scripts/data_preparation/all.sh | 7 + scripts/node2vec/generate_node_embeddings.py | 44 -- scripts/node2vec/node2vec.py | 157 ------- test/test_data.py | 8 - test/test_dataset.py | 15 - test/test_model.py | 20 - tests/test_all.py | 19 + tests/test_specific.py | 38 ++ tests/utils.py | 26 ++ 341 files changed, 13397 insertions(+), 16163 deletions(-) create mode 100644 .github/workflows/pylint.yml create mode 100644 .pylintrc create mode 100644 TODO.md create mode 100644 basicts/__init__.py delete mode 100644 basicts/archs/AGCRN_arch/AGCN.py delete mode 100644 basicts/archs/AGCRN_arch/AGCRN_arch.py delete mode 100644 basicts/archs/AGCRN_arch/__init__.py delete mode 100644 basicts/archs/D2STGNN_arch/DiffusionBlock/__init__.py delete mode 100644 basicts/archs/D2STGNN_arch/DiffusionBlock/dif_block.py delete mode 100644 basicts/archs/D2STGNN_arch/DiffusionBlock/dif_model.py delete mode 100644 basicts/archs/D2STGNN_arch/DynamicGraphConv/Utils/__init__.py delete mode 100644 basicts/archs/D2STGNN_arch/InherentBlock/__init__.py delete mode 100644 basicts/archs/D2STGNN_arch/InherentBlock/inh_block.py delete mode 100644 basicts/archs/D2STGNN_arch/InherentBlock/inh_model.py delete mode 100644 basicts/archs/D2STGNN_arch/__init__.py delete mode 100644 basicts/archs/DCRNN_arch/__init__.py delete mode 100644 basicts/archs/DGCRN_arch/__init__.py delete mode 100644 basicts/archs/GMAN_arch/GMAN_arch.py delete mode 100644 basicts/archs/GMAN_arch/__init__.py delete mode 100644 basicts/archs/GTS_arch/__init__.py delete mode 100644 basicts/archs/GraphWaveNet_arch/__init__.py delete mode 100644 basicts/archs/HI_arch/HI_arch.py delete mode 100644 basicts/archs/HI_arch/__init__.py delete mode 100644 basicts/archs/LSTM_arch/LSTM_arch.py delete mode 100644 basicts/archs/LSTM_arch/__init__.py delete mode 100644 basicts/archs/MTGNN_arch/MTGNN_arch.py delete mode 100644 basicts/archs/MTGNN_arch/__init__.py delete mode 100644 basicts/archs/STGCN_arch/__init__.py delete mode 100644 basicts/archs/STID_arch/MLP.py delete mode 100644 basicts/archs/STID_arch/STID_arch.py delete mode 100644 basicts/archs/STID_arch/__init__.py delete mode 100644 basicts/archs/STNorm_arch/__init__.py delete mode 100644 basicts/archs/Stat_arch/Stat_arch.py delete mode 100644 basicts/archs/Stat_arch/__init__.py delete mode 100644 basicts/archs/StemGNN_arch/__init__.py create mode 100644 basicts/archs/arch_zoo/agcrn_arch/__init__.py create mode 100644 basicts/archs/arch_zoo/agcrn_arch/agcn.py create mode 100644 basicts/archs/arch_zoo/agcrn_arch/agcrn_arch.py rename basicts/archs/{AGCRN_arch/AGCRNCell.py => arch_zoo/agcrn_arch/agcrn_cell.py} (68%) create mode 100644 basicts/archs/arch_zoo/d2stgnn_arch/__init__.py rename basicts/archs/{D2STGNN_arch/D2STGNN_arch.py => arch_zoo/d2stgnn_arch/d2stgnn_arch.py} (53%) rename basicts/archs/{D2STGNN_arch/Decouple => arch_zoo/d2stgnn_arch/decouple}/estimation_gate.py (100%) rename basicts/archs/{D2STGNN_arch/Decouple => arch_zoo/d2stgnn_arch/decouple}/residual_decomp.py (100%) create mode 100644 basicts/archs/arch_zoo/d2stgnn_arch/difusion_block/__init__.py create mode 100644 basicts/archs/arch_zoo/d2stgnn_arch/difusion_block/dif_block.py create mode 100644 basicts/archs/arch_zoo/d2stgnn_arch/difusion_block/dif_model.py rename basicts/archs/{D2STGNN_arch/DiffusionBlock => arch_zoo/d2stgnn_arch/difusion_block}/forecast.py (74%) rename basicts/archs/{D2STGNN_arch/DynamicGraphConv/DyGraphCons.py => arch_zoo/d2stgnn_arch/dynamic_graph_conv/dy_graph_conv.py} (56%) create mode 100644 basicts/archs/arch_zoo/d2stgnn_arch/dynamic_graph_conv/utils/__init__.py rename basicts/archs/{D2STGNN_arch/DynamicGraphConv/Utils => arch_zoo/d2stgnn_arch/dynamic_graph_conv/utils}/distance.py (100%) rename basicts/archs/{D2STGNN_arch/DynamicGraphConv/Utils => arch_zoo/d2stgnn_arch/dynamic_graph_conv/utils}/mask.py (100%) rename basicts/archs/{D2STGNN_arch/DynamicGraphConv/Utils => arch_zoo/d2stgnn_arch/dynamic_graph_conv/utils}/normalizer.py (100%) create mode 100644 basicts/archs/arch_zoo/d2stgnn_arch/inherent_block/__init__.py rename basicts/archs/{D2STGNN_arch/InherentBlock => arch_zoo/d2stgnn_arch/inherent_block}/forecast.py (65%) create mode 100644 basicts/archs/arch_zoo/d2stgnn_arch/inherent_block/inh_block.py create mode 100644 basicts/archs/arch_zoo/d2stgnn_arch/inherent_block/inh_model.py create mode 100644 basicts/archs/arch_zoo/dcrnn_arch/__init__.py rename basicts/archs/{DCRNN_arch/DCRNN_arch.py => arch_zoo/dcrnn_arch/dcrnn_arch.py} (56%) rename basicts/archs/{DCRNN_arch/DCRNN_cell.py => arch_zoo/dcrnn_arch/dcrnn_cell.py} (72%) create mode 100644 basicts/archs/arch_zoo/dgcrn_arch/__init__.py rename basicts/archs/{DGCRN_arch/DGCRN_arch.py => arch_zoo/dgcrn_arch/dgcrn_arch.py} (60%) rename basicts/archs/{DGCRN_arch/DGCRN_layer.py => arch_zoo/dgcrn_arch/dgcrn_layer.py} (96%) create mode 100644 basicts/archs/arch_zoo/gts_arch/__init__.py rename basicts/archs/{GTS_arch/GTS_arch.py => arch_zoo/gts_arch/gts_arch.py} (92%) rename basicts/archs/{GTS_arch/GTS_cell.py => arch_zoo/gts_arch/gts_cell.py} (67%) create mode 100644 basicts/archs/arch_zoo/gwnet_arch/__init__.py rename basicts/archs/{GraphWaveNet_arch/GraphWaveNet_arch.py => arch_zoo/gwnet_arch/gwnet_arch.py} (71%) create mode 100644 basicts/archs/arch_zoo/linear_arch/__init__.py create mode 100644 basicts/archs/arch_zoo/linear_arch/dlinear.py create mode 100644 basicts/archs/arch_zoo/linear_arch/linear.py create mode 100644 basicts/archs/arch_zoo/linear_arch/nlinear.py create mode 100644 basicts/archs/arch_zoo/mtgnn_arch/__init__.py create mode 100644 basicts/archs/arch_zoo/mtgnn_arch/mtgnn_arch.py rename basicts/archs/{MTGNN_arch/MTGNN_layers.py => arch_zoo/mtgnn_arch/mtgnn_layers.py} (72%) create mode 100644 basicts/archs/arch_zoo/stemgnn_arch/__init__.py rename basicts/archs/{StemGNN_arch/StemGNN_arch.py => arch_zoo/stemgnn_arch/stemgnn_arch.py} (71%) create mode 100644 basicts/archs/arch_zoo/stgcn_arch/__init__.py rename basicts/archs/{STGCN_arch/STGCN_arch.py => arch_zoo/stgcn_arch/stgcn_arch.py} (74%) rename basicts/archs/{STGCN_arch/STGCN_layers.py => arch_zoo/stgcn_arch/stgcn_layers.py} (80%) create mode 100644 basicts/archs/arch_zoo/stid_arch/__init__.py create mode 100644 basicts/archs/arch_zoo/stid_arch/mlp.py create mode 100644 basicts/archs/arch_zoo/stid_arch/stid_arch.py create mode 100644 basicts/archs/arch_zoo/stnorm_arch/__init__.py rename basicts/archs/{STNorm_arch/STNorm_arch.py => arch_zoo/stnorm_arch/stnorm_arch.py} (72%) create mode 100644 basicts/data/__init__.py delete mode 100644 basicts/data/base_dataset.py create mode 100644 basicts/data/dataset.py rename basicts/{archs => data}/registry.py (55%) create mode 100644 basicts/data/transform.py delete mode 100644 basicts/data/transforms.py create mode 100644 basicts/launcher.py create mode 100644 basicts/losses/__init__.py create mode 100644 basicts/metrics/__init__.py delete mode 100644 basicts/options/AGCRN/AGCRN_Electricity336.py delete mode 100644 basicts/options/AGCRN/AGCRN_METR-LA.py delete mode 100644 basicts/options/AGCRN/AGCRN_PEMS-BAY.py delete mode 100644 basicts/options/AGCRN/AGCRN_PEMS03.py delete mode 100644 basicts/options/AGCRN/AGCRN_PEMS04.py delete mode 100644 basicts/options/AGCRN/AGCRN_PEMS07.py delete mode 100644 basicts/options/AGCRN/AGCRN_PEMS08.py delete mode 100644 basicts/options/D2STGNN/D2STGNN_METR-LA.py delete mode 100644 basicts/options/D2STGNN/D2STGNN_PEMS-BAY.py delete mode 100644 basicts/options/D2STGNN/D2STGNN_PEMS03.py delete mode 100644 basicts/options/D2STGNN/D2STGNN_PEMS04.py delete mode 100644 basicts/options/D2STGNN/D2STGNN_PEMS07.py delete mode 100644 basicts/options/D2STGNN/D2STGNN_PEMS08.py delete mode 100644 basicts/options/DCRNN/DCRNN_METR-LA.py delete mode 100644 basicts/options/DCRNN/DCRNN_PEMS-BAY.py delete mode 100644 basicts/options/DCRNN/DCRNN_PEMS03.py delete mode 100644 basicts/options/DCRNN/DCRNN_PEMS04.py delete mode 100644 basicts/options/DCRNN/DCRNN_PEMS07.py delete mode 100644 basicts/options/DCRNN/DCRNN_PEMS08.py delete mode 100644 basicts/options/DGCRN/DGCRN_METR-LA.py delete mode 100644 basicts/options/DGCRN/DGCRN_PEMS-BAY.py delete mode 100644 basicts/options/DGCRN/DGCRN_PEMS03.py delete mode 100644 basicts/options/DGCRN/DGCRN_PEMS04.py delete mode 100644 basicts/options/DGCRN/DGCRN_PEMS07.py delete mode 100644 basicts/options/DGCRN/DGCRN_PEMS08.py delete mode 100644 basicts/options/GMAN/GMAN_METR-LA.py delete mode 100644 basicts/options/GMAN/GMAN_PEMS-BAY.py delete mode 100644 basicts/options/GMAN/GMAN_PEMS03.py delete mode 100644 basicts/options/GMAN/GMAN_PEMS04.py delete mode 100644 basicts/options/GMAN/GMAN_PEMS07.py delete mode 100644 basicts/options/GMAN/GMAN_PEMS08.py delete mode 100644 basicts/options/GTS/GTS_METR-LA.py delete mode 100644 basicts/options/GTS/GTS_PEMS-BAY.py delete mode 100644 basicts/options/GTS/GTS_PEMS03.py delete mode 100644 basicts/options/GTS/GTS_PEMS04.py delete mode 100644 basicts/options/GTS/GTS_PEMS07.py delete mode 100644 basicts/options/GTS/GTS_PEMS08.py delete mode 100644 basicts/options/GraphWaveNet/GraphWaveNet_METR-LA.py delete mode 100644 basicts/options/GraphWaveNet/GraphWaveNet_PEMS-BAY.py delete mode 100644 basicts/options/GraphWaveNet/GraphWaveNet_PEMS03.py delete mode 100644 basicts/options/GraphWaveNet/GraphWaveNet_PEMS04.py delete mode 100644 basicts/options/GraphWaveNet/GraphWaveNet_PEMS07.py delete mode 100644 basicts/options/GraphWaveNet/GraphWaveNet_PEMS08.py delete mode 100644 basicts/options/HI/HI_Electricity336.py delete mode 100644 basicts/options/HI/HI_METR-LA.py delete mode 100644 basicts/options/HI/HI_PEMS-BAY.py delete mode 100644 basicts/options/HI/HI_PEMS03.py delete mode 100644 basicts/options/HI/HI_PEMS04.py delete mode 100644 basicts/options/HI/HI_PEMS07.py delete mode 100644 basicts/options/HI/HI_PEMS08.py delete mode 100644 basicts/options/LSTM/LSTM_Electricity336.py delete mode 100644 basicts/options/LSTM/LSTM_METR-LA.py delete mode 100644 basicts/options/LSTM/LSTM_PEMS-BAY.py delete mode 100644 basicts/options/LSTM/LSTM_PEMS03.py delete mode 100644 basicts/options/LSTM/LSTM_PEMS04.py delete mode 100644 basicts/options/LSTM/LSTM_PEMS07.py delete mode 100644 basicts/options/LSTM/LSTM_PEMS08.py delete mode 100644 basicts/options/MTGNN/MTGNN_Electricity336.py delete mode 100644 basicts/options/STGCN/STGCN_METR-LA.py delete mode 100644 basicts/options/STGCN/STGCN_PEMS-BAY.py delete mode 100644 basicts/options/STGCN/STGCN_PEMS03.py delete mode 100644 basicts/options/STGCN/STGCN_PEMS04.py delete mode 100644 basicts/options/STGCN/STGCN_PEMS07.py delete mode 100644 basicts/options/STGCN/STGCN_PEMS08.py delete mode 100644 basicts/options/STID/STID_Electricity336.py delete mode 100644 basicts/options/STID/STID_METR-LA.py delete mode 100644 basicts/options/STID/STID_PEMS-BAY.py delete mode 100644 basicts/options/STID/STID_PEMS03.py delete mode 100644 basicts/options/STID/STID_PEMS04.py delete mode 100644 basicts/options/STID/STID_PEMS07.py delete mode 100644 basicts/options/STID/STID_PEMS08.py delete mode 100644 basicts/options/STNorm/STNorm_Electricity336.py delete mode 100644 basicts/options/STNorm/STNorm_METR-LA.py delete mode 100644 basicts/options/STNorm/STNorm_PEMS-BAY.py delete mode 100644 basicts/options/STNorm/STNorm_PEMS03.py delete mode 100644 basicts/options/STNorm/STNorm_PEMS04.py delete mode 100644 basicts/options/STNorm/STNorm_PEMS07.py delete mode 100644 basicts/options/STNorm/STNorm_PEMS08.py delete mode 100644 basicts/options/Stat/Stat_Electricity336.py delete mode 100644 basicts/options/Stat/Stat_METR-LA.py delete mode 100644 basicts/options/Stat/Stat_PEMS-BAY.py delete mode 100644 basicts/options/Stat/Stat_PEMS03.py delete mode 100644 basicts/options/Stat/Stat_PEMS04.py delete mode 100644 basicts/options/Stat/Stat_PEMS07.py delete mode 100644 basicts/options/Stat/Stat_PEMS08.py delete mode 100644 basicts/options/StemGNN/StemGNN_Electricity336.py delete mode 100644 basicts/options/StemGNN/StemGNN_METR-LA.py delete mode 100644 basicts/options/StemGNN/StemGNN_PEMS-BAY.py delete mode 100644 basicts/options/StemGNN/StemGNN_PEMS03.py delete mode 100644 basicts/options/StemGNN/StemGNN_PEMS04.py delete mode 100644 basicts/options/StemGNN/StemGNN_PEMS07.py delete mode 100644 basicts/options/StemGNN/StemGNN_PEMS08.py delete mode 100644 basicts/run.py delete mode 100644 basicts/runners/AGCRN_runner.py delete mode 100644 basicts/runners/D2STGNN_runner.py delete mode 100644 basicts/runners/DCRNN_runner.py delete mode 100644 basicts/runners/GMAN_runner.py delete mode 100644 basicts/runners/GTS_runner.py delete mode 100644 basicts/runners/GraphWaveNet_runner.py delete mode 100644 basicts/runners/HI_runner.py delete mode 100644 basicts/runners/LSTM_runner.py delete mode 100644 basicts/runners/STGCN_runner.py delete mode 100644 basicts/runners/STID_runner.py delete mode 100644 basicts/runners/STNorm_runner.py delete mode 100644 basicts/runners/Stat_runner.py delete mode 100644 basicts/runners/StemGNN_runner.py create mode 100644 basicts/runners/__init__.py rename basicts/runners/{short_mts_runner.py => base_tsf_runner.py} (51%) create mode 100644 basicts/runners/runner_zoo/agcrn_runner.py create mode 100644 basicts/runners/runner_zoo/d2stgnn_runner.py create mode 100644 basicts/runners/runner_zoo/dcrnn_runner.py create mode 100644 basicts/runners/runner_zoo/dgcrn_runner.py rename basicts/runners/{DGCRN_runner.py => runner_zoo/gts_runner.py} (60%) create mode 100644 basicts/runners/runner_zoo/gwnet_runner.py create mode 100644 basicts/runners/runner_zoo/linear_runner.py rename basicts/runners/{MTGNN_runner.py => runner_zoo/mtgnn_runner.py} (70%) create mode 100644 basicts/runners/runner_zoo/simple_tsf_runner.py create mode 100644 basicts/runners/runner_zoo/stemgnn_runner.py create mode 100644 basicts/runners/runner_zoo/stgcn_runner.py create mode 100644 basicts/runners/runner_zoo/stid_runner.py create mode 100644 basicts/runners/runner_zoo/stnorm_runner.py create mode 100644 basicts/utils/__init__.py delete mode 100644 basicts/utils/distance.py delete mode 100644 basicts/utils/options.py delete mode 100644 docs/DataFormat_CN.md delete mode 100644 docs/DataPreprocess_CN.md create mode 100644 examples/AGCRN/AGCRN_METR-LA.py create mode 100644 examples/AGCRN/AGCRN_PEMS-BAY.py create mode 100644 examples/AGCRN/AGCRN_PEMS03.py create mode 100644 examples/AGCRN/AGCRN_PEMS04.py create mode 100644 examples/AGCRN/AGCRN_PEMS07.py create mode 100644 examples/AGCRN/AGCRN_PEMS08.py create mode 100644 examples/D2STGNN/D2STGNN_METR-LA.py create mode 100644 examples/D2STGNN/D2STGNN_PEMS-BAY.py create mode 100644 examples/D2STGNN/D2STGNN_PEMS03.py create mode 100644 examples/D2STGNN/D2STGNN_PEMS04.py create mode 100644 examples/D2STGNN/D2STGNN_PEMS07.py create mode 100644 examples/D2STGNN/D2STGNN_PEMS08.py create mode 100644 examples/DCRNN/DCRNN_METR-LA.py create mode 100644 examples/DCRNN/DCRNN_PEMS-BAY.py create mode 100644 examples/DCRNN/DCRNN_PEMS03.py create mode 100644 examples/DCRNN/DCRNN_PEMS04.py create mode 100644 examples/DCRNN/DCRNN_PEMS07.py create mode 100644 examples/DCRNN/DCRNN_PEMS08.py create mode 100644 examples/DGCRN/DGCRN_METR-LA.py create mode 100644 examples/DGCRN/DGCRN_PEMS-BAY.py create mode 100644 examples/DGCRN/DGCRN_PEMS03.py create mode 100644 examples/DGCRN/DGCRN_PEMS04.py create mode 100644 examples/DGCRN/DGCRN_PEMS07.py create mode 100644 examples/DGCRN/DGCRN_PEMS08.py create mode 100644 examples/DLinear/DLinear_METR-LA.py create mode 100644 examples/DLinear/DLinear_PEMS-BAY.py create mode 100644 examples/DLinear/DLinear_PEMS03.py create mode 100644 examples/DLinear/DLinear_PEMS04.py create mode 100644 examples/DLinear/DLinear_PEMS07.py create mode 100644 examples/DLinear/DLinear_PEMS08.py create mode 100644 examples/GTS/GTS_METR-LA.py create mode 100644 examples/GTS/GTS_PEMS-BAY.py create mode 100644 examples/GTS/GTS_PEMS03.py create mode 100644 examples/GTS/GTS_PEMS04.py create mode 100644 examples/GTS/GTS_PEMS07.py create mode 100644 examples/GTS/GTS_PEMS08.py create mode 100644 examples/GTS/loss.py create mode 100644 examples/GWNet/GWNet_METR-LA.py create mode 100644 examples/GWNet/GWNet_PEMS-BAY.py create mode 100644 examples/GWNet/GWNet_PEMS03.py create mode 100644 examples/GWNet/GWNet_PEMS04.py create mode 100644 examples/GWNet/GWNet_PEMS07.py create mode 100644 examples/GWNet/GWNet_PEMS08.py create mode 100644 examples/Linear/Linear_METR-LA.py create mode 100644 examples/Linear/Linear_PEMS-BAY.py create mode 100644 examples/Linear/Linear_PEMS03.py create mode 100644 examples/Linear/Linear_PEMS04.py create mode 100644 examples/Linear/Linear_PEMS07.py create mode 100644 examples/Linear/Linear_PEMS08.py create mode 100644 examples/MLP/MLP_METR-LA.py create mode 100644 examples/MLP/MLP_arch.py create mode 100644 examples/MLP/MLP_runner.py rename {basicts/options => examples}/MTGNN/MTGNN_METR-LA.py (52%) rename {basicts/options => examples}/MTGNN/MTGNN_PEMS-BAY.py (52%) rename {basicts/options => examples}/MTGNN/MTGNN_PEMS03.py (52%) rename {basicts/options => examples}/MTGNN/MTGNN_PEMS04.py (52%) rename {basicts/options => examples}/MTGNN/MTGNN_PEMS07.py (52%) rename {basicts/options => examples}/MTGNN/MTGNN_PEMS08.py (52%) create mode 100644 examples/NLinear/NLinear_METR-LA.py create mode 100644 examples/NLinear/NLinear_PEMS-BAY.py create mode 100644 examples/NLinear/NLinear_PEMS03.py create mode 100644 examples/NLinear/NLinear_PEMS04.py create mode 100644 examples/NLinear/NLinear_PEMS07.py create mode 100644 examples/NLinear/NLinear_PEMS08.py create mode 100644 examples/STGCN/STGCN_METR-LA.py create mode 100644 examples/STGCN/STGCN_PEMS-BAY.py create mode 100644 examples/STGCN/STGCN_PEMS03.py create mode 100644 examples/STGCN/STGCN_PEMS04.py create mode 100644 examples/STGCN/STGCN_PEMS07.py create mode 100644 examples/STGCN/STGCN_PEMS08.py create mode 100644 examples/STID/STID_METR-LA.py create mode 100644 examples/STID/STID_PEMS-BAY.py create mode 100644 examples/STID/STID_PEMS03.py create mode 100644 examples/STID/STID_PEMS04.py create mode 100644 examples/STID/STID_PEMS07.py create mode 100644 examples/STID/STID_PEMS08.py create mode 100644 examples/STNorm/STNorm_METR-LA.py create mode 100644 examples/STNorm/STNorm_PEMS-BAY.py create mode 100644 examples/STNorm/STNorm_PEMS03.py create mode 100644 examples/STNorm/STNorm_PEMS04.py create mode 100644 examples/STNorm/STNorm_PEMS07.py create mode 100644 examples/STNorm/STNorm_PEMS08.py create mode 100644 examples/StemGNN/StemGNN_METR-LA.py create mode 100644 examples/StemGNN/StemGNN_PEMS-BAY.py create mode 100644 examples/StemGNN/StemGNN_PEMS03.py create mode 100644 examples/StemGNN/StemGNN_PEMS04.py create mode 100644 examples/StemGNN/StemGNN_PEMS07.py create mode 100644 examples/StemGNN/StemGNN_PEMS08.py create mode 100644 examples/run.py delete mode 100644 results/result.png create mode 100644 results/results.png delete mode 100644 scripts/data_preparation/Electricity336/generate_training_data.py create mode 100755 scripts/data_preparation/all.sh delete mode 100644 scripts/node2vec/generate_node_embeddings.py delete mode 100644 scripts/node2vec/node2vec.py delete mode 100644 test/test_data.py delete mode 100644 test/test_dataset.py delete mode 100644 test/test_model.py create mode 100644 tests/test_all.py create mode 100644 tests/test_specific.py create mode 100644 tests/utils.py diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 00000000..2e6a0ef9 --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,27 @@ +name: Pylint + +env: + FAIL_UNDER: "9.0" + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + pip install coverage + - name: Analysing the code with pylint + run: | + pylint --fail-under=${FAIL_UNDER} basicts scripts diff --git a/.gitignore b/.gitignore index 7773dafc..aeef3178 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ - - # dir __pycache__/ .vscode/ diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..3bc96073 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,435 @@ +# This Pylint rcfile contains a best-effort configuration to uphold the +# best-practices and style described in the Google Python style guide: +# https://google.github.io/styleguide/pyguide.html +# +# Its canonical open-source location is: +# https://google.github.io/styleguide/pylintrc + +[MASTER] + +# Files or directories to be skipped. They should be base names, not paths. +ignore=third_party + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns= + +# Pickle collected data for later comparisons. +persistent=no + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=4 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=abstract-method, + apply-builtin, + arguments-differ, + attribute-defined-outside-init, + backtick, + bad-option-value, + basestring-builtin, + buffer-builtin, + c-extension-no-member, + consider-using-enumerate, + consider-using-f-string, + cmp-builtin, + cmp-method, + coerce-builtin, + coerce-method, + delslice-method, + div-method, + duplicate-code, + eq-without-hash, + execfile-builtin, + file-builtin, + filter-builtin-not-iterating, + fixme, + getslice-method, + global-statement, + hex-method, + idiv-method, + implicit-str-concat-in-sequence, + import-error, + import-self, + import-star-module-level, + inconsistent-return-statements, + input-builtin, + intern-builtin, + invalid-str-codec, + locally-disabled, + logging-format-interpolation, + logging-fstring-interpolation, + long-builtin, + long-suffix, + map-builtin-not-iterating, + misplaced-comparison-constant, + missing-function-docstring, + missing-module-docstring, + metaclass-assignment, + next-method-called, + next-method-defined, + no-absolute-import, + no-else-break, + no-else-continue, + no-else-raise, + no-else-return, + no-init, # added + no-member, + no-name-in-module, + no-self-use, + nonzero-method, + oct-method, + old-division, + old-ne-operator, + old-octal-literal, + old-raise-syntax, + parameter-unpacking, + print-statement, + protected-access, + raising-string, + range-builtin-not-iterating, + raw_input-builtin, + rdiv-method, + reduce-builtin, + relative-import, + reload-builtin, + round-builtin, + setslice-method, + signature-differs, + standarderror-builtin, + suppressed-message, + sys-max-int, + too-few-public-methods, + too-many-ancestors, + too-many-arguments, + too-many-boolean-expressions, + too-many-branches, + too-many-instance-attributes, + too-many-locals, + too-many-nested-blocks, + too-many-public-methods, + too-many-return-statements, + too-many-statements, + trailing-newlines, + unichr-builtin, + unicode-builtin, + unnecessary-pass, + unpacking-in-except, + unspecified-encoding, + useless-else-on-loop, + useless-object-inheritance, + useless-suppression, + using-cmp-argument, + wrong-import-order, + xrange-builtin, + zip-builtin-not-iterating, + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=main,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl + +# Regular expression matching correct function names +function-rgx=^(?:(?PsetUp|tearDown|setUpModule|tearDownModule)|(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct constant names +const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression matching correct attribute names +attr-rgx=^_{0,2}[a-z][a-z0-9_]*$ + +# Regular expression matching correct argument names +argument-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct class names +class-rgx=^_?[A-Z][a-zA-Z0-9]*$ + +# Regular expression matching correct module names +module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$ + +# Regular expression matching correct method names +method-rgx=(?x)^(?:(?P_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P_{0,2}[a-z][a-z0-9_]*))$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=10 + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=150 + +# TODO(https://github.com/PyCQA/pylint/issues/3352): Direct pylint to exempt +# lines made too long by directives to pytype. + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=(?x)( + ^\s*(\#\ )??$| + ^\s*(from\s+\S+\s+)?import\s+.+$) + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=yes + +# Maximum number of lines in a module +max-module-lines=99999 + +# String used as indentation unit. The internal Google style guide mandates 2 +# spaces. Google's externaly-published style guide says 4, consistent with +# PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google +# projects (like TensorFlow). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=TODO + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=yes + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging,absl.logging,tensorflow.io.logging + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub, + TERMIOS, + Bastion, + rexec, + sets + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant, absl + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls, + class_ + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=StandardError, + Exception, + BaseException \ No newline at end of file diff --git a/README.md b/README.md index 98f714c0..49abf2ec 100644 --- a/README.md +++ b/README.md @@ -1,128 +1,155 @@ -# BasicTS +#
BasicTS: A Time Series Benchmark and Toolkit
+ +
[![EasyTorch](https://img.shields.io/badge/Developing%20with-EasyTorch-2077ff.svg)](https://github.com/cnstark/easytorch) [![LICENSE](https://img.shields.io/github/license/zezhishao/BasicTS.svg)](https://github.com/zezhishao/BasicTS/blob/master/LICENSE) +[![PyTorch](https://img.shields.io/badge/PyTorch-1.10.0-orange)](https://pytorch.org/) +[![python lint](https://github.com/zezhishao/BasicTS/actions/workflows/pylint.yml/badge.svg)](https://github.com/zezhishao/BasicTS/blob/master/.github/workflows/pylint.yml) + +
+ +BasicTS (**Basic** **T**ime **S**eries) is a PyTorch-based benchmark and toolbox for **time series forecasting** (TSF). + +On the one hand, BasicTS utilizes a ***unified and standard pipeline*** to give a fair and exhaustive reproduction and comparison of popular deep learning-based TSF models based on rich datasets. BasicTS now has a wealth of methods built-in and provides the results of their comparison. + +On the other hand, BasicTS provides users with ***easy-to-use and extensible interfaces*** to facilitate the quick design and evaluation of new models. At a minimum, users only need to define the model architecture, and all other details can be configured in a configuration file. + +## ✨ Highlighted Features + +BasicTS is developed based on [EasyTorch](https://github.com/cnstark/easytorch)[1], an easy-to-use and powerful open-source neural network training framework. +Thanks to EasyTorch, BasicTS has the following highlighted features: -## 0. What is BasicTS +### 😼 Fair Performance Review -BasicTS (**Basic** **T**ime **S**eries) is an open-source PyTorch-based benchmark and toolbox **for time series** . -At present, it only focuses on time series forecasting, and may add time series classification, anomaly detection, etc., in the future. +- 🛡**Rich Datasets**. BasicTS supports rich datasets to perform an exhaustive evaluation of a given model based on a unified pipeline. More datasets will be added in the future. -BasicTS provides users with a ***unified, standard pipeline***, which provides ***reproduction and fair comparision*** of popular deep learning-based time series models to inspire new innovations. +- ⚔️**Rich Baselines**. BasicTS has a wealth of built-in methods, such as Spatial-Temporal Graph Neural Network-based (STGNN) methods and Transformer-based methods (under construction👷). -BasicTS is developed based on [EasyTorch](https://github.com/cnstark/easytorch) [2], an easy-to-use and powerful open source neural network training framework. +### 👨‍💻 Developing with BasicTS -If this repository helps your research or work, I hope you could give me a ⭐, and I will keep updating it. If you need more features about BasicTS (e.g. more datasets or methods) or have any questions/suggestions, please feel free to let me know~ +- 🔧**Everything Based on Config**. Users can control all the details of the pipeline through a config file, such as the hyperparameter of dataloaders, optimization, and other tricks (*e.g.*, curriculum learning). -## 1. Supported Models and Datasets +- 💻**Minimum Code**. Users only need to implement key codes such as model architecture and data pre/post-processing to build their own deep learning projects. -### 1.1 Short-term Time Series Forecasting +- 📃**Save Training Log**. Support `logging` log system and `Tensorboard`, and encapsulate it as a unified interface, users can save customized training logs by calling simple interfaces. -| Model\Dataset | METR-LA | PEMS-BAY | PEMS04 | PEMS08 | PEMS03 | PEMS07 | Other Datasets | -|:-------------:|:-------:|:--------:|:------:|:------:|:------:|:------:|:--------------:| -| AR | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| VAR | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| HI | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| Graph WaveNet | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| DCRNN | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| STGCN | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| StemGNN | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| MTGNN | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| GTS | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| DGCRN | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| GMAN | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| AGCRN | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| STNorm | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| STID | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| D2STGNN | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| Other Models | | | | | | | | +- 🔦**Support All Devices**. BasicTS supports CPU, GPU and GPU distributed training (both single node multiple GPUs and multiple nodes) thanks to using EasyTorch as the backend. Users can use it by setting parameters without modifying any code. -Although we have tried our best to tune the hyperparameters in `basicts/options` for every model and every dataset, there is no guarantee that they are optimal. -Thus, any PRs for better hyper-parameters are welcomed to make BasicTS fairer. +## 💿 Dependencies -### 1.2 Long-term Time Series Forecasting +### OS -🕐 +We recommend using BasicTS on Linux systems (*e.g.* Ubuntu and CentOS). +Other systems (*e.g.*, Windows and macOS) have not been tested. -## 2. Installing Dependencies +### Python -We recommend that you install all dependencies by: +Python >= 3.6 (recommended >= 3.9). -```shell +[Miniconda](https://docs.conda.io/en/latest/miniconda.html) or [Anaconda](https://www.anaconda.com/) are recommended to create a virtual python environment. + +### Installing + +```bash pip install -r requirements.txt ``` -## 3. Codebase Designs and Conventions +### Warning -🕐 +BasicTS is built on PyTorch 1.9.1 or 1.10.0, while other versions have not been tested. -## 4. Usage -`git clone https://github.com/zezhishao/BasicTS.git` +## 🎯 Getting Started of Developing with BasicTS -### 4.1 Data Preparation and Preprocessing +### Preparing Data -#### 4.1.1 Data Preparation +- **Clone BasicTS** -You can download the raw datasets at [Google Drive](https://drive.google.com/drive/folders/14EJVODCU48fGK0FkyeVom_9lETh80Yjp) or [Baidu Yun](https://pan.baidu.com/s/18qonT9l1_NbvyAgpD4381g)(password: 0lrk), and unzip them to `datasets/raw_data/`. + ```bash + cd /path/to/your/project + git clone https://github.com/zezhishao/BasicTS.git + ``` -#### 4.1.2 Data Preprocessing +- **Download Raw Data** -```bash -cd /path/to/project -python scripts/data_preparation/$DATASET_NAME/generate_training_data.py -``` + You can download all the raw datasets at [Google Drive](https://drive.google.com/drive/folders/14EJVODCU48fGK0FkyeVom_9lETh80Yjp) or [Baidu Yun](https://pan.baidu.com/s/18qonT9l1_NbvyAgpD4381g)(password: 0lrk), and unzip them to `datasets/raw_data/`. -Replace `$DATASET_NAME` with one of `METR-LA`, `PEMS-BAY`, `PEMS03`, `PEMS04`, `PEMS07`, `PEMS08`, or any other supported dataset. +- **Pre-process Data** -The processed data will be placed in `datasets/$DATASET_NAME`. + ```bash + cd /path/to/your/project + python scripts/data_preparation/${DATASET_NAME}/generate_training_data.py + ``` -Details of preprocessing can be found in `docs/DataPreparation_CN.md`~(Chinese). + Replace `${DATASET_NAME}` with one of `METR-LA`, `PEMS-BAY`, `PEMS03`, `PEMS04`, `PEMS07`, `PEMS08`, or any other supported dataset. The processed data will be placed in `datasets/${DATASET_NAME}`. -### 4.2 Run a Time Series Forecasting Model + Or you can pre-process all datasets by. -We recommend running a time series model with the following command: + ```bash + cd /path/to/your/project + bash scripts/data_preparation/all.sh + ``` -```bash -cd /path/to/project -easytrain -c basicts/options/$METHOD_NAME/$METHOD_NAME_$DATASET_NAME.py --gpus '0' -``` +### 3 Steps to Evaluate Your Model -Replace the `$METHOD_NAME` and `$DATASET_NAME` with any supported method and dataset. For example, +- **Define Your Model Architecture** + + The `forward` function needs to follow the conventions of BasicTS. You can find an example of the Multi-Layer Perceptron (`MLP`) model in [examples/MLP/mlp_arch.py](examples/MLP/mlp_arch.py) + +- **Define Your Runner for Your Model** (Optional) + + BasicTS provides a unified and standard pipeline in `basicts.runner.BaseTimeSeriesForecastingRunner`. + Nevertheless, you still need to define the specific forward process (the `forward` function in the **runner**). + Fortunately, BasicTS also provides such an implementation in `basicts.runner.SimpleTimeSeriesForecastingRunner`, which can cover most of the situations. + [The runner](examples/MLP/mlp_runner.py) for the `MLP` model can also use this built-in runner. + You can also find more runners in `basicts.runners.runner_zoo` to learn more about the runner design. + +- **Configure your Configuration File** + + You can configure all the details of the pipeline and hyperparameters in a configuration file, *i.e.*, **everything is based on config**. + The configuration file is a `.py` file, in which you can import your model and runner and set all the options. BasicTS uses `EasyDict` to serve as a parameter container, which is extensible and flexible to use. + An example of the configuration file for the `MLP` model on the `METR-LA` dataset can be found in [examples/MLP/MLP_METR-LA.py](examples/MLP/MLP_METR-LA.py) + +### Run It! + +An example of a start script can be found in [examples/run.py](examples/run.py). +You can run your model by the following command: ```bash -easytrain -c basicts/options/GraphWaveNet/GraphWaveNet_METR-LA.py --gpus '0' +python examples/run.py -c /path/to/your/config/file.py --gpus '0' ``` -If you need to debug, you could run the `basicts/run.py` file. +## 📌 Examples -### 4.3 Train a Custom Model +### Reproducing Built-in Models -🕐 +BasicTS provides a wealth of built-in models. You can find all the built-in models and their corresponding runners in [`basicts/archs/arch_zoo`](basicts/archs/arch_zoo/) and [`basicts/runners/runner_zoo`](basicts/runners/runner_zoo/), respectively. You can reproduce these models by running the following command: -## 5. Detailed Docs +```bash +python examples/run.py -c examples/${MODEL_NAME}/${MODEL_NAME}_${DATASET_NAME}.py --gpus '0' +``` + +Replace `${DATASET_NAME}` and `${MODEL_NAME}` with any supported models and datasets. For example, you can run Graph WaveNet[2] on METR-LA dataset by: + +```bash +python examples/run.py -c examples/GWNet/GWNet_METR-LA.py --gpus '0' +``` -- data preparation: [data_preparation_CN.md](docs/DataFormat_CN.md) +### Customized Your Own Model -🕐 +- [Multi-Layer Perceptron (MLP)](examples/MLP) +- More... -## 6. Main Results +## 📉 Main Results -![Main results.](results/result.png) +![Main results.](results/results.png) -## 7. TODO +## 🔗 Acknowledgement -- [ ] : add the result of STID. -- [ ] : revise the data preprocessing of PEMS07, which only contains weekdays. -- [ ] : Add detailed documentation and a demo about data preprocessing. -- [ ] : Add more multivariate time series datasets: Traffic, Electricity, Exchange-Rate, ETTh1, ETTh2, ETTm1, Weather, Solar-Energy. -- [ ] : Different from the existing traffic datasets (PEMS0X, PEMS-BAY, METR-LA), these datasets have multiple usages in the existing datasets, and the baselines that need to be compared in different contexts are different. Therefore, it is necessary to add statistics for all datasets and describe their typical settings case by case. -- [ ] : Add statistical information of these dataset, and descibe their typical settings. -- [ ] : Support models like ASTGCN, ASTGNN, which take multi-periodicities data as input. -- [ ] : Add detailed docs about 4.2, e.g., the usage of gpu. -- [ ] : Update D2STGNN arch. -- [ ] : 模块化train_iters, val_iters, and test_iters中的过程。否则就会像GTS一样, 一旦模型有一点特殊 (例如多一个返回和不同的loss), 就必须重写整个train_iters, val_iters, and test_iters。 +BasicTS is developed based on [EasyTorch](https://github.com/cnstark/easytorch)[1], an easy-to-use and powerful open-source neural network training framework. -## References +## 📜 References -[1] Yuhao Wang. EasyTorch. , 2020. +- [1] Yuhao Wang. EasyTorch. , 2020. +- [2] Wu Z, Pan S, Long G, et al. Graph WaveNet for Deep Spatial-Temporal Graph Modeling[C]//The 28th International Joint Conference on Artificial Intelligence (IJCAI). International Joint Conferences on Artificial Intelligence Organization, 2019. diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..9d572fcc --- /dev/null +++ b/TODO.md @@ -0,0 +1,13 @@ +# 📆 TODO + +- Add Transformer-based methods, such as Informer, AutoFormer, FEDFormer, PyraFormer. +- Add more datasets, such as electricity, ETT, exchange rate, weather, traffic, and illness. +- Publish to PyPI +- Add more docs. + - Document of the Pipeline + - Build-in Time Series Forecasting Dataset + - Architecture Interfaces + - Runner Interfaces + - Configuration Document + - Cutomized Loss + - Utils diff --git a/basicts/__init__.py b/basicts/__init__.py new file mode 100644 index 00000000..7cc01675 --- /dev/null +++ b/basicts/__init__.py @@ -0,0 +1,5 @@ +from .launcher import launch_training + +__version__ = "0.1.0" + +__all__ = ["__version__", "launch_training"] diff --git a/basicts/archs/AGCRN_arch/AGCN.py b/basicts/archs/AGCRN_arch/AGCN.py deleted file mode 100644 index e059fd7f..00000000 --- a/basicts/archs/AGCRN_arch/AGCN.py +++ /dev/null @@ -1,27 +0,0 @@ -import torch -import torch.nn.functional as F -import torch.nn as nn - -class AVWGCN(nn.Module): - def __init__(self, dim_in, dim_out, cheb_k, embed_dim): - super(AVWGCN, self).__init__() - self.cheb_k = cheb_k - self.weights_pool = nn.Parameter(torch.FloatTensor(embed_dim, cheb_k, dim_in, dim_out)) - self.bias_pool = nn.Parameter(torch.FloatTensor(embed_dim, dim_out)) - - def forward(self, x, node_embeddings): - #x shaped[B, N, C], node_embeddings shaped [N, D] -> supports shaped [N, N] - #output shape [B, N, C] - node_num = node_embeddings.shape[0] - supports = F.softmax(F.relu(torch.mm(node_embeddings, node_embeddings.transpose(0, 1))), dim=1) - support_set = [torch.eye(node_num).to(supports.device), supports] - #default cheb_k = 3 - for k in range(2, self.cheb_k): - support_set.append(torch.matmul(2 * supports, support_set[-1]) - support_set[-2]) - supports = torch.stack(support_set, dim=0) - weights = torch.einsum('nd,dkio->nkio', node_embeddings, self.weights_pool) #N, cheb_k, dim_in, dim_out - bias = torch.matmul(node_embeddings, self.bias_pool) #N, dim_out - x_g = torch.einsum("knm,bmc->bknc", supports, x) #B, cheb_k, N, dim_in - x_g = x_g.permute(0, 2, 1, 3) # B, N, cheb_k, dim_in - x_gconv = torch.einsum('bnki,nkio->bno', x_g, weights) + bias #b, N, dim_out - return x_gconv diff --git a/basicts/archs/AGCRN_arch/AGCRN_arch.py b/basicts/archs/AGCRN_arch/AGCRN_arch.py deleted file mode 100644 index 1872f72d..00000000 --- a/basicts/archs/AGCRN_arch/AGCRN_arch.py +++ /dev/null @@ -1,106 +0,0 @@ -import torch -import torch.nn as nn -from basicts.archs.AGCRN_arch.AGCRNCell import AGCRNCell -from basicts.archs.registry import ARCH_REGISTRY - - -""" - Paper: Adaptive Graph Convolutional Recurrent Network for Traffic Forecasting - Official Code: https://github.com/LeiBAI/AGCRN -""" - -class AVWDCRNN(nn.Module): - def __init__(self, node_num, dim_in, dim_out, cheb_k, embed_dim, num_layers=1): - super(AVWDCRNN, self).__init__() - assert num_layers >= 1, 'At least one DCRNN layer in the Encoder.' - self.node_num = node_num - self.input_dim = dim_in - self.num_layers = num_layers - self.dcrnn_cells = nn.ModuleList() - self.dcrnn_cells.append(AGCRNCell(node_num, dim_in, dim_out, cheb_k, embed_dim)) - for _ in range(1, num_layers): - self.dcrnn_cells.append(AGCRNCell(node_num, dim_out, dim_out, cheb_k, embed_dim)) - - def forward(self, x, init_state, node_embeddings): - #shape of x: (B, T, N, D) - #shape of init_state: (num_layers, B, N, hidden_dim) - assert x.shape[2] == self.node_num and x.shape[3] == self.input_dim - seq_length = x.shape[1] - current_inputs = x - output_hidden = [] - for i in range(self.num_layers): - state = init_state[i] - inner_states = [] - for t in range(seq_length): - state = self.dcrnn_cells[i](current_inputs[:, t, :, :], state, node_embeddings) - inner_states.append(state) - output_hidden.append(state) - current_inputs = torch.stack(inner_states, dim=1) - #current_inputs: the outputs of last layer: (B, T, N, hidden_dim) - #output_hidden: the last state for each layer: (num_layers, B, N, hidden_dim) - #last_state: (B, N, hidden_dim) - return current_inputs, output_hidden - - def init_hidden(self, batch_size): - init_states = [] - for i in range(self.num_layers): - init_states.append(self.dcrnn_cells[i].init_hidden_state(batch_size)) - return torch.stack(init_states, dim=0) #(num_layers, B, N, hidden_dim) - - -@ARCH_REGISTRY.register() -class AGCRN(nn.Module): - def __init__(self, num_nodes, input_dim, rnn_units, output_dim, horizon, num_layers, default_graph, embed_dim, cheb_k): - super(AGCRN, self).__init__() - self.num_node = num_nodes - self.input_dim = input_dim - self.hidden_dim = rnn_units - self.output_dim = output_dim - self.horizon = horizon - self.num_layers = num_layers - - self.default_graph = default_graph - self.node_embeddings = nn.Parameter(torch.randn(self.num_node, embed_dim), requires_grad=True) - - self.encoder = AVWDCRNN(num_nodes, input_dim, rnn_units, cheb_k, - embed_dim, num_layers) - - #predictor - self.end_conv = nn.Conv2d(1, horizon * self.output_dim, kernel_size=(1, self.hidden_dim), bias=True) - - self.init_param() - - def init_param(self): - for p in self.parameters(): - if p.dim() > 1: - nn.init.xavier_uniform_(p) - else: - nn.init.uniform_(p) - # print('*****************Model Parameter*****************') - # only_num = False - # if not only_num: - # for name, param in self.named_parameters(): - # print(name, param.shape, param.requires_grad) - # total_num = sum([param.nelement() for param in self.parameters()]) - # print('Total params num: {}'.format(total_num)) - # print('*****************Finish Parameter****************') - - def forward(self, history_data: torch.Tensor) -> torch.Tensor: - """feedforward function of AGCRN. - - Args: - source (torch.Tensor): inputs with shape [B, L, N, C] - - Returns: - torch.Tensor: outputs with shape [B, L, N, C] - """ - init_state = self.encoder.init_hidden(history_data.shape[0]) - output, _ = self.encoder(history_data, init_state, self.node_embeddings) #B, T, N, hidden - output = output[:, -1:, :, :] #B, 1, N, hidden - - #CNN based predictor - output = self.end_conv((output)) #B, T*C, N, 1 - output = output.squeeze(-1).reshape(-1, self.horizon, self.output_dim, self.num_node) - output = output.permute(0, 1, 3, 2) #B, T, N, C - - return output diff --git a/basicts/archs/AGCRN_arch/__init__.py b/basicts/archs/AGCRN_arch/__init__.py deleted file mode 100644 index 294b7809..00000000 --- a/basicts/archs/AGCRN_arch/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from basicts.archs.AGCRN_arch.AGCRN_arch import AGCRN \ No newline at end of file diff --git a/basicts/archs/D2STGNN_arch/DiffusionBlock/__init__.py b/basicts/archs/D2STGNN_arch/DiffusionBlock/__init__.py deleted file mode 100644 index 04bcc638..00000000 --- a/basicts/archs/D2STGNN_arch/DiffusionBlock/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from basicts.archs.D2STGNN_arch.DiffusionBlock.dif_block import DifBlock \ No newline at end of file diff --git a/basicts/archs/D2STGNN_arch/DiffusionBlock/dif_block.py b/basicts/archs/D2STGNN_arch/DiffusionBlock/dif_block.py deleted file mode 100644 index c002bf24..00000000 --- a/basicts/archs/D2STGNN_arch/DiffusionBlock/dif_block.py +++ /dev/null @@ -1,31 +0,0 @@ -import torch.nn as nn -from basicts.archs.D2STGNN_arch.DiffusionBlock.forecast import Forecast -from basicts.archs.D2STGNN_arch.Decouple.residual_decomp import ResidualDecomp -from basicts.archs.D2STGNN_arch.DiffusionBlock.dif_model import STLocalizedConv - -class DifBlock(nn.Module): - def __init__(self, hidden_dim, fk_dim=256, use_pre=None, dy_graph=None, sta_graph=None, **model_args): - super().__init__() - self.pre_defined_graph = model_args['adjs'] - - self.localized_st_conv = STLocalizedConv(hidden_dim, pre_defined_graph=self.pre_defined_graph, use_pre=use_pre, dy_graph=dy_graph, sta_graph=sta_graph, **model_args) - - # sub and norm - self.residual_decompose = ResidualDecomp([-1, -1, -1, hidden_dim]) - # forecast - self.forecast_branch = Forecast(hidden_dim, fk_dim=fk_dim, **model_args) - # backcast - self.backcast_branch = nn.Linear(hidden_dim, hidden_dim) - - def forward(self, X, X_spa, dynamic_graph, static_graph): - Z = self.localized_st_conv(X_spa, dynamic_graph, static_graph) - # forecast branch - forecast_hidden = self.forecast_branch(X_spa, Z, self.localized_st_conv, dynamic_graph, static_graph) - # backcast branch - backcast_seq = self.backcast_branch(Z) - # Residual Decomposition - backcast_seq = backcast_seq - X = X[:, -backcast_seq.shape[1]:, :, :] - backcast_seq_res= self.residual_decompose(X, backcast_seq) - - return backcast_seq_res, forecast_hidden diff --git a/basicts/archs/D2STGNN_arch/DiffusionBlock/dif_model.py b/basicts/archs/D2STGNN_arch/DiffusionBlock/dif_model.py deleted file mode 100644 index 13c9a27b..00000000 --- a/basicts/archs/D2STGNN_arch/DiffusionBlock/dif_model.py +++ /dev/null @@ -1,87 +0,0 @@ -import torch -import torch.nn as nn - -class STLocalizedConv(nn.Module): - def __init__(self, hidden_dim, pre_defined_graph=None, use_pre=None, dy_graph=None, sta_graph=None, **model_args): - super().__init__() - # gated temporal conv - self.k_s = model_args['k_s'] - self.k_t = model_args['k_t'] - self.hidden_dim = hidden_dim - - # graph conv - self.pre_defined_graph = pre_defined_graph - self.use_predefined_graph = use_pre - self.use_dynamic_hidden_graph = dy_graph - self.use_static__hidden_graph = sta_graph - - self.support_len = len(self.pre_defined_graph) + int(dy_graph) + int(sta_graph) - self.num_matric = (int(use_pre) * len(self.pre_defined_graph) + len(self.pre_defined_graph) * int(dy_graph) + int(sta_graph)) * self.k_s + 1 - self.dropout = nn.Dropout(model_args['dropout']) - self.pre_defined_graph = self.get_graph(self.pre_defined_graph) - - self.fc_list_updt = nn.Linear(self.k_t * hidden_dim, self.k_t * hidden_dim, bias=False) - self.gcn_updt = nn.Linear(self.hidden_dim*self.num_matric, self.hidden_dim) - - # others - self.bn = nn.BatchNorm2d(self.hidden_dim) - self.activation = nn.ReLU() - - def gconv(self, support, X_k, X_0): - out = [X_0] - for graph in support: - if len(graph.shape) == 2: # staitic or predefined graph - pass - else: - graph = graph.unsqueeze(1) - H_k = torch.matmul(graph, X_k) - out.append(H_k) - out = torch.cat(out, dim=-1) - out = self.gcn_updt(out) - out = self.dropout(out) - return out - - def get_graph(self, support): - # Only used in static including static hidden graph and predefined graph, but not used for dynamic graph. - graph_ordered = [] - mask = 1 - torch.eye(support[0].shape[0]).to(support[0].device) - for graph in support: - k_1_order = graph # 1 order - graph_ordered.append(k_1_order * mask) - for k in range(2, self.k_s+1): # e.g., order = 3, k=[2, 3]; order = 2, k=[2] - k_1_order = torch.matmul(graph, k_1_order) - graph_ordered.append(k_1_order * mask) - # get st localed graph - st_local_graph = [] - for graph in graph_ordered: - graph = graph.unsqueeze(-2).expand(-1, self.k_t, -1) - graph = graph.reshape(graph.shape[0], graph.shape[1] * graph.shape[2]) - st_local_graph.append(graph) # [num_nodes, kernel_size x num_nodes] - return st_local_graph # [order, num_nodes, kernel_size x num_nodes] - - def forward(self, X, dynamic_graph, static_graph): - # X: [bs, seq, nodes, feat] - X = X.unfold(1, self.k_t, 1).permute(0, 1, 2, 4, 3) # [bs, seq, num_nodes, ks, num_feat] - batch_size, seq_len, num_nodes, kernel_size, num_feat = X.shape # seq_len is changing - - # support - support = [] - ## predefined graph - if self.use_predefined_graph: - support = support + self.pre_defined_graph - ## dynamic graph - if self.use_dynamic_hidden_graph: - support = support + dynamic_graph # k_order is caled in dynamic_graph_constructor component - ## predefined graphs and static hidden graphs - if self.use_static__hidden_graph: - support = support + self.get_graph(static_graph) - - # parallelize - X = X.reshape(batch_size, seq_len, num_nodes, kernel_size * num_feat) - out = self.fc_list_updt(X) # batch_size, seq_len, num_nodes, kernel_size * hidden_dim - out = self.activation(out) - out = out.view(batch_size, seq_len, num_nodes, kernel_size, num_feat) - X_0 = torch.mean(out, dim=-2) - X_k = out.transpose(-3, -2).reshape(batch_size, seq_len, kernel_size*num_nodes, num_feat) # batch_size, seq_len, kernel_size x num_nodes, hidden_dim - hidden = self.gconv(support, X_k, X_0) # Nx3N 3NxD -> NxD: batch_size, seq_len, num_nodes, hidden_dim - return hidden diff --git a/basicts/archs/D2STGNN_arch/DynamicGraphConv/Utils/__init__.py b/basicts/archs/D2STGNN_arch/DynamicGraphConv/Utils/__init__.py deleted file mode 100644 index 1585c38e..00000000 --- a/basicts/archs/D2STGNN_arch/DynamicGraphConv/Utils/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from basicts.archs.D2STGNN_arch.DynamicGraphConv.Utils.mask import * -from basicts.archs.D2STGNN_arch.DynamicGraphConv.Utils.normalizer import * -from basicts.archs.D2STGNN_arch.DynamicGraphConv.Utils.distance import * diff --git a/basicts/archs/D2STGNN_arch/InherentBlock/__init__.py b/basicts/archs/D2STGNN_arch/InherentBlock/__init__.py deleted file mode 100644 index 18a29126..00000000 --- a/basicts/archs/D2STGNN_arch/InherentBlock/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from basicts.archs.D2STGNN_arch.InherentBlock.inh_block import InhBlock diff --git a/basicts/archs/D2STGNN_arch/InherentBlock/inh_block.py b/basicts/archs/D2STGNN_arch/InherentBlock/inh_block.py deleted file mode 100644 index f206fb28..00000000 --- a/basicts/archs/D2STGNN_arch/InherentBlock/inh_block.py +++ /dev/null @@ -1,65 +0,0 @@ -import math -import torch -import torch.nn as nn -from basicts.archs.D2STGNN_arch.Decouple.residual_decomp import ResidualDecomp -from basicts.archs.D2STGNN_arch.InherentBlock.inh_model import RNNLayer, TransformerLayer -from basicts.archs.D2STGNN_arch.InherentBlock.forecast import Forecast - -class PositionalEncoding(nn.Module): - def __init__(self, d_model, dropout=None, max_len: int = 5000): - super().__init__() - self.dropout = nn.Dropout(p=dropout) - position = torch.arange(max_len).unsqueeze(1) - div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model)) - pe = torch.zeros(max_len, 1, d_model) - pe[:, 0, 0::2] = torch.sin(position * div_term) - pe[:, 0, 1::2] = torch.cos(position * div_term) - self.register_buffer('pe', pe) - - def forward(self, X): - X = X + self.pe[:X.size(0)] - X = self.dropout(X) - return X - -class InhBlock(nn.Module): - def __init__(self, hidden_dim, num_heads=4, bias=True, fk_dim=256, first=None, **model_args): - super().__init__() - self.num_feat = hidden_dim - self.hidden_dim = hidden_dim - - if first: - self.pos_encoder = PositionalEncoding(hidden_dim, model_args['dropout']) - else: - self.pos_encoder = None - self.rnn_layer = RNNLayer(hidden_dim, model_args['dropout']) - self.transformer_layer = TransformerLayer(hidden_dim, num_heads, model_args['dropout'], bias) - # forecast - self.forecast_block = Forecast(hidden_dim, fk_dim, **model_args) - # backcast - self.backcast_fc = nn.Linear(hidden_dim, hidden_dim) - # sub residual - self.sub_and_norm = ResidualDecomp([-1, -1, -1, hidden_dim]) - - def forward(self, X): - [batch_size, seq_len, num_nodes, num_feat] = X.shape - # Temporal Model - ## RNN - RNN_H_raw = self.rnn_layer(X) - ## Positional Encoding - if self.pos_encoder is not None: - RNN_H = self.pos_encoder(RNN_H_raw) - else: - RNN_H = RNN_H_raw - ## MultiHead Self Attention - Z = self.transformer_layer(RNN_H, RNN_H, RNN_H) - - # forecast branch - forecast_hidden = self.forecast_block(X, RNN_H_raw, Z, self.transformer_layer, self.rnn_layer, self.pos_encoder) - - # backcast branch - Z = Z.reshape(seq_len, batch_size, num_nodes, num_feat) - Z = Z.transpose(0, 1) - backcast_seq = self.backcast_fc(Z) - backcast_seq_res= self.sub_and_norm(X, backcast_seq) - - return backcast_seq_res, forecast_hidden diff --git a/basicts/archs/D2STGNN_arch/InherentBlock/inh_model.py b/basicts/archs/D2STGNN_arch/InherentBlock/inh_model.py deleted file mode 100644 index 46e08ff9..00000000 --- a/basicts/archs/D2STGNN_arch/InherentBlock/inh_model.py +++ /dev/null @@ -1,33 +0,0 @@ -import torch as th -import torch.nn as nn -from torch.nn import MultiheadAttention - -class RNNLayer(nn.Module): - def __init__(self, hidden_dim, dropout=None): - super().__init__() - self.hidden_dim = hidden_dim - self.gru_cell = nn.GRUCell(hidden_dim, hidden_dim) - self.dropout = nn.Dropout(dropout) - - def forward(self, X): - [batch_size, seq_len, num_nodes, hidden_dim] = X.shape - X = X.transpose(1, 2).reshape(batch_size * num_nodes, seq_len, hidden_dim) - hx = th.zeros_like(X[:, 0, :]) - output = [] - for _ in range(X.shape[1]): - hx = self.gru_cell(X[:, _, :], hx) - output.append(hx) - output = th.stack(output, dim=0) - output = self.dropout(output) - return output - -class TransformerLayer(nn.Module): - def __init__(self, hidden_dim, num_heads=4, dropout=None, bias=True): - super().__init__() - self.multi_head_self_attention = MultiheadAttention(hidden_dim, num_heads, dropout=dropout, bias=bias) - self.dropout = nn.Dropout(dropout) - - def forward(self, X, K, V): - Z = self.multi_head_self_attention(X, K, V)[0] - Z = self.dropout(Z) - return Z diff --git a/basicts/archs/D2STGNN_arch/__init__.py b/basicts/archs/D2STGNN_arch/__init__.py deleted file mode 100644 index 8b9a71b4..00000000 --- a/basicts/archs/D2STGNN_arch/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from basicts.archs.D2STGNN_arch.D2STGNN_arch import D2STGNN \ No newline at end of file diff --git a/basicts/archs/DCRNN_arch/__init__.py b/basicts/archs/DCRNN_arch/__init__.py deleted file mode 100644 index 0ae64edd..00000000 --- a/basicts/archs/DCRNN_arch/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from basicts.archs.DCRNN_arch.DCRNN_arch import DCRNN \ No newline at end of file diff --git a/basicts/archs/DGCRN_arch/__init__.py b/basicts/archs/DGCRN_arch/__init__.py deleted file mode 100644 index b118f2f3..00000000 --- a/basicts/archs/DGCRN_arch/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from basicts.archs.DGCRN_arch.DGCRN_arch import DGCRN \ No newline at end of file diff --git a/basicts/archs/GMAN_arch/GMAN_arch.py b/basicts/archs/GMAN_arch/GMAN_arch.py deleted file mode 100644 index 1be659cc..00000000 --- a/basicts/archs/GMAN_arch/GMAN_arch.py +++ /dev/null @@ -1,380 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -import math -from basicts.archs.registry import ARCH_REGISTRY - -""" - Paper: GMAN: A Graph Multi-Attention Network for Traffic Prediction - Ref Code: https://github.com/VincLee8188/GMAN-PyTorch/blob/master/model/model_.py - Official Code (TensorFlow): https://github.com/zhengchuanpan/GMAN - TODO: Compared to the official tensorflow version code, this pytorch implementation can achieve similar MAE performance. But somehow, RMSE and MAPE seem to be abnormally high. -""" - -class conv2d_(nn.Module): - def __init__(self, input_dims, output_dims, kernel_size, stride=(1, 1), - padding='SAME', use_bias=True, activation=F.relu, - bn_decay=None): - super(conv2d_, self).__init__() - self.activation = activation - if padding == 'SAME': - self.padding_size = math.ceil(kernel_size) - else: - self.padding_size = [0, 0] - self.conv = nn.Conv2d(input_dims, output_dims, kernel_size, stride=stride, - padding=0, bias=use_bias) - self.batch_norm = nn.BatchNorm2d(output_dims, momentum=bn_decay) - - def forward(self, x): - x = x.permute(0, 3, 2, 1) - x = F.pad(x, ([self.padding_size[1], self.padding_size[1], self.padding_size[0], self.padding_size[0]])) - x = self.conv(x) - x = self.batch_norm(x) - if self.activation is not None: - x = F.relu_(x) - return x.permute(0, 3, 2, 1) - - -class FC(nn.Module): - def __init__(self, input_dims, units, activations, bn_decay, use_bias=True): - super(FC, self).__init__() - if isinstance(units, int): - units = [units] - input_dims = [input_dims] - activations = [activations] - elif isinstance(units, tuple): - units = list(units) - input_dims = list(input_dims) - activations = list(activations) - assert type(units) == list - self.convs = nn.ModuleList([conv2d_( - input_dims=input_dim, output_dims=num_unit, kernel_size=[1, 1], stride=[1, 1], - padding='VALID', use_bias=use_bias, activation=activation, - bn_decay=bn_decay) for input_dim, num_unit, activation in - zip(input_dims, units, activations)]) - - def forward(self, x): - for conv in self.convs: - x = conv(x) - return x - - -class STEmbedding(nn.Module): - ''' - spatio-temporal embedding - SE: [num_vertex, D] - TE: [batch_size, num_his + num_pred, 2] (dayofweek, timeofday) - T: num of time steps in one day - D: output dims - retrun: [batch_size, num_his + num_pred, num_vertex, D] - ''' - - def __init__(self, D, bn_decay): - super(STEmbedding, self).__init__() - self.FC_se = FC( - input_dims=[D, D], units=[D, D], activations=[F.relu, None], - bn_decay=bn_decay) - - self.FC_te = FC( - input_dims=[295, D], units=[D, D], activations=[F.relu, None], - bn_decay=bn_decay) # input_dims = time step per day + days per week=288+7=295 - - def forward(self, SE, TE, T=288): - # spatial embedding - SE = SE.unsqueeze(0).unsqueeze(0) - SE = self.FC_se(SE) - # temporal embedding - dayofweek = torch.empty(TE.shape[0], TE.shape[1], 7).to(SE.device) - timeofday = torch.empty(TE.shape[0], TE.shape[1], T).to(SE.device) - for i in range(TE.shape[0]): - dayofweek[i] = F.one_hot(TE[..., 1][i].to(torch.int64) % 7, 7) - for j in range(TE.shape[0]): - timeofday[j] = F.one_hot(TE[..., 0][j].to(torch.int64) % 288, T) - TE = torch.cat((dayofweek, timeofday), dim=-1) - TE = TE.unsqueeze(dim=2) - TE = self.FC_te(TE) - del dayofweek, timeofday - return SE + TE - - -class spatialAttention(nn.Module): - ''' - spatial attention mechanism - X: [batch_size, num_step, num_vertex, D] - STE: [batch_size, num_step, num_vertex, D] - K: number of attention heads - d: dimension of each attention outputs - return: [batch_size, num_step, num_vertex, D] - ''' - - def __init__(self, K, d, bn_decay): - super(spatialAttention, self).__init__() - D = K * d - self.d = d - self.K = K - self.FC_q = FC(input_dims=2 * D, units=D, activations=F.relu, - bn_decay=bn_decay) - self.FC_k = FC(input_dims=2 * D, units=D, activations=F.relu, - bn_decay=bn_decay) - self.FC_v = FC(input_dims=2 * D, units=D, activations=F.relu, - bn_decay=bn_decay) - self.FC = FC(input_dims=D, units=D, activations=F.relu, - bn_decay=bn_decay) - - def forward(self, X, STE): - batch_size = X.shape[0] - X = torch.cat((X, STE), dim=-1) - # [batch_size, num_step, num_vertex, K * d] - query = self.FC_q(X) # [B, L, N, K*d] - key = self.FC_k(X) - value = self.FC_v(X) - # [K * batch_size, num_step, num_vertex, d] - query = torch.cat(torch.split(query, self.d, dim=-1), dim=0) # see https://github.com/VincLee8188/GMAN-PyTorch/issues/3 - key = torch.cat(torch.split(key, self.d, dim=-1), dim=0) - value = torch.cat(torch.split(value, self.d, dim=-1), dim=0) - # [K * batch_size, num_step, num_vertex, num_vertex] - attention = torch.matmul(query, key.transpose(2, 3)) - attention /= (self.d ** 0.5) - attention = F.softmax(attention, dim=-1) - # [batch_size, num_step, num_vertex, D] - X = torch.matmul(attention, value) - X = torch.cat(torch.split(X, batch_size, dim=0), dim=-1) # orginal K, change to batch_size - X = self.FC(X) - del query, key, value, attention - return X - - -class temporalAttention(nn.Module): - ''' - temporal attention mechanism - X: [batch_size, num_step, num_vertex, D] - STE: [batch_size, num_step, num_vertex, D] - K: number of attention heads - d: dimension of each attention outputs - return: [batch_size, num_step, num_vertex, D] - ''' - - def __init__(self, K, d, bn_decay, mask=True): - super(temporalAttention, self).__init__() - D = K * d - self.d = d - self.K = K - self.mask = mask - self.FC_q = FC(input_dims=2 * D, units=D, activations=F.relu, - bn_decay=bn_decay) - self.FC_k = FC(input_dims=2 * D, units=D, activations=F.relu, - bn_decay=bn_decay) - self.FC_v = FC(input_dims=2 * D, units=D, activations=F.relu, - bn_decay=bn_decay) - self.FC = FC(input_dims=D, units=D, activations=F.relu, - bn_decay=bn_decay) - - def forward(self, X, STE): - batch_size_ = X.shape[0] - X = torch.cat((X, STE), dim=-1) - # [batch_size, num_step, num_vertex, K * d] - query = self.FC_q(X) - key = self.FC_k(X) - value = self.FC_v(X) - # [K * batch_size, num_step, num_vertex, d] - query = torch.cat(torch.split(query, self.K, dim=-1), dim=0) - key = torch.cat(torch.split(key, self.K, dim=-1), dim=0) - value = torch.cat(torch.split(value, self.K, dim=-1), dim=0) - # query: [K * batch_size, num_vertex, num_step, d] - # key: [K * batch_size, num_vertex, d, num_step] - # value: [K * batch_size, num_vertex, num_step, d] - query = query.permute(0, 2, 1, 3) - key = key.permute(0, 2, 3, 1) - value = value.permute(0, 2, 1, 3) - # [K * batch_size, num_vertex, num_step, num_step] - attention = torch.matmul(query, key) - attention /= (self.d ** 0.5) - # mask attention score - if self.mask: - batch_size = X.shape[0] - num_step = X.shape[1] - num_vertex = X.shape[2] - mask = torch.ones(num_step, num_step) - mask = torch.tril(mask) - mask = torch.unsqueeze(torch.unsqueeze(mask, dim=0), dim=0) - mask = mask.repeat(self.K * batch_size, num_vertex, 1, 1) - mask = mask.to(torch.bool) - attention = torch.where(mask, attention, -2 ** 15 + 1) - # softmax - attention = F.softmax(attention, dim=-1) - # [batch_size, num_step, num_vertex, D] - X = torch.matmul(attention, value) - X = X.permute(0, 2, 1, 3) - X = torch.cat(torch.split(X, batch_size_, dim=0), dim=-1) # orginal K, change to batch_size - X = self.FC(X) - del query, key, value, attention - return X - - -class gatedFusion(nn.Module): - ''' - gated fusion - HS: [batch_size, num_step, num_vertex, D] - HT: [batch_size, num_step, num_vertex, D] - D: output dims - return: [batch_size, num_step, num_vertex, D] - ''' - - def __init__(self, D, bn_decay): - super(gatedFusion, self).__init__() - self.FC_xs = FC(input_dims=D, units=D, activations=None, - bn_decay=bn_decay, use_bias=False) - self.FC_xt = FC(input_dims=D, units=D, activations=None, - bn_decay=bn_decay, use_bias=True) - self.FC_h = FC(input_dims=[D, D], units=[D, D], activations=[F.relu, None], - bn_decay=bn_decay) - - def forward(self, HS, HT): - XS = self.FC_xs(HS) - XT = self.FC_xt(HT) - z = torch.sigmoid(torch.add(XS, XT)) - H = torch.add(torch.mul(z, HS), torch.mul(1 - z, HT)) - H = self.FC_h(H) - del XS, XT, z - return H - - -class STAttBlock(nn.Module): - def __init__(self, K, d, bn_decay, mask=False): - super(STAttBlock, self).__init__() - self.spatialAttention = spatialAttention(K, d, bn_decay) - self.temporalAttention = temporalAttention(K, d, bn_decay, mask=mask) - self.gatedFusion = gatedFusion(K * d, bn_decay) - - def forward(self, X, STE): - HS = self.spatialAttention(X, STE) - HT = self.temporalAttention(X, STE) - H = self.gatedFusion(HS, HT) - del HS, HT - return torch.add(X, H) - - -class transformAttention(nn.Module): - ''' - transform attention mechanism - X: [batch_size, num_his, num_vertex, D] - STE_his: [batch_size, num_his, num_vertex, D] - STE_pred: [batch_size, num_pred, num_vertex, D] - K: number of attention heads - d: dimension of each attention outputs - return: [batch_size, num_pred, num_vertex, D] - ''' - - def __init__(self, K, d, bn_decay): - super(transformAttention, self).__init__() - D = K * d - self.K = K - self.d = d - self.FC_q = FC(input_dims=D, units=D, activations=F.relu, - bn_decay=bn_decay) - self.FC_k = FC(input_dims=D, units=D, activations=F.relu, - bn_decay=bn_decay) - self.FC_v = FC(input_dims=D, units=D, activations=F.relu, - bn_decay=bn_decay) - self.FC = FC(input_dims=D, units=D, activations=F.relu, - bn_decay=bn_decay) - - def forward(self, X, STE_his, STE_pred): - batch_size = X.shape[0] - # [batch_size, num_step, num_vertex, K * d] - query = self.FC_q(STE_pred) - key = self.FC_k(STE_his) - value = self.FC_v(X) - # [K * batch_size, num_step, num_vertex, d] - query = torch.cat(torch.split(query, self.K, dim=-1), dim=0) - key = torch.cat(torch.split(key, self.K, dim=-1), dim=0) - value = torch.cat(torch.split(value, self.K, dim=-1), dim=0) - # query: [K * batch_size, num_vertex, num_pred, d] - # key: [K * batch_size, num_vertex, d, num_his] - # value: [K * batch_size, num_vertex, num_his, d] - query = query.permute(0, 2, 1, 3) - key = key.permute(0, 2, 3, 1) - value = value.permute(0, 2, 1, 3) - # [K * batch_size, num_vertex, num_pred, num_his] - attention = torch.matmul(query, key) - attention /= (self.d ** 0.5) - attention = F.softmax(attention, dim=-1) - # [batch_size, num_pred, num_vertex, D] - X = torch.matmul(attention, value) - X = X.permute(0, 2, 1, 3) - X = torch.cat(torch.split(X, batch_size, dim=0), dim=-1) - X = self.FC(X) - del query, key, value, attention - return X - - -@ARCH_REGISTRY.register() -class GMAN(nn.Module): - ''' - GMAN - X: [batch_size, num_his, num_vertx] - TE: [batch_size, num_his + num_pred, 2] (time-of-day, day-of-week) - SE: [num_vertex, K * d] - num_his: number of history steps - num_pred:number of prediction steps - T: one day is divided into T steps - L: number of STAtt blocks in the encoder/decoder - K: number of attention heads - d: dimension of each attention head outputs - return: [batch_size, num_pred, num_vertex] - ''' - - def __init__(self, SE, L, K, d, num_his, bn_decay): - super(GMAN, self).__init__() - D = K * d - self.num_his = num_his - self.SE = nn.Parameter(SE) - self.STEmbedding = STEmbedding(D, bn_decay) - self.STAttBlock_1 = nn.ModuleList([STAttBlock(K, d, bn_decay) for _ in range(L)]) - self.STAttBlock_2 = nn.ModuleList([STAttBlock(K, d, bn_decay) for _ in range(L)]) - self.transformAttention = transformAttention(K, d, bn_decay) - self.FC_1 = FC(input_dims=[1, D], units=[D, D], activations=[F.relu, None], - bn_decay=bn_decay) - self.FC_2 = FC(input_dims=[D, D], units=[D, 1], activations=[F.relu, None], - bn_decay=bn_decay) - - def forward(self, history_data: torch.Tensor, future_data: torch.Tensor) -> torch.Tensor: - """feedforward function of GMAN. - - Args: - X (torch.Tensor): Historical data with shape [B, L, N, C]. X[..., 1:3] is the 'time in day' and 'day in week' feature. - Y (torch.Tensor): Future data with shape [B, L, N, C]. Y[..., 1:3] is the 'time in day' and 'day in week' feature. - - Returns: - torch.Tensor: Predictions with shape [B, L, N, 1] - """ - - # prepare data - history_data[..., 1] = (history_data[..., 1] * 288).type(torch.LongTensor) - future_data[..., 1] = (future_data[..., 1] * 288).type(torch.LongTensor) - - TE_X = history_data[..., 0, 1:3] - TE_Y = future_data[..., 0, 1:3] - TE = torch.cat([TE_X, TE_Y], dim=1) - - X = history_data[..., [0]] - - # feed forward - # input - X = self.FC_1(X) - # STE - STE = self.STEmbedding(self.SE, TE) - STE_his = STE[:, :self.num_his] - STE_pred = STE[:, self.num_his:] - # encoder - for net in self.STAttBlock_1: - X = net(X, STE_his) - # transAtt - X = self.transformAttention(X, STE_his, STE_pred) - # decoder - for net in self.STAttBlock_2: - X = net(X, STE_pred) - # output - pred = self.FC_2(X) - del STE, STE_his, STE_pred - return pred diff --git a/basicts/archs/GMAN_arch/__init__.py b/basicts/archs/GMAN_arch/__init__.py deleted file mode 100644 index 5ad9e0ae..00000000 --- a/basicts/archs/GMAN_arch/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from basicts.archs.GMAN_arch.GMAN_arch import GMAN \ No newline at end of file diff --git a/basicts/archs/GTS_arch/__init__.py b/basicts/archs/GTS_arch/__init__.py deleted file mode 100644 index e2d25a76..00000000 --- a/basicts/archs/GTS_arch/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from basicts.archs.GTS_arch.GTS_arch import GTS \ No newline at end of file diff --git a/basicts/archs/GraphWaveNet_arch/__init__.py b/basicts/archs/GraphWaveNet_arch/__init__.py deleted file mode 100644 index f5ab9975..00000000 --- a/basicts/archs/GraphWaveNet_arch/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from basicts.archs.GraphWaveNet_arch.GraphWaveNet_arch import GraphWaveNet \ No newline at end of file diff --git a/basicts/archs/HI_arch/HI_arch.py b/basicts/archs/HI_arch/HI_arch.py deleted file mode 100644 index 31a24c2b..00000000 --- a/basicts/archs/HI_arch/HI_arch.py +++ /dev/null @@ -1,45 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -from basicts.archs.registry import ARCH_REGISTRY - -""" - Paper: Historical Inertia: A Neglected but Powerful Baseline for Long Sequence Time-series Forecasting -""" - -class HINetwork(nn.Module): - def __init__(self, input_length: int, output_length: int, channel=None, reverse=False): - """we use HI[1] as the baseline model for the pipline. - [1] Historical Inertia: A Neglected but Powerful Baseline for Long Sequence Time-series Forecasting - - Args: - input_length (int): input time series length - output_length (int): prediction time series length - channel (list, optional): selected channels. Defaults to None. - reverse (bool, optional): if reverse the prediction of HI. Defaults to False. - """ - super(HINetwork, self).__init__() - assert input_length >= output_length, "HI model requires input length > output length" - self.input_length = input_length - self.output_length = output_length - self.channel = channel - self.reverse = reverse - self.fake_param = nn.Linear(1, 1) - - def forward(self, history_data: torch.Tensor, **kwargs) -> torch.Tensor: - """feedforward function of HI. - - Args: - history_data (torch.Tensor): shape = [B, L_in, N, C] - - Returns: - torch.Tensor: model prediction [B, L_out, N, C]. - """ - B, L_in, N, C = history_data.shape - assert self.input_length == L_in, 'error input length' - if self.channel is not None: - history_data = history_data[..., self.channel] - prediction = history_data[:, -self.output_length:, :, :] - if self.reverse: - prediction = prediction.flip(dims=[1]) - return prediction diff --git a/basicts/archs/HI_arch/__init__.py b/basicts/archs/HI_arch/__init__.py deleted file mode 100644 index 0d611c53..00000000 --- a/basicts/archs/HI_arch/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from basicts.archs.HI_arch.HI_arch import HINetwork \ No newline at end of file diff --git a/basicts/archs/LSTM_arch/LSTM_arch.py b/basicts/archs/LSTM_arch/LSTM_arch.py deleted file mode 100644 index 49b57d75..00000000 --- a/basicts/archs/LSTM_arch/LSTM_arch.py +++ /dev/null @@ -1,44 +0,0 @@ -import torch -import torch.nn as nn -from basicts.archs.registry import ARCH_REGISTRY - - -@ARCH_REGISTRY.register() -class FCLSTM(nn.Module): - def __init__(self, input_dim, rnn_units, output_dim, horizon, num_layers, dropout=0.1): - super(FCLSTM, self).__init__() - self.input_dim = input_dim - self.hidden_dim = rnn_units - self.output_dim = output_dim - self.horizon = horizon - self.num_layers = num_layers - - self.encoder = nn.LSTM(input_size=self.input_dim, hidden_size=self.hidden_dim, num_layers=self.num_layers, batch_first=True, dropout=dropout) - - #predictor - self.end_fc = nn.Linear(self.num_layers*self.hidden_dim, self.horizon*self.output_dim) - - def forward(self, history_data: torch.Tensor) -> torch.Tensor: - """feedforward function of LSTM. - - Args: - source (torch.Tensor): inputs with shape [B, L, N, C] - - Returns: - torch.Tensor: outputs with shape [B, L, N, C] - """ - B, L, N, C = history_data.shape - # shared LSTM - history_data = history_data.transpose(1, 2) # [B, N, L, C] - inputs = history_data.reshape(B*N, L, C) - output, (h_n, c_n) = self.encoder(inputs) - h_n = h_n.transpose(0, 1) # [B*N, num_layers, hidden] - h_n = h_n.reshape(B*N, -1) # [B*N, num_layers * hidden] - h_n = h_n.view(B, N, -1) # [B, N, num_layers * hidden] - - # prediction - output = self.end_fc(h_n) # [B, N, self.horizon*self.output_dim] - output = output.view(B, N, self.horizon, self.output_dim) - output = output.transpose(1, 2) # [B, L, N, C] - - return output diff --git a/basicts/archs/LSTM_arch/__init__.py b/basicts/archs/LSTM_arch/__init__.py deleted file mode 100644 index 55b13776..00000000 --- a/basicts/archs/LSTM_arch/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from basicts.archs.LSTM_arch.LSTM_arch import FCLSTM as LSTM \ No newline at end of file diff --git a/basicts/archs/MTGNN_arch/MTGNN_arch.py b/basicts/archs/MTGNN_arch/MTGNN_arch.py deleted file mode 100644 index 0f4ed6df..00000000 --- a/basicts/archs/MTGNN_arch/MTGNN_arch.py +++ /dev/null @@ -1,140 +0,0 @@ -from basicts.archs.MTGNN_arch.MTGNN_layers import * -from basicts.archs.registry import ARCH_REGISTRY - -""" - Paper: Connecting the Dots: Multivariate Time Series Forecasting with Graph Neural Networks - Ref Official Code: https://github.com/nnzhan/MTGNN -""" - -@ARCH_REGISTRY.register() -class MTGNN(nn.Module): - def __init__(self, gcn_true, buildA_true, gcn_depth, num_nodes, predefined_A=None, static_feat=None, dropout=0.3, subgraph_size=20, node_dim=40, dilation_exponential=1, conv_channels=32, residual_channels=32, skip_channels=64, end_channels=128, seq_length=12, in_dim=2, out_dim=12, layers=3, propalpha=0.05, tanhalpha=3, layer_norm_affline=True): - super(MTGNN, self).__init__() - self.gcn_true = gcn_true - self.buildA_true = buildA_true - self.num_nodes = num_nodes - self.dropout = dropout - self.predefined_A = predefined_A - self.filter_convs = nn.ModuleList() - self.gate_convs = nn.ModuleList() - self.residual_convs = nn.ModuleList() - self.skip_convs = nn.ModuleList() - self.gconv1 = nn.ModuleList() - self.gconv2 = nn.ModuleList() - self.norm = nn.ModuleList() - self.start_conv = nn.Conv2d(in_channels=in_dim, out_channels=residual_channels, kernel_size=(1, 1)) - self.gc = graph_constructor(num_nodes, subgraph_size, node_dim, alpha=tanhalpha, static_feat=static_feat) - - self.seq_length = seq_length - kernel_size = 7 - if dilation_exponential>1: - self.receptive_field = int(1+(kernel_size-1)*(dilation_exponential**layers-1)/(dilation_exponential-1)) - else: - self.receptive_field = layers*(kernel_size-1) + 1 - - for i in range(1): - if dilation_exponential>1: - rf_size_i = int(1 + i*(kernel_size-1)*(dilation_exponential**layers-1)/(dilation_exponential-1)) - else: - rf_size_i = i*layers*(kernel_size-1)+1 - new_dilation = 1 - for j in range(1,layers+1): - if dilation_exponential > 1: - rf_size_j = int(rf_size_i + (kernel_size-1)*(dilation_exponential**j-1)/(dilation_exponential-1)) - else: - rf_size_j = rf_size_i+j*(kernel_size-1) - - self.filter_convs.append(dilated_inception(residual_channels, conv_channels, dilation_factor=new_dilation)) - self.gate_convs.append(dilated_inception(residual_channels, conv_channels, dilation_factor=new_dilation)) - self.residual_convs.append(nn.Conv2d(in_channels=conv_channels, out_channels=residual_channels, kernel_size=(1, 1))) - if self.seq_length>self.receptive_field: - self.skip_convs.append(nn.Conv2d(in_channels=conv_channels, out_channels=skip_channels, kernel_size=(1, self.seq_length-rf_size_j+1))) - else: - self.skip_convs.append(nn.Conv2d(in_channels=conv_channels, out_channels=skip_channels, kernel_size=(1, self.receptive_field-rf_size_j+1))) - - if self.gcn_true: - self.gconv1.append(mixprop(conv_channels, residual_channels, gcn_depth, dropout, propalpha)) - self.gconv2.append(mixprop(conv_channels, residual_channels, gcn_depth, dropout, propalpha)) - - if self.seq_length>self.receptive_field: - self.norm.append(LayerNorm((residual_channels, num_nodes, self.seq_length - rf_size_j + 1),elementwise_affine=layer_norm_affline)) - else: - self.norm.append(LayerNorm((residual_channels, num_nodes, self.receptive_field - rf_size_j + 1),elementwise_affine=layer_norm_affline)) - - new_dilation *= dilation_exponential - - self.layers = layers - self.end_conv_1 = nn.Conv2d(in_channels=skip_channels, out_channels=end_channels, kernel_size=(1,1), bias=True) - self.end_conv_2 = nn.Conv2d(in_channels=end_channels, out_channels=out_dim, kernel_size=(1,1), bias=True) - if self.seq_length > self.receptive_field: - self.skip0 = nn.Conv2d(in_channels=in_dim, out_channels=skip_channels, kernel_size=(1, self.seq_length), bias=True) - self.skipE = nn.Conv2d(in_channels=residual_channels, out_channels=skip_channels, kernel_size=(1, self.seq_length-self.receptive_field+1), bias=True) - - else: - self.skip0 = nn.Conv2d(in_channels=in_dim, out_channels=skip_channels, kernel_size=(1, self.receptive_field), bias=True) - self.skipE = nn.Conv2d(in_channels=residual_channels, out_channels=skip_channels, kernel_size=(1, 1), bias=True) - - - self.idx = torch.arange(self.num_nodes) - - - def forward(self, history_data: torch.Tensor, idx: int = None, **kwargs) -> torch.Tensor: - """feedforward function of MTGNN. - - Args: - history_data (torch.Tensor): history data with shape [B, L, N, C] - idx (int, optional): Graph Learning Hyperparameter. Defaults to None. - - Returns: - torch.Tensor: prediction - """ - # select feature - history_data = history_data.transpose(1, 3).contiguous() - seq_len = history_data.size(3) - assert seq_len==self.seq_length, 'input sequence length not equal to preset sequence length' - - if self.seq_length None: - super().__init__() - self.fc1 = nn.Conv2d(in_channels=input_dim, out_channels=hidden_dim, kernel_size=(1,1), bias=True) - self.fc2 = nn.Conv2d(in_channels=hidden_dim, out_channels=hidden_dim, kernel_size=(1,1), bias=True) - self.act = nn.ReLU() - self.drop = nn.Dropout(p=0.15) - - def forward(self, input_data:torch.Tensor) -> torch.Tensor: - """feed forward of MLP. - - Args: - input_data (torch.Tensor): input data with shape [B, D, N] - - Returns: - torch.Tensor: latent repr - """ - B, D, N, _ = input_data.shape - hidden = self.fc2(self.drop(self.act(self.fc1(input_data)))) # MLP - hidden = hidden + input_data # residual - return hidden diff --git a/basicts/archs/STID_arch/STID_arch.py b/basicts/archs/STID_arch/STID_arch.py deleted file mode 100644 index 22ff583e..00000000 --- a/basicts/archs/STID_arch/STID_arch.py +++ /dev/null @@ -1,97 +0,0 @@ -import torch -import torch.nn as nn -from basicts.archs.STID_arch.MLP import MLP_res -from basicts.archs.registry import ARCH_REGISTRY - - -@ARCH_REGISTRY.register() -class STID(nn.Module): - def __init__(self, **model_args): - super().__init__() - # attributes - self.num_nodes = model_args['num_nodes'] - self.node_dim = model_args['node_dim'] - self.input_len = model_args['input_len'] - self.input_dim = model_args['input_dim'] - self.embed_dim = model_args['embed_dim'] - self.output_len = model_args['output_len'] - self.num_layer = model_args['num_layer'] - self.temp_dim_tid = model_args['temp_dim_tid'] - self.temp_dim_diw = model_args['temp_dim_diw'] - - self.if_T_i_D = model_args['if_T_i_D'] - self.if_D_i_W = model_args['if_D_i_W'] - self.if_node = model_args['if_node'] - - # spatial embeddings - if self.if_node: - self.node_emb = nn.Parameter(torch.empty(self.num_nodes, self.node_dim)) - nn.init.xavier_uniform_(self.node_emb) - # temporal embeddings - if self.if_T_i_D: - self.T_i_D_emb = nn.Parameter(torch.empty(288, self.temp_dim_tid)) - nn.init.xavier_uniform_(self.T_i_D_emb) - if self.if_D_i_W: - self.D_i_W_emb = nn.Parameter(torch.empty(7, self.temp_dim_diw)) - nn.init.xavier_uniform_(self.D_i_W_emb) - - # embedding layer - self.time_series_emb_layer = nn.Conv2d(in_channels=self.input_dim * self.input_len, out_channels=self.embed_dim, kernel_size=(1, 1), bias=True) - - # encoding - self.hidden_dim = self.embed_dim+self.node_dim*int(self.if_node)+self.temp_dim_tid*int(self.if_D_i_W) + self.temp_dim_diw*int(self.if_T_i_D) - self.encoder = nn.Sequential(*[MLP_res(self.hidden_dim, self.hidden_dim) for _ in range(self.num_layer)]) - - # regression - self.regression_layer = nn.Conv2d(in_channels=self.hidden_dim, out_channels=self.output_len, kernel_size=(1,1), bias=True) - - def forward(self, history_data: torch.Tensor, **kwargs) -> torch.Tensor: - """feed forward. - - Args: - history_data (torch.Tensor): history data with shape [B, L, N, C] - - Returns: - torch.Tensor: prediction wit shape [B, L, N, C] - """ - # prepare data - X = history_data[..., range(self.input_dim)] - t_i_d_data = history_data[..., 1] - d_i_w_data = history_data[..., 2] - - if self.if_T_i_D: - T_i_D_emb = self.T_i_D_emb[(t_i_d_data[:, -1, :] * 288).type(torch.LongTensor)] # [B, N, D] - else: - T_i_D_emb = None - if self.if_D_i_W: - D_i_W_emb = self.D_i_W_emb[(d_i_w_data[:, -1, :]).type(torch.LongTensor)] # [B, N, D] - else: - D_i_W_emb = None - - # time series embedding - B, L, N, _ = X.shape - X = X.transpose(1, 2).contiguous() # B, N, L, 1 - X = X.view(B, N, -1).transpose(1, 2).unsqueeze(-1) # B, D, N, 1 - time_series_emb = self.time_series_emb_layer(X) # B, D, N, 1 - - node_emb = [] - if self.if_node: - # expand node embeddings - node_emb.append(self.node_emb.unsqueeze(0).expand(B, -1, -1).transpose(1, 2).unsqueeze(-1)) # B, D, N, 1 - # temporal embeddings - tem_emb = [] - if T_i_D_emb is not None: - tem_emb.append(T_i_D_emb.transpose(1, 2).unsqueeze(-1)) # B, D, N, 1 - if D_i_W_emb is not None: - tem_emb.append(D_i_W_emb.transpose(1, 2).unsqueeze(-1)) # B, D, N, 1 - - # concate all embeddings - hidden = torch.cat([time_series_emb] + node_emb + tem_emb, dim=1) - - # encoding - hidden = self.encoder(hidden) - - # regression - prediction = self.regression_layer(hidden) - - return prediction diff --git a/basicts/archs/STID_arch/__init__.py b/basicts/archs/STID_arch/__init__.py deleted file mode 100644 index e86e9c6e..00000000 --- a/basicts/archs/STID_arch/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from basicts.archs.STID_arch.STID_arch import STID \ No newline at end of file diff --git a/basicts/archs/STNorm_arch/__init__.py b/basicts/archs/STNorm_arch/__init__.py deleted file mode 100644 index 8bff3176..00000000 --- a/basicts/archs/STNorm_arch/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from basicts.archs.STNorm_arch.STNorm_arch import STNorm \ No newline at end of file diff --git a/basicts/archs/Stat_arch/Stat_arch.py b/basicts/archs/Stat_arch/Stat_arch.py deleted file mode 100644 index 2511942c..00000000 --- a/basicts/archs/Stat_arch/Stat_arch.py +++ /dev/null @@ -1,153 +0,0 @@ -""" - Statistical models including: MA (Moveing Average) AR (Auto Regression), VAR (Vector Auto Regression), and ARIMA (Autoregressive Integrated Moving Average (ARIMA). - All the random noise term is omitted. - Ref Code: https://github.com/doowb/sma -""" -import torch -import torch.nn as nn -import copy -from basicts.archs.registry import ARCH_REGISTRY - - -@ARCH_REGISTRY.register() -class SimpleMovingAverage(nn.Module): - def __init__(self, q: int, input_length: int, output_length: int): - """simple moving average as prediction - - Args: - q (int): sliding window size - input_length (int): length of input history data - output_length (int): length of prediction - """ - super(SimpleMovingAverage, self).__init__() - assert input_length >= q, "Error: window size > input data length" - self.q = q - self.output_length = output_length - self.input_length = input_length - - def forward(self, history_data: torch.Tensor, **kwargs) -> torch.Tensor: - """feed forward of MA: https://github.com/doowb/sma - forward([1, 2, 3, 4, 5, 6, 7, 8, 9]) | p=4; - //=> [ '2.50', '3.50', '4.50', '5.50', '6.50', '7.50' ] - //=> │ │ │ │ │ └─(6+7+8+9)/4 - //=> │ │ │ │ └─(5+6+7+8)/4 - //=> │ │ │ └─(4+5+6+7)/4 - //=> │ │ └─(3+4+5+6)/4 - //=> │ └─(2+3+4+5)/4 - //=> └─(1+2+3+4)/4 - - Args: - history_data (torch.Tensor): history data with shape [B, L, N, C] - - Returns: - torch.Tensor: MA prediction - """ - [B, L, N, C] = history_data.shape - assert L == self.input_length, "error input data length" - data_full = copy.copy(history_data) - for i in range(self.output_length): - data_in_window = data_full[:, -self.q:, :, :] - simple_avg = torch.mean(data_in_window, dim=1) # [B, N, C] - data_full = torch.cat([data_full, simple_avg.unsqueeze(1)], dim=1) - prediction = data_full[:, -self.output_length:, :, :] - return prediction - - -@ARCH_REGISTRY.register() -class AutoRegressive(nn.Module): - def __init__(self, p: int, input_length: int, output_length: int): - """Auto Regressive (AR) model - - Args: - p (int): sliding window size - input_length (int): length of input history data - output_length (int): length of prediction - """ - super(AutoRegressive, self).__init__() - assert input_length >= p, "Error: window size > input data length" - self.p = p - self.output_length = output_length - self.input_length = input_length - self.weight = nn.Parameter(torch.empty(p, 1)) - print("Notes: the weights of WMA model are unnormalized.") - self.c = nn.Parameter(torch.empty(1)) - nn.init.uniform_(self.weight) - nn.init.zeros_(self.c) - - def forward(self, history_data: torch.Tensor, **kwargs) -> torch.Tensor: - """feed forward of autoregressive model: https://en.wikipedia.org/wiki/Autoregressive_model - - Args: - history_data (torch.Tensor): history data with shape [B, L, N, C] - - Returns: - torch.Tensor: MA prediction - """ - [B, L, N, C] = history_data.shape - assert L == self.input_length, "error input data length" - data_full = copy.copy(history_data) - for i in range(self.output_length): - data_in_window = data_full[:, -self.p:, :, :] # [B, p, N, C] - data_in_window = data_in_window.permute(0, 2, 3, 1) # [B, N, C, p] - weight_avg = torch.matmul(data_in_window, self.weight).permute(0, 3, 1, 2) # [B, 1, N, C] - weight_avg = weight_avg + self.c # the noise term is omitted - data_full = torch.cat([data_full, weight_avg], dim=1) - prediction = data_full[:, -self.output_length:, :, :] - return prediction - - -@ARCH_REGISTRY.register() -class VectorAutoRegression(nn.Module): - def __init__(self, p: int, input_length: int, output_length: int, num_time_series: int): - """vector auto regressive model for multivariate time series forecasting - - Args: - p (int): sliding window size - input_length (int): length of input history data - output_length (int): length of prediction - num_time_series (int): number of time series - """ - super(VectorAutoRegression, self).__init__() - self.p = p - self.output_length = output_length - self.input_length = input_length - self.N = num_time_series - self.weight = nn.Parameter(torch.empty(p, self.N, self.N)) # [p, N, N] - print("Notes: the weights of VAR model are unnormalized.") - self.c = nn.Parameter(torch.empty(self.N, 1)) - nn.init.xavier_uniform_(self.weight) - nn.init.zeros_(self.c) - - def forward(self, history_data: torch.Tensor, **kwargs) -> torch.Tensor: - """feed forward of VAR: https://en.wikipedia.org/wiki/Vector_autoregression - - Args: - history_data (torch.Tensor): history data with shape [B, L, N, C] - - Returns: - torch.Tensor: VAR prediction - """ - [B, L, N, C] = history_data.shape - assert L == self.input_length, "error input data length" - data_full = copy.copy(history_data) - for i in range(self.output_length): - data_in_window = data_full[:, -self.p:, :, :] # [B, p, N, C] - data_in_window = data_in_window.permute(0, 3, 1, 2).unsqueeze(-1) # [B, C, p, N, 1] - weighted_data = torch.matmul(self.weight, data_in_window).squeeze(-1) # [B, C, p, N] - weight_avg = torch.mean(weighted_data, dim=-2).permute(0, 2, 1).unsqueeze(1) # [B, 1, N, C] - weight_avg = weight_avg + self.c # error term is omitted - data_full = torch.cat([data_full, weight_avg], dim=1) - prediction = data_full[:, -self.output_length:, :, :] - return prediction - - -@ARCH_REGISTRY.register() -class ARIMA(nn.Module): - def __init__(self): - super(ARIMA, self).__init__() - """TODO: ARIMA model requires unnormalized data to add N(0, 1) noise. - """ - pass - - def forward(self, history_data: torch.Tensor, **kwargs) -> torch.Tensor: - pass diff --git a/basicts/archs/Stat_arch/__init__.py b/basicts/archs/Stat_arch/__init__.py deleted file mode 100644 index 3c89f178..00000000 --- a/basicts/archs/Stat_arch/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from basicts.archs.Stat_arch.Stat_arch import * \ No newline at end of file diff --git a/basicts/archs/StemGNN_arch/__init__.py b/basicts/archs/StemGNN_arch/__init__.py deleted file mode 100644 index af90b08b..00000000 --- a/basicts/archs/StemGNN_arch/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from basicts.archs.StemGNN_arch.StemGNN_arch import StemGNN \ No newline at end of file diff --git a/basicts/archs/__init__.py b/basicts/archs/__init__.py index b306c9ed..d455f735 100644 --- a/basicts/archs/__init__.py +++ b/basicts/archs/__init__.py @@ -1,8 +1,18 @@ -import os +from .arch_zoo.stid_arch import STID +from .arch_zoo.gwnet_arch import GraphWaveNet +from .arch_zoo.dcrnn_arch import DCRNN +from .arch_zoo.d2stgnn_arch import D2STGNN +from .arch_zoo.stgcn_arch import STGCN +from .arch_zoo.mtgnn_arch import MTGNN +from .arch_zoo.stnorm_arch import STNorm +from .arch_zoo.agcrn_arch import AGCRN +from .arch_zoo.stemgnn_arch import StemGNN +from .arch_zoo.gts_arch import GTS +from .arch_zoo.dgcrn_arch import DGCRN +from .arch_zoo.linear_arch import Linear, DLinear, NLinear -from .registry import ARCH_REGISTRY -from easytorch.utils.registry import scan_modules - -__all__ = ['ARCH_REGISTRY'] - -scan_modules(os.getcwd(), __file__, ['__init__.py', 'builder.py']) +__all__ = ["STID", "GraphWaveNet", "DCRNN", + "D2STGNN", "STGCN", "MTGNN", + "STNorm", "AGCRN", "StemGNN", + "GTS", "DGCRN", "Linear", + "DLinear", "NLinear"] diff --git a/basicts/archs/arch_zoo/agcrn_arch/__init__.py b/basicts/archs/arch_zoo/agcrn_arch/__init__.py new file mode 100644 index 00000000..01b4cc8d --- /dev/null +++ b/basicts/archs/arch_zoo/agcrn_arch/__init__.py @@ -0,0 +1,3 @@ +from .agcrn_arch import AGCRN + +__all__ = ["AGCRN"] diff --git a/basicts/archs/arch_zoo/agcrn_arch/agcn.py b/basicts/archs/arch_zoo/agcrn_arch/agcn.py new file mode 100644 index 00000000..1cea52ee --- /dev/null +++ b/basicts/archs/arch_zoo/agcrn_arch/agcn.py @@ -0,0 +1,35 @@ +import torch +import torch.nn.functional as F +import torch.nn as nn + + +class AVWGCN(nn.Module): + def __init__(self, dim_in, dim_out, cheb_k, embed_dim): + super(AVWGCN, self).__init__() + self.cheb_k = cheb_k + self.weights_pool = nn.Parameter( + torch.FloatTensor(embed_dim, cheb_k, dim_in, dim_out)) + self.bias_pool = nn.Parameter(torch.FloatTensor(embed_dim, dim_out)) + + def forward(self, x, node_embeddings): + # x shaped[B, N, C], node_embeddings shaped [N, D] -> supports shaped [N, N] + # output shape [B, N, C] + node_num = node_embeddings.shape[0] + supports = F.softmax( + F.relu(torch.mm(node_embeddings, node_embeddings.transpose(0, 1))), dim=1) + support_set = [torch.eye(node_num).to(supports.device), supports] + # default cheb_k = 3 + for k in range(2, self.cheb_k): + support_set.append(torch.matmul( + 2 * supports, support_set[-1]) - support_set[-2]) + supports = torch.stack(support_set, dim=0) + # N, cheb_k, dim_in, dim_out + weights = torch.einsum( + 'nd,dkio->nkio', node_embeddings, self.weights_pool) + bias = torch.matmul(node_embeddings, self.bias_pool) # N, dim_out + x_g = torch.einsum("knm,bmc->bknc", supports, + x) # B, cheb_k, N, dim_in + x_g = x_g.permute(0, 2, 1, 3) # B, N, cheb_k, dim_in + x_gconv = torch.einsum('bnki,nkio->bno', x_g, + weights) + bias # b, N, dim_out + return x_gconv diff --git a/basicts/archs/arch_zoo/agcrn_arch/agcrn_arch.py b/basicts/archs/arch_zoo/agcrn_arch/agcrn_arch.py new file mode 100644 index 00000000..a64a1c8d --- /dev/null +++ b/basicts/archs/arch_zoo/agcrn_arch/agcrn_arch.py @@ -0,0 +1,108 @@ +import torch +import torch.nn as nn + +from .agcrn_cell import AGCRNCell + + +class AVWDCRNN(nn.Module): + def __init__(self, node_num, dim_in, dim_out, cheb_k, embed_dim, num_layers=1): + super(AVWDCRNN, self).__init__() + assert num_layers >= 1, 'At least one DCRNN layer in the Encoder.' + self.node_num = node_num + self.input_dim = dim_in + self.num_layers = num_layers + self.dcrnn_cells = nn.ModuleList() + self.dcrnn_cells.append( + AGCRNCell(node_num, dim_in, dim_out, cheb_k, embed_dim)) + for _ in range(1, num_layers): + self.dcrnn_cells.append( + AGCRNCell(node_num, dim_out, dim_out, cheb_k, embed_dim)) + + def forward(self, x, init_state, node_embeddings): + # shape of x: (B, T, N, D) + # shape of init_state: (num_layers, B, N, hidden_dim) + assert x.shape[2] == self.node_num and x.shape[3] == self.input_dim + seq_length = x.shape[1] + current_inputs = x + output_hidden = [] + for i in range(self.num_layers): + state = init_state[i] + inner_states = [] + for t in range(seq_length): + state = self.dcrnn_cells[i]( + current_inputs[:, t, :, :], state, node_embeddings) + inner_states.append(state) + output_hidden.append(state) + current_inputs = torch.stack(inner_states, dim=1) + # current_inputs: the outputs of last layer: (B, T, N, hidden_dim) + # output_hidden: the last state for each layer: (num_layers, B, N, hidden_dim) + #last_state: (B, N, hidden_dim) + return current_inputs, output_hidden + + def init_hidden(self, batch_size): + init_states = [] + for i in range(self.num_layers): + init_states.append( + self.dcrnn_cells[i].init_hidden_state(batch_size)) + # (num_layers, B, N, hidden_dim) + return torch.stack(init_states, dim=0) + + +class AGCRN(nn.Module): + """ + Paper: Adaptive Graph Convolutional Recurrent Network for Traffic Forecasting + Official Code: https://github.com/LeiBAI/AGCRN + Link: https://arxiv.org/abs/2007.02842 + """ + + def __init__(self, num_nodes, input_dim, rnn_units, output_dim, horizon, num_layers, default_graph, embed_dim, cheb_k): + super(AGCRN, self).__init__() + self.num_node = num_nodes + self.input_dim = input_dim + self.hidden_dim = rnn_units + self.output_dim = output_dim + self.horizon = horizon + self.num_layers = num_layers + + self.default_graph = default_graph + self.node_embeddings = nn.Parameter(torch.randn( + self.num_node, embed_dim), requires_grad=True) + + self.encoder = AVWDCRNN(num_nodes, input_dim, rnn_units, cheb_k, + embed_dim, num_layers) + + # predictor + self.end_conv = nn.Conv2d( + 1, horizon * self.output_dim, kernel_size=(1, self.hidden_dim), bias=True) + + self.init_param() + + def init_param(self): + for p in self.parameters(): + if p.dim() > 1: + nn.init.xavier_uniform_(p) + else: + nn.init.uniform_(p) + + def forward(self, history_data: torch.Tensor, future_data: torch.Tensor, batch_seen: int, epoch: int, train: bool, **kwargs) -> torch.Tensor: + """Feedforward function of AGCRN. + + Args: + history_data (torch.Tensor): inputs with shape [B, L, N, C]. + + Returns: + torch.Tensor: outputs with shape [B, L, N, C] + """ + + init_state = self.encoder.init_hidden(history_data.shape[0]) + output, _ = self.encoder( + history_data, init_state, self.node_embeddings) # B, T, N, hidden + output = output[:, -1:, :, :] # B, 1, N, hidden + + # CNN based predictor + output = self.end_conv((output)) # B, T*C, N, 1 + output = output.squeeze(-1).reshape(-1, self.horizon, + self.output_dim, self.num_node) + output = output.permute(0, 1, 3, 2) # B, T, N, C + + return output diff --git a/basicts/archs/AGCRN_arch/AGCRNCell.py b/basicts/archs/arch_zoo/agcrn_arch/agcrn_cell.py similarity index 68% rename from basicts/archs/AGCRN_arch/AGCRNCell.py rename to basicts/archs/arch_zoo/agcrn_arch/agcrn_cell.py index b751a3b9..d11c6d59 100644 --- a/basicts/archs/AGCRN_arch/AGCRNCell.py +++ b/basicts/archs/arch_zoo/agcrn_arch/agcrn_cell.py @@ -1,18 +1,22 @@ import torch import torch.nn as nn -from basicts.archs.AGCRN_arch.AGCN import AVWGCN + +from .agcn import AVWGCN + class AGCRNCell(nn.Module): def __init__(self, node_num, dim_in, dim_out, cheb_k, embed_dim): super(AGCRNCell, self).__init__() self.node_num = node_num self.hidden_dim = dim_out - self.gate = AVWGCN(dim_in+self.hidden_dim, 2*dim_out, cheb_k, embed_dim) - self.update = AVWGCN(dim_in+self.hidden_dim, dim_out, cheb_k, embed_dim) + self.gate = AVWGCN(dim_in+self.hidden_dim, 2 * + dim_out, cheb_k, embed_dim) + self.update = AVWGCN(dim_in+self.hidden_dim, + dim_out, cheb_k, embed_dim) def forward(self, x, state, node_embeddings): - #x: B, num_nodes, input_dim - #state: B, num_nodes, hidden_dim + # x: B, num_nodes, input_dim + # state: B, num_nodes, hidden_dim state = state.to(x.device) input_and_state = torch.cat((x, state), dim=-1) z_r = torch.sigmoid(self.gate(input_and_state, node_embeddings)) @@ -23,4 +27,4 @@ def forward(self, x, state, node_embeddings): return h def init_hidden_state(self, batch_size): - return torch.zeros(batch_size, self.node_num, self.hidden_dim) \ No newline at end of file + return torch.zeros(batch_size, self.node_num, self.hidden_dim) diff --git a/basicts/archs/arch_zoo/d2stgnn_arch/__init__.py b/basicts/archs/arch_zoo/d2stgnn_arch/__init__.py new file mode 100644 index 00000000..50fbf7f3 --- /dev/null +++ b/basicts/archs/arch_zoo/d2stgnn_arch/__init__.py @@ -0,0 +1,3 @@ +from .d2stgnn_arch import D2STGNN + +__all__ = ["D2STGNN"] diff --git a/basicts/archs/D2STGNN_arch/D2STGNN_arch.py b/basicts/archs/arch_zoo/d2stgnn_arch/d2stgnn_arch.py similarity index 53% rename from basicts/archs/D2STGNN_arch/D2STGNN_arch.py rename to basicts/archs/arch_zoo/d2stgnn_arch/d2stgnn_arch.py index 593f258a..91a4633e 100644 --- a/basicts/archs/D2STGNN_arch/D2STGNN_arch.py +++ b/basicts/archs/arch_zoo/d2stgnn_arch/d2stgnn_arch.py @@ -1,24 +1,21 @@ import torch import torch.nn as nn import torch.nn.functional as F -""" - Paper: Decoupled Dynamic Spatial-Temporal Graph Neural Network for Traffic Forecasting - Official Code: https://github.com/zezhishao/D2STGNN -""" -from basicts.archs.D2STGNN_arch.DiffusionBlock import DifBlock -from basicts.archs.D2STGNN_arch.InherentBlock import InhBlock -from basicts.archs.D2STGNN_arch.DynamicGraphConv.DyGraphCons import DynamicGraphConstructor -from basicts.archs.D2STGNN_arch.Decouple.estimation_gate import EstimationGate -from basicts.archs.registry import ARCH_REGISTRY +from .difusion_block import DifBlock +from .inherent_block import InhBlock +from .dynamic_graph_conv.dy_graph_conv import DynamicGraphConstructor +from .decouple.estimation_gate import EstimationGate class DecoupleLayer(nn.Module): def __init__(self, hidden_dim, fk_dim=256, first=False, **model_args): super().__init__() - self.spatial_gate = EstimationGate(model_args['node_hidden'], model_args['time_emb_dim'], 64, model_args['seq_length']) - self.dif_layer = DifBlock(hidden_dim, fk_dim=fk_dim, **model_args) - self.inh_layer = InhBlock(hidden_dim, fk_dim=fk_dim, first=first, **model_args) + self.spatial_gate = EstimationGate( + model_args['node_hidden'], model_args['time_emb_dim'], 64, model_args['seq_length']) + self.dif_layer = DifBlock(hidden_dim, fk_dim=fk_dim, **model_args) + self.inh_layer = InhBlock( + hidden_dim, fk_dim=fk_dim, first=first, **model_args) def forward(self, X: torch.Tensor, dynamic_graph: torch.Tensor, static_graph, E_u, E_d, T_D, D_W): """decouple layer @@ -37,57 +34,71 @@ def forward(self, X: torch.Tensor, dynamic_graph: torch.Tensor, static_graph, E_ torch.Tensor: the output of the forecast branch of Diffusion Block with shape (B, L'', N, D), where L''=output_seq_len / model_args['gap'] to avoid error accumulation in auto-regression. torch.Tensor: the output of the forecast branch of Inherent Block with shape (B, L'', N, D), where L''=output_seq_len / model_args['gap'] to avoid error accumulation in auto-regression. """ - X_spa = self.spatial_gate(E_u, E_d, T_D, D_W, X) - dif_backcast_seq_res, dif_forecast_hidden = self.dif_layer(X=X, X_spa=X_spa, dynamic_graph=dynamic_graph, static_graph=static_graph) - inh_backcast_seq_res, inh_forecast_hidden = self.inh_layer(dif_backcast_seq_res) + X_spa = self.spatial_gate(E_u, E_d, T_D, D_W, X) + dif_backcast_seq_res, dif_forecast_hidden = self.dif_layer( + X=X, X_spa=X_spa, dynamic_graph=dynamic_graph, static_graph=static_graph) + inh_backcast_seq_res, inh_forecast_hidden = self.inh_layer( + dif_backcast_seq_res) return inh_backcast_seq_res, dif_forecast_hidden, inh_forecast_hidden -@ARCH_REGISTRY.register() + class D2STGNN(nn.Module): + """ + Paper: Decoupled Dynamic Spatial-Temporal Graph Neural Network for Traffic Forecasting + Link: https://arxiv.org/abs/2206.09112 + Official Code: https://github.com/zezhishao/D2STGNN + """ def __init__(self, **model_args): super().__init__() # attributes - self._in_feat = model_args['num_feat'] - self._hidden_dim = model_args['num_hidden'] - self._node_dim = model_args['node_hidden'] - self._forecast_dim = 256 + self._in_feat = model_args['num_feat'] + self._hidden_dim = model_args['num_hidden'] + self._node_dim = model_args['node_hidden'] + self._forecast_dim = 256 self._output_hidden = 512 - self._output_dim = model_args['seq_length'] + self._output_dim = model_args['seq_length'] - self._num_nodes = model_args['num_nodes'] - self._k_s = model_args['k_s'] - self._k_t = model_args['k_t'] - self._num_layers = 5 + self._num_nodes = model_args['num_nodes'] + self._k_s = model_args['k_s'] + self._k_t = model_args['k_t'] + self._num_layers = 5 - model_args['use_pre'] = False - model_args['dy_graph'] = True + model_args['use_pre'] = False + model_args['dy_graph'] = True model_args['sta_graph'] = True - self._model_args = model_args + self._model_args = model_args # start embedding layer - self.embedding = nn.Linear(self._in_feat, self._hidden_dim) + self.embedding = nn.Linear(self._in_feat, self._hidden_dim) # time embedding - self.T_i_D_emb = nn.Parameter(torch.empty(288, model_args['time_emb_dim'])) - self.D_i_W_emb = nn.Parameter(torch.empty(7, model_args['time_emb_dim'])) + self.T_i_D_emb = nn.Parameter( + torch.empty(288, model_args['time_emb_dim'])) + self.D_i_W_emb = nn.Parameter( + torch.empty(7, model_args['time_emb_dim'])) # Decoupled Spatial Temporal Layer - self.layers = nn.ModuleList([DecoupleLayer(self._hidden_dim, fk_dim=self._forecast_dim, first=True, **model_args)]) + self.layers = nn.ModuleList([DecoupleLayer( + self._hidden_dim, fk_dim=self._forecast_dim, first=True, **model_args)]) for _ in range(self._num_layers - 1): - self.layers.append(DecoupleLayer(self._hidden_dim, fk_dim=self._forecast_dim, **model_args)) + self.layers.append(DecoupleLayer( + self._hidden_dim, fk_dim=self._forecast_dim, **model_args)) # dynamic and static hidden graph constructor if model_args['dy_graph']: - self.dynamic_graph_constructor = DynamicGraphConstructor(**model_args) - + self.dynamic_graph_constructor = DynamicGraphConstructor( + **model_args) + # node embeddings - self.node_emb_u = nn.Parameter(torch.empty(self._num_nodes, self._node_dim)) - self.node_emb_d = nn.Parameter(torch.empty(self._num_nodes, self._node_dim)) + self.node_emb_u = nn.Parameter( + torch.empty(self._num_nodes, self._node_dim)) + self.node_emb_d = nn.Parameter( + torch.empty(self._num_nodes, self._node_dim)) # output layer - self.out_fc_1 = nn.Linear(self._forecast_dim, self._output_hidden) - self.out_fc_2 = nn.Linear(self._output_hidden, model_args['gap']) + self.out_fc_1 = nn.Linear(self._forecast_dim, self._output_hidden) + self.out_fc_2 = nn.Linear(self._output_hidden, model_args['gap']) self.reset_parameter() @@ -105,59 +116,68 @@ def _graph_constructor(self, **inputs): else: static_graph = [] if self._model_args['dy_graph']: - dynamic_graph = self.dynamic_graph_constructor(**inputs) + dynamic_graph = self.dynamic_graph_constructor(**inputs) else: - dynamic_graph = [] + dynamic_graph = [] return static_graph, dynamic_graph def _prepare_inputs(self, X): - num_feat = self._model_args['num_feat'] + num_feat = self._model_args['num_feat'] # node embeddings - node_emb_u = self.node_emb_u # [N, d] - node_emb_d = self.node_emb_d # [N, d] + node_emb_u = self.node_emb_u # [N, d] + node_emb_d = self.node_emb_d # [N, d] # time slot embedding - T_i_D = self.T_i_D_emb[(X[:, :, :, num_feat] * 288).type(torch.LongTensor)] # [B, L, N, d] - D_i_W = self.D_i_W_emb[(X[:, :, :, num_feat+1]).type(torch.LongTensor)] # [B, L, N, d] + # [B, L, N, d] + T_i_D = self.T_i_D_emb[(X[:, :, :, num_feat] * + 288).type(torch.LongTensor)] + # [B, L, N, d] + D_i_W = self.D_i_W_emb[(X[:, :, :, num_feat+1]).type(torch.LongTensor)] # traffic signals X = X[:, :, :, :num_feat] return X, node_emb_u, node_emb_d, T_i_D, D_i_W - def forward(self, history_data, **kwargs): - r""" + def forward(self, history_data: torch.Tensor, future_data: torch.Tensor, batch_seen: int, epoch: int, train: bool, **kwargs) -> torch.Tensor: + """ Args: - X (Tensor): Input data with shape: [B, L, N, C] + history_data (Tensor): Input data with shape: [B, L, N, C] + Returns: - + torch.Tensor: outputs with shape [B, L, N, C] """ + X = history_data # ==================== Prepare Input Data ==================== # - X, E_u, E_d, T_D, D_W = self._prepare_inputs(X) + X, E_u, E_d, T_D, D_W = self._prepare_inputs(X) # ========================= Construct Graphs ========================== # - static_graph, dynamic_graph = self._graph_constructor(E_u=E_u, E_d=E_d, X=X, T_D=T_D, D_W=D_W) + static_graph, dynamic_graph = self._graph_constructor( + E_u=E_u, E_d=E_d, X=X, T_D=T_D, D_W=D_W) # Start embedding layer - X = self.embedding(X) + X = self.embedding(X) spa_forecast_hidden_list = [] tem_forecast_hidden_list = [] tem_backcast_seq_res = X for index, layer in enumerate(self.layers): - tem_backcast_seq_res, spa_forecast_hidden, tem_forecast_hidden = layer(tem_backcast_seq_res, dynamic_graph, static_graph, E_u, E_d, T_D, D_W) + tem_backcast_seq_res, spa_forecast_hidden, tem_forecast_hidden = layer( + tem_backcast_seq_res, dynamic_graph, static_graph, E_u, E_d, T_D, D_W) spa_forecast_hidden_list.append(spa_forecast_hidden) tem_forecast_hidden_list.append(tem_forecast_hidden) # Output Layer spa_forecast_hidden = sum(spa_forecast_hidden_list) tem_forecast_hidden = sum(tem_forecast_hidden_list) - forecast_hidden = spa_forecast_hidden + tem_forecast_hidden - + forecast_hidden = spa_forecast_hidden + tem_forecast_hidden + # regression layer - forecast = self.out_fc_2(F.relu(self.out_fc_1(F.relu(forecast_hidden)))) - forecast = forecast.transpose(1,2).contiguous().view(forecast.shape[0], forecast.shape[2], -1) + forecast = self.out_fc_2( + F.relu(self.out_fc_1(F.relu(forecast_hidden)))) + forecast = forecast.transpose(1, 2).contiguous().view( + forecast.shape[0], forecast.shape[2], -1) # reshape forecast = forecast.transpose(1, 2).unsqueeze(-1) diff --git a/basicts/archs/D2STGNN_arch/Decouple/estimation_gate.py b/basicts/archs/arch_zoo/d2stgnn_arch/decouple/estimation_gate.py similarity index 100% rename from basicts/archs/D2STGNN_arch/Decouple/estimation_gate.py rename to basicts/archs/arch_zoo/d2stgnn_arch/decouple/estimation_gate.py diff --git a/basicts/archs/D2STGNN_arch/Decouple/residual_decomp.py b/basicts/archs/arch_zoo/d2stgnn_arch/decouple/residual_decomp.py similarity index 100% rename from basicts/archs/D2STGNN_arch/Decouple/residual_decomp.py rename to basicts/archs/arch_zoo/d2stgnn_arch/decouple/residual_decomp.py diff --git a/basicts/archs/arch_zoo/d2stgnn_arch/difusion_block/__init__.py b/basicts/archs/arch_zoo/d2stgnn_arch/difusion_block/__init__.py new file mode 100644 index 00000000..94000120 --- /dev/null +++ b/basicts/archs/arch_zoo/d2stgnn_arch/difusion_block/__init__.py @@ -0,0 +1 @@ +from ..difusion_block.dif_block import DifBlock diff --git a/basicts/archs/arch_zoo/d2stgnn_arch/difusion_block/dif_block.py b/basicts/archs/arch_zoo/d2stgnn_arch/difusion_block/dif_block.py new file mode 100644 index 00000000..c360f3c6 --- /dev/null +++ b/basicts/archs/arch_zoo/d2stgnn_arch/difusion_block/dif_block.py @@ -0,0 +1,34 @@ +import torch.nn as nn + +from ..decouple.residual_decomp import ResidualDecomp +from .forecast import Forecast +from .dif_model import STLocalizedConv + + +class DifBlock(nn.Module): + def __init__(self, hidden_dim, fk_dim=256, use_pre=None, dy_graph=None, sta_graph=None, **model_args): + super().__init__() + self.pre_defined_graph = model_args['adjs'] + self.localized_st_conv = STLocalizedConv(hidden_dim, pre_defined_graph=self.pre_defined_graph, \ + use_pre=use_pre, dy_graph=dy_graph, sta_graph=sta_graph, **model_args) + # sub and norm + self.residual_decompose = ResidualDecomp([-1, -1, -1, hidden_dim]) + # forecast + self.forecast_branch = Forecast( + hidden_dim, fk_dim=fk_dim, **model_args) + # backcast + self.backcast_branch = nn.Linear(hidden_dim, hidden_dim) + + def forward(self, X, X_spa, dynamic_graph, static_graph): + Z = self.localized_st_conv(X_spa, dynamic_graph, static_graph) + # forecast branch + forecast_hidden = self.forecast_branch( + X_spa, Z, self.localized_st_conv, dynamic_graph, static_graph) + # backcast branch + backcast_seq = self.backcast_branch(Z) + # Residual Decomposition + backcast_seq = backcast_seq + X = X[:, -backcast_seq.shape[1]:, :, :] + backcast_seq_res = self.residual_decompose(X, backcast_seq) + + return backcast_seq_res, forecast_hidden diff --git a/basicts/archs/arch_zoo/d2stgnn_arch/difusion_block/dif_model.py b/basicts/archs/arch_zoo/d2stgnn_arch/difusion_block/dif_model.py new file mode 100644 index 00000000..dff8b707 --- /dev/null +++ b/basicts/archs/arch_zoo/d2stgnn_arch/difusion_block/dif_model.py @@ -0,0 +1,103 @@ +import torch +import torch.nn as nn + + +class STLocalizedConv(nn.Module): + def __init__(self, hidden_dim, pre_defined_graph=None, use_pre=None, dy_graph=None, sta_graph=None, **model_args): + super().__init__() + # gated temporal conv + self.k_s = model_args['k_s'] + self.k_t = model_args['k_t'] + self.hidden_dim = hidden_dim + + # graph conv + self.pre_defined_graph = pre_defined_graph + self.use_predefined_graph = use_pre + self.use_dynamic_hidden_graph = dy_graph + self.use_static__hidden_graph = sta_graph + + self.support_len = len(self.pre_defined_graph) + \ + int(dy_graph) + int(sta_graph) + self.num_matric = (int(use_pre) * len(self.pre_defined_graph) + len( + self.pre_defined_graph) * int(dy_graph) + int(sta_graph)) * self.k_s + 1 + self.dropout = nn.Dropout(model_args['dropout']) + self.pre_defined_graph = self.get_graph(self.pre_defined_graph) + + self.fc_list_updt = nn.Linear( + self.k_t * hidden_dim, self.k_t * hidden_dim, bias=False) + self.gcn_updt = nn.Linear( + self.hidden_dim*self.num_matric, self.hidden_dim) + + # others + self.bn = nn.BatchNorm2d(self.hidden_dim) + self.activation = nn.ReLU() + + def gconv(self, support, X_k, X_0): + out = [X_0] + for graph in support: + if len(graph.shape) == 2: # staitic or predefined graph + pass + else: + graph = graph.unsqueeze(1) + H_k = torch.matmul(graph, X_k) + out.append(H_k) + out = torch.cat(out, dim=-1) + out = self.gcn_updt(out) + out = self.dropout(out) + return out + + def get_graph(self, support): + # Only used in static including static hidden graph and predefined graph, but not used for dynamic graph. + graph_ordered = [] + mask = 1 - torch.eye(support[0].shape[0]).to(support[0].device) + for graph in support: + k_1_order = graph # 1 order + graph_ordered.append(k_1_order * mask) + # e.g., order = 3, k=[2, 3]; order = 2, k=[2] + for k in range(2, self.k_s+1): + k_1_order = torch.matmul(graph, k_1_order) + graph_ordered.append(k_1_order * mask) + # get st localed graph + st_local_graph = [] + for graph in graph_ordered: + graph = graph.unsqueeze(-2).expand(-1, self.k_t, -1) + graph = graph.reshape( + graph.shape[0], graph.shape[1] * graph.shape[2]) + # [num_nodes, kernel_size x num_nodes] + st_local_graph.append(graph) + # [order, num_nodes, kernel_size x num_nodes] + return st_local_graph + + def forward(self, X, dynamic_graph, static_graph): + # X: [bs, seq, nodes, feat] + # [bs, seq, num_nodes, ks, num_feat] + X = X.unfold(1, self.k_t, 1).permute(0, 1, 2, 4, 3) + # seq_len is changing + batch_size, seq_len, num_nodes, kernel_size, num_feat = X.shape + + # support + support = [] + # predefined graph + if self.use_predefined_graph: + support = support + self.pre_defined_graph + # dynamic graph + if self.use_dynamic_hidden_graph: + # k_order is caled in dynamic_graph_constructor component + support = support + dynamic_graph + # predefined graphs and static hidden graphs + if self.use_static__hidden_graph: + support = support + self.get_graph(static_graph) + + # parallelize + X = X.reshape(batch_size, seq_len, num_nodes, kernel_size * num_feat) + # batch_size, seq_len, num_nodes, kernel_size * hidden_dim + out = self.fc_list_updt(X) + out = self.activation(out) + out = out.view(batch_size, seq_len, num_nodes, kernel_size, num_feat) + X_0 = torch.mean(out, dim=-2) + # batch_size, seq_len, kernel_size x num_nodes, hidden_dim + X_k = out.transpose(-3, -2).reshape(batch_size, + seq_len, kernel_size*num_nodes, num_feat) + # Nx3N 3NxD -> NxD: batch_size, seq_len, num_nodes, hidden_dim + hidden = self.gconv(support, X_k, X_0) + return hidden diff --git a/basicts/archs/D2STGNN_arch/DiffusionBlock/forecast.py b/basicts/archs/arch_zoo/d2stgnn_arch/difusion_block/forecast.py similarity index 74% rename from basicts/archs/D2STGNN_arch/DiffusionBlock/forecast.py rename to basicts/archs/arch_zoo/d2stgnn_arch/difusion_block/forecast.py index 05286356..fa05190f 100644 --- a/basicts/archs/D2STGNN_arch/DiffusionBlock/forecast.py +++ b/basicts/archs/arch_zoo/d2stgnn_arch/difusion_block/forecast.py @@ -1,17 +1,18 @@ import torch import torch.nn as nn + class Forecast(nn.Module): def __init__(self, hidden_dim, fk_dim=None, **model_args): super().__init__() self.k_t = model_args['k_t'] self.output_seq_len = model_args['seq_length'] - self.forecast_fc = nn.Linear(hidden_dim, fk_dim) - self.model_args = model_args + self.forecast_fc = nn.Linear(hidden_dim, fk_dim) + self.model_args = model_args def forward(self, X, H, st_l_conv, dynamic_graph, static_graph): [B, seq_len_remain, B, D] = H.shape - [B, seq_len_input , B, D] = X.shape + [B, seq_len_input, B, D] = X.shape predict = [] history = X @@ -20,10 +21,10 @@ def forward(self, X, H, st_l_conv, dynamic_graph, static_graph): _1 = predict[-self.k_t:] if len(_1) < self.k_t: sub = self.k_t - len(_1) - _2 = history[:, -sub:, :, :] - _1 = torch.cat([_2] + _1, dim=1) + _2 = history[:, -sub:, :, :] + _1 = torch.cat([_2] + _1, dim=1) else: - _1 = torch.cat(_1, dim=1) + _1 = torch.cat(_1, dim=1) predict.append(st_l_conv(_1, dynamic_graph, static_graph)) predict = torch.cat(predict, dim=1) predict = self.forecast_fc(predict) diff --git a/basicts/archs/D2STGNN_arch/DynamicGraphConv/DyGraphCons.py b/basicts/archs/arch_zoo/d2stgnn_arch/dynamic_graph_conv/dy_graph_conv.py similarity index 56% rename from basicts/archs/D2STGNN_arch/DynamicGraphConv/DyGraphCons.py rename to basicts/archs/arch_zoo/d2stgnn_arch/dynamic_graph_conv/dy_graph_conv.py index 675ae44d..fe7c585e 100644 --- a/basicts/archs/D2STGNN_arch/DynamicGraphConv/DyGraphCons.py +++ b/basicts/archs/arch_zoo/d2stgnn_arch/dynamic_graph_conv/dy_graph_conv.py @@ -1,5 +1,7 @@ import torch.nn as nn -from basicts.archs.D2STGNN_arch.DynamicGraphConv.Utils import * + +from .utils import * + class DynamicGraphConstructor(nn.Module): def __init__(self, **model_args): @@ -7,25 +9,29 @@ def __init__(self, **model_args): # model args self.k_s = model_args['k_s'] # spatial order self.k_t = model_args['k_t'] # temporal kernel size - self.hidden_dim = model_args['num_hidden'] # hidden dimension of - self.node_dim = model_args['node_hidden'] # trainable node embedding dimension + # hidden dimension of + self.hidden_dim = model_args['num_hidden'] + # trainable node embedding dimension + self.node_dim = model_args['node_hidden'] - self.distance_function = DistanceFunction(**model_args) - self.mask = Mask(**model_args) - self.normalizer = Normalizer() - self.multi_order = MultiOrder(order=self.k_s) + self.distance_function = DistanceFunction(**model_args) + self.mask = Mask(**model_args) + self.normalizer = Normalizer() + self.multi_order = MultiOrder(order=self.k_s) def st_localization(self, graph_ordered): st_local_graph = [] for modality_i in graph_ordered: for k_order_graph in modality_i: - k_order_graph = k_order_graph.unsqueeze(-2).expand(-1, -1, self.k_t, -1) - k_order_graph = k_order_graph.reshape(k_order_graph.shape[0], k_order_graph.shape[1], k_order_graph.shape[2] * k_order_graph.shape[3]) + k_order_graph = k_order_graph.unsqueeze( + -2).expand(-1, -1, self.k_t, -1) + k_order_graph = k_order_graph.reshape( + k_order_graph.shape[0], k_order_graph.shape[1], k_order_graph.shape[2] * k_order_graph.shape[3]) st_local_graph.append(k_order_graph) return st_local_graph def forward(self, **inputs): - X = inputs['X'] + X = inputs['X'] E_d = inputs['E_d'] E_u = inputs['E_u'] T_D = inputs['T_D'] @@ -37,8 +43,8 @@ def forward(self, **inputs): # normalization dist_mx = self.normalizer(dist_mx) # multi order - mul_mx = self.multi_order(dist_mx) + mul_mx = self.multi_order(dist_mx) # spatial temporal localization dynamic_graphs = self.st_localization(mul_mx) - + return dynamic_graphs diff --git a/basicts/archs/arch_zoo/d2stgnn_arch/dynamic_graph_conv/utils/__init__.py b/basicts/archs/arch_zoo/d2stgnn_arch/dynamic_graph_conv/utils/__init__.py new file mode 100644 index 00000000..f3c93494 --- /dev/null +++ b/basicts/archs/arch_zoo/d2stgnn_arch/dynamic_graph_conv/utils/__init__.py @@ -0,0 +1,3 @@ +from .mask import * +from .normalizer import * +from .distance import * diff --git a/basicts/archs/D2STGNN_arch/DynamicGraphConv/Utils/distance.py b/basicts/archs/arch_zoo/d2stgnn_arch/dynamic_graph_conv/utils/distance.py similarity index 100% rename from basicts/archs/D2STGNN_arch/DynamicGraphConv/Utils/distance.py rename to basicts/archs/arch_zoo/d2stgnn_arch/dynamic_graph_conv/utils/distance.py diff --git a/basicts/archs/D2STGNN_arch/DynamicGraphConv/Utils/mask.py b/basicts/archs/arch_zoo/d2stgnn_arch/dynamic_graph_conv/utils/mask.py similarity index 100% rename from basicts/archs/D2STGNN_arch/DynamicGraphConv/Utils/mask.py rename to basicts/archs/arch_zoo/d2stgnn_arch/dynamic_graph_conv/utils/mask.py diff --git a/basicts/archs/D2STGNN_arch/DynamicGraphConv/Utils/normalizer.py b/basicts/archs/arch_zoo/d2stgnn_arch/dynamic_graph_conv/utils/normalizer.py similarity index 100% rename from basicts/archs/D2STGNN_arch/DynamicGraphConv/Utils/normalizer.py rename to basicts/archs/arch_zoo/d2stgnn_arch/dynamic_graph_conv/utils/normalizer.py diff --git a/basicts/archs/arch_zoo/d2stgnn_arch/inherent_block/__init__.py b/basicts/archs/arch_zoo/d2stgnn_arch/inherent_block/__init__.py new file mode 100644 index 00000000..521c8c90 --- /dev/null +++ b/basicts/archs/arch_zoo/d2stgnn_arch/inherent_block/__init__.py @@ -0,0 +1 @@ +from .inh_block import InhBlock diff --git a/basicts/archs/D2STGNN_arch/InherentBlock/forecast.py b/basicts/archs/arch_zoo/d2stgnn_arch/inherent_block/forecast.py similarity index 65% rename from basicts/archs/D2STGNN_arch/InherentBlock/forecast.py rename to basicts/archs/arch_zoo/d2stgnn_arch/inherent_block/forecast.py index 4f7fcf1d..e9224a46 100644 --- a/basicts/archs/D2STGNN_arch/InherentBlock/forecast.py +++ b/basicts/archs/arch_zoo/d2stgnn_arch/inherent_block/forecast.py @@ -1,31 +1,32 @@ import torch import torch.nn as nn + class Forecast(nn.Module): def __init__(self, hidden_dim, fk_dim, **model_args): super().__init__() self.output_seq_len = model_args['seq_length'] - self.model_args = model_args + self.model_args = model_args - self.forecast_fc = nn.Linear(hidden_dim, fk_dim) + self.forecast_fc = nn.Linear(hidden_dim, fk_dim) def forward(self, X, RNN_H, Z, transformer_layer, rnn_layer, pe): - [B, L, N, D] = X.shape - [L, B_N, D] = RNN_H.shape - [L, B_N, D] = Z.shape + [B, L, N, D] = X.shape + [L, B_N, D] = RNN_H.shape + [L, B_N, D] = Z.shape predict = [Z[-1, :, :].unsqueeze(0)] for _ in range(int(self.output_seq_len / self.model_args['gap'])-1): # RNN - _gru = rnn_layer.gru_cell(predict[-1][0], RNN_H[-1]).unsqueeze(0) - RNN_H = torch.cat([RNN_H, _gru], dim=0) + _gru = rnn_layer.gru_cell(predict[-1][0], RNN_H[-1]).unsqueeze(0) + RNN_H = torch.cat([RNN_H, _gru], dim=0) # Positional Encoding if pe is not None: RNN_H = pe(RNN_H) # Transformer - _Z = transformer_layer(_gru, K=RNN_H, V=RNN_H) + _Z = transformer_layer(_gru, K=RNN_H, V=RNN_H) predict.append(_Z) - + predict = torch.cat(predict, dim=0) predict = predict.reshape(-1, B, N, D) predict = predict.transpose(0, 1) diff --git a/basicts/archs/arch_zoo/d2stgnn_arch/inherent_block/inh_block.py b/basicts/archs/arch_zoo/d2stgnn_arch/inherent_block/inh_block.py new file mode 100644 index 00000000..431e1e24 --- /dev/null +++ b/basicts/archs/arch_zoo/d2stgnn_arch/inherent_block/inh_block.py @@ -0,0 +1,72 @@ +import math +import torch +import torch.nn as nn + +from ..decouple.residual_decomp import ResidualDecomp +from .inh_model import RNNLayer, TransformerLayer +from .forecast import Forecast + + +class PositionalEncoding(nn.Module): + def __init__(self, d_model, dropout=None, max_len: int = 5000): + super().__init__() + self.dropout = nn.Dropout(p=dropout) + position = torch.arange(max_len).unsqueeze(1) + div_term = torch.exp(torch.arange(0, d_model, 2) + * (-math.log(10000.0) / d_model)) + pe = torch.zeros(max_len, 1, d_model) + pe[:, 0, 0::2] = torch.sin(position * div_term) + pe[:, 0, 1::2] = torch.cos(position * div_term) + self.register_buffer('pe', pe) + + def forward(self, X): + X = X + self.pe[:X.size(0)] + X = self.dropout(X) + return X + + +class InhBlock(nn.Module): + def __init__(self, hidden_dim, num_heads=4, bias=True, fk_dim=256, first=None, **model_args): + super().__init__() + self.num_feat = hidden_dim + self.hidden_dim = hidden_dim + + if first: + self.pos_encoder = PositionalEncoding( + hidden_dim, model_args['dropout']) + else: + self.pos_encoder = None + self.rnn_layer = RNNLayer(hidden_dim, model_args['dropout']) + self.transformer_layer = TransformerLayer( + hidden_dim, num_heads, model_args['dropout'], bias) + # forecast + self.forecast_block = Forecast(hidden_dim, fk_dim, **model_args) + # backcast + self.backcast_fc = nn.Linear(hidden_dim, hidden_dim) + # sub residual + self.sub_and_norm = ResidualDecomp([-1, -1, -1, hidden_dim]) + + def forward(self, X): + [batch_size, seq_len, num_nodes, num_feat] = X.shape + # Temporal Model + # RNN + RNN_H_raw = self.rnn_layer(X) + # Positional Encoding + if self.pos_encoder is not None: + RNN_H = self.pos_encoder(RNN_H_raw) + else: + RNN_H = RNN_H_raw + # MultiHead Self Attention + Z = self.transformer_layer(RNN_H, RNN_H, RNN_H) + + # forecast branch + forecast_hidden = self.forecast_block( + X, RNN_H_raw, Z, self.transformer_layer, self.rnn_layer, self.pos_encoder) + + # backcast branch + Z = Z.reshape(seq_len, batch_size, num_nodes, num_feat) + Z = Z.transpose(0, 1) + backcast_seq = self.backcast_fc(Z) + backcast_seq_res = self.sub_and_norm(X, backcast_seq) + + return backcast_seq_res, forecast_hidden diff --git a/basicts/archs/arch_zoo/d2stgnn_arch/inherent_block/inh_model.py b/basicts/archs/arch_zoo/d2stgnn_arch/inherent_block/inh_model.py new file mode 100644 index 00000000..ce8e2799 --- /dev/null +++ b/basicts/archs/arch_zoo/d2stgnn_arch/inherent_block/inh_model.py @@ -0,0 +1,37 @@ +import torch as th +import torch.nn as nn +from torch.nn import MultiheadAttention + + +class RNNLayer(nn.Module): + def __init__(self, hidden_dim, dropout=None): + super().__init__() + self.hidden_dim = hidden_dim + self.gru_cell = nn.GRUCell(hidden_dim, hidden_dim) + self.dropout = nn.Dropout(dropout) + + def forward(self, X): + [batch_size, seq_len, num_nodes, hidden_dim] = X.shape + X = X.transpose(1, 2).reshape( + batch_size * num_nodes, seq_len, hidden_dim) + hx = th.zeros_like(X[:, 0, :]) + output = [] + for _ in range(X.shape[1]): + hx = self.gru_cell(X[:, _, :], hx) + output.append(hx) + output = th.stack(output, dim=0) + output = self.dropout(output) + return output + + +class TransformerLayer(nn.Module): + def __init__(self, hidden_dim, num_heads=4, dropout=None, bias=True): + super().__init__() + self.multi_head_self_attention = MultiheadAttention( + hidden_dim, num_heads, dropout=dropout, bias=bias) + self.dropout = nn.Dropout(dropout) + + def forward(self, X, K, V): + Z = self.multi_head_self_attention(X, K, V)[0] + Z = self.dropout(Z) + return Z diff --git a/basicts/archs/arch_zoo/dcrnn_arch/__init__.py b/basicts/archs/arch_zoo/dcrnn_arch/__init__.py new file mode 100644 index 00000000..0307d9f8 --- /dev/null +++ b/basicts/archs/arch_zoo/dcrnn_arch/__init__.py @@ -0,0 +1,3 @@ +from .dcrnn_arch import DCRNN + +__all__ = ['DCRNN'] diff --git a/basicts/archs/DCRNN_arch/DCRNN_arch.py b/basicts/archs/arch_zoo/dcrnn_arch/dcrnn_arch.py similarity index 56% rename from basicts/archs/DCRNN_arch/DCRNN_arch.py rename to basicts/archs/arch_zoo/dcrnn_arch/dcrnn_arch.py index d1ec8029..759be734 100644 --- a/basicts/archs/DCRNN_arch/DCRNN_arch.py +++ b/basicts/archs/arch_zoo/dcrnn_arch/dcrnn_arch.py @@ -1,53 +1,41 @@ import torch +from torch import nn import numpy as np -import torch.nn as nn -from basicts.archs.DCRNN_arch.DCRNN_cell import DCGRUCell -from basicts.archs.registry import ARCH_REGISTRY -""" - Paper: Diffusion Convolutional Recurrent Neural Network: Data-Driven Traffic Forecasting - Ref Official Code: - https://github.com/chnsh/DCRNN_PyTorch/blob/pytorch_scratch/model/pytorch/dcrnn_cell.py, - https://github.com/chnsh/DCRNN_PyTorch/blob/pytorch_scratch/model/pytorch/dcrnn_model.py -""" +from .dcrnn_cell import DCGRUCell + def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) + class Seq2SeqAttrs: def __init__(self, adj_mx, **model_kwargs): self.adj_mx = adj_mx - self.max_diffusion_step = int(model_kwargs.get('max_diffusion_step', 2)) - self.cl_decay_steps = int(model_kwargs.get('cl_decay_steps', 1000)) - self.filter_type = model_kwargs.get('filter_type', 'laplacian') - self.num_nodes = int(model_kwargs.get('num_nodes', 1)) - self.num_rnn_layers = int(model_kwargs.get('num_rnn_layers', 1)) - self.rnn_units = int(model_kwargs.get('rnn_units')) + self.max_diffusion_step = int( + model_kwargs.get("max_diffusion_step", 2)) + self.cl_decay_steps = int(model_kwargs.get("cl_decay_steps", 1000)) + self.filter_type = model_kwargs.get("filter_type", "laplacian") + self.num_nodes = int(model_kwargs.get("num_nodes", 1)) + self.num_rnn_layers = int(model_kwargs.get("num_rnn_layers", 1)) + self.rnn_units = int(model_kwargs.get("rnn_units")) self.hidden_state_size = self.num_nodes * self.rnn_units + class EncoderModel(nn.Module, Seq2SeqAttrs): def __init__(self, adj_mx, **model_kwargs): nn.Module.__init__(self) Seq2SeqAttrs.__init__(self, adj_mx, **model_kwargs) - self.input_dim = int(model_kwargs.get('input_dim', 1)) - self.seq_len = int(model_kwargs.get('seq_len')) # for the encoder + self.input_dim = int(model_kwargs.get("input_dim", 1)) + self.seq_len = int(model_kwargs.get("seq_len")) # for the encoder self.dcgru_layers = nn.ModuleList( [DCGRUCell(self.rnn_units, adj_mx, self.max_diffusion_step, self.num_nodes) for _ in range(self.num_rnn_layers)]) - + def forward(self, inputs, hidden_state=None): - """ - Encoder forward pass. - - :param inputs: shape (batch_size, self.num_nodes * self.input_dim) - :param hidden_state: (num_layers, batch_size, self.hidden_state_size) - optional, zeros if not provided - :return: output: # shape (batch_size, self.hidden_state_size) - hidden_state # shape (num_layers, batch_size, self.hidden_state_size) - (lower indices mean lower layers) - """ batch_size, _ = inputs.size() if hidden_state is None: - hidden_state = torch.zeros((self.num_rnn_layers, batch_size, self.hidden_state_size)).to(inputs.device) + hidden_state = torch.zeros( + (self.num_rnn_layers, batch_size, self.hidden_state_size)).to(inputs.device) hidden_states = [] output = inputs for layer_num, dcgru_layer in enumerate(self.dcgru_layers): @@ -55,7 +43,8 @@ def forward(self, inputs, hidden_state=None): hidden_states.append(next_hidden_state) output = next_hidden_state - return output, torch.stack(hidden_states) # runs in O(num_layers) so not too slow + # runs in O(num_layers) so not too slow + return output, torch.stack(hidden_states) class DecoderModel(nn.Module, Seq2SeqAttrs): @@ -63,23 +52,13 @@ def __init__(self, adj_mx, **model_kwargs): # super().__init__(is_training, adj_mx, **model_kwargs) nn.Module.__init__(self) Seq2SeqAttrs.__init__(self, adj_mx, **model_kwargs) - self.output_dim = int(model_kwargs.get('output_dim', 1)) - self.horizon = int(model_kwargs.get('horizon', 1)) # for the decoder + self.output_dim = int(model_kwargs.get("output_dim", 1)) + self.horizon = int(model_kwargs.get("horizon", 1)) # for the decoder self.projection_layer = nn.Linear(self.rnn_units, self.output_dim) self.dcgru_layers = nn.ModuleList( [DCGRUCell(self.rnn_units, adj_mx, self.max_diffusion_step, self.num_nodes) for _ in range(self.num_rnn_layers)]) - + def forward(self, inputs, hidden_state=None): - """ - Decoder forward pass. - - :param inputs: shape (batch_size, self.num_nodes * self.output_dim) - :param hidden_state: (num_layers, batch_size, self.hidden_state_size) - optional, zeros if not provided - :return: output: # shape (batch_size, self.num_nodes * self.output_dim) - hidden_state # shape (num_layers, batch_size, self.hidden_state_size) - (lower indices mean lower layers) - """ hidden_states = [] output = inputs for layer_num, dcgru_layer in enumerate(self.dcgru_layers): @@ -93,49 +72,48 @@ def forward(self, inputs, hidden_state=None): return output, torch.stack(hidden_states) -@ARCH_REGISTRY.register() class DCRNN(nn.Module, Seq2SeqAttrs): + """ + Paper: Diffusion Convolutional Recurrent Neural Network: Data-Driven Traffic Forecasting + Link: https://arxiv.org/abs/1707.01926 + Codes are modified from the official repo: + https://github.com/chnsh/DCRNN_PyTorch/blob/pytorch_scratch/model/pytorch/dcrnn_cell.py, + https://github.com/chnsh/DCRNN_PyTorch/blob/pytorch_scratch/model/pytorch/dcrnn_model.py + """ + def __init__(self, adj_mx, **model_kwargs): super().__init__() Seq2SeqAttrs.__init__(self, adj_mx, **model_kwargs) self.encoder_model = EncoderModel(adj_mx, **model_kwargs) self.decoder_model = DecoderModel(adj_mx, **model_kwargs) - self.cl_decay_steps = int(model_kwargs.get('cl_decay_steps', 2000)) - self.use_curriculum_learning = bool(model_kwargs.get('use_curriculum_learning', False)) + self.cl_decay_steps = int(model_kwargs.get("cl_decay_steps", 2000)) + self.use_curriculum_learning = bool( + model_kwargs.get("use_curriculum_learning", False)) def _compute_sampling_threshold(self, batches_seen): return self.cl_decay_steps / ( - self.cl_decay_steps + np.exp(batches_seen / self.cl_decay_steps)) + self.cl_decay_steps + np.exp(batches_seen / self.cl_decay_steps)) def encoder(self, inputs): - """ - encoder forward pass on t time steps - :param inputs: shape (seq_len, batch_size, num_sensor * input_dim) - :return: encoder_hidden_state: (num_layers, batch_size, self.hidden_state_size) - """ encoder_hidden_state = None for t in range(self.encoder_model.seq_len): - _, encoder_hidden_state = self.encoder_model(inputs[t], encoder_hidden_state) + _, encoder_hidden_state = self.encoder_model( + inputs[t], encoder_hidden_state) return encoder_hidden_state def decoder(self, encoder_hidden_state, labels=None, batches_seen=None): - """ - Decoder forward pass - :param encoder_hidden_state: (num_layers, batch_size, self.hidden_state_size) - :param labels: (self.horizon, batch_size, self.num_nodes * self.output_dim) [optional, not exist for inference] - :param batches_seen: global step [optional, not exist for inference] - :return: output: (self.horizon, batch_size, self.num_nodes * self.output_dim) - """ batch_size = encoder_hidden_state.size(1) - go_symbol = torch.zeros((batch_size, self.num_nodes * self.decoder_model.output_dim)).to(encoder_hidden_state.device) + go_symbol = torch.zeros( + (batch_size, self.num_nodes * self.decoder_model.output_dim)).to(encoder_hidden_state.device) decoder_hidden_state = encoder_hidden_state decoder_input = go_symbol outputs = [] for t in range(self.decoder_model.horizon): - decoder_output, decoder_hidden_state = self.decoder_model(decoder_input, decoder_hidden_state) + decoder_output, decoder_hidden_state = self.decoder_model( + decoder_input, decoder_hidden_state) decoder_input = decoder_output outputs.append(decoder_output) if self.training and self.use_curriculum_learning: @@ -146,7 +124,7 @@ def decoder(self, encoder_hidden_state, labels=None, batches_seen=None): return outputs def forward(self, history_data: torch.Tensor, future_data: torch.Tensor = None, batch_seen: int = None, **kwargs) -> torch.Tensor: - """feedforward function of DCRNN. + """Feedforward function of DCRNN. Args: history_data (torch.Tensor): history data with shape [L, B, N*C] @@ -157,19 +135,32 @@ def forward(self, history_data: torch.Tensor, future_data: torch.Tensor = None, torch.Tensor: prediction wit shape [L, B, N*C_out] """ + # reshape data + batch_size, length, num_nodes, channels = history_data.shape + history_data = history_data.reshape(batch_size, length, num_nodes * channels) # [B, L, N*C] + history_data = history_data.transpose(0, 1) # [L, B, N*C] + + if future_data is not None: + batch_size, length, num_nodes, channels = future_data.shape + future_data = future_data.reshape(batch_size, length, num_nodes * channels) # [B, L, N*C] + future_data = future_data.transpose(0, 1) # [L, B, N*C] + + # DCRNN encoder_hidden_state = self.encoder(history_data) - outputs = self.decoder(encoder_hidden_state, future_data, batches_seen=batch_seen) # [L, B, N*C_out] + outputs = self.decoder(encoder_hidden_state, future_data, + batches_seen=batch_seen) # [L, B, N*C_out] # reshape to B, L, N, C L, B, _ = outputs.shape - outputs = outputs.transpose(0, 1) #[B, L, N*C_out] - outputs = outputs.view(B, L, self.num_nodes, self.decoder_model.output_dim) + outputs = outputs.transpose(0, 1) # [B, L, N*C_out] + outputs = outputs.view(B, L, self.num_nodes, + self.decoder_model.output_dim) if not self.training: - assert future_data == None, 'Future data should not be visible when validating/testing.' + assert future_data == None, "Future data should not be visible when validating/testing." else: pass - + if batch_seen == 0: print("Warning: decoder only takes the first dimension as groundtruth.") print("Parameter Number: ".format(count_parameters(self))) diff --git a/basicts/archs/DCRNN_arch/DCRNN_cell.py b/basicts/archs/arch_zoo/dcrnn_arch/dcrnn_cell.py similarity index 72% rename from basicts/archs/DCRNN_arch/DCRNN_cell.py rename to basicts/archs/arch_zoo/dcrnn_arch/dcrnn_cell.py index c52043f2..4453fb14 100644 --- a/basicts/archs/DCRNN_arch/DCRNN_cell.py +++ b/basicts/archs/arch_zoo/dcrnn_arch/dcrnn_cell.py @@ -1,15 +1,9 @@ import torch -""" - Paper: Diffusion Convolutional Recurrent Neural Network: Data-Driven Traffic Forecasting - Ref Official Code: - https://github.com/chnsh/DCRNN_PyTorch/blob/pytorch_scratch/model/pytorch/dcrnn_cell.py, - https://github.com/chnsh/DCRNN_PyTorch/blob/pytorch_scratch/model/pytorch/dcrnn_model.py - Watch out the input groundtruth of decoder, which may cause bugs when you try to extend this code. - In order to train the model on multi-GPU, we send the parameter to different gpus in the feedforward process, which would hurt the efficiency. -""" class LayerParams: + """Layer parameters.""" + def __init__(self, rnn_network: torch.nn.Module, layer_type: str): self._rnn_network = rnn_network self._params_dict = {} @@ -22,8 +16,8 @@ def get_weights(self, shape): torch.nn.init.xavier_normal_(nn_param) self._params_dict[shape] = nn_param self._rnn_network.register_parameter( - '{}_weight_{}'.format(self._type, str(shape)), - nn_param) + '{}_weight_{}'.format(self._type, str(shape)), + nn_param) return self._params_dict[shape] def get_biases(self, length, bias_start=0.0): @@ -31,23 +25,24 @@ def get_biases(self, length, bias_start=0.0): biases = torch.nn.Parameter(torch.empty(length)) torch.nn.init.constant_(biases, bias_start) self._biases_dict[length] = biases - self._rnn_network.register_parameter('{}_biases_{}'.format(self._type, str(length)), biases) + self._rnn_network.register_parameter( + '{}_biases_{}'.format(self._type, str(length)), biases) return self._biases_dict[length] -class DCGRUCell(torch.nn.Module): - def __init__(self, num_units, adj_mx, max_diffusion_step, num_nodes, nonlinearity='tanh', use_gc_for_ru=True): - """ - :param num_units: - :param adj_mx: - :param max_diffusion_step: - :param num_nodes: - :param nonlinearity: - :param filter_type: "laplacian", "random_walk", "dual_random_walk". - :param use_gc_for_ru: whether to use Graph convolution to calculate the reset and update gates. - """ +class DCGRUCell(torch.nn.Module): + """ + Paper: Diffusion Convolutional Recurrent Neural Network: Data-Driven Traffic Forecasting + Link: https://arxiv.org/abs/1707.01926 + Codes are modified from the official repo: + https://github.com/chnsh/DCRNN_PyTorch/blob/pytorch_scratch/model/pytorch/dcrnn_cell.py, + https://github.com/chnsh/DCRNN_PyTorch/blob/pytorch_scratch/model/pytorch/dcrnn_model.py + Watch out the input groundtruth of decoder, which may cause bugs when you try to extend this code. + In order to train the model on multi-GPU, we send the parameter to different gpus in the feedforward process, which might hurt the efficiency. + """ + def __init__(self, num_units, adj_mx, max_diffusion_step, num_nodes, nonlinearity='tanh', use_gc_for_ru=True): super().__init__() self._activation = torch.tanh if nonlinearity == 'tanh' else torch.relu # support other nonlinearities up here? @@ -56,20 +51,13 @@ def __init__(self, num_units, adj_mx, max_diffusion_step, num_nodes, nonlinearit self._max_diffusion_step = max_diffusion_step self._use_gc_for_ru = use_gc_for_ru # for support in supports: - # self._supports.append(self._build_sparse_matrix(support)) + # self._supports.append(self._build_sparse_matrix(support)) self._supports = adj_mx self._fc_params = LayerParams(self, 'fc') self._gconv_params = LayerParams(self, 'gconv') - - def forward(self, inputs, hx): - """Gated recurrent unit (GRU) with Graph Convolution. - :param inputs: (B, num_nodes * input_dim) - :param hx: (B, num_nodes * rnn_units) - :return - - Output: A `2-D` tensor with shape `(B, num_nodes * rnn_units)`. - """ + def forward(self, inputs, hx): output_size = 2 * self._num_units if self._use_gc_for_ru: fn = self._gconv @@ -77,7 +65,8 @@ def forward(self, inputs, hx): fn = self._fc value = torch.sigmoid(fn(inputs, hx, output_size, bias_start=1.0)) value = torch.reshape(value, (-1, self._num_nodes, output_size)) - r, u = torch.split(tensor=value, split_size_or_sections=self._num_units, dim=-1) + r, u = torch.split( + tensor=value, split_size_or_sections=self._num_units, dim=-1) r = torch.reshape(r, (-1, self._num_nodes * self._num_units)) u = torch.reshape(u, (-1, self._num_nodes * self._num_units)) @@ -92,19 +81,20 @@ def forward(self, inputs, hx): def _concat(x, x_): x_ = x_.unsqueeze(0) return torch.cat([x, x_], dim=0) - + def _fc(self, inputs, state, output_size, bias_start=0.0): batch_size = inputs.shape[0] inputs = torch.reshape(inputs, (batch_size * self._num_nodes, -1)) state = torch.reshape(state, (batch_size * self._num_nodes, -1)) inputs_and_state = torch.cat([inputs, state], dim=-1) input_size = inputs_and_state.shape[-1] - weights = self._fc_params.get_weights((input_size, output_size)).to(inputs_and_state.device) + weights = self._fc_params.get_weights( + (input_size, output_size)).to(inputs_and_state.device) value = torch.sigmoid(torch.matmul(inputs_and_state, weights)) biases = self._fc_params.get_biases(output_size, bias_start) value += biases.to(inputs_and_state.device) return value - + def _gconv(self, inputs, state, output_size, bias_start=0.0): # Reshape input and state to (batch_size, num_nodes, input_dim/state_dim) batch_size = inputs.shape[0] @@ -115,7 +105,8 @@ def _gconv(self, inputs, state, output_size, bias_start=0.0): x = inputs_and_state x0 = x.permute(1, 2, 0) # (num_nodes, total_arg_size, batch_size) - x0 = torch.reshape(x0, shape=[self._num_nodes, input_size * batch_size]) + x0 = torch.reshape( + x0, shape=[self._num_nodes, input_size * batch_size]) x = torch.unsqueeze(x0, 0) if self._max_diffusion_step == 0: @@ -130,14 +121,20 @@ def _gconv(self, inputs, state, output_size, bias_start=0.0): x = self._concat(x, x2) x1, x0 = x2, x1 - num_matrices = len(self._supports) * self._max_diffusion_step + 1 # Adds for x itself. - x = torch.reshape(x, shape=[num_matrices, self._num_nodes, input_size, batch_size]) + # Adds for x itself. + num_matrices = len(self._supports) * self._max_diffusion_step + 1 + x = torch.reshape( + x, shape=[num_matrices, self._num_nodes, input_size, batch_size]) x = x.permute(3, 1, 2, 0) # (batch_size, num_nodes, input_size, order) - x = torch.reshape(x, shape=[batch_size * self._num_nodes, input_size * num_matrices]) - weights = self._gconv_params.get_weights((input_size * num_matrices, output_size)).to(x.device) - x = torch.matmul(x, weights) # (batch_size * self._num_nodes, output_size) - - biases = self._gconv_params.get_biases(output_size, bias_start).to(x.device) + x = torch.reshape( + x, shape=[batch_size * self._num_nodes, input_size * num_matrices]) + weights = self._gconv_params.get_weights( + (input_size * num_matrices, output_size)).to(x.device) + # (batch_size * self._num_nodes, output_size) + x = torch.matmul(x, weights) + + biases = self._gconv_params.get_biases( + output_size, bias_start).to(x.device) x += biases # Reshape res back to 2D: (batch_size, num_node, state_dim) -> (batch_size, num_node * state_dim) return torch.reshape(x, [batch_size, self._num_nodes * output_size]) diff --git a/basicts/archs/arch_zoo/dgcrn_arch/__init__.py b/basicts/archs/arch_zoo/dgcrn_arch/__init__.py new file mode 100644 index 00000000..8303eb46 --- /dev/null +++ b/basicts/archs/arch_zoo/dgcrn_arch/__init__.py @@ -0,0 +1,3 @@ +from .dgcrn_arch import DGCRN + +__all__ = ["DGCRN"] diff --git a/basicts/archs/DGCRN_arch/DGCRN_arch.py b/basicts/archs/arch_zoo/dgcrn_arch/dgcrn_arch.py similarity index 60% rename from basicts/archs/DGCRN_arch/DGCRN_arch.py rename to basicts/archs/arch_zoo/dgcrn_arch/dgcrn_arch.py index 470ce7a2..34ae93f4 100644 --- a/basicts/archs/DGCRN_arch/DGCRN_arch.py +++ b/basicts/archs/arch_zoo/dgcrn_arch/dgcrn_arch.py @@ -1,14 +1,14 @@ -import torch.nn.functional as F +import sys + +import numpy as np import torch import torch.nn as nn +import torch.nn.functional as F from torch.autograd import Variable -import numpy as np -from basicts.archs.DGCRN_arch.DGCRN_layer import * -from basicts.archs.registry import ARCH_REGISTRY -import sys + +from .dgcrn_layer import * -@ARCH_REGISTRY.register() class DGCRN(nn.Module): def __init__(self, gcn_depth, num_nodes, predefined_A=None, dropout=0.3, subgraph_size=20, node_dim=40, middle_dim=2, seq_length=12, in_dim=2, list_weight=[0.05, 0.95, 0.95], tanhalpha=3, cl_decay_steps=4000, rnn_size=64, hyperGNN_dim=16): super(DGCRN, self).__init__() @@ -32,23 +32,32 @@ def __init__(self, gcn_depth, num_nodes, predefined_A=None, dropout=0.3, subgrap self.hidden_size = self.rnn_size - dims_hyper = [self.hidden_size + in_dim, hyperGNN_dim, middle_dim, node_dim] + dims_hyper = [self.hidden_size + in_dim, + hyperGNN_dim, middle_dim, node_dim] - self.GCN1_tg = gcn(dims_hyper, gcn_depth, dropout, *list_weight, 'hyper') + self.GCN1_tg = gcn(dims_hyper, gcn_depth, + dropout, *list_weight, 'hyper') - self.GCN2_tg = gcn(dims_hyper, gcn_depth, dropout, *list_weight, 'hyper') + self.GCN2_tg = gcn(dims_hyper, gcn_depth, + dropout, *list_weight, 'hyper') - self.GCN1_tg_de = gcn(dims_hyper, gcn_depth, dropout, *list_weight, 'hyper') + self.GCN1_tg_de = gcn(dims_hyper, gcn_depth, + dropout, *list_weight, 'hyper') - self.GCN2_tg_de = gcn(dims_hyper, gcn_depth, dropout, *list_weight, 'hyper') + self.GCN2_tg_de = gcn(dims_hyper, gcn_depth, + dropout, *list_weight, 'hyper') - self.GCN1_tg_1 = gcn(dims_hyper, gcn_depth, dropout, *list_weight, 'hyper') + self.GCN1_tg_1 = gcn(dims_hyper, gcn_depth, + dropout, *list_weight, 'hyper') - self.GCN2_tg_1 = gcn(dims_hyper, gcn_depth, dropout, *list_weight, 'hyper') + self.GCN2_tg_1 = gcn(dims_hyper, gcn_depth, + dropout, *list_weight, 'hyper') - self.GCN1_tg_de_1 = gcn(dims_hyper, gcn_depth, dropout, *list_weight, 'hyper') + self.GCN1_tg_de_1 = gcn(dims_hyper, gcn_depth, + dropout, *list_weight, 'hyper') - self.GCN2_tg_de_1 = gcn(dims_hyper, gcn_depth, dropout, *list_weight, 'hyper') + self.GCN2_tg_de_1 = gcn(dims_hyper, gcn_depth, + dropout, *list_weight, 'hyper') self.fc_final = nn.Linear(self.hidden_size, self.output_dim) @@ -86,20 +95,26 @@ def step(self, input, Hidden_State, Cell_State, predefined_A, type='encoder', i= nodevec1 = self.emb1(self.idx) nodevec2 = self.emb2(self.idx) - hyper_input = torch.cat((x, Hidden_State.view(-1, self.num_nodes, self.hidden_size)), 2) + hyper_input = torch.cat( + (x, Hidden_State.view(-1, self.num_nodes, self.hidden_size)), 2) if type == 'encoder': - filter1 = self.GCN1_tg(hyper_input, predefined_A[0]) + self.GCN1_tg_1( hyper_input, predefined_A[1]) - filter2 = self.GCN2_tg(hyper_input, predefined_A[0]) + self.GCN2_tg_1( hyper_input, predefined_A[1]) + filter1 = self.GCN1_tg( + hyper_input, predefined_A[0]) + self.GCN1_tg_1(hyper_input, predefined_A[1]) + filter2 = self.GCN2_tg( + hyper_input, predefined_A[0]) + self.GCN2_tg_1(hyper_input, predefined_A[1]) if type == 'decoder': - filter1 = self.GCN1_tg_de(hyper_input, predefined_A[0]) + self.GCN1_tg_de_1( hyper_input, predefined_A[1]) - filter2 = self.GCN2_tg_de(hyper_input, predefined_A[0]) + self.GCN2_tg_de_1( hyper_input, predefined_A[1]) + filter1 = self.GCN1_tg_de( + hyper_input, predefined_A[0]) + self.GCN1_tg_de_1(hyper_input, predefined_A[1]) + filter2 = self.GCN2_tg_de( + hyper_input, predefined_A[0]) + self.GCN2_tg_de_1(hyper_input, predefined_A[1]) nodevec1 = torch.tanh(self.alpha * torch.mul(nodevec1, filter1)) nodevec2 = torch.tanh(self.alpha * torch.mul(nodevec2, filter2)) - a = torch.matmul(nodevec1, nodevec2.transpose(2, 1)) - torch.matmul(nodevec2, nodevec1.transpose(2, 1)) + a = torch.matmul(nodevec1, nodevec2.transpose(2, 1)) - \ + torch.matmul(nodevec2, nodevec1.transpose(2, 1)) adj = F.relu(torch.tanh(self.alpha * a)) @@ -112,36 +127,43 @@ def step(self, input, Hidden_State, Cell_State, predefined_A, type='encoder', i= combined = torch.cat((x, Hidden_State), -1) if type == 'encoder': - z = torch.sigmoid(self.gz1(combined, adp) + self.gz2(combined, adpT)) - r = torch.sigmoid(self.gr1(combined, adp) + self.gr2(combined, adpT)) + z = torch.sigmoid(self.gz1(combined, adp) + + self.gz2(combined, adpT)) + r = torch.sigmoid(self.gr1(combined, adp) + + self.gr2(combined, adpT)) temp = torch.cat((x, torch.mul(r, Hidden_State)), -1) Cell_State = torch.tanh(self.gc1(temp, adp) + self.gc2(temp, adpT)) elif type == 'decoder': - z = torch.sigmoid(self.gz1_de(combined, adp) + self.gz2_de(combined, adpT)) - r = torch.sigmoid(self.gr1_de(combined, adp) + self.gr2_de(combined, adpT)) + z = torch.sigmoid(self.gz1_de(combined, adp) + + self.gz2_de(combined, adpT)) + r = torch.sigmoid(self.gr1_de(combined, adp) + + self.gr2_de(combined, adpT)) temp = torch.cat((x, torch.mul(r, Hidden_State)), -1) - Cell_State = torch.tanh(self.gc1_de(temp, adp) + self.gc2_de(temp, adpT)) + Cell_State = torch.tanh(self.gc1_de( + temp, adp) + self.gc2_de(temp, adpT)) - Hidden_State = torch.mul(z, Hidden_State) + torch.mul(1 - z, Cell_State) + Hidden_State = torch.mul(z, Hidden_State) + \ + torch.mul(1 - z, Cell_State) return Hidden_State.view(-1, self.hidden_size), Cell_State.view(-1, self.hidden_size) - def forward(self, history_data:torch.Tensor, future_data:torch.Tensor=None, batch_seen:int=None, task_level:int=12, **kwargs) -> torch.Tensor: - """feedforward function of DGCRN. + def forward(self, history_data: torch.Tensor, future_data: torch.Tensor, batch_seen: int, epoch: int, train: bool,**kwargs) -> torch.Tensor: + """Feedforward function of DGCRN. Args: history_data (torch.Tensor): historical data with shape [B, L, N, C]. future_data (torch.Tensor, optional): ground truth. Defaults to None. - batches_seen (int, optional): batch num. Defaults to None. + batch_seen (int, optional): batch num. Defaults to None. task_level (int, optional): curriculum learning level. Defaults to 12. Returns: torch.Tensor: prediction with shape [B, L, N, 1] """ + task_level = kwargs["task_level"] input = history_data.transpose(1, 3) - ycl = future_data.transpose(1, 3) + ycl = future_data.transpose(1, 3) self.idx = self.idx.to(input.device) @@ -149,20 +171,23 @@ def forward(self, history_data:torch.Tensor, future_data:torch.Tensor=None, batc x = input batch_size = x.size(0) - Hidden_State, Cell_State = self.initHidden(batch_size * self.num_nodes, self.hidden_size) + Hidden_State, Cell_State = self.initHidden( + batch_size * self.num_nodes, self.hidden_size) Hidden_State = Hidden_State.to(input.device) - Cell_State = Cell_State.to(input.device) + Cell_State = Cell_State.to(input.device) outputs = None for i in range(self.seq_length): - Hidden_State, Cell_State = self.step(x[..., i].squeeze(-1), Hidden_State, Cell_State, predefined_A, 'encoder', i) + Hidden_State, Cell_State = self.step( + x[..., i].squeeze(-1), Hidden_State, Cell_State, predefined_A, 'encoder', i) if outputs is None: outputs = Hidden_State.unsqueeze(1) else: outputs = torch.cat((outputs, Hidden_State.unsqueeze(1)), 1) - go_symbol = torch.zeros((batch_size, self.output_dim, self.num_nodes)).to(input.device) + go_symbol = torch.zeros( + (batch_size, self.output_dim, self.num_nodes)).to(input.device) timeofday = ycl[:, [1], :, :] decoder_input = go_symbol @@ -171,15 +196,18 @@ def forward(self, history_data:torch.Tensor, future_data:torch.Tensor=None, batc for i in range(task_level): try: - decoder_input = torch.cat([decoder_input, timeofday[..., i]], dim=1) + decoder_input = torch.cat( + [decoder_input, timeofday[..., i]], dim=1) except: print(decoder_input.shape, timeofday.shape) sys.exit(0) - Hidden_State, Cell_State = self.step(decoder_input, Hidden_State, Cell_State, predefined_A, 'decoder', None) + Hidden_State, Cell_State = self.step( + decoder_input, Hidden_State, Cell_State, predefined_A, 'decoder', None) decoder_output = self.fc_final(Hidden_State) - decoder_input = decoder_output.view(batch_size, self.num_nodes, self.output_dim).transpose(1, 2) + decoder_input = decoder_output.view( + batch_size, self.num_nodes, self.output_dim).transpose(1, 2) outputs_final.append(decoder_output) if self.training and self.use_curriculum_learning: c = np.random.uniform(0, 1) @@ -188,11 +216,13 @@ def forward(self, history_data:torch.Tensor, future_data:torch.Tensor=None, batc outputs_final = torch.stack(outputs_final, dim=1) - outputs_final = outputs_final.view(batch_size, self.num_nodes, task_level, self.output_dim).transpose(1, 2) + outputs_final = outputs_final.view( + batch_size, self.num_nodes, task_level, self.output_dim).transpose(1, 2) - ramdom_predict= torch.zeros(batch_size, self.seq_length - task_level, self.num_nodes, self.output_dim).to(outputs_final.device) + ramdom_predict = torch.zeros(batch_size, self.seq_length - task_level, + self.num_nodes, self.output_dim).to(outputs_final.device) outputs_final = torch.cat([outputs_final, ramdom_predict], dim=1) - + return outputs_final def initHidden(self, batch_size, hidden_size): diff --git a/basicts/archs/DGCRN_arch/DGCRN_layer.py b/basicts/archs/arch_zoo/dgcrn_arch/dgcrn_layer.py similarity index 96% rename from basicts/archs/DGCRN_arch/DGCRN_layer.py rename to basicts/archs/arch_zoo/dgcrn_arch/dgcrn_layer.py index 7ad9fa7b..29ecb31f 100644 --- a/basicts/archs/DGCRN_arch/DGCRN_layer.py +++ b/basicts/archs/arch_zoo/dgcrn_arch/dgcrn_layer.py @@ -1,10 +1,8 @@ -from __future__ import division +from collections import OrderedDict + import torch import torch.nn as nn -from torch.nn import init -import numbers import torch.nn.functional as F -from collections import OrderedDict class gconv_RNN(nn.Module): diff --git a/basicts/archs/arch_zoo/gts_arch/__init__.py b/basicts/archs/arch_zoo/gts_arch/__init__.py new file mode 100644 index 00000000..8c79b7d2 --- /dev/null +++ b/basicts/archs/arch_zoo/gts_arch/__init__.py @@ -0,0 +1,3 @@ +from .gts_arch import GTS + +__all__ = ["GTS"] diff --git a/basicts/archs/GTS_arch/GTS_arch.py b/basicts/archs/arch_zoo/gts_arch/gts_arch.py similarity index 92% rename from basicts/archs/GTS_arch/GTS_arch.py rename to basicts/archs/arch_zoo/gts_arch/gts_arch.py index 28c5314a..f255526f 100644 --- a/basicts/archs/GTS_arch/GTS_arch.py +++ b/basicts/archs/arch_zoo/gts_arch/gts_arch.py @@ -1,20 +1,10 @@ +import numpy as np import torch import torch.nn as nn import torch.nn.functional as F -from basicts.archs.GTS_arch.GTS_cell import DCGRUCell -from basicts.archs.registry import ARCH_REGISTRY -import numpy as np -""" - Paper: - Discrete Graph Structure Learning for Forecasting Multiple Time Series, ICLR 2021. - Note: - Kindly note that the results of GTS may have some gaps with the original paper, - because it calculates the evaluation metrics in a slightly different manner. - Some details can be found in the appendix in the original paper and similar issues in its official repository: https://github.com/chaoshangcs/GTS/issues - Ref Official Code: - https://github.com/chaoshangcs/GTS -""" +from .gts_cell import DCGRUCell + def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) @@ -126,8 +116,20 @@ def forward(self, inputs, adj, hidden_state=None): return output, torch.stack(hidden_states) -@ARCH_REGISTRY.register() class GTS(nn.Module, Seq2SeqAttrs): + """ + Paper: + Discrete Graph Structure Learning for Forecasting Multiple Time Series, ICLR 2021. + Link: https://arxiv.org/abs/2101.06861 + Ref Official Code: + https://github.com/chaoshangcs/GTS + Note: + Kindly note that the results of GTS may have some gaps with the original paper, + because it calculates the evaluation metrics in a slightly different manner. + Some details can be found in the appendix in the original paper and + similar issues in its official repository: https://github.com/chaoshangcs/GTS/issues + """ + def __init__(self, **model_kwargs): """init GTS @@ -200,6 +202,7 @@ def encoder(self, inputs, adj): :param inputs: shape (seq_len, batch_size, num_sensor * input_dim) :return: encoder_hidden_state: (num_layers, batch_size, self.hidden_state_size) """ + encoder_hidden_state = None for t in range(self.encoder_model.seq_len): _, encoder_hidden_state = self.encoder_model(inputs[t], adj, encoder_hidden_state) @@ -214,6 +217,7 @@ def decoder(self, encoder_hidden_state, adj, labels=None, batches_seen=None): :param batches_seen: global step [optional, not exist for inference] :return: output: (self.horizon, batch_size, self.num_nodes * self.output_dim) """ + batch_size = encoder_hidden_state.size(1) go_symbol = torch.zeros((batch_size, self.num_nodes * self.decoder_model.output_dim)).to(encoder_hidden_state.device) decoder_hidden_state = encoder_hidden_state @@ -232,13 +236,25 @@ def decoder(self, encoder_hidden_state, adj, labels=None, batches_seen=None): outputs = torch.stack(outputs) return outputs - def forward(self, history_data, future_data=None, batch_seen=None, epoch=None): + def forward(self, history_data, future_data=None, batch_seen=None, epoch=None, **kwargs): """ - :param inputs: shape (seq_len, batch_size, num_sensor * input_dim) - :param labels: shape (horizon, batch_size, num_sensor * output) - :param batches_seen: batches seen till now + :param history_data: shape (seq_len, batch_size, num_sensor * input_dim) + :param future_data: shape (horizon, batch_size, num_sensor * output) + :param batch_seen: batches seen till now :return: output: (self.horizon, batch_size, self.num_nodes * self.output_dim) """ + + # reshape data + batch_size, length, num_nodes, channels = history_data.shape + history_data = history_data.reshape(batch_size, length, num_nodes * channels) # [B, L, N*C] + history_data = history_data.transpose(0, 1) # [L, B, N*C] + + if future_data is not None: + batch_size, length, num_nodes, channels = future_data.shape + future_data = future_data.reshape(batch_size, length, num_nodes * channels) # [B, L, N*C] + future_data = future_data.transpose(0, 1) # [L, B, N*C] + + # GTS inputs = history_data labels = future_data diff --git a/basicts/archs/GTS_arch/GTS_cell.py b/basicts/archs/arch_zoo/gts_arch/gts_cell.py similarity index 67% rename from basicts/archs/GTS_arch/GTS_cell.py rename to basicts/archs/arch_zoo/gts_arch/gts_cell.py index f6e28e93..528dee02 100644 --- a/basicts/archs/GTS_arch/GTS_cell.py +++ b/basicts/archs/arch_zoo/gts_arch/gts_cell.py @@ -1,5 +1,6 @@ -import numpy as np import torch +import numpy as np + class LayerParams: def __init__(self, rnn_network: torch.nn.Module, layer_type: str): @@ -13,7 +14,8 @@ def get_weights(self, shape): nn_param = torch.nn.Parameter(torch.empty(*shape)) torch.nn.init.xavier_normal_(nn_param) self._params_dict[shape] = nn_param - self._rnn_network.register_parameter('{}_weight_{}'.format(self._type, str(shape)), nn_param) + self._rnn_network.register_parameter( + '{}_weight_{}'.format(self._type, str(shape)), nn_param) return self._params_dict[shape] def get_biases(self, length, bias_start=0.0): @@ -21,24 +23,14 @@ def get_biases(self, length, bias_start=0.0): biases = torch.nn.Parameter(torch.empty(length)) torch.nn.init.constant_(biases, bias_start) self._biases_dict[length] = biases - self._rnn_network.register_parameter('{}_biases_{}'.format(self._type, str(length)), biases) + self._rnn_network.register_parameter( + '{}_biases_{}'.format(self._type, str(length)), biases) return self._biases_dict[length] class DCGRUCell(torch.nn.Module): def __init__(self, num_units, max_diffusion_step, num_nodes, nonlinearity='tanh', filter_type="laplacian", use_gc_for_ru=True): - """ - - :param num_units: - :param adj_mx: - :param max_diffusion_step: - :param num_nodes: - :param nonlinearity: - :param filter_type: "laplacian", "random_walk", "dual_random_walk". - :param use_gc_for_ru: whether to use Graph convolution to calculate the reset and update gates. - """ - super().__init__() self._activation = torch.tanh if nonlinearity == 'tanh' else torch.relu # support other nonlinearities up here? @@ -47,22 +39,6 @@ def __init__(self, num_units, max_diffusion_step, num_nodes, nonlinearity='tanh' self._max_diffusion_step = max_diffusion_step self._supports = [] self._use_gc_for_ru = use_gc_for_ru - - ''' - Option: - if filter_type == "laplacian": - supports.append(utils.calculate_scaled_laplacian(adj_mx, lambda_max=None)) - elif filter_type == "random_walk": - supports.append(utils.calculate_random_walk_matrix(adj_mx).T) - elif filter_type == "dual_random_walk": - supports.append(utils.calculate_random_walk_matrix(adj_mx).T) - supports.append(utils.calculate_random_walk_matrix(adj_mx.T).T) - else: - supports.append(utils.calculate_scaled_laplacian(adj_mx)) - for support in supports: - self._supports.append(self._build_sparse_matrix(support)) - ''' - self._fc_params = LayerParams(self, 'fc') self._gconv_params = LayerParams(self, 'gconv') @@ -82,7 +58,8 @@ def _calculate_random_walk_matrix(self, adj_mx): adj_mx = adj_mx + torch.eye(int(adj_mx.shape[0])).to(adj_mx.device) d = torch.sum(adj_mx, 1) d_inv = 1. / d - d_inv = torch.where(torch.isinf(d_inv), torch.zeros(d_inv.shape).to(d_inv.device), d_inv) + d_inv = torch.where(torch.isinf(d_inv), torch.zeros( + d_inv.shape).to(d_inv.device), d_inv) d_mat_inv = torch.diag(d_inv) random_walk_mx = torch.mm(d_mat_inv, adj_mx) return random_walk_mx @@ -101,9 +78,11 @@ def forward(self, inputs, hx, adj): fn = self._gconv else: fn = self._fc - value = torch.sigmoid(fn(inputs, adj_mx, hx, output_size, bias_start=1.0)) + value = torch.sigmoid( + fn(inputs, adj_mx, hx, output_size, bias_start=1.0)) value = torch.reshape(value, (-1, self._num_nodes, output_size)) - r, u = torch.split(tensor=value, split_size_or_sections=self._num_units, dim=-1) + r, u = torch.split( + tensor=value, split_size_or_sections=self._num_units, dim=-1) r = torch.reshape(r, (-1, self._num_nodes * self._num_units)) u = torch.reshape(u, (-1, self._num_nodes * self._num_units)) @@ -141,7 +120,8 @@ def _gconv(self, inputs, adj_mx, state, output_size, bias_start=0.0): x = inputs_and_state x0 = x.permute(1, 2, 0) # (num_nodes, total_arg_size, batch_size) - x0 = torch.reshape(x0, shape=[self._num_nodes, input_size * batch_size]) + x0 = torch.reshape( + x0, shape=[self._num_nodes, input_size * batch_size]) x = torch.unsqueeze(x0, 0) if self._max_diffusion_step == 0: @@ -154,26 +134,20 @@ def _gconv(self, inputs, adj_mx, state, output_size, bias_start=0.0): x2 = 2 * torch.mm(adj_mx, x1) - x0 x = self._concat(x, x2) x1, x0 = x2, x1 - ''' - Option: - for support in self._supports: - x1 = torch.sparse.mm(support, x0) - x = self._concat(x, x1) - - for k in range(2, self._max_diffusion_step + 1): - x2 = 2 * torch.sparse.mm(support, x1) - x0 - x = self._concat(x, x2) - x1, x0 = x2, x1 - ''' num_matrices = self._max_diffusion_step + 1 # Adds for x itself. - x = torch.reshape(x, shape=[num_matrices, self._num_nodes, input_size, batch_size]) + x = torch.reshape( + x, shape=[num_matrices, self._num_nodes, input_size, batch_size]) x = x.permute(3, 1, 2, 0) # (batch_size, num_nodes, input_size, order) - x = torch.reshape(x, shape=[batch_size * self._num_nodes, input_size * num_matrices]) + x = torch.reshape( + x, shape=[batch_size * self._num_nodes, input_size * num_matrices]) - weights = self._gconv_params.get_weights((input_size * num_matrices, output_size)).to(x.device) - x = torch.matmul(x, weights) # (batch_size * self._num_nodes, output_size) + weights = self._gconv_params.get_weights( + (input_size * num_matrices, output_size)).to(x.device) + # (batch_size * self._num_nodes, output_size) + x = torch.matmul(x, weights) - biases = self._gconv_params.get_biases(output_size, bias_start).to(x.device) + biases = self._gconv_params.get_biases( + output_size, bias_start).to(x.device) x += biases # Reshape res back to 2D: (batch_size, num_node, state_dim) -> (batch_size, num_node * state_dim) return torch.reshape(x, [batch_size, self._num_nodes * output_size]) diff --git a/basicts/archs/arch_zoo/gwnet_arch/__init__.py b/basicts/archs/arch_zoo/gwnet_arch/__init__.py new file mode 100644 index 00000000..505bf98f --- /dev/null +++ b/basicts/archs/arch_zoo/gwnet_arch/__init__.py @@ -0,0 +1,3 @@ +from .gwnet_arch import GraphWaveNet + +__all__ = ["GraphWaveNet"] diff --git a/basicts/archs/GraphWaveNet_arch/GraphWaveNet_arch.py b/basicts/archs/arch_zoo/gwnet_arch/gwnet_arch.py similarity index 71% rename from basicts/archs/GraphWaveNet_arch/GraphWaveNet_arch.py rename to basicts/archs/arch_zoo/gwnet_arch/gwnet_arch.py index 2ad65990..0665ad8e 100644 --- a/basicts/archs/GraphWaveNet_arch/GraphWaveNet_arch.py +++ b/basicts/archs/arch_zoo/gwnet_arch/gwnet_arch.py @@ -1,59 +1,70 @@ import torch -import torch.nn as nn +from torch import nn import torch.nn.functional as F -from torch.autograd import Variable -import sys -from basicts.archs.registry import ARCH_REGISTRY -""" - Paper: Graph WaveNet for Deep Spatial-Temporal Graph Modeling - Ref Official Code: https://github.com/nnzhan/Graph-WaveNet/blob/master/model.py -""" class nconv(nn.Module): + """Graph conv operation.""" + def __init__(self): - super(nconv,self).__init__() + super(nconv, self).__init__() - def forward(self,x, A): - x = torch.einsum('ncvl,vw->ncwl',(x,A)) + def forward(self, x, A): + x = torch.einsum('ncvl,vw->ncwl', (x, A)) return x.contiguous() + class linear(nn.Module): - def __init__(self,c_in,c_out): - super(linear,self).__init__() - self.mlp = torch.nn.Conv2d(c_in, c_out, kernel_size=(1, 1), padding=(0,0), stride=(1,1), bias=True) + """Linear layer.""" + + def __init__(self, c_in, c_out): + super(linear, self).__init__() + self.mlp = torch.nn.Conv2d(c_in, c_out, kernel_size=( + 1, 1), padding=(0, 0), stride=(1, 1), bias=True) - def forward(self,x): + def forward(self, x): return self.mlp(x) + class gcn(nn.Module): - def __init__(self,c_in,c_out,dropout,support_len=3,order=2): - super(gcn,self).__init__() + """Graph convolution network.""" + + def __init__(self, c_in, c_out, dropout, support_len=3, order=2): + super(gcn, self).__init__() self.nconv = nconv() c_in = (order*support_len+1)*c_in - self.mlp = linear(c_in,c_out) + self.mlp = linear(c_in, c_out) self.dropout = dropout self.order = order - def forward(self,x,support): + def forward(self, x, support): out = [x] for a in support: - x1 = self.nconv(x,a.to(x.device)) + x1 = self.nconv(x, a.to(x.device)) out.append(x1) for k in range(2, self.order + 1): - x2 = self.nconv(x1,a.to(x.device)) + x2 = self.nconv(x1, a.to(x.device)) out.append(x2) x1 = x2 - h = torch.cat(out,dim=1) + h = torch.cat(out, dim=1) h = self.mlp(h) h = F.dropout(h, self.dropout, training=self.training) return h -@ARCH_REGISTRY.register() class GraphWaveNet(nn.Module): - def __init__(self, num_nodes, dropout=0.3, supports=None, gcn_bool=True, addaptadj=True, aptinit=None, in_dim=2,out_dim=12,residual_channels=32,dilation_channels=32,skip_channels=256,end_channels=512,kernel_size=2,blocks=4,layers=2): + """ + Paper: Graph WaveNet for Deep Spatial-Temporal Graph Modeling + Link: https://arxiv.org/abs/1906.00121 + Ref Official Code: https://github.com/nnzhan/Graph-WaveNet/blob/master/model.py + """ + + def __init__(self, num_nodes, dropout=0.3, supports=None, + gcn_bool=True, addaptadj=True, aptinit=None, + in_dim=2, out_dim=12, residual_channels=32, + dilation_channels=32, skip_channels=256, end_channels=512, + kernel_size=2, blocks=4, layers=2): super(GraphWaveNet, self).__init__() self.dropout = dropout self.blocks = blocks @@ -70,7 +81,7 @@ def __init__(self, num_nodes, dropout=0.3, supports=None, gcn_bool=True, addapta self.start_conv = nn.Conv2d(in_channels=in_dim, out_channels=residual_channels, - kernel_size=(1,1)) + kernel_size=(1, 1)) self.supports = supports receptive_field = 1 @@ -83,9 +94,11 @@ def __init__(self, num_nodes, dropout=0.3, supports=None, gcn_bool=True, addapta if aptinit is None: if supports is None: self.supports = [] - self.nodevec1 = nn.Parameter(torch.randn(num_nodes, 10), requires_grad=True) - self.nodevec2 = nn.Parameter(torch.randn(10, num_nodes), requires_grad=True) - self.supports_len +=1 + self.nodevec1 = nn.Parameter( + torch.randn(num_nodes, 10), requires_grad=True) + self.nodevec2 = nn.Parameter( + torch.randn(10, num_nodes), requires_grad=True) + self.supports_len += 1 else: if supports is None: self.supports = [] @@ -96,9 +109,6 @@ def __init__(self, num_nodes, dropout=0.3, supports=None, gcn_bool=True, addapta self.nodevec2 = nn.Parameter(initemb2, requires_grad=True) self.supports_len += 1 - - - for b in range(blocks): additional_scope = kernel_size - 1 new_dilation = 1 @@ -106,7 +116,7 @@ def __init__(self, num_nodes, dropout=0.3, supports=None, gcn_bool=True, addapta # dilated convolutions self.filter_convs.append(nn.Conv2d(in_channels=residual_channels, out_channels=dilation_channels, - kernel_size=(1,kernel_size),dilation=new_dilation)) + kernel_size=(1, kernel_size), dilation=new_dilation)) self.gate_convs.append(nn.Conv1d(in_channels=residual_channels, out_channels=dilation_channels, @@ -122,28 +132,27 @@ def __init__(self, num_nodes, dropout=0.3, supports=None, gcn_bool=True, addapta out_channels=skip_channels, kernel_size=(1, 1))) self.bn.append(nn.BatchNorm2d(residual_channels)) - new_dilation *=2 + new_dilation *= 2 receptive_field += additional_scope additional_scope *= 2 if self.gcn_bool: - self.gconv.append(gcn(dilation_channels,residual_channels,dropout,support_len=self.supports_len)) - - + self.gconv.append( + gcn(dilation_channels, residual_channels, dropout, support_len=self.supports_len)) self.end_conv_1 = nn.Conv2d(in_channels=skip_channels, - out_channels=end_channels, - kernel_size=(1,1), - bias=True) + out_channels=end_channels, + kernel_size=(1, 1), + bias=True) self.end_conv_2 = nn.Conv2d(in_channels=end_channels, out_channels=out_dim, - kernel_size=(1,1), + kernel_size=(1, 1), bias=True) self.receptive_field = receptive_field - def forward(self, history_data: torch.Tensor, **kwargs) -> torch.Tensor: - """feedforward function of Graph WaveNet. + def forward(self, history_data: torch.Tensor, future_data: torch.Tensor, batch_seen: int, epoch: int, train: bool, **kwargs) -> torch.Tensor: + """Feedforward function of Graph WaveNet. Args: history_data (torch.Tensor): shape [B, L, N, C] @@ -151,10 +160,12 @@ def forward(self, history_data: torch.Tensor, **kwargs) -> torch.Tensor: Returns: torch.Tensor: [B, L, N, 1] """ + input = history_data.transpose(1, 3).contiguous() in_len = input.size(3) - if in_len torch.Tensor: # calculate the current adaptive adj matrix once per iteration new_supports = None if self.gcn_bool and self.addaptadj and self.supports is not None: - adp = F.softmax(F.relu(torch.mm(self.nodevec1, self.nodevec2)), dim=1) + adp = F.softmax( + F.relu(torch.mm(self.nodevec1, self.nodevec2)), dim=1) new_supports = self.supports + [adp] # WaveNet layers @@ -199,18 +211,16 @@ def forward(self, history_data: torch.Tensor, **kwargs) -> torch.Tensor: skip = 0 skip = s + skip - if self.gcn_bool and self.supports is not None: if self.addaptadj: x = self.gconv[i](x, new_supports) else: - x = self.gconv[i](x,self.supports) + x = self.gconv[i](x, self.supports) else: x = self.residual_convs[i](x) x = x + residual[:, :, :, -x.size(3):] - x = self.bn[i](x) x = F.relu(skip) diff --git a/basicts/archs/arch_zoo/linear_arch/__init__.py b/basicts/archs/arch_zoo/linear_arch/__init__.py new file mode 100644 index 00000000..923f2f77 --- /dev/null +++ b/basicts/archs/arch_zoo/linear_arch/__init__.py @@ -0,0 +1,5 @@ +from .linear import Linear +from .dlinear import DLinear +from .nlinear import NLinear + +__all__ = ["Linear", "DLinear", "NLinear"] diff --git a/basicts/archs/arch_zoo/linear_arch/dlinear.py b/basicts/archs/arch_zoo/linear_arch/dlinear.py new file mode 100644 index 00000000..ca3135bf --- /dev/null +++ b/basicts/archs/arch_zoo/linear_arch/dlinear.py @@ -0,0 +1,98 @@ +import torch +import torch.nn as nn + + +class moving_avg(nn.Module): + """Moving average block to highlight the trend of time series""" + + def __init__(self, kernel_size, stride): + super(moving_avg, self).__init__() + self.kernel_size = kernel_size + self.avg = nn.AvgPool1d(kernel_size=kernel_size, + stride=stride, padding=0) + + def forward(self, x): + # padding on the both ends of time series + front = x[:, 0:1, :].repeat(1, (self.kernel_size - 1) // 2, 1) + end = x[:, -1:, :].repeat(1, (self.kernel_size - 1) // 2, 1) + x = torch.cat([front, x, end], dim=1) + x = self.avg(x.permute(0, 2, 1)) + x = x.permute(0, 2, 1) + return x + + +class series_decomp(nn.Module): + """Series decomposition block""" + + def __init__(self, kernel_size): + super(series_decomp, self).__init__() + self.moving_avg = moving_avg(kernel_size, stride=1) + + def forward(self, x): + moving_mean = self.moving_avg(x) + res = x - moving_mean + return res, moving_mean + + +class DLinear(nn.Module): + """ + The implementation of the decomposition-linear model in Paper "Are Transformers Effective for Time Series Forecasting?" + Link: https://arxiv.org/abs/2205.13504 + """ + + def __init__(self, **model_args): + super(DLinear, self).__init__() + self.seq_len = model_args["seq_len"] + self.pred_len = model_args["pred_len"] + + # Decompsition Kernel Size + kernel_size = 25 + self.decompsition = series_decomp(kernel_size) + self.individual = model_args["individual"] + self.channels = model_args["enc_in"] + + if self.individual: + self.Linear_Seasonal = nn.ModuleList() + self.Linear_Trend = nn.ModuleList() + + for i in range(self.channels): + self.Linear_Seasonal.append( + nn.Linear(self.seq_len, self.pred_len)) + self.Linear_Trend.append( + nn.Linear(self.seq_len, self.pred_len)) + + else: + self.Linear_Seasonal = nn.Linear(self.seq_len, self.pred_len) + self.Linear_Trend = nn.Linear(self.seq_len, self.pred_len) + + def forward(self, history_data: torch.Tensor, future_data: torch.Tensor, batch_seen: int, epoch: int, train: bool, **kwargs) -> torch.Tensor: + """Feed forward of STID. + + Args: + history_data (torch.Tensor): history data with shape [B, L, N, C] + + Returns: + torch.Tensor: prediction wit shape [B, L, N, C] + """ + + assert history_data.shape[-1] == 1 # only use the target feature + x = history_data[..., 0] # B, L, N + seasonal_init, trend_init = self.decompsition(x) + seasonal_init, trend_init = seasonal_init.permute( + 0, 2, 1), trend_init.permute(0, 2, 1) + if self.individual: + seasonal_output = torch.zeros([seasonal_init.size(0), seasonal_init.size( + 1), self.pred_len], dtype=seasonal_init.dtype).to(seasonal_init.device) + trend_output = torch.zeros([trend_init.size(0), trend_init.size( + 1), self.pred_len], dtype=trend_init.dtype).to(trend_init.device) + for i in range(self.channels): + seasonal_output[:, i, :] = self.Linear_Seasonal[i]( + seasonal_init[:, i, :]) + trend_output[:, i, :] = self.Linear_Trend[i]( + trend_init[:, i, :]) + else: + seasonal_output = self.Linear_Seasonal(seasonal_init) + trend_output = self.Linear_Trend(trend_init) + + prediction = seasonal_output + trend_output + return prediction.permute(0, 2, 1).unsqueeze(-1) # [B, L, N, 1] diff --git a/basicts/archs/arch_zoo/linear_arch/linear.py b/basicts/archs/arch_zoo/linear_arch/linear.py new file mode 100644 index 00000000..ff4387fc --- /dev/null +++ b/basicts/archs/arch_zoo/linear_arch/linear.py @@ -0,0 +1,29 @@ +import torch +import torch.nn as nn + +class Linear(nn.Module): + """ + The implementation of the linear model in Paper "Are Transformers Effective for Time Series Forecasting?" + Link: https://arxiv.org/abs/2205.13504 + """ + + def __init__(self, **model_args): + super(Linear, self).__init__() + self.seq_len = model_args["seq_len"] + self.pred_len = model_args["pred_len"] + self.Linear = nn.Linear(self.seq_len, self.pred_len) + + def forward(self, history_data: torch.Tensor, future_data: torch.Tensor, batch_seen: int, epoch: int, train: bool, **kwargs) -> torch.Tensor: + """Feed forward of STID. + + Args: + history_data (torch.Tensor): history data with shape [B, L, N, C] + + Returns: + torch.Tensor: prediction wit shape [B, L, N, C] + """ + + assert history_data.shape[-1] == 1 # only use the target feature + history_data = history_data[..., 0] # B, L, N + prediction = self.Linear(history_data.permute(0, 2, 1)).permute(0, 2, 1).unsqueeze(-1) # B, L, N, 1 + return prediction diff --git a/basicts/archs/arch_zoo/linear_arch/nlinear.py b/basicts/archs/arch_zoo/linear_arch/nlinear.py new file mode 100644 index 00000000..53834c8c --- /dev/null +++ b/basicts/archs/arch_zoo/linear_arch/nlinear.py @@ -0,0 +1,32 @@ +import torch +import torch.nn as nn + +class NLinear(nn.Module): + """ + The implementation of the normalization-linear model in Paper "Are Transformers Effective for Time Series Forecasting?" + Link: https://arxiv.org/abs/2205.13504 + """ + + def __init__(self, **model_args): + super(NLinear, self).__init__() + self.seq_len = model_args["seq_len"] + self.pred_len = model_args["pred_len"] + self.Linear = nn.Linear(self.seq_len, self.pred_len) + + def forward(self, history_data: torch.Tensor, future_data: torch.Tensor, batch_seen: int, epoch: int, train: bool, **kwargs) -> torch.Tensor: + """Feed forward of STID. + + Args: + history_data (torch.Tensor): history data with shape [B, L, N, C] + + Returns: + torch.Tensor: prediction wit shape [B, L, N, C] + """ + assert history_data.shape[-1] == 1 # only use the target feature + x = history_data[..., 0] # B, L, N + # x: [Batch, Input length, Channel] + seq_last = x[:,-1:,:].detach() + x = x - seq_last + x = self.Linear(x.permute(0,2,1)).permute(0,2,1) + prediction = x + seq_last + return prediction.unsqueeze(-1) diff --git a/basicts/archs/arch_zoo/mtgnn_arch/__init__.py b/basicts/archs/arch_zoo/mtgnn_arch/__init__.py new file mode 100644 index 00000000..3d2c7d9b --- /dev/null +++ b/basicts/archs/arch_zoo/mtgnn_arch/__init__.py @@ -0,0 +1,3 @@ +from .mtgnn_arch import MTGNN + +__all__ = ["MTGNN"] diff --git a/basicts/archs/arch_zoo/mtgnn_arch/mtgnn_arch.py b/basicts/archs/arch_zoo/mtgnn_arch/mtgnn_arch.py new file mode 100644 index 00000000..23b714b3 --- /dev/null +++ b/basicts/archs/arch_zoo/mtgnn_arch/mtgnn_arch.py @@ -0,0 +1,165 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +from .mtgnn_layers import graph_constructor, dilated_inception, mixprop, LayerNorm + + +class MTGNN(nn.Module): + """ + Paper: Connecting the Dots: Multivariate Time Series Forecasting with Graph Neural Networks + Ref Official Code: https://github.com/nnzhan/MTGNN + Link: https://arxiv.org/abs/2005.11650 + """ + + def __init__(self, gcn_true, buildA_true, gcn_depth, num_nodes, predefined_A=None, static_feat=None, dropout=0.3, subgraph_size=20, node_dim=40, dilation_exponential=1, conv_channels=32, residual_channels=32, skip_channels=64, end_channels=128, seq_length=12, in_dim=2, out_dim=12, layers=3, propalpha=0.05, tanhalpha=3, layer_norm_affline=True): + super(MTGNN, self).__init__() + self.gcn_true = gcn_true + self.buildA_true = buildA_true + self.num_nodes = num_nodes + self.dropout = dropout + self.predefined_A = predefined_A + self.filter_convs = nn.ModuleList() + self.gate_convs = nn.ModuleList() + self.residual_convs = nn.ModuleList() + self.skip_convs = nn.ModuleList() + self.gconv1 = nn.ModuleList() + self.gconv2 = nn.ModuleList() + self.norm = nn.ModuleList() + self.start_conv = nn.Conv2d( + in_channels=in_dim, out_channels=residual_channels, kernel_size=(1, 1)) + self.gc = graph_constructor( + num_nodes, subgraph_size, node_dim, alpha=tanhalpha, static_feat=static_feat) + + self.seq_length = seq_length + kernel_size = 7 + if dilation_exponential > 1: + self.receptive_field = int( + 1+(kernel_size-1)*(dilation_exponential**layers-1)/(dilation_exponential-1)) + else: + self.receptive_field = layers*(kernel_size-1) + 1 + + for i in range(1): + if dilation_exponential > 1: + rf_size_i = int( + 1 + i*(kernel_size-1)*(dilation_exponential**layers-1)/(dilation_exponential-1)) + else: + rf_size_i = i*layers*(kernel_size-1)+1 + new_dilation = 1 + for j in range(1, layers+1): + if dilation_exponential > 1: + rf_size_j = int( + rf_size_i + (kernel_size-1)*(dilation_exponential**j-1)/(dilation_exponential-1)) + else: + rf_size_j = rf_size_i+j*(kernel_size-1) + + self.filter_convs.append(dilated_inception( + residual_channels, conv_channels, dilation_factor=new_dilation)) + self.gate_convs.append(dilated_inception( + residual_channels, conv_channels, dilation_factor=new_dilation)) + self.residual_convs.append(nn.Conv2d( + in_channels=conv_channels, out_channels=residual_channels, kernel_size=(1, 1))) + if self.seq_length > self.receptive_field: + self.skip_convs.append(nn.Conv2d( + in_channels=conv_channels, out_channels=skip_channels, kernel_size=(1, self.seq_length-rf_size_j+1))) + else: + self.skip_convs.append(nn.Conv2d(in_channels=conv_channels, out_channels=skip_channels, kernel_size=( + 1, self.receptive_field-rf_size_j+1))) + + if self.gcn_true: + self.gconv1.append( + mixprop(conv_channels, residual_channels, gcn_depth, dropout, propalpha)) + self.gconv2.append( + mixprop(conv_channels, residual_channels, gcn_depth, dropout, propalpha)) + + if self.seq_length > self.receptive_field: + self.norm.append(LayerNorm( + (residual_channels, num_nodes, self.seq_length - rf_size_j + 1), elementwise_affine=layer_norm_affline)) + else: + self.norm.append(LayerNorm( + (residual_channels, num_nodes, self.receptive_field - rf_size_j + 1), elementwise_affine=layer_norm_affline)) + + new_dilation *= dilation_exponential + + self.layers = layers + self.end_conv_1 = nn.Conv2d( + in_channels=skip_channels, out_channels=end_channels, kernel_size=(1, 1), bias=True) + self.end_conv_2 = nn.Conv2d( + in_channels=end_channels, out_channels=out_dim, kernel_size=(1, 1), bias=True) + if self.seq_length > self.receptive_field: + self.skip0 = nn.Conv2d(in_channels=in_dim, out_channels=skip_channels, kernel_size=( + 1, self.seq_length), bias=True) + self.skipE = nn.Conv2d(in_channels=residual_channels, out_channels=skip_channels, kernel_size=( + 1, self.seq_length-self.receptive_field+1), bias=True) + + else: + self.skip0 = nn.Conv2d(in_channels=in_dim, out_channels=skip_channels, kernel_size=( + 1, self.receptive_field), bias=True) + self.skipE = nn.Conv2d(in_channels=residual_channels, + out_channels=skip_channels, kernel_size=(1, 1), bias=True) + + self.idx = torch.arange(self.num_nodes) + + def forward(self, history_data: torch.Tensor, idx: int = None, **kwargs) -> torch.Tensor: + """feedforward function of MTGNN. + + Args: + history_data (torch.Tensor): history data with shape [B, L, N, C] + idx (int, optional): Graph Learning Hyperparameter. Defaults to None. + + Returns: + torch.Tensor: prediction + """ + # select feature + history_data = history_data.transpose(1, 3).contiguous() + seq_len = history_data.size(3) + assert seq_len == self.seq_length, 'input sequence length not equal to preset sequence length' + + if self.seq_length < self.receptive_field: + history_data = nn.functional.pad( + history_data, (self.receptive_field-self.seq_length, 0, 0, 0)) + + if self.gcn_true: + if self.buildA_true: + if idx is None: + adp = self.gc(self.idx) + else: + adp = self.gc(idx) + else: + adp = self.predefined_A + + x = self.start_conv(history_data) + skip = self.skip0( + F.dropout(history_data, self.dropout, training=self.training)) + for i in range(self.layers): + residual = x + filter = self.filter_convs[i](x) + filter = torch.tanh(filter) + gate = self.gate_convs[i](x) + gate = torch.sigmoid(gate) + x = filter * gate + x = F.dropout(x, self.dropout, training=self.training) + s = x + s = self.skip_convs[i](s) + skip = s + skip + if self.gcn_true: + x = self.gconv1[i](x, adp)+self.gconv2[i](x, + adp.transpose(1, 0)) + else: + x = self.residual_convs[i](x) + + x = x + residual[:, :, :, -x.size(3):] + if idx is None: + x = self.norm[i](x, self.idx) + else: + x = self.norm[i](x, idx) + # print(x.shape) + skip = self.skipE(x) + skip + # print(skip.shape) + x = F.relu(skip) + x = F.relu(self.end_conv_1(x)) + # print(x.shape) + x = self.end_conv_2(x) + # print(x.shape) + + return x diff --git a/basicts/archs/MTGNN_arch/MTGNN_layers.py b/basicts/archs/arch_zoo/mtgnn_arch/mtgnn_layers.py similarity index 72% rename from basicts/archs/MTGNN_arch/MTGNN_layers.py rename to basicts/archs/arch_zoo/mtgnn_arch/mtgnn_layers.py index c2693801..93efea8f 100644 --- a/basicts/archs/MTGNN_arch/MTGNN_layers.py +++ b/basicts/archs/arch_zoo/mtgnn_arch/mtgnn_layers.py @@ -1,111 +1,113 @@ +import numbers + import torch import torch.nn as nn from torch.nn import init -import numbers import torch.nn.functional as F class nconv(nn.Module): def __init__(self): - super(nconv,self).__init__() + super(nconv, self).__init__() - def forward(self,x, A): - x = torch.einsum('ncvl,vw->ncwl',(x,A)) + def forward(self, x, A): + x = torch.einsum('ncvl,vw->ncwl', (x, A)) return x.contiguous() + class dy_nconv(nn.Module): def __init__(self): - super(dy_nconv,self).__init__() + super(dy_nconv, self).__init__() - def forward(self,x, A): - x = torch.einsum('ncvl,nvwl->ncwl',(x,A)) + def forward(self, x, A): + x = torch.einsum('ncvl,nvwl->ncwl', (x, A)) return x.contiguous() + class linear(nn.Module): - def __init__(self,c_in,c_out,bias=True): - super(linear,self).__init__() - self.mlp = torch.nn.Conv2d(c_in, c_out, kernel_size=(1, 1), padding=(0,0), stride=(1,1), bias=bias) + def __init__(self, c_in, c_out, bias=True): + super(linear, self).__init__() + self.mlp = torch.nn.Conv2d(c_in, c_out, kernel_size=( + 1, 1), padding=(0, 0), stride=(1, 1), bias=bias) - def forward(self,x): + def forward(self, x): return self.mlp(x) class prop(nn.Module): - def __init__(self,c_in,c_out,gdep,dropout,alpha): + def __init__(self, c_in, c_out, gdep, dropout, alpha): super(prop, self).__init__() self.nconv = nconv() - self.mlp = linear(c_in,c_out) + self.mlp = linear(c_in, c_out) self.gdep = gdep self.dropout = dropout self.alpha = alpha - def forward(self,x,adj): + def forward(self, x, adj): adj = adj + torch.eye(adj.size(0)).to(x.device) d = adj.sum(1) h = x dv = d a = adj / dv.view(-1, 1) for i in range(self.gdep): - h = self.alpha*x + (1-self.alpha)*self.nconv(h,a) + h = self.alpha*x + (1-self.alpha)*self.nconv(h, a) ho = self.mlp(h) return ho class mixprop(nn.Module): - def __init__(self,c_in,c_out,gdep,dropout,alpha): + def __init__(self, c_in, c_out, gdep, dropout, alpha): super(mixprop, self).__init__() self.nconv = nconv() - self.mlp = linear((gdep+1)*c_in,c_out) + self.mlp = linear((gdep+1)*c_in, c_out) self.gdep = gdep self.dropout = dropout self.alpha = alpha - - def forward(self,x,adj): + def forward(self, x, adj): adj = adj + torch.eye(adj.size(0)).to(x.device) d = adj.sum(1) h = x out = [h] a = adj / d.view(-1, 1) for i in range(self.gdep): - h = self.alpha*x + (1-self.alpha)*self.nconv(h,a) + h = self.alpha*x + (1-self.alpha)*self.nconv(h, a) out.append(h) - ho = torch.cat(out,dim=1) + ho = torch.cat(out, dim=1) ho = self.mlp(ho) return ho + class dy_mixprop(nn.Module): - def __init__(self,c_in,c_out,gdep,dropout,alpha): + def __init__(self, c_in, c_out, gdep, dropout, alpha): super(dy_mixprop, self).__init__() self.nconv = dy_nconv() - self.mlp1 = linear((gdep+1)*c_in,c_out) - self.mlp2 = linear((gdep+1)*c_in,c_out) + self.mlp1 = linear((gdep+1)*c_in, c_out) + self.mlp2 = linear((gdep+1)*c_in, c_out) self.gdep = gdep self.dropout = dropout self.alpha = alpha - self.lin1 = linear(c_in,c_in) - self.lin2 = linear(c_in,c_in) - + self.lin1 = linear(c_in, c_in) + self.lin2 = linear(c_in, c_in) - def forward(self,x): + def forward(self, x): #adj = adj + torch.eye(adj.size(0)).to(x.device) #d = adj.sum(1) x1 = torch.tanh(self.lin1(x)) x2 = torch.tanh(self.lin2(x)) - adj = self.nconv(x1.transpose(2,1),x2) + adj = self.nconv(x1.transpose(2, 1), x2) adj0 = torch.softmax(adj, dim=2) - adj1 = torch.softmax(adj.transpose(2,1), dim=2) + adj1 = torch.softmax(adj.transpose(2, 1), dim=2) h = x out = [h] for i in range(self.gdep): - h = self.alpha*x + (1-self.alpha)*self.nconv(h,adj0) + h = self.alpha*x + (1-self.alpha)*self.nconv(h, adj0) out.append(h) - ho = torch.cat(out,dim=1) + ho = torch.cat(out, dim=1) ho1 = self.mlp1(ho) - h = x out = [h] for i in range(self.gdep): @@ -117,34 +119,36 @@ def forward(self,x): return ho1+ho2 - class dilated_1D(nn.Module): def __init__(self, cin, cout, dilation_factor=2): super(dilated_1D, self).__init__() self.tconv = nn.ModuleList() - self.kernel_set = [2,3,6,7] - self.tconv = nn.Conv2d(cin,cout,(1,7),dilation=(1,dilation_factor)) + self.kernel_set = [2, 3, 6, 7] + self.tconv = nn.Conv2d( + cin, cout, (1, 7), dilation=(1, dilation_factor)) - def forward(self,input): + def forward(self, input): x = self.tconv(input) return x + class dilated_inception(nn.Module): def __init__(self, cin, cout, dilation_factor=2): super(dilated_inception, self).__init__() self.tconv = nn.ModuleList() - self.kernel_set = [2,3,6,7] + self.kernel_set = [2, 3, 6, 7] cout = int(cout/len(self.kernel_set)) for kern in self.kernel_set: - self.tconv.append(nn.Conv2d(cin,cout,(1,kern),dilation=(1,dilation_factor))) + self.tconv.append(nn.Conv2d(cin, cout, (1, kern), + dilation=(1, dilation_factor))) - def forward(self,input): + def forward(self, input): x = [] for i in range(len(self.kernel_set)): x.append(self.tconv[i](input)) for i in range(len(self.kernel_set)): - x[i] = x[i][...,-x[-1].size(3):] - x = torch.cat(x,dim=1) + x[i] = x[i][..., -x[-1].size(3):] + x = torch.cat(x, dim=1) return x @@ -159,8 +163,8 @@ def __init__(self, nnodes, k, dim, alpha=3, static_feat=None): else: self.emb1 = nn.Embedding(nnodes, dim) self.emb2 = nn.Embedding(nnodes, dim) - self.lin1 = nn.Linear(dim,dim) - self.lin2 = nn.Linear(dim,dim) + self.lin1 = nn.Linear(dim, dim) + self.lin2 = nn.Linear(dim, dim) self.k = k self.dim = dim @@ -174,18 +178,19 @@ def forward(self, idx): nodevec2 = self.emb2(idx) else: idx = idx.to(self.static_feat.device) - nodevec1 = self.static_feat[idx,:] + nodevec1 = self.static_feat[idx, :] nodevec2 = nodevec1 nodevec1 = torch.tanh(self.alpha*self.lin1(nodevec1)) nodevec2 = torch.tanh(self.alpha*self.lin2(nodevec2)) - a = torch.mm(nodevec1, nodevec2.transpose(1,0))-torch.mm(nodevec2, nodevec1.transpose(1,0)) + a = torch.mm(nodevec1, nodevec2.transpose(1, 0)) - \ + torch.mm(nodevec2, nodevec1.transpose(1, 0)) adj = F.relu(torch.tanh(self.alpha*a)) mask = torch.zeros(idx.size(0), idx.size(0)).to(adj.device) mask.fill_(float('0')) - s1,t1 = adj.topk(self.k,1) - mask.scatter_(1,t1,s1.fill_(1)) + s1, t1 = adj.topk(self.k, 1) + mask.scatter_(1, t1, s1.fill_(1)) adj = adj*mask return adj @@ -194,16 +199,18 @@ def fullA(self, idx): nodevec1 = self.emb1(idx) nodevec2 = self.emb2(idx) else: - nodevec1 = self.static_feat[idx,:] + nodevec1 = self.static_feat[idx, :] nodevec2 = nodevec1 nodevec1 = torch.tanh(self.alpha*self.lin1(nodevec1)) nodevec2 = torch.tanh(self.alpha*self.lin2(nodevec2)) - a = torch.mm(nodevec1, nodevec2.transpose(1,0))-torch.mm(nodevec2, nodevec1.transpose(1,0)) + a = torch.mm(nodevec1, nodevec2.transpose(1, 0)) - \ + torch.mm(nodevec2, nodevec1.transpose(1, 0)) adj = F.relu(torch.tanh(self.alpha*a)) return adj + class graph_global(nn.Module): def __init__(self, nnodes, k, dim, alpha=3, static_feat=None): super(graph_global, self).__init__() @@ -223,7 +230,7 @@ def __init__(self, nnodes, k, dim, alpha=3, static_feat=None): self.lin1 = nn.Linear(xd, dim) else: self.emb1 = nn.Embedding(nnodes, dim) - self.lin1 = nn.Linear(dim,dim) + self.lin1 = nn.Linear(dim, dim) self.k = k self.dim = dim @@ -235,23 +242,22 @@ def forward(self, idx): nodevec1 = self.emb1(idx) nodevec2 = self.emb1(idx) else: - nodevec1 = self.static_feat[idx,:] + nodevec1 = self.static_feat[idx, :] nodevec2 = nodevec1 nodevec1 = torch.tanh(self.alpha*self.lin1(nodevec1)) nodevec2 = torch.tanh(self.alpha*self.lin1(nodevec2)) - a = torch.mm(nodevec1, nodevec2.transpose(1,0)) + a = torch.mm(nodevec1, nodevec2.transpose(1, 0)) adj = F.relu(torch.tanh(self.alpha*a)) mask = torch.zeros(idx.size(0), idx.size(0)) mask.fill_(float('0')) - s1,t1 = adj.topk(self.k,1) - mask.scatter_(1,t1,s1.fill_(1)) + s1, t1 = adj.topk(self.k, 1) + mask.scatter_(1, t1, s1.fill_(1)) adj = adj*mask return adj - class graph_directed(nn.Module): def __init__(self, nnodes, k, dim, alpha=3, static_feat=None): super(graph_directed, self).__init__() @@ -263,8 +269,8 @@ def __init__(self, nnodes, k, dim, alpha=3, static_feat=None): else: self.emb1 = nn.Embedding(nnodes, dim) self.emb2 = nn.Embedding(nnodes, dim) - self.lin1 = nn.Linear(dim,dim) - self.lin2 = nn.Linear(dim,dim) + self.lin1 = nn.Linear(dim, dim) + self.lin2 = nn.Linear(dim, dim) self.k = k self.dim = dim @@ -276,24 +282,26 @@ def forward(self, idx): nodevec1 = self.emb1(idx) nodevec2 = self.emb2(idx) else: - nodevec1 = self.static_feat[idx,:] + nodevec1 = self.static_feat[idx, :] nodevec2 = nodevec1 nodevec1 = torch.tanh(self.alpha*self.lin1(nodevec1)) nodevec2 = torch.tanh(self.alpha*self.lin2(nodevec2)) - a = torch.mm(nodevec1, nodevec2.transpose(1,0)) + a = torch.mm(nodevec1, nodevec2.transpose(1, 0)) adj = F.relu(torch.tanh(self.alpha*a)) mask = torch.zeros(idx.size(0), idx.size(0)) mask.fill_(float('0')) - s1,t1 = adj.topk(self.k,1) - mask.scatter_(1,t1,s1.fill_(1)) + s1, t1 = adj.topk(self.k, 1) + mask.scatter_(1, t1, s1.fill_(1)) adj = adj*mask return adj class LayerNorm(nn.Module): - __constants__ = ['normalized_shape', 'weight', 'bias', 'eps', 'elementwise_affine'] + __constants__ = ['normalized_shape', 'weight', + 'bias', 'eps', 'elementwise_affine'] + def __init__(self, normalized_shape, eps=1e-5, elementwise_affine=True): super(LayerNorm, self).__init__() if isinstance(normalized_shape, numbers.Integral): @@ -309,7 +317,6 @@ def __init__(self, normalized_shape, eps=1e-5, elementwise_affine=True): self.register_parameter('bias', None) self.reset_parameters() - def reset_parameters(self): if self.elementwise_affine: init.ones_(self.weight) @@ -317,10 +324,10 @@ def reset_parameters(self): def forward(self, input, idx): if self.elementwise_affine: - return F.layer_norm(input, tuple(input.shape[1:]), self.weight[:,idx,:], self.bias[:,idx,:], self.eps) + return F.layer_norm(input, tuple(input.shape[1:]), self.weight[:, idx, :], self.bias[:, idx, :], self.eps) else: return F.layer_norm(input, tuple(input.shape[1:]), self.weight, self.bias, self.eps) def extra_repr(self): return '{normalized_shape}, eps={eps}, ' \ - 'elementwise_affine={elementwise_affine}'.format(**self.__dict__) \ No newline at end of file + 'elementwise_affine={elementwise_affine}'.format(**self.__dict__) diff --git a/basicts/archs/arch_zoo/stemgnn_arch/__init__.py b/basicts/archs/arch_zoo/stemgnn_arch/__init__.py new file mode 100644 index 00000000..2154d27c --- /dev/null +++ b/basicts/archs/arch_zoo/stemgnn_arch/__init__.py @@ -0,0 +1,3 @@ +from .stemgnn_arch import StemGNN + +__all__ = ["StemGNN"] diff --git a/basicts/archs/StemGNN_arch/StemGNN_arch.py b/basicts/archs/arch_zoo/stemgnn_arch/stemgnn_arch.py similarity index 71% rename from basicts/archs/StemGNN_arch/StemGNN_arch.py rename to basicts/archs/arch_zoo/stemgnn_arch/stemgnn_arch.py index a9028abc..c8a6533e 100644 --- a/basicts/archs/StemGNN_arch/StemGNN_arch.py +++ b/basicts/archs/arch_zoo/stemgnn_arch/stemgnn_arch.py @@ -1,22 +1,7 @@ import torch import torch.nn as nn import torch.nn.functional as F -from basicts.archs.registry import ARCH_REGISTRY -""" - Paper: Spectral Temporal Graph Neural Network for Multivariate Time-series Forecasting - Ref Official Code: https://github.com/microsoft/StemGNN - Note: - There are some difference in implementation described in the paper as well as the source code. - Details can be found in [here](https://github.com/microsoft/StemGNN/issues/12) - We adopt the implementation of the code. - Details of difference: - - No reconstruction loss. - - No 1DConv. - - Use chebyshev polynomials to reduce time complexity. - - There is no the output layer composed of GLU and fully-connected (FC) sublayers as described in third paragraph in section 4.1. - - The experimental setting is not fair in StemGNN, and we can not reproduce the paper's performance. -""" class GLU(nn.Module): def __init__(self, input_channel, output_channel): @@ -27,6 +12,7 @@ def __init__(self, input_channel, output_channel): def forward(self, x): return torch.mul(self.linear_left(x), torch.sigmoid(self.linear_right(x))) + class StockBlockLayer(nn.Module): def __init__(self, time_step, unit, multi_layer, stack_cnt=0): super(StockBlockLayer, self).__init__() @@ -34,26 +20,36 @@ def __init__(self, time_step, unit, multi_layer, stack_cnt=0): self.unit = unit self.stack_cnt = stack_cnt self.multi = multi_layer - self.weight = nn.Parameter(torch.Tensor(1, 3 + 1, 1, self.time_step * self.multi, self.multi * self.time_step)) # [K+1, 1, in_c, out_c] + self.weight = nn.Parameter(torch.Tensor( + 1, 3 + 1, 1, self.time_step * self.multi, self.multi * self.time_step)) # [K+1, 1, in_c, out_c] nn.init.xavier_normal_(self.weight) - self.forecast = nn.Linear(self.time_step * self.multi, self.time_step * self.multi) - self.forecast_result = nn.Linear(self.time_step * self.multi, self.time_step) + self.forecast = nn.Linear( + self.time_step * self.multi, self.time_step * self.multi) + self.forecast_result = nn.Linear( + self.time_step * self.multi, self.time_step) if self.stack_cnt == 0: - self.backcast = nn.Linear(self.time_step * self.multi, self.time_step) + self.backcast = nn.Linear( + self.time_step * self.multi, self.time_step) self.backcast_short_cut = nn.Linear(self.time_step, self.time_step) self.relu = nn.ReLU() self.GLUs = nn.ModuleList() self.output_channel = 4 * self.multi for i in range(3): if i == 0: - self.GLUs.append(GLU(self.time_step * 4, self.time_step * self.output_channel)) - self.GLUs.append(GLU(self.time_step * 4, self.time_step * self.output_channel)) + self.GLUs.append( + GLU(self.time_step * 4, self.time_step * self.output_channel)) + self.GLUs.append( + GLU(self.time_step * 4, self.time_step * self.output_channel)) elif i == 1: - self.GLUs.append(GLU(self.time_step * self.output_channel, self.time_step * self.output_channel)) - self.GLUs.append(GLU(self.time_step * self.output_channel, self.time_step * self.output_channel)) + self.GLUs.append( + GLU(self.time_step * self.output_channel, self.time_step * self.output_channel)) + self.GLUs.append( + GLU(self.time_step * self.output_channel, self.time_step * self.output_channel)) else: - self.GLUs.append(GLU(self.time_step * self.output_channel, self.time_step * self.output_channel)) - self.GLUs.append(GLU(self.time_step * self.output_channel, self.time_step * self.output_channel)) + self.GLUs.append( + GLU(self.time_step * self.output_channel, self.time_step * self.output_channel)) + self.GLUs.append( + GLU(self.time_step * self.output_channel, self.time_step * self.output_channel)) def spe_seq_cell(self, input): batch_size, k, input_channel, node_cnt, time_step = input.size() @@ -62,15 +58,19 @@ def spe_seq_cell(self, input): ffted = torch.fft.fft(input, dim=-1) ffted_real = ffted.real ffted_imag = ffted.imag - ffted = torch.stack([ffted_real, ffted_imag], dim=-1) + ffted = torch.stack([ffted_real, ffted_imag], dim=-1) # ffted = torch.rfft(input, 1, onesided=False) - real = ffted[..., 0].permute(0, 2, 1, 3).contiguous().reshape(batch_size, node_cnt, -1) - img = ffted[..., 1].permute(0, 2, 1, 3).contiguous().reshape(batch_size, node_cnt, -1) + real = ffted[..., 0].permute( + 0, 2, 1, 3).contiguous().reshape(batch_size, node_cnt, -1) + img = ffted[..., 1].permute(0, 2, 1, 3).contiguous().reshape( + batch_size, node_cnt, -1) for i in range(3): real = self.GLUs[i * 2](real) img = self.GLUs[2 * i + 1](img) - real = real.reshape(batch_size, node_cnt, 4, -1).permute(0, 2, 1, 3).contiguous() - img = img.reshape(batch_size, node_cnt, 4, -1).permute(0, 2, 1, 3).contiguous() + real = real.reshape(batch_size, node_cnt, 4, - + 1).permute(0, 2, 1, 3).contiguous() + img = img.reshape(batch_size, node_cnt, 4, - + 1).permute(0, 2, 1, 3).contiguous() time_step_as_inner = torch.complex(real, img) iffted = torch.fft.ifft(time_step_as_inner, dim=-1).real # time_step_as_inner = torch.cat([real.unsqueeze(-1), img.unsqueeze(-1)], dim=-1) @@ -88,14 +88,30 @@ def forward(self, x, mul_L): forecast = self.forecast_result(forecast_source) if self.stack_cnt == 0: backcast_short = self.backcast_short_cut(x).squeeze(1) - backcast_source = torch.sigmoid(self.backcast(igfted) - backcast_short) + backcast_source = torch.sigmoid( + self.backcast(igfted) - backcast_short) else: backcast_source = None return forecast, backcast_source -@ARCH_REGISTRY.register() class StemGNN(nn.Module): + """ + Paper: Spectral Temporal Graph Neural Network for Multivariate Time-series Forecasting + Link: https://arxiv.org/abs/2103.07719 + Ref Official Code: https://github.com/microsoft/StemGNN + Note: + There are some difference in implementation described in the paper as well as the source code. + Details can be found in [here](https://github.com/microsoft/StemGNN/issues/12) + We adopt the implementation of the code. + Details of difference: + - No reconstruction loss. + - No 1DConv. + - Use chebyshev polynomials to reduce time complexity. + - There is no the output layer composed of GLU and fully-connected (FC) sublayers as described in third paragraph in section 4.1. + - The experimental setting is not fair in StemGNN, and we can not reproduce the paper's performance. + """ + def __init__(self, units, stack_cnt, time_step, multi_layer, horizon, dropout_rate=0.5, leaky_rate=0.2, **kwargs): super(StemGNN, self).__init__() self.unit = units @@ -122,33 +138,27 @@ def __init__(self, units, stack_cnt, time_step, multi_layer, horizon, dropout_ra self.dropout = nn.Dropout(p=dropout_rate) def get_laplacian(self, graph, normalize): - """ - return the laplacian of the graph. - :param graph: the graph structure without self loop, [N, N]. - :param normalize: whether to used the normalized laplacian. - :return: graph laplacian. - """ if normalize: D = torch.diag(torch.sum(graph, dim=-1) ** (-1 / 2)) - L = torch.eye(graph.size(0), device=graph.device, dtype=graph.dtype) - torch.mm(torch.mm(D, graph), D) + L = torch.eye(graph.size(0), device=graph.device, + dtype=graph.dtype) - torch.mm(torch.mm(D, graph), D) else: D = torch.diag(torch.sum(graph, dim=-1)) L = D - graph return L def cheb_polynomial(self, laplacian): - """ - Compute the Chebyshev Polynomial, according to the graph laplacian. - :param laplacian: the graph laplacian, [N, N]. - :return: the multi order Chebyshev laplacian, [K, N, N]. - """ N = laplacian.size(0) # [N, N] laplacian = laplacian.unsqueeze(0) - first_laplacian = torch.zeros([1, N, N], device=laplacian.device, dtype=torch.float) + first_laplacian = torch.zeros( + [1, N, N], device=laplacian.device, dtype=torch.float) second_laplacian = laplacian - third_laplacian = (2 * torch.matmul(laplacian, second_laplacian)) - first_laplacian - forth_laplacian = 2 * torch.matmul(laplacian, third_laplacian) - second_laplacian - multi_order_laplacian = torch.cat([first_laplacian, second_laplacian, third_laplacian, forth_laplacian], dim=0) + third_laplacian = ( + 2 * torch.matmul(laplacian, second_laplacian)) - first_laplacian + forth_laplacian = 2 * \ + torch.matmul(laplacian, third_laplacian) - second_laplacian + multi_order_laplacian = torch.cat( + [first_laplacian, second_laplacian, third_laplacian, forth_laplacian], dim=0) return multi_order_laplacian def latent_correlation_layer(self, x): @@ -182,16 +192,17 @@ def self_graph_attention(self, input): def graph_fft(self, input, eigenvectors): return torch.matmul(eigenvectors, input) - def forward(self, history_data, **kwargs): - """feedforward function of StemGNN. + def forward(self, history_data: torch.Tensor, future_data: torch.Tensor, batch_seen: int, epoch: int, train: bool, **kwargs) -> torch.Tensor: + """Feedforward function of StemGNN. Args: - history_data (torch.Tensor): [B, L, N] + history_data (torch.Tensor): [B, L, N, 1] Returns: torch.Tensor: [B, L, N, 1] """ - x = history_data + + x = history_data[..., 0] mul_L, attention = self.latent_correlation_layer(x) X = x.unsqueeze(1).permute(0, 1, 3, 2).contiguous() result = [] diff --git a/basicts/archs/arch_zoo/stgcn_arch/__init__.py b/basicts/archs/arch_zoo/stgcn_arch/__init__.py new file mode 100644 index 00000000..51e274b4 --- /dev/null +++ b/basicts/archs/arch_zoo/stgcn_arch/__init__.py @@ -0,0 +1,3 @@ +from .stgcn_arch import STGCNChebGraphConv as STGCN + +__all__ = ["STGCN"] diff --git a/basicts/archs/STGCN_arch/STGCN_arch.py b/basicts/archs/arch_zoo/stgcn_arch/stgcn_arch.py similarity index 74% rename from basicts/archs/STGCN_arch/STGCN_arch.py rename to basicts/archs/arch_zoo/stgcn_arch/stgcn_arch.py index 55bd8524..07d9b52a 100644 --- a/basicts/archs/STGCN_arch/STGCN_arch.py +++ b/basicts/archs/arch_zoo/stgcn_arch/stgcn_arch.py @@ -1,24 +1,23 @@ import torch import torch.nn as nn -from basicts.archs.STGCN_arch import STGCN_layers as layers -from basicts.archs.registry import ARCH_REGISTRY +from .stgcn_layers import STConvBlock, OutputBlock -""" + +class STGCNChebGraphConv(nn.Module): + """ Paper: Spatio-Temporal Graph Convolutional Networks: A Deep Learning Framework for Traffic Forecasting Official Code: https://github.com/VeritasYin/STGCN_IJCAI-18 (tensorflow) Ref Code: https://github.com/hazdzz/STGCN Note: https://github.com/hazdzz/STGCN/issues/9 -""" - + Link: https://arxiv.org/abs/1709.04875 + """ -@ARCH_REGISTRY.register() -class STGCN(nn.Module): # STGCNChebGraphConv contains 'TGTND TGTND TNFF' structure # ChebGraphConv is the graph convolution from ChebyNet. # Using the Chebyshev polynomials of the first kind as a graph filter. - + # T: Gated Temporal Convolution Layer (GLU or GTU) # G: Graph Convolution Layer (ChebGraphConv) # T: Gated Temporal Convolution Layer (GLU or GTU) @@ -37,17 +36,19 @@ class STGCN(nn.Module): # F: Fully-Connected Layer def __init__(self, Kt, Ks, blocks, T, n_vertex, act_func, graph_conv_type, gso, bias, droprate): - super(STGCN, self).__init__() + super(STGCNChebGraphConv, self).__init__() modules = [] for l in range(len(blocks) - 3): - modules.append(layers.STConvBlock(Kt, Ks, n_vertex, blocks[l][-1], blocks[l+1], act_func, graph_conv_type, gso, bias, droprate)) + modules.append(STConvBlock( + Kt, Ks, n_vertex, blocks[l][-1], blocks[l+1], act_func, graph_conv_type, gso, bias, droprate)) self.st_blocks = nn.Sequential(*modules) Ko = T - (len(blocks) - 3) * 2 * (Kt - 1) self.Ko = Ko assert Ko != 0, "Ko = 0." - self.output = layers.OutputBlock(Ko, blocks[-3][-1], blocks[-2], blocks[-1][0], n_vertex, act_func, bias, droprate) + self.output = OutputBlock( + Ko, blocks[-3][-1], blocks[-2], blocks[-1][0], n_vertex, act_func, bias, droprate) - def forward(self, history_data:torch.Tensor) -> torch.Tensor: + def forward(self, history_data: torch.Tensor, future_data: torch.Tensor, batch_seen: int, epoch: int, train: bool, **kwargs) -> torch.Tensor: """feedforward function of STGCN. Args: @@ -57,7 +58,7 @@ def forward(self, history_data:torch.Tensor) -> torch.Tensor: torch.Tensor: prediction with shape [B, L, N, C] """ x = history_data.permute(0, 3, 1, 2).contiguous() - + x = self.st_blocks(x) x = self.output(x) diff --git a/basicts/archs/STGCN_arch/STGCN_layers.py b/basicts/archs/arch_zoo/stgcn_arch/stgcn_layers.py similarity index 80% rename from basicts/archs/STGCN_arch/STGCN_layers.py rename to basicts/archs/arch_zoo/stgcn_arch/stgcn_layers.py index 9230c6f3..69e6d788 100644 --- a/basicts/archs/STGCN_arch/STGCN_layers.py +++ b/basicts/archs/arch_zoo/stgcn_arch/stgcn_layers.py @@ -1,73 +1,84 @@ import math + import torch import torch.nn as nn import torch.nn.functional as F import torch.nn.init as init + class Align(nn.Module): def __init__(self, c_in, c_out): super(Align, self).__init__() self.c_in = c_in self.c_out = c_out - self.align_conv = nn.Conv2d(in_channels=c_in, out_channels=c_out, kernel_size=(1, 1)) + self.align_conv = nn.Conv2d( + in_channels=c_in, out_channels=c_out, kernel_size=(1, 1)) def forward(self, x): if self.c_in > self.c_out: x = self.align_conv(x) elif self.c_in < self.c_out: batch_size, _, timestep, n_vertex = x.shape - x = torch.cat([x, torch.zeros([batch_size, self.c_out - self.c_in, timestep, n_vertex]).to(x)], dim=1) + x = torch.cat([x, torch.zeros( + [batch_size, self.c_out - self.c_in, timestep, n_vertex]).to(x)], dim=1) else: x = x - + return x + class CausalConv1d(nn.Conv1d): def __init__(self, in_channels, out_channels, kernel_size, stride=1, enable_padding=False, dilation=1, groups=1, bias=True): if enable_padding == True: self.__padding = (kernel_size - 1) * dilation else: self.__padding = 0 - super(CausalConv1d, self).__init__(in_channels, out_channels, kernel_size=kernel_size, stride=stride, padding=self.__padding, dilation=dilation, groups=groups, bias=bias) + super(CausalConv1d, self).__init__(in_channels, out_channels, kernel_size=kernel_size, + stride=stride, padding=self.__padding, dilation=dilation, groups=groups, bias=bias) def forward(self, input): result = super(CausalConv1d, self).forward(input) if self.__padding != 0: - return result[: , : , : -self.__padding] - + return result[:, :, : -self.__padding] + return result + class CausalConv2d(nn.Conv2d): def __init__(self, in_channels, out_channels, kernel_size, stride=1, enable_padding=False, dilation=1, groups=1, bias=True): kernel_size = nn.modules.utils._pair(kernel_size) stride = nn.modules.utils._pair(stride) dilation = nn.modules.utils._pair(dilation) if enable_padding == True: - self.__padding = [int((kernel_size[i] - 1) * dilation[i]) for i in range(len(kernel_size))] + self.__padding = [int((kernel_size[i] - 1) * dilation[i]) + for i in range(len(kernel_size))] else: self.__padding = 0 self.left_padding = nn.modules.utils._pair(self.__padding) - super(CausalConv2d, self).__init__(in_channels, out_channels, kernel_size, stride=stride, padding=0, dilation=dilation, groups=groups, bias=bias) - + super(CausalConv2d, self).__init__(in_channels, out_channels, kernel_size, + stride=stride, padding=0, dilation=dilation, groups=groups, bias=bias) + def forward(self, input): if self.__padding != 0: - input = F.pad(input, (self.left_padding[1], 0, self.left_padding[0], 0)) + input = F.pad( + input, (self.left_padding[1], 0, self.left_padding[0], 0)) result = super(CausalConv2d, self).forward(input) return result + class TemporalConvLayer(nn.Module): # Temporal Convolution Layer (GLU) # # |--------------------------------| * Residual Connection * # | | - # | |--->--- CasualConv2d ----- + -------| + # | |--->--- CasualConv2d ----- + -------| # -------|----| ⊙ ------> - # |--->--- CasualConv2d --- Sigmoid ---| + # |--->--- CasualConv2d --- Sigmoid ---| # - - #param x: tensor, [bs, c_in, ts, n_vertex] + + # param x: tensor, [bs, c_in, ts, n_vertex] def __init__(self, Kt, c_in, c_out, n_vertex, act_func): super(TemporalConvLayer, self).__init__() @@ -77,9 +88,11 @@ def __init__(self, Kt, c_in, c_out, n_vertex, act_func): self.n_vertex = n_vertex self.align = Align(c_in, c_out) if act_func == 'glu' or act_func == 'gtu': - self.causal_conv = CausalConv2d(in_channels=c_in, out_channels=2 * c_out, kernel_size=(Kt, 1), enable_padding=False, dilation=1) + self.causal_conv = CausalConv2d( + in_channels=c_in, out_channels=2 * c_out, kernel_size=(Kt, 1), enable_padding=False, dilation=1) else: - self.causal_conv = CausalConv2d(in_channels=c_in, out_channels=c_out, kernel_size=(Kt, 1), enable_padding=False, dilation=1) + self.causal_conv = CausalConv2d(in_channels=c_in, out_channels=c_out, kernel_size=( + Kt, 1), enable_padding=False, dilation=1) self.act_func = act_func self.sigmoid = nn.Sigmoid() self.tanh = nn.Tanh() @@ -87,7 +100,7 @@ def __init__(self, Kt, c_in, c_out, n_vertex, act_func): self.leaky_relu = nn.LeakyReLU() self.silu = nn.SiLU() - def forward(self, x): + def forward(self, x): x_in = self.align(x)[:, :, self.Kt - 1:, :] x_causal_conv = self.causal_conv(x) @@ -115,18 +128,20 @@ def forward(self, x): elif self.act_func == 'relu': x = self.relu(x_causal_conv + x_in) - + elif self.act_func == 'leaky_relu': x = self.leaky_relu(x_causal_conv + x_in) elif self.act_func == 'silu': x = self.silu(x_causal_conv + x_in) - + else: - raise NotImplementedError(f'ERROR: The activation function {self.act_func} is not implemented.') - + raise NotImplementedError( + f'ERROR: The activation function {self.act_func} is not implemented.') + return x + class ChebGraphConv(nn.Module): def __init__(self, c_in, c_out, Ks, gso, bias): super(ChebGraphConv, self).__init__() @@ -147,7 +162,7 @@ def reset_parameters(self): fan_in, _ = init._calculate_fan_in_and_fan_out(self.weight) bound = 1 / math.sqrt(fan_in) if fan_in > 0 else 0 init.uniform_(self.bias, -bound, bound) - + def forward(self, x): #bs, c_in, ts, n_vertex = x.shape x = torch.permute(x, (0, 2, 3, 1)) @@ -155,7 +170,8 @@ def forward(self, x): self.gso = self.gso.to(x.device) if self.Ks - 1 < 0: - raise ValueError(f'ERROR: the graph convolution kernel size Ks has to be a positive integer, but received {self.Ks}.') + raise ValueError( + f'ERROR: the graph convolution kernel size Ks has to be a positive integer, but received {self.Ks}.') elif self.Ks - 1 == 0: x_0 = x x_list = [x_0] @@ -168,8 +184,9 @@ def forward(self, x): x_1 = torch.einsum('hi,btij->bthj', self.gso, x) x_list = [x_0, x_1] for k in range(2, self.Ks): - x_list.append(torch.einsum('hi,btij->bthj', 2 * self.gso, x_list[k - 1]) - x_list[k - 2]) - + x_list.append(torch.einsum('hi,btij->bthj', 2 * + self.gso, x_list[k - 1]) - x_list[k - 2]) + x = torch.stack(x_list, dim=2) cheb_graph_conv = torch.einsum('btkhi,kij->bthj', x, self.weight) @@ -178,9 +195,10 @@ def forward(self, x): cheb_graph_conv = torch.add(cheb_graph_conv, self.bias) else: cheb_graph_conv = cheb_graph_conv - + return cheb_graph_conv + class GraphConv(nn.Module): def __init__(self, c_in, c_out, gso, bias): super(GraphConv, self).__init__() @@ -212,9 +230,10 @@ def forward(self, x): graph_conv = torch.add(second_mul, self.bias) else: graph_conv = second_mul - + return graph_conv + class GraphConvLayer(nn.Module): def __init__(self, graph_conv_type, c_in, c_out, Ks, gso, bias): super(GraphConvLayer, self).__init__() @@ -240,6 +259,7 @@ def forward(self, x): return x_gc_out + class STConvBlock(nn.Module): # STConv Block contains 'TGTND' structure # T: Gated Temporal Convolution Layer (GLU or GTU) @@ -250,9 +270,12 @@ class STConvBlock(nn.Module): def __init__(self, Kt, Ks, n_vertex, last_block_channel, channels, act_func, graph_conv_type, gso, bias, droprate): super(STConvBlock, self).__init__() - self.tmp_conv1 = TemporalConvLayer(Kt, last_block_channel, channels[0], n_vertex, act_func) - self.graph_conv = GraphConvLayer(graph_conv_type, channels[0], channels[1], Ks, gso, bias) - self.tmp_conv2 = TemporalConvLayer(Kt, channels[1], channels[2], n_vertex, act_func) + self.tmp_conv1 = TemporalConvLayer( + Kt, last_block_channel, channels[0], n_vertex, act_func) + self.graph_conv = GraphConvLayer( + graph_conv_type, channels[0], channels[1], Ks, gso, bias) + self.tmp_conv2 = TemporalConvLayer( + Kt, channels[1], channels[2], n_vertex, act_func) self.tc2_ln = nn.LayerNorm([n_vertex, channels[2]]) self.relu = nn.ReLU() self.dropout = nn.Dropout(p=droprate) @@ -267,6 +290,7 @@ def forward(self, x): return x + class OutputBlock(nn.Module): # Output block contains 'TNFF' structure # T: Gated Temporal Convolution Layer (GLU or GTU) @@ -276,9 +300,12 @@ class OutputBlock(nn.Module): def __init__(self, Ko, last_block_channel, channels, end_channel, n_vertex, act_func, bias, droprate): super(OutputBlock, self).__init__() - self.tmp_conv1 = TemporalConvLayer(Ko, last_block_channel, channels[0], n_vertex, act_func) - self.fc1 = nn.Linear(in_features=channels[0], out_features=channels[1], bias=bias) - self.fc2 = nn.Linear(in_features=channels[1], out_features=end_channel, bias=bias) + self.tmp_conv1 = TemporalConvLayer( + Ko, last_block_channel, channels[0], n_vertex, act_func) + self.fc1 = nn.Linear( + in_features=channels[0], out_features=channels[1], bias=bias) + self.fc2 = nn.Linear( + in_features=channels[1], out_features=end_channel, bias=bias) self.tc1_ln = nn.LayerNorm([n_vertex, channels[0]]) self.relu = nn.ReLU() self.leaky_relu = nn.LeakyReLU() @@ -292,4 +319,4 @@ def forward(self, x): x = self.relu(x) x = self.fc2(x).permute(0, 3, 1, 2) - return x \ No newline at end of file + return x diff --git a/basicts/archs/arch_zoo/stid_arch/__init__.py b/basicts/archs/arch_zoo/stid_arch/__init__.py new file mode 100644 index 00000000..64b16477 --- /dev/null +++ b/basicts/archs/arch_zoo/stid_arch/__init__.py @@ -0,0 +1,3 @@ +from .stid_arch import STID + +__all__ = ["STID"] diff --git a/basicts/archs/arch_zoo/stid_arch/mlp.py b/basicts/archs/arch_zoo/stid_arch/mlp.py new file mode 100644 index 00000000..17fccbc1 --- /dev/null +++ b/basicts/archs/arch_zoo/stid_arch/mlp.py @@ -0,0 +1,29 @@ +import torch +from torch import nn + + +class MultiLayerPerceptron(nn.Module): + """Multi-Layer Perceptron with residual links.""" + + def __init__(self, input_dim, hidden_dim) -> None: + super().__init__() + self.fc1 = nn.Conv2d( + in_channels=input_dim, out_channels=hidden_dim, kernel_size=(1, 1), bias=True) + self.fc2 = nn.Conv2d( + in_channels=hidden_dim, out_channels=hidden_dim, kernel_size=(1, 1), bias=True) + self.act = nn.ReLU() + self.drop = nn.Dropout(p=0.15) + + def forward(self, input_data: torch.Tensor) -> torch.Tensor: + """Feed forward of MLP. + + Args: + input_data (torch.Tensor): input data with shape [B, D, N] + + Returns: + torch.Tensor: latent repr + """ + + hidden = self.fc2(self.drop(self.act(self.fc1(input_data)))) # MLP + hidden = hidden + input_data # residual + return hidden diff --git a/basicts/archs/arch_zoo/stid_arch/stid_arch.py b/basicts/archs/arch_zoo/stid_arch/stid_arch.py new file mode 100644 index 00000000..9b52c1d6 --- /dev/null +++ b/basicts/archs/arch_zoo/stid_arch/stid_arch.py @@ -0,0 +1,115 @@ +import torch +from torch import nn + +from .mlp import MultiLayerPerceptron + + +class STID(nn.Module): + """ + The implementation of CIKM 2022 short paper + "Spatial-Temporal Identity: A Simple yet Effective Baseline for Multivariate Time Series Forecasting" + Link: https://arxiv.org/abs/2208.05233 + """ + + def __init__(self, **model_args): + super().__init__() + # attributes + self.num_nodes = model_args["num_nodes"] + self.node_dim = model_args["node_dim"] + self.input_len = model_args["input_len"] + self.input_dim = model_args["input_dim"] + self.embed_dim = model_args["embed_dim"] + self.output_len = model_args["output_len"] + self.num_layer = model_args["num_layer"] + self.temp_dim_tid = model_args["temp_dim_tid"] + self.temp_dim_diw = model_args["temp_dim_diw"] + + self.if_time_in_day = model_args["if_T_i_D"] + self.if_day_in_week = model_args["if_D_i_W"] + self.if_spatial = model_args["if_node"] + + # spatial embeddings + if self.if_spatial: + self.node_emb = nn.Parameter( + torch.empty(self.num_nodes, self.node_dim)) + nn.init.xavier_uniform_(self.node_emb) + # temporal embeddings + if self.if_time_in_day: + self.time_in_day_emb = nn.Parameter( + torch.empty(288, self.temp_dim_tid)) + nn.init.xavier_uniform_(self.time_in_day_emb) + if self.if_day_in_week: + self.day_in_week_emb = nn.Parameter( + torch.empty(7, self.temp_dim_diw)) + nn.init.xavier_uniform_(self.day_in_week_emb) + + # embedding layer + self.time_series_emb_layer = nn.Conv2d( + in_channels=self.input_dim * self.input_len, out_channels=self.embed_dim, kernel_size=(1, 1), bias=True) + + # encoding + self.hidden_dim = self.embed_dim+self.node_dim * \ + int(self.if_spatial)+self.temp_dim_tid*int(self.if_day_in_week) + \ + self.temp_dim_diw*int(self.if_time_in_day) + self.encoder = nn.Sequential( + *[MultiLayerPerceptron(self.hidden_dim, self.hidden_dim) for _ in range(self.num_layer)]) + + # regression + self.regression_layer = nn.Conv2d( + in_channels=self.hidden_dim, out_channels=self.output_len, kernel_size=(1, 1), bias=True) + + def forward(self, history_data: torch.Tensor, future_data: torch.Tensor, batch_seen: int, epoch: int, train: bool, **kwargs) -> torch.Tensor: + """Feed forward of STID. + + Args: + history_data (torch.Tensor): history data with shape [B, L, N, C] + + Returns: + torch.Tensor: prediction wit shape [B, L, N, C] + """ + + # prepare data + input_data = history_data[..., range(self.input_dim)] + + if self.if_time_in_day: + t_i_d_data = history_data[..., 1] + time_in_day_emb = self.time_in_day_emb[( + t_i_d_data[:, -1, :] * 288).type(torch.LongTensor)] + else: + time_in_day_emb = None + if self.if_day_in_week: + d_i_w_data = history_data[..., 2] + day_in_week_emb = self.day_in_week_emb[( + d_i_w_data[:, -1, :]).type(torch.LongTensor)] + else: + day_in_week_emb = None + + # time series embedding + batch_size, _, num_nodes, _ = input_data.shape + input_data = input_data.transpose(1, 2).contiguous() + input_data = input_data.view( + batch_size, num_nodes, -1).transpose(1, 2).unsqueeze(-1) + time_series_emb = self.time_series_emb_layer(input_data) + + node_emb = [] + if self.if_spatial: + # expand node embeddings + node_emb.append(self.node_emb.unsqueeze(0).expand( + batch_size, -1, -1).transpose(1, 2).unsqueeze(-1)) + # temporal embeddings + tem_emb = [] + if time_in_day_emb is not None: + tem_emb.append(time_in_day_emb.transpose(1, 2).unsqueeze(-1)) + if day_in_week_emb is not None: + tem_emb.append(day_in_week_emb.transpose(1, 2).unsqueeze(-1)) + + # concate all embeddings + hidden = torch.cat([time_series_emb] + node_emb + tem_emb, dim=1) + + # encoding + hidden = self.encoder(hidden) + + # regression + prediction = self.regression_layer(hidden) + + return prediction diff --git a/basicts/archs/arch_zoo/stnorm_arch/__init__.py b/basicts/archs/arch_zoo/stnorm_arch/__init__.py new file mode 100644 index 00000000..fdc70419 --- /dev/null +++ b/basicts/archs/arch_zoo/stnorm_arch/__init__.py @@ -0,0 +1,3 @@ +from .stnorm_arch import STNorm + +__all__ = ["STNorm"] diff --git a/basicts/archs/STNorm_arch/STNorm_arch.py b/basicts/archs/arch_zoo/stnorm_arch/stnorm_arch.py similarity index 72% rename from basicts/archs/STNorm_arch/STNorm_arch.py rename to basicts/archs/arch_zoo/stnorm_arch/stnorm_arch.py index 62a9797e..81465ee4 100644 --- a/basicts/archs/STNorm_arch/STNorm_arch.py +++ b/basicts/archs/arch_zoo/stnorm_arch/stnorm_arch.py @@ -1,15 +1,8 @@ import torch import torch.nn as nn import torch.nn.functional as F -from torch.autograd import Variable from torch.nn import Parameter -import sys -from basicts.archs.registry import ARCH_REGISTRY -""" - Paper: ST-Norm: Spatial and Temporal Normalization for Multi-variate Time Series Forecasting - Ref Official Code: https://github.com/JLDeng/ST-Norm/blob/master/models/Wavenet.py -""" class SNorm(nn.Module): def __init__(self, channels): @@ -18,19 +11,24 @@ def __init__(self, channels): self.gamma = nn.Parameter(torch.ones(channels)) def forward(self, x): - x_norm = (x - x.mean(2, keepdims=True)) / (x.var(2, keepdims=True, unbiased=True) + 0.00001) ** 0.5 + x_norm = (x - x.mean(2, keepdims=True)) / \ + (x.var(2, keepdims=True, unbiased=True) + 0.00001) ** 0.5 - out = x_norm * self.gamma.view(1, -1, 1, 1) + self.beta.view(1, -1, 1, 1) + out = x_norm * self.gamma.view(1, -1, 1, 1) + \ + self.beta.view(1, -1, 1, 1) return out + class TNorm(nn.Module): def __init__(self, num_nodes, channels, track_running_stats=True, momentum=0.1): super(TNorm, self).__init__() self.track_running_stats = track_running_stats self.beta = nn.Parameter(torch.zeros(1, channels, num_nodes, 1)) self.gamma = nn.Parameter(torch.ones(1, channels, num_nodes, 1)) - self.register_buffer('running_mean', torch.zeros(1, channels, num_nodes, 1)) - self.register_buffer('running_var', torch.ones(1, channels, num_nodes, 1)) + self.register_buffer( + 'running_mean', torch.zeros(1, channels, num_nodes, 1)) + self.register_buffer( + 'running_var', torch.ones(1, channels, num_nodes, 1)) self.momentum = momentum def forward(self, x): @@ -40,8 +38,10 @@ def forward(self, x): if self.training: n = x.shape[3] * x.shape[0] with torch.no_grad(): - self.running_mean = self.momentum * mean + (1 - self.momentum) * self.running_mean - self.running_var = self.momentum * var * n / (n - 1) + (1 - self.momentum) * self.running_var + self.running_mean = self.momentum * mean + \ + (1 - self.momentum) * self.running_mean + self.running_var = self.momentum * var * n / \ + (n - 1) + (1 - self.momentum) * self.running_var else: mean = self.running_mean var = self.running_var @@ -53,9 +53,14 @@ def forward(self, x): return out -@ARCH_REGISTRY.register() class STNorm(nn.Module): - def __init__(self, num_nodes, tnorm_bool, snorm_bool, in_dim,out_dim, channels,kernel_size,blocks,layers): + """ + Paper: ST-Norm: Spatial and Temporal Normalization for Multi-variate Time Series Forecasting + Link: https://dl.acm.org/doi/10.1145/3447548.3467330 + Ref Official Code: https://github.com/JLDeng/ST-Norm/blob/master/models/Wavenet.py + """ + + def __init__(self, num_nodes, tnorm_bool, snorm_bool, in_dim, out_dim, channels, kernel_size, blocks, layers): super(STNorm, self).__init__() self.blocks = blocks self.layers = layers @@ -79,7 +84,7 @@ def __init__(self, num_nodes, tnorm_bool, snorm_bool, in_dim,out_dim, channels,k self.start_conv = nn.Conv2d(in_channels=in_dim, out_channels=channels, - kernel_size=(1,1)) + kernel_size=(1, 1)) receptive_field = 1 self.dropout = nn.Dropout(0.2) @@ -96,30 +101,35 @@ def __init__(self, num_nodes, tnorm_bool, snorm_bool, in_dim,out_dim, channels,k self.tn.append(TNorm(num_nodes, channels)) if self.snorm_bool: self.sn.append(SNorm(channels)) - self.filter_convs.append(nn.Conv2d(in_channels=num * channels, out_channels=channels, kernel_size=(1,kernel_size),dilation=new_dilation)) + self.filter_convs.append(nn.Conv2d( + in_channels=num * channels, out_channels=channels, kernel_size=(1, kernel_size), dilation=new_dilation)) - self.gate_convs.append(nn.Conv1d(in_channels=num * channels, out_channels=channels, kernel_size=(1, kernel_size), dilation=new_dilation)) + self.gate_convs.append(nn.Conv1d( + in_channels=num * channels, out_channels=channels, kernel_size=(1, kernel_size), dilation=new_dilation)) # 1x1 convolution for residual connection - self.residual_convs.append(nn.Conv1d(in_channels=channels, out_channels=channels, kernel_size=(1, 1))) + self.residual_convs.append( + nn.Conv1d(in_channels=channels, out_channels=channels, kernel_size=(1, 1))) # 1x1 convolution for skip connection - self.skip_convs.append(nn.Conv1d(in_channels=channels, out_channels=channels, kernel_size=(1, 1))) - new_dilation *=2 + self.skip_convs.append( + nn.Conv1d(in_channels=channels, out_channels=channels, kernel_size=(1, 1))) + new_dilation *= 2 receptive_field += additional_scope additional_scope *= 2 - self.end_conv_1 = nn.Conv2d(in_channels=channels, out_channels=channels, kernel_size=(1,1), bias=True) + self.end_conv_1 = nn.Conv2d( + in_channels=channels, out_channels=channels, kernel_size=(1, 1), bias=True) self.end_conv_2 = nn.Conv2d(in_channels=channels, out_channels=out_dim, - kernel_size=(1,1), + kernel_size=(1, 1), bias=True) self.receptive_field = receptive_field - def forward(self, history_data, **kwargs): - """feedforward function of STNorm. + def forward(self, history_data: torch.Tensor, future_data: torch.Tensor, batch_seen: int, epoch: int, train: bool, **kwargs) -> torch.Tensor: + """Feedforward function of STNorm. Args: history_data (torch.Tensor): shape [B, C, N, L] @@ -127,10 +137,12 @@ def forward(self, history_data, **kwargs): Returns: torch.Tensor: [B, L, N, 1] """ + input = history_data.transpose(1, 3).contiguous() in_len = input.size(3) - if in_len None: - super().__init__() - assert mode in ['train', 'valid', 'test'], "error mode" - # read raw data (normalized) - data = load_pkl(raw_file_path) - processed_data = data['processed_data'] - self.data = torch.from_numpy(processed_data).float() # L, N, C - # read index - self.index = load_pkl(index_file_path)[mode] - - def __getitem__(self, index: int) -> tuple: - """get a sample. - - Args: - index (int): the iteration index (not the self.index) - - Returns: - tuple: (future_data, history_data), where the shape of each is L x N x C. - """ - idx = list(self.index[index]) - if isinstance(idx[0], int): - # continuous index - history_data = self.data[idx[0]:idx[1]] - future_data = self.data[idx[1]:idx[2]] - else: - # discontinuous index or custom index - # NOTE: current time $t$ should not included in the index[0] - history_index = idx[0] # list - assert idx[1] not in history_index, "current time t should not included in the idx[0]" - history_index.append(idx[1]) - history_data = self.data[history_index] - future_data = self.data[idx[1], idx[2]] - - return future_data, history_data - - def __len__(self): - """dataset length - - Returns: - int: dataset length - """ - return len(self.index) diff --git a/basicts/data/dataset.py b/basicts/data/dataset.py new file mode 100644 index 00000000..42731a2c --- /dev/null +++ b/basicts/data/dataset.py @@ -0,0 +1,73 @@ +import os + +import torch +from torch.utils.data import Dataset + +from ..utils import load_pkl + + +class TimeSeriesForecastingDataset(Dataset): + """Time series forecasting dataset.""" + + def __init__(self, data_file_path: str, index_file_path: str, mode: str) -> None: + super().__init__() + assert mode in ["train", "valid", "test"], "error mode" + self._check_if_file_exists(data_file_path, index_file_path) + # read raw data (normalized) + data = load_pkl(data_file_path) + processed_data = data["processed_data"] + self.data = torch.from_numpy(processed_data).float() + # read index + self.index = load_pkl(index_file_path)[mode] + + def _check_if_file_exists(self, data_file_path: str, index_file_path: str): + """Check if data file and index file exist. + + Args: + data_file_path (str): data file path + index_file_path (str): index file path + + Raises: + FileNotFoundError: no data file + FileNotFoundError: no index file + """ + + if not os.path.isfile(data_file_path): + raise FileNotFoundError("BasicTS can not find data file {0}".format(data_file_path)) + if not os.path.isfile(index_file_path): + raise FileNotFoundError("BasicTS can not find index file {0}".format(index_file_path)) + + def __getitem__(self, index: int) -> tuple: + """Get a sample. + + Args: + index (int): the iteration index (not the self.index) + + Returns: + tuple: (future_data, history_data), where the shape of each is L x N x C. + """ + + idx = list(self.index[index]) + if isinstance(idx[0], int): + # continuous index + history_data = self.data[idx[0]:idx[1]] + future_data = self.data[idx[1]:idx[2]] + else: + # discontinuous index or custom index + # NOTE: current time $t$ should not included in the index[0] + history_index = idx[0] # list + assert idx[1] not in history_index, "current time t should not included in the idx[0]" + history_index.append(idx[1]) + history_data = self.data[history_index] + future_data = self.data[idx[1], idx[2]] + + return future_data, history_data + + def __len__(self): + """Dataset length + + Returns: + int: dataset length + """ + + return len(self.index) diff --git a/basicts/archs/registry.py b/basicts/data/registry.py similarity index 55% rename from basicts/archs/registry.py rename to basicts/data/registry.py index bc06f826..826969df 100644 --- a/basicts/archs/registry.py +++ b/basicts/data/registry.py @@ -1,3 +1,3 @@ from easytorch.utils.registry import Registry -ARCH_REGISTRY = Registry('Arch') +SCALER_REGISTRY = Registry("Scaler") diff --git a/basicts/data/transform.py b/basicts/data/transform.py new file mode 100644 index 00000000..ec97a629 --- /dev/null +++ b/basicts/data/transform.py @@ -0,0 +1,56 @@ +import pickle + +import torch +import numpy as np + +from .registry import SCALER_REGISTRY + + +@SCALER_REGISTRY.register() +def standard_transform(data: np.array, output_dir: str, train_index: list) -> np.array: + """Standard normalization. + + Args: + data (np.array): raw time series data. + output_dir (str): output dir path. + train_index (list): train index. + + Returns: + np.array: normalized raw time series data. + """ + + # data: L, N, C + data_train = data[:train_index[-1][1], ...] + + mean, std = data_train[..., 0].mean(), data_train[..., 0].std() + + print("mean (training data):", mean) + print("std (training data):", std) + scaler = {} + scaler["func"] = re_standard_transform.__name__ + scaler["args"] = {"mean": mean, "std": std} + with open(output_dir + "/scaler.pkl", "wb") as f: + pickle.dump(scaler, f) + + def normalize(x): + return (x - mean) / std + + data_norm = normalize(data) + return data_norm + + +@SCALER_REGISTRY.register() +def re_standard_transform(data: torch.Tensor, **kwargs) -> torch.Tensor: + """Standard re-transformation. + + Args: + data (torch.Tensor): input data. + + Returns: + torch.Tensor: re-scaled data. + """ + + mean, std = kwargs["mean"], kwargs["std"] + data = data * std + data = data + mean + return data diff --git a/basicts/data/transforms.py b/basicts/data/transforms.py deleted file mode 100644 index b7d43095..00000000 --- a/basicts/data/transforms.py +++ /dev/null @@ -1,25 +0,0 @@ -from easytorch.utils.registry import Registry - -SCALER_REGISTRY = Registry('Scaler') - -""" -data normalization and re-normalization -""" - -# ====================================== re-normalizations ====================================== # -@SCALER_REGISTRY.register() -def re_max_min_normalization(x, **kwargs): - _min, _max = kwargs['min'], kwargs['max'] - x = (x + 1.) / 2. - x = 1. * x * (_max - _min) + _min - return x - -@SCALER_REGISTRY.register() -def standard_re_transform(x, **kwargs): - mean, std = kwargs['mean'], kwargs['std'] - x = x * std - x = x + mean - return x - -# ====================================== normalizations ====================================== # -# omitted to avoid redundancy, as they should only be used in data preprocessing in `scripts/data_preparation` diff --git a/basicts/launcher.py b/basicts/launcher.py new file mode 100644 index 00000000..2c7b639e --- /dev/null +++ b/basicts/launcher.py @@ -0,0 +1,19 @@ +from typing import Dict, Union + +import easytorch + +def launch_training(cfg: Union[Dict, str], gpus: str = None, node_rank: int = 0): + """Extended easytorch launch_training. + + Args: + cfg (Union[Dict, str]): Easytorch config. + gpus (str): set ``CUDA_VISIBLE_DEVICES`` environment variable. + node_rank (int): Rank of the current node. + """ + + # pre-processing of some possible future features, such as: + # registering model, runners. + # config checking + pass + # launch training based on easytorch + easytorch.launch_training(cfg=cfg, gpus=gpus, node_rank=node_rank) diff --git a/basicts/losses/__init__.py b/basicts/losses/__init__.py new file mode 100644 index 00000000..39d7c0a2 --- /dev/null +++ b/basicts/losses/__init__.py @@ -0,0 +1,4 @@ +from .losses import l1_loss, l2_loss +from ..metrics import masked_mae, masked_mape, masked_rmse, masked_mse + +__all__ = ["l1_loss", "l2_loss", "masked_mae", "masked_mape", "masked_rmse", "masked_mse"] diff --git a/basicts/losses/losses.py b/basicts/losses/losses.py index eab0a778..1691225c 100644 --- a/basicts/losses/losses.py +++ b/basicts/losses/losses.py @@ -1,12 +1,17 @@ -from basicts.metrics.mae import masked_mae as masked_l1_loss -from basicts.utils.misc import check_nan_inf -import torch import torch.nn.functional as F -def L1Loss(input, target, **kwargs): - return F.l1_loss(input, target) +from ..utils import check_nan_inf -def MSELoss(input, target, **kwargs): - check_nan_inf(input) - check_nan_inf(target) - return F.mse_loss(input, target) \ No newline at end of file + +def l1_loss(input_data, target_data): + """unmasked mae.""" + + return F.l1_loss(input_data, target_data) + + +def l2_loss(input_data, target_data): + """unmasked mse""" + + check_nan_inf(input_data) + check_nan_inf(target_data) + return F.mse_loss(input_data, target_data) diff --git a/basicts/metrics/__init__.py b/basicts/metrics/__init__.py new file mode 100644 index 00000000..5e69753e --- /dev/null +++ b/basicts/metrics/__init__.py @@ -0,0 +1,5 @@ +from .mae import masked_mae +from .mape import masked_mape +from .rmse import masked_rmse, masked_mse + +__all__ = ["masked_mae", "masked_mape", "masked_rmse", "masked_mse"] diff --git a/basicts/metrics/mae.py b/basicts/metrics/mae.py index 32601d8a..5901f7d2 100644 --- a/basicts/metrics/mae.py +++ b/basicts/metrics/mae.py @@ -1,9 +1,9 @@ import torch import numpy as np -# ============== MAE ================= # + def masked_mae(preds: torch.Tensor, labels: torch.Tensor, null_val: float = np.nan) -> torch.Tensor: - """masked mean absolute error. + """Masked mean absolute error. Args: preds (torch.Tensor): predicted values @@ -13,12 +13,13 @@ def masked_mae(preds: torch.Tensor, labels: torch.Tensor, null_val: float = np.n Returns: torch.Tensor: masked mean absolute error """ + if np.isnan(null_val): mask = ~torch.isnan(labels) else: - mask = (labels!=null_val) + mask = (labels != null_val) mask = mask.float() - mask /= torch.mean((mask)) + mask /= torch.mean((mask)) mask = torch.where(torch.isnan(mask), torch.zeros_like(mask), mask) loss = torch.abs(preds-labels) loss = loss * mask diff --git a/basicts/metrics/mape.py b/basicts/metrics/mape.py index f31a7f8a..2d59ef44 100644 --- a/basicts/metrics/mape.py +++ b/basicts/metrics/mape.py @@ -1,9 +1,8 @@ import torch import numpy as np -# ============== MAPE ================== # def masked_mape(preds: torch.Tensor, labels: torch.Tensor, null_val: float = np.nan) -> torch.Tensor: - """masked mean absolute percentage error. + """Masked mean absolute percentage error. Args: preds (torch.Tensor): predicted values @@ -13,14 +12,15 @@ def masked_mape(preds: torch.Tensor, labels: torch.Tensor, null_val: float = np. Returns: torch.Tensor: masked mean absolute percentage error """ + # fix very small values of labels, which should be 0. Otherwise, nan detector will fail. - labels = torch.where(labels<1e-2, torch.zeros_like(labels), labels) + labels = torch.where(labels < 1e-2, torch.zeros_like(labels), labels) if np.isnan(null_val): mask = ~torch.isnan(labels) else: - mask = (labels!=null_val) + mask = (labels != null_val) mask = mask.float() - mask /= torch.mean((mask)) + mask /= torch.mean((mask)) mask = torch.where(torch.isnan(mask), torch.zeros_like(mask), mask) loss = torch.abs(preds-labels)/labels loss = loss * mask diff --git a/basicts/metrics/rmse.py b/basicts/metrics/rmse.py index 8def1b65..31370630 100644 --- a/basicts/metrics/rmse.py +++ b/basicts/metrics/rmse.py @@ -1,9 +1,8 @@ import torch import numpy as np -# ============== RMSE ================= # def masked_mse(preds: torch.Tensor, labels: torch.Tensor, null_val: float = np.nan) -> torch.Tensor: - """masked mean squared error. + """Masked mean squared error. Args: preds (torch.Tensor): predicted values @@ -13,10 +12,11 @@ def masked_mse(preds: torch.Tensor, labels: torch.Tensor, null_val: float = np.n Returns: torch.Tensor: masked mean squared error """ + if np.isnan(null_val): mask = ~torch.isnan(labels) else: - mask = (labels!=null_val) + mask = (labels != null_val) mask = mask.float() mask /= torch.mean((mask)) mask = torch.where(torch.isnan(mask), torch.zeros_like(mask), mask) @@ -36,4 +36,5 @@ def masked_rmse(preds: torch.Tensor, labels: torch.Tensor, null_val: float = np. Returns: torch.Tensor: root mean squared error """ + return torch.sqrt(masked_mse(preds=preds, labels=labels, null_val=null_val)) diff --git a/basicts/options/AGCRN/AGCRN_Electricity336.py b/basicts/options/AGCRN/AGCRN_Electricity336.py deleted file mode 100644 index d3098a91..00000000 --- a/basicts/options/AGCRN/AGCRN_Electricity336.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.AGCRN_runner import AGCRNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import L1Loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'AGCRN model configuration' -CFG.RUNNER = AGCRNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "Electricity336" -CFG.DATASET_TYPE = 'Electricity consumption' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'AGCRN' -CFG.MODEL.PARAM = { - "num_nodes" : 336, - "input_dim" : 1, - "rnn_units" : 32, - "output_dim": 1, - "horizon" : 12, - "num_layers": 2, - "default_graph": True, - "embed_dim" : 2, - "cheb_k" : 2 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = L1Loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, -} -# CFG.TRAIN.LR_SCHEDULER = EasyDict() -# CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -# CFG.TRAIN.LR_SCHEDULER.PARAM= { -# "milestones":[5, 20, 40, 70], -# "gamma":0.3 -# } - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 12 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/AGCRN/AGCRN_METR-LA.py b/basicts/options/AGCRN/AGCRN_METR-LA.py deleted file mode 100644 index de6041b6..00000000 --- a/basicts/options/AGCRN/AGCRN_METR-LA.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.AGCRN_runner import AGCRNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'AGCRN model configuration' -CFG.RUNNER = AGCRNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "METR-LA" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'AGCRN' -CFG.MODEL.PARAM = { - "num_nodes" : 207, - "input_dim" : 2, - "rnn_units" : 64, - "output_dim": 1, - "horizon" : 12, - "num_layers": 2, - "default_graph": True, - "embed_dim" : 10, - "cheb_k" : 2 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, -} -# CFG.TRAIN.LR_SCHEDULER = EasyDict() -# CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -# CFG.TRAIN.LR_SCHEDULER.PARAM= { -# "milestones":[5, 20, 40, 70], -# "gamma":0.3 -# } - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/AGCRN/AGCRN_PEMS-BAY.py b/basicts/options/AGCRN/AGCRN_PEMS-BAY.py deleted file mode 100644 index f3ff316f..00000000 --- a/basicts/options/AGCRN/AGCRN_PEMS-BAY.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.AGCRN_runner import AGCRNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'AGCRN model configuration' -CFG.RUNNER = AGCRNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS-BAY" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'AGCRN' -CFG.MODEL.PARAM = { - "num_nodes" : 325, - "input_dim" : 2, - "rnn_units" : 64, - "output_dim": 1, - "horizon" : 12, - "num_layers": 2, - "default_graph": True, - "embed_dim" : 10, - "cheb_k" : 2 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, -} -# CFG.TRAIN.LR_SCHEDULER = EasyDict() -# CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -# CFG.TRAIN.LR_SCHEDULER.PARAM= { -# "milestones":[5, 20, 40, 70], -# "gamma":0.3 -# } - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/AGCRN/AGCRN_PEMS03.py b/basicts/options/AGCRN/AGCRN_PEMS03.py deleted file mode 100644 index 8b08a15e..00000000 --- a/basicts/options/AGCRN/AGCRN_PEMS03.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.AGCRN_runner import AGCRNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import L1Loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'AGCRN model configuration' -CFG.RUNNER = AGCRNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS03" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'AGCRN' -CFG.MODEL.PARAM = { - "num_nodes" : 358, - "input_dim" : 1, - "rnn_units" : 64, - "output_dim": 1, - "horizon" : 12, - "num_layers": 2, - "default_graph": True, - "embed_dim" : 10, - "cheb_k" : 2 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = L1Loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, -} -# CFG.TRAIN.LR_SCHEDULER = EasyDict() -# CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -# CFG.TRAIN.LR_SCHEDULER.PARAM= { -# "milestones":[5, 20, 40, 70], -# "gamma":0.3 -# } - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/AGCRN/AGCRN_PEMS04.py b/basicts/options/AGCRN/AGCRN_PEMS04.py deleted file mode 100644 index 95c5bd38..00000000 --- a/basicts/options/AGCRN/AGCRN_PEMS04.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.AGCRN_runner import AGCRNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import L1Loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'AGCRN model configuration' -CFG.RUNNER = AGCRNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS04" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'AGCRN' -CFG.MODEL.PARAM = { - "num_nodes" : 307, - "input_dim" : 1, - "rnn_units" : 64, - "output_dim": 1, - "horizon" : 12, - "num_layers": 2, - "default_graph": True, - "embed_dim" : 10, - "cheb_k" : 2 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = L1Loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, -} -# CFG.TRAIN.LR_SCHEDULER = EasyDict() -# CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -# CFG.TRAIN.LR_SCHEDULER.PARAM= { -# "milestones":[5, 20, 40, 70], -# "gamma":0.3 -# } - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/AGCRN/AGCRN_PEMS07.py b/basicts/options/AGCRN/AGCRN_PEMS07.py deleted file mode 100644 index fe9a06ca..00000000 --- a/basicts/options/AGCRN/AGCRN_PEMS07.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.AGCRN_runner import AGCRNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import L1Loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'AGCRN model configuration' -CFG.RUNNER = AGCRNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS07" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'AGCRN' -CFG.MODEL.PARAM = { - "num_nodes" : 883, - "input_dim" : 1, - "rnn_units" : 64, - "output_dim": 1, - "horizon" : 12, - "num_layers": 2, - "default_graph": True, - "embed_dim" : 10, - "cheb_k" : 2 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = L1Loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, -} -# CFG.TRAIN.LR_SCHEDULER = EasyDict() -# CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -# CFG.TRAIN.LR_SCHEDULER.PARAM= { -# "milestones":[5, 20, 40, 70], -# "gamma":0.3 -# } - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/AGCRN/AGCRN_PEMS08.py b/basicts/options/AGCRN/AGCRN_PEMS08.py deleted file mode 100644 index 4feec604..00000000 --- a/basicts/options/AGCRN/AGCRN_PEMS08.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.AGCRN_runner import AGCRNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import L1Loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'AGCRN model configuration' -CFG.RUNNER = AGCRNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS08" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'AGCRN' -CFG.MODEL.PARAM = { - "num_nodes" : 170, - "input_dim" : 1, - "rnn_units" : 64, - "output_dim": 1, - "horizon" : 12, - "num_layers": 2, - "default_graph": True, - "embed_dim" : 2, - "cheb_k" : 2 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = L1Loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, -} -# CFG.TRAIN.LR_SCHEDULER = EasyDict() -# CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -# CFG.TRAIN.LR_SCHEDULER.PARAM= { -# "milestones":[5, 20, 40, 70], -# "gamma":0.3 -# } - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/D2STGNN/D2STGNN_METR-LA.py b/basicts/options/D2STGNN/D2STGNN_METR-LA.py deleted file mode 100644 index fe3ffd43..00000000 --- a/basicts/options/D2STGNN/D2STGNN_METR-LA.py +++ /dev/null @@ -1,125 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.D2STGNN_runner import D2STGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'D2STGNN model configuration' -CFG.RUNNER = D2STGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "METR-LA" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'D2STGNN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "num_feat" : 1, - "num_hidden": 32, - "dropout" : 0.1, - "seq_length": 12, - "k_t" : 3, - "k_s" : 2, - "gap" : 3, - "num_nodes" : 207, - "adjs" : [torch.tensor(adj) for adj in adj_mx], - "num_layers": 5, - "num_modalities": 2, - "node_hidden" : 10, - "time_emb_dim" : 10, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":1.0e-5, - "eps":1.0e-8 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 30, 38, 46, 54, 62, 70, 80], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False -## curriculum learning -CFG.TRAIN.CL = EasyDict() -CFG.TRAIN.CL.WARM_EPOCHS = 0 -CFG.TRAIN.CL.CL_EPOCHS = 6 -CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 32 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/D2STGNN/D2STGNN_PEMS-BAY.py b/basicts/options/D2STGNN/D2STGNN_PEMS-BAY.py deleted file mode 100644 index 6e5bcba1..00000000 --- a/basicts/options/D2STGNN/D2STGNN_PEMS-BAY.py +++ /dev/null @@ -1,125 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.D2STGNN_runner import D2STGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'D2STGNN model configuration' -CFG.RUNNER = D2STGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS-BAY" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'D2STGNN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "num_feat" : 1, - "num_hidden": 32, - "dropout" : 0.1, - "seq_length": 12, - "k_t" : 3, - "k_s" : 2, - "gap" : 3, - "num_nodes" : 325, - "adjs" : [torch.tensor(adj) for adj in adj_mx], - "num_layers": 5, - "num_modalities": 2, - "node_hidden" : 12, - "time_emb_dim" : 12, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":1.0e-5, - "eps":1.0e-8 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 30, 38, 46, 54, 62, 70, 80], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False -## curriculum learning -CFG.TRAIN.CL = EasyDict() -CFG.TRAIN.CL.WARM_EPOCHS = 30 -CFG.TRAIN.CL.CL_EPOCHS = 3 -CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 32 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/D2STGNN/D2STGNN_PEMS03.py b/basicts/options/D2STGNN/D2STGNN_PEMS03.py deleted file mode 100644 index 0786a9e5..00000000 --- a/basicts/options/D2STGNN/D2STGNN_PEMS03.py +++ /dev/null @@ -1,125 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.D2STGNN_runner import D2STGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'D2STGNN model configuration' -CFG.RUNNER = D2STGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS03" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'D2STGNN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "num_feat" : 1, - "num_hidden": 32, - "dropout" : 0.2, - "seq_length": 12, - "k_t" : 3, - "k_s" : 2, - "gap" : 3, - "num_nodes" : 358, - "adjs" : [torch.tensor(adj) for adj in adj_mx], - "num_layers": 5, - "num_modalities": 2, - "node_hidden" : 10, - "time_emb_dim" : 10, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":1.0e-5, - "eps":1.0e-8 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 30, 38, 46, 54, 200], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 300 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 16 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False -## curriculum learning -CFG.TRAIN.CL = EasyDict() -CFG.TRAIN.CL.WARM_EPOCHS = 30 -CFG.TRAIN.CL.CL_EPOCHS = 3 -CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 16 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 16 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/D2STGNN/D2STGNN_PEMS04.py b/basicts/options/D2STGNN/D2STGNN_PEMS04.py deleted file mode 100644 index f7afc12b..00000000 --- a/basicts/options/D2STGNN/D2STGNN_PEMS04.py +++ /dev/null @@ -1,125 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.D2STGNN_runner import D2STGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'D2STGNN model configuration' -CFG.RUNNER = D2STGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS04" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'D2STGNN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "num_feat" : 1, - "num_hidden": 32, - "dropout" : 0.1, - "seq_length": 12, - "k_t" : 3, - "k_s" : 2, - "gap" : 3, - "num_nodes" : 307, - "adjs" : [torch.tensor(adj) for adj in adj_mx], - "num_layers": 5, - "num_modalities": 2, - "node_hidden" : 12, - "time_emb_dim" : 12, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":1.0e-5, - "eps":1.0e-8 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 30, 38, 46, 54, 200], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 300 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 16 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False -## curriculum learning -CFG.TRAIN.CL = EasyDict() -CFG.TRAIN.CL.WARM_EPOCHS = 30 -CFG.TRAIN.CL.CL_EPOCHS = 3 -CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 16 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 16 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/D2STGNN/D2STGNN_PEMS07.py b/basicts/options/D2STGNN/D2STGNN_PEMS07.py deleted file mode 100644 index 6a5975e0..00000000 --- a/basicts/options/D2STGNN/D2STGNN_PEMS07.py +++ /dev/null @@ -1,125 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.D2STGNN_runner import D2STGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'D2STGNN model configuration' -CFG.RUNNER = D2STGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS07" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'D2STGNN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "num_feat" : 1, - "num_hidden": 32, - "dropout" : 0.1, - "seq_length": 12, - "k_t" : 3, - "k_s" : 2, - "gap" : 3, - "num_nodes" : 883, - "adjs" : [torch.tensor(adj) for adj in adj_mx], - "num_layers": 5, - "num_modalities": 2, - "node_hidden" : 12, - "time_emb_dim" : 12, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":1.0e-5, - "eps":1.0e-8 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 30, 38, 46, 54, 200], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 300 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 16 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False -## curriculum learning -CFG.TRAIN.CL = EasyDict() -CFG.TRAIN.CL.WARM_EPOCHS = 30 -CFG.TRAIN.CL.CL_EPOCHS = 3 -CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 16 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 16 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/D2STGNN/D2STGNN_PEMS08.py b/basicts/options/D2STGNN/D2STGNN_PEMS08.py deleted file mode 100644 index 97cd64c7..00000000 --- a/basicts/options/D2STGNN/D2STGNN_PEMS08.py +++ /dev/null @@ -1,125 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.D2STGNN_runner import D2STGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'D2STGNN model configuration' -CFG.RUNNER = D2STGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS08" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'D2STGNN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "num_feat" : 1, - "num_hidden": 32, - "dropout" : 0.1, - "seq_length": 12, - "k_t" : 3, - "k_s" : 2, - "gap" : 3, - "num_nodes" : 170, - "adjs" : [torch.tensor(adj) for adj in adj_mx], - "num_layers": 5, - "num_modalities": 2, - "node_hidden" : 10, - "time_emb_dim" : 10, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":1.0e-5, - "eps":1.0e-8 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 30, 38, 46, 54, 200], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 300 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 16 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False -## curriculum learning -CFG.TRAIN.CL = EasyDict() -CFG.TRAIN.CL.WARM_EPOCHS = 30 -CFG.TRAIN.CL.CL_EPOCHS = 3 -CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 16 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 16 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/DCRNN/DCRNN_METR-LA.py b/basicts/options/DCRNN/DCRNN_METR-LA.py deleted file mode 100644 index 64050f43..00000000 --- a/basicts/options/DCRNN/DCRNN_METR-LA.py +++ /dev/null @@ -1,124 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.DCRNN_runner import DCRNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -resume = False # DCRNN does not allow to load parameters since it creates parameters in the first iteration -if not resume: - import random - _ = random.randint(-1e6, 1e6) - -# ================= general ================= # -CFG.DESCRIPTION = 'DCRNN model configuration' -CFG.RUNNER = DCRNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "METR-LA" -CFG.DATASET_TYPE = 'Traffic speed' -CFG._ = _ -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'DCRNN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "cl_decay_steps" : 2000, - "horizon" : 12, - "input_dim" : 2, - "max_diffusion_step": 2, - "num_nodes" : 207, - "num_rnn_layers" : 2, - "output_dim" : 1, - "rnn_units" : 64, - "seq_len" : 12, - "adj_mx" : [torch.tensor(i).cuda() for i in adj_mx], - "use_curriculum_learning": True -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.01, - "eps":1e-3 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[20, 30, 40, 50], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -CFG.TRAIN.SETUP_GRAPH = True -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/DCRNN/DCRNN_PEMS-BAY.py b/basicts/options/DCRNN/DCRNN_PEMS-BAY.py deleted file mode 100644 index 54b65e78..00000000 --- a/basicts/options/DCRNN/DCRNN_PEMS-BAY.py +++ /dev/null @@ -1,124 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.DCRNN_runner import DCRNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -resume = False # DCRNN does not allow to load parameters since it creates parameters in the first iteration -if not resume: - import random - _ = random.randint(-1e6, 1e6) - -# ================= general ================= # -CFG.DESCRIPTION = 'DCRNN model configuration' -CFG.RUNNER = DCRNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS-BAY" -CFG.DATASET_TYPE = 'Traffic speed' -CFG._ = _ -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'DCRNN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "cl_decay_steps" : 2000, - "horizon" : 12, - "input_dim" : 2, - "max_diffusion_step": 2, - "num_nodes" : 325, - "num_rnn_layers" : 2, - "output_dim" : 1, - "rnn_units" : 64, - "seq_len" : 12, - "adj_mx" : [torch.tensor(i).cuda() for i in adj_mx], - "use_curriculum_learning": True -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.01, - "eps":1e-3 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[20, 30, 40, 50], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -CFG.TRAIN.SETUP_GRAPH = True -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/DCRNN/DCRNN_PEMS03.py b/basicts/options/DCRNN/DCRNN_PEMS03.py deleted file mode 100644 index 181b1b07..00000000 --- a/basicts/options/DCRNN/DCRNN_PEMS03.py +++ /dev/null @@ -1,124 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.DCRNN_runner import DCRNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -resume = False # DCRNN does not allow to load parameters since it creates parameters in the first iteration -if not resume: - import random - _ = random.randint(-1e6, 1e6) - -# ================= general ================= # -CFG.DESCRIPTION = 'DCRNN model configuration' -CFG.RUNNER = DCRNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS03" -CFG.DATASET_TYPE = 'Traffic speed' -CFG._ = _ -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'DCRNN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "cl_decay_steps" : 2000, - "horizon" : 12, - "input_dim" : 2, - "max_diffusion_step": 2, - "num_nodes" : 358, - "num_rnn_layers" : 2, - "output_dim" : 1, - "rnn_units" : 64, - "seq_len" : 12, - "adj_mx" : [torch.tensor(i).cuda() for i in adj_mx], - "use_curriculum_learning": True -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, - "eps":1e-3 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[80], - "gamma":0.3 -} - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -CFG.TRAIN.SETUP_GRAPH = True -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/DCRNN/DCRNN_PEMS04.py b/basicts/options/DCRNN/DCRNN_PEMS04.py deleted file mode 100644 index ebcb74d7..00000000 --- a/basicts/options/DCRNN/DCRNN_PEMS04.py +++ /dev/null @@ -1,124 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.DCRNN_runner import DCRNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -resume = False # DCRNN does not allow to load parameters since it creates parameters in the first iteration -if not resume: - import random - _ = random.randint(-1e6, 1e6) - -# ================= general ================= # -CFG.DESCRIPTION = 'DCRNN model configuration' -CFG.RUNNER = DCRNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS04" -CFG.DATASET_TYPE = 'Traffic flow' -CFG._ = _ -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'DCRNN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "cl_decay_steps" : 2000, - "horizon" : 12, - "input_dim" : 2, - "max_diffusion_step": 2, - "num_nodes" : 307, - "num_rnn_layers" : 2, - "output_dim" : 1, - "rnn_units" : 64, - "seq_len" : 12, - "adj_mx" : [torch.tensor(i).cuda() for i in adj_mx], - "use_curriculum_learning": True -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, - "eps":1e-3 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[80], - "gamma":0.3 -} - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -CFG.TRAIN.SETUP_GRAPH = True -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/DCRNN/DCRNN_PEMS07.py b/basicts/options/DCRNN/DCRNN_PEMS07.py deleted file mode 100644 index c48d7258..00000000 --- a/basicts/options/DCRNN/DCRNN_PEMS07.py +++ /dev/null @@ -1,124 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.DCRNN_runner import DCRNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -resume = False # DCRNN does not allow to load parameters since it creates parameters in the first iteration -if not resume: - import random - _ = random.randint(-1e6, 1e6) - -# ================= general ================= # -CFG.DESCRIPTION = 'DCRNN model configuration' -CFG.RUNNER = DCRNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS07" -CFG.DATASET_TYPE = 'Traffic speed' -CFG._ = _ -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'DCRNN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "cl_decay_steps" : 2000, - "horizon" : 12, - "input_dim" : 2, - "max_diffusion_step": 2, - "num_nodes" : 883, - "num_rnn_layers" : 2, - "output_dim" : 1, - "rnn_units" : 64, - "seq_len" : 12, - "adj_mx" : [torch.tensor(i).cuda() for i in adj_mx], - "use_curriculum_learning": True -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, - "eps":1e-3 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[80], - "gamma":0.3 -} - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -CFG.TRAIN.SETUP_GRAPH = True -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/DCRNN/DCRNN_PEMS08.py b/basicts/options/DCRNN/DCRNN_PEMS08.py deleted file mode 100644 index f20b02c5..00000000 --- a/basicts/options/DCRNN/DCRNN_PEMS08.py +++ /dev/null @@ -1,124 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.DCRNN_runner import DCRNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -resume = False # DCRNN does not allow to load parameters since it creates parameters in the first iteration -if not resume: - import random - _ = random.randint(-1e6, 1e6) - -# ================= general ================= # -CFG.DESCRIPTION = 'DCRNN model configuration' -CFG.RUNNER = DCRNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS08" -CFG.DATASET_TYPE = 'Traffic flow' -CFG._ = _ -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'DCRNN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "cl_decay_steps" : 2000, - "horizon" : 12, - "input_dim" : 2, - "max_diffusion_step": 2, - "num_nodes" : 170, - "num_rnn_layers" : 2, - "output_dim" : 1, - "rnn_units" : 64, - "seq_len" : 12, - "adj_mx" : [torch.tensor(i).cuda() for i in adj_mx], - "use_curriculum_learning": True -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, - "eps":1e-3 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[80], - "gamma":0.3 -} - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -CFG.TRAIN.SETUP_GRAPH = True -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/DGCRN/DGCRN_METR-LA.py b/basicts/options/DGCRN/DGCRN_METR-LA.py deleted file mode 100644 index 26e47f49..00000000 --- a/basicts/options/DGCRN/DGCRN_METR-LA.py +++ /dev/null @@ -1,126 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.DGCRN_runner import DGCRNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'DGCRN model configuration' -CFG.RUNNER = DGCRNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "METR-LA" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'DGCRN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "gcn_depth" : 2, - "num_nodes" : 207, - "predefined_A": [torch.Tensor(_) for _ in adj_mx], - "dropout" : 0.3, - "subgraph_size" : 20, - "node_dim" : 40, - "middle_dim": 2, - "seq_length": 12, - "in_dim" : 2, - "list_weight": [0.05, 0.95, 0.95], - "tanhalpha" : 3, - "cl_decay_steps" : 4000, - "rnn_size" : 64, - "hyperGNN_dim" : 16 -} - -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":0.0001 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[100, 150], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False -## curriculum learning -CFG.TRAIN.CL = EasyDict() -CFG.TRAIN.CL.WARM_EPOCHS = 0 -CFG.TRAIN.CL.CL_EPOCHS = 6 -CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/DGCRN/DGCRN_PEMS-BAY.py b/basicts/options/DGCRN/DGCRN_PEMS-BAY.py deleted file mode 100644 index 9727b5dd..00000000 --- a/basicts/options/DGCRN/DGCRN_PEMS-BAY.py +++ /dev/null @@ -1,126 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.DGCRN_runner import DGCRNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'DGCRN model configuration' -CFG.RUNNER = DGCRNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS-BAY" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'DGCRN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "gcn_depth" : 2, - "num_nodes" : 325, - "predefined_A": [torch.Tensor(_) for _ in adj_mx], - "dropout" : 0.3, - "subgraph_size" : 20, - "node_dim" : 40, - "middle_dim": 2, - "seq_length": 12, - "in_dim" : 2, - "list_weight": [0.05, 0.95, 0.95], - "tanhalpha" : 3, - "cl_decay_steps" : 5500, - "rnn_size" : 64, - "hyperGNN_dim" : 16 -} - -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":0.0001 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[100, 150], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False -## curriculum learning -CFG.TRAIN.CL = EasyDict() -CFG.TRAIN.CL.WARM_EPOCHS = 0 -CFG.TRAIN.CL.CL_EPOCHS = 6 -CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/DGCRN/DGCRN_PEMS03.py b/basicts/options/DGCRN/DGCRN_PEMS03.py deleted file mode 100644 index fd91d907..00000000 --- a/basicts/options/DGCRN/DGCRN_PEMS03.py +++ /dev/null @@ -1,126 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.DGCRN_runner import DGCRNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'DGCRN model configuration' -CFG.RUNNER = DGCRNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS03" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'DGCRN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "gcn_depth" : 2, - "num_nodes" : 358, - "predefined_A": [torch.Tensor(_) for _ in adj_mx], - "dropout" : 0.3, - "subgraph_size" : 20, - "node_dim" : 40, - "middle_dim": 2, - "seq_length": 12, - "in_dim" : 2, - "list_weight": [0.05, 0.95, 0.95], - "tanhalpha" : 3, - "cl_decay_steps" : 4000, - "rnn_size" : 64, - "hyperGNN_dim" : 16 -} - -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":0.0001 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[100, 150], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False -## curriculum learning -CFG.TRAIN.CL = EasyDict() -CFG.TRAIN.CL.WARM_EPOCHS = 0 -CFG.TRAIN.CL.CL_EPOCHS = 6 -CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/DGCRN/DGCRN_PEMS04.py b/basicts/options/DGCRN/DGCRN_PEMS04.py deleted file mode 100644 index d7838cf2..00000000 --- a/basicts/options/DGCRN/DGCRN_PEMS04.py +++ /dev/null @@ -1,126 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.DGCRN_runner import DGCRNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'DGCRN model configuration' -CFG.RUNNER = DGCRNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS04" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'DGCRN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "gcn_depth" : 2, - "num_nodes" : 307, - "predefined_A": [torch.Tensor(_) for _ in adj_mx], - "dropout" : 0.3, - "subgraph_size" : 20, - "node_dim" : 40, - "middle_dim": 2, - "seq_length": 12, - "in_dim" : 2, - "list_weight": [0.05, 0.95, 0.95], - "tanhalpha" : 3, - "cl_decay_steps" : 4000, - "rnn_size" : 64, - "hyperGNN_dim" : 16 -} - -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":0.0001 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[100, 150], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False -## curriculum learning -CFG.TRAIN.CL = EasyDict() -CFG.TRAIN.CL.WARM_EPOCHS = 0 -CFG.TRAIN.CL.CL_EPOCHS = 6 -CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/DGCRN/DGCRN_PEMS07.py b/basicts/options/DGCRN/DGCRN_PEMS07.py deleted file mode 100644 index 376f0f04..00000000 --- a/basicts/options/DGCRN/DGCRN_PEMS07.py +++ /dev/null @@ -1,126 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.DGCRN_runner import DGCRNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'DGCRN model configuration' -CFG.RUNNER = DGCRNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS07" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'DGCRN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "gcn_depth" : 2, - "num_nodes" : 883, - "predefined_A": [torch.Tensor(_) for _ in adj_mx], - "dropout" : 0.3, - "subgraph_size" : 20, - "node_dim" : 40, - "middle_dim": 2, - "seq_length": 12, - "in_dim" : 2, - "list_weight": [0.05, 0.95, 0.95], - "tanhalpha" : 3, - "cl_decay_steps" : 4000, - "rnn_size" : 64, - "hyperGNN_dim" : 16 -} - -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":0.0001 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[100, 150], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 24 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False -## curriculum learning -CFG.TRAIN.CL = EasyDict() -CFG.TRAIN.CL.WARM_EPOCHS = 0 -CFG.TRAIN.CL.CL_EPOCHS = 6 -CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/DGCRN/DGCRN_PEMS08.py b/basicts/options/DGCRN/DGCRN_PEMS08.py deleted file mode 100644 index 42918067..00000000 --- a/basicts/options/DGCRN/DGCRN_PEMS08.py +++ /dev/null @@ -1,126 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.DGCRN_runner import DGCRNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'DGCRN model configuration' -CFG.RUNNER = DGCRNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS08" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'DGCRN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "gcn_depth" : 2, - "num_nodes" : 170, - "predefined_A": [torch.Tensor(_) for _ in adj_mx], - "dropout" : 0.3, - "subgraph_size" : 20, - "node_dim" : 40, - "middle_dim": 2, - "seq_length": 12, - "in_dim" : 2, - "list_weight": [0.05, 0.95, 0.95], - "tanhalpha" : 3, - "cl_decay_steps" : 4000, - "rnn_size" : 64, - "hyperGNN_dim" : 16 -} - -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":0.0001 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[100, 150], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False -## curriculum learning -CFG.TRAIN.CL = EasyDict() -CFG.TRAIN.CL.WARM_EPOCHS = 0 -CFG.TRAIN.CL.CL_EPOCHS = 6 -CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GMAN/GMAN_METR-LA.py b/basicts/options/GMAN/GMAN_METR-LA.py deleted file mode 100644 index 0b25f6df..00000000 --- a/basicts/options/GMAN/GMAN_METR-LA.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.GMAN_runner import GMANRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_node2vec_emb - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'GMAN model configuration' -CFG.RUNNER = GMANRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "METR-LA" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GMAN' -spatial_embed = load_node2vec_emb("datasets/" + CFG.DATASET_NAME + "/node2vec_emb.txt") -CFG.MODEL.PARAM = { - "SE": spatial_embed, - "L" : 5, - "K" : 8, - "d" : 8, - "num_his" : 12, - "bn_decay": 0.1 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 80], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 12 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 32 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GMAN/GMAN_PEMS-BAY.py b/basicts/options/GMAN/GMAN_PEMS-BAY.py deleted file mode 100644 index 8622d225..00000000 --- a/basicts/options/GMAN/GMAN_PEMS-BAY.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.GMAN_runner import GMANRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_node2vec_emb - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'GMAN model configuration' -CFG.RUNNER = GMANRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS-BAY" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GMAN' -spatial_embed = load_node2vec_emb("datasets/" + CFG.DATASET_NAME + "/node2vec_emb.txt") -CFG.MODEL.PARAM = { - "SE": spatial_embed, - "L" : 1, - "K" : 8, - "d" : 8, - "num_his" : 12, - "bn_decay": 0.1 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 80], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 16 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 32 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GMAN/GMAN_PEMS03.py b/basicts/options/GMAN/GMAN_PEMS03.py deleted file mode 100644 index 0657b4d6..00000000 --- a/basicts/options/GMAN/GMAN_PEMS03.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.GMAN_runner import GMANRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_node2vec_emb - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'GMAN model configuration' -CFG.RUNNER = GMANRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS03" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GMAN' -spatial_embed = load_node2vec_emb("datasets/" + CFG.DATASET_NAME + "/node2vec_emb.txt") -CFG.MODEL.PARAM = { - "SE": spatial_embed, - "L" : 1, - "K" : 8, - "d" : 8, - "num_his" : 12, - "bn_decay": 0.1 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 20, 40, 60, 80], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 16 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 32 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GMAN/GMAN_PEMS04.py b/basicts/options/GMAN/GMAN_PEMS04.py deleted file mode 100644 index e69f851d..00000000 --- a/basicts/options/GMAN/GMAN_PEMS04.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.GMAN_runner import GMANRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_node2vec_emb - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'GMAN model configuration' -CFG.RUNNER = GMANRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS04" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GMAN' -spatial_embed = load_node2vec_emb("datasets/" + CFG.DATASET_NAME + "/node2vec_emb.txt") -CFG.MODEL.PARAM = { - "SE": spatial_embed, - "L" : 1, - "K" : 8, - "d" : 8, - "num_his" : 12, - "bn_decay": 0.1 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 20, 40, 60, 80], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 16 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 32 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GMAN/GMAN_PEMS07.py b/basicts/options/GMAN/GMAN_PEMS07.py deleted file mode 100644 index 1e252b42..00000000 --- a/basicts/options/GMAN/GMAN_PEMS07.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.GMAN_runner import GMANRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_node2vec_emb - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'GMAN model configuration' -CFG.RUNNER = GMANRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS07" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GMAN' -spatial_embed = load_node2vec_emb("datasets/" + CFG.DATASET_NAME + "/node2vec_emb.txt") -CFG.MODEL.PARAM = { - "SE": spatial_embed, - "L" : 1, - "K" : 8, - "d" : 8, - "num_his" : 12, - "bn_decay": 0.1 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 80], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 4 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 32 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GMAN/GMAN_PEMS08.py b/basicts/options/GMAN/GMAN_PEMS08.py deleted file mode 100644 index 01cf0952..00000000 --- a/basicts/options/GMAN/GMAN_PEMS08.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.GMAN_runner import GMANRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_node2vec_emb - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'GMAN model configuration' -CFG.RUNNER = GMANRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS08" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GMAN' -spatial_embed = load_node2vec_emb("datasets/" + CFG.DATASET_NAME + "/node2vec_emb.txt") -CFG.MODEL.PARAM = { - "SE": spatial_embed, - "L" : 1, - "K" : 8, - "d" : 8, - "num_his" : 12, - "bn_decay": 0.1 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 20, 40, 60, 80], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 16 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 32 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GTS/GTS_METR-LA.py b/basicts/options/GTS/GTS_METR-LA.py deleted file mode 100644 index 65f4af82..00000000 --- a/basicts/options/GTS/GTS_METR-LA.py +++ /dev/null @@ -1,130 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.GTS_runner import GTSRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_pkl - -CFG = EasyDict() - -resume = False # DCRNN does not allow to load parameters since it creates parameters in the first iteration -if not resume: - import random - _ = random.randint(-1e6, 1e6) - -# ================= general ================= # -CFG.DESCRIPTION = 'GTS model configuration' -CFG.RUNNER = GTSRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "METR-LA" -CFG.DATASET_TYPE = 'Traffic speed' -CFG._ = _ -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GTS' -node_feats_full = load_pkl("datasets/{0}/data.pkl".format(CFG.DATASET_NAME))['processed_data'][..., 0] -train_index_list = load_pkl("datasets/{0}/index.pkl".format(CFG.DATASET_NAME))['train'] -node_feats = node_feats_full[:train_index_list[-1][-1], ...] -CFG.MODEL.PARAM = { - "cl_decay_steps" : 2000, - "filter_type" : "dual_random_walk", - "horizon" : 12, - "input_dim" : 2, - "l1_decay" : 0, - "max_diffusion_step": 3, - "num_nodes" : 207, - "num_rnn_layers" : 1, - "output_dim" : 1, - "rnn_units" : 64, - "seq_len" : 12, - "use_curriculum_learning": True, - "dim_fc" : 383664, - "node_feats" : node_feats, - "temp" : 0.5, - "k" : 10 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.005, - "eps":1e-3 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[20, 40], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -CFG.TRAIN.SETUP_GRAPH = True -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GTS/GTS_PEMS-BAY.py b/basicts/options/GTS/GTS_PEMS-BAY.py deleted file mode 100644 index 51fe5c7c..00000000 --- a/basicts/options/GTS/GTS_PEMS-BAY.py +++ /dev/null @@ -1,130 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.GTS_runner import GTSRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_pkl - -CFG = EasyDict() - -resume = False # DCRNN does not allow to load parameters since it creates parameters in the first iteration -if not resume: - import random - _ = random.randint(-1e6, 1e6) - -# ================= general ================= # -CFG.DESCRIPTION = 'GTS model configuration' -CFG.RUNNER = GTSRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS-BAY" -CFG.DATASET_TYPE = 'Traffic speed' -CFG._ = _ -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GTS' -node_feats_full = load_pkl("datasets/{0}/data.pkl".format(CFG.DATASET_NAME))['processed_data'][..., 0] -train_index_list = load_pkl("datasets/{0}/index.pkl".format(CFG.DATASET_NAME))['train'] -node_feats = node_feats_full[:train_index_list[-1][-1], ...] -CFG.MODEL.PARAM = { - "cl_decay_steps" : 2000, - "filter_type" : "dual_random_walk", - "horizon" : 12, - "input_dim" : 2, - "l1_decay" : 0, - "max_diffusion_step": 2, - "num_nodes" : 325, - "num_rnn_layers" : 1, - "output_dim" : 1, - "rnn_units" : 128, - "seq_len" : 12, - "use_curriculum_learning": True, - "dim_fc" : 583520, - "node_feats" : node_feats, - "temp" : 0.5, - "k" : 30 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "eps":1e-3 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[20, 30], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -CFG.TRAIN.SETUP_GRAPH = True -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GTS/GTS_PEMS03.py b/basicts/options/GTS/GTS_PEMS03.py deleted file mode 100644 index fb3f812e..00000000 --- a/basicts/options/GTS/GTS_PEMS03.py +++ /dev/null @@ -1,130 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.GTS_runner import GTSRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_pkl - -CFG = EasyDict() - -resume = False # DCRNN does not allow to load parameters since it creates parameters in the first iteration -if not resume: - import random - _ = random.randint(-1e6, 1e6) - -# ================= general ================= # -CFG.DESCRIPTION = 'GTS model configuration' -CFG.RUNNER = GTSRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS03" -CFG.DATASET_TYPE = 'Traffic flow' -CFG._ = _ -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GTS' -node_feats_full = load_pkl("datasets/{0}/data.pkl".format(CFG.DATASET_NAME))['processed_data'][..., 0] -train_index_list = load_pkl("datasets/{0}/index.pkl".format(CFG.DATASET_NAME))['train'] -node_feats = node_feats_full[:train_index_list[-1][-1], ...] -CFG.MODEL.PARAM = { - "cl_decay_steps" : 2000, - "filter_type" : "dual_random_walk", - "horizon" : 12, - "input_dim" : 2, - "l1_decay" : 0, - "max_diffusion_step": 3, - "num_nodes" : 358, - "num_rnn_layers" : 1, - "output_dim" : 1, - "rnn_units" : 64, - "seq_len" : 12, - "use_curriculum_learning": True, - "dim_fc" : 251456, - "node_feats" : node_feats, - "temp" : 0.5, - "k" : 30 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "eps":1e-3 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[20, 30], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -CFG.TRAIN.SETUP_GRAPH = True -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GTS/GTS_PEMS04.py b/basicts/options/GTS/GTS_PEMS04.py deleted file mode 100644 index c5546925..00000000 --- a/basicts/options/GTS/GTS_PEMS04.py +++ /dev/null @@ -1,130 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.GTS_runner import GTSRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_pkl - -CFG = EasyDict() - -resume = False # DCRNN does not allow to load parameters since it creates parameters in the first iteration -if not resume: - import random - _ = random.randint(-1e6, 1e6) - -# ================= general ================= # -CFG.DESCRIPTION = 'GTS model configuration' -CFG.RUNNER = GTSRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS04" -CFG.DATASET_TYPE = 'Traffic flow' -CFG._ = _ -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GTS' -node_feats_full = load_pkl("datasets/{0}/data.pkl".format(CFG.DATASET_NAME))['processed_data'][..., 0] -train_index_list = load_pkl("datasets/{0}/index.pkl".format(CFG.DATASET_NAME))['train'] -node_feats = node_feats_full[:train_index_list[-1][-1], ...] -CFG.MODEL.PARAM = { - "cl_decay_steps" : 2000, - "filter_type" : "dual_random_walk", - "horizon" : 12, - "input_dim" : 2, - "l1_decay" : 0, - "max_diffusion_step": 3, - "num_nodes" : 307, - "num_rnn_layers" : 1, - "output_dim" : 1, - "rnn_units" : 64, - "seq_len" : 12, - "use_curriculum_learning": True, - "dim_fc" : 162976, - "node_feats" : node_feats, - "temp" : 0.5, - "k" : 30 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "eps":1e-3 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[20, 30], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -CFG.TRAIN.SETUP_GRAPH = True -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GTS/GTS_PEMS07.py b/basicts/options/GTS/GTS_PEMS07.py deleted file mode 100644 index f3131513..00000000 --- a/basicts/options/GTS/GTS_PEMS07.py +++ /dev/null @@ -1,130 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.GTS_runner import GTSRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_pkl - -CFG = EasyDict() - -resume = False # DCRNN does not allow to load parameters since it creates parameters in the first iteration -if not resume: - import random - _ = random.randint(-1e6, 1e6) - -# ================= general ================= # -CFG.DESCRIPTION = 'GTS model configuration' -CFG.RUNNER = GTSRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS07" -CFG.DATASET_TYPE = 'Traffic flow' -CFG._ = _ -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GTS' -node_feats_full = load_pkl("datasets/{0}/data.pkl".format(CFG.DATASET_NAME))['processed_data'][..., 0] -train_index_list = load_pkl("datasets/{0}/index.pkl".format(CFG.DATASET_NAME))['train'] -node_feats = node_feats_full[:train_index_list[-1][-1], ...] -CFG.MODEL.PARAM = { - "cl_decay_steps" : 2000, - "filter_type" : "dual_random_walk", - "horizon" : 12, - "input_dim" : 2, - "l1_decay" : 0, - "max_diffusion_step": 2, - "num_nodes" : 883, - "num_rnn_layers" : 1, - "output_dim" : 1, - "rnn_units" : 64, - "seq_len" : 12, - "use_curriculum_learning": True, - "dim_fc" : 270816, - "node_feats" : node_feats, - "temp" : 0.5, - "k" : 30 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "eps":1e-3 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[20, 30], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -CFG.TRAIN.SETUP_GRAPH = True -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GTS/GTS_PEMS08.py b/basicts/options/GTS/GTS_PEMS08.py deleted file mode 100644 index f53d4a30..00000000 --- a/basicts/options/GTS/GTS_PEMS08.py +++ /dev/null @@ -1,130 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.GTS_runner import GTSRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_pkl - -CFG = EasyDict() - -resume = False # DCRNN does not allow to load parameters since it creates parameters in the first iteration -if not resume: - import random - _ = random.randint(-1e6, 1e6) - -# ================= general ================= # -CFG.DESCRIPTION = 'GTS model configuration' -CFG.RUNNER = GTSRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS08" -CFG.DATASET_TYPE = 'Traffic flow' -CFG._ = _ -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GTS' -node_feats_full = load_pkl("datasets/{0}/data.pkl".format(CFG.DATASET_NAME))['processed_data'][..., 0] -train_index_list = load_pkl("datasets/{0}/index.pkl".format(CFG.DATASET_NAME))['train'] -node_feats = node_feats_full[:train_index_list[-1][-1], ...] -CFG.MODEL.PARAM = { - "cl_decay_steps" : 2000, - "filter_type" : "dual_random_walk", - "horizon" : 12, - "input_dim" : 2, - "l1_decay" : 0, - "max_diffusion_step": 3, - "num_nodes" : 170, - "num_rnn_layers" : 1, - "output_dim" : 1, - "rnn_units" : 64, - "seq_len" : 12, - "use_curriculum_learning": True, - "dim_fc" : 171280, - "node_feats" : node_feats, - "temp" : 0.5, - "k" : 30 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "eps":1e-3 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[20, 30], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -CFG.TRAIN.SETUP_GRAPH = True -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GraphWaveNet/GraphWaveNet_METR-LA.py b/basicts/options/GraphWaveNet/GraphWaveNet_METR-LA.py deleted file mode 100644 index 2eb71be4..00000000 --- a/basicts/options/GraphWaveNet/GraphWaveNet_METR-LA.py +++ /dev/null @@ -1,121 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.GraphWaveNet_runner import GraphWaveNetRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'Graph WaveNet model configuration' -CFG.RUNNER = GraphWaveNetRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "METR-LA" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GraphWaveNet' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "num_nodes" : 207, - "supports" :[torch.tensor(i) for i in adj_mx], - "dropout" : 0.3, - "gcn_bool" : True, - "addaptadj" : True, - "aptinit" : None, - "in_dim" : 2, - "out_dim" : 12, - "residual_channels" : 32, - "dilation_channels" : 32, - "skip_channels" : 256, - "end_channels" : 512, - "kernel_size" : 2, - "blocks" : 4, - "layers" : 2 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GraphWaveNet/GraphWaveNet_PEMS-BAY.py b/basicts/options/GraphWaveNet/GraphWaveNet_PEMS-BAY.py deleted file mode 100644 index 88d60b94..00000000 --- a/basicts/options/GraphWaveNet/GraphWaveNet_PEMS-BAY.py +++ /dev/null @@ -1,121 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.GraphWaveNet_runner import GraphWaveNetRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'Graph WaveNet model configuration' -CFG.RUNNER = GraphWaveNetRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS-BAY" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GraphWaveNet' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "num_nodes" : 325, - "supports" :[torch.tensor(i) for i in adj_mx], - "dropout" : 0.3, - "gcn_bool" : True, - "addaptadj" : True, - "aptinit" : None, - "in_dim" : 2, - "out_dim" : 12, - "residual_channels" : 32, - "dilation_channels" : 32, - "skip_channels" : 256, - "end_channels" : 512, - "kernel_size" : 2, - "blocks" : 4, - "layers" : 2 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GraphWaveNet/GraphWaveNet_PEMS03.py b/basicts/options/GraphWaveNet/GraphWaveNet_PEMS03.py deleted file mode 100644 index 39e74e7a..00000000 --- a/basicts/options/GraphWaveNet/GraphWaveNet_PEMS03.py +++ /dev/null @@ -1,121 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.GraphWaveNet_runner import GraphWaveNetRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'Graph WaveNet model configuration' -CFG.RUNNER = GraphWaveNetRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS03" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GraphWaveNet' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "num_nodes" : 358, - "supports" :[torch.tensor(i) for i in adj_mx], - "dropout" : 0.3, - "gcn_bool" : True, - "addaptadj" : True, - "aptinit" : None, - "in_dim" : 2, - "out_dim" : 12, - "residual_channels" : 32, - "dilation_channels" : 32, - "skip_channels" : 256, - "end_channels" : 512, - "kernel_size" : 2, - "blocks" : 4, - "layers" : 2 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GraphWaveNet/GraphWaveNet_PEMS04.py b/basicts/options/GraphWaveNet/GraphWaveNet_PEMS04.py deleted file mode 100644 index 88c73a63..00000000 --- a/basicts/options/GraphWaveNet/GraphWaveNet_PEMS04.py +++ /dev/null @@ -1,121 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.GraphWaveNet_runner import GraphWaveNetRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'Graph WaveNet model configuration' -CFG.RUNNER = GraphWaveNetRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS04" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GraphWaveNet' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "num_nodes" : 307, - "supports" :[torch.tensor(i) for i in adj_mx], - "dropout" : 0.3, - "gcn_bool" : True, - "addaptadj" : True, - "aptinit" : None, - "in_dim" : 2, - "out_dim" : 12, - "residual_channels" : 32, - "dilation_channels" : 32, - "skip_channels" : 256, - "end_channels" : 512, - "kernel_size" : 2, - "blocks" : 4, - "layers" : 2 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GraphWaveNet/GraphWaveNet_PEMS07.py b/basicts/options/GraphWaveNet/GraphWaveNet_PEMS07.py deleted file mode 100644 index af3d5c03..00000000 --- a/basicts/options/GraphWaveNet/GraphWaveNet_PEMS07.py +++ /dev/null @@ -1,121 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.GraphWaveNet_runner import GraphWaveNetRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'Graph WaveNet model configuration' -CFG.RUNNER = GraphWaveNetRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS07" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GraphWaveNet' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "num_nodes" : 883, - "supports" :[torch.tensor(i) for i in adj_mx], - "dropout" : 0.3, - "gcn_bool" : True, - "addaptadj" : True, - "aptinit" : None, - "in_dim" : 2, - "out_dim" : 12, - "residual_channels" : 32, - "dilation_channels" : 32, - "skip_channels" : 256, - "end_channels" : 512, - "kernel_size" : 2, - "blocks" : 4, - "layers" : 2 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/GraphWaveNet/GraphWaveNet_PEMS08.py b/basicts/options/GraphWaveNet/GraphWaveNet_PEMS08.py deleted file mode 100644 index f5a79fae..00000000 --- a/basicts/options/GraphWaveNet/GraphWaveNet_PEMS08.py +++ /dev/null @@ -1,121 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.GraphWaveNet_runner import GraphWaveNetRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'Graph WaveNet model configuration' -CFG.RUNNER = GraphWaveNetRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS08" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'GraphWaveNet' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") -CFG.MODEL.PARAM = { - "num_nodes" : 170, - "supports" :[torch.tensor(i) for i in adj_mx], - "dropout" : 0.3, - "gcn_bool" : True, - "addaptadj" : True, - "aptinit" : None, - "in_dim" : 2, - "out_dim" : 12, - "residual_channels" : 32, - "dilation_channels" : 32, - "skip_channels" : 256, - "end_channels" : 512, - "kernel_size" : 2, - "blocks" : 4, - "layers" : 2 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/HI/HI_Electricity336.py b/basicts/options/HI/HI_Electricity336.py deleted file mode 100644 index 3ccd54bc..00000000 --- a/basicts/options/HI/HI_Electricity336.py +++ /dev/null @@ -1,106 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.HI_runner import HIRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'HI model configuration' -CFG.RUNNER = HIRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "Electricity336" -CFG.DATASET_TYPE = 'Electricity consumption' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'HINetwork' -CFG.MODEL.PARAM = { - 'input_length': 168, - 'output_length': 12, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.005, - "weight_decay":1.0e-5, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[50], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 1 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/HI/HI_METR-LA.py b/basicts/options/HI/HI_METR-LA.py deleted file mode 100644 index 96cc7874..00000000 --- a/basicts/options/HI/HI_METR-LA.py +++ /dev/null @@ -1,105 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.HI_runner import HIRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'HI model configuration' -CFG.RUNNER = HIRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "METR-LA" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'HINetwork' -CFG.MODEL.PARAM = { - 'input_length': 12, - 'output_length': 12 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.005, - "weight_decay":1.0e-5, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[50], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 1 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/HI/HI_PEMS-BAY.py b/basicts/options/HI/HI_PEMS-BAY.py deleted file mode 100644 index a3d2e38e..00000000 --- a/basicts/options/HI/HI_PEMS-BAY.py +++ /dev/null @@ -1,105 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.HI_runner import HIRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'HI model configuration' -CFG.RUNNER = HIRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS-BAY" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'HINetwork' -CFG.MODEL.PARAM = { - 'input_length': 12, - 'output_length': 12, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.005, - "weight_decay":1.0e-5, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[50], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 1 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/HI/HI_PEMS03.py b/basicts/options/HI/HI_PEMS03.py deleted file mode 100644 index b377f041..00000000 --- a/basicts/options/HI/HI_PEMS03.py +++ /dev/null @@ -1,105 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.HI_runner import HIRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'HI model configuration' -CFG.RUNNER = HIRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS03" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'HINetwork' -CFG.MODEL.PARAM = { - 'input_length': 12, - 'output_length': 12, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.005, - "weight_decay":1.0e-5, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[50], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 1 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/HI/HI_PEMS04.py b/basicts/options/HI/HI_PEMS04.py deleted file mode 100644 index cac1e526..00000000 --- a/basicts/options/HI/HI_PEMS04.py +++ /dev/null @@ -1,105 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.HI_runner import HIRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'HI model configuration' -CFG.RUNNER = HIRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS04" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'HINetwork' -CFG.MODEL.PARAM = { - 'input_length': 12, - 'output_length': 12, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.005, - "weight_decay":1.0e-5, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[50], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 1 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/HI/HI_PEMS07.py b/basicts/options/HI/HI_PEMS07.py deleted file mode 100644 index 841e3f56..00000000 --- a/basicts/options/HI/HI_PEMS07.py +++ /dev/null @@ -1,106 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.HI_runner import HIRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'HI model configuration' -CFG.RUNNER = HIRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS07" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'HINetwork' -CFG.MODEL.PARAM = { - 'input_length': 12, - 'output_length': 12, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.005, - "weight_decay":1.0e-5, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[50], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 1 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/HI/HI_PEMS08.py b/basicts/options/HI/HI_PEMS08.py deleted file mode 100644 index f8fce1e8..00000000 --- a/basicts/options/HI/HI_PEMS08.py +++ /dev/null @@ -1,106 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.HI_runner import HIRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'HI model configuration' -CFG.RUNNER = HIRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS08" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'HINetwork' -CFG.MODEL.PARAM = { - 'input_length': 12, - 'output_length': 12, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.005, - "weight_decay":1.0e-5, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[50], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 1 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/LSTM/LSTM_Electricity336.py b/basicts/options/LSTM/LSTM_Electricity336.py deleted file mode 100644 index 45e5d9ab..00000000 --- a/basicts/options/LSTM/LSTM_Electricity336.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.LSTM_runner import LSTMRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import L1Loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'AGCRN model configuration' -CFG.RUNNER = LSTMRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "Electricity336" -CFG.DATASET_TYPE = 'Electricity consumption' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'LSTM' -CFG.MODEL.PARAM = { - "input_dim" : 2, - "rnn_units" : 64, - "output_dim": 1, - "horizon" : 12, - "num_layers": 2, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = L1Loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, -} -# CFG.TRAIN.LR_SCHEDULER = EasyDict() -# CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -# CFG.TRAIN.LR_SCHEDULER.PARAM= { -# "milestones":[5, 20, 40, 70], -# "gamma":0.3 -# } - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 16 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/LSTM/LSTM_METR-LA.py b/basicts/options/LSTM/LSTM_METR-LA.py deleted file mode 100644 index 9625f3f9..00000000 --- a/basicts/options/LSTM/LSTM_METR-LA.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.LSTM_runner import LSTMRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import L1Loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'AGCRN model configuration' -CFG.RUNNER = LSTMRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "METR-LA" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'LSTM' -CFG.MODEL.PARAM = { - "input_dim" : 2, - "rnn_units" : 64, - "output_dim": 1, - "horizon" : 12, - "num_layers": 2, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = L1Loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, -} -# CFG.TRAIN.LR_SCHEDULER = EasyDict() -# CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -# CFG.TRAIN.LR_SCHEDULER.PARAM= { -# "milestones":[5, 20, 40, 70], -# "gamma":0.3 -# } - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 16 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/LSTM/LSTM_PEMS-BAY.py b/basicts/options/LSTM/LSTM_PEMS-BAY.py deleted file mode 100644 index cca4c691..00000000 --- a/basicts/options/LSTM/LSTM_PEMS-BAY.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.LSTM_runner import LSTMRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import L1Loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'AGCRN model configuration' -CFG.RUNNER = LSTMRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS-BAY" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'LSTM' -CFG.MODEL.PARAM = { - "input_dim" : 2, - "rnn_units" : 64, - "output_dim": 1, - "horizon" : 12, - "num_layers": 2, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = L1Loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, -} -# CFG.TRAIN.LR_SCHEDULER = EasyDict() -# CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -# CFG.TRAIN.LR_SCHEDULER.PARAM= { -# "milestones":[5, 20, 40, 70], -# "gamma":0.3 -# } - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 16 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/LSTM/LSTM_PEMS03.py b/basicts/options/LSTM/LSTM_PEMS03.py deleted file mode 100644 index ffd1bbd3..00000000 --- a/basicts/options/LSTM/LSTM_PEMS03.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.LSTM_runner import LSTMRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import L1Loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'AGCRN model configuration' -CFG.RUNNER = LSTMRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS03" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'LSTM' -CFG.MODEL.PARAM = { - "input_dim" : 2, - "rnn_units" : 64, - "output_dim": 1, - "horizon" : 12, - "num_layers": 2, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = L1Loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, -} -# CFG.TRAIN.LR_SCHEDULER = EasyDict() -# CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -# CFG.TRAIN.LR_SCHEDULER.PARAM= { -# "milestones":[5, 20, 40, 70], -# "gamma":0.3 -# } - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 16 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/LSTM/LSTM_PEMS04.py b/basicts/options/LSTM/LSTM_PEMS04.py deleted file mode 100644 index d5a961ed..00000000 --- a/basicts/options/LSTM/LSTM_PEMS04.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.LSTM_runner import LSTMRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import L1Loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'AGCRN model configuration' -CFG.RUNNER = LSTMRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS04" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'LSTM' -CFG.MODEL.PARAM = { - "input_dim" : 2, - "rnn_units" : 64, - "output_dim": 1, - "horizon" : 12, - "num_layers": 2, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = L1Loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, -} -# CFG.TRAIN.LR_SCHEDULER = EasyDict() -# CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -# CFG.TRAIN.LR_SCHEDULER.PARAM= { -# "milestones":[5, 20, 40, 70], -# "gamma":0.3 -# } - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 16 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/LSTM/LSTM_PEMS07.py b/basicts/options/LSTM/LSTM_PEMS07.py deleted file mode 100644 index cbb9f532..00000000 --- a/basicts/options/LSTM/LSTM_PEMS07.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.LSTM_runner import LSTMRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import L1Loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'AGCRN model configuration' -CFG.RUNNER = LSTMRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS07" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'LSTM' -CFG.MODEL.PARAM = { - "input_dim" : 2, - "rnn_units" : 64, - "output_dim": 1, - "horizon" : 12, - "num_layers": 2, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = L1Loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, -} -# CFG.TRAIN.LR_SCHEDULER = EasyDict() -# CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -# CFG.TRAIN.LR_SCHEDULER.PARAM= { -# "milestones":[5, 20, 40, 70], -# "gamma":0.3 -# } - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 16 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/LSTM/LSTM_PEMS08.py b/basicts/options/LSTM/LSTM_PEMS08.py deleted file mode 100644 index 71739a47..00000000 --- a/basicts/options/LSTM/LSTM_PEMS08.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.LSTM_runner import LSTMRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import L1Loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'AGCRN model configuration' -CFG.RUNNER = LSTMRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS08" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'LSTM' -CFG.MODEL.PARAM = { - "input_dim" : 2, - "rnn_units" : 64, - "output_dim": 1, - "horizon" : 12, - "num_layers": 2, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = L1Loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.003, -} -# CFG.TRAIN.LR_SCHEDULER = EasyDict() -# CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -# CFG.TRAIN.LR_SCHEDULER.PARAM= { -# "milestones":[5, 20, 40, 70], -# "gamma":0.3 -# } - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/MTGNN/MTGNN_Electricity336.py b/basicts/options/MTGNN/MTGNN_Electricity336.py deleted file mode 100644 index 8c73dd17..00000000 --- a/basicts/options/MTGNN/MTGNN_Electricity336.py +++ /dev/null @@ -1,139 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.MTGNN_runner import MTGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'MTGNN model configuration' -CFG.RUNNER = MTGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "Electricity336" -CFG.DATASET_TYPE = 'Electricity consumption' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'MTGNN' - -buildA_true = True -num_nodes = 336 -if buildA_true: # self-learned adjacency matrix - adj_mx = None -else: # use predefined adjacency matrix - _, adj_mx = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "doubletransition") - adj_mx = torch.tensor(adj_mx)-torch.eye(num_nodes) - -CFG.MODEL.PARAM = { - "gcn_true" : True, - "buildA_true": buildA_true, - "gcn_depth": 2, - "num_nodes": num_nodes, - "predefined_A":adj_mx, - "dropout":0.3, - "subgraph_size":20, - "node_dim":40, - "dilation_exponential":1, - "conv_channels":32, - "residual_channels":32, - "skip_channels":64, - "end_channels":128, - "seq_length":168, - "in_dim":2, - "out_dim":12, - "layers":3, - "propalpha":0.05, - "tanhalpha":3, - "layer_norm_affline":True -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":0.0001, -} - -# ================= train ================= # -CFG.TRAIN.CUSTOM = EasyDict() # MTGNN custom training args -CFG.TRAIN.CUSTOM.STEP_SIZE = 100 -CFG.TRAIN.CUSTOM.NUM_NODES = num_nodes -CFG.TRAIN.CUSTOM.NUM_SPLIT = 1 - -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 16 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False -## curriculum learning -CFG.TRAIN.CL = EasyDict() -CFG.TRAIN.CL.WARM_EPOCHS = 0 -CFG.TRAIN.CL.CL_EPOCHS = 3 -CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 32 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STGCN/STGCN_METR-LA.py b/basicts/options/STGCN/STGCN_METR-LA.py deleted file mode 100644 index 64b65445..00000000 --- a/basicts/options/STGCN/STGCN_METR-LA.py +++ /dev/null @@ -1,117 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.STGCN_runner import STGCNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STGCN model configuration' -CFG.RUNNER = STGCNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "METR-LA" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STGCN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "normlap") -adj_mx = torch.Tensor(adj_mx[0]) -CFG.MODEL.PARAM = { - "Ks" : 3, - "Kt" : 3, - "blocks" : [[1], [64, 16, 64], [64, 16, 64], [128, 128], [12]], - "T" : 12, - "n_vertex" : 207, - "act_func" : "glu", - "graph_conv_type" : "cheb_graph_conv", - "gso" : adj_mx, - "bias": True, - "droprate" : 0.5 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STGCN/STGCN_PEMS-BAY.py b/basicts/options/STGCN/STGCN_PEMS-BAY.py deleted file mode 100644 index 9550ddeb..00000000 --- a/basicts/options/STGCN/STGCN_PEMS-BAY.py +++ /dev/null @@ -1,118 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.STGCN_runner import STGCNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STGCN model configuration' -CFG.RUNNER = STGCNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS-BAY" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STGCN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "normlap") -adj_mx = torch.Tensor(adj_mx[0]) -CFG.MODEL.PARAM = { - "Ks" : 3, - "Kt" : 3, - "blocks" : [[1], [64, 16, 64], [64, 16, 64], [128, 128], [12]], - "T" : 12, - "n_vertex" : 325, - "act_func" : "glu", - "graph_conv_type" : "cheb_graph_conv", - "gso" : adj_mx, - "bias": True, - "droprate" : 0.5 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STGCN/STGCN_PEMS03.py b/basicts/options/STGCN/STGCN_PEMS03.py deleted file mode 100644 index 02d46662..00000000 --- a/basicts/options/STGCN/STGCN_PEMS03.py +++ /dev/null @@ -1,117 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.STGCN_runner import STGCNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STGCN model configuration' -CFG.RUNNER = STGCNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS03" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STGCN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "normlap") -adj_mx = torch.Tensor(adj_mx[0]) -CFG.MODEL.PARAM = { - "Ks" : 3, - "Kt" : 3, - "blocks" : [[1], [64, 16, 64], [64, 16, 64], [128, 128], [12]], - "T" : 12, - "n_vertex" : 358, - "act_func" : "glu", - "graph_conv_type" : "cheb_graph_conv", - "gso" : adj_mx, - "bias": True, - "droprate" : 0.5 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STGCN/STGCN_PEMS04.py b/basicts/options/STGCN/STGCN_PEMS04.py deleted file mode 100644 index d36a0d84..00000000 --- a/basicts/options/STGCN/STGCN_PEMS04.py +++ /dev/null @@ -1,117 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.STGCN_runner import STGCNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STGCN model configuration' -CFG.RUNNER = STGCNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS04" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STGCN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "normlap") -adj_mx = torch.Tensor(adj_mx[0]) -CFG.MODEL.PARAM = { - "Ks" : 3, - "Kt" : 3, - "blocks" : [[1], [64, 16, 64], [64, 16, 64], [128, 128], [12]], - "T" : 12, - "n_vertex" : 307, - "act_func" : "glu", - "graph_conv_type" : "cheb_graph_conv", - "gso" : adj_mx, - "bias": True, - "droprate" : 0.5 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STGCN/STGCN_PEMS07.py b/basicts/options/STGCN/STGCN_PEMS07.py deleted file mode 100644 index 138a3842..00000000 --- a/basicts/options/STGCN/STGCN_PEMS07.py +++ /dev/null @@ -1,117 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.STGCN_runner import STGCNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STGCN model configuration' -CFG.RUNNER = STGCNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS07" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STGCN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "normlap") -adj_mx = torch.Tensor(adj_mx[0]) -CFG.MODEL.PARAM = { - "Ks" : 3, - "Kt" : 3, - "blocks" : [[1], [64, 16, 64], [64, 16, 64], [128, 128], [12]], - "T" : 12, - "n_vertex" : 883, - "act_func" : "glu", - "graph_conv_type" : "cheb_graph_conv", - "gso" : adj_mx, - "bias": True, - "droprate" : 0.5 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STGCN/STGCN_PEMS08.py b/basicts/options/STGCN/STGCN_PEMS08.py deleted file mode 100644 index 7b4d6f04..00000000 --- a/basicts/options/STGCN/STGCN_PEMS08.py +++ /dev/null @@ -1,117 +0,0 @@ -import os -from easydict import EasyDict -import torch -# runner -from basicts.runners.STGCN_runner import STGCNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STGCN model configuration' -CFG.RUNNER = STGCNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS08" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STGCN' -adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "normlap") -adj_mx = torch.Tensor(adj_mx[0]) -CFG.MODEL.PARAM = { - "Ks" : 3, - "Kt" : 3, - "blocks" : [[1], [64, 16, 64], [64, 16, 64], [128, 128], [12]], - "T" : 12, - "n_vertex" : 170, - "act_func" : "glu", - "graph_conv_type" : "cheb_graph_conv", - "gso" : adj_mx, - "bias": True, - "droprate" : 0.5 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STID/STID_Electricity336.py b/basicts/options/STID/STID_Electricity336.py deleted file mode 100644 index 9c5e1335..00000000 --- a/basicts/options/STID/STID_Electricity336.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.STID_runner import STIDRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STID model configuration' -CFG.RUNNER = STIDRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "Electricity336" -CFG.DATASET_TYPE = 'Electricity consumption' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STID' -CFG.MODEL.PARAM = { - "num_nodes" : 336, - 'input_len' : 168, - 'input_dim' : 3, - 'embed_dim' : 32, - 'output_len': 12, - 'num_layer' : 3, - "if_node" : True, - 'node_dim' : 32, - "if_T_i_D" : True, - "if_D_i_W" : True, - 'temp_dim_tid' : 32, - 'temp_dim_diw' : 32, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] # traffic speed, time in day -CFG.MODEL.TARGET_FEATURES = [0] # traffic speed - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100, 50], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STID/STID_METR-LA.py b/basicts/options/STID/STID_METR-LA.py deleted file mode 100644 index bc4b9ca1..00000000 --- a/basicts/options/STID/STID_METR-LA.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.STID_runner import STIDRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'Basic MTS model configuration' -CFG.RUNNER = STIDRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "METR-LA" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STID' -CFG.MODEL.PARAM = { - "num_nodes" : 207, - 'input_len' : 12, - 'input_dim' : 3, - 'embed_dim' : 32, - 'output_len': 12, - 'num_layer' : 3, - "if_node" : True, - 'node_dim' : 32, - "if_T_i_D" : True, - "if_D_i_W" : True, - 'temp_dim_tid' : 32, - 'temp_dim_diw' : 32, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] # traffic speed, time in day -CFG.MODEL.TARGET_FEATURES = [0] # traffic speed - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 80], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STID/STID_PEMS-BAY.py b/basicts/options/STID/STID_PEMS-BAY.py deleted file mode 100644 index b60d9090..00000000 --- a/basicts/options/STID/STID_PEMS-BAY.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.STID_runner import STIDRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'Basic MTS model configuration' -CFG.RUNNER = STIDRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS-BAY" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STID' -CFG.MODEL.PARAM = { - "num_nodes" : 325, - 'input_len' : 12, - 'input_dim' : 3, - 'embed_dim' : 32, - 'output_len': 12, - 'num_layer' : 3, - "if_node" : True, - 'node_dim' : 32, - "if_T_i_D" : True, - "if_D_i_W" : True, - 'temp_dim_tid' : 32, - 'temp_dim_diw' : 32, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] # traffic speed, time in day -CFG.MODEL.TARGET_FEATURES = [0] # traffic speed - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 80], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STID/STID_PEMS03.py b/basicts/options/STID/STID_PEMS03.py deleted file mode 100644 index 2ae57c2e..00000000 --- a/basicts/options/STID/STID_PEMS03.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.STID_runner import STIDRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STID model configuration without DiW embedding' -CFG.RUNNER = STIDRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS03" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STID' -CFG.MODEL.PARAM = { - "num_nodes" : 358, - 'input_len' : 12, - 'input_dim' : 3, - 'embed_dim' : 32, - 'output_len': 12, - 'num_layer' : 3, - "if_node" : True, - 'node_dim' : 32, - "if_T_i_D" : True, - "if_D_i_W" : True, - 'temp_dim_tid' : 32, - 'temp_dim_diw' : 32, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] # traffic speed, time in day -CFG.MODEL.TARGET_FEATURES = [0] # traffic speed - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STID/STID_PEMS04.py b/basicts/options/STID/STID_PEMS04.py deleted file mode 100644 index 977ab25b..00000000 --- a/basicts/options/STID/STID_PEMS04.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.STID_runner import STIDRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STID model configuration without DiW embedding' -CFG.RUNNER = STIDRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS04" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STID' -CFG.MODEL.PARAM = { - "num_nodes" : 307, - 'input_len' : 12, - 'input_dim' : 3, - 'embed_dim' : 32, - 'output_len': 12, - 'num_layer' : 3, - "if_node" : True, - 'node_dim' : 32, - "if_T_i_D" : True, - "if_D_i_W" : True, - 'temp_dim_tid' : 32, - 'temp_dim_diw' : 32, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] # traffic speed, time in day -CFG.MODEL.TARGET_FEATURES = [0] # traffic speed - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STID/STID_PEMS07.py b/basicts/options/STID/STID_PEMS07.py deleted file mode 100644 index 1d88ada4..00000000 --- a/basicts/options/STID/STID_PEMS07.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.STID_runner import STIDRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STID model configuration' -CFG.RUNNER = STIDRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS07" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STID' -CFG.MODEL.PARAM = { - "num_nodes" : 883, - 'input_len' : 12, - 'input_dim' : 3, - 'embed_dim' : 32, - 'output_len': 12, - 'num_layer' : 3, - "if_node" : True, - 'node_dim' : 32, - "if_T_i_D" : True, - "if_D_i_W" : True, - 'temp_dim_tid' : 32, - 'temp_dim_diw' : 32, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] # traffic speed, time in day -CFG.MODEL.TARGET_FEATURES = [0] # traffic speed - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STID/STID_PEMS08.py b/basicts/options/STID/STID_PEMS08.py deleted file mode 100644 index f7e8cb3f..00000000 --- a/basicts/options/STID/STID_PEMS08.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.STID_runner import STIDRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STID model configuration' -CFG.RUNNER = STIDRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS08" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STID' -CFG.MODEL.PARAM = { - "num_nodes" : 170, - 'input_len' : 12, - 'input_dim' : 3, - 'embed_dim' : 32, - 'output_len': 12, - 'num_layer' : 3, - "if_node" : True, - 'node_dim' : 32, - "if_T_i_D" : True, - "if_D_i_W" : True, - 'temp_dim_tid' : 32, - 'temp_dim_diw' : 32, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] # traffic speed, time in day -CFG.MODEL.TARGET_FEATURES = [0] # traffic speed - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100, 50], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STNorm/STNorm_Electricity336.py b/basicts/options/STNorm/STNorm_Electricity336.py deleted file mode 100644 index 7c6837b2..00000000 --- a/basicts/options/STNorm/STNorm_Electricity336.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.STNorm_runner import STNormRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STNorm model configuration' -CFG.RUNNER = STNormRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "Electricity336" -CFG.DATASET_TYPE = 'Electricity consumption' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STNorm' -CFG.MODEL.PARAM = { - "num_nodes" : 336, - "tnorm_bool": True, - "snorm_bool": True, - "in_dim" : 2, - "out_dim" : 12, - "channels" : 32, - "kernel_size": 3, - "blocks" : 8, - "layers" : 4, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 4 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STNorm/STNorm_METR-LA.py b/basicts/options/STNorm/STNorm_METR-LA.py deleted file mode 100644 index 95e129dd..00000000 --- a/basicts/options/STNorm/STNorm_METR-LA.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.STNorm_runner import STNormRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STNorm model configuration' -CFG.RUNNER = STNormRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "METR-LA" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STNorm' -CFG.MODEL.PARAM = { - "num_nodes" : 207, - "tnorm_bool": True, - "snorm_bool": True, - "in_dim" : 2, - "out_dim" : 12, - "channels" : 32, - "kernel_size": 2, - "blocks" : 4, - "layers" : 2, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STNorm/STNorm_PEMS-BAY.py b/basicts/options/STNorm/STNorm_PEMS-BAY.py deleted file mode 100644 index 582033c6..00000000 --- a/basicts/options/STNorm/STNorm_PEMS-BAY.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.STNorm_runner import STNormRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STNorm model configuration' -CFG.RUNNER = STNormRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS-BAY" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STNorm' -CFG.MODEL.PARAM = { - "num_nodes" : 325, - "tnorm_bool": True, - "snorm_bool": True, - "in_dim" : 2, - "out_dim" : 12, - "channels" : 32, - "kernel_size": 2, - "blocks" : 4, - "layers" : 2, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STNorm/STNorm_PEMS03.py b/basicts/options/STNorm/STNorm_PEMS03.py deleted file mode 100644 index e2ca0fb6..00000000 --- a/basicts/options/STNorm/STNorm_PEMS03.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.STNorm_runner import STNormRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STNorm model configuration' -CFG.RUNNER = STNormRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS03" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STNorm' -CFG.MODEL.PARAM = { - "num_nodes" : 358, - "tnorm_bool": True, - "snorm_bool": True, - "in_dim" : 2, - "out_dim" : 12, - "channels" : 32, - "kernel_size": 2, - "blocks" : 4, - "layers" : 2, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STNorm/STNorm_PEMS04.py b/basicts/options/STNorm/STNorm_PEMS04.py deleted file mode 100644 index 7ee6daf5..00000000 --- a/basicts/options/STNorm/STNorm_PEMS04.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.STNorm_runner import STNormRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STNorm model configuration' -CFG.RUNNER = STNormRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS04" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STNorm' -CFG.MODEL.PARAM = { - "num_nodes" : 307, - "tnorm_bool": True, - "snorm_bool": True, - "in_dim" : 2, - "out_dim" : 12, - "channels" : 32, - "kernel_size": 2, - "blocks" : 4, - "layers" : 2, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STNorm/STNorm_PEMS07.py b/basicts/options/STNorm/STNorm_PEMS07.py deleted file mode 100644 index 2e1fb607..00000000 --- a/basicts/options/STNorm/STNorm_PEMS07.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.STNorm_runner import STNormRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STNorm model configuration' -CFG.RUNNER = STNormRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS07" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STNorm' -CFG.MODEL.PARAM = { - "num_nodes" : 883, - "tnorm_bool": True, - "snorm_bool": True, - "in_dim" : 2, - "out_dim" : 12, - "channels" : 32, - "kernel_size": 2, - "blocks" : 4, - "layers" : 2, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/STNorm/STNorm_PEMS08.py b/basicts/options/STNorm/STNorm_PEMS08.py deleted file mode 100644 index 137bc8a4..00000000 --- a/basicts/options/STNorm/STNorm_PEMS08.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.STNorm_runner import STNormRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'STNorm model configuration' -CFG.RUNNER = STNormRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS08" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'STNorm' -CFG.MODEL.PARAM = { - "num_nodes" : 170, - "tnorm_bool": True, - "snorm_bool": True, - "in_dim" : 2, - "out_dim" : 12, - "channels" : 32, - "kernel_size": 2, - "blocks" : 4, - "layers" : 2, -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002, - "weight_decay":0.0001, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50], - "gamma":0.5 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/Stat/Stat_Electricity336.py b/basicts/options/Stat/Stat_Electricity336.py deleted file mode 100644 index 9c5bbb97..00000000 --- a/basicts/options/Stat/Stat_Electricity336.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.Stat_runner import StatRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'Stat model configuration' -CFG.RUNNER = StatRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "Electricity336" -CFG.DATASET_TYPE = 'Electricity consumption' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'VectorAutoRegression' -CFG.MODEL.PARAM = { - 'p': 168, - 'input_length': 168, - 'output_length': 12, - 'num_time_series': 336 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":1.0e-5, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[50], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 4 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 32 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/Stat/Stat_METR-LA.py b/basicts/options/Stat/Stat_METR-LA.py deleted file mode 100644 index 5b9edc6b..00000000 --- a/basicts/options/Stat/Stat_METR-LA.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.Stat_runner import StatRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'Stat model configuration' -CFG.RUNNER = StatRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "METR-LA" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'VectorAutoRegression' -CFG.MODEL.PARAM = { - 'p': 12, - 'input_length': 12, - 'output_length': 12, - 'num_time_series': 207 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":1.0e-5, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[50], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/Stat/Stat_PEMS-BAY.py b/basicts/options/Stat/Stat_PEMS-BAY.py deleted file mode 100644 index ed21c58a..00000000 --- a/basicts/options/Stat/Stat_PEMS-BAY.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.Stat_runner import StatRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'Stat model configuration' -CFG.RUNNER = StatRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS-BAY" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'VectorAutoRegression' -CFG.MODEL.PARAM = { - 'p': 12, - 'input_length': 12, - 'output_length': 12, - 'num_time_series': 325 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":1.0e-5, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[50], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/Stat/Stat_PEMS03.py b/basicts/options/Stat/Stat_PEMS03.py deleted file mode 100644 index 2cad218a..00000000 --- a/basicts/options/Stat/Stat_PEMS03.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.Stat_runner import StatRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'Stat model configuration' -CFG.RUNNER = StatRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS03" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'VectorAutoRegression' -CFG.MODEL.PARAM = { - 'p': 12, - 'input_length': 12, - 'output_length': 12, - 'num_time_series': 358 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":1.0e-5, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[50], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/Stat/Stat_PEMS04.py b/basicts/options/Stat/Stat_PEMS04.py deleted file mode 100644 index eac8362a..00000000 --- a/basicts/options/Stat/Stat_PEMS04.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.Stat_runner import StatRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'Stat model configuration' -CFG.RUNNER = StatRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS04" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'VectorAutoRegression' -CFG.MODEL.PARAM = { - 'p': 12, - 'input_length': 12, - 'output_length': 12, - 'num_time_series': 307 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":1.0e-5, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[50], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/Stat/Stat_PEMS07.py b/basicts/options/Stat/Stat_PEMS07.py deleted file mode 100644 index 5c947790..00000000 --- a/basicts/options/Stat/Stat_PEMS07.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.Stat_runner import StatRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'Stat model configuration' -CFG.RUNNER = StatRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS07" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'VectorAutoRegression' -CFG.MODEL.PARAM = { - 'p': 12, - 'input_length': 12, - 'output_length': 12, - 'num_time_series': 883 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":1.0e-5, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[50], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 10 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/Stat/Stat_PEMS08.py b/basicts/options/Stat/Stat_PEMS08.py deleted file mode 100644 index 99d9c079..00000000 --- a/basicts/options/Stat/Stat_PEMS08.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.Stat_runner import StatRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'Stat model configuration' -CFG.RUNNER = StatRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS08" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'VectorAutoRegression' -CFG.MODEL.PARAM = { - 'p': 12, - 'input_length': 12, - 'output_length': 12, - 'num_time_series': 170 -} -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":1.0e-5, -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[50], - "gamma":0.1 -} - -# ================= train ================= # -CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 -} -CFG.TRAIN.NUM_EPOCHS = 100 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/StemGNN/StemGNN_Electricity336.py b/basicts/options/StemGNN/StemGNN_Electricity336.py deleted file mode 100644 index 1ca36c91..00000000 --- a/basicts/options/StemGNN/StemGNN_Electricity336.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.StemGNN_runner import StemGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -"""Different from the official code, we use Adam as the optimizer and MAE as the loss function since they bring better performance.""" - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'StemGNN model configuration' -CFG.RUNNER = StemGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "Electricity336" -CFG.DATASET_TYPE = 'Electricity consumption' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'StemGNN' -CFG.MODEL.PARAM = { - "units": 336, - "stack_cnt": 2, - "time_step": 168, - "multi_layer": 2, - "horizon": 12, - "dropout_rate": 0.5, - "leaky_rate": 0.2 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100], - "gamma":0.5 -} - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 16 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/StemGNN/StemGNN_METR-LA.py b/basicts/options/StemGNN/StemGNN_METR-LA.py deleted file mode 100644 index 6f3c5b3f..00000000 --- a/basicts/options/StemGNN/StemGNN_METR-LA.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.StemGNN_runner import StemGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -"""Different from the official code, we use Adam as the optimizer and MAE as the loss function since they bring better performance.""" - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'StemGNN model configuration' -CFG.RUNNER = StemGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "METR-LA" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'StemGNN' -CFG.MODEL.PARAM = { - "units": 207, - "stack_cnt": 2, - "time_step": 12, - "multi_layer": 5, - "horizon": 12, - "dropout_rate": 0.5, - "leaky_rate": 0.2 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.0004 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50], - "gamma":0.5 -} - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/StemGNN/StemGNN_PEMS-BAY.py b/basicts/options/StemGNN/StemGNN_PEMS-BAY.py deleted file mode 100644 index 55c4915b..00000000 --- a/basicts/options/StemGNN/StemGNN_PEMS-BAY.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.StemGNN_runner import StemGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -"""Different from the official code, we use Adam as the optimizer and MAE as the loss function since they bring better performance.""" - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'StemGNN model configuration' -CFG.RUNNER = StemGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS-BAY" -CFG.DATASET_TYPE = 'Traffic speed' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'StemGNN' -CFG.MODEL.PARAM = { - "units": 325, - "stack_cnt": 2, - "time_step": 12, - "multi_layer": 5, - "horizon": 12, - "dropout_rate": 0.5, - "leaky_rate": 0.2 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.0004 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50], - "gamma":0.5 -} - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/StemGNN/StemGNN_PEMS03.py b/basicts/options/StemGNN/StemGNN_PEMS03.py deleted file mode 100644 index 0f02ea52..00000000 --- a/basicts/options/StemGNN/StemGNN_PEMS03.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.StemGNN_runner import StemGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -"""Different from the official code, we use Adam as the optimizer and MAE as the loss function since they bring better performance.""" - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'StemGNN model configuration' -CFG.RUNNER = StemGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS03" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'StemGNN' -CFG.MODEL.PARAM = { - "units": 358, - "stack_cnt": 2, - "time_step": 12, - "multi_layer": 5, - "horizon": 12, - "dropout_rate": 0.5, - "leaky_rate": 0.2 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100], - "gamma":0.5 -} - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/StemGNN/StemGNN_PEMS04.py b/basicts/options/StemGNN/StemGNN_PEMS04.py deleted file mode 100644 index 68447440..00000000 --- a/basicts/options/StemGNN/StemGNN_PEMS04.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.StemGNN_runner import StemGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -"""Different from the official code, we use Adam as the optimizer and MAE as the loss function since they bring better performance.""" - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'StemGNN model configuration' -CFG.RUNNER = StemGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS04" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'StemGNN' -CFG.MODEL.PARAM = { - "units": 307, - "stack_cnt": 2, - "time_step": 12, - "multi_layer": 5, - "horizon": 12, - "dropout_rate": 0.5, - "leaky_rate": 0.2 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "RMSprop" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100], - "gamma":0.5 -} - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/StemGNN/StemGNN_PEMS07.py b/basicts/options/StemGNN/StemGNN_PEMS07.py deleted file mode 100644 index 29ddd0dc..00000000 --- a/basicts/options/StemGNN/StemGNN_PEMS07.py +++ /dev/null @@ -1,111 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.StemGNN_runner import StemGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -"""Different from the official code, we use Adam as the optimizer and MAE as the loss function since they bring better performance.""" - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'StemGNN model configuration' -CFG.RUNNER = StemGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS07" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'StemGNN' -CFG.MODEL.PARAM = { - "units": 883, - "stack_cnt": 2, - "time_step": 12, - "multi_layer": 5, - "horizon": 12, - "dropout_rate": 0.5, - "leaky_rate": 0.2 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100], - "gamma":0.5 -} - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/StemGNN/StemGNN_PEMS08.py b/basicts/options/StemGNN/StemGNN_PEMS08.py deleted file mode 100644 index daeaca4f..00000000 --- a/basicts/options/StemGNN/StemGNN_PEMS08.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -from easydict import EasyDict -# runner -from basicts.runners.StemGNN_runner import StemGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss - -"""Different from the official code, we use Adam as the optimizer and MAE as the loss function since they bring better performance.""" - -CFG = EasyDict() - -# ================= general ================= # -CFG.DESCRIPTION = 'StemGNN model configuration' -CFG.RUNNER = StemGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS08" -CFG.DATASET_TYPE = 'Traffic flow' -CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} - -# ================= environment ================= # -CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 -CFG.ENV.CUDNN = EasyDict() -CFG.ENV.CUDNN.ENABLED = True - - -# ================= model ================= # -CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'StemGNN' -CFG.MODEL.PARAM = { - "units": 170, - "stack_cnt": 2, - "time_step": 12, - "multi_layer": 5, - "horizon": 12, - "dropout_rate": 0.5, - "leaky_rate": 0.2 -} -CFG.MODEL.FROWARD_FEATURES = [0] -CFG.MODEL.TARGET_FEATURES = [0] - -# ================= optim ================= # -CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss -CFG.TRAIN.OPTIM = EasyDict() -CFG.TRAIN.OPTIM.TYPE = "Adam" -CFG.TRAIN.OPTIM.PARAM= { - "lr":0.002 -} -CFG.TRAIN.LR_SCHEDULER = EasyDict() -CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" -CFG.TRAIN.LR_SCHEDULER.PARAM= { - "milestones":[1, 50, 100], - "gamma":0.5 -} - -# ================= train ================= # -# CFG.TRAIN.CLIP_GRAD_PARAM = { -# 'max_norm': 5.0 -# } -CFG.TRAIN.NUM_EPOCHS = 200 -CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) -) -# train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 64 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False - -# ================= validate ================= # -CFG.VAL = EasyDict() -CFG.VAL.INTERVAL = 1 -# validating data -CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 64 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False - -# ================= test ================= # -CFG.TEST = EasyDict() -CFG.TEST.INTERVAL = 1 -# validating data -CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 64 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/run.py b/basicts/run.py deleted file mode 100644 index 3b2af878..00000000 --- a/basicts/run.py +++ /dev/null @@ -1,135 +0,0 @@ -import os -import sys -sys.path.append(os.path.abspath(os.path.dirname(os.path.dirname(__file__)))) -from argparse import ArgumentParser -from easytorch import launch_training - -def parse_args(): - parser = ArgumentParser(description='Run time series forecasting model in BasicTS framework based on EasyTorch!') - # parser.add_argument('-c', '--cfg', required=True, help='training config') - - # parser.add_argument('-c', '--cfg', default='basicts/options/HI/HI_METR-LA.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/HI/HI_PEMS-BAY.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/HI/HI_PEMS03.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/HI/HI_PEMS04.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/HI/HI_PEMS07.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/HI/HI_PEMS08.py', help='training config') - - # parser.add_argument('-c', '--cfg', default='basicts/options/Stat/Stat_METR-LA.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/Stat/Stat_PEMS-BAY.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/Stat/Stat_PEMS03.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/Stat/Stat_PEMS04.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/Stat/Stat_PEMS07.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/Stat/Stat_PEMS08.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/Stat/Stat_Electricity336.py', help='training config') - - # parser.add_argument('-c', '--cfg', default='basicts/options/DCRNN/DCRNN_METR-LA.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/DCRNN/DCRNN_PEMS-BAY.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/DCRNN/DCRNN_PEMS03.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/DCRNN/DCRNN_PEMS04.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/DCRNN/DCRNN_PEMS07.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/DCRNN/DCRNN_PEMS08.py', help='training config') - - # parser.add_argument('-c', '--cfg', default='basicts/options/StemGNN/StemGNN_METR-LA.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/StemGNN/StemGNN_PEMS-BAY.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/StemGNN/StemGNN_PEMS03.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/StemGNN/StemGNN_PEMS04.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/StemGNN/StemGNN_PEMS07.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/StemGNN/StemGNN_PEMS08.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/StemGNN/StemGNN_Electricity336.py', help='training config') - - # parser.add_argument('-c', '--cfg', default='basicts/options/GraphWaveNet/GraphWaveNet_METR-LA.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/GraphWaveNet/GraphWaveNet_PEMS-BAY.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/GraphWaveNet/GraphWaveNet_PEMS03.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/GraphWaveNet/GraphWaveNet_PEMS04.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/GraphWaveNet/GraphWaveNet_PEMS07.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/GraphWaveNet/GraphWaveNet_PEMS08.py', help='training config') - - # parser.add_argument('-c', '--cfg', default='basicts/options/STGCN/STGCN_METR-LA.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/STGCN/STGCN_PEMS-BAY.py', help='training config') - parser.add_argument('-c', '--cfg', default='basicts/options/STGCN/STGCN_PEMS03.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/STGCN/STGCN_PEMS04.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/STGCN/STGCN_PEMS07.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/STGCN/STGCN_PEMS08.py', help='training config') - - # parser.add_argument('-c', '--cfg', default='basicts/options/D2STGNN/D2STGNN_METR-LA.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/D2STGNN/D2STGNN_PEMS-BAY.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/D2STGNN/D2STGNN_PEMS03.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/D2STGNN/D2STGNN_PEMS04.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/D2STGNN/D2STGNN_PEMS07.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/D2STGNN/D2STGNN_PEMS08.py', help='training config') - - # parser.add_argument('-c', '--cfg', default='basicts/options/STID/STID_METR-LA.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/STID/STID_PEMS-BAY.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/STID/STID_PEMS03.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/STID/STID_PEMS04.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/STID/STID_PEMS07.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/STID/STID_PEMSS8.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/STID/STID_Electricity336.py', help='training config') - - # parser.add_argument('-c', '--cfg', default='basicts/options/D2STGNN/D2STGNN_PEMS-BAY.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/D2STGNN/D2STGNN_PEMS03.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/D2STGNN/D2STGNN_PEMS04.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/D2STGNN/D2STGNN_PEMS07.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/D2STGNN/D2STGNN_PEMS08.py', help='training config') - - # parser.add_argument('-c', '--cfg', default='basicts/options/MTGNN/MTGNN_METR-LA.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/MTGNN/MTGNN_PEMS-BAY.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/MTGNN/MTGNN_PEMS03.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/MTGNN/MTGNN_PEMS04.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/MTGNN/MTGNN_PEMS07.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/MTGNN/MTGNN_PEMS08.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/MTGNN/MTGNN_Electricity336.py', help='training config') - - # parser.add_argument('-c', '--cfg', default='basicts/options/AGCRN/AGCRN_METR-LA.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/AGCRN/AGCRN_PEMS-BAY.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/AGCRN/AGCRN_PEMS03.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/AGCRN/AGCRN_PEMS04.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/AGCRN/AGCRN_PEMS07.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/AGCRN/AGCRN_PEMS08.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/AGCRN/AGCRN_Electricity336.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/AGCRN/AGCRN_PEMS07.py', help='training config') - - # parser.add_argument('-c', '--cfg', default='basicts/options/LSTM/LSTM_METR-LA.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/LSTM/LSTM_PEMS-BAY.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/LSTM/LSTM_PEMS03.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/LSTM/LSTM_PEMS04.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/LSTM/LSTM_PEMS07.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/LSTM/LSTM_Electricity336.py', help='training config') - - # parser.add_argument('-c', '--cfg', default='basicts/options/STNorm/STNorm_METR-LA.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/STNorm/STNorm_PEMS-BAY.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/STNorm/STNorm_PEMS03.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/STNorm/STNorm_PEMS04.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/STNorm/STNorm_PEMS07.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/STNorm/STNorm_PEMS08.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/STNorm/STNorm_Electricity336.py', help='training config') - - # parser.add_argument('-c', '--cfg', default='basicts/options/DGCRN/DGCRN_METR-LA.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/DGCRN/DGCRN_PEMS-BAY.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/DGCRN/DGCRN_PEMS03.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/DGCRN/DGCRN_PEMS04.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/DGCRN/DGCRN_PEMS07.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/DGCRN/DGCRN_PEMS08.py', help='training config') - - # parser.add_argument('-c', '--cfg', default='basicts/options/GMAN/GMAN_METR-LA.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/GMAN/GMAN_PEMS-BAY.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/GMAN/GMAN_PEMS03.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/GMAN/GMAN_PEMS04.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/GMAN/GMAN_PEMS07.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/GMAN/GMAN_PEMS08.py', help='training config') - - # parser.add_argument('-c', '--cfg', default='basicts/options/GTS/GTS_METR-LA.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/GTS/GTS_PEMS-BAY.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/GTS/GTS_PEMS03.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/GTS/GTS_PEMS04.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/GTS/GTS_PEMS07.py', help='training config') - # parser.add_argument('-c', '--cfg', default='basicts/options/GTS/GTS_PEMS08.py', help='training config') - - parser.add_argument('--gpus', default='0', help='visible gpus') - return parser.parse_args() - -if __name__ == "__main__": - args = parse_args() - - launch_training(args.cfg, args.gpus) diff --git a/basicts/runners/AGCRN_runner.py b/basicts/runners/AGCRN_runner.py deleted file mode 100644 index c0847358..00000000 --- a/basicts/runners/AGCRN_runner.py +++ /dev/null @@ -1,60 +0,0 @@ -import torch -from basicts.runners.short_mts_runner import MTSRunner - -class AGCRNRunner(MTSRunner): - def __init__(self, cfg: dict): - super().__init__(cfg) - - def select_input_features(self, data: torch.Tensor) -> torch.Tensor: - """select input features. - - Args: - data (torch.Tensor): input history data, shape [B, L, N, C] - Returns: - torch.Tensor: reshaped data - """ - # select feature using self.forward_features - if self.forward_features is not None: - data = data[:, :, :, self.forward_features] - return data - - def select_target_features(self, data: torch.Tensor) -> torch.Tensor: - """select target feature - - Args: - data (torch.Tensor): prediction of the model with arbitrary shape. - - Returns: - torch.Tensor: reshaped data with shape [B, L, N, C] - """ - # select feature using self.target_features - data = data[:, :, :, self.target_features] - return data - - def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:bool = True, **kwargs) -> tuple: - """feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. - - Args: - data (tuple): data (future data, history data). [B, L, N, C] for each of them - epoch (int, optional): epoch number. Defaults to None. - iter_num (int, optional): iteration number. Defaults to None. - train (bool, optional): if in the training process. Defaults to True. - - Returns: - tuple: (prediction, real_value) - """ - # preprocess - future_data, history_data = data - history_data = self.to_running_device(history_data) # B, L, N, C - future_data = self.to_running_device(future_data) # B, L, N, C - B, L, N, C = future_data.shape - - history_data = self.select_input_features(history_data) - - # feed forward - prediction_data = self.model(history_data=history_data) # B, L, N, C - assert list(prediction_data.shape)[:3] == [B, L, N], "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" - # post process - prediction = self.select_target_features(prediction_data) - real_value = self.select_target_features(future_data) - return prediction, real_value diff --git a/basicts/runners/D2STGNN_runner.py b/basicts/runners/D2STGNN_runner.py deleted file mode 100644 index 459ecd23..00000000 --- a/basicts/runners/D2STGNN_runner.py +++ /dev/null @@ -1,60 +0,0 @@ -import torch -from basicts.runners.short_mts_runner import MTSRunner - -class D2STGNNRunner(MTSRunner): - def __init__(self, cfg: dict): - super().__init__(cfg) - - def select_input_features(self, data: torch.Tensor) -> torch.Tensor: - """select input features. - - Args: - data (torch.Tensor): input history data, shape [B, L, N, C] - Returns: - torch.Tensor: reshaped data - """ - # select feature using self.forward_features - if self.forward_features is not None: - data = data[:, :, :, self.forward_features] - return data - - def select_target_features(self, data: torch.Tensor) -> torch.Tensor: - """select target feature - - Args: - data (torch.Tensor): prediction of the model with arbitrary shape. - - Returns: - torch.Tensor: reshaped data with shape [B, L, N, C] - """ - # select feature using self.target_features - data = data[:, :, :, self.target_features] - return data - - def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:bool = True, **kwargs) -> tuple: - """feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. - - Args: - data (tuple): data (future data, history data). [B, L, N, C] for each of them - epoch (int, optional): epoch number. Defaults to None. - iter_num (int, optional): iteration number. Defaults to None. - train (bool, optional): if in the training process. Defaults to True. - - Returns: - tuple: (prediction, real_value) - """ - # preprocess - future_data, history_data = data - history_data = self.to_running_device(history_data) # B, L, N, C - future_data = self.to_running_device(future_data) # B, L, N, C - B, L, N, C = future_data.shape - - history_data = self.select_input_features(history_data) - - # feed forward - prediction_data = self.model(history_data=history_data) # B, L, N, C - assert list(prediction_data.shape)[:3] == [B, L, N], "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" - # post process - prediction = self.select_target_features(prediction_data) - real_value = self.select_target_features(future_data) - return prediction, real_value diff --git a/basicts/runners/DCRNN_runner.py b/basicts/runners/DCRNN_runner.py deleted file mode 100644 index f1019c71..00000000 --- a/basicts/runners/DCRNN_runner.py +++ /dev/null @@ -1,79 +0,0 @@ -import torch -from basicts.runners.short_mts_runner import MTSRunner - -class DCRNNRunner(MTSRunner): - def __init__(self, cfg: dict): - super().__init__(cfg) - - def setup_graph(self, data): - try: - self.train_iters(data, 0, 0) - except AttributeError: - pass - - def data_reshaper(self, data: torch.Tensor, channel=None) -> torch.Tensor: - """select input features and reshape data to fit the target model. - - Args: - data (torch.Tensor): input history data, shape [B, L, N, C] - channel (list): self-defined selected channels - Returns: - torch.Tensor: reshaped data - """ - # select feature using self.forward_features - if self.forward_features is not None and channel is None: - data = data[:, :, :, self.forward_features] - if channel is not None: - data = data[:, :, :, channel] - # reshape data [B, L, N, C] -> [L, B, N*C] (DCRNN required) - B, L, N, C = data.shape - data = data.reshape(B, L, N*C) # [B, L, N*C] - data = data.transpose(0, 1) # [L, B, N*C] - return data - - def data_i_reshape(self, data: torch.Tensor) -> torch.Tensor: - """select target features and reshape data back to the BasicTS framework - - Args: - data (torch.Tensor): prediction of the model with arbitrary shape. - - Returns: - torch.Tensor: reshaped data with shape [B, L, N, C] - """ - # reshape data - pass - # select feature using self.target_features - data = data[:, :, :, self.target_features] - return data - - def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:bool = True, **kwargs) -> tuple: - """feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. - - Args: - data (tuple): data (future data, history data). [B, L, N, C] for each of them - epoch (int, optional): epoch number. Defaults to None. - iter_num (int, optional): iteration number. Defaults to None. - train (bool, optional): if in the training process. Defaults to True. - - Returns: - tuple: (prediction, real_value) - """ - # preprocess - future_data, history_data = data - history_data = self.to_running_device(history_data) # B, L, N, C - future_data = self.to_running_device(future_data) # B, L, N, C - B, L, N, C = future_data.shape - - history_data = self.data_reshaper(history_data) - if train: - future_data_ = self.data_reshaper(future_data, channel=[0]) # teacher forcing only use the first dimension. - else: - future_data_ = None - - # feed forward - prediction_data = self.model(history_data=history_data, future_data=future_data_, batch_seen=iter_num, epoch=epoch) # B, L, N, C - assert list(prediction_data.shape)[:3] == [B, L, N], "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" - # post process - prediction = self.data_i_reshape(prediction_data) - real_value = self.data_i_reshape(future_data) - return prediction, real_value diff --git a/basicts/runners/GMAN_runner.py b/basicts/runners/GMAN_runner.py deleted file mode 100644 index f029a015..00000000 --- a/basicts/runners/GMAN_runner.py +++ /dev/null @@ -1,61 +0,0 @@ -import torch -from basicts.runners.short_mts_runner import MTSRunner - -class GMANRunner(MTSRunner): - def __init__(self, cfg: dict): - super().__init__(cfg) - - def select_input_features(self, data: torch.Tensor) -> torch.Tensor: - """select input features. - - Args: - data (torch.Tensor): input history data, shape [B, L, N, C] - Returns: - torch.Tensor: reshaped data - """ - # select feature using self.forward_features - if self.forward_features is not None: - data = data[:, :, :, self.forward_features] - return data - - def select_target_features(self, data: torch.Tensor) -> torch.Tensor: - """select target feature - - Args: - data (torch.Tensor): prediction of the model with arbitrary shape. - - Returns: - torch.Tensor: reshaped data with shape [B, L, N, C] - """ - # select feature using self.target_features - data = data[:, :, :, self.target_features] - return data - - def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:bool = True, **kwargs) -> tuple: - """feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. - - Args: - data (tuple): data (future data, history data). [B, L, N, C] for each of them - epoch (int, optional): epoch number. Defaults to None. - iter_num (int, optional): iteration number. Defaults to None. - train (bool, optional): if in the training process. Defaults to True. - - Returns: - tuple: (prediction, real_value) - """ - # preprocess - future_data, history_data = data - history_data = self.to_running_device(history_data) # B, L, N, C - future_data = self.to_running_device(future_data) # B, L, N, C - B, L, N, C = future_data.shape - - history_data = self.select_input_features(history_data) - future_data = self.select_input_features(future_data) - - # feed forward - prediction_data = self.model(history_data=history_data, future_data=future_data) # B, L, N, C - assert list(prediction_data.shape)[:3] == [B, L, N], "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" - # post process - prediction = self.select_target_features(prediction_data) - real_value = self.select_target_features(future_data) - return prediction, real_value diff --git a/basicts/runners/GTS_runner.py b/basicts/runners/GTS_runner.py deleted file mode 100644 index 8daff157..00000000 --- a/basicts/runners/GTS_runner.py +++ /dev/null @@ -1,190 +0,0 @@ -from typing import Tuple, Union -import torch -from basicts.runners.short_mts_runner import MTSRunner -from basicts.data.transforms import SCALER_REGISTRY -from easytorch.utils.dist import master_only - -""" - TODO: - 模块化train_iters, val_iters, and test_iters中的过程。 - 否则就会像GTS一样, 一旦模型有一点特殊 (例如多一个返回和不同的loss), 就必须重写整个train_iters, val_iters, and test_iters。 -""" - -class GTSRunner(MTSRunner): - def __init__(self, cfg: dict): - super().__init__(cfg) - - def setup_graph(self, data): - try: - self.train_iters(data, 0, 0) - except: - pass - - def data_reshaper(self, data: torch.Tensor, channel=None) -> torch.Tensor: - """select input features and reshape data to fit the target model. - - Args: - data (torch.Tensor): input history data, shape [B, L, N, C] - channel (list): self-defined selected channels - - Returns: - torch.Tensor: reshaped data - """ - # select feature using self.forward_features - if self.forward_features is not None and channel is None: - data = data[:, :, :, self.forward_features] - if channel is not None: - data = data[:, :, :, channel] - # reshape data [B, L, N, C] -> [L, B, N*C] (DCRNN required) - B, L, N, C = data.shape - data = data.reshape(B, L, N*C) # [B, L, N*C] - data = data.transpose(0, 1) # [L, B, N*C] - return data - - def data_i_reshape(self, data: torch.Tensor) -> torch.Tensor: - """select target features and reshape data back to the BasicTS framework - - Args: - data (torch.Tensor): prediction of the model with arbitrary shape. - - Returns: - torch.Tensor: reshaped data with shape [B, L, N, C] - """ - # reshape data - pass - # select feature using self.target_features - data = data[:, :, :, self.target_features] - return data - - def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:bool = True, **kwargs) -> tuple: - """feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. - - Args: - data (tuple): data (future data, history data). [B, L, N, C] for each of them - epoch (int, optional): epoch number. Defaults to None. - iter_num (int, optional): iteration number. Defaults to None. - train (bool, optional): if in the training process. Defaults to True. - - Returns: - tuple: (prediction, real_value) - """ - # preprocess - future_data, history_data = data - history_data = self.to_running_device(history_data) # B, L, N, C - future_data = self.to_running_device(future_data) # B, L, N, C - B, L, N, C = future_data.shape - - history_data = self.data_reshaper(history_data) - if train: - future_data_ = self.data_reshaper(future_data, channel=[0]) # teacher forcing only use the first dimension. - else: - future_data_ = None - - # feed forward - prediction_data, pred_adj, prior_adj = self.model(history_data=history_data, future_data=future_data_, batch_seen=iter_num, epoch=epoch) # B, L, N, C - assert list(prediction_data.shape)[:3] == [B, L, N], "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" - # post process - prediction = self.data_i_reshape(prediction_data) - real_value = self.data_i_reshape(future_data) - return prediction, real_value, pred_adj, prior_adj - - def train_iters(self, epoch: int, iter_index: int, data: Union[torch.Tensor, Tuple]) -> torch.Tensor: - """Training details. - - Args: - data (Union[torch.Tensor, Tuple]): Data provided by DataLoader - epoch (int): current epoch. - iter_index (int): current iter. - - Returns: - loss (torch.Tensor) - """ - iter_num = (epoch-1) * self.iter_per_epoch + iter_index - prediction, real_value, pred_adj, prior_adj = self.forward(data=data, epoch=epoch, iter_num=iter_num, train=True) - # re-scale data - prediction = SCALER_REGISTRY.get(self.scaler['func'])(prediction, **self.scaler['args']) - real_value = SCALER_REGISTRY.get(self.scaler['func'])(real_value, **self.scaler['args']) - # loss - if self.cl_param: - cl_length = self.curriculum_learning(epoch=epoch) - loss = self.loss(prediction[:, :cl_length, :, :], real_value[:, :cl_length, :, :], null_val=self.null_val) - else: - loss = self.loss(prediction, real_value, null_val=self.null_val) - # graph structure loss - prior_label = prior_adj.view(prior_adj.shape[0] * prior_adj.shape[1]).to(pred_adj.device) - pred_label = pred_adj.view(pred_adj.shape[0] * pred_adj.shape[1]) - graph_loss_function = torch.nn.BCELoss() - loss_g = graph_loss_function(pred_label, prior_label) - loss = loss + loss_g - # metrics - for metric_name, metric_func in self.metrics.items(): - metric_item = metric_func(prediction, real_value, null_val=self.null_val) - self.update_epoch_meter('train_'+metric_name, metric_item.item()) - return loss - - def val_iters(self, iter_index: int, data: Union[torch.Tensor, Tuple]): - """Validation details. - - Args: - data (Union[torch.Tensor, Tuple]): Data provided by DataLoader - train_epoch (int): current epoch if in training process. Else None. - iter_index (int): current iter. - """ - prediction, real_value, pred_adj, prior_adj = self.forward(data=data, epoch=None, iter_num=None, train=False) - # re-scale data - prediction = SCALER_REGISTRY.get(self.scaler['func'])(prediction, **self.scaler['args']) - real_value = SCALER_REGISTRY.get(self.scaler['func'])(real_value, **self.scaler['args']) - # loss - loss = self.loss(prediction, real_value, null_val=self.null_val) - # graph structure loss - prior_label = prior_adj.view(prior_adj.shape[0] * prior_adj.shape[1]).to(pred_adj.device) - pred_label = pred_adj.view(pred_adj.shape[0] * pred_adj.shape[1]) - graph_loss_function = torch.nn.BCELoss() - loss_g = graph_loss_function(pred_label, prior_label) - loss = loss + loss_g - - # metrics - for metric_name, metric_func in self.metrics.items(): - metric_item = metric_func(prediction, real_value, null_val=self.null_val) - self.update_epoch_meter('val_'+metric_name, metric_item.item()) - return loss - - @torch.no_grad() - @master_only - def test(self, train_epoch: int = None): - """test model. - - Args: - train_epoch (int, optional): current epoch if in training process. - """ - # test loop - prediction = [] - real_value = [] - for iter_index, data in enumerate(self.test_data_loader): - preds, testy, pred_adj, prior_adj = self.forward(data=data, epoch=train_epoch, iter_num=None, train=False) - prediction.append(preds) - real_value.append(testy) - prediction = torch.cat(prediction,dim=0) - real_value = torch.cat(real_value, dim=0) - # re-scale data - prediction = SCALER_REGISTRY.get(self.scaler['func'])(prediction, **self.scaler['args']) - real_value = SCALER_REGISTRY.get(self.scaler['func'])(real_value, **self.scaler['args']) - # summarize the results. - ## test performance of different horizon - for i in range(12): - # For horizon i, only calculate the metrics **at that time** slice here. - pred = prediction[:,i,:,:] - real = real_value[:,i,:,:] - # metrics - metric_results = {} - for metric_name, metric_func in self.metrics.items(): - metric_item = metric_func(pred, real, null_val=self.null_val) - metric_results[metric_name] = metric_item.item() - log = 'Evaluate best model on test data for horizon {:d}, Test MAE: {:.4f}, Test RMSE: {:.4f}, Test MAPE: {:.4f}' - log = log.format(i+1, metric_results['MAE'], metric_results['RMSE'], metric_results['MAPE']) - self.logger.info(log) - ## test performance overall - for metric_name, metric_func in self.metrics.items(): - metric_item = metric_func(prediction, real_value, null_val=self.null_val) - self.update_epoch_meter('test_'+metric_name, metric_item.item()) - metric_results[metric_name] = metric_item.item() diff --git a/basicts/runners/GraphWaveNet_runner.py b/basicts/runners/GraphWaveNet_runner.py deleted file mode 100644 index a71155ff..00000000 --- a/basicts/runners/GraphWaveNet_runner.py +++ /dev/null @@ -1,59 +0,0 @@ -import torch -from basicts.runners.short_mts_runner import MTSRunner - -class GraphWaveNetRunner(MTSRunner): - def __init__(self, cfg: dict): - super().__init__(cfg) - - def select_input_features(self, data: torch.Tensor) -> torch.Tensor: - """select input features. - - Args: - data (torch.Tensor): input history data, shape [B, L, N, C] - Returns: - torch.Tensor: reshaped data - """ - # select feature using self.forward_features - if self.forward_features is not None: - data = data[:, :, :, self.forward_features] - return data - - def select_target_features(self, data: torch.Tensor) -> torch.Tensor: - """select target feature - - Args: - data (torch.Tensor): prediction of the model with arbitrary shape. - - Returns: - torch.Tensor: reshaped data with shape [B, L, N, C] - """ - # select feature using self.target_features - data = data[:, :, :, self.target_features] - return data - - def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:bool = True, **kwargs) -> tuple: - """feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. - - Args: - data (tuple): data (future data, history ata) - epoch (int, optional): epoch number. Defaults to None. - iter_num (int, optional): iteration number. Defaults to None. - - Returns: - tuple: (prediction, real_value) - """ - # preprocess - future_data, history_data = data - history_data = self.to_running_device(history_data) # B, L, N, C - future_data = self.to_running_device(future_data) # B, L, N, C - B, L, N, C = future_data.shape - - history_data = self.select_input_features(history_data) - - # feed forward - prediction_data = self.model(history_data=history_data) # B, L, N, C - assert list(prediction_data.shape)[:3] == [B, L, N], "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" - # post process - prediction = self.select_target_features(prediction_data) - real_value = self.select_target_features(future_data) - return prediction, real_value diff --git a/basicts/runners/HI_runner.py b/basicts/runners/HI_runner.py deleted file mode 100644 index f5dd04dc..00000000 --- a/basicts/runners/HI_runner.py +++ /dev/null @@ -1,68 +0,0 @@ -import torch -from basicts.runners.short_mts_runner import MTSRunner - -class HIRunner(MTSRunner): - def __init__(self, cfg: dict): - super().__init__(cfg) - - def select_input_features(self, data: torch.Tensor) -> torch.Tensor: - """select input features. - - Args: - data (torch.Tensor): input history data, shape [B, L, N, C] - Returns: - torch.Tensor: reshaped data - """ - # select feature using self.forward_features - if self.forward_features is not None: - data = data[:, :, :, self.forward_features] - return data - - def select_target_features(self, data: torch.Tensor) -> torch.Tensor: - """select target feature - - Args: - data (torch.Tensor): prediction of the model with arbitrary shape. - - Returns: - torch.Tensor: reshaped data with shape [B, L, N, C] - """ - # select feature using self.target_features - data = data[:, :, :, self.target_features] - return data - - def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:bool = True, **kwargs) -> tuple: - """feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. - - Args: - data (tuple): data (future data, history data). [B, L, N, C] for each of them - epoch (int, optional): epoch number. Defaults to None. - iter_num (int, optional): iteration number. Defaults to None. - train (bool, optional): if in the training process. Defaults to True. - - Returns: - tuple: (prediction, real_value) - """ - # preprocess - future_data, history_data = data - history_data = self.to_running_device(history_data) # B, L, N, C - future_data = self.to_running_device(future_data) # B, L, N, C - B, L, N, C = future_data.shape - - history_data = self.select_input_features(history_data) - - # feed forward - prediction_data = self.model(history_data=history_data) # B, L, N, C - assert list(prediction_data.shape)[:3] == [B, L, N], "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" - # post process - prediction = self.select_target_features(prediction_data) - real_value = self.select_target_features(future_data) - return prediction, real_value - - def backward(self, loss: torch.Tensor): - """Backward and update params. - - Args: - loss (torch.Tensor): loss - """ - pass diff --git a/basicts/runners/LSTM_runner.py b/basicts/runners/LSTM_runner.py deleted file mode 100644 index c1afff63..00000000 --- a/basicts/runners/LSTM_runner.py +++ /dev/null @@ -1,60 +0,0 @@ -import torch -from basicts.runners.short_mts_runner import MTSRunner - -class LSTMRunner(MTSRunner): - def __init__(self, cfg: dict): - super().__init__(cfg) - - def select_input_features(self, data: torch.Tensor) -> torch.Tensor: - """select input features. - - Args: - data (torch.Tensor): input history data, shape [B, L, N, C] - Returns: - torch.Tensor: reshaped data - """ - # select feature using self.forward_features - if self.forward_features is not None: - data = data[:, :, :, self.forward_features] - return data - - def select_target_features(self, data: torch.Tensor) -> torch.Tensor: - """select target feature - - Args: - data (torch.Tensor): prediction of the model with arbitrary shape. - - Returns: - torch.Tensor: reshaped data with shape [B, L, N, C] - """ - # select feature using self.target_features - data = data[:, :, :, self.target_features] - return data - - def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:bool = True, **kwargs) -> tuple: - """feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. - - Args: - data (tuple): data (future data, history data). [B, L, N, C] for each of them - epoch (int, optional): epoch number. Defaults to None. - iter_num (int, optional): iteration number. Defaults to None. - train (bool, optional): if in the training process. Defaults to True. - - Returns: - tuple: (prediction, real_value) - """ - # preprocess - future_data, history_data = data - history_data = self.to_running_device(history_data) # B, L, N, C - future_data = self.to_running_device(future_data) # B, L, N, C - B, L, N, C = future_data.shape - - history_data = self.select_input_features(history_data) - - # feed forward - prediction_data = self.model(history_data=history_data) # B, L, N, C - assert list(prediction_data.shape)[:3] == [B, L, N], "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" - # post process - prediction = self.select_target_features(prediction_data) - real_value = self.select_target_features(future_data) - return prediction, real_value diff --git a/basicts/runners/STGCN_runner.py b/basicts/runners/STGCN_runner.py deleted file mode 100644 index ccd3b534..00000000 --- a/basicts/runners/STGCN_runner.py +++ /dev/null @@ -1,60 +0,0 @@ -import torch -from basicts.runners.short_mts_runner import MTSRunner - -class STGCNRunner(MTSRunner): - def __init__(self, cfg: dict): - super().__init__(cfg) - - def select_input_features(self, data: torch.Tensor) -> torch.Tensor: - """select input features. - - Args: - data (torch.Tensor): input history data, shape [B, L, N, C] - Returns: - torch.Tensor: reshaped data - """ - # select feature using self.forward_features - if self.forward_features is not None: - data = data[:, :, :, self.forward_features] - return data - - def select_target_features(self, data: torch.Tensor) -> torch.Tensor: - """select target feature - - Args: - data (torch.Tensor): prediction of the model with arbitrary shape. - - Returns: - torch.Tensor: reshaped data with shape [B, L, N, C] - """ - # select feature using self.target_features - data = data[:, :, :, self.target_features] - return data - - def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:bool = True, **kwargs) -> tuple: - """feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. - - Args: - data (tuple): data (future data, history data). [B, L, N, C] for each of them - epoch (int, optional): epoch number. Defaults to None. - iter_num (int, optional): iteration number. Defaults to None. - train (bool, optional): if in the training process. Defaults to True. - - Returns: - tuple: (prediction, real_value) - """ - # preprocess - future_data, history_data = data - history_data = self.to_running_device(history_data) # B, L, N, C - future_data = self.to_running_device(future_data) # B, L, N, C - B, L, N, C = future_data.shape - - history_data = self.select_input_features(history_data) - - # feed forward - prediction_data = self.model(history_data=history_data) # B, L, N, C - assert list(prediction_data.shape)[:3] == [B, L, N], "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" - # post process - prediction = self.select_target_features(prediction_data) - real_value = self.select_target_features(future_data) - return prediction, real_value diff --git a/basicts/runners/STID_runner.py b/basicts/runners/STID_runner.py deleted file mode 100644 index b5c85faa..00000000 --- a/basicts/runners/STID_runner.py +++ /dev/null @@ -1,60 +0,0 @@ -import torch -from basicts.runners.short_mts_runner import MTSRunner - -class STIDRunner(MTSRunner): - def __init__(self, cfg: dict): - super().__init__(cfg) - - def select_input_features(self, data: torch.Tensor) -> torch.Tensor: - """select input features. - - Args: - data (torch.Tensor): input history data, shape [B, L, N, C] - Returns: - torch.Tensor: reshaped data - """ - # select feature using self.forward_features - if self.forward_features is not None: - data = data[:, :, :, self.forward_features] - return data - - def select_target_features(self, data: torch.Tensor) -> torch.Tensor: - """select target feature - - Args: - data (torch.Tensor): prediction of the model with arbitrary shape. - - Returns: - torch.Tensor: reshaped data with shape [B, L, N, C] - """ - # select feature using self.target_features - data = data[:, :, :, self.target_features] - return data - - def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:bool = True, **kwargs) -> tuple: - """feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. - - Args: - data (tuple): data (future data, history data). [B, L, N, C] for each of them - epoch (int, optional): epoch number. Defaults to None. - iter_num (int, optional): iteration number. Defaults to None. - train (bool, optional): if in the training process. Defaults to True. - - Returns: - tuple: (prediction, real_value) - """ - # preprocess - future_data, history_data = data - history_data = self.to_running_device(history_data) # B, L, N, C - future_data = self.to_running_device(future_data) # B, L, N, C - B, L, N, C = future_data.shape - - history_data = self.select_input_features(history_data) - - # feed forward - prediction_data = self.model(history_data=history_data) # B, L, N, C - assert list(prediction_data.shape)[:3] == [B, L, N], "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" - # post process - prediction = self.select_target_features(prediction_data) - real_value = self.select_target_features(future_data) - return prediction, real_value diff --git a/basicts/runners/STNorm_runner.py b/basicts/runners/STNorm_runner.py deleted file mode 100644 index d0526424..00000000 --- a/basicts/runners/STNorm_runner.py +++ /dev/null @@ -1,60 +0,0 @@ -import torch -from basicts.runners.short_mts_runner import MTSRunner - -class STNormRunner(MTSRunner): - def __init__(self, cfg: dict): - super().__init__(cfg) - - def select_input_features(self, data: torch.Tensor) -> torch.Tensor: - """select input features. - - Args: - data (torch.Tensor): input history data, shape [B, L, N, C] - Returns: - torch.Tensor: reshaped data - """ - # select feature using self.forward_features - if self.forward_features is not None: - data = data[:, :, :, self.forward_features] - return data - - def select_target_features(self, data: torch.Tensor) -> torch.Tensor: - """select target feature - - Args: - data (torch.Tensor): prediction of the model with arbitrary shape. - - Returns: - torch.Tensor: reshaped data with shape [B, L, N, C] - """ - # select feature using self.target_features - data = data[:, :, :, self.target_features] - return data - - def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:bool = True, **kwargs) -> tuple: - """feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. - - Args: - data (tuple): data (future data, history data). [B, L, N, C] for each of them - epoch (int, optional): epoch number. Defaults to None. - iter_num (int, optional): iteration number. Defaults to None. - train (bool, optional): if in the training process. Defaults to True. - - Returns: - tuple: (prediction, real_value) - """ - # preprocess - future_data, history_data = data - history_data = self.to_running_device(history_data) # B, L, N, C - future_data = self.to_running_device(future_data) # B, L, N, C - B, L, N, C = future_data.shape - - history_data = self.select_input_features(history_data) - - # feed forward - prediction_data = self.model(history_data=history_data) # B, L, N, C - assert list(prediction_data.shape)[:3] == [B, L, N], "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" - # post process - prediction = self.select_target_features(prediction_data) - real_value = self.select_target_features(future_data) - return prediction, real_value diff --git a/basicts/runners/Stat_runner.py b/basicts/runners/Stat_runner.py deleted file mode 100644 index 01e72862..00000000 --- a/basicts/runners/Stat_runner.py +++ /dev/null @@ -1,60 +0,0 @@ -import torch -from basicts.runners.short_mts_runner import MTSRunner - -class StatRunner(MTSRunner): - def __init__(self, cfg: dict): - super().__init__(cfg) - - def select_input_features(self, data: torch.Tensor) -> torch.Tensor: - """select input features. - - Args: - data (torch.Tensor): input history data, shape [B, L, N, C] - Returns: - torch.Tensor: reshaped data - """ - # select feature using self.forward_features - if self.forward_features is not None: - data = data[:, :, :, self.forward_features] - return data - - def select_target_features(self, data: torch.Tensor) -> torch.Tensor: - """select target feature - - Args: - data (torch.Tensor): prediction of the model with arbitrary shape. - - Returns: - torch.Tensor: reshaped data with shape [B, L, N, C] - """ - # select feature using self.target_features - data = data[:, :, :, self.target_features] - return data - - def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:bool = True, **kwargs) -> tuple: - """feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. - - Args: - data (tuple): data (future data, history data). [B, L, N, C] for each of them - epoch (int, optional): epoch number. Defaults to None. - iter_num (int, optional): iteration number. Defaults to None. - train (bool, optional): if in the training process. Defaults to True. - - Returns: - tuple: (prediction, real_value) - """ - # preprocess - future_data, history_data = data - history_data = self.to_running_device(history_data) # B, L, N, C - future_data = self.to_running_device(future_data) # B, L, N, C - B, L, N, C = future_data.shape - - history_data = self.select_input_features(history_data) - - # feed forward - prediction_data = self.model(history_data=history_data, batch_seen=iter_num, epoch=epoch) # B, L, N, C - assert list(prediction_data.shape)[:3] == [B, L, N], "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" - # post process - prediction = self.select_target_features(prediction_data) - real_value = self.select_target_features(future_data) - return prediction, real_value diff --git a/basicts/runners/StemGNN_runner.py b/basicts/runners/StemGNN_runner.py deleted file mode 100644 index f6da133e..00000000 --- a/basicts/runners/StemGNN_runner.py +++ /dev/null @@ -1,61 +0,0 @@ -import torch -from tqdm import tqdm -from basicts.runners.short_mts_runner import MTSRunner - -class StemGNNRunner(MTSRunner): - def __init__(self, cfg: dict): - super().__init__(cfg) - - def select_input_features(self, data: torch.Tensor) -> torch.Tensor: - """select input features. - - Args: - data (torch.Tensor): input history data, shape [B, L, N, C] - Returns: - torch.Tensor: reshaped data - """ - # select feature using self.forward_features - if self.forward_features is not None: - data = data[:, :, :, self.forward_features][..., 0] # stemgnn only uses the first dimension - return data - - def select_target_features(self, data: torch.Tensor) -> torch.Tensor: - """select target feature - - Args: - data (torch.Tensor): prediction of the model with arbitrary shape. - - Returns: - torch.Tensor: reshaped data with shape [B, L, N, C] - """ - # select feature using self.target_features - data = data[:, :, :, self.target_features] - return data - - def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:bool = True, **kwargs) -> tuple: - """feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. - - Args: - data (tuple): data (future data, history data). [B, L, N, C] for each of them - epoch (int, optional): epoch number. Defaults to None. - iter_num (int, optional): iteration number. Defaults to None. - train (bool, optional): if in the training process. Defaults to True. - - Returns: - tuple: (prediction, real_value) - """ - # preprocess - future_data, history_data = data - history_data = self.to_running_device(history_data) # B, L, N, C - future_data = self.to_running_device(future_data) # B, L, N, C - B, L, N, C = future_data.shape - - history_data = self.select_input_features(history_data) - - # feed forward - prediction_data = self.model(history_data=history_data, batch_seen=iter_num, epoch=epoch) # B, L, N, C - assert list(prediction_data.shape)[:3] == [B, L, N], "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" - # post process - prediction = self.select_target_features(prediction_data) - real_value = self.select_target_features(future_data) - return prediction, real_value diff --git a/basicts/runners/__init__.py b/basicts/runners/__init__.py new file mode 100644 index 00000000..a5b5eaa5 --- /dev/null +++ b/basicts/runners/__init__.py @@ -0,0 +1,21 @@ +from .base_tsf_runner import BaseTimeSeriesForecastingRunner +from .runner_zoo.simple_tsf_runner import SimpleTimeSeriesForecastingRunner +from .runner_zoo.stid_runner import STIDRunner +from .runner_zoo.gwnet_runner import GraphWaveNetRunner +from .runner_zoo.dcrnn_runner import DCRNNRunner +from .runner_zoo.d2stgnn_runner import D2STGNNRunner +from .runner_zoo.stgcn_runner import STGCNRunner +from .runner_zoo.mtgnn_runner import MTGNNRunner +from .runner_zoo.stnorm_runner import STNormRunner +from .runner_zoo.agcrn_runner import AGCRNRunner +from .runner_zoo.stemgnn_runner import StemGNNRunner +from .runner_zoo.gts_runner import GTSRunner +from .runner_zoo.dgcrn_runner import DGCRNRunner +from .runner_zoo.linear_runner import LinearRunner + +__all__ = ["BaseTimeSeriesForecastingRunner", + "SimpleTimeSeriesForecastingRunner", "STIDRunner", + "GraphWaveNetRunner", "DCRNNRunner", "D2STGNNRunner", + "STGCNRunner", "MTGNNRunner", "STNormRunner", + "AGCRNRunner", "StemGNNRunner", "GTSRunner", + "DGCRNRunner", "LinearRunner"] diff --git a/basicts/runners/base_runner.py b/basicts/runners/base_runner.py index 38b77a45..070f0118 100644 --- a/basicts/runners/base_runner.py +++ b/basicts/runners/base_runner.py @@ -2,24 +2,22 @@ from typing import Dict import setproctitle - import torch from torch import nn from torch.utils.data import DataLoader - from easytorch import Runner -from easytorch.utils.dist import master_only from easytorch.utils import master_only from easytorch.core.data_loader import build_data_loader -from basicts.data.transforms import * -from basicts.archs import ARCH_REGISTRY class BaseRunner(Runner): - def __init__(self, cfg: dict): - """An expanded easytorch runner for benchmarking time series models. + """ + An expanded easytorch runner for benchmarking time series models. - Support test loader and test process. - Support setup_graph for the models acting like tensorflow. + """ + def __init__(self, cfg: dict): + """Init Args: cfg (dict): all in one configurations @@ -27,23 +25,26 @@ def __init__(self, cfg: dict): super().__init__(cfg) - self.val_interval = cfg['VAL'].get('INTERVAL', 1) # validate every `val_interval` epoch - self.test_interval = cfg['TEST'].get('INTERVAL', 1) # test every `test_interval` epoch + # validate every `val_interval` epoch + self.val_interval = cfg["VAL"].get("INTERVAL", 1) + # test every `test_interval` epoch + self.test_interval = cfg["TEST"].get("INTERVAL", 1) # declare data loader self.train_data_loader = None self.val_data_loader = None # set proctitle - proctitle_name = "{0}({1})".format(cfg['MODEL'].get("NAME", " "), cfg.get("DATASET_NAME", " ")) + proctitle_name = "{0}({1})".format(cfg["MODEL"].get( + "NAME", " "), cfg.get("DATASET_NAME", " ")) setproctitle.setproctitle("{0}@BasicTS".format(proctitle_name)) @staticmethod def define_model(cfg: Dict) -> nn.Module: - return ARCH_REGISTRY.build(cfg['MODEL']['NAME'], cfg['MODEL'].get('PARAM', {})) + return cfg["MODEL"]["ARCH"](**cfg.MODEL.PARAM) def build_train_data_loader(self, cfg: dict) -> DataLoader: - """Support 'setup_graph' for the models acting like tensorflow. + """Support "setup_graph" for the models acting like tensorflow. Args: cfg (dict): all in one configurations @@ -53,19 +54,19 @@ def build_train_data_loader(self, cfg: dict) -> DataLoader: """ train_data_loader = super().build_train_data_loader(cfg) - if cfg['TRAIN'].get('SETUP_GRAPH', False): + if cfg["TRAIN"].get("SETUP_GRAPH", False): for data in train_data_loader: self.setup_graph(data) break return train_data_loader - + def setup_graph(self, data: torch.Tensor): """Setup all parameters and the computation graph. Args: data (torch.Tensor): data necessary for a forward pass """ - + pass def init_training(self, cfg: dict): @@ -77,7 +78,7 @@ def init_training(self, cfg: dict): super().init_training(cfg) # init test - if hasattr(cfg, 'TEST'): + if hasattr(cfg, "TEST"): self.init_test(cfg) @master_only @@ -88,9 +89,9 @@ def init_test(self, cfg: dict): cfg (dict): config """ - self.test_interval = cfg['TEST'].get('INTERVAL', 1) + self.test_interval = cfg["TEST"].get("INTERVAL", 1) self.test_data_loader = self.build_test_data_loader(cfg) - self.register_epoch_meter('test_time', 'test', '{:.2f} (s)', plt=False) + self.register_epoch_meter("test_time", "test", "{:.2f} (s)", plt=False) def build_test_data_loader(self, cfg: dict) -> DataLoader: """Build val dataset and dataloader. @@ -105,7 +106,7 @@ def build_test_data_loader(self, cfg: dict) -> DataLoader: """ dataset = self.build_test_dataset(cfg) - return build_data_loader(dataset, cfg['TEST']['DATA']) + return build_data_loader(dataset, cfg["TEST"]["DATA"]) @staticmethod def build_test_dataset(cfg: dict): @@ -129,9 +130,9 @@ def on_epoch_end(self, epoch: int): """ # print train meters - self.print_epoch_meters('train') + self.print_epoch_meters("train") # tensorboard plt meters - self.plt_epoch_meters('train', epoch) + self.plt_epoch_meters("train", epoch) # validate if self.val_data_loader is not None and epoch % self.val_interval == 0: self.validate(train_epoch=epoch) @@ -163,15 +164,15 @@ def test_process(self, cfg: dict = None, train_epoch: int = None): self.model.eval() # test - self.test(train_epoch=train_epoch) + self.test() test_end_time = time.time() - self.update_epoch_meter('test_time', test_start_time - test_end_time) + self.update_epoch_meter("test_time", test_start_time - test_end_time) # print test meters - self.print_epoch_meters('test') + self.print_epoch_meters("test") if train_epoch is not None: # tensorboard plt meters - self.plt_epoch_meters('test', train_epoch // self.test_interval) + self.plt_epoch_meters("test", train_epoch // self.test_interval) self.on_test_end() diff --git a/basicts/runners/short_mts_runner.py b/basicts/runners/base_tsf_runner.py similarity index 51% rename from basicts/runners/short_mts_runner.py rename to basicts/runners/base_tsf_runner.py index cd9a1f05..5b271571 100644 --- a/basicts/runners/short_mts_runner.py +++ b/basicts/runners/base_tsf_runner.py @@ -1,45 +1,48 @@ import math from typing import Tuple, Union, Optional + import torch -from torch import nn -import numpy as np -from basicts.runners.base_runner import BaseRunner -from basicts.data.transforms import SCALER_REGISTRY -from basicts.utils.serialization import load_pkl from easytorch.utils.dist import master_only -class MTSRunner(BaseRunner): - """Runner for short term multivariate time series forecasting datasets. Typically, models predict the future 12 time steps based on historical time series. +from .base_runner import BaseRunner +from ..data import SCALER_REGISTRY +from ..utils import load_pkl +from ..metrics import masked_mae, masked_mape, masked_rmse + + +class BaseTimeSeriesForecastingRunner(BaseRunner): + """ + Runner for short term multivariate time series forecasting datasets. + Typically, models predict the future 12 time steps based on historical time series. Features: - Evaluate at horizon 3, 6, 12, and overall. - - Metrics: MAE, RMSE, MAPE. + - Metrics: MAE, RMSE, MAPE. The best model is the one with the smallest mae at validation. + - Loss: MAE (masked_mae). Allow customization. - Support curriculum learning. - Users only need to implement the `forward` function. - - Args: - BaseRunner (easytorch.easytorch.runner): base runner """ def __init__(self, cfg: dict): super().__init__(cfg) - self.dataset_name = cfg['DATASET_NAME'] - self.null_val = cfg['TRAIN'].get('NULL_VAL', 0) # different datasets have different null_values, e.g., 0.0 or np.nan. - self.dataset_type = cfg['DATASET_TYPE'] - self.forward_features = cfg['MODEL'].get('FROWARD_FEATURES', None) - self.target_features = cfg['MODEL'].get('TARGET_FEATURES', None) + self.dataset_name = cfg["DATASET_NAME"] + # different datasets have different null_values, e.g., 0.0 or np.nan. + self.null_val = cfg["TRAIN"].get("NULL_VAL", 0) + self.dataset_type = cfg["DATASET_TYPE"] # read scaler for re-normalization self.scaler = load_pkl("datasets/" + self.dataset_name + "/scaler.pkl") # define loss - self.loss = cfg['TRAIN']['LOSS'] + self.loss = cfg["TRAIN"]["LOSS"] # define metric - self.metrics = cfg['METRICS'] + self.metrics = {"MAE": masked_mae, "RMSE": masked_rmse, "MAPE": masked_mape} # curriculum learning for output. Note that this is different from the CL in Seq2Seq archs. - self.cl_param = cfg.TRAIN.get('CL', None) + self.cl_param = cfg.TRAIN.get("CL", None) if self.cl_param is not None: - self.warm_up_epochs = cfg.TRAIN.CL.get('WARM_EPOCHS', 0) - self.cl_epochs = cfg.TRAIN.CL.get('CL_EPOCHS') - self.prediction_length = cfg.TRAIN.CL.get('PREDICTION_LENGTH') + self.warm_up_epochs = cfg.TRAIN.CL.get("WARM_EPOCHS", 0) + self.cl_epochs = cfg.TRAIN.CL.get("CL_EPOCHS") + self.prediction_length = cfg.TRAIN.CL.get("PREDICTION_LENGTH") + # evaluation horizon + self.evaluation_horizons = cfg["TEST"].get("EVALUATION_HORIZONS", range(12)) def init_training(self, cfg: dict): """Initialize training. @@ -49,10 +52,10 @@ def init_training(self, cfg: dict): Args: cfg (dict): config """ - + super().init_training(cfg) - for key, value in self.metrics.items(): - self.register_epoch_meter("train_"+key, 'train', '{:.4f}') + for key, _ in self.metrics.items(): + self.register_epoch_meter("train_"+key, "train", "{:.4f}") def init_validation(self, cfg: dict): """Initialize validation. @@ -64,8 +67,8 @@ def init_validation(self, cfg: dict): """ super().init_validation(cfg) - for key, value in self.metrics.items(): - self.register_epoch_meter("val_"+key, 'val', '{:.4f}') + for key, _ in self.metrics.items(): + self.register_epoch_meter("val_"+key, "val", "{:.4f}") def init_test(self, cfg: dict): """Initialize test. @@ -77,8 +80,8 @@ def init_test(self, cfg: dict): """ super().init_test(cfg) - for key, value in self.metrics.items(): - self.register_epoch_meter("test_"+key, 'test', '{:.4f}') + for key, _ in self.metrics.items(): + self.register_epoch_meter("test_"+key, "test", "{:.4f}") def build_train_dataset(self, cfg: dict): """Build MNIST train dataset @@ -90,14 +93,16 @@ def build_train_dataset(self, cfg: dict): train dataset (Dataset) """ - raw_file_path = cfg["TRAIN"]["DATA"]["DIR"] + "/data.pkl" - index_file_path = cfg["TRAIN"]["DATA"]["DIR"] + "/index.pkl" - batch_size = cfg['TRAIN']['DATA']['BATCH_SIZE'] - dataset = cfg['DATASET_CLS'](raw_file_path, index_file_path, mode='train') + raw_file_path = "{0}/data.pkl".format(cfg["TRAIN"]["DATA"]["DIR"]) + index_file_path = "{0}/index_in{1}_out{2}.pkl".format( + cfg["TRAIN"]["DATA"]["DIR"], cfg["DATASET_INPUT_LEN"], cfg["DATASET_OUTPUT_LEN"]) + batch_size = cfg["TRAIN"]["DATA"]["BATCH_SIZE"] + dataset = cfg["DATASET_CLS"]( + raw_file_path, index_file_path, mode="train") print("train len: {0}".format(len(dataset))) - + self.iter_per_epoch = math.ceil(len(dataset) / batch_size) - + return dataset @staticmethod @@ -108,12 +113,14 @@ def build_val_dataset(cfg: dict): cfg (dict): config Returns: - train dataset (Dataset) + validation dataset (Dataset) """ - raw_file_path = cfg["VAL"]["DATA"]["DIR"] + "/data.pkl" - index_file_path = cfg["VAL"]["DATA"]["DIR"] + "/index.pkl" - dataset = cfg['DATASET_CLS'](raw_file_path, index_file_path, mode='valid') + raw_file_path = "{0}/data.pkl".format(cfg["VAL"]["DATA"]["DIR"]) + index_file_path = "{0}/index_in{1}_out{2}.pkl".format( + cfg["VAL"]["DATA"]["DIR"], cfg["DATASET_INPUT_LEN"], cfg["DATASET_OUTPUT_LEN"]) + dataset = cfg["DATASET_CLS"]( + raw_file_path, index_file_path, mode="valid") print("val len: {0}".format(len(dataset))) return dataset @@ -128,14 +135,16 @@ def build_test_dataset(cfg: dict): train dataset (Dataset) """ - raw_file_path = cfg["TEST"]["DATA"]["DIR"] + "/data.pkl" - index_file_path = cfg["TEST"]["DATA"]["DIR"] + "/index.pkl" - dataset = cfg['DATASET_CLS'](raw_file_path, index_file_path, mode='test') + raw_file_path = "{0}/data.pkl".format(cfg["TEST"]["DATA"]["DIR"]) + index_file_path = "{0}/index_in{1}_out{2}.pkl".format( + cfg["TEST"]["DATA"]["DIR"], cfg["DATASET_INPUT_LEN"], cfg["DATASET_OUTPUT_LEN"]) + dataset = cfg["DATASET_CLS"]( + raw_file_path, index_file_path, mode="test") print("test len: {0}".format(len(dataset))) return dataset def curriculum_learning(self, epoch: int = None) -> int: - """calculate task level in curriculum learning. + """Calculate task level in curriculum learning. Args: epoch (int, optional): current epoch if in training process, else None. Defaults to None. @@ -156,8 +165,8 @@ def curriculum_learning(self, epoch: int = None) -> int: cl_length = min(_, self.prediction_length) return cl_length - def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:bool = True, **kwargs) -> tuple: - """feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. + def forward(self, data: tuple, epoch: int = None, iter_num: int = None, train: bool = True, **kwargs) -> tuple: + """Feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. Args: data (tuple): data (future data, history data). [B, L, N, C] for each of them @@ -168,6 +177,7 @@ def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:boo Returns: tuple: (prediction, real_value). [B, L, N, C] for each of them. """ + raise NotImplementedError() def train_iters(self, epoch: int, iter_index: int, data: Union[torch.Tensor, Tuple]) -> torch.Tensor: @@ -183,20 +193,23 @@ def train_iters(self, epoch: int, iter_index: int, data: Union[torch.Tensor, Tup """ iter_num = (epoch-1) * self.iter_per_epoch + iter_index - prediction, real_value = self.forward(data=data, epoch=epoch, iter_num=iter_num, train=True) + forward_return = list(self.forward(data=data, epoch=epoch, iter_num=iter_num, train=True)) # re-scale data - prediction = SCALER_REGISTRY.get(self.scaler['func'])(prediction, **self.scaler['args']) - real_value = SCALER_REGISTRY.get(self.scaler['func'])(real_value, **self.scaler['args']) + prediction_rescaled = SCALER_REGISTRY.get(self.scaler["func"])(forward_return[0], **self.scaler["args"]) + real_value_rescaled = SCALER_REGISTRY.get(self.scaler["func"])(forward_return[1], **self.scaler["args"]) # loss if self.cl_param: cl_length = self.curriculum_learning(epoch=epoch) - loss = self.loss(prediction[:, :cl_length, :, :], real_value[:, :cl_length, :, :], null_val=self.null_val) + forward_return[0] = prediction_rescaled[:, :cl_length, :, :] + forward_return[1] = real_value_rescaled[:, :cl_length, :, :] else: - loss = self.loss(prediction, real_value, null_val=self.null_val) + forward_return[0] = prediction_rescaled + forward_return[1] = real_value_rescaled + loss = self.loss(*forward_return, null_val=self.null_val) # metrics for metric_name, metric_func in self.metrics.items(): - metric_item = metric_func(prediction, real_value, null_val=self.null_val) - self.update_epoch_meter('train_'+metric_name, metric_item.item()) + metric_item = metric_func(*forward_return[:2], null_val=self.null_val) + self.update_epoch_meter("train_"+metric_name, metric_item.item()) return loss def val_iters(self, iter_index: int, data: Union[torch.Tensor, Tuple]): @@ -208,20 +221,18 @@ def val_iters(self, iter_index: int, data: Union[torch.Tensor, Tuple]): iter_index (int): current iter. """ - prediction, real_value = self.forward(data=data, epoch=None, iter_num=None, train=False) + forward_return = self.forward(data=data, epoch=None, iter_num=None, train=False) # re-scale data - prediction = SCALER_REGISTRY.get(self.scaler['func'])(prediction, **self.scaler['args']) - real_value = SCALER_REGISTRY.get(self.scaler['func'])(real_value, **self.scaler['args']) - # loss - mae = self.loss(prediction, real_value, null_val=self.null_val) + prediction_rescaled = SCALER_REGISTRY.get(self.scaler["func"])(forward_return[0], **self.scaler["args"]) + real_value_rescaled = SCALER_REGISTRY.get(self.scaler["func"])(forward_return[1], **self.scaler["args"]) # metrics for metric_name, metric_func in self.metrics.items(): - metric_item = metric_func(prediction, real_value, null_val=self.null_val) - self.update_epoch_meter('val_'+metric_name, metric_item.item()) + metric_item = metric_func(prediction_rescaled, real_value_rescaled, null_val=self.null_val) + self.update_epoch_meter("val_"+metric_name, metric_item.item()) @torch.no_grad() @master_only - def test(self, train_epoch: int = None): + def test(self): """Evaluate the model. Args: @@ -230,34 +241,38 @@ def test(self, train_epoch: int = None): # test loop prediction = [] - real_value = [] - for iter_index, data in enumerate(self.test_data_loader): - preds, testy = self.forward(data, epoch=train_epoch, iter_num=None, train=False) - prediction.append(preds) - real_value.append(testy) - prediction = torch.cat(prediction,dim=0) + real_value = [] + for _, data in enumerate(self.test_data_loader): + forward_return = self.forward(data, epoch=None, iter_num=None, train=False) + prediction.append(forward_return[0]) # preds = forward_return[0] + real_value.append(forward_return[1]) # testy = forward_return[1] + prediction = torch.cat(prediction, dim=0) real_value = torch.cat(real_value, dim=0) # re-scale data - prediction = SCALER_REGISTRY.get(self.scaler['func'])(prediction, **self.scaler['args']) - real_value = SCALER_REGISTRY.get(self.scaler['func'])(real_value, **self.scaler['args']) + prediction = SCALER_REGISTRY.get(self.scaler["func"])( + prediction, **self.scaler["args"]) + real_value = SCALER_REGISTRY.get(self.scaler["func"])( + real_value, **self.scaler["args"]) # summarize the results. - ## test performance of different horizon - for i in range(12): + # test performance of different horizon + for i in self.evaluation_horizons: # For horizon i, only calculate the metrics **at that time** slice here. - pred = prediction[:,i,:,:] - real = real_value[:,i,:,:] + pred = prediction[:, i, :, :] + real = real_value[:, i, :, :] # metrics metric_results = {} for metric_name, metric_func in self.metrics.items(): metric_item = metric_func(pred, real, null_val=self.null_val) metric_results[metric_name] = metric_item.item() - log = 'Evaluate best model on test data for horizon {:d}, Test MAE: {:.4f}, Test RMSE: {:.4f}, Test MAPE: {:.4f}' - log = log.format(i+1, metric_results['MAE'], metric_results['RMSE'], metric_results['MAPE']) + log = "Evaluate best model on test data for horizon " + \ + "{:d}, Test MAE: {:.4f}, Test RMSE: {:.4f}, Test MAPE: {:.4f}" + log = log.format( + i+1, metric_results["MAE"], metric_results["RMSE"], metric_results["MAPE"]) self.logger.info(log) - ## test performance overall + # test performance overall for metric_name, metric_func in self.metrics.items(): metric_item = metric_func(prediction, real_value, null_val=self.null_val) - self.update_epoch_meter('test_'+metric_name, metric_item.item()) + self.update_epoch_meter("test_"+metric_name, metric_item.item()) metric_results[metric_name] = metric_item.item() @master_only @@ -269,4 +284,4 @@ def on_validating_end(self, train_epoch: Optional[int]): """ if train_epoch is not None: - self.save_best_model(train_epoch, 'val_MAE', greater_best=False) + self.save_best_model(train_epoch, "val_MAE", greater_best=False) diff --git a/basicts/runners/runner_zoo/agcrn_runner.py b/basicts/runners/runner_zoo/agcrn_runner.py new file mode 100644 index 00000000..cc3dbe74 --- /dev/null +++ b/basicts/runners/runner_zoo/agcrn_runner.py @@ -0,0 +1 @@ +from .simple_tsf_runner import SimpleTimeSeriesForecastingRunner as AGCRNRunner diff --git a/basicts/runners/runner_zoo/d2stgnn_runner.py b/basicts/runners/runner_zoo/d2stgnn_runner.py new file mode 100644 index 00000000..88b9f4a3 --- /dev/null +++ b/basicts/runners/runner_zoo/d2stgnn_runner.py @@ -0,0 +1 @@ +from .simple_tsf_runner import SimpleTimeSeriesForecastingRunner as D2STGNNRunner diff --git a/basicts/runners/runner_zoo/dcrnn_runner.py b/basicts/runners/runner_zoo/dcrnn_runner.py new file mode 100644 index 00000000..60c37bc6 --- /dev/null +++ b/basicts/runners/runner_zoo/dcrnn_runner.py @@ -0,0 +1,84 @@ +import torch + +from ..base_tsf_runner import BaseTimeSeriesForecastingRunner + + +class DCRNNRunner(BaseTimeSeriesForecastingRunner): + """Runner for DCRNN: add setup_graph and teacher forcing.""" + + def __init__(self, cfg: dict): + super().__init__(cfg) + self.forward_features = cfg["MODEL"].get("FROWARD_FEATURES", None) + self.target_features = cfg["MODEL"].get("TARGET_FEATURES", None) + + def setup_graph(self, data): + """The dcrnn official codes act like tensorflow, which create parameters in the first feedforward process.""" + try: + self.train_iters(1, 0, data) + except AttributeError: + pass + + def select_input_features(self, data: torch.Tensor) -> torch.Tensor: + """Select input features and reshape data to fit the target model. + + Args: + data (torch.Tensor): input history data, shape [B, L, N, C]. + + Returns: + torch.Tensor: reshaped data + """ + + # select feature using self.forward_features + if self.forward_features is not None: + data = data[:, :, :, self.forward_features] + return data + + def select_target_features(self, data: torch.Tensor) -> torch.Tensor: + """Select target features and reshape data back to the BasicTS framework + + Args: + data (torch.Tensor): prediction of the model with arbitrary shape. + + Returns: + torch.Tensor: reshaped data with shape [B, L, N, C] + """ + + # select feature using self.target_features + data = data[:, :, :, self.target_features] + return data + + def forward(self, data: tuple, epoch: int = None, iter_num: int = None, train: bool = True) -> tuple: + """Feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. + + Args: + data (tuple): data (future data, history data). [B, L, N, C] for each of them + epoch (int, optional): epoch number. Defaults to None. + iter_num (int, optional): iteration number. Defaults to None. + train (bool, optional): if in the training process. Defaults to True. + + Returns: + tuple: (prediction, real_value) + """ + + # preprocess + future_data, history_data = data + history_data = self.to_running_device(history_data) # B, L, N, C + future_data = self.to_running_device(future_data) # B, L, N, C + batch_size, length, num_nodes, _ = future_data.shape + + history_data = self.select_input_features(history_data) + if train: + # teacher forcing only use the first dimension. + _future_data = future_data[..., [0]] + else: + _future_data = None + + # feed forward + prediction_data = self.model(history_data=history_data, future_data=_future_data, + batch_seen=iter_num if self.model.training else None, epoch=epoch) + assert list(prediction_data.shape)[:3] == [batch_size, length, num_nodes], \ + "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" + # post process + prediction = self.select_target_features(prediction_data) + real_value = self.select_target_features(future_data) + return prediction, real_value diff --git a/basicts/runners/runner_zoo/dgcrn_runner.py b/basicts/runners/runner_zoo/dgcrn_runner.py new file mode 100644 index 00000000..97568230 --- /dev/null +++ b/basicts/runners/runner_zoo/dgcrn_runner.py @@ -0,0 +1 @@ +from .simple_tsf_runner import SimpleTimeSeriesForecastingRunner as DGCRNRunner diff --git a/basicts/runners/DGCRN_runner.py b/basicts/runners/runner_zoo/gts_runner.py similarity index 60% rename from basicts/runners/DGCRN_runner.py rename to basicts/runners/runner_zoo/gts_runner.py index dccb465d..838954c1 100644 --- a/basicts/runners/DGCRN_runner.py +++ b/basicts/runners/runner_zoo/gts_runner.py @@ -1,25 +1,37 @@ import torch -from basicts.runners.short_mts_runner import MTSRunner -class DGCRNRunner(MTSRunner): +from ..base_tsf_runner import BaseTimeSeriesForecastingRunner + + +class GTSRunner(BaseTimeSeriesForecastingRunner): def __init__(self, cfg: dict): super().__init__(cfg) + self.forward_features = cfg["MODEL"].get("FROWARD_FEATURES", None) + self.target_features = cfg["MODEL"].get("TARGET_FEATURES", None) + + def setup_graph(self, data): + try: + self.train_iters(1, 0, data) + except AttributeError: + pass def select_input_features(self, data: torch.Tensor) -> torch.Tensor: - """select input features and reshape data to fit the target model. + """Select input features and reshape data to fit the target model. Args: - data (torch.Tensor): input history data, shape [B, L, N, C] + data (torch.Tensor): input history data, shape [B, L, N, C]. + Returns: torch.Tensor: reshaped data """ + # select feature using self.forward_features if self.forward_features is not None: data = data[:, :, :, self.forward_features] return data - + def select_target_features(self, data: torch.Tensor) -> torch.Tensor: - """select target features and reshape data back to the BasicTS framework + """Select target features and reshape data back to the BasicTS framework Args: data (torch.Tensor): prediction of the model with arbitrary shape. @@ -27,6 +39,7 @@ def select_target_features(self, data: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: reshaped data with shape [B, L, N, C] """ + # select feature using self.target_features data = data[:, :, :, self.target_features] return data @@ -43,19 +56,25 @@ def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:boo Returns: tuple: (prediction, real_value) """ + # preprocess future_data, history_data = data history_data = self.to_running_device(history_data) # B, L, N, C future_data = self.to_running_device(future_data) # B, L, N, C - B, L, N, C = future_data.shape + batch_size, length, num_nodes, _ = future_data.shape - history_data = self.select_input_features(history_data) - future_data_ = self.select_input_features(future_data) + history_data = self.select_input_features(history_data) + if train: + # teacher forcing only use the first dimension. + _future_data = future_data[..., [0]] + else: + _future_data = None # feed forward - prediction_data = self.model(history_data=history_data, future_data=future_data_, batch_seen=iter_num, task_level=self.curriculum_learning(epoch)) # B, L, N, C - assert list(prediction_data.shape)[:3] == [B, L, N], "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" + prediction_data, pred_adj, prior_adj = self.model(history_data=history_data, future_data=_future_data, batch_seen=iter_num, epoch=epoch) # B, L, N, C + assert list(prediction_data.shape)[:3] == [batch_size, length, num_nodes], \ + "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" # post process prediction = self.select_target_features(prediction_data) real_value = self.select_target_features(future_data) - return prediction, real_value + return prediction, real_value, pred_adj, prior_adj diff --git a/basicts/runners/runner_zoo/gwnet_runner.py b/basicts/runners/runner_zoo/gwnet_runner.py new file mode 100644 index 00000000..e5c51c38 --- /dev/null +++ b/basicts/runners/runner_zoo/gwnet_runner.py @@ -0,0 +1 @@ +from .simple_tsf_runner import SimpleTimeSeriesForecastingRunner as GraphWaveNetRunner diff --git a/basicts/runners/runner_zoo/linear_runner.py b/basicts/runners/runner_zoo/linear_runner.py new file mode 100644 index 00000000..6b60a658 --- /dev/null +++ b/basicts/runners/runner_zoo/linear_runner.py @@ -0,0 +1,3 @@ +from .simple_tsf_runner import SimpleTimeSeriesForecastingRunner as LinearRunner + +__all__ = ["LinearRunner"] diff --git a/basicts/runners/MTGNN_runner.py b/basicts/runners/runner_zoo/mtgnn_runner.py similarity index 70% rename from basicts/runners/MTGNN_runner.py rename to basicts/runners/runner_zoo/mtgnn_runner.py index 057e69b5..459ead8b 100644 --- a/basicts/runners/MTGNN_runner.py +++ b/basicts/runners/runner_zoo/mtgnn_runner.py @@ -1,32 +1,39 @@ +from typing import Tuple, Union + import torch import numpy as np -from typing import Tuple, Union -from basicts.runners.short_mts_runner import MTSRunner +from ..base_tsf_runner import BaseTimeSeriesForecastingRunner -class MTGNNRunner(MTSRunner): + +class MTGNNRunner(BaseTimeSeriesForecastingRunner): def __init__(self, cfg: dict): super().__init__(cfg) + self.forward_features = cfg["MODEL"].get("FROWARD_FEATURES", None) + self.target_features = cfg["MODEL"].get("TARGET_FEATURES", None) + # graph training self.step_size = cfg.TRAIN.CUSTOM.STEP_SIZE self.num_nodes = cfg.TRAIN.CUSTOM.NUM_NODES self.num_split = cfg.TRAIN.CUSTOM.NUM_SPLIT - self.perm = None + self.perm = None def select_input_features(self, data: torch.Tensor) -> torch.Tensor: - """select input features. + """Select input features. Args: data (torch.Tensor): input history data, shape [B, L, N, C] + Returns: torch.Tensor: reshaped data """ + # select feature using self.forward_features if self.forward_features is not None: data = data[:, :, :, self.forward_features] return data - + def select_target_features(self, data: torch.Tensor) -> torch.Tensor: - """select target feature + """Select target feature Args: data (torch.Tensor): prediction of the model with arbitrary shape. @@ -34,12 +41,13 @@ def select_target_features(self, data: torch.Tensor) -> torch.Tensor: Returns: torch.Tensor: reshaped data with shape [B, L, N, C] """ + # select feature using self.target_features data = data[:, :, :, self.target_features] return data - def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:bool = True, **kwargs) -> tuple: - """feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. + def forward(self, data: tuple, epoch: int = None, iter_num: int = None, train: bool = True, **kwargs) -> tuple: + """Feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. Args: data (tuple): data (future data, history data). [B, L, N, C] for each of them @@ -50,20 +58,23 @@ def forward(self, data: tuple, epoch:int = None, iter_num: int = None, train:boo Returns: tuple: (prediction, real_value). [B, L, N, C] for each of them. """ + if train: future_data, history_data, idx = data else: future_data, history_data = data idx = None - history_data = self.to_running_device(history_data) # B, L, N, C - future_data = self.to_running_device(future_data) # B, L, N, C - B, L, N, C = future_data.shape + history_data = self.to_running_device(history_data) # B, L, N, C + future_data = self.to_running_device(future_data) # B, L, N, C + B, L, N, C = future_data.shape - history_data = self.select_input_features(history_data) - - prediction_data = self.model(history_data=history_data, idx=idx, batch_seen=iter_num, epoch=epoch) # B, L, N, C - assert list(prediction_data.shape)[:3] == [B, L, N], "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" + history_data = self.select_input_features(history_data) + + prediction_data = self.model( + history_data=history_data, idx=idx, batch_seen=iter_num, epoch=epoch) # B, L, N, C + assert list(prediction_data.shape)[:3] == [ + B, L, N], "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" # post process prediction = self.select_target_features(prediction_data) real_value = self.select_target_features(future_data) @@ -82,7 +93,8 @@ def train_iters(self, epoch: int, iter_index: int, data: Union[torch.Tensor, Tup Returns: loss (torch.Tensor) """ - if iter_index%self.step_size==0: + + if iter_index % self.step_size == 0: self.perm = np.random.permutation(range(self.num_nodes)) num_sub = int(self.num_nodes/self.num_split) for j in range(self.num_split): @@ -91,7 +103,7 @@ def train_iters(self, epoch: int, iter_index: int, data: Union[torch.Tensor, Tup raise else: idx = self.perm[j * num_sub:] - idx = torch.tensor(idx) + idx = torch.tensor(idx) future_data, history_data = data data = future_data[:, :, idx, :], history_data[:, :, idx, :], idx loss = super().train_iters(epoch, iter_index, data) diff --git a/basicts/runners/runner_zoo/simple_tsf_runner.py b/basicts/runners/runner_zoo/simple_tsf_runner.py new file mode 100644 index 00000000..26261a9e --- /dev/null +++ b/basicts/runners/runner_zoo/simple_tsf_runner.py @@ -0,0 +1,77 @@ +import torch + +from ..base_tsf_runner import BaseTimeSeriesForecastingRunner + + +class SimpleTimeSeriesForecastingRunner(BaseTimeSeriesForecastingRunner): + """Simple Runner: select forward features and target features.""" + + def __init__(self, cfg: dict): + super().__init__(cfg) + self.forward_features = cfg["MODEL"].get("FROWARD_FEATURES", None) + self.target_features = cfg["MODEL"].get("TARGET_FEATURES", None) + + def select_input_features(self, data: torch.Tensor) -> torch.Tensor: + """Select input features. + + Args: + data (torch.Tensor): input history data, shape [B, L, N, C] + + Returns: + torch.Tensor: reshaped data + """ + + # select feature using self.forward_features + if self.forward_features is not None: + data = data[:, :, :, self.forward_features] + return data + + def select_target_features(self, data: torch.Tensor) -> torch.Tensor: + """Select target feature. + + Args: + data (torch.Tensor): prediction of the model with arbitrary shape. + + Returns: + torch.Tensor: reshaped data with shape [B, L, N, C] + """ + + # select feature using self.target_features + data = data[:, :, :, self.target_features] + return data + + def forward(self, data: tuple, epoch: int = None, iter_num: int = None, train: bool = True, **kwargs) -> tuple: + """Feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. + + Args: + data (tuple): data (future data, history ata). + epoch (int, optional): epoch number. Defaults to None. + iter_num (int, optional): iteration number. Defaults to None. + train (bool, optional): if in the training process. Defaults to True. + + Returns: + tuple: (prediction, real_value) + """ + + # preprocess + future_data, history_data = data + history_data = self.to_running_device(history_data) # B, L, N, C + future_data = self.to_running_device(future_data) # B, L, N, C + batch_size, length, num_nodes, _ = future_data.shape + + history_data = self.select_input_features(history_data) + _future_data = self.select_input_features(future_data) + + # curriculum learning + if self.cl_param is None: + prediction_data = self.model(history_data=history_data, future_data=_future_data, batch_seen=iter_num, epoch=epoch, train=train) + else: + task_level = self.curriculum_learning(epoch) + prediction_data = self.model(history_data=history_data, future_data=_future_data, batch_seen=iter_num, epoch=epoch, train=train, task_level=task_level) + # feed forward + assert list(prediction_data.shape)[:3] == [batch_size, length, num_nodes], \ + "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" + # post process + prediction = self.select_target_features(prediction_data) + real_value = self.select_target_features(future_data) + return prediction, real_value diff --git a/basicts/runners/runner_zoo/stemgnn_runner.py b/basicts/runners/runner_zoo/stemgnn_runner.py new file mode 100644 index 00000000..2260cab4 --- /dev/null +++ b/basicts/runners/runner_zoo/stemgnn_runner.py @@ -0,0 +1 @@ +from .simple_tsf_runner import SimpleTimeSeriesForecastingRunner as StemGNNRunner diff --git a/basicts/runners/runner_zoo/stgcn_runner.py b/basicts/runners/runner_zoo/stgcn_runner.py new file mode 100644 index 00000000..2eb8b807 --- /dev/null +++ b/basicts/runners/runner_zoo/stgcn_runner.py @@ -0,0 +1 @@ +from .simple_tsf_runner import SimpleTimeSeriesForecastingRunner as STGCNRunner diff --git a/basicts/runners/runner_zoo/stid_runner.py b/basicts/runners/runner_zoo/stid_runner.py new file mode 100644 index 00000000..7f5b98c5 --- /dev/null +++ b/basicts/runners/runner_zoo/stid_runner.py @@ -0,0 +1 @@ +from .simple_tsf_runner import SimpleTimeSeriesForecastingRunner as STIDRunner diff --git a/basicts/runners/runner_zoo/stnorm_runner.py b/basicts/runners/runner_zoo/stnorm_runner.py new file mode 100644 index 00000000..aeeaa236 --- /dev/null +++ b/basicts/runners/runner_zoo/stnorm_runner.py @@ -0,0 +1 @@ +from .simple_tsf_runner import SimpleTimeSeriesForecastingRunner as STNormRunner diff --git a/basicts/utils/__init__.py b/basicts/utils/__init__.py new file mode 100644 index 00000000..cad8f2e0 --- /dev/null +++ b/basicts/utils/__init__.py @@ -0,0 +1,4 @@ +from .serialization import load_adj, load_pkl, dump_pkl, load_node2vec_emb +from .misc import clock, check_nan_inf, remove_nan_inf + +__all__ = ["load_adj", "load_pkl", "dump_pkl", "load_node2vec_emb", "clock", "check_nan_inf", "remove_nan_inf"] diff --git a/basicts/utils/adjacent_matrix_norm.py b/basicts/utils/adjacent_matrix_norm.py index 058a0928..417d4ac6 100644 --- a/basicts/utils/adjacent_matrix_norm.py +++ b/basicts/utils/adjacent_matrix_norm.py @@ -1,13 +1,12 @@ -#!/usr/bin/env python -# -*- encoding: utf-8 -*- -import scipy.sparse as sp import numpy as np +import scipy.sparse as sp from scipy.sparse import linalg + def calculate_symmetric_normalized_laplacian(adj: np.ndarray) -> np.matrix: - """Calculate yymmetric normalized laplacian. + """Calculate yymmetric normalized laplacian. Assuming unnormalized laplacian matrix is `L = D - A`, - then symmetric normalized laplacian matrix is: + then symmetric normalized laplacian matrix is: `L^{Sym} = D^-1/2 L D^-1/2 = D^-1/2 (D-A) D^-1/2 = I - D^-1/2 A D^-1/2` For node `i` and `j` where `i!=j`, L^{sym}_{ij} <=0. @@ -17,14 +16,18 @@ def calculate_symmetric_normalized_laplacian(adj: np.ndarray) -> np.matrix: Returns: np.matrix: Symmetric normalized laplacian L^{Sym} """ - adj = sp.coo_matrix(adj) - D = np.array(adj.sum(1)) - D_inv_sqrt = np.power(D, -0.5).flatten() # diagonals of D^{-1/2} - D_inv_sqrt[np.isinf(D_inv_sqrt)] = 0. - matrix_D_inv_sqrt = sp.diags(D_inv_sqrt) # D^{-1/2} - symmetric_normalized_laplacian = sp.eye(adj.shape[0]) - matrix_D_inv_sqrt.dot(adj).dot(matrix_D_inv_sqrt).tocoo() + + adj = sp.coo_matrix(adj) + degree = np.array(adj.sum(1)) + # diagonals of D^{-1/2} + degree_inv_sqrt = np.power(degree, -0.5).flatten() + degree_inv_sqrt[np.isinf(degree_inv_sqrt)] = 0. + matrix_degree_inv_sqrt = sp.diags(degree_inv_sqrt) # D^{-1/2} + symmetric_normalized_laplacian = sp.eye( + adj.shape[0]) - matrix_degree_inv_sqrt.dot(adj).dot(matrix_degree_inv_sqrt).tocoo() return symmetric_normalized_laplacian + def calculate_scaled_laplacian(adj: np.ndarray, lambda_max: int = 2, undirected: bool = True) -> np.matrix: """Re-scaled the eigenvalue to [-1, 1] by scaled the normalized laplacian matrix for chebyshev pol. According to `2017 ICLR GCN`, the lambda max is set to 2, and the graph is set to undirected. @@ -39,20 +42,23 @@ def calculate_scaled_laplacian(adj: np.ndarray, lambda_max: int = 2, undirected: Returns: np.matrix: The rescaled laplacian matrix. """ + if undirected: adj = np.maximum.reduce([adj, adj.T]) - L = calculate_symmetric_normalized_laplacian(adj) + laplacian_matrix = calculate_symmetric_normalized_laplacian(adj) if lambda_max is None: # manually cal the max lambda - lambda_max, _ = linalg.eigsh(L, 1, which='LM') + lambda_max, _ = linalg.eigsh(laplacian_matrix, 1, which='LM') lambda_max = lambda_max[0] - L = sp.csr_matrix(L) - M, _ = L.shape - I = sp.identity(M, format='csr', dtype=L.dtype) - L_res = (2 / lambda_max * L) - I - return L_res - -def symmetric_message_passing_adj(adj: np.ndarray) -> np.matrix: - """ Calculate the renormalized message passing adj in `GCN`. + laplacian_matrix = sp.csr_matrix(laplacian_matrix) + num_nodes, _ = laplacian_matrix.shape + identity_matrix = sp.identity( + num_nodes, format='csr', dtype=laplacian_matrix.dtype) + laplacian_res = (2 / lambda_max * laplacian_matrix) - identity_matrix + return laplacian_res + + +def calculate_symmetric_message_passing_adj(adj: np.ndarray) -> np.matrix: + """Calculate the renormalized message passing adj in `GCN`. A = A + I return D^{-1/2} A D^{-1/2} @@ -64,17 +70,19 @@ def symmetric_message_passing_adj(adj: np.ndarray) -> np.matrix: """ # add self loop - adj = adj + np.diag(np.ones(adj.shape[0], dtype=np.float32)) + adj = adj + np.diag(np.ones(adj.shape[0], dtype=np.float32)) # print("calculating the renormalized message passing adj, please ensure that self-loop has added to adj.") - adj = sp.coo_matrix(adj) - rowsum = np.array(adj.sum(1)) - d_inv_sqrt = np.power(rowsum, -0.5).flatten() + adj = sp.coo_matrix(adj) + row_sum = np.array(adj.sum(1)) + d_inv_sqrt = np.power(row_sum, -0.5).flatten() d_inv_sqrt[np.isinf(d_inv_sqrt)] = 0. - d_mat_inv_sqrt = sp.diags(d_inv_sqrt) - mp_adj = d_mat_inv_sqrt.dot(adj).transpose().dot(d_mat_inv_sqrt).astype(np.float32) + d_mat_inv_sqrt = sp.diags(d_inv_sqrt) + mp_adj = d_mat_inv_sqrt.dot(adj).transpose().dot( + d_mat_inv_sqrt).astype(np.float32) return mp_adj -def transition_matrix(adj: np.ndarray) -> np.matrix: + +def calculate_transition_matrix(adj: np.ndarray) -> np.matrix: """Calculate the transition matrix `P` proposed in DCRNN and Graph WaveNet. P = D^{-1}A = A/rowsum(A) @@ -84,11 +92,11 @@ def transition_matrix(adj: np.ndarray) -> np.matrix: Returns: np.matrix: Transition matrix P """ + adj = sp.coo_matrix(adj) - rowsum = np.array(adj.sum(1)).flatten() - d_inv = np.power(rowsum, -1).flatten() + row_sum = np.array(adj.sum(1)).flatten() + d_inv = np.power(row_sum, -1).flatten() d_inv[np.isinf(d_inv)] = 0. - d_mat= sp.diags(d_inv) - # P = d_mat.dot(adj) - P = d_mat.dot(adj).astype(np.float32).todense() - return P + d_mat = sp.diags(d_inv) + prob_matrix = d_mat.dot(adj).astype(np.float32).todense() + return prob_matrix diff --git a/basicts/utils/distance.py b/basicts/utils/distance.py deleted file mode 100644 index e69de29b..00000000 diff --git a/basicts/utils/misc.py b/basicts/utils/misc.py index 5ee91d41..35387c53 100644 --- a/basicts/utils/misc.py +++ b/basicts/utils/misc.py @@ -1,18 +1,22 @@ import time + import torch + def clock(func): + """clock decorator""" def clocked(*args, **kw): """decorator for clock""" t0 = time.perf_counter() result = func(*args, **kw) elapsed = time.perf_counter() - t0 name = func.__name__ - print('%s: %0.8fs...' % (name, elapsed)) + print("%s: %0.8fs..." % (name, elapsed)) return result return clocked -def check_nan_inf(tensor: torch.Tensor, raise_ex: bool = True): + +def check_nan_inf(tensor: torch.Tensor, raise_ex: bool = True) -> tuple: """check nan and in in tensor Args: @@ -24,26 +28,29 @@ def check_nan_inf(tensor: torch.Tensor, raise_ex: bool = True): Returns: dict: {'nan': bool, 'inf': bool} - torch.Tensor: Tensor + bool: if exist nan or if """ + # nan nan = torch.any(torch.isnan(tensor)) # inf inf = torch.any(torch.isinf(tensor)) # raise if raise_ex and (nan or inf): - raise Exception({"nan":nan, "inf":inf}) - return {"nan":nan, "inf":inf}, nan or inf + raise Exception({"nan": nan, "inf": inf}) + return {"nan": nan, "inf": inf}, nan or inf + def remove_nan_inf(tensor: torch.Tensor): """remove nan and inf in tensor Args: - tensor (_type_): _description_ + tensor (torch.Tensor): input tensor Returns: - _type_: _description_ + torch.Tensor: output tensor """ + tensor = torch.where(torch.isnan(tensor), torch.zeros_like(tensor), tensor) tensor = torch.where(torch.isinf(tensor), torch.zeros_like(tensor), tensor) return tensor diff --git a/basicts/utils/options.py b/basicts/utils/options.py deleted file mode 100644 index e69de29b..00000000 diff --git a/basicts/utils/serialization.py b/basicts/utils/serialization.py index 817c5975..4b579c93 100644 --- a/basicts/utils/serialization.py +++ b/basicts/utils/serialization.py @@ -1,29 +1,45 @@ -import torch import pickle -from basicts.utils.adjacent_matrix_norm import * -def load_pkl(pickle_file: str): - """load pickle data. +import torch +import numpy as np + +from .adjacent_matrix_norm import calculate_scaled_laplacian, calculate_symmetric_normalized_laplacian, calculate_symmetric_message_passing_adj, calculate_transition_matrix + + +def load_pkl(pickle_file: str) -> object: + """Load pickle data. Args: pickle_file (str): file path + + Returns: + object: loaded objected """ + try: - with open(pickle_file, 'rb') as f: - pickle_data = pickle.load(f) - except UnicodeDecodeError as e: - with open(pickle_file, 'rb') as f: - pickle_data = pickle.load(f, encoding='latin1') + with open(pickle_file, "rb") as f: + pickle_data = pickle.load(f) + except UnicodeDecodeError: + with open(pickle_file, "rb") as f: + pickle_data = pickle.load(f, encoding="latin1") except Exception as e: - print('Unable to load data ', pickle_file, ':', e) + print("Unable to load data ", pickle_file, ":", e) raise return pickle_data -def dump_pkl(obj, file_path): - """dumplicate pickle data.""" - with open(file_path, 'wb') as f: + +def dump_pkl(obj: object, file_path: str): + """Dumplicate pickle data. + + Args: + obj (object): object + file_path (str): file path + """ + + with open(file_path, "wb") as f: pickle.dump(obj, f) + def load_adj(file_path: str, adj_type: str): """load adjacency matrix. @@ -35,31 +51,35 @@ def load_adj(file_path: str, adj_type: str): list of numpy.matrix: list of preproceesed adjacency matrices np.ndarray: raw adjacency matrix """ + try: # METR and PEMS_BAY - sensor_ids, sensor_id_to_ind, adj_mx = load_pkl(file_path) - except: + _, _, adj_mx = load_pkl(file_path) + except ValueError: # PEMS04 adj_mx = load_pkl(file_path) if adj_type == "scalap": adj = [calculate_scaled_laplacian(adj_mx).astype(np.float32).todense()] elif adj_type == "normlap": - adj = [ calculate_symmetric_normalized_laplacian(adj_mx).astype(np.float32).todense()] + adj = [calculate_symmetric_normalized_laplacian( + adj_mx).astype(np.float32).todense()] elif adj_type == "symnadj": - adj = [symmetric_message_passing_adj(adj_mx).astype(np.float32).todense()] + adj = [calculate_symmetric_message_passing_adj( + adj_mx).astype(np.float32).todense()] elif adj_type == "transition": - adj = [transition_matrix(adj_mx).T] + adj = [calculate_transition_matrix(adj_mx).T] elif adj_type == "doubletransition": - adj = [transition_matrix(adj_mx).T, transition_matrix(adj_mx.T).T] + adj = [calculate_transition_matrix(adj_mx).T, calculate_transition_matrix(adj_mx.T).T] elif adj_type == "identity": adj = [np.diag(np.ones(adj_mx.shape[0])).astype(np.float32).todense()] - elif adj_type == 'original': + elif adj_type == "original": adj = adj_mx else: error = 0 assert error, "adj type not defined" return adj, adj_mx + def load_node2vec_emb(file_path: str) -> torch.Tensor: """load node2vec embedding @@ -69,14 +89,15 @@ def load_node2vec_emb(file_path: str) -> torch.Tensor: Returns: torch.Tensor: node2vec embedding """ + # spatial embedding - with open(file_path, mode='r') as f: + with open(file_path, mode="r") as f: lines = f.readlines() - temp = lines[0].split(' ') + temp = lines[0].split(" ") num_vertex, dims = int(temp[0]), int(temp[1]) - SE = torch.zeros((num_vertex, dims), dtype=torch.float32) + spatial_embeddings = torch.zeros((num_vertex, dims), dtype=torch.float32) for line in lines[1:]: - temp = line.split(' ') + temp = line.split(" ") index = int(temp[0]) - SE[index] = torch.tensor([float(ch) for ch in temp[1:]]) - return SE + spatial_embeddings[index] = torch.Tensor([float(ch) for ch in temp[1:]]) + return spatial_embeddings diff --git a/docs/DataFormat_CN.md b/docs/DataFormat_CN.md deleted file mode 100644 index 1d460295..00000000 --- a/docs/DataFormat_CN.md +++ /dev/null @@ -1,51 +0,0 @@ -# 数据格式 - -- [数据格式](#数据格式) - - [数据存储形式](#数据存储形式) - - [为什么要这样存储数据](#为什么要这样存储数据) - - [Reference](#reference) - -## 数据存储形式 - -为了灵活的读取、预处理数据,我们设计了一种高效、通用的数据读取/预处理流程。 - -假设原始时间序列为$\mathbf{X}$,时刻$t$处的数据为$\mathbf{X}_t$。 -那么在一般情况下,时间序列预测的一个训练样本就是指:在时间$t$时刻,使用历史的$p$个时间片:$\mathbf{X}_{t-p+1}, ..., \mathbf{X}_{t}$个时间片,预测未来$f$个时间片$\mathbf{X}_{t+1}, ..., \mathbf{X}_{t+f}$。 -但也有一些特殊情况,时间序列的样本是不连续的,例如许多包含多分辨率思想的工作[1][2]. - -为了提供**统一、通用**的data pipline,我们采用存储所有样本的index,而非训练样本的方式。 -具体来说,预处理代码将为每个数据集产生四个文件: - -- `index.pkl`: dict of list - - keys: train, valid, test - - values: 每个训练样本的index,分三种情况,用户可以方便地自定义 - - 连续(默认): [current_time_step_index-p+1, current_time_step_index, current_time_step_index+f] - - 不连续: [[x, x, ..., x], current_time_step_index, current_time_step_index+f] - - 其他 - -- `data.pkl`: dict of list - - keys: processed_data, other - - values: 归一化后并且添加好特征后的“原始”时间序列序列或者其他辅助时间序列 - - processed_data: np.array, L x N x C. L: 时间序列总长度, N: 多变量时间序列数量, C: 特征数量 - - other - -- `scaler.pkl`: dict of list - - keys: - - args: 归一化/反归一化参数,例如mean/std, min/max - - func: 反归一化函数 - - values - -- `adj_mx.pkl`: the pre-defined adjacent matrix - -借助numpy强大的功能,这样的操作方式可以在保证速度的情况下,极大地满足可扩展性,满足几乎所有模型的数据读取需求。 - -## 为什么要这样存储数据 - -统一的数据读取/预处理流程可以方便地、公平地对比不同的Baseline的效率,更简单的理解代码。 - -需要注意的是,尽管这种读取方法是高效的,但并不总是最快的。例如,将所有的样本预处理到本地可能更快一些,但那会损失通用能力。 - -## Reference - -[1] Attention Based Spatial-Temporal Graph Convolutional Networks for Traffic Flow Forecasting\ -[2] Learning Dynamics and Heterogeneity of Spatial-Temporal Graph Data for Traffic Forecasting diff --git a/docs/DataPreprocess_CN.md b/docs/DataPreprocess_CN.md deleted file mode 100644 index 150e4dcf..00000000 --- a/docs/DataPreprocess_CN.md +++ /dev/null @@ -1,169 +0,0 @@ -# 数据预处理 (以PEMS04数据集为例) - -[TOC] - -本文档以PEMS04为例,介绍BasicTS的数据预处理过程。包括:原始数据格式、预处理过程、预处理后数据格式。 - -PEMS04数据集的预处理代码位于`scripts/data_preparation/PEMS04/generate_training_data.py`。 - -您可以通过借鉴PEMS04数据集的预处理,添加您自己的数据集。 - -## 1 原始数据 - -PEMS04数据集来自于交通系统中,共包含来自307个交通传感器的数据。 - -原始数据位于`datasets/raw_data/PEMS04/PEMS04.npz`,它是一个`[16992, 307, 3]`大小的numpy数组。 - -其中,16992代表时间序列有16992个时间片,307代表总共有来自307个传感器的307条时间序列,3代表传感器每次采样三种特征。 - -## 2 预处理过程 - -时间序列的训练样本通常由一个长度为P+F的滑窗在原始时间序列上滑动得到。 -其中,前P个时刻作为历史数据,后F个时刻作为未来数据。 - -### 2.1 预处理参数 - -- `output_dir`: 预处理后文件存储位置。 -- `data_file_path`: 原生数据位置。 -- `graph_file_path`: 图结构数据位置(图结构是非必须的,假如您的数据集没有自带图结构或者您不知道如何构造图结构,可以忽略这一个参数)。 -- `history_seq_len`: 历史数据长度,即P的大小。 -- `future_seq_len`: 未来数据长度,即F的大小。 -- `steps_per_day`: 每天时间片的数量,和采样频率有关。例如每5分钟采样一次,那么该值为288。 -- `dow`: 是否添加day in week特征。 -- `C`: 选择要使用的特征维度。例如在PEMS04中,我们只需要使用传感器采集的3中特征数据值的第一个维度,所以`C=[0]`。 -- `train_ratio`:训练集占总样本量的比例。 -- `valid_ratio`:验证集占总样本量的比例。 - -### 2.2 主要的预处理过程 - -1. 读取原始数据 - -```python -import numpy as np -data = np.load(args.data_file_path)['data'] # 大小: [16992, 307, 3] -``` - -2. 根据原始时间序列的长度和`history_seq_len`和`future_seq_len`的大小,计算总样本的数量,并进一步计算训练、验证、测试样本的数量 - -```python -num_samples = L - (history_seq_len + future_seq_len) + 1 # 总样本数量 -train_num_short = round(num_samples * train_ratio) # 训练样本数量 -valid_num_short = round(num_samples * valid_ratio) # 验证样本数量 -test_num_short = num_samples - train_num_short - valid_num_short # 测试样本数量 -``` - -3. 产生样本的index list - -对于给定的时刻`t`,它的index是:`[t-history_seq_len, t, t+future_seq_len]` - -```python -index_list = [] -for t in range(history_seq_len, num_samples + history_seq_len): - index = (t-history_seq_len, t, t+future_seq_len) - index_list.append(index) -``` - -4. 数据归一化 - -不同的数据有不同的量级PEMS04数据集的量级在0到数百之间。 - -因此,对数据集归一化进行是必须的。 - -数据归一化最常用的是Z-score归一化,当然也有min-max归一化等其他方法。 - -PEMS04数据集使用Z-Score归一化函数`standard_transform`。 - -```python -scaler = standard_transform # 归一化函数工具 -data_norm = scaler(data, output_dir, train_index) -# data_norm:归一化后的数据 -# output_dir: 用来保存归一化过程中产生的一些参数方便以后使用,例如均值和方差大小。 -# train_index: 我们只在训练样本上计算归一化的参数。 -``` - -5. 添加额外的数据 - -通常我们会为数据集添加一些额外特征。例如PEMS04数据集中,我们为它添加了两个时间特征:tid和dow。 - -tid是一个大小在[0, 1]范围内的特征,它代表了每天的当前的时间 (time in day)。例如每天有288个时间片,那么每天的第10个时间片的tid特征的值就是`10/288`。 - -dow代表当前时间是周几(day of week),因此他的选值自然就是{0, 1, 2, 3, 4, 5, 6}中的一个。 - -需要注意的是,PEMS04数据集可能没有包含绝对时刻,因此我们只能计算相对的时刻:即假设第一个时间片的特征是`tid=0, dow=0`,然后顺序向后计算所有时间片的特征。 - -```python -# add external feature -feature_list = [data_norm] -if add_time_in_day: - # numerical time_in_day - time_ind = [i%args.steps_per_day / args.steps_per_day for i in range(data_norm.shape[0])] - time_ind = np.array(time_ind) - time_in_day = np.tile(time_ind, [1, N, 1]).transpose((2, 1, 0)) - feature_list.append(time_in_day) -if add_day_in_week: - # numerical day_in_week - day_in_week = [(i // args.steps_per_day)%7 for i in range(data_norm.shape[0])] - day_in_week = np.array(day_in_week) - day_in_week = np.tile(day_in_week, [1, N, 1]).transpose((2, 1, 0)) - feature_list.append(day_in_week) -processed_data = np.concatenate(feature_list, axis=-1) # 添加完external特征后的数据 -``` - -6. 保存预处理后的数据 - -```python -# 保存index -index = {} -index['train'] = train_index -index['valid'] = valid_index -index['test'] = test_index -pickle.dump(index, open(output_dir + "/index.pkl", "wb")) - -# 保存预处理后的data -data = {} -data['processed_data'] = processed_data -pickle.dump(data, open(output_dir + "/data.pkl", "wb")) - -# 保存图结构 -# 假如没有的话,可以跳过 -if os.path.exists(args.graph_file_path): - shutil.copyfile(args.graph_file_path, output_dir + '/adj_mx.pkl') # copy models -else: - generate_adj_PEMS04() - shutil.copyfile(args.graph_file_path, output_dir + '/adj_mx.pkl') # copy models -``` - -## 3 预处理后的数据 - -数据的存储形式的规定可以参考[data_preparation_CN.md](docs/DataFormat_CN.md)。 - -预处理后的数据会被保存在`datasets/PEMS04/`中。 - -以下所有文件都可以使用`utils/serialization.py`中的`load_pkl`函数读取。 - -### 3.1 data.pkl - -字典类型。`data['processed_data']`保存着预处理后的数据(数组)。 - -### 3.2 index.pkl - -字典类型。产生的训练、验证、测试的index list。 - -```python -index['train'] # train dataset的index list -index['valid'] # valid dataset的index list -index['test'] # test dataset的index list -``` - -### 3.3 scaler.pkl - -字典类型,保存着归一化函数以及需要使用的一些参数。例如: - -```python -scaler['func'] # 归一化函数 -scaler['args'] # 归一化函数需要使用到的参数,字典类型。例如 {"mean": mean, "std": std}. -``` - -### 3.4 adj_mx.pkl - -预定义的邻接矩阵。假如您的数据集没有自带图结构或者您不知道如何为他构造图结构,可以忽略这一个参数。 diff --git a/examples/AGCRN/AGCRN_METR-LA.py b/examples/AGCRN/AGCRN_METR-LA.py new file mode 100644 index 00000000..f0b7b321 --- /dev/null +++ b/examples/AGCRN/AGCRN_METR-LA.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import AGCRN +from basicts.runners import AGCRNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "AGCRN model configuration" +CFG.RUNNER = AGCRNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "METR-LA" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "AGCRN" +CFG.MODEL.ARCH = AGCRN +CFG.MODEL.PARAM = { + "num_nodes" : 207, + "input_dim" : 2, + "rnn_units" : 64, + "output_dim": 1, + "horizon" : 12, + "num_layers": 2, + "default_graph": True, + "embed_dim" : 10, + "cheb_k" : 2 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM= { + "lr":0.003, +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 100 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/AGCRN/AGCRN_PEMS-BAY.py b/examples/AGCRN/AGCRN_PEMS-BAY.py new file mode 100644 index 00000000..944c78ac --- /dev/null +++ b/examples/AGCRN/AGCRN_PEMS-BAY.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import AGCRN +from basicts.runners import AGCRNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "AGCRN model configuration" +CFG.RUNNER = AGCRNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS-BAY" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "AGCRN" +CFG.MODEL.ARCH = AGCRN +CFG.MODEL.PARAM = { + "num_nodes" : 325, + "input_dim" : 2, + "rnn_units" : 64, + "output_dim": 1, + "horizon" : 12, + "num_layers": 2, + "default_graph": True, + "embed_dim" : 10, + "cheb_k" : 2 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM= { + "lr":0.003, +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 100 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/AGCRN/AGCRN_PEMS03.py b/examples/AGCRN/AGCRN_PEMS03.py new file mode 100644 index 00000000..31612b35 --- /dev/null +++ b/examples/AGCRN/AGCRN_PEMS03.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import AGCRN +from basicts.runners import AGCRNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "AGCRN model configuration" +CFG.RUNNER = AGCRNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS03" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "AGCRN" +CFG.MODEL.ARCH = AGCRN +CFG.MODEL.PARAM = { + "num_nodes" : 358, + "input_dim" : 1, + "rnn_units" : 64, + "output_dim": 1, + "horizon" : 12, + "num_layers": 2, + "default_graph": True, + "embed_dim" : 10, + "cheb_k" : 2 +} +CFG.MODEL.FROWARD_FEATURES = [0] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM= { + "lr":0.003, +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/AGCRN/AGCRN_PEMS04.py b/examples/AGCRN/AGCRN_PEMS04.py new file mode 100644 index 00000000..bf06c487 --- /dev/null +++ b/examples/AGCRN/AGCRN_PEMS04.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import AGCRN +from basicts.runners import AGCRNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "AGCRN model configuration" +CFG.RUNNER = AGCRNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS04" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "AGCRN" +CFG.MODEL.ARCH = AGCRN +CFG.MODEL.PARAM = { + "num_nodes" : 307, + "input_dim" : 1, + "rnn_units" : 64, + "output_dim": 1, + "horizon" : 12, + "num_layers": 2, + "default_graph": True, + "embed_dim" : 10, + "cheb_k" : 2 +} +CFG.MODEL.FROWARD_FEATURES = [0] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM= { + "lr":0.003, +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/AGCRN/AGCRN_PEMS07.py b/examples/AGCRN/AGCRN_PEMS07.py new file mode 100644 index 00000000..c50547cf --- /dev/null +++ b/examples/AGCRN/AGCRN_PEMS07.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import AGCRN +from basicts.runners import AGCRNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "AGCRN model configuration" +CFG.RUNNER = AGCRNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS07" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "AGCRN" +CFG.MODEL.ARCH = AGCRN +CFG.MODEL.PARAM = { + "num_nodes" : 883, + "input_dim" : 1, + "rnn_units" : 64, + "output_dim": 1, + "horizon" : 12, + "num_layers": 2, + "default_graph": True, + "embed_dim" : 10, + "cheb_k" : 2 +} +CFG.MODEL.FROWARD_FEATURES = [0] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM= { + "lr":0.003, +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/AGCRN/AGCRN_PEMS08.py b/examples/AGCRN/AGCRN_PEMS08.py new file mode 100644 index 00000000..c7307a36 --- /dev/null +++ b/examples/AGCRN/AGCRN_PEMS08.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import AGCRN +from basicts.runners import AGCRNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "AGCRN model configuration" +CFG.RUNNER = AGCRNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS08" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "AGCRN" +CFG.MODEL.ARCH = AGCRN +CFG.MODEL.PARAM = { + "num_nodes" : 170, + "input_dim" : 1, + "rnn_units" : 64, + "output_dim": 1, + "horizon" : 12, + "num_layers": 2, + "default_graph": True, + "embed_dim" : 2, + "cheb_k" : 2 +} +CFG.MODEL.FROWARD_FEATURES = [0] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM= { + "lr":0.003, +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/D2STGNN/D2STGNN_METR-LA.py b/examples/D2STGNN/D2STGNN_METR-LA.py new file mode 100644 index 00000000..65bf3f4f --- /dev/null +++ b/examples/D2STGNN/D2STGNN_METR-LA.py @@ -0,0 +1,125 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import D2STGNN +from basicts.runners import D2STGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "D2STGNN model configuration" +CFG.RUNNER = D2STGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "METR-LA" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "D2STGNN" +CFG.MODEL.ARCH = D2STGNN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "num_feat": 1, + "num_hidden": 32, + "dropout": 0.1, + "seq_length": 12, + "k_t": 3, + "k_s": 2, + "gap": 3, + "num_nodes": 207, + "adjs": [torch.tensor(adj) for adj in adj_mx], + "num_layers": 5, + "num_modalities": 2, + "node_hidden": 10, + "time_emb_dim": 10, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 1.0e-5, + "eps": 1.0e-8 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 30, 38, 46, 54, 62, 70, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 100 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False +# curriculum learning +CFG.TRAIN.CL = EasyDict() +CFG.TRAIN.CL.WARM_EPOCHS = 0 +CFG.TRAIN.CL.CL_EPOCHS = 6 +CFG.TRAIN.CL.PREDICTION_LENGTH = 12 + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 32 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 32 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/D2STGNN/D2STGNN_PEMS-BAY.py b/examples/D2STGNN/D2STGNN_PEMS-BAY.py new file mode 100644 index 00000000..b79ee317 --- /dev/null +++ b/examples/D2STGNN/D2STGNN_PEMS-BAY.py @@ -0,0 +1,126 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import D2STGNN +from basicts.runners import D2STGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "D2STGNN model configuration" +CFG.RUNNER = D2STGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS-BAY" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "D2STGNN" +CFG.MODEL.ARCH = D2STGNN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "num_feat": 1, + "num_hidden": 32, + "dropout": 0.1, + "seq_length": 12, + "k_t": 3, + "k_s": 2, + "gap": 3, + "num_nodes": 325, + "adjs": [torch.tensor(adj) for adj in adj_mx], + "num_layers": 5, + "num_modalities": 2, + "node_hidden": 12, + "time_emb_dim": 12, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 1.0e-5, + "eps": 1.0e-8 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 30, 38, 46, 54, 62, 70, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 100 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False +# curriculum learning +CFG.TRAIN.CL = EasyDict() +CFG.TRAIN.CL.WARM_EPOCHS = 30 +CFG.TRAIN.CL.CL_EPOCHS = 3 +CFG.TRAIN.CL.PREDICTION_LENGTH = 12 + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 32 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 32 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/D2STGNN/D2STGNN_PEMS03.py b/examples/D2STGNN/D2STGNN_PEMS03.py new file mode 100644 index 00000000..b7bd9e66 --- /dev/null +++ b/examples/D2STGNN/D2STGNN_PEMS03.py @@ -0,0 +1,126 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import D2STGNN +from basicts.runners import D2STGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "D2STGNN model configuration" +CFG.RUNNER = D2STGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS03" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "D2STGNN" +CFG.MODEL.ARCH = D2STGNN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "num_feat": 1, + "num_hidden": 32, + "dropout": 0.1, + "seq_length": 12, + "k_t": 3, + "k_s": 2, + "gap": 3, + "num_nodes": 358, + "adjs": [torch.tensor(adj) for adj in adj_mx], + "num_layers": 5, + "num_modalities": 2, + "node_hidden": 12, + "time_emb_dim": 12, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 1.0e-5, + "eps": 1.0e-8 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 30, 38, 46, 54, 150], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 16 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False +# curriculum learning +CFG.TRAIN.CL = EasyDict() +CFG.TRAIN.CL.WARM_EPOCHS = 30 +CFG.TRAIN.CL.CL_EPOCHS = 3 +CFG.TRAIN.CL.PREDICTION_LENGTH = 12 + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 16 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 16 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/D2STGNN/D2STGNN_PEMS04.py b/examples/D2STGNN/D2STGNN_PEMS04.py new file mode 100644 index 00000000..e6f085d4 --- /dev/null +++ b/examples/D2STGNN/D2STGNN_PEMS04.py @@ -0,0 +1,126 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import D2STGNN +from basicts.runners import D2STGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "D2STGNN model configuration" +CFG.RUNNER = D2STGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS04" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "D2STGNN" +CFG.MODEL.ARCH = D2STGNN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "num_feat": 1, + "num_hidden": 32, + "dropout": 0.1, + "seq_length": 12, + "k_t": 3, + "k_s": 2, + "gap": 3, + "num_nodes": 307, + "adjs": [torch.tensor(adj) for adj in adj_mx], + "num_layers": 5, + "num_modalities": 2, + "node_hidden": 12, + "time_emb_dim": 12, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 1.0e-5, + "eps": 1.0e-8 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 30, 38, 46, 54, 150], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 16 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False +# curriculum learning +CFG.TRAIN.CL = EasyDict() +CFG.TRAIN.CL.WARM_EPOCHS = 30 +CFG.TRAIN.CL.CL_EPOCHS = 3 +CFG.TRAIN.CL.PREDICTION_LENGTH = 12 + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 16 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 16 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/D2STGNN/D2STGNN_PEMS07.py b/examples/D2STGNN/D2STGNN_PEMS07.py new file mode 100644 index 00000000..e10bf270 --- /dev/null +++ b/examples/D2STGNN/D2STGNN_PEMS07.py @@ -0,0 +1,126 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import D2STGNN +from basicts.runners import D2STGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "D2STGNN model configuration" +CFG.RUNNER = D2STGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS07" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "D2STGNN" +CFG.MODEL.ARCH = D2STGNN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "num_feat": 1, + "num_hidden": 32, + "dropout": 0.1, + "seq_length": 12, + "k_t": 3, + "k_s": 2, + "gap": 3, + "num_nodes": 883, + "adjs": [torch.tensor(adj) for adj in adj_mx], + "num_layers": 5, + "num_modalities": 2, + "node_hidden": 10, + "time_emb_dim": 10, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 1.0e-5, + "eps": 1.0e-8 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 30, 38, 46, 54, 150], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 16 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False +# curriculum learning +CFG.TRAIN.CL = EasyDict() +CFG.TRAIN.CL.WARM_EPOCHS = 30 +CFG.TRAIN.CL.CL_EPOCHS = 3 +CFG.TRAIN.CL.PREDICTION_LENGTH = 12 + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 16 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 16 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/D2STGNN/D2STGNN_PEMS08.py b/examples/D2STGNN/D2STGNN_PEMS08.py new file mode 100644 index 00000000..1ed7e0fd --- /dev/null +++ b/examples/D2STGNN/D2STGNN_PEMS08.py @@ -0,0 +1,126 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import D2STGNN +from basicts.runners import D2STGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "D2STGNN model configuration" +CFG.RUNNER = D2STGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS08" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "D2STGNN" +CFG.MODEL.ARCH = D2STGNN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "num_feat": 1, + "num_hidden": 32, + "dropout": 0.1, + "seq_length": 12, + "k_t": 3, + "k_s": 2, + "gap": 3, + "num_nodes": 170, + "adjs": [torch.tensor(adj) for adj in adj_mx], + "num_layers": 5, + "num_modalities": 2, + "node_hidden": 10, + "time_emb_dim": 10, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 1.0e-5, + "eps": 1.0e-8 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 30, 38, 46, 54, 150], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 16 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False +# curriculum learning +CFG.TRAIN.CL = EasyDict() +CFG.TRAIN.CL.WARM_EPOCHS = 30 +CFG.TRAIN.CL.CL_EPOCHS = 3 +CFG.TRAIN.CL.PREDICTION_LENGTH = 12 + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 16 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 16 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DCRNN/DCRNN_METR-LA.py b/examples/DCRNN/DCRNN_METR-LA.py new file mode 100644 index 00000000..d6eb0ffe --- /dev/null +++ b/examples/DCRNN/DCRNN_METR-LA.py @@ -0,0 +1,126 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import DCRNN +from basicts.runners import DCRNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# DCRNN does not allow to load parameters since it creates parameters in the first iteration +resume = False +if not resume: + import random + _ = random.randint(-1e6, 1e6) + +# ================= general ================= # +CFG.DESCRIPTION = "DCRNN model configuration" +CFG.RUNNER = DCRNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "METR-LA" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG._ = _ +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DCRNN" +CFG.MODEL.ARCH = DCRNN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "cl_decay_steps": 2000, + "horizon": 12, + "input_dim": 2, + "max_diffusion_step": 2, + "num_nodes": 207, + "num_rnn_layers": 2, + "output_dim": 1, + "rnn_units": 64, + "seq_len": 12, + "adj_mx": [torch.tensor(i).cuda() for i in adj_mx], + "use_curriculum_learning": True +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.01, + "eps": 1e-3 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [20, 30, 40, 50], + "gamma": 0.1 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 100 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +CFG.TRAIN.SETUP_GRAPH = True +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DCRNN/DCRNN_PEMS-BAY.py b/examples/DCRNN/DCRNN_PEMS-BAY.py new file mode 100644 index 00000000..e5e1da11 --- /dev/null +++ b/examples/DCRNN/DCRNN_PEMS-BAY.py @@ -0,0 +1,126 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import DCRNN +from basicts.runners import DCRNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# DCRNN does not allow to load parameters since it creates parameters in the first iteration +resume = False +if not resume: + import random + _ = random.randint(-1e6, 1e6) + +# ================= general ================= # +CFG.DESCRIPTION = "DCRNN model configuration" +CFG.RUNNER = DCRNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS-BAY" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG._ = _ +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DCRNN" +CFG.MODEL.ARCH = DCRNN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "cl_decay_steps": 2000, + "horizon": 12, + "input_dim": 2, + "max_diffusion_step": 2, + "num_nodes": 325, + "num_rnn_layers": 2, + "output_dim": 1, + "rnn_units": 64, + "seq_len": 12, + "adj_mx": [torch.tensor(i).cuda() for i in adj_mx], + "use_curriculum_learning": True +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.01, + "eps": 1e-3 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [20, 30, 40, 50], + "gamma": 0.1 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 100 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +CFG.TRAIN.SETUP_GRAPH = True +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DCRNN/DCRNN_PEMS03.py b/examples/DCRNN/DCRNN_PEMS03.py new file mode 100644 index 00000000..11fb9e0d --- /dev/null +++ b/examples/DCRNN/DCRNN_PEMS03.py @@ -0,0 +1,126 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import DCRNN +from basicts.runners import DCRNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# DCRNN does not allow to load parameters since it creates parameters in the first iteration +resume = False +if not resume: + import random + _ = random.randint(-1e6, 1e6) + +# ================= general ================= # +CFG.DESCRIPTION = "DCRNN model configuration" +CFG.RUNNER = DCRNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS03" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG._ = _ +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DCRNN" +CFG.MODEL.ARCH = DCRNN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "cl_decay_steps": 2000, + "horizon": 12, + "input_dim": 2, + "max_diffusion_step": 2, + "num_nodes": 358, + "num_rnn_layers": 2, + "output_dim": 1, + "rnn_units": 64, + "seq_len": 12, + "adj_mx": [torch.tensor(i).cuda() for i in adj_mx], + "use_curriculum_learning": True +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.003, + "eps": 1e-3 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [80], + "gamma": 0.3 +} + +# ================= train ================= # +# CFG.TRAIN.CLIP_GRAD_PARAM = { +# "max_norm": 5.0 +# } +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +CFG.TRAIN.SETUP_GRAPH = True +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DCRNN/DCRNN_PEMS04.py b/examples/DCRNN/DCRNN_PEMS04.py new file mode 100644 index 00000000..9e03f4e6 --- /dev/null +++ b/examples/DCRNN/DCRNN_PEMS04.py @@ -0,0 +1,126 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import DCRNN +from basicts.runners import DCRNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# DCRNN does not allow to load parameters since it creates parameters in the first iteration +resume = False +if not resume: + import random + _ = random.randint(-1e6, 1e6) + +# ================= general ================= # +CFG.DESCRIPTION = "DCRNN model configuration" +CFG.RUNNER = DCRNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS04" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG._ = _ +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DCRNN" +CFG.MODEL.ARCH = DCRNN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "cl_decay_steps": 2000, + "horizon": 12, + "input_dim": 2, + "max_diffusion_step": 2, + "num_nodes": 307, + "num_rnn_layers": 2, + "output_dim": 1, + "rnn_units": 64, + "seq_len": 12, + "adj_mx": [torch.tensor(i).cuda() for i in adj_mx], + "use_curriculum_learning": True +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.003, + "eps": 1e-3 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [80], + "gamma": 0.3 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +CFG.TRAIN.SETUP_GRAPH = True +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DCRNN/DCRNN_PEMS07.py b/examples/DCRNN/DCRNN_PEMS07.py new file mode 100644 index 00000000..8514f226 --- /dev/null +++ b/examples/DCRNN/DCRNN_PEMS07.py @@ -0,0 +1,126 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import DCRNN +from basicts.runners import DCRNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# DCRNN does not allow to load parameters since it creates parameters in the first iteration +resume = False +if not resume: + import random + _ = random.randint(-1e6, 1e6) + +# ================= general ================= # +CFG.DESCRIPTION = "DCRNN model configuration" +CFG.RUNNER = DCRNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS07" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG._ = _ +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DCRNN" +CFG.MODEL.ARCH = DCRNN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "cl_decay_steps": 2000, + "horizon": 12, + "input_dim": 2, + "max_diffusion_step": 2, + "num_nodes": 883, + "num_rnn_layers": 2, + "output_dim": 1, + "rnn_units": 64, + "seq_len": 12, + "adj_mx": [torch.tensor(i).cuda() for i in adj_mx], + "use_curriculum_learning": True +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.003, + "eps": 1e-3 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [80], + "gamma": 0.3 +} + +# ================= train ================= # +# CFG.TRAIN.CLIP_GRAD_PARAM = { +# "max_norm": 5.0 +# } +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +CFG.TRAIN.SETUP_GRAPH = True +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DCRNN/DCRNN_PEMS08.py b/examples/DCRNN/DCRNN_PEMS08.py new file mode 100644 index 00000000..82693ac4 --- /dev/null +++ b/examples/DCRNN/DCRNN_PEMS08.py @@ -0,0 +1,126 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import DCRNN +from basicts.runners import DCRNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# DCRNN does not allow to load parameters since it creates parameters in the first iteration +resume = False +if not resume: + import random + _ = random.randint(-1e6, 1e6) + +# ================= general ================= # +CFG.DESCRIPTION = "DCRNN model configuration" +CFG.RUNNER = DCRNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS08" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG._ = _ +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DCRNN" +CFG.MODEL.ARCH = DCRNN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "cl_decay_steps": 2000, + "horizon": 12, + "input_dim": 2, + "max_diffusion_step": 2, + "num_nodes": 170, + "num_rnn_layers": 2, + "output_dim": 1, + "rnn_units": 64, + "seq_len": 12, + "adj_mx": [torch.tensor(i).cuda() for i in adj_mx], + "use_curriculum_learning": True +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.003, + "eps": 1e-3 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [80], + "gamma": 0.3 +} + +# ================= train ================= # +# CFG.TRAIN.CLIP_GRAD_PARAM = { +# "max_norm": 5.0 +# } +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +CFG.TRAIN.SETUP_GRAPH = True +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DGCRN/DGCRN_METR-LA.py b/examples/DGCRN/DGCRN_METR-LA.py new file mode 100644 index 00000000..227019b9 --- /dev/null +++ b/examples/DGCRN/DGCRN_METR-LA.py @@ -0,0 +1,126 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import DGCRN +from basicts.runners import DGCRNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "DGCRN model configuration" +CFG.RUNNER = DGCRNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "METR-LA" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DGCRN" +CFG.MODEL.ARCH = DGCRN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "gcn_depth": 2, + "num_nodes": 207, + "predefined_A": [torch.Tensor(_) for _ in adj_mx], + "dropout": 0.3, + "subgraph_size": 20, + "node_dim": 40, + "middle_dim": 2, + "seq_length": 12, + "in_dim": 2, + "list_weight": [0.05, 0.95, 0.95], + "tanhalpha": 3, + "cl_decay_steps": 4000, + "rnn_size": 64, + "hyperGNN_dim": 16 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr":0.001, + "weight_decay":0.0001 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones":[100, 150], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False +## curriculum learning +CFG.TRAIN.CL = EasyDict() +CFG.TRAIN.CL.WARM_EPOCHS = 0 +CFG.TRAIN.CL.CL_EPOCHS = 6 +CFG.TRAIN.CL.PREDICTION_LENGTH = 12 + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DGCRN/DGCRN_PEMS-BAY.py b/examples/DGCRN/DGCRN_PEMS-BAY.py new file mode 100644 index 00000000..f89c2a0e --- /dev/null +++ b/examples/DGCRN/DGCRN_PEMS-BAY.py @@ -0,0 +1,126 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import DGCRN +from basicts.runners import DGCRNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "DGCRN model configuration" +CFG.RUNNER = DGCRNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS-BAY" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DGCRN" +CFG.MODEL.ARCH = DGCRN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "gcn_depth": 2, + "num_nodes": 325, + "predefined_A": [torch.Tensor(_) for _ in adj_mx], + "dropout": 0.3, + "subgraph_size": 20, + "node_dim": 40, + "middle_dim": 2, + "seq_length": 12, + "in_dim": 2, + "list_weight": [0.05, 0.95, 0.95], + "tanhalpha": 3, + "cl_decay_steps": 5500, + "rnn_size": 64, + "hyperGNN_dim": 16 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr":0.001, + "weight_decay":0.0001 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones":[100, 150], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False +## curriculum learning +CFG.TRAIN.CL = EasyDict() +CFG.TRAIN.CL.WARM_EPOCHS = 0 +CFG.TRAIN.CL.CL_EPOCHS = 6 +CFG.TRAIN.CL.PREDICTION_LENGTH = 12 + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DGCRN/DGCRN_PEMS03.py b/examples/DGCRN/DGCRN_PEMS03.py new file mode 100644 index 00000000..b2feebe9 --- /dev/null +++ b/examples/DGCRN/DGCRN_PEMS03.py @@ -0,0 +1,126 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import DGCRN +from basicts.runners import DGCRNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "DGCRN model configuration" +CFG.RUNNER = DGCRNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS03" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DGCRN" +CFG.MODEL.ARCH = DGCRN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "gcn_depth": 2, + "num_nodes": 358, + "predefined_A": [torch.Tensor(_) for _ in adj_mx], + "dropout": 0.3, + "subgraph_size": 20, + "node_dim": 40, + "middle_dim": 2, + "seq_length": 12, + "in_dim": 2, + "list_weight": [0.05, 0.95, 0.95], + "tanhalpha": 3, + "cl_decay_steps": 4000, + "rnn_size": 64, + "hyperGNN_dim": 16 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr":0.001, + "weight_decay":0.0001 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones":[100, 150], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False +## curriculum learning +CFG.TRAIN.CL = EasyDict() +CFG.TRAIN.CL.WARM_EPOCHS = 0 +CFG.TRAIN.CL.CL_EPOCHS = 6 +CFG.TRAIN.CL.PREDICTION_LENGTH = 12 + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DGCRN/DGCRN_PEMS04.py b/examples/DGCRN/DGCRN_PEMS04.py new file mode 100644 index 00000000..076c9ba4 --- /dev/null +++ b/examples/DGCRN/DGCRN_PEMS04.py @@ -0,0 +1,126 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import DGCRN +from basicts.runners import DGCRNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "DGCRN model configuration" +CFG.RUNNER = DGCRNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS04" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DGCRN" +CFG.MODEL.ARCH = DGCRN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "gcn_depth": 2, + "num_nodes": 307, + "predefined_A": [torch.Tensor(_) for _ in adj_mx], + "dropout": 0.3, + "subgraph_size": 20, + "node_dim": 40, + "middle_dim": 2, + "seq_length": 12, + "in_dim": 2, + "list_weight": [0.05, 0.95, 0.95], + "tanhalpha": 3, + "cl_decay_steps": 4000, + "rnn_size": 64, + "hyperGNN_dim": 16 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr":0.001, + "weight_decay":0.0001 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones":[100, 150], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False +## curriculum learning +CFG.TRAIN.CL = EasyDict() +CFG.TRAIN.CL.WARM_EPOCHS = 0 +CFG.TRAIN.CL.CL_EPOCHS = 6 +CFG.TRAIN.CL.PREDICTION_LENGTH = 12 + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DGCRN/DGCRN_PEMS07.py b/examples/DGCRN/DGCRN_PEMS07.py new file mode 100644 index 00000000..f718e828 --- /dev/null +++ b/examples/DGCRN/DGCRN_PEMS07.py @@ -0,0 +1,126 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import DGCRN +from basicts.runners import DGCRNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "DGCRN model configuration" +CFG.RUNNER = DGCRNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS07" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DGCRN" +CFG.MODEL.ARCH = DGCRN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "gcn_depth": 2, + "num_nodes": 883, + "predefined_A": [torch.Tensor(_) for _ in adj_mx], + "dropout": 0.3, + "subgraph_size": 20, + "node_dim": 40, + "middle_dim": 2, + "seq_length": 12, + "in_dim": 2, + "list_weight": [0.05, 0.95, 0.95], + "tanhalpha": 3, + "cl_decay_steps": 4000, + "rnn_size": 64, + "hyperGNN_dim": 16 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr":0.001, + "weight_decay":0.0001 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones":[100, 150], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 24 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False +## curriculum learning +CFG.TRAIN.CL = EasyDict() +CFG.TRAIN.CL.WARM_EPOCHS = 0 +CFG.TRAIN.CL.CL_EPOCHS = 6 +CFG.TRAIN.CL.PREDICTION_LENGTH = 12 + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DGCRN/DGCRN_PEMS08.py b/examples/DGCRN/DGCRN_PEMS08.py new file mode 100644 index 00000000..4228976b --- /dev/null +++ b/examples/DGCRN/DGCRN_PEMS08.py @@ -0,0 +1,126 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import DGCRN +from basicts.runners import DGCRNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "DGCRN model configuration" +CFG.RUNNER = DGCRNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS08" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DGCRN" +CFG.MODEL.ARCH = DGCRN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "gcn_depth": 2, + "num_nodes": 170, + "predefined_A": [torch.Tensor(_) for _ in adj_mx], + "dropout": 0.3, + "subgraph_size": 20, + "node_dim": 40, + "middle_dim": 2, + "seq_length": 12, + "in_dim": 2, + "list_weight": [0.05, 0.95, 0.95], + "tanhalpha": 3, + "cl_decay_steps": 4000, + "rnn_size": 64, + "hyperGNN_dim": 16 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr":0.001, + "weight_decay":0.0001 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones":[100, 150], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False +## curriculum learning +CFG.TRAIN.CL = EasyDict() +CFG.TRAIN.CL.WARM_EPOCHS = 0 +CFG.TRAIN.CL.CL_EPOCHS = 6 +CFG.TRAIN.CL.PREDICTION_LENGTH = 12 + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DLinear/DLinear_METR-LA.py b/examples/DLinear/DLinear_METR-LA.py new file mode 100644 index 00000000..cf5729d3 --- /dev/null +++ b/examples/DLinear/DLinear_METR-LA.py @@ -0,0 +1,104 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import DLinear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "METR-LA" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DLinear" +CFG.MODEL.ARCH = DLinear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12, + "individual": False, + "enc_in": 207 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DLinear/DLinear_PEMS-BAY.py b/examples/DLinear/DLinear_PEMS-BAY.py new file mode 100644 index 00000000..ec250a86 --- /dev/null +++ b/examples/DLinear/DLinear_PEMS-BAY.py @@ -0,0 +1,104 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import DLinear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS-BAY" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DLinear" +CFG.MODEL.ARCH = DLinear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12, + "individual": False, + "enc_in": 325 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DLinear/DLinear_PEMS03.py b/examples/DLinear/DLinear_PEMS03.py new file mode 100644 index 00000000..d276ff6a --- /dev/null +++ b/examples/DLinear/DLinear_PEMS03.py @@ -0,0 +1,104 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import DLinear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS03" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DLinear" +CFG.MODEL.ARCH = DLinear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12, + "individual": False, + "enc_in": 358 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DLinear/DLinear_PEMS04.py b/examples/DLinear/DLinear_PEMS04.py new file mode 100644 index 00000000..b1722a22 --- /dev/null +++ b/examples/DLinear/DLinear_PEMS04.py @@ -0,0 +1,104 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import DLinear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS04" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DLinear" +CFG.MODEL.ARCH = DLinear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12, + "individual": False, + "enc_in": 307 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DLinear/DLinear_PEMS07.py b/examples/DLinear/DLinear_PEMS07.py new file mode 100644 index 00000000..6255e3cb --- /dev/null +++ b/examples/DLinear/DLinear_PEMS07.py @@ -0,0 +1,104 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import DLinear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS07" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DLinear" +CFG.MODEL.ARCH = DLinear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12, + "individual": False, + "enc_in": 307 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/DLinear/DLinear_PEMS08.py b/examples/DLinear/DLinear_PEMS08.py new file mode 100644 index 00000000..c1e1cc36 --- /dev/null +++ b/examples/DLinear/DLinear_PEMS08.py @@ -0,0 +1,104 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import DLinear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS08" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "DLinear" +CFG.MODEL.ARCH = DLinear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12, + "individual": False, + "enc_in": 170 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/GTS/GTS_METR-LA.py b/examples/GTS/GTS_METR-LA.py new file mode 100644 index 00000000..cf535f49 --- /dev/null +++ b/examples/GTS/GTS_METR-LA.py @@ -0,0 +1,132 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import GTS +from basicts.runners import GTSRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.utils.serialization import load_pkl + +from .loss import gts_loss + + +CFG = EasyDict() + +# GTS does not allow to load parameters since it creates parameters in the first iteration +resume = False +if not resume: + import random + _ = random.randint(-1e6, 1e6) + +# ================= general ================= # +CFG.DESCRIPTION = "GTS model configuration" +CFG.RUNNER = GTSRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "METR-LA" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG._ = _ +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "GTS" +CFG.MODEL.ARCH = GTS +node_feats_full = load_pkl("datasets/{0}/data.pkl".format(CFG.DATASET_NAME))["processed_data"][..., 0] +train_index_list = load_pkl("datasets/{0}/index_in{1}_out{2}.pkl".format(CFG.DATASET_NAME, CFG.DATASET_INPUT_LEN, CFG.DATASET_OUTPUT_LEN))["train"] +node_feats = node_feats_full[:train_index_list[-1][-1], ...] +CFG.MODEL.PARAM = { + "cl_decay_steps": 2000, + "filter_type": "dual_random_walk", + "horizon": 12, + "input_dim": 2, + "l1_decay": 0, + "max_diffusion_step": 3, + "num_nodes": 207, + "num_rnn_layers": 1, + "output_dim": 1, + "rnn_units": 64, + "seq_len": 12, + "use_curriculum_learning": True, + "dim_fc": 383664, + "node_feats": node_feats, + "temp": 0.5, + "k": 10 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = gts_loss +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.005, + "eps": 1e-3 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [20, 40], + "gamma": 0.1 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +CFG.TRAIN.SETUP_GRAPH = True +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/GTS/GTS_PEMS-BAY.py b/examples/GTS/GTS_PEMS-BAY.py new file mode 100644 index 00000000..2272607a --- /dev/null +++ b/examples/GTS/GTS_PEMS-BAY.py @@ -0,0 +1,132 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import GTS +from basicts.runners import GTSRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.utils.serialization import load_pkl + +from .loss import gts_loss + + +CFG = EasyDict() + +# GTS does not allow to load parameters since it creates parameters in the first iteration +resume = False +if not resume: + import random + _ = random.randint(-1e6, 1e6) + +# ================= general ================= # +CFG.DESCRIPTION = "GTS model configuration" +CFG.RUNNER = GTSRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS-BAY" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG._ = _ +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "GTS" +CFG.MODEL.ARCH = GTS +node_feats_full = load_pkl("datasets/{0}/data.pkl".format(CFG.DATASET_NAME))["processed_data"][..., 0] +train_index_list = load_pkl("datasets/{0}/index_in{1}_out{2}.pkl".format(CFG.DATASET_NAME, CFG.DATASET_INPUT_LEN, CFG.DATASET_OUTPUT_LEN))["train"] +node_feats = node_feats_full[:train_index_list[-1][-1], ...] +CFG.MODEL.PARAM = { + "cl_decay_steps": 2000, + "filter_type": "dual_random_walk", + "horizon": 12, + "input_dim": 2, + "l1_decay": 0, + "max_diffusion_step": 2, + "num_nodes": 325, + "num_rnn_layers": 1, + "output_dim": 1, + "rnn_units": 128, + "seq_len": 12, + "use_curriculum_learning": True, + "dim_fc": 583520, + "node_feats": node_feats, + "temp": 0.5, + "k": 30 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = gts_loss +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.001, + "eps": 1e-3 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [20, 30], + "gamma": 0.1 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +CFG.TRAIN.SETUP_GRAPH = True +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/GTS/GTS_PEMS03.py b/examples/GTS/GTS_PEMS03.py new file mode 100644 index 00000000..f57fd50f --- /dev/null +++ b/examples/GTS/GTS_PEMS03.py @@ -0,0 +1,132 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import GTS +from basicts.runners import GTSRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.utils.serialization import load_pkl + +from .loss import gts_loss + + +CFG = EasyDict() + +# GTS does not allow to load parameters since it creates parameters in the first iteration +resume = False +if not resume: + import random + _ = random.randint(-1e6, 1e6) + +# ================= general ================= # +CFG.DESCRIPTION = "GTS model configuration" +CFG.RUNNER = GTSRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS03" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG._ = _ +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "GTS" +CFG.MODEL.ARCH = GTS +node_feats_full = load_pkl("datasets/{0}/data.pkl".format(CFG.DATASET_NAME))["processed_data"][..., 0] +train_index_list = load_pkl("datasets/{0}/index_in{1}_out{2}.pkl".format(CFG.DATASET_NAME, CFG.DATASET_INPUT_LEN, CFG.DATASET_OUTPUT_LEN))["train"] +node_feats = node_feats_full[:train_index_list[-1][-1], ...] +CFG.MODEL.PARAM = { + "cl_decay_steps": 2000, + "filter_type": "dual_random_walk", + "horizon": 12, + "input_dim": 2, + "l1_decay": 0, + "max_diffusion_step": 3, + "num_nodes": 358, + "num_rnn_layers": 1, + "output_dim": 1, + "rnn_units": 64, + "seq_len": 12, + "use_curriculum_learning": True, + "dim_fc": 251456, + "node_feats": node_feats, + "temp": 0.5, + "k": 30 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = gts_loss +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.001, + "eps": 1e-3 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [20, 30], + "gamma": 0.1 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +CFG.TRAIN.SETUP_GRAPH = True +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/GTS/GTS_PEMS04.py b/examples/GTS/GTS_PEMS04.py new file mode 100644 index 00000000..61db4a91 --- /dev/null +++ b/examples/GTS/GTS_PEMS04.py @@ -0,0 +1,132 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import GTS +from basicts.runners import GTSRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.utils.serialization import load_pkl + +from .loss import gts_loss + + +CFG = EasyDict() + +# GTS does not allow to load parameters since it creates parameters in the first iteration +resume = False +if not resume: + import random + _ = random.randint(-1e6, 1e6) + +# ================= general ================= # +CFG.DESCRIPTION = "GTS model configuration" +CFG.RUNNER = GTSRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS04" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG._ = _ +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "GTS" +CFG.MODEL.ARCH = GTS +node_feats_full = load_pkl("datasets/{0}/data.pkl".format(CFG.DATASET_NAME))["processed_data"][..., 0] +train_index_list = load_pkl("datasets/{0}/index_in{1}_out{2}.pkl".format(CFG.DATASET_NAME, CFG.DATASET_INPUT_LEN, CFG.DATASET_OUTPUT_LEN))["train"] +node_feats = node_feats_full[:train_index_list[-1][-1], ...] +CFG.MODEL.PARAM = { + "cl_decay_steps": 2000, + "filter_type": "dual_random_walk", + "horizon": 12, + "input_dim": 2, + "l1_decay": 0, + "max_diffusion_step": 3, + "num_nodes": 307, + "num_rnn_layers": 1, + "output_dim": 1, + "rnn_units": 64, + "seq_len": 12, + "use_curriculum_learning": True, + "dim_fc": 162976, + "node_feats": node_feats, + "temp": 0.5, + "k": 30 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = gts_loss +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.001, + "eps": 1e-3 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [20, 30], + "gamma": 0.1 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +CFG.TRAIN.SETUP_GRAPH = True +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/GTS/GTS_PEMS07.py b/examples/GTS/GTS_PEMS07.py new file mode 100644 index 00000000..1fb6c172 --- /dev/null +++ b/examples/GTS/GTS_PEMS07.py @@ -0,0 +1,132 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import GTS +from basicts.runners import GTSRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.utils.serialization import load_pkl + +from .loss import gts_loss + + +CFG = EasyDict() + +# GTS does not allow to load parameters since it creates parameters in the first iteration +resume = False +if not resume: + import random + _ = random.randint(-1e6, 1e6) + +# ================= general ================= # +CFG.DESCRIPTION = "GTS model configuration" +CFG.RUNNER = GTSRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS07" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG._ = _ +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "GTS" +CFG.MODEL.ARCH = GTS +node_feats_full = load_pkl("datasets/{0}/data.pkl".format(CFG.DATASET_NAME))["processed_data"][..., 0] +train_index_list = load_pkl("datasets/{0}/index_in{1}_out{2}.pkl".format(CFG.DATASET_NAME, CFG.DATASET_INPUT_LEN, CFG.DATASET_OUTPUT_LEN))["train"] +node_feats = node_feats_full[:train_index_list[-1][-1], ...] +CFG.MODEL.PARAM = { + "cl_decay_steps": 2000, + "filter_type": "dual_random_walk", + "horizon": 12, + "input_dim": 2, + "l1_decay": 0, + "max_diffusion_step": 2, + "num_nodes": 883, + "num_rnn_layers": 1, + "output_dim": 1, + "rnn_units": 64, + "seq_len": 12, + "use_curriculum_learning": True, + "dim_fc": 270816, + "node_feats": node_feats, + "temp": 0.5, + "k": 30 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = gts_loss +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.001, + "eps": 1e-3 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [20, 30], + "gamma": 0.1 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +CFG.TRAIN.SETUP_GRAPH = True +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/GTS/GTS_PEMS08.py b/examples/GTS/GTS_PEMS08.py new file mode 100644 index 00000000..f31513f5 --- /dev/null +++ b/examples/GTS/GTS_PEMS08.py @@ -0,0 +1,134 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import GTS +from basicts.runners import GTSRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.utils.serialization import load_pkl + +from .loss import gts_loss + + +CFG = EasyDict() + +# GTS does not allow to load parameters since it creates parameters in the first iteration +resume = False +if not resume: + import random + _ = random.randint(-1e6, 1e6) + +# ================= general ================= # +CFG.DESCRIPTION = "GTS model configuration" +CFG.RUNNER = GTSRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS08" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG._ = _ +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "GTS" +CFG.MODEL.ARCH = GTS +node_feats_full = load_pkl( + "datasets/{0}/data.pkl".format(CFG.DATASET_NAME))["processed_data"][..., 0] +train_index_list = load_pkl("datasets/{0}/index_in{1}_out{2}.pkl".format( + CFG.DATASET_NAME, CFG.DATASET_INPUT_LEN, CFG.DATASET_OUTPUT_LEN))["train"] +node_feats = node_feats_full[:train_index_list[-1][-1], ...] +CFG.MODEL.PARAM = { + "cl_decay_steps": 2000, + "filter_type": "dual_random_walk", + "horizon": 12, + "input_dim": 2, + "l1_decay": 0, + "max_diffusion_step": 3, + "num_nodes": 170, + "num_rnn_layers": 1, + "output_dim": 1, + "rnn_units": 64, + "seq_len": 12, + "use_curriculum_learning": True, + "dim_fc": 171280, + "node_feats": node_feats, + "temp": 0.5, + "k": 30 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = gts_loss +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.001, + "eps": 1e-3 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [20, 30], + "gamma": 0.1 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +CFG.TRAIN.SETUP_GRAPH = True +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/GTS/loss.py b/examples/GTS/loss.py new file mode 100644 index 00000000..2ad0a866 --- /dev/null +++ b/examples/GTS/loss.py @@ -0,0 +1,16 @@ +import torch +import numpy as np +from basicts.losses import masked_mae + + +def gts_loss(prediction, real_value, pred_adj, prior_adj, null_val = np.nan): + # graph loss + prior_label = prior_adj.view(prior_adj.shape[0] * prior_adj.shape[1]).to(pred_adj.device) + pred_label = pred_adj.view(pred_adj.shape[0] * pred_adj.shape[1]) + graph_loss_function = torch.nn.BCELoss() + loss_g = graph_loss_function(pred_label, prior_label) + # regression loss + loss_r = masked_mae(prediction, real_value, null_val=null_val) + # total loss + loss = loss_r + loss_g + return loss diff --git a/examples/GWNet/GWNet_METR-LA.py b/examples/GWNet/GWNet_METR-LA.py new file mode 100644 index 00000000..1142a956 --- /dev/null +++ b/examples/GWNet/GWNet_METR-LA.py @@ -0,0 +1,122 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import GraphWaveNet +from basicts.runners import GraphWaveNetRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Graph WaveNet model configuration" +CFG.RUNNER = GraphWaveNetRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "METR-LA" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "GraphWaveNet" +CFG.MODEL.ARCH = GraphWaveNet +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "num_nodes": 207, + "supports": [torch.tensor(i) for i in adj_mx], + "dropout": 0.3, + "gcn_bool": True, + "addaptadj": True, + "aptinit": None, + "in_dim": 2, + "out_dim": 12, + "residual_channels": 32, + "dilation_channels": 32, + "skip_channels": 256, + "end_channels": 512, + "kernel_size": 2, + "blocks": 4, + "layers": 2 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 100 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/GWNet/GWNet_PEMS-BAY.py b/examples/GWNet/GWNet_PEMS-BAY.py new file mode 100644 index 00000000..b5b3ceb8 --- /dev/null +++ b/examples/GWNet/GWNet_PEMS-BAY.py @@ -0,0 +1,122 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import GraphWaveNet +from basicts.runners import GraphWaveNetRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Graph WaveNet model configuration" +CFG.RUNNER = GraphWaveNetRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS-BAY" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "GraphWaveNet" +CFG.MODEL.ARCH = GraphWaveNet +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "num_nodes": 325, + "supports": [torch.tensor(i) for i in adj_mx], + "dropout": 0.3, + "gcn_bool": True, + "addaptadj": True, + "aptinit": None, + "in_dim": 2, + "out_dim": 12, + "residual_channels": 32, + "dilation_channels": 32, + "skip_channels": 256, + "end_channels": 512, + "kernel_size": 2, + "blocks": 4, + "layers": 2 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 100 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/GWNet/GWNet_PEMS03.py b/examples/GWNet/GWNet_PEMS03.py new file mode 100644 index 00000000..b7200664 --- /dev/null +++ b/examples/GWNet/GWNet_PEMS03.py @@ -0,0 +1,122 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import GraphWaveNet +from basicts.runners import GraphWaveNetRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Graph WaveNet model configuration" +CFG.RUNNER = GraphWaveNetRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS03" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "GraphWaveNet" +CFG.MODEL.ARCH = GraphWaveNet +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "num_nodes": 358, + "supports": [torch.tensor(i) for i in adj_mx], + "dropout": 0.3, + "gcn_bool": True, + "addaptadj": True, + "aptinit": None, + "in_dim": 2, + "out_dim": 12, + "residual_channels": 32, + "dilation_channels": 32, + "skip_channels": 256, + "end_channels": 512, + "kernel_size": 2, + "blocks": 4, + "layers": 2 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 100], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/GWNet/GWNet_PEMS04.py b/examples/GWNet/GWNet_PEMS04.py new file mode 100644 index 00000000..33fab968 --- /dev/null +++ b/examples/GWNet/GWNet_PEMS04.py @@ -0,0 +1,122 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import GraphWaveNet +from basicts.runners import GraphWaveNetRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Graph WaveNet model configuration" +CFG.RUNNER = GraphWaveNetRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS04" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "GraphWaveNet" +CFG.MODEL.ARCH = GraphWaveNet +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "num_nodes": 307, + "supports": [torch.tensor(i) for i in adj_mx], + "dropout": 0.3, + "gcn_bool": True, + "addaptadj": True, + "aptinit": None, + "in_dim": 2, + "out_dim": 12, + "residual_channels": 32, + "dilation_channels": 32, + "skip_channels": 256, + "end_channels": 512, + "kernel_size": 2, + "blocks": 4, + "layers": 2 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 100], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/GWNet/GWNet_PEMS07.py b/examples/GWNet/GWNet_PEMS07.py new file mode 100644 index 00000000..a5c33e43 --- /dev/null +++ b/examples/GWNet/GWNet_PEMS07.py @@ -0,0 +1,122 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import GraphWaveNet +from basicts.runners import GraphWaveNetRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Graph WaveNet model configuration" +CFG.RUNNER = GraphWaveNetRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS07" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "GraphWaveNet" +CFG.MODEL.ARCH = GraphWaveNet +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "num_nodes": 883, + "supports": [torch.tensor(i) for i in adj_mx], + "dropout": 0.3, + "gcn_bool": True, + "addaptadj": True, + "aptinit": None, + "in_dim": 2, + "out_dim": 12, + "residual_channels": 32, + "dilation_channels": 32, + "skip_channels": 256, + "end_channels": 512, + "kernel_size": 2, + "blocks": 4, + "layers": 2 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 100], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# validating data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/GWNet/GWNet_PEMS08.py b/examples/GWNet/GWNet_PEMS08.py new file mode 100644 index 00000000..d1c4bee5 --- /dev/null +++ b/examples/GWNet/GWNet_PEMS08.py @@ -0,0 +1,122 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import GraphWaveNet +from basicts.runners import GraphWaveNetRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Graph WaveNet model configuration" +CFG.RUNNER = GraphWaveNetRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS08" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "GraphWaveNet" +CFG.MODEL.ARCH = GraphWaveNet +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + + "/adj_mx.pkl", "doubletransition") +CFG.MODEL.PARAM = { + "num_nodes": 170, + "supports": [torch.tensor(i) for i in adj_mx], + "dropout": 0.3, + "gcn_bool": True, + "addaptadj": True, + "aptinit": None, + "in_dim": 2, + "out_dim": 12, + "residual_channels": 32, + "dilation_channels": 32, + "skip_channels": 256, + "end_channels": 512, + "kernel_size": 2, + "blocks": 4, + "layers": 2 +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 100], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/Linear/Linear_METR-LA.py b/examples/Linear/Linear_METR-LA.py new file mode 100644 index 00000000..e5f8f28a --- /dev/null +++ b/examples/Linear/Linear_METR-LA.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import Linear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "METR-LA" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "Linear" +CFG.MODEL.ARCH = Linear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/Linear/Linear_PEMS-BAY.py b/examples/Linear/Linear_PEMS-BAY.py new file mode 100644 index 00000000..e57a6907 --- /dev/null +++ b/examples/Linear/Linear_PEMS-BAY.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import Linear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS-BAY" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "Linear" +CFG.MODEL.ARCH = Linear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/Linear/Linear_PEMS03.py b/examples/Linear/Linear_PEMS03.py new file mode 100644 index 00000000..3a35632b --- /dev/null +++ b/examples/Linear/Linear_PEMS03.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import Linear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS03" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "Linear" +CFG.MODEL.ARCH = Linear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/Linear/Linear_PEMS04.py b/examples/Linear/Linear_PEMS04.py new file mode 100644 index 00000000..f80c9c5f --- /dev/null +++ b/examples/Linear/Linear_PEMS04.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import Linear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS04" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "Linear" +CFG.MODEL.ARCH = Linear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/Linear/Linear_PEMS07.py b/examples/Linear/Linear_PEMS07.py new file mode 100644 index 00000000..dc8fae8b --- /dev/null +++ b/examples/Linear/Linear_PEMS07.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import Linear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS07" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "Linear" +CFG.MODEL.ARCH = Linear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/Linear/Linear_PEMS08.py b/examples/Linear/Linear_PEMS08.py new file mode 100644 index 00000000..363421da --- /dev/null +++ b/examples/Linear/Linear_PEMS08.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import Linear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS08" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "Linear" +CFG.MODEL.ARCH = Linear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/MLP/MLP_METR-LA.py b/examples/MLP/MLP_METR-LA.py new file mode 100644 index 00000000..603db610 --- /dev/null +++ b/examples/MLP/MLP_METR-LA.py @@ -0,0 +1,110 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.data import TimeSeriesForecastingDataset + +from .MLP_arch import MultiLayerPerceptron +from .MLP_runner import SimpleTimeSeriesForecastingRunner +from basicts.losses import masked_mae + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Multi-layer perceptron model configuration" +CFG.RUNNER = SimpleTimeSeriesForecastingRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "METR-LA" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "MultiLayerPerceptron" +CFG.MODEL.ARCH = MultiLayerPerceptron +CFG.MODEL.PARAM = { + "history_seq_len": CFG.DATASET_INPUT_LEN, + "prediction_seq_len": CFG.DATASET_OUTPUT_LEN, + "hidden_dim": 32 +} +CFG.MODEL.FROWARD_FEATURES = [0] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 1.0e-5, + "eps": 1.0e-8 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 30, 38, 46, 54, 62, 70, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 100 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 32 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# evluation +CFG.TEST.EVALUATION_HORIZONS = range(12) +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 32 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/MLP/MLP_arch.py b/examples/MLP/MLP_arch.py new file mode 100644 index 00000000..0e242cd0 --- /dev/null +++ b/examples/MLP/MLP_arch.py @@ -0,0 +1,25 @@ +import torch +from torch import nn + +class MultiLayerPerceptron(nn.Module): + """Two fully connected layer.""" + + def __init__(self, history_seq_len: int, prediction_seq_len: int, hidden_dim: int): + super().__init__() + self.fc1 = nn.Linear(history_seq_len, hidden_dim) + self.fc2 = nn.Linear(hidden_dim, prediction_seq_len) + self.act = nn.ReLU() + + def forward(self, history_data: torch.Tensor, future_data: torch.Tensor, batch_seen: int, epoch: int, train: bool, **kwargs) -> torch.Tensor: + """Feedforward function of AGCRN. + + Args: + history_data (torch.Tensor): inputs with shape [B, L, N, C]. + + Returns: + torch.Tensor: outputs with shape [B, L, N, C] + """ + + history_data = history_data[..., 0].transpose(1, 2) # B, N, L + prediction = self.fc2(self.act(self.fc1(history_data))).transpose(1, 2) # B, L, N + return prediction.unsqueeze(-1) # B, L, N, C diff --git a/examples/MLP/MLP_runner.py b/examples/MLP/MLP_runner.py new file mode 100644 index 00000000..ad337c81 --- /dev/null +++ b/examples/MLP/MLP_runner.py @@ -0,0 +1,80 @@ +import torch + +from basicts.runners import BaseTimeSeriesForecastingRunner + + +class SimpleTimeSeriesForecastingRunner(BaseTimeSeriesForecastingRunner): + """Simple Runner: select forward features and target features. + Copy from basicts.runners.simple_tsf_runner.py.""" + + def __init__(self, cfg: dict): + super().__init__(cfg) + self.forward_features = cfg["MODEL"].get("FROWARD_FEATURES", None) + self.target_features = cfg["MODEL"].get("TARGET_FEATURES", None) + + def select_input_features(self, data: torch.Tensor) -> torch.Tensor: + """Select input features. + + Args: + data (torch.Tensor): input history data, shape [B, L, N, C] + + Returns: + torch.Tensor: reshaped data + """ + + # select feature using self.forward_features + if self.forward_features is not None: + data = data[:, :, :, self.forward_features] + return data + + def select_target_features(self, data: torch.Tensor) -> torch.Tensor: + """Select target feature. + + Args: + data (torch.Tensor): prediction of the model with arbitrary shape. + + Returns: + torch.Tensor: reshaped data with shape [B, L, N, C] + """ + + # select feature using self.target_features + data = data[:, :, :, self.target_features] + return data + + def forward(self, data: tuple, epoch: int = None, iter_num: int = None, train: bool = True, **kwargs) -> tuple: + """Feed forward process for train, val, and test. Note that the outputs are NOT re-scaled. + + Args: + data (tuple): data (future data, history ata). + epoch (int, optional): epoch number. Defaults to None. + iter_num (int, optional): iteration number. Defaults to None. + train (bool, optional): if in the training process. Defaults to True. + + Returns: + tuple: (prediction, real_value) + """ + + # preprocess + future_data, history_data = data + history_data = self.to_running_device(history_data) # B, L, N, C + future_data = self.to_running_device(future_data) # B, L, N, C + batch_size, length, num_nodes, _ = future_data.shape + + history_data = self.select_input_features(history_data) + _future_data = self.select_input_features(future_data) + + # curriculum learning + if self.cl_param is None: + prediction_data = self.model( + history_data=history_data, future_data=_future_data, batch_seen=iter_num, epoch=epoch, train=train) + else: + task_level = self.curriculum_learning(epoch) + prediction_data = self.model(history_data=history_data, future_data=_future_data, + batch_seen=iter_num, epoch=epoch, train=train, task_level=task_level) + # feed forward + assert list(prediction_data.shape)[:3] == [batch_size, length, num_nodes], \ + "error shape of the output, edit the forward function to reshape it to [B, L, N, C]" + # post process + prediction = self.select_target_features(prediction_data) + real_value = self.select_target_features(future_data) + return prediction, real_value diff --git a/basicts/options/MTGNN/MTGNN_METR-LA.py b/examples/MTGNN/MTGNN_METR-LA.py similarity index 52% rename from basicts/options/MTGNN/MTGNN_METR-LA.py rename to examples/MTGNN/MTGNN_METR-LA.py index a740610a..0026a6f4 100644 --- a/basicts/options/MTGNN/MTGNN_METR-LA.py +++ b/examples/MTGNN/MTGNN_METR-LA.py @@ -1,39 +1,39 @@ import os -from easydict import EasyDict +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) import torch -# runner -from basicts.runners.MTGNN_runner import MTGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj +from easydict import EasyDict +from basicts.archs import MTGNN +from basicts.runners import MTGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + CFG = EasyDict() # ================= general ================= # -CFG.DESCRIPTION = 'MTGNN model configuration' -CFG.RUNNER = MTGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "METR-LA" -CFG.DATASET_TYPE = 'Traffic speed' +CFG.DESCRIPTION = "MTGNN model configuration" +CFG.RUNNER = MTGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "METR-LA" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} # ================= environment ================= # CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 +CFG.ENV.SEED = 1 CFG.ENV.CUDNN = EasyDict() CFG.ENV.CUDNN.ENABLED = True # ================= model ================= # CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'MTGNN' +CFG.MODEL.NAME = "MTGNN" +CFG.MODEL.ARCH = MTGNN buildA_true = True num_nodes = 207 if buildA_true: # self-learned adjacency matrix @@ -43,7 +43,7 @@ adj_mx = torch.tensor(adj_mx)-torch.eye(num_nodes) CFG.MODEL.PARAM = { - "gcn_true" : True, + "gcn_true" : True, "buildA_true": buildA_true, "gcn_depth": 2, "num_nodes": num_nodes, @@ -64,17 +64,17 @@ "tanhalpha":3, "layer_norm_affline":True } -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] # ================= optim ================= # CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss +CFG.TRAIN.LOSS = masked_mae CFG.TRAIN.OPTIM = EasyDict() CFG.TRAIN.OPTIM.TYPE = "Adam" CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":0.0001, + "lr": 0.001, + "weight_decay": 0.0001, } # ================= train ================= # @@ -84,24 +84,24 @@ CFG.TRAIN.CUSTOM.NUM_SPLIT = 1 CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 + "max_norm": 5.0 } CFG.TRAIN.NUM_EPOCHS = 100 CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) ) # train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False ## curriculum learning CFG.TRAIN.CL = EasyDict() CFG.TRAIN.CL.WARM_EPOCHS = 0 @@ -113,25 +113,25 @@ CFG.VAL.INTERVAL = 1 # validating data CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 32 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False # ================= test ================= # CFG.TEST = EasyDict() CFG.TEST.INTERVAL = 1 -# validating data +# test data CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 32 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 32 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/MTGNN/MTGNN_PEMS-BAY.py b/examples/MTGNN/MTGNN_PEMS-BAY.py similarity index 52% rename from basicts/options/MTGNN/MTGNN_PEMS-BAY.py rename to examples/MTGNN/MTGNN_PEMS-BAY.py index cfa34edb..43b11670 100644 --- a/basicts/options/MTGNN/MTGNN_PEMS-BAY.py +++ b/examples/MTGNN/MTGNN_PEMS-BAY.py @@ -1,39 +1,39 @@ import os -from easydict import EasyDict +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) import torch -# runner -from basicts.runners.MTGNN_runner import MTGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj +from easydict import EasyDict +from basicts.archs import MTGNN +from basicts.runners import MTGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + CFG = EasyDict() # ================= general ================= # -CFG.DESCRIPTION = 'MTGNN model configuration' -CFG.RUNNER = MTGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS-BAY" -CFG.DATASET_TYPE = 'Traffic speed' +CFG.DESCRIPTION = "MTGNN model configuration" +CFG.RUNNER = MTGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS-BAY" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} # ================= environment ================= # CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 +CFG.ENV.SEED = 1 CFG.ENV.CUDNN = EasyDict() CFG.ENV.CUDNN.ENABLED = True # ================= model ================= # CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'MTGNN' +CFG.MODEL.NAME = "MTGNN" +CFG.MODEL.ARCH = MTGNN buildA_true = True num_nodes = 325 if buildA_true: # self-learned adjacency matrix @@ -43,7 +43,7 @@ adj_mx = torch.tensor(adj_mx)-torch.eye(num_nodes) CFG.MODEL.PARAM = { - "gcn_true" : True, + "gcn_true" : True, "buildA_true": buildA_true, "gcn_depth": 2, "num_nodes": num_nodes, @@ -64,17 +64,17 @@ "tanhalpha":3, "layer_norm_affline":True } -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] # ================= optim ================= # CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss +CFG.TRAIN.LOSS = masked_mae CFG.TRAIN.OPTIM = EasyDict() CFG.TRAIN.OPTIM.TYPE = "Adam" CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":0.0001, + "lr": 0.001, + "weight_decay": 0.0001, } # ================= train ================= # @@ -84,55 +84,54 @@ CFG.TRAIN.CUSTOM.NUM_SPLIT = 1 CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 + "max_norm": 5.0 } CFG.TRAIN.NUM_EPOCHS = 100 CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) ) # train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False ## curriculum learning CFG.TRAIN.CL = EasyDict() CFG.TRAIN.CL.WARM_EPOCHS = 0 CFG.TRAIN.CL.CL_EPOCHS = 3 CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - # ================= validate ================= # CFG.VAL = EasyDict() CFG.VAL.INTERVAL = 1 # validating data CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 32 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False # ================= test ================= # CFG.TEST = EasyDict() CFG.TEST.INTERVAL = 1 -# validating data +# test data CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 32 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 32 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/MTGNN/MTGNN_PEMS03.py b/examples/MTGNN/MTGNN_PEMS03.py similarity index 52% rename from basicts/options/MTGNN/MTGNN_PEMS03.py rename to examples/MTGNN/MTGNN_PEMS03.py index dbd84455..3adcc25e 100644 --- a/basicts/options/MTGNN/MTGNN_PEMS03.py +++ b/examples/MTGNN/MTGNN_PEMS03.py @@ -1,39 +1,39 @@ import os -from easydict import EasyDict +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) import torch -# runner -from basicts.runners.MTGNN_runner import MTGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj +from easydict import EasyDict +from basicts.archs import MTGNN +from basicts.runners import MTGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + CFG = EasyDict() # ================= general ================= # -CFG.DESCRIPTION = 'MTGNN model configuration' -CFG.RUNNER = MTGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS03" -CFG.DATASET_TYPE = 'Traffic flow' +CFG.DESCRIPTION = "MTGNN model configuration" +CFG.RUNNER = MTGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS03" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} # ================= environment ================= # CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 +CFG.ENV.SEED = 1 CFG.ENV.CUDNN = EasyDict() CFG.ENV.CUDNN.ENABLED = True # ================= model ================= # CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'MTGNN' +CFG.MODEL.NAME = "MTGNN" +CFG.MODEL.ARCH = MTGNN buildA_true = True num_nodes = 358 if buildA_true: # self-learned adjacency matrix @@ -43,7 +43,7 @@ adj_mx = torch.tensor(adj_mx)-torch.eye(num_nodes) CFG.MODEL.PARAM = { - "gcn_true" : True, + "gcn_true" : True, "buildA_true": buildA_true, "gcn_depth": 2, "num_nodes": num_nodes, @@ -64,17 +64,17 @@ "tanhalpha":3, "layer_norm_affline":True } -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] # ================= optim ================= # CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss +CFG.TRAIN.LOSS = masked_mae CFG.TRAIN.OPTIM = EasyDict() CFG.TRAIN.OPTIM.TYPE = "Adam" CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":0.0001, + "lr": 0.001, + "weight_decay": 0.0001, } # ================= train ================= # @@ -84,55 +84,54 @@ CFG.TRAIN.CUSTOM.NUM_SPLIT = 1 CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 + "max_norm": 5.0 } CFG.TRAIN.NUM_EPOCHS = 100 CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) ) # train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False ## curriculum learning CFG.TRAIN.CL = EasyDict() CFG.TRAIN.CL.WARM_EPOCHS = 0 CFG.TRAIN.CL.CL_EPOCHS = 3 CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - # ================= validate ================= # CFG.VAL = EasyDict() CFG.VAL.INTERVAL = 1 # validating data CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 32 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False # ================= test ================= # CFG.TEST = EasyDict() CFG.TEST.INTERVAL = 1 -# validating data +# test data CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 32 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 32 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/MTGNN/MTGNN_PEMS04.py b/examples/MTGNN/MTGNN_PEMS04.py similarity index 52% rename from basicts/options/MTGNN/MTGNN_PEMS04.py rename to examples/MTGNN/MTGNN_PEMS04.py index fccf5b40..d4f1397f 100644 --- a/basicts/options/MTGNN/MTGNN_PEMS04.py +++ b/examples/MTGNN/MTGNN_PEMS04.py @@ -1,39 +1,39 @@ import os -from easydict import EasyDict +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) import torch -# runner -from basicts.runners.MTGNN_runner import MTGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj +from easydict import EasyDict +from basicts.archs import MTGNN +from basicts.runners import MTGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + CFG = EasyDict() # ================= general ================= # -CFG.DESCRIPTION = 'MTGNN model configuration' -CFG.RUNNER = MTGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS04" -CFG.DATASET_TYPE = 'Traffic flow' +CFG.DESCRIPTION = "MTGNN model configuration" +CFG.RUNNER = MTGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS04" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} # ================= environment ================= # CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 +CFG.ENV.SEED = 1 CFG.ENV.CUDNN = EasyDict() CFG.ENV.CUDNN.ENABLED = True # ================= model ================= # CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'MTGNN' +CFG.MODEL.NAME = "MTGNN" +CFG.MODEL.ARCH = MTGNN buildA_true = True num_nodes = 307 if buildA_true: # self-learned adjacency matrix @@ -43,7 +43,7 @@ adj_mx = torch.tensor(adj_mx)-torch.eye(num_nodes) CFG.MODEL.PARAM = { - "gcn_true" : True, + "gcn_true" : True, "buildA_true": buildA_true, "gcn_depth": 2, "num_nodes": num_nodes, @@ -64,17 +64,17 @@ "tanhalpha":3, "layer_norm_affline":True } -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] # ================= optim ================= # CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss +CFG.TRAIN.LOSS = masked_mae CFG.TRAIN.OPTIM = EasyDict() CFG.TRAIN.OPTIM.TYPE = "Adam" CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":0.0001, + "lr": 0.001, + "weight_decay": 0.0001, } # ================= train ================= # @@ -84,55 +84,54 @@ CFG.TRAIN.CUSTOM.NUM_SPLIT = 1 CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 + "max_norm": 5.0 } CFG.TRAIN.NUM_EPOCHS = 100 CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) ) # train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False ## curriculum learning CFG.TRAIN.CL = EasyDict() CFG.TRAIN.CL.WARM_EPOCHS = 0 CFG.TRAIN.CL.CL_EPOCHS = 3 CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - # ================= validate ================= # CFG.VAL = EasyDict() CFG.VAL.INTERVAL = 1 # validating data CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 32 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False # ================= test ================= # CFG.TEST = EasyDict() CFG.TEST.INTERVAL = 1 -# validating data +# test data CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 32 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 32 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/MTGNN/MTGNN_PEMS07.py b/examples/MTGNN/MTGNN_PEMS07.py similarity index 52% rename from basicts/options/MTGNN/MTGNN_PEMS07.py rename to examples/MTGNN/MTGNN_PEMS07.py index 2962e27d..b554907d 100644 --- a/basicts/options/MTGNN/MTGNN_PEMS07.py +++ b/examples/MTGNN/MTGNN_PEMS07.py @@ -1,39 +1,39 @@ import os -from easydict import EasyDict +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) import torch -# runner -from basicts.runners.MTGNN_runner import MTGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj +from easydict import EasyDict +from basicts.archs import MTGNN +from basicts.runners import MTGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + CFG = EasyDict() # ================= general ================= # -CFG.DESCRIPTION = 'MTGNN model configuration' -CFG.RUNNER = MTGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS07" -CFG.DATASET_TYPE = 'Traffic flow' +CFG.DESCRIPTION = "MTGNN model configuration" +CFG.RUNNER = MTGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS07" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} # ================= environment ================= # CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 +CFG.ENV.SEED = 1 CFG.ENV.CUDNN = EasyDict() CFG.ENV.CUDNN.ENABLED = True # ================= model ================= # CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'MTGNN' +CFG.MODEL.NAME = "MTGNN" +CFG.MODEL.ARCH = MTGNN buildA_true = True num_nodes = 883 if buildA_true: # self-learned adjacency matrix @@ -43,7 +43,7 @@ adj_mx = torch.tensor(adj_mx)-torch.eye(num_nodes) CFG.MODEL.PARAM = { - "gcn_true" : True, + "gcn_true" : True, "buildA_true": buildA_true, "gcn_depth": 2, "num_nodes": num_nodes, @@ -64,17 +64,17 @@ "tanhalpha":3, "layer_norm_affline":True } -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] # ================= optim ================= # CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss +CFG.TRAIN.LOSS = masked_mae CFG.TRAIN.OPTIM = EasyDict() CFG.TRAIN.OPTIM.TYPE = "Adam" CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":0.0001, + "lr": 0.001, + "weight_decay": 0.0001, } # ================= train ================= # @@ -84,55 +84,54 @@ CFG.TRAIN.CUSTOM.NUM_SPLIT = 1 CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 + "max_norm": 5.0 } CFG.TRAIN.NUM_EPOCHS = 100 CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) ) # train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False ## curriculum learning CFG.TRAIN.CL = EasyDict() CFG.TRAIN.CL.WARM_EPOCHS = 0 CFG.TRAIN.CL.CL_EPOCHS = 3 CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - # ================= validate ================= # CFG.VAL = EasyDict() CFG.VAL.INTERVAL = 1 # validating data CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 32 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False # ================= test ================= # CFG.TEST = EasyDict() CFG.TEST.INTERVAL = 1 -# validating data +# test data CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 32 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 32 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/basicts/options/MTGNN/MTGNN_PEMS08.py b/examples/MTGNN/MTGNN_PEMS08.py similarity index 52% rename from basicts/options/MTGNN/MTGNN_PEMS08.py rename to examples/MTGNN/MTGNN_PEMS08.py index da672872..96003f82 100644 --- a/basicts/options/MTGNN/MTGNN_PEMS08.py +++ b/examples/MTGNN/MTGNN_PEMS08.py @@ -1,39 +1,39 @@ import os -from easydict import EasyDict +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) import torch -# runner -from basicts.runners.MTGNN_runner import MTGNNRunner -from basicts.data.base_dataset import BaseDataset -from basicts.metrics.mae import masked_mae -from basicts.metrics.mape import masked_mape -from basicts.metrics.rmse import masked_rmse -from basicts.losses.losses import masked_l1_loss -from basicts.utils.serialization import load_adj +from easydict import EasyDict +from basicts.archs import MTGNN +from basicts.runners import MTGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + CFG = EasyDict() # ================= general ================= # -CFG.DESCRIPTION = 'MTGNN model configuration' -CFG.RUNNER = MTGNNRunner -CFG.DATASET_CLS = BaseDataset -CFG.DATASET_NAME = "PEMS08" -CFG.DATASET_TYPE = 'Traffic flow' +CFG.DESCRIPTION = "MTGNN model configuration" +CFG.RUNNER = MTGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS08" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 CFG.GPU_NUM = 1 -CFG.METRICS = { - "MAE": masked_mae, - "RMSE": masked_rmse, - "MAPE": masked_mape -} # ================= environment ================= # CFG.ENV = EasyDict() -CFG.ENV.SEED = 1 +CFG.ENV.SEED = 1 CFG.ENV.CUDNN = EasyDict() CFG.ENV.CUDNN.ENABLED = True # ================= model ================= # CFG.MODEL = EasyDict() -CFG.MODEL.NAME = 'MTGNN' +CFG.MODEL.NAME = "MTGNN" +CFG.MODEL.ARCH = MTGNN buildA_true = True num_nodes = 170 if buildA_true: # self-learned adjacency matrix @@ -43,7 +43,7 @@ adj_mx = torch.tensor(adj_mx)-torch.eye(num_nodes) CFG.MODEL.PARAM = { - "gcn_true" : True, + "gcn_true" : True, "buildA_true": buildA_true, "gcn_depth": 2, "num_nodes": num_nodes, @@ -64,17 +64,17 @@ "tanhalpha":3, "layer_norm_affline":True } -CFG.MODEL.FROWARD_FEATURES = [0, 1] -CFG.MODEL.TARGET_FEATURES = [0] +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] # ================= optim ================= # CFG.TRAIN = EasyDict() -CFG.TRAIN.LOSS = masked_l1_loss +CFG.TRAIN.LOSS = masked_mae CFG.TRAIN.OPTIM = EasyDict() CFG.TRAIN.OPTIM.TYPE = "Adam" CFG.TRAIN.OPTIM.PARAM= { - "lr":0.001, - "weight_decay":0.0001, + "lr": 0.001, + "weight_decay": 0.0001, } # ================= train ================= # @@ -84,55 +84,54 @@ CFG.TRAIN.CUSTOM.NUM_SPLIT = 1 CFG.TRAIN.CLIP_GRAD_PARAM = { - 'max_norm': 5.0 + "max_norm": 5.0 } CFG.TRAIN.NUM_EPOCHS = 100 CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( - 'checkpoints', - '_'.join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) ) # train data -CFG.TRAIN.DATA = EasyDict() -CFG.TRAIN.NULL_VAL = 0.0 -## read data -CFG.TRAIN.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TRAIN.DATA.BATCH_SIZE = 32 -CFG.TRAIN.DATA.PREFETCH = False -CFG.TRAIN.DATA.SHUFFLE = True -CFG.TRAIN.DATA.NUM_WORKERS = 2 -CFG.TRAIN.DATA.PIN_MEMORY = False +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False ## curriculum learning CFG.TRAIN.CL = EasyDict() CFG.TRAIN.CL.WARM_EPOCHS = 0 CFG.TRAIN.CL.CL_EPOCHS = 3 CFG.TRAIN.CL.PREDICTION_LENGTH = 12 - # ================= validate ================= # CFG.VAL = EasyDict() CFG.VAL.INTERVAL = 1 # validating data CFG.VAL.DATA = EasyDict() -## read data -CFG.VAL.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.VAL.DATA.BATCH_SIZE = 32 -CFG.VAL.DATA.PREFETCH = False -CFG.VAL.DATA.SHUFFLE = False -CFG.VAL.DATA.NUM_WORKERS = 2 -CFG.VAL.DATA.PIN_MEMORY = False +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 32 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False # ================= test ================= # CFG.TEST = EasyDict() CFG.TEST.INTERVAL = 1 -# validating data +# test data CFG.TEST.DATA = EasyDict() -## read data -CFG.TEST.DATA.DIR = 'datasets/' + CFG.DATASET_NAME -## dataloader args, optional -CFG.TEST.DATA.BATCH_SIZE = 32 -CFG.TEST.DATA.PREFETCH = False -CFG.TEST.DATA.SHUFFLE = False -CFG.TEST.DATA.NUM_WORKERS = 2 -CFG.TEST.DATA.PIN_MEMORY = False +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 32 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/NLinear/NLinear_METR-LA.py b/examples/NLinear/NLinear_METR-LA.py new file mode 100644 index 00000000..c9e56558 --- /dev/null +++ b/examples/NLinear/NLinear_METR-LA.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import NLinear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "METR-LA" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "NLinear" +CFG.MODEL.ARCH = NLinear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/NLinear/NLinear_PEMS-BAY.py b/examples/NLinear/NLinear_PEMS-BAY.py new file mode 100644 index 00000000..c3efccf5 --- /dev/null +++ b/examples/NLinear/NLinear_PEMS-BAY.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import NLinear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS-BAY" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "NLinear" +CFG.MODEL.ARCH = NLinear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/NLinear/NLinear_PEMS03.py b/examples/NLinear/NLinear_PEMS03.py new file mode 100644 index 00000000..3abecb11 --- /dev/null +++ b/examples/NLinear/NLinear_PEMS03.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import NLinear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS03" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "NLinear" +CFG.MODEL.ARCH = NLinear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/NLinear/NLinear_PEMS04.py b/examples/NLinear/NLinear_PEMS04.py new file mode 100644 index 00000000..d7d5ccd7 --- /dev/null +++ b/examples/NLinear/NLinear_PEMS04.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import NLinear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS04" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "NLinear" +CFG.MODEL.ARCH = NLinear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/NLinear/NLinear_PEMS07.py b/examples/NLinear/NLinear_PEMS07.py new file mode 100644 index 00000000..dd11c10f --- /dev/null +++ b/examples/NLinear/NLinear_PEMS07.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import NLinear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS07" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "NLinear" +CFG.MODEL.ARCH = NLinear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/NLinear/NLinear_PEMS08.py b/examples/NLinear/NLinear_PEMS08.py new file mode 100644 index 00000000..fcdf42fe --- /dev/null +++ b/examples/NLinear/NLinear_PEMS08.py @@ -0,0 +1,102 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import LinearRunner +from basicts.archs import NLinear + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "Linear model configuration" +CFG.RUNNER = LinearRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS08" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "NLinear" +CFG.MODEL.ARCH = NLinear +CFG.MODEL.PARAM = { + "seq_len": 12, + "pred_len": 12 +} +CFG.MODEL.FROWARD_FEATURES = [0] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STGCN/STGCN_METR-LA.py b/examples/STGCN/STGCN_METR-LA.py new file mode 100644 index 00000000..049aa794 --- /dev/null +++ b/examples/STGCN/STGCN_METR-LA.py @@ -0,0 +1,117 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import STGCN +from basicts.runners import STGCNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STGCN model configuration" +CFG.RUNNER = STGCNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "METR-LA" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STGCN" +CFG.MODEL.ARCH = STGCN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "normlap") +adj_mx = torch.Tensor(adj_mx[0]) +CFG.MODEL.PARAM = { + "Ks" : 3, + "Kt" : 3, + "blocks" : [[1], [64, 16, 64], [64, 16, 64], [128, 128], [12]], + "T" : 12, + "n_vertex" : 207, + "act_func" : "glu", + "graph_conv_type" : "cheb_graph_conv", + "gso" : adj_mx, + "bias": True, + "droprate" : 0.5 +} +CFG.MODEL.FROWARD_FEATURES = [0] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 100 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STGCN/STGCN_PEMS-BAY.py b/examples/STGCN/STGCN_PEMS-BAY.py new file mode 100644 index 00000000..77fc04e4 --- /dev/null +++ b/examples/STGCN/STGCN_PEMS-BAY.py @@ -0,0 +1,117 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import STGCN +from basicts.runners import STGCNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STGCN model configuration" +CFG.RUNNER = STGCNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS-BAY" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STGCN" +CFG.MODEL.ARCH = STGCN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "normlap") +adj_mx = torch.Tensor(adj_mx[0]) +CFG.MODEL.PARAM = { + "Ks" : 3, + "Kt" : 3, + "blocks" : [[1], [64, 16, 64], [64, 16, 64], [128, 128], [12]], + "T" : 12, + "n_vertex" : 325, + "act_func" : "glu", + "graph_conv_type" : "cheb_graph_conv", + "gso" : adj_mx, + "bias": True, + "droprate" : 0.5 +} +CFG.MODEL.FROWARD_FEATURES = [0] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 100 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STGCN/STGCN_PEMS03.py b/examples/STGCN/STGCN_PEMS03.py new file mode 100644 index 00000000..563305bb --- /dev/null +++ b/examples/STGCN/STGCN_PEMS03.py @@ -0,0 +1,117 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import STGCN +from basicts.runners import STGCNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STGCN model configuration" +CFG.RUNNER = STGCNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS03" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STGCN" +CFG.MODEL.ARCH = STGCN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "normlap") +adj_mx = torch.Tensor(adj_mx[0]) +CFG.MODEL.PARAM = { + "Ks" : 3, + "Kt" : 3, + "blocks" : [[1], [64, 16, 64], [64, 16, 64], [128, 128], [12]], + "T" : 12, + "n_vertex" : 358, + "act_func" : "glu", + "graph_conv_type" : "cheb_graph_conv", + "gso" : adj_mx, + "bias": True, + "droprate" : 0.5 +} +CFG.MODEL.FROWARD_FEATURES = [0] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 100], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STGCN/STGCN_PEMS04.py b/examples/STGCN/STGCN_PEMS04.py new file mode 100644 index 00000000..b98ce67c --- /dev/null +++ b/examples/STGCN/STGCN_PEMS04.py @@ -0,0 +1,117 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import STGCN +from basicts.runners import STGCNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STGCN model configuration" +CFG.RUNNER = STGCNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS04" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STGCN" +CFG.MODEL.ARCH = STGCN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "normlap") +adj_mx = torch.Tensor(adj_mx[0]) +CFG.MODEL.PARAM = { + "Ks" : 3, + "Kt" : 3, + "blocks" : [[1], [64, 16, 64], [64, 16, 64], [128, 128], [12]], + "T" : 12, + "n_vertex" : 307, + "act_func" : "glu", + "graph_conv_type" : "cheb_graph_conv", + "gso" : adj_mx, + "bias": True, + "droprate" : 0.5 +} +CFG.MODEL.FROWARD_FEATURES = [0] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 100], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STGCN/STGCN_PEMS07.py b/examples/STGCN/STGCN_PEMS07.py new file mode 100644 index 00000000..50546138 --- /dev/null +++ b/examples/STGCN/STGCN_PEMS07.py @@ -0,0 +1,117 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import STGCN +from basicts.runners import STGCNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STGCN model configuration" +CFG.RUNNER = STGCNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS07" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STGCN" +CFG.MODEL.ARCH = STGCN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "normlap") +adj_mx = torch.Tensor(adj_mx[0]) +CFG.MODEL.PARAM = { + "Ks" : 3, + "Kt" : 3, + "blocks" : [[1], [64, 16, 64], [64, 16, 64], [128, 128], [12]], + "T" : 12, + "n_vertex" : 883, + "act_func" : "glu", + "graph_conv_type" : "cheb_graph_conv", + "gso" : adj_mx, + "bias": True, + "droprate" : 0.5 +} +CFG.MODEL.FROWARD_FEATURES = [0] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 100], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STGCN/STGCN_PEMS08.py b/examples/STGCN/STGCN_PEMS08.py new file mode 100644 index 00000000..2b714ca9 --- /dev/null +++ b/examples/STGCN/STGCN_PEMS08.py @@ -0,0 +1,117 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +import torch +from easydict import EasyDict +from basicts.archs import STGCN +from basicts.runners import STGCNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae +from basicts.utils import load_adj + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STGCN model configuration" +CFG.RUNNER = STGCNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS08" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STGCN" +CFG.MODEL.ARCH = STGCN +adj_mx, _ = load_adj("datasets/" + CFG.DATASET_NAME + "/adj_mx.pkl", "normlap") +adj_mx = torch.Tensor(adj_mx[0]) +CFG.MODEL.PARAM = { + "Ks" : 3, + "Kt" : 3, + "blocks" : [[1], [64, 16, 64], [64, 16, 64], [128, 128], [12]], + "T" : 12, + "n_vertex" : 170, + "act_func" : "glu", + "graph_conv_type" : "cheb_graph_conv", + "gso" : adj_mx, + "bias": True, + "droprate" : 0.5 +} +CFG.MODEL.FROWARD_FEATURES = [0] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 100], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STID/STID_METR-LA.py b/examples/STID/STID_METR-LA.py new file mode 100644 index 00000000..71a45a23 --- /dev/null +++ b/examples/STID/STID_METR-LA.py @@ -0,0 +1,112 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import STIDRunner +from basicts.archs import STID + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STID model configuration" +CFG.RUNNER = STIDRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "METR-LA" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STID" +CFG.MODEL.ARCH = STID +CFG.MODEL.PARAM = { + "num_nodes": 207, + "input_len": 12, + "input_dim": 3, + "embed_dim": 32, + "output_len": 12, + "num_layer": 3, + "if_node": True, + "node_dim": 32, + "if_T_i_D": True, + "if_D_i_W": True, + "temp_dim_tid": 32, + "temp_dim_diw": 32, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STID/STID_PEMS-BAY.py b/examples/STID/STID_PEMS-BAY.py new file mode 100644 index 00000000..7ed1cee7 --- /dev/null +++ b/examples/STID/STID_PEMS-BAY.py @@ -0,0 +1,112 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import STIDRunner +from basicts.archs import STID + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STID model configuration" +CFG.RUNNER = STIDRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS-BAY" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STID" +CFG.MODEL.ARCH = STID +CFG.MODEL.PARAM = { + "num_nodes": 325, + "input_len": 12, + "input_dim": 3, + "embed_dim": 32, + "output_len": 12, + "num_layer": 3, + "if_node": True, + "node_dim": 32, + "if_T_i_D": True, + "if_D_i_W": True, + "temp_dim_tid": 32, + "temp_dim_diw": 32, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] # traffic speed, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic speed + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STID/STID_PEMS03.py b/examples/STID/STID_PEMS03.py new file mode 100644 index 00000000..b06197c8 --- /dev/null +++ b/examples/STID/STID_PEMS03.py @@ -0,0 +1,112 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import STIDRunner +from basicts.archs import STID + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STID model configuration" +CFG.RUNNER = STIDRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS03" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STID" +CFG.MODEL.ARCH = STID +CFG.MODEL.PARAM = { + "num_nodes": 358, + "input_len": 12, + "input_dim": 3, + "embed_dim": 32, + "output_len": 12, + "num_layer": 3, + "if_node": True, + "node_dim": 32, + "if_T_i_D": True, + "if_D_i_W": True, + "temp_dim_tid": 32, + "temp_dim_diw": 32, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] # traffic flow, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic flow + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STID/STID_PEMS04.py b/examples/STID/STID_PEMS04.py new file mode 100644 index 00000000..77d68084 --- /dev/null +++ b/examples/STID/STID_PEMS04.py @@ -0,0 +1,112 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import STIDRunner +from basicts.archs import STID + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STID model configuration" +CFG.RUNNER = STIDRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS04" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STID" +CFG.MODEL.ARCH = STID +CFG.MODEL.PARAM = { + "num_nodes": 307, + "input_len": 12, + "input_dim": 3, + "embed_dim": 32, + "output_len": 12, + "num_layer": 3, + "if_node": True, + "node_dim": 32, + "if_T_i_D": True, + "if_D_i_W": True, + "temp_dim_tid": 32, + "temp_dim_diw": 32, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] # traffic flow, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic flow + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STID/STID_PEMS07.py b/examples/STID/STID_PEMS07.py new file mode 100644 index 00000000..53cc26b4 --- /dev/null +++ b/examples/STID/STID_PEMS07.py @@ -0,0 +1,113 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) + +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import STIDRunner +from basicts.archs import STID + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STID model configuration" +CFG.RUNNER = STIDRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS07" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STID" +CFG.MODEL.ARCH = STID +CFG.MODEL.PARAM = { + "num_nodes": 883, + "input_len": 12, + "input_dim": 3, + "embed_dim": 32, + "output_len": 12, + "num_layer": 3, + "if_node": True, + "node_dim": 32, + "if_T_i_D": True, + "if_D_i_W": True, + "temp_dim_tid": 32, + "temp_dim_diw": 32, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] # traffic flow, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic flow + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STID/STID_PEMS08.py b/examples/STID/STID_PEMS08.py new file mode 100644 index 00000000..7af2c7e3 --- /dev/null +++ b/examples/STID/STID_PEMS08.py @@ -0,0 +1,112 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.losses import masked_mae +from basicts.data import TimeSeriesForecastingDataset +from basicts.runners import STIDRunner +from basicts.archs import STID + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STID model configuration" +CFG.RUNNER = STIDRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS08" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STID" +CFG.MODEL.ARCH = STID +CFG.MODEL.PARAM = { + "num_nodes": 170, + "input_len": 12, + "input_dim": 3, + "embed_dim": 32, + "output_len": 12, + "num_layer": 3, + "if_node": True, + "node_dim": 32, + "if_T_i_D": True, + "if_D_i_W": True, + "temp_dim_tid": 32, + "temp_dim_diw": 32, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1, 2] # traffic flow, time in day +CFG.MODEL.TARGET_FEATURES = [0] # traffic flow + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 80], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 32 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STNorm/STNorm_METR-LA.py b/examples/STNorm/STNorm_METR-LA.py new file mode 100644 index 00000000..b53d122d --- /dev/null +++ b/examples/STNorm/STNorm_METR-LA.py @@ -0,0 +1,112 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import STNorm +from basicts.runners import STNormRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STNorm model configuration" +CFG.RUNNER = STNormRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "METR-LA" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STNorm" +CFG.MODEL.ARCH = STNorm +CFG.MODEL.PARAM = { + "num_nodes" : 207, + "tnorm_bool": True, + "snorm_bool": True, + "in_dim" : 2, + "out_dim" : 12, + "channels" : 32, + "kernel_size": 2, + "blocks" : 4, + "layers" : 2, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 100 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STNorm/STNorm_PEMS-BAY.py b/examples/STNorm/STNorm_PEMS-BAY.py new file mode 100644 index 00000000..2357f84c --- /dev/null +++ b/examples/STNorm/STNorm_PEMS-BAY.py @@ -0,0 +1,112 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import STNorm +from basicts.runners import STNormRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STNorm model configuration" +CFG.RUNNER = STNormRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS-BAY" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STNorm" +CFG.MODEL.ARCH = STNorm +CFG.MODEL.PARAM = { + "num_nodes" : 325, + "tnorm_bool": True, + "snorm_bool": True, + "in_dim" : 2, + "out_dim" : 12, + "channels" : 32, + "kernel_size": 2, + "blocks" : 4, + "layers" : 2, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 100 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STNorm/STNorm_PEMS03.py b/examples/STNorm/STNorm_PEMS03.py new file mode 100644 index 00000000..6321d01c --- /dev/null +++ b/examples/STNorm/STNorm_PEMS03.py @@ -0,0 +1,112 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import STNorm +from basicts.runners import STNormRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STNorm model configuration" +CFG.RUNNER = STNormRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS03" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STNorm" +CFG.MODEL.ARCH = STNorm +CFG.MODEL.PARAM = { + "num_nodes" : 358, + "tnorm_bool": True, + "snorm_bool": True, + "in_dim" : 2, + "out_dim" : 12, + "channels" : 32, + "kernel_size": 2, + "blocks" : 4, + "layers" : 2, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 100 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STNorm/STNorm_PEMS04.py b/examples/STNorm/STNorm_PEMS04.py new file mode 100644 index 00000000..f166fce0 --- /dev/null +++ b/examples/STNorm/STNorm_PEMS04.py @@ -0,0 +1,112 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import STNorm +from basicts.runners import STNormRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STNorm model configuration" +CFG.RUNNER = STNormRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS04" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STNorm" +CFG.MODEL.ARCH = STNorm +CFG.MODEL.PARAM = { + "num_nodes" : 307, + "tnorm_bool": True, + "snorm_bool": True, + "in_dim" : 2, + "out_dim" : 12, + "channels" : 32, + "kernel_size": 2, + "blocks" : 4, + "layers" : 2, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 100 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STNorm/STNorm_PEMS07.py b/examples/STNorm/STNorm_PEMS07.py new file mode 100644 index 00000000..b66bc040 --- /dev/null +++ b/examples/STNorm/STNorm_PEMS07.py @@ -0,0 +1,112 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import STNorm +from basicts.runners import STNormRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STNorm model configuration" +CFG.RUNNER = STNormRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS07" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STNorm" +CFG.MODEL.ARCH = STNorm +CFG.MODEL.PARAM = { + "num_nodes" : 883, + "tnorm_bool": True, + "snorm_bool": True, + "in_dim" : 2, + "out_dim" : 12, + "channels" : 32, + "kernel_size": 2, + "blocks" : 4, + "layers" : 2, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 100 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/STNorm/STNorm_PEMS08.py b/examples/STNorm/STNorm_PEMS08.py new file mode 100644 index 00000000..d3d17d1f --- /dev/null +++ b/examples/STNorm/STNorm_PEMS08.py @@ -0,0 +1,112 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import STNorm +from basicts.runners import STNormRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "STNorm model configuration" +CFG.RUNNER = STNormRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS08" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "STNorm" +CFG.MODEL.ARCH = STNorm +CFG.MODEL.PARAM = { + "num_nodes" : 170, + "tnorm_bool": True, + "snorm_bool": True, + "in_dim" : 2, + "out_dim" : 12, + "channels" : 32, + "kernel_size": 2, + "blocks" : 4, + "layers" : 2, +} +CFG.MODEL.FROWARD_FEATURES = [0, 1] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM = { + "lr": 0.002, + "weight_decay": 0.0001, +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.CLIP_GRAD_PARAM = { + "max_norm": 5.0 +} +CFG.TRAIN.NUM_EPOCHS = 100 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/StemGNN/StemGNN_METR-LA.py b/examples/StemGNN/StemGNN_METR-LA.py new file mode 100644 index 00000000..8a789a42 --- /dev/null +++ b/examples/StemGNN/StemGNN_METR-LA.py @@ -0,0 +1,107 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import StemGNN +from basicts.runners import StemGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + +"""Different from the official code, we use Adam as the optimizer and MAE as the loss function since they bring better performance.""" + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "StemGNN model configuration" +CFG.RUNNER = StemGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "METR-LA" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "StemGNN" +CFG.MODEL.ARCH = StemGNN +CFG.MODEL.PARAM = { + "units": 207, + "stack_cnt": 2, + "time_step": 12, + "multi_layer": 5, + "horizon": 12, + "dropout_rate": 0.5, + "leaky_rate": 0.2 +} +CFG.MODEL.FROWARD_FEATURES = [0] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM= { + "lr":0.0004 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/StemGNN/StemGNN_PEMS-BAY.py b/examples/StemGNN/StemGNN_PEMS-BAY.py new file mode 100644 index 00000000..a49f2252 --- /dev/null +++ b/examples/StemGNN/StemGNN_PEMS-BAY.py @@ -0,0 +1,107 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import StemGNN +from basicts.runners import StemGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + +"""Different from the official code, we use Adam as the optimizer and MAE as the loss function since they bring better performance.""" + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "StemGNN model configuration" +CFG.RUNNER = StemGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS-BAY" +CFG.DATASET_TYPE = "Traffic speed" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "StemGNN" +CFG.MODEL.ARCH = StemGNN +CFG.MODEL.PARAM = { + "units": 325, + "stack_cnt": 2, + "time_step": 12, + "multi_layer": 5, + "horizon": 12, + "dropout_rate": 0.5, + "leaky_rate": 0.2 +} +CFG.MODEL.FROWARD_FEATURES = [0] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM= { + "lr":0.0004 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/StemGNN/StemGNN_PEMS03.py b/examples/StemGNN/StemGNN_PEMS03.py new file mode 100644 index 00000000..b52d7f09 --- /dev/null +++ b/examples/StemGNN/StemGNN_PEMS03.py @@ -0,0 +1,107 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import StemGNN +from basicts.runners import StemGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + +"""Different from the official code, we use Adam as the optimizer and MAE as the loss function since they bring better performance.""" + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "StemGNN model configuration" +CFG.RUNNER = StemGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS03" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "StemGNN" +CFG.MODEL.ARCH = StemGNN +CFG.MODEL.PARAM = { + "units": 358, + "stack_cnt": 2, + "time_step": 12, + "multi_layer": 5, + "horizon": 12, + "dropout_rate": 0.5, + "leaky_rate": 0.2 +} +CFG.MODEL.FROWARD_FEATURES = [0] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM= { + "lr":0.002 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 100], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/StemGNN/StemGNN_PEMS04.py b/examples/StemGNN/StemGNN_PEMS04.py new file mode 100644 index 00000000..07f81e94 --- /dev/null +++ b/examples/StemGNN/StemGNN_PEMS04.py @@ -0,0 +1,107 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import StemGNN +from basicts.runners import StemGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + +"""Different from the official code, we use MAE as the loss function since they bring better performance.""" + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "StemGNN model configuration" +CFG.RUNNER = StemGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS04" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "StemGNN" +CFG.MODEL.ARCH = StemGNN +CFG.MODEL.PARAM = { + "units": 307, + "stack_cnt": 2, + "time_step": 12, + "multi_layer": 5, + "horizon": 12, + "dropout_rate": 0.5, + "leaky_rate": 0.2 +} +CFG.MODEL.FROWARD_FEATURES = [0] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "RMSprop" +CFG.TRAIN.OPTIM.PARAM= { + "lr":0.002 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 100], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/StemGNN/StemGNN_PEMS07.py b/examples/StemGNN/StemGNN_PEMS07.py new file mode 100644 index 00000000..2e64559c --- /dev/null +++ b/examples/StemGNN/StemGNN_PEMS07.py @@ -0,0 +1,107 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import StemGNN +from basicts.runners import StemGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + +"""Different from the official code, we use Adam as the optimizer and MAE as the loss function since they bring better performance.""" + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "StemGNN model configuration" +CFG.RUNNER = StemGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS07" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "StemGNN" +CFG.MODEL.ARCH = StemGNN +CFG.MODEL.PARAM = { + "units": 883, + "stack_cnt": 2, + "time_step": 12, + "multi_layer": 5, + "horizon": 12, + "dropout_rate": 0.5, + "leaky_rate": 0.2 +} +CFG.MODEL.FROWARD_FEATURES = [0] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM= { + "lr":0.002 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 100], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/StemGNN/StemGNN_PEMS08.py b/examples/StemGNN/StemGNN_PEMS08.py new file mode 100644 index 00000000..0d2ebd02 --- /dev/null +++ b/examples/StemGNN/StemGNN_PEMS08.py @@ -0,0 +1,107 @@ +import os +import sys + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../..")) +from easydict import EasyDict +from basicts.archs import StemGNN +from basicts.runners import StemGNNRunner +from basicts.data import TimeSeriesForecastingDataset +from basicts.losses import masked_mae + +"""Different from the official code, we use Adam as the optimizer and MAE as the loss function since they bring better performance.""" + +CFG = EasyDict() + +# ================= general ================= # +CFG.DESCRIPTION = "StemGNN model configuration" +CFG.RUNNER = StemGNNRunner +CFG.DATASET_CLS = TimeSeriesForecastingDataset +CFG.DATASET_NAME = "PEMS08" +CFG.DATASET_TYPE = "Traffic flow" +CFG.DATASET_INPUT_LEN = 12 +CFG.DATASET_OUTPUT_LEN = 12 +CFG.GPU_NUM = 1 + +# ================= environment ================= # +CFG.ENV = EasyDict() +CFG.ENV.SEED = 1 +CFG.ENV.CUDNN = EasyDict() +CFG.ENV.CUDNN.ENABLED = True + +# ================= model ================= # +CFG.MODEL = EasyDict() +CFG.MODEL.NAME = "StemGNN" +CFG.MODEL.ARCH = StemGNN +CFG.MODEL.PARAM = { + "units": 170, + "stack_cnt": 2, + "time_step": 12, + "multi_layer": 5, + "horizon": 12, + "dropout_rate": 0.5, + "leaky_rate": 0.2 +} +CFG.MODEL.FROWARD_FEATURES = [0] +CFG.MODEL.TARGET_FEATURES = [0] + +# ================= optim ================= # +CFG.TRAIN = EasyDict() +CFG.TRAIN.LOSS = masked_mae +CFG.TRAIN.OPTIM = EasyDict() +CFG.TRAIN.OPTIM.TYPE = "Adam" +CFG.TRAIN.OPTIM.PARAM= { + "lr":0.002 +} +CFG.TRAIN.LR_SCHEDULER = EasyDict() +CFG.TRAIN.LR_SCHEDULER.TYPE = "MultiStepLR" +CFG.TRAIN.LR_SCHEDULER.PARAM = { + "milestones": [1, 50, 100], + "gamma": 0.5 +} + +# ================= train ================= # +CFG.TRAIN.NUM_EPOCHS = 200 +CFG.TRAIN.CKPT_SAVE_DIR = os.path.join( + "checkpoints", + "_".join([CFG.MODEL.NAME, str(CFG.TRAIN.NUM_EPOCHS)]) +) +# train data +CFG.TRAIN.DATA = EasyDict() +CFG.TRAIN.NULL_VAL = 0.0 +# read data +CFG.TRAIN.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TRAIN.DATA.BATCH_SIZE = 64 +CFG.TRAIN.DATA.PREFETCH = False +CFG.TRAIN.DATA.SHUFFLE = True +CFG.TRAIN.DATA.NUM_WORKERS = 2 +CFG.TRAIN.DATA.PIN_MEMORY = False + +# ================= validate ================= # +CFG.VAL = EasyDict() +CFG.VAL.INTERVAL = 1 +# validating data +CFG.VAL.DATA = EasyDict() +# read data +CFG.VAL.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.VAL.DATA.BATCH_SIZE = 64 +CFG.VAL.DATA.PREFETCH = False +CFG.VAL.DATA.SHUFFLE = False +CFG.VAL.DATA.NUM_WORKERS = 2 +CFG.VAL.DATA.PIN_MEMORY = False + +# ================= test ================= # +CFG.TEST = EasyDict() +CFG.TEST.INTERVAL = 1 +# test data +CFG.TEST.DATA = EasyDict() +# read data +CFG.TEST.DATA.DIR = "datasets/" + CFG.DATASET_NAME +# dataloader args, optional +CFG.TEST.DATA.BATCH_SIZE = 64 +CFG.TEST.DATA.PREFETCH = False +CFG.TEST.DATA.SHUFFLE = False +CFG.TEST.DATA.NUM_WORKERS = 2 +CFG.TEST.DATA.PIN_MEMORY = False diff --git a/examples/run.py b/examples/run.py new file mode 100644 index 00000000..188395a8 --- /dev/null +++ b/examples/run.py @@ -0,0 +1,23 @@ +import os +import sys +from argparse import ArgumentParser + +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../..")) +from basicts import launch_training + +def parse_args(): + parser = ArgumentParser(description="Run time series forecasting model in BasicTS framework!") + # parser.add_argument("-c", "--cfg", default="examples/DGCRN/DGCRN_METR-LA.py", help="training config") + # parser.add_argument("-c", "--cfg", default="examples/STID/STID_METR-LA.py", help="training config") + # parser.add_argument("-c", "--cfg", default="examples/DCRNN/DCRNN_METR-LA.py", help="training config") + # parser.add_argument("-c", "--cfg", default="examples/GTS/GTS_PEMS03.py", help="training config") + # parser.add_argument("-c", "--cfg", default="examples/STID/STID_PEMS-BAY.py", help="training config") + parser.add_argument("-c", "--cfg", default="examples/Linear/NLinear_PEMS08.py", help="training config") + parser.add_argument("--gpus", default="0", help="visible gpus") + return parser.parse_args() + +if __name__ == "__main__": + args = parse_args() + + launch_training(args.cfg, args.gpus) diff --git a/results/result.png b/results/result.png deleted file mode 100644 index 00636e11a44baeb023fab5f911b4ac8521126414..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 495049 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

ZNkcaO;PWkcuqXq;^%9>CZ2IPL` zq3i5=6T^}kQt4;uOTGr_!{?p**wQC{{WJR@eb9sdDGuVWvo-MmtVK1J3$Ppl;ju<& z$j10wefZ8=Hvg5NRZR8qS*Gg=J;GwPV)qSX za(iUM{2SPszHcFfY9F>b1*{cCWAN|(27pVZIFWj~`LfpMMJiK2e|`3K7myu8@UsFe zHlak4J2Jhj+*eSN6;D(^;4j^zx;ssA%0ism_>zgtQy}n%BTUE8%eiuF%^A8%eFqds zDW>6+xGTv|h(BNi+K0B!jtu0~NG3hLO)P*Z^;@7R_7vTA`Mvqb;s}6K%v`B2ROws9 zmPXsJncqD(6HN~wIICp-Rzu5ruKrM~qQwMi;zI(X-LndP(i=-=Wxc{1YfO4KuwfE^ ze=;d+Lr&8i7{@tKnInY+&)s}V$`W(Dw~sfvya8^vJ2$iqCyI1NdHn^u*hKglfVxt% z_49NkbgZkmhWNuhE`IpW!i7V@_JViR{6cCM;)LM&{m$_K3b3WJSmb?6oAvkJJ&ql< z;qzhBD_}5DP}SyQVp9zUsgI#b1sxRMc-vk1z|Y(2! z^Xd-^up1vP*A&gN()sAZ8P?49hY-1X0(lIC>p`27tyeZhTMbWf=CX-ylf8K;@Utx` zv`FcG12(;<#^TAORo3U!vA9~W3QvdD)RU6wvP!TL=21E;+u9tE@8W43YqP#tQ7gxZ zRb)O)x<0ml8E zzry@_1f;xO&dc2Yy%*ftZA zx`m*rku__w@jk*@f^)9Lzjjn+edAE`o7sX~3vTuBbE4}{C?}1gM0ZTa0Gag(tm2~G zuxX=0YH3`(gObI)4sO+XgXs38u6~h8 z^m~x6=b*(0PBlo9xXxJvrD&QN&7#g-ao!u(QJvEgjHgSkj#)|N9SHxOB z_HOuby;c+P0Q20AZq@8+y1*^M7>7ID$ztoBFUv8oRq(s_DARvlKe4u_*H3KsUdpcs zkZ#-Wtk<9}xy5z2Z(TH1-bl*)vXd#2IA@sPIjOsF?W`3CMb3WC(4PEt9z)L z^v%1WZG)GC_&2=s9meb-@(pLjvb^+PI3C|e0sIrkWawBFd+hsMR znGD%<9h3UQPLyKfuKb41Z57ljpQY6>*k5T+msKGtFm_r$V_OV{6x8Q7wHr$9CuDc1 zHvuD@m#Tjm3liVa?wp;ISopEqS;x@B6MT8t82 zW4q8uey4WJMZic2yUN3$l|FD`=p*lUquOu`iFG0$sBgfd?mS2_pPJ&(;>Rz>)sh`T zc7{cxx#yYpjgSSfCi>J@jc+wmhQG;ma(cxEavfI7UFvagJV^P`yCHCC0!KFtr+4Fd zlp(MCPCM*^Pw>SVXsiKkSho&+zt^y#>(U72RiGtbN%a)bp6JcM`B`yt)%?1L{vb%R zp>AWYqDZxiZ+=FSWBs_y`t%vO*W+~c96(3BVtK=?5F}%Y)5>xwy7s8E&@&TL@k1v1 z17DH4c9>#fY948<>iO>FXI2-iWjHFMo?DEf_I+;(F3YvltClWvYeI8XDsq2Qg)^wCL%b%>Tx+)#Yn!kual{wpJEpKZtH5v-dQjubj^lHuQb%b8#Q|lFVqiKW~3no%0WDPs%j;!2QsNfC01(m$Bk#OlTZ(vH7uS`0azY+U6*j_Wu zC8FXLS@_w!K1B)l8<==vKwZHRDtmP({Q!IQQ^FMlAdP#fju;Y)ydgv*DQWJ}xG8Uk zi|w%RwT4eWq|x9`hqQH^=)${7?D-vzr(Zphf@PX&F8VLDs58*VrUT270eZm}*}Z3^ zjk1w-2U;VP!Tx$(wU%H5{eF*KRLiwp+dcobb9YPpnJ?>`zC_ujq~`(RrmT@eEjv(W zNpKyK_{Fj1q)Pl+sZ5pp-{NepR=3~DvdipkW2!26_muTVn=y(Vz(c`ANhhu)45Uxm zSk!-*I4j)$VV_PLDB>ndEhoRF@;ZrzIOG0{0|(!qz!g*wswga_&5|nxe>WFJqE!Fk z`wFvYd-GeF=!_6su@dgp0IuoG8*?n2m72@lJTT!Ra+txC|0W1Ti8(XQ9(akOL`nm4G? z!mV3``f=1QA2r-<7CU-Ih)&Ocsibp|N*dfxq~wBs*hwG3&!E!!1r({+OsJQoZ!f#d zQkczmFE~*C$m5~JI|vt#P2Xt|I3r#G^3^Me;>kYqEi5pqef_YOhrq%hWwjPHTf*-N=Mzr)IA}bXaM>#n%;HFiY0==e-B>^3kd? zda^4VF+qRD<`anl15Ki-v5dV*;4CIpUlFx#-jF^F)E#c_qoV2{kLXKN<{5R5 z)mi-q0U|dO*-7NiQX>4cj<4W$Y;Mf#w%ZFZ;@nNH{9jCj`ya~$mwYWlMbig%4g+oL zRrM)GlEg#DKXoZasV?2%dZ%*B9MpZU_O?vZ?gZd6_LGHW0CY&^`3QS2YrEmkq}69q z9V=!c&#@(pjWfYt84NxbC7(}K8(MLXhB|D8`@puBL+UO0e#nOp*^>*I|9sE4piFfCSp^x1 zQ3XQ5OF{BkApzU$Ts-3OcQ`arK?{x5aQKd994r20tzhUPpB53(vMF`oy0yl*dfxJD zL83ZQ%4cyeU+kA^krAaNS8=IXIc|1+!ttMi}bifSK7 zT3RpYwZ^A!V}_B-h;w-GV8mD@zyAmqhY?C+<<|`L@AeES=BRqX!OM<&?@ovNn@L=k zg&W+Xk{{pOX^>s6bs#4-M|viHtRiIf&NPB4`xx}8JFeTWf2f>F-Zw-`CE#Xhf4z>u z*VnqzO=(?^iu0QJkm!O?`+1adur+@C1^~hn(n|pN27pf&+Np_&lJutx>>Y~F^1OyZ zx8w^#ACK2ED~}B->v^%ke{4FbLmSp5$lX{qo2OZWz?sjLpQi}e%hK7Y$Mmk>XnNMp7Uu4AKO+m(K$n`HH| z)9foS4@GDL9>{&Rou?(J|Nhrr%{UWh!qeZ88tN`~gRyZJ1#o$~%>#eKsH^w&c4<2E zL{uyLA7wF^x}RzjnjotiIQqun$u&H%77i#sZqj~^xEbs<%QwsLT!BqcCy470ODO>C zGCnr#fd=uPV{U_svOAm)QtFW#ls>UKP?{ai`EL4Lz0na!dnh$TT{Lxh>s=AUsG%(o z(Ux|_EUOpUcy9k{wXw>8YQbP%sS9V!1!MT&;YM3+#9sGA_ANk9`)jT(%3Z*XH`lru zKc&dlm+1CCiJLVa zpeW5=*_qMYMZf-lV1TdNAA2^|?sj6zm*7B%z8?wLr^q#$HHdL>=Y9R`D|Sphv1K{+ zAHvRZaK<8h*d@uW|HI+fVJbwH_E*WDt`z)RsR|mZb0Cclybf7Ve$;J+EYd45ods&A zx*!N#O#c0^r9{67YlP4ei*)yu0NJu#^)Ds=R?Px&Zmn{Sw&#jFM7b0dSLJ9MSHMX&4G}WruWb2) z#YaIPNyeQ|c6i3E(Lv>`AVThTysNrnS=tig4#-P`vgFuB{sPm=sOYwry{B;00g$Hb zg?}zyb5oEYr>Hwg0cB6Mlv>Aw=|4U{>uN55JkZC&{R>NOJv|4Un;tI^19Dg86k?7( z06d}$@$y#?_PU+|;0*|afG;ET17KLYKW&EnWHHaU1<~QkA1Jg+ih43X1hLks)oQ9m zb5ZPS#HU+s0J3c3Y@g1}?#U5i8(=oL<1C7M9dB=A5`HyAfre<|#XXnKfMcd6o##N- zC}c!se*jC36YBLsa;~s>!zWwKjGFD3T)zBf!#HnxZxcAZ+Ybo?oMUIynCdu@mr>R7 z7uE5+UyXckd*oi{JFOAnZV0!TNs!Z@d`3u#26!A5%~CW1j)fu2SMu>C$R9cwQj_3* zpz1RqPvP#KA9MB&ntfw41ycFWL3?%C+(Zk|{m}U=0g^O?d}J}HiN9}XBY$;rjVA#m z$-9d^?*IzaQ%v4$BT=N8&r7ivmu|s-Z~{qt-I!HOz_|W;Auq$*LmJ3XXS?mx{l%dE z#u$7*^?R<3Juh`Zk;?oIG!}-n0r<|RA(_X3g+*CeAmq4qyS_F-K8Kmh#SzTyVOIdj z#v{Jj;7JbZ;#j`de@^)D;5Qm_0w?MwWy6fe#;J8~Qu{Rq_21~vr-C6CEk2M1CvgEv z%RDy~AlaPJ%mUto*8CDo6xstEQF_-9*eXZ2I{d#Qd>HZkv|^TXt)CnjdY_sD6>J{` zMsy6R^S9l(EGp~k;*M;r6Tl-CA=b>9e#CDMry0zSG)E%ykkVkzl(5$fI8-fOSpC7) z_cY*h;S(P>gPrns&^$m&L-?0hI;1v#?|=qYqxFkl^g_~OcDibJJ(nk|`{+XitflDy zrDdMs3R=1U74P=9W|NS^iyCeyUuopRw;dy^CE)a&>~j48Wd>+sV8(?b>bdGp^j*f= z9;nH$P+^<{ga2W;L#-^(N#<$|QC=ru2p2A?VSd0s&4zhP?~q3nl>_ouj?NE)*|0U? zPsP9R)X5~ue6K%Sr*$wte$QMUhUo`rMUB#PFuXD)zyS_U5aAm~eyRCHHx3pjwbh_@gC>T?4K9uwT052pVUdICtn3^1_C3-d8~pSO+=0@JnQKM*?!lXF-heI)RYDR9=F-rz&@@xlQaeI zL{9^I^iTUBAy?4%B2xgH*_Q9#ANxC6^6^7lZG&ZQEzJ|xlZ6=e4~PH*u&%YGHkxz=d$byr06;O z83Bk>X^=dv4p4^aH^ZU1Q5)CFlG54tw_e@Y_*joCX&RY5A+6_$u}GS*BuE4mDzZ;$ z<$L@**e}A4D2v1TG&_K#@a0)XfPm5bgHFa%jSR z2z~_xLvDL*f1o`VXtFfs{S5%IM6Sz#l7mu$ar&Ot1|>r5A#gps((%Z>e&cKigG4p{ z@yrP4+54&sw-<};!6mr1$ah_WCE?G&EG|eivMC%P zh5RtEZFx$gZ&0alT}Z?--Xk?+DC=qe*;!Ly0c$fz+`_fEqlc2+&m=N z=NZ=J&3oexqo&hf)5%tw_7}932kTB@e)TZi`bj{d!ua&=yo2Xw&VC^yy}??0sn+vo z3PNnn=KB!#S}~R1fqRUe&U{x%FUKG%|9uU}=Nn)!_3HvbRTIFC`PtDFp1RB(pT7FC z{9&rqEo=IF$sm#P-82D^>E-X&_q($4X>FYujWoR zR3^!sS!qWoGJO(6w%snOaR&2KI?TdqM!XUPLlwgtYA(E0jtavm6colOxI44;cAY-* za?UyaGw-$I-fF|jmLUnQE}T zym@zaOB!n{#TFh)>b&&l(@Nlt-8A>{BquoPJLzlk$2GECa6S^hcg;?q@Ypaj1FFeD z$-Etm5npc;O7DBW&;Ww}YCySwdP(O4aqOnVonMq(q^5v*2KkskmsKm1JU!cHKU;Y` zSbZsDHPGeuN6gkq+fCdz-~xKqCP|S)%0VfZ#~0zlQR;+P)G? zH9Gv59CeXsdzfi}6fiv}c-xQxyqt@H(V9mf-MQA}X#Vko&eCxQz@u(7zpMFG<2)0v z+2OY3{LeyFScI<(j&W3D~uV$h%Re)1qM0664K%$3^ka9w62;exH!f|H}EQl z1(Y{54q0_Jn@aVextmL9B+$qjBlbtGK)aD{h}<#riR z_UL*U+U9<~eM+ox2rX?cU4G5tZyaZ<0dJ*GaR^_N16Q{;fGDluKHM%RV>0AIUUhcX- zqEE^N(;Qzc(>y1#o!B*OBGOuu0q8NU)|El0RNuMP2NgFq$aFMpLxbm%k{s`K5{b~B zc2yn%!-l|np~WzLQ-_K-+!juxeQ;CMBd%EU+bq*eFq~BVg${@up}z(0{7TbZcY;_} z6fnk?)g^uQAt$M&i@^y9Z5Tfhne>DYEhMppzN{BtZ_%TV*nBLcS6uols08tF?Tf!; z7`kXki#rv@PjJV%-H?NLp!S@HNkiXaq$H|46jWSrBqQuS;2L(m$$6;>>v%lQE} z7q1CH6vm!;8bxS-aSk}B7ai`#B#f`-j3$_ajM2bJEl zzxnIW6Zl6TmP7<|Ay*vP`Ku<>QZ%p3&N603YjW$Z^hf)oqm_oqE>xxmXLomUN72sC z-Qa2mxF9CcApJ?O^&6V+b(J5Ftvle3O9A6`b^Ff5U8ycdie+vMMRT*bGm7J^oWESa z?OW^@TKc%Cg`;9O8RxF2mGCyjF8O$oc)Xx)`S9qcy-}rxj5d_k3ZDP1?cg}Z$+;;V z*||`c=Vk5E8J<1$!VIn%qL#|6S)!rvk3Jzvo`2(T9yc`zjkK>4Q@M#gOvC6r zao7`2w)(60jUbea6Eh{rBAMM8-fpa^@@~1#cS%dFrd^fw(rpKqHNd-k_Q~FNy(SOh^;(hHyreVKvKTZK#ai+e&mfMIuu9sYlSM$&dB%vH(ELi7 zI(+n3`vJWT;W%Czxq0^#p9r%doR{+ffs|e66D_@%{ae5|V@UAZmEkFNdo_MS z7SyBuDP~r*Q7fRfU-X_5dC6+d$Lt+)z*d(2_tH?u=Cq~-)S$L{H~p4qe3$NcN+q>d zIdnhMYm(-!Pv4Vr=$cX1QJBp%iBV=LW#b#nXX`C@O`g}zo!N5}<0jIZCpkH(D^L%n z^T>bB2NxfE^M-7DC|$4Af>Tk+`&CEecdH%m&hFl&3vlNr>ZFIAl;qE{MljBFWYgo9 zr|n0iqUMWaJ>{wXe$Tep^Co$Fx|}Z_f3hv?VHn$>fVz;JIo?n15b?jgjY@piFNw>^ zCN{@=@CNDhz}@h{#PrBTa?l)zE+Ks%8BNyTeFJaT34fV?;o?~?oc$d&!!WFh7z8;K@Jx; zV*?S8#(v)tx1@nArv*%-)g}F*MKsKbDkOU=B>`GJi_DWkpVpZRd-N#b!4%S3zMVSM zQ>%)f{)?|zo`(WlHUBhK(dx87h5u2P+EDRoHs z%plo#%tG37Bqc-bF0bV-+ICK0hdyDcEk8pHOTr}a#&+f=BRU{ zh{3fSQOHBs$npiX99`BJqVP-*_}FdCB>nOI12L7smtLGcfH)fdUEArlNod5Z_O|^% zWT|$(9d`QOPV^NQ^|Q0D`lh|@?;>D7zyfq9-N8r@$^;AjoMUU%=QI^1Pf59;Hnt5B zTp9b<#HQQPSXZ-n4LRANxayb-wv^qFS0<3Xc=iupQdOfaz+)4IPaWPJv@1tL_{1$N_-SeXI%<{^Ps(kH@}HV5qq? z=&hWHP~Kez=e1FQJ-kF#fd~QJL$;B9?(6}8Ot-;&;_$2$==|Bdftf4|{Na!Gv#Dn# z)6#2UB%?Xr<9jEvpcfzm%>BJz13)nAqd%AKjg!i%IrlN}g~LkPFLOa(LbL^!+XsO8 z3ITC@0h8q>yZYtPq$%wx8qKme7otz98)11cG@E?5s-)t1~c0MMuR{nRq&a z2~LM^7Tr5DfquFEId9DR%-|}ChdZ02%+9gw;Vsa-JVVJY`H!fdg~5QL1U>tLWLG#KzdxZZGzQELJG9P zF9OHJ3LSBG>?l5HuOwKE<41g|`vAUMqK3-(l= zH?P1cdM+G?)<)h1gl0)E)>_B~07Ln#U#!=+H39Iti%8J91`JkxQWOy@<8u_b0{|D- za3e`0T97<;2*4Y=@~0&rF==8e`z9Xz`~r+Qo!Vc{&1n*1(|`TUf%DH7kfMzMGynXX z=A$>&jGzTi4`zoEaGxBak*KfkzX6)f1_1rJX#~z(A;)tqD3U04C3))$1pp))M1Sm= z1VN$Y$Z<8i4M<)Nkb6dZaPlvfOBpiMczm6S1TSNzW_W>JO7yH2i<`i9eKQxRt7p%{ zkMh;ydK`>TOe2tJ;1jMGD=A}4)JzW;{vK91|FDq}V!O0shd(&|P3 zJZaG{D;A#pWWTTD0EU`UY^<5P3+H&DLqB~ILcyZ_d1Y?5{`y?=sAF5T)?u0%uLraJ zV!_!LXxf{RbsuCJFKLN^*`?H0aG96*(6Ri9@F+njD~VFr_&dVq86HE}Z{p{`DwW6C z_XEx8Wn@OX?|ru)2cMhokQjr$$x6U^?u)dt;Yj=XU@S(tP`fp1yL7R+2YM z!R*KEWH-qNaOW^S*wwdUlTA8E*-nE@wU>YZ*vb;mJUG1r@YBvO9hGCG4{I*WdG`KZ zA+E0NBp~0{hoRtX-oip8xyBWyzZ-(X>v31F(Jy0v>*Jl-a8mVW1~rf?O?X^Y-zSIZ z_d5mrNd}QQK}vy1BhX);BmbZTdzf}1^5ni>H$6M0KHv`6Y~1RHSbcl(#9rMJt6`>C z=zWY?{R>d>FM>`cN)nN8+W>U?NVPEKaRD6o$E0lgKtIUVNfsKW+i>3y7qCLbI(x6u zV8=d5(SUHTL#~ZU13^1{@iU4?1KJ9BwtyP!o(W9BN?cFtpxL5-iQkFz&4Eu+U+&qo zSjP!VmLt^zL#FyS4Kn15E6t%s^R|8G9HV;mmNO)XN|wi|6Kd4TSAiT8R4coAyFnkd zE=eEBx*^*(3{BxTKx=F0QMSU6<6>WhV<6n|;*pH-_j|rzzbz=sxPGMRX!YV~1>CSn9+(FX6ERV$)(a?A@{42!raB}K z?qSfns5oTZrY?Q&9BnYB4YLB|k9nC3Lmp<@niDw*_WV9!$M%Qm^j+_J{_4)D3OIjj-oX?>E_`(Kmx*AKgZ4M=4`8b5Y8^@=h2!!PstM^!X#d*O5lffgTe+;Wet>Suh%Uw*5}1=S zAQxoIFaNer?P)Yw;8YhjMj<2#9rA$dKFGF9R=V;6M2e(&J0K{|W~b&jLpoiUy{_I@ zi>uxoal0B|#S(FG-TKrXzh=76s~-O65y0`Vd}{OK{SLx;t_6$Um=q z`lWRK>+n_<`y!m&_+zOdJA~+G{Vrgc>)3JYT#DSkAc>(6^7vXGzQ)M{r(9C27o~E$ zco>L{iJIs`-DkA#rMqE)BowopucW=hlq)j5Htjo<$4Rho>r<+11 zK@K+n3R-gdH{bx~-0J$ttv+p1opPLv`6dZZrCOMIvf)xYE<3`z*bxU0JJ>~MIME-W z`M-yFs9qco97x2z(aoG5emj+rYBmZusqP>zauqvBN;hWkHQ-R5B}uKmz5lSr$q3bA zPoJ68?~L~Q#iG0W9y4_TXNa6DydaWJE$PYLX06-H7y|l1BX(8FT<%ZS$;wKeg)XV? znqTgwU^G+fPM=MvVUdng+ug-3E4r-Gm4#>aj>F^px4zN_Tac5!Cicr064RdX@XD>;aciuz{HK8lYtycgZM9h--&cw zTP;9V;v|Q9*{kJteV5&pfCcL%H=|n<*6}Y6CxcFl(!@Pd^x{R8PK%jZ#Firk4B_ZS zk3=4x>{6koC6qe2XUX^M^RO)?F~*58SmXx<(v57aWu8#udJl2R4O zW`G?tgWB6nU0wA)V;I)Ler4^SQ5T;86g#uaTCm7be>MrX3ChFf$at9SE)rZangO@^ zRtBE>9$GU0`q?qTgTF|ro_EX8nuF%9jS^K!4E8fl_PTX?O>U7fJM#us)~dmZ{s*?o zgu^2QJzWuqq5;*M2ZrB6JGh^zy(u-s>}Z@Ykc0 z{hkOHT&Xy@8suqAJ?L+I2RU*guC08I`KirI^D#A3vH_X+i}}^;%X;jmvbzr%oA7t( z+&>Vla^P@XzalRV_Yj{}7g{@1c@RV=eu*3BhavQMkm6>t&Dsprt%}L|N8AI{wa*VF z*_)l|4bxT$+zyoNyhgghJ^X+XyiG7X$P2rL>%Kf5IdO)pCX*T?tG4AmU_>%dNo*?) ztS}rTuIhdD=6ZBr7UQC>KAv9Y|X5oGTB^zNIhd@UKZ>gcs+WjknndQ@7~ zI8=50F@`AlVU^QNnCW!KX412qr1#KJ`r{t@cz|fhUCE>NIi)0F?-@Sem~`v50~!jw zvQcN_oQE#Mh{o3oksmhd05Na(ZQ3PKdU>w#QnHc}a44^$WH1-#WD_k-LH08}EtsD2b<3 z`_&;qTEAIvioi6H?cYguD~n^`G^X;m%k-V+V~V8VBSWp;SM|s>;jA$*zTKi^g>SJI zd8xgg{_9dJKQ4Kxx|kh}l29%c^3V!wmFXknI*LjU8BH-AGsO3unmXg(q9*CR&J)MV zx}3aG;Jhq)b%bTbd?@TMhYPRBuq?^Z^O{m{C{}nx%esMUV~FzcU9Bg->V~2x=#L9g z_PZeFs{o7{=I9vh?ZRlyUX>@U=Y!r{GESAyZ4;&a-H;3vfnU zF-m1HU7gT>uSrNwo)aF-bT5R$kw(_)y*%A?Sl+UsU8l>h=?L*vqHxCI0J#Qy%gPWU z&q-FF^$lXoJHsnOr}7fw4l84_9wL<@~dc3;SRs z(`!S4$}?hPV32oqBu94BtB}5%b8cg-51W^ou=%WV#AeQ_&_*x)?C9w#7xU%IyOD}t z$n;-U@&h4#>#f{i>Vh0S;XK3n9exfptnr7IpR)q5LgeImxGr6uJjRLQsL?uTu;1dE z*c2hX@z3kMQHf1epPK8v6<@`L^VdSNmKZK8^Ft%d_ka!7)88QceVezQEx{sBis4>a z6+7V+<|EWQBkVE=y)SU2HV`3Kj3qv#`iD-fhrv}|nG$;~c`OaosT;uzXhn8M%Z zjwRxvKI#bE_|>mL|Bl?^?_DN_k;T8W0B+3|nF&iT3{yR5tc3A_h)Mo)ud;xRQtYI@-Y-M3BwOgVr>YZZI?oirmEj-LChYRj{NX$K1V>Q z25E5645iWvmbotxTb0k*Yz2n*2~?VCkoROqq5yofJkwq9;#JJTBrJXJrKH|n#VvFe zn(OsB6s~ZCp584v#Srth>JwFF4vhdzz{`VLEnkvFal>$<)atC_132V&zMnN_-dmAYQg zZL_Pk?GqYdNgNKF@iI$0yUVYyxWdud%Y#iM%ps}o6_(>%sJnCO0h^gil;H=44I6^nx8#+-CL2p+Xf;oMGmr&gU)J&D%!B{)%CV*je4I z*SRM8mzA|03xjIOzwLjU@8>J%EwfcxWq+^JB>kRx-Q~6g88&RXCH_$+c>XK?wj(XL zE%orN^0<%ZMo!y~8I@Fl$IaP_Dvbr?ujYzDc`_%Z?K#HMFrs=FUT)GkK{)ba4mNG^ zDg+D)iumwc_U1f0a69iyCpJp zttbv&5Nonf@v~Fi16BJ++SDORMHp85iF-+Zj_rNuVo@|N=OCvUH-rR;h16ni54#!% z*%G1WJEN6!?dw6-v0W7<8YbrqZWoIU6WF+~OO6Qa+K&O745?_Y%Ne^MY=p8bX*D%=_8nqMpU#X_S5Wi?_Np7`JcTv^kz@pBLLXC zO##Qu0{ExLfdJ!FX0Iq-aAcN3zY zYm+L0Q&fNZr1=!ufPh>FG=15dXFY?*-LHQ)lja0dMDE-jpREZImoLCbG|c}w+{H#l za6DJ+qOFNU8Al`cekH3q(DCMCxst!v+*o~{s(ts}&DXE$cP~7)z6%+}d)s3WlA{s? z)1e02vtF>1?rst$5Dqe;>s)M-M9`P^;Bxkrxn%@1X>Sq@tLQe0 ztmva2FJIIxD?v3{f`{VTpAxa7w`Q$R%80x=t+WA$LK21(YIVB<0NsB>G z>DC;BXzpEw8+4;~ylwkBYFtyr6~l%P?(er)i`3#m3Ubkz`rFcRyF#mrjr10`+VXxU zopHfZK2M~!NMi1z<9^RCsnJ6Vx^8d0Ii7$1Xg{4~S7Iz()*C>Cs{D-Vg9zL8u)pG3 zsQv6}$|>5IvxoUi%TFZ+9Pi@DO!pct7osm@80ksF=L8EH(SGY{N+{*FZ5yY;*Rb|8 zU0<;)?yjs-Sww?99Z)kE5Z%OHHmW1={PVDR{y}#*?#DP>%05h&UuNUJsbz!ou8ihV zA_6(7Ep9*MwoFteHwq>XaK{Z_U@VOyQflXKWn(2=5|xHVI-uLiflK*jGttn_kBN>3 z^9@tTp`rD!p)n#xi9bc96oV;_rU+`Rvy|QnF?IdOXS(+}fE;zkmEc{%#9*+!D&^?R zGSR8)a{K)Vv>xd!Ooev=KL@U-(?uhIZf6(5@kv4NXBEIkdH$1x`&rWXoyj6%L*m4$QzWu ztE|JtS+Bpej*3u?`E?!S2u2AO*YMbZ+l%=+g|PX1eADQUf=^$+H-8U`D$!KdoX(xs zXTfkR4M49sLb<`H<(llc3U__MiWSj>$#)4Rhk0>Fib7oB!c`BuJ7`XI2VXD3CBD#& zHT6!gDqC1~5J$^@H+xS^2{rdDyPDU2_OkLsR+=%-*oSh@kgzm!j$Sw$KA**maTH;s z0^#i~vod+aw8FCqxryCx6+1%|dc3}E%er{ZTOzz%#xp%JnfR7Vu5&Df)9BO^!qI-zc;;Ny%U1`@Q~6 zg8eUYsz!OWlcwy8WUPSqS6bAI@DZ`Nr4NQn8)?nvR1Fe@6_c$Gpcx?*H{2f(LzvBWc2w%IO;3Ng&pC-v;I4LO{Epg=EbD z&+*U>9K^&btLkGU0N^wVLgq6v{{n^lG*U~%-S}PJE(D8}pE+JQ3E5LV2EvM*Y5=PP zU#_UEaoPqA>J;!ep>YS+OdDdM*%!K*O5{d;;dMXSJOHEC_$*!eA7ajNU27S{6Vkz% z6N?poZQg>kq3Qs85dEP!tSo-AK(PnNt8wlh?wz-G%MiF(k+?lNt~98$#5n~KT{A79 z(V{j&>xn$zkczJU&2^EK&Jv{D|1ioW z*G5(!ew!QLsctn!ewY>T< z907Zg$)Q3-4}!nShB8dsR*Csz`6xFkEe#)y*@g1}{ZmW0KSxZ1PEPMDYJc8d1hu>V z*NPy&F>H%RWBlX3G5|xFYYzeg`px2pN)$Q{rthL?Ac9CL09cQiA&Y>fn26!G2YP6| zfbItrhD76spmWIeA7H$qEG#9 z3B(=$It5wPZY4!^U=SoMfncB9MF3##tfwU`#MFZjRP@{c<1FwbIk|x+Lx(`*GTXSH0LS!ZK;|8`j93IM1Nfd}HZ)aSDfTv#mezPh$gzA4yRz1tv)R0Q@r2k^d{Jj21Y8k$vUTLr**RF+QA%Yxv(#;hsPa`D2fH3`%G?6<;S~A~mLDDMy zNss~zLVYwk$e9k?;2}s{6xkDe|FlKE>&tFoEU{czH8QpjE4ltUtL66?`1SAQ_j+%3 ziYM49QwaGZsGZowuyn<0FzbrNHs2{2Q~RkP1IA>4G?8Qnu;@$SJG3V(TvO5XVj#6I z=2_XdIs)nc!{pgga7DEdBfk?j`L|3M3xEiEh!47;^vw)cP`9ECU0eKsjmv3e1S=L< zbR)A7Cv*V5W*b2I2O^N|)?to-(i1FD(0OnOMksG0du~b$|D=a$=B}WQG|(jE3f)7w z0XdmCtlg`JK!rV?BpUX3bmO^Gdx)*HU>S;M zH)rfz&EiM25>*nJI-RkkXj0{$(*qI$MeC6HS=ro$4)Tl!;Q*(#9X>)5XK0+m%AJqt zbrk*Eb;))L8!N8U*C6piwYN@`f3wIsaL1w2haK+4u7y>F5NTjeN~_c0p5T_QdeJBL zFM=*ra;U<9r!r`410wfu*n43bS7>xqsu?*_gq}XROeG6UPe+oS=hK)1jxQbsvJIQ| zMIQ0Sn4|ebDN*k$Rnbkym~5o=Q?Q>?0JmM4iaj(kBwHM5hJr)gS8thqIlX-sS$Hu+ zlGEGx-{vN1FgLNS9Kn%?!fnyvQ{xZ%M;+g)=N$I=zG@U-Ffk+-%%CZK;p8;0Bzp`m0lkNbAAs%36A z54r{|-E7Df{iE|;Y#4+JC(lBEEWyCari^C2gWn_@eX&>tV1DHkOl361=2`hFsT&5K zFZL$E?2tG^rWRqOd+YVNKO;dh|I3VPBnPg{sx|HmXlk)~Yx7d!i<+>~q?@ao(=uilYyrQ7_JojQ3xAf@N2 z8QTda96yVV3Fl7gZ=H7Ge4!2WT3=u^j@dv|-nd9E3Xp56Z!AqIOCG&=%B*jAfwXp- z#?Rt6W2O1*O6T@*M??T%Lx1_n^^@vmc$j|YuWm_Oy9JVxNms^Vg2>(J-4UCf4_PJk zYf^gKOtS5N+J47X{h)3Vlt1f^i!AKZXu7cz2RthQ?`tqT z*5lN3wc}&rZ>(`E<9KQ0P!fo?GZj ze?pPc?@#?1fhyXjmjCB2HC0C)qdNtTZ0qXFcBN^l!`mz``%o|aI>yGstMadxs8Vh? zhQC^GF?%htz{up-{!4UH&6|&@sDB*#x}T(2Qdhm415$&9EvjkA=!HY4%jDoNjSp*b zE=#ANL`|I+d#@PM-Dn$tf))g_?jV1FwxGEgSn6`W)Lt9d_IG~7g)Rtsdi#J;@cL@J z74C?f2!5fx+07h4_#cY#%(oxud1%IdnRU}!WW6On^aaWpwfYEEem6tDtCaqKrU9-y z+utm;=C@uE;Ne=7RlDSdmZ;5NBVB!6KJTO19?PD~%S=VS%w@ipzhoWDBiLZb8Yq?` z5lb)!tR0{#HIb2n`6c7x>2~#pz2DR+X`e$k1(D&Ctof@F=}}fj@T*vC?gW% zouMi80Z3?DPWMH44(r!Wr_^JcY25&rr1a@^I}}KFOUMhz+UZ$ee0jz8)u=e?aT(FO zCo8s3K6g5(87)oEZq{GD>^EYa)U0d*nNnZi$V#`FIixl4_0JWc`P$Y`*S+tbU=7{X zzbbwr;T-zpil(kUXf{f+4lTKiGGIg*Co>~w(eEFPGaQ&qH{%fZ)`kMy8 z3A1$jJTgteY|YR~b>H`>+IEG)3h_qbjgv zH46szKFRXxh#+ckJOzFSN*&2e5SrPK)D$Q;J!J80-Ul0kYL#Zr@#;YZA{eM0?TuPr86>9@tBHUf`thzq<`-=n2rr-yTQ^k)V{LcGo2!~-QOROf4iV;Yu43i5W$1GnRdbplt2%Pqeber5w& zvgbiwWkjqag{?j$ z_>+AS1+qLju!#Als$I&-%`DK7$1^=M5md&v{IWjbidBuI!=`Jm4AZ`TKQCI$1htEx zKsJ6Awdu`U9=6p}5zG`(%#}kXQVMoI8-lX;GPyO$$|#3eb=9cmRy5sUAF^Q{*YspwIIxkoh;Y~bITW0b zfZ^70WmopKAoumqrDepznsAu$a$V+@-s-9>a@RSJUn8e#zh`)I(^-u?BT|8*#0`k zuT$-7l4jMP^DK1i51fj^aK;!gw4-2WhZM8&*RK7xZu>b)zD%HjvhJ;zKZ{A4 zdF}l(T-f2ulp0qnh!$CN`D3I%`^0Gz zkr|7PH5*N%&pyX5$ysQx8SM87$RDbBAvttD1`h{u!lz>xOd7}(Wh#&GA|DT#YX<;- z>z6r+v-iD30*Er&v=YwJJ8FA#Dq(e_WG%c2k@(lt{qu>$T}=>*A)L&9a<~?jBkfgNaBde#RcWfeyCVK7?_Viyn|QHa9x#Ce|X&^ z?D<<*q+w;wwOTT=z~xQ;4i;OEw!sXr#!w1Wetge#&s8a3}i4QpUHjwkl>PYiLcpk0b7@ z5nZZlT@!=~h_7k|?4n!O4^U7BCiP-0ss1%e_Nu${@Y&AWc@m&r#MD9LO#He{4p!Kg z>`IuHoT{YoL1ItySy{!ZMH8+b+uKbZa9z43s&mF(o%(k=A$VvyxW~u14T?O;K5JY+ zr+op@^@#3Y9G4~EGaUV^rW2RsuvRMoNFp< zeUe-8YfpI@K9U=r4HOcm3%YtaBIEkzkYzrT+N`{awR0Ic4WHj>&RPhF?8<112S7Ji zl-3Ufe`ym&%m98^D~HptzC9=Q#Yth@Y>(&Jt)resk4G zJe(xGuC1j1_+zAC@j2O#`#l}RdHDBCWbe;l^>~HNx?Mgh7R(5P9jWhVWv-UL(`GHG znhHIX|Enq8u+RYthi6#gy%L&+h9N3}A23HANW{w>%a=1F6q4=#vL?EVDNSF92O3Mgq>vP~)ou8^vr*Y+BJ(D7Mt92SB(MCifCfD-* z5R*4iBmr^|EU4=Xoq95tirk^|UrjM!e-Zge?E?z{ZXP~sn zqzKM?xYhTp%ii{~(JnqY=v@EKdK*WFF;qv9Cu1aQYsExcMO3hKEVMR1z+qRp4N|aQ z)t>sxjdQOa$>aMSM{BWl?=a=J^_~TGYAz>h;V-ZR5!#D?|0CY;_V(H;^9r}j5T1by zyr3N)xWYu*i5Dq;r!yzIW3r{8{E5TB|Guu9^^QH<6w#L`I%K6&onLRu;ClEpB~L`o zs6DS=N=e+gy}B)J3!-5$Qh6x0c^|KAj$itZp2rVANzXB-Y^zL(%8idj34+M3*(nRb z(kdWLjsW8TY%at0c-pnhr}sZ|g0xQJ+j}qZ5S@-NBFO2-#;Jmx8US@u9)wNX-PebUBKUhsFTA#mDNX`7aNFaf5l#YW%c)~ddyM7?}Y6{5?}5vX*0@2KUDhr?$2*~-34x=ONc(73mI*2`-dO0<6E)o?s;o7I#1 z(hXIJr-rKH{^#p$=2t(bNfdNLo55mH4ssu58S9$uT3iPdofh!}{=X-V z0HJ%$deawO(g$D$Si6xrWvXc_92Xmb*SmD06ez=?)ZB+Y8*DMw7kbR@?Nbq@GrjQ!o2t%U>@j;aO>N@U(9x zc{Xr?j5ICtD}2HHhULHIQ7W^w zmV+`~m@U{|t6DbetS8G*!|CX>aM%F^<>~kDe%-v4;JFn@v#O^w(yg+CBShYh^jeCm zYyg*)1J1oFVlOW)E+&W3k_HcBFU^irEbnsZqbmaa)2mH_Jpa?^%jj%jVd(lrbh5~m zGy_`|fyCxJUdTzXiAoM~O%zA{_t^zrm1-f{|R!7G#RevAI8kn$Sd^5jphSGMSK`^mnAirJfGKBj$Q?HJ~b zr-Xdm5r&W=`j7I4MwZdq5yL6Le!IyZori~|ZiG7>+b#N}`Z6Ph?y6Z%@&*uDzWRvs ztH-@w04pZ9m#gtzq4+#jCD`z$H=WO6j&TcE*RsbDRgZzUCD}^!d0yp=s{U8eVMP0Y z-?QwHD7gdY=R!+p1NH5RBKK}-$`FW@ehj0v^Ciu+)+hpz#JGW@x*xG^_F#tR^B~y3 zTzYdKpoZip(A0~R@P_rJ3TLC`iHf<3z{e89rSlbn6#a`p2P~XP&?{%y5jNujme$07 zmez#-@jl7MA_<>~5JDVL6(((^BYGCpnD9RhfCqs!=+WTlgZdtx{uhA)=!yT$EcK76 zC?7!tcnkQP&ZyP@8}&i0%z%A|r$Av5_y6`RaAv{>fB)1LW%T|BWAeY465%*N$+}^bxKa02fC-f~lo=!~0<#s-%(@f=73mi%Ab&}f6$~Db9c&%P{6E~$n+Nwm z*R@^kp#7+gV*Wgm>k%y!=vWQ`JlFDbU4gu67--s>byTNAyZxcU+KKb9@ z-~aV)E0JGDB~ik0x;!w_`wAg_`))sxkOaU(=#bH70?C?^N;URm(qbOCS!W5^7vo?5UH*ny%EVebi<=1kD1GRBgu zZe5z?#Mv;&6x+i8!I6nDmwcCOtC}*aiDGXR+G<0t2aCM=jmiOC1rUsw?*7Go=h(H%3AdL#1NeLS)7*66+$g~Q!{r`w|L?kat->6)jKW1;T#G^ zxY@?$9NGXQc}nyy!a9=D8dK`0W$66tWq&piKCzX+2HJpwx_%>pMPSkVj!GPsN~_JF9wae!sPq=3vmFAxx(!>F15>d-0{qit&+iZfyls3? zI<_n!YLj*WfidHU$W-uSH8vL|qt%n4FQ5$}P~(lXG-*XdmJ~KMdR~LYO z@})%T$rEbRP23~~ibR4XFTbnYo$wQ*et-T=N13YMN`nRrixFovb`p&MP~0O4y#^jW zVmTxsb5Ja&B8}~)_{4y)@CO30kRTL8x-Qo?%_ym3`*cFC7V8x-0<}PuNR#waB1F&N z_w#%g9BXQ6fK{scE&_x(4ZJM0@p^(}Q3mJ%YxtGcKF9L_ygcf%G2y2ep$3>TP^?3nDca{_ywWZe02!Y0xo~*}A$VTg|ao ztKuH3kHwk`CQo9v?A!=0UjDVVG8~N+taqsMne+c~p5e zZj=X{7OO0jq36JUw8eTFUG$6LOwiG`P&1Nj>i?LuTIb|o|2AgXQp85Iog4r6-7WnB))e{DKJ@#D}4pY@h$a5 ziqD-`g6gM!rx*YTJRX_mC&Kw!so;S{dA%x`H8QD7qeW9jIH{8%h=Wxtht{?g) zuC|dhZsH>>X!rO&$xAT&kkC|Z1U%+X&`# z9tLq*T&nmbXfY$ml*z(4!dS-iIhMyBNb8^EBMdeAYhEZ0_oufYqRrM!W&QDS=;H@w zAk;7c{sD4f1pl?)2{t+Y@5l(b^k47>0ejzP$vHEfdrVzAX<)7%`T%Cyi!>8A;JWUX z!1@)Ot?ZZQvi%v(^!Hyr9rfHQkgVqmd?nu4I^WFD%+9Gjdc9Fq;fKKh?K&IZ({h6*`m3`0PBiLB|6(6c`#h-4I6xMQ zLHH1S3@fj%cUNFF@g*lt9>l%`hE)SyI)TVnB*CB(j>+?B(b(G#FgjS>-kbvCbBm-w zC8lH>$e^3G_FU67&w{q-|LUc$FJX?Rw#DD_Uk%0}ZU&M=kwHN2!nmI$McuVporemy z$ghTo#O5Uih4^kxsocfz7cO19!jUcCl%J9?1zOSXqrcFn&r~c5d}4&bXWgxv3J%_9 zj5E=kSgNttMTcB<*b%EWuaE#2*5Nb0uf>`obHLvF6PXPbO>>J=S>nWq^g6c*cjL>? z4{_d8>-W{BTAm981upn*_Xw1M|1BO24P*h8;(MJP4t!e=&45}nxA-Gj{}E@H(%rvE zz)Z;}!ALyt(d^p?`0XA*Wx(ckr$GdBHe~%}xUB)Q4BhlzESW%f)*<@4Hoq-3M78a# z@KTiZzI_8{cb=7w?2Dm0Q~O+>AD+XSVd?LNEzc4R!M3#j1P|H@=4j`_J-l{{&nsnb z&-U1(>qrL+HIM%p06hPn27p6FS0t50Xcw4`-*1M`TK?+k7R7q!6*?pVv*b02m_iF` z!Y(d}{l{%3-r##vhtehaxV#qo&RACc6x?A@$g4RdLuJ)p(Ly{-*?86FRQ|`&Amx75 zWkV`c{M3{>&K##$6(!*rq4qSl=I}N_vVdR2A%`Oi!h#k*BUR|_0broV!z7=*RgW!7 zb(0ygAXi4RGQDVYmOZ`XL>9qeoo*Y!onS!88uWp2zw8FKQg~#|F{=~4`5={rT{SYg z#yz(Xn^(e~1*<6efqv5nVWnAy%5PD0*4ZO0`B38afwT-?T{=K-v39Zx47z_<)mqWa z9Wvr3$KkceRLhLTXhwUpWofA0@AOyu5+c{|bqwV$rPvE-&gx)oz)5Y<$-R>wYPq_g` zvj^{$Eq<@M z9H-Ww(iZCutk@v(0*vyG-w5#lr*JZT59tJ-49>vr>gho3K7<|uGMpkd@teb)D5$dB zsS~!t77vf*cXBx4L3}n!ssbQ#U0#x~hX?kp$$5dr%Sr&vfxSehfP; zey*U)hT0tXPxweh&NreC3faol2`6SQ_u{7ytNg-%sqK#;DPh7NFcbk(UVv}&wgsxr1X<~-v?hblOq#AM|l(IV4qIy@BDKqT~od$V1}H`;+37X zfQ$#{7*CKY(Bdt$$_33pVKx<0i^slm(RhLyrfjat=A9wVRc9=}R1Hcx_f!1)1Jh&) zO;g$F7qJ56jTCdL(wakK{PMiXUl{2qA3Y6LTVyq7eQwBibXrZ_XzD0lX~9;j=hyiW z?8%%Zz+o6Xwe3DyEdDpQrJfSo!M4(4SqSsSrw@yhSlyjb@Y@sq9m|7}qQbg5n&F)E zpljoAvezD@kQ|w!;RV5%LFu@9w4jseG2|^)i=o^g6EB(0_o7ucSK%h#Z8el>87zdl zHSPZ9TK=(@CK`IzxBPtI)Q;W@Y*ZPw?3a;IeLGEzpMY{riZODQ`*x30QVj*O;SFnh z8*SIb!|mreC<)&mQ{rQV0bNUzTCxq<&HRSez|)Q>EratLVMsUq>!pTpl*s;`e9p^; zdQT*WLq@INP}hc{@Ok@4K>droPs))xn*1;^b+1Qt;I|wy9P|_7=b1O<62dX~k7T-@ zP84>z5cg9I^Qyo*_X^yv&C6>3nG;<`f(;A>HgE~_;Y4B6`bnSAkdCCBdQ9A(I0M1& zFdy2c2DZ;dtxkM+q2JQ07x-D_L~H5kQE)os0qS1qWar&E-<-u2)P=rK>+~P;#Iuy+ z+l@zc3xPjsV{NZnKb>3aa*3Bd?}EjeirL8S>IMza>`$}K79Y@ zqk6O-jAc9jbPnv1(f*PSIWx>o*Fp`u?Xaaq7js!={)-*pvikh+?_}lHTn%yJk2FQ4 zS@{aAop^)MgE2uId98IwAbYL#k9;2CfNwRp;j9Q*7UQ{`S9q8-A>xbW#v!B!FOHeS z$`4FHKH4zf z0f7Li#!O?lngOBg6S(+`YioRT37aVmlFT)f*&aBRRZ{iYNi3Fqctf9c7?a8Be4c$A zxt8!_tg2%ro&i6iod*2*^QQ)KzTX2peK9h`vZB`B z)I+qFIPJi-!^|7_$Fv_k@VLw5j{O>dZ^Q95nl>o_x~S(iQ&pbkQuo`qA67Yj1-9?i z*>e4sE~jWD^}bX=i`hdhEC5cUQY6na%r`~*YC|QgX2dzNE!7|~S&c6Rmw(wMRItCS@ zkVNP{i*bDk2r2Gna>Cmc@Wf>qd=x7jpm$Aj*PML` z?kM$H@5VB>v{yM|Du2wBLfbc)${@u-X3xD%c!>YDA?lHIY5|xbwyJ7sGQX|VxLXz+ zxqr9Pa?{D8(*uqW-!_b7GjDR3i#d{G2(T_vxhgz~ZohK-zrvM2$#V$T6Q_5)Sh@-c zOZkDU@_}0Aq6$gzwnD_enN##tmn-LGpyasN%9(2U;hCn~;m+dK$<)-nrX;BAc7dwXy2MP$ zR-I{|eA`q9YjT%r1&doNcS&OJG) z4VAh@S?(aX1V2)>4JwRs4QAU}nxKFkaYZ(BZLa^4`1%v*B&^wXQ-)Lk#_rqkc{OXS z^pJ5;km;EiH&{vIj$a)AQQn5Dzlxqx@SQzi`Y5%d zWnL^AH?VvpRwV0v71DMUC!S^CltlF?prcWPoh_?`)N0uFR_uBK_sDEFw8PSY@s=0$iSkVc|i@Z2V)N+LUYVKGFUk|Ioig6uwH%`rbp z(_?ZWu81cLgpnMhhPde5P8$J2S5;)i%n%q#?>)~aEek``gIIw_##wu-cnB6=iuCSX zOC}Pof?&^(^jh^J=*m4qtaCc9G+#~3mxWHoxhRW5zk?(QO-*ZwqrKrYLzf3VvIuk{$tO`du30wk}7p#imVkjD$Py zQR0Nc4ItUO`d*3g-$M0Yp%RIT^r3)yRZZ&7<;eKisaI5;h!Sif2*zF3i`f#FngEi~ z`j0)+1;w6!L?IrVCO0nl86Op&q|(ZmJF^ed ztPqeZ^Z84I8-sG(=o{o5==(t`TW^JVnSyE}GolS>|#( z>HAl{*>*s^GD+EX@Q%BVK|M!;Z^wVaLd(TWi{lM>QT|77lMiOy2~&29`d7Y^h=9u- z1aq1gA9v*}AKZL`X%RkLqVyTWS*pnb4av4xuJL~;L;v}jaACjv87mLre{`w;L*|0H zqilhfNb+&=_D@12|GSfCwF8ts#23b@Fi8k>%J9J~K;D3kGzM(Qy9^+0SBQ)XbX6HctNUt2F%S7x(K@aM?t(qe8^Ph|L!1q z+V(+#bVx;bBgqO74f#Qm#21VP0Do#ZhasC_c){JWA%X``Js(+7UwU6zISG&+=D-`0 zI=Tj;%AGdBxjuT1Ee?z|OlRQw5;{IqTmRD+bZKvb7q!~LoVO+;lvV}Eri`0_#8!3< z6b8QcEh$R1o1p@dHutAj)NIdzi6v$g8vj>d5^_5%-QxLk2fXfNb5fuiRDPZ1bjEqC z)lR&qUVOOZk2;ldCd}V~c?e_g*<#G4@(KWY9Lna|F`eAee^>w$k`qxCbpFR(Fsl*E zL5;7pxF;QO1f8myTmV@t>8T8bhNj?Wvi;O_JtytupZR4zKzB zQGc!p74iErcVvCyug-0cj`eZ~Ocn}hQ-DdT(^TOOh1JE4ZopXPQrH!L+6R#>=D=W~ z3?uRV0%?-{5&$W80eDez2F@LCD!%r^6bcWZ0C-GHQZL^A4@qi^F=iqMIN>}(a22Ya zKmGbzRakzdE>Ef|&#mgOBo#Xflie?IvmG48;?d?%V<0SVy&}90>)dgl>Uw}CU zA{rS3F90mfxm9x_{nxl; zH@AT>4qiAEwHv(MMY~;&9011wUr873dTtdleyl7LynLInG zUA+_QdKNL3ps;5H%gSzEhRH2n3i!{S>ZS8RuS8s6z#2o1Z?m8meD0(RTvx0uyLHkC zG=9&5;EBl}1r55!PXfO`8DRb#y|$Zi;RAyY_CC99KJLai0Mp+I25{?*y9K(asO?_F zv#v29ifRB+s>NP>88mkg`WIHRKG$#BzLmcxtw^8Y@x20x8p2BjMs^*TCR30?4Gqwx z4`^o9Psd?FBpl7LuzSHb5XbS`15|e#0okY7$RdWHy;WaM-nXmC71YOP{h>FrFzy#) zv(?!>SUSddknghkOH4UH&RmSlYpZ}37SJFBz+zYSL4;KH=D|X9@+A-tYq(Z!q~~sd zCb)M~I2zfNa_O%Iej+u;cjtSfM`?!Nt-vf115qhk zKij5=yd<#&J32w8Mh1m_Rae4E?me)7g}b?u=`Sb-rPz`wtuQXt}UbH zg5{#_0bn9`jQ*P42lODaj6y}+zy><%IrSMd(EM7>Z|ejSOfJB9Hlk3t#lsRI-rNy8 zWT3W`;-r0_6A(*fG=@ zn_7tqPk<;OR1n&}oLT`>l{LG+!OOsmZDlPmX@`X|h$J!tCheeEYpU`m8-baK2YCon z%X#;YI|v;CUjoDDrd2j)>7cKn%+I^pjZY zU7dg_;R%E0rC}I=>mcZ((>%$PeGAh0+3>auqm;)kjaHFL9^08>r+Ve0s zR=dI6vYfP`S0Ry*Lz7*G`2v5z#HACyYt_#@2icb|SQ7*ef`wqFD2zCFL~e!7)Zf^7 z9LF%|p|w>drb*{AbLpy!cEFpEe^dq_OtkV4MipeOKY&GRkgZt>>TzXz*fwnD>8YfJ zncfgEFmL@*^zxfpoodGf9PhwXZ+tnD7yEZU*|mzd0TKba4t^WzDUL%384OZ09*FA!bVNa_|}oyl;9f^_n+!JVs@ETQnLi@ zEk1LEE<3xQD^Z;^n^!|+F#YNILkOOl;zD<3jKJLB67XS+Uju8Ba}YDzJ*KG${IsVc zQCU#CqN!ZEHku3xOe+r$Pf1ONL+1Vq;zN@GsJa9Z2Hhn3@_ktA!gx%qBS4SV!#XjZ zAn$tuQtfXNOB`>nzcQ1~5`KXRGM*&Fro~b;;^;Ty3sg~Md0V?CH6#7mk62AI(_~^t zGn#3QK%kgsL8(GkqMOTKeu^!2ER~9E;EGBU&-Vhe>Si#XYy&&;t5Pp1(q2}n7C^IU zPQYxNnqk#;7!p7)vo=eKbY?l_RQZSkKYY9j3$;N-kXK5!J?fRFivMYX-?xkf*?4ZE z)H*J*C$wbsGE>3w${du=-ThdGKg1h!-D)E}z+}oLvv7*6Q;hooTF$r?2lgGCjvBwp zr#6CT60Ugo5{CBG@(E7tc+8i;kDA$3u=v_+TCpR85c1hk-Z%0O!^UT0>P<_%N;8*q zdFBgU1KWaLvJv(*A+R3t8kfdF?-#=(=#~{YQ5#0-lGE1B9|uwwYfTIAy<)2X4e?Mh zs}l(`5q&GkINM8b*OzZK;qzz!jQSD6f6V0qNZ)9AiNU6WT%h9t3bQ)V5lr~yr|3Z? zlt-rBI$_&YvxLXs)NlTQu5;NvNM?3o!uaXbY*%NAbzW>qPc3-30Vx~89d8+zFWNs1 z{sQ86wyjf2RalJ!OmV^U9krOU0f(&T0S=|<21czu~N9#%ug$4V}9s1}S*bC}CoeL!kIlJ%^wj61V zs_+M zrtwT-UD7h2^Cj)XD2ki>t3~J=N(Jp(ZOJ98C2O!OpAJV)%0^TZ@0GEee^`NW23xU( zR^W)j4fg*KEENU7JU|moflO5fXN{G%-|8AZ`5!b1)Gkk~`*pBqKdsFrYWv^TVSCE6 ze+A$JR4y}fC&6HJgg#*Ib<(F8&SUS&h2Hmm@+qiYx?I!{F{OE-6R*{kE2QU5FMh$x zqY-*jvvP#K4K;H6E?;q%b}8$&zXA)CVfw^%JusF_&5-l3KuzsJ2)D|$c)|1oscQjX z4b(AyvYv;Y)a`y%zM)~1_wH6n5WiQs@hwd#d@yb~JMRa1k2wTvqo1W8((64Gm)--9Tk5UH20g54gxjxkRUHj`g|d zPtVusV5!&R1p80%!HTJo0*T>hIRtVS)^+q_q`i?VJLG`W(08O=og&X3hJueR8>HUJKpnda$>)uKn44GxY=s&E$3Km1$@2MY&&F}kj{EIn{ z+fzg_1piDl^H7@K=PG1B3CdiAN%I_#qT%wLA|oFdlamy{ezJjUtEWaDFxcoN8VTST zkIK>2X{{hCL0o%%(Sk8xHDRf)8ZoNTnDLCZo9UpOSW8j{WBUYa2y9E?0~P5uNYWpb zIe#dCu?XPqgCsYvQCDpklHF>;gma&eS{&;G803$V!(qyoNYX0E>Dv|;SSiZPmO^sx zlP!01xKINQ@@jU9%w8rvyNZXW&abfzJF4F!fscU>qApDzfs#m;cPGib7izGu1iJWj zP`D55Hk!CUc`rl<*M<8DyPz8!^BGsJJ!ZM6`uiJO16`TSDWnAoj~?>gJr#v#?=BeZ z`2JScyYfA@{!Y=CCs?0Ce@Ob@mzt&-8~a$h>+6mSyAjA!w|?FV2yv;uCGr3KVD>gL zeq^SZ|k;atdhnCD-<0i+I4kMvt7yvbNw`DBle9=ED@#3Uq~ zMQq2tkb9ZBudVi(hgf?@cWTlm%#bwq2`7}p@D`|4^6AwVEVTGuS?dVc(te?r^$Yq< zC6Kvf4%w^-xwayWqL3BP(h45aBg-W`Z*FX7a?WpiiB2UPE&Ip8U*zxoi>@EQF(tq; zOX#A1^EQD2_?{+w?kQbB3QB+0lUP&fG{1}40dLTKZC`-$)EM;<1Sy3fMX$TBOV0F# z-ift3S()f6YrDa#D<&w#dLr)5H_lSV4SGC#Cat2m`xWsW?Z0+C9zmJIDZ5p9 zKdGak4>kHL8@(5rg~>*k6_x~EFxd#)QvO{#MVJTqJihO*X%8AluaSSZ6}L?(;8aXr z1T@y}`y1$ynm+N-uR_rQnCC-oEA5yq`)MsRiq(D>}h+7TXE$9yq+ckfDg^!ZpC zZB#C$VMOTp{cO)}Zs^Lu#&VJBIq+K5?oO=s>}w3Xlx<{x>-a*^lB;yCGR&55PdG+B zB9oJo3vwE9kUm{sS$qlolf#YR>Y=^6x7PDVM+}71u`Q7)$FwZzpyXse-X-=f zsxP921E`q#g#*nAok)+&`;r5n6Tr)dVm*R%yHY0_uyWDtmxbSI=ty*heVf>ANysP( z7S@BcU+r27mrx24Ji&{LCKI>kb_$JC5-9A;&f|iLshpk;zRvxjG5Rx#S*4cQmkpY} z@A9@$-B&)ip{VHe)*p4JeC76c?w3!)G8v05zoJ2r{t&kNj>)Bti^t zs}8{(K*-Ianc_Z$X*+W#z$2cerP`KUKgeQt6#XcTXll!i?t2&u=X`r4a}4K|8_Utz zR|5u}8<)>9AwaSFOQVyBG;X-vj_V;Al2lf`K^uK_#_W){Jnuz?K@_qEOE%KJ)suDQ1UO^{u~cY}45iYiAzw!q>k$8Hk=@$}LC?39iL36GxL*SpCL8 zY}b9unW#ti6X`I$$r8j-17`?=f{0(LF1LEc$=-A9Btx}r{u)w?d|6ld#I6y=X9tJ! z*Ji!=-v%j{^MB>73YtD;tBEjNib)$6apJyIf2`0p!um47%I+hfNZ1aMSwb>_7jaIa zc!EMh#_LC}hh`aH1Ls#*!fi;?zs=;+%hbI|Ok&GtDNO4g?EQ6|lfZl)t@oHXot^2> zKkFF3DdgZ4Od~RaX+%WHzZY5Zo86PbCRrFrxf`5HVu~nv|%hNV7akH^T%)&TPW&G zC0?>{(kCAD#$*D*oN&lgz1f^#2WVjYzL6=w+MR=IR$WNGir5t5P#}2RZi0ooGzw}K zyEkt7UdL? zFSf*_2FY3%ezR|H+rgO*_(C?K2t3S91n;nX6R`RVCMaL%++O>tDB48oE1C8Q9`sr_ zpgWTa9Yi_>-=g$=O5QRYN{hW=a;#;d5X2%*>QAK~x>p^4WXhnyfvuNrCQrxVpz~F+ zv9jW-b}Qr5Y)Mi7+GUb+&XfVm-&{MO4NA-YRsGflF8+ztLHSZOc9Qs)G;DKu_>v7I z#yXoGng-C9F*A@W_R+`*g`mJB1WjyA2d7>9DA6(M`1%DjYI52+yZNmbox_Vu_m{fU zpL4WvQqrepZB5enW%dtxCj;B=aR{-~&}+9$ko~q8g9~e^kFkY6ADp?vJ(%*V*kg;I zzi14a25h-}Y?lxCciQ?OhuvC|<%Z9TPpn{Z%deYg_2uuQB4AIXmAC^vey>c5txHW^ zG|yB=z1Y7d4oEMp+;}mddTI@C1^erz7&2Q!^~dW>^ioFQEvI` zA}{x}Cj*_0P{lqO9D_h^0{zj~eds9nmy)xDr|l_GE=d!{)wH8tpQaC5E3fG%^qROg zEhfv822etjdWwrpel!{Sjjd_k*m@avNVD(ilar(ma}{0uA9pAvR~$%q4oBWb1acL}*j z)H(|I_Qi;Fu^@?DyPi=LS7AtQlU1Y=jai!3@w%zR^6ytYE|xgiKn2Re8TOaHSal&L zVJhC0+6ZT&%6i->6?RRKsIEq{#l?-x_g>S1-yy{Cf+wuEOQLn2C_;SN!2jVdi@)y? z8j5+$*LgHPv2MIbLVXJ7Zid!`u$C~V zi+W~IiA*EzRtH~T+#nqS;>I7QmW<0hKK@rl70lmWl)5l8CK9QL3%6?Q34a+)ig1O$ znr1YymP-FTOflSbFRWsz(eBbj=XISsM30GJ<>gG++UX7Ft78WMlt^r<;JKFfj#1-E zU%e!3VCOj@uz=G`N*tA0z&3SKb+!t5UsTCp48yV)KV6QA{NPbaWN4_8Y>$|YXW3%R z-}^z&3oO;cogN|Ex&TT^nsn9Uc_%%qCC)Vre8yumOL=OP0YI+!NxujrK+w7dT0Pmr z+p75y@1GGb0n>Z7F6mM2rFI9hv==@cs**Bb#QoM_M_TlKgy#i067je74G>KV)zIj; zS#C7EH0dqMAN?sl$8Z-1cjV-PHWE!6^-lFRq9A}x(F8^vne@u-<8TIrVa=Cpb;S<( z?bGxFV66QcYP@o}ve|2M2g+~3EC9xOFbl%}nFT283dGz%0;EGVMUO^n#JpEPPMQ`Q zv}-xF$qo~duACm80WRq5J0x;hmmU^Ec z!Z1b(I?ldD5-!+Fw4+UL-vs6(x>{w_bQoqYB;Vk_!DzJXx%`+3Z-N{D#9H|AeER!H zhJX*jw@*CVXpCkrcXv6X1o`aQD!f6pQEpty*=nywIp?*yPR-*9yTdfEJJ-ZiE8+&t zi8Ol6RQnUuG4H8imHBs|xjY7ScYIWmWBDBZnAi|brW-#+faLUj7KhpEg4z1nkkL|?#R8Fl_@@DHdjg$8}d4a?rTHiX^^ip@ss?THBcefhhyYcuABJr z)99$^4-32k`6m+h@v)z5E$qOX-;%(x2=`G1+q0|Gew2`*ZpGz^Jzaj4vI@(qq&nN& zyyDXNLc)~vny|B4KG^%-y9soKY_1&*UQp_1mG1F&HLA>0zbY#RkZA9*b*NhESJaVG zs6T0LxTCiP(={mVP^EDH?P?SL*F|%~jsT{%1cnUzFX{+j9)iRpF%vdV0Ij5BLdSt! zPabJ))t>-9s_VZhIGHfcnN#VPw3+{=k!Z;Q5#|5qpYidMHr}1o^N2%Z)LZT&IYRc8 zi1OF0z8AQGKVkp&`m6q5KG-mxoVYuIE9t)mAaS_>^$oC>VMd}3HV&ZlnGf(hLsIz- z9Yg}gD3}*$5ba-G>VL#ZZTG;4aM-$z><0f$smfcS^8UzxiU~@|>{XozfB9{&ig7yd zmyW_fR0(yZ3hzcZ8q+}Z+7HmB#Aj7r-OmGzJrzC@fIc(^;xYa|!j-jOo?GJ#?@8oG=_@9SQT6f%0i4Oj(Ml7vROD1V7zB z`vHhZb^!1eCE`gAv;!w1r&4PS3YG!D^0GM{z5{9YX#h@C;P&)Cn$Jd9C8@RJYt;)a zmC_{PGBNGzk3;e$_(nP;d{3?}fjQ*oz)KikMy>-Zw_w$zqCvt8|g}bpX%rKK=S-2LVRy zDbD$&A*QnyD*r1Q!IA3aiO?e9oWC-Ss=O4Eh3F_(&#z!CkwN7FN6XGySR@WC)#}aU zS5R3z06EGU>pzE4(pBa`*4|XC(AxV8(6LZY+tlF zs%WHEVy)r-3!nj~#zBUcP7=#M;I<;HJm+d4RMdfd1G7bJe|U&SyiI(@UcMxEBWE|UCJ~X|X zF@kA4qCjpG`MK_08jH>#@v$ zIs_=1351t`Z`T0JvchZR5zMP&!(&I&)I!Fcd(}a!tTw|G4eT=)Kon73Qg87F2`u&< z{?PsNDRrlfZ!A8*0Y~4ZCVIdJb7wI)1PkzhZ%Bj^pF!(Mg|;Agv`^iGBHID5Ic*KU z1(n|t*G-^dI=SD$ghHz@Z*Z6#fE(4$T((L)Z%_TaY|?Cne@X3s0;vd>M)P=lU4Xdt z3l{$bVtDdBKFAZ6>`8)qR$l)2uw$tYF6i9QzWT^K-rDeKr+zO(;4rB~R}#_&Q2KV6 z-s>GE#%-Ky2ec2c78+l;r`|BgzIJq`_m>3V)U5FK1t-e^;EmY>;Ne^M(y#sg4-jVn z1AQPmgB*P;ein(!M-T|yDt)xS26w%mRF)%%1{zY}bmN|b|A)A{-L-oZM}u?X77&KZ zpK>JO!K6fSFepzXkzSn+=4UFI{11LxHcWyURyJN67z2~oJe<_7n6z=}v9jz<03CoZmiw_1%ST#vHTMMu(kUAMA?w=f;>RKr<*ECKByIq)lJ zQs;LBvuvv2{%%V$H-YdK05(Y8nGxKP0}~bHD!Q4wU+~9}Cc)O#$C{_4I-S6uzu$M; z0s#_oJ*rKH$SV+BdgBXXV2Vu#w-KaqM(046y`dokp(<=cTLwfYeoA=wy5kDZwld{q z{)yc_+{I2)UP3I9ZE6k;k39ku)Vf{jA35YAra3rc^7GRJ z<{q3T7~7L_0WSJc4lH7|-!6X}!HlqbW8Sf4OcoF3TbgXL6vCW4>b~0gCgFY{gHj@< zY0=}%8~J9+lj%pL*-!`cDeutL0D-wRv(76g^UFpAYmp^Tp ze)(K`VxjY{%9Rok!``D8XSnK2g7s?fnZ#P(=x0_ec2A)SOO!>iLDE2$LGXBv z!$sr!6)0FqgcBlr8L_!#L zFEB)wxtr6(fTmD|;;qjd0do>|Yl@h_E2&^P9Rca*&G}yYVhMoD>coh{UIm;;0-&Q- zXm)cRe1LL`G{t%x>m`Nf!lyrZl&wx&*(8uRh*yvS(IvCEcLi-L;YKcqA>4J#9{e1w zJCTH4fV%k|?=eRemz8Hg+!!;-LlvzJcV}HxT$plHusn_x4Kn-vI)!gkSqm)=jW{C% za&1P@25&hVg5YknH=@cdmgxS+QlI|f-i?c?oHVS4St~OBPt;D^d%&hUW8Sdv_W4BG zAxbmXy|9DS72Pv=ZS7egDY}cLc_`vr=Pf0wcxeaT)dQw z-hvIO*J&D7-EuP4%R2Rz#~GVOQB)W8C3h9;+3!xF<`fhC#AlX+SE=61dkW}YB%7^N zQ_s!rETWUP0!`rzorxt;NeQS3->-K60DdhD>t`b?5BEn8ZYNB!en~^-M3`i{gg@7C zRDakF;cX(8aFwq9N=#HBM}0?_^lO?1Q;Ppr=k&#pAW`&5c!@=q+}BT!-{GyR-|wp> zP-ZepxOJ*n-pyXi2jFGGnfAmj`k=pXDeRC@#afas@CB#o%*;U7)n{RZ+6Zw@sEvxX zkx#7Tz1M+L2)+ZHpZN%egOnr<9d1WzruE$tS1xW9jh5_2u92Nb|67_LIU*va9|Djc zlhQ;>6QW-sOQTNLO=Xm-6ZK1uv;9D}36saNn;&&??-Vj?4XFZ5WLDMb*=Szm7Y|en z`8K)NM5GGA=)5Q%ZlhYCzT~Kmh6?HYxWmb+s2OGzIh=*Z?jUiv#u6P55&jerWD6gP z&{_Dd*UgO728@%*A9U7-m1(5nf5u6>dcbMdro%*}s&!qgCHC=%b^OfJ?ej$;+f2lCpT~&J< zNXohUEWb8eBi~$%*On9}q>>VrW_53ekFS`%^ui8j=jOULO&niKHj##gknfE$L8@i+gT*6EMauj`iiQ_KZ!=Sy8;w|*A8+p`Efl@QO-M2By^uU1Cm#L~}(T!9Mv!#b->6+y}= zT9=rLu==X8a|^+}0`F(9d+bg(VQX3_@iCMRmPI%bgscX_ZtmUdB-z}g5cf{^c?4ML zA0HR)c`_Z&6qc%ldw1B{hdB(J-D%ckM}h=w@gwYni!ZSrV+DxMO`hHM){nJZ9CB4U zY+Uj#ac}T{a;Ja|)X_8UqXE8u55pB=DF*Zsswh%onEfVIe2Rk;k?Z{ti9NbykkL7zY3RgNiE^a1-y57O*mw0zpG+LmQbDL+-+2od(i1i% z<(l8ko)`ibGQSD&e0uXbw&SlqTlgr%ZXR$h#>f(|+~qv7K#C*K%BM+mmx9{bl|>^2 zZ+-U*EzhZGV994XpdXDCligRQQ{u^7gHbcrQ}+qcmG_TyQEL;d(q)Y3=4SN!`NRk&=}VAwGZS*g^8|buE-zU9NzZrbgC@!g{S*x~xMKl8^<5 z6NJXuBOHCT=rKRW*}R=+Hu+(Bpw5Xk6SWF+v=u}2r+|(5)3i6=%h3XbUxQAg!|BG= z+u-(R>(<~pd-;~U4E>O}<&*Q*l2(ysA@DqP-w!Vm?!=6@&W3Y&Q3rDDr{r#HWl&VT zJxIt@uMP!U_dJU#Ok9U^rvH!O!X8=oe_|_HZlNn;lLuUohwI*gu~ZkXul#3DVm+wI zXv-J%8&}72uVGN_FL##7-VYFjZ)Ov>++XnfCHobg|Caf_Zgt@oW9#5 zhS*;z^*E~IosV2)#z2^DP)E3I8An^CN3Zb4V+Gt_TO$+>}{Cd1jK^Qaj z<7;j`%T`a^0Z;VfLL!zh%uN~zcmx8+Vd)+O)yMw7Qfk-x{RW-&+7w(YKg20Tz_+uS z-h4;pdOt?M-g+4M0;!ox{UJa7huGdQi5BswtuH;3fmktA5Y6K4yyi0$mo``r$)7`Y zAx45U@|6$oK-l~4Nh3}Nk))^Po$H(w6%W$_$ig(pqOc%M9pg1nLB;N=4m?Jh!wMAp z_6}_;jmtmjlIFk_iNUP}+^co9O^b55R2t_#Mtqg}C<_UbGX9-DNsGEn@U_X1~(s_F52$SWHqbo zwl4Dt^ayPDKEM{zKO?ie`zsl1&Sv=(vvReSEG%wUg1uwU>shhwo?wL#ua&>W=Wur< zt4Eegmv$9m``(g;sohrt#4HoFA@H#*!NQS5Z;G8|77FKVq9sQ|0%!fJKTjf=+^rGu z=N|tO^tc-&+hRf97YT2%+h6N(>ycVB5<%8_KScNX&ZUW9j&4?zd{)80e>IQ%EJOsw z4b}p=7IHq7eX^w}jzsHF1)fC3?|4d$90KTbjoKO#3HPBc#W#ktuRiH|w0uH?o445J z(;C^3CA3So=}wU(X0qvS{f$jKk+Nnb92s|x&Q|^{aRR*E3lK>7B+LgL)UhZ&@`w1* zwBW#H@NGWIbBIzMPpc$?x z%_n4BT_dotLsld&CO98vOQ%i{TV;~1`(GMZ-3nX@2<5QE1 ziqgjw$Mvg}s49E3K8ePRYH8*jj^HH&yX97JtQIf6l%C&y`Qgs!)zs72Dl_X^M{YG@ zQN21`{z{x}PLBcE*34m({Z&!lIG@wLeoB73?Kk)V%^a8o!tu3zq%E|af*seY1;V3b z+lKyJtcrv1Y5HlJu3Ql-X#6@BUMhGY+-An3n%;K(7I(oVvIb5b+tZl>>5#CdG0=Pe z9o`k0av2wsl=_$Kw$uV`6#L{l_4Fj4Q48!c7$A_29gX$EvUhxW^p*LXwOWwcz81pV zqaGp*`Ys3GAO+1#WRjdbzhRlFU~ha8L!)#mR^U>)Cq&SU|2=}~xYs6%Ci?h+U-t_l>}*c6XAuS*7y0{5?0^=dtjoTsS4EfK zIRhu^mEyE`eM)W}X4*+u_3NYIJ3?25xVhqi2WPiuK819q1B7cxug=VPo4~H6El#C9 z=FBv;HYP_`XKQ(ai8bGk`UJ9x(!;Tp-h+=xwyhf?gqoM;SuFdjsL;~XcnFUFQ??5PiLR z)o6DxHmx_EGDY8>bl=5eln8WQ87vRXfF8@|Q6-=K_)>(r@dM#yg7hrOX&A0Qt@7C^ zewX+oEsJi#K59LBGFkxZWz^v>L`3)mIYU)#F+KH`L>jgXhm;hgOkSsdT?{cdYj#|kS=c0RHZ|@%A@J%lW85Prd;YUuddeTHSNYrN>KLr|aki376Vtz_m(67V zKKr>Tz%@tG+P=Z>VO?NKkY!G-FL8i=%0CjT7@|8jk?uLR_Am*=WZV@8C^5$CX2~lWkxAP8ZM7y#^s>+OM?p5 z9bNW_P}EjslzhHF7B1@jH=S7Np`Z}4tJ$a7{265j$4{{FY)f}k6xkI~2fD~#s(Cn? z%2e-Qfo$BS@F7d?=s9DXbqviRA1;-XIP4V_ln^e()=VVMx?H1ojM2oVKaX$+Y-y^< zq>RsTF=RX-9ML{$9#M4a7#L@$M?<+H{Vjv7&)a{?znWUZ3xEAnVDgyq=-AOHlX>1T2;qkz01t^jb@BpwW+ykc1Pp;c_ zqqK9Js`?E-f36j7Q+3;u{dSV7H)gA=*RA=_{1yTh%_cu(2Z&T1h=rG?ds#HJuCWD0 zAv{9-=kvKwil`q_$In(kfQdxf{(9ur^A99xbZnhpMTd@TrWGcz}PQIss?tbu1Sy zu}zwyMVZhNFAjVNUP-nDK;KuWjb}~!RG)a{nE^Qm$GIl+=y$8KAE8+w9!=NOiN^MCmeGa~m7UkUQ7Ov{%R;|H@36bYv}xUg%r(d|yF zO)F>pny@7ff~)4JSd*7uj*t~%y67fVFi7*3ie>xG27#IO3_*b}yI5DK`pn~PMQcBd zFBHoIH;mGK*&8kO2>GpV=1*yRp==@6m;EMLIof|FiAGGir2fmN6Nj72CA!1Q$un=b zQkC%5n`u11knaKJ!!UO`>UBxMQy)~z8vK)w8?KM_I1mQ)jG#k{NuoI%Dig*sJj039R z_#r2PAH5?Isn~`iEF{|?lZiRMdxt(QMI8mPE;;s+q$GZ}*IF?(+c4mI@n1e%_LzKR z^96MAS-ME?u_I3~}SDwX(h8%0RWD9^}?ZDW4SR)PhSyFa_=8 zWoritv9cPjNWzDsLBsW?AOC(#dx_&e2P?cXXQ?nX<>7i^1L3*(G;MxP%v>k=E-Up3 z6CNU7?K<|mUuNpuP;OF~4T0$yd~>E_vy#Jh1U3mocuU{&60HNguzv_ZV)%cqRO|ep z;s^!;*Q<4n6C%!s-Dq{h-$%gYF@3r}OO6;8gm`l9^!**^ZZOQwnkt_|u{_<8R`vd8 zt`oB$kLHdjVsL~KV5-0&qaUbCF(t7^KSB(>7zny}xs7CQS{@B4&ZAnISuNqX2M+@G zx6wI?XCMD)3um^~v@r!3K@j)vRYN5gP6V|f#OJ_MG(aK?lNFm_vV^g2Aq0?7ibIg! zhE?|uo%p_!U(CJ2@c@F-59BIM`0l2+CjVu)UT|@#U`8jOJbaJy*Q$R^c7^Zi6al$g zr<(tAIOqh*9avQG#di;2hj#W_94K7YI*J(BlYE98CIg2D7f*t94d;tUjU*)%02e4$ z0a9gR3_)+>g6Q(Knmh3o1mu^HhArO1$QSj?Oe?ZfH}Rk&zK<`q_6d6*Sj~OEAJn5rwg4Jj4?(m>dkfH9*^8TNGCP3mr#oSCJ*-PEdUCgP zmUFr7;xBaC({P_$>02QCvGec&1TmE!iQUdl=8j>C@yE`vhm3M{~$fH;0j-#WXX2;SLT3UyQcYzC^vp zfrIsdIdSk3P_^GRc0k9Vn}Qu^!RM6C6`IfJzo2m!wep3*^(c{1OlxCwIf~_=*7^dI z5O)DZUGZEab%~Mc8_Q0ue_V~Op%;eZUh1n~moG4P#dj~a zBp-xDMf-iWafJMFv$&oz+C~q!+wUg&?{uDSLA}(fnku{+E|FavKKqSGHZH?>P(B6C zJi2Lk+e#UiJJ@qSkc>L|z6#}uP1QKC`n+0It#kUbW4+|m^Z9#@AJLaA4Wlo28l$&~ zaV2mX=C56_pRUwXjKu@+?5rw z-CIMYiit9t7sJ&D(IDJ)h$JOTco`?g9bMBG&wHAq(zwEUn81TA7 z7xD?(!Q*Zrt%LICn%$?|&LFbQ$7OpP2J7P-VBL*6zfSGAv*dgxcyrly0SZoCosV4qkQ}O8T zVA0qUwYRyb-FG7<_isCZ*G<<8SR7G&zSRKuqL;AwR8;3^$L-?Lg^RDcxwm2gk#(*= z5He5RqKBtWiQfU$3D26l&e}$q-1i1d>xro2!Kfo#&RFrHQE8r%+X?@RT4SWJp=Pe4 z_p&cJWt7K=L?nQPwDdAo>{O96NdDoxNV4rFLMI_*jGPAzoaanlxRKr`7I|(`KCin{ zwE1zD-U&agXF3=YTrOXKa>pZa{oQ}l2hptNPO$yt@x$VgzwXvM4C^H#8fSiNJ+IxH zpN;MO$@xqY^0((~XW^)rP>>RHWOAC2>V*q;`CH>MJ}+C#*C;0#b>tH?%Rg!_f%J$) zY~VWPM*j|K^T0zDac0^9fT}(=wP~fEwfcCRFJYRKRU)*!O%Y_kMb$|Mi4OT+N)b%-u)ZTrpXx;a49`O$|)pq@&a9 zy}pkY6XXw;JZ|0&qY3zGp1>dQJ-h^G3=89>SzubA*9Q^S9`{ao6fKv9_xkT2fRMV3 zCm<>@SVCwVTXwDz(1{JXlI@#;38SA7`=j~tzJ4B^MZjQR>5H%;pjytKoC@bk<N{JD{@iKd3`)rVQGJP`?b{j#^8s_vsgfMJXr|EV%`*z`=*@ z!`VIAeqNWt`e_wxjztLYgi_ylB)K_G-}>5WrqDfp_AQKh?PD<3 zjn&W=Yg>Knw5^1}8^;xJFAiLuTGXCE$k(!!L+tLjsISxxL_0@9tpBb+<}G}vJs={3 zI%`=uxwYU1doM%&H!T*a2Zv`S4X3(7jb%=$Z)QWdxQcvE{g2_R)M>AYF4wFr=3LxX zZk?{joq8+!eDH6|0>6bheFJW&vJ+HkTza}_f%x_J?h;KfDOzz9gFhyXV)u*-2&U_~ zQ%puXopa;$j%wSHTU;H|3tQj(OP9Ofan0}&ahX&jo2jKved5~(zL}Z)^_L7SCZTLOk%yzSMk^p?}?e;ii|1EnEi_f}%~R%alL5 zb}bK8^>_BoEs;VPVHI3J)xxB2wO8Yo|1DzbVRpWv5pE3Cc%|WiwtR}jUoG54fn7teF=elwNrEqJMUb6FYs+ zvuCdGGE{I;DpMghe-_jt`PDtJ0$?moSoYq#2`UwO(LrB(Ciq{t2q>@MC0^LfhIo~h zF=3GMNq;Mr_Mr`dHK9B?3|aN~e;9iUs3^Oz4VRMcmR6JzB?bv;L{JP;Qjue*O4|8js6RoqpsC zp{KS=b)g|KcY7P0pwVT?!F0dWt2EF7P10D4`6lCAFuu$1$2${gQU9VHzx0Uq7MRo?}p3-O; zst#L>TibF)3^W)r`bKdCLCkgSl9CB^+0b6Sc_Q=_wLJB|+XL zpjEZI5rNuXsbY3%ByDXq&vKTDlAL#dA5EOGlZOn^R%iNGg)?J1$}x0)tvxAn?9o}s z*@sPcx7jUF`E1lH;Y za@rnyZDNj_=bU}N$nGNtB-j!h;h?@@6RfcY5_99i(?qwwMI3ik{dVb(KUu1Y2t8hM zo3L*@AVd26GGWCLe$+03vvS(>>fbB?Ff5)Hky;(A(+W{aX}T=*nEIMoW^GOqsAMiY zBlXb`q8y4uFR>q=qFD^1oVxVebCT`*ghqKX_!URKCc!g_aX$J8nUJUAeU-ND^50@p z(Q}H9PT~8(nj_&JdG!lDilc$kG*7)$*&)NO2xj-;G7&UTyiA(NIMK?A z=3^sw3eFs2Bdt)xVGPQRw(x^3qbjx@3C0h(A#b6VzMZ*KxufQDU9U6T$w@^Q8#y_ndHw(mSA?!med+T9Y#^agJVSqs427{x@@hX%une?D#DN1GyfSj zHw8MdO){5?uc%am;<(w6#*7rU)pAJerv{8(&Ieh|^2pJmhMc5sx z+z@9;Lc3LqZ~J4+g!X1#$Zz5biX&dJ6eTC@FLX$(9=O%Bl(18~*Q>}+i%RY|d z+%wddL8Ce_hOke^sQ5_f(BFG0rs{HkzA_|*1sq#$HohakX1j?AQo4g;l~g952RTx) zXS?;b77Ca-0oyhF?T&K6)dE4DTBenhww4NOi>UFUSf$Ut-iDGwc5?Ibz2YY&sEJBH ziCtPLdAGXd;nZ8YL6RwUd@pH5Kf9~^+~fA$a3uV>G2yb?o`n@7excH7``Mh8AwfxF z7OIJiHMobgyUMlq3wH< zm%=@0g2|ZIWN4K(g0G~Isrz>6p#_aAlhP>aeku>$gio30mOI&|Fe>Xa!UM!Jl$w=h z<4TzGZx~MhSEmv69@#M+Z_maF{m}dpvUNW3?2ID$RTjz^bF|v)e$Q=ta}0m`=e&^NYO8G zi(HDA&n#EaMNiuGl`;Pm zb#j{aVJn$m`xFTyPLm9R%fVrGLCzpA+m57vo>9^55~euQ|TyH`yTv^mXz(C z>Tjim4slsr*O%FCJVRgL1m6#v2Q(GIV^c=4TIjYKRCiCA_WP$o$`chs45olHr^SkQ zqa!O)x%{6)5O~TNC!oVkYJyCFfZOeQHHPkptuNHeSv1^d5f7JfJ2pIp_B+z=ixB%* zN$YD?2z|rkdn2Q+M`tUaANlZ1{ntFnXw9jX$Gs2bTBH7jtrRv=^v&nWcYE~m8pF8@ z*(xmu{9WEV)paN&UF1ukZS!=YFP`+QzM*CL{j%it9ozjuN2vZ(JkXhrbU+Dbl2zTl;aRZ31f2h&V$o!^Tdu%M-_N|c6l0<1wJ+?h;4D0I0vIm_Hy5pmq& zF?G%l2utfA#%T4s>@7o<2s`X^N=(gQCdijSg)ok7e z(-E;M=W20X_xtr`bu*eKf9D{7ZDSo8nAfNYq?8Da1$zBBeb_-^SF*zq!U6kFcGn}SDAF2mDe?eT=HKFZ0Q@u9p;QWB!Z94 zXmY70CLR|lcsy}Yo_tpED9f`@odUVsu300gqgX(?!#~37^Dob($@zK6-IwYxkjK@#?g}a;4$wf9x5he zqJK1CY#OlbNn3UxOcueLu-J)Xz6?^Dn3=P(GyQ^U@Wlgp0?3|fQM09>;~snvHZmn{ zil5}@e%?}QnFW(GEfX&y<(u!;mm=C;oP)01F|AnDpn~R)lJ#L5s3w>{Q>3-=Alspr zSN`KlVvqjm^8@7@tfNnh#=4Ow-HSNB~%!do_SaF+N5Z4mu#U-BS&Po z18QiRJR`0v4jBDHeXR?%x3RF~mrvTby?6*yKDY;1;7%(};=uBLfR+<(suJTB@gMEC z$WD1hdqJp|HpAIH?KI9F=kJNDvOwgyvnhsr3TMfEa2PO1b4oD(p$&9*t%PwM4FrOs zHgUq_iU$oJgFdR=&GE#TGKImkd6|ZEJ|3s9ReBX>`i}tB&h@z*@(ZUQ8x*o!%<6qDGKFoXQCG~p-EVVrDn5x9PY4r^0cOt^2NWLpEz`3(aTRM3u`7E4 zzx5RLWoFEphfza|j8YPlXGY^faLv;+hV`xGm40|R5WqD@4-v z@#G$!=-9SVi5ba1M4pK3R4wovc_JnINSR)jr_${>Iz%O$n6(R1 z{=d68g^NkIwn0=4T?{nP4WnJf3iHpfJpku03r7F+G5?&uqi*3vC7$9<8PCtDgu*cY z(U435)u$wLpF-3RS}1t6rwJ}dF!Rv5_0!hwiR^ZAi<=WO*d^f^liz~(&f{rP)9QV9 zvn_eW%=YBz*NX&|J1yn@b-6S8?s?^^G9&|Jx&=Q)LwEDssF`u!nnc_uK#xY>Wq;xW z*Vs{l)yk-_>`_{E1j@bOTSXgr_~(`0DkF?4KylpoT-!;c;A-h!=dPIPGwP4>PHh%b zVcki%dJa87Mj=G$6h!D^TyR2#nb4?TGoXchz9C(z6sjDS(+dZ8A!VSr_7_}m+<7@D z{-)|klaX;k#1UF+%LbBs>psdDuik36VjQW6&pb=hF-^3V#~t{9D^r-s(jr*P%!f2c zum|C9Z;p$`r^U@O{e=;^1C>Iz0@ub~lCV5{3T}LbnV1n`DKCfd?GHCQ3ej?0MHKE(#P@Skdo5 zv%odiY8K`No=(U2H94!9IRa7y9uG8__r=!4-*Hzh7!H&(R~5mpc%F6J{|`883i}Hm z+i$1yy{~MAIAo=?qS+z%*YwMqQx(~Ij68}-C`W0H7ux>~A(G*sCi>5aW_IzvNb~>w zC-^+H*Aci6x(NOg7UWQ1<1a}hyA_W7i-P_ELjSklS;1@O$n@^I{s;Mc{Hw{K`LW(b zuCjGZ4K*Ri^Nj$n@s~d($We_9|B@v>NEfhi`syB)BxQ!obDJfh#5}M z@F-$ab9SGa2<8Pi!%kPUGicJFDR%<6Mza`D!=0?X4iaZT#@CKj*Qo5_Mv-U zqvq%fuS>iyf6st;pFa1wIJU4;zz_a_FESS(DR#1a003xrMa<+uvk$Y~@13`e0J$_0 zziUGizXI(`Om}sz&()p5cUp_X>;ab-7&B?LC1SX8FB&hA*4yKaSC01!?~C(GEPr`pB#=YD zKHu+sW$*Ndoxw3Xamx=U4h|j`flAet^kAlfOKs8JCd|=y6USHZMZ5JVfhy11jg3*h z9ZiI7WxOz9rkAB_9~NRKmq^GS*TsO7-@G&3vv7FF``SIWtU+!e?^V3oabAg#C-u0Q zzL^fGHjs<&&_>;8&;f>+1>8$eMz5rq0~fk7Q*cl425)%hCk{1GC9i=I=65Qv0$#x+ z&O&s6Ba1Kcq;5@2o7VDw8H0A*;r)J)Ff{d7!G;2&&b~K$4mNfZmhABQhq>j!mEhESD&}XGi2bDPa+2BCmhfpr#czfm zfCr(*>$e?`HwH?C%9+&`b0seGV|&Z*AgdPgEaLUfGHZ5*O!Si3LXKP9Y$LNOz#7&G zZ^EH4Hck(uF5tAE^E?@KiSzVn6Jyp855tC^FWZ6$LXD`0hxG5i`--zNxBwr?!W?); z-6zG0h9-J}{SK+eN5oYUcYR4b znxIJ@>7R={FcCkvdHu~q?9~)U0--XZ6cF*v+j35Me~o6!Ntb_(W?#{0n?cCsvzxAQ zuGE9`2SCsoIs}f`GSHulS333S@RKDF8~56i=~zepj6JE>xu|9XVkAG1+jdpXaBJhD;|tLPSb8FHO%hr7C9=BTcRQ;rWH>Ib z#TgJYCVqLrybofs>BrS>;mEMp-2AJ7gM<2T``hK`ECxwCc=j^)mY3Wk70J@!O?6Gq zlI44ct`puu4j<6yd;_*lEgS_v_u1?ladE=Wdr?bP2k?#9Zu^>H@yjH0DOm!oRZ@ZY zG!*30vlWnX?@RZ5kR(^9-grLxwgcpakNV}lQx@EKz51m`hIAmi|C@01bClYnTI%*W zxKc6eFSO4dPE+wC4d59&`NCe%(2eyCWZ%>ny#M4uD|HH3bN)^XG-Qk-03H#eOU@jIb_XKqJML+e87v(JHau z$y(EL%a~fsQj);$>GwE8m4&TUpG4q(&BrIO)@1;_#wzZANY)%;22L-!<4=(ut)c8s zsUk@OPyZ4S`c*klX(^arI0crsOazM#3Nc>JNdq#_z8Lnq_Ss*6OjDt|9^@ENy2F5d z&{DKNolvk9Q$`Ab;QPX-UKd>bxES~Rp9<7Q!lQ7#xoWo_X1A5~2^VfxjD6<{lW2a5 z2s8fl3)pru4Q5j&<0*OB+z)}Nx*8uR;hZP1g8@;9Pv0z8;DMFiIpl`vRX zMDv2-{?_Xf;0rdY+9657e(zm>W))wO(0N)rn=cuOdISGd4GLL??KSfru=zc7!Y~j7 z?1!>H3!QYj5$G_xxI;4NLJpmXTkj-jEz|H6aq?Zgr!ASDUA}gecShuD+I>s4#4X0Wr4^teq2S}9t)fcA%SSw?0x6(dAGPR8LbFor!(2mgKc8uFg2IO( z<;-J|sDDbnrP|O!SHXtzT4I~>$W=O-!SR~S)Zk*$Q~%*xP`Z(zok~u;6p7e? zEMv8#TkFZ7+nQ<(TYD;BUX?Nv9a>%)T@PRT93t>z&8g{knR>rRHciEK=*?8SrXr81 zt<-7rq3z;$9kOTkW?IQGb7IgEQENKVF{rXh)x3z&368ETNCNJ3#Moa8*G)gF-46p; zR4yqnyjGK-?X8UChySs1hD!q9-MWWq%z*l145Fa{%OxXu7 zT}*)ScAY>>i`E-KihoaUzj?-z!X{)cs(`C~BC}KpM{74OqrT<>=jK;s@n$g_KRgvi zhh%2E?r#rF;`DMp8PYxf66h1RhmgM3^VDZp8R-27hGblR_nhvq3|n$tt@RO@^U!am z^5W*?Y^JpN_`TD|m02!TNs5AThIN3j>5~tV5P>sAdMQiA@`KlZLBxUpRK*|K?I_k0f+b=Z*G6WhQNz*=J8U& z6N+w5I2~bUR!~|Oo^mnqnITU9!bHbBa3?$5e&O)LJu9N$CS{l^KyOIh(01@h6=tp~ zUyfTIar=vn{==vqDW|;A$vF_RyJPt1VZ0X`Ta#_Z)@d!U6&IVQz%e3pqA55AFKn87 zEZp9p3G2}%$8Zqxy?@Cz=#uu z)82E(hod1mZ?zCpfGLpp&i5AmXL0b7)E)1Ba_LlC!Hac z!NS;7{CcovDHt1$YK`7Z4_JgZo{#L)R6K*xJ>RhT0=I9tBmRsdgu>mqo~Fy2ih6c= z-gcy%2F%)|eIxa;uEmkAz9Nd%HqP3uAGQMLNg~h*PqPUZ1L=O-JW16|Uq0tV&Yt0u zoM3l);)`5QDQRVTPVu@Fm&Fiyz8ee%y1>a{r%%4r0M0VP4DJ~7f!25unC%ps0Rq8I ze;uQYA6@-4!G$(v8+^S|HPY~khR~D%fhtw6fg@IlG|Rpu2SmRaFt(rmj+c_g>k!#%8-82zjv>4IJ*Q#kCn3+VYUb=R#Y=LMD8NP zT74$4gq5a-D?^l-jaD@VFn{>xh04W=(t4wnTxdz&**(XXH6dKAR;WMxZI$k}OtZ$W zT!#%PirZ!E3~?3k+6gNegqQtsHQuM_N|7I16jZ!{BZg<}kE7J)v?x7z>p~EmX&Gk~ z^!f~)Wp0SFQ+5)6%(U(U3AV8NX{vUh>NadbQMfeUA>-vYLfshM&PSPCI!;5$TTH^H zBnp3^cBK#7a7Ec}=q~E^wQU&lE@{F&OS?Jw$9|EaWIu((o~4h7 zih1$7a^PQ%g8lI4E-hc5^nuKc=z#~Jo=tUbEuFLz^`_+-179rgBQtaAtNnylLVrf| zeaZ+zN)x=gQR8@Ew0#k!Z2YcYN$f!D@p@+n95>d;8B`vMaHa?q;3e36k$My@j~ZLe z5qRt2!v71er!1~nsFXs@_a|O&{K@-^3HRe^ffNoTOEJ4JrKPmkiVVZB@SgH~t5PY& znwKYy7@nn7;YHf!BMJaBiFo9d?LTZqZN};Hn!N)zUipd)#`m&WEHpCt50zI4dBsOf zw0`&f>UzFFPkDcM&Fdz1F>cGLP-Uu4bKTEB80=8)$sK)D zMlXC7`j%&vyi_5uraTijGt#E>uE^orq{+svZFRLhA*AFHb4~t%x6e(q0~~=vG=TxZ zFMy!%Bob(;TyiRG9S6MG;W4=-AE5enZs0($a;F?UW?p#xV}}5u@VEDXp3n-f_pq^P zEpZKy?2X1^czfVIveL9`PeqJM z(pc`R-Bo_duCy(2J$s(S=(+XtZzG5Z{AKKPWP;;PluX9p--vdJTB{`Z@Cpjw4cAGq zL)+WNeiB~!Tm{iH5;=R^@_uK$Xdcs#%nb!;oZG#hvQ~hlUKS;|+!`uj>^c$>%|RP` z`UmflT4Uz?m(shO75+X8<<#sAK4|5v*g7Rxp{6Cmp1`Aa7vv;l7a!sqxia)qjCnqC zg*LTkLR0tt)A)ct|{xWn34#&<$Le}&9vl}^qR41a{q0mP2Bd{`5E zMVLJp%O9ji3G+jlohAgkWqKLH$)m|hYQys{+Cpn1beyKq$qogDgjzyM{Mx+;4tmTY zM(^4hZuMeC`2LYm4WY&`J>n|tw2i3~;LUO)~@?Pg|B9C6@+EyNIt@ibcC?MA*OM^6p% znd8a@IxJ}&E{!pe&X)cQ^-(b@M|?9^T+8<1H`nWsOO=K?W%vLIw)B0CNk=< z4Y{G_v`$U5*KDfZpR~<{CvOZ|oWg253W7o<&}|!3ESNcK^A;YQ4^oF%D9}2oc?{^) zDH??PUMx97m6)&&k9@j&$DO=nrp}DBEtMI~cx3_jhIuv2Ib<>sP_noUXg^zA;7$(f z8&IhblQlW{f_K@uU2`M+5TWKpEZ^MynJEtxDhljP)P(X1QKjvQT9J0dtLnVbGK_sP zMK(6ISRJ(Bh{RiZVeNIBlrGQ)P&L8HKjTjyEx?j&{GwTJIimX&Id;#&dkTMx8N8EE zRUFl(b@I6pF?g$4(pF<*Ij-@(W)FXyfi!(OIP}&mmWCfUdjvbCDmR?jH>o_hAq3qy z^X-DDNDOL#!A3+V`B~uj&y@hdjVF(yU!ok5#8VnHk#eHVTJ1jQ_6)LO1qT^3SxXNn zS-Qwd%%AE1k#P7V23{Td+}c0RfyL}A+aihjfNc8b)g*B;R? z4`R9{nEaK4RDgTH3>f?4$$MorwnjuaEV#8GAVKvW=~yW$Fjl^G2{~YY-PLX0*UadE^rRykHmfLwKh8!BD0eyfs0TI`+&LgA)#-j_U=Z zSiYp6p&v`pXZ_lkv{dtY3v0IMHf&W~cp?_6JNn7KG8W#)uTcFYJC>p&&yb?Q?w*r> z?#~>tu#|UGWmum@mb%sLg3es;mdzGj!BEmVn$4Jy3r6FGBF)l!LtdiCatYC7TX`Jk zxJen}{P-YT0}r;3u5ACQ=3x5ie7YrBDyf(pSuZ)idlP{$i{G7W< zY;xWH(Wy-3VtP9VvruR1BI^F|KJAGr?Q5j_Al>j1N0B(B(=OIi&O{jl4)b&r5EpP` zFiRbN3D?oLLNkoj0pZW%=q{Hg?-`Z8UfAZgmz_oSe?YW>)eK+x|1P3B_SarCMbfhlPC9R zeFPes7l_g}>}p6v1T4BRuu;CR(VHgH&@fQo1i-4!&_od#&LQVw!1{2d8;Gy zLqWKX=h0|kU_hRVnf8~CMaJ4}+Sal<`KEwSb$jli$qmNIg^$sjN+!72`t4RPo6RVF z#GEIru+Tgv2wTe~Q9%vg%En`7yxVS84#4r}Yqw#wA_;e^DSAo7ep9nywr3Puf4h(b zE@tZ%JZ7tP+FN+cIA|#Th~_dUUh1#fZf4<23Qo17PAV$TM~hPD2{eZ%ar2-rMCkeaKpAUr$08PDtqVoeCTJF=$gNfvmlYm6{EHY|6) zJ4q+;>U1!*$P|(}lGx&ImvM zPPB27WyM!*A8w8@k>h$YdvdC%OKvBZYoT)MY!EBbzL6O! z{DZv`c++IhuA&73Gr5f3PG&Y#9!)x=U(%W=usjwn0RodEkIyB!`CRi>p1D|otq&oC zs!97X$z!pg)cn4#*7OrPcJI8&&mPkVx;+ugeg?*S#DY_wRcsGQ z>}F`d(DzAl=&zIHlG%gcUjvU#-j9C`a%^q7o@-w})&KwCHh|KeL5O4d zL@8hIKXJ_Sgm{f_!&~1*qwGIy!^;of_ur(+;|0#CXG4{aLTI6A&}vG(kGkQHPjbYcG&P zw}J1bx}CeUm3$i{jU^9VK;rtgrL}t^Yl<_-D`;Xjd->R#_7pXDJ$t$c*=&%nc03Id z1Q&r|Ub0kAdj;GwHBd=99pTJ0h#}nusls8=L`U&X7aQu0c9xbo82K(k(?C9b=jJ1D z1qjU!-qM0E4pSaN{H|ROdQddv1)AhPLt1-p+6_wpF>Ml5v*;BN4eT-lJkwAhow&qf z;L?{~)POHnH=-tPFm`@W+QBsJ1Qh3)h+Rs)aA$YxoI&x*wnp$4zqy91ulfRso;!SR zDGWGt4>aWIHxY_5s4RLIWBFi1#jPqGPEVKcwdxnAV}VB&HNnJCG})u4RY4t=Y|&)Um%=8EB~gB4Sbz9`i0qBYvrxPJSzKTrGG+n44q*mM zmL#Ydc^2dg*`^vdR7>`OI2HTV6-Y>^T+tQwt5ynyL&!h)*Y5^;TX}nfqunWQzbM7F z64bRN0r2~SOC0fCbWdvIxsvbA_-@zT>J3BJkk)VJ?~!x?DwXs1=73?Mj&LI7bbo*m z^M9C%7ig7~y8pxD;zCB^T#4I{;rjo9RYEaArkbdnf=5}}3V2JLE$Ish{prb^+{pCrhj-~K7}%L{cN(Wv39Wz@ zW4+4lY!1NXbX{A)Wx3>;GuVLZ>)_D?VjP7d>mVlwL`XQjyuh10Hv#4zWoS)>j8*2p zt?^|C#VwZ;xt@4HTfTb*;G?;+lh}e|xq_eVkR%9w`ZNChm!MR-xCw$T}C|pIyAbK1-59>AA|;tHrUI ze9N*SUwssTDz-?0CYJ&|_$GK}Cyefab&#C!;*Wy&zJg0L#RZVQ2b5<)?$CzB^;tg( zsy*}qQ)(;3|4eVe5DoLXX1reh1`gy?Xd~qU9(--5V9t1LZ(xPVS4@X}7d;I0?e|#a z)sB!<_JFWf>gNuy02GB4Y;{{q8z)-kRD;j!Eq<319cl_^LWVEaG%y^?)5HOJ>aJet zoPU-8o{kG>J>(u55}Z^_VUGx|(Bo(}sXLn}Lub|XF%YF#@ zkcgk^64iqK+x+Zd9EAJ?X7ix))v!yzyJ$4{go4+8^ra*YW7cRoH|fYN-9mo|l`OsC z!;rXGAgO2s?OJA1mNR-6{yMA>qA`yL$Vp`cF@pHVG+HE1e)b>HTb6&qiG<(8O#r1? zpH2Qre|UTkoyn>eDWJg21%g-Kj;j~}FA4*r@;tX;oV_V`%*nUbwJ(v1k*>6KM(-a! zGgF|*YDdXtdhYt9MeWcSFwcuT0$xZL>WN#sa_&ckIz##m zG~ZGCeu@*Z|FOXZ)!!;lCOT~CWF49tTQ-3#1>aJb34-HXO~`s9^cdd(-$r**=~BOb zi@5Ap>YQk;E@9pDOCa`+JT=p(4!vnk@EwjfXAsq{NJE_)Z`B|B&Mu@fk_h$hW%p6pFm0OEh{eU3U17U%wj%rG|A0k9Dz+M<*H3ml|dej#3Z~x61URh0}Di==hZSIM8y&zCd%b$Y)kZ6E^V( zbiv>sWTV4kFE z{Oqv};u6b!wY~!bLbB@5zhp%aVHzv`Ort46iBPDVrjfT3;8>8^KJfB!jIxt?T|XZJ z2c~bHBhk_ioQB0q;6IaABFrC9`8}*0c+U&6690yGFsEODls#irH6jLtEr=IY)!UP! zM@A+R+nGvK>9jai_1b39uT^nauDRO;>l+M1~xl>o8= zQki3C3Ly44I>rs6dp8*42xu{>SD?sMD2Ok;HYtyiyK365V70Q1TGPeJIPTPpK!iy@ z@sIO_9*zX?;9E?97~7e$P<1MJueIV54(yhX_ij^hHM$?wd5cpCr)Wb8AS&lp6F6Ct z)lX7zNfjRu2XfjnwRk-X^_}q+*Duxm_+!)Nvoeei2BqTOXAfmZgt4^wJe*uf!Fp_f zQ^tU0>(P|vJQAHq`|f-ToF}|g9jQa=pn8Que5vpV0cc|=Zf*vx0w>!In9v^U#9^bG zV)GEAd_g8o0TJ6g0(f5w3nuh^lXr!6-*Z&Y``xc@lFlJ|T|R!i&9a!#-LV`jUJcq( zUP6lqI~!ci=u!Ad{4-QJGa|5(%?u0$w=H0sNgfJxiY%kUUV`NfN8m(z~D%Ib|S$uzkkOW^C; z#s}_2^1Mz7{)jG4xtOFNK?g^N1ag_Xjd*sn-m`&vYf@OO%~^!4VNex}h0(F|l;xxF z;5R0@#G0Fsk-TLhTv9BgPIqzg57nAa&2JYF(Pb_uU!!GZq zQmR_hQgXQ;z20)jt7$vdTQjj=cfGCxi`?6HeojACFRgv zB&zXS{Ax9eclx|3`^$c;=4-A6Iu?gAV~v`V=BcjJ>i+7pn()XF=*u8e)mCxd zWQpBd)1p>e?pVak5jVcD9eXCafDP(D!h@g^PU_{xgXA4kjbtMF+L8$aMBfhZ;PT4R zpR+aL-CBVfIIE`cuH=Mt1(NC7hF)X8FV^d8LU-B)F?zTxc$G6)SOOCc(R0A-IQ4br z@zT0M(q?HH5#wzSAN25aJeY0G?oDRgc@C_kmh^ol48G^Jld8#B^HSkZy5G^0vb!?( zehEst`0-8oc-qKTr%v2lQVNoO3}z;FQfp>9=x zQ=t9FHruw55SJcG(<(d?aPc^}|Aq^x%1wC6rYbIS*;ATjpx0X0$vvW$U-%mI)e0#m zIRM{%z-?SOS)3;*-}*M-=i>tW8^deD?WrGt1OdVX8IkHlt`|YpEaF7Zr?A)&saV3# z2fYe(S}NCiY@>IJ2sRn!c*pmVOCrHh_;Xxr0|kjFWy`)~UReCG{S@iqk5>|FVO8)Q z$(dYJ36nm2;1zzL1rAdp-6*ed(UDTCWSX&hb}vv}__fRPpCrvsdOnO?$~Vm4Mgh}l zyx~_Zr#D(EEh=qc5|w|p@`7r)_`YgAkR>IA)9C6&P)iaDQnZl8qtjtz5#6NQYQp<{ zWcA)vF4%%1`ggHJ-_6oZgS9bP;n&TPY|mKQZgc z!wJVI^%Ty%+jJ+u=LKOoRy4oWV^V+r4%SG~*M<6(sT>$nXecWufh9&8WozTADIW@; z!yiZRz^!@M9>Jz|;Cd*S^C|zf8V?i@u6`##;>+@|6Nzx#+K*738e+ds* zK;=kKt>mEBu=7}VeyzZuSKQa1XeNnl--wo)QItppI!0h$(QFK|Txr;3=nz@D8dD0U z9dMEop+L%ryuq{Y5`JFsb2CXO1Y$0ZF6Pi`s1`ey)&gR0giVxExeNjijnQ#z*(-y$!hSYT7U;bk8V4iB0vBh zwS++}j2oiA-hJ9q{X^s?LDw9k!sA>V8;g!X#1Ixe74gY9hWryEy*VqKHWcW6yohUcJ4sh$v22 zaSm8!YVQXr)8X=>zkuRqn+Z0UFpw+Bio>_UtTXE2z3?5N*kZ9Bl?;Et+Z^SaHK9W$ z?gl5*V@BlNkP+Fg&NL_IDMkkSKUgHieb}z}lSmIFDaPzsXL=Sgztz|pIq<~U)g0tw z&NB5T|HG1XsnQ7Uq6o)`rFyV8y}8^h_GnBStqCU^%0)~&_9aOpb`U|rqGLz)eJ+K+ zEj09QAz7y@&puk1_iYHPnMyIr2aQ%Hd7$1LXf()e)=mFA7~)FUHb5Bpr2E)uFNwp= zt(*MWG~Uuq>)t1doB~(`&Mg&K^sg^A7}A_wc+n#iu&|%eZP}}L&11xD7r)3KM3X2} z$dHjihU!2?}(TU30gY9bq2hQaXV|1|NzJM|r1ej_Tu= zUXQg>Ze2;w420+cTAMDAs)zGhVlu?Od0Bz|J`U`d+!BLZzjmj!Bk;ziYWdA1j)dgA zBlU&Fu%Rw9w9x}WYbFp^50ibNKot-&)m9bR6R+K3Q4)*zx;Q<9G$x7VlG3J57q7IAyHesF6Po9v-5Prr@+ zoOTRLEmnl9Tsc|6_uU|;P@5iK1x!=Ta`q(L9uO8%Ij9|D`r(n|O2?%q6ro4-da29| zI%HNg5OokZ^vB_(E-oveQXMWpc)x5L^C2f;IR(1zXeJ;pDp77bK-7@Gm{YO$kJ}R> zb5lJcv3^}HVdg+O{C5?!OBLw|Tr!)UtEnbhC^HolCbJXXn^|hLwWd{_0fTMSm!yK& z84AMZu^S%i&$pI~>%X^cfY^CxqSQEDIusPGR9;p@h#>7-$g8m{T(NnqMxxtW17ify-{ukqT(cLj ziKyFOdJxX0nMqtYy_u97XF$j(6MHiYH1Mc4_07rF)eK!#L#dAWUPYOgBMgtWt+kY| zI~ICSJu?rA%0Dx$nD1Sd0~O)(NfwwFfVkwI3z^j2>tNeXo#K#p^J1NhOLG+6p9-j(QYoU#7E~X;L89s#l0B z71Ucc>c8JfplRdIye7|!{(V6j&A{?0T2pZEJV+ZFy^=R4ytNn~J>;!UJ} z80@>nQsBu!Naja(PUhR4hbD=2$yrO&J^iGLC%j?-3p;A5mh|(kB%kgemex3){B!>V zL8K8+FGkIan|&F>SV*hlOAQanwIPY1009p_6uBf&_Oow! zP+XnyaSa=r*NOav4m?6=dl$qQB44Dst^d6-UbVfTKMY%H`G=@DhM1+o*F%3wl?#o` zGT)-`qYQmDn8L;YS!2w&U4CgZChDAriB}x_F%V8m$lsGsh?ho{+IgL31|?2pcm_BwR1@zA3DB&+MAh-igF}w^k;Ue? zFhZf%7VM{kIg~7hD~SQR_ph7)PZ>#i;R(5%bpB_k`DgFh$3;n@)VyYrRAq214Fc<4 z3zqJ;J0fdVOWktDT=P#M-^*{v_rkUXJ=HB}=EZq(1PAK@Kl3@&8LrK$^FJnj%*&Sv zU!ke=z zDTi5i85c(U1>JQ5aNH!aF@`xFvpE)(F2t>ouFXr*K8DF!TK;Yd-})e|7g69c((o&N zfw0bZleE?U`y$n7jS}V&S)v6jbnmUO+=BG`$qLUOue4RQoKOt?ImB9j&kYu-g%PqTyG*;*aa=~ttjS0(!$YC*p4E40l5P~6K{kkRUP9_ z9MJXDW`GMQ?&xK#0K6omg$Y>&KM+R1QsPe&xc|)pIKBcAHjcNO6#1`rlPQsbX2G_p z%Hl{W-&6`YCjKhiO)?Paw-)9+1WNO(1ngHdI|M&8@_!Bb8SS#Fw)$l^k;fDvBc~cL z2Vs`rzl<&qMo9 z8SVd46{f#bB{FQNX%hc0Rl(=UF6Cu{sEP+oEVi-!{IzwPVT0mls{innKzo_onL*Evx zA@t>q-~itzD5)p!yPObuY4EGz;@>X=|Fzkix1llF6HpTN0H;d1gvha*887&Y&U%{e zyf*}P(gXfKh)NhlROb6Q<^DVP5Be1UnRVyqdo#=z)<0hvlhcbvcmb-t9aB-$Ytx`@ z#s(B}bpm-%>cKtD0krkWLqQglS=8)FjfvRi8>Y{ZOWi+4E58@)F81^F~e0 zfr40>mO=C93&G4z2R*zBmL3ex&mq|t;yq>zqX*M!#tO~o z5|0TWA$|r#l-0EhNPtSc?Hk-KzC()gdqS310IwACT_&xG!eGBYWu1XcqUDtHk=zXz z0)mTPYp*59ZBY#szw3S9{G3uMd;YHG54&jqiKRp5v~(jxmy~VD)IS|R)Zy8IcK{7d zY8!H%gfc3IM*|_O(V}r>K5x_Iqpe5}@Y#S)K1EBsx!*=yaA1(GGYt$m7fD;~pRt7v zxfVsHuskHrwx4uZ4g?-68&$^eKCm6@w>Wu-2h@lS1}81xH6CUXM&$-fBhm=N#383g&QjaUc{Wi{O>hs zR{L=DyfPaeB{{^OB--$im2(r_2Qqpot%gamW1R=#-*_#@x{Lka^&D}RAYN--L1=}N zo4^DZiBKp|6Wj**Xtdd|ENyiRKC;lfY^XNtx}*bjYZL@xnfc+&`4MJcc!17Co@ z4zQ$L0HelA;ND(ylZ(wXmb#8-g*Uh;Z_OR%JaT;lH){kR=2!k)6(e+a4-M0B=r|P& zUOA_=#}a)#$FqySZkT8%)$V`IAg4f>Ke~F#?e&zPK+Kgqc1ZC`>pakv5V<7?}MTsQ1TT`x#x;O$R$Gm+Hje%FBJA);xJ z*X@rQ79gq2O^>5MaJz9orsNa~^H$a9>(vB&D#=jH46&|A{rw93i1KLK<^G%jbW=K3 z0rOwi1F24L@ZU-2av;%sysZY-9$#4wC&WI9@=k~u=9WJN3odSazQs-(v76rP|XiQx| zV5!6LPUJzKh=N(FwGBQHeKd_hQTQUzUH7SiM!lgxNb4qmn{E)K*!A9gu*9&VeE%KA zfc0g<`XXr5ney;@PwNbX^k~J11B3tV%#@SD_TRnWBilMBHRFMjQuY1mmke)-AG|mp zse$t|2b_g3H9#3PgMy|K?ew*Y00^>?WB$O*3v2%iYZ{>-O31@ z!15xVeKwCb9crk)w9fiz)m@y8XO|~C@_5EsCtUwl2z*Uz$4y;fNF`n@1n1&A_%Rgj zV*DcA1^DRgl-Hl^*bUxtbWF5ie3pBD@>canYMv3-b_l`)!oGX-KItp2plwz}8U61A zDDcaVl=>%Rhq{tRwF9R0u>#DCei`q0qpwQ7_)n;?{tvp|I;yH}-yfzV1nE>7rIGFi z5mCBJI;Fce-5@9}-Q6kO-6^PaNJ%#vese$PocoS@-+TXJIE2k+&$Z_I&QFr9PW3Zw zod08ZA)g0^7XnrD@);HWQjU;nD6C3kl~4uxT<@M|$7@$cFberrSP#&#Uu(?>LAB!Q3oobc*{nfl zS*3_j9kh~8ew#cPlMC&nIhDQZJIdODzOGY{dMkXS5uKE3=d-UPkgA?ToFAk`@e}_Y1ApdQ<(o4S=0Y0v)S((`iAyMKdN@GXi z#wWd)Qh3MJX8yPwd7vIDHw3^@9+Y#wTTJ@J=r=HgfZ-BP-3(Z&MU@ry-^;|uLZ*gGD54NG2WXr4{@3ICJ9Ud zw12h%K{L%(C+A-98P1u0@1evn2YyOlKJ}S7a~+1rbr;s&`V08^CPVOXp2Ij>wP*bl zOK9GMdz5s-2w8LLI^~|}!1m_}SP@HNkmjs`Ltl-DiWq*p1x(l~+CreBti;X9t{EhG zI1dU1*_*-GqzGx_FrEVt!>Xp~jo7vV(#jmFL;#%vif~@3KnZ!6QKUZ(s3bqq|6Tl^ zr|=zNRBO_dPdeygkmShXDHR_l-~Dk(%~?~_4+|`cUrauMqjt2&G=gP#(!a(&&ZGQikyS2^3Ly;5*eYZ1E%Y=V znt;|N+H;v#sM9*o)Llq(G5qO`y~De=Q*kD8@nH>gcT#MW|F|Am zDCsmYG7zawNcgBm$2g0rQTTCbn@+mo)vP(H6;KAG}ri`7@F^2eJ#%CF$bu>!4lTA*JI{}XgCm9W_= zn_j*oKMp+TSL1(5^VdY9dq{dTd{VL}cyX>&*f@zEftn6|Wv}g+ws5RwbwOi0&1rm3 z&6@l~kSY?ME=qzx}=?O6B(dZ0YigFC)j5T zYfsYig%PR`rh}Q)V6P>Tqe(yOO_SND?{=h%sz4+{HuGg(VtF2aOAdhDLRls+VsL0C z;*`wyZFk^@kSH6Q{@M@@9)Rp6<)B+HoS0LmGVJk&@U!KmJIsjikzBMYq)^Mcb^Emd zZ0_d?)TA}~DIo5oMjSq>?03)pw*{rP5KTmi-wE)S>Gtz>W^D1E?OFhNKlO8VB^z08 z_Y6CLaG(AiMVqi)&^+2}Z+*IQ^4U$+^0{Ems`R$-@A|J#Y+{7($ zcUYP$=?ck4{6mH09cX~{5yb*i{H*(6pFq%8HgPfwj@BR4c!=7neC!CebhMv6a(_BP zaw%LgNbRSb;9KYQBjtnmjJtW1BqFRUDd7p@V-Wie9E?g4d{FBn@96p!eTx=|Eqybi zAo_8`!tMnqBZD-SkMbR6WvJA}yrpvQL_+6$g(Jc9&Ard1N3 z-0Oqeu2&*@Y_HgpUoKJ1wJQ8%f3+@%mZioqOy)Zb6c&~kdlrl92j8@bT+^|bSCsVb zQm)D&E_}~X4t0DEv{j+c@U{f1i4FW?$dWDK{zxMbd{Fh-Hz1A@<@w{CW*}zfR&<9N zfJe-I@bSj4XR=D{0XPd6BVHfyE7|qI(~M#@=uyLGoXV@DsNiDq!Cm-}UK%1Gd{mMq zR)kX%n$?puph8U_wACa2<+L(yM)(jf?uEl+C~e&x&toW|08iuDNeX{wBJztrHjaFo zNEF8m!RTgv5Dy-;;r_l;xM`0FdFWaNMP%~_-$rE6Emg7~W))63qO2LV1%jhd(9^be zb|S3*Spt=g;9X_DJvX6XQ;a)u?Sa3^eSFY*5QafVX2%ol<}DD5Qow?I9Y$#l0-tLs zU5o_ZEgdDDn9EY%!_*li&B45K7<$Pe0F)2iRWS{*MDYM&T~Qjh@9NT=5fNsP=U+K~ z^&j9jeY)2_{4(mQ!E@q0Y_tK^s|7pZrjY9HE>_Xe01RZ|qpJ^ha{Do}X=aC2AU}m2 ztDC*-ri=t-1H9fmX3P9kP2VDS{cG8^-cypD`b;3y)d|gYm~5XLs0cpcm5tx;HHJe` zWEQ$U=8;?PF}QbFmQ2ZJIqjvfdPiV=i@H8XYVO8o(pyX-flJ);EB%vG#()G>NCC%& zbB{OS43hAge=}T~wO=ev&@-MNV!L_r?l=Xtcg;MxYJTb?VBb?uf9H>vClmOelA^`A z_xS^#@y&huu@_87KVO6N|H5wS^=iaEs+SZRGuTC5te7vjHYPsve15A{RE;>uXVA_E z^0+o~tTLyZ#qF_g$CFqL^#we{0>-D#Jiemn9%osz>cX!?$*RQh~ zhHk?tv=z&8o6L8-fz1J;33pQ!a48!$i zj-G3!-#vc0-%wXkLqBH3t_7iqJ>er-T9PGae%*z3&MVV7WqlyqTWG^I$DWWOVm^>` zf*IRxo9(pfk5c01JDp`4rH;0o3*$em;qi_zzn#!2Ai9=eqry(G!gYoxE*jfhqfZPf z_6%_UP=f3rW7`fb&zAW$3+#=dQ;L3q?2I3)S0+#(2GpN>*te*sTid`|b03iPt7d6v zRhtGhxL%KJlcLlh%nih}!MfAH^fBbK9nRnW*pbz~qq!-FZHj|lk0+*OCXM+n?`Lct zSQS!o8toqrby~w#-ul4f*B2xC=<*Tpp#@4riK4HU8U;4XMEAD2ygGKDPm5S-4X?#N zpU1vuvoTE{5$2;n397T(+L?9~-S5VxQ(xw*gpS{Vm}#5bEGThk-}lT>o+q?;h}7Z> z5knG1C6t&3K_?p0Y~4X>Ypyg%6`0Bg{O!0{VeeH4Obyk<>}h!V+mn@&oSoW4Vko8h@vyqCuACRCT7oQlJc@V)z`ZzcCWNrkpxi}ZfXN42eghKA# zIrKV9z&CQ^Fol00O)H$STmTi{&|gYpK_&|z-o?7M9cs_eZ1?QB!cOq<*^hO$`EnIT zbdlc4j|%({s?M6X;#-yy{uN#vi`TLZl=UB#zjacnqi*E*;CvF8a&-E@Tpbh_T&6A) z&j&~XV zGAngd@m!yp>@xe7P7Wb;2~77bqMc~cv+sK1)lbQIVX=x$r#Db~(=}|}a#;&7X32iU zmd0xew29mTa;n3#s6|BW58D)m;FzSg^c`?3;S~nD&cL6@_+#Eq&UrE{B!Bv- zK=+ocZ)T_ZKwNO%z|}fFPG>>-1)`ZQRaD$%Ir&EN6+`L`T=SYBRRBuqxFQIoSb00b zPPSQw?$i-1Y2*H3`{RyJ&8MEJCNRzzDP6#HG<3*_X}M1&QFAB09~DVtOT1W;(l$*I zd)KobNWrL*?YRz`db5`oS;#AY_MYL;`JxXLL^y7PnDF|!tzX9=VrC&C#isHH6O|&n z5EUHigyH})Hi7wZNSsIzEZ_2Na3#%WK8cn5BTdSoFV@X}w%&L6{E?(MEpiWuW#p6< z{NqJfmBu=v)y#aH-4iD-P+Ul?>Qp@v;t;I$Ozrf>J{J><{F-vRfy6BFo^K#Si1rzF zt2iPIIw@3TvE@Wtny$6y@r<5u|CyFtAC=|GI5R>I6kFw1 zg|Db3@n2lgGe|+bJVL|`<(&HYtDAjOFD#FuX7w%B*PJJ+Cr`W;gq@JFhk>%Lu=iWm zEA+NaACBorNT%}^UZubas*QpOR%vMG`hv{+6G%i-}q`Z^j6z-K+BKT6aOvmHQ<`ILO1p_16yS#I)=V>%iWy^v%=Rgm< zhk18~X&cl<#;X-RB0k@x%QZJwTqM-&rAsciLNbxVLO{cX7txhB7cFZ z5n9&D>6nm6M{%mzy_nOlOO-b1X_|@X9!wwOuy%3h-usnfYc{SV&A?UX&t?{VN4< zs*OHc_5^K^L2@)TjtW}!dBV?&DHaS)%y{bt~=_chEjkzPsqQ&S|V0(>h{z z{22O4U`c=XWAL9t-#Cb9jP#((!Mx2f)(xr#-aR6&D>o7byksXf@g?X=zYi6rRPV$I z6vONC?e#NyLWYA%d=wM*sX=01Z1qv?J2!$3Huzq)fBndR|3a*dn!GAnw>RQwg-VPN z;zwohKfZ8DbRs>zwbwHE-(SJNg32J^ak7MUr2Jn$+G#-#udz0U4+nApRJGy= z3&!}TM+Nr-^BN)0@iu}0p(YUOumTHl`U|3?OGYn&G=78UhXEolxTC)VHe^Xo@UZUI z8#G+BfW8Uq&XyRF+oC&IddnK{HoPCOgHap+lgje~=}5o8i&gH%5uLIF#H=~RJY$gs z2(N7cH|qk!#%uUs%C{zh`A z7np?o`3^ucm+dKlE@aKCvh_5=>*}`~I@EKE?}q>yamelK;YYemZ{k?7cRTNP5q+LOk}03-1|A%W_VB0|1WYC>_A=kb~-hOE8Q?`X1IV%qq|p zLttg9uxDdqlK6b2W2XY=7}wqApQ;f z;1EzAKEqd-Z1l7PRzq=JLSez?;s_X$yW)ax!!X|J5>K8l`}CcKLOfb~_Qwgf9t`cj zy9KG^Fk*??Y=E>LJXD z!O{bCDa=Uig>T=tyu`&rYYB% zP_v<-&~G`~-&7zZbix_ZCyW!$r5TbALYd@%7~sSt$3}pxKY>3No6`3l#wxHbEsn;d(q*_y zBs^pJ+_P#3ibhoP(YN$~n++vC%v16@|8XfRyibkbzc5eS15w9y@B>Wo3CnCZH5Bmp=Wu(9AVp9I0tzhBSAduD7Ne4Sl)6qL z9?`lN_f2N$gQE5ra5z1G09yI6fSgG<&gl?r3y)=>ORr^XYqiv-+oUiZgSD(ICMIw^ z2D5sLCm3gw$~wb{1+}8AAjv8C&tMm8LKa`6n-?x&!~Q@)z|jaA>^rs+bv)yd=m``D zxwB~l9syKY>S4ByE)1aVB?ar6AzH1k^snd_fJQVY01KUI$&yMsSLr(W#}51> zn)vKhLC|W%%@9vYubBk6OOt8S*39>uBo3j7vuj&5%<&2Qb11F>SE;BEF4t^x5M$ZB zStqrhVH;LgPK>@$cIOJ`qET8=a?Gl$>Rh~3zH+xHgcxB~Ag<2FZ3>xegO;coUsD z-iC;2;Ka%}8hTQ{@s7!$Z(>s-PD$s>zHCER;CkC2j7CSlcD<5_%zyXAKWTE*gTBKrb1cz;=> zyUaTn6|ZfB>_NKHzlae7i4iHLkw`A^{^=NdpDI|Hs1dts#{!xfmOc03A$$hQ$gHp& zvOQL^Btc-cPDlL`y!P`jEc`djS7oB6musejhQD_SI%%Wh5}vw&9kZlG7AXNmirA4z z-@Cdw0oqkc%~#BOkN=Jt8J|<@M2W7N(E-FxE~L{F{DOr;(lNiL4sqHIm9JNb<+}fq-Ne*C7V{NKv+r{+Uz_sNwZtcw51ERZT&7DxP(zCsYKsgTP}Og04z4|wn5;=#xn-lpBKGvhkP>Kr@@(-= zNdwNn!rvX$hadCQkx24{v^9BBaL&|KyF4AE>~1QJzA%TuFBsKA7fJm3JoLMTr>mgA zhBC=fXm|V#)|2H*oyzPomu-x5v{buxkKHDm^}Sy1e_s|}!t?p|qzzC=8CsROWY!ES z%oAEN_gUj!vN`$Bhrsjc>XI4YR4khSPj@F&WsknJh%1nY_jB0ZLqVO)v}#Onw}ks- zbB5qm(6^=gyn5f*NQ3mMYF@%H1co4vrEXv+}*qsR)DDg9IZ!Wgupt91e0TPU$0 z=S2)o_0DISSloM<&({itc(i#@MLv`sC1-Y6hb<{C;aYnQ4Ijx@_C}F(u*>^X`-sSq z(336--b>mqtU)9KRu*|rA6DP`_@fMCBV{uhipbLbpc@iAIO&gSj@mqyW-UcDT_$rd zPetga^*SuxpmhF9949<_d{(y>^5T=$@a5*lIJy1x9f&@dsq*L&MQxJ}Ll~lpfBu@) z8@WSIe}5M3C$EW=rrg8gi^yGF5`W(JII>%P%3^{3SUZ+g2?*K2oRqNYz;t4oAgqvY zs1FYJ*5U#BDOr-U1QX&i;V)<(o4N!UN@#xdS{AHZhx!s6g@S6qyOn(pO)mYzluK!<=(M2~k=H#A zc0`fwW2Y_@3imz;WM>T`k& z3H{qswzJOjBv$S)xT9WN3AvfA)#hnsa>qd`&zv@Tj5OymPtRJ?znbo!1hx~X4-#zA z>E%qggiQ!k8ov#CJdqz~wRa55>nX#dR;X!S`6}T)Zue{m1zTKkEZ6ij-EI8u8eM}@ zr%KMr0-n#7J?Vw{$OloS-vT&Nq2G2!JTN`?>NVH+aIn!MsY`|AF-M(Y>Jr@_nCH=U z-jY8J-(j7JYwfz_{Mx^rM^W12=ck|jG672@9KmkC#n~etu-ETL2@Ym;V18fw(n2S_ z)LrytuPec=a6|8)f9WR#7B8njw-K-lAyvegeaQO=vFIK#VNu&wrTB5wO3|Y~3)x^3 zy7Cg{9m+g7adR9OpWog%o!_NM-T@6+qC*YQeGcch9&8)R5mzb?02OxGy{#xY?{{eH>#hT#{A(-1}p&~aGFWNPXKR*3wxOg=14bF=PNg!J6Q(tCaTOPYv z=#hgb*97SgBD_+ow17xara;mPH=v5yt@Z9_#nihcRli3&23n!LPp?BGh6=T{mxn5^ zH@b#5VMfZ`>Htn+e_=frRdVou0GPRdzk{pi@5|i)Qs9J|}OIW2@uOF({Oj`^}~yr!-e$rUl#M4k^wc zO82V>q9Y>n@XIb9qK`)DTxV;7J}W^-wQs`*kZh0wpEj*`$Q8 zLHDcVG&C5!w04wtn0`)l(r${vkZPc*GbX{XU*T_c-ag7Fj@JNG*rG^kr0@_`t8^f7 zw-qq=>!`}u+}CK!VgzVH@!pkW@_jwCce)5Ig+k-s&<`CH8>R&h?9`XP9FUX)eA%+G zGTG6*?)mxK!`=v%#L17!Q6VwN<4OMs6aB7nE@JD^mAPYJL?&O+|19E(?=QYKRm_GN zAb$m9MBWxKDn!fD|Q30`fI!>5&_ z#g<){gq!QFzRBuKYYW51I6?0iqD_?@8R`9k_rP8qi3U$3Yk>uQFhCT#tDvVI`iUvoy|H+eb%jVhThy0U zhV9UFAHgMlMZI@-x#>wTie;jbk{7kL+BZ`NmucaNJE`44|3vNWrjC~f2JV?x%9gkV zI;u%Yk`QE-?HZ9-zMtS3{^U`>TeA_0Y`t!!C7e_reGQctP1=fi&?@t)QJ5s$qL~w| z`WWtb>hhJ1XRxFc@+E91Ym+&mMZ>n(fuZltkD7^P*t3w@Jv&dHB+ zMk$Pq;ii6qQ$T_pdFg+~&$^BtAbM1_6q9NVzv=+zpa=If*j|(LZV#0r%zJY8i|vCG zBk3LBP|kJj-Hr=-y|g~2q_$`P`l$P95+%pm7%+=1_PWVY7hbRzOhQ?X||roRxVS(4dVE*q?Vtn`!GY{Y~YkV zIzBR{eBZO)_oCmN1>|Mv`DKwMdOoKldK<}Nk#$95%8O*@8fN-Fc%;RL5sknHVRVZ* z>sk~=%Iliee>)E;T~$a?8mtmX)!*IjJReJozg&Bw?BmR%f9gEAf+TiE;AUGO;N%Kj@w$(2x`kpHi8Sa8*bkj zkKk_Y-jkN~-tueG`i+U;NLr9IVd{dLl!ACvWsxO>JC{QZHu2d}h9h~p@`$ptO zbQ`g(d5?H;{x#qAgP|;AWa(|;ew*z>Krt^$(pGU8mNHcG!RHo>&Ge@V@rSdAwpP{` zNfEavs)0Guzl9;Cc>E-u&U>1w9E7^Pbnln?t#0xW#!XRw@ zbAnS4IjTQ1E6LU)cw;Nz9csWiFbaQBr1#m8Qh;s>hu@Hw0Ql*yG* zAeYRriGqr$Z73_HNP7?2a!&WJV7W=q|b>#L{83xuU z1Q#+Fixk9dmE&F6?_O=x?1hfx=dyDkH3#@v2w(Kt%CjbYo*ByWKOJ&lY%NQk9}lEQ zIS2haq!%yGXCpaere=DjVy$H8HvLt4W%TO~JjZ;+#YVrRd9P1|E0$V*bvqs7P7}#c z$c!86lD2Hk3cB1kEihciJS?v4&;k*tOK>vHpsn4ACa0<3SwZmeWZ#T|M9{QnIeG$V- zymc#0wh`BU_i87QbgrLT{aw9keItPkP9?DWGIDxkXy=U;K^rDTzfD5enXMGK*cualQZJ|8FVa@$TQ2PZ`^+!N~JdYFNN3Z}0 zo919YZ^Akq87+YjF?}k7))kF8!<1cWg#NpaNI$>sSpk;gXn&97A>|H$-M_$*a35AO z@%^W5%h-^cY`XWI*EP|v_ZI(y(gA*ofbzQEC& zGx4b9Im!RV21SV=7)L|!2?MFv+>r_ZR_q3telr}Hzrz*=IDr}w*KIT2|Mk-jl^OTeew=eW!!3`9YRCxheb`GyL#e~2zNOM%33bbvD| ziO5h`3%(!E8L+irzI%qAp5tkW`v890Wdh_$8ww%f9((~eIz6KIulS`d9l4n@ymri^ znPt!uu_Z8NYG{FCK!EqNT|zKm0yM@-U4fG3fiBNmQZ_Unjkr-*8A3kPhlEyc{mA48 z{*j8?Iid^0Dqp;ZsJ?oC0mm&MltenwTUSGc=DJ=|v=dz&cfopK_;xv#DXcRW`4DqY zBqG3A6jH-4DSGbja$RNFk=ae3$Da&w;VNmkEX|GU;8?a>GM^u}PWMx$1VA_oYAeW8 zeW>cel;k+0_V!9?x&h46bS7YAAbkWH=NnO+pG6<8H_&IAVWa@+u3k_Xp)XD%6vQ9x zgIF3x*7VRM%z=MY@QxCtA*o(}ze9n%ZKv;3r|8A33glu=W8qvw+w#>!3oJ7%({Y%g z4Ru$m73LPnH2Y@Rs!4baj``}p^KourZU{&>8r^wmdBeBnp*Q`N-?zU#&!oq?2h>bL zUsp7S-+ys4lOr$tVIS)Qy8d-QGeN=ySdvQ+gP%(0>-af580^P}6l)Cn?hHZjeSOi$*@hB*%+JP}4*e_eUC1DD*(@>1=6fZiv)zIH= zD<4M?^uH?bdv%W>34gc*rj27TO;u%rF6(;$%uD%#Xd4rV6tBy$tl;%@Qu`)gPr9Or zq^=nvD)wz*L24AnQS5aYMNAOmXzoNKyha@k4LTtk?eXhl6*=w%eFOg|_dsC24g|Bp7l32u2D6YV{$#Xu0Gp0A1{v{rjcH92!kdidnmTAjZqE|VWE-rgzCBocW z<@2O5%PM5cL?g_s5z6JYLv)$HCipg@9snuwp4J=KyLSc87ud@|O)lbX5Kx9tW6sy^ zoK1-8->8mfPc_K+(jn-V-^1Kw7dg6@Qa(lmw1?Y1N<0h0cBt{|KDSUB&V?NyVmWB&-zD?p7S<-6se~!Z)`e6e6g!vC zEZc-*g*dVKY7#~XpWy5~w<=rPa0BW2XskbRCG6X%iSfv@!{9j!U=A4ykIgUDPWqxS zgGU7a7Oqt=yhpw*4n%2GY~|sy5ei~vj!0x9P}e>SNHe^ge^HVJu4^k~q`v6pw_RU% z{Ndq$9@xWlil+#sgh&+1m$0Oq%r#*oINCPstxkPr$@A}jSk<-xsi~3h*56zam&(Ep z@GDYNF28L+qM(`-YMky8nrL^QwOBt z`?hlaH1Z4UET1kgq-_9R;dMSGZ8b~J{|8a2GjR&|ujpbX(L`tD|7aLXEJ`h%ubrd8LY$K3ruj4# zGL*1qGATg((>IZuoblZxLMW|ceA@I?DEO^p%hHNkK#ZnF6`_ppAL}Rr zHXJSfGhT+}J-1~n(`;K!zAQUNCMH7?tggU^r|kt3CgO07V5gDJ{a}bYeK1Fm|Js=U zCddL^2((yV4lySH1<(ZP)7DHzNep?i<-evB$?H8`=Ri{PwopI%j~|sx|KKoRtO`fzlp>Fr;kP7DJ3eQ198v?_LYe=5Cn}(05`-N=t6ui0k_`u zo>HM^PF$l8Eyh&5_4)A2RggF)^N`Tq@d#W;t1G^bJ_Pqw+hJ&i6I|Qb=dXYG3R7h})(q6n$$tZp4 zJH!tOV&JLNcB<(9cgqDZm6vs)VzTGedf^&>0(}{UY%#oyN{`qOu-3}#_YKiyIWw56rcH>GDm?7%@$w4eV-ArYA zld?AAO`(*S{?T<&DuKM!7r4z$yOfii|HbsVQ#_djM0ZjG8anAPjPf_Ly-CHlyAV7I ztud&UUWsB;l`6X)DvZzx$a?v<6Ft8BWzhZ{chuI6$0#fW(AY^ci(y2Ep&b`7?xFo=-Kg6E-y^X(k*io8&a{KK%j=$ttq7>%uEk70ztltkA1V>+WC3R+|s zQ&kYSi#>5eQ84GvmOX%_GL8PaDXVW*AZSRx6v3Rho-YHcCIzBrEVtkpST_ivhK z#N%nR6pfvP0yaLI^+qzF>UF9jH+K6uOQ%o;=y?JpBEqp;B9EV=`j)8-)2o@`Gr|0} zmjZDmv+q9O;D#2;`BTl~`xm(_X;$U=Rd&z6=>82J$G%;`E-XH4(9B|3qS64{W8n5L>kz>4eW^)x-U z(9*hagcfmooJ7B*X=Iz4yuj=xjj)luQi*u#o-xF_fx0z<{*di$`tMa_Sf+cVXVCXL ztXITmHNUao4>VQXR{)ti&E`rBccG$Ok#bRqLqpb}z>e zP>EGfd`XTLb~*3TulD1qDsu4cu2d*AL$Oq#5V9!bpOyVTT=zVxZkRTFPE+I4j7E|ET=ms8O%wandb;5t7AREN^2FdA0`SkF62S-(*~#gpIkC{e%VWoao*KN z(5r1^PIx`Q`WEIJ<0VZkapET(-3ZW8smcXbeu7VqSG^){RqZX zIdbFN!)Q9B$0lp`qk)HGjQ5xg%?7_U-m0~71R3Ma1r8c1zB^oeo{pr4|w z3Nur;D}mmxmGB}3JO=na3eM?!+UNeIq6{pcr46XnlX~aOF=RF5CtIeSVXB|+b=-xL zjwJS4?>FZ>?Jy;NMgol%)fW5Kstn5Cn7 z1kNRZ<*pJ*keeugO{4q%580h`7nhuxELS zNJmzh@r_mDjqBI81K&9}Q>^kh8ytCj5&6;93ytTNsluyq;9go7`WKaCG<@&|bwQPH zNLT$zM;T+H24Q}koLt!ybFC>tVrVF6I{b1W98GVzl=Uw|2kNT)=#AA@A$@D|`~5;e z)5_92;BnpK6DHBaUeoL77w=uU?&nP-JuaXdh|91hbEugH6xX|-8s)t|@r#P&yJfbs z|H?4_N;(l|6bxJ|S51?H0s^V`6>a1a*6<}E-BS&^MAab?_SP5rBt#|`lT(e#kaldE zpB-uRs_R+8UW8Q3L-3#F$7!eV7W;BW!fSH5M02hkqDZK%lR}D5|F|v3oXP9=@i9Ji zrPHZ1Iy(3s(RiDFWqFh~%{puWWsKE?ScncLdD|;|0qj%F0PjAH#5JR=M6%w;HN$NS z?~`|B4(C=cyj@9*yqR+tmn&z?F9CPUAQ|-`qVX;ptE?x~QIs{*v&I8->#yWy9Mjm`nCcHkCE6zg_ZfAOmpMie6X7Fe zLU?9?q+TdGV|w7L`dSUk3MN*!FI5MdZnvHb7j%`A6p&Bzoq&8Y@k1-EJD6bA!9o2I z#H3EPN`Cp^D38E&=s^u>pH!^>7LTb(xXmp3%`jrkdHV@-8yua0H*GG37 zJGQOk*gc(K<5<425|-=Q<}Fg|T4TDVb+Hpkc93Fv5hZ}!14N4MV!z5BPUkS0MJX@4 z9kNK7dgR=V*q9ywR$CT!s_$%YBxu@c!YU=j^l-RZuG%bjZ@kRvNu-xQTJ}dnX)(h0 z9&;X16O$mF4mb3$pObdX`eL2=IsE~^{j40wHD7hV4=a~F4~=43?NM*M*RxzM4U}nd z_l5n2)=XAT_o~Cf;d4O>kzWJ<4CW2iK_X#}L5E>Ch{2LKryB0hBNJ!8zhtMq;q!Ef zsZXWfX!UN;oT&Cs;!kS~gQ0K}2hz)S+Q{~whJUvGMFfPsT8BOg?54%(aG5T*d{A!9 zoUlq4sr}fRsgQIxpRaB?2IMkyIM`0K(HTd}g<%9fDSY)vaTA67bqBHDA|-vS7nELA zWb;UlpPIEXH+v!#cTBJn;}p1xYY!AAyv(o4Osc-Jcuk}yZ}Q<-aCFqXP>k+t&Fq7fAR{@&)j8 zQ0lYUJKBb8r()OBm!^5E&xbFTYBTi>8MXxw#M9T3{xA?n4qvS6sgc1SJ7G$gfDYbK z^+efd)rOa_fYd>spvGgTws>2=7zOsmTvfTA(N8N;aQ#Ub(H4h}K zBLDqLC&XmV%zrZ-h`!bLyb`SK#8XDqM%eOF$abv6H+ua>nP2NEfplQLmAg|jO?&;o zJ*FRVCE1}5z6t**W>E@*r5MN0k3R1Ghj2K*UD^VP&UPNfemo8=#VGW0%Ae)%lmvu174cPFi7Wz&#}%^>UF99@2h?218LBkP9^E#6;c z>103^f84Aw98I>Fi%QExZ#g-TmO{k%;8-6n`A`~9 ze(PYq9j3rlNvM(y;}Tlw#mo%eV8g&v>e-A!+%j5k8J@<$xIWNB9ayuU=Htd#$rwmu z#HOo@la`h3DW0jxDar)WjJf3ToBN-K-t!FD1tU2OEcfl45YqQ=A3~OP?&&Frj~Q?m zcb1H_S)P({TP3KKA7s7V{Z;q3J6k%L*QkA*N~0#AJlT;R_hqT)t%%t#f4;?3&(1fb zU)3PO?E3ihRPUrKlPzms(U4N2P$=4U#iwu>?-9!YxqD>CvpA^(u6Jm1lt{9egYUO7c z%qFlix#DCpE{CjB7&j2RBVvmV=|ynTT|XD|q3DX&IW6Jo`|P5uAHUV|pJ9!Sr5Hv- zw!R7|8IBS~ah~?Y_t~z%z*`3LS6=oDpqU}v97ukv-xpJk;Yj|zK@IF3_;&fXYHYhN z^G~IhjCQki#=8seue%XLMU0+*>__Q3;2&i=H7$b4t1ACW4V4>z(UjWTVU`pb9wxny zgq>(kZ9C2&B^c~Sh3+j2J_Zq3C^JWeZ0|2h{X;!noJUZ#i0BbawfwaRK^3pg0Zj^V}GPCwn{w2Ysl{;i*a zMfBHl(vdA%{>P%iIJq&q+;mgmF>PtMmR9nQS_RI3h%>sfrZeF)`}e|I6TYRqtpkNw ziWt3kE2}G&8kaO%BOwTW{w&yDaEoX1A60Dk7#@oS-{8BzK*KG>{k_gi;9F7cv4rXb-0SA|S=-K`lK^)i~-z60yZ>~26Z^t z(FNcBI3kpBxpRLBZDX_h625HFH_>j(&m}j8wxRMU*_$k7Og+k)H?a&Ysgo)#qy0r) zC2;v-he4m*a@DiO#yUXO{qlS`+v%17Zu*sPe5%_!QCc8C5&B+6>TnHlFJM-r_az}u z?g@0W{)|PQlje}7`{JE|8eKpp-Uqlonx>t~S%$m4#rduUke-*k+78t5!$fI&`frn4 zs-s`>ti=an&aoVzZ$V2q;>dqYK@hqW{0jqgSSyn~Md-{b(zY?M2Jm5)Bqt%h9x>WJ z2h~N+ZP%P2lF7vD_o1X&1v+*$o!qv3`fo57%W$SGQ!8}nX3I6P8CW-ISl6M=hs9Bi zrzdXf0K%^&h-}V=_Cs|?Q8vFc0H(epJX8OkeCr9TQk_eVW-b|>+3$1X%@_w31u4^h z3i8me1jjv%s>A#ew;H5AAsOagag7uPvw&@WC58y0?6DZ z?k|CVrX5K@Zul$!?HvL0pdTwev5+cl^3Sx|`+AgY{m}`mPVC3Ef8^}0W6=S>lcm~d z_&+~*{&T3k(RaOBXfXwTUXp? zNBH3wsrvx-an8L;OE04(g{*G;IPdAPpvoS=OrhUDE9b3yUvI@)1Z*-@Q2g5$#z=W% z0+M^sfAK6_4=Enty|ueTRYFtv&p-8Ve29X@k1^K!AKtxK03d#oWk%+=h_E1$zP|ZK z`ANtj7HjZEF;MgmsO5;xssW_pw;M=*=1pL&i2q4l6IEj^%uC?9GxeCSW)Z3bDzA)t z07^o{Eer#;9ZtYD(F~>B^%p@lEqBmXGX?qkPi)y4HnOkQ^9o;_T7QZJ{-h>PfJ$T) zo`cY>zZBp%uN(=EW2orf9F#HNgi|G3|hPB?e98$I#y)gUa&`lCqD_;=58CKr+lu6Oejvvg)yqi11O~;iO05% zRL*JuA7b_-Otm5f8$QCg5i;(gWvFqOv{xA7&X25_y{LLgmM!sQ8gh6Wj2GUD#eie=26yFBQc+q=t_bm6I6`Tp9(6pf0XV4_Y1b{ZpXB# zEx<)~A_VKpam3fZemPeb>RGf*u%LlFPVdyW%TBs5OD_RAeW+vL5O4)PFXq(23!KT( zfTg(anCHma*&x<%SDD={6(`sJ+d^T&W<9tBDGF-YS&VTlT!_7lZ4s^7PF}nyk=%|D z-$sG3?hk*-IzZ-sHyZ_%^YGJX=lxoaTK)OLcdOJpLWZjMhBYsNX{ZDWhP!}@bS&l# zVus7lhUc1q#h61X7eO#L?zV-y6UBBYww4*SUTvEAGMkQ{CPHo+b~sKfd2`DU1%w>f zcake1x6t468Y7_4{86U|@{RDirSXJT)^d=D*6?9kuwA#a76C>n*O-I53A?iq!}s?I zlx@~hxSkyA&3X=HfFF=_@4Zx&Hw8qwnuJ}|S~iV&JrZaEc7gIQ&I|aD^v^?>p3bcW zDWbt!*f-6ZgZz?LU|`p9Y!cny0^d(9wPY z*wITR>)%?=pv*qtPxOc^A*2tkF2s$iGu*{oJN5-qI6MO}2VjA6|Kar$gvQ-F13%no zq(JL+B!~~|_;jN?DQD*FBA-8XCVm#qYT74z4HHMjAI)#SNG#3TYzGt-ed2i^o*k(? zuBW&x0yNJl7+CjY=RoU?=3iE;JN$Eu_OlLP^gKvWv|&^VNCg(Fb3n#uv*$+me^Y`+ z_777Os4S8mYz;97TOC9rz7*tx%#oH@WIKRuvA}K=I<>%s?67~R_b@TV7QJtl0ArCw zacQColj?)Rq3-a#T0n_cDV2hkYPoG8>~A^WOQc6(!dtvq()Gd+%8n$fPM=mkD_U>E zq3Ah>ly>SOkfb9q@V`+;8X#5h_L5OWz8|Sh3%WTWx1!`N04ZZ$=p}90@8Il26_!Yn zao>yn22r$k(P8S*UZ6swBV>>e(t?5qTKH>QY|F7yW>yB>7m1*gVysf!HDPzHi`%N1 zs8;>a;nH$Qn~VA&OVV3KgC3u+ zQEZM((;F2?MD%TgCn(sXQKv`HLB%Ck^=RyMPJ4ZL(G3pkoD<-;p-bW_{Dyj4Mvxp| zJ>_+EF}|z)aP&o=<{9XDNmy1r^046aBjg^?dJEoK-tV^{Xl@lX6H{Y?r#O zQjlx4?SQOtU@=zTwjT|3C?i*rT}IH^KyZ8IF}XE=L3M}o$|FV0Ar))@huYDFT;S)` zqcReo8t*|b@(#Bx?R(W@;;)8WEyWq7h`Rix)bt)MX6sj9fgB}nviRwe+j70D{k-bZ z6!Co;?Asb1qlD8tPVPInJ53V@7pMmz+b?sHM|OZDL@x#`M?3f62hjL_LDHXoW1tS~ zk)@->k3xA1eJztkVJ7He;J*2{+nsTM0H?tVC_awkQ7c|xG~|*Amd_(jxLQA9HM|bt z%>Es$OI|M%raW}L9T+d&@6lRX6n1VtpuU8N))TfASJRr3KDbHYs)5gkKxadZHM8Z2 zm!|)sP`LFUhY+|N6?eImT>x@B+r+E^PWh)kXNu;0-z~dVZ>;ekrRJ5uFdfcVBBp9l zWP;88=uqm5B2~CF`IT9OC|f8~l-K0!Jpzvx$xhl+NC)uHK}(5)N3w@4{!Ub#CR`yNi4knzpwMw!*-WWf14qC@Nm{~7i_58So3V63FJl+8x5s;PU4IXdJ0zC)Al5r$wCx1CX ze@@AaR_(z@>f)*D8BNsA{T&T^f6U&)`tjgeV{nm1S#%DOe7!%Fc~V4KJ+pC?68K%e zI#Mvf@rKAFnkC7Q6o`aB%`dB&*>U-=0b9;57YL8vu%8tiVUyRi>>Vt;KpHSno}0;S zx;TUFn>I6y^npYnLNkBi2y6DXEwueyc$RtZ~`U#W`B;qawC8q#vYd8#dmS_#y` z%$u4Fl>H#%kz8Fe^NI4pZ$68oK+JCzl!|51u$&%?k~=`c^Sw*JM`~2>8Oa)>aN{x> z;;5N<-eIK{!=jsh$LG`N-Ci8*nGvQ2!u^OlJCr)$p?XOorYa-d>R#4lxz>FpaY&Rm z5t|#i18`k9E!mR`0A&lMS%K9eyHQn=b!yi0jDZaHU@2PT?po3D=ixa+wQgu|B46%I zEkct?8`fs=8h_!&0)u74V$XM3&q%829a>3(N*BLl*=KX~Idqfao}R!+IYcj{OHY%f z^Y5pbB4F{!!W_~qO4;Bn&Qh|cx;vKOYs^rD*BG|EVF{Hz^4h4$?|xxBm;KUpdLbDM z&cc!7C0Vhp>ejWckZ7cfp>}hLIoN@C*|Wc?uzgR^ubjJ4Y>GLzIOM^ORTHIUclgQk zKt=Zp9Dbuh9MC7xd3B!bA$Mv zL{+tF_S4GDbMdD|A&){`Q)itvcw5jOum{;9J)?D$eLxPi7r%0qEBUdg%=L}3O zFHuj!!i;-8ja`?oYvOi(TY}eNd!mM@4#e^iSpjYa@+WPDT5F+8KC;Fgssl@iO7CCwJkHy|OoGQ+vtI&Mkf#9TvsMVs5hd0YP2s1~aJ`luP*!q%C$j zna0K*xGAdVdE>)f8@94M%#>7dwLayM4zHW*o`1r88vXrvL>IJiR%5VV_5?$5B_+pIG>NmL~E60#U7Fib2=Y z;$wz%A#xeK)D?ouB&9wklkW|@NcQU2f3BG8+0RZ!a;-9*uC?MEf|T7?K7q1-MEnV* zV?Z3kjJmK75+q%7Pn#fIxXyvzv%npLf@JPqze(Q$=X4i9_7gnGXc3x-8Ob*sCbR++n!xI5Hv5bFZu9f^wt8qKFtyB>in zPwtQ1dR(%W1RY(VJ5E@G7SStn0un_Wb8_c+ROzvfioCPn+Rz*0`*^tfNKui8J>ZDQ zRGM7kav+a>Esaq9lzS)S zCPPxyyMic56FGtO?2yy$jt2R4nj=qKgzGg&=M!?|B9kGBoZHw=>dKrk8Drzyd8JyJ zMCQv;-ZH*DvR&n0!~k;7MvB5@a1I;yVL^esm~V3-nvWplps`YkH6kB>lq>Y8sr3UZzu?26$oXDdz&Q>lFYMciv6i=apidW><0jd55~C zLxg-i)foj6r>j6%b&YMb4Y5uu`iZM(D&07dO=ZwoR(CWhlIKg6P3fAh&dB}Fy?&cx z7Rm5^{&+!>V|0&jjV9<5pE5Vz2YSdj+NBn6*PY4*GQJR&x_T_)lmD*MrzgK75+ol8 z2jNfWKsuXK2PGgmoC4DerSHk(J3pJf)*c(}0U@e!YoH)z_qn*I2Kxn<^8EQQr_j#b zU?G(+gMHvgSDX4O_uF|~-MK|U6i8*6=vvIWJ0>>w@(Xw>QM3VHpjr)2gEkiPQ?SSU zfqthq!6rR^3XtlQL%pnoAPKn!nxC2Qjgbxz4Wq-Sq~X;sPn*RWL;hyk`^X7+W>VvV z18Nbkl_%$ccAU>pU>$BkRy8SN(2U9+Vu5-Bm z5OK90n#U3P!;8&JLs=kT#_CJBM73UE$cYRNPaW8$Ub$z?B!McEn6~|0z-Gy(Cq>qd zcmk7yMFavQ^_N{D!N6o+CXMT_Y0!q{pOHme{dV2|Oe%6XrU|YTRQzFgcK79j$1mt<~C1gWEi8?h_ zWB4~gl-GGT$912IzYzyaoT;v&AS?s zdrE=V2B9>cNQu4n(Vqu;`FHP1iVdp_fQo=M_Cza|Cy#lqJ^O^^&UO##xTs>S3-Vds|RoOiiE2mzEJtwwS0OQ^+}7d;0u`b)%a}=xU?;q^7vvRfffwE?#AL;$} z`6y}x0_5whHPg97D?!lg9@)h;C4y+9CjdqPe)49nmOfyq>1>NLeD_lDG5Dw9<(#N4 z;(88&wKnoJs~hM439-7pV6EUywIgh^vq+c~?(w7nG9Fz*ZZT)oQ|$ES^wbwJI1~|= zQstfjHy_kw=rRJR`U0$tUr)U&{)SjZopLRNg2bIC)C?sV1r8pdfM6K853U&fEB^v# z$abPsX-KVdK!%m;wMfk*j&}_S#h8XBN!f*Kcv-Hg5}B@|y(>^W=wbJD3{Nm~wWmAy z?jMwNL!Iyv9t|f-pQE;7dF8Ff?{7pn-FiB5|2@%;PE0y(^JLQ_5X?~Awf^&u+4FXz zF(;tJ$@P-zr~pwFz=-P~d+Qw2`y6(TD- zCsw}&n5zS&?;KGMrgVSfP$oZ5z`>OaJ>uZUMUM?c=gH&%Vdr*5&-|0EU7&65y^cp@H+1r|x`jPQ6h1AmJS6)7Fh=cgC|(VMj@%$9GEtu6I>FT>2mtlEVcb-^}mxJhRJ`he$J^}rQ)isO7RLi`R#zB z7-md?z0_B?s4ZPgl^7g#a4`3#+gwdRJ$p%B+8F(CSX{uYb$A&8OEvp@z> z2H=m73D|%Y_!6Qblh#6ua7<fGmm~fS+ujBxzn9^la--VNwXXMqVhGz~4L_In zRGBe;r2RoU83?_9%)0o8&+6cvM%sNfOdB$no!`?7?yntdTwsTq>$2fDzWV4KxFj${ zI1PIlkVkll8BF17VL|ggG@wY3Bi>c$<%`D7BX5gXy@xs&fIO4&#I+ISF3CSAX{-f= zg!%HJ)la<^)8Q2`mv;d=Sg3-cID*chJRLVrz}DO==9U7il=mX@w<)}+L6)H#Jq@pE zingu^u9HG3P0;(WELktg$}1b_?b>~!rBZ`v-cl=d!AT&H9utb%;Nte>G5&0um}!I` zm0{gt(Hlr-opdTP?io1{A!dxmjS@eBl^&HSzT@&Bf4j)=kta`DuU-jW2DlVU{cyF} zeix}3&|N0s*$kny@PV(nl-eCup9wb0+PE6v7P%f8&(KXC6R@>ro%w7VTF%S)qAge3 zMs%i5PEDzNkXY2dsf6~6VpD2n`bqL2Y-S#lxqcXxw#D8=i*5{CpT)UqZP&>>gh0E@ ztx2J9^S(E%rvsQ#69sbv27~fFZ_Ej-&rj8pP)xi1!p4GKj>5q4oC8)3zQaceR_wYV zMOmzcs5PeqStQhdWhDunityQM%D>pqggaT-_{jxg?L_tE3eKkHd>q};xMaIdQU2NN zc`Y(z8&YP0)Vfm8=J|u?ryopGcY=|aUJcli9&hg7fWXR!zlE@Pu%A9JeRp>#zF?OO zi=S=n4S$MCtAM`jYewz${+pEBAjd&VE&;`SQrZ?JjZsR|?N0GxjIYC+0JkBva+Ed@ z6)5${0D>;xbE3^KTF$RoNZ(YJ?=R|4D0*|9Ef%aV)}dn}FMec7`(|>4V?K0-|g)4U5TwLLZyT1k}jSDa;f6DFB_Ti@IunA=4^5(#5KX zT8XYJ#r$^gSMj61ausA8uwMCLRYEgw*;vnH(V5Hi;Ol}Y)F>x zD;GGzbnYc`?HkYYJkCqrj_1taZ(4+B=~z`*ywnV^{1B)EO6%e`oz8G3eWm;{p18NN z_#ZWbJ%iPKUW2L6(6qX@*heHnHFv8^_DI=M8ke@pj zDFrht3AurHb}p{mu1@4XY=5px*!Q`tNFmNgqK-XI);)+%`ONgB1{gY8%!{SAtpD zmy!mnhV3L=GTgaiRL1Lu|4+e7)m>aqCf}R-zh+-kJ&P7D5z*clpGJ(RF0mGj;=rw4vrt@wIHK zKm_P9Ir$l|#e%*m6l!#up+}~y-i~`be$*yFmEZh98@v|;z1kqai6n2$k#5Rr3)Y=75D0NFhs4i6s?3F1D|9q zO+)H~n#@4^AAmX9Fnl#HODDU6br>~UNOyJ)BWV|p3g&9$^6pR@GqgE^mn70_d*{us z`x_-6gt8TBxhcjZxO{Hai!uClZY9fr3Th-LLeF%PB|i>;(SWX=2|A3q&n&lLdFJ3M zSH%_gD7D6(gWx5lgMMpW*f{42YjZ|)#)CI1M4oJ?Ifd;z#N8_ak{H}M5)MU-+M1wY z>7)sM1{DIHrak`!G`7q_P803~eb+HnOb`N|;$}}$icl=dj7uh$H7Zl3J&T|OF!tHc z^vPRx*3Y>!#5@{u5i}0gq-H-Dm~Qr-@X*iO(L!SYHt|gD2+wo^Zed*L5wd>Za1g_x zFz;yb)FJY_7v*%Mo2bfXK4G;A-LifK;|71h!tSS;Iu>^*&lPf@oxo}QAyX4_@gmzI zO@g&x#x6viudPa=uI2d2*~Q88l&&-UWz}|O>3+Qf+h4ATh>5>P#)EDyY_?w{G8SyV zq#x6fT@m|wdHkj#p;y_kI83S8CvoO%NOeu%@PA-u;ftWm7xP6d*C{sxX^XR}LgQtb zRDLz9IZJeDXDqw+k*E~0w675U@Ajy|>e;^f9wJG7a$TP$O<~o|QcJC%hVNm(`)Z$m z@_{DNX#f_8b{NIqb}-y`IL@2xTa2mB(S8+wH^aQa^cI_>-TU8thI>+46PPmb&d)cp zjV!S~_i1P)$W~@MygwFmlP6yoI+IAh^##T$_Em^FMG~Ls6pB5S@6chiC+-aBmbFEK zV(owbtxyoxRin0k z53>c|Oq0n^?Z|l-DUN%UR%SV2aPx%LV0MLScx@qGhksd+>}@E}!FG3@df>^kal86J zo$M_fJlQzM_FM7^ioby5F}Ymt?#Mlx8^DwCqzj`KdWF8=6^gBA-^n_=$Z!n-FR-Qi zI2F(&o&kHbJ?Y~hzrVIMT4mhIpXKH_8+&pU<^E%KeCl)@dT)QL3H?c#czebJDJk&$feK9Va~}- z#JY98s*4(CWe*45vZu?v;T*h4@z!Gf+oJ0Hm1^4_m-13hB?AlQSxp^^%PQIj`t#&1 zKCIOq3so%-t}sRfi@U@{DK?WCLROo*$AWRCmlZt7N3$Q{tZ05HKZ&}MV6*rb5)<$) z_@|d1d4cNK(>Zg#!YJ8=XN^7|ttlNcI2;BPb5q|&sIZ0d&WRoVzI9O*|3SyKGQaz@ z1OO3NhU_~_{A%ot){n2T$Z;d6e)MR%3J^P+PH&kPy3piU)&1BC#^P~Yc*xc@ZmR$? zm$u)0jgRV^7a8+fmkV+(L#t@0CkHhSsQ03x?2KREZ_goFSMeXi9J`j4Nhz&5Lzt3L z^wtzKGM^Bus7Mm7F7m*(90l9%Ox~Tl_>ipW9(l@ofM!{JMOBiqVKrkq0?&B+hAfP- z#o=g@Pq>7%H@Kv;!_b3WzlNxEIGRuH=R&j06EvQBb4Bx5VKodcTOyXSL@W7TPCfN! z%oNoR_Qqv5`TTtb2Rup6(7fl{POgYl#6Y;4Xe#X)F2{_W;Cq4TrM7;Lq^UcO?RW#P!B3pNW3S*y%L^b`K|Vcp}p#Baw&IWz3p9{S6o+wgeScIkPQ za(c@PUxpW(=EZ)qbWYL7x1%1iUdJSh{)CrHZ*{b~qLZf=FpI8^9}kE!Z2E~|RlC>R z8E$WVTBS$eI#Q=I-S$L7oC0Sr?orcex(6BxHy{3rzsJ&xUBod2T8>cT>es5k46 zy_N2zkO>YMcbI%0Q*#f}{b<5RdbF-X(!5ukpqYsE6s!oFs-#GhK0$n%l4NfWKkO4Q z%T1}(4yGK>8*A5!8r!%;PChc_O=q^TM~$Mnl7m}2UJ+k1$Ta85tass`4cxq{fA4p# zU-N0X)aFhzJ*$Urm_*5+C_i|=xlH_?pMZNBHm=4_y?#UT>!QkfDw^r}z{7_LFKsTT zoG1T0sUx900k357G~PoiMs)EPPw(tZ9(SxnzIot-KSR8SOud#tT}ec$qO-38bys*y^83w`;%b4GeB)h-n{l#7I}hztnv7s<_h>mxtVw(MH_kn@<(qmvc;S zW^2Ae6p)6lhf0d887$8-eV6y}flEZT5bF={hlhWgsy4cN)bg^mPpdkz1*WeFd>hwu zie3-~_O({S=kJ6@-S;_tEi&kP9}^xV_@H{c|24?xtY+B9^dK#CLOJ};!B<8QaA>m5R-l@r;8J9*0IcIwbq*e?o8Mc54UVL8mvEk3mWg$2?q0u?_{P{ zjch@e&o%oGaK{V&LJR$y=<|L3Y9I@8IQ^}1oet|=B@>oNlzn&qm`i)@yQAsA4qEq@ z0v49`#{9iITNbl0U+%!W9j}s77^QUC@CXhF2qc;oIjFABQbaV~$HtLjlk%Sn45srV z+Qk)CHlhp4C9uGA7~F03H*T1ojaM^I`zlpr0%M}QF8Tan)h^QPXp$yj?c*Q*;Drau zRGjb-SouSL%7Wc4uTsgy0M*R&>+P^=j}Zdt0GD?7QYSIBGSee`FBLyBrC4&yU*9(s zI#=MhQuT3)OgX1b;s%`9Mu_&6+YGTmr#d&;`uuyiDSYA84-@9$siB*A!-{o8l$|(* z8Ml1+u(V#lIS}pM z`Z_3~^`j?4aNqV-KC7}`YOqn=zNDLfXQx^-n)Tbv6-) z^Hd^-w9jLWM-AkxG~f7cX3t7^gAOai+bS`ML21eS!y7?28)&YZsug2f&Wm@?RXHt|pcR!QAky4}TAs8ewzrr-qe2a`y zZSv-MXt!nQBgwjkW)=U=(J_v1dWarqLwt>_y_4NhA|aju*VH!@Qj zaV~unILy*=1u5C6ErW96bd>GBJyt$kDA(0o&IR|Hj9d070UI_p*N_5P0oDGB-(Epk zD(6Z|LK7j2H+wqy-TJDWWUdJM1|GTwqJK}DO|v7_*6_lAnAw+o14Q*?5x z3yJGG@phxGj+*I>;?pneRA%}rK7PYY>^dqGt=|{}4U?Q!v3c&-H%>^Yr}-djiN^Pj zBRd+)f?=qQC0&IxPI_ffdWZ0qmnd(s1=o_wuzv+FwvkJrXa`*a-!%NSB={KK{0+og z@=Ux>WYZ|2#vAdE_@RU3tGIxntC5--*i^vTw68|kgx|$G~3ZyILA%)|N?sLtQ z3|)LTfh90Z-}NA}y8RCpj7eW zs{hL$;nOcKB}sd~N_`alzW#$;y{p$F?avlfq=0j+z?2I$)rA}`MPDCs`~RE0WW+h~ zPx*h-p0mn@1uD!dCIOeW9sBGRXGQb1$CV)6YKXU_oy8VVoG$-KYE#FCMZ5q+d(lZY)Vh*35Gznd($X7!ecTcJEZ)PXyK@cr` z428R)G7w?AYff-oz)E%{@mVKJeh129l}k`7X4HC8%hd~dgxYIB-2E=M=dpq2i_YN= zB%J)+Fg`$)3V%Mj-lFvu0FJW2gvo=_$aNf#w=4$fvUC>>aU@n z0WP1NEX-3tvKgg2^>0A${ayyoH=dBq ztoWxQ4aH^#?6;08L0H^*T;Z1 z|6*M)L6}zGC&g1sOhnzsj+z-jaK;4A<^3m2jJM%`V+gz0`7Y=zlltdpHUc4e&jilt zXVVbQbD$tvoI|r*!>k7mqiw@zyJs$YoT_zz19BTTRMzs4`rjLFHnp|^hx-=t8N9G{ zvXx8MVb^^e&ox~KWbQwDG$haNAwk(v%8@GX$3><@R4?G>7wh(73Kyrh&uya4hhIXY zCp!0kjUHyK8d{5Dh+r*?{0uRZf&05~ry+GQWRGAMkJ#lBnzj*QDt(ew0aHo61Ig;e zUNr5Tw=%jIEP1K>(zbg3n>UqhdhY_+4^$^8meOhmHzfH1Z`#G+{dIjo=KA*S;OaRaY()gz)9GUohOx6ZQ+a30(-ubat(zVwvnl};TfY+88@=M3aqmv{|G z>k&5($pXV^myDSliFSU=WT)8P<3-06UU?7;S|N7_W~CPnikVpKLBZ6vWUn|B1 zOZZ|^t^n47Z(mjo6c^o-pv>VHZI@VD2J>vh#XQC^xk-rVy_9kzWT;3s#})ogP@LJe z=!eJjU&LkiDF`?GgQ}KIG1t1kzpxC9SHIsrV+d?$`VWoC`%70kI+nm9P}jt&5)e~m zcPF7RvA$c;iZ^wAUsF5>&XmMEMATFdrZWB}pUL>Jt6_co14t{m(hqxloh!g)v;lFr z)E;v${aE1okgB;N(qCnm|M({oU4r~GW*=h} z&t#xJxrAIYC>y$abBt6$sCvN2=+iu(nEJd}XzQ-Ji{y|i9Cv?Iw>cht9Bc{TXpJSt zPMcMz96f>*1Q3Qve(9FkJqPBFuQHE$2BX`CH7)_B3B~yPV;@eS`c2UAG+;_IEISs1>b*QJn>@j>k(>>* z0eV4cZT8X6yL$kW^b_@NW?zzSOY`3YMsKP8$*(idW%BNtCf_98L95huk3#rY$_vS* z3_Bw*Sl-%FE_JK3Y3(N|cOVtw;{V7rBtpR|(to_*jsd`Gu#>OW8x1_22)xDQVQ1$E)P^1X~IEAuw_dD^=x6y>O{`83*q- zFvI5g9L{^x=Ni>sAgI#3Gf~X#TbH^W~fYV5oic|WolK_0J&u4yRm7- zx{$=Z+IYOYr`Cat&g~f%)UZp$tT*-$X&#;bv=rp1;i84RZwffA|79ujTvMqLW6rcC zg*wqjx{qS|NA&Hs=wz`4Xv{AB$#D;>V9L9f#-LGU0gI;Q)sEAiP76Ns)#+=kB%TQV zKaC;RWPT?AYmfv@q7O*z>9Ax!yMV&areRJ^Kb~Jm(l8$c8!)^2>`Ef+)FPQKoWRP! zp-Ul@Do0480eB~srlS+$KXivMES6PK0hn{fsS7aM6}(VpBYHPw-$eRYh%1lX7Ep@3 zhnBE8(2JJser2&#emlvU^!Eii!_E0`dQm=4#Yr1%z<3H&+HINRP6n&3H!D9N6WmO( zP#Y1VHpkjAJ(n^0#!k^r6*a5vOg*U0{sO7eeSY^51N&j$7_lkV>Jiu}ORU6}@8pec zgR{b0Q|H2${+@XyXZd}cR~cbYq?wBHu@Ijg;SicUM;UpjN5%b)dS8c~h>cz8<7Tqq zbdXd=hhzD}n-ofiJXi^3csZL&pQ%h9rpoKhn%^_%t0!1$Vr{_}6n*QT3cMRh@xEfc zDDphQ2ZgeC0bus2^gzrhYToncyi;x@IO8dT?zW$`h4wlsfTV7po$ z0(l=}qF`mvZ?849z@6zR&!?$4_v65u1fG->(f$gbL5Q|;+HeBdg%=IcqQh;0cq_etuYl5KwjZ1kq zxnOT^0*5qO{HKB+p|;hbaP0c#w}xKpH@#3i_;ucPg)hO;W`rk*&N7x7=gp&|ObgJ$ z*2(!ILq!{tu`>$zds@?fe|^l5*&!8lO_~RH5^Ti_%~Vfu$pm2eH=Zao31DU#%}J?7 ziIL1eWtmjF)^(nBDtQ@Ova3 z^!Pn3JntW2*5e)CP@DDaSzYlmZCHXJ;tF7pUP_Pl3Mh^`l_PUZUz zrl=QU^0=fXP~t5*#P_kbq!x;{t2j2`Ge{-;*DD&CyKp{6^PHz`F4$`nd%rQo%*D5K zoPiB(BU1%~V~S{=lxvGnz2dR&kHlFA;aF@KN7e*y>3z5~uCB~>lxL4`#h0@6V@nOVE=GWXr1za*RpO?L+b9!x3 zPP=OyclLkw?VWn8R2%j=?j$FOcm1^N?G|-hCb1}8c`W9Wc)s5)X{dZPhW8tCQ^tx4Iv+mJ%!; zV`fekGxyNKn0mNRlkO&*O@u;6-RvJW^fb1;*9DeZ@7kw&9`(N!k)#(6n*G*G1Y$Jx z-rkWs901GpyQz1LzwqLn%r2>Yt>%7AW#|(5vA^jvG*7aZ#s>1DZ*jqq+dWxzvteww z);jgl%oFcMo4U~@>%y!r85uvQvB|Al`qiAD*Hb#t(Nra#>5Dp*K@PRoGdTsDea(8* z2=Ej>WH7EWPFvIx^Ak50FWtJSg;*D`_F9>u+EUf<%83vlkwu@N>iG!tTTWt4Z)bZZ zTnuE~9dR#5U!NuRt}O#6Pd&ArvYFwb8Lp=aXqrB1I9}OL-Xe>j0)z6b$m8r_{E>d- zXzd-F?+Xrt12Y@zwB;3~jQ(q%_smsAC-g@f3$tD&$2A;=+2&FrGx7|Rs#bjS zfjhOup$g+*)?Rx{&a}zQbt2A*uGcT^?{<8nW^R8KKQEX>9P~NX~ z)Q{XR8GiV{TgWXeJ%X(9?Iu2rkV!JnwO&IJ1{vl@D85y! z)O#R%*=IU1$C-bOO)|@PGxP{egCsF3xwdN2QWVt}?nfrpE>s@K&kH@~rYdI}$B?!s z8eXcyeAax&o)5|9@V$J+pWp;n zJAGF5oxrhVe3r)ay2J4!Y$pj)-x>Ogto8 zp-6fuJmOtFE~T`@xnsUQtt>NYLSi4%R`4UpW2;m zd2kETiJ8wsNj3)9C}gU?^h@^9P5o5M_g|NN84{Hmo|oA^*%$Ed!({*&+P*BuI4b+a;d*S zU3fa*c;r4WXe9W!C-H{Fcpf)B5G#hd)8D&;&@gQ`jo6r|t(jK(nvFRT4Uevx8{U15 zeQcOSky7WbcXY50iX3=13o?)0B5YV6)Y-rBJ?@Vl51y6JH19}i!ONqiSkn{4 zu)RsK7;NM}%Ra`SQX(iH#N+sd>aGTjN@d^aM}$2PX9nN}8Lx>vmrk10I`EDOPj>d! zBD*N$CE-5#(=Yk#lpM&Jxi2C>2)L5?!u`XYXLa{49w?xrws+=^yQW~WXx&tk5wh|`L8TrOSQuZcA|~~fh5i<^-TIoSEz{h8?SMvlK$8os>Vz=WBqY>c8@5N zrR#MUuZlMlLxsU)JgKO0Hq`+kJi(&`7nd%x#J>yGOQzA-qpgX9hGTVSYfw!an`Nkn zGOLYj8M-SC_eeeEdCO$0_hZno`Kn*a5uJJA6SkCI6LrVxvbWC7=buf8H6e0tUAHH~ zVp?yZZ}L5ORaxP}bB+C->zS}+Sc!smP0+#?bz$KbJiQiY(}axPaeJON)uRe6!z#z! zM*U=h7NtIIon4<<)Fdy-^Zll-2W0Eruh9T1$Skw9X?4Ft=HGm4q$EBhk7Kb69)eL2 z;gj5|`DM`hdP@M+Q= zd1xr0MP5bma;~QZvRJgpsg{2r4EJNl5ZGF=QZPE(fnS7OI(P9_d|lb$x79p;zOKNS zzw^v1a?Sg$=y>W|uv6Kj@owoGdLTqw2ZD{iraF6v z3nTEIZAuBm$n3VApOHpPUEjBl2q_iv3kcs)O~fzH@EDew5#v4mK{qY=2SgLcK4b z{Z#LsvAp4Rr4q@_I@*Wn+_=k?uu}J4eBrGPpoq@hjYXW!^N^G8tX9oDt>$&_r7Wj@ zH;~=S6x;hfFA)SYRM++y>UYsbSs#_Oc`vXX+4TtjclNdh2b;U zduPZ{-JyTs{;c1l#P{`y`Jp8cHeZ*z;c6KWUz#zt)7N>2hIs+wC|rN4kvsOL)E%Zg z94o3)q!p!&L%gahALaL4#dvh5zixrjXd(-?9JMg1u+>7b>qia#7rKVcHp}SZEkDQA zv0tHwGLx3$zcDTG1`B0w!d>w}v?uzl&v)+2CuMyV0W`WjEi(RE23B%-hq+47i*7TN zTY&Y3X^ZSItVn%%Uw-(7>DwjKptSyYs=1e)-{74xRb-f1m`!|PptgiIO_`sp=E=?8 z0^L*7FrsuIQ?Htzkwa_u^$5=xv~@(^)>cMWo6&f&oa@tI|54nlFE2CtvmAf)QuhXv zLY3m-@%_Ws8Ghewva3mjC)Fx`IJCS`R`Er}Cj|2Mlp(lk5sJ|KK7Hu9>q%i>!+{=h z6L!QYFEz2C+i&=Ar`2;tc&4qN+wu7#&WV5{o?aM(mG(>-;=@k~9vN?C9wI7|FkEb_ z1Knba7TNe5ranBvkekolB|QuTC@vZDPD@}T!NvmQPc9dq(8VI$0_)cW+OXfSJ!m6P zuO%_0r^#M0RKZfSVOVRdikj{Wo-uK{BazJ+ks8`pMq4y|RCN^kGajih@bSHtdy~LS zw{}HG=uZ2t_Uy+PGNeZw`6It_>era+SpMp)H92>NlB?*XRL`wc@1(ehD1AC|P$t8$ zbtPk_ekj3xA;{nT2wp25?0!aeh$YM!G(nPL4*M+Y&KD9k#H}Z0d^-WjG>AtdJFh9| zMq^u&c7<0`;(t#;-%1v3wJQJn|KsZ|psMQH_FuXsqy-64Qj{(!K|rOXLusT_M7kRh zR6sx)6r_8D#HK-zP6^qBgdniVy@?HHE}!@PzW@J>^PI8AKn4N}m~+j!=KZ^`>+<;i zP0;04SMDlB5V3vhm2Kpm-FA;So1Em-6eGp})48bxp{-LtH9{UK%?S14*7|lcgW19W z=5Bix@2O<$Z8tSK3~wado|W&q4pG! z*KC7Q_nNe;;1j!`g}~Xd%6JAe;)Q-Mz3P4_SjGSC^}AP|>b&rhDft>58O^`E$liq9 zKvTSF+_!9(|Cd+Wp;Zpi#rWl$$5%)RNdDy&*#oW>rEKC;WuEi*WnOL(Onn2)zuR#P zZ#mfMdu&p~=*b!s&LW6N2rk0FRp}pdGyL;1v=eC4vcxALpi?Gu!?OXuPy`ij#4&wO zh3RZY2l&Q1hCCMdxc~M#z{6ESW^=Jti{$_3;{W^z|L0q_2yn&1O6`uG_z>}y-|9O+ z4))6GYY@=L16ay8B0MJ&i2kG=pkWmbjtF!Y+)A#)xDB6g= zL79$21~4vA9?MI=KUKYP)zN04v|H;kkx&b%pyFO0urC6kS-;qdX3(jQ)fve84Qc|N zeosNlCFJk20~}2}!wYlN(~3~1L}%cC%_c!?$sO6ib*MZ8C7>>w69zy(Zzh$7aegP` zJ8F`gL8!!0GFEiv8hF4oy~eb*QJ@#Xp_b&JXke_Nw+nu3=kuPgXx*%U@X68RO6Ln; zhf*HUQse^kHHnaT<>UJ}k$9Nau+)aJ-02yVmfrZM82R<#Su02oDn6YX1Q8qJDosM@}+V%;vAUPoGBsFFWECwanQL4Sx9T3y@$ESrNAik3U}NnA8bav14VSsH%NV$Ku zmo}!z8L^0Y7b1|3gO5wy9sCk6Xx9%hcNY*h(_;kZ);?%LUXVK-O*n>{u2Wi!KPsnw zf?{+qVJwGiP+lfkGZ!`g-N)E>aaw^;sC7rF;SeOe%bl;wq9iXHdS%G%7yc$ccX(qi z&UPz?^SqHA-)hN0bW2HO3#w&z(*HBApB9~+j1MZ+DxQ%$A&Re}dF(YVW9z6g)5%tO zlZ*2CGYTy6yqvwfoAyrq0Q9UV@vO)l6=_ZuoB=_|7j(;X%g$#5x6@!kY57(ywDz1( zr`KuY<$$g13Mkt4o)v;%PTDtpY|ox&zpl7?fl^Ll3Is9^6h**>VheX_QkEh!c#m~V z7K`PNSN7R_Zx{zv#>yYg{c~VHp=0k4#RnyLcX6D9I=1Cql3;6-jcix{6{fnfJl*kE ze?a{_3!W4z)yRGcB#SfIAkO0gk~ALiqFB#<^zsHiQL=VF;NSIbkMkMLjbd@Acys)2v(b%dJ z#W}mvkLsiNvZeVI-rf{2F6$E+d|U;ds~!jjFJiz*>M$-?L|qrlzXORfFu1H}`PS7B zPkl!thoUbxROE_rqmz`Sr!E$!&pn5>0d|A9)IJ7)0dE_djV=ZRiGGc z)!_xScPD_bVlFsXPog20FM+pk=QP~AM~C36&gGM(kKbr*=A_O71y)`7Gymdi;5s7J zIA2C9dB88XK~wA?e%ooop#C0M?q;(dKNdPBzff_$G8g+67>1lhjYBzlE!u# zJ+Frx9<^WsYZ8%fp=QfL=59tV@`1=S#JSqY?2|7Cfe|`?xZ=6bqxk&=&NRZY(xZl_A-628W zDPiAQf*gNvh7@#DG7AsttauVleGJx9IhVD#b&%U)GA;}5)3W8p{nX9wESDFxa%$-y zUQKiLtlGDvZXO70lj#D1u5iPHUUo)Dn6SEUR5m7k!<}uOh2$v?xz?1pvb^(bEu5I&f~iFgO|8y=_+zf0 zh$1U$XRp<>U(*xWvxjfAMr4mja)~b3#riQn1cOJy+}Lau8gE>6yey&&%(7f=x;<>=JQ#7W7e;<~&CJp%p&Iv)pGOs*P_c z+qX9spY#5%&#>R}GPpgVns?n>Lu38oX~2AEkDHgSxQ-BugrSXWGxgGUfFYKdXkUfu z1ZjaH)J z#62Rxlj5IFQd<5z=N%EGiVl(GoU<%8r?Ff-oI_l^qO}qCeA$Ud^qPf0O`jw6VUC2n z5h1^2`#5bQ$%Ukplj3*x>wVPsQ}vKEzm|1&4{4_&VNI|!J^v&_0#_%<47m?D)$TVSsURSqWVV_;4D0sD@dn$D*^+;-W% z5l;XWs^<}qB#VB#qYs$tPrth*MH&Q3ULXQcpZ0w+HQ1B_G*Ls-nVr0RJR158#a<7* z$IbvnEg*UAYK`A7vo1Lt6INn-oQVP3&xj1BnnX9kgM*G_lPkEz;I8lJ>kMudg5~uU z+zWc-J2qU{>pcr#ka***B$dtWFUS3|Tv{0ote6cIjZ}*l2&;43QXidI?EL%oHqJD{Qcwd{c3FktWgv_SmnJB{%%P zxP}0=`ib6;o37RhdEJVH1VZl9GBHK@?>DS&EZS>p(McG!yXB$X@+7LKk7gf;-;l8P ztwOt7a&9C0*!h$;gA`#H2_wmC-yCTu{!R~z7Hit}C9Vf*tv}Nd=W7CwcRlumO*bTS zt~h(y*BClU8uc-uchJV~VxkitrA(P7J@plxeY?%?0?4N4VTe1HtX0=dM9d>N9sNN= z`4KffES~j{0xOHVpAXeI-J+4b?RsZi7VZW=Dswf`Ipi(5(14Ybg?Cfrjl8_^N2+Sa zqdM7vt&uN*(feyu6f{kQ{k4+vG;aG9&k81lz>)U2wX2DG1TqLPMV4Pk3zTXzF`EG6 zMh)1$Zbenf^~ya@i(hBx@T=6WTqG7z!(ZIbqWeeT0i~Q-&+QvvxtK>Nug#eUb0#OU zC!$A%VgXjnwPtuhaNUp?i$$=gmT;0l&Y)8(6FQn@n;d6f4g-OG*O!hL;zUlC>^an1 z&t?U(h{egLtX&I84%G&pBRo?-HPR2(N+A`O*h=2&p*wsz*5)DWX*FQU=Xc#gy`~7elgf(maU|QqZ{bTEQRzr=l?0Qo_JQ@~ zN_VnwwfJuBHklR1VK+G@gP?0{L7&=BXhnqV8DN3jVqNR6kA14*8J~y^6V6h! zzBW{A0G(Fz&fp>60vYFL#N%15q;g`$EJ>CSbC5dd!8A*8FkHvTj@4+h*AW~+639M0 zu=mK%>yD|8ghFooGHcGZ#8YMNSc+7?A{LGsy41leu9lB@ax0t#dT3_@Cz9==?DypwanAKgo_ z25O4RZVEl=aRa3$3HKYnOu+-{7Rg5mk}(wJf|2_2G=A6DINxL2=X4ffdc4w@Lae3Z z43D8vVluw^YI?q8r(EJh%$!EX(MIrO!Kmf(=WEXT?UcWe1-#^~*IL%=`@CrEtzXBG zx0MdvDD8dwy8duPQZ193Gsh#zba{R_kFeYun?CA;#=s?E2iHj(O37}W<_gV=(y^SI zxYpS&)eb<((3KsMSSDO-q7p6sZS+cLm4vu<#l~XTIIE#(GN!$ZMooW+yTZhNhK7qf z)cWaL<9oT%BKVef0+XC$4$AYl?>LK9{DghfV0@LJM~l_+G?n06w#?IOq}g4bKTa*! zBUaO#jNG8O$}9m6o6Fho6Zx~HdU>rF{P=)+fuWtMjj%PXj6!n;mc;(Tgi0weTtjt6 zFdoU4*?_zT_kDF3C;yY>^GcP(R-lK4q`!ne4T?Pg&g7B)M zl{;w+K4(=Fs#330?#q|FSnyf@U6Z;p@Z}mg8W|hE9=Oo{h^=zgq2#x)2KJnMLvMD= z=ZV+n59HJK8i`!8q!@o>@%v*x16_NY8+Nigd-cc5xyJe?+gcS8CGD?UnTx&zGN3sZ z|Diu2DS3n%q>Gu}uKa!4&y_0@{=CB&=Y2gETDL(Fa=W2GG!ju7?Rf9S-jZK_^RB|2x^rZTB3Euo zpjN3vs@iECv4na=LKNH7osY_Drk#uL7G4V(d>6UAt8d796c!as*q=(X?e%lrX?u7S zUW(^yBaG^FK!A|UXJ z>|0tYFCN?N$lwD~0;O@u_dtm+*sN-Jdq`K)Ek)@bly~)dW@4A5 zE*VcGb0K55aBm6wVJp}~%BrKp$X@Jyh%X_Qvg|Rze_f~gUPH^y{YH1~I2Dcrf~?Tb z8T%Ht3N4PwZ)N>~i6*(-yrh*+6w({9k@JN~T%T0izu;eT8W+fN-~Hl8#2wLgONDTC zxf7-AiHLv|De9yz&DHWtSK(LehP#K~3Jw!aDkO>`$gsiIWaOGqpE~`LEt%Ope)-8R zNVkKcSziA%u~6p+suP>LIU`ZRsq}5Ki_BW0t(^cK&*0zyQU`bihu#7c0&DOh91F6yZzK8-J8w6ao;ms zR%6hDT{&*TW2}Y*={EsnSQ0IfM@wB+i7JA~? zw%sh63$U?HK|8gp-GptyQ2`|ay?Iowp>izAl4s2A)Z5ShVMnU;gi5Cuz@ftMR&vYc zwMhCsN@Rc5zW%VF>qH2mmek3@uW`ZS@M?bor+|L}(t20$O9W*1%d;cFBpxaDyk~dd z*y#AcProi#5|@Udde@NEWA8ZLM6#6$dQzbKl=VFg4PWJVE$V2vFBx25c6@#%Tyq?^ zH#wJi&N@-u*nrgWT6_hmrWlc#OCFPk0j18vVFw1bcuBs5-HcD~^3nx%k?{E4aC>c( z=gS`vANo>izUSgQl3HZMW4TcJgJ)>gTeZ1hiCk;)xDlD^sXw`v%4?Ugg258>(ai|w z3(A-c*0#-Pqx$5}&r0g zii;)E|Bl~?><%T8{fJ_a!1dGRHpl1DUcQee%S>4NIiN(9dbnC-NffZIja>_jcC8E(95M0F2x`vO zOK<;Y1h3%Qc?(`iX=&;cGO~zRvS1->dn-X6C6I)b`i&t+DM{v~R1Ddmup~=l%&kyk zZYOrNTyAp(dzH6Uz7H2NC|Gc=8PgD@-oQ@fXaAE^8x10AY6?#V4BE|(vwqJ^*!}KI z{>nffsd?{Xly|&|qQ{^)J9*Qg9bbGncI9R;OPd#&^Y=JJ`gYBq-ZDE%KD2{^~uU#=!DH3ck8kHV9+ zR0utt%R8w~_I{?_o%zuZlfQktbDIKmLde?7_2SxFshvr}hrHWhgp$9br)5^KSHjVw zqw89ibzlal4Ag9HD7#>ya_TF3%#w8!Qx%^E7(fy2UzUFRvG6iQxDS@S+#rn+CEa&gFhXJNV z*>$4L+L~s*~}z&G{4i{-Idn3&gLX+p;)BIq!>M? zCTBiJXzR&=$BtgSWXJb>sc1~|;Du0Io8zy}1@lruy67Vf`VC&RI8@dkw?}LeyBIF5 zGrv4O%V#6T?|9a{Dje$=5A$%2{~au~Pu|Q@a>e@?GEyOFtGr0P{Ql_LV>2Rb6WH7E z6Ecs4BYe*q_)uyU@MR~np44q^82rUG^w_g>xxayJVMnH*RxmE{xN}dWTxIhs7!Vl} z`C0k{+-03wO^FgCByMLtvwIbcOPGr>8`m0|nxH?m zjIV<&SbX$;@!&)Sxe9mtpQc^|_b<;x%RpU>3p^mPy?HnqNNr{tLh_vvHqb_xRv~7j zt{=VmyMTXN=UJ@n=M6TMvtOcp7aAw(wd(%#aLrROOi{TjQWkW`TyjlcV(xa7y@Z3Z ztyO!l03m-=&EsvB1QKL$?7D=*DQVcSeDW~M_gm<%jRP3d)9e;uM^jWJ#_V&{du3%gEJDI0& zf4MXfM+y4|@r)&Wmf<=A@BNgJ_Ly72%L5(TvoJMP#JZk-PVG+ZDAwtsiroNEMq6+GlBU(M0vXk|J|%GMe@v?%L#( z|FW*N8wA>gJOkaYlBDR#UUtjpV#*o#ey;-)TLpW_{ioRN!Xr^cVl^Sc1nQi=Kb-s@ke8z>=%3qHZSBn}GJ z_W$?K;f6j(%d-C0&*+fWi=zq`(8%WwwenkCPC|Mls5Qk4*yA@qyW2Gwkjpgb=^J&I zf%tP5AaERKMTJwW8>C0;?z(;U)-lG0+MrgwH_m$&*^ zVS~W0snF5!g&*ioZ=^BX$Tv~<84*KkU!Q+Du1;%KteQwtv9&vL23(q&Cirw{KjdL# zPhHP5*KrW*o$}vJ;7lU;u3vCh5_zPjTgJV-tBZiiV`nmpAPBQ!%qU|;1;VWU)o=E+zhEi1g87%Q_uhn3|;=> zk{jKJ9I1kU>(X>K=$pR)Ca_3=0a`FxD%V%JPz$a`9%KpGTiN^Yc09J?&=fI|LC&GfFYdD3F zF$0snaLCyTu&21#?~jhKw#7-Ftfh zuu91Cp(&~{#q{I?7=Uibf7FuoUlYy-)!XwG%s42`#s)sv%TZD=fPhsCciCUjRM85g z4SFs#QvBFc;uQRyiX?pY6SGU7gTVAPQ_h~UeP)$BFi+Aih&8=KA%KtJVA&(OT&8dR zp?K-})^RwW<%l~7fYM>PMQn-@1St6jVW#rbN~&){49w{F3IxDthcLae$5q7@22^S3 zqEm((iIz4mskbjd^)};vVJ)Gl_CJ%!Br70ty?=Lh4W#!x^x+SH%Bl_Y5oHj;O4OtK zT!8=zy*e{SS4~dl`->+a*ylWYk24a4>Wo%2ZWcd2Q@vdk4nap2uA%^qBQGi(87sCG ziU5bcTPcUx=q@gT{f+cv2ttRP&q<4n2Bu#;HBkQPm1P6he2R8Ku13io=JsEvd5FC6 zZ1(7)QrXB*pyN0PYK{$OW_A~t6|v7DwtB7Rh5Ad%6vXG&!c&41^?Tc1t0A>Ky(o{B zTRw4Ddj?T*hq)9dN_0k_u3a*U?|NjPZTLT?(^a0C2x(5YYXaMTf$O3A8Sa~8!dhMc z>3ia_M-T0zf&l*!esb(+dT=cWy%cq(NG{n0-Mp;PR@L3+j={m0q(W&WTEhREdsSgo z-kgMDG{0z+-p0aq9#{(fiN08pE|dL3N5-OtZ~P*U%6#Ht*k z{C_By1f^r_?Ao4sm!-mZCP-K5Kq15`w z03f`XmVPeU_WF5(Ms59quOsa>K=@UL?-4!bj(;-X8B!h^6o0H`*dC=J1!5+3HjYTm z&Md1-3+quTVV}*%whlbKA9+ zEQ0{ovrW)`OHjlv7==H_$B{C>n@Y9o+d z)wQ2G2+n!Gem)>{pD{W0GeM-jnk^hO@n-w&;V&m{`o5Rj;;MfTu<+GeooQM)x-*O( z{vS&Ce@&&h&||`Dw<(m634+>Ly`k8G>(h_O`R=ht7rj~UeZWmu&M06MqK@6c%@>us z$WUN)m1hmDHv&+&J^$qotAw(S<#dCVO}&c}GfXi7w7luxPoNFrv?ZHje{x43|9}<{ z;R>Hp)(;GV5Emo%Mb|=;spf_6A>; z^(!y$2dM2S&LjlnG-@Gui2+r^nI;g54JfH>8hPyq0DQ?PIS`ec*2nT?7kqNS(*RP ztXXJzHy{po-hAI_6@NUNB$zJZ+`l_!la$BZ2l)+`(Y;hh1RmF9%REs>#szn6ZZs?b zNuR;$0x;3d5rOdO^BfYxoKf*2j7fE{6O(1FUB4vm3pZLOzL06*0;EvvzQ{qr3p$8=jvH_K};?tU4;}PFS9O zFb9r~fJ@NSvaXa7T?i`oAN&w#BO#&AglIsS{);WVApN)Bxae+tycU`!xoh4tPGGL# zM2ZIPM4$MyOUWQF#mSNb6J&j)W${^@t#l^~VR`9$!9Hw*MVJPXFsiHOu}iQeLB?$C zwZ{ZwIg~)Lw(gFz8nAv3Q55^2vh3;SE(h%PNMN~AS}cU#Ri}=x49z7=da70o>9jXL z5T~>C_`Err(Z4y^AF{6%yqBr%$y^}AjRX%R4aN4*22#Q=H9|TUf>jy@!sXU|m5Ea1+++5EXC5izctOrRC5X}^sb!D`b?wkJJz_6Uh-7#hL*;9pvszyzu` zByd(3-=w(m{WFWSd3q53{LqJmiGY&s$5tO({{tk1Z&f(-2|YvQ&MoW`-nHNv%y8x=`8lZ^fSox?cP%x9`Y`{ejy z)ovH!s|i@Ri3zAiJ$Yp5{;a8%u*UuvEsQM`99dc2eibVhza86*1+Dkci1CM?`ZOFr zD1D?#jtbtET49b)rLBrZr+il`hEo3qROJO-~dGkcDaS zH5_OYc18!L>!BP^|Rg;ntL+cl&CHCVcU*O$9a{QoWhCe)seILCm z%r@|Yj!l?4hd|qZ!u`ZY)Uqr;^4|y$%Fejr=LEp?tTO~|a8tUTg^P}+UI=I@SE^7m z;XLMG_OBA6aL&t+moEzwldxR=p7jdv>1-1yKZr;Tm+(2#Ed9~?{D$l;w>R>>IrXB{ zQlYBfM*4^nAcPOvu`-dJdz?V=b4qt^^gD`BoFzH*Y3Sq%r=DZHS*DQ@S7083lVKTF#Kfwunmw2?`5jZM5DwH?NXD>C^a zcr{wmJ3EkkR2z!$VV<4_t9A3Q4SeI5gkT*0+H8`hwMH$CP)Q*cLnEn$& zimHi_cs|KW7|-Nf`%bE%+L;w?`VhM$jGN@+Iq9c`bZ3S$)+%cv16yq$%j(zNIAJOz zvx`JZgz>!hG^!5hfnK}-ikK_AofiwAUElWBj>GF}u248FpPBTi@_EoqyYFKvFF@4CTU%3E zo`<8y+$Zol;Xh1tHq&rB#D0w<)n5&kKG`?PY21P=8uE9-*p{N)&wHM7p=(H@?M1>| zGwo~3Lqd!M{FL{#y^b5H)v4?jt?j8}6IP_1VjN^9c}WoEh9 zYQFw7t29oi2zdG@J93?j5EQ_OmR4}$ujWhZA6`)qeo0!xha<4Izyoyv5hH1F3U%e@ z*6vHir60;sJz!3>`m!;<0VBBA77&e zUJw^qTMU*cWp5Z|MPf`_oZG@X^3AS^shAo;b$YHFyOh7kUU88SaK(3JWC&0y>!F0) zy)4yphJW!V!g=GFX6`InS57p>79r&4{R6Nl_QWZBl{J`e>&r>vK22{mZ^z-HHs+#U zQ+`QQ;{|oD29jzm=-Qtw5Lj}qKVbazK+^m^N6UU$_X;eLmX|BhgI|?^iz`gA`kC8} zsVjAR&Te?*N=VjkX5qTsUeT{Z@Th&?@{4G2)@b+(Nw|MYEmfL39uYgu{gd{p8pD~E zqE?=%mc%TA|IMdYBVG|?ZVkt@rT(Jti?g9h%?qf0-8u-)>oWYGuap%aeYzOiCVZkuOt)nxb=YA>^B@TwyfB*U}@HGO*S;Hg0nd7wwY-BYr#?{VrYt=972l9?;pLW2# zR^V#L0=^l0rZ!?`9|K$Yyi~BebrTJ*yB{C;%JTQJHB&178_dVNZP3fsU}1qET2X#{ zrqKM)kuVLpK_a7Z$2wuWr-srMlJl4qXJ{?wjAjQzag_RA~D{xe_k(@8RgNi&S$9+yXj*eNUW&(~9sBlN< z$RR6%sCesLn10~=8f;t8<42{dan`fnXWsr4nItbRPCz?ou{-E)b?8>?KyB_s8SPc% zV<3l=lh70T_S0zRyQNOoKG|hANJ+2R4ewu{6g3De9~8V^Y0E0r<3J*y$^&~Xzk+kq#5lim&KC+I; z?~L(4VwR%!Ey35SuCX_##wus}F4wPjIlGJ__%+Lm;P=rYQ`gGIReg_xStA;`WVIphtUHKt7NxxJSYM%7+BoX3|poI5s?JuGxsC`dSgt z;U;2_39zq3S;0rnrPL@6{5N&7i*fnSh-VbzYebqOW^eMVY04n4wB01v;RsikR$Wnx z!@km1Q45e}l!NK_c6<^nKylo>BOb}xQ*KZIBYE`>D(CPS6fGYq zW-V3w^D!(h+Jf~~V$ktX*wg0Bc+wx@E(qynP=$mUj|db7qTR8&Z(12+Suk1Pf7U2HkC~f?$#7F{Alw6|Y{DHpPVFof zKep5{7s0>^L+awW4bdD~1f8)(I#uXx^2MBYNVVP6<05Nn7LVHL;&(LB#8tkkrCRMWG{6^3+#wisIey!H=EL}w9D-B@?;y6PP z2jP0p^jhF4TqAb3W|5HMe34M@hgYKU>hyy->k%%5>zw#$0fvF-Gm>N2$OPF*$s%8F=k{E<@EV?$`42=aa63G-580`(x?rL zub#Qa7M5XJcE_Rvv{81)2p0?3EzFO*w9T4C5yF$zK8jwq2jSnF4&)?(&-e3B-fG|( zb14MNPGc+x@LzXWzsSIPqvZwv8bWsLF!aa|Yiu6a7CkoOnWYWFa=_s&L4Sw<=W^>9 z0e1p}TFH9#js?bPJ^fnGd*ls|0LJ=y{OxfXu1YF)Ar|$+(@zqb%+Vh(H5x!BHBA1% zn&pAKR|*I!=bXIEn;Vh!kKoXg0hRG)Wf>3>OmD-Otl_&MgplR)S%I;cTH!y%Ws01ezX?=YcdJK9BI(~v5CtJ zs25B&i-&Dz!qKuPtEb9{T36@Fe;$gA|I1}||8f}=5zm!pc7GK{c9c5Tq;~%*jDnWO z+CKc{GT-SRSO5QV8A!)7oaw6P@ZWFze-fF0-XCEE%w;#OdSBt+DaL#|pdsr+^}M3| z|B(d!R~q!c|8B&AYk4Uf`Q-iw-I$kK%E!m1m+0L+y2Fr43(**ofYnvWxDbUi5A;fM zF)9{(1uAyvz0rTJ+$LfboL~5lCW*VYRUq(a%tqjeUF*pG_O>7a08$i)|D7EjFhK06 zm$hD`>|fat&`CuQD2IkX6@9#z0ZqFi;k(hk%TV z%%;{3BvOzCf$%Xq0DeRPzn$-p4av@wsh@K_NCz1BmW2kD=*}__^?u5+4T@h5^&oG& z5fqrGO+cMncwxr5{^vYUNq^q^1Zl}_Kc(?ug9dK{STU5MGJoA`{++rl@y9U`pWEsM zBthx7uAe3ELk3>fUl;sPpj)=5u07^C5|ERhkl5%rwOK4V&EH}m#2HRpkQ{{BGP9ld z0}i*DxUTF_vcyh(E5?~YBpFq-dk$Rfn(`8$&pCAlJ^#ik-qZ}+6QdtmRAt5I-NIzz zkBn8KI$@Hi%(-Rjq=z}qhoe#rz;$dMI4kMUn?zi08nhPgY_9=vQF_>P1eV4I^*A}t zY`ql4Tx5at-15xk2y`$g%YZo#jozo1wSS(l3B(x88l|SrCyMD+Bl{V@M3oAi=TeKk z()Wb*Y%cy*`SP2JtPrJGKOvRExvJ^@)k|ol%m&V5 zrkwM}^8lK8K(0$nkGg_D32Lvy{~Y+lZA017jz2&Ok8r**@W}cc)IpuKC_Zb)5d>`8 ze9{_DK!HJR90~`&0P1;0ZxuX)OOP_9vjPQP75@o&pmV{dHrM)F-E}{Y(!TUJBp{*( z0cMv)ngPXvnMzZi=~v$^dqX0h<<#C*bobrkMfYdOv|;}q0CN&hKtneEKWZm5^daMB zwM7gQ0<8??#h*bEHN=)FY9E}D6UQ`kF!3OM5>h@5K{?rh02-F=*LZ#GuN6sbes#jh zdO8wye&hA+WmaS`p>FHh{JLx`pVkLE;X92RH(L9_GF&LLZ4V{gG?;-%oO7VPksf&f zKLO5qe?YkFvrF)2s)=2>07`*WND*KZQTLVVvq`!&uKh>5OQ`sD>}ZF@(vcwc`l>ry z0&)phQ++b?-|NPJcdS2`0X^+J!gs68;{E1DT}41yP`^%=woJkbr%IH`-2B=-ulBw# zZf+MR>wNrB%`{W13y3f_3`{)rDCCSmgxh&9_!BVNcVKDpo&lj;Anmp=rvg(R-jsFT z`GG&`FG^8i0p0Dxi)jJlaPFc`URWbkGeEzyma$U{*|C_Zz4eLz2GVwLHb(CE0;OUeH@_Ny%*@&A9%o#}##RQr&h#DrSEbt?*cR~yhVlkjI3J8QmWOgB4k*R(} zvBY1i_fwGJ0>`!RShd(~K~3$d%yo_B!e+U3o*>`ufE=w-bz3UW-&ADuCNH%hcS4iI z8}oa>V&>jl2}jEXnDI}#69T+wWQeNfT$;*0sJ5hjK>&-cQ?kMFH4wJ9Rd=M|t(5i> z_)-;Noqghx>3hBUC|TCPCfQ9v+@!S+EHouJgAf|H1chGRAWQsT*{J4qdr}4`D&KfO zIRcc66fV6112n3mlx6=Q$;%ePmxK!{viU))d}hC)P5SyXt;YYdlZR}n*&yr4wDhBH z8jrqGonGjNB_8OW6Ma)k#A}kn5Qlw>{pHa_p{!{9wHXg4sXt?DVu-J#8p@+ci0ZQB zrpl7TmsZ(xtQ8)GEi)`WP1GoD>ad=aSs~sV;Q?N8toT9}Vi!5w4?^UP7ey<4-t0>x z#kTz=DB1a#XH2#1qh>l;CVR}!Wwv_7Y7w6Yoj2Hexom%aQ6rF?bqz~wh-QW+1}MLJ zILEn<*_=CVGPwr^$km?=nausaCWZxi6xi|=`C~wIvFOYhu$nVj7lm%>L!Ya)75Icf zppELSmdjZ?7PSW_5=8c4TZcRpqht!O8dMPk#6Kyh)7Ps-K&T(zgm5!S$w zZA8}MsQWzuA0sCPR;<7&EuAn{qRb54rzvsJ#!sYv$jCWTsx0s&@7YO@I6Su&4Pol{ zF!(FlbC23fK*cMr63yv`CX3od@X0sXTRqea=ODAX9;$G) zfYLa40B7D_pEY+Ttx)%Q)$Nh$%8GKf1C(xVq=ufziym-q*x{d=r znPH{t)N4GWpvblBM zEfm~O%n2xYZStKZC@q9`gB}+u++`9iXz1@`q)y~N8^ig-tascWT~rhKsQ1NCMG`sxMlsf-+4@M4ysv)@K5JDeFvm?JwrVg9cuxFDQ#9P3x9vjr`DuliQ9M@ z13nq4aXpXLe$_ZdJdqAFFrrn3-KSyIz!P%z61q?GBgr(}LVd7aloI7zI~Bi*e-kVE0`-HshyUp-l_l<;ySlfG+3(PoBkIwiA~-B$Xpr?iNxwizJi& zC~aVqvEGo~6i7|=lDWA}KAh(#>E-#loLQK4@;t=a-TV!LFK*z#{eEtl77m}sXv@1F zw-L&Y^wD7*bytC?^LsO~E1-vk6ctL}ob&izaXRyMO_Tu*!(ZpP{aY(uo=5HC(cH?J zrFatl*vvqzrg8iJ;mkq^MeK`5E7nah_zA1Ck>pLaUkANh!VjM;26>Yk(u$W18lQ2oSjCE<-iLOu0yUmga8(FiHum0|wyifyZ8eLB#~o7lbHlAYNEUS^mnOEo&CmRgYNvz4fF};tamEGDnwf_ zh-bX$=P?$uF(xqlV7{Dii48x9pf|PC5-zuK-v?~FDYo-h_VHU467A;&V{iDJUEVGu9$$Tf#R9gY!;bx9q%5i`n_ZOdhT%>an?6(*ahT zd%F=Gp{cuHqTE!w9!uM6`&C+e^QOL)HK~+JMM-iYT|-Z_6p_r-KTLd~MwxYxv2F(! z2s>$9PN)ns9}v%b<9$Gw@`n(>r4Yp&XJ7r1=Z=vnD-?R%H;1339c0fdpABEA*wwq} zEj;eMfnRn9K&3kki#XknHi$FYlf}A8B4E!8`WZccR3d)mgpu!q+s_#0yLR*4BReuf zg1#wiy{nW_-3h5bc@uIf>Y(9jcvhN;F5i=NSB=sn)Szom7dQ8lGs1knK0)!BJs^g) zgb;aa$H5cQ9BkmKUMk0JwvAcx|~ix=RU(x-FGh+hkwTGx`g_YKSN+wu^iXO z+{zT^nEsVC3V(2JK2FTTq}s&(HIo}A!7be=)L(JSAl&<&-B_?LqaWUv$WOC^Bl%rFA`rq$Iwk-wJtiJ#(Fx`e zKT5&O>xs$eb6jvNSeQN*8 zJ7a&>n-QOpKaqhGp>l-`Bau>D6nM&~N<@Z=OTSqOzZ?pdzAlwp;-*z4%4$E6$l<%b zHp@IMc`>9e!B|+GuYqpgCyQ|XVy>XoDc*;TAS0!$dBxGfgE5>GX#jPMWEZEnTDNk5 z{<1@Zrp3aymt;jh3=j;vUv$cfk6t*mt5$tBa`!C?vjj5))9~Qz9{=?3h#;#W857voW~Q zK#|?;+ZHpj{j1cDD>i1+q$ws-lIl)BF<0i~`whLMs~YhyJuC~hyL`>r?j)OXA~gtf zxo~o&q?o-e$Yn=*C{ZHVQMwXxwb_y)&nnGBr0ymXAIPgV6{Ku4ZvIj`ekdua;mB5@ zC4>BF2HJ3>v@Rs56zUk#Qj2gab<-B{xPpYdKjmd|2TjC$_9m}bc0b1bxpbRk!`LQy z6wjs6pBn7yndlgvAXq_1kpH-ja3GTiMQD5aR2Rvi=yiV1eXg@Q%Cl_&u-V&3Qa zL`$1&j6QRM5!K;ju4gZMo4>og;SahPw)@)90t%LSrP}0gm)LN;RrJ4q(L^3cPR&`f zg^%w36nxbo269s;LQf+b!U85Fw}rmxUfWP?l$h@_1j^2~ZzoRZ z!9wo^BQM~@cplKmur=hE-f^&RAp${k-j+*QjTKYz0wxt5H7i}tAF3yij(v(``derg z(oTw!ke+%@+0%UwRJwjyX4^gQdw@|q!iDQ%VNj=Jywj)z(iLQ`wXrga)#HR+JPH2# zUCk_dH&6YoippBCQLPHipS|Dw9?T~BC58e$C3D8xZOT*!paSmzt!DzX)iC*sL+rdw zl|M+Uz=b^8?{}Vk8q+x-+(+JkY@*!}ftvDcho}lM1l@!uE8CBTZnVjb8%U zoe0cv>{G&7G4M$XEX~Z*BgWFT0`GGI%V^zvo~rQnf+ZKsLL6KpC5J!F>M17KXa{`mB0Xj(P$2X~w4zEZpfX~Q=`zZe zKUjIEbE{a&SUDwZn`VZvxTo;jgE@=HmSsgt+PhfNi~HOJbE`yY4R)s+K1QUd0@82j zOD0g41~*4WT{~(qb`!&zB~}Sp7Lx)v(PQ~M`0r7iB%|UBNySW)M>s`^f+0+ZOqone z>v`~(NRk6i(*@r8(5hga!2Q8^bW-huM;>|?0|i`ScrYs|w4cjJUq~K1{XsW2=jQK@ zv)N+yw7+655XiDa^`i%Zj+GX*ufL$*6^`x|i5RkL8zJG3Em21Qw->zEzdaNv7vBAK z20|JiFGMuVgcBW*TDQlhjWgv!NR#Y@TZ8Cfru}U#=88D=!H%Sxy_@PBnI~^ZmgTX8 zMf$6Ra|6LZ1w2;x)UL^@P>N>GuPt^1$T3Ug-$oK|z$og%%1OZ6f2IWQkE z#M~rDuf|&Z#r#QW*!NX%2-YoTZ*^XBOFR}IW?MO43`;8^Y&bH+Gj73kNW4&*D%ge) zyQ{jKK1Et&RVDUdK^%H2Pz)n8o*f1Pl0Ch9X`Z`vMVzGRGd z&?{zVvrl49uL$4)L@GV$A&pqr2y1u(j$)i*+rO>exMi^x%6 zf=A2L<uCK6_FzIs&@yh2f4eSV+IqJ5`sfkYa_dAFH|?NTk$v`(`25nGR$E^+MYyrLRY|1bKDU6LFh^U(MbzNLlG+cGfL086le&mxV+=;go1gEt8+jeooPu!pn>1 z2gQ^x?a5tL=Z5I>@Y3~#OlY1FehE{u>~^^&CM>GUnYc5j{7&o$N*=>m?q4~j07EpkT`-21slfK2 zU=&A#ZpI51*6}P7_AZZfQ~Q9v=%QAc{krTfpIQG2)A=qDWLINDPkdDadpYVn2PB2@ zLwvf_9cHq4zuNzD^iHA>2JSn4R$+2n6?9ZE8tNIZh&A9ImINx)dDPg~mD%=Y*;M#a zcPCPwJbHzQLrq+akiK(i>oPTsIIVL#k#?C6wj1*!LkvEZNNpD|&V!I~!E6)~SaJDO zbRPT=8X4C*&-Bly*`@JV3_#2iCWK~IiU&JA2tWFZ#D6_z1DfMKwOCRwzom~9+uABK zr))ZSUTcsMmu<);`@f24l#kGvL9rUYiVDjQ)){Z(xjcu}3%=m82rDa8G0V`)Pcb*p z`#}$+&d3d+BqWev0QP#IZV=?maMBCuq_X-6j2o>XN(F6wP*1H}FpOJbMa}SG@ENKf zP1fB|_gN;{3nEd=uWyd|CL}U;-fR5tk{>G%H!v}F`F?)fRIPtDBE}lctXyHwnE6_y;petMzV1}EJlz; z?a2?P!}*=|mV`$0fC{cT2U>-fNTx=t*htRSIs2J4tf=i_Rh{3O{$0@2FgJN& zM(1XOcNr#w#zN}g?xBby5y1_EPC9}d)jzS z_MdoLI05Q+$;87rgXe6N31Z-sSHGo>y$>#gk7q1_=P$QUOigUj{9XilHe!w1Yg7TJ znUKyTSHsCmt5$E;3#Olybp+i!UUh*LxR9Ma@*`5yBC9Sr(I;;n|( zDTjNRX+GZb#qjuc_F302I7?dC8&(cM{t4nvCkFrj7u6Yz9NPzWjDQ_U3Nxl`^0MKj zWQ7`UW|%BWQYPdta#;!LEHKuWq68-AUQj#yZ0P|n{cMQch1sF%JZ-~iA{}z){!EJX zBDvrWL@cOYCkh7mj>u8C0Z1*<3~R=lX}ih+wzmPGXG5nAsC7>p%=Xm9LNSq)(Z~2I zXpx%Gq;&5EH){R=H}rpxoQRo8=7&3k9`~aT5r-+vI{wkTh`uzx2q1WgbK5_z*o_Xc zgQJjCZh;=_5IhArJ4xOWKlHd!Kq81k%0*(}b?pvzM2baX#eBgK-Gw0Lt^I{j?vyq) zis1V68O~>#B4{bL#I-00xyTTh5+=6*r~N$)_A8Dh)Le%DEadn>t${DV;;`4 z8z5W*_Y%fy4qN{IT*e5Ddg{Ju`!xae+e?G*0zerC`gA_9sn%{qB5U+}I72rX>ln$vxNaoT({H*wKc@={MSP9*8a~G{DET- zEUWkXV@_YVpiRT;bvQ8G@ZtD6LUg~L_jfBo8@=W7yHvKX12M=hL~83kSDulF^XDx& zo^`A!>FT`;gtqN`Fr9|@p4x7zWJzIB3q%$fqj7pXc(T${!=|{)D&t)?D|{=yxdS|s zc0XkB6_%Mg!ZNEKhs`fff)1ze$InN6DGYKKXWtKY5WK!*G%A8S03Aq927YoX;y;LZ z1;MkLy7zGGJp1uV{JNv8SIIjU(yuQov8*JXa%2N* zpdW9?Nj4?IIUE_m((Jm~drzIXtr7gT>~+lKY*V6!ZNuW9Y$f*&9TsW{tJ?4NO1``#pzaJkllAXu zEE?4%fylhD{m|E}2-Bi5U3~<952ek1#N|sakM>NSHMZ?d2n#J?UK23SHRv&Vls}>D zRv^Lfi$w_D{A#4E!SzX>*}5xc1xywZs=D7v7X5Aw7|(NcvDrRvy!@XLMcFnTJ=1## z6y{i;5`HKgK@9uRKVzL0QE%6GfCzPI{iNu_QRRxHXs}y3KnMpXm(Mf?Gx~+W1o5=? z+B3BbA)UWm19o@9MTBn7CBbPMa|1GbOa&$Md{ynJAlgV^njbsgzD5R0| z#~0ec4_QCFV574CNb>EKν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 diff --git a/results/results.png b/results/results.png new file mode 100644 index 0000000000000000000000000000000000000000..710494855a4a49f2d607d2c78f1a605d54ef2ce7 GIT binary patch literal 486284 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

tz29lUK!Zj#vdvb(1@=DruH6EVEq~T{& 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

V#KZ6R@ec}leKTGgx^n}wy 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 np.array: - """standard normalization. - - Args: - data (np.array): raw time series data. - output_dir (str): output dir path. - train_index (list): train index. - - Returns: - np.array: normalized raw time series data. - """ - # data: L, N, C - data_train = data[:train_index[-1][1], ...] - - mean, std = data_train[..., 0].mean(), data_train[..., 0].std() - - print("mean (training data):", mean) - print("std (training data):", std) - scaler = {} - scaler['func'] = standard_re_transform.__name__ - scaler['args'] = {"mean":mean, "std":std} - pickle.dump(scaler, open(output_dir + "/scaler.pkl", 'wb')) - - def normalize(x): - return (x - mean) / std - - data_norm = normalize(data) - return data_norm - -def standard_re_transform(x, **kwargs): - mean, std = kwargs['mean'], kwargs['std'] - x = x * std - x = x + mean - return x - -def generate_data(args): - """preprocess and generate train/valid/test datasets. - - Args: - args (Namespace): args for processing data. - """ - C = args.C - future_seq_len = args.future_seq_len - history_seq_len = args.history_seq_len - add_time_in_day = True - add_day_in_week = args.dow - output_dir = args.output_dir - - # read data - data = np.loadtxt(args.data_file_path, delimiter=',') - data = np.expand_dims(data, axis=-1) - data = data[..., C] - print("Data shape: {0}".format(data.shape)) - - L, N, F = data.shape - num_samples = L - (history_seq_len + future_seq_len) + 1 - train_num_short = round(num_samples * train_ratio) - valid_num_short = round(num_samples * valid_ratio) - test_num_short = num_samples - train_num_short - valid_num_short - print("train_num_short:{0}".format(train_num_short)) - print("valid_num_short:{0}".format(valid_num_short)) - print("test_num_short:{0}".format(test_num_short)) - - index_list = [] - for t in range(history_seq_len, num_samples + history_seq_len): - index = (t-history_seq_len, t, t+future_seq_len) - index_list.append(index) - train_index = index_list[:train_num_short] - valid_index = index_list[train_num_short: train_num_short + valid_num_short] - test_index = index_list[train_num_short + valid_num_short: train_num_short + valid_num_short + test_num_short] - - scaler = standard_transform - data_norm = scaler(data, output_dir, train_index) - - # add external feature - feature_list = [data_norm] - if add_time_in_day: - # numerical time_in_day - time_ind = [i%args.steps_per_day / args.steps_per_day for i in range(data_norm.shape[0])] - time_ind = np.array(time_ind) - time_in_day = np.tile(time_ind, [1, N, 1]).transpose((2, 1, 0)) - feature_list.append(time_in_day) - if add_day_in_week: - # numerical day_in_week - day_in_week = [(i // args.steps_per_day)%7 for i in range(data_norm.shape[0])] - day_in_week = np.array(day_in_week) - day_in_week = np.tile(day_in_week, [1, N, 1]).transpose((2, 1, 0)) - feature_list.append(day_in_week) - - processed_data = np.concatenate(feature_list, axis=-1) - - # dump data - index = {} - index['train'] = train_index - index['valid'] = valid_index - index['test'] = test_index - pickle.dump(index, open(output_dir + "/index.pkl", "wb")) - - data = {} - data['processed_data'] = processed_data - pickle.dump(data, open(output_dir + "/data.pkl", "wb")) - -if __name__ == "__main__": - history_seq_len = 168 # sliding window size for generating history sequence and target sequence - future_seq_len = 12 - - train_ratio = 0.6 - valid_ratio = 0.2 - C = [0] # selected channels - steps_per_day = 12 # 60min - - name = "Electricity336" - dow = True # if add day_of_week feature - output_dir = 'datasets/' + name - data_file_path = 'datasets/raw_data/{0}/{1}.csv'.format(name, name) - - parser = argparse.ArgumentParser() - parser.add_argument("--output_dir", type=str, default=output_dir, help="Output directory.") - parser.add_argument("--data_file_path", type=str, default=data_file_path, help="Raw traffic readings.") - parser.add_argument("--history_seq_len", type=int, default=history_seq_len, help="Sequence Length.") - parser.add_argument("--future_seq_len", type=int, default=future_seq_len, help="Sequence Length.") - parser.add_argument("--steps_per_day", type=int, default=steps_per_day, help="Sequence Length.") - parser.add_argument("--dow", type=bool, default=dow, help='Add feature day_of_week.') - parser.add_argument("--C", type=list, default=C, help='Selected channels.') - parser.add_argument("--train_ratio", type=float, default=train_ratio, help='Train ratio') - parser.add_argument("--valid_ratio", type=float, default=valid_ratio, help='Validate ratio.') - - args = parser.parse_args() - if os.path.exists(args.output_dir): - reply = str(input(f'{args.output_dir} exists. Do you want to overwrite it? (y/n)')).lower().strip() - if reply[0] != 'y': exit - else: - os.makedirs(args.output_dir) - generate_data(args) diff --git a/scripts/data_preparation/METR-LA/generate_training_data.py b/scripts/data_preparation/METR-LA/generate_training_data.py index 8e0f953f..d44d69f9 100644 --- a/scripts/data_preparation/METR-LA/generate_training_data.py +++ b/scripts/data_preparation/METR-LA/generate_training_data.py @@ -1,89 +1,56 @@ -import argparse -import pickle +import os +import sys import shutil +import pickle +import argparse + import numpy as np -import os import pandas as pd -""" -METR-LA dataset (traffic speed dataset) default settings: - - normalization: - standard norm - - dataset division: - 7:1:2 - - windows size: - 12 - - features: - traffic speed - time in day - day in week - - target: - predicting the traffic speed -""" - -def standard_transform(data: np.array, output_dir: str, train_index: list) -> np.array: - """standard normalization. +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../../..")) +from basicts.data.transform import standard_transform - Args: - data (np.array): raw time series data. - output_dir (str): output dir path. - train_index (list): train index. - Returns: - np.array: normalized raw time series data. - """ - # data: L, N, C - data_train = data[:train_index[-1][1], ...] - - mean, std = data_train[..., 0].mean(), data_train[..., 0].std() - - print("mean (training data):", mean) - print("std (training data):", std) - scaler = {} - scaler['func'] = standard_re_transform.__name__ - scaler['args'] = {"mean":mean, "std":std} - pickle.dump(scaler, open(output_dir + "/scaler.pkl", 'wb')) - - def normalize(x): - return (x - mean) / std - - data_norm = normalize(data) - return data_norm - -def standard_re_transform(x, **kwargs): - mean, std = kwargs['mean'], kwargs['std'] - x = x * std - x = x + mean - return x - -def generate_data(args): - """preprocess and generate train/valid/test datasets. +def generate_data(args: argparse.Namespace): + """Preprocess and generate train/valid/test datasets. + Default settings of METR-LA dataset: + - Normalization method: standard norm. + - Dataset division: 7:1:2. + - Window size: history 12, future 12. + - Channels (features): three channels [traffic speed, time of day, day of week] + - Target: predict the traffic speed of the future 12 time steps. Args: - args (Namespace): args for processing data. + args (argparse): configurations of preprocessing """ - C = args.C - future_seq_len = args.future_seq_len + + target_channel = args.target_channel + future_seq_len = args.future_seq_len history_seq_len = args.history_seq_len - add_time_in_day = True - add_day_in_week = args.dow - output_dir = args.output_dir + add_time_of_day = args.tod + add_day_of_week = args.dow + output_dir = args.output_dir + train_ratio = args.train_ratio + valid_ratio = args.valid_ratio + data_file_path = args.data_file_path + graph_file_path = args.graph_file_path # read data - df = pd.read_hdf(args.data_file_path) + df = pd.read_hdf(data_file_path) data = np.expand_dims(df.values, axis=-1) - data = data[..., C] - print("Data shape: {0}".format(data.shape)) + data = data[..., target_channel] + print("raw time series shape: {0}".format(data.shape)) - L, N, F = data.shape - num_samples = L - (history_seq_len + future_seq_len) + 1 + l, n, f = data.shape + num_samples = l - (history_seq_len + future_seq_len) + 1 train_num_short = round(num_samples * train_ratio) valid_num_short = round(num_samples * valid_ratio) - test_num_short = num_samples - train_num_short - valid_num_short - print("train_num_short:{0}".format(train_num_short)) - print("valid_num_short:{0}".format(valid_num_short)) - print("test_num_short:{0}".format(test_num_short)) + test_num_short = num_samples - train_num_short - valid_num_short + print("number of training samples:{0}".format(train_num_short)) + print("number of validation samples:{0}".format(valid_num_short)) + print("number of test samples:{0}".format(test_num_short)) index_list = [] for t in range(history_seq_len, num_samples + history_seq_len): @@ -92,69 +59,95 @@ def generate_data(args): train_index = index_list[:train_num_short] valid_index = index_list[train_num_short: train_num_short + valid_num_short] - test_index = index_list[train_num_short + valid_num_short: train_num_short + valid_num_short + test_num_short] - - scaler = standard_transform - data_norm = scaler(data, output_dir, train_index) + test_index = index_list[train_num_short + + valid_num_short: train_num_short + valid_num_short + test_num_short] + + scaler = standard_transform + data_norm = scaler(data, output_dir, train_index) # add external feature feature_list = [data_norm] - if add_time_in_day: - # numerical time_in_day - time_ind = (df.index.values - df.index.values.astype("datetime64[D]")) / np.timedelta64(1, "D") - time_in_day = np.tile(time_ind, [1, N, 1]).transpose((2, 1, 0)) - feature_list.append(time_in_day) - - if add_day_in_week: - # numerical day_in_week + if add_time_of_day: + # numerical time_of_day + tod = ( + df.index.values - df.index.values.astype("datetime64[D]")) / np.timedelta64(1, "D") + tod_tiled = np.tile(tod, [1, n, 1]).transpose((2, 1, 0)) + feature_list.append(tod_tiled) + + if add_day_of_week: + # numerical day_of_week dow = df.index.dayofweek - dow_tiled = np.tile(dow, [1, N, 1]).transpose((2, 1, 0)) + dow_tiled = np.tile(dow, [1, n, 1]).transpose((2, 1, 0)) feature_list.append(dow_tiled) processed_data = np.concatenate(feature_list, axis=-1) # dump data index = {} - index['train'] = train_index - index['valid'] = valid_index - index['test'] = test_index - pickle.dump(index, open(output_dir + "/index.pkl", "wb")) + index["train"] = train_index + index["valid"] = valid_index + index["test"] = test_index + with open(output_dir + "/index_in{0}_out{1}.pkl".format(history_seq_len, future_seq_len), "wb") as f: + pickle.dump(index, f) data = {} - data['processed_data'] = processed_data - pickle.dump(data, open(output_dir + "/data.pkl", "wb")) + data["processed_data"] = processed_data + with open(output_dir + "/data.pkl", "wb") as f: + pickle.dump(data, f) # copy adj - shutil.copyfile(args.graph_file_path, output_dir + '/adj_mx.pkl') # copy models + shutil.copyfile(graph_file_path, output_dir + "/adj_mx.pkl") + if __name__ == "__main__": - history_seq_len = 12 # sliding window size for generating history sequence and target sequence - future_seq_len = 12 - - train_ratio = 0.7 - valid_ratio = 0.1 - C = [0] # selected channels - - name = "METR-LA" - dow = True # if add day_of_week feature - output_dir = 'datasets/' + name - data_file_path = 'datasets/raw_data/{0}/{1}.h5'.format(name, name) - graph_file_path = 'datasets/raw_data/{0}/adj_{1}.pkl'.format(name, name) - - parser = argparse.ArgumentParser() - parser.add_argument("--output_dir", type=str, default=output_dir, help="Output directory.") - parser.add_argument("--data_file_path", type=str, default=data_file_path, help="Raw traffic readings.") - parser.add_argument("--graph_file_path", type=str, default=graph_file_path, help="Raw traffic readings.") - parser.add_argument("--history_seq_len", type=int, default=history_seq_len, help="Sequence Length.") - parser.add_argument("--future_seq_len", type=int, default=future_seq_len, help="Sequence Length.") - parser.add_argument("--dow", type=bool, default=dow, help='Add feature day_of_week.') - parser.add_argument("--C", type=list, default=C, help='Selected channels.') - parser.add_argument("--train_ratio", type=float, default=train_ratio, help='Train ratio') - parser.add_argument("--valid_ratio", type=float, default=valid_ratio, help='Validate ratio.') - - args = parser.parse_args() - if os.path.exists(args.output_dir): - reply = str(input(f'{args.output_dir} exists. Do you want to overwrite it? (y/n)')).lower().strip() - if reply[0] != 'y': exit + # sliding window size for generating history sequence and target sequence + HISTORY_SEQ_LEN = 12 + FUTURE_SEQ_LEN = 12 + + TRAIN_RATIO = 0.7 + VALID_RATIO = 0.1 + TARGET_CHANNEL = [0] # target channel(s) + + DATASET_NAME = "METR-LA" + TOD = True # if add time_of_day feature + DOW = True # if add day_of_week feature + OUTPUT_DIR = "datasets/" + DATASET_NAME + DATA_FILE_PATH = "datasets/raw_data/{0}/{0}.h5".format(DATASET_NAME) + GRAPH_FILE_PATH = "datasets/raw_data/{0}/adj_{0}.pkl".format(DATASET_NAME) + + parser = argparse.ArgumentParser() + parser.add_argument("--output_dir", type=str, + default=OUTPUT_DIR, help="Output directory.") + parser.add_argument("--data_file_path", type=str, + default=DATA_FILE_PATH, help="Raw traffic readings.") + parser.add_argument("--graph_file_path", type=str, + default=GRAPH_FILE_PATH, help="Raw traffic readings.") + parser.add_argument("--history_seq_len", type=int, + default=HISTORY_SEQ_LEN, help="Sequence Length.") + parser.add_argument("--future_seq_len", type=int, + default=FUTURE_SEQ_LEN, help="Sequence Length.") + parser.add_argument("--tod", type=bool, default=TOD, + help="Add feature time_of_day.") + parser.add_argument("--dow", type=bool, default=DOW, + help="Add feature day_of_week.") + parser.add_argument("--target_channel", type=list, + default=TARGET_CHANNEL, help="Selected channels.") + parser.add_argument("--train_ratio", type=float, + default=TRAIN_RATIO, help="Train ratio") + parser.add_argument("--valid_ratio", type=float, + default=VALID_RATIO, help="Validate ratio.") + args_metr = parser.parse_args() + + # print args + print("-"*(20+45+5)) + for key, value in sorted(vars(args_metr).items()): + print("|{0:>20} = {1:<45}|".format(key, str(value))) + print("-"*(20+45+5)) + + if os.path.exists(args_metr.output_dir): + reply = str(input( + f"{args_metr.output_dir} exists. Do you want to overwrite it? (y/n)")).lower().strip() + if reply[0] != "y": + sys.exit(0) else: - os.makedirs(args.output_dir) - generate_data(args) + os.makedirs(args_metr.output_dir) + generate_data(args_metr) diff --git a/scripts/data_preparation/PEMS-BAY/generate_training_data.py b/scripts/data_preparation/PEMS-BAY/generate_training_data.py index e41234c9..789db414 100644 --- a/scripts/data_preparation/PEMS-BAY/generate_training_data.py +++ b/scripts/data_preparation/PEMS-BAY/generate_training_data.py @@ -1,89 +1,56 @@ -import argparse -import pickle +import os +import sys import shutil +import pickle +import argparse + import numpy as np -import os import pandas as pd -""" -PEMS-BAY dataset (traffic speed dataset) default settings: - - normalization: - standard norm - - dataset division: - 7:1:2 - - windows size: - 12 - - features: - traffic speed - time in day - day in week - - target: - predicting the traffic speed -""" - -def standard_transform(data: np.array, output_dir: str, train_index: list) -> np.array: - """standard normalization. +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../../..")) +from basicts.data.transform import standard_transform - Args: - data (np.array): raw time series data. - output_dir (str): output dir path. - train_index (list): train index. - Returns: - np.array: normalized raw time series data. - """ - # data: L, N, C - data_train = data[:train_index[-1][1], ...] - - mean, std = data_train[..., 0].mean(), data_train[..., 0].std() - - print("mean (training data):", mean) - print("std (training data):", std) - scaler = {} - scaler['func'] = standard_re_transform.__name__ - scaler['args'] = {"mean":mean, "std":std} - pickle.dump(scaler, open(output_dir + "/scaler.pkl", 'wb')) - - def normalize(x): - return (x - mean) / std - - data_norm = normalize(data) - return data_norm - -def standard_re_transform(x, **kwargs): - mean, std = kwargs['mean'], kwargs['std'] - x = x * std - x = x + mean - return x - -def generate_data(args): - """preprocess and generate train/valid/test datasets. +def generate_data(args: argparse.Namespace): + """Preprocess and generate train/valid/test datasets. + Default settings of PEMS-BAY dataset: + - Normalization method: standard norm. + - Dataset division: 7:1:2. + - Window size: history 12, future 12. + - Channels (features): three channels [traffic speed, time of day, day of week] + - Target: predict the traffic speed of the future 12 time steps. Args: - args (Namespace): args for processing data. + args (argparse): configurations of preprocessing """ - C = args.C - future_seq_len = args.future_seq_len + + target_channel = args.target_channel + future_seq_len = args.future_seq_len history_seq_len = args.history_seq_len - add_time_in_day = True - add_day_in_week = args.dow - output_dir = args.output_dir + add_time_of_day = args.tod + add_day_of_week = args.dow + output_dir = args.output_dir + train_ratio = args.train_ratio + valid_ratio = args.valid_ratio + data_file_path = args.data_file_path + graph_file_path = args.graph_file_path # read data - df = pd.read_hdf(args.data_file_path) + df = pd.read_hdf(data_file_path) data = np.expand_dims(df.values, axis=-1) - data = data[..., C] - print("Data shape: {0}".format(data.shape)) + data = data[..., target_channel] + print("raw time series shape: {0}".format(data.shape)) - L, N, F = data.shape - num_samples = L - (history_seq_len + future_seq_len) + 1 + l, n, f = data.shape + num_samples = l - (history_seq_len + future_seq_len) + 1 train_num_short = round(num_samples * train_ratio) valid_num_short = round(num_samples * valid_ratio) - test_num_short = num_samples - train_num_short - valid_num_short - print("train_num_short:{0}".format(train_num_short)) - print("valid_num_short:{0}".format(valid_num_short)) - print("test_num_short:{0}".format(test_num_short)) + test_num_short = num_samples - train_num_short - valid_num_short + print("number of training samples:{0}".format(train_num_short)) + print("number of validation samples:{0}".format(valid_num_short)) + print("number of test samples:{0}".format(test_num_short)) index_list = [] for t in range(history_seq_len, num_samples + history_seq_len): @@ -92,69 +59,95 @@ def generate_data(args): train_index = index_list[:train_num_short] valid_index = index_list[train_num_short: train_num_short + valid_num_short] - test_index = index_list[train_num_short + valid_num_short: train_num_short + valid_num_short + test_num_short] - - scaler = standard_transform - data_norm = scaler(data, output_dir, train_index) + test_index = index_list[train_num_short + + valid_num_short: train_num_short + valid_num_short + test_num_short] + + scaler = standard_transform + data_norm = scaler(data, output_dir, train_index) # add external feature feature_list = [data_norm] - if add_time_in_day: - # numerical time_in_day - time_ind = (df.index.values - df.index.values.astype("datetime64[D]")) / np.timedelta64(1, "D") - time_in_day = np.tile(time_ind, [1, N, 1]).transpose((2, 1, 0)) - feature_list.append(time_in_day) - - if add_day_in_week: - # numerical day_in_week + if add_time_of_day: + # numerical time_of_day + tod = ( + df.index.values - df.index.values.astype("datetime64[D]")) / np.timedelta64(1, "D") + tod_tiled = np.tile(tod, [1, n, 1]).transpose((2, 1, 0)) + feature_list.append(tod_tiled) + + if add_day_of_week: + # numerical day_of_week dow = df.index.dayofweek - dow_tiled = np.tile(dow, [1, N, 1]).transpose((2, 1, 0)) + dow_tiled = np.tile(dow, [1, n, 1]).transpose((2, 1, 0)) feature_list.append(dow_tiled) processed_data = np.concatenate(feature_list, axis=-1) # dump data index = {} - index['train'] = train_index - index['valid'] = valid_index - index['test'] = test_index - pickle.dump(index, open(output_dir + "/index.pkl", "wb")) + index["train"] = train_index + index["valid"] = valid_index + index["test"] = test_index + with open(output_dir + "/index_in{0}_out{1}.pkl".format(history_seq_len, future_seq_len), "wb") as f: + pickle.dump(index, f) data = {} - data['processed_data'] = processed_data - pickle.dump(data, open(output_dir + "/data.pkl", "wb")) + data["processed_data"] = processed_data + with open(output_dir + "/data.pkl", "wb") as f: + pickle.dump(data, f) # copy adj - shutil.copyfile(args.graph_file_path, output_dir + '/adj_mx.pkl') # copy models + shutil.copyfile(graph_file_path, output_dir + "/adj_mx.pkl") + if __name__ == "__main__": - history_seq_len = 12 # sliding window size for generating history sequence and target sequence - future_seq_len = 12 - - train_ratio = 0.7 - valid_ratio = 0.1 - C = [0] # selected channels - - name = "PEMS-BAY" - dow = True # if add day_of_week feature - output_dir = 'datasets/' + name - data_file_path = 'datasets/raw_data/{0}/{1}.h5'.format(name, name) - graph_file_path = 'datasets/raw_data/{0}/adj_{1}.pkl'.format(name, name) - - parser = argparse.ArgumentParser() - parser.add_argument("--output_dir", type=str, default=output_dir, help="Output directory.") - parser.add_argument("--data_file_path", type=str, default=data_file_path, help="Raw traffic readings.") - parser.add_argument("--graph_file_path", type=str, default=graph_file_path, help="Raw traffic readings.") - parser.add_argument("--history_seq_len", type=int, default=history_seq_len, help="Sequence Length.") - parser.add_argument("--future_seq_len", type=int, default=future_seq_len, help="Sequence Length.") - parser.add_argument("--dow", type=bool, default=dow, help='Add feature day_of_week.') - parser.add_argument("--C", type=list, default=C, help='Selected channels.') - parser.add_argument("--train_ratio", type=float, default=train_ratio, help='Train ratio') - parser.add_argument("--valid_ratio", type=float, default=valid_ratio, help='Validate ratio.') - - args = parser.parse_args() - if os.path.exists(args.output_dir): - reply = str(input(f'{args.output_dir} exists. Do you want to overwrite it? (y/n)')).lower().strip() - if reply[0] != 'y': exit + # sliding window size for generating history sequence and target sequence + HISTORY_SEQ_LEN = 12 + FUTURE_SEQ_LEN = 12 + + TRAIN_RATIO = 0.7 + VALID_RATIO = 0.1 + TARGET_CHANNEL = [0] # target channel(s) + + DATASET_NAME = "PEMS-BAY" + TOD = True # if add time_of_day feature + DOW = True # if add day_of_week feature + OUTPUT_DIR = "datasets/" + DATASET_NAME + DATA_FILE_PATH = "datasets/raw_data/{0}/{0}.h5".format(DATASET_NAME) + GRAPH_FILE_PATH = "datasets/raw_data/{0}/adj_{0}.pkl".format(DATASET_NAME) + + parser = argparse.ArgumentParser() + parser.add_argument("--output_dir", type=str, + default=OUTPUT_DIR, help="Output directory.") + parser.add_argument("--data_file_path", type=str, + default=DATA_FILE_PATH, help="Raw traffic readings.") + parser.add_argument("--graph_file_path", type=str, + default=GRAPH_FILE_PATH, help="Raw traffic readings.") + parser.add_argument("--history_seq_len", type=int, + default=HISTORY_SEQ_LEN, help="Sequence Length.") + parser.add_argument("--future_seq_len", type=int, + default=FUTURE_SEQ_LEN, help="Sequence Length.") + parser.add_argument("--tod", type=bool, default=TOD, + help="Add feature time_of_day.") + parser.add_argument("--dow", type=bool, default=DOW, + help="Add feature day_of_week.") + parser.add_argument("--target_channel", type=list, + default=TARGET_CHANNEL, help="Selected channels.") + parser.add_argument("--train_ratio", type=float, + default=TRAIN_RATIO, help="Train ratio") + parser.add_argument("--valid_ratio", type=float, + default=VALID_RATIO, help="Validate ratio.") + args_metr = parser.parse_args() + + # print args + print("-"*(20+45+5)) + for key, value in sorted(vars(args_metr).items()): + print("|{0:>20} = {1:<45}|".format(key, str(value))) + print("-"*(20+45+5)) + + if os.path.exists(args_metr.output_dir): + reply = str(input( + f"{args_metr.output_dir} exists. Do you want to overwrite it? (y/n)")).lower().strip() + if reply[0] != "y": + sys.exit(0) else: - os.makedirs(args.output_dir) - generate_data(args) + os.makedirs(args_metr.output_dir) + generate_data(args_metr) diff --git a/scripts/data_preparation/PEMS03/generate_adj_mx.py b/scripts/data_preparation/PEMS03/generate_adj_mx.py index 88c07932..33c6ab2e 100644 --- a/scripts/data_preparation/PEMS03/generate_adj_mx.py +++ b/scripts/data_preparation/PEMS03/generate_adj_mx.py @@ -1,170 +1,84 @@ import os -import numpy as np import csv import pickle -def get_adjacency_matrix(distance_df_filename, num_of_vertices, id_filename=None): - ''' - Parameters - ---------- - distance_df_filename: str, path of the csv file contains edges information - - num_of_vertices: int, the number of vertices - - Returns - ---------- - A: np.ndarray, adjacency matrix - - ''' - if 'npy' in distance_df_filename: - adj_mx = np.load(distance_df_filename) - return adj_mx, None - else: - A = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - distaneA = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - # distance file中的id并不是从0开始的 所以要进行重新的映射;id_filename是节点的顺序 - if id_filename: - with open(id_filename, 'r') as f: - id_dict = {int(i): idx for idx, i in enumerate(f.read().strip().split('\n'))} # 把节点id(idx)映射成从0开始的索引 - with open(distance_df_filename, 'r') as f: - f.readline() # 略过表头那一行 - reader = csv.reader(f) - for row in reader: - if len(row) != 3: - continue - i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[id_dict[i], id_dict[j]] = 1 - distaneA[id_dict[i], id_dict[j]] = distance - return A, distaneA - else: # distance file中的id直接从0开始 - with open(distance_df_filename, 'r') as f: - f.readline() - reader = csv.reader(f) - for row in reader: - if len(row) != 3: - continue - i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[i, j] = 1 - distaneA[i, j] = distance - return A, distaneA - -def get_adjacency_matrix_2direction(distance_df_filename, num_of_vertices, id_filename=None): - ''' - Parameters - ---------- - distance_df_filename: str, path of the csv file contains edges information - num_of_vertices: int, the number of vertices - - Returns - ---------- - A: np.ndarray, adjacency matrix - ''' - if 'npy' in distance_df_filename: - adj_mx = np.load(distance_df_filename) - return adj_mx, None - else: - A = np.zeros((int(num_of_vertices), int(num_of_vertices)), dtype=np.float32) - distaneA = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - # distance file中的id并不是从0开始的 所以要进行重新的映射;id_filename是节点的顺序 - if id_filename: - with open(id_filename, 'r') as f: - id_dict = {int(i): idx for idx, i in enumerate(f.read().strip().split('\n'))} # 把节点id(idx)映射成从0开始的索引 - with open(distance_df_filename, 'r') as f: - f.readline() # 略过表头那一行 - reader = csv.reader(f) - for row in reader: - if len(row) != 3: - continue - i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[id_dict[i], id_dict[j]] = 1 - A[id_dict[j], id_dict[i]] = 1 - distaneA[id_dict[i], id_dict[j]] = distance - distaneA[id_dict[j], id_dict[i]] = distance - return A, distaneA - else: # distance file中的id直接从0开始 - with open(distance_df_filename, 'r') as f: - f.readline() - reader = csv.reader(f) - for row in reader: - if len(row) != 3: - continue - i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[i, j] = 1 - A[j, i] = 1 - distaneA[i, j] = distance - distaneA[j, i] = distance - return A, distaneA +import numpy as np -def get_adjacency_matrix_2direction(distance_df_filename, num_of_vertices, id_filename=None): - ''' - Parameters - ---------- - distance_df_filename: str, path of the csv file contains edges information +def get_adjacency_matrix(distance_df_filename: str, num_of_vertices: int, id_filename: str = None) -> tuple: + """Generate adjacency matrix. - num_of_vertices: int, the number of vertices + Args: + distance_df_filename (str): path of the csv file contains edges information + num_of_vertices (int): number of vertices + id_filename (str, optional): id filename. Defaults to None. - Returns - ---------- - A: np.ndarray, adjacency matrix + Returns: + tuple: two adjacency matrix. + np.array: connectivity-based adjacency matrix A (A[i, j]=0 or A[i, j]=1) + np.array: distance-based adjacency matrix A + """ - ''' - if 'npy' in distance_df_filename: + if "npy" in distance_df_filename: adj_mx = np.load(distance_df_filename) return adj_mx, None else: - A = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - distaneA = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - # distance file中的id并不是从0开始的 所以要进行重新的映射;id_filename是节点的顺序 + adjacency_matrix_connectivity = np.zeros((int(num_of_vertices), int( + num_of_vertices)), dtype=np.float32) + adjacency_matrix_distance = np.zeros((int(num_of_vertices), int(num_of_vertices)), + dtype=np.float32) if id_filename: - with open(id_filename, 'r') as f: - id_dict = {int(i): idx for idx, i in enumerate(f.read().strip().split('\n'))} # 把节点id(idx)映射成从0开始的索引 - with open(distance_df_filename, 'r') as f: - f.readline() # 略过表头那一行 + # the id in the distance file does not start from 0, so it needs to be remapped + with open(id_filename, "r") as f: + id_dict = {int(i): idx for idx, i in enumerate( + f.read().strip().split("\n"))} # map node idx to 0-based index (start from 0) + with open(distance_df_filename, "r") as f: + f.readline() # omit the first line reader = csv.reader(f) for row in reader: if len(row) != 3: continue i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[id_dict[i], id_dict[j]] = 1 - A[id_dict[j], id_dict[i]] = 1 - distaneA[id_dict[i], id_dict[j]] = distance - distaneA[id_dict[j], id_dict[i]] = distance - return A, distaneA - else: # distance file中的id直接从0开始 - with open(distance_df_filename, 'r') as f: + adjacency_matrix_connectivity[id_dict[i], id_dict[j]] = 1 + adjacency_matrix_connectivity[id_dict[j], id_dict[i]] = 1 + adjacency_matrix_distance[id_dict[i], + id_dict[j]] = distance + adjacency_matrix_distance[id_dict[j], + id_dict[i]] = distance + return adjacency_matrix_connectivity, adjacency_matrix_distance + else: + # ids in distance file start from 0 + with open(distance_df_filename, "r") as f: f.readline() reader = csv.reader(f) for row in reader: if len(row) != 3: continue i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[i, j] = 1 - A[j, i] = 1 - distaneA[i, j] = distance - distaneA[j, i] = distance - return A, distaneA + adjacency_matrix_connectivity[i, j] = 1 + adjacency_matrix_connectivity[j, i] = 1 + adjacency_matrix_distance[i, j] = distance + adjacency_matrix_distance[j, i] = distance + return adjacency_matrix_connectivity, adjacency_matrix_distance + -def generate_adj_PEMS03(): - direction = True +def generate_adj_pems03(): distance_df_filename, num_of_vertices = "datasets/raw_data/PEMS03/PEMS03.csv", 358 - if os.path.exists(distance_df_filename.split(".")[0] + ".txt"): - id_filename = distance_df_filename.split(".")[0] + ".txt" + if os.path.exists(distance_df_filename.split(".", maxsplit=1)[0] + ".txt"): + id_filename = distance_df_filename.split(".", maxsplit=1)[0] + ".txt" else: id_filename = None - if direction: - adj_mx, distance_mx = get_adjacency_matrix_2direction(distance_df_filename, num_of_vertices, id_filename=id_filename) - else: - adj_mx, distance_mx = get_adjacency_matrix(distance_df_filename, num_of_vertices, id_filename=id_filename) - # TODO: the self loop is missing + adj_mx, distance_mx = get_adjacency_matrix( + distance_df_filename, num_of_vertices, id_filename=id_filename) + # the self loop is missing add_self_loop = False if add_self_loop: + print("adding self loop to adjacency matrices.") adj_mx = adj_mx + np.identity(adj_mx.shape[0]) distance_mx = distance_mx + np.identity(distance_mx.shape[0]) - pickle.dump(adj_mx, open("datasets/raw_data/PEMS03/adj_PEMS03.pkl", 'wb')) - pickle.dump(distance_mx, open("datasets/raw_data/PEMS03/adj_PEMS03_distance.pkl", 'wb')) + else: + print("kindly note that there is no self loop in adjacency matrices.") + with open("datasets/raw_data/PEMS03/adj_PEMS03.pkl", "wb") as f: + pickle.dump(adj_mx, f) + with open("datasets/raw_data/PEMS03/adj_PEMS03_distance.pkl", "wb") as f: + pickle.dump(distance_mx, f) diff --git a/scripts/data_preparation/PEMS03/generate_training_data.py b/scripts/data_preparation/PEMS03/generate_training_data.py index 4bc38b64..f02e5954 100644 --- a/scripts/data_preparation/PEMS03/generate_training_data.py +++ b/scripts/data_preparation/PEMS03/generate_training_data.py @@ -1,169 +1,163 @@ -import argparse -import pickle +import os +import sys import shutil +import pickle +import argparse + import numpy as np -import os -from generate_adj_mx import generate_adj_PEMS03 - -""" -PEMS03 dataset (traffic flow dataset) default settings: - - sampling frequency: - 5min - - normalization: - standard norm - - dataset division: - 6:2:2 - - windows size: - 12 - - features: - traffic flow - --traffic occupy--(not used) - --traffic speed--(not used) - time in day - day in week - - target: - predicting the traffic speed -""" - -def standard_transform(data: np.array, output_dir: str, train_index: list) -> np.array: - """standard normalization. +from generate_adj_mx import generate_adj_pems03 +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../../..")) +from basicts.data.transform import standard_transform - Args: - data (np.array): raw time series data. - output_dir (str): output dir path. - train_index (list): train index. - Returns: - np.array: normalized raw time series data. - """ - # data: L, N, C - data_train = data[:train_index[-1][1], ...] - - mean, std = data_train[..., 0].mean(), data_train[..., 0].std() - - print("mean (training data):", mean) - print("std (training data):", std) - scaler = {} - scaler['func'] = standard_re_transform.__name__ - scaler['args'] = {"mean":mean, "std":std} - pickle.dump(scaler, open(output_dir + "/scaler.pkl", 'wb')) - - def normalize(x): - return (x - mean) / std - - data_norm = normalize(data) - return data_norm - -def standard_re_transform(x, **kwargs): - mean, std = kwargs['mean'], kwargs['std'] - x = x * std - x = x + mean - return x - -def generate_data(args): - """preprocess and generate train/valid/test datasets. +def generate_data(args: argparse.Namespace): + """Preprocess and generate train/valid/test datasets. + Default settings of PEMS03 dataset: + - Normalization method: standard norm. + - Dataset division: 6:2:2. + - Window size: history 12, future 12. + - Channels (features): three channels [traffic flow, time of day, day of week] + - Target: predict the traffic speed of the future 12 time steps. Args: - args (Namespace): args for processing data. + args (argparse): configurations of preprocessing """ - C = args.C - future_seq_len = args.future_seq_len + + target_channel = args.target_channel + future_seq_len = args.future_seq_len history_seq_len = args.history_seq_len - add_time_in_day = True - add_day_in_week = args.dow + add_time_of_day = args.tod + add_day_of_week = args.dow output_dir = args.output_dir + train_ratio = args.train_ratio + valid_ratio = args.valid_ratio + data_file_path = args.data_file_path + graph_file_path = args.graph_file_path + steps_per_day = args.steps_per_day # read data - data = np.load(args.data_file_path)['data'] - data = data[..., C] - print("Data shape: {0}".format(data.shape)) + data = np.load(data_file_path)["data"] + data = data[..., target_channel] + print("raw time series shape: {0}".format(data.shape)) - L, N, F = data.shape - num_samples = L - (history_seq_len + future_seq_len) + 1 + l, n, f = data.shape + num_samples = l - (history_seq_len + future_seq_len) + 1 train_num_short = round(num_samples * train_ratio) valid_num_short = round(num_samples * valid_ratio) - test_num_short = num_samples - train_num_short - valid_num_short - print("train_num_short:{0}".format(train_num_short)) - print("valid_num_short:{0}".format(valid_num_short)) - print("test_num_short:{0}".format(test_num_short)) + test_num_short = num_samples - train_num_short - valid_num_short + print("number of training samples:{0}".format(train_num_short)) + print("number of validation samples:{0}".format(valid_num_short)) + print("number of test samples:{0}".format(test_num_short)) - index_list = [] + index_list = [] for t in range(history_seq_len, num_samples + history_seq_len): index = (t-history_seq_len, t, t+future_seq_len) index_list.append(index) + train_index = index_list[:train_num_short] valid_index = index_list[train_num_short: train_num_short + valid_num_short] - test_index = index_list[train_num_short + valid_num_short: train_num_short + valid_num_short + test_num_short] - + test_index = index_list[train_num_short + + valid_num_short: train_num_short + valid_num_short + test_num_short] + scaler = standard_transform data_norm = scaler(data, output_dir, train_index) # add external feature feature_list = [data_norm] - if add_time_in_day: - # numerical time_in_day - time_ind = [i%args.steps_per_day / args.steps_per_day for i in range(data_norm.shape[0])] - time_ind = np.array(time_ind) - time_in_day = np.tile(time_ind, [1, N, 1]).transpose((2, 1, 0)) - feature_list.append(time_in_day) - if add_day_in_week: - # numerical day_in_week - day_in_week = [(i // args.steps_per_day)%7 for i in range(data_norm.shape[0])] - day_in_week = np.array(day_in_week) - day_in_week = np.tile(day_in_week, [1, N, 1]).transpose((2, 1, 0)) - feature_list.append(day_in_week) + if add_time_of_day: + # numerical time_of_day + tod = [i % steps_per_day / + steps_per_day for i in range(data_norm.shape[0])] + tod = np.array(tod) + tod_tiled = np.tile(tod, [1, n, 1]).transpose((2, 1, 0)) + feature_list.append(tod_tiled) + + if add_day_of_week: + # numerical day_of_week + dow = [(i // steps_per_day) % 7 for i in range(data_norm.shape[0])] + dow = np.array(dow) + dow_tiled = np.tile(dow, [1, n, 1]).transpose((2, 1, 0)) + feature_list.append(dow_tiled) processed_data = np.concatenate(feature_list, axis=-1) # dump data index = {} - index['train'] = train_index - index['valid'] = valid_index - index['test'] = test_index - pickle.dump(index, open(output_dir + "/index.pkl", "wb")) + index["train"] = train_index + index["valid"] = valid_index + index["test"] = test_index + with open(output_dir + "/index_in{0}_out{1}.pkl".format(history_seq_len, future_seq_len), "wb") as f: + pickle.dump(index, f) data = {} - data['processed_data'] = processed_data - pickle.dump(data, open(output_dir + "/data.pkl", "wb")) + data["processed_data"] = processed_data + with open(output_dir + "/data.pkl", "wb") as f: + pickle.dump(data, f) # copy adj - if os.path.exists(args.graph_file_path): - shutil.copyfile(args.graph_file_path, output_dir + '/adj_mx.pkl') # copy models + if os.path.exists(graph_file_path): + # copy + shutil.copyfile(graph_file_path, output_dir + "/adj_mx.pkl") else: - generate_adj_PEMS03() - shutil.copyfile(args.graph_file_path, output_dir + '/adj_mx.pkl') # copy models + # generate and copy + generate_adj_pems03() + shutil.copyfile(graph_file_path, output_dir + "/adj_mx.pkl") + if __name__ == "__main__": - history_seq_len = 12 # sliding window size for generating history sequence and target sequence - future_seq_len = 12 - - train_ratio = 0.6 - valid_ratio = 0.2 - C = [0] # selected channels - steps_per_day = 288 # 5min - - name = "PEMS03" - dow = True # if add day_of_week feature - output_dir = 'datasets/' + name - data_file_path = 'datasets/raw_data/{0}/{1}.npz'.format(name, name) - graph_file_path = 'datasets/raw_data/{0}/adj_{1}.pkl'.format(name, name) - - parser = argparse.ArgumentParser() - parser.add_argument("--output_dir", type=str, default=output_dir, help="Output directory.") - parser.add_argument("--data_file_path", type=str, default=data_file_path, help="Raw traffic readings.") - parser.add_argument("--graph_file_path", type=str, default=graph_file_path, help="Raw traffic readings.") - parser.add_argument("--history_seq_len", type=int, default=history_seq_len, help="Sequence Length.") - parser.add_argument("--future_seq_len", type=int, default=future_seq_len, help="Sequence Length.") - parser.add_argument("--steps_per_day", type=int, default=steps_per_day, help="Sequence Length.") - parser.add_argument("--dow", type=bool, default=dow, help='Add feature day_of_week.') - parser.add_argument("--C", type=list, default=C, help='Selected channels.') - parser.add_argument("--train_ratio", type=float, default=train_ratio, help='Train ratio') - parser.add_argument("--valid_ratio", type=float, default=valid_ratio, help='Validate ratio.') - - args = parser.parse_args() - if os.path.exists(args.output_dir): - reply = str(input(f'{args.output_dir} exists. Do you want to overwrite it? (y/n)')).lower().strip() - if reply[0] != 'y': exit + # sliding window size for generating history sequence and target sequence + HISTORY_SEQ_LEN = 12 + FUTURE_SEQ_LEN = 12 + + TRAIN_RATIO = 0.6 + VALID_RATIO = 0.2 + TARGET_CHANNEL = [0] # target channel(s) + STEPS_PER_DAY = 288 + + DATASET_NAME = "PEMS03" + TOD = True # if add time_of_day feature + DOW = True # if add day_of_week feature + OUTPUT_DIR = "datasets/" + DATASET_NAME + DATA_FILE_PATH = "datasets/raw_data/{0}/{0}.npz".format(DATASET_NAME) + GRAPH_FILE_PATH = "datasets/raw_data/{0}/adj_{0}.pkl".format(DATASET_NAME) + + parser = argparse.ArgumentParser() + parser.add_argument("--output_dir", type=str, + default=OUTPUT_DIR, help="Output directory.") + parser.add_argument("--data_file_path", type=str, + default=DATA_FILE_PATH, help="Raw traffic readings.") + parser.add_argument("--graph_file_path", type=str, + default=GRAPH_FILE_PATH, help="Raw traffic readings.") + parser.add_argument("--history_seq_len", type=int, + default=HISTORY_SEQ_LEN, help="Sequence Length.") + parser.add_argument("--future_seq_len", type=int, + default=FUTURE_SEQ_LEN, help="Sequence Length.") + parser.add_argument("--steps_per_day", type=int, + default=STEPS_PER_DAY, help="Sequence Length.") + parser.add_argument("--tod", type=bool, default=TOD, + help="Add feature time_of_day.") + parser.add_argument("--dow", type=bool, default=DOW, + help="Add feature day_of_week.") + parser.add_argument("--target_channel", type=list, + default=TARGET_CHANNEL, help="Selected channels.") + parser.add_argument("--train_ratio", type=float, + default=TRAIN_RATIO, help="Train ratio") + parser.add_argument("--valid_ratio", type=float, + default=VALID_RATIO, help="Validate ratio.") + args_metr = parser.parse_args() + + # print args + print("-"*(20+45+5)) + for key, value in sorted(vars(args_metr).items()): + print("|{0:>20} = {1:<45}|".format(key, str(value))) + print("-"*(20+45+5)) + + if os.path.exists(args_metr.output_dir): + reply = str(input( + f"{args_metr.output_dir} exists. Do you want to overwrite it? (y/n)")).lower().strip() + if reply[0] != "y": + sys.exit(0) else: - os.makedirs(args.output_dir) - generate_data(args) + os.makedirs(args_metr.output_dir) + generate_data(args_metr) diff --git a/scripts/data_preparation/PEMS04/generate_adj_mx.py b/scripts/data_preparation/PEMS04/generate_adj_mx.py index 27dcd4d9..47d97ffc 100644 --- a/scripts/data_preparation/PEMS04/generate_adj_mx.py +++ b/scripts/data_preparation/PEMS04/generate_adj_mx.py @@ -1,170 +1,84 @@ import os -import numpy as np import csv import pickle -def get_adjacency_matrix(distance_df_filename, num_of_vertices, id_filename=None): - ''' - Parameters - ---------- - distance_df_filename: str, path of the csv file contains edges information - - num_of_vertices: int, the number of vertices - - Returns - ---------- - A: np.ndarray, adjacency matrix - - ''' - if 'npy' in distance_df_filename: - adj_mx = np.load(distance_df_filename) - return adj_mx, None - else: - A = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - distaneA = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - # distance file中的id并不是从0开始的 所以要进行重新的映射;id_filename是节点的顺序 - if id_filename: - with open(id_filename, 'r') as f: - id_dict = {int(i): idx for idx, i in enumerate(f.read().strip().split('\n'))} # 把节点id(idx)映射成从0开始的索引 - with open(distance_df_filename, 'r') as f: - f.readline() # 略过表头那一行 - reader = csv.reader(f) - for row in reader: - if len(row) != 3: - continue - i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[id_dict[i], id_dict[j]] = 1 - distaneA[id_dict[i], id_dict[j]] = distance - return A, distaneA - else: # distance file中的id直接从0开始 - with open(distance_df_filename, 'r') as f: - f.readline() - reader = csv.reader(f) - for row in reader: - if len(row) != 3: - continue - i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[i, j] = 1 - distaneA[i, j] = distance - return A, distaneA - -def get_adjacency_matrix_2direction(distance_df_filename, num_of_vertices, id_filename=None): - ''' - Parameters - ---------- - distance_df_filename: str, path of the csv file contains edges information - num_of_vertices: int, the number of vertices - - Returns - ---------- - A: np.ndarray, adjacency matrix - ''' - if 'npy' in distance_df_filename: - adj_mx = np.load(distance_df_filename) - return adj_mx, None - else: - A = np.zeros((int(num_of_vertices), int(num_of_vertices)), dtype=np.float32) - distaneA = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - # distance file中的id并不是从0开始的 所以要进行重新的映射;id_filename是节点的顺序 - if id_filename: - with open(id_filename, 'r') as f: - id_dict = {int(i): idx for idx, i in enumerate(f.read().strip().split('\n'))} # 把节点id(idx)映射成从0开始的索引 - with open(distance_df_filename, 'r') as f: - f.readline() # 略过表头那一行 - reader = csv.reader(f) - for row in reader: - if len(row) != 3: - continue - i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[id_dict[i], id_dict[j]] = 1 - A[id_dict[j], id_dict[i]] = 1 - distaneA[id_dict[i], id_dict[j]] = distance - distaneA[id_dict[j], id_dict[i]] = distance - return A, distaneA - else: # distance file中的id直接从0开始 - with open(distance_df_filename, 'r') as f: - f.readline() - reader = csv.reader(f) - for row in reader: - if len(row) != 3: - continue - i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[i, j] = 1 - A[j, i] = 1 - distaneA[i, j] = distance - distaneA[j, i] = distance - return A, distaneA +import numpy as np -def get_adjacency_matrix_2direction(distance_df_filename, num_of_vertices, id_filename=None): - ''' - Parameters - ---------- - distance_df_filename: str, path of the csv file contains edges information +def get_adjacency_matrix(distance_df_filename: str, num_of_vertices: int, id_filename: str = None) -> tuple: + """Generate adjacency matrix. - num_of_vertices: int, the number of vertices + Args: + distance_df_filename (str): path of the csv file contains edges information + num_of_vertices (int): number of vertices + id_filename (str, optional): id filename. Defaults to None. - Returns - ---------- - A: np.ndarray, adjacency matrix + Returns: + tuple: two adjacency matrix. + np.array: connectivity-based adjacency matrix A (A[i, j]=0 or A[i, j]=1) + np.array: distance-based adjacency matrix A + """ - ''' - if 'npy' in distance_df_filename: + if "npy" in distance_df_filename: adj_mx = np.load(distance_df_filename) return adj_mx, None else: - A = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - distaneA = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - # distance file中的id并不是从0开始的 所以要进行重新的映射;id_filename是节点的顺序 + adjacency_matrix_connectivity = np.zeros((int(num_of_vertices), int( + num_of_vertices)), dtype=np.float32) + adjacency_matrix_distance = np.zeros((int(num_of_vertices), int(num_of_vertices)), + dtype=np.float32) if id_filename: - with open(id_filename, 'r') as f: - id_dict = {int(i): idx for idx, i in enumerate(f.read().strip().split('\n'))} # 把节点id(idx)映射成从0开始的索引 - with open(distance_df_filename, 'r') as f: - f.readline() # 略过表头那一行 + # the id in the distance file does not start from 0, so it needs to be remapped + with open(id_filename, "r") as f: + id_dict = {int(i): idx for idx, i in enumerate( + f.read().strip().split("\n"))} # map node idx to 0-based index (start from 0) + with open(distance_df_filename, "r") as f: + f.readline() # omit the first line reader = csv.reader(f) for row in reader: if len(row) != 3: continue i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[id_dict[i], id_dict[j]] = 1 - A[id_dict[j], id_dict[i]] = 1 - distaneA[id_dict[i], id_dict[j]] = distance - distaneA[id_dict[j], id_dict[i]] = distance - return A, distaneA - else: # distance file中的id直接从0开始 - with open(distance_df_filename, 'r') as f: + adjacency_matrix_connectivity[id_dict[i], id_dict[j]] = 1 + adjacency_matrix_connectivity[id_dict[j], id_dict[i]] = 1 + adjacency_matrix_distance[id_dict[i], + id_dict[j]] = distance + adjacency_matrix_distance[id_dict[j], + id_dict[i]] = distance + return adjacency_matrix_connectivity, adjacency_matrix_distance + else: + # ids in distance file start from 0 + with open(distance_df_filename, "r") as f: f.readline() reader = csv.reader(f) for row in reader: if len(row) != 3: continue i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[i, j] = 1 - A[j, i] = 1 - distaneA[i, j] = distance - distaneA[j, i] = distance - return A, distaneA + adjacency_matrix_connectivity[i, j] = 1 + adjacency_matrix_connectivity[j, i] = 1 + adjacency_matrix_distance[i, j] = distance + adjacency_matrix_distance[j, i] = distance + return adjacency_matrix_connectivity, adjacency_matrix_distance + -def generate_adj_PEMS04(): - direction = True +def generate_adj_pems04(): distance_df_filename, num_of_vertices = "datasets/raw_data/PEMS04/PEMS04.csv", 307 - if os.path.exists(distance_df_filename.split(".")[0] + ".txt"): - id_filename = distance_df_filename.split(".")[0] + ".txt" + if os.path.exists(distance_df_filename.split(".", maxsplit=1)[0] + ".txt"): + id_filename = distance_df_filename.split(".", maxsplit=1)[0] + ".txt" else: id_filename = None - if direction: - adj_mx, distance_mx = get_adjacency_matrix_2direction(distance_df_filename, num_of_vertices, id_filename=id_filename) - else: - adj_mx, distance_mx = get_adjacency_matrix(distance_df_filename, num_of_vertices, id_filename=id_filename) - # TODO: the self loop is missing + adj_mx, distance_mx = get_adjacency_matrix( + distance_df_filename, num_of_vertices, id_filename=id_filename) + # the self loop is missing add_self_loop = False if add_self_loop: + print("adding self loop to adjacency matrices.") adj_mx = adj_mx + np.identity(adj_mx.shape[0]) distance_mx = distance_mx + np.identity(distance_mx.shape[0]) - pickle.dump(adj_mx, open("datasets/raw_data/PEMS04/adj_PEMS04.pkl", 'wb')) - pickle.dump(distance_mx, open("datasets/raw_data/PEMS04/adj_PEMS04_distance.pkl", 'wb')) + else: + print("kindly note that there is no self loop in adjacency matrices.") + with open("datasets/raw_data/PEMS04/adj_PEMS04.pkl", "wb") as f: + pickle.dump(adj_mx, f) + with open("datasets/raw_data/PEMS04/adj_PEMS04_distance.pkl", "wb") as f: + pickle.dump(distance_mx, f) diff --git a/scripts/data_preparation/PEMS04/generate_training_data.py b/scripts/data_preparation/PEMS04/generate_training_data.py index 653616f6..8ad3e771 100644 --- a/scripts/data_preparation/PEMS04/generate_training_data.py +++ b/scripts/data_preparation/PEMS04/generate_training_data.py @@ -1,167 +1,163 @@ -import argparse -import pickle +import os +import sys import shutil +import pickle +import argparse + import numpy as np -import os -from generate_adj_mx import generate_adj_PEMS04 - -""" -PEMS04 dataset (traffic flow dataset) default settings: - - normalization: - standard norm - - dataset division: - 6:2:2 - - windows size: - 12 - - features: - traffic flow - --traffic occupy--(not used) - --traffic speed--(not used) - time in day - day in week - - target: - predicting the traffic speed -""" - -def standard_transform(data: np.array, output_dir: str, train_index: list) -> np.array: - """standard normalization. +from generate_adj_mx import generate_adj_pems04 +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../../..")) +from basicts.data.transform import standard_transform - Args: - data (np.array): raw time series data. - output_dir (str): output dir path. - train_index (list): train index. - Returns: - np.array: normalized raw time series data. - """ - # data: L, N, C - data_train = data[:train_index[-1][1], ...] - - mean, std = data_train[..., 0].mean(), data_train[..., 0].std() - - print("mean (training data):", mean) - print("std (training data):", std) - scaler = {} - scaler['func'] = standard_re_transform.__name__ - scaler['args'] = {"mean":mean, "std":std} - pickle.dump(scaler, open(output_dir + "/scaler.pkl", 'wb')) - - def normalize(x): - return (x - mean) / std - - data_norm = normalize(data) - return data_norm - -def standard_re_transform(x, **kwargs): - mean, std = kwargs['mean'], kwargs['std'] - x = x * std - x = x + mean - return x - -def generate_data(args): - """preprocess and generate train/valid/test datasets. +def generate_data(args: argparse.Namespace): + """Preprocess and generate train/valid/test datasets. + Default settings of PEMS04 dataset: + - Normalization method: standard norm. + - Dataset division: 6:2:2. + - Window size: history 12, future 12. + - Channels (features): three channels [traffic flow, time of day, day of week] + - Target: predict the traffic speed of the future 12 time steps. Args: - args (Namespace): args for processing data. + args (argparse): configurations of preprocessing """ - C = args.C - future_seq_len = args.future_seq_len + + target_channel = args.target_channel + future_seq_len = args.future_seq_len history_seq_len = args.history_seq_len - add_time_in_day = True - add_day_in_week = args.dow + add_time_of_day = args.tod + add_day_of_week = args.dow output_dir = args.output_dir + train_ratio = args.train_ratio + valid_ratio = args.valid_ratio + data_file_path = args.data_file_path + graph_file_path = args.graph_file_path + steps_per_day = args.steps_per_day # read data - data = np.load(args.data_file_path)['data'] - data = data[..., C] - print("Data shape: {0}".format(data.shape)) + data = np.load(data_file_path)["data"] + data = data[..., target_channel] + print("raw time series shape: {0}".format(data.shape)) - L, N, F = data.shape - num_samples = L - (history_seq_len + future_seq_len) + 1 + l, n, f = data.shape + num_samples = l - (history_seq_len + future_seq_len) + 1 train_num_short = round(num_samples * train_ratio) valid_num_short = round(num_samples * valid_ratio) - test_num_short = num_samples - train_num_short - valid_num_short - print("train_num_short:{0}".format(train_num_short)) - print("valid_num_short:{0}".format(valid_num_short)) - print("test_num_short:{0}".format(test_num_short)) + test_num_short = num_samples - train_num_short - valid_num_short + print("number of training samples:{0}".format(train_num_short)) + print("number of validation samples:{0}".format(valid_num_short)) + print("number of test samples:{0}".format(test_num_short)) - index_list = [] + index_list = [] for t in range(history_seq_len, num_samples + history_seq_len): index = (t-history_seq_len, t, t+future_seq_len) index_list.append(index) + train_index = index_list[:train_num_short] valid_index = index_list[train_num_short: train_num_short + valid_num_short] - test_index = index_list[train_num_short + valid_num_short: train_num_short + valid_num_short + test_num_short] - + test_index = index_list[train_num_short + + valid_num_short: train_num_short + valid_num_short + test_num_short] + scaler = standard_transform data_norm = scaler(data, output_dir, train_index) # add external feature feature_list = [data_norm] - if add_time_in_day: - # numerical time_in_day - time_ind = [i%args.steps_per_day / args.steps_per_day for i in range(data_norm.shape[0])] - time_ind = np.array(time_ind) - time_in_day = np.tile(time_ind, [1, N, 1]).transpose((2, 1, 0)) - feature_list.append(time_in_day) - if add_day_in_week: - # numerical day_in_week - day_in_week = [(i // args.steps_per_day)%7 for i in range(data_norm.shape[0])] - day_in_week = np.array(day_in_week) - day_in_week = np.tile(day_in_week, [1, N, 1]).transpose((2, 1, 0)) - feature_list.append(day_in_week) + if add_time_of_day: + # numerical time_of_day + tod = [i % steps_per_day / + steps_per_day for i in range(data_norm.shape[0])] + tod = np.array(tod) + tod_tiled = np.tile(tod, [1, n, 1]).transpose((2, 1, 0)) + feature_list.append(tod_tiled) + + if add_day_of_week: + # numerical day_of_week + dow = [(i // steps_per_day) % 7 for i in range(data_norm.shape[0])] + dow = np.array(dow) + dow_tiled = np.tile(dow, [1, n, 1]).transpose((2, 1, 0)) + feature_list.append(dow_tiled) processed_data = np.concatenate(feature_list, axis=-1) # dump data index = {} - index['train'] = train_index - index['valid'] = valid_index - index['test'] = test_index - pickle.dump(index, open(output_dir + "/index.pkl", "wb")) + index["train"] = train_index + index["valid"] = valid_index + index["test"] = test_index + with open(output_dir + "/index_in{0}_out{1}.pkl".format(history_seq_len, future_seq_len), "wb") as f: + pickle.dump(index, f) data = {} - data['processed_data'] = processed_data - pickle.dump(data, open(output_dir + "/data.pkl", "wb")) + data["processed_data"] = processed_data + with open(output_dir + "/data.pkl", "wb") as f: + pickle.dump(data, f) # copy adj if os.path.exists(args.graph_file_path): - shutil.copyfile(args.graph_file_path, output_dir + '/adj_mx.pkl') # copy models + # copy + shutil.copyfile(args.graph_file_path, output_dir + "/adj_mx.pkl") else: - generate_adj_PEMS04() - shutil.copyfile(args.graph_file_path, output_dir + '/adj_mx.pkl') # copy models + # generate and copy + generate_adj_pems04() + shutil.copyfile(graph_file_path, output_dir + "/adj_mx.pkl") + if __name__ == "__main__": - history_seq_len = 12 # sliding window size for generating history sequence and target sequence - future_seq_len = 12 - - train_ratio = 0.6 - valid_ratio = 0.2 - C = [0] # selected channels - steps_per_day = 288 # 5min - - name = "PEMS04" - dow = True # if add day_of_week feature - output_dir = 'datasets/' + name - data_file_path = 'datasets/raw_data/{0}/{1}.npz'.format(name, name) - graph_file_path = 'datasets/raw_data/{0}/adj_{1}.pkl'.format(name, name) - - parser = argparse.ArgumentParser() - parser.add_argument("--output_dir", type=str, default=output_dir, help="Output directory.") - parser.add_argument("--data_file_path", type=str, default=data_file_path, help="Raw traffic readings.") - parser.add_argument("--graph_file_path", type=str, default=graph_file_path, help="Raw traffic readings.") - parser.add_argument("--history_seq_len", type=int, default=history_seq_len, help="Sequence Length.") - parser.add_argument("--future_seq_len", type=int, default=future_seq_len, help="Sequence Length.") - parser.add_argument("--steps_per_day", type=int, default=steps_per_day, help="Sequence Length.") - parser.add_argument("--dow", type=bool, default=dow, help='Add feature day_of_week.') - parser.add_argument("--C", type=list, default=C, help='Selected channels.') - parser.add_argument("--train_ratio", type=float, default=train_ratio, help='Train ratio') - parser.add_argument("--valid_ratio", type=float, default=valid_ratio, help='Validate ratio.') - - args = parser.parse_args() - if os.path.exists(args.output_dir): - reply = str(input(f'{args.output_dir} exists. Do you want to overwrite it? (y/n)')).lower().strip() - if reply[0] != 'y': exit + # sliding window size for generating history sequence and target sequence + HISTORY_SEQ_LEN = 12 + FUTURE_SEQ_LEN = 12 + + TRAIN_RATIO = 0.6 + VALID_RATIO = 0.2 + TARGET_CHANNEL = [0] # target channel(s) + STEPS_PER_DAY = 288 + + DATASET_NAME = "PEMS04" + TOD = True # if add time_of_day feature + DOW = True # if add day_of_week feature + OUTPUT_DIR = "datasets/" + DATASET_NAME + DATA_FILE_PATH = "datasets/raw_data/{0}/{0}.npz".format(DATASET_NAME) + GRAPH_FILE_PATH = "datasets/raw_data/{0}/adj_{0}.pkl".format(DATASET_NAME) + + parser = argparse.ArgumentParser() + parser.add_argument("--output_dir", type=str, + default=OUTPUT_DIR, help="Output directory.") + parser.add_argument("--data_file_path", type=str, + default=DATA_FILE_PATH, help="Raw traffic readings.") + parser.add_argument("--graph_file_path", type=str, + default=GRAPH_FILE_PATH, help="Raw traffic readings.") + parser.add_argument("--history_seq_len", type=int, + default=HISTORY_SEQ_LEN, help="Sequence Length.") + parser.add_argument("--future_seq_len", type=int, + default=FUTURE_SEQ_LEN, help="Sequence Length.") + parser.add_argument("--steps_per_day", type=int, + default=STEPS_PER_DAY, help="Sequence Length.") + parser.add_argument("--tod", type=bool, default=TOD, + help="Add feature time_of_day.") + parser.add_argument("--dow", type=bool, default=DOW, + help="Add feature day_of_week.") + parser.add_argument("--target_channel", type=list, + default=TARGET_CHANNEL, help="Selected channels.") + parser.add_argument("--train_ratio", type=float, + default=TRAIN_RATIO, help="Train ratio") + parser.add_argument("--valid_ratio", type=float, + default=VALID_RATIO, help="Validate ratio.") + args_metr = parser.parse_args() + + # print args + print("-"*(20+45+5)) + for key, value in sorted(vars(args_metr).items()): + print("|{0:>20} = {1:<45}|".format(key, str(value))) + print("-"*(20+45+5)) + + if os.path.exists(args_metr.output_dir): + reply = str(input( + f"{args_metr.output_dir} exists. Do you want to overwrite it? (y/n)")).lower().strip() + if reply[0] != "y": + sys.exit(0) else: - os.makedirs(args.output_dir) - generate_data(args) + os.makedirs(args_metr.output_dir) + generate_data(args_metr) diff --git a/scripts/data_preparation/PEMS07/generate_adj_mx.py b/scripts/data_preparation/PEMS07/generate_adj_mx.py index ae9f5c71..e307a013 100644 --- a/scripts/data_preparation/PEMS07/generate_adj_mx.py +++ b/scripts/data_preparation/PEMS07/generate_adj_mx.py @@ -1,170 +1,84 @@ import os -import numpy as np import csv import pickle -def get_adjacency_matrix(distance_df_filename, num_of_vertices, id_filename=None): - ''' - Parameters - ---------- - distance_df_filename: str, path of the csv file contains edges information - - num_of_vertices: int, the number of vertices - - Returns - ---------- - A: np.ndarray, adjacency matrix - - ''' - if 'npy' in distance_df_filename: - adj_mx = np.load(distance_df_filename) - return adj_mx, None - else: - A = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - distaneA = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - # distance file中的id并不是从0开始的 所以要进行重新的映射;id_filename是节点的顺序 - if id_filename: - with open(id_filename, 'r') as f: - id_dict = {int(i): idx for idx, i in enumerate(f.read().strip().split('\n'))} # 把节点id(idx)映射成从0开始的索引 - with open(distance_df_filename, 'r') as f: - f.readline() # 略过表头那一行 - reader = csv.reader(f) - for row in reader: - if len(row) != 3: - continue - i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[id_dict[i], id_dict[j]] = 1 - distaneA[id_dict[i], id_dict[j]] = distance - return A, distaneA - else: # distance file中的id直接从0开始 - with open(distance_df_filename, 'r') as f: - f.readline() - reader = csv.reader(f) - for row in reader: - if len(row) != 3: - continue - i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[i, j] = 1 - distaneA[i, j] = distance - return A, distaneA - -def get_adjacency_matrix_2direction(distance_df_filename, num_of_vertices, id_filename=None): - ''' - Parameters - ---------- - distance_df_filename: str, path of the csv file contains edges information - num_of_vertices: int, the number of vertices - - Returns - ---------- - A: np.ndarray, adjacency matrix - ''' - if 'npy' in distance_df_filename: - adj_mx = np.load(distance_df_filename) - return adj_mx, None - else: - A = np.zeros((int(num_of_vertices), int(num_of_vertices)), dtype=np.float32) - distaneA = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - # distance file中的id并不是从0开始的 所以要进行重新的映射;id_filename是节点的顺序 - if id_filename: - with open(id_filename, 'r') as f: - id_dict = {int(i): idx for idx, i in enumerate(f.read().strip().split('\n'))} # 把节点id(idx)映射成从0开始的索引 - with open(distance_df_filename, 'r') as f: - f.readline() # 略过表头那一行 - reader = csv.reader(f) - for row in reader: - if len(row) != 3: - continue - i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[id_dict[i], id_dict[j]] = 1 - A[id_dict[j], id_dict[i]] = 1 - distaneA[id_dict[i], id_dict[j]] = distance - distaneA[id_dict[j], id_dict[i]] = distance - return A, distaneA - else: # distance file中的id直接从0开始 - with open(distance_df_filename, 'r') as f: - f.readline() - reader = csv.reader(f) - for row in reader: - if len(row) != 3: - continue - i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[i, j] = 1 - A[j, i] = 1 - distaneA[i, j] = distance - distaneA[j, i] = distance - return A, distaneA +import numpy as np -def get_adjacency_matrix_2direction(distance_df_filename, num_of_vertices, id_filename=None): - ''' - Parameters - ---------- - distance_df_filename: str, path of the csv file contains edges information +def get_adjacency_matrix(distance_df_filename: str, num_of_vertices: int, id_filename: str = None) -> tuple: + """Generate adjacency matrix. - num_of_vertices: int, the number of vertices + Args: + distance_df_filename (str): path of the csv file contains edges information + num_of_vertices (int): number of vertices + id_filename (str, optional): id filename. Defaults to None. - Returns - ---------- - A: np.ndarray, adjacency matrix + Returns: + tuple: two adjacency matrix. + np.array: connectivity-based adjacency matrix A (A[i, j]=0 or A[i, j]=1) + np.array: distance-based adjacency matrix A + """ - ''' - if 'npy' in distance_df_filename: + if "npy" in distance_df_filename: adj_mx = np.load(distance_df_filename) return adj_mx, None else: - A = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - distaneA = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - # distance file中的id并不是从0开始的 所以要进行重新的映射;id_filename是节点的顺序 + adjacency_matrix_connectivity = np.zeros((int(num_of_vertices), int( + num_of_vertices)), dtype=np.float32) + adjacency_matrix_distance = np.zeros((int(num_of_vertices), int(num_of_vertices)), + dtype=np.float32) if id_filename: - with open(id_filename, 'r') as f: - id_dict = {int(i): idx for idx, i in enumerate(f.read().strip().split('\n'))} # 把节点id(idx)映射成从0开始的索引 - with open(distance_df_filename, 'r') as f: - f.readline() # 略过表头那一行 + # the id in the distance file does not start from 0, so it needs to be remapped + with open(id_filename, "r") as f: + id_dict = {int(i): idx for idx, i in enumerate( + f.read().strip().split("\n"))} # map node idx to 0-based index (start from 0) + with open(distance_df_filename, "r") as f: + f.readline() # omit the first line reader = csv.reader(f) for row in reader: if len(row) != 3: continue i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[id_dict[i], id_dict[j]] = 1 - A[id_dict[j], id_dict[i]] = 1 - distaneA[id_dict[i], id_dict[j]] = distance - distaneA[id_dict[j], id_dict[i]] = distance - return A, distaneA - else: # distance file中的id直接从0开始 - with open(distance_df_filename, 'r') as f: + adjacency_matrix_connectivity[id_dict[i], id_dict[j]] = 1 + adjacency_matrix_connectivity[id_dict[j], id_dict[i]] = 1 + adjacency_matrix_distance[id_dict[i], + id_dict[j]] = distance + adjacency_matrix_distance[id_dict[j], + id_dict[i]] = distance + return adjacency_matrix_connectivity, adjacency_matrix_distance + else: + # ids in distance file start from 0 + with open(distance_df_filename, "r") as f: f.readline() reader = csv.reader(f) for row in reader: if len(row) != 3: continue i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[i, j] = 1 - A[j, i] = 1 - distaneA[i, j] = distance - distaneA[j, i] = distance - return A, distaneA + adjacency_matrix_connectivity[i, j] = 1 + adjacency_matrix_connectivity[j, i] = 1 + adjacency_matrix_distance[i, j] = distance + adjacency_matrix_distance[j, i] = distance + return adjacency_matrix_connectivity, adjacency_matrix_distance + -def generate_adj_PEMS07(): - direction = True +def generate_adj_pems07(): distance_df_filename, num_of_vertices = "datasets/raw_data/PEMS07/PEMS07.csv", 883 - if os.path.exists(distance_df_filename.split(".")[0] + ".txt"): - id_filename = distance_df_filename.split(".")[0] + ".txt" + if os.path.exists(distance_df_filename.split(".", maxsplit=1)[0] + ".txt"): + id_filename = distance_df_filename.split(".", maxsplit=1)[0] + ".txt" else: id_filename = None - if direction: - adj_mx, distance_mx = get_adjacency_matrix_2direction(distance_df_filename, num_of_vertices, id_filename=id_filename) - else: - adj_mx, distance_mx = get_adjacency_matrix(distance_df_filename, num_of_vertices, id_filename=id_filename) - # TODO: the self loop is missing + adj_mx, distance_mx = get_adjacency_matrix( + distance_df_filename, num_of_vertices, id_filename=id_filename) + # the self loop is missing add_self_loop = False if add_self_loop: + print("adding self loop to adjacency matrices.") adj_mx = adj_mx + np.identity(adj_mx.shape[0]) distance_mx = distance_mx + np.identity(distance_mx.shape[0]) - pickle.dump(adj_mx, open("datasets/raw_data/PEMS07/adj_PEMS07.pkl", 'wb')) - pickle.dump(distance_mx, open("datasets/raw_data/PEMS07/adj_PEMS07_distance.pkl", 'wb')) + else: + print("kindly note that there is no self loop in adjacency matrices.") + with open("datasets/raw_data/PEMS07/adj_PEMS07.pkl", "wb") as f: + pickle.dump(adj_mx, f) + with open("datasets/raw_data/PEMS07/adj_PEMS07_distance.pkl", "wb") as f: + pickle.dump(distance_mx, f) diff --git a/scripts/data_preparation/PEMS07/generate_training_data.py b/scripts/data_preparation/PEMS07/generate_training_data.py index 86995a41..3d490aee 100644 --- a/scripts/data_preparation/PEMS07/generate_training_data.py +++ b/scripts/data_preparation/PEMS07/generate_training_data.py @@ -1,167 +1,163 @@ -import argparse -import pickle +import os +import sys import shutil +import pickle +import argparse + import numpy as np -import os -from generate_adj_mx import generate_adj_PEMS07 - -""" -PEMS07 dataset (traffic flow dataset) default settings: - - normalization: - standard norm - - dataset division: - 6:2:2 - - windows size: - 12 - - features: - traffic flow - --traffic occupy--(not used) - --traffic speed--(not used) - time in day - day in week - - target: - predicting the traffic speed -""" - -def standard_transform(data: np.array, output_dir: str, train_index: list) -> np.array: - """standard normalization. +from generate_adj_mx import generate_adj_pems07 +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../../..")) +from basicts.data.transform import standard_transform - Args: - data (np.array): raw time series data. - output_dir (str): output dir path. - train_index (list): train index. - Returns: - np.array: normalized raw time series data. - """ - # data: L, N, C - data_train = data[:train_index[-1][1], ...] - - mean, std = data_train[..., 0].mean(), data_train[..., 0].std() - - print("mean (training data):", mean) - print("std (training data):", std) - scaler = {} - scaler['func'] = standard_re_transform.__name__ - scaler['args'] = {"mean":mean, "std":std} - pickle.dump(scaler, open(output_dir + "/scaler.pkl", 'wb')) - - def normalize(x): - return (x - mean) / std - - data_norm = normalize(data) - return data_norm - -def standard_re_transform(x, **kwargs): - mean, std = kwargs['mean'], kwargs['std'] - x = x * std - x = x + mean - return x - -def generate_data(args): - """preprocess and generate train/valid/test datasets. +def generate_data(args: argparse.Namespace): + """Preprocess and generate train/valid/test datasets. + Default settings of PEMS07 dataset: + - Normalization method: standard norm. + - Dataset division: 6:2:2. + - Window size: history 12, future 12. + - Channels (features): three channels [traffic flow, time of day, day of week] + - Target: predict the traffic speed of the future 12 time steps. Args: - args (Namespace): args for processing data. + args (argparse): configurations of preprocessing """ - C = args.C - future_seq_len = args.future_seq_len + + target_channel = args.target_channel + future_seq_len = args.future_seq_len history_seq_len = args.history_seq_len - add_time_in_day = True - add_day_in_week = args.dow + add_time_of_day = args.tod + add_day_of_week = args.dow output_dir = args.output_dir + train_ratio = args.train_ratio + valid_ratio = args.valid_ratio + data_file_path = args.data_file_path + graph_file_path = args.graph_file_path + steps_per_day = args.steps_per_day # read data - data = np.load(args.data_file_path)['data'] - data = data[..., C] - print("Data shape: {0}".format(data.shape)) + data = np.load(data_file_path)["data"] + data = data[..., target_channel] + print("raw time series shape: {0}".format(data.shape)) - L, N, F = data.shape - num_samples = L - (history_seq_len + future_seq_len) + 1 + l, n, f = data.shape + num_samples = l - (history_seq_len + future_seq_len) + 1 train_num_short = round(num_samples * train_ratio) valid_num_short = round(num_samples * valid_ratio) - test_num_short = num_samples - train_num_short - valid_num_short - print("train_num_short:{0}".format(train_num_short)) - print("valid_num_short:{0}".format(valid_num_short)) - print("test_num_short:{0}".format(test_num_short)) + test_num_short = num_samples - train_num_short - valid_num_short + print("number of training samples:{0}".format(train_num_short)) + print("number of validation samples:{0}".format(valid_num_short)) + print("number of test samples:{0}".format(test_num_short)) - index_list = [] + index_list = [] for t in range(history_seq_len, num_samples + history_seq_len): index = (t-history_seq_len, t, t+future_seq_len) index_list.append(index) + train_index = index_list[:train_num_short] valid_index = index_list[train_num_short: train_num_short + valid_num_short] - test_index = index_list[train_num_short + valid_num_short: train_num_short + valid_num_short + test_num_short] - + test_index = index_list[train_num_short + + valid_num_short: train_num_short + valid_num_short + test_num_short] + scaler = standard_transform data_norm = scaler(data, output_dir, train_index) # add external feature feature_list = [data_norm] - if add_time_in_day: - # numerical time_in_day - time_ind = [i%args.steps_per_day / args.steps_per_day for i in range(data_norm.shape[0])] - time_ind = np.array(time_ind) - time_in_day = np.tile(time_ind, [1, N, 1]).transpose((2, 1, 0)) - feature_list.append(time_in_day) - if add_day_in_week: - # numerical day_in_week - day_in_week = [(i // args.steps_per_day)%7 for i in range(data_norm.shape[0])] - day_in_week = np.array(day_in_week) - day_in_week = np.tile(day_in_week, [1, N, 1]).transpose((2, 1, 0)) - feature_list.append(day_in_week) + if add_time_of_day: + # numerical time_of_day + tod = [i % steps_per_day / + steps_per_day for i in range(data_norm.shape[0])] + tod = np.array(tod) + tod_tiled = np.tile(tod, [1, n, 1]).transpose((2, 1, 0)) + feature_list.append(tod_tiled) + + if add_day_of_week: + # numerical day_of_week + dow = [(i // steps_per_day) % 7 for i in range(data_norm.shape[0])] + dow = np.array(dow) + dow_tiled = np.tile(dow, [1, n, 1]).transpose((2, 1, 0)) + feature_list.append(dow_tiled) processed_data = np.concatenate(feature_list, axis=-1) # dump data index = {} - index['train'] = train_index - index['valid'] = valid_index - index['test'] = test_index - pickle.dump(index, open(output_dir + "/index.pkl", "wb")) + index["train"] = train_index + index["valid"] = valid_index + index["test"] = test_index + with open(output_dir + "/index_in{0}_out{1}.pkl".format(history_seq_len, future_seq_len), "wb") as f: + pickle.dump(index, f) data = {} - data['processed_data'] = processed_data - pickle.dump(data, open(output_dir + "/data.pkl", "wb")) + data["processed_data"] = processed_data + with open(output_dir + "/data.pkl", "wb") as f: + pickle.dump(data, f) # copy adj if os.path.exists(args.graph_file_path): - shutil.copyfile(args.graph_file_path, output_dir + '/adj_mx.pkl') # copy models + # copy + shutil.copyfile(args.graph_file_path, output_dir + "/adj_mx.pkl") else: - generate_adj_PEMS07() - shutil.copyfile(args.graph_file_path, output_dir + '/adj_mx.pkl') # copy models + # generate and copy + generate_adj_pems07() + shutil.copyfile(graph_file_path, output_dir + "/adj_mx.pkl") + if __name__ == "__main__": - history_seq_len = 12 # sliding window size for generating history sequence and target sequence - future_seq_len = 12 - - train_ratio = 0.6 - valid_ratio = 0.2 - C = [0] # selected channels - steps_per_day = 288 # 5min - - name = "PEMS07" - dow = True # if add day_of_week feature - output_dir = 'datasets/' + name - data_file_path = 'datasets/raw_data/{0}/{1}.npz'.format(name, name) - graph_file_path = 'datasets/raw_data/{0}/adj_{1}.pkl'.format(name, name) - - parser = argparse.ArgumentParser() - parser.add_argument("--output_dir", type=str, default=output_dir, help="Output directory.") - parser.add_argument("--data_file_path", type=str, default=data_file_path, help="Raw traffic readings.") - parser.add_argument("--graph_file_path", type=str, default=graph_file_path, help="Raw traffic readings.") - parser.add_argument("--history_seq_len", type=int, default=history_seq_len, help="Sequence Length.") - parser.add_argument("--future_seq_len", type=int, default=future_seq_len, help="Sequence Length.") - parser.add_argument("--steps_per_day", type=int, default=steps_per_day, help="Sequence Length.") - parser.add_argument("--dow", type=bool, default=dow, help='Add feature day_of_week.') - parser.add_argument("--C", type=list, default=C, help='Selected channels.') - parser.add_argument("--train_ratio", type=float, default=train_ratio, help='Train ratio') - parser.add_argument("--valid_ratio", type=float, default=valid_ratio, help='Validate ratio.') - - args = parser.parse_args() - if os.path.exists(args.output_dir): - reply = str(input(f'{args.output_dir} exists. Do you want to overwrite it? (y/n)')).lower().strip() - if reply[0] != 'y': exit + # sliding window size for generating history sequence and target sequence + HISTORY_SEQ_LEN = 12 + FUTURE_SEQ_LEN = 12 + + TRAIN_RATIO = 0.6 + VALID_RATIO = 0.2 + TARGET_CHANNEL = [0] # target channel(s) + STEPS_PER_DAY = 288 + + DATASET_NAME = "PEMS07" + TOD = True # if add time_of_day feature + DOW = True # if add day_of_week feature + OUTPUT_DIR = "datasets/" + DATASET_NAME + DATA_FILE_PATH = "datasets/raw_data/{0}/{0}.npz".format(DATASET_NAME) + GRAPH_FILE_PATH = "datasets/raw_data/{0}/adj_{0}.pkl".format(DATASET_NAME) + + parser = argparse.ArgumentParser() + parser.add_argument("--output_dir", type=str, + default=OUTPUT_DIR, help="Output directory.") + parser.add_argument("--data_file_path", type=str, + default=DATA_FILE_PATH, help="Raw traffic readings.") + parser.add_argument("--graph_file_path", type=str, + default=GRAPH_FILE_PATH, help="Raw traffic readings.") + parser.add_argument("--history_seq_len", type=int, + default=HISTORY_SEQ_LEN, help="Sequence Length.") + parser.add_argument("--future_seq_len", type=int, + default=FUTURE_SEQ_LEN, help="Sequence Length.") + parser.add_argument("--steps_per_day", type=int, + default=STEPS_PER_DAY, help="Sequence Length.") + parser.add_argument("--tod", type=bool, default=TOD, + help="Add feature time_of_day.") + parser.add_argument("--dow", type=bool, default=DOW, + help="Add feature day_of_week.") + parser.add_argument("--target_channel", type=list, + default=TARGET_CHANNEL, help="Selected channels.") + parser.add_argument("--train_ratio", type=float, + default=TRAIN_RATIO, help="Train ratio") + parser.add_argument("--valid_ratio", type=float, + default=VALID_RATIO, help="Validate ratio.") + args_metr = parser.parse_args() + + # print args + print("-"*(20+45+5)) + for key, value in sorted(vars(args_metr).items()): + print("|{0:>20} = {1:<45}|".format(key, str(value))) + print("-"*(20+45+5)) + + if os.path.exists(args_metr.output_dir): + reply = str(input( + f"{args_metr.output_dir} exists. Do you want to overwrite it? (y/n)")).lower().strip() + if reply[0] != "y": + sys.exit(0) else: - os.makedirs(args.output_dir) - generate_data(args) + os.makedirs(args_metr.output_dir) + generate_data(args_metr) diff --git a/scripts/data_preparation/PEMS08/generate_adj_mx.py b/scripts/data_preparation/PEMS08/generate_adj_mx.py index 1f9fd2ff..1484b46e 100644 --- a/scripts/data_preparation/PEMS08/generate_adj_mx.py +++ b/scripts/data_preparation/PEMS08/generate_adj_mx.py @@ -1,170 +1,84 @@ -import numpy as np +import os import csv import pickle -import os - -def get_adjacency_matrix(distance_df_filename, num_of_vertices, id_filename=None): - ''' - Parameters - ---------- - distance_df_filename: str, path of the csv file contains edges information - - num_of_vertices: int, the number of vertices - - Returns - ---------- - A: np.ndarray, adjacency matrix - ''' - if 'npy' in distance_df_filename: - adj_mx = np.load(distance_df_filename) - return adj_mx, None - else: - A = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - distaneA = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - # distance file中的id并不是从0开始的 所以要进行重新的映射;id_filename是节点的顺序 - if id_filename: - with open(id_filename, 'r') as f: - id_dict = {int(i): idx for idx, i in enumerate(f.read().strip().split('\n'))} # 把节点id(idx)映射成从0开始的索引 - with open(distance_df_filename, 'r') as f: - f.readline() # 略过表头那一行 - reader = csv.reader(f) - for row in reader: - if len(row) != 3: - continue - i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[id_dict[i], id_dict[j]] = 1 - distaneA[id_dict[i], id_dict[j]] = distance - return A, distaneA - else: # distance file中的id直接从0开始 - with open(distance_df_filename, 'r') as f: - f.readline() - reader = csv.reader(f) - for row in reader: - if len(row) != 3: - continue - i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[i, j] = 1 - distaneA[i, j] = distance - return A, distaneA - -def get_adjacency_matrix_2direction(distance_df_filename, num_of_vertices, id_filename=None): - ''' - Parameters - ---------- - distance_df_filename: str, path of the csv file contains edges information - num_of_vertices: int, the number of vertices - - Returns - ---------- - A: np.ndarray, adjacency matrix - ''' - if 'npy' in distance_df_filename: - adj_mx = np.load(distance_df_filename) - return adj_mx, None - else: - A = np.zeros((int(num_of_vertices), int(num_of_vertices)), dtype=np.float32) - distaneA = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - # distance file中的id并不是从0开始的 所以要进行重新的映射;id_filename是节点的顺序 - if id_filename: - with open(id_filename, 'r') as f: - id_dict = {int(i): idx for idx, i in enumerate(f.read().strip().split('\n'))} # 把节点id(idx)映射成从0开始的索引 - with open(distance_df_filename, 'r') as f: - f.readline() # 略过表头那一行 - reader = csv.reader(f) - for row in reader: - if len(row) != 3: - continue - i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[id_dict[i], id_dict[j]] = 1 - A[id_dict[j], id_dict[i]] = 1 - distaneA[id_dict[i], id_dict[j]] = distance - distaneA[id_dict[j], id_dict[i]] = distance - return A, distaneA - else: # distance file中的id直接从0开始 - with open(distance_df_filename, 'r') as f: - f.readline() - reader = csv.reader(f) - for row in reader: - if len(row) != 3: - continue - i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[i, j] = 1 - A[j, i] = 1 - distaneA[i, j] = distance - distaneA[j, i] = distance - return A, distaneA +import numpy as np -def get_adjacency_matrix_2direction(distance_df_filename, num_of_vertices, id_filename=None): - ''' - Parameters - ---------- - distance_df_filename: str, path of the csv file contains edges information +def get_adjacency_matrix(distance_df_filename: str, num_of_vertices: int, id_filename: str = None) -> tuple: + """Generate adjacency matrix. - num_of_vertices: int, the number of vertices + Args: + distance_df_filename (str): path of the csv file contains edges information + num_of_vertices (int): number of vertices + id_filename (str, optional): id filename. Defaults to None. - Returns - ---------- - A: np.ndarray, adjacency matrix + Returns: + tuple: two adjacency matrix. + np.array: connectivity-based adjacency matrix A (A[i, j]=0 or A[i, j]=1) + np.array: distance-based adjacency matrix A + """ - ''' - if 'npy' in distance_df_filename: + if "npy" in distance_df_filename: adj_mx = np.load(distance_df_filename) return adj_mx, None else: - A = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - distaneA = np.zeros((int(num_of_vertices), int(num_of_vertices)), - dtype=np.float32) - # distance file中的id并不是从0开始的 所以要进行重新的映射;id_filename是节点的顺序 + adjacency_matrix_connectivity = np.zeros((int(num_of_vertices), int( + num_of_vertices)), dtype=np.float32) + adjacency_matrix_distance = np.zeros((int(num_of_vertices), int(num_of_vertices)), + dtype=np.float32) if id_filename: - with open(id_filename, 'r') as f: - id_dict = {int(i): idx for idx, i in enumerate(f.read().strip().split('\n'))} # 把节点id(idx)映射成从0开始的索引 - with open(distance_df_filename, 'r') as f: - f.readline() # 略过表头那一行 + # the id in the distance file does not start from 0, so it needs to be remapped + with open(id_filename, "r") as f: + id_dict = {int(i): idx for idx, i in enumerate( + f.read().strip().split("\n"))} # map node idx to 0-based index (start from 0) + with open(distance_df_filename, "r") as f: + f.readline() # omit the first line reader = csv.reader(f) for row in reader: if len(row) != 3: continue i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[id_dict[i], id_dict[j]] = 1 - A[id_dict[j], id_dict[i]] = 1 - distaneA[id_dict[i], id_dict[j]] = distance - distaneA[id_dict[j], id_dict[i]] = distance - return A, distaneA - else: # distance file中的id直接从0开始 - with open(distance_df_filename, 'r') as f: + adjacency_matrix_connectivity[id_dict[i], id_dict[j]] = 1 + adjacency_matrix_connectivity[id_dict[j], id_dict[i]] = 1 + adjacency_matrix_distance[id_dict[i], + id_dict[j]] = distance + adjacency_matrix_distance[id_dict[j], + id_dict[i]] = distance + return adjacency_matrix_connectivity, adjacency_matrix_distance + else: + # ids in distance file start from 0 + with open(distance_df_filename, "r") as f: f.readline() reader = csv.reader(f) for row in reader: if len(row) != 3: continue i, j, distance = int(row[0]), int(row[1]), float(row[2]) - A[i, j] = 1 - A[j, i] = 1 - distaneA[i, j] = distance - distaneA[j, i] = distance - return A, distaneA + adjacency_matrix_connectivity[i, j] = 1 + adjacency_matrix_connectivity[j, i] = 1 + adjacency_matrix_distance[i, j] = distance + adjacency_matrix_distance[j, i] = distance + return adjacency_matrix_connectivity, adjacency_matrix_distance + -def generate_adj_PEMS08(): - direction = True +def generate_adj_pems08(): distance_df_filename, num_of_vertices = "datasets/raw_data/PEMS08/PEMS08.csv", 170 - if os.path.exists(distance_df_filename.split(".")[0] + ".txt"): - id_filename = distance_df_filename.split(".")[0] + ".txt" + if os.path.exists(distance_df_filename.split(".", maxsplit=1)[0] + ".txt"): + id_filename = distance_df_filename.split(".", maxsplit=1)[0] + ".txt" else: id_filename = None - if direction: - adj_mx, distance_mx = get_adjacency_matrix_2direction(distance_df_filename, num_of_vertices, id_filename=id_filename) - else: - adj_mx, distance_mx = get_adjacency_matrix(distance_df_filename, num_of_vertices, id_filename=id_filename) - # TODO: the self loop is missing + adj_mx, distance_mx = get_adjacency_matrix( + distance_df_filename, num_of_vertices, id_filename=id_filename) + # the self loop is missing add_self_loop = False if add_self_loop: + print("adding self loop to adjacency matrices.") adj_mx = adj_mx + np.identity(adj_mx.shape[0]) distance_mx = distance_mx + np.identity(distance_mx.shape[0]) - pickle.dump(adj_mx, open("datasets/raw_data/PEMS08/adj_PEMS08.pkl", 'wb')) - pickle.dump(distance_mx, open("datasets/raw_data/PEMS08/adj_PEMS08_distance.pkl", 'wb')) + else: + print("kindly note that there is no self loop in adjacency matrices.") + with open("datasets/raw_data/PEMS08/adj_PEMS08.pkl", "wb") as f: + pickle.dump(adj_mx, f) + with open("datasets/raw_data/PEMS08/adj_PEMS08_distance.pkl", "wb") as f: + pickle.dump(distance_mx, f) diff --git a/scripts/data_preparation/PEMS08/generate_training_data.py b/scripts/data_preparation/PEMS08/generate_training_data.py index 4ce76e88..c7786a3e 100644 --- a/scripts/data_preparation/PEMS08/generate_training_data.py +++ b/scripts/data_preparation/PEMS08/generate_training_data.py @@ -1,167 +1,163 @@ -import argparse -import pickle +import os +import sys import shutil +import pickle +import argparse + import numpy as np -import os -from generate_adj_mx import generate_adj_PEMS08 - -""" -PEMS08 dataset (traffic flow dataset) default settings: - - normalization: - standard norm - - dataset division: - 6:2:2 - - windows size: - 12 - - features: - traffic flow - --traffic occupy--(not used) - --traffic speed--(not used) - time in day - day in week - - target: - predicting the traffic speed -""" - -def standard_transform(data: np.array, output_dir: str, train_index: list) -> np.array: - """standard normalization. +from generate_adj_mx import generate_adj_pems08 +# TODO: remove it when basicts can be installed by pip +sys.path.append(os.path.abspath(__file__ + "/../../../..")) +from basicts.data.transform import standard_transform - Args: - data (np.array): raw time series data. - output_dir (str): output dir path. - train_index (list): train index. - Returns: - np.array: normalized raw time series data. - """ - # data: L, N, C - data_train = data[:train_index[-1][1], ...] - - mean, std = data_train[..., 0].mean(), data_train[..., 0].std() - - print("mean (training data):", mean) - print("std (training data):", std) - scaler = {} - scaler['func'] = standard_re_transform.__name__ - scaler['args'] = {"mean":mean, "std":std} - pickle.dump(scaler, open(output_dir + "/scaler.pkl", 'wb')) - - def normalize(x): - return (x - mean) / std - - data_norm = normalize(data) - return data_norm - -def standard_re_transform(x, **kwargs): - mean, std = kwargs['mean'], kwargs['std'] - x = x * std - x = x + mean - return x - -def generate_data(args): - """preprocess and generate train/valid/test datasets. +def generate_data(args: argparse.Namespace): + """Preprocess and generate train/valid/test datasets. + Default settings of PEMS08 dataset: + - Normalization method: standard norm. + - Dataset division: 6:2:2. + - Window size: history 12, future 12. + - Channels (features): three channels [traffic flow, time of day, day of week] + - Target: predict the traffic speed of the future 12 time steps. Args: - args (Namespace): args for processing data. + args (argparse): configurations of preprocessing """ - C = args.C - future_seq_len = args.future_seq_len + + target_channel = args.target_channel + future_seq_len = args.future_seq_len history_seq_len = args.history_seq_len - add_time_in_day = True - add_day_in_week = args.dow + add_time_of_day = args.tod + add_day_of_week = args.dow output_dir = args.output_dir + train_ratio = args.train_ratio + valid_ratio = args.valid_ratio + data_file_path = args.data_file_path + graph_file_path = args.graph_file_path + steps_per_day = args.steps_per_day # read data - data = np.load(args.data_file_path)['data'] - data = data[..., C] - print("Data shape: {0}".format(data.shape)) + data = np.load(data_file_path)["data"] + data = data[..., target_channel] + print("raw time series shape: {0}".format(data.shape)) - L, N, F = data.shape - num_samples = L - (history_seq_len + future_seq_len) + 1 + l, n, f = data.shape + num_samples = l - (history_seq_len + future_seq_len) + 1 train_num_short = round(num_samples * train_ratio) valid_num_short = round(num_samples * valid_ratio) - test_num_short = num_samples - train_num_short - valid_num_short - print("train_num_short:{0}".format(train_num_short)) - print("valid_num_short:{0}".format(valid_num_short)) - print("test_num_short:{0}".format(test_num_short)) + test_num_short = num_samples - train_num_short - valid_num_short + print("number of training samples:{0}".format(train_num_short)) + print("number of validation samples:{0}".format(valid_num_short)) + print("number of test samples:{0}".format(test_num_short)) - index_list = [] + index_list = [] for t in range(history_seq_len, num_samples + history_seq_len): index = (t-history_seq_len, t, t+future_seq_len) index_list.append(index) + train_index = index_list[:train_num_short] valid_index = index_list[train_num_short: train_num_short + valid_num_short] - test_index = index_list[train_num_short + valid_num_short: train_num_short + valid_num_short + test_num_short] - + test_index = index_list[train_num_short + + valid_num_short: train_num_short + valid_num_short + test_num_short] + scaler = standard_transform data_norm = scaler(data, output_dir, train_index) # add external feature feature_list = [data_norm] - if add_time_in_day: - # numerical time_in_day - time_ind = [i%args.steps_per_day / args.steps_per_day for i in range(data_norm.shape[0])] - time_ind = np.array(time_ind) - time_in_day = np.tile(time_ind, [1, N, 1]).transpose((2, 1, 0)) - feature_list.append(time_in_day) - if add_day_in_week: - # numerical day_in_week - day_in_week = [(i // args.steps_per_day)%7 for i in range(data_norm.shape[0])] - day_in_week = np.array(day_in_week) - day_in_week = np.tile(day_in_week, [1, N, 1]).transpose((2, 1, 0)) - feature_list.append(day_in_week) - + if add_time_of_day: + # numerical time_of_day + tod = [i % steps_per_day / + steps_per_day for i in range(data_norm.shape[0])] + tod = np.array(tod) + tod_tiled = np.tile(tod, [1, n, 1]).transpose((2, 1, 0)) + feature_list.append(tod_tiled) + + if add_day_of_week: + # numerical day_of_week + dow = [(i // steps_per_day) % 7 for i in range(data_norm.shape[0])] + dow = np.array(dow) + dow_tiled = np.tile(dow, [1, n, 1]).transpose((2, 1, 0)) + feature_list.append(dow_tiled) + processed_data = np.concatenate(feature_list, axis=-1) # dump data index = {} - index['train'] = train_index - index['valid'] = valid_index - index['test'] = test_index - pickle.dump(index, open(output_dir + "/index.pkl", "wb")) + index["train"] = train_index + index["valid"] = valid_index + index["test"] = test_index + with open(output_dir + "/index_in{0}_out{1}.pkl".format(history_seq_len, future_seq_len), "wb") as f: + pickle.dump(index, f) data = {} - data['processed_data'] = processed_data - pickle.dump(data, open(output_dir + "/data.pkl", "wb")) + data["processed_data"] = processed_data + with open(output_dir + "/data.pkl", "wb") as f: + pickle.dump(data, f) # copy adj if os.path.exists(args.graph_file_path): - shutil.copyfile(args.graph_file_path, output_dir + '/adj_mx.pkl') # copy models + # copy + shutil.copyfile(args.graph_file_path, output_dir + "/adj_mx.pkl") else: - generate_adj_PEMS08() - shutil.copyfile(args.graph_file_path, output_dir + '/adj_mx.pkl') # copy models + # generate and copy + generate_adj_pems08() + shutil.copyfile(graph_file_path, output_dir + "/adj_mx.pkl") + if __name__ == "__main__": - history_seq_len = 12 # sliding window size for generating history sequence and target sequence - future_seq_len = 12 - - train_ratio = 0.6 - valid_ratio = 0.2 - C = [0] # selected channels - steps_per_day = 288 # 5min - - name = "PEMS08" - dow = True # if add day_of_week feature - output_dir = 'datasets/' + name - data_file_path = 'datasets/raw_data/{0}/{1}.npz'.format(name, name) - graph_file_path = 'datasets/raw_data/{0}/adj_{1}.pkl'.format(name, name) - - parser = argparse.ArgumentParser() - parser.add_argument("--output_dir", type=str, default=output_dir, help="Output directory.") - parser.add_argument("--data_file_path", type=str, default=data_file_path, help="Raw traffic readings.",) - parser.add_argument("--graph_file_path", type=str, default=graph_file_path, help="Raw traffic readings.",) - parser.add_argument("--history_seq_len", type=int, default=history_seq_len, help="Sequence Length.") - parser.add_argument("--future_seq_len", type=int, default=future_seq_len, help="Sequence Length.") - parser.add_argument("--steps_per_day", type=int, default=steps_per_day, help="Sequence Length.") - parser.add_argument("--dow", type=bool, default=dow, help='Add feature day_of_week.') - parser.add_argument("--C", type=list, default=C, help='Selected channels.') - parser.add_argument("--train_ratio", type=float, default=train_ratio, help='Train ratio') - parser.add_argument("--valid_ratio", type=float, default=valid_ratio, help='Validate ratio.') - - args = parser.parse_args() - if os.path.exists(args.output_dir): - reply = str(input(f'{args.output_dir} exists. Do you want to overwrite it? (y/n)')).lower().strip() - if reply[0] != 'y': exit + # sliding window size for generating history sequence and target sequence + HISTORY_SEQ_LEN = 12 + FUTURE_SEQ_LEN = 12 + + TRAIN_RATIO = 0.6 + VALID_RATIO = 0.2 + TARGET_CHANNEL = [0] # target channel(s) + STEPS_PER_DAY = 288 + + DATASET_NAME = "PEMS08" + TOD = True # if add time_of_day feature + DOW = True # if add day_of_week feature + OUTPUT_DIR = "datasets/" + DATASET_NAME + DATA_FILE_PATH = "datasets/raw_data/{0}/{0}.npz".format(DATASET_NAME) + GRAPH_FILE_PATH = "datasets/raw_data/{0}/adj_{0}.pkl".format(DATASET_NAME) + + parser = argparse.ArgumentParser() + parser.add_argument("--output_dir", type=str, + default=OUTPUT_DIR, help="Output directory.") + parser.add_argument("--data_file_path", type=str, + default=DATA_FILE_PATH, help="Raw traffic readings.") + parser.add_argument("--graph_file_path", type=str, + default=GRAPH_FILE_PATH, help="Raw traffic readings.") + parser.add_argument("--history_seq_len", type=int, + default=HISTORY_SEQ_LEN, help="Sequence Length.") + parser.add_argument("--future_seq_len", type=int, + default=FUTURE_SEQ_LEN, help="Sequence Length.") + parser.add_argument("--steps_per_day", type=int, + default=STEPS_PER_DAY, help="Sequence Length.") + parser.add_argument("--tod", type=bool, default=TOD, + help="Add feature time_of_day.") + parser.add_argument("--dow", type=bool, default=DOW, + help="Add feature day_of_week.") + parser.add_argument("--target_channel", type=list, + default=TARGET_CHANNEL, help="Selected channels.") + parser.add_argument("--train_ratio", type=float, + default=TRAIN_RATIO, help="Train ratio") + parser.add_argument("--valid_ratio", type=float, + default=VALID_RATIO, help="Validate ratio.") + args_metr = parser.parse_args() + + # print args + print("-"*(20+45+5)) + for key, value in sorted(vars(args_metr).items()): + print("|{0:>20} = {1:<45}|".format(key, str(value))) + print("-"*(20+45+5)) + + if os.path.exists(args_metr.output_dir): + reply = str(input( + f"{args_metr.output_dir} exists. Do you want to overwrite it? (y/n)")).lower().strip() + if reply[0] != "y": + sys.exit(0) else: - os.makedirs(args.output_dir) - generate_data(args) + os.makedirs(args_metr.output_dir) + generate_data(args_metr) diff --git a/scripts/data_preparation/all.sh b/scripts/data_preparation/all.sh new file mode 100755 index 00000000..43f7506f --- /dev/null +++ b/scripts/data_preparation/all.sh @@ -0,0 +1,7 @@ +#!/bin/bash +python scripts/data_preparation/METR-LA/generate_training_data.py +python scripts/data_preparation/PEMS-BAY/generate_training_data.py +python scripts/data_preparation/PEMS03/generate_training_data.py +python scripts/data_preparation/PEMS04/generate_training_data.py +python scripts/data_preparation/PEMS07/generate_training_data.py +python scripts/data_preparation/PEMS08/generate_training_data.py diff --git a/scripts/node2vec/generate_node_embeddings.py b/scripts/node2vec/generate_node_embeddings.py deleted file mode 100644 index af436ea3..00000000 --- a/scripts/node2vec/generate_node_embeddings.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Code ref: https://github.com/zhengchuanpan/GMAN/blob/master/METR/node2vec/generateSE.py -""" - -import os -import sys -sys.path.append(os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))) -import argparse -import node2vec -import networkx as nx -from gensim.models import Word2Vec -from basicts.utils.serialization import load_pkl - -def generate_node_embeddings(args): - try: - # METR and PEMS_BAY - _, _, adj_mx = load_pkl("datasets/{0}/adj_mx.pkl".format(args.dataset_name)) - except: - # PEMS0X - adj_mx = load_pkl("datasets/{0}/adj_mx.pkl".format(args.dataset_name)) - - nx_G = nx.from_numpy_array(adj_mx, create_using=nx.DiGraph()) - G = node2vec.Graph(nx_G, args.is_directed, args.p, args.q) - G.preprocess_transition_probs() - walks = G.simulate_walks(args.num_walks, args.walk_length) - - walks = [list(map(str, walk)) for walk in walks] - - model = Word2Vec(walks, vector_size = args.vector_size, window = 10, min_count=0, sg=1, workers = 8, epochs = args.epochs) - model.wv.save_word2vec_format("datasets/{0}/node2vec_emb.txt".format(args.dataset_name)) - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--dataset_name", type=str, default='METR-LA', help='dataset name.') - parser.add_argument("--is_directed", type=bool, default=True, help="direct graph.") - parser.add_argument("--p", type=int, default=2, help="p in node2vec.",) - parser.add_argument("--q", type=int, default=1, help="q in node2vec.",) - parser.add_argument("--num_walks", type=int, default=100, help="number of walks..",) - parser.add_argument("--vector_size", type=int, default=64, help='dimension of node vector.') - parser.add_argument("--walk_length", type=int, default=80, help='walk length.') - parser.add_argument("--epochs", type=int, default=1000, help='epochs') - args = parser.parse_args() - print(args) - generate_node_embeddings(args) diff --git a/scripts/node2vec/node2vec.py b/scripts/node2vec/node2vec.py deleted file mode 100644 index c03f312a..00000000 --- a/scripts/node2vec/node2vec.py +++ /dev/null @@ -1,157 +0,0 @@ -''' -Aditya Grover and Jure Leskovec. node2vec: Scalable Feature Learning for Networks. In KDD, 2016. -https://github.com/aditya-grover/node2vec - -Code ref: https://github.com/zhengchuanpan/GMAN/blob/master/METR/node2vec/node2vec.py -''' - -import numpy as np -import networkx as nx -import random - - -class Graph(): - def __init__(self, nx_G, is_directed, p, q): - self.G = nx_G - self.is_directed = is_directed - self.p = p - self.q = q - - def node2vec_walk(self, walk_length, start_node): - ''' - Simulate a random walk starting from start node. - ''' - G = self.G - alias_nodes = self.alias_nodes - alias_edges = self.alias_edges - - walk = [start_node] - - while len(walk) < walk_length: - cur = walk[-1] - cur_nbrs = sorted(G.neighbors(cur)) - if len(cur_nbrs) > 0: - if len(walk) == 1: - walk.append(cur_nbrs[alias_draw(alias_nodes[cur][0], alias_nodes[cur][1])]) - else: - prev = walk[-2] - next = cur_nbrs[alias_draw(alias_edges[(prev, cur)][0], - alias_edges[(prev, cur)][1])] - walk.append(next) - else: - break - - return walk - - def simulate_walks(self, num_walks, walk_length): - ''' - Repeatedly simulate random walks from each node. - ''' - G = self.G - walks = [] - nodes = list(G.nodes()) - print ('Walk iteration:') - for walk_iter in range(num_walks): - print (str(walk_iter+1), '/', str(num_walks)) - random.shuffle(nodes) - for node in nodes: - walks.append(self.node2vec_walk(walk_length=walk_length, start_node=node)) - - return walks - - def get_alias_edge(self, src, dst): - ''' - Get the alias edge setup lists for a given edge. - ''' - G = self.G - p = self.p - q = self.q - - unnormalized_probs = [] - for dst_nbr in sorted(G.neighbors(dst)): - if dst_nbr == src: - unnormalized_probs.append(G[dst][dst_nbr]['weight']/p) - elif G.has_edge(dst_nbr, src): - unnormalized_probs.append(G[dst][dst_nbr]['weight']) - else: - unnormalized_probs.append(G[dst][dst_nbr]['weight']/q) - norm_const = sum(unnormalized_probs) - normalized_probs = [float(u_prob)/norm_const for u_prob in unnormalized_probs] - - return alias_setup(normalized_probs) - - def preprocess_transition_probs(self): - ''' - Preprocessing of transition probabilities for guiding the random walks. - ''' - G = self.G - is_directed = self.is_directed - - alias_nodes = {} - for node in G.nodes(): - unnormalized_probs = [G[node][nbr]['weight'] for nbr in sorted(G.neighbors(node))] - norm_const = sum(unnormalized_probs) - normalized_probs = [float(u_prob)/norm_const for u_prob in unnormalized_probs] - alias_nodes[node] = alias_setup(normalized_probs) - - alias_edges = {} - triads = {} - - if is_directed: - for edge in G.edges(): - alias_edges[edge] = self.get_alias_edge(edge[0], edge[1]) - else: - for edge in G.edges(): - alias_edges[edge] = self.get_alias_edge(edge[0], edge[1]) - alias_edges[(edge[1], edge[0])] = self.get_alias_edge(edge[1], edge[0]) - - self.alias_nodes = alias_nodes - self.alias_edges = alias_edges - - return - - -def alias_setup(probs): - ''' - Compute utility lists for non-uniform sampling from discrete distributions. - Refer to https://hips.seas.harvard.edu/blog/2013/03/03/the-alias-method-efficient-sampling-with-many-discrete-outcomes/ - for details - ''' - K = len(probs) - q = np.zeros(K) - J = np.zeros(K, dtype=np.int) - - smaller = [] - larger = [] - for kk, prob in enumerate(probs): - q[kk] = K*prob - if q[kk] < 1.0: - smaller.append(kk) - else: - larger.append(kk) - - while len(smaller) > 0 and len(larger) > 0: - small = smaller.pop() - large = larger.pop() - - J[small] = large - q[large] = q[large] + q[small] - 1.0 - if q[large] < 1.0: - smaller.append(large) - else: - larger.append(large) - - return J, q - -def alias_draw(J, q): - ''' - Draw sample from a non-uniform discrete distribution using alias sampling. - ''' - K = len(J) - - kk = int(np.floor(np.random.rand()*K)) - if np.random.rand() < q[kk]: - return kk - else: - return J[kk] - \ No newline at end of file diff --git a/test/test_data.py b/test/test_data.py deleted file mode 100644 index 855be374..00000000 --- a/test/test_data.py +++ /dev/null @@ -1,8 +0,0 @@ -import numpy as np -import pickle as pkl -name = 'METR-LA' -name = 'PEMS-BAY' - -data = pkl.load(open('datasets/{0}/data.pkl'.format(name), 'rb')) # 34272, 207, 3 -args = pkl.load(open('datasets/{0}/args.pkl'.format(name), 'rb')) -index = pkl.load(open('datasets/{0}/index.pkl'.format(name), 'rb')) diff --git a/test/test_dataset.py b/test/test_dataset.py deleted file mode 100644 index 8cb92522..00000000 --- a/test/test_dataset.py +++ /dev/null @@ -1,15 +0,0 @@ -import os -import sys -sys.path.append(os.path.abspath(os.path.dirname(os.path.dirname(__file__)))) -from operator import index -from basicts.data.base_dataset import BaseDataset - -dataset_name = 'METR-LA' -raw_file_path = 'datasets/{0}/data.pkl'.format(dataset_name) -index_file_path = 'datasets/{0}/index.pkl'.format(dataset_name) -mode = 'train' -dataset = BaseDataset(raw_file_path, index_file_path, mode) -for _ in dataset: - data = _ - a = 1 - break \ No newline at end of file diff --git a/test/test_model.py b/test/test_model.py deleted file mode 100644 index 50e24692..00000000 --- a/test/test_model.py +++ /dev/null @@ -1,20 +0,0 @@ -import torch -import torch.nn as nn -import os -import sys -sys.path.append(os.path.abspath(os.path.dirname(os.path.dirname(__file__)))) - -from basicts.archs.Stat_arch import SimpleMovingAverage, AutoRegressive, VectorAutoRegression -from basicts.archs.MTGNN_arch import MTGNN -sma = SimpleMovingAverage(12, 12, 12) -data = torch.randn(64, 12, 207, 3) -pred = sma(data) - -wma = AutoRegressive(12, 12, 12) -data = torch.randn(64, 12, 207, 3) -pred = wma(data) - -var = VectorAutoRegression(12, 12, 12, 207) -data = torch.randn(64, 12, 207, 3) -pred = var(data) -a = 1 \ No newline at end of file diff --git a/tests/test_all.py b/tests/test_all.py new file mode 100644 index 00000000..2e919d31 --- /dev/null +++ b/tests/test_all.py @@ -0,0 +1,19 @@ +import os +import time +import sys + +from utils import test + +sys.path.append(os.path.abspath(__file__ + "/../..")) + +if __name__ == "__main__": + MODELS = os.listdir("examples") + MODELS.remove("run.py") + DATASETS = os.listdir("datasets") + DATASETS.remove("raw_data") + DATASETS.remove("README.md") + + with open("test_specific_" + time.strftime("%Y-%m-%d_%H:%M:%S", time.localtime()) + ".log", 'w') as f: + for model in MODELS: + for dataset in DATASETS: + test(model, dataset, f) diff --git a/tests/test_specific.py b/tests/test_specific.py new file mode 100644 index 00000000..0fdf47f6 --- /dev/null +++ b/tests/test_specific.py @@ -0,0 +1,38 @@ +import os +import time +import sys + +from utils import test + +sys.path.append(os.path.abspath(__file__ + "/../..")) + +if __name__ == "__main__": + MODELS = os.listdir("examples") + MODELS.remove("run.py") + + DATASETS = os.listdir("datasets") + DATASETS.remove("raw_data") + DATASETS.remove("README.md") + + print("Current support models: {0}".format(MODELS)) + print("Current support datasets: {0}".format(DATASETS)) + + reply_models = str(input(f"Please select the names of the models to test, separated by commas: ")) + if reply_models == "": + reply_models = MODELS + else: + reply_models = reply_models.strip().replace(" ", "").split(",") + + reply_datasets = str(input(f"Please select the names of the datasets to test, separated by commas: ")) + if reply_datasets == "": + reply_datasets = DATASETS + else: + reply_datasets = reply_datasets.strip().replace(" ", "").split(",") + + print("Models to test: {0}".format(reply_models)) + print("Datasets to test: {0}".format(reply_datasets)) + + with open("test_specific_" + time.strftime("%Y-%m-%d_%H:%M:%S", time.localtime()) + ".log", 'w') as f: + for model in reply_models: + for dataset in reply_datasets: + test(model, dataset, f) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..2d86251c --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,26 @@ +import logging +import traceback +import easytorch +from easytorch import launch_training + + +def test(model, dataset, exception_log_file): + CFG = __import__("examples.{0}.{0}_{1}".format(model, dataset), + fromlist=["examples.{0}.{0}_{1}".format(model, dataset)]).CFG + # CFG.TRAIN.NUM_EPOCHS = 1 + # CFG.ENV.SEED = seed + print(("*" * 60 + "{0:>10}" + "@{1:<10}" + "*" * 60).format(model, dataset)) + try: + launch_training(CFG, "0") + except Exception as e: + exception_log_file.write("\n" + "*" * 60 + "{0:>10}@{1:<22}".format( + model, dataset + "test failed.") + "*" * 60 + "\n") + traceback.print_exc(limit=1, file=exception_log_file) + # safely delete all the handlers of 'easytorch-training' logger and re-add them, so as to get the correct log file path. + logger = logging.getLogger("easytorch-training") + for h in logger.handlers: + h.close() + logger.handlers = [] + easytorch.utils.logging.logger_initialized.remove("easytorch-training") + + print("*" * 141)

}m51KNOug^CN>fNIsCwX^v+&yUc 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