2주 간의 KoELECTRA 개발기 - 1부

2주 간의 KoELECTRA 개발기 - 1부

2주 간의 KoELECTRA 개발을 마치고, 그 과정을 글로 남기려고 한다.

이 글을 읽으신 분들은 내가 했던 삽질(?)을 최대한 덜 하길 바라는 마음이다:)

1부에는 실제 학습을 돌리기 전까지의 과정을 다룰 예정이다.

Github Repo: https://github.com/monologg/KoELECTRA

문제 의식

한국에 Public하게 공개되어 있는 한국어 PLM(Pretrained Language Model)에는 크게 3가지가 있다.

  1. SKT의 KoBERT
  2. TwoBlock AI의 HanBERT
  3. ETRI의 KorBERT

3가지 모두 좋은 성능을 보이지만, 3가지 모두 단점이 존재한다. 각각의 단점을 정리하면 아래와 같다.

단점
KoBERT Vocab size (8002개)가 상대적으로 작음
HanBERT Tokenizer로 인해 Ubuntu 환경에서만 사용 가능
KorBERT API 신청 등의 과정 필요

특히 3가지 모두 공통적으로 Huggingface Transformers 라이브러리에서 사용하려면 tokenization 파일을 따로 만들어야 하는 단점이 있다.

KoBERTHanBERT의 경우 내가 직접 tokenization 파일을 만들어서 github에 배포했지만, 일부 사용자들이 불편함을 토로하기도 했다.

그래서 이번 기회에 위의 단점들을 모두 해결한 한국어 PLM을 개발하고 싶었다.

  1. Vocab size가 어느 정도 커야 함 (30000개 정도)
  2. 모든 OS에서 사용 가능
  3. tokenization 파일을 만들 필요 없이 곧바로 사용 가능
  4. 어느 정도 성능까지 보장되어야 함

위의 4가지를 모두 만족시키기 위하여 시작한 프로젝트가 바로 KoELECTRA 이다.

Tokenizer

실제 현업에서도 좋은 성능을 위해 Mecab + Sentencepiece를 많이 사용하는 것으로 알고 있다. 그러나 공식 BERT, ELECTRA 등은 Wordpiece를 사용하고 있으며, transformers에서도 공식적으로 Wordpiece만 지원하고 있다.

즉, ELECTRA에서 Mecab이나 Sentencepiece를 사용하려면 추가적으로 tokenization 파일을 만들어야 하며, transformers 라이브러리의 api가 바뀌면 내가 직접 그에 맞게 tokenization 파일을 바꿔줘야 한다는 것이다. (KoBERT의 경우 실제로도 그렇게 하고 있다ㅠ)

이러한 문제들 때문에 이번 프로젝트에서는 무조건으로 Wordpiece를 사용하는 방안으로 진행하였다.

Wordpiece

“Wordpiece vocab을 만들 때 Huggingface의 Tokenizers 라이브러리를 쓰는 것이 가장 좋다.”

import argparse
from tokenizers import BertWordPieceTokenizer

parser = argparse.ArgumentParser()

parser.add_argument("--corpus_file", type=str)
parser.add_argument("--vocab_size", type=int, default=32000)
parser.add_argument("--limit_alphabet", type=int, default=6000)

args = parser.parse_args()

tokenizer = BertWordPieceTokenizer(
vocab_file=None,
clean_text=True,
handle_chinese_chars=True,
strip_accents=False, # Must be False if cased model
lowercase=False,
wordpieces_prefix="##"
)

tokenizer.train(
files=[args.corpus_file],
limit_alphabet=args.limit_alphabet,
vocab_size=args.vocab_size
)

tokenizer.save("./", "bert-wordpiece")
  • Vocab size의 경우 관례적으로 많이 쓰이는 약 3만개로 세팅하였다.
  • 전처리 없이 원본 Corpus만 가지고 vocab 만들면 성능이 굉장히 안 좋음
  • character coverage를 최대한 높게 잡는 것이 좋다고 판단 (즉, corpus에서 등장했던 모든 character를 vocab에 포함시킴)
    • 예를 들어 퀭메일이란 단어가 있다고 가정하자.
    • 만일 이 vocab에 없다면 퀭메일 전체를 [UNK]로 처리하게 된다.
    • 만일 이 vocab 안에 있으면 퀭 + ##메일로 tokenize가 될 수 있어 ##메일이란 단어의 의미를 가져갈 수 있다.

(자세한 내용은 이전에 포스팅한 [나만의 BERT Wordpiece Vocab 만들기]을 참고)

전처리 (Preprocessing)

“첫째도 전처리! 둘째도 전처리! 셋째도 전처리!”
“PLM의 성능에 가장 큰 영향을 주는 것은 corpus quality이다!”

크롤링한 뉴스의 문장 하나를 살펴보자

[주요기사] ☞ [포토 스토리] 무허가 도시광산 을 아시나요? ☞ [따뜻한 사진 한 장] 사랑, 하나가 되어 가는 길 <찰나의 기록, 순간의 진실 / KPPA 바로가기> Copyrightsⓒ 한국사진기자협회(www.kppa.or.kr), powered by castnet. 무단 전재 및 재배포 금지 보내기

문제는 이런 문장이 매우 많은데다가, 이걸 Pretrain에 넣을 시 성능이 나빠질 게 뻔하다….

이렇게 noise가 많은 Corpus로 vocab을 만들고 pretrain까지 하면 성능이 좋을 리가 없다.

아래는 내가 적용한 대표적인 전처리 기준이다.

  • 한자, 일부 특수문자 제거
  • 한국어 문장 분리기 (kss) 사용
  • 뉴스 관련 문장은 제거 (무단전재, (서울=뉴스1) 등 포함되면 무조건 제외)

사실 전처리의 기준에 정답은 없다. 가장 중요한 것은 자신의 Task에 맞게 전처리 기준을 세우는 것이다. 내가 생각했던 Task 들에는 한자는 중요하지 않다고 판단해서 지운 것이지, 한자가 꼭 필요한 Task의 경우에는 지우면 안 될 것이다.

TPU 사용 관련 Tip

(자세한 내용은 이전에 포스팅한 [TPU를 이용하여 Electra Pretraining하기]을 참고)

1. Tensorflow Research Cloud(TFRC)를 쓰면 TPU가 무료

→ 이미 공식 코드가 TPU를 완벽히 지원하기에, 직접 ELECTRA를 만들고 싶다면 GPU보다는 TPU를 쓰는 것을 강력히 권장한다.

2. VM Instance는 작은 것(n1-standard-1)을 써도 상관 없다

→ ELECTRA를 GCP에서 학습하려면 TPU, Bucket, VM Instance 이렇게 3개가 필요하다. 그런데 저장소는 Bucket이, 연산은 TPU가 처리하기 때문에 VM Instance는 가벼운 것을 써도 된다.
n1-standard-1는 시간당 약 $0.037, n1-standard-4는 시간당 약 $0.14이다. (비용이 무려 2배 차이!!)

3. TPU를 쓰는 경우 모든 input file은 Cloud storage bucket을 통해야만 한다

→ 이것도 처음에 몰랐다가 고생했던 삽질 중 하나다. tf.estimator.tpu 쪽 코드를 쓰는 경우 로컬 데이터가 아닌 Bucket을 통해야만 한다. (관련 FAQ)
→ 다행히도 Bucket의 비용이 비싸지가 않다 (특정 리전에 만들어 놓으면 1GB당 월 약 $0.03)

4. VM Instance와 TPU를 만들 때 ctpu up 명령어를 사용해라

→ VM Instance와 TPU를 따로 따로 생성하면 처음에 정상적으로 작동하지 않는 경우가 있었다. 아래와 같이 cloud shell로 명령어를 한 번만 치면 VM과 TPU가 동시에 생성된다😃

$ ctpu up --zone=europe-west4-a --tf-version=1.15 \
--tpu-size=v3-8 --machine-type=n1-standard-2 \
--disk-size-gb=20 --name={$VM_NAME}

Configuration의 함정

앞에서도 언급했듯이 ELECTRA Pretraining은 공식 코드를 그대로 가져다 쓰는 것이 좋다. 공식 코드를 가져다쓰기 전에 논문코드 분석을 어느 정도 하고 진행하는 것을 권장한다.

그럼에도 좀 헷갈렸던 부분이 있어 여기서 언급하려 한다.

1. 공식 레포에서 제공하는 small 모델은 정확히는 small++ 모델이다


공식 코드에서도 알 수 있듯이 small로 공개된 모델은 정확히 말하면 small++ 모델이다. 둘의 차이점은 아래와 같다.

max_seq_len generator_hidden_size
small 128 0.25
small++ 512 1.0

(generator_hidden_size=1.0이란 것은 discriminatorgenerator의 hidden_size가 같다는 것이다)

이러한 부분이 논문에 자세히 나와 있지 않아 처음에 small 모델을 만들 때 진짜 small로 만들었다가 다시 small++로 만드는 수고를 거쳤다…. (이 글을 읽은 분들은 이 삽질을 안 하길 빈다.)

hparams.json
{
"tpu_name": "electra-small",
"tpu_zone": "europe-west4-a",
"num_train_steps": 1000000,
"save_checkpoints_steps": 50000,
"train_batch_size": 128,
"learning_rate": 5e-4,
"vocab_size": 32200,
"max_seq_length": 512,
"generator_hidden_size": 1.0
}

2. max_seq_length를 128로 줄인다면 max_position_embeddings도 128로 줄여야 한다

간혹 커스터마이즈된 모델을 만들 때 max_seq_length를 줄이고자 하는 경우가 있다.

그럴 시 max_seq_length만 줄이면 해결된다고 오해할 수 있는데, max_position_embeddings도 줄여줘야 transformers 라이브러리에 맞게 변환할 때 문제가 생기지 않는다. (transformers가 max_position_embeddings으로 최대 길이를 알아내기 때문!)

더 큰 함정은 아래와 같이 model_hparam_overrides라는 attribute 안에 max_position_embeddings를 넣어줘야 하는 것이다!

hparams.json
{
"max_seq_length": 128,
"model_hparam_overrides": {
"max_position_embeddings": 128
}
}

마치며

1부에서는 실제 Pretraining을 시작하기 전의 준비 과정을 다뤘다.

2주 간의 KoELECTRA 개발기 - 2부 에서는 실제 Pretraining, Transformers 포팅, Finetuning 등을 다룰 예정이다:)

Reference