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로 설정 했을때
      • 생성되는 시계열 부분부분들
  • (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
        

Categories: ,

Updated: