자동미분(Automatic differentiation, Algorithmic differentiation, computational differentiation, auto-differentiation, 또는 줄여서 간단히 auto-diff) 은 컴퓨터 프로그램에 의해서 구체화된 함수의 미분을 산술적으로 계산할 때 사용하는 기술의 집합을 말합니다. 컴퓨터 프로그램에서 구체화한 함수는 아무리 복잡해보이더라도 기본적인 산술 연산 (덧셈, 뺄셈, 곱셉, 나눗셈 등)과 기본적인 함수 (지수, 로그, 싸인, 코싸인 등) 의 연속적인 실행으로 이루어진다는 아이디어를 기반으로 합니다. 복잡한 함수도 연쇄 법칙(Chain Rule)을 이용함으로써 합성함수를 구성하는 각 기본함의 미분의 곱으로 나타내고 반복적으로 계산함으로써 자동으로 복잡한 함수를 정확하고 효율적으로 미분할 수 있습니다.   


자동미분(Automatic Differentiation)은 딥러닝에서 오차 역전파 알고리즘을 사용해서 모델을 학습할 때 유용하게 사용합니다. TensorFlow는 Gradient Tapes 를 이용하면 즉시 실행 모드(eager execution mode)에서 쉽게 오차 역전파를 수행할 수 있습니다. 


이번 포스팅에서는 


1. 즉시실행모드에서 자동미분을 하기 위해 Gradient tapes 이 필요한 이유

2. TensorFlow에서 Gradient tapes를 이용해 자동미분하는 방법

3. 시그모이드 함수 자동미분 시각화

4. 테이프가 볼 것을 조정하는 방법 (Controlling what the tape watches)

5. Python 조건절 분기문을 이용한 테이프 기록 흐름 조정 (Control flow)


에 대해서 소개하겠습니다. 


이번 포스팅은 TensorFlow 튜토리얼의 내용을 번역하고 코드를 대부분 사용하였습니다. 


먼저 numpy, tensorflow, matplotlib 모듈을 importing 하겠습니다. 



import numpy as np

import matplotlib.pyplot as plt

import tensorflow as tf


print(tf.__version__)

2.4.0-dev20200913




  1. 즉시 실행 모드에서 자동미분을 하기 위해 Gradient tapes 이 필요한 이유


예를 들어서  y = a * x 라는 방정식에서 a 와 y 가 상수(constant)이고 x 가 변수(Varialbe) 일 때, 이 방정식을 오차역전파법으로 풀어서 변수 x 를 구하고자 한다고 합시다. 그러면 간단한 손실함수인 loss = abs(a * x - y) 를 최소로 하는 x 를 구하면 됩니다. 


아래 예의 경우 8.0 = 2.0 * x 방정식 함수로 부터 변수 x 의 값을 구하고자 합니다. x 를 10.0 에서 시작해서 abs(a * x - y) 손실함수 값을 최소로 하는 x 를 구하려면 '손실함수에 대한 x의 미분 (the gradient of the loss with respect to x)를 구해서 x 값을 미분값만큼 빼서 갱신해주어야 합니다. 


그런데 아래의 TensorFlow 2.x 버전에서의 Python 즉시실행모드(eager mode)에서 손실(Loss) 을 "바로 즉시 계산"(eager execution)해보려서 출력 결과를 보면 numpy=12.0 인 Tensor 상수입니다. 여기서 자동미분을 하려고 하면 문제가 생기는데요, 왜냐하면 자동미분을 하기 위해 필요한 함수와 계산 식의 연산 과정과 입력 값에 대한 정보가 즉시실행모드에서는 없기 때문입니다. (입력 값과 연산 과정은 저장 안하고 즉시실행해서 결과만 출력했음).  



a, y = tf.constant(2.0), tf.constant(8.0)

x = tf.Variable(10.0) # In practice, we start with a random value

loss = tf.math.abs(a * x - y)


loss

[Out] <tf.Tensor: shape=(), dtype=float32, numpy=12.0>

 




  2. TensorFlow에서 Gradient tapes를 이용해 자동미분하는 방법


이 문제를 해결하기 위해 TensorFlow 는 중간 연산 과정(함수, 연산)을 테이프(tape)에 차곡차곡 기록해주는 Gradient tapes 를 제공합니다. 


with tf.GradientTape() as tape: 로 저장할 tape을 지정해주면, 이후의 GradientTape() 문맥 아래에서의 TensorFlow의 연관 연산 코드는 tape에 저장이 됩니다. 이렇게 tape에 저장된 연산 과정 (함수, 연산식) 을 가져다가 TensorFlow는 dx = tape.gradient(loss, x) 로 후진 모드 자동 미분 (Reverse mode automatic differentiation) 방법으로 손실에 대한 x의 미분을 계산합니다. 이렇게 계산한 손실에 대한 x의 미분을 역전파(backpropagation)하여 x의 값을 갱신(update)하는 작업을 반복하므로써 변수 x의 답을 찾아가는 학습을 진행합니다. 


위의 (1)번에서 소개한 8.0 = 2.0 * x 의 방정식에서는 변수 x = 4.0 이 답인데요, TensorFlow 의 GradientTape() 과 tape.gradient() 를 사용해서 오차 역전파 방법으로 갱신하여 문제를 풀어보겠습니다. 처음에 x = 10.0에서 시작형 4회 반복하니 x = 4.0 으로 답을 잘 찾아갔군요.  



# UDF for training

def train_func():

    with tf.GradientTape() as tape:

        loss = tf.math.abs(a * x - y)

    

    # calculate gradient

    dx =  tape.gradient(loss, x)

    print('x = {}, dx = {:.2f}'.format(x.numpy(), dx))

    

    # update x <- x - dx

    x.assign(x - dx)



# Run train_func() UDF repeately

for i in range(4):

    train_func()


[Out]
x = 10.0, dx = 2.00
x = 8.0, dx = 2.00
x = 6.0, dx = 2.00
x = 4.0, dx = 0.00

 




라는 함수에서 (Target) y에 대한 (Source) x의 미분 (derivative of target y with respect to source x) 을 TensorFlow의 GradientTape.gradient() 메소드를 사용해서 계산해보겠습니다. (우리는 이미 고등학교 때 배웠던 미분에 따라 라는 것을 알고 있습니다. x = 4.0 이므로 dy/dx 를 풀면 2 * 4.0 = 8.0 이겠군요.)


GradientTape.gradient(target, sources)



x = tf.Variable(4.0)


with tf.GradientTape() as tape:

    y = x ** 2

    

    

# dy = 2x * dx

dy_dx = tape.gradient(y, x)

dy_dx.numpy()

[Out] 8.0

 




위의 간단한 예에서는 스칼라(Scalar) 를 사용하였다면, tf.GradientTape() 은 어떤 형태의 텐서에 대해서도 사용할 수 있습니다. GradientTape.gradient(target, sources) 에서 sources 에는 여러개 변수를 리스트나 사전형(Dictionary) 형태로 입력해줄 수 있습니다.  



# tf.GradientTape works as easily on any tensor. 

w = tf.Variable(tf.random.normal((4, 2)), name='w')

b = tf.Variable(tf.zeros(2, dtype=tf.float32), name='b')

x = [[1.0, 2.0, 3.0, 4.0]]


with tf.GradientTape(persistent=True) as tape:

    y = x @ w + b

    loss = tf.reduce_mean(y ** 2)

    

# To get the gradienet of y w.r.t both variables, w and b

[dl_dw, dl_db] = tape.gradient(loss, [w, b])



print('loss:', loss)

print('w:', w)

print('b:', b)

[Out]
loss: tf.Tensor(74.95752, shape=(), dtype=float32)
w: <tf.Variable 'w:0' shape=(4, 2) dtype=float32, numpy=
array([[-0.05742593, -0.06279723],
       [-0.7892129 ,  1.8175325 ],
       [ 3.1122675 ,  0.12920259],
       [ 0.8164586 ,  0.3712036 ]], dtype=float32)>
b: <tf.Variable 'b:0' shape=(2,) dtype=float32, numpy=array([0., 0.], dtype=float32)>

 



Target에 대한 Source의 미분 결과는 Source의 형상(shape)을 따릅니다. 가령, 위의 예에서 손실함수(Loss)에 대한 가중치(w)의 미분 (derivative of loss with respect to weight) 의 결과는 Source에 해당하는 가중치(w)의 형상을 따라서 (4, 2) 가 됩니다. 



print('shape of w:', w.shape)

print('shape of dl_dw:', dl_dw.shape)

[Out]
shape of w: (4, 2)
shape of dl_dw: (4, 2)

 




  3. 시그모이드 함수의 자동미분 시각화 (Auto-diff plot of Sigmoid function)


딥러닝에서 활성화 함수 중의 하나로 사용되는 S 곡선 모양의 시그모이드 함수 (Sigmoid function) 은 아래와 같습니다. 


이 시그모이드 함수를 x 에 대해서 미분하면 아래와 같습니다. 


시그모이드 함수와 시그모이드 함수의 미분 값을 x 가 -10.0에서 10.0 사이의 200개의 관측치에 대해 TensorFlow의 tf.GradientTape() 와 GradientTae=pe.gradient(Sigmoid_func, x) 로 계산해서 하나의 그래프에 겹쳐서 시각화를 해보겠습니다. 



x = tf.linspace(-10.0, 10.0, 200+1)


with tf.GradientTape() as tape:

    tape.watch(x)

    y = tf.nn.sigmoid(x)


# For an element-wise calculation, 

# the gradient of the sum gives the derivative of each element

# with respect to its input-element, since each element is independent.

dy_dx = tape.gradient(y, x)



# plot of y, dy/dx

plt.plot(x, y, 'r-', label='y')

plt.plot(x, dy_dx, 'b--', label='dy/dx')

plt.legend()

_ = plt.xlabel('x')


plt.show()






  4. 테이프가 "볼(저장)" 것을 조정하는 방법 (Colltrolling whtat the tape "watches")


TensorFlow 가 오차역전파 알고리즘으로 학습(training by backpropagation algorithm)할 때 사용하는 자동미분은 '학습 가능한 변수 (trainable tf.Variable)'를 대상으로 합니다. 


따라서 만약에 (1) 변수가 아닌 상수 ('tf.Tensor') 라면 TensorFlow가 안 보는 (not watched), 즉 기록하지 않는 (not recorded) 것이 기본 설정이므로 자동 미분을 할 수 없으며, (2) 변수(tf.Variable) 더라도 '학습 가능하지 않으면 (not trainable)' 자동 미분을 할 수 없습니다



# A trainable variable

x0 = tf.Variable(3.0, name='x0')


# Not trainable

x1 = tf.Variable(3.0, name='x1', trainable=False)


# Not a Variable: A variable + tensor returns a tensor.

x2 = tf.Variable(2.0, name='x2') + 1.0


# Not a variable

x3 = tf.constant(3.0, name='x3')


with tf.GradientTape() as tape:

    y = (x0**2) + (x1**2) + (x2**2)


# Only x0 is a trainable tf.Variable, --> can calculate gradient

grad = tape.gradient(y, [x0, x1, x2, x3]) 



for g in grad:

    print(g)


[Out] 

tf.Tensor(6.0, shape=(), dtype=float32) # <-- dy_dx0, trainable variable None # <-- dy_dx1, not trainable None # <-- dy_dx2, not a variable None # <-- dy_dx3, not a variable

 



y 에 대한 tf.Tensor 인 x의 미분을 구하기 위해서는 GradientTape.watch(x) 를 호출해서 tf.Tensor 인 x를 테이프에 기록해주면 됩니다. 



x = tf.constant(3.0)

with tf.GradientTape() as tape:

    tape.watch(x) # watch 'x' and then record it onto tape

    y = x**2


# dy = 2x * dx

dy_dx = tape.gradient(y, x)

print(dy_dx.numpy())

[Out] 6.0

 



TensorFlow의 테이프에 기록되는 '학습 가능한 변수'는 GradientTape.watched_variables() 메소드로 확인할 수 있습니다. 



[var.name for var in tape.watched_variables()]

[Out] ['Variable:0']

 



반대로, 모든 변수(tf.Variable)를 테이프에 기록하는 TensorFlow의 기본설정을 비활성화(disable)하려면 watch_accessed_variables=False 으로 매개변수를 설정해주면 됩니다. 

아래의 예에서는 x0, x1 의 두 개의 tf.Variable 모두를 watch_accessed_variables=False 로 비활성화해놓고, GradientTape.watch(x1) 으로 x1 변수만 테이프에 기록해서 자동 미분을 해보겠습니다. 



x0 = tf.Variable(0.0)

x1 = tf.Variable(10.0)


with tf.GradientTape(watch_accessed_variables=False) as tape:

    tape.watch(x1)

    y0 = tf.math.sin(x0)

    y1 = tf.nn.softplus(x1)

    y = y0 + y1

    ys = tf.reduce_sum(y)



# dy = 2x * dx

grad = tape.gradient(ys, {'x0': x0, 'x1': x1})


print('dy/dx0:', grad['x0'])

print('dy/dx1:', grad['x1'].numpy())

[Out]

dy/dx0: None dy/dx1: 0.9999546

 




  5. Python 조건절을 사용한 테이프 기록 흐름을 조정

     (Control flow using Python condition statements)


Python 의 조건절을 이용한 분기문을 이용하면 TensorFlow 의 tf.GradientTape() 맥락 아래에서 테이프에 기록하는 연산 과정의 흐름을 조건절에 해당하는 것만 취사선택해서 기록할 수 있습니다. 


아래 예에서는 if x > 0.0 일 때는 result = v0, 그렇지 않을 때는(else) result = v1 ** 2 로 result 의 연산을 다르게 해서 테이프에 기록하고, GradientTape.gradient() 로 자동미분 할 때는 앞서 if else 조건절에서 True에 해당되어서 실제 연산이 일어났던 연산에 미분을 연결해서 계산하라는 코드입니다. 


아래 예에서는 x = tf.constant(1.0) 으로서 if x > 0.0 조건절을 만족(True)하므로 result = v0 연산이 실행되고 뒤에 tape.gradient()에서 자동미분이 되었으며, else 조건절 아래의 연산은 실행되지 않았으므로 뒤에 tape.gradient()의 자동미분은 해당되지 않았습니다 (dv1: None)



x = tf.constant(1.0)


v0 = tf.Variable(2.0)

v1 = tf.Variable(2.0)


with tf.GradientTape(persistent=True) as tape:

    tape.watch(x)


    # Python flow control

    if x > 0.0:

        result = v0

    else:

        result = v1**2 


dv0, dv1 = tape.gradient(result, [v0, v1])


print('dv0:', dv0)

print('dv1:', dv1)

[Out] 
dv0: tf.Tensor(1.0, shape=(), dtype=float32)
dv1: None

 



[ Reference ]

* Automatic Differentiation: https://en.wikipedia.org/wiki/Automatic_differentiation

* TensorFlow Gradient tapes and Automatic differentiation
   : https://www.tensorflow.org/guide/autodiff?hl=en


많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. 



728x90
반응형
Posted by Rfriend
,

지난번 포스팅에서는 TensorFlow 1.x 버전이 지연(게으른) 실행 (Lazy Execution) 인데 반해 TensorFlow 2.x 버전은 즉시 실행 (Eager Execution) 으로 바뀌었다는 점에 대해서 TenforFlow의 대표적인 자료구조인 상수, 변수, Placeholder 별로 간단한 예를 들어서 비교 설명해 보았습니다. 


이번 포스팅에서는 TensorFlow 2.x 버전에서 크게 바뀐 것 중에서 "고수준 API (High-Level API) 로서 Keras API를 채택"하였다는 것에 대해서 소개하겠습니다. 


TensorFlow 2.x 는 텐서 연산은 C++로 고속으로 수행하며, 저수준 API에는 Python wrapper 의 TensorFlow가 있고, 고수준 API로는 Keras 를 사용하는 아키텍처입니다. Keras도 당연히 CPU 뿐만 아니라 GPU 를 사용한 병렬처리 학습이 가능하며, 다양한 애플리케이션/디바이스에 배포도 가능합니다. 



Keras는 Google 에서 딥러닝 연구를 하고 있는 Francois Chollet 이 만든 Deep Learning Library 인데요, TensorFlow 2.x 버전에 고수준 API 로 포함이 되었습니다. (고마워요 Google!!!)


TensorFlow 1.x 버전을 사용할 때는 TensorFlow 설치와 별도로 Native Keras를 따로 설치하고 또 Keras를 별도로 import 해서 사용해야 했습니다. 


하지만 TensorFlow 2.x 버전 부터는 TensorFlow 만 설치하면 되며, Keras는 별도로 설치하지 않아도 됩니다. 또한 Keras API를 사용할 때도 TensorFlow를 Importing 한 후에 tf.keras.models 와 같이 tf.keras 로 시작해서 뒤에 Keras의 메소드를 써주면 됩니다. (아래의 MNIST 분류 예시에서 자세히 소개)


 

TensorFlow 1.x 

TensorFlow 2.x 

 설치 (Install)

 $ pip install tensorflow==1.15

 $ pip install keras

 $ pip install tensorflow==2.3.0 

 (Keras는 별도 설치 필요 없음)

 Importing

 import tensorflow as tf

from keras import layers

 import tensorflow as tf

 from tf.keras import layers




위의 Kreas 로고 이미지는 https://keras.io 정식 홈페이지의 메인 화면에 있는 것인데요, "Simple. Flexible. Powerful." 가 Keras의 모토이며, Keras를 아래처럼 소개하고 있습니다. 


"Keras는 TensorFlow 기계학습 플랫폼 위에서 실행되는 Python 기반의 딥러닝 API 입니다. Keras는 빠른 실험을 가능하게 해주는데 초점을 두고 개발되었습니다. 아이디어에서부터 결과를 얻기까지 가능한 빠르게 할 수 있다는 것은 훌륭한 연구를 하는데 있어 매우 중요합니다" 

* Keras reference: https://keras.io/about/


백문이 불여일견이라고, Keras를 사용하여 MNIST 손글씨 0~9 숫자를 분류하는 매우 간단한 DNN (Deep Neural Network) 모델을 만들어보겠습니다.


DNN Classifier 모델 개발 및 적용 예시는 (1) 데이터 준비, (2) 모델 구축, (3) 모델 컴파일, (4) 모델 학습, (5) 모델 평가, (6) 예측의 절차에 따라서 진행하겠습니다. 




[ MNIST 데이터 손글씨 숫자 분류 DNN 모델 ]



  (1) 데이터 준비 (Preparing the data)


먼저 TensorFlow 를 importing 하고 버전(TensorFlow 2.3.0)을 확인해보겠습니다. 

 


import tensorflow as tf

import numpy as np


print(tf.__version__)

2.3.0




Keras 에는 MNIST 데이터셋이 내장되어 있어서 tf.keras.datasets.mnist.load_data() 메소드로 로딩을 할 수 있습니다. Training set에 6만개, Test set에 1만개의 손글씨 숫자 데이터와 정답지 라벨 데이터가 있습니다. 



(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()


print('shape of x_train:', x_train.shape)

print('shape of y_train:', y_train.shape)

print('shape of x_test:', x_test.shape)

print('shape of y_test:', y_test.shape)

shape of x_train: (60000, 28, 28)
shape of y_train: (60000,)
shape of x_test: (10000, 28, 28)
shape of y_test: (10000,)




 MNIST 데이터셋이 어떻게 생겼는지 확인해보기 위해서 Training set의 0번째 이미지 데이터 배열(28 by 28 array)과 정답지 라벨 ('5'), 그리고 matplotlib 으로 시각화를 해보겠습니다. 



x_train[0]

[Out] array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 18, 18, 18, 126, 136, 175, 26, 166, 255, 247, 127, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 30, 36, 94, 154, 170, 253, 253, 253, 253, 253, 225, 172, 253, 242, 195, 64, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 49, 238, 253, 253, 253, 253, 253, 253, 253, 253, 251, 93, 82, 82, 56, 39, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 18, 219, 253, 253, 253, 253, 253, 198, 182, 247, 241, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 80, 156, 107, 253, 253, 205, 11, 0, 43, 154, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 1, 154, 253, 90, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 139, 253, 190, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 190, 253, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 241, 225, 160, 108, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 81, 240, 253, 253, 119, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 45, 186, 253, 253, 150, 27, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 93, 252, 253, 187, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 249, 253, 249, 64, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 46, 130, 183, 253, 253, 207, 2, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 39, 148, 229, 253, 253, 253, 250, 182, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 114, 221, 253, 253, 253, 253, 201, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 23, 66, 213, 253, 253, 253, 253, 198, 81, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 18, 171, 219, 253, 253, 253, 253, 195, 80, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 55, 172, 226, 253, 253, 253, 253, 244, 133, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 136, 253, 253, 253, 212, 135, 132, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)



y_train[0]

[Out] 5


import matplotlib.pyplot as plt

plt.rcParams['figure.figsize'] = (5, 5)

plt.imshow(x_train[0])

plt.show()




Deep Neural Network 모델의 input으로 넣기 위해 (28 by 28) 2차원 배열 (2D array) 이미지를 (28 * 28 = 784) 의 옆으로 길게 펼친 데이터 형태로 변형(Reshape) 하겠습니다(Flatten() 메소드를 사용해도 됨). 그리고 0~255 사이의 값을 0~1 사이의 실수 값으로 변환하겠습니다.  



# reshape and normalization

x_train = x_train.reshape((60000, 28 * 28)) / 255.0

x_test = x_test.reshape((10000, 28 * 28)) / 255.0





  (2) 모델 구축 (Building the model)


이제 본격적으로 Keras를 사용해보겠습니다. Keras의 핵심 데이터 구조로 모델(Models)층(Layers)있습니다. 모델을 구축할 때는 (a) 순차적으로 층을 쌓아가는 Sequential model 과, (b) 복잡한 구조의 모델을 만들 때 사용하는 Keras functional API 의 두 가지 방법이 있습니다. 


이미지 데이터 분류는 CNN (Convolutional Neural Network) 을 사용하면 효과적인데요, 이번 포스팅에서는 간단하게 Sequential model 을 사용하여 위의 (1)에서 전처리한 Input 이미지 데이터를 받아서, 1개의 완전히 연결된 은닉층 (Fully Connected Hidden Layer) 을 쌓고, 10개의 classes 별 확률을 반환하는 FC(Fully Connected) Output Layer 를 쌓아서 만든 DNN(Deep Neural Network) 모델을 만들어보겠습니다.  (Functional API 는 PyTorch 와 유사한데요, 나중에 별도의 포스팅에서 소개하도록 하겠습니다.)


add() 메소드를 사용하면 마치 레고 블록을 쌓듯이 차곡차곡 순서대로 원하는 층을 쌓아서 딥러닝 모델을 설계할 수 있습니다. (Dense, CNN, RNN, Embedding 등 모두 가능). Dense() 층의 units 매개변수에는 층별 노드 개수를 지정해주며, 활성화 함수는 activation 매개변수에서 지정해줍니다. 은닉층에서는 'Relu' 활성화함수를, Output 층에는 10개 classes에 대한 확률을 반환하므로 'softmax' 활성화함수를 사용하였습니다. 



# Sequential model

model = tf.keras.models.Sequential()


# Stacking layers

model.add(tf.keras.layers.Dense(units=128, activation='relu', input_shape=(28*28,)))

model.add(tf.keras.layers.Dense(units=10, activation='softmax'))




이렇게 층을 쌓아서 만든 DNN (Deep Neural Network) 모델이 어떻게 생겼는지 summary() 함수로 출력해보고, tf.keras.utils.plot_model() 메소드로 시각화해서 확인해보겠습니다. 



model.summary()

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 128)               100480    
_________________________________________________________________
dense_1 (Dense)              (None, 10)                1290      
=================================================================
Total params: 101,770
Trainable params: 101,770
Non-trainable params: 0
_____________________________________


# keras model visualization

tf.keras.utils.plot_model(model, 

                          to_file='model_plot.png', 

                          show_shapes=True)


* 참고로 Keras의 tf.keras.utils.plot_model() 메소드를 사용하려면 사전에 pydot, pygraphviz 라이브러리 설치, Graphviz 설치가 필요합니다. 




  (3) 모델 컴파일 (Compiling the model)


위의 (2)번에서 딥러닝 모델을 구축하였다면, 이제 기계가 이 모델을 이해할 수 있고 학습 절차를 설정할 수 있도록 컴파일(Compile) 해줍니다. 


compile() 메소드에는 딥러닝 학습 시 사용하는 (a) 오차 역전파 경사하강법 시 최적화 알고리즘(optimizer), (b) 손실함수(loss function), 그리고 (c) 성과평가 지표(metrics) 를 설정해줍니다. 



model.compile(optimizer='sgd', 

              loss='sparse_categorical_crossentropy', 

              metrics=['accuracy']

)

 

* 참고로, input data의 다수 클래스에 대한 정답 레이블(ground-truth labels)을 바로 사용하여 손실함수를 계산할 때는 loss='sparse_categorical_crossentropy' 을 사용하며, One-hot encoding 형태로 정답 레이블이 되어있다면 loss='categorical_crossentropy' 를 사용합니다. 



Keras 는 위에서 처럼 optimizer='sgd' 처럼 기본 설정값을 사용해서 간단하게 코딩할수도 있으며, 만약 사용자가 좀더 유연하고 강력하게 설정값을 조정하고 싶다면 아래의 예시처럼 하위클래스(subclassing)을 사용해서 세부 옵션을 지정해줄 수도 있습니다. 



model.compile(

    optimizer=keras.optimizers.SGD(learning_rate=0.01, momentum=0.9, nesterov=True)

    loss=keras.losses.sparse_categorical_crossentropy

)

 




  (4) 모델 학습 (Training the model)


데이터 준비와 모델 구축, 그리고 모델 컴파일이 끝났으므로 이제 6만개의 훈련 데이터셋(Training set) 중에서 80% (4.8만개 데이터) 는 모델 학습(fitting)에, 20%(1.2만개 데이터)는 검증(validation)용으로 사용하여 DNN 모델을 학습해보겠습니다. (참고로, 1만개의 Test set은 모델 학습에서는 사용하지 않으며, 최종 모델에 대해서 제일 마지막에 모델 성능 평가를 위해서만 사용합니다.)


epochs = 5 는 전체 데이터를 5번 반복(iteration)해서 사용해서 학습한다는 의미입니다. 

verbose = 1 은 모델 학습 진행상황 막대를 출력하라는 의미입니다. ('1' 이 default이므로 생략 가능)

(0 = silent, 1 = progress bar, 2 = one line per epoch)

validation_split=0.2 는 검증용 데이터셋을 별도로 만들어놓지 않았을 때 학습용 데이터셋에서 지정한 비율(예: 20%) 만큼을 분할하여 (hyperparameter tuning을 위한) 검증용 데이터셋으로 이용하라는 의미입니다. 

batch_size=32 는 한번에 데이터 32개씩 batch로 가져다가 학습에 사용하라는 의미입니다. 


이번 포스팅은 Keras의 기본 사용법에 대한 개략적인 소개이므로 너무 자세하게는 안들어가겠구요, callbacks는 나중에 별도의 포스팅에서 자세히 설명하겠습니다. 


학습이 진행될수록 (즉, epochs 이 증가할 수록) 검증용 데이터셋에 대한 손실 값(loss value)은 낮아지고 정확도(accuracy)는 올라가고 있네요. 



model.fit(x_train, y_train, 

          epochs=5, 

          verbose=1, 

          validation_split=0.2)


Epoch 1/5
1500/1500 [==============================] - 1s 922us/step - loss: 0.7291 - accuracy: 0.8146 - val_loss: 0.3795 - val_accuracy: 0.8989
Epoch 2/5
1500/1500 [==============================] - 1s 811us/step - loss: 0.3605 - accuracy: 0.9007 - val_loss: 0.3081 - val_accuracy: 0.9145
Epoch 3/5
1500/1500 [==============================] - 1s 785us/step - loss: 0.3061 - accuracy: 0.9140 - val_loss: 0.2763 - val_accuracy: 0.9215
Epoch 4/5
1500/1500 [==============================] - 1s 772us/step - loss: 0.2749 - accuracy: 0.9228 - val_loss: 0.2519 - val_accuracy: 0.9290
Epoch 5/5
1500/1500 [==============================] - 1s 827us/step - loss: 0.2525 - accuracy: 0.9289 - val_loss: 0.2355 - val_accuracy: 0.9339
Out[14]:
<tensorflow.python.keras.callbacks.History at 0x7fc7920a1760>

 




  (5) 모델 평가 (Evaluating the model)


evaluate() 메소드를 사용하여 모델의 손실 값(Loss value) 과 컴파일 단계에서 추가로 설정해준 성능지표 값 (Metrics values) 을 Test set 에 대하여 평가할 수 있습니다. (다시 한번 강조하자면, Test set은 위의 (4)번 학습 단계에서 절대로 사용되어서는 안됩니다!)


Test set에 대한 cross entropy 손실값은 0.234, 정확도(accuracy)는 93.68% 가 나왔네요. (CNN 모델을 이용하고 hyperparameter tuning을 하면 99%까지 정확도를 올릴 수 있습니다.)



model.evaluate(x_test, y_test)

313/313 [==============================] - 0s 801us/step - loss: 0.2342 - accuracy: 0.9368
Out[15]:
[0.23418554663658142, 0.9368000030517578]





  (6) 예측 (Prediction for new data) 


위에서 학습한 DNN 모델을 사용해서 MNIST 이미지 데이터에 대해서 predict() 메소드를 사용하여 예측을 해보겠습니다. (별도의 새로운 데이터가 없으므로 test set 데이터를 사용함)


예측 결과의 0번째 데이터를 살펴보니 '7' 이라고 모델이 예측을 했군요. 



preds = model.predict(x_test, batch_size=128)


# returns an array of probability per classes

preds[0]

array([8.6389220e-05, 3.9117887e-07, 5.6002376e-04, 2.2136024e-03,
       2.7992455e-06, 1.4370076e-04, 5.6979918e-08, 9.9641860e-01,
       2.2985958e-05, 5.5149395e-04], dtype=float32)


# position of max probability

np.argmax(preds[0])

[Out] 7




실제 정답 레이블(ground-truth labels)과 비교해보니 '7' 이 정답이 맞네요. 



y_test[0]

[Out] 7


plt.imshow(x_test[0].reshape(28, 28))

plt.show()




  (7) 모델 저장, 불러오기 (Saving and Loading the model)


딥러닝 모델의 성과 평가 결과 기준치를 충족하여 현장 적용이 가능한 경우라면 모델의 요소와 학습된 가중치 정보를 파일 형태로 저장하고, 배포, 로딩해서 (재)활용할 수 있습니다. 


-  HDF5 파일로 전체 모델/가중치 저장하기


# Save the entire model to a HDF5 file.

model.save('mnist_dnn_model.h5') 




- 저장된 'mnist_dnn_model.h5' 모델/가중치 파일을 'new_model' 이름으로 불러오기


# Recreate the exact same model, including its weights and the optimizer

new_model = tf.keras.models.load_model('mnist_dnn_model.h5')


new_model.summary()

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                (None, 128)               100480    
_________________________________________________________________
dense_1 (Dense)              (None, 10)                1290      
=================================================================
Total params: 101,770
Trainable params: 101,770
Non-trainable params: 0
_________________________________________________________________




TensorFlow 2.x 에서부터 High-level API 로 탑재되어 있는 Keras API 에 대한 간단한 MNIST 분류 DNN 모델 개발과 예측 소개를 마치겠습니다. 


* About Kerashttps://keras.io/about/

* Keras API Reference: https://keras.io/api/

* Save and Load model: https://www.tensorflow.org/tutorials/keras/save_and_load


많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)


728x90
반응형
Posted by Rfriend
,