본문 바로가기
## 오래된 게시글 (미관리) ##/Python (Linux)

31. Python - 규제(릿지 회귀, 라쏘 회귀, 엘라스틱넷, 조기종료)

by #Glacier 2019. 1. 27.
반응형

앞에서도 보았듯 과대적합을 감소시키는 방법 중 하나는 모델을 규제하는 것인데, 자유도를 줄임으로서 데이터에 과대적합되기 어려워지기 때문입니다. 예를 들어, 다항 회귀 모델을 규제하는 간단한 방법은, 다항식의 차수를 감소시키는 것입니다.


선형 회귀 모델에서는 보통 모델의 가중치를 제한함으로서 규제를 가합니다. 각기 다른 방법으로 가중치를 제한하는 릿지 회귀, 라쏘 회귀, 엘라스틱넷을 살펴보겠습니다.


1) 릿지 회귀

릿지 회귀(또는 티호노프 규제, Tikhonov) 는 규제가 추가된 선형 회귀 버전입니다. 

규제항 이 비용 함수에 추가됩니다. 이는 학습 알고리즘을 데이터에 맞추는 것 뿐만 아니라 모델의 가중치가 가능한 작게 유지되도록 노력하게 됩니다. 규제항은 훈련하는 동안에만 비용함수에 추가됩니다. 모델의 훈련이 끝나면 모델의 성능을 규제가 없는 성능 지표로 평가합니다. 


[주의] 일반적으로 훈련하는 동안 사용되는 비용 함수와 테스트에서 사용하는 성능 지표는 다릅니다. 

   규제를 떠나서 이들이 다른 이유는 훈련에 사용되는 비용 함수는 최적화를 위해 미분 가능해야 하기 때문입니다.

   반면 테스트에 사용되는 성능 지표는 최종 목표에 가능한 한 가까워야 합니다. 로그 손실같은 비용 함수를 사용하여

   훈련시킨 분류기를 정밀도/재현율로 평가하는 것이 좋은 예입니다.


하이퍼파라미터 는 모델을 얼마나 많이 규제할 지 조절합니다.  = 0 이면, 릿지 회귀는 선형 회귀와 같아집니다.

가 아주 크게되면 모든 가중치가 거의 0에 가까워지고, 결국 데이터의 평균을 지나는 수평선이 됩니다.

아래는 릿지 회귀의 비용 함수입니다.


편향 는 규제되지 않습니다. (합 기호가 i=0이 아니고, 1부터 시작합니다.)

w를 특성의 가중치 벡터() 라고 정의하면, 규제항은 와 같습니다.

여기서 가 가중치 벡터의  노름입니다. 경사 하강법에 적용하려면 MSE 그래디언트 벡터에 를 더하면 됩니다.

릿지 회귀는 입력 특성에 민감하기 때문에 수행하기 전에 미리 데이터의 스케일을 맞추는 것이 중요합니다. (ex, StandardScaler)

**규제항에 1/2를 곱한 것은 미분 결과를 간단하게 만들기 위해서입니다.


이제 코드를 통해 알아보도록 하겠습니다. 


from sklearn.linear_model import Ridge

#시드 고정

np.random.seed(42)


m = 20

#가우시안 표준 정규 분포를 따르는 난수를 20행 1열로 만듭니다.

X = 3 * np.random.rand(m,1)

y = 1+ 0.5*X + np.random.randn(m,1) / 1.5


#numpy.linspace는 시작점, 끝점, 만들 개수를 받습니다.

#따라서 0부터 3까지 100개로 쪼개어 만들고, 100행 1열로 만듭니다.

X_new = np.linspace(0,3,100).reshape(100,1)


#zip(*iterable)은 동일한 개수로 이루어진 자료형을 묶어주는 역할을 하는 함수입니다.

#즉 알파값에 따라 표현색상과 방식이 b-, g--, r:로 정해집니다.

def plot_model(model_class, polynomial, alphas, **model_kargs):

    for alpha, style in zip(alphas, ('b-', 'g--', 'r:')) :

  #알파가 0이면 선형회귀

        model = model_class(alpha, **model_kargs) if alpha > 0 else LinearRegression()

  #다항회귀일 경우

        if polynomial :

            model = Pipeline ([

                                ('poly_features', PolynomialFeatures(degree=10, include_bias=False)),

                                ('std_scaler', StandardScaler()),

                                ('regul_reg', model),

                                ])

        model.fit(X,y)

        y_new_regul = model.predict(X_new)

  #알파가 0이 아니면 lw = 2 

        lw = 2 if alpha > 0 else 1

        plt.plot (X_new, y_new_regul, style, linewidth = lw, label=r'$alpha  = {}$'.format(alpha))

    #기본 plot

    plt.plot(X, y, 'b.', linewidth = 3)

    plt.legend(loc='upper left', fontsize=15)

    plt.xlabel('$x_1$', fontsize=18)

    plt.axis([0,3,0,4])

    

plt.figure(figsize=(8,4))

plt.subplot(121)

#릿지회귀를 하되 다항회귀 = False, 알파값 지정 0, 10, 100

plot_model(Ridge, polynomial=False, alphas = (0,10, 100), random_state=42)

plt.ylabel('$y$', rotation=0, fontsize=18)

plt.subplot(122)

#릿지회귀를 하되 다항회귀 = True, 알파값 0, 10**-5(0.00001), 1

#아마 이렇게 알파값을 지정한 이유는 스케일링 때문일 것입니다.

#따로 설명은 나와있지 않은데, 

plot_model(Ridge, polynomial=True,  alphas=(0, 10**-5, 1), random_state=42)


plt.show()



위 그림은, 선형 데이터에 몇 가지 다른 alpha를 사용해 릿지 모델을 훈련시킨 결과입니다.

왼쪽 그래프는, 평범한 릿지 모델을 사용해 선형 예측을 만들었고, 오른쪽 그래프는 PolynomialFeatures(degree=10)을 사용해 데이터를 확장한 후, 스케일을 조정하고 릿지 모델을 적용한 것입니다. 결국 이는 릿지 규제를 사용한 다항 회귀가 됩니다.

alpha를 증가시킬수록 직선에 가까워지고, 값을 줄이면 모델의 분산은 줄지만 편향은 커지게 됩니다.


선형회귀와 마찬가지로, 릿지 회귀를 계산하기 위해 정규방정식을 사용할 수도 있고, 경사 하강법을 사용할 수도 있습니다.

장단점은 이전과 같습니다. 아래의 식은 정규방정식의 해 이며, 

A는 편향에 해당하는 맨 왼쪽 위의 원소가 0인 (n+1)x(n+1)의 단위행렬(identity matrix) 입니다.

단위행렬은 주대각선이 1이고, 그 외에는 모두 0인 정방 행렬입니다. 편향에 해당하는 세타제로는 규제에 포함되지 않으므로 단위행렬의 주대각선 첫 번째 원소가 0이 되어야 합니다.


[Ridge 회귀의 정규방정식]


아래는, 사이킷런에서 정규방정식을 사용한 릿지 회귀를 적용하는 예 입니다.

안드레 루이 숄레스키(Andre-Louis Cholesky)가 발견한 행렬 분해(matrix factorization)를 사용하여 위 식을 변형한 방정식을 사용합니다.


#대안으로 Ridge에 solver='sag'를 사용할 수 있습니다. 확률적 평균 경사 하강법(Stochastic Average Gradient Descent)은 SGD의 변종입니다. 자세한 내용은 브리티시 콜롬비아 대학의 마크 슈미트 등이 만든 (http://goo.gl/vxVyA2)를 참고

#sag는 확률적 경사 하강법과 비슷하지만 현재 그래디언트와 이전 스텝에서 구한 모든 그래디언트를 합하여 평균한 값으로 모델 파라미터를 갱신합니다. 사이킷런 0.19버전에는 sag버전의 개량버전인 saga 알고리즘이 추가되었습니다. saga에 대한 간단한 예는 이 책의 저자 블로그 (http://goo.gl/oXfTDt)를 참고


from sklearn.linear_model import Ridge

ridge_reg = Ridge(alpha=1, solver ='cholesky')

ridge_reg.fit(X,y)

ridge_reg.predict([[1.5]])

array([[1.55071465]])

sgd_reg = SGDRegressor(max_iter=5, penalty ='l2')

sgd_reg.fit(X, y.ravel())

sgd_reg.predict([[1.5]])

array([1.12795912])

Penalty 매개변수는 사용할 규제를 지정합니다. 'l2'는 SGD가 비용 함수에 가중치 벡터의 l2노름의 제곱을 2로 나눈 규제항을 추가하게 만듭니다. 즉, 릿지 회귀와 같습니다.


ridge_reg = Ridge(alpha=1, solver="sag", random_state=42)

ridge_reg.fit(X, y)

ridge_reg.predict([[1.5]])

array([[1.5507201]])




라쏘(Least Absolute Shrinkage and Selection Operator, LASSO)회귀는 선형 회귀의 또 다른 규제된 버전입니다.

릿지 회귀처럼 비용 함수에 규제항을 더하지만 노름의 제곱을 2로 나눈 것 대신 가중치 벡터의  노름을 사용합니다.


[라쏘 회귀의 비용 함수]


from sklearn.linear_model import Lasso


plt.figure(figsize=(8,4))

plt.subplot(121)

plot_model(Lasso, polynomial=False, alphas=(0, 0.1, 1), random_state=42)

plt.ylabel('$y$', rotation=0, fontsize=18)

plt.subplot(122)

plot_model(Lasso, polynomial=True, alphas=(0, 10**-7, 1), tol=1, random_state=42)


plt.show()


#라쏘 회귀는 앞선 릿지 모델보다 조금 더 작은 alpha값을 사용했습니다.



라쏘 회귀의 중요한 특징은 덜 중요한 특성의 가중치를 완전히 제거하려고 한다는 점입니다.(즉, 가중치가 0이 됩니다.)

예를 들어 오른쪽 초록색 점선은 2차 방정식처럼 보이며 거의 선형성을 띄고 있습니다.

즉, 차수가 높은 다항 특성의 가중치가 모두 0이 되었습니다. 

다시 말해, 라쏘 회귀는 자동으로 특성을 선택하고 희소 모델(sparse model)을 만듭니다. (0이 아닌 특성의 가중치가 높습니다.)


[NOTE]

라쏘 비용 함수에서, 배치 경사 하강법의 경로가 종착지로 가는 통로에서 지그재그로 튀는 경향을 보이는데, 이는 에서 갑자기 기울기가 바뀌기 때문입니다. 전역 최솟값에 수렴하기 위해서는 학습률을 점진적으로 줄여 나가야 합니다.


라쏘의 비용 함수는 일 때, 미분 가능하지 않습니다. 하지만 0일 때, 서브그래디언트 벡터(subgradient vector, g)를 사용하면 경사 하강법을 적용하는 데 문제가 없습니다.

다음의 식은 경사 하강법을 위해 라쏘 비용 함수에 사용할 수 있는 서브그래디언트 벡터 공식입니다.



다음은 Lasso 클래스를 사용한 간단한 사이킷런 예제입니다. Lasso대신 SGDRegressor(penalty='l1')을 사용할 수도 있습니다.

from sklearn.linear_model import Lasso

lasso_reg = Lasso(alpha=0.1)

lasso_reg.fit(X,y)

lasso_reg.predict([[1.5]])

array([1.53788174])

엘라스틱넷(Elastic Net)은 릿지 회귀와 라쏘 회귀의 절충 모델입니다.

규제항은 릿지와 라쏘의 규제항을 단순히 더해서 사용하며, 혼합 정도는 혼합 비율(r)을 사용해 조절합니다.

r=0이면 엘라스틱넷은 릿지 회귀와 같고, r=1이면 라쏘 회귀와 같습니다.


[엘라스틱넷 비용 함수]


그럼 보통의 선형 회귀(규제가 없는 모델), 릿지, 라쏘, 엘라스틱넷을 언제 사용해야 할까요?

적어도 규제가 약간 있는 것이 대부분의 경우에 좋으므로 일반적으로 평범한 선형 회귀는 피해야 합니다.

릿지가 기본이 되지만, 실제로 쓰이는 특성이 몇 개 뿐이라고 의심되면 라쏘나 엘라스틱넷이 낫습니다. 

이 모델들은 앞서 말했던 것 처럼 불필요한 특성의 가중치를 0으로 만듭니다.

특성 수가 훈련 샘플 수 보다 많거나 특성 몇 개가 강하게 연관되어 있을 때는 보통 라쏘가 문제를 일으키므로 엘라스틱넷을 선호합니다.


다음은 사이킷런의 ElasticNet을 사용한 간단한 예제입니다.

#l1_ratio가 혼합비율(r)입니다.

from sklearn.linear_model import ElasticNet

elastic_net = ElasticNet(alpha=0.1, l1_ratio=0.5, random_state=42)

elastic_net.fit(X,y)

elastic_net.predict([[1.5]])

array([1.54333232])

# 라쏘는 특성 수가 샘플 수(n)보다 많으면 최대 n개의 특성을 선택합니다. 또한 여러 특성이 강하게 연관되어 있으면 이들 중 임의의 특성 하나를 선택합니다.



[조기 종료]


경사 하강법과 같은 반복적인 학습 알고리즘을 규제하는 아주 색다른 방식은 검증 에러가 최솟값에 도달하면 바로 훈련을 중지시키는 것입니다. 이를 조기 종료(early stopping)라고 합니다.


아래의 그림은 배치 경사 하강법으로 훈련시킨 복잡한 모델(고차원 다항 회귀 모델)을 보여줍니다.

에포크가 진행됨에 따라 알고리즘이 점차 학습되어 훈련 세트에 대한 예측 에러(RMSE)와 검증세트에 대한 예측 에러가 줄어듭니다. 그러나 잠시 후 감소하던 검증 에러가 멈추었다가 다시 상승합니다. 모델이 훈련 데이터에 과대 적합되기 시작함을 의미합니다.

조기 종료는 검증 에러가 최소에 도달하는 즉시 훈련을 멈춥니다. 이 규제 테크닉은 매우 효과적이고 간단해서 제프리 힌튼이 훌륭한 공짜 점심이라고도 불렀습니다.


#조기 종료

#시드 고정

np.random.seed(42)

m = 100

#가우시안 분포를 따르는 난수 생성, 100행 1열

X = 6 * np.random.rand(m,1) - 3

y = 2 + X + 0.5 * X**2 + np.random.randn(m,1)


#ravel()함수는 다차원 배열을 1차원 배열로 펴주는 함수입니다.

#ravel( x, order='C', 'F', 'K')가 들어갈 수 있으며, order = C는 default입니다.

#C는 3행 4열이 있다면, 행순서대로 펴주며, F는 Fortran과 같은 순서로 열대로 가져옵니다.

#K는 메모리에서 발생하는 순서대로 인덱싱하여 1차원 배열로 만듭니다.

#이때 3차원 이상일 경우, 즉 array를 차원으로 구분할 경우, 위와 동일하지만 차원 순서대로 갑니다.

#따라서 x는 보면 세로로 긴 모양, y는 가로로 펴지겠죠.

X_train, X_val, y_train, y_val = train_test_split(X[:50], y[:50].ravel(), test_size=0.5, random_state=10)


poly_scaler = Pipeline([

    ('poly_features', PolynomialFeatures(degree=90, include_bias=False)),

    ('std_scaler', StandardScaler()),

    ])


X_train_poly_scaled = poly_scaler.fit_transform(X_train)

X_val_poly_scaled = poly_scaler.transform(X_val)


sgd_reg = SGDRegressor(max_iter=1, penalty=None, eta0=0.0005, warm_start=True,

                                                        learning_rate='constant', random_state=42)


n_epochs = 500

train_errors, val_errors = [], []


for epoch in range(n_epochs):

    sgd_reg.fit(X_train_poly_scaled, y_train)

    y_train_predict = sgd_reg.predict(X_train_poly_scaled)

    y_val_predict = sgd_reg.predict(X_val_poly_scaled)

    train_errors.append(mean_squared_error(y_train, y_train_predict))

    val_errors.append(mean_squared_error(y_val, y_val_predict))

    

best_epoch = np.argmin(val_errors)

best_val_rmse = np.sqrt(val_errors[best_epoch])


plt.annotate('best model', xy = (best_epoch, best_val_rmse), xytext =(best_epoch, best_val_rmse+1),

                            ha='center', arrowprops =dict(facecolor='black', shrink=0.05), fontsize=16,)


best_val_rmse -= 0.03 #단지 그래프를 더 잘 보기 위함

plt.plot([0, n_epochs], [best_val_rmse, best_val_rmse], 'k:', linewidth=2)

plt.plot(np.sqrt(val_errors), 'b-', linewidth=3, label='validation set')

plt.plot(np.sqrt(train_errors), 'r--', linewidth=2, label='test set')

plt.legend(loc='upper right', fontsize=14)

plt.xlabel('epoch', fontsize=14)

plt.ylabel('RMSE', fontsize=14)

plt.show()



다음은 조기 종료를 위한 기본 구현 코드입니다.


from sklearn.base import clone


#데이터 준비

poly_sclaer = Pipeline ([

    ('poly_features', PolynomialFeatures(degree=90, include_bias=False)),

    ('std_scaler', StandardScaler()) ])


X_train_poly_scaled = poly_scaler.fit_transform(X_train)

y_val_poly_scaled = poly_scaler.transform(X_val)


sgd_reg = SGDRegressor(n_iter=1, warm_start=True, penalty=None, learning_rate='constant',

                                                       eta0 = 0.0005)


minimum_val_error = float('inf')

best_epoch = None

best_model= None

for epoch in range(1000) :

    sgd_reg.fit(X_train_poly_scaled, y_train) #훈련을 이어서 진행

    y_val_predict  = sgd_reg.predict(X_val_poly_scaled)

    val_error = mean_squared_error(y_val, y_val_predict)

    if val_error < minimum_val_error:

        minimum_val_error = val_error

        best_epoch = epoch

        best_model = clone(sgd_reg)


#warm_start = True로 지정하면, fit()메서드가 호출될 때 처음부터 다시 시작하지 않고 이전 모델 파라미터에서 훈련을 이어갑니다.

(227,
 SGDRegressor(alpha=0.0001, average=False, early_stopping=False, epsilon=0.1,
        eta0=0.0005, fit_intercept=True, l1_ratio=0.15,
        learning_rate='constant', loss='squared_loss', max_iter=None,
        n_iter=1, n_iter_no_change=5, penalty=None, power_t=0.25,
        random_state=None, shuffle=True, tol=None, validation_fraction=0.1,
        verbose=0, warm_start=True))


 블로그 

출처


이 글의 상당 부분은  [핸즈온 머신러닝, 한빛미디어/오렐리앙 제롱/박해선] 서적을 참고하였습니다.


나머지는 부수적인 함수나 메서드에 대해 부족한 설명을 적어두었습니다.

학습용으로 포스팅 하는 것이기 때문에 복제보다는 머신러닝에 관심이 있다면 구매해보시길 추천합니다.


도움이 되셨다면 로그인 없이 가능한

아래 하트♥공감 버튼을 꾹 눌러주세요! 



반응형