텐서플로우를 이용한 간단한 다항식 피팅(regression): 텐서플로우 기초 요소 이해

사실상 전통적인 분자동역학 분야에서는 딥러닝을 필요로 하지 않는다. 머신러닝이라 부르든, 그냥 regression이라고 부르든, 아무튼 '주어진 목적함수에 대해 목표치와 예측치 사이의 오차를 최소로 줄여나가는 fitting process에 대해서만 알고 있다면, 누구나 퍼텐셜을 피팅 할 수 있다. 텐서플로우가 주목을 받은 이유는 다양한 deep layer 구조를 쉽게 구현할 수 있도록 코드가 잘 짜여져 있기 때문인데, 그러한 연유로 만약 당신이 potential fitting을 한다면 굳이 텐서플로우를 이용할 필요는 없다.

허나, 나는 이번에 EAM potential을 피팅하는 프로젝트를 진행하면서 텐서플로우를 작업 툴로 선정했다. 그 이유는 여러가지가 있는데, 어쨌든 학문에 몸담으로 사람으로서 세간의 방법론적인 유행에 발은 맞춰놔야 하지 않나, 하는 생각도 들고, 또 앞으로 퍼텐셜의 형태가 종래의 형태와 같이 수식의 형태를 띄지 않고 인공신경망 구조로 학습되어 일종의 예측 프로그램 형태로 발전되지 않을까, 하는 생각도 들었기 때문이다.

이번 포스트는 텐서플로우 공식 홈에 있는 regression 예제 코드를 면밀히 살펴보면서, 텐서플로우의 특징, 사용방법들을 한꺼번에 알아볼 생각이다. 그러면서 나도 텐서플로우와 조금은 친해져 봐야지. 예제 코드는 다음에서 확인할 수 있다[예제코드].

먼저 코드의 맨 윗 단을 보면 다음과 같이 세 개의 모듈이 import 되어 있다.

import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt

'numpy'는 굳이 쓰지 않아도 되는 모듈이지만, 행렬을 많이 사용하는 코드에서 이 모듈을 쓰지 않으면 코딩이 굉장히 귀찮아진다. 여러가지 연산이라든지 행렬 선언, precision 설정 등등에서 numpy를 통한 선언이 훨씬 편하고 유용하므로 numpy를 사용하는 습관을 기르는 것이 좋다. 'matplotlib.pyplot'은 결과를 plotting해주는 툴이다.

plt.ion()
n_observations = 100fig, ax = plt.subplots(1, 1)
xs = np.linspace(-3, 3, n_observations)
ys = np.sin(xs) + np.random.uniform(-0.5, 0.5, n_observations)
ax.scatter(xs, ys)
fig.show()
plt.draw()

다음에는 plt를 활성화하고 피팅에 활용할 dummy data를 만든다. 여기서는 -3 < x < 3인 구간에서 정의된 100개의 점을 사용하는데, 그 점의 형태가 y = sin(x) 함수 갚에서 임의의 perturbation을 주어 흐트려 놓은 형태를 띄게 만들었다.

X = tf.placeholder(tf.float32)
Y = tf.placeholder(tf.float32)

그 다음에는 X, Y라는 두 가지변수를 선언 하는데, 이 대목에서 우리는 텐서플로우의 중요한 특징을 살펴볼 수 있다. 텐서플로우는 constant와 Variable 두 종류의 텐서를 갖고 있다. (각각이 뭔지는 뒤에서 후술.) 그런데, 위를 보면 tf.constant나 tf.Variable 매서드를 사용하지 않고 tf.placholder라는 매서드를 통해 변수를 선언하는 것을 볼 수가 있다.  이게 대체 무엇이고, 왜 필요한 것일까?

placeholder를 직역하면 대략 '자리선점자' 혹은 '자리예약자' 정도가 된다. placeholder는 '내가 지정한 이런 이름과 이런 데이터 형식에 따라 메모리에 변수를 생성할꺼야.'라고 통보하는 일종의 예고장이다. 이렇게 설명하면 우리가 보통 해오던 memory allocation이랑 뭐가 다르냐고 물어올 수도 있겠다. 그런데 차이점이 분명히 존재한다. 이 질문에 답하기 위해서는 텐서플로우의 작동 방식에 대해 이해가 필요해서 텐서플로우의 구성요소들이 어떤 역할을 하고 어떻게 작동하는지를 설멍하기 위해 한가지 예시를 들겠다.

텐서플로우 코드가 연극이라고 치자. '그래프'라는 요소가 있는데 이것은 일종의 시나리오다. 등장인물들이 어떤 행동을 하고 그것이 또 어떻게 다음 내용과 이어지는지 그 흐름을 보여준다. 선언된 변수들은 시나리오 내 등장인물들을 연기하는 배우들이라고 볼수 있다. 코드 내에서 사용되는 무수한 method들은 이야기를 끌어나가는 사건사고들이며 동시에 연극의 흐름을 이어주는 무대장치들이다. 그리고 마지막으로 제일 중요한 세션, 세션은 '연극의 상영'을 의미한다.

좀더 부연설명을 하겠다. 연극을 하려면 시나리오가 나와야하고, 등장인물에 맞는 배우들이 섭외되어야 한다. 그리고 이들이 연기를 할 수 있게 무대장치도 있어야 하겠지. 그런데 이런 것들은 무대 뒤에서 사전에 준비가 된다. 그리고 실제 연극은 무대의 막이 올랐을 때만 진행된다.

텐서플로우도 연극과 같은 방식으로 구동된다. 세션이 열리고 그 위에 사전에 짜놓았던 그래프가 올라가고 변수가 선언돼야 비로소 계산이 돌아간다. 우리가 지금까지 해왔던 여타의 코딩들은 코드가 읽히는 순간부터 이미 세션이 열려 있다고 볼 수 있지만, 텐서플로우는 그런 방식으로 작동하지 않는다.

이러한 특징을 보여주는 대표적인 요소가 바로 위에서 마주친 placeholder이다. 해당 line에서는 아직 세션이 초기화 되지 않으므로, 우리는 placeholder라는 매서드를 이용해서 '나중에 세션이 활성화 되면 이러저러한 변수를 선언한다'고 일러만 두는 것이다. 변수에 실제 값을 입력하는 것은 뒤에 나오겠지만 'feed_dict' 매서드를 이용, dictionary 형태로 공급한다.

Y_pred = tf.Variable(tf.random_normal([1]), name='bias')
for pow_i in range(1, 5):
    W = tf.Variable(tf.random_normal([1]), name='weight_%d' % pow_i)
    Y_pred = tf.add(tf.multiply(tf.pow(X, pow_i), W), Y_pred)

다음 부분에서는 모델에 따라 예측된 값이 저장될 변수를 선언한다. 자세히 보면 일단 변수가 이전과는 다르게 'Variable' 매서드를 통해 선언된 것을 확인할 수가 있다. 왜 이전에는 placeholder를 사용하고 여기서는 Variable 매서드를 사용할까? 두 변수의 값이 어떻게 사용되는 가를 보면 그 이유를 알 수 있다. 이전에 placeholder로 선언된 변수는 그 값을 미리 선언해 둔 'xs'와 'ys' array에서 값을 하나씩 빼와 트레이닝의 데이터로 사용되는 변수이다. 중요한 점은 이 변수들은 오로지 피팅을 위한 재료로 사용될 뿐 그 값이 minimisation 됨에 따라서 업데이트될 필요가 없다는 점이다.

하지만, tf.Variable로 선언된 변수들을 보면, 5차 다항식의 파라메터 값인 'bias' 값과 'weight_1' 부터 'weight_5' 까지의 training variables로서, 모델을 트레이닝하는 과정에서 업데이트 되는 변수들인 것을 확인할 수 있다. 즉, tf.Variable로 선언하는 변수들은 optimizer가 cost를 줄여나갈 때 gradient를 구하는 대상변수가 된다. 내가 처음에 텐서플로우를 사용하면서 가장 황당했던 것이, 세션에서 트레이닝을 시작하면 별다른 트레이닝 타겟 지정 없이도 알아서 변수들을 찾아간다는 점이었는데, 이러한 특징이 바로 Variable 변수 선언 메서드 덕분에 가능한 일이다.

아래쪽의 for문으로 정리된 내용은 5차 까지 올라가는 다항식의 형태를 표현 하기 위한 것이고, 아래에 보면 bias 값부터 1차, 2차, .., 5차 거듭제곱 까지의 값에 각각의 weight를 곱해 더한 결과값을 Y_pred에 저장하는 것을 볼 수 있다.

cost = tf.reduce_sum(tf.pow(Y_pred - Y, 2)) / (n_observations - 1)

윗 줄은 현재 선택한 weight-set에서 예측된 Y 값이 실제값에서 얼마나 벗어나 있는지를 계산하는 error function 혹은 cost function, 혹은 loss function을 정의하는 부분이다.

learning_rate = 0.01
optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost)

이 부분에서는 트레이닝시의 학습률과 최적화에 사용할 모델을 선정하고, 최소화할 loss function을 지정한다. 실제로는 이 예제에 적혀 있는 'GradientDescentOptimizer'  보다 'AdamsOpimizer'를 더 애용한다.

n_epochs = 1000
with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())

    # Fit all training data
    prev_training_cost = 0.0
    for epoch_i in range(n_epochs):
        for (x, y) in zip(xs, ys):
            sess.run(optimizer, feed_dict={X: x, Y: y})

        training_cost = sess.run(cost, feed_dict={X: xs, Y: ys})
        print(training_cost)

        if epoch_i % 100 == 0:
            ax.plot(xs, Y_pred.eval(feed_dict={X: xs}, session=sess), k', alpha=epoch_i / n_epochs)
            fig.show()
            plt.draw()

        # Allow the training to quit if we've reached a minimum
        if np.abs(prev_training_cost - training_cost) < 0.000001:
            break
        prev_training_cost = training_cost

이 부분은 실제로 세션이 초기화되어 피팅이 진행되는 부분이다. 맨 윗줄부터 천천히 살펴보도록 하자. 'n_epochs'는 여기서 계산 횟수의 upper limit이다.

그 바로 아래에는 드디어 세션이 시작된다. 이 때 'global_variables_initializer()' 매서드에 의해 앞서 선언했던 변수들이 비로소 모두 초기화되어 그래프 안에 구현된다.

그 아래를 보면, 각각 변수 X, Y에 xs, ys array로부터의 값을 하나씩 불러와 넣어준 뒤 아까 우리가 선택했던 optimiser를 이용해 weight를 최적화 해 나간다. 그 다음에는 optimisation을 한번 수행한 뒤 그 모델을 이용한 예측값을 계산하고 이를 토대로 loss function의 값을 계산한다. 맨 아래쪽에서는 loss function의 값이 충분이 작을 경우 피팅을 끝내고, 그렇지 않은 경우 피팅 과정을 반복하는 조건문이 있다.

ax.set_ylim([-3, 3])
fig.show()
plt.waitforbuttonpress()

마지막 세줄은 결과를 plotting 하는 코드들이다.

자, 이렇게 간단한 다항식 피팅 코드를 살펴봤다. 내가 간략하게 주요 개념들을 설명하긴 했지만, 텐서플로우의 작동방식을 이해하고 또 직접 코딩을 하려면 더 많이 고민하고 배워야 한다. 예를 들어, 내가 처음 텐서플로우를 접했을 때

sess.run(optimizer, feed_dict={X: x, Y: y})

이런 식의 구동 방식이 나에게는 상당히 당혹스러웠다. 텐서플로우를 빠른 시간에 익히고, 또 잘써먹기 위해서는 결국 텐서플로우가 갖고 있는 수많은 매서드들에 익숙해지는 것이 핵심이다. 어떤 arguments를 받고 어떻게 데이터를 처리해서 output을 주는지, 비슷한 기능을 하는 또다른 매서드로는 어떤 것들이 있는지, 등등 말이다.



Comments

Popular posts from this blog

비리얼(virial)이란 무엇인가?

분자동역학(Molecular Dynamics)에서 Time Integration의 개념.

Reciprocal lattice란 무엇인가?