트랜스포머 아키텍처 깊이 있게 파헤치기
이 글은 “Attention Is All You Need”에 따라 정리된 트랜스포머 아키텍처의 혁신, 과학적 기반, 공식, 그리고 코드까지 깊이 있게 살펴봅니다. 이 글은 AI로 번역되었습니다. 오역이 있을 경우 댓글로 알려주세요.
Created on September 12|Last edited on September 12
Comment
다음과 같은 언어 모델의 혁신 이후 ELMo 그리고 ULMFit, 트랜스포머는 자연어 처리 (NLP) 분야를席권했다. 이들은 다음과 같은 인기 있는 언어 모델의 기반이 된다 GPT-3 그리고 DALL·E또한, 도구들 Hugging Face Transformers 라이브러리 머신러닝 엔지니어들이 다양한 NLP 과제를 쉽게 해결할 수 있게 했고, 이후의 수많은 NLP 혁신을 촉진했다.
이 글에서는 우리는 a 트랜스포머 심층 탐구 논문에서 설명된 아키텍처 “Attention Is All You Need” (2017) 그리고 이를 사용해 코드로 구현해 보세요 PyTorch.
목차
토크나이제이션
먼저, 텍스트에 대해 계산을 수행하려면 이를 수치적으로 표현하는 방법이 필요합니다. 토크나이제이션은 문자열 형태의 텍스트를 압축된 기호의 시퀀스로 분해하는 과정입니다. 이 과정을 거치면 텍스트의 일부를 나타내는 정수로 구성된 벡터가 생성됩니다.
Transformer 논문에서는 바이트-쌍 인코딩 (BPE) 토크나이제이션 방식으로. BPE는 가장 자주 등장하는 연속된 바이트(즉, 문자)를 하나의 바이트(즉, 정수)로 합쳐 압축하는 형태의 압축 기법입니다.
최근 연구에 따르면 BPE는 최적이 아니다 그리고 최근의 언어 모델인 BERT 사용하다 a WordPiece 대신 tokenizer를 사용합니다. WordPiece는 전체 단어를 하나의 토큰으로 자주 분할하므로 디코딩이 더 쉽고 직관적입니다. 반면 BPE는 종종 단어 조각으로 토크나이즈합니다. 이로 인해 이상한 토크나이즈 기호가 생기거나 개별 문자(즉, 알파벳 글자)가 미지 토큰으로 처리되는 문제가 발생할 수 있습니다. 따라서 예시에서는 WordPiece를 tokenizer로 사용합니다. 자체 코퍼스로 tokenizer를 학습시킬 수도 있지만, 실제로는 거의 항상 사전 학습된 tokenizer를 사용합니다. 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, ByteNet) 또는 순환 (순환 신경망, 장단기 메모리 네트워크). 트랜스포머 이전에도 어텐션은 이미 일부 NLP 분야의 돌파구를 열었습니다 (Luong 외, 2015) 하지만 그 당시에는 합성곱이나 순환 없이도 효과적인 모델을 만들 수 있다는 사실이 분명하지 않았습니다. 따라서 “Attention Is All You Need”라는 주장은 상당히 대담한 선언이었습니다.
어텐션 레이어는 쿼리 간의 매핑을 학습할 수 있습니다 () 그리고 키 집합 () 값 () 쌍입니다. 이 명칭들은 사용하는 NLP 애플리케이션에 따라 의미가 달라질 수 있어 혼란스러울 수 있습니다. 우리가 트랜스포머를 구현할 때는 입력을 선형 투영한 것이라고 생각해도 충분합니다. 쿼리, 키, 값이라는 이름은 전통적인 정보 검색 이론에서 왔으며, 이 용어들이 어디서 비롯되었는지 예시로 간단히 설명하겠습니다.
유튜브에서 영상을 검색할 때는 검색창에 문구를 입력합니다(즉, 쿼리). ). 검색 엔진은 이 쿼리를 유튜브 영상 제목, 설명 등과 대응시키는 데 사용합니다(즉, 키). ). 이 매핑을 바탕으로 가장 관련성 높은 동영상을 추천해 줍니다(즉, 값 ). (예시 소스)
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)))
피드포워드
모든 어텐션 층 위에는 피드포워드 네트워크가 추가됩니다. 이는 두 개의 완전연결 층으로 구성되며, a 활성화 함수()와 내부 층에 대한 드롭아웃을 사용합니다. 트랜스포머 논문에서 사용된 표준 차원은 다음과 같습니다 입력 층과 내부 층에 대해.
전체 계산식은 다음과 같습니다 .
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]])
현재 출력이 목표와 크게 다르게 나오는 이유는, 이 시점에서 모델의 가중치가 균일하게 초기화되어 있기 때문입니다. 트랜스포머 모델을 처음부터 학습시키려면 상당한 계산 자원이 필요합니다. 기본(base) 모델을 학습하기 위해 원 논문의 저자들은 NVIDIA P100 GPU 8대로 12시간 동안 학습했습니다. 더 큰 모델들은 GPU 8대로 3.5일이나 걸렸습니다! 실제 적용에는 사전 학습된 트랜스포머 모델을 사용하고, 여러분의 애플리케이션에 맞게 파인튜닝할 것을 권합니다. The Hugging Face 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
