GDN 코드 리뷰
( 논문 리뷰 : https://seunghan96.github.io/ts/gnn/ts26/ )
1. Import Dataset
(1) MTS
Multivariate Time Series 데이터 크기
-
train
: (1565, 27)- time length = 1565
- number of nodes = 27 ( 서로 다른 27개의 시계열이 존재 )
-
test
: (2049, 28)-
time length = 2049
-
number of nodes = 27
-
label (attack 여부) = 1
( 27개의 시계열 별로 각각 존재하는 것이 아닌, “시점”당 하나씩 존재 )
-
dataset = 'msl'
train = pd.read_csv(f'./data/{dataset}/train.csv', sep=',', index_col=0)
test = pd.read_csv(f'./data/{dataset}/test.csv', sep=',', index_col=0)
if 'attack' in train.columns:
train = train.drop(columns=['attack'])
(2) Feature Map
-
feature_map
: (list) 27개의 node 이름 ( 시계열 ID ) -
fc_struc
: (dict) 본인을 제외한 나머지 node들을 value로 가지는 딕셔너리
feature_map = get_feature_map(dataset)
fc_struc = get_fc_graph_struc(dataset)
(3) Edge Index
위의 fc_struc
& feature_map
를 사용해서 생성
fc_edge_index
의 크기 : (2, 702)
-
2 : edge 양 끝
-
702 : edge의 개수
( 702 = 27 x (27-1) )
- (1) 27개의 시계열은 서로 모두 서로 연결이 되어있다
- (2) 양방향성
fc_edge_index = build_loc_net(fc_struc,
list(train.columns),
feature_map=feature_map)
fc_edge_index = torch.tensor(fc_edge_index, dtype = torch.long)
edge_index_sets
: edge 정보를 담는 곳
- 여기서는 1개뿐! ( =
fc_edge_index
)
edge_index_sets = []
edge_index_sets.append(fc_edge_index)
(4) 데이터셋 완성
train_dataset_indata
의 크기 : 28
- 28개의 원소로 구성된 1개의 list이다.
- 각각의 원소는 1565의 time length를 가짐
- 마지막 (28번째) 원소는, label이다 ( = 0 )
test_dataset_indata
의 크기 : 28
- 28개의 원소로 구성된 1개의 list이다.
- 각각의 원소는 2049의 time length를 가짐
- 마지막 (28번째) 원소는, label이다 ( 0/1 )
train_dataset_indata = construct_data(train,
feature_map,
labels=0)
test_dataset_indata = construct_data(test,
feature_map,
labels=test.attack.tolist())
2. Dataset & Data Loader
(1) Dataset
(train) TimeDataset 클래스에 넣어주는 요소 2가지
- (1) (28x1565의) MTS 정보 ( = \(X\) )
- (2) edge 연결 정보 ( = \(A\) )
아래의 config cfg
에 담긴 내용
slide_win
: sliding window ( = 15 )slide_stride
: slide stride ( = 5 )- [1,2,…15] -> [16] 예측
- [6,7,…,20] -> [21] 예측
train_dataset = TimeDataset(train_dataset_indata,
fc_edge_index,
mode='train',
config=cfg)
test_dataset = TimeDataset(test_dataset_indata,
fc_edge_index,
mode='test',
config=cfg)
train_dataset
내의 속성들
-
(1)
train_dataset.x
: (310, 27, 15)- 310이란?
- 1565 길이의 time series를
- window size=15 & stride=5로 설정 했을때
- 생성되는 시계열 부분부분들
- 310이란?
-
(2)
train_dataset.y
: (310, 27)-
가장 마지막 +1 번째( 여기서는 t=16번째 )의 시계열 값
( 주의 : label값이 아니다. x와 마찬가지로 “시계열”값 이다 )
-
-
(3)
train_dataset.labels
: (310)- “27개의 node 전체에 대해, 가장 마지막 +1 번째( 여기서는 t=16번째 )의 이상치 여부
(2) Data Loader
(임의로 hyperparameter 생성)
seed = 0
batch_size = 32
val_ratio = 0.1
Train & Val 나누기
- 310개의 TS 부분들을, 0.9 : 0.1 ( 279 : 31 )로 나눈다.
train_dataloader, val_dataloader = get_loaders(train_dataset,
seed,
batch_size,
val_ratio = val_ratio)
test_dataloader = DataLoader(test_dataset,
batch_size=batch_size,
shuffle=False,
num_workers=0)
Dataloader가 뱉어내는 값 : 총 4개
- (1) X : (batch size, node 개수, window size)
- ex) (32, 27, 15)
- (2) y : (batch size, node 개수, 1) ……. window의 가장 마지막 값
- ex) (32, 27, 1)
- (3) label : (batch size, 1)
- ex) (32, 1)
- (4) edge index : (batch size, 2, edge 개수)
- ex) (32, 2, ??)
3. Modeling 준비 과정
dim = 64
slide_win = 15
out_layer_num = 1
out_layer_inter_dim = 256
topk = 20
device = 'cuda' if torch.cuda.is_available() else 'cpu'
(1) 함수 1 : get_batch_edge_index
역할 : batch개 만큼 엣지를 복제해줌
- Input : ( 2, edge_num )
- Output : ( 2, edge_num x batch size )
(2) 클래스 1 : OutLayer
- layer_num : 쌓을 FC layer의 개수
- (in_num, inter_num)
- (inter_num, inter_num)
- …
- (inter_num, 1)
- Batch Norm & ReLU 포함
-
이것들을 담은 ModuleList 형태로 생성
- 최종 output은 scalar 값
(3) 클래스 2 : GraphLayer
[1] forward 함수 : input으로 들어오는 data의 크기
- (1) x : (batch_size x node 개수, window size)
- ex) ( 32x27, 15 )
- (2) edge_index : ( 2, node개수 x top k x batch_size )
- ex) ( 2, 27x5x32 )
- (3) embedding : ( batch_size x node개수, embed_dim )
- ex) ( 32x27, 256 )
[2] message 함수 : input으로 들어오는 data의 크기
- (1) x_i : (batch_size x node 개수, heads x out_channels)
- (2) x_j : (batch_size x node 개수, heads x out_channels)
- (3) edge_index_i :
- (4) size_i :
- (5) embedding : ( bs x node개수, embed_dim )
- (6) edges : ( 2, node개수 x top k x batch_size )
만약 들어오는 input \(x\) 가…
- (1) list 라면 = 2개의 node를 가지고 있다면
- (2) tensor라면 = 1개의 node만을 가지고 있음
class GraphLayer(MessagePassing):
def __init__(self, in_channels, out_channels, heads=1, concat=True,
negative_slope=0.2, dropout=0, bias=True, inter_dim=-1,**kwargs):
super(GraphLayer, self).__init__(aggr='add', **kwargs)
self.in_channels = in_channels # in_channels == window size
self.out_channels = out_channels
self.heads = heads
self.concat = concat
self.negative_slope = negative_slope
self.dropout = dropout
self.__alpha__ = None
self.lin = Linear(in_channels, heads * out_channels, bias=False)
self.att_i = Parameter(torch.Tensor(1, heads, out_channels))
self.att_j = Parameter(torch.Tensor(1, heads, out_channels))
self.att_em_i = Parameter(torch.Tensor(1, heads, out_channels))
self.att_em_j = Parameter(torch.Tensor(1, heads, out_channels))
if bias and concat:
self.bias = Parameter(torch.Tensor(heads * out_channels))
elif bias and not concat:
self.bias = Parameter(torch.Tensor(out_channels))
else:
self.register_parameter('bias', None)
self.reset_parameters()
def reset_parameters(self):
glorot(self.lin.weight)
glorot(self.att_i)
glorot(self.att_j)
zeros(self.att_em_i)
zeros(self.att_em_j)
zeros(self.bias)
def forward(self, x, edge_index, embedding, return_attention_weights=False):
# (1) node(들)을 linear layer에 통과시킴
## [IN] x의 크기 : (batch_size x node 개수, window size)
## [OUT] x의 크기 : (batch_size x node 개수, heads x out_channels)
if torch.is_tensor(x):
x = self.lin(x)
x = (x, x)
else:
x = (self.lin(x[0]), self.lin(x[1]))
# (2) self-loop 추가하기
edge_index, _ = remove_self_loops(edge_index)
edge_index, _ = add_self_loops(edge_index,
num_nodes=x[1].size(self.node_dim))
# (3) propagate하기
out = self.propagate(edge_index, x=x,
embedding=embedding, edges=edge_index,
return_attention_weights=return_attention_weights)
# (4) concat / mean
if self.concat:
out = out.view(-1, self.heads * self.out_channels)
else:
out = out.mean(dim=1)
if self.bias is not None:
out = out + self.bias
if return_attention_weights:
alpha, self.__alpha__ = self.__alpha__, None
return out, (edge_index, alpha)
else:
return out
def message(self, x_i, x_j, edge_index_i, size_i,
embedding,
edges,
return_attention_weights):
# (1) input으로 들어온 2개의 node x_i, x_j는,
## linear layer를 거쳐 나와서
## 그 크기는 ( batch size, out_channel*n_heads ) 이다
x_i = x_i.view(-1, self.heads, self.out_channels)
x_j = x_j.view(-1, self.heads, self.out_channels)
# (2) 이 node에 해당하는 node embedding을 꺼내고
if embedding is not None:
embedding_i, embedding_j = embedding[edge_index_i], embedding[edges[0]]
embedding_i = embedding_i.unsqueeze(1).repeat(1,self.heads,1)
embedding_j = embedding_j.unsqueeze(1).repeat(1,self.heads,1)
key_i = torch.cat((x_i, embedding_i), dim=-1)
key_j = torch.cat((x_j, embedding_j), dim=-1)
cat_att_i = torch.cat((self.att_i, self.att_em_i), dim=-1)
cat_att_j = torch.cat((self.att_j, self.att_em_j), dim=-1)
alpha = (key_i * cat_att_i).sum(-1) + (key_j * cat_att_j).sum(-1)
alpha = alpha.view(-1, self.heads, 1)
alpha = F.leaky_relu(alpha, self.negative_slope)
alpha = softmax(alpha, edge_index_i, size_i)
if return_attention_weights:
self.__alpha__ = alpha
alpha = F.dropout(alpha, p=self.dropout, training=self.training)
return x_j * alpha.view(-1, self.heads, 1)
def __repr__(self):
return '{}({}, {}, heads={})'.format(self.__class__.__name__,
self.in_channels,
self.out_channels, self.heads)
(4) 클래스 3 : GNNLayer
- input으로 들어오는 data의 크기
- (1) x : (batch_size x node 개수, window size)
- (2) edge_index : ( 2, node개수 x top k x batch_size )
- (3) embedding : ( bs x node개수, embed_dim )
- (4) node_num : 스칼라 ( num_node x batch_size )
class GNNLayer(nn.Module):
def __init__(self, in_channel, out_channel, inter_dim=0, heads=1, node_num=100):
super(GNNLayer, self).__init__()
self.gnn = GraphLayer(in_channel, out_channel,
inter_dim=inter_dim, heads=heads, concat=False)
self.bn = nn.BatchNorm1d(out_channel)
self.relu = nn.ReLU()
self.leaky_relu = nn.LeakyReLU()
def forward(self, x, edge_index, embedding=None, node_num=0):
output = self.gnn(x, edge_index, embedding,
return_attention_weights=True)
out, (new_edge_index, att_weight) = output
self.att_weight_1 = att_weight
self.edge_index_1 = new_edge_index
out = self.bn(out)
return self.relu(out)
4. 최종 Model
2개의 input
- (1) X : (batch size, node 개수, window size)
- (2) edge index : (batch size, 2, edge 개수)
class GDN(nn.Module):
def __init__(self, edge_index_sets, num_node, embed_dim=64,
out_layer_inter_dim=256, input_dim=10, out_layer_num=1, topk=20):
#----------------------------------------------------------#
super(GDN, self).__init__()
#----------------------------------------------------------#
device = get_device()
#----------------------------------------------------------#
self.edge_index_sets = edge_index_sets
self.num_node = num_node
self.embed_dim = embed_dim
self.out_layer_inter_dim = out_layer_inter_dim
self.input_dim = input_dim
self.out_layer_num = out_layer_num
self.topk = topk
#----------------------------------------------------------#
num_edge_set = len(self.edge_index_sets)
#----------------------------------------------------------#
self.embedding = nn.Embedding(num_node, embed_dim)
self.bn_outlayer_in = nn.BatchNorm1d(embed_dim)
self.node_embedding = None
self.learned_graph = None
self.gnn_layers = nn.ModuleList([
GNNLayer(input_dim, embed_dim,
inter_dim=embed_dim*2, heads=1) for i in range(num_edge_set)])
self.out_layer = OutLayer(dim*num_edge_set, out_layer_num,
inter_num = out_layer_inter_dim)
self.dp = nn.Dropout(0.2)
#----------------------------------------------------------#
self.cache_edge_index_sets = [None] * num_edge_set
self.cache_embed_index = None
self.init_params()
def init_params(self):
nn.init.kaiming_uniform_(self.embedding.weight, a=math.sqrt(5))
def forward(self, data, org_edge_index):
x = data.clone().detach()
device = data.device
batch_size, num_node, all_feature = x.shape # (batch_size, node 개수, window size)
x = x.view(-1, all_feature).contiguous() # (batch_size*node 개수, window size)
gcn_outs = []
for i, edge_index in enumerate(self.edge_index_sets):
# edge_index : (2,702) ....... 702개의 edge pair
num_edge = edge_index.shape[1] # 702
#---------------------------------------------------------------------#
# (1) batch_edge_index
#---------------------------------------------------------------------#
### = edge 모음 ( (2, 702 x bs) )
### = bs개만큼 복제한 것
cache_edge_index = self.cache_edge_index_sets[i]
if cache_edge_index is None or cache_edge_index.shape[1] != num_edge*batch_size:
self.cache_edge_index_sets[i] = get_batch_edge_index(edge_index, batch_size, num_node).to(device)
batch_edge_index = self.cache_edge_index_sets[i]
#---------------------------------------------------------------------#
# (2) node별 임베딩 벡터 가져오기
#---------------------------------------------------------------------#
### 1) all_embeddings : NODE들의 임베딩 ...... ( node개수, embed_dim ) -> ( bs x node개수, embed_dim )
### 2) weights : NODE들의 임베딩 ........... ( node개수, embed_dim )
### 3) weights_norm : weights의 norm ........ ( node개수, 1 )
all_embeddings = self.embedding(torch.arange(num_node).to(device)) # node 0~26의 embedding을 table에서 인덱스를 사용하여 가져옴
weights = all_embeddings.detach().clone()
all_embeddings = all_embeddings.repeat(batch_size, 1)
weights = weights.view(num_node, -1)
weights_norm = weights.norm(dim=-1).view(-1,1)
#---------------------------------------------------------------------#
# (3) 유사도 계산하기
#---------------------------------------------------------------------#
### cos_ji_mat : embedding 내적으로 계산한 cosine similarity
## 아래의 세개의 matrix 전부 ........ ( node 개수, node 개수 )
cos_ji_mat = torch.matmul(weights, weights.T)
normed_mat = torch.matmul(weights_norm, weights_norm.T)
cos_ji_mat = cos_ji_mat / normed_mat
#---------------------------------------------------------------------#
# (4) 유사도 높은 상위 k개 노드 고르기
#---------------------------------------------------------------------#
### topk_indices_ji : ( node개수, top k )
topk_indices_ji = torch.topk(cos_ji_mat, self.topk, dim=-1)[1]
self.learned_graph = topk_indices_ji
### gated_i : ( 1, node개수 x top k )
### gated_j : ( 1, node개수 x top k )
### gated_edge_index : ( 2, node개수 x top k )
### batch_gated_edge_index : ( 2, node개수 x top k x bs )
gated_i = torch.arange(0, num_node).T.unsqueeze(1).repeat(1, self.topk).flatten().to(device).unsqueeze(0)
gated_j = topk_indices_ji.flatten().unsqueeze(0)
gated_edge_index = torch.cat((gated_j, gated_i), dim=0)
batch_gated_edge_index = get_batch_edge_index(gated_edge_index, batch_size, num_node).to(device)
# [ gnn_layers에 들어가는 input ]
## 1) x : (batch_size x node 개수, window size)
## 2) batch_gated_edge_index : ( 2, node개수 x top k x bs )
## 3) num_node : 스칼라 ( num_node x batch_size )
## 4) embedding : ( bs x node개수, embed_dim )
gcn_out = self.gnn_layers[i](x,
batch_gated_edge_index,
num_node=num_node*batch_size,
embedding=all_embeddings)
gcn_outs.append(gcn_out)
x = torch.cat(gcn_outs, dim=1)
x = x.view(batch_size, num_node, -1)
indexes = torch.arange(0,num_node).to(device)
out = torch.mul(x, self.embedding(indexes))
out = out.permute(0,2,1)
out = F.relu(self.bn_outlayer_in(out))
out = out.permute(0,2,1)
out = self.dp(out)
out = self.out_layer(out)
out = out.view(-1, num_node)
return out