times flies like an arrow
와 같은 문장이 주어졌을 때 flies
가 파리인지, 날라간다인지 해석하기 위해 주변 토큰들에게 어텐션값을 계산한다. 이때는 아마도 time
과 arrow
토큰 임베딩에 더 높은 가중치 값을 할당하게 될것이다contextualized embedding
이라고 부른다## 어텐션 함수로 변형시키기
def self_attention(query,key,value):
import torch
from math import sqrt
import torch.nn.functional as F
"""
query,key,value 텐서는
쿼리 형태: (N,query_len,embed_dim)
키 형태: (N,key_len,embed_dim)
에너지 형태: (N,heads,query_len,key_len)
"""
dim_k = key.size(-1)
"""
책에서는 다음과 같은 방법이 제시되었지만
scores= torch.bmm(query, key.transpose(1,2))/sqrt(dim_k)
einsum이 tensor-contradiction을 직관적으로 활용한다는 점에서 다음과 같이 수정하였다
위와 아래의 scores를 비교하면 True가 나온다
"""
scores= torch.einsum('ijk,ilk->ijl',[query,key])/sqrt(dim_k)
#소프트 맥스 함수 적용하기
weights= F.softmax(scores, dim=-1)
weights.sum(dim=-1)
#마지막 계산하기
"""기존 방법 : attn_outputs= torch.bmm(weights,value)
attention shape: (N,query_len,key_len)
value shape: (N,value_len,embed_dim)
result: (N,query_len,embed_dim)
여기선 key_len과 value_len이 같은것을 활용"""
attn_outputs= torch.einsum('ijk,ijl->ikl',[weights,value])
return attn_outputs
"""
멀티헤드 어텐션을 구현하기 위해 먼저 어텐션헤드를 정의한다
입력받은 query,key,value값을 self_attention함수를 처리한 결과
를 반환한다
"""
class AttentionHead(nn.Module):
def __init__(self,embed_dim, head_dim):
super().__init__()
self.q = nn.Linear(embed_dim, head_dim)
self.k = nn.Linear(embed_dim, head_dim)
self.v = nn.Linear(embed_dim, head_dim)
def forward(self,hidden_states):
attn_outputs= self_attention(
self.q(hidden_states),
self.k(hidden_states),
self.v(hidden_states))
return attn_outputs
"""
멀티헤드 어텐션을 종합적으로 구현한다
"""
class MultiHeadAttention(nn.Module):
"""
config= AutoConfig.from_pretrained(model_ckpt)
로 기존 사전학습모델의 정보를 활용하는 것으로 보임
"""
def __init__(self,config):
super().__init__()
embed_dim = config.hidden_size
num_heads= config.num_attention_heads
head_dim = embed_dim // num_heads
self.heads = nn.ModuleList(
[AttentionHead(embed_dim,head_dim) for _ in range(num_heads)]
)
self.output_linear = nn.Linear(embed_dim,embed_dim)
def forward(self,hidden_states):
"""torch.cat: 주어진 차원을 기준으로 텐서를 붙인다concatenate"""
x= torch.cat([h(hidden_states) for h in self.heads],dim=-1)
x= self.output_linear(x)
return x
multihead_attn = MultiHeadAttention(config)
attn_output= multihead_attn(inputs_embeds)
attn_output.size()
from bertviz import head_view
from transformers import AutoModel
model = AutoModel.from_pretrained(model_ckpt,
output_attentions=True)
sentence_a = "time flies like an arrow"
sentence_b = "fruit flies like a banana"
viz_inputs = tokenizer(sentence_a,sentence_b, return_tensors='pt')
attention= model(**viz_inputs).attentions
sentence_b_start= (viz_inputs.token_type_ids ==0).sum(dim=1)
tokens =tokenizer.convert_ids_to_tokens(viz_inputs.input_ids[0])
head_view(attention,tokens,sentence_b_start,heads=[8])
"""
가중치 학습 결과 첫번째 문장의 'flies'에서 가장 중요하게 여긴 단어로
'arrow'가 꼽혔고, 두번째 문장의 학습 결과로 'fruit'와 'banana'를 선택하였다
모델이 문맥에 따라 'flies'가 어떤 의미인지, 명사인지 동사인지 구별한 것이다
"""
flowchart BT
A[x] --> B;
B[F_1=xW_1+b_1] --> C;
C["F_2= max(0,F_1)"] --> D ;
D["F_3= F_2W_2+b_2"];
N,query_len,embed_dim
으로 이루어진 텐서를 N,query_len,intermediate_size
> N,query_len,embed_dim
순으로 변환시킴)class FeedForward(nn.Module):
def __init__(self,config):
"""AutoCOnfig 클래스를 활용하여 bert-base-uncased
체크포인트와 관련된 config.json 파일을 로드한다
트랜스포머에서의 모든 체크포인트는 vocab_size,hidden_size와
같은 다양한 파라미터가 지정된 설정 파일이 할당된다
이 예제 같은 경우 각 입력 단어의 정수ID가 nn.Embedding에 저장된
30,522개의 임베딩 벡터중 하나에 매핑되고 이때, 벡터의 크기는
768이다
피드 포워드 층에 (batch_size,seq_len,hidden_len)의 텐서가
입력되면 각 다른 (batch_size,seq_len)마다 진행함
"""
self.linear_1 = nn.Linear(config.hidden_size,config.intermediate_size)
self.linear_2 = nn.Linear(config.intermediate_size,config.hidden_size)
self.gelu= nn.GELU()
self.dropout = nn.Dropout(config.hidden_dropout_prob)
def forward(self,x):
x= self.linear_1(x)
x= self.gelu(x)
x= self.linear_2(x)
x= self.dropout(x)
return x
flowchart BT
A[x] --> B;
B["F(x)"] --> C;
A[x] -->C;
C["H(x)=x+F(x)"];
"""
종합되어 작동하는 Transformer의 Encoder-layer
입력 텐서와 출력 텐서의 크기가 같다는 특징을 갖고 있다
단 아직 토큰의 위치정보를 활용하지 않았다
"""
class TransformerEncoderLayer(nn.Module):
def __init__(self,config):
super().__init__()
"""
각각의 batch마다 전체 channel에 대한 normalization을 새행한다
"""
self.layer_norm_1 = nn.LayerNorm(config.hidden_size)
self.layer_norm_2= nn.LayerNorm(config.hidden_size)
self.attention= MultiHeadAttention(config)
self.feed_foward =FeedForward(config)
def forward(self,x):
#층 정규화를 적용하여 입력을 쿼리,키,값으로 복사한다
hidden_states= self.layer_norm_1(x)
#어텐션에 스킵 연결을 적용한다
x= x + self.attention(hidden_states)
#스킵 연결과 피드포워드 층을 적용한다
x= x+ self.feed_foward(self.layer_norm_2(x))
return x
#입력 임베딩으로 테스트하기
encoder_layer= TransformerEncoderLayer(config)
inputs_embeds.shape ,encoder_layer(inputs_embeds).size()
"""위치정보를 임베딩 하기 위하여 사용하는 방법
"""
class Embeddings(nn.Module):
def __init__(self,config):
super().__init__()
self.token_embeddings= nn.Embedding(
config.vocab_size,
config.hidden_size
)
self.positional_embeddings= nn.Embedding(
config.max_position_embeddings,
config.hidden_size
)
self.layer_norm = nn.LayerNorm(
config.hidden_size, eps=1e-12
)
self.dropout = nn.Dropout()
def forward(self,input_ids):
"""
1.input_ids의 seq_length를 확인한다.
2.해당 길이만큼의 1,2,...,seq_lenth-1을 담은 배열을 만든다
3.토큰들을 임베딩 벡터로 변환한다
4.config.max_positional_embeddings를 활용하여 임베딩을
시행한다
5.임베딩된 결과를 더하고 LN과 dropout을 시행하여 반환한다
"""
seq_length= input_ids.size(1)
positional_ids= torch.arange(seq_length,
dtype=torch.long).unsqueeze(0)
token_embeddings= self.token_embeddings(input_ids)
positional_embeddings= self.positional_embeddings(positional_ids)
embeddings= token_embeddings+ positional_embeddings
embeddings= self.layer_norm(embeddings)
embeddings= self.dropout(embeddings)
return embeddings
"""여기서 inputs는 텍스트 시퀀스를 정수 인코딩한 값임"""
embedding_layer = Embeddings(config)
embedding_layer(inputs.input_ids).size()
![[PositionalEncoding.jpg]]
class TransformerForSequenceClassification(nn.Module):
def __init__(self,config):
"""
Encoder를 거쳐 나온 결과값이 num_label개만큼만
되도록 config.num_labels를 통하여 설정한다
해당 결과값은 정규화되지 않은 logit이다
"""
super().__init__()
self.encoder =TransformerEncoder(config)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
self.classifier = nn.Linear(config.hidden_size, config.num_labels)
def forward(self,x):
x =self.encoder(x)[:,0,:] #[CLS]토큰의 은닉상태를 선택한다
x= self.dropout(x)
x= self.classifier(x)
return x
config.num_labels= 3
encoder_classifier =TransformerForSequenceClassification(config)
encoder_classifier(inputs.input_ids).size()
"""
torch.tril():하삼각행렬lower triangular matrix를 만드는 명령어
대각선을 포함하여 아래 성분들은 1이고, 대각선 위는 모두 0인 형태이다
이후 Tensor.maksed_fill()을 활용하여 0을 음의 무한대로 바꾸면
어텐션 헤드가 미래 토큰에 대한 정보를 참조할 수 없게 된다
"""
seq_len = inputs.input_ids.size(-1)
mask= torch.tril(torch.ones(seq_len,seq_len)).unsqueeze(0)
mask[0]
def scaled_dot_product_attention(query,key,value,mask=None):
"""
쿼리 형태: (N,query_len,embed_dim)
키 형태: (N,key_len,embed_dim)
에너지 형태: (N,heads,query_len,key_len)
"""
dim_k= query.size(-1) #임베딩 벡터 차원의 sqrt로 scaling해줌
scores= torch.einsum('ijk,ilk->ijl',[query,key])/sqrt(dim_k)
if mask is not None:
scores= scores.masked_fill(mask==0, float('-1e+10'))
weights= F.softmax(scores,dim=-1)
"""기존 방법 : attn_outputs= torch.bmm(weights,value)
attention shape: (N,query_len,key_len)
value shape: (N,value_len,embed_dim)
result: (N,query_len,embed_dim)
여기선 key_len과 value_len이 같은것을 활용"""
attn_output= torch.einsum('ijk,ijl->ikl',[weights,value])
return attn_output