
Transformer의 발표 이후 NLP 분야의 발전 속도는 가히 엄청났다. Transformer 구조를 활용하여 엄청난 양의 데이터를 학습하기 시작했고, 현재의 LLM (Large Language Model) 열풍으로까지 이어졌다. 그 중 대중들에게 공개가 되며 큰 이슈와 파장을 일으킨 모델, openAI에서 개발한 ChatGPT가 있다. 현재는 인간의 지능을 몇 배나 뛰어넘는 모습을 보여준다. ChatGPT의 시초가 되는 모델, GPT-1에 대해 알아보고 실제로 구현해볼 것이다.
GPT-1 이후 openAI에서 발표한 모델들은 데이터셋과 모델 파라미터 등을 전부 비공개로 해놓아 필자의 역량에서 모델 구현은 GPT-1 까지가 한계일 것 같다..
역시나 Transformer에 대한 이해가 필수적인 글이다. 이 글을 참고하길 바란다.
논문 링크
기존 언어 모델 훈련 과정에서 문제는 labeled data가 부족하다는 점이었다. 특히나 질의, 유사도 분석 등 특정 과제를 수행하기 위한 task-specific labeled data가 부족했다. 반면, 인터넷, 서적 등에서 그냥 긁어올 수 있는 unlabeled data는 무궁무진하게 많다.
GPT-1에서는 바로 이 부분을 활용하고자 한다. GPT의 이름 (Generative Pre-training)에서 볼 수 있듯이, 엄청난 양의 데이터에 대해 비지도 사전학습을 시킨 뒤 특정 task에 맞게 labeled data로 fine-tuning 하는 것이다.
그 결과 GPT-1은 다양한 자연어처리 과제에서 전반적으로 높은 정확도를 기록할 수 있었다!
자연어 처리 분야에서 label이 없는 데이터로 비지도학습을 하는 기술은 점점 중요해지고 있다. 시간과 비용 측면에서 큰 이득을 볼 수 있고 언어 자체에 대한 좋은 representaion을 학습하여 성능 향상을 기대할 수 있다. 하지만 이러한 비지도 학습은 두 가지 측면에서 어려움이 있는데:
1) it is unclear what type of optimization objectives are most effective at learning text representations that are useful for transfer
2) there is no consensus on the most effective way to transfer these learned representations to the target task
따라서 본 논문에서는 unsupervised pre-training + supervised fine-tuning 의 방법을 제안하며, 최소한의 모델 변형으로 target task로의 전이가 가능하도록 한다.
전체적으로 2단계의 학습 과정을 거치는데:
먼저 대용량 unlabeled data로 언어 자체에 대한 'universal representation'을 학습한 뒤, target task의 labeled data로 파라미터를 조정하며 세부 영역에 대한 모델의 능력을 키운다.
이때 GPT-1의 모델 자체는 Transformer의 디코더 부분을 사용한다.
1) Semi-supervised learning for NLP
Sequence labeling, text classification 등 과제에서 큰 각광을 받았다. 그러나 이전까지의 연구에서는 '단어' 단위로만 활용되었기 때문에 본 논문에서는 구, 문장 level의 수준으로 활용하고자 한다.
2) Unsupervised pre-training
'Good initialization point'를 찾는 과정이며, 이미지 분류, 음성 인식 등의 분야에서 좋은 성과를 보였다. 자연어 처리 분야에서도 신경망을 pre-training 하는 선행 연구가 존재하는데, 이는 LSTM 구조를 사용하였기에 긴 문장 처리에서 어려움이 있었다. 따라서 본 논문에서는 Transformer를 활용하였고, 다양한 분야에서 최소한의 변형으로도 좋은 성능을 낼 수 있었다.
3) Auxiliary training objectives
보조 목적 함수를 target task 목적 함수에 추가하는 것은 성능 면에서 증명된 방법이다. 본 논문에서도 역시 supervised learning 단계에서 보조 목적 함수를 추가한다.
훈련 과정:
1) 대규모 text corpus로 LM 학습
2) labeled data로 각 task에 맞게 fine-tuning
학습 목적 함수는 NLP 분야에서 흔히 볼 수 있는 log-likelihood 최대화 함수이다. 즉, 이전 k개의 단어들을 보고 다음 단어를 예측하게 되는 next-word prediction 형태이다.
여기서 language model로 GPT-1은 Transformer의 디코더 부분만을 여러 레이어로 사용한다.
Next-word prediction 방식으로 학습을 하기에 디코더 구조가 적합하며, 모델 구조가 좀 더 간결해져 연산량의 이점도 존재한다.
위 구조에서 볼 수 있듯 디코더 레이어를 12개 쌓았으며, 인코더를 사용하지 않기 때문에 기존의 encoder-decoder attention (cross attention) 부분은 사용하지 않는다.
이제 labeled data로 parameter 조정 단계를 거치는데, 사전 학습된 모델에 input을 넣어 최종 출력 을 얻는다. 그 후 linear layer 를 통과시켜 y에 대한 예측값을 얻어 마찬가지로 log-likelihood 최대 함수를 계산한다.
여기에 3-1의 pre-training loss를 보조 목적 함수로 더하여 최종적인 fine-tuning loss를 계산한다.

지금까지 설명한 구조는 text classification task에 적용되는 것이다. 만약 다른 task를 위해 fine-tuning 한다면 어떻게 조정해야 할까?
앞서 설명했듯이 GPT-1은 최소한의 input 구조 변화로 전이 학습이 가능하게 한다. Traversal-style approach라고 하며, 모델 구조 자체를 바꾸는 것이 아니라 각 task에 맞는 구조화된 입력 (structured input)을 사용한다.

1) Text classification task
단순 텍스트 분류 문제로, 전체 문장이나 글을 입력으로 넣으면 된다.
2) Textual entailment task
전제 (premise)와 가정 (hypothesis), 두 문장과 중간에 문장 구분자 (delim)를 함께 입력 받는다. 두 문장 사이의 관계를 분류하게 된다.
3) Similarity task
두 문장이 얼마나 유사한지 측정하는 문제다. 이때는 두 문장의 순서가 관계없기 때문에 순서가 바뀐 두 입력 시퀀스를 받아 마지막에 eliment-wise addition 해준다.
4) Question Answering and Commonsense Reasoning task
지문 z, 질문 q, 그리고 정답 리스트 {}가 주어지는 문제로, 특정 질문에 대한 정답을 고르는 문제다. 이 경우 z, q, {a_k}를 구분자로 연결한 여러 시퀀스를 독립적으로 입력한다. 그 이후 softmax 연산으로 가장 정답 분포에 가까운 답을 선택한다.
4가지의 task에 대해 다양한 데이터셋으로 실험을 해본 결과, 총 12개의 데이터셋 중 9개에 대해 SOTA를 달성하였다.
1) Classification & Semantic Similarity
분류 문제, 유사도 문제에서 대체로 좋은 성능을 보여준다.
2) Natural Language Inference
위에서 설명한 textual entailment task로, 6개 중 5개 데이터셋에서 최고의 성능을 보여준다.
3) Question answering and common sense reasoning
마찬가지로 기존보다 좋은 성능을 보여준다.
1) Impact of Number of Layers Transferred
전이된 레이어 개수에 따른 성능을 실험한 결과이다. 사전학습된 레이어를 많이 사용할수록 성능도 올라가는 현상을 보여주어, downstream task에 대한 유용한 정보를 갖고 있다고 볼 수 있다.
2) Zero Shot Behaviors
왜 Transformer의 사전 학습이 효과적인지 밝히고자 한 실험이다. Fine-tuning 없이 사전 학습만을 하고 다양한 task에서의 성능 (zero-shot performance)을 실험해보았다. LSTM (점선)과 비교했을 때 모든 task에서 더 좋은 성능을 보여주었고, 사전 학습 횟수가 많을수록 성능이 안정적으로 증가하는 모습이다.
사전 학습 과정은 downstream task을 해결하는데 도움을 주고, Transformer 구조가 이에 적합함을 증명한다.
3) Ablation studies
세 가지 ablation 실험을 진행한 결과는 다음과 같다:
1) Fine-tuning 단계에서 보조 목적 함수는 작은 데이터셋보다 큰 데이터셋 (QQP 등)을 학습할 때 도움이 됐다.
2) LSTM과 비교했을 때 현저히 높은 성능을 보여준다.
3) Pre-training을 하지 않을 경우 모든 과제에서 성능이 크게 하락한다.
모델 구조의 대부분은 이미 Transformer에서 설명하였다. 여기선 설명이 필요한 부분만 남겨놓겠다. Pre-training과 분류 문제를 위한 fine-tuning 과정도 구현하였는데, 시간과 자원 문제로 적은 양의 학습을 진행했다.
전체 코드는 깃허브 참조.
기존 Transformer 구조와 다른 점은 Decoder 부분에 cross-attention이 없다는 것뿐이다.
class DecoderLayer(nn.Module):
def __init__(self, d_model, n_heads, d_ff, resid_drop):
super().__init__()
self.mha = MHA(d_model, n_heads)
self.dropout1 = nn.Dropout(resid_drop)
self.layernorm1 = nn.LayerNorm(d_model, eps=1e-5)
self.ffn = FFN(d_model, d_ff)
self.dropout2 = nn.Dropout(resid_drop)
self.layernorm2 = nn.LayerNorm(d_model, eps=1e-5)
def forward(self, x, attn_mask):
# Masked-MHA layer (with residual shortcut connection)
residual = self.mha(x, x, x, attn_mask)
residual = self.dropout1(residual)
x = self.layernorm1(x + residual)
# FFN layer (with residual shortcut connection)
residual = self.ffn(x)
residual = self.dropout2(residual)
output = self.layernorm2(x + residual)
return output
GPTLMHead는 사전학습 모델로 사용되며, GPTClsHead는 분류 task를 위한 fine-tuning 모델이다.
### Language Model (pre-training)
class GPTLMHead(nn.Module):
def __init__(self, gpt):
super().__init__()
vocab_size, d_model = gpt.decoder.embedding.weight.size()
self.gpt = gpt
self.linear = nn.Linear(d_model, vocab_size, bias = False)
self.linear.weight = gpt.decoder.embedding.weight
def forward(self, x):
x = self.gpt(x)
lm_logits = self.linear(x)
return lm_logits
### Classification Model (fine-tuning)
class GPTClsHead(nn.Module):
def __init__(self, gpt, n_class, cls_token_id, cls_drop=0.1):
super().__init__()
vocab_size, d_model = gpt.decoder.embedding.weight.size()
self.cls_token_id = cls_token_id
self.gpt = gpt
# LM
self.linear1 = nn.Linear(d_model, vocab_size, bias=False)
self.linear1.weight = gpt.decoder.embedding.weight
# Cls
self.linear2 = nn.Linear(d_model, n_class)
self.dropout = nn.Dropout(cls_drop)
nn.init.normal_(self.linear2.weight, std=0.02)
nn.init.normal_(self.linear2.bias, 0)
def forward(self, x):
outputs = self.gpt(x)
lm_logits = self.linear1(outputs)
outputs = outputs[x.eq(self.cls_token_id)]
cls_logits = self.linear2(self.dropout(outputs))
return lm_logits, cls_logits
먼저 WikiText-2로 사전 학습을 진행하며, 이후 IMDB Reviews Dataset (영화 리뷰 감성 분석 데이터셋) 으로 fine-tuning을 하였다. 위에서 설명하였듯이 fine-tuning 시 사용하는 loss function은 pre-training loss를 보조 목적 함수로 활용한다.
lm_logits, cls_logits = model(inputs)
lm_logits = lm_logits[:, :-1].contiguous()
## Loss Function w/ Auxiliary Function
lm_loss = F.cross_entropy(lm_logits.view(-1, lm_logits.size(-1)),
inputs[:, 1:].contiguous().view(-1), ignore_index=0) # L1 (Auxiliary)
cls_loss = F.cross_entropy(cls_logits, labels) # L2
loss = cls_loss + (auxiliary_ratio * lm_loss) # L3
상세 코드: https://github.com/tony3ynot/GPT-1
GPT-1은 chatGPT의 시초로 LLM 분야의 큰 기둥이 되는 모델이다. 라이벌 모델 격인 BERT도 존재했지만, 현재까지 대세는 여전히 GPT다. 부족한 데이터셋을 극복하기 위해 대형 corpus로 사전학습을 하고, 몇 안되는 task-specific 데이터로 fine-tuning 하는 방식은 놀라울 정도로 효과적이었고, 이후 LLM 모델들의 기본 학습 방식이 되었다.
다만 OpenAI에서는 GPT-1의 여러 한계점을 제시한다.
1) 매우 높은 연산량
2) 한정된 텍스트로 인한 한계와 편향
3) 일반화 취약성
이제 모델 구조 자체보단 데이터셋과 학습 방식에서 성능 향상에 대한 해답을 찾아내야 한다. 이후 발표된 GPT-2, GPT-3, instruct GPT 등은 모두 모델 구조 자체의 큰 변화보단 더 효율적인 학습 방식, 적합한 데이터셋을 찾는데에 주목하였다. 앞으로도 이러한 기조가 유지되지 않을까 생각한다.
Radford, et al. "Improving Language Understanding by Generative Pre-Training". 2018
[18′ OpenAI] GPT-1 : Improving Language Understanding by Generative Pre-Training
[논문리뷰] GPT-1(Improving Language Understandingby Generative Pre-Training)의 이해
lyeoni 님의 깃허브 코드에서 많은 도움을 받았습니다.