트랜스포머 아키텍처 완전 분석
이 글은 “Attention Is All You Need”에 제시된 트랜스포머 아키텍처의 혁신, 과학적 근거, 공식, 그리고 코드까지 깊이 있게 파헤칩니다. 이 글은 AI 번역본입니다. 오역 가능성이 있으면 댓글로 자유롭게 알려 주세요.
Created on September 12|Last edited on September 12
Comment
언어 모델의 돌파구 이후 ELMo 그리고 ULMFiT, 트랜스포머는 자연어 처리 (NLP) 분야를席권으로 휩쓸었습니다. 이들은 다음과 같은 인기 있는 언어 모델의 기반입니다 GPT-3 그리고 달리. 또한, 다음과 같은 도구들 HuggingFace Transformers 라이브러리 머신러닝 엔지니어들이 다양한 NLP 과제를 손쉽게 해결할 수 있게 했으며, 이후의 수많은 NLP 혁신을 촉진했습니다.
목차
토크나이제이션
무엇보다 먼저, 텍스트에 계산을 수행하려면 텍스트를 숫자로 표현하는 방법이 필요합니다. 토크나이제이션은 텍스트 문자열을 압축된 기호들의 시퀀스로 분해하는 과정입니다. 이 과정을 거치면 텍스트의 일부를 나타내는 정수들로 이루어진 벡터가 생성됩니다.
트랜스포머 논문에서는 바이트 쌍 인코딩 (BPE) 토크나이제이션 방법으로 사용합니다. BPE는 가장 자주 등장하는 연속된 바이트(즉, 문자)를 단일 바이트(즉, 정수)로 압축하는 형태의 압축 기법입니다.
최근 연구는 BPE는 최적이 아니다 그리고 최근의 언어 모델인 BERT 사용하다 WordPiece 대신 토크나이저로 WordPiece를 사용합니다. WordPiece는 종종 하나의 단어를 하나의 토큰으로 분할하기 때문에 디코딩이 더 쉽고 직관적입니다. 반면 BPE는 단어의 일부 조각으로 토크나이즈하는 경우가 많습니다. 이로 인해 이상한 토크나이즈 기호가 생기거나 개별 문자(예: 알파벳 글자)가 미지 토큰으로 처리되는 문제가 발생할 수 있습니다. 따라서 예시에서는 WordPiece 토크나이저를 사용합니다. 텍스트 코퍼스에 대해 직접 토크나이저를 학습시킬 수도 있지만, 실제로는 실무자들이 거의 항상 사전 학습된 토크나이저를 사용합니다. The HuggingFace Transformers 라이브러리 사전 학습된 토크나이저를 쉽게 사용할 수 있게 해줍니다.
>>> from transformers import BertTokenizer>>> tok = BertTokenizer.from_pretrained("bert-base-uncased")Downloading: 100%|█████████████████████████| 1.04M/1.04M [00:00<00:00, 1.55MB/s]Downloading: 100%|████████████████████████████| 456k/456k [00:00<00:00, 848kB/s]>>> tok("Hello, how are you doing?")['inputs_ids']{'input_ids': [101, 7592, 2129, 2024, 2017, 2725, 1029, 102]}>>> tok("The Frenchman spoke in the [MASK] language and ate 🥖")['input_ids']{'input_ids': [101, 1996, 26529, 3764, 1999, 1996, 103, 2653, 1998, 8823, 100, 102]}>>> tok("[CLS] [SEP] [MASK] [UNK]")['input_ids']{'input_ids': [101, 101, 102, 103, 100, 102]}
토크나이저는 인코딩의 시작을 나타내는 토큰을 자동으로 포함한다는 점에 유의하세요 ([CLS] == 101) 그리고 인코딩의 끝(즉, SEP준비 ([SEP] == 102). 다른 특수 토큰으로는 마스킹 ([MASK] == 103) 그리고 미지 기호 ([UNK] = 100예: 🥖 이모지의 경우).
임베딩
텍스트를 올바르게 표현하기 위해, 시퀀스에 있는 각 토큰은 임베딩을 통해 벡터로 변환됩니다. 임베딩의 가중치가 트랜스포머 모델의 다른 부분과 함께 학습되므로, 이는 일종의 신경망 계층으로 볼 수 있습니다. 임베딩은 어휘의 각 단어에 대한 벡터를 포함하며, 이러한 가중치는 정규 분포로 초기화됩니다. 어휘의 크기를 지정해야 한다는 점에 유의하세요 () 그리고 모델의 차원 () 모델을 초기화할 때 (). 마지막으로 가중치에 다음을 곱합니다 정규화 단계로서.
import torchfrom torch import nnclass Embed(nn.Module):def __init__(self, vocab: int, d_model: int = 512):super(Embed, self).__init__()self.d_model = d_modelself.vocab = vocabself.emb = nn.Embedding(self.vocab, self.d_model)self.scaling = torch.sqrt(self.d_model)def forward(self, x):return self.emb(x) * self.scaling
위치 인코딩
순환 신경망과 합성곱 신경망과는 달리, 이 모델 자체에는 시퀀스에서 임베딩된 토큰들의 상대적 위치에 대한 정보가 없습니다. 따라서 인코더와 디코더의 입력 임베딩에 인코딩을 더해 이러한 정보를 주입해야 합니다. 이 정보는 여러 방식으로 추가할 수 있으며, 고정적일 수도 있고 학습될 수도 있습니다. 트랜스포머는 각 위치에 대해 사인과 코사인 변환을 사용합니다 (). 짝수 차원에는 사인을 사용하고 () 그리고 홀수 차원에는 코사인을 사용합니다 ()."
코드에서는 수치적 오버플로를 피하기 위해 위치 인코딩을 로그 공간에서 계산합니다.
import torchfrom torch import nnfrom torch.autograd import Variableclass PositionalEncoding(nn.Module):def __init__(self, d_model: int = 512, dropout: float = .1, max_len: int = 5000):super(PositionalEncoding, self).__init__()self.dropout = nn.Dropout(dropout)# Compute the positional encodings in log spacepe = torch.zeros(max_len, d_model)position = torch.arange(0, max_len).unsqueeze(1)div_term = torch.exp(torch.arange(0, d_model, 2) * -(torch.log(torch.Tensor([10000.0])) / d_model))pe[:, 0::2] = torch.sin(position * div_term)pe[:, 1::2] = torch.cos(position * div_term)pe = pe.unsqueeze(0)self.register_buffer('pe', pe)def forward(self, x):x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)return self.dropout(x)
멀티헤드 어텐션
트랜스포머 이전에는 시퀀스로부터 학습하기 위한 AI 연구의 패러다임이 합성곱(WaveNet, 바이트넷) 또는 순환 (순환신경망, 장단기 메모리). 트랜스포머 이전에도 어텐션은 이미 일부 NLP 분야에서 돌파구를 열었습니다 (Luong 외, 2015)였지만, 당시에는 합성곱이나 순환 없이도 효과적인 모델을 만들 수 있다는 것이 자명하지 않았습니다. 따라서 “Attention Is All You Need”라는 주장은 상당히 대담한 선언이었습니다.
어텐션 레이어는 쿼리() 그리고 키 집합() 값 () 쌍입니다. 이 이름들의 의미는 사용하는 NLP 애플리케이션에 따라 달라질 수 있어 혼란스러울 수 있습니다. 우리가 트랜스포머를 구현하는 맥락에서는 입력에 대한 선형 투영으로 생각하면 충분합니다. query, key, value라는 명칭은 전통적인 정보 검색 이론에서 유래했으며, 간단한 예시를 통해 이 용어들이 어디에서 왔는지 설명하겠습니다.
유튜브에서 영상을 찾을 때는 검색창에 문구를 입력합니다(즉, 쿼리). ). 검색 엔진은 이를 유튜브 동영상의 제목, 설명 등과 매칭하는 데 사용합니다(즉, 키) ). 이 매핑을 사용해 당신에게 가장 관련성 높은 동영상을 추천합니다(즉, 값 ). (예시 소스)
NLP에서 어텐션의 성능을 끌어올린 혁신 중 하나는 저자들이 “스케일드 도트 프로덕트 어텐션”이라고 부른 것입니다. 이는 다음과 동일하지만 곱셈 어텐션, 하지만 추가로 그리고 키 차원으로 스케일링됩니다 . 이는 차원이 클수록 곱셈 어텐션의 성능을 더 좋게 만듭니다. 결과는 소프트맥스 활성화 함수를 거쳐 처리됩니다 () 그리고 곱해집니다 .
import torchfrom torch import nnclass Attention:def __init__(self, dropout: float = 0.):super(Attention, self).__init__()self.dropout = nn.Dropout(dropout)self.softmax = nn.Softmax(dim=-1)def forward(self, query, key, value, mask=None):d_k = query.size(-1)scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)if mask is not None:scores = scores.masked_fill(mask == 0, -1e9)p_attn = self.dropout(self.softmax(scores))return torch.matmul(p_attn, value)def __call__(self, query, key, value, mask=None):return self.forward(query, key, value, mask)
디코더에서는 특정 위치를 매우 큰 음수로 채워 넣어 마스킹하여 어텐션 하위 계층을 처리합니다 ( 또는 ). 이는 모델이 이후 위치에 주의를 기울여 ‘치팅’하는 것을 방지하기 위한 것입니다. 이렇게 하면 다음 토큰을 예측할 때 모델이 오직 이전 위치의 단어들에만 주의를 기울일 수 있게 됩니다.
어텐션 메커니즘 자체만으로도 매우 강력하며, 행렬 곱셈에 최적화된 GPU와 TPU 같은 현대 하드웨어에서 효율적으로 계산할 수 있습니다. 그러나 단일 어텐션 층은 오직 하나의 표현만 허용합니다. 따라서 트랜스포머에서는 여러 개의 어텐션 헤드를 사용합니다. 이렇게 하면 모델이 다양한 패턴과 표현을 학습할 수 있습니다. 논문에서는 연결된 여러 개의 어텐션 층. 최종 식은 다음과 같습니다:
여기서
from torch import nnfrom copy import deepcopyclass MultiHeadAttention(nn.Module):def __init__(self, h: int = 8, d_model: int = 512, dropout: float = 0.1):super(MultiHeadAttention, self).__init__()self.d_k = d_model // hself.h = hself.attn = Attention(dropout)self.lindim = (d_model, d_model)self.linears = nn.ModuleList([deepcopy(nn.Linear(*self.lindim)) for _ in range(4)])self.final_linear = nn.Linear(*self.lindim, bias=False)self.dropout = nn.Dropout(p=dropout)def forward(self, query, key, value, mask=None):if mask is not None:mask = mask.unsqueeze(1)query, key, value = [l(x).view(query.size(0), -1, self.h, self.d_k).transpose(1, 2) \for l, x in zip(self.linears, (query, key, value))]nbatches = query.size(0)x = self.attn(query, key, value, mask=mask)# Concatenate and multiply by W^Ox = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)return self.final_linear(x)
기술 메모: The .contiguous 메서드는 이후에 추가됩니다 .transpose, 왜냐하면 .transpose 원본 텐서와 기본 메모리 저장소를 공유합니다. 호출하면 .view 그다음에는 연속된 텐서가 필요합니다 (문서). The .view 메서드는 효율적인 재구성, 슬라이싱 및 원소별 연산을 가능하게 합니다 (문서)."
각 헤드의 차원이 다음으로 나뉘기 때문에 , 전체 계산량은 전체 차원을 사용하는 단일 어텐션 헤드를 사용할 때와 유사합니다 (). 그러나 이 방식에서는 헤드 단위로 계산을 병렬화할 수 있어 최신 하드웨어에서 속도가 크게 향상됩니다. 이는 합성곱이나 순환 없이도 효과적인 언어 모델을 학습할 수 있게 한 혁신 중 하나입니다.
잔차 연결과 레이어 정규화
AI 연구 커뮤니티는 다음과 같은 개념들이 잔차 연결 그리고 (배치) 정규화 성능을 개선하고 학습 시간을 줄이며 더 깊은 네트워크를 학습할 수 있게 합니다. 따라서 트랜스포머는 모든 어텐션 층과 모든 피드포워드 층 뒤에 잔차 연결과 정규화를 갖추고 있습니다. 추가로, 드롭아웃 더 나은 일반화를 위해 각 층에 추가됩니다.
정규화
최신 딥러닝 기반 컴퓨터 비전 모델은 종종 다음과 같은 특징을 갖습니다 배치 정규화. 그러나 이러한 유형의 정규화는 큰 배치 크기에 의존하며 순환 구조와는 자연스럽게 맞지 않습니다. 전통적인 트랜스포머 아키텍처는 레이어 정규화 대신. 레이어 정규화는 작은 배치 크기에서도 안정적으로 동작합니다 (")."
레이어 정규화를 계산하기 위해 먼저 평균을 계산합니다 및 표준편차 미니배치의 각 샘플마다 개별적으로.
그다음 정규화 단계는 다음과 같이 정의됩니다:
여기서 그리고 학습 가능한 파라미터입니다. 소수의 표준편차가 매우 작을 때 수치적 안정성을 위해 추가됩니다 입니다 .
from torch import nnclass LayerNorm(nn.Module):def __init__(self, features: int, eps: float = 1e-6):super(LayerNorm, self).__init__()self.gamma = nn.Parameter(torch.ones(features))self.beta = nn.Parameter(torch.zeros(features))self.eps = epsdef forward(self, x):mean = x.mean(-1, keepdim=True)std = x.std(-1, keepdim=True)return self.gamma * (x - mean) / (std + self.eps) + self.beta
잔차
잔차 연결은 네트워크의 이전 층(즉, 서브레이어) 출력에 현재 층의 출력을 더하는 것을 의미합니다. 이렇게 하면 네트워크가 사실상 일부 층을 ‘건너뛸’ 수 있어, 매우 깊은 네트워크를 구성할 수 있습니다.
각 층의 최종 출력은 다음과 같습니다
from torch import nnclass ResidualConnection(nn.Module):def __init__(self, size: int = 512, dropout: float = .1):super(ResidualConnection, self).__init__()self.norm = LayerNorm(size)self.dropout = nn.Dropout(dropout)def forward(self, x, sublayer):return x + self.dropout(sublayer(self.norm(x)))
피드 포워드
각 어텐션 층 위에는 피드 포워드 네트워크가 추가됩니다. 이는 두 개의 완전연결 층으로 구성되며, 활성화 함수 ()와 내부 층에 대한 드롭아웃을 사용합니다. Transformer 논문에서 사용된 표준 차원은 다음과 같습니다 입력 층과 함께 사용할 경우 내부 층에 대해.
전체 계산은 다음과 같습니다 .
from torch import nnclass FeedForward(nn.Module):def __init__(self, d_model: int = 512, d_ff: int = 2048, dropout: float = .1):super(FeedForward, self).__init__()self.l1 = nn.Linear(d_model, d_ff)self.l2 = nn.Linear(d_ff, d_model)self.relu = nn.ReLU()self.dropout = nn.Dropout(dropout)def forward(self, x):return self.l2(self.dropout(self.relu(self.l1(x))))
인코더 - 디코더
인코딩
이제 모델의 인코더와 디코더를 구성하는 모든 요소가 준비되었습니다. 하나의 인코더 레이어는 멀티헤드 어텐션 레이어와 그 뒤에 오는 피드포워드 네트워크로 이루어집니다. 앞서 언급했듯이, 잔차 연결과 레이어 정규화도 포함합니다.
from torch import nnfrom copy import deepcopyclass EncoderLayer(nn.Module):def __init__(self, size: int, self_attn: MultiHeadAttention, feed_forward: FeedForward, dropout: float = .1):super(EncoderLayer, self).__init__()self.self_attn = self_attnself.feed_forward = feed_forwardself.sub1 = ResidualConnection(size, dropout)self.sub2 = ResidualConnection(size, dropout)self.size = sizedef forward(self, x, mask):x = self.sub1(x, lambda x: self.self_attn(x, x, x, mask))return self.sub2(x, self.feed_forward)
논문에서의 최종 트랜스포머 인코더는 동일한 인코더 레이어 6개로 구성되며, 그 뒤에 레이어 정규화가 적용됩니다.
class Encoder(nn.Module):def __init__(self, layer, n: int = 6):super(Encoder, self).__init__()self.layers = nn.ModuleList([deepcopy(layer) for _ in range(n)])self.norm = LayerNorm(layer.size)def forward(self, x, mask):for layer in self.layers:x = layer(x, mask)return self.norm(x)
디코딩
디코딩 레이어는 마스킹된 멀티헤드 어텐션 레이어에 이어, 인코더의 출력을 메모리로 포함하는 멀티헤드 어텐션 레이어로 구성됩니다. 메모리는 인코더의 출력입니다. 마지막으로 피드포워드 네트워크를 거칩니다. 다시 말해, 이 모든 구성 요소에는 잔차 연결과 레이어 정규화가 포함됩니다.
from torch import nnfrom copy import deepcopyclass DecoderLayer(nn.Module):def __init__(self, size: int, self_attn: MultiHeadAttention, src_attn: MultiHeadAttention,feed_forward: FeedForward, dropout: float = .1):super(DecoderLayer, self).__init__()self.size = sizeself.self_attn = self_attnself.src_attn = src_attnself.feed_forward = feed_forwardself.sub1 = ResidualConnection(size, dropout)self.sub2 = ResidualConnection(size, dropout)self.sub3 = ResidualConnection(size, dropout)def forward(self, x, memory, src_mask, tgt_mask):x = self.sub1(x, lambda x: self.self_attn(x, x, x, tgt_mask))x = self.sub2(x, lambda x: self.src_attn(x, memory, memory, src_mask))return self.sub3(x, self.feed_forward)
논문에서의 최종 인코더와 마찬가지로, 디코더 역시 동일한 레이어 6개로 구성되며 그 뒤에 레이어 정규화가 적용됩니다.
class Decoder(nn.Module):def __init__(self, layer: DecoderLayer, n: int = 6):super(Decoder, self).__init__()self.layers = nn.ModuleList([deepcopy(layer) for _ in range(n)])self.norm = LayerNorm(layer.size)def forward(self, x, memory, src_mask, tgt_mask):for layer in self.layers:x = layer(x, memory, src_mask, tgt_mask)return self.norm(x)
이제 인코더와 디코더의 상위 수준 표현을 바탕으로 최종 인코더-디코더 블록을 쉽게 구성할 수 있습니다.
from torch import nnclass EncoderDecoder(nn.Module):def __init__(self, encoder: Encoder, decoder: Decoder,src_embed: Embed, tgt_embed: Embed, final_layer: Output):super(EncoderDecoder, self).__init__()self.encoder = encoderself.decoder = decoderself.src_embed = src_embedself.tgt_embed = tgt_embedself.final_layer = final_layerdef forward(self, src, tgt, src_mask, tgt_mask):return self.final_layer(self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask))def encode(self, src, src_mask):return self.encoder(self.src_embed(src), src_mask)def decode(self, memory, src_mask, tgt, tgt_mask):return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)
최종 출력
마지막으로, 디코더에서 나온 벡터 출력은 최종 출력으로 변환되어야 합니다. 언어 번역과 같은 시퀀스-투-시퀀스 문제에서는 각 위치마다 전체 어휘에 대한 확률 분포가 됩니다. 하나의 완전연결 레이어가 디코더의 출력을 다음과 같은 행렬로 변환합니다. 로짓, 이는 타깃 어휘의 차원을 가집니다. 이 숫자들은 소프트맥스 활성화 함수를 통해 어휘에 대한 확률 분포로 변환됩니다. 코드에서는 다음을 사용합니다 , 더 빠르고 수치적으로 안정적이기 때문입니다.
예를 들어, 번역된 문장이 있다고 해봅시다. 토큰과 전체 어휘는 토큰입니다. 그 결과로 나온 출력은 행렬이 됩니다. . 그다음 우리는 마지막 차원을 따라 합하여 출력 토큰 벡터를 얻습니다 토크나이저를 통해 텍스트 문자열로 디코딩할 수 있는
from torch import nnclass Output(nn.Module):def __init__(self, input_dim: int, output_dim: int):super(Output, self).__init__()self.l1 = nn.Linear(input_dim, output_dim)self.log_softmax = nn.LogSoftmax(dim=-1)def forward(self, x: torch.Tensor) -> torch.Tensor:logits = self.l1(x)return self.log_softmax(logits)
모델 초기화
논문과 동일한 차원으로 트랜스포머 모델을 구성합니다. 초기화 전략은 Xavier/Glorot 초기화이며, 이는 다음 범위의 균등 분포에서 값을 선택하는 방식입니다. . 모든 바이어스는 초기화됩니다 .
from torch import nndef make_model(input_vocab: int, output_vocab: int, d_model: int = 512):encoder = Encoder(EncoderLayer(d_model, MultiHeadAttention(), FeedForward()))decoder = Decoder(DecoderLayer(d_model, MultiHeadAttention(), MultiHeadAttention(), FeedForward()))input_embed= nn.Sequential(Embed(vocab=input_vocab), PositionalEncoding())output_embed = nn.Sequential(Embed(vocab=output_vocab), PositionalEncoding())output = Output(input_dim=d_model, output_dim=output_vocab)model = EncoderDecoder(encoder, decoder, input_embed, output_embed, output)# Initialize parameters with Xavier uniformfor p in model.parameters():if p.dim() > 1:nn.init.xavier_uniform_(p)return model
이 함수는 시퀀스-투-시퀀스 문제를 학습할 수 있는 PyTorch 모델을 반환합니다. 아래에는 토크나이즈된 입력과 출력을 사용하여 이를 활용하는 더미 예제가 있습니다. 어휘집이 단지 다음과 같다고 가정해봅시다. 입력과 출력을 위한 단어들.
# Tokenized symbols for source and target.>>> src = torch.tensor([[1, 2, 3, 4, 5]])>>> src_mask = torch.tensor([[1, 1, 1, 1, 1]])>>> tgt = torch.tensor([[6, 7, 8, 0, 0]])>>> tgt_mask = torch.tensor([[1, 1, 1, 0, 0]])# Create PyTorch model>>> model = make_model(input_vocab=10, output_vocab=10)# Do inference and take tokens with highest probability through argmax along the vocabulary axis (-1)>>> result = model(src, tgt, src_mask, tgt_mask)>>> result.argmax(dim=-1)tensor([[6, 6, 4, 3, 6]])
이 시점에서는 모델의 가중치가 균등하게 초기화되어 있어 출력이 목표와 크게 다릅니다. 트랜스포머 모델을 처음부터 학습시키려면 상당한 계산 자원이 필요합니다. 기본 모델을 학습하기 위해 원 논문의 저자들은 NVIDIA P100 GPU 8대로 12시간 동안 학습했습니다. 더 큰 모델들은 GPU 8대로 학습하는 데 3.5일이 걸렸습니다! 저는 사전 학습된 트랜스포머 모델을 사용한 뒤, 여러분의 응용 분야에 맞게 파인튜닝할 것을 권합니다. The HuggingFace Transformers 라이브러리 이미 파인튜닝할 수 있는 사전 학습 모델이 많이 준비되어 있습니다.
이것으로 모두 끝입니다! 트랜스포머 아키텍처에 대한 이번 딥다이브가 즐거우셨길 바랍니다!
출처 및 추가 학습 자료
추천 논문
온라인 자료
Add a comment
Panel
"Lastly, the weights are multiplied by \sqrt{d_{model}as a normalization step."
Interestingly, this is exactly the input-layer scaling needed to make an infinite-width network train properly, see: https://arxiv.org/pdf/2011.14522.pdfReply
