머신 러닝 교과서/ 텐서플로를 사용하여 신경망 훈련

고성능 머신 러닝 라이브러리 텐서플로

  • 텐서플로는 머신 러닝 작업 속도를 크게 높여준다.
    • 기본적으로 파이썬은 GIL(Global Interpreter Lock) 때문에 하나의 코어만 사용할 수 있다. 멀티프로세싱 라이브러릴 사용해서 여러 개의 코어에 연산을 분산할 수 있지만 최성능 데스크톱이라도 16개 이상의 코어를 가진 경우는 거의 없다.
    • 그러나 아주 간단한 다층 퍼셉트론이라도 하나의 은닉층이 100개의 유닛을 갖기 쉽기기 때문에, 단일 프로세스로는 수행하기가 어렵다.
    • 이 문제에 대한 해결책은 GPU를 사용하는 것이다.  최신 CPU 가격의 60% 정도로 코어 개수가 240배나 많고 초당 부동소수점 연산을 12배나 많이 할 수 있는 GPU를 구매할 수 있다.
    • 특정 GPU에 맞는 코드를 작성하는 일은 파이썬 인터프리터에서 코드를 실행하는 것처럼 간단하지 않다. CUDA나 OpenCL처럼 특정 GPU를 사용할 수 있도록 도와주는 패키지가 있지만 CUDA나 OpenCL에서 코드를 작성하는 것은 머신 러닝 알고리즘을 구현하고 실행하기에 편리한 환경이 아니다. 다행히 이런 이유 때문에 텐서플로가 개발되었다.

텐서플로란?

  • 텐서플로는 머신 러닝 알고리즘을 구현하고 실행하기 위한 프로그래밍 인터페이스로서 확장이 용이하고 다양한 플랫폼을 지원하며 딥러닝을 위한 간단한 인터페이스도 포함하고 있다.
    • 텐서플로는 구글 브레인 팀 연구자와 엔지니어들이 개발했는데, 초창기에는 구글 내부 용도로 개발되었지만 2015년 11월 오픈 소스 라이선스로 릴리스 되었다.
  • 머신 러닝 모델의 훈련 성능을 향상시키기 위해 텐서플로는 CPU와 GPU를 모두 활용할 수 있다. 그러나 GPU를 사용할 때 최대 성능을 이끌어낼 수 있다.
    • 텐서플로는 공식적으로 CUDA 기반의 GPU를 지원한다. OpenCL 기반의 GPU 지원은 아직 실험적이다.
    • 텐서플로는 여러 프로그래밍 언어를 지원하는데 파이썬 API는 완전히 성숙되어 있기 때문에 많은 머신 러닝, 딥러닝 기술자들에게 인기가 높다.
    • 텐서플로는 공식적인 C++ API도 가지고 있다. 자바, 하스켈, Node Js, Go 같은 언어를 위한 API는 아직 안정적이지 않다.
  • 텐서플로 연산은 데이터 흐름을 표현하는 유향 그래프(directed graph)를 구성하여 수행된다.

텐서플로 학습 방법

  • 먼저 텐서플로의 저수준 API를 배워보겠다. 이런 방식으로 모델을 만드는 것이 처음에는 조금 번거로울 수 있지만 프로그래머가 기본 연산을 연결하여 복잡한 모델을 개발할 수 있도록 자유도를 높여주는 장점이 있다.
    • 텐서플로 1.1.0 버전부터 저수준 API 위에 고수준 API가 추가되었다.
    • 텐서플로 1.4.0 버전부터는 케라스가 contrib 모듈에서 벗어나 핵심 모듈(tf.keras)이 되었고, 텐서플로 2.0에서는 tf.keras가 최상위 파이썬 API가 되었다. 이런 고수준 API를 사용하면 훨씬 빠르게 모델을 만들고 실험할 수 있다.
  • 우선 저수준 API를 배운 후에 고수준 API인 tf.keras를 살펴보겠다.

텐서플로 시작

  • 파이썬의 pip 인스톨러를 사용하여 PyPI로부터 텐서플로를 설치할 수 있다.
pip install tensorflow
  • GPU를 사용하려면 CUDA 툴킷과 NVIDIA cuDNN을 설치해야 한다. GPU용 텐서플로는 다음과 같이 설치할 수 있다.
pip install tensorflow-gpu
  • 텐서플로는 노드 집합으로 구성된 계산 그래프를 바탕으로 한다. 각 노드는 0개 이상의 입력이나 출력을 갖는 연산을 나타낸다. 계산 그래프의 에지를 따라 이동하는 값을 텐서(tensor)라고 한다.
  • 텐서는 스칼라(scalar), 벡터, 행렬이 일반화된 것으로 생각할 수 있다.
    • 구체적으로 말해 스칼라는 랭크 0 텐서이고, 백터는 랭크 1 텐서, 행렬은 랭크 2 텐서로 정의한다.
    • 행렬을 세 번째 차원으로 쌓아 올리면 랭크 3 텐서가 된다.
  • 텐서플로의 스칼라를 사용한 첫 번째 예제로 1차원 데이터셋 x 와 가중치 w , 절편 b 로부터 최종입력 z 를 계산해 보겠다.

z = w \times x + b

  • 아래 코드는 텐서플로 1.x 방식의 저수준 API를 사용하여 이 식을 구현한 것이다.
import tensorflow as tf

# 그래프를 생성한다
g = tf.Graph()

with g.as_default():
    x = tf.compat.v1.placeholder(dtype=tf.float32, shape=(None), name='x')
   w = tf.Variable(2.0, name='weight')
    b = tf.Variable(0.7, name='bias')
    z = w * x + b

    init = tf.compat.v1.global_variables_initializer()

# 세션을 만들고 그래프 g를 전달한다.
with tf.compat.v1.Session(graph=g) as sess:
    ## w와 b를 초기화한다.
    sess.run(init)

    ## z를 평가한다.
   for t in [1.0, 0.6, -1.8]:
        print('x=%4.1f --> z=%4.1f' % (t, sess.run(z, feed_dict={x:t})))
  • 보통 텐서플로 1.x 방식의 저수준 API로 개발할 때 입력 데이터(x, y 또는 튜닝이 필요한 다른 파라미터)를 위해 플레이스홀더(placeholder)를 정의한다.
    • 그 다음 가중치 행렬을 정의하고 입력에서부터 출력까지 연결된 모델을 만든다.
    • 최적화 문제인 경우에는 손실 함수 또는 비용 함수를 정의하고 어떤 최적화 알고리즘을 사용할지 결정한다.
    • 텐서플로의 그래프는 정의된 모든 요소를 노드로 포함시킨다.
  • 그 다음 세션을 만들고 변수를 초기화한다. 앞서 shape=(None) 으로 플레이스홀더 x를 만들었다. 입력 데이터 크기를 정의하지 않았으므로 다음과 같이 배치 데이터를 한 번에 전달하여 원소 하나씩 차례대로 모델에 주입할 수 있다.
with tf.compat.v1.Session(graph=g) as sess:
    sess.run(init)
    print(sess.run(z, feed_dict={x:[1., 2., 3.]}))
  • 이 코드에 있는 텐서 z를 출력해 보면 다음과 같다.
print(z)
# Tensor("add:0", dtype=float32)
  • 텐서 이름 “add:0″은 z가 덧셈 연산의 첫 번째 출력이라는 것을 알려준다.
    • 텐서 z에는 실제 어떤 값도 들어있지 않다. 세션을 열고 z를 평가해야 비로소 값을 얻을 수 있다.
    • 이렇게 텐서플로 1.x 방식에서는 그래프 정의 단계와 실행 단계로 크게 나누어져 있다.
    • z를 평가할 때 필요한 데이터가 있다면 feed_dict 매개변수를 통해 플레이스홀더에 데이터를 주입해야 한다.
  • 텐서플로 2.x 방식으로 위와 동일한 계산을 만들어 보겠다.
import tensorflow as tf

w = tf.Variable(2.0, name='weight')
b = tf.Variable(0.7, name='bias')

for x in [1.0, 0.6, -1.8]:
    z = w * x + b
    print('x=%4.1f --> z=%4.1f' % (x, z))
  • 훨씬 간단하게 수행할 수 있다. Session 객체를 만들어서 플레이스홀더에 데이터를 주입하는 대신 파이썬 리스트를 사용하여 직접 z 값을 계산했다.
    • 텐서플로 2.x 방식에서는 변수 초기화 과정도 불필요하다.
  • z 값을 출력하면 다음과 같다.
print(z)
tf.Tensor(-2.8999999, shape=(), dtype=float32)
  • 여기서 z는 덧셈 연산의 출력 텐서가 아니라 실제 값을 가진 텐서이다.
  • 또 다음과 같이 for 반복문을 사용하지 않고 리스트 데이터를 한 번에 계산할 수도 있다.
z = w * [1., 2., 3.] + b
print(z.numpy())
  • 위 코드처럼 텐서 값을 넘파이 배열로 출력하려면 numpy() 메서드를 사용하면 된다.

배열 구조 다루기

  • 텐서플로에서 배열 구조를 어떻게 다루는지 알아보자.
    • 다음 코드를 실행하면 3 x 2 x 3 크기의 간단한 랭크 3 텐서를 만든다. 그 다음 크기를 바꾸고 텐서플로의 최적화된 연산을 사용하여 각 열의 합을 계산한다.
import tensorflow as tf
import numpy as np

x_array = np.arange(18).reshape(3, 2, 3)
x2 = tf.reshape(x_array, shape=(-1, 6))

## 각 열의 합을 계산한다
xsum = tf.reduce_sum(x2, axis=0)

## 각 열의 평균을 계산한다
xmean = tf.reduce_mean(x2, axis=0)

print('입력 크기:', x_array.shape)
print('크기가 변경된 입력:\n', x2.numpy())
print('열의 합:\n', xsum.numpy())
print('열의 평균:\n', xmean.numpy())

# 결과
# 입력 크기: (3, 2, 3)
# 크기가 변경된 입력:
# [[ 0 1 2 3 4 5]
# [ 6 7 8 9 10 11]
# [12 13 14 15 16 17]]
# 열의 합:
# [18 21 24 27 30 33]
# 열의 평균:
# [ 6 7 8 9 10 11]
  • reshape 메서드의 첫 번째 차원으로 -1을 사용했는데, 텐서 크기를 바꿀 때 특정 차원에 -1을 사용하면 텐서 전체 크기와남은 차원에 따라 계산된다.
    • 예컨대 tf.reshape(tensor, shape=(-1,))은 일렬로 텐서를 펼친다.
  • tf.reduce_sum과 tf.reduce_mean은 각각 텐서의 합과 평균을 계산하는데, axis 매개변수는 축소될 차원을 지정한다.
    • axis=0은 행 차원을 축소시켜 각 열의 합이나 평균을 계산한다.
    • axis=1은 열 차원을 축소시켜 각 행의 합이나 평균을 계산한다.
    • axis 매개변수의 기본값은 None으로 모든 차원을 축소하여 스칼라 텐서를 반환한다.

텐서플로 저수준 API로 간단한 모델 개발

  • 텐서플로를 이용하여 최소 제곱법(Ordinary Least Squares, OLS) 회귀를 구현해보자
  • 먼저 10개의 훈련 샘플로 이루어진 작은 1차원 데이터셋을 만든다.
import tensorflow as tf
import numpy as np

X_train = np.arange(10).reshape((10, 1))
y_train = np.array([1.0, 1.3, 3.1, 2.0, 5.0, 6.3, 6.6, 7.4, 8.0, 9.0])
  • 이 데이터셋으로 입력 x 에서 출력 y 를 예측하는 선형 회귀 모델을 훈련하려고 한다. 이 모델을 TfLinreg라는 이름의 클래스로 구현해 보자.
    • 이 클래스에서 훈련되는 변수인 가중치 w 와 절편 b 를 정의하자.
    • z = w \times x + b 처럼 선형 회귀 모델을 정의한 후 평균 제곱 오차(Mean Squared Error, MSE)를 비용함수로 정의한다.
    • 모델 가중치를 학습하기 위해 경사 하강법 옵티마이저를 사용한다.
    • 전체 코드는 아래와 같다.
class TfLinreg(object):

   def __init__(self, learning_rate=0.01):
        ## 가중치와 절편을 정의한다
       self.w = tf.Variable(tf.zeros(shape=(1)))
        self.b = tf.Variable(tf.zeros(shape=(1)))

       ## 경사 하강법 옵티마지어를 설정한다
    self.optimizer = tf.keras.optimizers.SGD(lr=learning_rate)   

   def fit(self, X, y, num_epochs=10):
       ## 비용 함수의 값을 저장하기 위한 리스트를 정의한다
        training_costs = []

       for step in range(num_epochs):
           ## 자동 미분을 위해 연산 과정을 기록한다
            with tf.GradientTape() as tape:
               z_net = self.w * X + self.b
               z_net = tf.reshape(z_net, [-1])
               sqr_errors = tf.square(y - z_net)
               mean_cost = tf.reduce_mean(sqr_errors)           

           ## 비용함수에 대한 가중치의 그래디언트를 계산한다
            grads = tape.gradient(mean_cost, [self.w, self.b])

            ## 옵티마이저에 그래디언트를 반영한다
            self.optimizer.apply_gradients(zip(grads, [self.w, self.b]))

           ## 비용 함수의 값을 저장한다
           training_costs.append(mean_cost.numpy())       

        return training_costs
  • 저수준 API를 사용하여 신경망을 구현할 때 그리디언트를 가장 손쉽게 계산하는 방법은 tf.GradientTape() 컨텍스트(context)를 사용하는 것이다.
    • GradientTape()는 with 블록 안에 있는 텐서플로 변수를 자동으로 감시한다. 변수에 대한 그래디언트를 구하려면 GradientTape 객체의 gradient() 메서드를 사용한다.
    • 이 메서드의 첫 번째 매개변수는 미분 대상 텐서 또는 텐서 리스트고, 두 번째 매개변수는 그래디언트를 구하려는 변수의 리스트이다.
    • 똑똑하게도 텐서플로는 gradient(0 메서드가 호출된 후 사용한 자원을 자동으로 반납한다.
  • 그래디언트를 계산했다면 옵티마이저에 이를 전달하여 가중치를 업데이트해야 한다. 옵티마이저의 apply_gradient() 메서드는 그래디언트와 변수를 쌍으로 하는 튜플의 리스트를 입력받는다.
    • 위 코드에서는 파이썬의 zip 반복자를 사용하여 이를 간결하게 표현했다.
  • 모델을 정의했으니 매개변수 기본값으로 이 클래스의 인스턴스를 만들고 모델을 훈련해 보자.
lrmodel = TfLinreg()
training_costs = lrmodel.fit(X_train, y_train)
  •  열 번의 에포크 동안 계산된 훈련 비용을 그래프로 그려서 모델이 수렴하는지 확인해 보자
import matplotlib.pyplot as plt

plt.plot(range(1, len(training_costs) + 1), training_costs)
plt.tight_layout()
plt.xlabel('Epoch')
plt.ylabel('Training Cost')
plt.show()

  • 비용 함수 값을 보면 이 데이터셋에 잘 맞는 회귀 모델을 만든 것 같다. 입력 특성을 기반으로 예측을 만들기 위해 새로운 메서드를 추가해 보자. 이 메서드는 타깃 데이터 없이 특성 값만 사용한다.
def predict(self, X):
        return self.w * X + self.b
  • 이제 훈련 데이터에서 학습된 선형 회귀 곡선을 그려 보자
plt.scatter(X_train, y_train, marker='s', s=50, label='Training Data')
plt.plot(range(X_train.shape[0]), lrmodel.predict(X_train), color='gray', marker='o', markersize=6, linewidth=3, label='LinReg Model')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.tight_layout()
plt.show()

tf.keras API로 다층 신경망 훈련

  • 케라스는 2015년 초에 개발되기 시작했고 현재 가장 인기 있고 널리 사용되는 딥러닝 라이브러리 중 하나이다. 케라스는 씨아노(Theano)와 텐서플로를 백엔드(backend)로 사용한다.
  • 텐서플로와 비슷하게 케라스는 GPU를 사용하여 신경망 훈련을 가속시킬 수 있다. 매우 직관적이고 사용하기 쉬운 API를 가진 것이 눈에 띄는 특징이다. 단 몇 줄의 코드만으로 신경망을 구현할 수 있다.
  • 케라스는 처음에 씨아노를 백엔드로 사용하는 독립된 API로 릴리스 되었고 나중에 추가로 텐서플로를 지원했다. 텐서플로 버전 1.1.0 부터는 케라스가 텐서플로에 통합되었다. 텐서플로 1.1.0 이상을 쓰고 있다면 케라스를 따로 설치할 필요가 없다.
  • 텐서플로 1.4.0 부터는 케라스가 contrib 서브모듈 밖으로 이동하여 텐서플로의 핵심 모듈(tf.keras)가 되었고, 텐서플로 2.0에서는 tf.keras가 표준 파이썬 API가 되었다.
  • 케라스를 사용하면 텐서플로를 사용하여 신경망 모델을 손쉽게 구현할 수 있다.

훈련 데이터 준비

  • tf.keras 고수준 API를 사용하여 어떻게 신경망을 훈련하는지 알아보자. 이를 위해 MNIST 데이터셋의 손글씨 숫자를 분류하는 다층 퍼셉트론을 만들어보자.
  • 파일을 내려받은 후 현재 작업 디렉터리 안에 mnist 폴더를 만들어 옮긴다. 그 다음 12장에서 구현한 load_mnist 함수를 사용하여 훈련 데이터셋과 테스트 데이터셋을 로드한다.
X_train, y_train = mn.load_mnist('mnist/', kind='train')
X_test, y_test = mn.load_mnist('mnist/', kind='t10k')

mean_vals = np.mean(X_train, axis=0)
std_val = np.std(X_train)

X_train_centered = (X_train - mean_vals) / std_val
X_test_centered = (X_test - mean_vals) / std_val

del X_train, X_test

print(X_train_centered.shape, y_train.shape)
print(X_test_centered.shape, y_test.shape)
  • 일관된 결과를 만들기 위해 넘파이 난수 초깃값을 설정하자
np.random.seed(123)
  • 훈련 데이터를 준비하기 위해 클래스 레이블(0-9 사이의 정수)을 원-핫 인코딩으로 변경하자. tf.keras에는 이를 위한 편리한 도구가 준비되어 있다.
y_train_onehot = tf.keras.utils.to_categorical(y_train)

print('처음 3개 레이블: ', y_train[:3])
print('\n처음 3개 레이블 (원-핫):\n ', y_train_onehot[:3])

### 결과
# 처음 3개 레이블: [5 0 4]
# 처음 3개 레이블 (원-핫):
# [[0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]
# [1. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
# [0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]]
  • to_categorical 함수는 입력 텐서에서 가장 큰 정수를 찾아 원-핫 인코딩 크기를 결정한다. 앞 예에서처럼 정수 0은 첫 번째 원소가 1이 되는 식이다.
    • num_classes 매개변수를 사용하여 입력의 최댓값보다 더 큰 원-핫 인코딩을 만들 수도 있다.

피드포워드 신경망 구성

  • 이제 신경망을 구현하겠다. 간단하게 세 개의 완전 연결된 층을 만들어 보자.
    • 처음 두 개의 층은 하이퍼볼릭 탄젠트(tanh) 활성화 함수를 가진 50개의 은닉 유닛으로 이루어진다.
    • 마지막 층은 열 개의 클래스 레이블에 해당하는 열 개의 은닉 유닛을 가진다.
    • 마지막 층은 각 클래스의 확률을 계산하기 위해 소프트맥스(softmax) 함수를 사용한다.
    • 다음 코드처럼 tf.keras는 이런 작업을 매우 간단하게 처리할 수 있다
model = tf.keras.models.Sequential()

model.add(tf.keras.layers.Dense(units=50, input_dim=X_train_centered.shape[1], kernel_initializer='glorot_uniform', bias_initializer='zeros', activation='tanh'))

model.add(tf.keras.layers.Dense(units=50, input_dim=50, kernel_initializer='glorot_uniform', bias_initializer='zeros', activation='tanh'))

model.add(tf.keras.layers.Dense(units=y_train_onehot.shape[1], input_dim=50, kernel_initializer='glorot_uniform', bias_initializer='zeros', activation='softmax'))
  • 먼저 Sequential 클래스를 사용하여 피드포워드 신경망을 구현하는 새로운 모델을 초기화한다. 그 다음 원하는 만큼 층을 추가할 수 있다.
    • 처음 추가한 층은 입력층과 연결되기 떄문에 input_dim 속성이 훈련 세트에 있는 특성(열) 개수와 일치해야 한다.
    • 또 두 개의 연속된 층에서 출력 유닛(units)과 입력 유닛(input_dim)이 일치해야 한다. 위 코드에서 두 개의 은닉층은 50개의 은닉 유닛과 한 개의 절편 유닛을 가진다.
    • 출력층의 유닛 개수는 고유한 클래스 레이블의 개수와 같아야 한다. 원-핫 인코딩된 클래스 레이블 배열의 열 개수이다.
  • 지금까지 만든 모델 구조를 summary() 메서드를 사용하여 출력하면 다음과 같다.
    • 출력의 시작 부분이 신경망 입력에 가까운 층이고 끝부분이 신경망 출력에 가까운 층이다.
model.summary()

#### 결과
# Model: "sequential"
# _________________________________________________________________
# Layer (type) Output Shape Param #
# =================================================================
# dense (Dense) (None, 50) 39250
# _________________________________________________________________
# dense_1 (Dense) (None, 50) 2550
# _________________________________________________________________
# dense_2 (Dense) (None, 10) 510
# =================================================================
# Total params: 42,310
# Trainable params: 42,310
# Non-trainable params: 0

피드포워드 신경망 훈련

  • 모델 구성을 마치면 훈련을 수행하기 전에 모델을 컴파일 해야 한다. 이 단계에서 최적화할 손실 함수를 정의하고 최적화에 사용할 경사 하강법 옵티마이저를 선택한다.
    • 이전 장에서 사용해 보았던 확률적 경사 하강법 최적화를 선택하겠다.
    • 또 에포크마다 학습률을 조절하기 위한 학습률 감쇠 상수와 모멘텀 값을 지정한다.
    • 마지막으로 비용 함수(또는 손실 함수)를 categorical_crossentropy로 설정한다.
  • 이진 크로스 엔트로피는 로지스틱 손실 함수의 기술적인 표현이다.
    • 범주형 크로스 엔트로피는 소프트맥스를 사용하여 다중 클래스 예측으로 일반화한 것이다. 이에 대해서는 차후에 설명하겠다.
    • 옵티마이저 설정과 모델을 컴파일하는 코드는 다음과 같다.
sgd_optimizer = tf.keras.optimizers.SGD(lr=0.001, decay=1e-7, momentum=.9)
model.compile(optimizer=sgd_optimizer, loss='categorical_crossentropy')
  • 모델을 컴파일한 후 fit 메서드를 호출하여 훈련시킨다.
    • 여기서는 미니 배치 경사하강법을 사용하겠다. 배치마다 담긴 훈련 샘플 개수는 64개다.
    • 50번의 에포크 동안 MLP를 훈련시키겠다.
    • verbose=1로 설정하여 훈련하는 동안 비용 함수의 최적화 과정을 따라가보겠다.
    • validation_split 매개변수는 아주 유용한데, 0.1로 설정하면 훈련 데이터의 10%를 검증 데이터로 따로 떼어낸다. 에포크마다 이 데이터로 검증 점수를 계산하므로 모델이 과대적합되었는지 모니터링 할 수 있다.
history = model.fit(X_train_centered, y_train_onehot, batch_size=64, epochs=50, verbose=1, validation_split=0.1)

### 결과
# Train on 54000 samples, validate on 6000 samples
# Epoch 1/50
# 2020-05-01 10:08:46.207749: I tensorflow/stream_executor/platform/default/dso_loader.cc:44] Successfully opened dynamic library cublas64_100.dll
# 54000/54000 [=========================] - 2s 43us/sample - loss: 0.6957 - val_loss: 0.3554
# Epoch 2/50
# 54000/54000 [=========================] - 1s 27us/sample - loss: 0.3662 - val_loss: 0.2747
# Epoch 3/50
# 54000/54000 [=========================] - 2s 28us/sample - loss: 0.3039 - val_loss: 0.2395
# ...
# Epoch 50/50
# 54000/54000 [=========================] - 2s 28us/sample - loss: 0.0479 - val_loss: 0.1110
  • 훈련하는 동안 비용 함수 값을 출력하는 기능은 아주 유용하다. 훈련 도중에 비용이 감소하는지 여부를 빨리 확인해서 감소하지 않는다면 하이퍼파라미터를 튜닝하기 위해 알고리즘을 일찍 멈출 수 있다.
  • 클래스 레이블을 예측하려면 predict_classes 메서드를 사용하여 정수로 된 클래스 레이블을 얻을 수 있다.
  • 클래스 레이블을 예측하려면 predict_classes 메서드를 사용하여 정수로된 클래스 레이블을 얻을 수 있다.
    • 마지막으로 훈련 세트와 테스트 세트에서 모델 정확도를 출력해 보자
y_train_pred = model.predict_classes(X_train_centered, verbose=0)
correct_preds = np.sum(y_train==y_train_pred, axis=0)
train_acc = correct_preds / y_train.shape[0]
print('처음 3개 예측: ', y_train_pred[:3])
print('훈련 정확도: %.2f%%' % (train_acc * 100))

### 결과
# 처음 3개 예측: [5 0 4]
# 훈련 정확도: 98.91%

y_test_pred = model.predict_classes(X_test_centered, verbose=0)
correct_preds = np.sum(y_test==y_test_pred, axis=0)
test_acc = correct_preds / y_test.shape[0]
print('처음 3개 예측: ', y_test_pred[:3])
print('훈련 정확도: %.2f%%' % (test_acc * 100))

### 결과
# 처음 3개 예측: [7 2 1]
# 훈련 정확도: 96.37%

다층 신경망의 활성화 함수 선택

  • 지금까지 다층 피드포워드 신경망을 쉽게 이해하기 위해 시그모이드 활성화 함수만 설명했다. 12장에서 다층 퍼셉트론을 구현할 때 출력층과 은닉층에 이 함수를 사용했다.
    • 이반적으로 다른 문헌에서 부르는 것처럼 이 활성화 함수를 시그모이드 함수라고 했는데, 좀 더 정확한 정의는 로지스틱(logistic) 함수이다.
  • 기술적으로는 미분 가능하다면 어떤 함수라도 다층 신경망의 활성화 함수로 사용할 수 있다.
    • 아달린에서처럼 선형 활성화 함수도 사용할 수 있지만, 실제로 은닉층이나 출력층에서 선형 활성화 함수를 사용하는 것이 그리 유용하지는 않다.
    • 복잡한 문제를 해결하기 위해서는 일반적인 인공 신경망에 비선형성이 필요하기 때문이다.
    • 선형 함수를 합치면 결국 하나의 선형 함수가 된다.
  • 12장에서 사용한 로지스틱 활성화 함수가 뉴런 개념을 가장 비슷하게 흉내 낸 함수이다. 이 함수 출력을 뉴런의 활성화 여부에 대한 확률로 생각할 수 있다.
  • 로지스틱 활성화 함수는 큰 음수 입력이 들어오면 문제가 된다.
    • 이 경우 시그모이드 함수의 출력이 0에 가까워지기 때문이다. 시그모이드 함수가 0에 가까운 출력을 내면 신경망이 매우 느리게 학습한다.
    • 또한 훈련 과정에서 지역 최솟값에 갇힐 가능성이 높다.
    • 이런 이유로 은닉층에 하이퍼볼릭 탄젠트 함수를 더 선호한다.

로지스틱 함수 요약

  • 종종 시그모이드 함수라고 불리는 로지스틱 함수는 시그모이드 함수의 특별한 경우이다.
    • 3장 로지스틱 회귀에 관한 절에서 로지스틱 함수를 사용하여 이진 분류 문제일 때 샘플 x 가 양성 클래스(클래스 1)에 속할 확률을 모델링 했다.
    • 최종 입력 z 는 다음 식으로 계산된다.

z = w_{0} x_{0} + w_{1} x_{1} + ... + w_{m} x_{m} = \sum_{i=0}^{m} w_{i} x_{i} = w^{T} x

  • 로지스틱 함수는 다음과 같이 계산한다.

\phi_{logistic}(z) = {1 \over 1 + e^{-z}}

  • w_{0} 는 절편 유닛이다 (y 축과의 교차점. 즉 x_{0} = 1 이다)
    • 구체적인 예를 들기 위해 2차원 데이터 포인트 x 와 다음과 같은 가중치 벡터 w 로 구성된 모델을 가정해 보자
import numpy as np

X = np.array([1, 1.4, 2.5])
w = np.array([0.4, 0.3, 0.5])

def net_input(X, w):
    return np.dot(X, w)

def logistic(z):
    return 1.0 / (1.0 + np.exp(-z))

def logistic_activation(X, w):
    z = net_input(X, w)
    return logistic(z)
  • 이 특성과 가중치 값을 사용하여 최종 입력을 계산하고 이것으로 로지스틱 뉴런의 활성화 출력을 구하면 0.888을 얻는다. 이 샘플 x가 양성 클래스에 속할 확률이 88.8%라고 해석할 수 있다.
  • 12장에서 여러 개의 로지스틱 활성화 유닛으로 구성된 출력층 값을 계산하기 위해 원-핫 인코딩 기법을 사용했다.
    • 다음 코드에서처럼 여러 개의 로지스틱 활성화 유닛으로 구성된 출력층은 의미 있게 해석할만한 확률 값을 만들지 못한다.
W = np.array([[1.1, 1.2, 0.8, 0.4], [0.2, 0.4, 1.0, 0.2], [0.6, 1.5, 1.2, 0.7]])
A = np.array([[1, 0.1, 0.4, 0.6]])
Z = np.dot(W, A[0])

y_probas = logistic(Z)

print('최종 입력: \n', Z)
print('출력 유닛: \n', y_probas)

### 결과
# 최종 입력:
# [1.78 0.76 1.65]
# 출력 유닛:
# [0.85569687 0.68135373 0.83889105]

  • 출력에서 볼 수 있듯이 클래스가 세 개일 때 결과 확률을 이해하기 어렵다. 그것은 세 개의 합이 1이 아니기 때문이다.
    • 사실 클래스 소속 확률을 구하는 것이 아니라 클래스 레이블을 예측하기 위해서만 사용한다면 큰 문제는 아니다. 앞의 출력 결과에서 클래스 레이블을 예측하는 방법은 가장 큰 값을 선택하는 것이다.
  • 어떤 경우에는 다중 클래스 예측 문제에서 의미 있는 클래스 확률을 계산할 필요가 있다. 다음 절에서는 이런 문제를 다루기 위해 로지스틱 함수를 일반화한 softmax 함수를 살펴보겠다.

소프트맥스 함수를 사용하여 다중 클래스 확률 예측

  • 이전 절에서 argmax 함수를 사용하여 클래스 레이블을 구현하는 방법을 보았는데, 사실 소프트맥스는 간접적인 argmax 함수이다.
    • 하나의 클래스 인덱스를 찾는 대신 각 클래스의 확률을 반환하므로 다중 클래스 환경(다중 로지스틱 회귀(multinomial logistic regression))에서 의미 있는 클래스 확률을 계산할 수 있다.
  • softmax 함수는 특정 샘플의 최종 입력이 z 일 때 i 번째 클래스에 속할 확률을 M 개의 선형 함수 합으로 나누어 정규화한 것이다.

P(y=i|z) = \phi (z) = {e^{Z_{i}} \over \sum_{j=1}^{M} e^{Z_{j}}}

  • softmax 함수의 동작을 확인해 보기 위해 직접 만들어 보자
def softmax(z):
    return np.exp(z) / np.sum(np.exp(z))

y_probas = softmax(Z)
print('확률:\n', y_probas)

# 결과
# 확률:
# [0.44668973 0.16107406 0.39223621]
  • 예측된 클래스 확률 합은 1이 되었다. 예측 클래스 레이블은 로지스틱 출력에 argmax 함수를 적용한 것과 같다. softmax 함수는 출력을 정규화하여 다중 클래스일 때 의미 있는 클래스 소속 확률을 만든다.

하이퍼볼릭 탄젠트로 출력 범위 넓히기

  • 인공 신경망의 은닉층에 많이 사용하는 또 다른 시그모이드 함수는 하이퍼볼릭 탄젠트(hyperbolic tangent) (보통 tanh 라고 함)이다. 이 함수는 스케일이 조정된 로지스틱 함수라고 생각할 수 있다.

\phi_{logistic}(z) = {1 \over 1 + e^{-z}}

\phi_{tanh}(z) = 2 \times \phi_{logistic} (2z) - 1 = {e^{z} - e^{-z} \over e^{z} + e^{-z}}

  • 로지스틱 함수에 비해 하이퍼볼릭 탄젠트 함수의 장점은 출력 범위를 (-1, 1) 사이로 넓혀서 역전파 알고리즘의 수렴을 향상시킬 수 있다는 것이다.
    • 로지스틱 함수는 (0, 1) 범위의 출력 신호를 반환한다.
  • 두 시그모이드 함수를 그래프로 그리면 아래와 같다.
def tanh(z):
   e_p = np.exp(z)
   e_m = np.exp(-z)
    return (e_p - e_m) / (e_p + e_m)

  • 두 시그모이드 곡선은 매우 비슷하지만 tanh 함수가 logistic 함수보다 2배 큰 범위 출력을 갖는다.
  • 위에서는 tanh 함수와 logistic 함수를 직접 구현했지만 실전에서는 넘파이의 tanh 함수와 사이파이의 special 모듈에서 두 함수를 사용할 수 있다.

렐루 활성화 함수

  • 렐루(Rectified Linear Unit, ReLU)는 심층 신경망에 자주 사용되는 또 다른 활성화 함수이다. 렐루를 알아보기 전에 하이퍼볼릭 탄젠트와 로지스틱 활성화 함수의 그래디언트 소실 문제(vanishing gradient problem)를 살펴보자.
  • 최종 입력이 z_{1} = 20 에서 z_{2} = 25 로 바뀐다고 가정하자. 하이퍼볼릭 탄젠트 활성화 함수를 계산하면 \phi (z_{1}) \approx 1.0 \phi (z_{2}) \approx 1.0 이므로 출력에 변화가 없다.
    • 이는 최종 입력에 대한 활성화 함수의 도함수가 w_{0} 가 커짐에 따라 줄어든다는 뜻이다.
    • 결국 그래디언트가 0에 아주 가까워지기 때문에 훈련 과정 동안 가중치가 매우 느리게 학습된다.
    • 렐루 활성화 문제는 이런 문제를 해결한다. 렐루 함수의 수학적 정의는 다음과 같다.

\phi (z) = \max(0, z)

  • 렐루도 신경망이 복잡한 함수를 학습하기에 좋은 비선형 함수이다. 입력 값이 양수면 입력에 대한 렐루의 도함수는 항상 1이다.
    • 이것이 그래디언트 소실 문제를 해결해 주므로 심층 신경망에 적합하다.
    • 다음 장에서는 다층 합성곱 신경망을 위한 활성화 함수로 렐루 함수를 사용해 보겠다.

[ssba]

The author

Player가 아니라 Creator가 되려는 자/ suyeongpark@abyne.com

댓글 남기기

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.