Y Tech Blog search

Light GBM 설명 및 사용법

|

1. Light GBM: A Highly Efficient Gradient Boosting Decision Tree 논문 리뷰

1.1. Background and Introduction

다중 분류, 클릭 예측, 순위 학습 등에 주로 사용되는 Gradient Boosting Decision Tree (GBDT)는 굉장히 유용한 머신러닝 알고리즘이며, XGBoost나 pGBRT 등 효율적인 기법의 설계를 가능하게 하였다. 이러한 구현은 많은 엔지니어링 최적화를 이룩하였지만 고차원이고 큰 데이터 셋에서는 만족스러운 결과를 내지 못하는 경우도 있었다. 왜냐하면 모든 가능한 분할점에 대해 정보 획득을 평가하기 위해 데이터 개체 전부를 스캔해야 했기 때문이다. 이는 당연하게도, 굉장히 시간 소모적이다.

본 논문은 이 문제를 해결하기 위해 2가지 최신 기술을 도입하였다.
첫 번째는 GOSS: Gradient-based One-Side Sampling이며, 기울기가 큰 데이터 개체가 정보 획득에 있어 더욱 큰 역할을 한다는 아이디어에 입각해 만들어진 테크닉이다. 큰 기울기를 갖는 개체들은 유지되며, 작은 기울기를 갖는 데이터 개체들은 일정 확률에 의해 랜덤하게 제거된다.

두 번째는 EFB: Exclusive Feature Bundling으로, 변수 개수를 줄이기 위해 상호배타적인 변수들을 묶는 기법이다. 원핫 인코딩된 변수와 같이 희소한(Sparse) 변수 공간에서는 많은 변수들이 상호 배타적인 경우가 많다. (0이 굉장히 많기 때문에) 본 테크닉은, 최적 묶음 문제를 그래프 색칠 문제로 치환하고 일정 근사 비율을 갖는 Greedy 알고리즘으로 이 문제를 해결한다.

1.2. Preliminaries

GBDT는 Decision Tree의 앙상블 모델이다. 각각의 반복에서 GBDT는 음의 기울기(잔차 오차)를 적합함으로써 Decision Tree를 학습시킨다. 이 학습 과정에서 가장 시간이 많이 소모되는 과정이 바로 최적의 분할점들을 찾는 것인데, 이를 위한 대표적인 방법에는 Pre-sorted(사전 정렬) 알고리즘Histogram-based 알고리즘이 있다.

Pre-sorted 알고리즘의 경우 사전 정렬한 변수 값에 대해 가능한 모든 분할점을 나열함으로써 간단하게 최적의 분할점을 찾을 수 있지만, 효율적이지 못하다는 단점이 있다. Histogram-based 알고리즘은 연속적인 변수 값을 이산적인 구간(bin)으로 나누고, 이 구간을 사용하여 학습과정 속에서 피쳐 히스토그램을 구성한다.

학습 데이터의 양을 줄이기 위해 가장 쉽게 생각할 수 있는 방법은 Down Sampling이 될 것이다. 이는 만약 데이터 개체의 중요도(Weight)가 설정한 임계값을 넘지 못할 경우 데이터 개체들이 필터링되는 과정을 말한다. SGB의 경우 약한 학습기를 학습시킬 때 무작위 부분집합을 사용하지만, SGB를 제외한 Down Sampling 방식은 AdaBoost에 기반하였기 때문에 바로 GBDT에 적용시킬 수 없다. 왜냐하면 AdaBoost와 달리 GBDT에는 데이터 개체에 기본 가중치가 존재하지 않기 대문이다.

비슷한 방식으로 피쳐 수를 줄이기 위해서는, 약한(Weak) 피쳐를 필터링하는 것이 자연스러울 것이다. 그러나 이러한 접근법은 변수들 사이에 중대한 중복요소가 있을 것이라는 가정에 의존하는데, 실제로는 이 가정이 옳지 않을 수도 있다.

실제 상황에서 사용되는 대용량 데이터셋은 많은 경우에 희소한(Sparse) 데이터셋일 확률이 높다. Pre-sorted 알고리즘에 기반한 GBDT의 경우 0값을 무시함으로써 학습 비용을 절감할 수 있지만, Histogram-based 알고리즘에 기반한 GBDT에는 효율적인 희소값 최적화 방법이 없다. 그 이유는 Histogram-based 알고리즘은 피쳐 값이 0이든 1이든, 각 데이터 개체마다 피쳐 구간(Bin) 값을 추출해야하기 때문이다. 따라서 Histogram-based 알고리즘에 기반한 GBDT가 희소 변수를 효과적으로 활용할 방안이 요구된다. 이를 해결하기 위한 방법이 바로 앞서 소개한 GOSSEFB인 것이다. GOSS는 데이터 개체 수를 줄이고, EFB는 피쳐 수를 줄이는 방법론이다.

1.3. GOSS: Gradient-based One-Sided Sampling

AdaBoost에서 Sample Weight는 데이터 개체의 중요도를 알려주는 역할을 수행하였다. GBDT에서는 기울기(Gradient)가 이 역할을 수행한다. 각 데이터 개체의 기울기가 작으면 훈련 오차가 작다는 것을 의미하므로, 이는 학습이 잘 되었다는 뜻이다. 이후 이 데이터를 그냥 제거한다면 데이터의 분포가 변화할 것이므로, 다른 접근법(GOSS)이 필요하다.

GOSS의 아이디어는 직관적이다. 큰 Gradient(훈련이 잘 안된)를 갖는 데이터 개체들은 모두 남겨두고, 작은 Gradient를 갖는 데이터 개체들에서는 무작위 샘플링을 진행하는 것이다. 이를 좀 더 상세히 설명하자면 아래와 같다.

1) 데이터 개체들의 Gradient의 절대값에 따라 데이터 개체들을 정렬함
2) 상위 100a% 개의 개체를 추출함
3) 나머지 개체들 집합에서 100b% 개의 개체를 무작위로 추출함
4) 정보 획득을 계산할 때, 위의 2-3 과정을 통해 추출된 Sampled Data를 상수( $ \frac{1-a} {b} $ )를 이용하여 증폭시킴

위 그림에 대하여 추가적으로 부연설명을 하면,
topN, randN은 2, 3 과정에서 뽑는 개수를 의미하며,
topSet, randSet 은 2, 3 과정에서 뽑힌 데이터 개체 집합을 의미한다.
w[randSet] x= fact은 증폭 벡터를 구성하는 과정으로, 증폭 벡터는 randSet에 해당하는 원소는 fact 값을 가지고, 나머지 원소는 1의 값을 가지는 벡터이다.

마지막으로 L: Weak Learner에 저장된 정보는, 훈련데이터, Loss, 증폭된 w벡터로 정리할 수 있겠다.

1.4. EFB: Exclusive Feature Bundling

희소한 변수 공간의 특성에 따라 배타적인 변수들을 하나의 변수로 묶을 수 있다. 그리고 이를 배타적 변수 묶음(Exclusive Feature Bundle)이라고 부른다. 정교하게 디자인된 변수 탐색 알고리즘을 통해, 각각의 변수들로 했던 것과 마찬가지로 변수 묶음들로부터도 동일한 변수 히스토그램들을 생성할 수 있게 된다.

이제 1) 어떤 변수들이 함께 묶여야 하는지 정해야 하며, 2) 어떻게 묶음을 구성할 것인가에 대해 알아볼 것이다.

정리: 변수들을 가장 적은 수의 배타적 묶음으로 나누는 문제는 NP-hard이다.
(NP-hard의 뜻을 알아보기 위해서는 이곳을 참조하길 바란다.)

증명: 그래프 색칠 문제를 본 논문의 문제로 환원한다. 그래프 색칠 문제는 NP-hard이므로 우리는 결론은 추론 가능하다.

$ G = (V, E) $ 라는 임의의 그래프가 있다고 하자. 이 G의 발생 행렬(Incidence Matrix)의 들이 우리 문제의 변수에 해당한다. 위 정리에서 최적의 묶음 전략을 찾는 것은 NP-hard라고 하였는데, 이는 다항 시간 안에 정확한 해를 구하는 것이 불가능하다는 의미이다. 따라서 좋은 근사 알고리즘을 찾기 위해서는 최적 묶음 문제를 그래프 색칠 문제로 치환해야 한다. 이 치환은 변수(feature)들을 꼭짓점(vertices)으로 간주하고 만약 두 변수가 상호배타적일 경우 그들 사이에 변(edge)을 추가하는 방식으로 이루어진다. 이후 Greedy 알고리즘을 사용한다.

1)에 관한 알고리즘을 설명하자면 다음과 같다.

  • 각 변마다 가중치가 있는 그래프를 구성하는데, 여기서 가중치는 변수들간의 충돌(conflicts)을 의미한다. 여기서 충돌이란 non-zero value가 동시에 존재하여 상호배타적이지 않은 상황을 의미한다.
  • 그래프 내에 있는 꼭짓점 차수에 따라 내림차순으로 변수들을 정렬한다.
  • 정렬한 리스트에 있는 각 변수를 확인하면서 이들을 작은 충돌(γ로 제어함)이 있는 기존 묶음에 할당하거나, 새로운 묶음을 만든다.

이 알고리즘의 시간 복잡도는 변수들의 개수의 제곱에 해당하며, 이는 나름 괜찮은 수준이지만 만약 변수들의 수가 매우 많다면 개선이 필요하다고 판단된다. 따라서 본 논문은 그래프를 직접 구성하지 않고 0이 아닌 값의 개수에 따라 정렬하는 방식(0이 아닌 값이 많을 수록 충돌을 일으킬 확률이 높으므로)으로 알고리즘을 수정하였다.

2)에 관해서 이야기하자면, 가장 중요한 것은 변수 묶음들로부터 원래(original) 변수들의 값을 식별할 수 있어야 한다는 것이다. Histogram-based 알고리즘은 변수의 연속적인 값 대신 이산적인 구간(bin)을 저장하므로, 배타적 변수들을 각각 다른 구간에 두어 변수 묶음을 구성할 수 있다. 이는 변수의 원래 값에 offset을 더하는 것으로 이루어 질 수 있다.

예를 들어 변수 묶음에 변수 2개가 속한다고 할 때,
원래 변수 A는 [0, 10)의 값을 취하고, 원래 변수 B는 [0, 20)의 값을 취한다.
이대로 두면 [0, 10) 범위 내에서 두 변수는 겹칠 것이므로,
변수 B에 offset 10을 더하여 가공한 변수가 [10, 30)의 값을 취하게 한다.
이후 A, B를 병합하고 [0, 30] 범위의 변수 묶음을 사용하여 기존의 변수 A, B를 대체한다.


2. Light GBM 적용

본 글에서는 Kaggle-Santander 데이터를 이용하여 간단한 적용 예시를 보이도록 하겠다. 초기에 lightgbm은 독자적인 모듈로 설계되었으나 편의를 위해 scikit-learn wrapper로 호환이 가능하게 추가로 설계되었다. 본 글에서는 scikit-learn wrapper Light GBM을 기준으로 설명할 것이다.

# Santander Data
   ID  var3  var15   ...    saldo_medio_var44_ult3     var38  TARGET
0   1     2     23   ...                       0.0  39205.17       0
1   3     2     34   ...                       0.0  49278.03       0
2   4     2     23   ...                       0.0  67333.77       0
[3 rows x 371 columns]

n_estimators 파라미터는 반복 수행하는 트리의 개수를 의미한다. 너무 크게 지정하면 학습 시간이 오래 걸리고 과적합이 발생할 수 있으니, 파라미터 튜닝 시에는 크지 않은 숫자로 지정하는 것이 좋다. num_leaves 파라미터는 하나의 트리가 가질 수 있는 최대 리프의 개수인데, 이 개수를 높이면 정확도는 높아지지만 트리의 깊이가 커져 모델의 복잡도가 증가한다는 점에 유의해야 한다.

먼저 기본적인 모델을 불러온다.

from lightgbm import LGBMClassifier
lgbm = LGBMClassifier(n_estimators=200)

공식문서을 참조하면 아래와 같은 몇몇 주의사항을 볼 수 있다.

Light GBM은 leaf-wise 방식을 취하고 있기 때문에 수렴이 굉장히 빠르지만, 파라미터 조정에 실패할 경우 과적합을 초래할 수 있다.

max_depth 파라미터는 트리의 최대 깊이를 의미하는데, 위에서 설명한 num_leaves 파라미터와 중요한 관계를 지닌다. 과적합을 방지하기 위해 num_leaves는 2^(max_depth)보다 작아야 한다. 예를 들어 max_depth가 7이기 때문에, 2^(max_depth)=98이 되는데, 이 때 num_leaves를 이보다 작은 70~80 정도로 설정하는 것이 낫다.

min_child_samples 파라미터는 최종 결정 클래스인 Leaf Node가 되기 위해서 최소한으로 필요한 데이터 개체의 수를 의미하며, 과적합을 제어하는 파라미터이다. 이 파라미터의 최적값은 훈련 데이터의 개수와 num_leaves에 의해 결정된다. 너무 큰 숫자로 설정하면 under-fitting이 일어날 수 있으며, 아주 큰 데이터셋이라면 적어도 수백~수천 정도로 가정하는 것이 편리하다.

sub_sample 파라미터는 과적합을 제어하기 위해 데이터를 샘플링하는 비율을 의미한다.

지금까지 설명한 num_leaves, max_depth, min_child_samples, sub_sample 파라미터가 Light GBM 파라미터 튜닝에 있어서 가장 중요한 파라미터들이다. 이들은 하나씩 튜닝할 수도 있고, 한 번에 튜닝할 수도 있다. 학습 데이터의 성격과 여유 시간에 따라 선택해야 한다. 이들에 대한 최적값을 어느 정도 확보했다면, 다음 단계로 넘어가도 좋다.

colsample_bytree 파라미터는 개별 트리를 학습할 때마다 무작위로 선택하는 피쳐의 비율을 제어한다. reg_alpha는 L1 규제를, reg_lambda는 L2 규제를 의미한다. 이들은 과적합을 제어하기에 좋은 옵션들이다.

learning_rate은 후반부에 건드리는 것이 좋은데, 초반부터 너무 작은 학습률을 지정하면 효율이 크게 떨어질 수 있기 때문이다. 정교한 결과를 위해, 마지막 순간에 더욱 좋은 결과를 도출하기 위해 영혼까지 끌어모으고 싶다면, learning_rate는 낮추고 num_estimators는 크게 하여 최상의 결과를 내보도록 하자.

다음은 위에서 처음 소개한 Santander Data를 바탕으로 한 예시이다.

params = {'max_depth': [10, 15, 20],
          'min_child_samples': [20, 40, 60],
          'subsample': [0.8, 1]}

grid = GridSearchCV(lgbm, param_grid=params)
grid.fit(X_train, Y_train, early_stopping_rounds=100, eval_metric='auc',
         eval_set=[(X_train, Y_train), (X_val, Y_val)])

print("최적 파라미터: ", grid.best_params_)
lgbm_roc_score = roc_auc_score(Y_test, grid.predict_proba(X_test)[:, 1], average='macro')
print("ROC AUC: {0:.4f}".format(lgbm_roc_score))

# 위 결과를 적용하여 재학습
lgbm = LGBMClassifier(n_estimators=1000, num_leaves=50, subsample=0.8,
                      min_child_samples=60, max_depth=20)

evals = [(X_test, Y_test)]
lgbm.fit(X_train, Y_train, early_stopping_rounds=100, eval_metric='auc',
         eval_set=evals, verbose=True)

score = roc_auc_score(Y_test, grid.predict_proba(X_test)[:, 1], average='macro')
print("ROC AUC: {0:.4f}".format(score))

Reference

LightGBM 공식 문서
논문 파이썬 머신러닝 완벽 가이드, 권철민, 위키북스

Comment  Read more

Seaborn Module 사용법

|

1. Seaborn 모듈 개요

Seaborn은 Matplotlib에 기반하여 제작된 파이썬 데이터 시각화 모듈이다. 고수준의 인터페이스를 통해 직관적이고 아름다운 그래프를 그릴 수 있다. 본 글은 Seaborn 공식 문서의 Tutorial 과정을 정리한 것임을 밝힌다.

그래프 저장 방법은 아래와 같이 matplotlib과 동일하다.

fig = plt.gcf()
fig.savefig('graph.png', dpi=300, format='png', bbox_inches="tight", facecolor="white")

2. Plot Aesthetics

2.1. Style Management

sns.set_style(style=None, rc=None)
:: 그래프 배경을 설정함

  • style = “darkgrid”, “whitegrid”, “dark”, “white”, “ticks”
  • rc = [dict], 세부 사항을 조정함

sns.despine(offset=None, trim=False, top=True, right=True, left=False, bottom=False)
:: Plot의 위, 오른쪽 축을 제거함

  • top, right, left, bottom = True로 설정하면 그 축을 제거함
  • offset = [integer or dict], 축과 실제 그래프가 얼마나 떨어져 있을지 설정함

만약 일시적으로 Figure Style을 변경하고 싶다면 아래와 같이 with 구문을 사용하면 된다.

data = np.random.normal(size=(20, 6)) + np.arange(6) / 2

with sns.axes_style("darkgrid"):
    plt.subplot(211)
    sns.violinplot(data=data)
    plt.subplot(212)
    sns.barplot(data=data)
    plt.show()

전체 Style을 변경하여 지속적으로 사용하고 싶다면, 아래와 같은 절차를 거치면 된다.

sns.axes_style()

# 배경 스타일을 darkgrid로 적용하고 투명도를 0.9로
sns.set_style("darkgrid", {"axes.facecolor": "0.9"})

# 혹은 간단하게 darkgrid만 적용하고 싶다면,
sns.set(style="darkgrid")

2.2. Color Management

현재의 Color Palette를 확인하고 싶다면 다음과 같이 코드를 입력하면 된다.

current_palette = sns.color_palette()
sns.palplot(current_palette)

우리는 이 Palette를 무궁무진하게 변화시킬 수 있는데, 가장 기본적인 테마는 총 6개가 있다.

deep, muted, pastel, bright, dark, colorblind

지금부터 color_palette 메서드를 통해 palette를 바꾸는 법에 대해 알아볼 것이다.

sns.color_palette(palette=None, n_colors=None)
:: color palette를 정의하는 색깔 list를 반환함

  • palette = [string], Palette 이름
  • n_colors = [Integer]

Palette에는 위에서 본 6가지 기본 테마 외에도, hls, husl, Set1, Blues_d, RdBu 등 수많은 matplotlib palette를 사용할 수 있다. 만약 직접 RGB를 설정하고 싶다면 아래와 같이 설정하는 것도 가능하다.

new_palette = ["#9b59b6", "#3498db", "#95a5a6", "#e74c3c", "#34495e", "#2ecc71"]

혹은 xkcd를 이용하여 이름으로 색깔을 불러올 수도 있다.

colors = ["windows blue", "amber", "greyish", "faded green", "dusty purple"]
sns.palplot(sns.xkcd_palette(colors))
  • Categorical Color Palette 대표 예시
    위에서부터 paired, Set2
  • Sequential Color Palette 대표 예시
    위에서부터 Blues, BuGn_r, GnBu_d

또 하나 유용한 기능은 cubehelix palette이다.

cubehelix_palette = sns.cubehelix_palette(8, start=2, rot=0.2, dark=0, light=.95,
                                          reverse=True, as_cmap=True)
x, y = np.random.multivariate_normal([0, 0], [[1, -0.5], [-0.5, 1]], size=300).T
cmap = sns.cubehelix_palette(dark=0.3, light=1, as_cmap=True)
sns.kdeplot(x, y, cmap=cmap, shade=True)

간단한 인터페이스를 원한다면 아래와 같은 방식도 가능하다.

pal = sns.light_palette("green", reverse=False, as_cmap=True)
palt = sns.dark_palette("purple", reverse=True, as_cmap=True)
pal = sns.dark_palette("palegreen", reverse=False, as_cmap=True)

양쪽으로 발산하는 Color Palette를 원한다면, 아래와 같은 방식으로 코드를 입력하면 된다.

diverging_palette = sns.color_palette("coolwarm", 7)
diverging_palette = sns.diverging_palette(h_neg=10, h_pos=200, s=85, l=25, n=7,
                                          sep=10, center='light', as_cmap=False)

# h_neg, h_pos = anchor hues, [0, 359]
# s: anchor saturation
# l: anchor lightness
# n: number of colors in the palette
# center: light or dark

아래 결과는 다음과 같다.

또한, 만약 color_palette 류의 메서드로 하나 하나 설정을 바꾸는 것이 아니라 전역 설정을 바꾸고 싶다면, set_palette를 이용하면 된다.

sns.set_palette('hust')

참고로 cubeleix palette를 이용하여 heatmap을 그리는 방법에 대해 첨부한다.

arr = np.array(np.abs(np.random.randn(49))).reshape((7,7))
mask = np.tril_indices_from(arr)
arr[mask] = False

palette = sns.cubehelix_palette(n_colors=7, start=0, rot=0.3,
                                light=1.0, dark=0.2, reverse=False, as_cmap=True)
sns.heatmap(arr, cmap=palette)
plt.show()

3. Plotting Functions

Seaborn의 Plotting 메서드들 중 가장 중요한 위치에 있는 메서드들은 아래와 같다.

메서드 기능 종류
relplot 2개의 연속형 변수 사이의 통계적 관계를 조명 scatter, line
catplot 범주형 변수를 포함하여 변수 사이의 통계적 관계를 조명 swarm, strip, box, violin, bar, point

데이터의 분포를 그리기 위해서는 distplot, kdeplot, jointplot 등을 사용할 수 있다.

3.1. Visualizing statistical relationships

sns.relplot(x, y, kind, hue, size, style, data, row, col, col_wrap, row_order, col_order, palette, …)
:: 2개의 연속형 변수 사이의 통계적 관계를 조명함, 각종 옵션으로 추가적으로 변수를 삽입할 수도 있음

  • hue, size, style = [string], 3개의 변수를 더 추가할 수 있음
  • col = [string], 여러 그래프를 한 번에 그릴 수 있게 해줌. 변수 명을 입력하면 됨
  • kind = [string], scatter 또는 line 입력
  • 자세한 설명은 이곳을 확인

3.1.1. Scatter plot

sns.relplot(x="total_bill", y="tip", size="size", sizes=(15, 200), data=tips)

3.1.2. Line plot

  • 일반적인 Line Plot
    df = pd.DataFrame(dict(time=np.arange(500),
                         value=np.random.randn(500).cumsum()))
    sns.relplot(x="time", y="value", kind="line", data=df)
    
  • 같은 x 값에 여러 y가 존재할 때 (Aggregation)
    데이터가 아래와 같이 생겼다고 가정하자. (timepoint 값에 여러 개의 signal 값이 존재하는 상황)
subject timepoint event region signal
s13 18 stim parietal -0.017
s5 14 stim parietal -0.081
s12 14 stim parietal -0.810
s11 18 stim parietal -0.0461
s10 18 stim parietal -0.0379

이 때, 위와 같은 경우에는 자연스럽게 Confidence Interval이 추가된다. 만약 이를 제거하고 싶으면, argument에 ci=None을 추가하면 되며, 만약 ci=”sd”로 입력하면, 표준편차가 표시된다.

sns.relplot(x="timepoint", y="signal", kind="line", data=fmri, ci="sd")

우측이 ci=”sd”이다.

여러 변수 사이의 관계를 탐구하기 위해 다음과 같은 그래프를 그릴 수도 있다.

pal = sns.cubehelix_palette(light=0.8, n_colors=2)
sns.relplot(x="timepoint", y="signal", hue="region", style="event",
            palette=pal, dashes=False, markers=True, kind="line", data=fmri)

3.1.3. 여러 그래프 한 번에 그리기

여러 그래프를 한 번에 그리고 싶다면 아래와 같은 방법을 사용하면 된다. 이는 다른 seaborn 메서드에도 두루 적용할 수 있는 방법이다. col에 지정된 변수 내 값이 너무 많으면, col_wrap[integer]을 통해 한 행에 나타낼 그래프의 수를 조정할 수 있다.

# Showing multiple relationships with facets
sns.relplot(x="timepoint", y="signal", hue="subject", col="region",
            row="event", height=3, kind="line", estimator=None, data=fmri)

3.2. Plotting with categorical data

범주형 변수를 포함한 여러 변수들의 통계적 관계를 조명하는 catplot은 kind=swarm, strip, box, violin, bar, point 설정을 통해 다양한 그래프를 그릴 수 있게 해준다.

sns.catplot(x, y, kind, hue, data, row, col, col_wrap, order, row_order, col_order, hue_order, palette, …)
:: 범주형 변수를 포함한 여러 변수들의 통계적 관계를 조명함

  • x, y, hue = [string], 그래프를 그릴 변수들의 이름
  • row, col = [string], faceting of the grid를 결정할 범주형 변수의 이름
  • 자세한 설명은 이곳을 확인

3.2.1. Categorical Scatterplots: strip, swarm

sns.catplot(x="day", y="total_bill", jitter=False, data=tips)
sns.catplot(x="day", y="total_bill", hue="sex", kind="swarm",
            order=["Sun", "Sat", "Thur", "Fri"], data=tips)

swarm 그래프는 그래프 포인트끼리 겹치는 것을 막아준다. (overlapping 방지) 가로로 그리고 싶으면, x와 y의 순서를 바꿔주면 된다.

3.2.2. Distribution of observations within categories: box, violin

위와 같은 그래프에서 분포를 잘 알아보기 위해서는 다음과 같은 기능을 사용하면 된다.

tips["weekend"] = tips["day"].isin(["Sat", "Sun"])

sns.catplot(x="day", y="total_bill", hue="weekend", orient='v',
            kind="box", dodge=False, data=tips, legend=True)
sns.catplot(x="total_bill", y="day", hue="time",
            kind="violin", bw=.15, cut=0, data=tips)

그래프 내부에 선(Inner Stick)을 추가하고 싶거나, Scatter Plot과 Distribution Plot을 동시에 그리고 싶다면 아래의 기능을 사용하면 된다.

# Inner Stick 사용
sns.violinplot(x="day", y="total_bill", hue="sex", data=tips,
               split=True, inner="stick", palette="Set3")

# 결합: 분포와 실제 데이터까지 한번에 보여주는 방법
g = sns.catplot(x="day", y="total_bill", kind="violin", inner=None, data=tips)
sns.swarmplot(x="day", y="total_bill", color="k", size=3, data=tips, ax=g.ax)

3.2.3. Statistical Estimation within categories: barplot, countplot, pointplot

아래는 기본적인 Barplot, Countplot, Pointplot을 그리는 방법에 대한 소개이다.

# bar
sns.catplot(x="sex", y="survived", hue="class", kind="bar", data=titanic)

# 그냥 count를 세고 싶다면
sns.catplot(x="deck", kind="count", palette="ch:.25", data=titanic)

# point
sns.catplot(x="class", y="survived", hue="sex", data=titanic,
               palette={"male": "g", "female": "m"}, kind="point",
               markers=["^", "o"], linestyles=["-", "--"])

3.2.4. Showing multiple relationships with facets

# catplot 역시 relplot 처럼 col argument를 사용해 여러 그래프를 그릴 수 있음
sns.catplot(x="day", y="total_bill", hue="smoker",
            col="time", aspect=0.6, kind="swarm", data=tips)

3.3. Visualizing the distribution of a dataset

3.2.1. 일변량 분포

sns.distplot(a, bins, hist=True, rug=False, fit=None, color=None, vertical=False, norm_hist=False, axlabel, label, ax …)
:: 관찰 값들의 일변량 분포를 그림

  • a = [Series, 1d array, list], Observed data이며, Series에 name 속성이 있다면 이것이 label로 사용될 것임
  • hist = [bool], True면 히스토그램을 그림
  • 자세한 설명은 이곳을 확인

다음은 예시이다.

x = np.random.normal(size=100)
sns.distplot(x, hist=True, bins=20, kde=True, rug=True)

sns.kdeplot(data, data2, shade=False, vertical=False, kernel=’gau’, bw=’scott’, gridsize=100, cut=3, legend=True …)
:: 일변량 or 이변량의 Kernel Density Estimate 그래프를 그림

  • data = [1d array-like], Input Data
  • data2 = [1d array-like], 2번째 Input Data, 옵션이며 추가할 경우 이변량 KDE가 그려질 것임
  • bw = {‘scott’, ‘silverman’, scalar, pair of scalars}, kernel size를 결정함, 히스토그램에서의 bin size와 유사한 역할을 수행함, bw 값이 작을 수록 실제 데이터에 근접하게 그래프가 그려짐
  • gridsize = [int], Evaluation Grid에서의 이산형 포인트의 개수
  • cut = [scalar], 그래프의 시작 지점을 설정함
  • 자세한 설명은 이곳을 확인
sns.kdeplot(x, shade=True, bw=0.2, label='bw: 0.2')
sns.kdeplot(x, shade=False, bw=2, label='bw: 2')
plt.legend(loc='best')

x = np.random.gamma(6, size=200)
sns.distplot(x, kde=False, fit=stats.gamma)

3.2.2. 이변량 분포

# Prep
mean, cov = [0, 1], [(1, .5), (.5, 1)]
data = np.random.multivariate_normal(mean, cov, 200)
df = pd.DataFrame(data, columns=["x", "y"])
x, y = np.random.multivariate_normal(mean, cov, 1000).T
cmap = sns.cubehelix_palette(as_cmap=True, start=0, rot=0.5, dark=0, light=0.9)

# Scatter plot
sns.jointplot(x="x", y="y", data=df, color='k')

# Hexbin plot
with sns.axes_style("white"):
    sns.jointplot(x=x, y=y, kind="hex", cmap=cmap)

# Kernel Density Estimation
sns.jointplot(x="x", y="y", data=df, kind="kde", cmap=cmap)

이변량 분포에서 KDE와 rug를 결합하면 아래와 같은 그래프를 그릴 수도 있다.

f, ax = plt.subplots(figsize=(6, 6))
sns.kdeplot(df.x, df.y, ax=ax)
sns.rugplot(df.x, color="g", ax=ax)
sns.rugplot(df.y, vertical=True, ax=ax);

만약 Density의 연속성을 부드럽게 표현하고 싶다면, Contour Level을 조 정하면 된다.

cmap = sns.cubehelix_palette(as_cmap=True, dark=0, light=1, reverse=True)
sns.kdeplot(df.x, df.y, cmap=cmap, n_levels=60, shade=True)

3.2.3. Pairwise 관계 시각화

iris = sns.load_dataset('iris')
sns.pairplot(iris)

g = sns.PairGrid(iris)
g.map_diag(sns.kdeplot)
g.map_offdiag(sns.kdeplot, cmap="Blues_d", n_levels=6);

4. Multi-plot grids

이곳을 참조할 것


Reference

Seaborn 공식 문서

Comment  Read more

Imbalanced Learning

|

1. Imbalanced Learning (불균형 학습) 개요

비정상 거래 탐지와 같은 케이스의 경우, 정상적인 거래 보다는 정상 범위에서 벗어난 것으로 판단되는 거래 기록의 비중이 현저하게 작을 것이다. 그런데 보통의 알고리즘으로 이러한 비정상 거래를 찾아내기 에는 이러한 데이터의 불균형이 중요한 이슈로 작용하는데, 본 글에서는 이러한 불균형 학습과 관련된 논의를 해보고자 한다.

알고리즘 자체로 Class 불균형을 해소하는 방법을 제외하면, Over-Sampling과 Under-Sampling 방법이 가장 대표적인 방법이라고 할 수 있다.

1.1. Over-Sampling

Over-Sampling은 부족한 데이터를 추가하는 방식으로 진행되며, 크게 3가지로 구분할 수 있다.

첫 번째 방법은 무작위 추출인데, 단순하게 랜덤하게 부족한 Class의 데이터를 복제하여 데이터셋에 추가하는 것이다.

두 번째 방법은 위와 달리 기존 데이터를 단순히 복사하는 것에 그치지 않고, 어떠한 방법론에 의해 합성된 데이터를 생성하는 것이다. 이후에 설명할 SMOTE 기법이 본 방법의 대표적인 예에 해당한다.

세 번째 방법은 어떤 특별한 기준에 의해 복제할 데이터를 정하고 이를 실행하는 것이다.

1.2. Under-Sampling

Over-Sampling과 반대로 Under-Sampling은 정상 데이터의 수를 줄여 데이터셋의 균형을 맞추는 것인데, 주의해서 사용하지 않으면 매우 중요한 정보를 잃을 수도 있기 때문에 확실한 근거를 바탕으로 사용해야 하는 방법이다.

Under-Sampling의 대표적인 예로는 RUS가 있고, 이는 단순히 Random Under Sampling을 뜻한다.


2. SMOTE 기법

SMOTE는 Synthetic Minority Oversampling TEchnique의 약자로, 2002년에 처음 등장하여 현재(2019.10)까지 8천 회가 넘는 인용 수를 보여주고 있는 Over-Sampling의 대표적인 알고리즘이다.

알고리즘의 원리 자체는 간단하다. Boostrap이나 KNN 모델 기법을 기반으로 하는데, 그 원리는 다음과 같다.

  • 소수(위 예시에선 비정상) 데이터 중 1개의 Sample을 선택한다. 이 Sample을 기준 Sample이라 명명한다.
  • 기준 Sample과 거리(유클리드 거리)가 가까운 k개의 Sample(KNN)을 찾는다. 이 k개의 Sample 중 랜덤하게 1개의 Sample을 선택한다. 이 Sample을 KNN Sample이라 명명한다.
  • 새로운 Synthetic Sample은 아래와 같이 계산한다. \(X_{new} = X_i + (X_k - X_i) * \delta\)

    $X_{new}$: Synthetic Sample
    $X_i$: 기준 Sample
    $X_k$: KNN Sample
    $\delta$: 0 ~ 1 사이에서 생성된 난수

본 과정을 일정 수 만큼 진행하면 아래 그림과 같이 새로운 합성 데이터가 생성됨을 알 수 있다.

간단한 예시를 보면,

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from imblearn.over_sampling import SMOTE
from sklearn.preprocessing import MinMaxScaler

x, y = make_classification(n_features=2, n_informative=2, n_samples=20, weights= [0.8, 0.2],
                           n_redundant=0, n_clusters_per_class=1, random_state=0)
scaler = MinMaxScaler(feature_range=(0, 1))
x = scaler.fit_transform(x)

# SMOTE 이전
df1 = pd.DataFrame(np.concatenate([x, y.reshape(-1, 1)], axis=1),
                  columns=['col1', 'col2', 'result'])
sns.relplot(x='col1', y='col2', hue='result', data=df1)
plt.show()

# SMOTE 이후
sm = SMOTE(ratio='auto', kind='regular', k_neighbors=3)
X, Y = sm.fit_sample(x, list(y))

df2 = pd.DataFrame(np.concatenate([X, Y.reshape(-1, 1)], axis=1),
                  columns=['col1', 'col2', 'result'])

sns.relplot(x='col1', y='col2', hue='result', data=df2)
plt.show()

다음 그림들에서 위는 SMOTE 이전의 데이터를, 아래는 SMOTE 이후의 데이터 분포를 보여준다.


3. 추가할 것

MSMOTE, Borderline SMOTE, Adasyn


Reference

참고 블로그
파이썬 머신러닝 완벽 가이드, 권철민, 위키북스

Comment  Read more

Contextual Bandit and Tree Heuristic

|

1. Contextual Bandit의 개념

Contextual Bandit 문제를 알기 위해선 Multi-Armed Bandit 문제의 개념에 대해 숙지하고 있어야 한다.
위 개념에 대해 알기를 원한다면 여기를 참고하기 바란다.

Multi-Armed Bandit 문제에서 Context 개념이 추가된 Contextual Bandit 문제는 대표적으로 추천 시스템에서 활용될 수 있다. 단 전통적인 추천 시스템을 구축할 때는 Ground Truth y 값, 즉 실제로 고객이 어떠한 상품을 좋아하는지에 대한 해답을 안고 시작하지만, Contextual Bandit과 관련된 상황에서는 그러한 이점이 주어지지 않는다.

그림을 통해 파악해보자.

첫 번째 그림은 전통적인 추천시스템에 관한 것이고, 두 번째 그림은 Contextual Bandit 문제와 관련된 것이다.

온라인 상황에서 우리가 고객에게 어떠한 상품을 제시하였을 때, 고객이 그 상품을 원하지 않는다면 우리는 새로운 시도를 통해 고객이 어떠한 상품을 좋아할지 파악하도록 노력해야 한다. 이것이 바로 Exploration이다.

만약 고객이 그 상품에 호의적인 반응을 보였다면, 이 또한 중요한 데이터로 적재되어 이후에 동일 혹은 유사한 고객에게 상품을 추천해 주는 데에 있어 이용될 것이다. 이 것이 Exploitation이다.

위 그림에 나와 있듯이, Contextual Bandit 문제 해결의 핵심은, Context(고객의 정보)를 활용하여 Exploitation과 Exploration의 균형을 찾아 어떤 Action을 취할 것인가에 대한 효과적인 학습을 진행하는 것이다.


2. Lin UCB

Lin UCB는 A contextual-bandit approach to personalized news article recommendation논문에 처음 소개된 알고리즘으로, Thompson Sampling과 더불어 Contextual Bandit 문제를 푸는 가장 대표적이고 기본적인 알고리즘으로 소개되어 있다.

이 알고리즘의 기본 개념은 아래와 같다.

Context Vector를 어떻게 구성할 것인가에 따라 Linear Disjoint Model과 Linear Hybrid Model로 구분된다. Hyperparameter인 Alpha가 커질 수록 Exploration에 더욱 가중치를 두게 되며, 결과는 이 Alpha에 다소 영향을 받는 편이다.

본 알고리즘은 이후 Tree Heuristic과의 비교를 위해 테스트 용으로 사용될 예정이다.


3. Tree Heuristic

3.1 Tree Boost

Tree Heuristic에 접근하기 위해서는 먼저 그 전신이라고 할 수 있는 Tree Boost 알고리즘에 대해 알아야 한다. 본 알고리즘은 A practical method for solving contextual bandit problems using decision trees 논문에서 소개되었다.

Tree Boost는 Thompson Sampling의 개념을 차용하여 설계된 알고리즘이다. 위의 Lin UCB가 Context와 Reward 사이의 관계를 선형적으로 정의하였다면, 본 알고리즘은 Tree 계열의 모델로써 이 관계를 정의한다.

Tree Boost의 작동 원리를 알아 보자. 한 고객의 정보가 입수되었다. 이 정보는 1개의 Context Vector라고 할 수 있다. 우리가 취할 수 있는 Action이 총 k개 있다고 가정하면, 각각의 Action과 연결된 Tree 모델에 방금 입수한 Context Vector를 투입하고 Reward가 1이 될 확률(Score)값을 얻는다. 가장 높은 값을 갖는 Action을 선택하여 고객에게 제시한다.

제시가 끝난 후에 고객의 반응(Reward가 1인지, 0인지)이 확인되었다면, 이를 제시하였던 Action의 Sub-data에 적재한다. 즉, 각 데이터(Design Matrix)는 제시한 Action의 Sub-data에 소속되는 것이다. 이 Sub-data들을 모두 모으면 전체 데이터가 구성된다.

Sub-data 내에서 부트스트랩을 여러 번 진행하고 그 중 하나를 선택하여 Tree 모델에 적합시키는데, 이 과정이 Exploration 과정에 해당하며, 선택된 데이터셋과 Tree 모델은 Thompson Sampling에서 사용되는 샘플 1개에 해당한다.

이후에 설명하겠지만, Tree Boost의 성능은 뛰어난 편이다. 그러나 이 모든 과정을 거치기에는 굉장히 많은 시간이 소요되며, 신속성이 중요한 평가 포인트라고 할 수 있는 Contextual Bandit 알고리즘들 사이에서 현실적으로 우위를 보이기는 어려운 것이 사실이다. 따라서 아래에 있는 Tree Heuristic이라는 알고리즘이 제시되었다고 볼 수 있다.

3.2 Tree Heuristic

Tree Boost와의 가장 큰 차이점은 바로, 한 Trial에 한 번만 적합을 진행하여 속도를 향상시켰다는 점이다. Tree Boost의 경우 각 Action 마다 부트스트랩 과정을 여러 번 시키고, 또 선택된 데이터에 Action 수 만큼 모델을 적합해야 했기 때문에 굉장히 오랜 시간이 소요되었는데 Tree Heuristic은 그러한 과정을 겪을 필요가 없는 것이다.

알고리즘의 실질적인 작동원리는 아래 그림과 코드를 보면 상세히 설명되어 있다.

"""
Tree Heuristic Implementation with Striatum Module
본 알고리즘은 Striatum Module의 가장 기본적인 class들을 활용하였음
"""

import time
import warnings
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from striatum.bandit.bandit import BaseBandit
from striatum.storage import history, action, model

from sklearn.externals.joblib import Parallel, delayed
from sklearn.multiclass import _fit_binary, OneVsRestClassifier
from sklearn.preprocessing import LabelBinarizer
from sklearn.tree import DecisionTreeClassifier


# 터미널을 클린하게 해야 함
warnings.simplefilter(action='ignore', category=FutureWarning)
warnings.simplefilter(action='ignore', category=UserWarning)


class CustomOneVsRestClassifier(OneVsRestClassifier):
    """
    현재 scikit-learn의 OneVsRestClassifier class 의 경우,
    내부에 있는 Classifier 객체들이 독립적이지 않아 개별 접근이 불가능함
    따라서 개별 접근이 가능하게 (각 Action 별로 다른 모델이 필요하므로)
    본 클래스를 수정해주어야 함

    참조: https://www.oipapio.com/question-3339267
    """

    def __init__(self, estimators, n_jobs=1):
        super(CustomOneVsRestClassifier, self).__init__(estimators, n_jobs)
        self.estimators = estimators
        self.n_jobs = n_jobs

    def fit(self, X, y):
        self.label_binarizer_ = LabelBinarizer(sparse_output=True)
        Y = self.label_binarizer_.fit_transform(y)
        Y = Y.tocsc()
        self.classes_ = self.label_binarizer_.classes_
        columns = (col.toarray().ravel() for col in Y.T)

        # This is where we change the training method
        self.estimators_ = Parallel(n_jobs=self.n_jobs)(delayed(_fit_binary)(
            estimator, X, column, classes=[
                "not %s" % self.label_binarizer_.classes_[i],
                self.label_binarizer_.classes_[i]])
            for i, (column, estimator) in enumerate(zip(columns, self.estimators)))
        return self


class RecommendationCls(object):
    """
    우리가 추천한 Action 의 정보들을 저장할 클래스
    """
    def __init__(self, action, score, reward=None):
        self.action = action
        self.score = score
        self.reward = reward


class TreeHeuristic(BaseBandit):
    """
    Tree Heuristic Algorithm:
    Context 와 Reward 의 관계를 Tree Model 로서 정의내리고,
    Decision Tree 의 학습결과에 기반하여 Beta 분포 Sampling 을 진행하여 Action 을 선택하는 알고리즘임
    """

    def __init__(self,
                 history_storage,
                 model_storage,
                 action_storage,
                 n_actions,
                 ):
        super(TreeHeuristic, self).__init__(history_storage, model_storage, action_storage,
                                            recommendation_cls=RecommendationCls)

        # 1) history_storage 에는 매 trial 에서 진행되었던 기본적인 record 가 담겨 있음
        # 2) model_storage 는 Lin UCB 에서는 model parameter 가 저장되는 공간인데, 본 알고리즘에선 사실 쓰임새는 없음
        # 3) action_storage 에는 선택된 Action 의 ID 와 Score 가 저장됨

        # oracle: Action 수 만큼의 Decision Tree 를 담고 있음
        # n_actions: Action 수
        # n_features: Feature 수
        # D: Action 별로 적재한 데이터, 딕셔너리구조이며 value 자리에는 각 Action 에 맞는 np.array 가 적재됨
        # first_context = 첫 손님, 처음 Input 으로 주어지는 Context
        #               -> 얘를 저장하여 가짜 데이터를 만듦, build 메서드를 참고

        self.oracles = CustomOneVsRestClassifier([DecisionTreeClassifier() for i in range(n_actions)])
        self.n_actions = n_actions
        self.n_features = None
        self.D = None
        self.first_context = None

    def build(self, first_context, actions):
        """
        1) first_context 저장 및 n_features 저장
        2) Action objects 를 self._action_storage 에 저장함
        3) 가짜 데이터를 집어 넣어 D 를 만듦
        4) 초기 fitting 을 진행 함

        :param first_context: np.array (n_features, ) 첫 번째 context
        :param actions: list of action objects(Striatum 모듈 기본 class), action 의 종류를 담고 있음
        """
        self.first_context = first_context
        self.n_features = first_context.shape[0]

        self._action_storage.add(actions)

        # Add Fabricated Data
        # 적합을 진행하려고 하는데 만약 Label 이 오직 0만 존재한다거나 하는 상황이 오면
        # Classifier 를 그 데이터에 적합시키는 것은 불가능함
        # 가짜 데이터를 D 에 미리 적재함으로써 이 문제를 해결함 (논문 참조)
        # 데이터의 개수가 늘어날 수록 이 가짜 데이터의 영향력은 약화됨
        # D 에서 각 Action 에 맞는 np.array 의 마지막 열은 실제 Reward 값이며, 그 외의 열에는 Feature 값이 들어감
        x1, x2 = np.append(first_context, 0), np.append(first_context, 1)
        X = np.array([x1, x2])

        D = {action_id: X for action_id in self._action_storage.iterids()}

        oracles = self.oracles

        # 위에서 만든 가짜 데이터를 적합함
        for index, action_id in enumerate(list(self._action_storage.iterids())):
            oracle = oracles.estimators[index]
            oracle.fit(D[action_id][:, :-1], D[action_id][:, -1])

        self.D = D
        self.oracles = oracles

    def sample_from_beta(self, context):
        """
        :param context: np.array (n_features, ), 고객 1명의 context 임
        :return: history_id -- 저장 기록 index
                 recommendations -- 수행한 action 과 그 action 의 score 를 저장하는 class,
                                    위에서 만든 RecommendationCls class 의 Instance 임

        아래 loop 내의 코드는 Decision Tree 내부에 접근하는 과정을 다루고 있음
        접근 방법 참고:
        https://lovit.github.io/machine%20learning/2018/04/30/get_rules_from_trained_decision_tree/
        """
        oracles = self.oracles
        samples = []

        # Prediction 을 위해 reshaping 을 해줌
        context_vector = context.reshape(1, -1)

        for i, action_id in enumerate(list(self._action_storage.iterids())):
            oracle = oracles.estimators[i]

            # 각 DT 모델에 context 를 투입하여 당도한 leaf node 의 index 를 얻음
            leaf_index = oracle.apply(context_vector)[0]

            # 해당 leaf node 의 n0, n1 값을 얻음
            # n0: number of failure in the leaf node selected
            # n1: number of success in the leaf node selected
            n0 = oracle.tree_.value[leaf_index][0][0]
            n1 = oracle.tree_.value[leaf_index][0][1]

            # 이를 베타분포에 반영해주고, 여기서 sampling 을 진행함

            sample = np.random.beta(a=1 + n1, b=1 + n0, size=1)
            samples.append(sample)

        # Sample 값 중 가장 높은 값을 갖는 Action 을 선택함
        target = np.argmax(samples)
        recommendation_id = list(self._action_storage.iterids())[target]

        recommendations = self._recommendation_cls(
            action=self._action_storage.get(recommendation_id),
            score=np.max(samples)
        )

        history_id = self._history_storage.add_history(context, recommendations)

        return history_id, recommendations

    def update_D(self, action_id, context, reward):
        """
        추천한 Action 의 결과로 받은 Reward 와 Context 를 결합하여 데이터 딕셔너리 D 업데이트를 진행 함

        :param action_id: integer, D 에서 어떤 데이터를 업데이트할지 결정함
        :param context: np.array (n_samples, ), 고객 1명의 context 임
        :param reward: 실제 Reward -- 0 또는 1
        """
        D = self.D

        # new_data: context 와 reward 를 붙인 np.array
        new_data = np.append(context, reward).reshape(1, -1)

        # 해당 Action 의 데이터에 적재함
        D[action_id] = np.append(D[action_id], new_data, axis=0)

        self.D = D

    def update_tree(self, action_id):
        """
        해당 Action 에 소속된 Decision Tree 를 적합하여 업그레이드 함

        :param action_id: integer
        """
        D = self.D
        oracles = self.oracles

        action_index = list(self._action_storage.iterids()).index(action_id)
        oracle = oracles.estimators[action_index]
        oracle.fit(D[action_id][:, :-1], D[action_id][:, -1])

        self.oracles = oracles

    def reward(self, history_id, rewards):
        """
        self._history_storage.unrewarded_histories 에 있는,
        아직 Reward 를 받지 못한 기록들을 제거함

        :param history_id: Integer, sample_from_beta 메서드의 output
        :param rewards: Dictionary, {action_id : 0 or 1}
        """
        self._history_storage.add_reward(history_id, rewards)

    def add_action(self, actions):
        """
        새로운 Action 이 추가되었을 때,
        1) action_storage 를 업데이트하고
        2) D 에 새로운 가짜 데이터를 적재하며
        3) oracle 에 새로 추가된 Action 의 개수만큼 Decision Tree 를 추가하여
        4) 앞서 만든 가짜 데이터에 적합함

        :param actions: set of actions
        """

        oracles = self.oracles
        x = self.first_context
        D = self.D

        self._action_storage.add(actions)

        num_new_actions = len(actions)

        # 새롭게 정의된 Decision Tree 에 적합을 시작할 수 있게 기본 (가짜) 데이터셋을 넣어줌
        # 이어서 새롭게 Decision Tree 들을 추가된 Action 의 개수 만큼 만들어준 이후
        # 각 Action 에 매칭되는 Decision Tree 에 적합함
        x1, x2 = np.append(x, 0), np.append(x, 1)
        X = np.array([x1, x2])

        new_trees = [DecisionTreeClassifier() for j in range(num_new_actions)]

        for new_action_obj, new_tree in zip(actions, new_trees):
            # 여기서 new_action_obj 는 Striatum 패키지의 기본 class 로 짜여 있어
            # 그 class 의 attribute 인 id 를 불러와야 integer 인 action_id 를 쓸 수 있음
            new_action_id = new_action_obj.id
            D[new_action_id] = X
            new_tree.fit(D[new_action_id][:, :-1], D[new_action_id][:, -1])

            # 새로 적합한 Decision Tree 를 추가해 줌
            oracles.estimators.append(new_tree)

        self.oracles = oracles
        self.D = D

    def remove_action(self, action_id):
        """
        이제는 필요 없어진 Action을 제거한다.

        :param action_id: integer
        """
        D = self.D
        self._action_storage.remove(action_id)

        del D[action_id]

        self.D = D


# Preparation
def make_arm(arm_ids):
    """
    선택할 수 있는 Action 의 리스트를 받아
    Striatum 모듈의 Action Object 로 변환함

    이 작업을 거쳐야 위 Action Object 들을 Tree Heuristic 과 같은 Contextual Bandit class 의
    내부 Attribute 인 _action_storage 에 저장할 수 있음

    :param arm_ids: list,
    :return:
    """
    arms = []
    for arm_id in arm_ids:
        arm = action.Action(arm_id)
        arms.append(arm)
    return arms


# Training: Movie Lens Data
def train_movielens(max_iter=163683, batch_size=100):
    # 데이터 전처리 방법에 대해 알고자 한다면...
    # 참고: https://striatum.readthedocs.io/en/latest/auto_examples/index.html#general-examples

    streaming_batch = pd.read_csv('streaming_batch.csv', sep='\t', names=['user_id'], engine='c')
    user_feature = pd.read_csv('user_feature.csv', sep='\t', header=0, index_col=0, engine='c')
    arm_ids = list(pd.read_csv('actions.csv', sep='\t', header=0, engine='c')['movie_id'])
    reward_list = pd.read_csv('reward_list.csv', sep='\t', header=0, engine='c')

    streaming_batch = streaming_batch.iloc[0:max_iter]

    # 아래 n_actions 인자에서 처음 시점에서의 Action 의 개수를 정의 함
    th = TreeHeuristic(history.MemoryHistoryStorage(), model.MemoryModelStorage(),
                       action.MemoryActionStorage(), n_actions=50)
    actions = make_arm(arm_ids=arm_ids)

    reward_sum = 0
    y = []

    print("Starting Now...")
    start = time.time()

    for i in range(max_iter):
        context = np.array(user_feature[user_feature.index == streaming_batch.iloc[i, 0]])[0]

        if i == 0:
            th.build(first_context=context, actions=actions)

        history_id, recommendations = th.sample_from_beta(context=context)

        watched_list = reward_list[reward_list['user_id'] == streaming_batch.iloc[i, 0]]

        if recommendations.action.id not in list(watched_list['movie_id']):
            # 잘 못 맞췄으면 0점을 얻음
            th.reward(history_id, {recommendations.action.id: 0.0})
            th.update_D(context=context, action_id=recommendations.action.id, reward=0.0)

        else:
            # 잘 맞춨으면 1점을 얻음
            th.reward(history_id, {recommendations.action.id: 1.0})
            th.update_D(context=context, action_id=recommendations.action.id, reward=1.0)
            reward_sum += 1

        if i % batch_size == 0 and i != 0:
            for action_chosen in th._action_storage.iterids():
                th.update_tree(action_id=action_chosen)

        if i % 100 == 0:
            print("Step: {} -- Average Reward: {}".format(i, np.round(reward_sum / (i+1), 4)))

        y.append(reward_sum / (i + 1))

    print("Time: {}".format(time.time() - start))
    x = list(range(max_iter))
    plt.figure()
    plt.plot(x, y, c='r')
    plt.title("Cumulative Average Reward of \n Tree Heuristic: Movie Lens Data")
    plt.show()


# Training: Cover Type Data
def train_covtype(n_samples=581000, batch_size=3000):
    file = pd.read_csv("covtype.data", header=None)
    data = file.values
    np.random.shuffle(data)

    X, temp = data[:, 0:54], data[:, 54]
    Y = pd.get_dummies(temp).values

    actions = make_arm(list(range(7)))

    th = TreeHeuristic(history.MemoryHistoryStorage(), model.MemoryModelStorage(),
                       action.MemoryActionStorage(), n_actions=7)

    th.build(first_context=X[0], actions=actions)

    reward_sum = 0
    y = []

    print("Starting Now...")
    start = time.time()

    for i in range(n_samples):

        context = X[i]
        history_id, recommendations = th.sample_from_beta(context=context)

        # 실제 Reward 를 받고 이를 누적함
        actual_reward = Y[i, recommendations.action.id]
        reward_sum += actual_reward

        th.reward(history_id, {recommendations.action.id: actual_reward})

        # D는 매 trial 마다 업데이트해 주어야 함
        th.update_D(context=context, action_id=recommendations.action.id, reward=actual_reward)

        # batch size 만큼을 모아서 적합해줌
        if i % batch_size == 0 and i != 0:
            for action_chosen in th._action_storage.iterids():
                th.update_tree(action_id=action_chosen)

        # 로그는 100개 마다 찍음
        if i % 100 == 0:
            print("Step: {} -- Average Reward: {}".format(i, np.round(reward_sum / (i+1), 4)))

        y.append(reward_sum/(i+1))

    print("Time: {}".format(time.time() - start))
    x = list(range(n_samples))
    y[0] = 0
    plt.figure()
    plt.plot(x, y, c='r')
    plt.title("Cumulative Average Reward Flow of \n Tree Heuristic: Cover type Data")
    plt.show()

Test는 전통적으로 자주 애용되었던 Movielens 데이터와 Covtype 데이터로 진행할 수 있다. 아래 속도와 관련된 지표는 GPU가 없는 Laptop에 의한 것임을 밝혀둔다.

위 두 데이터의 경우, Tree Heuristic 알고리즘이 Lin UCB보다 우수한 성능을 보이는 것으로 확인되었다. 비록 Lin UCB보다는 속도 면에서 열위를 보이기는 하지만, Tree 구조에 기반한 모델이므로 해석에 있어 강점을 보일 수 있다는 점과 우수한 성능 때문에 충분히 기능할 수 있는 알고리즘으로 판단된다.

Test1: Covtype Data

알고리즘 10% Dataset

(58,100)
20% Dataset

(116,200)
50% Dataset

(290,500)
100% Dataset

(581,000)
비고
Lin UCB 0.7086

(23.66초)
0.7126

(49.39초)
0.7165

(137.19초)
0.7180

(5분 39초)
alpha=0.2
Tree Heuristic 0.7154

(100.65초)
0.7688

(6분 48초)
0.8261

(2463.70초)
0.8626

(2시간 37분)
3000 trial이

지날 때 마다 적합

Test2: Movielens Data

알고리즘 10% Dataset

(16,400)
20% Dataset

(32,700)
50% Dataset

(81,800)
100% Dataset

(163,600)
비고
Lin UCB 0.7521 0.7668 0.7746 0.7567

(6분 14초)
alpha=0.2
Tree Heuristic 0.7683 0.8017 0.8183 0.8346

(33분 16초)
100 trial이

지날 때 마다 적합

Reference

Lin UCB 논문 Tree Heuristic 논문

Comment  Read more

OpenAI GPT-2 - Language Models are Unsupervised Multitask Learners(GPT2 논문 설명)

|

이 글에서는 2019년 2월 Alec Radford 등이 발표한 OpenAI GPT-2: Language Models are Unsupervised Multitask Learners를 살펴보도록 한다.

코드와 논문은 여기에서 볼 수 있지만, 전체버전은 성능이 너무 강력하다는 이유로 공개되지 않았다.

중요한 부분만 적을 예정이므로 전체가 궁금하면 원 논문을 찾아 읽어보면 된다.


OpenAI GPT-2 - Language Models are Unsupervised Multitask Learners

논문 링크: OpenAI GPT-2 - Language Models are Unsupervised Multitask Learners

홈페이지: OpenAI

Tensorflow code: Official Code

초록(Abstract)

질답(QA), 기계번역, 독해, 요약과 같은 자연어처리 과제들은 대개 과제에 특화된 dataset과 지도학습을 통해 이루어졌다. 이 논문에서, 언어모델은 WebText라는 수백만 개의 웹페이지를 모은 새로운 dataset에서 학습될 때 어떤 명시적인 지도 없이 이러한 과제들을 학습하기 시작했음을 보인다. 문서와 질문이 있을 때 대답을 생성하는 언어모델은 CoQA dataset에서 55 F1 score를 달성하였고 이는 127k 이상의 학습데이터 사용 없이 4개 중 3개의 기준시스템을 능가한 것이다.
이 언어모델의 capacity는 zero-shot task transfer의 성공에 필수적이다. 이 논문에서 제시되는 가장 큰 모델 GPT-2는 15억 개의 Transformer parameter를 가지며 zero-shot 환경에서 8개 중 7개에서 state-of-the-art를 달성하였는데 이 큰 모델도 WebText에서 과소적합(underfitting) 현상을 보인다. 모델에서 나온 예는 이러한 개선점을 반영하며 일관성 있는 텍스트 단락을 포함한다. 이러한 발견은 자연적 설명으로부터 과제수행능력을 배우는 언어처리모델을 개발하는 촉망되는 방법을 시사한다.


1. 서론(Introduction)

기계학습 시스템은 큰 dataset과 고용량의 모델, 지도학습 등을 통해 빠르게 발전해왔다. 이러한 방법으로 개발된 모델들은 불안정하며 매우 좁은 범위의 문제에서만 뛰어난 능력을 발휘한다. 그래서 데이터를 수동 분류하는 과정 없이도 더 범용적인 모델을 개발할 필요가 있다.

현재 기계학습체계를 개발하는 주된 방법은 목표 과제에 맞는 dataset을 찾아서, 이를 학습/검증 단계로 나누어 학습 후 IID(independet and identically distributed)로 성능을 측정하는 방법이다. 이는 좁은 범위의 과제에서는 매우 효과적이나 범용적인 이해를 필요로 하는 독해나 다양한 이미지 분류시스템 등의 문제에서는 높은 성능을 내지 못했다.
많은 연구가 단일 영역의 dataset과 단일 과제에만 맞춘 학습에만 치중되었었다. 최근에야 넓은 범위의 dataset과 여러 과제에 대한 GLUE benchmark 등이 제안되기 시작했다.

다중작업 학습(Multitask learning)은 일반성능을 높이는 유망한 방법이지만 아직 초기 연구 단계이다. 최근에는 Learning and Evaluating General Linguistic Intelligence 등의 연구가 성능 향상을 이뤄냈지만, 최근의 기계학습 시스템은 일반화를 위해서는 수십만 개 정도의 학습 샘플을 필요로 하는데 이는 다중작업 학습을 위해서는 그 몇 배가 필요하다는 것을 뜻한다.

가장 성능이 높은 언어처리모델은 사전학습(pre-training)과 지도 세부학습(supervised fine-tuning)의 결합으로 만들어졌다. 이 접근법은 transfer과 더불어 긴 역사를 가졌다.

  • 단어벡터를 학습시킨 후 과제특화된(task-specific) 모델구조에 입력으로 넣고
  • 순환형 네트워크의 문맥표현이 전이되고
  • 최근에는 과제특화된 모델구조는 더 이상 중요하지 않으며 많은 self-attention block만으로 충분하다고 한다.

이러한 방법들은 여전히 지도학습을 필요로 한다. 만약 지도 데이터가 최소한으로 또는 전혀 필요하지 않다면, 일반상식 추론이나 감정분석과 같은 특정 과제들을 수행하는 데 큰 발전이 있을 것이다.

이 논문에서는, 위의 두 방향의 연구를 결합하여 전이학습의 더 일반적인 방법의 흐름을 잏는다. 언어모델이 어떤 parameter나 모델구조의 변화 없이도 zero-shot setting 하에서 downstream task를 수행할 수 있음을 보인다. 이 접근법은 언어모델이 zero-shot setting 하에서 넓은 범위의 과제를 수행할 수 있는 가능성을 보이며, 전도유망하고 경쟁력 있으며 과제에 따라서는 state-of-the-art를 달성하였다.


2. 접근법(Approach)

핵심은 언어모델링(language modeling)이다. 언어모델링은 보통 각 원소가 일련의 symbol $(s_1, s_2, …, s_n)$으로 구성된 예제 $(x_1, x_2, …, x_n)$에서 비지도분포 추정을 하는 것으로 정의된다. 언어는 자연적으로 연속된 순서를 가지므로 보통 조건부확률의 곱으로 이루어진 symbol에 따른 합동확률로 구해진다:

[p(x) = \prod_{i=1}^n p(s_n \vert s_1, …, s_{n-1})]

이 접근법은 어떤 조건부확률 $p(s_{n-k}, …, s_n \vert s_1, …, s_{n-k-1})$만큼이나 $p(x)$의 다루기 쉬운 샘플링을 가능하게 하였다. 최근에는 이러한 조건부확률을 매우 잘 계산하는 Transformer 등이 만들어졌다.

단일 과제 수행의 학습은 조건부분포 $p(output \vert input)$를 추정하는 확률 framework로 표현될 수 있다. 범용시스템은 여러 다른 과제들을 수행할 수 있어야 하기 때문에 같은 입력이라도 입력뿐 아니라 과제의 종류라는 조건이 들어가야 한다. 따라서 $p(output \vert input, task)$로 표현되어야 한다. 이는 다중학습과 메타학습 환경에서 다양하게 형식을 갖는다.
과제 조건을 다는 것은 모델구조 수준이나 MAML 최적화 framework에서 알고리즘 수준에서 구현되기도 한다. 그러나 McCann에서 나온 것과 같이, 언어는 과제/입력/출력 모두를 일련의 symbol로 명시하는 유연한 방법을 제공한다.

  • 예를 들어 번역학습은 (프랑스어로 번역, 영어 텍스트, 프랑스어 텍스트)로 표현된다(translate to french, english text, french text).
  • 독해는 (질문에 대답, 문서, 질문, 대답)이다(answer the question, document, question, answer).

McCann은 MQAN이라는 단일 모델로 학습하는 것이 가능했다고 설명하며 이 형식으로 많은 과제를 수행하였다고 한다.

언어모델링은 또한 어떤 symbol이 예측할 출력인지에 대한 명시적인 지도 없이도 McCann의 과제들을 학습할 수 있다. Sequence의 부분집합에 대해서만 평가하더라도 지도목적함수는 비지도목적함수와 같기 때문에, 전역최소값(global minimum)은 비지도학습에서와 지도학습에서 같은 값을 가진다. 즉 비지도목적함수에서 수렴하게 할 수 있다. 예비실험에서, 충분히 큰 언어모델은 이런 toy-ish 환경에서 다중작업 학습이 가능했으나 학습은 명시적 지도학습에서보다 훨씬 느렸다.

대화(dialog) 데이터는 꽤 괜찮은 학습 방법이지만, 상호작용이 필요없는 인터넷 사이트에 존재하는 방대한 양의 데이터가 더 낫다는 판단을 내렸다. 충분한 용량을 가지는 언어모델이라면 데이터 조달 방법과는 관련없이 더 나은 예측을 위한 수행을 시작할 것이라는 추측이 있다. 만약 지금 가능하다면, 아마 현실적으로 비지도 다중작업 학습이 될 것이다.

Examples

2.1. Training Dataset

많은 선행연구에서 사용된 dataset은 뉴스와 같이 한 영역에서만 가져온 데이터로 구성되어 있었다. 이 논문에서는 가능한 한 다양한 출처로부터 가져오려고 하였다.

이러한 점에서 촉망받는 것은 Common Crawl과 갈은 web scraping 자료인데, 이 중 많은 양이 이해할 수 없는(unintelligible, 또는 품질이 떨어지는) 데이터라 한다. 따라서 이 논문에서는 단순 크롤링이 아닌 고품질의 데이터를 얻는 다른 방법을 사용하기로 했다.

  • 사람에 의해 필터링된 글만을 사용하기로 하였다:
    • Reddit에서 3 karma 이상을 받은 글에 포함된 외부링크의 글을 가져왔다.
    • 결과적으로 45M개의 링크를 가져왔다.
  • Dataset 이름은 WebText라 하였다.
  • 텍스트 추출을 위해 DragnetNewspaper 내용추출기를 사용했다.
  • 2017년 12월 이후의 글과 위키피디아 글은 제거했으며, 중복제거 등을 거쳐 8M개의 문서, 40GB의 텍스트를 확보하였다.
    • 위키피디아는 다른 dataset에서 흔하고, 학습과 측정 단계에서의 데이터가 겹치는 문제로 인해 분석이 복잡해질 수 있어 제외했다.

2.2. Input Representation

범용언어모델은 어떤 문자열의 확률도 계산할 수 있어야 한다. 현재 대규모 언어모델은 소문자화, 토큰화, 모델링 가능한 문자열이 차지하는 공간을 제한하기 위한 사전외 token과 같은 전처리 과정을 거친다. Unicode 문자열을 UTF-8 형식으로 처리하는 것은 이를 우아하게 만족시키며 byte수준 언어모델은 One Billion Word Benchmark와 같은 대규모 dataset에서 단어수준 언어모델에 뒤떨어진다. WebText에서도 역시 그러한 성능차이를 확인하였다.

Byte Pair Encoding(BPE)는 글자(byte)와 단어의 적당한 중간 단위를 쓴다.

  • 이는 자주 나오는 symbol sequence의 단어수준 입력과 자주 나오지 않는 symbol sequence의 글자수준 입력을 적절히 보간(interpolate)한다.
  • 그 이름과는 달리 BPE 구현은 byte sequence가 아닌 Unicode code points에서 동작한다. 이러한 구현은 모든 Unicode 문자열을 모델링하기 위해 전체 Unicode symbol의 공간만큼을 필요로 한다.
  • multi-symbol token을 추가하기 전 13만 개의 token을 포함하는 기본사전을 필요로 하게 된다. 이는 보통의 3만 2천~6만 4천 token의 사전보다 엄청나게 큰 것이다.
    • 이와는 달리 byte수준의 BPE의 사전은 256개만의 token을 포함한다.
  • 그러나 BPE를 byte sequence에 직접 적용하는 것은 BPE가 token 사전을 구춘하기 위한 heuristic에 기반한 greedy frequency를 사용하기 때문에 최적이 아니게 된다.
  • BPE는 dog의 다양한 형태, dog.dog!dog? 등을 가진다.
    • 이는 한정적인 사전과 모델의 공간을 최적이 아니게 사용하게 된다.
    • 이를 피하기 위해 BPE가 어떤 byte sequence로부터도 문자 범주를 넘어 병합하는 것을 막았다.
  • 여러 vocab token의 최소부분만을 추가할 때 압축효율성을 크게 증가시키는 공간을 위한 예외를 두었다.

이러한 입력표현은 단어수준 언어모델의 경험적 이점과 문자수준 접근법의 일반성을 결합할 수 있게 한다. 이 논문의 접근법이 어떤 Unicode 문자열에든 확률을 부여할 수 있기 때문에, 이는 이 논문의 모델을 전처리, 토큰화, 사전크기 등과 관련없이 어떤 dataset에서도 평가할 수 있게 만든다.

2.3. Model

Transformer가 기본 구조이며, OpenAI Gpt-1의 구조를 대부분 따른다. 약간의 차이는 있는데,

  • Layer 정규화가 pre-activation residual network처럼 각 sub-block의 입력으로 옮겨졌다.
    • 추가 layer 정규화가 마지막 self-attention block 이후에 추가되었다.
  • 모델 깊이에 따른 residual path의 누적에 관한 부분의 초기화 방법이 변경되었다.
    • $N$이 residual layer의 수라 할 때, residual layer의 가중치에 $1 / \sqrt{N}$을 곱했다.
  • 사전은 50,257개로 확장되었다.
  • 문맥고려범위(context size)가 512~1024개의 token으로 늘어났으며 batch size도 512로 증가했다.

3. 실험(Experiments)

모델은 크기가 각각 다른 4개를 만들어 실험했다. 각 모델의 크기는 다음과 같다.

Parameters Layers d_{model}
117M 12 768
345M 24 1024
762M 36 1280
1542M 48 1600

가장 작은 모델은 크기가 OpenAI GPT-1와 같고, 두 번째는 BERT와 같다. 가장 큰 모델인 GPT-2는 10배 이상 크다.

각 모델의 learning rate는 WebText의 5%를 떼서 만든 held-out 샘플을 사용하여 수동 조정하였다. 모든 모델은 여전히 WebText에 과소적합(underfitted)되었으며 더 오래 학습시키면 더 높은 성능을 얻을 수 있을 것이다.

3.1. Language Modeling

GPT-2는 문자단위(byte level)로 되어 있어 손실이 있는 전처리나 토큰화 등이 필요하지 않으며, 어떤 언더 모델 benchmark에도 사용할 수 있다. 평가는 WebText 언어모델에 따른 dataset의 로그확률을 계산하는 방식으로 통일했다. WebText 언어모델은 일반 분포를 심하게 벗어난 것, 이를테면 대단히 규격화된 텍스트, 분리된 구두점이나 축약형, 섞인 문장에 대해 평가받으며, <UNK>는 WebText에 400억 byte 중 26번밖에 나타나지 않는다.

결과는 아래 표에 나와 있으며, 어떤 미세조정도 거치지 않고 zero-shot 환경에서 8개 중 7개에서 state-of-the-art를 달성하였다.

Results

3.2. Children’s Boot Test

품사(고유명사, 명사, 동사, 전치사)에 따른 언어 모델의 성능을 측정하기 위한 dataset이다. 원 논문에 소개된 내용을 따라 각 선택의 확률과 언어모델의 선택에 따른 문장의 나머지 부분에 대한 확률을 계산하고 가장 높은 확률의 선택지를 선택하게 했다. 결과는 89.1% $\to$ 93.3%가 되었다.

Results

3.3. LAMBADA

텍스트의 장거리 의존성(long-range dependencies)을 평가한다. 간단히 말하면 정확도는 19% $\to$ 52.66%, perplexity는 99.8 $\to$ 8.6으로 향상시켰다.

3.4. Winograd Schema Challenge

텍스트에 존재하는 중의성(또는 모호한 부분, ambiguities)을 해석하는 능력을 측정함으로써 일반상식 추론능력을 평가한다. GPT-2는 정확도를 7% 증가시켜 70.70%를 달성했다.

Results

3.5. Reading Comprehension

CoQA(The Conversation Question Answering dataset)을 7개의 분야에서 가져온 문서에서 질문자-답변자의 자연어 대화로 이루어진 dataset이다. CoQA 테스트는 독해능력과 대화에 기반한 모델의 답변능력을 평가한다. GPT-2는 55 F1 score를 달성해 4개 중 3개의 다른 모델을 능가했는데 이는 심지어 주어진 127k 이상의 수동 수집된 질답 쌍으로 학습시키지 않은 것이다. 지도가 포함된 state-of-the-art인 BERT는 89 F1 score에 근접하였다. 그러나 어떤 미세조정 없이 55점을 달성했다는 것은 상당히 고무적인 일이다.

3.6. Summarization

CNN과 Daily Mail dataset으로 요약 능력을 평가했다. 총합 6.4점의 향상을 보였다.

Results

3.7. Translation

번역에서는 예상외로 별로 좋은 결과를 내지 못했다고 한다.

3.8. Question Answering

답변한 문장이 ‘완전히 일치하는’ 평가 방법을 썼을 때 GPT-2는 약 4.1%의 정확도를 보여 일반적으로 5.3배의 정확도를 보인다. 그러나 GPT-2가 생성한 30개의 가장 ‘자신있는’ 답변은 아래 그림에 나와 있는데 이는 정보검색과 문서추출 질답을 병행한 열린분야 질답 시스템에 비하면 50%까지 낮은 성능을 보인다.

Results

4. 일반화 vs 암기(Generalization vs Memorization)

최근 연구에서 많이 사용되는 데이터셋 중에는 거의 동일한 이미지를 여럿 포함하는 것이 있는데, 예를 들면 CIFAR-10의 경우 3.3%의 이미지가 train / test 세트에서 겹친다. 이는 기계학습 시스템의 일반화 성능을 과대평가하게 만든다. 이는 성능 측정에 방해 요인이 되기 때문에 데이터셋이 이러한 요인이 없는지 확인하는 것은 중요하다.

그래서 WebText에 대해 8-gram 학습세트 토큰을 포함하는 Bloom 필터를 만들어 테스트해보았고, 결과는 다음과 같다.

Results

물론 WebText는 써도 괜찮다는 결론이 나왔다. 그런데 다른 데이터셋의 경우에는 생각보다 높은 겹침(overlap) 현상이 나타나는 것을 볼 수 있다.

비슷한 텍스트들이 성능에 어떤 영향을 미치는지 이해하고 정량화하는 것은 중요한 부분이다. 더 나은 데이터중복제거(de-duplication) 기술은 매우 중요하며, 중복제거에 기반한 n-gram overlap을 사용하는 것은 훌륭한 방법이며 이 논문에서는 이를 추천한다.

WebText LM의 성능을 결정하는 또 다른 잠재적 방법은 암기(기억, memorization)이 held-out set에 어떤 영향을 미치는지 살펴보는 것이다. train / test set에 대해 성능을 평가한 다음 그림은 그 거대한 GPT-2가 WebText에 대해 여전히 과소적합(underfitted)되었으며 암기에 의한 것이 아님을 보여준다.

Results

5. 관련 연구(Related Work)

이 논문의 많은 부분은 더 큰 데이터셋으로 학습된 더 큰 언어모델의 성능을 측정하는 데 쓰였다. 다른 많은 연구들도 이와 비슷하다(scaled RNN based LM 등).

iWeb Corpus와 같은 거대한 웹페이지의 텍스트 말뭉치를 필터링하고 구축하는 대안을 제시하거나, 언어문제를 위한 사전학습 방법, 벡터표현, seq2seq 등이 연구되었으며 최근 연구들은 언어모델의 사전학습이 잡담이나 대화 같은 어려운 생성문제에 맞춰 미세조정할 때 도움이 된다는 것을 밝혀내었다.


6. 토의(Discussion)

많은 연구들은 지도/비지도 사전학습 표현에 대한 학습, 이해, 비판적 평가에 관해 연구를 진행해 왔다. 이 논문은 비지도 학습이 아직 연구할 거리가 더 남아 있음을 밝혔다.

GPT-2의 zero-shot 학습 성능은 독해 등에서 좋은 성능을 보였으나 요약과 같은 문제에서는 기본적인 성능만을 보여주었다. 꽤 괜찮긴 해도 실제 사용하기엔 여전히 무리이다.

GPT-2의 성능이 많은 과제에서 괜찮긴 한데, 미세조정을 통한 그 한계가 얼마인지는 분명하지 않다. 그렇지만 이 논문의 저자들은 decaNLP나 GLUE와 갈은 벤치마크에서 미세조정(fine-tuning)할 것을 계획하고 있다고 한다.
또 GPT-2의 학습데이터와 그 크기가 BERT에서 말한 단방향 표현의 비효율성을 극복할 수 있을 만큼 충분한지도 확실치 않다고 한다.


7. 결론(Conclusion)

큰 크기의 언어모델이 충분히 크고 다양한 데이터셋에서 학습된다면 많은 분야와 데이터셋에서 괜찮은 성능을 보여준다. GPT-2의 zero-shot은 8개 중 7개의 주요 언어 과제에서 state-of-the-art를 달성하였다.
모델이 zero-shot으로 다양한 과제에서 잘 수행한다는 것은 대용량의 모델이 외부 지도 없이 충분히 크고 다양한 말뭉치로부터 학습하면 많은 문제를 잘 수행할 가능성을 최대화할 것을 제시한다.

Acknowledgements

언제나 있는 감사의 인사


Refenrences

논문 참조. 많은 레퍼런스가 있다.


Citation

@article{radford2019language,
  title={Language Models are Unsupervised Multitask Learners},
  author={Radford, Alec and Wu, Jeff and Child, Rewon and Luan, David and Amodei, Dario and Sutskever, Ilya},
  year={2019}
}

8. Appendix A: Samples

8.1. Model capacity

가장 작은 WebText LM과 GPT-2가 본 적 없는(unseen) WebTest 테스트 기사에 대해 생성한 문장들을 비교하여 나열한 것이 표 7~11에 있다. 256개의 단어(token)을 주고 다음 256를 생성하는 것이다. 논문에 따르면 잘 된 것을 골라 가져온 것은 아니라고 한다.
그 중 하나를 가져와 보았다. 기계가 생성한 문장치고 깔끔하다.

Generation Results

8.2. Text Memorization

게티즈버그 연설 유명한 인용문이나 연설 같은 문장이 주어질 때 GPT-2가 (그대로) ‘암기’하는 행동을 보이는 것이 관찰되었다.
이런 현상이 얼마나 일어나는지 보기 위해 test set 기사를 주고 GPT-2가 생성한 문장과 실제 문장의 완성한 것과의 overlap 비율을 비교해 보았다.
결과는 기준보다 그 비율이 낮다고 한다.

8.3. Diversity

표 12는 같은 문장을 주고 나머지를 생성하라 했을 때 얼마나 다양한 문장들을 생성하는지 본 것이다.

Generation Results

8.4. Robustness

표 13은 앞에서 언급 한 유니콘 뉴스 기사를 보여준다. 이 모델이 분포 문맥을 다룰 수는 있지만 이러한 샘플의 품질은 일반적으로 낮다.

Generation Results

Comment  Read more