이 글은 NGBoost: Natural Gradient Boosting for Probabilistic Prediction논문을 참고하여 정리하였음을 먼저 밝힙니다. 논문에서 사용한 방법론을 간단하게 설명하고, 라이브러리를 이용하여 코드를 구현한 후 추가적으로 설명드리겠습니다. 혹시 제가 잘못 알고 있는 점이나 보안할 점이 있다면 댓글 부탁드립니다.
0. Summary
기존 regression 모델은 점 추정치를 맞추는데 초점을 두었습니다. 이 논문에서는 probabilistic regression 모델을 사용하여 공분산을 조건부로 사용한 결과 공간에 대한 전체 확률 분포를 찾고자 합니다.
즉, 위의 그림과 같이 각 x 지점에서의 각각 다른 평균과 분산을 추정하는 것이 목표입니다. 위와 같은 문제를 해결하기 위하여 기존 다양한 방법을 시도하였으나, 그 방법은 유연하지 못하고 느리고 비전문가들의 접근이 어려운 문제점이 있었습니다.
따라서 위와 같은 방법으로 각 데이터의 인풋 별로 해당 값을 가장 잘 설명하는 파라미터(여기에서는 평균과 분산을 의미)를 찾는 알고리즘을 제시합니다. 자세한 내용은 아래에 차근차근 설명드리겠습니다.
1. Proper Scoring Rules
파라미터를 학습하기 위해서는 적합한 scoring 함수가 정의되어야 합니다. 우선 naive하게 생각하면 논문에 적합한 scoring 함수는 예측에 사용할 확률분포 함수 P가 주어졌을 때, 하나의 관측치 y에 대한 scoring 함수가 필요합니다. 즉, S(P, y)라 정의할 수 있습니다. 또한 Q를 y에 관한 정답 분포라고 정의하면 다음과 같은 수식이 만족되어야 합니다.
위의 수식의 의미하는 바는 적합한 scoring 함수라면, 정답 분포의 점수 값의 기댓값은 예측 분포의 점수 값의 기댓값보다는 항상 적게 설계되어야 한다는 것입니다.
따라서 우리는 고정된 분포 P가 아닌 찾고자하는 파라미터 \(\theta\)에 관한 S(\(\theta\), y)로 scoring 함수를 말할 수 있습니다. naive하게 정의한 S(\(\theta\), y)를 이용하여 논문에서는 2가지 scoring 함수를 제시합니다.
1) MLE (일반적인 방법론)
흔히 사용하는 maximum likelihood을 이용한 scoring 함수입니다.
2) CRPS (Continuous Ranked Probability Score)
MLE 보다 robust하다고 알려진 CRPS를 이용한 scoring 함수입니다. MLE의 문제점은 log함수를 사용하므로 예상 값과 관측치와의 차이가 많이 날수록 score 값이 급격하게 상승하는 문제점이 있습니다.
따라서 위의 그림처럼 CRPS는 관측치가 평균보다 멀어질 때 보다 완만하게 score 값이 증가함을 알 수 있습니다.
2. The Generalized Natural Gradient
일반적인 gradient descent 방법으로 접근하면, 위에서 정의한 scoring 함수를 최소화하는 방식으로 학습되어야 합니다. 즉, \(\nabla\)S(\(\theta\), y)를 정의하려 합니다.
위의 수식처럼 찾고자하는 \(\theta\)를 어느 방향이 되었건 score 값이 증가하는 방향으로 학습시키는 것이 목적입니다. 위의 수식에서 주의해야 할 점은 수식을 reparameterize 할 때, gradient 값이 불변이 아니라는 점입니다. 즉, \({P_\theta}\)를 \({P_{z(\theta)}}\)(y \(\in\) A)로 reparameterize를 할 때, \(\psi\) = z(\(\theta\))라 가정하면 \({P_{z(\theta)}}\)(y \(\in\) A) = \({P_{z(\psi)}}\)(y \(\in\) A)라 할 수 있습니다. 그러나 \(\theta\)에서 \(\theta\)+d\(\theta\) 값을 계산하여 얻은 확률분포와 \(\psi\)에서 \(\psi\)+d\(\psi\) 값을 계산하여 얻은 확률분포는 같지 않습니다. (\({P_{z(\theta)}}\)(y \(\in\) A) \(\neq\) \({P_{z(\psi)}}\)(y \(\in\) A))
위와 같은 문제가 나타나는 이유는 해당 파라미터가 일반적으로 gradient에 사용하는 distance에서 얻은 결과가 아닌 분포에서 얻은 결과기 때문입니다. 따라서 논문에서는 \(\nabla\)대신 \({\tilde{\nabla}}\)를 사용하여 natural gradient로 정의합니다.
위의 문제를 해결한 Natural gradient에 대해 좀 더 자세히 알아보기 전에 우선 divergences라는 개념을 먼저 설명드리겠습니다. Divergence는 거리의 개념이 아닌 분포의 개념에서 사용하는 수식으로 다음과 같이 Q, P의 분포가 얼마나 다른지를 구하고자 할 때 주로 사용합니다.
논문에서는 위에서 정의한 1)MLE에는 KL(Kullback-Leibler) divergence를 사용했으며, 2)CRPS에는 L2 divergence를 사용하였습니다. 일반적으로 divergence는 symmetric하지 않지만, 매개변수의 아주 작은 변경은 거의 대칭적이므로 국소적으로는 metric으로 사용할 수 있습니다.
따라서 divergence metric으로 정의하여 새롭게 얻은 natural gradient는 다음과 같습니다.
수식에서 보실 수 있듯이 distance metric 대신 divergence metric을 사용하였습니다.
이제, 위에서 정의한 optimization 문제를 해결하기 위하여 다음과 같은 방법을 사용하였습니다.
\({I_{S}(\theta)}\)는 \(\theta\)에서의 Riemannian statistical manifold metric입니다.
위의 그림은 저자가 github에 추가적으로 공개한 자료에서 가져온 그림입니다. 즉, 기존의 분포를 고려한 gradient 식에 \({I_{S}(\theta)}\)를 곱하면 거리 기반 식으로 표기할 수 있는데, 반대로 그 metric의 역함수를 곱하여 찾고자 합니다.
또한 scoring 함수에 따라 \({I_{S}(\theta)}\) 값이 달라지는데,
1) MLE
Fisher Information에 의해 위와 같이 정의됩니다.
2) CRPS
L2 divergence를 사용하기 때문에 위와 같이 정의됩니다.
논문에서 구하고자 하는 평균과 표준편차에 log를 씌운 값을 각각 x, y축으로 정의하고 gradient의 모양을 관찰하면 다음과 같이 기존의 gradient 방향과 조금 다른 결과를 관찰할 수 있습니다.
3. Parameters Update
논문에서 찾고자 하는 각 데이터에 해당 parameter \(\theta\)는 다음과 같은 알고리즘으로 학습되어 얻을 수 있습니다. (여기서는 \(\theta\) = (\(\mu\), log \(\sigma\)) 입니다.)
위 알고리즘에서 보실 수 있듯이 \(\rho\)라는 m번째 반복 시점에 대한 scaling factors를 사용하여 얻고자 하는 \(\theta\)값을 조절합니다. 또한 작은 lr 값(실험에서는 0.1, 0.01을 사용)을 추가적으로 곱하여 사용하였습니다.
4. Experiments
우선 학습을 시각화하면 다음과 같습니다.
위에 해당하는 그림은 일반적인 gradient를 사용한 결과이고, 아래에 해당하는 그림은 natural gradient를 사용한 결과입니다. 가운데의 진한 선이 평균에 해당하는 부분인데, 위에 그림은 평균값이 고정된 반면, 아래 그림은 fitting 후 평균 또한 변하여 보다 끝이 오므려진 구간 추정의 결과를 얻을 수 있습니다.
1) 구간 추정
구간 추정에서는 거의 모든 데이터에 대하여 가장 좋거나 준수한 추정을 한다는 것을 확인할 수 있습니다.
2) 점 추정
점 추정에서도 좋은 결과를 얻긴 하지만, 일반적인 boosting 모델이 조금 더 잘 나오는 것을 확인할 수 있습니다.
5. Code review
저자가 공개한 module을 사용하여 쉽게 구현할 수 있습니다.
1) import module
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from ngboost import NGBRegressor
from xgboost import XGBRegressor
from sklearn.ensemble import GradientBoostingRegressor
from lightgbm import LGBMRegressor
from catboost import CatBoostRegressor
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
2) dataset 설정
X, Y = load_boston(True)
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.2)
3) 학습
ngb = NGBRegressor().fit(X_train, Y_train)
xgb = XGBRegressor().fit(X_train, Y_train)
gbm = GradientBoostingRegressor().fit(X_train, Y_train)
lgbm = LGBMRegressor().fit(X_train, Y_train)
catb = CatBoostRegressor().fit(X_train, Y_train)
Y_preds_ngb = ngb.predict(X_test)
Y_preds_xgb = xgb.predict(X_test)
Y_preds_gbm = gbm.predict(X_test)
Y_preds_lgbm = lgbm.predict(X_test)
Y_preds_catb = catb.predict(X_test)
test_MSE_ngb = mean_squared_error(Y_preds_ngb, Y_test)
test_MSE_xgb = mean_squared_error(Y_preds_xgb, Y_test)
test_MSE_gbm = mean_squared_error(Y_preds_gbm, Y_test)
test_MSE_lgbm = mean_squared_error(Y_preds_lgbm, Y_test)
test_MSE_catb = mean_squared_error(Y_preds_catb, Y_test)
4) 결과
print('Test MSE NGBoost:\t', test_MSE_ngb)
print('Test MSE XGBoost:\t', test_MSE_xgb)
print('Test MSE GBM:\t\t', test_MSE_gbm)
print('Test MSE LightGBM:\t', test_MSE_lgbm)
print('Test MSE Catboost:\t', test_MSE_catb)
해당 boston data에 대해서는 주로 사용하는 boost 모델과 비교하였을 때, 비교적 좋은 성능이 나온 것을 확인하실 수 있습니다.