728x90

Attention is all you need논문의 pytorch Implementation 코드를 리뷰하는 포스팅이다. 

https://github.com/jadore801120/attention-is-all-you-need-pytorch

 

GitHub - jadore801120/attention-is-all-you-need-pytorch: A PyTorch implementation of the Transformer model in "Attention is All

A PyTorch implementation of the Transformer model in "Attention is All You Need". - GitHub - jadore801120/attention-is-all-you-need-pytorch: A PyTorch implementation of the Transformer mo...

github.com

Transformer 코드 리뷰는 세 개의 포스팅으로 나누어 진행을 할 것이고 본 포스팅은 두 번째로 모델 구조 부분이다.

다만 자세한 모델 설명은 이전의 포스팅에서 설명했으므로 생략하겠다.

1. Word Embedding

지난 전처리에서 vocab size는 10077이였다. 이 vocab 내 모든 단어들을 고유 벡터로 만들어서 512차원으로 embedding 시켜주기 위해 nn.Embedding을 사용한다.

import torch.nn as nn
vacab_len = 10077
import torch.nn as nn
embedding_layer = nn.Embedding(num_embeddings=vacab_len, 
                               embedding_dim=512,
                               padding_idx=1)

print('vocab 내 모든 단어들을 고유 벡터로 만들어서 512차원으로 embedding 시켜줌')
print(f'LOOK UP TABLE SIZE:  {embedding_layer.weight.shape}')
print(embedding_layer.weight)
vocab 내 모든 단어들을 고유 벡터로 만들어서 512차원으로 embedding 시켜줌
LOOK UP TABLE SIZE:  torch.Size([10077, 512])
Parameter containing:
tensor([[ 0.9914, -0.4633, -1.2087,  ..., -2.0206, -2.9503, -0.4875],
        [ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000],
        [-0.5413, -0.2046, -0.0866,  ..., -0.1290,  0.0060,  0.8837],
        ...,
        [-0.6048, -0.5161,  0.6198,  ..., -0.0604, -0.0853,  0.4896],
        [ 0.2955,  0.1371,  0.4321,  ..., -0.3920,  0.7107,  0.5773],
        [-0.6375, -0.2004,  0.9800,  ...,  0.6815,  0.3131,  1.8058]],
       requires_grad=True)

 

2. Positional Encoding

transformer에서 문장 내 각 token의 위치정보를 넣어주기 위해 positional encoding을 해준다. Positional Encoding은 embedding과 차원이 동일하며, embedding vector와 더하여 위치정보를 추가하게 된다.

 

class PositionalEncoding(nn.Module):
    def __init__(self, d_hid, n_position=200):
        super(PositionalEncoding, self).__init__()
        # Not a parameter
        self.register_buffer('pos_table', self._get_sinusoid_encoding_table(n_position, d_hid)) # ①
        
    def _get_sinusoid_encoding_table(self, n_position, d_hid):
        '''Sinusoid position encoding table'''
        
        def get_position_angle_vec(position):
            return [position / np.power(10000, 2 * (hid_j // 2) / d_hid) for hid_j in range(d_hid)]
        
        sinusoid_table = np.array([get_position_angle_vec(pos_i) for pos_i in range(n_position)])
        sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2]) # dim 2i
        sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2]) # dim 2i+1
        
        return torch.FloatTensor(sinusoid_table).unsqueeze(0)
    
    def forward(self, x):
        return x + self.pos_table[:, :x.size(1)].clone().detach() # x : torch.size([328, 36, 512])
  1. torch.nn.Module.register_buffer는 parameter가 아니라 말 그대로 buffer를 수행하기 위한 목적으로 활용한다. 자세한 설명은 다음의 포스팅을 참고하면 된다.

3. Encoder

3-1. Multi head Attention

class ScaledDotProductAttention(nn.Module):
    ''' Scaled Dot-Product Attention '''

    def __init__(self, temperature, attn_dropout=0.1):
        super().__init__()
        self.temperature = temperature
        self.dropout = nn.Dropout(attn_dropout)

    def forward(self, q, k, v, mask=None):
        
        # ① attention score 계산 
        attn = torch.matmul(q / self.temperature, k.transpose(2, 3))
        
        # ② attention을 계산하지 않는 위치를 매우작은값으로 치환
        # - 매우작은 값은 softmax를 거칠때 0과 가까운 값이 되므로 무시한다.
        if mask is not None:
            attn = attn.masked_fill(mask == 0, -1e9)

        # ③ soft max를 이용하여 attention weight 계산
        attn = self.dropout(F.softmax(attn, dim=-1))
        
        # ④ 해당 분포값에 v를 곱하여 attention value를 구한다
        output = torch.matmul(attn, v)
        
        return output, attn
        # attention values, attention weight
class MultiHeadAttention(nn.Module):
    ''' Multi-Head Attention module '''

    def __init__(self, n_head, d_model, d_k, d_v, dropout=0.1):
        super().__init__()

        self.n_head = n_head
        self.d_k = d_k
        self.d_v = d_v

        self.w_qs = nn.Linear(d_model, n_head * d_k, bias=False)
        self.w_ks = nn.Linear(d_model, n_head * d_k, bias=False)
        self.w_vs = nn.Linear(d_model, n_head * d_v, bias=False)
        self.fc = nn.Linear(n_head * d_v, d_model, bias=False)

        self.attention = ScaledDotProductAttention(temperature=d_k ** 0.5)

        self.dropout = nn.Dropout(dropout)
        self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)


    def forward(self, q, k, v, mask=None):

        d_k, d_v, n_head = self.d_k, self.d_v, self.n_head
        sz_b, len_q, len_k, len_v = q.size(0), q.size(1), k.size(1), v.size(1)

        residual = q

        # Pass through the pre-attention projection: b x lq x (n*dv)
        # Separate different heads: b x lq x n x dv
        q = self.w_qs(q).view(sz_b, len_q, n_head, d_k) # [256, len, 8, 64]
        k = self.w_ks(k).view(sz_b, len_k, n_head, d_k) # [256, len, 8, 64]
        v = self.w_vs(v).view(sz_b, len_v, n_head, d_v) # [256, len, 8, 64]

        # Transpose for attention dot product: b x n x lq x dv
        q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2) # [256, 8, len, 64]

        if mask is not None:
            mask = mask.unsqueeze(1)   # For head axis broadcasting.

        q, attn = self.attention(q, k, v, mask=mask) # [256, 8, len, 64], [256, 8, len, 36]

        # Transpose to move the head dimension back: b x lq x n x dv
        # Combine the last two dimensions to concatenate all the heads together: b x lq x (n*dv)
        q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1) # [256, 36, 512]
        q = self.dropout(self.fc(q))
        q += residual

        q = self.layer_norm(q)

        return q, attn

Multi Head Attention은 세가지 요소를 입력으로 받는다. 

  • query
  • key
  • value

3-2. Position wise Feed Forward

class PositionwiseFeedForward(nn.Module):
    ''' A two-feed-forward-layer module '''

    def __init__(self, d_in, d_hid, dropout=0.1):
        super().__init__()
        self.w_1 = nn.Linear(d_in, d_hid) # position-wise 512 -> 2048
        self.w_2 = nn.Linear(d_hid, d_in) # position-wise 2048 -> 512
        self.layer_norm = nn.LayerNorm(d_in, eps=1e-6)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):

        residual = x

        x = self.w_2(F.relu(self.w_1(x)))
        x = self.dropout(x)
        x += residual

        x = self.layer_norm(x)

        return x

3-3. Encoder Layer

다음은 하나의 인코더 레이어를 정의한다. 인코더 레이어는 입력과 출력의 차원이 같다. 이러한 특징을 이용해 인코더 레이어를 여러 번 중첩해 사용할 수 있다.

class EncoderLayer(nn.Module):
    ''' Compose with two layers '''

    def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1):
        super(EncoderLayer, self).__init__()
        self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
        self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)

    def forward(self, enc_input, slf_attn_mask=None):
        enc_output, enc_slf_attn = self.slf_attn(
            enc_input, enc_input, enc_input, mask=slf_attn_mask)
        enc_output = self.pos_ffn(enc_output)
        return enc_output, enc_slf_attn

여기서 slf_attn_mask는 pad_idx에 대하여 mask값을 0으로 처리하는 다음과 같은 함수이다. 이는 필요 없는 패딩 인덱스에 대해 attention 연산을 안 하겠다는 의미이다.

def get_pad_mask(seq, pad_idx):
    return (seq != pad_idx).unsqueeze(-2)

3-4. Encoder Architechture

이 클래스에서 전체 인코더 아키텍처를 정의한다.

class Encoder(nn.Module):
    ''' A encoder model with self attention mechanism. '''

    def __init__(
            self, n_src_vocab, d_word_vec, n_layers, n_head, d_k, d_v,
            d_model, d_inner, pad_idx, dropout=0.1, n_position=200, scale_emb=False):
        

        super().__init__()
        
        # 모든 단어들을 embedding (고유 백터를 가진 차원으로 변경)
        # 현재 코드에서는10077의 vocab 내 단어들을 512 차원으로 embedding 시킴
        self.src_word_emb = nn.Embedding(n_src_vocab, d_word_vec, padding_idx=pad_idx)
        
        # 문장의 최대길이까지 positional encoding을함
        # positional encoding : rnn이 아니므로 순서에 대한 정보를 반영하기위한 방법
        self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position)
        self.dropout = nn.Dropout(p=dropout)
        
        # multiple encoder
        # encoder layer stack으로 encoder 층을 6개로 쌓는다.
        # encoder layer : multihead attentions, feedforward로 구성
        self.layer_stack = nn.ModuleList([
            EncoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout)
            for _ in range(n_layers)])
        
        # layer_norm 
        self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
        self.scale_emb = scale_emb
        self.d_model = d_model

    def forward(self, src_seq, src_mask, return_attns=True):

        enc_slf_attn_list = []

        # -- Forward
        # ① word embedding
        enc_output = self.src_word_emb(src_seq)
        if self.scale_emb:
            enc_output *= self.d_model ** 0.5
            
        # ② positional encoding
        enc_output = self.dropout(self.position_enc(enc_output))
        
        # normalization
        enc_output = self.layer_norm(enc_output)
        
        # ③ stacked encoder layers
        for enc_layer in self.layer_stack:
            enc_output, enc_slf_attn = enc_layer(enc_output, slf_attn_mask=src_mask)
            enc_slf_attn_list += [enc_slf_attn] if return_attns else []
            
        if return_attns:
            return enc_output, enc_slf_attn_list
        return enc_output,

 

 

4. Decoder

4-1. Decoder Layer

decoder layer도 encoder layer와 같이 입력과 출력의 차원이 같다. 이러한 특징을 이용해 트랜스포머의 decoder는 decoder layer를 여러 번 중첩해 사용한다.

또한 decoder layer는 encoder layer와 다르게 두 개의 multi head attention이 사용된다. 

Masked Multi-head Attention에서는 뒤쪽 단어의 점수를 참고하지 않도록 대각행렬 위쪽을 마스킹 처리를 해준다.

# masked multi-head attention을 하기 위한 mask 과정
def get_subsequent_mask(seq):
    ''' For masking out the subsequent info. '''
    sz_b, len_s = seq.size()
    subsequent_mask = (1 - torch.triu(
        torch.ones((1, len_s, len_s), device=seq.device), diagonal=1)).bool() # 대각행렬 윗부분을 False로 치환
    return subsequent_mask
class DecoderLayer(nn.Module):
    ''' Compose with three layers '''

    def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1):
        super(DecoderLayer, self).__init__()
        
        # 
        self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
        self.enc_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
        self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)

    def forward(
            self, dec_input, enc_output,
            slf_attn_mask=None, dec_enc_attn_mask=None):
        
        # ① sublayer 1: decoder input 값에 대한 self attention
        dec_output, dec_slf_attn = self.slf_attn(dec_input, dec_input, dec_input, mask=slf_attn_mask)
        
        # ② sublayer 2 : ①의 결과값을 query로, key,value는 encoder의 output 값으로 attention
        dec_output, dec_enc_attn = self.enc_attn(dec_output, enc_output, enc_output, mask=dec_enc_attn_mask)
        
        # ③ sublayer 3 : position wise feed forward를 통과
        dec_output = self.pos_ffn(dec_output)
        
        return dec_output, dec_slf_attn, dec_enc_attn

4-2. Decoder

class Decoder(nn.Module):
    ''' A decoder model with self attention mechanism. '''
    def __init__(
            self, n_trg_vocab, d_word_vec, n_layers, n_head, d_k, d_v,
            d_model, d_inner, pad_idx, n_position=200, dropout=0.1, scale_emb=False):

        super().__init__()
        
        # target word embedding
        self.trg_word_emb = nn.Embedding(n_trg_vocab, d_word_vec, padding_idx=pad_idx)
        
        # positional encoding
        self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position)
        self.dropout = nn.Dropout(p=dropout)
        
        # stacked decoder layers
        self.layer_stack = nn.ModuleList([
            DecoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout)
            for _ in range(n_layers)])
        
        # layer_normalization
        self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
        self.scale_emb = scale_emb
        self.d_model = d_model

    def forward(self, trg_seq, trg_mask, enc_output, src_mask, return_attns=True):

        dec_slf_attn_list, dec_enc_attn_list = [], []

        # -- Forward
        
        # ① target words embedding
        dec_output = self.trg_word_emb(trg_seq)
        if self.scale_emb:
            dec_output *= self.d_model ** 0.5
            
        # ② positional encoding
        dec_output = self.dropout(self.position_enc(dec_output))
        dec_output = self.layer_norm(dec_output)

        # ③ decoder_layer stacked
        for dec_layer in self.layer_stack:
            dec_output, dec_slf_attn, dec_enc_attn = dec_layer(
                dec_output, enc_output, slf_attn_mask=trg_mask, dec_enc_attn_mask=src_mask)
            
            dec_slf_attn_list += [dec_slf_attn] if return_attns else []
            dec_enc_attn_list += [dec_enc_attn] if return_attns else []

        if return_attns:
            return dec_output, dec_slf_attn_list, dec_enc_attn_list
        return dec_output,

5. Transformer Model

마지막으로 transformer model을 구현해보자. transformer 구조는 크게보면 다음과 같다.

  1. source, target 데이터에 대한 mask 생성
  2. encoder 층 통과
  3. decoder 층 통과
  4. 최종 가중치 층을 통과
''' Define the Transformer model '''
import torch
import torch.nn as nn
import numpy as np
#from transformer.Layers import EncoderLayer, DecoderLayer

class Transformer(nn.Module):
    ''' A sequence to sequence model with attention mechanism. '''

    def __init__(
            self, n_src_vocab, n_trg_vocab, src_pad_idx, trg_pad_idx,
            d_word_vec=512, d_model=512, d_inner=2048,
            n_layers=6, n_head=8, d_k=64, d_v=64, dropout=0.1, n_position=200,
            trg_emb_prj_weight_sharing=True, emb_src_trg_weight_sharing=True,
            scale_emb_or_prj='prj'):

        super().__init__()
        
        # padding index 저장
        self.src_pad_idx, self.trg_pad_idx = src_pad_idx, trg_pad_idx

        assert scale_emb_or_prj in ['emb', 'prj', 'none']
        scale_emb = (scale_emb_or_prj == 'emb') if trg_emb_prj_weight_sharing else False
        self.scale_prj = (scale_emb_or_prj == 'prj') if trg_emb_prj_weight_sharing else False
        self.d_model = d_model

        # encoder 정의
        self.encoder = Encoder(
            n_src_vocab=n_src_vocab, n_position=n_position,
            d_word_vec=d_word_vec, d_model=d_model, d_inner=d_inner,
            n_layers=n_layers, n_head=n_head, d_k=d_k, d_v=d_v,
            pad_idx=src_pad_idx, dropout=dropout, scale_emb=scale_emb)

        # decoder 정의
        self.decoder = Decoder(
            n_trg_vocab=n_trg_vocab, n_position=n_position,
            d_word_vec=d_word_vec, d_model=d_model, d_inner=d_inner,
            n_layers=n_layers, n_head=n_head, d_k=d_k, d_v=d_v,
            pad_idx=trg_pad_idx, dropout=dropout, scale_emb=scale_emb)

        # 최종 output layers 정의
        self.trg_word_prj = nn.Linear(d_model, n_trg_vocab, bias=False)

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

        assert d_model == d_word_vec, \
        'To facilitate the residual connections, \
         the dimensions of all module outputs shall be the same.'

        if trg_emb_prj_weight_sharing:
            # Share the weight between target word embedding & last dense layer
            self.trg_word_prj.weight = self.decoder.trg_word_emb.weight

        if emb_src_trg_weight_sharing:
            self.encoder.src_word_emb.weight = self.decoder.trg_word_emb.weight

    def forward(self, src_seq, trg_seq):
        
        # ① source, target 데이터에 대한 mask 생성
        src_mask = get_pad_mask(src_seq, self.src_pad_idx)
        trg_mask = get_pad_mask(trg_seq, self.trg_pad_idx) & get_subsequent_mask(trg_seq)

        # ② encoder 층을 통과
        #enc_output, *_ = self.encoder(src_seq, src_mask)
        enc_output, *_ = self.encoder(src_seq, src_mask)
        
        # ③ decoder 층을 통과
        #dec_output, *_ = self.decoder(trg_seq, trg_mask, enc_output, src_mask)
        dec_output, attention1, attention2 = self.decoder(trg_seq, trg_mask, enc_output, src_mask)
        
        # ④ 최종 weight 층을 통과
        seq_logit = self.trg_word_prj(dec_output)
        if self.scale_prj:
            seq_logit *= self.d_model ** -0.5

        return seq_logit.view(-1, seq_logit.size(2))

 

여기까지 transformer를 구성하는 모듈들에 대해 알아보았다. 다음 포스팅에는 전체적인 학습과 옵티마이저, 예측 함수에 대해 알아보도록 하겠다.

728x90
728x90

Attention is all you need논문의 pytorch Implementation 코드를 리뷰하는 포스팅이다. 

https://github.com/jadore801120/attention-is-all-you-need-pytorch

 

GitHub - jadore801120/attention-is-all-you-need-pytorch: A PyTorch implementation of the Transformer model in "Attention is All

A PyTorch implementation of the Transformer model in "Attention is All You Need". - GitHub - jadore801120/attention-is-all-you-need-pytorch: A PyTorch implementation of the Transformer mo...

github.com

Transformer 코드 리뷰는 세 개의 포스팅으로 나누어 진행을 할 것이고 본 포스팅은 그 첫 번째로 전처리 부분이다.

 

1. Prepocess

먼저 필요한 라이브러리를 임포트해주고 파라미터를 EasyDict를 통해 선언해주자.

import spacy
import torchtext.data # torchtext의 버전은 0.3.1을 사용
import torchtext.datasets
import dill as pickle
import easydict
opt = easydict.EasyDict({
        "lang_src" : 'de_core_news_sm',# source 언어 / spacy 사용시
        "lang_trg" : 'en_core_web_sm', # target 언어 / spacy 사용시
        "save_data" : True, 
        "data_src" : None, 
        "data_trg" : None,
        "max_len" :100, # 한 문장에 들어가는 최대 token 수 / 해당 갯수 이상이면 버림
        "min_word_count" : 3, # vocab을 만들어 줄때 최소 갯수 : 해당 갯수 미만이면 <unk>로 vocab이 형성됨
        "keep_case": True, # 대소문자 구분 
        "share_vocab" : "store_true",  # target vocab과 source vocab을 하나로 합치는지 여부
    })

1.1 Tokenizers

tokenizers는 문장을 개별 토큰으로 변환해주는 데 사용된다. 본 코드에서는 nlp를 쉽게 처리할 수 있도록 도와주는 패키지인 spacy를 이용하여 토큰화를 한다. 먼저 영어와 독일어 전처리 모듈을 설치하자.

!python -m spacy download en
!python -m spacy download de

모듈을 설치한 후 독일어와 영어를 토큰화 해주자.

src_lang_model = spacy.load('de_core_news_sm') # 독일어 토큰화
trg_lang_model = spacy.load('en_core_web_sm') # 영어 토큰화

토큰화의 예시는 다음과 같다.

# 토큰화 예시
tokenized = trg_lang_model.tokenizer('I am a student.')

for i, token in enumerate(tokenized):
    print(f'index {i}: {token.text}')
index 0: I
index 1: am
index 2: a
index 3: student
index 4: .

독일어와 영어의 토큰화 함수를 정의한다.

# 독일어 문장을 토큰화하는 함수
def tokenize_src(text):
    return [tok.text for tok in src_lang_model.tokenizer(text)]

# 영어 문장을 토큰화하는 함수
def tokenize_trg(text):
    return [tok.text for tok in trg_lang_model.tokenizer(text)]

default 토큰(padding unknown, start, end token)을 정의하고 torchtext의 Field 라이브러리를 사용하여 데이터 처리를 준비한다.

  • 소스(SRC) : 독일어
  • 목표(TRG) : 영어

Field는 데이터타입과 이를 텐서로 변환할 지시사항과 함께 정의하는 것이다. Field는 텐서로 표현될 수 있는 덱스트 데이터 타입을 처리하고, 각 토큰을 숫자 인덱스로 맵핑시켜주는 단어장(vocab) 객체가 있다. 또한 토큰화 하는 함수, 전처리 등을 지정할 수 있다.

PAD_WORD = '<blank>' # padding token
UNK_WORD = '<unk>' # unknown token
BOS_WORD = '<s>' # start token
EOS_WORD = '</s>' # end token

SRC = torchtext.data.Field(
    tokenize=tokenize_src, lower=False,
    pad_token=PAD_WORD, init_token=BOS_WORD, eos_token=EOS_WORD)

TRG = torchtext.data.Field(
    tokenize=tokenize_trg, lower=False,
    pad_token=PAD_WORD, init_token=BOS_WORD, eos_token=EOS_WORD)

이제 영어-독일어 번역 데이터셋인 Multi30k를 불러오자. (약 3만개의 영어, 독일어 문장)

MAX_LEN = opt.max_len # 문장의 최대 허용 token 갯수, token의 갯수보다 문장내 토큰의 갯수가 return False
MIN_FREQ = opt.min_word_count # vocab을 만들어 줄때 최소 갯수 : 해당 갯수 미만이면 <unk>로 vocab이 형성됨

def filter_examples_with_length(x):
    return len(vars(x)['src']) <= MAX_LEN and len(vars(x)['trg']) <= MAX_LEN

train, val, test = torchtext.datasets.Multi30k.splits(
        exts = ('.de', '.en'),
        fields = (SRC, TRG),
        filter_pred = filter_examples_with_length)
        
print(f"학습 데이터셋(training dataset) 크기: {len(train.examples)}개")
print(f"평가 데이터셋(validation dataset) 크기: {len(val.examples)}개")
print(f"테스트 데이터셋(testing dataset) 크기: {len(test.examples)}개")
학습 데이터셋(training dataset) 크기: 29000개
평가 데이터셋(validation dataset) 크기: 1014개
테스트 데이터셋(testing dataset) 크기: 1000개

학습 데이터 중 하나를 선택해 출력해보자.

# 학습 데이터 중 하나를 선택해 출력
print(vars(train.examples[25])['src'])
print(vars(train.examples[25])['trg'])
['Eine', 'Person', 'in', 'einem', 'blauen', 'Mantel', 'steht', 'auf', 'einem', 'belebten', 'Gehweg', 'und', 'betrachtet', 'ein', 'Gemälde', 'einer', 'Straßenszene', '.']
['A', 'person', 'dressed', 'in', 'a', 'blue', 'coat', 'is', 'standing', 'in', 'on', 'a', 'busy', 'sidewalk', ',', 'studying', 'painting', 'of', 'a', 'street', 'scene', '.']

'Eine Person in einem blauen Mantel steht auf einem belebten Gehweg und betrachtet ein Gemälde einer Straßenszene .'
=> 'A person in a blue coat stands on a busy sidewalk and looks at a painting of a street scene'
(파란 코트를 입은 사람이 붐비는 인도에 서서 거리의 풍경을 보고 있다.)

 

1.2 Build Vocab

field 객체의 build_vocab 메서드를 이용해 영어와 독일어 단어 사전을 생성한다. 여기서는 최소 3번 이상 등장한 단어만을 선택하는데 이때, 2번 이하로 나오는 단어는 '<unk>'token으로 변환된다.

SRC.build_vocab(train.src, min_freq=MIN_FREQ)
TRG.build_vocab(train.trg, min_freq=MIN_FREQ)

print(f'len(SRC) : {len(SRC.vocab)}')
print(f'len(TRG) : {len(TRG.vocab)}')
len(SRC) : 5497
len(TRG) : 4727

영어 단어의 토큰화 예시를 살펴보자.

  • stoi : 토큰 문자열로 매핑하는 문자열 식별자.
  • itos : 숫자 식별자로 인덱싱 된 토큰 문자열 목록
print(TRG.vocab.stoi['abcabc']) # 없는 단어 : 0
print(TRG.vocab.stoi[TRG.pad_token]) # 패딩 : 1
print(TRG.vocab.stoi['<s>']) # <s> : 2
print(TRG.vocab.stoi['</s>']) # </s> : 3
print(TRG.vocab.stoi['python']) # 2번 이하 단어
print(TRG.vocab.stoi["world"])
0
1
2
3
0
1870

본 코드에서는 share_vocab함수를 사용한다. 이 함수는 여러 개의 tokenizer를 사용할 때만 사용하는데 여러 개일 경우 같은 단어가 복수의 토큰 값을 가지게 되기 때문에 하나로 합치는 과정이 필요하기 때문에 사용한다. 본 논문은 독어와 영어에 중복되는 단어가 있기 때문에 사용한 것 같다.

for w, _ in SRC.vocab.stoi.items():
    # TODO: Also update the `freq`, although it is not likely to be used.
    if w not in TRG.vocab.stoi:
        TRG.vocab.stoi[w] = len(TRG.vocab.stoi)
TRG.vocab.itos = [None] * len(TRG.vocab.stoi)
for w, i in TRG.vocab.stoi.items():
    TRG.vocab.itos[i] = w
SRC.vocab.stoi = TRG.vocab.stoi
SRC.vocab.itos = TRG.vocab.itos
print('[Info] Get merged vocabulary size:', len(TRG.vocab))
[Info] Get merged vocabulary size: 10079

1.3 Data save

위의 과정을 통해 생성한 option과 vocab(src, trg), train, valid, test data를 저장해야 한다.

data = {
    'settings': opt,
    'vocab': {'src': SRC, 'trg': TRG},
    'train': train.examples,
    'valid': val.examples,
    'test': test.examples}

print('[Info] Dumping the processed data to pickle file', opt.save_data)
with open("m30k_deen_shr.pkl","wb") as f:
    pickle.dump(data,f)
[Info] Dumping the processed data to pickle file True

 

다음 포스팅에서는 Attention is all you need의 메인이 되는 Transformer 아키택쳐에 대해 리뷰해보겠다.

728x90
728x90

detach()와 clone()은 기존 Tensor를 복사하는 방법 중 하나이다.

tensor.clone()은 기존 텐서의 내용을 복사한 텐서를 생성하겠다는 의미이며, detach()는 기존 텐서에서 기울기가 전파되지 않는 텐서이다.

즉, tensor.clone().detach()는 기존 텐서를 복사한 새로운 텐서를 생성하지만 기울기에 영향을 주지는 않겠다는 의미이다.

구분 메모리 계산 그래프 상주 유무
tensor.clone() 새롭게 할당 계산 그래프에 계속 상주
tensor.detach() 공유해서 사용 계산 그래프에 상주하지 않음
tensor.clone().detach() 새롭게 할당 계산 그래프에 상주하지 않음

 

728x90

'딥러닝 > Pytorch' 카테고리의 다른 글

Pytorch 기본 예제  (0) 2022.12.24
[Pytorch] register_buffer  (0) 2022.08.01
pytorch - nn.function과 nn의 차이점  (0) 2022.06.19
torch - GPU 사용하기  (0) 2022.06.13
Pytorch nn.ModuleList  (0) 2022.05.30
728x90

transformer를 공부하던 중 positional encoding부분에서 register_buffer라는 부분이 생소하여 포스팅으로 기록한다.

torch.nn.Module.register_buffer는 매개 변수로 간주되지 않는 버퍼를 등록하는 데 사용된다. register_buffer는 다음과 같은 특징이 있다.

  1. optimizer가 update하지 않는다.
  2. state_dict에는 저장된다.
  3. GPU에서 작동한다.

즉, 네트워크를 구성하는데 있어 중간에 업데이트를 하지 않는 일반 layer를 넣고 싶을 때 사용한다.

728x90

'딥러닝 > Pytorch' 카테고리의 다른 글

Pytorch 기본 예제  (0) 2022.12.24
tensor의 복사방법  (0) 2022.08.06
pytorch - nn.function과 nn의 차이점  (0) 2022.06.19
torch - GPU 사용하기  (0) 2022.06.13
Pytorch nn.ModuleList  (0) 2022.05.30
728x90

파이토치로 모델링을 할 때 nn을 사용할 때도 있고 nn.functional을 사용할 때도 있다.

이 둘의 차이점은 무엇인지 알아보자.

 

먼저 nn을 사용하는 코드는 다음과 같다.

import torch
import torch.nn as nn

inputs = torch.randn(64, 3, 244, 244)
conv = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, padding=1)
outputs = conv(inputs)
layer = nn.Conv2d(1, 1, 3)

nn.functional을 사용하는 코드는 다음과 같다.

import torch.nn.functional as F

inputs = torch.randn(64, 3, 244, 244)
weight = torch.randn(64, 3, 3, 3)
bias = torch.randn(64)
outputs = F.conv2d(inputs, weight, bias, padding=1)

 

nn.Conv2d에서 input_channel과 output_channel을 사용해서 연산했다면 functional.conv2d는 입력과 가중치 자체를 직접 넣어준다. 이때 직접 넣어준다는 의미는 가중치를 전달해야 할 때마다 가중치 값을 새로 정의해야 함을 의미한다.

다음은 nn과 nn.functional을 비교한 표이다.

구분 nn nn.functional
형태 nn.Conv2d : 클래스
nn.Module 클래스를 상속받아 사용
nn.functional.conv2d : 함수
def function (input)으로 정의된 순수한 함수
호출 방법 하이퍼파라미터를 전달한 후 함수 호출을 통해 데이터 전달 함수를 호출할 때 하이퍼파라미터, 데이터 전달
위치 nn.Sequential 내에 위치 nn.Sequential에 위치할 수 없음
파라미터 파라미터를 새로 정의할 필요 없음 가중치를 수동으로 전달해야 할 때마다 자체 가중치를 정의

 

728x90

'딥러닝 > Pytorch' 카테고리의 다른 글

tensor의 복사방법  (0) 2022.08.06
[Pytorch] register_buffer  (0) 2022.08.01
torch - GPU 사용하기  (0) 2022.06.13
Pytorch nn.ModuleList  (0) 2022.05.30
Pytorch nn.Embedding()  (0) 2022.05.30
728x90

공식 Documentation

Python List와 마찬가지로 nn.Module을 저장하는 역할을 한다.

예제

list를 nn.ModuleList()로 감싸 주면 된다.

class MyModule(nn.Module):
    def __init__(self):
        super(MyModule, self).__init__()
        self.linears = nn.ModuleList([nn.Linear(10, 10) for i in range(10)])

    def forward(self, x):
        # ModuleList can act as an iterable, or be indexed using ints
        for i, l in enumerate(self.linears):
            x = self.linears[i // 2](x) + l(x)
        return x

 

nn.Sequential()과의 차이점

  • nn.Sequential() : 안에 들어가는 모듈들을 연결해주고, 하나의 뉴럴넷을 정의한다. 즉, 나열된 모듈들의 output shape과 input shape이 일치해야 한다는 것. 
  • nn.ModuleList() : 개별적으로 모듈들이 담겨있는 리스트. 모듈들의 연결관계가 정의되지 않는다. 즉, forward 함수에서 ModulList 내의 모듈들을 이용하여 적절한 연결관계를 정의하는 과정이 필수적이다.

 

참고자료

https://pytorch.org/docs/stable/generated/torch.nn.ModuleList.html

 

ModuleList — PyTorch 1.11.0 documentation

Shortcuts

pytorch.org

https://dongsarchive.tistory.com/67

 

nn.ModuleList vs nn.Sequential

파이토치 코드를 보다보면 자주 등장하는 두 가지 클래스다. 비슷하게 쓰이는것 같으면서도 그 차이점을 구별해라 하면 말하기 어려운데, 구글링을 해 보니 친절한 답변이 있어서 가져왔다. (링

dongsarchive.tistory.com

 

728x90

'딥러닝 > Pytorch' 카테고리의 다른 글

pytorch - nn.function과 nn의 차이점  (0) 2022.06.19
torch - GPU 사용하기  (0) 2022.06.13
Pytorch nn.Embedding()  (0) 2022.05.30
Pytorch 기본 문법 - 모델 평가  (0) 2022.05.25
Pytorch 기본 문법 - 모델 훈련  (0) 2022.05.23
728x90

Embedding이란 임의의 길이의 실수 벡터로 밀집되게 표현하는 일련의 방법을 의미한다.

Pytorch는 임베딩 벡터를 사용하는 방법이 크게 두 가지 있다. 바로 임베딩 층을 만들어 훈련 데이터로부터 처음부터 임베딩 벡터를 학습하는 방법과 미리 사전에 훈련된 임베딩 벡터들을 가져와 사용하는 방법이다. 전자에 해당하는 방법이 pytorch에서는 nn.Embedding()을 사용하여 구현한다.

공식 Documentation

This module is often used to store word embeddings and retrieve them using indices. The input to the module is a list of indices, and the output is the corresponding word embeddings.

즉, 이 모듈은 단어 임베딩을 저장하고 인덱스를 사용하여 검색하는 데 자주 사용되고, 모듈에 대한 입력은 인덱스 목록이고 출력은 해당 단어 임베딩이라는 뜻이다.

 

입력 파라미터

torch.nn.Embedding(num_embeddings, embedding_dim, padding_idx=None, 
                   max_norm=None, norm_type=2.0, scale_grad_by_freq=False, 
                   sparse=False, _weight=None, device=None, dtype=None)
  • num_embeddings (int) - 임베딩을 위한 사전의 크기
  • embedding_dim (int) - 임베딩 벡터의 크기
  • padding_idx (int, optional) - 선택적으로 사용하는 인자. 패딩을 위한 토큰의 인덱스를 알려줌.

 

예제

import torch.nn as nn

# 크기 3의 텐서 10개가 포함된 임베딩 모듈
embedding = nn.Embedding(10, 3)

# 각각 4개의 인덱스로 구성된 2개의 표본 배치
input = torch.LongTensor([[1,2,4,5], [4,3,2,9]])
embedding(input)
tensor([[[-0.4509, -0.8820, -2.2471],
         [ 0.4561, -0.7103,  1.2992],
         [-0.4517,  0.1363, -0.4372],
         [-0.6953,  1.3094, -0.1399]],

        [[-0.4517,  0.1363, -0.4372],
         [-0.1696,  0.2681, -1.6542],
         [ 0.4561, -0.7103,  1.2992],
         [-0.4525, -1.2973,  0.6719]]], grad_fn=<EmbeddingBackward0>)

 

출처

https://pytorch.org/docs/stable/generated/torch.nn.Embedding.html

728x90

'딥러닝 > Pytorch' 카테고리의 다른 글

torch - GPU 사용하기  (0) 2022.06.13
Pytorch nn.ModuleList  (0) 2022.05.30
Pytorch 기본 문법 - 모델 평가  (0) 2022.05.25
Pytorch 기본 문법 - 모델 훈련  (0) 2022.05.23
Pytorch 기본 문법 - 파라미터 정의  (0) 2022.05.23
728x90

모델에 대한 평가는 함수와 모듈을 이용하는 두 가지 방법이 있다.

1. 함수를 이용하여 모델을 평가하는 방법

import torch
import torchmetrics

preds = torch.randn(10, 5).softmax(dim=-1)
target = torch.randint(5, (10,))

acc = torchmetrics.functional.accuracy(preds, target) # 모델을 평가하기 위해 torchmetrics.functional.accuracy 이용

2. 모듈을 이용하여 모델을 평가하는 방법

import torch
import torchmetrics
metric = torchmetrics.Accuracy()

n_batchs = 10
for i in range(n_batchs):
    preds = torch.randn(10, 5).softmax(dim=-1)
    target = torch.randint(5, (10,))
    
    acc = torchmetrics.functional.accuracy(preds, target)
    print(f'accuracy on batch {i}: {acc}') # 현재 배치에서 모델 정확도
    
acc = metric.compute()
print(f'accuracy no all data: {acc}') # 모든 배치에서 모델 평가

 

혹은 사이킷런에서 제공하는 confusion metrix를 이용하는 방법도 고려해 볼 수 있다.

728x90

'딥러닝 > Pytorch' 카테고리의 다른 글

Pytorch nn.ModuleList  (0) 2022.05.30
Pytorch nn.Embedding()  (0) 2022.05.30
Pytorch 기본 문법 - 모델 훈련  (0) 2022.05.23
Pytorch 기본 문법 - 파라미터 정의  (0) 2022.05.23
Pytorch 기본 문법 - 모델 정의  (0) 2022.05.23
728x90

구체적인 훈련 방법에 대해 알아보자. 가장 먼저 필요한 절차가 optimizer.zero_grad() 메서드를 이용하여 기울기를 초기화하는 것이다. 파이토치는 기울기 값을 계산하기 위해 loss.backward() 메서드를 이용하는데, 이것을 사용하면 새로운 기울기 값이 이전 기울기 값에 누적하여 계산된다. 

이 방법은 순환 신경망 모델을 구현할 때 효과적이지만 누적 계산이 필요하지 않은 모델에 대해서는 불필요하다. 따라서 기울기 값에 대해 누적 계산이 필요하지 않을 때는 입력 값을 모델에 적용하기 전에 optimizer.zero_grad() 메서드를 호출하여 미분 값이 누적되지 않게 초기화해 주어야 한다.

 

딥러닝 학습 절차 파이토치 학습 절차
모델, 손실 함수, 옵티마이저 정의 모델, 손실 함수, 옵티마이저 정의
optimizer.zero_grad( ):
전방향 학습, 기울기 초기화
전방향 학습 (입력 -> 출력 계산) output = model(input) : 출력 계산
손실 함수로 출력과 정답의 차이 계산 loss = loss_fn(output, target) : 오차 계산
역전파 학습 loss.backward( ) : 역전파 학습
기울기 업데이트 optimizer.step( ) : 기울기 업데이트

 

다음은 loss.backward() 메서드를 이용하여 기울기를 자동 계산한다. loss_backward()는 배치가 반복될 때마다 오차가 중첩적으로 쌓이게 되므로 매번 zero_grad()를 사용하여 미분 값을 0으로 초기화한다.

다음은 모델을 훈련시키는 예시 코드이다.

for epoch in range(100):
    yhat = model(x_train)
    loss = criterion(yhat, y_train)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
728x90
728x90

모델을 학습하기 전에 필요한 파라미터들을 정의한다. 사전에 정의할 파라미터는 다음과 같다.

  • 손실 함수 : 학습하는 동안 출력과 실제 값 사이의 오차를 측정한다. 손실 함수로 많이 사용되는 것은 다음과 같다.
    • BCELoss : 이진 분류를 위해 사용
    • CrossEntropyloss : 다중 클래스 분류를 위해 사용
    • MSELoss : 회귀 모델에서 사용
  • 옵티마이저 : 데이터와 손실 함수를 바탕으로 모델의 업데이트 방법을 결정한다. 다음은 옵티마이저의 주요 특성이다.
    • optimizer는 step() 메서드를 통해 전달받은 파라미터를 업데이트한다.
    • 모델의 파라미터별로 다른 기준을 적용시킬 수 있다.
    • torch.optim.Optimizer (params, defaults)는 모든 옵티마이저의 기본이 되는 클래스이다.
    • zero_grad() 메서드는 옵티마이저에 사용된 파라미터들의 기울기를 0으로 만든다.
    • torch.optim.lr_scheduler는 에포크에 따라 학습률을 조절할 수 있다.
  • 학습률 스케줄러 : 미리 지정한 횟수의 에포크를 지날 때마다 학습률을 감소시켜 준다. 학습률 스케줄러를 이용하면 학습 초기에는 빠른 학습을 진행하다가 전역 최소점ㅇ 근처에 다다르면 학습률을 줄여서 최적점을 찾아갈 수 있도록 해준다. 학습률 스케줄러의 종류는 다음과 같다.
    • optim.lr_scheduler.StepLR : 람다 함수를 이용하여 그 함수의 결과를 학습률로 설정한다.
    • optim.lr_scheduler.MultiStepLR : StepLR과 비슷하지만 특정 단계가 아닌 지정된 에포크에만 감마 바율로 감소시킨다.
    • optim.lr_scheduler.ExponentialLR : 에포크마다 이전 학습률에 감마만큼 곱한다.
    • optim.lr_scheduler.CosineAnnealingLR : 학습률을 코사인함수의 형태처럼 변화시킨다. 따라서 학습률이 커지기도 작아지기도 한다.
    • optim.lr_scheduler.ReduceLROnPlateau : 학습이 잘되고 있는지 아닌지에 따라 동적으로 학습률을 변화시킬 수 있다.

 

다음은 모델의 파라미터를 정의하는 예시 코드이다.

from torch.optim import optimizer

criterion = torch.nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer=optimizer, 
                                              lr_lambda=lambda eopch: 0.95 ** epoch)

for eopch in range(1, 100+1): # 에포크 수만큼 데이터를 반복하여 처리
    for x, y in dataloader: # 배치 크기만큼 데이터를 가져와서 학습 진행
        optimizer.zero_grad()

loss_fn(model(x), y).backward()
optimizer.step()
scheduler.step()
728x90

+ Recent posts