본문 바로가기

문돌이 존버/데이터 분석

버트(BERT) 개념 간단히 이해하기

반응형
본 글은 "딥 러닝을 이용한 자연어 처리 입문"을 학습하며 작성한 것입니다. 중간중간 제가 이해한 내용을 좀 더 풀어서 썼습니다. 문제가 된다면 비공개 처리하겠습니다.

버트(BERT) 개념

BERT(Bidirectional Encoder Representations from Transformers)는 2018년 구글이 공개한 사전 훈련된(pre-trained) 모델입니다. 트랜스포머를 이용해 구현되었으며 위키피디아(25억 단어)와 BooksCorpus(8억 단어)와 같은 레이블(label)이 없는 텍스트 데이터로 훈련되었습니다.

일반적으로 BERT를 통해 레이블이 있는 다른 작업(task)에서 추가 훈련과 함께 하이퍼파라미터를 재조정하면 성능이 높게 나옵니다. 다른 작업에 대해 파라미터 재조정을 위한 추가 훈련 과정을 파인튜닝(fine-tuning)이라고 합니다.

BERT의 크기

출처: 딥 러닝을 이용한 자연어 처리 입문

BERT의 기본 구조는 트랜스포머의 인코더를 쌓아올린 구조입니다. Base 버전에선 총 12개를 쌓았으며, Large 버전에선 총 24개를 쌓았습니다.

L = 인코더 층 개수
D = $d_{model}$ 크기(인코더와 디코더 입력/출력의 크기 = 임베딩 벡터 차원)
A = 셀프 어텐션 헤드 개수
BERT-Base: L=12, D=768, A=12 -> 110M개의 파라미터
BASE-Large: L=24, D=1024, A=16 -> 340M개의 파라미터
복습!
트랜스포머는 기존의 seq2seq처럼 인코더-디코더 구조를 유지하지만 그 단위가 N개 존재한다는 것이 특징이었습니다.

참고로 BERT-Base는 BERT보다 앞서 등장한 Open AI GPT-1과 하이퍼파라미터가 동일한데, 이는 BERT 연구진이 직접적으로 GPT-1과 성능 비교를 위해 GPT-1과 동등한 크기로 설계했기 때문이라고 합니다. 밑에서 설명하는 내용은 BERT-Base를 기준으로 합니다.

BERT의 문맥을 반영한 임베딩(Contextual Embedding)

BERT의 입력은 임베딩 층(embedding layer)을 지난 임베딩 벡터들입니다. $d_{model}$을 768로 정의하여 모든 단어들은 768차원의 임베딩 벡터가 되어 입력으로 사용됩니다. 출력 벡터 역시 모두 768차원입니다.

출처: 딥 러닝을 이용한 자연어 처리 입문

BERT 연산을 거친 후의 출력 임베딩은 문장의 문맥을 모두 참고한 임베딩이 됩니다. 아래 [CLS]란 벡터는 초기 입력일 땐 단순히 임베딩 층을 지난 임베딩 벡터였지만, BERT를 지나면 [CLS], I, love, you 라는 모든 단어 벡터들을 참고한 문맥 정보를 가진 벡터가 되는 것이죠.

출처: 딥 러닝을 이용한 자연어 처리 입문

하나의 단어가 모든 단어를 참고하는 연산은 BERT의 12개 층에서 전부 이루어지는 연산입니다. 따라서 BERT의 첫 번째 층의 출력 임베딩은 두 번째 층의 입력 임베딩이 됩니다.

출처: 딥 러닝을 이용한 자연어 처리 입문

BERT가 문맥을 반영한 출력 임베딩을 얻는 방식은 바로 셀프 어텐션을 이용하는 것입니다. 내부적으로 12개 각 층마다 멀티 헤드 셀프 어텐션과 포지션 와이즈 피드 포워드 신경망을 수행하는 것이죠.

BERT의 서브워드 토크나이저: WordPiece

BERT는 단어보다 더 작은 단위로 쪼개는 서브워드 토크나이저(subword tokenizer)를 사용합니다. 서브워드 토크나이저는 기본적으로 자주 등장하는 단어는 그대로 단어 집합에 추가하지만, 자주 등장하지 않는 단어는 더 작은 단위인 서브워드로 분리되어 서브워드들이 단어 집합에 추가된다는 아이디어를 갖고 있습니다.

단어 집합이 갖추어지면 토큰화를 수행하며, 수행 방식은 아래와 같습니다.

준비물: 이미 훈련 데이터로부터 만들어진 단어 집합

1. 토큰이 단어 집합에 존재 => 해당 토큰을 분리하지 않음
2. 토큰이 단어 집합에 존재하지 않음 => 해당 토큰을 서브워드로 분리
                                                     해당 토큰의 첫 번째 서브워드를 제외한 나머지 서브워드들은 앞에                                                           '##'를 붙인 것을 토큰으로 함

예를 들어, embeddings라는 단어가 입력으로 들어왔을 때 BERT는 단어 집합에 해당 단어가 존재하지 않았다고 해봅시다. 서브워드 토크나이저가 아닌 토크나이저라면 여기서 OOV(out of vocabulary) 문제가 발생하지만, 서브워드 토크나이저의 경우 해당 단어를 더 쪼개려고 시도합니다. 만약 BERT의 단어 집합에 em, ##bed, ##ding, #s라는 서브워드들이 존재한다면 embeddings는 em, ##bed, ##ding, #s로 분리됩니다. 여기서 ##은 서브워드들이 단어 중간부터 등장하는 것임을 알려주기 위한 기호입니다.

아래 코드 샘플을 살펴봅시다.

import pandas as pd
from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
result = tokenizer.tokenize('Here is the sentence I want embeddings for.')
print(result)

embeddings는 단어 집합에 존재하지 않으므로 서브단위로 분리되었습니다. 실제로 BERT의 단어 집합에 특정 단어가 있는지 조회하려면 .vocab[] 을 사용하면 됩니다.

print(tokenizer.vocab['here'])

단어 here이 정수 인코딩을 위해 단어 집합 내부적으로 2182라는 정수로 매핑되어졌다는 의미입니다. 존재하지 않는 단어 embeddings를 조회하면 KeyError가 발생합니다.

print(tokenizer.vocab['embeddings'])

대신 em, ##bed, ##ding, ##s는 모두 단어 집합에 존재한다고 나옵니다. 그렇다면 BERT 단어 집합 전체는 총 몇 개일까요?

with open('vocab.txt', 'w') as f:
    for token in tokenizer.vocab.keys():
        f.write(token + '\n')
df = pd.read_fwf('vocab.txt', header=None) # read_fwf: 고정 너비 형식의 table을 DataFrame으로 읽어들이는 함수
df

print('단어 집합의 크기:', len(df))

BERT에서 사용되는 특별 토큰들과 그와 매핑되는 정수
[PAD] - 0
[UNK] - 100
[CLS] - 101
[SEP] - 102
[MASK] - 103

포지션 임베딩(Position Embedding)

트랜스포머에선 포지셔널 인코딩이란 방법을 통해 단어의 위치 정보를 표현했습니다. 사인 함수와 코사인 함수를 사용하여 위치에 따라 다른 값을 가지는 행렬을 만들어 이를 단어 벡터들과 더하는 방식이었죠. BERT는 이와 유사하지만 학습을 통해 위치 정보를 얻는 포지션 임베딩을 사용합니다.

출처: 딥 러닝을 이용한 자연어 처리 입문

위 그림에서 보이듯 위치 정보를 위한 임베딩 층을 하나 더 사용합니다. 문장의 길이가 4라면 4개의 포지션 임베딩 벡터를 학습시키고, BERT의 입력마다 더해주면 됩니다.

- 첫 번째 단어 임베딩 벡터 + 0번 포지션 임베딩 벡터
- 두 번째 단어 임베딩 벡터 + 1번 포지션 임베딩 벡터
- 세 번째 단어 임베딩 벡터 + 2번 포지션 임베딩 벡터
- 네 번째 단어 임베딩 벡터 + 3번 포지션 임베딩 벡터

BERT에선 문장의 최대 길이를 512로 하므로 총 512개의 포지션 임베딩 벡터가 학습됩니다. 지금까지 나온 임베딩 층을 정리하면 1) 단어 집합의 크기가 30,522개인 단어 벡터를 위한 임베딩 층2) 512개의 포지션 벡터를 위한 임베딩 층이 있습니다.

BERT의 사전 훈련

출처: 딥 러닝을 이용한 자연어 처리 입문

BERT 논문에 첨부된 그림은 BERT, GPT-1, ELMo의 구조적인 차이를 보여줍니다. ELMo의 경우 정방향 LSTM과 역방향 LSTM을 각각 훈련시키는 방식으로 양방향 언어 모델을 만들었습니다. GPT-1은 트랜스포머(Trm)의 디코더를 이전 단어들로부터 다음 단어를 예측하는 방식의 단방향 언어 모델입니다. 반면, BERT는 화살표가 양방향으로 뻗어나가는 모습으로 이는 마스크드 언어 모델(Masked Language Model, MLM)을 통한 것입니다.

BERT의 사전 훈련 방법은 크게 2가지입니다. 첫 번째로 마스크드 언어 모델, 두 번째로 다음 문장 예측(Next sentence prediction, NSP)입니다.

마스크드 언어 모델

BERT는 사전 훈련을 위해 입력 텍스트의 15% 단어를 랜덤으로 마스킹합니다. 이후 신경망이 가려진 단어들을 예측하도록 합니다. 쉽게 말해 빈칸 뚫고 맞추기 학습입니다.

더 정확히는 전부 [MASK]로 변경하지 않고 랜덤으로 선택된 15% 단어들은 다시 다음과 같은 비율로 규칙이 적용됩니다.

1. 80% 단어들은 [MASK]로 변경
예) The man went to the store -> The man went to the [MASK]

2. 10% 단어들은 램덤으로 단어 변경
예) The man went to the store -> The man went to the dog

3. 10% 단어들은 동일하게 유지
예) The man went to the store -> The man went to the store

이는 [MASK]만 사용할 경우 [MASK] 토큰이 파인튜닝 단계에선 나타나지 않기 때문에 사전 학습 단계와 파인튜닝 단계에서의 불일치 문제를 피하기 위함입니다. 전체 단어 관점에서 비율을 살펴보면 아래와 같습니다.

출처: 딥 러닝을 이용한 자연어 처리 입문

전체 단어의 12%는 [MASK]로 변경되고, 1.5%는 랜덤으로 또 다른 단어로 변경되고, 1.5%는 단어가 변경되지 않지만 BERT는 해당 단어가 원래 단어인지, 변경된 단어인지 알 수 없습니다.

예를 들어, 'My dog is cute. he likes playing'이란 문장이 있고, 서브워드 토크나이저에 의해 ['my', 'dog', 'is' 'cute', 'he', 'likes', 'play', '##ing']로 토큰화되었다고 합시다. BERT의 입력으로 사용되면서 아래처럼 변경될 수 있습니다.

출처: 딥 러닝을 이용한 자연어 처리 입문

'dog' 토큰이 [MASK]로 변경되었고 BERT는 원래 단어를 맞추려고 합니다. 여기서 출력층에 있는 다른 위치의 벡터들은 예측과 학습에 사용되지 않고, 오직 'dog' 위치의 출력층 벡터만이 사용됩니다. 구체적으로는 BERT의 손실 함수에서 다른 위치에서의 예측은 무시합니다. 출력층에서는 예측을 위해 단어 집합의 크기만큼의 밀집층(Dense layer)에 소프트맥스 함수가 사용된 1개의 층을 사용하여 원래 단어가 무엇인지를 맞추게 됩니다.

출처: 딥 러닝을 이용한 자연어 처리 입문

위의 경우에는 BERT가 랜덤 단어 'king'으로 변경된 토큰에 대해서도, 변경되지 않은 단어 'play'에 대해서도 원래 단어가 무엇인지 추가로 예측해야 합니다.

다음 문장 예측

BERT는 두 개의 문장을 준 후에 이 문장이 이어지는 문장인지 아닌지를 맞추는 방식으로 훈련합니다. 이를 위해 50:50 비율로 실제 이어지는 두 개의 문장과 랜덤으로 이어붙인 두 개의 문장이 주어집니다. 이를 각각 Sentence A, Sentence B라고 하였을 때, 아래는 문장의 연속성을 확인한 경우와 그렇지 않은 경우를 보여줍니다.

이어지는 문장인 경우
Sentence A : The man went to the store.
Sentence B : He bought a gallon of milk.
Label = IsNextSentence

이어지는 문장이 아닌 경우
Sentence A : The man went to the store.
Sentence B : dogs are so cute.
Label = NotNextSentence

출처: 딥 러닝을 이용한 자연어 처리 입문

BERT의 입력에선 [SEP]라는 특별 토큰을 사용해서 문장을 구분합니다. 첫 번째 문장의 끝에 [SEP] 토큰을 넣고, 두 번째 문장이 끝나면 역시 [SEP] 토큰을 붙여줍니다. 그리고 이 두 문장이 실제 이어지는 문장인지 아닌지를 [CLS] 토큰 위치의 출력층에서 이진 분류 문제를 풀도록 합니다. [CLS] 토큰은 BERT가 분류 문제를 풀기 위해 추가된 특별 토큰입니다. 또한 위의 그림처럼 마스크드 언어 모델과 다음 문장 예측은 따로 학습하는 것이 아니라 loss를 합하여 학습이 동시에 이루어집니다.

BERT가 언어 모델 외에도 다음 문장 예측이라는 태스크를 학습하는 이유는 QA(Question Answering)NLI(Natural Language Inference)와 같이 두 문장의 관계를 이해하는 것이 중요한 태스크들도 있기 때문입니다.

세그먼트 임베딩(Segment Embedding)

문장 구분을 위해 BERT는 세그먼트 임베딩이란 또 다른 임베딩 층을 사용합니다. 첫 번째 문장에는 Sentence 0 임베딩, 두 번째 문장에는 Sentence 1 임베딩을 더해주는 방식으로 임베딩 벡터는 2개만 사용됩니다.

출처: 딥 러닝을 이용한 자연어 처리 입문

결론적으로 BERT는 총 3개의 임베딩 층이 사용됩니다.

WordPiece Embedding
- 실질적인 입력이 되는 워드 임베딩
- 임베딩 벡터의 종류는 단어 집합의 크기로 30,522개

Position Embedding
- 위치 정보를 학습하기 위한 임베딩
- 임베딩 벡터의 종류는 문장의 최대 길이인 512개

Segment Embedding
- 두 개의 문장을 구분하기 위한 임베딩
- 임베딩 벡터의 종류는 문장의 최대 개수인 2개

많은 문헌에서 BERT가 문장 중간의 [SEP] 토큰과 두 종류의 세그먼트 임베딩을 통해서 두 개의 문장을 구분하여 입력받을 수 있다고 설명하고 있지만, 사실 BERT에 두 개의 문장이 들어간다는 표현에서의 문장이라는 것은 실제 우리가 알고 있는 문장의 단위는 아닙니다. 예를 들어 QA 문제를 푸는 경우에는 [SEP]와 세그먼트 임베딩을 기준으로 구분되는 [질문(Question), 본문(Paragraph)] 두 종류의 텍스트를 입력받지만, Paragraph 1개는 실제로는 다수의 문장으로 구성될수 있습니다. 다시 말해 [SEP]와 세그먼트 임베딩으로 구분되는 BERT의 입력에서의 두 개의 문장은 실제로는 두 종류의 텍스트, 두 개의 문서일 수 있습니다.

BERT가 두 개의 문장을 입력받을 필요가 없는 경우도 있습니다. 예를 들어 네이버 영화 리뷰 분류나 IMDB 리뷰 분류와 같은 감성 분류 태스크에서는 한 개의 문서에 대해서만 분류를 하는 것이므로, 이 경우에는 BERT의 전체 입력에 Sentence 0 임베딩만을 더해줍니다. 아래에서 설명할 파인 튜닝 유형(1번, 2번)이 대표적 예시입니다.

BERT 파인튜닝 하기

하나의 텍스트에 대한 텍스트 분류 유형(Single Text Classification)

출처: 딥 러닝을 이용한 자연어 처리 입문

이 유형은 영화 리뷰 감성 분류, 로이터 뉴스 분류 등과 같이 입력된 문서에 대해서 분류를 하는 유형으로 문서의 시작에 [CLS] 라는 토큰을 입력합니다(사전 훈련 단계에서도 사용한다고 설명). BERT를 실질적으로 사용하는 단계인 파인튜닝 단계에서도 마찬가지입니다. 텍스트 분류 문제를 풀기 위해서 [CLS] 토큰의 위치의 출력층에서 밀집층(Dense layer) 또는 같은 이름으로는 완전 연결층(fully-connected layer)이라고 불리는 층들을 추가하여 분류에 대한 예측을 하게됩니다.

하나의 텍스트에 대한 태깅 작업(Tagging)

출처: 딥 러닝을 이용한 자연어 처리 입문

대표적으로 문장의 각 단어에 품사를 태깅하는 품사(POS) 태깅 작업과 개체를 태깅하는 개체명 인식 작업(NER)이 있습니다. 출력층에서는 입력 텍스트의 각 토큰의 위치에 밀집층을 사용하여 분류에 대한 예측을 하게 됩니다.

텍스트의 쌍에 대한 분류 또는 회귀 문제(Text Pair Classification or Regression)

출처: 딥 러닝을 이용한 자연어 처리 입문

텍스트의 쌍을 입력으로 받는 대표적인 태스크로 자연어 추론(Natural language inference)이 있습니다. 자연어 추론 문제란, 두 문장이 주어졌을 때, 하나의 문장이 다른 문장과 논리적으로 어떤 관계에 있는지를 분류하는 것입니다. 유형으로는 모순 관계(contradiction), 함의 관계(entailment), 중립 관계(neutral)가 있습니다.

이러한 태스크의 경우에는 입력 텍스트가 1개가 아니므로, 텍스트 사이에 [SEP] 토큰을 집어넣고, Sentence 0 임베딩과 Sentence 1 임베딩이라는 두 종류의 세그먼트 임베딩을 모두 사용하여 문서를 구분합니다.

질의 응답(Question Answering)

출처: 딥 러닝을 이용한 자연어 처리 입문

BERT로 QA를 풀기 위해서 질문과 본문이라는 두 개의 텍스트의 쌍을 입력합니다. 대표적인 데이터셋으로 SQuAD(Stanford Question Answering Dataset) v1.1이 있습니다. 이 데이터셋을 푸는 방법은 질문과 본문을 입력받으면, 본문의 일부분을 추출해서 질문에 답변하는 것입니다. 실제로 이 데이터셋은 영어로 되어있지만 한국어로 예시를 들어보겠습니다.

질문: 강우가 떨어지도록 영향을 주는 것은?
본문: 기상학에서 강우는 대기 수증기가 응결되어 중력의 영향을 받고 떨어지는 것을 의미합니다. 강우의 주요 형태는 이슬비, 비, 진눈깨비, 눈, 싸락눈 및 우박이 있습니다.

정답: 중력

어텐션 마스크(Attention Mask)

출처: 딥 러닝을 이용한 자연어 처리 입문

BERT를 실제로 실습할 때 어텐션 마스크라는 시퀀스 입력이 추가로 필요합니다. 어텐션 마스크는 BERT가 어텐션 연산을 할 때, 불필요하게 패딩 토큰에 대해서 어텐션을 하지 않도록 실제 단어와 패딩 토큰을 구분할 수 있도록 알려주는 입력입니다. 0과 1 두 가지 값을 가지는데, 숫자 1은 해당 토큰은 실제 단어이므로 마스킹을 하지 않는다라는 의미이고, 숫자 0은 해당 토큰은 패딩 토큰이므로 마스킹을 한다는 의미입니다. 위의 그림과 같이 실제 단어의 위치에는 1, 패딩 토큰의 위치에는 0의 값을 가지는 시퀀스를 만들어 BERT의 또 다른 입력으로 사용하면 됩니다.

기타

훈련 데이터는 위키피디아(25억 단어)와 BooksCorpus(8억 단어) ≈ 33억 단어
WordPiece 토크나이저로 토큰화를 수행 후 15% 비율에 대해서 마스크드 언어 모델 학습
두 문장 Sentence A와 B의 합한 길이, 즉 최대 입력의 길이는 512로 제한
100만 step 훈련 ≈ (총 합 33억 단어 코퍼스에 대해 40 에포크 학습)
옵티마이저: 아담(Adam)
학습률(learning rate): $10^-4$
가중치 감소(Weight Decay): L2 정규화로 0.01 적용
드롭 아웃: 모든 레이어에 대해서 0.1 적용
활성화 함수: relu 함수가 아닌 gelu 함수
배치 크기(Batch size): 256

실습

!pip install --upgrade tensorflow # tensorflow 2.3 버전 이상 필요

import tensorflow as tf
print(tf.__version__) # 2.6.2
from transformers import TFBertForMaskedLM
from transformers import AutoTokenizer

# 한국어 BERT 모델: klue/bert-base
model = TFBertForMaskedLM.from_pretrained('klue/bert-base', from_pt=True) # 마스크드 언어 모델 형태로 로드 / from_pt=True: 해당 모델이 기존에는 pytorch로 학습된 모델이지만 tensorflow에서 사용한다는 의미
tokenizer = AutoTokenizer.from_pretrained('klue/bert-base') # 모델 학습시 사용된 토크나이저 로드
inputs = tokenizer('치킨은 정말 맛있는 [MASK]다.', return_tensors='tf')
print(inputs['input_ids']) # 정수 인코딩 결과 확인

# 세그먼트 인코딩 결과 확인
print(inputs['token_type_ids']) # 현재 입력은 문장이 1개이므로 모두 0 시퀀스 획득

# 실제 단어와 패딩 토큰을 구분하는 용도인 어텐션 마스크 확인
print(inputs['attention_mask']) # 현재 입력에는 패딩이 없으므로 모두 1 시퀀스 획득

from transformers import FillMaskPipeline
pipe = FillMaskPipeline(model=model, tokenizer=tokenizer) # 모델과 토크나이저 지정
pipe('치킨은 정말 맛있는 [MASK]다.') # 마스크드 언어 모델의 예측 결과를 정리해서 표시

pipe('어제 회식을 해서 속이 [MASK]다.')

참조
https://wikidocs.net/115055
https://wikidocs.net/152922
728x90
반응형