‘텐서플로 첫걸음’ 2장에서 선형 회귀의 예제를 간단하게 만들어 텐서플로우를 사용해 최적의 모델 파라미터를 찾았습니다. 여기에서 사용한 최적화 기법이 경사하강법(Gradient Descent)입니다. 경사하강법은 제 블로그의 글(https://tensorflow.blog/해커에게-전해들은-머신러닝-1/)에서 뿐만 아니라 온라인에서나 여러 책들에서 손쉽게 찾아 볼 수 있습니다.
경사하강법을 간단히 요약하면 손실 함수loss function 또는 비용 함수cost function라 불리는 목적 함수를 정의하고 이 함수의 값이 최소화되는 파라미터를 찾는 방법입니다. 함수의 최소값을 찾기 위해 현재 위치(보통 임의의 위치)에서 시작해서 기울기를 따라 조금씩 더 낮은 위치로 내려갑니다. 손실 함수가 뉴럴 네트워크의 여러개의 레이어를 포함하고 있을 때는 각 레이어의 가중치에 대해서 편미분한 값을 다음 레이어로 전달하는 방식으로 효율적인 계산을 꾀합니다. 이런 기법은 미분의 체인룰을 이용한 것으로 하나의 손실 함수 미분을 여러개의 미분으로 나누어 계산할 수 있습니다. 그런데 텐서플로우에서는 이런 과정을 GradientDescentOptimizer
클래스를 사용하여 손쉽게 처리했습니다. 이 클래스가 어떤 일을 하는 것일까요? 이 글에서 간단하게나마 경사하강법을 직접 텐서플로우 코드로 구성해 보도록 하겠습니다.
텐서플로우에서 미분값-보통 이를 그냥 그래디언트라고 부릅니다-을 계산하는 함수가 tf.gradients
입니다. tf.gradients
는 두개의 매개변수를 받습니다. 첫 번째 매개변수는 편미분을 하려는 대상 텐서이고 두 번째 매개변수는 편미분 변수에 해당하는 텐서입니다.
tf.graidents(y, x)
~
아주 간단한 덧셈 식을 만들어 보겠습니다. 각각 1과 2의 값을 가지는 상수 텐서를 만들고 이를 더하는 식입니다.
x = tf.constant(1.0) b = tf.constant(2.0) y = x + b
1차식의 미분은 변수의 계수가 되므로 y
를 x
나 b
로 미분한 값은 그냥 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 = 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]
이 값이 정확한지 어떻게 확인해 볼 수 있을까요? 위에서 시그모이드 함수의 미분 공식을 유도해 보았습니다. 이므로
y
값만 알고 있으면 이 식을 직접 계산해 볼 수 있습니다. 위에서 정의된 식으로 부터 y
는 4이고, 이므로,
sz = 1/(1+np.exp(-4.0)) sz * (1 - sz)
0.017662706213291107
조금 다르지만 거의 동일합니다. 그럼 이 번에는 x
에 대한 시그모이드의 편미분은 어떻게 될까요?
sess.run(tf.gradients(s, x))
[0.035325468]
이 계산이 맞는지 확인하려면 처음에 언급했던 미분의 체인룰을 사용하면 됩니다. x
에 대한 시그모이드(s
)의 편미분을 체인룰을 이용해 분할해 보면 다음과 같습니다.
여기에서 는
tf.gradients(s, y)
이고 는
tf.gradients(y, x)
입니다. 이 두 값은 앞에서 모두 구했으므로 곱해 주기만 하면 되네요. 입니다. 이렇게 시그모이드의 편미분을 직접 확인해 볼 수 있습니다.
마지막으로 b
에 대한 시그모이드 함수의 그래디언트를 확인해 보겠습니다. 는 같고
는 1임을 이미 알고 있기 때문에
x
에 대한 그래디언트의 절반이 될 것임을 짐작할 수 있습니다.
sess.run(tf.gradients(s, b))
[0.017662734]
그럼 이번에는 렐루(ReLU) 함수에 대한 편미분을 알아 보겠습니다. 렐루 함수는 아래 그림과 같이 0 이상일 때는 입력값을 그대로 전달하고 0 이하일 때는 0으로 만듭니다.
렐루 함수를 r
이라 하고, y
값이 0보다 클 때는 r = y
이므로 미분값 이되고
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')
책의 예제에서는 경사하강법으로 손실 함수를 최소화하는 모델 파라미터를 찾기 위해 텐서플로우의 GradientDescentOptimizer
를 사용했습니다. 여기서는 대신 tf.griedents
를 사용해 보겠습니다.
optimizer = tf.train.GradientDescentOptimizer(0.5) train = optimizer.minimize(loss)
이 코드가 하는 일은 손실함수 모델 파라메타 W
, b
에 대해 손실 함수 loss
의 미분을 구하여 각각 W
와 b
에 업데이트 하는 것입니다. 동일한 작업을 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
에 두 번째 매개변수로 여러개의 텐서를 리스트로 넘겨서 한번에 여러개의 그래디언트를 구할 수 있습니다. W
와 b
는 변수이기 때문에 직접 값을 대입할 수 없고 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)
사실 GradientDescentOptimizer
를 비롯하여 모든 최적화 클래스들이 tf.gradients
함수를 사용합니다.
이 글에서 경사하강법과 체인룰을 직접 텐서플로우 코드로 적용해 보았습니다. tf.gradients
함수를 사용하면 최적화 클래스를 사용하는 것보다 조금 더 직관적이며 이론과 코드를 연결하여 이해하기 좋습니다. 하지만 모든 알고리즘을 tf.gradients
로 만드는 것은 아주 힘든 일입니다. ^^
이 글에 있는 코드는 깃허브에 있는 주피터 노트북으로 참고하세요.