NAVER CLOVA 이기창님의 『BERT와 GPT로 배우는 자연어 처리』 책 내용 요약과, 알고 있는 내용을 기반으로 각색하여 작성되었습니다. 이 책은 전반적으로 누구나 이해하기 쉬운 비유와 그림을 사용하고 핵심만 이야기 해주기 때문에 정보의 홍수로부터 자유로울 수 있다는 것이 장점인 것 같습니다. 쉬운 실습도 함께 포함되어 있어 자연어처리에 입문하는 사람이 보면 큰 맥락을 파악하기에 좋은 책이 되는 것 같습니다.

 

1. Transformer: 최근 자연어처리의 핵심 모델

최근 자연어처리를 이해하는 데 있어서 알아야 할 핵심 한 가지가 있다면 Transformer이다. 이 책에서 설명하는 BERT 모델과 GPT 모델은 모두 Transformer를 기반으로 하기 때문이다. BERT와 GPT 모델이 자연어처리 트렌드에 있어 의의가 있는 것은 크게 3가지 이유가 있다. 첫 번째는 Transformer를 통해 대규모 언어 모델 학습을 할 수 있게 되었고, 두 번째는 대규모 언어 모델 학습을 통해 전이 학습(Transfer Learning)이 가능해졌다는 것이며, 세 번째는 대규모 언어 모델 학습과 전이 학습을 통해 기존 존재하던 모델들의 성능을 압도했기 때문이다. 다른 말로 표현하면 자연어처리 패러다임의 변화(paradigm shift)를 가져온 것이다. Transformer에 대해 자세히 알기 전에 먼저 전이 학습이란 무엇인가를 살펴보자.

 

2. 전이 학습 (Transfer Learning)

전이 학습이란 쉽게 말해 축구, 배구, 농구, 탁구 등의 여러 운동을 넓게 미리 가르쳐 일종의 운동신경을 만들어두면 배우지 않았던 특정 종목인 피구, 야구도 쉽게 배울 수 있는 것을 의미한다. 물리학, 화학, 지구과학을 미리 배워두면 배웠던 지식들로 하여금 생명과학이라는 분야에도 "전이"를 통해 쉽게 배울 수 있는 것과 같다. 정리하자면 특정 태스크를 학습한 모델을 다른 태스크 수행에 재사용하는 기법이라 할 수 있다. 이 전이 학습의 장점으로는 모델학습 속도 향상이 가능하고, 새로운 태스크를 더 잘 수행할 수 있다는 점이다. 이러한 전이 학습을 활용하는 대표적인 모델의 예시가 BERT와 GPT인 것이다. 전이 학습 방법은 크게 두 가지로 나뉜다. 업스트림 태스크(upstream task)와 다운스트림 태스크(downstream task)다.

 

2.1 업스트림 태스크 (upstream task)

업스트림 태스크는 쉽게 말해 일단 넓게 배우는 것이다. 넓게 배우기 위해 대규모 말뭉치(corpus)를 학습한다. 학습하기 위한 대표적인 방법으로 크게 2가지로 다음 단어(문장) 맞히기 (Next Sentence Prediction, NSP)와 빈칸 맞히기 (Masked Language Model, MLM)가 있다. NSP는 일종의 문장간의 연관성을 학습하여 어떤 단어 뒤에 나올 다음 단어는 무엇인가 맞히는 것이며, 빈칸 맞히기는 단어에 빈칸을 둔 뒤 이를 맞히는 것이다. 이러한 방법을 통해 업스트림 태스크를 수행하는 것을 pre-training이라고 한다. 대표적으로 BERT 모델은 위 두 가지 방식 모두를 사용하여 업스트림 태스크(=pre-training)를 수행하고, GPT는 NSP 방법만을 통해 업스트림 태스크를 수행한다. 

 

NSP에 대해 조금더 구체적으로 이야기하면, "티끌 모아 __"일 때, "티끌 모아"는 문맥이 되고, "__"는 맞춰야할 다음 단어가 된다. 정답이 "태산"일 경우 태산에 대한 확률 값을 높이고 이외의 모든 단어는 확률을 낮추는 방향으로 모델을 업데이트 하는 것이다.

 

MLM 또한 예를 들어 "티끌 __ 태산"일 때, 티끌과 태산은 문맥이 되며 "__"은 맞혀야 할 빈칸 단어가 된다. 정답이 "모아"일 경우 "모아"에 대한 확률이 높아지며 이외의 단어는 확률이 낮아지는 방향으로 모델이 업데이트 된다.

 

이러한 업스트림 태스크의 장점은 self-supervised 학습이 가능하다는 것이다. 다른 말로 손수 라벨을 붙인 데이터가 필요한 지도 학습과 달리 수작업 라벨링 작업 없이 학습 데이터 생성이 가능하다는 것이다. 즉 데이터 내에서 정답을 생성하고 이를 바탕으로 학습하는 방법을 의미한다.

 

2.2 다운스트림 태스크 (downstream task)

다운스트림 태스크는 쉽게 말해 깊게 배우는 것을 의미한다. 업스트림 태스크를 통해 넓게 배운 다음 깊게 배우는 것이다. 다른 말로 downstream task는 실제로 구체적으로 풀고자하는 문제를 수행하는 것을 의미한다. 여러가지 task를 생성할 수 있지만 대표적인 예시는 아래와 같다.

- 문장 분류 (SC): → 문장에 대한 긍정, 부정, 중립에 대한 확률 값을 반환하는 태스크이다.

- 자연어 추론 (NLI) → 문장에 대한 참, 거짓, 중립 확률 값 반환하는 태스크이다.

- 개체명 인식 (NER) → 기관명, 인명, 지명 등 개체명 범주 확률값 반환하는 태스크이다.

- 질의 응답 (QA) → 질문이 주어질 때 답변에 대한 확률값을 반환하는 태스크이다 (각 단어가 정답의 시작일 확률값 + 끝일 확률값 반환

- 문장 생성 (SG) → 문장을 입력 받고 어휘 전체에 대한 확률 값 반환하는 태스크이다.

 

다운스트림 태스크에는 크게 3가지 방법이 존재한다. 파인 튜닝(fine-tuning)프롬프트 튜닝(prompt-tuning)인컨텍스트 러닝(in-context learning)이다. 

 

2.2.1 파인튜닝 (fine-tuning)

다운스트림 태스크 데이터 전체를 사용하는 것으로, 모델 전체를 업데이트 하는 것이 특징이다. 단점으로는 언어 모델이 크면 클수록 모델 전체 업데이트에 필요한 계산 비용이 발생한다. 이러한 단점으로 인해 프롬프트 튜닝과 인컨텍스트 러닝이 주목을 받는다.

 

2.2.2 프롬프트 튜닝(prompt tuning)

다운스트림 태스크 데이터 전체를 사용해서 모델을 일부 업데이트 하는 방법을 의미한다. 

 

2.2.3 인컨텍스트 러닝(in-context learning)

다운스트림 태스크 데이터 일부만 사용하는 방법으로 모델을 업데이트하지 않고 다운스트림 태스크를 수행하는 방법이다. 인컨텍스트 러닝은 크게 3가지 방식으로 나뉜다. 제로샷 러닝(zero-shot learning), 원샷 러닝(one-shot learning), 퓨샷 러닝(few-shot learning)이다.

 

- 제로샷 러닝: 다운스트림 태스크 데이터를 전혀 사용하지 않고 모델이 바로 다운스트림 태스크를 수행하는 것이다.

- 원샷 러닝: 다운스트림 태스크 데이터를 1건만 사용하는 것을 의미한다. 모델이 1건의 데이터가 어떻게 수행되는지 참고한 뒤 다운스트림 태스크를 수행한다.

- 퓨샷 러닝: 다운스트림 태스크 데이터를 몇 건만 사용하는 것을 의미한다. 모델은 몇 건의 데이터가 어떻게 수행되는지 참고한 뒤 다운스트림 태스크를 수행한다.

 

전이 학습에 대한 큰 범주의 내용은 위와 같다. 다음은 이제 Transformer란 무엇이고 BERT와 GPT에 어떻게 영향을 주었는지 알아보자.

 

3. 트랜스포머 (Transformer)

트랜스포머는 2017년 구글(Google)에서 제안한 시퀀스 투 시퀀스(sequence to sequence) 모델로 자연어처리를 위해 만들어졌고 이후에는 컴퓨터 비전에도 사용되고 있다(Vision Transformer). 앞서 말했듯 BERT와 GPT 또한 Transformer를 기반으로 만들어졌다. 트랜스포머 모델은 크게 인코더와 디코더로 구성되는데 BERT는 트랜스포머의 인코더, GPT는 트랜스포머의 디코더를 사용하여 만들어진 것이 특징이다. BERT는 Bidirectional Encoder Representation from Transformer의 약자이며 GPT는 Generative Pre-trained Transformer의 약자이다. 이름에서 모두 Transformer가 사용된 것을 알 수 있다. 트랜스포머에 대해 구체적으로 알아보기 위해 간단히 배경, 활용, 특징, 구조에 대해 차례대로 알아보자.

 

3.1 트랜스포머 모델 탄생 배경

트랜스포머 모델이 나오게 된 배경은 기존 자연어 처리 모델의 단점 때문이다. 트랜스포머 모델은 시퀀스 투 시퀀스 모델이라 했다. 이 시퀀스 투 시퀀스 모델을 처리하는 기존의 모델은 RNN 계열 모델이었다. 대표적으로 LSTM이 있었는데 이 방식의 단점은 크게 2가지 였다. long-term dependency 문제와 비병렬성 문제였다. long-term dependency 문제란 문장의 길이가 길고 단어 사이의 간격이 클수록 모델이 "잊게" 되어 단어 간의 관계 파악이 어려워지는 문제이며, 비병렬성의 경우 문장을 처리하는데 있어 LSTM 모델이 순차적으로 처리하기 때문에 시간 복잡도가 높다는 단점이다. 이를 극복하는 모델이 바로 트랜스포머 모델이다. 병렬처리를 통해 속도가 빠르면서도, 긴 문장 또한 관계 파악을 분명하게 처리할 수 있는 것이다.

 

3.2 트랜스포머 모델 활용 및 특징

이러한 트랜스포머 모델이 활용된 부분은 자연어처리 태스크 중 기계번역에 가장 처음으로 사용되었다. 예를 들면 소스 언어 (source language)인 한국어를 타겟 언어(target language)인 영어로 번역하는 것이다. 인코더를 통해 소스 언어의 시퀀스를 압축하며, 디코더를 통해 타겟 언어의 시퀀스를 생성하는 것이다.

소스 언어: 어제, 카페, 갔었어, 거기, 사람, 많더라

타겟 언어: I, went, to, the, cafe, there, were, many, people, there

 

여기서 특징은 소스 언어의 길이(시퀀스)와 타겟 언어의 길이(시퀀스)가 달라도 해당 태스크를 수행할 수 있다는 것이다. 트랜스포머의 특징 중 하나는 임의의 시퀀스나 속성이 다른 시퀀스 변환 작업 또한 가능하다. 예를 들면 필리핀 앞바다 한 달치 기온 데이터를 기반으로 1주일간 하루 단위 태풍 발생 예측이 가능하다. 즉, 기온 시퀀스로 태풍 발생 여부 시퀀스를 예측할 수 있는 것이다. 

 

트랜스포머의 최종 출력값 즉, 디코더의 출력은 타겟 언어의 어휘 수 만큼의 차원으로 구성된 벡터이다. 학습은 인코더와 디코더에 입력이 주어질 때 정답에 해당하는 단어의 확률값을 높이는 방식으로 수행된다. "어제 카페 갔었어 거기 사람 많더라"라는 입력이 들어갈 경우 출력으로는 "I went to cafe there were a lot of people"이 나오는데, 이 때 가장 처음 번역되어야 할 "I"에 대한 확률값이 높아지고 나머지 went, to, cafe, there, were, a, lot, of, people는 확률값이 낮아지고 "I"다음에 나올 단어는 "went"일 때 "went"에 대한 확률값이 높아지고 나머지 단어는 확률이 낮아지는 것이다.

 

트랜스포머의 활용에 있어 특징 중 하나는 학습 도중의 디코더 입력과 학습 후 인퍼런스 때의 디코더의 입력이 다르다는 것이다. 학습 과정에는 디코더의 입력에 맞혀야 할 단어가 went라면 이전의 정답 타겟 시퀀스인 "<s> I"를 입력한다. 반면 인퍼런스 과정에는 현재 디코더 입력에 직전 디코딩 결과를 사용한다. 

 

3.3 트랜스포머 모델 구조

트랜스포머 모델은 앞서 언급한대로 아래와 같이 크게 인코더와 디코더로 구성되어 있다. 

왼쪽 회색 박스에 해당하는 영역이 인코더이며 오른쪽에 회색박스에 해당하는 영역이 디코더이다. 실제 트랜스포머를 사용할 때는 인코더를 N개 디코더를 N개 만큼 쌓아 사용한다. 인코더와 디코더에는 공통적인 요소는 멀티 헤드 어텐션(Multi-Head Attention), 피드포워드 뉴럴넷(Feedforward Neural Network), 잔차 연결 & 레이어 정규화(Residual-Connection & Layer Normalization)이다. 그 중 트랜스포머 모델의 가장 핵심이 되는 것은 멀티 헤드 어텐션이다. 따라서 먼저 멀티 헤드 어텐션에 대해 알아보자.

 

 

3.3.1 셀프 어텐션(self attention)

트랜스포머가 다른 sequence to sequence 모델에 비해 경쟁력을 가질 수 있는 원천은 셀프 어텐션(self attention)에 있다. 정확히는 멀티 헤드 어텐션인데 이 멀티 헤드 어텐션이란 단순히 셀프 어텐션을 여러 개(head)를 붙인 것에 불과하다. 먼저 어텐션(attention)이란 단어 시퀀스 요소 중 중요한 요소에만 집중해 특정 태스크의 성능을 올리는 기법을 뜻한다. 앞서 언급한대로 셀프 어텐션은 RNN의 단점인 long-term dependency 문제와 비병렬성 문제를 극복하는 방식으로 동작한다. 또한 어텐션은 CNN의 단점 또한 극복하는 방법이기도 하다. CNN의 단점은 컨볼루션 필터의 크기를 넘어서는 문맥을 읽어내기 어렵다는 것이다. 때문에 어텐션은 RNN과 CNN의 핵심 단점인 전체 문맥을 고려할 수 없다는 것을 극복하는 방법이다. 개별 단어와 전체 입력 시퀀스를 대상으로 어텐션 계산을 진행하는 방식으로 동작한다. 모든 경우의 수를 고려하기 때문에 지역적 문맥만 고려할 수 있는 CNN보다 강하며, 시퀀스의 길이가 길어도 RNN처럼 정보를 잊거나 (기울기 소실로 인해) 정보가 왜곡되지 않는다. 셀프 어텐션은 이 어텐션 개념을 자신에게 수행하는 것이다. 가령 "I love you"라는 시퀀스가 있을 경우 아래와 같이 모든 경우의 수에서 어텐션 스코어를 계산해 특정 시퀀스 요소(I, love, you)가 어떤 것과 가장 연관성이 높은지 판단한다.

I → I

I → love

I → you

love → I

love → love

love → you

you → I

you → love

you → you

 

3.3.1.1 셀프 어텐션 동작 원리

셀프 어텐션의 어텐션 스코어를 계산하기 위해 필요한 것은 Query, Key, Value이다. 다른 말로 셀프 어텐션이란 Query, Key, Value의 세 요소 사이의 문맥적 관계성을 추출하는 과정이라할 수 있다. Query, Key, Value로부터 문맥적 관계성을 추출하는 절차는 아래와 같다.

 

(1) Query, Key, Value 생성 하기

먼저 Q, K, V 행렬을 생성해야 한다. 이는 입력 벡터 시퀀스인 $X$를, 랜덤 초기화된 행렬 $W_Q$, $W_K$, $W_V$과 곱해서 Q, K, V 행렬을 생성한다. 수식으로 나타내면 아래와 같다. 

$Q = X \times W_Q$

$K = X \times W_K$

$V = X \times W_V$

이후 $W_Q, W_K, W_V$ 세 행렬은 태스크를 잘 수행하는 방향으로 학습 과정에서 업데이트 된다.

 

(2) 셀프 어텐션 출력값 계산

(1)의 과정을 통해 Q, K, V 행렬을 계산했다면 셀프 어텐션을 계산할 수 있게 된다. 과정을 단일 수식으로 나타내면 다음과 같다.

 

$Attention(Q, K,V) = softmax$ $({QK^T\over \sqrt{d_K}})V$

 

이를 풀어서 이야기하면 $Q \times K^T$한 뒤, 모든 요소 값을 $K$ 차원 수의 제곱근으로 나누고(if 차원 = 3, $d_K=3$), 이 행렬을 행 단위로 소프트맥스를 취해 스코어 행렬로 만들어 주는 것이다. 그리고 이 스코어 행렬에 V를 행렬곱하면 셀프 어텐션 계산이 된다.

 

이러한 수식을 가지게 되는 이유는 다음과 같다.

1. $QK^T$라는 내적을 통해 시퀀스 요소 간의 유사도를 구할 수 있다. 예를 들어 $Q$에는 I, love, you라는 시퀀스가 올 경우 3개의 행과 전체 어휘 차원수의 열을 가지게 되고, $K^T$는 I, love, you라는 시퀀스가 3차원 열을 만들고 전체 어휘 차원수 만큼이 행이 되는 것이다. 결과적으로 $QK^T$는 I, love, you라는 시퀀스 간의 유사도, 정확히는 벡터의 유사도를 구할 수 있는 것이다. 

 

2. $\sqrt{d_K}$로 나누어줌으로써 안정적인 경사값(gradient)를 계산할 수 있게 된다. 참고로 이러한 셀프 어텐션에서 $QK^T$의 내적을 계산한 다음 $\sqrt{d_K}$로 나누는 것을 스케일 닷 프로덕트 어텐션(scaled dot product attention)이라고도 부른다.

 

3. softmax 함수를 통해 정규화를 한다. 이를 통해 모든 요소는 0~1 사이의 값을 갖게 된다. 참고로 softmax 함수를 수식으로 나타내면 $softmax (x_i)= {exp(x_i) \over \sum_jexp(x_j)}$이다.

 

4. 가중치 행렬 $V$를 행렬곱함으로써 단어 벡터를 계산할 수 있게 된다. 

 

3.3.1.2 셀프 어텐션 동작 원리 실습

이러한 위의 셀프 어텐션을 구하는 과정을 파이토치로 계산하는 과정은 다음과 같다. 

import torch
import numpy as np
from torch.nn.functional import softmax

# 1. define input vector sequence: 3 x 4 = (number of words what entered) * (word embedding dimension)
x = torch.tensor([
    [1.0, 0.0, 1.0, 0.0],
    [0.0, 2.0, 0.0, 2.0],
    [1.0, 1.0, 1.0, 1.0],
])

# 2. define weighted query, weighted key, weigted value
w_query = torch.tensor([
    [1.0, 0.0, 1.0],
    [1.0, 0.0, 0.0],
    [0.0, 0.0, 1.0],
    [0.0, 1.0, 1.0]
])

w_key = torch.tensor([
    [0.0, 0.0, 1.0],
    [1.0, 1.0, 0.0],
    [0.0, 1.0, 0.0],
    [1.0, 1.0, 0.0]
])

v_value = torch.tensor([
    [0.0, 2.0, 0.0],
    [0.0, 3.0, 0.0],
    [1.0, 0.0, 3.0],
    [1.0, 1.0, 0.0]
])

# 3. create query, key, value
queries = torch.matmul(x, w_query)
keys = torch.matmul(x, w_key)
values = torch.matmul(x, v_value)

# 4. create attension score
attn_scores = torch.matmul(queries, keys.T)

# 5. apply softmax function
key_dim_sqrt = np.sqrt(keys.shape[-1])
attn_probs = softmax(attn_scores / key_dim_sqrt)

# 6. weighted sum with values
weighted_values = torch.matmul(attn_probs, values)
print (weighted_values)

 

3.3.1.3 멀티 헤드 어텐션

멀티 헤드 어텐션이란 앞서 언급한대로 셀프 어텐션을 동시에 여러 번(multi-head) 수행하는 것을 의미한다. 여러 헤드가 독자적으로 셀프어텐션을 계산하는 것이다. 비유를 하자면 같은 문서(입력)를 두고 여러 명(헤드)이 함께 읽는 것이다. 여러 번의 셀프 어텐션을 수행하여 결과값을 더함으로써 일종의 앙상블 효과를 낸다. 치우치지 않고 정확성이 높은 결과를 도출할 수 있는 것이다. 

 

개별 헤드의 셀프 어텐션 수행 결과 = $\times$ $W^O$

$W^O$의 크기 = 셀프 어텐션 수행 결과 행렬 열 수 $\times$ 목표 차원 수

최종 수행 결과 → 입력 단어 수 $\times$ 목표 차원 수

 

여기까지 트랜스포머의 구성요소 중 멀티 헤드 어텐션에 대해 알아보았다. 다음으로는 피드포워드 뉴럴넷, 잔차 연결, 레이어 정규화, 드롭 아웃에 대해 알아보자.

 

3.3.2 피드포워드 뉴럴넷(feedforward neural network)

피드포워드 뉴럴넷의 구성요소는 DNN의 형태로 Input layer, hidden layer, output layer로 3가지로 구성되어 있으며, 피드포워드 뉴럴넷의 학습대상은 weight와 bias이다. 흔히 알고 있는 가장 기본적인 뉴럴넷 구조이다.

 

 

3.3.3 잔차 연결(residual connection)

잔차 연결이란 블록 또는 레이어 계산을 건너뛰는 경로를 하나 두는 것을 의미한다. 책의 그림상 3개의 잔차 연결을 두면 8개의 새로운 경로가 생기는 것을 확인할 수 있었다. 이러한 잔차 연결의 역할은 모델이 다양한 관점에서 블록이나 레이어 계산을 수행 가능하다. 딥러닝 모델은 레이어가 많아질수록 학습이 어려워진다. 그이유는 모델 업데이트를 위한 gradient가 전달되는 경로가 길어지기 때문이다. 하지만, 잔차 연결을 통해 모델 중간에 블록을 건너뛰는 경로를 설정함으로써 학습을 쉽게하는 효과가 있다.

 

 

3.3.4 레이어 정규화(layer normalization)

미니 배치 인스턴스 별로 평균 ($\mathbb{E}[x])$을 빼주고 표준편차($\sqrt{\mathbb{V}[x]}$로 나눠 정규화를 수행하는 기법이다. 이를 통해 학습이 안정되고 속도가 빨라지는 효과를 얻을 수 있다. 레이어 정규화에 사용되는 수식 요소는 엡실론($\epsilon$), 감마($\\gamma$), 베타($\beta$)이다. 감마, 베타는 학습 과정에서 업데이트 되는 가중치이며 엡실론은 분모가 0이 되는 것을 방지하기 위해 사용된다. 보통 $1e^{-5}$로 설정한다. 딥러닝 프레임워크 중 하나인 파이토치에서 레이어 정규화를 위한 LayerNorm 객체는 감마와 베타를 각각 1과 0으로 초기화 하며 이후 업데이트 과정을 거친다.

 

3.3.5 드롭아웃(dropout)

드롭아웃은 트랜스포머에서 추가적인 요소로 더 나은 일반화를 위해 사용된다. 드롭아웃은 과적합(overfit) 방지 기법으로 모델이 표현력이 좋아서 외워버리는 것을 방지한다. 드롭아웃을 적용할 때 딥러닝 프레임워크 중 하나인 파이토치의 특징은 torch.nn.Dropout을 사용할 때 안정적인 학습을 위해 각 요소 값에 1/(1-p)를 곱하는 역할을 수행한다. 예를 들면 드롭아웃 비율을 p=0.2로 설정할 경우 1/(1-p)에 의해 드롭아웃 적용후 살아남은 요소값에 각각 1.25를 곱하게 된다. 드롭아웃은 일반적으로 0.1을 사용한다. 또 드롭아웃은 학습 과정에만 적용하고 인퍼런스에는 적용하지 않는 것이 특징이다.

 

 

4. 토큰화

토큰화란 문장을 작은 단위로 분리하는 것을 의미한다. BERT와 GPT와 같은 자연어처리 모델에 입력을 위해서 우선적으로 토큰화 절차를 필요로 한다. 토큰화 방법은 크게 3가지가 존재한다. 단어 단위, 문자 단위, 서브워드 단위이다..

 

단어 단위 토큰화 (word-level)

단어(어절) 단위로 토큰화를 수행하는 것을 의미한다. 예시로는 "어제 카페 갔었어"라는 문장은 "어제", "카페", "갔었어"로 분리 되는 식이다. 이 방식의 단점은 가능한한 많은 단어에 대해 고려해야 하기 때문에 어휘 집합(lexical set)의 크기가 커질 수 있고, 커지면 모델 학습이 어려워지게 된다.

 

문자 단위 토큰화 (character level)

문자 단위로 토큰화를 수행하는 것을 의미한다. 문자 단위란 ㄱ, ㄴ, ㄷ, ㄹ / a, b, c, d를 의미한다. 참고로 한글로 표현 가능한 글자는 총 11,172개이다. 문자 단위 토큰화 방식의 장점은 해당 언어의 모든 언어를 포함할 수 있기 때문에 미등록된 토큰(Out Of Vocabulary, OOV) 문제로부터 자유롭다. 반면 단점으로는 각 문자 토큰은 의미있는 단위가 어렵다는 것이다. 예를 들면 "제"의 어와 "어미"의 어의 구분이 사라지게 된다. 또한 토큰 시퀀스 길이가 길어지기 때문에 학습 성능이 저하된다는 단점도 존재한다. 예시로는 "어제 카페 갔었어" → 어, 제, 카, 페, 갔, 었, 어로 변환되어 토큰 시퀀스가 길어지는 것이다.

 

서브워드 단위 토큰화 (sub-word level)

단어 단위 토큰화와 문자 단위 토큰화의 중간 형태로 둘의 장점만 취한 것이다. 특징은 어휘 집합의 크기가 지나치게 크지도 않고, 미등록 토큰 문제를 해결하며, 토큰 시퀀스가 너무 길어지지 않게 하는 특징을 가진다. 대표적인 구현 예시가 BPE (Byte Pair Encoding)이다.

 

4.1 Byte Pair Encoding (BPE)

BPE는 1994년에 처음제안된 정보 압축 알고리즘이다. 하지만 근래에는 자연어처리의 토큰화 기법으로 사용된다.(BPE는 기계 번역분야에서 가장 먼저 쓰임) 토큰화를 진행 할 때 빈도수 높은 문자열을 병합해 데이터를 압축한다. 압축 알고리즘의 동작 절차 예시는는 다음과 같다.

 

1. aaabdaaabac가 있을 때 빈도수가 가장 높은 aaa를 Z로 치환해 ZabdZabac로 치환한다.

2. 다음 빈도수가 높은 ab를 Y로 치환해 ZYdZYac로 만든다.

3. ZY를 다시 X로 치환하여 XdXac로 치환한다. 

 

기존의 어휘수는 (a, b, c, d) 였으나 압축 어휘수는 (a, b, c, d, Z, Y, X)로 늘었고, 압축 전 글자수는 11글자에서 압축 후 글자수는 5글자가 되었다. 이러한 BPE 알고리즘의 장점은 사전 크기 증가를 적당히 억제하면서도 정보를 효율적으로 압축 가능하며, 분석 대상 언어에 대한 지식이 필요하지 않다는 것이다. 이러한 BPE 알고리즘의 대표적인 활용 예시는 GPT 모델에서 사용된다. GPT 모델은 BPE를 통해 토큰화를 수행한다. 반면 BERT는 wordpiece로 토큰화를 수행한다.

 

4.2 BPE 어휘 집합 구축

BPE 어휘 집합은 구축은 한 마디로 요약하면 고빈도 bi-gram 쌍을 병합하는 방식으로 구축한다. 구체적으로는 먼저, pre-tokenize를 통해 corpus의 모든 문장을 공백기준으로 나눈다. 이후 사용자가 정의한 크기(어휘 집합)이 될 때 까지 가장 높은 빈도수대로 추가한다. 높은 빈도수란 n-gram을 기준으로 하며, 일반적으로 성능과 계산량의 trade-off 관계로 인해 5-gram 아래를 사용하는 것이 좋다고 알려져 있다. 

 

4.3 WordPiece (워드피스)

워드피스 알고리즘 또한 토큰화를 수행하는 방법 중 하나로 자주 등장한 문자열을 토큰으로 인식한다는 점에서 BPE와 본질적으로 유사성을 가진다. 차이점으로는 병합 기준에 있다. 워드피스는 BPE 처럼 단순 빈도수 기반이 아니라, 우도(likelihood)를 가장 높이는 쌍을 병합하는 것이다. 한 마디로 워드피스는 우도를 가장 높이는 글자 쌍을 병합한다.

 

4.4 어휘 집합 구축하기

BPE & wordpiece 기반 토크나이저 만들기

 

Q. 왜 wordpiece에는 special char인 [PAD]를 사용하지 않는가? 정확히는 BPE는 special char [PAD]를 추가해주는데 wordpiece tokenizer에는 그런 과정이 없는가?

A. ?

 

Q. 왜 위 둘의 결과물은 유니코드로 되어 있어 보기 힘들까?

A. 학습 데이터가 한글이고, 한글은 3개의 유니코드 바이트로 표현됨. GPT 모델은 바이트 기준 BPE를 적용하기 때문임.

 

indexing: 토큰 → 인덱스

  • 예시: “별루 였다..” → 4957, 451, 363, 263, 0, 0, 0, ...

input_ids: vocab.txt 또는 vocab.json에 순서대로 부여된 번호로 나타내는 토큰 인덱스 시퀀스

attention_mask: 일반 토큰(1), 패딩 토큰(0) 구분하는 역할

token_type_ids: BERT 모델은 기본적으로 문장 2개 이상을 입력받기에 문장 번호를 이것으로 구분

 

 

5. Pretrained Language Model (PLM)

언어 모델이란 단어 시퀀스에 확률을 부여하는 모델이다. 수식은 $P(w_1, w_2, w_3, ..., w_n)$로 표현 가능하다. 넓은 의미에서의 언어 모델은 맥락이 전제된 상태서 특정 단어(w)가 나타날 조건부 확률이다. 수식으로는 $P(w|context)$로 표현 한다. 예시는 $P(운전|난폭) = {P(난폭, 운전) \over P(난폭)}$이다. 만약 잘 학습된 한국어 모델이 있을 경우 $P(무모|운전)$ 보다 $P(난폭|운전)$이 확률이 더 높을 것이다. 

 

단어가 3개 동시에 등장 확률은 $P(w_1, w_2,w_3) = P(w_1) * P(w_2|w_1) * P(w_3|w_1,w_2)$이 된다. 

언어 모델을 조건부 확률로 다시 쓰면, $P(w_1, w_2,w_3, w_4, \dots, w_n) = \prod_{i=1}^nP(w_i|w_1,w_2,w_3,\dots,w_{i-1})$이다.

 

언어 모델 종류에는 크게 순방향과 역방향이 있다. 순방향의 예시는 "어제 → 카페 → 갔었어 → 거기 → 사람 → 많더라" 순으로 순차적으로 예측하는 것을 의미한다. 대표적인 예시로는 GPT와 ELMo가 이 방식으로 pretrain을 수행한다. 역방향의 예시는 "많더라 → 사람 → 거기 → 갔어어 → 카페 → 어제"로 역순으로 예측하는 것을 의미한다. 대표적으로 ELMo가 이 방식으로 pretrain을 수행한다. 참고로 ELMo는 순방향, 역방향을 모두 활용하는 기법이다.

 

5.1 마스크 언어 모델(Masked Language Model)

마스크 언어 모델이란 학습 대상 문장에 빈칸을 만들고 빈칸에 올 적절한 단어를 분류하는 과정으로 학습하는 것이다. 대표적으로 BERT가 이 방식을 이용하여 업스트림 태스크를 수행한다. 마스크 언어 모델의 장점은 문장 전체의 맥락을 참고 가능하다는 것이다. 때문에 마스크 언어 모델에 양방향 성질이 있다고 말한다.

 

5.2 스킵-그램 모델(skip-gram model)

단어 앞뒤 특정 범위(window size)를 지정 후, 범위 내에 어떤 단어 올지 분류하는 과정으로 학습하는 것이다. 예시로는 window size = 2라 가정할 경우, 아래와 같이 center word 앞 뒤로 2개씩 단어(context word)가 있고 context word를 기반으로 center word를 예측(분류)하는 방식이다.

  1. 어제 카페 갔었어 거기 사람 많더라
  2. 어제 카페 갔었어 거기 사람 많더라

 

 

 

6. 자연어 처리 태스크 종류

6.1 문서 분류 태스크

문서 분류 모델은 (이 책에서 사용하는) 입력 문장을 토큰화한 뒤 [CLS]와 [SEP]를 토큰 시퀀스 앞뒤에 붙인다. 이후 BERT 모델에 입력하고 문장 수준 벡터인 pooler_output을 뽑는다. 이 벡터에 추가 모듈을 덧붙여 모델 전체의 출력이 긍정 확률, 부정 확률 형태로 만든다.

  • pooler_output 벡터 뒤 붙는 추가 모듈은 pooler_output 벡터에 드롭아웃 적용 일부를 768차원중 일부를 0으로 변경한다.
  • 만약 분류 대상이 2가지라면 가중치 행렬 크기는 768 * 2가 됨.

여기서 pooler_output이란 무엇일까? pooler_output은 메서드의 하나로 last_hidden_state라는 메서드와 연관이 있다. pooler_output과 last_hidden_state는 모두 BERT 모델에서 단어와 문장을 벡터로 변환한 것이다. 구체적으로 pooler_output은 BERT 모델에서의 최종 출력값인 벡터 시퀀스이다. 예를 들어 768차원으로 임베딩된다 가정하고, 입력 문장이 [”안녕하세요”, “저의”, "이름은“, "로이입니다”]일 경우 pooler_output은 4 x 768의 shape을 가지게 된다. 즉 아래와 같은 형태로 특정 단어에 대한 확률값을 벡터를 가진 요소이다. (예시를 위해 확률값은 임의 설정함)

 

안녕하세요 = [0.01, 0.04, 0.09, ... , 0.02]

저의         = [0.02, 0.03, 0.04, ... , 0.01]

이름은      = [0.04, 0.07, 0.09, ... , 0.09]

로이입니다 = [0.03, 0.06, 0.01, ... , 0.1]

 

이러한 pooler_output을 다르게 말해 문장 수준 임베딩이라 할 수 있다. 다시 말해 4개의 문장(단어)이 768차원의 벡터로 바뀐 것이다. 1개의 벡터 (1x768)은 하나의 문장 전체를 표현한다. pooler_output 메서드를 통해 이 결과값을 확인할 수 있다.

 

반면 last_hidden_state는 단어 수준 임베딩을 의미하는 것으로 예를 들어 4x10x768의 3차원 형태로 표현될 수 있다. 이는 4개의 문장이 길이 10을 가지고 있고, 768차원 형태라는 것이다.

 

 

 

 

6.1.1 문서 분류 모델 학습하기

Korpora 라이브러리를 통해 데이터를 내려받을 수 있다. 문서 분류 모델 학습을 위해 NSMC 데이터셋을 다운로드 받는다. 이후 데이터로더를 통해 학습 데이터를 배치 단위로 모델에 공급한다. 배치 단위로 공급할 때 데이터셋 내에 있는 인스턴스를 배치 크기 만큼으로 뽑는다. 데이터셋이 100개가 있다고 가정할 경우 인스턴스는 개별 요소 1개가 되며 배치크기가 10이라면 인스턴스 10개가 모여 1개의 배치를 이루게 된다.

  •  

6.1.2 인퍼런스 실습

 

토크나이저 초기화

from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained(
	args.pretrained_model_name, 
	do_lower_case=False,
)

체크포인트 로드

import torch
fine_tuned_model_ckpt = torch.load(
	args.downstream_model_checkpoint_fpath,
	map_location=torch.device("cpu"),
)

BERT 설정 로드

from transformers import BertConfig
pt_model_config = BertConfig.from_pretrained(
	args.pretrained_model_name,num_labels=fine_tuned_model_ckpt["stte_dict"]["model.classifier.bas"].shape.numel(),
)

BERT 모델 초기화

from transformers import BertForSequenceClassification
model = BertForSequenceClassificiation(pt_model_config)

체크포인트 주입

model.load_state_dict({k.replace(”model.”, “”): v for k, v in fine_tuned_model_cpkt[’state_dict’].items()})

평가 모드 전환

model.eval()

 

 

6.2 문장 쌍 분류 태스크

문장 2개가 주어졌을 때 문장 사이 관계가 어떤 범주일지 분류하는 것을 의미한다. 두 문장 관계는 참(entailment), 거짓(contradiction), 중립 또는 판단 불가(neutral)로 가려낼 수 있다. 예를 들어 "나 출근했어 + 난 백수야"는 거짓이 된다. 반면 "나 출근했어 + 난 개발자다"면 중립이 되는 식이다. 문장 쌍 분류 태스크를 위해 업스테이지가 공개한 KLUE-NLI 데이터셋을 활용하여 실습을 진행하였다.

 

모델 구조

문장 쌍 분류 모델은 전제와 가설 두 문장을 사용한다. 따라서 "[CLS] + 전제 + [SEP] + 가설 + [SEP]"의 형태로 이어 붙인다. 이후 토큰화 후 모델에 입력한 뒤, 문장 수준 벡터(pooler_output)을 추출한다. pooler_output에는 전제와 가설의 의미가 응축되어 있음. 여기에 추가 모듈을 붙여 모델 전체 출력이 아래의 형태가 되도록 한다.

  • 전제에 대해 가설이 참일 확률
  • 전제에 대해 가설이 거짓일 확률
  • 전제에 대해 가설이 중립일 확률

 

 

6.3 개체명 인식 태스크

범주 수가 m개이고 입력 토큰이 n개일 때 문서 분류, 문장 쌍 분류 모델은 모델 출력은 m차원의 확률 벡터 1개이다. 시퀀스 레이블링은 m차원 확률벡터가 n개 만들어진다.

 

방법론 입력 출력 대표 과제

문서 분류 문장 1개 한 문서가 속하는 범주에 대한 확률 감성 분석
문장 쌍 분류 문장 2개 두 문서를 아우르는 범주에 대한 확률 자연어 추론
시퀀스 레이블링 문장 1개 토큰 각각의 범주 확률 개체명 인식

 

 

6.4 질의응답 태스크

질의응답 태스크의 유형은 다양하지만 본 책에서는 지문(context)에서 답을 찾는 것으로함 즉, open-book qa이다.

모델의 입력은 질문과 지문(Question, Context)가 되며, 모델 출력은 입력한 각 토큰이 [정답 시작일 확률, 정답 끝일 확률]이 된다. 질의응답 모델은 출력 확률을 적당한 후처리 과정을 통해 사람이 이해 가능한 형태로 가공한다. 실습을 위해 LG CNS가 공개한 KorQuAD 1.0 데이터를 활용하였다. 모델 구조는 토큰화 후, "[CLS] 질문 [SEP] 지문 [CLS]"의 형태로 입력된다.

 

 

6.5 문장 생성 태스크

문장 생성은 컨텍스트(context)가 주어졌을 때 다음 단어로 어떤 단어가 오는 게 적절한지 분류하는 것이다. 모델 입력은 컨텍스트가 되며 모델 출력은 다음 토큰이 등장할 확률이 된다. 수식으로 일반화하여 나타내면 $P(w|context)$이다. 조금 구체적으로는 $P(w|안녕)$일 경우 "안녕" 다음에 올 단어의 확률이 출력된다. 이런 문장 생성 태스크는 문서 분류, 문서 쌍 분류, 개체명 인식, 질의응답 태스크와 특성이 다르다. 가장 큰 차이는 모델 구조이다.

 

모델 구조 GPT BERT
pretrain task 다음 단어 맞히기 빈칸 맞히기
fine tuining 다음 단어 맞히기 각 다운스트림 태스크

 

GPT는 BERT와 달리 pretrain task와 fine tuning 자체가 다음 단어를 맞히는 것으로, 문장 생성 Task에 더 적합한 모델이라 할 수 있다. 

 

6.5.1 토크나이저

  • eos_token은 문장 마지막에 붙는 스페셜 토큰으로 SK Telecom이 모델을 pretrain할 때 지정했으므로 같은 방식으로 사용
  • from transformers import PreTrainedTokenizerFast tokenier = PreTrainedTokenizerFast.from_pretrained( args.pretrained_model_name, eos_token=”</s>”, )

 

6.5.2 문장 생성 전략 수립

문장 생성을 위해서는 문장 생성 전략 수립이 제일 중요하다. 이를 위해, 문장 생성을 위한 단어를 탐색하는 테크닉 종류 두 가지가 있다. 크게 빔서치그리드 서치로 나뉜다. 두 방법 모두 최고 확률을 내는 단어 시퀀스를 찾는 방법이다. 차이점이 있다면 빔서치의 계산량이 그리디 서치의 계산량보다 많다는 것이다. 그 이유는 그리디 서치는 최고 확률을 내는 한 가지 경우의 수를 내지만, 빔서치의 경우 빔 크기만큼의 경우의 수를 낼 수 있기 때문이다. 빔서치는 그리디 서치보다 조금 더 높은 확률을 내는 문장생성이 가능하다는 장점이 있다. 이런 빔서치와 그리드 서치에 도움을 주는 내부 파라미터가 4개 있다. repetition_penalty, top-k sampling, top-p sampling, temperature scaling이다. 

 

repetition_penalty

반복되는 표현을 줄여주는 역할을 한다. default 값은 1.0이다. 만약 이 값을 적용하지 않는다면 그리디 서치와 동일한 효과를 낸다.

 

top-k sampling

모델이 예측한 토큰 확률 분포에서 확률값이 가장 높은 k개 토큰 가운데 하나를 다음 토큰으로 선택하는 기법이다. top_k를 1로 설정할 경우 그리디 서치와 동일한 효과를 낸다.

 

top-p sampling

확률값이 높은 순서대로 내림차순 정렬 후 누적 확률값이 p 이상인 최소 개수의 토큰 집합 가운데 하나를 다음 토큰으로 선택하는 기법이다. 뉴클리어스 샘플링이라고도 한다. 특징은 0에 가까울수록 후보 토큰이 줄어 그리디 서치와 비슷해지며, 1이 되면 모든 확률값을 고려하기 때문에 이론상 어휘 전체를 고려하게 된다.

 

템퍼러처 스케일링(temperature scaling)

토큰 확률 분포의 모양을 변경하는데 이는 모델의 출력 로짓의 모든 요소값을 temperature로 나누는 방식이기 때문이다. 예를 들어 로짓: [-1.0 2.0 3.0], temperature: 2라면 템퍼러처 스케일링 후 로짓은 [-0.5 1.0 1.5]가 된다. 이 템퍼러처 스케일링의 특징은 0에 가까울수록 확률분포모양이 원래보다 뾰족해진다. 또 1보다 큰 값을 설정할 시 확률분포가 평평(uniform) 해진다. 핵심은 이 값이 1보다 적으면 상대적으로 정확한 문장, 1보다 크면 상대적으로 다양한 문장이 생성되는 특징을 가진다. 템퍼러처 스케일링은 top-k 샘플링과 top-p 샘플링과 함께 적용해야 의미가 있다고 한다.

 

 

 

 

7. BERT와 GPT 비교와 모델 크기를 줄이는 기법

BERT는 의미 추출에 강점을, GPT는 문장 생성에 강점을 지닌다. BERT는 트랜스포머의 인코더를 사용했고 GPT는 디코더만을 사용했다. 최근 자연어처리 트렌드는 모델 크기를 키우는 것으로 크기를 키움으로써 언어 모델의 품질이 향상되고 다운스트림 태스크의 성능도 좋아지게 하는 것이다. 하지만 모델의 크기가 너무 커지면 계산 복잡도가 높아지는데 이러한 계산량 또는 모델 크기를 줄이려는 여러 시도가 있는데 대표적으로 4가지가 있다.

디스틸레이션 (distillation)퀀타이제이션 (quantization)프루닝 (pruning), 파라미터 공유 (weight sharing)

 

 

8. 학습 파이프라인

자연어 처리 모델을 학습시키기 위한 일련의 파이프라인은 다음과 같다.

1. 하이퍼파라미터 설정 (learning rate, batch size, epochs, ...)

2. 학습 데이터셋 준비

3. pretrain된 모델 준비

4. tokenizer 준비

5. 데이터 로더 준비

6. 태스크 정의 

7. 모델 학습

의 과정으로 이루어진다.

 

여기서 데이터 로더란 데이터를 모델의 입력에 필요한 형태인 배치(batch)로 만들어주는 역할을 한다. 배치를 만들기 위해 전체 데이터셋 가운데 일부 샘플(instance)를 추출해 배치로 구성한다. 배치 구성을 위한 인스턴스 추출방식에는 랜덤 샘플링과 시퀀셜 샘플링 방식이 있다. 랜덤 샘플링 방식은 주로 train 데이터셋을 구성할 때 사용되고 시퀀셜 샘플링은 valid, test 데이터셋을 구성할 때 사용한다. 배치란 인스턴스의 합이다. 여기서 인스턴스란 문장과 라벨을 갖는 하나의 가장 작은 요소이다. 여기서 인스턴스란 문장과 라벨을 갖는 하나의 가장 작은 요소이며, 인스턴스가 모여서 배치를 이루게 된다. 데이터 로더에는 컬레이트라는 과정이 있다. 컬레이트는 모델의 최종 입력으로 만들어주는 과정이다. 태스크 정의에는 주로 요즘은 파이토치 라이트닝을 사용한다. 그 이유는 딥러닝 모델 학습시 반복적인 내용을 대신 수행해줘 사용자가 모델 구축에만 신경쓸 수 있도록 돕기 때문이다.

 

 

 

9. 자연어 처리 관련 양질 국내 강의

 

 

 

10. 기타 내용

극성 레이블 (polarity label)

  • 예시: 부정 = 0, 긍정 = 1

model.eval()

  • 학습때만 필요한 기능들을 꺼서 평가 모드로 동작하도록 함

tokenizer.encode()

  • 입력 문장 토큰화 후 정수 인덱싱하는 역할 수행
  • 인자로 return_tensors=’pt’를 주게되면 인덱싱 결과를 파이토치 텐서 자료형으로 변환

언어 모델 유용성

  • NSP, MLM: 데이터 제작 비용 절감
  • Transfer Learning

언어 모델 최종 또는 중간 출력값 = 임베딩(Embedding) or 리프레젠테이션(representation)

+ Recent posts