# Kobert Tokenize
from KoBERT.kobert_hf.kobert_tokenizer import KoBERTTokenizer
kobert_tokenizer = KoBERTTokenizer.from_pretrained('skt/kobert-base-v1')

UNK_IDX,PAD_IDX, CLS_IDX, SEP_IDX, MASK_IDX = 0, 1, 2, 3, 4

def yield_tokens(data):
  for data_sample in tqdm(data):
    yield kobert_tokenizer.tokenize(data_sample)

special_symbols = ['[PAD]','[UNK]', '[CLS]', '[SEP]', '[MASK]']

vocab_transform = build_vocab_from_iterator(yield_tokens(trainset['input']),min_freq=1,specials=special_symbols,special_first=True)

vocab_transform.set_default_index(UNK_IDX)


# Sentence Piece Tokenize
with open('nlp.txt', 'w', encoding='utf8') as f:
    f.write('\n'.join(trainset['input']))

corpus = 'nlp.txt'
prefix = "nlp"
vocab_size = 10000
spm.SentencePieceTrainer.train(
    f"--input={corpus} --model_prefix={prefix} --vocab_size={vocab_size + 5}" + 
    " --model_type=unigram" + # unigram (default), bpe, char, word
    " --max_sentence_length=9999" +
    " --pad_id=0 --pad_piece=[PAD]" + 
    " --unk_id=1 --unk_piece=[UNK]" + 
    " --bos_id=2 --bos_piece=[BOS]" + 
    " --eos_id=3 --eos_piece=[EOS]" +
    " --user_defined_symbols=[MASK]") 


vocab_list = pd.read_csv('nlp.vocab', sep='\t', header=None)
vocab_list.reset_index(inplace=True)

sp = spm.SentencePieceProcessor()
vocab_file = "nlp.model"
sp.load(vocab_file)

PAD_IDX, UNK_IDX, BOS_IDX, EOS_IDX, MASK_IDX,   = 0, 1, 2, 3, 4

def yield_tokens(data):
  for data_sample in tqdm(data):
    yield sp.encode_as_pieces(data_sample)

special_symbols = ['[PAD]','[UNK]', '[BOS]', '[EOS]', '[MASK]']

vocab_transform = build_vocab_from_iterator(yield_tokens(trainset['input']),min_freq=1,specials=special_symbols,special_first=True)

vocab_transform.set_default_index(UNK_IDX)


# Mecab Tokenize
UNK_IDX,PAD_IDX, BOS_IDX, EOS_IDX, MASK_IDX = 0, 1, 2, 3, 4

def yield_tokens(data):
  for data_sample in tqdm(data):
    yield mecab.morphs(data_sample)

special_symbols = ['[UNK]','[PAD]', '[BOS]', '[EOS]','[MASK]']

vocab_transform = build_vocab_from_iterator(yield_tokens(trainset['input']),min_freq=1,specials=special_symbols,special_first=True)

vocab_transform.set_default_index(UNK_IDX)
# Make dataset

# input sequence tensor 변환
def input_tensor_transform(token_ids):
  return torch.cat((torch.tensor(CLS_IDX).reshape(-1),token_ids.reshape(-1),torch.tensor(SEP_IDX).reshape(-1)),dim = 0)

# Tensor Dataset 생성
def make_dataset(data, method, max_len,mask):
  if method == 'sentence_piece':
    X_batch = [torch.tensor(vocab_transform(sp.encode_as_pieces(x))) for x in data['input']]
    
  elif method == 'kobert':
    X_batch = [torch.tensor(vocab_transform(kobert_tokenizer.tokenize(x))) for x in data['input']]
  
  elif method == 'Mecab':
    X_batch = [torch.tensor(vocab_transform(mecab.morphs(x))) for x in data['input']]
  
  
  if mask==True:
    X_batch_mask=[]
    
    for text in X_batch:
      if len(text) >= 20:
        input_mask = torch.FloatTensor(len(text)).uniform_() >= 0.85
        X_batch_mask.append(torch.tensor([UNK_IDX if input_mask[i]==True else x  for i, x in enumerate(text)]))
      else:
        X_batch_mask.append(torch.tensor(text))
    # X_batch = [input_tensor_transform(x) for x in X_batch_mask]
    X_batch = torch.tensor(pad_sequences(X_batch, maxlen=max_len, dtype='long', padding='post', truncating='post', value=PAD_IDX))
    y_batch = torch.tensor(data['label']).long()

  else:
    X_batch = [input_tensor_transform(x) for x in X_batch]
    X_batch = torch.tensor(pad_sequences(X_batch, maxlen=max_len, dtype='long', padding='post', truncating='post', value=PAD_IDX))
    y_batch = torch.tensor(data['label']).long()

  return TensorDataset(X_batch,y_batch)
# Transformer Encoder

class EncoderBlock(nn.Module):
  def __init__(self, embed_dim, num_heads, ffn_dim, dropout):
    super(EncoderBlock, self).__init__()
    self.self_att = nn.MultiheadAttention(embed_dim=embed_dim, num_heads=num_heads,batch_first=True)
    self.ffn = nn.Sequential(nn.Linear(embed_dim,ffn_dim), nn.ReLU(), nn.Linear(ffn_dim, embed_dim))
    self.layernorm1 = nn.LayerNorm(embed_dim,eps=1e-5)
    self.layernorm2 = nn.LayerNorm(embed_dim,eps=1e-5)
    self.dropout1 = nn.Dropout(dropout)
    self.dropout2 = nn.Dropout(dropout)


  #forward(self, query: Tensor, key: Tensor, value: Tensor, key_padding_mask: Optional[Tensor] = None, need_weights: bool = True, attn_mask: Optional[Tensor] = None) -> Tuple[Tensor, Optional[Tensor]]:
  def forward(self, input, method, key_padding_mask):
    if method == 'pre_LN':
      norm_input = self.layernorm1(input)
      
      attn_output, _ = self.self_att(norm_input, norm_input, norm_input, key_padding_mask=key_padding_mask)
      attn_output = self.dropout1(attn_output)
      input2 = input + attn_output
      norm_input2 = self.layernorm2(input2)
      output = self.ffn(norm_input2)
      output = self.dropout2(output)
      return input2+output

    elif method == 'post_LN':
      
      attn_output,_ = self.self_att(input, input, input, key_padding_mask=key_padding_mask)
      attn_output = self.dropout1(attn_output)
      input2 = self.layernorm1(input + attn_output)
      output = self.dropout2(self.ffn(input2))
      return self.layernorm2(input2 + output)
class TransformerClassifier(nn.Module):
  def __init__(self,input_vocab_size,embed_dim,num_heads,ffn_dim, N, dropout, hid_dim,output_dim,max_len,method):
    super(TransformerClassifier,self).__init__()
    self.input_tok_emb = TokenEmbedding(input_vocab_size, embed_dim)
    self.positional_encoding = PositionalEncoding(embed_dim)
  
    self.encoder_layers_lstm = clones(LSTMEncoderBlock(embed_dim = embed_dim,num_heads = num_heads,ffn_dim = ffn_dim, dropout=dropout),N)
    self.encoder_layers_cnn = clones(CNNEncoderBlock(embed_dim = embed_dim,num_heads = num_heads,ffn_dim = ffn_dim, dropout=dropout,kernel_size=3,padding=1),N)
    self.encoder_layers = clones(EncoderBlock(embed_dim = embed_dim,num_heads = num_heads,ffn_dim = ffn_dim, dropout=dropout),N)
    
    self.linear1 = nn.Linear(embed_dim,hid_dim)
    self.linear2 = nn.Linear(hid_dim,output_dim)
    self.linear3 = nn.Linear(hid_dim*2,output_dim)
    self.relu = nn.ReLU()
    self.lstm = nn.LSTM(embed_dim, hid_dim, batch_first=True,bidirectional=True)
    self.method = method
    
  def forward(self, input, input_padding_mask):
    
    if self.method=='lstm':
      x = self.positional_encoding(self.input_tok_emb(input))
      for layer in self.encoder_layers_lstm:
        x = layer(x,'ver_1', input_padding_mask)

      x = self.lstm(x)
      x = self.linear3(x[0][:,-1,:])
      return x

    elif self.method=='cnn':
      x = self.positional_encoding(self.input_tok_emb(input))
      for layer in self.encoder_layers_cnn:
        x = layer(x, input_padding_mask)
      
      x = x[:,0,:]
      x = self.relu(self.linear1(x))
      x = self.linear2(x)
      return x

    else:
      x = self.positional_encoding(self.input_tok_emb(input))
      for layer in self.encoder_layers:
        x = layer(x,'post_LN', input_padding_mask)

      x = x[:,0,:]
      x = self.relu(self.linear1(x))
      x = self.linear2(x)
      return x
# Train, Eval

def train_epoch(model, optimizer):
  model.train()
  losses = 0
  total_acc, total_count = 0, 0
  train_dataloader = DataLoader(torch_trainset, batch_size=batch_size, shuffle = True,drop_last = False)

  for input,label in train_dataloader:
    
    input = input.to(DEVICE)
    label = label.to(DEVICE)
    input_padding_mask = create_mask(input)
    logits = model(input,input_padding_mask)
    # grad 초기화
    optimizer.zero_grad()
    # cross entropy loss
    loss = criterion(logits, label)
    # accuracy 
    total_acc += (logits.argmax(-1) == label).sum().item()
    total_count += label.size(0)
    
    loss.backward()
    optimizer.step()
    losses += loss.item()
    
  return losses/len(train_dataloader), total_acc/total_count


def evaluate(model):
  model.eval()
  losses = 0
  total_acc, total_count = 0, 0
  valid_dataloader = DataLoader(torch_validset, batch_size=batch_size, shuffle = True,drop_last = False)

  for input,label in valid_dataloader:
    input = input.to(DEVICE)
    label = label.to(DEVICE)
    input_padding_mask = create_mask(input)
    logits = model(input,input_padding_mask)

    loss = criterion(logits, label)
    total_acc += (logits.argmax(-1) == label).sum().item()
    total_count += label.size(0)
    
    losses += loss.item()
      
  return losses/len(valid_dataloader), total_acc/total_count
# Exec

# Parameter setting
import math
torch.manual_seed(0)

batch_size = 512
embed_dim = 256
num_heads = 4

ffn_dim = 256
N =3
dropout = 0.1 
hid_dim = 256
output_dim = len(train['label'].unique())

max_len = max(len(x) for x in [sp.encode_as_ids(x) for x in trainset['input']])

input_vocab_size = len(vocab_transform)

tokenize_method = 'sentence_piece' # 'sentence_piece', 'kobert', 'Mecab'

print('vocab_size : ',input_vocab_size)
print('max_len : ',max_len)

torch_trainset = make_dataset(trainset, tokenize_method, max_len,mask=False)
torch_validset = make_dataset(validset, tokenize_method, max_len,mask=False)


# Model Setting
classifier = TransformerClassifier(input_vocab_size,embed_dim, num_heads,
                                   ffn_dim, N, dropout, hid_dim, output_dim,max_len,method='lstm')

classifier.to(DEVICE)

for p in classifier.parameters():
    if p.dim() > 1:
        nn.init.xavier_uniform_(p)

criterion = torch.nn.CrossEntropyLoss()

optimizer = torch.optim.Adam(classifier.parameters(),lr=0.0001, betas=(0.9, 0.98))

early_stopping = EarlyStopping(patience=10, verbose=True)


# Model Training
from timeit import default_timer as timer
NUM_EPOCHS = 10

for epoch in range(1, NUM_EPOCHS+1):
    start_time = timer()
    train_loss, train_acc= train_epoch(classifier, optimizer)
    end_time = timer()
    with torch.no_grad():
      valid_loss, valid_acc= evaluate(classifier)
    
    early_stopping(valid_loss, classifier)
    if early_stopping.early_stop:
      print("Early stopping")
      break
    print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, Train Acc : {train_acc:.3f} | Val loss: {valid_loss:.3f}, Val Acc : {valid_acc:.3f} | "f"Epoch time = {(end_time - start_time):.3f}s"))

 

 


• 저자 :M. Lewis, Y. Liu, N. Goyal, M. Ghazvininejad, A. Mohamed, O. Levy, V. Stoyanov, L. Zettlemoyer

 

 

 

 

1. Introduction

Masked Language Model(MLM)을 통한 denoising autoencoder로 NLP 태스크에서 Self-supervised 방법들이 엄청난 성공을 거두게 되었다. 하지만 이러한 방법은 특정 태스크에 집중하여 적용할 수 있는 분야가 한정되는 단점이 존재한다.

본 논문에서는 Seq2Seq 구조의 denoising autoencoder 모델을 제안하여 적용 가능한 end task의 범위를 넓히고자 하였다. Encoder에서 bidirectional한 정보를 학습하고 decoder에서 원본 텍스트를 생성하는 구조로 구성된다. 이러한 학습을 통해 fine tuning시 text generation 부터 comprehension task 까지 현재 SOTA격인 RoBERTa와 비슷한 성능을 보인다고 제시한다.

 

 

 

2. Model

❒ Architecture

앞서 언급했듯이 BART는 bidirectional encoder와 left-to-right autoregressive decoder로 구성된 Seq2Seq 모델이다.(Base model : 6 Layers / Large model : 12 Layers)

기존 GPT와의 차이점으로는 GPT는 activation function이 ReLU가 사용됐지만, BART에서는 GELUs 가 활용되었고 모델 초기화 파라미터를 모델 N(0, 0.02)으로 설정했다. 그 외의 구조는 Transformer NMT Base 모델 구조와 동일하게 encoder, decoder 간의 cross-attention이 매 encoder layer에서 일어난다.

또한 BERT는 최종 word-prediction을 위해 추가로 feed-forward layer가 필요했지만, BART는 그렇지 않다는 점에서 차이가 존재하고, 같은 수의 레이어일 때, BERT보다 약 10% 많은 파라미터 수를 갖는다.

 

 Pre-training

 

• Pre training 을 위한 5가지 noise 성능 비교

 

1) Token Masking: BERT처럼 랜덤 토큰을 masking하고 이를 복구하는 방식

 

2) Token Deletion: 랜덤 토큰을 삭제하고 이를 복구하는 방식. 토큰 복구뿐만 아니라 삭제된 위치까지 찾도록 학습

 

3) Text Infilling: 포아송 분포를 따르는 길이의 text span을 생성해서 이를 하나의 mask 토큰으로 masking. 
(SpanBERT 에 영감을 받았으나, [MASK] token의 SBO를 맞추는 것이 아닌 token의 숫자를 예측한다는 점이 다름)

[참고] SPAN BERT Span Boundary Objective(SBO)

4) Sentence Permutation: 여러개의 Document를 문장 단위로 나눠서 섞는 방법

 

5) Document Rotation: 하나의 token을 뽑은 후, 그 token을 시작점으로 회전함 → 문서의 start point를 찾도록 학습

 

 

5가지 Noise 방법

 

 

 

3. Fine Tuning

 Sequence Classification Tasks

동일한 토큰이 encoder와 decoder의 input으로 적용되고 decoder에서의 마지막 hidden state가 classifier layer를 거쳐 Classification이 수행된다(아래 첫번째 그림). BERT의 첫번째 CLS 토큰을 활용하여 Classification을 수행하는 것과 비슷하지만(아래 두번째 그림), BART의 경우 모든 문맥정보의 attention을 반영한 최종 hidden state값을 활용할 수 있다.

(Token Classification Task의 경우 최종 hidden state가 아닌, 각 토큰별 hidden state값을 classification에 활용)

 

BERT Classification

 

  Sequence Generation Tasks

BART는 auto regressive 디코더를 통해 직접적인 fine-tuning이 가능하다. 대표적인 sequence generation task인 abstract QA, summarization와 같은 경우, encoder의 입력으로 input sequence가 들어가고 decoder에서 출력이 생성된다.

 

  Machine Translation Tasks

기계 번역 태스크에서는 encoder 부분에서의 약간의 변형이 있다. Embedding layer를 랜덤하게 Initialized parameter 기반의 encoder로 대체되며 이 encoder layer가 다른 외국어를 원래의 언어로 de-noise할 수 있는 입력으로 만들도록 학습된다. 이 새로운 encoder는 두단계로 학습하는데 두 방법 모두 cross-entropy loss로 backpropagate 한다. 처음에는 대부분의 BART 파라미터는 freeze 시키고 인코더와 BART의 position embedding, encoder의 첫번째 레이어 self-attention input projection matrix만 학습시키며, 두번째 단계에서는 모든 파라미터를 원래대로 학습시킨다.

 

 

 

 

4. Results

BART를 활용한 기타 SOTA 모델들과의 성능비교 부분을 특히 중요한 부분에 대해서 간략하게 정리했다. BART는 RoBERTa와 동일한 160GB의 학습데이터를 사용하여 Pre training을 거쳤으며, 언어이해 태스크에서 RoBERTa와 비슷한 수준의 성능을 보였다. NLU task인 기계독해 SQuAD v1.0, v2.0 모두 유사한 F1 score를 보였다. 또한, BERT나 RoBERTa에서는 적용에 제한이 되는 NLG Tasks에서도 SOTA 모델과 유사한 성능을 보였다. 언어 생성 태스크인 XSum 요약 태스크에서는 선행 연구 최고 성능인 31.27 Rouge-L 점수보다 5.98점 높은 37.25 Rouge-L 점수를 획득하였다.

 

 

 

TRANS-BLSTM를 활용한 Text Classification (KLUE Dataset) 실험

  • 이전에 논문 리뷰 글을 올렸던 TRANS-BLSTM 구조를 코드로 구현해보고 한국어 뉴스 기사 타이틀로 7개의 카테고리를 분류하는 Classification Task에 적용하여 성능비교를 진행하였다.

 

❒ Code

1. Tokenize

  • Sentence Piece 
    : vocab size를 6000으로 설정하고 unigram 기반으로 sentence piece tokenizer를 학습하여 활용
with open('nlp.txt', 'w', encoding='utf8') as f:
    f.write('\n'.join(trainset['input']))

corpus = 'nlp.txt'
prefix = "nlp"
vocab_size = 6000
spm.SentencePieceTrainer.train(
    f"--input={corpus} --model_prefix={prefix} --vocab_size={vocab_size + 5}" + 
    " --model_type=unigram" + # unigram (default), bpe, char, word
    " --max_sentence_length=9999" +
    " --pad_id=0 --pad_piece=[PAD]" +
    " --unk_id=1 --unk_piece=[UNK]" +
    " --bos_id=2 --bos_piece=[BOS]" +
    " --eos_id=3 --eos_piece=[EOS]" +
    " --user_defined_symbols=[MASK]")


vocab_list = pd.read_csv('nlp.vocab', sep='\t', header=None)
vocab_list.reset_index(inplace=True)

sp = spm.SentencePieceProcessor()
vocab_file = "nlp.model"
sp.load(vocab_file)

 

 

 

2. Encoder

  • Vanilla Transformer Encoder
    : 초기 제안되었던 Post-LN 방식과 최근 주로 활용된다고 하는 Pre-LN 방식 포함

    class EncoderBlock(nn.Module):
      def __init__(self, embed_dim, num_heads, ffn_dim, dropout):
        super(EncoderBlock, self).__init__()
        self.self_att = nn.MultiheadAttention(embed_dim=embed_dim, num_heads=num_heads,batch_first=True)
        self.ffn = nn.Sequential(nn.Linear(embed_dim,ffn_dim), nn.ReLU(), nn.Linear(ffn_dim, embed_dim))
        self.layernorm1 = nn.LayerNorm(embed_dim,eps=1e-5)
        self.layernorm2 = nn.LayerNorm(embed_dim,eps=1e-5)
        self.dropout1 = nn.Dropout(dropout)
        self.dropout2 = nn.Dropout(dropout)
      
    
      def forward(self, input, method, key_padding_mask):
        if method == 'pre_LN':
          norm_input = self.layernorm1(input)
          attn_output, _ = self.self_att(norm_input, norm_input, norm_input, key_padding_mask=key_padding_mask)
          attn_output = self.dropout1(attn_output)
          input2 = input + attn_output
          norm_input2 = self.layernorm2(input2)
          output = self.ffn(norm_input2)
          output = self.dropout2(output)
          return input2+output
    
        elif method == 'post_LN':
          attn_output,_ = self.self_att(input, input, input, key_padding_mask=key_padding_mask)
          attn_output = self.dropout1(attn_output)
          input2 = self.layernorm1(input + attn_output)
          output = self.dropout2(self.ffn(input2))
          return self.layernorm2(input2 + output)



  • Transformer Encoder + Bidirectional LSTM
    : TRANS-BLSTM 논문에서 제안된 아키텍쳐 구현
  • TRANS-BLSTM: Transformer with Bidirectional LSTM for Language Understanding
class LSTMEncoderBlock(nn.Module):
  def __init__(self, embed_dim, num_heads, ffn_dim, dropout):
    super(LSTMEncoderBlock, self).__init__()
    self.self_att = nn.MultiheadAttention(embed_dim=embed_dim, num_heads=num_heads,batch_first=True)
    self.ffn = nn.Sequential(nn.Linear(embed_dim,ffn_dim), nn.ReLU(), nn.Linear(ffn_dim, embed_dim))
    self.dropout1 = nn.Dropout(dropout)
    self.dropout2 = nn.Dropout(dropout)
    self.lstm = nn.LSTM(embed_dim, embed_dim, batch_first=True,bidirectional=True)
    self.linear = nn.Linear(embed_dim*2,embed_dim)
    self.layernorm1 = nn.LayerNorm(embed_dim,eps=1e-5)
    self.layernorm2 = nn.LayerNorm(embed_dim,eps=1e-5)
    
  def forward(self, input, method, key_padding_mask):
    if method == 'ver_1':
      attn_output,_ = self.self_att(input, input, input, key_padding_mask=key_padding_mask)
      attn_output = self.dropout1(attn_output)
      output1 = self.layernorm1(input + attn_output)
      output2 = self.lstm(output1)
      output2 = self.linear(output2[0])
      return self.layernorm2(output1 + output2)

    elif method == 'ver_2':
      attn_output,_ = self.self_att(input, input, input, key_padding_mask=key_padding_mask)
      attn_output = self.dropout1(attn_output)
      output1 = self.layernorm1(input + attn_output)
      output2 = self.dropout2(self.ffn(output1))
      output3 = self.lstm(input)
      output3 = self.linear(output3[0])
      return self.layernorm2(output1 + output2+ output3)

 

 

  • Transformer Encoder + CNN
    : LSTM이 아닌 CNN을 Encoder 구조에 적용
class CNNEncoderBlock(nn.Module):
  def __init__(self, embed_dim, num_heads, ffn_dim, dropout,kernel_size,padding):
    super(CNNEncoderBlock, self).__init__()
    self.self_att = nn.MultiheadAttention(embed_dim=embed_dim, num_heads=num_heads,batch_first=True)
    self.ffn = nn.Sequential(nn.Linear(embed_dim,ffn_dim), nn.ReLU(), nn.Linear(ffn_dim, embed_dim))
    self.layernorm1 = nn.LayerNorm(embed_dim,eps=1e-5)
    self.layernorm2 = nn.LayerNorm(embed_dim,eps=1e-5)
    self.dropout1 = nn.Dropout(dropout)
    self.dropout2 = nn.Dropout(dropout)
    self.linear = nn.Linear(embed_dim*2,embed_dim)
    self.cnn_1 = nn.Conv1d(embed_dim, embed_dim,kernel_size= kernel_size,padding=padding)


  def forward(self, input, key_padding_mask):
    attn_output,_  = self.self_att(input, input, input, key_padding_mask=key_padding_mask)
    attn_output = self.dropout1(attn_output)
    input1 = self.layernorm1(input + attn_output)
    input2 = self.cnn_1(input1.transpose(-1,-2).contiguous()).transpose(-1,-2).contiguous()
    output = self.dropout2(input2)
    return self.layernorm2(input2 + output)

 

 

3. Final Modeling

  • Positional Encoding
class PositionalEncoding(nn.Module):
    def __init__(self,emb_size: int,maxlen: int = 128,dropout=0.1):
        super(PositionalEncoding, self).__init__()
        den = torch.exp(- torch.arange(0, emb_size, 2)* math.log(10000) / emb_size)
        pos = torch.arange(0, maxlen).reshape(maxlen, 1)
        pos_embedding = torch.zeros((maxlen, emb_size))
        pos_embedding[:, 0::2] = torch.sin(pos * den)
        pos_embedding[:, 1::2] = torch.cos(pos * den)
        pos_embedding = pos_embedding.unsqueeze(0)
        self.register_buffer('pos_embedding', pos_embedding)
        self.dropout = nn.Dropout(dropout)

    def forward(self, token_embedding):
        return self.dropout(token_embedding + self.pos_embedding[:,:token_embedding.size(1), :])


class TokenEmbedding(nn.Module):
    def __init__(self, vocab_size: int, emb_size):
        super(TokenEmbedding, self).__init__()
        self.embedding = nn.Embedding(vocab_size, emb_size)
        self.emb_size = emb_size

    def forward(self, tokens):
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)

 

  • Classifier(Decoder)
    1) TRANS-BLSTM의 경우, 논문에서 언급한대로 BiLSTM Decoder로 적용하여 최종 output을 산출
    2) 이외의 경우, 아래 이미지와 같이 sequence representation 정보를 aggregate되도록 학습된 첫번째 'CLS' 토큰 정보를 활용
    Text Classification
class TransformerClassifier(nn.Module):
  def __init__(self,input_vocab_size,embed_dim,num_heads,ffn_dim, N, dropout, hid_dim,output_dim,max_len,method):
    super(TransformerClassifier,self).__init__()
    self.input_tok_emb = TokenEmbedding(input_vocab_size, embed_dim)
    self.positional_encoding = PositionalEncoding(embed_dim)
  
    self.encoder_layers_lstm = clones(LSTMEncoderBlock(embed_dim = embed_dim,num_heads = num_heads,ffn_dim = ffn_dim, dropout=dropout),N)
    self.encoder_layers_cnn = clones(CNNEncoderBlock(embed_dim = embed_dim,num_heads = num_heads,ffn_dim = ffn_dim, dropout=dropout,kernel_size=3,padding=1),N)
    self.encoder_layers = clones(EncoderBlock(embed_dim = embed_dim,num_heads = num_heads,ffn_dim = ffn_dim, dropout=dropout),N)
    
    self.linear1 = nn.Linear(embed_dim,hid_dim)
    self.linear2 = nn.Linear(hid_dim,output_dim)
    self.linear3 = nn.Linear(hid_dim*2,output_dim)
    self.relu = nn.ReLU()
    self.lstm = nn.LSTM(embed_dim, hid_dim, batch_first=True,bidirectional=True)
    self.method = method
    
  def forward(self, input, input_padding_mask):
    
    if self.method=='lstm':
      x = self.positional_encoding(self.input_tok_emb(input))
      for layer in self.encoder_layers_lstm:
        x = layer(x,'ver_1', input_padding_mask)

      x = self.lstm(x)
      x = self.linear3(x[0][:,-1,:])
      return x

    elif self.method=='cnn':
      x = self.positional_encoding(self.input_tok_emb(input))
      for layer in self.encoder_layers_cnn:
        x = layer(x, input_padding_mask)
      
      x = x[:,0,:]
      x = self.relu(self.linear1(x))
      x = self.linear2(x)
      return x

    else:
      x = self.positional_encoding(self.input_tok_emb(input))
      for layer in self.encoder_layers:
        x = layer(x,'post_LN', input_padding_mask)

      x = x[:,0,:]
      x = self.relu(self.linear1(x))
      x = self.linear2(x)
      return x

 

 

❒ Result

3가지 방식의 Encoder 아키텍쳐(Vanilla Transformer, Encoder+LSTM, Encoder+CNN)를 적용하여 성능 비교를 진행하였다.

하이퍼 파라미터는 Batch size : 256 / 임베딩 차원 : 256 / 헤드 수 : 4 / FFN 차원 : 512 / LSTM, CNN hidden dim : 256 로 설정하고 Encoder Layer 수를 늘려가면서(2~6) F1-Score를 비교하였다. (5fold CV로 스코어 측정)

그 결과 아래 표와 같이 스코어가 산출되었으며, CNN을 적용한 Encoder 모델은 가장 낮은 성능을 보였다. Amazon AWS AI에서 제안한 TRANS-BLSTM 모델이 가장 높은 스코어를 보였지만 5개 레이어 이상부터 급격히 감소하는 모습을 보였다. 이는 데이터셋의 Input Sequence(뉴스 타이틀)가 짧으며, 매 Layer 마다 Bidirectional LSTM 연산과 Self Attention이 포함되어서 출력되는 sentence representation이 오히려 왜곡되어 학습됐지 않을까 생각되었다. 

 

 

 

  • 저자 : Zhiheng Huang, Peng Xu, Davis Liang, Ajay Mishra, Bing Xiang
  • 논문 : https://arxiv.org/abs/2003.07000

- 본 논문은 Amazon AWS AI 에서 Transformer의 Encoder에 Bidirectional LSTM을 적용한 새로운 아키텍쳐를 제안한 논문이다. 

2020년 게재되고 인용 수는 적지만 기존 Encoder 구조에 다른 모델을 적용한 점이 흥미로워 논문리뷰와 코드구현&실습까지 진행해 보았다.

 

 

❒  Introduction

BERT 등장이전에는 BLSTM을 기반으로 한 machine translation과 speech recognition 모델이 sota를 기록해왔다.

또한 다양한 선행연구에서 BERT의 hidden layer size를 단순히 늘리는 것은 모델 성능에 도움을 주지 않는다는 한계점을 제시했다.

따라서 BERT에서 활용한 transformer encoder 구조와 BLSTM을 결합하면 성능을 더욱 높일 수 있을 것이라는 가정에서 이 두 모델을 결합하려는 시도를 하였다고 한다.

그 결과, 본 논문에서 BERT의 성능을 강화하기 위해 Transformer Encoder와 Bidirectional LSTM(BLSTM) 아키텍쳐를 결합하여 Trans-BLSTM 모델을 새로 제안하였다.

본 논문의 주요 기여점은 transformer와 BLSTM을 하나의 단일 모델로 결합하여 SQuAD, GLUE NLP 벤치마크 데이터셋에서 BERT baseline 성능을 높였다는 점을 들 수 있다.

 

 

 TRANS-BLSTM Architectures

TRANS-BLSTM은 BERT와 마찬가지로 Encoder만 활용하여 hidden representation을 추출하고 linear layer, softmax layer를 거치는 아키텍쳐를 기반으로 한다. 기존 실험 결과, transformer model의 단일 distillation BLSTM 모델은 굉장히 낮은 성능을 보였다. 따라서 BLSTM을 transformer와 결합하여 sequence modeling에서 상호 보완적 효과를 통해 다양한 downstream task에서 성능을 향상시키는지 실험을 진행하였다. 본 논문에서 제안한 2가지 아키텍쳐는 아래와 같다.

 

 

첫번째는 기존 feedforward layer를 BLSTM layer로 대체한 구조이며, 두번째는 동일한 encoder 방식에 BLSTM layer를 통해 추출된 representation을 포함하여 add&norm을 거치는 구조이다. BLSTM의 output은 embed dim *2 이기 때문에 두가지 아키텍쳐 모두 Linear layer를 통해 다시 embed dim으로 맞추어서 최종 add&norm에 활용된다는 공통점이 있다.

추가적으로 본 논문에서는, Decoder(Classifier) 부분 또한 Linear layer가 아닌 Bi-LSTM layer로 대체하여, downstream task fine tuning시 sequential prediction을 향상시킬 수 있다고 한다.
Encoder에 BLSTM을 결합함으로써 pre-training을 위한 maked language model, Next sentence prediction 학습에 도움을 준다고 논문에서 말하고 있다. Pre-training을 위한 loss function은 BERT와 동일하게 MLM, NSP를 활용하였다.

 

 

  Experiment

BERT와의 성능 비교를 위해 BERT에서 활용한 동일 데이터를 pre-training을 위해 활용하였고, tokenize와 학습 방식도 모두 동일하게 하였다. (memory consumption을 줄이기 위해 max length는 512 → 256으로 줄임)

 

SQuAD Test results

SQuAD Dataset 실험 결과, ALBERT가 SOTA 성능을 보였으며 본 논문에서 제안한 TRANS-BLSTM 모델(decoder 부분 또한 BLSTM 적용)이 유사한 성능을 보이고 있음을 알 수 있다.

 

GLUE Benchmark Test results

GLUE Benchmark 실험에서도 BERT보다 대부분 더 높은 스코어를 보이고 있음을 알 수 있다.

 

 

 Conclusion

LSTM 아케텍쳐를 transformer와 결합하여 self-attention에서 sequence 정보를 더 정확하게 학습을 할 수 있게되고 단순히 layer를 여러개 쌓아서 더 성능이 좋아지지 않는 BERT의 한계점을 보완한다는 점에서 흥미롭게 보았다.

하지만 transformer의 주요 기여점 중 하나인 병렬 연산을 통한 학습 시간 단축의 장점을 소실했다는 점을 한계로 들 수 있을 것 같다.

 

추가적으로, TRANS-BLSTM 모델을 직접 구현해보기 위해 김성훈 교수님이 대표로 있는 AI 스타트업 업스테이지에서 공개한 KLUE 데이터를 기반으로 자체적인 text classfication 코드 구현을 해보았다. Pre-training 없이 바로 supervised-learning을 통해 vanilla transformer encoder 아키텍쳐와 TRANS-BLSTM뿐만이 아닌 cnn을 적용한 transformer encoder 아키텍쳐를 구현해보고 성능 비교를 한 내용을 별도로 정리하였다.  (https://hoonsnote.tistory.com/13)

대회 설명

7일(Day 0~ Day6) 동안의 데이터를 인풋으로 활용하여, 향후 2일(Day7 ~ Day8) 동안의 30분 간격의 발전량(TARGET)을 예측 (1일당 48개씩 총 96개 타임스텝에 대한 예측)

 

데이터 구성

Hour - 시간
Minute - 분
DHI - 수평면 산란일사량(Diffuse Horizontal Irradiance (W/m2))
DNI - 직달일사량(Direct Normal Irradiance (W/m2))
WS - 풍속(Wind Speed (m/s))
RH - 상대습도(Relative Humidity (%))
T - 기온(Temperature (Degree C))
Target - 태양광 발전량 (kW)

 

모델링 과정

본 대회에서 Light GBM을 활용한 예측과 CNN을 활용한 예측을 진행하였다. 본 대회의 평가지표인 pinball_loss(quantile)를 최소화하는 모델 구축을 위해 두 가지 방법 모두 quantile_loss를 loss_function으로 설정하였다.
또한, 예측해야하는 2일 중 각각의 날짜를 Target값으로 설정하여 결론적으로는, 일자별(2) * 0.1~0.9quantile별(9)별로 총 18번의 학습을 통해 예측값을 구하였다.

 

Light GBM

본 대회의 데이터는 Target값인 태양광 발전량이 0인 시간대(해가 떠있지 않은 시간)가 높은 비중을 차지하고 있다.
해가 떠있는 시간대에서의 Target값에 대한 정밀한 예측값 산출을 위해 Binary Classification(0 : 발전량 X / 1 : 발전량 O) 진행 후(Validation 정확도 99% 이상), 1로 예측된 값에 대해서만 추가적인 Regression 모델링 작업을 진행하였습니다. 파생 변수로는 일출/일몰 시간, 일정 날짜 동안의 동시간대 기본 변수들의 평균값 등을 추가적으로 생성하였다.

 

 

github : https://github.com/hoonsnote/Dacon/blob/8b574e16b9deacb30cdf95569a46d5b8efea2307/Solar%20power%20generation%20forecast/1.LightGBM.ipynb




 

  • Library import 및 데이터 불러오기
import warnings
warnings.filterwarnings(action='ignore')
import pandas as pd
import numpy as np
from tqdm.notebook import tqdm
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder, StandardScaler, OneHotEncoder, RobustScaler, MinMaxScaler
import seaborn as sns
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.metrics import accuracy_score, recall_score, roc_curve, precision_score, f1_score, auc, mean_absolute_error
import matplotlib.pyplot as plt
from datetime import datetime
from sklearn.ensemble import RandomForestClassifier
import time
from sklearn.metrics import accuracy_score


filepath = '/content/drive/MyDrive/dacon/solar/'
train=pd.read_csv(filepath+'train/train.csv')

# for i in range(0,81):
#     test = "test_%d = pd.read_csv('/content/drive/MyDrive/dacon/solar/test/%d.csv')"%(i,i)
#     exec(test)

submission=pd.read_csv(filepath+'sample_submission.csv')

 

 

  • Train Set 파생변수 생성
# 일별 Target MAX 값 Column 생성 
def max_feature(data):
  max = data.groupby('Day').max()[['TARGET']].reset_index()
  data = pd.merge(data, max, on ='Day', how ='left')
  data.rename({'TARGET_x' : 'TARGET', 'TARGET_y':'max_tagrget'}, axis=1, inplace=True)
  
  return data
# 일출/일몰 시간 변수 생성
def suntime_feature(data):
  data['day_time'] = data['Hour'] * 2 + data['Minute'] * 1/30 

  no_0 = data[data['TARGET'] != 0 ]

  rise = no_0.groupby('Day').min()[['day_time']].reset_index()
  data = pd.merge(data, rise, on ='Day', how ='left')
  data.rename({'day_time_x' : 'day_time', 'day_time_y':'rising_time'}, axis=1, inplace=True)

  sunset = no_0.groupby('Day').max()[['day_time']].reset_index()
  data = pd.merge(data, sunset, on ='Day', how ='left')
  data.rename({'day_time_x' : 'day_time', 'day_time_y':'set_time'}, axis=1, inplace=True)

  return data
# 동시간대의 4, 7일 별 Target, DHI, DNI, T 평균값 생성
step = [4,7]
date_time = np.arange(0,48,1)

def mean_feature(data, feature):
  for i in tqdm(step):
    tmp_df = pd.DataFrame()
    for j in date_time:
      tmp_df = tmp_df.append(data[data['day_time'] == j].rolling(window=i).mean())[[feature]]
    data.loc[:,'mean_{}'.format(feature)+str(i)] = tmp_df.sort_index().values
    
    
def new_day(data):
  data.reset_index(inplace= True)

  data['new_day'] = None
  data['new_day'][0] = 0

  for i in tqdm(range(1, len(data))):
    if data['Day'][i] == data['Day'][i-1]:
     data['new_day'][i] = data['new_day'][i-1]
    else:
     data['new_day'][i] = data['new_day'][i-1] + 1 
  return data
# 파생변수 생성
train = max_feature(train)
train = suntime_feature(train)

feature = ['TARGET','DHI','DNI','T']
for feat in feature:
  mean_feature(train,feat)

train = new_day(train)


# 훈련용 Target 값 Columns 생성
train['target1'] = train['TARGET'].shift(-48) # 1일 만큼 Shift한 변수 생성
train['target2'] = train['TARGET'].shift(-96) # 2일 만큼 Shift한 변수 생성

 

  • Binary Classification Modeling
# 전처리 과정 중 발생한 null값 포함 row 제거
train = train.dropna()
train.reset_index(drop = True, inplace=True)
train_copy = train.copy()
features = train.columns
features = features.drop(['target1','target2','Minute','Day','Hour'])

# Binary 종속 변수 생성
train['target1_c'] = train['target1'].apply(lambda x: int(1) if x > 0 else 0) 
train['target2_c'] = train['target2'].apply(lambda x: int(1) if x > 0 else 0) 

X_train_1, X_valid_1, Y_train_1, Y_valid_1 = train_test_split(train[features], train['target1_c'], test_size=0.2, random_state=0)
X_train_2, X_valid_2, Y_train_2, Y_valid_2 = train_test_split(train[features], train['target2_c'], test_size=0.2, random_state=0)
start = time.time()
RF1 = RandomForestClassifier(random_state=0)
RF1.fit(X_train_1, Y_train_1)
print(time.time() - start)

RF2 = RandomForestClassifier(random_state=0)
RF2.fit(X_train_2, Y_train_2)
print(time.time() - start)
print(accuracy_score(Y_valid_1, RF1.predict(X_valid_1)))
print(accuracy_score(Y_valid_2, RF2.predict(X_valid_2)))

# Binary Classfication의 Accuracy 99% 이상임을 확인

 

 

 

  • Test Set 파생변수 생성
def make_features(train):
  train.reset_index(inplace= True)

  train['new_day'] = None
  train['new_day'][0] = 0

  for i in tqdm(range(1, len(train))):
    if train['Day'][i] == train['Day'][i-1]:
      train['new_day'][i] = train['new_day'][i-1]
    else:
      train['new_day'][i] = train['new_day'][i-1] + 1 

  max = train.groupby('new_day').max()[['TARGET']].reset_index()
  train = pd.merge(train, max, on ='new_day', how ='left')
  train.rename({'TARGET_x' : 'TARGET', 'TARGET_y':'max_tagrget'}, axis=1, inplace=True)

  train['day_time'] = train['Hour'] * 2 + train['Minute'] * 1/30
  no_0 = train[train['TARGET'] != 0 ]

  rise = no_0.groupby('new_day').min()[['day_time']].reset_index()
  train = pd.merge(train, rise, on ='new_day', how ='left')
  train.rename({'day_time_x' : 'day_time', 'day_time_y':'rising_time'}, axis=1, inplace=True)

  sunset = no_0.groupby('new_day').max()[['day_time']].reset_index()
  train = pd.merge(train, sunset, on ='new_day', how ='left')
  train.rename({'day_time_x' : 'day_time', 'day_time_y':'set_time'}, axis=1, inplace=True)

  return train



test = pd.DataFrame()
for i in tqdm(range(81)):
  file_path = '/content/drive/MyDrive/dacon/solar/test/' + str(i) + '.csv'
  temp = pd.read_csv(file_path)
  test = test.append(temp)
test = make_features(df_test)

feature = ['TARGET','DHI','DNI','T']
for feat in feature:
  mean_feature(test,feat)
  
test =  test[test['Day'] == 6]
test.reset_index(drop = True, inplace=True)



# Test Set 에 대해서 0, 1 Classification 이후, 1인 값들의 Index 추출

preds1 = RF1.predict(test[features])
preds2 = RF2.predict(test[features])

test['pred_c_1'] = preds1
test['pred_c_2'] = preds2

reg1 = test[test['pred_c_1'] == 1]
reg2 = test[test['pred_c_2'] == 1]

ind1 = reg1.index
ind2 = reg2.index

 

  • Light GBM Modeling(예측값 0 제외 Regression)
from lightgbm import LGBMRegressor

quantiles = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]

def LGBM(q, X_train, y_train, X_valid, y_valid, X_test):
  model = LGBMRegressor(objective='quantile', alpha=q,
                        n_estimators=500, bagging_fraction=0.7, learning_rate=0.027, subsample=0.7)                                        
  model.fit(X_train, y_train, eval_metric = ['quantile'], 
        eval_set=[(X_valid, y_valid)], early_stopping_rounds=300, verbose=500)
  pred = pd.Series(model.predict(X_test).round(2))
  return pred, model

# Target 예측
def prediction(X_train, y_train, X_valid, y_valid, X_test):
    LGBM_models=[]
    LGBM_actual_pred = pd.DataFrame()
    for q in quantiles:
        print(q)
        pred , model = LGBM(q, X_train, y_train, X_valid, y_valid, X_test)
        LGBM_models.append(model)
        LGBM_actual_pred = pd.concat([LGBM_actual_pred,pred],axis=1)
    LGBM_actual_pred.columns=quantiles  
    return LGBM_models, LGBM_actual_pred
a = train.iloc[:42000]
b = train.iloc[42000:]

X_valid_1 = np.array(b[b['target1'] > 0][features])
y_valid_1 = np.array(b[b['target1'] > 0]['target1'])
X_train_1 = np.array(a[a['target1'] > 0][features])
y_train_1 = np.array(a[a['target1'] > 0]['target1'])
X_test = np.array(test[features].iloc[ind1])

start = time.time()
models_1, results_1 = prediction(X_train_1, y_train_1, X_valid_1, y_valid_1, X_test)
results_1.sort_index()[:48]
print(time.time() - start)

 

  • 예측 결과 DataFrame 생성
results_1['index'] = ind1
results_2['index'] = ind2

del test['index']
test.reset_index(inplace=True)

test = pd.merge(test, results_1, on='index',how='left')
test = pd.merge(test, results_2, on='index',how='left')

first_cols = ['0.1_x', '0.2_x', '0.3_x', '0.4_x', '0.5_x', '0.6_x','0.7_x', '0.8_x', '0.9_x']
second_cols = ['0.1_y', '0.2_y', '0.3_y', '0.4_y', '0.5_y','0.6_y', '0.7_y', '0.8_y', '0.9_y']
cols = submission.columns.tolist()[1:]

prediction = []
for a in range(81):
    prediction.extend(test.iloc[a*48:a*48+48][first_cols].values)
    prediction.extend(test.iloc[a*48:a*48+48][second_cols].values)
    
    
submission[cols] = prediction
submission.fillna(0, inplace=True)
submission.set_index('id', inplace=True)
submission[submission <= 0] = 0 # 음수는 0으로 변환
submission[submission >= 100] = 99.9 # 최대값 99.9로 제한
submission.reset_index(inplace=True)
submission.iloc[10:30]

 

Graph Neural Networks

Knowledge graph, Molecular graph, Social graph 등 graph 구조를 통해서만 표현될 수 있는 데이터들을 활용하기 위해 Graph Neural Networks(GNN)의 사용이 점점 증가하고 있는 추세이다. Graph 데이터를 활용하는 태스크들은 아래와 같다. 

  • Node classification : 주어진 노드의 타입을 예측
  • Link Prediction : 특정 노드들이 연결될지 예측
  • Community detection : 노드의 군집이 densely 연결되어있는지 확인
  • Network similarity : 두개의 네트워크가 얼마나 비슷한지 확인

이러한 문제를 풀기위해 초기에는 Multi-hop Similarity, DeepWalk과 같은 Node 임베딩이 활용되었다. 하지만 NLP에서도 초창기에는 자주 활용되었던 Word2Vec, FastText와 같은 임베딩이 현재는 잘 활용되지 않고 RNN, Transformer과 같은 모델에서 supervised learning으로 학습되듯이 GNN이 본격적으로 등장하고 나서부터는 Node 임베딩이 잘 활용되지 않고 있다.

 

본 포스팅 글에서 설명할 GCN은 GNN 아키텍쳐 중에서도 가장 유명하며 이미지에서의 CNN과 유사한 장점들을 지니고 있다. 이미지에서 각 pixel들이 weight sharing 필터 연산을 통해 연산되며 Parallel Translation 문제를 방지할 수 있듯이, 그래프에서도 인접 노드들에 대한 local 정보를 동일한 weight로 연산되어 인접한 정보를 학습할 수 있다.

 

GCN(Graph Convolutional Networks)이 처음 제안된 논문에서는 semi-supervised learning 효과를 입증했다(Kipf, T. N., & Welling, M. 2016). 이 논문에서 제안한 GCN은 Adjacency Matrix를 활용하여 연결된 노드들의 feature를 연산하고 학습하는 방식으로 semi-supervised learning에 대한 성능을 높이고자 하였다. Citation network, Knowledge graph 데이터로 실험 결과, Label rate가 0.001~ 0.036임에도 불구하고 비교 SOTA 모델들 대비 월등한 분류 정확도를 보였다.

 

이러한 GCN 구조에 대해 'Semi-supervised classification with graph convolutional networks' 논문을 토대로 정리해보고자 한다. 또한 추후 NLP Task에도 적용하며 코드를 구현해볼 예정이다.

 

 

GCN Architecture

전체 수식은 아래와 같이 표현되며 그림과 같이 노드 feature와 weight matrix와의 연산을 기반으로 한다.

 

$H^{(l+1)}=\sigma(D̃^{-\frac{1}{2}}ÃD̃^{-\frac{1}{2}}H^{(l)}W^{(l)})$

 

 

Step 1 : Adjacency Matrix 정규화       $ D̃^{-\frac{1}{2}}ÃD̃^{-\frac{1}{2}} $

 

각 노드에 연결된 edge의 수에 따라 Adjacency Matrix를 Normalization → 연결이 많이 되어있는 노드는 연산을 거듭할수록 값이 커지고 연결이 적은 부분은 상대적으로 작아지기 때문에 추후 gradient 학습 시 연결이 적은부분은 학습이 안될 수 있다. 따라서 연결된 edge 수가 많을수록 더 작은 가중치가 부여되도록 정규화하는 작업이다.

 

✔️ 원래의 인접행렬은 self loop을 의미하는 diagonal 부분은 0으로 들어가지만 GCN에서 Conv Layer가 늘어남에 따라 자기 자신 노드와 연결된 노드들의 feature를 연결하여 계산하기 위해 self loop에도 1값을 부여한다. 즉 Ã = Adjacency matrix + Identity matrix 를 기준으로 한다. (각 노드의 degree를 나타내는 Degree matrix 도 동일)

 

Step 2 : Hidden State, Weight Matrix 곱 연산     $ H^{(l)}W^{(l)}$ 

 

각 노드의 hidden state와 weight matrix 곱을 통해 새로운 representation을 구하는 과정이다. 이 연산을 통해 산출된 값은 인접 노드들간의 Neighborhood Aggregation을 위해 활용된다.

 

 

Step 3 : 정규화된 adjancecy matrix와 새로운 representation 곱 연산

새롭게 정의된 representation을 기준으로 인접 행렬과의 곱을 통해 연결된 노드들의 정보를 취합하는 과정이다. 이후 최종적으로 Activation(ReLU)를 통해 비선형 학습을 한다.

이 과정까지의 연산을 행렬연산으로 표현하면 다음과 같이 표현할 수 있다. (activation 제외)

( n : node 수 / f : feature dim / d : # of convolution filter)

 

 

Step 4 : Readout

GCN 논문에서는 설명되지 않았지만, Graph 구조의 특성 상 노드의 순서에 따라 값의 변동이 생길 수 있는 Permutation Invariance 문제를 방지하기 위해 최종 output layer 이전에 readout 과정을 거친다. 대표적인 readout 방법으로는 MLP를 활용한 각 노드 Feature의 Node-wise summation이 있다. (CNN 의 FC layer와 유사한 개념)

 

 

 

 

✔️ Example

아래 예시는 2개의 layer를 쌓았을 때의 수식이며, 그림을 통해 이러한 연산을 거친 후 최종 output node값과 ground truth와의 loss를 구하는 것을 알 수 있다.

 

 

$Z=softmax(\widetilde{A} ReLU(\widetilde{A}XW_0)W_1)$

 

 

 


Reference

  • Kipf, T. N., & Welling, M. (2016). Semi-supervised classification with graph convolutional networks. arXiv preprint arXiv:1609.02907.

+ Recent posts