My LogisticRegression!

39 minute read

Machine Learining

기본적 세팅

# 파이썬 ≥3.5 필수
import sys
assert sys.version_info >= (3, 5)

# 사이킷런 ≥0.20 필수
import sklearn

# 공통 모듈 임포트
import numpy as np
import os

# 노트북 실행 결과를 동일하게 유지하기 위해
np.random.seed(42)

# 깔끔한 그래프 출력을 위해
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rc('axes', labelsize=14)
mpl.rc('xtick', labelsize=12)
mpl.rc('ytick', labelsize=12)

# 그림을 저장할 위치
PROJECT_ROOT_DIR = "."
CHAPTER_ID = "training_linear_models"
IMAGES_PATH = os.path.join(PROJECT_ROOT_DIR, "images", CHAPTER_ID)
os.makedirs(IMAGES_PATH, exist_ok=True)

def save_fig(fig_id, tight_layout=True, fig_extension="png", resolution=300):
    path = os.path.join(IMAGES_PATH, fig_id + "." + fig_extension)
    print("그림 저장:", fig_id)
    if tight_layout:
        plt.tight_layout()
    plt.savefig(path, format=fig_extension, dpi=resolution)
    
# 어레이 데이터를 csv 파일로 저장하기
def save_data(fileName, arrayName, header=''):
    np.savetxt(fileName, arrayName, delimiter=',', header=header, comments='')

데이터 가져오기

일단 데이터를 불러온다. 데이터는 사이킷런에서 제공하는 데이터를 가져오는데 이는 데이터를 가져오는 것만 사용하므로 로지스틱 회귀에는 사이킷 런의 사용이 전혀 없음을 다시 한 번 알린다.

참고: Bunch 자료형은 기본적으로 dict 자료형과 동일하다. 차이점은 인덱싱을 속성(attribute)처럼 처리할 수 있다는 것 뿐이다.

from sklearn import datasets
iris = datasets.load_iris()

붓꽃 데이터의 키를 알아보았다.

iris.keys()
dict_keys(['DESCR', 'data', 'target', 'feature_names', 'target_names'])

'data' 키에 해당하는 값은 (150, 4) 모양의 넘파이 어레이다.

  • 150: 150개의 붓꽃 샘플
  • 4: 네 개의 특성. 차례대로 꽃받침 길이, 꽃받침 너비, 꽃잎 길이, 꽃잎 너비.

아래 코드는 처음 5개의 샘플 데이터를 보여준다.

참고: 원래 iris['data'][:5]로 값을 확인하지만 Bunch 자료형이기에 아래와 같이 클래스의 속성 형식으로 확인할 수도 있다. 인용부호를 사용하지 않음에 주의해야 한다.

iris.data[:5]
array([[ 5.1,  3.5,  1.4,  0.2],
       [ 4.9,  3. ,  1.4,  0.2],
       [ 4.7,  3.2,  1.3,  0.2],
       [ 4.6,  3.1,  1.5,  0.2],
       [ 5. ,  3.6,  1.4,  0.2]])

'target' 키는 150개 붓꽃에 대한 품종을 1차원 어레이로 담고 있다.

  • 0: 세토사(Iris-Setosa)
  • 1: 버시컬러(Iris-Versicolor)
  • 2: 버지니카(Iris-Virginica)

언급된 순서대로 50개씩 세 품종의 타깃이 저장되어 있으며, 처음 5개의 샘플은 세토사 품종임을 아래처럼 확인할 수 있다.

iris.target[:5]
array([0, 0, 0, 0, 0])

인덱스 50번부터는 버시컬러 종이 있다.

iris.target[50:55]
array([1, 1, 1, 1, 1])

인덱스 100번부터는 버지니카 종이 있다.

iris.target[100:105]
array([2, 2, 2, 2, 2])

과제1 : 로지스틱 회귀 구현

1단계 : 데이터 준비

꽃잎 너비(petal width)를 이용해 버지니카 종인지 아닌지를 분류하는 이진분류 하기 위한 데이터셋을 준비한다.

X = iris["data"][:, 3:]                   # 1개의 특성(꽃잎 너비)만 사용
y = (iris["target"] == 2).astype(np.int)  # 버지니카(Virginica) 품종일 때 1(양성)

모든 샘플에 편향을 추가한다. 이유는 아래 수식을 행렬 연산으로 보다 간단하게 처리하기 위해 0번 특성값 $x_0$이 항상 1이라고 가정하기 때문이다.

$\theta_0\cdot 1 + \theta_1\cdot x_1 + \cdots + \theta_n\cdot x_n = \theta_0\cdot x_0 + \theta_1\cdot x_1 + \cdots + \theta_n\cdot x_n $

X_with_bias = np.c_[np.ones([len(X), 1]), X]

결과를 일정하게 유지하기 위해 랜덤 시드를 지정합니다:

np.random.seed(2042)

2단계 : 데이터 분할

데이터셋을 훈련, 검증, 테스트 용도로 6대 2대 2의 비율로 무작위로 분할한다.

  • 훈련 세트: 60%
  • 검증 세트: 20%
  • 테스트 세트: 20%

아래 코드는 사이킷런의 train_test_split() 함수를 사용하지 않고 수동으로 무작위 분할하는 방법을 보여준다. 먼저 각 세트의 크기를 결정한다.

test_ratio = 0.2                                         # 테스트 세트 비율 = 20%
validation_ratio = 0.2                                   # 검증 세트 비율 = 20%
total_size = len(X_with_bias)                            # 전체 데이터셋 크기

test_size = int(total_size * test_ratio)                 # 테스트 세트 크기: 전체의 20%
validation_size = int(total_size * validation_ratio)     # 검증 세트 크기: 전체의 20%
train_size = total_size - test_size - validation_size    # 훈련 세트 크기: 전체의 60%

데이터셋을 분할할 때 각각의 샘플들이 몰리지 않고 골고루 섞이기 위해 np.random.permutation() 함수를 이용하여 인덱스를 무작위로 섞는다

rnd_indices = np.random.permutation(total_size)

인덱스가 무작위로 섞였기 때문에 다음과 같이 데이터를 분할 하여도 무작위로 뽑는 효과를 얻습니다. 방법은 섞인 인덱스를 이용하여 지정된 6:2:2의 비율로 훈련, 검증, 테스트 세트로 분할하는 것이다.

X_train = X_with_bias[rnd_indices[:train_size]]
y_train = y[rnd_indices[:train_size]]

X_valid = X_with_bias[rnd_indices[train_size:-test_size]]
y_valid = y[rnd_indices[train_size:-test_size]]

X_test = X_with_bias[rnd_indices[-test_size:]]
y_test = y[rnd_indices[-test_size:]]

3단계 : 타깃 변환

처음 데이터를 받아오면 타깃은 0, 1, 2로 설정되어 있다. 차례대로 세토사, 버시컬러, 버지니카 품종을 가리킨다. 하지만 우리는 여기서 버지니카 이냐 아니냐의 이진 분류를 목표로한다.

따라서 1단계에서 레이블을 바꿔주었다. 즉 버지니카 종이면 1(양성) 아니면 0으로 바꿔주었습니다.

y_train[:5]
array([0, 0, 1, 0, 0])

훈련 세트의 레이블이 잘 바뀌어 있습니다.

y_test[:5]
array([0, 1, 1, 0, 0])

테스트 세트의 레이블도 잘 바뀌어 있음이 확인된다.

y_valid[:5]
array([0, 1, 0, 0, 0])

검증 세트의 레이블도 잘 바뀌어 있음이 확인된다.

학습을 위해 타깃을 원-핫 벡터로 변환해야 한다. 여기서 클래스는 1개임이 중요합니다.

그리고 1의 인덱스를 따로 추출하여 그 인덱스를 이용해 변환해주었습니다.

def to_one_hot(y):
    n_classes = y.max()                     # 클래스 수 : 1개
    m = len(y)                              # 샘플 수
    Y_one_hot = np.zeros((m, n_classes))    # (샘플 수, 클래스 수) 0-벡터 생성
    pos = np.where(y>0)                     # y(레이블)에서 1의 인덱스 값들만 추출해냈다.
    Y_one_hot[pos, 0] = 1                   # 샘플 별로 양성의 값만 1로 변경. (넘파이 인덱싱 활용)
    return Y_one_hot

1차원 어레이를 2차원의 (m(샘플의수), 1) 어레이로 바꾸었습니다.

이유는 로지스틱 회귀는 샘플이 주어지면 버지니카로 속할 확률을 구하고 구해진 결과를 실제 확률과 함께 이용하여 비용함수를 계산하기 때문에 함수의 계산을 뒤에서 행렬로 할 때 편리하게 하기 위함입니다.

버지니카 속할 확률을 계산해야 하기 때문에 품종을 0, 1, 2 등의 하나의 숫자로 두기 보다는 버지니카는 1, 나머지는 0인 확률값으로 이루어진 어레이로 다루어야 로지스틱 회귀가 계산한 버지니카(양성) 확률과 연결된다.

Y_train_one_hot = to_one_hot(y_train)
Y_valid_one_hot = to_one_hot(y_valid)
Y_test_one_hot = to_one_hot(y_test)

훈련 세트, 검증 세트, 테스트 세트의 레이블(양성)을 원-핫 벡터로 모두 변환 해 주었습니다.

Y_train_one_hot[:5]
array([[ 0.],
       [ 0.],
       [ 1.],
       [ 0.],
       [ 0.]])

훈련세트를 대표로 확인해보니 잘 바뀌었음을 확인하였습니다.

4단계 : 로지스틱 함수 구현

확률 추정은 아래 식을 통해 이루어진다. $
<파이썬으로\ 구현해야\ 할\ 함수이다.> $

$
\hat p = h_\theta(x) = \sigma(\theta^T\, \mathbf{x}) $

$\sigma()$ 는 시그모이드 함수를 가리킨다.

$
\sigma(t) = \frac{1}{1 + e^{-t}} $

다음은 시그모이드 함수의 그래프이다.

t = np.linspace(-10, 10, 100)
sig = 1 / (1 + np.exp(-t))
plt.figure(figsize=(9, 3))
plt.plot([-10, 10], [0, 0], "k-")
plt.plot([-10, 10], [0.5, 0.5], "k:")
plt.plot([-10, 10], [1, 1], "k:")
plt.plot([0, 0], [-1.1, 1.1], "k-")
plt.plot(t, sig, "b-", linewidth=2, label=r"$\sigma(t) = \frac{1}{1 + e^{-t}}$")
plt.xlabel("t")
plt.legend(loc="upper left", fontsize=20)
plt.axis([-10, 10, -0.1, 1.1])
save_fig("logistic_function_plot")
plt.show()
그림 저장: logistic_function_plot

png

샘플 $\mathbf{x}$에 대한 로지스틱 회귀 예측값은 다음과 같다.

$
\hat y = \begin{cases} 0 & \hat p < 0.5 \text{ 일때}
1 & \hat p \ge 0.5 \text{ 일때} \end{cases} $

$\theta$ : 함수에 해당하는 파라미터 벡터
$\theta$의 개수는 샘플의 특성 수와 같다.

$
\Theta = \begin{bmatrix} \theta_0
\theta_1
\vdots &
\theta_n \end{bmatrix} $

따라서 (n, 1) 행렬 모양의 2차원 어레이로 훈련 과정에서 학습되어진다.

아래에서 정의되는 myLogistic() 함수는 $\theta^T\, \mathbf{x}$로 계산되어지는 각 샘플에 대한 양성에 대한 점수로 이루어진 (샘플 수, 1)-행렬 모양의 인자를 기대한다.

아래 훈련 과정에서 사용되는 myLogistic() 함수의 인자는 다음과 같은 모양의 2차원 넘파이 어레이다.

$\mathbf{x}^{(i)}$는 $i$번째 샘플을 가리킨다.

$
\mathbf{X}_{\textit{train}}\, \Theta = \begin{bmatrix} \theta^T\, \mathbf{x}^{(1)}
\theta^T\, \mathbf{x}^{(2)}
\vdots &
\theta^T\, \mathbf{x}^{(m)} \end{bmatrix} $

def myLogistic(logits):
    exps = 1/(1+np.exp(-logits))                     # 샘플별로 점수를 구한 후 시그모이드 함수 적용
    return exps                                      # 샘플별 로지스틱 점수로 이루어진 어레이 반환 즉, 양성에 대한 확률의 어레이를 반환한다.

5단계 : 경사하강법 활용 훈련

경사하강법을 구현하기 위해 아래 비용함수와 비용함수의 그레이디언트를 파이썬으로 구현할 수 있어야 한다.

  • 비용 함수($m$은 샘플 수) :

$
J(\boldsymbol{\theta}) = -\dfrac{1}{m} \sum\limits_{i=1}^{m}{\left[ y^{(i)} log\left(\hat{p}^{(i)}\right) + (1 - y^{(i)}) log\left(1 - \hat{p}^{(i)}\right)\right]} $

  • 그레이디언트 공식 :

$
\dfrac{\partial}{\partial \theta_j} \text{J}(\boldsymbol{\theta}) = \dfrac{1}{m}\sum\limits_{i=1}^{m}\left(\mathbf{\sigma(\boldsymbol{\theta}}^T \mathbf{x}^{(i)}) - y^{(i)}\right)\, x_j^{(i)} $

주의사항: 수학적으로 $\hat{p}^{(i)} = 0$이면 $\log\left(\hat{p}^{(i)}\right)$는 정의되지 않는다. NaN 값을 피하기 위해 $\hat{p}_{(i)}$에 작은 값 epsilon 추가한다. 여기서는 1e-7을 사용한다. 사실 너무 작은 epsilon을 0에 더하면 컴퓨터가 그래도 0으로 취급할 수 있음에 조심해야 한다.

버지니아(양성)의 파라미터로 이루어진 $(n+1, 1)$ 행렬 모양의 2차원 넘파이 어레이 $\Theta$를 생성하기 위해 $n+1$을 확인한다.

n_inputs = X_train.shape[1]           # 특성 수(n) + 1, 여기서는 특성수(1개) + 1 = 2
n_inputs
2

2가 확인되었습니다.

파라미터 $\Theta$를 무작위로 초기 설정한다.

Theta = np.random.randn(n_inputs, 1)

확인 해보겠습니다.

Theta
array([[ 0.11330361],
       [-0.23452355]])

무작위로 설정된 파라미터입니다.

배치 경사하강법 훈련은 아래 코드를 통해 이루어진다.

배치 경사 하강법은 전체 데이터셋을 훈련한 후에 파라미터를 조정하는 훈련방법입니다.

  • eta = 0.01: 학습률
  • n_iterations = 5001 : 에포크 수
  • m = len(X_train): 훈련 세트 크기, 즉 훈련 샘플 수
  • epsilon = 1e-7: $\log$ 값이 항상 계산되도록 더해지는 작은 실수
  • logits: 모든 샘플에 대한 양성의 점수, 즉 $\mathbf{X}_{\textit{train}}\, \Theta$
  • Y_proba: 모든 샘플에 대해 계산된 양성 확률, 즉 $\hat P$
#  배치 경사하강법 구현
eta = 0.01
n_iterations = 5001
m = len(X_train)
epsilon = 1e-7

for iteration in range(n_iterations):     # 5001번 반복 훈련
    logits = X_train.dot(Theta)
    Y_proba = myLogistic(logits)
    
    if iteration % 500 == 0:              # 500 에포크마다 손실(비용) 계산해서 출력
        loss = -np.mean(np.sum(Y_train_one_hot * np.log(Y_proba + epsilon), axis=1))
        print(iteration, loss)
    
    error = Y_proba - Y_train_one_hot     # 그레이디언트 계산.
    gradients = 1/m * X_train.T.dot(error)
    
    Theta = Theta - eta * gradients       # 파라미터 업데이트
0 0.2971334343
500 0.219459217287
1000 0.192330000328
1500 0.174697592316
2000 0.162280918735
2500 0.153086309606
3000 0.145982369172
3500 0.140298666885
4000 0.135620401701
4500 0.131679841757
5000 0.128297375165

각각의 500번 마다 비용(손실)을 출력하여 확인해보니 점점 비용(손실)이 줄어들었음을 확인 할 수 있습니다.

Theta
array([[-3.40439513],
       [ 2.10506706]])

다음은 훈련된 파라미터 $\Theta$의 값입니다.

검증세트로 정확도 확인

검증 세트에 대한 예측과 정확도는 다음과 같다. logits, Y_proba를 검증 세트인 X_valid를 이용하여 계산한다.

예측값은 Y_proba가 0.5 이상이면 1(양성) 미만이면 0(음성)으로 한다.

일단 로지스틱 회귀에서 구한 점수(시그모이드 함수에서 계산된 값)들을 최종적으로 양성인지 음성인지 예측해주는 myLogisticPredict() 함수를 만들었습니다.

def myLogisticPredict(Y_proba):
  a = np.where(Y_proba >= 0.5, 1, Y_proba)
  b = np.where(a < 0.5, 0, a)
  return b

훈련 세트로 훈련된 5개의 샘플에 대한 확률값들로 확인해보겠습니다.

Y_proba[:5]
array([[ 0.04819737],
       [ 0.33900575],
       [ 0.69118405],
       [ 0.33900575],
       [ 0.33900575]])

훈련세트로 사용된 5개 샘플에 대한 양성에 대한 확률값입니다.

myLogisticPredict(Y_proba)[:5]
array([[ 0.],
       [ 0.],
       [ 1.],
       [ 0.],
       [ 0.]])

실제 예측 값으로 1이면 양성(버지니카)으로 예측하고 0이면 음성(버지니카가 아니다)로 예측되었음을 확인하였습니다.

logits = X_valid.dot(Theta)              
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)          # 확률에 따라 0.5 이상이면 1(양성)으로 , 미만이면 0(음성)으로 예측

accuracy_score = np.mean(y_predict == Y_valid_one_hot)  # 정확도 계산
accuracy_score
0.96666666666666667

96% 정도의 정확도를 보이고 있습니다. (이때, y_valid가 아닌 Y_valid_one_hot을 써서 정확도를 구한 것이 핵심입니다. y_predict와 형태를 맞추기 위함입니다. 저는 따로 myLogisticPredict()함수를 정의 했기 때문입니다.)

훈련세트에 대한 예측의 정확도와 비교해보기 위해 훈련세트의 정확도도 확인해보았습니다.

logits = X_train.dot(Theta)              
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)          # 확률에 따라 0.5 이상이면 1(양성)으로 , 미만이면 0(음성)으로 예측

accuracy_score = np.mean(y_predict == Y_train_one_hot)  # 정확도 계산
accuracy_score
0.96666666666666667

96% 로 검증 세트의 정확도와 비슷하다.

특성을 2개나 3개 그 이상으로 늘린다면 더 높은 정확도를 기대할 수 있을까라는 기대감으로 특성을 2개로 다시 해보겠습니다.

특성을 2개로 추가하여 확인

꽃잎의 길이와 너비를 특성으로 하는 데이터세트와 레이블을 이용합니다.

X = iris["data"][:, (2, 3)]  # 꽃잎의 길이와 너비
y = (iris["target"] == 2).astype(np.int)  # 버지니카(Virginica) 품종일 때 1(양성)

행렬 계산을 위해 편향을 추가합니다.

X_with_bias = np.c_[np.ones([len(X), 1]), X] # 편향 추가

데이터세트를 훈련, 검증, 테스트 세트로 분할합니다. 비율은 6:2:2입니다.

X_train = X_with_bias[rnd_indices[:train_size]]
y_train = y[rnd_indices[:train_size]]

X_valid = X_with_bias[rnd_indices[train_size:-test_size]]
y_valid = y[rnd_indices[train_size:-test_size]]

X_test = X_with_bias[rnd_indices[-test_size:]]
y_test = y[rnd_indices[-test_size:]]

레이블을 다시 원핫 벡터로 바꾸어줍니다.

Y_train_one_hot = to_one_hot(y_train)
Y_valid_one_hot = to_one_hot(y_valid)
Y_test_one_hot = to_one_hot(y_test)

파라미터의 갯수를 위해 파라미터 수를 받습니다.

n_inputs = X_train.shape[1]   # 세타의 특성수 +1

파라미터의 갯수는 특성수 +1 이므로 3임이 확인되었습니다.

n_inputs
3

파라미터를 무작위로 초기 설정합니다.

Theta = np.random.randn(n_inputs, 1) # 무작위 세타

초기 설정된 파라미터 값들을 확인하였습니다.

Theta 
array([[-0.20774285],
       [ 0.43433246],
       [-0.66647126]])

배치 경사하강법을 이용한 실제 훈련입니다.

#  배치 경사하강법 구현
eta = 0.01
n_iterations = 5001
m = len(X_train)
epsilon = 1e-7

for iteration in range(n_iterations):     # 5001번 반복 훈련
    logits = X_train.dot(Theta)
    Y_proba = myLogistic(logits)
    
    if iteration % 500 == 0:              # 500 에포크마다 손실(비용) 계산해서 출력
        loss = -np.mean(np.sum(Y_train_one_hot * np.log(Y_proba + epsilon), axis=1))
        print(iteration, loss)
    
    error = Y_proba - Y_train_one_hot     # 그레이디언트 계산.
    gradients = 1/m * X_train.T.dot(error)
    
    Theta = Theta - eta * gradients       # 파라미터 업데이트
0 0.118258644532
500 0.2116901005
1000 0.188538425688
1500 0.172640667974
2000 0.161132902379
2500 0.152392257481
3000 0.145484802929
3500 0.139849872141
4000 0.135134825673
4500 0.131107969108
5000 0.127611054964

500번의 에포크 반복 훈련마다 손실을 출력해서 확인할수 있습니다.

Theta
array([[-3.78079762],
       [ 0.30362874],
       [ 1.43820006]])

훈련된 파라미터의 값입니다.

이번에도 검증세트를 이용한 정확도를 검사해 보겠습니다.

logits = X_valid.dot(Theta)              
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)          # 확률에 따라 0.5 이상이면 1(양성)으로 , 미만이면 0(음성)으로 예측

accuracy_score = np.mean(y_predict == Y_valid_one_hot)  # 정확도 계산
accuracy_score
0.96666666666666667

이번에도 96%로 특성 1개일때와 비슷하였습니다.

혹시나 하여 검증세트의 예측값과 검증세트의 실제 레이블 값을 확인해보았습니다.

검증세트의 예측값을 확인해보았습니다.

y_predict[:10]
array([[ 0.],
       [ 1.],
       [ 0.],
       [ 0.],
       [ 0.],
       [ 1.],
       [ 1.],
       [ 0.],
       [ 0.],
       [ 0.]])

검증세트의 실제 레이블 값을 확인해보았습니다.

Y_valid_one_hot[:10]
array([[ 0.],
       [ 1.],
       [ 0.],
       [ 0.],
       [ 0.],
       [ 1.],
       [ 1.],
       [ 0.],
       [ 0.],
       [ 0.]])

앞의 샘플 10개는 정확한 예측을 하고 있음을 알수 있었습니다.

logits = X_train.dot(Theta)              
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)          # 확률에 따라 0.5 이상이면 1(양성)으로 , 미만이면 0(음성)으로 예측

accuracy_score = np.mean(y_predict == Y_train_one_hot)  # 정확도 계산
accuracy_score
0.96666666666666667

훈련 세트에 대해서도 예측에 대한 정확도를 검사해 보았습니다.

96%로 똑같았습니다.

왜 항상 같은 96%가 나오는지에 대한 이유를 생각해 보았지만, 확실한 해답을 찾진 못하였습니다. 다만, 제 생각에는 꽃잎의 너비가 버지니카의 양성 판단에 가장 큰 영향을 미치기 때문이 아닐까 라는 생각을 하였습니다.

  • 그렇게 생각한 이유는 너비에 대한 가중치 (특성이 1개 일때 $\theta_1$, 특성이 2개일 때 $\theta_2$)의 절댓값이 가장 컷기 때문에 버지니카 예측의 큰 비중을 너비에 두고 있어서 이지 않을까 라는 생각을 하였습니다.

6단계 : 규제 활용 경사하강법 훈련

다시 기존의 특성 1개의 데이터세트를 이용하여 훈련하겠습니다.

특성 1개(꽃잎의 너비)의 데이터와 레이블을 가져옵니다.

X = iris["data"][:, 3:]                   # 1개의 특성(꽃잎 너비)만 사용
y = (iris["target"] == 2).astype(np.int)  # 버지니카(Virginica) 품종일 때 1(양성)

데이터 세트에 편향을 추가합니다.

X_with_bias = np.c_[np.ones([len(X), 1]), X] # 편향 추가

데이터세트 훈련, 검증, 테스트 세트로 분할합니다. 6:2:2

X_train = X_with_bias[rnd_indices[:train_size]]
y_train = y[rnd_indices[:train_size]]

X_valid = X_with_bias[rnd_indices[train_size:-test_size]]
y_valid = y[rnd_indices[train_size:-test_size]]

X_test = X_with_bias[rnd_indices[-test_size:]]
y_test = y[rnd_indices[-test_size:]]

레이블을 원-핫 벡터 처리합니다.

Y_train_one_hot = to_one_hot(y_train)
Y_valid_one_hot = to_one_hot(y_valid)
Y_test_one_hot = to_one_hot(y_test)

파라미터 수를 만들기 위해 특성 수를 받습니다.

n_inputs = X_train.shape[1]   # 세타의 특성수 +1
n_inputs
2

2가 확인되었습니다.(편향의 가중치 $\theta_0$ 포함)

Theta = np.random.randn(n_inputs, 1) # 무작위 세타
Theta
array([[-0.71757054],
       [ 1.0188498 ]])

무작위로 파라미터의 값을 초기 설정합니다.

$\ell_2$ 규제가 추가된 경사하강법 훈련을 구현한다. 코드는 기본적으로 동일하다. 다만 손실(비용)에 $\ell_2$ 페널티가 추가되었고 그래디언트에도 항이 추가되었다(Theta의 첫 번째 원소는 편향이므로 규제하지 않습니다).

  • 학습률 eta 증가됨.
  • alpha = 0.1: 규제 강도
eta = 0.1
n_iterations = 5001
m = len(X_train)
epsilon = 1e-7
alpha = 0.1        # 규제 하이퍼파라미터


for iteration in range(n_iterations):
    logits = X_train.dot(Theta)
    Y_proba = myLogistic(logits)
    
    if iteration % 500 == 0:
        xentropy_loss = -np.mean(np.sum(Y_train_one_hot * np.log(Y_proba + epsilon), axis=1))
        l2_loss = 1/2 * np.sum(np.square(Theta[1:]))  # 편향은 규제에서 제외
        loss = xentropy_loss + alpha * l2_loss        # l2 규제가 추가된 손실
        print(iteration, loss)
    
    error = Y_proba - Y_train_one_hot
    l2_loss_gradients = np.r_[np.zeros([1, 1]), alpha * Theta[1:]]   # l2 규제 그레이디언트
    gradients = 1/m * X_train.T.dot(error) + l2_loss_gradients
    
    Theta = Theta - eta * gradients
0 0.130846930918
500 0.280115020438
1000 0.286454993264
1500 0.28705360973
2000 0.287109487591
2500 0.287114698353
3000 0.287115184227
3500 0.287115229531
4000 0.287115233755
4500 0.287115234149
5000 0.287115234186

손실이 규제가 없을때보다 상대적으로 높아짐이 확인되었습니다.

Theta
array([[-2.42754909],
       [ 1.30151453]])

훈련된 파라미터 값을 확인하였습니다.

logits = X_valid.dot(Theta)
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)

accuracy_score = np.mean(y_predict == Y_valid_one_hot)
accuracy_score
0.90000000000000002

검증세트에 대한 정확도는 90% 입니다. 규제가 없을 때보다 오히려 정확도는 떨어졌습니다.

logits = X_train.dot(Theta)
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)

accuracy_score = np.mean(y_predict == Y_train_one_hot)
accuracy_score
0.88888888888888884

혹시나 하여 훈련세트에 대한 예측도 확인해 보았습니다. 88%였습니다.

규제는 과대적합인 경우 사용하면 효과를 기대 할 수 있지만, 5단계의 검증세트를 이용한 확인 결과 과대적합의 가능성은 낮아 보였으므로 그 규제에 대한 효과는 크게 얻을 수 없었던 것 같습니다.

7단계 : 조기종료를 추가한 훈련

위 규제가 사용된 모델의 훈련 과정에서 매 에포크마다 검증 세트에 대한 손실을 계산하여 오차가 줄어들다가 증가하기 시작할 때 멈추도록 한다.

위 6단계 모델에서 훈련마다 손실을 출력한 부분을 보면 손실의 변화가 5단계보다 상대적으로 작음을 확인할 수 있습니다.

eta = 0.1 
n_iterations = 5001
m = len(X_train)
epsilon = 1e-7
alpha = 0.1            # 규제 하이퍼파라미터
best_loss = np.infty   # 최소 손실값 기억 변수

Theta = np.random.randn(n_inputs, 1) # 무작위 세타



for iteration in range(n_iterations):
    # 훈련 및 손실 계산
    logits = X_train.dot(Theta)
    Y_proba = myLogistic(logits)
    error = Y_proba - Y_train_one_hot
    gradients = 1/m * X_train.T.dot(error) + np.r_[np.zeros([1, 1]), alpha * Theta[1:]]
    Theta = Theta - eta * gradients

    # 검증 세트에 대한 손실 계산
    logits = X_valid.dot(Theta)
    Y_proba = myLogistic(logits)
    xentropy_loss = -np.mean(np.sum(Y_valid_one_hot * np.log(Y_proba + epsilon), axis=1))
    l2_loss = 1/2 * np.sum(np.square(Theta[1:]))
    loss = xentropy_loss + alpha * l2_loss
    
    # 500 에포크마다 검증 세트에 대한 손실 출력
    if iteration % 500 == 0:
        print(iteration, loss)
        
    # 에포크마다 최소 손실값 업데이트
    if loss < best_loss:
        best_loss = loss
    else:                                      # 에포크가 줄어들지 않으면 바로 훈련 종료
        print(iteration - 1, best_loss)        # 종료되지 이전 에포크의 손실값 출력
        print(iteration, loss, "조기 종료!")
        break
0 0.468308168361
86 0.231844003268
87 0.231848241305 조기 종료!

86에서 87번째의 에포크에 손실이 상승하였으므로 조기 종료 되었습니다.

logits = X_valid.dot(Theta)
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)

accuracy_score = np.mean(y_predict == Y_valid_one_hot)
accuracy_score
0.90000000000000002

그럼에도 불구하고 검증세트에 대한 정확도는 여전히 90%입니다.

8단계 : 테스트 세트 정확도 평가

logits = X_test.dot(Theta)
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)

accuracy_score = np.mean(y_predict == Y_test_one_hot)
accuracy_score
0.90000000000000002

90%의 정확도를 보여준다. 최종적으로 검증세트에서 확인한 모델의 정확도와 같다. 하지만 랜덤시드를 바꾸거나, 데이터의 양을 늘리거나 데이터 분할이 달라진다면 다른 결과가 나올 것으로 예측된다.

과제2 : 일대다(OvR)방식 다중클래스 분류 알고리즘 구현

일대다 방식(OvR) : 각각의 클래스에 대한 확률을 계산하고 그 중 가장 높은 확률의 클래스로 예측하는 방식을 말한다.

1단계 : 데이터 준비

데이터 세트를 가져 옵니다. 너비만을 특성으로 사용하고 레이블은 각각의 클래스마다의 레이블을 따로 가져왔습니다.

즉, 세토사, 버시컬러, 버지니카 3가지 클래스에 대한 레이블을 각각 가져왔습니다.

X = iris["data"][:, 3:]                   # 1개의 특성(꽃잎 너비)만 사용
y = iris["target"].astype(np.int)
y0 = (iris["target"] == 0).astype(np.int)  # 세토사 품종일 때 1(양성)
y1 = (iris["target"] == 1).astype(np.int)  # 버시컬러 품종일 때 1(양성)
y2 = (iris["target"] == 2).astype(np.int)  # 버지니카 품종일 때 1(양성)

각 데이터 샘플에 편향을 넣습니다.

X_with_bias = np.c_[np.ones([len(X), 1]), X]

2단계 : 데이터 분할

샘플이 몰리지 않게 인덱스를 섞어서 무작위로 데이터 분할을 할 수 있도록 해줍니다.

rnd_indices = np.random.permutation(total_size)

훈련, 검증, 테스트 세트를 분류합니다.

test_ratio = 0.2                                         # 테스트 세트 비율 = 20%
validation_ratio = 0.2                                   # 검증 세트 비율 = 20%
total_size = len(X_with_bias)                            # 전체 데이터셋 크기

test_size = int(total_size * test_ratio)                 # 테스트 세트 크기: 전체의 20%
validation_size = int(total_size * validation_ratio)     # 검증 세트 크기: 전체의 20%
train_size = total_size - test_size - validation_size    # 훈련 세트 크기: 전체의 60%
X_train = X_with_bias[rnd_indices[:train_size]]
y_train = y[rnd_indices[:train_size]]                 # 3가지 클래스에 대한 레이블
y_train0 = y0[rnd_indices[:train_size]]               #세토사에 대한 훈련 세트의 레이블
y_train1 = y1[rnd_indices[:train_size]]               #버시컬러에 대한 훈련 세트의 레이블
y_train2 = y2[rnd_indices[:train_size]]               #버지니카에 대한 훈련 세트의 레이블

X_valid = X_with_bias[rnd_indices[train_size:-test_size]]
y_train = y[rnd_indices[:train_size]]                    # 3가지 클래스에 대한 레이블
y_valid0 = y0[rnd_indices[train_size:-test_size]]       #세토사에 대한 검증 세트의 레이블
y_valid1 = y1[rnd_indices[train_size:-test_size]]       #버시컬러에 대한 검증 세트의 레이블
y_valid2 = y2[rnd_indices[train_size:-test_size]]       #버지니카에 대한 검증 세트의 레이블

X_test = X_with_bias[rnd_indices[-test_size:]]
y_train = y[rnd_indices[:train_size]]                  # 3가지 클래스에 대한 레이블
y_test0 = y0[rnd_indices[-test_size:]]                 #세토사에 대한 테스트 세트의 레이블
y_test1 = y1[rnd_indices[-test_size:]]                 #버시컬러에 대한 테스트 세트의 레이블
y_test2 = y2[rnd_indices[-test_size:]]                 #버지니카에 대한 테스트 세트의 레이블

3단계 : 타깃 변환

훈련 과정에서 구해진 확률을 이용해 비용함수를 계산할 때 행렬의 계산을 편리하게 하기 위해 각각의 레이블들을 원-핫 벡터로 바꾸어 줍니다.

Y_train_one_hot0 = to_one_hot(y_train0)       #세토사 훈련세트 레이블 원-핫 벡터
Y_valid_one_hot0 = to_one_hot(y_valid0)       #세토사 검증세트 레이블 원-핫 벡터
Y_test_one_hot0 = to_one_hot(y_test0)         #세토사 테스트세트 레이블 원-핫 벡터

Y_train_one_hot1 = to_one_hot(y_train1)       #버시컬러 훈련세트 레이블 원-핫 벡터
Y_valid_one_hot1 = to_one_hot(y_valid1)       #버시컬러 검증세트 레이블 원-핫 벡터
Y_test_one_hot1 = to_one_hot(y_test1)         #버시컬러 테스트세트 레이블 원-핫 벡터

Y_train_one_hot2 = to_one_hot(y_train2)       #버지니카 훈련세트 레이블 원-핫 벡터
Y_valid_one_hot2 = to_one_hot(y_valid2)       #버지니카 검증세트 레이블 원-핫 벡터
Y_test_one_hot2 = to_one_hot(y_test2)         #버지니카 테스트세트 레이블 원-핫 벡터

4단계 : 다중클래스 분류 알고리즘 구현

먼저 각각의 클래스에 대한 로지스틱 회귀 확률을 구하여야 한다.

그러기 위해 각각의 클래스의 모델에 대해 훈련시킨다.

세토사 로지스틱 회귀모델 훈련

n_inputs = X_train.shape[1]  # 특성수 + 1  여기선 2 이다.
n_inputs
2

특성 수에 따라 파라미터를 만들기 위해 사용한 코드이다 (특성수 + 1)

세토사에 대한 모델의 파라미터를 무작위로 초기 설정한다.

Theta0 = np.random.randn(n_inputs, 1)  #Theta0는 세토사에 대한 로지스틱 모델의 파라미터
Theta0
array([[ 0.63844637],
       [-0.50527016]])

과제 1에서 구현한 조기종료와 규제를 추가한 경사하강법으로 훈련시킨다.

eta = 0.1 
n_iterations = 5001
m = len(X_train)
epsilon = 1e-7
alpha = 0.1            # 규제 하이퍼파라미터
best_loss = np.infty   # 최소 손실값 기억 변수





for iteration in range(n_iterations):
    # 훈련 및 손실 계산
    logits = X_train.dot(Theta0)
    Y_proba0 = myLogistic(logits)
    a_proba = Y_proba0
    error = Y_proba0 - Y_train_one_hot0
    gradients = 1/m * X_train.T.dot(error) + np.r_[np.zeros([1, 1]), alpha * Theta0[1:]]
    Theta0 = Theta0 - eta * gradients

    # 검증 세트에 대한 손실 계산
    logits = X_valid.dot(Theta0)
    Y_proba0 = myLogistic(logits)
    xentropy_loss = -np.mean(np.sum(Y_valid_one_hot0 * np.log(Y_proba0 + epsilon), axis=1))
    l2_loss = 1/2 * np.sum(np.square(Theta0[1:]))
    loss = xentropy_loss + alpha * l2_loss
    
    # 500 에포크마다 검증 세트에 대한 손실 출력
    if iteration % 500 == 0:
        print(iteration, loss)
        
    # 에포크마다 최소 손실값 업데이트
    if loss < best_loss:
      best_loss = loss
    else:                                      # 에포크가 줄어들지 않으면 바로 훈련 종료
      print(iteration - 1, best_loss)        # 종료되지 이전 에포크의 손실값 출력
      print(iteration, loss, "조기 종료!")
      break
0 0.141213878039
0 0.141213878039
1 0.145473171472 조기 종료!

1 에포크 째에 손실(비용)이 상승해서 조기 종료 되었습니다. 운이 좋은 경우 인거 같습니다. .

logits = X_valid.dot(Theta0)
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)

accuracy_score = np.mean(y_predict == Y_valid_one_hot0)
accuracy_score
0.96666666666666667

검증세트로 정확도를 평가해보니 약 96% 정도였습니다.

이때 훈련된 파라미터를 확인해보겠습니다.

Theta0
array([[ 0.6096832 ],
       [-0.58061876]])

실제로 초기에 랜덤으로 설정된 파라미터와 값이 유사하게 보입니다. 운이 좋은 경우입니다.

이것이 세토사에 대한 로지스틱 회귀를 위한 훈련된 모델의 파라미터입니다.

우리가 필요한 것은 각각의 클래스에 대한 확률이므로

훈련 세트의 세토사에 대한 확률인 a_proba을 필요로 합니다.

a_proba[:5]
array([[ 0.4384142 ],
       [ 0.62584972],
       [ 0.45185759],
       [ 0.38572218],
       [ 0.51973818]])

각각의 샘플의 세토사 클래스에 대한 확률 입니다.

버시컬러 로지스틱 회귀모델 훈련

버시컬러에 대한 모델의 파라미터를 무작위 초기 설정합니다.

Theta1 = np.random.randn(n_inputs, 1)  #Theta1는 버시컬러에 대한 로지스틱 모델의 파라미터

마찬가지로 과제1에서 구현한 로지스틱 회귀 (규제와 조기종료가 추가된) 경사하강법 훈련을 합니다.

eta = 0.1 
n_iterations = 5001
m = len(X_train)
epsilon = 1e-7
alpha = 0.1            # 규제 하이퍼파라미터
best_loss = np.infty   # 최소 손실값 기억 변수





for iteration in range(n_iterations):
    # 훈련 및 손실 계산
    logits = X_train.dot(Theta1)
    Y_proba1 = myLogistic(logits)
    b_proba = Y_proba1
    error = Y_proba1 - Y_train_one_hot1
    gradients = 1/m * X_train.T.dot(error) + np.r_[np.zeros([1, 1]), alpha * Theta1[1:]]
    Theta1 = Theta1 - eta * gradients

    # 검증 세트에 대한 손실 계산
    logits = X_valid.dot(Theta1)
    Y_proba1 = myLogistic(logits)
    xentropy_loss = -np.mean(np.sum(Y_valid_one_hot1 * np.log(Y_proba1 + epsilon), axis=1))
    l2_loss = 1/2 * np.sum(np.square(Theta1[1:]))
    loss = xentropy_loss + alpha * l2_loss
    
    # 500 에포크마다 검증 세트에 대한 손실 출력
    if iteration % 500 == 0:
        print(iteration, loss)
        
    # 에포크마다 최소 손실값 업데이트
    if loss < best_loss:
        best_loss = loss
    else:                                      # 에포크가 줄어들지 않으면 바로 훈련 종료
        print(iteration - 1, best_loss)        # 종료되지 이전 에포크의 손실값 출력
        print(iteration, loss, "조기 종료!")
        break
0 0.552880210743
53 0.427191502707
54 0.427202700222 조기 종료!

54 에포크째에 상승해서 조기 종료 되었습니다.

logits = X_valid.dot(Theta1)
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)

accuracy_score = np.mean(y_predict == Y_valid_one_hot1)
accuracy_score
0.59999999999999998

검증세트를 이용해 확인한 정확도는 약 60% 확인됩니다.

훈련된 파라미터를 확인해 보겠습니다.

Theta1
array([[-0.11625955],
       [-0.36650515]])

우리가 필요한 것은 각각의 클래스에 대한 확률이므로

훈련 세트의 버시컬러에 대한 확률인 b_proba을 필요로 합니다.

b_proba[:5]
array([[ 0.33115537],
       [ 0.45419084],
       [ 0.33942054],
       [ 0.29915803],
       [ 0.38214714]])

각각 샘플의 버시컬러 클래스에 대한 확률 입니다.

버지니카 로지스틱 회귀모델 훈련

버지니카에 대한 모델의 파라미터를 무작위 초기 설정합니다.

Theta2 = np.random.randn(n_inputs, 1)  #Theta2는 버지니카에 대한 로지스틱 모델의 파라미터

마찬가지로 과제1에서 구현한 로지스틱 회귀 (규제와 조기종료가 추가된) 경사하강법 훈련을 합니다.

eta = 0.1 
n_iterations = 5001
m = len(X_train)
epsilon = 1e-7
alpha = 0.1            # 규제 하이퍼파라미터
best_loss = np.infty   # 최소 손실값 기억 변수





for iteration in range(n_iterations):
    # 훈련 및 손실 계산
    logits = X_train.dot(Theta2)
    Y_proba2 = myLogistic(logits)
    c_proba = Y_proba2
    error = Y_proba2 - Y_train_one_hot2
    gradients = 1/m * X_train.T.dot(error) + np.r_[np.zeros([1, 1]), alpha * Theta2[1:]]
    Theta2 = Theta2 - eta * gradients

    # 검증 세트에 대한 손실 계산
    logits = X_valid.dot(Theta2)
    Y_proba2 = myLogistic(logits)
    xentropy_loss = -np.mean(np.sum(Y_valid_one_hot2 * np.log(Y_proba2 + epsilon), axis=1))
    l2_loss = 1/2 * np.sum(np.square(Theta2[1:]))
    loss = xentropy_loss + alpha * l2_loss
    
    # 500 에포크마다 검증 세트에 대한 손실 출력
    if iteration % 500 == 0:
        print(iteration, loss)
        
    # 에포크마다 최소 손실값 업데이트
    if loss < best_loss:
        best_loss = loss
    else:                                      # 에포크가 줄어들지 않으면 바로 훈련 종료
        print(iteration - 1, best_loss)        # 종료되지 이전 에포크의 손실값 출력
        print(iteration, loss, "조기 종료!")
        break
0 0.736844263195
66 0.220944083936
67 0.220951555855 조기 종료!

67 번째 에포크에서 비용(손실)이 상승하여 조기 종료 되었습니다.

logits = X_valid.dot(Theta2)
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)

accuracy_score = np.mean(y_predict == Y_valid_one_hot2)
accuracy_score
0.8666666666666667

검증세트를 이용해 확인한 정확도는 약 87% 확인됩니다.

훈련된 파라미터를 확인해 보겠습니다.

Theta2
array([[-0.69772669],
       [ 0.41785196]])

우리가 필요한 것은 각각의 클래스에 대한 확률이므로

훈련 세트의 버지니카에 대한 확률인 c_proba을 필요로 합니다.

c_proba[:5]
array([[ 0.4928388 ],
       [ 0.35330881],
       [ 0.48256182],
       [ 0.53392038],
       [ 0.43156905]])

각각의 샘플의 버지니카 클래스에 대한 확률입니다.

다중 클래스 분류 예측

다음은 훈련세트를 이용한 예측이다

이제 샘플마다 각각의 클래스에 대한 확률을 구했으니 가장 큰 확률을 가진 클래스로 예측하도록 해야한다.

먼저 각각의 클래스에 대한 확률들을 하나의 데이터로 합쳐보았다.

multi = np.c_[a_proba, b_proba]
multi[:5]
array([[ 0.4384142 ,  0.33115537],
       [ 0.62584972,  0.45419084],
       [ 0.45185759,  0.33942054],
       [ 0.38572218,  0.29915803],
       [ 0.51973818,  0.38214714]])

먼저 세토사에 대한 확률과 버시컬러 로지스틱에 대한 확률들을 합쳤다.

multiClass = np.c_[multi, c_proba]
multiClass[:5]
array([[ 0.4384142 ,  0.33115537,  0.4928388 ],
       [ 0.62584972,  0.45419084,  0.35330881],
       [ 0.45185759,  0.33942054,  0.48256182],
       [ 0.38572218,  0.29915803,  0.53392038],
       [ 0.51973818,  0.38214714,  0.43156905]])

최종적으로 세가지 클래스에 대한 확률을 하나의 데이터로 합쳤다.

그리고 이제 개별 샘플에 대해 클래스별 확률 중 가장 높은 것을 선택하는 코드를 작성하였다. 인덱스가 곧 클래스이므로 (0:세토사, 1:버시컬러, 2:버지니카) 다음과 같이 작성하였다.

multi_predict = np.argmax(multiClass, axis=1)
multi_predict
array([2, 0, 2, 2, 0, 0, 0, 2, 2, 0, 0, 0, 2, 0, 2, 0, 0, 2, 2, 0, 2, 0, 2,
       0, 2, 0, 0, 0, 0, 2, 2, 0, 0, 2, 2, 2, 0, 2, 0, 2, 0, 0, 0, 2, 2, 0,
       0, 2, 2, 0, 0, 2, 0, 2, 2, 0, 2, 2, 0, 0, 2, 2, 0, 2, 2, 2, 0, 2, 2,
       0, 2, 0, 0, 2, 0, 2, 2, 2, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0], dtype=int64)

즉, 첫번째 샘플은 버지니카로, 두번 째는 세토사로 예측되었다.

이제 훈련세트에 대한 예측의 정확도를 평가하여 보자

accuracy_score = np.mean(multi_predict == y_train)  # 정확도 계산
accuracy_score
0.69999999999999996

약 70%의 정확도를 보입니다.

이제 검증세트에 대해서도 확인하여 보자

검증세트를 훈련된 각각의 클래스에 대한 모델의 파라미터를 이용해 점수를 구한다.

logits0 = X_valid.dot(Theta0)
logits1 = X_valid.dot(Theta1)
logits2 = X_valid.dot(Theta2)

그리고 로지스틱 회귀를 통한 각각의 클래스에 대한 확률을 구한다.

Y_proba0 = myLogistic(logits0)
Y_proba1 = myLogistic(logits1)
Y_proba2 = myLogistic(logits2)

그리고 각각의 확률 데이터들을 하나로 뭉쳐서 최댓값의 인덱스를 출력하도록한다.

(인덱스의 값과 레이블의 값이 동일하도록 되어있기 때문이다.)

 vaild_prob = np.c_[Y_proba0, Y_proba1,Y_proba2]
 valid_predict = np.argmax(vaild_prob, axis=1)
 valid_predict
array([0, 0, 0, 0, 2, 2, 0, 2, 2, 2, 0, 2, 0, 2, 0, 2, 0, 0, 2, 2, 2, 0, 2,
       2, 0, 0, 2, 0, 2, 0], dtype=int64)

각각의 예측 클래스가 확인된다. 0은 세토사 2는 버지니카를 말한다.

1 즉, 버시컬러로 예측하는 경우가 없다. 확률을 확인하여 보자

vaild_prob 
array([[ 0.5072656 ,  0.38159949,  0.43048449],
       [ 0.63451167,  0.4618468 ,  0.34165189],
       [ 0.46378324,  0.35601187,  0.46144689],
       [ 0.62094459,  0.45275125,  0.35111167],
       [ 0.39283031,  0.31518935,  0.51359836],
       [ 0.36550387,  0.29958605,  0.53443969],
       [ 0.62094459,  0.45275125,  0.35111167],
       [ 0.39283031,  0.31518935,  0.51359836],
       [ 0.44937827,  0.34765414,  0.47184633],
       [ 0.32613027,  0.27703411,  0.5654554 ],
       [ 0.60718567,  0.44368716,  0.36068989],
       [ 0.39283031,  0.31518935,  0.51359836],
       [ 0.46378324,  0.35601187,  0.46144689],
       [ 0.31350109,  0.26975383,  0.57569318],
       [ 0.62094459,  0.45275125,  0.35111167],
       [ 0.33901696,  0.28443435,  0.55516147],
       [ 0.59325448,  0.43466039,  0.37038025],
       [ 0.47824891,  0.36445826,  0.45108091],
       [ 0.4350577 ,  0.33938921,  0.48227025],
       [ 0.37907119,  0.30733261,  0.52402948],
       [ 0.4350577 ,  0.33938921,  0.48227025],
       [ 0.62094459,  0.45275125,  0.35111167],
       [ 0.4067617 ,  0.32315323,  0.50315537],
       [ 0.44937827,  0.34765414,  0.47184633],
       [ 0.46378324,  0.35601187,  0.46144689],
       [ 0.46378324,  0.35601187,  0.46144689],
       [ 0.44937827,  0.34765414,  0.47184633],
       [ 0.46378324,  0.35601187,  0.46144689],
       [ 0.32613027,  0.27703411,  0.5654554 ],
       [ 0.62094459,  0.45275125,  0.35111167]])

실제로 2번째 열(버시컬러에 대한 확률)이 가장 높은 샘플은 없다.

일단 정확도를 확인하여 보자

accuracy_score = np.mean(valid_predict == y_valid)  # 정확도 계산
accuracy_score
0.33333333333333331

약 33%로 훈련세트의 정확도의 절반도 되질 않는다.

버시컬러에 대한 확률값이 문제가 되질 않을까 생각되었다.

실제로 훈련세트에 대한 예측에서도 버시컬러에 대한 예측은 없었다. 버시컬러의 로지스틱 회귀 모델의 파라미터를 더 적절한 값을 찾아야할지 고민이 되었다.

데이터 수를 늘리거나, 이용하는 특성을 늘리는 방법이 더 나을 수도 있겠다는 생각을 해 보았다. 아니면 꽃잎의 너비라는 특성말고 다른 것으로 하였다면 다른 결과를 얻을 수도 있을겠다.

테스트 세트 평가

테스트 세트를 훈련된 각각의 클래스에 대한 모델의 파라미터를 이용해 점수를 구한다.

logits0 = X_test.dot(Theta0)
logits1 = X_test.dot(Theta1)
logits2 = X_test.dot(Theta2)

그리고 로지스틱 회귀를 통한 각각의 클래스에 대한 확률을 구한다

Y_proba0 = myLogistic(logits0)
Y_proba1 = myLogistic(logits1)
Y_proba2 = myLogistic(logits2)

그리고 각각의 확률 데이터들을 하나로 뭉쳐서 최댓값의 인덱스를 출력하도록한다.

(인덱스의 값과 레이블의 값이 동일하도록 되어있기 때문이다.)

vaild_prob = np.c_[Y_proba0, Y_proba1,Y_proba2]
valid_predict = np.argmax(vaild_prob, axis=1)
valid_predict
array([0, 0, 0, 2, 2, 2, 0, 2, 2, 2, 2, 2, 0, 0, 0, 2, 0, 0, 2, 0, 0, 2, 2,
       2, 2, 0, 0, 0, 0, 2], dtype=int64)

이제 정확도를 평가해보자.

accuracy_score = np.mean(valid_predict == y_test)  # 정확도 계산
accuracy_score
0.26666666666666666

약 27%로 검증세트보다 정확도가 더 낮다.

과제 3 A : 낮/밤 구분 로지스틱 회귀

1단계 : 데이터 가져오기

구글 드라이브에 마운트 된 파일을 가져옵니다.

from google.colab import drive
drive.mount('/content/drive')
---------------------------------------------------------------------------

ImportError                               Traceback (most recent call last)

<ipython-input-98-fc2b4561b498> in <module>()
----> 1 from google.colab import drive
      2 drive.mount('/content/drive')


ImportError: No module named 'google'

파일의 경로를 말합니다.

filename = '/content/drive/MyDrive/Colab Notebooks/머신러닝/'

이제 파일 안의 사진들을 가져오겠습니다. PIL 모듈을 사용하였습니다.

from PIL import Image

 

for i in range(1, 104):               #103개의 사진을 가져 오겠습니다.
  k = str(i)
  img = Image.open(filename+ k +".jpg")
  pic = img.resize((256, 256))        #사진의 사이즈를 256*256로 맞추었습니다.(너무 커지면 특성수가 많아져 계산에 시간이 많이 걸릴것 같았습니다.)
  pix = np.array(pic, dtype='uint8') #각각의 픽셀 정보를 배열로 받았습니다.
  pixF = np.ravel(pix, order='C')    # 1차원의 리스트로 바꾸었습니다.
  
  if i == 1 :
    dataset = pixF
  else:
    dataset = np.vstack([dataset, pixF])  # (샘플의 수, 특성의 수(픽셀정보))- 행렬로 데이터세트를 구성하였습니다.





제가 준비한 모든 사진의 데이터를 가져왔습니다.

dataset.shape

103개의 샘플과 각각의 특성은 196608개입니다.

특성수가 196608개인 것에 대해 설명하겠습니다.

pic

먼저 제가 가져온 사진 중의 하나입니다.

pix.shape

이 사진의 픽셀정보를 배열로 바꾸면 다음과 같이 (256, 256, 3)의 3차 행렬이 나옵니다.

MNIST의 경우와 달리 컬러 사진이기에 마지막에 차원은 RGB 컬러에 대한 정보를 말합니다.

즉 따라서 각각의 샘플의 특성수는 2562563 = 196608 이렇게 나오게 된 것입니다.

여기서 편향을 추가 시키겠습니다.

dataset_with_bias = np.c_[np.ones([len(dataset), 1]), dataset]

레이블 생성

다음은 밤과 낮에 대한 레이블을 생성하겠습니다. 0이면 밤, 1이면 낮으로 레이블 값을 선택하여 직접 생성하였습니다.

y_day=[1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1]
y = np.array(y_day)
y.shape

103개의 샘플에 대한 레이블 값이 잘 만들어졌습니다.

2단계 : 데이터 분할

데이터셋을 훈련, 검증, 테스트 용도로 6대 2대 2의 비율로 무작위로 분할한다.

  • 훈련 세트: 60%
  • 검증 세트: 20%
  • 테스트 세트: 20%
test_ratio = 0.2                                         # 테스트 세트 비율 = 20%
validation_ratio = 0.2                                   # 검증 세트 비율 = 20%
total_size = len(dataset_with_bias)                      # 전체 데이터셋 크기

test_size = int(total_size * test_ratio)                 # 테스트 세트 크기: 전체의 20%
validation_size = int(total_size * validation_ratio)     # 검증 세트 크기: 전체의 20%
train_size = total_size - test_size - validation_size    # 훈련 세트 크기: 전체의 60%

데이터셋을 분할할 때 각각의 샘플들이 몰리지 않고 골고루 섞이기 위해 np.random.permutation() 함수를 이용하여 인덱스를 무작위로 섞는다

rnd_indices = np.random.permutation(total_size)

인덱스가 무작위로 섞였기 때문에 다음과 같이 데이터를 분할 하여도 무작위로 뽑는 효과를 얻습니다. 방법은 섞인 인덱스를 이용하여 지정된 6:2:2의 비율로 훈련, 검증, 테스트 세트로 분할하는 것이다.

X_train = dataset_with_bias[rnd_indices[:train_size]]
y_train = y[rnd_indices[:train_size]]

X_valid = dataset_with_bias[rnd_indices[train_size:-test_size]]
y_valid = y[rnd_indices[train_size:-test_size]]

X_test = dataset_with_bias[rnd_indices[-test_size:]]
y_test = y[rnd_indices[-test_size:]]

훈련, 검증, 테스트 세트로 분할을 했습니다.

3단계 : 타깃 변환

훈련과정에서의 비용(손실)의 계산과 정확도 비교를 위해 원-핫 벡터로 타깃을 바꿔주겠습니다.

Y_train_one_hot = to_one_hot(y_train)
Y_valid_one_hot = to_one_hot(y_valid)
Y_test_one_hot = to_one_hot(y_test)

잘 바뀌었나 확인해보겠습니다.

Y_train_one_hot[:5]

원하는 배열의 모양으로 바뀌었습니다.

4단계 : 로지스틱 회귀모델 구현

다음 로지스틱 회귀 모델을 이용하겠습니다.

def myLogistic(logits):
    exps = 1/(1+np.exp(-logits))                     # 샘플별로 점수를 구한 후 시그모이드 함수 적용
    return exps                                      # 샘플별 로지스틱 점수로 이루어진 어레이 반환 즉, 양성에 대한 확률의 어레이를 반환한다.

확률 예측값 : $\hat p = h_\theta(x) =\sigma(\theta^T\, \mathbf{x})$

시그모이드 함수 : $\sigma(t) = \frac{1}{1 + e^{-t}}$

다음의 공식을 구현한 것입니다.

5단계 : 훈련

$(n+1, 1)$ 행렬 모양의 2차원 넘파이 어레이 $\Theta$를 생성하기 위해 다음(특성의 수+1)을 확인한다.

n_inputs = dataset_with_bias.shape[1]
n_inputs

파라미터의 값을 무작위로 초기 설정합니다.

Theta = np.random.randn(n_inputs, 1)
Theta

조기종료 없는 배치경사하강법 (l2규제 포함)

배치 경사하강법 훈련은 아래 코드를 통해 이루어진다.

배치 경사 하강법은 전체 데이터셋을 훈련한 후에 파라미터를 조정하는 훈련방법입니다.

  • eta = 0.01: 학습률
  • n_iterations = 5001 : 에포크 수
  • m = len(X_train): 훈련 세트 크기, 즉 훈련 샘플 수
  • epsilon = 1e-7: $\log$ 값이 항상 계산되도록 더해지는 작은 실수
  • logits: 모든 샘플에 대한 양성의 점수, 즉 $\mathbf{X}_{\textit{train}}\, \Theta$
  • Y_proba: 모든 샘플에 대해 계산된 양성 확률, 즉 $\hat P$
eta = 0.1 
n_iterations = 5001
m = len(X_train)
epsilon = 1e-7
alpha = 0.1            # 규제 하이퍼파라미터
best_loss = np.infty   # 최소 손실값 기억 변수




for iteration in range(n_iterations):
    # 훈련 및 손실 계산
    logits = X_train.dot(Theta)
    Y_proba = myLogistic(logits)
    error = Y_proba - Y_train_one_hot
    gradients = 1/m * X_train.T.dot(error) + np.r_[np.zeros([1, 1]), alpha * Theta[1:]]
    Theta = Theta - eta * gradients

    # 검증 세트에 대한 손실 계산
    logits = X_valid.dot(Theta)
    Y_proba = myLogistic(logits)
    xentropy_loss = -np.mean(np.sum(Y_valid_one_hot * np.log(Y_proba + epsilon), axis=1))
    l2_loss = 1/2 * np.sum(np.square(Theta[1:]))
    loss = xentropy_loss + alpha * l2_loss
    
    # 500 에포크마다 검증 세트에 대한 손실 출력
    if iteration % 500 == 0:
        print(iteration, loss)
        
    

보시면 500에포크마다의 손실의 차이가 갑자기 커지는 경우도 있고, 꾸준히 감소하는 경향을 보이지 않기에 조기종료를 하면 적절한 파라미터를 찾기 전에 일찍 종료 될 것 같아 조기 종료를 하지 않았습니다.

일단 방금 찾은 모델의 파라미터에 대해서는 기억하도록 하겠습니다.

ThetaDay = Theta
ThetaDay

그럼 조기 종료를 포함한 훈련을 다시 해보겠습니다.

조기종료를 추가한 훈련

조기종료를 하는 코드를 추가한 훈련입니다.

eta = 0.1 
n_iterations = 5001
m = len(X_train)
epsilon = 1e-7
alpha = 0.1            # 규제 하이퍼파라미터
best_loss = np.infty   # 최소 손실값 기억 변수

Theta = np.random.randn(n_inputs, 1) # 무작위 세타



for iteration in range(n_iterations):
    # 훈련 및 손실 계산
    logits = X_train.dot(Theta)
    Y_proba = myLogistic(logits)
    error = Y_proba - Y_train_one_hot
    gradients = 1/m * X_train.T.dot(error) + np.r_[np.zeros([1, 1]), alpha * Theta[1:]]
    Theta = Theta - eta * gradients

    # 검증 세트에 대한 손실 계산
    logits = X_valid.dot(Theta)
    Y_proba = myLogistic(logits)
    xentropy_loss = -np.mean(np.sum(Y_valid_one_hot * np.log(Y_proba + epsilon), axis=1))
    l2_loss = 1/2 * np.sum(np.square(Theta[1:]))
    loss = xentropy_loss + alpha * l2_loss
    
    # 500 에포크마다 검증 세트에 대한 손실 출력
    if iteration % 500 == 0:
        print(iteration, loss)
        
    # 에포크마다 최소 손실값 업데이트
    if loss < best_loss:
        best_loss = loss
    else:                                      # 에포크가 줄어들지 않으면 바로 훈련 종료
        print(iteration - 1, best_loss)        # 종료되지 이전 에포크의 손실값 출력
        print(iteration, loss, "조기 종료!")
        break

보시면 바로 1번째에서 손실이 커져서 종료 되므로 우리가 찾는 적절한 파라미터를 기대하기 힘든 것 같습니다.

학습률 조정 훈련

그렇다면 이번엔 학습률을 조정해 보았습니다. (0.1 → 0.4 로 상승)

eta = 0.4 
n_iterations = 5001
m = len(X_train)
epsilon = 1e-7
alpha = 0.1            # 규제 하이퍼파라미터
best_loss = np.infty   # 최소 손실값 기억 변수

Theta = np.random.randn(n_inputs, 1) # 무작위 세타



for iteration in range(n_iterations):
    # 훈련 및 손실 계산
    logits = X_train.dot(Theta)
    Y_proba = myLogistic(logits)
    error = Y_proba - Y_train_one_hot
    gradients = 1/m * X_train.T.dot(error) + np.r_[np.zeros([1, 1]), alpha * Theta[1:]]
    Theta = Theta - eta * gradients

    # 검증 세트에 대한 손실 계산
    logits = X_valid.dot(Theta)
    Y_proba = myLogistic(logits)
    xentropy_loss = -np.mean(np.sum(Y_valid_one_hot * np.log(Y_proba + epsilon), axis=1))
    l2_loss = 1/2 * np.sum(np.square(Theta[1:]))
    loss = xentropy_loss + alpha * l2_loss
    
    # 500 에포크마다 검증 세트에 대한 손실 출력
    if iteration % 500 == 0:
        print(iteration, loss)
        
    # 에포크마다 최소 손실값 업데이트
    if loss < best_loss:
        best_loss = loss
    else:                                      # 에포크가 줄어들지 않으면 바로 훈련 종료
        print(iteration - 1, best_loss)        # 종료되지 이전 에포크의 손실값 출력
        print(iteration, loss, "조기 종료!")
        break

마찬가지로 너무 빨리 조기 종료되어 적절한 파라미터를 찾기 전에 종료 되었을 수도 있기에 조기종료를 사용하지 않은 모델을 사용 하도록 하겠습니다.

실제로 조기종료와 학습률 조정을 추가해서 훈련한 모델의 파라미터를 이용해 검증세트에 대해 정확도를 확인해보았습니다.

logits = X_valid.dot(Theta)
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)

accuracy_score = np.mean(y_predict == Y_valid_one_hot)
accuracy_score

25%의 정확도로 제 생각엔 최적의 파라미터를 찾았다고는 볼 수 없다고 판단되어 조기종료를 하지 않은 훈련모델의 파라미터를 사용하였습니다.

정확도 평가 : 훈련세트

아까 기억한 파라미터를 이용하여 훈련세트에 대한 예측의 정확도를 평가해보겠습니다

logits = X_train.dot(ThetaDay)
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)

accuracy_score = np.mean(y_predict == Y_train_one_hot)
accuracy_score

정확도 1.0 즉, 100%를 말하는데 운이 좋은 경우가 아닐까 생각합니다.

정확도 평가 : 검증세트

마찬가지로 기억한 파라미터를 이용하여 검증세트에 대해 예측값을 구하고 정확도를 평가해보겠습니다.

logits = X_valid.dot(ThetaDay)
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)

accuracy_score = np.mean(y_predict == Y_valid_one_hot)
accuracy_score

검증세트에 대해서는 정확도가 약 75%로 나타납니다.

6단계 : 테스트 세트 정확도 평가

logits = X_test.dot(ThetaDay)
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)

accuracy_score = np.mean(y_predict == Y_test_one_hot)
accuracy_score

0.9 즉, 약 90%의 정확도를 기대할 수 있습니다.

과제 3 B : 낮/밤, 실내/실외 분류 다중 레이블 분류 모델

낮과 밤 , 실내와 실외는 서로 배타적인 구분이 아니므로 각각에 대한 로지스틱 회귀모델 즉, 총 2개의 로지스틱 회귀모델을 구현한 후 그 결과를 합쳐서 정확도를 판단하는 방법으로 구현하겠습니다.

먼저 과제3 A에서 낮/밤 로지스틱 회귀 모델을 구현하고 위에서 이미 훈련시켰기에 낮/밤 분류에서는 다시 여기서 또 같은 구현을 해서 훈련시키는 낭비는 하지 않고 그 훈련된 파라미터 값들을 이용한다. 그래서 여기서 실내/실외의 로지스틱 회귀 모델을 구현한 후 훈련시킨 뒤 두 모델에 데이터를 각각 주입하여 다중 레이블 분류을 구현하겠습니다.

1.실내/실외 로지스틱 회귀 모델 구현

1단계 : 타깃 데이터 준비

다른 필요한 데이터들은 모두 과제 3에서 준비 되어 있으니 우리에게 필요한 것은 실내/실외에 대한 레이블입니다. 그래서 직접 생성하였습니다.

실내의 경우 0, 실외의 경우 1로 레이블 값을 주었습니다.

y_inside=[1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0 ]
y = np.array(y_inside)
y.shape

샘플(103개)에 대한 실내/실외에 대한 레이블 값을 잘 주었습니다.

2단계 : 데이터 분할

이제 바뀐 레이블(실내/실외)에 대해 앞에 준비된 데이터에 맞게 분할을 해야합니다.

y_train = y[rnd_indices[:train_size]]

y_valid = y[rnd_indices[train_size:-test_size]]

y_test = y[rnd_indices[-test_size:]]

6:2:2로 훈련, 검증, 테스트 세트의 각 샘플에 알맞게 매칭이 되도록 레이블을 분할 하였습니다.

3단계 : 타깃 변환

낮/밤 분류 훈련과 마찬가지로 비용(손실)의 계산과 정확도 계산에 사용할 때 알맞은 형태로 맞추기 위해 원-핫 벡터를 사용하였습니다.(여기서도 실외(양성) or 실내(음성)을 구분하는 것이므로 클래스가 1개입니다!)

Y_train_one_hot = to_one_hot(y_train)
Y_valid_one_hot = to_one_hot(y_valid)
Y_test_one_hot = to_one_hot(y_test)
Y_train_one_hot[:5]

잘 바뀌었음을 확인하였습니다.

4단계 : 로지스틱 회귀모델 구현

과제 1에서 구현한 것이며, 낮/밤에서도 사용한 모델입니다.

def myLogistic(logits):
    exps = 1/(1+np.exp(-logits))                     # 샘플별로 점수를 구한 후 시그모이드 함수 적용
    return exps                                      # 샘플별 로지스틱 점수로 이루어진 어레이 반환 즉, 양성에 대한 확률의 어레이를 반환한다.

확률 예측값 : $\hat p = h_\theta(x) =\sigma(\theta^T\, \mathbf{x})$

시그모이드 함수 : $\sigma(t) = \frac{1}{1 + e^{-t}}$

다음의 공식을 구현한 것입니다.

5단계 : 훈련

조기종료 없는 배치경사하강법 (l2규제 포함)

파라미터의 값을 무작위로 초기 설정합니다.

Theta = np.random.randn(n_inputs, 1)
Theta

배치 경사하강법 훈련은 아래 코드를 통해 이루어진다.

배치 경사 하강법은 전체 데이터셋을 훈련한 후에 파라미터를 조정하는 훈련방법입니다.

  • eta = 0.01: 학습률
  • n_iterations = 5001 : 에포크 수
  • m = len(X_train): 훈련 세트 크기, 즉 훈련 샘플 수
  • epsilon = 1e-7: $\log$ 값이 항상 계산되도록 더해지는 작은 실수
  • logits: 모든 샘플에 대한 양성의 점수, 즉 $\mathbf{X}_{\textit{train}}\, \Theta$
  • Y_proba: 모든 샘플에 대해 계산된 양성 확률, 즉 $\hat P$
eta = 0.1 
n_iterations = 5001
m = len(X_train)
epsilon = 1e-7
alpha = 0.1            # 규제 하이퍼파라미터
best_loss = np.infty   # 최소 손실값 기억 변수





for iteration in range(n_iterations):
    # 훈련 및 손실 계산
    logits = X_train.dot(Theta)
    Y_proba = myLogistic(logits)
    error = Y_proba - Y_train_one_hot
    gradients = 1/m * X_train.T.dot(error) + np.r_[np.zeros([1, 1]), alpha * Theta[1:]]
    Theta = Theta - eta * gradients

    # 검증 세트에 대한 손실 계산
    logits = X_valid.dot(Theta)
    Y_proba = myLogistic(logits)
    xentropy_loss = -np.mean(np.sum(Y_valid_one_hot * np.log(Y_proba + epsilon), axis=1))
    l2_loss = 1/2 * np.sum(np.square(Theta[1:]))
    loss = xentropy_loss + alpha * l2_loss
    
    # 500 에포크마다 검증 세트에 대한 손실 출력
    if iteration % 500 == 0:
        print(iteration, loss)
        

마찬가지로 손실(비용)의 값이 감소/증가가 여러번 나타난다. 따라서 조기 종료를 할 경우 최적의 파라미터를 찾기전에 일찍 종료될 가능성이 있으므로 조기종료는 없이 하도록 하는 것이 좋을 것 같다.

이 훈련된 모델의 파라미터를 기억한다.

ThetaInside = Theta
ThetaInside

그리고 이 훈련세트에 대한 예측의 정확도를 확인해보자

정확도 평가 : 훈련세트

아까 기억한 파라미터를 이용하여 훈련세트에 대한 예측의 정확도를 평가해보겠습니다

logits = X_train.dot(ThetaInside)
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)

accuracy_score = np.mean(y_predict == Y_train_one_hot)
accuracy_score

이번에도 정확도 1.0 즉, 100%를 말하는데 운이 좋은 경우가 아닐까 생각합니다.

정확도 평가 : 검증세트

마찬가지로 기억한 파라미터를 이용하여 검증세트에 대해 예측값을 구하고 정확도를 평가해보겠습니다.

logits = X_valid.dot(ThetaInside)
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)

accuracy_score = np.mean(y_predict == Y_valid_one_hot)
accuracy_score

검증세트에 대해 정확도 약 75%가 확인되었습니다.

6단계 : 테스트 세트 정확도 평가

logits = X_test.dot(ThetaInside)
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)

accuracy_score = np.mean(y_predict == Y_test_one_hot)
accuracy_score

0.75 즉, 약 75%의 정확도를 기대할 수 있습니다.

2.다중 레이블 분류 모델 구현

1단계 : 레이블 만들기

먼저 레이블 값을 다시 만들어 주어야한다. 낮/밤 레이블 값과, 실내/실외 레이블 값을 합쳐서 만들었다.

y1 = np.array(y_day)       # 낮/밤에 대한 레이블
y2 = np.array(y_inside)    # 실내/실외에 대한 레이블

y = np.c_[y1, y2]          # 다중 레이블 분류에 대한 레이블
y[:5]

(샘플수, 2(낮/밤+실내/실외))-배열로 잘 되었음을 확인하였습니다.

그리고 다시 데이터세트의 분할과 알맞게 레이블도 분할 해 주어야 합니다.

y_train = y[rnd_indices[:train_size]]

y_valid = y[rnd_indices[train_size:-test_size]]

y_test = y[rnd_indices[-test_size:]]

2단계 : 훈련세트에 대한 예측값

위에서 모델을 훈련시키고 그래서 그 모델들의 파라미터를 알고 있으므로 훈련세트를 모델에 주입시켜 예측값을 알아보자. 즉, 예측을 다중 레이블로 해보자.

먼저 훈련 세트의 각 샘플들을 낮/밤 로지스틱 회귀 모델 주입하여 예측값을 만들었습니다.

logits = X_train.dot(ThetaDay)
Y_proba = myLogistic(logits)
y_predictDay = myLogisticPredict(Y_proba)

각각의 예측값들을 확인해 보았습니다. ( 1(양성) : 낮, 0(음성) : 밤 )

y_predictDay[:5]

다음으로 훈련 세트의 각 샘플들을 실내/실외 로지스틱 회귀 모델에 주입하여 예측값을 만들었습니다.

logits = X_train.dot(ThetaInside)
Y_proba = myLogistic(logits)
y_predictInside = myLogisticPredict(Y_proba)

각각의 예측값들을 확인해 보았습니다. ( 1(양성) : 실외, 0(음성) : 실내 )

y_predictInside[:5]

자, 다음은 다중 레이블의 형식에 맞도록 두 예측값을 합쳐서 (샘플수, 2)-배열로 만들어 주겠습니다.

y_predict = np.c_[y_predictDay, y_predictInside]
y_predict[:5]

잘 합쳐져서 원하는 형태가 만들어 졌습니다.

정확도

accuracy_score = np.mean(y_predict == y_train)
accuracy_score

위에서 각각 훈련세트에 대해 낮/밤과 실내/실외가 100%의 정확도를 보였기에 여기 다중 레이블 분류 모델에서도 100%의 정확도를 보인다.

3단계 : 검증세트에 대한 예측값

마찬가지로 테스트 세트에 대해서도 구현한 다중 레이블 분류 모델을 적용하여 보자.

먼저 검증 세트의 각 샘플들을 낮/밤 로지스틱 회귀 모델 주입하여 예측값을 만들었습니다.

logits = X_valid.dot(ThetaDay)
Y_proba = myLogistic(logits)
y_predictDay = myLogisticPredict(Y_proba)

각각의 예측값들을 확인해 보았습니다. ( 1(양성) : 낮, 0(음성) : 밤 )

y_predictDay[:5]

다음으로 테스트세트의 각 샘플들을 실내/실외 로지스틱 회귀 모델에 주입하여 예측값을 만들었습니다.

logits = X_valid.dot(ThetaInside)
Y_proba = myLogistic(logits)
y_predictInside = myLogisticPredict(Y_proba)

각각의 예측값들을 확인해 보았습니다. ( 1(양성) : 실외, 0(음성) : 실내 )

y_predictInside[:5]

자, 다음은 다중 레이블의 형식에 맞도록 두 예측값을 합쳐서 (샘플수, 2)-배열로 만들어 주겠습니다.

y_predict = np.c_[y_predictDay, y_predictInside]
y_predict[:5]

잘 합쳐져서 원하는 형태가 만들어 졌습니다.

정확도

accuracy_score = np.mean(y_predict == y_valid)
accuracy_score

검증세트에 대한 정확도는 약 75%입니다.

4단계 : 테스트세트에 대한 예측값

마찬가지로 테스트 세트에 대해서도 구현한 다중 레이블 분류 모델을 적용하여 보자.

먼저 테스트세트의 각 샘플들을 낮/밤 로지스틱 회귀 모델 주입하여 예측값을 만들었습니다.

logits = X_test.dot(ThetaDay)
Y_proba = myLogistic(logits)
y_predictDay = myLogisticPredict(Y_proba)

각각의 예측값들을 확인해 보았습니다. ( 1(양성) : 낮, 0(음성) : 밤 )

y_predictDay[:5]

다음으로 테스트세트의 각 샘플들을 실내/실외 로지스틱 회귀 모델에 주입하여 예측값을 만들었습니다.

logits = X_test.dot(ThetaInside)
Y_proba = myLogistic(logits)
y_predictInside = myLogisticPredict(Y_proba)

각각의 예측값들을 확인해 보았습니다. ( 1(양성) : 실외, 0(음성) : 실내 )

y_predictInside[:5]

자, 다음은 다중 레이블의 형식에 맞도록 두 예측값을 합쳐서 (샘플수, 2)-배열로 만들어 주겠습니다.

y_predict = np.c_[y_predictDay, y_predictInside]
y_predict[:5]

잘 합쳐져서 원하는 형태가 만들어 졌습니다.

정확도

accuracy_score = np.mean(y_predict == y_test)
accuracy_score

테스트세트에 대한 정확도는 약 82.5%로 보입니다.

과제 3 C : 구현한 알고리즘과 사이킷런의 로지스틱 회귀 모델 성능 비교

제가 구현한 알고리즘은 조기종료를 사용하냐 안하냐에 따라 차이가 있는 것 같으니(l2규제는 모두 포함) 두가지 모두다 이용하겠습니다.

성능 비교할 모델

  • 조기종료를 포함하지 않고 직접 구현한 로지스틱 회귀 모델 알고리즘
  • 조기종료를 추가해서 구현한 로지스틱 회귀 모델 알고리즘
  • 사이킷런에서 제공하는 LogisticRegression 모델

성능의 기준

  • 테스트 세트에 대한 정확도

수행할 작업

  • 낮과 밤의 이진분류(과제 3 A)

레이블 세팅

낮/밤 레이블을 다시 가져와서하고 배열로 만들고, 훈련, 검증, 데이터 세트로 분할합니다.

y = np.array(y_day)

y_train = y[rnd_indices[:train_size]]

y_valid = y[rnd_indices[train_size:-test_size]]

y_test = y[rnd_indices[-test_size:]]

확인해보겠습니다.

y_train

잘 되었고, 원-핫 벡터로 바꿔주겠습니다. (분류를 원하는 클래스는 1개입니다. 1(낮) 아니면 0(밤)입니다.)

Y_train_one_hot = to_one_hot(y_train)
Y_valid_one_hot = to_one_hot(y_valid)
Y_test_one_hot = to_one_hot(y_test)

확인해보겠습니다.

Y_train_one_hot[:5]

잘 세팅 되었습니다.

조기종료 없는 직접 구현 알고리즘 성능(테스트세트 정확도)

이 모델의 훈련은 과제 3 A에서 진행되어 그 파라미터 값을 ThetaDay에 저장 해 두었으니 바로 이용하여 테스트 세트에 대한 정확도를 구하겠습니다.

logits = X_test.dot(ThetaDay)
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)

accuracy_score = np.mean(y_predict == Y_test_one_hot)
accuracy_score

약 90%의 정확도를 보여줍니다.

조기종료 추가된 직접 구현 알고리즘 성능(테스트세트 정확도)

먼저 훈련을 통해 모델의 파라미터 값을 찾겠습니다.

  • eta = 0.01: 학습률
  • n_iterations = 5001 : 에포크 수
  • m = len(X_train): 훈련 세트 크기, 즉 훈련 샘플 수
  • epsilon = 1e-7: $\log$ 값이 항상 계산되도록 더해지는 작은 실수
  • logits: 모든 샘플에 대한 양성의 점수, 즉 $\mathbf{X}_{\textit{train}}\, \Theta$
  • Y_proba: 모든 샘플에 대해 계산된 양성 확률, 즉 $\hat P$
eta = 0.1 
n_iterations = 5001
m = len(X_train)
epsilon = 1e-7
alpha = 0.1            # 규제 하이퍼파라미터
best_loss = np.infty   # 최소 손실값 기억 변수


Theta = np.random.randn(n_inputs, 1) # 무작위 세타



for iteration in range(n_iterations):
    # 훈련 및 손실 계산
    logits = X_train.dot(Theta)
    Y_proba = myLogistic(logits)
    error = Y_proba - Y_train_one_hot
    gradients = 1/m * X_train.T.dot(error) + np.r_[np.zeros([1, 1]), alpha * Theta[1:]]
    Theta = Theta - eta * gradients

    # 검증 세트에 대한 손실 계산
    logits = X_valid.dot(Theta)
    Y_proba = myLogistic(logits)
    xentropy_loss = -np.mean(np.sum(Y_valid_one_hot * np.log(Y_proba + epsilon), axis=1))
    l2_loss = 1/2 * np.sum(np.square(Theta[1:]))
    loss = xentropy_loss + alpha * l2_loss
    
    # 500 에포크마다 검증 세트에 대한 손실 출력
    if iteration % 500 == 0:
        print(iteration, loss)
        
    # 에포크마다 최소 손실값 업데이트
    if loss < best_loss:
        best_loss = loss
    else:                                      # 에포크가 줄어들지 않으면 바로 훈련 종료
        print(iteration - 1, best_loss)        # 종료되지 이전 에포크의 손실값 출력
        print(iteration, loss, "조기 종료!")
        break

1번째 에포크에서 비용(손실)이 상승했기 때문에 바로 조기 종료 되었습니다. 1번째 에포크에서 조기 종료 되어 사실 더 좋은 파라미터를 찾기 전에 일찍 종료 된 것은 아닐까 걱정은 되지만 이 파라미터를 이용해 보겠습니다.

ThetaDay2 = Theta
ThetaDay2

이 파라미터를 저장하고 확인하여 보았습니다.

이제 테스트 세트에 대한 정확도(성능)을 평가해보겠습니다.

logits = X_test.dot(ThetaDay2)
Y_proba = myLogistic(logits)
y_predict = myLogisticPredict(Y_proba)

accuracy_score = np.mean(y_predict == Y_test_one_hot)
accuracy_score

0.65 즉, 검증세트에 대해서 약 65%의 정확도를 나타냅니다.

사이킷런이 제공 하는 LogisticRegression 모델 성능(테스트세트 정확도)

먼저 훈련 세트를 주입하여 사이킷런의 LogisticRegression 모델을 훈련시켰습니다.

from sklearn.linear_model import LogisticRegression
log_reg = LogisticRegression(solver="lbfgs", random_state=42)
log_reg.fit(X_train, y_train)

그리고 훈련 세트에 대한 예측을 만들어보았습니다.

t = log_reg.predict(X_train)

그리고 훈련세트에 대한 정확도를 평가해 았습니다.

accuracy_score = np.mean(t == y_train)
accuracy_score

100%의 정확도를 보입니다.

마찬가지로 검증세트에 대해서도 예측을 하여 정확도를 평가해 보았습니다.

t = log_reg.predict(X_valid)

accuracy_score = np.mean(t == y_valid)
accuracy_score

약 80%의 정확도를 보입니다.

마지막으로 테스트 세트에 대해 정확도를 평가해 보겠습니다.

t = log_reg.predict(X_test)

accuracy_score = np.mean(t == y_test)
accuracy_score

약 90%의 정확도를 보여주고 있습니다.

결론

성능 평가 (테스트 세트에 대한 예측의 정확도 )

  • 조기종료를 포함하지 않고 직접 구현한 로지스틱 회귀 모델 알고리즘 : 약 90%
  • 조기종료를 추가해서 구현한 로지스틱 회귀 모델 알고리즘 : 약 65%
  • 사이킷런에서 제공하는 LogisticRegression 모델 : 약 90%

즉, 조기종료를 포함하지 않고 제가 구현한 로지스틱 회귀 모델 알고리즘사이킷런에서 제공하는 LogisticRegression 모델성능(정확도)은 같았고, 조기종료를 추가한 로지스틱 회귀 모델의 알고리즘은 두 모델에 비해 상대적으로 낮은 성능을 보여주고 있습니다.

먼저 제가 직접 로지스틱 회귀 모델(조기종료X) 알고리즘을 구현 한 것이 사이킷런의 LogisticRegression과 거의 유사하게 잘 되었기 때문에 비슷한 성능이 나온 것이 아닐까 기대가 되었고,

조기종료를 추가한 모델의 비교적 낮은 성능의 이유는 모델의 적절한 파라미터를 찾기 전에 너무 일찍 파라미터가 종료 되었기 때문이 아닐까 생각되었습니다.

해결방안으로는 매 에포크마다 계산한 손실이 한 번 증가하였다고 조기 종료하지 말고 연속적 혹은 지속적으로 손실이 증가한다면 조기 종료하는 방식으로 고려 해볼 수 있겠다고 생각하였습니다.

Updated: