들어가며

지난 글에서는 로지스틱 함수를 사용하여 두 개의 클래스를 구별하는 이진 분류기에 대해서 다루어보았습니다. 이번에는 여러개의 클래스를 한 번에 분류할 수 있는 소프트맥스 회귀에 대해서 알아보도록 하겠습니다.

목차

1. 소프트맥스의 정의

2. 비용함수(크로스 엔트로피 비용 함수)

3. 코드 구현

1. 소프트맥스의 정의

소프트맥스 회귀 또는 다항 로지시틱 회귀는 소프트 맥스 함수(softmax function)을 사용하여 각 클래스일 확률을 추정하여 이 중 가장 높은 확률을 가진 클래스를 선택하는 것입니다. 이를 쉽게 알아보겠습니다.

 

우선 각 클래스는 자신만의 파라미터 벡터가 존재하며 새로운 샘플이 입력되었을 경우 각 클래스의 벡터와 샘플의 곱을 계산하여 이 중 가장 큰값을 선택하는 방식입니다.

출처 : https://dojinkimm.github.io/ml/2019/11/10/handson-ch4.html

따라서 위와 같은 식이 성립되고 s(x)는 대게 확률이 아니라 일정값이 출력됩니다. 따라서 이를 확률값으로 추출하기 위해서 소프트맥스 함수를 사용합니다.

결국 추정된 클래스는 확률이 가장높은 = s(x)의 값이 가장 높은 클래스가 정해지기에 사실상 클래스만 추정하고 확률은 필요없을 때 마지막에 소프트맥스 함수를 사용하는것은 생략해도됩니다.

2. 비용함수

클래스를 추정하는 방법은 간단히 알아보았으니 이번에는 모델을 학습시키기 위해서 비용함수를 구하는 방법에 대해서 알아보도록 하겠습니다.

여기서 사용할 비용함수는 크로스 엔트로피 함수(cross-entropy function)으로서 타킷 클래스와 예측확률값의 로그값을 곱하것들의 합에 부호를 바꾼것을 의미합니다.

이렇게 한다면 타킷 클래스에 대한 확률값이 감소할 수록 비용함수는 증가하는 구조를 가지고 있기에 비용함수로 적절합니다.

(y_k(i)는 해당 클래스라면 1 다른 클래스라면 0을 나타내는 one-hot 인코딩된 레이블을 의미합니다.)

 

자 이제 비용함수를 구했으니 이에 대한 gradient vector(편도함수)를 구해보도록 하겠습니다.

gradient vector를 보니 비용함수는 복잡했지만 이는 단순히 예측확률에 타깃값을 제외하고 샘플을 곱한 것입니다. 이렇게 단순해진이유는 사실 고의적으로 수학적 기교를 이용해서 의도했기 때문입니다. (안그러면 굳이 확률을 구하기위해 exp를 하거나 비용함수로 로그값을 사용하지 않아도됩니다.) 따라서 매번 학습을 진행할 때 간단한 연산을 통해 빠르게 학습이 가능합니다.

3.코드구현

마지막으로 소프트맥스 회귀를 코드로 구현해보겟습니다. 소프트맥스 회귀는 따로 모듈이 존재하지 않고 LogisticRegression에서 multi_class="multinomial', solver="lbfgs"를 설정함으로써 구현가능합니다.

y = iris["target"]

softmax_reg = LogisticRegression(multi_class="multinomial",solver="lbfgs", C=10, random_state=42)
softmax_reg.fit(X, y)

예측값을 통해 결정경계 표시 그래프 생성

x0, x1 = np.meshgrid(
        np.linspace(0, 8, 500).reshape(-1, 1),
        np.linspace(0, 3.5, 200).reshape(-1, 1),
    )
X_new = np.c_[x0.ravel(), x1.ravel()]


y_proba = softmax_reg.predict_proba(X_new)
y_predict = softmax_reg.predict(X_new)

zz1 = y_proba[:, 1].reshape(x0.shape)
zz = y_predict.reshape(x0.shape)

plt.figure(figsize=(10, 4))
plt.plot(X[y==2, 0], X[y==2, 1], "g^", label="Iris-Virginica")
plt.plot(X[y==1, 0], X[y==1, 1], "bs", label="Iris-Versicolor")
plt.plot(X[y==0, 0], X[y==0, 1], "yo", label="Iris-Setosa")

from matplotlib.colors import ListedColormap
custom_cmap = ListedColormap(['#fafab0','#9898ff','#a0faa0'])

plt.contourf(x0, x1, zz, cmap=custom_cmap)
contour = plt.contour(x0, x1, zz1, cmap=plt.cm.brg)
plt.clabel(contour, inline=1, fontsize=12)
plt.xlabel("Petal length", fontsize=14)
plt.ylabel("Petal width", fontsize=14)
plt.legend(loc="center left", fontsize=14)
plt.axis([0, 7, 0, 3.5])
save_fig("softmax_regression_contour_plot")
plt.show()

들어가며

지난 시간에는 선형회귀에 이어 다항회귀에 대해서 알아보았습니다. 이번에는 회귀를 통해서 분류(Clustering)을 진행할 수 있는 알고리즘인 Logistic Regression 에 대해서 알아보도록 하겠습니다. 

 

목차

1. Logistic Regression

    1.1 확률추정

    1.2 훈련과 비용함수

    1.3 결정경계

 

1. Logistic Regression

로지스틱 회귀란 기본적으로 회귀 알고리즘을 사용하지만 마지막 결과값에서 로지스틱 함수를 통해서 확률을 계산하고 이를 토대로 분류를 진행하는 알고리즘입니다.

출처 : https://dojinkimm.github.io/ml/2019/11/10/handson-ch4.html

위 수식을 보면 파라미터와 샘플의 곱까지는 기존의 회귀와 동일하지만 이를 감싸는 새로운 함수를 볼 수 있습니다. 이것이 바로 로지스틱 함수입니다.

로지스틱 함수는 변수를 입력할 경우 이를 0~1의 범위로 변환해주는 함수 입니다. 즉 이 함수를 통해서 확률값을 계산하고 마지막으로 분류(어떤 클래스에 대한 확률이 가장 높은지 계산)할 수 있습니다.

위 계산식 처럼 마지막 결과값이 50%이상인경우는 1 50%미만인 경우는 0으로 출력하여 분류를 할 수 있습니다.

1.1 확률추정

확률계산하는 방법을 알았으니 이제 파라미터가 학습을 하기 위해서는 비용함수를 구해야합니다. 

하나의 훈련 샘플에 대한 비용함수는 클래스가 0인 경우 결괏값이 1로 갈 수록 증가해야하며 1인 경우는 결괏값이 0으로 갈수록 증가해야합니다. 따라서 아래의 공식이 적합합니다.

1.2 훈련과 비용함수

하나의 샘플에 대한 비용함수를 알았으니 전체 비용함수는 모든 샘플에 대한 비용함수의 평균을 구하면 됩니다.

Logistic 비용함수(log loss)는 선형 회귀와는 다르게 정규방정식이 존재하지 않습니다. 그러나 log loss는 볼록함수이기 때문에 경사항강법으로 global minimum에 수렴하는 것이 보장됩니다.

 

꽃잎에 대한 데이터가 담긴 Iris데이터를 예로 들어 구현해보겠습니다.

# iris데이터를 불러옵니다.
from sklearn import datasets
import numpy as np
iris = datasets.load_iris()
list(iris.keys())

# 꽃잎의 너비와 샘플의 클래스가 Iris-Vriginica인 경우의 인덱스를 1로 아니면 0으로하는 리스트를 생성
X = iris["data"][:, 3:]  # petal width
y = (iris["target"] == 2).astype(np.int)  # 1 if Iris-Virginica, else 0

# 선형 알고리즘을 사용하기 위해 solver="liblinear" 호출후 데이터를 학습시킵니다.
from sklearn.linear_model import LogisticRegression
log_reg = LogisticRegression(solver="liblinear", random_state=42)
log_reg.fit(X, y)

# 그래프로 출력해봅니다.
X_new = np.linspace(0, 3, 1000).reshape(-1, 1)
y_proba = log_reg.predict_proba(X_new)

plt.plot(X_new, y_proba[:, 1], "g-", linewidth=2, label="Iris-Virginica")
plt.plot(X_new, y_proba[:, 0], "b--", linewidth=2, label="Not Iris-Virginica")

코드를 통해서 꽃잎의 넓이를 통해서 종류를 예측하는 모델을 생성했습니다.

1.3 결정경계

위 그래프는 이전 그래프에서 내용을 조금 추가한 것입니다. 여기서 중요한 점은 두 곡선이 교차하는 결정경계입니다. 훈련된 모델을 Petal width가 1.6 근처인 지점에서 결과값이 바뀌는 것을 알 수 있습니다.

들어가며

지난글에서는 학습곡선을 통해서 모델 학습이 과대적합인지 과소적합인지 파악하여 모델을 결정하는방법에 대해서 알아보았습니다. 과소적합인 경우에는 더 좋은 모델과 하이퍼파라미터를 선택하면되지만 과대적합인 경우에는 어떻게 해결해야할까요? 이번 글에서는 비용함수에 변수를 추가하여 모델을 규제하여 과대적합을 방지하는 방법에 대해서 알아보겠습니다.

 

목차

1. 과대적합 발생이유

2. 과대적합 해소방법

    1.릿지 회귀(티호노프 규제)

    2.라쏘 회귀(Lasso Regression)

    3.엘라스틱넷

    4.조기종료

 

3.1 과대적합 발생이유

과대적합이 발생하는 이유는 각 파라미터 변수들의 절댓값이 크게 설정되기 때문입니다. 아래 그림은 이후에 설명할 규제의 정도에 따른 학습결과인데 미리 이를 예로 설명하겠습니다. 오른쪽 그림은 차수를 10차로 설정한 그림입니다. 파란색 곡선을 보면 다른 곡선보다 데이터를 따라가기 위해서 과하게 출렁이는 모습을 볼 수 있습니다. 이런 경우가 바로 파라미터(가중치, 각 항의 계수)들의 절댓값이 크기에 발생합니다. 

3.2. 과대적합 해소방법

위에서 파라미터의 절댓값이 크기에 과대적합이 발생하는 사실을 알았습니다. 그렇다면 과대적합을 해소하기 위해서는 인위적으로 파라미터의 절댓값을 감소시켜야합니다. 이를 위한 방법은 여러가지가 있습니다. 이를 차례대로 알아보도록 하겠습니다.

3.2.1 릿지회귀( 티호노프 회귀 )

첫 번째 방법은 학습에 사용하는 비용함수에 L2노름을 추가하는 것입니다. 이렇게 하면 편도함수를 구할 때 기존의 MSE 편도함수 뿐만아니라 알파와 해당 파라미터값을 곱한 값을 합하여 구하게 될 것입니다. 이는 곧 파라미터 갱신에 사용되므로 결과적으로 L2노름을 사용하지 않았을 때보다 파라미터의 절댓값 변화가 낮아지게되는 것입니다.

위 그림을 정확히 설명하자면 릿지 회귀에서 알파값에 따른 곡선의 변화를 나타냅니다. 알파값이 증가할 수록 각 파라미터가 낮아져 곡선의 복잡성이 낮아져 오버피팅이 줄어드는 것을 확인할 수 있습니다. 참고로 릿지 회귀는 특성의 범위에 민감하게 반응하므로 학습이전에 StandardScaling이 필요합니다.

코드구현

코드로는 sklearn.linear_model에서 직접 Ridge모듈을 선택하거나 SGDRegressor에서 penalty를 l2로 설정하는 두 가지 방법이 있습니다. Ridge에서 solver를 cholesky로 설정할 경우 정규방정식을 통해서 학습을 진행합니다.

1. Ridge

from sklearn.linear_model import Ridge
ridge_reg = Ridge(alpha=1, solver="cholesky", random_state=42)
ridge_reg.fit(X, y)
ridge_reg.predict([[1.5]])
#result = array([[1.55071465]])

2. SGDRegressor

sgd_reg = SGDRegressor(max_iter=50, tol=-np.infty, penalty="l2", random_state=42)
sgd_reg.fit(X, y.ravel())
sgd_reg.predict([[1.5]])
# resutl = array([1.49905184])

3.2.2 Lasso 회귀

Lasso는 least absolute shrinkage and selection operator의 줄임말로서 쉽게 해석하면 중요한 특성에 맞춰서 축약한다는 소리입니다. 우선 Lasso는 릿지회귀와 비슷하게 비용함수에 변수를 추가하는데 이 때는 L1노름을 추가합니다.

출처 : https://dojinkimm.github.io/ml/2019/11/10/handson-ch4.html

이제 위 비용함수가 어떻게 특성을 축약하는지 알아봅시다. 위 비용함수를 파라미터 갱신에 사용한다고 생각합시다. 편도함수를 구하면 새로 추가한 L1노름 부분은 알파값과 해당 파라미터의 양,음에 따라서 부호가 정해질 것입니다. 즉 갱신을 할 때 모든 파라미터들은 알파값이라는 동일한 값을 통해서 갱신이 진행됩니다. 따라서 초기값이 작은 파라미터의 경우 먼저 0으로 수렴하게 됩니다. 아래의 그림으로 더 쉽게 설명하겠습니다.

왼쪽 위의 경우에서 둥근 등고선은 MSE비용함수를 나타내고 삼각형은 L1노름을 나타냅니다. 이 부분에서 노란 삼각형은 초기에 (0.25,-1)부분에서 파라미터가 시작한 경우 갱신이 진행되는 과정을 설명하고있는 것입니다. L1노름의 경우에는 기울기의 절댓값이 1로 각 파라미터의 절댓값이 감소하는 것이 보입니다. (기울기가 1인 이유는 변수 알파에 의해서만 갱신이 되기 때문입니다.) 이렇게 진행되다가 세타1이 먼저 0으로 수렴하고 이후 다시 세타2가 0으로 수렴하고 있는 모습입니다. 즉 L1노름이 계속 갱신하게 되면 별로 중요하지 않은 파라미터는 먼저 0으로 수렴하여 삭제되고 이 후 살아남은 파라미터가 모델을 학습하는 모습을 볼 수 있습니다. 최종적으로 오른쪽 두 개의 그래프를 살펴보면 Lasso는 별로 중요하지 않은 세타2는 삭제한 반면 릿지는 모든 파라미터를 활용하여 최적화가되는 모습을 볼 수 있습니다.

 

이러한 특성때문에 중요한 몇개의 특성만으로 모델을 표현하고 싶은 경우(특성 축소)에 라쏘를 선택할 수 있지만 릿지만큼의 정확성을 기대하는것은 힘들 수 있습니다.

 

코드구현

라쏘 또한 릿지함수와 마찬가지로 sklearn.linear_model에서 Lasso모듈을 사용하여 구현할 수 있습니다.

 

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)

save_fig("lasso_regression_plot")
plt.show()

3.2.3 엘라스틱넷

엘라스틱넷은 한 마디로 릿지와 Lasso회귀를 섞어놓은 것을 의미합니다.

출처 : https://dojinkimm.github.io/ml/2019/11/10/handson-ch4.html

릿지와 라쏘를 이해했다면 위 공식은 아주 쉬울것입니다. 혼합비율 상수 r을 사용하여 릿지와 라쏘를 조율하여 둘 다 사용하고있는 모습입니다. 보통 라쏘보다 엘라스틱넷이 더 선호되는데 그 이유는 만약 샘플수가 특성수 보다 많은 경우나 특성들이 강한 상관관계를 지닌경우 Lasso가 제대로 작동하지 않기 때문입니다.

 

코드구현

from sklear.linear_model import ElasticNet

elastic_net = ElasticNet(alpha=0.1, l1_ratio=0.5)
elastic_net.fit(X,y)
elastic_net.predict([[1.5]])
# array([1.54333xxx])

3.2.4 조기종료 (early stopping)

조기종료는 말 그대로 학습을 진행하는 와중 학습을 종료하는 것입니다. 이는 아주 간단한 개념이면서도 좋은 결과를 얻을 수 있는데 점진적으로 학습이 진행될때 검증 세트의 결과값이 최소이며 앞으로 더 좋은 결과가 기대대지 않을경우 해당 최소 구간에서 학습을 종료하는 것을 의미합니다.

들어가며

이전 글에서는 데이터 간의 비선형관계를 구하기 위한 다항회귀에 대해서 알아보았습니다. 이제 데이터에 적합한 차수(degree( 2차, 3차 등 ))를 어떻게 구할지 고민해야합니다. 이번에는 학습곡선을 통해서 다항회귀를 할때 차수를 적절하게 구하는 방법에 대해서 알아보도록 하겠습니다.

목차

1. 차수의 중요성

2. 학습곡선을 통한 적합성파악

3. 모델의 일반화 오차

 

1. 차수의 중요성

출처 : https://github.com/ageron/handson-ml2/blob/master/04_training_linear_models.ipynb

위 그림은 이전 글(다항회귀)에서 사용한 2차곡선형의 데이터 샘플입니다. 만약 Degree를 300이나 1로 지정할 경우 어떻게 되는지 한번 알아보도록 하겠습니다.

from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

for style, width, degree in (("g-", 1, 300), ("b--", 2, 2), ("r-+", 2, 1)):
    polybig_features = PolynomialFeatures(degree=degree, include_bias=False)
    std_scaler = StandardScaler()
    lin_reg = LinearRegression()
    polynomial_regression = Pipeline([
            ("poly_features", polybig_features),
            ("std_scaler", std_scaler),
            ("lin_reg", lin_reg),
        ])
    polynomial_regression.fit(X, y)
    y_newbig = polynomial_regression.predict(X_new)
    plt.plot(X_new, y_newbig, style, label=str(degree), linewidth=width)

plt.plot(X, y, "b.", linewidth=3)
plt.legend(loc="upper left")
plt.xlabel("$x_1$", fontsize=18)
plt.ylabel("$y$", rotation=0, fontsize=18)
plt.axis([-3, 3, 0, 10])
save_fig("high_degree_polynomials_plot")
plt.show()

코드 구현결과를 보면 300차인 경우 2차에 비해서 데이터에 최대한 맞추려고 계속 지그재그로 진행하는 모습이 보입니다. 

반면 1차의 경우 2차에 비해서 데이터에 비적합한 모습을 보입니다. 즉 너무 degree가 높은 경우 데이터에 과대적합되며 너무 낮은 경우 과소적합이 될 수 있습니다. 이번에는 학습곡선을 이용해서 이 적합도를 한 눈에 파악하도록 구현하겠습니다.

2. 학습곡선을 통한 적합성파악

데이터에서 훈련,검증 세트를 추출하고 훈련세트를 하나부터 점차 학습시키면서 RMSE를 구하여 이를 그래프로 나타내는 학습곡선을 구현하도록 하겠습니다. (여기서는 PolynomialFeatures를 사용하지 않으므로 degree를 1차로 진행하는 직선방정식이라고 생각하시면됩니다.)

from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split

def plot_learning_curves(model, X, y):
    X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=10)
    train_errors, val_errors = [], []
    for m in range(1, len(X_train)):
        model.fit(X_train[:m], y_train[:m])
        y_train_predict = model.predict(X_train[:m])
        y_val_predict = model.predict(X_val)
        train_errors.append(mean_squared_error(y_train[:m], y_train_predict))
        val_errors.append(mean_squared_error(y_val, y_val_predict))

    plt.plot(np.sqrt(train_errors), "r-+", linewidth=2, label="train")
    plt.plot(np.sqrt(val_errors), "b-", linewidth=3, label="val")
    plt.legend(loc="upper right", fontsize=14)   # not shown in the book
    plt.xlabel("Training set size", fontsize=14) # not shown
    plt.ylabel("RMSE", fontsize=14)
lin_reg = LinearRegression()
plot_learning_curves(lin_reg, X, y)
plt.axis([0, 80, 0, 3])                         # not shown in the book
save_fig("underfitting_learning_curves_plot")   # not shown
plt.show() 

곡선을 보니 사이즈가 15이하인 경우까지는 학습이 점차 진행되어 훈련세트와 검증세트의 괴리가 줄어드는 것이 보입니다. 그러나 그 이후 만나는 지점이 1.5 ~ 2.0으로 둘 다 상당한 오차가 발생하는 것으로 보입니다. 이는 전형적인 과소적합의 형태입니다. 이번에는 degree를 10정도로 지정하여 학습곡선을 그려보겠습니다.

from sklearn.pipeline import Pipeline

polynomial_regression = Pipeline([
        ("poly_features", PolynomialFeatures(degree=10, include_bias=False)),
        ("lin_reg", LinearRegression()),
    ])

plot_learning_curves(polynomial_regression, X, y)
plt.axis([0, 80, 0, 3])           # not shown
save_fig("learning_curves_plot")  # not shown
plt.show()   

이번에는 두 곡선의 평균점이 1내외로 이전보다는 나아진것으로 보입니다. 그러나 45부터는 훈련 세트가 증가해도 검증세트와 훈련세트의 RMSE 괴리가 줄어들지 않는 것을 알 수 있습니다. 이는 전형적인 과대적합 모델입니다. 이렇게 degree를 바꿔가며 학습곡선을 파악하면 최적화된 degree를 구할 수 있습니다.

3. 모델의 일반화 오차

모델의 예측값과 결과값 사이의 오차는 일반적으로 3가지 요인을 들 수 있습니다.

1. 편향 : 데이터를 분석할 모델을 잘못 가정하는 것을 의미합니다. 모델 자체의 종류를 잘못 선택하거나 위에서처럼 하이퍼파라미터(degree)를 잘못 설정한 경우입니다. 보통 모델이 복잡할 수록 편향 또한 증가합니다.

2. 분산 : 데이터의 분포정도에 따른 오차발생 정도입니다. 보통 모델이 간단할 경우 예측값과의 분산 정도가 증가합니다.

3. 가우시안 노이즈 : 데이터 자체의 노이즈 정도입니다.

+ Recent posts