├── models ├── stgcn │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-38.pyc │ │ ├── layers.cpython-38.pyc │ │ └── models.cpython-38.pyc │ ├── models.py │ └── layers.py ├── __pycache__ │ ├── lstm.cpython-38.pyc │ ├── ma2gcn.cpython-38.pyc │ ├── ASTGCN_r.cpython-38.pyc │ └── graph_wavenet.cpython-38.pyc ├── lstm.py ├── graph_wavenet.py ├── ma2gcn.py └── ASTGCN_r.py ├── imgs ├── arch.png ├── adj_attention.png └── ttb_pipeline.png ├── data ├── io_matrix_grid.npy └── feature_matrix_grid.npy ├── trainer ├── __pycache__ │ ├── __init__.cpython-38.pyc │ ├── trainer_lstm.cpython-38.pyc │ ├── trainer_stgcn.cpython-38.pyc │ ├── trainer_astgcn.cpython-38.pyc │ ├── trainer_ma2gcn.cpython-38.pyc │ └── trainer_graph_wavenet.cpython-38.pyc ├── trainer_astgcn.py ├── trainer_graph_wavenet.py ├── trainer_stgcn.py ├── trainer_lstm.py └── trainer_ma2gcn.py ├── requirements.txt ├── .idea └── .gitignore ├── config ├── config_arima.yaml ├── config_svr.yaml ├── config_lstm.yaml ├── config_graph_wavenet.yaml ├── config_astgcn.yaml ├── config_stgcn.yaml └── config_ma2gcn.yaml ├── README.md ├── main ├── main_svr.py ├── main_arima.py ├── main_lstm.py ├── main_astgcn.py ├── main_graph_wavenet.py ├── main_stgcn.py └── main_ma2gcn.py └── utils.py /models/stgcn/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['layers', 'models'] -------------------------------------------------------------------------------- /imgs/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/imgs/arch.png -------------------------------------------------------------------------------- /data/io_matrix_grid.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/data/io_matrix_grid.npy -------------------------------------------------------------------------------- /imgs/adj_attention.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/imgs/adj_attention.png -------------------------------------------------------------------------------- /imgs/ttb_pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/imgs/ttb_pipeline.png -------------------------------------------------------------------------------- /data/feature_matrix_grid.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/data/feature_matrix_grid.npy -------------------------------------------------------------------------------- /models/__pycache__/lstm.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/models/__pycache__/lstm.cpython-38.pyc -------------------------------------------------------------------------------- /models/__pycache__/ma2gcn.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/models/__pycache__/ma2gcn.cpython-38.pyc -------------------------------------------------------------------------------- /models/__pycache__/ASTGCN_r.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/models/__pycache__/ASTGCN_r.cpython-38.pyc -------------------------------------------------------------------------------- /trainer/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/trainer/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /models/__pycache__/graph_wavenet.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/models/__pycache__/graph_wavenet.cpython-38.pyc -------------------------------------------------------------------------------- /models/stgcn/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/models/stgcn/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /models/stgcn/__pycache__/layers.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/models/stgcn/__pycache__/layers.cpython-38.pyc -------------------------------------------------------------------------------- /models/stgcn/__pycache__/models.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/models/stgcn/__pycache__/models.cpython-38.pyc -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.23.2 2 | pandas==2.2.1 3 | PyYAML==6.0.1 4 | scikit_learn==1.4.1.post1 5 | scipy==1.12.0 6 | statsmodels==0.14.1 7 | torch==1.12.1 8 | -------------------------------------------------------------------------------- /trainer/__pycache__/trainer_lstm.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/trainer/__pycache__/trainer_lstm.cpython-38.pyc -------------------------------------------------------------------------------- /trainer/__pycache__/trainer_stgcn.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/trainer/__pycache__/trainer_stgcn.cpython-38.pyc -------------------------------------------------------------------------------- /trainer/__pycache__/trainer_astgcn.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/trainer/__pycache__/trainer_astgcn.cpython-38.pyc -------------------------------------------------------------------------------- /trainer/__pycache__/trainer_ma2gcn.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/trainer/__pycache__/trainer_ma2gcn.cpython-38.pyc -------------------------------------------------------------------------------- /trainer/__pycache__/trainer_graph_wavenet.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachysun/Taxi_Traffic_Benchmark/HEAD/trainer/__pycache__/trainer_graph_wavenet.cpython-38.pyc -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /config/config_arima.yaml: -------------------------------------------------------------------------------- 1 | dataset: 2 | adj_data_path: '../data/adjacency_matrix_grid.csv' 3 | feature_matrix_data_path: '../data/feature_matrix_grid.npy' 4 | input_seq: 12 5 | output_seq: 1 6 | num_of_vertices: 225 7 | batch_size: 8 8 | split_rate: [7, 2, 1] 9 | device: None 10 | -------------------------------------------------------------------------------- /config/config_svr.yaml: -------------------------------------------------------------------------------- 1 | dataset: 2 | adj_data_path: '../data/adjacency_matrix_grid.csv' 3 | feature_matrix_data_path: '../data/feature_matrix_grid.npy' 4 | input_seq: 12 5 | output_seq: 1 6 | num_of_vertices: 225 7 | batch_size: 8 8 | split_rate: [7, 2, 1] 9 | device: cuda:0 10 | -------------------------------------------------------------------------------- /models/lstm.py: -------------------------------------------------------------------------------- 1 | import torch.nn as nn 2 | 3 | 4 | class LSTM(nn.Module): 5 | def __init__(self, input_size, hidden_size, output_size): 6 | super(LSTM, self).__init__() 7 | self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True) 8 | self.fc = nn.Linear(hidden_size, output_size) 9 | 10 | def forward(self, x): 11 | lstm_out, _ = self.lstm(x) 12 | out = self.fc(lstm_out[:, -1, :]) 13 | return out 14 | -------------------------------------------------------------------------------- /config/config_lstm.yaml: -------------------------------------------------------------------------------- 1 | dataset: 2 | adj_data_path: '../data/adjacency_matrix_grid.csv' 3 | feature_matrix_data_path: '../data/feature_matrix_grid.npy' 4 | input_seq: 12 5 | output_seq: 1 6 | num_of_vertices: 225 7 | batch_size: 256 8 | device: cuda:0 9 | split_rate: [7, 2, 1] 10 | 11 | optimize: 12 | learning_rate: 0.001 13 | weight_decay: 0.0001 14 | name: Adam 15 | 16 | trainer: 17 | in_dim: 2 18 | hidden_dim: 16 19 | out_dim: 2 20 | theta: 0.5 21 | epoch: 300 22 | model_path: '../saved_models' 23 | results_path: '../results' 24 | -------------------------------------------------------------------------------- /config/config_graph_wavenet.yaml: -------------------------------------------------------------------------------- 1 | dataset: 2 | adj_data_path: '../data/adjacency_matrix_grid.csv' 3 | feature_matrix_data_path: '../data/feature_matrix_grid.npy' 4 | input_seq: 12 5 | output_seq: 1 6 | num_of_vertices: 225 7 | batch_size: 8 8 | device: cuda:0 9 | split_rate: [7, 2, 1] 10 | 11 | optimize: 12 | learning_rate: 0.001 13 | weight_decay: 0.0001 14 | name: Adam 15 | 16 | trainer: 17 | K: 3 18 | theta: 0.5 19 | epoch: 300 20 | dropout: 0.5 21 | gcn_bool: True 22 | aptonly: False 23 | addaptadj: True 24 | randomadj: True 25 | nhid: 32 26 | in_dim: 2 27 | out_dim: 1 28 | model_path: '../saved_models' 29 | results_path: '../results' 30 | -------------------------------------------------------------------------------- /config/config_astgcn.yaml: -------------------------------------------------------------------------------- 1 | dataset: 2 | adj_data_path: '../data/adjacency_matrix_grid.csv' 3 | feature_matrix_data_path: '../data/feature_matrix_grid.npy' 4 | input_seq: 12 5 | output_seq: 1 6 | num_of_vertices: 225 7 | batch_size: 8 8 | device: cuda:0 9 | split_rate: [7, 2, 1] 10 | 11 | optimize: 12 | learning_rate: 0.001 13 | weight_decay: 0.0001 14 | name: Adam 15 | 16 | trainer: 17 | K: 3 18 | theta: 0.5 19 | epoch: 300 20 | ctx: 0 21 | in_channels: 2 22 | nb_block: 2 23 | nb_chev_filter: 64 24 | nb_time_filter: 64 25 | start_epoch: 0 26 | num_of_hours: 1 27 | num_of_days: 0 28 | num_of_weeks: 0 29 | model_path: '../saved_models' 30 | results_path: '../results' 31 | -------------------------------------------------------------------------------- /config/config_stgcn.yaml: -------------------------------------------------------------------------------- 1 | dataset: 2 | adj_data_path: '../data/adjacency_matrix_grid.csv' 3 | feature_matrix_data_path: '../data/feature_matrix_grid.npy' 4 | input_seq: 12 5 | output_seq: 1 6 | num_of_vertices: 225 7 | batch_size: 8 8 | device: cuda:0 9 | split_rate: [7, 2, 1] 10 | 11 | optimize: 12 | learning_rate: 0.001 13 | weight_decay: 0.0001 14 | name: Adam 15 | 16 | trainer: 17 | Kt: 3 18 | Ks: 3 # [2, 3] 19 | theta: 0.5 20 | epoch: 300 21 | n_his: 12 22 | n_pred: 1 23 | channel: 2 24 | act_func: glu # [glu, gtu] 25 | graph_conv_type: cheb_graph_conv # [cheb_graph_conv, graph_conv] 26 | stblock_num: 2 27 | enable_bias: True 28 | dropout: 0.5 29 | model_path: '../saved_models' 30 | results_path: '../results' -------------------------------------------------------------------------------- /config/config_ma2gcn.yaml: -------------------------------------------------------------------------------- 1 | dataset: 2 | origin_adj_data_path: '../data/adjacency_matrix_grid.csv' 3 | io_adj_data_path: '../data/io_matrix_grid.npy' 4 | feature_matrix_data_path: '../data/feature_matrix_grid.npy' 5 | input_seq: 12 6 | output_seq: 1 7 | batch_size: 8 8 | device: cuda:0 9 | basic_interval: 5 # minutes 10 | io_matrix_interval: 3 # hours 11 | split_rate: [7, 2, 1] 12 | 13 | # Adam, AdamW, RMSprop 14 | optimize: 15 | learning_rate: 0.001 16 | weight_decay: 0.0001 17 | name: Adam 18 | 19 | trainer: 20 | K: 3 21 | epoch: 300 22 | theta: 0.5 23 | block_nums: 3 24 | drop_rate: 0.00 25 | adj_hidden_dim: 16 26 | kernel_size: [1, 3] 27 | in_channel_list: [2, 16, 8] 28 | out_channel_list: [16, 8, 2] 29 | in_tcn_dim_list: [2, 16, 8] 30 | out_tcn_dim_list: [2, 16, 8] 31 | model_path: '../saved_models' 32 | results_path: '../results' 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Taxi Traffic Benchmark 2 | 3 | ### Pipeline 4 | 5 | 6 | 7 | ### Dataset 8 | 9 | - [Shanghai Taxi](https://cse.hkust.edu.hk/scrg/) 10 | - **Note:** This link has expired. The processed data is located in the "/data" directory. 11 | 12 | ### Baseline 13 | 14 | - ARIMA 15 | - SVR 16 | - LSTM 17 | - STGCN[[Paper]](https://arxiv.org/abs/1709.04875) [[Code]](./models/stgcn) [[Official Code]](https://github.com/hazdzz/STGCN) 18 | - ASTGCN[[Paper]](https://ojs.aaai.org/index.php/AAAI/article/view/3881) [[Code]](./models/ASTGCN_r.py) [[Official Code]](https://github.com/guoshnBJTU/ASTGCN-r-pytorch) 19 | - Graph WaveNet[[Paper]](https://arxiv.org/pdf/1906.00121) [[Code]](./models/graph_wavenet.py) [[Official Code]](https://github.com/nnzhan/Graph-WaveNet) 20 | 21 | ### Proposed 22 | 23 | [[Paper]](https://arxiv.org/abs/2401.08727)**MA2GCN: Multi Adjacency relationship Attention Graph Convolutional 24 | Networks for Traffic Prediction using Trajectory data** 25 | 26 | | | | 27 | | :----------------------------------------------------------: | :----------------------------------------------------------: | 28 | | Architecture | Multi Adjacency relationship Attention mechanism | 29 | 30 | ### Related Links 31 | 32 | - GPS2Graph [Link](https://github.com/zachysun/Gps2graph) 33 | 34 | ### Important Notes 35 | 36 | - The Shanghai Taxi Dataset rarely appears in traffic prediction tasks, so the six baselines above can be used as a reference. 37 | - The 'io_matrix' in code corresponds to the 'vehicle entry and exit matrix ' in ma2gcn paper. The 'io_adj' in code corresponds to the $A_{mo}$ in ma2gcn paper. 38 | - The six baselines can't use the mobility matrix as it's specificly used for proposed ma2gcn model. 39 | - Due to the mobility matrix is time sensitive, the dataset needs to be divided in chronological order and cannot be shuffled. For the six baselines, there is no such limit. 40 | - The proposed model is on arxiv.org. Now it can be seen as a simple technical report exploring the use of taxi trajectories for traffic prediction tasks. There's still significant potential for enhancement. 41 | -------------------------------------------------------------------------------- /models/stgcn/models.py: -------------------------------------------------------------------------------- 1 | import torch.nn as nn 2 | from models.stgcn import layers 3 | 4 | 5 | class STGCNChebGraphConv(nn.Module): 6 | # STGCNChebGraphConv contains 'TGTND TGTND TNFF' structure 7 | # ChebGraphConv is the graph convolution from ChebyNet. 8 | # Using the Chebyshev polynomials of the first kind as a graph filter. 9 | 10 | # T: Gated Temporal Convolution Layer (GLU or GTU) 11 | # G: Graph Convolution Layer (ChebGraphConv) 12 | # T: Gated Temporal Convolution Layer (GLU or GTU) 13 | # N: Layer Normolization 14 | # D: Dropout 15 | 16 | # T: Gated Temporal Convolution Layer (GLU or GTU) 17 | # G: Graph Convolution Layer (ChebGraphConv) 18 | # T: Gated Temporal Convolution Layer (GLU or GTU) 19 | # N: Layer Normolization 20 | # D: Dropout 21 | 22 | # T: Gated Temporal Convolution Layer (GLU or GTU) 23 | # N: Layer Normalization 24 | # F: Fully-Connected Layer 25 | # F: Fully-Connected Layer 26 | 27 | def __init__(self, blocks, n_vertex, Kt, Ks, act_func, graph_conv_type, gso, enable_bias, droprate, n_his): 28 | super(STGCNChebGraphConv, self).__init__() 29 | modules = [] 30 | for l in range(len(blocks) - 3): 31 | modules.append(layers.STConvBlock(Kt, Ks, n_vertex, blocks[l][-1], blocks[l + 1], act_func, 32 | graph_conv_type, gso, enable_bias, droprate)) 33 | self.st_blocks = nn.Sequential(*modules) 34 | Ko = n_his - (len(blocks) - 3) * 2 * (Kt - 1) 35 | self.Ko = Ko 36 | if self.Ko > 1: 37 | self.output = layers.OutputBlock(Ko, blocks[-3][-1], blocks[-2], blocks[-1][0], n_vertex, act_func, 38 | enable_bias, droprate) 39 | elif self.Ko == 0: 40 | self.fc1 = nn.Linear(in_features=blocks[-3][-1], out_features=blocks[-2][0], bias=enable_bias) 41 | self.fc2 = nn.Linear(in_features=blocks[-2][0], out_features=blocks[-1][0], bias=enable_bias) 42 | self.relu = nn.ReLU() 43 | self.leaky_relu = nn.LeakyReLU() 44 | self.silu = nn.SiLU() 45 | self.dropout = nn.Dropout(p=droprate) 46 | 47 | def forward(self, x): 48 | x = self.st_blocks(x) 49 | if self.Ko > 1: 50 | x = self.output(x) 51 | elif self.Ko == 0: 52 | x = self.fc1(x.permute(0, 2, 3, 1)) 53 | x = self.relu(x) 54 | x = self.fc2(x).permute(0, 3, 1, 2) 55 | 56 | return x 57 | -------------------------------------------------------------------------------- /main/main_svr.py: -------------------------------------------------------------------------------- 1 | from utils import ProcessData, masked_mae_np, masked_mape_np, masked_rmse_np, StandardScalar_np 2 | import numpy as np 3 | from sklearn.svm import SVR 4 | import yaml 5 | import argparse 6 | 7 | 8 | def load_config(config_path): 9 | with open(config_path, 'r', encoding='utf-8') as f: 10 | config = yaml.safe_load(f) 11 | return config 12 | 13 | 14 | def main(config): 15 | data_config = config['dataset'] 16 | feature_matrix = np.load(data_config['feature_matrix_data_path']) 17 | # shape -> (number of samples, sequence length, number of vertexes, features) 18 | train_data, train_labels, val_data, val_labels, test_data, test_labels = ProcessData(feature_matrix, 19 | data_config['input_seq'], 20 | data_config['output_seq'], 21 | data_config['split_rate']) 22 | 23 | scalar = StandardScalar_np(train_data, axis=(0, 1, 2)) 24 | data_x_sets = [train_data, val_data, test_data] 25 | data_y_sets = [train_labels, val_labels, test_labels] 26 | data_x_normed_sets = [] 27 | data_y_real_sets = [] 28 | data_y_normed_sets = [] 29 | 30 | for data in data_x_sets: 31 | data_normed = scalar.transform(data) 32 | data_x_normed_sets.append(data_normed) 33 | train_x, val_x, test_x = data_x_normed_sets 34 | 35 | for data in data_y_sets: 36 | data_y_real_sets.append(data) 37 | train_real_y, val_real_y, test_real_y = data_y_real_sets 38 | 39 | for data in data_y_sets: 40 | data_normed = scalar.transform(data) 41 | data_y_normed_sets.append(data_normed) 42 | train_normed_y, val_normed_y, test_normed_y = data_y_normed_sets 43 | 44 | X_train = np.einsum('btnf->bnft', train_x) 45 | X_train = X_train.reshape(-1, X_train.shape[-1]) 46 | y_train = np.reshape(train_normed_y, (-1)) 47 | X_test = np.einsum('btnf->bnft', test_x) 48 | X_test = X_test.reshape(-1, X_test.shape[-1]) 49 | y_test = np.reshape(test_real_y, (-1)) 50 | b, t, n, f = test_real_y.shape 51 | 52 | svr = SVR(kernel='rbf') 53 | svr.fit(X_train, y_train) 54 | y_pred = svr.predict(X_test) 55 | y_pred = scalar.inverse_transform(y_pred.reshape(b, t, n, f)).reshape(-1) 56 | 57 | loss = masked_mae_np(y_pred, y_test, 0.0) 58 | mape = masked_mape_np(y_pred, y_test, 0.0) 59 | rmse = masked_rmse_np(y_pred, y_test, 0.0) 60 | 61 | print(loss) 62 | print(mape) 63 | print(rmse) 64 | 65 | 66 | if __name__ == '__main__': 67 | parser = argparse.ArgumentParser() 68 | parser.add_argument('--config_path', type=str, default='../config/config_svr.yaml', 69 | help='Config path') 70 | args = parser.parse_args() 71 | configs = load_config(args.config_path) 72 | main(configs) 73 | -------------------------------------------------------------------------------- /main/main_arima.py: -------------------------------------------------------------------------------- 1 | from utils import ProcessData, masked_mae_np, masked_mape_np, masked_rmse_np 2 | from statsmodels.tsa.arima.model import ARIMA 3 | import numpy as np 4 | import yaml 5 | import argparse 6 | 7 | 8 | def load_config(config_path): 9 | with open(config_path, 'r', encoding='utf-8') as f: 10 | config = yaml.safe_load(f) 11 | return config 12 | 13 | 14 | def main(config): 15 | data_config = config['dataset'] 16 | feature_matrix = np.load(data_config['feature_matrix_data_path']) 17 | # shape -> (number of samples, sequence length, number of vertexes, features) 18 | train_data, train_labels, val_data, val_labels, test_data, test_labels = ProcessData(feature_matrix, 19 | data_config['input_seq'], 20 | data_config['output_seq'], 21 | data_config['split_rate']) 22 | 23 | data = test_data 24 | labels = test_labels 25 | reshaped_data = data.transpose(0, 2, 1, 3).reshape(-1, data.shape[1], data.shape[3]) 26 | reshaped_data_1 = reshaped_data[..., 0] 27 | reshaped_data_2 = reshaped_data[..., 1] 28 | reshaped_labels = labels.transpose(0, 2, 1, 3).reshape(-1, labels.shape[1], labels.shape[3]) 29 | reshaped_labels_1 = reshaped_labels[..., 0] 30 | reshaped_labels_2 = reshaped_labels[..., 1] 31 | 32 | preds = [] 33 | for i in range(reshaped_data_1.shape[0]): 34 | node_feature_data = reshaped_data_1[i] 35 | model = ARIMA(node_feature_data, order=(1, 1, 1)) 36 | model_fit = model.fit() 37 | y_pred = model_fit.forecast(steps=1)[0] 38 | preds.append(y_pred) 39 | 40 | preds = np.array(preds) 41 | preds = preds.reshape(len(preds), 1) 42 | loss1 = masked_mae_np(preds, reshaped_labels_1, 0.0) 43 | mape1 = masked_mape_np(preds, reshaped_labels_1, 0.0) 44 | rmse1 = masked_rmse_np(preds, reshaped_labels_1, 0.0) 45 | 46 | preds2 = [] 47 | for i in range(reshaped_data_2.shape[0]): 48 | node_feature_data = reshaped_data_2[i] 49 | model = ARIMA(node_feature_data, order=(1, 1, 1)) 50 | model_fit = model.fit() 51 | y_pred = model_fit.forecast(steps=1)[0] 52 | preds2.append(y_pred) 53 | 54 | preds2 = np.array(preds) 55 | preds2 = preds.reshape(len(preds), 1) 56 | loss2 = masked_mae_np(preds, reshaped_labels_2, 0.0) 57 | mape2 = masked_mape_np(preds, reshaped_labels_2, 0.0) 58 | rmse2 = masked_rmse_np(preds, reshaped_labels_2, 0.0) 59 | 60 | print((loss1 + loss2) / 2) 61 | print((mape1 + mape2) / 2) 62 | print((rmse1 + rmse2) / 2) 63 | 64 | 65 | if __name__ == '__main__': 66 | parser = argparse.ArgumentParser() 67 | parser.add_argument('--config_path', type=str, default='../config/config_arima.yaml', 68 | help='Config path') 69 | args = parser.parse_args() 70 | configs = load_config(args.config_path) 71 | main(configs) 72 | -------------------------------------------------------------------------------- /trainer/trainer_astgcn.py: -------------------------------------------------------------------------------- 1 | from utils import masked_mae, masked_mape, masked_rmse 2 | import numpy as np 3 | import torch.nn as nn 4 | import torch 5 | 6 | 7 | class trainer: 8 | def __init__(self, model, optimizer, scaler, theta): 9 | self.model = model 10 | self.criterion = masked_mae 11 | self.optimizer = optimizer 12 | self.scaler = scaler 13 | self.theta = theta 14 | for p in model.parameters(): 15 | if p.dim() > 1: 16 | nn.init.xavier_uniform_(p) 17 | else: 18 | nn.init.uniform_(p) 19 | 20 | def train(self, x, y_real): 21 | self.optimizer.zero_grad() 22 | 23 | output = self.model(x) 24 | y_pred = self.scaler.inverse_transform(output) 25 | loss = self.theta * self.criterion(y_pred[..., 0], y_real[..., 0], 0.0) + \ 26 | (1 - self.theta) * self.criterion(y_pred[..., 1], y_real[..., 1], 0.0) 27 | 28 | loss.backward() 29 | self.optimizer.step() 30 | 31 | mape = masked_mape(y_pred, y_real, 0.0).item() 32 | rmse = masked_rmse(y_pred, y_real, 0.0).item() 33 | return loss.item(), mape, rmse 34 | 35 | def train_one_epoch(self, train_loader): 36 | self.model.train() 37 | loss_inner_epoch = [] 38 | mape_inner_epoch = [] 39 | rmse_inner_epoch = [] 40 | for batch_idx, (x, y_real) in enumerate(train_loader): 41 | 42 | loss, mape, rmse = self.train(x, y_real) 43 | loss_inner_epoch.append(loss) 44 | mape_inner_epoch.append(mape) 45 | rmse_inner_epoch.append(rmse) 46 | 47 | return np.array(loss_inner_epoch).mean(), np.array(mape_inner_epoch).mean(), \ 48 | np.array(rmse_inner_epoch).mean() 49 | 50 | def eval(self, x, y_real): 51 | output = self.model(x) 52 | y_pred = self.scaler.inverse_transform(output) 53 | 54 | loss = self.theta * self.criterion(y_pred[..., 0], y_real[..., 0], 0.0) + \ 55 | (1 - self.theta) * self.criterion(y_pred[..., 1], y_real[..., 1], 0.0) 56 | 57 | mape = masked_mape(y_pred, y_real, 0.0).item() 58 | rmse = masked_rmse(y_pred, y_real, 0.0).item() 59 | return loss.item(), mape, rmse 60 | 61 | def eval_one_epoch(self, val_loader): 62 | self.model.eval() 63 | loss_inner_epoch = [] 64 | mape_inner_epoch = [] 65 | rmse_inner_epoch = [] 66 | for batch_idx, (x, y_real) in enumerate(val_loader): 67 | loss, mape, rmse = self.eval(x, y_real) 68 | loss_inner_epoch.append(loss) 69 | mape_inner_epoch.append(mape) 70 | rmse_inner_epoch.append(rmse) 71 | 72 | return np.array(loss_inner_epoch).mean(), np.array(mape_inner_epoch).mean(), \ 73 | np.array(rmse_inner_epoch).mean() 74 | 75 | def test(self, test_loader): 76 | loss_inner_epoch = [] 77 | mape_inner_epoch = [] 78 | rmse_inner_epoch = [] 79 | with torch.no_grad(): 80 | for batch_idx, (x, y_real) in enumerate(test_loader): 81 | loss, mape, rmse = self.eval(x, y_real) 82 | loss_inner_epoch.append(loss) 83 | mape_inner_epoch.append(mape) 84 | rmse_inner_epoch.append(rmse) 85 | 86 | return np.array(loss_inner_epoch).mean(), np.array(mape_inner_epoch).mean(), \ 87 | np.array(rmse_inner_epoch).mean() 88 | -------------------------------------------------------------------------------- /trainer/trainer_graph_wavenet.py: -------------------------------------------------------------------------------- 1 | from utils import masked_mae, masked_mape, masked_rmse 2 | import numpy as np 3 | import torch.nn as nn 4 | import torch 5 | 6 | 7 | class trainer: 8 | def __init__(self, model, optimizer, scaler, theta): 9 | self.model = model 10 | self.criterion = masked_mae 11 | self.optimizer = optimizer 12 | self.scaler = scaler 13 | self.theta = theta 14 | for p in model.parameters(): 15 | if p.dim() > 1: 16 | nn.init.xavier_uniform_(p) 17 | else: 18 | nn.init.uniform_(p) 19 | 20 | def train(self, x, y_real): 21 | self.optimizer.zero_grad() 22 | 23 | output = self.model(x) 24 | y_pred = self.scaler.inverse_transform(output) 25 | loss = self.theta * self.criterion(y_pred[..., 0], y_real[..., 0], 0.0) + \ 26 | (1 - self.theta) * self.criterion(y_pred[..., 1], y_real[..., 1], 0.0) 27 | 28 | loss.backward() 29 | self.optimizer.step() 30 | 31 | mape = masked_mape(y_pred, y_real, 0.0).item() 32 | rmse = masked_rmse(y_pred, y_real, 0.0).item() 33 | return loss.item(), mape, rmse 34 | 35 | def train_one_epoch(self, train_loader): 36 | self.model.train() 37 | loss_inner_epoch = [] 38 | mape_inner_epoch = [] 39 | rmse_inner_epoch = [] 40 | for batch_idx, (x, y_real) in enumerate(train_loader): 41 | 42 | loss, mape, rmse = self.train(x, y_real) 43 | loss_inner_epoch.append(loss) 44 | mape_inner_epoch.append(mape) 45 | rmse_inner_epoch.append(rmse) 46 | 47 | return np.array(loss_inner_epoch).mean(), np.array(mape_inner_epoch).mean(), \ 48 | np.array(rmse_inner_epoch).mean() 49 | 50 | def eval(self, x, y_real): 51 | output = self.model(x) 52 | y_pred = self.scaler.inverse_transform(output) 53 | 54 | loss = self.theta * self.criterion(y_pred[..., 0], y_real[..., 0], 0.0) + \ 55 | (1 - self.theta) * self.criterion(y_pred[..., 1], y_real[..., 1], 0.0) 56 | 57 | mape = masked_mape(y_pred, y_real, 0.0).item() 58 | rmse = masked_rmse(y_pred, y_real, 0.0).item() 59 | return loss.item(), mape, rmse 60 | 61 | def eval_one_epoch(self, val_loader): 62 | self.model.eval() 63 | loss_inner_epoch = [] 64 | mape_inner_epoch = [] 65 | rmse_inner_epoch = [] 66 | for batch_idx, (x, y_real) in enumerate(val_loader): 67 | loss, mape, rmse = self.eval(x, y_real) 68 | loss_inner_epoch.append(loss) 69 | mape_inner_epoch.append(mape) 70 | rmse_inner_epoch.append(rmse) 71 | 72 | return np.array(loss_inner_epoch).mean(), np.array(mape_inner_epoch).mean(), \ 73 | np.array(rmse_inner_epoch).mean() 74 | 75 | def test(self, test_loader): 76 | loss_inner_epoch = [] 77 | mape_inner_epoch = [] 78 | rmse_inner_epoch = [] 79 | with torch.no_grad(): 80 | for batch_idx, (x, y_real) in enumerate(test_loader): 81 | loss, mape, rmse = self.eval(x, y_real) 82 | loss_inner_epoch.append(loss) 83 | mape_inner_epoch.append(mape) 84 | rmse_inner_epoch.append(rmse) 85 | 86 | return np.array(loss_inner_epoch).mean(), np.array(mape_inner_epoch).mean(), \ 87 | np.array(rmse_inner_epoch).mean() 88 | 89 | -------------------------------------------------------------------------------- /trainer/trainer_stgcn.py: -------------------------------------------------------------------------------- 1 | from utils import masked_mae, masked_mape, masked_rmse 2 | import numpy as np 3 | import torch.nn as nn 4 | import torch 5 | 6 | 7 | class trainer: 8 | def __init__(self, model, optimizer, scaler, theta): 9 | self.model = model 10 | self.criterion = masked_mae 11 | self.optimizer = optimizer 12 | self.scaler = scaler 13 | self.theta = theta 14 | for p in model.parameters(): 15 | if p.dim() > 1: 16 | nn.init.xavier_uniform_(p) 17 | else: 18 | nn.init.uniform_(p) 19 | 20 | def train(self, x, y_real): 21 | self.optimizer.zero_grad() 22 | 23 | output = self.model(x) 24 | y_pred = self.scaler.inverse_transform(output.permute(0, 2, 3, 1)) 25 | loss = self.theta * self.criterion(y_pred[..., 0], y_real[..., 0], 0.0) + \ 26 | (1 - self.theta) * self.criterion(y_pred[..., 1], y_real[..., 1], 0.0) 27 | 28 | loss.backward() 29 | self.optimizer.step() 30 | 31 | mape = masked_mape(y_pred, y_real, 0.0).item() 32 | rmse = masked_rmse(y_pred, y_real, 0.0).item() 33 | return loss.item(), mape, rmse 34 | 35 | def train_one_epoch(self, train_loader): 36 | self.model.train() 37 | loss_inner_epoch = [] 38 | mape_inner_epoch = [] 39 | rmse_inner_epoch = [] 40 | for batch_idx, (x, y_real) in enumerate(train_loader): 41 | 42 | loss, mape, rmse = self.train(x, y_real) 43 | loss_inner_epoch.append(loss) 44 | mape_inner_epoch.append(mape) 45 | rmse_inner_epoch.append(rmse) 46 | 47 | return np.array(loss_inner_epoch).mean(), np.array(mape_inner_epoch).mean(), \ 48 | np.array(rmse_inner_epoch).mean() 49 | 50 | def eval(self, x, y_real): 51 | output = self.model(x) 52 | y_pred = self.scaler.inverse_transform(output.permute(0, 2, 3, 1)) 53 | 54 | loss = self.theta * self.criterion(y_pred[..., 0], y_real[..., 0], 0.0) + \ 55 | (1 - self.theta) * self.criterion(y_pred[..., 1], y_real[..., 1], 0.0) 56 | 57 | mape = masked_mape(y_pred, y_real, 0.0).item() 58 | rmse = masked_rmse(y_pred, y_real, 0.0).item() 59 | return loss.item(), mape, rmse 60 | 61 | def eval_one_epoch(self, val_loader): 62 | self.model.eval() 63 | loss_inner_epoch = [] 64 | mape_inner_epoch = [] 65 | rmse_inner_epoch = [] 66 | for batch_idx, (x, y_real) in enumerate(val_loader): 67 | loss, mape, rmse = self.eval(x, y_real) 68 | loss_inner_epoch.append(loss) 69 | mape_inner_epoch.append(mape) 70 | rmse_inner_epoch.append(rmse) 71 | 72 | return np.array(loss_inner_epoch).mean(), np.array(mape_inner_epoch).mean(), \ 73 | np.array(rmse_inner_epoch).mean() 74 | 75 | def test(self, test_loader): 76 | loss_inner_epoch = [] 77 | mape_inner_epoch = [] 78 | rmse_inner_epoch = [] 79 | with torch.no_grad(): 80 | for batch_idx, (x, y_real) in enumerate(test_loader): 81 | loss, mape, rmse = self.eval(x, y_real) 82 | loss_inner_epoch.append(loss) 83 | mape_inner_epoch.append(mape) 84 | rmse_inner_epoch.append(rmse) 85 | 86 | return np.array(loss_inner_epoch).mean(), np.array(mape_inner_epoch).mean(), \ 87 | np.array(rmse_inner_epoch).mean() 88 | -------------------------------------------------------------------------------- /trainer/trainer_lstm.py: -------------------------------------------------------------------------------- 1 | from utils import masked_mae, masked_mape, masked_rmse 2 | import numpy as np 3 | import torch.nn as nn 4 | import torch 5 | 6 | 7 | class trainer: 8 | def __init__(self, model, optimizer, scaler, theta): 9 | self.model = model 10 | self.criterion = masked_mae 11 | self.optimizer = optimizer 12 | self.scaler = scaler 13 | self.theta = theta 14 | for p in model.parameters(): 15 | if p.dim() > 1: 16 | nn.init.xavier_uniform_(p) 17 | else: 18 | nn.init.uniform_(p) 19 | 20 | def train(self, x, y_real): 21 | self.optimizer.zero_grad() 22 | 23 | output = self.model(x) 24 | y_pred = self.scaler.inverse_transform(output) 25 | y_pred = y_pred.unsqueeze(1) 26 | loss = self.theta * self.criterion(y_pred[..., 0], y_real[..., 0], 0.0) + \ 27 | (1 - self.theta) * self.criterion(y_pred[..., 1], y_real[..., 1], 0.0) 28 | 29 | loss.backward() 30 | self.optimizer.step() 31 | 32 | mape = masked_mape(y_pred, y_real, 0.0).item() 33 | rmse = masked_rmse(y_pred, y_real, 0.0).item() 34 | return loss.item(), mape, rmse 35 | 36 | def train_one_epoch(self, train_loader): 37 | self.model.train() 38 | loss_inner_epoch = [] 39 | mape_inner_epoch = [] 40 | rmse_inner_epoch = [] 41 | for batch_idx, (x, y_real) in enumerate(train_loader): 42 | 43 | loss, mape, rmse = self.train(x, y_real) 44 | loss_inner_epoch.append(loss) 45 | mape_inner_epoch.append(mape) 46 | rmse_inner_epoch.append(rmse) 47 | 48 | return np.array(loss_inner_epoch).mean(), np.array(mape_inner_epoch).mean(), \ 49 | np.array(rmse_inner_epoch).mean() 50 | 51 | def eval(self, x, y_real): 52 | output = self.model(x) 53 | y_pred = self.scaler.inverse_transform(output) 54 | y_pred = y_pred.unsqueeze(1) 55 | loss = self.theta * self.criterion(y_pred[..., 0], y_real[..., 0], 0.0) + \ 56 | (1 - self.theta) * self.criterion(y_pred[..., 1], y_real[..., 1], 0.0) 57 | 58 | mape = masked_mape(y_pred, y_real, 0.0).item() 59 | rmse = masked_rmse(y_pred, y_real, 0.0).item() 60 | return loss.item(), mape, rmse 61 | 62 | def eval_one_epoch(self, val_loader): 63 | self.model.eval() 64 | loss_inner_epoch = [] 65 | mape_inner_epoch = [] 66 | rmse_inner_epoch = [] 67 | for batch_idx, (x, y_real) in enumerate(val_loader): 68 | loss, mape, rmse = self.eval(x, y_real) 69 | loss_inner_epoch.append(loss) 70 | mape_inner_epoch.append(mape) 71 | rmse_inner_epoch.append(rmse) 72 | 73 | return np.array(loss_inner_epoch).mean(), np.array(mape_inner_epoch).mean(), \ 74 | np.array(rmse_inner_epoch).mean() 75 | 76 | def test(self, test_loader): 77 | loss_inner_epoch = [] 78 | mape_inner_epoch = [] 79 | rmse_inner_epoch = [] 80 | with torch.no_grad(): 81 | for batch_idx, (x, y_real) in enumerate(test_loader): 82 | loss, mape, rmse = self.eval(x, y_real) 83 | loss_inner_epoch.append(loss) 84 | mape_inner_epoch.append(mape) 85 | rmse_inner_epoch.append(rmse) 86 | 87 | return np.array(loss_inner_epoch).mean(), np.array(mape_inner_epoch).mean(), \ 88 | np.array(rmse_inner_epoch).mean() -------------------------------------------------------------------------------- /trainer/trainer_ma2gcn.py: -------------------------------------------------------------------------------- 1 | from utils import masked_mae, masked_mape, masked_rmse 2 | import numpy as np 3 | import torch.nn as nn 4 | import torch 5 | 6 | 7 | class trainer: 8 | def __init__(self, model, optimizer, scaler, theta, io_range, io_adj): 9 | self.model = model 10 | self.criterion = masked_mae 11 | self.optimizer = optimizer 12 | self.scaler = scaler 13 | self.theta = theta 14 | self.io_range = io_range 15 | self.io_adj = io_adj 16 | for p in model.parameters(): 17 | if p.dim() > 1: 18 | nn.init.xavier_uniform_(p) 19 | else: 20 | nn.init.uniform_(p) 21 | 22 | def train(self, x, y_real, cur_io_adj): 23 | self.optimizer.zero_grad() 24 | 25 | output, cur_io_adj = self.model((x, cur_io_adj)) 26 | y_pred = self.scaler.inverse_transform(output) 27 | loss = self.theta * self.criterion(y_pred[..., 0], y_real[..., 0], 0.0) + \ 28 | (1 - self.theta) * self.criterion(y_pred[..., 1], y_real[..., 1], 0.0) 29 | 30 | loss.backward() 31 | self.optimizer.step() 32 | 33 | mape = masked_mape(y_pred, y_real, 0.0).item() 34 | rmse = masked_rmse(y_pred, y_real, 0.0).item() 35 | return loss.item(), mape, rmse 36 | 37 | def train_one_epoch(self, train_loader): 38 | self.model.train() 39 | loss_inner_epoch = [] 40 | mape_inner_epoch = [] 41 | rmse_inner_epoch = [] 42 | for batch_idx, (x, y_real) in enumerate(train_loader): 43 | batch_num, _, _, _ = x.shape 44 | batch_start = batch_idx * batch_num 45 | batch_end = (batch_idx + 1) * batch_num 46 | io_start = np.digitize(batch_start, self.io_range) - 1 47 | io_end = np.digitize(batch_end, self.io_range) - 1 48 | cur_io_adj = (self.io_adj[io_start] + self.io_adj[io_end]) / 2 49 | loss, mape, rmse = self.train(x, y_real, cur_io_adj) 50 | loss_inner_epoch.append(loss) 51 | mape_inner_epoch.append(mape) 52 | rmse_inner_epoch.append(rmse) 53 | 54 | return np.array(loss_inner_epoch).mean(), np.array(mape_inner_epoch).mean(), \ 55 | np.array(rmse_inner_epoch).mean() 56 | 57 | def eval(self, x, y_real, cur_io_adj): 58 | output, cur_io_adj = self.model((x, cur_io_adj)) 59 | y_pred = self.scaler.inverse_transform(output) 60 | 61 | loss = self.theta * self.criterion(y_pred[..., 0], y_real[..., 0], 0.0) + \ 62 | (1 - self.theta) * self.criterion(y_pred[..., 1], y_real[..., 1], 0.0) 63 | 64 | mape = masked_mape(y_pred, y_real, 0.0).item() 65 | rmse = masked_rmse(y_pred, y_real, 0.0).item() 66 | return loss.item(), mape, rmse 67 | 68 | def eval_one_epoch(self, val_loader, cur_io_adj): 69 | self.model.eval() 70 | loss_inner_epoch = [] 71 | mape_inner_epoch = [] 72 | rmse_inner_epoch = [] 73 | for batch_idx, (x, y_real) in enumerate(val_loader): 74 | loss, mape, rmse = self.eval(x, y_real, cur_io_adj) 75 | loss_inner_epoch.append(loss) 76 | mape_inner_epoch.append(mape) 77 | rmse_inner_epoch.append(rmse) 78 | 79 | return np.array(loss_inner_epoch).mean(), np.array(mape_inner_epoch).mean(), \ 80 | np.array(rmse_inner_epoch).mean() 81 | 82 | def test(self, test_loader, cur_io_adj): 83 | loss_inner_epoch = [] 84 | mape_inner_epoch = [] 85 | rmse_inner_epoch = [] 86 | with torch.no_grad(): 87 | for batch_idx, (x, y_real) in enumerate(test_loader): 88 | loss, mape, rmse = self.eval(x, y_real, cur_io_adj) 89 | loss_inner_epoch.append(loss) 90 | mape_inner_epoch.append(mape) 91 | rmse_inner_epoch.append(rmse) 92 | 93 | return np.array(loss_inner_epoch).mean(), np.array(mape_inner_epoch).mean(), \ 94 | np.array(rmse_inner_epoch).mean() 95 | -------------------------------------------------------------------------------- /main/main_lstm.py: -------------------------------------------------------------------------------- 1 | from utils import ProcessData, StandardScalar 2 | from models.lstm import LSTM 3 | import yaml 4 | import argparse 5 | import numpy as np 6 | import torch 7 | import torch.optim as optim 8 | from trainer.trainer_lstm import trainer 9 | from torch.utils.data import DataLoader, TensorDataset 10 | 11 | 12 | def load_config(config_path): 13 | with open(config_path, 'r', encoding='utf-8') as f: 14 | config = yaml.safe_load(f) 15 | return config 16 | 17 | 18 | def main(config): 19 | # ------------------------ Data loading ------------------------ 20 | data_config = config['dataset'] 21 | optim_config = config['optimize'] 22 | trainer_config = config['trainer'] 23 | feature_matrix = np.load(data_config['feature_matrix_data_path']) 24 | # shape -> (number of samples, sequence length, number of vertexes, features) 25 | train_data, train_labels, val_data, val_labels, test_data, test_labels = ProcessData(feature_matrix, 26 | data_config['input_seq'], 27 | data_config['output_seq'], 28 | data_config['split_rate']) 29 | # data scalar 30 | device = data_config['device'] 31 | scalar = StandardScalar(train_data, device, axis=(0, 1, 2)) 32 | data_x_sets = [train_data, val_data, test_data] 33 | data_y_sets = [train_labels, val_labels, test_labels] 34 | data_x_normed_sets = [] 35 | data_y_real_sets = [] 36 | 37 | for data in data_x_sets: 38 | data_normed = scalar.transform(data) 39 | data_normed = data_normed.transpose(0, 2, 1, 3) 40 | data_normed = data_normed.reshape(-1, data_normed.shape[2], data_normed.shape[3]) 41 | data_normed = torch.Tensor(data_normed).to(device) 42 | data_x_normed_sets.append(data_normed) 43 | train_x, val_x, test_x = data_x_normed_sets 44 | 45 | for data in data_y_sets: 46 | data = data.transpose(0, 2, 1, 3) 47 | data = data.reshape(-1, data.shape[2], data.shape[3]) 48 | data_real = torch.Tensor(data).to(device) 49 | data_y_real_sets.append(data_real) 50 | train_real_y, val_real_y, test_real_y = data_y_real_sets 51 | 52 | data_final_list = [train_x, train_real_y, val_x, val_real_y, test_x, test_real_y] 53 | 54 | # get dataloader 55 | data_loaders = [] 56 | for i in range(0, len(data_final_list), 2): 57 | dataset = TensorDataset(data_final_list[i], data_final_list[i + 1]) 58 | data_loader = DataLoader(dataset, batch_size=data_config['batch_size']) 59 | data_loaders.append(data_loader) 60 | 61 | # ------------------------ Model setting ------------------------ 62 | 63 | model = LSTM(trainer_config['in_dim'], trainer_config['hidden_dim'], trainer_config['out_dim']) 64 | model.to(device) 65 | 66 | if optim_config['name'] == 'Adam': 67 | optimizer = optim.Adam(model.parameters(), 68 | lr=optim_config['learning_rate'], 69 | weight_decay=optim_config['weight_decay']) 70 | 71 | elif optim_config['name'] == 'AdamW': 72 | optimizer = optim.AdamW(model.parameters(), 73 | lr=optim_config['learning_rate'], 74 | weight_decay=optim_config['weight_decay']) 75 | 76 | elif optim_config['name'] == 'RMSprop': 77 | optimizer = optim.RMSprop(model.parameters(), 78 | lr=optim_config['learning_rate'], 79 | weight_decay=optim_config['weight_decay']) 80 | else: 81 | raise NotImplementedError(f'ERROR: The optimizer {optim_config["name"]} is not implemented.') 82 | 83 | engine = trainer(model, optimizer, scalar, trainer_config['theta']) 84 | # --------------------------- Train/Validation ------------------------- 85 | print('Training...') 86 | train_loss_epochs = [] 87 | train_mape_epochs = [] 88 | train_rmse_epochs = [] 89 | val_loss_epochs = [] 90 | val_mape_epochs = [] 91 | val_rmse_epochs = [] 92 | for epoch in range(trainer_config['epoch']): 93 | loss, mape, rmse = engine.train_one_epoch(data_loaders[0]) 94 | train_loss_epochs.append(loss) 95 | train_mape_epochs.append(mape) 96 | train_rmse_epochs.append(rmse) 97 | 98 | print(f'epoch:{epoch}, train_loss(MAE):{train_loss_epochs[epoch]}, ' 99 | f'train_MAPE:{train_mape_epochs[epoch]}, train_RMSE:{train_rmse_epochs[epoch]}') 100 | 101 | loss, mape, rmse = engine.eval_one_epoch(data_loaders[1]) 102 | val_loss_epochs.append(loss) 103 | val_mape_epochs.append(mape) 104 | val_rmse_epochs.append(rmse) 105 | 106 | print(f'val_loss(MAE):{val_loss_epochs[epoch]}, ' 107 | f'val_MAPE:{val_mape_epochs[epoch]}, val_RMSE:{val_rmse_epochs[epoch]}') 108 | 109 | torch.save(model.state_dict(), trainer_config['model_path'] + '/' + 'lstm_pretrained' + '.pth') 110 | print('Model saved!') 111 | 112 | # --------------------------- Test ------------------------- 113 | print('Testing...') 114 | test_loss, test_mape, test_rmse = engine.test(data_loaders[2]) 115 | print(f'test_loss(MAE):{test_loss}, test_mape:{test_mape}, test_rmse:{test_rmse}') 116 | 117 | 118 | if __name__ == '__main__': 119 | parser = argparse.ArgumentParser() 120 | parser.add_argument('--config_path', type=str, default='../config/config_lstm.yaml', 121 | help='Config path') 122 | args = parser.parse_args() 123 | configs = load_config(args.config_path) 124 | main(configs) 125 | -------------------------------------------------------------------------------- /main/main_astgcn.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import argparse 3 | import numpy as np 4 | import pandas as pd 5 | import torch 6 | import torch.optim as optim 7 | from models.ASTGCN_r import make_model 8 | from utils import ProcessData, StandardScalar 9 | from torch.utils.data import DataLoader, TensorDataset 10 | from trainer.trainer_astgcn import trainer 11 | 12 | 13 | def load_config(config_path): 14 | with open(config_path, 'r', encoding='utf-8') as f: 15 | config = yaml.safe_load(f) 16 | return config 17 | 18 | 19 | def main(config): 20 | # ------------------------ Data loading ------------------------ 21 | data_config = config['dataset'] 22 | optim_config = config['optimize'] 23 | trainer_config = config['trainer'] 24 | feature_matrix = np.load(data_config['feature_matrix_data_path']) 25 | samples = feature_matrix.shape[0] 26 | # shape -> (number of samples, sequence length, number of vertexes, features) 27 | train_data, train_labels, val_data, val_labels, test_data, test_labels = ProcessData(feature_matrix, 28 | data_config['input_seq'], 29 | data_config['output_seq'], 30 | data_config['split_rate']) 31 | # data scalar 32 | device = data_config['device'] 33 | scalar = StandardScalar(train_data, device, axis=(0, 1, 2)) 34 | data_x_sets = [train_data, val_data, test_data] 35 | data_y_sets = [train_labels, val_labels, test_labels] 36 | data_x_normed_sets = [] 37 | data_y_real_sets = [] 38 | for data in data_x_sets: 39 | data_normed = scalar.transform(data) 40 | data_normed = np.einsum('btnf->bnft', data_normed) 41 | data_normed = torch.Tensor(data_normed).to(device) 42 | data_x_normed_sets.append(data_normed) 43 | train_x, val_x, test_x = data_x_normed_sets 44 | for data in data_y_sets: 45 | data_real = torch.Tensor(data).to(device) 46 | data_y_real_sets.append(data_real) 47 | train_real_y, val_real_y, test_real_y = data_y_real_sets 48 | data_final_list = [train_x, train_real_y, val_x, val_real_y, test_x, test_real_y] 49 | 50 | # get dataloader 51 | data_loaders = [] 52 | for i in range(0, len(data_final_list), 2): 53 | dataset = TensorDataset(data_final_list[i], data_final_list[i + 1]) 54 | data_loader = DataLoader(dataset, batch_size=data_config['batch_size']) 55 | data_loaders.append(data_loader) 56 | 57 | # get adjacency matrix 58 | origin_adj = pd.read_csv(data_config['adj_data_path'], header=None) 59 | origin_adj = torch.Tensor(origin_adj.values).to(device) 60 | 61 | # ------------------------ Model setting ------------------------ 62 | 63 | model = make_model(data_config['device'], trainer_config['nb_block'], trainer_config['in_channels'], 64 | trainer_config['K'], trainer_config['nb_chev_filter'], trainer_config['nb_time_filter'], 65 | trainer_config['num_of_hours'], origin_adj, data_config['output_seq'], 66 | data_config['input_seq'], data_config['num_of_vertices']) 67 | model.to(device) 68 | 69 | if optim_config['name'] == 'Adam': 70 | optimizer = optim.Adam(model.parameters(), 71 | lr=optim_config['learning_rate'], 72 | weight_decay=optim_config['weight_decay']) 73 | 74 | elif optim_config['name'] == 'AdamW': 75 | optimizer = optim.AdamW(model.parameters(), 76 | lr=optim_config['learning_rate'], 77 | weight_decay=optim_config['weight_decay']) 78 | 79 | elif optim_config['name'] == 'RMSprop': 80 | optimizer = optim.RMSprop(model.parameters(), 81 | lr=optim_config['learning_rate'], 82 | weight_decay=optim_config['weight_decay']) 83 | else: 84 | raise NotImplementedError(f'ERROR: The optimizer {optim_config["name"]} is not implemented.') 85 | 86 | engine = trainer(model, optimizer, scalar, trainer_config['theta']) 87 | # --------------------------- Train/Validation ------------------------- 88 | print('Training...') 89 | train_loss_epochs = [] 90 | train_mape_epochs = [] 91 | train_rmse_epochs = [] 92 | val_loss_epochs = [] 93 | val_mape_epochs = [] 94 | val_rmse_epochs = [] 95 | for epoch in range(trainer_config['epoch']): 96 | loss, mape, rmse = engine.train_one_epoch(data_loaders[0]) 97 | train_loss_epochs.append(loss) 98 | train_mape_epochs.append(mape) 99 | train_rmse_epochs.append(rmse) 100 | 101 | print(f'epoch:{epoch}, train_loss(MAE):{train_loss_epochs[epoch]}, ' 102 | f'train_MAPE:{train_mape_epochs[epoch]}, train_RMSE:{train_rmse_epochs[epoch]}') 103 | 104 | loss, mape, rmse = engine.eval_one_epoch(data_loaders[1]) 105 | val_loss_epochs.append(loss) 106 | val_mape_epochs.append(mape) 107 | val_rmse_epochs.append(rmse) 108 | 109 | print(f'val_loss(MAE):{val_loss_epochs[epoch]}, ' 110 | f'val_MAPE:{val_mape_epochs[epoch]}, val_RMSE:{val_rmse_epochs[epoch]}') 111 | 112 | torch.save(model.state_dict(), trainer_config['model_path'] + '/' + 'astgcn_pretrained' + '.pth') 113 | print('Model saved!') 114 | 115 | # --------------------------- Test ------------------------- 116 | print('Testing...') 117 | test_loss, test_mape, test_rmse = engine.test(data_loaders[2]) 118 | print(f'test_loss(MAE):{test_loss}, test_mape:{test_mape}, test_rmse:{test_rmse}') 119 | 120 | 121 | if __name__ == '__main__': 122 | parser = argparse.ArgumentParser() 123 | parser.add_argument('--config_path', type=str, default='../config/config_astgcn.yaml', 124 | help='Config path') 125 | args = parser.parse_args() 126 | configs = load_config(args.config_path) 127 | main(configs) 128 | -------------------------------------------------------------------------------- /main/main_graph_wavenet.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import argparse 3 | import numpy as np 4 | import pandas as pd 5 | import torch 6 | import torch.optim as optim 7 | from models.graph_wavenet import gwnet 8 | from utils import ProcessData, StandardScalar 9 | from torch.utils.data import DataLoader, TensorDataset 10 | from utils import masked_mae 11 | from trainer.trainer_graph_wavenet import trainer 12 | 13 | 14 | def load_config(config_path): 15 | with open(config_path, 'r', encoding='utf-8') as f: 16 | config = yaml.safe_load(f) 17 | return config 18 | 19 | 20 | def main(config): 21 | # ------------------------ Data loading ------------------------ 22 | data_config = config['dataset'] 23 | optim_config = config['optimize'] 24 | trainer_config = config['trainer'] 25 | feature_matrix = np.load(data_config['feature_matrix_data_path']) 26 | samples = feature_matrix.shape[0] 27 | # shape -> (number of samples, sequence length, number of vertexes, features) 28 | train_data, train_labels, val_data, val_labels, test_data, test_labels = ProcessData(feature_matrix, 29 | data_config['input_seq'], 30 | data_config['output_seq'], 31 | data_config['split_rate']) 32 | # data scalar 33 | device = data_config['device'] 34 | scalar = StandardScalar(train_data, device, axis=(0, 1, 2)) 35 | data_x_sets = [train_data, val_data, test_data] 36 | data_y_sets = [train_labels, val_labels, test_labels] 37 | data_x_normed_sets = [] 38 | data_y_real_sets = [] 39 | for data in data_x_sets: 40 | data_normed = scalar.transform(data) 41 | data_normed = np.einsum('btnf->bfnt', data_normed) 42 | data_normed = torch.Tensor(data_normed).to(device) 43 | data_x_normed_sets.append(data_normed) 44 | train_x, val_x, test_x = data_x_normed_sets 45 | for data in data_y_sets: 46 | data_real = torch.Tensor(data).to(device) 47 | data_y_real_sets.append(data_real) 48 | train_real_y, val_real_y, test_real_y = data_y_real_sets 49 | data_final_list = [train_x, train_real_y, val_x, val_real_y, test_x, test_real_y] 50 | 51 | # get dataloader 52 | data_loaders = [] 53 | for i in range(0, len(data_final_list), 2): 54 | dataset = TensorDataset(data_final_list[i], data_final_list[i + 1]) 55 | data_loader = DataLoader(dataset, batch_size=data_config['batch_size']) 56 | data_loaders.append(data_loader) 57 | 58 | # get adjacency matrix 59 | origin_adj = pd.read_csv(data_config['adj_data_path'], header=None) 60 | origin_adj = torch.Tensor(origin_adj.values).to(device) 61 | 62 | # ------------------------ Model setting ------------------------ 63 | supports = [origin_adj] 64 | if trainer_config['randomadj']: 65 | adjinit = None 66 | else: 67 | adjinit = supports[0] 68 | if trainer_config['aptonly']: 69 | supports = None 70 | 71 | model = gwnet(device, data_config['num_of_vertices'], trainer_config['dropout'], supports, 72 | trainer_config['gcn_bool'], trainer_config['addaptadj'], adjinit, 73 | trainer_config['in_dim'], trainer_config['out_dim'], trainer_config['nhid'], 74 | trainer_config['nhid'], trainer_config['nhid'] * 8, trainer_config['nhid'] * 16) 75 | model.to(device) 76 | 77 | if optim_config['name'] == 'Adam': 78 | optimizer = optim.Adam(model.parameters(), 79 | lr=optim_config['learning_rate'], 80 | weight_decay=optim_config['weight_decay']) 81 | 82 | elif optim_config['name'] == 'AdamW': 83 | optimizer = optim.AdamW(model.parameters(), 84 | lr=optim_config['learning_rate'], 85 | weight_decay=optim_config['weight_decay']) 86 | 87 | elif optim_config['name'] == 'RMSprop': 88 | optimizer = optim.RMSprop(model.parameters(), 89 | lr=optim_config['learning_rate'], 90 | weight_decay=optim_config['weight_decay']) 91 | else: 92 | raise NotImplementedError(f'ERROR: The optimizer {optim_config["name"]} is not implemented.') 93 | 94 | engine = trainer(model, optimizer, scalar, trainer_config['theta']) 95 | # --------------------------- Train/Validation ------------------------- 96 | print('Training...') 97 | train_loss_epochs = [] 98 | train_mape_epochs = [] 99 | train_rmse_epochs = [] 100 | val_loss_epochs = [] 101 | val_mape_epochs = [] 102 | val_rmse_epochs = [] 103 | for epoch in range(trainer_config['epoch']): 104 | loss, mape, rmse = engine.train_one_epoch(data_loaders[0]) 105 | train_loss_epochs.append(loss) 106 | train_mape_epochs.append(mape) 107 | train_rmse_epochs.append(rmse) 108 | 109 | print(f'epoch:{epoch}, train_loss(MAE):{train_loss_epochs[epoch]}, ' 110 | f'train_MAPE:{train_mape_epochs[epoch]}, train_RMSE:{train_rmse_epochs[epoch]}') 111 | 112 | loss, mape, rmse = engine.eval_one_epoch(data_loaders[1]) 113 | val_loss_epochs.append(loss) 114 | val_mape_epochs.append(mape) 115 | val_rmse_epochs.append(rmse) 116 | 117 | print(f'val_loss(MAE):{val_loss_epochs[epoch]}, ' 118 | f'val_MAPE:{val_mape_epochs[epoch]}, val_RMSE:{val_rmse_epochs[epoch]}') 119 | 120 | torch.save(model.state_dict(), trainer_config['model_path'] + '/' + 'gwnet_pretrained' + '.pth') 121 | print('Model saved!') 122 | 123 | # --------------------------- Test ------------------------- 124 | print('Testing...') 125 | test_loss, test_mape, test_rmse = engine.test(data_loaders[2]) 126 | print(f'test_loss(MAE):{test_loss}, test_mape:{test_mape}, test_rmse:{test_rmse}') 127 | 128 | 129 | if __name__ == '__main__': 130 | parser = argparse.ArgumentParser() 131 | parser.add_argument('--config_path', type=str, default='../config/config_graph_wavenet.yaml', 132 | help='Config path') 133 | args = parser.parse_args() 134 | configs = load_config(args.config_path) 135 | main(configs) 136 | -------------------------------------------------------------------------------- /main/main_stgcn.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import argparse 3 | import numpy as np 4 | import pandas as pd 5 | import torch 6 | import torch.optim as optim 7 | from models.stgcn.models import STGCNChebGraphConv 8 | from utils import ProcessData, StandardScalar 9 | from torch.utils.data import DataLoader, TensorDataset 10 | from utils import masked_mae, calculate_scaled_laplacian_torch 11 | from trainer.trainer_stgcn import trainer 12 | 13 | 14 | def load_config(config_path): 15 | with open(config_path, 'r', encoding='utf-8') as f: 16 | config = yaml.safe_load(f) 17 | return config 18 | 19 | 20 | def main(config): 21 | # ------------------------ Data loading ------------------------ 22 | data_config = config['dataset'] 23 | optim_config = config['optimize'] 24 | trainer_config = config['trainer'] 25 | feature_matrix = np.load(data_config['feature_matrix_data_path']) 26 | samples = feature_matrix.shape[0] 27 | # shape -> (number of samples, sequence length, number of vertexes, features) 28 | train_data, train_labels, val_data, val_labels, test_data, test_labels = ProcessData(feature_matrix, 29 | data_config['input_seq'], 30 | data_config['output_seq'], 31 | data_config['split_rate']) 32 | # data scalar 33 | device = data_config['device'] 34 | scalar = StandardScalar(train_data, device, axis=(0, 1, 2)) 35 | data_x_sets = [train_data, val_data, test_data] 36 | data_y_sets = [train_labels, val_labels, test_labels] 37 | data_x_normed_sets = [] 38 | data_y_real_sets = [] 39 | for data in data_x_sets: 40 | data_normed = scalar.transform(data) 41 | data_normed = np.einsum('btnf->bftn', data_normed) 42 | data_normed = torch.Tensor(data_normed).to(device) 43 | data_x_normed_sets.append(data_normed) 44 | train_x, val_x, test_x = data_x_normed_sets 45 | for data in data_y_sets: 46 | data_real = torch.Tensor(data).to(device) 47 | data_y_real_sets.append(data_real) 48 | train_real_y, val_real_y, test_real_y = data_y_real_sets 49 | data_final_list = [train_x, train_real_y, val_x, val_real_y, test_x, test_real_y] 50 | 51 | # get dataloader 52 | data_loaders = [] 53 | for i in range(0, len(data_final_list), 2): 54 | dataset = TensorDataset(data_final_list[i], data_final_list[i + 1]) 55 | data_loader = DataLoader(dataset, batch_size=data_config['batch_size']) 56 | data_loaders.append(data_loader) 57 | 58 | # get adjacency matrix 59 | origin_adj = pd.read_csv(data_config['adj_data_path'], header=None) 60 | origin_adj = torch.Tensor(origin_adj.values).to(device) 61 | # ------------------------ Model setting ------------------------ 62 | 63 | Ko = trainer_config['n_his'] - (trainer_config['Kt'] - 1) * 2 * trainer_config['stblock_num'] 64 | blocks = [[trainer_config['channel']]] 65 | for l in range(trainer_config['stblock_num']): 66 | blocks.append([64, 16, 64]) 67 | if Ko == 0: 68 | blocks.append([128]) 69 | elif Ko > 0: 70 | blocks.append([128, 128]) 71 | blocks.append([trainer_config['channel']]) 72 | 73 | gso = calculate_scaled_laplacian_torch(origin_adj) 74 | 75 | model = STGCNChebGraphConv(blocks, data_config['num_of_vertices'], trainer_config['Kt'], trainer_config['Ks'], 76 | trainer_config['act_func'], trainer_config['graph_conv_type'], gso 77 | ,trainer_config['enable_bias'], trainer_config['dropout'], trainer_config['n_his']) 78 | model.to(device) 79 | 80 | if optim_config['name'] == 'Adam': 81 | optimizer = optim.Adam(model.parameters(), 82 | lr=optim_config['learning_rate'], 83 | weight_decay=optim_config['weight_decay']) 84 | 85 | elif optim_config['name'] == 'AdamW': 86 | optimizer = optim.AdamW(model.parameters(), 87 | lr=optim_config['learning_rate'], 88 | weight_decay=optim_config['weight_decay']) 89 | 90 | elif optim_config['name'] == 'RMSprop': 91 | optimizer = optim.RMSprop(model.parameters(), 92 | lr=optim_config['learning_rate'], 93 | weight_decay=optim_config['weight_decay']) 94 | else: 95 | raise NotImplementedError(f'ERROR: The optimizer {optim_config["name"]} is not implemented.') 96 | 97 | engine = trainer(model, optimizer, scalar, trainer_config['theta']) 98 | # --------------------------- Train/Validation ------------------------- 99 | print('Training...') 100 | train_loss_epochs = [] 101 | train_mape_epochs = [] 102 | train_rmse_epochs = [] 103 | val_loss_epochs = [] 104 | val_mape_epochs = [] 105 | val_rmse_epochs = [] 106 | for epoch in range(trainer_config['epoch']): 107 | loss, mape, rmse = engine.train_one_epoch(data_loaders[0]) 108 | train_loss_epochs.append(loss) 109 | train_mape_epochs.append(mape) 110 | train_rmse_epochs.append(rmse) 111 | 112 | print(f'epoch:{epoch}, train_loss(MAE):{train_loss_epochs[epoch]}, ' 113 | f'train_MAPE:{train_mape_epochs[epoch]}, train_RMSE:{train_rmse_epochs[epoch]}') 114 | 115 | loss, mape, rmse = engine.eval_one_epoch(data_loaders[1]) 116 | val_loss_epochs.append(loss) 117 | val_mape_epochs.append(mape) 118 | val_rmse_epochs.append(rmse) 119 | 120 | print(f'val_loss(MAE):{val_loss_epochs[epoch]}, ' 121 | f'val_MAPE:{val_mape_epochs[epoch]}, val_RMSE:{val_rmse_epochs[epoch]}') 122 | 123 | torch.save(model.state_dict(), trainer_config['model_path'] + '/' + 'stgcn_pretrained' + '.pth') 124 | print('Model saved!') 125 | 126 | # --------------------------- Test ------------------------- 127 | print('Testing...') 128 | test_loss, test_mape, test_rmse = engine.test(data_loaders[2]) 129 | print(f'test_loss(MAE):{test_loss}, test_mape:{test_mape}, test_rmse:{test_rmse}') 130 | 131 | if __name__ == '__main__': 132 | parser = argparse.ArgumentParser() 133 | parser.add_argument('--config_path', type=str, default='../config/config_stgcn.yaml', 134 | help='Config path') 135 | args = parser.parse_args() 136 | configs = load_config(args.config_path) 137 | main(configs) 138 | -------------------------------------------------------------------------------- /main/main_ma2gcn.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import argparse 3 | import numpy as np 4 | import pandas as pd 5 | import torch 6 | import torch.optim as optim 7 | from torch.utils.data import DataLoader, TensorDataset 8 | from models.ma2gcn import MA2GCN 9 | from trainer.trainer_ma2gcn import trainer 10 | from utils import ProcessData, TransAdj, StandardScalar 11 | 12 | 13 | def load_config(config_path): 14 | with open(config_path, 'r', encoding='utf-8') as f: 15 | config = yaml.safe_load(f) 16 | return config 17 | 18 | 19 | def main(config): 20 | # ------------------------ Data loading ------------------------ 21 | data_config = config['dataset'] 22 | optim_config = config['optimize'] 23 | trainer_config = config['trainer'] 24 | feature_matrix = np.load(data_config['feature_matrix_data_path']) 25 | samples = feature_matrix.shape[0] 26 | # shape -> (number of samples, sequence length, number of vertexes, features) 27 | train_data, train_labels, val_data, val_labels, test_data, test_labels = ProcessData(feature_matrix, 28 | data_config['input_seq'], 29 | data_config['output_seq'], 30 | data_config['split_rate']) 31 | # data scalar 32 | device = data_config['device'] 33 | scalar = StandardScalar(train_data, device, axis=(0, 1, 2)) 34 | data_x_sets = [train_data, val_data, test_data] 35 | data_y_sets = [train_labels, val_labels, test_labels] 36 | data_x_normed_sets = [] 37 | data_y_real_sets = [] 38 | for data in data_x_sets: 39 | data_normed = scalar.transform(data) 40 | data_normed = torch.Tensor(data_normed).to(device) 41 | data_x_normed_sets.append(data_normed) 42 | train_x, val_x, test_x = data_x_normed_sets 43 | for data in data_y_sets: 44 | data_real = torch.Tensor(data).to(device) 45 | data_y_real_sets.append(data_real) 46 | train_real_y, val_real_y, test_real_y = data_y_real_sets 47 | data_final_list = [train_x, train_real_y, val_x, val_real_y, test_x, test_real_y] 48 | 49 | # get dataloader 50 | data_loaders = [] 51 | for i in range(0, len(data_final_list), 2): 52 | dataset = TensorDataset(data_final_list[i], data_final_list[i + 1]) 53 | data_loader = DataLoader(dataset, batch_size=data_config['batch_size']) 54 | data_loaders.append(data_loader) 55 | 56 | # get adjacency matrix 57 | origin_adj = pd.read_csv(data_config['origin_adj_data_path'], header=None) 58 | origin_adj = torch.Tensor(origin_adj.values).to(device) 59 | io_matrix = np.load(data_config['io_adj_data_path']) 60 | io_adj = torch.Tensor(TransAdj(io_matrix)).to(device) 61 | 62 | # process io adj 63 | io_bound = data_config['io_matrix_interval'] * 60 / data_config['basic_interval'] 64 | io_range = np.array(range(0, samples + 1, int(io_bound))) 65 | 66 | # get cur_io_adj for val and test 67 | val_start = train_data.shape[0] 68 | val_cur_io_adj = io_adj[np.digitize(val_start, io_range) - 2] 69 | test_start = val_start + val_data.shape[0] 70 | test_cur_io_adj = io_adj[np.digitize(test_start, io_range) - 2] 71 | 72 | # ------------------------ Model parameters getting ------------------------ 73 | blocks_num = trainer_config['block_nums'] 74 | kernel_size = trainer_config['kernel_size'] 75 | in_tcn_dim_list = trainer_config['in_tcn_dim_list'] 76 | adj_input_dim = origin_adj.shape[0] 77 | _, input_seq, nodes_num, features = train_x.shape 78 | # [Start]--get receptive field, dilation list, all_feature_dim, input_seq(padded) 79 | cur_dilation_list = [0] * blocks_num 80 | all_feature_dim = [0] * blocks_num 81 | cur_dilation_list[0] = 1 82 | receptive_field = 1 83 | additional_scope = kernel_size[1] - 1 84 | 85 | for i in range(blocks_num): 86 | receptive_field = receptive_field + additional_scope 87 | additional_scope *= 2 88 | 89 | new_input_seq = receptive_field 90 | all_feature_dim[0] = new_input_seq * features 91 | 92 | for j in range(1, blocks_num): 93 | cur_dilation_list[j] = 2 * cur_dilation_list[j - 1] 94 | all_feature_dim[j] = in_tcn_dim_list[j] * ( 95 | new_input_seq - cur_dilation_list[j - 1] * (kernel_size[1] - 1)) 96 | new_input_seq = new_input_seq - cur_dilation_list[j - 1] * (kernel_size[1] - 1) 97 | # [End]--get receptive field, dilation list, all_feature_dim, new_input_seq(padded) 98 | 99 | # ------------------------ Model setting ------------------------ 100 | model = MA2GCN(trainer_config['block_nums'], trainer_config['K'], adj_input_dim, trainer_config['adj_hidden_dim'], 101 | origin_adj, trainer_config['in_channel_list'], trainer_config['out_channel_list'], 102 | trainer_config['in_tcn_dim_list'], trainer_config['out_tcn_dim_list'], 103 | cur_dilation_list, trainer_config['kernel_size'], nodes_num, all_feature_dim, 104 | receptive_field, trainer_config['drop_rate'], device) 105 | model.to(device) 106 | 107 | if optim_config['name'] == 'Adam': 108 | optimizer = optim.Adam(model.parameters(), 109 | lr=optim_config['learning_rate'], 110 | weight_decay=optim_config['weight_decay']) 111 | 112 | elif optim_config['name'] == 'AdamW': 113 | optimizer = optim.AdamW(model.parameters(), 114 | lr=optim_config['learning_rate'], 115 | weight_decay=optim_config['weight_decay']) 116 | 117 | elif optim_config['name'] == 'RMSprop': 118 | optimizer = optim.RMSprop(model.parameters(), 119 | lr=optim_config['learning_rate'], 120 | weight_decay=optim_config['weight_decay']) 121 | else: 122 | raise NotImplementedError(f'ERROR: The optimizer {optim_config["name"]} is not implemented.') 123 | 124 | engine = trainer(model, optimizer, scalar, trainer_config['theta'], io_range, io_adj) 125 | # --------------------------- Train/Validation ------------------------- 126 | print('Training...') 127 | train_loss_epochs = [] 128 | train_mape_epochs = [] 129 | train_rmse_epochs = [] 130 | val_loss_epochs = [] 131 | val_mape_epochs = [] 132 | val_rmse_epochs = [] 133 | for epoch in range(trainer_config['epoch']): 134 | loss, mape, rmse = engine.train_one_epoch(data_loaders[0]) 135 | train_loss_epochs.append(loss) 136 | train_mape_epochs.append(mape) 137 | train_rmse_epochs.append(rmse) 138 | 139 | print(f'epoch:{epoch}, train_loss(MAE):{train_loss_epochs[epoch]}, ' 140 | f'train_MAPE:{train_mape_epochs[epoch]}, train_RMSE:{train_rmse_epochs[epoch]}') 141 | 142 | loss, mape, rmse = engine.eval_one_epoch(data_loaders[1], val_cur_io_adj) 143 | val_loss_epochs.append(loss) 144 | val_mape_epochs.append(mape) 145 | val_rmse_epochs.append(rmse) 146 | 147 | print(f'val_loss(MAE):{val_loss_epochs[epoch]}, ' 148 | f'val_MAPE:{val_mape_epochs[epoch]}, val_RMSE:{val_rmse_epochs[epoch]}') 149 | 150 | torch.save(model.state_dict(), trainer_config['model_path'] + '/' + 'ma2gcn_pretrained' + '.pth') 151 | print('Model saved!') 152 | 153 | # --------------------------- Test ------------------------- 154 | print('Testing...') 155 | test_loss, test_mape, test_rmse = engine.test(data_loaders[2], test_cur_io_adj) 156 | print(f'test_loss(MAE):{test_loss}, test_mape:{test_mape}, test_rmse:{test_rmse}') 157 | 158 | 159 | if __name__ == '__main__': 160 | parser = argparse.ArgumentParser() 161 | parser.add_argument('--config_path', type=str, default='../config/config_ma2gcn.yaml', 162 | help='Config path') 163 | args = parser.parse_args() 164 | configs = load_config(args.config_path) 165 | main(configs) 166 | -------------------------------------------------------------------------------- /models/graph_wavenet.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.nn.functional as F 4 | 5 | 6 | class nconv(nn.Module): 7 | def __init__(self): 8 | super(nconv, self).__init__() 9 | 10 | def forward(self, x, A): 11 | x = torch.einsum('ncvl,vw->ncwl', (x, A)) 12 | return x.contiguous() 13 | 14 | 15 | class linear(nn.Module): 16 | def __init__(self, c_in, c_out): 17 | super(linear, self).__init__() 18 | self.mlp = torch.nn.Conv2d(c_in, c_out, kernel_size=(1, 1), padding=(0, 0), stride=(1, 1), bias=True) 19 | 20 | def forward(self, x): 21 | return self.mlp(x) 22 | 23 | 24 | class gcn(nn.Module): 25 | def __init__(self, c_in, c_out, dropout, support_len=3, order=2): 26 | super(gcn, self).__init__() 27 | self.nconv = nconv() 28 | c_in = (order * support_len + 1) * c_in 29 | self.mlp = linear(c_in, c_out) 30 | self.dropout = dropout 31 | self.order = order 32 | 33 | def forward(self, x, support): 34 | out = [x] 35 | for a in support: 36 | x1 = self.nconv(x, a) 37 | out.append(x1) 38 | for k in range(2, self.order + 1): 39 | x2 = self.nconv(x1, a) 40 | out.append(x2) 41 | x1 = x2 42 | 43 | h = torch.cat(out, dim=1) 44 | h = self.mlp(h) 45 | h = F.dropout(h, self.dropout, training=self.training) 46 | return h 47 | 48 | 49 | class gwnet(nn.Module): 50 | def __init__(self, device, num_nodes, dropout=0.3, supports=None, gcn_bool=True, addaptadj=True, 51 | aptinit=None, in_dim=2, out_dim=12, residual_channels=32, dilation_channels=32, 52 | skip_channels=256, end_channels=512, kernel_size=2, blocks=4, layers=2): 53 | super(gwnet, self).__init__() 54 | self.dropout = dropout 55 | self.blocks = blocks 56 | self.layers = layers 57 | self.gcn_bool = gcn_bool 58 | self.addaptadj = addaptadj 59 | 60 | self.filter_convs = nn.ModuleList() 61 | self.gate_convs = nn.ModuleList() 62 | self.residual_convs = nn.ModuleList() 63 | self.skip_convs = nn.ModuleList() 64 | self.bn = nn.ModuleList() 65 | self.gconv = nn.ModuleList() 66 | 67 | self.start_conv = nn.Conv2d(in_channels=in_dim, 68 | out_channels=residual_channels, 69 | kernel_size=(1, 1)) 70 | self.supports = supports 71 | 72 | receptive_field = 1 73 | 74 | self.supports_len = 0 75 | if supports is not None: 76 | self.supports_len += len(supports) 77 | 78 | if gcn_bool and addaptadj: 79 | if aptinit is None: 80 | if supports is None: 81 | self.supports = [] 82 | self.nodevec1 = nn.Parameter(torch.randn(num_nodes, 10).to(device), requires_grad=True).to(device) 83 | self.nodevec2 = nn.Parameter(torch.randn(10, num_nodes).to(device), requires_grad=True).to(device) 84 | self.supports_len += 1 85 | else: 86 | if supports is None: 87 | self.supports = [] 88 | m, p, n = torch.svd(aptinit) 89 | initemb1 = torch.mm(m[:, :10], torch.diag(p[:10] ** 0.5)) 90 | initemb2 = torch.mm(torch.diag(p[:10] ** 0.5), n[:, :10].t()) 91 | self.nodevec1 = nn.Parameter(initemb1, requires_grad=True).to(device) 92 | self.nodevec2 = nn.Parameter(initemb2, requires_grad=True).to(device) 93 | self.supports_len += 1 94 | 95 | for b in range(blocks): 96 | additional_scope = kernel_size - 1 97 | new_dilation = 1 98 | for i in range(layers): 99 | # dilated convolutions 100 | self.filter_convs.append(nn.Conv2d(in_channels=residual_channels, 101 | out_channels=dilation_channels, 102 | kernel_size=(1, kernel_size), dilation=new_dilation)) 103 | 104 | self.gate_convs.append(nn.Conv2d(in_channels=residual_channels, 105 | out_channels=dilation_channels, 106 | kernel_size=(1, kernel_size), dilation=new_dilation)) 107 | 108 | # 1x1 convolution for residual connection 109 | self.residual_convs.append(nn.Conv2d(in_channels=dilation_channels, 110 | out_channels=residual_channels, 111 | kernel_size=(1, 1))) 112 | 113 | # 1x1 convolution for skip connection 114 | self.skip_convs.append(nn.Conv2d(in_channels=dilation_channels, 115 | out_channels=skip_channels, 116 | kernel_size=(1, 1))) 117 | 118 | self.bn.append(nn.BatchNorm2d(residual_channels)) 119 | new_dilation *= 2 120 | receptive_field += additional_scope 121 | additional_scope *= 2 122 | if self.gcn_bool: 123 | self.gconv.append(gcn(dilation_channels, residual_channels, dropout, support_len=self.supports_len)) 124 | 125 | self.end_conv_1 = nn.Conv2d(in_channels=skip_channels, 126 | out_channels=end_channels, 127 | kernel_size=(1, 1), 128 | bias=True) 129 | 130 | self.end_conv_2 = nn.Conv2d(in_channels=end_channels, 131 | out_channels=out_dim, 132 | kernel_size=(1, 1), 133 | bias=True) 134 | 135 | self.receptive_field = receptive_field + 1 136 | 137 | def forward(self, input): 138 | in_len = input.size(3) 139 | if in_len < self.receptive_field: 140 | x = nn.functional.pad(input, (self.receptive_field - in_len, 0, 0, 0)) 141 | else: 142 | x = input 143 | x = self.start_conv(x) 144 | skip = 0 145 | 146 | # calculate the current adaptive adj matrix once per iteration 147 | new_supports = None 148 | if self.gcn_bool and self.addaptadj and self.supports is not None: 149 | adp = F.softmax(F.relu(torch.mm(self.nodevec1, self.nodevec2)), dim=1) 150 | new_supports = self.supports + [adp] 151 | 152 | # WaveNet layers 153 | for i in range(self.blocks * self.layers): 154 | 155 | # |----------------------------------------| *residual* 156 | # | | 157 | # | |-- conv -- tanh --| | 158 | # -> dilate -|----| * ----|-- 1x1 -- + --> *input* 159 | # |-- conv -- sigm --| | 160 | # 1x1 161 | # | 162 | # ---------------------------------------> + -------------> *skip* 163 | 164 | residual = x 165 | # dilated convolution 166 | filter = self.filter_convs[i](residual) 167 | filter = torch.tanh(filter) 168 | gate = self.gate_convs[i](residual) 169 | gate = torch.sigmoid(gate) 170 | x = filter * gate 171 | 172 | # parametrized skip connection 173 | 174 | s = x 175 | s = self.skip_convs[i](s) 176 | try: 177 | skip = skip[:, :, :, -s.size(3):] 178 | except: 179 | skip = 0 180 | skip = s + skip 181 | 182 | if self.gcn_bool and self.supports is not None: 183 | if self.addaptadj: 184 | x = self.gconv[i](x, new_supports) 185 | else: 186 | x = self.gconv[i](x, self.supports) 187 | else: 188 | x = self.residual_convs[i](x) 189 | 190 | x = x + residual[:, :, :, -x.size(3):] 191 | 192 | x = self.bn[i](x) 193 | 194 | x = F.relu(skip) 195 | x = F.relu(self.end_conv_1(x)) 196 | x = self.end_conv_2(x) 197 | return x 198 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import torch 3 | from scipy.sparse.linalg import eigs 4 | 5 | 6 | # ------------------------ Process data ------------------------ 7 | 8 | def ProcessData(data, in_seq_length, out_seq_length, rates): 9 | """ 10 | :param data: raw data 11 | :param in_seq_length: the number of timestamps for prediction 12 | :param out_seq_length: the number of timestamps as labels 13 | :param rates: train/val/test 14 | :return: shape -> (number of samples, sequence length, number of vertexes, features) 15 | """ 16 | # Set parameters 17 | features = data 18 | input_seq_length = in_seq_length 19 | output_seq_length = out_seq_length 20 | train_rate = rates[0] 21 | val_rate = rates[1] 22 | 23 | assert 10 - train_rate - val_rate == rates[2] 24 | 25 | # Get time sequence length 26 | data_length = features.shape[0] 27 | 28 | # Get index of train/val/test set 29 | train_length = int(train_rate / 10 * (data_length - input_seq_length - output_seq_length)) 30 | val_length = int(val_rate / 10 * (data_length - input_seq_length - output_seq_length)) 31 | 32 | train_start_idx = 0 33 | train_end_idx = train_length + 1 34 | 35 | val_start_idx = train_end_idx 36 | val_end_idx = val_start_idx + val_length + 1 37 | 38 | test_start_idx = val_end_idx 39 | test_end_idx = data_length - input_seq_length - output_seq_length + 1 40 | 41 | # Split dataset 42 | def SplitData(start_idx, end_idx): 43 | inputs = [] 44 | labels = [] 45 | for i in range(start_idx, end_idx): 46 | inputs.append(features[i:i + input_seq_length]) 47 | labels.append(features[i + input_seq_length:i + input_seq_length + output_seq_length]) 48 | return np.array(inputs), np.array(labels) 49 | 50 | train_data, train_labels = SplitData(train_start_idx, train_end_idx) 51 | val_data, val_labels = SplitData(val_start_idx, val_end_idx) 52 | test_data, test_labels = SplitData(test_start_idx, test_end_idx) 53 | 54 | return train_data, train_labels, val_data, val_labels, test_data, test_labels 55 | 56 | 57 | def TransAdj(io_matrix): 58 | # Set parameter 59 | io_matrix = io_matrix 60 | 61 | # Calculate weights 62 | num_trains = io_matrix[:, :, :, 0] 63 | travel_times = io_matrix[:, :, :, 1] 64 | weights = np.divide(num_trains, travel_times, out=np.zeros_like(num_trains), where=travel_times != 0) 65 | 66 | # Get adj_matrix 67 | adj_matrix = np.zeros((io_matrix.shape[0], io_matrix.shape[1], io_matrix.shape[1])) 68 | mask = np.sum(io_matrix, axis=-1) != 0 69 | adj_matrix[mask] = weights[mask] 70 | 71 | return adj_matrix 72 | 73 | 74 | class StandardScalar: 75 | def __init__(self, data, device, axis=None): 76 | # Get mean, std, axis 77 | self.axis = axis 78 | self.mean = data.mean(axis=self.axis) 79 | self.std = data.std(axis=self.axis) 80 | self.device = device 81 | 82 | def transform(self, data): 83 | return (data - self.mean) / self.std 84 | 85 | def inverse_transform(self, data): 86 | self.std = torch.Tensor(self.std).to(self.device) 87 | self.mean = torch.Tensor(self.mean).to(self.device) 88 | return (data * self.std) + self.mean 89 | 90 | 91 | class StandardScalar_np: 92 | def __init__(self, data, axis=None): 93 | # Get mean, std, axis 94 | self.axis = axis 95 | self.mean = data.mean(axis=self.axis) 96 | self.std = data.std(axis=self.axis) 97 | 98 | def transform(self, data): 99 | return (data - self.mean) / self.std 100 | 101 | def inverse_transform(self, data): 102 | return (data * self.std) + self.mean 103 | 104 | 105 | # ------------------------ Model tools ------------------------ 106 | 107 | # Get L_tilde or L_sym-I or -D^-(1/2)*A*D^-(1/2) 108 | 109 | def calculate_scaled_laplacian_torch(adj): 110 | """ 111 | torch version 112 | """ 113 | D = torch.diag(torch.sum(adj, dim=1)) 114 | L = D - adj 115 | 116 | eigenvalues, _ = torch.linalg.eig(L) 117 | lambda_max = eigenvalues.real.max() 118 | 119 | return (2 * L) / lambda_max - torch.eye(adj.shape[0]).cuda() 120 | 121 | 122 | def calculate_scaled_laplacian_np(adj): 123 | """ 124 | numpy version 125 | """ 126 | # if adj is tensor(cuda) 127 | adj = adj.cpu() 128 | adj = adj.detach().numpy() 129 | adj = np.array(adj) 130 | 131 | D = np.diag(np.sum(adj, axis=1)) 132 | L = D - adj 133 | 134 | lambda_max = eigs(L, k=1, which='LR')[0].real 135 | 136 | return (2 * L) / lambda_max - np.identity(adj.shape[0]) 137 | 138 | 139 | # Get chebyshev polynomials 140 | def get_Tk(L_tilde, K): 141 | L_tilde = L_tilde.cpu() 142 | L_tilde = L_tilde.detach().numpy() 143 | L_tilde = np.array(L_tilde) 144 | 145 | T_ks = [] 146 | N = L_tilde.shape[0] 147 | T_ks.append(np.identity(N)) 148 | T_ks.append(L_tilde) 149 | 150 | for i in range(2, K): 151 | T_ks.append(2 * L_tilde * T_ks[i - 1] - T_ks[i - 2]) 152 | 153 | return T_ks 154 | 155 | 156 | # Get current io matrix 157 | def IoMatrixSelect(io_adj, past_hours): 158 | number_segments = io_adj.shape[0] 159 | base_hour_interval = int(24 / number_segments) 160 | past_hours = past_hours 161 | 162 | return io_adj[int(past_hours / base_hour_interval)] 163 | 164 | 165 | # ------------------------ Metrics ------------------------ 166 | # MASK MAE 167 | def masked_mae(preds, labels, null_val=np.nan): 168 | if np.isnan(null_val): 169 | mask = ~torch.isnan(labels) 170 | else: 171 | mask = (labels != null_val) 172 | mask = mask.float() 173 | mask /= torch.mean((mask)) 174 | mask = torch.where(torch.isnan(mask), torch.zeros_like(mask), mask) 175 | loss = torch.abs(preds - labels) 176 | loss = loss * mask 177 | loss = torch.where(torch.isnan(loss), torch.zeros_like(loss), loss) 178 | return torch.mean(loss) 179 | 180 | 181 | def masked_mae_np(preds, labels, null_val=np.nan): 182 | if np.isnan(null_val): 183 | mask = ~np.isnan(labels) 184 | else: 185 | mask = (labels != null_val) 186 | mask = mask.astype(float) 187 | mask /= np.mean(mask) 188 | mask = np.where(np.isnan(mask), np.zeros_like(mask), mask) 189 | loss = np.abs(preds - labels) 190 | loss = loss * mask 191 | loss = np.where(np.isnan(loss), np.zeros_like(loss), loss) 192 | return np.mean(loss) 193 | 194 | 195 | # MASK MAPE 196 | def masked_mape(preds, labels, null_val=np.nan): 197 | if np.isnan(null_val): 198 | mask = ~torch.isnan(labels) 199 | else: 200 | mask = (labels != null_val) 201 | mask = mask.float() 202 | mask /= torch.mean((mask)) 203 | mask = torch.where(torch.isnan(mask), torch.zeros_like(mask), mask) 204 | loss = torch.abs(preds - labels) / labels 205 | loss = loss * mask 206 | loss = torch.where(torch.isnan(loss), torch.zeros_like(loss), loss) 207 | return torch.mean(loss) 208 | 209 | 210 | def masked_mape_np(preds, labels, null_val=np.nan): 211 | if np.isnan(null_val): 212 | mask = ~np.isnan(labels) 213 | else: 214 | mask = (labels != null_val) 215 | mask = mask.astype(float) 216 | mask /= np.mean(mask) 217 | mask = np.where(np.isnan(mask), np.zeros_like(mask), mask) 218 | loss = np.abs(preds - labels) / labels 219 | loss = loss * mask 220 | loss = np.where(np.isnan(loss), np.zeros_like(loss), loss) 221 | return np.mean(loss) 222 | 223 | 224 | # MASK MSE/RMSE 225 | def masked_mse(preds, labels, null_val=np.nan): 226 | if np.isnan(null_val): 227 | mask = ~torch.isnan(labels) 228 | else: 229 | mask = (labels != null_val) 230 | mask = mask.float() 231 | mask /= torch.mean((mask)) 232 | mask = torch.where(torch.isnan(mask), torch.zeros_like(mask), mask) 233 | loss = (preds - labels) ** 2 234 | loss = loss * mask 235 | loss = torch.where(torch.isnan(loss), torch.zeros_like(loss), loss) 236 | return torch.mean(loss) 237 | 238 | 239 | def masked_rmse(preds, labels, null_val=np.nan): 240 | return torch.sqrt(masked_mse(preds=preds, labels=labels, null_val=null_val)) 241 | 242 | 243 | def masked_mse_np(preds, labels, null_val=np.nan): 244 | if np.isnan(null_val): 245 | mask = ~np.isnan(labels) 246 | else: 247 | mask = (labels != null_val) 248 | mask = mask.astype(float) 249 | mask /= np.mean(mask) 250 | mask = np.where(np.isnan(mask), np.zeros_like(mask), mask) 251 | loss = (preds - labels) ** 2 252 | loss = loss * mask 253 | loss = np.where(np.isnan(loss), np.zeros_like(loss), loss) 254 | return np.mean(loss) 255 | 256 | 257 | def masked_rmse_np(preds, labels, null_val=np.nan): 258 | return np.sqrt(masked_mse_np(preds=preds, labels=labels, null_val=null_val)) 259 | -------------------------------------------------------------------------------- /models/ma2gcn.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import torch 3 | import torch.nn as nn 4 | import torch.nn.functional as F 5 | from utils import calculate_scaled_laplacian_torch, get_Tk 6 | 7 | 8 | # ------------------------ Graph convolution ------------------------ 9 | 10 | class GraphConv(nn.Module): 11 | """ 12 | - GraphConv: 13 | - Params: in_channels*, out_channels*, device, K 14 | - Input: x*(b, t_in, n, c_in), Tks* 15 | - Output: x(b, t_in, n, c_out) 16 | """ 17 | 18 | def __init__(self, K, in_channels, out_channels, device): 19 | super(GraphConv, self).__init__() 20 | self.K = K 21 | self.device = device 22 | self.in_channels = in_channels 23 | self.out_channels = out_channels 24 | self.Theta = nn.ParameterList( 25 | [nn.Parameter(torch.FloatTensor(in_channels, out_channels)).to(device) for _ in range(K)] 26 | ) 27 | 28 | def forward(self, x, Tks): 29 | batch_size, seq_length, num_of_vertices, in_channels = x.shape 30 | output = torch.zeros(batch_size, seq_length, num_of_vertices, self.out_channels).to(self.device) 31 | 32 | for k in range(self.K): 33 | T_k = Tks[k] 34 | theta_k = self.Theta[k] 35 | 36 | temp = torch.einsum('vv,btvc->btvc', (T_k, x)) 37 | output = output + torch.einsum('btvc,co->btvo', (temp, theta_k)) 38 | 39 | return torch.Tensor(output) 40 | 41 | 42 | # ------------------------ Adjacency attention mechanism------------------------ 43 | 44 | class AdjAttention(nn.Module): 45 | """ 46 | - AdjAttention: 47 | - Params: adj_input_dim, adj_hidden_dim(For weights) 48 | - Input: adj_list*:[origin_adj(n, n), origin_adj_2(n, n), dynamic_adj(n, n), cur_io_adj(n, n)] 49 | - Output: adj_aggregated(n, n) 50 | """ 51 | 52 | def __init__(self, adj_input_dim, adj_hidden_dim): 53 | super(AdjAttention, self).__init__() 54 | 55 | self.W = nn.Linear(adj_input_dim, adj_hidden_dim) 56 | self.V = nn.Linear(adj_hidden_dim, 1) 57 | 58 | def forward(self, adj_list): 59 | # adj_list: a list of adjacency matrices with shape (num_nodes, num_nodes) 60 | num_nodes, _ = adj_list[0].shape 61 | 62 | # Compute weights for each adjacency matrix 63 | weights = [] 64 | for adj in adj_list: 65 | x = F.relu(self.W(adj)) 66 | x = self.V(x) 67 | x = x.view(num_nodes, 1) 68 | alpha = x.mean() 69 | weights.append(alpha) 70 | 71 | weights = F.softmax(torch.Tensor(weights), dim=0) 72 | # Compute weighted sum of adjacency matrices 73 | adj_aggregated = torch.zeros_like(adj_list[0]) 74 | for i in range(len(weights)): 75 | adj_aggregated += adj_list[i] * weights[i] 76 | 77 | return adj_aggregated 78 | 79 | 80 | # ------------------------ Gated TCN module ------------------------ 81 | 82 | class GatedTCN(nn.Module): 83 | """ 84 | - GatedTCN: 85 | - Params: in_tcn_dim, out_tcn_dim, cur_dilation, kernel_size 86 | - Input: x*(b, c, t_in, n) 87 | - Output: x(b, c, t_out, n) 88 | """ 89 | 90 | def __init__(self, in_tcn_dim, out_tcn_dim, cur_dilation, kernel_size): 91 | super(GatedTCN, self).__init__() 92 | self.filter_convs = nn.Conv2d(in_channels=in_tcn_dim, 93 | out_channels=out_tcn_dim, 94 | kernel_size=kernel_size, dilation=cur_dilation) 95 | 96 | self.gated_convs = nn.Conv2d(in_channels=in_tcn_dim, 97 | out_channels=out_tcn_dim, 98 | kernel_size=kernel_size, dilation=cur_dilation) 99 | 100 | def forward(self, x): 101 | x = torch.einsum('btvf->bfvt', x) 102 | filter = self.filter_convs(x) 103 | filter = torch.tanh(filter) 104 | gate = self.gated_convs(x) 105 | gate = torch.sigmoid(gate) 106 | x = filter * gate 107 | x = torch.einsum('bfvt->btvf', x) 108 | return x 109 | 110 | 111 | # ------------------------ Dynamic adjacency generator ------------------------ 112 | 113 | class DynamicAdjGen(nn.Module): 114 | """ 115 | - DynamicAdjGen: 116 | - Params: nodes_num, all_feature_dim*, device 117 | - Input: x*(b, t, n, c), origin_adj(n, n), cur_io_adj*(n, n) 118 | - Output: adj_list:[origin_adj(n, n), origin_adj_2(n, n), dynamic_adj(n, n), cur_io_adj(n, n)] 119 | """ 120 | 121 | def __init__(self, nodes_num, all_feature_dim, device): 122 | super(DynamicAdjGen, self).__init__() 123 | self.all_feature_dim = all_feature_dim 124 | self.nodes_num = nodes_num 125 | self.node_vec1 = nn.Parameter(torch.FloatTensor(self.all_feature_dim, self.nodes_num // 10)).to(device) 126 | self.node_vec2 = nn.Parameter(torch.FloatTensor(self.nodes_num, self.nodes_num)).to(device) 127 | nn.init.uniform_(self.node_vec1) 128 | nn.init.uniform_(self.node_vec2) 129 | 130 | def forward(self, x, origin_adj, cur_io_adj): 131 | b, t, n, c = x.shape 132 | x = x.reshape(b, n, t * c) 133 | x_emb = x @ self.node_vec1 134 | x_emb_T = torch.einsum('bnf->bfn', x_emb) 135 | x_selfdot = torch.mean(torch.einsum('bnf,bfm->bnm', (x_emb, x_emb_T)), dim=0) 136 | x_selfdot = x_selfdot.reshape(n, n) 137 | dynamic_adj = x_selfdot @ self.node_vec2 138 | dynamic_adj = F.softmax(dynamic_adj, dim=1) 139 | origin_adj = F.softmax(origin_adj, dim=1) 140 | origin_adj_2 = origin_adj @ origin_adj.T 141 | cur_io_adj = F.softmax(cur_io_adj, dim=1) 142 | 143 | return [origin_adj, origin_adj_2, dynamic_adj, cur_io_adj] 144 | # return [origin_adj, origin_adj_2, cur_io_adj] # without dynamic adjacency matrix 145 | 146 | 147 | # ------------------------ Multi adjacency attention convolution Block ------------------------ 148 | 149 | 150 | class MA2ConvBlock(nn.Module): 151 | """ 152 | - MA2ConvBlock: 153 | - Params: adj_input_dim/adj_hidden_dim/origin_adj(n, n)(for AdjAttention layer); 154 | device(for GraphConv layer and DynamicAdjGen layer); 155 | K/in_channel/out_channel(for GraphConv layer); 156 | in_tcn_dim/out_tcn_dim/cur_dilation/kernel_size(for GatedTCN layer); 157 | nodes_num/all_feature_dim(for DynamicAdjGen layer); 158 | - Input: x*(b, t_in, n, c_in), cur_io_adj*(for DynamicAdjGen layer); 159 | - Output: x(b, t_out, n, c_out) 160 | """ 161 | 162 | def __init__(self, adj_input_dim, adj_hidden_dim, K, in_channel, out_channel, 163 | in_tcn_dim, out_tcn_dim, origin_adj, nodes_num, all_feature_dim, 164 | cur_dilation, kernel_size, droprate, device): 165 | super(MA2ConvBlock, self).__init__() 166 | 167 | self.adj_gen = DynamicAdjGen(nodes_num, all_feature_dim, device) 168 | self.adj_att = AdjAttention(adj_input_dim, adj_hidden_dim) 169 | self.gated_tcn = GatedTCN(in_tcn_dim, out_tcn_dim, cur_dilation, kernel_size) 170 | self.cheb_gcn = GraphConv(K, in_channel, out_channel, device) 171 | self.residual = nn.Conv2d(in_channels=in_channel, out_channels=out_channel, 172 | kernel_size=(1, 1)) 173 | self.origin_adj = origin_adj 174 | self.K = K 175 | self.device = device 176 | self.dropout = nn.Dropout(droprate) 177 | 178 | def forward(self, x_with_io): 179 | x = x_with_io[0] 180 | residual = x 181 | cur_io_adj = x_with_io[1] 182 | x_t = self.gated_tcn(x) 183 | adj_list = self.adj_gen(x, self.origin_adj, cur_io_adj) 184 | adj_agg = self.adj_att(adj_list) 185 | 186 | # # [start] without attention module 187 | # adj_mean = torch.zeros_like(adj_list[0]) 188 | # ratio = 1/len(adj_list) 189 | # for adj in adj_list: 190 | # adj_mean = adj_mean + ratio * adj 191 | # L_tilde = calculate_scaled_laplacian_torch(adj_mean) 192 | # # [end] without attention module 193 | 194 | L_tilde = calculate_scaled_laplacian_torch(adj_agg) 195 | 196 | # # [start]without attention module and dynamic adjacency matrix 197 | # L_tilde = calculate_scaled_laplacian_torch(self.origin_adj) 198 | # # [end]without attention module and dynamic adjacency matrix 199 | 200 | Tks = torch.Tensor(np.array(get_Tk(L_tilde, self.K))).to(self.device) 201 | x_t_g = self.cheb_gcn(x_t, Tks) 202 | x_t_g = self.dropout(x_t_g) 203 | 204 | residual = residual.permute(0, 3, 1, 2) 205 | x_residual = self.residual(residual) 206 | x_residual = x_residual.permute(0, 2, 3, 1) 207 | x_t_g = x_t_g + x_residual[:, -x_t_g.size(1):, :, :] 208 | 209 | return (x_t_g, cur_io_adj) 210 | 211 | 212 | # ------------------------ Main architecture[Multi adjacency attention GCN] ------------------------ 213 | 214 | class MA2GCN(nn.Module): 215 | """ 216 | - MA2GCN: 217 | - Params: blocks, adj_input_dim/adj_hidden_dim/origin_adj(n, n)(for AdjAttention layer); 218 | device(for GraphConv layer and DynamicAdjGen layer); 219 | K/in_channel_list/out_channel_list(for GraphConv layer); 220 | in_tcn_dim_list/out_tcn_dim_list/cur_dilation/kernel_size(for GatedTCN layer); 221 | nodes_num/all_feature_dim(for DynamicAdjGen layer); 222 | - Input: x*(b, t_in, n, c), cur_io_adj*(for DynamicAdjGen layer); 223 | - Output: y(b, t_out, n, c) 224 | """ 225 | 226 | def __init__(self, blocks_num, K, adj_input_dim, adj_hidden_dim, origin_adj, 227 | in_channel_list, out_channel_list, in_tcn_dim_list, 228 | out_tcn_dim_list, cur_dilation_list, kernel_size, 229 | nodes_num, all_feature_dim, receptive_field, droprate, device): 230 | super(MA2GCN, self).__init__() 231 | modules = [] 232 | for i in range(blocks_num): 233 | modules.append(MA2ConvBlock(adj_input_dim, adj_hidden_dim, K, in_channel_list[i], out_channel_list[i], 234 | in_tcn_dim_list[i], out_tcn_dim_list[i], origin_adj, nodes_num, 235 | all_feature_dim[i], cur_dilation_list[i], kernel_size, droprate, device)) 236 | self.blocks = nn.Sequential(*modules) 237 | self.receptive_field = receptive_field 238 | 239 | def forward(self, x_with_io): 240 | x = x_with_io[0] 241 | input_seq = x.shape[1] 242 | # b,t,n,c -> b,c,n,t 243 | x = x.permute(0, 3, 2, 1) 244 | if input_seq < self.receptive_field: 245 | x_pad = F.pad(x, (self.receptive_field - input_seq, 0, 0, 0)) 246 | else: 247 | x_pad = input 248 | x_pad = x_pad.permute(0, 3, 2, 1) 249 | x_with_io = (x_pad, x_with_io[1]) 250 | return self.blocks(x_with_io) 251 | -------------------------------------------------------------------------------- /models/ASTGCN_r.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.nn.functional as F 4 | from utils import calculate_scaled_laplacian_torch, get_Tk 5 | 6 | 7 | class Spatial_Attention_layer(nn.Module): 8 | """ 9 | compute spatial attention scores 10 | """ 11 | 12 | def __init__(self, DEVICE, in_channels, num_of_vertices, num_of_timesteps): 13 | super(Spatial_Attention_layer, self).__init__() 14 | self.W1 = nn.Parameter(torch.FloatTensor(num_of_timesteps).to(DEVICE)) 15 | self.W2 = nn.Parameter(torch.FloatTensor(in_channels, num_of_timesteps).to(DEVICE)) 16 | self.W3 = nn.Parameter(torch.FloatTensor(in_channels).to(DEVICE)) 17 | self.bs = nn.Parameter(torch.FloatTensor(1, num_of_vertices, num_of_vertices).to(DEVICE)) 18 | self.Vs = nn.Parameter(torch.FloatTensor(num_of_vertices, num_of_vertices).to(DEVICE)) 19 | 20 | def forward(self, x): 21 | """ 22 | :param x: (batch_size, N, F_in, T) 23 | :return: (B,N,N) 24 | """ 25 | 26 | lhs = torch.matmul(torch.matmul(x, self.W1), self.W2) # (b,N,F,T)(T)->(b,N,F)(F,T)->(b,N,T) 27 | 28 | rhs = torch.matmul(self.W3, x).transpose(-1, -2) # (F)(b,N,F,T)->(b,N,T)->(b,T,N) 29 | 30 | product = torch.matmul(lhs, rhs) # (b,N,T)(b,T,N) -> (B, N, N) 31 | 32 | S = torch.matmul(self.Vs, torch.sigmoid(product + self.bs)) # (N,N)(B, N, N)->(B,N,N) 33 | 34 | S_normalized = F.softmax(S, dim=1) 35 | 36 | return S_normalized 37 | 38 | 39 | class cheb_conv_withSAt(nn.Module): 40 | """ 41 | K-order chebyshev graph convolution 42 | """ 43 | 44 | def __init__(self, K, cheb_polynomials, in_channels, out_channels): 45 | """ 46 | :param K: int 47 | :param in_channles: int, num of channels in the input sequence 48 | :param out_channels: int, num of channels in the output sequence 49 | """ 50 | super(cheb_conv_withSAt, self).__init__() 51 | self.K = K 52 | self.cheb_polynomials = cheb_polynomials 53 | self.in_channels = in_channels 54 | self.out_channels = out_channels 55 | self.DEVICE = cheb_polynomials[0].device 56 | self.Theta = nn.ParameterList( 57 | [nn.Parameter(torch.FloatTensor(in_channels, out_channels).to(self.DEVICE)) for _ in range(K)]) 58 | 59 | def forward(self, x, spatial_attention): 60 | """ 61 | Chebyshev graph convolution operation 62 | :param x: (batch_size, N, F_in, T) 63 | :return: (batch_size, N, F_out, T) 64 | """ 65 | 66 | batch_size, num_of_vertices, in_channels, num_of_timesteps = x.shape 67 | 68 | outputs = [] 69 | 70 | for time_step in range(num_of_timesteps): 71 | 72 | graph_signal = x[:, :, :, time_step] # (b, N, F_in) 73 | 74 | output = torch.zeros(batch_size, num_of_vertices, self.out_channels).to(self.DEVICE) # (b, N, F_out) 75 | 76 | for k in range(self.K): 77 | T_k = self.cheb_polynomials[k] # (N,N) 78 | 79 | T_k_with_at = T_k.mul(spatial_attention) # (N,N)*(N,N) = (N,N) 多行和为1, 按着列进行归一化 80 | 81 | theta_k = self.Theta[k] # (in_channel, out_channel) 82 | 83 | rhs = T_k_with_at.permute(0, 2, 1).matmul( 84 | graph_signal) # (N, N)(b, N, F_in) = (b, N, F_in) 因为是左乘,所以多行和为1变为多列和为1,即一行之和为1,进行左乘 85 | 86 | # temp = torch.einsum('nvv,nvc->nvc', (T_k_with_at, graph_signal)) 87 | # b = (rhs == temp) 88 | 89 | output = output + rhs.matmul(theta_k) # (b, N, F_in)(F_in, F_out) = (b, N, F_out) 90 | 91 | outputs.append(output.unsqueeze(-1)) # (b, N, F_out, 1) 92 | 93 | return F.relu(torch.cat(outputs, dim=-1)) # (b, N, F_out, T) 94 | 95 | 96 | class Temporal_Attention_layer(nn.Module): 97 | def __init__(self, DEVICE, in_channels, num_of_vertices, num_of_timesteps): 98 | super(Temporal_Attention_layer, self).__init__() 99 | self.U1 = nn.Parameter(torch.FloatTensor(num_of_vertices).to(DEVICE)) 100 | self.U2 = nn.Parameter(torch.FloatTensor(in_channels, num_of_vertices).to(DEVICE)) 101 | self.U3 = nn.Parameter(torch.FloatTensor(in_channels).to(DEVICE)) 102 | self.be = nn.Parameter(torch.FloatTensor(1, num_of_timesteps, num_of_timesteps).to(DEVICE)) 103 | self.Ve = nn.Parameter(torch.FloatTensor(num_of_timesteps, num_of_timesteps).to(DEVICE)) 104 | 105 | def forward(self, x): 106 | """ 107 | :param x: (batch_size, N, F_in, T) 108 | :return: (B, T, T) 109 | """ 110 | _, num_of_vertices, num_of_features, num_of_timesteps = x.shape 111 | 112 | lhs = torch.matmul(torch.matmul(x.permute(0, 3, 2, 1), self.U1), self.U2) 113 | # x:(B, N, F_in, T) -> (B, T, F_in, N) 114 | # (B, T, F_in, N)(N) -> (B,T,F_in) 115 | # (B,T,F_in)(F_in,N)->(B,T,N) 116 | 117 | rhs = torch.matmul(self.U3, x) # (F)(B,N,F,T)->(B, N, T) 118 | 119 | product = torch.matmul(lhs, rhs) # (B,T,N)(B,N,T)->(B,T,T) 120 | 121 | E = torch.matmul(self.Ve, torch.sigmoid(product + self.be)) # (B, T, T) 122 | 123 | E_normalized = F.softmax(E, dim=1) 124 | 125 | return E_normalized 126 | 127 | 128 | class cheb_conv(nn.Module): 129 | """ 130 | K-order chebyshev graph convolution 131 | """ 132 | 133 | def __init__(self, K, cheb_polynomials, in_channels, out_channels): 134 | ''' 135 | :param K: int 136 | :param in_channles: int, num of channels in the input sequence 137 | :param out_channels: int, num of channels in the output sequence 138 | ''' 139 | super(cheb_conv, self).__init__() 140 | self.K = K 141 | self.cheb_polynomials = cheb_polynomials 142 | self.in_channels = in_channels 143 | self.out_channels = out_channels 144 | self.DEVICE = cheb_polynomials[0].device 145 | self.Theta = nn.ParameterList( 146 | [nn.Parameter(torch.FloatTensor(in_channels, out_channels).to(self.DEVICE)) for _ in range(K)]) 147 | 148 | def forward(self, x): 149 | """ 150 | Chebyshev graph convolution operation 151 | :param x: (batch_size, N, F_in, T) 152 | :return: (batch_size, N, F_out, T) 153 | """ 154 | 155 | batch_size, num_of_vertices, in_channels, num_of_timesteps = x.shape 156 | 157 | outputs = [] 158 | 159 | for time_step in range(num_of_timesteps): 160 | 161 | graph_signal = x[:, :, :, time_step] # (b, N, F_in) 162 | 163 | output = torch.zeros(batch_size, num_of_vertices, self.out_channels).to(self.DEVICE) # (b, N, F_out) 164 | 165 | for k in range(self.K): 166 | T_k = self.cheb_polynomials[k] # (N,N) 167 | 168 | theta_k = self.Theta[k] # (in_channel, out_channel) 169 | 170 | rhs = graph_signal.permute(0, 2, 1).matmul(T_k).permute(0, 2, 1) 171 | 172 | output = output + rhs.matmul(theta_k) 173 | 174 | outputs.append(output.unsqueeze(-1)) 175 | 176 | return F.relu(torch.cat(outputs, dim=-1)) 177 | 178 | 179 | class ASTGCN_block(nn.Module): 180 | 181 | def __init__(self, DEVICE, in_channels, K, nb_chev_filter, nb_time_filter, time_strides, cheb_polynomials, 182 | num_of_vertices, num_of_timesteps): 183 | super(ASTGCN_block, self).__init__() 184 | self.TAt = Temporal_Attention_layer(DEVICE, in_channels, num_of_vertices, num_of_timesteps) 185 | self.SAt = Spatial_Attention_layer(DEVICE, in_channels, num_of_vertices, num_of_timesteps) 186 | self.cheb_conv_SAt = cheb_conv_withSAt(K, cheb_polynomials, in_channels, nb_chev_filter) 187 | self.time_conv = nn.Conv2d(nb_chev_filter, nb_time_filter, kernel_size=(1, 3), stride=(1, time_strides), 188 | padding=(0, 1)) 189 | self.residual_conv = nn.Conv2d(in_channels, nb_time_filter, kernel_size=(1, 1), stride=(1, time_strides)) 190 | self.ln = nn.LayerNorm(nb_time_filter) # 需要将channel放到最后一个维度上 191 | 192 | def forward(self, x): 193 | """ 194 | :param x: (batch_size, N, F_in, T) 195 | :return: (batch_size, N, nb_time_filter, T) 196 | """ 197 | batch_size, num_of_vertices, num_of_features, num_of_timesteps = x.shape 198 | 199 | # TAt 200 | temporal_At = self.TAt(x) # (b, T, T) 201 | 202 | x_TAt = torch.matmul(x.reshape(batch_size, -1, num_of_timesteps), temporal_At).reshape(batch_size, 203 | num_of_vertices, 204 | num_of_features, 205 | num_of_timesteps) 206 | # SAt 207 | spatial_At = self.SAt(x_TAt) 208 | 209 | # cheb gcn 210 | spatial_gcn = self.cheb_conv_SAt(x, spatial_At) # (b,N,F,T) 211 | # spatial_gcn = self.cheb_conv(x) 212 | 213 | # convolution along the time axis 214 | time_conv_output = self.time_conv( 215 | spatial_gcn.permute(0, 2, 1, 3)) # (b,N,F,T)->(b,F,N,T) 用(1,3)的卷积核去做->(b,F,N,T) 216 | 217 | # residual shortcut 218 | x_residual = self.residual_conv(x.permute(0, 2, 1, 3)) # (b,N,F,T)->(b,F,N,T) 用(1,1)的卷积核去做->(b,F,N,T) 219 | 220 | x_residual = self.ln(F.relu(x_residual + time_conv_output).permute(0, 3, 2, 1)).permute(0, 2, 3, 1) 221 | # (b,F,N,T)->(b,T,N,F) -ln-> (b,T,N,F)->(b,N,F,T) 222 | 223 | return x_residual 224 | 225 | 226 | class ASTGCN_submodule(nn.Module): 227 | 228 | def __init__(self, DEVICE, nb_block, in_channels, K, nb_chev_filter, nb_time_filter, time_strides, cheb_polynomials, 229 | num_for_predict, len_input, num_of_vertices): 230 | """ 231 | :param nb_block: 232 | :param in_channels: 233 | :param K: 234 | :param nb_chev_filter: 235 | :param nb_time_filter: 236 | :param time_strides: 237 | :param cheb_polynomials: 238 | :param nb_predict_step: 239 | """ 240 | 241 | super(ASTGCN_submodule, self).__init__() 242 | 243 | self.BlockList = nn.ModuleList([ASTGCN_block(DEVICE, in_channels, K, nb_chev_filter, nb_time_filter, 244 | time_strides, cheb_polynomials, num_of_vertices, len_input)]) 245 | 246 | self.BlockList.extend([ASTGCN_block(DEVICE, nb_time_filter, K, nb_chev_filter, nb_time_filter, 1, 247 | cheb_polynomials, num_of_vertices, len_input // time_strides) for _ in 248 | range(nb_block - 1)]) 249 | 250 | self.final_conv = nn.Conv2d(int(len_input / time_strides), num_for_predict, 251 | kernel_size=(1, nb_time_filter - in_channels + 1)) 252 | 253 | self.DEVICE = DEVICE 254 | 255 | self.to(DEVICE) 256 | 257 | def forward(self, x): 258 | """ 259 | :param x: (B, N_nodes, F_in, T_in) 260 | :return: (B, N_nodes, T_out) 261 | """ 262 | for block in self.BlockList: 263 | x = block(x) 264 | 265 | output = self.final_conv(x.permute(0, 3, 1, 2)) 266 | 267 | return output 268 | 269 | 270 | def make_model(DEVICE, nb_block, in_channels, K, nb_chev_filter, nb_time_filter, time_strides, adj_mx, num_for_predict, 271 | len_input, num_of_vertices): 272 | """ 273 | 274 | :param DEVICE: 275 | :param nb_block: 276 | :param in_channels: 277 | :param K: 278 | :param nb_chev_filter: 279 | :param nb_time_filter: 280 | :param time_strides: 281 | :param cheb_polynomials: 282 | :param nb_predict_step: 283 | :param len_input 284 | :return: 285 | """ 286 | L_tilde = calculate_scaled_laplacian_torch(adj_mx) 287 | cheb_polynomials = [torch.from_numpy(i).type(torch.FloatTensor).to(DEVICE) for i in get_Tk(L_tilde, K)] 288 | model = ASTGCN_submodule(DEVICE, nb_block, in_channels, K, nb_chev_filter, nb_time_filter, time_strides, 289 | cheb_polynomials, num_for_predict, len_input, num_of_vertices) 290 | 291 | for p in model.parameters(): 292 | if p.dim() > 1: 293 | nn.init.xavier_uniform_(p) 294 | else: 295 | nn.init.uniform_(p) 296 | 297 | return model 298 | -------------------------------------------------------------------------------- /models/stgcn/layers.py: -------------------------------------------------------------------------------- 1 | import math 2 | import torch 3 | import torch.nn as nn 4 | import torch.nn.functional as F 5 | import torch.nn.init as init 6 | 7 | 8 | class Align(nn.Module): 9 | def __init__(self, c_in, c_out): 10 | super(Align, self).__init__() 11 | self.c_in = c_in 12 | self.c_out = c_out 13 | self.align_conv = nn.Conv2d(in_channels=c_in, out_channels=c_out, kernel_size=(1, 1)) 14 | 15 | def forward(self, x): 16 | if self.c_in > self.c_out: 17 | x = self.align_conv(x) 18 | elif self.c_in < self.c_out: 19 | batch_size, _, timestep, n_vertex = x.shape 20 | x = torch.cat([x, torch.zeros([batch_size, self.c_out - self.c_in, timestep, n_vertex]).to(x)], dim=1) 21 | else: 22 | x = x 23 | 24 | return x 25 | 26 | 27 | class CausalConv1d(nn.Conv1d): 28 | def __init__(self, in_channels, out_channels, kernel_size, stride=1, enable_padding=False, dilation=1, groups=1, 29 | bias=True): 30 | if enable_padding == True: 31 | self.__padding = (kernel_size - 1) * dilation 32 | else: 33 | self.__padding = 0 34 | super(CausalConv1d, self).__init__(in_channels, out_channels, kernel_size=kernel_size, stride=stride, 35 | padding=self.__padding, dilation=dilation, groups=groups, bias=bias) 36 | 37 | def forward(self, input): 38 | result = super(CausalConv1d, self).forward(input) 39 | if self.__padding != 0: 40 | return result[:, :, : -self.__padding] 41 | 42 | return result 43 | 44 | 45 | class CausalConv2d(nn.Conv2d): 46 | def __init__(self, in_channels, out_channels, kernel_size, stride=1, enable_padding=False, dilation=1, groups=1, 47 | bias=True): 48 | kernel_size = nn.modules.utils._pair(kernel_size) 49 | stride = nn.modules.utils._pair(stride) 50 | dilation = nn.modules.utils._pair(dilation) 51 | if enable_padding == True: 52 | self.__padding = [int((kernel_size[i] - 1) * dilation[i]) for i in range(len(kernel_size))] 53 | else: 54 | self.__padding = 0 55 | self.left_padding = nn.modules.utils._pair(self.__padding) 56 | super(CausalConv2d, self).__init__(in_channels, out_channels, kernel_size, stride=stride, padding=0, 57 | dilation=dilation, groups=groups, bias=bias) 58 | 59 | def forward(self, input): 60 | if self.__padding != 0: 61 | input = F.pad(input, (self.left_padding[1], 0, self.left_padding[0], 0)) 62 | result = super(CausalConv2d, self).forward(input) 63 | 64 | return result 65 | 66 | 67 | class TemporalConvLayer(nn.Module): 68 | 69 | # Temporal Convolution Layer (GLU) 70 | # 71 | # |--------------------------------| * Residual Connection * 72 | # | | 73 | # | |--->--- CasualConv2d ----- + -------| 74 | # -------|----| ⊙ ------> 75 | # |--->--- CasualConv2d --- Sigmoid ---| 76 | # 77 | 78 | # param x: tensor, [bs, c_in, ts, n_vertex] 79 | 80 | def __init__(self, Kt, c_in, c_out, n_vertex, act_func): 81 | super(TemporalConvLayer, self).__init__() 82 | self.Kt = Kt 83 | self.c_in = c_in 84 | self.c_out = c_out 85 | self.n_vertex = n_vertex 86 | self.align = Align(c_in, c_out) 87 | if act_func == 'glu' or act_func == 'gtu': 88 | self.causal_conv = CausalConv2d(in_channels=c_in, out_channels=2 * c_out, kernel_size=(Kt, 1), 89 | enable_padding=False, dilation=1) 90 | else: 91 | self.causal_conv = CausalConv2d(in_channels=c_in, out_channels=c_out, kernel_size=(Kt, 1), 92 | enable_padding=False, dilation=1) 93 | self.act_func = act_func 94 | self.sigmoid = nn.Sigmoid() 95 | self.tanh = nn.Tanh() 96 | self.relu = nn.ReLU() 97 | self.leaky_relu = nn.LeakyReLU() 98 | self.silu = nn.SiLU() 99 | 100 | def forward(self, x): 101 | x_in = self.align(x)[:, :, self.Kt - 1:, :] 102 | x_causal_conv = self.causal_conv(x) 103 | 104 | if self.act_func == 'glu' or self.act_func == 'gtu': 105 | x_p = x_causal_conv[:, : self.c_out, :, :] 106 | x_q = x_causal_conv[:, -self.c_out:, :, :] 107 | 108 | if self.act_func == 'glu': 109 | # GLU was first purposed in 110 | # *Language Modeling with Gated Convolutional Networks*. 111 | # URL: https://arxiv.org/abs/1612.08083 112 | # Input tensor X is split by a certain dimension into tensor X_a and X_b. 113 | # In PyTorch, GLU is defined as X_a ⊙ Sigmoid(X_b). 114 | # URL: https://pytorch.org/docs/master/nn.functional.html#torch.nn.functional.glu 115 | # (x_p + x_in) ⊙ Sigmoid(x_q) 116 | x = torch.mul((x_p + x_in), self.sigmoid(x_q)) 117 | 118 | else: 119 | # Tanh(x_p + x_in) ⊙ Sigmoid(x_q) 120 | x = torch.mul(self.tanh(x_p + x_in), self.sigmoid(x_q)) 121 | 122 | elif self.act_func == 'relu': 123 | x = self.relu(x_causal_conv + x_in) 124 | 125 | elif self.act_func == 'leaky_relu': 126 | x = self.leaky_relu(x_causal_conv + x_in) 127 | 128 | elif self.act_func == 'silu': 129 | x = self.silu(x_causal_conv + x_in) 130 | 131 | else: 132 | raise NotImplementedError(f'ERROR: The activation function {self.act_func} is not implemented.') 133 | 134 | return x 135 | 136 | 137 | class ChebGraphConv(nn.Module): 138 | def __init__(self, c_in, c_out, Ks, gso, bias): 139 | super(ChebGraphConv, self).__init__() 140 | self.c_in = c_in 141 | self.c_out = c_out 142 | self.Ks = Ks 143 | self.gso = gso 144 | self.weight = nn.Parameter(torch.FloatTensor(Ks, c_in, c_out)) 145 | if bias: 146 | self.bias = nn.Parameter(torch.FloatTensor(c_out)) 147 | else: 148 | self.register_parameter('bias', None) 149 | self.reset_parameters() 150 | 151 | def reset_parameters(self): 152 | init.kaiming_uniform_(self.weight, a=math.sqrt(5)) 153 | if self.bias is not None: 154 | fan_in, _ = init._calculate_fan_in_and_fan_out(self.weight) 155 | bound = 1 / math.sqrt(fan_in) if fan_in > 0 else 0 156 | init.uniform_(self.bias, -bound, bound) 157 | 158 | def forward(self, x): 159 | # bs, c_in, ts, n_vertex = x.shape 160 | x = torch.permute(x, (0, 2, 3, 1)) 161 | 162 | if self.Ks - 1 < 0: 163 | raise ValueError( 164 | f'ERROR: the graph convolution kernel size Ks has to be a positive integer, but received {self.Ks}.') 165 | elif self.Ks - 1 == 0: 166 | x_0 = x 167 | x_list = [x_0] 168 | elif self.Ks - 1 == 1: 169 | x_0 = x 170 | x_1 = torch.einsum('hi,btij->bthj', self.gso, x) 171 | x_list = [x_0, x_1] 172 | elif self.Ks - 1 >= 2: 173 | x_0 = x 174 | x_1 = torch.einsum('hi,btij->bthj', self.gso, x) 175 | x_list = [x_0, x_1] 176 | for k in range(2, self.Ks): 177 | x_list.append(torch.einsum('hi,btij->bthj', 2 * self.gso, x_list[k - 1]) - x_list[k - 2]) 178 | 179 | x = torch.stack(x_list, dim=2) 180 | 181 | cheb_graph_conv = torch.einsum('btkhi,kij->bthj', x, self.weight) 182 | 183 | if self.bias is not None: 184 | cheb_graph_conv = torch.add(cheb_graph_conv, self.bias) 185 | else: 186 | cheb_graph_conv = cheb_graph_conv 187 | 188 | return cheb_graph_conv 189 | 190 | 191 | class GraphConv(nn.Module): 192 | def __init__(self, c_in, c_out, gso, bias): 193 | super(GraphConv, self).__init__() 194 | self.c_in = c_in 195 | self.c_out = c_out 196 | self.gso = gso 197 | self.weight = nn.Parameter(torch.FloatTensor(c_in, c_out)) 198 | if bias: 199 | self.bias = nn.Parameter(torch.FloatTensor(c_out)) 200 | else: 201 | self.register_parameter('bias', None) 202 | self.reset_parameters() 203 | 204 | def reset_parameters(self): 205 | init.kaiming_uniform_(self.weight, a=math.sqrt(5)) 206 | if self.bias is not None: 207 | fan_in, _ = init._calculate_fan_in_and_fan_out(self.weight) 208 | bound = 1 / math.sqrt(fan_in) if fan_in > 0 else 0 209 | init.uniform_(self.bias, -bound, bound) 210 | 211 | def forward(self, x): 212 | # bs, c_in, ts, n_vertex = x.shape 213 | x = torch.permute(x, (0, 2, 3, 1)) 214 | 215 | first_mul = torch.einsum('hi,btij->bthj', self.gso, x) 216 | second_mul = torch.einsum('bthi,ij->bthj', first_mul, self.weight) 217 | 218 | if self.bias is not None: 219 | graph_conv = torch.add(second_mul, self.bias) 220 | else: 221 | graph_conv = second_mul 222 | 223 | return graph_conv 224 | 225 | 226 | class GraphConvLayer(nn.Module): 227 | def __init__(self, graph_conv_type, c_in, c_out, Ks, gso, bias): 228 | super(GraphConvLayer, self).__init__() 229 | self.graph_conv_type = graph_conv_type 230 | self.c_in = c_in 231 | self.c_out = c_out 232 | self.align = Align(c_in, c_out) 233 | self.Ks = Ks 234 | self.gso = gso 235 | if self.graph_conv_type == 'cheb_graph_conv': 236 | self.cheb_graph_conv = ChebGraphConv(c_out, c_out, Ks, gso, bias) 237 | elif self.graph_conv_type == 'graph_conv': 238 | self.graph_conv = GraphConv(c_out, c_out, gso, bias) 239 | 240 | def forward(self, x): 241 | x_gc_in = self.align(x) 242 | if self.graph_conv_type == 'cheb_graph_conv': 243 | x_gc = self.cheb_graph_conv(x_gc_in) 244 | elif self.graph_conv_type == 'graph_conv': 245 | x_gc = self.graph_conv(x_gc_in) 246 | x_gc = x_gc.permute(0, 3, 1, 2) 247 | x_gc_out = torch.add(x_gc, x_gc_in) 248 | 249 | return x_gc_out 250 | 251 | 252 | class STConvBlock(nn.Module): 253 | # STConv Block contains 'TGTND' structure 254 | # T: Gated Temporal Convolution Layer (GLU or GTU) 255 | # G: Graph Convolution Layer (ChebGraphConv or GraphConv) 256 | # T: Gated Temporal Convolution Layer (GLU or GTU) 257 | # N: Layer Normolization 258 | # D: Dropout 259 | 260 | def __init__(self, Kt, Ks, n_vertex, last_block_channel, channels, act_func, graph_conv_type, gso, bias, droprate): 261 | super(STConvBlock, self).__init__() 262 | self.tmp_conv1 = TemporalConvLayer(Kt, last_block_channel, channels[0], n_vertex, act_func) 263 | self.graph_conv = GraphConvLayer(graph_conv_type, channels[0], channels[1], Ks, gso, bias) 264 | self.tmp_conv2 = TemporalConvLayer(Kt, channels[1], channels[2], n_vertex, act_func) 265 | self.tc2_ln = nn.LayerNorm([n_vertex, channels[2]]) 266 | self.relu = nn.ReLU() 267 | self.dropout = nn.Dropout(p=droprate) 268 | 269 | def forward(self, x): 270 | x = self.tmp_conv1(x) 271 | x = self.graph_conv(x) 272 | x = self.relu(x) 273 | x = self.tmp_conv2(x) 274 | x = self.tc2_ln(x.permute(0, 2, 3, 1)).permute(0, 3, 1, 2) 275 | x = self.dropout(x) 276 | 277 | return x 278 | 279 | 280 | class OutputBlock(nn.Module): 281 | # Output block contains 'TNFF' structure 282 | # T: Gated Temporal Convolution Layer (GLU or GTU) 283 | # N: Layer Normolization 284 | # F: Fully-Connected Layer 285 | # F: Fully-Connected Layer 286 | 287 | def __init__(self, Ko, last_block_channel, channels, end_channel, n_vertex, act_func, bias, droprate): 288 | super(OutputBlock, self).__init__() 289 | self.tmp_conv1 = TemporalConvLayer(Ko, last_block_channel, channels[0], n_vertex, act_func) 290 | self.fc1 = nn.Linear(in_features=channels[0], out_features=channels[1], bias=bias) 291 | self.fc2 = nn.Linear(in_features=channels[1], out_features=end_channel, bias=bias) 292 | self.tc1_ln = nn.LayerNorm([n_vertex, channels[0]]) 293 | self.relu = nn.ReLU() 294 | self.leaky_relu = nn.LeakyReLU() 295 | self.silu = nn.SiLU() 296 | self.dropout = nn.Dropout(p=droprate) 297 | 298 | def forward(self, x): 299 | x = self.tmp_conv1(x) 300 | x = self.tc1_ln(x.permute(0, 2, 3, 1)) 301 | x = self.fc1(x) 302 | x = self.relu(x) 303 | x = self.fc2(x).permute(0, 3, 1, 2) 304 | 305 | return x 306 | --------------------------------------------------------------------------------