NLP/Transformers for NLP

3. Pretraining a RoBERTa Model from Scratch

메린지 2022. 3. 15. 21:25

이 챕터에서 우리는 scratch로부터의 RoBERTa model을 살펴볼 것이다. 이 모델은 BERT 모델에 필요한 트랜스포머 구조의 부분을 사용한다. 또한 여기서는 어떠한 사전학습된 토크나이저나 모델도 사용되지 않는다. RoBERTa model은 다음 15개의 스텝 과정으로 완성된다. 우리는 단계적으로 마스킹된 토큰으로 언어 모델링을 수행할 수 있는 모델에 대해 이전에 배운 트랜스포머의 지식을 사용한다. 챕터1에서 우리는 오리지날 트랜스포머의 구조에 대해서 배우고 챕터2에서 fine-tuning BERT 모델과 사전학습된 BERT를 fine-tuned하는 것을 배웠다. 여기서는 주피터 노트북과 HJugging Face의 아주 매끄러운 모듈을 기반으로하여 scratch로부터의 사전학습된 트랜스포머 모델을 쌓는 것에 대해 초점을 둘 것이다. KantaiBERT는 먼저 Immanuel Kant의 서적을 살펴봐야한다. 우리는 어떻게 데이터를 얻었는지 봐야한다. 이 장을 위해서 우리만의 데이터셋을 어떻게 생산해내야할지에 대해서 생각해야한다. KantaiBERT는 scratch부터의 그것만의 토크나이저를 학습했다. 사전 훈련과정동안 사용되는 병합 그리고 어휘 파일을 구축한다. KantaiBERT는 그리고 데이터셋을 실행하고 트레이너를 초기화하며 모델을 학습시킨다. 마지막으로 KantaiBERT는 실험적인 downstream 언어 모델링을 수행하기 위해 훈련된 모델을 사용하고 Immanuel Kant의 logic을 사용해 마스킹을 채운다.

Training a tokenizer and pretraining a transformer

이 챕터에서 우리는 KantaiBERT라는 명칭을 가진 트랜스포머를 BERT같은 모델에 대해 Hugging Face가 제공하는 구조를 이용하여 훈련시킨다. 우리는 챕터2에서 배운 모델의 구조 이론을 이용한다. 우리는 KantaiBERT를 이전 챕터에서 습득한 지식을 기반으로 쌓아올리며 설명한다. KantaiBERT는 Robustly Optimized BERT Pretraining Approach (RoBERTa) 이다 - BERT의 구조를 기반으로 했다. 최초의 BERT 모델은 미숙했다. RoBERTa는 downstream 업무에서의 사전학습 트랜스포머의 성능을 향상시켰다. RoBERTa는 사전학습 과정에서의 메커니즘을 발전시켰다, 예를 들어 이것은 WordPiece 토크나이저를 사용하지 않고 바이트 단위의 Byte Pair Encoding (BPE)을 사용했다. BERT같은 KantaiBERT는 마스킹된 언어모델을 사용하여 훈련된다. KantaiBERT는 6 layers, 12 heads, 84,095,008 파라미터가 있는 작은 모델로 훈련된다. 물론 매개변수의 개수는 충분히 커보일 수 있지만 6 layers와 12 heads에 포진되어 있기 때문에 상대적으로 작아지게 한다. 이 작은 모델은 사전학습 경험을 스무스하게 만들어서 각 단계가 결과를 보기 위해 기다리는 시간없이 실시간으로 보이도록 한다. KantaiBERT는 DistilBERT같은 모델인데 이것은 6 layers와 12heads로 같은 구조를 가지고 있기 때문이다. DistailBERT는 BERT의 distilled 버전인데, 우리는 이 커다란 모델이 엄청난 성능을 보여주는 것을 알고있다. 하지만 스마트폰에서 모델을 실행하고 싶으면 어떻게 할 것인가? 소형화는 기술적 진화의 주요 키이다. 트랜스포머는 실행되는 동안 같은 경로를 따라간다. Distilled한 버전의 BERT를 이용하는 Hugging Face의 접근법은 따라서 좋은 진보이다. 증류 혹은 미래의 다른 방식들은 사전학습 중 최고를 내는 명확한 방법이고 downstream 업무에서 필요한 효율적인 방법일 것이다. KantaiBERT는 GPT-2에서 사용된 적이 있는 Byte 단위의 byte 쌍 인코딩 토크나이저로 실행한다. 특별한 토큰은 RoBERTa에서 사용된다. BERT 모델은 종종 workpiece 토크나이저를 사용한다. 토큰이 segment의 어디 부분에 속하는지 지칭할 수 있는 토큰 타입 ID는 없다. segment는 토큰 </s>로 분리된다. KantaiBERT는 커스텀 데이터셋을 사용하고, 토크나이저를 훈련시키며, 트랜스포머 모델을 훈련시키고 저장하며 마스킹된 언어 모델링 예시를 작동한다.

Building KantaiBERT from scratch

우리는 KantaiBERT를 15단계를 거쳐 만들고 마스킹된 언어 모델링에 사용한다. 코랩에서 KantaiBERTipynb를 업로드한다. 15가지 스텝을 살펴본다.

Step 1: Loading the dataset

데이터셋을 사용할 수 있도록 준비하는 것은 훈련시킬 객관적인 방법을 제공하고 트랜스포머를 비교하도록 한다. 챕터4에서 우리는 여러 데이터셋을 탐구할 것이다. 그러나 이 챕터에서의 목표는 트랜스포머의 훈련과정을 이해하는 것이고 추가적인 시간 없이 실시간으로 실행을 확인하는 것이다. Immanuel Kant의 작업물을 사용하도록 고르고, 아이디어는 인간같은 로직과 downstream reasoning task를 위한 사전훈련 reasoning을 소개하는 것이다. kant.txt는 이 챕터에서 트랜스포머 모델을 훈련시키는 작은 훈련 데이터셋을 제공한다.

#@title Step 1: Loading the Dataset
#1.Load kant.txt using the Colab file manager
#2.Downloading the file from GitHub
!curl -L https://raw.githubusercontent.com/PacktPublishing/Transformers-for-Natural-Language-Processing/master/Chapter03/kant.txt --output "kant.txt"

Step 2: Installing Hugging Face transformers

우리는 트랜스포머와 토크나이저를 설치하지만 코랩 VM의 인스턴스 안에서 텐서플로우가 필요하지 않다.

#@title Step 2:Installing Hugging Face Transformers
# We won't need TensorFlow here
!pip uninstall -y tensorflow
# Install `transformers` from master
!pip install git+https://github.com/huggingface/transformers
!pip list | grep -E 'transformers|tokenizers'
# transformers version at notebook update --- 2.9.1
# tokenizers version at notebook update --- 0.7.0

Step 3: Training a tokenizer

이 부분에서 사전학습된 토크나이저를 사용하지 않는다. 그러나 훈련과정은 scratch로부터의 훈련된 토크나이저를 포함한다. ByteLevelBPETokenizer()는 kant를 이용하여 훈련된다. 바이트 단위의 토크나이저는 string이나 word를 sub-string이나 sub-word로 나눈다. 많은 것들 중에서 두 가지 메인 장점을 가진다.

  • 토크나이저는 작은 구성요소로 단어를 쪼갤 수 있다. 그리고 이러한 구성요소들을 통계적으로 유사한 것끼리 결합한다. 예로 "smaller"와 "smallest"는 "small" , "er", "est"로 되고, 토크나이저는 더 학습해서 "sm"과 "all"을 얻을 수 있다. 여기서 단어는 sub-words 토큰으로 쪼개질 수 있고 단순히 "small"이 아닌 더 작은 sub-word 부분이 된다.
  • string의 청크는 WorkPiece 단계 인코딩을 이용하여 unknown unk_token로 분류될 수 있고 실제로 사라진다.

이 모델에서 우리는 다음의 파라미터를 통해 토크나이저를 학습시킨다.

  • files = paths -> 데이터셋의 경로
  • vocab_size = 52_000 -> 토크나이저 모델의 길이
  • min_frequency = 2 -> 최소 frequency threshold
  • special_tokens = [] -> 특별 토큰 리스트

여기서 스페셜 토큰은

  • <s> : start
  • <pad> : padding
  • </s> : end
  • <unk> : unknown
  • <mask> : 언어 모델 mask token

토크나이저는 병합된 sub-string 토큰을 생산해서 훈련시키고 빈도를 분석한다. 우선 string을 토큰화한다.

['Ġthe', 'Ġtoken', 'izer']

위의 단어를 Ġ로 토큰화 될 것이다.

#@title Step 3: Training a Tokenizer
%%time
from pathlib import Path
from tokenizers import ByteLevelBPETokenizer

paths = [str(x) for x in Path(".").glob("**/*.txt")]

# Initialize a tokenizer
tokenizer = ByteLevelBPETokenizer()

# Customize training
tokenizer.train(files=paths, vocab_size=52_000, min_frequency=2, 
special_tokens=[
 "<s>",
 "<pad>",
 "</s>",
 "<unk>",
 "<mask>",
])

토크나이저는 훈련되고 저장될 준비가 되었다.

Step 4: Saving the files to disk

토크나이저는 학습될때 두 가지 파일을 생성한다.

  • 토큰화된 sub-strings를 병합한 것을 포함한 merges.txt
  • 토큰화된 sub-strings의 일부를 포함한 vocab.json
#@title Step 4: Saving the files to disk
import os
token_dir = '/content/KantaiBERT'
if not os.path.exists(token_dir):
 os.makedirs(token_dir)
tokenizer.save_model('KantaiBERT')

생성된 파일 두 개를 살펴보면,

#version: 0.2 - Trained by `huggingface/tokenizers`
# merges.txt
Ġ t
h e
Ġ a
o n
i n
Ġ o
Ġt he
r e
i t
Ġo f

# vocab.json
[…,"Ġthink":955,"preme":956,"ĠE":957,"Ġout":958,"Ġdut":959,"aly":960,"Ġexp":961,…]

Step 5: Loading the trained tokenizer files

이제 사전학습된 토크나이저 파일을 로드해야한다. 하지만 우리는 우리만의 토크나이저를 학습시켰기 때문에 그 파일을 로드한다.

#@title Step 5 Loading the Trained Tokenizer Files 
from tokenizers.implementations import ByteLevelBPETokenizer
from tokenizers.processors import BertProcessing
tokenizer = ByteLevelBPETokenizer(
 "./KantaiBERT/vocab.json",
 "./KantaiBERT/merges.txt",
)

토크나이저는 시퀀스를 인코딩할 수 있다.

tokenizer.encode("The Critique of Pure Reason.").tokens
['The', 'ĠCritique', 'Ġof', 'ĠPure', 'ĠReason', '.']

토큰화된 것을 볼 수 있을 뿐 아니라 개수도 알 수 있다.

tokenizer.encode("The Critique of Pure Reason.")
Encoding(num_tokens=6, attributes=[ids, type_ids, tokens, offsets, 
attention_mask, special_tokens_mask, overflowing])

결과는 6개로 나오고, 이제 토크나이저는 BERT 모델 형태에 맞게 맞춰져 처리된다. 전처리기는 start와 end 토큰을 추가한다.

tokenizer._tokenizer.post_processor = BertProcessing(
 ("</s>", tokenizer.token_to_id("</s>")),
 ("<s>", tokenizer.token_to_id("<s>")),
)
tokenizer.enable_truncation(max_length=512)

이제 전처리 시퀀스를 인코딩한다.

tokenizer.encode("The Critique of Pure Reason.")
Encoding(num_tokens=8, attributes=[ids, type_ids, tokens, offsets, 
attention_mask, special_tokens_mask, overflowing])

위와 다르게 결과는 8개로 나온다. start 와 end token이 추가되었기 때문이다.

Step 6: Checking resource constraints: GPU and CUDA

KantaiBERT는 GPU에서 최적의 속도를 낸다. 우리는 먼저 현재 NVIDIA GPU가 있는지 확인한다.

#@title Step 6: Checking Resource Constraints: GPU and NVIDIA 
!nvidia-smi

#@title Checking that PyTorch Sees CUDA
import torch
torch.cuda.is_available()

이제 True 결과가 출력되면 CUDA가 있는지 확인된 것이다. Compute Unified Device Architecture (CUDA)는 NVDIA에 의해 개발되었고 NVIDIA 카드의 작동 전원을 병렬적으로 사용한다.

Step 7: Defining the configuration of the model

우리는 RoBERTa-type 트랜스포머 모델을 DistillBERT 트랜스포머와 같은 수의 층과 헤드를 사용하여 사전학습시킬 것이다. 모델은 단어 사이즈 52,000, 12 attention heads, 6 layers를 가진다.

#@title Step 7: Defining the configuration of the Model
from transformers import RobertaConfig
config = RobertaConfig(
 vocab_size=52_000,
 max_position_embeddings=514,
 num_attention_heads=12,
 num_hidden_layers=6,
 type_vocab_size=1,
)

Step 8: Reloading the tokenizer in transformers

이제 우리는 우리가 학습시킨 토크나이저를 로드했고, 그 토크나이저는 RobertaTokenizer.from_pretained() 에 있다.

#@title Step 8: Re-creating the Tokenizer in Transformers
from transformers import RobertaTokenizer
tokenizer = RobertaTokenizer.from_pretrained("./KantaiBERT", max_length=512)

Step 9: Initializing a model from scratch

여기서 우리는 scratch의 모델을 초기화하고 모델의 사이즈를 검사한다. 먼저 RoBERTa를 import한다.

#@title Step 9: Initializing a Model From Scratch
from transformers import RobertaForMaskedLM
model = RobertaForMaskedLM(config=config)
print(model)

여기서 우리는 6 layer와 12 heads를 가진 BERT 모델이 출력되는 것을 볼 수 있다.

Exploring the parameters

84,095,008 파라미터를 포함하고 모델은 작다. 사이즈를 확인할 수 있다.

print(model.num_parameters())
# 84095008

파라미터를 자세히 살펴보기 위해 먼저 LP에 저장된 파라미터와 파라미터 리스트의 길이를 계산한다.

#@title Exploring the Parameters
LP=list(model.parameters())
lp=len(LP)
print(lp)
# 108

텐서 안에 있는 행렬과 벡터를 확인하면,

Parameter containing:
tensor([[-0.0175, -0.0210, -0.0334, ..., 0.0054, -0.0113, 0.0183],
 [ 0.0020, -0.0354, -0.0221, ..., 0.0220, -0.0060, -0.0032],
 [ 0.0001, -0.0002, 0.0036, ..., -0.0265, -0.0057, -0.0352],
 ...,
 [-0.0125, -0.0418, 0.0190, ..., -0.0069, 0.0175, -0.0308],
 [ 0.0072, -0.0131, 0.0069, ..., 0.0002, -0.0234, 0.0042],
 [ 0.0008, 0.0281, 0.0168, ..., -0.0113, -0.0075, 0.0014]],
 requires_grad=True)

어떻게 트랜스포머가 빌드되었는지 이해할 수 있다.

모델안에서 모든 파라미터를 합쳐서 모델의 파라미터 수를 계산한다. 예를 들면

< 단어(52,000) * 차원(768) / 메모리 벡터 사이즈 (1 * 768) / 많은 다른 차원 >

여기서 우리는 d_model = 768인 것을 알 수 있고, 12 heads를 가진다. 각헤드의 d_k의 차원은 따라서 d_k = d_model /12 = 64 이다. 다시 말하자면 이것은 트랜스포머의 구조 블럭의 최적화된 Lego concept이다. 이제 우리는 모델의 파라미터 수가 계산되고 84,095,008로 도달할 수 있게 되는지 알 수 있다. 우리는 더 나아가 각 텐서의 매개변수를 셀 수 있다. 먼저 프로그램에서 매개변수 카운터를 초기화하여 np (number of parameters)로 하고 파라미터 리스트 안의 lp 요소의 수를 통과한다. 2차원의 몇 파라미터를 확인할 수 있고 몇 개는 1차원이다. 찾을 수 있는 쉬운 방법은 시도하고 LP 안에 있는 파라미터가 2차원인지 아닌지 보는것이다. 만약 2차원 파라미터가 있다면 두번째 차원은 L2>0, PL2 = True일 것이다. 만약 1차원이면 L2=1, PL2=False이다. L1은 매개변수의 첫 차원의 크기이다. L3는 정의된 매개변수의 크기이다. 그리고 아까 np에 L3를 더해준다. 파라미터의 합계를 구할거지만 또한 우리는 정확히 트랜스포머의 매개변수의 수를 계산하는지 보고 싶어한다.