
자동 미분(automatic differentiation)은 머신러닝 및 최적화에서 중요한 기술로, 함수의 도함수(미분)를 자동으로 계산하는 방법을 말한다. 이는 모델의 매개변수에 대한 손실 함수의 기울기(gradient)를 효율적으로 계산하여 최적화 알고리즘(예: 경사 하강법)을 사용하여 모델을 학습시킬 때 필수적이다. 먼저 필요한 모듈(텐서플로우)을 불러온다.
import tensorflow as tf
GradientTape는 TensorFlow에서 제공하는 자동 미분(automatic differentiation) 기능을 활용하기 위한 API이다. 이 API는 주로 머신러닝 모델의 최적화 과정에서 사용되며, 특히 경사 하강법(gradient descent)과 같은 최적화 알고리즘에서 모델 매개변수에 대한 손실 함수의 그래디언트(기울기)를 계산하는 데에 쓰인다.
tf.GradientTape는 컨텍스트 안에서 실행된 모든 연산을 테이프에 기록한다. 그 다음, 텐서플로우는 reverse mode differentiation을 사용해 테이프에 기록된 연산의 그래디언트를 계산한다.
x = tf.ones((2, 2))
with tf.GradientTape() as t:
t.watch(x)
y = tf.reduce_sum(x)
z = tf.multiply(y, y)
print("y : ",y)
print("z : ",z)
# 입력 텐서 x에 대한 z의 도함수
dz_dx = t.gradient(z, x)
for i in [0, 1]:
for j in [0, 1]:
# assert 뒤의 조건이 True가 아니면 AssertError가 발생합니다.
assert dz_dx[i][j].numpy() == 8.0
print("dz_dx : ",dz_dx)
텐서 생성 및 GradientTape 설정
x = tf.ones((2, 2)): 모든 요소가 1인 2x2 행렬을 생성한다.tf.GradientTape()를 사용하여 t라는 GradientTape 객체를 생성한다. GradientTape 객체는 범위 내에서 실행된 모든 연산을 기록한다.t.watch(x): x를 GradientTape에 대상으로 지정한다. 기본적으로 TensorFlow는 변수와 텐서를 자동으로 추적하지만, 명시적으로 지정할 수도 있다.y = tf.reduce_sum(x): x의 모든 요소의 합을 계산하여 y에 저장한다. 따라서 y는 4.0 (2x2 행렬의 모든 요소가 1이므로).z = tf.multiply(y, y): y의 제곱을 계산하여 z에 저장한다. 따라서 z는 16.0 (y가 4.0이므로 4.0의 제곱은 16.0).결과 출력
tf.reduce_sum(x)의 결과로 계산된 스칼라 텐서이고,tf.multiply(y, y)의 결과로 계산된 스칼라 텐서이다.도함수 계산
t.gradient(z, x): z를 x에 대해 미분한 값을 계산한다. 즉, dz/dx를 구한다.도함수 확인
dz_dx[i][j].numpy() == 8.0 조건을 만족하는지 확인한다. 위 코드에서 z = y^2 이므로 dz/dx = 2y dy/dx가 되고, y는 tf.reduce_sum(x)이므로 dy/dx = 1이 되어 dz/dx = 2 * y = 8.0이 된다.도함수 출력
dz_dx는 x에 대한 z의 도함수로, x와 동일한 크기의 텐서이다.
실행 결과
y : tf.Tensor(4.0, shape=(), dtype=float32)
z : tf.Tensor(16.0, shape=(), dtype=float32)
dz_dx : tf.Tensor(
[[8. 8.]
[8. 8.]], shape=(2, 2), dtype=float32)
또, tf.GradientTape 컨텍스트 안에서 계산된 중간값에 대한 그래디언트도 구할 수 있다.
x = tf.ones((2, 2))
with tf.GradientTape() as t:
t.watch(x)
y = tf.reduce_sum(x)
z = tf.multiply(y, y)
print("y : ",y)
print("z : ",z)
# 테이프를 사용하여 중간값 y에 대한 도함수를 계산합니다.
dz_dy = t.gradient(z, y)
assert dz_dy.numpy() == 8.0
print("dz_dy : ",dz_dy)
실행결과
y : tf.Tensor(4.0, shape=(), dtype=float32)
z : tf.Tensor(16.0, shape=(), dtype=float32)
dz_dy : tf.Tensor(8.0, shape=(), dtype=float32)
기본적으로 GradientTape.gradient() 메서드가 호출되면 GradientTape에 포함된 리소스가 해제된다. 동일한 연산에 대해 여러 그래디언트를 계산하려면 persistent(지속성 있는) 그래디언트 테이프를 생성하면 된다. 이 그래디언트 테이프는 gradient() 메서드의 다중 호출을 허용한다. 테이프 객체가 garbage collection(동적으로 할당했던 메모리 영역 중에서 필요없게 된 영역)을 해제하면 리소스가 해제된다. 이에 대한 예시 코드는 다음과 같다.
x = tf.constant(3.0)
with tf.GradientTape(persistent=True) as t:
t.watch(x)
y = x * x
z = y * y
dz_dx = t.gradient(z, x) # 108.0 (4*x^3 at x = 3)
dy_dx = t.gradient(y, x) # 6.0
del t # 테이프에 대한 참조를 삭제합니다.
print("dz_dx : ",dz_dx)
print("dy_dx : ",dy_dx)
실행결과
dz_dx : tf.Tensor(108.0, shape=(), dtype=float32)
dy_dx : tf.Tensor(6.0, shape=(), dtype=float32)
연산이 실행되는 순서대로 테이프에 기록되므로, 파이썬 제어 흐름(예를 들어, if, while, for문 같은)이 자연스럽게 처리된다.
이 코드 예제는 tf.GradientTape를 사용하여 주어진 함수 f(x, y)의 그래디언트를 계산하는 방법을 설명하다. persistent=True 옵션을 사용하지 않았기 때문에 GradientTape 객체는 with 블록이 끝나면 해제된다.
def f(x, y):
output = 1.0
for i in range(y):
if i > 1 and i < 5:
output = tf.multiply(output, x)
return output
def grad(x, y):
with tf.GradientTape() as t:
t.watch(x)
out = f(x, y)
return t.gradient(out, x)
x = tf.convert_to_tensor(2.0)
print(grad(x, 6).numpy())
print(grad(x, 5).numpy())
print(grad(x, 4).numpy())
실행 결과
12.0
12.0
4.0
이 코드는 TensorFlow의 GradientTape를 사용하여 이계 미분을 계산하는 예제다. 이중 GradientTape 컨텍스트를 사용하여 함수의 1차 미분과 2차 미분을 계산한다.
x = tf.Variable(1.0) # 1.0으로 초기화된 텐서플로 변수를 생성합니다.
with tf.GradientTape() as t:
with tf.GradientTape() as t2:
y = x * x * x
# 't' 컨텍스트 매니저 안의 그래디언트를 계산합니다.
# 이것은 또한 그래디언트 연산 자체도 미분가능하다는 것을 의미합니다.
dy_dx = t2.gradient(y, x)
d2y_dx2 = t.gradient(dy_dx, x)
print(dy_dx.numpy())
print(d2y_dx2.numpy())
실행결과
3.0
6.0
이중 GradientTape 설정 및 연산 기록
여기서는 두 개의 GradientTape 컨텍스트가 중첩되어 있다.
GradientTape (t2) : y = x * x * x 연산을 기록GradientTape (t): 내부 GradientTape에서 계산된 1차 미분 연산을 기록1차 미분 계산
내부 GradientTape t2를 사용하여 y를 x에 대해 미분한 값, 즉 1차 미분 dy_dx를 계산한다.
2차 미분 계산
외부 GradientTape t를 사용하여 dy_dx를 x에 대해 미분한 값, 즉 2차 미분 d2y_dx2를 계산한다.