절대적 주의사항 ! ⚠️⚠️

본 코드는 절대 절대 제가 구현한 것이 아닙니다.

주요 모델을 소개하는 논문을 읽어도 결국 코드까지 이해해야지 겉핥기가 아니라는 점을 깨닫고 코드를 구현해보고 싶었으나 정말 빈 종이에 하는 코딩은 감이 안 잡히는게 너무 많아서 (특히 파이토치로 구현하자니) 친절하게 구현해주신 분들 링크를 참고해서 이해해보았습니다.

제가 아직 초보자여서 구현해주신 분들 코드도 단번에 이해가 되는 것이 아니기 때문에 까먹을까봐 최대한 제가 이해한 방식으로 풀어서 다시 정리해보았습니다.

절대 제가 구현한게 아님에도 원 코드 제공하시는 분들께 저작권 관련 불쾌함+불편함을 초래할까봐 걱정이 앞섭니다. 모든 설명 앞에는 ‘원 링크 주소’와 ‘제 코드가 아닙니다. 원 링크를 참고해주세요’ 를 꼭 적어놓겠습니다.

다시 말하지만 제가 구현한 코드가 아닙니다 ㅠㅠ 원링크 꼭 확인해주세요 !

전체 모델 구조 : aladdinpersson’s

position encoding, 마스킹 부분 : incredible.ai

training 부분 : towardsdatascience

파이토치 ,주요 모듈 import 하기

import torch
import torch.nn as nn
import numpy as np

SelfAttention class 정의하기

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

class SelfAttention(nn.Module):
    def __init__(self, embed_size, heads):
        super(SelfAttention, self).__init__()
        self.embed_size = embed_size
        self.heads = heads
        self.head_dim = embed_size // heads

        assert (
            self.head_dim * heads == embed_size
        ), "Embedding size needs to be divisible by heads"

        self.values = nn.Linear(self.head_dim, self.head_dim, bias=False)
        self.keys = nn.Linear(self.head_dim, self.head_dim, bias=False)
        self.queries = nn.Linear(self.head_dim, self.head_dim, bias=False)
        self.fc_out = nn.Linear(heads * self.head_dim, embed_size)

알아야 할 개념들 :

1) embed_size, heads, head_dim

한 단어당 embed_size 가 8 이라고 하고 heads 가 4 개라고 하면 각 head_dim 은 2가 됩니다.

IMG_008D5A40458B-1

2) nn.Linear(input 차원,output 차원)

이 부분이 조금 햇갈렸는데 nn.Linear 에서는 features 개수만 고려하고 데이터가 몇 개인지까지는 인자로 넣어주지 않아도 됩니다.

가령 torch.nn.Linear(3,1,bias=True) 라고 한다면 input 데이터는 m x 3 이고 여기에 3 x 1 인 linear layer 을 선형적으로 결합한다면 최종 output 값은 m x 1 이 됩니다.

SelfAttention class 어텐션 연산 코드 짜기

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

def forward(self, values, keys, query, mask):
        # Get number of training examples
        N = query.shape[0]

        value_len, key_len, query_len = values.shape[1], keys.shape[1], query.shape[1]

        # Split the embedding into self.heads different pieces
        values = values.reshape(N, value_len, self.heads, self.head_dim)
        keys = keys.reshape(N, key_len, self.heads, self.head_dim)
        query = query.reshape(N, query_len, self.heads, self.head_dim)

        values = self.values(values)  # (N, value_len, heads, head_dim)
        keys = self.keys(keys)  # (N, key_len, heads, head_dim)
        queries = self.queries(query)  # (N, query_len, heads, heads_dim)

        # Einsum does matrix mult. for query*keys for each training example
        # with every other training example, don't be confused by einsum
        # it's just how I like doing matrix multiplication & bmm

        energy = torch.einsum("nqhd,nkhd->nhqk", [queries, keys])
        # queries shape: (N, query_len, heads, heads_dim),
        # keys shape: (N, key_len, heads, heads_dim)
        # energy: (N, heads, query_len, key_len)

        # Mask padded indices so their weights become 0
        if mask is not None:
            energy = energy.masked_fill(mask == 0, float("-1e20"))

        # Normalize energy values similarly to seq2seq + attention
        # so that they sum to 1. Also divide by scaling factor for
        # better stability
        attention = torch.softmax(energy / (self.head_dim ** (1 / 2)), dim=3)
        # attention shape: (N, heads, query_len, key_len)

        out = torch.einsum("nhql,nlhd->nqhd", [attention, values]).reshape(
            N, query_len, self.heads * self.head_dim
        )
        # attention shape: (N, heads, query_len, key_len)
        # values shape: (N, value_len, heads, heads_dim)
        # out after matrix multiply: (N, query_len, heads, head_dim), then
        # we reshape and flatten the last two dimensions.

        out = self.fc_out(out)
        # Linear layer doesn't modify the shape, final shape will be
        # (N, query_len, embed_size)

        return out

알아야 할 개념들 :

1) 데이터 형식에 대한 이해

def forward(self, values, keys, query, mask): 에서 values, keys, query 의 차원은 ?

values : N (배치 내 training set 의 개수) , value_len (각 문장의 길이), heads (총 헤드 수) , head_dim (각 헤드 당 차원)

가령 한 배치당 256 개의 examples 가 있고 각 문장(;example) 의 길이가 3개이고 총 헤드는 4개가 있고 (원 단어 당 차원이 8이면) head 당 차원이 2 라고 하면

values 의 shape 은 (256,3,4,2) 가 됩니다.

keys, query 도 동일하게 shape 이 (256,3,4,2) 가 됩니다.

IMG_AF97C51C77C7-1

위 그림의 형식을 만들어주기 위해서 (256,3,8) 로 되어있는 텐서들을

​ values = values.reshape(N, value_len, self.heads, self.head_dim) ​ keys = keys.reshape(N, key_len, self.heads, self.head_dim) ​ query = query.reshape(N, query_len, self.heads, self.head_dim)

이렇게 reshape 해준 후 아까 SelfAttention class 정의하기 부분의 linear 층을 통과시켜주었습니다.

​ values = self.values(values) # (N, value_len, heads, head_dim) ​ keys = self.keys(keys) # (N, key_len, heads, head_dim) ​ queries = self.queries(query) # (N, query_len, heads, heads_dim)

2) torch.einsum

얘는 아직도 진짜 진짜 신기한대요. 사실 einsum 에 관해서는 절반도 채 이해를 하지 못한 것 같으나 막상 쓰는 데는 너무 편리해서 감으로 알아두었습니다. 연산해야되는 두 데이터의 인풋 shape, 연산으로 나온 output 값의 shape 만 지정해주면 알아서 어떠한 연산을 해야하는지 척척 가늠해서 연산을 해준다고 합니다. 어떻게 이게 가능하지.. ?

​ energy = torch.einsum(“nqhd,nkhd->nhqk”, [queries, keys]) ​

즉 이렇게 지정하면 queries 의 input shape 는 nqhd 이고, keys 의 input shape 는 nkhd 야 두 개를 연산하면 차원은 nhqk 가 되어야하니까 알아서 해야할 연산을 생각해서 연산을 해줘 봐 입니다

n (example 개수) q (query_len) h (head 수) d (head 차원)

n (example 개수) k (key_len) h (head 수) d (head 차원)

연산 후 -> n (example 개수) h (head 수) q (query_len) k (key_len) 입니다.

소프트멕스를 취하기 전 각각의 어텐션 스코어가 nhqk 가 나와야하는 이유는

IMG_6F58E4E6031B-1

이렇게 되기 때문입니다.

3) energy.masked_fill(mask == 0, float(“-1e20”))

masked_fill 에서는 첫 번째 인자로 지정한 값이 보이면 두 번 째 인자로 마스킹해버립니다. 디코더의 마스킹 부분 때문에 필요합니다.

4) softmax 연산할 때 인자로 dim 지정해주기

attention = torch.softmax(energy / (self.head_dim ** (1 / 2)), dim=3) 에서 dim=3 이면

n (example 개수) h (head 수) q (query_len) k (key_len) 이므로 k (key_len) 을 기준으로 소프트멕스를 취해주겠다는 것이 됩니다.

Key_len 이 3이어서 그림을 보면 한 쿼리당 어텐션 스코어가 3개씩 존재합니다 (1️⃣ 2️⃣ 3️⃣ / 4️⃣ 5️⃣ 6️⃣ …) 즉 key_len 이 기준이 되므로 각 head 마다 softmax (1️⃣ 2️⃣ 3️⃣;attention score about query 1 ) ,softmax(4️⃣ 5️⃣ 6️⃣;attention score about query 2) 이런 식으로 소프트멕스 값이 나옵니다.

또 리스케일링한 어텐션은 key 의 차원에 루트씌운 값으로 나눠줘야하므로

스크린샷 2021-03-23 오후 12 48 52

energy / (self.head_dim **(1 / 2)) 해주었습니다.

5) value 곱해주고 reshape

이제 value 를 곱해주면

소프트멕스를 취해준 어텐션의 shape 은 n (example 개수) h (head 수) q (query_len) k (key_len) 입니다.

또한 앞~~서서 정의한 value 의 shape 은 N, v (value_len), h (self.heads), d (self.head_dim) 입니다.

어차피 value_len=key_len=query_len = 문장 길이 이므로 편의상 원 코드 저자가 einsum 넣을 때는 명칭을 l 로 통일한 것 같습니다.

out = torch.einsum(“nhql,nlhd->nqhd”, [attention, values])

IMG_30E9557DB253-1

out = torch.einsum(“nhql,nlhd->nqhd”, [attention, values]).reshape( N, query_len, self.heads * self.head_dim )

여기에 reshape 하여 차원을 4차원에서 3차원으로 단순화시켜주었습니다

KakaoTalk_Photo_2021-03-23-13-18-09

사실 엄연히 head 쪼개서 변수 값 넣어주고 multihead attention 연산하는 코드인데 왜 클래스 명을 SelfAttention 으로 하였는지 조금 의문이 듭니다.

SelfAttention 클래스 최종 코드입니다

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

import torch
import torch.nn as nn


class SelfAttention(nn.Module):
    def __init__(self, embed_size, heads):
        super(SelfAttention, self).__init__()
        self.embed_size = embed_size
        self.heads = heads
        self.head_dim = embed_size // heads

        assert (
            self.head_dim * heads == embed_size
        ), "Embedding size needs to be divisible by heads"

        self.values = nn.Linear(self.head_dim, self.head_dim, bias=False)
        self.keys = nn.Linear(self.head_dim, self.head_dim, bias=False)
        self.queries = nn.Linear(self.head_dim, self.head_dim, bias=False)
        self.fc_out = nn.Linear(heads * self.head_dim, embed_size)

    def forward(self, values, keys, query, mask):
        # Get number of training examples
        N = query.shape[0]

        value_len, key_len, query_len = values.shape[1], keys.shape[1], query.shape[1]

        # Split the embedding into self.heads different pieces
        values = values.reshape(N, value_len, self.heads, self.head_dim)
        keys = keys.reshape(N, key_len, self.heads, self.head_dim)
        query = query.reshape(N, query_len, self.heads, self.head_dim)

        values = self.values(values)  # (N, value_len, heads, head_dim)
        keys = self.keys(keys)  # (N, key_len, heads, head_dim)
        queries = self.queries(query)  # (N, query_len, heads, heads_dim)

        # Einsum does matrix mult. for query*keys for each training example
        # with every other training example, don't be confused by einsum
        # it's just how I like doing matrix multiplication & bmm

        energy = torch.einsum("nqhd,nkhd->nhqk", [queries, keys])
        # queries shape: (N, query_len, heads, heads_dim),
        # keys shape: (N, key_len, heads, heads_dim)
        # energy: (N, heads, query_len, key_len)

        # Mask padded indices so their weights become 0
        if mask is not None:
            energy = energy.masked_fill(mask == 0, float("-1e20"))

        # Normalize energy values similarly to seq2seq + attention
        # so that they sum to 1. Also divide by scaling factor for
        # better stability
        attention = torch.softmax(energy / (self.embed_size ** (1 / 2)), dim=3)
        # attention shape: (N, heads, query_len, key_len)

        out = torch.einsum("nhql,nlhd->nqhd", [attention, values]).reshape(
            N, query_len, self.heads * self.head_dim
        )
        # attention shape: (N, heads, query_len, key_len)
        # values shape: (N, value_len, heads, heads_dim)
        # out after matrix multiply: (N, query_len, heads, head_dim), then
        # we reshape and flatten the last two dimensions.

        out = self.fc_out(out)
        # Linear layer doesn't modify the shape, final shape will be
        # (N, query_len, embed_size)

        return out

TransformerBlock 정의하기

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

class TransformerBlock(nn.Module):
    def __init__(self, embed_size, heads, dropout, forward_expansion):
        super(TransformerBlock, self).__init__()
        self.attention = SelfAttention(embed_size, heads)
        self.norm1 = nn.LayerNorm(embed_size)
        self.norm2 = nn.LayerNorm(embed_size)

        self.feed_forward = nn.Sequential(
            nn.Linear(embed_size, forward_expansion * embed_size),
            nn.ReLU(),
            nn.Linear(forward_expansion * embed_size, embed_size),
        )

        self.dropout = nn.Dropout(dropout)

스크린샷 2021-03-23 오후 1 26 51

첫 번째 add&norm 에 사용할 변수가 self.norm1 이고 두번째 add&norm 에 사용할 변수가 self.norm2 입니다.

그리고 feed_forward 는 공식 그대로 Linear ->ReLU -> Linear 해주었습니다.

nn.Linear(embed_size, forward_expansion * embed_size) 이므로 최종 shape 은

N x (forward_expansion x embed_size) 가 됩니다. forward_expansion 하이퍼파라미터는 말그대로 feed forward 들어가면서 차원 수를 얼마나 팽창시켜줄 것인지를 지정하는 것 같습니다.

TransformerBlock 정해준 값들로 레이어 구성하기

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

def forward(self, value, key, query, mask):
        attention = self.attention(value, key, query, mask)

        # Add skip connection, run through normalization and finally dropout
        x = self.dropout(self.norm1(attention + query))
        forward = self.feed_forward(x)
        out = self.dropout(self.norm2(forward + x))
        return out

앞서 정의한 self.attention 은 SelfAttention(embed_size, heads) 이므로 SelfAttention 클래스의 인스턴스입니다.

attention = self.attention(value, key, query, mask) 함으로써 SelfAttention 클래스의 forward 메소드가 소환되고 어텐션이 다 수행된 최종 결과가 attention 변수에 담기게 됩니다.

Add&norm 의 경우에는 h1,h2,h3 가 앞서서 SelfAttention 클래스에서 정의한 각 타임스텝에서의 nn.Linear layer 이라고 치면 이를 통해 생성된 query,key,value 가 multi-head attention 의 인풋이 되는 것이므로 self.norm1(attention + query) 이렇게 첫번째 add&norm 을 해준 것으로 보여집니다. multihead attention 의 인풋으로 여길 수 있는 것이 총 세 개나 되어서 query 를 대표 인풋으로 생각해서 넣어준 것 같으나 사람마다 이 부분은 조금 다르게 짤 수도 있을 것 같습니다.

KakaoTalk_Photo_2021-03-23-13-39-33

TransformerBlock 최종 코드입니다

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

class TransformerBlock(nn.Module):
    def __init__(self, embed_size, heads, dropout, forward_expansion):
        super(TransformerBlock, self).__init__()
        self.attention = SelfAttention(embed_size, heads)
        self.norm1 = nn.LayerNorm(embed_size)
        self.norm2 = nn.LayerNorm(embed_size)

        self.feed_forward = nn.Sequential(
            nn.Linear(embed_size, forward_expansion * embed_size),
            nn.ReLU(),
            nn.Linear(forward_expansion * embed_size, embed_size),
        )

        self.dropout = nn.Dropout(dropout)
        
    def forward(self, value, key, query, mask):
        attention = self.attention(value, key, query, mask)

        # Add skip connection, run through normalization and finally dropout
        x = self.dropout(self.norm1(attention + query))
        forward = self.feed_forward(x)
        out = self.dropout(self.norm2(forward + x))
        return out

인코더 클래스 정의하기

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

class Encoder(nn.Module):
    def __init__(
        self,
        src_vocab_size,
        embed_size,
        num_layers,
        heads,
        device,
        forward_expansion,
        dropout,
        max_length,
    ):

        super(Encoder, self).__init__()
        self.embed_size = embed_size
        self.device = device
        self.word_embedding = nn.Embedding(src_vocab_size, embed_size)
        self.position_embedding = nn.Embedding(max_length, embed_size)

        self.layers = nn.ModuleList(
            [
                TransformerBlock(
                    embed_size,
                    heads,
                    dropout=dropout,
                    forward_expansion=forward_expansion,
                )
                for _ in range(num_layers)
            ]
        )

        self.dropout = nn.Dropout(dropout)

self.position_embedding 다른 버전

원 링크 : incredible.ai

제 코드가 아닙니다. 원 링크를 참고해주세요


pe = np.zeros([max_length, embed_size])
for pos in range(max_length):
    for i in range(0, embed_size, 2):
        pe[pos, i] = np.sin(pos / (10000 ** ((2 * i) / embed_dim)))
        pe[pos, i + 1] = np.cos(pos / (10000 ** ((2 * (i + 1)) / embed_dim)))

self.position_embedding=torch.from_numpy(pe)

1) position_embedding

토큰에 위치 정보를 제대로 주기 위해서는 두 번째 방법이 정확한 것 같습니다. 첫 번째 방법은 편의를 위해 단순히 임베딩 시켜줘서 position_embedding 도 추가했다 라는 느낌만 준 것 같습니다.

2) nn.ModuleList

만들어준 블록들 각각을 아이템 느낌으로 ModuleList 라는 리스트에 저장해놓는다고 생각하면 된다고 합니다. ModuleList 로 반복문도 돌릴 수 있고 인덱싱도 가능하다고 합니다. 현재 코드에서는 nn.ModuleList 에 TransformerBlock 인스턴스를 넣어줬습니다.

인코더 정해준 값으로 레이어 구성하기

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

def forward(self, x, mask):
        N, seq_length = x.shape
        out = self.dropout(
            (self.word_embedding(x) + self.position_embedding)
        )

        # In the Encoder the query, key, value are all the same, it's in the
        # decoder this will change. This might look a bit odd in this case.
        for layer in self.layers:
            out = layer(out, out, out, mask)

        return out

스크린샷 2021-03-23 오후 2 01 24

다음과 같은 TransformerBlock 인스턴스의 forward 함수를 불러오기 위해서는 value,key,query,mask 의 인자가 들어가야 합니다. (->TransformerBlock 의 forward 함수 -> SelfAttention 의 forward 함수 ->어텐션 계산) 이렇게 됩니다.

그런데 맨 처음 value,key,query 를 만들기 위해서 들어가는 초기 input 은 모두 동일하게 임베딩된 바로 후 의 데이터일 것이므로 세 인자 모두 임베딩을 막 마친 out 값을 넣어줍니다.

인코더 클래스 최종 코드

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

class Encoder(nn.Module):
    def __init__(
        self,
        src_vocab_size,
        embed_size,
        num_layers,
        heads,
        device,
        forward_expansion,
        dropout,
        max_length,
    ):

        super(Encoder, self).__init__()
        self.embed_size = embed_size
        self.device = device
        self.word_embedding = nn.Embedding(src_vocab_size, embed_size)
        self.position_embedding = nn.Embedding(max_length, embed_size)

        self.layers = nn.ModuleList(
            [
                TransformerBlock(
                    embed_size,
                    heads,
                    dropout=dropout,
                    forward_expansion=forward_expansion,
                )
                for _ in range(num_layers)
            ]
        )

        self.dropout = nn.Dropout(dropout)

    def forward(self, x, mask):
        N, seq_length = x.shape
        positions = torch.arange(0, seq_length).expand(N, seq_length).to(self.device)
        out = self.dropout(
            (self.word_embedding(x) + self.position_embedding(positions))
        )

        # In the Encoder the query, key, value are all the same, it's in the
        # decoder this will change. This might look a bit odd in this case.
        for layer in self.layers:
            out = layer(out, out, out, mask)

        return out

디코더 블록 정의하기

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

class DecoderBlock(nn.Module):
    def __init__(self, embed_size, heads, forward_expansion, dropout, device):
        super(DecoderBlock, self).__init__()
        self.norm = nn.LayerNorm(embed_size)
        self.attention = SelfAttention(embed_size, heads=heads)
        self.transformer_block = TransformerBlock(
            embed_size, heads, dropout, forward_expansion
        )
        self.dropout = nn.Dropout(dropout)

디코더 블록에도 마찬가지로 self.transformer_block 이라는 TransformerBlock 인스턴스를 만들어주었습니다.

디코더 블록 정해준 값으로 레이어 구성하기

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

def forward(self, x, value, key, src_mask, trg_mask):
        attention = self.attention(x, x, x, trg_mask)
        query = self.dropout(self.norm(attention + x))
        out = self.transformer_block(value, key, query, src_mask)
        return out

디코더에서 query 는 맨 처음 masked multi-head attention 을 거친 값이 되고 key,value 는 인코더로부터 넘어오기 때문에 디코더에 들어가는 input data (;target sentence) 가 x 이고 인코더에서 최종 넘어온 output 값이 value,key 입니다.

디코더 블록 최종 코드입니다

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

class DecoderBlock(nn.Module):
    def __init__(self, embed_size, heads, forward_expansion, dropout, device):
        super(DecoderBlock, self).__init__()
        self.norm = nn.LayerNorm(embed_size)
        self.attention = SelfAttention(embed_size, heads=heads)
        self.transformer_block = TransformerBlock(
            embed_size, heads, dropout, forward_expansion
        )
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, value, key, src_mask, trg_mask):
        attention = self.attention(x, x, x, trg_mask)
        query = self.dropout(self.norm(attention + x))
        out = self.transformer_block(value, key, query, src_mask)
        return out

(디코더 블록을 활용할) 디코더 구조가 담긴 디코더 클래스 정의하기

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

class Decoder(nn.Module):
    def __init__(
        self,
        trg_vocab_size,
        embed_size,
        num_layers,
        heads,
        forward_expansion,
        dropout,
        device,
        max_length,
    ):
        super(Decoder, self).__init__()
        self.device = device
        self.word_embedding = nn.Embedding(trg_vocab_size, embed_size)
        self.position_embedding = nn.Embedding(max_length, embed_size)

        self.layers = nn.ModuleList(
            [
                DecoderBlock(embed_size, heads, forward_expansion, dropout, device)
                for _ in range(num_layers)
            ]
        )
        self.fc_out = nn.Linear(embed_size, trg_vocab_size)
        self.dropout = nn.Dropout(dropout)

마찬가지로 position embedding 은 sin,cos 이용해서 구현한 다른 버전을 사용해주었습니다.

원 링크 : incredible.ai

제 코드가 아닙니다. 원 링크를 참고해주세요

pe = np.zeros([max_length, embed_size])
for pos in range(max_length):
    for i in range(0, embed_size, 2):
        pe[pos, i] = np.sin(pos / (10000 ** ((2 * i) / embed_dim)))
        pe[pos, i + 1] = np.cos(pos / (10000 ** ((2 * (i + 1)) / embed_dim)))

self.position_embedding=torch.from_numpy(pe)

nn.ModuleList 에 DecoderBlock 인스턴스를 넣어주었습니다. for _ in range(num_layers)

코드를 추가하면 해당 블록 인스턴스를 num_layers 만큼 반복한다는 의미 같은데 이렇게도 쓰일 수 있군요.. ! 맞겠죠 ?

디코더 클래스 정해준 값으로 레이어 구성하기

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

def forward(self, x, enc_out, src_mask, trg_mask):
        N, seq_length = x.shape
        x = self.dropout((self.word_embedding(x) + self.position_embedding)

        for layer in self.layers:
            x = layer(x, enc_out, enc_out, src_mask, trg_mask)

        out = self.fc_out(x)

        return out

아 알고보니까 for layer in self.layers 인데 [DecoderBlock 인스턴스,for_in range(num_layers)] 이렇게 되다보니까 DecoderBlock 이 num_layers 만큼 반복되어서 쌓아지나보네요

enc_out 은 인코딩에서 넘어온 output 값입니다. key,value 모두 enc_out 에서 넘어와야하므로 두 인자 모두 enc_out 을 넣어주었습니다.

디코더는 아무래도 구성이 조금 더 복잡해 디코더 블록을 활용한 디코더 클래스 이렇게 스텝별로 만들어준 것 같습니다.

디코더 클래스 최종 코드입니다

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

class Decoder(nn.Module):
    def __init__(
        self,
        trg_vocab_size,
        embed_size,
        num_layers,
        heads,
        forward_expansion,
        dropout,
        device,
        max_length,
    ):
        super(Decoder, self).__init__()
        self.device = device
        self.word_embedding = nn.Embedding(trg_vocab_size, embed_size)
        self.position_embedding = nn.Embedding(max_length, embed_size)

        self.layers = nn.ModuleList(
            [
                DecoderBlock(embed_size, heads, forward_expansion, dropout, device)
                for _ in range(num_layers)
            ]
        )
        self.fc_out = nn.Linear(embed_size, trg_vocab_size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, enc_out, src_mask, trg_mask):
        N, seq_length = x.shape
        positions = torch.arange(0, seq_length).expand(N, seq_length).to(self.device)
        x = self.dropout((self.word_embedding(x) + self.position_embedding(positions)))

        for layer in self.layers:
            x = layer(x, enc_out, enc_out, src_mask, trg_mask)

        out = self.fc_out(x)

        return out

트랜스포머 정의하기

인코더와 디코더를 모두 연동시킬 대망의 트랜스포머 모델 클래스입니다.

스크린샷 2021-03-23 오후 2 33 13

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

class Transformer(nn.Module):
    def __init__(
        self,
        src_vocab_size,
        trg_vocab_size,
        src_pad_idx,
        trg_pad_idx,
        embed_size=512,
        num_layers=6,
        forward_expansion=4,
        heads=8,
        dropout=0,
        device="cpu",
        max_length=100,
    ):

        super(Transformer, self).__init__()

        self.encoder = Encoder(
            src_vocab_size,
            embed_size,
            num_layers,
            heads,
            device,
            forward_expansion,
            dropout,
            max_length,
        )

        self.decoder = Decoder(
            trg_vocab_size,
            embed_size,
            num_layers,
            heads,
            forward_expansion,
            dropout,
            device,
            max_length,
        )

        self.src_pad_idx = src_pad_idx
        self.trg_pad_idx = trg_pad_idx
        self.device = device

1) src_pad_idx,trg_pad_idx

말그대로 패딩이 들어간 인덱스입니다. 한 문장 내에서 하이퍼파라미터로 설정한 문장 최장 길이를 벗어난 토큰은 padding 처리가 됩니다.

2) Encoder 클래스, Decoder 클래스 인스턴스화

callable 한 Encoder 클래스 Decoder 클래스의 인스턴스를 만들어주었습니다.

mask 만들기

SelfAttention 코드에

if mask is not None: energy = energy.masked_fill(mask == 0, float(“-1e20”))

부분이 있었습니다. 즉 마스크 자리가 0인 부분은 어텐션 스코어가 음수 무한대가 되게 바꾸어서 softmax 를 취하면 0이 되게해야합니다 (;가려버려야 합니다)

원 링크 : incredible.ai

제 코드가 아닙니다. 원 링크를 참고해주세요

def create_mask(src: torch.Tensor,
                trg: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
    src_mask = _create_padding_mask(src, self.src_pad_idx)
    trg_mask = None
    if trg is not None:
        trg_mask = _create_padding_mask(trg, self.trg_pad_idx)  # (256, 1, 33)
        nopeak_mask = _create_nopeak_mask(trg)  # (1, 33, 33)
        trg_mask = trg_mask & nopeak_mask  # (256, 33, 33)
    return src_mask, trg_mask

def _create_padding_mask(seq: torch.Tensor, pad_idx: int) -> torch.Tensor:
    """
    seq 형태를  (256, 33) -> (256, 1, 33) 이렇게 변경합니다.

    아래와 같이 padding index부분을 False로 변경합니다. (리턴 tensor)
    아래의 vector 하나당 sentence라고 보면 되고, True로 되어 있는건 단어가 있다는 뜻.
    tensor([[[ True,  True,  True,  True, False, False, False]],
            [[ True,  True, False, False, False, False, False]],
            [[ True,  True,  True,  True,  True,  True, False]]])
    """
    return (seq != pad_idx).unsqueeze(-2)

def _create_nopeak_mask(trg) -> torch.Tensor:
    """
    NO PEAK MASK
    Target의 경우 그 다음 단어를 못보게 가린다
    """
    batch_size, seq_len = trg.size()
    nopeak_mask = (1 - torch.triu(torch.ones(1, seq_len, seq_len, device=trg.device), diagonal=1)).bool()
    return nopeak_mask

sentences = torch.Tensor([[2., 2., 2., 2., 1., 1., 1.],
                          [2., 2., 1., 1., 1., 1., 1.],
                          [2., 2., 2., 2., 2., 2., 1.]])
src_mask, trg_mask = create_mask(sentences, sentences, 1, 1)

print('sentences:', sentences.shape)
print('src_mask :', src_mask.shape)
print('trg_mask :', trg_mask.shape)
# sentences: torch.Size([3, 7])
# src_mask : torch.Size([3, 1, 7])
# trg_mask : torch.Size([3, 7, 7])

1) def (인자) -> torch.Tensor 등등의 타입

마이너한 부분인데 저는 이렇게 쓰여진 코드를 처음 봐서 궁금했습니다. 아웃풋 값의 타입은 torch.Tensor 야 ! 이렇게 한번 더 확실하게 명시해주는 의미라고 합니다.

2) (seq != pad_idx).unsqueeze(-2)

(seq != pad_idx) 는 맨 처음 인자로 넣어준 torch.Tensor shape 과 똑같은 tensor 에 조건문 결과값을 반환합니다.

원래 source 의 shape 은 (mxn) 즉 m 개의 training sets, n개의 문장 길이일텐데 unsqueeze(2) 이므로 가운데 1인 차원을 하나 추가해줍니다. 가령 3문장, 한 문장 길이 7 (3x7) 이었다면 (seq != pad_idx).unsqueeze(-2)를 하면 (3x1x7) 이 됩니다.

스크린샷 2021-03-23 오후 2 51 17

조건문이 seq!=pad_idx 이므로 pad_idx 자리에는 False 가 반환됩니다.

3) nopeak_mask = (1 - torch.triu(torch.ones(1, seq_len, seq_len, device=trg.device), diagonal=1)).bool()

torch.triu 는 이러한 메소드입니다. 즉 diagonal 로 지정한 값을 기준으로 이하 하상각 인덱스들은 전부 0이 됩니다.

스크린샷 2021-03-23 오후 2 55 03

다음 토큰의 어텐션 값을 보지 못하도록 마스킹하는 것이므로 0자리가 전부 1 (=True) 로 반대로 바뀌어야하므로 1에서 빼줍니다.

4) src_mask,trg_mask

source sentence 가 들어가는 인코더에선 애초에 masked multi-head attention 이 필요가 없습니다. 애초에 아무 의미가 없는 패딩처리된 토큰만 마스킹해주면 됩니다.

Trg_mask 에서는 패딩처리된 토큰 마스킹 + 현재 인덱스 기준으로 뒤 토큰 마스킹 을 같이 해줘야하므로 & 연산자를 이용합니다. (& 연산자는 True&True 가 아니면 전부 False 를 반환하므로 !)

제가 출처에 남긴 코드 제공하신 분께서 너무 자세히 설명해주셨습니다. 다시 한번 링크를 남기겠습니다. 그저 감사할 따름입니다.

Transformer 코드 구현

트랜스포머 레이어 구성하기

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

def forward(self, src, trg):
        src_mask,trg_mask = self.create_mask(src)
        enc_src = self.encoder(src, src_mask)
        out = self.decoder(trg, enc_src, src_mask, trg_mask)
        return out

마스크 만들어주기 -> 인코더에 source 데이터와 src_mask 넣어주기 -> 인코더에서 나온 값을 key,value 로 , target 데이터를 query 로 넣어주기

test 해보기

원 링크 : aladdinpersson’s

제 코드가 아닙니다. 원 링크를 참고해주세요

if __name__ == "__main__":
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print(device)

    x = torch.tensor([[1, 5, 6, 4, 3, 9, 5, 2, 0], [1, 8, 7, 3, 4, 5, 6, 7, 2]]).to(
        device
    )
    trg = torch.tensor([[1, 7, 4, 3, 5, 9, 2, 0], [1, 5, 6, 2, 4, 7, 6, 2]]).to(device)

    src_pad_idx = 0
    trg_pad_idx = 0
    src_vocab_size = 10
    trg_vocab_size = 10
    model = Transformer(src_vocab_size, trg_vocab_size, src_pad_idx, trg_pad_idx, device=device).to(
        device
    )
    out = model(x, trg[:, :-1])
    print(out.shape)

Training

원 링크 : towardsdatascience

제 코드가 아닙니다. 원 링크를 참고해주세요

src_pad_idx = 0
trg_pad_idx = 0
src_vocab_size = len(EN_TEXT.vocab)
trg_vocab_size = len(FR_TEXT.vocab)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = Transformer(src_vocab_size, trg_vocab_size, src_pad_idx, trg_pad_idx, device=device).to(
        device)
for p in model.parameters():
    if p.dim() > 1:
        nn.init.xavier_uniform_(p)

optim = torch.optim.Adam(model.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9)

def train_model(epochs, print_every=100):
    
    model.train()
    
    start = time.time()
    temp = start
    
    total_loss = 0
    
    for epoch in range(epochs):
       
        for i, batch in enumerate(train_iter):
            src = batch.English.transpose(0,1)
            trg = batch.French.transpose(0,1)
            # the French sentence we input has all words except
            # the last, as it is using each word to predict the next
            
            trg_input = trg[:, :-1]
            
            # the words we are trying to predict
            #view(-1) ; similar to flatten
            targets = trg[:, 1:].contiguous().view(-1)
            
            preds = model(src, trg_input)
            
            optim.zero_grad()
            
            loss = F.cross_entropy(preds.view(-1, preds.size(-1)),
            targets, ignore_index=trg_pad_idx)
            loss.backward()
            optim.step()
            
            total_loss += loss.data[0]
            if (i + 1) % print_every == 0:
                loss_avg = total_loss / print_every
                print("time = %dm, epoch %d, iter = %d, loss = %.3f,
                %ds per %d iters" % ((time.time() - start) // 60,
                epoch + 1, i + 1, loss_avg, time.time() - temp,
                print_every))
                total_loss = 0
                temp = time.time()

training 부분 코드는 데이터를 봐야지 알 것 같은데 데이터 가공하는 과정부터 코드 뜯어보고 공부하려면 너무 머리가 어지럽다.

파이토치가

optim.zero_grad

loss 정의

loss.backward() #gradient 계산

optim.step() #gradient descent

순으로 학습한다는 것을 이해한 걸로 만족하려고 한다.

view,reshape,transpose,permute 의 차이

continguous 함수의 의미

느낀 점

열심히 기초를 닦아서 서서히 저도 제 스스로 코드를 구현해보고 싶어요. 마치 네발 자전거를 타다가 서서히 세발 자전거를 타듯이 ? 아직은 너무 어렵고 모르는 것들이 한가득입니다.

그리고 오늘 이거 하느라 너무 녹초가 되어서 라라랜드 들으면서 집 가서 쉬려구요 쿨쿨쿨