본문 바로가기

공부/AI

Machine Learning Methodology & analysis 공부했다

 계속해서 AI에서 사용하는 알고리즘들에 대해서 공부를 하고 있는 중이다. 항상 그러하듯이 이론과 실제는 다르다. 내가 좋아하고 자주 듣기도 하는 말이 있는데 바로 '하드웨어는 거짓말을 하지 않는다.'라는 것이다. 여기에서 하드웨어는 컴퓨터 그 자체를 의미하기도 한다. 


내가 짠 프로그램이 작동하지 않는다고?? 하드웨어는 거짓말을 하지 않는다. 그냥 내가 잘못한거다!!!!


 차치하고, 오늘 배운 내용들은 크게 다음과 같다. 


● 데이터셋 나누기

● Underfitting / Overfitting

● Skewed Classes 

● Precision/Recall


 

 데이서셋을 나눈다는 것이 무슨 말인가 하면, 우리가 100개의 데이터를 가지고 있다고 하였을 때, 그것들을 모두 학습하는데 써버린다면, 지금 가지고 있는 데이터들을 외우게 하는 것과 다를 바가 없어진다. 그래서 가진 데이터의 일부는 학습을 시키고 미리 얼마만큼을 빼 두었다가 이는 인공지능을 시험하는데 이를 사용해야 한다. 

Copyright © 2018. Alina Inc. All Rights Reserved.



 테스트셋을 뽑을 때는 뒤에서 부터 몇개 이렇게 하기보다는 전체에서 랜덤하게 선별을 해야 하고, 그 비율은 위에 자료와 같이 학습셋:테스트셋 = 7:3 정도 비율로 하는 것이 좋다고 한다. 강좌에서는 간단한 예제가 있는데 아래와 같다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import numpy as np
import tensorflow as tf
from tensorflow import keras
 
def get_data():
    boston_housing = keras.datasets.boston_housing
    (train_data, train_labels), (test_data, test_labels) = boston_housing.load_data(test_split=0.3)
    return train_data, np.expand_dims(train_labels, axis=1), test_data, np.expand_dims(test_labels, axis=1)
 
x_train, y_train, x_test, y_test = get_data()
#print(x_train.shape)
#print(x_test.shape)
#print(y_train.shape)
 
num_inputs = x_train.shape[-1]
num_outputs =  y_train.shape[-1]
print(num_inputs)
print(num_outputs)
 
x_input = tf.placeholder(tf.float32, [None, num_inputs])
y_input = tf.placeholder(tf.float32, [None, num_outputs])
 
theta0_var = tf.Variable(np.zeros([num_outputs], dtype=np.float32))
thetan_var = tf.Variable(np.zeros([num_inputs, num_outputs], dtype=np.float32))
y_output = theta0_var + tf.matmul(x_input, thetan_var)
cost_output = tf.reduce_mean((y_input - y_output) ** 2)
learning_rate = 0.001
train_step = tf.train.AdamOptimizer(learning_rate).minimize(cost_output)
#print('Done')
 
max_epoch = 10000
check_point = max_epoch / 5
with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
 
    for i in range(max_epoch):
        sess.run(train_step, feed_dict={x_input: x_train, y_input: y_train})
        if (i + 1) % check_point == 0:
            print('Done: {}%'.format(i / max_epoch * 100))
 
    train_cost = sess.run(cost_output, feed_dict={x_input: x_train, y_input: y_train})
    test_cost = sess.run(cost_output, feed_dict={x_input: x_test, y_input: y_test})
 
print('train cost: {:.2f}, test cost: {:.2f}'.format(train_cost, test_cost))
 
cs

위 결과에서 Error를 말하라고 하면, Train Error가 아니라 Test Error를 말해 주어야 하는 것이다.

Copyright © 2018. Alina Inc. All Rights Reserved.


 방금의 경우는 매우 간단했는데, 당연히 실제 프로젝트를 구성하려고 하면 여러 모델들 중에서 하나를 선택하고 그것을 검증하고 또, 새로운 데이터들로 평가를 해야 한다. 보통 이 비율은 6:2:2 로 한다고 하며 모델에 학습시킬 데이터들을 집어넣어줄 때에도 우리가 구분하고자 하는 것들에 대해서 얼마나 자세한 정보를 줘야 하는지, 크기는 얼마나 해줄지, 얼마나 여러번 학습을 시킬지 전부 생각해서 학습시켜야 한다. - ㅠㅠ항상 개발 환경 구성이 제일 귀찮고 어려운 법입니다.ㅠㅠ 다만, 모델 선택에 있어서 예를 들어주고 있다.

 다항식(polynomial)형태의 모델을 잡아서 얼마나 복잡하게(다항식의 차수) 설계할 것인지를 고민하고 있다고 하자. 1~10차 다항식 모두에 대해서 매개변수들을 최적화한 다음(Gradient Descent등으로) Validation data들로 검증을 해서 가장 낮은 Error를 지닌 차수를 선택한다. 그러면 이 모델과 Test data를 가지고 구한 Error가 곧 최종적인 결과라고 할 수 있는 것이다~!

● Underfitting / Overfitting


Copyright © 2018. Alina Inc. All Rights Reserved.


 이번에는 아마 오늘 제일 중요한 내용이 아닐까 싶은 Underfitting과 Overfitting에 대해서 알아보자. 위 그림을 보면 직관적으로 알 수 있을 것 같은데, Underfitting이란 너무 train을 적게 시켜서 대략적인 분포만 보일 뿐 쓸만한 결과를 내놓지 못하는 경우이고, Overfitting은 너무 학습을 많이 시켜서 그 데이타셋 자체를 외워버린 것 같은 결과를 내놓는 경우이다. 

이는 마치 우리가 시험기간에 공부를 하는 것과 비슷한데....

교수님 : 자 여러분~!! 그동안 책에서 과제로 풀어오라 했던 문제들 있지요?? 그걸 참고해서 잘 공부하고 우리 즐거운 시험시간에 봅시다~  "ㅅ오ㅅ"

학생 A : 기출 한번씩만 풀어봐야지~ 어차피 개념만 알면 되니까!! 룰룰루~~
학생 B : 개념?? 점수만 잘 맞으면 되지!! 기출과 솔루션을 달달 외운다!!!
학생 C : 개념을 다시 익히면서 연습문제들과 기출로 적당히 점검을 해야겠다~!!


 ==========================즐거운 시험 시간========================

 학생 A : 뭐야?? 이거 어떤 상황인지는 알겠는데, 풀지는 못하겠잖아!! 재수강 각이다.
 학생 B : 오~ 몇개는 숫자만 달라졌구만~ 응?? 이건 처음 보는 건데 B+정도 되려나 ㅠ
 학생 C : (자, 이 상황에서 A는 Underfitting, B는 Overfitting된 경우라고 할 수 있어요!!)

 허접한 예시였는데, 이렇게라도 이해가 되었다면 일단은 성공이다. 그럼 실질적으로 내가 얼마 정도 학습을 시켜야 하는지(최적화 기법을 몇 회나 반복할지) 보기 위해서 그래프로 나타내고자 한다. 보통 Train error와 Validation error 간의 차이를 통해 이를 알 수가 있다고 한다. 
 

Copyright © 2018. Alina Inc. All Rights Reserved.



 그래프에서 파란색이 Train Error를 나타내고, 주황색이 Validation Error를 나타내고 있다. 학습을 많이 시킬수록 당연히 Error들은 작아질 것이다. 다만, 앞서 말한 바와 같이 너무 많이 시키면 기출문제를 외우는 것과 다름이 없으므로 이 반복의 주기를 얼마나 해야 하는지 위에서 나타내고 있다.

 잘 보일지 모르겠는데 Validation Error는 급격히 작아지다가 최소점을 찍고 다시 조금 올라가는 경향을 보인다. 이 때 최소점을 찍는 그 순간이 제일 최적으로 학습이 된 순간이라고 한다. 이는 비단 학습에만 적용되는 것이 아니라. 

Copyright © 2018. Alina Inc. All Rights Reserved.


 모델의 복잡도를 얼마나 해주어야 하는지(앞서 말하였던 다항식 모델에서의 차수와 같이), Overfitting을 막기 위한 Regularization이라는 딥러닝 알고리즘이 있는데, 이 때 사용되는 특정한 매개변수 람다를 얼마 정도 해주어야 하는지 등에서도 이 최적의 상황을 찾는 방법이 쓰일 수 있다고 한다. 

Copyright © 2018. Alina Inc. All Rights Reserved.


 이번에는 Data의 크기에 대해서 각 Error값이 어떠한 경향을 보이는지에 대한 이야기이다. 사실 나는 처음 보았을 때 잘 이해가 되질 않았는데 차근차근 풀어가면 다음과 같다.
 
 Underfitting인 경우 당연히 Error값은 크다. 그런데, Data의 크기가 커질수록 틀린 결과들은 계속해서 많아질 것이고, 결국, Train error와 Validation error값의 차이는 작아질 것이다. 

 Overfitting인 경우 Data가 작을 때 Train Error값은 거의 0에 가까울 정도로 작다. Data의 크기가 커질 수록 전부 외우는 것은 불가능하고, 그래서 틀리는 경우들이 늘어날 것이다. Validation Error의 경우 Data가 커짐에 따라 작아지겠지만 - 문제가 많아질 수록 기출문제와 비슷한 문제들의 수고 많아질 것이기에 - Train error와는 큰 차이를 가질 것이다.

 실제로, Cosine함수에 Noise를 추가해서 Data set을 만들어 보고, 이들을 Polynomial(다항식)으로 나타낸다면 얼마 정도의(몇 차원의) 복잡도를 가지는 것이 좋을지 테스트를 해보자. 다항식 모델이란 위에서 말하였던 사진에서와 같이 몇차식으로 이 데이터들을 fitting하는 것이 좋을 것인지에 대한 모델을 의미한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
 
def true_fun(X):
    return np.cos(1.5 * np.pi * X)
 
def generate_data(size):
    X = np.sort(np.random.rand(size))
    y = true_fun(X) + np.random.randn(size) * 0.1
    return X[:, np.newaxis], y
 
def build_model(degree):
    polynomial_features = PolynomialFeatures(degree=degree, include_bias=False)
    linear_regression = LinearRegression()
    return Pipeline([("polynomial_features", polynomial_features), ("linear_regression", linear_regression)])
 
def get_mse(model, X, y):
    y_pred = model.predict(X)
    return mean_squared_error(y, y_pred)
 
def plot_model(model, X_train, y_train):
    X = np.linspace(01100)
    plt.plot(X, model.predict(X[:, np.newaxis]), label="Model")
    plt.plot(X, true_fun(X), label="True function")
    plt.scatter(X_train, y_train, edgecolor='b', s=20, label="Samples")
    plt.xlabel("x")
    plt.ylabel("y")
    plt.xlim((01))
    plt.ylim((-22))
    plt.legend(loc="best")
    plt.title('Degree {}\nMSE = {:.2e}'.format(d, mses['train'][-1]))
print('done')
 
np.random.seed(0)
data_size = 70
X_train, y_train = generate_data(int(data_size * 0.7))
X_val, y_val = generate_data(int(data_size * 0.3))
 
max_degree = 35
degrees = range(1, max_degree + 1)
mses = {'train': [], 'val': []}
plot_degrees = [14, max_degree]
 
plt.close('all')
plt.figure(figsize=(14,5))
 
for d in degrees:
    model = build_model(d)
    model.fit(X_train, y_train)
    mses['train'].append(get_mse(model, X_train, y_train))
    mses['val'].append(get_mse(model, X_val, y_val))
    if d in plot_degrees:
        ax = plt.subplot(1len(plot_degrees), plot_degrees.index(d) + 1)
        plt.setp(ax, xticks=(), yticks=())
        plot_model(model, X_train, y_train)
 
plt.show()
 
 
plt.close('all')
plt.plot(degrees, mses['train'])
plt.plot(degrees, mses['val'])
 
plt.xlabel('model complexity(degree of polynomial)')
plt.ylabel('mean squared error')
plt.legend(['train''validation'], loc='upper center')
 
plt.show()
cs



 실행을 시켜보면 총 두 종류의 plot이 나오는데 그 중 첫번째인 위 plot에서는 몇차식의 다항식을 사용할 때 가장 fit할지를 보여주기 위한 그림이다. 1차의 경우 Underfitting을 보이는 것을 알 수 있고, 35차식의 경우 너무 Overfitting되어서 다항식 자체도 괴상한 모양을 가지는 것을 알 수 있다. 4차식을 보면 적당한 것을 알 수 있지만 과연 이것이 최적의 선택일지에 대해서는 아직 모르는 상태이다. 그러면 이제 모델의 복잡성에 대한 Train Error - Validation Error의 관계를 한 번 살펴보자.




 자 앞서 말한 바와 같이 Validation Error가 최저를 찍는 지점이 나타났다!!! 오 4와 상당히 가까운 값이 나왔지만 4보다는 조금 작은 것 같다! 결론적으로, 이렇게 모델의 복잡도를 얼마 정도로 설정해 주어야 하는지에 대해서 알아보았다.

● Skewed Classes 

Skewed란 '비뚤어진'이라는 뜻이다. 비뚤어진 분류, 무슨 경우이지 잘 실감이 나질 않는데 다음의 예를 보자.

Copyright © 2018. Alina Inc. All Rights Reserved.



 암 환자의 예시가 나와 있다. 사람들 중에서 암 환자의 비율은 극도로 낮다.(혹은 그렇다고 하자.) 그래서 학습시킨 결과가 1%의 오차를 가지고 있다고 해도 암 환자의 비율이 이보다도 작은 0.5%라면 이 학습은 의미가 없게 되는 것이다. 

 인공지능은 아마도 대부분의 사람들이 암 환자가 아니기 때문에 모든 사람들에 대해서 암 환자가 아니라고 하더라도 "거의" 맞는 것처럼 보이기 때문이다. 그러면, 이렇게 한 쪽의 데이터 수가 많은 상황에서 분류를 제대로 하기 위해서는 어떻게 해야 할까?? 

Precision/Recall
○ Sensitivity/Specificity

 강좌에서는 크게 위 두 가지 방법을 제시하고 있는데 당장은 Precision/Recall에 대해서 알려준다.


● Precision/Recall


Copyright © 2018. Alina Inc. All Rights Reserved.


 우선은 위의 표를 보자. 암 환자 예측이 틀렸다고 했을 때, 틀린 경우가 두 가지가 있다.

1. 암 환자가 아닌데 암이라고 했을 경우(FP) 
2. 암 환자인데 암이 아니라고 했을 경우(FN)

마찬가지로 맞은 상황에도 두 가지 경우가 있다.

1. 암 환자인데 암이라고 했을 경우(TP)
2. 암이 아닌데 암이 아니라고 했을 경우(TN)

 이 각각의 상황들을 표로 나타낸 것이 위의 그림이고 Precision이란 정밀도, 곧 예측 결과가 암이 맞다고 했을 때, 실제로 암이었던 비율에 대한 상수이다. 일반적으로 정밀도를 이야기 할 때, 전체 무엇 중에서 결과가 맞은 비율을 생각하지 않는가? 바로 그거다.

Copyright © 2018. Alina Inc. All Rights Reserved.


 이번에는 Recall이다. 이는 실제로 암에 걸렸던 사람들 중에서 인공지능도 암이라고 판정했던 비율이라고 할 수 있다. 그래서 TP / (TP + FN)이 되겠다. 

그럼 이 Precision-Recall 개념과 Logistic Regression의 개념을 합해 보자!!

Copyright © 2018. Alina Inc. All Rights Reserved.


 기존의 Logistic Regression에서는 0.5를 기준으로 이보다 높으면 positive 낮으면 negative라고 했었다. 이 0.5를 Threshold라고 하는데 이는 '임계점'이라고 할 수 있다. 마치 광전 효과에서 특정 값이상 세기의 빛은 쐬어 주어야 전자가 나오는 것 처럼 임계점 전에는 아무 일도 일어나지 않다가 이 임계점을 넘는 순간에 변화가 생기는 것이다. 

 Threshold를 높일수록 precision은 높아지고, recall은 낮아진다
 Threshold를 낯출수록 precision은 낮아지고, recall은 높아진다
 이렇게 precision과 recall은 상충 관계이다. 

 아~ 그렇구나!! 그런데 이게 무슨 도움이 될까? 이는 모델의 성능을 측정하는데 사용할 수 있다.

Copyright © 2018. Alina Inc. All Rights Reserved.


 F-score라는 것이 있다. 자세한 것은 위를 읽어보면 알 수 있지만, 이 값은 Precision과 Recall이 비슷한 값을 가질수록 높다. 더불어 이는 Score이기 때문에 높을수록 모델의 성능이 좋다. 그럼 이번에는 이 Precision-Recall 개념을 이용해서 가장 적합한 Threshold를 찾는 실습을 진행해 보자!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import numpy as np
import pandas as pd
import urllib.request
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
from sklearn.metrics import precision_recall_curve
import matplotlib.pyplot as plt
 
def lr_train_and_predict(X, y):
    m = LogisticRegression().fit(X, y)
    return m, m.predict(X)
 
def draw_precision_recall_graph(precision, recall):
    plt.close('all')
    plt.step(recall, precision, color='b', alpha=0.2, where='post')
 
    step_kwargs = {'step''post'}
    plt.fill_between(recall, precision, alpha=0.2, color='b'**step_kwargs)
 
    plt.xlabel('Recall')
    plt.ylabel('Precision')
    plt.show()
 
# 데이터 다운로드
url = 'https://raw.githubusercontent.com/salopge/datasets/master/skewed_data.csv'
urllib.request.urlretrieve(url, './skewed_data.csv')
 
# 평형/기울어짐 2개의 상태로 나누기
data = pd.read_csv('skewed_data.csv', names=['var1''var2''var3''label'])
 
# 데이터 학습 및 예측
X_org = data.loc[:, 'var1':'var3']
y_org = data.label
model_org, pred_y_org = lr_train_and_predict(X_org, y_org)
 
y_prob_org = model_org.predict_proba(X_org)[:, 1]
print(y_prob_org[:5])
 
precision, recall, threshold = precision_recall_curve(y_org, y_prob_org)
print('pre', precision[:5])
print('reca', recall[:5])
print(threshold[:5])
 
draw_precision_recall_graph(precision, recall)
f1_score = 2 * precision * recall / (precision + recall)
print(f1_score[:5])
 
best_threshold = threshold[np.argmax(f1_score)]
print(best_threshold)
 
cs

 



 코드의 설명을 해 보자면   

1. skewed되어 있는 데이터를 불러 온다.
2. 기존의 0.5였던 threshold의 값을 요리조리 바꾸어본다.
3. precision/recall의 값을 찾는다. (그리고 그림으로 보여준다.)
4. precision/recall값들을 통해 f1_score를 계산한다.
5. f1_score들을 비교한다.
6.가장 높은 점수를 보이는 threshold의 값을 뱉는 코드이다!!

  이렇게 머신 러닝의 방법론과 분석을 공부해서 정리하였다. 요즘 공부를 하는 빈도가 뜸해졌는데, 도서관이라도 가면서 다시 의욕을 불태워야겠다!!!