이 페이지는 Cloud Translation API를 통해 번역되었습니다.
Switch to English

고급 자동 차별화

TensorFlow.org에서보기 Google Colab에서 실행 GitHub에서 소스보기노트북 다운로드

자동 미분 가이드 에는 기울기를 계산하는 데 필요한 모든 것이 포함되어 있습니다. 이 가이드는tf.GradientTape api의 더 깊고 덜 일반적인 기능에 중점을 둡니다.

설정

import tensorflow as tf

import matplotlib as mpl
import matplotlib.pyplot as plt

mpl.rcParams['figure.figsize'] = (8, 6)

그라데이션 기록 제어

자동 미분 가이드 에서 기울기 계산을 구축하는 동안 테이프가 감시하는 변수와 텐서를 제어하는 ​​방법을 확인했습니다.

테이프에는 녹음을 조작하는 방법도 있습니다.

그래디언트 기록을 중지하려면 GradientTape.stop_recording() 을 사용하여 일시적으로 기록을 중단 할 수 있습니다.

모델 중간에서 복잡한 작업을 구별하지 않으려는 경우 오버 헤드를 줄이는 데 유용 할 수 있습니다. 여기에는 메트릭 또는 중간 결과 계산이 포함될 수 있습니다.

x = tf.Variable(2.0)
y = tf.Variable(3.0)

with tf.GradientTape() as t:
  x_sq = x * x
  with t.stop_recording():
    y_sq = y * y
  z = x_sq + y_sq

grad = t.gradient(z, {'x': x, 'y': y})

print('dz/dx:', grad['x'])  # 2*x => 4
print('dz/dy:', grad['y'])
dz/dx: tf.Tensor(4.0, shape=(), dtype=float32)
dz/dy: None

완전히 reset() 시작하려면 reset() . 단순히 그래디언트 테이프 블록을 종료하고 다시 시작하는 것이 일반적으로 읽기 더 쉽지만 테이프 블록을 종료하는 것이 어렵거나 불가능할 때 reset 을 사용할 수 있습니다.

x = tf.Variable(2.0)
y = tf.Variable(3.0)
reset = True

with tf.GradientTape() as t:
  y_sq = y * y
  if reset:
    # Throw out all the tape recorded so far
    t.reset()
  z = x * x + y_sq

grad = t.gradient(z, {'x': x, 'y': y})

print('dz/dx:', grad['x'])  # 2*x => 4
print('dz/dy:', grad['y'])
dz/dx: tf.Tensor(4.0, shape=(), dtype=float32)
dz/dy: None

그라디언트 중지

위의 전역 테이프 컨트롤과 달리 tf.stop_gradient 함수는 훨씬 더 정확합니다. 테이프 자체에 액세스 할 필요없이 그라디언트가 특정 경로를 따라 흐르는 것을 막는 데 사용할 수 있습니다.

x = tf.Variable(2.0)
y = tf.Variable(3.0)

with tf.GradientTape() as t:
  y_sq = y**2
  z = x**2 + tf.stop_gradient(y_sq)

grad = t.gradient(z, {'x': x, 'y': y})

print('dz/dx:', grad['x'])  # 2*x => 4
print('dz/dy:', grad['y'])
dz/dx: tf.Tensor(4.0, shape=(), dtype=float32)
dz/dy: None

사용자 지정 그라디언트

경우에 따라 기본값을 사용하는 대신 그라디언트 계산 방법을 정확하게 제어 할 수 있습니다. 이러한 상황은 다음과 같습니다.

  • 작성중인 새 작업에 대해 정의 된 그라디언트가 없습니다.
  • 기본 계산은 수치 적으로 불안정합니다.
  • 순방향 패스에서 값 비싼 계산을 캐시하려고합니다.
  • 그라디언트를 수정하지 않고 값을 수정하려고합니다 (예 : tf.clip_by_value , tf.math.round ).

새 작업을 작성하려면 tf.RegisterGradient 를 사용하여 직접 설정할 수 있습니다. 자세한 내용은 해당 페이지를 참조하십시오. (그라디언트 레지스트리는 전역이므로주의해서 변경하십시오.)

후자의 세 가지 경우에는 tf.custom_gradient 를 사용할 수 있습니다.

다음은 중간 그라디언트에tf.clip_by_norm 을 적용하는 예입니다.

# Establish an identity operation, but clip during the gradient pass
@tf.custom_gradient
def clip_gradients(y):
  def backward(dy):
    return tf.clip_by_norm(dy, 0.5)
  return y, backward

v = tf.Variable(2.0)
with tf.GradientTape() as t:
  output = clip_gradients(v * v)
print(t.gradient(output, v))  # calls "backward", which clips 4 to 2
tf.Tensor(2.0, shape=(), dtype=float32)

자세한 내용은 tf.custom_gradient 데코레이터를 참조하세요.

여러 테이프

여러 테이프가 원활하게 상호 작용합니다. 예를 들어, 여기에서 각 테이프는 서로 다른 텐서 세트를 감시합니다.

x0 = tf.constant(0.0)
x1 = tf.constant(0.0)

with tf.GradientTape() as tape0, tf.GradientTape() as tape1:
  tape0.watch(x0)
  tape1.watch(x1)

  y0 = tf.math.sin(x0)
  y1 = tf.nn.sigmoid(x1)

  y = y0 + y1

  ys = tf.reduce_sum(y)
tape0.gradient(ys, x0).numpy()   # cos(x) => 1.0
1.0
tape1.gradient(ys, x1).numpy()   # sigmoid(x1)*(1-sigmoid(x1)) => 0.25
0.25

고차 그라디언트

GradientTape 컨텍스트 관리자 내부의 작업은 자동 차별화를 위해 기록됩니다. 해당 컨텍스트에서 그래디언트가 계산되면 그래디언트 계산도 기록됩니다. 결과적으로 똑같은 API가 고차 그라디언트에도 작동합니다. 예를 들면 :

x = tf.Variable(1.0)  # Create a Tensorflow variable initialized to 1.0

with tf.GradientTape() as t2:
  with tf.GradientTape() as t1:
    y = x * x * x

  # Compute the gradient inside the outer `t2` context manager
  # which means the gradient computation is differentiable as well.
  dy_dx = t1.gradient(y, x)
d2y_dx2 = t2.gradient(dy_dx, x)

print('dy_dx:', dy_dx.numpy())  # 3 * x**2 => 3.0
print('d2y_dx2:', d2y_dx2.numpy())  # 6 * x => 6.0
dy_dx: 3.0
d2y_dx2: 6.0

이것이 스칼라 함수의 2 차 도함수를 제공하지만 GradientTape.gradient 는 스칼라의 기울기 만 계산하기 때문에이 패턴은 헤세 행렬을 생성하도록 일반화되지 않습니다. Hessian을 구성하려면 Jacobian 섹션 아래의 Hessian 예제를 참조하십시오.

" GradientTape.gradient 중첩 된 호출"은 다음 예제에서와 같이 그라디언트에서 스칼라를 계산할 때 좋은 패턴입니다. 그러면 결과 스칼라가 두 번째 그라디언트 계산의 소스 역할을합니다.

예 : 입력 그래디언트 정규화

많은 모델이 "적대적인 예"에 취약합니다. 이 기술 모음은 모델의 입력을 수정하여 모델의 출력을 혼동합니다. 가장 간단한 구현 은 입력과 관련하여 출력의 기울기를 따라 한 단계를 수행합니다. "입력 기울기".

적대적 예제에 대한 견고성을 높이는 한 가지 기술은 입력 기울기 의 크기를 최소화하려는 입력 기울기 정규화 입니다. 입력 기울기가 작 으면 출력의 변화도 작아야합니다.

다음은 입력 그라디언트 정규화의 순진한 구현입니다. 구현은 다음과 같습니다.

  1. 내부 테이프를 사용하여 입력에 대한 출력의 기울기를 계산합니다.
  2. 입력 기울기의 크기를 계산합니다.
  3. 모델과 관련하여 해당 크기의 기울기를 계산합니다.
x = tf.random.normal([7, 5])

layer = tf.keras.layers.Dense(10, activation=tf.nn.relu)
with tf.GradientTape() as t2:
  # The inner tape only takes the gradient with respect to the input,
  # not the variables.
  with tf.GradientTape(watch_accessed_variables=False) as t1:
    t1.watch(x)
    y = layer(x)
    out = tf.reduce_sum(layer(x)**2)
  # 1. Calculate the input gradient.
  g1 = t1.gradient(out, x)
  # 2. Calculate the magnitude of the input gradient.
  g1_mag = tf.norm(g1)

# 3. Calculate the gradient of the magnitude with respect to the model.
dg1_mag = t2.gradient(g1_mag, layer.trainable_variables)
[var.shape for var in dg1_mag]
[TensorShape([5, 10]), TensorShape([10])]

Jacobians

이전의 모든 예제는 일부 소스 텐서와 관련하여 스칼라 대상의 기울기를 사용했습니다.

야 코비 행렬 은 벡터 값 함수의 기울기를 나타냅니다. 각 행에는 벡터 요소 중 하나의 그라디언트가 포함됩니다.

GradientTape.jacobian 메서드를 사용하면 야 코비 행렬을 효율적으로 계산할 수 있습니다.

참고 :

  • gradient 처럼 : sources 인수는 텐서 또는 텐서 컨테이너 일 수 있습니다.
  • gradient 와 달리 : target 텐서는 단일 텐서 여야합니다.

스칼라 소스

첫 번째 예로, 스칼라 소스에 대한 벡터 타겟의 야 코비 행렬이 있습니다.

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

with tf.GradientTape() as tape:
  y = tf.nn.sigmoid(x+delta)

dy_dx = tape.jacobian(y, delta)

스칼라에 대해 Jacobian을 취하면 결과는 target 의 모양을 가지며 소스에 대한 각 요소의 기울기를 제공합니다.

print(y.shape)
print(dy_dx.shape)
(201,)
(201,)

plt.plot(x.numpy(), y, label='y')
plt.plot(x.numpy(), dy_dx, label='dy/dx')
plt.legend()
_ = plt.xlabel('x')

png

텐서 소스

입력이 스칼라이든 텐서이든 GradientTape.jacobian 은 대상의 각 요소에 대해 소스의 각 요소의 기울기를 효율적으로 계산합니다.

예를 들어이 레이어의 출력 모양은 (10, 7) .

x = tf.random.normal([7, 5])
layer = tf.keras.layers.Dense(10, activation=tf.nn.relu)

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

y.shape
TensorShape([7, 10])

그리고 레이어의 커널 모양은 (5, 10) .

layer.kernel.shape
TensorShape([5, 10])

커널에 대한 출력의 야 코비 행렬 모양은 함께 연결된 두 모양입니다.

j = tape.jacobian(y, layer.kernel)
j.shape
TensorShape([7, 10, 5, 10])

대상의 차원을 합산하면 GradientTape.gradient 에서 계산 한 합계의 그래디언트가 남습니다.

g = tape.gradient(y, layer.kernel)
print('g.shape:', g.shape)

j_sum = tf.reduce_sum(j, axis=[0, 1])
delta = tf.reduce_max(abs(g - j_sum)).numpy()
assert delta < 1e-3
print('delta:', delta)
g.shape: (5, 10)
delta: 4.7683716e-07

예 : Hessian

tf.GradientTape 는 헤세 행렬을 구성하는 명시적인 방법을 제공하지 않지만 GradientTape.jacobian 메서드를 사용하여 만들 수 있습니다.

x = tf.random.normal([7, 5])
layer1 = tf.keras.layers.Dense(8, activation=tf.nn.relu)
layer2 = tf.keras.layers.Dense(6, activation=tf.nn.relu)

with tf.GradientTape() as t2:
  with tf.GradientTape() as t1:
    x = layer1(x)
    x = layer2(x)
    loss = tf.reduce_mean(x**2)

  g = t1.gradient(loss, layer1.kernel)

h = t2.jacobian(g, layer1.kernel)
print(f'layer.kernel.shape: {layer1.kernel.shape}')
print(f'h.shape: {h.shape}')
layer.kernel.shape: (5, 8)
h.shape: (5, 8, 5, 8)

뉴턴의 방법 단계에이 Hessian을 사용하려면 먼저 축을 행렬로 평평하게하고 그래디언트를 벡터로 평평하게합니다.

n_params = tf.reduce_prod(layer1.kernel.shape)

g_vec = tf.reshape(g, [n_params, 1])
h_mat = tf.reshape(h, [n_params, n_params])

헤세 행렬은 대칭이어야합니다.

def imshow_zero_center(image, **kwargs):
  lim = tf.reduce_max(abs(image))
  plt.imshow(image, vmin=-lim, vmax=lim, cmap='seismic', **kwargs)
  plt.colorbar()
imshow_zero_center(h_mat)

png

Newton의 방법 업데이트 단계는 다음과 같습니다.

eps = 1e-3
eye_eps = tf.eye(h_mat.shape[0])*eps
# X(k+1) = X(k) - (∇²f(X(k)))^-1 @ ∇f(X(k))
# h_mat = ∇²f(X(k))
# g_vec = ∇f(X(k))
update = tf.linalg.solve(h_mat + eye_eps, g_vec)

# Reshape the update and apply it to the variable.
_ = layer1.kernel.assign_sub(tf.reshape(update, layer1.kernel.shape))

이것은 단일 tf.Variable 대해 상대적으로 간단하지만, 이것을 사소하지 않은 모델에 적용하려면 여러 변수에 걸쳐 완전한 헤세 tf.Variable 을 생성하기 위해 신중한 연결 및 분할이 필요합니다.

Batch Jacobian

경우에 따라 각 대상-소스 쌍에 대한 Jacobian이 독립적 인 소스 스택과 관련하여 각 대상 스택의 야 코비 행렬을 사용하려고합니다.

예를 들어, 여기에서 입력 x 는 형태 (batch, ins) 이고 출력 y 는 형태 (batch, outs) 입니다.

x = tf.random.normal([7, 5])

layer1 = tf.keras.layers.Dense(8, activation=tf.nn.elu)
layer2 = tf.keras.layers.Dense(6, activation=tf.nn.elu)

with tf.GradientTape(persistent=True, watch_accessed_variables=False) as tape:
  tape.watch(x)
  y = layer1(x)
  y = layer2(y)

y.shape
TensorShape([7, 6])

의 전체 코비안 y 에 대한 x 의 모양이 (batch, ins, batch, outs) 만 원하는 경우에도 (batch, ins, outs) .

j = tape.jacobian(y, x)
j.shape
TensorShape([7, 6, 7, 5])

스택에있는 각 항목의 기울기가 독립적 인 경우이 텐서의 모든 (batch, batch) 슬라이스는 대각 행렬입니다.

imshow_zero_center(j[:, 0, :, 0])
_ = plt.title('A (batch, batch) slice')

png

def plot_as_patches(j):
  # Reorder axes so the diagonals will each form a contiguous patch.
  j = tf.transpose(j, [1, 0, 3, 2])
  # Pad in between each patch.
  lim = tf.reduce_max(abs(j))
  j = tf.pad(j, [[0, 0], [1, 1], [0, 0], [1, 1]],
             constant_values=-lim)
  # Reshape to form a single image.
  s = j.shape
  j = tf.reshape(j, [s[0]*s[1], s[2]*s[3]])
  imshow_zero_center(j, extent=[-0.5, s[2]-0.5, s[0]-0.5, -0.5])

plot_as_patches(j)
_ = plt.title('All (batch, batch) slices are diagonal')

png

원하는 결과를 얻으려면 중복 batch 차원을 합산하거나 tf.einsum 사용하여 대각선을 tf.einsum 있습니다.

j_sum = tf.reduce_sum(j, axis=2)
print(j_sum.shape)
j_select = tf.einsum('bxby->bxy', j)
print(j_select.shape)
(7, 6, 5)
(7, 6, 5)

처음에 추가 차원없이 계산을 수행하는 것이 훨씬 더 효율적입니다. GradientTape.batch_jacobian 메서드가 정확히 수행합니다.

jb = tape.batch_jacobian(y, x)
jb.shape
WARNING:tensorflow:5 out of the last 5 calls to <function pfor.<locals>.f at 0x7f9a400e8620> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has experimental_relax_shapes=True option that relaxes argument shapes that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/tutorials/customization/performance#python_or_tensor_args and https://www.tensorflow.org/api_docs/python/tf/function for  more details.

TensorShape([7, 6, 5])
error = tf.reduce_max(abs(jb - j_sum))
assert error < 1e-3
print(error.numpy())
0.0

x = tf.random.normal([7, 5])

layer1 = tf.keras.layers.Dense(8, activation=tf.nn.elu)
bn = tf.keras.layers.BatchNormalization()
layer2 = tf.keras.layers.Dense(6, activation=tf.nn.elu)

with tf.GradientTape(persistent=True, watch_accessed_variables=False) as tape:
  tape.watch(x)
  y = layer1(x)
  y = bn(y, training=True)
  y = layer2(y)

j = tape.jacobian(y, x)
print(f'j.shape: {j.shape}')
WARNING:tensorflow:6 out of the last 6 calls to <function pfor.<locals>.f at 0x7f9a401090d0> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has experimental_relax_shapes=True option that relaxes argument shapes that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/tutorials/customization/performance#python_or_tensor_args and https://www.tensorflow.org/api_docs/python/tf/function for  more details.
j.shape: (7, 6, 7, 5)

plot_as_patches(j)

_ = plt.title('These slices are not diagonal')
_ = plt.xlabel("Don't use `batch_jacobian`")

png

이 경우 batch_jacobian 여전히 실행되고 예상 된 모양으로 무언가 를 반환하지만 내용의 의미가 명확하지 않습니다.

jb = tape.batch_jacobian(y, x)
print(f'jb.shape: {jb.shape}')
WARNING:tensorflow:7 out of the last 7 calls to <function pfor.<locals>.f at 0x7f9a4c0637b8> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has experimental_relax_shapes=True option that relaxes argument shapes that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/tutorials/customization/performance#python_or_tensor_args and https://www.tensorflow.org/api_docs/python/tf/function for  more details.
jb.shape: (7, 6, 5)