NLP/AI기술 자연어처리 전문가 양성 과정 3기_NLP

Day 47. Transformers

이니니 2022. 7. 6. 13:39
transformer 모델을 쉽게 불러오고 학습시킬 수 있게 하는 라이브러리인 transformers에 대해 알아보도록 하겠습니다.
transformers 라이브러리는 NLP분야에서 transformer 기반 모델을 사용하고자 할 때 가장 많이 활용되는 라이브러리이며, 연구자와 실무자 가릴 것 없이 매우 많이 사용하고 있습니다. 

 

 

AI에서 transformer와 bert가 처음 나왔을 때, 모델을 구현하는 코드들이 곳곳에 퍼져있어서 원하는 코드를 찾으려면 많은 시간이 걸렸다. 따라서, 자연어처리 연구자들이 이러한 코드들을 한 번에 모아놓은 library가 huggingface에 존재하는 transformers library이다.

transformers는 다양한 nlp 딥러닝 모델들, 그 가중치들을 제공하고 있다. transformers는 pytorch 기반이다.

 

 

이러한 library들을 실제로 사용해보자. BERT 모델을 사용한다고 하자.

① transformers를 설치해야 한다.

* conda를 사용할 경우
>>> conda install transformers -c huggingface


* pip을 사용할 경우
>>> pip install transformers

# 설치된 버전 확인
>>> pip list | grep transformers

 

 

설치 후, BERT라는 모델을 사용하는데 필요한 것이 3가지 정도 있다.

㉮ BERT의 아키텍처

    - BERT의 아키텍처는 transformer 모델의 encoder 구조와 동일하다.(코드도 같음)

㉯ BERT의 가중치

    - 아키텍처와 가중치는 별개이다. 모델의 아키텍처 안의 각각의 파라미터를 가중치라고 한다. 가중치는 모델 자체의 값이다. transformer encoder 아키텍처에 BERT의 가중치를 넣는다면, 그 모델은 BERT가 될 것이다.

㉰ BERT의 Tokenizer

    - 모델 아키텍처와 tokenizer는 별개이다. 그러나, BERT는 가중치까지 포함된 개념이므로, 이 가중치는 특별한 tokenizer를 필요로 한다. BERT를 사전학습 시킬 때, 특정한 tokenizer와 아키텍처 기반으로 했기 때문이다.

 

 

여기서 문제가 하나 발생한다. BERT의 아키텍처와 tokenizer는 모델의 코드를 짤 수 있다면, 만들 수 있다. 하지만, BERT의 가중치를 확보하는 것은 매우 힘든 일이다. 가중치를 확보한다는 것은, 수많은 학습이 필요하다는 것이고, 수많은 데이터가 필요하다는 의미이다. BERT를 학습시키기 위해서는 매우 많은 GPU도 필요할 것이다. 

그렇기 때문에, huggingface의 library에서 이러한 가중치를 공개해서 사람들이 쉽게 사전학습된 가중치를 가지고 적용할 수 있도록 돕는다.

 

 

huggingface에서 BERT페이지에 들어가면, BERT에 대한 설명이 나와있고, 어떻게 쓰는지에 대한 설명이 나와있다.

BERT를 쓰기 위해서는 layer가 몇 층인가, hidden dimension의 크기가 얼마인가, vocab size는 얼마여야하는가 등의 정보가 필요하다.

이러한 모델의 아키텍처에 대한 설정을 잡아주는 것을 config에서 볼 수 있다. config의 정보를 사용하면, 원하는 형태의 아키텍처를 만들 수 있다.

config는 다른 모델에서도 존재한다.

 

 

위에서 말했듯이, Tokenizer도 필요하다. BertTokenizer를 이용해 쉽게 사용할 수 있다.

 

 

또한, BertModel에 대한 class도 제공하고 있다.

 

그 외에도 BERT를 사용하는데 필요한 여러 가지 class들을 제공하고 있다.

 


꿀팁!!!

vscode나 pycharm같은 개발환경을 사용할 때, colab처럼 셀 단위로 실행시키고 싶다면 다음과 같이 하자. code 1과 code 2를 셀 단위로 실행시킬 수 있을 것이다.

#%%

code 1

#%%

code 2


 

다음으로 코드를 작성해보자. 

위에서 말한 라이브러리들을 import해주자.

from transformers import BertConfig, BertModel

 

BERT모델의 아키텍처만 만들고 싶다면, 다음과 같이하자. config 값들을 바꾸어도 된다.

config = BertConfig(
    hidden_size = 256,
    num_hidden_layer = 4,
    num_attention_heads = 8,
    # intermediate란, Feed-Forward-network 안에서의 hidden dimension이다.
    intermediate_size = 1024,   # 보통 intermediate_size는 hidden_size의 4배 정도로 설정한다.
    hidden_act = 'relu'   # gelu가 아닌 다른 함수를 사용해도 된다.
)

print(config)

결과는 다음과 같다.

우리가 원하는 대로 config 파일을 생성하였다.

 

이 파일을 기준으로 새로운 BERT 모델을 생성하고 싶다면(transformer encoder 모델만 필요하고, BERT의 가중치는 필요하지 않을 경우), 아래와 같이 실행해주면 된다.

model = BertModel(config)

print(model)

config에서 구성한대로 새로운 BERT encoder 모델이 생성된 것을 알 수 있다.

 

 

이 모델을 사용하기 위해서는, input을 적절히 넣어주어야 한다.

huggingface페이지의 BertModel에서의 forward를 살펴보면, 여러 가지 파라미터들이 존재하지만, 사용하는 것을 정해져 있다고 봐도 된다.

먼저, input_ids는 우리가 알고 있는 token id에 해당한다. 이는 input 문장을 tokenize 하고 난 후, 숫자로 매핑한 것을 말한다.

 

다음으로, attention_mask가 많이 쓰인다. attention_mask는 배치화를 했을 때, sentence 길이가 맞지 않으면, 길이가 짧은 부분에 0으로 padding을 넣어 모든 sentence의 길이를 맞춰주어야 한다.

이 padding에 해당하는 부분은 transformer encoder가 참조하면 안된다. 그래서, 실제로 참조해야하는 부분을 1로 변환하고, 참조하지 않아야하는 부분을 0으로 치환해주는 것이 attention_mask이다.

>>> padded_sequences["attention_mask"]

[[1, 1, 1, 1, 1, 1, 1, 1, 0, 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]]

만약, 문장이 하나만 있다면, 0으로 padding해주는 과정 없이 그 문장 길이를 그대로 받을 것이다.

 

다음으로, token_type_ids를 쓴다. BERT의 NSP구조에서 앞 문장과 뒷 문장을 구분하기 위해 사용한다. 앞 문장을 0으로 넣어주고, 뒷 문장을 1로 넣어준다.

 

다음으로, position_ids를 사용한다. transformer라는 것은, 단어 간의 위치가 달라져도 self-attention이기 때문에 순서가 영향을 끼치지 않는다. 그래서 순서에 대한 정보를 position_ids로 넣어준다.

위 네 가지를 전부 사용했다면, 다음과 같이 쓸 수 있다.

import torch

# input_ids
input_ids = torch.tensor([[101, 138, 18696, 155, 1942, 3190, 1144, 1572, 13745, 1104, 159, 9664, 2107, 102]])

# attention_mask
attention_mask = torch.ones_like(input_ids) # input_ids와 똑같은 1로 구성된 배열을 얻을 수 있다.

# token_type_ids
token_type_ids = torch.zeros_like(input_ids)

# position_ids
position_ids = torch.arange(0, len(input_ids[0]))[None, :]

output = model(input_ids = input_ids, attention_mask = attention_mask, token_type_ids = token_type_ids,
               position_ids = position_ids)

print(output)

position_ids를 지칭하지 않았을 때에는 자동으로 0부터 숫자를 붙여준다. token_type_ids도 넣어주지 않으면, 0으로 된 기본값으로 자동으로 들어간다. 또한, 문장의 길이가 전부 같다면, attention_mask도 사용하지 않아도 된다.(다 참조함) 

사실, 위의 예제는 input_ids만 넣어주어도 위의 출력과 같을 것이다.

 

 

위 예제에서 output에는 어떠한 것들이 있을까? BERT모델의 output을 보려면, BertForPreTrainingOutput을 사용해야 한다. 여기에서는 hidden_state, loss 등을 살펴볼 수 있다.

 

위 예제에서의 output의 결과로, bertmodel과 projection의 사이의 last_hidden_state가 저장될 것이다.

huggingface에서 제공하는 output은 특이한 특성을 갖는다. 다음과 같이 두 가지처럼 모두 사용할 수 있다. 이들은 모두 last_hidden_state를 구하는 것이다. dictionary형태로도 사용 가능하지만, .을 붙여서 사용할 수도 있다.

 

 

위의 예제에서는 bertmodel 위에 projection을 얹은 모델을 구현했지만, bertmodel과 projection 모두를 포함하는 모델로써 바로 사용할 수 있는 것은 BertForMaskedLM이다.

 

만약, BERT의 사전학습을 시키고 싶다면, BertConfig에서 원하는 크기를 만들고 BertForMaskedLM안에 넣어 학습시키면 된다. 

 

 

 

지금까지는 BERT의 아키텍처를 만드는 과정이었다. 우리가 BERT를 사용하기 위해서는 huggingface에서 가중치를 다운받아 적용해야 한다. huggingface안의 models에는 다양한 가중치들을 제공한다.

예를 들어, bert-base-uncased는 대소문자를 전부 소문자로 바꿔주고, base 크기로 학습시킨 모델이다.

다음과 같이 사용한다면, huggingface에 존재하는 가중치들을 쉽게 사용할 수 있다.

config = BertConfig.from_pretrained('bert-base-uncased')

print(config)

이를 살펴보면, bert-base-uncased 모델은 BertForMaskedLM으로 학습되어져 있고, 파라미터들의 값을 확인할 수 있다. 또한, model_type이 bert라는 것도 알려주고 있다.

 

huggingface에서는 AutoConfig도 제공하고 있다. 다음과 같이 사용해보자

from transformers import AutoConfig

config = AutoConfig.from_pretrained('bert-base-uncased')

print(config)

이는 BertConfig처럼 새로 만드는 것은 불가능하지만, 그저 가중치들을 들고 올 경우에는, 자동으로 그 모델의 config를 들고온다. 출력이 위의 예와 같은 것을 알 수 있다.

 

AutoModelForSequenceClassification를 사용하면, 자동으로 bert-base-uncased에 해당하는 모델도 바로 들고오기도 한다.
from transformers import AutoModelForSequenceClassification

model = AutoModelForSequenceClassification.from_pretrained('bert-base-uncased')

print(model)

AutoModel을 사용할 수도 있지만, 지금은 BertMaskedLM을 사용해서 진행해볼 것이다.

 

 

하지만, 절대로 아래와 같이 해서는 안된다.

config = BertConfig.from_pretrained('bert-base-uncased')

model = BertForMaskedLM(config)

print(model)

이는 config파일을 기반으로 새로운 BertForMaskedLM을 만들어준 것이다. 이것은 가중치를 random initialization 시켜준 것이기 때문에 의미가 없다. Bert에 대한 가중치가 없기 때문이다.

 

그러므로, 아래와 같이 사용하도록 하자. 이 모델의 가중치들은 bert-base-uncased에서 가져온 가중치와 같다.

model = BertForMaskedLM.from_pretrained('bert-base-uncased')

print(model)

print로 출력되는 값은 위와 아래의 예시가 전부 같을 것이다. 모델 아키텍처는 같기 때문이다. 하지만, 위의 예제는 random initialization 된 것이지만, 아래의 예제는 사전학습이 된 것을 가져온 점이 다르다.

 

 

다음으로는 BERT의 tokenizer를 가져와보자. BertTokenizer는 BERT의 기본 tokenizer이다. BertTokenizerFast는 Ruby 기반으로 쓰여진 속도가 빠른 tokenizer이다. 상황에 맞게 사용해보자

tokenizer도 BERT의 가중치를 갖는 것을 사용해야하기 때문에, 아래와 같이 사용해야한다.

from transformers import BertForMaskedLM, BertTokenizerFast

tokenizer = BertTokenizerFast.from_pretrained('bert-base-uncased')
model = BertForMaskedLM.from_pretrained('bert-base-uncased')

print(tokenizer)

tokenizer도 기능이 많다. 위의 예제로 tokenizer를 출력하면, 아래와 같은 정보를 얻을 수 있다. BERT의 vocab size나 input dimension도 같은 것을 볼 수 있다.

PreTrainedTokenizerFast(name_or_path='bert-base-uncased', vocab_size=30522, model_max_len=512, is_fast=True, padding_side='right', truncation_side='right', special_tokens={'unk_token': '[UNK]', 'sep_token': '[SEP]', 'pad_token': '[PAD]', 'cls_token': '[CLS]', 'mask_token': '[MASK]'})

 

tokenizer를 설정한 후, 어떠한 문장을 tokenizing하고 싶다면, 바로 사용하면 된다. 아래와 같이 출력될 것이다.

 

만약, token_type_ids와 attention_mask가 필요하지 않다면 다음과 같이 해주면 된다.

 

이처럼, huggingface에서 제공하는 tokenizer들은 다른 모델들에서도 비슷한 역할을 수행한다. 

tokenizer의 또 다른 기능으로는, list 형태로 출력되는 값을 pytorch 형태로 바꿀 수도 있다. input_ids가 배치화된것도 볼 수 있다.

 

 

이렇게 나온 input_ids를 decoding하고 싶다면, 다음과 같이 해주자.

input_ids = tokenizer('Some Sentence').input_ids

print(input_ids)
print(tokenizer.decode(input_ids))

[CLS] 토큰과 [SEP] 토큰까지 붙여진 것을 볼 수 있다.

 

조금 더 구체적인 토큰화 과정을 알고 싶다면, 다음과 같이 해주자.

input_ids = tokenizer('Some Sentence').input_ids

print(input_ids)
print(tokenizer.decode(input_ids))
print(tokenizer.convert_ids_to_tokens(input_ids))

 

tokenizer는 자동으로 배치화(sentence길이가 맞지 않는 것을 맞춰주는 것. padding으로 함)까지 해주고, sentence를 여러 개로 받는 것도 가능하다.

tokenizer(['Some sentence', 'I like it!'], padding=True, return_tensors='pt')

이외에도 여러가지 메소드들이 존재한다.

 

 

이것들을 실제로 사용해보자.

encodings = tokenizer('We are very happy to [MASK] you the Transformers library', return_tensors = 'pt')

print(encodings)

 [MASK]가 103 token으로 잘 들어가 있는 것을 볼 수 있다.

위 문장에서 [MASK]에는 아마 'show'가 들어가야 할 것이다. 이 모델이 MLM로 잘 학습되었다면, 옳은 결과를 내보일 것이다.

encodings가 dictionary형태이기 때문에 조금 더 쉽게 표현해보자. keyword unpacking(**)을 사용하자.

outputs = model(**encodings)

print(outputs)

MLM에 대한 output을 확인할 수 있다. 여기는 logit value만 있다. logit value는 최종적으로 나온 projection일 것이다. 

 

이를 다시 tokenizing을 해보자. [MASK]로 잘 들어가 있다.

tokenizer.convert_ids_to_tokens(encodings.input_ids[0])

 

logit value를 argmax로 구하게 된다면 다음과 같다.

outputs.logits.argmax(-1)

이는 각 차원에서 가장 확률이 높았던 token을 받게 될 것이다. 맨 처음 토큰과 맨 마지막 토큰은 [CLS], [SEP]이므로, 이들을 제외한다면 다음과 같다.

outputs.logits.argmax(-1)[0, 1:-1]

이것을 다시 decode해보자

tokenizer.decode(outputs.logits.argmax(-1)[0, 1:-1])

 

[MASK] 부분이 show로 알맞게 출력된 모습을 볼 수 있다!!

 

하지만, 우리는 down stream task을 하기 위한 코드를 사용하고 싶을 것이다. BERT는 다른 task를 진행하기 위한 여러 가지 코드도 존재한다. huggingface 페이지에 자세히 나와있다.

 


 

huggingface에서는 datasets라는 library도 제공한다. 여기서는 아주 유명한 dataset들과 사람들이 올린 dataset들도 가져올 수 있다. 데이터셋은 다음과 같이 불러올 수 있다. 우리는 유명한 IMDB라는 영화리뷰 데이터셋을 들고 올 것이다.

먼저 설치해주는 것을 잊지 말자

pip install datasets
from datasets import load_dataset

data = load_dataset('imdb')

print(data)

위와 같은 데이터셋들이 있는 것을 알 수 있다.

dictionary 형태로 세부사항을 볼 수 있다.

 

첫 번째 데이터를 출력하면 다음과 같다.

data['train'][0]

 

이러한 데이터들을 여러 개 출력하는 것도 가능하다.

 

데이터를 살펴보면, 전처리가 안된 것을 알 수 있다. 전처리해주는 코드를 짜보자. 여기서는 <br />만 삭제해 줄 것이다.

huggingface에는 map이라는 메소드로 for문 없이 모든 data를 processing시킬 수 있어서 유용하게 사용가능하다.

import re

def preprocess(sample):
    return {
        'text' : ' '.join(re.sub(r'<[^(?:/>)]+/>', ' ', sample['text']).split()),
        'label' : sample['label']
    }

preprocessed = data.map(preprocess)

<br />이 전부 없어진 것을 확인할 수 있다.

 

똑같은 방식으로, tokenizing하는 전처리도 코드를 짜보자.

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased', use_fast = True)
preprocessed = preprocessed.map(
    lambda sample : dict(tokenizer(sample['text'], truncation = True)),
    remove_columns = ['text'],
    batched = True
)
preprocessed

위와 같은 결과를 얻을 수 있다. 자세한 것을 출력해보면 전처리가 잘 된 것을 볼 수 있다.

하지만, 이들은 길이가 각각 다 다르기 때문에 학습을 시키기 위해서는 padding을 해주어야 한다. 이것도 transformers에서 편리하게 제공하고 있다.

from transformers import DataCollatorWithPadding

collator = DataCollatorWithPadding(tokenizer)

 

또한, data loader도 만들어주어야 한다. 우리는 torch안에 존재하는 dataloader를 사용해보도록 하자.

from torch.utils.data import DataLoader

train_loader = DataLoader(preprocessed['train'], batch_size=16, collate_fn = collator)
next(iter(train_loader))

이처럼 잘 배치화된 output을 볼 수 있다. 한 가지를 뽑아 shape을 살펴보면 다음과 같다.

next(iter(train_loader))['input_ids'].shape

batch size가 16이고, 이전의 코드에서 truncation을 사용하였기 때문에, 최대 길이인 512임을 알 수 있다.

 

이제 모델을 들고와보자. 다음과 같이 학습시킬 수 있다.

from transformers import AutoModelForSequenceClassification

model = AutoModelForSequenceClassification.from_pretrained('bert-base-uncased')
#model.cuda()


import torch

optimizer = torch.optim.AdamW(model.parameters())


for epoch in range(3):
    print(f'Epoch : {epoch}')
    model.train()
    for encodings in train_loader:
        encodings = {key : value.cuda() for key, value in encodings.items()}
        outputs = model(**encodings)
        outputs.loss.backward()
        print('\rLoss: ', outputs.loss.item(), end = '')
        optimizer.step()
        optimizer.zero_grad()

 

huggingface에서 train code도 쉽게 짤 수 있게끔 제공해준다. 예시는 다음과 같다.

from transformers import TrainingArguments, trainer

training_args = TrainingArguments(
    num_train_epochs = 3,
    per_device_train_batch_size = 16,
    output_dir = 'dump/test',
)

trainer = Trainer(
    model=model,
    args = Training_args,
    train_dataset = preprocessed_train,
    eval_dataset = preprocessed_val,
    data_collator = collator
)

 

이제, huggingface의 전반적인 개요를 알았습니다!  transformers library를 직접 사용해봅시다.

'NLP > AI기술 자연어처리 전문가 양성 과정 3기_NLP' 카테고리의 다른 글

Day 49. Applications  (0) 2022.07.07
Day 48. GPT  (0) 2022.07.06
Day 45. NLP Quiz2  (0) 2022.06.20
Day 44. Transformer  (0) 2022.06.19
Day 43. Tokenization  (0) 2022.06.15