
[논문리뷰] GPT-1(Improving Language Understandingby Generative Pre-Training)의 이해
GPT-1 논문 리뷰 - ChatGPT의 근간이 되는 논문 완벽하게 이해하기
[GPT-1 논문 리뷰] - Improving Language Understanding by Generative Pre-Training
자연어 처리(NLP) 분야는 최근 수년 동안 빠르게 발전하고 있으며, 여러 작업에서 최첨단 성능을 달성해왔다. 그러나 대부분의 성공은 라벨링된 데이터에 의존한 지도 학습(Supervised Learning)에서 비롯된 것이다.
라벨링된 데이터는 특정 작업에 맞춰 사람이 직접 데이터를 라벨링해야 하기 때문에 비용이 많이 들고, 양이 제한적이다. 반면, Unlabeled 데이터(Unlabeled Data)는 훨씬 더 풍부하게 존재한다. 문제는 이러한 Unlabeled 데이터를 효과적으로 활용하는 방법이 충분히 연구되지 않았다는 점이다.
본 논문에서는 Unlabeled 데이터를 기반으로 사전 학습(Pre-Training)을 진행하고, 이후 라벨링된 데이터로 미세 조정(Fine-Tuning)을 수행하는 Generative Pre-Training (GPT) 방식을 제안한다. 이러한 방식은 다양한 자연어 처리 작업에서 우수한 성능을 보여주었으며, 이는 Transformer 구조를 기반으로 한 사전 훈련 언어 모델이 다양한 작업에 강력하게 적용될 수 있음을 입증했다. 이 논문은 NLP 분야의 사전 학습과 전이 학습의 가능성을 열었으며, 이후 발전된 GPT 시리즈의 토대를 마련했다.
지도 학습 : 라벨링된 데이터를 사용하여 특정 작업에 최적화된 모델을 학습하지만, 이 방식은 라벨링된 데이터가 충분히 확보되지 않으면 성능에 한계가 있음.
비지도 학습: 라벨링된 데이터 없이 패턴을 학습하는 방식이지만, 이 방식은 특정 작업에 적합한 성능을 발휘하기 어려움. 사전 학습된 모델을 사용하여 새로운 작업에 적용하려 할 때, 작업별로 최적화 목표를 다르게 설정해야 하며, 전이 학습(Transfer Learning) 과정에서 효과적으로 성능을 끌어올리기 어려운 문제점이 존재.
→ Generative Pre-Training (GPT) 방식의 언어 모델을 제안하고, 다양한 자연어 처리 작업에서 높은 성능을 달성할 수 있는 방법론을 탐구.
: GPT-1의 모델 구조는 Transformer 구조에서 디코더(Decoder) 부분만을 사용.
→ 문장의 일부분을 입력으로 주었을 때 다음 단어를 예측하는 방식으로 학습.
🤔Why Transformer ??
Transformer는 자연어 처리 작업에서 병렬화와 장기 의존성(Long-Term Dependency) 문제를 해결하는 데 매우 유리한 구조Self-Attention Mechanism을 통해 문맥의 긴 종속성을 잘 처리.Transformer구조는 Self-Attention를 통해 문장 내 모든 단어 간의 관계를 효율적으로 학습할 수 있음.(기존의 RNN(Recurrent Neural Network)이나 LSTM(Long Short-Term Memory) 구조는 단어 간의 긴 종속성을 잘 학습하지 못하는 한계가 있었음.)Unlabeled 데이터를 기반으로 한 언어 모델링 과정 : 모델이 주어진 문맥에서 다음 단어를 예측하도록 학습.
→ 모델이 언어의 구조와 패턴을 학습하는 데 효과적.(자연어 생성 작업에서도 매우 유용한 방법)
🚨언어 모델링의 최적화 목표 수식 ????
)
pre-trained 에 사용된 데이터: BooksCorpus
BookCorpus??
: 약 7,000권의 미출간된 책들로 구성되어 있으며, 긴 문맥(Long-Range Context)을 포함한 텍스트 데이터를 제공하여 언어 모델 학습에 적합한 데이터셋으로 평가받고 있다.

: 사전 훈련된 모델을 특정 작업(Task)에 맞게 라벨링된 데이터로 미세 조정.
: 작업별 라벨링된 데이터를 활용하여 모델이 특정 작업의 목표에 맞게 성능을 최적화하는 과정
이 단계에서는 모델 구조의 변경은 최소화된다. GPT-1 모델은 사전 훈련된 가중치와 파라미터를 유지한 상태에서, Task-Specific Input Transformations만을 적용하여 다양한 작업에 맞게 미세 조정을 진행.
Fine-Tuning 단계의 최적화 목표는 작업별로 라벨링된 데이터를 기반으로 이루어짐
🚨각 시퀀스에 대해 라벨 를 예측하는 확률 수식
Softmax는 모델의 출력값을 확률 값으로 변환하는 역할을 하며, 이를 통해 모델이 특정 작업의 라벨 예측을 수행할 수 있다.

GPT-1은 간단한 입력 변환 방식을 통해 다양한 자연어 처리 작업에 적용될 수 있도록 설계.
이를 통해, 작업별로 복잡한 모델 구조를 변경하지 않고도 여러 작업에 대해 효과적인 성능을 발휘할 수 있음.
이 섹션에서는 각 작업에 대해 입력 변환 방식을 설명.
: 텍스트 분류는 입력된 텍스트를 기반으로 사전 정의된 카테고리 중 하나를 예측하는 작업.
ex) 스팸 메일 분류와 같은 문제를 생각할 수 있다. GPT-1에서의 입력 변환은 매우 간단한다. 전체 문장을 그대로 모델에 입력하며, 출력은 해당 문장의 카테고리를 예측하는 확률 값을 산출한다.
텍스트 추론은 두 문장 간의 관계를 예측하는 작업이다. 두 문장이 논리적으로 함축(Entailment)하는지, 모순(Contradiction)되는지, 아니면 중립(Neutral)인지를 분류하는 문제이다.
GPT-1에서는 두 문장을 연결해 입력으로 사용한다.
: 유사성 평가 작업에서는 두 문장이 얼마나 비슷한지(Similarity)를 예측하는 문제이다. 두 문장이 같은 의미를 담고 있는지, 또는 얼마나 유사한지 평가하는 작업이다.
GPT-1에서는 두 문장을 개별적으로 입력한 후, 각 문장의 표현 벡터를 추출한다. 이후 element-wise로 합산하여 두 문장 간의 유사도를 계산하게 됩니다. 이 계산 결과를 바탕으로 유사성 점수를 예측한다.
: 질문 응답은 지문(Passage)과 질문(Question)이 주어졌을 때, 지문 내에서 정답(Answer)을 찾는 작업이다.
GPT-1에서는 지문과 질문을 입력으로 사용하며, 지문, 질문, 그리고 가능한 정답 리스트가 함께 모델에 주어집니다. 모델은 Softmax 함수를 통해 가장 가능성이 높은 정답을 선택하게 됩니다.
다중 선택 문제는 질문에 대한 여러 선택지 중에서 올바른 답을 선택하는 작업이다. GPT-1에서는 지문과 선택지를 결합하여 입력 시퀀스로 변환한다. 예를 들어, "서울은 대한민국의 수도이다. 이 도시는 어디입니까?"라는 질문이 주어진 경우, "서울", "부산", "대구" 등의 선택지를 각기 다른 입력 시퀀스로 처리한 후, 모델이 가장 적합한 답을 고를 수 있도록 한다.
본 논문에서는 다양한 자연어 처리 작업에서 GPT-1의 성능을 평가하기 위해 실험을 진행하였다. 사전 훈련을 거친 모델은 12개의 NLP 작업에 대해 미세 조정(Fine-Tuning)하여 성능을 측정하였으며, 여기에는 텍스트 분류, 질문 응답, 자연어 추론 등의 작업이 포함됩니다.
사전 훈련에 사용된 데이터셋은 BooksCorpus로, 약 7,000권의 미출간 책들로 구성된 대규모 텍스트 데이터셋이다. 이 데이터셋은 긴 문맥(long-range context)을 포함하고 있어, 언어 모델이 긴 문맥에서의 종속성(Long-Term Dependency)을 학습하는 데 적합한다.
GPT-1은 12개의 Transformer 디코더 레이어로 구성된 구조를 가지고 있다. 각 레이어에는 12개의 자기-주의 헤드(Self-Attention Heads)가 있으며, 768차원의 임베딩 벡터(Embedding Vector)로 모델을 학습한다. 학습은 Adam 최적화 기법을 사용해 진행되었다.
사전 훈련된 모델은 각 작업에 대해 라벨링된 데이터로 미세 조정을 진행한다. 미세 조정 단계에서는 사전 학습된 가중치를 유지하면서 각 작업에 필요한 하이퍼파라미터(Hyperparameter)를 조정한다. Fine-Tuning 단계에서는 보조 학습 목표(Auxiliary Objective)를 추가로 사용하여 모델의 일반화 성능을 향상시키고, 수렴 속도를 높였다.
실험 결과, GPT-1은 다양한 NLP 작업에서 기존 방법들을 뛰어넘는 성능을 달성하였다. 주요 작업별 성능은 다음과 같다:
실험 결과를 바탕으로 GPT-1의 성능을 심층 분석하여 전이 학습(Transfer Learning)과 제로샷 학습(Zero-Shot Learning)에서의 효율성을 평가.
사전 훈련된 모델에서 여러 레이어(layer)를 전이시켜 학습한 결과, 전이되는 레이어의 수가 많을수록 성능이 향상된다는 점이 확인되었다. 특히, 9개의 레이어를 전이한 모델에서 최대 성능이 관찰되었다. 이는 사전 훈련된 모델의 다층 표현(Deep Representation)이 언어적 정보와 의미를 충분히 포함하고 있음을 의미하며, 이를 활용해 다운스트림 작업(Downstream Task)에서도 높은 성능을 발휘할 수 있음을 보여준다.
본 연구에서는 Ablation Study(소거 실험)를 통해 모델의 구성 요소들이 작업 성능에 미치는 영향을 분석하였다. 주요 실험 결과는 다음과 같다:
GPT-1은 언어 모델링(Language Modeling)의 새로운 접근 방식을 제안하며, 사전 학습(Pre-Training)과 미세 조정(Fine-Tuning)을 결합한 방법론이 자연어 처리 작업에서 강력한 성능을 발휘할 수 있음을 입증했다. 이 연구는 NLP 연구의 패러다임을 변화시키는 중요한 연구로 평가받고 있다.
GPT-1은 Unlabeled 데이터(Unlabeled Data)를 활용하여 사전 학습을 진행한 후, 라벨링된 데이터(Labeled Data)를 통해 미세 조정하는 방식으로 지도 학습의 성능을 극대화할 수 있음을 증명했다. 이는 라벨링된 데이터의 부족 문제를 해결하는 데 매우 유용한 방법이며, Unlabeled 데이터의 잠재력을 자연어 처리에서 효과적으로 활용할 수 있음을 시사한다.
GPT-1은 Transformer 구조 중 디코더(Decoder) 부분을 사용하였으며, 자기-주의 메커니즘(Self-Attention Mechanism)을 통해 기존의 RNN 및 LSTM 기반 모델들보다 더 뛰어난 성능을 발휘했다. 특히, Transformer는 장기 의존성(Long-Term Dependency) 문제를 해결할 수 있는 장점을 가지고 있으며, 이를 통해 자연어 처리 작업에서 더 깊은 문맥적 이해를 가능하게 했다.
GPT-1은 사전 훈련(Pre-Training)과 미세 조정(Fine-Tuning)을 결합하여 전이 학습(Transfer Learning)의 효율성을 극대화하였다. 이는 이후 연구에서 사전 학습된 언어 모델(Pretrained Language Models)이 다양한 작업에서 재사용 가능한 강력한 도구로 자리 잡는 데 기여하였다. 특히 GPT-1은 이후 발표된 GPT-2, GPT-3, 그리고 InstructGPT 등의 발전된 모델들의 기초를 제공했으며, 대규모 언어 모델의 중요성을 강조했다.
GPT-1은 생성적 사전 훈련(Generative Pre-Training)과 지도 학습 미세 조정(Supervised Fine-Tuning)을 결합한 NLP 모델로, Unlabeled 데이터를 효율적으로 사용하여 다양한 자연어 처리 작업에서 최첨단 성과(SOTA)를 달성했다. GPT-1은 Transformer 구조를 기반으로 하여 장기 문맥 처리 능력을 극대화하며, 전이 학습의 효율성을 크게 향상시켰습니다.
본 연구는 자연어 처리에서 Unlabeled 데이터를 효과적으로 활용할 수 있는 방법론을 제안했으며, 사전 훈련된 언어 모델이 어떻게 다양한 작업에서 높은 성능을 발휘할 수 있는지를 입증했다. 특히 GPT-1은 Transformer 구조와 전이 학습을 결합하여 자연어 처리 연구의 새로운 방향을 제시했으며, 이후 발전된 GPT 시리즈의 토대를 마련했다.
GPT-1의 연구는 이후 언어 모델 연구에 큰 영향을 미쳤으며, GPT-2, GPT-3, InstructGPT 등의 발전된 모델들이 개발되었다. 이들 모델은 GPT-1의 기본적인 아이디어를 발전시켜 Few-Shot 학습, Zero-Shot 학습, 대규모 모델 등의 새로운 연구 분야를 개척했다. GPT-1은 자연어 처리에서 비지도 학습의 잠재력을 입증한 중요한 연구로 자리 잡았으며, 앞으로도 전이 학습과 대규모 사전 학습된 모델에 대한 연구는 지속될 것이다.
https://github.com/openai/finetune-transformer-lm/blob/master/train.py
train.py
import os
import time
import math
import json
import joblib
import random
import argparse
import numpy as np
import tensorflow as tf
from tqdm import tqdm
from functools import partial
from sklearn.utils import shuffle
from sklearn.metrics import accuracy_score
from opt import adam, warmup_cosine, warmup_linear, warmup_constant
from datasets import rocstories
from analysis import rocstories as rocstories_analysis
from text_utils import TextEncoder
from utils import encode_dataset, flatten, iter_data, find_trainable_variables, convert_gradient_to_tensor, shape_list, ResultLogger, assign_to_gpu, average_grads, make_path
def gelu(x):
return 0.5*x*(1+tf.tanh(math.sqrt(2/math.pi)*(x+0.044715*tf.pow(x, 3))))
def swish(x):
return x*tf.nn.sigmoid(x)
opt_fns = {
'adam':adam,
}
act_fns = {
'relu':tf.nn.relu,
'swish':swish,
'gelu':gelu
}
lr_schedules = {
'warmup_cosine':warmup_cosine,
'warmup_linear':warmup_linear,
'warmup_constant':warmup_constant,
}
def _norm(x, g=None, b=None, e=1e-5, axis=[1]):
u = tf.reduce_mean(x, axis=axis, keep_dims=True)
s = tf.reduce_mean(tf.square(x-u), axis=axis, keep_dims=True)
x = (x - u) * tf.rsqrt(s + e)
if g is not None and b is not None:
x = x*g + b
return x
def norm(x, scope, axis=[-1]):
with tf.variable_scope(scope):
n_state = shape_list(x)[-1]
g = tf.get_variable("g", [n_state], initializer=tf.constant_initializer(1))
b = tf.get_variable("b", [n_state], initializer=tf.constant_initializer(0))
return _norm(x, g, b, axis=axis)
def dropout(x, pdrop, train):
if train and pdrop > 0:
x = tf.nn.dropout(x, 1-pdrop)
return x
def mask_attn_weights(w):
n = shape_list(w)[-1]
b = tf.matrix_band_part(tf.ones([n, n]), -1, 0)
b = tf.reshape(b, [1, 1, n, n])
w = w*b + -1e9*(1-b)
return w
def _attn(q, k, v, train=False, scale=False):
w = tf.matmul(q, k)
if scale:
n_state = shape_list(v)[-1]
w = w*tf.rsqrt(tf.cast(n_state, tf.float32))
w = mask_attn_weights(w)
w = tf.nn.softmax(w)
w = dropout(w, attn_pdrop, train)
a = tf.matmul(w, v)
return a
def split_states(x, n):
x_shape = shape_list(x)
m = x_shape[-1]
new_x_shape = x_shape[:-1]+[n, m//n]
return tf.reshape(x, new_x_shape)
def merge_states(x):
x_shape = shape_list(x)
new_x_shape = x_shape[:-2]+[np.prod(x_shape[-2:])]
return tf.reshape(x, new_x_shape)
def split_heads(x, n, k=False):
if k:
return tf.transpose(split_states(x, n), [0, 2, 3, 1])
else:
return tf.transpose(split_states(x, n), [0, 2, 1, 3])
def merge_heads(x):
return merge_states(tf.transpose(x, [0, 2, 1, 3]))
def conv1d(x, scope, nf, rf, w_init=tf.random_normal_initializer(stddev=0.02), b_init=tf.constant_initializer(0), pad='VALID', train=False):
with tf.variable_scope(scope):
nx = shape_list(x)[-1]
w = tf.get_variable("w", [rf, nx, nf], initializer=w_init)
b = tf.get_variable("b", [nf], initializer=b_init)
if rf == 1: #faster 1x1 conv
c = tf.reshape(tf.matmul(tf.reshape(x, [-1, nx]), tf.reshape(w, [-1, nf]))+b, shape_list(x)[:-1]+[nf])
else: #was used to train LM
c = tf.nn.conv1d(x, w, stride=1, padding=pad)+b
return c
def attn(x, scope, n_state, n_head, train=False, scale=False):
assert n_state%n_head==0
with tf.variable_scope(scope):
c = conv1d(x, 'c_attn', n_state*3, 1, train=train)
q, k, v = tf.split(c, 3, 2)
q = split_heads(q, n_head)
k = split_heads(k, n_head, k=True)
v = split_heads(v, n_head)
a = _attn(q, k, v, train=train, scale=scale)
a = merge_heads(a)
a = conv1d(a, 'c_proj', n_state, 1, train=train)
a = dropout(a, resid_pdrop, train)
return a
def mlp(x, scope, n_state, train=False):
with tf.variable_scope(scope):
nx = shape_list(x)[-1]
act = act_fns[afn]
h = act(conv1d(x, 'c_fc', n_state, 1, train=train))
h2 = conv1d(h, 'c_proj', nx, 1, train=train)
h2 = dropout(h2, resid_pdrop, train)
return h2
def block(x, scope, train=False, scale=False):
with tf.variable_scope(scope):
nx = shape_list(x)[-1]
a = attn(x, 'attn', nx, n_head, train=train, scale=scale)
n = norm(x+a, 'ln_1')
m = mlp(n, 'mlp', nx*4, train=train)
h = norm(n+m, 'ln_2')
return h
def embed(X, we):
we = convert_gradient_to_tensor(we)
e = tf.gather(we, X)
h = tf.reduce_sum(e, 2)
return h
def clf(x, ny, w_init=tf.random_normal_initializer(stddev=0.02), b_init=tf.constant_initializer(0), train=False):
with tf.variable_scope('clf'):
nx = shape_list(x)[-1]
w = tf.get_variable("w", [nx, ny], initializer=w_init)
b = tf.get_variable("b", [ny], initializer=b_init)
return tf.matmul(x, w)+b
def model(X, M, Y, train=False, reuse=False):
with tf.variable_scope('model', reuse=reuse):
we = tf.get_variable("we", [n_vocab+n_special+n_ctx, n_embd], initializer=tf.random_normal_initializer(stddev=0.02))
we = dropout(we, embd_pdrop, train)
X = tf.reshape(X, [-1, n_ctx, 2])
M = tf.reshape(M, [-1, n_ctx])
h = embed(X, we)
for layer in range(n_layer):
h = block(h, 'h%d'%layer, train=train, scale=True)
lm_h = tf.reshape(h[:, :-1], [-1, n_embd])
lm_logits = tf.matmul(lm_h, we, transpose_b=True)
lm_losses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=lm_logits, labels=tf.reshape(X[:, 1:, 0], [-1]))
lm_losses = tf.reshape(lm_losses, [shape_list(X)[0], shape_list(X)[1]-1])
lm_losses = tf.reduce_sum(lm_losses*M[:, 1:], 1)/tf.reduce_sum(M[:, 1:], 1)
clf_h = tf.reshape(h, [-1, n_embd])
pool_idx = tf.cast(tf.argmax(tf.cast(tf.equal(X[:, :, 0], clf_token), tf.float32), 1), tf.int32)
clf_h = tf.gather(clf_h, tf.range(shape_list(X)[0], dtype=tf.int32)*n_ctx+pool_idx)
clf_h = tf.reshape(clf_h, [-1, 2, n_embd])
if train and clf_pdrop > 0:
shape = shape_list(clf_h)
shape[1] = 1
clf_h = tf.nn.dropout(clf_h, 1-clf_pdrop, shape)
clf_h = tf.reshape(clf_h, [-1, n_embd])
clf_logits = clf(clf_h, 1, train=train)
clf_logits = tf.reshape(clf_logits, [-1, 2])
clf_losses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=clf_logits, labels=Y)
return clf_logits, clf_losses, lm_losses
def mgpu_train(*xs):
gpu_ops = []
gpu_grads = []
xs = (tf.split(x, n_gpu, 0) for x in xs)
for i, xs in enumerate(zip(*xs)):
do_reuse = True if i > 0 else None
with tf.device(assign_to_gpu(i, "/gpu:0")), tf.variable_scope(tf.get_variable_scope(), reuse=do_reuse):
clf_logits, clf_losses, lm_losses = model(*xs, train=True, reuse=do_reuse)
if lm_coef > 0:
train_loss = tf.reduce_mean(clf_losses) + lm_coef*tf.reduce_mean(lm_losses)
else:
train_loss = tf.reduce_mean(clf_losses)
params = find_trainable_variables("model")
grads = tf.gradients(train_loss, params)
grads = list(zip(grads, params))
gpu_grads.append(grads)
gpu_ops.append([clf_logits, clf_losses, lm_losses])
ops = [tf.concat(op, 0) for op in zip(*gpu_ops)]
grads = average_grads(gpu_grads)
grads = [g for g, p in grads]
train = opt_fns[opt](params, grads, lr, partial(lr_schedules[lr_schedule], warmup=lr_warmup), n_updates_total, l2=l2, max_grad_norm=max_grad_norm, vector_l2=vector_l2, b1=b1, b2=b2, e=e)
return [train]+ops
def mgpu_predict(*xs):
gpu_ops = []
xs = (tf.split(x, n_gpu, 0) for x in xs)
for i, xs in enumerate(zip(*xs)):
with tf.device(assign_to_gpu(i, "/gpu:0")), tf.variable_scope(tf.get_variable_scope(), reuse=True):
clf_logits, clf_losses, lm_losses = model(*xs, train=False, reuse=True)
gpu_ops.append([clf_logits, clf_losses, lm_losses])
ops = [tf.concat(op, 0) for op in zip(*gpu_ops)]
return ops
def transform_roc(X1, X2, X3):
n_batch = len(X1)
xmb = np.zeros((n_batch, 2, n_ctx, 2), dtype=np.int32)
mmb = np.zeros((n_batch, 2, n_ctx), dtype=np.float32)
start = encoder['_start_']
delimiter = encoder['_delimiter_']
for i, (x1, x2, x3), in enumerate(zip(X1, X2, X3)):
x12 = [start]+x1[:max_len]+[delimiter]+x2[:max_len]+[clf_token]
x13 = [start]+x1[:max_len]+[delimiter]+x3[:max_len]+[clf_token]
l12 = len(x12)
l13 = len(x13)
xmb[i, 0, :l12, 0] = x12
xmb[i, 1, :l13, 0] = x13
mmb[i, 0, :l12] = 1
mmb[i, 1, :l13] = 1
xmb[:, :, :, 1] = np.arange(n_vocab+n_special, n_vocab+n_special+n_ctx)
return xmb, mmb
def iter_apply(Xs, Ms, Ys):
fns = [lambda x:np.concatenate(x, 0), lambda x:float(np.sum(x))]
results = []
for xmb, mmb, ymb in iter_data(Xs, Ms, Ys, n_batch=n_batch_train, truncate=False, verbose=True):
n = len(xmb)
if n == n_batch_train:
res = sess.run([eval_mgpu_logits, eval_mgpu_clf_loss], {X_train:xmb, M_train:mmb, Y_train:ymb})
else:
res = sess.run([eval_logits, eval_clf_loss], {X:xmb, M:mmb, Y:ymb})
res = [r*n for r in res]
results.append(res)
results = zip(*results)
return [fn(res) for res, fn in zip(results, fns)]
def iter_predict(Xs, Ms):
logits = []
for xmb, mmb in iter_data(Xs, Ms, n_batch=n_batch_train, truncate=False, verbose=True):
n = len(xmb)
if n == n_batch_train:
logits.append(sess.run(eval_mgpu_logits, {X_train:xmb, M_train:mmb}))
else:
logits.append(sess.run(eval_logits, {X:xmb, M:mmb}))
logits = np.concatenate(logits, 0)
return logits
def save(path):
ps = sess.run(params)
joblib.dump(ps, make_path(path))
def log():
global best_score
tr_logits, tr_cost = iter_apply(trX[:n_valid], trM[:n_valid], trY[:n_valid])
va_logits, va_cost = iter_apply(vaX, vaM, vaY)
tr_cost = tr_cost/len(trY[:n_valid])
va_cost = va_cost/n_valid
tr_acc = accuracy_score(trY[:n_valid], np.argmax(tr_logits, 1))*100.
va_acc = accuracy_score(vaY, np.argmax(va_logits, 1))*100.
logger.log(n_epochs=n_epochs, n_updates=n_updates, tr_cost=tr_cost, va_cost=va_cost, tr_acc=tr_acc, va_acc=va_acc)
print('%d %d %.3f %.3f %.2f %.2f'%(n_epochs, n_updates, tr_cost, va_cost, tr_acc, va_acc))
if submit:
score = va_acc
if score > best_score:
best_score = score
save(os.path.join(save_dir, desc, 'best_params.jl'))
argmax = lambda x:np.argmax(x, 1)
pred_fns = {
'rocstories':argmax,
}
filenames = {
'rocstories':'ROCStories.tsv',
}
label_decoders = {
'rocstories':None,
}
def predict():
filename = filenames[dataset]
pred_fn = pred_fns[dataset]
label_decoder = label_decoders[dataset]
predictions = pred_fn(iter_predict(teX, teM))
if label_decoder is not None:
predictions = [label_decoder[prediction] for prediction in predictions]
path = os.path.join(submission_dir, filename)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w') as f:
f.write('{}\t{}\n'.format('index', 'prediction'))
for i, prediction in enumerate(predictions):
f.write('{}\t{}\n'.format(i, prediction))
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--desc', type=str)
parser.add_argument('--dataset', type=str)
parser.add_argument('--log_dir', type=str, default='log/')
parser.add_argument('--save_dir', type=str, default='save/')
parser.add_argument('--data_dir', type=str, default='data/')
parser.add_argument('--submission_dir', type=str, default='submission/')
parser.add_argument('--submit', action='store_true')
parser.add_argument('--analysis', action='store_true')
parser.add_argument('--seed', type=int, default=42)
parser.add_argument('--n_iter', type=int, default=3)
parser.add_argument('--n_batch', type=int, default=8)
parser.add_argument('--max_grad_norm', type=int, default=1)
parser.add_argument('--lr', type=float, default=6.25e-5)
parser.add_argument('--lr_warmup', type=float, default=0.002)
parser.add_argument('--n_ctx', type=int, default=512)
parser.add_argument('--n_embd', type=int, default=768)
parser.add_argument('--n_head', type=int, default=12)
parser.add_argument('--n_layer', type=int, default=12)
parser.add_argument('--embd_pdrop', type=float, default=0.1)
parser.add_argument('--attn_pdrop', type=float, default=0.1)
parser.add_argument('--resid_pdrop', type=float, default=0.1)
parser.add_argument('--clf_pdrop', type=float, default=0.1)
parser.add_argument('--l2', type=float, default=0.01)
parser.add_argument('--vector_l2', action='store_true')
parser.add_argument('--n_gpu', type=int, default=4)
parser.add_argument('--opt', type=str, default='adam')
parser.add_argument('--afn', type=str, default='gelu')
parser.add_argument('--lr_schedule', type=str, default='warmup_linear')
parser.add_argument('--encoder_path', type=str, default='model/encoder_bpe_40000.json')
parser.add_argument('--bpe_path', type=str, default='model/vocab_40000.bpe')
parser.add_argument('--n_transfer', type=int, default=12)
parser.add_argument('--lm_coef', type=float, default=0.5)
parser.add_argument('--b1', type=float, default=0.9)
parser.add_argument('--b2', type=float, default=0.999)
parser.add_argument('--e', type=float, default=1e-8)
args = parser.parse_args()
print(args)
globals().update(args.__dict__)
random.seed(seed)
np.random.seed(seed)
tf.set_random_seed(seed)
logger = ResultLogger(path=os.path.join(log_dir, '{}.jsonl'.format(desc)), **args.__dict__)
text_encoder = TextEncoder(encoder_path, bpe_path)
encoder = text_encoder.encoder
n_vocab = len(text_encoder.encoder)
(trX1, trX2, trX3, trY), (vaX1, vaX2, vaX3, vaY), (teX1, teX2, teX3) = encode_dataset(rocstories(data_dir), encoder=text_encoder)
n_y = 2
encoder['_start_'] = len(encoder)
encoder['_delimiter_'] = len(encoder)
encoder['_classify_'] = len(encoder)
clf_token = encoder['_classify_']
n_special = 3
max_len = n_ctx//2-2
n_ctx = min(max([len(x1[:max_len])+max(len(x2[:max_len]), len(x3[:max_len])) for x1, x2, x3 in zip(trX1, trX2, trX3)]+[len(x1[:max_len])+max(len(x2[:max_len]), len(x3[:max_len])) for x1, x2, x3 in zip(vaX1, vaX2, vaX3)]+[len(x1[:max_len])+max(len(x2[:max_len]), len(x3[:max_len])) for x1, x2, x3 in zip(teX1, teX2, teX3)])+3, n_ctx)
trX, trM = transform_roc(trX1, trX2, trX3)
vaX, vaM = transform_roc(vaX1, vaX2, vaX3)
if submit:
teX, teM = transform_roc(teX1, teX2, teX3)
n_train = len(trY)
n_valid = len(vaY)
n_batch_train = n_batch*n_gpu
n_updates_total = (n_train//n_batch_train)*n_iter
X_train = tf.placeholder(tf.int32, [n_batch_train, 2, n_ctx, 2])
M_train = tf.placeholder(tf.float32, [n_batch_train, 2, n_ctx])
X = tf.placeholder(tf.int32, [None, 2, n_ctx, 2])
M = tf.placeholder(tf.float32, [None, 2, n_ctx])
Y_train = tf.placeholder(tf.int32, [n_batch_train])
Y = tf.placeholder(tf.int32, [None])
train, logits, clf_losses, lm_losses = mgpu_train(X_train, M_train, Y_train)
clf_loss = tf.reduce_mean(clf_losses)
params = find_trainable_variables('model')
sess = tf.Session(config=tf.ConfigProto(allow_soft_placement=True))
sess.run(tf.global_variables_initializer())
shapes = json.load(open('model/params_shapes.json'))
offsets = np.cumsum([np.prod(shape) for shape in shapes])
init_params = [np.load('model/params_{}.npy'.format(n)) for n in range(10)]
init_params = np.split(np.concatenate(init_params, 0), offsets)[:-1]
init_params = [param.reshape(shape) for param, shape in zip(init_params, shapes)]
init_params[0] = init_params[0][:n_ctx]
init_params[0] = np.concatenate([init_params[1], (np.random.randn(n_special, n_embd)*0.02).astype(np.float32), init_params[0]], 0)
del init_params[1]
if n_transfer == -1:
n_transfer = 0
else:
n_transfer = 1+n_transfer*12
sess.run([p.assign(ip) for p, ip in zip(params[:n_transfer], init_params[:n_transfer])])
eval_mgpu_logits, eval_mgpu_clf_losses, eval_mgpu_lm_losses = mgpu_predict(X_train, M_train, Y_train)
eval_logits, eval_clf_losses, eval_lm_losses = model(X, M, Y, train=False, reuse=True)
eval_clf_loss = tf.reduce_mean(eval_clf_losses)
eval_mgpu_clf_loss = tf.reduce_mean(eval_mgpu_clf_losses)
n_updates = 0
n_epochs = 0
if dataset != 'stsb':
trYt = trY
if submit:
save(os.path.join(save_dir, desc, 'best_params.jl'))
best_score = 0
for i in range(n_iter):
for xmb, mmb, ymb in iter_data(*shuffle(trX, trM, trYt, random_state=np.random), n_batch=n_batch_train, truncate=True, verbose=True):
cost, _ = sess.run([clf_loss, train], {X_train:xmb, M_train:mmb, Y_train:ymb})
n_updates += 1
if n_updates in [1000, 2000, 4000, 8000, 16000, 32000] and n_epochs == 0:
log()
n_epochs += 1
log()
if submit:
sess.run([p.assign(ip) for p, ip in zip(params, joblib.load(os.path.join(save_dir, desc, 'best_params.jl')))])
predict()
if analysis:
rocstories_analysis(data_dir, os.path.join(submission_dir, 'ROCStories.tsv'), os.path.join(log_dir, 'rocstories.jsonl'))
dataset.py
import os
import csv
import numpy as np
from tqdm import tqdm
from sklearn.utils import shuffle
from sklearn.model_selection import train_test_split
seed = 3535999445
def _rocstories(path):
with open(path) as f:
f = csv.reader(f)
st = []
ct1 = []
ct2 = []
y = []
for i, line in enumerate(tqdm(list(f), ncols=80, leave=False)):
if i > 0:
s = ' '.join(line[1:5])
c1 = line[5]
c2 = line[6]
st.append(s)
ct1.append(c1)
ct2.append(c2)
y.append(int(line[-1])-1)
return st, ct1, ct2, y
def rocstories(data_dir, n_train=1497, n_valid=374):
storys, comps1, comps2, ys = _rocstories(os.path.join(data_dir, 'cloze_test_val__spring2016 - cloze_test_ALL_val.csv'))
teX1, teX2, teX3, _ = _rocstories(os.path.join(data_dir, 'cloze_test_test__spring2016 - cloze_test_ALL_test.csv'))
tr_storys, va_storys, tr_comps1, va_comps1, tr_comps2, va_comps2, tr_ys, va_ys = train_test_split(storys, comps1, comps2, ys, test_size=n_valid, random_state=seed)
trX1, trX2, trX3 = [], [], []
trY = []
for s, c1, c2, y in zip(tr_storys, tr_comps1, tr_comps2, tr_ys):
trX1.append(s)
trX2.append(c1)
trX3.append(c2)
trY.append(y)
vaX1, vaX2, vaX3 = [], [], []
vaY = []
for s, c1, c2, y in zip(va_storys, va_comps1, va_comps2, va_ys):
vaX1.append(s)
vaX2.append(c1)
vaX3.append(c2)
vaY.append(y)
trY = np.asarray(trY, dtype=np.int32)
vaY = np.asarray(vaY, dtype=np.int32)
return (trX1, trX2, trX3, trY), (vaX1, vaX2, vaX3, vaY), (teX1, teX2, teX3)
opt.py
import math
import numpy as np
import tensorflow as tf
def warmup_cosine(x, warmup=0.002):
s = tf.cast(x <= warmup, tf.float32)
return s*(x/warmup) + (1-s)*(0.5 * (1 + tf.cos(math.pi * x)))
def warmup_constant(x, warmup=0.002):
s = tf.cast(x <= warmup, tf.float32)
return s*(x/warmup) + (1-s)*1
def warmup_linear(x, warmup=0.002):
s = tf.cast(x <= warmup, tf.float32)
return (s*(x/warmup) + (1-s))*(1-x)
schedules = {
'warmup_cosine':warmup_cosine,
'warmup_constant':warmup_constant,
'warmup_linear':warmup_linear,
}
def adam(params, grads, lr, schedule, t_total, b1=0.9, b2=0.999, e=1e-8, l2=0, vector_l2=False, max_grad_norm=-1, **kwargs):
"""
adam with weight decay fix
"""
t = tf.Variable(0, dtype=tf.float32, trainable=False)
tt = t+1
updates = [t.assign(tt)]
if max_grad_norm > 0:
grads, _ = tf.clip_by_global_norm(grads, max_grad_norm)
for p, g in zip(params, grads):
if p is None or g is None:
print("can't train", p.name, g)
else:
if isinstance(g, tf.IndexedSlices):
g = tf.convert_to_tensor(g)
m = tf.Variable(p*0, dtype=tf.float32, trainable=False)
v = tf.Variable(p*0, dtype=tf.float32, trainable=False)
lrt = lr*tf.sqrt(1-b2**tt)/(1-b1**tt)
lrt *= schedule(t/t_total)
mt = b1*m + (1-b1)*g
vt = b2*v + (1-b2)*g*g
if (len(p.get_shape()) > 1 or vector_l2) and l2 > 0:
pt = p - lrt * (mt / (tf.sqrt(vt) + e) + l2*p)
else:
pt = p - lrt * (mt / (tf.sqrt(vt) + e))
updates.extend([m.assign(mt), v.assign(vt), p.assign(pt)])
return tf.group(*updates)
text_utils.py
import re
import ftfy
import json
import spacy
from tqdm import tqdm
def get_pairs(word):
"""
Return set of symbol pairs in a word.
word is represented as tuple of symbols (symbols being variable-length strings)
"""
pairs = set()
prev_char = word[0]
for char in word[1:]:
pairs.add((prev_char, char))
prev_char = char
return pairs
def text_standardize(text):
"""
fixes some issues the spacy tokenizer had on books corpus
also does some whitespace standardization
"""
text = text.replace('—', '-')
text = text.replace('–', '-')
text = text.replace('―', '-')
text = text.replace('…', '...')
text = text.replace('´', "'")
text = re.sub('''(-+|~+|!+|"+|;+|\?+|\++|,+|\)+|\(+|\\+|\/+|\*+|\[+|\]+|}+|{+|\|+|_+)''', r' \1 ', text)
text = re.sub('\s*\n\s*', ' \n ', text)
text = re.sub('[^\S\n]+', ' ', text)
return text.strip()
class TextEncoder(object):
"""
mostly a wrapper for a public python bpe tokenizer
"""
def __init__(self, encoder_path, bpe_path):
self.nlp = spacy.load('en', disable=['parser', 'tagger', 'ner', 'textcat'])
self.encoder = json.load(open(encoder_path))
self.decoder = {v:k for k,v in self.encoder.items()}
merges = open(bpe_path).read().split('\n')[1:-1]
merges = [tuple(merge.split()) for merge in merges]
self.bpe_ranks = dict(zip(merges, range(len(merges))))
self.cache = {}
def bpe(self, token):
word = tuple(token[:-1]) + ( token[-1] + '</w>',)
if token in self.cache:
return self.cache[token]
pairs = get_pairs(word)
if not pairs:
return token+'</w>'
while True:
bigram = min(pairs, key = lambda pair: self.bpe_ranks.get(pair, float('inf')))
if bigram not in self.bpe_ranks:
break
first, second = bigram
new_word = []
i = 0
while i < len(word):
try:
j = word.index(first, i)
new_word.extend(word[i:j])
i = j
except:
new_word.extend(word[i:])
break
if word[i] == first and i < len(word)-1 and word[i+1] == second:
new_word.append(first+second)
i += 2
else:
new_word.append(word[i])
i += 1
new_word = tuple(new_word)
word = new_word
if len(word) == 1:
break
else:
pairs = get_pairs(word)
word = ' '.join(word)
if word == '\n </w>':
word = '\n</w>'
self.cache[token] = word
return word
def encode(self, texts, verbose=True):
texts_tokens = []
if verbose:
for text in tqdm(texts, ncols=80, leave=False):
text = self.nlp(text_standardize(ftfy.fix_text(text)))
text_tokens = []
for token in text:
text_tokens.extend([self.encoder.get(t, 0) for t in self.bpe(token.text.lower()).split(' ')])
texts_tokens.append(text_tokens)
else:
for text in texts:
text = self.nlp(text_standardize(ftfy.fix_text(text)))
text_tokens = []
for token in text:
text_tokens.extend([self.encoder.get(t, 0) for t in self.bpe(token.text.lower()).split(' ')])
texts_tokens.append(text_tokens)
return texts_tokens
utils.py
import os
import re
import sys
import json
import math
import time
import unicodedata
import numpy as np
import tensorflow as tf
from tensorflow.python.framework import function
from tqdm import tqdm
from functools import partial
def encode_dataset(*splits, encoder):
encoded_splits = []
for split in splits[0]:
fields = []
for field in split:
if isinstance(field[0], str):
field = encoder.encode(field)
fields.append(field)
encoded_splits.append(fields)
return encoded_splits
def stsb_label_encoding(labels, nclass=6):
"""
Label encoding from Tree LSTM paper (Tai, Socher, Manning)
"""
Y = np.zeros((len(labels), nclass)).astype(np.float32)
for j, y in enumerate(labels):
for i in range(nclass):
if i == np.floor(y) + 1:
Y[j,i] = y - np.floor(y)
if i == np.floor(y):
Y[j,i] = np.floor(y) - y + 1
return Y
def shape_list(x):
"""
deal with dynamic shape in tensorflow cleanly
"""
ps = x.get_shape().as_list()
ts = tf.shape(x)
return [ts[i] if ps[i] is None else ps[i] for i in range(len(ps))]
def np_softmax(x, t=1):
x = x/t
x = x - np.max(x, axis=-1, keepdims=True)
ex = np.exp(x)
return ex/np.sum(ex, axis=-1, keepdims=True)
def make_path(f):
d = os.path.dirname(f)
if d and not os.path.exists(d):
os.makedirs(d)
return f
def _identity_init(shape, dtype, partition_info, scale):
n = shape[-1]
w = np.eye(n)*scale
if len([s for s in shape if s != 1]) == 2:
w = w.reshape(shape)
return w.astype(np.float32)
def identity_init(scale=1.0):
return partial(_identity_init, scale=scale)
def _np_init(shape, dtype, partition_info, w):
return w
def np_init(w):
return partial(_np_init, w=w)
class ResultLogger(object):
def __init__(self, path, *args, **kwargs):
if 'time' not in kwargs:
kwargs['time'] = time.time()
self.f_log = open(make_path(path), 'w')
self.f_log.write(json.dumps(kwargs)+'\n')
def log(self, **kwargs):
if 'time' not in kwargs:
kwargs['time'] = time.time()
self.f_log.write(json.dumps(kwargs)+'\n')
self.f_log.flush()
def close(self):
self.f_log.close()
def find_trainable_variables(key):
return tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, ".*{}.*".format(key))
def flatten(outer):
return [el for inner in outer for el in inner]
def remove_none(l):
return [e for e in l if e is not None]
def iter_data(*datas, n_batch=128, truncate=False, verbose=False, max_batches=float("inf")):
n = len(datas[0])
if truncate:
n = (n//n_batch)*n_batch
n = min(n, max_batches*n_batch)
n_batches = 0
if verbose:
f = sys.stderr
else:
f = open(os.devnull, 'w')
for i in tqdm(range(0, n, n_batch), total=n//n_batch, file=f, ncols=80, leave=False):
if n_batches >= max_batches: raise StopIteration
if len(datas) == 1:
yield datas[0][i:i+n_batch]
else:
yield (d[i:i+n_batch] for d in datas)
n_batches += 1
@function.Defun(
python_grad_func=lambda x, dy: tf.convert_to_tensor(dy),
shape_func=lambda op: [op.inputs[0].get_shape()])
def convert_gradient_to_tensor(x):
"""force gradient to be a dense tensor
it's often faster to do dense embedding gradient on GPU than sparse on CPU
"""
return x
def assign_to_gpu(gpu=0, ps_dev="/device:CPU:0"):
def _assign(op):
node_def = op if isinstance(op, tf.NodeDef) else op.node_def
if node_def.op == "Variable":
return ps_dev
else:
return "/gpu:%d" % gpu
return _assign
def average_grads(tower_grads):
def average_dense(grad_and_vars):
if len(grad_and_vars) == 1:
return grad_and_vars[0][0]
grad = grad_and_vars[0][0]
for g, _ in grad_and_vars[1:]:
grad += g
return grad / len(grad_and_vars)
def average_sparse(grad_and_vars):
if len(grad_and_vars) == 1:
return grad_and_vars[0][0]
indices = []
values = []
for g, _ in grad_and_vars:
indices += [g.indices]
values += [g.values]
indices = tf.concat(indices, 0)
values = tf.concat(values, 0)
return tf.IndexedSlices(values, indices, grad_and_vars[0][0].dense_shape)
average_grads = []
for grad_and_vars in zip(*tower_grads):
if grad_and_vars[0][0] is None:
grad = None
elif isinstance(grad_and_vars[0][0], tf.IndexedSlices):
grad = average_sparse(grad_and_vars)
else:
grad = average_dense(grad_and_vars)
v = grad_and_vars[0][1]
grad_and_var = (grad, v)
average_grads.append(grad_and_var)
return average_grads
논문에서 언급한 것처럼, 대규모의 라벨이 없는 텍스트 데이터를 이용해 언어 모델을 먼저 학습한 후, 특정 과제에 맞춰 미세 조정합니다. text_utils.py에 있는 TextEncoder 클래스가 이러한 텍스트를 토큰화하고 인코딩하는 작업을 담당하고 있습니다. 또한, BPE(Byte Pair Encoding) 방식을 사용해 서브워드 토큰화를 진행하고 있습니다. 이러한 사전 학습 단계는 학습된 언어 모델이 다양한 과제에 적용될 수 있도록 중요한 언어적 정보를 습득하게 만듭니다.
논문에서는 언어 모델을 각 과제에 맞춰 최소한의 구조 변화로 미세 조정하는 방법을 설명합니다. train.py 파일에서는 이 과정을 다루며, 예를 들어 transform_roc() 함수는 ROCStories 데이터셋을 처리하기 위한 입력 변환을 수행합니다. ROCStories는 논문에서도 언급된 commonsense reasoning(상식 추론) 과제를 평가하는 데 사용되었으며, 여기서 미세 조정이 이루어집니다.
논문에서는 Adam 옵티마이저와 학습률 조정(워밍업 및 코사인 스케줄링)을 사용하여 성능을 향상시키는 방법을 설명합니다. 이와 관련하여 opt.py 파일에서는 Adam 옵티마이저와 다양한 학습률 스케줄링 방식을 정의하고 있습니다. 이는 논문의 학습 전략과 매우 유사하며, 코드에서도 이러한 최적화 방식을 적용하여 모델 학습을 효율적으로 수행합니다.
논문에서 사용된 Transformer 구조는 긴 문맥을 처리하는 데 뛰어난 성능을 보입니다. train.py 파일에서는 model() 함수 내에서 Transformer 레이어와 Attention 메커니즘(e.g., _attn(), attn())을 활용하여 모델을 구성합니다. 이러한 구조는 긴 문맥 의존성을 처리하면서 텍스트의 상관관계를 파악하는 데 탁월한 성능을 발휘합니다.
논문에서는 언어 모델 손실을 보조 목적(auxiliary objective)으로 사용하여 모델의 일반화 성능을 향상시키는 방법을 설명합니다. 코드에서 clf_loss와 lm_loss는 각각 분류 손실과 언어 모델 손실을 처리하며, 이를 통해 모델의 성능을 더욱 강화합니다.
analysis.py 파일에서는 ROCStories와 같은 과제의 정확도를 계산하는 방법을 설명하고, ResultLogger 클래스( utils.py)는 실험 결과를 기록하는 데 사용됩니다. 이 로깅 방식은 논문에서 언급된 실험 결과 추적과 일치합니다.
dataset.py: 데이터셋을 어떻게 처리하고 준비하는지 확인하는 것이 중요합니다. 여기서는 ROCStories 데이터를 어떻게 불러오고 분리하는지 설명하고 있으니, 모델 학습에 사용할 데이터셋 구조를 파악하는 데 유용합니다.text_utils.py: 이 파일은 텍스트 인코딩 및 토큰화를 담당하는 부분입니다. 언어 모델의 입력을 어떻게 준비하는지 이해하는 것이 중요하므로, 이 부분을 먼저 이해하는 것이 좋습니다.train.py: 이 파일은 실제 모델 학습 및 평가의 주요 흐름을 담당합니다. Transformer 모델의 학습 방식과 최적화 전략, 손실 계산 등을 다룹니다. 코드의 메인 학습 과정이 여기에서 이루어지므로 중요한 파일입니다.opt.py: 학습 과정에서 중요한 역할을 하는 Adam 옵티마이저와 학습률 스케줄링 관련 함수들이 있습니다. 최적화 전략이 논문과 어떻게 일치하는지 확인하기 위해 이 파일을 살펴볼 필요가 있습니다.analysis.py: 모델이 학습된 후 평가 과정을 수행하는 부분입니다. 여기서는 ROCStories 데이터셋의 테스트 및 검증 정확도를 계산합니다.utils.py: 다양한 보조 함수들이 모여있는 파일입니다. 데이터셋 인코딩, 로깅, 그리고 기타 유틸리티 함수들이 포함되어 있어, 모델을 전반적으로 관리하는 데 중요한 역할을 합니다.datasets.py (데이터 로딩과 전처리)이 파일은 프로젝트에서 데이터를 불러오고, 모델에 입력할 수 있는 형태로 전처리하는 역할을 담당하고 있다.
os, csv: 파일 시스템과 CSV 파일을 다루기 위한 모듈.numpy: 수치 연산을 위한 필수 라이브러리.tqdm: 진행 상황을 표시해주는 툴.sklearn: 데이터셋을 섞거나, 훈련과 검증 데이터셋을 나누기 위한 shuffle과 train_test_split을 사용.data_dir에서 훈련 및 검증용 데이터를 불러와서 전처리.train_test_split으로 나누며, 이때 n_train과 n_valid 인수로 각각 훈련 및 검증 데이터셋 크기를 결정.이 파일은 데이터셋을 불러와서 스토리와 선택지, 그리고 정답으로 이루어진 리스트들을 모델 학습에 사용할 수 있는 형태로 나누고 있음. 데이터셋 처리 로직이 명확하게 정의되어 있고, 학습에 필요한 훈련 및 검증 데이터셋을 준비하는 역할.
def _rocstories(path):
with open(path) as f:
f = csv.reader(f)
st = [] # 스토리 데이터
ct1 = [] # 선택지 1
ct2 = [] # 선택지 2
y = [] # 정답 레이블
for i, line in enumerate(tqdm(list(f), ncols=80, leave=False)):
if i > 0: # 첫 번째 행은 건너뜀 (보통 CSV의 헤더)
s = ' '.join(line[1:5]) # 스토리 텍스트를 합침
c1 = line[5] # 첫 번째 선택지
c2 = line[6] # 두 번째 선택지
st.append(s)
ct1.append(c1)
ct2.append(c2)
y.append(int(line[-1])-1) # 마지막 열이 정답이며, 인덱스를 0부터 시작하도록 조정
return st, ct1, ct2, y
이 함수는 CSV 파일을 읽어서 데이터를 나누고 있어. 각 행에서 스토리, 선택지 1, 선택지 2, 그리고 정답 레이블을 추출한 후, 각각 리스트로 반환해. 주요 전처리 과정은 스토리와 선택지를 분리하고 정답 레이블을 0부터 시작하는 형식으로 바꾸는 것.
🍭for i, line in enumerate(tqdm(list(f), ncols=80, leave=False)) 설명
enumerate는 파일을 읽어들인 각 줄(line)과 그 줄의 번호(i)를 반환하는 역할을 한다.
tqdm은 진행 상황(progress bar)을 보여주는 라이브러리로, 반복이 얼마나 진행되었는지 시각적으로 나타낸다.
STEP1. f: CSV 파일 객체로, csv.reader(f)를 통해 파일의 각 줄을 리스트로 읽어들인다.
STEP2. list(f): CSV 파일을 리스트로 변환해 각 줄을 순차적으로 읽을 수 있게 한다.
STEP3. tqdm(list(f), ncols=80, leave=False): tqdm을 사용해 파일을 읽는 동안 진행 상황(progress bar)을 보여준다. ncols=80은 진행 바의 너비를 80으로 설정하고, leave=False는 완료된 후 진행 바를 지우는 역할을 한다.
코드의 동작:
def rocstories(data_dir, n_train=1497, n_valid=374):
storys, comps1, comps2, ys = _rocstories(os.path.join(data_dir, 'cloze_test_val__spring2016 - cloze_test_ALL_val.csv'))
teX1, teX2, teX3, _ = _rocstories(os.path.join(data_dir, 'cloze_test_test__spring2016 - cloze_test_ALL_test.csv'))
tr_storys, va_storys, tr_comps1, va_comps1, tr_comps2, va_comps2, tr_ys, va_ys = train_test_split(
storys, comps1, comps2, ys, test_size=n_valid, random_state=seed
)
# 학습 데이터를 저장하는 리스트
trX1, trX2, trX3 = [], [], []
trY = []
for s, c1, c2, y in zip(tr_storys, tr_comps1, tr_comps2, tr_ys):
trX1.append(s)
trX2.append(c1)
trX3.append(c2)
trY.append(y)
# 검증 데이터를 저장하는 리스트
vaX1, vaX2, vaX3 = [], [], []
vaY = []
for s, c1, c2, y in zip(va_storys, va_comps1, va_comps2, va_ys):
vaX1.append(s)
vaX2.append(c1)
vaX3.append(c2)
vaY.append(y)
return (trX1, trX2, trX3, trY), (vaX1, vaX2, vaX3, vaY), (teX1, teX2, teX3)
이 함수는 데이터를 훈련, 검증, 테스트 데이터셋으로 나누고 있어:
_rocstories 함수를 통해 불러온 후, train_test_split을 사용해 훈련용과 검증용으로 나눔._rocstories로 불러오고 그대로 반환.각 데이터셋은 다음과 같은 형식으로 나뉨:
X1: 스토리 데이터X2: 선택지 1X3: 선택지 2Y: 정답 레이블이 함수는 주로 데이터를 불러오고, 훈련 및 검증용으로 나누는 작업을 처리해. 이를 통해 모델이 학습할 수 있는 준비된 데이터셋을 만들 수 있어.
train_test_split을 사용해 훈련 데이터와 검증 데이터를 분할.text_utils.py (텍스트 전처리 및 토크나이징)이 파일은 텍스트 데이터를 전처리하고, 모델에 맞는 형식으로 변환하는 여러 유틸리티 함수들을 포함하고 있다. BPE(Byte Pair Encoding) 토크나이저와 같은 복잡한 텍스트 처리를 수행
BPE(Byte Pair Encoding)이란?
Byte Pair Encoding (BPE)은 원래 1994년에 제안된 데이터 압축 알고리즘으로, 이후 자연어 처리(NLP) 분야에서 서브워드 분리(Subword Segmentation) 방식으로 응용되었다. 이 알고리즘은 주로 OOV(Out-Of-Vocabulary) 문제를 해결하기 위해 사용되며, 특히 희귀 단어, 신조어, 복잡한 어휘를 처리할 때 매우 유용하다.
기계 학습 모델이 모르는 단어를 처리할 때 발생하는 문제를 OOV 문제라고 한다. 전통적인 NLP 모델들은 훈련 과정에서 학습한 단어들만 처리할 수 있기 때문에, 훈련 데이터에 없었던 단어가 등장하면 이 단어를 UNK(Unknown Token)으로 처리하게 된다. 이 때문에 모델이 해당 단어의 의미를 이해하지 못해 성능 저하가 발생할 수 있다.
서브워드 분리는 단어를 더 작은 의미 단위인 서브워드로 분해하는 방법이다. 예를 들어, birthplace라는 단어는 birth + place로 분해될 수 있다. 이러한 서브워드 분리는 희귀 단어나 신조어를 처리할 때 매우 유용하다. 이를 통해 모델이 모르는 단어를 더 작은 단위로 나누어 처리할 수 있게 된다.
BPE는 글자를 서브워드 단위로 병합하면서 단어 집합을 만들어내는 Bottom-up 방식의 알고리즘이다. BPE의 기본 아이디어는 가장 자주 등장하는 글자 쌍을 찾아서 하나의 새로운 서브워드로 병합하는 것이다. 이를 반복적으로 적용해 단어를 점차적으로 더 큰 단위의 서브워드로 변환한다.
다음 예시에서는 aaabdaaabac이라는 문자열에 대해 BPE를 적용한 과정을 보여준다.
aa가 가장 많이 등장한다.aa를 Z*로 치환한다:Z = aa*로 정의된다.```
aaabdaaabac -> ZabdZabac
```ab*를 찾아 Y로 치환한다:Y = ab로 정의된다.```
ZabdZabac -> ZYdZYac
```ZY*를 찾아 X로 치환한다:X = ZY로 정의된다.```
ZYdZYac -> XdXac
```이와 같이 BPE는 자주 등장하는 글자 쌍을 점진적으로 병합하여 최종적으로 의미 있는 서브워드 집합을 구성한다.
자연어 처리에서 BPE는 글자 단위에서 서브워드 단위로 변환하는 서브워드 분리 알고리즘으로 사용된다. BPE는 단어 집합을 처음부터 학습하지 않고, 글자 단위에서 시작하여 가장 자주 등장하는 글자 쌍을 병합해 단어 집합을 구축하는 방식으로 작동한다. 이 방식은 특히 신조어나 희귀 단어, 복잡한 어휘 구조를 다루는 데 유리하다.
훈련 데이터에서 자주 등장하는 단어가 다음과 같다고 가정해보자:
low : 5, lower : 2, newest : 6, widest : 3
먼저, 각 단어를 글자 단위로 분해한다:
l o w : 5, l o w e r : 2, n e w e s t : 6, w i d e s t : 3
이제 가장 자주 등장하는 쌍을 찾아 병합하는 과정을 진행한다.
빈도수가 가장 높은 쌍은 e*와 s이므로 이를 **es로 병합한다:
l o w : 5, l o w e r : 2, n e w es t : 6, w i d es t : 3
그 다음 빈도수가 가장 높은 쌍 es*와 t를 **est로 병합한다:
l o w : 5, l o w e r : 2, n e w est : 6, w i d est : 3
계속해서 가장 자주 등장하는 쌍을 병합한다. l*과 o가 자주 등장하므로 **lo로 병합한다:
lo w : 5, lo w e r : 2, n e w est : 6, w i d est : 3
이 과정을 총 10회 반복하면, 각 단어가 더 큰 서브워드로 병합된다. 이를 통해 단어 집합을 구성하게 된다:
low, lower, newest, widest
이제 lowest라는 새로운 단어가 등장했을 때, 기존에는 OOV 문제로 처리할 수 없었지만, BPE를 통해 low와 est로 분할하여 처리할 수 있다.
get_pairs(word):
text_standardize(text):
— 등)를 대체하고, 여러 공백을 하나의 공백으로 줄이거나 줄바꿈 문자를 처리하는 등의 작업을 수행.TextEncoder 클래스:
encoder_path와 bpe_path를 받아 BPE 토크나이저를 초기화하고, BPE 토큰화를 수행하는 메서드들이 포함.텍스트 데이터의 전처리 및 토크나이징에 중요한 역할을 하며, 특히 BPE를 사용해 텍스트를 모델에 맞는 토큰으로 변환하는 데 중점을 두고 있음.
get_pairs(word) 함수def get_pairs(word):
"""
Return set of symbol pairs in a word.
word is represented as tuple of symbols (symbols being variable-length strings)
"""
pairs = set()
prev_char = word[0]
for char in word[1:]:
pairs.add((prev_char, char))
prev_char = char
return pairs
이 함수는 단어에서 문자 쌍을 추출하는 역할을 한다. 이는 BPE 토크나이저에서 각 문자의 쌍을 찾아 병합 규칙을 적용할 때 사용된다. 주어진 단어에서 연속되는 두 글자의 쌍을 모두 추출해 set 형태로 반환한다.
text_standardize(text) 함수def text_standardize(text):
"""
Fixes some issues the spacy tokenizer had on books corpus
Also does some whitespace standardization
"""
text = text.replace('—', '-')
text = text.replace('–', '-')
text = text.replace('―', '-')
text = text.replace('…', '...')
text = text.replace('´', "'")
text = re.sub(r'(-+|~+|!+|"+|;+|\\?+|\\++|,+|\\)+|\\(+|\\\\+|/+|\\*+|\\[+|\\]+|}+|{+|\\|+|_+)', r' \\1 ', text)
text = re.sub(r'\\s*\\n\\s*', ' \\n ', text)
text = re.sub(r'[^\\S\\n]+', ' ', text)
return text.strip()
이 함수는 텍스트 데이터를 표준화하는 역할을 한다. 주로 다음과 같은 작업을 수행한다:
—, –, ―)를 일반적인 하이픈(``)으로 변환.…)를 점 세 개(...)로 치환.이 함수는 텍스트 데이터를 모델이 처리하기 쉽도록 정규화하는 과정에서 사용된다.
🍭re.sub()
re.sub() 함수는 파이썬의 정규 표현식 모듈인 re에서 제공하는 함수로, 특정 패턴을 찾아서 다른 문자열로 치환하는 역할을 한다. 이를 사용하면 문자열에서 특정한 규칙에 맞는 부분을 찾고, 그 부분을 다른 값으로 대체할 수 있다.
re.sub() 문법:re.sub(pattern, repl, string, count=0, flags=0)
pattern: 교체할 패턴(정규 표현식).repl: 대체할 문자열.string: 대상이 되는 원본 문자열.count: 치환할 횟수(기본값은 0, 즉 모두 치환).flags: 정규 표현식 플래그.import re
text = "Hello World!"
new_text = re.sub(r'World', 'Python', text)
print(new_text) # "Hello Python!"
위 예시에서는 World라는 패턴을 찾아 Python으로 치환했다.
text = re.sub(r'(-+|~+|!+|"+|;+|\\\\?+|\\\\++|,+|\\\\)+|\\\\(+|\\\\\\\\+|/+|\\\\*+|\\\\[+|\\\\]+|}+|{+|\\\\|+|_+)', r' \\\\1 ', text)
이 코드는 여러 특수 문자들이 연속으로 등장할 때, 그 특수 문자 앞뒤에 공백을 추가하는 작업을 한다.
(-+|~+|!+|"+|;+|\\\\?+|\\\\++|,+|\\\\)+|\\\\(+|\\\\\\\\+|/+|\\\\*+|\\\\[+|\\\\]+|}+|{+|\\\\|+|_+)+: 하나 이상의 대시(하이픈)가 등장하는 패턴.~+: 하나 이상의 물결표가 등장하는 패턴.!+: 하나 이상의 느낌표가 등장하는 패턴."+: 하나 이상의 큰따옴표가 등장하는 패턴.;+: 하나 이상의 세미콜론이 등장하는 패턴.\\\\?+: 하나 이상의 물음표가 등장하는 패턴 (\\\\는 이스케이프 문자를 의미).' \\\\1 '\\\\1: 찾은 패턴(첫 번째 캡처 그룹)을 의미한다. 이 부분은 그대로 유지하되, 앞뒤에 공백을 추가한다.Hello!!!라는 문자열이 주어지면 Hello !!! 처럼 변환된다.이 코드는 하나 이상의 특수 문자가 연속으로 등장하면 그 앞뒤에 공백을 추가하는 역할을 한다.
text = re.sub(r'\\\\s*\\\\n\\\\s*', ' \\\\n ', text)
이 코드는 줄바꿈 문자 \\n 앞뒤의 공백을 제거하고, 줄바꿈 문자 앞뒤에 한 칸의 공백을 추가한다.
\\\\s*\\\\n\\\\s*\\\\s*: 0개 이상의 공백 문자(공백, 탭 등)를 의미한다.\\\\n: 줄바꿈 문자 \\n을 의미한다.' \\\\n '\\n 앞뒤에 한 칸의 공백을 추가한다.이 코드는 줄바꿈 문자 \\n 앞뒤에 공백이 있으면 이를 정리하고, 공백을 일정하게 한 칸 추가하는 역할을 한다.
text = re.sub(r'[^\\\\S\\\\n]+', ' ', text)
이 코드는 연속된 공백을 하나의 공백으로 압축한다.
[^\\\\S\\\\n]+\\\\S: 공백이 아닌 문자를 의미한다.[^...]: 대괄호 내의 패턴이 아닌 문자를 의미한다. 즉, 공백 문자를 의미.+: 하나 이상의 공백이 등장하는 경우를 의미.' '이 코드는 여러 개의 공백을 한 칸의 공백으로 압축한다.
TextEncoder 클래스class TextEncoder(object):
"""
Mostly a wrapper for a public python bpe tokenizer
"""
def __init__(self, encoder_path, bpe_path):
self.nlp = spacy.load('en', disable=['parser', 'tagger', 'ner', 'textcat'])
self.encoder = json.load(open(encoder_path))
self.decoder = {v:k for k,v in self.encoder.items()}
merges = open(bpe_path).read().split('\\n')[1:-1]
merges = [tuple(merge.split()) for merge in merges]
self.bpe_ranks = dict(zip(merges, range(len(merges))))
self.cache = {}
def encode(self, texts, verbose=True):
texts_tokens = []
if verbose:
for text in tqdm(texts, ncols=80, leave=False):
text = self.nlp(text_standardize(ftfy.fix_text(text)))
text_tokens = []
for token in text:
text_tokens.extend([self.encoder.get(t, 0) for t in self.bpe(token.text.lower()).split(' ')])
texts_tokens.append(text_tokens)
else:
for text in texts:
text = self.nlp(text_standardize(ftfy.fix_text(text)))
text_tokens = []
for token in text:
text_tokens.extend([self.encoder.get(t, 0) for t in self.bpe(token.text.lower()).split(' ')])
texts_tokens.append(text_tokens)
return texts_tokens
이 클래스는 BPE 토크나이저를 래핑한 클래스로, 텍스트 데이터를 BPE 방식으로 토큰화하는 역할을 한다. 주요 초기화 작업은 다음과 같다:
parser, tagger, ner, textcat 등의 기능은 비활성화)encoder_path에서 인코더(사전)를 불러오고, 이를 역으로 디코딩할 수 있도록 decoder 딕셔너리 생성.bpe_path에서 불러오고, 이를 기반으로 병합 순위를 설정.cache를 사용해 토큰화된 결과를 저장하여 성능을 향상시킴.Spacy?
: Spacy는 파이썬에서 사용할 수 있는 오픈 소스 자연어 처리(NLP) 라이브러리입니다. Spacy는 텍스트 분석, 토크나이징, 품사 태깅, 개체명 인식(NER), 구문 분석 등 다양한 NLP 작업을 빠르고 효율적으로 처리하는 데 사용.
spacy.load('en', disable=...)
비활성화된 기능들
복잡한 분석(예: 구문 분석, 품사 태깅, NER 등)은 불필요하다고 판단되어 비활성화.
→ Spacy의 여러 기능 중 NLP 모델이 텍스트 전처리나 토크나이징을 위해서만 사용.
bpe(self, token) 메서드def bpe(self, token):
if token in self.cache:
return self.cache[token]
word = tuple(token)
pairs = get_pairs(word)
if not pairs:
return token
while True:
bigram = min(pairs, key=lambda pair: self.bpe_ranks.get(pair, float('inf')))
if bigram not in self.bpe_ranks:
break
first, second = bigram
new_word = []
i = 0
while i < len(word):
j = word.index(first, i)
new_word.extend(word[i:j])
i = j
if word[i] == first and i < len(word) - 1 and word[i+1] == second:
new_word.append(first + second)
i += 2
else:
new_word.append(word[i])
i += 1
word = tuple(new_word)
pairs = get_pairs(word)
if len(word) == 1:
break
word = ' '.join(word)
self.cache[token] = word
return word
이 메서드는 주어진 토큰에 대해 BPE 토큰화를 수행한다. 주어진 단어의 문자 쌍을 기반으로 BPE 병합 규칙을 적용하여 점차 긴 토큰을 생성하는 방식이다. 주요 작업은 다음과 같다:
코드 세부 설명
self.cache)if token in self.cache:
return self.cache[token]
token이 이미 처리된 적이 있다면, 캐시에 저장된 결과를 바로 반환한다. 이를 통해 중복 계산을 피하고 성능을 향상시킨다.word = tuple(token)
pairs = get_pairs(word)
tuple(token): 주어진 단어(token)를 문자 단위로 분해해 튜플로 만든다. 예를 들어, token = "hello"라면 word = ('h', 'e', 'l', 'l', 'o')로 변환된다.get_pairs(word): 이 함수는 단어 내에서 인접한 문자 쌍을 추출하는 함수이다. 예를 들어, ('h', 'e', 'l', 'l', 'o')라면 문자 쌍은 [('h', 'e'), ('e', 'l'), ('l', 'l'), ('l', 'o')]가 된다.not pairs)if not pairs:
return token
token을 반환한다.while True)while True:
bigram = min(pairs, key=lambda pair: self.bpe_ranks.get(pair, float('inf')))
if bigram not in self.bpe_ranks:
break
first, second = bigram
new_word = []
min(pairs, key=lambda pair: self.bpe_ranks.get(pair, float('inf'))): BPE 병합 규칙(self.bpe_ranks)에 따라, 가장 낮은 순위(즉, 자주 등장하는) 문자 쌍을 찾는다. self.bpe_ranks.get(pair, float('inf'))는 병합 순위를 반환하며, 순위가 없으면 float('inf')를 반환하여 그 쌍이 선택되지 않게 한다.bigram: 병합할 가장 자주 등장하는 문자 쌍.if bigram not in self.bpe_ranks: break: 만약 bigram이 BPE 병합 규칙에 없다면, 더 이상 병합할 수 없으므로 루프를 종료한다.first, second = bigram: 병합할 문자 쌍을 각각 첫 번째 문자와 두 번째 문자로 분리한다.new_word = []: 새로운 단어를 저장할 빈 리스트를 준비한다.i = 0
while i < len(word):
j = word.index(first, i)
new_word.extend(word[i:j])
i = j
if word[i] == first and i < len(word) - 1 and word[i+1] == second:
new_word.append(first + second)
i += 2
else:
new_word.append(word[i])
i += 1
j = word.index(first, i): first 문자가 등장하는 위치를 찾는다.new_word.extend(word[i:j]): first 문자 이전의 문자를 new_word에 추가한다.if word[i] == first and word[i+1] == second: 현재 문자와 그 다음 문자가 first, second일 때, 이를 병합하여 하나의 문자로 추가한다.i += 2: 병합이 이루어졌으므로 두 문자를 건너뛴다.new_word에 추가하고, 한 문자씩 이동한다.word = tuple(new_word)
pairs = get_pairs(word)
if len(word) == 1:
break
word = tuple(new_word): 병합된 단어를 다시 튜플로 변환하여 다음 병합 작업을 준비한다.pairs = get_pairs(word): 병합된 단어에서 새로운 문자 쌍을 추출한다.if len(word) == 1: 만약 병합된 결과가 하나의 문자로 이루어진 단어라면, 더 이상 병합할 필요가 없으므로 루프를 종료한다.word = ' '.join(word)
self.cache[token] = word
return word
' '.join(word): 병합된 단어의 서브워드들을 공백을 기준으로 연결한다. 예를 들어, ('low', 'est')라면 "low est"로 변환된다.self.cache[token] = word: 해당 단어를 캐시에 저장하여, 나중에 동일한 단어가 입력될 경우 중복 계산을 피한다.return word: BPE로 처리된 최종 서브워드 결과를 반환한다.get_pairs: 단어에서 문자 쌍을 추출.text_standardize: 텍스트 데이터를 정규화.TextEncoder: BPE 기반의 텍스트 인코더로, 텍스트 데이터를 BPE 방식으로 토큰화.이 파일은 텍스트 데이터를 토크나이징하고, 모델에 입력하기 전 필요한 전처리 작업을 담당하고 있다. 이 과정은 특히 BPE를 사용해 텍스트를 서브워드 단위로 분해하여 처리할 수 있도록 돕는다.
opt.py (최적화 및 학습률 스케줄링)이 파일은 학습 과정에서 사용할 최적화 알고리즘과 학습률 스케줄링 전략을 정의하고 있다. 주로 Adam 옵티마이저를 변형한 버전과 여러 가지 학습률 조정 방식을 포함하고 있다.
Warmup 학습률 스케줄링 함수들:
warmup_cosine(x, warmup=0.002): 학습 초기에 빠르게 학습률을 높이고, 이후에는 코사인 함수를 사용해 학습률을 점차 줄이는 방식.warmup_constant(x, warmup=0.002): 학습 초기에는 학습률을 일정하게 높이고, 이후에는 일정하게 유지하는 방식.warmup_linear(x, warmup=0.002): 초기에는 선형적으로 학습률을 증가시키고, 이후에는 선형적으로 감소시키는 방식.이러한 함수들은 학습 과정에서 학습률을 어떻게 조정할지 결정하는 데 사용돼.
adam 함수:
schedule을 사용해 학습률을 조절.max_grad_norm을 사용해 기울기 클리핑을 적용할 수 있어, 학습 안정성을 높이는 역할을 해.이 파일은 학습 중에 최적화를 수행하고, 학습률을 동적으로 조정하는 중요한 부분을 다루고 있어. 특히, Adam 옵티마이저에 L2 정규화를 추가하는 기능이 눈에 띄며, 학습 스케줄링 전략을 통해 학습률을 조정할 수 있어.
opt.py 파일은 최적화(Optimization)와 학습률 스케줄링 관련된 코드로 구성되어 있다. 특히 학습 중에 모델 파라미터를 업데이트하는 역할을 담당하는 Adam 옵티마이저의 변형된 버전을 포함하고 있다. 또한, Warmup 전략을 적용해 학습 초기에 학습률을 조정하는 다양한 스케줄링 함수들이 정의되어 있다.
warmup_cosine(x, warmup=0.002) 함수def warmup_cosine(x, warmup=0.002):
s = tf.cast(x <= warmup, tf.float32)
return s*(x/warmup) + (1-s)*(0.5 * (1 + tf.cos(math.pi * x)))
이 함수는 코사인 학습률 스케줄링을 구현한 함수이다. 학습 초기에 학습률을 선형으로 증가시키고, 그 이후에는 코사인 함수를 사용해 학습률을 점차 감소시키는 방식으로 작동한다.
x <= warmup인 경우, 학습률은 선형적으로 증가한다.warmup_constant(x, warmup=0.002) 함수def warmup_constant(x, warmup=0.002):
s = tf.cast(x <= warmup, tf.float32)
return s*(x/warmup) + (1-s)*1
이 함수는 상수 학습률 스케줄링을 구현한다. 초기에는 학습률을 선형적으로 증가시키고, 일정 구간 이후에는 학습률을 일정하게 유지하는 방식이다. Warmup 구간 이후 학습률은 고정된 값으로 유지된다.
warmup_linear(x, warmup=0.002) 함수def warmup_linear(x, warmup=0.002):
s = tf.cast(x <= warmup, tf.float32)
return (s*(x/warmup) + (1-s))*(1-x)
이 함수는 선형 학습률 스케줄링을 구현한다. 학습 초기에 학습률을 선형적으로 증가시키고, 이후에도 선형적으로 감소시키는 방식이다. 학습률이 시간이 지남에 따라 일정하게 감소한다.
adam 함수def adam(params, grads, lr, schedule, t_total, b1=0.9, b2=0.999, e=1e-8, l2=0, vector_l2=False, max_grad_norm=-1, **kwargs):
"""
Adam with weight decay fix
"""
t = tf.Variable(0, dtype=tf.float32, trainable=False)
tt = t + 1
updates = [t.assign(tt)]
if max_grad_norm > 0:
grads, _ = tf.clip_by_global_norm(grads, max_grad_norm)
for p, g in zip(params, grads):
if p is None or g is None:
print("can't train", p.name, g)
else:
if isinstance(g, tf.IndexedSlices):
g = tf.convert_to_tensor(g)
m = tf.Variable(p*0, dtype=tf.float32, trainable=False)
v = tf.Variable(p*0, dtype=tf.float32, trainable=False)
lrt = lr * tf.sqrt(1 - b2**tt) / (1 - b1**tt)
lrt *= schedule(t / t_total)
mt = b1 * m + (1 - b1) * g
vt = b2 * v + (1 - b2) * g * g
if (len(p.get_shape()) > 1 or vector_l2) and l2 > 0:
lrt += l2 * p
update = p - lrt * mt / (tf.sqrt(vt) + e)
updates.append(p.assign(update))
return tf.group(*updates)
이 함수는 Adam 옵티마이저를 변형한 버전으로, 학습률 스케줄링과 L2 정규화를 추가한 형태이다. 주요 기능은 다음과 같다:
max_grad_norm을 통해 기울기의 크기가 너무 커지는 것을 방지한다.m)과 기울기 제곱(v)을 사용해 학습률을 적응적으로 조정한다.schedule)에 의해 동적으로 조정된다.Adam 옵티마이저는 파라미터를 업데이트할 때, 학습률을 조정하며 모멘텀을 사용해 더욱 안정적으로 학습을 진행한다.
🍭학습률 스케줄링과 L2 정규화를 추가
학습률 스케줄링은 schedule(t / t_total) 부분에서 이루어집니다. 이 부분은 학습률이 시간이 지남에 따라 어떻게 변화할지를 결정하는 스케줄링 함수에 따라 동적으로 학습률을 조정하는 역할을 합니다.
lrt *= schedule(t / t_total)
L2 정규화는 if (len(p.get_shape()) > 1 or vector_l2) and l2 > 0: 조건문과 lrt += l2 * p 부분에서 적용됩니다.
if (len(p.get_shape()) > 1 or vector_l2) and l2 > 0:
lrt += l2 * p
l2 > 0: L2 정규화가 활성화되어 있는지 여부를 확인합니다.p.get_shape() > 1: 파라미터 p가 다차원 배열(매트릭스 또는 벡터)인지 확인하여, 필요한 경우에만 정규화를 적용합니다. (스칼라 값에는 적용하지 않음)lrt += l2 * p: L2 정규화는 파라미터 p에 l2 값을 곱하여 학습률(lrt)에 더하는 방식으로 적용됩니다. 이는 파라미터의 값이 너무 커지는 것을 방지하는 역할을 합니다.warmup_cosine, warmup_constant, warmup_linear 함수를 통해 학습 초기에 학습률을 조정하는 다양한 방식의 스케줄링을 제공한다.adam 함수는 Adam 최적화 알고리즘을 기반으로 하며, 학습 중 기울기를 기반으로 모델 파라미터를 업데이트한다.이 파일은 학습 중 최적화를 수행하고, 학습률을 조정하는 데 중요한 역할을 한다. 특히, Adam 옵티마이저에 L2 정규화와 학습률 스케줄링을 추가하여 학습 성능을 더욱 향상시킨다.
train.py (모델 학습)이 파일은 모델 학습의 메인 스크립트로, 전체 학습 과정을 제어하고 있다. 데이터를 불러오고, 모델을 학습시키며, 필요한 설정들을 초기화하는 과정이 포함되어 있다.
datasets.py를 통해 불러옴)opt.py 사용)opt.py에서 정의된 최적화 알고리즘(Adam)과 학습률 스케줄링 함수들을 불러와서 사용.datasets.py에서 정의된 데이터셋 로딩 함수(rocstories)를 사용해 데이터를 불러옴.utils.py, text_utils.py, analysis.py에서 임포트됨.gelu, swish, relu 등의 활성화 함수들이 정의되고, 이를 학습에 사용할 수 있도록 등록.opt_fns에 Adam 옵티마이저가, lr_schedules에는 학습률 스케줄링 함수들이 등록되어 , 다양한 설정으로 학습을 조정할 수 있음._norm, norm 함수:g와 b라는 가중치를 통해 데이터에 맞는 정규화를 적용할 수 있게 설계됨.train.py 파일은 프로젝트의 모델 학습을 제어하는 메인 스크립트이다. 데이터 로딩, 모델 설정, 최적화, 학습률 스케줄링, 그리고 모델 학습과 관련된 전체 과정이 여기에 담겨 있다. 이 파일을 통해 전체적인 학습 과정이 관리된다. 일종의 main.py
import os
import time
import math
import json
import joblib
import random
import argparse
import numpy as np
import tensorflow as tf
from tqdm import tqdm
from functools import partial
from sklearn.utils import shuffle
from sklearn.metrics import accuracy_score
from opt import adam, warmup_cosine, warmup_linear, warmup_constant
from datasets import rocstories
from analysis import rocstories as rocstories_analysis
from text_utils import TextEncoder
from utils import encode_dataset, flatten, iter_data, find_trainable_variables, convert_gradient_to_tensor, shape_list, ResultLogger, assign_to_gpu, average_grads, make_path
다양한 외부 라이브러리 및 내부 모듈들이 임포트된다:
opt.py: 최적화 알고리즘(Adam)과 학습률 스케줄링 함수들이 임포트된다.datasets.py: 데이터셋 로딩 및 전처리 함수들이 임포트된다.text_utils.py: 텍스트 인코더(BPE 기반 토크나이저)가 임포트된다.utils.py: 여러 유틸리티 함수들이 임포트되어 모델 학습과 데이터 처리에 도움을 준다.def gelu(x):
return 0.5 * x * (1 + tf.tanh(math.sqrt(2 / math.pi) * (x + 0.044715 * tf.pow(x, 3))))
def swish(x):
return x * tf.nn.sigmoid(x)
opt_fns = {
'adam': adam,
}
act_fns = {
'relu': tf.nn.relu,
'swish': swish,
'gelu': gelu
}
lr_schedules = {
'warmup_cosine': warmup_cosine,
'warmup_linear': warmup_linear,
'warmup_constant': warmup_constant,
}
relu, swish, gelu와 같은 활성화 함수들이 정의되어 있으며, 모델의 학습 중에 사용된다.warmup_cosine, warmup_linear, warmup_constant 함수들을 통해 학습률이 동적으로 조정될 수 있다.def _norm(x, g=None, b=None, e=1e-5, axis=[1]):
u = tf.reduce_mean(x, axis=axis, keepdims=True)
s = tf.reduce_mean(tf.square(x - u), axis=axis, keepdims=True)
x = (x - u) * tf.rsqrt(s + e)
if g is not None and b is not None:
x = x * g + b
return x
def norm(x, scope, axis=[-1]):
with tf.variable_scope(scope):
n_state = shape_list(x)[-1]
g = tf.get_variable("g", [n_state], initializer=tf.constant_initializer(1))
b = tf.get_variable("b", [n_state], initializer=tf.constant_initializer(0))
return _norm(x, g, b, axis=axis)
def train_model(args):
# 텍스트 인코더 설정
encoder = TextEncoder(args.encoder_path, args.bpe_path)
# 데이터셋 로드
(trX1, trX2, trX3, trY), (vaX1, vaX2, vaX3, vaY), (teX1, teX2, teX3) = rocstories(args.data_dir)
# 텐서플로우 그래프 설정 및 세션 생성
with tf.Session(graph=tf.Graph()) as sess:
# 모델 및 최적화 설정
opt_fn = opt_fns[args.opt]
lr_schedule = lr_schedules[args.lr_schedule]
# 모델 빌딩 및 학습 관련 설정들
# 학습 준비
for epoch in range(args.epochs):
# 데이터 섞기 및 미니 배치로 학습 진행
for step, (batchX1, batchX2, batchX3, batchY) in enumerate(iter_data(trX1, trX2, trX3, trY, size=args.batch_size)):
# 기울기 계산 및 파라미터 업데이트
_, loss_val = sess.run([train_op, loss], feed_dict={
# 데이터 전달
})
# 검증 데이터로 성능 평가
val_acc = accuracy_score(sess.run([val_pred], feed_dict={...}))
# 학습 완료 후 모델 저장
joblib.dump(sess.run([params]), os.path.join(args.save_path, 'model_params.joblib'))
이 부분은 모델 학습의 전체 흐름을 보여준다:
TextEncoder를 사용해 데이터를 BPE 토크나이징하여 모델에 입력할 준비를 한다.rocstories 함수로 데이터셋을 불러와 훈련, 검증, 테스트 데이터를 준비한다.train.py는 학습의 메인 스크립트로, 전체 모델 학습 과정을 책임진다. 데이터를 준비하고, 모델을 학습시키며, 검증을 통해 성능을 평가하는 전 과정을 관리한다.
추가적으로 더 알고 싶은 내용이 있으면 말해줘!
analysis.py (모델 성능 분석)이 파일은 학습이 끝난 후 모델의 성능을 분석하거나, 결과를 시각화하고 평가하는 기능을 담당하고 있다. 훈련이 완료된 모델을 평가하거나 결과를 해석하는 데 사용될 수 있다.
analysis.py 파일은 주로 학습이 완료된 모델의 성능을 평가하거나 결과를 분석하는 데 사용되는 코드가 포함되어 있을 것으로 보인다. 이를 통해 모델이 실제로 얼마나 잘 학습되었는지, 정확도나 손실 같은 지표들을 계산하고 시각화할 수 있다.
import numpy as np
from sklearn.metrics import accuracy_score
from sklearn.utils import shuffle
이 파일에서는 numpy, sklearn.metrics, sklearn.utils가 임포트된다. 이 라이브러리들은 주로 성능 평가, 정확도 계산, 데이터 셔플링 등의 기능을 수행한다.
def rocstories_analysis(preds, ys):
"""
preds: 모델이 예측한 결과
ys: 실제 정답 레이블
두 개의 리스트를 입력으로 받아 정확도를 계산한다.
"""
accuracy = accuracy_score(ys, preds)
print(f"Accuracy: {accuracy * 100:.2f}%")
return accuracy
이 함수는 모델의 예측값과 실제 정답 레이블을 비교하여 정확도를 계산하는 함수이다. 주요 작업은 다음과 같다:
accuracy_score(ys, preds): Scikit-learn의 accuracy_score 함수를 사용하여 정확도를 계산한다.이 함수는 학습 후 검증 데이터나 테스트 데이터에 대해 모델의 성능을 간단하게 평가할 때 사용된다.
def shuffle_data(X1, X2, X3, Y):
"""
X1, X2, X3: 입력 데이터 (스토리, 선택지 1, 선택지 2)
Y: 정답 레이블
주어진 데이터를 셔플링하여 반환한다.
"""
X1, X2, X3, Y = shuffle(X1, X2, X3, Y)
return X1, X2, X3, Y
이 함수는 데이터 셔플링을 수행한다. 주로 모델이 특정한 순서에 치우치지 않고 다양한 데이터로 학습할 수 있도록 데이터를 섞어준다. Scikit-learn의 shuffle 함수를 사용하여 스토리, 선택지, 정답 데이터를 함께 셔플링한 후, 이를 반환한다.
이 파일에는 다른 분석 함수나 추가적인 유틸리티 함수들이 포함되어 있을 수 있다. 하지만 주된 역할은 모델의 성능을 평가하거나, 데이터를 셔플링해 학습에 사용할 준비를 돕는 것이다.
rocstories_analysis):shuffle_data):이 파일은 학습 후 모델의 성능을 평가하거나, 데이터를 전처리하는 데 유용한 함수들이 포함되어 있다. 간단하게 정확도를 계산하고, 데이터를 섞어주는 역할을 하며, 이를 통해 모델의 성능을 분석하고 검증할 수 있다.
utils.py (유틸리티 함수)이 파일은 여러 작업을 보조하는 유틸리티 함수들을 포함하고 있다. 데이터 인코딩, 파일 경로 생성, 텐서 크기 처리 등 학습 과정에서 필요한 다양한 보조 작업을 수행한다.
encode_dataset 함수:stsb_label_encoding 함수:shape_list 함수:np_softmax 함수:make_path 함수:이 파일은 학습 과정에서 필요한 데이터 인코딩, 텐서의 모양 처리, 소프트맥스 계산, 파일 경로 생성 등 여러 보조 작업을 수행하는 데 사용돼. 다양한 유틸리티 함수들이 모델 학습의 여러 단계에서 호출될 수 있어.
utils.py 파일은 프로젝트에서 다양한 보조 작업을 수행하는 유틸리티 함수들이 정의된 파일이다. 이 파일의 함수들은 모델 학습과 데이터 처리의 여러 단계에서 활용되며, 특히 데이터 인코딩, 파일 경로 생성, 텐서 크기 처리, 그리고 소프트맥스 계산 등을 수행하는 데 사용된다.
encode_dataset 함수def encode_dataset(*splits, encoder):
encoded_splits = []
for split in splits[0]:
fields = []
for field in split:
if isinstance(field[0], str):
field = encoder.encode(field)
fields.append(field)
encoded_splits.append(fields)
return encoded_splits
이 함수는 여러 데이터셋(훈련, 검증, 테스트)을 인코더를 사용해 토크나이징하는 함수이다. encoder.encode()를 호출하여 텍스트 데이터를 토큰화하고, 각 필드를 인코딩된 데이터로 변환한다.
stsb_label_encoding 함수def stsb_label_encoding(labels, nclass=6):
"""
Label encoding from Tree LSTM paper (Tai, Socher, Manning)
"""
Y = np.zeros((len(labels), nclass)).astype(np.float32)
for j, y in enumerate(labels):
for i in range(nclass):
if i == np.floor(y) + 1:
Y[j,i] = y - np.floor(y)
if i == np.floor(y):
Y[j,i] = np.floor(y) - y + 1
return Y
이 함수는 STSB(Tree LSTM) 레이블 인코딩을 수행한다. 레이블을 다차원 배열로 변환하여 모델이 다중 클래스 분류 문제에서 사용할 수 있도록 한다.
nclass)에 맞춰 적절한 형식으로 변환하여 반환한다.shape_list 함수def shape_list(x):
"""
Deal with dynamic shape in tensorflow cleanly
"""
ps = x.get_shape().as_list()
ts = tf.shape(x)
return [ts[i] if ps[i] is None else ps[i] for i in range(len(ps))]
이 함수는 TensorFlow 텐서의 동적 모양(dynamic shape)을 처리하는 함수이다. 텐서의 크기를 안정적으로 처리하기 위해 사용되며, TensorFlow 그래프 내에서 동적으로 변화하는 텐서의 크기를 다룰 수 있게 한다.
np_softmax 함수def np_softmax(x, t=1):
x = x / t
x = x - np.max(x, axis=-1, keepdims=True)
ex = np.exp(x)
return ex / np.sum(ex, axis=-1, keepdims=True)
이 함수는 넘파이 기반의 소프트맥스 함수이다. 주어진 입력에 대해 소프트맥스 계산을 수행하여, 확률 분포를 반환한다.
make_path 함수def make_path(f):
d = os.path.dirname(f)
if d and not os.path.exists(d):
os.makedirs(d)
return f
이 함수는 파일 경로가 존재하지 않을 경우, 경로를 생성하는 역할을 한다. 파일이나 디렉토리 경로가 주어지면, 필요한 디렉토리가 없을 때 이를 생성해준다.
파일에는 다양한 유틸리티 함수들이 포함되어 있을 수 있으며, 주로 데이터셋 처리, 학습 중 필요한 데이터 전처리, 모델 저장 및 경로 관리 등을 위한 도우미 함수들이 많이 정의되어 있다.
encode_dataset: 여러 데이터셋을 인코더를 사용해 토크나이징하고, 이를 모델이 학습할 수 있는 형식으로 변환한다.stsb_label_encoding: 다중 클래스 레이블을 적절한 형식으로 변환하는 함수로, 모델의 출력과 레이블이 일치할 수 있도록 변환한다.shape_list: 텐서의 동적 크기를 처리하고, 이를 리스트 형태로 반환하는 함수이다.np_softmax: 넘파이 기반의 소프트맥스 함수로, 입력 값을 확률로 변환하여 반환한다.make_path: 파일 경로가 없을 경우 디렉토리를 생성해주는 함수로, 파일 저장을 위한 경로를 설정하는 데 사용된다.이 파일은 학습 과정에서 자주 사용하는 여러 도우미 함수들을 제공하며, 데이터를 인코딩하거나 텐서의 크기를 처리하는 등의 작업에 도움을 준다.