학습 과정을 시각화

  • 지금까지 수행한 결과는 정량적인 평가로 테스트 데이터에 대한 예측 정확도를 측정하였다.
  • 학습 데이터에 대해서도 오차 함수의 값이 예측 정확도을 출력하기는 했어도 이는 학습이 진행되는지만을 확인하기 위한 것이었고 어떤 학습이 진행되고 있는지 대략적인 파악만 가능했다.
  • 데이터의 규모가 커지면 검증 데이터를 사용해서 학습을 평가하는 작업도 적절한 선에서 실시해야 한다.
  • 테스트 데이터에 대한 예측 정확도는 (데이터가 1개일 경우) 결과가 하나의 수치로 표현되지만 학습 데이터 또는 검증 데이터에 관한 예측 정확도는 각 epoch마다 평가해야 하므로 여러 개의 수치를 한 눈에 볼 수 있어야 하며, 이는 숫자들을 보는 것보다는 그래프 형태로 볼 수 있으면 직관적으로 파악이 가능하다.
  • 지금까지 구현한 코드에 다음 처리 과정을 추가해보도록 한다.
    • 검증 데이터를 사용해서 학습시키고 예측한다.
    • 학습시킬 때의 예측 정확도를 시각화한다.


TensorFlow로 구현

  • 학습 데이터와 검증 데이터, 테스트 데이터를 준비하는 부분 구현
    • 전체 데이터를 학습 데이터 테스트 데이터로 나누고, 다시 학습 데이터를 학습 데이터와 학습한 모델을 평가하기 위한 검증 데이터로 나눈다.
if __name__ == '__main__':
    MNIST = datasets.fetch_mldata('MNIST original', data_home='.')

    n = len(MNIST.data)
    indices = np.random.permutation(range(n))[:N]
    X = MNIST.data[indices]
    Y = MNIST.target[indices]
    Y = np.eye(10)[Y.astype(int)]  # 1-of-K hot coding 표현으로 변환

    test_size = 0.2
    # 학습 데이터와 테스트 데이터를 80:20으로 나눈다
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=test_size)

    # 학습 데이터의 20%를 검증 데이터로 사용한다
    X_train, X_val, Y_train, Y_val = train_test_split(X_train, Y_train, test_size=test_size)


  • 모든 학습 데이터를 학습 데이터와 검증 데이터로 완전히 분리하고 같은 검증 데이터를 모델 평가에 사용하는 기법을 홀드아웃 검증(hold-out validation)이라고 한다.
  • 학습 데이터를 $K$개의 데이터 셋으로 나누고 그 중 하나를 검증 데이터로 사용하고 나머지 $K-1$개의 데이터를 학습에 사용하는 기법을 $K$-분할교차 검증($K$-cross validation)이라고 한다.
    • $K$-분할교차 검증에서는 $K$개 나눈 데이터 셋을 조합해서 총 $K$번의 학습과 검증을 수행한다.
    • 이 과정에서 얻어진 예측 정확도의 평균을 모델의 성능으로 간주한다.
    • $K$-분할교차 검증이 일반화 성능을 매우 엄밀하게 평가할 수 있지만 딥러닝에서는 모델을 학습시키는데 방대한 시간이 걸릴 경우가 많기 때문에 $K$-분할교차 검증은 잘 사용되지 않는다.


  • 검증 데이터에 대한 오차나 예측 정확도는 각 epoch마다 평가해야 하기 때문에 모델을 학습시키는 부분은 다음과 같이 구현한다.
for epoch in range(epoch):
    X_, Y_ = shuffle(x_train, y_train)
    
    for i in range(n_batches):
        start = i * batch_size
        end = start + batch_size
        
        sess.run(train, feed_dict={
            x: X_[start:end],
            y: Y_[start:end],
            keeP_prob: p_keep
        })
        
    # 검증 데이터를 사용해 평가한다
    val_loss = loss.eval(session=sess, feed_dict={
        x: x_val,
        y: v_val,
        keep_prob: 1.0
    })
    
    val_acc = accuracy.eval(session=sess, feed_dict={
        x: x_val,
        y: y_val,
        keep_prob: 1.0
    })


  • 검증 데이터에 대한 손실을 나타내는 val_loss와 예측 정확도를 나타내는 val_acc를 각 epoch마다 출력해도 학습이 진행되는 상황을 확인할 수 있지만 시각화를 위해 저장해둘 변수를 설정한다. 프로그램 선언부에 명시한다.
history = {
    'val_loss' = [],
    'val_acc' = []
}


  • 이제 각 epoch마다 나오는 val_lossval_acc를 추가해 저장한다.
    # 학습 진행 상황 데이터를 저장할 변수 설정
    history = {'val_loss': [],
               'val_acc': []}
    
    for epoch in range(epochs):
        X_, Y_ = shuffle(X_train, Y_train)

        for i in range(n_batches):

            start = i * batch_size
            end = start + batch_size

            sess.run(train, feed_dict={x: X_[start: end], y: Y_[start: end], keep_prob: p_keep})

        # 검증 데이터를 사용해 정확도 측정
        val_loss = loss.eval(session=sess, feed_dict={x: X_val
                                                      y: Y_val,
                                                      keep_prob: 1.0})

        val_accuracy = accuracy.eval(session=sess, feed_dict={x: X_val,
                                                              y: Y_val,
                                                              keep_prob: 1.0})

        # 검증 데이터에 대한 학습 진행 상황을 저장
        history['val_loss'].append(val_loss)
        history['val_acc'].append(val_accuracy)


matplotlib 패키지를 이용해 그래프 그리기

  • 검증 데이터의 정확도를 그래프로 그리자
import matplotlib.pyplot as plt
import matplotlib

    # 폰트 설정
    font_name = matplotlib.font_manager.FontProperties(fname="c:/Windows/Fonts/malgun.ttf").get_name()
    matplotlib.rc('font', family=font_name)
    # 도화지 생성
    fig = plt.figure()
    # 정확도 그래프 그리기
    plt.plot(range(epochs), history['val_acc'], label='Accuracy', color='darkred')
    # 축 이름
    plt.xlabel('epochs')
    plt.ylabel('검증 정확도(%)')
    plt.title('epoch에 따른 검증 데이터에 대한 정확도 그래프')
    plt.grid(linestyle='--', color='lavender')
    # 그래프 표시
    plt.show()
    # 그래프 저장
    plt.savefig('mnist_tensorflow_acc.png')


  • 전체 코드
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
import matplotlib

def inference(x, keep_prob, n_in, n_hiddens, n_out):
    def weight_variable(shape):
        initial = tf.truncated_normal(shape, stddev=0.01)
        return tf.Variable(initial)

    def bias_variable(shape):
        initial = tf.zeros(shape)
        return tf.Variable(initial)

    # 입력층-은닉층과 은닉층-은닉층 설계
    for i, n_hidden in enumerate(n_hiddens):
        if i == 0:  # 입력층-은닉층
            input = x
            input_dim = n_in
        else:   # 은닉층-은닉층
            input = output  # 출력층이 다음 입력층이 된다
            input_dim = n_hiddens[i-1]

        W = weight_variable([input_dim, n_hidden])  # weight의 shape은 입력층 차원(784) x 뉴런의 개수
        b = bias_variable([n_hidden])               # bias의 shape은 뉴런의 개수

        hidden_layer = tf.nn.relu(tf.matmul(input, W) + b)  # 은닉층의 활성화 함수로 ReLU를 사용
        output = tf.nn.dropout(hidden_layer, keep_prob=keep_prob)   # 출력은 드롭아웃을 적용

    # 은닉층-출력층 설계
    W_out = weight_variable([n_hiddens[-1], n_out]) # 출력층의 weight shape은 뉴런의 개수 x 출력층의 차원(10개)
    b_out = bias_variable([n_out])                  # bias의 shape은 출력층의 차원

    hypothesis = tf.nn.softmax(tf.matmul(output, W_out) + b_out)    # 출력 활성화 함수는 softmax를 사용

    return hypothesis

def loss(hypothesis, y):
    # cross entropy
    cross_entropy = tf.reduce_mean(-tf.reduce_sum(y * tf.log(hypothesis), reduction_indices=[1]))

    return cross_entropy

def train(loss):
    optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.01)
    train = optimizer.minimize(loss)

    return train

def accuracy(hypothesis, y):
    correct_prediction = tf.equal(tf.argmax(hypothesis, 1), tf.argmax(y, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

    return accuracy


if __name__ == '__main__':
    MNIST = datasets.fetch_mldata('MNIST original', data_home='.')

    n = len(MNIST.data)
    N = int(n * 1.0)
    indices = np.random.permutation(range(n))[:N]
    X = MNIST.data[indices]
    Y = MNIST.target[indices]
    Y = np.eye(10)[Y.astype(int)]  # 1-of-K hot coding 표현으로 변환

    test_size = 0.2
    # 학습 데이터와 테스트 데이터를 80:20으로 나눈다
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=test_size)

    # 학습 데이터의 20%를 검증 데이터로 사용한다
    X_train, X_val, Y_train, Y_val = train_test_split(X_train, Y_train, test_size=test_size)

    # 2. 모델을 설정한다
    # 입출력 데이터와 은닉층의 뉴런 개수 설정
    n_in = len(X[0])  # 입력 데이터 이미지의 크기 28x28(=784)
    n_hiddens = [200, 200, 200]  # 각 은닉층의 뉴런 개수
    n_out = len(Y[0])  # 출력 데이터의 개수 (0~9)
    p_keep = 0.5        # 드롭아웃 확률의 비율

    x = tf.placeholder(tf.float32, shape=[None, n_in])
    y = tf.placeholder(tf.float32, shape=[None, n_out])

    keep_prob = tf.placeholder(tf.float32)

    # 가설식을 그래프에 추가
    hypothesis = inference(x, keep_prob, n_in=n_in, n_hiddens=n_hiddens, n_out=n_out)

    # 오차 함수를 그래프에 추가
    loss = loss(hypothesis, y)

    # 학습 함수를 그래프에 추가
    train = train(loss)

    # 정확도 예측 함수 그래프에 추가
    accuracy = accuracy(hypothesis, y)

    # 3. 모델을 학습시키면서 그래프를 실행
    epochs = 50
    batch_size = 200

    init = tf.global_variables_initializer()
    sess = tf.Session()
    sess.run(init)

    n_batches = len(X_train) // batch_size

    # 학습 진행 상황 데이터를 저장할 변수 설정
    history = {'val_loss': [],
               'val_acc': []}

    for epoch in range(epochs):
        X_, Y_ = shuffle(X_train, Y_train)

        for i in range(n_batches):

            start = i * batch_size
            end = start + batch_size

            sess.run(train, feed_dict={x: X_[start: end], y: Y_[start: end], keep_prob: p_keep})

        # 검증 데이터를 사용해 정확도 측정
        val_loss = loss.eval(session=sess, feed_dict={x: X_val,
                                                      y: Y_val,
                                                      keep_prob: 1.0})

        val_accuracy = accuracy.eval(session=sess, feed_dict={x: X_val,
                                                              y: Y_val,
                                                              keep_prob: 1.0})

        # 검증 데이터에 대한 학습 진행 상황을 저장
        history['val_loss'].append(val_loss)
        history['val_acc'].append(val_accuracy * 100)

        print(f'epoch : {epoch:03}, Validation loss : {val_loss:.7}, Validation accuracy : {val_accuracy*100:.6} %')

    # 4. 모델을 평가한다
    accuracy_rate = accuracy.eval(session=sess, feed_dict={x: X_test, y: Y_test, keep_prob: 1.0})
    print(f'Test Accuracy : {accuracy_rate*100:.6} %')

    # 폰트 설정
    font_name = matplotlib.font_manager.FontProperties(fname="c:/Windows/Fonts/malgun.ttf").get_name()
    matplotlib.rc('font', family=font_name)
    # 도화지 생성
    fig = plt.figure()
    # 정확도 그래프 그리기
    plt.plot(range(epochs), history['val_acc'], label='Accuracy', color='darkred')
    # 축 이름
    plt.xlabel('epochs')
    plt.ylabel('검증 정확도(%)')
    plt.title('epoch에 따른 검증 데이터에 대한 정확도 그래프')
    plt.grid(linestyle='--', color='lavender')
    # 그래프 표시
    plt.show()
    plt.savefig('mnist_tensorflow_acc.png')


  • 코드를 실행하면 그림 $11.1$과 같은 그래프를 볼 수 있다

그림 11.1 epoch에 따른 검증 정확도 변화


  • 그림 $11.1$을 보면 검증 정확도가 점점 올라갔다가 오느 시점부터는 전혀 학습이 이루어지지 않는다는 것을 알 수 있다.
  • 이는 경사 소실 문제와 마찬가지로 softmax 함수(sigmoid 함수)를 통해 전달되는 경사값이 너무 작아져서 $0$처럼 취급되어 계산되는 것이 원인이다.
  • 이 문제 해결을을 위해서 오차 함수 식을 다음과 같이 수정해야만 한다.
    • 계산에 사용되는 하한값(또는 상한값)을 새롭게 추가된 tf.clip_by_value()가 결정한다.
    • 이 하한값을 1e-10$(=10^{-10})$과 같이 작은 값으로 설정해두면 학습을 계산하는데  악영향을 주지 않으면서 $0$으로 나누는 문제를 방지할 수 있다.
def loss(hypothesis, y):
    # cross entropy
    cross_entropy = tf.reduce_mean(-tf.reduce_sum(y * tf.log(tf.clip_by_value(hypothesis, 1e-10, 1.0)),
                                                  reduction_indices=[1]))


  • 오차 함수 식을 수정한 결과 출력된 그래프는 그림 $11.2$와 같다.

그림 11.2 epoch에 따른 검증 정확도 변화(오차 함수 수정 후)


  • 오차도 함께 그래프에 표시해보자

# 폰트 설정 font_name = matplotlib.font_manager.FontProperties(fname="c:/Windows/Fonts/malgun.ttf").get_name() matplotlib.rc('font', family=font_name) # 도화지 생성 fig = plt.figure() # 큰 도화지(fig)를 1x1로 나눈 도화지에 첫 번째 칸에 ax_acc 그래프를 추가한다 ax_acc = fig.add_subplot(111) # 정확도 그래프 그리기 ax_acc.plot(range(epochs), history['val_acc'], label='정확도(%)', color='darkred') # 축 이름 plt.text(3, 14.7, "<----------------정확도(%)", verticalalignment='top', horizontalalignment='right') plt.xlabel('epochs') plt.ylabel('정확도(%)') ax_acc.grid(linestyle='--', color='lavender') # ax_loos 그래프를 x축을 동일하게 사용하여 겹쳐 그리기 선언 ax_loss = ax_acc.twinx() ax_loss.plot(range(epochs), history['val_loss'], label='오차', color='darkblue') plt.text(3, 2.2, "<----------------오차", verticalalignment='top', horizontalalignment='left') plt.ylabel('오차') ax_loss.yaxis.tick_right() ax_loss.grid(linestyle='--', color='lavender') # 그래프 표시 plt.show() plt.legend() plt.savefig('mnist_tensorflow_acc.png')


  • 정확도와 오차를 함께 그린 그래프는 $11.3$과 같다.

그림 11.2 epoch에 따른 검증 정확도와 오차를 함께 그린 그래프



Keras로 구현

  • Keras에서는 model.fit()의 반환값에 학습이 진행된 정도를 나타내는 값이 저장된 결과가 들어있다.
  • 검증 데이터를 사용해서 학습시키는 경우에는 학습시킬 때 다음과 같이 validation_data=를 사용하면 hist 변수에 오차와 정확도가 저장된다.
hist = model.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, validation_data=(X_val, Y_val))

val_loss = hist.history['val_loss']
val_acc = hist.history['val_acc']


  • 정확도를 그래프로 그리면 그림 $11.3$과 같다.
epochs = 50
batch_size = 200

hist = model.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, validation_data=(X_val, Y_val))
val_loss = hist.history['val_loss']
val_acc = hist.history['val_acc']

font_name = matplotlib.font_manager.FontProperties(fname="c:/Windows/Fonts/malgun.ttf").get_name()
matplotlib.rc('font', family=font_name)

fig = plt.figure()
ax_acc = fig.add_subplot(111)

ax_acc.plot(range(epochs), val_acc, label='정확도(%)', color='darkred')
plt.text(3, 14.7, "<----------------정확도(%)", verticalalignment='top', horizontalalignment='right')
plt.xlabel('epochs')
ax_acc.grid(linestyle='--', color='lavender')

plt.show()


그림 11.3 Keras로 그린 epoch에 따른 검증 정확도 그래프


  • 그림 $11.3$도 어느 순간부터 학습이 제대로 진행되지 않는 것을 알 수 있다. 그 이유는 매개변수를 초기화하는 부분에 있다.
  • TensorFlow에서는 weight를 초기화하기 위하여 tf.truncated_normal(shape, stddev=0.01) 함수를 사용하였지만 Keras에서는 아무 것도 사용하지 않아 학습이 제대로 진행되지 않는 것이다.
  • weight의 초기값을 어떻게 결정하는지가 학습이 제대로 이루어지는지 아니면 실패하는지에 영향을 준다. 초기값을 설정하는 몇 가지 기법이 연구가 된 상태이다.
  • Keras에서는 Dense() 함수에 kernel_initializer=라는 매개변수가 초기값을 결정하는데 영향을 준다. 매개변수를 사용하는 방법은 다음과 같다.
    • Keras에서 미리 alias로 정의되어 있는 것 이외의 것을 사용할 때에는 keras.backend를 통해 처리를 수행한다.
    • weight_variable() 함수 내부에서 K.truncated_normal(shape, stddev=0.01)이 TensorFlow에서 구현했을 때처럼 표준편차가 $0.01$인 절단 정규분포를 따르는 난수를 반환한다.
    • Dense(kernel_initializer=weight_variable) 함수가 실행되면 TensorFlow에서 실행되었던 것처럼 초기값을 가진 weight를 생성한다.
from keras import backend as K

def weight_variable(shape):
    return K.truncated_normal(shape, stddev=0.01)
model = Sequential()

for i, input_dim in enumerate(([n_in] + n_hiddens)[:-1]):
    model.add(Dense(input_dim=input_dim, units=n_hiddens[i], kernel_initializer=weight_variable))
    model.add(LeakyReLU(alpha=alpha))
    model.add(Dropout(p_keep))

model.add(Dense(units=n_out, kernel_initializer=weight_variable))
model.add(Activation('softmax'))


  • 절단정규분포는 keras.initializersTruncatedNormal() 함수를 통해 사용할 수도 있다.
    • 이 때 weight_variable()은 정의하지 않아도 된다.
from keras.initializers import TruncatedNormal

model.add(Dense(units=n_out, kernel_initializer=TruncatedNormal(stddev=0.01)))


  • Keras에서 NumPy로 생성한 난수를 그대로 사용해도 된다.
    • 이 때 weight_variable()은 다음과 같이 정의한다.
def weight_variable(shape):
return np.random.normal(scale=0.01, size=shape)


  • 전체 코드를 실행하면 그림 $11.4$와 같은 그래프를 얻을 수 있다.
from sklearn import datasets
from sklearn.model_selection import train_test_split
import numpy as np
from keras.models import Sequential
from keras.layers import Dense, Activation, Dropout
from keras.optimizers import SGD
from keras import backend as K
from keras.layers.advanced_activations import LeakyReLU
import matplotlib.pyplot as plt
import matplotlib

MNIST = datasets.fetch_mldata('MNIST original', data_home='.')

n = len(MNIST.data)
N = int(n * 1.00)  # 70,000개의 데이터를 모두 사용
test_size = 0.2

# 학습 데이터와 테스트 데이터를 나누기 전에 입력 데이터를 무작위로 섞는다
indices = np.random.permutation(range(n))[:N]
X = MNIST.data[indices]
Y = MNIST.target[indices]
Y = np.eye(10)[Y.astype(int)]  # 1-of-K hot coding 표현으로 변환

# 학습 데이터와 테스트 데이터를 80:20으로 나눈다
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=test_size)
X_train, X_val, Y_train, Y_val = train_test_split(X_train, Y_train, test_size=test_size)

n_in = len(X[0])  # 입력 데이터 이미지의 크기 28x28(=784)
n_hiddens = [200, 200, 200]  # 각 은닉층의 뉴런 개수
n_out = len(Y[0])  # 출력 데이터의 개수 (0~9)
alpha = 0.01
p_keep = 0.5        # 드롭아웃 확률의 비율

def weight_variable(shape):
    return K.truncated_normal(shape, stddev=0.01)

model = Sequential()

for i, input_dim in enumerate(([n_in] + n_hiddens)[:-1]):
    model.add(Dense(input_dim=input_dim, units=n_hiddens[i], kernel_initializer=weight_variable))
    model.add(LeakyReLU(alpha=alpha))
    model.add(Dropout(p_keep))

model.add(Dense(units=n_out, kernel_initializer=weight_variable))
model.add(Activation('softmax'))

model.compile(loss='categorical_crossentropy', optimizer=SGD(lr=0.01), metrics=['accuracy'])

epochs = 50
batch_size = 200

hist = model.fit(X_train, Y_train, epochs=epochs, batch_size=batch_size, validation_data=(X_val, Y_val))
val_loss = hist.history['val_loss']
val_acc = hist.history['val_acc']

font_name = matplotlib.font_manager.FontProperties(fname="c:/Windows/Fonts/malgun.ttf").get_name()
matplotlib.rc('font', family=font_name)

fig = plt.figure()
ax_acc = fig.add_subplot(111)

ax_acc.plot(range(epochs), val_acc, label='정확도(%)', color='darkred')
plt.text(3, 14.7, "<----------------정확도(%)", verticalalignment='top', horizontalalignment='right')
plt.xlabel('epochs')
plt.ylabel('정확도(%)')
ax_acc.grid(linestyle='--', color='lavender')

ax_loss = ax_acc.twinx()
ax_loss.plot(range(epochs), val_loss, label='오차', color='darkblue')
plt.text(3, 2.2, "<----------------오차", verticalalignment='top', horizontalalignment='left')
plt.ylabel('오차')
ax_loss.yaxis.tick_right()
ax_loss.grid(linestyle='--', color='lavender')

plt.legend()
plt.show()
ax_acc.legend()
plt.savefig('mnist_keras_plot.png')

evaluation = model.evaluate(X_test, Y_test)
print(f'\nloss : {evaluation[0]}, accuracy : {evaluation[1]*100:.6} %')


그림 11.4 weight를 초기화시켜 학습시킨 모델의 그래프



출처 : 정석으로 배우는 딥러닝

+ Recent posts