또르르's 개발 Story

[20-2] HuggingFace's Transformers - GPT-2 본문

부스트캠프 AI 테크 U stage/실습

[20-2] HuggingFace's Transformers - GPT-2

또르르21 2021. 2. 21. 17:28

HuggingFace는 Transformer에 기반한 다양한 모델을 제공합니다.

Huggingface's의 다양한 모델과 사용법들은 아래 링크에 존재합니다.

 

https://huggingface.co/transformers/index.html

 

Transformers — transformers 4.3.0 documentation

Blenderbot (from Facebook) released with the paper Recipes for building an open-domain chatbot by Stephen Roller, Emily Dinan, Naman Goyal, Da Ju, Mary Williamson, Yinhan Liu, Jing Xu, Myle Ott, Kurt Shuster, Eric M. Smith, Y-Lan Boureau, Jason Weston. Ble

huggingface.co

https://github.com/huggingface/transformers

 

huggingface/transformers

🤗Transformers: State-of-the-art Natural Language Processing for Pytorch and TensorFlow 2.0. - huggingface/transformers

github.com

https://huggingface.co/models

 

Hugging Face – On a mission to solve NLP, one commit at a time.

Text Classification • Updated Dec 11, 2020 • 507k

huggingface.co

 

1️⃣ 설정

 

transformers를 install합니다.

!pip install transformers

필요한 모듈을 import 합니다.

from transformers import *

from torch import nn

from tqdm import tqdm


import torch

 

 

2️⃣ GPT-2 불러오기

 

Pre-train된 GPT-2의 config, tokenizer, model을 각각 불러올 수 있습니다.

gpt_name = 'gpt2'

from_pretrained 함수는 대량 데이터로 훈련된 모델을 가지고 옵니다.

만약 from_pretrained로 불러오지 않으면 구조는 똑같지만, 훈련이 되지않은 모델을 가지고 옵니다.

주의할점은 model과 tokenizer가 같은 모델을 불러와야합니다. (small, base, large와 같이 model과 tokenizer는 같은 데이터로 학습되어 있기 때문에)

config = GPT2Config.from_pretrained(gpt_name)

tokenizer = GPT2Tokenizer.from_pretrained(gpt_name)

model = GPT2Model.from_pretrained(gpt_name)

config는 아래와 같은 구조를 가집니다.

>>> config


GPT2Config {
  "activation_function": "gelu_new",
  "architectures": [
    "GPT2LMHeadModel"
  ],
  "attn_pdrop": 0.1,
  "bos_token_id": 50256,
  "embd_pdrop": 0.1,
  "eos_token_id": 50256,
  "gradient_checkpointing": false,
  "initializer_range": 0.02,
  "layer_norm_epsilon": 1e-05,
  "model_type": "gpt2",
  "n_ctx": 1024,				# x embedding size
  "n_embd": 768,				# hidden size
  "n_head": 12,
  "n_inner": null,
  "n_layer": 12,
  "n_positions": 1024,
  "resid_pdrop": 0.1,
  "summary_activation": null,
  "summary_first_dropout": 0.1,
  "summary_proj_to_labels": true,
  "summary_type": "cls_index",
  "summary_use_proj": true,
  "task_specific_params": {
    "text-generation": {
      "do_sample": true,
      "max_length": 50
    }
  },
  "transformers_version": "4.3.2",
  "use_cache": true,
  "vocab_size": 50257
}

tokenizer는 vocab_size와 speical_tokens를 보여줍니다.

>>> tokenizer

# vocab_size=50257

# 'bos_token' : <SOS> token

# 'eos_token' : <EOS> toekn

# no <PAD> token : GPT2는 단방향이기 때문에 그 다음 token을 보지 않거나, 무시해버림

PreTrainedTokenizer(name_or_path='gpt2', vocab_size=50257, model_max_len=1024, is_fast=False, padding_side='right', special_tokens={'bos_token': AddedToken("<|endoftext|>", rstrip=False, lstrip=False, single_word=False, normalized=True), 'eos_token': AddedToken("<|endoftext|>", rstrip=False, lstrip=False, single_word=False, normalized=True), 'unk_token': AddedToken("<|endoftext|>", rstrip=False, lstrip=False, single_word=False, normalized=True)})

model은 각 Attention들과 in_feature와 out_feature을 보여주며, 최종 fine-tuning이 어떻게 나오는지 알려줍니다.

>>> model

GPT2Model(
  (wte): Embedding(50257, 768)
  (wpe): Embedding(1024, 768)
  (drop): Dropout(p=0.1, inplace=False)
  (h): ModuleList(
    (0): Block(
      (ln_1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
      (attn): Attention(
        (c_attn): Conv1D()
        (c_proj): Conv1D()
        (attn_dropout): Dropout(p=0.1, inplace=False)
        (resid_dropout): Dropout(p=0.1, inplace=False)
      )
      (ln_2): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
      (mlp): MLP(
        (c_fc): Conv1D()
        (c_proj): Conv1D()
        (dropout): Dropout(p=0.1, inplace=False)
      )
    )
    
    ...
    
    (11): Block(
      (ln_1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
      (attn): Attention(
        (c_attn): Conv1D()
        (c_proj): Conv1D()
        (attn_dropout): Dropout(p=0.1, inplace=False)
        (resid_dropout): Dropout(p=0.1, inplace=False)
      )
      (ln_2): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
      (mlp): MLP(
        (c_fc): Conv1D()
        (c_proj): Conv1D()
        (dropout): Dropout(p=0.1, inplace=False)
      )
    )
  )
  (ln_f): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
)

 

3️⃣ Tokenizer 사용

 

예시 문장은 아래와 같습니다.

sentence = "I want to go home."

1) token -> ids

예시 문장을 tokenizer 하면 다음과 같은 구성을 가지게 됩니다.

  • input_ids : (pre-trained 된) vocab 내에 정의된 index => 자동으로 앞에 101 '[CLS]', 뒤에 102 '[SEP]'를 붙여줌
  • token_type_ids : 문장을 index, 한 문장이기 때문에 다 0
  • attention_mask : padding(0으로 된 값)만 가려주는 값
output = tokenizer(sentence)


>>> output

{'input_ids': [40, 765, 284, 467, 1363, 13], 'attention_mask': [1, 1, 1, 1, 1, 1]}

혹은 직접 tokenize 함수를 호출할 수도 있습니다.

tokenized = tokenizer.tokenize(sentence)
>>> tokenized

['I', 'Ġwant', 'Ġto', 'Ġgo', 'Ġhome', '.']		# Ġ는 띄어쓰기를 character한 것

또한 다음과 vocabulary를 확인할 수 있습니다.

vocab = tokenizer.get_vocab()


>>> print(len(vocab))

50257

여기서 [endoftext] token은 vocab의 가장 마지막 token입니다.

>>> vocab['<|endoftext|>'] # 가장 마지막에 있는 token (end token)

50256

tokenizer의 convert_tokens_to_ids를 사용하면 token_ids를 쉽게 출력할 수 있습니다.

token_ids = tokenizer.convert_tokens_to_ids(tokenized)


>>> print(token_ids)

[40, 765, 284, 467, 1363, 13]

tokenizer의 encode를 사용하면 token_ids를 출력합니다.

token_ids = tokenizer.encode(sentence)


>>> print(token_ids)

[40, 765, 284, 467, 1363, 13]

2) ids -> token

반대로 원문으로 되돌릴 수도 있습니다.

sentence = tokenizer.convert_tokens_to_string(tokenized)


>>> print(sentence)

I want to go home.

마찬가지로 convert_ids_to_tokens 함수를 사용하면 ids를 token으로 되돌릴 수 있습니다.

tokens = tokenizer.convert_ids_to_tokens(token_ids)


>>> print(tokens)

['I', 'Ġwant', 'Ġto', 'Ġgo', 'Ġhome', '.']

convert_tokens_to_string 함수를 사용하면 string 형태로 나오게됩니다.

sentence = tokenizer.convert_tokens_to_string(tokens)


>>> print(sentence)

I want to go home.

 

 

4️⃣ 데이터 전처리

 

Sample data를 GPT-2에 넣을 수 있는 형태로 전처리합니다.

data = [
  "I want to go home.",
  "My dog's name is Max.",
  "Natural Language Processing is my favorite research field.",
  "Welcome. How can I help you?",
  "Shoot for the moon. Even if you miss, you'll land among the stars."
]

padding을 수행합니다.

max_len = 0

batch = []


for sent in tqdm(data):

  token_ids = tokenizer.encode(sent)
  
  max_len = max(max_len, len(token_ids))
  
  batch.append(token_ids)

tokenizer를 수행해 token을 id로 변환해줍니다.

pad_id = 0


for i, token_ids in enumerate(tqdm(batch)):

  if len(token_ids) < max_len:
  
    batch[i] = token_ids + [pad_id] * (max_len-len(token_ids))
batch = torch.LongTensor(batch)


>>> print(batch)

tensor([[   40,   765,   284,   467,  1363,    13,     0,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0],
        [ 3666,  3290,   338,  1438,   318,  5436,    13,     0,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0],
        [35364, 15417, 28403,   318,   616,  4004,  2267,  2214,    13,     0,
             0,     0,     0,     0,     0,     0,     0,     0],
        [14618,    13,  1374,   460,   314,  1037,   345,    30,     0,     0,
             0,     0,     0,     0,     0,     0,     0,     0],
        [ 2484,  1025,   329,   262,  8824,    13,  3412,   611,   345,  2051,
            11,   345,  1183,  1956,  1871,   262,  5788,    13]])
            
         
>>> print(batch.shape)

torch.Size([5, 18])

batch 크기만큼의 mask를 만들어줍니다.

batch_mask = (batch != pad_id).float()


>>> print(batch_mask)

tensor([[1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
         0., 0.],
        [1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0., 0.,
         0., 0.],
        [1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0.,
         0., 0.],
        [1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0., 0.,
         0., 0.],
        [1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
         1., 1.]])
         
         
>>> print(batch.shape)

torch.Size([5, 18])

 

 

5️⃣ GPT-2 사용

 

GPT-2 model에다가 batch와 attention_mask를 넣어줍니다.

outputs = model(input_ids=batch, attention_mask=batch_mask)

여기서 자세히 보면 0번째 index에 'last_hidden_state'가 존재하는 것을 알 수 있습니다.

따라서 hidden state의 마지막 layer 출력값을 가져옵니다.

# B: batch size, L: max length, d_h: hidden size

last_hidden_states = outputs[0]  # (B, L, d_h)


>>> print(last_hidden_states.shape)

torch.Size([5, 18, 768])

다음과 같이 fully connected layer를 하나 사용하여 다음 단어를 예측할 수 있습니다.

lm_linear = nn.Linear(config.hidden_size, config.vocab_size)
# V: vocab size
lm_output = lm_linear(last_hidden_states)  # (B, L, V)

>>> print(lm_output)

tensor([[[-0.7913,  3.6519,  3.5032,  ...,  0.6125, -0.1065, -0.1929],
         [-1.4443,  6.7288,  6.9894,  ...,  1.5242,  0.7410,  0.7647],
         [-4.0697, 12.1660, 12.2526,  ...,  0.4085,  2.7958, -0.4065],
         ...,
         [-3.7830, 11.3455, 12.0138,  ...,  2.5964,  0.9142,  1.3958],
         [-3.7818, 11.3579, 12.0331,  ...,  2.5883,  0.9291,  1.3894],
         [-3.7783, 11.3682, 12.0460,  ...,  2.5851,  0.9382,  1.3849]],

        [[-0.6966,  3.2385,  3.1239,  ...,  0.5808, -0.0614, -0.2034],
         [-1.9884,  9.2645,  8.7747,  ...,  1.5402,  1.4220, -0.1500],
         [-2.3617,  9.1607,  9.4420,  ...,  1.3571,  2.8291, -0.3722],
         ...,
         [-3.8542, 11.5922, 12.0352,  ...,  2.5888,  1.0430,  1.2891],
         [-3.8524, 11.6009, 12.0449,  ...,  2.5863,  1.0460,  1.2877],
         [-3.8581, 11.6095, 12.0552,  ...,  2.5826,  1.0502,  1.2887]],

        [[-0.7175,  2.7248,  2.6493,  ...,  0.2418,  0.0711, -0.1356],
         [-2.2305,  7.6880,  7.5684,  ..., -0.0646,  1.0513, -0.1847],
         [-2.2820,  7.6720,  8.2406,  ...,  0.2072,  2.1117,  0.1297],
         ...,
         [-3.7283, 11.3132, 11.9637,  ...,  2.9279,  1.1766,  1.7487],
         [-3.7396, 11.3168, 11.9674,  ...,  2.9194,  1.1816,  1.7538],
         [-3.7550, 11.3327, 11.9841,  ...,  2.9043,  1.1911,  1.7489]],

        [[-0.9455,  2.9493,  2.7908,  ..., -0.0831,  0.1867, -0.2864],
         [-2.5054,  9.1022,  9.3702,  ...,  2.7348,  0.2493,  1.3105],
         [-2.8008, 10.2121, 11.0764,  ...,  1.9745,  1.9565,  0.6839],
         ...,
         [-3.0636,  9.6677, 10.1732,  ...,  2.8035,  0.5857,  1.3267],
         [-3.0689,  9.6734, 10.1781,  ...,  2.7997,  0.5890,  1.3274],
         [-3.0759,  9.6843, 10.1862,  ...,  2.7965,  0.5934,  1.3289]],

        [[-0.5438,  3.2860,  3.1016,  ...,  0.7102, -0.2854, -0.2231],
         [-1.7813,  8.1649,  8.4487,  ...,  2.0985,  0.9220,  0.6952],
         [-1.7021,  7.9246,  7.5861,  ...,  1.8540,  0.9416,  0.5683],
         ...,
         [-2.9117, 10.7740, 11.3179,  ...,  1.7744,  1.6347, -0.3579],
         [-1.8014,  9.8287,  9.7994,  ...,  1.9349,  1.1544,  0.2743],
         [-4.6167, 11.6725, 12.0909,  ...,  2.3268,  1.1195,  1.3845]]],
       grad_fn=<AddBackward0>)


>>> print(lm_output.shape)

torch.Size([5, 18, 50257])

 

6️⃣ GPT-2 다양한 모델

 

GPT-2의 다양한 head를 추가한 모델을 제공합니다.

 

https://huggingface.co/transformers/model_doc/gpt2.html

 

OpenAI GPT2 — transformers 4.3.0 documentation

past_key_values (tuple(tuple(torch.FloatTensor)), optional, returned when use_cache=True is passed or when config.use_cache=True) – Tuple of tuple(torch.FloatTensor) of length config.n_layers, with each tuple having 2 tensors of shape (batch_size, num_he

huggingface.co

 

  • GPT2LMHeadModel : Language Modeling 수행
lm_model = GPT2LMHeadModel.from_pretrained(gpt_name)

GPT2LMHeadModel은 마지막 Linear에서 vocab size만큼의 vector를 출력합니다. (단어를 맞춰야 하기 때문)

>>> lm_model

GPT2LMHeadModel(
  (transformer): GPT2Model(
    (wte): Embedding(50257, 768)
    (wpe): Embedding(1024, 768)
    (drop): Dropout(p=0.1, inplace=False)
    (h): ModuleList(
      (0): Block(
        (ln_1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
        (attn): Attention(
          (c_attn): Conv1D()
          (c_proj): Conv1D()
          (attn_dropout): Dropout(p=0.1, inplace=False)
          (resid_dropout): Dropout(p=0.1, inplace=False)
        )
        (ln_2): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
        (mlp): MLP(
          (c_fc): Conv1D()
          (c_proj): Conv1D()
          (dropout): Dropout(p=0.1, inplace=False)
        )
      )

	...

      (11): Block(
        (ln_1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
        (attn): Attention(
          (c_attn): Conv1D()
          (c_proj): Conv1D()
          (attn_dropout): Dropout(p=0.1, inplace=False)
          (resid_dropout): Dropout(p=0.1, inplace=False)
        )
        (ln_2): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
        (mlp): MLP(
          (c_fc): Conv1D()
          (c_proj): Conv1D()
          (dropout): Dropout(p=0.1, inplace=False)
        )
      )
    )
    (ln_f): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
  )
  (lm_head): Linear(in_features=768, out_features=50257, bias=False)		# vocab size만큼 출력
)

GPT2LMHeadModel은 input_ids labels를 함께 줄 경우 자동으로 cross entropy loss까지 계산해줍니다.
labels가 주어지지 않을 경우엔 기존과 동일하게 결과만 주어집니다.

outputs = lm_model(input_ids=batch, attention_mask=batch_mask, labels=batch)
>>> outputs


CausalLMOutputWithCrossAttentions([('loss',				# loss 출력
                                    tensor(6.2792, grad_fn=<NllLossBackward>)),
                                   ('logits',
                                    tensor([[[ -39.3084,  -39.0100,  -41.8374,  ...,  -46.9337,  -44.9073,
                                               -39.5149],
                                             [ -68.6073,  -68.2685,  -74.1494,  ...,  -76.0179,  -78.3470,
                                               -72.2143],
                                             [-137.8778, -137.4907, -143.7934,  ..., -147.5330, -148.3120,
                                              -140.3080],
                                             ...,
                                             [-128.7599, -127.7347, -129.9467,  ..., -140.4377, -140.0011,
                                              -125.0607],
                                             [-128.9034, -127.8914, -130.1397,  ..., -140.5851, -140.1500,
                                              -125.2252],
                                             [-128.9673, -127.9750, -130.2378,  ..., -140.6508, -140.2140,
                                              -125.3020]],
                                              
                                              ...

loss는 outputs의 0번째에 존재합니다.

loss = outputs[0]


>>> print(loss)

tensor(6.2792, grad_fn=<NllLossBackward>)

logits는 outputs의 1번쨰에 존재합니다.

logits = outputs[1]


>>> print(logits.shape)

torch.Size([5, 18, 50257])

 

7️⃣ Special token 추가

 

경우에 따라선 별도의 special token을 추가하고 싶을 수 있습니다.

huggingface에는 special token을 추가할 수 있습니다.

 

GPT-2의 vocab은 50257 크기입니다.

>>> print(vocab)

{'!': 0, '"': 1, '#': 2, '$': 3, '%': 4, '&': 5, "'": 6, '(': 7, ')': 8, '*': 9, '+': 10, ',': 11, '-': 12, '.': 13, '/': 14, '0': 15, '1': 16, '2': 17, '3': 18, '4': 19, '5': 20, '6': 21, '7': 22, '8': 23, '9': 24, ':': 25, ';': 26, '<': 27, ..


>>> print(len(vocab))

50257

special token을 추가하기 위해 token 정의를 해줍니다.

special_tokens = {
    'bos_token': '[BOS]',
    'eos_token': '[EOS]',
    'pad_token': '[PAD]',
    'additional_special_tokens': ['[SP1]', '[SP2]']
}

tokenizer의 add_special_tokens 함수를 사용하면 tokenizer의 vocab에 token이 추가되는 것을 알 수 있습니다.

num_new_tokens = tokenizer.add_special_tokens(special_tokens)


>>> print(num_new_tokens)

5

vocab을 확인하면 50257개에서 5개가 추가된 50262개라는 것을 알 수 있습니다.

vocab = tokenizer.get_vocab()


>>> print(len(vocab)) # 5개가 증가한 것을 알 수 있다. (마지막에 추가됨)

50262

Special token을 추가했다면 거기에 맞게 모델의 embedding layer의 input size도 바꿔주어야 합니다.

>>> model.resize_token_embeddings(len(vocab))   # 바뀐 vocab size를 model 알려주서 input size를 변경해야함

Embedding(50262, 768)

model을 확인하면 embedding layer의 input size가 바뀐 것을 알 수 있습니다.

>>> model

GPT2Model(
  (wte): Embedding(50262, 768)			# input size가 변경
  (wpe): Embedding(1024, 768)
  (drop): Dropout(p=0.1, inplace=False)
  (h): ModuleList(
    (0): Block(
      (ln_1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
      (attn): Attention(
        (c_attn): Conv1D()
        (c_proj): Conv1D()
        (attn_dropout): Dropout(p=0.1, inplace=False)
        (resid_dropout): Dropout(p=0.1, inplace=False)
      )
      (ln_2): LayerNorm((768,), eps=1e-05, elementwise_affine=True)
      (mlp): MLP(
        (c_fc): Conv1D()
        (c_proj): Conv1D()
        (dropout): Dropout(p=0.1, inplace=False)
      )
    )
    
    ...

 

Comments