경사하강법: tf.gradients

‘텐서플로 첫걸음’ 2장에서 선형 회귀의 예제를 간단하게 만들어 텐서플로우를 사용해 최적의 모델 파라미터를 찾았습니다. 여기에서 사용한 최적화 기법이 경사하강법(Gradient Descent)입니다. 경사하강법은 제 블로그의 글(https://tensorflow.blog/해커에게-전해들은-머신러닝-1/)에서 뿐만 아니라 온라인에서나 여러 책들에서 손쉽게 찾아 볼 수 있습니다.

경사하강법을 간단히 요약하면 손실 함수loss function 또는 비용 함수cost function라 불리는 목적 함수를 정의하고 이 함수의 값이 최소화되는 파라미터를 찾는 방법입니다. 함수의 최소값을 찾기 위해 현재 위치(보통 임의의 위치)에서 시작해서 기울기를 따라 조금씩 더 낮은 위치로 내려갑니다. 손실 함수가 뉴럴 네트워크의 여러개의 레이어를 포함하고 있을 때는 각 레이어의 가중치에 대해서 편미분한 값을 다음 레이어로 전달하는 방식으로 효율적인 계산을 꾀합니다. 이런 기법은 미분의 체인룰을 이용한 것으로 하나의 손실 함수 미분을 여러개의 미분으로 나누어 계산할 수 있습니다. 그런데 텐서플로우에서는 이런 과정을 GradientDescentOptimizer 클래스를 사용하여 손쉽게 처리했습니다. 이 클래스가 어떤 일을 하는 것일까요? 이 글에서 간단하게나마 경사하강법을 직접 텐서플로우 코드로 구성해 보도록 하겠습니다.

텐서플로우에서 미분값-보통 이를 그냥 그래디언트라고 부릅니다-을 계산하는 함수가 tf.gradients 입니다. tf.gradients는 두개의 매개변수를 받습니다. 첫 번째 매개변수는 편미분을 하려는 대상 텐서이고 두 번째 매개변수는 편미분 변수에 해당하는 텐서입니다.

tf.graidents(y, x) ~ \dfrac{\partial y}{\partial x}

아주 간단한 덧셈 식을 만들어 보겠습니다. 각각 1과 2의 값을 가지는 상수 텐서를 만들고 이를 더하는 식입니다.

x = tf.constant(1.0)
b = tf.constant(2.0)
y = x + b

1차식의 미분은 변수의 계수가 되므로 yxb로 미분한 값은 그냥 1이 됩니다. tf.gradients를 사용해 정말 그런지 확인해 보겠습니다.

sess.run(tf.gradients(y, x))

[1.0]

sess.run(tf.gradients(y, b))

[1.0]

이는 당연한 결과이며 우리가 학창시절에 배웠던 것과 동일합니다. 이번에는 변수 x의 계수를 바꾸어 결과의 차이를 확인해 보겠습니다.

y = 2 * x + b
sess.run(tf.gradients(y, x))

[2.0]

앞서 예로부터 미리 짐작할 수 있듯이 x에 대해 편미분한 결과는 상수항으로 간주된 b항이 제외되고 x의 계수값 2만 남았습니다.

뉴럴 네트워크에서 많이 사용되는 시그모이드 함수의 그래디언트는 어떻게 될까요? 시그모이드 함수의 미분 정의를 잠시 살펴 보면 다음과 같습니다(이 유도 과정을 외우거나 할 필요는 전혀 없습니다).

s = \dfrac{1}{1+e^{-z}} 이라 할 때,

\dfrac{\partial s}{\partial z} = \dfrac{\partial}{\partial z}\left(\dfrac{1}{1+e^{-z}}\right) = \dfrac{\partial}{\partial z}(1+e^{-z})^{-1} = -(1+e^{-z})^{-2}\dfrac{\partial e^{-z}}{\partial z} \\ \\ = -(1+e^{-z})^{-2}(-e^{-z}) = (1+e^{-z})^{-2}e^{-z} = \dfrac{e^{-z}}{(1+e^{-z})^2} \\ \\ = \dfrac{1}{1+e^{-z}} \cdot \dfrac{e^{-z}}{1+e^{-z}} = \dfrac{1}{1+e^{-z}} \cdot \dfrac{e^{-z}+1-1}{1+e^{-z}} \\ \\ = \dfrac{1}{1+e^{-z}} \cdot \left(1-\dfrac{1}{1+e^{-z}}\right) = s(1-s)

즉, s = 0.1 이면 미분값은 0.1*(1-0.1) = 0.09가 됩니다. 이제 시그모이드 함수의 미분 공식을 알게되었으니 텐서플로우로 계산을 확인해 보겠습니다.

s = tf.sigmoid(y)

위에서 만든 1차식을 텐서플로우의 tf.sigmoid 함수에 적용해 연산 노드 s 를 만들었습니다. 이제 y에 대한 이 시그모이드 연산 노드의 그래디언트를 계산해 보겠습니다.

sess.run(tf.gradients(s, y))

[0.017662734]

이 값이 정확한지 어떻게 확인해 볼 수 있을까요? 위에서 시그모이드 함수의 미분 공식을 유도해 보았습니다. \dfrac{\partial s}{\partial y} = s(1-s) 이므로 y 값만 알고 있으면 이 식을 직접 계산해 볼 수 있습니다. 위에서 정의된 식으로 부터 y는 4이고, s = \dfrac{1}{1+e^{-y}} 이므로,

sz = 1/(1+np.exp(-4.0))
sz * (1 - sz)

0.017662706213291107

조금 다르지만 거의 동일합니다. 그럼 이 번에는 x에 대한 시그모이드의 편미분은 어떻게 될까요?

sess.run(tf.gradients(s, x))

[0.035325468]

이 계산이 맞는지 확인하려면 처음에 언급했던 미분의 체인룰을 사용하면 됩니다. x에 대한 시그모이드(s)의 편미분을 체인룰을 이용해 분할해 보면 다음과 같습니다.

\dfrac{\partial s}{\partial x} = \dfrac{\partial s}{\partial y}\dfrac{\partial y}{\partial x}

여기에서 \dfrac{\partial s}{\partial y}tf.gradients(s, y)이고 \dfrac{\partial y}{\partial x}tf.gradients(y, x)입니다. 이 두 값은 앞에서 모두 구했으므로 곱해 주기만 하면 되네요. \dfrac{\partial s}{\partial y}\dfrac{\partial y}{\partial x} = 0.017662734 \times 2.0 = 0.035325468 입니다. 이렇게 시그모이드의 편미분을 직접 확인해 볼 수 있습니다.

마지막으로 b에 대한 시그모이드 함수의 그래디언트를 확인해 보겠습니다. \dfrac{\partial s}{\partial y}는 같고 \dfrac{\partial y}{\partial b}는 1임을 이미 알고 있기 때문에 x에 대한 그래디언트의 절반이 될 것임을 짐작할 수 있습니다.

sess.run(tf.gradients(s, b))

[0.017662734]

그럼 이번에는 렐루(ReLU) 함수에 대한 편미분을 알아 보겠습니다. 렐루 함수는 아래 그림과 같이 0 이상일 때는 입력값을 그대로 전달하고 0 이하일 때는 0으로 만듭니다.

relu

렐루 함수를 r이라 하고, y 값이 0보다 클 때는 r = y 이므로 미분값 \dfrac{\partial r}{\partial y} = 1이되고 y 값이 0보다 작을 때는 r = 0이므로 미분도 0입니다. 그럼 텐서플로우로 확인해 보겠습니다.

r = tf.nn.relu(y)
sess.run(tf.gradients(r, x))

[2.0]

렐루 연산 노드 x에 대한 r의 미분값은 x값 그대로 2가 리턴되었습니다. 이번에는 의도적으로 y 값을 음수로 만들어 렐루 함수를 통과시켜 보겠습니다.

r = tf.nn.relu(y - 5)
sess.run(tf.gradients(r, x))

0

예상대로 0이 리턴되었네요.

이제 tf.gradients의 작동 방식에 대해 어느 정도 감히 잡혔으니 조금 더 실전같은 예제를 만들어 보겠습니다. ‘텐서플로 첫걸음’ 2장에 있는 회귀 분석 예제는 데이터셋을 인위적으로 만들어 놓고 이에 근사하는 직선을 찾는 문제입니다. 여기서도 똑같은 문제를 재현해 보겠습니다. 문제 자체는 큰 의미가 없지만 tf.gradients의 사용법을 확인하는 데 중점을 두겠습니다.

먼저 1000개의 데이터를 0.1x + 3의 직선을 따라 조금 랜덤하게 분포하도록 만들고 그래프를 그려 보겠습니다.

num_points = 1000
x_data = []
y_data = []
for i in range(num_points):
    x1 = np.random.normal(0.0, 0.55)
    y1 = x1 * 0.1 + 0.3 + np.random.normal(0.0, 0.03)
    x_data.append(x1)
    y_data.append(y1)
plt.plot(x_data, y_data, 'ro')

gradient-1

책의 예제에서는 경사하강법으로 손실 함수를 최소화하는 모델 파라미터를 찾기 위해 텐서플로우의 GradientDescentOptimizer를 사용했습니다. 여기서는 대신 tf.griedents를 사용해 보겠습니다.

optimizer = tf.train.GradientDescentOptimizer(0.5)
train = optimizer.minimize(loss)

이 코드가 하는 일은 손실함수 모델 파라메타 W, b에 대해 손실 함수 loss의 미분을 구하여 각각 Wb에 업데이트 하는 것입니다. 동일한 작업을 tf.gradients로 바꾸면 다음과 같습니다.

dW, db = tf.gradients(loss, [W, b])
update_W = tf.assign(W, W - 0.5 * dW)
update_b = tf.assign(b, b - 0.5 * db)

tf.gradients에 두 번째 매개변수로 여러개의 텐서를 리스트로 넘겨서 한번에 여러개의 그래디언트를 구할 수 있습니다. Wb는 변수이기 때문에 직접 값을 대입할 수 없고 tf.assign 함수를 사용합니다. 텐서플로우의 변수와 텐서에 대해서는 ‘TF의 텐서와 상수, 변수, 플레이스홀더‘를 참고해 주세요.

이제 학습을 할 차례입니다. 앞에서 만든 그래프와 혼돈을 피하기 위해 구분하여 계산 그래프를 만들 겠습니다.

g1 = tf.Graph() 
with g1.as_default():
    W = tf.Variable(tf.random_uniform([1], -1.0, 1.0)) 
    b = tf.Variable(tf.zeros([1])) 
    y = W * x_data + b 
    loss = tf.reduce_mean(tf.square(y - y_data))
    dW, db = tf.gradients(loss, [W, b]) 
    update_W = tf.assign(W, W - 0.5 * dW) 
    update_b = tf.assign(b, b - 0.5 * db)

모델을 훈련시킨다는 것은 데이터 이용해 오차를 구하고 오차에 대한 그래디언트를 계산해서 모델 파라미터를 업데이트 하는 것입니다. 텐서플로우로 실행할 작업은 모델 파라미터를 업데이트 하는 update_W와 update_b 텐서를 실행하는 일입니다.

with tf.Session(graph=g1) as sess:
    sess.run(tf.global_variables_initializer())
    for step in range(20):
    sess.run([update_W, update_b])
    Wp, bp = sess.run([W, b])
    # 산포도 그리기
    plt.plot(x_data, y_data, 'ro')
    # 직선 그리기
    plt.plot(x_data, Wp * x_data + bp)

gradient-2

사실 GradientDescentOptimizer를 비롯하여 모든 최적화 클래스들이 tf.gradients 함수를 사용합니다.

이 글에서 경사하강법과 체인룰을 직접 텐서플로우 코드로 적용해 보았습니다. tf.gradients 함수를 사용하면 최적화 클래스를 사용하는 것보다 조금 더 직관적이며 이론과 코드를 연결하여 이해하기 좋습니다. 하지만 모든 알고리즘을 tf.gradients로 만드는 것은 아주 힘든 일입니다. ^^

이 글에 있는 코드는 깃허브에 있는 주피터 노트북으로 참고하세요.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중

This site uses Akismet to reduce spam. Learn how your comment data is processed.