Colab에서 TPU로 BERT 처음부터 학습시키기 - Tensorflow/Google ver.

Colab에서 TPU로 BERT 처음부터 학습시키기 - Tensorflow/Google ver.

2018년말부터 현재까지 NLP 연구에서 BERT는 여전히 압도적인 위치를 차지하고 있다.

한편, BERT모델을 사용하는 이유 중 가장 큰 것 하나가 바로 한국어로 Pretrained된 모델이 있다는 점이다. Google에서 논문을 처음 공개했을 때 Multilingual pretrained model을 공개해 Fine-tuning만으로도 우리가 필요한 데이터셋에 맞춰 분류기를 만드는 등의 여러 응용이 가능하고, 동시에 높은 성능을 보여주었기 때문에 BERT 자체를 학습시키는 것에 대해서는 크게 신경쓰지 않은 것이 사실이다.

한편 작년 ETRI의 한국어 BERT 언어모델, 그리고 SKTBrain의 KoBERT 등 한국어 데이터셋으로 학습시킨 모델들이 등장했고, 이런 모델들을 Fine-tuning할 경우 기존 구글의 다국어 모델을 사용한 것보다 성능이 조금이라도 더 잘 나오기도 한다. (특히 정제되지 않은 글에 대해 좀 더 나은 성능을 보여줬다. OOV문제가 덜한 편이었다.)

다만 이런 모델들 역시 굉장히 ‘보편적’ 글로 학습된 것이라 도메인 특화된 분야에 있어서는 성능이 잘 나오지 않을 수도 있다. 따라서 특수한 경우의 특수한 도메인에 최적화된 Pretrained model을 만든다면 우리의 NLP 모델도 좀 더 성능이 좋아질 수 있다!

이번 글에서는 BERT 모델을 TPU와 Tensorflow를 이용해 처음부터 학습시켜보는 과정을 다뤄본다.

이번 글은 Colab Notebook: Pre-training BERT from scratch with cloud TPU를 기반으로 작성되었습니다.

어떤 환경에서 개발하나?

2020년 2월 26일자 기준 Google Colab에서 TPU를 활성화 시킨 상태에서 정상적으로 학습이 가능하다.

단, GCP 서비스 중 Cloud Bucket을 사용하기 때문에 활성화된 GCP 계정이 필요하다. (가입하면 1년 쓸 수 있는 $300을 준다!)

준비물

  • Google 계정 & 구글 Colab
  • GCP Storage Bucket

필요한 라이브러리 설치하기

BERT를 학습시키기 위해서는 Tokenized된 데이터를 넣어줘야 한다. 이때 우리는 토크나이저로 sentencepiece 를 주로 사용한다.

1
2
!pip install sentencepiece
!git clone https://github.com/google-research/bert

sentencepiece 대신 konlpy 등을 사용할 수 있습니다.

또한, 구글 리서치에서 공식적으로 제공하는 BERT Repo를 받아서 쓰면 모델과 최적화 등을 곧바로 가져와 사용할 수 있다.

필요한 패키지 가져오기

tensorflow, sentencepiece등을 가져오고 bert 레포에서 모델 등을 가져온다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import os
import sys
import json
import nltk
import random
import logging
import tensorflow as tf
import sentencepiece as spm

from glob import glob
from google.colab import auth, drive
from tensorflow.keras.utils import Progbar

sys.path.append("bert")

from bert import modeling, optimization, tokenization
from bert.run_pretraining import input_fn_builder, model_fn_builder

auth.authenticate_user()

마지막 줄(19번째 줄)에서는 GCP 계정으로 구글에 로그인 한 뒤 나온 토큰 값을 입력하면 구글드라이브에 대한 접근과 GCS 버킷에 대한 접근을 허용해줄 수 있다.

이 부분은 이후 Tensorflow에서 TPU를 접근할 때 GCS에 있는 자원에만 접근이 가능하기 때문에 모델 파일과 데이터셋을 GCS 버킷에 업로드할 때 필요하다.

만약 이부분이 진행되지 않으면 TPU에서는 [local] 파일 시스템에 접근할 수 없다는 NotImplementedError가 발생한다.

한편, PyTorch/XLA에서는 TPU 디바이스를 로컬 GPU처럼 간편하게 연결해서 Tensor 객체를 자유롭게 주고받던데, 어떤 방식으로 구현했는지 의문이다.

TPU 위치 찾기

Colab에서 TPU를 활성화시키면 os.environ['COLAB_TPU_ADDR'] 이라는 시스템 환경변수에 GRPC로 통신가능한 로컬 IP를 얻을 수 있다.

아래 코드를 통해 학습 과정을 로깅하는 것과 함께 TPU에 연결하는 것을 설정해줄 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# configure logging
log = logging.getLogger('tensorflow')
log.setLevel(logging.INFO)

# create formatter and add it to the handlers
formatter = logging.Formatter('%(asctime)s : %(message)s')
sh = logging.StreamHandler()
sh.setLevel(logging.INFO)
sh.setFormatter(formatter)
log.handlers = [sh]

if 'COLAB_TPU_ADDR' in os.environ:
log.info("Using TPU runtime")
USE_TPU = True
TPU_ADDRESS = 'grpc://' + os.environ['COLAB_TPU_ADDR']

with tf.Session(TPU_ADDRESS) as session:
log.info('TPU address is ' + TPU_ADDRESS)
# Upload credentials to TPU.
with open('/content/adc.json', 'r') as f:
auth_info = json.load(f)
tf.contrib.cloud.configure_gcs(session, credentials=auth_info)

else:
log.warning('Not connected to TPU runtime')
USE_TPU = False

위에서 설정한 과정은 /content/adc.json 파일에 저장되고, 이 파일 설정으로 TPU를 사용하게 된다.

데이터 다운받기

학습하는 데이터를 모으는 것은 사실 학습과 별개로 굉장히 중요한 부분이다.

이번에는 간단하게 OpenSubtitles 데이터셋을 이용해 한글 데이터를 다운받아보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
AVAILABLE =  {'af','ar','bg','bn','br','bs','ca','cs',
'da','de','el','en','eo','es','et','eu',
'fa','fi','fr','gl','he','hi','hr','hu',
'hy','id','is','it','ja','ka','kk','ko',
'lt','lv','mk','ml','ms','nl','no','pl',
'pt','pt_br','ro','ru','si','sk','sl','sq',
'sr','sv','ta','te','th','tl','tr','uk',
'ur','vi','ze_en','ze_zh','zh','zh_cn',
'zh_en','zh_tw','zh_zh'}

LANG_CODE = "ko" #@param {type:"string"}

assert LANG_CODE in AVAILABLE, "Invalid language code selected"

!wget http://opus.nlpl.eu/download.php?f=OpenSubtitles/v2016/mono/OpenSubtitles.raw.'$LANG_CODE'.gz -O dataset.txt.gz
!gzip -d dataset.txt.gz
!tail dataset.txt

11번째줄의 “LANG_CODE”를 변경해주면 원하는 언어의 데이터셋을 받을 수 있다.

한글 데이터셋의 경우 압축된 상태 기준으로 약 8MB의 데이터셋이다.

(옵션) 일부만 사용해 학습하기

데이터셋 전체를 사용해 학습하면 학습시간이 굉장히 오래 걸린다.

만약 실제 모델을 얻고싶은 것이 아니라 단순히 학습 가능한지만 알고 싶다면 아래 첫 줄에서 DEMO_MODE=True 로 설정해주면 수량을 100만개 데이터로만 학습한다.

1
2
3
4
5
6
7
8
9
DEMO_MODE = True #@param {type:"boolean"}

if DEMO_MODE:
CORPUS_SIZE = 1000000
else:
CORPUS_SIZE = 100000000 #@param {type: "integer"}

!(head -n $CORPUS_SIZE dataset.txt) > subdataset.txt
!mv subdataset.txt dataset.txt

텍스트 데이터 전처리하기

텍스트 데이터에서 많이 쓰는 문장부호나 기타 이모티콘🤩등을 학습에서 제거할지, 제거하지 않을지는 우리가 학습시키는 모델이 어떤 목적이냐에 따라 달라진다.

위와 같은 이모티콘은 사용 빈도가 낮은 편이기 때문에 위 이모티콘을 임베딩에 포함시킬 경우 Vocab의 용량이 굉장히 커지게 되어 보편적인 Language Model을 만들기 위해서는 보통 특수문자나 이모티콘 등을 제거해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
regex_tokenizer = nltk.RegexpTokenizer("\w+")

def normalize_text(text):
# lowercase text
text = str(text).lower()
# remove non-UTF
text = text.encode("utf-8", "ignore").decode()
# remove punktuation symbols
text = " ".join(regex_tokenizer.tokenize(text))
return text

def count_lines(filename):
count = 0
with open(filename) as fi:
for line in fi:
count += 1
return count
1
2
3
4
5
6
7
8
9
10
11
12
13
14
RAW_DATA_FPATH = "dataset.txt" #@param {type: "string"}
PRC_DATA_FPATH = "proc_dataset.txt" #@param {type: "string"}

# apply normalization to the dataset
# this will take a minute or two

total_lines = count_lines(RAW_DATA_FPATH)
bar = Progbar(total_lines)

with open(RAW_DATA_FPATH,encoding="utf-8") as fi:
with open(PRC_DATA_FPATH, "w",encoding="utf-8") as fo:
for l in fi:
fo.write(normalize_text(l)+"\n")
bar.add(1)

데이터셋 텍스트 토크나이징하기

BERT등 NLP 모델을 학습시킬때는 토크나이징한 Vocab의 크기를 적절히 제한하는 것이 모델의 성능을 높이는데 도움이 된다. (용량도 역시)

큰 모델일수록 Vocab의 크기도 커지지만, 보통의 경우는 3만개 내외의 Vocab을 만드는 것으로 보인다.

1
2
3
4
5
6
7
8
9
10
11
12
13
MODEL_PREFIX = "tokenizer" #@param {type: "string"}
VOC_SIZE = 32000 #@param {type:"integer"}
SUBSAMPLE_SIZE = 12800000 #@param {type:"integer"}
NUM_PLACEHOLDERS = 256 #@param {type:"integer"}

SPM_COMMAND = ('--input={} --model_prefix={} '
'--vocab_size={} --input_sentence_size={} '
'--shuffle_input_sentence=true '
'--bos_id=-1 --eos_id=-1').format(
PRC_DATA_FPATH, MODEL_PREFIX,
VOC_SIZE - NUM_PLACEHOLDERS, SUBSAMPLE_SIZE)

spm.SentencePieceTrainer.Train(SPM_COMMAND)

위 코드를 실행하면 SentencePiece에서 해당 모델을 열심히 잘라가며 Vocab을 생성하고, 이후 텍스트를 자르기 위한 Tokenizer를 학습한다.

학습된 Sentencepiece Vocab을 로딩해주자.

1
2
3
4
5
6
7
8
9
10
11
12
def read_sentencepiece_vocab(filepath):
voc = []
with open(filepath, encoding='utf-8') as fi:
for line in fi:
voc.append(line.split("\t")[0])
# skip the first <unk> token
voc = voc[1:]
return voc

snt_vocab = read_sentencepiece_vocab("{}.vocab".format(MODEL_PREFIX))
print("Learnt vocab size: {}".format(len(snt_vocab)))
print("Sample tokens: {}".format(random.sample(snt_vocab, 10)))

위 SentencePiece를 통해 학습한 Vocab을 BERT가 이해하는 형태로 바꿔주기 위해서는 _로 시작한 토큰들을 ## 으로 시작하도록 바꿔주면 되고, ["[PAD]","[UNK]","[CLS]","[SEP]","[MASK]"]의 경우는 BERT에서 사용하는 특수 토큰이기 때문에 해당 토큰에 대한 정보들을 추가해 최종적인 bert_vocab을 만들어준다.

1
2
3
4
5
6
7
8
9
def parse_sentencepiece_token(token):
if token.startswith("▁"):
return token[1:]
else:
return "##" + token

bert_vocab = list(map(parse_sentencepiece_token, snt_vocab))
ctrl_symbols = ["[PAD]","[UNK]","[CLS]","[SEP]","[MASK]"]
bert_vocab = ctrl_symbols + bert_vocab

마지막으로 앞서서 사용하지 않은 vocab range에 있는 것들을 넣어 bert_vocab의 크기를 앞서 지정한 VOC_SIZE에 맞춰준다.

1
2
bert_vocab += ["[UNUSED_{}]".format(i) for i in range(VOC_SIZE - len(bert_vocab))]
print(len(bert_vocab))

이 과정이 마무리되면 vocab.txt 텍스트 파일에 위 토큰들을 모두 한줄 한줄 저장해주면 이후에 사용할 것들이 끝나게 된다.

1
2
3
4
5
VOC_FNAME = "vocab.txt" #@param {type:"string"}

with open(VOC_FNAME, "w") as fo:
for token in bert_vocab:
fo.write(token+"\n")

학습 데이터 쪼개기

학습 데이터의 크기가 굉장히 클 수 있기 때문에 학습 원천 데이터를 적당한 사이즈로 잘라준다.

1
2
3
!mkdir ./shards
!split -a 4 -l 256000 -d $PRC_DATA_FPATH ./shards/shard_
!ls ./shards/

BERT Pretraining을 위한 데이터 변수 설정하기

BERT를 위한 데이터를 준비하는 과정에 있어서 몇가지 설정해줘야 하는 것들이 있다.

  • MAX_SEQ_LENGTH: BERT의 모델 입력의 최장 토큰 길이
    • 이 이상으로는 BERT모델이 이해하지 못한다.
  • MASKED_LM_PROB: BERT의 학습 중 Masked LM의 비율을 조정한다.
  • MAX_PREDICTIONS: Sequence별 예측할 최대 길이
  • DO_LOWER_CASE: 영문자를 lower(소문자화) 할 지. 한글에는 의미없다.
  • PROCESSES: 전처리할때 CPU 몇개 쓸지
  • PRETRAINING_DIR: 프리트레인 데이터 폴더 이름
1
2
3
4
5
6
MAX_SEQ_LENGTH = 128 #@param {type:"integer"}
MASKED_LM_PROB = 0.15 #@param
MAX_PREDICTIONS = 20 #@param {type:"integer"}
DO_LOWER_CASE = True #@param {type:"boolean"}
PROCESSES = 4 #@param {type:"integer"}
PRETRAINING_DIR = "pretraining_data" #@param {type:"string"}

위ㅏ 같이 설정을 진행한 뒤, 아래 코드를 실행하면 Pretraining Data가 만들어진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
XARGS_CMD = ("ls ./shards/ | "
"xargs -n 1 -P {} -I{} "
"python3 bert/create_pretraining_data.py "
"--input_file=./shards/{} "
"--output_file={}/{}.tfrecord "
"--vocab_file={} "
"--do_lower_case={} "
"--max_predictions_per_seq={} "
"--max_seq_length={} "
"--masked_lm_prob={} "
"--random_seed=34 "
"--dupe_factor=5")

XARGS_CMD = XARGS_CMD.format(PROCESSES, '{}', '{}', PRETRAINING_DIR, '{}',
VOC_FNAME, DO_LOWER_CASE,
MAX_PREDICTIONS, MAX_SEQ_LENGTH, MASKED_LM_PROB)

tf.gfile.MkDir(PRETRAINING_DIR)
!$XARGS_CMD

GCP 버킷에 모델 & 학습 데이터 올리기

Tensorflow를 통해 TPU로 학습을 진행하려면 앞서 언급한 것과 같이 GCS에 데이터와 모델을 업로드해야 한다.

첫째 줄의 BUCKET_NAME을 개개인의 GCS 버킷이름으로 수정하면 된다.

1
2
3
BUCKET_NAME = "이부분을_수정해_주세요" #@param {type:"string"}
MODEL_DIR = "bert_model" #@param {type:"string"}
tf.gfile.MkDir(MODEL_DIR)

BERT Model Hyper Parameters 설정하기

BERT 모델을 어떤 구조의 모델을 사용할지에 대한 Hyper Parameters를 설정해야 한다.

얼마나 Dropout을 해줄지, Bidirectional하게 할지, Activation Function을 뭘로 해줄지, Hidden Size를 얼마나 해줄지, Attention Head를 몇개로 해줄지, 레이어를 몇 층으로 쌓을 지 등등…

아래 설정은 BERT Base 모델의 기본값이다. 아래 값을 수정하면 좀 더 다르게 학습된 BERT 모델을 만들 수 있게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
bert_base_config = {
"attention_probs_dropout_prob": 0.1,
"directionality": "bidi",
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 768,
"initializer_range": 0.02,
"intermediate_size": 3072,
"max_position_embeddings": 512,
"num_attention_heads": 12,
"num_hidden_layers": 12,
"pooler_fc_size": 768,
"pooler_num_attention_heads": 12,
"pooler_num_fc_layers": 3,
"pooler_size_per_head": 128,
"pooler_type": "first_token_transform",
"type_vocab_size": 2,
"vocab_size": VOC_SIZE
}

with open("{}/bert_config.json".format(MODEL_DIR), "w") as fo:
json.dump(bert_base_config, fo, indent=2)

with open("{}/{}".format(MODEL_DIR, VOC_FNAME), "w") as fo:
for token in bert_vocab:
fo.write(token+"\n")

그리고 앞서 만들어준 모델, 프리트레이닝 데이터를 GCS 버킷에 업로드한다.

1
2
if BUCKET_NAME:
!gsutil -m cp -r $MODEL_DIR $PRETRAINING_DIR gs://$BUCKET_NAME

모델 학습 Hyper Parameters 설정하기

GCS 버킷에 데이터와 모델을 모두 업로드해준 뒤, 실제 TPU에서 학습을 진행하도록 명령을 넘겨줘야 한다.

첫번째 줄의 BUCKET_NAME만 위와 동일하게 설정해주면 된다.

중간의 BATCH_SIZE, LEARNING_RATE, TRAIN_STEPS, NUM_TPU_CORES 등의 변수를 조절해 모델의 학습 속도를 결정할 수 있다.

Colab의 TPU는 v3-8이므로 NUM_TPU_CORES는 8Core가 최대다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
BUCKET_NAME = "beomi-blog-sample" #@param {type:"string"}
MODEL_DIR = "bert_model" #@param {type:"string"}
PRETRAINING_DIR = "pretraining_data" #@param {type:"string"}
VOC_FNAME = "vocab.txt" #@param {type:"string"}

# Input data pipeline config
TRAIN_BATCH_SIZE = 128 #@param {type:"integer"}
MAX_PREDICTIONS = 20 #@param {type:"integer"}
MAX_SEQ_LENGTH = 128 #@param {type:"integer"}
MASKED_LM_PROB = 0.15 #@param

# Training procedure config
EVAL_BATCH_SIZE = 64
LEARNING_RATE = 2e-5
TRAIN_STEPS = 1000000 #@param {type:"integer"}
SAVE_CHECKPOINTS_STEPS = 2500 #@param {type:"integer"}
NUM_TPU_CORES = 8

if BUCKET_NAME:
BUCKET_PATH = "gs://{}".format(BUCKET_NAME)
else:
BUCKET_PATH = "."

BERT_GCS_DIR = "{}/{}".format(BUCKET_PATH, MODEL_DIR)
DATA_GCS_DIR = "{}/{}".format(BUCKET_PATH, PRETRAINING_DIR)

VOCAB_FILE = os.path.join(BERT_GCS_DIR, VOC_FNAME)
CONFIG_FILE = os.path.join(BERT_GCS_DIR, "bert_config.json")

INIT_CHECKPOINT = tf.train.latest_checkpoint(BERT_GCS_DIR)

bert_config = modeling.BertConfig.from_json_file(CONFIG_FILE)
input_files = tf.gfile.Glob(os.path.join(DATA_GCS_DIR,'*tfrecord'))

log.info("Using checkpoint: {}".format(INIT_CHECKPOINT))
log.info("Using {} data shards".format(len(input_files)))

모델을 TPU로 올리고 학습하기

model_fn 이라는 이름의 딥러닝 모델 설정 객체를 만들어주고 TPU에 연결해준 뒤, 어떻게 학습을 하고 어디에 CheckPoint를 정리할지 등을 지정해줘야 한다.

이후 TPUEstimator를 통해 모델 객체, 설정 객체를 전달해주고, 해당 estimator를 estimator.train() 하면 TPU 위에서 BERT 모델의 학습이 진행된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
model_fn = model_fn_builder(
bert_config=bert_config,
init_checkpoint=INIT_CHECKPOINT,
learning_rate=LEARNING_RATE,
num_train_steps=TRAIN_STEPS,
num_warmup_steps=10,
use_tpu=USE_TPU,
use_one_hot_embeddings=True)

tpu_cluster_resolver = tf.contrib.cluster_resolver.TPUClusterResolver(TPU_ADDRESS)

run_config = tf.contrib.tpu.RunConfig(
cluster=tpu_cluster_resolver,
model_dir=BERT_GCS_DIR,
save_checkpoints_steps=SAVE_CHECKPOINTS_STEPS,
tpu_config=tf.contrib.tpu.TPUConfig(
iterations_per_loop=SAVE_CHECKPOINTS_STEPS,
num_shards=NUM_TPU_CORES,
per_host_input_for_training=tf.contrib.tpu.InputPipelineConfig.PER_HOST_V2))

estimator = tf.contrib.tpu.TPUEstimator(
use_tpu=USE_TPU,
model_fn=model_fn,
config=run_config,
train_batch_size=TRAIN_BATCH_SIZE,
eval_batch_size=EVAL_BATCH_SIZE)

train_input_fn = input_fn_builder(
input_files=input_files,
max_seq_length=MAX_SEQ_LENGTH,
max_predictions_per_seq=MAX_PREDICTIONS,
is_training=True)

# 학습하자!!
estimator.train(input_fn=train_input_fn, max_steps=TRAIN_STEPS)

어디에 모델이 저장되나?

estimator.train 코드를 실행하면 TPU위에서 학습이 이뤄지고, 동시에 우리가 지정한 체크포인트(SAVE_CHECKPOINTS_STEPS)별로 GCS 버킷의 폴더에 모델의 가중치값이 저장된다.

BERT 모델의 CheckPoint는 GCS에 업로드된다.

Google Colab은 Pro를 쓰더라도 최대 24시간이 한계이고, 거대한 데이터로 학습시킬때는 세션이 종료되기 때문에 체크포인트를 가져와 해당 부분부터 학습을 재게하는 것이 필요하다.

이와 같이 GCS 버킷에 저장을 하는 것을 통해서 BERT 모델을 Colab TPU로 처음부터 끝까지 우리 데이터를 통해 학습시킬수 있다.

References

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×